diff --git a/.github/actions/prepare-build/action.yml b/.github/actions/prepare-build/action.yml new file mode 100644 index 0000000000..ce75b7a57c --- /dev/null +++ b/.github/actions/prepare-build/action.yml @@ -0,0 +1,47 @@ +name: "Prebuilt steps for build" +description: "Reusable steps for multiple jobs" +inputs: + java_ver: + required: true + description: "Java version to install" + ghc_ver: + required: true + description: "GHC version to install" + github_ref: + required: true + description: "Git reference" + os: + required: true + description: "Target OS" + cache_path: + required: false + default: "~/.cabal/store" + description: "Cache path" + cabal_ver: + required: false + default: 3.10.1.0 + description: "GHC version to install" +runs: + using: "composite" + steps: + - name: Setup Haskell + uses: simplex-chat/setup-haskell-action@v2 + with: + ghc-version: ${{ inputs.ghc_ver }} + cabal-version: ${{ inputs.cabal_ver }} + + - name: Setup Java + if: startsWith(inputs.github_ref, 'refs/tags/v') + uses: actions/setup-java@v3 + with: + distribution: 'corretto' + java-version: ${{ inputs.java_ver }} + cache: 'gradle' + + - name: Restore cached build + uses: actions/cache@v4 + with: + path: | + ${{ inputs.cache_path }} + dist-newstyle + key: ${{ inputs.os }}-ghc${{ inputs.ghc_ver }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }} diff --git a/.github/actions/prepare-release/action.yml b/.github/actions/prepare-release/action.yml new file mode 100644 index 0000000000..e0d32bd596 --- /dev/null +++ b/.github/actions/prepare-release/action.yml @@ -0,0 +1,39 @@ +name: "Upload binary and update hash" +description: "Reusable steps for multiple jobs" +inputs: + bin_path: + required: true + description: "Path to binary to upload" + bin_name: + required: true + description: "Name of uploaded binary" + bin_hash: + required: true + description: "Message with SHA to include in release" + github_ref: + required: true + description: "Github reference" + github_token: + required: true + description: "Github token" +runs: + using: "composite" + steps: + - name: Upload file with specific name + if: startsWith(inputs.github_ref, 'refs/tags/v') + uses: simplex-chat/upload-release-action@v2 + with: + repo_token: ${{ inputs.github_token }} + file: ${{ inputs.bin_path }} + asset_name: ${{ inputs.bin_name }} + tag: ${{ inputs.github_ref }} + + - name: Add hash to release notes + if: startsWith(inputs.github_ref, 'refs/tags/v') + uses: simplex-chat/action-gh-release@v2 + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + with: + append_body: true + body: | + ${{ inputs.bin_hash }} diff --git a/.github/actions/swap/action.yml b/.github/actions/swap/action.yml new file mode 100644 index 0000000000..87d670b147 --- /dev/null +++ b/.github/actions/swap/action.yml @@ -0,0 +1,44 @@ +name: 'Set Swap Space' +description: 'Add moar swap' +branding: + icon: 'crop' + color: 'orange' +inputs: + swap-size-gb: + description: 'Swap space to create, in Gigabytes.' + required: false + default: '10' +runs: + using: "composite" + steps: + - name: Swap space report before modification + shell: bash + run: | + echo "Memory and swap:" + free -h + echo + swapon --show + echo + - name: Set Swap + shell: bash + run: | + export SWAP_FILE=$(swapon --show=NAME | tail -n 1) + echo "Swap file: $SWAP_FILE" + if [ -z "$SWAP_FILE" ]; then + SWAP_FILE=/opt/swapfile + else + sudo swapoff $SWAP_FILE + sudo rm $SWAP_FILE + fi + sudo fallocate -l ${{ inputs.swap-size-gb }}G $SWAP_FILE + sudo chmod 600 $SWAP_FILE + sudo mkswap $SWAP_FILE + sudo swapon $SWAP_FILE + - name: Swap space report after modification + shell: bash + run: | + echo "Memory and swap:" + free -h + echo + swapon --show + echo diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index afdb9bea1a..ca1bc79510 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,23 +5,75 @@ on: branches: - master - stable - - users tags: - "v*" - "!*-fdroid" + - "!*-armv7a" pull_request: + paths-ignore: + - "apps/ios" + - "apps/multiplatform" + - "blog" + - "docs" + - "fastlane" + - "images" + - "packages" + - "website" + - "README.md" + - "PRIVACY.md" + +# This workflow uses custom actions (prepare-build and prepare-release) defined in: +# +# .github/actions/ +# ├── prepare-build +# │ └── action.yml +# └── prepare-release +# └── action.yml + +# Important! +# Do not use always(), it makes build unskippable. +# See: https://github.com/actions/runner/issues/1846#issuecomment-1246102753 jobs: - prepare-release: - if: startsWith(github.ref, 'refs/tags/v') + +# ============================= +# Global variables +# ============================= + +# That is the only and less hacky way to setup global variables +# to use in strategy matrix (env:/YAML anchors doesn't work). +# See: https://github.com/orgs/community/discussions/56787#discussioncomment-6041789 +# https://github.com/actions/runner/issues/1182 +# https://stackoverflow.com/a/77549656 + + variables: + runs-on: ubuntu-latest + outputs: + GHC_VER: 9.6.3 + JAVA_VER: 17 + steps: + - name: Dummy job when we have just simple variables + if: false + run: echo + +# ============================= +# Create release +# ============================= + +# Create release, but only if it's triggered by tag push. +# On pull requests/commits push, this job will always complete. + + maybe-release: runs-on: ubuntu-latest steps: - name: Clone project + if: startsWith(github.ref, 'refs/tags/v') uses: actions/checkout@v3 - name: Build changelog id: build_changelog - uses: mikepenz/release-changelog-builder-action@v4 + if: startsWith(github.ref, 'refs/tags/v') + uses: simplex-chat/release-changelog-builder-action@v5 with: configuration: .github/changelog_conf.json failOnError: true @@ -31,7 +83,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create release - uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/v') + uses: simplex-chat/action-gh-release@v2 with: body: ${{ steps.build_changelog.outputs.changelog }} prerelease: true @@ -41,147 +94,295 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - build: - name: build-${{ matrix.os }} - if: always() - needs: prepare-release - runs-on: ${{ matrix.os }} +# ========================= +# Linux Build +# ========================= + + build-linux: + name: "ubuntu-${{ matrix.os }} (CLI,Desktop), GHC: ${{ matrix.ghc }}" + needs: [maybe-release, variables] + runs-on: ubuntu-${{ matrix.os }} strategy: fail-fast: false matrix: include: - - os: ubuntu-20.04 - cache_path: ~/.cabal/store - asset_name: simplex-chat-ubuntu-20_04-x86-64 - desktop_asset_name: simplex-desktop-ubuntu-20_04-x86_64.deb - - os: ubuntu-22.04 - cache_path: ~/.cabal/store - asset_name: simplex-chat-ubuntu-22_04-x86-64 + - os: 22.04 + ghc: "8.10.7" + should_run: ${{ !(github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }} + - os: 22.04 + ghc: ${{ needs.variables.outputs.GHC_VER }} + cli_asset_name: simplex-chat-ubuntu-22_04-x86-64 desktop_asset_name: simplex-desktop-ubuntu-22_04-x86_64.deb - - os: macos-latest - cache_path: ~/.cabal/store - asset_name: simplex-chat-macos-x86-64 - desktop_asset_name: simplex-desktop-macos-x86_64.dmg - - os: windows-latest - cache_path: C:/cabal - asset_name: simplex-chat-windows-x86-64 - desktop_asset_name: simplex-desktop-windows-x86_64.msi + should_run: true + - os: 24.04 + ghc: ${{ needs.variables.outputs.GHC_VER }} + cli_asset_name: simplex-chat-ubuntu-24_04-x86-64 + desktop_asset_name: simplex-desktop-ubuntu-24_04-x86_64.deb + should_run: true steps: - - name: Configure pagefile (Windows) - if: matrix.os == 'windows-latest' - uses: al-cheb/configure-pagefile-action@v1.3 - with: - minimum-size: 16GB - maximum-size: 16GB - disk-root: "C:" - - - name: Clone project + - name: Checkout Code + if: matrix.should_run == true uses: actions/checkout@v3 - - name: Setup Haskell - uses: haskell-actions/setup@v2 + - name: Setup swap + if: matrix.ghc == '8.10.7' && matrix.should_run == true + uses: ./.github/actions/swap with: - ghc-version: "9.6.3" - cabal-version: "3.10.1.0" + swap-size-gb: 30 - - name: Cache dependencies - uses: actions/cache@v3 + # Otherwise we run out of disk space with Docker build + - name: Free disk space + if: matrix.should_run == true + shell: bash + run: ./scripts/ci/linux_util_free_space.sh + + - name: Restore cached build + if: matrix.should_run == true + uses: actions/cache@v4 with: path: | - ${{ matrix.cache_path }} + ~/.cabal/store dist-newstyle - key: ${{ matrix.os }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }} + key: ubuntu-${{ matrix.os }}-ghc${{ matrix.ghc }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }} - # / Unix + - name: Set up Docker Buildx + if: matrix.should_run == true + uses: simplex-chat/docker-setup-buildx-action@v3 - - name: Unix prepare cabal.project.local for Mac - if: matrix.os == 'macos-latest' + - name: Build and cache Docker image + if: matrix.should_run == true + uses: simplex-chat/docker-build-push-action@v6 + with: + context: . + load: true + file: Dockerfile.build + tags: build/${{ matrix.os }}:latest + build-args: | + TAG=${{ matrix.os }} + GHC=${{ matrix.ghc }} + + # Docker needs these flags for AppImage build: + # --device /dev/fuse + # --cap-add SYS_ADMIN + # --security-opt apparmor:unconfined + - name: Start container + if: matrix.should_run == true shell: bash run: | - echo "ignore-project: False" >> cabal.project.local - echo "package direct-sqlcipher" >> cabal.project.local - echo " extra-include-dirs: /usr/local/opt/openssl@1.1/include" >> cabal.project.local - echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local - echo " flags: +openssl" >> cabal.project.local + docker run -t -d \ + --device /dev/fuse \ + --cap-add SYS_ADMIN \ + --security-opt apparmor:unconfined \ + --name builder \ + -v ~/.cabal:/root/.cabal \ + -v /home/runner/work/_temp:/home/runner/work/_temp \ + -v ${{ github.workspace }}:/project \ + build/${{ matrix.os }}:latest - - name: Install AppImage dependencies - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04' - run: sudo apt install -y desktop-file-utils - - - name: Install pkg-config for Mac - if: matrix.os == 'macos-latest' - run: brew install pkg-config - - - name: Unix prepare cabal.project.local for Ubuntu - if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04' + - name: Prepare cabal.project.local + if: matrix.should_run == true shell: bash run: | echo "ignore-project: False" >> cabal.project.local echo "package direct-sqlcipher" >> cabal.project.local echo " flags: +openssl" >> cabal.project.local - - name: Unix build CLI - id: unix_cli_build - if: matrix.os != 'windows-latest' + # chmod/git commands are used to workaround permission issues when cache is restored + - name: Build CLI + if: matrix.should_run == true + shell: docker exec -t builder sh -eu {0} + run: | + chmod -R 777 dist-newstyle ~/.cabal && git config --global --add safe.directory '*' + cabal clean + cabal update + cabal build -j --enable-tests + mkdir -p /out + for i in simplex-chat simplex-chat-test; do + bin=$(find /project/dist-newstyle -name "$i" -type f -executable) + chmod +x "$bin" + mv "$bin" /out/ + done + strip /out/simplex-chat + + - name: Copy tests from container + if: matrix.should_run == true shell: bash run: | - cabal build --enable-tests - path=$(cabal list-bin simplex-chat) - echo "bin_path=$path" >> $GITHUB_OUTPUT - echo "bin_hash=$(echo SHA2-512\(${{ matrix.asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + docker cp builder:/out/simplex-chat-test . - - name: Unix upload CLI binary to release - if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest' - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ steps.unix_cli_build.outputs.bin_path }} - asset_name: ${{ matrix.asset_name }} - tag: ${{ github.ref }} - - - name: Unix update CLI binary hash - if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest' - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - append_body: true - body: | - ${{ steps.unix_cli_build.outputs.bin_hash }} - - - name: Setup Java - if: startsWith(github.ref, 'refs/tags/v') - uses: actions/setup-java@v3 - with: - distribution: 'corretto' - java-version: '17' - cache: 'gradle' - - - name: Linux build desktop - id: linux_desktop_build - if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04') + - name: Copy CLI from container and prepare it + id: linux_cli_prepare + if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true shell: bash + run: | + docker cp builder:/out/simplex-chat ./${{ matrix.cli_asset_name }} + path="${{ github.workspace }}/${{ matrix.cli_asset_name }}" + echo "bin_path=$path" >> $GITHUB_OUTPUT + echo "bin_hash=$(echo SHA2-256\(${{ matrix.cli_asset_name }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + + - name: Upload CLI + if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true + uses: ./.github/actions/prepare-release + with: + bin_path: ${{ steps.linux_cli_prepare.outputs.bin_path }} + bin_name: ${{ matrix.cli_asset_name }} + bin_hash: ${{ steps.linux_cli_prepare.outputs.bin_hash }} + github_ref: ${{ github.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Desktop + if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true + shell: docker exec -t builder sh -eu {0} run: | scripts/desktop/build-lib-linux.sh cd apps/multiplatform ./gradlew packageDeb - path=$(echo $PWD/release/main/deb/simplex_*_amd64.deb) - echo "package_path=$path" >> $GITHUB_OUTPUT - echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - - - name: Linux make AppImage - id: linux_appimage_build - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04' + + - name: Prepare Desktop + id: linux_desktop_build + if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true shell: bash run: | - scripts/desktop/make-appimage-linux.sh - path=$(echo $PWD/apps/multiplatform/release/main/*imple*.AppImage) - echo "appimage_path=$path" >> $GITHUB_OUTPUT - echo "appimage_hash=$(echo SHA2-512\(simplex-desktop-x86_64.AppImage\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + path=$(echo ${{ github.workspace }}/apps/multiplatform/release/main/deb/simplex_*_amd64.deb ) + echo "package_path=$path" >> $GITHUB_OUTPUT + echo "package_hash=$(echo SHA2-256\(${{ matrix.desktop_asset_name }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - - name: Mac build desktop + - name: Upload Desktop + uses: ./.github/actions/prepare-release + if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true + with: + bin_path: ${{ steps.linux_desktop_build.outputs.package_path }} + bin_name: ${{ matrix.desktop_asset_name }} + bin_hash: ${{ steps.linux_desktop_build.outputs.package_hash }} + github_ref: ${{ github.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build AppImage + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == '22.04' && matrix.should_run == true + shell: docker exec -t builder sh -eu {0} + run: | + scripts/desktop/make-appimage-linux.sh + + - name: Prepare AppImage + id: linux_appimage_build + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == '22.04' && matrix.should_run == true + shell: bash + run: | + path=$(echo ${{ github.workspace }}/apps/multiplatform/release/main/*imple*.AppImage) + echo "appimage_path=$path" >> $GITHUB_OUTPUT + echo "appimage_hash=$(echo SHA2-256\(simplex-desktop-x86_64.AppImage\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + + - name: Upload AppImage + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == '22.04' && matrix.should_run == true + uses: ./.github/actions/prepare-release + with: + bin_path: ${{ steps.linux_appimage_build.outputs.appimage_path }} + bin_name: "simplex-desktop-x86_64.AppImage" + bin_hash: ${{ steps.linux_appimage_build.outputs.appimage_hash }} + github_ref: ${{ github.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Fix permissions for cache + if: matrix.should_run == true + shell: bash + run: | + sudo chmod -R 777 dist-newstyle ~/.cabal + sudo chown -R $(id -u):$(id -g) dist-newstyle ~/.cabal + + - name: Run tests + if: matrix.should_run == true + timeout-minutes: 120 + shell: bash + run: | + i=1 + attempts=1 + ${{ (github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }} && attempts=3 + while [ "$i" -le "$attempts" ]; do + if ./simplex-chat-test; then + break + else + echo "Attempt $i failed, retrying..." + i=$((i + 1)) + sleep 1 + fi + done + if [ "$i" -gt "$attempts" ]; then + echo "All "$attempts" attempts failed." + exit 1 + fi + +# ========================= +# MacOS Build +# ========================= + + build-macos: + name: "${{ matrix.os }} (CLI,Desktop), GHC: ${{ matrix.ghc }}" + needs: [maybe-release, variables] + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + ghc: ${{ needs.variables.outputs.GHC_VER }} + cli_asset_name: simplex-chat-macos-aarch64 + desktop_asset_name: simplex-desktop-macos-aarch64.dmg + openssl_dir: "/opt/homebrew/opt" + - os: macos-13 + ghc: ${{ needs.variables.outputs.GHC_VER }} + cli_asset_name: simplex-chat-macos-x86-64 + desktop_asset_name: simplex-desktop-macos-x86_64.dmg + openssl_dir: "/usr/local/opt" + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Prepare build + uses: ./.github/actions/prepare-build + with: + java_ver: ${{ needs.variables.outputs.JAVA_VER }} + ghc_ver: ${{ matrix.ghc }} + os: ${{ matrix.os }} + github_ref: ${{ github.ref }} + + - name: Install OpenSSL + run: brew install openssl@3.0 + + - name: Prepare cabal.project.local + shell: bash + run: | + echo "ignore-project: False" >> cabal.project.local + echo "package simplexmq" >> cabal.project.local + echo " extra-include-dirs: ${{ matrix.opnessl_dir }}/openssl@3.0/include" >> cabal.project.local + echo " extra-lib-dirs: ${{ matrix.openssl_dir}}/openssl@3.0/lib" >> cabal.project.local + echo "" >> cabal.project.local + echo "package direct-sqlcipher" >> cabal.project.local + echo " extra-include-dirs: ${{ matrix.openssl_dir }}/openssl@3.0/include" >> cabal.project.local + echo " extra-lib-dirs: ${{ matrix.openssl_dir }}/openssl@3.0/lib" >> cabal.project.local + echo " flags: +openssl" >> cabal.project.local + + - name: Build CLI + id: mac_cli_build + shell: bash + run: | + cabal build -j --enable-tests + path=$(cabal list-bin simplex-chat) + echo "bin_path=$path" >> $GITHUB_OUTPUT + echo "bin_hash=$(echo SHA2-256\(${{ matrix.cli_asset_name }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + + - name: Upload CLI + if: startsWith(github.ref, 'refs/tags/v') + uses: ./.github/actions/prepare-release + with: + bin_path: ${{ steps.mac_cli_build.outputs.bin_path }} + bin_name: ${{ matrix.cli_asset_name }} + bin_hash: ${{ steps.mac_cli_build.outputs.bin_hash }} + github_ref: ${{ github.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Desktop id: mac_desktop_build - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest' + if: startsWith(github.ref, 'refs/tags/v') shell: bash env: APPLE_SIMPLEX_SIGNING_KEYCHAIN: ${{ secrets.APPLE_SIMPLEX_SIGNING_KEYCHAIN }} @@ -191,79 +392,77 @@ jobs: scripts/ci/build-desktop-mac.sh path=$(echo $PWD/apps/multiplatform/release/main/dmg/SimpleX-*.dmg) echo "package_path=$path" >> $GITHUB_OUTPUT - echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + echo "package_hash=$(echo SHA2-256\(${{ matrix.desktop_asset_name }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - - name: Linux upload desktop package to release - if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04') - uses: svenstaro/upload-release-action@v2 + - name: Upload Desktop + if: startsWith(github.ref, 'refs/tags/v') + uses: ./.github/actions/prepare-release with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ steps.linux_desktop_build.outputs.package_path }} - asset_name: ${{ matrix.desktop_asset_name }} - tag: ${{ github.ref }} + bin_path: ${{ steps.mac_desktop_build.outputs.package_path }} + bin_name: ${{ matrix.desktop_asset_name }} + bin_hash: ${{ steps.mac_desktop_build.outputs.package_hash }} + github_ref: ${{ github.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} - - name: Linux update desktop package hash - if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04') - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - append_body: true - body: | - ${{ steps.linux_desktop_build.outputs.package_hash }} - - - name: Linux upload AppImage to release - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04' - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ steps.linux_appimage_build.outputs.appimage_path }} - asset_name: simplex-desktop-x86_64.AppImage - tag: ${{ github.ref }} - - - name: Linux update AppImage hash - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04' - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - append_body: true - body: | - ${{ steps.linux_appimage_build.outputs.appimage_hash }} - - - name: Mac upload desktop package to release - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest' - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ steps.mac_desktop_build.outputs.package_path }} - asset_name: ${{ matrix.desktop_asset_name }} - tag: ${{ github.ref }} - - - name: Mac update desktop package hash - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest' - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - append_body: true - body: | - ${{ steps.mac_desktop_build.outputs.package_hash }} - - - name: Unix test - if: matrix.os != 'windows-latest' - timeout-minutes: 30 + - name: Run tests + timeout-minutes: 120 shell: bash - run: cabal test --test-show-details=direct + run: | + i=1 + attempts=1 + ${{ (github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }} && attempts=3 + while [ "$i" -le "$attempts" ]; do + if cabal test --test-show-details=direct; then + break + else + echo "Attempt $i failed, retrying..." + i=$((i + 1)) + sleep 1 + fi + done + if [ "$i" -gt "$attempts" ]; then + echo "All "$attempts" attempts failed." + exit 1 + fi - # Unix / +# ========================= +# Windows Build +# ========================= - # / Windows - # rm -rf dist-newstyle/src/direct-sq* is here because of the bug in cabal's dependency which prevents second build from finishing + build-windows: + name: "${{ matrix.os }} (CLI,Desktop), GHC: ${{ matrix.ghc }}" + needs: [maybe-release, variables] + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + ghc: ${{ needs.variables.outputs.GHC_VER }} + cli_asset_name: simplex-chat-windows-x86-64 + desktop_asset_name: simplex-desktop-windows-x86_64.msi + steps: + - name: Checkout Code + uses: actions/checkout@v3 + - name: Prepare build + uses: ./.github/actions/prepare-build + with: + java_ver: ${{ needs.variables.outputs.JAVA_VER }} + ghc_ver: ${{ matrix.ghc }} + os: ${{ matrix.os }} + cache_path: "C:/cabal" + github_ref: ${{ github.ref }} + + - name: Configure pagefile (Windows) + uses: simplex-chat/configure-pagefile-action@v1.4 + with: + minimum-size: 16GB + maximum-size: 16GB + disk-root: "C:" + - name: 'Setup MSYS2' - if: matrix.os == 'windows-latest' - uses: msys2/setup-msys2@v2 + uses: simplex-chat/setup-msys2@v2 with: msystem: ucrt64 update: true @@ -275,15 +474,14 @@ jobs: toolchain:p cmake:p - - - name: Windows build - id: windows_build - if: matrix.os == 'windows-latest' + # rm -rf dist-newstyle/src/direct-sq* is here because of the bug in cabal's dependency which prevents second build from finishing + - name: Build CLI + id: windows_cli_build shell: msys2 {0} run: | - export PATH=$PATH:/c/ghcup/bin + export PATH=$PATH:/c/ghcup/bin:$(echo /c/tools/ghc-*/bin || echo) scripts/desktop/prepare-openssl-windows.sh - openssl_windows_style_path=$(echo `pwd`/dist-newstyle/openssl-1.1.1w | sed 's#/\([a-zA-Z]\)#\1:#' | sed 's#/#\\#g') + openssl_windows_style_path=$(echo `pwd`/dist-newstyle/openssl-3.0.15 | sed 's#/\([a-zA-Z]\)#\1:#' | sed 's#/#\\#g') rm cabal.project.local 2>/dev/null || true echo "ignore-project: False" >> cabal.project.local echo "package direct-sqlcipher" >> cabal.project.local @@ -293,61 +491,42 @@ jobs: rm -rf dist-newstyle/src/direct-sq* sed -i "s/, unix /--, unix /" simplex-chat.cabal - cabal build --enable-tests + cabal build -j --enable-tests rm -rf dist-newstyle/src/direct-sq* path=$(cabal list-bin simplex-chat | tail -n 1) echo "bin_path=$path" >> $GITHUB_OUTPUT - echo "bin_hash=$(echo SHA2-512\(${{ matrix.asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + echo "bin_hash=$(echo SHA2-256\(${{ matrix.cli_asset_name }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - - name: Windows upload CLI binary to release - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest' - uses: svenstaro/upload-release-action@v2 + - name: Upload CLI + if: startsWith(github.ref, 'refs/tags/v') + uses: ./.github/actions/prepare-release with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ steps.windows_build.outputs.bin_path }} - asset_name: ${{ matrix.asset_name }} - tag: ${{ github.ref }} + bin_path: ${{ steps.windows_cli_build.outputs.bin_path }} + bin_name: ${{ matrix.cli_asset_name }} + bin_hash: ${{ steps.windows_cli_build.outputs.bin_hash }} + github_ref: ${{ github.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} - - name: Windows update CLI binary hash - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest' - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - append_body: true - body: | - ${{ steps.windows_build.outputs.bin_hash }} - - - name: Windows build desktop + - name: Build Desktop id: windows_desktop_build - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest' + if: startsWith(github.ref, 'refs/tags/v') shell: msys2 {0} run: | - export PATH=$PATH:/c/ghcup/bin + export PATH=$PATH:/c/ghcup/bin:$(echo /c/tools/ghc-*/bin || echo) scripts/desktop/build-lib-windows.sh cd apps/multiplatform ./gradlew 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 - echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - - - name: Windows upload desktop package to release - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest' - uses: svenstaro/upload-release-action@v2 - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ steps.windows_desktop_build.outputs.package_path }} - asset_name: ${{ matrix.desktop_asset_name }} - tag: ${{ github.ref }} + echo "package_hash=$(echo SHA2-256\(${{ matrix.desktop_asset_name }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - - name: Windows update desktop package hash - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest' - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload Desktop + if: startsWith(github.ref, 'refs/tags/v') + uses: ./.github/actions/prepare-release with: - append_body: true - body: | - ${{ steps.windows_desktop_build.outputs.package_hash }} - - # Windows / + bin_path: ${{ steps.windows_desktop_build.outputs.package_path }} + bin_name: ${{ matrix.desktop_asset_name }} + bin_hash: ${{ steps.windows_desktop_build.outputs.package_hash }} + github_ref: ${{ github.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 72a7cf2b94..b396c9a289 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -5,14 +5,20 @@ on: pull_request_target: types: [opened, closed, synchronize] +permissions: + actions: write + contents: write + pull-requests: write + statuses: write + jobs: CLAssistant: runs-on: ubuntu-latest steps: - name: "CLA Assistant" - if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request' + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' # Beta Release - uses: cla-assistant/github-action@v2.1.3-beta + uses: cla-assistant/github-action@v2.3.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # the below token should have repo scope and must be manually added by you in the repository's secret @@ -33,4 +39,4 @@ jobs: #custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA' #custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.' #lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true) - #use-dco-flag: true - If you are using DCO instead of CLA \ No newline at end of file + #use-dco-flag: true - If you are using DCO instead of CLA diff --git a/.github/workflows/reproduce-schedule.yml b/.github/workflows/reproduce-schedule.yml new file mode 100644 index 0000000000..7de44addc7 --- /dev/null +++ b/.github/workflows/reproduce-schedule.yml @@ -0,0 +1,45 @@ +name: Reproduce latest release + +on: + workflow_dispatch: + schedule: + - cron: '0 2 * * *' # every day at 02:00 night + +jobs: + reproduce: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Get latest release + shell: bash + run: | + curl --proto '=https' \ + --tlsv1.2 \ + -sSf -L \ + 'https://api.github.com/repos/simplex-chat/simplex-chat/releases/latest' \ + 2>/dev/null | \ + grep -i "tag_name" | \ + awk -F \" '{print "TAG="$4}' >> $GITHUB_ENV + + - name: Execute reproduce script + run: | + ${GITHUB_WORKSPACE}/scripts/reproduce-builds.sh "$TAG" + + - name: Check if build has been reproduced + env: + url: ${{ secrets.STATUS_SIMPLEX_WEBHOOK_URL }} + user: ${{ secrets.STATUS_SIMPLEX_WEBHOOK_USER }} + pass: ${{ secrets.STATUS_SIMPLEX_WEBHOOK_PASS }} + run: | + if [ -f "${GITHUB_WORKSPACE}/$TAG/_sha256sums" ]; then + exit 0 + else + curl --proto '=https' --tlsv1.2 -sSf \ + -u "${user}:${pass}" \ + -H 'Content-Type: application/json' \ + -d '{"title": "👾 GitHub: Runner", "description": "⛔️ '"$TAG"' did not reproduce."}' \ + "$url" + exit 1 + fi diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index 039c136464..5fbe8293bc 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -4,13 +4,13 @@ on: push: branches: - master - - stable paths: - website/** - images/** - blog/** - docs/** - .github/workflows/web.yml + - PRIVACY.md jobs: build: @@ -33,7 +33,7 @@ jobs: ./website/web.sh - name: Deploy - uses: peaceiris/actions-gh-pages@v3 + uses: simplex-chat/actions-gh-pages@v3 with: publish_dir: ./website/_site github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index d7106ec8fa..645b55ec9d 100644 --- a/.gitignore +++ b/.gitignore @@ -54,12 +54,14 @@ website/translations.json website/src/img/images/ website/src/images/ website/src/js/lottie.min.js +website/src/privacy.md # Generated files website/package/generated* # Ignore build tool output, e.g. code coverage website/.nyc_output/ website/coverage/ +result # Ignore API documentation website/api-docs/ diff --git a/Dockerfile b/Dockerfile index 834f2374a6..cdcbc40d7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,32 +1,41 @@ -FROM ubuntu:focal AS build +ARG TAG=22.04 -# Install curl and simplex-chat-related dependencies -RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev libssl-dev +FROM ubuntu:${TAG} AS build + +### Build stage + +# Install curl and git and simplex-chat dependencies +RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev llvm-12 llvm-12-dev libnuma-dev libssl-dev + +# Specify bootstrap Haskell versions +ENV BOOTSTRAP_HASKELL_GHC_VERSION=9.6.3 +ENV BOOTSTRAP_HASKELL_CABAL_VERSION=3.10.1.0 # Install ghcup -RUN a=$(arch); curl https://downloads.haskell.org/~ghcup/$a-linux-ghcup -o /usr/bin/ghcup && \ - chmod +x /usr/bin/ghcup - -# Install ghc -RUN ghcup install ghc 9.6.3 -# Install cabal -RUN ghcup install cabal 3.10.1.0 -# Set both as default -RUN ghcup set ghc 9.6.3 && \ - ghcup set cabal 3.10.1.0 - -COPY . /project -WORKDIR /project +RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh # Adjust PATH ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH" +# Set both as default +RUN ghcup set ghc "${BOOTSTRAP_HASKELL_GHC_VERSION}" && \ + ghcup set cabal "${BOOTSTRAP_HASKELL_CABAL_VERSION}" + +COPY . /project +WORKDIR /project + # Adjust build RUN cp ./scripts/cabal.project.local.linux ./cabal.project.local # Compile simplex-chat RUN cabal update -RUN cabal install +RUN cabal build exe:simplex-chat --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library' +# Strip the binary from debug symbols to reduce size +RUN bin=$(find /project/dist-newstyle -name "simplex-chat" -type f -executable) && \ + mv "$bin" ./ && \ + strip ./simplex-chat + +# Copy compiled app from build stage FROM scratch AS export-stage -COPY --from=build /root/.cabal/bin/simplex-chat / +COPY --from=build /project/simplex-chat / diff --git a/Dockerfile.build b/Dockerfile.build new file mode 100644 index 0000000000..76bb1127f2 --- /dev/null +++ b/Dockerfile.build @@ -0,0 +1,92 @@ +# syntax=docker/dockerfile:1.7.0-labs +ARG TAG=24.04 +FROM ubuntu:${TAG} AS build + +### Build stage + +ARG GHC=9.6.3 +ARG CABAL=3.10.1.0 +ARG JAVA=17 + +ENV TZ=Etc/UTC \ + DEBIAN_FRONTEND=noninteractive + +# Install curl, git and and simplex-chat dependencies +RUN apt-get update && \ + apt-get install -y curl \ + libpq-dev \ + git \ + sqlite3 \ + libsqlite3-dev \ + build-essential \ + libgmp3-dev \ + zlib1g-dev \ + llvm \ + cmake \ + llvm-dev \ + libnuma-dev \ + libssl-dev \ + desktop-file-utils \ + patchelf \ + ca-certificates \ + zip \ + wget \ + fuse3 \ + file \ + appstream \ + gpg \ + unzip &&\ + ln -s /bin/fusermount /bin/fusermount3 || : + +# Install Java Coretto +# Required, because official Java in Ubuntu +# depends on libjpeg.so.8 and liblcms2.so.2 which are NOT copied into final +# /usr/lib/runtime/lib directory and I do not have time to figure out gradle.kotlin +# to fix this :( +RUN curl --proto '=https' --tlsv1.2 -sSf 'https://apt.corretto.aws/corretto.key' | gpg --dearmor -o /usr/share/keyrings/corretto-keyring.gpg &&\ + echo "deb [signed-by=/usr/share/keyrings/corretto-keyring.gpg] https://apt.corretto.aws stable main" > /etc/apt/sources.list.d/corretto.list &&\ + apt update &&\ + apt install -y java-${JAVA}-amazon-corretto-jdk + +# Specify bootstrap Haskell versions +ENV BOOTSTRAP_HASKELL_GHC_VERSION=${GHC} +ENV BOOTSTRAP_HASKELL_CABAL_VERSION=${CABAL} + +# Do not install Stack +ENV BOOTSTRAP_HASKELL_INSTALL_NO_STACK=true +ENV BOOTSTRAP_HASKELL_INSTALL_NO_STACK_HOOK=true + +# Install ghcup +RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh + +# Adjust PATH +ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH" + +# Set both as default +RUN ghcup set ghc "${GHC}" && \ + ghcup set cabal "${CABAL}" + +#===================== +# Install Android SDK +#===================== +ARG SDK_VERSION=13114758 + +ENV SDK_VERSION=$SDK_VERSION \ + ANDROID_HOME=/root + +RUN curl -L -o tools.zip "https://dl.google.com/android/repository/commandlinetools-linux-${SDK_VERSION}_latest.zip" && \ + unzip tools.zip && rm tools.zip && \ + mv cmdline-tools tools && mkdir "$ANDROID_HOME/cmdline-tools" && mv tools "$ANDROID_HOME/cmdline-tools/" && \ + ln -s "$ANDROID_HOME/cmdline-tools/tools" "$ANDROID_HOME/cmdline-tools/latest" + +ENV PATH="$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/cmdline-tools/tools/bin" + +# https://askubuntu.com/questions/885658/android-sdk-repositories-cfg-could-not-be-loaded +RUN mkdir -p ~/.android ~/.gradle && \ + touch ~/.android/repositories.cfg && \ + echo 'org.gradle.console=plain' > ~/.gradle/gradle.properties &&\ + yes | sdkmanager --licenses >/dev/null + +ENV PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/build-tools + +WORKDIR /project diff --git a/PRIVACY.md b/PRIVACY.md index dbd48940f6..18e5539726 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,134 +1,246 @@ -# SimpleX Chat Terms & Privacy Policy +--- +layout: layouts/privacy.html +permalink: /privacy/index.html +--- -SimpleX Chat is the first communication platform that has no user profile IDs of any kind, not even random numbers. Not only it has no access to your messages (thanks to open-source double-ratchet end-to-end encryption protocol and additional encryption layers), it also has no access to your profile and contacts - we cannot observe your connections graph. +# SimpleX Chat Operators Privacy Policy and Conditions of Use -If you believe that some of the clauses in this document are not aligned with our mission or principles, please raise it with us via [email](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). +## Summary + +[Introduction](#introduction) and [General principles](#general-principles) cover SimpleX Chat network design, the network operators, and the principles of privacy and security provided by SimpleX network. + +[Privacy policy](#privacy-policy) covers: +- data stored only on your device - [your profiles](#user-profiles), delivered [messages and files](#messages-and-files). You can transfer this information to another device, and you are responsible for its preservation - if you delete the app it will be lost. +- [private message delivery](#private-message-delivery) that protects your IP address and connection graph from the destination servers. +- [undelivered messages and files](#storage-of-messages-and-files-on-the-servers) stored on the servers. +- [how users connect](#connections-with-other-users) without any user profile identifiers. +- [iOS push notifications](#ios-push-notifications) privacy limitations. +- [user support](#user-support), [SimpleX directory](#simplex-directory) and [any other data](#another-information-stored-on-the-servers) that may be stored on the servers. +- [preset server operators](#preset-server-operators) and the [information they may share](#information-preset-server-operators-may-share). +- [source code license](#source-code-license) and [updates to this document](#updates). + +[Conditions of Use](#conditions-of-use-of-software-and-infrastructure) are the conditions you need to accept to use SimpleX Chat applications and the relay servers of preset operators. Their purpose is to protect the users and preset server operators. + +*Please note*: this summary and any links in this document are provided for information only - they are not a part of the Privacy Policy and Conditions of Use. + +## Introduction + +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. + +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). ## Privacy Policy -SimpleX Chat Ltd. ("SimpleX Chat") uses the best 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 cannot be compromised by the servers via [man-in-the-middle attack](./docs/GLOSSARY.md#man-in-the-middle-attack). +### General principles -SimpleX Chat is built on top of SimpleX messaging and application platform that uses a new message routing protocol allowing to establish private connections without having any kind of addresses that identify its users - we don't use emails, phone numbers, usernames, identity keys or any other user identifiers to pass messages between the users. +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 Chat security assessment was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2.0 – see [the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). +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. -### Information you provide +SimpleX software is similar in its design approach to email clients and browsers - it allows you to have full control of your data and freely choose the relay server operators, in the same way you choose which website or email provider to use, or use your own relay servers, simply by changing the configuration of the client software. The only current restriction to that is Apple push notifications - at the moment they can only be delivered via the servers operated by SimpleX Chat Ltd, as explained below. We are exploring the solutions to deliver push notifications to iOS devices via other providers or users' own servers. + +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. + +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). + +The cryptographic review of SimpleX protocols design was done in July 2024 by Trail of Bits – see [the announcement](/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md). + +### Your information #### User profiles -We do not store user profiles. The profile you create in the app is local to your device. +Servers used by SimpleX Chat apps do not create, store or identify user chat profiles. The profiles you can create in the app are local to your device, and can be removed at any time via the app. -When you create a user profile, no records are created on our servers, and we have no access to any part of your profile information, and even to the fact that you created a profile - it is a local record stored only on your device. That means that if you delete the app, and have no backup, you will permanently lose all the data and the private connections you create with other users. +When you create the local profile, no records are created on any of the relay servers, and infrastructure operators, whether preset in the app or any other, have no access to any part of your information, and even to the fact that you created a profile - it is a local record stored only on your device. That means that if you delete the app, and have no backup, you will permanently lose all your data and the private connections you created with other software users. + +You can transfer the profile to another device by creating a backup of the app data and restoring it on the new device, but you cannot use more than one device with the copy of the same profile at the same time - it will disrupt any active conversations on either or both devices, as a security property of end-to-end encryption. #### Messages and Files -SimpleX Chat cannot decrypt or otherwise access the content or even the size of your messages and files you send or receive. Each message is padded to a fixed size of 16kb. Each file is sent in chunks of 256kb, 1mb or 8mb via all or some of the configured file servers. Both messages and files are sent end-to-end encrypted, and the servers do not have technical means to compromise this encryption, because part of the [key exchange](./docs/GLOSSARY.md#key-exchange) happens out-of-band. +SimpleX relay servers cannot decrypt or otherwise access the content or even the size of your messages and files you send or receive. Each message is padded to a fixed size of 16kb. Each file is sent in chunks of 64kb, 256kb, 1mb or 4mb via all or some of the configured file relay servers. Both messages and files are sent end-to-end encrypted, and the servers do not have technical means to compromise this encryption, because part of the [key exchange](/docs/GLOSSARY.md#key-exchange) happens out-of-band. -Your message history is stored only on your own device and the devices of your contacts. While the recipients' devices are offline SimpleX Chat temporarily stores end-to-end encrypted messages on the messaging (SMP) servers that are preset in the app or chosen by the users. +Your message history is stored only on your own device and the devices of your contacts. While the recipients' devices are offline, messaging relay servers temporarily store end-to-end encrypted messages – you can configure which relay servers are used to receive the messages from the new contacts, and you can manually change them for the existing contacts too. -The messages are permanently removed from the preset servers as soon as they are delivered. Undelivered messages are deleted after the time that is configured in the messaging servers you use (21 days for preset messaging servers). +#### Private message delivery -The files are stored on file (XFTP) servers for the time configured in the file servers you use (48 hours for preset file servers). +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. -If a messaging or file servers are restarted, the encrypted message or the record of the file can be stored in a backup file until it is overwritten by the next restart (usually within 1 week). +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. + +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 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 about mailboxes) are created on chosen messaging servers, that can be the preset servers or the servers that you configured in the app, in case it allows such configuration. SimpleX uses separate queues for direct and response messages, that the client applications prefer to create on two different servers, in case you have more than one server configured in the app, which is the default. +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. -At the time of updating this document all our client applications allow configuring the servers. Our servers do not store information about which queues are linked to your profile on the device, and they do not collect any information that would allow us to establish that these queues are related to your device or your profile - the access to each queue is authorized by two anonymous unique cryptographic keys, different for each queue, and separate for sender and recipient of the messages. +Preset and unmodified SimpleX relay servers do not store information about which queues are linked to your profile on the device, and they do not collect any information that would allow infrastructure owners and operators to establish that these queues are related to your device or your profile - the access to each queue is authorized by two anonymous unique cryptographic keys, different for each queue, and separate for sender and recipient of the messages. + +#### 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). + +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. + +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. #### iOS Push Notifications +This section applies only to the notification servers operated by SimpleX Chat Ltd. + When you choose to use instant push notifications in SimpleX iOS app, because the design of push notifications requires storing the device token on notification server, the notifications server can observe how many messaging queues your device has notifications enabled for, and approximately how many messages are sent to each queue. -Notification server cannot observe the actual addresses of these queues, as a separate address is used to subscribe to the notifications. It also cannot observe who, or even how many contacts, send messages to you, as notifications are delivered to your device end-to-end encrypted by the messaging servers. +Preset notification server cannot observe the actual addresses of these queues, as a separate address is used to subscribe to the notifications. It also cannot observe who sends messages to you. Apple push notifications servers can only observe how many notifications are sent to you, but not from how many contacts, or from which messaging relays, as notifications are delivered to your device end-to-end encrypted by one of the preset notification servers - these notifications only contain end-to-end encrypted metadata, not even encrypted message content, and they look completely random to Apple push notification servers. -It also does not allow to see message content or sizes, as the actual messages are not sent via the notification server, only the fact that the message is available and where it can be received from (the latter information is encrypted, so that the notification server cannot observe it). You can read more about the design of iOS push notifications [here](https://simplex.chat/blog/20220404-simplex-chat-instant-notifications.html#our-ios-approach-has-one-trade-off). +You can read more about the design of iOS push notifications [here](./blog/20220404-simplex-chat-instant-notifications.md#our-ios-approach-has-one-trade-off). #### Another information stored on the servers -Additional technical information can be stored on our servers, including randomly generated authentication tokens, keys, push tokens, and other material that is necessary to transmit messages. SimpleX Chat limits this additional technical information to the minimum required to operate the Services. +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. -#### SimpleX Directory Service +#### SimpleX Directory -[SimpleX directory service](./docs/DIRECTORY.md) stores: your search requests, the messages and the members profiles in the group. You can connect to SimpleX Directory Service 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). +This section applies only to the experimental group directory operated by SimpleX Chat Ltd. -#### User Support. +[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). -If you contact SimpleX Chat any personal data you may share with us 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. +#### Public groups and content channels -### Information we may share +You may participate in a public group and receive content from a public channel (Group). In case you send messages or comments to the Group, you grant a license: +- to all recipients: + - to share your messages with the new Group members and outside of the group, e.g. via quoting (replying), forwarding and copy-pasting your message. When your message is deleted or marked as deleted, the copies of your message will not be deleted. + - 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. -We operate our Services using third parties. While we do not share any user data, these third party may access the encrypted user data as it is stored or transmitted via our servers. +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. -We use a third party for email services - if you ask for support via email, your and SimpleX Chat email providers may access these emails according to their privacy policies and terms of service. +#### User Support -The cases when SimpleX Chat may need to share the data we temporarily store on the servers: +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. -- To meet any applicable law, regulation, legal process or enforceable governmental request. -- To enforce applicable Terms, including investigation of potential violations. +### 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 must not provide general access to their servers or the data on their servers to each other. + +Preset server operators will provide non-administrative access to control port of preset servers to SimpleX Chat Ltd, for the purposes of removing illegal content identified in publicly accessible resources (contact and group addresses, and downloadable files). This control port access only allows deleting known links and files, and accessing aggregate server-wide statistics, but does NOT allow enumerating any information on the servers or accessing statistics related to specific users. + +### Information Preset Server Operators May Share + +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). + +The cases when the preset server operators may share the data temporarily stored on the servers: + +- To meet any applicable law, or enforceable governmental request or court order. +- To enforce applicable terms, including investigation of potential violations. - To detect, prevent, or otherwise address fraud, security, or technical issues. -- To protect against harm to the rights, property, or safety of SimpleX Chat, our users, or the public as required or permitted by law. +- 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. -At the time of updating this document, we have never provided or have been requested the access to our servers or any information from our servers by any third parties. If we are ever requested to provide such access or information, we will follow the due legal process. +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). + +### Source code license + +As this software is fully open-source and provided under AGPLv3 license, all infrastructure owners and operators, and the developers of the client and server applications who use the SimpleX Chat source code, are required to publish any changes to this software under the same AGPLv3 license - including any modifications to the servers. + +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. ### Updates -We will update this Privacy Policy as needed so that it is current, accurate, and as clear as possible. Your continued use of our Services confirms your acceptance of our updated Privacy Policy. +This Privacy Policy applies to SimpleX Chat Ltd and all other preset server operators you use in the app. -Please also read our Terms of Service below. +This Privacy Policy may be updated as needed so that it is current, accurate, and as clear as possible. When it is updated, you will have to review and accept the changed policy within 30 days of such changes to continue using preset relay servers. Even if you fail to accept the changed policy, your continued use of SimpleX Chat software applications and preset relay servers confirms your acceptance of the updated Privacy Policy. -If you have questions about our Privacy Policy please contact us via [email](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). +Please also read The Conditions of Use of Software and Infrastructure below. -## Terms of Service +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). -You accept our Terms of Service ("Terms") by installing or using any of our apps or services ("Services"). +## Conditions of Use of Software and Infrastructure -**Minimal age**. You must be at least 13 years old to use our Services. The minimum age to use our Services without parental approval may be higher in your country. +You accept the Conditions of Use of Software and Infrastructure ("Conditions") by installing or using any of SimpleX Chat software or using any of server infrastructure (collectively referred to as "Applications") operated by the Preset Server Operators, including SimpleX Chat Ltd, whether these servers are preset in the software or not. -**Accessing the servers**. For the efficiency of the network access, the apps access all queues you create on any server via the same network (TCP/IP) connection. Our servers do not collect information about which queues were accessed via the same connection, so we cannot establish which queues belong to the same users. Whoever might observe your network traffic would know which 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. Please refer to our [technical design document](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information about our privacy model and known security and privacy risks. +**Minimal age**. You must be at least 13 years old to use SimpleX Chat Applications. The minimum age to use SimpleX Applications without parental approval may be higher in your country. -**Privacy of user data**. We do not retain any data we transmit for any longer than necessary to provide the Services. We only collect aggregate statistics across all users, not per user - we do not have information about how many people use SimpleX Chat (we only know an approximate number of app installations and the aggregate traffic through our servers). In any case, we do not and will not sell or in any way monetize user data. +**Infrastructure**. Infrastructure of the preset server operators includes messaging and file relay servers. SimpleX Chat Ltd also provides iOS push notification servers for public use. This infrastructure does not have any modifications from the [published open-source code](https://github.com/simplex-chat/simplexmq) available under AGPLv3 license. Any infrastructure provider, whether commercial or not, is required by the Affero clause (named after Affero Inc. company that pioneered the community-based Q&A sites in early 2000s) to publish any modifications under the same license. The statements in relation to Infrastructure and relay servers anywhere in this document assume no modifications to the published code, even in the cases when it is not explicitly stated. -**Operating our services**. For the purpose of operating our Services, you agree that your end-to-end encrypted messages are transferred via our servers in the United Kingdom, the United States and other countries where we have or use facilities and service providers or partners. +**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. -**Software**. You agree to downloading and installing updates to our Services when they are available; they would only be automatic if you configure your devices in this way. +**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. -**Traffic and device costs**. You are solely responsible for the traffic and device costs on which you use our Services, and any associated taxes. +**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. -**Legal and acceptable usage**. You agree to use our Services only for legal and acceptable purposes. You will not use (or assist others in using) our Services in ways that: 1) violate or infringe the rights of SimpleX Chat, our users, or others, including privacy, publicity, intellectual property, or other proprietary rights; 2) involve sending illegal or impermissible communications, e.g. spam. +**Operating Infrastructure**. For the purpose of using SimpleX Chat Software, if you continue using preset servers, you agree that your end-to-end encrypted messages are transferred via the preset servers in any countries where preset server operators have or use facilities and service providers or partners. The information about geographic location and hosting providers of the preset messaging servers is available on server pages. -**Damage to SimpleX Chat**. You must not (or assist others to) access, use, modify, distribute, transfer, or exploit our Services in unauthorized manners, or in ways that harm SimpleX Chat, our Services, or systems. For example, you must not 1) access our Services or systems without authorization, other than by using the apps; 2) disrupt the integrity or performance of our Services; 3) collect information about our users in any manner; or 4) sell, rent, or charge for our Services. +**Software**. You agree to downloading and installing updates to SimpleX Chat Applications when they are available; they would only be automatic if you configure your devices in this way. -**Keeping your data secure**. SimpleX Chat is the first messaging platform that is 100% private by design - we neither have ability to access your messages, nor we have information about who you communicate with. That means that you are solely responsible for keeping your device and your user profile safe and secure. If you lose your phone or remove the app, you will not be able to recover the lost data, unless you made a back up. +**Traffic and device costs**. You are solely responsible for the traffic and device costs that you incur while using SimpleX Chat Applications, and any associated taxes. -**Storing the messages on the device**. The messages are stored in the encrypted database on your device. Whether and how database passphrase is stored is determined by the configuration of the application you use. Legacy databases created prior to 2023 or in CLI (terminal) app may remain unencrypted, and it will be indicated in the app. In this case, if you make a backup of the app data and store it unencrypted, the backup provider may be able to access the messages. Please note, that the beta version of desktop app currently stores the database passphrase in the configuration file in plaintext, so you may need to remove passphrase from the device via the app configuration. +**Legal usage**. You agree to use SimpleX Chat Applications only for legal purposes. You will not use (or assist others in using) the Applications in ways that: 1) violate or infringe the rights of Software users, SimpleX Chat Ltd, other preset server operators, or others, including privacy, publicity, intellectual property, or other proprietary rights; 2) involve sending illegal communications, e.g. spam. While server operators cannot access content or identify messages or groups, in some cases the links to the illegal communications can be shared publicly on social media or websites. Preset server operators reserve the right to remove such links from the preset servers and disrupt the conversations that send illegal content via their servers, whether they were reported by the users or discovered by the operators themselves. -**Storing the files on the device**. The files are stored on your device unencrypted. If you make a backup of the app data and store it unencrypted, the backup provider will be able to access the files. +**Damage to SimpleX Chat Ltd and Preset Server Operators**. You must not (or assist others to) access, use, modify, distribute, transfer, or exploit SimpleX Chat Applications in unauthorized manners, or in ways that harm Software users, SimpleX Chat Ltd, other preset server operators, their Infrastructure, or any other systems. For example, you must not 1) access preset operators' Infrastructure or systems without authorization, in any way other than by using the Software or by using a 3rd party client applications that satisfies the requirements of the Conditions of use (see the next section); 2) disrupt the integrity or performance of preset operators' Infrastructure; 3) collect information about the users in any manner; or 4) sell, rent, or charge for preset operators' Infrastructure. This does not prohibit you from providing your own Infrastructure to others, whether free or for a fee, as long as you do not violate these Conditions and AGPLv3 license, including the requirement to publish any modifications of the relay server software. -**No Access to Emergency Services**. Our Services do not provide access to emergency service providers like the police, fire department, hospitals, or other public safety organizations. Make sure you can contact emergency service providers through a mobile, fixed-line telephone, or other service. +**3rd party client applications**. You may use a 3rd party application (App) to access preset operators' Infrastructure or systems, provided that this App: +- is compatible with the protocol specifications not older than 1 year, +- provides user-to-user messaging only or enables automated chat bots sending messages requested by users (in case of bots, it must be made clear to the users that these are automated bots), +- implements the same limits, rules and restrictions as Software, +- requires that the users accept the same Conditions of use of preset operators' Infrastructure as in Software prior to providing access to this Infrastructure, +- displays the notice that it is the App for using SimpleX network, +- provides its source code under open-source license accessible to the users via the App interface. In case the App uses the source code of Software, the App's source code must be provided under AGPLv3 license, and in case it is developed without using Software code its source code must be provided under any widely recognized free open-source license, +- does NOT use the branding of SimpleX Chat Ltd without the permission, +- does NOT pretend to be Software, +- complies with these Conditions of use. -**Third-party services**. Our Services may allow you to access, use, or interact with third-party websites, apps, content, and other products and services. When you use third-party services, their terms and privacy policies govern your use of those services. +**Keeping your data secure**. SimpleX Chat is the first communication software that aims to be 100% private by design - server software neither has the ability to access your messages, nor it has information about who you communicate with. That means that you are solely responsible for keeping your device, your user profile and any data safe and secure. If you lose your phone or remove the Software from the device, you will not be able to recover the lost data, unless you made a back up. To protect the data you need to make regular backups, as using old backups may disrupt your communication with some of the contacts. SimpleX Chat Ltd and other preset server operators are not responsible for any data loss. -**Your Rights**. You own the messages and the information you transmit through our Services. Your recipients are able to retain the messages you receive from you; there is no technical ability to delete data from their devices. While there are various app features that allow deleting messages from the recipients' devices, such as _disappearing messages_ and _full message deletion_, their functioning on your recipients' devices cannot be guaranteed or enforced, as the device may be offline or have a modified version of the app. +**Storing the messages on the device**. The messages are stored in the encrypted database on your device. Whether and how database passphrase is stored is determined by the configuration of the Software you use. The databases created prior to 2023 or in CLI (terminal) app may remain unencrypted, and it will be indicated in the app interface. In this case, if you make a backup of the data and store it unencrypted, the backup provider may be able to access the messages. Please note, that the desktop apps can be configured to store the database passphrase in the configuration file in plaintext, and unless you set the passphrase when first running the app, a random passphrase will be used and stored on the device. You can remove it from the device via the app settings. -**License**. SimpleX Chat grants you a limited, revocable, non-exclusive, and non-transferable license to use our Services in accordance with these Terms. The source-code of services is available and can be used under [AGPL v3 license](https://github.com/simplex-chat/simplex-chat/blob/stable/LICENSE) +**Storing the files on the device**. The files currently sent and received in the apps by default (except CLI app) are stored on your device encrypted using unique keys, different for each file, that are stored in the database. Once the message that the file was attached to is removed, even if the copy of the encrypted file is retained, it should be impossible to recover the key allowing to decrypt the file. This local file encryption may affect app performance, and it can be disabled via the app settings. This change will only affect the new files. If you later re-enable the encryption, it will also affect only the new files. If you make a backup of the app data and store it unencrypted, the backup provider will be able to access any unencrypted files. In any case, irrespective of the storage setting, the files are always sent by all apps end-to-end encrypted. -**SimpleX Chat Rights**. We own all copyrights, trademarks, domains, logos, trade secrets, and other intellectual property rights associated with our Services. You may not use our copyrights, trademarks, domains, logos, and other intellectual property rights unless you have our written permission, and unless under an open-source license distributed together with the source code. To report copyright, trademark, or other intellectual property infringement, please contact chat@simplex.chat. +**No Access to Emergency Services**. SimpleX Chat Applications do not provide access to emergency service providers like the police, fire department, hospitals, or other public safety organizations. Make sure you can contact emergency service providers through a mobile, fixed-line telephone, or other service. -**Disclaimers**. YOU USE OUR SERVICES AT YOUR OWN RISK AND SUBJECT TO THE FOLLOWING DISCLAIMERS. WE PROVIDE OUR SERVICES ON AN “AS IS” BASIS WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, AND FREEDOM FROM COMPUTER VIRUS OR OTHER HARMFUL CODE. SIMPLEX DOES NOT WARRANT THAT ANY INFORMATION PROVIDED BY US IS ACCURATE, COMPLETE, OR USEFUL, THAT OUR SERVICES WILL BE OPERATIONAL, ERROR-FREE, SECURE, OR SAFE, OR THAT OUR SERVICES WILL FUNCTION WITHOUT DISRUPTIONS, DELAYS, OR IMPERFECTIONS. WE DO NOT CONTROL, AND ARE NOT RESPONSIBLE FOR, CONTROLLING HOW OR WHEN OUR USERS USE OUR SERVICES. WE ARE NOT RESPONSIBLE FOR THE ACTIONS OR INFORMATION (INCLUDING CONTENT) OF OUR USERS OR OTHER THIRD PARTIES. YOU RELEASE US, AFFILIATES, DIRECTORS, OFFICERS, EMPLOYEES, PARTNERS, AND AGENTS ("SIMPLEX PARTIES") FROM ANY CLAIM, COMPLAINT, CAUSE OF ACTION, CONTROVERSY, OR DISPUTE (TOGETHER, "CLAIM") AND DAMAGES, KNOWN AND UNKNOWN, RELATING TO, ARISING OUT OF, OR IN ANY WAY CONNECTED WITH ANY SUCH CLAIM YOU HAVE AGAINST ANY THIRD PARTIES. +**Third-party services**. SimpleX Chat Applications may allow you to access, use, or interact with the websites of SimpleX Chat Ltd, preset server operators or other third-party websites, apps, content, and other products and services. When you use third-party services, their terms and privacy policies govern your use of those services. -**Limitation of liability**. THE SIMPLEX PARTIES WILL NOT BE LIABLE TO YOU FOR ANY LOST PROFITS OR CONSEQUENTIAL, SPECIAL, PUNITIVE, INDIRECT, OR INCIDENTAL DAMAGES RELATING TO, ARISING OUT OF, OR IN ANY WAY IN CONNECTION WITH OUR TERMS, US, OR OUR SERVICES, EVEN IF THE SIMPLEX PARTIES HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. OUR AGGREGATE LIABILITY RELATING TO, ARISING OUT OF, OR IN ANY WAY IN CONNECTION WITH OUR TERMS, US, OR OUR SERVICES WILL NOT EXCEED ONE DOLLAR ($1). THE FOREGOING DISCLAIMER OF CERTAIN DAMAGES AND LIMITATION OF LIABILITY WILL APPLY TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW. THE LAWS OF SOME JURISDICTIONS MAY NOT ALLOW THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES, SO SOME OR ALL OF THE EXCLUSIONS AND LIMITATIONS SET FORTH ABOVE MAY NOT APPLY TO YOU. NOTWITHSTANDING ANYTHING TO THE CONTRARY IN OUR TERMS, IN SUCH CASES, THE LIABILITY OF THE SIMPLEX PARTIES WILL BE LIMITED TO THE EXTENT PERMITTED BY APPLICABLE LAW. +**Your Rights**. You own the messages and the information you transmit through SimpleX Applications. Your recipients are able to retain the messages they receive from you; there is no technical ability to delete data from their devices. While there are various app features that allow deleting messages from the recipients' devices, such as _disappearing messages_ and _full message deletion_, their functioning on your recipients' devices cannot be guaranteed or enforced, as the device may be offline or have a modified version of the Software. At the same time, repudiation property of the end-to-end encryption algorithm allows you to plausibly deny having sent the message, like you can deny what you said in a private face-to-face conversation, as the recipient cannot provide any proof to the third parties, by design. -**Availability**. Our Services may be interrupted, including for maintenance, upgrades, or network or equipment failures. We may discontinue some or all of our Services, including certain features and the support for certain devices and platforms, at any time. +**License**. SimpleX Chat Ltd grants you a limited, revocable, non-exclusive, and non-transferable license to use SimpleX Chat Applications in accordance with these Conditions. The source-code of Applications is available and can be used under [AGPL v3 license](https://github.com/simplex-chat/simplex-chat/blob/stable/LICENSE). -**Resolving disputes**. You agree to resolve any Claim you have with us relating to or arising from our Terms, us, or our Services in the courts of England and Wales. You also agree to submit to the personal jurisdiction of such courts for the purpose of resolving all such disputes. The laws of England govern our Terms, as well as any disputes, whether in court or arbitration, which might arise between SimpleX Chat and you, without regard to conflict of law provisions. +**SimpleX Chat Ltd Rights**. SimpleX Chat Ltd (and, where applicable, preset server operators) owns all copyrights, trademarks, domains, logos, trade secrets, and other intellectual property rights associated with the Applications. You may not use SimpleX Chat Ltd copyrights, trademarks, domains, logos, and other intellectual property rights unless you have SimpleX Chat Ltd written permission, and unless under an open-source license distributed together with the source code. To report copyright, trademark, or other intellectual property infringement, please contact chat@simplex.chat. -**Changes to the terms**. SimpleX Chat may update the Terms from time to time. Your continued use of our Services confirms your acceptance of our updated Terms and supersedes any prior Terms. You will comply with all applicable export control and trade sanctions laws. Our Terms cover the entire agreement between you and SimpleX Chat regarding our Services. If you do not agree with our Terms, you should stop using our Services. +**Disclaimers**. YOU USE SIMPLEX APPLICATIONS AT YOUR OWN RISK AND SUBJECT TO THE FOLLOWING DISCLAIMERS. SIMPLEX CHAT LTD PROVIDES APPLICATIONS ON AN “AS IS” BASIS WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, AND FREEDOM FROM COMPUTER VIRUS OR OTHER HARMFUL CODE. SIMPLEX CHAT LTD DOES NOT WARRANT THAT ANY INFORMATION PROVIDED BY THEM IS ACCURATE, COMPLETE, OR USEFUL, THAT THEIR APPLICATIONS WILL BE OPERATIONAL, ERROR-FREE, SECURE, OR SAFE, OR THAT THEIR APPLICATIONS WILL FUNCTION WITHOUT DISRUPTIONS, DELAYS, OR IMPERFECTIONS. SIMPLEX CHAT LTD AND OTHER PRESET OPERATORS DO NOT CONTROL, AND ARE NOT RESPONSIBLE FOR, CONTROLLING HOW OR WHEN THE USERS USE APPLICATIONS. SIMPLEX CHAT LTD AND OTHER PRESET OPERATORS ARE NOT RESPONSIBLE FOR THE ACTIONS OR INFORMATION (INCLUDING CONTENT) OF THEIR USERS OR OTHER THIRD PARTIES. YOU RELEASE SIMPLEX CHAT LTD, OTHER PRESET OPERATORS, AFFILIATES, DIRECTORS, OFFICERS, EMPLOYEES, PARTNERS, AND AGENTS ("SIMPLEX PARTIES") FROM ANY CLAIM, COMPLAINT, CAUSE OF ACTION, CONTROVERSY, OR DISPUTE (TOGETHER, "CLAIM") AND DAMAGES, KNOWN AND UNKNOWN, RELATING TO, ARISING OUT OF, OR IN ANY WAY CONNECTED WITH ANY SUCH CLAIM YOU HAVE AGAINST ANY THIRD PARTIES. -**Enforcing the terms**. If we fail to enforce any of our Terms, that does not mean we waive the right to enforce them. If any provision of the Terms is deemed unlawful, void, or unenforceable, that provision shall be deemed severable from our Terms and shall not affect the enforceability of the remaining provisions. Our Services are not intended for distribution to or use in any country where such distribution or use would violate local law or would subject us to any regulations in another country. We reserve the right to limit our Services in any country. If you have specific questions about these Terms, please contact us at chat@simplex.chat. +**Limitation of liability**. THE SIMPLEX PARTIES WILL NOT BE LIABLE TO YOU FOR ANY LOST PROFITS OR CONSEQUENTIAL, SPECIAL, PUNITIVE, INDIRECT, OR INCIDENTAL DAMAGES RELATING TO, ARISING OUT OF, OR IN ANY WAY IN CONNECTION WITH OUR CONDITIONS, US, OR SIMPLEX APPLICATIONS, EVEN IF THE SIMPLEX PARTIES HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. THE AGGREGATE LIABILITY OF THE SIMPLEX PARTIES RELATING TO, ARISING OUT OF, OR IN ANY WAY IN CONNECTION WITH THESE CONDITIONS, THE SIMPLEX PARTIES, OR THE APPLICATIONS WILL NOT EXCEED ONE DOLLAR ($1). THE FOREGOING DISCLAIMER OF CERTAIN DAMAGES AND LIMITATION OF LIABILITY WILL APPLY TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW. THE LAWS OF SOME JURISDICTIONS MAY NOT ALLOW THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES, SO SOME OR ALL OF THE EXCLUSIONS AND LIMITATIONS SET FORTH ABOVE MAY NOT APPLY TO YOU. NOTWITHSTANDING ANYTHING TO THE CONTRARY IN THE CONDITIONS, IN SUCH CASES, THE LIABILITY OF THE SIMPLEX PARTIES WILL BE LIMITED TO THE EXTENT PERMITTED BY APPLICABLE LAW. -**Ending these Terms**. You may end these Terms with SimpleX Chat at any time by deleting SimpleX Chat app(s) from your device and discontinuing use of our Services. The provisions related to Licenses, Disclaimers, Limitation of Liability, Resolving dispute, Availability, Changes to the terms, Enforcing the terms, and Ending these Terms will survive termination of your relationship with SimpleX Chat. +**Availability**. The Applications may be disrupted, including for maintenance, upgrades, or network or equipment failures. SimpleX Chat Ltd may discontinue some or all of their Applications, including certain features and the support for certain devices and platforms, at any time. Preset server operators may discontinue providing the servers, at any time. -Updated August 17, 2023 +**Resolving disputes**. You agree to resolve any Claim you have with SimpleX Chat Ltd and/or preset server operators relating to or arising from these Conditions, them, or the Applications in the courts of England and Wales. You also agree to submit to the personal jurisdiction of such courts for the purpose of resolving all such disputes. The laws of England govern these Conditions, as well as any disputes, whether in court or arbitration, which might arise between SimpleX Chat Ltd (or preset server operators) and you, without regard to conflict of law provisions. + +**Changes to the conditions**. SimpleX Chat Ltd may update the Conditions from time to time. The updated conditions have to be accepted within 30 days. Even if you fail to accept updated conditions, your continued use of SimpleX Chat Applications confirms your acceptance of the updated Conditions and supersedes any prior Conditions. You will comply with all applicable export control and trade sanctions laws. These Conditions cover the entire agreement between you and SimpleX Chat Ltd, and any preset server operators where applicable, regarding SimpleX Chat Applications. If you do not agree with these Conditions, you should stop using the Applications. + +**Enforcing the conditions**. If SimpleX Chat Ltd or preset server operators fail to enforce any of these Conditions, that does not mean they waive the right to enforce them. If any provision of the Conditions is deemed unlawful, void, or unenforceable, that provision shall be deemed severable from the Conditions and shall not affect the enforceability of the remaining provisions. The Applications are not intended for distribution to or use in any country where such distribution or use would violate local law or would subject SimpleX Chat Ltd to any regulations in another country. SimpleX Chat Ltd reserve the right to limit the access to the Applications in any country. Preset operators reserve the right to limit access to their servers in any country. If you have specific questions about these Conditions, please contact SimpleX Chat Ltd at chat@simplex.chat. + +**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 diff --git a/README.md b/README.md index 254a66c2bc..554c6068d9 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ [![Join on Reddit](https://img.shields.io/reddit/subreddit-subscribers/SimpleXChat?style=social)](https://www.reddit.com/r/SimpleXChat) ![Follow on Mastodon](https://img.shields.io/mastodon/follow/108619463746856738?domain=https%3A%2F%2Fmastodon.social&style=social) -| 30/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md) | +| 30/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md), [PL](/docs/lang/pl/README.md) | SimpleX logo # SimpleX - the first messaging platform that has no user identifiers of any kind - 100% private by design! -[](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html)     [](https://www.privacyguides.org/en/real-time-communication/#simplex-chat)     [](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/) +[](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html)     [](https://www.privacyguides.org/en/real-time-communication/#simplex-chat)     [](https://www.whonix.org/wiki/Chat#Recommendation)     [](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/) ## Welcome to SimpleX Chat! @@ -18,7 +18,7 @@ 2. ↔️ [Connect to the team](#connect-to-the-team), [join user groups](#join-user-groups) and [follow our updates](#follow-our-updates). 3. 🤝 [Make a private connection](#make-a-private-connection) with a friend. 4. 🔤 [Help translating SimpleX Chat](#help-translating-simplex-chat). -5. ⚡️ [Contribute](#contribute) and [help us with donations](#help-us-with-donations). +5. ⚡️ [Contribute](#contribute) and [support us with donations](#please-support-us-with-your-donations). [Learn more about SimpleX Chat](#contents). @@ -72,9 +72,9 @@ You must: Messages not following these rules will be deleted, the right to send messages may be revoked, and the access to the new members to the group may be temporarily restricted, to prevent re-joining under a different name - our imperfect group moderation does not have a better solution at the moment. -You can join an English-speaking users group if you want to ask any questions: [#SimpleX-Group-4](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2Fw2GlucRXtRVgYnbt_9ZP-kmt76DekxxS%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA0tJhTyMGUxznwmjb7aT24P1I1Wry_iURTuhOFlMb1Eo%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22WoPxjFqGEDlVazECOSi2dg%3D%3D%22%7D) +You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FiBkJE72asZX1NUZaYFIeKRVk6oVjb-iv%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAinqu3j74AMjODLoIRR487ZW6ysip_dlpD6Zxk18SPFY%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22groupLinkId%22%3A%223wAFGCLygQHR5AwynZOHlQ%3D%3D%22%7D) -There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F6eHqy7uAbZPOcA6qBtrQgQquVlt4Ll91%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAqV_pg3FF00L98aCXp4D3bOs4Sxv_UmSd-gb0juVoQVs%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22XonlixcHBIb2ijCehbZoiw%3D%3D%22%7D) for developers who build on SimpleX platform: +There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) for developers who build on SimpleX platform: - chat bots and automations - integrations with other apps @@ -83,7 +83,7 @@ There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=s There are groups in other languages, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users: -[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking), [\#SimpleX-ES](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FaJ8O1O8A8GbeoaHTo_V8dcefaCl7ouPb%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA034qWTA3sWcTsi6aWhNf9BA34vKVCFaEBdP2R66z6Ao%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22wiZ1v_wNjLPlT-nCSB-bRA%3D%3D%22%7D) (Spanish-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking). +[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FmfiivxDKWFuowXrQOp11jsY8TuP__rBL%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAiz3pKNwvKudckFYMUfgoT0s96B0jfZ7ALHAu7rtE9HQ%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22jZeJpXGrRXQJU_-MSJ_v2A%3D%3D%22%7D) (German-speaking), [\#SimpleX-ES](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FJ5ES83pJimY2BRklS8fvy_iQwIU37xra%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA0F0STP6UqN_12_k2cjjTrIjFgBGeWhOAmbY1qlk3pnM%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22VmUU0fqmYdCRmVCyvStvHA%3D%3D%22%7D) (Spanish-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FxCHBE_6PBRMqNEpm4UQDHXb9cz-mN7dd%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAetqlcM7zTCRw-iatnwCrvpJSto7lq5Yv6AsBMWv7GSM%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22foO5Xw4hhjOa_x7zET7otw%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FVXQTB0J2lLjYkgjWByhl6-1qmb5fgZHh%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAI6JaEWezfSwvcoTEkk6au-gkjrXR2ew2OqZYMYBvayk%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22ORH9OEe8Duissh-hslfeVg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FqpHu0psOUdYfc11yQCzSyq5JhijrBzZT%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEACZ_7fbwlM45wl6cGif8cY47oPQ_AMdP0ATqOYLA6zHY%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%229uRQRTir3ealdcSfB0zsrw%3D%3D%22%7D) (Italian-speaking). You can join either by opening these links in the app or by opening them in a desktop browser and scanning the QR code. @@ -110,6 +110,15 @@ After you connect, you can [verify connection security code](./blog/20230103-sim Read about the app features and settings in the new [User guide](./docs/guide/README.md). +## Contribute + +We would love to have you join the development! You can help us with: + +- [share the color theme](./docs/THEMES.md) you use in Android app! +- writing a tutorial or recipes about hosting servers, chat bot automations, etc. +- contributing to SimpleX Chat knowledge-base. +- developing features - please connect to us via chat so we can help you get started. + ## Help translating SimpleX Chat Thanks to our users and [Weblate](https://hosted.weblate.org/engage/simplex-chat/), SimpleX Chat apps, website and documents are translated to many other languages. @@ -127,6 +136,7 @@ Join our translators to help SimpleX grow! |🇫🇮 fi|Suomi | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/fi/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fi/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fi/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fi/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fi/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fi/)|| |🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/fr/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fr/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fr/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fr/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fr/)|[✓](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/fr)| |🇮🇱 he|עִברִית | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/he/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/he/)
-||| +|🇭🇺 hu|Magyar | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/hu/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/hu/)
-||| |🇮🇹 it|Italiano |[unbranched](https://github.com/unbranched)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/it/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/it/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/it/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/it/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/it/)|| |🇯🇵 ja|日本語 | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/ja/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ja/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ja/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ja/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/ja/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/ja/)|| |🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/nl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/nl/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/nl/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/nl/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/nl/)|| @@ -134,21 +144,13 @@ Join our translators to help SimpleX grow! |🇧🇷 pt-BR|Português||[![android app](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/)
-|[![website](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/pt_BR/)|| |🇷🇺 ru|Русский ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/ru/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ru/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ru/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)||| |🇹🇭 th|ภาษาไทย |[titapa-punpun](https://github.com/titapa-punpun)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/th/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/th/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/th/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/th/)||| +|🇹🇷 tr|Türkçe | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/tr/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/tr/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/tr/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/tr/)||| |🇺🇦 uk|Українська| |[![android app](https://hosted.weblate.org/widgets/simplex-chat/uk/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/uk/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/uk/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/uk/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/uk/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/uk/)|| |🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)

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

[![website](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)|| Languages in progress: Arabic, Japanese, Korean, Portuguese and [others](https://hosted.weblate.org/projects/simplex-chat/#languages). We will be adding more languages as some of the already added are completed – please suggest new languages, review the [translation guide](./docs/TRANSLATIONS.md) and get in touch with us! -## Contribute - -We would love to have you join the development! You can help us with: - -- [share the color theme](./docs/THEMES.md) you use in Android app! -- writing a tutorial or recipes about hosting servers, chat bot automations, etc. -- contributing to SimpleX Chat knowledge-base. -- developing features - please connect to us via chat so we can help you get started. - -## Help us with donations +## Please support us with your donations Huge thank you to everybody who donated to SimpleX Chat! @@ -156,20 +158,21 @@ We are prioritizing users privacy and security - it would be impossible without Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure. -Your donations help us raise more funds – any amount, even the price of the cup of coffee, would make a big difference for us. +Your donations help us raise more funds - any amount, even the price of the cup of coffee, would make a big difference for us. It is possible to donate via: -- [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us. -- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies. -- Monero: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt -- Bitcoin: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG -- BCH: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG -- USDT: - - BNB Smart Chain: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2 - - Tron: TNnTrKLBmdy2Wn3cAQR98dAVvWhLskQGfW -- Ethereum: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2 -- Solana: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L +- [GitHub](https://github.com/sponsors/simplex-chat) (commission-free) or [OpenCollective](https://opencollective.com/simplex-chat) (~10% commission). +- BTC: bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u +- XMR: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt +- BCH: bitcoincash:qq6c8vfvxqrk6rhdysgvkhqc24sggkfsx5nqvdlqcg +- ETH: 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692 +- USDT (Ethereum): 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692 +- ZEC: t1fwjQW5gpFhDqXNhxqDWyF9j9WeKvVS5Jg +- ZEC shielded: u16rnvkflumf5uw9frngc2lymvmzgdr2mmc9unyu0l44unwfmdcpfm0axujd2w34ct3ye709azxsqge45705lpvvqu264ltzvfay55ygyq +- DOGE: D99pV4n9TrPxBPCkQGx4w4SMSa6QjRBxPf +- SOL: 7JCf5m3TiHmYKZVr6jCu1KeZVtb9Y1jRMQDU69p5ARnu +- please ask if you want to donate any other coins. Thank you, @@ -232,22 +235,28 @@ You can use SimpleX with your own servers and still communicate with people usin Recent and important updates: +[Mar 8, 2025. SimpleX Chat v6.3: new user experience and safety in public groups](./blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.md) + +[Jan 14, 2025. SimpleX network: large groups and privacy-preserving content moderation](./blog/20250114-simplex-network-large-groups-privacy-preserving-content-moderation.md) + +[Dec 10, 2024. SimpleX network: preset servers operated by Flux, business chats and more with v6.2 of the apps](./20241210-simplex-network-v6-2-servers-by-flux-business-chats.md) + +[Oct 14, 2024. SimpleX network: security review of protocols design by Trail of Bits, v6.1 released with better calls and user experience.](./blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md) + +[Aug 14, 2024. SimpleX network: the investment from Jack Dorsey and Asymmetric, v6.0 released with the new user experience and private message routing](./blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md) + +[Jun 4, 2024. SimpleX network: private message routing, v5.8 released with IP address protection and chat themes](./blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md) + +[Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md) + [Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](./blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md). -[Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](./blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md). - -[Jul 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](./blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md). - -[May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](./blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md). - [Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md). [Mar 1, 2023. SimpleX File Transfer Protocol – send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](./blog/20230301-simplex-file-transfer-protocol.md). [Nov 8, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). -[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md). - [All updates](./blog) ## :zap: Quick installation of a terminal app @@ -287,25 +296,28 @@ What is already implemented: 1. Instead of user profile identifiers used by all other platforms, even the most private ones, SimpleX uses [pairwise per-queue identifiers](./docs/GLOSSARY.md#pairwise-pseudonymous-identifier) (2 addresses for each unidirectional message queue, with an optional 3rd address for push notifications on iOS, 2 queues in each connection between the users). It makes observing the network graph on the application level more difficult, as for `n` users there can be up to `n * (n-1)` message queues. 2. [End-to-end encryption](./docs/GLOSSARY.md#end-to-end-encryption) in each message queue using [NaCl cryptobox](https://nacl.cr.yp.to/box.html). This is added to allow redundancy in the future (passing each message via several servers), to avoid having the same ciphertext in different queues (that would only be visible to the attacker if TLS is compromised). The encryption keys used for this encryption are not rotated, instead we are planning to rotate the queues. Curve25519 keys are used for key negotiation. 3. [Double ratchet](./docs/GLOSSARY.md#double-ratchet-algorithm) end-to-end encryption in each conversation between two users (or group members). This is the same algorithm that is used in Signal and many other messaging apps; it provides OTR messaging with [forward secrecy](./docs/GLOSSARY.md#forward-secrecy) (each message is encrypted by its own ephemeral key) and [break-in recovery](./docs/GLOSSARY.md#post-compromise-security) (the keys are frequently re-negotiated as part of the message exchange). Two pairs of Curve448 keys are used for the initial [key agreement](./docs/GLOSSARY.md#key-agreement-protocol), initiating party passes these keys via the connection link, accepting side - in the header of the confirmation message. -4. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well). -5. Several levels of [content padding](./docs/GLOSSARY.md#message-padding) to frustrate message size attacks. -6. All message metadata, including the time when the message was received by the server (rounded to a second) is sent to the recipients inside an encrypted envelope, so even if TLS is compromised it cannot be observed. -7. Only TLS 1.2/1.3 are allowed for client-server connections, limited to cryptographic algorithms: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448. -8. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key. -9. To protect your IP address all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details. -10. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings. -11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections. -12. Manual messaging queue rotations to move conversation to another SMP relay. -13. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html). -14. Local files encryption, except videos (to be added later). +4. [Post-quantum resistant key exchange](./docs/GLOSSARY.md#post-quantum-cryptography) in double ratchet protocol *on every ratchet step*. Read more in [this post](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md) and also see this [publication by Apple]( https://security.apple.com/blog/imessage-pq3/) explaining the need for post-quantum key rotation. +5. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well). +6. Several levels of [content padding](./docs/GLOSSARY.md#message-padding) to frustrate message size attacks. +7. All message metadata, including the time when the message was received by the server (rounded to a second) is sent to the recipients inside an encrypted envelope, so even if TLS is compromised it cannot be observed. +8. Only TLS 1.2/1.3 are allowed for client-server connections, limited to cryptographic algorithms: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448. +9. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key. +10. To protect your IP address from unknown messaging relays, and for per-message transport anonymity (compared with Tor/VPN per-connection anonymity), from v6.0 all SimpleX Chat clients use private message routing by default. Read more in [this post](./blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md#private-message-routing). +11. To protect your IP address from unknown file relays, when SOCKS proxy is not enabled SimpleX Chat clients ask for a confirmation before downloading the files from unknown servers. +12. To protect your IP address from known servers all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details. +13. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings. +14. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections. +15. Manual messaging queue rotations to move conversation to another SMP relay. +16. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html). +17. Local files encryption. +18. [Reproducible server builds](./docs/SERVER.md#reproduce-builds). We plan to add: -1. Senders' SMP relays and recipients' XFTP relays to reduce traffic and conceal IP addresses from the relays chosen, and potentially controlled, by another party. -2. Post-quantum resistant key exchange in double ratchet protocol. -3. Automatic message queue rotation and redundancy. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days). -4. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time. -5. Reproducible builds – this is the limitation of the development stack, but we will be investing into solving this problem. Users can still build all applications and services from the source code. +1. Automatic message queue rotation and redundancy. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days). +2. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time. +3. Reproducible clients builds – this is a complex problem, but we are aiming to have it in 2025 at least partially. +4. Recipients' XFTP relays to reduce traffic and conceal IP addresses from the relays chosen, and potentially controlled, by another party. ## For developers @@ -369,12 +381,16 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A - ✅ Desktop client. - ✅ Encryption of local files stored in the app. - ✅ Using mobile profiles from the desktop app. -- 🏗 Improve experience for the new users. -- 🏗 Post-quantum resistant key exchange in double ratchet protocol. +- ✅ Private notes. +- ✅ Improve sending videos (including encryption of locally stored videos). +- ✅ Post-quantum resistant key exchange in double ratchet protocol. +- ✅ Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic). +- ✅ Support multiple network operators in the app. - 🏗 Large groups, communities and public channels. -- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic). +- 🏗 Short links to connect and join groups. +- 🏗 Improve stability and reduce battery usage. +- 🏗 Improve experience for the new users. - Privacy & security slider - a simple way to set all settings at once. -- Improve sending videos (including encryption of locally stored videos). - SMP queue redundancy and rotation (manual is supported). - Include optional message into connection request sent via contact address. - Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.). @@ -392,7 +408,9 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A [SimpleX protocols and security model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) was reviewed, and had many breaking changes and improvements in v1.0.0. -The security audit was performed in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2.0 – see [the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). +The implementation security assessment of SimpleX cryptography and networking was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about) – see [the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). + +The cryptographic review of SimpleX protocols was done in July 2024 by Trail of Bits – see [the announcement](./blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md). SimpleX Chat is still a relatively early stage platform (the mobile apps were released in March 2022), so you may discover some bugs and missing features. We would really appreciate if you let us know anything that needs to be fixed or improved. @@ -402,13 +420,13 @@ We have never provided or have been requested access to our servers or any infor We do not log IP addresses of the users and we do not perform any traffic correlation on our servers. If transport level security is critical you must use Tor or some other similar network to access messaging servers. We will be improving the client applications to reduce the opportunities for traffic correlation. -Please read more in [Terms & privacy policy](./PRIVACY.md). +Please read more in [Privacy Policy](./PRIVACY.md). ## Security contact -To report a security vulnerability, please send us email to chat@simplex.chat. We will coordinate the fix and disclosure. Please do NOT report security vulnerabilities via GitHub issues. +Please see our [Security Policy](./docs/SECURITY.md) on how to report security vulnerabilities to us. We will coordinate the fix and disclosure. -Please treat any findings of possible traffic correlation attacks allowing to correlate two different conversations to the same user, other than covered in [the threat model](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#threat-model), as security vulnerabilities, and follow this disclosure process. +Please do NOT report security vulnerabilities via GitHub issues. ## License diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index 145e362797..3f6998c9ec 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -9,33 +9,18 @@ import Foundation import UIKit import SimpleXChat +import SwiftUI class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { logger.debug("AppDelegate: didFinishLaunchingWithOptions") application.registerForRemoteNotifications() - if #available(iOS 17.0, *) { trackKeyboard() } + removePasscodesIfReinstalled() + prepareForLaunch() + deleteOldChatArchive() return true } - @available(iOS 17.0, *) - private func trackKeyboard() { - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) - } - - @available(iOS 17.0, *) - @objc func keyboardWillShow(_ notification: Notification) { - if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { - ChatModel.shared.keyboardHeight = keyboardFrame.cgRectValue.height - } - } - - @available(iOS 17.0, *) - @objc func keyboardWillHide(_ notification: Notification) { - ChatModel.shared.keyboardHeight = 0 - } - func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let token = deviceToken.map { String(format: "%02hhx", $0) }.joined() logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)") @@ -69,7 +54,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { try await apiVerifyToken(token: token, nonce: nonce, code: verification) m.tokenStatus = .active } catch { - if let cr = error as? ChatResponse, case .chatCmdError(_, .errorAgent(.NTF(.AUTH))) = cr { + if let cr = error as? ChatError, case .errorAgent(.NTF(.AUTH)) = cr { m.tokenStatus = .expired } logger.error("AppDelegate: didReceiveRemoteNotification: apiVerifyToken or apiIntervalNofication error: \(responseError(error))") @@ -122,6 +107,23 @@ class AppDelegate: NSObject, UIApplicationDelegate { BGManager.shared.receiveMessages(complete) } + private func removePasscodesIfReinstalled() { + // Check for the database existence, because app and self destruct passcodes + // will be saved and restored by iOS when a user deletes and re-installs the app. + // In this case the database and settings will be deleted, but the passcodes won't be. + // Deleting passcodes ensures that the user will not get stuck on "Opening app..." screen. + if (kcAppPassword.get() != nil || kcSelfDestructPassword.get() != nil) && + !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) && !hasDatabase() { + _ = kcAppPassword.remove() + _ = kcSelfDestructPassword.remove() + _ = kcDatabasePassword.remove() + } + } + + private func prepareForLaunch() { + try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) + } + static func keepScreenOn(_ on: Bool) { UIApplication.shared.isIdleTimerDisabled = on } @@ -129,13 +131,79 @@ class AppDelegate: NSObject, UIApplicationDelegate { class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate { var window: UIWindow? + static var windowStatic: UIWindow? var windowScene: UIWindowScene? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + UITableView.appearance().backgroundColor = .clear guard let windowScene = scene as? UIWindowScene else { return } self.windowScene = windowScene window = windowScene.keyWindow - window?.tintColor = UIColor(cgColor: getUIAccentColorDefault()) - window?.overrideUserInterfaceStyle = getUserInterfaceStyleDefault() + SceneDelegate.windowStatic = windowScene.keyWindow + migrateAccentColorAndTheme() + ThemeManager.applyTheme(currentThemeDefault.get()) + ThemeManager.adjustWindowStyle() + } + + private func migrateAccentColorAndTheme() { + let defs = UserDefaults.standard + /// For checking migration +// themeOverridesDefault.set([]) +// currentThemeDefault.set(DefaultTheme.SYSTEM_THEME_NAME) +// defs.set(0.5, forKey: DEFAULT_ACCENT_COLOR_RED) +// defs.set(0.3, forKey: DEFAULT_ACCENT_COLOR_GREEN) +// defs.set(0.8, forKey: DEFAULT_ACCENT_COLOR_BLUE) + + let userInterfaceStyle = getUserInterfaceStyleDefault() + if defs.double(forKey: DEFAULT_ACCENT_COLOR_GREEN) == 0 && userInterfaceStyle == .unspecified { + // No migration needed or already migrated + return + } + + let defaultAccentColor = Color(cgColor: CGColor(red: 0.000, green: 0.533, blue: 1.000, alpha: 1)) + let accentColor = Color(cgColor: getUIAccentColorDefault()) + if accentColor != defaultAccentColor { + let colors = ThemeColors(primary: accentColor.toReadableHex()) + var overrides = themeOverridesDefault.get() + var themeIds = currentThemeIdsDefault.get() + switch userInterfaceStyle { + case .light: + let light = ThemeOverrides(base: DefaultTheme.LIGHT, colors: colors, wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename)) + overrides.append(light) + themeOverridesDefault.set(overrides) + themeIds[DefaultTheme.LIGHT.themeName] = light.themeId + currentThemeIdsDefault.set(themeIds) + ThemeManager.applyTheme(DefaultTheme.LIGHT.themeName) + case .dark: + let dark = ThemeOverrides(base: DefaultTheme.DARK, colors: colors, wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename)) + overrides.append(dark) + themeOverridesDefault.set(overrides) + themeIds[DefaultTheme.DARK.themeName] = dark.themeId + currentThemeIdsDefault.set(themeIds) + ThemeManager.applyTheme(DefaultTheme.DARK.themeName) + case .unspecified: + let light = ThemeOverrides(base: DefaultTheme.LIGHT, colors: colors, wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename)) + let dark = ThemeOverrides(base: DefaultTheme.DARK, colors: colors, wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename)) + overrides.append(light) + overrides.append(dark) + themeOverridesDefault.set(overrides) + themeIds[DefaultTheme.LIGHT.themeName] = light.themeId + themeIds[DefaultTheme.DARK.themeName] = dark.themeId + currentThemeIdsDefault.set(themeIds) + ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME) + @unknown default: () + } + } else if userInterfaceStyle != .unspecified { + let themeName = switch userInterfaceStyle { + case .light: DefaultTheme.LIGHT.themeName + case .dark: DefaultTheme.DARK.themeName + default: DefaultTheme.SYSTEM_THEME_NAME + } + ThemeManager.applyTheme(themeName) + } + defs.removeObject(forKey: DEFAULT_ACCENT_COLOR_RED) + defs.removeObject(forKey: DEFAULT_ACCENT_COLOR_GREEN) + defs.removeObject(forKey: DEFAULT_ACCENT_COLOR_BLUE) + defs.removeObject(forKey: DEFAULT_USER_INTERFACE_STYLE) } } diff --git a/apps/ios/Shared/Assets.xcassets/checkmark.2.symbolset/Contents.json b/apps/ios/Shared/Assets.xcassets/checkmark.2.symbolset/Contents.json new file mode 100644 index 0000000000..8e38b499dd --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/checkmark.2.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info": { + "author": "xcode", + "version": 1 + }, + "symbols": [ + { + "filename": "checkmark.2.svg", + "idiom": "universal" + } + ] +} \ No newline at end of file diff --git a/apps/ios/Shared/Assets.xcassets/checkmark.2.symbolset/checkmark.2.svg b/apps/ios/Shared/Assets.xcassets/checkmark.2.symbolset/checkmark.2.svg new file mode 100644 index 0000000000..577fa1db76 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/checkmark.2.symbolset/checkmark.2.svg @@ -0,0 +1,227 @@ + + + checkmark.2 + + + + + + + Weight/Scale Variations + + + Ultralight + + + Thin + + + Light + + + Regular + + + Medium + + + Semibold + + + Bold + + + Heavy + + + Black + + + + + + + + + + + + + Design Variations + + + Symbols are supported in up to nine weights and three scales. + + + For optimal layout with text and other symbols, vertically align + + + symbols with the adjacent text. + + + + + + + + + Margins + + + Leading and trailing margins on the left and right side of each symbol + + + + can be adjusted by modifying the x-location of the margin guidelines. + + + + Modifications are automatically applied proportionally to all + + + scales and weights. + + + + + + Exporting + + + Symbols should be outlined when exporting to ensure the + + + design is preserved when submitting to Xcode. + + + Template v.5.0 + + + Requires Xcode 15 or greater + + + Generated from double.checkmark + + + Typeset at 100.0 points + + + Small + + + Medium + + + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/ios/Shared/Assets.xcassets/checkmark.wide.symbolset/Contents.json b/apps/ios/Shared/Assets.xcassets/checkmark.wide.symbolset/Contents.json new file mode 100644 index 0000000000..11a91cb811 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/checkmark.wide.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info": { + "author": "xcode", + "version": 1 + }, + "symbols": [ + { + "filename": "checkmark.wide.svg", + "idiom": "universal" + } + ] +} \ No newline at end of file diff --git a/apps/ios/Shared/Assets.xcassets/checkmark.wide.symbolset/checkmark.wide.svg b/apps/ios/Shared/Assets.xcassets/checkmark.wide.symbolset/checkmark.wide.svg new file mode 100644 index 0000000000..b5dfc6b3de --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/checkmark.wide.symbolset/checkmark.wide.svg @@ -0,0 +1,218 @@ + + + checkmark.wide + + + + + + + Weight/Scale Variations + + + Ultralight + + + Thin + + + Light + + + Regular + + + Medium + + + Semibold + + + Bold + + + Heavy + + + Black + + + + + + + + + + + + + Design Variations + + + Symbols are supported in up to nine weights and three scales. + + + For optimal layout with text and other symbols, vertically align + + + symbols with the adjacent text. + + + + + + + + + Margins + + + Leading and trailing margins on the left and right side of each symbol + + + + can be adjusted by modifying the x-location of the margin guidelines. + + + + Modifications are automatically applied proportionally to all + + + scales and weights. + + + + + + Exporting + + + Symbols should be outlined when exporting to ensure the + + + design is preserved when submitting to Xcode. + + + Template v.5.0 + + + Requires Xcode 15 or greater + + + Generated from double.checkmark + + + Typeset at 100.0 points + + + Small + + + Medium + + + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/ios/Shared/Assets.xcassets/flux_logo-light.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/flux_logo-light.imageset/Contents.json new file mode 100644 index 0000000000..d3a15f9a33 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/flux_logo-light.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Flux_logo_blue_white.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/flux_logo-light.imageset/Flux_logo_blue_white.png b/apps/ios/Shared/Assets.xcassets/flux_logo-light.imageset/Flux_logo_blue_white.png new file mode 100644 index 0000000000..e1d6dda4fe Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/flux_logo-light.imageset/Flux_logo_blue_white.png differ diff --git a/apps/ios/Shared/Assets.xcassets/flux_logo.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/flux_logo.imageset/Contents.json new file mode 100644 index 0000000000..ad18e60448 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/flux_logo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Flux_logo_blue.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/flux_logo.imageset/Flux_logo_blue.png b/apps/ios/Shared/Assets.xcassets/flux_logo.imageset/Flux_logo_blue.png new file mode 100644 index 0000000000..87f1373d75 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/flux_logo.imageset/Flux_logo_blue.png differ diff --git a/apps/ios/Shared/Assets.xcassets/flux_logo_symbol.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/flux_logo_symbol.imageset/Contents.json new file mode 100644 index 0000000000..16686bdf80 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/flux_logo_symbol.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Flux_symbol_blue-white.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/flux_logo_symbol.imageset/Flux_symbol_blue-white.png b/apps/ios/Shared/Assets.xcassets/flux_logo_symbol.imageset/Flux_symbol_blue-white.png new file mode 100644 index 0000000000..0793b0ee85 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/flux_logo_symbol.imageset/Flux_symbol_blue-white.png differ diff --git a/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/Contents.json new file mode 100644 index 0000000000..cb29f09fe1 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "vertical_logo_x1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "vertical_logo_x2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "vertical_logo_x3.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x1.png b/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x1.png new file mode 100644 index 0000000000..f916e43ea9 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x1.png differ diff --git a/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x2.png b/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x2.png new file mode 100644 index 0000000000..bb35878f0c Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x2.png differ diff --git a/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x3.png b/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x3.png new file mode 100644 index 0000000000..c55f481b36 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x3.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/Contents.json new file mode 100644 index 0000000000..a1747ab6ba --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "wallpaper_cats@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "wallpaper_cats@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "wallpaper_cats@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/wallpaper_cats@1x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/wallpaper_cats@1x.png new file mode 100644 index 0000000000..7d4624c3f9 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/wallpaper_cats@1x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/wallpaper_cats@2x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/wallpaper_cats@2x.png new file mode 100644 index 0000000000..1015139393 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/wallpaper_cats@2x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/wallpaper_cats@3x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/wallpaper_cats@3x.png new file mode 100644 index 0000000000..9bff3eb3d0 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/wallpaper_cats@3x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/Contents.json new file mode 100644 index 0000000000..c6bc439be2 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "wallpaper_flowers@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "wallpaper_flowers@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "wallpaper_flowers@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/wallpaper_flowers@1x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/wallpaper_flowers@1x.png new file mode 100644 index 0000000000..965f552599 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/wallpaper_flowers@1x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/wallpaper_flowers@2x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/wallpaper_flowers@2x.png new file mode 100644 index 0000000000..0cb219acd3 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/wallpaper_flowers@2x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/wallpaper_flowers@3x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/wallpaper_flowers@3x.png new file mode 100644 index 0000000000..59246eb50d Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/wallpaper_flowers@3x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/Contents.json new file mode 100644 index 0000000000..556d01a6f2 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "wallpaper_hearts@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "wallpaper_hearts@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "wallpaper_hearts@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/wallpaper_hearts@1x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/wallpaper_hearts@1x.png new file mode 100644 index 0000000000..780ff13513 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/wallpaper_hearts@1x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/wallpaper_hearts@2x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/wallpaper_hearts@2x.png new file mode 100644 index 0000000000..cee89e57d9 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/wallpaper_hearts@2x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/wallpaper_hearts@3x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/wallpaper_hearts@3x.png new file mode 100644 index 0000000000..35da7c7aed Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/wallpaper_hearts@3x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/Contents.json new file mode 100644 index 0000000000..aba5903ec0 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "wallpaper_kids@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "wallpaper_kids@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "wallpaper_kids@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/wallpaper_kids@1x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/wallpaper_kids@1x.png new file mode 100644 index 0000000000..83e48b4f78 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/wallpaper_kids@1x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/wallpaper_kids@2x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/wallpaper_kids@2x.png new file mode 100644 index 0000000000..1927c2fe2a Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/wallpaper_kids@2x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/wallpaper_kids@3x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/wallpaper_kids@3x.png new file mode 100644 index 0000000000..f5f15d3643 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/wallpaper_kids@3x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/Contents.json new file mode 100644 index 0000000000..59c209b134 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "wallpaper_school@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "wallpaper_school@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "wallpaper_school@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/wallpaper_school@1x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/wallpaper_school@1x.png new file mode 100644 index 0000000000..c95ac60b6e Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/wallpaper_school@1x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/wallpaper_school@2x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/wallpaper_school@2x.png new file mode 100644 index 0000000000..81a3a3d94d Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/wallpaper_school@2x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/wallpaper_school@3x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/wallpaper_school@3x.png new file mode 100644 index 0000000000..f6e1cce383 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/wallpaper_school@3x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/Contents.json new file mode 100644 index 0000000000..4e56988263 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "wallpaper_travel@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "wallpaper_travel@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "wallpaper_travel@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/wallpaper_travel@1x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/wallpaper_travel@1x.png new file mode 100644 index 0000000000..c1d825b86e Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/wallpaper_travel@1x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/wallpaper_travel@2x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/wallpaper_travel@2x.png new file mode 100644 index 0000000000..d640f10c7c Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/wallpaper_travel@2x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/wallpaper_travel@3x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/wallpaper_travel@3x.png new file mode 100644 index 0000000000..64ec137331 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/wallpaper_travel@3x.png differ diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index d7b9fef218..2ad8d546f2 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -9,11 +9,24 @@ import SwiftUI import Intents import SimpleXChat +private enum NoticesSheet: Identifiable { + case whatsNew(updatedConditions: Bool) + + var id: String { + switch self { + case .whatsNew: return "whatsNew" + } + } +} + struct ContentView: View { @EnvironmentObject var chatModel: ChatModel @ObservedObject var alertManager = AlertManager.shared @ObservedObject var callController = CallController.shared + @ObservedObject var appSheetState = AppSheetState.shared @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme + @EnvironmentObject var sceneDelegate: SceneDelegate var contentAccessAuthenticationExtended: Bool @@ -27,11 +40,15 @@ struct ContentView: View { @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false @AppStorage(DEFAULT_NOTIFICATION_ALERT_SHOWN) private var notificationAlertShown = false - @State private var showSettings = false - @State private var showWhatsNew = false + @State private var noticesShown = false + @State private var noticesSheetItem: NoticesSheet? = nil @State private var showChooseLAMode = false @State private var showSetPasscode = false + @State private var waitingForOrPassedAuth = true @State private var chatListActionSheet: ChatListActionSheet? = nil + @State private var chatListUserPickerSheet: UserPickerSheet? = nil + + private let callTopPadding: CGFloat = 40 private enum ChatListActionSheet: Identifiable { case planAndConnectSheet(sheet: PlanAndConnectActionSheet) @@ -48,19 +65,45 @@ struct ContentView: View { } var body: some View { + if #available(iOS 16.0, *) { + allViews() + .scrollContentBackground(.hidden) + } else { + // on iOS 15 scroll view background disabled in SceneDelegate + allViews() + } + } + + func allViews() -> some View { ZStack { + let showCallArea = chatModel.activeCall != nil && chatModel.activeCall?.callState != .waitCapabilities && chatModel.activeCall?.callState != .invitationAccepted // contentView() has to be in a single branch, so that enabling authentication doesn't trigger re-rendering and close settings. // i.e. with separate branches like this settings are closed: `if prefPerformLA { ... contentView() ... } else { contentView() } if !prefPerformLA || accessAuthenticated { contentView() + .padding(.top, showCallArea ? callTopPadding : 0) } else { lockButton() + .padding(.top, showCallArea ? callTopPadding : 0) } + + if showCallArea, let call = chatModel.activeCall { + VStack { + activeCallInteractiveArea(call) + Spacer() + } + } + if chatModel.showCallView, let call = chatModel.activeCall { callView(call) } - if !showSettings, let la = chatModel.laRequest { + + if chatListUserPickerSheet == nil, let la = chatModel.laRequest { LocalAuthView(authRequest: la) + .onDisappear { + // this flag is separate from accessAuthenticated to show initializationView while we wait for authentication + waitingForOrPassedAuth = accessAuthenticated + } } else if showSetPasscode { SetAppPasscodeView { chatModel.contentViewAccessAuthenticated = true @@ -73,15 +116,11 @@ struct ContentView: View { showSetPasscode = false alertManager.showAlert(laPasscodeNotSetAlert()) } - } - if chatModel.chatDbStatus == nil { + } else if chatModel.chatDbStatus == nil && AppChatState.shared.value != .stopped && waitingForOrPassedAuth { initializationView() } } .alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! } - .sheet(isPresented: $showSettings) { - SettingsView(showSettings: $showSettings) - } .confirmationDialog("SimpleX Lock mode", isPresented: $showChooseLAMode, titleVisibility: .visible) { Button("System authentication") { initialEnableLA() } Button("Passcode entry") { showSetPasscode = true } @@ -120,6 +159,17 @@ struct ContentView: View { break } } + .onAppear { + reactOnDarkThemeChanges(systemInDarkThemeCurrently) + } + .onChange(of: colorScheme) { scheme in + // It's needed to update UI colors when iOS wants to make screenshot after going to background, + // so when a user changes his global theme from dark to light or back, the app will adapt to it + reactOnDarkThemeChanges(scheme == .dark) + } + .onChange(of: theme.name) { _ in + ThemeManager.adjustWindowStyle() + } } @ViewBuilder private func contentView() -> some View { @@ -131,11 +181,11 @@ struct ContentView: View { if case .onboardingComplete = step, chatModel.currentUser != nil { mainView() - .actionSheet(item: $chatListActionSheet) { sheet in - switch sheet { - case let .planAndConnectSheet(sheet): return planAndConnectActionSheet(sheet, dismiss: false) + .actionSheet(item: $chatListActionSheet) { sheet in + switch sheet { + case let .planAndConnectSheet(sheet): return planAndConnectActionSheet(sheet, dismiss: false) + } } - } } else { OnboardingView(onboarding: step) } @@ -159,6 +209,40 @@ struct ContentView: View { } } + private func activeCallInteractiveArea(_ call: Call) -> some View { + HStack { + Text(call.contact.displayName).font(.body).foregroundColor(.white) + Spacer() + CallDuration(call: call) + } + .padding(.horizontal) + .frame(height: callTopPadding) + .background(Color(uiColor: UIColor(red: 47/255, green: 208/255, blue: 88/255, alpha: 1))) + .onTapGesture { + chatModel.activeCallViewIsCollapsed = false + } + } + + struct CallDuration: View { + let call: Call + @State var text: String = "" + @State var timer: Timer? = nil + + var body: some View { + Text(text).frame(minWidth: text.count <= 5 ? 52 : 77, alignment: .leading).offset(x: 4).font(.body).foregroundColor(.white) + .onAppear { + timer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { timer in + if let connectedAt = call.connectedAt { + text = durationText(Int(Date.now.timeIntervalSince1970 - connectedAt.timeIntervalSince1970)) + } + } + } + .onDisappear { + _ = timer?.invalidate() + } + } + } + private func lockButton() -> some View { Button(action: authenticateContentViewAccess) { Label("Unlock", systemImage: "lock") } } @@ -172,32 +256,47 @@ struct ContentView: View { .frame(maxWidth: .infinity, maxHeight: .infinity ) .background( Rectangle() - .fill(.background) - ) + .fill(theme.colors.background) + ) } private func mainView() -> some View { ZStack(alignment: .top) { - ChatListView(showSettings: $showSettings).privacySensitive(protectScreen) + ChatListView(activeUserPickerSheet: $chatListUserPickerSheet) + .redacted(reason: appSheetState.redactionReasons(protectScreen)) .onAppear { requestNtfAuthorization() // Local Authentication notice is to be shown on next start after onboarding is complete - if (!prefLANoticeShown && prefShowLANotice && !chatModel.chats.isEmpty) { + if (!prefLANoticeShown && prefShowLANotice && chatModel.chats.count > 2) { prefLANoticeShown = true alertManager.showAlert(laNoticeAlert()) } else if !chatModel.showCallView && CallController.shared.activeCallInvitation == nil { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - if !showWhatsNew { - showWhatsNew = shouldShowWhatsNew() + if !noticesShown { + let showWhatsNew = shouldShowWhatsNew() + let showUpdatedConditions = chatModel.conditions.conditionsAction?.showNotice ?? false + noticesShown = showWhatsNew || showUpdatedConditions + if showWhatsNew || showUpdatedConditions { + noticesSheetItem = .whatsNew(updatedConditions: showUpdatedConditions) + } } } } prefShowLANotice = true connectViaUrl() + showReRegisterTokenAlert() } .onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() } - .sheet(isPresented: $showWhatsNew) { - WhatsNewView() + .onChange(of: chatModel.reRegisterTknStatus) { _ in showReRegisterTokenAlert() } + .sheet(item: $noticesSheetItem) { item in + switch item { + case let .whatsNew(updatedConditions): + WhatsNewView(updatedConditions: updatedConditions) + .modifier(ThemedBackground()) + .if(updatedConditions) { v in + v.task { await setConditionsNotified_() } + } + } } if chatModel.setDeliveryReceipts { SetDeliveryReceiptsView() @@ -207,6 +306,21 @@ struct ContentView: View { .onContinueUserActivity("INStartCallIntent", perform: processUserActivity) .onContinueUserActivity("INStartAudioCallIntent", perform: processUserActivity) .onContinueUserActivity("INStartVideoCallIntent", perform: processUserActivity) + .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in + if let url = userActivity.webpageURL { + logger.debug("onContinueUserActivity.NSUserActivityTypeBrowsingWeb: \(url)") + chatModel.appOpenUrl = url + } + } + } + + private func setConditionsNotified_() async { + do { + let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId + try await setConditionsNotified(conditionsId: conditionsId) + } catch let error { + logger.error("setConditionsNotified error: \(responseError(error))") + } } private func processUserActivity(_ activity: NSUserActivity) { @@ -225,9 +339,18 @@ struct ContentView: View { if let contactId = contacts?.first?.personHandle?.value, let chat = chatModel.getChat(contactId), case let .direct(contact) = chat.chatInfo { - logger.debug("callToRecentContact: schedule call") - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - CallController.shared.startCall(contact, mediaType) + let activeCall = chatModel.activeCall + // This line works when a user clicks on a video button in CallKit UI while in call. + // The app tries to make another call to the same contact and overwite activeCall instance making its state broken + if let activeCall, contactId == activeCall.contact.id, mediaType == .video, !activeCall.hasVideo { + Task { + await chatModel.callCommand.processCommand(.media(source: .camera, enable: true)) + } + } else if activeCall == nil { + logger.debug("callToRecentContact: schedule call") + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + CallController.shared.startCall(contact, mediaType) + } } } } @@ -320,12 +443,12 @@ struct ContentView: View { } func connectViaUrl() { - dismissAllSheets() { - let m = ChatModel.shared - if let url = m.appOpenUrl { - m.appOpenUrl = nil + let m = ChatModel.shared + if let url = m.appOpenUrl { + m.appOpenUrl = nil + dismissAllSheets() { var path = url.path - if (path == "/contact" || path == "/invitation") { + if (path == "/contact" || path == "/invitation" || path == "/a" || path == "/c" || path == "/g" || path == "/i") { path.removeFirst() let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)") planAndConnect( @@ -342,6 +465,21 @@ struct ContentView: View { } } + func showReRegisterTokenAlert() { + dismissAllSheets() { + let m = ChatModel.shared + if let errorTknStatus = m.reRegisterTknStatus, let token = chatModel.deviceToken { + chatModel.reRegisterTknStatus = nil + AlertManager.shared.showAlert(Alert( + title: Text("Notifications error"), + message: Text(tokenStatusInfo(errorTknStatus, register: true)), + primaryButton: .default(Text("Register")) { reRegisterToken(token: token) }, + secondaryButton: .cancel() + )) + } + } + } + private func showPlanAndConnectAlert(_ alert: PlanAndConnectAlert) { AlertManager.shared.showAlert(planAndConnectAlert(alert, dismiss: false)) } diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift new file mode 100644 index 0000000000..3bf4cb7b56 --- /dev/null +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -0,0 +1,2281 @@ +// +// APITypes.swift +// SimpleX +// +// Created by EP on 01/05/2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SimpleXChat +import SwiftUI + +// some constructors are used in SEChatCommand or NSEChatCommand types as well - they must be syncronised +enum ChatCommand: ChatCmdProtocol { + case showActiveUser + case createActiveUser(profile: Profile?, pastTimestamp: Bool) + case listUsers + case apiSetActiveUser(userId: Int64, viewPwd: String?) + case setAllContactReceipts(enable: Bool) + case apiSetUserContactReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) + case apiSetUserGroupReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) + case apiHideUser(userId: Int64, viewPwd: String) + case apiUnhideUser(userId: Int64, viewPwd: String) + case apiMuteUser(userId: Int64) + case apiUnmuteUser(userId: Int64) + case apiDeleteUser(userId: Int64, delSMPQueues: Bool, viewPwd: String?) + case startChat(mainApp: Bool, enableSndFiles: Bool) + case checkChatRunning + case apiStopChat + case apiActivateChat(restoreChat: Bool) + case apiSuspendChat(timeoutMicroseconds: Int) + case apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) + case apiSetEncryptLocalFiles(enable: Bool) + case apiExportArchive(config: ArchiveConfig) + case apiImportArchive(config: ArchiveConfig) + case apiDeleteStorage + case apiStorageEncryption(config: DBEncryptionConfig) + case testStorageEncryption(key: String) + case apiSaveSettings(settings: AppSettings) + case apiGetSettings(settings: AppSettings) + case apiGetChatTags(userId: Int64) + case apiGetChats(userId: Int64) + case apiGetChat(chatId: ChatId, pagination: ChatPagination, search: String) + case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) + case apiSendMessages(type: ChatType, id: Int64, live: Bool, ttl: Int?, composedMessages: [ComposedMessage]) + case apiCreateChatTag(tag: ChatTagData) + case apiSetChatTags(type: ChatType, id: Int64, tagIds: [Int64]) + case apiDeleteChatTag(tagId: Int64) + case apiUpdateChatTag(tagId: Int64, tagData: ChatTagData) + case apiReorderChatTags(tagIds: [Int64]) + case apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage]) + case apiReportMessage(groupId: Int64, chatItemId: Int64, reportReason: ReportReason, reportText: String) + case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool) + case apiDeleteChatItem(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) + case apiDeleteMemberChatItem(groupId: Int64, itemIds: [Int64]) + case apiArchiveReceivedReports(groupId: Int64) + case apiDeleteReceivedReports(groupId: Int64, itemIds: [Int64], mode: CIDeleteMode) + case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction) + case apiGetReactionMembers(userId: Int64, groupId: Int64, itemId: Int64, reaction: MsgReaction) + case apiPlanForwardChatItems(toChatType: ChatType, toChatId: Int64, itemIds: [Int64]) + case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?) + case apiGetNtfToken + case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) + case apiVerifyToken(token: DeviceToken, nonce: String, code: String) + case apiCheckToken(token: DeviceToken) + case apiDeleteToken(token: DeviceToken) + case apiGetNtfConns(nonce: String, encNtfInfo: String) + case apiGetConnNtfMessages(connMsgReqs: [ConnMsgReq]) + case apiNewGroup(userId: Int64, incognito: Bool, groupProfile: GroupProfile) + case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) + case apiJoinGroup(groupId: Int64) + case apiMembersRole(groupId: Int64, memberIds: [Int64], memberRole: GroupMemberRole) + case apiBlockMembersForAll(groupId: Int64, memberIds: [Int64], blocked: Bool) + case apiRemoveMembers(groupId: Int64, memberIds: [Int64], withMessages: Bool) + case apiLeaveGroup(groupId: Int64) + case apiListMembers(groupId: Int64) + case apiUpdateGroupProfile(groupId: Int64, groupProfile: GroupProfile) + case apiCreateGroupLink(groupId: Int64, memberRole: GroupMemberRole, short: Bool) + case apiGroupLinkMemberRole(groupId: Int64, memberRole: GroupMemberRole) + case apiDeleteGroupLink(groupId: Int64) + case apiGetGroupLink(groupId: Int64) + case apiCreateMemberContact(groupId: Int64, groupMemberId: Int64) + case apiSendMemberContactInvitation(contactId: Int64, msg: MsgContent) + case apiTestProtoServer(userId: Int64, server: String) + case apiGetServerOperators + case apiSetServerOperators(operators: [ServerOperator]) + case apiGetUserServers(userId: Int64) + case apiSetUserServers(userId: Int64, userServers: [UserOperatorServers]) + case apiValidateServers(userId: Int64, userServers: [UserOperatorServers]) + case apiGetUsageConditions + case apiSetConditionsNotified(conditionsId: Int64) + case apiAcceptConditions(conditionsId: Int64, operatorIds: [Int64]) + case apiSetChatItemTTL(userId: Int64, seconds: Int64) + case apiGetChatItemTTL(userId: Int64) + case apiSetChatTTL(userId: Int64, type: ChatType, id: Int64, seconds: Int64?) + case apiSetNetworkConfig(networkConfig: NetCfg) + case apiGetNetworkConfig + case apiSetNetworkInfo(networkInfo: UserNetworkInfo) + case reconnectAllServers + case reconnectServer(userId: Int64, smpServer: String) + case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) + case apiSetMemberSettings(groupId: Int64, groupMemberId: Int64, memberSettings: GroupMemberSettings) + case apiContactInfo(contactId: Int64) + case apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64) + case apiContactQueueInfo(contactId: Int64) + case apiGroupMemberQueueInfo(groupId: Int64, groupMemberId: Int64) + case apiSwitchContact(contactId: Int64) + case apiSwitchGroupMember(groupId: Int64, groupMemberId: Int64) + case apiAbortSwitchContact(contactId: Int64) + case apiAbortSwitchGroupMember(groupId: Int64, groupMemberId: Int64) + case apiSyncContactRatchet(contactId: Int64, force: Bool) + case apiSyncGroupMemberRatchet(groupId: Int64, groupMemberId: Int64, force: Bool) + case apiGetContactCode(contactId: Int64) + case apiGetGroupMemberCode(groupId: Int64, groupMemberId: Int64) + case apiVerifyContact(contactId: Int64, connectionCode: String?) + case apiVerifyGroupMember(groupId: Int64, groupMemberId: Int64, connectionCode: String?) + case apiAddContact(userId: Int64, short: Bool, incognito: Bool) + case apiSetConnectionIncognito(connId: Int64, incognito: Bool) + case apiChangeConnectionUser(connId: Int64, userId: Int64) + case apiConnectPlan(userId: Int64, connLink: String) + case apiConnect(userId: Int64, incognito: Bool, connLink: CreatedConnLink) + case apiConnectContactViaAddress(userId: Int64, incognito: Bool, contactId: Int64) + case apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode) + case apiClearChat(type: ChatType, id: Int64) + case apiListContacts(userId: Int64) + case apiUpdateProfile(userId: Int64, profile: Profile) + case apiSetContactPrefs(contactId: Int64, preferences: Preferences) + case apiSetContactAlias(contactId: Int64, localAlias: String) + case apiSetGroupAlias(groupId: Int64, localAlias: String) + case apiSetConnectionAlias(connId: Int64, localAlias: String) + case apiSetUserUIThemes(userId: Int64, themes: ThemeModeOverrides?) + case apiSetChatUIThemes(chatId: String, themes: ThemeModeOverrides?) + case apiCreateMyAddress(userId: Int64, short: Bool) + case apiDeleteMyAddress(userId: Int64) + case apiShowMyAddress(userId: Int64) + case apiSetProfileAddress(userId: Int64, on: Bool) + case apiAddressAutoAccept(userId: Int64, autoAccept: AutoAccept?) + case apiAcceptContact(incognito: Bool, contactReqId: Int64) + case apiRejectContact(contactReqId: Int64) + // WebRTC calls + case apiSendCallInvitation(contact: Contact, callType: CallType) + case apiRejectCall(contact: Contact) + case apiSendCallOffer(contact: Contact, callOffer: WebRTCCallOffer) + case apiSendCallAnswer(contact: Contact, answer: WebRTCSession) + case apiSendCallExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo) + case apiEndCall(contact: Contact) + case apiGetCallInvitations + case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus) + // WebRTC calls / + case apiGetNetworkStatuses + case apiChatRead(type: ChatType, id: Int64) + case apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64]) + case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) + case receiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?, inline: Bool?) + case setFileToReceive(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?) + case cancelFile(fileId: Int64) + // remote desktop commands + case setLocalDeviceName(displayName: String) + case connectRemoteCtrl(xrcpInvitation: String) + case findKnownRemoteCtrl + case confirmRemoteCtrl(remoteCtrlId: Int64) + case verifyRemoteCtrlSession(sessionCode: String) + case listRemoteCtrls + case stopRemoteCtrl + case deleteRemoteCtrl(remoteCtrlId: Int64) + case apiUploadStandaloneFile(userId: Int64, file: CryptoFile) + case apiDownloadStandaloneFile(userId: Int64, url: String, file: CryptoFile) + case apiStandaloneFileInfo(url: String) + // misc + case showVersion + case getAgentSubsTotal(userId: Int64) + case getAgentServersSummary(userId: Int64) + case resetAgentServersStats + case string(String) + + var cmdString: String { + get { + switch self { + case .showActiveUser: return "/u" + case let .createActiveUser(profile, pastTimestamp): + let user = NewUser(profile: profile, pastTimestamp: pastTimestamp) + return "/_create user \(encodeJSON(user))" + case .listUsers: return "/users" + case let .apiSetActiveUser(userId, viewPwd): return "/_user \(userId)\(maybePwd(viewPwd))" + case let .setAllContactReceipts(enable): return "/set receipts all \(onOff(enable))" + case let .apiSetUserContactReceipts(userId, userMsgReceiptSettings): + let umrs = userMsgReceiptSettings + return "/_set receipts contacts \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))" + case let .apiSetUserGroupReceipts(userId, userMsgReceiptSettings): + let umrs = userMsgReceiptSettings + return "/_set receipts groups \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))" + case let .apiHideUser(userId, viewPwd): return "/_hide user \(userId) \(encodeJSON(viewPwd))" + case let .apiUnhideUser(userId, viewPwd): return "/_unhide user \(userId) \(encodeJSON(viewPwd))" + case let .apiMuteUser(userId): return "/_mute user \(userId)" + case let .apiUnmuteUser(userId): return "/_unmute user \(userId)" + case let .apiDeleteUser(userId, delSMPQueues, viewPwd): return "/_delete user \(userId) del_smp=\(onOff(delSMPQueues))\(maybePwd(viewPwd))" + case let .startChat(mainApp, enableSndFiles): return "/_start main=\(onOff(mainApp)) snd_files=\(onOff(enableSndFiles))" + case .checkChatRunning: return "/_check running" + case .apiStopChat: return "/_stop" + case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))" + case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)" + case let .apiSetAppFilePaths(filesFolder, tempFolder, assetsFolder): return "/set file paths \(encodeJSON(AppFilePaths(appFilesFolder: filesFolder, appTempFolder: tempFolder, appAssetsFolder: assetsFolder)))" + case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))" + case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))" + case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))" + case .apiDeleteStorage: return "/_db delete" + case let .apiStorageEncryption(cfg): return "/_db encryption \(encodeJSON(cfg))" + case let .testStorageEncryption(key): return "/db test key \(key)" + case let .apiSaveSettings(settings): return "/_save app settings \(encodeJSON(settings))" + case let .apiGetSettings(settings): return "/_get app settings \(encodeJSON(settings))" + case let .apiGetChatTags(userId): return "/_get tags \(userId)" + case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on" + case let .apiGetChat(chatId, pagination, search): return "/_get chat \(chatId) \(pagination.cmdString)" + + (search == "" ? "" : " search=\(search)") + case let .apiGetChatItemInfo(type, id, itemId): return "/_get item info \(ref(type, id)) \(itemId)" + case let .apiSendMessages(type, id, live, ttl, composedMessages): + let msgs = encodeJSON(composedMessages) + let ttlStr = ttl != nil ? "\(ttl!)" : "default" + return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)" + case let .apiCreateChatTag(tag): return "/_create tag \(encodeJSON(tag))" + case let .apiSetChatTags(type, id, tagIds): return "/_tags \(ref(type, id)) \(tagIds.map({ "\($0)" }).joined(separator: ","))" + case let .apiDeleteChatTag(tagId): return "/_delete tag \(tagId)" + case let .apiUpdateChatTag(tagId, tagData): return "/_update tag \(tagId) \(encodeJSON(tagData))" + case let .apiReorderChatTags(tagIds): return "/_reorder tags \(tagIds.map({ "\($0)" }).joined(separator: ","))" + case let .apiCreateChatItems(noteFolderId, composedMessages): + let msgs = encodeJSON(composedMessages) + return "/_create *\(noteFolderId) json \(msgs)" + case let .apiReportMessage(groupId, chatItemId, reportReason, reportText): + return "/_report #\(groupId) \(chatItemId) reason=\(reportReason) \(reportText)" + case let .apiUpdateChatItem(type, id, itemId, um, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(um.cmdString)" + case let .apiDeleteChatItem(type, id, itemIds, mode): return "/_delete item \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)" + case let .apiDeleteMemberChatItem(groupId, itemIds): return "/_delete member item #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ","))" + case let .apiArchiveReceivedReports(groupId): return "/_archive reports #\(groupId)" + case let .apiDeleteReceivedReports(groupId, itemIds, mode): return "/_delete reports #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)" + case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))" + case let .apiGetReactionMembers(userId, groupId, itemId, reaction): return "/_reaction members \(userId) #\(groupId) \(itemId) \(encodeJSON(reaction))" + case let .apiPlanForwardChatItems(type, id, itemIds): return "/_forward plan \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ","))" + case let .apiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl): + let ttlStr = ttl != nil ? "\(ttl!)" : "default" + return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) ttl=\(ttlStr)" + 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)" + case let .apiCheckToken(token): return "/_ntf check \(token.cmdString)" + case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)" + case let .apiGetNtfConns(nonce, encNtfInfo): return "/_ntf conns \(nonce) \(encNtfInfo)" + case let .apiGetConnNtfMessages(connMsgReqs): return "/_ntf conn messages \(connMsgReqs.map { $0.cmdString }.joined(separator: ","))" + case let .apiNewGroup(userId, incognito, groupProfile): return "/_group \(userId) incognito=\(onOff(incognito)) \(encodeJSON(groupProfile))" + case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)" + case let .apiJoinGroup(groupId): return "/_join #\(groupId)" + case let .apiMembersRole(groupId, memberIds, memberRole): return "/_member role #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) \(memberRole.rawValue)" + case let .apiBlockMembersForAll(groupId, memberIds, blocked): return "/_block #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) blocked=\(onOff(blocked))" + case let .apiRemoveMembers(groupId, memberIds, withMessages): return "/_remove #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) messages=\(onOff(withMessages))" + case let .apiLeaveGroup(groupId): return "/_leave #\(groupId)" + case let .apiListMembers(groupId): return "/_members #\(groupId)" + case let .apiUpdateGroupProfile(groupId, groupProfile): return "/_group_profile #\(groupId) \(encodeJSON(groupProfile))" + case let .apiCreateGroupLink(groupId, memberRole, short): return "/_create link #\(groupId) \(memberRole) short=\(onOff(short))" + case let .apiGroupLinkMemberRole(groupId, memberRole): return "/_set link role #\(groupId) \(memberRole)" + case let .apiDeleteGroupLink(groupId): return "/_delete link #\(groupId)" + case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)" + case let .apiCreateMemberContact(groupId, groupMemberId): return "/_create member contact #\(groupId) \(groupMemberId)" + case let .apiSendMemberContactInvitation(contactId, mc): return "/_invite member contact @\(contactId) \(mc.cmdString)" + case let .apiTestProtoServer(userId, server): return "/_server test \(userId) \(server)" + case .apiGetServerOperators: return "/_operators" + case let .apiSetServerOperators(operators): return "/_operators \(encodeJSON(operators))" + case let .apiGetUserServers(userId): return "/_servers \(userId)" + case let .apiSetUserServers(userId, userServers): return "/_servers \(userId) \(encodeJSON(userServers))" + case let .apiValidateServers(userId, userServers): return "/_validate_servers \(userId) \(encodeJSON(userServers))" + case .apiGetUsageConditions: return "/_conditions" + case let .apiSetConditionsNotified(conditionsId): return "/_conditions_notified \(conditionsId)" + case let .apiAcceptConditions(conditionsId, operatorIds): return "/_accept_conditions \(conditionsId) \(joinedIds(operatorIds))" + case let .apiSetChatItemTTL(userId, seconds): return "/_ttl \(userId) \(chatItemTTLStr(seconds: seconds))" + case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)" + case let .apiSetChatTTL(userId, type, id, seconds): return "/_ttl \(userId) \(ref(type, id)) \(chatItemTTLStr(seconds: seconds))" + case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))" + case .apiGetNetworkConfig: return "/network" + case let .apiSetNetworkInfo(networkInfo): return "/_network info \(encodeJSON(networkInfo))" + case .reconnectAllServers: return "/reconnect" + case let .reconnectServer(userId, smpServer): return "/reconnect \(userId) \(smpServer)" + case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))" + case let .apiSetMemberSettings(groupId, groupMemberId, memberSettings): return "/_member settings #\(groupId) \(groupMemberId) \(encodeJSON(memberSettings))" + case let .apiContactInfo(contactId): return "/_info @\(contactId)" + case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)" + case let .apiContactQueueInfo(contactId): return "/_queue info @\(contactId)" + case let .apiGroupMemberQueueInfo(groupId, groupMemberId): return "/_queue info #\(groupId) \(groupMemberId)" + case let .apiSwitchContact(contactId): return "/_switch @\(contactId)" + case let .apiSwitchGroupMember(groupId, groupMemberId): return "/_switch #\(groupId) \(groupMemberId)" + case let .apiAbortSwitchContact(contactId): return "/_abort switch @\(contactId)" + case let .apiAbortSwitchGroupMember(groupId, groupMemberId): return "/_abort switch #\(groupId) \(groupMemberId)" + case let .apiSyncContactRatchet(contactId, force): if force { + return "/_sync @\(contactId) force=on" + } else { + return "/_sync @\(contactId)" + } + case let .apiSyncGroupMemberRatchet(groupId, groupMemberId, force): if force { + return "/_sync #\(groupId) \(groupMemberId) force=on" + } else { + return "/_sync #\(groupId) \(groupMemberId)" + } + case let .apiGetContactCode(contactId): return "/_get code @\(contactId)" + case let .apiGetGroupMemberCode(groupId, groupMemberId): return "/_get code #\(groupId) \(groupMemberId)" + case let .apiVerifyContact(contactId, .some(connectionCode)): return "/_verify code @\(contactId) \(connectionCode)" + case let .apiVerifyContact(contactId, .none): return "/_verify code @\(contactId)" + case let .apiVerifyGroupMember(groupId, groupMemberId, .some(connectionCode)): return "/_verify code #\(groupId) \(groupMemberId) \(connectionCode)" + case let .apiVerifyGroupMember(groupId, groupMemberId, .none): return "/_verify code #\(groupId) \(groupMemberId)" + case let .apiAddContact(userId, short, incognito): return "/_connect \(userId) short=\(onOff(short)) 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 .apiConnect(userId, incognito, connLink): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connLink.connFullLink) \(connLink.connShortLink ?? "")" + case let .apiConnectContactViaAddress(userId, incognito, contactId): return "/_connect contact \(userId) incognito=\(onOff(incognito)) \(contactId)" + case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id)) \(chatDeleteMode.cmdString)" + case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))" + case let .apiListContacts(userId): return "/_contacts \(userId)" + case let .apiUpdateProfile(userId, profile): return "/_profile \(userId) \(encodeJSON(profile))" + case let .apiSetContactPrefs(contactId, preferences): return "/_set prefs @\(contactId) \(encodeJSON(preferences))" + case let .apiSetContactAlias(contactId, localAlias): return "/_set alias @\(contactId) \(localAlias.trimmingCharacters(in: .whitespaces))" + case let .apiSetGroupAlias(groupId, localAlias): return "/_set alias #\(groupId) \(localAlias.trimmingCharacters(in: .whitespaces))" + case let .apiSetConnectionAlias(connId, localAlias): return "/_set alias :\(connId) \(localAlias.trimmingCharacters(in: .whitespaces))" + case let .apiSetUserUIThemes(userId, themes): return "/_set theme user \(userId) \(themes != nil ? encodeJSON(themes) : "")" + case let .apiSetChatUIThemes(chatId, themes): return "/_set theme \(chatId) \(themes != nil ? encodeJSON(themes) : "")" + case let .apiCreateMyAddress(userId, short): return "/_address \(userId) short=\(onOff(short))" + case let .apiDeleteMyAddress(userId): return "/_delete_address \(userId)" + case let .apiShowMyAddress(userId): return "/_show_address \(userId)" + case let .apiSetProfileAddress(userId, on): return "/_profile_address \(userId) \(onOff(on))" + case let .apiAddressAutoAccept(userId, autoAccept): return "/_auto_accept \(userId) \(AutoAccept.cmdString(autoAccept))" + case let .apiAcceptContact(incognito, contactReqId): return "/_accept incognito=\(onOff(incognito)) \(contactReqId)" + case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)" + case let .apiSendCallInvitation(contact, callType): return "/_call invite @\(contact.apiId) \(encodeJSON(callType))" + case let .apiRejectCall(contact): return "/_call reject @\(contact.apiId)" + case let .apiSendCallOffer(contact, callOffer): return "/_call offer @\(contact.apiId) \(encodeJSON(callOffer))" + case let .apiSendCallAnswer(contact, answer): return "/_call answer @\(contact.apiId) \(encodeJSON(answer))" + case let .apiSendCallExtraInfo(contact, extraInfo): return "/_call extra @\(contact.apiId) \(encodeJSON(extraInfo))" + case let .apiEndCall(contact): return "/_call end @\(contact.apiId)" + case .apiGetCallInvitations: return "/_call get" + case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)" + case .apiGetNetworkStatuses: return "/_network_statuses" + case let .apiChatRead(type, id): return "/_read chat \(ref(type, id))" + case let .apiChatItemsRead(type, id, itemIds): return "/_read chat items \(ref(type, id)) \(joinedIds(itemIds))" + case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))" + case let .receiveFile(fileId, userApprovedRelays, encrypt, inline): return "/freceive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))" + case let .setFileToReceive(fileId, userApprovedRelays, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))" + case let .cancelFile(fileId): return "/fcancel \(fileId)" + case let .setLocalDeviceName(displayName): return "/set device name \(displayName)" + case let .connectRemoteCtrl(xrcpInv): return "/connect remote ctrl \(xrcpInv)" + case .findKnownRemoteCtrl: return "/find remote ctrl" + case let .confirmRemoteCtrl(rcId): return "/confirm remote ctrl \(rcId)" + case let .verifyRemoteCtrlSession(sessCode): return "/verify remote ctrl \(sessCode)" + case .listRemoteCtrls: return "/list remote ctrls" + case .stopRemoteCtrl: return "/stop remote ctrl" + case let .deleteRemoteCtrl(rcId): return "/delete remote ctrl \(rcId)" + case let .apiUploadStandaloneFile(userId, file): return "/_upload \(userId) \(file.filePath)" + case let .apiDownloadStandaloneFile(userId, link, file): return "/_download \(userId) \(link) \(file.filePath)" + case let .apiStandaloneFileInfo(link): return "/_download info \(link)" + case .showVersion: return "/version" + case let .getAgentSubsTotal(userId): return "/get subs total \(userId)" + case let .getAgentServersSummary(userId): return "/get servers summary \(userId)" + case .resetAgentServersStats: return "/reset servers stats" + case let .string(str): return str + } + } + } + + var cmdType: String { + get { + switch self { + case .showActiveUser: return "showActiveUser" + case .createActiveUser: return "createActiveUser" + case .listUsers: return "listUsers" + case .apiSetActiveUser: return "apiSetActiveUser" + case .setAllContactReceipts: return "setAllContactReceipts" + case .apiSetUserContactReceipts: return "apiSetUserContactReceipts" + case .apiSetUserGroupReceipts: return "apiSetUserGroupReceipts" + case .apiHideUser: return "apiHideUser" + case .apiUnhideUser: return "apiUnhideUser" + case .apiMuteUser: return "apiMuteUser" + case .apiUnmuteUser: return "apiUnmuteUser" + case .apiDeleteUser: return "apiDeleteUser" + case .startChat: return "startChat" + case .checkChatRunning: return "checkChatRunning" + case .apiStopChat: return "apiStopChat" + case .apiActivateChat: return "apiActivateChat" + case .apiSuspendChat: return "apiSuspendChat" + case .apiSetAppFilePaths: return "apiSetAppFilePaths" + case .apiSetEncryptLocalFiles: return "apiSetEncryptLocalFiles" + case .apiExportArchive: return "apiExportArchive" + case .apiImportArchive: return "apiImportArchive" + case .apiDeleteStorage: return "apiDeleteStorage" + case .apiStorageEncryption: return "apiStorageEncryption" + case .testStorageEncryption: return "testStorageEncryption" + case .apiSaveSettings: return "apiSaveSettings" + case .apiGetSettings: return "apiGetSettings" + case .apiGetChatTags: return "apiGetChatTags" + case .apiGetChats: return "apiGetChats" + case .apiGetChat: return "apiGetChat" + case .apiGetChatItemInfo: return "apiGetChatItemInfo" + case .apiSendMessages: return "apiSendMessages" + case .apiCreateChatTag: return "apiCreateChatTag" + case .apiSetChatTags: return "apiSetChatTags" + case .apiDeleteChatTag: return "apiDeleteChatTag" + case .apiUpdateChatTag: return "apiUpdateChatTag" + case .apiReorderChatTags: return "apiReorderChatTags" + case .apiCreateChatItems: return "apiCreateChatItems" + case .apiReportMessage: return "apiReportMessage" + case .apiUpdateChatItem: return "apiUpdateChatItem" + case .apiDeleteChatItem: return "apiDeleteChatItem" + case .apiConnectContactViaAddress: return "apiConnectContactViaAddress" + case .apiDeleteMemberChatItem: return "apiDeleteMemberChatItem" + case .apiArchiveReceivedReports: return "apiArchiveReceivedReports" + case .apiDeleteReceivedReports: return "apiDeleteReceivedReports" + case .apiChatItemReaction: return "apiChatItemReaction" + case .apiGetReactionMembers: return "apiGetReactionMembers" + case .apiPlanForwardChatItems: return "apiPlanForwardChatItems" + case .apiForwardChatItems: return "apiForwardChatItems" + case .apiGetNtfToken: return "apiGetNtfToken" + case .apiRegisterToken: return "apiRegisterToken" + case .apiVerifyToken: return "apiVerifyToken" + case .apiCheckToken: return "apiCheckToken" + case .apiDeleteToken: return "apiDeleteToken" + case .apiGetNtfConns: return "apiGetNtfConns" + case .apiGetConnNtfMessages: return "apiGetConnNtfMessages" + case .apiNewGroup: return "apiNewGroup" + case .apiAddMember: return "apiAddMember" + case .apiJoinGroup: return "apiJoinGroup" + case .apiMembersRole: return "apiMembersRole" + case .apiBlockMembersForAll: return "apiBlockMembersForAll" + case .apiRemoveMembers: return "apiRemoveMembers" + case .apiLeaveGroup: return "apiLeaveGroup" + case .apiListMembers: return "apiListMembers" + case .apiUpdateGroupProfile: return "apiUpdateGroupProfile" + case .apiCreateGroupLink: return "apiCreateGroupLink" + case .apiGroupLinkMemberRole: return "apiGroupLinkMemberRole" + case .apiDeleteGroupLink: return "apiDeleteGroupLink" + case .apiGetGroupLink: return "apiGetGroupLink" + case .apiCreateMemberContact: return "apiCreateMemberContact" + case .apiSendMemberContactInvitation: return "apiSendMemberContactInvitation" + case .apiTestProtoServer: return "apiTestProtoServer" + case .apiGetServerOperators: return "apiGetServerOperators" + case .apiSetServerOperators: return "apiSetServerOperators" + case .apiGetUserServers: return "apiGetUserServers" + case .apiSetUserServers: return "apiSetUserServers" + case .apiValidateServers: return "apiValidateServers" + case .apiGetUsageConditions: return "apiGetUsageConditions" + case .apiSetConditionsNotified: return "apiSetConditionsNotified" + case .apiAcceptConditions: return "apiAcceptConditions" + case .apiSetChatItemTTL: return "apiSetChatItemTTL" + case .apiGetChatItemTTL: return "apiGetChatItemTTL" + case .apiSetChatTTL: return "apiSetChatTTL" + case .apiSetNetworkConfig: return "apiSetNetworkConfig" + case .apiGetNetworkConfig: return "apiGetNetworkConfig" + case .apiSetNetworkInfo: return "apiSetNetworkInfo" + case .reconnectAllServers: return "reconnectAllServers" + case .reconnectServer: return "reconnectServer" + case .apiSetChatSettings: return "apiSetChatSettings" + case .apiSetMemberSettings: return "apiSetMemberSettings" + case .apiContactInfo: return "apiContactInfo" + case .apiGroupMemberInfo: return "apiGroupMemberInfo" + case .apiContactQueueInfo: return "apiContactQueueInfo" + case .apiGroupMemberQueueInfo: return "apiGroupMemberQueueInfo" + case .apiSwitchContact: return "apiSwitchContact" + case .apiSwitchGroupMember: return "apiSwitchGroupMember" + case .apiAbortSwitchContact: return "apiAbortSwitchContact" + case .apiAbortSwitchGroupMember: return "apiAbortSwitchGroupMember" + case .apiSyncContactRatchet: return "apiSyncContactRatchet" + case .apiSyncGroupMemberRatchet: return "apiSyncGroupMemberRatchet" + case .apiGetContactCode: return "apiGetContactCode" + case .apiGetGroupMemberCode: return "apiGetGroupMemberCode" + case .apiVerifyContact: return "apiVerifyContact" + case .apiVerifyGroupMember: return "apiVerifyGroupMember" + case .apiAddContact: return "apiAddContact" + case .apiSetConnectionIncognito: return "apiSetConnectionIncognito" + case .apiChangeConnectionUser: return "apiChangeConnectionUser" + case .apiConnectPlan: return "apiConnectPlan" + case .apiConnect: return "apiConnect" + case .apiDeleteChat: return "apiDeleteChat" + case .apiClearChat: return "apiClearChat" + case .apiListContacts: return "apiListContacts" + case .apiUpdateProfile: return "apiUpdateProfile" + case .apiSetContactPrefs: return "apiSetContactPrefs" + case .apiSetContactAlias: return "apiSetContactAlias" + case .apiSetGroupAlias: return "apiSetGroupAlias" + case .apiSetConnectionAlias: return "apiSetConnectionAlias" + case .apiSetUserUIThemes: return "apiSetUserUIThemes" + case .apiSetChatUIThemes: return "apiSetChatUIThemes" + case .apiCreateMyAddress: return "apiCreateMyAddress" + case .apiDeleteMyAddress: return "apiDeleteMyAddress" + case .apiShowMyAddress: return "apiShowMyAddress" + case .apiSetProfileAddress: return "apiSetProfileAddress" + case .apiAddressAutoAccept: return "apiAddressAutoAccept" + case .apiAcceptContact: return "apiAcceptContact" + case .apiRejectContact: return "apiRejectContact" + case .apiSendCallInvitation: return "apiSendCallInvitation" + case .apiRejectCall: return "apiRejectCall" + case .apiSendCallOffer: return "apiSendCallOffer" + case .apiSendCallAnswer: return "apiSendCallAnswer" + case .apiSendCallExtraInfo: return "apiSendCallExtraInfo" + case .apiEndCall: return "apiEndCall" + case .apiGetCallInvitations: return "apiGetCallInvitations" + case .apiCallStatus: return "apiCallStatus" + case .apiGetNetworkStatuses: return "apiGetNetworkStatuses" + case .apiChatRead: return "apiChatRead" + case .apiChatItemsRead: return "apiChatItemsRead" + case .apiChatUnread: return "apiChatUnread" + case .receiveFile: return "receiveFile" + case .setFileToReceive: return "setFileToReceive" + case .cancelFile: return "cancelFile" + case .setLocalDeviceName: return "setLocalDeviceName" + case .connectRemoteCtrl: return "connectRemoteCtrl" + case .findKnownRemoteCtrl: return "findKnownRemoteCtrl" + case .confirmRemoteCtrl: return "confirmRemoteCtrl" + case .verifyRemoteCtrlSession: return "verifyRemoteCtrlSession" + case .listRemoteCtrls: return "listRemoteCtrls" + case .stopRemoteCtrl: return "stopRemoteCtrl" + case .deleteRemoteCtrl: return "deleteRemoteCtrl" + case .apiUploadStandaloneFile: return "apiUploadStandaloneFile" + case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile" + case .apiStandaloneFileInfo: return "apiStandaloneFileInfo" + case .showVersion: return "showVersion" + case .getAgentSubsTotal: return "getAgentSubsTotal" + case .getAgentServersSummary: return "getAgentServersSummary" + case .resetAgentServersStats: return "resetAgentServersStats" + case .string: return "console command" + } + } + } + + func ref(_ type: ChatType, _ id: Int64) -> String { + "\(type.rawValue)\(id)" + } + + func joinedIds(_ ids: [Int64]) -> String { + ids.map { "\($0)" }.joined(separator: ",") + } + + func chatItemTTLStr(seconds: Int64?) -> String { + if let seconds = seconds { + return String(seconds) + } else { + return "default" + } + } + + var obfuscated: ChatCommand { + switch self { + case let .apiStorageEncryption(cfg): + return .apiStorageEncryption(config: DBEncryptionConfig(currentKey: obfuscate(cfg.currentKey), newKey: obfuscate(cfg.newKey))) + case let .apiSetActiveUser(userId, viewPwd): + return .apiSetActiveUser(userId: userId, viewPwd: obfuscate(viewPwd)) + case let .apiHideUser(userId, viewPwd): + return .apiHideUser(userId: userId, viewPwd: obfuscate(viewPwd)) + case let .apiUnhideUser(userId, viewPwd): + return .apiUnhideUser(userId: userId, viewPwd: obfuscate(viewPwd)) + case let .apiDeleteUser(userId, delSMPQueues, viewPwd): + return .apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues, viewPwd: obfuscate(viewPwd)) + case let .testStorageEncryption(key): + return .testStorageEncryption(key: obfuscate(key)) + default: return self + } + } + + private func obfuscate(_ s: String) -> String { + s == "" ? "" : "***" + } + + private func obfuscate(_ s: String?) -> String? { + if let s = s { + return obfuscate(s) + } + return nil + } + + private func onOffParam(_ param: String, _ b: Bool?) -> String { + if let b = b { + return " \(param)=\(onOff(b))" + } + return "" + } + + private func maybePwd(_ pwd: String?) -> String { + pwd == "" || pwd == nil ? "" : " " + encodeJSON(pwd) + } +} + +// ChatResponse is split to three enums to reduce stack size used when parsing it, parsing large enums is very inefficient. +enum ChatResponse0: Decodable, ChatAPIResult { + case activeUser(user: User) + case usersList(users: [UserInfo]) + case chatStarted + case chatRunning + case chatStopped + case apiChats(user: UserRef, chats: [ChatData]) + case apiChat(user: UserRef, chat: ChatData, navInfo: NavigationInfo?) + case chatTags(user: UserRef, userTags: [ChatTag]) + case chatItemInfo(user: UserRef, chatItem: AChatItem, chatItemInfo: ChatItemInfo) + case serverTestResult(user: UserRef, testServer: String, testFailure: ProtocolTestFailure?) + case serverOperatorConditions(conditions: ServerOperatorConditions) + case userServers(user: UserRef, userServers: [UserOperatorServers]) + case userServersValidation(user: UserRef, serverErrors: [UserServersError]) + case usageConditions(usageConditions: UsageConditions, conditionsText: String, acceptedConditions: UsageConditions?) + case chatItemTTL(user: UserRef, chatItemTTL: Int64?) + case networkConfig(networkConfig: NetCfg) + case contactInfo(user: UserRef, contact: Contact, connectionStats_: ConnectionStats?, customUserProfile: Profile?) + case groupMemberInfo(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?) + case queueInfo(user: UserRef, rcvMsgInfo: RcvMsgInfo?, queueInfo: ServerQueueInfo) + case contactSwitchStarted(user: UserRef, contact: Contact, connectionStats: ConnectionStats) + case groupMemberSwitchStarted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) + case contactSwitchAborted(user: UserRef, contact: Contact, connectionStats: ConnectionStats) + case groupMemberSwitchAborted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) + case contactRatchetSyncStarted(user: UserRef, contact: Contact, connectionStats: ConnectionStats) + case groupMemberRatchetSyncStarted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) + case contactCode(user: UserRef, contact: Contact, connectionCode: String) + case groupMemberCode(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionCode: String) + case connectionVerified(user: UserRef, verified: Bool, expectedCode: String) + case tagsUpdated(user: UserRef, userTags: [ChatTag], chatTags: [Int64]) + + var responseType: String { + switch self { + case .activeUser: "activeUser" + case .usersList: "usersList" + case .chatStarted: "chatStarted" + case .chatRunning: "chatRunning" + case .chatStopped: "chatStopped" + case .apiChats: "apiChats" + case .apiChat: "apiChat" + case .chatTags: "chatTags" + case .chatItemInfo: "chatItemInfo" + case .serverTestResult: "serverTestResult" + case .serverOperatorConditions: "serverOperators" + case .userServers: "userServers" + case .userServersValidation: "userServersValidation" + case .usageConditions: "usageConditions" + case .chatItemTTL: "chatItemTTL" + case .networkConfig: "networkConfig" + case .contactInfo: "contactInfo" + case .groupMemberInfo: "groupMemberInfo" + case .queueInfo: "queueInfo" + case .contactSwitchStarted: "contactSwitchStarted" + case .groupMemberSwitchStarted: "groupMemberSwitchStarted" + case .contactSwitchAborted: "contactSwitchAborted" + case .groupMemberSwitchAborted: "groupMemberSwitchAborted" + case .contactRatchetSyncStarted: "contactRatchetSyncStarted" + case .groupMemberRatchetSyncStarted: "groupMemberRatchetSyncStarted" + case .contactCode: "contactCode" + case .groupMemberCode: "groupMemberCode" + case .connectionVerified: "connectionVerified" + case .tagsUpdated: "tagsUpdated" + } + } + + var details: String { + switch self { + case let .activeUser(user): return String(describing: user) + case let .usersList(users): return String(describing: users) + case .chatStarted: return noDetails + case .chatRunning: return noDetails + case .chatStopped: return noDetails + case let .apiChats(u, chats): return withUser(u, String(describing: chats)) + case let .apiChat(u, chat, navInfo): return withUser(u, "chat: \(String(describing: chat))\nnavInfo: \(String(describing: navInfo))") + case let .chatTags(u, userTags): return withUser(u, "userTags: \(String(describing: userTags))") + case let .chatItemInfo(u, chatItem, chatItemInfo): return withUser(u, "chatItem: \(String(describing: chatItem))\nchatItemInfo: \(String(describing: chatItemInfo))") + case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\nresult: \(String(describing: testFailure))") + case let .serverOperatorConditions(conditions): return "conditions: \(String(describing: conditions))" + case let .userServers(u, userServers): return withUser(u, "userServers: \(String(describing: userServers))") + case let .userServersValidation(u, serverErrors): return withUser(u, "serverErrors: \(String(describing: serverErrors))") + case let .usageConditions(usageConditions, _, acceptedConditions): return "usageConditions: \(String(describing: usageConditions))\nacceptedConditions: \(String(describing: acceptedConditions))" + case let .chatItemTTL(u, chatItemTTL): return withUser(u, String(describing: chatItemTTL)) + case let .networkConfig(networkConfig): return String(describing: networkConfig) + case let .contactInfo(u, contact, connectionStats_, customUserProfile): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats_: \(String(describing: connectionStats_))\ncustomUserProfile: \(String(describing: customUserProfile))") + case let .groupMemberInfo(u, groupInfo, member, connectionStats_): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_))") + case let .queueInfo(u, rcvMsgInfo, queueInfo): + let msgInfo = if let info = rcvMsgInfo { encodeJSON(info) } else { "none" } + return withUser(u, "rcvMsgInfo: \(msgInfo)\nqueueInfo: \(encodeJSON(queueInfo))") + case let .contactSwitchStarted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))") + case let .groupMemberSwitchStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))") + case let .contactSwitchAborted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))") + case let .groupMemberSwitchAborted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))") + case let .contactRatchetSyncStarted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))") + case let .groupMemberRatchetSyncStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))") + case let .contactCode(u, contact, connectionCode): return withUser(u, "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)") + case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)") + case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)") + case let .tagsUpdated(u, userTags, chatTags): return withUser(u, "userTags: \(String(describing: userTags))\nchatTags: \(String(describing: chatTags))") + } + } + + static func fallbackResult(_ type: String, _ json: NSDictionary) -> ChatResponse0? { + if type == "apiChats" { + if let r = parseApiChats(json) { + return .apiChats(user: r.user, chats: r.chats) + } + } else if type == "apiChat" { + if let jApiChat = json["apiChat"] as? NSDictionary, + let user: UserRef = try? decodeObject(jApiChat["user"] as Any), + let jChat = jApiChat["chat"] as? NSDictionary, + let (chat, navInfo) = try? parseChatData(jChat, jApiChat["navInfo"] as? NSDictionary) { + return .apiChat(user: user, chat: chat, navInfo: navInfo) + } + } + return nil + } +} + +enum ChatResponse1: Decodable, ChatAPIResult { + case invitation(user: UserRef, connLinkInvitation: CreatedConnLink, connection: PendingContactConnection) + case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection) + case connectionUserChanged(user: UserRef, fromConnection: PendingContactConnection, toConnection: PendingContactConnection, newUser: UserRef) + case connectionPlan(user: UserRef, connLink: CreatedConnLink, connectionPlan: ConnectionPlan) + case sentConfirmation(user: UserRef, connection: PendingContactConnection) + case sentInvitation(user: UserRef, connection: PendingContactConnection) + case sentInvitationToContact(user: UserRef, contact: Contact, customUserProfile: Profile?) + case contactAlreadyExists(user: UserRef, contact: Contact) + case contactDeleted(user: UserRef, contact: Contact) + case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection) + case groupDeletedUser(user: UserRef, groupInfo: GroupInfo) + case chatCleared(user: UserRef, chatInfo: ChatInfo) + case userProfileNoChange(user: User) + case userProfileUpdated(user: User, fromProfile: Profile, toProfile: Profile, updateSummary: UserProfileUpdateSummary) + case userPrivacy(user: User, updatedUser: User) + case contactAliasUpdated(user: UserRef, toContact: Contact) + case groupAliasUpdated(user: UserRef, toGroup: GroupInfo) + case connectionAliasUpdated(user: UserRef, toConnection: PendingContactConnection) + case contactPrefsUpdated(user: User, fromContact: Contact, toContact: Contact) + case userContactLink(user: User, contactLink: UserContactLink) + case userContactLinkUpdated(user: User, contactLink: UserContactLink) + case userContactLinkCreated(user: User, connLinkContact: CreatedConnLink) + case userContactLinkDeleted(user: User) + case acceptingContactRequest(user: UserRef, contact: Contact) + case contactRequestRejected(user: UserRef) + case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus]) + case newChatItems(user: UserRef, chatItems: [AChatItem]) + 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) + case chatItemNotChanged(user: UserRef, chatItem: AChatItem) + case chatItemReaction(user: UserRef, added: Bool, reaction: ACIReaction) + case reactionMembers(user: UserRef, memberReactions: [MemberReaction]) + case chatItemsDeleted(user: UserRef, chatItemDeletions: [ChatItemDeletion], byUser: Bool) + case contactsList(user: UserRef, contacts: [Contact]) + + var responseType: String { + switch self { + case .invitation: "invitation" + case .connectionIncognitoUpdated: "connectionIncognitoUpdated" + case .connectionUserChanged: "connectionUserChanged" + case .connectionPlan: "connectionPlan" + case .sentConfirmation: "sentConfirmation" + case .sentInvitation: "sentInvitation" + case .sentInvitationToContact: "sentInvitationToContact" + case .contactAlreadyExists: "contactAlreadyExists" + case .contactDeleted: "contactDeleted" + case .contactConnectionDeleted: "contactConnectionDeleted" + case .groupDeletedUser: "groupDeletedUser" + case .chatCleared: "chatCleared" + case .userProfileNoChange: "userProfileNoChange" + case .userProfileUpdated: "userProfileUpdated" + case .userPrivacy: "userPrivacy" + case .contactAliasUpdated: "contactAliasUpdated" + case .groupAliasUpdated: "groupAliasUpdated" + case .connectionAliasUpdated: "connectionAliasUpdated" + case .contactPrefsUpdated: "contactPrefsUpdated" + case .userContactLink: "userContactLink" + case .userContactLinkUpdated: "userContactLinkUpdated" + case .userContactLinkCreated: "userContactLinkCreated" + case .userContactLinkDeleted: "userContactLinkDeleted" + case .acceptingContactRequest: "acceptingContactRequest" + case .contactRequestRejected: "contactRequestRejected" + case .networkStatuses: "networkStatuses" + case .newChatItems: "newChatItems" + case .groupChatItemsDeleted: "groupChatItemsDeleted" + case .forwardPlan: "forwardPlan" + case .chatItemUpdated: "chatItemUpdated" + case .chatItemNotChanged: "chatItemNotChanged" + case .chatItemReaction: "chatItemReaction" + case .reactionMembers: "reactionMembers" + case .chatItemsDeleted: "chatItemsDeleted" + case .contactsList: "contactsList" + } + } + + var details: String { + switch self { + case let .contactDeleted(u, contact): return withUser(u, String(describing: contact)) + case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection)) + case let .groupDeletedUser(u, groupInfo): return withUser(u, String(describing: groupInfo)) + case let .chatCleared(u, chatInfo): return withUser(u, String(describing: chatInfo)) + case .userProfileNoChange: return noDetails + case let .userProfileUpdated(u, _, toProfile, _): return withUser(u, String(describing: toProfile)) + case let .userPrivacy(u, updatedUser): return withUser(u, String(describing: updatedUser)) + case let .contactAliasUpdated(u, toContact): return withUser(u, String(describing: toContact)) + case let .groupAliasUpdated(u, toGroup): return withUser(u, String(describing: toGroup)) + case let .connectionAliasUpdated(u, toConnection): return withUser(u, String(describing: toConnection)) + case let .contactPrefsUpdated(u, fromContact, toContact): return withUser(u, "fromContact: \(String(describing: fromContact))\ntoContact: \(String(describing: toContact))") + case let .userContactLink(u, contactLink): return withUser(u, contactLink.responseDetails) + case let .userContactLinkUpdated(u, contactLink): return withUser(u, contactLink.responseDetails) + case let .userContactLinkCreated(u, connLink): return withUser(u, String(describing: connLink)) + case .userContactLinkDeleted: return noDetails + case let .acceptingContactRequest(u, contact): return withUser(u, String(describing: contact)) + case .contactRequestRejected: return noDetails + case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses)) + case let .newChatItems(u, chatItems): + let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n") + return withUser(u, itemsString) + 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))") + case let .chatItemUpdated(u, chatItem): return withUser(u, String(describing: chatItem)) + case let .chatItemNotChanged(u, chatItem): return withUser(u, String(describing: chatItem)) + case let .chatItemReaction(u, added, reaction): return withUser(u, "added: \(added)\n\(String(describing: reaction))") + case let .reactionMembers(u, reaction): return withUser(u, "memberReactions: \(String(describing: reaction))") + case let .chatItemsDeleted(u, items, byUser): + let itemsString = items.map { item in + "deletedChatItem:\n\(String(describing: item.deletedChatItem))\ntoChatItem:\n\(String(describing: item.toChatItem))" }.joined(separator: "\n") + return withUser(u, itemsString + "\nbyUser: \(byUser)") + case let .contactsList(u, contacts): return withUser(u, String(describing: contacts)) + case let .invitation(u, connLinkInvitation, connection): return withUser(u, "connLinkInvitation: \(connLinkInvitation)\nconnection: \(connection)") + case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection)) + case let .connectionUserChanged(u, fromConnection, toConnection, newUser): return withUser(u, "fromConnection: \(String(describing: fromConnection))\ntoConnection: \(String(describing: toConnection))\nnewUserId: \(String(describing: newUser.userId))") + case let .connectionPlan(u, connLink, connectionPlan): return withUser(u, "connLink: \(String(describing: connLink))\nconnectionPlan: \(String(describing: connectionPlan))") + case let .sentConfirmation(u, connection): return withUser(u, String(describing: connection)) + case let .sentInvitation(u, connection): return withUser(u, String(describing: connection)) + case let .sentInvitationToContact(u, contact, _): return withUser(u, String(describing: contact)) + case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact)) + } + } +} + +enum ChatResponse2: Decodable, ChatAPIResult { + // group responses + case groupCreated(user: UserRef, groupInfo: GroupInfo) + case sentGroupInvitation(user: UserRef, groupInfo: GroupInfo, contact: Contact, member: GroupMember) + case userAcceptedGroupSent(user: UserRef, groupInfo: GroupInfo, hostContact: Contact?) + case userDeletedMembers(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], withMessages: Bool) + case leftMemberUser(user: UserRef, groupInfo: GroupInfo) + case groupMembers(user: UserRef, group: SimpleXChat.Group) + case membersRoleUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], toRole: GroupMemberRole) + case membersBlockedForAllUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], blocked: Bool) + case groupUpdated(user: UserRef, toGroup: GroupInfo) + case groupLinkCreated(user: UserRef, groupInfo: GroupInfo, connLinkContact: CreatedConnLink, memberRole: GroupMemberRole) + case groupLink(user: UserRef, groupInfo: GroupInfo, connLinkContact: CreatedConnLink, memberRole: GroupMemberRole) + case groupLinkDeleted(user: UserRef, groupInfo: GroupInfo) + case newMemberContact(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember) + case newMemberContactSentInv(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember) + // receiving file responses + case rcvFileAccepted(user: UserRef, chatItem: AChatItem) + case rcvFileAcceptedSndCancelled(user: UserRef, rcvFileTransfer: RcvFileTransfer) + case standaloneFileInfo(fileMeta: MigrationFileLinkData?) + case rcvStandaloneFileCreated(user: UserRef, rcvFileTransfer: RcvFileTransfer) + case rcvFileCancelled(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer) + // sending file responses + case sndFileCancelled(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer]) + case sndStandaloneFileCreated(user: UserRef, fileTransferMeta: FileTransferMeta) // returned by _upload + case sndFileStartXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta) // not used + case sndFileCancelledXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta) + // call invitations + case callInvitations(callInvitations: [RcvCallInvitation]) + // notifications + case ntfTokenStatus(status: NtfTknStatus) + case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode, ntfServer: String) + case ntfConns(ntfConns: [NtfConn]) + case connNtfMessages(receivedMsgs: [RcvNtfMsgInfo]) + // remote desktop responses + case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo]) + case remoteCtrlConnecting(remoteCtrl_: RemoteCtrlInfo?, ctrlAppInfo: CtrlAppInfo, appVersion: String) + case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo) + // misc + case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration]) + case cmdOk(user_: UserRef?) + case agentSubsTotal(user: UserRef, subsTotal: SMPServerSubs, hasSession: Bool) + case agentServersSummary(user: UserRef, serversSummary: PresentedServersSummary) + case agentSubsSummary(user: UserRef, subsSummary: SMPServerSubs) + case archiveExported(archiveErrors: [ArchiveError]) + case archiveImported(archiveErrors: [ArchiveError]) + case appSettings(appSettings: AppSettings) + + var responseType: String { + switch self { + case .groupCreated: "groupCreated" + case .sentGroupInvitation: "sentGroupInvitation" + case .userAcceptedGroupSent: "userAcceptedGroupSent" + case .userDeletedMembers: "userDeletedMembers" + case .leftMemberUser: "leftMemberUser" + case .groupMembers: "groupMembers" + case .membersRoleUser: "membersRoleUser" + case .membersBlockedForAllUser: "membersBlockedForAllUser" + case .groupUpdated: "groupUpdated" + case .groupLinkCreated: "groupLinkCreated" + case .groupLink: "groupLink" + case .groupLinkDeleted: "groupLinkDeleted" + case .newMemberContact: "newMemberContact" + case .newMemberContactSentInv: "newMemberContactSentInv" + case .rcvFileAccepted: "rcvFileAccepted" + case .rcvFileAcceptedSndCancelled: "rcvFileAcceptedSndCancelled" + case .standaloneFileInfo: "standaloneFileInfo" + case .rcvStandaloneFileCreated: "rcvStandaloneFileCreated" + case .rcvFileCancelled: "rcvFileCancelled" + case .sndFileCancelled: "sndFileCancelled" + case .sndStandaloneFileCreated: "sndStandaloneFileCreated" + case .sndFileStartXFTP: "sndFileStartXFTP" + case .sndFileCancelledXFTP: "sndFileCancelledXFTP" + case .callInvitations: "callInvitations" + case .ntfTokenStatus: "ntfTokenStatus" + case .ntfToken: "ntfToken" + case .ntfConns: "ntfConns" + case .connNtfMessages: "connNtfMessages" + case .remoteCtrlList: "remoteCtrlList" + case .remoteCtrlConnecting: "remoteCtrlConnecting" + case .remoteCtrlConnected: "remoteCtrlConnected" + case .versionInfo: "versionInfo" + case .cmdOk: "cmdOk" + case .agentSubsTotal: "agentSubsTotal" + case .agentServersSummary: "agentServersSummary" + case .agentSubsSummary: "agentSubsSummary" + case .archiveExported: "archiveExported" + case .archiveImported: "archiveImported" + case .appSettings: "appSettings" + } + } + + var details: String { + switch self { + case let .groupCreated(u, groupInfo): return withUser(u, String(describing: groupInfo)) + 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)") + case let .leftMemberUser(u, groupInfo): return withUser(u, String(describing: groupInfo)) + case let .groupMembers(u, group): return withUser(u, String(describing: group)) + case let .membersRoleUser(u, groupInfo, members, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\ntoRole: \(toRole)") + case let .membersBlockedForAllUser(u, groupInfo, members, blocked): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(members)\nblocked: \(blocked)") + case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup)) + case let .groupLinkCreated(u, groupInfo, connLinkContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnLinkContact: \(connLinkContact)\nmemberRole: \(memberRole)") + case let .groupLink(u, groupInfo, connLinkContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnLinkContact: \(connLinkContact)\nmemberRole: \(memberRole)") + case let .groupLinkDeleted(u, groupInfo): return withUser(u, String(describing: groupInfo)) + case let .newMemberContact(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)") + case let .newMemberContactSentInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)") + case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem)) + case .rcvFileAcceptedSndCancelled: return noDetails + case let .standaloneFileInfo(fileMeta): return String(describing: fileMeta) + case .rcvStandaloneFileCreated: return noDetails + case let .rcvFileCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .sndFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem)) + case .sndStandaloneFileCreated: return noDetails + case let .sndFileStartXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .sndFileCancelledXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .callInvitations(invs): return String(describing: invs) + case let .ntfTokenStatus(status): return String(describing: status) + case let .ntfToken(token, status, ntfMode, ntfServer): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)\nntfServer: \(ntfServer)" + case let .ntfConns(ntfConns): return String(describing: ntfConns) + case let .connNtfMessages(receivedMsgs): return "receivedMsgs: \(String(describing: receivedMsgs))" + case let .remoteCtrlList(remoteCtrls): return String(describing: remoteCtrls) + case let .remoteCtrlConnecting(remoteCtrl_, ctrlAppInfo, appVersion): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nctrlAppInfo:\n\(String(describing: ctrlAppInfo))\nappVersion: \(appVersion)" + case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl) + case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))" + case .cmdOk: return noDetails + case let .agentSubsTotal(u, subsTotal, hasSession): return withUser(u, "subsTotal: \(String(describing: subsTotal))\nhasSession: \(hasSession)") + case let .agentServersSummary(u, serversSummary): return withUser(u, String(describing: serversSummary)) + case let .agentSubsSummary(u, subsSummary): return withUser(u, String(describing: subsSummary)) + case let .archiveExported(archiveErrors): return String(describing: archiveErrors) + case let .archiveImported(archiveErrors): return String(describing: archiveErrors) + case let .appSettings(appSettings): return String(describing: appSettings) + } + } +} + +enum ChatEvent: Decodable, ChatAPIResult { + case chatSuspended + case contactSwitch(user: UserRef, contact: Contact, switchProgress: SwitchProgress) + case groupMemberSwitch(user: UserRef, groupInfo: GroupInfo, member: GroupMember, switchProgress: SwitchProgress) + case contactRatchetSync(user: UserRef, contact: Contact, ratchetSyncProgress: RatchetSyncProgress) + case groupMemberRatchetSync(user: UserRef, groupInfo: GroupInfo, member: GroupMember, ratchetSyncProgress: RatchetSyncProgress) + case contactDeletedByContact(user: UserRef, contact: Contact) + case contactConnected(user: UserRef, contact: Contact, userCustomProfile: Profile?) + case contactConnecting(user: UserRef, contact: Contact) + case contactSndReady(user: UserRef, contact: Contact) + case receivedContactRequest(user: UserRef, contactRequest: UserContactRequest) + case contactUpdated(user: UserRef, toContact: Contact) + case groupMemberUpdated(user: UserRef, groupInfo: GroupInfo, fromMember: GroupMember, toMember: GroupMember) + case contactsMerged(user: UserRef, intoContact: Contact, mergedContact: Contact) + case networkStatus(networkStatus: NetworkStatus, connections: [String]) + case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus]) + case newChatItems(user: UserRef, chatItems: [AChatItem]) + case chatItemsStatusesUpdated(user: UserRef, chatItems: [AChatItem]) + case chatItemUpdated(user: UserRef, chatItem: AChatItem) + case chatItemReaction(user: UserRef, added: Bool, reaction: ACIReaction) + case chatItemsDeleted(user: UserRef, chatItemDeletions: [ChatItemDeletion], byUser: Bool) + // group events + case groupChatItemsDeleted(user: UserRef, groupInfo: GroupInfo, chatItemIDs: Set, byUser: Bool, member_: GroupMember?) + case receivedGroupInvitation(user: UserRef, groupInfo: GroupInfo, contact: Contact, memberRole: GroupMemberRole) + case userAcceptedGroupSent(user: UserRef, groupInfo: GroupInfo, hostContact: Contact?) + case groupLinkConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember) + case businessLinkConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember, fromContact: Contact) + case joinedGroupMemberConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember, member: GroupMember) + case memberRole(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole) + case memberBlockedForAll(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, blocked: Bool) + case deletedMemberUser(user: UserRef, groupInfo: GroupInfo, member: GroupMember, withMessages: Bool) + case deletedMember(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, deletedMember: GroupMember, withMessages: Bool) + case leftMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember) + case groupDeleted(user: UserRef, groupInfo: GroupInfo, member: GroupMember) + case userJoinedGroup(user: UserRef, groupInfo: GroupInfo) + case joinedGroupMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember) + case connectedToGroupMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember, memberContact: Contact?) + case groupUpdated(user: UserRef, toGroup: GroupInfo) + case newMemberContactReceivedInv(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember) + // receiving file events + case rcvFileAccepted(user: UserRef, chatItem: AChatItem) + case rcvFileAcceptedSndCancelled(user: UserRef, rcvFileTransfer: RcvFileTransfer) + case rcvFileStart(user: UserRef, chatItem: AChatItem) // send by chats + case rcvFileProgressXFTP(user: UserRef, chatItem_: AChatItem?, receivedSize: Int64, totalSize: Int64, rcvFileTransfer: RcvFileTransfer) + case rcvFileComplete(user: UserRef, chatItem: AChatItem) + case rcvStandaloneFileComplete(user: UserRef, targetPath: String, rcvFileTransfer: RcvFileTransfer) + case rcvFileSndCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer) + case rcvFileError(user: UserRef, chatItem_: AChatItem?, agentError: AgentErrorType, rcvFileTransfer: RcvFileTransfer) + case rcvFileWarning(user: UserRef, chatItem_: AChatItem?, agentError: AgentErrorType, rcvFileTransfer: RcvFileTransfer) + // sending file events + case sndFileStart(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) + case sndFileComplete(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) + case sndFileRcvCancelled(user: UserRef, chatItem_: AChatItem?, sndFileTransfer: SndFileTransfer) + case sndFileProgressXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64) + case sndFileRedirectStartXFTP(user: UserRef, fileTransferMeta: FileTransferMeta, redirectMeta: FileTransferMeta) + case sndFileCompleteXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta) + case sndStandaloneFileComplete(user: UserRef, fileTransferMeta: FileTransferMeta, rcvURIs: [String]) + case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String) + case sndFileWarning(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String) + // call events + case callInvitation(callInvitation: RcvCallInvitation) + case callOffer(user: UserRef, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool) + case callAnswer(user: UserRef, contact: Contact, answer: WebRTCSession) + case callExtraInfo(user: UserRef, contact: Contact, extraInfo: WebRTCExtraInfo) + case callEnded(user: UserRef, contact: Contact) + case contactDisabled(user: UserRef, contact: Contact) + // notification marker + case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgAckInfo) + // remote desktop responses + case remoteCtrlFound(remoteCtrl: RemoteCtrlInfo, ctrlAppInfo_: CtrlAppInfo?, appVersion: String, compatible: Bool) + case remoteCtrlSessionCode(remoteCtrl_: RemoteCtrlInfo?, sessionCode: String) + case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo) + case remoteCtrlStopped(rcsState: RemoteCtrlSessionState, rcStopReason: RemoteCtrlStopReason) + // pq + case contactPQEnabled(user: UserRef, contact: Contact, pqEnabled: Bool) + + var responseType: String { + switch self { + case .chatSuspended: "chatSuspended" + case .contactSwitch: "contactSwitch" + case .groupMemberSwitch: "groupMemberSwitch" + case .contactRatchetSync: "contactRatchetSync" + case .groupMemberRatchetSync: "groupMemberRatchetSync" + case .contactDeletedByContact: "contactDeletedByContact" + case .contactConnected: "contactConnected" + case .contactConnecting: "contactConnecting" + case .contactSndReady: "contactSndReady" + case .receivedContactRequest: "receivedContactRequest" + case .contactUpdated: "contactUpdated" + case .groupMemberUpdated: "groupMemberUpdated" + case .contactsMerged: "contactsMerged" + case .networkStatus: "networkStatus" + case .networkStatuses: "networkStatuses" + case .newChatItems: "newChatItems" + case .chatItemsStatusesUpdated: "chatItemsStatusesUpdated" + case .chatItemUpdated: "chatItemUpdated" + case .chatItemReaction: "chatItemReaction" + case .chatItemsDeleted: "chatItemsDeleted" + case .groupChatItemsDeleted: "groupChatItemsDeleted" + case .receivedGroupInvitation: "receivedGroupInvitation" + case .userAcceptedGroupSent: "userAcceptedGroupSent" + case .groupLinkConnecting: "groupLinkConnecting" + case .businessLinkConnecting: "businessLinkConnecting" + case .joinedGroupMemberConnecting: "joinedGroupMemberConnecting" + case .memberRole: "memberRole" + case .memberBlockedForAll: "memberBlockedForAll" + case .deletedMemberUser: "deletedMemberUser" + case .deletedMember: "deletedMember" + case .leftMember: "leftMember" + case .groupDeleted: "groupDeleted" + case .userJoinedGroup: "userJoinedGroup" + case .joinedGroupMember: "joinedGroupMember" + case .connectedToGroupMember: "connectedToGroupMember" + case .groupUpdated: "groupUpdated" + case .newMemberContactReceivedInv: "newMemberContactReceivedInv" + case .rcvFileAccepted: "rcvFileAccepted" + case .rcvFileAcceptedSndCancelled: "rcvFileAcceptedSndCancelled" + case .rcvFileStart: "rcvFileStart" + case .rcvFileProgressXFTP: "rcvFileProgressXFTP" + case .rcvFileComplete: "rcvFileComplete" + case .rcvStandaloneFileComplete: "rcvStandaloneFileComplete" + case .rcvFileSndCancelled: "rcvFileSndCancelled" + case .rcvFileError: "rcvFileError" + case .rcvFileWarning: "rcvFileWarning" + case .sndFileStart: "sndFileStart" + case .sndFileComplete: "sndFileComplete" + case .sndFileRcvCancelled: "sndFileRcvCancelled" + case .sndFileProgressXFTP: "sndFileProgressXFTP" + case .sndFileRedirectStartXFTP: "sndFileRedirectStartXFTP" + case .sndFileCompleteXFTP: "sndFileCompleteXFTP" + case .sndStandaloneFileComplete: "sndStandaloneFileComplete" + case .sndFileError: "sndFileError" + case .sndFileWarning: "sndFileWarning" + case .callInvitation: "callInvitation" + case .callOffer: "callOffer" + case .callAnswer: "callAnswer" + case .callExtraInfo: "callExtraInfo" + case .callEnded: "callEnded" + case .contactDisabled: "contactDisabled" + case .ntfMessage: "ntfMessage" + case .remoteCtrlFound: "remoteCtrlFound" + case .remoteCtrlSessionCode: "remoteCtrlSessionCode" + case .remoteCtrlConnected: "remoteCtrlConnected" + case .remoteCtrlStopped: "remoteCtrlStopped" + case .contactPQEnabled: "contactPQEnabled" + } + } + + var details: String { + switch self { + case .chatSuspended: return noDetails + case let .contactSwitch(u, contact, switchProgress): return withUser(u, "contact: \(String(describing: contact))\nswitchProgress: \(String(describing: switchProgress))") + case let .groupMemberSwitch(u, groupInfo, member, switchProgress): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nswitchProgress: \(String(describing: switchProgress))") + case let .contactRatchetSync(u, contact, ratchetSyncProgress): return withUser(u, "contact: \(String(describing: contact))\nratchetSyncProgress: \(String(describing: ratchetSyncProgress))") + case let .groupMemberRatchetSync(u, groupInfo, member, ratchetSyncProgress): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nratchetSyncProgress: \(String(describing: ratchetSyncProgress))") + case let .contactDeletedByContact(u, contact): return withUser(u, String(describing: contact)) + case let .contactConnected(u, contact, _): return withUser(u, String(describing: contact)) + case let .contactConnecting(u, contact): return withUser(u, String(describing: contact)) + case let .contactSndReady(u, contact): return withUser(u, String(describing: contact)) + case let .receivedContactRequest(u, contactRequest): return withUser(u, String(describing: contactRequest)) + case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact)) + case let .groupMemberUpdated(u, groupInfo, fromMember, toMember): return withUser(u, "groupInfo: \(groupInfo)\nfromMember: \(fromMember)\ntoMember: \(toMember)") + case let .contactsMerged(u, intoContact, mergedContact): return withUser(u, "intoContact: \(intoContact)\nmergedContact: \(mergedContact)") + case let .networkStatus(status, conns): return "networkStatus: \(String(describing: status))\nconnections: \(String(describing: conns))" + case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses)) + case let .newChatItems(u, chatItems): + let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n") + return withUser(u, itemsString) + case let .chatItemsStatusesUpdated(u, chatItems): + let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n") + return withUser(u, itemsString) + case let .chatItemUpdated(u, chatItem): return withUser(u, String(describing: chatItem)) + case let .chatItemReaction(u, added, reaction): return withUser(u, "added: \(added)\n\(String(describing: reaction))") + case let .chatItemsDeleted(u, items, byUser): + let itemsString = items.map { item in + "deletedChatItem:\n\(String(describing: item.deletedChatItem))\ntoChatItem:\n\(String(describing: item.toChatItem))" }.joined(separator: "\n") + return withUser(u, itemsString + "\nbyUser: \(byUser)") + 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 .receivedGroupInvitation(u, groupInfo, contact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\ncontact: \(contact)\nmemberRole: \(memberRole)") + case let .userAcceptedGroupSent(u, groupInfo, hostContact): return withUser(u, "groupInfo: \(groupInfo)\nhostContact: \(String(describing: hostContact))") + case let .groupLinkConnecting(u, groupInfo, hostMember): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(String(describing: hostMember))") + case let .businessLinkConnecting(u, groupInfo, hostMember, fromContact): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(String(describing: hostMember))\nfromContact: \(String(describing: fromContact))") + case let .joinedGroupMemberConnecting(u, groupInfo, hostMember, member): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(hostMember)\nmember: \(member)") + case let .memberRole(u, groupInfo, byMember, member, fromRole, toRole): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\nmember: \(member)\nfromRole: \(fromRole)\ntoRole: \(toRole)") + case let .memberBlockedForAll(u, groupInfo, byMember, member, blocked): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\nmember: \(member)\nblocked: \(blocked)") + case let .deletedMemberUser(u, groupInfo, member, withMessages): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)\nwithMessages: \(withMessages)") + case let .deletedMember(u, groupInfo, byMember, deletedMember, withMessages): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\ndeletedMember: \(deletedMember)\nwithMessages: \(withMessages)") + case let .leftMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") + case let .groupDeleted(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") + case let .userJoinedGroup(u, groupInfo): return withUser(u, String(describing: groupInfo)) + case let .joinedGroupMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") + case let .connectedToGroupMember(u, groupInfo, member, memberContact): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)\nmemberContact: \(String(describing: memberContact))") + case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup)) + case let .newMemberContactReceivedInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)") + case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem)) + case .rcvFileAcceptedSndCancelled: return noDetails + case let .rcvFileStart(u, chatItem): return withUser(u, String(describing: chatItem)) + case let .rcvFileProgressXFTP(u, chatItem, receivedSize, totalSize, _): return withUser(u, "chatItem: \(String(describing: chatItem))\nreceivedSize: \(receivedSize)\ntotalSize: \(totalSize)") + case let .rcvStandaloneFileComplete(u, targetPath, _): return withUser(u, targetPath) + case let .rcvFileComplete(u, chatItem): return withUser(u, String(describing: chatItem)) + case let .rcvFileSndCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .rcvFileError(u, chatItem, agentError, _): return withUser(u, "agentError: \(String(describing: agentError))\nchatItem: \(String(describing: chatItem))") + case let .rcvFileWarning(u, chatItem, agentError, _): return withUser(u, "agentError: \(String(describing: agentError))\nchatItem: \(String(describing: chatItem))") + case let .sndFileStart(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .sndFileComplete(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .sndFileRcvCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .sndFileProgressXFTP(u, chatItem, _, sentSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nsentSize: \(sentSize)\ntotalSize: \(totalSize)") + case let .sndFileRedirectStartXFTP(u, _, redirectMeta): return withUser(u, String(describing: redirectMeta)) + case let .sndFileCompleteXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .sndStandaloneFileComplete(u, _, rcvURIs): return withUser(u, String(rcvURIs.count)) + case let .sndFileError(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))") + case let .sndFileWarning(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))") + case let .callInvitation(inv): return String(describing: inv) + case let .callOffer(u, contact, callType, offer, sharedKey, askConfirmation): return withUser(u, "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))") + case let .callAnswer(u, contact, answer): return withUser(u, "contact: \(contact.id)\nanswer: \(String(describing: answer))") + case let .callExtraInfo(u, contact, extraInfo): return withUser(u, "contact: \(contact.id)\nextraInfo: \(String(describing: extraInfo))") + case let .callEnded(u, contact): return withUser(u, "contact: \(contact.id)") + case let .contactDisabled(u, contact): return withUser(u, String(describing: contact)) + case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))") + case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible): return "remoteCtrl:\n\(String(describing: remoteCtrl))\nctrlAppInfo_:\n\(String(describing: ctrlAppInfo_))\nappVersion: \(appVersion)\ncompatible: \(compatible)" + case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nsessionCode: \(sessionCode)" + case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl) + case let .remoteCtrlStopped(rcsState, rcStopReason): return "rcsState: \(String(describing: rcsState))\nrcStopReason: \(String(describing: rcStopReason))" + case let .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)") + } + } +} + +struct NewUser: Encodable { + var profile: Profile? + var pastTimestamp: Bool +} + +enum ChatPagination { + static let INITIAL_COUNT = 75 + static let PRELOAD_COUNT = 100 + static let UNTIL_PRELOAD_COUNT = 50 + + case last(count: Int) + case after(chatItemId: Int64, count: Int) + case before(chatItemId: Int64, count: Int) + case around(chatItemId: Int64, count: Int) + case initial(count: Int) + + var cmdString: String { + switch self { + case let .last(count): return "count=\(count)" + case let .after(chatItemId, count): return "after=\(chatItemId) count=\(count)" + case let .before(chatItemId, count): return "before=\(chatItemId) count=\(count)" + case let .around(chatItemId, count): return "around=\(chatItemId) count=\(count)" + case let .initial(count): return "initial=\(count)" + } + } +} + +enum ConnectionPlan: Decodable, Hashable { + case invitationLink(invitationLinkPlan: InvitationLinkPlan) + case contactAddress(contactAddressPlan: ContactAddressPlan) + case groupLink(groupLinkPlan: GroupLinkPlan) + case error(chatError: ChatError) +} + +enum InvitationLinkPlan: Decodable, Hashable { + case ok + case ownLink + case connecting(contact_: Contact?) + case known(contact: Contact) +} + +enum ContactAddressPlan: Decodable, Hashable { + case ok + case ownLink + case connectingConfirmReconnect + case connectingProhibit(contact: Contact) + case known(contact: Contact) + case contactViaAddress(contact: Contact) +} + +enum GroupLinkPlan: Decodable, Hashable { + case ok + case ownLink(groupInfo: GroupInfo) + case connectingConfirmReconnect + case connectingProhibit(groupInfo_: GroupInfo?) + case known(groupInfo: GroupInfo) +} + +struct ChatTagData: Encodable { + var emoji: String? + var text: String +} + +struct UpdatedMessage: Encodable { + var msgContent: MsgContent + var mentions: [String: Int64] + + var cmdString: String { + "json \(encodeJSON(self))" + } +} + +enum ChatDeleteMode: Codable { + case full(notify: Bool) + case entity(notify: Bool) + case messages + + var cmdString: String { + switch self { + case let .full(notify): "full notify=\(onOff(notify))" + case let .entity(notify): "entity notify=\(onOff(notify))" + case .messages: "messages" + } + } + + var isEntity: Bool { + switch self { + case .entity: return true + default: return false + } + } +} + +enum NetworkStatus: Decodable, Equatable { + case unknown + case connected + case disconnected + case error(connectionError: String) + + var statusString: LocalizedStringKey { + switch self { + case .connected: "connected" + case .error: "error" + default: "connecting" + } + } + + var statusExplanation: LocalizedStringKey { + switch self { + case .connected: "You are connected to the server used to receive messages from this contact." + case let .error(err): "Trying to connect to the server used to receive messages from this contact (error: \(err))." + default: "Trying to connect to the server used to receive messages from this contact." + } + } + + var imageName: String { + switch self { + case .unknown: "circle.dotted" + case .connected: "circle.fill" + case .disconnected: "ellipsis.circle.fill" + case .error: "exclamationmark.circle.fill" + } + } +} + +enum ForwardConfirmation: Decodable, Hashable { + case filesNotAccepted(fileIds: [Int64]) + case filesInProgress(filesCount: Int) + case filesMissing(filesCount: Int) + case filesFailed(filesCount: Int) +} + +struct ConnNetworkStatus: Decodable { + var agentConnId: String + var networkStatus: NetworkStatus +} + +struct UserMsgReceiptSettings: Codable { + var enable: Bool + var clearOverrides: Bool +} + + +struct UserContactLink: Decodable, Hashable { + var connLinkContact: CreatedConnLink + var autoAccept: AutoAccept? + + var responseDetails: String { + "connLinkContact: \(connLinkContact)\nautoAccept: \(AutoAccept.cmdString(autoAccept))" + } +} + +struct AutoAccept: Codable, Hashable { + var businessAddress: Bool + var acceptIncognito: Bool + var autoReply: MsgContent? + + static func cmdString(_ autoAccept: AutoAccept?) -> String { + guard let autoAccept = autoAccept else { return "off" } + var s = "on" + if autoAccept.acceptIncognito { + s += " incognito=on" + } else if autoAccept.businessAddress { + s += " business" + } + guard let msg = autoAccept.autoReply else { return s } + return s + " " + msg.cmdString + } +} + +struct DeviceToken: Decodable { + var pushProvider: PushProvider + var token: String + + var cmdString: String { + "\(pushProvider) \(token)" + } +} + +enum PushEnvironment: String { + case development + case production +} + +enum PushProvider: String, Decodable { + case apns_dev + case apns_prod + + init(env: PushEnvironment) { + switch env { + case .development: self = .apns_dev + case .production: self = .apns_prod + } + } +} + +// This notification mode is for app core, UI uses AppNotificationsMode.off to mean completely disable, +// and .local for periodic background checks +enum NotificationsMode: String, Decodable, SelectableItem { + case off = "OFF" + case periodic = "PERIODIC" + case instant = "INSTANT" + + var label: LocalizedStringKey { + switch self { + case .off: "No push server" + case .periodic: "Periodic" + case .instant: "Instant" + } + } + + var icon: String { + switch self { + case .off: return "arrow.clockwise" + case .periodic: return "timer" + case .instant: return "bolt" + } + } + + var id: String { self.rawValue } + + static var values: [NotificationsMode] = [.instant, .periodic, .off] +} + +struct RemoteCtrlInfo: Decodable { + var remoteCtrlId: Int64 + var ctrlDeviceName: String + var sessionState: RemoteCtrlSessionState? + + var deviceViewName: String { + ctrlDeviceName == "" ? "\(remoteCtrlId)" : ctrlDeviceName + } +} + +enum RemoteCtrlSessionState: Decodable { + case starting + case searching + case connecting + case pendingConfirmation(sessionCode: String) + case connected(sessionCode: String) +} + +enum RemoteCtrlStopReason: Decodable { + case discoveryFailed(chatError: ChatError) + case connectionFailed(chatError: ChatError) + case setupFailed(chatError: ChatError) + case disconnected +} + +struct CtrlAppInfo: Decodable { + var appVersionRange: AppVersionRange + var deviceName: String +} + +struct AppVersionRange: Decodable { + var minVersion: String + var maxVersion: String +} + +struct CoreVersionInfo: Decodable { + var version: String + var simplexmqVersion: String + var simplexmqCommit: String +} + +struct ArchiveConfig: Encodable { + var archivePath: String + var disableCompression: Bool? +} + +struct DBEncryptionConfig: Codable { + var currentKey: String + var newKey: String +} + +enum OperatorTag: String, Codable { + case simplex = "simplex" + case flux = "flux" +} + +struct ServerOperatorInfo { + var description: [String] + var website: URL + var selfhost: (text: String, link: URL)? = nil + var logo: String + var largeLogo: String + var logoDarkMode: String + var largeLogoDarkMode: String +} + +let operatorsInfo: Dictionary = [ + .simplex: ServerOperatorInfo( + description: [ + "SimpleX Chat is the first communication network that has no user profile IDs of any kind, not even random numbers or identity keys.", + "SimpleX Chat Ltd develops the communication software for SimpleX network." + ], + website: URL(string: "https://simplex.chat")!, + logo: "decentralized", + largeLogo: "logo", + logoDarkMode: "decentralized-light", + largeLogoDarkMode: "logo-light" + ), + .flux: ServerOperatorInfo( + description: [ + "Flux is the largest decentralized cloud, based on a global network of user-operated nodes.", + "Flux offers a powerful, scalable, and affordable cutting edge technology platform for all.", + "Flux operates servers in SimpleX network to improve its privacy and decentralization." + ], + website: URL(string: "https://runonflux.com")!, + selfhost: (text: "Self-host SimpleX servers on Flux", link: URL(string: "https://home.runonflux.io/apps/marketplace?q=simplex")!), + logo: "flux_logo_symbol", + largeLogo: "flux_logo", + logoDarkMode: "flux_logo_symbol", + largeLogoDarkMode: "flux_logo-light" + ), +] + +struct UsageConditions: Decodable { + var conditionsId: Int64 + var conditionsCommit: String + var notifiedAt: Date? + var createdAt: Date + + static var sampleData = UsageConditions( + conditionsId: 1, + conditionsCommit: "11a44dc1fd461a93079f897048b46998db55da5c", + notifiedAt: nil, + createdAt: Date.now + ) +} + +enum UsageConditionsAction: Decodable { + case review(operators: [ServerOperator], deadline: Date?, showNotice: Bool) + case accepted(operators: [ServerOperator]) + + var showNotice: Bool { + switch self { + case let .review(_, _, showNotice): showNotice + case .accepted: false + } + } +} + +struct ServerOperatorConditions: Decodable { + var serverOperators: [ServerOperator] + var currentConditions: UsageConditions + var conditionsAction: UsageConditionsAction? + + static var empty = ServerOperatorConditions( + serverOperators: [], + currentConditions: UsageConditions(conditionsId: 0, conditionsCommit: "empty", notifiedAt: nil, createdAt: .now), + conditionsAction: nil + ) +} + +enum ConditionsAcceptance: Equatable, Codable, Hashable { + case accepted(acceptedAt: Date?, autoAccepted: Bool) + // If deadline is present, it means there's a grace period to review and accept conditions during which user can continue to use the operator. + // No deadline indicates it's required to accept conditions for the operator to start using it. + case required(deadline: Date?) + + var conditionsAccepted: Bool { + switch self { + case .accepted: true + case .required: false + } + } + + var usageAllowed: Bool { + switch self { + case .accepted: true + case let .required(deadline): deadline != nil + } + } +} + +struct ServerOperator: Identifiable, Equatable, Codable { + var operatorId: Int64 + var operatorTag: OperatorTag? + var tradeName: String + var legalName: String? + var serverDomains: [String] + var conditionsAcceptance: ConditionsAcceptance + var enabled: Bool + var smpRoles: ServerRoles + var xftpRoles: ServerRoles + + var id: Int64 { operatorId } + + static func == (l: ServerOperator, r: ServerOperator) -> Bool { + l.operatorId == r.operatorId && l.operatorTag == r.operatorTag && l.tradeName == r.tradeName && l.legalName == r.legalName && + l.serverDomains == r.serverDomains && l.conditionsAcceptance == r.conditionsAcceptance && l.enabled == r.enabled && + l.smpRoles == r.smpRoles && l.xftpRoles == r.xftpRoles + } + + var legalName_: String { + legalName ?? tradeName + } + + var info: ServerOperatorInfo { + return if let operatorTag = operatorTag { + operatorsInfo[operatorTag] ?? ServerOperator.dummyOperatorInfo + } else { + ServerOperator.dummyOperatorInfo + } + } + + static let dummyOperatorInfo = ServerOperatorInfo( + description: ["Default"], + website: URL(string: "https://simplex.chat")!, + logo: "decentralized", + largeLogo: "logo", + logoDarkMode: "decentralized-light", + largeLogoDarkMode: "logo-light" + ) + + func logo(_ colorScheme: ColorScheme) -> String { + colorScheme == .light ? info.logo : info.logoDarkMode + } + + func largeLogo(_ colorScheme: ColorScheme) -> String { + colorScheme == .light ? info.largeLogo : info.largeLogoDarkMode + } + + static var sampleData1 = ServerOperator( + operatorId: 1, + operatorTag: .simplex, + tradeName: "SimpleX Chat", + legalName: "SimpleX Chat Ltd", + serverDomains: ["simplex.im"], + conditionsAcceptance: .accepted(acceptedAt: nil, autoAccepted: false), + enabled: true, + smpRoles: ServerRoles(storage: true, proxy: true), + xftpRoles: ServerRoles(storage: true, proxy: true) + ) +} + +struct ServerRoles: Equatable, Codable { + var storage: Bool + var proxy: Bool +} + +struct UserOperatorServers: Identifiable, Equatable, Codable { + var `operator`: ServerOperator? + var smpServers: [UserServer] + var xftpServers: [UserServer] + + var id: String { + if let op = self.operator { + "\(op.operatorId)" + } else { + "nil operator" + } + } + + var operator_: ServerOperator { + get { + self.operator ?? ServerOperator( + operatorId: 0, + operatorTag: nil, + tradeName: "", + legalName: "", + serverDomains: [], + conditionsAcceptance: .accepted(acceptedAt: nil, autoAccepted: false), + enabled: false, + smpRoles: ServerRoles(storage: true, proxy: true), + xftpRoles: ServerRoles(storage: true, proxy: true) + ) + } + set { `operator` = newValue } + } + + static var sampleData1 = UserOperatorServers( + operator: ServerOperator.sampleData1, + smpServers: [UserServer.sampleData.preset], + xftpServers: [UserServer.sampleData.xftpPreset] + ) + + static var sampleDataNilOperator = UserOperatorServers( + operator: nil, + smpServers: [UserServer.sampleData.preset], + xftpServers: [UserServer.sampleData.xftpPreset] + ) +} + +enum UserServersError: Decodable { + case noServers(protocol: ServerProtocol, user: UserRef?) + case storageMissing(protocol: ServerProtocol, user: UserRef?) + case proxyMissing(protocol: ServerProtocol, user: UserRef?) + case duplicateServer(protocol: ServerProtocol, duplicateServer: String, duplicateHost: String) + + var globalError: String? { + switch self { + case let .noServers(`protocol`, _): + switch `protocol` { + case .smp: return globalSMPError + case .xftp: return globalXFTPError + } + case let .storageMissing(`protocol`, _): + switch `protocol` { + case .smp: return globalSMPError + case .xftp: return globalXFTPError + } + case let .proxyMissing(`protocol`, _): + switch `protocol` { + case .smp: return globalSMPError + case .xftp: return globalXFTPError + } + default: return nil + } + } + + var globalSMPError: String? { + switch self { + case let .noServers(.smp, user): + let text = NSLocalizedString("No message servers.", comment: "servers error") + if let user = user { + return userStr(user) + " " + text + } else { + return text + } + case let .storageMissing(.smp, user): + let text = NSLocalizedString("No servers to receive messages.", comment: "servers error") + if let user = user { + return userStr(user) + " " + text + } else { + return text + } + case let .proxyMissing(.smp, user): + let text = NSLocalizedString("No servers for private message routing.", comment: "servers error") + if let user = user { + return userStr(user) + " " + text + } else { + return text + } + default: + return nil + } + } + + var globalXFTPError: String? { + switch self { + case let .noServers(.xftp, user): + let text = NSLocalizedString("No media & file servers.", comment: "servers error") + if let user = user { + return userStr(user) + " " + text + } else { + return text + } + case let .storageMissing(.xftp, user): + let text = NSLocalizedString("No servers to send files.", comment: "servers error") + if let user = user { + return userStr(user) + " " + text + } else { + return text + } + case let .proxyMissing(.xftp, user): + let text = NSLocalizedString("No servers to receive files.", comment: "servers error") + if let user = user { + return userStr(user) + " " + text + } else { + return text + } + default: + return nil + } + } + + private func userStr(_ user: UserRef) -> String { + String.localizedStringWithFormat(NSLocalizedString("For chat profile %@:", comment: "servers error"), user.localDisplayName) + } +} + +struct UserServer: Identifiable, Equatable, Codable, Hashable { + var serverId: Int64? + var server: String + var preset: Bool + var tested: Bool? + var enabled: Bool + var deleted: Bool + var createdAt = Date() + + static func == (l: UserServer, r: UserServer) -> Bool { + l.serverId == r.serverId && l.server == r.server && l.preset == r.preset && l.tested == r.tested && + l.enabled == r.enabled && l.deleted == r.deleted + } + + var id: String { "\(server) \(createdAt)" } + + static var empty = UserServer(serverId: nil, server: "", preset: false, tested: nil, enabled: false, deleted: false) + + var isEmpty: Bool { + server.trimmingCharacters(in: .whitespaces) == "" + } + + struct SampleData { + var preset: UserServer + var custom: UserServer + var untested: UserServer + var xftpPreset: UserServer + } + + static var sampleData = SampleData( + preset: UserServer( + serverId: 1, + server: "smp://abcd@smp8.simplex.im", + preset: true, + tested: true, + enabled: true, + deleted: false + ), + custom: UserServer( + serverId: 2, + server: "smp://abcd@smp9.simplex.im", + preset: false, + tested: false, + enabled: false, + deleted: false + ), + untested: UserServer( + serverId: 3, + server: "smp://abcd@smp10.simplex.im", + preset: false, + tested: nil, + enabled: true, + deleted: false + ), + xftpPreset: UserServer( + serverId: 4, + server: "xftp://abcd@xftp8.simplex.im", + preset: true, + tested: true, + enabled: true, + deleted: false + ) + ) + + enum CodingKeys: CodingKey { + case serverId + case server + case preset + case tested + case enabled + case deleted + } +} + +enum ProtocolTestStep: String, Decodable, Equatable { + case connect + case disconnect + case createQueue + case secureQueue + case deleteQueue + case createFile + case uploadFile + case downloadFile + case compareFile + case deleteFile + + var text: String { + switch self { + case .connect: return NSLocalizedString("Connect", comment: "server test step") + case .disconnect: return NSLocalizedString("Disconnect", comment: "server test step") + case .createQueue: return NSLocalizedString("Create queue", comment: "server test step") + case .secureQueue: return NSLocalizedString("Secure queue", comment: "server test step") + case .deleteQueue: return NSLocalizedString("Delete queue", comment: "server test step") + case .createFile: return NSLocalizedString("Create file", comment: "server test step") + case .uploadFile: return NSLocalizedString("Upload file", comment: "server test step") + case .downloadFile: return NSLocalizedString("Download file", comment: "server test step") + case .compareFile: return NSLocalizedString("Compare file", comment: "server test step") + case .deleteFile: return NSLocalizedString("Delete file", comment: "server test step") + } + } +} + +struct ProtocolTestFailure: Decodable, Error, Equatable { + var testStep: ProtocolTestStep + var testError: AgentErrorType + + static func == (l: ProtocolTestFailure, r: ProtocolTestFailure) -> Bool { + l.testStep == r.testStep + } + + var localizedDescription: String { + let err = String.localizedStringWithFormat(NSLocalizedString("Test failed at step %@.", comment: "server test failure"), testStep.text) + switch testError { + case .SMP(_, .AUTH): + return err + " " + NSLocalizedString("Server requires authorization to create queues, check password", comment: "server test error") + case .XFTP(.AUTH): + return err + " " + NSLocalizedString("Server requires authorization to upload, check password", comment: "server test error") + case .BROKER(_, .NETWORK): + return err + " " + NSLocalizedString("Possibly, certificate fingerprint in server address is incorrect", comment: "server test error") + default: + return err + } + } +} + +struct MigrationFileLinkData: Codable { + let networkConfig: NetworkConfig? + + struct NetworkConfig: Codable { + let socksProxy: String? + let networkProxy: NetworkProxy? + let hostMode: HostMode? + let requiredHostMode: Bool? + + func transformToPlatformSupported() -> NetworkConfig { + return if let hostMode, let requiredHostMode { + NetworkConfig( + socksProxy: nil, + networkProxy: nil, + hostMode: hostMode == .onionViaSocks ? .onionHost : hostMode, + requiredHostMode: requiredHostMode + ) + } else { self } + } + } + + func addToLink(link: String) -> String { + "\(link)&data=\(encodeJSON(self).addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!)" + } + + static func readFromLink(link: String) -> MigrationFileLinkData? { +// standaloneFileInfo(link) + nil + } +} + +struct AppSettings: Codable, Equatable { + var networkConfig: NetCfg? = nil + var networkProxy: NetworkProxy? = nil + var privacyEncryptLocalFiles: Bool? = nil + var privacyAskToApproveRelays: Bool? = nil + var privacyAcceptImages: Bool? = nil + var privacyLinkPreviews: Bool? = nil + var privacyShowChatPreviews: Bool? = nil + var privacySaveLastDraft: Bool? = nil + var privacyProtectScreen: Bool? = nil + var privacyMediaBlurRadius: Int? = nil + var notificationMode: AppSettingsNotificationMode? = nil + var notificationPreviewMode: NotificationPreviewMode? = nil + var webrtcPolicyRelay: Bool? = nil + var webrtcICEServers: [String]? = nil + var confirmRemoteSessions: Bool? = nil + var connectRemoteViaMulticast: Bool? = nil + var connectRemoteViaMulticastAuto: Bool? = nil + var developerTools: Bool? = nil + var confirmDBUpgrades: Bool? = nil + var androidCallOnLockScreen: AppSettingsLockScreenCalls? = nil + var iosCallKitEnabled: Bool? = nil + var iosCallKitCallsInRecents: Bool? = nil + var uiProfileImageCornerRadius: Double? = nil + var uiChatItemRoundness: Double? = nil + var uiChatItemTail: Bool? = nil + var uiColorScheme: String? = nil + var uiDarkColorScheme: String? = nil + var uiCurrentThemeIds: [String: String]? = nil + var uiThemes: [ThemeOverrides]? = nil + var oneHandUI: Bool? = nil + var chatBottomBar: Bool? = nil + + func prepareForExport() -> AppSettings { + var empty = AppSettings() + let def = AppSettings.defaults + if networkConfig != def.networkConfig { empty.networkConfig = networkConfig } + if networkProxy != def.networkProxy { empty.networkProxy = networkProxy } + if privacyEncryptLocalFiles != def.privacyEncryptLocalFiles { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles } + if privacyAskToApproveRelays != def.privacyAskToApproveRelays { empty.privacyAskToApproveRelays = privacyAskToApproveRelays } + if privacyAcceptImages != def.privacyAcceptImages { empty.privacyAcceptImages = privacyAcceptImages } + if privacyLinkPreviews != def.privacyLinkPreviews { empty.privacyLinkPreviews = privacyLinkPreviews } + if privacyShowChatPreviews != def.privacyShowChatPreviews { empty.privacyShowChatPreviews = privacyShowChatPreviews } + if privacySaveLastDraft != def.privacySaveLastDraft { empty.privacySaveLastDraft = privacySaveLastDraft } + if privacyProtectScreen != def.privacyProtectScreen { empty.privacyProtectScreen = privacyProtectScreen } + if privacyMediaBlurRadius != def.privacyMediaBlurRadius { empty.privacyMediaBlurRadius = privacyMediaBlurRadius } + if notificationMode != def.notificationMode { empty.notificationMode = notificationMode } + if notificationPreviewMode != def.notificationPreviewMode { empty.notificationPreviewMode = notificationPreviewMode } + if webrtcPolicyRelay != def.webrtcPolicyRelay { empty.webrtcPolicyRelay = webrtcPolicyRelay } + if webrtcICEServers != def.webrtcICEServers { empty.webrtcICEServers = webrtcICEServers } + if confirmRemoteSessions != def.confirmRemoteSessions { empty.confirmRemoteSessions = confirmRemoteSessions } + if connectRemoteViaMulticast != def.connectRemoteViaMulticast {empty.connectRemoteViaMulticast = connectRemoteViaMulticast } + if connectRemoteViaMulticastAuto != def.connectRemoteViaMulticastAuto { empty.connectRemoteViaMulticastAuto = connectRemoteViaMulticastAuto } + if developerTools != def.developerTools { empty.developerTools = developerTools } + if confirmDBUpgrades != def.confirmDBUpgrades { empty.confirmDBUpgrades = confirmDBUpgrades } + if androidCallOnLockScreen != def.androidCallOnLockScreen { empty.androidCallOnLockScreen = androidCallOnLockScreen } + if iosCallKitEnabled != def.iosCallKitEnabled { empty.iosCallKitEnabled = iosCallKitEnabled } + if iosCallKitCallsInRecents != def.iosCallKitCallsInRecents { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents } + if uiProfileImageCornerRadius != def.uiProfileImageCornerRadius { empty.uiProfileImageCornerRadius = uiProfileImageCornerRadius } + if uiChatItemRoundness != def.uiChatItemRoundness { empty.uiChatItemRoundness = uiChatItemRoundness } + if uiChatItemTail != def.uiChatItemTail { empty.uiChatItemTail = uiChatItemTail } + if uiColorScheme != def.uiColorScheme { empty.uiColorScheme = uiColorScheme } + if uiDarkColorScheme != def.uiDarkColorScheme { empty.uiDarkColorScheme = uiDarkColorScheme } + if uiCurrentThemeIds != def.uiCurrentThemeIds { empty.uiCurrentThemeIds = uiCurrentThemeIds } + if uiThemes != def.uiThemes { empty.uiThemes = uiThemes } + if oneHandUI != def.oneHandUI { empty.oneHandUI = oneHandUI } + if chatBottomBar != def.chatBottomBar { empty.chatBottomBar = chatBottomBar } + return empty + } + + static var defaults: AppSettings { + AppSettings ( + networkConfig: NetCfg.defaults, + networkProxy: NetworkProxy.def, + privacyEncryptLocalFiles: true, + privacyAskToApproveRelays: true, + privacyAcceptImages: true, + privacyLinkPreviews: true, + privacyShowChatPreviews: true, + privacySaveLastDraft: true, + privacyProtectScreen: false, + privacyMediaBlurRadius: 0, + notificationMode: AppSettingsNotificationMode.instant, + notificationPreviewMode: NotificationPreviewMode.message, + webrtcPolicyRelay: true, + webrtcICEServers: [], + confirmRemoteSessions: false, + connectRemoteViaMulticast: true, + connectRemoteViaMulticastAuto: true, + developerTools: false, + confirmDBUpgrades: false, + androidCallOnLockScreen: AppSettingsLockScreenCalls.show, + iosCallKitEnabled: true, + iosCallKitCallsInRecents: false, + uiProfileImageCornerRadius: 22.5, + uiChatItemRoundness: 0.75, + uiChatItemTail: true, + uiColorScheme: DefaultTheme.SYSTEM_THEME_NAME, + uiDarkColorScheme: DefaultTheme.SIMPLEX.themeName, + uiCurrentThemeIds: nil as [String: String]?, + uiThemes: nil as [ThemeOverrides]?, + oneHandUI: true, + chatBottomBar: true + ) + } +} + +enum AppSettingsNotificationMode: String, Codable { + case off + case periodic + case instant + + func toNotificationsMode() -> NotificationsMode { + switch self { + case .instant: .instant + case .periodic: .periodic + case .off: .off + } + } + + static func from(_ mode: NotificationsMode) -> AppSettingsNotificationMode { + switch mode { + case .instant: .instant + case .periodic: .periodic + case .off: .off + } + } +} + +//enum NotificationPreviewMode: Codable { +// case hidden +// case contact +// case message +//} + +enum AppSettingsLockScreenCalls: String, Codable { + case disable + case show + case accept +} + +struct UserNetworkInfo: Codable, Equatable { + let networkType: UserNetworkType + let online: Bool +} + +enum UserNetworkType: String, Codable { + case none + case cellular + case wifi + case ethernet + case other + + var text: LocalizedStringKey { + switch self { + case .none: "No network connection" + case .cellular: "Cellular" + case .wifi: "WiFi" + case .ethernet: "Wired ethernet" + case .other: "Other" + } + } +} + +struct RcvMsgInfo: Codable { + var msgId: Int64 + var msgDeliveryId: Int64 + var msgDeliveryStatus: String + var agentMsgId: Int64 + var agentMsgMeta: String +} + +struct ServerQueueInfo: Codable { + var server: String + var rcvId: String + var sndId: String + var ntfId: String? + var status: String + var info: QueueInfo +} + +struct QueueInfo: Codable { + var qiSnd: Bool + var qiNtf: Bool + var qiSub: QSub? + var qiSize: Int + var qiMsg: MsgInfo? +} + +struct QSub: Codable { + var qSubThread: QSubThread + var qDelivered: String? +} + +enum QSubThread: String, Codable { + case noSub + case subPending + case subThread + case prohibitSub +} + +struct MsgInfo: Codable { + var msgId: String + var msgTs: Date + var msgType: MsgType +} + +enum MsgType: String, Codable { + case message + case quota +} + +struct PresentedServersSummary: Codable { + var statsStartedAt: Date + var allUsersSMP: SMPServersSummary + var allUsersXFTP: XFTPServersSummary + var currentUserSMP: SMPServersSummary + var currentUserXFTP: XFTPServersSummary +} + +struct SMPServersSummary: Codable { + var smpTotals: SMPTotals + var currentlyUsedSMPServers: [SMPServerSummary] + var previouslyUsedSMPServers: [SMPServerSummary] + var onlyProxiedSMPServers: [SMPServerSummary] +} + +struct SMPTotals: Codable { + var sessions: ServerSessions + var subs: SMPServerSubs + var stats: AgentSMPServerStatsData +} + +struct SMPServerSummary: Codable, Identifiable { + var smpServer: String + var known: Bool? + var sessions: ServerSessions? + var subs: SMPServerSubs? + var stats: AgentSMPServerStatsData? + + var id: String { smpServer } + + var hasSubs: Bool { subs != nil } + + var sessionsOrNew: ServerSessions { sessions ?? ServerSessions.newServerSessions } + + var subsOrNew: SMPServerSubs { subs ?? SMPServerSubs.newSMPServerSubs } +} + +struct ServerSessions: Codable { + var ssConnected: Int + var ssErrors: Int + var ssConnecting: Int + + static var newServerSessions = ServerSessions( + ssConnected: 0, + ssErrors: 0, + ssConnecting: 0 + ) + + var hasSess: Bool { ssConnected > 0 } +} + +struct SMPServerSubs: Codable { + var ssActive: Int + var ssPending: Int + + static var newSMPServerSubs = SMPServerSubs( + ssActive: 0, + ssPending: 0 + ) + + var total: Int { ssActive + ssPending } + + var shareOfActive: Double { + guard total != 0 else { return 0.0 } + return Double(ssActive) / Double(total) + } +} + +struct AgentSMPServerStatsData: Codable { + var _sentDirect: Int + var _sentViaProxy: Int + var _sentProxied: Int + var _sentDirectAttempts: Int + var _sentViaProxyAttempts: Int + var _sentProxiedAttempts: Int + var _sentAuthErrs: Int + var _sentQuotaErrs: Int + var _sentExpiredErrs: Int + var _sentOtherErrs: Int + var _recvMsgs: Int + var _recvDuplicates: Int + var _recvCryptoErrs: Int + var _recvErrs: Int + var _ackMsgs: Int + var _ackAttempts: Int + var _ackNoMsgErrs: Int + var _ackOtherErrs: Int + var _connCreated: Int + var _connSecured: Int + var _connCompleted: Int + var _connDeleted: Int + var _connDelAttempts: Int + var _connDelErrs: Int + var _connSubscribed: Int + var _connSubAttempts: Int + var _connSubIgnored: Int + var _connSubErrs: Int + var _ntfKey: Int + var _ntfKeyAttempts: Int + var _ntfKeyDeleted: Int + var _ntfKeyDeleteAttempts: Int +} + +struct XFTPServersSummary: Codable { + var xftpTotals: XFTPTotals + var currentlyUsedXFTPServers: [XFTPServerSummary] + var previouslyUsedXFTPServers: [XFTPServerSummary] +} + +struct XFTPTotals: Codable { + var sessions: ServerSessions + var stats: AgentXFTPServerStatsData +} + +struct XFTPServerSummary: Codable, Identifiable { + var xftpServer: String + var known: Bool? + var sessions: ServerSessions? + var stats: AgentXFTPServerStatsData? + var rcvInProgress: Bool + var sndInProgress: Bool + var delInProgress: Bool + + var id: String { xftpServer } +} + +struct AgentXFTPServerStatsData: Codable { + var _uploads: Int + var _uploadsSize: Int64 + var _uploadAttempts: Int + var _uploadErrs: Int + var _downloads: Int + var _downloadsSize: Int64 + var _downloadAttempts: Int + var _downloadAuthErrs: Int + var _downloadErrs: Int + var _deletions: Int + var _deleteAttempts: Int + var _deleteErrs: Int +} + +struct AgentNtfServerStatsData: Codable { + var _ntfCreated: Int + var _ntfCreateAttempts: Int + var _ntfChecked: Int + var _ntfCheckAttempts: Int + var _ntfDeleted: Int + var _ntfDelAttempts: Int +} diff --git a/apps/ios/Shared/Model/AudioRecPlay.swift b/apps/ios/Shared/Model/AudioRecPlay.swift index a9d0d6c1d9..99851f4be8 100644 --- a/apps/ios/Shared/Model/AudioRecPlay.swift +++ b/apps/ios/Shared/Model/AudioRecPlay.swift @@ -179,7 +179,7 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate { if playback { if AVAudioSession.sharedInstance().category != .playback { logger.log("AudioSession: playback") - try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, options: .duckOthers) + try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, options: [.duckOthers, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP]) } } else { if AVAudioSession.sharedInstance().category != .soloAmbient { diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 0cc281fda9..9b9fda0397 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -30,9 +30,18 @@ actor TerminalItems { } } - func addCommand(_ start: Date, _ cmd: ChatCommand, _ resp: ChatResponse) async { + func addCommand(_ start: Date, _ cmd: ChatCommand, _ res: APIResult) async { await add(.cmd(start, cmd)) - await add(.resp(.now, resp)) + await addResult(res) + } + + func addResult(_ res: APIResult) async { + let item: TerminalItem = switch res { + case let .result(r): .res(.now, r) + case let .error(e): .err(.now, e) + case let .invalid(type, json): .bad(.now, type, json) + } + await add(item) } } @@ -43,30 +52,246 @@ private func addTermItem(_ items: inout [TerminalItem], _ item: TerminalItem) { items.append(item) } +class ItemsModel: ObservableObject { + static let shared = ItemsModel() + private let publisher = ObservableObjectPublisher() + private var bag = Set() + var reversedChatItems: [ChatItem] = [] { + willSet { publisher.send() } + } + var itemAdded = false { + willSet { publisher.send() } + } + + let chatState = ActiveChatState() + + // Publishes directly to `objectWillChange` publisher, + // this will cause reversedChatItems to be rendered without throttling + @Published var isLoading = false + @Published var showLoadingProgress: ChatId? = nil + + private var navigationTimeoutTask: Task? = nil + private var loadChatTask: Task? = nil + + var lastItemsLoaded: Bool { + chatState.splits.isEmpty || chatState.splits.first != reversedChatItems.first?.id + } + + init() { + publisher + .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true) + .sink { self.objectWillChange.send() } + .store(in: &bag) + } + + func loadOpenChat(_ chatId: ChatId, willNavigate: @escaping () -> Void = {}) { + navigationTimeoutTask?.cancel() + loadChatTask?.cancel() + navigationTimeoutTask = Task { + do { + try await Task.sleep(nanoseconds: 250_000000) + await MainActor.run { + ChatModel.shared.chatId = chatId + willNavigate() + } + } catch {} + } + loadChatTask = Task { + await MainActor.run { self.isLoading = true } +// try? await Task.sleep(nanoseconds: 1000_000000) + await loadChat(chatId: chatId) + if !Task.isCancelled { + await MainActor.run { + self.isLoading = false + self.showLoadingProgress = nil + } + } + } + } + + func loadOpenChatNoWait(_ chatId: ChatId, _ openAroundItemId: ChatItem.ID? = nil) { + navigationTimeoutTask?.cancel() + loadChatTask?.cancel() + loadChatTask = Task { + // try? await Task.sleep(nanoseconds: 1000_000000) + await loadChat(chatId: chatId, openAroundItemId: openAroundItemId, clearItems: openAroundItemId == nil) + if !Task.isCancelled { + await MainActor.run { + if openAroundItemId == nil { + ChatModel.shared.chatId = chatId + } + } + } + } + } +} + +class ChatTagsModel: ObservableObject { + static let shared = ChatTagsModel() + + @Published var userTags: [ChatTag] = [] + @Published var activeFilter: ActiveFilter? = nil + @Published var presetTags: [PresetTag:Int] = [:] + @Published var unreadTags: [Int64:Int] = [:] + + func updateChatTags(_ chats: [Chat]) { + let tm = ChatTagsModel.shared + var newPresetTags: [PresetTag:Int] = [:] + var newUnreadTags: [Int64:Int] = [:] + for chat in chats { + for tag in PresetTag.allCases { + if presetTagMatchesChat(tag, chat.chatInfo, chat.chatStats) { + newPresetTags[tag] = (newPresetTags[tag] ?? 0) + 1 + } + } + if chat.unreadTag, let tags = chat.chatInfo.chatTags { + for tag in tags { + newUnreadTags[tag] = (newUnreadTags[tag] ?? 0) + 1 + } + } + } + presetTags = newPresetTags + unreadTags = newUnreadTags + clearActiveChatFilterIfNeeded() + } + + func updateChatFavorite(favorite: Bool, wasFavorite: Bool) { + let count = presetTags[.favorites] + if favorite && !wasFavorite { + presetTags[.favorites] = (count ?? 0) + 1 + } else if !favorite && wasFavorite, let count { + presetTags[.favorites] = max(0, count - 1) + clearActiveChatFilterIfNeeded() + } + } + + func addPresetChatTags(_ chatInfo: ChatInfo, _ chatStats: ChatStats) { + for tag in PresetTag.allCases { + if presetTagMatchesChat(tag, chatInfo, chatStats) { + presetTags[tag] = (presetTags[tag] ?? 0) + 1 + } + } + } + + func removePresetChatTags(_ chatInfo: ChatInfo, _ chatStats: ChatStats) { + for tag in PresetTag.allCases { + if presetTagMatchesChat(tag, chatInfo, chatStats) { + if let count = presetTags[tag] { + if count > 1 { + presetTags[tag] = count - 1 + } else { + presetTags.removeValue(forKey: tag) + } + } + } + } + clearActiveChatFilterIfNeeded() + } + + func markChatTagRead(_ chat: Chat) -> Void { + if chat.unreadTag, let tags = chat.chatInfo.chatTags { + decTagsReadCount(tags) + } + } + + func updateChatTagRead(_ chat: Chat, wasUnread: Bool) -> Void { + guard let tags = chat.chatInfo.chatTags else { return } + let nowUnread = chat.unreadTag + if nowUnread && !wasUnread { + for tag in tags { + unreadTags[tag] = (unreadTags[tag] ?? 0) + 1 + } + } else if !nowUnread && wasUnread { + decTagsReadCount(tags) + } + } + + func decTagsReadCount(_ tags: [Int64]) -> Void { + for tag in tags { + if let count = unreadTags[tag] { + unreadTags[tag] = max(0, count - 1) + } + } + } + + func changeGroupReportsTag(_ by: Int = 0) { + if by == 0 { return } + presetTags[.groupReports] = max(0, (presetTags[.groupReports] ?? 0) + by) + clearActiveChatFilterIfNeeded() + } + + func clearActiveChatFilterIfNeeded() { + let clear = switch activeFilter { + case let .presetTag(tag): (presetTags[tag] ?? 0) == 0 + case let .userTag(tag): !userTags.contains(tag) + case .unread, nil: false + } + if clear { activeFilter = nil } + } +} + +class NetworkModel: ObservableObject { + // map of connections network statuses, key is agent connection id + @Published var networkStatuses: Dictionary = [:] + + static let shared = NetworkModel() + + private init() { } + + func setContactNetworkStatus(_ contact: Contact, _ status: NetworkStatus) { + if let conn = contact.activeConn { + networkStatuses[conn.agentConnId] = status + } + } + + func contactNetworkStatus(_ contact: Contact) -> NetworkStatus { + if let conn = contact.activeConn { + networkStatuses[conn.agentConnId] ?? .unknown + } else { + .unknown + } + } +} + +/// ChatItemWithMenu can depend on previous or next item for it's appearance +/// This dummy model is used to force an update of all chat items, +/// when they might have changed appearance. +class ChatItemDummyModel: ObservableObject { + static let shared = ChatItemDummyModel() + func sendUpdate() { objectWillChange.send() } +} + final class ChatModel: ObservableObject { @Published var onboardingStage: OnboardingStage? @Published var setDeliveryReceipts = false @Published var v3DBMigration: V3DBMigrationState = v3DBMigrationDefault.get() - @Published var currentUser: User? + @Published var currentUser: User? { + didSet { + ThemeManager.applyTheme(currentThemeDefault.get()) + } + } @Published var users: [UserInfo] = [] @Published var chatInitialized = false @Published var chatRunning: Bool? @Published var chatDbChanged = false @Published var chatDbEncrypted: Bool? @Published var chatDbStatus: DBMigrationResult? + @Published var ctrlInitInProgress: Bool = false + @Published var notificationResponse: UNNotificationResponse? // local authentication @Published var contentViewAccessAuthenticated: Bool = false @Published var laRequest: LocalAuthRequest? // list of chat "previews" - @Published var chats: [Chat] = [] - // map of connections network statuses, key is agent connection id - @Published var networkStatuses: Dictionary = [:] + @Published private(set) var chats: [Chat] = [] + @Published var deletedChats: Set = [] // current chat @Published var chatId: String? - @Published var reversedChatItems: [ChatItem] = [] + @Published var openAroundItemId: ChatItem.ID? = nil var chatItemStatuses: Dictionary = [:] @Published var chatToTop: String? @Published var groupMembers: [GMember] = [] + @Published var groupMembersIndexes: Dictionary = [:] // groupMemberId to index in groupMembers list + @Published var membersLoaded = false // items in the terminal view @Published var showingTerminal = false @Published var terminalItems: [TerminalItem] = [] @@ -76,8 +301,10 @@ final class ChatModel: ObservableObject { @Published var deviceToken: DeviceToken? @Published var savedToken: DeviceToken? @Published var tokenRegistered = false + @Published var reRegisterTknStatus: NtfTknStatus? = nil @Published var tokenStatus: NtfTknStatus? @Published var notificationMode = NotificationsMode.off + @Published var notificationServer: String? @Published var notificationPreview: NotificationPreviewMode = ntfPreviewModeGroupDefault.get() // pending notification actions @Published var ntfContactRequest: NTFContactRequest? @@ -87,16 +314,19 @@ final class ChatModel: ObservableObject { @Published var activeCall: Call? let callCommand: WebRTCCommandProcessor = WebRTCCommandProcessor() @Published var showCallView = false + @Published var activeCallViewIsCollapsed = false // remote desktop @Published var remoteCtrlSession: RemoteCtrlSession? - // currently showing QR code - @Published var connReqInv: String? + // currently showing invitation + @Published var showingInvitation: ShowingInvitation? + @Published var migrationState: MigrationToState? = MigrationToDeviceState.makeMigrationState() // audio recording and playback @Published var stopPreviousRecPlay: URL? = nil // coordinates currently playing source @Published var draft: ComposeState? @Published var draftChatId: String? - // tracks keyboard height via subscription in AppDelegate - @Published var keyboardHeight: CGFloat = 0 + @Published var networkInfo = UserNetworkInfo(networkType: .other, online: true) + // usage conditions + @Published var conditions: ServerOperatorConditions = .empty var messageDelivery: Dictionary Void> = [:] @@ -104,6 +334,8 @@ final class ChatModel: ObservableObject { static let shared = ChatModel() + let im = ItemsModel.shared + static var ok: Bool { ChatModel.shared.chatDbStatus == .ok } let ntfEnableLocal = true @@ -136,7 +368,7 @@ final class ChatModel: ObservableObject { } func removeUser(_ user: User) { - if let i = getUserIndex(user), users[i].user.userId != currentUser?.userId { + if let i = getUserIndex(user) { users.remove(at: i) } } @@ -169,18 +401,47 @@ final class ChatModel: ObservableObject { } } + func populateGroupMembersIndexes() { + groupMembersIndexes.removeAll() + for (i, member) in groupMembers.enumerated() { + groupMembersIndexes[member.groupMemberId] = i + } + } + func getGroupMember(_ groupMemberId: Int64) -> GMember? { - groupMembers.first { $0.groupMemberId == groupMemberId } + if let i = groupMembersIndexes[groupMemberId] { + return groupMembers[i] + } + return nil + } + + func loadGroupMembers(_ groupInfo: GroupInfo, updateView: @escaping () -> Void = {}) async { + let groupMembers = await apiListMembers(groupInfo.groupId) + await MainActor.run { + if chatId == groupInfo.id { + self.groupMembers = groupMembers.map { GMember.init($0) } + self.populateGroupMembersIndexes() + self.membersLoaded = true + updateView() + } + } } private func getChatIndex(_ id: String) -> Int? { chats.firstIndex(where: { $0.id == id }) } - func addChat(_ chat: Chat, at position: Int = 0) { - withAnimation { - chats.insert(chat, at: position) + func addChat(_ chat: Chat) { + if chatId == nil { + withAnimation { addChat_(chat, at: 0) } + } else { + addChat_(chat, at: 0) } + popChatCollector.throttlePopChat(chat.chatInfo.id, currentPosition: 0) + } + + func addChat_(_ chat: Chat, at position: Int = 0) { + chats.insert(chat, at: position) } func updateChatInfo(_ cInfo: ChatInfo) { @@ -215,6 +476,7 @@ final class ChatModel: ObservableObject { updateChatInfo(cInfo) } else if addMissing { addChat(Chat(chatInfo: cInfo, chatItems: [])) + ChatTagsModel.shared.addPresetChatTags(cInfo, ChatStats()) } } @@ -238,26 +500,10 @@ final class ChatModel: ObservableObject { } } - func updateChats(with newChats: [ChatData]) { - for i in 0.. 0 { - if chatId == nil { - withAnimation { popChat_(i) } - } else if chatId == cInfo.id { - chatToTop = cInfo.id - } else { - popChat_(i) - } + unreadCollector.changeUnreadCounter(cInfo.id, by: 1, unreadMentions: cItem.meta.userMention ? 1 : 0) } + popChatCollector.throttlePopChat(cInfo.id, currentPosition: i) } else { addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) } @@ -308,7 +551,7 @@ final class ChatModel: ObservableObject { var res: Bool if let chat = getChat(cInfo.id) { if let pItem = chat.chatItems.last { - if pItem.id == cItem.id || (chatId == cInfo.id && reversedChatItems.first(where: { $0.id == cItem.id }) == nil) { + if pItem.id == cItem.id || (chatId == cInfo.id && im.reversedChatItems.first(where: { $0.id == cItem.id }) == nil) { chat.chatItems = [cItem] } } else { @@ -319,24 +562,27 @@ final class ChatModel: ObservableObject { addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) res = true } + if cItem.isDeletedContent || cItem.meta.itemDeleted != nil { + VoiceItemState.stopVoiceInChatView(cInfo, cItem) + } // update current chat return chatId == cInfo.id ? _upsertChatItem(cInfo, cItem) : res } private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool { if let i = getChatItemIndex(cItem) { - withAnimation { - _updateChatItem(at: i, with: cItem) - } + _updateChatItem(at: i, with: cItem) + ChatItemDummyModel.shared.sendUpdate() return false } else { - withAnimation(itemAnimation()) { - var ci = cItem - if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus { - ci.meta.itemStatus = status - } - reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0) + var ci = cItem + if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus { + ci.meta.itemStatus = status } + im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0) + im.chatState.itemAdded((ci.id, ci.isRcvNew), hasLiveDummy ? 1 : 0) + im.itemAdded = true + ChatItemDummyModel.shared.sendUpdate() return true } @@ -350,7 +596,7 @@ final class ChatModel: ObservableObject { func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem, status: CIStatus? = nil) { if chatId == cInfo.id, let i = getChatItemIndex(cItem) { - withAnimation { + withConditionalAnimation { _updateChatItem(at: i, with: cItem) } } else if let status = status { @@ -359,17 +605,17 @@ final class ChatModel: ObservableObject { } private func _updateChatItem(at i: Int, with cItem: ChatItem) { - reversedChatItems[i] = cItem - reversedChatItems[i].viewTimestamp = .now + im.reversedChatItems[i] = cItem + im.reversedChatItems[i].viewTimestamp = .now } func getChatItemIndex(_ cItem: ChatItem) -> Int? { - reversedChatItems.firstIndex(where: { $0.id == cItem.id }) + im.reversedChatItems.firstIndex(where: { $0.id == cItem.id }) } func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) { if cItem.isRcvNew { - decreaseUnreadCounter(cInfo) + unreadCollector.changeUnreadCounter(cInfo.id, by: -1, unreadMentions: cItem.meta.userMention ? -1 : 0) } // update previews if let chat = getChat(cInfo.id) { @@ -380,24 +626,65 @@ final class ChatModel: ObservableObject { // remove from current chat if chatId == cInfo.id { if let i = getChatItemIndex(cItem) { - _ = withAnimation { - self.reversedChatItems.remove(at: i) + withAnimation { + let item = im.reversedChatItems.remove(at: i) + im.chatState.itemsRemoved([(item.id, i, item.isRcvNew)], im.reversedChatItems.reversed()) } } } + VoiceItemState.stopVoiceInChatView(cInfo, cItem) + } + + func removeMemberItems(_ removedMember: GroupMember, byMember: GroupMember, _ groupInfo: GroupInfo) { + // this should not happen, only another member can "remove" user, user can only "leave" (another event). + if byMember.groupMemberId == groupInfo.membership.groupMemberId { + logger.debug("exiting removeMemberItems") + return + } + if chatId == groupInfo.id { + for i in 0.. 0, + let updatedItem = removedUpdatedItem(chat.chatItems[0]) { + chat.chatItems = [updatedItem] + } + + func removedUpdatedItem(_ item: ChatItem) -> ChatItem? { + let newContent: CIContent + if case .groupSnd = item.chatDir, removedMember.groupMemberId == groupInfo.membership.groupMemberId { + newContent = .sndModerated + } else if case let .groupRcv(groupMember) = item.chatDir, groupMember.groupMemberId == removedMember.groupMemberId { + newContent = .rcvModerated + } else { + return nil + } + var updatedItem = item + updatedItem.meta.itemDeleted = .moderated(deletedTs: Date.now, byGroupMember: byMember) + if groupInfo.fullGroupPreferences.fullDelete.on { + updatedItem.content = newContent + } + if item.isActiveReport { + decreaseGroupReportsCounter(groupInfo.id) + } + return updatedItem + } } func nextChatItemData(_ chatItemId: Int64, previous: Bool, map: @escaping (ChatItem) -> T?) -> T? { - guard var i = reversedChatItems.firstIndex(where: { $0.id == chatItemId }) else { return nil } + guard var i = im.reversedChatItems.firstIndex(where: { $0.id == chatItemId }) else { return nil } if previous { - while i < reversedChatItems.count - 1 { + while i < im.reversedChatItems.count - 1 { i += 1 - if let res = map(reversedChatItems[i]) { return res } + if let res = map(im.reversedChatItems[i]) { return res } } } else { while i > 0 { i -= 1 - if let res = map(reversedChatItems[i]) { return res } + if let res = map(im.reversedChatItems[i]) { return res } } } return nil @@ -415,10 +702,22 @@ final class ChatModel: ObservableObject { } } + func updateCurrentUserUiThemes(uiThemes: ThemeModeOverrides?) { + guard var current = currentUser, current.uiThemes != uiThemes else { return } + current.uiThemes = uiThemes + let i = users.firstIndex(where: { $0.user.userId == current.userId }) + if let i { + users[i].user = current + } + currentUser = current + } + func addLiveDummy(_ chatInfo: ChatInfo) -> ChatItem { let cItem = ChatItem.liveDummy(chatInfo.chatType) withAnimation { - reversedChatItems.insert(cItem, at: 0) + im.reversedChatItems.insert(cItem, at: 0) + im.chatState.itemAdded((cItem.id, cItem.isRcvNew), 0) + im.itemAdded = true } return cItem } @@ -426,130 +725,247 @@ final class ChatModel: ObservableObject { func removeLiveDummy(animated: Bool = true) { if hasLiveDummy { if animated { - withAnimation { _ = reversedChatItems.removeFirst() } + withAnimation { _ = im.reversedChatItems.removeFirst() } } else { - _ = reversedChatItems.removeFirst() + _ = im.reversedChatItems.removeFirst() } } } private var hasLiveDummy: Bool { - reversedChatItems.first?.isLiveDummy == true + im.reversedChatItems.first?.isLiveDummy == true } - func markChatItemsRead(_ cInfo: ChatInfo) { + func markAllChatItemsRead(_ cInfo: ChatInfo) { // update preview _updateChat(cInfo.id) { chat in - self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount) + self.decreaseUnreadCounter(user: self.currentUser!, chat: chat) + ChatTagsModel.shared.markChatTagRead(chat) chat.chatStats = ChatStats() } // update current chat if chatId == cInfo.id { - markCurrentChatRead() - } - } - - private func markCurrentChatRead(fromIndex i: Int = 0) { - var j = i - while j < reversedChatItems.count { - markChatItemRead_(j) - j += 1 - } - } - - func markChatItemsRead(_ cInfo: ChatInfo, aboveItem: ChatItem? = nil) { - if let cItem = aboveItem { - if chatId == cInfo.id, let i = getChatItemIndex(cItem) { - markCurrentChatRead(fromIndex: i) - _updateChat(cInfo.id) { chat in - var unreadBelow = 0 - var j = i - 1 - while j >= 0 { - if case .rcvNew = self.reversedChatItems[j].meta.itemStatus { - unreadBelow += 1 - } - j -= 1 - } - // update preview - let markedCount = chat.chatStats.unreadCount - unreadBelow - if markedCount > 0 { - chat.chatStats.unreadCount -= markedCount - self.decreaseUnreadCounter(user: self.currentUser!, by: markedCount) - } - } + var i = 0 + while i < im.reversedChatItems.count { + markChatItemRead_(i) + i += 1 } - } else { - markChatItemsRead(cInfo) + im.chatState.itemsRead(nil, im.reversedChatItems.reversed()) } } - func markChatUnread(_ cInfo: ChatInfo, unreadChat: Bool = true) { _updateChat(cInfo.id) { chat in + let wasUnread = chat.unreadTag chat.chatStats.unreadChat = unreadChat + ChatTagsModel.shared.updateChatTagRead(chat, wasUnread: wasUnread) } } func clearChat(_ cInfo: ChatInfo) { // clear preview if let chat = getChat(cInfo.id) { - self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount) + self.decreaseUnreadCounter(user: self.currentUser!, chat: chat) chat.chatItems = [] + ChatTagsModel.shared.markChatTagRead(chat) chat.chatStats = ChatStats() chat.chatInfo = cInfo } // clear current chat if chatId == cInfo.id { chatItemStatuses = [:] - reversedChatItems = [] + im.reversedChatItems = [] + im.chatState.clear() } } - func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) { - // update preview - decreaseUnreadCounter(cInfo) - // update current chat - if chatId == cInfo.id, let i = getChatItemIndex(cItem) { - markChatItemRead_(i) + func markChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID], _ mentionsRead: Int) { + if self.chatId == cInfo.id { + var unreadItemIds: Set = [] + var i = 0 + var ids = Set(itemIds) + while i < im.reversedChatItems.count && !ids.isEmpty { + let item = im.reversedChatItems[i] + if ids.contains(item.id) && item.isRcvNew { + markChatItemRead_(i) + unreadItemIds.insert(item.id) + ids.remove(item.id) + } + i += 1 + } + im.chatState.itemsRead(unreadItemIds, im.reversedChatItems.reversed()) + } + self.unreadCollector.changeUnreadCounter(cInfo.id, by: -itemIds.count, unreadMentions: -mentionsRead) + } + + private let unreadCollector = UnreadCollector() + + class UnreadCollector { + private let subject = PassthroughSubject() + private var bag = Set() + private var unreadCounts: [ChatId: (unread: Int, mentions: Int)] = [:] + + init() { + subject + .debounce(for: 1, scheduler: DispatchQueue.main) + .sink { + let m = ChatModel.shared + for (chatId, (unread, mentions)) in self.unreadCounts { + if unread != 0 || mentions != 0, let i = m.getChatIndex(chatId) { + m.changeUnreadCounter(i, by: unread, unreadMentions: mentions) + } + } + self.unreadCounts = [:] + } + .store(in: &bag) + } + + func changeUnreadCounter(_ chatId: ChatId, by count: Int, unreadMentions: Int) { + let (unread, mentions) = self.unreadCounts[chatId] ?? (0, 0) + self.unreadCounts[chatId] = (unread + count, mentions + unreadMentions) + subject.send() + } + } + + let popChatCollector = PopChatCollector() + + class PopChatCollector { + private let subject = PassthroughSubject() + private var bag = Set() + private var chatsToPop: [ChatId: Date] = [:] + private let popTsComparator = KeyPathComparator(\.popTs, order: .reverse) + + init() { + subject + .throttle(for: 2, scheduler: DispatchQueue.main, latest: true) + .sink { self.popCollectedChats() } + .store(in: &bag) + } + + func throttlePopChat(_ chatId: ChatId, currentPosition: Int) { + let m = ChatModel.shared + if currentPosition > 0 && m.chatId == chatId { + m.chatToTop = chatId + } + if currentPosition > 0 || !chatsToPop.isEmpty { + chatsToPop[chatId] = Date.now + subject.send() + } + } + + func clear() { + chatsToPop = [:] + } + + func popCollectedChats() { + let m = ChatModel.shared + var ixs: IndexSet = [] + var chs: [Chat] = [] + // collect chats that received updates + for (chatId, popTs) in self.chatsToPop { + // Currently opened chat is excluded, removing it from the list would navigate out of it + // It will be popped to top later when user exits from the list. + if m.chatId != chatId, let i = m.getChatIndex(chatId) { + ixs.insert(i) + let ch = m.chats[i] + ch.popTs = popTs + chs.append(ch) + } + } + + let removeInsert = { + m.chats.remove(atOffsets: ixs) + // sort chats by pop timestamp in descending order + m.chats.insert(contentsOf: chs.sorted(using: self.popTsComparator), at: 0) + } + + if m.chatId == nil { + withAnimation { removeInsert() } + } else { + removeInsert() + } + + self.chatsToPop = [:] } } private func markChatItemRead_(_ i: Int) { - let meta = reversedChatItems[i].meta + let meta = im.reversedChatItems[i].meta if case .rcvNew = meta.itemStatus { - reversedChatItems[i].meta.itemStatus = .rcvRead - reversedChatItems[i].viewTimestamp = .now + im.reversedChatItems[i].meta.itemStatus = .rcvRead + im.reversedChatItems[i].viewTimestamp = .now if meta.itemLive != true, let ttl = meta.itemTimed?.ttl { - reversedChatItems[i].meta.itemTimed?.deleteAt = .now + TimeInterval(ttl) + im.reversedChatItems[i].meta.itemTimed?.deleteAt = .now + TimeInterval(ttl) } } } - func decreaseUnreadCounter(_ cInfo: ChatInfo) { - if let i = getChatIndex(cInfo.id) { - chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount - 1 - decreaseUnreadCounter(user: currentUser!) - } + func changeUnreadCounter(_ chatIndex: Int, by count: Int, unreadMentions: Int) { + let wasUnread = chats[chatIndex].unreadTag + let stats = chats[chatIndex].chatStats + chats[chatIndex].chatStats.unreadCount = stats.unreadCount + count + chats[chatIndex].chatStats.unreadMentions = stats.unreadMentions + unreadMentions + ChatTagsModel.shared.updateChatTagRead(chats[chatIndex], wasUnread: wasUnread) + changeUnreadCounter(user: currentUser!, by: count) } func increaseUnreadCounter(user: any UserLike) { changeUnreadCounter(user: user, by: 1) - NtfManager.shared.incNtfBadgeCount() + } + + func decreaseUnreadCounter(user: any UserLike, chat: Chat) { + let by = chat.chatInfo.chatSettings?.enableNtfs == .mentions + ? chat.chatStats.unreadMentions + : chat.chatStats.unreadCount + decreaseUnreadCounter(user: user, by: by) } func decreaseUnreadCounter(user: any UserLike, by: Int = 1) { changeUnreadCounter(user: user, by: -by) - NtfManager.shared.decNtfBadgeCount(by: by) } private func changeUnreadCounter(user: any UserLike, by: Int) { if let i = users.firstIndex(where: { $0.user.userId == user.userId }) { users[i].unreadCount += by } + NtfManager.shared.changeNtfBadgeCount(by: by) } func totalUnreadCountForAllUsers() -> Int { - chats.filter { $0.chatInfo.ntfsEnabled }.reduce(0, { count, chat in count + chat.chatStats.unreadCount }) + - users.filter { !$0.user.activeUser }.reduce(0, { unread, next -> Int in unread + next.unreadCount }) + var unread: Int = 0 + for chat in chats { + switch chat.chatInfo.chatSettings?.enableNtfs { + case .all: unread += chat.chatStats.unreadCount + case .mentions: unread += chat.chatStats.unreadMentions + default: () + } + } + for u in users { + if !u.user.activeUser { + unread += u.unreadCount + } + } + return unread + } + + func increaseGroupReportsCounter(_ chatId: ChatId) { + changeGroupReportsCounter(chatId, 1) + } + + func decreaseGroupReportsCounter(_ chatId: ChatId, by: Int = 1) { + changeGroupReportsCounter(chatId, -by) + } + + private func changeGroupReportsCounter(_ chatId: ChatId, _ by: Int = 0) { + if by == 0 { return } + + if let i = getChatIndex(chatId) { + let chat = chats[i] + let wasReportsCount = chat.chatStats.reportsCount + chat.chatStats.reportsCount = max(0, chat.chatStats.reportsCount + by) + let nowReportsCount = chat.chatStats.reportsCount + let by = wasReportsCount == 0 && nowReportsCount > 0 ? 1 : (wasReportsCount > 0 && nowReportsCount == 0) ? -1 : 0 + ChatTagsModel.shared.changeGroupReportsTag(by) + } } // this function analyses "connected" events and assumes that each member will be there only once @@ -558,8 +974,8 @@ final class ChatModel: ObservableObject { var ns: [String] = [] if let ciCategory = chatItem.mergeCategory, var i = getChatItemIndex(chatItem) { - while i < reversedChatItems.count { - let ci = reversedChatItems[i] + while i < im.reversedChatItems.count { + let ci = im.reversedChatItems[i] if ci.mergeCategory != ciCategory { break } if let m = ci.memberConnected { ns.append(m.displayName) @@ -574,7 +990,7 @@ final class ChatModel: ObservableObject { // returns the index of the passed item and the next item (it has smaller index) func getNextChatItem(_ ci: ChatItem) -> (Int?, ChatItem?) { if let i = getChatItemIndex(ci) { - (i, i > 0 ? reversedChatItems[i - 1] : nil) + (i, i > 0 ? im.reversedChatItems[i - 1] : nil) } else { (nil, nil) } @@ -584,10 +1000,10 @@ final class ChatModel: ObservableObject { // and the previous visible item with another merge category func getPrevShownChatItem(_ ciIndex: Int?, _ ciCategory: CIMergeCategory?) -> (Int?, ChatItem?) { guard var i = ciIndex else { return (nil, nil) } - let fst = reversedChatItems.count - 1 + let fst = im.reversedChatItems.count - 1 while i < fst { i = i + 1 - let ci = reversedChatItems[i] + let ci = im.reversedChatItems[i] if ciCategory == nil || ciCategory != ci.mergeCategory { return (i - 1, ci) } @@ -597,12 +1013,17 @@ final class ChatModel: ObservableObject { // returns the previous member in the same merge group and the count of members in this group func getPrevHiddenMember(_ member: GroupMember, _ range: ClosedRange) -> (GroupMember?, Int) { + let items = im.reversedChatItems var prevMember: GroupMember? = nil var memberIds: Set = [] for i in range { - if case let .groupRcv(m) = reversedChatItems[i].chatDir { - if prevMember == nil && m.groupMemberId != member.groupMemberId { prevMember = m } - memberIds.insert(m.groupMemberId) + if i < items.count { + if case let .groupRcv(m) = items[i].chatDir { + if prevMember == nil && m.groupMemberId != member.groupMemberId { prevMember = m } + memberIds.insert(m.groupMemberId) + } + } else { + logger.error("getPrevHiddenMember: index >= count of reversed items: \(i) vs \(items.count), range: \(String(describing: range))") } } return (prevMember, memberIds.count) @@ -610,6 +1031,7 @@ final class ChatModel: ObservableObject { func popChat(_ id: String) { if let i = getChatIndex(id) { + // no animation here, for it not to look like it just moved when leaving the chat popChat_(i) } } @@ -620,17 +1042,23 @@ final class ChatModel: ObservableObject { } func dismissConnReqView(_ id: String) { - if let connReqInv = connReqInv, - let c = getChat(id), - case let .contactConnection(contactConnection) = c.chatInfo, - connReqInv == contactConnection.connReqInv { + if id == showingInvitation?.pcc.id { + markShowingInvitationUsed() dismissAllSheets() } } + func markShowingInvitationUsed() { + showingInvitation?.connChatUsed = true + } + func removeChat(_ id: String) { withAnimation { - chats.removeAll(where: { $0.id == id }) + if let i = getChatIndex(id) { + let removed = chats.remove(at: i) + ChatTagsModel.shared.removePresetChatTags(removed.chatInfo, removed.chatStats) + removeWallpaperFilesFromChat(removed) + } } } @@ -642,14 +1070,17 @@ final class ChatModel: ObservableObject { } // update current chat if chatId == groupInfo.id { - if let i = groupMembers.firstIndex(where: { $0.groupMemberId == member.groupMemberId }) { + if let i = groupMembersIndexes[member.groupMemberId] { withAnimation(.default) { self.groupMembers[i].wrapped = member self.groupMembers[i].created = Date.now } return false } else { - withAnimation { groupMembers.append(GMember(member)) } + withAnimation { + groupMembers.append(GMember(member)) + groupMembersIndexes[member.groupMemberId] = groupMembers.count - 1 + } return true } } else { @@ -666,42 +1097,27 @@ final class ChatModel: ObservableObject { } } - func unreadChatItemCounts(itemsInView: Set) -> UnreadChatItemCounts { - var i = 0 - var totalBelow = 0 - var unreadBelow = 0 - while i < reversedChatItems.count - 1 && !itemsInView.contains(reversedChatItems[i].viewId) { - totalBelow += 1 - if reversedChatItems[i].isRcvNew { - unreadBelow += 1 + func removeWallpaperFilesFromChat(_ chat: Chat) { + if case let .direct(contact) = chat.chatInfo { + removeWallpaperFilesFromTheme(contact.uiThemes) + } else if case let .group(groupInfo) = chat.chatInfo { + removeWallpaperFilesFromTheme(groupInfo.uiThemes) + } + } + + func removeWallpaperFilesFromAllChats(_ user: User) { + // Currently, only removing everything from currently active user is supported. Inactive users are TODO + if user.userId == currentUser?.userId { + chats.forEach { + removeWallpaperFilesFromChat($0) } - i += 1 - } - return UnreadChatItemCounts(totalBelow: totalBelow, unreadBelow: unreadBelow) - } - - func topItemInView(itemsInView: Set) -> ChatItem? { - let maxIx = reversedChatItems.count - 1 - var i = 0 - let inView = { itemsInView.contains(self.reversedChatItems[$0].viewId) } - while i < maxIx && !inView(i) { i += 1 } - while i < maxIx && inView(i) { i += 1 } - return reversedChatItems[min(i - 1, maxIx)] - } - - func setContactNetworkStatus(_ contact: Contact, _ status: NetworkStatus) { - if let conn = contact.activeConn { - networkStatuses[conn.agentConnId] = status } } +} - func contactNetworkStatus(_ contact: Contact) -> NetworkStatus { - if let conn = contact.activeConn { - networkStatuses[conn.agentConnId] ?? .unknown - } else { - .unknown - } - } +struct ShowingInvitation { + var pcc: PendingContactConnection + var connChatUsed: Bool } struct NTFContactRequest { @@ -709,16 +1125,12 @@ struct NTFContactRequest { var chatId: String } -struct UnreadChatItemCounts { - var totalBelow: Int - var unreadBelow: Int -} - -final class Chat: ObservableObject, Identifiable { +final class Chat: ObservableObject, Identifiable, ChatLike { @Published var chatInfo: ChatInfo @Published var chatItems: [ChatItem] @Published var chatStats: ChatStats var created = Date.now + fileprivate var popTs: Date? init(_ cData: ChatData) { self.chatInfo = cData.chatInfo @@ -740,25 +1152,14 @@ final class Chat: ObservableObject, Identifiable { ) } - var userCanSend: Bool { - switch chatInfo { - case .direct: return true - case let .group(groupInfo): - let m = groupInfo.membership - return m.memberActive && m.memberRole >= .member - default: return false + var unreadTag: Bool { + switch chatInfo.chatSettings?.enableNtfs { + case .all: chatStats.unreadChat || chatStats.unreadCount > 0 + case .mentions: chatStats.unreadChat || chatStats.unreadMentions > 0 + default: chatStats.unreadChat } } - - var userIsObserver: Bool { - switch chatInfo { - case let .group(groupInfo): - let m = groupInfo.membership - return m.memberActive && m.memberRole == .observer - default: return false - } - } - + var id: ChatId { get { chatInfo.id } } var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } } diff --git a/apps/ios/Shared/Model/NSESubscriber.swift b/apps/ios/Shared/Model/NSESubscriber.swift index f52e72beab..a4a5dc8152 100644 --- a/apps/ios/Shared/Model/NSESubscriber.swift +++ b/apps/ios/Shared/Model/NSESubscriber.swift @@ -16,13 +16,13 @@ private var nseSubscribers: [UUID:NSESubscriber] = [:] private let SUSPENDING_TIMEOUT: TimeInterval = 2 // timeout should be larger than SUSPENDING_TIMEOUT -func waitNSESuspended(timeout: TimeInterval, dispatchQueue: DispatchQueue = DispatchQueue.main, suspended: @escaping (Bool) -> Void) { +func waitNSESuspended(timeout: TimeInterval, suspended: @escaping (Bool) -> Void) { if timeout <= SUSPENDING_TIMEOUT { logger.warning("waitNSESuspended: small timeout \(timeout), using \(SUSPENDING_TIMEOUT + 1)") } var state = nseStateGroupDefault.get() if case .suspended = state { - dispatchQueue.async { suspended(true) } + DispatchQueue.main.async { suspended(true) } return } let id = UUID() @@ -45,7 +45,7 @@ func waitNSESuspended(timeout: TimeInterval, dispatchQueue: DispatchQueue = Disp logger.debug("waitNSESuspended notifySuspended: calling suspended(\(ok))") suspendedCalled = true nseSubscribers.removeValue(forKey: id) - dispatchQueue.async { suspended(ok) } + DispatchQueue.main.async { suspended(ok) } } } diff --git a/apps/ios/Shared/Model/NetworkObserver.swift b/apps/ios/Shared/Model/NetworkObserver.swift new file mode 100644 index 0000000000..84c35afa07 --- /dev/null +++ b/apps/ios/Shared/Model/NetworkObserver.swift @@ -0,0 +1,73 @@ +// +// NetworkObserver.swift +// SimpleX (iOS) +// +// Created by Avently on 05.04.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import Network +import SimpleXChat + +class NetworkObserver { + static let shared = NetworkObserver() + private let queue: DispatchQueue = DispatchQueue(label: "chat.simplex.app.NetworkObserver") + private var prevInfo: UserNetworkInfo? = nil + private var monitor: NWPathMonitor? + private let monitorLock: DispatchQueue = DispatchQueue(label: "chat.simplex.app.monitorLock") + + func restartMonitor() { + monitorLock.sync { + monitor?.cancel() + let mon = NWPathMonitor() + mon.pathUpdateHandler = { [weak self] path in + self?.networkPathChanged(path: path) + } + mon.start(queue: queue) + monitor = mon + } + } + + private func networkPathChanged(path: NWPath) { + let info = UserNetworkInfo( + networkType: networkTypeFromPath(path), + online: path.status == .satisfied + ) + if (prevInfo != info) { + prevInfo = info + setNetworkInfo(info) + } + } + + private func networkTypeFromPath(_ path: NWPath) -> UserNetworkType { + if path.usesInterfaceType(.wiredEthernet) { + .ethernet + } else if path.usesInterfaceType(.wifi) { + .wifi + } else if path.usesInterfaceType(.cellular) { + .cellular + } else if path.usesInterfaceType(.other) { + .other + } else { + .none + } + } + + private static var networkObserver: NetworkObserver? = nil + + private func setNetworkInfo(_ info: UserNetworkInfo) { + logger.debug("setNetworkInfo Network changed: \(String(describing: info))") + DispatchQueue.main.sync { + ChatModel.shared.networkInfo = info + } + if !hasChatCtrl() { return } + self.monitorLock.sync { + do { + try apiSetNetworkInfo(info) + } catch let err { + logger.error("setNetworkInfo error: \(responseError(err))") + } + } + } +} diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index f1fdcc018e..da55bd90d0 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -26,20 +26,37 @@ enum NtfCallAction { class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { static let shared = NtfManager() + public var navigatingToChat = false private var granted = false private var prevNtfTime: Dictionary = [:] + override init() { + super.init() + UNUserNotificationCenter.current().delegate = self + } + // Handle notification when app is in background func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler handler: () -> Void) { logger.debug("NtfManager.userNotificationCenter: didReceive") - let content = response.notification.request.content + if appStateGroupDefault.get() == .active { + processNotificationResponse(response) + } else { + logger.debug("NtfManager.userNotificationCenter: remember response in model") + ChatModel.shared.notificationResponse = response + } + handler() + } + + func processNotificationResponse(_ ntfResponse: UNNotificationResponse) { let chatModel = ChatModel.shared - let action = response.actionIdentifier - logger.debug("NtfManager.userNotificationCenter: didReceive: action \(action), categoryIdentifier \(content.categoryIdentifier)") + let content = ntfResponse.notification.request.content + let action = ntfResponse.actionIdentifier + logger.debug("NtfManager.processNotificationResponse: didReceive: action \(action), categoryIdentifier \(content.categoryIdentifier)") if let userId = content.userInfo["userId"] as? Int64, userId != chatModel.currentUser?.userId { + logger.debug("NtfManager.processNotificationResponse changeActiveUser") changeActiveUser(userId, viewPwd: nil) } if content.categoryIdentifier == ntfCategoryContactRequest && (action == ntfActionAcceptContact || action == ntfActionAcceptContactIncognito), @@ -57,9 +74,13 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { chatModel.ntfCallInvitationAction = (chatId, ntfAction) } } else { - chatModel.chatId = content.targetContentIdentifier + if let chatId = content.targetContentIdentifier { + self.navigatingToChat = true + ItemsModel.shared.loadOpenChat(chatId) { + self.navigatingToChat = false + } + } } - handler() } private func ntfCallAction(_ content: UNNotificationContent, _ action: String) -> (ChatId, NtfCallAction)? { @@ -74,7 +95,6 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { return nil } - // Handle notification when the app is in foreground func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, @@ -183,6 +203,12 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: NSLocalizedString("SimpleX encrypted message or connection event", comment: "notification") + ), + UNNotificationCategory( + identifier: ntfCategoryManyEvents, + actions: [], + intentIdentifiers: [], + hiddenPreviewsBodyPlaceholder: NSLocalizedString("New events", comment: "notification") ) ]) } @@ -208,29 +234,28 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { } } } - center.delegate = self } func notifyContactRequest(_ user: any UserLike, _ contactRequest: UserContactRequest) { logger.debug("NtfManager.notifyContactRequest") - addNotification(createContactRequestNtf(user, contactRequest)) + addNotification(createContactRequestNtf(user, contactRequest, 0)) } func notifyContactConnected(_ user: any UserLike, _ contact: Contact) { logger.debug("NtfManager.notifyContactConnected") - addNotification(createContactConnectedNtf(user, contact)) + addNotification(createContactConnectedNtf(user, contact, 0)) } func notifyMessageReceived(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) { logger.debug("NtfManager.notifyMessageReceived") - if cInfo.ntfsEnabled { - addNotification(createMessageReceivedNtf(user, cInfo, cItem)) + if cInfo.ntfsEnabled(chatItem: cItem) { + addNotification(createMessageReceivedNtf(user, cInfo, cItem, 0)) } } func notifyCallInvitation(_ invitation: RcvCallInvitation) { logger.debug("NtfManager.notifyCallInvitation") - addNotification(createCallInvitationNtf(invitation)) + addNotification(createCallInvitationNtf(invitation, 0)) } func setNtfBadgeCount(_ count: Int) { @@ -238,12 +263,8 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { ntfBadgeCountGroupDefault.set(count) } - func decNtfBadgeCount(by count: Int = 1) { - setNtfBadgeCount(max(0, UIApplication.shared.applicationIconBadgeNumber - count)) - } - - func incNtfBadgeCount(by count: Int = 1) { - setNtfBadgeCount(UIApplication.shared.applicationIconBadgeNumber + count) + func changeNtfBadgeCount(by count: Int = 1) { + setNtfBadgeCount(max(0, UIApplication.shared.applicationIconBadgeNumber + count)) } private func addNotification(_ content: UNMutableNotificationContent) { diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index eff3110962..d92411decd 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -11,44 +11,42 @@ import UIKit import Dispatch import BackgroundTasks import SwiftUI -import SimpleXChat +@preconcurrency import SimpleXChat private var chatController: chat_ctrl? -// currentChatVersion in core -public let CURRENT_CHAT_VERSION: Int = 2 - -// version range that supports establishing direct connection with a group member (xGrpDirectInvVRange in core) -public let CREATE_MEMBER_CONTACT_VRANGE = VersionRange(minVersion: 2, maxVersion: CURRENT_CHAT_VERSION) +private let networkStatusesLock = DispatchQueue(label: "chat.simplex.app.network-statuses.lock") enum TerminalItem: Identifiable { case cmd(Date, ChatCommand) - case resp(Date, ChatResponse) + case res(Date, ChatAPIResult) + case err(Date, ChatError) + case bad(Date, String, Data?) var id: Date { - get { - switch self { - case let .cmd(id, _): return id - case let .resp(id, _): return id - } + switch self { + case let .cmd(d, _): d + case let .res(d, _): d + case let .err(d, _): d + case let .bad(d, _, _): d } } var label: String { - get { - switch self { - case let .cmd(_, cmd): return "> \(cmd.cmdString.prefix(30))" - case let .resp(_, resp): return "< \(resp.responseType)" - } + switch self { + case let .cmd(_, cmd): "> \(cmd.cmdString.prefix(30))" + case let .res(_, res): "< \(res.responseType)" + case let .err(_, err): "< error \(err.errorType)" + case let .bad(_, type, _): "< * \(type)" } } var details: String { - get { - switch self { - case let .cmd(_, cmd): return cmd.cmdString - case let .resp(_, resp): return resp.details - } + switch self { + case let .cmd(_, cmd): cmd.cmdString + case let .res(_, res): res.details + case let .err(_, err): String(describing: err) + case let .bad(_, _, json): dataToString(json) } } } @@ -90,51 +88,77 @@ private func withBGTask(bgDelay: Double? = nil, f: @escaping () -> T) -> T { return r } -func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil) -> ChatResponse { - logger.debug("chatSendCmd \(cmd.cmdType)") - let start = Date.now - let resp = bgTask - ? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd) } - : sendSimpleXCmd(cmd) - logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)") - if case let .response(_, json) = resp { - logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)") +@inline(__always) +func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, log: Bool = true) throws -> R { + let res: APIResult = chatApiSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl: ctrl, log: log) + return try apiResult(res) +} + +func chatApiSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, log: Bool = true) -> APIResult { + if log { + logger.debug("chatSendCmd \(cmd.cmdType)") } - Task { - await TerminalItems.shared.addCommand(start, cmd.obfuscated, resp) + let start = Date.now + let resp: APIResult = bgTask + ? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd, ctrl) } + : sendSimpleXCmd(cmd, ctrl) + if log { + logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)") + if case let .invalid(_, json) = resp { + logger.debug("chatSendCmd \(cmd.cmdType) response: \(dataToString(json))") + } + Task { + await TerminalItems.shared.addCommand(start, cmd.obfuscated, resp) + } } return resp } -func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil) async -> ChatResponse { +@inline(__always) +func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, log: Bool = true) async throws -> R { + let res: APIResult = await chatApiSendCmd(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl: ctrl, log: log) + return try apiResult(res) +} + +@inline(__always) +func chatApiSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, log: Bool = true) async -> APIResult { await withCheckedContinuation { cont in - cont.resume(returning: chatSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay)) + cont.resume(returning: chatApiSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl: ctrl, log: log)) } } -func chatRecvMsg() async -> ChatResponse? { +@inline(__always) +func apiResult(_ res: APIResult) throws -> R { + switch res { + case let .result(r): return r + case let .error(e): throw e + case let .invalid(type, _): throw ChatError.unexpectedResult(type: type) + } +} + +func chatRecvMsg(_ ctrl: chat_ctrl? = nil) async -> APIResult? { await withCheckedContinuation { cont in - _ = withBGTask(bgDelay: msgDelay) { () -> ChatResponse? in - let resp = recvSimpleXMsg() - cont.resume(returning: resp) - return resp + _ = withBGTask(bgDelay: msgDelay) { () -> APIResult? in + let evt: APIResult? = recvSimpleXMsg(ctrl) + cont.resume(returning: evt) + return evt } } } -func apiGetActiveUser() throws -> User? { - let r = chatSendCmdSync(.showActiveUser) +func apiGetActiveUser(ctrl: chat_ctrl? = nil) throws -> User? { + let r: APIResult = chatApiSendCmdSync(.showActiveUser, ctrl: ctrl) switch r { - case let .activeUser(user): return user - case .chatCmdError(_, .error(.noActiveUser)): return nil - default: throw r + case let .result(.activeUser(user)): return user + case .error(.error(.noActiveUser)): return nil + default: throw r.unexpected } } -func apiCreateActiveUser(_ p: Profile?, sameServers: Bool = false, pastTimestamp: Bool = false) throws -> User { - let r = chatSendCmdSync(.createActiveUser(profile: p, sameServers: sameServers, pastTimestamp: pastTimestamp)) +func apiCreateActiveUser(_ p: Profile?, pastTimestamp: Bool = false, ctrl: chat_ctrl? = nil) throws -> User { + let r: ChatResponse0 = try chatSendCmdSync(.createActiveUser(profile: p, pastTimestamp: pastTimestamp), ctrl: ctrl) if case let .activeUser(user) = r { return user } - throw r + throw r.unexpected } func listUsers() throws -> [UserInfo] { @@ -145,41 +169,35 @@ func listUsersAsync() async throws -> [UserInfo] { return try listUsersResponse(await chatSendCmd(.listUsers)) } -private func listUsersResponse(_ r: ChatResponse) throws -> [UserInfo] { +private func listUsersResponse(_ r: ChatResponse0) throws -> [UserInfo] { if case let .usersList(users) = r { return users.sorted { $0.user.chatViewName.compare($1.user.chatViewName) == .orderedAscending } } - throw r + throw r.unexpected } func apiSetActiveUser(_ userId: Int64, viewPwd: String?) throws -> User { - let r = chatSendCmdSync(.apiSetActiveUser(userId: userId, viewPwd: viewPwd)) + let r: ChatResponse0 = try chatSendCmdSync(.apiSetActiveUser(userId: userId, viewPwd: viewPwd)) if case let .activeUser(user) = r { return user } - throw r + throw r.unexpected } func apiSetActiveUserAsync(_ userId: Int64, viewPwd: String?) async throws -> User { - let r = await chatSendCmd(.apiSetActiveUser(userId: userId, viewPwd: viewPwd)) + let r: ChatResponse0 = try await chatSendCmd(.apiSetActiveUser(userId: userId, viewPwd: viewPwd)) if case let .activeUser(user) = r { return user } - throw r + throw r.unexpected } func apiSetAllContactReceipts(enable: Bool) async throws { - let r = await chatSendCmd(.setAllContactReceipts(enable: enable)) - if case .cmdOk = r { return } - throw r + try await sendCommandOkResp(.setAllContactReceipts(enable: enable)) } func apiSetUserContactReceipts(_ userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) async throws { - let r = await chatSendCmd(.apiSetUserContactReceipts(userId: userId, userMsgReceiptSettings: userMsgReceiptSettings)) - if case .cmdOk = r { return } - throw r + try await sendCommandOkResp(.apiSetUserContactReceipts(userId: userId, userMsgReceiptSettings: userMsgReceiptSettings)) } func apiSetUserGroupReceipts(_ userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) async throws { - let r = await chatSendCmd(.apiSetUserGroupReceipts(userId: userId, userMsgReceiptSettings: userMsgReceiptSettings)) - if case .cmdOk = r { return } - throw r + try await sendCommandOkResp(.apiSetUserGroupReceipts(userId: userId, userMsgReceiptSettings: userMsgReceiptSettings)) } func apiHideUser(_ userId: Int64, viewPwd: String) async throws -> User { @@ -199,79 +217,88 @@ func apiUnmuteUser(_ userId: Int64) async throws -> User { } func setUserPrivacy_(_ cmd: ChatCommand) async throws -> User { - let r = await chatSendCmd(cmd) + let r: ChatResponse1 = try await chatSendCmd(cmd) if case let .userPrivacy(_, updatedUser) = r { return updatedUser } - throw r + throw r.unexpected } func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool, viewPwd: String?) async throws { - let r = await chatSendCmd(.apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues, viewPwd: viewPwd)) - if case .cmdOk = r { return } - throw r + try await sendCommandOkResp(.apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues, viewPwd: viewPwd)) } -func apiStartChat() throws -> Bool { - let r = chatSendCmdSync(.startChat(subscribe: true, expire: true, xftp: true)) +func apiStartChat(ctrl: chat_ctrl? = nil) throws -> Bool { + let r: ChatResponse0 = try chatSendCmdSync(.startChat(mainApp: true, enableSndFiles: true), ctrl: ctrl) switch r { case .chatStarted: return true case .chatRunning: return false - default: throw r + default: throw r.unexpected + } +} + +func apiCheckChatRunning() throws -> Bool { + let r: ChatResponse0 = try chatSendCmdSync(.checkChatRunning) + switch r { + case .chatRunning: return true + case .chatStopped: return false + default: throw r.unexpected } } func apiStopChat() async throws { - let r = await chatSendCmd(.apiStopChat) + let r: ChatResponse0 = try await chatSendCmd(.apiStopChat) switch r { case .chatStopped: return - default: throw r + default: throw r.unexpected } } func apiActivateChat() { chatReopenStore() - let r = chatSendCmdSync(.apiActivateChat(restoreChat: true)) - if case .cmdOk = r { return } - logger.error("apiActivateChat error: \(String(describing: r))") + do { + try sendCommandOkRespSync(.apiActivateChat(restoreChat: true)) + } catch { + logger.error("apiActivateChat error: \(responseError(error))") + } } func apiSuspendChat(timeoutMicroseconds: Int) { - let r = chatSendCmdSync(.apiSuspendChat(timeoutMicroseconds: timeoutMicroseconds)) - if case .cmdOk = r { return } - logger.error("apiSuspendChat error: \(String(describing: r))") + do { + try sendCommandOkRespSync(.apiSuspendChat(timeoutMicroseconds: timeoutMicroseconds)) + } catch { + logger.error("apiSuspendChat error: \(responseError(error))") + } } -func apiSetTempFolder(tempFolder: String) throws { - let r = chatSendCmdSync(.setTempFolder(tempFolder: tempFolder)) +func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String, ctrl: chat_ctrl? = nil) throws { + let r: ChatResponse2 = try chatSendCmdSync(.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder), ctrl: ctrl) if case .cmdOk = r { return } - throw r -} - -func apiSetFilesFolder(filesFolder: String) throws { - let r = chatSendCmdSync(.setFilesFolder(filesFolder: filesFolder)) - if case .cmdOk = r { return } - throw r -} - -func setXFTPConfig(_ cfg: XFTPFileConfig?) throws { - let r = chatSendCmdSync(.apiSetXFTPConfig(config: cfg)) - if case .cmdOk = r { return } - throw r + throw r.unexpected } func apiSetEncryptLocalFiles(_ enable: Bool) throws { - let r = chatSendCmdSync(.apiSetEncryptLocalFiles(enable: enable)) - if case .cmdOk = r { return } - throw r + try sendCommandOkRespSync(.apiSetEncryptLocalFiles(enable: enable)) } -func apiExportArchive(config: ArchiveConfig) async throws { - try await sendCommandOkResp(.apiExportArchive(config: config)) +func apiSaveAppSettings(settings: AppSettings) throws { + try sendCommandOkRespSync(.apiSaveSettings(settings: settings)) +} + +func apiGetAppSettings(settings: AppSettings) throws -> AppSettings { + let r: ChatResponse2 = try chatSendCmdSync(.apiGetSettings(settings: settings)) + if case let .appSettings(settings) = r { return settings } + throw r.unexpected +} + +func apiExportArchive(config: ArchiveConfig) async throws -> [ArchiveError] { + let r: ChatResponse2 = try await chatSendCmd(.apiExportArchive(config: config)) + if case let .archiveExported(archiveErrors) = r { return archiveErrors } + throw r.unexpected } func apiImportArchive(config: ArchiveConfig) async throws -> [ArchiveError] { - let r = await chatSendCmd(.apiImportArchive(config: config)) + let r: ChatResponse2 = try await chatSendCmd(.apiImportArchive(config: config)) if case let .archiveImported(archiveErrors) = r { return archiveErrors } - throw r + throw r.unexpected } func apiDeleteStorage() async throws { @@ -282,6 +309,10 @@ func apiStorageEncryption(currentKey: String = "", newKey: String = "") async th try await sendCommandOkResp(.apiStorageEncryption(config: DBEncryptionConfig(currentKey: currentKey, newKey: newKey))) } +func testStorageEncryption(key: String, ctrl: chat_ctrl? = nil) async throws { + try await sendCommandOkResp(.testStorageEncryption(key: key), ctrl: ctrl) +} + func apiGetChats() throws -> [ChatData] { let userId = try currentUserId("apiGetChats") return try apiChatsResponse(chatSendCmdSync(.apiGetChats(userId: userId))) @@ -292,48 +323,104 @@ func apiGetChatsAsync() async throws -> [ChatData] { return try apiChatsResponse(await chatSendCmd(.apiGetChats(userId: userId))) } -private func apiChatsResponse(_ r: ChatResponse) throws -> [ChatData] { +private func apiChatsResponse(_ r: ChatResponse0) throws -> [ChatData] { if case let .apiChats(_, chats) = r { return chats } - throw r + throw r.unexpected } -func apiGetChat(type: ChatType, id: Int64, search: String = "") throws -> Chat { - let r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: .last(count: 50), search: search)) - if case let .apiChat(_, chat) = r { return Chat.init(chat) } - throw r +func apiGetChatTags() throws -> [ChatTag] { + let userId = try currentUserId("apiGetChatTags") + let r: ChatResponse0 = try chatSendCmdSync(.apiGetChatTags(userId: userId)) + if case let .chatTags(_, tags) = r { return tags } + throw r.unexpected } -func apiGetChatItems(type: ChatType, id: Int64, pagination: ChatPagination, search: String = "") async throws -> [ChatItem] { - let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: pagination, search: search)) - if case let .apiChat(_, chat) = r { return chat.chatItems } - throw r +func apiGetChatTagsAsync() async throws -> [ChatTag] { + let userId = try currentUserId("apiGetChatTags") + let r: ChatResponse0 = try await chatSendCmd(.apiGetChatTags(userId: userId)) + if case let .chatTags(_, tags) = r { return tags } + throw r.unexpected } -func loadChat(chat: Chat, search: String = "") { - do { - let cInfo = chat.chatInfo - let m = ChatModel.shared +let loadItemsPerPage = 50 + +func apiGetChat(chatId: ChatId, pagination: ChatPagination, search: String = "") async throws -> (Chat, NavigationInfo) { + let r: ChatResponse0 = try await chatSendCmd(.apiGetChat(chatId: chatId, pagination: pagination, search: search)) + if case let .apiChat(_, chat, navInfo) = r { return (Chat.init(chat), navInfo ?? NavigationInfo()) } + throw r.unexpected +} + +func loadChat(chat: Chat, search: String = "", clearItems: Bool = true) async { + await loadChat(chatId: chat.chatInfo.id, search: search, clearItems: clearItems) +} + +func loadChat(chatId: ChatId, search: String = "", openAroundItemId: ChatItem.ID? = nil, clearItems: Bool = true) async { + let m = ChatModel.shared + let im = ItemsModel.shared + await MainActor.run { m.chatItemStatuses = [:] - m.reversedChatItems = [] - let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search) - m.updateChatInfo(chat.chatInfo) - m.reversedChatItems = chat.chatItems.reversed() - } catch let error { - logger.error("loadChat error: \(responseError(error))") + if clearItems { + im.reversedChatItems = [] + ItemsModel.shared.chatState.clear() + } } + await apiLoadMessages(chatId, openAroundItemId != nil ? .around(chatItemId: openAroundItemId!, count: loadItemsPerPage) : (search == "" ? .initial(count: loadItemsPerPage) : .last(count: loadItemsPerPage)), im.chatState, search, openAroundItemId, { 0...0 }) } func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -> ChatItemInfo { - let r = await chatSendCmd(.apiGetChatItemInfo(type: type, id: id, itemId: itemId)) + let r: ChatResponse0 = try await chatSendCmd(.apiGetChatItemInfo(type: type, id: id, itemId: itemId)) if case let .chatItemInfo(_, _, chatItemInfo) = r { return chatItemInfo } - throw r + throw r.unexpected } -func apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false, ttl: Int? = nil) async -> ChatItem? { +func apiPlanForwardChatItems(type: ChatType, id: Int64, itemIds: [Int64]) async throws -> ([Int64], ForwardConfirmation?) { + let r: ChatResponse1 = try await chatSendCmd(.apiPlanForwardChatItems(toChatType: type, toChatId: id, itemIds: itemIds)) + if case let .forwardPlan(_, chatItemIds, forwardConfimation) = r { return (chatItemIds, forwardConfimation) } + throw r.unexpected +} + +func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? { + let cmd: ChatCommand = .apiForwardChatItems(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemIds: itemIds, ttl: ttl) + return await processSendMessageCmd(toChatType: toChatType, cmd: cmd) +} + +func apiCreateChatTag(tag: ChatTagData) async throws -> [ChatTag] { + let r: ChatResponse0 = try await chatSendCmd(.apiCreateChatTag(tag: tag)) + if case let .chatTags(_, userTags) = r { + return userTags + } + throw r.unexpected +} + +func apiSetChatTags(type: ChatType, id: Int64, tagIds: [Int64]) async throws -> ([ChatTag], [Int64]) { + let r: ChatResponse0 = try await chatSendCmd(.apiSetChatTags(type: type, id: id, tagIds: tagIds)) + if case let .tagsUpdated(_, userTags, chatTags) = r { + return (userTags, chatTags) + } + throw r.unexpected +} + +func apiDeleteChatTag(tagId: Int64) async throws { + try await sendCommandOkResp(.apiDeleteChatTag(tagId: tagId)) +} + +func apiUpdateChatTag(tagId: Int64, tag: ChatTagData) async throws { + try await sendCommandOkResp(.apiUpdateChatTag(tagId: tagId, tagData: tag)) +} + +func apiReorderChatTags(tagIds: [Int64]) async throws { + try await sendCommandOkResp(.apiReorderChatTags(tagIds: tagIds)) +} + +func apiSendMessages(type: ChatType, id: Int64, live: Bool = false, ttl: Int? = nil, composedMessages: [ComposedMessage]) async -> [ChatItem]? { + let cmd: ChatCommand = .apiSendMessages(type: type, id: id, live: live, ttl: ttl, composedMessages: composedMessages) + return await processSendMessageCmd(toChatType: type, cmd: cmd) +} + +private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async -> [ChatItem]? { let chatModel = ChatModel.shared - let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl) - let r: ChatResponse - if type == .direct { + let r: APIResult + if toChatType == .direct { var cItem: ChatItem? = nil let endTask = beginBGTask({ if let cItem = cItem { @@ -342,76 +429,128 @@ func apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: } } }) - r = await chatSendCmd(cmd, bgTask: false) - if case let .newChatItem(_, aChatItem) = r { - cItem = aChatItem.chatItem - chatModel.messageDelivery[aChatItem.chatItem.id] = endTask - return cItem + r = await chatApiSendCmd(cmd, bgTask: false) + if case let .result(.newChatItems(_, aChatItems)) = r { + let cItems = aChatItems.map { $0.chatItem } + if let cItemLast = cItems.last { + cItem = cItemLast + chatModel.messageDelivery[cItemLast.id] = endTask + } + return cItems } if let networkErrorAlert = networkErrorAlert(r) { AlertManager.shared.showAlert(networkErrorAlert) } else { - sendMessageErrorAlert(r) + sendMessageErrorAlert(r.unexpected) } endTask() return nil } else { - r = await chatSendCmd(cmd, bgDelay: msgDelay) - if case let .newChatItem(_, aChatItem) = r { - return aChatItem.chatItem + r = await chatApiSendCmd(cmd, bgDelay: msgDelay) + if case let .result(.newChatItems(_, aChatItems)) = r { + return aChatItems.map { $0.chatItem } } - sendMessageErrorAlert(r) + sendMessageErrorAlert(r.unexpected) return nil } } -private func sendMessageErrorAlert(_ r: ChatResponse) { - logger.error("apiSendMessage error: \(String(describing: r))") +func apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage]) async -> [ChatItem]? { + let r: APIResult = await chatApiSendCmd(.apiCreateChatItems(noteFolderId: noteFolderId, composedMessages: composedMessages)) + if case let .result(.newChatItems(_, aChatItems)) = r { return aChatItems.map { $0.chatItem } } + createChatItemsErrorAlert(r.unexpected) + return nil +} + +func apiReportMessage(groupId: Int64, chatItemId: Int64, reportReason: ReportReason, reportText: String) async -> [ChatItem]? { + let r: APIResult = await chatApiSendCmd(.apiReportMessage(groupId: groupId, chatItemId: chatItemId, reportReason: reportReason, reportText: reportText)) + if case let .result(.newChatItems(_, aChatItems)) = r { return aChatItems.map { $0.chatItem } } + + logger.error("apiReportMessage error: \(String(describing: r))") + AlertManager.shared.showAlertMsg( + title: "Error creating report", + message: "Error: \(responseError(r.unexpected))" + ) + return nil +} + +private func sendMessageErrorAlert(_ r: ChatError) { + logger.error("send message error: \(String(describing: r))") AlertManager.shared.showAlertMsg( title: "Error sending message", - message: "Error: \(String(describing: r))" + message: "Error: \(responseError(r))" ) } -func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool = false) async throws -> ChatItem { - let r = await chatSendCmd(.apiUpdateChatItem(type: type, id: id, itemId: itemId, msg: msg, live: live), bgDelay: msgDelay) - if case let .chatItemUpdated(_, aChatItem) = r { return aChatItem.chatItem } - throw r +private func createChatItemsErrorAlert(_ r: ChatError) { + logger.error("apiCreateChatItems error: \(String(describing: r))") + AlertManager.shared.showAlertMsg( + title: "Error creating message", + message: "Error: \(responseError(r))" + ) +} + +func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool = false) async throws -> ChatItem { + let r: ChatResponse1 = try await chatSendCmd(.apiUpdateChatItem(type: type, id: id, itemId: itemId, updatedMessage: updatedMessage, live: live), bgDelay: msgDelay) + switch r { + case let .chatItemUpdated(_, aChatItem): return aChatItem.chatItem + case let .chatItemNotChanged(_, aChatItem): return aChatItem.chatItem + default: throw r.unexpected + } } func apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction) async throws -> ChatItem { - let r = await chatSendCmd(.apiChatItemReaction(type: type, id: id, itemId: itemId, add: add, reaction: reaction), bgDelay: msgDelay) + let r: ChatResponse1 = try await chatSendCmd(.apiChatItemReaction(type: type, id: id, itemId: itemId, add: add, reaction: reaction), bgDelay: msgDelay) if case let .chatItemReaction(_, _, reaction) = r { return reaction.chatReaction.chatItem } - throw r + throw r.unexpected } -func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) async throws -> (ChatItem, ChatItem?) { - let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemId: itemId, mode: mode), bgDelay: msgDelay) - if case let .chatItemDeleted(_, deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) } - throw r +func apiGetReactionMembers(groupId: Int64, itemId: Int64, reaction: MsgReaction) async throws -> [MemberReaction] { + let userId = try currentUserId("apiGetReactionMemebers") + let r: ChatResponse1 = try await chatSendCmd(.apiGetReactionMembers(userId: userId, groupId: groupId, itemId: itemId, reaction: reaction )) + if case let .reactionMembers(_, memberReactions) = r { return memberReactions } + throw r.unexpected } -func apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64) async throws -> (ChatItem, ChatItem?) { - let r = await chatSendCmd(.apiDeleteMemberChatItem(groupId: groupId, groupMemberId: groupMemberId, itemId: itemId), bgDelay: msgDelay) - if case let .chatItemDeleted(_, deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) } - throw r +func apiDeleteChatItems(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] { + let r: ChatResponse1 = try await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemIds: itemIds, mode: mode), bgDelay: msgDelay) + if case let .chatItemsDeleted(_, items, _) = r { return items } + throw r.unexpected } -func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode) { - let r = chatSendCmdSync(.apiGetNtfToken) +func apiDeleteMemberChatItems(groupId: Int64, itemIds: [Int64]) async throws -> [ChatItemDeletion] { + let r: ChatResponse1 = try await chatSendCmd(.apiDeleteMemberChatItem(groupId: groupId, itemIds: itemIds), bgDelay: msgDelay) + if case let .chatItemsDeleted(_, items, _) = r { return items } + throw r.unexpected +} + +func apiArchiveReceivedReports(groupId: Int64) async throws -> ChatResponse1 { + let r: ChatResponse1 = try await chatSendCmd(.apiArchiveReceivedReports(groupId: groupId), bgDelay: msgDelay) + if case .groupChatItemsDeleted = r { return r } + throw r.unexpected +} + +func apiDeleteReceivedReports(groupId: Int64, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] { + let r: ChatResponse1 = try await chatSendCmd(.apiDeleteReceivedReports(groupId: groupId, itemIds: itemIds, mode: mode), bgDelay: msgDelay) + if case let .chatItemsDeleted(_, chatItemDeletions, _) = r { return chatItemDeletions } + throw r.unexpected +} + +func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode, String?) { + let r: APIResult = chatApiSendCmdSync(.apiGetNtfToken) switch r { - case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode) - case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off) + case let .result(.ntfToken(token, status, ntfMode, ntfServer)): return (token, status, ntfMode, ntfServer) + case .error(.errorAgent(.CMD(.PROHIBITED, _))): return (nil, nil, .off, nil) default: - logger.debug("apiGetNtfToken response: \(String(describing: r), privacy: .public)") - return (nil, nil, .off) + logger.debug("apiGetNtfToken response: \(String(describing: r))") + return (nil, nil, .off, nil) } } func apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) async throws -> NtfTknStatus { - let r = await chatSendCmd(.apiRegisterToken(token: token, notificationMode: notificationMode)) + let r: ChatResponse2 = try await chatSendCmd(.apiRegisterToken(token: token, notificationMode: notificationMode)) if case let .ntfTokenStatus(status) = r { return status } - throw r + throw r.unexpected } func registerToken(token: DeviceToken) { @@ -423,7 +562,12 @@ func registerToken(token: DeviceToken) { Task { do { let status = try await apiRegisterToken(token: token, notificationMode: mode) - await MainActor.run { m.tokenStatus = status } + await MainActor.run { + m.tokenStatus = status + if !status.workingToken { + m.reRegisterTknStatus = status + } + } } catch let error { logger.error("registerToken apiRegisterToken error: \(responseError(error))") } @@ -431,36 +575,129 @@ func registerToken(token: DeviceToken) { } } +func tokenStatusInfo(_ status: NtfTknStatus, register: Bool) -> String { + String.localizedStringWithFormat(NSLocalizedString("Token status: %@.", comment: "token status"), status.text) + + "\n" + status.info(register: register) +} + +func reRegisterToken(token: DeviceToken) { + let m = ChatModel.shared + let mode = m.notificationMode + logger.debug("reRegisterToken \(mode.rawValue)") + Task { + do { + let status = try await apiRegisterToken(token: token, notificationMode: mode) + await MainActor.run { + m.tokenStatus = status + showAlert( + status.workingToken + ? NSLocalizedString("Notifications status", comment: "alert title") + : NSLocalizedString("Notifications error", comment: "alert title"), + message: tokenStatusInfo(status, register: false) + ) + } + } catch let error { + logger.error("reRegisterToken apiRegisterToken error: \(responseError(error))") + await MainActor.run { + showAlert( + NSLocalizedString("Error registering for notifications", comment: "alert title"), + message: responseError(error) + ) + } + } + } +} + func apiVerifyToken(token: DeviceToken, nonce: String, code: String) async throws { try await sendCommandOkResp(.apiVerifyToken(token: token, nonce: nonce, code: code)) } +func apiCheckToken(token: DeviceToken) async throws -> NtfTknStatus { + let r: ChatResponse2 = try await chatSendCmd(.apiCheckToken(token: token)) + if case let .ntfTokenStatus(status) = r { return status } + throw r.unexpected +} + func apiDeleteToken(token: DeviceToken) async throws { try await sendCommandOkResp(.apiDeleteToken(token: token)) } -func getUserProtoServers(_ serverProtocol: ServerProtocol) throws -> UserProtoServers { - let userId = try currentUserId("getUserProtoServers") - let r = chatSendCmdSync(.apiGetUserProtoServers(userId: userId, serverProtocol: serverProtocol)) - if case let .userProtoServers(_, servers) = r { return servers } - throw r -} - -func setUserProtoServers(_ serverProtocol: ServerProtocol, servers: [ServerCfg]) async throws { - let userId = try currentUserId("setUserProtoServers") - try await sendCommandOkResp(.apiSetUserProtoServers(userId: userId, serverProtocol: serverProtocol, servers: servers)) -} - func testProtoServer(server: String) async throws -> Result<(), ProtocolTestFailure> { let userId = try currentUserId("testProtoServer") - let r = await chatSendCmd(.apiTestProtoServer(userId: userId, server: server)) + let r: ChatResponse0 = try await chatSendCmd(.apiTestProtoServer(userId: userId, server: server)) if case let .serverTestResult(_, _, testFailure) = r { if let t = testFailure { return .failure(t) } return .success(()) } - throw r + throw r.unexpected +} + +func getServerOperators() async throws -> ServerOperatorConditions { + let r: ChatResponse0 = try await chatSendCmd(.apiGetServerOperators) + if case let .serverOperatorConditions(conditions) = r { return conditions } + logger.error("getServerOperators error: \(String(describing: r))") + throw r.unexpected +} + +func getServerOperatorsSync() throws -> ServerOperatorConditions { + let r: ChatResponse0 = try chatSendCmdSync(.apiGetServerOperators) + if case let .serverOperatorConditions(conditions) = r { return conditions } + logger.error("getServerOperators error: \(String(describing: r))") + throw r.unexpected +} + +func setServerOperators(operators: [ServerOperator]) async throws -> ServerOperatorConditions { + let r: ChatResponse0 = try await chatSendCmd(.apiSetServerOperators(operators: operators)) + if case let .serverOperatorConditions(conditions) = r { return conditions } + logger.error("setServerOperators error: \(String(describing: r))") + throw r.unexpected +} + +func getUserServers() async throws -> [UserOperatorServers] { + let userId = try currentUserId("getUserServers") + let r: ChatResponse0 = try await chatSendCmd(.apiGetUserServers(userId: userId)) + if case let .userServers(_, userServers) = r { return userServers } + logger.error("getUserServers error: \(String(describing: r))") + throw r.unexpected +} + +func setUserServers(userServers: [UserOperatorServers]) async throws { + let userId = try currentUserId("setUserServers") + let r: ChatResponse2 = try await chatSendCmd(.apiSetUserServers(userId: userId, userServers: userServers)) + if case .cmdOk = r { return } + logger.error("setUserServers error: \(String(describing: r))") + throw r.unexpected +} + +func validateServers(userServers: [UserOperatorServers]) async throws -> [UserServersError] { + let userId = try currentUserId("validateServers") + let r: ChatResponse0 = try await chatSendCmd(.apiValidateServers(userId: userId, userServers: userServers)) + if case let .userServersValidation(_, serverErrors) = r { return serverErrors } + logger.error("validateServers error: \(String(describing: r))") + throw r.unexpected +} + +func getUsageConditions() async throws -> (UsageConditions, String?, UsageConditions?) { + let r: ChatResponse0 = try await chatSendCmd(.apiGetUsageConditions) + if case let .usageConditions(usageConditions, conditionsText, acceptedConditions) = r { return (usageConditions, conditionsText, acceptedConditions) } + logger.error("getUsageConditions error: \(String(describing: r))") + throw r.unexpected +} + +func setConditionsNotified(conditionsId: Int64) async throws { + let r: ChatResponse2 = try await chatSendCmd(.apiSetConditionsNotified(conditionsId: conditionsId)) + if case .cmdOk = r { return } + logger.error("setConditionsNotified error: \(String(describing: r))") + throw r.unexpected +} + +func acceptConditions(conditionsId: Int64, operatorIds: [Int64]) async throws -> ServerOperatorConditions { + let r: ChatResponse0 = try await chatSendCmd(.apiAcceptConditions(conditionsId: conditionsId, operatorIds: operatorIds)) + if case let .serverOperatorConditions(conditions) = r { return conditions } + logger.error("acceptConditions error: \(String(describing: r))") + throw r.unexpected } func getChatItemTTL() throws -> ChatItemTTL { @@ -473,9 +710,15 @@ func getChatItemTTLAsync() async throws -> ChatItemTTL { return try chatItemTTLResponse(await chatSendCmd(.apiGetChatItemTTL(userId: userId))) } -private func chatItemTTLResponse(_ r: ChatResponse) throws -> ChatItemTTL { - if case let .chatItemTTL(_, chatItemTTL) = r { return ChatItemTTL(chatItemTTL) } - throw r +private func chatItemTTLResponse(_ r: ChatResponse0) throws -> ChatItemTTL { + if case let .chatItemTTL(_, chatItemTTL) = r { + if let ttl = chatItemTTL { + return ChatItemTTL(ttl) + } else { + throw RuntimeError("chatItemTTLResponse: invalid ttl") + } + } + throw r.unexpected } func setChatItemTTL(_ chatItemTTL: ChatItemTTL) async throws { @@ -483,22 +726,38 @@ func setChatItemTTL(_ chatItemTTL: ChatItemTTL) async throws { try await sendCommandOkResp(.apiSetChatItemTTL(userId: userId, seconds: chatItemTTL.seconds)) } -func getNetworkConfig() async throws -> NetCfg? { - let r = await chatSendCmd(.apiGetNetworkConfig) - if case let .networkConfig(cfg) = r { return cfg } - throw r +func setChatTTL(chatType: ChatType, id: Int64, _ chatItemTTL: ChatTTL) async throws { + let userId = try currentUserId("setChatItemTTL") + try await sendCommandOkResp(.apiSetChatTTL(userId: userId, type: chatType, id: id, seconds: chatItemTTL.value)) } -func setNetworkConfig(_ cfg: NetCfg) throws { - let r = chatSendCmdSync(.apiSetNetworkConfig(networkConfig: cfg)) +func getNetworkConfig() async throws -> NetCfg? { + let r: ChatResponse0 = try await chatSendCmd(.apiGetNetworkConfig) + if case let .networkConfig(cfg) = r { return cfg } + throw r.unexpected +} + +func setNetworkConfig(_ cfg: NetCfg, ctrl: chat_ctrl? = nil) throws { + let r: ChatResponse2 = try chatSendCmdSync(.apiSetNetworkConfig(networkConfig: cfg), ctrl: ctrl) if case .cmdOk = r { return } - throw r + throw r.unexpected +} + +func apiSetNetworkInfo(_ networkInfo: UserNetworkInfo) throws { + let r: ChatResponse2 = try chatSendCmdSync(.apiSetNetworkInfo(networkInfo: networkInfo)) + if case .cmdOk = r { return } + throw r.unexpected } func reconnectAllServers() async throws { try await sendCommandOkResp(.reconnectAllServers) } +func reconnectServer(smpServer: String) async throws { + let userId = try currentUserId("reconnectServer") + try await sendCommandOkResp(.reconnectServer(userId: userId, smpServer: smpServer)) +} + func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) async throws { try await sendCommandOkResp(.apiSetChatSettings(type: type, id: id, chatSettings: chatSettings)) } @@ -508,106 +767,135 @@ func apiSetMemberSettings(_ groupId: Int64, _ groupMemberId: Int64, _ memberSett } func apiContactInfo(_ contactId: Int64) async throws -> (ConnectionStats?, Profile?) { - let r = await chatSendCmd(.apiContactInfo(contactId: contactId)) + let r: ChatResponse0 = try await chatSendCmd(.apiContactInfo(contactId: contactId)) if case let .contactInfo(_, _, connStats, customUserProfile) = r { return (connStats, customUserProfile) } - throw r + throw r.unexpected } -func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, ConnectionStats?) { - let r = chatSendCmdSync(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId)) +func apiGroupMemberInfoSync(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, ConnectionStats?) { + let r: ChatResponse0 = try chatSendCmdSync(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId)) if case let .groupMemberInfo(_, _, member, connStats_) = r { return (member, connStats_) } - throw r + throw r.unexpected +} + +func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (GroupMember, ConnectionStats?) { + let r: ChatResponse0 = try await chatSendCmd(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId)) + if case let .groupMemberInfo(_, _, member, connStats_) = r { return (member, connStats_) } + throw r.unexpected +} + +func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) { + let r: ChatResponse0 = try await chatSendCmd(.apiContactQueueInfo(contactId: contactId)) + if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) } + throw r.unexpected +} + +func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) { + let r: ChatResponse0 = try await chatSendCmd(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId)) + if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) } + throw r.unexpected } func apiSwitchContact(contactId: Int64) throws -> ConnectionStats { - let r = chatSendCmdSync(.apiSwitchContact(contactId: contactId)) + let r: ChatResponse0 = try chatSendCmdSync(.apiSwitchContact(contactId: contactId)) if case let .contactSwitchStarted(_, _, connectionStats) = r { return connectionStats } - throw r + throw r.unexpected } func apiSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) throws -> ConnectionStats { - let r = chatSendCmdSync(.apiSwitchGroupMember(groupId: groupId, groupMemberId: groupMemberId)) + let r: ChatResponse0 = try chatSendCmdSync(.apiSwitchGroupMember(groupId: groupId, groupMemberId: groupMemberId)) if case let .groupMemberSwitchStarted(_, _, _, connectionStats) = r { return connectionStats } - throw r + throw r.unexpected } func apiAbortSwitchContact(_ contactId: Int64) throws -> ConnectionStats { - let r = chatSendCmdSync(.apiAbortSwitchContact(contactId: contactId)) + let r: ChatResponse0 = try chatSendCmdSync(.apiAbortSwitchContact(contactId: contactId)) if case let .contactSwitchAborted(_, _, connectionStats) = r { return connectionStats } - throw r + throw r.unexpected } func apiAbortSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) throws -> ConnectionStats { - let r = chatSendCmdSync(.apiAbortSwitchGroupMember(groupId: groupId, groupMemberId: groupMemberId)) + let r: ChatResponse0 = try chatSendCmdSync(.apiAbortSwitchGroupMember(groupId: groupId, groupMemberId: groupMemberId)) if case let .groupMemberSwitchAborted(_, _, _, connectionStats) = r { return connectionStats } - throw r + throw r.unexpected } func apiSyncContactRatchet(_ contactId: Int64, _ force: Bool) throws -> ConnectionStats { - let r = chatSendCmdSync(.apiSyncContactRatchet(contactId: contactId, force: force)) + let r: ChatResponse0 = try chatSendCmdSync(.apiSyncContactRatchet(contactId: contactId, force: force)) if case let .contactRatchetSyncStarted(_, _, connectionStats) = r { return connectionStats } - throw r + throw r.unexpected } func apiSyncGroupMemberRatchet(_ groupId: Int64, _ groupMemberId: Int64, _ force: Bool) throws -> (GroupMember, ConnectionStats) { - let r = chatSendCmdSync(.apiSyncGroupMemberRatchet(groupId: groupId, groupMemberId: groupMemberId, force: force)) + let r: ChatResponse0 = try chatSendCmdSync(.apiSyncGroupMemberRatchet(groupId: groupId, groupMemberId: groupMemberId, force: force)) if case let .groupMemberRatchetSyncStarted(_, _, member, connectionStats) = r { return (member, connectionStats) } - throw r + throw r.unexpected } func apiGetContactCode(_ contactId: Int64) async throws -> (Contact, String) { - let r = await chatSendCmd(.apiGetContactCode(contactId: contactId)) + let r: ChatResponse0 = try await chatSendCmd(.apiGetContactCode(contactId: contactId)) if case let .contactCode(_, contact, connectionCode) = r { return (contact, connectionCode) } - throw r + throw r.unexpected } -func apiGetGroupMemberCode(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, String) { - let r = chatSendCmdSync(.apiGetGroupMemberCode(groupId: groupId, groupMemberId: groupMemberId)) +func apiGetGroupMemberCode(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (GroupMember, String) { + let r: ChatResponse0 = try await chatSendCmd(.apiGetGroupMemberCode(groupId: groupId, groupMemberId: groupMemberId)) if case let .groupMemberCode(_, _, member, connectionCode) = r { return (member, connectionCode) } - throw r + throw r.unexpected } func apiVerifyContact(_ contactId: Int64, connectionCode: String?) -> (Bool, String)? { - let r = chatSendCmdSync(.apiVerifyContact(contactId: contactId, connectionCode: connectionCode)) - if case let .connectionVerified(_, verified, expectedCode) = r { return (verified, expectedCode) } + let r: APIResult = chatApiSendCmdSync(.apiVerifyContact(contactId: contactId, connectionCode: connectionCode)) + if case let .result(.connectionVerified(_, verified, expectedCode)) = r { return (verified, expectedCode) } logger.error("apiVerifyContact error: \(String(describing: r))") return nil } func apiVerifyGroupMember(_ groupId: Int64, _ groupMemberId: Int64, connectionCode: String?) -> (Bool, String)? { - let r = chatSendCmdSync(.apiVerifyGroupMember(groupId: groupId, groupMemberId: groupMemberId, connectionCode: connectionCode)) - if case let .connectionVerified(_, verified, expectedCode) = r { return (verified, expectedCode) } + let r: APIResult = chatApiSendCmdSync(.apiVerifyGroupMember(groupId: groupId, groupMemberId: groupMemberId, connectionCode: connectionCode)) + if case let .result(.connectionVerified(_, verified, expectedCode)) = r { return (verified, expectedCode) } logger.error("apiVerifyGroupMember error: \(String(describing: r))") return nil } -func apiAddContact(incognito: Bool) async -> (String, PendingContactConnection)? { +func apiAddContact(incognito: Bool) async -> ((CreatedConnLink, PendingContactConnection)?, Alert?) { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiAddContact: no current user") - return nil + return (nil, nil) } - let r = await chatSendCmd(.apiAddContact(userId: userId, incognito: incognito), bgTask: false) - if case let .invitation(_, connReqInvitation, connection) = r { return (connReqInvitation, connection) } - AlertManager.shared.showAlert(connectionErrorAlert(r)) - return nil + let short = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS) + let r: APIResult = await chatApiSendCmd(.apiAddContact(userId: userId, short: short, incognito: incognito), bgTask: false) + if case let .result(.invitation(_, connLinkInv, connection)) = r { return ((connLinkInv, connection), nil) } + let alert = connectionErrorAlert(r) + return (nil, alert) } func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> PendingContactConnection? { - let r = await chatSendCmd(.apiSetConnectionIncognito(connId: connId, incognito: incognito)) + let r: ChatResponse1 = try await chatSendCmd(.apiSetConnectionIncognito(connId: connId, incognito: incognito)) if case let .connectionIncognitoUpdated(_, toConnection) = r { return toConnection } - throw r + throw r.unexpected } -func apiConnectPlan(connReq: String) async throws -> ConnectionPlan { - let userId = try currentUserId("apiConnectPlan") - let r = await chatSendCmd(.apiConnectPlan(userId: userId, connReq: connReq)) - if case let .connectionPlan(_, connectionPlan) = r { return connectionPlan } - logger.error("apiConnectPlan error: \(responseError(r))") - throw r +func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> PendingContactConnection { + let r: ChatResponse1 = try await chatSendCmd(.apiChangeConnectionUser(connId: connId, userId: userId)) + + if case let .connectionUserChanged(_, _, toConnection, _) = r {return toConnection} + throw r.unexpected } -func apiConnect(incognito: Bool, connReq: String) async -> (ConnReqType, PendingContactConnection)? { - let (r, alert) = await apiConnect_(incognito: incognito, connReq: connReq) +func apiConnectPlan(connLink: String) 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 chatApiSendCmd(.apiConnectPlan(userId: userId, connLink: connLink)) + if case let .result(.connectionPlan(_, connLink, connPlan)) = r { return ((connLink, connPlan), nil) } + let alert = apiConnectResponseAlert(r.unexpected) ?? connectionErrorAlert(r) + return (nil, alert) +} + +func apiConnect(incognito: Bool, connLink: CreatedConnLink) async -> (ConnReqType, PendingContactConnection)? { + let (r, alert) = await apiConnect_(incognito: incognito, connLink: connLink) if let alert = alert { AlertManager.shared.showAlert(alert) return nil @@ -616,48 +904,74 @@ func apiConnect(incognito: Bool, connReq: String) async -> (ConnReqType, Pending } } -func apiConnect_(incognito: Bool, connReq: String) async -> ((ConnReqType, PendingContactConnection)?, Alert?) { +func apiConnect_(incognito: Bool, connLink: CreatedConnLink) async -> ((ConnReqType, PendingContactConnection)?, Alert?) { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiConnect: no current user") return (nil, nil) } - let r = await chatSendCmd(.apiConnect(userId: userId, incognito: incognito, connReq: connReq)) + let r: APIResult = await chatApiSendCmd(.apiConnect(userId: userId, incognito: incognito, connLink: connLink)) let m = ChatModel.shared switch r { - case let .sentConfirmation(_, connection): + case let .result(.sentConfirmation(_, connection)): return ((.invitation, connection), nil) - case let .sentInvitation(_, connection): + case let .result(.sentInvitation(_, connection)): return ((.contact, connection), nil) - case let .contactAlreadyExists(_, contact): + case let .result(.contactAlreadyExists(_, contact)): if let c = m.getContactChat(contact.contactId) { - await MainActor.run { m.chatId = c.id } + ItemsModel.shared.loadOpenChat(c.id) } let alert = contactAlreadyExistsAlert(contact) return (nil, alert) - case .chatCmdError(_, .error(.invalidConnReq)): - let alert = mkAlert( + default: () + } + let alert = apiConnectResponseAlert(r.unexpected) ?? connectionErrorAlert(r) + return (nil, alert) +} + +private func apiConnectResponseAlert(_ r: ChatError) -> Alert? { + switch r { + case .error(.invalidConnReq): + mkAlert( title: "Invalid connection link", message: "Please check that you used the correct link or ask your contact to send you another one." ) - return (nil, alert) - case .chatCmdError(_, .errorAgent(.SMP(.AUTH))): - let alert = mkAlert( + case .error(.unsupportedConnReq): + mkAlert( + title: "Unsupported connection link", + message: "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." + ) + case .errorAgent(.SMP(_, .AUTH)): + mkAlert( title: "Connection error (AUTH)", message: "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." ) - return (nil, alert) - case let .chatCmdError(_, .errorAgent(.INTERNAL(internalErr))): + case let .errorAgent(.SMP(_, .BLOCKED(info))): + Alert( + title: Text("Connection blocked"), + message: Text("Connection is blocked by server operator:\n\(info.reason.text)"), + primaryButton: .default(Text("Ok")), + secondaryButton: .default(Text("How it works")) { + DispatchQueue.main.async { + UIApplication.shared.open(contentModerationPostLink) + } + } + ) + case .errorAgent(.SMP(_, .QUOTA)): + mkAlert( + title: "Undelivered messages", + message: "The connection reached the limit of undelivered messages, your contact may be offline." + ) + case let .errorAgent(.INTERNAL(internalErr)): if internalErr == "SEUniqueID" { - let alert = mkAlert( + mkAlert( title: "Already connected?", message: "It seems like you are already connected via this link. If it is not the case, there was an error (\(responseError(r)))." ) - return (nil, alert) + } else { + nil } - default: () + default: nil } - let alert = connectionErrorAlert(r) - return (nil, alert) } func contactAlreadyExistsAlert(_ contact: Contact) -> Alert { @@ -667,13 +981,13 @@ func contactAlreadyExistsAlert(_ contact: Contact) -> Alert { ) } -private func connectionErrorAlert(_ r: ChatResponse) -> Alert { +private func connectionErrorAlert(_ r: APIResult) -> Alert { if let networkErrorAlert = networkErrorAlert(r) { return networkErrorAlert } else { return mkAlert( title: "Connection error", - message: "Error: \(String(describing: r))" + message: "Error: \(responseError(r.unexpected))" ) } } @@ -683,26 +997,45 @@ func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> (Co logger.error("apiConnectContactViaAddress: no current user") return (nil, nil) } - let r = await chatSendCmd(.apiConnectContactViaAddress(userId: userId, incognito: incognito, contactId: contactId)) - if case let .sentInvitationToContact(_, contact, _) = r { return (contact, nil) } - logger.error("apiConnectContactViaAddress error: \(responseError(r))") + let r: APIResult = await chatApiSendCmd(.apiConnectContactViaAddress(userId: userId, incognito: incognito, contactId: contactId)) + if case let .result(.sentInvitationToContact(_, contact, _)) = r { return (contact, nil) } + logger.error("apiConnectContactViaAddress error: \(responseError(r.unexpected))") let alert = connectionErrorAlert(r) return (nil, alert) } -func apiDeleteChat(type: ChatType, id: Int64, notify: Bool? = nil) async throws { - let r = await chatSendCmd(.apiDeleteChat(type: type, id: id, notify: notify), bgTask: false) +func apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async throws { + let chatId = type.rawValue + id.description + DispatchQueue.main.async { ChatModel.shared.deletedChats.insert(chatId) } + defer { DispatchQueue.main.async { ChatModel.shared.deletedChats.remove(chatId) } } + let r: ChatResponse1 = try await chatSendCmd(.apiDeleteChat(type: type, id: id, chatDeleteMode: chatDeleteMode), bgTask: false) if case .direct = type, case .contactDeleted = r { return } if case .contactConnection = type, case .contactConnectionDeleted = r { return } if case .group = type, case .groupDeletedUser = r { return } - throw r + throw r.unexpected } -func deleteChat(_ chat: Chat, notify: Bool? = nil) async { +func apiDeleteContact(id: Int64, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async throws -> Contact { + let type: ChatType = .direct + let chatId = type.rawValue + id.description + if case .full = chatDeleteMode { + DispatchQueue.main.async { ChatModel.shared.deletedChats.insert(chatId) } + } + defer { + if case .full = chatDeleteMode { + DispatchQueue.main.async { ChatModel.shared.deletedChats.remove(chatId) } + } + } + let r: ChatResponse1 = try await chatSendCmd(.apiDeleteChat(type: type, id: id, chatDeleteMode: chatDeleteMode), bgTask: false) + if case let .contactDeleted(_, contact) = r { return contact } + throw r.unexpected +} + +func deleteChat(_ chat: Chat, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async { do { let cInfo = chat.chatInfo - try await apiDeleteChat(type: cInfo.chatType, id: cInfo.apiId, notify: notify) - DispatchQueue.main.async { ChatModel.shared.removeChat(cInfo.id) } + try await apiDeleteChat(type: cInfo.chatType, id: cInfo.apiId, chatDeleteMode: chatDeleteMode) + await MainActor.run { ChatModel.shared.removeChat(cInfo.id) } } catch let error { logger.error("deleteChat apiDeleteChat error: \(responseError(error))") AlertManager.shared.showAlertMsg( @@ -712,10 +1045,43 @@ func deleteChat(_ chat: Chat, notify: Bool? = nil) async { } } +func deleteContactChat(_ chat: Chat, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async -> Alert? { + do { + let cInfo = chat.chatInfo + let ct = try await apiDeleteContact(id: cInfo.apiId, chatDeleteMode: chatDeleteMode) + await MainActor.run { + switch chatDeleteMode { + case .full: + ChatModel.shared.removeChat(cInfo.id) + case .entity: + ChatModel.shared.removeChat(cInfo.id) + ChatModel.shared.addChat(Chat( + chatInfo: .direct(contact: ct), + chatItems: chat.chatItems + )) + case .messages: + ChatModel.shared.removeChat(cInfo.id) + ChatModel.shared.addChat(Chat( + chatInfo: .direct(contact: ct), + chatItems: [] + )) + } + } + } catch let error { + logger.error("deleteContactChat apiDeleteContact error: \(responseError(error))") + return mkAlert( + title: "Error deleting chat!", + message: "Error: \(responseError(error))" + ) + } + return nil +} + + func apiClearChat(type: ChatType, id: Int64) async throws -> ChatInfo { - let r = await chatSendCmd(.apiClearChat(type: type, id: id), bgTask: false) + let r: ChatResponse1 = try await chatSendCmd(.apiClearChat(type: type, id: id), bgTask: false) if case let .chatCleared(_, updatedChatInfo) = r { return updatedChatInfo } - throw r + throw r.unexpected } func clearChat(_ chat: Chat) async { @@ -730,98 +1096,125 @@ func clearChat(_ chat: Chat) async { func apiListContacts() throws -> [Contact] { let userId = try currentUserId("apiListContacts") - let r = chatSendCmdSync(.apiListContacts(userId: userId)) + let r: ChatResponse1 = try chatSendCmdSync(.apiListContacts(userId: userId)) if case let .contactsList(_, contacts) = r { return contacts } - throw r + throw r.unexpected } func apiUpdateProfile(profile: Profile) async throws -> (Profile, [Contact])? { let userId = try currentUserId("apiUpdateProfile") - let r = await chatSendCmd(.apiUpdateProfile(userId: userId, profile: profile)) + let r: APIResult = await chatApiSendCmd(.apiUpdateProfile(userId: userId, profile: profile)) switch r { - case .userProfileNoChange: return (profile, []) - case let .userProfileUpdated(_, _, toProfile, updateSummary): return (toProfile, updateSummary.changedContacts) - case .chatCmdError(_, .errorStore(.duplicateName)): return nil; - default: throw r + case .result(.userProfileNoChange): return (profile, []) + case let .result(.userProfileUpdated(_, _, toProfile, updateSummary)): return (toProfile, updateSummary.changedContacts) + case .error(.errorStore(.duplicateName)): return nil; + default: throw r.unexpected } } func apiSetProfileAddress(on: Bool) async throws -> User? { let userId = try currentUserId("apiSetProfileAddress") - let r = await chatSendCmd(.apiSetProfileAddress(userId: userId, on: on)) + let r: ChatResponse1 = try await chatSendCmd(.apiSetProfileAddress(userId: userId, on: on)) switch r { case .userProfileNoChange: return nil case let .userProfileUpdated(user, _, _, _): return user - default: throw r + default: throw r.unexpected } } func apiSetContactPrefs(contactId: Int64, preferences: Preferences) async throws -> Contact? { - let r = await chatSendCmd(.apiSetContactPrefs(contactId: contactId, preferences: preferences)) + let r: ChatResponse1 = try await chatSendCmd(.apiSetContactPrefs(contactId: contactId, preferences: preferences)) if case let .contactPrefsUpdated(_, _, toContact) = r { return toContact } - throw r + throw r.unexpected } func apiSetContactAlias(contactId: Int64, localAlias: String) async throws -> Contact? { - let r = await chatSendCmd(.apiSetContactAlias(contactId: contactId, localAlias: localAlias)) + let r: ChatResponse1 = try await chatSendCmd(.apiSetContactAlias(contactId: contactId, localAlias: localAlias)) if case let .contactAliasUpdated(_, toContact) = r { return toContact } - throw r + throw r.unexpected +} + +func apiSetGroupAlias(groupId: Int64, localAlias: String) async throws -> GroupInfo? { + let r: ChatResponse1 = try await chatSendCmd(.apiSetGroupAlias(groupId: groupId, localAlias: localAlias)) + if case let .groupAliasUpdated(_, toGroup) = r { return toGroup } + throw r.unexpected } func apiSetConnectionAlias(connId: Int64, localAlias: String) async throws -> PendingContactConnection? { - let r = await chatSendCmd(.apiSetConnectionAlias(connId: connId, localAlias: localAlias)) + let r: ChatResponse1 = try await chatSendCmd(.apiSetConnectionAlias(connId: connId, localAlias: localAlias)) if case let .connectionAliasUpdated(_, toConnection) = r { return toConnection } - throw r + throw r.unexpected } -func apiCreateUserAddress() async throws -> String { +func apiSetUserUIThemes(userId: Int64, themes: ThemeModeOverrides?) async -> Bool { + do { + try await sendCommandOkResp(.apiSetUserUIThemes(userId: userId, themes: themes)) + return true + } catch { + logger.error("apiSetUserUIThemes bad response: \(responseError(error))") + return false + } +} + +func apiSetChatUIThemes(chatId: ChatId, themes: ThemeModeOverrides?) async -> Bool { + do { + try await sendCommandOkResp(.apiSetChatUIThemes(chatId: chatId, themes: themes)) + return true + } catch { + logger.error("apiSetChatUIThemes bad response: \(responseError(error))") + return false + } +} + + +func apiCreateUserAddress(short: Bool) async throws -> CreatedConnLink { let userId = try currentUserId("apiCreateUserAddress") - let r = await chatSendCmd(.apiCreateMyAddress(userId: userId)) - if case let .userContactLinkCreated(_, connReq) = r { return connReq } - throw r + let r: ChatResponse1 = try await chatSendCmd(.apiCreateMyAddress(userId: userId, short: short)) + if case let .userContactLinkCreated(_, connLink) = r { return connLink } + throw r.unexpected } func apiDeleteUserAddress() async throws -> User? { let userId = try currentUserId("apiDeleteUserAddress") - let r = await chatSendCmd(.apiDeleteMyAddress(userId: userId)) + let r: ChatResponse1 = try await chatSendCmd(.apiDeleteMyAddress(userId: userId)) if case let .userContactLinkDeleted(user) = r { return user } - throw r + throw r.unexpected } func apiGetUserAddress() throws -> UserContactLink? { let userId = try currentUserId("apiGetUserAddress") - return try userAddressResponse(chatSendCmdSync(.apiShowMyAddress(userId: userId))) + return try userAddressResponse(chatApiSendCmdSync(.apiShowMyAddress(userId: userId))) } func apiGetUserAddressAsync() async throws -> UserContactLink? { let userId = try currentUserId("apiGetUserAddressAsync") - return try userAddressResponse(await chatSendCmd(.apiShowMyAddress(userId: userId))) + return try userAddressResponse(await chatApiSendCmd(.apiShowMyAddress(userId: userId))) } -private func userAddressResponse(_ r: ChatResponse) throws -> UserContactLink? { +private func userAddressResponse(_ r: APIResult) throws -> UserContactLink? { switch r { - case let .userContactLink(_, contactLink): return contactLink - case .chatCmdError(_, chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil - default: throw r + case let .result(.userContactLink(_, contactLink)): return contactLink + case .error(.errorStore(storeError: .userContactLinkNotFound)): return nil + default: throw r.unexpected } } func userAddressAutoAccept(_ autoAccept: AutoAccept?) async throws -> UserContactLink? { let userId = try currentUserId("userAddressAutoAccept") - let r = await chatSendCmd(.apiAddressAutoAccept(userId: userId, autoAccept: autoAccept)) + let r: APIResult = await chatApiSendCmd(.apiAddressAutoAccept(userId: userId, autoAccept: autoAccept)) switch r { - case let .userContactLinkUpdated(_, contactLink): return contactLink - case .chatCmdError(_, chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil - default: throw r + case let .result(.userContactLinkUpdated(_, contactLink)): return contactLink + case .error(.errorStore(storeError: .userContactLinkNotFound)): return nil + default: throw r.unexpected } } func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Contact? { - let r = await chatSendCmd(.apiAcceptContact(incognito: incognito, contactReqId: contactReqId)) + let r: APIResult = await chatApiSendCmd(.apiAcceptContact(incognito: incognito, contactReqId: contactReqId)) let am = AlertManager.shared - if case let .acceptingContactRequest(_, contact) = r { return contact } - if case .chatCmdError(_, .errorAgent(.SMP(.AUTH))) = r { + if case let .result(.acceptingContactRequest(_, contact)) = r { return contact } + if case .error(.errorAgent(.SMP(_, .AUTH))) = r { am.showAlertMsg( title: "Connection error (AUTH)", message: "Sender may have deleted the connection request." @@ -832,64 +1225,180 @@ func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Cont logger.error("apiAcceptContactRequest error: \(String(describing: r))") am.showAlertMsg( title: "Error accepting contact request", - message: "Error: \(String(describing: r))" + message: "Error: \(responseError(r.unexpected))" ) } return nil } func apiRejectContactRequest(contactReqId: Int64) async throws { - let r = await chatSendCmd(.apiRejectContact(contactReqId: contactReqId)) + let r: ChatResponse1 = try await chatSendCmd(.apiRejectContact(contactReqId: contactReqId)) if case .contactRequestRejected = r { return } - throw r + throw r.unexpected } -func apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) async throws { - try await sendCommandOkResp(.apiChatRead(type: type, id: id, itemRange: itemRange)) +func apiChatRead(type: ChatType, id: Int64) async throws { + try await sendCommandOkResp(.apiChatRead(type: type, id: id)) +} + +func apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64]) async throws { + try await sendCommandOkResp(.apiChatItemsRead(type: type, id: id, itemIds: itemIds)) } func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws { try await sendCommandOkResp(.apiChatUnread(type: type, id: id, unreadChat: unreadChat)) } -func receiveFile(user: any UserLike, fileId: Int64, encrypted: Bool, auto: Bool = false) async { - if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: encrypted, auto: auto) { - await chatItemSimpleUpdate(user, chatItem) - } -} - -func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? { - let r = await chatSendCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline)) - let am = AlertManager.shared - if case let .rcvFileAccepted(_, chatItem) = r { return chatItem } - if case .rcvFileAcceptedSndCancelled = r { - logger.debug("apiReceiveFile error: sender cancelled file transfer") - if !auto { - am.showAlertMsg( - title: "Cannot receive file", - message: "Sender cancelled file transfer." - ) - } - } else if let networkErrorAlert = networkErrorAlert(r) { - logger.error("apiReceiveFile network error: \(String(describing: r))") - am.showAlert(networkErrorAlert) +func uploadStandaloneFile(user: any UserLike, file: CryptoFile, ctrl: chat_ctrl? = nil) async -> (FileTransferMeta?, String?) { + let r: APIResult = await chatApiSendCmd(.apiUploadStandaloneFile(userId: user.userId, file: file), ctrl: ctrl) + if case let .result(.sndStandaloneFileCreated(_, fileTransferMeta)) = r { + return (fileTransferMeta, nil) } else { - switch chatError(r) { - case .fileCancelled: - logger.debug("apiReceiveFile ignoring fileCancelled error") - case .fileAlreadyReceiving: - logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error") - default: - logger.error("apiReceiveFile error: \(String(describing: r))") - am.showAlertMsg( - title: "Error receiving file", - message: "Error: \(String(describing: r))" - ) - } + let err = responseError(r.unexpected) + logger.error("uploadStandaloneFile error: \(err)") + return (nil, err) } - return nil } +func downloadStandaloneFile(user: any UserLike, url: String, file: CryptoFile, ctrl: chat_ctrl? = nil) async -> (RcvFileTransfer?, String?) { + let r: APIResult = await chatApiSendCmd(.apiDownloadStandaloneFile(userId: user.userId, url: url, file: file), ctrl: ctrl) + if case let .result(.rcvStandaloneFileCreated(_, rcvFileTransfer)) = r { + return (rcvFileTransfer, nil) + } else { + let err = responseError(r.unexpected) + logger.error("downloadStandaloneFile error: \(err)") + return (nil, err) + } +} + +func standaloneFileInfo(url: String, ctrl: chat_ctrl? = nil) async -> MigrationFileLinkData? { + let r: APIResult = await chatApiSendCmd(.apiStandaloneFileInfo(url: url), ctrl: ctrl) + if case let .result(.standaloneFileInfo(fileMeta)) = r { + return fileMeta + } else { + logger.error("standaloneFileInfo error: \(responseError(r.unexpected))") + return nil + } +} + +func receiveFile(user: any UserLike, fileId: Int64, userApprovedRelays: Bool = false, auto: Bool = false) async { + await receiveFiles( + user: user, + fileIds: [fileId], + userApprovedRelays: userApprovedRelays, + auto: auto + ) +} + +func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool = false, auto: Bool = false) async { + var fileIdsToApprove: [Int64] = [] + var srvsToApprove: Set = [] + var otherFileErrs: [APIResult] = [] + + for fileId in fileIds { + let r: APIResult = await chatApiSendCmd( + .receiveFile( + fileId: fileId, + userApprovedRelays: userApprovedRelays || !privacyAskToApproveRelaysGroupDefault.get(), + encrypted: privacyEncryptLocalFilesGroupDefault.get(), + inline: nil + ) + ) + switch r { + case let .result(.rcvFileAccepted(_, chatItem)): + await chatItemSimpleUpdate(user, chatItem) + // TODO when aChatItem added + // case let .rcvFileAcceptedSndCancelled(user, aChatItem, _): + // await chatItemSimpleUpdate(user, aChatItem) + // Task { cleanupFile(aChatItem) } + case let .error(.error(.fileNotApproved(fileId, unknownServers))): + fileIdsToApprove.append(fileId) + srvsToApprove.formUnion(unknownServers) + default: + otherFileErrs.append(r) + } + } + + if !auto { + let otherErrsStr = fileErrorStrs(otherFileErrs) + // If there are not approved files, alert is shown the same way both in case of singular and plural files reception + if !fileIdsToApprove.isEmpty { + let srvs = srvsToApprove + .map { s in + if let srv = parseServerAddress(s), !srv.hostnames.isEmpty { + srv.hostnames[0] + } else { + serverHost(s) + } + } + .sorted() + .joined(separator: ", ") + let fIds = fileIdsToApprove + await MainActor.run { + showAlert( + title: NSLocalizedString("Unknown servers!", comment: "alert title"), + message: ( + String.localizedStringWithFormat(NSLocalizedString("Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.", comment: "alert message"), srvs) + + (otherErrsStr != "" ? "\n\n" + String.localizedStringWithFormat(NSLocalizedString("Other file errors:\n%@", comment: "alert message"), otherErrsStr) : "") + ), + buttonTitle: NSLocalizedString("Download", comment: "alert button"), + buttonAction: { + Task { + logger.debug("apiReceiveFile fileNotApproved alert - in Task") + if let user = ChatModel.shared.currentUser { + await receiveFiles(user: user, fileIds: fIds, userApprovedRelays: true) + } + } + }, + cancelButton: true + ) + } + } else if otherFileErrs.count == 1 { // If there is a single other error, we differentiate on it + let errorResponse = otherFileErrs.first! + switch errorResponse { + case let .result(.rcvFileAcceptedSndCancelled(_, rcvFileTransfer)): + logger.debug("receiveFiles error: sender cancelled file transfer \(rcvFileTransfer.fileId)") + await MainActor.run { + showAlert( + NSLocalizedString("Cannot receive file", comment: "alert title"), + message: NSLocalizedString("Sender cancelled file transfer.", comment: "alert message") + ) + } + case .error(.error(.fileCancelled)), .error(.error(.fileAlreadyReceiving)): + logger.debug("receiveFiles ignoring FileCancelled or FileAlreadyReceiving error") + default: + await MainActor.run { + showAlert( + NSLocalizedString("Error receiving file", comment: "alert title"), + message: responseError(errorResponse.unexpected) + ) + } + } + } else if otherFileErrs.count > 1 { // If there are multiple other errors, we show general alert + await MainActor.run { + showAlert( + NSLocalizedString("Error receiving file", comment: "alert title"), + message: String.localizedStringWithFormat(NSLocalizedString("File errors:\n%@", comment: "alert message"), otherErrsStr) + ) + } + } + } + + func fileErrorStrs(_ errs: [APIResult]) -> String { + var errStr = "" + if errs.count >= 1 { + errStr = String(describing: errs[0].unexpected) + } + if errs.count >= 2 { + errStr += "\n\(String(describing: errs[1].unexpected))" + } + if errs.count > 2 { + errStr += "\nand \(errs.count - 2) other error(s)" + } + return errStr + } +} + func cancelFile(user: User, fileId: Int64) async { if let chatItem = await apiCancelFile(fileId: fileId) { await chatItemSimpleUpdate(user, chatItem) @@ -897,13 +1406,13 @@ func cancelFile(user: User, fileId: Int64) async { } } -func apiCancelFile(fileId: Int64) async -> AChatItem? { - let r = await chatSendCmd(.cancelFile(fileId: fileId)) +func apiCancelFile(fileId: Int64, ctrl: chat_ctrl? = nil) async -> AChatItem? { + let r: APIResult = await chatApiSendCmd(.cancelFile(fileId: fileId), ctrl: ctrl) switch r { - case let .sndFileCancelled(_, chatItem, _, _) : return chatItem - case let .rcvFileCancelled(_, chatItem, _) : return chatItem + case let .result(.sndFileCancelled(_, chatItem, _, _)) : return chatItem + case let .result(.rcvFileCancelled(_, chatItem, _)) : return chatItem default: - logger.error("apiCancelFile error: \(String(describing: r))") + logger.error("apiCancelFile error: \(responseError(r.unexpected))") return nil } } @@ -913,9 +1422,9 @@ func setLocalDeviceName(_ displayName: String) throws { } func connectRemoteCtrl(desktopAddress: String) async throws -> (RemoteCtrlInfo?, CtrlAppInfo, String) { - let r = await chatSendCmd(.connectRemoteCtrl(xrcpInvitation: desktopAddress)) + let r: ChatResponse2 = try await chatSendCmd(.connectRemoteCtrl(xrcpInvitation: desktopAddress)) if case let .remoteCtrlConnecting(rc_, ctrlAppInfo, v) = r { return (rc_, ctrlAppInfo, v) } - throw r + throw r.unexpected } func findKnownRemoteCtrl() async throws { @@ -923,21 +1432,21 @@ func findKnownRemoteCtrl() async throws { } func confirmRemoteCtrl(_ rcId: Int64) async throws -> (RemoteCtrlInfo?, CtrlAppInfo, String) { - let r = await chatSendCmd(.confirmRemoteCtrl(remoteCtrlId: rcId)) + let r: ChatResponse2 = try await chatSendCmd(.confirmRemoteCtrl(remoteCtrlId: rcId)) if case let .remoteCtrlConnecting(rc_, ctrlAppInfo, v) = r { return (rc_, ctrlAppInfo, v) } - throw r + throw r.unexpected } func verifyRemoteCtrlSession(_ sessCode: String) async throws -> RemoteCtrlInfo { - let r = await chatSendCmd(.verifyRemoteCtrlSession(sessionCode: sessCode)) + let r: ChatResponse2 = try await chatSendCmd(.verifyRemoteCtrlSession(sessionCode: sessCode)) if case let .remoteCtrlConnected(rc) = r { return rc } - throw r + throw r.unexpected } func listRemoteCtrls() throws -> [RemoteCtrlInfo] { - let r = chatSendCmdSync(.listRemoteCtrls) + let r: ChatResponse2 = try chatSendCmdSync(.listRemoteCtrls) if case let .remoteCtrlList(rcInfo) = r { return rcInfo } - throw r + throw r.unexpected } func stopRemoteCtrl() async throws { @@ -948,19 +1457,10 @@ func deleteRemoteCtrl(_ rcId: Int64) async throws { try await sendCommandOkResp(.deleteRemoteCtrl(remoteCtrlId: rcId)) } -func networkErrorAlert(_ r: ChatResponse) -> Alert? { - switch r { - case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))): - return mkAlert( - title: "Connection timeout", - message: "Please check your network connection with \(serverHostname(addr)) and try again." - ) - case let .chatCmdError(_, .errorAgent(.BROKER(addr, .NETWORK))): - return mkAlert( - title: "Connection error", - message: "Please check your network connection with \(serverHostname(addr)) and try again." - ) - default: +func networkErrorAlert(_ res: APIResult) -> Alert? { + if case let .error(e) = res, let alert = getNetworkErrorAlert(e) { + return mkAlert(title: alert.title, message: alert.message) + } else { return nil } } @@ -968,7 +1468,17 @@ func networkErrorAlert(_ r: ChatResponse) -> Alert? { func acceptContactRequest(incognito: Bool, contactRequest: UserContactRequest) async { if let contact = await apiAcceptContactRequest(incognito: incognito, contactReqId: contactRequest.apiId) { let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: []) - DispatchQueue.main.async { ChatModel.shared.replaceChat(contactRequest.id, chat) } + await MainActor.run { + ChatModel.shared.replaceChat(contactRequest.id, chat) + NetworkModel.shared.setContactNetworkStatus(contact, .connected) + } + if contact.sndReady { + DispatchQueue.main.async { + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(chat.id) + } + } + } } } @@ -1009,10 +1519,16 @@ func apiEndCall(_ contact: Contact) async throws { try await sendCommandOkResp(.apiEndCall(contact: contact)) } -func apiGetCallInvitations() throws -> [RcvCallInvitation] { - let r = chatSendCmdSync(.apiGetCallInvitations) +func apiGetCallInvitationsSync() throws -> [RcvCallInvitation] { + let r: ChatResponse2 = try chatSendCmdSync(.apiGetCallInvitations) if case let .callInvitations(invs) = r { return invs } - throw r + throw r.unexpected +} + +func apiGetCallInvitations() async throws -> [RcvCallInvitation] { + let r: ChatResponse2 = try await chatSendCmd(.apiGetCallInvitations) + if case let .callInvitations(invs) = r { return invs } + throw r.unexpected } func apiCallStatus(_ contact: Contact, _ status: String) async throws { @@ -1024,20 +1540,18 @@ func apiCallStatus(_ contact: Contact, _ status: String) async throws { } func apiGetNetworkStatuses() throws -> [ConnNetworkStatus] { - let r = chatSendCmdSync(.apiGetNetworkStatuses) + let r: ChatResponse1 = try chatSendCmdSync(.apiGetNetworkStatuses) if case let .networkStatuses(_, statuses) = r { return statuses } - throw r + throw r.unexpected } -func markChatRead(_ chat: Chat, aboveItem: ChatItem? = nil) async { +func markChatRead(_ chat: Chat) async { do { if chat.chatStats.unreadCount > 0 { - let minItemId = chat.chatStats.minUnreadItemId - let itemRange = (minItemId, aboveItem?.id ?? chat.chatItems.last?.id ?? minItemId) let cInfo = chat.chatInfo - try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: itemRange) + try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId) await MainActor.run { - withAnimation { ChatModel.shared.markChatItemsRead(cInfo, aboveItem: aboveItem) } + withAnimation { ChatModel.shared.markAllChatItemsRead(cInfo) } } } if chat.chatStats.unreadChat { @@ -1060,39 +1574,40 @@ func markChatUnread(_ chat: Chat, unreadChat: Bool = true) async { } } -func apiMarkChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async { +func apiMarkChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID], mentionsRead: Int) async { do { - logger.debug("apiMarkChatItemRead: \(cItem.id)") - try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: (cItem.id, cItem.id)) - await MainActor.run { ChatModel.shared.markChatItemRead(cInfo, cItem) } + try await apiChatItemsRead(type: cInfo.chatType, id: cInfo.apiId, itemIds: itemIds) + DispatchQueue.main.async { + ChatModel.shared.markChatItemsRead(cInfo, itemIds, mentionsRead) + } } catch { - logger.error("apiMarkChatItemRead apiChatRead error: \(responseError(error))") + logger.error("apiChatItemsRead error: \(responseError(error))") } } -private func sendCommandOkResp(_ cmd: ChatCommand) async throws { - let r = await chatSendCmd(cmd) +private func sendCommandOkResp(_ cmd: ChatCommand, ctrl: chat_ctrl? = nil) async throws { + let r: ChatResponse2 = try await chatSendCmd(cmd, ctrl: ctrl) if case .cmdOk = r { return } - throw r + throw r.unexpected } private func sendCommandOkRespSync(_ cmd: ChatCommand) throws { - let r = chatSendCmdSync(cmd) + let r: ChatResponse2 = try chatSendCmdSync(cmd) if case .cmdOk = r { return } - throw r + throw r.unexpected } func apiNewGroup(incognito: Bool, groupProfile: GroupProfile) throws -> GroupInfo { let userId = try currentUserId("apiNewGroup") - let r = chatSendCmdSync(.apiNewGroup(userId: userId, incognito: incognito, groupProfile: groupProfile)) + let r: ChatResponse2 = try chatSendCmdSync(.apiNewGroup(userId: userId, incognito: incognito, groupProfile: groupProfile)) if case let .groupCreated(_, groupInfo) = r { return groupInfo } - throw r + throw r.unexpected } func apiAddMember(_ groupId: Int64, _ contactId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember { - let r = await chatSendCmd(.apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole)) + let r: ChatResponse2 = try await chatSendCmd(.apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole)) if case let .sentGroupInvitation(_, _, _, member) = r { return member } - throw r + throw r.unexpected } enum JoinGroupResult { @@ -1102,25 +1617,31 @@ enum JoinGroupResult { } func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult { - let r = await chatSendCmd(.apiJoinGroup(groupId: groupId)) + let r: APIResult = await chatApiSendCmd(.apiJoinGroup(groupId: groupId)) switch r { - case let .userAcceptedGroupSent(_, groupInfo, _): return .joined(groupInfo: groupInfo) - case .chatCmdError(_, .errorAgent(.SMP(.AUTH))): return .invitationRemoved - case .chatCmdError(_, .errorStore(.groupNotFound)): return .groupNotFound - default: throw r + case let .result(.userAcceptedGroupSent(_, groupInfo, _)): return .joined(groupInfo: groupInfo) + case .error(.errorAgent(.SMP(_, .AUTH))): return .invitationRemoved + case .error(.errorStore(.groupNotFound)): return .groupNotFound + default: throw r.unexpected } } -func apiRemoveMember(_ groupId: Int64, _ memberId: Int64) async throws -> GroupMember { - let r = await chatSendCmd(.apiRemoveMember(groupId: groupId, memberId: memberId), bgTask: false) - if case let .userDeletedMember(_, _, member) = r { return member } - throw r +func apiRemoveMembers(_ groupId: Int64, _ memberIds: [Int64], _ withMessages: Bool = false) async throws -> [GroupMember] { + let r: ChatResponse2 = try await chatSendCmd(.apiRemoveMembers(groupId: groupId, memberIds: memberIds, withMessages: withMessages), bgTask: false) + if case let .userDeletedMembers(_, _, members, withMessages) = r { return members } + throw r.unexpected } -func apiMemberRole(_ groupId: Int64, _ memberId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember { - let r = await chatSendCmd(.apiMemberRole(groupId: groupId, memberId: memberId, memberRole: memberRole), bgTask: false) - if case let .memberRoleUser(_, _, member, _, _) = r { return member } - throw r +func apiMembersRole(_ groupId: Int64, _ memberIds: [Int64], _ memberRole: GroupMemberRole) async throws -> [GroupMember] { + let r: ChatResponse2 = try await chatSendCmd(.apiMembersRole(groupId: groupId, memberIds: memberIds, memberRole: memberRole), bgTask: false) + if case let .membersRoleUser(_, _, members, _) = r { return members } + throw r.unexpected +} + +func apiBlockMembersForAll(_ groupId: Int64, _ memberIds: [Int64], _ blocked: Bool) async throws -> [GroupMember] { + let r: ChatResponse2 = try await chatSendCmd(.apiBlockMembersForAll(groupId: groupId, memberIds: memberIds, blocked: blocked), bgTask: false) + if case let .membersBlockedForAllUser(_, _, members, _) = r { return members } + throw r.unexpected } func leaveGroup(_ groupId: Int64) async { @@ -1133,14 +1654,15 @@ func leaveGroup(_ groupId: Int64) async { } func apiLeaveGroup(_ groupId: Int64) async throws -> GroupInfo { - let r = await chatSendCmd(.apiLeaveGroup(groupId: groupId), bgTask: false) + let r: ChatResponse2 = try await chatSendCmd(.apiLeaveGroup(groupId: groupId), bgTask: false) if case let .leftMemberUser(_, groupInfo) = r { return groupInfo } - throw r + throw r.unexpected } +// use ChatModel's loadGroupMembers from views func apiListMembers(_ groupId: Int64) async -> [GroupMember] { - let r = await chatSendCmd(.apiListMembers(groupId: groupId)) - if case let .groupMembers(_, group) = r { return group.members } + let r: APIResult = await chatApiSendCmd(.apiListMembers(groupId: groupId)) + if case let .result(.groupMembers(_, group)) = r { return group.members } return [] } @@ -1148,61 +1670,82 @@ func filterMembersToAdd(_ ms: [GMember]) -> [Contact] { let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil } return ChatModel.shared.chats .compactMap{ $0.chatInfo.contact } - .filter{ !memberContactIds.contains($0.apiId) } + .filter{ c in c.sendMsgEnabled && !c.nextSendGrpInv && !memberContactIds.contains(c.apiId) } .sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() } } func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws -> GroupInfo { - let r = await chatSendCmd(.apiUpdateGroupProfile(groupId: groupId, groupProfile: groupProfile)) + let r: ChatResponse2 = try await chatSendCmd(.apiUpdateGroupProfile(groupId: groupId, groupProfile: groupProfile)) if case let .groupUpdated(_, toGroup) = r { return toGroup } - throw r + throw r.unexpected } -func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (String, GroupMemberRole) { - let r = await chatSendCmd(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole)) - if case let .groupLinkCreated(_, _, connReq, memberRole) = r { return (connReq, memberRole) } - throw r +func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (CreatedConnLink, GroupMemberRole) { + let short = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS) + let r: ChatResponse2 = try await chatSendCmd(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole, short: short)) + if case let .groupLinkCreated(_, _, connLink, memberRole) = r { return (connLink, memberRole) } + throw r.unexpected } -func apiGroupLinkMemberRole(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (String, GroupMemberRole) { - let r = await chatSendCmd(.apiGroupLinkMemberRole(groupId: groupId, memberRole: memberRole)) - if case let .groupLink(_, _, connReq, memberRole) = r { return (connReq, memberRole) } - throw r +func apiGroupLinkMemberRole(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (CreatedConnLink, GroupMemberRole) { + let r: ChatResponse2 = try await chatSendCmd(.apiGroupLinkMemberRole(groupId: groupId, memberRole: memberRole)) + if case let .groupLink(_, _, connLink, memberRole) = r { return (connLink, memberRole) } + throw r.unexpected } func apiDeleteGroupLink(_ groupId: Int64) async throws { - let r = await chatSendCmd(.apiDeleteGroupLink(groupId: groupId)) + let r: ChatResponse2 = try await chatSendCmd(.apiDeleteGroupLink(groupId: groupId)) if case .groupLinkDeleted = r { return } - throw r + throw r.unexpected } -func apiGetGroupLink(_ groupId: Int64) throws -> (String, GroupMemberRole)? { - let r = chatSendCmdSync(.apiGetGroupLink(groupId: groupId)) +func apiGetGroupLink(_ groupId: Int64) throws -> (CreatedConnLink, GroupMemberRole)? { + let r: APIResult = chatApiSendCmdSync(.apiGetGroupLink(groupId: groupId)) switch r { - case let .groupLink(_, _, connReq, memberRole): - return (connReq, memberRole) - case .chatCmdError(_, chatError: .errorStore(storeError: .groupLinkNotFound)): + case let .result(.groupLink(_, _, connLink, memberRole)): + return (connLink, memberRole) + case .error(.errorStore(storeError: .groupLinkNotFound)): return nil - default: throw r + default: throw r.unexpected } } func apiCreateMemberContact(_ groupId: Int64, _ groupMemberId: Int64) async throws -> Contact { - let r = await chatSendCmd(.apiCreateMemberContact(groupId: groupId, groupMemberId: groupMemberId)) + let r: ChatResponse2 = try await chatSendCmd(.apiCreateMemberContact(groupId: groupId, groupMemberId: groupMemberId)) if case let .newMemberContact(_, contact, _, _) = r { return contact } - throw r + throw r.unexpected } func apiSendMemberContactInvitation(_ contactId: Int64, _ msg: MsgContent) async throws -> Contact { - let r = await chatSendCmd(.apiSendMemberContactInvitation(contactId: contactId, msg: msg), bgDelay: msgDelay) + let r: ChatResponse2 = try await chatSendCmd(.apiSendMemberContactInvitation(contactId: contactId, msg: msg), bgDelay: msgDelay) if case let .newMemberContactSentInv(_, contact, _, _) = r { return contact } - throw r + throw r.unexpected } func apiGetVersion() throws -> CoreVersionInfo { - let r = chatSendCmdSync(.showVersion) + let r: ChatResponse2 = try chatSendCmdSync(.showVersion) if case let .versionInfo(info, _, _) = r { return info } - throw r + throw r.unexpected +} + +func getAgentSubsTotal() async throws -> (SMPServerSubs, Bool) { + let userId = try currentUserId("getAgentSubsTotal") + let r: ChatResponse2 = try await chatSendCmd(.getAgentSubsTotal(userId: userId), log: false) + if case let .agentSubsTotal(_, subsTotal, hasSession) = r { return (subsTotal, hasSession) } + logger.error("getAgentSubsTotal error: \(String(describing: r))") + throw r.unexpected +} + +func getAgentServersSummary() throws -> PresentedServersSummary { + let userId = try currentUserId("getAgentServersSummary") + let r: ChatResponse2 = try chatSendCmdSync(.getAgentServersSummary(userId: userId), log: false) + if case let .agentServersSummary(_, serversSummary) = r { return serversSummary } + logger.error("getAgentServersSummary error: \(String(describing: r))") + throw r.unexpected +} + +func resetAgentServersStats() async throws { + try await sendCommandOkResp(.resetAgentServersStats) } private func currentUserId(_ funcName: String) throws -> Int64 { @@ -1212,26 +1755,67 @@ private func currentUserId(_ funcName: String) throws -> Int64 { throw RuntimeError("\(funcName): no current user") } -func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool = true, confirmMigrations: MigrationConfirmation? = nil) throws { +func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = nil, refreshInvitations: Bool = true, confirmMigrations: MigrationConfirmation? = nil) throws { logger.debug("initializeChat") let m = ChatModel.shared + m.ctrlInitInProgress = true + defer { m.ctrlInitInProgress = false } (m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey, confirmMigrations: confirmMigrations) if m.chatDbStatus != .ok { return } + NetworkObserver.shared.restartMonitor() // If we migrated successfully means previous re-encryption process on database level finished successfully too if encryptionStartedDefault.get() { encryptionStartedDefault.set(false) } - try apiSetTempFolder(tempFolder: getTempFilesDirectory().path) - try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) - try setXFTPConfig(getXFTPCfg()) + try apiSetAppFilePaths(filesFolder: getAppFilesDirectory().path, tempFolder: getTempFilesDirectory().path, assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path) try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get()) m.chatInitialized = true m.currentUser = try apiGetActiveUser() + m.conditions = try getServerOperatorsSync() + if shouldImportAppSettingsDefault.get() { + do { + let appSettings = try apiGetAppSettings(settings: AppSettings.current.prepareForExport()) + appSettings.importIntoApp() + shouldImportAppSettingsDefault.set(false) + } catch { + logger.error("Error while importing app settings: \(error)") + } + } if m.currentUser == nil { onboardingStageDefault.set(.step1_SimpleXInfo) privacyDeliveryReceiptsSet.set(true) m.onboardingStage = .step1_SimpleXInfo - } else if start { + } else if confirmStart { + showStartChatAfterRestartAlert { start in + do { + if start { AppChatState.shared.set(.active) } + try chatInitialized(start: start, refreshInvitations: refreshInvitations) + } catch let error { + logger.error("ChatInitialized error: \(error)") + } + } + } else { + try chatInitialized(start: start, refreshInvitations: refreshInvitations) + } +} + +func showStartChatAfterRestartAlert(result: @escaping (_ start: Bool) -> Void) { + AlertManager.shared.showAlert(Alert( + title: Text("Start chat?"), + message: Text("Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat."), + primaryButton: .default(Text("Ok")) { + result(true) + }, + secondaryButton: .cancel { + result(false) + } + )) +} + +private func chatInitialized(start: Bool, refreshInvitations: Bool) throws { + let m = ChatModel.shared + if m.currentUser == nil { return } + if start { try startChat(refreshInvitations: refreshInvitations) } else { m.chatRunning = false @@ -1241,31 +1825,34 @@ func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool } } -func startChat(refreshInvitations: Bool = true) throws { +func startChat(refreshInvitations: Bool = true, onboarding: Bool = false) throws { logger.debug("startChat") let m = ChatModel.shared try setNetworkConfig(getNetCfg()) - let justStarted = try apiStartChat() + let chatRunning = try apiCheckChatRunning() m.users = try listUsers() - if justStarted { + if !chatRunning { try getUserChatData() NtfManager.shared.setNtfBadgeCount(m.totalUnreadCountForAllUsers()) if (refreshInvitations) { - try refreshCallInvitations() + Task { try await refreshCallInvitations() } } - (m.savedToken, m.tokenStatus, m.notificationMode) = apiGetNtfToken() + (m.savedToken, m.tokenStatus, m.notificationMode, m.notificationServer) = apiGetNtfToken() + _ = try apiStartChat() // deviceToken is set when AppDelegate.application(didRegisterForRemoteNotificationsWithDeviceToken:) is called, // when it is called before startChat if let token = m.deviceToken { registerToken(token: token) } - withAnimation { - let savedOnboardingStage = onboardingStageDefault.get() - m.onboardingStage = [.step1_SimpleXInfo, .step2_CreateProfile].contains(savedOnboardingStage) && m.users.count == 1 - ? .step3_CreateSimpleXAddress - : savedOnboardingStage - if m.onboardingStage == .onboardingComplete && !privacyDeliveryReceiptsSet.get() { - m.setDeliveryReceipts = true + if !onboarding { + withAnimation { + let savedOnboardingStage = onboardingStageDefault.get() + m.onboardingStage = [.step1_SimpleXInfo, .step2_CreateProfile].contains(savedOnboardingStage) && m.users.count == 1 + ? .step3_ChooseServerOperators + : savedOnboardingStage + if m.onboardingStage == .onboardingComplete && !privacyDeliveryReceiptsSet.get() { + m.setDeliveryReceipts = true + } } } } @@ -1274,6 +1861,15 @@ func startChat(refreshInvitations: Bool = true) throws { chatLastStartGroupDefault.set(Date.now) } +func startChatWithTemporaryDatabase(ctrl: chat_ctrl) throws -> User? { + logger.debug("startChatWithTemporaryDatabase") + let migrationActiveUser = try? apiGetActiveUser(ctrl: ctrl) ?? apiCreateActiveUser(Profile(displayName: "Temp", fullName: ""), ctrl: ctrl) + try setNetworkConfig(getNetCfg(), ctrl: ctrl) + try apiSetAppFilePaths(filesFolder: getMigrationTempFilesDirectory().path, tempFolder: getMigrationTempFilesDirectory().path, assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path, ctrl: ctrl) + _ = try apiStartChat(ctrl: ctrl) + return migrationActiveUser +} + func changeActiveUser(_ userId: Int64, viewPwd: String?) { do { try changeActiveUser_(userId, viewPwd: viewPwd) @@ -1289,8 +1885,12 @@ private func changeActiveUser_(_ userId: Int64, viewPwd: String?) throws { try getUserChatData() } -func changeActiveUserAsync_(_ userId: Int64, viewPwd: String?) async throws { - let currentUser = try await apiSetActiveUserAsync(userId, viewPwd: viewPwd) +func changeActiveUserAsync_(_ userId: Int64?, viewPwd: String?) async throws { + let currentUser = if let userId = userId { + try await apiSetActiveUserAsync(userId, viewPwd: viewPwd) + } else { + try apiGetActiveUser() + } let users = try await listUsersAsync() await MainActor.run { let m = ChatModel.shared @@ -1299,7 +1899,7 @@ func changeActiveUserAsync_(_ userId: Int64, viewPwd: String?) async throws { } try await getUserChatDataAsync() await MainActor.run { - if var (_, invitation) = ChatModel.shared.callInvitations.first(where: { _, inv in inv.user.userId == userId }) { + if let currentUser = currentUser, var (_, invitation) = ChatModel.shared.callInvitations.first(where: { _, inv in inv.user.userId == userId }) { invitation.user = currentUser activateCall(invitation) } @@ -1311,18 +1911,38 @@ func getUserChatData() throws { m.userAddress = try apiGetUserAddress() m.chatItemTTL = try getChatItemTTL() let chats = try apiGetChats() - m.chats = chats.map { Chat.init($0) } + let tags = try apiGetChatTags() + m.updateChats(chats) + let tm = ChatTagsModel.shared + tm.activeFilter = nil + tm.userTags = tags + tm.updateChatTags(m.chats) } private func getUserChatDataAsync() async throws { - let userAddress = try await apiGetUserAddressAsync() - let chatItemTTL = try await getChatItemTTLAsync() - let chats = try await apiGetChatsAsync() - await MainActor.run { - let m = ChatModel.shared - m.userAddress = userAddress - m.chatItemTTL = chatItemTTL - m.chats = chats.map { Chat.init($0) } + let m = ChatModel.shared + let tm = ChatTagsModel.shared + if m.currentUser != nil { + let userAddress = try await apiGetUserAddressAsync() + let chatItemTTL = try await getChatItemTTLAsync() + let chats = try await apiGetChatsAsync() + let tags = try await apiGetChatTagsAsync() + await MainActor.run { + m.userAddress = userAddress + m.chatItemTTL = chatItemTTL + m.updateChats(chats) + tm.activeFilter = nil + tm.userTags = tags + tm.updateChatTags(m.chats) + } + } else { + await MainActor.run { + m.userAddress = nil + m.updateChats([]) + tm.activeFilter = nil + tm.userTags = [] + tm.presetTags = [:] + } } } @@ -1331,6 +1951,8 @@ class ChatReceiver { private var receiveMessages = true private var _lastMsgTime = Date.now + var messagesChannel: ((APIResult) -> Void)? = nil + static let shared = ChatReceiver() var lastMsgTime: Date { get { _lastMsgTime } } @@ -1344,14 +1966,20 @@ class ChatReceiver { } func receiveMsgLoop() async { - // TODO use function that has timeout - if let msg = await chatRecvMsg() { - self._lastMsgTime = .now - await processReceivedMsg(msg) - } - if self.receiveMessages { + while self.receiveMessages { + if let msg = await chatRecvMsg() { + self._lastMsgTime = .now + Task { await TerminalItems.shared.addResult(msg) } + switch msg { + case let .result(evt): await processReceivedMsg(evt) + case let .error(err): logger.debug("chatRecvMsg error: \(responseError(err))") + case let .invalid(type, json): logger.debug("chatRecvMsg event: * \(type) \(dataToString(json))") + } + if let messagesChannel { + messagesChannel(msg) + } + } _ = try? await Task.sleep(nanoseconds: 7_500_000) - await receiveMsgLoop() } } @@ -1363,11 +1991,9 @@ class ChatReceiver { } } -func processReceivedMsg(_ res: ChatResponse) async { - Task { - await TerminalItems.shared.add(.resp(.now, res)) - } +func processReceivedMsg(_ res: ChatEvent) async { let m = ChatModel.shared + let n = NetworkModel.shared logger.debug("processReceivedMsg: \(res.responseType)") switch res { case let .contactDeletedByContact(user, contact): @@ -1390,7 +2016,7 @@ func processReceivedMsg(_ res: ChatResponse) async { NtfManager.shared.notifyContactConnected(user, contact) } await MainActor.run { - m.setContactNetworkStatus(contact, .connected) + n.setContactNetworkStatus(contact, .connected) } case let .contactConnecting(user, contact): if active(user) && contact.directOrUsed { @@ -1402,6 +2028,19 @@ func processReceivedMsg(_ res: ChatResponse) async { } } } + case let .contactSndReady(user, contact): + if active(user) && contact.directOrUsed { + await MainActor.run { + m.updateContact(contact) + if let conn = contact.activeConn { + m.dismissConnReqView(conn.id) + m.removeChat(conn.id) + } + } + } + await MainActor.run { + n.setContactNetworkStatus(contact, .connected) + } case let .receivedContactRequest(user, contactRequest): if active(user) { let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest) @@ -1434,71 +2073,79 @@ func processReceivedMsg(_ res: ChatResponse) async { if active(user) && m.hasChat(mergedContact.id) { await MainActor.run { if m.chatId == mergedContact.id { - m.chatId = intoContact.id + ItemsModel.shared.loadOpenChat(mergedContact.id) } m.removeChat(mergedContact.id) } } - case let .contactsSubscribed(_, contactRefs): - await updateContactsStatus(contactRefs, status: .connected) - case let .contactsDisconnected(_, contactRefs): - await updateContactsStatus(contactRefs, status: .disconnected) - case let .contactSubSummary(_, contactSubscriptions): - await MainActor.run { - for sub in contactSubscriptions { -// no need to update contact here, and it is slow -// if active(user) { -// m.updateContact(sub.contact) -// } - if let err = sub.contactError { - processContactSubError(sub.contact, err) - } else { - m.setContactNetworkStatus(sub.contact, .connected) - } - } - } case let .networkStatus(status, connections): - await MainActor.run { + // dispatch queue to synchronize access + networkStatusesLock.sync { + var ns = n.networkStatuses + // slow loop is on the background thread for cId in connections { - m.networkStatuses[cId] = status + ns[cId] = status + } + // fast model update is on the main thread + DispatchQueue.main.sync { + n.networkStatuses = ns } } case let .networkStatuses(_, statuses): () - await MainActor.run { + // dispatch queue to synchronize access + networkStatusesLock.sync { + var ns = n.networkStatuses + // slow loop is on the background thread for s in statuses { - m.networkStatuses[s.agentConnId] = s.networkStatus + ns[s.agentConnId] = s.networkStatus + } + // fast model update is on the main thread + DispatchQueue.main.sync { + n.networkStatuses = ns } } - case let .newChatItem(user, aChatItem): - let cInfo = aChatItem.chatInfo - let cItem = aChatItem.chatItem - await MainActor.run { - if active(user) { - m.addChatItem(cInfo, cItem) - } else if cItem.isRcvNew && cInfo.ntfsEnabled { - m.increaseUnreadCounter(user: user) + case let .newChatItems(user, chatItems): + for chatItem in chatItems { + let cInfo = chatItem.chatInfo + let cItem = chatItem.chatItem + await MainActor.run { + if active(user) { + m.addChatItem(cInfo, cItem) + if cItem.isActiveReport { + m.increaseGroupReportsCounter(cInfo.id) + } + } else if cItem.isRcvNew && cInfo.ntfsEnabled(chatItem: cItem) { + m.increaseUnreadCounter(user: user) + } + } + if let file = cItem.autoReceiveFile() { + Task { + await receiveFile(user: user, fileId: file.fileId, auto: true) + } + } + if cItem.showNotification { + NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) } } - if let file = cItem.autoReceiveFile() { - Task { - await receiveFile(user: user, fileId: file.fileId, encrypted: cItem.encryptLocalFile, auto: true) + case let .chatItemsStatusesUpdated(user, chatItems): + for chatItem in chatItems { + let cInfo = chatItem.chatInfo + let cItem = chatItem.chatItem + if !cItem.isDeletedContent && active(user) { + await MainActor.run { m.updateChatItem(cInfo, cItem, status: cItem.meta.itemStatus) } } - } - if cItem.showNotification { - NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) - } - case let .chatItemStatusUpdated(user, aChatItem): - let cInfo = aChatItem.chatInfo - let cItem = aChatItem.chatItem - if !cItem.isDeletedContent && active(user) { - await MainActor.run { m.updateChatItem(cInfo, cItem, status: cItem.meta.itemStatus) } - } - if let endTask = m.messageDelivery[cItem.id] { - switch cItem.meta.itemStatus { - case .sndSent: endTask() - case .sndErrorAuth: endTask() - case .sndError: endTask() - default: () + if let endTask = m.messageDelivery[cItem.id] { + switch cItem.meta.itemStatus { + case .sndNew: () + case .sndSent: endTask() + case .sndRcvd: endTask() + case .sndErrorAuth: endTask() + case .sndError: endTask() + case .sndWarning: endTask() + case .rcvNew: () + case .rcvRead: () + case .invalid: () + } } } case let .chatItemUpdated(user, aChatItem): @@ -1509,23 +2156,33 @@ func processReceivedMsg(_ res: ChatResponse) async { m.updateChatItem(r.chatInfo, r.chatReaction.chatItem) } } - case let .chatItemDeleted(user, deletedChatItem, toChatItem, _): + case let .chatItemsDeleted(user, items, _): if !active(user) { - if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled { - await MainActor.run { - m.decreaseUnreadCounter(user: user) + for item in items { + let d = item.deletedChatItem + if item.toChatItem == nil && d.chatItem.isRcvNew && d.chatInfo.ntfsEnabled(chatItem: d.chatItem) { + await MainActor.run { + m.decreaseUnreadCounter(user: user) + } } } return } await MainActor.run { - if let toChatItem = toChatItem { - _ = m.upsertChatItem(toChatItem.chatInfo, toChatItem.chatItem) - } else { - m.removeChatItem(deletedChatItem.chatInfo, deletedChatItem.chatItem) + for item in items { + if let toChatItem = item.toChatItem { + _ = m.upsertChatItem(toChatItem.chatInfo, toChatItem.chatItem) + } else { + m.removeChatItem(item.deletedChatItem.chatInfo, item.deletedChatItem.chatItem) + } + if item.deletedChatItem.chatItem.isActiveReport { + m.decreaseGroupReportsCounter(item.deletedChatItem.chatInfo.id) + } } } + case let .groupChatItemsDeleted(user, groupInfo, chatItemIDs, _, member_): + await groupChatItemsDeleted(user, groupInfo, chatItemIDs, member_) case let .receivedGroupInvitation(user, groupInfo, _, _): if active(user) { await MainActor.run { @@ -1545,7 +2202,7 @@ func processReceivedMsg(_ res: ChatResponse) async { } case let .groupLinkConnecting(user, groupInfo, hostMember): if !active(user) { return } - + await MainActor.run { m.updateGroup(groupInfo) if let hostConn = hostMember.activeConn { @@ -1553,22 +2210,40 @@ func processReceivedMsg(_ res: ChatResponse) async { m.removeChat(hostConn.id) } } + case let .businessLinkConnecting(user, groupInfo, _, fromContact): + if !active(user) { return } + + await MainActor.run { + m.updateGroup(groupInfo) + } + if m.chatId == fromContact.id { + ItemsModel.shared.loadOpenChat(groupInfo.id) + } + await MainActor.run { + m.removeChat(fromContact.id) + } case let .joinedGroupMemberConnecting(user, groupInfo, _, member): if active(user) { await MainActor.run { _ = m.upsertGroupMember(groupInfo, member) } } - case let .deletedMemberUser(user, groupInfo, _): // TODO update user member + case let .deletedMemberUser(user, groupInfo, member, withMessages): // TODO update user member if active(user) { await MainActor.run { m.updateGroup(groupInfo) + if withMessages { + m.removeMemberItems(groupInfo.membership, byMember: member, groupInfo) + } } } - case let .deletedMember(user, groupInfo, _, deletedMember): + case let .deletedMember(user, groupInfo, byMember, deletedMember, withMessages): if active(user) { await MainActor.run { _ = m.upsertGroupMember(groupInfo, deletedMember) + if withMessages { + m.removeMemberItems(deletedMember, byMember: byMember, groupInfo) + } } } case let .leftMember(user, groupInfo, member): @@ -1603,7 +2278,7 @@ func processReceivedMsg(_ res: ChatResponse) async { } if let contact = memberContact { await MainActor.run { - m.setContactNetworkStatus(contact, .connected) + n.setContactNetworkStatus(contact, .connected) } } case let .groupUpdated(user, toGroup): @@ -1619,6 +2294,13 @@ func processReceivedMsg(_ res: ChatResponse) async { _ = m.upsertGroupMember(groupInfo, member) } } + case let .memberBlockedForAll(user, groupInfo, byMember: _, member: member, blocked: _): + if active(user) { + await MainActor.run { + m.updateGroup(groupInfo) + _ = m.upsertGroupMember(groupInfo, member) + } + } case let .newMemberContactReceivedInv(user, contact, _, _): if active(user) { await MainActor.run { @@ -1627,6 +2309,10 @@ func processReceivedMsg(_ res: ChatResponse) async { } case let .rcvFileAccepted(user, aChatItem): // usually rcvFileAccepted is a response, but it's also an event for XFTP files auto-accepted from NSE await chatItemSimpleUpdate(user, aChatItem) +// TODO when aChatItem added +// case let .rcvFileAcceptedSndCancelled(user, aChatItem, _): // usually rcvFileAcceptedSndCancelled is a response, but it's also an event for XFTP files auto-accepted from NSE +// await chatItemSimpleUpdate(user, aChatItem) +// Task { cleanupFile(aChatItem) } case let .rcvFileStart(user, aChatItem): await chatItemSimpleUpdate(user, aChatItem) case let .rcvFileComplete(user, aChatItem): @@ -1634,27 +2320,44 @@ func processReceivedMsg(_ res: ChatResponse) async { case let .rcvFileSndCancelled(user, aChatItem, _): await chatItemSimpleUpdate(user, aChatItem) Task { cleanupFile(aChatItem) } - case let .rcvFileProgressXFTP(user, aChatItem, _, _): - await chatItemSimpleUpdate(user, aChatItem) - case let .rcvFileError(user, aChatItem): - await chatItemSimpleUpdate(user, aChatItem) - Task { cleanupFile(aChatItem) } + case let .rcvFileProgressXFTP(user, aChatItem, _, _, _): + if let aChatItem = aChatItem { + await chatItemSimpleUpdate(user, aChatItem) + } + case let .rcvFileError(user, aChatItem, _, _): + if let aChatItem = aChatItem { + await chatItemSimpleUpdate(user, aChatItem) + Task { cleanupFile(aChatItem) } + } + case let .rcvFileWarning(user, aChatItem, _, _): + if let aChatItem = aChatItem { + await chatItemSimpleUpdate(user, aChatItem) + } case let .sndFileStart(user, aChatItem, _): await chatItemSimpleUpdate(user, aChatItem) case let .sndFileComplete(user, aChatItem, _): await chatItemSimpleUpdate(user, aChatItem) Task { cleanupDirectFile(aChatItem) } case let .sndFileRcvCancelled(user, aChatItem, _): - await chatItemSimpleUpdate(user, aChatItem) - Task { cleanupDirectFile(aChatItem) } + if let aChatItem = aChatItem { + await chatItemSimpleUpdate(user, aChatItem) + Task { cleanupDirectFile(aChatItem) } + } case let .sndFileProgressXFTP(user, aChatItem, _, _, _): - await chatItemSimpleUpdate(user, aChatItem) + if let aChatItem = aChatItem { + await chatItemSimpleUpdate(user, aChatItem) + } case let .sndFileCompleteXFTP(user, aChatItem, _): await chatItemSimpleUpdate(user, aChatItem) - Task { cleanupFile(aChatItem) } - case let .sndFileError(user, aChatItem): - await chatItemSimpleUpdate(user, aChatItem) - Task { cleanupFile(aChatItem) } + case let .sndFileError(user, aChatItem, _, _): + if let aChatItem = aChatItem { + await chatItemSimpleUpdate(user, aChatItem) + Task { cleanupFile(aChatItem) } + } + case let .sndFileWarning(user, aChatItem, _, _): + if let aChatItem = aChatItem { + await chatItemSimpleUpdate(user, aChatItem) + } case let .callInvitation(invitation): await MainActor.run { m.callInvitations[invitation.contact.id] = invitation @@ -1664,7 +2367,6 @@ func processReceivedMsg(_ res: ChatResponse) async { await withCall(contact) { call in await MainActor.run { call.callState = .offerReceived - call.peerMedia = callType.media call.sharedKey = sharedKey } let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY) @@ -1700,21 +2402,35 @@ func processReceivedMsg(_ res: ChatResponse) async { } case .chatSuspended: chatSuspended() - case let .contactSwitch(_, contact, switchProgress): - await MainActor.run { - m.updateContactConnectionStats(contact, switchProgress.connectionStats) + case let .contactSwitch(user, contact, switchProgress): + if active(user) { + await MainActor.run { + m.updateContactConnectionStats(contact, switchProgress.connectionStats) + } } - case let .groupMemberSwitch(_, groupInfo, member, switchProgress): - await MainActor.run { - m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats) + case let .groupMemberSwitch(user, groupInfo, member, switchProgress): + if active(user) { + await MainActor.run { + m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats) + } } - case let .contactRatchetSync(_, contact, ratchetSyncProgress): - await MainActor.run { - m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats) + case let .contactRatchetSync(user, contact, ratchetSyncProgress): + if active(user) { + await MainActor.run { + m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats) + } } - case let .groupMemberRatchetSync(_, groupInfo, member, ratchetSyncProgress): - await MainActor.run { - m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats) + case let .groupMemberRatchetSync(user, groupInfo, member, ratchetSyncProgress): + if active(user) { + await MainActor.run { + m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats) + } + } + case let .contactDisabled(user, contact): + if active(user) { + await MainActor.run { + m.updateContact(contact) + } } case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible): await MainActor.run { @@ -1738,12 +2454,30 @@ func processReceivedMsg(_ res: ChatResponse) async { let state = UIRemoteCtrlSessionState.connected(remoteCtrl: remoteCtrl, sessionCode: m.remoteCtrlSession?.sessionCode ?? "") m.remoteCtrlSession = m.remoteCtrlSession?.updateState(state) } - case .remoteCtrlStopped: + case let .remoteCtrlStopped(_, rcStopReason): // This delay is needed to cancel the session that fails on network failure, // e.g. when user did not grant permission to access local network yet. if let sess = m.remoteCtrlSession { await MainActor.run { m.remoteCtrlSession = nil + dismissAllSheets() { + switch rcStopReason { + case .disconnected: + () + case .connectionFailed(.errorAgent(.RCP(.identity))): + AlertManager.shared.showAlertMsg( + title: "Connection with desktop stopped", + message: "This link was used with another mobile device, please create a new link on the desktop." + ) + default: + AlertManager.shared.showAlert(Alert( + title: Text("Connection with desktop stopped"), + message: Text("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."), + primaryButton: .default(Text("Ok")), + secondaryButton: .default(Text("Copy error")) { UIPasteboard.general.string = String(describing: rcStopReason) } + )) + } + } } if case .connected = sess.sessionState { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { @@ -1751,6 +2485,12 @@ func processReceivedMsg(_ res: ChatResponse) async { } } } + case let .contactPQEnabled(user, contact, _): + if active(user) { + await MainActor.run { + m.updateContact(contact) + } + } default: logger.debug("unsupported event: \(res.responseType)") } @@ -1766,12 +2506,13 @@ func processReceivedMsg(_ res: ChatResponse) async { func switchToLocalSession() { let m = ChatModel.shared + let n = NetworkModel.shared m.remoteCtrlSession = nil do { m.users = try listUsers() try getUserChatData() let statuses = (try apiGetNetworkStatuses()).map { s in (s.agentConnId, s.networkStatus) } - m.networkStatuses = Dictionary(uniqueKeysWithValues: statuses) + n.networkStatuses = Dictionary(uniqueKeysWithValues: statuses) } catch let error { logger.debug("error updating chat data: \(responseError(error))") } @@ -1787,57 +2528,86 @@ func chatItemSimpleUpdate(_ user: any UserLike, _ aChatItem: AChatItem) async { let cItem = aChatItem.chatItem if active(user) { if await MainActor.run(body: { m.upsertChatItem(cInfo, cItem) }) { - NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) + if cItem.showNotification { + NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) + } } } } -func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) async { +func groupChatItemsDeleted(_ user: UserRef, _ groupInfo: GroupInfo, _ chatItemIDs: Set, _ member_: GroupMember?) async { let m = ChatModel.shared + if !active(user) { + do { + let users = try listUsers() + await MainActor.run { + m.users = users + } + } catch { + logger.error("Error loading users: \(error)") + } + return + } + let im = ItemsModel.shared + let cInfo = ChatInfo.group(groupInfo: groupInfo) await MainActor.run { - for c in contactRefs { - m.networkStatuses[c.agentConnId] = status + m.decreaseGroupReportsCounter(cInfo.id, by: chatItemIDs.count) + } + var notFound = chatItemIDs.count + for ci in im.reversedChatItems { + if chatItemIDs.contains(ci.id) { + let deleted = if case let .groupRcv(groupMember) = ci.chatDir, let member_, groupMember.groupMemberId != member_.groupMemberId { + CIDeleted.moderated(deletedTs: Date.now, byGroupMember: member_) + } else { + CIDeleted.deleted(deletedTs: Date.now) + } + await MainActor.run { + var newItem = ci + newItem.meta.itemDeleted = deleted + _ = m.upsertChatItem(cInfo, newItem) + } + notFound -= 1 + if notFound == 0 { break } } } } -func processContactSubError(_ contact: Contact, _ chatError: ChatError) { +func refreshCallInvitations() async throws { let m = ChatModel.shared - var err: String - switch chatError { - case .errorAgent(agentError: .BROKER(_, .NETWORK)): err = "network" - case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted" - default: err = String(describing: chatError) - } - m.setContactNetworkStatus(contact, .error(connectionError: err)) -} - -func refreshCallInvitations() throws { - let m = ChatModel.shared - let callInvitations = try justRefreshCallInvitations() - if let (chatId, ntfAction) = m.ntfCallInvitationAction, - let invitation = m.callInvitations.removeValue(forKey: chatId) { - m.ntfCallInvitationAction = nil - CallController.shared.callAction(invitation: invitation, action: ntfAction) - } else if let invitation = callInvitations.last(where: { $0.user.showNotifications }) { - activateCall(invitation) + let callInvitations = try await apiGetCallInvitations() + await MainActor.run { + m.callInvitations = callsByChat(callInvitations) + if let (chatId, ntfAction) = m.ntfCallInvitationAction, + let invitation = m.callInvitations.removeValue(forKey: chatId) { + m.ntfCallInvitationAction = nil + CallController.shared.callAction(invitation: invitation, action: ntfAction) + } else if let invitation = callInvitations.last(where: { $0.user.showNotifications }) { + activateCall(invitation) + } } } -func justRefreshCallInvitations() throws -> [RcvCallInvitation] { - let m = ChatModel.shared - let callInvitations = try apiGetCallInvitations() - m.callInvitations = callInvitations.reduce(into: [ChatId: RcvCallInvitation]()) { result, inv in result[inv.contact.id] = inv } - return callInvitations +func justRefreshCallInvitations() async throws { + let callInvitations = try apiGetCallInvitationsSync() + await MainActor.run { + ChatModel.shared.callInvitations = callsByChat(callInvitations) + } +} + +private func callsByChat(_ callInvitations: [RcvCallInvitation]) -> [ChatId: RcvCallInvitation] { + callInvitations.reduce(into: [ChatId: RcvCallInvitation]()) { + result, inv in result[inv.contact.id] = inv + } } func activateCall(_ callInvitation: RcvCallInvitation) { - if !callInvitation.user.showNotifications { return } let m = ChatModel.shared + logger.debug("reportNewIncomingCall activeCallUUID \(String(describing: m.activeCall?.callUUID)) invitationUUID \(String(describing: callInvitation.callUUID))") + if !callInvitation.user.showNotifications || m.activeCall?.callUUID == callInvitation.callUUID { return } CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in if let error = error { DispatchQueue.main.async { - m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil + m.callInvitations[callInvitation.contact.id]?.callUUID = nil } logger.error("reportNewIncomingCall error: \(error.localizedDescription)") } else { diff --git a/apps/ios/Shared/Model/SuspendChat.swift b/apps/ios/Shared/Model/SuspendChat.swift index 9b03f38f3c..92bcdcac53 100644 --- a/apps/ios/Shared/Model/SuspendChat.swift +++ b/apps/ios/Shared/Model/SuspendChat.swift @@ -19,11 +19,13 @@ let terminationTimeout: Int = 3 // seconds let activationDelay: TimeInterval = 1.5 +let nseSuspendTimeout: TimeInterval = 5 + private func _suspendChat(timeout: Int) { // this is a redundant check to prevent logical errors, like the one fixed in this PR let state = AppChatState.shared.value if !state.canSuspend { - logger.error("_suspendChat called, current state: \(state.rawValue, privacy: .public)") + logger.error("_suspendChat called, current state: \(state.rawValue)") } else if ChatModel.ok { AppChatState.shared.set(.suspending) apiSuspendChat(timeoutMicroseconds: timeout * 1000000) @@ -34,6 +36,18 @@ private func _suspendChat(timeout: Int) { } } +let seSubscriber = seMessageSubscriber { + switch $0 { + case let .state(state): + switch state { + case .inactive: + if AppChatState.shared.value.inactive { activateChat() } + case .sendingMessage: + if AppChatState.shared.value.canSuspend { suspendChat() } + } + } +} + func suspendChat() { suspendLockQueue.sync { _suspendChat(timeout: appSuspendTimeout) @@ -105,26 +119,16 @@ func initChatAndMigrate(refreshInvitations: Bool = true) { let m = ChatModel.shared if (!m.chatInitialized) { m.v3DBMigration = v3DBMigrationDefault.get() - if AppChatState.shared.value == .stopped { - AlertManager.shared.showAlert(Alert( - title: Text("Start chat?"), - message: Text("Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat."), - primaryButton: .default(Text("Ok")) { - AppChatState.shared.set(.active) - initialize(start: true) - }, - secondaryButton: .cancel { - initialize(start: false) - } - )) + if AppChatState.shared.value == .stopped && storeDBPassphraseGroupDefault.get() && kcDatabasePassword.get() != nil { + initialize(start: true, confirmStart: true) } else { initialize(start: true) } } - func initialize(start: Bool) { + func initialize(start: Bool, confirmStart: Bool = false) { do { - try initializeChat(start: m.v3DBMigration.startChat && start, refreshInvitations: refreshInvitations) + try initializeChat(start: m.v3DBMigration.startChat && start, confirmStart: m.v3DBMigration.startChat && confirmStart, refreshInvitations: refreshInvitations) } catch let error { AlertManager.shared.showAlertMsg( title: start ? "Error starting chat" : "Error opening chat", @@ -134,20 +138,33 @@ func initChatAndMigrate(refreshInvitations: Bool = true) { } } -func startChatAndActivate(dispatchQueue: DispatchQueue = DispatchQueue.main, _ completion: @escaping () -> Void) { +func startChatForCall() { + logger.debug("DEBUGGING: startChatForCall") + if ChatModel.shared.chatRunning == true { + ChatReceiver.shared.start() + logger.debug("DEBUGGING: startChatForCall: after ChatReceiver.shared.start") + } + if .active != AppChatState.shared.value { + logger.debug("DEBUGGING: startChatForCall: before activateChat") + activateChat() + logger.debug("DEBUGGING: startChatForCall: after activateChat") + } +} + +func startChatAndActivate(_ completion: @escaping () -> Void) { logger.debug("DEBUGGING: startChatAndActivate") if ChatModel.shared.chatRunning == true { ChatReceiver.shared.start() logger.debug("DEBUGGING: startChatAndActivate: after ChatReceiver.shared.start") } - if .active == AppChatState.shared.value { + if case .active = AppChatState.shared.value { completion() } else if nseStateGroupDefault.get().inactive { activate() } else { // setting app state to "activating" to notify NSE that it should suspend setAppState(.activating) - waitNSESuspended(timeout: 10, dispatchQueue: dispatchQueue) { ok in + waitNSESuspended(timeout: nseSuspendTimeout) { ok in if !ok { // if for some reason NSE failed to suspend, // e.g., it crashed previously without setting its state to "suspended", diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index f72ffcaaaf..f8d69c5fc8 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -19,6 +19,7 @@ struct SimpleXApp: App { @Environment(\.scenePhase) var scenePhase @State private var enteredBackgroundAuthenticated: TimeInterval? = nil + @State private var appOpenUrlLater: URL? init() { DispatchQueue.global(qos: .background).sync { @@ -39,17 +40,30 @@ struct SimpleXApp: App { // so that it's computed by the time view renders, and not on event after rendering ContentView(contentAccessAuthenticationExtended: !authenticationExpired()) .environmentObject(chatModel) + .environmentObject(AppTheme.shared) .onOpenURL { url in logger.debug("ContentView.onOpenURL: \(url)") - chatModel.appOpenUrl = url + if AppChatState.shared.value == .active { + chatModel.appOpenUrl = url + } else { + appOpenUrlLater = url + } } .onAppear() { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { - initChatAndMigrate() + // Present screen for continue migration if it wasn't finished yet + if chatModel.migrationState != nil { + // It's important, otherwise, user may be locked in undefined state + onboardingStageDefault.set(.step1_SimpleXInfo) + chatModel.onboardingStage = onboardingStageDefault.get() + } else if kcAppPassword.get() == nil || kcSelfDestructPassword.get() == nil { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + initChatAndMigrate() + } } } .onChange(of: scenePhase) { phase in logger.debug("scenePhase was \(String(describing: scenePhase)), now \(String(describing: phase))") + AppSheetState.shared.scenePhaseActive = phase == .active switch (phase) { case .background: // --- authentication @@ -73,10 +87,27 @@ struct SimpleXApp: App { if appState != .stopped { startChatAndActivate { - if appState.inactive && chatModel.chatRunning == true { - updateChats() - if !chatModel.showCallView && !CallController.shared.hasActiveCalls() { - updateCallInvitations() + if chatModel.chatRunning == true { + if let ntfResponse = chatModel.notificationResponse { + chatModel.notificationResponse = nil + NtfManager.shared.processNotificationResponse(ntfResponse) + } + if appState.inactive { + Task { + await updateChats() + if !chatModel.showCallView && !CallController.shared.hasActiveCalls() { + await updateCallInvitations() + } + if let url = appOpenUrlLater { + await MainActor.run { + appOpenUrlLater = nil + chatModel.appOpenUrl = url + } + } + } + } else if let url = appOpenUrlLater { + appOpenUrlLater = nil + chatModel.appOpenUrl = url } } } @@ -98,12 +129,12 @@ struct SimpleXApp: App { if legacyDatabase, case .documents = dbContainerGroupDefault.get() { dbContainerGroupDefault.set(.documents) setMigrationState(.offer) - logger.debug("SimpleXApp init: using legacy DB in documents folder: \(getAppDatabasePath(), privacy: .public)*.db") + logger.debug("SimpleXApp init: using legacy DB in documents folder: \(getAppDatabasePath())*.db") } else { dbContainerGroupDefault.set(.group) setMigrationState(.ready) - logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath(), privacy: .public)*.db") - logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not", privacy: .public) present") + logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath())*.db") + logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not") present") } } @@ -121,16 +152,17 @@ struct SimpleXApp: App { } } - private func updateChats() { + private func updateChats() async { do { - let chats = try apiGetChats() - chatModel.updateChats(with: chats) + let chats = try await apiGetChatsAsync() + await MainActor.run { chatModel.updateChats(chats) } if let id = chatModel.chatId, - let chat = chatModel.getChat(id) { - loadChat(chat: chat) + let chat = chatModel.getChat(id), + !NtfManager.shared.navigatingToChat { + Task { await loadChat(chat: chat, clearItems: false) } } if let ncr = chatModel.ntfContactRequest { - chatModel.ntfContactRequest = nil + await MainActor.run { chatModel.ntfContactRequest = nil } if case let .contactRequest(contactRequest) = chatModel.getChat(ncr.chatId)?.chatInfo { Task { await acceptContactRequest(incognito: ncr.incognito, contactRequest: contactRequest) } } @@ -140,9 +172,9 @@ struct SimpleXApp: App { } } - private func updateCallInvitations() { + private func updateCallInvitations() async { do { - try refreshCallInvitations() + try await refreshCallInvitations() } catch let error { logger.error("apiGetCallInvitations: cannot update call invitations \(responseError(error))") } diff --git a/apps/ios/Shared/Theme/Theme.swift b/apps/ios/Shared/Theme/Theme.swift new file mode 100644 index 0000000000..de67390026 --- /dev/null +++ b/apps/ios/Shared/Theme/Theme.swift @@ -0,0 +1,199 @@ +// +// Theme.swift +// SimpleX (iOS) +// +// Created by Avently on 14.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI +import SimpleXChat + +var CurrentColors: ThemeManager.ActiveTheme = ThemeManager.currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + +var MenuTextColor: Color { if isInDarkTheme() { AppTheme.shared.colors.onBackground.opacity(0.8) } else { Color.black } } +var NoteFolderIconColor: Color { AppTheme.shared.appColors.primaryVariant2 } + +func isInDarkTheme() -> Bool { !CurrentColors.colors.isLight } + +class AppTheme: ObservableObject, Equatable { + static let shared = AppTheme(name: CurrentColors.name, base: CurrentColors.base, colors: CurrentColors.colors, appColors: CurrentColors.appColors, wallpaper: CurrentColors.wallpaper) + + var name: String + var base: DefaultTheme + @ObservedObject var colors: Colors + @ObservedObject var appColors: AppColors + @ObservedObject var wallpaper: AppWallpaper + + init(name: String, base: DefaultTheme, colors: Colors, appColors: AppColors, wallpaper: AppWallpaper) { + self.name = name + self.base = base + self.colors = colors + self.appColors = appColors + self.wallpaper = wallpaper + } + + static func == (lhs: AppTheme, rhs: AppTheme) -> Bool { + lhs.name == rhs.name && + lhs.colors == rhs.colors && + lhs.appColors == rhs.appColors && + lhs.wallpaper == rhs.wallpaper + } + + func updateFromCurrentColors() { + objectWillChange.send() + name = CurrentColors.name + base = CurrentColors.base + colors.updateColorsFrom(CurrentColors.colors) + appColors.updateColorsFrom(CurrentColors.appColors) + wallpaper.updateWallpaperFrom(CurrentColors.wallpaper) + } +} + +struct ThemedBackground: ViewModifier { + @EnvironmentObject var theme: AppTheme + var grouped: Bool = false + + func body(content: Content) -> some View { + content + .background( + theme.base == DefaultTheme.SIMPLEX + ? LinearGradient( + colors: [ + grouped + ? theme.colors.background.lighter(0.4).asGroupedBackground(theme.base.mode) + : theme.colors.background.lighter(0.4), + grouped + ? theme.colors.background.darker(0.4).asGroupedBackground(theme.base.mode) + : theme.colors.background.darker(0.4) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + : LinearGradient( + colors: [], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .background( + theme.base == DefaultTheme.SIMPLEX + ? Color.clear + : grouped + ? theme.colors.background.asGroupedBackground(theme.base.mode) + : theme.colors.background + ) + } +} + +var systemInDarkThemeCurrently: Bool { + return UITraitCollection.current.userInterfaceStyle == .dark +} + +func reactOnDarkThemeChanges(_ inDarkNow: Bool) { + if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME && CurrentColors.colors.isLight == inDarkNow { + // Change active colors from light to dark and back based on system theme + ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME) + } +} + +extension ThemeWallpaper { + public func importFromString() -> ThemeWallpaper { + if preset == nil, let image { + // Need to save image from string and to save its path + if let parsed = imageFromBase64(image), + let filename = saveWallpaperFile(image: parsed) { + var copy = self + copy.image = nil + copy.imageFile = filename + return copy + } else { + return ThemeWallpaper() + } + } else { + return self + } + } + + func withFilledWallpaperBase64() -> ThemeWallpaper { + let aw = toAppWallpaper() + let type = aw.type + let preset: String? = if case let WallpaperType.preset(filename, _) = type { filename } else { nil } + let scale: Float? = if case let WallpaperType.preset(_, scale) = type { scale } else { if case let WallpaperType.image(_, scale, _) = type { scale } else { 1.0 } } + let scaleType: WallpaperScaleType? = if case let WallpaperType.image(_, _, scaleType) = type { scaleType } else { nil } + let image: String? = if case WallpaperType.image = type, let image = type.uiImage { resizeImageToStrSizeSync(image, maxDataSize: 5_000_000) } else { nil } + return ThemeWallpaper ( + preset: preset, + scale: scale, + scaleType: scaleType, + background: aw.background?.toReadableHex(), + tint: aw.tint?.toReadableHex(), + image: image, + imageFile: nil + ) + } +} + +extension ThemeModeOverride { + func removeSameColors(_ base: DefaultTheme, colorsToCompare tc: ThemeColors) -> ThemeModeOverride { + let wallpaperType = WallpaperType.from(wallpaper) ?? WallpaperType.empty + let w: ThemeWallpaper + switch wallpaperType { + case let WallpaperType.preset(filename, scale): + let p = PresetWallpaper.from(filename) + w = ThemeWallpaper( + preset: filename, + scale: scale ?? wallpaper?.scale, + scaleType: nil, + background: p?.background[base]?.toReadableHex(), + tint: p?.tint[base]?.toReadableHex(), + image: nil, + imageFile: nil + ) + case WallpaperType.image: + w = ThemeWallpaper( + preset: nil, + scale: nil, + scaleType: WallpaperScaleType.fill, + background: Color.clear.toReadableHex(), + tint: Color.clear.toReadableHex(), + image: nil, + imageFile: nil + ) + default: + w = ThemeWallpaper() + } + let wallpaper: ThemeWallpaper? = if let wallpaper { + ThemeWallpaper( + preset: wallpaper.preset, + scale: wallpaper.scale != w.scale ? wallpaper.scale : nil, + scaleType: wallpaper.scaleType != w.scaleType ? wallpaper.scaleType : nil, + background: wallpaper.background != w.background ? wallpaper.background : nil, + tint: wallpaper.tint != w.tint ? wallpaper.tint : nil, + image: wallpaper.image, + imageFile: wallpaper.imageFile + ) + } else { + nil + } + return ThemeModeOverride( + mode: self.mode, + colors: ThemeColors( + primary: colors.primary != tc.primary ? colors.primary : nil, + primaryVariant: colors.primaryVariant != tc.primaryVariant ? colors.primaryVariant : nil, + secondary: colors.secondary != tc.secondary ? colors.secondary : nil, + secondaryVariant: colors.secondaryVariant != tc.secondaryVariant ? colors.secondaryVariant : nil, + background: colors.background != tc.background ? colors.background : nil, + surface: colors.surface != tc.surface ? colors.surface : nil, + title: colors.title != tc.title ? colors.title : nil, + primaryVariant2: colors.primaryVariant2 != tc.primaryVariant2 ? colors.primary : nil, + sentMessage: colors.sentMessage != tc.sentMessage ? colors.sentMessage : nil, + sentQuote: colors.sentQuote != tc.sentQuote ? colors.sentQuote : nil, + receivedMessage: colors.receivedMessage != tc.receivedMessage ? colors.receivedMessage : nil, + receivedQuote: colors.receivedQuote != tc.receivedQuote ? colors.receivedQuote : nil + ), + wallpaper: wallpaper + ) + } +} diff --git a/apps/ios/Shared/Theme/ThemeManager.swift b/apps/ios/Shared/Theme/ThemeManager.swift new file mode 100644 index 0000000000..4166619d04 --- /dev/null +++ b/apps/ios/Shared/Theme/ThemeManager.swift @@ -0,0 +1,303 @@ +// +// ThemeManager.swift +// SimpleX (iOS) +// +// Created by Avently on 03.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI +import SimpleXChat + +class ThemeManager { + struct ActiveTheme: Equatable { + let name: String + let base: DefaultTheme + let colors: Colors + let appColors: AppColors + var wallpaper: AppWallpaper = AppWallpaper(background: nil, tint: nil, type: .empty) + + func toAppTheme() -> AppTheme { + AppTheme(name: name, base: base, colors: colors, appColors: appColors, wallpaper: wallpaper) + } + } + + private static func systemDarkThemeColors() -> (Colors, DefaultTheme) { + switch systemDarkThemeDefault.get() { + case DefaultTheme.DARK.themeName: (DarkColorPalette, DefaultTheme.DARK) + case DefaultTheme.SIMPLEX.themeName: (SimplexColorPalette, DefaultTheme.SIMPLEX) + case DefaultTheme.BLACK.themeName: (BlackColorPalette, DefaultTheme.BLACK) + default: (SimplexColorPalette, DefaultTheme.SIMPLEX) + } + } + + private static func nonSystemThemeName() -> String { + let themeName = currentThemeDefault.get() + return if themeName != DefaultTheme.SYSTEM_THEME_NAME { + themeName + } else { + systemInDarkThemeCurrently ? systemDarkThemeDefault.get() : DefaultTheme.LIGHT.themeName + } + } + + static func defaultActiveTheme(_ appSettingsTheme: [ThemeOverrides]) -> ThemeOverrides? { + let nonSystemThemeName = nonSystemThemeName() + let defaultThemeId = currentThemeIdsDefault.get()[nonSystemThemeName] + return appSettingsTheme.getTheme(defaultThemeId) + } + + static func defaultActiveTheme(_ perUserTheme: ThemeModeOverrides?, _ appSettingsTheme: [ThemeOverrides]) -> ThemeModeOverride { + let perUserTheme = !CurrentColors.colors.isLight ? perUserTheme?.dark : perUserTheme?.light + if let perUserTheme { + return perUserTheme + } + let defaultTheme = defaultActiveTheme(appSettingsTheme) + return ThemeModeOverride(mode: CurrentColors.base.mode, colors: defaultTheme?.colors ?? ThemeColors(), wallpaper: defaultTheme?.wallpaper ?? ThemeWallpaper.from(PresetWallpaper.school.toType(CurrentColors.base), nil, nil)) + } + + static func currentColors(_ themeOverridesForType: WallpaperType?, _ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverrides?, _ appSettingsTheme: [ThemeOverrides]) -> ActiveTheme { + let themeName = currentThemeDefault.get() + let nonSystemThemeName = nonSystemThemeName() + let defaultTheme = defaultActiveTheme(appSettingsTheme) + + let baseTheme = switch nonSystemThemeName { + case DefaultTheme.LIGHT.themeName: ActiveTheme(name: DefaultTheme.LIGHT.themeName, base: DefaultTheme.LIGHT, colors: LightColorPalette.clone(), appColors: LightColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.LIGHT))) + case DefaultTheme.DARK.themeName: ActiveTheme(name: DefaultTheme.DARK.themeName, base: DefaultTheme.DARK, colors: DarkColorPalette.clone(), appColors: DarkColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.DARK))) + case DefaultTheme.SIMPLEX.themeName: ActiveTheme(name: DefaultTheme.SIMPLEX.themeName, base: DefaultTheme.SIMPLEX, colors: SimplexColorPalette.clone(), appColors: SimplexColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.SIMPLEX))) + case DefaultTheme.BLACK.themeName: ActiveTheme(name: DefaultTheme.BLACK.themeName, base: DefaultTheme.BLACK, colors: BlackColorPalette.clone(), appColors: BlackColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.BLACK))) + default: ActiveTheme(name: DefaultTheme.LIGHT.themeName, base: DefaultTheme.LIGHT, colors: LightColorPalette.clone(), appColors: LightColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.LIGHT))) + } + + let perUserTheme = baseTheme.colors.isLight ? perUserTheme?.light : perUserTheme?.dark + let theme = appSettingsTheme.sameTheme(themeOverridesForType ?? perChatTheme?.type ?? perUserTheme?.type ?? defaultTheme?.wallpaper?.toAppWallpaper().type, nonSystemThemeName) ?? defaultTheme + + if theme == nil && perUserTheme == nil && perChatTheme == nil && themeOverridesForType == nil { + return ActiveTheme(name: themeName, base: baseTheme.base, colors: baseTheme.colors, appColors: baseTheme.appColors, wallpaper: baseTheme.wallpaper) + } + let presetWallpaperTheme: ThemeColors? = if let themeOverridesForType, case let WallpaperType.preset(filename, _) = themeOverridesForType { + PresetWallpaper.from(filename)?.colors[baseTheme.base] + } else if let wallpaper = perChatTheme?.wallpaper { + if let preset = wallpaper.preset { PresetWallpaper.from(preset)?.colors[baseTheme.base] } else { nil } + } else if let wallpaper = perUserTheme?.wallpaper { + if let preset = wallpaper.preset { PresetWallpaper.from(preset)?.colors[baseTheme.base] } else { nil } + } else { + if let preset = theme?.wallpaper?.preset { PresetWallpaper.from(preset)?.colors[baseTheme.base] } else { nil } + } + + let themeOrEmpty = theme ?? ThemeOverrides(base: baseTheme.base) + let colors = themeOrEmpty.toColors(themeOrEmpty.base, perChatTheme?.colors, perUserTheme?.colors, presetWallpaperTheme) + return ActiveTheme( + name: themeName, + base: baseTheme.base, + colors: colors, + appColors: themeOrEmpty.toAppColors(themeOrEmpty.base, perChatTheme?.colors, perChatTheme?.type, perUserTheme?.colors, perUserTheme?.type, presetWallpaperTheme), + wallpaper: themeOrEmpty.toAppWallpaper(themeOverridesForType, perChatTheme, perUserTheme, colors.background) + ) + } + + static func currentThemeOverridesForExport(_ themeOverridesForType: WallpaperType?, _ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverrides?) -> ThemeOverrides { + let current = currentColors(themeOverridesForType, perChatTheme, perUserTheme, themeOverridesDefault.get()) + let wType = current.wallpaper.type + let wBackground = current.wallpaper.background + let wTint = current.wallpaper.tint + let w: ThemeWallpaper? = if case WallpaperType.empty = wType { + nil + } else { + ThemeWallpaper.from(wType, wBackground?.toReadableHex(), wTint?.toReadableHex()).withFilledWallpaperBase64() + } + return ThemeOverrides( + themeId: "", + base: current.base, + colors: ThemeColors.from(current.colors, current.appColors), + wallpaper: w + ) + } + + static func applyTheme(_ theme: String) { + currentThemeDefault.set(theme) + CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + AppTheme.shared.updateFromCurrentColors() + let tint = UIColor(CurrentColors.colors.primary) + if SceneDelegate.windowStatic?.tintColor != tint { + SceneDelegate.windowStatic?.tintColor = tint + } +// applyNavigationBarColors(CurrentColors.toAppTheme()) + } + + static func adjustWindowStyle() { + let style = switch currentThemeDefault.get() { + case DefaultTheme.LIGHT.themeName: UIUserInterfaceStyle.light + case DefaultTheme.SYSTEM_THEME_NAME: UIUserInterfaceStyle.unspecified + default: UIUserInterfaceStyle.dark + } + if SceneDelegate.windowStatic?.overrideUserInterfaceStyle != style { + SceneDelegate.windowStatic?.overrideUserInterfaceStyle = style + } + } + +// static func applyNavigationBarColors(_ theme: AppTheme) { +// let baseColors = switch theme.base { +// case DefaultTheme.LIGHT: LightColorPaletteApp +// case DefaultTheme.DARK: DarkColorPaletteApp +// case DefaultTheme.SIMPLEX: SimplexColorPaletteApp +// case DefaultTheme.BLACK: BlackColorPaletteApp +// } +// let isDefaultColor = baseColors.title == theme.appColors.title +// +// let title = UIColor(theme.appColors.title) +// if !isDefaultColor && UINavigationBar.appearance().titleTextAttributes?.first as? UIColor != title { +// UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: title] +// UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: title] +// } else { +// UINavigationBar.appearance().titleTextAttributes = nil +// UINavigationBar.appearance().largeTitleTextAttributes = nil +// } +// } + + static func changeDarkTheme(_ theme: String) { + systemDarkThemeDefault.set(theme) + CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + AppTheme.shared.updateFromCurrentColors() + } + + static func saveAndApplyThemeColor(_ baseTheme: DefaultTheme, _ name: ThemeColor, _ color: Color? = nil, _ pref: CodableDefault<[ThemeOverrides]>? = nil) { + let nonSystemThemeName = baseTheme.themeName + let pref = pref ?? themeOverridesDefault + let overrides = pref.get() + let themeId = currentThemeIdsDefault.get()[nonSystemThemeName] + let prevValue = overrides.getTheme(themeId) ?? ThemeOverrides(base: baseTheme) + pref.set(overrides.replace(prevValue.withUpdatedColor(name, color?.toReadableHex()))) + var themeIds = currentThemeIdsDefault.get() + themeIds[nonSystemThemeName] = prevValue.themeId + currentThemeIdsDefault.set(themeIds) + applyTheme(currentThemeDefault.get()) + } + + static func applyThemeColor(name: ThemeColor, color: Color? = nil, pref: Binding) { + pref.wrappedValue = pref.wrappedValue.withUpdatedColor(name, color?.toReadableHex()) + } + + static func saveAndApplyWallpaper(_ baseTheme: DefaultTheme, _ type: WallpaperType?, _ pref: CodableDefault<[ThemeOverrides]>?) { + let nonSystemThemeName = baseTheme.themeName + let pref = pref ?? themeOverridesDefault + let overrides = pref.get() + let theme = overrides.sameTheme(type, baseTheme.themeName) + var prevValue = theme ?? ThemeOverrides(base: baseTheme) + prevValue.wallpaper = if let type { + if case WallpaperType.empty = type { + nil as ThemeWallpaper? + } else { + ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint) + } + } else { + nil + } + pref.set(overrides.replace(prevValue)) + var themeIds = currentThemeIdsDefault.get() + themeIds[nonSystemThemeName] = prevValue.themeId + currentThemeIdsDefault.set(themeIds) + applyTheme(currentThemeDefault.get()) + } + + static func copyFromSameThemeOverrides(_ type: WallpaperType?, _ lowerLevelOverride: ThemeModeOverride?, _ pref: Binding) -> Bool { + let overrides = themeOverridesDefault.get() + let sameWallpaper: ThemeWallpaper? = if let wallpaper = lowerLevelOverride?.wallpaper, lowerLevelOverride?.type?.sameType(type) == true { + wallpaper + } else { + overrides.sameTheme(type, CurrentColors.base.themeName)?.wallpaper + } + guard let sameWallpaper else { + if let type { + var w: ThemeWallpaper = ThemeWallpaper.from(type, nil, nil) + w.scale = nil + w.scaleType = nil + w.background = nil + w.tint = nil + pref.wrappedValue = ThemeModeOverride(mode: CurrentColors.base.mode, wallpaper: w) + } else { + // Make an empty wallpaper to override any top level ones + pref.wrappedValue = ThemeModeOverride(mode: CurrentColors.base.mode, wallpaper: ThemeWallpaper()) + } + return true + } + var type = sameWallpaper.toAppWallpaper().type + if case let WallpaperType.image(filename, scale, scaleType) = type, sameWallpaper.imageFile == filename { + // same image file. Needs to be copied first in order to be able to remove the file once it's not needed anymore without affecting main theme override + if let filename = saveWallpaperFile(url: getWallpaperFilePath(filename)) { + type = WallpaperType.image(filename, scale, scaleType) + } else { + logger.error("Error while copying wallpaper from global overrides to chat overrides") + return false + } + } + var prevValue = pref.wrappedValue + var w = ThemeWallpaper.from(type, nil, nil) + w.scale = nil + w.scaleType = nil + w.background = nil + w.tint = nil + prevValue.colors = ThemeColors() + prevValue.wallpaper = w + pref.wrappedValue = prevValue + return true + } + + static func applyWallpaper(_ type: WallpaperType?, _ pref: Binding) { + var prevValue = pref.wrappedValue + prevValue.wallpaper = if let type { + ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint) + } else { + nil + } + pref.wrappedValue = prevValue + } + + static func saveAndApplyThemeOverrides(_ theme: ThemeOverrides, _ pref: CodableDefault<[ThemeOverrides]>? = nil) { + let wallpaper = theme.wallpaper?.importFromString() + let nonSystemThemeName = theme.base.themeName + let pref: CodableDefault<[ThemeOverrides]> = pref ?? themeOverridesDefault + let overrides = pref.get() + var prevValue = overrides.getTheme(nil, wallpaper?.toAppWallpaper().type, theme.base) ?? ThemeOverrides(base: theme.base) + if let imageFile = prevValue.wallpaper?.imageFile { + try? FileManager.default.removeItem(at: getWallpaperFilePath(imageFile)) + } + prevValue.base = theme.base + prevValue.colors = theme.colors + prevValue.wallpaper = wallpaper + pref.set(overrides.replace(prevValue)) + currentThemeDefault.set(nonSystemThemeName) + var currentThemeIds = currentThemeIdsDefault.get() + currentThemeIds[nonSystemThemeName] = prevValue.themeId + currentThemeIdsDefault.set(currentThemeIds) + applyTheme(nonSystemThemeName) + } + + static func resetAllThemeColors(_ pref: CodableDefault<[ThemeOverrides]>? = nil) { + let nonSystemThemeName = nonSystemThemeName() + let pref: CodableDefault<[ThemeOverrides]> = pref ?? themeOverridesDefault + let overrides = pref.get() + guard let themeId = currentThemeIdsDefault.get()[nonSystemThemeName], + var prevValue = overrides.getTheme(themeId) + else { return } + prevValue.colors = ThemeColors() + prevValue.wallpaper?.background = nil + prevValue.wallpaper?.tint = nil + pref.set(overrides.replace(prevValue)) + applyTheme(currentThemeDefault.get()) + } + + static func resetAllThemeColors(_ pref: Binding) { + var prevValue = pref.wrappedValue + prevValue.colors = ThemeColors() + prevValue.wallpaper?.background = nil + prevValue.wallpaper?.tint = nil + pref.wrappedValue = prevValue + } + + static func removeTheme(_ themeId: String?) { + var themes = themeOverridesDefault.get().map { $0 } + themes.removeAll(where: { $0.themeId == themeId }) + themeOverridesDefault.set(themes) + } +} diff --git a/apps/ios/Shared/Views/Call/ActiveCallView.swift b/apps/ios/Shared/Views/Call/ActiveCallView.swift index e613476a1d..ab7a47b944 100644 --- a/apps/ios/Shared/Views/Call/ActiveCallView.swift +++ b/apps/ios/Shared/Views/Call/ActiveCallView.swift @@ -9,57 +9,100 @@ import SwiftUI import WebKit import SimpleXChat +import AVFoundation struct ActiveCallView: View { @EnvironmentObject var m: ChatModel + @Environment(\.colorScheme) var colorScheme @ObservedObject var call: Call @Environment(\.scenePhase) var scenePhase @State private var client: WebRTCClient? = nil - @State private var activeCall: WebRTCClient.Call? = nil @State private var localRendererAspectRatio: CGFloat? = nil + @State var remoteContentMode: UIView.ContentMode = .scaleAspectFill @Binding var canConnectCall: Bool + @State var prevColorScheme: ColorScheme = .dark + @State var pipShown = false + @State var wasConnected = false var body: some View { - ZStack(alignment: .bottom) { - if let client = client, [call.peerMedia, call.localMedia].contains(.video), activeCall != nil { - GeometryReader { g in - let width = g.size.width * 0.3 - ZStack(alignment: .topTrailing) { - CallViewRemote(client: client, activeCall: $activeCall) - CallViewLocal(client: client, activeCall: $activeCall, localRendererAspectRatio: $localRendererAspectRatio) + ZStack(alignment: .topLeading) { + ZStack(alignment: .bottom) { + if let client = client, call.hasVideo { + GeometryReader { g in + let width = g.size.width * 0.3 + ZStack(alignment: .topTrailing) { + ZStack(alignment: .center) { + // For some reason, when the view in GeometryReader and ZStack is visible, it steals clicks on a back button, so showing something on top like this with background color helps (.clear color doesn't work) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.primary.opacity(0.000001)) + + CallViewRemote(client: client, call: call, activeCallViewIsCollapsed: $m.activeCallViewIsCollapsed, contentMode: $remoteContentMode, pipShown: $pipShown) + .onTapGesture { + remoteContentMode = remoteContentMode == .scaleAspectFill ? .scaleAspectFit : .scaleAspectFill + } + + Group { + let localVideoTrack = client.activeCall?.localVideoTrack ?? client.notConnectedCall?.localCameraAndTrack?.1 + if localVideoTrack != nil { + CallViewLocal(client: client, localRendererAspectRatio: $localRendererAspectRatio, pipShown: $pipShown) + .onDisappear { + localRendererAspectRatio = nil + } + } else { + Rectangle().fill(.black) + } + } .cornerRadius(10) - .frame(width: width, height: width / (localRendererAspectRatio ?? 1)) + .frame(width: width, height: localRendererAspectRatio == nil ? (g.size.width < g.size.height ? width * 1.33 : width / 1.33) : width / (localRendererAspectRatio ?? 1)) .padding([.top, .trailing], 17) + } } } - } - if let call = m.activeCall, let client = client { - ActiveCallOverlay(call: call, client: client) + if let call = m.activeCall, let client = client, (!pipShown || !call.hasVideo) { + ActiveCallOverlay(call: call, client: client) + } } } + .allowsHitTesting(!m.activeCallViewIsCollapsed) + .opacity(m.activeCallViewIsCollapsed ? 0 : 1) .onAppear { - logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase), privacy: .public), canConnectCall \(canConnectCall)") + logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase)), canConnectCall \(canConnectCall)") AppDelegate.keepScreenOn(true) + Task { + await askRequiredPermissions() + } createWebRTCClient() dismissAllSheets() + hideKeyboard() + prevColorScheme = colorScheme } .onChange(of: canConnectCall) { _ in - logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall, privacy: .public)") + logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall)") createWebRTCClient() } + .onChange(of: m.activeCallViewIsCollapsed) { _ in + hideKeyboard() + } .onDisappear { logger.debug("ActiveCallView: disappear") Task { await m.callCommand.setClient(nil) } AppDelegate.keepScreenOn(false) client?.endCall() + CallSoundsPlayer.shared.stop() + try? AVAudioSession.sharedInstance().setCategory(.soloAmbient) + if (wasConnected) { + CallSoundsPlayer.shared.vibrate(long: true) + } } - .background(.black) - .preferredColorScheme(.dark) + .background(m.activeCallViewIsCollapsed ? .clear : .black) + // Quite a big delay when opening/closing the view when a scheme changes (globally) this way. It's not needed when CallKit is used since status bar is green with white text on it + .preferredColorScheme(m.activeCallViewIsCollapsed || CallController.useCallKit() ? prevColorScheme : .dark) } private func createWebRTCClient() { if client == nil && canConnectCall { - client = WebRTCClient($activeCall, { msg in await MainActor.run { processRtcMessage(msg: msg) } }, $localRendererAspectRatio) + client = WebRTCClient({ msg in await MainActor.run { processRtcMessage(msg: msg) } }, $localRendererAspectRatio) Task { await m.callCommand.setClient(client) } @@ -69,12 +112,12 @@ struct ActiveCallView: View { @MainActor private func processRtcMessage(msg: WVAPIMessage) { if call == m.activeCall, - let call = m.activeCall, - let client = client { + let call = m.activeCall, + let client = client { logger.debug("ActiveCallView: response \(msg.resp.respType)") switch msg.resp { case let .capabilities(capabilities): - let callType = CallType(media: call.localMedia, capabilities: capabilities) + let callType = CallType(media: call.initialCallType, capabilities: capabilities) Task { do { try await apiSendCallInvitation(call.contact, callType) @@ -85,12 +128,17 @@ struct ActiveCallView: View { call.callState = .invitationSent call.localCapabilities = capabilities } + if call.hasVideo && !AVAudioSession.sharedInstance().hasExternalAudioDevice() { + try? AVAudioSession.sharedInstance().setCategory(.playback, options: [.allowBluetooth, .allowAirPlay, .allowBluetoothA2DP]) + } + CallSoundsPlayer.shared.startConnectingCallSound() + activeCallWaitDeliveryReceipt() } case let .offer(offer, iceCandidates, capabilities): Task { do { try await apiSendCallOffer(call.contact, offer, iceCandidates, - media: call.localMedia, capabilities: capabilities) + media: call.initialCallType, capabilities: capabilities) } catch { logger.error("apiSendCallOffer \(responseError(error))") } @@ -108,6 +156,7 @@ struct ActiveCallView: View { } await MainActor.run { call.callState = .negotiated + CallSoundsPlayer.shared.stop() } } case let .ice(iceCandidates): @@ -122,13 +171,22 @@ struct ActiveCallView: View { if let callStatus = WebRTCCallStatus.init(rawValue: state.connectionState), case .connected = callStatus { call.direction == .outgoing - ? CallController.shared.reportOutgoingCall(call: call, connectedAt: nil) - : CallController.shared.reportIncomingCall(call: call, connectedAt: nil) + ? CallController.shared.reportOutgoingCall(call: call, connectedAt: nil) + : CallController.shared.reportIncomingCall(call: call, connectedAt: nil) call.callState = .connected + call.connectedAt = .now + if !wasConnected { + CallSoundsPlayer.shared.vibrate(long: false) + wasConnected = true + } } if state.connectionState == "closed" { closeCallView(client) + if let callUUID = m.activeCall?.callUUID { + CallController.shared.endCall(callUUID: callUUID) + } m.activeCall = nil + m.activeCallViewIsCollapsed = false } Task { do { @@ -140,10 +198,23 @@ struct ActiveCallView: View { case let .connected(connectionInfo): call.callState = .connected call.connectionInfo = connectionInfo + call.connectedAt = .now + if !wasConnected { + CallSoundsPlayer.shared.vibrate(long: false) + wasConnected = true + } + case let .peerMedia(source, enabled): + switch source { + case .mic: call.peerMediaSources.mic = enabled + case .camera: call.peerMediaSources.camera = enabled + case .screenAudio: call.peerMediaSources.screenAudio = enabled + case .screenVideo: call.peerMediaSources.screenVideo = enabled + case .unknown: () + } case .ended: closeCallView(client) call.callState = .ended - if let uuid = call.callkitUUID { + if let uuid = call.callUUID { CallController.shared.endCall(callUUID: uuid) } case .ok: @@ -153,6 +224,7 @@ struct ActiveCallView: View { case .end: closeCallView(client) m.activeCall = nil + m.activeCallViewIsCollapsed = false default: () } case let .error(message): @@ -165,6 +237,44 @@ struct ActiveCallView: View { } } + private func activeCallWaitDeliveryReceipt() { + ChatReceiver.shared.messagesChannel = { msg in + guard let call = ChatModel.shared.activeCall, call.callState == .invitationSent else { + ChatReceiver.shared.messagesChannel = nil + return + } + if case let .result(.chatItemsStatusesUpdated(_, chatItems)) = msg, + chatItems.contains(where: { ci in + ci.chatInfo.id == call.contact.id && + ci.chatItem.content.isSndCall && + ci.chatItem.meta.itemStatus.isSndRcvd + }) { + CallSoundsPlayer.shared.startInCallSound() + ChatReceiver.shared.messagesChannel = nil + } + } + } + + private func askRequiredPermissions() async { + let mic = await WebRTCClient.isAuthorized(for: .audio) + await MainActor.run { + call.localMediaSources.mic = mic + } + let cameraAuthorized = AVCaptureDevice.authorizationStatus(for: .video) == .authorized + var camera = call.initialCallType == .audio || cameraAuthorized + if call.initialCallType == .video && !cameraAuthorized { + camera = await WebRTCClient.isAuthorized(for: .video) + await MainActor.run { + if camera, let client { + client.setCameraEnabled(true) + } + } + } + if !mic || !camera { + WebRTCClient.showUnauthorizedAlert(for: !mic ? .audio : .video) + } + } + private func closeCallView(_ client: WebRTCClient) { if m.activeCall != nil { m.showCallView = false @@ -176,166 +286,237 @@ struct ActiveCallOverlay: View { @EnvironmentObject var chatModel: ChatModel @ObservedObject var call: Call var client: WebRTCClient + @ObservedObject private var deviceManager = CallAudioDeviceManager.shared var body: some View { VStack { - switch call.localMedia { - case .video: - callInfoView(call, .leading) + switch call.hasVideo { + case true: + videoCallInfoView(call) .foregroundColor(.white) .opacity(0.8) - .padding() - - Spacer() - - HStack { - toggleAudioButton() - Spacer() - Color.clear.frame(width: 40, height: 40) - Spacer() - endCallButton() - Spacer() - if call.videoEnabled { - flipCameraButton() - } else { - Color.clear.frame(width: 40, height: 40) + .padding(.horizontal) + // Fixed vertical padding required for preserving position of buttons row when changing audio-to-video and back in landscape orientation. + // Otherwise, bigger padding is added by SwiftUI when switching call types + .padding(.vertical, 10) + case false: + ZStack(alignment: .topLeading) { + Button { + chatModel.activeCallViewIsCollapsed = true + } label: { + Label("Back", systemImage: "chevron.left") + .padding() + .foregroundColor(.white.opacity(0.8)) } - Spacer() - toggleVideoButton() + VStack { + ProfileImage(imageStr: call.contact.profile.image, size: 192) + audioCallInfoView(call) + } + .foregroundColor(.white) + .opacity(0.8) + .padding(.horizontal) + .padding(.vertical, 10) + .frame(maxHeight: .infinity) } - .padding(.horizontal, 20) - .padding(.bottom, 16) - .frame(maxWidth: .infinity, alignment: .center) - - case .audio: - VStack { - ProfileImage(imageStr: call.contact.profile.image) - .scaledToFit() - .frame(width: 192, height: 192) - callInfoView(call, .center) - } - .foregroundColor(.white) - .opacity(0.8) - .padding() - .frame(maxHeight: .infinity) - - Spacer() - - ZStack(alignment: .bottom) { - toggleAudioButton() - .frame(maxWidth: .infinity, alignment: .leading) - endCallButton() - toggleSpeakerButton() - .frame(maxWidth: .infinity, alignment: .trailing) - } - .padding(.bottom, 60) - .padding(.horizontal, 48) } + + Spacer() + + HStack { + toggleMicButton() + Spacer() + audioDeviceButton() + Spacer() + endCallButton() + Spacer() + if call.localMediaSources.camera { + flipCameraButton() + } else { + Color.clear.frame(width: 60, height: 60) + } + Spacer() + toggleCameraButton() + } + .padding(.horizontal, 20) + .padding(.bottom, 16) + .frame(maxWidth: 440, alignment: .center) } .frame(maxWidth: .infinity) + .onAppear { + deviceManager.start() + } + .onDisappear { + deviceManager.stop() + } } - private func callInfoView(_ call: Call, _ alignment: Alignment) -> some View { + private func audioCallInfoView(_ call: Call) -> some View { VStack { Text(call.contact.chatViewName) .lineLimit(1) .font(.title) - .frame(maxWidth: .infinity, alignment: alignment) + .frame(maxWidth: .infinity, alignment: .center) Group { Text(call.callState.text) HStack { Text(call.encryptionStatus) if let connInfo = call.connectionInfo { - Text("(") + Text(connInfo.text) + Text(")") + Text(verbatim: "(") + Text(connInfo.text) + Text(verbatim: ")") } } } .font(.subheadline) - .frame(maxWidth: .infinity, alignment: alignment) + .frame(maxWidth: .infinity, alignment: .center) + } + } + + private func videoCallInfoView(_ call: Call) -> some View { + VStack { + Button { + chatModel.activeCallViewIsCollapsed = true + } label: { + HStack(alignment: .center, spacing: 16) { + Image(systemName: "chevron.left") + .resizable() + .frame(width: 10, height: 18) + Text(call.contact.chatViewName) + .lineLimit(1) + .font(.title) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + Group { + Text(call.callState.text) + HStack { + Text(call.encryptionStatus) + if let connInfo = call.connectionInfo { + Text(verbatim: "(") + Text(connInfo.text) + Text(verbatim: ")") + } + } + } + .font(.subheadline) + .frame(maxWidth: .infinity, alignment: .leading) } } private func endCallButton() -> some View { let cc = CallController.shared - return callButton("phone.down.fill", width: 60, height: 60) { - if let uuid = call.callkitUUID { + return callButton("phone.down.fill", .red, padding: 10) { + if let uuid = call.callUUID { cc.endCall(callUUID: uuid) } else { cc.endCall(call: call) {} } } - .foregroundColor(.red) } - private func toggleAudioButton() -> some View { - controlButton(call, call.audioEnabled ? "mic.fill" : "mic.slash") { + private func toggleMicButton() -> some View { + controlButton(call, call.localMediaSources.mic ? "mic.fill" : "mic.slash", padding: 14) { Task { - client.setAudioEnabled(!call.audioEnabled) - DispatchQueue.main.async { - call.audioEnabled = !call.audioEnabled - } + if await WebRTCClient.isAuthorized(for: .audio) { + client.setAudioEnabled(!call.localMediaSources.mic) + } else { WebRTCClient.showUnauthorizedAlert(for: .audio) } + } + } + } + + func audioDeviceButton() -> some View { + // Check if the only input is microphone. And in this case show toggle button, + // If there are more inputs, it probably means something like bluetooth headphones are available + // and in this case show iOS button for choosing different output. + // There is no way to get available outputs, only inputs + Group { + if deviceManager.availableInputs.allSatisfy({ $0.portType == .builtInMic }) { + toggleSpeakerButton() + } else { + audioDevicePickerButton() + } + } + .onChange(of: call.localMediaSources.hasVideo) { hasVideo in + let current = AVAudioSession.sharedInstance().currentRoute.outputs.first?.portType + let speakerEnabled = current == .builtInSpeaker + let receiverEnabled = current == .builtInReceiver + // react automatically only when receiver were selected, otherwise keep an external device selected + if !speakerEnabled && hasVideo && receiverEnabled { + client.setSpeakerEnabledAndConfigureSession(!speakerEnabled, skipExternalDevice: true) + call.speakerEnabled = !speakerEnabled } } } private func toggleSpeakerButton() -> some View { - controlButton(call, call.speakerEnabled ? "speaker.wave.2.fill" : "speaker.wave.1.fill") { - Task { - client.setSpeakerEnabledAndConfigureSession(!call.speakerEnabled) - DispatchQueue.main.async { - call.speakerEnabled = !call.speakerEnabled - } - } + controlButton(call, !call.peerMediaSources.mic ? "speaker.slash" : call.speakerEnabled ? "speaker.wave.2.fill" : "speaker.wave.1.fill", padding: !call.peerMediaSources.mic ? 16 : call.speakerEnabled ? 15 : 17) { + let speakerEnabled = AVAudioSession.sharedInstance().currentRoute.outputs.first?.portType == .builtInSpeaker + client.setSpeakerEnabledAndConfigureSession(!speakerEnabled) + call.speakerEnabled = !speakerEnabled + } + .onAppear { + deviceManager.call = call + //call.speakerEnabled = AVAudioSession.sharedInstance().currentRoute.outputs.first?.portType == .builtInSpeaker } } - private func toggleVideoButton() -> some View { - controlButton(call, call.videoEnabled ? "video.fill" : "video.slash") { + private func toggleCameraButton() -> some View { + controlButton(call, call.localMediaSources.camera ? "video.fill" : "video.slash", padding: call.localMediaSources.camera ? 16 : 14) { Task { - client.setVideoEnabled(!call.videoEnabled) - DispatchQueue.main.async { - call.videoEnabled = !call.videoEnabled - } + if await WebRTCClient.isAuthorized(for: .video) { + client.setCameraEnabled(!call.localMediaSources.camera) + } else { WebRTCClient.showUnauthorizedAlert(for: .video) } } } + .disabled(call.initialCallType == .audio && client.activeCall?.peerHasOldVersion == true) } - @ViewBuilder private func flipCameraButton() -> some View { - controlButton(call, "arrow.triangle.2.circlepath") { + private func flipCameraButton() -> some View { + controlButton(call, "arrow.triangle.2.circlepath", padding: 12) { Task { - client.flipCamera() + if await WebRTCClient.isAuthorized(for: .video) { + client.flipCamera() + } } } } - @ViewBuilder private func controlButton(_ call: Call, _ imageName: String, _ perform: @escaping () -> Void) -> some View { - if call.hasMedia { - callButton(imageName, width: 50, height: 38, perform) - .foregroundColor(.white) - .opacity(0.85) - } else { - Color.clear.frame(width: 50, height: 38) - } + private func controlButton(_ call: Call, _ imageName: String, padding: CGFloat, _ perform: @escaping () -> Void) -> some View { + callButton(imageName, call.peerMediaSources.hasVideo ? Color.black.opacity(0.2) : Color.white.opacity(0.2), padding: padding, perform) } - private func callButton(_ imageName: String, width: CGFloat, height: CGFloat, _ perform: @escaping () -> Void) -> some View { + private func audioDevicePickerButton() -> some View { + AudioDevicePicker() + .opacity(0.8) + .scaleEffect(2) + .padding(10) + .frame(width: 60, height: 60) + .background(call.peerMediaSources.hasVideo ? Color.black.opacity(0.2) : Color.white.opacity(0.2)) + .clipShape(.circle) + } + + private func callButton(_ imageName: String, _ background: Color, padding: CGFloat, _ perform: @escaping () -> Void) -> some View { Button { perform() } label: { Image(systemName: imageName) .resizable() .scaledToFit() - .frame(maxWidth: width, maxHeight: height) + .padding(padding) + .frame(width: 60, height: 60) + .background(background) } + .foregroundColor(whiteColorWithAlpha) + .clipShape(.circle) + } + + private var whiteColorWithAlpha: Color { + get { Color(red: 204 / 255, green: 204 / 255, blue: 204 / 255) } } } struct ActiveCallOverlay_Previews: PreviewProvider { static var previews: some View { Group{ - ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callkitUUID: UUID(), callState: .offerSent, localMedia: .video), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil))) + ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, initialCallType: .video), client: WebRTCClient({ _ in }, Binding.constant(nil))) .background(.black) - ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callkitUUID: UUID(), callState: .offerSent, localMedia: .audio), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil))) + ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, initialCallType: .audio), client: WebRTCClient({ _ in }, Binding.constant(nil))) .background(.black) } } diff --git a/apps/ios/Shared/Views/Call/AudioDevicePicker.swift b/apps/ios/Shared/Views/Call/AudioDevicePicker.swift new file mode 100644 index 0000000000..be41741ab5 --- /dev/null +++ b/apps/ios/Shared/Views/Call/AudioDevicePicker.swift @@ -0,0 +1,25 @@ +// +// MPVolumeView.swift +// SimpleX (iOS) +// +// Created by Avently on 24.04.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI +import UIKit +import AVKit + +struct AudioDevicePicker: UIViewRepresentable { + func makeUIView(context: Context) -> some UIView { + let v = AVRoutePickerView(frame: .zero) + v.activeTintColor = .white + v.tintColor = .white + return v + } + + func updateUIView(_ uiView: UIViewType, context: Context) { + + } +} diff --git a/apps/ios/Shared/Views/Call/CallAudioDeviceManager.swift b/apps/ios/Shared/Views/Call/CallAudioDeviceManager.swift new file mode 100644 index 0000000000..d56849d16a --- /dev/null +++ b/apps/ios/Shared/Views/Call/CallAudioDeviceManager.swift @@ -0,0 +1,67 @@ +// +// CallAudioDeviceManager.swift +// SimpleX (iOS) +// +// Created by Avently on 23.04.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI +import SimpleXChat +import AVKit +import WebRTC + +class CallAudioDeviceManager: ObservableObject { + static let shared = CallAudioDeviceManager() + let audioSession: AVAudioSession + let nc = NotificationCenter.default + + var call: Call? + var timer: Timer? = nil + + // Actually, only one output + @Published var outputs: [AVAudioSessionPortDescription] + @Published var currentDevice: AVAudioSessionPortDescription? = nil + // All devices that can record audio (the ones that can play audio are not included) + @Published var availableInputs: [AVAudioSessionPortDescription] = [] + + + init(_ audioSession: AVAudioSession? = nil) { + self.audioSession = audioSession ?? RTCAudioSession.sharedInstance().session + self.outputs = self.audioSession.currentRoute.outputs + self.availableInputs = self.audioSession.availableInputs ?? [] + } + + func reloadDevices() { + outputs = audioSession.currentRoute.outputs + currentDevice = audioSession.currentRoute.outputs.first + availableInputs = audioSession.availableInputs ?? [] + call?.speakerEnabled = currentDevice?.portType == .builtInSpeaker + + + // Workaround situation: + // have bluetooth device connected, choosing speaker, disconnecting bluetooth device. In this case iOS will not post notification, so do it manually + timer?.invalidate() + if availableInputs.contains(where: { $0.portType != .builtInReceiver && $0.portType != .builtInSpeaker }) { + timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { t in + self.reloadDevices() + } + } + } + + @objc func audioCallback(notification: Notification) { + reloadDevices() + + logger.debug("Changes in devices, current audio devices: \(String(describing: self.availableInputs.map({ $0.portType.rawValue }))), output: \(String(describing: self.currentDevice?.portType.rawValue))") + } + + func start() { + nc.addObserver(self, selector: #selector(audioCallback), name: AVAudioSession.routeChangeNotification, object: nil) + } + + func stop() { + nc.removeObserver(self, name: AVAudioSession.routeChangeNotification, object: nil) + timer?.invalidate() + } +} diff --git a/apps/ios/Shared/Views/Call/CallController.swift b/apps/ios/Shared/Views/Call/CallController.swift index fcd3a85584..1f28180e87 100644 --- a/apps/ios/Shared/Views/Call/CallController.swift +++ b/apps/ios/Shared/Views/Call/CallController.swift @@ -51,7 +51,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse func provider(_ provider: CXProvider, perform action: CXStartCallAction) { logger.debug("CallController.provider CXStartCallAction") - if callManager.startOutgoingCall(callUUID: action.callUUID) { + if callManager.startOutgoingCall(callUUID: action.callUUID.uuidString.lowercased()) { action.fulfill() provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil) } else { @@ -61,12 +61,30 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { logger.debug("CallController.provider CXAnswerCallAction") - if callManager.answerIncomingCall(callUUID: action.callUUID) { - // WebRTC call should be in connected state to fulfill. - // Otherwise no audio and mic working on lockscreen - fulfillOnConnect = action - } else { - action.fail() + Task { + let chatIsReady = await waitUntilChatStarted(timeoutMs: 30_000, stepMs: 500) + logger.debug("CallController chat started \(chatIsReady) \(ChatModel.shared.chatInitialized) \(ChatModel.shared.chatRunning == true) \(String(describing: AppChatState.shared.value))") + if !chatIsReady { + action.fail() + return + } + if !ChatModel.shared.callInvitations.values.contains(where: { inv in inv.callUUID == action.callUUID.uuidString.lowercased() }) { + try? await justRefreshCallInvitations() + logger.debug("CallController: updated call invitations chat") + } + await MainActor.run { + logger.debug("CallController.provider will answer on call") + + if callManager.answerIncomingCall(callUUID: action.callUUID.uuidString.lowercased()) { + logger.debug("CallController.provider answered on call") + // WebRTC call should be in connected state to fulfill. + // Otherwise no audio and mic working on lockscreen + fulfillOnConnect = action + } else { + logger.debug("CallController.provider will fail the call") + action.fail() + } + } } } @@ -75,7 +93,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse // Should be nil here if connection was in connected state fulfillOnConnect?.fail() fulfillOnConnect = nil - callManager.endCall(callUUID: action.callUUID) { ok in + callManager.endCall(callUUID: action.callUUID.uuidString.lowercased()) { ok in if ok { action.fulfill() } else { @@ -86,7 +104,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse } func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { - if callManager.enableMedia(media: .audio, enable: !action.isMuted, callUUID: action.callUUID) { + if callManager.enableMedia(source: .mic, enable: !action.isMuted, callUUID: action.callUUID.uuidString.lowercased()) { action.fulfill() } else { action.fail() @@ -103,7 +121,23 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession) RTCAudioSession.sharedInstance().isAudioEnabled = true do { - try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: .mixWithOthers) + let hasVideo = ChatModel.shared.activeCall?.hasVideo == true + if hasVideo { + try audioSession.setCategory(.playAndRecord, mode: .videoChat, options: [.defaultToSpeaker, .mixWithOthers, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP]) + } else { + try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.mixWithOthers, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP]) + } + // Without any delay sound is not playing from speaker or external device in incoming call + Task { + for i in 0 ... 3 { + try? await Task.sleep(nanoseconds: UInt64(i) * 300_000000) + if let preferred = audioSession.preferredInputDevice() { + await MainActor.run { try? audioSession.setPreferredInput(preferred) } + } else if hasVideo { + await MainActor.run { try? audioSession.overrideOutputAudioPort(.speaker) } + } + } + } logger.debug("audioSession category set") try audioSession.setActive(true) logger.debug("audioSession activated") @@ -130,7 +164,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse // The delay allows to accept the second call before suspending a chat // see `.onChange(of: scenePhase)` in SimpleXApp DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in - logger.debug("CallController: shouldSuspendChat \(String(describing: self?.shouldSuspendChat), privacy: .public)") + logger.debug("CallController: shouldSuspendChat \(String(describing: self?.shouldSuspendChat))") if ChatModel.shared.activeCall == nil && self?.shouldSuspendChat == true { self?.shouldSuspendChat = false suspendChat() @@ -140,48 +174,80 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse } } + private func waitUntilChatStarted(timeoutMs: UInt64, stepMs: UInt64) async -> Bool { + logger.debug("CallController waiting until chat started") + var t: UInt64 = 0 + repeat { + if ChatModel.shared.chatInitialized, ChatModel.shared.chatRunning == true, case .active = AppChatState.shared.value { + return true + } + _ = try? await Task.sleep(nanoseconds: stepMs * 1000000) + t += stepMs + } while t < timeoutMs + return false + } + @objc(pushRegistry:didUpdatePushCredentials:forType:) func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { - logger.debug("CallController: didUpdate push credentials for type \(type.rawValue, privacy: .public)") + logger.debug("CallController: didUpdate push credentials for type \(type.rawValue)") } func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) { - logger.debug("CallController: did receive push with type \(type.rawValue, privacy: .public)") + logger.debug("CallController: did receive push with type \(type.rawValue)") if type != .voIP { completion() return } - logger.debug("CallController: initializing chat") - if (!ChatModel.shared.chatInitialized) { - initChatAndMigrate(refreshInvitations: false) + if AppChatState.shared.value == .stopped { + self.reportExpiredCall(payload: payload, completion) + return } - startChatAndActivate(dispatchQueue: DispatchQueue.global()) { - self.shouldSuspendChat = true - // There are no invitations in the model, as it was processed by NSE - _ = try? justRefreshCallInvitations() - // logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))") - // Extract the call information from the push notification payload - let m = ChatModel.shared - if let contactId = payload.dictionaryPayload["contactId"] as? String, - let invitation = m.callInvitations[contactId] { - let update = self.cxCallUpdate(invitation: invitation) - if let uuid = invitation.callkitUUID { - logger.debug("CallController: report pushkit call via CallKit") - let update = self.cxCallUpdate(invitation: invitation) - self.provider.reportNewIncomingCall(with: uuid, update: update) { error in - if error != nil { - m.callInvitations.removeValue(forKey: contactId) - } - // Tell PushKit that the notification is handled. - completion() + // Extract the call information from the push notification payload + let m = ChatModel.shared + if let contactId = payload.dictionaryPayload["contactId"] as? String, + let displayName = payload.dictionaryPayload["displayName"] as? String, + let callUUID = payload.dictionaryPayload["callUUID"] as? String, + let uuid = UUID(uuidString: callUUID), + let callTsInterval = payload.dictionaryPayload["callTs"] as? TimeInterval, + let mediaStr = payload.dictionaryPayload["media"] as? String, + let media = CallMediaType(rawValue: mediaStr) { + let update = self.cxCallUpdate(contactId, displayName, media) + let callTs = Date(timeIntervalSince1970: callTsInterval) + if callTs.timeIntervalSinceNow >= -180 { + logger.debug("CallController: report pushkit call via CallKit") + self.provider.reportNewIncomingCall(with: uuid, update: update) { error in + if error != nil { + m.callInvitations.removeValue(forKey: contactId) } - } else { - self.reportExpiredCall(update: update, completion) + // Tell PushKit that the notification is handled. + completion() } } else { - self.reportExpiredCall(payload: payload, completion) + logger.debug("CallController will expire call 1") + self.reportExpiredCall(update: update, completion) + } + } else { + logger.debug("CallController will expire call 2") + self.reportExpiredCall(payload: payload, completion) + } + + //DispatchQueue.main.asyncAfter(deadline: .now() + 10) { + if (!ChatModel.shared.chatInitialized) { + logger.debug("CallController: initializing chat") + do { + try initializeChat(start: true, refreshInvitations: false) + } catch let error { + logger.error("CallController: initializing chat error: \(error)") + if let call = ChatModel.shared.activeCall { + self.endCall(call: call, completed: completion) + } + return } } + logger.debug("CallController: initialized chat") + startChatForCall() + logger.debug("CallController: started chat") + self.shouldSuspendChat = true } // This function fulfils the requirement to always report a call when PushKit notification is received, @@ -211,8 +277,8 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse } func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) { - logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callkitUUID), privacy: .public)") - if CallController.useCallKit(), let uuid = invitation.callkitUUID { + logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callUUID))") + if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) { if invitation.callTs.timeIntervalSinceNow >= -180 { let update = cxCallUpdate(invitation: invitation) provider.reportNewIncomingCall(with: uuid, update: update, completion: completion) @@ -233,6 +299,14 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse return update } + private func cxCallUpdate(_ contactId: String, _ displayName: String, _ media: CallMediaType) -> CXCallUpdate { + let update = CXCallUpdate() + update.remoteHandle = CXHandle(type: .generic, value: contactId) + update.hasVideo = media == .video + update.localizedCallerName = displayName + return update + } + func reportIncomingCall(call: Call, connectedAt dateConnected: Date?) { logger.debug("CallController: reporting incoming call connected") if CallController.useCallKit() { @@ -244,14 +318,14 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse func reportOutgoingCall(call: Call, connectedAt dateConnected: Date?) { logger.debug("CallController: reporting outgoing call connected") - if CallController.useCallKit(), let uuid = call.callkitUUID { + if CallController.useCallKit(), let callUUID = call.callUUID, let uuid = UUID(uuidString: callUUID) { provider.reportOutgoingCall(with: uuid, connectedAt: dateConnected) } } func reportCallRemoteEnded(invitation: RcvCallInvitation) { logger.debug("CallController: reporting remote ended") - if CallController.useCallKit(), let uuid = invitation.callkitUUID { + if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) { provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded) } else if invitation.contact.id == activeCallInvitation?.contact.id { activeCallInvitation = nil @@ -260,14 +334,17 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse func reportCallRemoteEnded(call: Call) { logger.debug("CallController: reporting remote ended") - if CallController.useCallKit(), let uuid = call.callkitUUID { + if CallController.useCallKit(), let callUUID = call.callUUID, let uuid = UUID(uuidString: callUUID) { provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded) } } func startCall(_ contact: Contact, _ media: CallMediaType) { logger.debug("CallController.startCall") - let uuid = callManager.newOutgoingCall(contact, media) + let callUUID = callManager.newOutgoingCall(contact, media) + guard let uuid = UUID(uuidString: callUUID) else { + return + } if CallController.useCallKit() { let handle = CXHandle(type: .generic, value: contact.id) let action = CXStartCallAction(call: uuid, handle: handle) @@ -279,19 +356,17 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse update.localizedCallerName = contact.displayName self.provider.reportCall(with: uuid, updated: update) } - } else if callManager.startOutgoingCall(callUUID: uuid) { - if callManager.startOutgoingCall(callUUID: uuid) { - logger.debug("CallController.startCall: call started") - } else { - logger.error("CallController.startCall: no active call") - } + } else if callManager.startOutgoingCall(callUUID: callUUID) { + logger.debug("CallController.startCall: call started") + } else { + logger.error("CallController.startCall: no active call") } } func answerCall(invitation: RcvCallInvitation) { logger.debug("CallController: answering a call") - if CallController.useCallKit(), let callUUID = invitation.callkitUUID { - requestTransaction(with: CXAnswerCallAction(call: callUUID)) + if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) { + requestTransaction(with: CXAnswerCallAction(call: uuid)) } else { callManager.answerIncomingCall(invitation: invitation) } @@ -300,10 +375,13 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse } } - func endCall(callUUID: UUID) { - logger.debug("CallController: ending the call with UUID \(callUUID.uuidString)") + func endCall(callUUID: String) { + let uuid = UUID(uuidString: callUUID) + logger.debug("CallController: ending the call with UUID \(callUUID)") if CallController.useCallKit() { - requestTransaction(with: CXEndCallAction(call: callUUID)) + if let uuid { + requestTransaction(with: CXEndCallAction(call: uuid)) + } } else { callManager.endCall(callUUID: callUUID) { ok in if ok { @@ -351,7 +429,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse private func requestTransaction(with action: CXAction, onSuccess: @escaping () -> Void = {}) { controller.request(CXTransaction(action: action)) { error in if let error = error { - logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription, privacy: .public)") + logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription)") } else { logger.debug("CallController.requestTransaction requested transaction successfully") onSuccess() diff --git a/apps/ios/Shared/Views/Call/CallManager.swift b/apps/ios/Shared/Views/Call/CallManager.swift index 194af3ab01..a3e1df2301 100644 --- a/apps/ios/Shared/Views/Call/CallManager.swift +++ b/apps/ios/Shared/Views/Call/CallManager.swift @@ -10,25 +10,25 @@ import Foundation import SimpleXChat class CallManager { - func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> UUID { - let uuid = UUID() - let call = Call(direction: .outgoing, contact: contact, callkitUUID: uuid, callState: .waitCapabilities, localMedia: media) + func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> String { + let uuid = UUID().uuidString.lowercased() + let call = Call(direction: .outgoing, contact: contact, callUUID: uuid, callState: .waitCapabilities, initialCallType: media) call.speakerEnabled = media == .video ChatModel.shared.activeCall = call return uuid } - func startOutgoingCall(callUUID: UUID) -> Bool { + func startOutgoingCall(callUUID: String) -> Bool { let m = ChatModel.shared - if let call = m.activeCall, call.callkitUUID == callUUID { + if let call = m.activeCall, call.callUUID == callUUID { m.showCallView = true - Task { await m.callCommand.processCommand(.capabilities(media: call.localMedia)) } + Task { await m.callCommand.processCommand(.capabilities(media: call.initialCallType)) } return true } return false } - func answerIncomingCall(callUUID: UUID) -> Bool { + func answerIncomingCall(callUUID: String) -> Bool { if let invitation = getCallInvitation(callUUID) { answerIncomingCall(invitation: invitation) return true @@ -42,9 +42,9 @@ class CallManager { let call = Call( direction: .incoming, contact: invitation.contact, - callkitUUID: invitation.callkitUUID, + callUUID: invitation.callUUID, callState: .invitationAccepted, - localMedia: invitation.callType.media, + initialCallType: invitation.callType.media, sharedKey: invitation.sharedKey ) call.speakerEnabled = invitation.callType.media == .video @@ -68,17 +68,17 @@ class CallManager { } } - func enableMedia(media: CallMediaType, enable: Bool, callUUID: UUID) -> Bool { - if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID { + func enableMedia(source: CallMediaSource, enable: Bool, callUUID: String) -> Bool { + if let call = ChatModel.shared.activeCall, call.callUUID == callUUID { let m = ChatModel.shared - Task { await m.callCommand.processCommand(.media(media: media, enable: enable)) } + Task { await m.callCommand.processCommand(.media(source: source, enable: enable)) } return true } return false } - func endCall(callUUID: UUID, completed: @escaping (Bool) -> Void) { - if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID { + func endCall(callUUID: String, completed: @escaping (Bool) -> Void) { + if let call = ChatModel.shared.activeCall, call.callUUID == callUUID { endCall(call: call) { completed(true) } } else if let invitation = getCallInvitation(callUUID) { endCall(invitation: invitation) { completed(true) } @@ -92,6 +92,7 @@ class CallManager { if case .ended = call.callState { logger.debug("CallManager.endCall: call ended") m.activeCall = nil + m.activeCallViewIsCollapsed = false m.showCallView = false completed() } else { @@ -100,6 +101,7 @@ class CallManager { await m.callCommand.processCommand(.end) await MainActor.run { m.activeCall = nil + m.activeCallViewIsCollapsed = false m.showCallView = false completed() } @@ -124,8 +126,8 @@ class CallManager { } } - private func getCallInvitation(_ callUUID: UUID) -> RcvCallInvitation? { - if let (_, invitation) = ChatModel.shared.callInvitations.first(where: { (_, inv) in inv.callkitUUID == callUUID }) { + private func getCallInvitation(_ callUUID: String) -> RcvCallInvitation? { + if let (_, invitation) = ChatModel.shared.callInvitations.first(where: { (_, inv) in inv.callUUID == callUUID }) { return invitation } return nil diff --git a/apps/ios/Shared/Views/Call/CallViewRenderers.swift b/apps/ios/Shared/Views/Call/CallViewRenderers.swift index 93766ced1c..e779093a24 100644 --- a/apps/ios/Shared/Views/Call/CallViewRenderers.swift +++ b/apps/ios/Shared/Views/Call/CallViewRenderers.swift @@ -6,70 +6,313 @@ import SwiftUI import WebRTC import SimpleXChat +import AVKit struct CallViewRemote: UIViewRepresentable { var client: WebRTCClient - var activeCall: Binding - - init(client: WebRTCClient, activeCall: Binding) { - self.client = client - self.activeCall = activeCall - } + @ObservedObject var call: Call + @State var enablePip: (Bool) -> Void = {_ in } + @Binding var activeCallViewIsCollapsed: Bool + @Binding var contentMode: UIView.ContentMode + @Binding var pipShown: Bool func makeUIView(context: Context) -> UIView { let view = UIView() - if let call = activeCall.wrappedValue { - let remoteRenderer = RTCMTLVideoView(frame: view.frame) - remoteRenderer.videoContentMode = .scaleAspectFill - client.addRemoteRenderer(call, remoteRenderer) - addSubviewAndResize(remoteRenderer, into: view) + let remoteCameraRenderer = RTCMTLVideoView(frame: view.frame) + remoteCameraRenderer.videoContentMode = contentMode + remoteCameraRenderer.tag = 0 + + let screenVideo = call.peerMediaSources.screenVideo + let remoteScreenRenderer = RTCMTLVideoView(frame: view.frame) + remoteScreenRenderer.videoContentMode = contentMode + remoteScreenRenderer.tag = 1 + remoteScreenRenderer.alpha = screenVideo ? 1 : 0 + + context.coordinator.cameraRenderer = remoteCameraRenderer + context.coordinator.screenRenderer = remoteScreenRenderer + client.addRemoteCameraRenderer(remoteCameraRenderer) + client.addRemoteScreenRenderer(remoteScreenRenderer) + if screenVideo { + addSubviewAndResize(remoteScreenRenderer, remoteCameraRenderer, into: view) + } else { + addSubviewAndResize(remoteCameraRenderer, remoteScreenRenderer, into: view) + } + + if AVPictureInPictureController.isPictureInPictureSupported() { + makeViewWithRTCRenderer(remoteCameraRenderer, remoteScreenRenderer, view, context) } return view } + + func makeViewWithRTCRenderer(_ remoteCameraRenderer: RTCMTLVideoView, _ remoteScreenRenderer: RTCMTLVideoView, _ view: UIView, _ context: Context) { + let pipRemoteCameraRenderer = RTCMTLVideoView(frame: view.frame) + pipRemoteCameraRenderer.videoContentMode = .scaleAspectFill + + let pipRemoteScreenRenderer = RTCMTLVideoView(frame: view.frame) + pipRemoteScreenRenderer.videoContentMode = .scaleAspectFill + + let pipVideoCallViewController = AVPictureInPictureVideoCallViewController() + pipVideoCallViewController.preferredContentSize = CGSize(width: 1080, height: 1920) + let pipContentSource = AVPictureInPictureController.ContentSource( + activeVideoCallSourceView: view, + contentViewController: pipVideoCallViewController + ) + + let pipController = AVPictureInPictureController(contentSource: pipContentSource) + pipController.canStartPictureInPictureAutomaticallyFromInline = true + pipController.delegate = context.coordinator + context.coordinator.pipController = pipController + context.coordinator.willShowHide = { show in + if show { + client.addRemoteCameraRenderer(pipRemoteCameraRenderer) + client.addRemoteScreenRenderer(pipRemoteScreenRenderer) + context.coordinator.relayout() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + activeCallViewIsCollapsed = true + } + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + activeCallViewIsCollapsed = false + } + } + } + context.coordinator.didShowHide = { show in + if show { + remoteCameraRenderer.isHidden = true + remoteScreenRenderer.isHidden = true + } else { + client.removeRemoteCameraRenderer(pipRemoteCameraRenderer) + client.removeRemoteScreenRenderer(pipRemoteScreenRenderer) + remoteCameraRenderer.isHidden = false + remoteScreenRenderer.isHidden = false + } + pipShown = show + } + context.coordinator.relayout = { + let camera = call.peerMediaSources.camera + let screenVideo = call.peerMediaSources.screenVideo + pipRemoteCameraRenderer.alpha = camera ? 1 : 0 + pipRemoteScreenRenderer.alpha = screenVideo ? 1 : 0 + if screenVideo { + addSubviewAndResize(pipRemoteScreenRenderer, pipRemoteCameraRenderer, pip: true, into: pipVideoCallViewController.view) + } else { + addSubviewAndResize(pipRemoteCameraRenderer, pipRemoteScreenRenderer, pip: true, into: pipVideoCallViewController.view) + } + (pipVideoCallViewController.view.subviews[0] as! RTCMTLVideoView).videoContentMode = contentMode + (pipVideoCallViewController.view.subviews[1] as! RTCMTLVideoView).videoContentMode = .scaleAspectFill + } + DispatchQueue.main.async { + enablePip = { enable in + if enable != pipShown /* pipController.isPictureInPictureActive */ { + if enable { + pipController.startPictureInPicture() + } else { + pipController.stopPictureInPicture() + } + } + } + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(client) + } func updateUIView(_ view: UIView, context: Context) { logger.debug("CallView.updateUIView remote") + let camera = view.subviews.first(where: { $0.tag == 0 })! + let screen = view.subviews.first(where: { $0.tag == 1 })! + let screenVideo = call.peerMediaSources.screenVideo + if screenVideo && screen.alpha == 0 { + screen.alpha = 1 + addSubviewAndResize(screen, camera, into: view) + } else if !screenVideo && screen.alpha == 1 { + screen.alpha = 0 + addSubviewAndResize(camera, screen, into: view) + } + (view.subviews[0] as! RTCMTLVideoView).videoContentMode = contentMode + (view.subviews[1] as! RTCMTLVideoView).videoContentMode = .scaleAspectFill + + camera.alpha = call.peerMediaSources.camera ? 1 : 0 + screen.alpha = call.peerMediaSources.screenVideo ? 1 : 0 + + DispatchQueue.main.async { + if activeCallViewIsCollapsed != pipShown { + enablePip(activeCallViewIsCollapsed) + } else if pipShown { + context.coordinator.relayout() + } + } + } + + // MARK: - Coordinator + class Coordinator: NSObject, AVPictureInPictureControllerDelegate { + var cameraRenderer: RTCMTLVideoView? + var screenRenderer: RTCMTLVideoView? + var client: WebRTCClient + var pipController: AVPictureInPictureController? = nil + var willShowHide: (Bool) -> Void = { _ in } + var didShowHide: (Bool) -> Void = { _ in } + var relayout: () -> Void = {} + + required init(_ client: WebRTCClient) { + self.client = client + } + + func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + willShowHide(true) + } + + func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + didShowHide(true) + } + + func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) { + logger.error("PiP failed to start: \(error.localizedDescription)") + } + + func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + willShowHide(false) + } + + func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + didShowHide(false) + } + + deinit { + // TODO: deinit is not called when changing call type from audio to video and back, + // which causes many renderers can be created and added to stream (if enabling/disabling + // video while not yet connected in outgoing call) + pipController?.stopPictureInPicture() + pipController?.canStartPictureInPictureAutomaticallyFromInline = false + pipController?.contentSource = nil + pipController?.delegate = nil + pipController = nil + if let cameraRenderer { + client.removeRemoteCameraRenderer(cameraRenderer) + } + if let screenRenderer { + client.removeRemoteScreenRenderer(screenRenderer) + } + } + } + + class SampleBufferVideoCallView: UIView { + override class var layerClass: AnyClass { + get { return AVSampleBufferDisplayLayer.self } + } + + var sampleBufferDisplayLayer: AVSampleBufferDisplayLayer { + return layer as! AVSampleBufferDisplayLayer + } } } struct CallViewLocal: UIViewRepresentable { var client: WebRTCClient - var activeCall: Binding var localRendererAspectRatio: Binding + @State var pipStateChanged: (Bool) -> Void = {_ in } + @Binding var pipShown: Bool - init(client: WebRTCClient, activeCall: Binding, localRendererAspectRatio: Binding) { + init(client: WebRTCClient, localRendererAspectRatio: Binding, pipShown: Binding) { self.client = client - self.activeCall = activeCall self.localRendererAspectRatio = localRendererAspectRatio + self._pipShown = pipShown } func makeUIView(context: Context) -> UIView { let view = UIView() - if let call = activeCall.wrappedValue { - let localRenderer = RTCEAGLVideoView(frame: .zero) - client.addLocalRenderer(call, localRenderer) - client.startCaptureLocalVideo(call) - addSubviewAndResize(localRenderer, into: view) + let localRenderer = RTCEAGLVideoView(frame: .zero) + context.coordinator.renderer = localRenderer + client.addLocalRenderer(localRenderer) + addSubviewAndResize(localRenderer, nil, into: view) + DispatchQueue.main.async { + pipStateChanged = { shown in + localRenderer.isHidden = shown + } } return view } + func makeCoordinator() -> Coordinator { + Coordinator(client) + } + func updateUIView(_ view: UIView, context: Context) { logger.debug("CallView.updateUIView local") + pipStateChanged(pipShown) + } + + // MARK: - Coordinator + class Coordinator: NSObject, AVPictureInPictureControllerDelegate { + var renderer: RTCEAGLVideoView? + var client: WebRTCClient + + required init(_ client: WebRTCClient) { + self.client = client + } + + deinit { + if let renderer { + client.removeLocalRenderer(renderer) + } + } } } -private func addSubviewAndResize(_ view: UIView, into containerView: UIView) { - containerView.addSubview(view) - view.translatesAutoresizingMaskIntoConstraints = false - containerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view]|", - options: [], - metrics: nil, - views: ["view": view])) +private func addSubviewAndResize(_ fullscreen: UIView, _ end: UIView?, pip: Bool = false, into containerView: UIView) { + if containerView.subviews.firstIndex(of: fullscreen) == 0 && ((end == nil && containerView.subviews.count == 1) || (end != nil && containerView.subviews.firstIndex(of: end!) == 1)) { + // Nothing to do, elements on their places + return + } + containerView.removeConstraints(containerView.constraints) + containerView.subviews.forEach { sub in sub.removeFromSuperview()} - containerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view]|", + containerView.addSubview(fullscreen) + fullscreen.translatesAutoresizingMaskIntoConstraints = false + fullscreen.layer.cornerRadius = 0 + fullscreen.layer.masksToBounds = false + + if let end { + containerView.addSubview(end) + end.translatesAutoresizingMaskIntoConstraints = false + end.layer.cornerRadius = pip ? 8 : 10 + end.layer.masksToBounds = true + } + + let constraintFullscreenV = NSLayoutConstraint.constraints( + withVisualFormat: "V:|[fullscreen]|", options: [], metrics: nil, - views: ["view": view])) + views: ["fullscreen": fullscreen] + ) + let constraintFullscreenH = NSLayoutConstraint.constraints( + withVisualFormat: "H:|[fullscreen]|", + options: [], + metrics: nil, + views: ["fullscreen": fullscreen] + ) + + containerView.addConstraints(constraintFullscreenV) + containerView.addConstraints(constraintFullscreenH) + + if let end { + let constraintEndWidth = NSLayoutConstraint( + item: end, attribute: .width, relatedBy: .equal, toItem: containerView, attribute: .width, multiplier: pip ? 0.5 : 0.3, constant: 0 + ) + let constraintEndHeight = NSLayoutConstraint( + item: end, attribute: .height, relatedBy: .equal, toItem: containerView, attribute: .width, multiplier: pip ? 0.5 * 1.33 : 0.3 * 1.33, constant: 0 + ) + let constraintEndX = NSLayoutConstraint( + item: end, attribute: .leading, relatedBy: .equal, toItem: containerView, attribute: .trailing, multiplier: pip ? 0.5 : 0.7, constant: pip ? -8 : -17 + ) + let constraintEndY = NSLayoutConstraint( + item: end, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: pip ? -8 : -92 + ) + containerView.addConstraint(constraintEndWidth) + containerView.addConstraint(constraintEndHeight) + containerView.addConstraint(constraintEndX) + containerView.addConstraint(constraintEndY) + } containerView.layoutIfNeeded() } diff --git a/apps/ios/Shared/Views/Call/IncomingCallView.swift b/apps/ios/Shared/Views/Call/IncomingCallView.swift index c2d5dabd48..5479a9fada 100644 --- a/apps/ios/Shared/Views/Call/IncomingCallView.swift +++ b/apps/ios/Shared/Views/Call/IncomingCallView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct IncomingCallView: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @ObservedObject var cc = CallController.shared var body: some View { @@ -30,21 +31,21 @@ struct IncomingCallView: View { VStack(alignment: .leading, spacing: 6) { HStack { if m.users.count > 1 { - ProfileImage(imageStr: invitation.user.image, color: .white) - .frame(width: 24, height: 24) + ProfileImage(imageStr: invitation.user.image, size: 24, color: .white) } Image(systemName: invitation.callType.media == .video ? "video.fill" : "phone.fill").foregroundColor(.green) Text(invitation.callTypeText) } HStack { ProfilePreview(profileOf: invitation.contact, color: .white) + .padding(.vertical, 6) Spacer() callButton("Reject", "phone.down.fill", .red) { cc.endCall(invitation: invitation) } - callButton("Ignore", "multiply", .accentColor) { + callButton("Ignore", "multiply", .primary) { cc.activeCallInvitation = nil } @@ -64,7 +65,7 @@ struct IncomingCallView: View { .padding(.horizontal, 16) .padding(.vertical, 12) .frame(maxWidth: .infinity) - .background(Color(uiColor: .tertiarySystemGroupedBackground)) + .modifier(ThemedBackground()) .onAppear { dismissAllSheets() } } @@ -77,7 +78,7 @@ struct IncomingCallView: View { .frame(width: 24, height: 24) Text(text) .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } .frame(minWidth: 44) }) diff --git a/apps/ios/Shared/Views/Call/SoundPlayer.swift b/apps/ios/Shared/Views/Call/SoundPlayer.swift index 17c13ab403..c7803a0cb8 100644 --- a/apps/ios/Shared/Views/Call/SoundPlayer.swift +++ b/apps/ios/Shared/Views/Call/SoundPlayer.swift @@ -8,6 +8,7 @@ import Foundation import AVFoundation +import UIKit class SoundPlayer { static let shared = SoundPlayer() @@ -43,3 +44,63 @@ class SoundPlayer { audioPlayer = nil } } + +class CallSoundsPlayer { + static let shared = CallSoundsPlayer() + private var audioPlayer: AVAudioPlayer? + private var playerTask: Task = Task {} + + private func start(_ soundName: String, delayMs: Double) { + audioPlayer?.stop() + playerTask.cancel() + logger.debug("start \(soundName)") + guard let path = Bundle.main.path(forResource: soundName, ofType: "mp3", inDirectory: "sounds") else { + logger.debug("start: file not found") + return + } + do { + let player = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path)) + if player.prepareToPlay() { + audioPlayer = player + } + } catch { + logger.debug("start: AVAudioPlayer error \(error.localizedDescription)") + } + + playerTask = Task { + while let player = audioPlayer { + player.play() + do { + try await Task.sleep(nanoseconds: UInt64((player.duration * 1_000_000_000) + delayMs * 1_000_000)) + } catch { + break + } + } + } + } + + func startConnectingCallSound() { + start("connecting_call", delayMs: 0) + } + + func startInCallSound() { + // Taken from https://github.com/TelegramOrg/Telegram-Android + // https://github.com/TelegramOrg/Telegram-Android/blob/master/LICENSE + start("in_call", delayMs: 1000) + } + + func stop() { + playerTask.cancel() + audioPlayer?.stop() + audioPlayer = nil + } + + func vibrate(long: Bool) { + // iOS just don't want to vibrate more than once after a short period of time, and all 'styles' feel the same + if long { + AudioServicesPlayAlertSound(kSystemSoundID_Vibrate) + } else { + UIImpactFeedbackGenerator(style: .heavy).impactOccurred() + } + } +} diff --git a/apps/ios/Shared/Views/Call/WebRTC.swift b/apps/ios/Shared/Views/Call/WebRTC.swift index c21ef5019a..ef9135761c 100644 --- a/apps/ios/Shared/Views/Call/WebRTC.swift +++ b/apps/ios/Shared/Views/Call/WebRTC.swift @@ -18,47 +18,49 @@ class Call: ObservableObject, Equatable { var direction: CallDirection var contact: Contact - var callkitUUID: UUID? - var localMedia: CallMediaType + var callUUID: String? + var initialCallType: CallMediaType + @Published var localMediaSources: CallMediaSources @Published var callState: CallState @Published var localCapabilities: CallCapabilities? - @Published var peerMedia: CallMediaType? + @Published var peerMediaSources: CallMediaSources = CallMediaSources() @Published var sharedKey: String? - @Published var audioEnabled = true @Published var speakerEnabled = false - @Published var videoEnabled: Bool @Published var connectionInfo: ConnectionInfo? + @Published var connectedAt: Date? = nil init( direction: CallDirection, contact: Contact, - callkitUUID: UUID?, + callUUID: String?, callState: CallState, - localMedia: CallMediaType, + initialCallType: CallMediaType, sharedKey: String? = nil ) { self.direction = direction self.contact = contact - self.callkitUUID = callkitUUID + self.callUUID = callUUID self.callState = callState - self.localMedia = localMedia + self.initialCallType = initialCallType self.sharedKey = sharedKey - self.videoEnabled = localMedia == .video + self.localMediaSources = CallMediaSources( + mic: AVCaptureDevice.authorizationStatus(for: .audio) == .authorized, + camera: initialCallType == .video && AVCaptureDevice.authorizationStatus(for: .video) == .authorized) } var encrypted: Bool { get { localEncrypted && sharedKey != nil } } - var localEncrypted: Bool { get { localCapabilities?.encryption ?? false } } + private var localEncrypted: Bool { get { localCapabilities?.encryption ?? false } } var encryptionStatus: LocalizedStringKey { get { switch callState { case .waitCapabilities: return "" case .invitationSent: return localEncrypted ? "e2e encrypted" : "no e2e encryption" case .invitationAccepted: return sharedKey == nil ? "contact has no e2e encryption" : "contact has e2e encryption" - default: return !localEncrypted ? "no e2e encryption" : sharedKey == nil ? "contact has no e2e encryption" : "e2e encrypted" + default: return !localEncrypted ? "no e2e encryption" : sharedKey == nil ? "contact has no e2e encryption" : "e2e encrypted" } } } - var hasMedia: Bool { get { callState == .offerSent || callState == .negotiated || callState == .connected } } + var hasVideo: Bool { get { localMediaSources.hasVideo || peerMediaSources.hasVideo } } } enum CallDirection { @@ -103,18 +105,28 @@ struct WVAPIMessage: Equatable, Decodable, Encodable { var command: WCallCommand? } +struct CallMediaSources: Equatable, Codable { + var mic: Bool = false + var camera: Bool = false + var screenAudio: Bool = false + var screenVideo: Bool = false + + var hasVideo: Bool { get { camera || screenVideo } } +} + enum WCallCommand: Equatable, Encodable, Decodable { case capabilities(media: CallMediaType) case start(media: CallMediaType, aesKey: String? = nil, iceServers: [RTCIceServer]? = nil, relay: Bool? = nil) case offer(offer: String, iceCandidates: String, media: CallMediaType, aesKey: String? = nil, iceServers: [RTCIceServer]? = nil, relay: Bool? = nil) case answer(answer: String, iceCandidates: String) case ice(iceCandidates: String) - case media(media: CallMediaType, enable: Bool) + case media(source: CallMediaSource, enable: Bool) case end enum CodingKeys: String, CodingKey { case type case media + case source case aesKey case offer case answer @@ -165,9 +177,9 @@ enum WCallCommand: Equatable, Encodable, Decodable { case let .ice(iceCandidates): try container.encode("ice", forKey: .type) try container.encode(iceCandidates, forKey: .iceCandidates) - case let .media(media, enable): + case let .media(source, enable): try container.encode("media", forKey: .type) - try container.encode(media, forKey: .media) + try container.encode(source, forKey: .media) try container.encode(enable, forKey: .enable) case .end: try container.encode("end", forKey: .type) @@ -203,9 +215,9 @@ enum WCallCommand: Equatable, Encodable, Decodable { let iceCandidates = try container.decode(String.self, forKey: CodingKeys.iceCandidates) self = .ice(iceCandidates: iceCandidates) case "media": - let media = try container.decode(CallMediaType.self, forKey: CodingKeys.media) + let source = try container.decode(CallMediaSource.self, forKey: CodingKeys.source) let enable = try container.decode(Bool.self, forKey: CodingKeys.enable) - self = .media(media: media, enable: enable) + self = .media(source: source, enable: enable) case "end": self = .end default: @@ -222,6 +234,7 @@ enum WCallResponse: Equatable, Decodable { case ice(iceCandidates: String) case connection(state: ConnectionState) case connected(connectionInfo: ConnectionInfo) + case peerMedia(source: CallMediaSource, enabled: Bool) case ended case ok case error(message: String) @@ -236,6 +249,8 @@ enum WCallResponse: Equatable, Decodable { case state case connectionInfo case message + case source + case enabled } var respType: String { @@ -247,6 +262,7 @@ enum WCallResponse: Equatable, Decodable { case .ice: return "ice" case .connection: return "connection" case .connected: return "connected" + case .peerMedia: return "peerMedia" case .ended: return "ended" case .ok: return "ok" case .error: return "error" @@ -281,6 +297,10 @@ enum WCallResponse: Equatable, Decodable { case "connected": let connectionInfo = try container.decode(ConnectionInfo.self, forKey: CodingKeys.connectionInfo) self = .connected(connectionInfo: connectionInfo) + case "peerMedia": + let source = try container.decode(CallMediaSource.self, forKey: CodingKeys.source) + let enabled = try container.decode(Bool.self, forKey: CodingKeys.enabled) + self = .peerMedia(source: source, enabled: enabled) case "ended": self = .ended case "ok": @@ -322,6 +342,10 @@ extension WCallResponse: Encodable { case let .connected(connectionInfo): try container.encode("connected", forKey: .type) try container.encode(connectionInfo, forKey: .connectionInfo) + case let .peerMedia(source, enabled): + try container.encode("peerMedia", forKey: .type) + try container.encode(source, forKey: .source) + try container.encode(enabled, forKey: .enabled) case .ended: try container.encode("ended", forKey: .type) case .ok: @@ -374,7 +398,7 @@ actor WebRTCCommandProcessor { func shouldRunCommand(_ client: WebRTCClient, _ c: WCallCommand) -> Bool { switch c { case .capabilities, .start, .offer, .end: true - default: client.activeCall.wrappedValue != nil + default: client.activeCall != nil } } } @@ -429,17 +453,18 @@ struct RTCIceServer: Codable, Equatable { } // the servers are expected in this format: -// stun:stun.simplex.im:443?transport=tcp -// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443?transport=tcp +// stuns:stun.simplex.im:443?transport=tcp +// turns:private2:Hxuq2QxUjnhj96Zq2r4HjqHRj@turn.simplex.im:443?transport=tcp func parseRTCIceServer(_ str: String) -> RTCIceServer? { var s = replaceScheme(str, "stun:") + s = replaceScheme(s, "stuns:") s = replaceScheme(s, "turn:") s = replaceScheme(s, "turns:") if let u: URL = URL(string: s), let scheme = u.scheme, let host = u.host, let port = u.port, - u.path == "" && (scheme == "stun" || scheme == "turn" || scheme == "turns") { + u.path == "" && (scheme == "stun" || scheme == "stuns" || scheme == "turn" || scheme == "turns") { let query = u.query == nil || u.query == "" ? "" : "?" + (u.query ?? "") return RTCIceServer( urls: ["\(scheme):\(host):\(port)\(query)"], diff --git a/apps/ios/Shared/Views/Call/WebRTCClient.swift b/apps/ios/Shared/Views/Call/WebRTCClient.swift index 933a3c745e..db7910836e 100644 --- a/apps/ios/Shared/Views/Call/WebRTCClient.swift +++ b/apps/ios/Shared/Views/Call/WebRTCClient.swift @@ -23,15 +23,24 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg struct Call { var connection: RTCPeerConnection var iceCandidates: IceCandidates - var localMedia: CallMediaType var localCamera: RTCVideoCapturer? - var localVideoSource: RTCVideoSource? - var localStream: RTCVideoTrack? - var remoteStream: RTCVideoTrack? - var device: AVCaptureDevice.Position = .front + var localAudioTrack: RTCAudioTrack? + var localVideoTrack: RTCVideoTrack? + var remoteAudioTrack: RTCAudioTrack? + var remoteVideoTrack: RTCVideoTrack? + var remoteScreenAudioTrack: RTCAudioTrack? + var remoteScreenVideoTrack: RTCVideoTrack? + var device: AVCaptureDevice.Position var aesKey: String? var frameEncryptor: RTCFrameEncryptor? var frameDecryptor: RTCFrameDecryptor? + var peerHasOldVersion: Bool + } + + struct NotConnectedCall { + var audioTrack: RTCAudioTrack? + var localCameraAndTrack: (RTCVideoCapturer, RTCVideoTrack)? + var device: AVCaptureDevice.Position = .front } actor IceCandidates { @@ -49,68 +58,77 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg } private let rtcAudioSession = RTCAudioSession.sharedInstance() - private let audioQueue = DispatchQueue(label: "audio") + private let audioQueue = DispatchQueue(label: "chat.simplex.app.audio") private var sendCallResponse: (WVAPIMessage) async -> Void - var activeCall: Binding + var activeCall: Call? + var notConnectedCall: NotConnectedCall? private var localRendererAspectRatio: Binding + var cameraRenderers: [RTCVideoRenderer] = [] + var screenRenderers: [RTCVideoRenderer] = [] + @available(*, unavailable) override init() { fatalError("Unimplemented") } - required init(_ activeCall: Binding, _ sendCallResponse: @escaping (WVAPIMessage) async -> Void, _ localRendererAspectRatio: Binding) { + required init(_ sendCallResponse: @escaping (WVAPIMessage) async -> Void, _ localRendererAspectRatio: Binding) { self.sendCallResponse = sendCallResponse - self.activeCall = activeCall self.localRendererAspectRatio = localRendererAspectRatio rtcAudioSession.useManualAudio = CallController.useCallKit() rtcAudioSession.isAudioEnabled = !CallController.useCallKit() - logger.debug("WebRTCClient: rtcAudioSession has manual audio \(self.rtcAudioSession.useManualAudio) and audio enabled \(self.rtcAudioSession.isAudioEnabled)}") + logger.debug("WebRTCClient: rtcAudioSession has manual audio \(self.rtcAudioSession.useManualAudio) and audio enabled \(self.rtcAudioSession.isAudioEnabled)") super.init() } let defaultIceServers: [WebRTC.RTCIceServer] = [ - WebRTC.RTCIceServer(urlStrings: ["stun:stun.simplex.im:443"]), - WebRTC.RTCIceServer(urlStrings: ["turn:turn.simplex.im:443?transport=udp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"), - WebRTC.RTCIceServer(urlStrings: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"), + WebRTC.RTCIceServer(urlStrings: ["stuns:stun.simplex.im:443"]), + //WebRTC.RTCIceServer(urlStrings: ["turns:turn.simplex.im:443?transport=udp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj"), + WebRTC.RTCIceServer(urlStrings: ["turns:turn.simplex.im:443?transport=tcp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj"), ] func initializeCall(_ iceServers: [WebRTC.RTCIceServer]?, _ mediaType: CallMediaType, _ aesKey: String?, _ relay: Bool?) -> Call { let connection = createPeerConnection(iceServers ?? getWebRTCIceServers() ?? defaultIceServers, relay) connection.delegate = self - createAudioSender(connection) - var localStream: RTCVideoTrack? = nil - var remoteStream: RTCVideoTrack? = nil + let device = notConnectedCall?.device ?? .front var localCamera: RTCVideoCapturer? = nil - var localVideoSource: RTCVideoSource? = nil - if mediaType == .video { - (localStream, remoteStream, localCamera, localVideoSource) = createVideoSender(connection) + var localAudioTrack: RTCAudioTrack? = nil + var localVideoTrack: RTCVideoTrack? = nil + if let localCameraAndTrack = notConnectedCall?.localCameraAndTrack { + (localCamera, localVideoTrack) = localCameraAndTrack + } else if notConnectedCall == nil && mediaType == .video { + (localCamera, localVideoTrack) = createVideoTrackAndStartCapture(device) } + if let audioTrack = notConnectedCall?.audioTrack { + localAudioTrack = audioTrack + } else if notConnectedCall == nil { + localAudioTrack = createAudioTrack() + } + notConnectedCall?.localCameraAndTrack = nil + notConnectedCall?.audioTrack = nil + var frameEncryptor: RTCFrameEncryptor? = nil var frameDecryptor: RTCFrameDecryptor? = nil if aesKey != nil { let encryptor = RTCFrameEncryptor.init(sizeChange: Int32(WebRTCClient.ivTagBytes)) encryptor.delegate = self frameEncryptor = encryptor - connection.senders.forEach { $0.setRtcFrameEncryptor(encryptor) } let decryptor = RTCFrameDecryptor.init(sizeChange: -Int32(WebRTCClient.ivTagBytes)) decryptor.delegate = self frameDecryptor = decryptor - // Has no video receiver in outgoing call if applied here, see [peerConnection(_ connection: RTCPeerConnection, didChange newState] - // connection.receivers.forEach { $0.setRtcFrameDecryptor(decryptor) } } return Call( connection: connection, iceCandidates: IceCandidates(), - localMedia: mediaType, localCamera: localCamera, - localVideoSource: localVideoSource, - localStream: localStream, - remoteStream: remoteStream, + localAudioTrack: localAudioTrack, + localVideoTrack: localVideoTrack, + device: device, aesKey: aesKey, frameEncryptor: frameEncryptor, - frameDecryptor: frameDecryptor + frameDecryptor: frameDecryptor, + peerHasOldVersion: false ) } @@ -151,18 +169,24 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg func sendCallCommand(command: WCallCommand) async { var resp: WCallResponse? = nil - let pc = activeCall.wrappedValue?.connection + let pc = activeCall?.connection switch command { - case .capabilities: + case let .capabilities(media): // outgoing + let localCameraAndTrack: (RTCVideoCapturer, RTCVideoTrack)? = media == .video + ? createVideoTrackAndStartCapture(.front) + : nil + notConnectedCall = NotConnectedCall(audioTrack: createAudioTrack(), localCameraAndTrack: localCameraAndTrack, device: .front) resp = .capabilities(capabilities: CallCapabilities(encryption: WebRTCClient.enableEncryption)) - case let .start(media: media, aesKey, iceServers, relay): + case let .start(media: media, aesKey, iceServers, relay): // incoming logger.debug("starting incoming call - create webrtc session") - if activeCall.wrappedValue != nil { endCall() } + if activeCall != nil { endCall() } let encryption = WebRTCClient.enableEncryption let call = initializeCall(iceServers?.toWebRTCIceServers(), media, encryption ? aesKey : nil, relay) - activeCall.wrappedValue = call + activeCall = call + setupLocalTracks(true, call) let (offer, error) = await call.connection.offer() if let offer = offer { + setupEncryptionForLocalTracks(call) resp = .offer( offer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: offer.type.toSdpType(), sdp: offer.sdp))), iceCandidates: compressToBase64(input: encodeJSON(await self.getInitialIceCandidates())), @@ -172,18 +196,24 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg } else { resp = .error(message: "offer error: \(error?.localizedDescription ?? "unknown error")") } - case let .offer(offer, iceCandidates, media, aesKey, iceServers, relay): - if activeCall.wrappedValue != nil { + case let .offer(offer, iceCandidates, media, aesKey, iceServers, relay): // outgoing + if activeCall != nil { resp = .error(message: "accept: call already started") } else if !WebRTCClient.enableEncryption && aesKey != nil { resp = .error(message: "accept: encryption is not supported") } else if let offer: CustomRTCSessionDescription = decodeJSON(decompressFromBase64(input: offer)), let remoteIceCandidates: [RTCIceCandidate] = decodeJSON(decompressFromBase64(input: iceCandidates)) { let call = initializeCall(iceServers?.toWebRTCIceServers(), media, WebRTCClient.enableEncryption ? aesKey : nil, relay) - activeCall.wrappedValue = call + activeCall = call let pc = call.connection if let type = offer.type, let sdp = offer.sdp { if (try? await pc.setRemoteDescription(RTCSessionDescription(type: type.toWebRTCSdpType(), sdp: sdp))) != nil { + setupLocalTracks(false, call) + setupEncryptionForLocalTracks(call) + pc.transceivers.forEach { transceiver in + transceiver.setDirection(.sendRecv, error: nil) + } + await adaptToOldVersion(pc.transceivers.count <= 2) let (answer, error) = await pc.answer() if let answer = answer { self.addIceCandidates(pc, remoteIceCandidates) @@ -200,7 +230,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg } } } - case let .answer(answer, iceCandidates): + case let .answer(answer, iceCandidates): // incoming if pc == nil { resp = .error(message: "answer: call not started") } else if pc?.localDescription == nil { @@ -212,6 +242,9 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg let type = answer.type, let sdp = answer.sdp, let pc = pc { if (try? await pc.setRemoteDescription(RTCSessionDescription(type: type.toWebRTCSdpType(), sdp: sdp))) != nil { + var currentDirection: RTCRtpTransceiverDirection = .sendOnly + pc.transceivers[2].currentDirection(¤tDirection) + await adaptToOldVersion(currentDirection == .sendOnly) addIceCandidates(pc, remoteIceCandidates) resp = .ok } else { @@ -226,13 +259,11 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg } else { resp = .error(message: "ice: call not started") } - case let .media(media, enable): - if activeCall.wrappedValue == nil { + case let .media(source, enable): + if activeCall == nil { resp = .error(message: "media: call not started") - } else if activeCall.wrappedValue?.localMedia == .audio && media == .video { - resp = .error(message: "media: no video") } else { - enableMedia(media, enable) + await enableMedia(source, enable) resp = .ok } case .end: @@ -247,7 +278,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg func getInitialIceCandidates() async -> [RTCIceCandidate] { await untilIceComplete(timeoutMs: 750, stepMs: 150) {} - let candidates = await activeCall.wrappedValue?.iceCandidates.getAndClear() ?? [] + let candidates = await activeCall?.iceCandidates.getAndClear() ?? [] logger.debug("WebRTCClient: sending initial ice candidates: \(candidates.count)") return candidates } @@ -255,7 +286,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg func waitForMoreIceCandidates() { Task { await untilIceComplete(timeoutMs: 12000, stepMs: 1500) { - let candidates = await self.activeCall.wrappedValue?.iceCandidates.getAndClear() ?? [] + let candidates = await self.activeCall?.iceCandidates.getAndClear() ?? [] if candidates.count > 0 { logger.debug("WebRTCClient: sending more ice candidates: \(candidates.count)") await self.sendIceCandidates(candidates) @@ -272,25 +303,202 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg ) } - func enableMedia(_ media: CallMediaType, _ enable: Bool) { - logger.debug("WebRTCClient: enabling media \(media.rawValue) \(enable)") - media == .video ? setVideoEnabled(enable) : setAudioEnabled(enable) + func setupMuteUnmuteListener(_ transceiver: RTCRtpTransceiver, _ track: RTCMediaStreamTrack) { + // logger.log("Setting up mute/unmute listener in the call without encryption for mid = \(transceiver.mid)") + Task { + var lastBytesReceived: Int64 = 0 + // muted initially + var mutedSeconds = 4 + while let call = self.activeCall, transceiver.receiver.track?.readyState == .live { + let stats: RTCStatisticsReport = await call.connection.statistics(for: transceiver.receiver) + let stat = stats.statistics.values.first(where: { stat in stat.type == "inbound-rtp"}) + if let stat { + //logger.debug("Stat \(stat.debugDescription)") + let bytes = stat.values["bytesReceived"] as! Int64 + if bytes <= lastBytesReceived { + mutedSeconds += 1 + if mutedSeconds == 3 { + await MainActor.run { + self.onMediaMuteUnmute(transceiver.mid, true) + } + } + } else { + if mutedSeconds >= 3 { + await MainActor.run { + self.onMediaMuteUnmute(transceiver.mid, false) + } + } + lastBytesReceived = bytes + mutedSeconds = 0 + } + } + try? await Task.sleep(nanoseconds: 1000_000000) + } + } } - func addLocalRenderer(_ activeCall: Call, _ renderer: RTCEAGLVideoView) { - activeCall.localStream?.add(renderer) + @MainActor + func onMediaMuteUnmute(_ transceiverMid: String?, _ mute: Bool) { + guard let activeCall = ChatModel.shared.activeCall else { return } + let source = mediaSourceFromTransceiverMid(transceiverMid) + logger.log("Mute/unmute \(source.rawValue) track = \(mute) with mid = \(transceiverMid ?? "nil")") + if source == .mic && activeCall.peerMediaSources.mic == mute { + activeCall.peerMediaSources.mic = !mute + } else if (source == .camera && activeCall.peerMediaSources.camera == mute) { + activeCall.peerMediaSources.camera = !mute + } else if (source == .screenAudio && activeCall.peerMediaSources.screenAudio == mute) { + activeCall.peerMediaSources.screenAudio = !mute + } else if (source == .screenVideo && activeCall.peerMediaSources.screenVideo == mute) { + activeCall.peerMediaSources.screenVideo = !mute + } + } + + @MainActor + func enableMedia(_ source: CallMediaSource, _ enable: Bool) { + logger.debug("WebRTCClient: enabling media \(source.rawValue) \(enable)") + source == .camera ? setCameraEnabled(enable) : setAudioEnabled(enable) + } + + @MainActor + func adaptToOldVersion(_ peerHasOldVersion: Bool) { + activeCall?.peerHasOldVersion = peerHasOldVersion + if peerHasOldVersion { + logger.debug("The peer has an old version. Remote audio track is nil = \(self.activeCall?.remoteAudioTrack == nil), video = \(self.activeCall?.remoteVideoTrack == nil)") + onMediaMuteUnmute("0", false) + if activeCall?.remoteVideoTrack != nil { + onMediaMuteUnmute("1", false) + } + if ChatModel.shared.activeCall?.localMediaSources.camera == true && ChatModel.shared.activeCall?.peerMediaSources.camera == false { + logger.debug("Stopping video track for the old version") + activeCall?.connection.senders[1].track = nil + ChatModel.shared.activeCall?.localMediaSources.camera = false + (activeCall?.localCamera as? RTCCameraVideoCapturer)?.stopCapture() + activeCall?.localCamera = nil + activeCall?.localVideoTrack = nil + } + } + } + + func addLocalRenderer(_ renderer: RTCEAGLVideoView) { + if let activeCall { + if let track = activeCall.localVideoTrack { + track.add(renderer) + } + } else if let notConnectedCall { + if let track = notConnectedCall.localCameraAndTrack?.1 { + track.add(renderer) + } + } // To get width and height of a frame, see videoView(videoView:, didChangeVideoSize) renderer.delegate = self } + func removeLocalRenderer(_ renderer: RTCEAGLVideoView) { + if let activeCall { + if let track = activeCall.localVideoTrack { + track.remove(renderer) + } + } else if let notConnectedCall { + if let track = notConnectedCall.localCameraAndTrack?.1 { + track.remove(renderer) + } + } + renderer.delegate = nil + } + func videoView(_ videoView: RTCVideoRenderer, didChangeVideoSize size: CGSize) { guard size.height > 0 else { return } localRendererAspectRatio.wrappedValue = size.width / size.height } + func setupLocalTracks(_ incomingCall: Bool, _ call: Call) { + let pc = call.connection + let transceivers = call.connection.transceivers + let audioTrack = call.localAudioTrack + let videoTrack = call.localVideoTrack + + if incomingCall { + let micCameraInit = RTCRtpTransceiverInit() + // streamIds required for old versions which adds tracks from stream, not from track property + micCameraInit.streamIds = ["micCamera"] + + let screenAudioVideoInit = RTCRtpTransceiverInit() + screenAudioVideoInit.streamIds = ["screenAudioVideo"] + + // incoming call, no transceivers yet. But they should be added in order: mic, camera, screen audio, screen video + // mid = 0, mic + if let audioTrack { + pc.addTransceiver(with: audioTrack, init: micCameraInit) + } else { + pc.addTransceiver(of: .audio, init: micCameraInit) + } + // mid = 1, camera + if let videoTrack { + pc.addTransceiver(with: videoTrack, init: micCameraInit) + } else { + pc.addTransceiver(of: .video, init: micCameraInit) + } + // mid = 2, screenAudio + pc.addTransceiver(of: .audio, init: screenAudioVideoInit) + // mid = 3, screenVideo + pc.addTransceiver(of: .video, init: screenAudioVideoInit) + } else { + // new version + if transceivers.count > 2 { + // Outgoing call. All transceivers are ready. Don't addTrack() because it will create new transceivers, replace existing (nil) tracks + transceivers + .first(where: { elem in mediaSourceFromTransceiverMid(elem.mid) == .mic })? + .sender.track = audioTrack + transceivers + .first(where: { elem in mediaSourceFromTransceiverMid(elem.mid) == .camera })? + .sender.track = videoTrack + } else { + // old version, only two transceivers + if let audioTrack { + pc.add(audioTrack, streamIds: ["micCamera"]) + } else { + // it's important to have any track in order to be able to turn it on again (currently it's off) + let sender = pc.add(createAudioTrack(), streamIds: ["micCamera"]) + sender?.track = nil + } + if let videoTrack { + pc.add(videoTrack, streamIds: ["micCamera"]) + } else { + // it's important to have any track in order to be able to turn it on again (currently it's off) + let localVideoSource = WebRTCClient.factory.videoSource() + let localVideoTrack = WebRTCClient.factory.videoTrack(with: localVideoSource, trackId: "video0") + let sender = pc.add(localVideoTrack, streamIds: ["micCamera"]) + sender?.track = nil + } + } + } + } + + func mediaSourceFromTransceiverMid(_ mid: String?) -> CallMediaSource { + switch mid { + case "0": + return .mic + case "1": + return .camera + case "2": + return .screenAudio + case "3": + return .screenVideo + default: + return .unknown + } + } + + // Should be called after local description set + func setupEncryptionForLocalTracks(_ call: Call) { + if let encryptor = call.frameEncryptor { + call.connection.senders.forEach { $0.setRtcFrameEncryptor(encryptor) } + } + } + func frameDecryptor(_ decryptor: RTCFrameDecryptor, mediaType: RTCRtpMediaType, withFrame encrypted: Data) -> Data? { guard encrypted.count > 0 else { return nil } - if var key: [CChar] = activeCall.wrappedValue?.aesKey?.cString(using: .utf8), + if var key: [CChar] = activeCall?.aesKey?.cString(using: .utf8), let pointer: UnsafeMutableRawPointer = malloc(encrypted.count) { memcpy(pointer, (encrypted as NSData).bytes, encrypted.count) let isKeyFrame = encrypted[0] & 1 == 0 @@ -304,7 +512,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg func frameEncryptor(_ encryptor: RTCFrameEncryptor, mediaType: RTCRtpMediaType, withFrame unencrypted: Data) -> Data? { guard unencrypted.count > 0 else { return nil } - if var key: [CChar] = activeCall.wrappedValue?.aesKey?.cString(using: .utf8), + if var key: [CChar] = activeCall?.aesKey?.cString(using: .utf8), let pointer: UnsafeMutableRawPointer = malloc(unencrypted.count + WebRTCClient.ivTagBytes) { memcpy(pointer, (unencrypted as NSData).bytes, unencrypted.count) let isKeyFrame = unencrypted[0] & 1 == 0 @@ -327,14 +535,42 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg } } - func addRemoteRenderer(_ activeCall: Call, _ renderer: RTCVideoRenderer) { - activeCall.remoteStream?.add(renderer) + func addRemoteCameraRenderer(_ renderer: RTCVideoRenderer) { + if activeCall?.remoteVideoTrack != nil { + activeCall?.remoteVideoTrack?.add(renderer) + } else { + cameraRenderers.append(renderer) + } } - func startCaptureLocalVideo(_ activeCall: Call) { + func removeRemoteCameraRenderer(_ renderer: RTCVideoRenderer) { + if activeCall?.remoteVideoTrack != nil { + activeCall?.remoteVideoTrack?.remove(renderer) + } else { + cameraRenderers.removeAll(where: { $0.isEqual(renderer) }) + } + } + + func addRemoteScreenRenderer(_ renderer: RTCVideoRenderer) { + if activeCall?.remoteScreenVideoTrack != nil { + activeCall?.remoteScreenVideoTrack?.add(renderer) + } else { + screenRenderers.append(renderer) + } + } + + func removeRemoteScreenRenderer(_ renderer: RTCVideoRenderer) { + if activeCall?.remoteScreenVideoTrack != nil { + activeCall?.remoteScreenVideoTrack?.remove(renderer) + } else { + screenRenderers.removeAll(where: { $0.isEqual(renderer) }) + } + } + + func startCaptureLocalVideo(_ device: AVCaptureDevice.Position?, _ capturer: RTCVideoCapturer?) { #if targetEnvironment(simulator) guard - let capturer = activeCall.localCamera as? RTCFileVideoCapturer + let capturer = (activeCall?.localCamera ?? notConnectedCall?.localCameraAndTrack?.0) as? RTCFileVideoCapturer else { logger.error("Unable to work with a file capturer") return @@ -344,10 +580,10 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg capturer.startCapturing(fromFileNamed: "sounds/video.mp4") #else guard - let capturer = activeCall.localCamera as? RTCCameraVideoCapturer, - let camera = (RTCCameraVideoCapturer.captureDevices().first { $0.position == activeCall.device }) + let capturer = capturer as? RTCCameraVideoCapturer, + let camera = (RTCCameraVideoCapturer.captureDevices().first { $0.position == device }) else { - logger.error("Unable to find a camera") + logger.error("Unable to find a camera or local track") return } @@ -373,19 +609,6 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg #endif } - private func createAudioSender(_ connection: RTCPeerConnection) { - let streamId = "stream" - let audioTrack = createAudioTrack() - connection.add(audioTrack, streamIds: [streamId]) - } - - private func createVideoSender(_ connection: RTCPeerConnection) -> (RTCVideoTrack?, RTCVideoTrack?, RTCVideoCapturer?, RTCVideoSource?) { - let streamId = "stream" - let (localVideoTrack, localCamera, localVideoSource) = createVideoTrack() - connection.add(localVideoTrack, streamIds: [streamId]) - return (localVideoTrack, connection.transceivers.first { $0.mediaType == .video }?.receiver.track as? RTCVideoTrack, localCamera, localVideoSource) - } - private func createAudioTrack() -> RTCAudioTrack { let audioConstrains = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil) let audioSource = WebRTCClient.factory.audioSource(with: audioConstrains) @@ -393,7 +616,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg return audioTrack } - private func createVideoTrack() -> (RTCVideoTrack, RTCVideoCapturer, RTCVideoSource) { + private func createVideoTrackAndStartCapture(_ device: AVCaptureDevice.Position) -> (RTCVideoCapturer, RTCVideoTrack) { let localVideoSource = WebRTCClient.factory.videoSource() #if targetEnvironment(simulator) @@ -403,18 +626,30 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg #endif let localVideoTrack = WebRTCClient.factory.videoTrack(with: localVideoSource, trackId: "video0") - return (localVideoTrack, localCamera, localVideoSource) + startCaptureLocalVideo(device, localCamera) + return (localCamera, localVideoTrack) } func endCall() { - guard let call = activeCall.wrappedValue else { return } + if #available(iOS 16.0, *) { + _endCall() + } else { + // Fixes `connection.close()` getting locked up in iOS15 + DispatchQueue.global(qos: .utility).async { self._endCall() } + } + } + + private func _endCall() { + (notConnectedCall?.localCameraAndTrack?.0 as? RTCCameraVideoCapturer)?.stopCapture() + guard let call = activeCall else { return } logger.debug("WebRTCClient: ending the call") - activeCall.wrappedValue = nil call.connection.close() call.connection.delegate = nil call.frameEncryptor?.delegate = nil call.frameDecryptor?.delegate = nil + (call.localCamera as? RTCCameraVideoCapturer)?.stopCapture() audioSessionToDefaults() + activeCall = nil } func untilIceComplete(timeoutMs: UInt64, stepMs: UInt64, action: @escaping () async -> Void) async { @@ -423,7 +658,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg _ = try? await Task.sleep(nanoseconds: stepMs * 1000000) t += stepMs await action() - } while t < timeoutMs && activeCall.wrappedValue?.connection.iceGatheringState != .complete + } while t < timeoutMs && activeCall?.connection.iceGatheringState != .complete } } @@ -484,11 +719,40 @@ extension WebRTCClient: RTCPeerConnectionDelegate { logger.debug("Connection should negotiate") } + func peerConnection(_ peerConnection: RTCPeerConnection, didStartReceivingOn transceiver: RTCRtpTransceiver) { + if let track = transceiver.receiver.track { + DispatchQueue.main.async { + // Doesn't work for outgoing video call (audio in video call works ok still, same as incoming call) +// if let decryptor = self.activeCall?.frameDecryptor { +// transceiver.receiver.setRtcFrameDecryptor(decryptor) +// } + let source = self.mediaSourceFromTransceiverMid(transceiver.mid) + switch source { + case .mic: self.activeCall?.remoteAudioTrack = track as? RTCAudioTrack + case .camera: + self.activeCall?.remoteVideoTrack = track as? RTCVideoTrack + self.cameraRenderers.forEach({ renderer in + self.activeCall?.remoteVideoTrack?.add(renderer) + }) + self.cameraRenderers.removeAll() + case .screenAudio: self.activeCall?.remoteScreenAudioTrack = track as? RTCAudioTrack + case .screenVideo: + self.activeCall?.remoteScreenVideoTrack = track as? RTCVideoTrack + self.screenRenderers.forEach({ renderer in + self.activeCall?.remoteScreenVideoTrack?.add(renderer) + }) + self.screenRenderers.removeAll() + case .unknown: () + } + } + self.setupMuteUnmuteListener(transceiver, track) + } + } + func peerConnection(_ connection: RTCPeerConnection, didChange newState: RTCIceConnectionState) { debugPrint("Connection new connection state: \(newState.toString() ?? "" + newState.rawValue.description) \(connection.receivers)") - guard let call = activeCall.wrappedValue, - let connectionStateString = newState.toString(), + guard let connectionStateString = newState.toString(), let iceConnectionStateString = connection.iceConnectionState.toString(), let iceGatheringStateString = connection.iceGatheringState.toString(), let signalingStateString = connection.signalingState.toString() @@ -509,18 +773,14 @@ extension WebRTCClient: RTCPeerConnectionDelegate { switch newState { case .checking: - if let frameDecryptor = activeCall.wrappedValue?.frameDecryptor { + if let frameDecryptor = activeCall?.frameDecryptor { connection.receivers.forEach { $0.setRtcFrameDecryptor(frameDecryptor) } } - let enableSpeaker: Bool - switch call.localMedia { - case .video: enableSpeaker = true - default: enableSpeaker = false - } + let enableSpeaker: Bool = ChatModel.shared.activeCall?.localMediaSources.hasVideo == true setSpeakerEnabledAndConfigureSession(enableSpeaker) case .connected: sendConnectedEvent(connection) case .disconnected, .failed: endCall() - default: do {} + default: () } } } @@ -532,7 +792,7 @@ extension WebRTCClient: RTCPeerConnectionDelegate { func peerConnection(_ connection: RTCPeerConnection, didGenerate candidate: WebRTC.RTCIceCandidate) { // logger.debug("Connection generated candidate \(candidate.debugDescription)") Task { - await self.activeCall.wrappedValue?.iceCandidates.append(candidate.toCandidate(nil, nil)) + await self.activeCall?.iceCandidates.append(candidate.toCandidate(nil, nil)) } } @@ -587,11 +847,42 @@ extension WebRTCClient: RTCPeerConnectionDelegate { } extension WebRTCClient { - func setAudioEnabled(_ enabled: Bool) { - setTrackEnabled(RTCAudioTrack.self, enabled) + static func isAuthorized(for type: AVMediaType) async -> Bool { + let status = AVCaptureDevice.authorizationStatus(for: type) + var isAuthorized = status == .authorized + if status == .notDetermined { + isAuthorized = await AVCaptureDevice.requestAccess(for: type) + } + return isAuthorized } - func setSpeakerEnabledAndConfigureSession( _ enabled: Bool) { + static func showUnauthorizedAlert(for type: AVMediaType) { + if type == .audio { + AlertManager.shared.showAlert(Alert( + title: Text("No permission to record speech"), + message: Text("To record speech please grant permission to use Microphone."), + primaryButton: .default(Text("Open Settings")) { + DispatchQueue.main.async { + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) + } + }, + secondaryButton: .cancel() + )) + } else if type == .video { + AlertManager.shared.showAlert(Alert( + title: Text("No permission to record video"), + message: Text("To record video please grant permission to use Camera."), + primaryButton: .default(Text("Open Settings")) { + DispatchQueue.main.async { + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) + } + }, + secondaryButton: .cancel() + )) + } + } + + func setSpeakerEnabledAndConfigureSession( _ enabled: Bool, skipExternalDevice: Bool = false) { logger.debug("WebRTCClient: configuring session with speaker enabled \(enabled)") audioQueue.async { [weak self] in guard let self = self else { return } @@ -600,9 +891,23 @@ extension WebRTCClient { self.rtcAudioSession.unlockForConfiguration() } do { - try self.rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue) - try self.rtcAudioSession.setMode(AVAudioSession.Mode.voiceChat.rawValue) - try self.rtcAudioSession.overrideOutputAudioPort(enabled ? .speaker : .none) + let hasExternalAudioDevice = self.rtcAudioSession.session.hasExternalAudioDevice() + if enabled { + try self.rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue, with: [.defaultToSpeaker, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP]) + try self.rtcAudioSession.setMode(AVAudioSession.Mode.videoChat.rawValue) + if hasExternalAudioDevice && !skipExternalDevice, let preferred = self.rtcAudioSession.session.preferredInputDevice() { + try self.rtcAudioSession.setPreferredInput(preferred) + } else { + try self.rtcAudioSession.overrideOutputAudioPort(.speaker) + } + } else { + try self.rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue, with: [.allowBluetooth, .allowAirPlay, .allowBluetoothA2DP]) + try self.rtcAudioSession.setMode(AVAudioSession.Mode.voiceChat.rawValue) + try self.rtcAudioSession.overrideOutputAudioPort(.none) + } + if hasExternalAudioDevice && !skipExternalDevice { + logger.debug("WebRTCClient: configuring session with external device available, skip configuring speaker") + } try self.rtcAudioSession.setActive(true) logger.debug("WebRTCClient: configuring session with speaker enabled \(enabled) success") } catch let error { @@ -631,25 +936,70 @@ extension WebRTCClient { } } - func setVideoEnabled(_ enabled: Bool) { - setTrackEnabled(RTCVideoTrack.self, enabled) + @MainActor + func setAudioEnabled(_ enabled: Bool) { + if activeCall != nil { + activeCall?.localAudioTrack = enabled ? createAudioTrack() : nil + activeCall?.connection.transceivers.first(where: { t in mediaSourceFromTransceiverMid(t.mid) == .mic })?.sender.track = activeCall?.localAudioTrack + } else if notConnectedCall != nil { + notConnectedCall?.audioTrack = enabled ? createAudioTrack() : nil + } + ChatModel.shared.activeCall?.localMediaSources.mic = enabled + } + + @MainActor + func setCameraEnabled(_ enabled: Bool) { + if let call = activeCall { + if enabled { + if call.localVideoTrack == nil { + let device = activeCall?.device ?? notConnectedCall?.device ?? .front + let (camera, track) = createVideoTrackAndStartCapture(device) + activeCall?.localCamera = camera + activeCall?.localVideoTrack = track + } + } else { + (call.localCamera as? RTCCameraVideoCapturer)?.stopCapture() + activeCall?.localCamera = nil + activeCall?.localVideoTrack = nil + } + call.connection.transceivers + .first(where: { t in mediaSourceFromTransceiverMid(t.mid) == .camera })? + .sender.track = activeCall?.localVideoTrack + ChatModel.shared.activeCall?.localMediaSources.camera = activeCall?.localVideoTrack != nil + } else if let call = notConnectedCall { + if enabled { + let device = activeCall?.device ?? notConnectedCall?.device ?? .front + notConnectedCall?.localCameraAndTrack = createVideoTrackAndStartCapture(device) + } else { + (call.localCameraAndTrack?.0 as? RTCCameraVideoCapturer)?.stopCapture() + notConnectedCall?.localCameraAndTrack = nil + } + ChatModel.shared.activeCall?.localMediaSources.camera = notConnectedCall?.localCameraAndTrack != nil + } } func flipCamera() { - switch activeCall.wrappedValue?.device { - case .front: activeCall.wrappedValue?.device = .back - case .back: activeCall.wrappedValue?.device = .front - default: () - } - if let call = activeCall.wrappedValue { - startCaptureLocalVideo(call) + let device = activeCall?.device ?? notConnectedCall?.device + if activeCall != nil { + activeCall?.device = device == .front ? .back : .front + } else { + notConnectedCall?.device = device == .front ? .back : .front } + startCaptureLocalVideo( + activeCall?.device ?? notConnectedCall?.device, + (activeCall?.localCamera ?? notConnectedCall?.localCameraAndTrack?.0) as? RTCCameraVideoCapturer + ) + } +} + +extension AVAudioSession { + func hasExternalAudioDevice() -> Bool { + availableInputs?.allSatisfy({ $0.portType == .builtInMic }) != true } - private func setTrackEnabled(_ type: T.Type, _ enabled: Bool) { - activeCall.wrappedValue?.connection.transceivers - .compactMap { $0.sender.track as? T } - .forEach { $0.isEnabled = enabled } + func preferredInputDevice() -> AVAudioSessionPortDescription? { +// logger.debug("Preferred input device: \(String(describing: self.availableInputs?.filter({ $0.portType != .builtInMic })))") + return availableInputs?.filter({ $0.portType != .builtInMic }).last } } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift index d0f4b6e55a..62a41c504a 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift @@ -9,10 +9,9 @@ import SwiftUI import SimpleXChat -let chatImageColorLight = Color(red: 0.9, green: 0.9, blue: 0.9) -let chatImageColorDark = Color(red: 0.2, green: 0.2, blue: 0.2) struct ChatInfoToolbar: View { @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat var imageSize: CGFloat = 32 @@ -25,30 +24,30 @@ struct ChatInfoToolbar: View { } ChatInfoImage( chat: chat, - color: colorScheme == .dark - ? chatImageColorDark - : chatImageColorLight + size: imageSize, + color: Color(uiColor: .tertiaryLabel) ) - .frame(width: imageSize, height: imageSize) .padding(.trailing, 4) - VStack { - let t = Text(cInfo.displayName).font(.headline) - (cInfo.contact?.verified == true ? contactVerifiedShield + t : t) - .lineLimit(1) - if cInfo.fullName != "" && cInfo.displayName != cInfo.fullName { - Text(cInfo.fullName).font(.subheadline) - .lineLimit(1) + let t = Text(cInfo.displayName).font(.headline) + (cInfo.contact?.verified == true ? contactVerifiedShield + t : t) + .lineLimit(1) + .if (cInfo.fullName != "" && cInfo.displayName != cInfo.fullName) { v in + VStack(spacing: 0) { + v + Text(cInfo.fullName).font(.subheadline) + .lineLimit(1) + .padding(.top, -2) + } } - } } - .foregroundColor(.primary) + .foregroundColor(theme.colors.onBackground) .frame(width: 220) } private var contactVerifiedShield: Text { - (Text(Image(systemName: "checkmark.shield")) + Text(" ")) + (Text(Image(systemName: "checkmark.shield")) + textSpace) .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .baselineOffset(1) .kerning(-2) } @@ -57,5 +56,6 @@ struct ChatInfoToolbar: View { struct ChatInfoToolbar_Previews: PreviewProvider { static var previews: some View { ChatInfoToolbar(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])) + .environmentObject(CurrentColors.toAppTheme()) } } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index b702c2cc23..8194c8fe6f 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -7,7 +7,7 @@ // import SwiftUI -import SimpleXChat +@preconcurrency import SimpleXChat func infoRow(_ title: LocalizedStringKey, _ value: String) -> some View { HStack { @@ -36,20 +36,20 @@ func localizedInfoRow(_ title: LocalizedStringKey, _ value: LocalizedStringKey) } } -@ViewBuilder func smpServers(_ title: LocalizedStringKey, _ servers: [String]) -> some View { +@ViewBuilder func smpServers(_ title: LocalizedStringKey, _ servers: [String], _ secondaryColor: Color) -> some View { if servers.count > 0 { HStack { Text(title).frame(width: 120, alignment: .leading) Button(serverHost(servers[0])) { UIPasteboard.general.string = servers.joined(separator: ";") } - .foregroundColor(.secondary) + .foregroundColor(secondaryColor) .lineLimit(1) } } } -private func serverHost(_ s: String) -> String { +func serverHost(_ s: String) -> String { if let i = s.range(of: "@")?.lowerBound { return String(s[i...].dropFirst()) } else { @@ -90,27 +90,37 @@ enum SendReceipts: Identifiable, Hashable { struct ChatInfoView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction + @ObservedObject var networkModel = NetworkModel.shared @ObservedObject var chat: Chat @State var contact: Contact - @Binding var connectionStats: ConnectionStats? - @Binding var customUserProfile: Profile? @State var localAlias: String - @Binding var connectionCode: String? + @State var featuresAllowed: ContactFeaturesAllowed + @State var currentFeaturesAllowed: ContactFeaturesAllowed + var onSearch: () -> Void + @State private var connectionStats: ConnectionStats? = nil + @State private var customUserProfile: Profile? = nil + @State private var connectionCode: String? = nil @FocusState private var aliasTextFieldFocused: Bool @State private var alert: ChatInfoViewAlert? = nil - @State private var showDeleteContactActionSheet = false + @State private var actionSheet: SomeActionSheet? = nil + @State private var sheet: SomeSheet? = nil + @State private var showConnectContactViaAddressDialog = false @State private var sendReceipts = SendReceipts.userDefault(true) @State private var sendReceiptsUserDefault = true + @State private var progressIndicator = false @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false - + enum ChatInfoViewAlert: Identifiable { case clearChatAlert case networkStatusAlert case switchAddressAlert case abortSwitchAddressAlert case syncConnectionForceAlert - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + case queueInfo(info: String) + case someAlert(alert: SomeAlert) + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -119,109 +129,172 @@ struct ChatInfoView: View { case .switchAddressAlert: return "switchAddressAlert" case .abortSwitchAddressAlert: return "abortSwitchAddressAlert" case .syncConnectionForceAlert: return "syncConnectionForceAlert" + case let .queueInfo(info): return "queueInfo \(info)" + case let .someAlert(alert): return "chatInfoSomeAlert \(alert.id)" case let .error(title, _): return "error \(title)" } } } - + var body: some View { NavigationView { - List { - contactInfoHeader() - .listRowBackground(Color.clear) - .contentShape(Rectangle()) - .onTapGesture { - aliasTextFieldFocused = false - } - - Group { + ZStack { + List { + contactInfoHeader() + .listRowBackground(Color.clear) + .contentShape(Rectangle()) + .onTapGesture { + aliasTextFieldFocused = false + } + localAliasTextEdit() - } - .listRowBackground(Color.clear) - .listRowSeparator(.hidden) - - if let customUserProfile = customUserProfile { - Section("Incognito") { - HStack { - Text("Your random profile") - Spacer() - Text(customUserProfile.chatViewName) - .foregroundStyle(.indigo) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .padding(.bottom, 18) + + GeometryReader { g in + HStack(alignment: .center, spacing: 8) { + let buttonWidth = g.size.width / 4 + searchButton(width: buttonWidth) + AudioCallButton(chat: chat, contact: contact, connectionStats: $connectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) } + VideoButton(chat: chat, contact: contact, connectionStats: $connectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) } + if let nextNtfMode = chat.chatInfo.nextNtfMode { + muteButton(width: buttonWidth, nextNtfMode: nextNtfMode) + } } } - } - - Section { - if let code = connectionCode { verifyCodeButton(code) } - contactPreferencesButton() - sendReceiptsOption() - if let connStats = connectionStats, - connStats.ratchetSyncAllowed { - synchronizeConnectionButton() + .padding(.trailing) + .frame(maxWidth: .infinity) + .frame(height: infoViewActionButtonHeight) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 8)) + + if let customUserProfile = customUserProfile { + Section(header: Text("Incognito").foregroundColor(theme.colors.secondary)) { + HStack { + Text("Your random profile") + Spacer() + Text(customUserProfile.chatViewName) + .foregroundStyle(.indigo) + } + } } -// } else if developerTools { -// synchronizeConnectionButtonForce() -// } - } - .disabled(!contact.ready || !contact.active) - - if let contactLink = contact.contactLink { + Section { - SimpleXLinkQRCode(uri: contactLink) - Button { - showShareSheet(items: [simplexChatLink(contactLink)]) - } label: { - Label("Share address", systemImage: "square.and.arrow.up") + if let code = connectionCode { verifyCodeButton(code) } + contactPreferencesButton() + sendReceiptsOption() + if let connStats = connectionStats, + connStats.ratchetSyncAllowed { + synchronizeConnectionButton() } - } header: { - Text("Address") - } footer: { - Text("You can share this address with your contacts to let them connect with **\(contact.displayName)**.") - } - } + // } else if developerTools { + // synchronizeConnectionButtonForce() + // } - if contact.ready && contact.active { - Section("Servers") { - networkStatusRow() - .onTapGesture { - alert = .networkStatusAlert + NavigationLink { + ChatWallpaperEditorSheet(chat: chat) + } label: { + Label("Chat theme", systemImage: "photo") + } + // } else if developerTools { + // synchronizeConnectionButtonForce() + // } + } + .disabled(!contact.ready || !contact.active) + + Section { + ChatTTLOption(chat: chat, progressIndicator: $progressIndicator) + } footer: { + Text("Delete chat messages from your device.") + } + + if let conn = contact.activeConn { + Section { + infoRow(Text(String("E2E encryption")), conn.connPQEnabled ? "Quantum resistant" : "Standard") + } + } + + if let contactLink = contact.contactLink { + Section { + SimpleXLinkQRCode(uri: contactLink) + Button { + showShareSheet(items: [simplexChatLink(contactLink)]) + } label: { + Label("Share address", systemImage: "square.and.arrow.up") } - if let connStats = connectionStats { - Button("Change receiving address") { - alert = .switchAddressAlert - } - .disabled( - connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } - || connStats.ratchetSyncSendProhibited - ) - if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) { - Button("Abort changing address") { - alert = .abortSwitchAddressAlert + } header: { + Text("Address") + .foregroundColor(theme.colors.secondary) + } footer: { + Text("You can share this address with your contacts to let them connect with **\(contact.displayName)**.") + .foregroundColor(theme.colors.secondary) + } + } + + if contact.ready && contact.active { + Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) { + networkStatusRow() + .onTapGesture { + alert = .networkStatusAlert + } + if let connStats = connectionStats { + Button("Change receiving address") { + alert = .switchAddressAlert } .disabled( - connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch } + connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } || connStats.ratchetSyncSendProhibited ) + if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) { + Button("Abort changing address") { + alert = .abortSwitchAddressAlert + } + .disabled( + connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch } + || connStats.ratchetSyncSendProhibited + ) + } + smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }, theme.colors.secondary) + smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }, theme.colors.secondary) + } + } + } + + Section { + clearChatButton() + deleteContactButton() + } + + if developerTools { + Section(header: Text("For console").foregroundColor(theme.colors.secondary)) { + infoRow("Local name", chat.chatInfo.localDisplayName) + infoRow("Database ID", "\(chat.chatInfo.apiId)") + Button ("Debug delivery") { + Task { + do { + let info = queueInfoText(try await apiContactQueueInfo(chat.chatInfo.apiId)) + await MainActor.run { alert = .queueInfo(info: info) } + } catch let e { + logger.error("apiContactQueueInfo error: \(responseError(e))") + let a = getErrorAlert(e, "Error") + await MainActor.run { alert = .error(title: a.title, error: a.message) } + } + } } - smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }) - smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }) } } } - - Section { - clearChatButton() - deleteContactButton() - } - - if developerTools { - Section(header: Text("For console")) { - infoRow("Local name", chat.chatInfo.localDisplayName) - infoRow("Database ID", "\(chat.chatInfo.apiId)") - } + .modifier(ThemedBackground(grouped: true)) + .navigationBarHidden(true) + .disabled(progressIndicator) + .opacity(progressIndicator ? 0.6 : 1) + + if progressIndicator { + ProgressView().scaleEffect(2) } } - .navigationBarHidden(true) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .onAppear { @@ -229,6 +302,23 @@ struct ChatInfoView: View { sendReceiptsUserDefault = currentUser.sendRcptsContacts } sendReceipts = SendReceipts.fromBool(contact.chatSettings.sendRcpts, userDefault: sendReceiptsUserDefault) + + Task { + do { + let (stats, profile) = try await apiContactInfo(chat.chatInfo.apiId) + let (ct, code) = try await apiGetContactCode(chat.chatInfo.apiId) + await MainActor.run { + connectionStats = stats + customUserProfile = profile + connectionCode = code + if contact.activeConn?.connectionCode != ct.activeConn?.connectionCode { + chat.chatInfo = .direct(contact: ct) + } + } + } catch let error { + logger.error("apiContactInfo or apiGetContactCode error: \(responseError(error))") + } + } } .alert(item: $alert) { alertItem in switch(alertItem) { @@ -236,45 +326,52 @@ struct ChatInfoView: View { case .networkStatusAlert: return networkStatusAlert() case .switchAddressAlert: return switchAddressAlert(switchContactAddress) case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress) - case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) }) + case .syncConnectionForceAlert: + return syncConnectionForceAlert({ + Task { + if let stats = await syncContactConnection(contact, force: true, showAlert: { alert = .someAlert(alert: $0) }) { + connectionStats = stats + dismiss() + } + } + }) + case let .queueInfo(info): return queueInfoAlert(info) + case let .someAlert(a): return a.alert case let .error(title, error): return mkAlert(title: title, message: error) } } - .actionSheet(isPresented: $showDeleteContactActionSheet) { - if contact.ready && contact.active { - return ActionSheet( - title: Text("Delete contact?\nThis cannot be undone!"), - buttons: [ - .destructive(Text("Delete and notify contact")) { deleteContact(notify: true) }, - .destructive(Text("Delete")) { deleteContact(notify: false) }, - .cancel() - ] - ) + .actionSheet(item: $actionSheet) { $0.actionSheet } + .sheet(item: $sheet) { + if #available(iOS 16.0, *) { + $0.content + .presentationDetents([.fraction($0.fraction)]) } else { - return ActionSheet( - title: Text("Delete contact?\nThis cannot be undone!"), - buttons: [ - .destructive(Text("Delete")) { deleteContact() }, - .cancel() - ] + $0.content + } + } + .onDisappear { + if currentFeaturesAllowed != featuresAllowed { + showAlert( + title: NSLocalizedString("Save preferences?", comment: "alert title"), + buttonTitle: NSLocalizedString("Save and notify contact", comment: "alert button"), + buttonAction: { savePreferences() }, + cancelButton: true ) } } } - + private func contactInfoHeader() -> some View { - VStack { + VStack(spacing: 8) { let cInfo = chat.chatInfo - ChatInfoImage(chat: chat, color: Color(uiColor: .tertiarySystemFill)) - .frame(width: 192, height: 192) - .padding(.top, 12) - .padding() + ChatInfoImage(chat: chat, size: 192, color: Color(uiColor: .tertiarySystemFill)) + .padding(.vertical, 12) if contact.verified { ( Text(Image(systemName: "checkmark.shield")) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .font(.title2) - + Text(" ") + + textSpace + Text(contact.profile.displayName) .font(.largeTitle) ) @@ -297,7 +394,7 @@ struct ChatInfoView: View { } .frame(maxWidth: .infinity, alignment: .center) } - + private func localAliasTextEdit() -> some View { TextField("Set contact name…", text: $localAlias) .disableAutocorrection(true) @@ -312,9 +409,9 @@ struct ChatInfoView: View { setContactAlias() } .multilineTextAlignment(.center) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } - + private func setContactAlias() { Task { do { @@ -329,6 +426,25 @@ struct ChatInfoView: View { } } + private func searchButton(width: CGFloat) -> some View { + InfoViewButton(image: "magnifyingglass", title: "search", width: width) { + dismiss() + onSearch() + } + .disabled(!contact.ready || chat.chatItems.isEmpty) + } + + private func muteButton(width: CGFloat, nextNtfMode: MsgFilter) -> some View { + return InfoViewButton( + image: nextNtfMode.iconFilled, + title: "\(nextNtfMode.text(mentions: false))", + width: width + ) { + toggleNotifications(chat, enableNtfs: nextNtfMode) + } + .disabled(!contact.ready || !contact.active) + } + private func verifyCodeButton(_ code: String) -> some View { NavigationLink { VerifyCodeView( @@ -350,6 +466,7 @@ struct ChatInfoView: View { ) .navigationBarTitleDisplayMode(.inline) .navigationTitle("Security code") + .modifier(ThemedBackground(grouped: true)) } label: { Label( contact.verified ? "View security code" : "Verify security code", @@ -357,21 +474,23 @@ struct ChatInfoView: View { ) } } - + private func contactPreferencesButton() -> some View { NavigationLink { ContactPreferencesView( contact: $contact, - featuresAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences), - currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences) + featuresAllowed: $featuresAllowed, + currentFeaturesAllowed: $currentFeaturesAllowed, + savePreferences: savePreferences ) .navigationBarTitle("Contact preferences") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { Label("Contact preferences", systemImage: "switch.2") } } - + private func sendReceiptsOption() -> some View { Picker(selection: $sendReceipts) { ForEach([.yes, .no, .userDefault(sendReceiptsUserDefault)]) { (opt: SendReceipts) in @@ -385,7 +504,7 @@ struct ChatInfoView: View { setSendReceipts() } } - + private func setSendReceipts() { var chatSettings = chat.chatInfo.chatSettings ?? ChatSettings.defaults chatSettings.sendRcpts = sendReceipts.bool() @@ -394,13 +513,18 @@ struct ChatInfoView: View { private func synchronizeConnectionButton() -> some View { Button { - syncContactConnection(force: false) + Task { + if let stats = await syncContactConnection(contact, force: false, showAlert: { alert = .someAlert(alert: $0) }) { + connectionStats = stats + dismiss() + } + } } label: { Label("Fix connection", systemImage: "exclamationmark.arrow.triangle.2.circlepath") .foregroundColor(.orange) } } - + private func synchronizeConnectionButtonForce() -> some View { Button { alert = .syncConnectionForceAlert @@ -409,36 +533,43 @@ struct ChatInfoView: View { .foregroundColor(.red) } } - + private func networkStatusRow() -> some View { HStack { Text("Network status") Image(systemName: "info.circle") - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) .font(.system(size: 14)) Spacer() - Text(chatModel.contactNetworkStatus(contact).statusString) - .foregroundColor(.secondary) + Text(networkModel.contactNetworkStatus(contact).statusString) + .foregroundColor(theme.colors.secondary) serverImage() } } - + private func serverImage() -> some View { - let status = chatModel.contactNetworkStatus(contact) + let status = networkModel.contactNetworkStatus(contact) return Image(systemName: status.imageName) - .foregroundColor(status == .connected ? .green : .secondary) + .foregroundColor(status == .connected ? .green : theme.colors.secondary) .font(.system(size: 12)) } - + private func deleteContactButton() -> some View { Button(role: .destructive) { - showDeleteContactActionSheet = true + deleteContactDialog( + chat, + contact, + dismissToChatList: true, + showAlert: { alert = .someAlert(alert: $0) }, + showActionSheet: { actionSheet = $0 }, + showSheetContent: { sheet = $0 } + ) } label: { - Label("Delete contact", systemImage: "trash") + Label("Delete contact", systemImage: "person.badge.minus") .foregroundColor(Color.red) } } - + private func clearChatButton() -> some View { Button() { alert = .clearChatAlert @@ -447,26 +578,7 @@ struct ChatInfoView: View { .foregroundColor(Color.orange) } } - - private func deleteContact(notify: Bool? = nil) { - Task { - do { - try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, notify: notify) - await MainActor.run { - dismiss() - chatModel.chatId = nil - chatModel.removeChat(chat.chatInfo.id) - } - } catch let error { - logger.error("deleteContactAlert apiDeleteChat error: \(responseError(error))") - let a = getErrorAlert(error, "Error deleting contact") - await MainActor.run { - alert = .error(title: a.title, error: a.message) - } - } - } - } - + private func clearChatAlert() -> Alert { Alert( title: Text("Clear conversation?"), @@ -480,14 +592,14 @@ struct ChatInfoView: View { secondaryButton: .cancel() ) } - + private func networkStatusAlert() -> Alert { Alert( title: Text("Network status"), - message: Text(chatModel.contactNetworkStatus(contact).statusExplanation) + message: Text(networkModel.contactNetworkStatus(contact).statusExplanation) ) } - + private func switchContactAddress() { Task { do { @@ -506,7 +618,7 @@ struct ChatInfoView: View { } } } - + private func abortSwitchContactAddress() { Task { do { @@ -524,24 +636,436 @@ struct ChatInfoView: View { } } } - - private func syncContactConnection(force: Bool) { + + private func savePreferences() { Task { do { - let stats = try apiSyncContactRatchet(contact.apiId, force) - connectionStats = stats - await MainActor.run { - chatModel.updateContactConnectionStats(contact, stats) - dismiss() + let prefs = contactFeaturesAllowedToPrefs(featuresAllowed) + if let toContact = try await apiSetContactPrefs(contactId: contact.contactId, preferences: prefs) { + await MainActor.run { + contact = toContact + chatModel.updateContact(toContact) + currentFeaturesAllowed = featuresAllowed + } } - } catch let error { - logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))") - let a = getErrorAlert(error, "Error synchronizing connection") - await MainActor.run { - alert = .error(title: a.title, error: a.message) + } catch { + logger.error("ContactPreferencesView apiSetContactPrefs error: \(responseError(error))") + } + } + } +} + +struct ChatTTLOption: View { + @ObservedObject var chat: Chat + @Binding var progressIndicator: Bool + @State private var currentChatItemTTL: ChatTTL = ChatTTL.userDefault(.seconds(0)) + @State private var chatItemTTL: ChatTTL = ChatTTL.chat(.seconds(0)) + + var body: some View { + Picker("Delete messages after", selection: $chatItemTTL) { + ForEach(ChatItemTTL.values) { ttl in + Text(ttl.deleteAfterText).tag(ChatTTL.chat(ttl)) + } + let defaultTTL = ChatTTL.userDefault(ChatModel.shared.chatItemTTL) + Text(defaultTTL.text).tag(defaultTTL) + + if case .chat(let ttl) = chatItemTTL, case .seconds = ttl { + Text(ttl.deleteAfterText).tag(chatItemTTL) + } + } + .disabled(progressIndicator) + .frame(height: 36) + .onChange(of: chatItemTTL) { ttl in + if ttl == currentChatItemTTL { return } + setChatTTL( + ttl, + hasPreviousTTL: !currentChatItemTTL.neverExpires, + onCancel: { chatItemTTL = currentChatItemTTL } + ) { + progressIndicator = true + Task { + let m = ChatModel.shared + do { + try await setChatTTL(chatType: chat.chatInfo.chatType, id: chat.chatInfo.apiId, ttl) + await loadChat(chat: chat, clearItems: true) + await MainActor.run { + progressIndicator = false + currentChatItemTTL = chatItemTTL + if ItemsModel.shared.reversedChatItems.isEmpty && m.chatId == chat.id, + let chat = m.getChat(chat.id) { + chat.chatItems = [] + m.replaceChat(chat.id, chat) + } + } + } + catch let error { + logger.error("setChatTTL error \(responseError(error))") + await loadChat(chat: chat, clearItems: true) + await MainActor.run { + chatItemTTL = currentChatItemTTL + progressIndicator = false + } + } } } } + .onAppear { + let sm = ChatModel.shared + let ttl = chat.chatInfo.ttl(sm.chatItemTTL) + chatItemTTL = ttl + currentChatItemTTL = ttl + } + } +} + +func syncContactConnection(_ contact: Contact, force: Bool, showAlert: (SomeAlert) -> Void) async -> ConnectionStats? { + do { + let stats = try apiSyncContactRatchet(contact.apiId, force) + await MainActor.run { + ChatModel.shared.updateContactConnectionStats(contact, stats) + } + return stats + } catch let error { + logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))") + let a = getErrorAlert(error, "Error synchronizing connection") + await MainActor.run { + showAlert( + SomeAlert( + alert: mkAlert(title: a.title, message: a.message), + id: "syncContactConnection error" + ) + ) + } + return nil + } +} + +struct AudioCallButton: View { + var chat: Chat + var contact: Contact + @Binding var connectionStats: ConnectionStats? + var width: CGFloat + var showAlert: (SomeAlert) -> Void + + var body: some View { + CallButton( + chat: chat, + contact: contact, + connectionStats: $connectionStats, + image: "phone.fill", + title: "call", + mediaType: .audio, + width: width, + showAlert: showAlert + ) + } +} + +struct VideoButton: View { + var chat: Chat + var contact: Contact + @Binding var connectionStats: ConnectionStats? + var width: CGFloat + var showAlert: (SomeAlert) -> Void + + var body: some View { + CallButton( + chat: chat, + contact: contact, + connectionStats: $connectionStats, + image: "video.fill", + title: "video", + mediaType: .video, + width: width, + showAlert: showAlert + ) + } +} + +private struct CallButton: View { + var chat: Chat + var contact: Contact + @Binding var connectionStats: ConnectionStats? + var image: String + var title: LocalizedStringKey + var mediaType: CallMediaType + var width: CGFloat + var showAlert: (SomeAlert) -> Void + + var body: some View { + let canCall = contact.ready && contact.active && chat.chatInfo.featureEnabled(.calls) && ChatModel.shared.activeCall == nil + + InfoViewButton(image: image, title: title, disabledLook: !canCall, width: width) { + if canCall { + if let connStats = connectionStats { + if connStats.ratchetSyncState == .ok { + if CallController.useCallKit() { + CallController.shared.startCall(contact, mediaType) + } else { + // When CallKit is not used, colorscheme will be changed and it will be visible if not hiding sheets first + dismissAllSheets(animated: true) { + CallController.shared.startCall(contact, mediaType) + } + } + } else if connStats.ratchetSyncAllowed { + showAlert(SomeAlert( + alert: Alert( + title: Text("Fix connection?"), + message: Text("Connection requires encryption renegotiation."), + primaryButton: .default(Text("Fix")) { + Task { + if let stats = await syncContactConnection(contact, force: false, showAlert: showAlert) { + connectionStats = stats + } + } + }, + secondaryButton: .cancel() + ), + id: "can't call contact, fix connection" + )) + } else { + showAlert(SomeAlert( + alert: mkAlert( + title: "Can't call contact", + message: "Encryption renegotiation in progress." + ), + id: "can't call contact, encryption renegotiation in progress" + )) + } + } + } else if contact.nextSendGrpInv { + showAlert(SomeAlert( + alert: mkAlert( + title: "Can't call contact", + message: "Send message to enable calls." + ), + id: "can't call contact, send message" + )) + } else if !contact.active { + showAlert(SomeAlert( + alert: mkAlert( + title: "Can't call contact", + message: "Contact is deleted." + ), + id: "can't call contact, contact deleted" + )) + } else if !contact.ready { + showAlert(SomeAlert( + alert: mkAlert( + title: "Can't call contact", + message: "Connecting to contact, please wait or check later!" + ), + id: "can't call contact, contact not ready" + )) + } else if !chat.chatInfo.featureEnabled(.calls) { + switch chat.chatInfo.showEnableCallsAlert { + case .userEnable: + showAlert(SomeAlert( + alert: Alert( + title: Text("Allow calls?"), + message: Text("You need to allow your contact to call to be able to call them."), + primaryButton: .default(Text("Allow")) { + allowFeatureToContact(contact, .calls) + }, + secondaryButton: .cancel() + ), + id: "allow calls" + )) + case .askContact: + showAlert(SomeAlert( + alert: mkAlert( + title: "Calls prohibited!", + message: "Please ask your contact to enable calls." + ), + id: "calls prohibited, ask contact" + )) + case .other: + showAlert(SomeAlert( + alert: mkAlert( + title: "Calls prohibited!", + message: "Please check yours and your contact preferences." + ) + , id: "calls prohibited, other" + )) + } + } else { + showAlert(SomeAlert( + alert: mkAlert(title: "Can't call contact"), + id: "can't call contact" + )) + } + } + .disabled(ChatModel.shared.activeCall != nil) + } +} + +let infoViewActionButtonHeight: CGFloat = 60 + +struct InfoViewButton: View { + var image: String + var title: LocalizedStringKey + var disabledLook: Bool = false + var width: CGFloat + var action: () -> Void + + var body: some View { + VStack(spacing: 4) { + Image(systemName: image) + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + Text(title) + .font(.caption) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .foregroundColor(.accentColor) + .background(Color(.secondarySystemGroupedBackground)) + .cornerRadius(10.0) + .frame(width: width, height: infoViewActionButtonHeight) + .disabled(disabledLook) + .onTapGesture(perform: action) + } +} + +struct ChatWallpaperEditorSheet: View { + @Environment(\.dismiss) var dismiss + @EnvironmentObject var theme: AppTheme + @State private var globalThemeUsed: Bool = false + @State var chat: Chat + @State private var themes: ThemeModeOverrides + + init(chat: Chat) { + self.chat = chat + self.themes = if case let ChatInfo.direct(contact) = chat.chatInfo, let uiThemes = contact.uiThemes { + uiThemes + } else if case let ChatInfo.group(groupInfo) = chat.chatInfo, let uiThemes = groupInfo.uiThemes { + uiThemes + } else { + ThemeModeOverrides() + } + } + + var body: some View { + let preferred = themes.preferredMode(!theme.colors.isLight) + let initialTheme = preferred ?? ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + ChatWallpaperEditor( + initialTheme: initialTheme, + themeModeOverride: initialTheme, + applyToMode: themes.light == themes.dark ? nil : initialTheme.mode, + globalThemeUsed: $globalThemeUsed, + save: { applyToMode, newTheme in + await save(applyToMode, newTheme, $chat) + } + ) + .navigationTitle("Chat theme") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.inline) + .onAppear { + globalThemeUsed = preferred == nil + } + .onChange(of: theme.base.mode) { _ in + globalThemeUsed = themesFromChat(chat).preferredMode(!theme.colors.isLight) == nil + } + .onChange(of: ChatModel.shared.chatId) { _ in + dismiss() + } + } + + private func themesFromChat(_ chat: Chat) -> ThemeModeOverrides { + if case let ChatInfo.direct(contact) = chat.chatInfo, let uiThemes = contact.uiThemes { + uiThemes + } else if case let ChatInfo.group(groupInfo) = chat.chatInfo, let uiThemes = groupInfo.uiThemes { + uiThemes + } else { + ThemeModeOverrides() + } + } + + private static var updateBackendTask: Task = Task {} + private func save( + _ applyToMode: DefaultThemeMode?, + _ newTheme: ThemeModeOverride?, + _ chat: Binding + ) async { + let unchangedThemes: ThemeModeOverrides = themesFromChat(chat.wrappedValue) + var wallpaperFiles = Set([unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile]) + var changedThemes: ThemeModeOverrides? = unchangedThemes + let light: ThemeModeOverride? = if let newTheme { + ThemeModeOverride(mode: DefaultThemeMode.light, colors: newTheme.colors, wallpaper: newTheme.wallpaper?.withFilledWallpaperPath()) + } else { + nil + } + let dark: ThemeModeOverride? = if let newTheme { + ThemeModeOverride(mode: DefaultThemeMode.dark, colors: newTheme.colors, wallpaper: newTheme.wallpaper?.withFilledWallpaperPath()) + } else { + nil + } + + if let applyToMode { + switch applyToMode { + case DefaultThemeMode.light: + changedThemes?.light = light + case DefaultThemeMode.dark: + changedThemes?.dark = dark + } + } else { + changedThemes?.light = light + changedThemes?.dark = dark + } + if changedThemes?.light != nil || changedThemes?.dark != nil { + let light = changedThemes?.light + let dark = changedThemes?.dark + let currentMode = CurrentColors.base.mode + // same image file for both modes, copy image to make them as different files + if var light, var dark, let lightWallpaper = light.wallpaper, let darkWallpaper = dark.wallpaper, let lightImageFile = lightWallpaper.imageFile, let darkImageFile = darkWallpaper.imageFile, lightWallpaper.imageFile == darkWallpaper.imageFile { + let imageFile = if currentMode == DefaultThemeMode.light { + darkImageFile + } else { + lightImageFile + } + let filePath = saveWallpaperFile(url: getWallpaperFilePath(imageFile)) + if currentMode == DefaultThemeMode.light { + dark.wallpaper?.imageFile = filePath + changedThemes = ThemeModeOverrides(light: changedThemes?.light, dark: dark) + } else { + light.wallpaper?.imageFile = filePath + changedThemes = ThemeModeOverrides(light: light, dark: changedThemes?.dark) + } + } + } else { + changedThemes = nil + } + wallpaperFiles.remove(changedThemes?.light?.wallpaper?.imageFile) + wallpaperFiles.remove(changedThemes?.dark?.wallpaper?.imageFile) + wallpaperFiles.forEach(removeWallpaperFile) + + let changedThemesConstant = changedThemes + ChatWallpaperEditorSheet.updateBackendTask.cancel() + ChatWallpaperEditorSheet.updateBackendTask = Task { + do { + try await Task.sleep(nanoseconds: 300_000000) + if await apiSetChatUIThemes(chatId: chat.id, themes: changedThemesConstant) { + if case var ChatInfo.direct(contact) = chat.wrappedValue.chatInfo { + contact.uiThemes = changedThemesConstant + await MainActor.run { + ChatModel.shared.updateChatInfo(ChatInfo.direct(contact: contact)) + chat.wrappedValue = Chat.init(chatInfo: ChatInfo.direct(contact: contact)) + themes = themesFromChat(chat.wrappedValue) + } + } else if case var ChatInfo.group(groupInfo) = chat.wrappedValue.chatInfo { + groupInfo.uiThemes = changedThemesConstant + + await MainActor.run { + ChatModel.shared.updateChatInfo(ChatInfo.group(groupInfo: groupInfo)) + chat.wrappedValue = Chat.init(chatInfo: ChatInfo.group(groupInfo: groupInfo)) + themes = themesFromChat(chat.wrappedValue) + } + } + } + } catch { + // canceled task + } + } } } @@ -572,15 +1096,267 @@ func syncConnectionForceAlert(_ syncConnectionForce: @escaping () -> Void) -> Al ) } +func queueInfoText(_ info: (RcvMsgInfo?, ServerQueueInfo)) -> String { + let (rcvMsgInfo, qInfo) = info + var msgInfo: String + if let rcvMsgInfo { msgInfo = encodeJSON(rcvMsgInfo) } else { msgInfo = "none" } + return String.localizedStringWithFormat(NSLocalizedString("server queue info: %@\n\nlast received msg: %@", comment: "queue info"), encodeJSON(qInfo), msgInfo) +} + +func queueInfoAlert(_ info: String) -> Alert { + Alert( + title: Text("Message queue info"), + message: Text(info), + primaryButton: .default(Text("Ok")), + secondaryButton: .default(Text("Copy")) { UIPasteboard.general.string = info } + ) +} + +func deleteContactDialog( + _ chat: Chat, + _ contact: Contact, + dismissToChatList: Bool, + showAlert: @escaping (SomeAlert) -> Void, + showActionSheet: @escaping (SomeActionSheet) -> Void, + showSheetContent: @escaping (SomeSheet) -> Void +) { + if contact.sndReady && contact.active && !contact.chatDeleted { + deleteContactOrConversationDialog(chat, contact, dismissToChatList, showAlert, showActionSheet, showSheetContent) + } else if contact.sndReady && contact.active && contact.chatDeleted { + deleteContactWithoutConversation(chat, contact, dismissToChatList, showAlert, showActionSheet) + } else { // !(contact.sndReady && contact.active) + deleteNotReadyContact(chat, contact, dismissToChatList, showAlert, showActionSheet) + } +} + +func setChatTTL(_ ttl: ChatTTL, hasPreviousTTL: Bool, onCancel: @escaping () -> Void, onConfirm: @escaping () -> Void) { + let title = if ttl.neverExpires { + NSLocalizedString("Disable automatic message deletion?", comment: "alert title") + } else if ttl.usingDefault || hasPreviousTTL { + NSLocalizedString("Change automatic message deletion?", comment: "alert title") + } else { + NSLocalizedString("Enable automatic message deletion?", comment: "alert title") + } + + let message = if ttl.neverExpires { + NSLocalizedString("Messages in this chat will never be deleted.", comment: "alert message") + } else { + NSLocalizedString("This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.", comment: "alert message") + } + + showAlert(title, message: message) { + [ + UIAlertAction( + title: ttl.neverExpires ? NSLocalizedString("Disable delete messages", comment: "alert button") : NSLocalizedString("Delete messages", comment: "alert button"), + style: .destructive, + handler: { _ in onConfirm() } + ), + UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel, handler: { _ in onCancel() }) + ] + } +} + +private func deleteContactOrConversationDialog( + _ chat: Chat, + _ contact: Contact, + _ dismissToChatList: Bool, + _ showAlert: @escaping (SomeAlert) -> Void, + _ showActionSheet: @escaping (SomeActionSheet) -> Void, + _ showSheetContent: @escaping (SomeSheet) -> Void +) { + showActionSheet(SomeActionSheet( + actionSheet: ActionSheet( + title: Text("Delete contact?"), + buttons: [ + .destructive(Text("Only delete conversation")) { + deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .messages, dismissToChatList, showAlert) + }, + .destructive(Text("Delete contact")) { + showSheetContent(SomeSheet( + content: { AnyView( + DeleteActiveContactDialog( + chat: chat, + contact: contact, + dismissToChatList: dismissToChatList, + showAlert: showAlert + ) + ) }, + id: "DeleteActiveContactDialog" + )) + }, + .cancel() + ] + ), + id: "deleteContactOrConversationDialog" + )) +} + +private func deleteContactMaybeErrorAlert( + _ chat: Chat, + _ contact: Contact, + chatDeleteMode: ChatDeleteMode, + _ dismissToChatList: Bool, + _ showAlert: @escaping (SomeAlert) -> Void +) { + Task { + let alert_ = await deleteContactChat(chat, chatDeleteMode: chatDeleteMode) + if let alert = alert_ { + showAlert(SomeAlert(alert: alert, id: "deleteContactMaybeErrorAlert, error")) + } else { + if dismissToChatList { + await MainActor.run { + ChatModel.shared.chatId = nil + } + DispatchQueue.main.async { + dismissAllSheets(animated: true) { + if case .messages = chatDeleteMode, showDeleteConversationNoticeDefault.get() { + AlertManager.shared.showAlert(deleteConversationNotice(contact)) + } else if chatDeleteMode.isEntity, showDeleteContactNoticeDefault.get() { + AlertManager.shared.showAlert(deleteContactNotice(contact)) + } + } + } + } else { + if case .messages = chatDeleteMode, showDeleteConversationNoticeDefault.get() { + showAlert(SomeAlert(alert: deleteConversationNotice(contact), id: "deleteContactMaybeErrorAlert, deleteConversationNotice")) + } else if chatDeleteMode.isEntity, showDeleteContactNoticeDefault.get() { + showAlert(SomeAlert(alert: deleteContactNotice(contact), id: "deleteContactMaybeErrorAlert, deleteContactNotice")) + } + } + } + } +} + +private func deleteConversationNotice(_ contact: Contact) -> Alert { + return Alert( + title: Text("Conversation deleted!"), + message: Text("You can send messages to \(contact.displayName) from Archived contacts."), + primaryButton: .default(Text("Don't show again")) { + showDeleteConversationNoticeDefault.set(false) + }, + secondaryButton: .default(Text("Ok")) + ) +} + +private func deleteContactNotice(_ contact: Contact) -> Alert { + return Alert( + title: Text("Contact deleted!"), + message: Text("You can still view conversation with \(contact.displayName) in the list of chats."), + primaryButton: .default(Text("Don't show again")) { + showDeleteContactNoticeDefault.set(false) + }, + secondaryButton: .default(Text("Ok")) + ) +} + +enum ContactDeleteMode { + case full + case entity + + public func toChatDeleteMode(notify: Bool) -> ChatDeleteMode { + switch self { + case .full: .full(notify: notify) + case .entity: .entity(notify: notify) + } + } +} + +struct DeleteActiveContactDialog: View { + @Environment(\.dismiss) var dismiss + @EnvironmentObject var theme: AppTheme + var chat: Chat + var contact: Contact + var dismissToChatList: Bool + var showAlert: (SomeAlert) -> Void + @State private var keepConversation = false + + var body: some View { + NavigationView { + List { + Section { + Toggle("Keep conversation", isOn: $keepConversation) + + Button(role: .destructive) { + dismiss() + deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: contactDeleteMode.toChatDeleteMode(notify: false), dismissToChatList, showAlert) + } label: { + Text("Delete without notification") + } + + Button(role: .destructive) { + dismiss() + deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: contactDeleteMode.toChatDeleteMode(notify: true), dismissToChatList, showAlert) + } label: { + Text("Delete and notify contact") + } + } footer: { + Text("Contact will be deleted - this cannot be undone!") + .foregroundColor(theme.colors.secondary) + } + } + .modifier(ThemedBackground(grouped: true)) + } + } + + var contactDeleteMode: ContactDeleteMode { + keepConversation ? .entity : .full + } +} + +private func deleteContactWithoutConversation( + _ chat: Chat, + _ contact: Contact, + _ dismissToChatList: Bool, + _ showAlert: @escaping (SomeAlert) -> Void, + _ showActionSheet: @escaping (SomeActionSheet) -> Void +) { + showActionSheet(SomeActionSheet( + actionSheet: ActionSheet( + title: Text("Confirm contact deletion?"), + buttons: [ + .destructive(Text("Delete and notify contact")) { + deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .full(notify: true), dismissToChatList, showAlert) + }, + .destructive(Text("Delete without notification")) { + deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .full(notify: false), dismissToChatList, showAlert) + }, + .cancel() + ] + ), + id: "deleteContactWithoutConversation" + )) +} + +private func deleteNotReadyContact( + _ chat: Chat, + _ contact: Contact, + _ dismissToChatList: Bool, + _ showAlert: @escaping (SomeAlert) -> Void, + _ showActionSheet: @escaping (SomeActionSheet) -> Void +) { + showActionSheet(SomeActionSheet( + actionSheet: ActionSheet( + title: Text("Confirm contact deletion?"), + buttons: [ + .destructive(Text("Confirm")) { + deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .full(notify: false), dismissToChatList, showAlert) + }, + .cancel() + ] + ), + id: "deleteNotReadyContact" + )) +} + struct ChatInfoView_Previews: PreviewProvider { static var previews: some View { ChatInfoView( chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), contact: Contact.sampleData, - connectionStats: Binding.constant(nil), - customUserProfile: Binding.constant(nil), localAlias: "", - connectionCode: Binding.constant(nil) + featuresAllowed: contactUserPrefsToFeaturesAllowed(Contact.sampleData.mergedPreferences), + currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(Contact.sampleData.mergedPreferences), + onSearch: {} ) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift index bcdeb7fd9c..30f5e7a589 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift @@ -9,6 +9,7 @@ import SwiftUI class AnimatedImageView: UIView { var image: UIImage? = nil var imageView: UIImageView? = nil + var cMode: UIView.ContentMode = .scaleAspectFit override init(frame: CGRect) { super.init(frame: frame) @@ -18,11 +19,12 @@ class AnimatedImageView: UIView { fatalError("Not implemented") } - convenience init(image: UIImage) { + convenience init(image: UIImage, contentMode: UIView.ContentMode) { self.init() self.image = image + self.cMode = contentMode imageView = UIImageView(gifImage: image) - imageView!.contentMode = .scaleAspectFit + imageView!.contentMode = contentMode self.addSubview(imageView!) } @@ -35,7 +37,7 @@ class AnimatedImageView: UIView { if let subview = self.subviews.first as? UIImageView { if image.imageData != subview.gifImage?.imageData { imageView = UIImageView(gifImage: image) - imageView!.contentMode = .scaleAspectFit + imageView!.contentMode = contentMode self.addSubview(imageView!) subview.removeFromSuperview() } @@ -47,13 +49,15 @@ class AnimatedImageView: UIView { struct SwiftyGif: UIViewRepresentable { private let image: UIImage + private let contentMode: UIView.ContentMode - init(image: UIImage) { + init(image: UIImage, contentMode: UIView.ContentMode = .scaleAspectFit) { self.image = image + self.contentMode = contentMode } func makeUIView(context: Context) -> AnimatedImageView { - AnimatedImageView(image: image) + AnimatedImageView(image: image, contentMode: contentMode) } func updateUIView(_ imageView: AnimatedImageView, context: Context) { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift index f0bf43dffe..0283e9c07e 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct CICallItemView: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat var chatItem: ChatItem var status: CICallStatus @@ -22,7 +23,7 @@ struct CICallItemView: View { switch status { case .pending: if sent { - Image(systemName: "phone.arrow.up.right").foregroundColor(.secondary) + Image(systemName: "phone.arrow.up.right").foregroundColor(theme.colors.secondary) } else { acceptCallButton() } @@ -35,9 +36,7 @@ struct CICallItemView: View { case .error: missedCallIcon(sent).foregroundColor(.orange) } - chatItem.timestampText - .font(.caption) - .foregroundColor(.secondary) + CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary, showStatus: false, showEdited: false) .padding(.bottom, 8) .padding(.horizontal, 12) } @@ -51,28 +50,28 @@ struct CICallItemView: View { Image(systemName: "phone.connection").foregroundColor(.green) } - @ViewBuilder private func endedCallIcon(_ sent: Bool) -> some View { + private func endedCallIcon(_ sent: Bool) -> some View { HStack { Image(systemName: "phone.down") - Text(durationText(duration)).foregroundColor(.secondary) + Text(durationText(duration)).foregroundColor(theme.colors.secondary) } } @ViewBuilder private func acceptCallButton() -> some View { if case let .direct(contact) = chat.chatInfo { - Button { - if let invitation = m.callInvitations[contact.id] { - CallController.shared.answerCall(invitation: invitation) - logger.debug("acceptCallButton call answered") - } else { - AlertManager.shared.showAlertMsg(title: "Call already ended!") - } - } label: { - Label("Answer call", systemImage: "phone.arrow.down.left") - } + Label("Answer call", systemImage: "phone.arrow.down.left") + .foregroundColor(theme.colors.primary) + .simultaneousGesture(TapGesture().onEnded { + if let invitation = m.callInvitations[contact.id] { + CallController.shared.answerCall(invitation: invitation) + logger.debug("acceptCallButton call answered") + } else { + AlertManager.shared.showAlertMsg(title: "Call already ended!") + } + }) } else { - Image(systemName: "phone.arrow.down.left").foregroundColor(.secondary) + Image(systemName: "phone.arrow.down.left").foregroundColor(theme.colors.secondary) } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift index 03afa30331..02be8af73b 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift @@ -11,14 +11,17 @@ import SimpleXChat struct CIChatFeatureView: View { @EnvironmentObject var m: ChatModel + @Environment(\.revealed) var revealed: Bool + @ObservedObject var im = ItemsModel.shared + @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem - @Binding var revealed: Bool var feature: Feature var icon: String? = nil var iconColor: Color var body: some View { - if !revealed, let fs = mergedFeautures() { + if !revealed, let fs = mergedFeatures() { HStack { ForEach(fs, content: featureIconView) } @@ -47,12 +50,12 @@ struct CIChatFeatureView: View { } } - private func mergedFeautures() -> [FeatureInfo]? { + private func mergedFeatures() -> [FeatureInfo]? { var fs: [FeatureInfo] = [] var icons: Set = [] if var i = m.getChatItemIndex(chatItem) { - while i < m.reversedChatItems.count, - let f = featureInfo(m.reversedChatItems[i]) { + while i < im.reversedChatItems.count, + let f = featureInfo(im.reversedChatItems[i]) { if !icons.contains(f.icon) { fs.insert(f, at: 0) icons.insert(f.icon) @@ -65,10 +68,10 @@ struct CIChatFeatureView: View { private func featureInfo(_ ci: ChatItem) -> FeatureInfo? { switch ci.content { - case let .rcvChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor, param) - case let .sndChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor, param) - case let .rcvGroupFeature(feature, preference, param): FeatureInfo(feature, preference.enable.iconColor, param) - case let .sndGroupFeature(feature, preference, param): FeatureInfo(feature, preference.enable.iconColor, param) + case let .rcvChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor(theme.colors.secondary), param) + case let .sndChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor(theme.colors.secondary), param) + case let .rcvGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary), param) + case let .sndGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary), param) default: nil } } @@ -80,7 +83,7 @@ struct CIChatFeatureView: View { if let param = f.param { HStack { i - chatEventText(Text(param)).lineLimit(1) + chatEventText(Text(param), theme.colors.secondary).lineLimit(1) } } else { i @@ -92,7 +95,7 @@ struct CIChatFeatureView: View { Image(systemName: icon ?? feature.iconFilled) .foregroundColor(iconColor) .scaleEffect(feature.iconScale) - chatEventText(chatItem) + chatEventText(chatItem, theme.colors.secondary) } .padding(.horizontal, 6) .padding(.vertical, 4) @@ -103,6 +106,9 @@ struct CIChatFeatureView: View { struct CIChatFeatureView_Previews: PreviewProvider { static var previews: some View { let enabled = FeatureEnabled(forUser: false, forContact: false) - CIChatFeatureView(chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), revealed: Binding.constant(true), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor) + CIChatFeatureView( + chat: Chat.sampleData, + chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor(.secondary) + ).environment(\.revealed, true) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift index b6be3f837e..1375b87a5a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift @@ -17,6 +17,7 @@ struct CIEventView: View { .padding(.horizontal, 6) .padding(.vertical, 4) .textSelection(.disabled) + .lineLimit(4) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift index e52a92a3c6..67f7b69e2c 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct CIFeaturePreferenceView: View { @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem var feature: ChatFeature var allowed: FeatureAllowed @@ -19,15 +20,15 @@ struct CIFeaturePreferenceView: View { var body: some View { HStack(alignment: .center, spacing: 4) { Image(systemName: feature.icon) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .scaleEffect(feature.iconScale) if let ct = chat.chatInfo.contact, allowed != .no && ct.allowsFeature(feature) && !ct.userAllowsFeature(feature) { let setParam = feature == .timedMessages && ct.mergedPreferences.timedMessages.userPreference.preference.ttl == nil featurePreferenceView(acceptText: setParam ? "Set 1 day" : "Accept") - .onTapGesture { + .simultaneousGesture(TapGesture().onEnded { allowFeatureToContact(ct, feature, param: setParam ? 86400 : nil) - } + }) } else { featurePreferenceView() } @@ -40,17 +41,17 @@ struct CIFeaturePreferenceView: View { private func featurePreferenceView(acceptText: LocalizedStringKey? = nil) -> some View { var r = Text(CIContent.preferenceText(feature, allowed, param) + " ") .fontWeight(.light) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) if let acceptText { r = r + Text(acceptText) .fontWeight(.medium) - .foregroundColor(.accentColor) - + Text(" ") + .foregroundColor(theme.colors.primary) + + Text(verbatim: " ") } r = r + chatItem.timestampText .fontWeight(.light) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) return r.font(.caption) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index 4ae2296f46..b0b404d8b5 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -11,15 +11,19 @@ import SimpleXChat struct CIFileView: View { @EnvironmentObject var m: ChatModel - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme let file: CIFile? let edited: Bool + var smallViewSize: CGFloat? var body: some View { - let metaReserve = edited - ? " " - : " " - Button(action: fileAction) { + if smallViewSize != nil { + fileIndicator() + .simultaneousGesture(TapGesture().onEnded(fileAction)) + } else { + let metaReserve = edited + ? " " + : " " HStack(alignment: .bottom, spacing: 6) { fileIndicator() .padding(.top, 5) @@ -30,12 +34,12 @@ struct CIFileView: View { Text(file.fileName) .lineLimit(1) .multilineTextAlignment(.leading) - .foregroundColor(.primary) + .foregroundColor(theme.colors.onBackground) Text(prettyFileSize + metaReserve) .font(.caption) .lineLimit(1) .multilineTextAlignment(.leading) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } else { Text(metaReserve) @@ -45,48 +49,45 @@ struct CIFileView: View { .padding(.bottom, 6) .padding(.leading, 10) .padding(.trailing, 12) + .simultaneousGesture(TapGesture().onEnded(fileAction)) + .disabled(!itemInteractive) } - .disabled(!itemInteractive) } + @inline(__always) private var itemInteractive: Bool { if let file = file { switch (file.fileStatus) { - case .sndStored: return false + case .sndStored: return file.fileProtocol == .local case .sndTransfer: return false - case .sndComplete: return false + case .sndComplete: return true case .sndCancelled: return false - case .sndError: return false + case .sndError: return true + case .sndWarning: return true case .rcvInvitation: return true case .rcvAccepted: return true case .rcvTransfer: return false + case .rcvAborted: return true case .rcvComplete: return true case .rcvCancelled: return false - case .rcvError: return false + case .rcvError: return true + case .rcvWarning: return true case .invalid: return false } } return false } - private func fileSizeValid() -> Bool { - if let file = file { - return file.fileSize <= getMaxFileSize(file.fileProtocol) - } - return false - } - private func fileAction() { logger.debug("CIFileView fileAction") if let file = file { switch (file.fileStatus) { - case .rcvInvitation: - if fileSizeValid() { + case .rcvInvitation, .rcvAborted: + if fileSizeValid(file) { Task { - logger.debug("CIFileView fileAction - in .rcvInvitation, in Task") + logger.debug("CIFileView fileAction - in .rcvInvitation, .rcvAborted, in Task") if let user = m.currentUser { - let encrypted = privacyEncryptLocalFilesGroupDefault.get() - await receiveFile(user: user, fileId: file.fileId, encrypted: encrypted) + await receiveFile(user: user, fileId: file.fileId) } } } else { @@ -108,12 +109,35 @@ struct CIFileView: View { title: "Waiting for file", message: "File will be received when your contact is online, please wait or check later!" ) + case .local: () } case .rcvComplete: logger.debug("CIFileView fileAction - in .rcvComplete") if let fileSource = getLoadedFileSource(file) { saveCryptoFile(fileSource) } + case let .rcvError(rcvFileError): + logger.debug("CIFileView fileAction - in .rcvError") + showFileErrorAlert(rcvFileError) + case let .rcvWarning(rcvFileError): + logger.debug("CIFileView fileAction - in .rcvWarning") + showFileErrorAlert(rcvFileError, temporary: true) + case .sndStored: + logger.debug("CIFileView fileAction - in .sndStored") + if file.fileProtocol == .local, let fileSource = getLoadedFileSource(file) { + saveCryptoFile(fileSource) + } + case .sndComplete: + logger.debug("CIFileView fileAction - in .sndComplete") + if let fileSource = getLoadedFileSource(file) { + saveCryptoFile(fileSource) + } + case let .sndError(sndFileError): + logger.debug("CIFileView fileAction - in .sndError") + showFileErrorAlert(sndFileError) + case let .sndWarning(sndFileError): + logger.debug("CIFileView fileAction - in .sndWarning") + showFileErrorAlert(sndFileError, temporary: true) default: break } } @@ -126,18 +150,21 @@ struct CIFileView: View { switch file.fileProtocol { case .xftp: progressView() case .smp: fileIcon("doc.fill") + case .local: fileIcon("doc.fill") } case let .sndTransfer(sndProgress, sndTotal): switch file.fileProtocol { case .xftp: progressCircle(sndProgress, sndTotal) case .smp: progressView() + case .local: EmptyView() } case .sndComplete: fileIcon("doc.fill", innerIcon: "checkmark", innerIconSize: 10) case .sndCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) case .sndError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) + case .sndWarning: fileIcon("doc.fill", innerIcon: "exclamationmark.triangle.fill", innerIconSize: 10) case .rcvInvitation: - if fileSizeValid() { - fileIcon("arrow.down.doc.fill", color: .accentColor) + if fileSizeValid(file) { + fileIcon("arrow.down.doc.fill", color: theme.colors.primary) } else { fileIcon("doc.fill", color: .orange, innerIcon: "exclamationmark", innerIconSize: 12) } @@ -148,9 +175,12 @@ struct CIFileView: View { } else { progressView() } + case .rcvAborted: + fileIcon("doc.fill", color: theme.colors.primary, innerIcon: "exclamationmark.arrow.circlepath", innerIconSize: 12) case .rcvComplete: fileIcon("doc.fill") case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) case .rcvError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) + case .rcvWarning: fileIcon("doc.fill", innerIcon: "exclamationmark.triangle.fill", innerIconSize: 10) case .invalid: fileIcon("doc.fill", innerIcon: "questionmark", innerIconSize: 10) } } else { @@ -159,21 +189,22 @@ struct CIFileView: View { } private func fileIcon(_ icon: String, color: Color = Color(uiColor: .tertiaryLabel), innerIcon: String? = nil, innerIconSize: CGFloat? = nil) -> some View { - ZStack(alignment: .center) { + let size = smallViewSize ?? 30 + return ZStack(alignment: .center) { Image(systemName: icon) .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 30, height: 30) + .frame(width: size, height: size) .foregroundColor(color) if let innerIcon = innerIcon, - let innerIconSize = innerIconSize { + let innerIconSize = innerIconSize, (smallViewSize == nil || file?.showStatusIconInSmallView == true) { Image(systemName: innerIcon) .resizable() .aspectRatio(contentMode: .fit) .frame(maxHeight: 16) .frame(width: innerIconSize, height: innerIconSize) .foregroundColor(.white) - .padding(.top, 12) + .padding(.top, size / 2.5) } } } @@ -194,6 +225,13 @@ struct CIFileView: View { } } +func fileSizeValid(_ file: CIFile?) -> Bool { + if let file = file { + return file.fileSize <= getMaxFileSize(file.fileProtocol) + } + return false +} + func saveCryptoFile(_ fileSource: CryptoFile) { if let cfArgs = fileSource.cryptoArgs { let url = getAppFilePath(fileSource.filePath) @@ -218,6 +256,26 @@ func saveCryptoFile(_ fileSource: CryptoFile) { } } +func showFileErrorAlert(_ err: FileError, temporary: Bool = false) { + let title: String = if temporary { + NSLocalizedString("Temporary file error", comment: "file error alert title") + } else { + NSLocalizedString("File error", comment: "file error alert title") + } + if let btn = err.moreInfoButton { + showAlert(title, message: err.errorInfo) { + [ + okAlertAction, + UIAlertAction(title: NSLocalizedString("How it works", comment: "alert button"), style: .default, handler: { _ in + UIApplication.shared.open(contentModerationPostLink) + }) + ] + } + } else { + showAlert(title, message: err.errorInfo) + } +} + struct CIFileView_Previews: PreviewProvider { static var previews: some View { let sentFile: ChatItem = ChatItem( @@ -235,17 +293,18 @@ struct CIFileView_Previews: PreviewProvider { file: nil ) Group { - ChatItemView(chat: Chat.sampleData, chatItem: sentFile, revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(), revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation), revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation), revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation), revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: fileChatItemWtFile, revealed: Binding.constant(false)) + ChatItemView(chat: Chat.sampleData, chatItem: sentFile, scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(), scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation), scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation), scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation), scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: fileChatItemWtFile, scrollToItemId: { _ in }) } + .environment(\.revealed, false) .previewLayout(.fixed(width: 360, height: 360)) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift index 72013877ca..3fcf578875 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift @@ -11,7 +11,9 @@ import SimpleXChat struct CIGroupInvitationView: View { @EnvironmentObject var chatModel: ChatModel - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme + @Environment(\.showTimestamp) var showTimestamp: Bool + @ObservedObject var chat: Chat var chatItem: ChatItem var groupInvitation: CIGroupInvitation var memberRole: GroupMemberRole @@ -20,6 +22,8 @@ struct CIGroupInvitationView: View { @State private var inProgress = false @State private var progressByTimeout = false + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false + var body: some View { let action = !chatItem.chatDir.sent && groupInvitation.status == .pending let v = ZStack(alignment: .bottomTrailing) { @@ -37,16 +41,22 @@ struct CIGroupInvitationView: View { VStack(alignment: .leading, spacing: 2) { groupInvitationText() .overlay(DetermineWidth()) - Text(chatIncognito ? "Tap to join incognito" : "Tap to join") - .foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : .accentColor) - .font(.callout) - .padding(.trailing, 60) - .overlay(DetermineWidth()) + ( + Text(chatIncognito ? "Tap to join incognito" : "Tap to join") + .foregroundColor(inProgress ? theme.colors.secondary : chatIncognito ? .indigo : theme.colors.primary) + .font(.callout) + + Text(verbatim: " ") + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) + ) + .overlay(DetermineWidth()) } } else { - groupInvitationText() - .padding(.trailing, 60) - .overlay(DetermineWidth()) + ( + groupInvitationText() + + Text(verbatim: " ") + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) + ) + .overlay(DetermineWidth()) } } .padding(.bottom, 2) @@ -56,14 +66,11 @@ struct CIGroupInvitationView: View { } } - chatItem.timestampText - .font(.caption) - .foregroundColor(.secondary) + CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary, showStatus: false, showEdited: false) } .padding(.horizontal, 12) .padding(.vertical, 6) - .background(chatItemFrameColor(chatItem, colorScheme)) - .cornerRadius(18) + .background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) } .textSelection(.disabled) .onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 } .onChange(of: inProgress) { inProgress in @@ -77,12 +84,12 @@ struct CIGroupInvitationView: View { } if action { - v.onTapGesture { + v.simultaneousGesture(TapGesture().onEnded { inProgress = true joinGroup(groupInvitation.groupId) { await MainActor.run { inProgress = false } } - } + }) .disabled(inProgress) } else { v @@ -92,7 +99,7 @@ struct CIGroupInvitationView: View { private func groupInfoView(_ action: Bool) -> some View { var color: Color if action && !inProgress { - color = chatIncognito ? .indigo : .accentColor + color = chatIncognito ? .indigo : theme.colors.primary } else { color = Color(uiColor: .tertiaryLabel) } @@ -100,9 +107,9 @@ struct CIGroupInvitationView: View { ProfileImage( imageStr: groupInvitation.groupProfile.image, iconName: "person.2.circle.fill", + size: 44, color: color ) - .frame(width: 44, height: 44) .padding(.trailing, 4) VStack(alignment: .leading) { let p = groupInvitation.groupProfile @@ -115,7 +122,7 @@ struct CIGroupInvitationView: View { } } - private func groupInvitationText() -> some View { + private func groupInvitationText() -> Text { Text(groupInvitationStr()) .font(.callout) } @@ -137,8 +144,8 @@ struct CIGroupInvitationView: View { struct CIGroupInvitationView_Previews: PreviewProvider { static var previews: some View { Group { - CIGroupInvitationView(chatItem: ChatItem.getGroupInvitationSample(), groupInvitation: CIGroupInvitation.getSample(groupProfile: GroupProfile(displayName: "team", fullName: "team")), memberRole: .admin) - CIGroupInvitationView(chatItem: ChatItem.getGroupInvitationSample(), groupInvitation: CIGroupInvitation.getSample(status: .accepted), memberRole: .admin) + CIGroupInvitationView(chat: Chat.sampleData, chatItem: ChatItem.getGroupInvitationSample(), groupInvitation: CIGroupInvitation.getSample(groupProfile: GroupProfile(displayName: "team", fullName: "team")), memberRole: .admin) + CIGroupInvitationView(chat: Chat.sampleData, chatItem: ChatItem.getGroupInvitationSample(), groupInvitation: CIGroupInvitation.getSample(status: .accepted), memberRole: .admin) } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift index 9ae52ae01b..d30369339d 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -11,34 +11,47 @@ import SimpleXChat struct CIImageView: View { @EnvironmentObject var m: ChatModel - @Environment(\.colorScheme) var colorScheme let chatItem: ChatItem - let image: String + var scrollToItemId: ((ChatItem.ID) -> Void)? = nil + var preview: UIImage? let maxWidth: CGFloat - @Binding var imgWidth: CGFloat? - @State var scrollProxy: ScrollViewProxy? - @State var metaColor: Color - @State private var showFullScreenImage = false + var imgWidth: CGFloat? + var smallView: Bool = false + @Binding var showFullScreenImage: Bool + @State private var blurred: Bool = UserDefaults.standard.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) > 0 var body: some View { let file = chatItem.file VStack(alignment: .center, spacing: 6) { if let uiImage = getLoadedImage(file) { - imageView(uiImage) + Group { if smallView { smallViewImageView(uiImage) } else { imageView(uiImage) } } .fullScreenCover(isPresented: $showFullScreenImage) { - FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage, scrollProxy: scrollProxy) + FullScreenMediaView(chatItem: chatItem, scrollToItemId: scrollToItemId, image: uiImage, showView: $showFullScreenImage) } - .onTapGesture { showFullScreenImage = true } - } else if let data = Data(base64Encoded: dropImagePrefix(image)), - let uiImage = UIImage(data: data) { - imageView(uiImage) - .onTapGesture { + .if(!smallView) { view in + view.modifier(PrivacyBlur(blurred: $blurred)) + } + .if(!blurred) { v in + v.simultaneousGesture(TapGesture().onEnded { showFullScreenImage = true }) + } + .onChange(of: m.activeCallViewIsCollapsed) { _ in + showFullScreenImage = false + } + } else if let preview { + Group { + if smallView { + smallViewImageView(preview) + } else { + imageView(preview).modifier(PrivacyBlur(blurred: $blurred)) + } + } + .simultaneousGesture(TapGesture().onEnded { if let file = file { switch file.fileStatus { - case .rcvInvitation: + case .rcvInvitation, .rcvAborted: Task { if let user = m.currentUser { - await receiveFile(user: user, fileId: file.fileId, encrypted: chatItem.encryptLocalFile) + await receiveFile(user: user, fileId: file.fileId) } } case .rcvAccepted: @@ -53,33 +66,63 @@ struct CIImageView: View { title: "Waiting for image", message: "Image will be received when your contact is online, please wait or check later!" ) + case .local: () } case .rcvTransfer: () // ? case .rcvComplete: () // ? case .rcvCancelled: () // TODO + case let .rcvError(rcvFileError): + showFileErrorAlert(rcvFileError) + case let .rcvWarning(rcvFileError): + showFileErrorAlert(rcvFileError, temporary: true) + case let .sndError(sndFileError): + showFileErrorAlert(sndFileError) + case let .sndWarning(sndFileError): + showFileErrorAlert(sndFileError, temporary: true) default: () } } - } + }) } } + .onDisappear { + showFullScreenImage = false + } } private func imageView(_ img: UIImage) -> some View { - let w = img.size.width <= img.size.height ? maxWidth * 0.75 : img.imageData == nil ? .infinity : maxWidth - DispatchQueue.main.async { imgWidth = w } + let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth return ZStack(alignment: .topTrailing) { if img.imageData == nil { Image(uiImage: img) .resizable() .scaledToFit() - .frame(maxWidth: w) + .frame(width: w) } else { SwiftyGif(image: img) .frame(width: w, height: w * img.size.height / img.size.width) .scaledToFit() } - loadingIndicator() + if !blurred || !showDownloadButton(chatItem.file?.fileStatus) { + loadingIndicator() + } + } + } + + private func smallViewImageView(_ img: UIImage) -> some View { + ZStack(alignment: .topTrailing) { + if img.imageData == nil { + Image(uiImage: img) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: maxWidth, height: maxWidth) + } else { + SwiftyGif(image: img, contentMode: .scaleAspectFill) + .frame(width: maxWidth, height: maxWidth) + } + if chatItem.file?.showStatusIconInSmallView == true { + loadingIndicator() + } } } @@ -90,18 +133,22 @@ struct CIImageView: View { switch file.fileProtocol { case .xftp: progressView() case .smp: EmptyView() + case .local: EmptyView() } case .sndTransfer: progressView() case .sndComplete: fileIcon("checkmark", 10, 13) case .sndCancelled: fileIcon("xmark", 10, 13) case .sndError: fileIcon("xmark", 10, 13) + case .sndWarning: fileIcon("exclamationmark.triangle.fill", 10, 13) case .rcvInvitation: fileIcon("arrow.down", 10, 13) case .rcvAccepted: fileIcon("ellipsis", 14, 11) case .rcvTransfer: progressView() + case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11) + case .rcvComplete: EmptyView() case .rcvCancelled: fileIcon("xmark", 10, 13) case .rcvError: fileIcon("xmark", 10, 13) + case .rcvWarning: fileIcon("exclamationmark.triangle.fill", 10, 13) case .invalid: fileIcon("questionmark", 10, 13) - default: EmptyView() } } } @@ -109,9 +156,9 @@ struct CIImageView: View { private func fileIcon(_ icon: String, _ size: CGFloat, _ padding: CGFloat) -> some View { Image(systemName: icon) .resizable() + .invertedForegroundStyle() .aspectRatio(contentMode: .fit) .frame(width: size, height: size) - .foregroundColor(metaColor) .padding(padding) } @@ -122,4 +169,12 @@ struct CIImageView: View { .tint(.white) .padding(8) } + + private func showDownloadButton(_ fileStatus: CIFileStatus?) -> Bool { + switch fileStatus { + case .rcvInvitation: true + case .rcvAborted: true + default: false + } + } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift index 0299a5e6f8..5e9fa691de 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift @@ -7,9 +7,11 @@ // import SwiftUI +import SimpleXChat struct CIInvalidJSONView: View { - var json: String + @EnvironmentObject var theme: AppTheme + var json: Data? @State private var showJSON = false var body: some View { @@ -21,18 +23,17 @@ struct CIInvalidJSONView: View { .padding(.horizontal, 12) .padding(.vertical, 6) .background(Color(uiColor: .tertiarySystemGroupedBackground)) - .cornerRadius(18) .textSelection(.disabled) - .onTapGesture { showJSON = true } - .sheet(isPresented: $showJSON) { - invalidJSONView(json) + .simultaneousGesture(TapGesture().onEnded { showJSON = true }) + .appSheet(isPresented: $showJSON) { + invalidJSONView(dataToString(json)) } } } func invalidJSONView(_ json: String) -> some View { VStack(alignment: .leading, spacing: 16) { - Button { + Button { // this is used in the sheet, Button works here showShareSheet(items: [json]) } label: { Image(systemName: "square.and.arrow.up") @@ -44,10 +45,11 @@ func invalidJSONView(_ json: String) -> some View { } .frame(maxHeight: .infinity) .padding() + .modifier(ThemedBackground()) } struct CIInvalidJSONView_Previews: PreviewProvider { static var previews: some View { - CIInvalidJSONView(json: "{}") + CIInvalidJSONView(json: "{}".data(using: .utf8)!) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift index 4c12c7312a..f9dbaede63 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift @@ -10,36 +10,59 @@ import SwiftUI import SimpleXChat struct CILinkView: View { - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme let linkPreview: LinkPreview + @State private var blurred: Bool = UserDefaults.standard.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) > 0 var body: some View { VStack(alignment: .center, spacing: 6) { - if let data = Data(base64Encoded: dropImagePrefix(linkPreview.image)), - let uiImage = UIImage(data: data) { + if let uiImage = imageFromBase64(linkPreview.image) { Image(uiImage: uiImage) .resizable() .scaledToFit() + .modifier(PrivacyBlur(blurred: $blurred)) + .if(!blurred) { v in + v.simultaneousGesture(TapGesture().onEnded { + openBrowserAlert(uri: linkPreview.uri) + }) + } } VStack(alignment: .leading, spacing: 6) { Text(linkPreview.title) .lineLimit(3) -// if linkPreview.description != "" { -// Text(linkPreview.description) -// .font(.subheadline) -// .lineLimit(12) -// } Text(linkPreview.uri.absoluteString) .font(.caption) .lineLimit(1) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } .padding(.horizontal, 12) .frame(maxWidth: .infinity, alignment: .leading) + .simultaneousGesture(TapGesture().onEnded { + openBrowserAlert(uri: linkPreview.uri) + }) } } } +func openBrowserAlert(uri: URL) { + showAlert( + NSLocalizedString("Open link?", comment: "alert title"), + message: uri.absoluteString, + actions: {[ + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "alert action"), + style: .default, + handler: { _ in } + ), + UIAlertAction( + title: NSLocalizedString("Open", comment: "alert action"), + style: .default, + handler: { _ in UIApplication.shared.open(uri) } + ) + ]} + ) +} + struct LargeLinkPreview_Previews: PreviewProvider { static var previews: some View { let preview = LinkPreview( diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift index da82ed4dd2..2898a318a9 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct CIMemberCreatedContactView: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem var body: some View { @@ -19,12 +20,11 @@ struct CIMemberCreatedContactView: View { case let .groupRcv(groupMember): if let contactId = groupMember.memberContactId { memberCreatedContactView(openText: "Open") - .onTapGesture { - dismissAllSheets(animated: true) - DispatchQueue.main.async { - m.chatId = "@\(contactId)" + .simultaneousGesture(TapGesture().onEnded { + ItemsModel.shared.loadOpenChat("@\(contactId)") { + dismissAllSheets(animated: true) } - } + }) } else { memberCreatedContactView() } @@ -43,12 +43,12 @@ struct CIMemberCreatedContactView: View { r = r + Text(openText) .fontWeight(.medium) - .foregroundColor(.accentColor) - + Text(" ") + .foregroundColor(theme.colors.primary) + + Text(verbatim: " ") } r = r + chatItem.timestampText .fontWeight(.light) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) return r.font(.caption) } @@ -56,11 +56,11 @@ struct CIMemberCreatedContactView: View { if let member = chatItem.memberDisplayName { return Text(member + " " + chatItem.content.text + " ") .fontWeight(.light) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } else { return Text(chatItem.content.text + " ") .fontWeight(.light) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift index c189abde24..fc73778239 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -11,97 +11,173 @@ import SimpleXChat struct CIMetaView: View { @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme + @Environment(\.showTimestamp) var showTimestamp: Bool var chatItem: ChatItem - var metaColor = Color.secondary - var paleMetaColor = Color(UIColor.tertiaryLabel) + var metaColor: Color + var paleMetaColor = Color(uiColor: .tertiaryLabel) + var showStatus = true + var showEdited = true + var invertedMaterial = false + + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false var body: some View { if chatItem.isDeletedContent { chatItem.timestampText.font(.caption).foregroundColor(metaColor) } else { - let meta = chatItem.meta - let ttl = chat.chatInfo.timedMessagesTTL - let encrypted = chatItem.encryptedFile - switch meta.itemStatus { - case let .sndSent(sndProgress): - switch sndProgress { - case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent) - case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent) + ZStack { + ciMetaText( + chatItem.meta, + chatTTL: chat.chatInfo.timedMessagesTTL, + encrypted: chatItem.encryptedFile, + color: metaColor, + paleColor: paleMetaColor, + colorMode: invertedMaterial + ? .invertedMaterial + : .normal, + showStatus: showStatus, + showEdited: showEdited, + showViaProxy: showSentViaProxy, + showTimesamp: showTimestamp + ).invertedForegroundStyle(enabled: invertedMaterial) + if invertedMaterial { + ciMetaText( + chatItem.meta, + chatTTL: chat.chatInfo.timedMessagesTTL, + encrypted: chatItem.encryptedFile, + colorMode: .normal, + onlyOverrides: true, + showStatus: showStatus, + showEdited: showEdited, + showViaProxy: showSentViaProxy, + showTimesamp: showTimestamp + ) } - case let .sndRcvd(_, sndProgress): - switch sndProgress { - case .complete: - ZStack { - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1) - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2) - } - case .partial: - ZStack { - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1) - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2) - } - } - default: - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor) } } } } -enum SentCheckmark { - case sent - case rcvd1 - case rcvd2 +enum MetaColorMode { + // Renders provided colours + case normal + // Fully transparent meta - used for reserving space + case transparent + // Renders white on dark backgrounds and black on light ones + case invertedMaterial + + func resolve(_ c: Color?) -> Color? { + switch self { + case .normal: c + case .transparent: .clear + case .invertedMaterial: nil + } + } + + func statusSpacer(_ sent: Bool) -> Text { + switch self { + case .normal, .transparent: + Text( + sent + ? Image("checkmark.wide") + : Image(systemName: "circlebadge.fill") + ).foregroundColor(.clear) + case .invertedMaterial: textSpace.kerning(13) + } + } } -func ciMetaText(_ meta: CIMeta, chatTTL: Int?, encrypted: Bool?, color: Color = .clear, transparent: Bool = false, sent: SentCheckmark? = nil) -> Text { +func ciMetaText( + _ meta: CIMeta, + chatTTL: Int?, + encrypted: Bool?, + color: Color = .clear, // we use this function to reserve space without rendering meta + paleColor: Color? = nil, + primaryColor: Color = .accentColor, + colorMode: MetaColorMode = .normal, + onlyOverrides: Bool = false, // only render colors that differ from base + showStatus: Bool = true, + showEdited: Bool = true, + showViaProxy: Bool, + showTimesamp: Bool +) -> Text { var r = Text("") - if meta.itemEdited { - r = r + statusIconText("pencil", color) + var space: Text? = nil + let appendSpace = { + if let sp = space { + r = r + sp + space = nil + } + } + let resolved = colorMode.resolve(color) + if showEdited, meta.itemEdited { + r = r + statusIconText("pencil", resolved) } if meta.disappearing { - r = r + statusIconText("timer", color).font(.caption2) + r = r + statusIconText("timer", resolved).font(.caption2) let ttl = meta.itemTimed?.ttl if ttl != chatTTL { - r = r + Text(shortTimeText(ttl)).foregroundColor(color) + r = r + colored(Text(shortTimeText(ttl)), resolved) } - r = r + Text(" ") + space = textSpace } - if let (icon, statusColor) = meta.statusIcon(color) { - let t = Text(Image(systemName: icon)).font(.caption2) - let gap = Text(" ").kerning(-1.25) - let t1 = t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67)) - switch sent { - case nil: r = r + t1 - case .sent: r = r + t1 + gap - case .rcvd1: r = r + t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67)) + gap - case .rcvd2: r = r + gap + t1 + if showViaProxy, meta.sentViaProxy == true { + appendSpace() + r = r + statusIconText("arrow.forward", resolved?.opacity(0.67)).font(.caption2) + } + if showStatus { + appendSpace() + if let (image, statusColor) = meta.itemStatus.statusIcon(color, paleColor ?? color, primaryColor) { + let metaColor = if onlyOverrides && statusColor == color { + Color.clear + } else { + colorMode.resolve(statusColor) + } + r = r + colored(Text(image), metaColor) + } else if !meta.disappearing { + r = r + colorMode.statusSpacer(meta.itemStatus.sent) } - r = r + Text(" ") - } else if !meta.disappearing { - r = r + statusIconText("circlebadge.fill", .clear) + Text(" ") + space = textSpace } if let enc = encrypted { - r = r + statusIconText(enc ? "lock" : "lock.open", color) + Text(" ") + appendSpace() + r = r + statusIconText(enc ? "lock" : "lock.open", resolved) + space = textSpace + } + if showTimesamp { + appendSpace() + r = r + colored(meta.timestampText, resolved) } - r = r + meta.timestampText.foregroundColor(color) return r.font(.caption) } -private func statusIconText(_ icon: String, _ color: Color) -> Text { - Text(Image(systemName: icon)).foregroundColor(color) +@inline(__always) +private func statusIconText(_ icon: String, _ color: Color?) -> Text { + colored(Text(Image(systemName: icon)), color) +} + +// Applying `foregroundColor(nil)` breaks `.invertedForegroundStyle` modifier +@inline(__always) +private func colored(_ t: Text, _ color: Color?) -> Text { + if let color { + t.foregroundColor(color) + } else { + t + } } struct CIMetaView_Previews: PreviewProvider { + static let metaColor = Color.secondary static var previews: some View { Group { - CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete))) - CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial))) - CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete))) - CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial))) - CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete))) - CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true)) - CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample()) + CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete)), metaColor: metaColor) + CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial)), metaColor: metaColor) + CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete)), metaColor: metaColor) + CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial)), metaColor: metaColor) + CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete)), metaColor: metaColor) + CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true), metaColor: metaColor) + CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(), metaColor: metaColor) } .previewLayout(.fixed(width: 360, height: 100)) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift index 3ad45d6987..4e5713c263 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift @@ -13,18 +13,22 @@ let decryptErrorReason: LocalizedStringKey = "It can happen when you or your con struct CIRcvDecryptionError: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat + @Environment(\.showTimestamp) var showTimestamp: Bool var msgDecryptError: MsgDecryptError var msgCount: UInt32 var chatItem: ChatItem @State private var alert: CIRcvDecryptionErrorAlert? + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false + enum CIRcvDecryptionErrorAlert: Identifiable { case syncAllowedAlert(_ syncConnection: () -> Void) case syncNotSupportedContactAlert case syncNotSupportedMemberAlert case decryptionErrorAlert - case error(title: LocalizedStringKey, error: LocalizedStringKey) + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -44,7 +48,7 @@ struct CIRcvDecryptionError: View { if case let .group(groupInfo) = chat.chatInfo, case let .groupRcv(groupMember) = chatItem.chatDir { do { - let (member, stats) = try apiGroupMemberInfo(groupInfo.apiId, groupMember.groupMemberId) + let (member, stats) = try apiGroupMemberInfoSync(groupInfo.apiId, groupMember.groupMemberId) if let s = stats { m.updateGroupMemberConnectionStats(groupInfo, member, s) } @@ -59,43 +63,46 @@ struct CIRcvDecryptionError: View { case .syncNotSupportedContactAlert: return Alert(title: Text("Fix not supported by contact"), message: message()) case .syncNotSupportedMemberAlert: return Alert(title: Text("Fix not supported by group member"), message: message()) case .decryptionErrorAlert: return Alert(title: Text("Decryption error"), message: message()) - case let .error(title, error): return Alert(title: Text(title), message: Text(error)) + case let .error(title, error): return mkAlert(title: title, message: error) } } } - @ViewBuilder private func viewBody() -> some View { - if case let .direct(contact) = chat.chatInfo, - let contactStats = contact.activeConn?.connectionStats { - if contactStats.ratchetSyncAllowed { - decryptionErrorItemFixButton(syncSupported: true) { - alert = .syncAllowedAlert { syncContactConnection(contact) } + private func viewBody() -> some View { + Group { + if case let .direct(contact) = chat.chatInfo, + let contactStats = contact.activeConn?.connectionStats { + if contactStats.ratchetSyncAllowed { + decryptionErrorItemFixButton(syncSupported: true) { + alert = .syncAllowedAlert { syncContactConnection(contact) } + } + } else if !contactStats.ratchetSyncSupported { + decryptionErrorItemFixButton(syncSupported: false) { + alert = .syncNotSupportedContactAlert + } + } else { + basicDecryptionErrorItem() } - } else if !contactStats.ratchetSyncSupported { - decryptionErrorItemFixButton(syncSupported: false) { - alert = .syncNotSupportedContactAlert + } else if case let .group(groupInfo) = chat.chatInfo, + case let .groupRcv(groupMember) = chatItem.chatDir, + let mem = m.getGroupMember(groupMember.groupMemberId), + let memberStats = mem.wrapped.activeConn?.connectionStats { + if memberStats.ratchetSyncAllowed { + decryptionErrorItemFixButton(syncSupported: true) { + alert = .syncAllowedAlert { syncMemberConnection(groupInfo, groupMember) } + } + } else if !memberStats.ratchetSyncSupported { + decryptionErrorItemFixButton(syncSupported: false) { + alert = .syncNotSupportedMemberAlert + } + } else { + basicDecryptionErrorItem() } } else { basicDecryptionErrorItem() } - } else if case let .group(groupInfo) = chat.chatInfo, - case let .groupRcv(groupMember) = chatItem.chatDir, - let mem = m.getGroupMember(groupMember.groupMemberId), - let memberStats = mem.wrapped.activeConn?.connectionStats { - if memberStats.ratchetSyncAllowed { - decryptionErrorItemFixButton(syncSupported: true) { - alert = .syncAllowedAlert { syncMemberConnection(groupInfo, groupMember) } - } - } else if !memberStats.ratchetSyncSupported { - decryptionErrorItemFixButton(syncSupported: false) { - alert = .syncNotSupportedMemberAlert - } - } else { - basicDecryptionErrorItem() - } - } else { - basicDecryptionErrorItem() } + .background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) } } private func basicDecryptionErrorItem() -> some View { @@ -112,24 +119,22 @@ struct CIRcvDecryptionError: View { } ( Text(Image(systemName: "exclamationmark.arrow.triangle.2.circlepath")) - .foregroundColor(syncSupported ? .accentColor : .secondary) + .foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary) .font(.callout) - + Text(" ") + + textSpace + Text("Fix connection") - .foregroundColor(syncSupported ? .accentColor : .secondary) + .foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary) .font(.callout) - + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true) + + Text(verbatim: " ") + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) ) } .padding(.horizontal, 12) - CIMetaView(chat: chat, chatItem: chatItem) + CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary) .padding(.horizontal, 12) } - .onTapGesture(perform: { onClick() }) + .simultaneousGesture(TapGesture().onEnded(onClick)) .padding(.vertical, 6) - .background(Color(uiColor: .tertiarySystemGroupedBackground)) - .cornerRadius(18) .textSelection(.disabled) } @@ -139,17 +144,15 @@ struct CIRcvDecryptionError: View { Text(chatItem.content.text) .foregroundColor(.red) .italic() - + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true) + + Text(verbatim: " ") + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) } .padding(.horizontal, 12) - CIMetaView(chat: chat, chatItem: chatItem) + CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary) .padding(.horizontal, 12) } - .onTapGesture(perform: { onClick() }) + .simultaneousGesture(TapGesture().onEnded(onClick)) .padding(.vertical, 6) - .background(Color(uiColor: .tertiarySystemGroupedBackground)) - .cornerRadius(18) .textSelection(.disabled) } @@ -158,13 +161,13 @@ struct CIRcvDecryptionError: View { let why = Text(decryptErrorReason) switch msgDecryptError { case .ratchetHeader: - message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why + message = Text("\(msgCount) messages failed to decrypt.") + textNewLine + why case .tooManySkipped: - message = Text("\(msgCount) messages skipped.") + Text("\n") + why + message = Text("\(msgCount) messages skipped.") + textNewLine + why case .ratchetEarlier: - message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why + message = Text("\(msgCount) messages failed to decrypt.") + textNewLine + why case .other: - message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why + message = Text("\(msgCount) messages failed to decrypt.") + textNewLine + why case .ratchetSync: message = Text("Encryption re-negotiation failed.") } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift index be8b25a0fc..eacbe9360a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift @@ -13,84 +13,167 @@ import Combine struct CIVideoView: View { @EnvironmentObject var m: ChatModel - @Environment(\.colorScheme) var colorScheme private let chatItem: ChatItem - private let image: String + private let preview: UIImage? @State private var duration: Int @State private var progress: Int = 0 @State private var videoPlaying: Bool = false private let maxWidth: CGFloat - @Binding private var videoWidth: CGFloat? - @State private var scrollProxy: ScrollViewProxy? - @State private var preview: UIImage? = nil + private var videoWidth: CGFloat? + private let smallView: Bool @State private var player: AVPlayer? @State private var fullPlayer: AVPlayer? @State private var url: URL? - @State private var showFullScreenPlayer = false + @State private var urlDecrypted: URL? + @State private var decryptionInProgress: Bool = false + @Binding private var showFullScreenPlayer: Bool @State private var timeObserver: Any? = nil @State private var fullScreenTimeObserver: Any? = nil @State private var publisher: AnyCancellable? = nil + private var sizeMultiplier: CGFloat { smallView ? 0.38 : 1 } + @State private var blurred: Bool = UserDefaults.standard.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) > 0 - init(chatItem: ChatItem, image: String, duration: Int, maxWidth: CGFloat, videoWidth: Binding, scrollProxy: ScrollViewProxy?) { + init(chatItem: ChatItem, preview: UIImage?, duration: Int, maxWidth: CGFloat, videoWidth: CGFloat?, smallView: Bool = false, showFullscreenPlayer: Binding) { self.chatItem = chatItem - self.image = image + self.preview = preview self._duration = State(initialValue: duration) self.maxWidth = maxWidth - self._videoWidth = videoWidth - self.scrollProxy = scrollProxy - if let url = getLoadedVideo(chatItem.file) { - self._player = State(initialValue: VideoPlayerView.getOrCreatePlayer(url, false)) - self._fullPlayer = State(initialValue: AVPlayer(url: url)) - self._url = State(initialValue: url) - } - if let data = Data(base64Encoded: dropImagePrefix(image)), - let uiImage = UIImage(data: data) { - self._preview = State(initialValue: uiImage) - } + self.videoWidth = videoWidth + self.smallView = smallView + self._showFullScreenPlayer = showFullscreenPlayer } var body: some View { let file = chatItem.file - ZStack { + ZStack(alignment: smallView ? .topLeading : .center) { ZStack(alignment: .topLeading) { - if let file = file, let preview = preview, let player = player, let url = url { - videoView(player, url, file, preview, duration) - } else if let data = Data(base64Encoded: dropImagePrefix(image)), - let uiImage = UIImage(data: data) { - imageView(uiImage) - .onTapGesture { - if let file = file { - switch file.fileStatus { - case .rcvInvitation: - receiveFileIfValidSize(file: file, encrypted: false, receiveFile: receiveFile) - case .rcvAccepted: - switch file.fileProtocol { - case .xftp: - AlertManager.shared.showAlertMsg( - title: "Waiting for video", - message: "Video will be received when your contact completes uploading it." - ) - case .smp: - AlertManager.shared.showAlertMsg( - title: "Waiting for video", - message: "Video will be received when your contact is online, please wait or check later!" - ) - } - case .rcvTransfer: () // ? - case .rcvComplete: () // ? - case .rcvCancelled: () // TODO - default: () - } + if let file, let preview { + if let urlDecrypted { + if smallView { + smallVideoView(urlDecrypted, file, preview) + } else if let player { + videoView(player, urlDecrypted, file, preview, duration) } + } else if file.loaded { + if smallView { + smallVideoViewEncrypted(file, preview) + } else { + videoViewEncrypted(file, preview, duration) + } + } else { + Group { if smallView { smallViewImageView(preview, file) } else { imageView(preview) } } + .simultaneousGesture(TapGesture().onEnded { + switch file.fileStatus { + case .rcvInvitation, .rcvAborted: + receiveFileIfValidSize(file: file, receiveFile: receiveFile) + case .rcvAccepted: + switch file.fileProtocol { + case .xftp: + AlertManager.shared.showAlertMsg( + title: "Waiting for video", + message: "Video will be received when your contact completes uploading it." + ) + case .smp: + AlertManager.shared.showAlertMsg( + title: "Waiting for video", + message: "Video will be received when your contact is online, please wait or check later!" + ) + case .local: () + } + case .rcvTransfer: () // ? + case .rcvComplete: () // ? + case .rcvCancelled: () // TODO + default: () + } + }) } } - durationProgress() + if !smallView { + durationProgress() + } } - if let file = file, case .rcvInvitation = file.fileStatus { - Button { - receiveFileIfValidSize(file: file, encrypted: false, receiveFile: receiveFile) - } label: { + if !blurred, let file, showDownloadButton(file.fileStatus) { + if !smallView || !file.showStatusIconInSmallView { playPauseIcon("play.fill") + .simultaneousGesture(TapGesture().onEnded { + receiveFileIfValidSize(file: file, receiveFile: receiveFile) + }) + } + } + } + .fullScreenCover(isPresented: $showFullScreenPlayer) { + if let decrypted = urlDecrypted { + fullScreenPlayer(decrypted) + } + } + .onAppear { + setupPlayer(chatItem.file) + } + .onChange(of: chatItem.file) { file in + // ChatItem can be changed in small view on chat list screen + setupPlayer(file) + } + .onDisappear { + showFullScreenPlayer = false + } + } + + private func setupPlayer(_ file: CIFile?) { + let newUrl = getLoadedVideo(file) + if newUrl == url { + return + } + url = nil + urlDecrypted = nil + player = nil + fullPlayer = nil + if let newUrl { + let decrypted = file?.fileSource?.cryptoArgs == nil ? newUrl : file?.fileSource?.decryptedGet() + urlDecrypted = decrypted + if let decrypted = decrypted { + player = VideoPlayerView.getOrCreatePlayer(decrypted, false) + fullPlayer = AVPlayer(url: decrypted) + } + url = newUrl + } + } + + private func showDownloadButton(_ fileStatus: CIFileStatus?) -> Bool { + switch fileStatus { + case .rcvInvitation: true + case .rcvAborted: true + default: false + } + } + + private func videoViewEncrypted(_ file: CIFile, _ defaultPreview: UIImage, _ duration: Int) -> some View { + return ZStack(alignment: .topTrailing) { + ZStack(alignment: .center) { + let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local) + imageView(defaultPreview) + .simultaneousGesture(TapGesture().onEnded { + decrypt(file: file) { + showFullScreenPlayer = urlDecrypted != nil + } + }) + .onChange(of: m.activeCallViewIsCollapsed) { _ in + showFullScreenPlayer = false + } + if !blurred { + if !decryptionInProgress { + playPauseIcon(canBePlayed ? "play.fill" : "play.slash") + .simultaneousGesture(TapGesture().onEnded { + decrypt(file: file) { + if urlDecrypted != nil { + videoPlaying = true + player?.play() + } + } + }) + .disabled(!canBePlayed) + } else { + videoDecryptionProgress() + } } } } @@ -98,10 +181,9 @@ struct CIVideoView: View { private func videoView(_ player: AVPlayer, _ url: URL, _ file: CIFile, _ preview: UIImage, _ duration: Int) -> some View { let w = preview.size.width <= preview.size.height ? maxWidth * 0.75 : maxWidth - DispatchQueue.main.async { videoWidth = w } return ZStack(alignment: .topTrailing) { ZStack(alignment: .center) { - let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete + let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local) VideoPlayerView(player: player, url: url, showControls: false) .frame(width: w, height: w * preview.size.height / preview.size.width) .onChange(of: m.stopPreviousRecPlay) { playingUrl in @@ -110,32 +192,34 @@ struct CIVideoView: View { videoPlaying = false } } - .fullScreenCover(isPresented: $showFullScreenPlayer) { - fullScreenPlayer(url) - } - .onTapGesture { - switch player.timeControlStatus { - case .playing: - player.pause() - videoPlaying = false - case .paused: - if canBePlayed { - showFullScreenPlayer = true + .modifier(PrivacyBlur(enabled: !videoPlaying, blurred: $blurred)) + .if(!blurred) { v in + v.simultaneousGesture(TapGesture().onEnded { + switch player.timeControlStatus { + case .playing: + player.pause() + videoPlaying = false + case .paused: + if canBePlayed { + showFullScreenPlayer = true + } + default: () } - default: () - } + }) } - if !videoPlaying { - Button { - m.stopPreviousRecPlay = url - player.play() - } label: { - playPauseIcon(canBePlayed ? "play.fill" : "play.slash") - } - .disabled(!canBePlayed) + .onChange(of: m.activeCallViewIsCollapsed) { _ in + showFullScreenPlayer = false + } + if !videoPlaying && !blurred { + playPauseIcon(canBePlayed ? "play.fill" : "play.slash") + .simultaneousGesture(TapGesture().onEnded { + m.stopPreviousRecPlay = url + player.play() + }) + .disabled(!canBePlayed) } } - loadingIndicator() + fileStatusIcon() } .onAppear { addObserver(player, url) @@ -147,70 +231,137 @@ struct CIVideoView: View { } } - private func playPauseIcon(_ image: String, _ color: Color = .white) -> some View { - Image(systemName: image) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 12, height: 12) - .foregroundColor(color) - .padding(.leading, 4) - .frame(width: 40, height: 40) - .background(Color.black.opacity(0.35)) - .clipShape(Circle()) - } - - private func durationProgress() -> some View { - HStack { - Text("\(durationText(videoPlaying ? progress : duration))") - .foregroundColor(.white) - .font(.caption) - .padding(.vertical, 3) - .padding(.horizontal, 6) - .background(Color.black.opacity(0.35)) - .cornerRadius(10) - .padding([.top, .leading], 6) - - if let file = chatItem.file, !videoPlaying { - Text("\(ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary))") - .foregroundColor(.white) - .font(.caption) - .padding(.vertical, 3) - .padding(.horizontal, 6) - .background(Color.black.opacity(0.35)) - .cornerRadius(10) - .padding(.top, 6) + private func smallVideoViewEncrypted(_ file: CIFile, _ preview: UIImage) -> some View { + return ZStack(alignment: .topLeading) { + let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local) + smallViewImageView(preview, file) + .onTapGesture { // this is shown in chat list, where onTapGesture works + decrypt(file: file) { + showFullScreenPlayer = urlDecrypted != nil + } + } + .onChange(of: m.activeCallViewIsCollapsed) { _ in + showFullScreenPlayer = false + } + if file.showStatusIconInSmallView { + // Show nothing + } else if !decryptionInProgress { + playPauseIcon(canBePlayed ? "play.fill" : "play.slash") + } else { + videoDecryptionProgress() } } } + private func smallVideoView(_ url: URL, _ file: CIFile, _ preview: UIImage) -> some View { + return ZStack(alignment: .topLeading) { + smallViewImageView(preview, file) + .onTapGesture { // this is shown in chat list, where onTapGesture works + showFullScreenPlayer = true + } + .onChange(of: m.activeCallViewIsCollapsed) { _ in + showFullScreenPlayer = false + } + + if !file.showStatusIconInSmallView { + playPauseIcon("play.fill") + } + } + } + + + private func playPauseIcon(_ image: String, _ color: Color = .white) -> some View { + Image(systemName: image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: smallView ? 12 * sizeMultiplier * 1.6 : 12, height: smallView ? 12 * sizeMultiplier * 1.6 : 12) + .foregroundColor(color) + .padding(.leading, smallView ? 0 : 4) + .frame(width: 40 * sizeMultiplier, height: 40 * sizeMultiplier) + .background(Color.black.opacity(0.35)) + .clipShape(Circle()) + } + + private func videoDecryptionProgress(_ color: Color = .white) -> some View { + ProgressView() + .progressViewStyle(.circular) + .frame(width: smallView ? 12 * sizeMultiplier : 12, height: smallView ? 12 * sizeMultiplier : 12) + .tint(color) + .frame(width: smallView ? 40 * sizeMultiplier * 0.9 : 40, height: smallView ? 40 * sizeMultiplier * 0.9 : 40) + .background(Color.black.opacity(0.35)) + .clipShape(Circle()) + } + + private var fileSizeString: String { + if let file = chatItem.file, !videoPlaying { + " " + ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary) + } else { + "" + } + } + + private func durationProgress() -> some View { + Text((durationText(videoPlaying ? progress : duration)) + fileSizeString) + .invertedForegroundStyle() + .font(.caption) + .padding(.vertical, 6) + .padding(.horizontal, 12) + } + private func imageView(_ img: UIImage) -> some View { - let w = img.size.width <= img.size.height ? maxWidth * 0.75 : .infinity - DispatchQueue.main.async { videoWidth = w } + let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth return ZStack(alignment: .topTrailing) { Image(uiImage: img) .resizable() .scaledToFit() - .frame(maxWidth: w) - loadingIndicator() + .frame(width: w) + .modifier(PrivacyBlur(blurred: $blurred)) + if !blurred || !showDownloadButton(chatItem.file?.fileStatus) { + fileStatusIcon() + } } } - @ViewBuilder private func loadingIndicator() -> some View { + private func smallViewImageView(_ img: UIImage, _ file: CIFile) -> some View { + ZStack(alignment: .center) { + Image(uiImage: img) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: maxWidth, height: maxWidth) + if file.showStatusIconInSmallView { + fileStatusIcon() + .allowsHitTesting(false) + } + } + } + + @ViewBuilder private func fileStatusIcon() -> some View { if let file = chatItem.file { switch file.fileStatus { case .sndStored: switch file.fileProtocol { case .xftp: progressView() case .smp: EmptyView() + case .local: EmptyView() } case let .sndTransfer(sndProgress, sndTotal): switch file.fileProtocol { case .xftp: progressCircle(sndProgress, sndTotal) case .smp: progressView() + case .local: EmptyView() } case .sndComplete: fileIcon("checkmark", 10, 13) case .sndCancelled: fileIcon("xmark", 10, 13) - case .sndError: fileIcon("xmark", 10, 13) + case let .sndError(sndFileError): + fileIcon("xmark", 10, 13) + .simultaneousGesture(TapGesture().onEnded { + showFileErrorAlert(sndFileError) + }) + case let .sndWarning(sndFileError): + fileIcon("exclamationmark.triangle.fill", 10, 13) + .simultaneousGesture(TapGesture().onEnded { + showFileErrorAlert(sndFileError, temporary: true) + }) case .rcvInvitation: fileIcon("arrow.down", 10, 13) case .rcvAccepted: fileIcon("ellipsis", 14, 11) case let .rcvTransfer(rcvProgress, rcvTotal): @@ -219,10 +370,20 @@ struct CIVideoView: View { } else { progressView() } + case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11) + case .rcvComplete: EmptyView() case .rcvCancelled: fileIcon("xmark", 10, 13) - case .rcvError: fileIcon("xmark", 10, 13) + case let .rcvError(rcvFileError): + fileIcon("xmark", 10, 13) + .simultaneousGesture(TapGesture().onEnded { + showFileErrorAlert(rcvFileError) + }) + case let .rcvWarning(rcvFileError): + fileIcon("exclamationmark.triangle.fill", 10, 13) + .simultaneousGesture(TapGesture().onEnded { + showFileErrorAlert(rcvFileError, temporary: true) + }) case .invalid: fileIcon("questionmark", 10, 13) - default: EmptyView() } } } @@ -230,10 +391,10 @@ struct CIVideoView: View { private func fileIcon(_ icon: String, _ size: CGFloat, _ padding: CGFloat) -> some View { Image(systemName: icon) .resizable() + .invertedForegroundStyle() .aspectRatio(contentMode: .fit) .frame(width: size, height: size) - .foregroundColor(.white) - .padding(padding) + .padding(smallView ? 0 : padding) } private func progressView() -> some View { @@ -241,26 +402,24 @@ struct CIVideoView: View { .progressViewStyle(.circular) .frame(width: 16, height: 16) .tint(.white) - .padding(11) + .padding(smallView ? 0 : 11) } private func progressCircle(_ progress: Int64, _ total: Int64) -> some View { Circle() .trim(from: 0, to: Double(progress) / Double(total)) - .stroke( - Color(uiColor: .white), - style: StrokeStyle(lineWidth: 2) - ) + .stroke(style: StrokeStyle(lineWidth: 2)) + .invertedForegroundStyle() .rotationEffect(.degrees(-90)) .frame(width: 16, height: 16) - .padding([.trailing, .top], 11) + .padding([.trailing, .top], smallView ? 0 : 11) } // TODO encrypt: where file size is checked? - private func receiveFileIfValidSize(file: CIFile, encrypted: Bool, receiveFile: @escaping (User, Int64, Bool, Bool) async -> Void) { + private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool, Bool) async -> Void) { Task { if let user = m.currentUser { - await receiveFile(user, file.fileId, encrypted, false) + await receiveFile(user, file.fileId, false, false) } } } @@ -270,7 +429,7 @@ struct CIVideoView: View { Color.black.edgesIgnoringSafeArea(.all) VideoPlayer(player: fullPlayer) .overlay(alignment: .topLeading, content: { - Button(action: { showFullScreenPlayer = false }, + Button(action: { showFullScreenPlayer = false }, // this is used in full screen player, Button works here label: { Image(systemName: "multiply") .resizable() @@ -293,7 +452,8 @@ struct CIVideoView: View { ) .onAppear { DispatchQueue.main.asyncAfter(deadline: .now()) { - m.stopPreviousRecPlay = url + // Prevent feedback loop - setting `ChatModel`s property causes `onAppear` to be called on iOS17+ + if m.stopPreviousRecPlay != url { m.stopPreviousRecPlay = url } if let player = fullPlayer { player.play() var played = false @@ -323,6 +483,24 @@ struct CIVideoView: View { } } + private func decrypt(file: CIFile, completed: (() -> Void)? = nil) { + if decryptionInProgress { return } + decryptionInProgress = true + Task { + urlDecrypted = await file.fileSource?.decryptedGetOrCreate(&ChatModel.shared.filesToDelete) + await MainActor.run { + if let decrypted = urlDecrypted { + if !smallView { + player = VideoPlayerView.getOrCreatePlayer(decrypted, false) + } + fullPlayer = AVPlayer(url: decrypted) + } + decryptionInProgress = false + completed?() + } + } + } + private func addObserver(_ player: AVPlayer, _ url: URL) { timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.01, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), queue: .main) { time in if let item = player.currentItem { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift index 2e54ba4143..715e606a74 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift @@ -11,18 +11,30 @@ import SimpleXChat struct CIVoiceView: View { @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem let recordingFile: CIFile? let duration: Int - @Binding var audioPlayer: AudioPlayer? - @Binding var playbackState: VoiceMessagePlaybackState - @Binding var playbackTime: TimeInterval? + @State var audioPlayer: AudioPlayer? = nil + @State var playbackState: VoiceMessagePlaybackState = .noPlayback + @State var playbackTime: TimeInterval? = nil + @Binding var allowMenu: Bool + var smallViewSize: CGFloat? @State private var seek: (TimeInterval) -> Void = { _ in } var body: some View { Group { - if chatItem.chatDir.sent { + if smallViewSize != nil { + HStack(spacing: 10) { + player() + playerTime() + .allowsHitTesting(false) + if .playing == playbackState || (playbackTime ?? 0) > 0 || !allowMenu { + playbackSlider() + } + } + } else if chatItem.chatDir.sent { VStack (alignment: .trailing, spacing: 6) { HStack { if .playing == playbackState || (playbackTime ?? 0) > 0 || !allowMenu { @@ -53,7 +65,13 @@ struct CIVoiceView: View { } private func player() -> some View { - VoiceMessagePlayer( + let sizeMultiplier: CGFloat = if let sz = smallViewSize { + voiceMessageSizeBasedOnSquareSize(sz) / 56 + } else { + 1 + } + return VoiceMessagePlayer( + chat: chat, chatItem: chatItem, recordingFile: recordingFile, recordingTime: TimeInterval(duration), @@ -62,7 +80,8 @@ struct CIVoiceView: View { audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, - allowMenu: $allowMenu + allowMenu: $allowMenu, + sizeMultiplier: sizeMultiplier ) } @@ -72,7 +91,7 @@ struct CIVoiceView: View { playbackState: $playbackState, playbackTime: $playbackTime ) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } private func playbackSlider() -> some View { @@ -89,10 +108,11 @@ struct CIVoiceView: View { allowMenu = true } } + .tint(theme.colors.primary) } private func metaView() -> some View { - CIMetaView(chat: chat, chatItem: chatItem) + CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary) } } @@ -117,8 +137,9 @@ struct VoiceMessagePlayerTime: View { } struct VoiceMessagePlayer: View { + @ObservedObject var chat: Chat @EnvironmentObject var chatModel: ChatModel - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem var recordingFile: CIFile? var recordingTime: TimeInterval @@ -128,23 +149,49 @@ struct VoiceMessagePlayer: View { @Binding var audioPlayer: AudioPlayer? @Binding var playbackState: VoiceMessagePlaybackState @Binding var playbackTime: TimeInterval? + @Binding var allowMenu: Bool + var sizeMultiplier: CGFloat var body: some View { ZStack { if let recordingFile = recordingFile { switch recordingFile.fileStatus { - case .sndStored: playbackButton() - case .sndTransfer: playbackButton() + case .sndStored: + if recordingFile.fileProtocol == .local { + playbackButton() + } else { + loadingIcon() + } + case .sndTransfer: loadingIcon() case .sndComplete: playbackButton() case .sndCancelled: playbackButton() - case .sndError: playbackButton() - case .rcvInvitation: downloadButton(recordingFile) + case let .sndError(sndFileError): + fileStatusIcon("multiply", 14) + .simultaneousGesture(TapGesture().onEnded { + showFileErrorAlert(sndFileError) + }) + case let .sndWarning(sndFileError): + fileStatusIcon("exclamationmark.triangle.fill", 16) + .simultaneousGesture(TapGesture().onEnded { + showFileErrorAlert(sndFileError, temporary: true) + }) + case .rcvInvitation: downloadButton(recordingFile, "play.fill") case .rcvAccepted: loadingIcon() case .rcvTransfer: loadingIcon() + case .rcvAborted: downloadButton(recordingFile, "exclamationmark.arrow.circlepath") case .rcvComplete: playbackButton() case .rcvCancelled: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) - case .rcvError: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) + case let .rcvError(rcvFileError): + fileStatusIcon("multiply", 14) + .simultaneousGesture(TapGesture().onEnded { + showFileErrorAlert(rcvFileError) + }) + case let .rcvWarning(rcvFileError): + fileStatusIcon("exclamationmark.triangle.fill", 16) + .simultaneousGesture(TapGesture().onEnded { + showFileErrorAlert(rcvFileError, temporary: true) + }) case .invalid: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) } } else { @@ -152,84 +199,126 @@ struct VoiceMessagePlayer: View { } } .onAppear { + if audioPlayer == nil { + let small = sizeMultiplier != 1 + audioPlayer = small ? VoiceItemState.smallView[VoiceItemState.id(chat, chatItem)]?.audioPlayer : VoiceItemState.chatView[VoiceItemState.id(chat, chatItem)]?.audioPlayer + playbackState = (small ? VoiceItemState.smallView[VoiceItemState.id(chat, chatItem)]?.playbackState : VoiceItemState.chatView[VoiceItemState.id(chat, chatItem)]?.playbackState) ?? .noPlayback + playbackTime = small ? VoiceItemState.smallView[VoiceItemState.id(chat, chatItem)]?.playbackTime : VoiceItemState.chatView[VoiceItemState.id(chat, chatItem)]?.playbackTime + } seek = { to in audioPlayer?.seek(to) } - audioPlayer?.onTimer = { playbackTime = $0 } + let audioPath: URL? = if let recordingSource = getLoadedFileSource(recordingFile) { + getAppFilePath(recordingSource.filePath) + } else { + nil + } + let chatId = chatModel.chatId + let userId = chatModel.currentUser?.userId + audioPlayer?.onTimer = { + playbackTime = $0 + notifyStateChange() + // Manual check here is needed because when this view is not visible, SwiftUI don't react on stopPreviousRecPlay, chatId and current user changes and audio keeps playing when it should stop + if (audioPath != nil && chatModel.stopPreviousRecPlay != audioPath) || chatModel.chatId != chatId || chatModel.currentUser?.userId != userId { + stopPlayback() + } + } audioPlayer?.onFinishPlayback = { playbackState = .noPlayback playbackTime = TimeInterval(0) + notifyStateChange() + } + // One voice message was paused, then scrolled far from it, started to play another one, drop to stopped state + if let audioPath, chatModel.stopPreviousRecPlay != audioPath { + stopPlayback() } } .onChange(of: chatModel.stopPreviousRecPlay) { it in if let recordingFileName = getLoadedFileSource(recordingFile)?.filePath, chatModel.stopPreviousRecPlay != getAppFilePath(recordingFileName) { - audioPlayer?.stop() - playbackState = .noPlayback - playbackTime = TimeInterval(0) + stopPlayback() } } .onChange(of: playbackState) { state in allowMenu = state == .paused || state == .noPlayback + // Notify activeContentPreview in ChatPreviewView that playback is finished + if state == .noPlayback, let recordingFileName = getLoadedFileSource(recordingFile)?.filePath, + chatModel.stopPreviousRecPlay == getAppFilePath(recordingFileName) { + chatModel.stopPreviousRecPlay = nil + } + } + .onChange(of: chatModel.chatId) { _ in + stopPlayback() + } + .onDisappear { + if sizeMultiplier == 1 && chatModel.chatId == nil { + stopPlayback() + } } } - @ViewBuilder private func playbackButton() -> some View { - switch playbackState { - case .noPlayback: - Button { - if let recordingSource = getLoadedFileSource(recordingFile) { - startPlayback(recordingSource) + private func playbackButton() -> some View { + let icon = switch playbackState { + case .noPlayback: "play.fill" + case .playing: "pause.fill" + case .paused: "play.fill" + } + return playPauseIcon(icon, theme.colors.primary) + .simultaneousGesture(TapGesture().onEnded { _ in + switch playbackState { + case .noPlayback: + if let recordingSource = getLoadedFileSource(recordingFile) { + startPlayback(recordingSource) + } + case .playing: + audioPlayer?.pause() + playbackState = .paused + notifyStateChange() + case .paused: + audioPlayer?.play() + playbackState = .playing + notifyStateChange() } - } label: { - playPauseIcon("play.fill") - } - case .playing: - Button { - audioPlayer?.pause() - playbackState = .paused - } label: { - playPauseIcon("pause.fill") - } - case .paused: - Button { - audioPlayer?.play() - playbackState = .playing - } label: { - playPauseIcon("play.fill") - } - } + }) } - private func playPauseIcon(_ image: String, _ color: Color = .accentColor) -> some View { + private func playPauseIcon(_ image: String, _ color: Color/* = .accentColor*/) -> some View { ZStack { Image(systemName: image) .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 20, height: 20) + .frame(width: 20 * sizeMultiplier, height: 20 * sizeMultiplier) .foregroundColor(color) .padding(.leading, image == "play.fill" ? 4 : 0) - .frame(width: 56, height: 56) - .background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear) + .frame(width: 56 * sizeMultiplier, height: 56 * sizeMultiplier) + .background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear) .clipShape(Circle()) if recordingTime > 0 { ProgressCircle(length: recordingTime, progress: $playbackTime) - .frame(width: 53, height: 53) // this + ProgressCircle lineWidth = background circle diameter + .frame(width: 53 * sizeMultiplier, height: 53 * sizeMultiplier) // this + ProgressCircle lineWidth = background circle diameter } } } - private func downloadButton(_ recordingFile: CIFile) -> some View { - Button { - Task { - if let user = chatModel.currentUser { - await receiveFile(user: user, fileId: recordingFile.fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get()) + private func downloadButton(_ recordingFile: CIFile, _ icon: String) -> some View { + playPauseIcon(icon, theme.colors.primary) + .simultaneousGesture(TapGesture().onEnded { + Task { + if let user = chatModel.currentUser { + await receiveFile(user: user, fileId: recordingFile.fileId) + } } - } - } label: { - playPauseIcon("play.fill") + }) + } + + func notifyStateChange() { + if sizeMultiplier != 1 { + VoiceItemState.smallView[VoiceItemState.id(chat, chatItem)] = VoiceItemState(audioPlayer: audioPlayer, playbackState: playbackState, playbackTime: playbackTime) + } else { + VoiceItemState.chatView[VoiceItemState.id(chat, chatItem)] = VoiceItemState(audioPlayer: audioPlayer, playbackState: playbackState, playbackTime: playbackTime) } } private struct ProgressCircle: View { + @EnvironmentObject var theme: AppTheme var length: TimeInterval @Binding var progress: TimeInterval? @@ -237,7 +326,7 @@ struct VoiceMessagePlayer: View { Circle() .trim(from: 0, to: ((progress ?? TimeInterval(0)) / length)) .stroke( - Color.accentColor, + theme.colors.primary, style: StrokeStyle(lineWidth: 3) ) .rotationEffect(.degrees(-90)) @@ -245,26 +334,103 @@ struct VoiceMessagePlayer: View { } } + private func fileStatusIcon(_ image: String, _ size: CGFloat) -> some View { + Image(systemName: image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: size * sizeMultiplier, height: size * sizeMultiplier) + .foregroundColor(Color(uiColor: .tertiaryLabel)) + .frame(width: 56 * sizeMultiplier, height: 56 * sizeMultiplier) + .background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear) + .clipShape(Circle()) + } + private func loadingIcon() -> some View { ProgressView() - .frame(width: 30, height: 30) - .frame(width: 56, height: 56) - .background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear) + .frame(width: 30 * sizeMultiplier, height: 30 * sizeMultiplier) + .frame(width: 56 * sizeMultiplier, height: 56 * sizeMultiplier) + .background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear) .clipShape(Circle()) } private func startPlayback(_ recordingSource: CryptoFile) { - chatModel.stopPreviousRecPlay = getAppFilePath(recordingSource.filePath) + let audioPath = getAppFilePath(recordingSource.filePath) + let chatId = chatModel.chatId + let userId = chatModel.currentUser?.userId + chatModel.stopPreviousRecPlay = audioPath audioPlayer = AudioPlayer( - onTimer: { playbackTime = $0 }, + onTimer: { + playbackTime = $0 + notifyStateChange() + // Manual check here is needed because when this view is not visible, SwiftUI don't react on stopPreviousRecPlay, chatId and current user changes and audio keeps playing when it should stop + if chatModel.stopPreviousRecPlay != audioPath || chatModel.chatId != chatId || chatModel.currentUser?.userId != userId { + stopPlayback() + } + }, onFinishPlayback: { playbackState = .noPlayback playbackTime = TimeInterval(0) + notifyStateChange() } ) audioPlayer?.start(fileSource: recordingSource, at: playbackTime) playbackState = .playing + notifyStateChange() } + + private func stopPlayback() { + audioPlayer?.stop() + playbackState = .noPlayback + playbackTime = TimeInterval(0) + notifyStateChange() + } +} + +@inline(__always) +func voiceMessageSizeBasedOnSquareSize(_ squareSize: CGFloat) -> CGFloat { + let squareToCircleRatio = 0.935 + return squareSize + squareSize * (1 - squareToCircleRatio) +} + +class VoiceItemState { + var audioPlayer: AudioPlayer? + var playbackState: VoiceMessagePlaybackState + var playbackTime: TimeInterval? + + init(audioPlayer: AudioPlayer? = nil, playbackState: VoiceMessagePlaybackState, playbackTime: TimeInterval? = nil) { + self.audioPlayer = audioPlayer + self.playbackState = playbackState + self.playbackTime = playbackTime + } + + @inline(__always) + static func id(_ chat: Chat, _ chatItem: ChatItem) -> String { + "\(chat.id) \(chatItem.id)" + } + + @inline(__always) + static func id(_ chatInfo: ChatInfo, _ chatItem: ChatItem) -> String { + "\(chatInfo.id) \(chatItem.id)" + } + + static func stopVoiceInSmallView(_ chatInfo: ChatInfo, _ chatItem: ChatItem) { + let id = id(chatInfo, chatItem) + if let item = smallView[id] { + item.audioPlayer?.stop() + ChatModel.shared.stopPreviousRecPlay = nil + } + } + + static func stopVoiceInChatView(_ chatInfo: ChatInfo, _ chatItem: ChatItem) { + let id = id(chatInfo, chatItem) + if let item = chatView[id] { + item.audioPlayer?.stop() + ChatModel.shared.stopPreviousRecPlay = nil + } + } + + static var smallView: [String: VoiceItemState] = [:] + static var chatView: [String: VoiceItemState] = [:] } struct CIVoiceView_Previews: PreviewProvider { @@ -289,15 +455,12 @@ struct CIVoiceView_Previews: PreviewProvider { chatItem: ChatItem.getVoiceMsgContentSample(), recordingFile: CIFile.getSample(fileName: "voice.m4a", fileSize: 65536, fileStatus: .rcvComplete), duration: 30, - audioPlayer: .constant(nil), - playbackState: .constant(.playing), - playbackTime: .constant(TimeInterval(20)), allowMenu: Binding.constant(true) ) - ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(), revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWtFile, revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, scrollToItemId: { _ in }, allowMenu: .constant(true)) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(), scrollToItemId: { _ in }, allowMenu: .constant(true)) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItemId: { _ in }, allowMenu: .constant(true)) + ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWtFile, scrollToItemId: { _ in }, allowMenu: .constant(true)) } .previewLayout(.fixed(width: 360, height: 360)) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift index 4763707421..ed2340b6c4 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift @@ -10,22 +10,21 @@ import SwiftUI import SimpleXChat struct DeletedItemView: View { - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat var chatItem: ChatItem var body: some View { HStack(alignment: .bottom, spacing: 0) { Text(chatItem.content.text) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .italic() - CIMetaView(chat: chat, chatItem: chatItem) + CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary) .padding(.horizontal, 12) } .padding(.leading, 12) .padding(.vertical, 6) - .background(chatItemFrameColor(chatItem, colorScheme)) - .cornerRadius(18) + .background(chatItemFrameColor(chatItem, theme)) .textSelection(.disabled) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift index f57e45fed0..250d9d5636 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct EmojiItemView: View { @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem var body: some View { @@ -18,7 +19,7 @@ struct EmojiItemView: View { emojiText(chatItem.content.text) .padding(.top, 8) .padding(.horizontal, 6) - CIMetaView(chat: chat, chatItem: chatItem) + CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary) .padding(.bottom, 8) .padding(.horizontal, 12) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift index af5c917dc8..f4e2a4135a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift @@ -12,21 +12,24 @@ import SwiftUI import SimpleXChat struct FramedCIVoiceView: View { + @EnvironmentObject var theme: AppTheme + @ObservedObject var chat: Chat var chatItem: ChatItem let recordingFile: CIFile? let duration: Int - + + @State var audioPlayer: AudioPlayer? = nil + @State var playbackState: VoiceMessagePlaybackState = .noPlayback + @State var playbackTime: TimeInterval? = nil + @Binding var allowMenu: Bool - - @Binding var audioPlayer: AudioPlayer? - @Binding var playbackState: VoiceMessagePlaybackState - @Binding var playbackTime: TimeInterval? - + @State private var seek: (TimeInterval) -> Void = { _ in } var body: some View { HStack { VoiceMessagePlayer( + chat: chat, chatItem: chatItem, recordingFile: recordingFile, recordingTime: TimeInterval(duration), @@ -35,14 +38,15 @@ struct FramedCIVoiceView: View { audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, - allowMenu: $allowMenu + allowMenu: $allowMenu, + sizeMultiplier: 1 ) VoiceMessagePlayerTime( recordingTime: TimeInterval(duration), playbackState: $playbackState, playbackTime: $playbackTime ) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .frame(width: 50, alignment: .leading) if .playing == playbackState || (playbackTime ?? 0) > 0 || !allowMenu { playbackSlider() @@ -88,12 +92,13 @@ struct FramedCIVoiceView_Previews: PreviewProvider { file: CIFile.getSample(fileStatus: .sndComplete) ) Group { - ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWithQuote, revealed: Binding.constant(false)) + ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWithQuote, scrollToItemId: { _ in }) } + .environment(\.revealed, false) .previewLayout(.fixed(width: 360, height: 360)) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 51dfa3cb50..b27d266d8a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -9,40 +9,44 @@ import SwiftUI import SimpleXChat -let sentColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12) -let sentColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.17) -private let sentQuoteColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.11) -private let sentQuoteColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.09) - struct FramedItemView: View { @EnvironmentObject var m: ChatModel - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat var chatItem: ChatItem - @Binding var revealed: Bool + var scrollToItemId: (ChatItem.ID) -> Void + var preview: UIImage? var maxWidth: CGFloat = .infinity - @State var scrollProxy: ScrollViewProxy? = nil @State var msgWidth: CGFloat = 0 - @State var imgWidth: CGFloat? = nil - @State var videoWidth: CGFloat? = nil - @State var metaColor = Color.secondary + var imgWidth: CGFloat? = nil + var videoWidth: CGFloat? = nil + @State private var useWhiteMetaColor: Bool = false @State var showFullScreenImage = false @Binding var allowMenu: Bool - - @Binding var audioPlayer: AudioPlayer? - @Binding var playbackState: VoiceMessagePlaybackState - @Binding var playbackTime: TimeInterval? + @State private var showFullscreenGallery: Bool = false var body: some View { let v = ZStack(alignment: .bottomTrailing) { VStack(alignment: .leading, spacing: 0) { - if let di = chatItem.meta.itemDeleted { + if chatItem.isReport { + if chatItem.meta.itemDeleted == nil { + let txt = chatItem.chatDir.sent ? + Text("Only you and moderators see it") : + Text("Only sender and moderators see it") + + framedItemHeader(icon: "flag", iconColor: .red, caption: txt.italic()) + } else { + framedItemHeader(icon: "flag", caption: Text("archived report").italic()) + } + } else if let di = chatItem.meta.itemDeleted { switch di { case let .moderated(_, byGroupMember): framedItemHeader(icon: "flag", caption: Text("moderated by \(byGroupMember.displayName)").italic()) case .blocked: framedItemHeader(icon: "hand.raised", caption: Text("blocked").italic()) - default: + case .blockedByAdmin: + framedItemHeader(icon: "hand.raised", caption: Text("blocked by admin").italic()) + case .deleted: framedItemHeader(icon: "trash", caption: Text("marked deleted").italic()) } } else if chatItem.meta.isLive { @@ -51,40 +55,53 @@ struct FramedItemView: View { if let qi = chatItem.quotedItem { ciQuoteView(qi) - .onTapGesture { - if let proxy = scrollProxy, - let ci = m.reversedChatItems.first(where: { $0.id == qi.itemId }) { + .simultaneousGesture(TapGesture().onEnded { + if let ci = ItemsModel.shared.reversedChatItems.first(where: { $0.id == qi.itemId }) { withAnimation { - proxy.scrollTo(ci.viewId, anchor: .bottom) + scrollToItemId(ci.id) } + } else if let id = qi.itemId { + scrollToItemId(id) + } else { + showQuotedItemDoesNotExistAlert() } - } + }) + } else if let itemForwarded = chatItem.meta.itemForwarded { + framedItemHeader(icon: "arrowshape.turn.up.forward", caption: Text(itemForwarded.text(chat.chatInfo.chatType)).italic(), pad: true) } - ChatItemContentView(chat: chat, chatItem: chatItem, revealed: $revealed, msgContentView: framedMsgContentView) + ChatItemContentView(chat: chat, chatItem: chatItem, msgContentView: framedMsgContentView) .padding(chatItem.content.msgContent != nil ? 0 : 4) .overlay(DetermineWidth()) } - .onPreferenceChange(MetaColorPreferenceKey.self) { metaColor = $0 } - if chatItem.content.msgContent != nil { - CIMetaView(chat: chat, chatItem: chatItem, metaColor: metaColor) - .padding(.horizontal, 12) - .padding(.bottom, 6) - .overlay(DetermineWidth()) - .accessibilityLabel("") + if let content = chatItem.content.msgContent { + CIMetaView( + chat: chat, + chatItem: chatItem, + metaColor: theme.colors.secondary, + invertedMaterial: useWhiteMetaColor + ) + .padding(.horizontal, 12) + .padding(.bottom, 6) + .overlay(DetermineWidth()) + .accessibilityLabel("") } } - .background(chatItemFrameColorMaybeImageOrVideo(chatItem, colorScheme)) - .cornerRadius(18) + .background { chatItemFrameColorMaybeImageOrVideo(chatItem, theme).modifier(ChatTailPadding()) } .onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 } - switch chatItem.meta.itemStatus { - case .sndErrorAuth: - v.onTapGesture { msgDeliveryError("Most likely this contact has deleted the connection with you.") } - case let .sndError(agentError): - v.onTapGesture { msgDeliveryError("Unexpected error: \(agentError)") } - default: v + if let (title, text) = chatItem.meta.itemStatus.statusInfo { + v.simultaneousGesture(TapGesture().onEnded { + AlertManager.shared.showAlert( + Alert( + title: Text(title), + message: Text(text) + ) + ) + }) + } else { + v } } @@ -101,40 +118,46 @@ struct FramedItemView: View { .padding(.bottom, 2) } else { switch (chatItem.content.msgContent) { - case let .image(text, image): - CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy, metaColor: metaColor) + case let .image(text, _): + CIImageView(chatItem: chatItem, scrollToItemId: scrollToItemId, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth, showFullScreenImage: $showFullscreenGallery) .overlay(DetermineWidth()) if text == "" && !chatItem.meta.isLive { Color.clear .frame(width: 0, height: 0) - .preference( - key: MetaColorPreferenceKey.self, - value: .white - ) + .onAppear { + useWhiteMetaColor = true + } + .onDisappear { + useWhiteMetaColor = false + } } else { ciMsgContentView(chatItem) } - case let .video(text, image, duration): - CIVideoView(chatItem: chatItem, image: image, duration: duration, maxWidth: maxWidth, videoWidth: $videoWidth, scrollProxy: scrollProxy) + case let .video(text, _, duration): + CIVideoView(chatItem: chatItem, preview: preview, duration: duration, maxWidth: maxWidth, videoWidth: videoWidth, showFullscreenPlayer: $showFullscreenGallery) .overlay(DetermineWidth()) if text == "" && !chatItem.meta.isLive { Color.clear .frame(width: 0, height: 0) - .preference( - key: MetaColorPreferenceKey.self, - value: .white - ) + .onAppear { + useWhiteMetaColor = true + } + .onDisappear { + useWhiteMetaColor = false + } } else { ciMsgContentView(chatItem) } case let .voice(text, duration): - FramedCIVoiceView(chatItem: chatItem, recordingFile: chatItem.file, duration: duration, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime) + FramedCIVoiceView(chat: chat, chatItem: chatItem, recordingFile: chatItem.file, duration: duration, allowMenu: $allowMenu) .overlay(DetermineWidth()) if text != "" { ciMsgContentView(chatItem) } case let .file(text): ciFileView(chatItem, text) + case let .report(text, reason): + ciMsgContentView(chatItem, txtPrefix: reason.attrString) case let .link(_, preview): CILinkView(linkPreview: preview) ciMsgContentView(chatItem) @@ -149,33 +172,27 @@ struct FramedItemView: View { } } } - - private func msgDeliveryError(_ err: LocalizedStringKey) { - AlertManager.shared.showAlertMsg( - title: "Message delivery error", - message: err - ) - } - @ViewBuilder func framedItemHeader(icon: String? = nil, caption: Text) -> some View { + @ViewBuilder func framedItemHeader(icon: String? = nil, iconColor: Color? = nil, caption: Text, pad: Bool = false) -> some View { let v = HStack(spacing: 6) { if let icon = icon { Image(systemName: icon) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 14, height: 14) + .foregroundColor(iconColor ?? theme.colors.secondary) } caption .font(.caption) .lineLimit(1) } - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .padding(.horizontal, 12) .padding(.top, 6) - .padding(.bottom, chatItem.quotedItem == nil ? 6 : 0) // TODO think how to regroup + .padding(.bottom, pad || (chatItem.quotedItem == nil && chatItem.meta.itemForwarded == nil) ? 6 : 0) .overlay(DetermineWidth()) .frame(minWidth: msgWidth, alignment: .leading) - .background(chatItemFrameContextColor(chatItem, colorScheme)) + .background(chatItemFrameContextColor(chatItem, theme)) if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth { v.frame(maxWidth: mediaWidth, alignment: .leading) } else { @@ -184,11 +201,11 @@ struct FramedItemView: View { } @ViewBuilder private func ciQuoteView(_ qi: CIQuote) -> some View { + let backgroundColor = chatItemFrameContextColor(chatItem, theme) let v = ZStack(alignment: .topTrailing) { switch (qi.content) { case let .image(_, image): - if let data = Data(base64Encoded: dropImagePrefix(image)), - let uiImage = UIImage(data: data) { + if let uiImage = imageFromBase64(image) { ciQuotedMsgView(qi) .padding(.trailing, 70).frame(minWidth: msgWidth, alignment: .leading) Image(uiImage: uiImage) @@ -200,8 +217,7 @@ struct FramedItemView: View { ciQuotedMsgView(qi) } case let .video(_, image, _): - if let data = Data(base64Encoded: dropImagePrefix(image)), - let uiImage = UIImage(data: data) { + if let uiImage = imageFromBase64(image) { ciQuotedMsgView(qi) .padding(.trailing, 70).frame(minWidth: msgWidth, alignment: .leading) Image(uiImage: uiImage) @@ -227,8 +243,8 @@ struct FramedItemView: View { // if enable this always, size of the framed voice message item will be incorrect after end of playback .overlay { if case .voice = chatItem.content.msgContent {} else { DetermineWidth() } } .frame(minWidth: msgWidth, alignment: .leading) - .background(chatItemFrameContextColor(chatItem, colorScheme)) - + .background(backgroundColor) + .environment(\.containerBackground, UIColor(backgroundColor)) if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth { v.frame(maxWidth: mediaWidth, alignment: .leading) } else { @@ -240,21 +256,25 @@ struct FramedItemView: View { Group { if let sender = qi.getSender(membership()) { VStack(alignment: .leading, spacing: 2) { - Text(sender).font(.caption).foregroundColor(.secondary) + Text(sender) + .font(.caption) + .foregroundColor(qi.chatDir == .groupSnd ? .accentColor : theme.colors.secondary) + .lineLimit(1) ciQuotedMsgTextView(qi, lines: 2) } } else { ciQuotedMsgTextView(qi, lines: 3) } } + .fixedSize(horizontal: false, vertical: true) .padding(.top, 6) .padding(.horizontal, 12) } - + + @inline(__always) private func ciQuotedMsgTextView(_ qi: CIQuote, lines: Int) -> some View { - MsgContentView(chat: chat, text: qi.text, formattedText: qi.formattedText) + MsgContentView(chat: chat, text: qi.text, formattedText: qi.formattedText, textStyle: .subheadline) .lineLimit(lines) - .font(.subheadline) .padding(.bottom, 6) } @@ -275,22 +295,27 @@ struct FramedItemView: View { } } - @ViewBuilder private func ciMsgContentView(_ ci: ChatItem) -> some View { + @ViewBuilder private func ciMsgContentView(_ ci: ChatItem, txtPrefix: NSAttributedString? = 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 let v = MsgContentView( chat: chat, text: text, - formattedText: text == "" ? [] : ci.formattedText, + formattedText: ft, + textStyle: .body, meta: ci.meta, - rightToLeft: rtl + mentions: ci.mentions, + userMemberId: chat.chatInfo.groupInfo?.membership.memberId, + rightToLeft: rtl, + prefix: txtPrefix ) + .environment(\.containerBackground, UIColor(chatItemFrameColor(ci, theme))) .multilineTextAlignment(rtl ? .trailing : .leading) .padding(.vertical, 6) .padding(.horizontal, 12) .overlay(DetermineWidth()) .frame(minWidth: 0, alignment: .leading) - .textSelection(.enabled) if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth { v.frame(maxWidth: mediaWidth, alignment: .leading) @@ -298,7 +323,7 @@ struct FramedItemView: View { v } } - + @ViewBuilder private func ciFileView(_ ci: ChatItem, _ text: String) -> some View { CIFileView(file: chatItem.file, edited: chatItem.meta.itemEdited) .overlay(DetermineWidth()) @@ -316,6 +341,13 @@ struct FramedItemView: View { return videoWidth } } + + private func showQuotedItemDoesNotExistAlert() { + AlertManager.shared.showAlertMsg( + title: "No message", + message: "This message was deleted or not received yet." + ) + } } func isRightToLeft(_ s: String) -> Bool { @@ -325,51 +357,44 @@ func isRightToLeft(_ s: String) -> Bool { return false } -private struct MetaColorPreferenceKey: PreferenceKey { - static var defaultValue = Color.secondary - static func reduce(value: inout Color, nextValue: () -> Color) { - value = nextValue() - } -} - func onlyImageOrVideo(_ ci: ChatItem) -> Bool { if case let .image(text, _) = ci.content.msgContent { - return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && text == "" + return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && ci.meta.itemForwarded == nil && text == "" } else if case let .video(text, _, _) = ci.content.msgContent { - return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && text == "" + return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && ci.meta.itemForwarded == nil && text == "" } return false } -func chatItemFrameColorMaybeImageOrVideo(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color { +func chatItemFrameColorMaybeImageOrVideo(_ ci: ChatItem, _ theme: AppTheme) -> Color { onlyImageOrVideo(ci) ? Color.clear - : chatItemFrameColor(ci, colorScheme) + : chatItemFrameColor(ci, theme) } -func chatItemFrameColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color { +func chatItemFrameColor(_ ci: ChatItem, _ theme: AppTheme) -> Color { ci.chatDir.sent - ? (colorScheme == .light ? sentColorLight : sentColorDark) - : Color(uiColor: .tertiarySystemGroupedBackground) + ? theme.appColors.sentMessage + : theme.appColors.receivedMessage } -func chatItemFrameContextColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color { +func chatItemFrameContextColor(_ ci: ChatItem, _ theme: AppTheme) -> Color { ci.chatDir.sent - ? (colorScheme == .light ? sentQuoteColorLight : sentQuoteColorDark) - : Color(uiColor: .quaternarySystemFill) + ? theme.appColors.sentQuote + : theme.appColors.receivedQuote } struct FramedItemView_Previews: PreviewProvider { static var previews: some View { Group{ - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line "), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat"), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line "), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat"), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) } .previewLayout(.fixed(width: 360, height: 200)) } @@ -378,17 +403,18 @@ struct FramedItemView_Previews: PreviewProvider { struct FramedItemView_Edited_Previews: PreviewProvider { static var previews: some View { Group { - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, 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"), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, 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"), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, 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"), itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, 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"), itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) } + .environment(\.revealed, false) .previewLayout(.fixed(width: 360, height: 200)) } } @@ -396,17 +422,18 @@ struct FramedItemView_Edited_Previews: PreviewProvider { struct FramedItemView_Deleted_Previews: PreviewProvider { static var previews: some View { Group { - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, 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"), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, 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"), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, 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"), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, 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"), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) } + .environment(\.revealed, false) .previewLayout(.fixed(width: 360, height: 200)) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift index 0e721acdcb..10e5efa298 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift @@ -14,11 +14,11 @@ import AVKit struct FullScreenMediaView: View { @EnvironmentObject var m: ChatModel @State var chatItem: ChatItem + var scrollToItemId: ((ChatItem.ID) -> Void)? @State var image: UIImage? @State var player: AVPlayer? = nil @State var url: URL? = nil @Binding var showView: Bool - @State var scrollProxy: ScrollViewProxy? @State private var showNext = false @State private var nextImage: UIImage? @State private var nextPlayer: AVPlayer? @@ -71,9 +71,7 @@ struct FullScreenMediaView: View { let w = abs(t.width) if t.height > 60 && t.height > w * 2 { showView = false - if let proxy = scrollProxy { - proxy.scrollTo(chatItem.viewId) - } + scrollToItemId?(chatItem.id) } else if w > 60 && w > abs(t.height) * 2 && !scrolling { let previous = t.width > 0 scrolling = true @@ -128,7 +126,7 @@ struct FullScreenMediaView: View { .scaledToFit() } } - .onTapGesture { showView = false } + .onTapGesture { showView = false } // this is used in full screen view, onTapGesture works } private func videoView( _ player: AVPlayer, _ url: URL) -> some View { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift index 1aa0093c9a..47a30f6cf3 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct IntegrityErrorItemView: View { @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme var msgError: MsgErrorType var chatItem: ChatItem @@ -30,8 +31,8 @@ struct IntegrityErrorItemView: View { case .msgBadHash: AlertManager.shared.showAlert(Alert( title: Text("Bad message hash"), - message: Text("The hash of the previous message is different.") + Text("\n") + - Text(decryptErrorReason) + Text("\n") + + message: Text("The hash of the previous message is different.") + textNewLine + + Text(decryptErrorReason) + textNewLine + Text("Please report it to the developers.") )) case .msgBadId: msgBadIdAlert() @@ -46,7 +47,7 @@ struct IntegrityErrorItemView: View { message: Text(""" The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised. - """) + Text("\n") + + """) + textNewLine + Text("Please report it to the developers.") )) } @@ -54,6 +55,7 @@ struct IntegrityErrorItemView: View { struct CIMsgError: View { @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem var onTap: () -> Void @@ -62,15 +64,14 @@ struct CIMsgError: View { Text(chatItem.content.text) .foregroundColor(.red) .italic() - CIMetaView(chat: chat, chatItem: chatItem) + CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary) .padding(.horizontal, 12) } .padding(.leading, 12) .padding(.vertical, 6) - .background(Color(uiColor: .tertiarySystemGroupedBackground)) - .cornerRadius(18) + .background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) } .textSelection(.disabled) - .onTapGesture(perform: onTap) + .simultaneousGesture(TapGesture().onEnded(onTap)) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift index c6af95e6f6..87a9b2ce61 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift @@ -11,19 +11,18 @@ import SimpleXChat struct MarkedDeletedItemView: View { @EnvironmentObject var m: ChatModel - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme + @Environment(\.revealed) var revealed: Bool @ObservedObject var chat: Chat var chatItem: ChatItem - @Binding var revealed: Bool var body: some View { - (Text(mergedMarkedDeletedText).italic() + Text(" ") + chatItem.timestampText) + (Text(mergedMarkedDeletedText).italic() + textSpace + chatItem.timestampText) .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .padding(.horizontal, 12) .padding(.vertical, 6) - .background(chatItemFrameColor(chatItem, colorScheme)) - .cornerRadius(18) + .background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) } .textSelection(.disabled) } @@ -33,10 +32,11 @@ struct MarkedDeletedItemView: View { var i = m.getChatItemIndex(chatItem) { var moderated = 0 var blocked = 0 + var blockedByAdmin = 0 var deleted = 0 var moderatedBy: Set = [] - while i < m.reversedChatItems.count, - let ci = .some(m.reversedChatItems[i]), + while i < ItemsModel.shared.reversedChatItems.count, + let ci = .some(ItemsModel.shared.reversedChatItems[i]), ci.mergeCategory == ciCategory, let itemDeleted = ci.meta.itemDeleted { switch itemDeleted { @@ -44,16 +44,19 @@ struct MarkedDeletedItemView: View { moderated += 1 moderatedBy.insert(byGroupMember.displayName) case .blocked: blocked += 1 + case .blockedByAdmin: blockedByAdmin += 1 case .deleted: deleted += 1 } i += 1 } - let total = moderated + blocked + deleted + let total = moderated + blocked + blockedByAdmin + deleted return total <= 1 ? markedDeletedText : total == moderated ? "\(total) messages moderated by \(moderatedBy.joined(separator: ", "))" - : total == blocked + : total == blockedByAdmin + ? "\(total) messages blocked by admin" + : total == blocked + blockedByAdmin ? "\(total) messages blocked" : "\(total) messages marked deleted" } else { @@ -61,11 +64,18 @@ struct MarkedDeletedItemView: View { } } + // same texts are in markedDeletedText in ChatPreviewView, but it returns String; + // can be refactored into a single function if functions calling these are changed to return same type var markedDeletedText: LocalizedStringKey { - switch chatItem.meta.itemDeleted { - case let .moderated(_, byGroupMember): "moderated by \(byGroupMember.displayName)" - case .blocked: "blocked" - default: "marked deleted" + if chatItem.meta.itemDeleted != nil, chatItem.isReport { + "archived report" + } else { + switch chatItem.meta.itemDeleted { + case let .moderated(_, byGroupMember): "moderated by \(byGroupMember.displayName)" + case .blocked: "blocked" + case .blockedByAdmin: "blocked by admin" + case .deleted, nil: "marked deleted" + } } } } @@ -73,7 +83,10 @@ struct MarkedDeletedItemView: View { struct MarkedDeletedItemView_Previews: PreviewProvider { static var previews: some View { Group { - MarkedDeletedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true)) + MarkedDeletedItemView( + chat: Chat.sampleData, + chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)) + ).environment(\.revealed, true) } .previewLayout(.fixed(width: 360, height: 200)) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index d0d2bdf3dd..e04584dfff 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -9,47 +9,76 @@ import SwiftUI import SimpleXChat -private let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1) +let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1) -private let noTyping = Text(" ") - -private let typingIndicators: [Text] = [ - (typing(.black) + typing() + typing()), - (typing(.bold) + typing(.black) + typing()), - (typing() + typing(.bold) + typing(.black)), - (typing() + typing() + typing(.bold)) -] - -private func typing(_ w: Font.Weight = .light) -> Text { - Text(".").fontWeight(w) +private func typing(_ theme: AppTheme, _ descr: UIFontDescriptor, _ ws: [UIFont.Weight]) -> NSMutableAttributedString { + let res = NSMutableAttributedString() + for w in ws { + res.append(NSAttributedString(string: ".", attributes: [ + .font: UIFont.monospacedSystemFont(ofSize: descr.pointSize, weight: w), + .kern: -2 as NSNumber, + .foregroundColor: UIColor(theme.colors.secondary) + ])) + } + return res } struct MsgContentView: View { @ObservedObject var chat: Chat + @Environment(\.showTimestamp) var showTimestamp: Bool + @Environment(\.containerBackground) var containerBackground: UIColor + @EnvironmentObject var theme: AppTheme var text: String var formattedText: [FormattedText]? = nil + var textStyle: UIFont.TextStyle var sender: String? = nil var meta: CIMeta? = nil + var mentions: [String: CIMention]? = nil + var userMemberId: String? = nil var rightToLeft = false + var prefix: NSAttributedString? = nil + @State private var showSecrets: Set = [] @State private var typingIdx = 0 @State private var timer: Timer? + @State private var typingIndicators: [NSAttributedString] = [] + @State private var noTyping = NSAttributedString(string: " ") + @State private var phase: CGFloat = 0 + + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false var body: some View { + let v = msgContentView() if meta?.isLive == true { - msgContentView() - .onAppear { switchTyping() } + v.onAppear { + let descr = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body) + noTyping = NSAttributedString(string: " ", attributes: [ + .font: UIFont.monospacedSystemFont(ofSize: descr.pointSize, weight: .regular), + .kern: -2 as NSNumber, + .foregroundColor: UIColor(theme.colors.secondary) + ]) + switchTyping() + } .onDisappear(perform: stopTyping) .onChange(of: meta?.isLive, perform: switchTyping) .onChange(of: meta?.recent, perform: switchTyping) } else { - msgContentView() + v } } private func switchTyping(_: Bool? = nil) { if let meta = meta, meta.isLive && meta.recent { + if typingIndicators.isEmpty { + let descr = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body) + typingIndicators = [ + typing(theme, descr, [.black, .light, .light]), + typing(theme, descr, [.bold, .black, .light]), + typing(theme, descr, [.light, .bold, .black]), + typing(theme, descr, [.light, .light, .bold]) + ] + } timer = timer ?? Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { _ in - typingIdx = (typingIdx + 1) % typingIndicators.count + typingIdx = typingIdx + 1 } } else { stopTyping() @@ -59,92 +88,279 @@ struct MsgContentView: View { private func stopTyping() { timer?.invalidate() timer = nil + typingIdx = 0 } - private func msgContentView() -> Text { - var v = messageText(text, formattedText, sender) + @inline(__always) + private func msgContentView() -> some View { + let r = messageText(text, formattedText, textStyle: textStyle, sender: sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, backgroundColor: containerBackground, prefix: prefix) + let s = r.string + let t: Text if let mt = meta { if mt.isLive { - v = v + typingIndicator(mt.recent) + s.append(typingIndicator(mt.recent)) } - v = v + reserveSpaceForMeta(mt) + t = Text(AttributedString(s)) + reserveSpaceForMeta(mt) + } else { + t = Text(AttributedString(s)) } - return v + return msgTextResultView(r, t, showSecrets: $showSecrets) } - private func typingIndicator(_ recent: Bool) -> Text { - return (recent ? typingIndicators[typingIdx] : noTyping) - .font(.body.monospaced()) - .kerning(-2) - .foregroundColor(.secondary) + @inline(__always) + private func typingIndicator(_ recent: Bool) -> NSAttributedString { + recent && !typingIndicators.isEmpty + ? typingIndicators[typingIdx % 4] + : noTyping } + @inline(__always) private func reserveSpaceForMeta(_ mt: CIMeta) -> Text { - (rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true) + (rightToLeft ? textNewLine : Text(verbatim: " ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) } } -func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, icon: String? = nil, preview: Bool = false) -> Text { - let s = text - var res: Text - if let ft = formattedText, ft.count > 0 && ft.count <= 200 { - res = formatText(ft[0], preview) - var i = 1 - while i < ft.count { - res = res + formatText(ft[i], preview) - i = i + 1 - } - } else { - res = Text(s) - } - - if let i = icon { - res = Text(Image(systemName: i)).foregroundColor(Color(uiColor: .tertiaryLabel)) + Text(" ") + res - } - - if let s = sender { - let t = Text(s) - return (preview ? t : t.fontWeight(.medium)) + Text(": ") + res - } else { - return res - } +func msgTextResultView(_ r: MsgTextResult, _ t: Text, showSecrets: Binding>? = nil) -> some View { + t.if(r.hasSecrets, transform: hiddenSecretsView) + .if(r.handleTaps) { $0.overlay(handleTextTaps(r.string, showSecrets: showSecrets)) } } -private func formatText(_ ft: FormattedText, _ preview: Bool) -> Text { - let t = ft.text - if let f = ft.format { - switch (f) { - case .bold: return Text(t).bold() - case .italic: return Text(t).italic() - case .strikeThrough: return Text(t).strikethrough() - case .snippet: return Text(t).font(.body.monospaced()) - case .secret: return Text(t).foregroundColor(.clear).underline(color: .primary) - case let .colored(color): return Text(t).foregroundColor(color.uiColor) - case .uri: return linkText(t, t, preview, prefix: "") - case let .simplexLink(linkType, simplexUri, smpHosts): - switch privacySimplexLinkModeDefault.get() { - case .description: return linkText(simplexLinkText(linkType, smpHosts), simplexUri, preview, prefix: "") - case .full: return linkText(t, simplexUri, preview, prefix: "") - case .browser: return linkText(t, simplexUri, preview, prefix: "") +@inline(__always) +private func handleTextTaps(_ s: NSAttributedString, showSecrets: Binding>? = nil) -> some View { + return GeometryReader { g in + Rectangle() + .fill(Color.clear) + .contentShape(Rectangle()) + .simultaneousGesture(DragGesture(minimumDistance: 0).onEnded { event in + let t = event.translation + if t.width * t.width + t.height * t.height > 100 { return } + let framesetter = CTFramesetterCreateWithAttributedString(s as CFAttributedString) + let path = CGPath(rect: CGRect(origin: .zero, size: g.size), transform: nil) + let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, s.length), path, nil) + let point = CGPoint(x: event.location.x, y: g.size.height - event.location.y) // Flip y for UIKit + var index: CFIndex? + if let lines = CTFrameGetLines(frame) as? [CTLine] { + var origins = [CGPoint](repeating: .zero, count: lines.count) + CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins) + for i in 0 ..< lines.count { + let bounds = CTLineGetBoundsWithOptions(lines[i], .useOpticalBounds) + if bounds.offsetBy(dx: origins[i].x, dy: origins[i].y).contains(point) { + index = CTLineGetStringIndexForPosition(lines[i], point) + break + } + } + } + if let index, let (url, browser) = attributedStringLink(s, for: index) { + if browser { + openBrowserAlert(uri: url) + } else { + UIApplication.shared.open(url) + } + } + }) + } + + func attributedStringLink(_ s: NSAttributedString, for index: CFIndex) -> (URL, Bool)? { + var linkURL: URL? + var browser: Bool = false + s.enumerateAttributes(in: NSRange(location: 0, length: s.length)) { attrs, range, stop in + if index >= range.location && index < range.location + range.length { + if let url = attrs[linkAttrKey] as? NSURL { + linkURL = url.absoluteURL + browser = attrs[webLinkAttrKey] != nil + } else if let showSecrets, let i = attrs[secretAttrKey] as? Int { + if showSecrets.wrappedValue.contains(i) { + showSecrets.wrappedValue.remove(i) + } else { + showSecrets.wrappedValue.insert(i) + } + } + stop.pointee = true } - case .email: return linkText(t, t, preview, prefix: "mailto:") - case .phone: return linkText(t, t.replacingOccurrences(of: " ", with: ""), preview, prefix: "tel:") } - } else { - return Text(t) + return if let linkURL { (linkURL, browser) } else { nil } } } -private func linkText(_ s: String, _ link: String, _ preview: Bool, prefix: String, color: Color = Color(uiColor: uiLinkColor), uiColor: UIColor = uiLinkColor) -> Text { - preview - ? Text(s).foregroundColor(color).underline(color: color) - : Text(AttributedString(s, attributes: AttributeContainer([ - .link: NSURL(string: prefix + link) as Any, - .foregroundColor: uiColor as Any - ]))).underline() +func hiddenSecretsView(_ v: V) -> some View { + v.overlay( + GeometryReader { g in + let size = (g.size.width + g.size.height) / 1.4142 + Image("vertical_logo") + .resizable(resizingMode: .tile) + .frame(width: size, height: size) + .rotationEffect(.degrees(45), anchor: .center) + .position(x: g.size.width / 2, y: g.size.height / 2) + .clipped() + .saturation(0.65) + .opacity(0.35) + } + .mask(v) + ) } -private func simplexLinkText(_ linkType: SimplexLinkType, _ smpHosts: [String]) -> String { +private let linkAttrKey = NSAttributedString.Key("chat.simplex.app.link") + +private let webLinkAttrKey = NSAttributedString.Key("chat.simplex.app.webLink") + +private let secretAttrKey = NSAttributedString.Key("chat.simplex.app.secret") + +typealias MsgTextResult = (string: NSMutableAttributedString, hasSecrets: Bool, handleTaps: Bool) + +func messageText( + _ text: String, + _ formattedText: [FormattedText]?, + textStyle: UIFont.TextStyle = .body, + sender: String?, + preview: Bool = false, + mentions: [String: CIMention]?, + userMemberId: String?, + showSecrets: Set?, + backgroundColor: UIColor, + prefix: NSAttributedString? = nil +) -> MsgTextResult { + let res = NSMutableAttributedString() + let descr = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle) + let font = UIFont.preferredFont(forTextStyle: textStyle) + let plain: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: UIColor.label + ] + let secretColor = backgroundColor.withAlphaComponent(1) + var link: [NSAttributedString.Key: Any]? + var hasSecrets = false + var handleTaps = false + + if let sender { + if preview { + res.append(NSAttributedString(string: sender + ": ", attributes: plain)) + } else { + var attrs = plain + attrs[.font] = UIFont(descriptor: descr.addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: UIFont.Weight.medium]]), size: descr.pointSize) + res.append(NSAttributedString(string: sender, attributes: attrs)) + res.append(NSAttributedString(string: ": ", attributes: plain)) + } + } + + if let prefix { + res.append(prefix) + } + + if let fts = formattedText, fts.count > 0 { + var bold: UIFont? + var italic: UIFont? + var snippet: UIFont? + var mention: UIFont? + var secretIdx: Int = 0 + for ft in fts { + var t = ft.text + var attrs = plain + switch (ft.format) { + case .bold: + bold = bold ?? UIFont(descriptor: descr.addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: UIFont.Weight.bold]]), size: descr.pointSize) + attrs[.font] = bold + case .italic: + italic = italic ?? UIFont(descriptor: descr.withSymbolicTraits(.traitItalic) ?? descr, size: descr.pointSize) + attrs[.font] = italic + case .strikeThrough: + attrs[.strikethroughStyle] = NSUnderlineStyle.single.rawValue + case .snippet: + snippet = snippet ?? UIFont.monospacedSystemFont(ofSize: descr.pointSize, weight: .regular) + attrs[.font] = snippet + case .secret: + if let showSecrets { + if !showSecrets.contains(secretIdx) { + attrs[.foregroundColor] = UIColor.clear + attrs[.backgroundColor] = secretColor + } + attrs[secretAttrKey] = secretIdx + secretIdx += 1 + handleTaps = true + } else { + attrs[.foregroundColor] = UIColor.clear + attrs[.backgroundColor] = secretColor + } + hasSecrets = true + case let .colored(color): + if let c = color.uiColor { + attrs[.foregroundColor] = UIColor(c) + } + case .uri: + attrs = linkAttrs() + if !preview { + let s = t.lowercased() + let link = s.hasPrefix("http://") || s.hasPrefix("https://") + ? t + : "https://" + t + attrs[linkAttrKey] = NSURL(string: link) + attrs[webLinkAttrKey] = true + handleTaps = true + } + case let .simplexLink(linkType, simplexUri, smpHosts): + attrs = linkAttrs() + if !preview { + attrs[linkAttrKey] = NSURL(string: simplexUri) + handleTaps = true + } + if case .description = privacySimplexLinkModeDefault.get() { + t = simplexLinkText(linkType, smpHosts) + } + case let .mention(memberName): + if let m = mentions?[memberName] { + mention = mention ?? UIFont(descriptor: descr.addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold]]), size: descr.pointSize) + attrs[.font] = mention + if let ref = m.memberRef { + let name: String = if let alias = ref.localAlias, alias != "" { + "\(alias) (\(ref.displayName))" + } else { + ref.displayName + } + if m.memberId == userMemberId { + attrs[.foregroundColor] = UIColor.tintColor + } + t = mentionText(name) + } else { + t = mentionText(memberName) + } + } + case .email: + attrs = linkAttrs() + if !preview { + attrs[linkAttrKey] = NSURL(string: "mailto:" + ft.text) + handleTaps = true + } + case .phone: + attrs = linkAttrs() + if !preview { + attrs[linkAttrKey] = NSURL(string: "tel:" + t.replacingOccurrences(of: " ", with: "")) + handleTaps = true + } + case .none: () + } + res.append(NSAttributedString(string: t, attributes: attrs)) + } + } else { + res.append(NSMutableAttributedString(string: text, attributes: plain)) + } + + return (string: res, hasSecrets: hasSecrets, handleTaps: handleTaps) + + func linkAttrs() -> [NSAttributedString.Key: Any] { + link = link ?? [ + .font: font, + .foregroundColor: uiLinkColor, + .underlineStyle: NSUnderlineStyle.single.rawValue + ] + return link! + } +} + +@inline(__always) +private func mentionText(_ name: String) -> String { + name.contains(" @") ? "@'\(name)'" : "@\(name)" +} + +func simplexLinkText(_ linkType: SimplexLinkType, _ smpHosts: [String]) -> String { linkType.description + " " + "(via \(smpHosts.first ?? "?"))" } @@ -155,6 +371,7 @@ struct MsgContentView_Previews: PreviewProvider { chat: Chat.sampleData, text: chatItem.text, formattedText: chatItem.formattedText, + textStyle: .body, sender: chatItem.memberDisplayName, meta: chatItem.meta ) diff --git a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift new file mode 100644 index 0000000000..dfc620c402 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift @@ -0,0 +1,132 @@ +// +// ChatItemForwardingView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 12.04.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ChatItemForwardingView: View { + @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @Environment(\.dismiss) var dismiss + + var chatItems: [ChatItem] + var fromChatInfo: ChatInfo + @Binding var composeState: ComposeState + + @State private var searchText: String = "" + @State private var alert: SomeAlert? + private let chatsToForwardTo = filterChatsToForwardTo(chats: ChatModel.shared.chats) + + var body: some View { + NavigationView { + forwardListView() + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { + dismiss() + } + } + ToolbarItem(placement: .principal) { + Text("Forward") + .bold() + } + } + } + .modifier(ThemedBackground()) + .alert(item: $alert) { $0.alert } + } + + private func forwardListView() -> some View { + VStack(alignment: .leading) { + if !chatsToForwardTo.isEmpty { + List { + let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase + let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { foundChat($0, s) } + ForEach(chats) { chat in + forwardListChatView(chat) + .disabled(chatModel.deletedChats.contains(chat.chatInfo.id)) + } + } + .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always)) + .modifier(ThemedBackground(grouped: true)) + } else { + ZStack { + emptyList() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .modifier(ThemedBackground()) + } + } + } + + private func emptyList() -> some View { + Text("No filtered chats") + .foregroundColor(theme.colors.secondary) + .frame(maxWidth: .infinity) + } + + @ViewBuilder private func forwardListChatView(_ chat: Chat) -> some View { + let prohibited = chatItems.map { ci in + chat.prohibitedByPref( + hasSimplexLink: hasSimplexLink(ci.content.msgContent?.text), + isMediaOrFileAttachment: ci.content.msgContent?.isMediaOrFileAttachment ?? false, + isVoice: ci.content.msgContent?.isVoice ?? false + ) + }.contains(true) + + Button { + if prohibited { + alert = SomeAlert( + alert: mkAlert( + title: "Cannot forward message", + message: "Selected chat preferences prohibit this message." + ), + id: "forward prohibited by preferences" + ) + } else { + dismiss() + if chat.id == fromChatInfo.id { + composeState = ComposeState( + message: composeState.message, + preview: composeState.linkPreview != nil ? composeState.preview : .noPreview, + contextItem: .forwardingItems(chatItems: chatItems, fromChatInfo: fromChatInfo) + ) + } else { + composeState = ComposeState.init(forwardingItems: chatItems, fromChatInfo: fromChatInfo) + ItemsModel.shared.loadOpenChat(chat.id) + } + } + } label: { + HStack { + ChatInfoImage(chat: chat, size: 30) + .padding(.trailing, 2) + Text(chat.chatInfo.chatViewName) + .foregroundColor(prohibited ? theme.colors.secondary : theme.colors.onBackground) + .lineLimit(1) + if chat.chatInfo.incognito { + Spacer() + Image(systemName: "theatermasks") + .resizable() + .scaledToFit() + .frame(width: 22, height: 22) + .foregroundColor(theme.colors.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } +} + +#Preview { + ChatItemForwardingView( + chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")], + fromChatInfo: .direct(contact: Contact.sampleData), + composeState: Binding.constant(ComposeState(message: "hello")) + ).environmentObject(CurrentColors.toAppTheme()) +} + diff --git a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift index 83c4cdcda6..cd75d1b0cd 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -11,16 +11,21 @@ import SimpleXChat struct ChatItemInfoView: View { @EnvironmentObject var chatModel: ChatModel - @Environment(\.colorScheme) var colorScheme + @Environment(\.dismiss) var dismiss + @EnvironmentObject var theme: AppTheme var ci: ChatItem + var userMemberId: String? @Binding var chatItemInfo: ChatItemInfo? @State private var selection: CIInfoTab = .history @State private var alert: CIInfoViewAlert? = nil + @State private var messageStatusLimited: Bool = true + @State private var fileStatusLimited: Bool = true @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false enum CIInfoTab { case history case quote + case forwarded case delivery } @@ -53,7 +58,9 @@ struct ChatItemInfoView: View { } private var title: String { - ci.chatDir.sent + ci.localNote + ? NSLocalizedString("Saved message", comment: "message info title") + : ci.chatDir.sent ? NSLocalizedString("Sent message", comment: "message info title") : NSLocalizedString("Received message", comment: "message info title") } @@ -66,9 +73,20 @@ struct ChatItemInfoView: View { if ci.quotedItem != nil { numTabs += 1 } + if chatItemInfo?.forwardedFromChatItem != nil { + numTabs += 1 + } return numTabs } + private var local: Bool { + switch ci.chatDir { + case .localSnd: true + case .localRcv: true + default: false + } + } + @ViewBuilder private func itemInfoView() -> some View { if numTabs > 1 { TabView(selection: $selection) { @@ -84,12 +102,22 @@ struct ChatItemInfoView: View { Label("History", systemImage: "clock") } .tag(CIInfoTab.history) + .modifier(ThemedBackground()) if let qi = ci.quotedItem { quoteTab(qi) .tabItem { Label("In reply to", systemImage: "arrowshape.turn.up.left") } .tag(CIInfoTab.quote) + .modifier(ThemedBackground()) + } + if let forwardedFromItem = chatItemInfo?.forwardedFromChatItem { + forwardedFromTab(forwardedFromItem) + .tabItem { + Label(local ? "Saved" : "Forwarded", systemImage: "arrowshape.turn.up.forward") + } + .tag(CIInfoTab.forwarded) + .modifier(ThemedBackground()) } } .onAppear { @@ -99,18 +127,23 @@ struct ChatItemInfoView: View { } } else { historyTab() + .modifier(ThemedBackground()) } } - @ViewBuilder private func details() -> some View { + private func details() -> some View { let meta = ci.meta - VStack(alignment: .leading, spacing: 16) { + return VStack(alignment: .leading, spacing: 16) { Text(title) .font(.largeTitle) .bold() .padding(.bottom) - infoRow("Sent at", localTimestamp(meta.itemTs)) + if ci.localNote { + infoRow("Created at", localTimestamp(meta.itemTs)) + } else { + infoRow("Sent at", localTimestamp(meta.itemTs)) + } if !ci.chatDir.sent { infoRow("Received at", localTimestamp(meta.createdAt)) } @@ -131,11 +164,40 @@ struct ChatItemInfoView: View { if developerTools { infoRow("Database ID", "\(meta.itemId)") infoRow("Record updated at", localTimestamp(meta.updatedAt)) + let msv = infoRow("Message status", ci.meta.itemStatus.id) + Group { + if messageStatusLimited { + msv.lineLimit(1) + } else { + msv + } + } + .onTapGesture { + withAnimation { + messageStatusLimited.toggle() + } + } + + if let file = ci.file { + let fsv = infoRow("File status", file.fileStatus.id) + Group { + if fileStatusLimited { + fsv.lineLimit(1) + } else { + fsv + } + } + .onTapGesture { + withAnimation { + fileStatusLimited.toggle() + } + } + } } } } - @ViewBuilder private func historyTab() -> some View { + private func historyTab() -> some View { GeometryReader { g in let maxWidth = (g.size.width - 32) * 0.84 ScrollView { @@ -155,7 +217,7 @@ struct ChatItemInfoView: View { } else { Text("No history") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity) } } @@ -165,14 +227,14 @@ struct ChatItemInfoView: View { } } - @ViewBuilder private func itemVersionView(_ itemVersion: ChatItemVersion, _ maxWidth: CGFloat, current: Bool) -> some View { - VStack(alignment: .leading, spacing: 4) { - textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil) - .allowsHitTesting(false) + private func itemVersionView(_ itemVersion: ChatItemVersion, _ maxWidth: CGFloat, current: Bool) -> some View { + let backgroundColor = chatItemFrameColor(ci, theme) + return VStack(alignment: .leading, spacing: 4) { + textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil, backgroundColor: UIColor(backgroundColor)) .padding(.horizontal, 12) .padding(.vertical, 6) - .background(chatItemFrameColor(ci, colorScheme)) - .cornerRadius(18) + .background(backgroundColor) + .modifier(ChatItemClipped()) .contextMenu { if itemVersion.msgContent.text != "" { Button { @@ -196,17 +258,33 @@ struct ChatItemInfoView: View { .frame(maxWidth: maxWidth, alignment: .leading) } - @ViewBuilder private func textBubble(_ text: String, _ formattedText: [FormattedText]?, _ sender: String? = nil) -> some View { + @ViewBuilder private func textBubble(_ text: String, _ formattedText: [FormattedText]?, _ sender: String? = nil, backgroundColor: UIColor) -> some View { if text != "" { - messageText(text, formattedText, sender) + TextBubble(text: text, formattedText: formattedText, sender: sender, mentions: ci.mentions, userMemberId: userMemberId, backgroundColor: backgroundColor) } else { Text("no text") .italic() - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } - @ViewBuilder private func quoteTab(_ qi: CIQuote) -> some View { + private struct TextBubble: View { + @EnvironmentObject var theme: AppTheme + var text: String + var formattedText: [FormattedText]? + var sender: String? = nil + var mentions: [String: CIMention]? + var userMemberId: String? + var backgroundColor: UIColor + @State private var showSecrets: Set = [] + + var body: some View { + let r = messageText(text, formattedText, sender: sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, backgroundColor: backgroundColor) + return msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets) + } + } + + private func quoteTab(_ qi: CIQuote) -> some View { GeometryReader { g in let maxWidth = (g.size.width - 32) * 0.84 ScrollView { @@ -224,14 +302,14 @@ struct ChatItemInfoView: View { } } - @ViewBuilder private func quotedMsgView(_ qi: CIQuote, _ maxWidth: CGFloat) -> some View { - VStack(alignment: .leading, spacing: 4) { - textBubble(qi.text, qi.formattedText, qi.getSender(nil)) - .allowsHitTesting(false) + private func quotedMsgView(_ qi: CIQuote, _ maxWidth: CGFloat) -> some View { + let backgroundColor = quotedMsgFrameColor(qi, theme) + return VStack(alignment: .leading, spacing: 4) { + textBubble(qi.text, qi.formattedText, qi.getSender(nil), backgroundColor: UIColor(backgroundColor)) .padding(.horizontal, 12) .padding(.vertical, 6) - .background(quotedMsgFrameColor(qi, colorScheme)) - .cornerRadius(18) + .background(quotedMsgFrameColor(qi, theme)) + .modifier(ChatItemClipped()) .contextMenu { if qi.text != "" { Button { @@ -254,13 +332,82 @@ struct ChatItemInfoView: View { .frame(maxWidth: maxWidth, alignment: .leading) } - func quotedMsgFrameColor(_ qi: CIQuote, _ colorScheme: ColorScheme) -> Color { + func quotedMsgFrameColor(_ qi: CIQuote, _ theme: AppTheme) -> Color { (qi.chatDir?.sent ?? false) - ? (colorScheme == .light ? sentColorLight : sentColorDark) - : Color(uiColor: .tertiarySystemGroupedBackground) + ? theme.appColors.sentMessage + : theme.appColors.receivedMessage } - @ViewBuilder private func deliveryTab(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View { + private func forwardedFromTab(_ forwardedFromItem: AChatItem) -> some View { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + details() + Divider().padding(.vertical) + Text(local ? "Saved from" : "Forwarded from") + .font(.title2) + .padding(.bottom, 4) + forwardedFromView(forwardedFromItem) + } + .padding() + } + .frame(maxHeight: .infinity, alignment: .top) + } + + private func forwardedFromView(_ forwardedFromItem: AChatItem) -> some View { + VStack(alignment: .leading, spacing: 8) { + Button { + Task { + await MainActor.run { + ItemsModel.shared.loadOpenChat(forwardedFromItem.chatInfo.id) { + dismiss() + } + } + } + } label: { + forwardedFromSender(forwardedFromItem) + } + + if !local { + Divider().padding(.top, 32) + Text("Recipient(s) can't see who this message is from.") + .font(.caption) + .foregroundColor(theme.colors.secondary) + } + } + } + + private func forwardedFromSender(_ forwardedFromItem: AChatItem) -> some View { + HStack { + ChatInfoImage(chat: Chat(chatInfo: forwardedFromItem.chatInfo), size: 48) + .padding(.trailing, 6) + + if forwardedFromItem.chatItem.chatDir.sent { + VStack(alignment: .leading) { + Text("you") + .italic() + .foregroundColor(theme.colors.onBackground) + Text(forwardedFromItem.chatInfo.chatViewName) + .foregroundColor(theme.colors.secondary) + .lineLimit(1) + } + } else if case let .groupRcv(groupMember) = forwardedFromItem.chatItem.chatDir { + VStack(alignment: .leading) { + Text(groupMember.chatViewName) + .foregroundColor(theme.colors.onBackground) + .lineLimit(1) + Text(forwardedFromItem.chatInfo.chatViewName) + .foregroundColor(theme.colors.secondary) + .lineLimit(1) + } + } else { + Text(forwardedFromItem.chatInfo.chatViewName) + .foregroundColor(theme.colors.onBackground) + .lineLimit(1) + } + } + } + + private func deliveryTab(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View { ScrollView { VStack(alignment: .leading, spacing: 16) { details() @@ -275,57 +422,44 @@ struct ChatItemInfoView: View { .frame(maxHeight: .infinity, alignment: .top) } - @ViewBuilder private func memberDeliveryStatusesView(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View { - VStack(alignment: .leading, spacing: 12) { + private func memberDeliveryStatusesView(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View { + LazyVStack(alignment: .leading, spacing: 12) { let mss = membersStatuses(memberDeliveryStatuses) if !mss.isEmpty { ForEach(mss, id: \.0.groupMemberId) { memberStatus in - memberDeliveryStatusView(memberStatus.0, memberStatus.1) + memberDeliveryStatusView(memberStatus.0, memberStatus.1, memberStatus.2) } } else { Text("No delivery information") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } } - private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus)] { + private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, GroupSndStatus, Bool?)] { memberDeliveryStatuses.compactMap({ mds in if let mem = chatModel.getGroupMember(mds.groupMemberId) { - return (mem.wrapped, mds.memberDeliveryStatus) + return (mem.wrapped, mds.memberDeliveryStatus, mds.sentViaProxy) } else { return nil } }) } - private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus) -> some View { + private func memberDeliveryStatusView(_ member: GroupMember, _ status: GroupSndStatus, _ sentViaProxy: Bool?) -> some View { HStack{ - ProfileImage(imageStr: member.image) - .frame(width: 30, height: 30) + MemberProfileImage(member, size: 30) .padding(.trailing, 2) Text(member.chatViewName) .lineLimit(1) Spacer() + if sentViaProxy == true { + Image(systemName: "arrow.forward") + .foregroundColor(theme.colors.secondary).opacity(0.67) + } let v = Group { - if let (icon, statusColor) = status.statusIcon(Color.secondary) { - switch status { - case .sndRcvd: - ZStack(alignment: .trailing) { - Image(systemName: icon) - .foregroundColor(statusColor.opacity(0.67)) - .padding(.trailing, 6) - Image(systemName: icon) - .foregroundColor(statusColor.opacity(0.67)) - } - default: - Image(systemName: icon) - .foregroundColor(statusColor) - } - } else { - Image(systemName: "ellipsis") - .foregroundColor(Color.secondary) - } + let (image, statusColor) = status.statusIcon(theme.colors.secondary, theme.colors.primary) + image.foregroundColor(statusColor) } if let (title, text) = status.statusInfo { @@ -341,7 +475,12 @@ struct ChatItemInfoView: View { private func itemInfoShareText() -> String { let meta = ci.meta var shareText: [String] = [String.localizedStringWithFormat(NSLocalizedString("# %@", comment: "copied message info title, # "), title), ""] - shareText += [String.localizedStringWithFormat(NSLocalizedString("Sent at: %@", comment: "copied message info"), localTimestamp(meta.itemTs))] + shareText += [String.localizedStringWithFormat( + ci.localNote + ? NSLocalizedString("Created at: %@", comment: "copied message info") + : NSLocalizedString("Sent at: %@", comment: "copied message info"), + localTimestamp(meta.itemTs)) + ] if !ci.chatDir.sent { shareText += [String.localizedStringWithFormat(NSLocalizedString("Received at: %@", comment: "copied message info"), localTimestamp(meta.createdAt))] } @@ -362,8 +501,12 @@ struct ChatItemInfoView: View { if developerTools { shareText += [ String.localizedStringWithFormat(NSLocalizedString("Database ID: %d", comment: "copied message info"), meta.itemId), - String.localizedStringWithFormat(NSLocalizedString("Record updated at: %@", comment: "copied message info"), localTimestamp(meta.updatedAt)) + String.localizedStringWithFormat(NSLocalizedString("Record updated at: %@", comment: "copied message info"), localTimestamp(meta.updatedAt)), + String.localizedStringWithFormat(NSLocalizedString("Message status: %@", comment: "copied message info"), meta.itemStatus.id) ] + if let file = ci.file { + shareText += [String.localizedStringWithFormat(NSLocalizedString("File status: %@", comment: "copied message info"), file.fileStatus.id)] + } } if let qi = ci.quotedItem { shareText += ["", NSLocalizedString("## In reply to", comment: "copied message info")] @@ -413,6 +556,6 @@ func localTimestamp(_ date: Date) -> String { struct ChatItemInfoView_Previews: PreviewProvider { static var previews: some View { - ChatItemInfoView(ci: ChatItem.getSample(1, .directSnd, .now, "hello"), chatItemInfo: Binding.constant(nil)) + ChatItemInfoView(ci: ChatItem.getSample(1, .directSnd, .now, "hello"), userMemberId: Chat.sampleData.chatInfo.groupInfo?.membership.memberId, chatItemInfo: Binding.constant(nil)) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index 657df60654..f5558bcd93 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -9,50 +9,71 @@ import SwiftUI import SimpleXChat +extension EnvironmentValues { + struct ShowTimestamp: EnvironmentKey { + static let defaultValue: Bool = true + } + + struct Revealed: EnvironmentKey { + static let defaultValue: Bool = true + } + + struct ContainerBackground: EnvironmentKey { + static let defaultValue: UIColor = .clear + } + + var showTimestamp: Bool { + get { self[ShowTimestamp.self] } + set { self[ShowTimestamp.self] = newValue } + } + + var revealed: Bool { + get { self[Revealed.self] } + set { self[Revealed.self] = newValue } + } + + var containerBackground: UIColor { + get { self[ContainerBackground.self] } + set { self[ContainerBackground.self] = newValue } + } +} + struct ChatItemView: View { @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme + @Environment(\.showTimestamp) var showTimestamp: Bool + @Environment(\.revealed) var revealed: Bool var chatItem: ChatItem + var scrollToItemId: (ChatItem.ID) -> Void var maxWidth: CGFloat = .infinity - @State var scrollProxy: ScrollViewProxy? = nil - @Binding var revealed: Bool @Binding var allowMenu: Bool - @Binding var audioPlayer: AudioPlayer? - @Binding var playbackState: VoiceMessagePlaybackState - @Binding var playbackTime: TimeInterval? + init( chat: Chat, chatItem: ChatItem, + scrollToItemId: @escaping (ChatItem.ID) -> Void, showMember: Bool = false, maxWidth: CGFloat = .infinity, - scrollProxy: ScrollViewProxy? = nil, - revealed: Binding<Bool>, - allowMenu: Binding<Bool> = .constant(false), - audioPlayer: Binding<AudioPlayer?> = .constant(nil), - playbackState: Binding<VoiceMessagePlaybackState> = .constant(.noPlayback), - playbackTime: Binding<TimeInterval?> = .constant(nil) + allowMenu: Binding<Bool> = .constant(false) ) { self.chat = chat self.chatItem = chatItem + self.scrollToItemId = scrollToItemId self.maxWidth = maxWidth - _scrollProxy = .init(initialValue: scrollProxy) - _revealed = revealed _allowMenu = allowMenu - _audioPlayer = audioPlayer - _playbackState = playbackState - _playbackTime = playbackTime } var body: some View { let ci = chatItem if chatItem.meta.itemDeleted != nil && (!revealed || chatItem.isDeletedContent) { - MarkedDeletedItemView(chat: chat, chatItem: chatItem, revealed: $revealed) - } else if ci.quotedItem == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive { + MarkedDeletedItemView(chat: chat, chatItem: chatItem) + } else if ci.quotedItem == nil && ci.meta.itemForwarded == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive { if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) { EmojiItemView(chat: chat, chatItem: ci) } else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent { - CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, allowMenu: $allowMenu) + CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: $allowMenu) } else if ci.content.msgContent == nil { - ChatItemContentView(chat: chat, chatItem: chatItem, revealed: $revealed, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case + ChatItemContentView(chat: chat, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case } else { framedItemView() } @@ -62,15 +83,41 @@ struct ChatItemView: View { } private func framedItemView() -> some View { - FramedItemView(chat: chat, chatItem: chatItem, revealed: $revealed, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime) + let preview = chatItem.content.msgContent + .flatMap { + switch $0 { + case let .image(_, image): image + case let .video(_, image, _): image + default: nil + } + } + .flatMap { imageFromBase64($0) } + let adjustedMaxWidth = { + if let preview, preview.size.width <= preview.size.height { + maxWidth * 0.75 + } else { + maxWidth + } + }() + return FramedItemView( + chat: chat, + chatItem: chatItem, + scrollToItemId: scrollToItemId, + preview: preview, + maxWidth: maxWidth, + imgWidth: adjustedMaxWidth, + videoWidth: adjustedMaxWidth, + allowMenu: $allowMenu + ) } } struct ChatItemContentView<Content: View>: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @Environment(\.revealed) var revealed: Bool @ObservedObject var chat: Chat var chatItem: ChatItem - @Binding var revealed: Bool var msgContentView: () -> Content @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @@ -97,18 +144,23 @@ struct ChatItemContentView<Content: View>: View { case .sndGroupEvent: eventItemView() case .rcvConnEvent: eventItemView() case .sndConnEvent: eventItemView() - case let .rcvChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor) - case let .sndChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor) + case let .rcvChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor(theme.colors.secondary)) + case let .sndChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor(theme.colors.secondary)) case let .rcvChatPreference(feature, allowed, param): CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param) case let .sndChatPreference(feature, _, _): - CIChatFeatureView(chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: .secondary) - case let .rcvGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor) - case let .sndGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor) + CIChatFeatureView(chat: chat, chatItem: chatItem, feature: feature, icon: feature.icon, iconColor: theme.colors.secondary) + case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary)) + case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary)) case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red) case let .rcvGroupFeatureRejected(feature): chatFeatureView(feature, .red) case .sndModerated: deletedItemView() case .rcvModerated: deletedItemView() + 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 .invalidJSON(json): CIInvalidJSONView(json: json) } } @@ -122,29 +174,29 @@ struct ChatItemContentView<Content: View>: View { } private func groupInvitationItemView(_ groupInvitation: CIGroupInvitation, _ memberRole: GroupMemberRole) -> some View { - CIGroupInvitationView(chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole, chatIncognito: chat.chatInfo.incognito) + CIGroupInvitationView(chat: chat, chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole, chatIncognito: chat.chatInfo.incognito) } private func eventItemView() -> some View { - return CIEventView(eventText: eventItemViewText()) + CIEventView(eventText: eventItemViewText(theme.colors.secondary)) } - private func eventItemViewText() -> Text { + private func eventItemViewText(_ secondaryColor: Color) -> Text { if !revealed, let t = mergedGroupEventText { - return chatEventText(t + Text(" ") + chatItem.timestampText) + return chatEventText(t + textSpace + chatItem.timestampText, secondaryColor) } else if let member = chatItem.memberDisplayName { return Text(member + " ") .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(secondaryColor) .fontWeight(.light) - + chatEventText(chatItem) + + chatEventText(chatItem, secondaryColor) } else { - return chatEventText(chatItem) + return chatEventText(chatItem, secondaryColor) } } private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View { - CIChatFeatureView(chatItem: chatItem, revealed: $revealed, feature: feature, iconColor: iconColor) + CIChatFeatureView(chat: chat, chatItem: chatItem, feature: feature, iconColor: iconColor) } private var mergedGroupEventText: Text? { @@ -164,41 +216,58 @@ struct ChatItemContentView<Content: View>: View { } else if ns.count == 0 { Text("\(count) group events") } else if count > ns.count { - Text(members) + Text(" ") + Text("and \(count - ns.count) other events") + Text(members) + textSpace + Text("and \(count - ns.count) other events") } else { Text(members) } } + + private func directE2EEInfoText(_ info: E2EEInfo) -> Text { + info.pqEnabled + ? Text("Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.") + .font(.caption) + .foregroundColor(theme.colors.secondary) + .fontWeight(.light) + : e2eeInfoNoPQText() + } + + private func e2eeInfoNoPQText() -> Text { + Text("Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.") + .font(.caption) + .foregroundColor(theme.colors.secondary) + .fontWeight(.light) + } } -func chatEventText(_ text: Text) -> Text { +func chatEventText(_ text: Text, _ secondaryColor: Color) -> Text { text .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(secondaryColor) .fontWeight(.light) } -func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text) -> Text { - chatEventText(Text(eventText) + Text(" ") + ts) +func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text, _ secondaryColor: Color) -> Text { + chatEventText(Text(eventText) + textSpace + ts, secondaryColor) } -func chatEventText(_ ci: ChatItem) -> Text { - chatEventText("\(ci.content.text)", ci.timestampText) +func chatEventText(_ ci: ChatItem, _ secondaryColor: Color) -> Text { + chatEventText("\(ci.content.text)", ci.timestampText, secondaryColor) } struct ChatItemView_Previews: PreviewProvider { static var previews: some View { Group{ - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(), revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(false)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true)) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(), scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), scrollToItemId: { _ in }).environment(\.revealed, true) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), scrollToItemId: { _ in }).environment(\.revealed, true) } + .environment(\.revealed, false) .previewLayout(.fixed(width: 360, height: 70)) .environmentObject(Chat.sampleData) } @@ -217,7 +286,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider { quotedItem: nil, file: nil ), - revealed: Binding.constant(true) + scrollToItemId: { _ in } ) ChatItemView( chat: Chat.sampleData, @@ -228,7 +297,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider { quotedItem: nil, file: nil ), - revealed: Binding.constant(true) + scrollToItemId: { _ in } ) ChatItemView( chat: Chat.sampleData, @@ -239,7 +308,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider { quotedItem: nil, file: nil ), - revealed: Binding.constant(true) + scrollToItemId: { _ in } ) ChatItemView( chat: Chat.sampleData, @@ -250,7 +319,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider { quotedItem: nil, file: nil ), - revealed: Binding.constant(true) + scrollToItemId: { _ in } ) ChatItemView( chat: Chat.sampleData, @@ -261,9 +330,10 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider { quotedItem: nil, file: nil ), - revealed: Binding.constant(true) + scrollToItemId: { _ in } ) } + .environment(\.revealed, true) .previewLayout(.fixed(width: 360, height: 70)) .environmentObject(Chat.sampleData) } diff --git a/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift b/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift new file mode 100644 index 0000000000..07034cf8ec --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift @@ -0,0 +1,511 @@ +// +// ChatItemsLoader.swift +// SimpleX (iOS) +// +// Created by Stanislav Dmitrenko on 17.12.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SimpleXChat +import SwiftUI + +let TRIM_KEEP_COUNT = 200 + +func apiLoadMessages( + _ chatId: ChatId, + _ pagination: ChatPagination, + _ chatState: ActiveChatState, + _ search: String = "", + _ openAroundItemId: ChatItem.ID? = nil, + _ visibleItemIndexesNonReversed: @MainActor () -> ClosedRange<Int> = { 0 ... 0 } +) async { + let chat: Chat + let navInfo: NavigationInfo + do { + (chat, navInfo) = try await apiGetChat(chatId: chatId, pagination: pagination, search: search) + } catch let error { + logger.error("apiLoadMessages error: \(responseError(error))") + return + } + + let chatModel = ChatModel.shared + + // For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes + let paginationIsInitial = switch pagination { case .initial: true; default: false } + let paginationIsLast = switch pagination { case .last: true; default: false } + // When openAroundItemId is provided, chatId can be different too + if ((chatModel.chatId != chat.id || chat.chatItems.isEmpty) && !paginationIsInitial && !paginationIsLast && openAroundItemId == nil) || Task.isCancelled { + return + } + + let unreadAfterItemId = chatState.unreadAfterItemId + + let oldItems = Array(ItemsModel.shared.reversedChatItems.reversed()) + var newItems: [ChatItem] = [] + switch pagination { + case .initial: + let newSplits: [Int64] = if !chat.chatItems.isEmpty && navInfo.afterTotal > 0 { [chat.chatItems.last!.id] } else { [] } + if chatModel.getChat(chat.id) == nil { + chatModel.addChat(chat) + } + await MainActor.run { + chatModel.chatItemStatuses.removeAll() + ItemsModel.shared.reversedChatItems = chat.chatItems.reversed() + chatModel.updateChatInfo(chat.chatInfo) + chatState.splits = newSplits + if !chat.chatItems.isEmpty { + chatState.unreadAfterItemId = chat.chatItems.last!.id + } + chatState.totalAfter = navInfo.afterTotal + chatState.unreadTotal = chat.chatStats.unreadCount + chatState.unreadAfter = navInfo.afterUnread + chatState.unreadAfterNewestLoaded = navInfo.afterUnread + + PreloadState.shared.clear() + } + case let .before(paginationChatItemId, _): + newItems.append(contentsOf: oldItems) + let indexInCurrentItems = oldItems.firstIndex(where: { $0.id == paginationChatItemId }) + guard let indexInCurrentItems else { return } + let (newIds, _) = mapItemsToIds(chat.chatItems) + let wasSize = newItems.count + let visibleItemIndexes = await MainActor.run { visibleItemIndexesNonReversed() } + let modifiedSplits = removeDuplicatesAndModifySplitsOnBeforePagination( + unreadAfterItemId, &newItems, newIds, chatState.splits, visibleItemIndexes + ) + let insertAt = max((indexInCurrentItems - (wasSize - newItems.count) + modifiedSplits.trimmedIds.count), 0) + newItems.insert(contentsOf: chat.chatItems, at: insertAt) + let newReversed: [ChatItem] = newItems.reversed() + await MainActor.run { + ItemsModel.shared.reversedChatItems = newReversed + chatState.splits = modifiedSplits.newSplits + chatState.moveUnreadAfterItem(modifiedSplits.oldUnreadSplitIndex, modifiedSplits.newUnreadSplitIndex, oldItems) + } + case let .after(paginationChatItemId, _): + newItems.append(contentsOf: oldItems) + let indexInCurrentItems = oldItems.firstIndex(where: { $0.id == paginationChatItemId }) + guard let indexInCurrentItems else { return } + + let mappedItems = mapItemsToIds(chat.chatItems) + let newIds = mappedItems.0 + let (newSplits, unreadInLoaded) = removeDuplicatesAndModifySplitsOnAfterPagination( + mappedItems.1, paginationChatItemId, &newItems, newIds, chat, chatState.splits + ) + let indexToAdd = min(indexInCurrentItems + 1, newItems.count) + let indexToAddIsLast = indexToAdd == newItems.count + newItems.insert(contentsOf: chat.chatItems, at: indexToAdd) + let new: [ChatItem] = newItems + let newReversed: [ChatItem] = newItems.reversed() + await MainActor.run { + ItemsModel.shared.reversedChatItems = newReversed + chatState.splits = newSplits + chatState.moveUnreadAfterItem(chatState.splits.first ?? new.last!.id, new) + // loading clear bottom area, updating number of unread items after the newest loaded item + if indexToAddIsLast { + chatState.unreadAfterNewestLoaded -= unreadInLoaded + } + } + case .around: + var newSplits: [Int64] + if openAroundItemId == nil { + newItems.append(contentsOf: oldItems) + newSplits = await removeDuplicatesAndUpperSplits(&newItems, chat, chatState.splits, visibleItemIndexesNonReversed) + } else { + newSplits = [] + } + let (itemIndex, splitIndex) = indexToInsertAround(chat.chatInfo.chatType, chat.chatItems.last, to: newItems, Set(newSplits)) + //indexToInsertAroundTest() + newItems.insert(contentsOf: chat.chatItems, at: itemIndex) + newSplits.insert(chat.chatItems.last!.id, at: splitIndex) + let newReversed: [ChatItem] = newItems.reversed() + let orderedSplits = newSplits + await MainActor.run { + ItemsModel.shared.reversedChatItems = newReversed + chatState.splits = orderedSplits + chatState.unreadAfterItemId = chat.chatItems.last!.id + chatState.totalAfter = navInfo.afterTotal + chatState.unreadTotal = chat.chatStats.unreadCount + chatState.unreadAfter = navInfo.afterUnread + + if let openAroundItemId { + chatState.unreadAfterNewestLoaded = navInfo.afterUnread + ChatModel.shared.openAroundItemId = openAroundItemId + ChatModel.shared.chatId = chatId + } else { + // no need to set it, count will be wrong + // chatState.unreadAfterNewestLoaded = navInfo.afterUnread + } + PreloadState.shared.clear() + } + case .last: + newItems.append(contentsOf: oldItems) + let newSplits = await removeDuplicatesAndUnusedSplits(&newItems, chat, chatState.splits) + newItems.append(contentsOf: chat.chatItems) + let items = newItems + await MainActor.run { + ItemsModel.shared.reversedChatItems = items.reversed() + chatState.splits = newSplits + chatModel.updateChatInfo(chat.chatInfo) + chatState.unreadAfterNewestLoaded = 0 + } + } +} + + +private class ModifiedSplits { + let oldUnreadSplitIndex: Int + let newUnreadSplitIndex: Int + let trimmedIds: Set<Int64> + let newSplits: [Int64] + + init(oldUnreadSplitIndex: Int, newUnreadSplitIndex: Int, trimmedIds: Set<Int64>, newSplits: [Int64]) { + self.oldUnreadSplitIndex = oldUnreadSplitIndex + self.newUnreadSplitIndex = newUnreadSplitIndex + self.trimmedIds = trimmedIds + self.newSplits = newSplits + } +} + +private func removeDuplicatesAndModifySplitsOnBeforePagination( + _ unreadAfterItemId: Int64, + _ newItems: inout [ChatItem], + _ newIds: Set<Int64>, + _ splits: [Int64], + _ visibleItemIndexes: ClosedRange<Int> +) -> ModifiedSplits { + var oldUnreadSplitIndex: Int = -1 + var newUnreadSplitIndex: Int = -1 + var lastSplitIndexTrimmed: Int? = nil + var allowedTrimming = true + var index = 0 + /** keep the newest [TRIM_KEEP_COUNT] items (bottom area) and oldest [TRIM_KEEP_COUNT] items, trim others */ + let trimLowerBound = visibleItemIndexes.upperBound + TRIM_KEEP_COUNT + let trimUpperBound = newItems.count - TRIM_KEEP_COUNT + let trimRange = trimUpperBound >= trimLowerBound ? trimLowerBound ... trimUpperBound : -1 ... -1 + var trimmedIds = Set<Int64>() + let prevTrimLowerBound = visibleItemIndexes.upperBound + TRIM_KEEP_COUNT + 1 + let prevTrimUpperBound = newItems.count - TRIM_KEEP_COUNT + let prevItemTrimRange = prevTrimUpperBound >= prevTrimLowerBound ? prevTrimLowerBound ... prevTrimUpperBound : -1 ... -1 + var newSplits = splits + + newItems.removeAll(where: { + let invisibleItemToTrim = trimRange.contains(index) && allowedTrimming + let prevItemWasTrimmed = prevItemTrimRange.contains(index) && allowedTrimming + // may disable it after clearing the whole split range + if !splits.isEmpty && $0.id == splits.first { + // trim only in one split range + allowedTrimming = false + } + let indexInSplits = splits.firstIndex(of: $0.id) + if let indexInSplits { + lastSplitIndexTrimmed = indexInSplits + } + if invisibleItemToTrim { + if prevItemWasTrimmed { + trimmedIds.insert($0.id) + } else { + newUnreadSplitIndex = index + // prev item is not supposed to be trimmed, so exclude current one from trimming and set a split here instead. + // this allows to define splitRange of the oldest items and to start loading trimmed items when user scrolls in the opposite direction + if let lastSplitIndexTrimmed { + var new = newSplits + new[lastSplitIndexTrimmed] = $0.id + newSplits = new + } else { + newSplits = [$0.id] + newSplits + } + } + } + if unreadAfterItemId == $0.id { + oldUnreadSplitIndex = index + } + index += 1 + return (invisibleItemToTrim && prevItemWasTrimmed) || newIds.contains($0.id) + }) + // will remove any splits that now becomes obsolete because items were merged + newSplits = newSplits.filter { split in !newIds.contains(split) && !trimmedIds.contains(split) } + return ModifiedSplits(oldUnreadSplitIndex: oldUnreadSplitIndex, newUnreadSplitIndex: newUnreadSplitIndex, trimmedIds: trimmedIds, newSplits: newSplits) +} + +private func removeDuplicatesAndModifySplitsOnAfterPagination( + _ unreadInLoaded: Int, + _ paginationChatItemId: Int64, + _ newItems: inout [ChatItem], + _ newIds: Set<Int64>, + _ chat: Chat, + _ splits: [Int64] +) -> ([Int64], Int) { + var unreadInLoaded = unreadInLoaded + var firstItemIdBelowAllSplits: Int64? = nil + var splitsToRemove: Set<Int64> = [] + let indexInSplitRanges = splits.firstIndex(of: paginationChatItemId) + // Currently, it should always load from split range + let loadingFromSplitRange = indexInSplitRanges != nil + let topSplits: [Int64] + var splitsToMerge: [Int64] + if let indexInSplitRanges, loadingFromSplitRange && indexInSplitRanges + 1 <= splits.count { + splitsToMerge = Array(splits[indexInSplitRanges + 1 ..< splits.count]) + topSplits = Array(splits[0 ..< indexInSplitRanges + 1]) + } else { + splitsToMerge = [] + topSplits = [] + } + newItems.removeAll(where: { new in + let duplicate = newIds.contains(new.id) + if loadingFromSplitRange && duplicate { + if splitsToMerge.contains(new.id) { + splitsToMerge.removeAll(where: { $0 == new.id }) + splitsToRemove.insert(new.id) + } else if firstItemIdBelowAllSplits == nil && splitsToMerge.isEmpty { + // we passed all splits and found duplicated item below all of them, which means no splits anymore below the loaded items + firstItemIdBelowAllSplits = new.id + } + } + if duplicate && new.isRcvNew { + unreadInLoaded -= 1 + } + return duplicate + }) + var newSplits: [Int64] = [] + if firstItemIdBelowAllSplits != nil { + // no splits below anymore, all were merged with bottom items + newSplits = topSplits + } else { + if !splitsToRemove.isEmpty { + var new = splits + new.removeAll(where: { splitsToRemove.contains($0) }) + newSplits = new + } + let enlargedSplit = splits.firstIndex(of: paginationChatItemId) + if let enlargedSplit { + // move the split to the end of loaded items + var new = splits + new[enlargedSplit] = chat.chatItems.last!.id + newSplits = new + } + } + return (newSplits, unreadInLoaded) +} + +private func removeDuplicatesAndUpperSplits( + _ newItems: inout [ChatItem], + _ chat: Chat, + _ splits: [Int64], + _ visibleItemIndexesNonReversed: @MainActor () -> ClosedRange<Int> +) async -> [Int64] { + if splits.isEmpty { + removeDuplicates(&newItems, chat) + return splits + } + + var newSplits = splits + let visibleItemIndexes = await MainActor.run { visibleItemIndexesNonReversed() } + let (newIds, _) = mapItemsToIds(chat.chatItems) + var idsToTrim: [BoxedValue<Set<Int64>>] = [] + idsToTrim.append(BoxedValue(Set())) + var index = 0 + newItems.removeAll(where: { + let duplicate = newIds.contains($0.id) + if (!duplicate && visibleItemIndexes.lowerBound > index) { + idsToTrim.last?.boxedValue.insert($0.id) + } + if visibleItemIndexes.lowerBound > index, let firstIndex = newSplits.firstIndex(of: $0.id) { + newSplits.remove(at: firstIndex) + // closing previous range. All items in idsToTrim that ends with empty set should be deleted. + // Otherwise, the last set should be excluded from trimming because it is in currently visible split range + idsToTrim.append(BoxedValue(Set())) + } + + index += 1 + return duplicate + }) + if !idsToTrim.last!.boxedValue.isEmpty { + // it has some elements to trim from currently visible range which means the items shouldn't be trimmed + // Otherwise, the last set would be empty + idsToTrim.removeLast() + } + let allItemsToDelete = idsToTrim.compactMap { set in set.boxedValue }.joined() + if !allItemsToDelete.isEmpty { + newItems.removeAll(where: { allItemsToDelete.contains($0.id) }) + } + return newSplits +} + +private func removeDuplicatesAndUnusedSplits( + _ newItems: inout [ChatItem], + _ chat: Chat, + _ splits: [Int64] +) async -> [Int64] { + if splits.isEmpty { + removeDuplicates(&newItems, chat) + return splits + } + + var newSplits = splits + let (newIds, _) = mapItemsToIds(chat.chatItems) + newItems.removeAll(where: { + let duplicate = newIds.contains($0.id) + if duplicate, let firstIndex = newSplits.firstIndex(of: $0.id) { + newSplits.remove(at: firstIndex) + } + return duplicate + }) + return newSplits +} + +// ids, number of unread items +private func mapItemsToIds(_ items: [ChatItem]) -> (Set<Int64>, Int) { + var unreadInLoaded = 0 + var ids: Set<Int64> = Set() + var i = 0 + while i < items.count { + let item = items[i] + ids.insert(item.id) + if item.isRcvNew { + unreadInLoaded += 1 + } + i += 1 + } + return (ids, unreadInLoaded) +} + +private func removeDuplicates(_ newItems: inout [ChatItem], _ chat: Chat) { + let (newIds, _) = mapItemsToIds(chat.chatItems) + newItems.removeAll { newIds.contains($0.id) } +} + +private typealias SameTimeItem = (index: Int, item: ChatItem) + +// return (item index, split index) +private func indexToInsertAround(_ chatType: ChatType, _ lastNew: ChatItem?, to: [ChatItem], _ splits: Set<Int64>) -> (Int, Int) { + guard to.count > 0, let lastNew = lastNew else { return (0, 0) } + // group sorting: item_ts, item_id + // everything else: created_at, item_id + let compareByTimeTs = chatType == .group + // in case several items have the same time as another item in the `to` array + var sameTime: [SameTimeItem] = [] + + // trying to find new split index for item looks difficult but allows to not use one more loop. + // The idea is to memorize how many splits were till any index (map number of splits until index) + // and use resulting itemIndex to decide new split index position. + // Because of the possibility to have many items with the same timestamp, it's possible to see `itemIndex < || == || > i`. + var splitsTillIndex: [Int] = [] + var splitsPerPrevIndex = 0 + + for i in 0 ..< to.count { + let item = to[i] + + splitsPerPrevIndex = splits.contains(item.id) ? splitsPerPrevIndex + 1 : splitsPerPrevIndex + splitsTillIndex.append(splitsPerPrevIndex) + + let itemIsNewer = (compareByTimeTs ? item.meta.itemTs > lastNew.meta.itemTs : item.meta.createdAt > lastNew.meta.createdAt) + if itemIsNewer || i + 1 == to.count { + if (compareByTimeTs ? lastNew.meta.itemTs == item.meta.itemTs : lastNew.meta.createdAt == item.meta.createdAt) { + sameTime.append((i, item)) + } + // time to stop the loop. Item is newer or it's the last item in `to` array, taking previous items and checking position inside them + let itemIndex: Int + if sameTime.count > 1, let first = sameTime.sorted(by: { prev, next in prev.item.meta.itemId < next.item.id }).first(where: { same in same.item.id > lastNew.id }) { + itemIndex = first.index + } else if sameTime.count == 1 { + itemIndex = sameTime[0].item.id > lastNew.id ? sameTime[0].index : sameTime[0].index + 1 + } else { + itemIndex = itemIsNewer ? i : i + 1 + } + let splitIndex = splitsTillIndex[min(itemIndex, splitsTillIndex.count - 1)] + let prevItemSplitIndex = itemIndex == 0 ? 0 : splitsTillIndex[min(itemIndex - 1, splitsTillIndex.count - 1)] + return (itemIndex, splitIndex == prevItemSplitIndex ? splitIndex : prevItemSplitIndex) + } + + if (compareByTimeTs ? lastNew.meta.itemTs == item.meta.itemTs : lastNew.meta.createdAt == item.meta.createdAt) { + sameTime.append(SameTimeItem(index: i, item: item)) + } else { + sameTime = [] + } + } + // shouldn't be here + return (to.count, splits.count) +} + +private func indexToInsertAroundTest() { + func assert(_ one: (Int, Int), _ two: (Int, Int)) { + if one != two { + logger.debug("\(String(describing: one)) != \(String(describing: two))") + fatalError() + } + } + + let itemsToInsert = [ChatItem.getSample(3, .groupSnd, Date.init(timeIntervalSince1970: 3), "")] + let items1 = [ + ChatItem.getSample(0, .groupSnd, Date.init(timeIntervalSince1970: 0), ""), + ChatItem.getSample(1, .groupSnd, Date.init(timeIntervalSince1970: 1), ""), + ChatItem.getSample(2, .groupSnd, Date.init(timeIntervalSince1970: 2), "") + ] + assert(indexToInsertAround(.group, itemsToInsert.last, to: items1, Set([1])), (3, 1)) + + let items2 = [ + ChatItem.getSample(0, .groupSnd, Date.init(timeIntervalSince1970: 0), ""), + ChatItem.getSample(1, .groupSnd, Date.init(timeIntervalSince1970: 1), ""), + ChatItem.getSample(2, .groupSnd, Date.init(timeIntervalSince1970: 3), "") + ] + assert(indexToInsertAround(.group, itemsToInsert.last, to: items2, Set([2])), (3, 1)) + + let items3 = [ + ChatItem.getSample(0, .groupSnd, Date.init(timeIntervalSince1970: 0), ""), + ChatItem.getSample(1, .groupSnd, Date.init(timeIntervalSince1970: 3), ""), + ChatItem.getSample(2, .groupSnd, Date.init(timeIntervalSince1970: 3), "") + ] + assert(indexToInsertAround(.group, itemsToInsert.last, to: items3, Set([1])), (3, 1)) + + let items4 = [ + ChatItem.getSample(0, .groupSnd, Date.init(timeIntervalSince1970: 0), ""), + ChatItem.getSample(4, .groupSnd, Date.init(timeIntervalSince1970: 3), ""), + ChatItem.getSample(5, .groupSnd, Date.init(timeIntervalSince1970: 3), "") + ] + assert(indexToInsertAround(.group, itemsToInsert.last, to: items4, Set([4])), (1, 0)) + + let items5 = [ + ChatItem.getSample(0, .groupSnd, Date.init(timeIntervalSince1970: 0), ""), + ChatItem.getSample(2, .groupSnd, Date.init(timeIntervalSince1970: 3), ""), + ChatItem.getSample(4, .groupSnd, Date.init(timeIntervalSince1970: 3), "") + ] + assert(indexToInsertAround(.group, itemsToInsert.last, to: items5, Set([2])), (2, 1)) + + let items6 = [ + ChatItem.getSample(4, .groupSnd, Date.init(timeIntervalSince1970: 4), ""), + ChatItem.getSample(5, .groupSnd, Date.init(timeIntervalSince1970: 4), ""), + ChatItem.getSample(6, .groupSnd, Date.init(timeIntervalSince1970: 4), "") + ] + assert(indexToInsertAround(.group, itemsToInsert.last, to: items6, Set([5])), (0, 0)) + + let items7 = [ + ChatItem.getSample(4, .groupSnd, Date.init(timeIntervalSince1970: 4), ""), + ChatItem.getSample(5, .groupSnd, Date.init(timeIntervalSince1970: 4), ""), + ChatItem.getSample(6, .groupSnd, Date.init(timeIntervalSince1970: 4), "") + ] + assert(indexToInsertAround(.group, nil, to: items7, Set([6])), (0, 0)) + + let items8 = [ + ChatItem.getSample(2, .groupSnd, Date.init(timeIntervalSince1970: 4), ""), + ChatItem.getSample(4, .groupSnd, Date.init(timeIntervalSince1970: 3), ""), + ChatItem.getSample(5, .groupSnd, Date.init(timeIntervalSince1970: 4), "") + ] + assert(indexToInsertAround(.group, itemsToInsert.last, to: items8, Set([2])), (0, 0)) + + let items9 = [ + ChatItem.getSample(2, .groupSnd, Date.init(timeIntervalSince1970: 3), ""), + ChatItem.getSample(4, .groupSnd, Date.init(timeIntervalSince1970: 3), ""), + ChatItem.getSample(5, .groupSnd, Date.init(timeIntervalSince1970: 4), "") + ] + assert(indexToInsertAround(.group, itemsToInsert.last, to: items9, Set([5])), (1, 0)) + + let items10 = [ + ChatItem.getSample(4, .groupSnd, Date.init(timeIntervalSince1970: 3), ""), + ChatItem.getSample(5, .groupSnd, Date.init(timeIntervalSince1970: 3), ""), + ChatItem.getSample(6, .groupSnd, Date.init(timeIntervalSince1970: 4), "") + ] + assert(indexToInsertAround(.group, itemsToInsert.last, to: items10, Set([4])), (0, 0)) + + let items11: [ChatItem] = [] + assert(indexToInsertAround(.group, itemsToInsert.last, to: items11, Set([])), (0, 0)) +} diff --git a/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift b/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift new file mode 100644 index 0000000000..0a55ed48cc --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift @@ -0,0 +1,456 @@ +// +// ChatItemsMerger.swift +// SimpleX (iOS) +// +// Created by Stanislav Dmitrenko on 02.12.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct MergedItems: Hashable, Equatable { + let items: [MergedItem] + let splits: [SplitRange] + // chat item id, index in list + let indexInParentItems: Dictionary<Int64, Int> + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.hashValue == rhs.hashValue + } + + func hash(into hasher: inout Hasher) { + hasher.combine("\(items.hashValue)") + } + + static func create(_ items: [ChatItem], _ revealedItems: Set<Int64>, _ chatState: ActiveChatState) -> MergedItems { + if items.isEmpty { + return MergedItems(items: [], splits: [], indexInParentItems: [:]) + } + + let unreadCount = chatState.unreadTotal + + let unreadAfterItemId = chatState.unreadAfterItemId + let itemSplits = chatState.splits + var mergedItems: [MergedItem] = [] + // Indexes of splits here will be related to reversedChatItems, not chatModel.chatItems + var splitRanges: [SplitRange] = [] + var indexInParentItems = Dictionary<Int64, Int>() + var index = 0 + var unclosedSplitIndex: Int? = nil + var unclosedSplitIndexInParent: Int? = nil + var visibleItemIndexInParent = -1 + var unreadBefore = unreadCount - chatState.unreadAfterNewestLoaded + var lastRevealedIdsInMergedItems: BoxedValue<[Int64]>? = nil + var lastRangeInReversedForMergedItems: BoxedValue<ClosedRange<Int>>? = nil + var recent: MergedItem? = nil + while index < items.count { + let item = items[index] + let prev = index >= 1 ? items[index - 1] : nil + let next = index + 1 < items.count ? items[index + 1] : nil + let category = item.mergeCategory + let itemIsSplit = itemSplits.contains(item.id) + + if item.id == unreadAfterItemId { + unreadBefore = unreadCount - chatState.unreadAfter + } + if item.isRcvNew { + unreadBefore -= 1 + } + + let revealed = item.mergeCategory == nil || revealedItems.contains(item.id) + if recent != nil, case let .grouped(items, _, _, _, mergeCategory, unreadIds, _, _) = recent, mergeCategory == category, let first = items.boxedValue.first, !revealedItems.contains(first.item.id) && !itemIsSplit { + let listItem = ListItem(item: item, prevItem: prev, nextItem: next, unreadBefore: unreadBefore) + items.boxedValue.append(listItem) + + if item.isRcvNew { + unreadIds.boxedValue.insert(item.id) + } + if let lastRevealedIdsInMergedItems, let lastRangeInReversedForMergedItems { + if revealed { + lastRevealedIdsInMergedItems.boxedValue.append(item.id) + } + lastRangeInReversedForMergedItems.boxedValue = lastRangeInReversedForMergedItems.boxedValue.lowerBound ... index + } + } else { + visibleItemIndexInParent += 1 + let listItem = ListItem(item: item, prevItem: prev, nextItem: next, unreadBefore: unreadBefore) + if item.mergeCategory != nil { + if item.mergeCategory != prev?.mergeCategory || lastRevealedIdsInMergedItems == nil { + lastRevealedIdsInMergedItems = BoxedValue(revealedItems.contains(item.id) ? [item.id] : []) + } else if revealed, let lastRevealedIdsInMergedItems { + lastRevealedIdsInMergedItems.boxedValue.append(item.id) + } + lastRangeInReversedForMergedItems = BoxedValue(index ... index) + recent = MergedItem.grouped( + items: BoxedValue([listItem]), + revealed: revealed, + revealedIdsWithinGroup: lastRevealedIdsInMergedItems!, + rangeInReversed: lastRangeInReversedForMergedItems!, + mergeCategory: item.mergeCategory, + unreadIds: BoxedValue(item.isRcvNew ? Set(arrayLiteral: item.id) : Set()), + startIndexInReversedItems: index, + hash: listItem.genHash(revealedItems.contains(prev?.id ?? -1), revealedItems.contains(next?.id ?? -1)) + ) + } else { + lastRangeInReversedForMergedItems = nil + recent = MergedItem.single( + item: listItem, + startIndexInReversedItems: index, + hash: listItem.genHash(revealedItems.contains(prev?.id ?? -1), revealedItems.contains(next?.id ?? -1)) + ) + } + mergedItems.append(recent!) + } + if itemIsSplit { + // found item that is considered as a split + if let unclosedSplitIndex, let unclosedSplitIndexInParent { + // it was at least second split in the list + splitRanges.append(SplitRange(itemId: items[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index - 1, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent - 1)) + } + unclosedSplitIndex = index + unclosedSplitIndexInParent = visibleItemIndexInParent + } else if index + 1 == items.count, let unclosedSplitIndex, let unclosedSplitIndexInParent { + // just one split for the whole list, there will be no more, it's the end + splitRanges.append(SplitRange(itemId: items[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent)) + } + indexInParentItems[item.id] = visibleItemIndexInParent + index += 1 + } + return MergedItems( + items: mergedItems, + splits: splitRanges, + indexInParentItems: indexInParentItems + ) + } + + // Use this check to ensure that mergedItems state based on currently actual state of global + // splits and reversedChatItems + func isActualState() -> Bool { + let im = ItemsModel.shared + // do not load anything if global splits state is different than in merged items because it + // will produce undefined results in terms of loading and placement of items. + // Same applies to reversedChatItems + return indexInParentItems.count == im.reversedChatItems.count && + splits.count == im.chatState.splits.count && + // that's just an optimization because most of the time only 1 split exists + ((splits.count == 1 && splits[0].itemId == im.chatState.splits[0]) || splits.map({ split in split.itemId }).sorted() == im.chatState.splits.sorted()) + } +} + + +enum MergedItem: Identifiable, Hashable, Equatable { + // equatable and hashable implementations allows to see the difference and correctly scroll to items we want + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.hash == rhs.hash + } + + var id: Int64 { newest().item.id } + + func hash(into hasher: inout Hasher) { + hasher.combine(hash) + } + + var hash: String { + switch self { + case .single(_, _, let hash): hash + " 1" + case .grouped(let items, _, _, _, _, _, _, let hash): hash + " \(items.boxedValue.count)" + } + } + + // the item that is always single, cannot be grouped and always revealed + case single( + item: ListItem, + startIndexInReversedItems: Int, + hash: String + ) + + /** The item that can contain multiple items or just one depending on revealed state. When the whole group of merged items is revealed, + * there will be multiple [Grouped] items with revealed flag set to true. When the whole group is collapsed, it will be just one instance + * of [Grouped] item with all grouped items inside [items]. In other words, number of [MergedItem] will always be equal to number of + * visible items in ChatView's EndlessScrollView */ + case grouped ( + items: BoxedValue<[ListItem]>, + revealed: Bool, + // it stores ids for all consecutive revealed items from the same group in order to hide them all on user's action + // it's the same list instance for all Grouped items within revealed group + /** @see reveal */ + revealedIdsWithinGroup: BoxedValue<[Int64]>, + rangeInReversed: BoxedValue<ClosedRange<Int>>, + mergeCategory: CIMergeCategory?, + unreadIds: BoxedValue<Set<Int64>>, + startIndexInReversedItems: Int, + hash: String + ) + + func revealItems(_ reveal: Bool, _ revealedItems: Binding<Set<Int64>>) { + if case .grouped(let items, _, let revealedIdsWithinGroup, _, _, _, _, _) = self { + var newRevealed = revealedItems.wrappedValue + var i = 0 + if reveal { + while i < items.boxedValue.count { + newRevealed.insert(items.boxedValue[i].item.id) + i += 1 + } + } else { + while i < revealedIdsWithinGroup.boxedValue.count { + newRevealed.remove(revealedIdsWithinGroup.boxedValue[i]) + i += 1 + } + revealedIdsWithinGroup.boxedValue.removeAll() + } + revealedItems.wrappedValue = newRevealed + } + } + + var startIndexInReversedItems: Int { + get { + switch self { + case let .single(_, startIndexInReversedItems, _): startIndexInReversedItems + case let .grouped(_, _, _, _, _, _, startIndexInReversedItems, _): startIndexInReversedItems + } + } + } + + func hasUnread() -> Bool { + switch self { + case let .single(item, _, _): item.item.isRcvNew + case let .grouped(_, _, _, _, _, unreadIds, _, _): !unreadIds.boxedValue.isEmpty + } + } + + func newest() -> ListItem { + switch self { + case let .single(item, _, _): item + case let .grouped(items, _, _, _, _, _, _, _): items.boxedValue[0] + } + } + + func oldest() -> ListItem { + switch self { + case let .single(item, _, _): item + case let .grouped(items, _, _, _, _, _, _, _): items.boxedValue[items.boxedValue.count - 1] + } + } + + func lastIndexInReversed() -> Int { + switch self { + case .single: startIndexInReversedItems + case let .grouped(items, _, _, _, _, _, _, _): startIndexInReversedItems + items.boxedValue.count - 1 + } + } +} + +struct SplitRange { + let itemId: Int64 + /** range of indexes inside reversedChatItems where the first element is the split (it's index is [indexRangeInReversed.first]) + * so [0, 1, 2, -100-, 101] if the 3 is a split, SplitRange(indexRange = 3 .. 4) will be this SplitRange instance + * (3, 4 indexes of the splitRange with the split itself at index 3) + * */ + let indexRangeInReversed: ClosedRange<Int> + /** range of indexes inside LazyColumn where the first element is the split (it's index is [indexRangeInParentItems.first]) */ + let indexRangeInParentItems: ClosedRange<Int> +} + +struct ListItem: Hashable { + let item: ChatItem + let prevItem: ChatItem? + let nextItem: ChatItem? + // how many unread items before (older than) this one (excluding this one) + let unreadBefore: Int + + private func chatDirHash(_ chatDir: CIDirection?) -> Int { + guard let chatDir else { return 0 } + return switch chatDir { + case .directSnd: 0 + case .directRcv: 1 + case .groupSnd: 2 + case let .groupRcv(mem): "\(mem.groupMemberId) \(mem.displayName) \(mem.memberStatus.rawValue) \(mem.memberRole.rawValue) \(mem.image?.hash ?? 0)".hash + case .localSnd: 4 + case .localRcv: 5 + } + } + + // using meta.hashValue instead of parts takes much more time so better to use partial meta here + func genHash(_ prevRevealed: Bool, _ nextRevealed: Bool) -> String { + "\(item.meta.itemId) \(item.meta.updatedAt.hashValue) \(item.meta.itemEdited) \(item.meta.itemDeleted?.hashValue ?? 0) \(item.meta.itemTimed?.hashValue ?? 0) \(item.meta.itemStatus.hashValue) \(item.meta.sentViaProxy ?? false) \(item.mergeCategory?.hashValue ?? 0) \(chatDirHash(item.chatDir)) \(item.reactions.hashValue) \(item.meta.isRcvNew) \(item.text.hash) \(item.file?.hashValue ?? 0) \(item.quotedItem?.itemId ?? 0) \(unreadBefore) \(prevItem?.id ?? 0) \(chatDirHash(prevItem?.chatDir)) \(prevItem?.mergeCategory?.hashValue ?? 0) \(prevRevealed) \(nextItem?.id ?? 0) \(chatDirHash(nextItem?.chatDir)) \(nextItem?.mergeCategory?.hashValue ?? 0) \(nextRevealed)" + } +} + +class ActiveChatState { + var splits: [Int64] = [] + var unreadAfterItemId: Int64 = -1 + // total items after unread after item (exclusive) + var totalAfter: Int = 0 + var unreadTotal: Int = 0 + // exclusive + var unreadAfter: Int = 0 + // exclusive + var unreadAfterNewestLoaded: Int = 0 + + func moveUnreadAfterItem(_ toItemId: Int64?, _ nonReversedItems: [ChatItem]) { + guard let toItemId else { return } + let currentIndex = nonReversedItems.firstIndex(where: { $0.id == unreadAfterItemId }) + let newIndex = nonReversedItems.firstIndex(where: { $0.id == toItemId }) + guard let currentIndex, let newIndex else { + return + } + unreadAfterItemId = toItemId + let unreadDiff = newIndex > currentIndex + ? -nonReversedItems[currentIndex + 1..<newIndex + 1].filter { $0.isRcvNew }.count + : nonReversedItems[newIndex + 1..<currentIndex + 1].filter { $0.isRcvNew }.count + unreadAfter += unreadDiff + } + + func moveUnreadAfterItem(_ fromIndex: Int, _ toIndex: Int, _ nonReversedItems: [ChatItem]) { + if fromIndex == -1 || toIndex == -1 { + return + } + unreadAfterItemId = nonReversedItems[toIndex].id + let unreadDiff = toIndex > fromIndex + ? -nonReversedItems[fromIndex + 1..<toIndex + 1].filter { $0.isRcvNew }.count + : nonReversedItems[toIndex + 1..<fromIndex + 1].filter { $0.isRcvNew }.count + unreadAfter += unreadDiff + } + + func clear() { + splits = [] + unreadAfterItemId = -1 + totalAfter = 0 + unreadTotal = 0 + unreadAfter = 0 + unreadAfterNewestLoaded = 0 + } + + func itemsRead(_ itemIds: Set<Int64>?, _ newItems: [ChatItem]) { + guard let itemIds else { + // special case when the whole chat became read + unreadTotal = 0 + unreadAfter = 0 + return + } + var unreadAfterItemIndex: Int = -1 + // since it's more often that the newest items become read, it's logical to loop from the end of the list to finish it faster + var i = newItems.count - 1 + var ids = itemIds + // intermediate variables to prevent re-setting state value a lot of times without reason + var newUnreadTotal = unreadTotal + var newUnreadAfter = unreadAfter + while i >= 0 { + let item = newItems[i] + if item.id == unreadAfterItemId { + unreadAfterItemIndex = i + } + if ids.contains(item.id) { + // was unread, now this item is read + if (unreadAfterItemIndex == -1) { + newUnreadAfter -= 1 + } + newUnreadTotal -= 1 + ids.remove(item.id) + if ids.isEmpty { + break + } + } + i -= 1 + } + unreadTotal = newUnreadTotal + unreadAfter = newUnreadAfter + } + + func itemAdded(_ item: (Int64, Bool), _ index: Int) { + if item.1 { + unreadAfter += 1 + unreadTotal += 1 + } + } + + func itemsRemoved(_ itemIds: [(Int64, Int, Bool)], _ newItems: [ChatItem]) { + var newSplits: [Int64] = [] + for split in splits { + let index = itemIds.firstIndex(where: { (delId, _, _) in delId == split }) + // deleted the item that was right before the split between items, find newer item so it will act like the split + if let index { + let idx = itemIds[index].1 - itemIds.filter { (_, delIndex, _) in delIndex <= index }.count + let newSplit = newItems.count > idx && idx >= 0 ? newItems[idx].id : nil + // it the whole section is gone and splits overlap, don't add it at all + if let newSplit, !newSplits.contains(newSplit) { + newSplits.append(newSplit) + } + } else { + newSplits.append(split) + } + } + splits = newSplits + + let index = itemIds.firstIndex(where: { (delId, _, _) in delId == unreadAfterItemId }) + // unread after item was removed + if let index { + let idx = itemIds[index].1 - itemIds.filter { (_, delIndex, _) in delIndex <= index }.count + var newUnreadAfterItemId = newItems.count > idx && idx >= 0 ? newItems[idx].id : nil + let newUnreadAfterItemWasNull = newUnreadAfterItemId == nil + if newUnreadAfterItemId == nil { + // everything on top (including unread after item) were deleted, take top item as unread after id + newUnreadAfterItemId = newItems.first?.id + } + if let newUnreadAfterItemId { + unreadAfterItemId = newUnreadAfterItemId + totalAfter -= itemIds.filter { (_, delIndex, _) in delIndex > index }.count + unreadTotal -= itemIds.filter { (_, delIndex, isRcvNew) in delIndex <= index && isRcvNew }.count + unreadAfter -= itemIds.filter { (_, delIndex, isRcvNew) in delIndex > index && isRcvNew }.count + if newUnreadAfterItemWasNull { + // since the unread after item was moved one item after initial position, adjust counters accordingly + if newItems.first?.isRcvNew == true { + unreadTotal += 1 + unreadAfter -= 1 + } + } + } else { + // all items were deleted, 0 items in chatItems + unreadAfterItemId = -1 + totalAfter = 0 + unreadTotal = 0 + unreadAfter = 0 + } + } else { + totalAfter -= itemIds.count + } + } +} + +class BoxedValue<T: Hashable>: Equatable, Hashable { + static func == (lhs: BoxedValue<T>, rhs: BoxedValue<T>) -> Bool { + lhs.boxedValue == rhs.boxedValue + } + + func hash(into hasher: inout Hasher) { + hasher.combine("\(self)") + } + + var boxedValue : T + init(_ value: T) { + self.boxedValue = value + } +} + +@MainActor +func visibleItemIndexesNonReversed(_ listState: EndlessScrollView<MergedItem>.ListState, _ mergedItems: MergedItems) -> ClosedRange<Int> { + let zero = 0 ... 0 + let items = mergedItems.items + if items.isEmpty { + return zero + } + let newest = items.count > listState.firstVisibleItemIndex ? items[listState.firstVisibleItemIndex].startIndexInReversedItems : nil + let oldest = items.count > listState.lastVisibleItemIndex ? items[listState.lastVisibleItemIndex].lastIndexInReversed() : nil + guard let newest, let oldest else { + return zero + } + let size = ItemsModel.shared.reversedChatItems.count + let range = size - oldest ... size - newest + if range.lowerBound < 0 || range.upperBound < 0 { + return zero + } + + // visible items mapped to their underlying data structure which is ItemsModel.shared.reversedChatItems.reversed() + return range +} diff --git a/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift b/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift new file mode 100644 index 0000000000..c1a1eec7d2 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift @@ -0,0 +1,185 @@ +// +// ChatScrollHelpers.swift +// SimpleX (iOS) +// +// Created by Stanislav Dmitrenko on 20.12.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +func loadLastItems(_ loadingMoreItems: Binding<Bool>, loadingBottomItems: Binding<Bool>, _ chat: Chat) async { + await MainActor.run { + loadingMoreItems.wrappedValue = true + loadingBottomItems.wrappedValue = true + } + try? await Task.sleep(nanoseconds: 500_000000) + if ChatModel.shared.chatId != chat.chatInfo.id { + await MainActor.run { + loadingMoreItems.wrappedValue = false + loadingBottomItems.wrappedValue = false + } + return + } + await apiLoadMessages(chat.chatInfo.id, ChatPagination.last(count: 50), ItemsModel.shared.chatState) + await MainActor.run { + loadingMoreItems.wrappedValue = false + loadingBottomItems.wrappedValue = false + } +} + +class PreloadState { + static let shared = PreloadState() + var prevFirstVisible: Int64 = Int64.min + var prevItemsCount: Int = 0 + var preloading: Bool = false + + func clear() { + prevFirstVisible = Int64.min + prevItemsCount = 0 + preloading = false + } +} + +func preloadIfNeeded( + _ allowLoadMoreItems: Binding<Bool>, + _ ignoreLoadingRequests: Binding<Int64?>, + _ listState: EndlessScrollView<MergedItem>.ListState, + _ mergedItems: BoxedValue<MergedItems>, + loadItems: @escaping (Bool, ChatPagination) async -> Bool, + loadLastItems: @escaping () async -> Void +) { + let state = PreloadState.shared + guard !listState.isScrolling && !listState.isAnimatedScrolling, + !state.preloading, + listState.totalItemsCount > 0 + else { + return + } + if state.prevFirstVisible != listState.firstVisibleItemId as! Int64 || state.prevItemsCount != mergedItems.boxedValue.indexInParentItems.count { + state.preloading = true + let allowLoadMore = allowLoadMoreItems.wrappedValue + Task { + defer { state.preloading = false } + var triedToLoad = true + await preloadItems(mergedItems.boxedValue, allowLoadMore, listState, ignoreLoadingRequests) { pagination in + triedToLoad = await loadItems(false, pagination) + return triedToLoad + } + if triedToLoad { + state.prevFirstVisible = listState.firstVisibleItemId as! Int64 + state.prevItemsCount = mergedItems.boxedValue.indexInParentItems.count + } + // it's important to ask last items when the view is fully covered with items. Otherwise, visible items from one + // split will be merged with last items and position of scroll will change unexpectedly. + if listState.itemsCanCoverScreen && !ItemsModel.shared.lastItemsLoaded { + await loadLastItems() + } + } + } else if listState.itemsCanCoverScreen && !ItemsModel.shared.lastItemsLoaded { + state.preloading = true + Task { + defer { state.preloading = false } + await loadLastItems() + } + } +} + +func preloadItems( + _ mergedItems: MergedItems, + _ allowLoadMoreItems: Bool, + _ listState: EndlessScrollView<MergedItem>.ListState, + _ ignoreLoadingRequests: Binding<Int64?>, + _ loadItems: @escaping (ChatPagination) async -> Bool) +async { + let allowLoad = allowLoadMoreItems || mergedItems.items.count == listState.lastVisibleItemIndex + 1 + let remaining = ChatPagination.UNTIL_PRELOAD_COUNT + let firstVisibleIndex = listState.firstVisibleItemIndex + + if !(await preloadItemsBefore()) { + await preloadItemsAfter() + } + + func preloadItemsBefore() async -> Bool { + let splits = mergedItems.splits + let lastVisibleIndex = listState.lastVisibleItemIndex + var lastIndexToLoadFrom: Int? = findLastIndexToLoadFromInSplits(firstVisibleIndex, lastVisibleIndex, remaining, splits) + let items: [ChatItem] = ItemsModel.shared.reversedChatItems.reversed() + if splits.isEmpty && !items.isEmpty && lastVisibleIndex > mergedItems.items.count - remaining { + lastIndexToLoadFrom = items.count - 1 + } + let loadFromItemId: Int64? + if allowLoad, let lastIndexToLoadFrom { + let index = items.count - 1 - lastIndexToLoadFrom + loadFromItemId = index >= 0 ? items[index].id : nil + } else { + loadFromItemId = nil + } + guard let loadFromItemId, ignoreLoadingRequests.wrappedValue != loadFromItemId else { + return false + } + let sizeWas = items.count + let firstItemIdWas = items.first?.id + let triedToLoad = await loadItems(ChatPagination.before(chatItemId: loadFromItemId, count: ChatPagination.PRELOAD_COUNT)) + if triedToLoad && sizeWas == ItemsModel.shared.reversedChatItems.count && firstItemIdWas == ItemsModel.shared.reversedChatItems.last?.id { + ignoreLoadingRequests.wrappedValue = loadFromItemId + return false + } + return triedToLoad + } + + func preloadItemsAfter() async { + let splits = mergedItems.splits + let split = splits.last(where: { $0.indexRangeInParentItems.contains(firstVisibleIndex) }) + // we're inside a splitRange (top --- [end of the splitRange --- we're here --- start of the splitRange] --- bottom) + let reversedItems: [ChatItem] = ItemsModel.shared.reversedChatItems + if let split, split.indexRangeInParentItems.lowerBound + remaining > firstVisibleIndex { + let index = split.indexRangeInReversed.lowerBound + if index >= 0 { + let loadFromItemId = reversedItems[index].id + _ = await loadItems(ChatPagination.after(chatItemId: loadFromItemId, count: ChatPagination.PRELOAD_COUNT)) + } + } + } +} + +func oldestPartiallyVisibleListItemInListStateOrNull(_ listState: EndlessScrollView<MergedItem>.ListState) -> ListItem? { + if listState.lastVisibleItemIndex < listState.items.count { + return listState.items[listState.lastVisibleItemIndex].oldest() + } else { + return listState.items.last?.oldest() + } +} + +private func findLastIndexToLoadFromInSplits(_ firstVisibleIndex: Int, _ lastVisibleIndex: Int, _ remaining: Int, _ splits: [SplitRange]) -> Int? { + for split in splits { + // before any split + if split.indexRangeInParentItems.lowerBound > firstVisibleIndex { + if lastVisibleIndex > (split.indexRangeInParentItems.lowerBound - remaining) { + return split.indexRangeInReversed.lowerBound - 1 + } + break + } + let containsInRange = split.indexRangeInParentItems.contains(firstVisibleIndex) + if containsInRange { + if lastVisibleIndex > (split.indexRangeInParentItems.upperBound - remaining) { + return split.indexRangeInReversed.upperBound + } + break + } + } + return nil +} + +/// Disable animation on iOS 15 +func withConditionalAnimation<Result>( + _ animation: Animation? = .default, + _ body: () throws -> Result +) rethrows -> Result { + if #available(iOS 16.0, *) { + try withAnimation(animation, body) + } else { + try body() + } +} diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 6e2c0c1555..c136ebc01b 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -9,43 +9,63 @@ import SwiftUI import SimpleXChat import SwiftyGif +import Combine private let memberImageSize: CGFloat = 34 struct ChatView: View { @EnvironmentObject var chatModel: ChatModel - @Environment(\.colorScheme) var colorScheme + @ObservedObject var im = ItemsModel.shared + @State var mergedItems: BoxedValue<MergedItems> = BoxedValue(MergedItems.create(ItemsModel.shared.reversedChatItems, [], ItemsModel.shared.chatState)) + @State var revealedItems: Set<Int64> = Set() + @State var theme: AppTheme = buildTheme() @Environment(\.dismiss) var dismiss + @Environment(\.colorScheme) var colorScheme @Environment(\.presentationMode) var presentationMode + @Environment(\.scenePhase) var scenePhase @State @ObservedObject var chat: Chat @State private var showChatInfoSheet: Bool = false @State private var showAddMembersSheet: Bool = false @State private var composeState = ComposeState() + @State private var selectedRange = NSRange() @State private var keyboardVisible = false + @State private var keyboardHiddenDate = Date.now @State private var connectionStats: ConnectionStats? @State private var customUserProfile: Profile? @State private var connectionCode: String? - @State private var tableView: UITableView? - @State private var loadingItems = false - @State private var firstPage = false - @State private var itemsInView: Set<String> = [] - @State private var scrollProxy: ScrollViewProxy? - @State private var searchMode = false + @State private var loadingMoreItems = false + @State private var loadingTopItems = false + @State private var requestedTopScroll = false + @State private var loadingBottomItems = false + @State private var requestedBottomScroll = false + @State private var showSearch = false @State private var searchText: String = "" @FocusState private var searchFocussed // opening GroupMemberInfoView on member icon - @State private var membersLoaded = false @State private var selectedMember: GMember? = nil // opening GroupLinkView on link button (incognito) @State private var showGroupLinkSheet: Bool = false - @State private var groupLink: String? + @State private var groupLink: CreatedConnLink? @State private var groupLinkMemberRole: GroupMemberRole = .member + @State private var forwardedChatItems: [ChatItem] = [] + @State private var selectedChatItems: Set<Int64>? = nil + @State private var showDeleteSelectedMessages: Bool = false + @State private var showArchiveSelectedReports: Bool = false + @State private var allowToDeleteSelectedMessagesForAll: Bool = false + @State private var allowLoadMoreItems: Bool = false + @State private var ignoreLoadingRequests: Int64? = nil + @State private var animatedScrollingInProgress: Bool = false + @State private var floatingButtonModel: FloatingButtonModel = FloatingButtonModel() + + @State private var scrollView: EndlessScrollView<MergedItem> = EndlessScrollView(frame: .zero) + + @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial var body: some View { if #available(iOS 16.0, *) { viewBody - .scrollDismissesKeyboard(.immediately) - .keyboardPadding() + .scrollDismissesKeyboard(.immediately) + .toolbarBackground(.hidden, for: .navigationBar) } else { viewBody } @@ -53,93 +73,264 @@ struct ChatView: View { private var viewBody: some View { let cInfo = chat.chatInfo - return VStack(spacing: 0) { - if searchMode { - searchToolbar() - Divider() + return ZStack { + let wallpaperImage = theme.wallpaper.type.image + let wallpaperType = theme.wallpaper.type + let backgroundColor = theme.wallpaper.background ?? wallpaperType.defaultBackgroundColor(theme.base, theme.colors.background) + let tintColor = theme.wallpaper.tint ?? wallpaperType.defaultTintColor(theme.base) + Color.clear.ignoresSafeArea(.all) + .if(wallpaperImage != nil) { view in + view.modifier( + ChatViewBackground(image: wallpaperImage!, imageType: wallpaperType, background: backgroundColor, tint: tintColor) + ) } - ZStack(alignment: .trailing) { - chatItemsList() - if let proxy = scrollProxy { - floatingButtons(proxy) + VStack(spacing: 0) { + ZStack(alignment: .bottomTrailing) { + chatItemsList() + if let groupInfo = chat.chatInfo.groupInfo, !composeState.message.isEmpty { + GroupMentionsView(groupInfo: groupInfo, composeState: $composeState, selectedRange: $selectedRange, keyboardVisible: $keyboardVisible) + } + FloatingButtons(theme: theme, scrollView: scrollView, chat: chat, loadingMoreItems: $loadingMoreItems, loadingTopItems: $loadingTopItems, requestedTopScroll: $requestedTopScroll, loadingBottomItems: $loadingBottomItems, requestedBottomScroll: $requestedBottomScroll, animatedScrollingInProgress: $animatedScrollingInProgress, listState: scrollView.listState, model: floatingButtonModel, reloadItems: { + mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState) + scrollView.updateItems(mergedItems.boxedValue.items) + } + ) + } + connectingText() + if selectedChatItems == nil { + let reason = chat.chatInfo.userCantSendReason + ComposeView( + chat: chat, + composeState: $composeState, + keyboardVisible: $keyboardVisible, + keyboardHiddenDate: $keyboardHiddenDate, + selectedRange: $selectedRange, + disabledText: reason?.composeLabel + ) + .disabled(!cInfo.sendMsgEnabled) + .if(!cInfo.sendMsgEnabled) { v in + v.disabled(true).onTapGesture { + AlertManager.shared.showAlertMsg( + title: "You can't send messages!", + message: reason?.alertMessage + ) + } + } + } else { + SelectedItemsBottomToolbar( + chatItems: ItemsModel.shared.reversedChatItems, + selectedChatItems: $selectedChatItems, + chatInfo: chat.chatInfo, + deleteItems: { forAll in + allowToDeleteSelectedMessagesForAll = forAll + showDeleteSelectedMessages = true + }, + archiveItems: { + showArchiveSelectedReports = true + }, + moderateItems: { + if case let .group(groupInfo) = chat.chatInfo { + showModerateSelectedMessagesAlert(groupInfo) + } + }, + forwardItems: forwardSelectedMessages + ) } } - - Spacer(minLength: 0) - - connectingText() - ComposeView( - chat: chat, - composeState: $composeState, - keyboardVisible: $keyboardVisible - ) - .disabled(!cInfo.sendMsgEnabled) + if im.showLoadingProgress == chat.id { + ProgressView().scaleEffect(2) + } + } + .safeAreaInset(edge: .top) { + VStack(spacing: .zero) { + if showSearch { searchToolbar() } + Divider() + } + .background(ToolbarMaterial.material(toolbarMaterial)) } - .padding(.top, 1) .navigationTitle(cInfo.chatViewName) + .background(theme.colors.background) .navigationBarTitleDisplayMode(.inline) + .environmentObject(theme) + .confirmationDialog(selectedChatItems?.count == 1 ? "Delete message?" : "Delete \((selectedChatItems?.count ?? 0)) messages?", isPresented: $showDeleteSelectedMessages, titleVisibility: .visible) { + Button("Delete for me", role: .destructive) { + if let selected = selectedChatItems { + deleteMessages(chat, selected.sorted(), .cidmInternal, moderate: false, deletedSelectedMessages) } + } + if allowToDeleteSelectedMessagesForAll { + Button(broadcastDeleteButtonText(chat), role: .destructive) { + if let selected = selectedChatItems { + allowToDeleteSelectedMessagesForAll = false + deleteMessages(chat, selected.sorted(), .cidmBroadcast, moderate: false, deletedSelectedMessages) + } + } + } + } + .confirmationDialog(selectedChatItems?.count == 1 ? "Archive report?" : "Archive \((selectedChatItems?.count ?? 0)) reports?", isPresented: $showArchiveSelectedReports, titleVisibility: .visible) { + Button("For me", role: .destructive) { + if let selected = selectedChatItems { + archiveReports(chat.chatInfo, selected.sorted(), false, deletedSelectedMessages) + } + } + if case let ChatInfo.group(groupInfo) = chat.chatInfo, groupInfo.membership.memberActive { + Button("For all moderators", role: .destructive) { + if let selected = selectedChatItems { + archiveReports(chat.chatInfo, selected.sorted(), true, deletedSelectedMessages) + } + } + } + } + .appSheet(item: $selectedMember) { member in + Group { + if case let .group(groupInfo) = chat.chatInfo { + GroupMemberInfoView( + groupInfo: groupInfo, + chat: chat, + groupMember: member, + navigation: true + ) + } + } + } + // it should be presented on top level in order to prevent a bug in SwiftUI on iOS 16 related to .focused() modifier in AddGroupMembersView's search field + .appSheet(isPresented: $showAddMembersSheet) { + Group { + if case let .group(groupInfo) = cInfo { + AddGroupMembersView(chat: chat, groupInfo: groupInfo) + } + } + } + .sheet(isPresented: Binding( + get: { !forwardedChatItems.isEmpty }, + set: { isPresented in + if !isPresented { + forwardedChatItems = [] + selectedChatItems = nil + } + } + )) { + if #available(iOS 16.0, *) { + ChatItemForwardingView(chatItems: forwardedChatItems, fromChatInfo: chat.chatInfo, composeState: $composeState) + .presentationDetents([.fraction(0.8)]) + } else { + ChatItemForwardingView(chatItems: forwardedChatItems, fromChatInfo: chat.chatInfo, composeState: $composeState) + } + } .onAppear { + scrollView.listState.onUpdateListener = onChatItemsUpdated + selectedChatItems = nil + revealedItems = Set() initChatView() + if im.isLoading { + Task { + try? await Task.sleep(nanoseconds: 500_000000) + await MainActor.run { + if im.isLoading { + im.showLoadingProgress = chat.id + } + } + } + } } .onChange(of: chatModel.chatId) { cId in - if cId != nil { + showChatInfoSheet = false + selectedChatItems = nil + revealedItems = Set() + stopAudioPlayer() + if let cId { + if let c = chatModel.getChat(cId) { + chat = c + } + scrollView.listState.onUpdateListener = onChatItemsUpdated initChatView() + theme = buildTheme() + closeSearch() + mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState) + scrollView.updateItems(mergedItems.boxedValue.items) + + if let openAround = chatModel.openAroundItemId, let index = mergedItems.boxedValue.indexInParentItems[openAround] { + scrollView.scrollToItem(index) + } else if let unreadIndex = mergedItems.boxedValue.items.lastIndex(where: { $0.hasUnread() }) { + scrollView.scrollToItem(unreadIndex) + } else { + scrollView.scrollToBottom() + } + if chatModel.openAroundItemId != nil { + chatModel.openAroundItemId = nil + } } else { dismiss() } } - .onDisappear { - VideoPlayerView.players.removeAll() - if chatModel.chatId == cInfo.id && !presentationMode.wrappedValue.isPresented { - chatModel.chatId = nil - DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { - if chatModel.chatId == nil { - chatModel.chatItemStatuses = [:] - chatModel.reversedChatItems = [] - chatModel.groupMembers = [] - membersLoaded = false + .onChange(of: chatModel.openAroundItemId) { openAround in + if let openAround { + closeSearch() + mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState) + scrollView.updateItems(mergedItems.boxedValue.items) + chatModel.openAroundItemId = nil + + if let index = mergedItems.boxedValue.indexInParentItems[openAround] { + scrollView.scrollToItem(index) + } + + // this may already being loading because of changed chat id (see .onChange(of: chat.id) + if !loadingBottomItems { + allowLoadMoreItems = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + allowLoadMoreItems = true } } } } + .onDisappear { + VideoPlayerView.players.removeAll() + stopAudioPlayer() + if chatModel.chatId == cInfo.id && !presentationMode.wrappedValue.isPresented { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { + if chatModel.chatId == nil { + chatModel.chatItemStatuses = [:] + ItemsModel.shared.reversedChatItems = [] + ItemsModel.shared.chatState.clear() + chatModel.groupMembers = [] + chatModel.groupMembersIndexes.removeAll() + chatModel.membersLoaded = false + } + } + } + } + .onChange(of: colorScheme) { _ in + theme = buildTheme() + } .toolbar { ToolbarItem(placement: .principal) { - if case let .direct(contact) = cInfo { + if selectedChatItems != nil { + SelectedItemsTopToolbar(selectedChatItems: $selectedChatItems) + } else if case let .direct(contact) = cInfo { Button { Task { - do { - let (stats, profile) = try await apiContactInfo(chat.chatInfo.apiId) - let (ct, code) = try await apiGetContactCode(chat.chatInfo.apiId) - await MainActor.run { - connectionStats = stats - customUserProfile = profile - connectionCode = code - if contact.activeConn?.connectionCode != ct.activeConn?.connectionCode { - chat.chatInfo = .direct(contact: ct) - } - } - } catch let error { - logger.error("apiContactInfo or apiGetContactCode error: \(responseError(error))") - } - await MainActor.run { showChatInfoSheet = true } + showChatInfoSheet = true } } label: { ChatInfoToolbar(chat: chat) } - .sheet(isPresented: $showChatInfoSheet, onDismiss: { - connectionStats = nil - customUserProfile = nil - connectionCode = nil - }) { - ChatInfoView(chat: chat, contact: contact, connectionStats: $connectionStats, customUserProfile: $customUserProfile, localAlias: chat.chatInfo.localAlias, connectionCode: $connectionCode) + .appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) { + ChatInfoView( + chat: chat, + contact: contact, + localAlias: chat.chatInfo.localAlias, + featuresAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences), + currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences), + onSearch: { focusSearch() } + ) } } else if case let .group(groupInfo) = cInfo { Button { - Task { await loadGroupMembers(groupInfo) { showChatInfoSheet = true } } + Task { await chatModel.loadGroupMembers(groupInfo) { showChatInfoSheet = true } } } label: { ChatInfoToolbar(chat: chat) + .tint(theme.colors.primary) } - .appSheet(isPresented: $showChatInfoSheet) { + .appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) { GroupChatInfoView( chat: chat, groupInfo: Binding( @@ -148,84 +339,93 @@ struct ChatView: View { chat.chatInfo = .group(groupInfo: gInfo) chat.created = Date.now } - ) + ), + onSearch: { focusSearch() }, + localAlias: groupInfo.localAlias ) } + } else if case .local = cInfo { + ChatInfoToolbar(chat: chat) } } ToolbarItem(placement: .navigationBarTrailing) { - switch cInfo { - case let .direct(contact): - HStack { - if contact.allowsFeature(.calls) { - callButton(contact, .audio, imageName: "phone") - .disabled(!contact.ready || !contact.active) + if selectedChatItems != nil { + Button { + withAnimation { + selectedChatItems = nil } - Menu { - if contact.allowsFeature(.calls) { - Button { - CallController.shared.startCall(contact, .video) - } label: { - Label("Video call", systemImage: "video") + } label: { + Text("Cancel") + } + } else { + switch cInfo { + case let .direct(contact): + HStack { + let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser + if callsPrefEnabled { + if chatModel.activeCall == nil { + callButton(contact, .audio, imageName: "phone") + .disabled(!contact.ready || !contact.active) + } else if let call = chatModel.activeCall, call.contact.id == cInfo.id { + endCallButton(call) } - .disabled(!contact.ready || !contact.active) } - searchButton() - toggleNtfsButton(chat) - .disabled(!contact.ready || !contact.active) - } label: { - Image(systemName: "ellipsis") - } - } - case let .group(groupInfo): - HStack { - if groupInfo.canAddMembers { - if (chat.chatInfo.incognito) { - groupLinkButton() - .appSheet(isPresented: $showGroupLinkSheet) { - GroupLinkView( - groupId: groupInfo.groupId, - groupLink: $groupLink, - groupLinkMemberRole: $groupLinkMemberRole, - showTitle: true, - creatingGroup: false - ) - } - } else { - addMembersButton() - .appSheet(isPresented: $showAddMembersSheet) { - AddGroupMembersView(chat: chat, groupInfo: groupInfo) + Menu { + if callsPrefEnabled && chatModel.activeCall == nil { + Button { + CallController.shared.startCall(contact, .video) + } label: { + Label("Video call", systemImage: "video") } + .disabled(!contact.ready || !contact.active) + } + searchButton() + ToggleNtfsButton(chat: chat) + .disabled(!contact.ready || !contact.active) + } label: { + Image(systemName: "ellipsis") } } - Menu { - searchButton() - toggleNtfsButton(chat) - } label: { - Image(systemName: "ellipsis") + case let .group(groupInfo): + HStack { + if groupInfo.canAddMembers { + if (chat.chatInfo.incognito) { + groupLinkButton() + .appSheet(isPresented: $showGroupLinkSheet) { + GroupLinkView( + groupId: groupInfo.groupId, + groupLink: $groupLink, + groupLinkMemberRole: $groupLinkMemberRole, + showTitle: true, + creatingGroup: false + ) + } + } else { + addMembersButton() + } + } + Menu { + searchButton() + ToggleNtfsButton(chat: chat) + } label: { + Image(systemName: "ellipsis") + } } + case .local: + searchButton() + default: + EmptyView() } - default: - EmptyView() } } } } - private func loadGroupMembers(_ groupInfo: GroupInfo, updateView: @escaping () -> Void = {}) async { - let groupMembers = await apiListMembers(groupInfo.groupId) - await MainActor.run { - if chatModel.chatId == groupInfo.id { - chatModel.groupMembers = groupMembers.map { GMember.init($0) } - membersLoaded = true - updateView() - } - } - } - private func initChatView() { let cInfo = chat.chatInfo - if case let .direct(contact) = cInfo { + // This check prevents the call to apiContactInfo after the app is suspended, and the database is closed. + if case .active = scenePhase, + case let .direct(contact) = cInfo { Task { do { let (stats, _) = try await apiContactInfo(chat.chatInfo.apiId) @@ -239,7 +439,8 @@ struct ChatView: View { } } } - if chatModel.draftChatId == cInfo.id, let draft = chatModel.draft { + if chatModel.draftChatId == cInfo.id && !composeState.forwarding, + let draft = chatModel.draft { composeState = draft } if chat.chatStats.unreadChat { @@ -247,15 +448,49 @@ struct ChatView: View { await markChatUnread(chat, unreadChat: false) } } + floatingButtonModel.updateOnListChange(scrollView.listState) + } + + private func scrollToItemId(_ itemId: ChatItem.ID) { + Task { + do { + var index = mergedItems.boxedValue.indexInParentItems[itemId] + if index == nil { + let pagination = ChatPagination.around(chatItemId: itemId, count: ChatPagination.PRELOAD_COUNT * 2) + let oldSize = ItemsModel.shared.reversedChatItems.count + let triedToLoad = await loadChatItems(chat, pagination) + if !triedToLoad { + return + } + var repeatsLeft = 50 + while oldSize == ItemsModel.shared.reversedChatItems.count && repeatsLeft > 0 { + try await Task.sleep(nanoseconds: 20_000000) + repeatsLeft -= 1 + } + index = mergedItems.boxedValue.indexInParentItems[itemId] + } + if let index { + closeKeyboardAndRun { + Task { + await MainActor.run { animatedScrollingInProgress = true } + await scrollView.scrollToItemAnimated(min(ItemsModel.shared.reversedChatItems.count - 1, index)) + await MainActor.run { animatedScrollingInProgress = false } + } + } + } + } catch { + logger.error("Error scrolling to item: \(error)") + } + } } private func searchToolbar() -> some View { - HStack { - HStack { + HStack(spacing: 12) { + HStack(spacing: 4) { Image(systemName: "magnifyingglass") TextField("Search", text: $searchText) .focused($searchFocussed) - .foregroundColor(.primary) + .foregroundColor(theme.colors.onBackground) .frame(maxWidth: .infinity) Button { @@ -264,152 +499,400 @@ struct ChatView: View { Image(systemName: "xmark.circle.fill").opacity(searchText == "" ? 0 : 1) } } - .padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6)) - .foregroundColor(.secondary) - .background(Color(.secondarySystemBackground)) + .padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7)) + .foregroundColor(theme.colors.secondary) + .background(Color(.tertiarySystemFill)) .cornerRadius(10.0) Button ("Cancel") { - searchText = "" - searchMode = false - searchFocussed = false - DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { - chatModel.reversedChatItems = [] - loadChat(chat: chat) - } + closeSearch() + searchTextChanged("") } } .padding(.horizontal) .padding(.vertical, 8) } - + private func voiceWithoutFrame(_ ci: ChatItem) -> Bool { - ci.content.msgContent?.isVoice == true && ci.content.text.count == 0 && ci.quotedItem == nil + ci.content.msgContent?.isVoice == true && ci.content.text.count == 0 && ci.quotedItem == nil && ci.meta.itemForwarded == nil + } + + private func filtered(_ reversedChatItems: Array<ChatItem>) -> Array<ChatItem> { + reversedChatItems + .enumerated() + .filter { (index, chatItem) in + if let mergeCategory = chatItem.mergeCategory, index > 0 { + mergeCategory != reversedChatItems[index - 1].mergeCategory + } else { + true + } + } + .map { $0.element } } private func chatItemsList() -> some View { let cInfo = chat.chatInfo return GeometryReader { g in - ScrollViewReader { proxy in - ScrollView { - LazyVStack(spacing: 0) { - ForEach(chatModel.reversedChatItems, id: \.viewId) { ci in - let voiceNoFrame = voiceWithoutFrame(ci) - let maxWidth = cInfo.chatType == .group - ? voiceNoFrame - ? (g.size.width - 28) - 42 - : (g.size.width - 28) * 0.84 - 42 - : voiceNoFrame - ? (g.size.width - 32) - : (g.size.width - 32) * 0.84 - chatItemView(ci, maxWidth) - .scaleEffect(x: 1, y: -1, anchor: .center) - .onAppear { - itemsInView.insert(ci.viewId) - loadChatItems(cInfo, ci, proxy) - if ci.isRcvNew { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { - if chatModel.chatId == cInfo.id && itemsInView.contains(ci.viewId) { - Task { - await apiMarkChatItemRead(cInfo, ci) - } - } - } - } - } - .onDisappear { - itemsInView.remove(ci.viewId) - } - } - } + //let _ = logger.debug("Reloading chatItemsList with number of itmes: \(im.reversedChatItems.count)") + ScrollRepresentable(scrollView: scrollView) { (index: Int, mergedItem: MergedItem) in + let ci = switch mergedItem { + case let .single(item, _, _): item.item + case let .grouped(items, _, _, _, _, _, _, _): items.boxedValue.last!.item } - .onAppear { - scrollProxy = proxy + let voiceNoFrame = voiceWithoutFrame(ci) + let maxWidth = cInfo.chatType == .group + ? voiceNoFrame + ? (g.size.width - 28) - 42 + : (g.size.width - 28) * 0.84 - 42 + : voiceNoFrame + ? (g.size.width - 32) + : (g.size.width - 32) * 0.84 + return ChatItemWithMenu( + chat: $chat, + index: index, + isLastItem: index == mergedItems.boxedValue.items.count - 1, + chatItem: ci, + scrollToItemId: scrollToItemId, + merged: mergedItem, + maxWidth: maxWidth, + composeState: $composeState, + selectedMember: $selectedMember, + showChatInfoSheet: $showChatInfoSheet, + revealedItems: $revealedItems, + selectedChatItems: $selectedChatItems, + forwardedChatItems: $forwardedChatItems, + searchText: $searchText, + closeKeyboardAndRun: closeKeyboardAndRun + ) + // crashes on Cell size calculation without this line + .environmentObject(ChatModel.shared) + .environmentObject(theme) // crashes without this line when scrolling to the first unread in EndlessScrollVIew + .id(ci.id) // Required to trigger `onAppear` on iOS15 + } + .onAppear { + if !im.isLoading { + updateWithInitiallyLoadedItems() } - .onTapGesture { hideKeyboard() } - .onChange(of: searchText) { _ in - loadChat(chat: chat, search: searchText) + } + .onChange(of: im.isLoading) { loading in + if !loading { + updateWithInitiallyLoadedItems() } - .onChange(of: chatModel.chatId) { _ in - if let chatId = chatModel.chatId, let c = chatModel.getChat(chatId) { - chat = c - showChatInfoSheet = false - loadChat(chat: c) - DispatchQueue.main.async { - scrollToBottom(proxy) - } + } + .onChange(of: im.reversedChatItems) { items in + mergedItems.boxedValue = MergedItems.create(items, revealedItems, im.chatState) + scrollView.updateItems(mergedItems.boxedValue.items) + if im.itemAdded { + im.itemAdded = false + if scrollView.listState.firstVisibleItemIndex < 2 { + scrollView.scrollToBottomAnimated() + } else { + scrollView.scroll(by: 34) } } } + .onChange(of: revealedItems) { revealed in + mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealed, im.chatState) + scrollView.updateItems(mergedItems.boxedValue.items) + } + .onChange(of: chat.id) { _ in + allowLoadMoreItems = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + allowLoadMoreItems = true + } + } + .padding(.vertical, -100) + .onTapGesture { hideKeyboard() } + .onChange(of: searchText) { s in + if showSearch { + searchTextChanged(s) + } + } } - .scaleEffect(x: 1, y: -1, anchor: .center) } @ViewBuilder private func connectingText() -> some View { if case let .direct(contact) = chat.chatInfo, - !contact.ready, + !contact.sndReady, contact.active, !contact.nextSendGrpInv { Text("connecting…") .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .padding(.top) } else { EmptyView() } } - - private func floatingButtons(_ proxy: ScrollViewProxy) -> some View { - let counts = chatModel.unreadChatItemCounts(itemsInView: itemsInView) - return VStack { - let unreadAbove = chat.chatStats.unreadCount - counts.unreadBelow - if unreadAbove > 0 { - circleButton { - unreadCountText(unreadAbove) - .font(.callout) - .foregroundColor(.accentColor) + + private func updateWithInitiallyLoadedItems() { + if mergedItems.boxedValue.items.isEmpty { + mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, ItemsModel.shared.chatState) + } + let unreadIndex = mergedItems.boxedValue.items.lastIndex(where: { $0.hasUnread() }) + let unreadItemId: Int64? = if let unreadIndex { mergedItems.boxedValue.items[unreadIndex].newest().item.id } else { nil } + // this helps to speed up initial process of setting scroll position and reduce time needed + // to layout items on screen + if let unreadIndex, let unreadItemId { + scrollView.setScrollPosition(unreadIndex, unreadItemId) + } + scrollView.updateItems(mergedItems.boxedValue.items) + if let unreadIndex { + scrollView.scrollToItem(unreadIndex) + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + allowLoadMoreItems = true + } + } + + private func searchTextChanged(_ s: String) { + Task { + await loadChat(chat: chat, search: s) + mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState) + await MainActor.run { + scrollView.updateItems(mergedItems.boxedValue.items) + } + if !s.isEmpty { + scrollView.scrollToBottom() + } else if let index = scrollView.listState.items.lastIndex(where: { $0.hasUnread() }) { + // scroll to the top unread item + scrollView.scrollToItem(index) + } else { + scrollView.scrollToBottom() + } + } + } + + class FloatingButtonModel: ObservableObject { + @Published var unreadAbove: Int = 0 + @Published var unreadBelow: Int = 0 + @Published var isNearBottom: Bool = true + @Published var date: Date? = nil + @Published var isDateVisible: Bool = false + var hideDateWorkItem: DispatchWorkItem? = nil + + func updateOnListChange(_ listState: EndlessScrollView<MergedItem>.ListState) { + let lastVisibleItem = oldestPartiallyVisibleListItemInListStateOrNull(listState) + let unreadBelow = if let lastVisibleItem { + max(0, ItemsModel.shared.chatState.unreadTotal - lastVisibleItem.unreadBefore) + } else { + 0 + } + let unreadAbove = ItemsModel.shared.chatState.unreadTotal - unreadBelow + let date: Date? = + if let lastVisible = listState.visibleItems.last { + Calendar.current.startOfDay(for: lastVisible.item.oldest().item.meta.itemTs) + } else { + nil } - .onTapGesture { scrollUp(proxy) } - .contextMenu { - Button { - if let ci = chatModel.topItemInView(itemsInView: itemsInView) { - Task { - await markChatRead(chat, aboveItem: ci) + + // set the counters and date indicator + DispatchQueue.main.async { [weak self] in + guard let it = self else { return } + it.setDate(visibility: true) + it.unreadAbove = unreadAbove + it.unreadBelow = unreadBelow + it.date = date + } + + // set floating button indication mode + let nearBottom = listState.firstVisibleItemIndex < 1 + if nearBottom != self.isNearBottom { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { [weak self] in + self?.isNearBottom = nearBottom + } + } + + // hide Date indicator after 1 second of no scrolling + hideDateWorkItem?.cancel() + let workItem = DispatchWorkItem { [weak self] in + guard let it = self else { return } + it.setDate(visibility: false) + it.hideDateWorkItem = nil + } + DispatchQueue.main.async { [weak self] in + self?.hideDateWorkItem = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: workItem) + } + } + + func resetDate() { + date = nil + isDateVisible = false + } + + private func setDate(visibility isVisible: Bool) { + if isVisible { + if !isNearBottom, + !isDateVisible, + let date, !Calendar.current.isDateInToday(date) { + withAnimation { self.isDateVisible = true } + } + } else if isDateVisible { + withAnimation { self.isDateVisible = false } + } + } + + } + + private struct FloatingButtons: View { + let theme: AppTheme + let scrollView: EndlessScrollView<MergedItem> + let chat: Chat + @Binding var loadingMoreItems: Bool + @Binding var loadingTopItems: Bool + @Binding var requestedTopScroll: Bool + @Binding var loadingBottomItems: Bool + @Binding var requestedBottomScroll: Bool + @Binding var animatedScrollingInProgress: Bool + let listState: EndlessScrollView<MergedItem>.ListState + @ObservedObject var model: FloatingButtonModel + let reloadItems: () -> Void + + var body: some View { + ZStack(alignment: .top) { + if let date = model.date { + DateSeparator(date: date) + .padding(.vertical, 4).padding(.horizontal, 8) + .background(.thinMaterial) + .clipShape(Capsule()) + .opacity(model.isDateVisible ? 1 : 0) + .padding(.vertical, 4) + } + VStack { + if model.unreadAbove > 0 && !animatedScrollingInProgress { + if loadingTopItems && requestedTopScroll { + circleButton { ProgressView() } + } else { + circleButton { + unreadCountText(model.unreadAbove) + .font(.callout) + .foregroundColor(theme.colors.primary) + } + .onTapGesture { + if loadingTopItems { + requestedTopScroll = true + requestedBottomScroll = false + } else { + scrollToTopUnread() + } + } + .contextMenu { + Button { + Task { + await markChatRead(chat) + } + } label: { + Label("Mark read", systemImage: "checkmark") + } + } + } + } + Spacer() + if listState.firstVisibleItemIndex != 0 && !animatedScrollingInProgress { + if loadingBottomItems && requestedBottomScroll { + circleButton { ProgressView() } + } else { + circleButton { + Group { + if model.unreadBelow > 0 { + unreadCountText(model.unreadBelow) + .font(.callout) + .foregroundColor(theme.colors.primary) + } else { + Image(systemName: "chevron.down").foregroundColor(theme.colors.primary) + } + } + } + .onTapGesture { + if loadingBottomItems || !ItemsModel.shared.lastItemsLoaded { + requestedTopScroll = false + requestedBottomScroll = true + } else { + scrollToBottom() + } } } - } label: { - Label("Mark read", systemImage: "checkmark") } } + .padding() + .frame(maxWidth: .infinity, alignment: .trailing) } - Spacer() - if counts.unreadBelow > 0 { - circleButton { - unreadCountText(counts.unreadBelow) - .font(.callout) - .foregroundColor(.accentColor) + .onChange(of: loadingTopItems) { loading in + if !loading && requestedTopScroll { + requestedTopScroll = false + scrollToTopUnread() } - .onTapGesture { scrollToBottom(proxy) } - } else if counts.totalBelow > 16 { - circleButton { - Image(systemName: "chevron.down") - .foregroundColor(.accentColor) + } + .onChange(of: loadingBottomItems) { loading in + if !loading && requestedBottomScroll && ItemsModel.shared.lastItemsLoaded { + requestedBottomScroll = false + scrollToBottom() + } + } + .onDisappear(perform: model.resetDate) + } + + private func scrollToTopUnread() { + Task { + if !ItemsModel.shared.chatState.splits.isEmpty { + await MainActor.run { loadingMoreItems = true } + await loadChat(chatId: chat.id, openAroundItemId: nil, clearItems: false) + await MainActor.run { reloadItems() } + if let index = listState.items.lastIndex(where: { $0.hasUnread() }) { + await MainActor.run { animatedScrollingInProgress = true } + await scrollView.scrollToItemAnimated(index) + await MainActor.run { animatedScrollingInProgress = false } + } + await MainActor.run { loadingMoreItems = false } + } else if let index = listState.items.lastIndex(where: { $0.hasUnread() }) { + await MainActor.run { animatedScrollingInProgress = true } + // scroll to the top unread item + await scrollView.scrollToItemAnimated(index) + await MainActor.run { animatedScrollingInProgress = false } + } else { + logger.debug("No more unread items, total: \(listState.items.count)") } - .onTapGesture { scrollToBottom(proxy) } } } - .padding() - } - - private func circleButton<Content: View>(_ content: @escaping () -> Content) -> some View { - ZStack { - Circle() - .foregroundColor(Color(uiColor: .tertiarySystemGroupedBackground)) - .frame(width: 44, height: 44) - content() + + private func scrollToBottom() { + animatedScrollingInProgress = true + Task { + await scrollView.scrollToItemAnimated(0, top: false) + await MainActor.run { animatedScrollingInProgress = false } + } + } + + private func circleButton<Content: View>(_ content: @escaping () -> Content) -> some View { + ZStack { + Circle() + .foregroundColor(Color(uiColor: .tertiarySystemGroupedBackground)) + .frame(width: 44, height: 44) + content() + } } } - + + private struct DateSeparator: View { + let date: Date + + var body: some View { + Text(String.localizedStringWithFormat( + NSLocalizedString("%@, %@", comment: "format for date separator in chat"), + date.formatted(.dateTime.weekday(.abbreviated)), + date.formatted( + Calendar.current.isDate(date, equalTo: .now, toGranularity: .year) + ? .dateTime.day().month(.abbreviated) + : .dateTime.day().month(.abbreviated).year() + ) + )) + .font(.callout) + .fontWeight(.medium) + .foregroundStyle(.secondary) + } + } + private func callButton(_ contact: Contact, _ media: CallMediaType, imageName: String) -> some View { Button { CallController.shared.startCall(contact, media) @@ -417,21 +900,55 @@ struct ChatView: View { Image(systemName: imageName) } } - + + private func endCallButton(_ call: Call) -> some View { + Button { + if CallController.useCallKit(), let callUUID = call.callUUID { + CallController.shared.endCall(callUUID: callUUID) + } else { + CallController.shared.endCall(call: call) {} + } + } label: { + Image(systemName: "phone.down.fill").tint(.red) + } + } + private func searchButton() -> some View { Button { - searchMode = true - searchFocussed = true - searchText = "" + focusSearch() } label: { Label("Search", systemImage: "magnifyingglass") } } - + + private func focusSearch() { + showSearch = true + searchFocussed = true + searchText = "" + } + + private func closeSearch() { + showSearch = false + searchText = "" + searchFocussed = false + } + + private func closeKeyboardAndRun(_ action: @escaping () -> Void) { + var delay: TimeInterval = 0 + if keyboardVisible || keyboardHiddenDate.timeIntervalSinceNow >= -1 || showSearch { + delay = 0.5 + closeSearch() + hideKeyboard() + } + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + action() + } + } + private func addMembersButton() -> some View { Button { if case let .group(gInfo) = chat.chatInfo { - Task { await loadGroupMembers(gInfo) { showAddMembersSheet = true } } + Task { await chatModel.loadGroupMembers(gInfo) { showAddMembersSheet = true } } } } label: { Image(systemName: "person.crop.circle.badge.plus") @@ -457,141 +974,555 @@ struct ChatView: View { } } - private func loadChatItems(_ cInfo: ChatInfo, _ ci: ChatItem, _ proxy: ScrollViewProxy) { - if let firstItem = chatModel.reversedChatItems.last, firstItem.id == ci.id { - if loadingItems || firstPage { return } - loadingItems = true - Task { - do { - let items = try await apiGetChatItems( - type: cInfo.chatType, - id: cInfo.apiId, - pagination: .before(chatItemId: firstItem.id, count: 50), - search: searchText - ) - await MainActor.run { - if items.count == 0 { - firstPage = true - } else { - chatModel.reversedChatItems.append(contentsOf: items.reversed()) - } - loadingItems = false - } - } catch let error { - logger.error("apiGetChat error: \(responseError(error))") - await MainActor.run { loadingItems = false } + private func showModerateSelectedMessagesAlert(_ groupInfo: GroupInfo) { + guard let count = selectedChatItems?.count, count > 0 else { return } + + AlertManager.shared.showAlert(Alert( + title: Text(count == 1 ? "Delete member message?" : "Delete \(count) messages of members?"), + message: Text( + groupInfo.fullGroupPreferences.fullDelete.on + ? (count == 1 ? "The message will be deleted for all members." : "The messages will be deleted for all members.") + : (count == 1 ? "The message will be marked as moderated for all members." : "The messages will be marked as moderated for all members.") + ), + primaryButton: .destructive(Text("Delete")) { + if let selected = selectedChatItems { + deleteMessages(chat, selected.sorted(), .cidmBroadcast, moderate: true, deletedSelectedMessages) } + }, + secondaryButton: .cancel() + )) + } + + private func deletedSelectedMessages() async { + await MainActor.run { + withAnimation { + selectedChatItems = nil } } } - - @ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View { - ChatItemWithMenu( - chat: chat, - chatItem: ci, - maxWidth: maxWidth, - composeState: $composeState, - selectedMember: $selectedMember, - chatView: self + + private func forwardSelectedMessages() { + Task { + do { + if let selectedChatItems { + let (validItems, confirmation) = try await apiPlanForwardChatItems( + type: chat.chatInfo.chatType, + id: chat.chatInfo.apiId, + itemIds: Array(selectedChatItems) + ) + if let confirmation { + if validItems.count > 0 { + showAlert( + String.localizedStringWithFormat( + NSLocalizedString("Forward %d message(s)?", comment: "alert title"), + validItems.count + ), + message: forwardConfirmationText(confirmation) + "\n" + + NSLocalizedString("Forward messages without files?", comment: "alert message") + ) { + switch confirmation { + case let .filesNotAccepted(fileIds): + [forwardAction(validItems), downloadAction(fileIds), cancelAlertAction] + default: + [forwardAction(validItems), cancelAlertAction] + } + } + } else { + showAlert( + NSLocalizedString("Nothing to forward!", comment: "alert title"), + message: forwardConfirmationText(confirmation) + ) { + switch confirmation { + case let .filesNotAccepted(fileIds): + [downloadAction(fileIds), cancelAlertAction] + default: + [okAlertAction] + } + } + } + } else { + await openForwardingSheet(validItems) + } + } + } catch { + logger.error("Plan forward chat items failed: \(error.localizedDescription)") + } + } + + func forwardConfirmationText(_ fc: ForwardConfirmation) -> String { + switch fc { + case let .filesNotAccepted(fileIds): + String.localizedStringWithFormat( + NSLocalizedString("%d file(s) were not downloaded.", comment: "forward confirmation reason"), + fileIds.count + ) + case let .filesInProgress(filesCount): + String.localizedStringWithFormat( + NSLocalizedString("%d file(s) are still being downloaded.", comment: "forward confirmation reason"), + filesCount + ) + case let .filesMissing(filesCount): + String.localizedStringWithFormat( + NSLocalizedString("%d file(s) were deleted.", comment: "forward confirmation reason"), + filesCount + ) + case let .filesFailed(filesCount): + String.localizedStringWithFormat( + NSLocalizedString("%d file(s) failed to download.", comment: "forward confirmation reason"), + filesCount + ) + } + } + + func forwardAction(_ items: [Int64]) -> UIAlertAction { + UIAlertAction( + title: NSLocalizedString("Forward messages", comment: "alert action"), + style: .default, + handler: { _ in Task { await openForwardingSheet(items) } } + ) + } + + func downloadAction(_ fileIds: [Int64]) -> UIAlertAction { + UIAlertAction( + title: NSLocalizedString("Download files", comment: "alert action"), + style: .default, + handler: { _ in + Task { + if let user = ChatModel.shared.currentUser { + await receiveFiles(user: user, fileIds: fileIds) + } + } + } + ) + } + + func openForwardingSheet(_ items: [Int64]) async { + let im = ItemsModel.shared + var items = Set(items) + var fci = [ChatItem]() + for reversedChatItem in im.reversedChatItems { + if items.contains(reversedChatItem.id) { + items.remove(reversedChatItem.id) + fci.insert(reversedChatItem, at: 0) + } + if items.isEmpty { break } + } + await MainActor.run { forwardedChatItems = fci } + } + } + + private func loadChatItems(_ chat: Chat, _ pagination: ChatPagination) async -> Bool { + if loadingMoreItems { return false } + await MainActor.run { + loadingMoreItems = true + if case .before = pagination { + loadingTopItems = true + } else if case .after = pagination { + loadingBottomItems = true + } + } + let triedToLoad = await loadChatItemsUnchecked(chat, pagination) + await MainActor.run { + loadingMoreItems = false + if case .before = pagination { + loadingTopItems = false + } else if case .after = pagination { + loadingBottomItems = false + } + } + return triedToLoad + } + + private func loadChatItemsUnchecked(_ chat: Chat, _ pagination: ChatPagination) async -> Bool { + await apiLoadMessages( + chat.chatInfo.id, + pagination, + im.chatState, + searchText, + nil, + { visibleItemIndexesNonReversed(scrollView.listState, mergedItems.boxedValue) } + ) + return true + } + + func stopAudioPlayer() { + VoiceItemState.chatView.values.forEach { $0.audioPlayer?.stop() } + VoiceItemState.chatView = [:] + } + + func onChatItemsUpdated() { + if !mergedItems.boxedValue.isActualState() { + //logger.debug("Items are not actual, waiting for the next update: \(String(describing: mergedItems.boxedValue.splits)) \(ItemsModel.shared.chatState.splits), \(mergedItems.boxedValue.indexInParentItems.count) vs \(ItemsModel.shared.reversedChatItems.count)") + return + } + floatingButtonModel.updateOnListChange(scrollView.listState) + preloadIfNeeded( + $allowLoadMoreItems, + $ignoreLoadingRequests, + scrollView.listState, + mergedItems, + loadItems: { unchecked, pagination in + if unchecked { + await loadChatItemsUnchecked(chat, pagination) + } else { + await loadChatItems(chat, pagination) + } + }, + loadLastItems: { + if !loadingMoreItems { + await loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat) + } + } ) } private struct ChatItemWithMenu: View { @EnvironmentObject var m: ChatModel - @Environment(\.colorScheme) var colorScheme - @ObservedObject var chat: Chat - var chatItem: ChatItem - var maxWidth: CGFloat + @EnvironmentObject var theme: AppTheme + @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileRadius = defaultProfileImageCorner + @Binding @ObservedObject var chat: Chat + @ObservedObject var dummyModel: ChatItemDummyModel = .shared + let index: Int + let isLastItem: Bool + let chatItem: ChatItem + let scrollToItemId: (ChatItem.ID) -> Void + let merged: MergedItem + let maxWidth: CGFloat @Binding var composeState: ComposeState @Binding var selectedMember: GMember? - var chatView: ChatView + @Binding var showChatInfoSheet: Bool + @Binding var revealedItems: Set<Int64> @State private var deletingItem: ChatItem? = nil @State private var showDeleteMessage = false @State private var deletingItems: [Int64] = [] @State private var showDeleteMessages = false - @State private var revealed = false + @State private var archivingReports: Set<Int64>? = nil + @State private var showArchivingReports = false @State private var showChatItemInfoSheet: Bool = false @State private var chatItemInfo: ChatItemInfo? + @State private var msgWidth: CGFloat = 0 + @State private var touchInProgress: Bool = false + + @Binding var selectedChatItems: Set<Int64>? + @Binding var forwardedChatItems: [ChatItem] + + @Binding var searchText: String + var closeKeyboardAndRun: (@escaping () -> Void) -> Void @State private var allowMenu: Bool = true + @State private var markedRead = false + @State private var markReadTask: Task<Void, Never>? = nil + @State private var actionSheet: SomeActionSheet? = nil - @State private var audioPlayer: AudioPlayer? - @State private var playbackState: VoiceMessagePlaybackState = .noPlayback - @State private var playbackTime: TimeInterval? + var revealed: Bool { revealedItems.contains(chatItem.id) } + + typealias ItemSeparation = (timestamp: Bool, largeGap: Bool, date: Date?) + + private func reveal(_ yes: Bool) -> Void { + merged.revealItems(yes, $revealedItems) + } + + func getItemSeparation(_ chatItem: ChatItem, _ prevItem: ChatItem?) -> ItemSeparation { + guard let prevItem else { + return ItemSeparation(timestamp: true, largeGap: true, date: nil) + } + + let sameMemberAndDirection = if case .groupRcv(let prevGroupMember) = prevItem.chatDir, case .groupRcv(let groupMember) = chatItem.chatDir { + groupMember.groupMemberId == prevGroupMember.groupMemberId + } else { + chatItem.chatDir.sent == prevItem.chatDir.sent + } + let largeGap = !sameMemberAndDirection || prevItem.meta.itemTs.timeIntervalSince(chatItem.meta.itemTs) > 60 + + return ItemSeparation( + timestamp: largeGap || formatTimestampMeta(chatItem.meta.itemTs) != formatTimestampMeta(prevItem.meta.itemTs), + largeGap: largeGap, + date: Calendar.current.isDate(chatItem.meta.itemTs, inSameDayAs: prevItem.meta.itemTs) ? nil : prevItem.meta.itemTs + ) + } + + func shouldShowAvatar(_ current: ChatItem, _ older: ChatItem?) -> Bool { + let oldIsGroupRcv = switch older?.chatDir { + case .groupRcv: true + default: false + } + let sameMember = switch (older?.chatDir, current.chatDir) { + case (.groupRcv(let oldMember), .groupRcv(let member)): + oldMember.memberId == member.memberId + default: + false + } + if case .groupRcv = current.chatDir, (older == nil || (!oldIsGroupRcv || !sameMember)) { + return true + } else { + return false + } + } var body: some View { - let (currIndex, nextItem) = m.getNextChatItem(chatItem) - let ciCategory = chatItem.mergeCategory - if (ciCategory != nil && ciCategory == nextItem?.mergeCategory) { - // memberConnected events and deleted items are aggregated at the last chat item in a row, see ChatItemView - ZStack {} // scroll doesn't work if it's EmptyView() + let im = ItemsModel.shared + + let last = isLastItem ? im.reversedChatItems.last : nil + let listItem = merged.newest() + let item = listItem.item + let range: ClosedRange<Int>? = if case let .grouped(_, _, _, rangeInReversed, _, _, _, _) = merged { + rangeInReversed.boxedValue } else { - let (prevHidden, prevItem) = m.getPrevShownChatItem(currIndex, ciCategory) - let range = itemsRange(currIndex, prevHidden) - if revealed, let range = range { - let items = Array(zip(Array(range), m.reversedChatItems[range])) - ForEach(items, id: \.1.viewId) { (i, ci) in - let prev = i == prevHidden ? prevItem : m.reversedChatItems[i + 1] - chatItemView(ci, nil, prev) + nil + } + let showAvatar = shouldShowAvatar(item, listItem.nextItem) + let single = switch merged { + case .single: true + default: false + } + let itemSeparation = getItemSeparation(item, single || revealed ? listItem.prevItem: nil) + return VStack(spacing: 0) { + if let last { + DateSeparator(date: last.meta.itemTs).padding(8) + } + chatItemListView(range, showAvatar, item, itemSeparation) + .overlay { + if let selected = selectedChatItems, chatItem.canBeDeletedForSelf { + Color.clear + .contentShape(Rectangle()) + .simultaneousGesture(TapGesture().onEnded { + let checked = selected.contains(chatItem.id) + selectUnselectChatItem(select: !checked, chatItem) + }) + } } + if let date = itemSeparation.date { + DateSeparator(date: date).padding(8) + } + } + .onAppear { + if markedRead { + return } else { - chatItemView(chatItem, range, prevItem) + markedRead = true + } + if let range { + let (itemIds, unreadMentions) = unreadItemIds(range) + if !itemIds.isEmpty { + waitToMarkRead { + await apiMarkChatItemsRead(chat.chatInfo, itemIds, mentionsRead: unreadMentions) + } + } + } else if chatItem.isRcvNew { + waitToMarkRead { + await apiMarkChatItemsRead(chat.chatInfo, [chatItem.id], mentionsRead: chatItem.meta.userMention ? 1 : 0) + } + } + } + .onDisappear { + markReadTask?.cancel() + markedRead = false + } + .actionSheet(item: $actionSheet) { $0.actionSheet } + // skip updating struct on touch if no need to show GoTo button + .if(touchInProgress || searchIsNotBlank || (chatItem.meta.itemForwarded != nil && chatItem.meta.itemForwarded != .unknown)) { + // long press listener steals taps from top-level listener, so repeating it's logic here as well + $0.onTapGesture { + hideKeyboard() + } + .onLongPressGesture(minimumDuration: .infinity, perform: {}, onPressingChanged: { pressing in + touchInProgress = pressing + }) + } + } + + private func unreadItemIds(_ range: ClosedRange<Int>) -> ([ChatItem.ID], Int) { + let im = ItemsModel.shared + var unreadItems: [ChatItem.ID] = [] + var unreadMentions: Int = 0 + + for i in range { + if i < 0 || i >= im.reversedChatItems.count { + break + } + let ci = im.reversedChatItems[i] + if ci.isRcvNew { + unreadItems.append(ci.id) + if ci.meta.userMention { + unreadMentions += 1 + } + } + } + + return (unreadItems, unreadMentions) + } + + private func waitToMarkRead(_ op: @Sendable @escaping () async -> Void) { + markReadTask = Task { + do { + _ = try await Task.sleep(nanoseconds: 600_000000) + if m.chatId == chat.chatInfo.id { + await op() + } + } catch { + // task was cancelled } } } - @ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ prevItem: ChatItem?) -> some View { + private var searchIsNotBlank: Bool { + get { + searchText.count > 0 && !searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } + } + + @available(iOS 16.0, *) + struct MemberLayout: Layout { + let spacing: Double + let msgWidth: Double + + private func sizes(subviews: Subviews, proposal: ProposedViewSize) -> (CGSize, CGSize) { + assert(subviews.count == 2, "member layout must contain exactly two subviews") + let roleSize = subviews[1].sizeThatFits(proposal) + let memberSize = subviews[0].sizeThatFits( + ProposedViewSize( + width: (proposal.width ?? msgWidth) - roleSize.width, + height: proposal.height + ) + ) + return (memberSize, roleSize) + } + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize { + let (memberSize, roleSize) = sizes(subviews: subviews, proposal: proposal) + return CGSize( + width: min( + proposal.width ?? msgWidth, + max(msgWidth, roleSize.width + spacing + memberSize.width) + ), + height: max(memberSize.height, roleSize.height) + ) + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) { + let (memberSize, roleSize) = sizes(subviews: subviews, proposal: proposal) + subviews[0].place( + at: CGPoint(x: bounds.minX, y: bounds.midY - memberSize.height / 2), + proposal: ProposedViewSize(memberSize) + ) + subviews[1].place( + at: CGPoint( + x: bounds.minX + max(memberSize.width + spacing, msgWidth - roleSize.width), + y: bounds.midY - roleSize.height / 2 + ), + proposal: ProposedViewSize(roleSize) + ) + } + } + + @ViewBuilder func chatItemListView( + _ range: ClosedRange<Int>?, + _ showAvatar: Bool, + _ ci: ChatItem, + _ itemSeparation: ItemSeparation + ) -> some View { + let bottomPadding: Double = itemSeparation.largeGap ? 10 : 2 if case let .groupRcv(member) = ci.chatDir, - case let .group(groupInfo) = chat.chatInfo { - let (prevMember, memCount): (GroupMember?, Int) = - if let range = range { - m.getPrevHiddenMember(member, range) - } else { - (nil, 1) - } - if prevItem == nil || showMemberImage(member, prevItem) || prevMember != nil { + case .group = chat.chatInfo { + if showAvatar { VStack(alignment: .leading, spacing: 4) { if ci.content.showMemberName { - Text(memberNames(member, prevMember, memCount)) - .font(.caption) - .foregroundStyle(.secondary) - .padding(.leading, memberImageSize + 14) - .padding(.top, 7) - } - HStack(alignment: .top, spacing: 8) { - ProfileImage(imageStr: member.memberProfile.image) - .frame(width: memberImageSize, height: memberImageSize) - .onTapGesture { - if chatView.membersLoaded { - selectedMember = m.getGroupMember(member.groupMemberId) - } else { - Task { - await chatView.loadGroupMembers(groupInfo) { - selectedMember = m.getGroupMember(member.groupMemberId) + Group { + let (prevMember, memCount): (GroupMember?, Int) = + if let range = range { + m.getPrevHiddenMember(member, range) + } else { + (nil, 1) + } + if memCount == 1 && member.memberRole > .member { + Group { + if #available(iOS 16.0, *) { + MemberLayout(spacing: 16, msgWidth: msgWidth) { + Text(member.chatViewName) + .lineLimit(1) + Text(member.memberRole.text) + .fontWeight(.semibold) + .lineLimit(1) + .padding(.trailing, 8) + } + } else { + HStack(spacing: 16) { + Text(member.chatViewName) + .lineLimit(1) + Text(member.memberRole.text) + .fontWeight(.semibold) + .lineLimit(1) + .layoutPriority(1) } } } + .frame( + maxWidth: maxWidth, + alignment: chatItem.chatDir.sent ? .trailing : .leading + ) + } else { + Text(memberNames(member, prevMember, memCount)) + .lineLimit(2) } - .appSheet(item: $selectedMember) { member in - GroupMemberInfoView(groupInfo: groupInfo, groupMember: member, navigation: true) - } - chatItemWithMenu(ci, range, maxWidth) + } + .font(.caption) + .foregroundStyle(.secondary) + .padding(.leading, memberImageSize + 14 + (selectedChatItems != nil && ci.canBeDeletedForSelf ? 12 + 24 : 0)) + .padding(.top, 3) // this is in addition to message sequence gap + } + HStack(alignment: .center, spacing: 0) { + if selectedChatItems != nil && ci.canBeDeletedForSelf { + SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems) + .padding(.trailing, 12) + } + HStack(alignment: .top, spacing: 10) { + MemberProfileImage(member, size: memberImageSize, backgroundColor: theme.colors.background) + .simultaneousGesture(TapGesture().onEnded { + if let mem = m.getGroupMember(member.groupMemberId) { + selectedMember = mem + } else { + let mem = GMember.init(member) + m.groupMembers.append(mem) + m.groupMembersIndexes[member.groupMemberId] = m.groupMembers.count - 1 + selectedMember = mem + } + }) + chatItemWithMenu(ci, range, maxWidth, itemSeparation) + .onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 } + } } } - .padding(.top, 5) + .padding(.bottom, bottomPadding) .padding(.trailing) .padding(.leading, 12) } else { - chatItemWithMenu(ci, range, maxWidth) - .padding(.top, 5) - .padding(.trailing) - .padding(.leading, memberImageSize + 8 + 12) + HStack(alignment: .center, spacing: 0) { + if selectedChatItems != nil && ci.canBeDeletedForSelf { + SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems) + .padding(.leading, 12) + } + chatItemWithMenu(ci, range, maxWidth, itemSeparation) + .padding(.trailing) + .padding(.leading, 10 + memberImageSize + 12) + } + .padding(.bottom, bottomPadding) } } else { - chatItemWithMenu(ci, range, maxWidth) - .padding(.horizontal) - .padding(.top, 5) + HStack(alignment: .center, spacing: 0) { + if selectedChatItems != nil && ci.canBeDeletedForSelf { + if chat.chatInfo.chatType == .group { + SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems) + .padding(.leading, 12) + } else { + SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems) + .padding(.leading) + } + } + chatItemWithMenu(ci, range, maxWidth, itemSeparation) + .padding(.horizontal) + } + .padding(.bottom, bottomPadding) } } @@ -606,27 +1537,29 @@ struct ChatView: View { } } - @ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ maxWidth: CGFloat) -> some View { + func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ maxWidth: CGFloat, _ itemSeparation: ItemSeparation) -> some View { let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading - let uiMenu: Binding<UIMenu> = Binding( - get: { UIMenu(title: "", children: menu(ci, range, live: composeState.liveMessage != nil)) }, - set: { _ in } - ) - - VStack(alignment: alignment.horizontal, spacing: 3) { - ChatItemView( - chat: chat, - chatItem: ci, - maxWidth: maxWidth, - scrollProxy: chatView.scrollProxy, - revealed: $revealed, - allowMenu: $allowMenu, - audioPlayer: $audioPlayer, - playbackState: $playbackState, - playbackTime: $playbackTime - ) - .uiKitContextMenu(menu: uiMenu, allowMenu: $allowMenu) - .accessibilityLabel("") + return VStack(alignment: alignment.horizontal, spacing: 3) { + HStack { + if ci.chatDir.sent { + goToItemButton(true) + } + ChatItemView( + chat: chat, + chatItem: ci, + scrollToItemId: scrollToItemId, + maxWidth: maxWidth, + allowMenu: $allowMenu + ) + .environment(\.revealed, revealed) + .environment(\.showTimestamp, itemSeparation.timestamp) + .modifier(ChatItemClipped(ci, tailVisible: itemSeparation.largeGap && (ci.meta.itemDeleted == nil || revealed))) + .contextMenu { menu(ci, range, live: composeState.liveMessage != nil) } + .accessibilityLabel("") + if !ci.chatDir.sent { + goToItemButton(false) + } + } if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 { chatItemReactions(ci) .padding(.bottom, 4) @@ -634,33 +1567,41 @@ struct ChatView: View { } .confirmationDialog("Delete message?", isPresented: $showDeleteMessage, titleVisibility: .visible) { Button("Delete for me", role: .destructive) { - deleteMessage(.cidmInternal) + deleteMessage(.cidmInternal, moderate: false) } - if let di = deletingItem, di.meta.editable { - Button(broadcastDeleteButtonText, role: .destructive) { - deleteMessage(.cidmBroadcast) + if let di = deletingItem, di.meta.deletable && !di.localNote && !di.isReport { + Button(broadcastDeleteButtonText(chat), role: .destructive) { + deleteMessage(.cidmBroadcast, moderate: false) } } } .confirmationDialog(deleteMessagesTitle, isPresented: $showDeleteMessages, titleVisibility: .visible) { Button("Delete for me", role: .destructive) { - deleteMessages() + deleteMessages(chat, deletingItems, moderate: false) + } + } + .confirmationDialog(archivingReports?.count == 1 ? "Archive report?" : "Archive \(archivingReports?.count ?? 0) reports?", isPresented: $showArchivingReports, titleVisibility: .visible) { + Button("For me", role: .destructive) { + if let reports = self.archivingReports { + archiveReports(chat.chatInfo, reports.sorted(), false) + self.archivingReports = [] + } + } + if case let ChatInfo.group(groupInfo) = chat.chatInfo, groupInfo.membership.memberActive { + Button("For all moderators", role: .destructive) { + if let reports = self.archivingReports { + archiveReports(chat.chatInfo, reports.sorted(), true) + self.archivingReports = [] + } + } } } .frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment) .frame(minWidth: 0, maxWidth: .infinity, alignment: alignment) - .onDisappear { - if ci.content.msgContent?.isVoice == true { - allowMenu = true - audioPlayer?.stop() - playbackState = .noPlayback - playbackTime = TimeInterval(0) - } - } .sheet(isPresented: $showChatItemInfoSheet, onDismiss: { chatItemInfo = nil }) { - ChatItemInfoView(ci: ci, chatItemInfo: $chatItemInfo) + ChatItemInfoView(ci: ci, userMemberId: chat.chatInfo.groupInfo?.membership.memberId, chatItemInfo: $chatItemInfo) } } @@ -684,145 +1625,201 @@ struct ChatView: View { Text("\(r.totalReacted)") .font(.caption) .fontWeight(r.userReacted ? .bold : .light) - .foregroundColor(r.userReacted ? .accentColor : .secondary) + .foregroundColor(r.userReacted ? theme.colors.primary : theme.colors.secondary) } } .padding(.horizontal, 6) .padding(.vertical, 4) - - if chat.chatInfo.featureEnabled(.reactions) && (ci.allowAddReaction || r.userReacted) { - v.onTapGesture { + .if(chat.chatInfo.featureEnabled(.reactions) && (ci.allowAddReaction || r.userReacted)) { v in + v.simultaneousGesture(TapGesture().onEnded { setReaction(ci, add: !r.userReacted, reaction: r.reaction) + }) + } + switch chat.chatInfo { + case let .group(groupInfo): + v.contextMenu { + ReactionContextMenu( + groupInfo: groupInfo, + itemId: ci.id, + reactionCount: r, + selectedMember: $selectedMember, + profileRadius: profileRadius + ) } - } else { + case let .direct(contact): + v.contextMenu { + contactReactionMenu(contact, r) + } + default: v } } } } - private func menu(_ ci: ChatItem, _ range: ClosedRange<Int>?, live: Bool) -> [UIMenuElement] { - var menu: [UIMenuElement] = [] - if let mc = ci.content.msgContent, ci.meta.itemDeleted == nil || revealed { - let rs = allReactions(ci) - if chat.chatInfo.featureEnabled(.reactions) && ci.allowAddReaction, - rs.count > 0 { - var rm: UIMenu - if #available(iOS 16, *) { - var children: [UIMenuElement] = Array(rs.prefix(topReactionsCount(rs))) - if let sm = reactionUIMenu(rs) { - children.append(sm) - } - rm = UIMenu(title: "", options: .displayInline, children: children) - rm.preferredElementSize = .small - } else { - rm = reactionUIMenuPreiOS16(rs) - } - menu.append(rm) + @ViewBuilder + private func menu(_ ci: ChatItem, _ range: ClosedRange<Int>?, live: Bool) -> some View { + if case let .group(gInfo) = chat.chatInfo, ci.isReport, ci.meta.itemDeleted == nil { + if ci.chatDir != .groupSnd, gInfo.membership.memberRole >= .moderator { + archiveReportButton(ci) } - if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live { - menu.append(replyUIAction(ci)) + deleteButton(ci, label: "Delete report") + } else if let mc = ci.content.msgContent, !ci.isReport, ci.meta.itemDeleted == nil || revealed { + if chat.chatInfo.featureEnabled(.reactions) && ci.allowAddReaction, + availableReactions.count > 0 { + reactionsGroup + } + if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live && !ci.localNote { + replyButton } let fileSource = getLoadedFileSource(ci.file) let fileExists = if let fs = fileSource, FileManager.default.fileExists(atPath: getAppFilePath(fs.filePath).path) { true } else { false } let copyAndShareAllowed = !ci.content.text.isEmpty || (ci.content.msgContent?.isImage == true && fileExists) if copyAndShareAllowed { - menu.append(shareUIAction(ci)) - menu.append(copyUIAction(ci)) + shareButton(ci) + copyButton(ci) } if let fileSource = fileSource, fileExists { if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) { if image.imageData != nil { - menu.append(saveFileAction(fileSource)) + saveButton(file: fileSource) } else { - menu.append(saveImageAction(image)) + saveButton(image: image) } } else { - menu.append(saveFileAction(fileSource)) + saveButton(file: fileSource) } + } else if let file = ci.file, case .rcvInvitation = file.fileStatus, fileSizeValid(file) { + downloadButton(file: file) } if ci.meta.editable && !mc.isVoice && !live { - menu.append(editAction(ci)) + editButton(chatItem) + } + if ci.meta.itemDeleted == nil + && (ci.file == nil || (fileSource != nil && fileExists)) + && !ci.isLiveDummy && !live { + forwardButton + } + if !ci.isLiveDummy { + viewInfoButton(ci) } - menu.append(viewInfoUIAction(ci)) if revealed { - menu.append(hideUIAction()) + hideButton() } - if ci.meta.itemDeleted == nil, + if ci.meta.itemDeleted == nil && !ci.localNote, let file = ci.file, - let cancelAction = file.cancelAction { - menu.append(cancelFileUIAction(file.fileId, cancelAction)) + let cancelAction = file.cancelAction { + cancelFileButton(file.fileId, cancelAction) } if !live || !ci.meta.isLive { - menu.append(deleteUIAction(ci)) + deleteButton(ci) } - if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo) { - menu.append(moderateUIAction(ci, groupInfo)) + if ci.chatDir != .groupSnd { + if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo) { + moderateButton(ci, groupInfo) + } else if ci.meta.itemDeleted == nil && chat.groupFeatureEnabled(.reports), + case let .group(gInfo) = chat.chatInfo, + gInfo.membership.memberRole == .member + && !live + && composeState.voiceMessageRecordingState == .noRecording { + reportButton(ci) + } } } else if ci.meta.itemDeleted != nil { if revealed { - menu.append(hideUIAction()) + hideButton() } else if !ci.isDeletedContent { - menu.append(revealUIAction()) + revealButton(ci) } else if range != nil { - menu.append(expandUIAction()) + expandButton() } - menu.append(viewInfoUIAction(ci)) - menu.append(deleteUIAction(ci)) + viewInfoButton(ci) + deleteButton(ci) } else if ci.isDeletedContent { - menu.append(viewInfoUIAction(ci)) - menu.append(deleteUIAction(ci)) + viewInfoButton(ci) + deleteButton(ci) } else if ci.mergeCategory != nil && ((range?.count ?? 0) > 1 || revealed) { - menu.append(revealed ? shrinkUIAction() : expandUIAction()) + if revealed { shrinkButton() } else { expandButton() } + deleteButton(ci) + } else if ci.showLocalDelete { + deleteButton(ci) + } else { + EmptyView() + } + if selectedChatItems == nil && ci.canBeDeletedForSelf { + Divider() + selectButton(ci) } - return menu } - - private func replyUIAction(_ ci: ChatItem) -> UIAction { - UIAction( - title: NSLocalizedString("Reply", comment: "chat item action"), - image: UIImage(systemName: "arrowshape.turn.up.left") - ) { _ in + + var replyButton: Button<some View> { + Button { withAnimation { if composeState.editing { - composeState = ComposeState(contextItem: .quotedItem(chatItem: ci)) + composeState = ComposeState(contextItem: .quotedItem(chatItem: chatItem)) } else { - composeState = composeState.copy(contextItem: .quotedItem(chatItem: ci)) + composeState = composeState.copy(contextItem: .quotedItem(chatItem: chatItem)) } } + } label: { + Label( + NSLocalizedString("Reply", comment: "chat item action"), + systemImage: "arrowshape.turn.up.left" + ) + } + } + + var forwardButton: Button<some View> { + Button { + forwardedChatItems = [chatItem] + } label: { + Label( + NSLocalizedString("Forward", comment: "chat item action"), + systemImage: "arrowshape.turn.up.forward" + ) + } + } + + private var reactionsGroup: some View { + if #available(iOS 16.4, *) { + return ControlGroup { + if availableReactions.count > 4 { + reactions(till: 3) + Menu { + reactions(from: 3) + } label: { + Image(systemName: "ellipsis") + } + } else { reactions() } + }.controlGroupStyle(.compactMenu) + } else { + return Menu { + reactions() + } label: { + Label( + NSLocalizedString("React…", comment: "chat item menu"), + systemImage: "face.smiling" + ) + } } } - private func reactionUIMenuPreiOS16(_ rs: [UIAction]) -> UIMenu { - UIMenu( - title: NSLocalizedString("React…", comment: "chat item menu"), - image: UIImage(systemName: "face.smiling"), - children: rs - ) - } - - @available(iOS 16.0, *) - private func reactionUIMenu(_ rs: [UIAction]) -> UIMenu? { - var children = rs - children.removeFirst(min(rs.count, topReactionsCount(rs))) - if children.count == 0 { return nil } - return UIMenu( - title: "", - image: UIImage(systemName: "ellipsis"), - children: children - ) - } - - private func allReactions(_ ci: ChatItem) -> [UIAction] { - MsgReaction.values.compactMap { r in - ci.reactions.contains(where: { $0.userReacted && $0.reaction == r }) - ? nil - : UIAction(title: r.text) { _ in setReaction(ci, add: true, reaction: r) } + func reactions(from: Int? = nil, till: Int? = nil) -> some View { + ForEach(availableReactions[(from ?? 0)..<(till ?? availableReactions.count)]) { reaction in + Button(reaction.text) { + setReaction(chatItem, add: true, reaction: reaction) + } } } - private func topReactionsCount(_ rs: [UIAction]) -> Int { - rs.count > 4 ? 3 : 4 + /// Reactions, which has not been used yet + private var availableReactions: Array<MsgReaction> { + MsgReaction.values + .filter { reaction in + !chatItem.reactions.contains { + $0.userReacted && $0.reaction == reaction + } + } } private func setReaction(_ ci: ChatItem, add: Bool, reaction: MsgReaction) { @@ -845,24 +1842,23 @@ struct ChatView: View { } } - private func shareUIAction(_ ci: ChatItem) -> UIAction { - UIAction( - title: NSLocalizedString("Share", comment: "chat item action"), - image: UIImage(systemName: "square.and.arrow.up") - ) { _ in + private func shareButton(_ ci: ChatItem) -> Button<some View> { + Button { var shareItems: [Any] = [ci.content.text] if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) { shareItems.append(image) } showShareSheet(items: shareItems) + } label: { + Label( + NSLocalizedString("Share", comment: "chat item action"), + systemImage: "square.and.arrow.up" + ) } } - - private func copyUIAction(_ ci: ChatItem) -> UIAction { - UIAction( - title: NSLocalizedString("Copy", comment: "chat item action"), - image: UIImage(systemName: "doc.on.doc") - ) { _ in + + private func copyButton(_ ci: ChatItem) -> Button<some View> { + Button { if case let .image(text, _) = ci.content.msgContent, text == "", let image = getLoadedImage(ci.file) { @@ -870,43 +1866,79 @@ struct ChatView: View { } else { UIPasteboard.general.string = ci.content.text } - } - } - - private func saveImageAction(_ image: UIImage) -> UIAction { - UIAction( - title: NSLocalizedString("Save", comment: "chat item action"), - image: UIImage(systemName: "square.and.arrow.down") - ) { _ in - UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) - } - } - - private func saveFileAction(_ fileSource: CryptoFile) -> UIAction { - UIAction( - title: NSLocalizedString("Save", comment: "chat item action"), - image: UIImage(systemName: fileSource.cryptoArgs == nil ? "square.and.arrow.down" : "lock.open") - ) { _ in - saveCryptoFile(fileSource) - } - } - - private func editAction(_ ci: ChatItem) -> UIAction { - UIAction( - title: NSLocalizedString("Edit", comment: "chat item action"), - image: UIImage(systemName: "square.and.pencil") - ) { _ in - withAnimation { - composeState = ComposeState(editingItem: ci) - } + } label: { + Label("Copy", systemImage: "doc.on.doc") } } - private func viewInfoUIAction(_ ci: ChatItem) -> UIAction { - UIAction( - title: NSLocalizedString("Info", comment: "chat item action"), - image: UIImage(systemName: "info.circle") - ) { _ in + func saveButton(image: UIImage) -> Button<some View> { + Button { + UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) + } label: { + Label( + NSLocalizedString("Save", comment: "chat item action"), + systemImage: "square.and.arrow.down" + ) + } + } + + func saveButton(file: CryptoFile) -> Button<some View> { + Button { + saveCryptoFile(file) + } label: { + Label( + NSLocalizedString("Save", comment: "chat item action"), + systemImage: file.cryptoArgs == nil ? "square.and.arrow.down" : "lock.open" + ) + } + } + + func downloadButton(file: CIFile) -> Button<some View> { + Button { + Task { + logger.debug("ChatView downloadFileAction, in Task") + if let user = m.currentUser { + await receiveFile(user: user, fileId: file.fileId) + } + } + } label: { + Label( + NSLocalizedString("Download", comment: "chat item action"), + systemImage: "arrow.down.doc" + ) + } + } + + private func editButton(_ ci: ChatItem) -> Button<some View> { + Button { + withAnimation { + composeState = ComposeState(editingItem: ci) + } + } label: { + Label( + NSLocalizedString("Edit", comment: "chat item action"), + systemImage: "square.and.pencil" + ) + } + } + + private func selectButton(_ ci: ChatItem) -> Button<some View> { + Button { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + withAnimation { + selectUnselectChatItem(select: true, ci) + } + } + } label: { + Label( + NSLocalizedString("Select", comment: "chat item action"), + systemImage: "checkmark.circle" + ) + } + } + + private func viewInfoButton(_ ci: ChatItem) -> Button<some View> { + Button { Task { do { let cInfo = chat.chatInfo @@ -915,22 +1947,23 @@ struct ChatView: View { chatItemInfo = ciInfo } if case let .group(gInfo) = chat.chatInfo { - await chatView.loadGroupMembers(gInfo) + await m.loadGroupMembers(gInfo) } } catch let error { logger.error("apiGetChatItemInfo error: \(responseError(error))") } await MainActor.run { showChatItemInfoSheet = true } } + } label: { + Label( + NSLocalizedString("Info", comment: "chat item action"), + systemImage: "info.circle" + ) } } - private func cancelFileUIAction(_ fileId: Int64, _ cancelAction: CancelAction) -> UIAction { - return UIAction( - title: cancelAction.uiAction, - image: UIImage(systemName: "xmark"), - attributes: [.destructive] - ) { _ in + private func cancelFileButton(_ fileId: Int64, _ cancelAction: CancelAction) -> Button<some View> { + Button { AlertManager.shared.showAlert(Alert( title: Text(cancelAction.alert.title), message: Text(cancelAction.alert.message), @@ -943,34 +1976,37 @@ struct ChatView: View { }, secondaryButton: .cancel() )) + } label: { + Label( + cancelAction.uiAction, + systemImage: "xmark" + ) } } - private func hideUIAction() -> UIAction { - UIAction( - title: NSLocalizedString("Hide", comment: "chat item action"), - image: UIImage(systemName: "eye.slash") - ) { _ in - withAnimation { - revealed = false + private func hideButton() -> Button<some View> { + Button { + withConditionalAnimation { + reveal(false) } + } label: { + Label( + NSLocalizedString("Hide", comment: "chat item action"), + systemImage: "eye.slash" + ) } } - - private func deleteUIAction(_ ci: ChatItem) -> UIAction { - UIAction( - title: NSLocalizedString("Delete", comment: "chat item action"), - image: UIImage(systemName: "trash"), - attributes: [.destructive] - ) { _ in - if !revealed && ci.meta.itemDeleted != nil, + + private func deleteButton(_ ci: ChatItem, label: LocalizedStringKey = "Delete") -> Button<some View> { + Button(role: .destructive) { + if !revealed, let currIndex = m.getChatItemIndex(ci), let ciCategory = ci.mergeCategory { let (prevHidden, _) = m.getPrevShownChatItem(currIndex, ciCategory) if let range = itemsRange(currIndex, prevHidden) { var itemIds: [Int64] = [] for i in range { - itemIds.append(m.reversedChatItems[i].id) + itemIds.append(ItemsModel.shared.reversedChatItems[i].id) } showDeleteMessages = true deletingItems = itemIds @@ -982,6 +2018,8 @@ struct ChatView: View { showDeleteMessage = true deletingItem = ci } + } label: { + Label(label, systemImage: "trash") } } @@ -995,63 +2033,106 @@ struct ChatView: View { } } - private func moderateUIAction(_ ci: ChatItem, _ groupInfo: GroupInfo) -> UIAction { - UIAction( - title: NSLocalizedString("Moderate", comment: "chat item action"), - image: UIImage(systemName: "flag"), - attributes: [.destructive] - ) { _ in + private func moderateButton(_ ci: ChatItem, _ groupInfo: GroupInfo) -> Button<some View> { + Button(role: .destructive) { AlertManager.shared.showAlert(Alert( title: Text("Delete member message?"), message: Text( - groupInfo.fullGroupPreferences.fullDelete.on - ? "The message will be deleted for all members." - : "The message will be marked as moderated for all members." - ), + groupInfo.fullGroupPreferences.fullDelete.on + ? "The message will be deleted for all members." + : "The message will be marked as moderated for all members." + ), primaryButton: .destructive(Text("Delete")) { deletingItem = ci - deleteMessage(.cidmBroadcast) + deleteMessage(.cidmBroadcast, moderate: true) }, secondaryButton: .cancel() )) + } label: { + Label( + NSLocalizedString("Moderate", comment: "chat item action"), + systemImage: "flag" + ) } } - private func revealUIAction() -> UIAction { - UIAction( - title: NSLocalizedString("Reveal", comment: "chat item action"), - image: UIImage(systemName: "eye") - ) { _ in - withAnimation { - revealed = true + private func archiveReportButton(_ cItem: ChatItem) -> Button<some View> { + Button { + archivingReports = [cItem.id] + showArchivingReports = true + } label: { + Label("Archive report", systemImage: "archivebox") + } + } + + private func revealButton(_ ci: ChatItem) -> Button<some View> { + Button { + withConditionalAnimation { + reveal(true) } + } label: { + Label( + NSLocalizedString("Reveal", comment: "chat item action"), + systemImage: "eye" + ) } } - private func expandUIAction() -> UIAction { - UIAction( - title: NSLocalizedString("Expand", comment: "chat item action"), - image: UIImage(systemName: "arrow.up.and.line.horizontal.and.arrow.down") - ) { _ in - withAnimation { - revealed = true + private func expandButton() -> Button<some View> { + Button { + withConditionalAnimation { + reveal(true) } + } label: { + Label( + NSLocalizedString("Expand", comment: "chat item action"), + systemImage: "arrow.up.and.line.horizontal.and.arrow.down" + ) } } - private func shrinkUIAction() -> UIAction { - UIAction( - title: NSLocalizedString("Hide", comment: "chat item action"), - image: UIImage(systemName: "arrow.down.and.line.horizontal.and.arrow.up") - ) { _ in - withAnimation { - revealed = false + private func shrinkButton() -> Button<some View> { + Button { + withConditionalAnimation { + reveal(false) } + } label: { + Label ( + NSLocalizedString("Hide", comment: "chat item action"), + systemImage: "arrow.down.and.line.horizontal.and.arrow.up" + ) } } - private var broadcastDeleteButtonText: LocalizedStringKey { - chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone" + private func reportButton(_ ci: ChatItem) -> Button<some View> { + Button(role: .destructive) { + var buttons: [ActionSheet.Button] = ReportReason.supportedReasons.map { reason in + .default(Text(reason.text)) { + withAnimation { + if composeState.editing { + composeState = ComposeState(preview: .noPreview, contextItem: .reportedItem(chatItem: chatItem, reason: reason)) + } else { + composeState = composeState.copy(preview: .noPreview, contextItem: .reportedItem(chatItem: chatItem, reason: reason)) + } + } + } + } + + buttons.append(.cancel()) + + actionSheet = SomeActionSheet( + actionSheet: ActionSheet( + title: Text("Report reason?"), + buttons: buttons + ), + id: "reportChatMessage" + ) + } label: { + Label ( + NSLocalizedString("Report", comment: "chat item action"), + systemImage: "flag" + ) + } } var deleteMessagesTitle: LocalizedStringKey { @@ -1059,101 +2140,351 @@ struct ChatView: View { return n == 1 ? "Delete message?" : "Delete \(n) messages?" } - private func deleteMessages() { - let itemIds = deletingItems - if itemIds.count > 0 { - let chatInfo = chat.chatInfo - Task { - var deletedItems: [ChatItem] = [] - for itemId in itemIds { - do { - let (di, _) = try await apiDeleteChatItem( - type: chatInfo.chatType, - id: chatInfo.apiId, - itemId: itemId, - mode: .cidmInternal - ) - deletedItems.append(di) - } catch { - logger.error("ChatView.deleteMessage error: \(error.localizedDescription)") - } - } - await MainActor.run { - for di in deletedItems { - m.removeChatItem(chatInfo, di) - } + private func selectUnselectChatItem(select: Bool, _ ci: ChatItem) { + selectedChatItems = selectedChatItems ?? [] + var itemIds: [Int64] = [] + if !revealed, + let currIndex = m.getChatItemIndex(ci), + let ciCategory = ci.mergeCategory { + let (prevHidden, _) = m.getPrevShownChatItem(currIndex, ciCategory) + if let range = itemsRange(currIndex, prevHidden) { + for i in range { + itemIds.append(ItemsModel.shared.reversedChatItems[i].id) } + } else { + itemIds.append(ci.id) } + } else { + itemIds.append(ci.id) + } + if select { + if let sel = selectedChatItems { + selectedChatItems = sel.union(itemIds) + } + } else { + itemIds.forEach { selectedChatItems?.remove($0) } } } - private func deleteMessage(_ mode: CIDeleteMode) { + private func deleteMessage(_ mode: CIDeleteMode, moderate: Bool) { logger.debug("ChatView deleteMessage") Task { logger.debug("ChatView deleteMessage: in Task") do { if let di = deletingItem { - var deletedItem: ChatItem - var toItem: ChatItem? - if case .cidmBroadcast = mode, - let (groupInfo, groupMember) = di.memberToModerate(chat.chatInfo) { - (deletedItem, toItem) = try await apiDeleteMemberChatItem( + let r = if case .cidmBroadcast = mode, + moderate, + let (groupInfo, _) = di.memberToModerate(chat.chatInfo) { + try await apiDeleteMemberChatItems( groupId: groupInfo.apiId, - groupMemberId: groupMember.groupMemberId, - itemId: di.id + itemIds: [di.id] ) } else { - (deletedItem, toItem) = try await apiDeleteChatItem( + try await apiDeleteChatItems( type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, - itemId: di.id, + itemIds: [di.id], mode: mode ) } - DispatchQueue.main.async { - deletingItem = nil - if let toItem = toItem { - _ = m.upsertChatItem(chat.chatInfo, toItem) - } else { - m.removeChatItem(chat.chatInfo, deletedItem) + if let itemDeletion = r.first { + await MainActor.run { + deletingItem = nil + if let toItem = itemDeletion.toChatItem { + _ = m.upsertChatItem(chat.chatInfo, toItem.chatItem) + } else { + m.removeChatItem(chat.chatInfo, itemDeletion.deletedChatItem.chatItem) + } + let deletedItem = itemDeletion.deletedChatItem.chatItem + if deletedItem.isActiveReport { + m.decreaseGroupReportsCounter(chat.chatInfo.id) + } } } } } catch { - logger.error("ChatView.deleteMessage error: \(error.localizedDescription)") + logger.error("ChatView.deleteMessage error: \(error)") } } } + + @ViewBuilder private func contactReactionMenu(_ contact: Contact, _ r: CIReactionCount) -> some View { + if !r.userReacted || r.totalReacted > 1 { + Button { showChatInfoSheet = true } label: { + profileMenuItem(Text(contact.displayName), contact.image, radius: profileRadius) + } + } + if r.userReacted { + Button {} label: { + profileMenuItem(Text("you"), m.currentUser?.profile.image, radius: profileRadius) + } + .disabled(true) + } + } + + func goToItemInnerButton(_ alignStart: Bool, _ image: String, touchInProgress: Bool, _ onClick: @escaping () -> Void) -> some View { + Image(systemName: image) + .resizable() + .frame(width: 13, height: 13) + .padding([alignStart ? .trailing : .leading], 10) + .tint(theme.colors.secondary.opacity(touchInProgress ? 1.0 : 0.4)) + .simultaneousGesture(TapGesture().onEnded(onClick)) + } + + @ViewBuilder + func goToItemButton(_ alignStart: Bool) -> some View { + let chatTypeApiIdMsgId = chatItem.meta.itemForwarded?.chatTypeApiIdMsgId + if searchIsNotBlank { + goToItemInnerButton(alignStart, "magnifyingglass", touchInProgress: touchInProgress) { + closeKeyboardAndRun { + ItemsModel.shared.loadOpenChatNoWait(chat.id, chatItem.id) + } + } + } else if let chatTypeApiIdMsgId { + goToItemInnerButton(alignStart, "arrow.right", touchInProgress: touchInProgress) { + closeKeyboardAndRun { + let (chatType, apiId, msgId) = chatTypeApiIdMsgId + ItemsModel.shared.loadOpenChatNoWait("\(chatType.rawValue)\(apiId)", msgId) + } + } + } + } + + private struct SelectedChatItem: View { + @EnvironmentObject var theme: AppTheme + var ciId: Int64 + @Binding var selectedChatItems: Set<Int64>? + @State var checked: Bool = false + var body: some View { + Image(systemName: checked ? "checkmark.circle.fill" : "circle") + .resizable() + .foregroundColor(checked ? theme.colors.primary : Color(uiColor: .tertiaryLabel)) + .frame(width: 24, height: 24) + .onAppear { + checked = selectedChatItems?.contains(ciId) == true + } + .onChange(of: selectedChatItems) { selected in + checked = selected?.contains(ciId) == true + } + } + } + } +} + +private func broadcastDeleteButtonText(_ chat: Chat) -> LocalizedStringKey { + chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone" +} + +private func deleteMessages(_ chat: Chat, _ deletingItems: [Int64], _ mode: CIDeleteMode = .cidmInternal, moderate: Bool, _ onSuccess: @escaping () async -> Void = {}) { + let itemIds = deletingItems + if itemIds.count > 0 { + let chatInfo = chat.chatInfo + Task { + do { + let deletedItems = if case .cidmBroadcast = mode, + moderate, + case .group = chat.chatInfo { + try await apiDeleteMemberChatItems( + groupId: chatInfo.apiId, + itemIds: itemIds + ) + } else { + try await apiDeleteChatItems( + type: chatInfo.chatType, + id: chatInfo.apiId, + itemIds: itemIds, + mode: mode + ) + } + + await MainActor.run { + for di in deletedItems { + if let toItem = di.toChatItem { + _ = ChatModel.shared.upsertChatItem(chat.chatInfo, toItem.chatItem) + } else { + ChatModel.shared.removeChatItem(chatInfo, di.deletedChatItem.chatItem) + } + let deletedItem = di.deletedChatItem.chatItem + if deletedItem.isActiveReport { + ChatModel.shared.decreaseGroupReportsCounter(chat.chatInfo.id) + } + } + } + await onSuccess() + } catch { + logger.error("ChatView.deleteMessages error: \(error.localizedDescription)") + } + } + } +} + +func archiveReports(_ chatInfo: ChatInfo, _ itemIds: [Int64], _ forAll: Bool, _ onSuccess: @escaping () async -> Void = {}) { + if itemIds.count > 0 { + Task { + do { + let deleted = try await apiDeleteReceivedReports( + groupId: chatInfo.apiId, + itemIds: itemIds, + mode: forAll ? CIDeleteMode.cidmBroadcast : CIDeleteMode.cidmInternalMark + ) + + await MainActor.run { + for di in deleted { + if let toItem = di.toChatItem { + _ = ChatModel.shared.upsertChatItem(chatInfo, toItem.chatItem) + } else { + ChatModel.shared.removeChatItem(chatInfo, di.deletedChatItem.chatItem) + } + let deletedItem = di.deletedChatItem.chatItem + if deletedItem.isActiveReport { + ChatModel.shared.decreaseGroupReportsCounter(chatInfo.id) + } + } + } + await onSuccess() + } catch { + logger.error("ChatView.archiveReports error: \(error.localizedDescription)") + } + } + } +} + +private func buildTheme() -> AppTheme { + if let cId = ChatModel.shared.chatId, let chat = ChatModel.shared.getChat(cId) { + let perChatTheme = if case let .direct(contact) = chat.chatInfo { + contact.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight) + } else if case let .group(groupInfo) = chat.chatInfo { + groupInfo.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight) + } else { + nil as ThemeModeOverride? + } + let overrides = if perChatTheme != nil { + ThemeManager.currentColors(nil, perChatTheme, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + } else { + nil as ThemeManager.ActiveTheme? + } + let theme = overrides ?? CurrentColors + return AppTheme(name: theme.name, base: theme.base, colors: theme.colors, appColors: theme.appColors, wallpaper: theme.wallpaper) + } else { + return AppTheme.shared + } +} + +struct ReactionContextMenu: View { + @EnvironmentObject var m: ChatModel + let groupInfo: GroupInfo + var itemId: Int64 + var reactionCount: CIReactionCount + @Binding var selectedMember: GMember? + var profileRadius: CGFloat + @State private var memberReactions: [MemberReaction] = [] + + var body: some View { + groupMemberReactionList() + .task { + await loadChatItemReaction() + } + } + + @ViewBuilder private func groupMemberReactionList() -> some View { + if memberReactions.isEmpty { + ForEach(Array(repeating: 0, count: reactionCount.totalReacted), id: \.self) { _ in + textSpace + } + } else { + ForEach(memberReactions, id: \.groupMember.groupMemberId) { mr in + let mem = mr.groupMember + let userMember = mem.groupMemberId == groupInfo.membership.groupMemberId + Button { + if let member = m.getGroupMember(mem.groupMemberId) { + selectedMember = member + } else { + let member = GMember.init(mem) + m.groupMembers.append(member) + m.groupMembersIndexes[member.groupMemberId] = m.groupMembers.count - 1 + selectedMember = member + } + } label: { + profileMenuItem(Text(mem.displayName), mem.image, radius: profileRadius) + } + .disabled(userMember) + } + } } - private func scrollToBottom(_ proxy: ScrollViewProxy) { - if let ci = chatModel.reversedChatItems.first { - withAnimation { proxy.scrollTo(ci.viewId, anchor: .top) } - } - } - - private func scrollUp(_ proxy: ScrollViewProxy) { - if let ci = chatModel.topItemInView(itemsInView: itemsInView) { - withAnimation { proxy.scrollTo(ci.viewId, anchor: .top) } + private func loadChatItemReaction() async { + do { + let memberReactions = try await apiGetReactionMembers( + groupId: groupInfo.groupId, + itemId: itemId, + reaction: reactionCount.reaction + ) + await MainActor.run { + self.memberReactions = memberReactions + } + } catch let error { + logger.error("apiGetReactionMembers error: \(responseError(error))") } } } -@ViewBuilder func toggleNtfsButton(_ chat: Chat) -> some View { - Button { - toggleNotifications(chat, enableNtfs: !chat.chatInfo.ntfsEnabled) - } label: { - if chat.chatInfo.ntfsEnabled { - Label("Mute", systemImage: "speaker.slash") +func profileMenuItem(_ nameText: Text, _ image: String?, radius: CGFloat) -> some View { + HStack { + nameText + if let image, let img = imageFromBase64(image) { + Image(uiImage: maskToCustomShape(img, size: 30, radius: radius)) } else { - Label("Unmute", systemImage: "speaker.wave.2") + Image(systemName: "person.crop.circle") } } } -func toggleNotifications(_ chat: Chat, enableNtfs: Bool) { +func maskToCustomShape(_ image: UIImage, size: CGFloat, radius: CGFloat) -> UIImage { + let path = Path { path in + if radius >= 50 { + path.addEllipse(in: CGRect(x: 0, y: 0, width: size, height: size)) + } else if radius <= 0 { + path.addRect(CGRect(x: 0, y: 0, width: size, height: size)) + } else { + let cornerRadius = size * CGFloat(radius) / 100 + path.addRoundedRect( + in: CGRect(x: 0, y: 0, width: size, height: size), + cornerSize: CGSize(width: cornerRadius, height: cornerRadius), + style: .continuous + ) + } + } + + return UIGraphicsImageRenderer(size: CGSize(width: size, height: size)).image { context in + context.cgContext.addPath(path.cgPath) + context.cgContext.clip() + let scale = size / max(image.size.width, image.size.height) + let imageSize = CGSize(width: image.size.width * scale, height: image.size.height * scale) + let imageOrigin = CGPoint( + x: (size - imageSize.width) / 2, + y: (size - imageSize.height) / 2 + ) + image.draw(in: CGRect(origin: imageOrigin, size: imageSize)) + } +} + +struct ToggleNtfsButton: View { + @ObservedObject var chat: Chat + + var body: some View { + if let nextMode = chat.chatInfo.nextNtfMode { + Button { + toggleNotifications(chat, enableNtfs: nextMode) + } label: { + Label(nextMode.text(mentions: chat.chatInfo.hasMentions), systemImage: nextMode.icon) + } + } + } +} + +func toggleNotifications(_ chat: Chat, enableNtfs: MsgFilter) { var chatSettings = chat.chatInfo.chatSettings ?? ChatSettings.defaults - chatSettings.enableNtfs = enableNtfs ? .all : .none + chatSettings.enableNtfs = enableNtfs updateChatSettings(chat, chatSettings: chatSettings) } @@ -1168,6 +2499,9 @@ func updateChatSettings(_ chat: Chat, chatSettings: ChatSettings) { do { try await apiSetChatSettings(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, chatSettings: chatSettings) await MainActor.run { + let wasFavorite = chat.chatInfo.chatSettings?.favorite ?? false + ChatTagsModel.shared.updateChatFavorite(favorite: chatSettings.favorite, wasFavorite: wasFavorite) + let wasUnread = chat.unreadTag switch chat.chatInfo { case var .direct(contact): contact.chatSettings = chatSettings @@ -1177,6 +2511,7 @@ func updateChatSettings(_ chat: Chat, chatSettings: ChatSettings) { ChatModel.shared.updateGroup(groupInfo) default: () } + ChatTagsModel.shared.updateChatTagRead(chat, wasUnread: wasUnread) } } catch let error { logger.error("apiSetChatSettings error \(responseError(error))") @@ -1188,7 +2523,7 @@ struct ChatView_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() chatModel.chatId = "@1" - chatModel.reversedChatItems = [ + ItemsModel.shared.reversedChatItems = [ ChatItem.getSample(1, .directSnd, .now, "hello"), ChatItem.getSample(2, .directRcv, .now, "hi"), ChatItem.getSample(3, .directRcv, .now, "hi there"), diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeFileView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeFileView.swift index bc6a96aa86..1ec46816f5 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeFileView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeFileView.swift @@ -9,7 +9,7 @@ import SwiftUI struct ComposeFileView: View { - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme let fileName: String let cancelFile: (() -> Void) let cancelEnabled: Bool @@ -32,9 +32,8 @@ struct ComposeFileView: View { } .padding(.vertical, 1) .padding(.trailing, 12) - .frame(height: 50) - .background(colorScheme == .light ? sentColorLight : sentColorDark) + .frame(height: 54) + .background(theme.appColors.sentMessage) .frame(maxWidth: .infinity) - .padding(.top, 8) } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift index edaf86912c..14026d79d1 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift @@ -10,7 +10,7 @@ import SwiftUI import SimpleXChat struct ComposeImageView: View { - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme let images: [String] let cancelImage: (() -> Void) let cancelEnabled: Bool @@ -18,10 +18,7 @@ struct ComposeImageView: View { var body: some View { HStack(alignment: .center, spacing: 8) { let imgs: [UIImage] = images.compactMap { image in - if let data = Data(base64Encoded: dropImagePrefix(image)) { - return UIImage(data: data) - } - return nil + imageFromBase64(image) } if imgs.count == 0 { ProgressView() @@ -48,9 +45,9 @@ struct ComposeImageView: View { } .padding(.vertical, 1) .padding(.trailing, 12) - .background(colorScheme == .light ? sentColorLight : sentColorDark) + .background(theme.appColors.sentMessage) + .frame(minHeight: 54) .frame(maxWidth: .infinity) - .padding(.top, 8) } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift index cc779851ab..e629a984df 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift @@ -10,44 +10,15 @@ import SwiftUI import LinkPresentation import SimpleXChat -func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) { - logger.debug("getLinkMetadata: fetching URL preview") - LPMetadataProvider().startFetchingMetadata(for: url){ metadata, error in - if let e = error { - logger.error("Error retrieving link metadata: \(e.localizedDescription)") - } - if let metadata = metadata, - let imageProvider = metadata.imageProvider, - imageProvider.canLoadObject(ofClass: UIImage.self) { - imageProvider.loadObject(ofClass: UIImage.self){ object, error in - var linkPreview: LinkPreview? = nil - if let error = error { - logger.error("Couldn't load image preview from link metadata with error: \(error.localizedDescription)") - } else { - if let image = object as? UIImage, - let resized = resizeImageToStrSize(image, maxDataSize: 14000), - let title = metadata.title, - let uri = metadata.originalURL { - linkPreview = LinkPreview(uri: uri, title: title, image: resized) - } - } - cb(linkPreview) - } - } else { - cb(nil) - } - } -} - struct ComposeLinkView: View { - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme let linkPreview: LinkPreview? var cancelPreview: (() -> Void)? = nil let cancelEnabled: Bool var body: some View { HStack(alignment: .center, spacing: 8) { - if let linkPreview = linkPreview { + if let linkPreview { linkPreviewView(linkPreview) } else { ProgressView() @@ -62,15 +33,14 @@ struct ComposeLinkView: View { } .padding(.vertical, 1) .padding(.trailing, 12) - .background(colorScheme == .light ? sentColorLight : sentColorDark) + .background(theme.appColors.sentMessage) + .frame(minHeight: 54) .frame(maxWidth: .infinity) - .padding(.top, 8) } private func linkPreviewView(_ linkPreview: LinkPreview) -> some View { HStack(alignment: .center, spacing: 8) { - if let data = Data(base64Encoded: dropImagePrefix(linkPreview.image)), - let uiImage = UIImage(data: data) { + if let uiImage = imageFromBase64(linkPreview.image) { Image(uiImage: uiImage) .resizable() .aspectRatio(contentMode: .fit) @@ -82,10 +52,10 @@ struct ComposeLinkView: View { Text(linkPreview.uri.absoluteString) .font(.caption) .lineLimit(1) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } .padding(.vertical, 5) - .frame(maxWidth: .infinity, minHeight: 60, maxHeight: 60) + .frame(maxWidth: .infinity, minHeight: 60) } } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index d089c7d6fe..8993de886f 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -11,6 +11,8 @@ import SimpleXChat import SwiftyGif import PhotosUI +let MAX_NUMBER_OF_MENTIONS = 3 + enum ComposePreview { case noPreview case linkPreview(linkPreview: LinkPreview?) @@ -19,10 +21,12 @@ enum ComposePreview { case filePreview(fileName: String, file: URL) } -enum ComposeContextItem { +enum ComposeContextItem: Equatable { case noContextItem case quotedItem(chatItem: ChatItem) case editingItem(chatItem: ChatItem) + case forwardingItems(chatItems: [ChatItem], fromChatInfo: ChatInfo) + case reportedItem(chatItem: ChatItem, reason: ReportReason) } enum VoiceMessageRecordingState { @@ -37,31 +41,41 @@ struct LiveMessage { var sentMsg: String? } +typealias MentionedMembers = [String: CIMention] + struct ComposeState { var message: String + var parsedMessage: [FormattedText] var liveMessage: LiveMessage? = nil var preview: ComposePreview var contextItem: ComposeContextItem var voiceMessageRecordingState: VoiceMessageRecordingState var inProgress = false var useLinkPreviews: Bool = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) + var mentions: MentionedMembers = [:] init( message: String = "", + parsedMessage: [FormattedText] = [], liveMessage: LiveMessage? = nil, preview: ComposePreview = .noPreview, contextItem: ComposeContextItem = .noContextItem, - voiceMessageRecordingState: VoiceMessageRecordingState = .noRecording + voiceMessageRecordingState: VoiceMessageRecordingState = .noRecording, + mentions: MentionedMembers = [:] ) { self.message = message + self.parsedMessage = parsedMessage self.liveMessage = liveMessage self.preview = preview self.contextItem = contextItem self.voiceMessageRecordingState = voiceMessageRecordingState + self.mentions = mentions } init(editingItem: ChatItem) { - self.message = editingItem.content.text + let text = editingItem.content.text + self.message = text + self.parsedMessage = editingItem.formattedText ?? FormattedText.plain(text) self.preview = chatItemPreview(chatItem: editingItem) self.contextItem = .editingItem(chatItem: editingItem) if let emc = editingItem.content.msgContent, @@ -70,24 +84,51 @@ struct ComposeState { } else { self.voiceMessageRecordingState = .noRecording } + self.mentions = editingItem.mentions ?? [:] + } + + init(forwardingItems: [ChatItem], fromChatInfo: ChatInfo) { + self.message = "" + self.parsedMessage = [] + self.preview = .noPreview + self.contextItem = .forwardingItems(chatItems: forwardingItems, fromChatInfo: fromChatInfo) + self.voiceMessageRecordingState = .noRecording } func copy( message: String? = nil, + parsedMessage: [FormattedText]? = nil, liveMessage: LiveMessage? = nil, preview: ComposePreview? = nil, contextItem: ComposeContextItem? = nil, - voiceMessageRecordingState: VoiceMessageRecordingState? = nil + voiceMessageRecordingState: VoiceMessageRecordingState? = nil, + mentions: MentionedMembers? = nil ) -> ComposeState { ComposeState( message: message ?? self.message, + parsedMessage: parsedMessage ?? self.parsedMessage, liveMessage: liveMessage ?? self.liveMessage, preview: preview ?? self.preview, contextItem: contextItem ?? self.contextItem, - voiceMessageRecordingState: voiceMessageRecordingState ?? self.voiceMessageRecordingState + voiceMessageRecordingState: voiceMessageRecordingState ?? self.voiceMessageRecordingState, + mentions: mentions ?? self.mentions ) } - + + func mentionMemberName(_ name: String) -> String { + var n = 0 + var tryName = name + while mentions[tryName] != nil { + n += 1 + tryName = "\(name)_\(n)" + } + return tryName + } + + var memberMentions: [String: Int64] { + self.mentions.compactMapValues { $0.memberRef?.groupMemberId } + } + var editing: Bool { switch contextItem { case .editingItem: return true @@ -102,12 +143,37 @@ struct ComposeState { } } + var forwarding: Bool { + switch contextItem { + case .forwardingItems: return true + default: return false + } + } + + var reporting: Bool { + switch contextItem { + case .reportedItem: return true + default: return false + } + } + + var submittingValidReport: Bool { + switch contextItem { + case let .reportedItem(_, reason): + switch reason { + case .other: return !message.isEmpty + default: return true + } + default: return false + } + } + var sendEnabled: Bool { switch preview { case let .mediaPreviews(media): return !media.isEmpty case .voicePreview: return voiceMessageRecordingState == .finished case .filePreview: return true - default: return !message.isEmpty || liveMessage != nil + default: return !message.isEmpty || forwarding || liveMessage != nil || submittingValidReport } } @@ -152,8 +218,15 @@ struct ComposeState { } } + var manyMediaPreviews: Bool { + switch preview { + case let .mediaPreviews(mediaPreviews): return mediaPreviews.count > 1 + default: return false + } + } + var attachmentDisabled: Bool { - if editing || liveMessage != nil || inProgress { return true } + if editing || forwarding || liveMessage != nil || inProgress || reporting { return true } switch preview { case .noPreview: return false case .linkPreview: return false @@ -161,6 +234,25 @@ struct ComposeState { } } + var attachmentPreview: Bool { + switch preview { + case .noPreview: false + case .linkPreview: false + case let .mediaPreviews(mediaPreviews): !mediaPreviews.isEmpty + case .voicePreview: false + case .filePreview: true + } + } + + var placeholder: String? { + switch contextItem { + case let .reportedItem(_, reason): + return reason.text + default: + return nil + } + } + var empty: Bool { message == "" && noPreview } @@ -229,11 +321,16 @@ enum UploadContent: Equatable { struct ComposeView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat @Binding var composeState: ComposeState @Binding var keyboardVisible: Bool + @Binding var keyboardHiddenDate: Date + @Binding var selectedRange: NSRange + var disabledText: LocalizedStringKey? = nil @State var linkUrl: URL? = nil + @State var hasSimplexLink: Bool = false @State var prevLinkUrl: URL? = nil @State var pendingLinkUrl: URL? = nil @State var cancelledLinks: Set<String> = [] @@ -253,12 +350,34 @@ struct ComposeView: View { // this is a workaround to fire an explicit event in certain cases @State private var stopPlayback: Bool = false - @AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true + @UserDefault(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true + @UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial var body: some View { VStack(spacing: 0) { + Divider() if chat.chatInfo.contact?.nextSendGrpInv ?? false { ContextInvitingContactMemberView() + Divider() + } + + if case let .reportedItem(_, reason) = composeState.contextItem { + reportReasonView(reason) + Divider() + } + // preference checks should match checks in forwarding list + let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks) + let fileProhibited = composeState.attachmentPreview && !chat.groupFeatureEnabled(.files) + let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice) + if simplexLinkProhibited { + msgNotAllowedView("SimpleX links not allowed", icon: "link") + Divider() + } else if fileProhibited { + msgNotAllowedView("Files and media not allowed", icon: "doc") + Divider() + } else if voiceProhibited { + msgNotAllowedView("Voice messages not allowed", icon: "mic") + Divider() } contextItemView() switch (composeState.editing, composeState.preview) { @@ -273,12 +392,13 @@ struct ComposeView: View { Image(systemName: "paperclip") .resizable() } - .disabled(composeState.attachmentDisabled || !chat.userCanSend || (chat.chatInfo.contact?.nextSendGrpInv ?? false)) + .disabled(composeState.attachmentDisabled || !chat.chatInfo.sendMsgEnabled || (chat.chatInfo.contact?.nextSendGrpInv ?? false)) .frame(width: 25, height: 25) - .padding(.bottom, 12) + .padding(.bottom, 16) .padding(.leading, 12) + .tint(theme.colors.primary) if case let .group(g) = chat.chatInfo, - !g.fullGroupPreferences.files.on { + !g.fullGroupPreferences.files.on(for: g.membership) { b.disabled(true).onTapGesture { AlertManager.shared.showAlertMsg( title: "Files and media prohibited!", @@ -291,11 +411,12 @@ struct ComposeView: View { ZStack(alignment: .leading) { SendMessageView( composeState: $composeState, + selectedRange: $selectedRange, sendMessage: { ttl in sendMessage(ttl: ttl) resetLinkPreview() }, - sendLiveMessage: sendLiveMessage, + sendLiveMessage: chat.chatInfo.chatType != .local ? sendLiveMessage : nil, updateLiveMessage: updateLiveMessage, cancelLiveMessage: { composeState.liveMessage = nil @@ -303,6 +424,7 @@ struct ComposeView: View { }, nextSendGrpInv: chat.chatInfo.contact?.nextSendGrpInv ?? false, voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice), + disableSendButton: simplexLinkProhibited || fileProhibited || voiceProhibited, showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert, startVoiceMessageRecording: { Task { @@ -314,40 +436,46 @@ struct ComposeView: View { timedMessageAllowed: chat.chatInfo.featureEnabled(.timedMessages), onMediaAdded: { media in if !media.isEmpty { chosenMedia = media }}, keyboardVisible: $keyboardVisible, + keyboardHiddenDate: $keyboardHiddenDate, sendButtonColor: chat.chatInfo.incognito ? .indigo.opacity(colorScheme == .dark ? 1 : 0.7) - : .accentColor + : theme.colors.primary ) .padding(.trailing, 12) - .background(.background) - .disabled(!chat.userCanSend) + .disabled(!chat.chatInfo.sendMsgEnabled) - if chat.userIsObserver { - Text("you are observer") + if let disabledText { + Text(disabledText) .italic() - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .padding(.horizontal, 12) - .onTapGesture { - AlertManager.shared.showAlertMsg( - title: "You can't send messages!", - message: "Please contact group admin." - ) - } } } } } - .onChange(of: composeState.message) { _ in + .background { + Color.clear + .overlay(ToolbarMaterial.material(toolbarMaterial)) + .ignoresSafeArea(.all, edges: .bottom) + } + .onChange(of: composeState.message) { msg in + let parsedMsg = parseSimpleXMarkdown(msg) + composeState = composeState.copy(parsedMessage: parsedMsg ?? FormattedText.plain(msg)) if composeState.linkPreviewAllowed { - if composeState.message.count > 0 { - showLinkPreview(composeState.message) + if msg.count > 0 { + showLinkPreview(parsedMsg) } else { resetLinkPreview() + hasSimplexLink = false } + } else if msg.count > 0 && !chat.groupFeatureEnabled(.simplexLinks) { + (_, hasSimplexLink) = getSimplexLink(parsedMsg) + } else { + hasSimplexLink = false } } - .onChange(of: chat.userCanSend) { canSend in - if !canSend { + .onChange(of: chat.chatInfo.sendMsgEnabled) { sendEnabled in + if !sendEnabled { cancelCurrentVoiceRecording() clearCurrentDraft() clearState() @@ -397,7 +525,7 @@ struct ComposeView: View { Task { var media: [(String, UploadContent)] = [] for content in selected { - if let img = resizeImageToStrSize(content.uiImage, maxDataSize: 14000) { + if let img = await resizeImageToStrSize(content.uiImage, maxDataSize: 14000) { media.append((img, content)) await MainActor.run { composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: media)) @@ -489,7 +617,7 @@ struct ComposeView: View { } private func addMediaContent(_ content: UploadContent) async { - if let img = resizeImageToStrSize(content.uiImage, maxDataSize: 14000) { + if let img = await resizeImageToStrSize(content.uiImage, maxDataSize: 14000) { var newMedia: [(String, UploadContent?)] = [] if case var .mediaPreviews(media) = composeState.preview { media.append((img, content)) @@ -580,6 +708,7 @@ struct ComposeView: View { cancelPreview: cancelLinkPreview, cancelEnabled: !composeState.inProgress ) + Divider() case let .mediaPreviews(mediaPreviews: media): ComposeImageView( images: media.map { (img, _) in img }, @@ -588,6 +717,7 @@ struct ComposeView: View { chosenMedia = [] }, cancelEnabled: !composeState.editing && !composeState.inProgress) + Divider() case let .voicePreview(recordingFileName, _): ComposeVoiceView( recordingFileName: recordingFileName, @@ -600,6 +730,7 @@ struct ComposeView: View { cancelEnabled: !composeState.editing && !composeState.inProgress, stopPlayback: $stopPlayback ) + Divider() case let .filePreview(fileName, _): ComposeFileView( fileName: fileName, @@ -607,9 +738,42 @@ struct ComposeView: View { composeState = composeState.copy(preview: .noPreview) }, cancelEnabled: !composeState.editing && !composeState.inProgress) + Divider() } } + private func msgNotAllowedView(_ reason: LocalizedStringKey, icon: String) -> some View { + HStack { + Image(systemName: icon).foregroundColor(theme.colors.secondary) + Text(reason).italic() + } + .padding(12) + .frame(minHeight: 54) + .frame(maxWidth: .infinity, alignment: .leading) + .background(.thinMaterial) + } + + + private func reportReasonView(_ reason: ReportReason) -> some View { + let reportText = switch reason { + case .spam: NSLocalizedString("Report spam: only group moderators will see it.", comment: "report reason") + case .profile: NSLocalizedString("Report member profile: only group moderators will see it.", comment: "report reason") + case .community: NSLocalizedString("Report violation: only group moderators will see it.", comment: "report reason") + case .illegal: NSLocalizedString("Report content: only group moderators will see it.", comment: "report reason") + case .other: NSLocalizedString("Report other: only group moderators will see it.", comment: "report reason") + case .unknown: "" // Should never happen + } + + return Text(reportText) + .italic() + .font(.caption) + .padding(12) + .frame(minHeight: 44) + .frame(maxWidth: .infinity, alignment: .leading) + .background(.thinMaterial) + } + + @ViewBuilder private func contextItemView() -> some View { switch composeState.contextItem { case .noContextItem: @@ -617,17 +781,36 @@ struct ComposeView: View { case let .quotedItem(chatItem: quotedItem): ContextItemView( chat: chat, - contextItem: quotedItem, + contextItems: [quotedItem], contextIcon: "arrowshape.turn.up.left", cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) } ) + Divider() case let .editingItem(chatItem: editingItem): ContextItemView( chat: chat, - contextItem: editingItem, + contextItems: [editingItem], contextIcon: "pencil", cancelContextItem: { clearState() } ) + Divider() + case let .forwardingItems(chatItems, _): + ContextItemView( + chat: chat, + contextItems: chatItems, + contextIcon: "arrowshape.turn.up.forward", + cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) } + ) + Divider() + case let .reportedItem(chatItem: reportedItem, _): + ContextItemView( + chat: chat, + contextItems: [reportedItem], + contextIcon: "flag", + cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }, + contextIconForeground: Color.red + ) + Divider() } } @@ -643,16 +826,25 @@ struct ComposeView: View { var sent: ChatItem? let msgText = text ?? composeState.message let liveMessage = composeState.liveMessage + let mentions = composeState.memberMentions if !live { if liveMessage != nil { composeState = composeState.copy(liveMessage: nil) } await sending() } if chat.chatInfo.contact?.nextSendGrpInv ?? false { await sendMemberContactInvitation() + } else if case let .forwardingItems(chatItems, fromChatInfo) = composeState.contextItem { + // Composed text is send as a reply to the last forwarded item + sent = await forwardItems(chatItems, fromChatInfo, ttl).last + if !composeState.message.isEmpty { + _ = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl, mentions: mentions) + } } else if case let .editingItem(ci) = composeState.contextItem { sent = await updateMessage(ci, live: live) } else if let liveMessage = liveMessage, liveMessage.sentMsg != nil { sent = await updateMessage(liveMessage.chatItem, live: live) + } else if case let .reportedItem(chatItem, reason) = composeState.contextItem { + sent = await send(reason, chatItemId: chatItem.id) } else { var quoted: Int64? = nil if case let .quotedItem(chatItem: quotedItem) = composeState.contextItem { @@ -661,42 +853,67 @@ struct ComposeView: View { switch (composeState.preview) { case .noPreview: - sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl) + 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) - case let .mediaPreviews(mediaPreviews: media): + sent = await send(checkLinkPreview(), quoted: quoted, live: live, ttl: ttl, mentions: mentions) + case let .mediaPreviews(media): + // TODO: CHECK THIS let last = media.count - 1 + var msgs: [ComposedMessage] = [] if last >= 0 { for i in 0..<last { - if case (_, .video(_, _, _)) = media[i] { - sent = await sendVideo(media[i], ttl: ttl) - } else { - sent = await sendImage(media[i], ttl: ttl) + if i > 0 { + // Sleep to allow `progressByTimeout` update be rendered + try? await Task.sleep(nanoseconds: 100_000000) + } + if let (fileSource, msgContent) = mediaContent(media[i], text: "") { + msgs.append(ComposedMessage(fileSource: fileSource, msgContent: msgContent)) } - _ = try? await Task.sleep(nanoseconds: 100_000000) } - if case (_, .video(_, _, _)) = media[last] { - sent = await sendVideo(media[last], text: msgText, quoted: quoted, live: live, ttl: ttl) - } else { - sent = await sendImage(media[last], text: msgText, quoted: quoted, live: live, ttl: ttl) + if let (fileSource, msgContent) = mediaContent(media[last], text: msgText) { + msgs.append(ComposedMessage(fileSource: fileSource, quotedItemId: quoted, msgContent: msgContent)) } } - if sent == nil { - sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl) + if msgs.isEmpty { + msgs = [ComposedMessage(quotedItemId: quoted, msgContent: .text(msgText))] } + sent = await send(msgs, live: live, ttl: ttl).last + case let .voicePreview(recordingFileName, duration): stopPlayback.toggle() let file = voiceCryptoFile(recordingFileName) - sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: file, ttl: ttl) + sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: file, ttl: ttl, mentions: mentions) case let .filePreview(_, file): - if let savedFile = saveFileFromURL(file, encrypted: privacyEncryptLocalFilesGroupDefault.get()) { - sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live, ttl: ttl) + if let savedFile = saveFileFromURL(file) { + sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live, ttl: ttl, mentions: mentions) } } } - await MainActor.run { clearState(live: live) } + await MainActor.run { + let wasForwarding = composeState.forwarding + clearState(live: live) + if wasForwarding, + chatModel.draftChatId == chat.chatInfo.id, + let draft = chatModel.draft { + composeState = draft + } + } return sent + func mediaContent(_ media: (String, UploadContent?), text: String) -> (CryptoFile?, MsgContent)? { + let (previewImage, uploadContent) = media + return switch uploadContent { + case let .simpleImage(image): + (saveImage(image), .image(text: text, image: previewImage)) + case let .animatedImage(image): + (saveAnimImage(image), .image(text: text, image: previewImage)) + case let .video(_, url, duration): + (moveTempFileFromURL(url), .video(text: text, image: previewImage, duration: duration)) + case .none: + nil + } + } + func sending() async { await MainActor.run { composeState.inProgress = true } } @@ -723,7 +940,7 @@ struct ComposeView: View { type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, itemId: ei.id, - msg: mc, + updatedMessage: UpdatedMessage(msgContent: mc, mentions: composeState.memberMentions), live: live ) await MainActor.run { @@ -755,27 +972,13 @@ struct ComposeView: View { return .voice(text: msgText, duration: duration) case .file: return .file(msgText) + case .report(_, let reason): + return .report(text: msgText, reason: reason) case .unknown(let type, _): return .unknown(type: type, text: msgText) } } - func sendImage(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? { - let (image, data) = imageData - if let data = data, let savedFile = saveAnyImage(data) { - return await send(.image(text: text, image: image), quoted: quoted, file: savedFile, live: live, ttl: ttl) - } - return nil - } - - func sendVideo(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? { - let (image, data) = imageData - if case let .video(_, url, duration) = data, let savedFile = moveTempFileFromURL(url) { - return await send(.video(text: text, image: image, duration: duration), quoted: quoted, file: savedFile, live: live, ttl: ttl) - } - return nil - } - func voiceCryptoFile(_ fileName: String) -> CryptoFile? { if !privacyEncryptLocalFilesGroupDefault.get() { return CryptoFile.plain(fileName) @@ -790,33 +993,93 @@ struct ComposeView: View { return nil } } - - func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? { - if let chatItem = await apiSendMessage( - type: chat.chatInfo.chatType, - id: chat.chatInfo.apiId, - file: file, - quotedItemId: quoted, - msg: mc, + + func send(_ reportReason: ReportReason, chatItemId: Int64) async -> ChatItem? { + if let chatItems = await apiReportMessage( + groupId: chat.chatInfo.apiId, + chatItemId: chatItemId, + reportReason: reportReason, + reportText: msgText + ) { + await MainActor.run { + for chatItem in chatItems { + chatModel.addChatItem(chat.chatInfo, chatItem) + } + } + return chatItems.first + } + + return nil + } + + func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?, mentions: [String: Int64]) async -> ChatItem? { + await send( + [ComposedMessage(fileSource: file, quotedItemId: quoted, msgContent: mc, mentions: mentions)], live: live, ttl: ttl + ).first + } + + func send(_ msgs: [ComposedMessage], live: Bool, ttl: Int?) async -> [ChatItem] { + if let chatItems = chat.chatInfo.chatType == .local + ? await apiCreateChatItems(noteFolderId: chat.chatInfo.apiId, composedMessages: msgs) + : await apiSendMessages( + type: chat.chatInfo.chatType, + id: chat.chatInfo.apiId, + live: live, + ttl: ttl, + composedMessages: msgs + ) { + await MainActor.run { + chatModel.removeLiveDummy(animated: false) + for chatItem in chatItems { + chatModel.addChatItem(chat.chatInfo, chatItem) + } + } + return chatItems + } + for msg in msgs { + if let file = msg.fileSource { + removeFile(file.filePath) + } + } + return [] + } + + func forwardItems(_ forwardedItems: [ChatItem], _ fromChatInfo: ChatInfo, _ ttl: Int?) async -> [ChatItem] { + if let chatItems = await apiForwardChatItems( + toChatType: chat.chatInfo.chatType, + toChatId: chat.chatInfo.apiId, + fromChatType: fromChatInfo.chatType, + fromChatId: fromChatInfo.apiId, + itemIds: forwardedItems.map { $0.id }, + ttl: ttl ) { await MainActor.run { - chatModel.removeLiveDummy(animated: false) - chatModel.addChatItem(chat.chatInfo, chatItem) + for chatItem in chatItems { + chatModel.addChatItem(chat.chatInfo, chatItem) + } + if forwardedItems.count != chatItems.count { + showAlert( + String.localizedStringWithFormat( + NSLocalizedString("%d messages not forwarded", comment: "alert title"), + forwardedItems.count - chatItems.count + ), + message: NSLocalizedString("Messages were deleted after you selected them.", comment: "alert message") + ) + } } - return chatItem + return chatItems + } else { + return [] } - if let file = file { - removeFile(file.filePath) - } - return nil } func checkLinkPreview() -> MsgContent { switch (composeState.preview) { case let .linkPreview(linkPreview: linkPreview): - if let url = parseMessage(msgText), + if let parsedMsg = parseSimpleXMarkdown(msgText), + let url = getSimplexLink(parsedMsg).url, let linkPreview = linkPreview, url == linkPreview.uri { return .link(text: msgText, preview: linkPreview) @@ -827,14 +1090,6 @@ struct ComposeView: View { return .text(msgText) } } - - func saveAnyImage(_ img: UploadContent) -> CryptoFile? { - switch img { - case let .simpleImage(image): return saveImage(image) - case let .animatedImage(image): return saveAnimImage(image) - default: return nil - } - } } private func startVoiceMessageRecording() async { @@ -943,9 +1198,9 @@ struct ComposeView: View { } } - private func showLinkPreview(_ s: String) { + private func showLinkPreview(_ parsedMsg: [FormattedText]?) { prevLinkUrl = linkUrl - linkUrl = parseMessage(s) + (linkUrl, hasSimplexLink) = getSimplexLink(parsedMsg) if let url = linkUrl { if url != composeState.linkPreview?.uri && url != pendingLinkUrl { pendingLinkUrl = url @@ -962,13 +1217,17 @@ struct ComposeView: View { } } - private func parseMessage(_ msg: String) -> URL? { - let parsedMsg = parseSimpleXMarkdown(msg) - let uri = parsedMsg?.first(where: { ft in + private func getSimplexLink(_ parsedMsg: [FormattedText]?) -> (url: URL?, hasSimplexLink: Bool) { + guard let parsedMsg else { return (nil, false) } + let url: URL? = if let uri = parsedMsg.first(where: { ft in ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) - }) - if let uri = uri { return URL(string: uri.text) } - else { return nil } + }) { + URL(string: uri.text) + } else { + nil + } + let simplexLink = parsedMsgHasSimplexLink(parsedMsg) + return (url, simplexLink) } private func isSimplexLink(_ link: String) -> Bool { @@ -976,6 +1235,9 @@ struct ComposeView: View { } private func cancelLinkPreview() { + if let pendingLink = pendingLinkUrl?.absoluteString { + cancelledLinks.insert(pendingLink) + } if let uri = composeState.linkPreview?.uri.absoluteString { cancelledLinks.insert(uri) } @@ -987,11 +1249,14 @@ struct ComposeView: View { if pendingLinkUrl == url { composeState = composeState.copy(preview: .linkPreview(linkPreview: nil)) getLinkPreview(url: url) { linkPreview in - if let linkPreview = linkPreview, - pendingLinkUrl == url { + if let linkPreview, pendingLinkUrl == url { composeState = composeState.copy(preview: .linkPreview(linkPreview: linkPreview)) - pendingLinkUrl = nil + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + composeState = composeState.copy(preview: .noPreview) + } } + pendingLinkUrl = nil } } } @@ -1008,18 +1273,23 @@ struct ComposeView_Previews: PreviewProvider { static var previews: some View { let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []) @State var composeState = ComposeState(message: "hello") + @State var selectedRange = NSRange() return Group { ComposeView( chat: chat, composeState: $composeState, - keyboardVisible: Binding.constant(true) + keyboardVisible: Binding.constant(true), + keyboardHiddenDate: Binding.constant(Date.now), + selectedRange: $selectedRange ) .environmentObject(ChatModel()) ComposeView( chat: chat, composeState: $composeState, - keyboardVisible: Binding.constant(true) + keyboardVisible: Binding.constant(true), + keyboardHiddenDate: Binding.constant(Date.now), + selectedRange: $selectedRange ) .environmentObject(ChatModel()) } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift index 2617bc77bc..441a68fccb 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift @@ -25,7 +25,7 @@ func voiceMessageTime_(_ time: TimeInterval?) -> String { struct ComposeVoiceView: View { @EnvironmentObject var chatModel: ChatModel - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme var recordingFileName: String @Binding var recordingTime: TimeInterval? @Binding var recordingState: VoiceMessageRecordingState @@ -50,9 +50,9 @@ struct ComposeVoiceView: View { } .padding(.vertical, 1) .frame(height: ComposeVoiceView.previewHeight) - .background(colorScheme == .light ? sentColorLight : sentColorDark) + .background(theme.appColors.sentMessage) + .frame(minHeight: 54) .frame(maxWidth: .infinity) - .padding(.top, 8) } private func recordingMode() -> some View { @@ -80,7 +80,7 @@ struct ComposeVoiceView: View { Button { startPlayback() } label: { - playPauseIcon("play.fill") + playPauseIcon("play.fill", theme.colors.primary) } Text(voiceMessageTime_(recordingTime)) case .playing: @@ -88,7 +88,7 @@ struct ComposeVoiceView: View { audioPlayer?.pause() playbackState = .paused } label: { - playPauseIcon("pause.fill") + playPauseIcon("pause.fill", theme.colors.primary) } Text(voiceMessageTime_(playbackTime)) case .paused: @@ -96,7 +96,7 @@ struct ComposeVoiceView: View { audioPlayer?.play() playbackState = .playing } label: { - playPauseIcon("play.fill") + playPauseIcon("play.fill", theme.colors.primary) } Text(voiceMessageTime_(playbackTime)) } @@ -131,7 +131,7 @@ struct ComposeVoiceView: View { } } - private func playPauseIcon(_ image: String, _ color: Color = .accentColor) -> some View { + private func playPauseIcon(_ image: String, _ color: Color) -> some View { Image(systemName: image) .resizable() .aspectRatio(contentMode: .fit) @@ -147,9 +147,11 @@ struct ComposeVoiceView: View { } label: { Image(systemName: "multiply") } + .tint(theme.colors.primary) } struct SliderBar: View { + @EnvironmentObject var theme: AppTheme var length: TimeInterval @Binding var progress: TimeInterval? var seek: (TimeInterval) -> Void @@ -158,10 +160,12 @@ struct ComposeVoiceView: View { Slider(value: Binding(get: { progress ?? TimeInterval(0) }, set: { seek($0) }), in: 0 ... length) .frame(maxWidth: .infinity) .frame(height: 4) + .tint(theme.colors.primary) } } private struct ProgressBar: View { + @EnvironmentObject var theme: AppTheme var length: TimeInterval @Binding var progress: TimeInterval? @@ -169,7 +173,7 @@ struct ComposeVoiceView: View { GeometryReader { geometry in ZStack { Rectangle() - .fill(Color.accentColor) + .fill(theme.colors.primary) .frame(width: min(CGFloat((progress ?? TimeInterval(0)) / length) * geometry.size.width, geometry.size.width), height: 4) .animation(.linear, value: progress) } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift index acb4f6d3e1..82090f312a 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift @@ -9,19 +9,18 @@ import SwiftUI struct ContextInvitingContactMemberView: View { - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme var body: some View { HStack { Image(systemName: "message") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) Text("Send direct message to connect") } .padding(12) - .frame(minHeight: 50) + .frame(minHeight: 54) .frame(maxWidth: .infinity, alignment: .leading) - .background(colorScheme == .light ? sentColorLight : sentColorDark) - .padding(.top, 8) + .background(.thinMaterial) } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift index 868ae3274a..845442c75f 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift @@ -10,11 +10,13 @@ import SwiftUI import SimpleXChat struct ContextItemView: View { - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat - let contextItem: ChatItem + let contextItems: [ChatItem] let contextIcon: String let cancelContextItem: () -> Void + var contextIconForeground: Color? = nil + var showSender: Bool = true var body: some View { HStack { @@ -22,14 +24,23 @@ struct ContextItemView: View { .resizable() .aspectRatio(contentMode: .fit) .frame(width: 16, height: 16) - .foregroundColor(.secondary) - if let sender = contextItem.memberDisplayName { - VStack(alignment: .leading, spacing: 4) { - Text(sender).font(.caption).foregroundColor(.secondary) - msgContentView(lines: 2) - } + .foregroundColor(contextIconForeground ?? theme.colors.secondary) + if let singleItem = contextItems.first, contextItems.count == 1 { + if showSender, let sender = singleItem.memberDisplayName { + VStack(alignment: .leading, spacing: 4) { + Text(sender).font(.caption).foregroundColor(theme.colors.secondary) + msgContentView(lines: 2, contextItem: singleItem) + } + } else { + msgContentView(lines: 3, contextItem: singleItem) + } } else { - msgContentView(lines: 3) + Text( + chat.chatInfo.chatType == .local + ? "Saving \(contextItems.count) messages" + : "Forwarding \(contextItems.count) messages" + ) + .italic() } Spacer() Button { @@ -39,28 +50,52 @@ struct ContextItemView: View { } label: { Image(systemName: "multiply") } + .tint(theme.colors.primary) } .padding(12) - .frame(minHeight: 50) + .frame(minHeight: 54) .frame(maxWidth: .infinity) - .background(chatItemFrameColor(contextItem, colorScheme)) - .padding(.top, 8) + .background(background) } - private func msgContentView(lines: Int) -> some View { - MsgContentView( - chat: chat, - text: contextItem.text, - formattedText: contextItem.formattedText - ) - .multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading) - .lineLimit(lines) + private var background: Color { + contextItems.first + .map { chatItemFrameColor($0, theme) } + ?? Color(uiColor: .tertiarySystemBackground) + } + + private func msgContentView(lines: Int, contextItem: ChatItem) -> some View { + contextMsgPreview(contextItem) + .multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading) + .lineLimit(lines) + } + + private func contextMsgPreview(_ 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 t = attachment() + Text(AttributedString(r.string)) + return t.if(r.hasSecrets, transform: hiddenSecretsView) + + func attachment() -> Text { + let isFileLoaded = if let fileSource = getLoadedFileSource(contextItem.file) { + FileManager.default.fileExists(atPath: getAppFilePath(fileSource.filePath).path) + } else { false } + switch contextItem.content.msgContent { + case .file: return isFileLoaded ? image("doc.fill") : Text("") + case .image: return image("photo") + case .voice: return isFileLoaded ? image("play.fill") : Text("") + default: return Text("") + } + } + + func image(_ s: String) -> Text { + Text(Image(systemName: s)).foregroundColor(Color(uiColor: .tertiaryLabel)) + textSpace + } } } struct ContextItemView_Previews: PreviewProvider { static var previews: some View { let contextItem: ChatItem = ChatItem.getSample(1, .directSnd, .now, "hello") - return ContextItemView(chat: Chat.sampleData, contextItem: contextItem, contextIcon: "pencil.circle", cancelContextItem: {}) + return ContextItemView(chat: Chat.sampleData, contextItems: [contextItem], contextIcon: "pencil.circle", cancelContextItem: {}, contextIconForeground: Color.red) } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift index 3eead5b0af..d809fd7b76 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift @@ -16,27 +16,26 @@ struct NativeTextEditor: UIViewRepresentable { @Binding var disableEditing: Bool @Binding var height: CGFloat @Binding var focused: Bool - let alignment: TextAlignment + @Binding var lastUnfocusedDate: Date + @Binding var placeholder: String? + @Binding var selectedRange: NSRange let onImagesAdded: ([UploadContent]) -> Void - private let minHeight: CGFloat = 37 + static let minHeight: CGFloat = 39 - private let defaultHeight: CGFloat = { - let field = CustomUITextField(height: Binding.constant(0)) - field.textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 6, right: 4) - return min(max(field.sizeThatFits(CGSizeMake(field.frame.size.width, CGFloat.greatestFiniteMagnitude)).height, 37), 360).rounded(.down) - }() - - func makeUIView(context: Context) -> UITextView { - let field = CustomUITextField(height: _height) + func makeUIView(context: Context) -> CustomUITextField { + let field = CustomUITextField(parent: self, height: _height) + field.backgroundColor = .clear field.text = text - field.textAlignment = alignment == .leading ? .left : .right + field.textAlignment = alignment(text) field.autocapitalizationType = .sentences field.setOnTextChangedListener { newText, images in if !disableEditing { - // Speed up the process of updating layout, reduce jumping content on screen - if !isShortEmoji(newText) { updateHeight(field) } text = newText + field.textAlignment = alignment(text) + field.updateFont() + // Speed up the process of updating layout, reduce jumping content on screen + field.updateHeight() } else { field.text = text } @@ -44,52 +43,60 @@ struct NativeTextEditor: UIViewRepresentable { onImagesAdded(images) } } - field.setOnFocusChangedListener { focused = $0 } + field.setOnFocusChangedListener { + focused = $0 + if !focused { + lastUnfocusedDate = .now + } + } field.delegate = field field.textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 6, right: 4) - updateFont(field) - updateHeight(field) + field.setPlaceholderView() + field.updateFont() + field.updateHeight(updateBindingNow: false) return field } - func updateUIView(_ field: UITextView, context: Context) { - field.text = text - field.textAlignment = alignment == .leading ? .left : .right - updateFont(field) - updateHeight(field) - } - - private func updateHeight(_ field: UITextView) { - let maxHeight = min(360, field.font!.lineHeight * 12) - // When having emoji in text view and then removing it, sizeThatFits shows previous size (too big for empty text view), so using work around with default size - let newHeight = field.text == "" - ? defaultHeight - : min(max(field.sizeThatFits(CGSizeMake(field.frame.size.width, CGFloat.greatestFiniteMagnitude)).height, minHeight), maxHeight).rounded(.down) - - if field.frame.size.height != newHeight { - field.frame.size = CGSizeMake(field.frame.size.width, newHeight) - (field as! CustomUITextField).invalidateIntrinsicContentHeight(newHeight) + func updateUIView(_ field: CustomUITextField, context: Context) { + if field.markedTextRange == nil && field.text != text { + field.text = text + field.textAlignment = alignment(text) + field.updateFont() + field.updateHeight(updateBindingNow: false) + } + if field.placeholder != placeholder { + field.placeholder = placeholder + } + if field.selectedRange != selectedRange { + field.selectedRange = selectedRange } - } - - private func updateFont(_ field: UITextView) { - field.font = isShortEmoji(field.text) - ? (field.text.count < 4 ? largeEmojiUIFont : mediumEmojiUIFont) - : UIFont.preferredFont(forTextStyle: .body) } } -private class CustomUITextField: UITextView, UITextViewDelegate { +private func alignment(_ text: String) -> NSTextAlignment { + isRightToLeft(text) ? .right : .left +} + +class CustomUITextField: UITextView, UITextViewDelegate { + var parent: NativeTextEditor? var height: Binding<CGFloat> var newHeight: CGFloat = 0 var onTextChanged: (String, [UploadContent]) -> Void = { newText, image in } var onFocusChanged: (Bool) -> Void = { focused in } - - init(height: Binding<CGFloat>) { + + private let placeholderLabel: UILabel = UILabel() + + init(parent: NativeTextEditor?, height: Binding<CGFloat>) { + self.parent = parent self.height = height super.init(frame: .zero, textContainer: nil) } + var placeholder: String? { + get { placeholderLabel.text } + set { placeholderLabel.text = newValue } + } + required init?(coder: NSCoder) { fatalError("Not implemented") } @@ -102,16 +109,63 @@ private class CustomUITextField: UITextView, UITextViewDelegate { invalidateIntrinsicContentSize() } - override var intrinsicContentSize: CGSize { - if height.wrappedValue != newHeight { - DispatchQueue.main.asyncAfter(deadline: .now(), execute: { self.height.wrappedValue = self.newHeight }) + func updateHeight(updateBindingNow: Bool = true) { + let maxHeight = min(360, font!.lineHeight * 12) + let newHeight = min(max(sizeThatFits(CGSizeMake(frame.size.width, CGFloat.greatestFiniteMagnitude)).height, NativeTextEditor.minHeight), maxHeight).rounded(.down) + + if self.newHeight != newHeight { + frame.size = CGSizeMake(frame.size.width, newHeight) + invalidateIntrinsicContentHeight(newHeight) + if updateBindingNow { + self.height.wrappedValue = newHeight + } else { + DispatchQueue.main.async { + self.height.wrappedValue = newHeight + } + } } - return CGSizeMake(0, newHeight) + } + + func updateFont() { + let newFont = isShortEmoji(text) + ? (text.count < 4 ? largeEmojiUIFont : mediumEmojiUIFont) + : UIFont.preferredFont(forTextStyle: .body) + if font != newFont { + font = newFont + // force apply new font because it has problem with doing it when the field had two emojis + if text.count == 0 { + text = " " + text = "" + } + } + } + + override func layoutSubviews() { + super.layoutSubviews() + updateHeight() + } + + override var intrinsicContentSize: CGSize { + CGSizeMake(0, newHeight) } func setOnTextChangedListener(onTextChanged: @escaping (String, [UploadContent]) -> Void) { self.onTextChanged = onTextChanged } + + func setPlaceholderView() { + placeholderLabel.textColor = .lightGray + placeholderLabel.font = UIFont.preferredFont(forTextStyle: .body) + placeholderLabel.isHidden = !text.isEmpty + placeholderLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(placeholderLabel) + + NSLayoutConstraint.activate([ + placeholderLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 7), + placeholderLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -7), + placeholderLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8) + ]) + } func setOnFocusChangedListener(onFocusChanged: @escaping (Bool) -> Void) { self.onFocusChanged = onFocusChanged @@ -160,6 +214,7 @@ private class CustomUITextField: UITextView, UITextViewDelegate { } func textViewDidChange(_ textView: UITextView) { + placeholderLabel.isHidden = !text.isEmpty if textView.markedTextRange == nil { var images: [UploadContent] = [] var rangeDiff = 0 @@ -191,10 +246,22 @@ private class CustomUITextField: UITextView, UITextViewDelegate { func textViewDidBeginEditing(_ textView: UITextView) { onFocusChanged(true) + updateSelectedRange(textView) } func textViewDidEndEditing(_ textView: UITextView) { onFocusChanged(false) + updateSelectedRange(textView) + } + + func textViewDidChangeSelection(_ textView: UITextView) { + updateSelectedRange(textView) + } + + private func updateSelectedRange(_ textView: UITextView) { + if parent?.selectedRange != textView.selectedRange { + parent?.selectedRange = textView.selectedRange + } } } @@ -205,7 +272,9 @@ struct NativeTextEditor_Previews: PreviewProvider{ disableEditing: Binding.constant(false), height: Binding.constant(100), focused: Binding.constant(false), - alignment: TextAlignment.leading, + lastUnfocusedDate: Binding.constant(.now), + placeholder: Binding.constant("Placeholder"), + selectedRange: Binding.constant(NSRange(location: 0, length: 0)), onImagesAdded: { _ in } ) .fixedSize(horizontal: false, vertical: true) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index 8f7b23c888..e7b02c9aea 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -13,6 +13,9 @@ private let liveMsgInterval: UInt64 = 3000_000000 struct SendMessageView: View { @Binding var composeState: ComposeState + @Binding var selectedRange: NSRange + @EnvironmentObject var theme: AppTheme + @Environment(\.isEnabled) var isEnabled var sendMessage: (Int?) -> Void var sendLiveMessage: (() async -> Void)? = nil var updateLiveMessage: (() async -> Void)? = nil @@ -20,6 +23,7 @@ struct SendMessageView: View { var nextSendGrpInv: Bool = false var showVoiceMessageButton: Bool = true var voiceMessageAllowed: Bool = true + var disableSendButton = false var showEnableVoiceMessagesAlert: ChatInfo.ShowEnableVoiceMessagesAlert = .other var startVoiceMessageRecording: (() -> Void)? = nil var finishVoiceMessageRecording: (() -> Void)? = nil @@ -29,8 +33,9 @@ struct SendMessageView: View { @State private var holdingVMR = false @Namespace var namespace @Binding var keyboardVisible: Bool + @Binding var keyboardHiddenDate: Date var sendButtonColor = Color.accentColor - @State private var teHeight: CGFloat = 42 + @State private var teHeight: CGFloat = NativeTextEditor.minHeight @State private var teFont: Font = .body @State private var sendButtonSize: CGFloat = 29 @State private var sendButtonOpacity: CGFloat = 1 @@ -38,57 +43,57 @@ struct SendMessageView: View { @State private var showCustomTimePicker = false @State private var selectedDisappearingMessageTime: Int? = customDisappearingMessageTimeDefault.get() @State private var progressByTimeout = false - @AppStorage(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false + @UserDefault(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false var body: some View { - ZStack { - HStack(alignment: .bottom) { - ZStack(alignment: .leading) { - if case .voicePreview = composeState.preview { - Text("Voice message…") - .font(teFont.italic()) - .multilineTextAlignment(.leading) - .foregroundColor(.secondary) - .padding(.horizontal, 10) - .padding(.vertical, 8) - .frame(maxWidth: .infinity) - } else { - let alignment: TextAlignment = isRightToLeft(composeState.message) ? .trailing : .leading - NativeTextEditor( - text: $composeState.message, - disableEditing: $composeState.inProgress, - height: $teHeight, - focused: $keyboardVisible, - alignment: alignment, - onImagesAdded: onMediaAdded - ) - .allowsTightening(false) - .fixedSize(horizontal: false, vertical: true) - } - } - - if progressByTimeout { - ProgressView() - .scaleEffect(1.4) - .frame(width: 31, height: 31, alignment: .center) - .padding([.bottom, .trailing], 3) - } else { - VStack(alignment: .trailing) { - if teHeight > 100 && !composeState.inProgress { - deleteTextButton() - Spacer() - } - composeActionButtons() - } - .frame(height: teHeight, alignment: .bottom) - } + let composeShape = RoundedRectangle(cornerSize: CGSize(width: 20, height: 20)) + ZStack(alignment: .leading) { + if case .voicePreview = composeState.preview { + Text("Voice message…") + .font(teFont.italic()) + .multilineTextAlignment(.leading) + .foregroundColor(theme.colors.secondary) + .padding(.horizontal, 10) + .padding(.vertical, 8) + .padding(.trailing, 32) + .frame(maxWidth: .infinity) + } else { + NativeTextEditor( + text: $composeState.message, + disableEditing: $composeState.inProgress, + height: $teHeight, + focused: $keyboardVisible, + lastUnfocusedDate: $keyboardHiddenDate, + placeholder: Binding(get: { composeState.placeholder }, set: { _ in }), + selectedRange: $selectedRange, + onImagesAdded: onMediaAdded + ) + .padding(.trailing, 32) + .allowsTightening(false) + .fixedSize(horizontal: false, vertical: true) } - .padding(.vertical, 1) - .overlay( - RoundedRectangle(cornerSize: CGSize(width: 20, height: 20)) - .strokeBorder(.secondary, lineWidth: 0.3, antialiased: true) - ) } + .overlay(alignment: .topTrailing, content: { + if !progressByTimeout && teHeight > 100 && !composeState.inProgress { + deleteTextButton() + } + }) + .overlay(alignment: .bottomTrailing, content: { + if progressByTimeout { + ProgressView() + .scaleEffect(1.4) + .frame(width: 31, height: 31, alignment: .center) + .padding([.bottom, .trailing], 4) + } else { + composeActionButtons() + // required for intercepting clicks + .background(.white.opacity(0.000001)) + } + }) + .padding(.vertical, 1) + .background(theme.colors.background) + .clipShape(composeShape) + .overlay(composeShape.strokeBorder(.secondary, lineWidth: 0.5).opacity(0.7)) .onChange(of: composeState.message, perform: { text in updateFont(text) }) .onChange(of: composeState.inProgress) { inProgress in if inProgress { @@ -106,9 +111,12 @@ struct SendMessageView: View { let vmrs = composeState.voiceMessageRecordingState if nextSendGrpInv { inviteMemberContactButton() + } else if case .reportedItem = composeState.contextItem { + sendMessageButton() } else if showVoiceMessageButton && composeState.message.isEmpty && !composeState.editing + && !composeState.forwarding && composeState.liveMessage == nil && ((composeState.noPreview && vmrs == .noRecording) || (vmrs == .recording && holdingVMR)) { @@ -164,7 +172,7 @@ struct SendMessageView: View { !composeState.sendEnabled || composeState.inProgress ) - .frame(width: 29, height: 29) + .frame(width: 31, height: 31) .padding([.bottom, .trailing], 4) } @@ -184,9 +192,10 @@ struct SendMessageView: View { !composeState.sendEnabled || composeState.inProgress || (!voiceMessageAllowed && composeState.voicePreview) || - composeState.endLiveDisabled + composeState.endLiveDisabled || + disableSendButton ) - .frame(width: 29, height: 29) + .frame(width: 31, height: 31) .contextMenu{ sendButtonContextMenuItems() } @@ -226,6 +235,7 @@ struct SendMessageView: View { !composeState.editing { if case .noContextItem = composeState.contextItem, !composeState.voicePreview, + !composeState.manyMediaPreviews, let send = sendLiveMessage, let update = updateLiveMessage { Button { @@ -246,6 +256,8 @@ struct SendMessageView: View { } private struct RecordVoiceMessageButton: View { + @Environment(\.isEnabled) var isEnabled + @EnvironmentObject var theme: AppTheme var startVoiceMessageRecording: (() -> Void)? var finishVoiceMessageRecording: (() -> Void)? @Binding var holdingVMR: Bool @@ -253,12 +265,14 @@ struct SendMessageView: View { @State private var pressed: TimeInterval? = nil var body: some View { - Button(action: {}) { - Image(systemName: "mic.fill") - .foregroundColor(.accentColor) - } + Image(systemName: isEnabled ? "mic.fill" : "mic") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .foregroundColor(isEnabled ? theme.colors.primary : theme.colors.secondary) + .opacity(holdingVMR ? 0.7 : 1) .disabled(disabled) - .frame(width: 29, height: 29) + .frame(width: 31, height: 31) .padding([.bottom, .trailing], 4) ._onButtonGesture { down in if down { @@ -266,9 +280,7 @@ struct SendMessageView: View { pressed = ProcessInfo.processInfo.systemUptime startVoiceMessageRecording?() } else { - let now = ProcessInfo.processInfo.systemUptime - if let pressed = pressed, - now - pressed >= 1 { + if let pressed, ProcessInfo.processInfo.systemUptime - pressed >= 1 { finishVoiceMessageRecording?() } holdingVMR = false @@ -308,10 +320,13 @@ struct SendMessageView: View { } } label: { Image(systemName: "mic") - .foregroundColor(.secondary) + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .foregroundColor(theme.colors.secondary) } .disabled(composeState.inProgress) - .frame(width: 29, height: 29) + .frame(width: 31, height: 31) .padding([.bottom, .trailing], 4) } @@ -322,7 +337,7 @@ struct SendMessageView: View { Image(systemName: "multiply") .resizable() .scaledToFit() - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) .frame(width: 15, height: 15) } .frame(width: 29, height: 29) @@ -339,7 +354,7 @@ struct SendMessageView: View { Image(systemName: "bolt.fill") .resizable() .scaledToFit() - .foregroundColor(.accentColor) + .foregroundColor(isEnabled ? theme.colors.primary : theme.colors.secondary) .frame(width: 20, height: 20) } .frame(width: 29, height: 29) @@ -382,7 +397,7 @@ struct SendMessageView: View { } Task { _ = try? await Task.sleep(nanoseconds: liveMsgInterval) - while composeState.liveMessage != nil { + while await composeState.liveMessage != nil { await update() _ = try? await Task.sleep(nanoseconds: liveMsgInterval) } @@ -393,10 +408,10 @@ struct SendMessageView: View { private func finishVoiceMessageRecordingButton() -> some View { Button(action: { finishVoiceMessageRecording?() }) { Image(systemName: "stop.fill") - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) } .disabled(composeState.inProgress) - .frame(width: 29, height: 29) + .frame(width: 31, height: 31) .padding([.bottom, .trailing], 4) } @@ -412,8 +427,10 @@ struct SendMessageView: View { struct SendMessageView_Previews: PreviewProvider { static var previews: some View { @State var composeStateNew = ComposeState() + @State var selectedRange = NSRange() let ci = ChatItem.getSample(1, .directSnd, .now, "hello") @State var composeStateEditing = ComposeState(editingItem: ci) + @State var selectedRangeEditing = NSRange() @State var sendEnabled: Bool = true return Group { @@ -422,9 +439,11 @@ struct SendMessageView_Previews: PreviewProvider { Spacer(minLength: 0) SendMessageView( composeState: $composeStateNew, + selectedRange: $selectedRange, sendMessage: { _ in }, onMediaAdded: { _ in }, - keyboardVisible: Binding.constant(true) + keyboardVisible: Binding.constant(true), + keyboardHiddenDate: Binding.constant(Date.now) ) } VStack { @@ -432,9 +451,11 @@ struct SendMessageView_Previews: PreviewProvider { Spacer(minLength: 0) SendMessageView( composeState: $composeStateEditing, + selectedRange: $selectedRangeEditing, sendMessage: { _ in }, onMediaAdded: { _ in }, - keyboardVisible: Binding.constant(true) + keyboardVisible: Binding.constant(true), + keyboardHiddenDate: Binding.constant(Date.now) ) } } diff --git a/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift b/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift index ff1892d996..e4489e46ee 100644 --- a/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift @@ -12,10 +12,12 @@ import SimpleXChat struct ContactPreferencesView: View { @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @Binding var contact: Contact - @State var featuresAllowed: ContactFeaturesAllowed - @State var currentFeaturesAllowed: ContactFeaturesAllowed + @Binding var featuresAllowed: ContactFeaturesAllowed + @Binding var currentFeaturesAllowed: ContactFeaturesAllowed @State private var showSaveDialogue = false + let savePreferences: () -> Void var body: some View { let user: User = chatModel.currentUser! @@ -35,7 +37,7 @@ struct ContactPreferencesView: View { .disabled(currentFeaturesAllowed == featuresAllowed) } } - .modifier(BackButton { + .modifier(BackButton(disabled: Binding.constant(false)) { if currentFeaturesAllowed == featuresAllowed { dismiss() } else { @@ -47,7 +49,10 @@ struct ContactPreferencesView: View { savePreferences() dismiss() } - Button("Exit without saving") { dismiss() } + Button("Exit without saving") { + featuresAllowed = currentFeaturesAllowed + dismiss() + } } } @@ -66,8 +71,8 @@ struct ContactPreferencesView: View { .frame(height: 36) infoRow("Contact allows", pref.contactPreference.allow.text) } - header: { featureHeader(feature, enabled) } - footer: { featureFooter(feature, enabled) } + header: { featureHeader(feature, enabled).foregroundColor(theme.colors.secondary) } + footer: { featureFooter(feature, enabled).foregroundColor(theme.colors.secondary) } } private func timedMessagesFeatureSection() -> some View { @@ -102,8 +107,8 @@ struct ContactPreferencesView: View { infoRow("Delete after", timeText(pref.contactPreference.ttl)) } } - header: { featureHeader(.timedMessages, enabled) } - footer: { featureFooter(.timedMessages, enabled) } + header: { featureHeader(.timedMessages, enabled).foregroundColor(theme.colors.secondary) } + footer: { featureFooter(.timedMessages, enabled).foregroundColor(theme.colors.secondary) } } private func featureHeader(_ feature: ChatFeature, _ enabled: FeatureEnabled) -> some View { @@ -116,24 +121,6 @@ struct ContactPreferencesView: View { private func featureFooter(_ feature: ChatFeature, _ enabled: FeatureEnabled) -> some View { Text(feature.enabledDescription(enabled)) - .frame(height: 36, alignment: .topLeading) - } - - private func savePreferences() { - Task { - do { - let prefs = contactFeaturesAllowedToPrefs(featuresAllowed) - if let toContact = try await apiSetContactPrefs(contactId: contact.contactId, preferences: prefs) { - await MainActor.run { - contact = toContact - chatModel.updateContact(toContact) - currentFeaturesAllowed = featuresAllowed - } - } - } catch { - logger.error("ContactPreferencesView apiSetContactPrefs error: \(responseError(error))") - } - } } } @@ -141,8 +128,9 @@ struct ContactPreferencesView_Previews: PreviewProvider { static var previews: some View { ContactPreferencesView( contact: Binding.constant(Contact.sampleData), - featuresAllowed: ContactFeaturesAllowed.sampleData, - currentFeaturesAllowed: ContactFeaturesAllowed.sampleData + featuresAllowed: Binding.constant(ContactFeaturesAllowed.sampleData), + currentFeaturesAllowed: Binding.constant(ContactFeaturesAllowed.sampleData), + savePreferences: {} ) } } diff --git a/apps/ios/Shared/Views/Chat/EndlessScrollView.swift b/apps/ios/Shared/Views/Chat/EndlessScrollView.swift new file mode 100644 index 0000000000..cc61754b26 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/EndlessScrollView.swift @@ -0,0 +1,715 @@ +// +// EndlessScrollView.swift +// SimpleX (iOS) +// +// Created by Stanislav Dmitrenko on 25.01.2025. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct ScrollRepresentable<Content: View, ScrollItem>: UIViewControllerRepresentable where ScrollItem : Identifiable, ScrollItem: Hashable { + + let scrollView: EndlessScrollView<ScrollItem> + let content: (Int, ScrollItem) -> Content + + func makeUIViewController(context: Context) -> ScrollController { + ScrollController.init(scrollView: scrollView, content: content) + } + + func updateUIViewController(_ controller: ScrollController, context: Context) {} + + class ScrollController: UIViewController { + let scrollView: EndlessScrollView<ScrollItem> + fileprivate var items: [ScrollItem] = [] + fileprivate var content: ((Int, ScrollItem) -> Content)! + + fileprivate init(scrollView: EndlessScrollView<ScrollItem>, content: @escaping (Int, ScrollItem) -> Content) { + self.scrollView = scrollView + self.content = content + super.init(nibName: nil, bundle: nil) + self.view = scrollView + scrollView.createCell = createCell + scrollView.updateCell = updateCell + } + + required init?(coder: NSCoder) { fatalError() } + + private func createCell(_ index: Int, _ items: [ScrollItem], _ cellsToReuse: inout [UIView]) -> UIView { + let item: ScrollItem? = index >= 0 && index < items.count ? items[index] : nil + let cell: UIView + if #available(iOS 16.0, *), false { + let c: UITableViewCell = cellsToReuse.isEmpty ? UITableViewCell() : cellsToReuse.removeLast() as! UITableViewCell + if let item { + c.contentConfiguration = UIHostingConfiguration { self.content(index, item) } + .margins(.all, 0) + .minSize(height: 1) // Passing zero will result in system default of 44 points being used + } + cell = c + } else { + let c = cellsToReuse.isEmpty ? HostingCell<Content>() : cellsToReuse.removeLast() as! HostingCell<Content> + if let item { + c.set(content: self.content(index, item), parent: self) + } + cell = c + } + cell.isHidden = false + cell.backgroundColor = .clear + let size = cell.systemLayoutSizeFitting(CGSizeMake(scrollView.bounds.width, CGFloat.greatestFiniteMagnitude)) + cell.frame.size.width = scrollView.bounds.width + cell.frame.size.height = size.height + return cell + } + + private func updateCell(cell: UIView, _ index: Int, _ items: [ScrollItem]) { + let item = items[index] + if #available(iOS 16.0, *), false { + (cell as! UITableViewCell).contentConfiguration = UIHostingConfiguration { self.content(index, item) } + .margins(.all, 0) + .minSize(height: 1) // Passing zero will result in system default of 44 points being used + } else { + if let cell = cell as? HostingCell<Content> { + cell.set(content: self.content(index, item), parent: self) + } else { + fatalError("Unexpected Cell Type for: \(item)") + } + } + let size = cell.systemLayoutSizeFitting(CGSizeMake(scrollView.bounds.width, CGFloat.greatestFiniteMagnitude)) + cell.frame.size.width = scrollView.bounds.width + cell.frame.size.height = size.height + cell.setNeedsLayout() + } + } +} + +class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestureRecognizerDelegate where ScrollItem : Identifiable, ScrollItem: Hashable { + + /// Stores actual state of the scroll view and all elements drawn on the screen + let listState: ListState = ListState() + + /// Just some random big number that will probably be enough to scrolling down and up without reaching the end + var initialOffset: CGFloat = 100000000 + + /// Default item id when no items in the visible items list. Something that will never be in real data + fileprivate static var DEFAULT_ITEM_ID: any Hashable { get { Int64.min } } + + /// Storing an offset that was already used for laying down content to be able to see the difference + var prevProcessedOffset: CGFloat = 0 + + /// When screen is being rotated, it's important to track the view size and adjust scroll offset accordingly because the view doesn't know that the content + /// starts from bottom and ends at top, not vice versa as usual + var oldScreenHeight: CGFloat = 0 + + /// Not 100% correct height of the content since the items loaded lazily and their dimensions are unkown until they are on screen + var estimatedContentHeight: ContentHeight = ContentHeight() + + /// Specify here the value that is small enough to NOT see any weird animation when you scroll to items. Minimum expected item size is ok. Scroll speed depends on it too + var averageItemHeight: CGFloat = 30 + + /// This is used as a multiplier for difference between current index and scrollTo index using [averageItemHeight] as well. Increase it to get faster speed + var scrollStepMultiplier: CGFloat = 0.37 + + /// Adds content padding to top + var insetTop: CGFloat = 100 + + /// Adds content padding to bottom + var insetBottom: CGFloat = 100 + + var scrollToItemIndexDelayed: Int? = nil + + /// The second scroll view that is used only for purpose of displaying scroll bar with made-up content size and scroll offset that is gathered from main scroll view, see [estimatedContentHeight] + let scrollBarView: UIScrollView = UIScrollView(frame: .zero) + + /// Stores views that can be used to hold new content so it will be faster to replace something than to create the whole view from scratch + var cellsToReuse: [UIView] = [] + + /// Enable debug to see hundreds of logs + var debug: Bool = false + + var createCell: (Int, [ScrollItem], inout [UIView]) -> UIView? = { _, _, _ in nil } + var updateCell: (UIView, Int, [ScrollItem]) -> Void = { cell, _, _ in } + + + override init(frame: CGRect) { + super.init(frame: frame) + self.delegate = self + } + + required init?(coder: NSCoder) { fatalError() } + + class ListState: NSObject { + + /// Will be called on every change of the items array, visible items, and scroll position + var onUpdateListener: () -> Void = {} + + /// Items that were used to lay out the screen + var items: [ScrollItem] = [] { + didSet { + onUpdateListener() + } + } + + /// It is equai to the number of [items] + var totalItemsCount: Int { + items.count + } + + /// The items with their positions and other useful information. Only those that are visible on screen + var visibleItems: [EndlessScrollView<ScrollItem>.VisibleItem] = [] + + /// Index in [items] of the first item on screen. This is intentiallty not derived from visible items because it's is used as a starting point for laying out the screen + var firstVisibleItemIndex: Int = 0 + + /// Unique item id of the first visible item on screen + var firstVisibleItemId: any Hashable = EndlessScrollView<ScrollItem>.DEFAULT_ITEM_ID + + /// Item offset of the first item on screen. Most of the time it's non-positive but it can be positive as well when a user produce overscroll effect on top/bottom of the scroll view + var firstVisibleItemOffset: CGFloat = -100 + + /// Index of the last visible item on screen + var lastVisibleItemIndex: Int { + visibleItems.last?.index ?? 0 + } + + /// Specifies if visible items cover the whole screen or can cover it (if overscrolled) + var itemsCanCoverScreen: Bool = false + + /// Whether there is a non-animated scroll to item in progress or not + var isScrolling: Bool = false + /// Whether there is an animated scroll to item in progress or not + var isAnimatedScrolling: Bool = false + + override init() { + super.init() + } + } + + class VisibleItem { + let index: Int + let item: ScrollItem + let view: UIView + var offset: CGFloat + + init(index: Int, item: ScrollItem, view: UIView, offset: CGFloat) { + self.index = index + self.item = item + self.view = view + self.offset = offset + } + } + + class ContentHeight { + /// After that you should see overscroll effect. When scroll positon is far from + /// top/bottom items, these values are estimated based on items count multiplied by averageItemHeight or real item height (from visible items). Example: + /// [ 10, 9, 8, 7, (6, 5, 4, 3), 2, 1, 0] - 6, 5, 4, 3 are visible and have know heights but others have unknown height and for them averageItemHeight will be used to calculate the whole content height + var topOffsetY: CGFloat = 0 + var bottomOffsetY: CGFloat = 0 + + var virtualScrollOffsetY: CGFloat = 0 + + /// How much distance were overscolled on top which often means to show sticky scrolling that should scroll back to real position after a users finishes dragging the scrollView + var overscrolledTop: CGFloat = 0 + + /// Adds content padding to bottom and top + var inset: CGFloat = 100 + + /// Estimated height of the contents of scroll view + var height: CGFloat { + get { bottomOffsetY - topOffsetY } + } + + /// Estimated height of the contents of scroll view + distance of overscrolled effect. It's only updated when number of item changes to prevent jumping of scroll bar + var virtualOverscrolledHeight: CGFloat { + get { + bottomOffsetY - topOffsetY + overscrolledTop - inset * 2 + } + } + + func update( + _ contentOffset: CGPoint, + _ listState: ListState, + _ averageItemHeight: CGFloat, + _ updateStaleHeight: Bool + ) { + let lastVisible = listState.visibleItems.last + let firstVisible = listState.visibleItems.first + guard let last = lastVisible, let first = firstVisible else { + topOffsetY = contentOffset.y + bottomOffsetY = contentOffset.y + virtualScrollOffsetY = 0 + overscrolledTop = 0 + return + } + topOffsetY = last.view.frame.origin.y - CGFloat(listState.totalItemsCount - last.index - 1) * averageItemHeight - self.inset + bottomOffsetY = first.view.frame.origin.y + first.view.bounds.height + CGFloat(first.index) * averageItemHeight + self.inset + virtualScrollOffsetY = contentOffset.y - topOffsetY + overscrolledTop = max(0, last.index == listState.totalItemsCount - 1 ? last.view.frame.origin.y - contentOffset.y : 0) + } + } + + var topY: CGFloat { + get { contentOffset.y } + } + + var bottomY: CGFloat { + get { contentOffset.y + bounds.height } + } + + override func layoutSubviews() { + super.layoutSubviews() + if contentSize.height == 0 { + setup() + } + let newScreenHeight = bounds.height + if newScreenHeight != oldScreenHeight && oldScreenHeight != 0 { + contentOffset.y += oldScreenHeight - newScreenHeight + scrollBarView.frame = CGRectMake(frame.width - 10, self.insetTop, 10, frame.height - self.insetTop - self.insetBottom) + } + oldScreenHeight = newScreenHeight + adaptItems(listState.items, false) + if let index = scrollToItemIndexDelayed { + scrollToItem(index) + scrollToItemIndexDelayed = nil + } + } + + private func setup() { + contentSize = CGSizeMake(frame.size.width, initialOffset * 2) + prevProcessedOffset = initialOffset + contentOffset = CGPointMake(0, initialOffset) + + showsVerticalScrollIndicator = false + scrollBarView.showsHorizontalScrollIndicator = false + panGestureRecognizer.delegate = self + addGestureRecognizer(scrollBarView.panGestureRecognizer) + superview!.addSubview(scrollBarView) + } + + func updateItems(_ items: [ScrollItem], _ forceReloadVisible: Bool = false) { + if !Thread.isMainThread { + logger.error("Use main thread to update items") + return + } + if bounds.height == 0 { + self.listState.items = items + // this function requires to have valid bounds and it will be called again once it has them + return + } + adaptItems(items, forceReloadVisible) + snapToContent(animated: false) + } + + /// [forceReloadVisible]: reloads every item that was visible regardless of hashValue changes + private func adaptItems(_ items: [ScrollItem], _ forceReloadVisible: Bool, overridenOffset: CGFloat? = nil) { + let start = Date.now + // special case when everything was removed + if items.isEmpty { + listState.visibleItems.forEach { item in item.view.removeFromSuperview() } + listState.visibleItems = [] + listState.itemsCanCoverScreen = false + listState.firstVisibleItemId = EndlessScrollView<ScrollItem>.DEFAULT_ITEM_ID + listState.firstVisibleItemIndex = 0 + listState.firstVisibleItemOffset = -insetTop + + estimatedContentHeight.update(contentOffset, listState, averageItemHeight, true) + scrollBarView.contentSize = .zero + scrollBarView.contentOffset = .zero + + prevProcessedOffset = contentOffset.y + // this check is just to prevent didSet listener from firing on the same empty array, no use for this + if !self.listState.items.isEmpty { + self.listState.items = items + } + return + } + + let contentOffsetY = overridenOffset ?? contentOffset.y + + var oldVisible = listState.visibleItems + var newVisible: [VisibleItem] = [] + var visibleItemsHeight: CGFloat = 0 + let offsetsDiff = contentOffsetY - prevProcessedOffset + + var shouldBeFirstVisible = items.firstIndex(where: { item in item.id == listState.firstVisibleItemId as! ScrollItem.ID }) ?? 0 + + var wasFirstVisibleItemOffset = listState.firstVisibleItemOffset + var alreadyChangedIndexWhileScrolling = false + var allowOneMore = false + var nextOffsetY: CGFloat = 0 + var i = shouldBeFirstVisible + // building list of visible items starting from the first one that should be visible + while i >= 0 && i < items.count { + let item = items[i] + let visibleIndex = oldVisible.firstIndex(where: { vis in vis.item.id == item.id }) + let visible: VisibleItem? + if let visibleIndex { + let v = oldVisible.remove(at: visibleIndex) + if forceReloadVisible || v.view.bounds.width != bounds.width || v.item.hashValue != item.hashValue { + let wasHeight = v.view.bounds.height + updateCell(v.view, i, items) + if wasHeight < v.view.bounds.height && i == 0 && shouldBeFirstVisible == i { + v.view.frame.origin.y -= v.view.bounds.height - wasHeight + } + } + visible = v + } else { + visible = nil + } + if shouldBeFirstVisible == i { + if let vis = visible { + + if // there is auto scroll in progress and the first item has a higher offset than bottom part + // of the screen. In order to make scrolling down & up equal in time, we treat this as a sign to + // re-make the first visible item + (listState.isAnimatedScrolling && vis.view.frame.origin.y + vis.view.bounds.height < contentOffsetY + bounds.height) || + // the fist visible item previously is hidden now, remove it and move on + !isVisible(vis.view) { + let newIndex: Int + if listState.isAnimatedScrolling { + // skip many items to make the scrolling take less time + var indexDiff = !alreadyChangedIndexWhileScrolling ? Int(ceil(abs(offsetsDiff / averageItemHeight))) : 0 + // if index was already changed, no need to change it again. Otherwise, the scroll will overscoll and return back animated. Because it means the whole screen was scrolled + alreadyChangedIndexWhileScrolling = true + + indexDiff = offsetsDiff <= 0 ? indexDiff : -indexDiff + newIndex = max(0, min(items.count - 1, i + indexDiff)) + // offset for the first visible item can now be 0 because the previous first visible item doesn't exist anymore + wasFirstVisibleItemOffset = 0 + } else { + // don't skip multiple items if it's manual scrolling gesture + newIndex = i + (offsetsDiff <= 0 ? 1 : -1) + } + shouldBeFirstVisible = newIndex + i = newIndex + + cellsToReuse.append(vis.view) + hideAndRemoveFromSuperviewIfNeeded(vis.view) + continue + } + } + let vis: VisibleItem + if let visible { + vis = VisibleItem(index: i, item: item, view: visible.view, offset: offsetToBottom(visible.view)) + } else { + let cell = createCell(i, items, &cellsToReuse)! + cell.frame.origin.y = bottomY + wasFirstVisibleItemOffset - cell.frame.height + vis = VisibleItem(index: i, item: item, view: cell, offset: offsetToBottom(cell)) + } + if vis.view.superview == nil { + addSubview(vis.view) + } + newVisible.append(vis) + visibleItemsHeight += vis.view.frame.height + nextOffsetY = vis.view.frame.origin.y + } else { + let vis: VisibleItem + if let visible { + vis = VisibleItem(index: i, item: item, view: visible.view, offset: offsetToBottom(visible.view)) + nextOffsetY -= vis.view.frame.height + vis.view.frame.origin.y = nextOffsetY + } else { + let cell = createCell(i, items, &cellsToReuse)! + nextOffsetY -= cell.frame.height + cell.frame.origin.y = nextOffsetY + vis = VisibleItem(index: i, item: item, view: cell, offset: offsetToBottom(cell)) + } + if vis.view.superview == nil { + addSubview(vis.view) + } + newVisible.append(vis) + visibleItemsHeight += vis.view.frame.height + } + if abs(nextOffsetY) < contentOffsetY && !allowOneMore { + break + } else if abs(nextOffsetY) < contentOffsetY { + allowOneMore = false + } + i += 1 + } + if let firstVisible = newVisible.first, firstVisible.view.frame.origin.y + firstVisible.view.frame.height < contentOffsetY + bounds.height, firstVisible.index > 0 { + var offset: CGFloat = firstVisible.view.frame.origin.y + firstVisible.view.frame.height + let index = firstVisible.index + for i in stride(from: index - 1, through: 0, by: -1) { + let item = items[i] + let visibleIndex = oldVisible.firstIndex(where: { vis in vis.item.id == item.id }) + let vis: VisibleItem + if let visibleIndex { + let visible = oldVisible.remove(at: visibleIndex) + visible.view.frame.origin.y = offset + vis = VisibleItem(index: i, item: item, view: visible.view, offset: offsetToBottom(visible.view)) + } else { + let cell = createCell(i, items, &cellsToReuse)! + cell.frame.origin.y = offset + vis = VisibleItem(index: i, item: item, view: cell, offset: offsetToBottom(cell)) + } + if vis.view.superview == nil { + addSubview(vis.view) + } + offset += vis.view.frame.height + newVisible.insert(vis, at: 0) + visibleItemsHeight += vis.view.frame.height + if offset >= contentOffsetY + bounds.height { + break + } + } + } + + // removing already unneeded visible items + oldVisible.forEach { vis in + cellsToReuse.append(vis.view) + hideAndRemoveFromSuperviewIfNeeded(vis.view) + } + let itemsCountChanged = listState.items.count != items.count + prevProcessedOffset = contentOffsetY + + listState.visibleItems = newVisible + // bottom drawing starts from 0 until top visible area at least (bound.height - insetTop) or above top bar (bounds.height). + // For visible items to preserve offset after adding more items having such height is enough + listState.itemsCanCoverScreen = visibleItemsHeight >= bounds.height - insetTop + + listState.firstVisibleItemId = listState.visibleItems.first?.item.id ?? EndlessScrollView<ScrollItem>.DEFAULT_ITEM_ID + listState.firstVisibleItemIndex = listState.visibleItems.first?.index ?? 0 + listState.firstVisibleItemOffset = listState.visibleItems.first?.offset ?? -insetTop + // updating the items with the last step in order to call listener with fully updated state + listState.items = items + + estimatedContentHeight.update(contentOffset, listState, averageItemHeight, itemsCountChanged) + scrollBarView.contentSize = CGSizeMake(bounds.width, estimatedContentHeight.virtualOverscrolledHeight) + scrollBarView.contentOffset = CGPointMake(0, estimatedContentHeight.virtualScrollOffsetY) + scrollBarView.isHidden = listState.visibleItems.count == listState.items.count && (listState.visibleItems.isEmpty || -listState.firstVisibleItemOffset + (listState.visibleItems.last?.offset ?? 0) + insetTop < bounds.height) + + if debug { + println("time spent \((-start.timeIntervalSinceNow).description.prefix(5).replacingOccurrences(of: "0.000", with: "<0").replacingOccurrences(of: "0.", with: ""))") + } + } + + func setScrollPosition(_ index: Int, _ id: Int64, _ offset: CGFloat = 0) { + listState.firstVisibleItemIndex = index + listState.firstVisibleItemId = id + listState.firstVisibleItemOffset = offset == 0 ? -bounds.height + insetTop + insetBottom : offset + } + + func scrollToItem(_ index: Int, top: Bool = true) { + if index >= listState.items.count || listState.isScrolling || listState.isAnimatedScrolling { + return + } + if bounds.height == 0 || contentSize.height == 0 { + scrollToItemIndexDelayed = index + return + } + listState.isScrolling = true + defer { + listState.isScrolling = false + } + + // just a faster way to set top item as requested index + listState.firstVisibleItemIndex = index + listState.firstVisibleItemId = listState.items[index].id + listState.firstVisibleItemOffset = -bounds.height + insetTop + insetBottom + scrollBarView.flashScrollIndicators() + adaptItems(listState.items, false) + + var adjustedOffset = self.contentOffset.y + var i = 0 + + var upPrev = index > listState.firstVisibleItemIndex + //let firstOrLastIndex = upPrev ? listState.visibleItems.last?.index ?? 0 : listState.firstVisibleItemIndex + //let step: CGFloat = max(0.1, CGFloat(abs(index - firstOrLastIndex)) * scrollStepMultiplier) + + var stepSlowdownMultiplier: CGFloat = 1 + while i < 200 { + let up = index > listState.firstVisibleItemIndex + if upPrev != up { + stepSlowdownMultiplier = stepSlowdownMultiplier * 0.5 + upPrev = up + } + + // these two lines makes scrolling's finish non-linear and NOT overscroll visually when reach target index + let firstOrLastIndex = up ? listState.visibleItems.last?.index ?? 0 : listState.firstVisibleItemIndex + let step: CGFloat = max(0.1, CGFloat(abs(index - firstOrLastIndex)) * scrollStepMultiplier) * stepSlowdownMultiplier + + let offsetToScroll = (up ? -averageItemHeight : averageItemHeight) * step + adjustedOffset += offsetToScroll + if let item = listState.visibleItems.first(where: { $0.index == index }) { + let y = if top { + min(estimatedContentHeight.bottomOffsetY - bounds.height, item.view.frame.origin.y - insetTop) + } else { + max(estimatedContentHeight.topOffsetY - insetTop - insetBottom, item.view.frame.origin.y + item.view.bounds.height - bounds.height + insetBottom) + } + setContentOffset(CGPointMake(contentOffset.x, y), animated: false) + scrollBarView.flashScrollIndicators() + break + } + contentOffset = CGPointMake(contentOffset.x, adjustedOffset) + adaptItems(listState.items, false) + snapToContent(animated: false) + i += 1 + } + adaptItems(listState.items, false) + snapToContent(animated: false) + estimatedContentHeight.update(contentOffset, listState, averageItemHeight, true) + } + + func scrollToItemAnimated(_ index: Int, top: Bool = true) async { + if index >= listState.items.count || listState.isScrolling || listState.isAnimatedScrolling { + return + } + listState.isAnimatedScrolling = true + defer { + listState.isAnimatedScrolling = false + } + var adjustedOffset = self.contentOffset.y + var i = 0 + + var upPrev = index > listState.firstVisibleItemIndex + //let firstOrLastIndex = upPrev ? listState.visibleItems.last?.index ?? 0 : listState.firstVisibleItemIndex + //let step: CGFloat = max(0.1, CGFloat(abs(index - firstOrLastIndex)) * scrollStepMultiplier) + + var stepSlowdownMultiplier: CGFloat = 1 + while i < 200 { + let up = index > listState.firstVisibleItemIndex + if upPrev != up { + stepSlowdownMultiplier = stepSlowdownMultiplier * 0.5 + upPrev = up + } + + // these two lines makes scrolling's finish non-linear and NOT overscroll visually when reach target index + let firstOrLastIndex = up ? listState.visibleItems.last?.index ?? 0 : listState.firstVisibleItemIndex + let step: CGFloat = max(0.1, CGFloat(abs(index - firstOrLastIndex)) * scrollStepMultiplier) * stepSlowdownMultiplier + + //println("Scrolling step \(step) \(stepSlowdownMultiplier) index \(index) \(firstOrLastIndex) \(index - firstOrLastIndex) \(adjustedOffset), up \(up), i \(i)") + + let offsetToScroll = (up ? -averageItemHeight : averageItemHeight) * step + adjustedOffset += offsetToScroll + if let item = listState.visibleItems.first(where: { $0.index == index }) { + let y = if top { + min(estimatedContentHeight.bottomOffsetY - bounds.height, item.view.frame.origin.y - insetTop) + } else { + max(estimatedContentHeight.topOffsetY - insetTop - insetBottom, item.view.frame.origin.y + item.view.bounds.height - bounds.height + insetBottom) + } + setContentOffset(CGPointMake(contentOffset.x, y), animated: true) + scrollBarView.flashScrollIndicators() + break + } + contentOffset = CGPointMake(contentOffset.x, adjustedOffset) + + // skipping unneded relayout if this offset is already processed + if prevProcessedOffset - contentOffset.y != 0 { + adaptItems(listState.items, false) + snapToContent(animated: false) + } + // let UI time to update to see the animated position change + await MainActor.run {} + + i += 1 + } + estimatedContentHeight.update(contentOffset, listState, averageItemHeight, true) + } + + func scrollToBottom() { + scrollToItem(0, top: false) + } + + func scrollToBottomAnimated() { + Task { + await scrollToItemAnimated(0, top: false) + } + } + + func scroll(by: CGFloat, animated: Bool = true) { + setContentOffset(CGPointMake(contentOffset.x, contentOffset.y + by), animated: animated) + } + + func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { + if !listState.items.isEmpty { + scrollToBottomAnimated() + } + return false + } + + private func snapToContent(animated: Bool) { + let topBlankSpace = estimatedContentHeight.height < bounds.height ? bounds.height - estimatedContentHeight.height : 0 + if topY < estimatedContentHeight.topOffsetY - topBlankSpace { + setContentOffset(CGPointMake(0, estimatedContentHeight.topOffsetY - topBlankSpace), animated: animated) + } else if bottomY > estimatedContentHeight.bottomOffsetY { + setContentOffset(CGPointMake(0, estimatedContentHeight.bottomOffsetY - bounds.height), animated: animated) + } + } + + func offsetToBottom(_ view: UIView) -> CGFloat { + bottomY - (view.frame.origin.y + view.frame.height) + } + + /// If I try to .removeFromSuperview() right when I need to remove the view, it is possible to crash the app when the view was hidden in result of + /// pressing Hide in menu on top of the revealed item within the group. So at that point the item should still be attached to the view + func hideAndRemoveFromSuperviewIfNeeded(_ view: UIView) { + if view.isHidden { + // already passed this function + return + } + (view as? ReusableView)?.prepareForReuse() + view.isHidden = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + if view.isHidden { view.removeFromSuperview() } + } + } + + /// Synchronizing both scrollViews + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + true + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !decelerate { + snapToContent(animated: true) + } + } + + override var contentOffset: CGPoint { + get { super.contentOffset } + set { + var newOffset = newValue + let topBlankSpace = estimatedContentHeight.height < bounds.height ? bounds.height - estimatedContentHeight.height : 0 + if contentOffset.y > 0 && newOffset.y < estimatedContentHeight.topOffsetY - topBlankSpace && contentOffset.y > newOffset.y { + if !isDecelerating { + newOffset.y = min(contentOffset.y, newOffset.y + abs(newOffset.y - estimatedContentHeight.topOffsetY + topBlankSpace) / 1.8) + } else { + DispatchQueue.main.async { + self.setContentOffset(newValue, animated: false) + self.snapToContent(animated: true) + } + } + } else if contentOffset.y > 0 && newOffset.y + bounds.height > estimatedContentHeight.bottomOffsetY && contentOffset.y < newOffset.y { + if !isDecelerating { + newOffset.y = max(contentOffset.y, newOffset.y - abs(newOffset.y + bounds.height - estimatedContentHeight.bottomOffsetY) / 1.8) + } else { + DispatchQueue.main.async { + self.setContentOffset(newValue, animated: false) + self.snapToContent(animated: true) + } + } + } + super.contentOffset = newOffset + } + } + + private func stopScrolling() { + let offsetYToStopAt = if abs(contentOffset.y - estimatedContentHeight.topOffsetY) < abs(bottomY - estimatedContentHeight.bottomOffsetY) { + estimatedContentHeight.topOffsetY + } else { + estimatedContentHeight.bottomOffsetY - bounds.height + } + setContentOffset(CGPointMake(contentOffset.x, offsetYToStopAt), animated: false) + } + + func isVisible(_ view: UIView) -> Bool { + if view.superview == nil { + return false + } + return view.frame.intersects(CGRectMake(0, contentOffset.y, bounds.width, bounds.height)) + } +} + +private func println(_ text: String) { + print("\(Date.now.timeIntervalSince1970): \(text)") +} diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index b89c006c61..7cd543af10 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -21,6 +21,7 @@ struct AddGroupMembersView: View { struct AddGroupMembersViewCommon: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme var chat: Chat @State var groupInfo: GroupInfo var creatingGroup: Bool = false @@ -34,7 +35,7 @@ struct AddGroupMembersViewCommon: View { private enum AddGroupMembersAlert: Identifiable { case prohibitedToInviteIncognito - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -46,14 +47,13 @@ struct AddGroupMembersViewCommon: View { var body: some View { if creatingGroup { - NavigationView { - addGroupMembersView() - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button ("Skip") { addedMembersCb(selectedContacts) } - } + addGroupMembersView() + .navigationBarBackButtonHidden() + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button ("Skip") { addedMembersCb(selectedContacts) } } - } + } } else { addGroupMembersView() } @@ -70,7 +70,7 @@ struct AddGroupMembersViewCommon: View { if (membersToAdd.isEmpty) { Text("No contacts to add") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .padding() .frame(maxWidth: .infinity, alignment: .center) .listRowBackground(Color.clear) @@ -78,7 +78,12 @@ struct AddGroupMembersViewCommon: View { let count = selectedContacts.count Section { if creatingGroup { - groupPreferencesButton($groupInfo, true) + GroupPreferencesButton( + groupInfo: $groupInfo, + preferences: groupInfo.fullGroupPreferences, + currentPreferences: groupInfo.fullGroupPreferences, + creatingGroup: true + ) } rolePicker() inviteMembersButton() @@ -90,21 +95,25 @@ struct AddGroupMembersViewCommon: View { Button { selectedContacts.removeAll() } label: { Text("Clear").font(.caption) } Spacer() Text("\(count) contact(s) selected") + .foregroundColor(theme.colors.secondary) } } else { Text("No contacts selected") .frame(maxWidth: .infinity, alignment: .trailing) + .foregroundColor(theme.colors.secondary) } } } Section { - searchFieldView(text: $searchText, focussed: $searchFocussed) + searchFieldView(text: $searchText, focussed: $searchFocussed, theme.colors.primary, theme.colors.secondary) .padding(.leading, 2) let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase let members = s == "" ? membersToAdd : membersToAdd.filter { $0.chatViewName.localizedLowercase.contains(s) } - ForEach(members) { contact in - contactCheckView(contact) + ForEach(members + [dummyContact]) { contact in + if contact.contactId != dummyContact.contactId { + contactCheckView(contact) + } } } } @@ -119,20 +128,30 @@ struct AddGroupMembersViewCommon: View { message: Text("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") ) case let .error(title, error): - return Alert(title: Text(title), message: Text(error)) + return mkAlert(title: title, message: error) } } .onChange(of: selectedContacts) { _ in searchFocussed = false } + .modifier(ThemedBackground(grouped: true)) } + // Resolves keyboard losing focus bug in iOS16 and iOS17, + // when there are no items inside `ForEach(memebers)` loop + private let dummyContact: Contact = { + var dummy = Contact.sampleData + dummy.contactId = -1 + return dummy + }() + private func inviteMembersButton() -> some View { - Button { + let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Invite to group" : "Invite to chat" + return Button { inviteMembers() } label: { HStack { - Text("Invite to group") + Text(label) Image(systemName: "checkmark") } } @@ -156,10 +175,8 @@ struct AddGroupMembersViewCommon: View { private func rolePicker() -> some View { Picker("New member role", selection: $selectedRole) { - ForEach(GroupMemberRole.allCases) { role in - if role <= groupInfo.membership.memberRole && role != .author { - Text(role.text) - } + ForEach(GroupMemberRole.supportedRoles.filter({ $0 <= groupInfo.membership.memberRole })) { role in + Text(role.text) } } .frame(height: 36) @@ -172,14 +189,14 @@ struct AddGroupMembersViewCommon: View { var iconColor: Color if prohibitedToInviteIncognito { icon = "theatermasks.circle.fill" - iconColor = Color(uiColor: .tertiaryLabel) + iconColor = Color(uiColor: .tertiaryLabel).asAnotherColorFromSecondary(theme) } else { if checked { icon = "checkmark.circle.fill" - iconColor = .accentColor + iconColor = theme.colors.primary } else { icon = "circle" - iconColor = Color(uiColor: .tertiaryLabel) + iconColor = Color(uiColor: .tertiaryLabel).asAnotherColorFromSecondary(theme) } } return Button { @@ -194,11 +211,10 @@ struct AddGroupMembersViewCommon: View { } } label: { HStack{ - ProfileImage(imageStr: contact.image) - .frame(width: 30, height: 30) + ProfileImage(imageStr: contact.image, size: 30) .padding(.trailing, 2) Text(ChatInfo.direct(contact: contact).chatViewName) - .foregroundColor(prohibitedToInviteIncognito ? .secondary : .primary) + .foregroundColor(prohibitedToInviteIncognito ? theme.colors.secondary : theme.colors.onBackground) .lineLimit(1) Spacer() Image(systemName: icon) @@ -208,7 +224,7 @@ struct AddGroupMembersViewCommon: View { } } -func searchFieldView(text: Binding<String>, focussed: FocusState<Bool>.Binding) -> some View { +func searchFieldView(text: Binding<String>, focussed: FocusState<Bool>.Binding, _ onBackgroundColor: Color, _ secondaryColor: Color) -> some View { HStack { Image(systemName: "magnifyingglass") .resizable() @@ -217,8 +233,9 @@ func searchFieldView(text: Binding<String>, focussed: FocusState<Bool>.Binding) .padding(.trailing, 10) TextField("Search", text: text) .focused(focussed) - .foregroundColor(.primary) + .foregroundColor(onBackgroundColor) .frame(maxWidth: .infinity) + .autocorrectionDisabled(true) Image(systemName: "xmark.circle.fill") .resizable() .scaledToFit() @@ -229,7 +246,7 @@ func searchFieldView(text: Binding<String>, focussed: FocusState<Bool>.Binding) focussed.wrappedValue = false } } - .foregroundColor(.secondary) + .foregroundColor(secondaryColor) .frame(height: 36) } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 09ead880ad..15749b0761 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -13,17 +13,23 @@ let SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20 struct GroupChatInfoView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction @ObservedObject var chat: Chat @Binding var groupInfo: GroupInfo + var onSearch: () -> Void + @State var localAlias: String + @FocusState private var aliasTextFieldFocused: Bool @State private var alert: GroupChatInfoViewAlert? = nil - @State private var groupLink: String? + @State private var groupLink: CreatedConnLink? @State private var groupLinkMemberRole: GroupMemberRole = .member - @State private var showAddMembersSheet: Bool = false + @State private var groupLinkNavLinkActive: Bool = false + @State private var addMembersNavLinkActive: Bool = false @State private var connectionStats: ConnectionStats? @State private var connectionCode: String? @State private var sendReceipts = SendReceipts.userDefault(true) @State private var sendReceiptsUserDefault = true + @State private var progressIndicator = false @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @State private var searchText: String = "" @FocusState private var searchFocussed @@ -36,8 +42,10 @@ struct GroupChatInfoView: View { case largeGroupReceiptsDisabled case blockMemberAlert(mem: GroupMember) case unblockMemberAlert(mem: GroupMember) + case blockForAllAlert(mem: GroupMember) + case unblockForAllAlert(mem: GroupMember) case removeMemberAlert(mem: GroupMember) - case error(title: LocalizedStringKey, error: LocalizedStringKey) + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -48,6 +56,8 @@ struct GroupChatInfoView: View { case .largeGroupReceiptsDisabled: return "largeGroupReceiptsDisabled" case let .blockMemberAlert(mem): return "blockMemberAlert \(mem.groupMemberId)" case let .unblockMemberAlert(mem): return "unblockMemberAlert \(mem.groupMemberId)" + case let .blockForAllAlert(mem): return "blockForAllAlert \(mem.groupMemberId)" + case let .unblockForAllAlert(mem): return "unblockForAllAlert \(mem.groupMemberId)" case let .removeMemberAlert(mem): return "removeMemberAlert \(mem.groupMemberId)" case let .error(title, _): return "error \(title)" } @@ -58,80 +68,114 @@ struct GroupChatInfoView: View { NavigationView { let members = chatModel.groupMembers .filter { m in let status = m.wrapped.memberStatus; return status != .memLeft && status != .memRemoved } - .sorted { $0.displayName.lowercased() < $1.displayName.lowercased() } + .sorted { $0.wrapped.memberRole > $1.wrapped.memberRole } - List { - groupInfoHeader() - .listRowBackground(Color.clear) - - Section { - if groupInfo.canEdit { - editGroupButton() - } - if groupInfo.groupProfile.description != nil || groupInfo.canEdit { - addOrEditWelcomeMessage() - } - groupPreferencesButton($groupInfo) - if members.filter({ $0.wrapped.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT { - sendReceiptsOption() - } else { - sendReceiptsOptionDisabled() - } - } header: { - Text("") - } footer: { - Text("Only group owners can change group preferences.") - } - - Section("\(members.count + 1) members") { - if groupInfo.canAddMembers { - groupLinkButton() - if (chat.chatInfo.incognito) { - Label("Invite members", systemImage: "plus") - .foregroundColor(Color(uiColor: .tertiaryLabel)) - .onTapGesture { alert = .cantInviteIncognitoAlert } + ZStack { + List { + groupInfoHeader() + .listRowBackground(Color.clear) + + localAliasTextEdit() + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .padding(.bottom, 18) + + infoActionButtons() + .padding(.horizontal) + .frame(maxWidth: .infinity) + .frame(height: infoViewActionButtonHeight) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + + Section { + if groupInfo.isOwner && groupInfo.businessChat == nil { + editGroupButton() + } + if groupInfo.groupProfile.description != nil || (groupInfo.isOwner && groupInfo.businessChat == nil) { + addOrEditWelcomeMessage() + } + GroupPreferencesButton(groupInfo: $groupInfo, preferences: groupInfo.fullGroupPreferences, currentPreferences: groupInfo.fullGroupPreferences) + if members.filter({ $0.wrapped.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT { + sendReceiptsOption() } else { - addMembersButton() + sendReceiptsOptionDisabled() } + + NavigationLink { + ChatWallpaperEditorSheet(chat: chat) + } label: { + Label("Chat theme", systemImage: "photo") + } + } header: { + Text("") + } footer: { + 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) } - if members.count > 8 { - searchFieldView(text: $searchText, focussed: $searchFocussed) - .padding(.leading, 8) + + Section { + ChatTTLOption(chat: chat, progressIndicator: $progressIndicator) + } footer: { + Text("Delete chat messages from your device.") } - let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase - let filteredMembers = s == "" ? members : members.filter { $0.wrapped.chatViewName.localizedLowercase.contains(s) } - MemberRowView(groupInfo: groupInfo, groupMember: GMember(groupInfo.membership), user: true, alert: $alert) - ForEach(filteredMembers) { member in - ZStack { - NavigationLink { - memberInfoView(member) - } label: { - EmptyView() + + Section(header: Text("\(members.count + 1) members").foregroundColor(theme.colors.secondary)) { + if groupInfo.canAddMembers { + if groupInfo.businessChat == nil { + groupLinkButton() } - .opacity(0) - MemberRowView(groupInfo: groupInfo, groupMember: member, alert: $alert) + if (chat.chatInfo.incognito) { + Label("Invite members", systemImage: "plus") + .foregroundColor(Color(uiColor: .tertiaryLabel)) + .onTapGesture { alert = .cantInviteIncognitoAlert } + } else { + addMembersButton() + } + } + searchFieldView(text: $searchText, focussed: $searchFocussed, theme.colors.onBackground, theme.colors.secondary) + .padding(.leading, 8) + let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase + let filteredMembers = s == "" + ? members + : members.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) } + MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: GMember(groupInfo.membership), user: true, alert: $alert) + ForEach(filteredMembers) { member in + MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: member, alert: $alert) + } + } + + Section { + clearChatButton() + if groupInfo.canDelete { + deleteGroupButton() + } + if groupInfo.membership.memberCurrent { + leaveGroupButton() + } + } + + if developerTools { + Section(header: Text("For console").foregroundColor(theme.colors.secondary)) { + infoRow("Local name", chat.chatInfo.localDisplayName) + infoRow("Database ID", "\(chat.chatInfo.apiId)") } } } - - Section { - clearChatButton() - if groupInfo.canDelete { - deleteGroupButton() - } - if groupInfo.membership.memberCurrent { - leaveGroupButton() - } - } - - if developerTools { - Section(header: Text("For console")) { - infoRow("Local name", chat.chatInfo.localDisplayName) - infoRow("Database ID", "\(chat.chatInfo.apiId)") - } + .modifier(ThemedBackground(grouped: true)) + .navigationBarHidden(true) + .disabled(progressIndicator) + .opacity(progressIndicator ? 0.6 : 1) + + if progressIndicator { + ProgressView().scaleEffect(2) } } - .navigationBarHidden(true) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .alert(item: $alert) { alertItem in @@ -143,8 +187,10 @@ struct GroupChatInfoView: View { case .largeGroupReceiptsDisabled: return largeGroupReceiptsDisabledAlert() case let .blockMemberAlert(mem): return blockMemberAlert(groupInfo, mem) case let .unblockMemberAlert(mem): return unblockMemberAlert(groupInfo, mem) + case let .blockForAllAlert(mem): return blockForAllAlert(groupInfo, mem) + case let .unblockForAllAlert(mem): return unblockForAllAlert(groupInfo, mem) case let .removeMemberAlert(mem): return removeMemberAlert(mem) - case let .error(title, error): return Alert(title: Text(title), message: Text(error)) + case let .error(title, error): return mkAlert(title: title, message: error) } } .onAppear { @@ -160,17 +206,15 @@ struct GroupChatInfoView: View { logger.error("GroupChatInfoView apiGetGroupLink: \(responseError(error))") } } - .keyboardPadding() } private func groupInfoHeader() -> some View { VStack { let cInfo = chat.chatInfo - ChatInfoImage(chat: chat, color: Color(uiColor: .tertiarySystemFill)) - .frame(width: 192, height: 192) + ChatInfoImage(chat: chat, size: 192, color: Color(uiColor: .tertiarySystemFill)) .padding(.top, 12) .padding() - Text(cInfo.displayName) + Text(cInfo.groupInfo?.groupProfile.displayName ?? cInfo.displayName) .font(.largeTitle) .multilineTextAlignment(.center) .lineLimit(4) @@ -185,60 +229,216 @@ struct GroupChatInfoView: View { .frame(maxWidth: .infinity, alignment: .center) } - private func addMembersButton() -> some View { - NavigationLink { - AddGroupMembersView(chat: chat, groupInfo: groupInfo) - .onAppear { - searchFocussed = false - Task { - let groupMembers = await apiListMembers(groupInfo.groupId) - await MainActor.run { - chatModel.groupMembers = groupMembers.map { GMember.init($0) } - } + private func localAliasTextEdit() -> some View { + TextField("Set chat name…", text: $localAlias) + .disableAutocorrection(true) + .focused($aliasTextFieldFocused) + .submitLabel(.done) + .onChange(of: aliasTextFieldFocused) { focused in + if !focused { + setGroupAlias() + } + } + .onSubmit { + setGroupAlias() + } + .multilineTextAlignment(.center) + .foregroundColor(theme.colors.secondary) + } + + private func setGroupAlias() { + Task { + do { + if let gInfo = try await apiSetGroupAlias(groupId: chat.chatInfo.apiId, localAlias: localAlias) { + await MainActor.run { + chatModel.updateGroup(gInfo) } } - } label: { - Label("Invite members", systemImage: "plus") + } catch { + logger.error("setGroupAlias error: \(responseError(error))") + } + } + } + + func infoActionButtons() -> some View { + GeometryReader { g in + let buttonWidth = g.size.width / 4 + HStack(alignment: .center, spacing: 8) { + searchButton(width: buttonWidth) + if groupInfo.canAddMembers { + addMembersActionButton(width: buttonWidth) + } + if let nextNtfMode = chat.chatInfo.nextNtfMode { + muteButton(width: buttonWidth, nextNtfMode: nextNtfMode) + } + } + .frame(maxWidth: .infinity, alignment: .center) } } + private func searchButton(width: CGFloat) -> some View { + InfoViewButton(image: "magnifyingglass", title: "search", width: width) { + dismiss() + onSearch() + } + .disabled(!groupInfo.ready || chat.chatItems.isEmpty) + } + + private func addMembersActionButton(width: CGFloat) -> some View { + ZStack { + if chat.chatInfo.incognito { + InfoViewButton(image: "link.badge.plus", title: "invite", width: width) { + groupLinkNavLinkActive = true + } + + NavigationLink(isActive: $groupLinkNavLinkActive) { + groupLinkDestinationView() + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } else { + InfoViewButton(image: "person.fill.badge.plus", title: "invite", width: width) { + addMembersNavLinkActive = true + } + + NavigationLink(isActive: $addMembersNavLinkActive) { + addMembersDestinationView() + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } + } + .disabled(!groupInfo.ready) + } + + private func muteButton(width: CGFloat, nextNtfMode: MsgFilter) -> some View { + return InfoViewButton( + image: nextNtfMode.iconFilled, + title: "\(nextNtfMode.text(mentions: true))", + width: width + ) { + toggleNotifications(chat, enableNtfs: nextNtfMode) + } + .disabled(!groupInfo.ready) + } + + private func addMembersButton() -> some View { + let label: LocalizedStringKey = switch groupInfo.businessChat?.chatType { + case .customer: "Add team members" + case .business: "Add friends" + case .none: "Invite members" + } + return NavigationLink { + addMembersDestinationView() + } label: { + Label(label, systemImage: "plus") + } + } + + private func addMembersDestinationView() -> some View { + AddGroupMembersView(chat: chat, groupInfo: groupInfo) + .onAppear { + searchFocussed = false + Task { + await chatModel.loadGroupMembers(groupInfo) + } + } + } + private struct MemberRowView: View { + var chat: Chat var groupInfo: GroupInfo @ObservedObject var groupMember: GMember + @EnvironmentObject var theme: AppTheme var user: Bool = false @Binding var alert: GroupChatInfoViewAlert? var body: some View { let member = groupMember.wrapped - let v = HStack{ - ProfileImage(imageStr: member.image) - .frame(width: 38, height: 38) + let v1 = HStack{ + MemberProfileImage(member, size: 38) .padding(.trailing, 2) // TODO server connection status VStack(alignment: .leading) { - let t = Text(member.chatViewName).foregroundColor(member.memberIncognito ? .indigo : .primary) + let t = Text(member.chatViewName).foregroundColor(member.memberIncognito ? .indigo : theme.colors.onBackground) (member.verified ? memberVerifiedShield + t : t) .lineLimit(1) - let s = Text(member.memberStatus.shortText) - (user ? Text ("you: ") + s : s) + (user ? Text ("you: ") + Text(member.memberStatus.shortText) : Text(memberConnStatus(member))) .lineLimit(1) .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } Spacer() - let role = member.memberRole - if role == .owner || role == .admin { - Text(member.memberRole.text) - .foregroundColor(.secondary) + memberInfo(member) + } + + let v = ZStack { + if user { + v1 + } else { + NavigationLink { + memberInfoView() + } label: { + EmptyView() + } + .opacity(0) + v1 } } if user { v - } else if member.canBeRemoved(groupInfo: groupInfo) { - removeSwipe(member, blockSwipe(member, v)) + } else if groupInfo.membership.memberRole >= .admin { + // TODO if there are more actions, refactor with lists of swipeActions + let canBlockForAll = member.canBlockForAll(groupInfo: groupInfo) + let canRemove = member.canBeRemoved(groupInfo: groupInfo) + if canBlockForAll && canRemove { + removeSwipe(member, blockForAllSwipe(member, v)) + } else if canBlockForAll { + blockForAllSwipe(member, v) + } else if canRemove { + removeSwipe(member, v) + } else { + v + } } else { - blockSwipe(member, v) + if !member.blockedByAdmin { + blockSwipe(member, v) + } else { + v + } + } + } + + private func memberInfoView() -> some View { + GroupMemberInfoView(groupInfo: groupInfo, chat: chat, groupMember: groupMember) + .navigationBarHidden(false) + } + + private func memberConnStatus(_ member: GroupMember) -> LocalizedStringKey { + if member.activeConn?.connDisabled ?? false { + return "disabled" + } else if member.activeConn?.connInactive ?? false { + return "inactive" + } else { + return member.memberStatus.shortText + } + } + + @ViewBuilder private func memberInfo(_ member: GroupMember) -> some View { + if member.blocked { + Text("blocked") + .foregroundColor(theme.colors.secondary) + } else { + let role = member.memberRole + if [.owner, .admin, .observer].contains(role) { + Text(member.memberRole.text) + .foregroundColor(theme.colors.secondary) + } } } @@ -248,13 +448,31 @@ struct GroupChatInfoView: View { Button { alert = .blockMemberAlert(mem: member) } label: { - Label("Block member", systemImage: "hand.raised").foregroundColor(.secondary) + Label("Block member", systemImage: "hand.raised").foregroundColor(theme.colors.secondary) } } else { Button { alert = .unblockMemberAlert(mem: member) } label: { - Label("Unblock member", systemImage: "hand.raised.slash").foregroundColor(.accentColor) + Label("Unblock member", systemImage: "hand.raised.slash").foregroundColor(theme.colors.primary) + } + } + } + } + + private func blockForAllSwipe<V: View>(_ member: GroupMember, _ v: V) -> some View { + v.swipeActions(edge: .leading) { + if member.blockedByAdmin { + Button { + alert = .unblockForAllAlert(mem: member) + } label: { + Label("Unblock for all", systemImage: "hand.raised.slash").foregroundColor(theme.colors.primary) + } + } else { + Button { + alert = .blockForAllAlert(mem: member) + } label: { + Label("Block for all", systemImage: "hand.raised").foregroundColor(theme.colors.secondary) } } } @@ -270,24 +488,19 @@ struct GroupChatInfoView: View { } } } - } - private func memberInfoView(_ groupMember: GMember) -> some View { - GroupMemberInfoView(groupInfo: groupInfo, groupMember: groupMember) - .navigationBarHidden(false) + private var memberVerifiedShield: Text { + (Text(Image(systemName: "checkmark.shield")) + textSpace) + .font(.caption) + .baselineOffset(2) + .kerning(-2) + .foregroundColor(theme.colors.secondary) + } } private func groupLinkButton() -> some View { NavigationLink { - GroupLinkView( - groupId: groupInfo.groupId, - groupLink: $groupLink, - groupLinkMemberRole: $groupLinkMemberRole, - showTitle: false, - creatingGroup: false - ) - .navigationBarTitle("Group link") - .navigationBarTitleDisplayMode(.large) + groupLinkDestinationView() } label: { if groupLink == nil { Label("Create group link", systemImage: "link.badge.plus") @@ -297,6 +510,19 @@ struct GroupChatInfoView: View { } } + private func groupLinkDestinationView() -> some View { + GroupLinkView( + groupId: groupInfo.groupId, + groupLink: $groupLink, + groupLinkMemberRole: $groupLinkMemberRole, + showTitle: false, + creatingGroup: false + ) + .navigationBarTitle("Group link") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } + private func editGroupButton() -> some View { NavigationLink { GroupProfileView( @@ -304,6 +530,7 @@ struct GroupChatInfoView: View { groupProfile: groupInfo.groupProfile ) .navigationBarTitle("Group profile") + .modifier(ThemedBackground()) .navigationBarTitleDisplayMode(.large) } label: { Label("Edit group profile", systemImage: "pencil") @@ -312,8 +539,13 @@ struct GroupChatInfoView: View { private func addOrEditWelcomeMessage() -> some View { NavigationLink { - GroupWelcomeView(groupId: groupInfo.groupId, groupInfo: $groupInfo) + GroupWelcomeView( + groupInfo: $groupInfo, + groupProfile: groupInfo.groupProfile, + welcomeText: groupInfo.groupProfile.description ?? "" + ) .navigationTitle("Welcome message") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { groupInfo.groupProfile.description == nil @@ -322,11 +554,12 @@ struct GroupChatInfoView: View { } } - private func deleteGroupButton() -> some View { + @ViewBuilder private func deleteGroupButton() -> some View { + let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Delete group" : "Delete chat" Button(role: .destructive) { alert = .deleteGroupAlert } label: { - Label("Delete group", systemImage: "trash") + Label(label, systemImage: "trash") .foregroundColor(Color.red) } } @@ -341,19 +574,21 @@ struct GroupChatInfoView: View { } private func leaveGroupButton() -> some View { - Button(role: .destructive) { + let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group" : "Leave chat" + return Button(role: .destructive) { alert = .leaveGroupAlert } label: { - Label("Leave group", systemImage: "rectangle.portrait.and.arrow.right") + Label(label, systemImage: "rectangle.portrait.and.arrow.right") .foregroundColor(Color.red) } } // TODO reuse this and clearChatAlert with ChatInfoView private func deleteGroupAlert() -> Alert { + let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?" return Alert( - title: Text("Delete group?"), - message: deleteGroupAlertMessage(), + title: Text(label), + message: deleteGroupAlertMessage(groupInfo), primaryButton: .destructive(Text("Delete")) { Task { do { @@ -372,10 +607,6 @@ struct GroupChatInfoView: View { ) } - private func deleteGroupAlertMessage() -> Text { - groupInfo.membership.memberCurrent ? Text("Group will be deleted for all members - this cannot be undone!") : Text("Group will be deleted for you - this cannot be undone!") - } - private func clearChatAlert() -> Alert { Alert( title: Text("Clear conversation?"), @@ -391,9 +622,15 @@ struct GroupChatInfoView: View { } private func leaveGroupAlert() -> Alert { - Alert( - title: Text("Leave group?"), - message: Text("You will stop receiving messages from this group. Chat history will be preserved."), + let titleLabel: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group?" : "Leave chat?" + let messageLabel: LocalizedStringKey = ( + groupInfo.businessChat == nil + ? "You will stop receiving messages from this group. Chat history will be preserved." + : "You will stop receiving messages from this chat. Chat history will be preserved." + ) + return Alert( + title: Text(titleLabel), + message: Text(messageLabel), primaryButton: .destructive(Text("Leave")) { Task { await leaveGroup(chat.chatInfo.apiId) @@ -437,18 +674,25 @@ struct GroupChatInfoView: View { } private func removeMemberAlert(_ mem: GroupMember) -> Alert { - Alert( + let messageLabel: LocalizedStringKey = ( + groupInfo.businessChat == nil + ? "Member will be removed from group - this cannot be undone!" + : "Member will be removed from chat - this cannot be undone!" + ) + return Alert( title: Text("Remove member?"), - message: Text("Member will be removed from group - this cannot be undone!"), + message: Text(messageLabel), primaryButton: .destructive(Text("Remove")) { Task { do { - let updatedMember = try await apiRemoveMember(groupInfo.groupId, mem.groupMemberId) + let updatedMembers = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId]) await MainActor.run { - _ = chatModel.upsertGroupMember(groupInfo, updatedMember) + updatedMembers.forEach { updatedMember in + _ = chatModel.upsertGroupMember(groupInfo, updatedMember) + } } } catch let error { - logger.error("apiRemoveMember error: \(responseError(error))") + logger.error("apiRemoveMembers error: \(responseError(error))") let a = getErrorAlert(error, "Error removing member") alert = .error(title: a.title, error: a.message) } @@ -459,33 +703,80 @@ struct GroupChatInfoView: View { } } -func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bool = false) -> some View { - NavigationLink { - GroupPreferencesView( - groupInfo: groupInfo, - preferences: groupInfo.wrappedValue.fullGroupPreferences, - currentPreferences: groupInfo.wrappedValue.fullGroupPreferences, - creatingGroup: creatingGroup - ) - .navigationBarTitle("Group preferences") - .navigationBarTitleDisplayMode(.large) - } label: { - if creatingGroup { - Text("Set group preferences") - } else { - Label("Group preferences", systemImage: "switch.2") - } - } +func deleteGroupAlertMessage(_ groupInfo: GroupInfo) -> Text { + groupInfo.businessChat == nil ? ( + groupInfo.membership.memberCurrent ? Text("Group will be deleted for all members - this cannot be undone!") : Text("Group will be deleted for you - this cannot be undone!") + ) : ( + groupInfo.membership.memberCurrent ? Text("Chat will be deleted for all members - this cannot be undone!") : Text("Chat will be deleted for you - this cannot be undone!") + ) } -private var memberVerifiedShield: Text { - (Text(Image(systemName: "checkmark.shield")) + Text(" ")) - .font(.caption) - .baselineOffset(2) - .kerning(-2) - .foregroundColor(.secondary) +struct GroupPreferencesButton: View { + @Binding var groupInfo: GroupInfo + @State var preferences: FullGroupPreferences + @State var currentPreferences: FullGroupPreferences + var creatingGroup: Bool = false + + private var label: LocalizedStringKey { + groupInfo.businessChat == nil ? "Group preferences" : "Chat preferences" + } + + var body: some View { + NavigationLink { + GroupPreferencesView( + groupInfo: $groupInfo, + preferences: $preferences, + currentPreferences: currentPreferences, + creatingGroup: creatingGroup, + savePreferences: savePreferences + ) + .navigationBarTitle(label) + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + .onDisappear { + let saveText = NSLocalizedString( + creatingGroup ? "Save" : "Save and notify group members", + comment: "alert button" + ) + + if groupInfo.fullGroupPreferences != preferences { + showAlert( + title: NSLocalizedString("Save preferences?", comment: "alert title"), + buttonTitle: saveText, + buttonAction: { savePreferences() }, + cancelButton: true + ) + } + } + } label: { + if creatingGroup { + Text("Set group preferences") + } else { + Label(label, systemImage: "switch.2") + } + } + } + + private func savePreferences() { + Task { + do { + var gp = groupInfo.groupProfile + gp.groupPreferences = toGroupPreferences(preferences) + let gInfo = try await apiUpdateGroup(groupInfo.groupId, gp) + await MainActor.run { + groupInfo = gInfo + ChatModel.shared.updateGroup(gInfo) + currentPreferences = preferences + } + } catch { + logger.error("GroupPreferencesView apiUpdateGroup error: \(responseError(error))") + } + } + } + } + func cantInviteIncognitoAlert() -> Alert { Alert( title: Text("Can't invite contacts!"), @@ -504,7 +795,9 @@ struct GroupChatInfoView_Previews: PreviewProvider { static var previews: some View { GroupChatInfoView( chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), - groupInfo: Binding.constant(GroupInfo.sampleData) + groupInfo: Binding.constant(GroupInfo.sampleData), + onSearch: {}, + localAlias: "" ) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift index bf2179bea4..a11c073a42 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift @@ -10,18 +10,21 @@ import SwiftUI import SimpleXChat struct GroupLinkView: View { + @EnvironmentObject var theme: AppTheme var groupId: Int64 - @Binding var groupLink: String? + @Binding var groupLink: CreatedConnLink? @Binding var groupLinkMemberRole: GroupMemberRole var showTitle: Bool = false var creatingGroup: Bool = false var linkCreatedCb: (() -> Void)? = nil + @State private var showShortLink = true @State private var creatingLink = false @State private var alert: GroupLinkAlert? + @State private var shouldCreate = true private enum GroupLinkAlert: Identifiable { case deleteLink - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -33,14 +36,13 @@ struct GroupLinkView: View { var body: some View { if creatingGroup { - NavigationView { - groupLinkView() - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button ("Continue") { linkCreatedCb?() } - } + groupLinkView() + .navigationBarBackButtonHidden() + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button ("Continue") { linkCreatedCb?() } } - } + } } else { groupLinkView() } @@ -69,9 +71,10 @@ struct GroupLinkView: View { } } .frame(height: 36) - SimpleXLinkQRCode(uri: groupLink) + SimpleXCreatedLinkQRCode(link: groupLink, short: $showShortLink) + .id("simplex-qrcode-view-for-\(groupLink.simplexChatUri(short: showShortLink))") Button { - showShareSheet(items: [simplexChatLink(groupLink)]) + showShareSheet(items: [groupLink.simplexChatUri(short: showShortLink)]) } label: { Label("Share link", systemImage: "square.and.arrow.up") } @@ -92,6 +95,10 @@ struct GroupLinkView: View { .frame(maxWidth: .infinity) } } + } header: { + if let groupLink, groupLink.connShortLink != nil { + ToggleShortLinkHeader(text: Text(""), link: groupLink, short: $showShortLink) + } } .alert(item: $alert) { alert in switch alert { @@ -111,7 +118,7 @@ struct GroupLinkView: View { }, secondaryButton: .cancel() ) case let .error(title, error): - return Alert(title: Text(title), message: Text(error)) + return mkAlert(title: title, message: error) } } .onChange(of: groupLinkMemberRole) { _ in @@ -125,11 +132,13 @@ struct GroupLinkView: View { } } .onAppear { - if groupLink == nil && !creatingLink { + if groupLink == nil && !creatingLink && shouldCreate { createGroupLink() } + shouldCreate = false } } + .modifier(ThemedBackground(grouped: true)) } private func createGroupLink() { @@ -155,8 +164,8 @@ struct GroupLinkView: View { struct GroupLinkView_Previews: PreviewProvider { static var previews: some View { - @State var groupLink: String? = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D" - @State var noGroupLink: String? = nil + @State var groupLink: CreatedConnLink? = CreatedConnLink(connFullLink: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", connShortLink: nil) + @State var noGroupLink: CreatedConnLink? = nil return Group { GroupLinkView(groupId: 1, groupLink: $groupLink, groupLinkMemberRole: Binding.constant(.member)) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 7e336c3328..79ad242366 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -11,12 +11,18 @@ import SimpleXChat struct GroupMemberInfoView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction @State var groupInfo: GroupInfo + @ObservedObject var chat: Chat @ObservedObject var groupMember: GMember var navigation: Bool = false @State private var connectionStats: ConnectionStats? = nil @State private var connectionCode: String? = nil + @State private var connectionLoaded: Bool = false + @State private var knownContactChat: Chat? = nil + @State private var knownContact: Contact? = nil + @State private var knownContactConnectionStats: ConnectionStats? = nil @State private var newRole: GroupMemberRole = .member @State private var alert: GroupMemberInfoViewAlert? @State private var sheet: PlanAndConnectActionSheet? @@ -27,24 +33,32 @@ struct GroupMemberInfoView: View { enum GroupMemberInfoViewAlert: Identifiable { case blockMemberAlert(mem: GroupMember) case unblockMemberAlert(mem: GroupMember) + case blockForAllAlert(mem: GroupMember) + case unblockForAllAlert(mem: GroupMember) case removeMemberAlert(mem: GroupMember) case changeMemberRoleAlert(mem: GroupMember, role: GroupMemberRole) case switchAddressAlert case abortSwitchAddressAlert case syncConnectionForceAlert case planAndConnectAlert(alert: PlanAndConnectAlert) - case error(title: LocalizedStringKey, error: LocalizedStringKey) + case queueInfo(info: String) + case someAlert(alert: SomeAlert) + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { case let .blockMemberAlert(mem): return "blockMemberAlert \(mem.groupMemberId)" case let .unblockMemberAlert(mem): return "unblockMemberAlert \(mem.groupMemberId)" + case let .blockForAllAlert(mem): return "blockForAllAlert \(mem.groupMemberId)" + case let .unblockForAllAlert(mem): return "unblockForAllAlert \(mem.groupMemberId)" case let .removeMemberAlert(mem): return "removeMemberAlert \(mem.groupMemberId)" case let .changeMemberRoleAlert(mem, role): return "changeMemberRoleAlert \(mem.groupMemberId) \(role.rawValue)" case .switchAddressAlert: return "switchAddressAlert" case .abortSwitchAddressAlert: return "abortSwitchAddressAlert" case .syncConnectionForceAlert: return "syncConnectionForceAlert" case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)" + case let .queueInfo(info): return "queueInfo \(info)" + case let .someAlert(alert): return "someAlert \(alert.id)" case let .error(title, _): return "error \(title)" } } @@ -58,10 +72,11 @@ struct GroupMemberInfoView: View { } } - private func knownDirectChat(_ contactId: Int64) -> Chat? { + private func knownDirectChat(_ contactId: Int64) -> (Chat, Contact)? { if let chat = chatModel.getContactChat(contactId), - chat.chatInfo.contact?.directOrUsed == true { - return chat + let contact = chat.chatInfo.contact, + contact.directOrUsed == true { + return (chat, contact) } else { return nil } @@ -69,23 +84,25 @@ struct GroupMemberInfoView: View { private func groupMemberInfoView() -> some View { ZStack { - VStack { - let member = groupMember.wrapped - List { - groupMemberInfoHeader(member) - .listRowBackground(Color.clear) + let member = groupMember.wrapped + List { + groupMemberInfoHeader(member) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .padding(.bottom, 18) + + infoActionButtons(member) + .padding(.horizontal) + .frame(maxWidth: .infinity) + .frame(height: infoViewActionButtonHeight) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + + if connectionLoaded { if member.memberActive { Section { - if let contactId = member.memberContactId, let chat = knownDirectChat(contactId) { - knownDirectChatButton(chat) - } else if groupInfo.fullGroupPreferences.directMessages.on { - if let contactId = member.memberContactId { - newDirectChatButton(contactId) - } else if member.activeConn?.peerChatVRange.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) ?? false { - createMemberContactButton() - } - } if let code = connectionCode { verifyCodeButton(code) } if let connStats = connectionStats, connStats.ratchetSyncAllowed { @@ -105,8 +122,8 @@ struct GroupMemberInfoView: View { } label: { Label("Share address", systemImage: "square.and.arrow.up") } - if let contactId = member.memberContactId { - if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on { + if member.memberContactId != nil { + if knownContactChat == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) { connectViaAddressButton(contactLink) } } else { @@ -114,13 +131,16 @@ struct GroupMemberInfoView: View { } } header: { Text("Address") + .foregroundColor(theme.colors.secondary) } footer: { Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.") + .foregroundColor(theme.colors.secondary) } } - Section("Member") { - infoRow("Group", groupInfo.displayName) + Section(header: Text("Member").foregroundColor(theme.colors.secondary)) { + let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Group" : "Chat" + infoRow(label, groupInfo.displayName) if let roles = member.canChangeRoleTo(groupInfo: groupInfo) { Picker("Change role", selection: $newRole) { @@ -132,23 +152,17 @@ struct GroupMemberInfoView: View { } else { infoRow("Role", member.memberRole.text) } - - // TODO invited by - need to get contact by contact id - if let conn = member.activeConn { - let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel) - infoRow("Connection", connLevelDesc) - } } if let connStats = connectionStats { - Section("Servers") { + Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) { // TODO network connection status Button("Change receiving address") { alert = .switchAddressAlert } .disabled( connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } - || connStats.ratchetSyncSendProhibited + || !member.sendMsgEnabled ) if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) { Button("Abort changing address") { @@ -156,73 +170,105 @@ struct GroupMemberInfoView: View { } .disabled( connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch } - || connStats.ratchetSyncSendProhibited + || !member.sendMsgEnabled ) } - smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }) - smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }) + smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }, theme.colors.secondary) + smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }, theme.colors.secondary) } } - Section { - if member.memberSettings.showMessages { - blockMemberButton(member) - } else { - unblockMemberButton(member) - } - if member.canBeRemoved(groupInfo: groupInfo) { - removeMemberButton(member) - } + if groupInfo.membership.memberRole >= .admin { + adminDestructiveSection(member) + } else { + nonAdminBlockSection(member) } if developerTools { - Section("For console") { + Section(header: Text("For console").foregroundColor(theme.colors.secondary)) { infoRow("Local name", member.localDisplayName) infoRow("Database ID", "\(member.groupMemberId)") + if let conn = member.activeConn { + let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel) + infoRow("Connection", connLevelDesc) + } + Button ("Debug delivery") { + Task { + do { + let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId)) + await MainActor.run { alert = .queueInfo(info: info) } + } catch let e { + logger.error("apiContactQueueInfo error: \(responseError(e))") + let a = getErrorAlert(e, "Error") + await MainActor.run { alert = .error(title: a.title, error: a.message) } + } + } + } } } + } - .navigationBarHidden(true) - .onAppear { - if #unavailable(iOS 16) { - // this condition prevents re-setting picker - if !justOpened { return } + } + .navigationBarHidden(true) + .task { + if #unavailable(iOS 16) { + // this condition prevents re-setting picker + if !justOpened { return } + } + justOpened = false + newRole = member.memberRole + do { + let (_, stats) = try await apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId) + let (mem, code) = member.memberActive ? try await apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil) + await MainActor.run { + _ = chatModel.upsertGroupMember(groupInfo, mem) + connectionStats = stats + connectionCode = code + connectionLoaded = true } - justOpened = false - DispatchQueue.main.async { - newRole = member.memberRole - do { - let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId) - let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil) - _ = chatModel.upsertGroupMember(groupInfo, mem) - connectionStats = stats - connectionCode = code - } catch let error { - logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))") + } catch let error { + await MainActor.run { + connectionLoaded = true + } + logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))") + } + if let contactId = member.memberContactId, let (contactChat, contact) = knownDirectChat(contactId) { + knownContactChat = contactChat + knownContact = contact + do { + let (stats, _) = try await apiContactInfo(contactChat.chatInfo.apiId) + await MainActor.run { + knownContactConnectionStats = stats } + } catch let error { + logger.error("apiContactInfo error: \(responseError(error))") } } - .onChange(of: newRole) { newRole in - if newRole != member.memberRole { - alert = .changeMemberRoleAlert(mem: member, role: newRole) - } - } - .onChange(of: member.memberRole) { role in - newRole = role + } + .onChange(of: newRole) { newRole in + if newRole != member.memberRole { + alert = .changeMemberRoleAlert(mem: member, role: newRole) } } + .onChange(of: member.memberRole) { role in + newRole = role + } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .alert(item: $alert) { alertItem in switch(alertItem) { case let .blockMemberAlert(mem): return blockMemberAlert(groupInfo, mem) case let .unblockMemberAlert(mem): return unblockMemberAlert(groupInfo, mem) + case let .blockForAllAlert(mem): return blockForAllAlert(groupInfo, mem) + case let .unblockForAllAlert(mem): return unblockForAllAlert(groupInfo, mem) case let .removeMemberAlert(mem): return removeMemberAlert(mem) case let .changeMemberRoleAlert(mem, _): return changeMemberRoleAlert(mem) case .switchAddressAlert: return switchAddressAlert(switchMemberAddress) case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress) case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) }) case let .planAndConnectAlert(alert): return planAndConnectAlert(alert, dismiss: true) - case let .error(title, error): return Alert(title: Text(title), message: Text(error)) + case let .queueInfo(info): return queueInfoAlert(info) + case let .someAlert(a): return a.alert + case let .error(title, error): return mkAlert(title: title, message: error) } } .actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) } @@ -231,6 +277,68 @@ struct GroupMemberInfoView: View { ProgressView().scaleEffect(2) } } + .onChange(of: chat.chatInfo) { c in + if case let .group(gI) = chat.chatInfo { + groupInfo = gI + } + } + .modifier(ThemedBackground(grouped: true)) + } + + func infoActionButtons(_ member: GroupMember) -> some View { + GeometryReader { g in + let buttonWidth = g.size.width / 4 + HStack(alignment: .center, spacing: 8) { + if let chat = knownContactChat, let contact = knownContact { + knownDirectChatButton(chat, width: buttonWidth) + AudioCallButton(chat: chat, contact: contact, connectionStats: $knownContactConnectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) } + VideoButton(chat: chat, contact: contact, connectionStats: $knownContactConnectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) } + } else if groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) { + if let contactId = member.memberContactId { + newDirectChatButton(contactId, width: buttonWidth) + } else if member.versionRange.maxVersion >= CREATE_MEMBER_CONTACT_VERSION { + createMemberContactButton(member, width: buttonWidth) + } + InfoViewButton(image: "phone.fill", title: "call", disabledLook: true, width: buttonWidth) { showSendMessageToEnableCallsAlert() + } + InfoViewButton(image: "video.fill", title: "video", disabledLook: true, width: buttonWidth) { showSendMessageToEnableCallsAlert() + } + } else { // no known contact chat && directMessages are off + InfoViewButton(image: "message.fill", title: "message", disabledLook: true, width: buttonWidth) { showDirectMessagesProhibitedAlert("Can't message member") + } + InfoViewButton(image: "phone.fill", title: "call", disabledLook: true, width: buttonWidth) { showDirectMessagesProhibitedAlert("Can't call member") + } + InfoViewButton(image: "video.fill", title: "video", disabledLook: true, width: buttonWidth) { showDirectMessagesProhibitedAlert("Can't call member") + } + } + } + .frame(maxWidth: .infinity, alignment: .center) + } + } + + func showSendMessageToEnableCallsAlert() { + alert = .someAlert(alert: SomeAlert( + alert: mkAlert( + title: "Can't call member", + message: "Send message to enable calls." + ), + id: "can't call member, send message" + )) + } + + func showDirectMessagesProhibitedAlert(_ title: LocalizedStringKey) { + let messageLabel: LocalizedStringKey = ( + groupInfo.businessChat == nil + ? "Direct messages between members are prohibited." + : "Direct messages between members are prohibited in this chat." + ) + alert = .someAlert(alert: SomeAlert( + alert: mkAlert( + title: title, + message: messageLabel + ), + id: "can't message member, direct messages prohibited" + )) } func connectViaAddressButton(_ contactLink: String) -> some View { @@ -247,73 +355,102 @@ struct GroupMemberInfoView: View { } } - func knownDirectChatButton(_ chat: Chat) -> some View { - Button { - dismissAllSheets(animated: true) - DispatchQueue.main.async { - chatModel.chatId = chat.id - } - } label: { - Label("Send direct message", systemImage: "message") - } - } - - func newDirectChatButton(_ contactId: Int64) -> some View { - Button { - do { - let chat = try apiGetChat(type: .direct, id: contactId) - chatModel.addChat(chat) + func knownDirectChatButton(_ chat: Chat, width: CGFloat) -> some View { + InfoViewButton(image: "message.fill", title: "message", width: width) { + ItemsModel.shared.loadOpenChat(chat.id) { dismissAllSheets(animated: true) - DispatchQueue.main.async { - chatModel.chatId = chat.id - } - } catch let error { - logger.error("openDirectChatButton apiGetChat error: \(responseError(error))") } - } label: { - Label("Send direct message", systemImage: "message") } } - func createMemberContactButton() -> some View { - Button { - progressIndicator = true + func newDirectChatButton(_ contactId: Int64, width: CGFloat) -> some View { + InfoViewButton(image: "message.fill", title: "message", width: width) { Task { - do { - let memberContact = try await apiCreateMemberContact(groupInfo.apiId, groupMember.groupMemberId) - await MainActor.run { - progressIndicator = false - chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact))) - dismissAllSheets(animated: true) - chatModel.chatId = memberContact.id - chatModel.setContactNetworkStatus(memberContact, .connected) - } - } catch let error { - logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))") - let a = getErrorAlert(error, "Error creating member contact") - await MainActor.run { - progressIndicator = false - alert = .error(title: a.title, error: a.message) - } + ItemsModel.shared.loadOpenChat("@\(contactId)") { + dismissAllSheets(animated: true) + } + } + } + } + + func createMemberContactButton(_ member: GroupMember, width: CGFloat) -> some View { + InfoViewButton( + image: "message.fill", + title: "message", + disabledLook: + !( + member.sendMsgEnabled || + (member.activeConn?.connectionStats?.ratchetSyncAllowed ?? false) + ), + width: width + ) { + if member.sendMsgEnabled { + progressIndicator = true + Task { + do { + let memberContact = try await apiCreateMemberContact(groupInfo.apiId, groupMember.groupMemberId) + await MainActor.run { + progressIndicator = false + chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact))) + ItemsModel.shared.loadOpenChat(memberContact.id) { + dismissAllSheets(animated: true) + } + NetworkModel.shared.setContactNetworkStatus(memberContact, .connected) + } + } catch let error { + logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))") + let a = getErrorAlert(error, "Error creating member contact") + await MainActor.run { + progressIndicator = false + alert = .error(title: a.title, error: a.message) + } + } + } + } else if let connStats = connectionStats { + if connStats.ratchetSyncAllowed { + alert = .someAlert(alert: SomeAlert( + alert: Alert( + title: Text("Fix connection?"), + message: Text("Connection requires encryption renegotiation."), + primaryButton: .default(Text("Fix")) { + syncMemberConnection(force: false) + }, + secondaryButton: .cancel() + ), + id: "can't message member, fix connection" + )) + } else if connStats.ratchetSyncInProgress { + alert = .someAlert(alert: SomeAlert( + alert: mkAlert( + title: "Can't message member", + message: "Encryption renegotiation in progress." + ), + id: "can't message member, encryption renegotiation in progress" + )) + } else { + alert = .someAlert(alert: SomeAlert( + alert: mkAlert( + title: "Can't message member", + message: "Connection not ready." + ), + id: "can't message member, connection not ready" + )) } } - } label: { - Label("Send direct message", systemImage: "message") } } private func groupMemberInfoHeader(_ mem: GroupMember) -> some View { VStack { - ProfileImage(imageStr: mem.image, color: Color(uiColor: .tertiarySystemFill)) - .frame(width: 192, height: 192) + MemberProfileImage(mem, size: 192, color: Color(uiColor: .tertiarySystemFill)) .padding(.top, 12) .padding() if mem.verified { ( Text(Image(systemName: "checkmark.shield")) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .font(.title2) - + Text(" ") + + textSpace + Text(mem.displayName) .font(.largeTitle) ) @@ -359,6 +496,7 @@ struct GroupMemberInfoView: View { ) .navigationBarTitleDisplayMode(.inline) .navigationTitle("Security code") + .modifier(ThemedBackground()) } label: { Label( member.verified ? "View security code" : "Verify security code", @@ -385,6 +523,55 @@ struct GroupMemberInfoView: View { } } + @ViewBuilder private func adminDestructiveSection(_ mem: GroupMember) -> some View { + let canBlockForAll = mem.canBlockForAll(groupInfo: groupInfo) + let canRemove = mem.canBeRemoved(groupInfo: groupInfo) + if canBlockForAll || canRemove { + Section { + if canBlockForAll { + if mem.blockedByAdmin { + unblockForAllButton(mem) + } else { + blockForAllButton(mem) + } + } + if canRemove { + removeMemberButton(mem) + } + } + } + } + + private func nonAdminBlockSection(_ mem: GroupMember) -> some View { + Section { + if mem.blockedByAdmin { + Label("Blocked by admin", systemImage: "hand.raised") + .foregroundColor(theme.colors.secondary) + } else if mem.memberSettings.showMessages { + blockMemberButton(mem) + } else { + unblockMemberButton(mem) + } + } + } + + private func blockForAllButton(_ mem: GroupMember) -> some View { + Button(role: .destructive) { + alert = .blockForAllAlert(mem: mem) + } label: { + Label("Block for all", systemImage: "hand.raised") + .foregroundColor(.red) + } + } + + private func unblockForAllButton(_ mem: GroupMember) -> some View { + Button { + alert = .unblockForAllAlert(mem: mem) + } label: { + Label("Unblock for all", systemImage: "hand.raised.slash") + } + } + private func blockMemberButton(_ mem: GroupMember) -> some View { Button(role: .destructive) { alert = .blockMemberAlert(mem: mem) @@ -412,19 +599,26 @@ struct GroupMemberInfoView: View { } private func removeMemberAlert(_ mem: GroupMember) -> Alert { - Alert( + let label: LocalizedStringKey = ( + groupInfo.businessChat == nil + ? "Member will be removed from group - this cannot be undone!" + : "Member will be removed from chat - this cannot be undone!" + ) + return Alert( title: Text("Remove member?"), - message: Text("Member will be removed from group - this cannot be undone!"), + message: Text(label), primaryButton: .destructive(Text("Remove")) { Task { do { - let updatedMember = try await apiRemoveMember(groupInfo.groupId, mem.groupMemberId) + let updatedMembers = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId]) await MainActor.run { - _ = chatModel.upsertGroupMember(groupInfo, updatedMember) + updatedMembers.forEach { updatedMember in + _ = chatModel.upsertGroupMember(groupInfo, updatedMember) + } dismiss() } } catch let error { - logger.error("apiRemoveMember error: \(responseError(error))") + logger.error("apiRemoveMembers error: \(responseError(error))") let a = getErrorAlert(error, "Error removing member") alert = .error(title: a.title, error: a.message) } @@ -437,18 +631,28 @@ struct GroupMemberInfoView: View { private func changeMemberRoleAlert(_ mem: GroupMember) -> Alert { Alert( title: Text("Change member role?"), - message: mem.memberCurrent ? Text("Member role will be changed to \"\(newRole.text)\". All group members will be notified.") : Text("Member role will be changed to \"\(newRole.text)\". The member will receive a new invitation."), + message: ( + mem.memberCurrent + ? ( + groupInfo.businessChat == nil + ? Text("Member role will be changed to \"\(newRole.text)\". All group members will be notified.") + : Text("Member role will be changed to \"\(newRole.text)\". All chat members will be notified.") + ) + : Text("Member role will be changed to \"\(newRole.text)\". The member will receive a new invitation.") + ), primaryButton: .default(Text("Change")) { Task { do { - let updatedMember = try await apiMemberRole(groupInfo.groupId, mem.groupMemberId, newRole) + let updatedMembers = try await apiMembersRole(groupInfo.groupId, [mem.groupMemberId], newRole) await MainActor.run { - _ = chatModel.upsertGroupMember(groupInfo, updatedMember) + updatedMembers.forEach { updatedMember in + _ = chatModel.upsertGroupMember(groupInfo, updatedMember) + } } - + } catch let error { newRole = mem.memberRole - logger.error("apiMemberRole error: \(responseError(error))") + logger.error("apiMembersRole error: \(responseError(error))") let a = getErrorAlert(error, "Error changing role") alert = .error(title: a.title, error: a.message) } @@ -517,6 +721,21 @@ struct GroupMemberInfoView: View { } } +func MemberProfileImage( + _ mem: GroupMember, + size: CGFloat, + color: Color = Color(uiColor: .tertiarySystemGroupedBackground), + backgroundColor: Color? = nil +) -> some View { + ProfileImage( + imageStr: mem.image, + size: size, + color: color, + backgroundColor: backgroundColor, + blurred: mem.blocked + ) +} + func blockMemberAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert { Alert( title: Text("Block member?"), @@ -560,10 +779,48 @@ func updateMemberSettings(_ gInfo: GroupInfo, _ member: GroupMember, _ memberSet } } +func blockForAllAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert { + Alert( + title: Text("Block member for all?"), + message: Text("All new messages from \(mem.chatViewName) will be hidden!"), + primaryButton: .destructive(Text("Block for all")) { + blockMemberForAll(gInfo, mem, true) + }, + secondaryButton: .cancel() + ) +} + +func unblockForAllAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert { + Alert( + title: Text("Unblock member for all?"), + message: Text("Messages from \(mem.chatViewName) will be shown!"), + primaryButton: .default(Text("Unblock for all")) { + blockMemberForAll(gInfo, mem, false) + }, + secondaryButton: .cancel() + ) +} + +func blockMemberForAll(_ gInfo: GroupInfo, _ member: GroupMember, _ blocked: Bool) { + Task { + do { + let updatedMembers = try await apiBlockMembersForAll(gInfo.groupId, [member.groupMemberId], blocked) + await MainActor.run { + updatedMembers.forEach { updatedMember in + _ = ChatModel.shared.upsertGroupMember(gInfo, updatedMember) + } + } + } catch let error { + logger.error("apiBlockMembersForAll error: \(responseError(error))") + } + } +} + struct GroupMemberInfoView_Previews: PreviewProvider { static var previews: some View { GroupMemberInfoView( groupInfo: GroupInfo.sampleData, + chat: Chat.sampleData, groupMember: GMember.sampleData ) } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift new file mode 100644 index 0000000000..9bb4a0cc35 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift @@ -0,0 +1,249 @@ +// +// GroupMentions.swift +// SimpleX (iOS) +// +// Created by Diogo Cunha on 30/01/2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +let MENTION_START: Character = "@" +let QUOTE: Character = "'" +let MEMBER_ROW_SIZE: CGFloat = 60 +let MAX_VISIBLE_MEMBER_ROWS: CGFloat = 4.8 + +struct GroupMentionsView: View { + @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme + var groupInfo: GroupInfo + @Binding var composeState: ComposeState + @Binding var selectedRange: NSRange + @Binding var keyboardVisible: Bool + + @State private var isVisible = false + @State private var currentMessage: String = "" + @State private var mentionName: String = "" + @State private var mentionRange: NSRange? + @State private var mentionMemberId: String? + @State private var sortedMembers: [GMember] = [] + + var body: some View { + ZStack(alignment: .bottom) { + if isVisible { + let filtered = filteredMembers() + if filtered.count > 0 { + Color.white.opacity(0.01) + .edgesIgnoringSafeArea(.all) + .onTapGesture { + isVisible = false + } + VStack(spacing: 0) { + Spacer() + Divider() + let scroll = ScrollView { + LazyVStack(spacing: 0) { + ForEach(Array(filtered.enumerated()), id: \.element.wrapped.groupMemberId) { index, member in + let mentioned = mentionMemberId == member.wrapped.memberId + let disabled = composeState.mentions.count >= MAX_NUMBER_OF_MENTIONS && !mentioned + ZStack(alignment: .bottom) { + memberRowView(member.wrapped, mentioned) + .contentShape(Rectangle()) + .disabled(disabled) + .opacity(disabled ? 0.6 : 1) + .onTapGesture { + memberSelected(member) + } + .padding(.horizontal) + .frame(height: MEMBER_ROW_SIZE) + + Divider() + .padding(.leading) + .padding(.leading, 48) + } + } + } + } + .frame(maxHeight: MEMBER_ROW_SIZE * min(MAX_VISIBLE_MEMBER_ROWS, CGFloat(filtered.count))) + .background(Color(UIColor.systemBackground)) + + if #available(iOS 16.0, *) { + scroll.scrollDismissesKeyboard(.never) + } else { + scroll + } + } + } + } + } + .onChange(of: composeState.parsedMessage) { parsedMsg in + currentMessage = composeState.message + messageChanged(currentMessage, parsedMsg, selectedRange) + } + .onChange(of: selectedRange) { r in + // This condition is needed to prevent messageChanged called twice, + // because composeState.formattedText triggers later when message changes. + // The condition is only true if position changed without text change + if currentMessage == composeState.message { + messageChanged(currentMessage, composeState.parsedMessage, r) + } + } + .onAppear { + currentMessage = composeState.message + } + } + + private func filteredMembers() -> [GMember] { + let s = mentionName.lowercased() + return s.isEmpty + ? sortedMembers + : sortedMembers.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) } + } + + private func messageChanged(_ msg: String, _ parsedMsg: [FormattedText], _ range: NSRange) { + removeUnusedMentions(parsedMsg) + if let (ft, r) = selectedMarkdown(parsedMsg, range) { + switch ft.format { + case let .mention(name): + isVisible = true + mentionName = name + mentionRange = r + mentionMemberId = composeState.mentions[name]?.memberId + if !m.membersLoaded { + Task { + await m.loadGroupMembers(groupInfo) + sortMembers() + } + } + return + case .none: () // + let pos = range.location + if range.length == 0, let (at, atRange) = getCharacter(msg, pos - 1), at == "@" { + let prevChar = getCharacter(msg, pos - 2)?.char + if prevChar == nil || prevChar == " " || prevChar == "\n" { + isVisible = true + mentionName = "" + mentionRange = atRange + mentionMemberId = nil + Task { + await m.loadGroupMembers(groupInfo) + sortMembers() + } + return + } + } + default: () + } + } + closeMemberList() + } + + private func sortMembers() { + sortedMembers = m.groupMembers.filter({ m in + let status = m.wrapped.memberStatus + return status != .memLeft && status != .memRemoved && status != .memInvited + }) + .sorted { $0.wrapped.memberRole > $1.wrapped.memberRole } + } + + private func removeUnusedMentions(_ parsedMsg: [FormattedText]) { + let usedMentions: Set<String> = Set(parsedMsg.compactMap { ft in + if case let .mention(name) = ft.format { name } else { nil } + }) + if usedMentions.count < composeState.mentions.count { + composeState = composeState.copy(mentions: composeState.mentions.filter({ usedMentions.contains($0.key) })) + } + } + + private func getCharacter(_ s: String, _ pos: Int) -> (char: String.SubSequence, range: NSRange)? { + if pos < 0 || pos >= s.count { return nil } + let r = NSRange(location: pos, length: 1) + return if let range = Range(r, in: s) { + (s[range], r) + } else { + nil + } + } + + private func selectedMarkdown(_ parsedMsg: [FormattedText], _ range: NSRange) -> (FormattedText, NSRange)? { + if parsedMsg.isEmpty { return nil } + var i = 0 + var pos: Int = 0 + while i < parsedMsg.count && pos + parsedMsg[i].text.count < range.location { + pos += parsedMsg[i].text.count + i += 1 + } + // the second condition will be true when two markdowns are selected + return i >= parsedMsg.count || range.location + range.length > pos + parsedMsg[i].text.count + ? nil + : (parsedMsg[i], NSRange(location: pos, length: parsedMsg[i].text.count)) + } + + private func memberSelected(_ member: GMember) { + if let range = mentionRange, mentionMemberId == nil || mentionMemberId != member.wrapped.memberId { + addMemberMention(member, range) + } + } + + private func addMemberMention(_ member: GMember, _ r: NSRange) { + guard let range = Range(r, in: composeState.message) else { return } + var mentions = composeState.mentions + var newName: String + if let mm = mentions.first(where: { $0.value.memberId == member.wrapped.memberId }) { + newName = mm.key + } else { + newName = composeState.mentionMemberName(member.wrapped.memberProfile.displayName) + } + mentions[newName] = CIMention(groupMember: member.wrapped) + var msgMention = newName.contains(" ") || newName.last?.isPunctuation == true + ? "@'\(newName)'" + : "@\(newName)" + var newPos = r.location + msgMention.count + let newMsgLength = composeState.message.count + msgMention.count - r.length + print(newPos) + print(newMsgLength) + if newPos == newMsgLength { + msgMention += " " + newPos += 1 + } + composeState = composeState.copy( + message: composeState.message.replacingCharacters(in: range, with: msgMention), + mentions: mentions + ) + selectedRange = NSRange(location: newPos, length: 0) + closeMemberList() + keyboardVisible = true + } + + private func closeMemberList() { + isVisible = false + mentionName = "" + mentionRange = nil + mentionMemberId = nil + } + + private func memberRowView(_ member: GroupMember, _ mentioned: Bool) -> some View { + return HStack{ + MemberProfileImage(member, size: 38) + .padding(.trailing, 2) + VStack(alignment: .leading) { + let t = Text(member.localAliasAndFullName).foregroundColor(member.memberIncognito ? .indigo : theme.colors.onBackground) + (member.verified ? memberVerifiedShield() + t : t) + .lineLimit(1) + } + Spacer() + if mentioned { + Image(systemName: "checkmark") + } + } + + func memberVerifiedShield() -> Text { + (Text(Image(systemName: "checkmark.shield")) + textSpace) + .font(.caption) + .baselineOffset(2) + .kerning(-2) + .foregroundColor(theme.colors.secondary) + } + } +} diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift index 860a6febb0..ed39c401ce 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift @@ -9,13 +9,21 @@ import SwiftUI import SimpleXChat +private let featureRoles: [(role: GroupMemberRole?, text: LocalizedStringKey)] = [ + (nil, "all members"), + (.admin, "admins"), + (.owner, "owners") +] + struct GroupPreferencesView: View { @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @Binding var groupInfo: GroupInfo - @State var preferences: FullGroupPreferences - @State var currentPreferences: FullGroupPreferences + @Binding var preferences: FullGroupPreferences + var currentPreferences: FullGroupPreferences let creatingGroup: Bool + let savePreferences: () -> Void @State private var showSaveDialogue = false var body: some View { @@ -24,12 +32,15 @@ struct GroupPreferencesView: View { List { featureSection(.timedMessages, $preferences.timedMessages.enable) featureSection(.fullDelete, $preferences.fullDelete.enable) - featureSection(.directMessages, $preferences.directMessages.enable) + featureSection(.directMessages, $preferences.directMessages.enable, $preferences.directMessages.role) featureSection(.reactions, $preferences.reactions.enable) - featureSection(.voice, $preferences.voice.enable) - featureSection(.files, $preferences.files.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.canEdit { + if groupInfo.isOwner { Section { Button("Reset") { preferences = currentPreferences } Button(saveText) { savePreferences() } @@ -47,7 +58,7 @@ struct GroupPreferencesView: View { preferences.timedMessages.ttl = currentPreferences.timedMessages.ttl } } - .modifier(BackButton { + .modifier(BackButton(disabled: Binding.constant(false)) { if currentPreferences == preferences { dismiss() } else { @@ -59,16 +70,19 @@ struct GroupPreferencesView: View { savePreferences() dismiss() } - Button("Exit without saving") { dismiss() } + Button("Exit without saving") { + preferences = currentPreferences + dismiss() + } } } - private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding<GroupFeatureEnabled>) -> some View { + private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding<GroupFeatureEnabled>, _ enableForRole: Binding<GroupMemberRole?>? = nil) -> some View { Section { - let color: Color = enableFeature.wrappedValue == .on ? .green : .secondary + let color: Color = enableFeature.wrappedValue == .on ? .green : theme.colors.secondary let icon = enableFeature.wrappedValue == .on ? feature.iconFilled : feature.icon let timedOn = feature == .timedMessages && enableFeature.wrappedValue == .on - if groupInfo.canEdit { + if groupInfo.isOwner { let enable = Binding( get: { enableFeature.wrappedValue == .on }, set: { on, _ in enableFeature.wrappedValue = on ? .on : .off } @@ -76,6 +90,7 @@ struct GroupPreferencesView: View { settingsRow(icon, color: color) { Toggle(feature.text, isOn: enable) } + .disabled(feature == .reports) // remove in 6.4 if timedOn { DropdownCustomTimePicker( selection: $preferences.timedMessages.ttl, @@ -86,6 +101,14 @@ struct GroupPreferencesView: View { ) .frame(height: 36) } + if enableFeature.wrappedValue == .on, let enableForRole { + Picker("Enabled for", selection: enableForRole) { + ForEach(featureRoles, id: \.role) { fr in + Text(fr.text) + } + } + .frame(height: 36) + } } else { settingsRow(icon, color: color) { infoRow(Text(feature.text), enableFeature.wrappedValue.text) @@ -93,26 +116,25 @@ struct GroupPreferencesView: View { if timedOn { infoRow("Delete after", timeText(preferences.timedMessages.ttl)) } + if enableFeature.wrappedValue == .on, let enableForRole { + HStack { + Text("Enabled for").foregroundColor(theme.colors.secondary) + Spacer() + Text( + featureRoles.first(where: { fr in fr.role == enableForRole.wrappedValue })?.text + ?? "all members" + ) + .foregroundColor(theme.colors.secondary) + } + } } } footer: { - Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.canEdit)) - .frame(height: 36, alignment: .topLeading) + Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.isOwner)) + .foregroundColor(theme.colors.secondary) } - } - - private func savePreferences() { - Task { - do { - var gp = groupInfo.groupProfile - gp.groupPreferences = toGroupPreferences(preferences) - let gInfo = try await apiUpdateGroup(groupInfo.groupId, gp) - await MainActor.run { - groupInfo = gInfo - chatModel.updateGroup(gInfo) - currentPreferences = preferences - } - } catch { - logger.error("GroupPreferencesView apiUpdateGroup error: \(responseError(error))") + .onChange(of: enableFeature.wrappedValue) { enabled in + if case .off = enabled { + enableForRole?.wrappedValue = nil } } } @@ -122,9 +144,10 @@ struct GroupPreferencesView_Previews: PreviewProvider { static var previews: some View { GroupPreferencesView( groupInfo: Binding.constant(GroupInfo.sampleData), - preferences: FullGroupPreferences.sampleData, + preferences: Binding.constant(FullGroupPreferences.sampleData), currentPreferences: FullGroupPreferences.sampleData, - creatingGroup: false + creatingGroup: false, + savePreferences: {} ) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift index 18cc3f4d80..1617edd11f 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift @@ -110,10 +110,13 @@ struct GroupProfileView: View { } } .onChange(of: chosenImage) { image in - if let image = image { - groupProfile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500) - } else { - groupProfile.image = nil + Task { + let resized: String? = if let image { + await resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500) + } else { + nil + } + await MainActor.run { groupProfile.image = resized } } } .onAppear { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift index 0e47d9dddf..97bff70efb 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift @@ -11,29 +11,34 @@ import SimpleXChat struct GroupWelcomeView: View { @Environment(\.dismiss) var dismiss: DismissAction - @EnvironmentObject private var m: ChatModel - var groupId: Int64 + @EnvironmentObject var theme: AppTheme @Binding var groupInfo: GroupInfo - @State private var welcomeText: String = "" + @State var groupProfile: GroupProfile + @State var welcomeText: String @State private var editMode = true @FocusState private var keyboardVisible: Bool @State private var showSaveDialog = false + @State private var showSecrets: Set<Int> = [] + + let maxByteCount = 1200 var body: some View { VStack { - if groupInfo.canEdit { + if groupInfo.isOwner && groupInfo.businessChat == nil { editorView() - .modifier(BackButton { - if welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil) { + .modifier(BackButton(disabled: Binding.constant(false)) { + if welcomeTextUnchanged() { dismiss() } else { showSaveDialog = true } }) - .confirmationDialog("Save welcome message?", isPresented: $showSaveDialog) { - Button("Save and update group profile") { - save() - dismiss() + .confirmationDialog( + welcomeTextFitsLimit() ? "Save welcome message?" : "Welcome message is too long", + isPresented: $showSaveDialog + ) { + if welcomeTextFitsLimit() { + Button("Save and update group profile") { save() } } Button("Exit without saving") { dismiss() } } @@ -47,15 +52,16 @@ struct GroupWelcomeView: View { } } .onAppear { - welcomeText = groupInfo.groupProfile.description ?? "" - keyboardVisible = true + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + keyboardVisible = true + } } } private func textPreview() -> some View { - messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil) - .allowsHitTesting(false) - .frame(minHeight: 140, alignment: .topLeading) + let r = messageText(welcomeText, parseSimpleXMarkdown(welcomeText), sender: nil, mentions: nil, userMemberId: nil, showSecrets: showSecrets, backgroundColor: UIColor(theme.colors.background)) + return msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets) + .frame(minHeight: 130, alignment: .topLeading) .frame(maxWidth: .infinity, alignment: .leading) } @@ -67,7 +73,7 @@ struct GroupWelcomeView: View { Group { if welcomeText.isEmpty { TextEditor(text: Binding.constant(NSLocalizedString("Enter welcome message…", comment: "placeholder"))) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .disabled(true) } TextEditor(text: $welcomeText) @@ -75,7 +81,7 @@ struct GroupWelcomeView: View { } .padding(.horizontal, -5) .padding(.top, -8) - .frame(height: 140, alignment: .topLeading) + .frame(height: 130, alignment: .topLeading) .frame(maxWidth: .infinity, alignment: .leading) } } else { @@ -94,6 +100,9 @@ struct GroupWelcomeView: View { } .disabled(welcomeText.isEmpty) copyButton() + } footer: { + Text(!welcomeTextFitsLimit() ? "Message too large" : "") + .foregroundColor(.red) } Section { @@ -114,7 +123,15 @@ struct GroupWelcomeView: View { Button("Save and update group profile") { save() } - .disabled(welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil)) + .disabled(welcomeTextUnchanged() || !welcomeTextFitsLimit()) + } + + private func welcomeTextUnchanged() -> Bool { + welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil) + } + + private func welcomeTextFitsLimit() -> Bool { + chatJsonLength(welcomeText) <= maxByteCount } private func save() { @@ -124,11 +141,13 @@ struct GroupWelcomeView: View { if welcome?.count == 0 { welcome = nil } - var groupProfileUpdated = groupInfo.groupProfile - groupProfileUpdated.description = welcome - groupInfo = try await apiUpdateGroup(groupId, groupProfileUpdated) - m.updateGroup(groupInfo) - welcomeText = welcome ?? "" + groupProfile.description = welcome + let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile) + await MainActor.run { + groupInfo = gInfo + ChatModel.shared.updateGroup(gInfo) + dismiss() + } } catch let error { logger.error("apiUpdateGroup error: \(responseError(error))") } @@ -138,6 +157,6 @@ struct GroupWelcomeView: View { struct GroupWelcomeView_Previews: PreviewProvider { static var previews: some View { - GroupWelcomeView(groupId: 1, groupInfo: Binding.constant(GroupInfo.sampleData)) + GroupProfileView(groupInfo: Binding.constant(GroupInfo.sampleData), groupProfile: GroupProfile.sampleData) } } diff --git a/apps/ios/Shared/Views/Chat/ScrollViewCells.swift b/apps/ios/Shared/Views/Chat/ScrollViewCells.swift new file mode 100644 index 0000000000..d062627d5b --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ScrollViewCells.swift @@ -0,0 +1,52 @@ +// +// ScrollViewCells.swift +// SimpleX (iOS) +// +// Created by Stanislav Dmitrenko on 27.01.2025. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +protocol ReusableView { + func prepareForReuse() +} + +/// `UIHostingConfiguration` back-port for iOS14 and iOS15 +/// Implemented as a `UIView` that wraps and manages a generic `UIHostingController` +final class HostingCell<Hosted: View>: UIView, ReusableView { + private let hostingController = UIHostingController<Hosted?>(rootView: nil) + + /// Updates content of the cell + /// For reference: https://noahgilmore.com/blog/swiftui-self-sizing-cells/ + func set(content: Hosted, parent: UIViewController) { + hostingController.view.backgroundColor = .clear + hostingController.rootView = content + if let hostingView = hostingController.view { + hostingView.invalidateIntrinsicContentSize() + if hostingController.parent != parent { parent.addChild(hostingController) } + if !subviews.contains(hostingController.view) { + addSubview(hostingController.view) + hostingView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + hostingView.leadingAnchor + .constraint(equalTo: leadingAnchor), + hostingView.trailingAnchor + .constraint(equalTo: trailingAnchor), + hostingView.topAnchor + .constraint(equalTo: topAnchor), + hostingView.bottomAnchor + .constraint(equalTo: bottomAnchor) + ]) + } + if hostingController.parent != parent { hostingController.didMove(toParent: parent) } + } else { + fatalError("Hosting View not loaded \(hostingController)") + } + } + + func prepareForReuse() { + //super.prepareForReuse() + hostingController.rootView = nil + } +} diff --git a/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift b/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift new file mode 100644 index 0000000000..85d6b279c5 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift @@ -0,0 +1,153 @@ +// +// SelectableChatItemToolbars.swift +// SimpleX (iOS) +// +// Created by Stanislav Dmitrenko on 30.07.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct SelectedItemsTopToolbar: View { + @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme + @Binding var selectedChatItems: Set<Int64>? + + var body: some View { + let count = selectedChatItems?.count ?? 0 + return Text(count == 0 ? "Nothing selected" : "Selected \(count)").font(.headline) + .foregroundColor(theme.colors.onBackground) + .frame(width: 220) + } +} + +struct SelectedItemsBottomToolbar: View { + @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme + let chatItems: [ChatItem] + @Binding var selectedChatItems: Set<Int64>? + var chatInfo: ChatInfo + // Bool - delete for everyone is possible + var deleteItems: (Bool) -> Void + var archiveItems: () -> Void + var moderateItems: () -> Void + //var shareItems: () -> Void + var forwardItems: () -> Void + @State var deleteEnabled: Bool = false + @State var deleteForEveryoneEnabled: Bool = false + + @State var canArchiveReports: Bool = false + + @State var canModerate: Bool = false + @State var moderateEnabled: Bool = false + + @State var forwardEnabled: Bool = false + + @State var deleteCountProhibited = false + @State var forwardCountProhibited = false + + var body: some View { + VStack(spacing: 0) { + Divider() + + HStack(alignment: .center) { + Button { + if canArchiveReports { + archiveItems() + } else { + deleteItems(deleteForEveryoneEnabled) + } + } label: { + Image(systemName: "trash") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20, alignment: .center) + .foregroundColor(!deleteEnabled || deleteCountProhibited ? theme.colors.secondary: .red) + } + .disabled(!deleteEnabled || deleteCountProhibited) + + Spacer() + Button { + moderateItems() + } label: { + Image(systemName: "flag") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20, alignment: .center) + .foregroundColor(!moderateEnabled || deleteCountProhibited ? theme.colors.secondary : .red) + } + .disabled(!moderateEnabled || deleteCountProhibited) + .opacity(canModerate ? 1 : 0) + + Spacer() + Button { + forwardItems() + } label: { + Image(systemName: "arrowshape.turn.up.forward") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20, alignment: .center) + .foregroundColor(!forwardEnabled || forwardCountProhibited ? theme.colors.secondary : theme.colors.primary) + } + .disabled(!forwardEnabled || forwardCountProhibited) + } + .frame(maxHeight: .infinity) + .padding([.leading, .trailing], 12) + } + .onAppear { + recheckItems(chatInfo, chatItems, selectedChatItems) + } + .onChange(of: chatInfo) { info in + recheckItems(info, chatItems, selectedChatItems) + } + .onChange(of: chatItems) { items in + recheckItems(chatInfo, items, selectedChatItems) + } + .onChange(of: selectedChatItems) { selected in + recheckItems(chatInfo, chatItems, selected) + } + .frame(height: 55.5) + .background(.thinMaterial) + } + + private func recheckItems(_ chatInfo: ChatInfo, _ chatItems: [ChatItem], _ selectedItems: Set<Int64>?) { + let count = selectedItems?.count ?? 0 + deleteCountProhibited = count == 0 || count > 200 + forwardCountProhibited = count == 0 || count > 20 + canModerate = possibleToModerate(chatInfo) + let groupInfo: GroupInfo? = if case let ChatInfo.group(groupInfo: info) = chatInfo { + info + } else { + nil + } + if let selected = selectedItems { + let me: Bool + let onlyOwnGroupItems: Bool + (deleteEnabled, deleteForEveryoneEnabled, canArchiveReports, me, onlyOwnGroupItems, forwardEnabled, selectedChatItems) = chatItems.reduce((true, true, true, true, true, true, [])) { (r, ci) in + if selected.contains(ci.id) { + var (de, dee, ar, me, onlyOwnGroupItems, fe, sel) = r + de = de && ci.canBeDeletedForSelf + dee = dee && ci.meta.deletable && !ci.localNote && !ci.isReport + ar = ar && ci.isActiveReport && ci.chatDir != .groupSnd && groupInfo != nil && groupInfo!.membership.memberRole >= .moderator + onlyOwnGroupItems = onlyOwnGroupItems && ci.chatDir == .groupSnd && !ci.isReport + me = me && ci.content.msgContent != nil && ci.memberToModerate(chatInfo) != nil && !ci.isReport + fe = fe && ci.content.msgContent != nil && ci.meta.itemDeleted == nil && !ci.isLiveDummy && !ci.isReport + sel.insert(ci.id) // we are collecting new selected items here to account for any changes in chat items list + return (de, dee, ar, me, onlyOwnGroupItems, fe, sel) + } else { + return r + } + } + moderateEnabled = me && !onlyOwnGroupItems + } + } + + private func possibleToModerate(_ chatInfo: ChatInfo) -> Bool { + return switch chatInfo { + case let .group(groupInfo): + groupInfo.membership.memberRole >= .admin + default: false + } + } +} diff --git a/apps/ios/Shared/Views/Chat/VerifyCodeView.swift b/apps/ios/Shared/Views/Chat/VerifyCodeView.swift index 75e31c26ed..7b01fe0300 100644 --- a/apps/ios/Shared/Views/Chat/VerifyCodeView.swift +++ b/apps/ios/Shared/Views/Chat/VerifyCodeView.swift @@ -10,6 +10,7 @@ import SwiftUI struct VerifyCodeView: View { @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme var displayName: String @State var connectionCode: String? @State var connectionVerified: Bool @@ -30,7 +31,7 @@ struct VerifyCodeView: View { HStack { if connectionVerified { Image(systemName: "checkmark.shield") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) Text("\(displayName) is verified") } else { Text("\(displayName) is not verified") @@ -66,6 +67,7 @@ struct VerifyCodeView: View { ScanCodeView(connectionVerified: $connectionVerified, verify: verify) .navigationBarTitleDisplayMode(.large) .navigationTitle("Scan code") + .modifier(ThemedBackground()) } label: { Label("Scan code", systemImage: "qrcode") } @@ -122,5 +124,6 @@ struct VerifyCodeView: View { struct VerifyCodeView_Previews: PreviewProvider { static var previews: some View { VerifyCodeView(displayName: "alice", connectionCode: "12345 67890 12345 67890", connectionVerified: false, verify: {_ in nil}) + .environmentObject(CurrentColors.toAppTheme()) } } diff --git a/apps/ios/Shared/Views/ChatList/ChatHelp.swift b/apps/ios/Shared/Views/ChatList/ChatHelp.swift index 7741512432..7abab33177 100644 --- a/apps/ios/Shared/Views/ChatList/ChatHelp.swift +++ b/apps/ios/Shared/Views/ChatList/ChatHelp.swift @@ -10,8 +10,7 @@ import SwiftUI struct ChatHelp: View { @EnvironmentObject var chatModel: ChatModel - @Binding var showSettings: Bool - @State private var showAddChat = false + let dismissSettingsSheet: DismissAction var body: some View { ScrollView { chatHelp() } @@ -24,7 +23,7 @@ struct ChatHelp: View { VStack(alignment: .leading, spacing: 0) { Text("To ask any questions and to receive updates:") Button("connect to SimpleX Chat developers.") { - showSettings = false + dismissSettingsSheet() DispatchQueue.main.async { UIApplication.shared.open(simplexTeamURL) } @@ -39,13 +38,13 @@ struct ChatHelp: View { HStack(spacing: 8) { Text("Tap button ") - NewChatButton(showAddChat: $showAddChat) + NewChatMenuButton() Text("above, then choose:") } - Text("**Create link / QR code** for your contact to use.") - Text("**Paste received link** or open it in the browser and tap **Open in mobile app**.") - Text("**Scan QR code**: to connect to your contact in person or via video call.") + Text("**Create 1-time link**: to create and share a new invitation link.") + Text("**Scan / Paste link**: to connect via a link you received.") + Text("**Create group**: to create a new group.") } .padding(.top, 24) @@ -63,8 +62,9 @@ struct ChatHelp: View { } struct ChatHelp_Previews: PreviewProvider { + @Environment(\.dismiss) static var mockDismiss + static var previews: some View { - @State var showSettings = false - return ChatHelp(showSettings: $showSettings) + ChatHelp(dismissSettingsSheet: mockDismiss) } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 18464b3bb5..81d78fbadd 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -9,34 +9,58 @@ import SwiftUI import SimpleXChat -private let rowHeights: [DynamicTypeSize: CGFloat] = [ - .xSmall: 68, - .small: 72, - .medium: 76, - .large: 80, - .xLarge: 88, - .xxLarge: 94, - .xxxLarge: 104, - .accessibility1: 90, - .accessibility2: 100, - .accessibility3: 120, - .accessibility4: 130, - .accessibility5: 140 +typealias DynamicSizes = ( + rowHeight: CGFloat, + profileImageSize: CGFloat, + mediaSize: CGFloat, + incognitoSize: CGFloat, + chatInfoSize: CGFloat, + unreadCorner: CGFloat, + unreadPadding: CGFloat +) + +private let dynamicSizes: [DynamicTypeSize: DynamicSizes] = [ + .xSmall: (68, 55, 33, 22, 18, 9, 3), + .small: (72, 57, 34, 22, 18, 9, 3), + .medium: (76, 60, 36, 22, 18, 10, 4), + .large: (80, 63, 38, 24, 20, 10, 4), + .xLarge: (88, 67, 41, 24, 20, 10, 4), + .xxLarge: (100, 71, 44, 27, 22, 11, 4), + .xxxLarge: (110, 75, 48, 30, 24, 12, 5), + .accessibility1: (110, 75, 48, 30, 24, 12, 5), + .accessibility2: (114, 75, 48, 30, 24, 12, 5), + .accessibility3: (124, 75, 48, 30, 24, 12, 5), + .accessibility4: (134, 75, 48, 30, 24, 12, 5), + .accessibility5: (144, 75, 48, 30, 24, 12, 5) ] +private let defaultDynamicSizes: DynamicSizes = dynamicSizes[.large]! + +func dynamicSize(_ font: DynamicTypeSize) -> DynamicSizes { + dynamicSizes[font] ?? defaultDynamicSizes +} + struct ChatListNavLink: View { @EnvironmentObject var chatModel: ChatModel - @Environment(\.dynamicTypeSize) private var dynamicTypeSize + @EnvironmentObject var theme: AppTheme + @EnvironmentObject var chatTagsModel: ChatTagsModel + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = false @ObservedObject var chat: Chat + @Binding var parentSheet: SomeSheet<AnyView>? @State private var showContactRequestDialog = false @State private var showJoinGroupDialog = false @State private var showContactConnectionInfo = false @State private var showInvalidJSON = false - @State private var showDeleteContactActionSheet = false + @State private var alert: SomeAlert? = nil + @State private var actionSheet: SomeActionSheet? = nil + @State private var sheet: SomeSheet<AnyView>? = nil @State private var showConnectContactViaAddressDialog = false @State private var inProgress = false @State private var progressByTimeout = false + var dynamicRowHeight: CGFloat { dynamicSize(userFont).rowHeight } + var body: some View { Group { switch chat.chatInfo { @@ -44,6 +68,8 @@ struct ChatListNavLink: View { contactNavLink(contact) case let .group(groupInfo): groupNavLink(groupInfo) + case let .local(noteFolder): + noteFolderNavLink(noteFolder) case let .contactRequest(cReq): contactRequestNavLink(cReq) case let .contactConnection(cConn): @@ -61,18 +87,26 @@ struct ChatListNavLink: View { progressByTimeout = false } } + .actionSheet(item: $actionSheet) { $0.actionSheet } } - - @ViewBuilder private func contactNavLink(_ contact: Contact) -> some View { + + private func contactNavLink(_ contact: Contact) -> some View { Group { - if contact.activeConn == nil && contact.profile.contactLink != nil { + if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) - .frame(height: rowHeights[dynamicTypeSize]) + .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button { - showDeleteContactActionSheet = true + deleteContactDialog( + chat, + contact, + dismissToChatList: false, + showAlert: { alert = $0 }, + showActionSheet: { actionSheet = $0 }, + showSheetContent: { sheet = $0 } + ) } label: { - Label("Delete", systemImage: "trash") + deleteLabel } .tint(.red) } @@ -83,51 +117,44 @@ struct ChatListNavLink: View { } } else { NavLinkPlain( - tag: chat.chatInfo.id, + chatId: chat.chatInfo.id, selection: $chatModel.chatId, label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) } ) + .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .leading, allowsFullSwipe: true) { markReadButton() toggleFavoriteButton() - toggleNtfsButton(chat) + toggleNtfsButton(chat: chat) } .swipeActions(edge: .trailing, allowsFullSwipe: true) { + tagChatButton(chat) if !chat.chatItems.isEmpty { clearChatButton() } Button { - if contact.ready || !contact.active { - showDeleteContactActionSheet = true - } else { - AlertManager.shared.showAlert(deletePendingContactAlert(chat, contact)) - } + deleteContactDialog( + chat, + contact, + dismissToChatList: false, + showAlert: { alert = $0 }, + showActionSheet: { actionSheet = $0 }, + showSheetContent: { sheet = $0 } + ) } label: { - Label("Delete", systemImage: "trash") + deleteLabel } .tint(.red) } - .frame(height: rowHeights[dynamicTypeSize]) } } - .actionSheet(isPresented: $showDeleteContactActionSheet) { - if contact.ready && contact.active { - return ActionSheet( - title: Text("Delete contact?\nThis cannot be undone!"), - buttons: [ - .destructive(Text("Delete and notify contact")) { Task { await deleteChat(chat, notify: true) } }, - .destructive(Text("Delete")) { Task { await deleteChat(chat, notify: false) } }, - .cancel() - ] - ) + .alert(item: $alert) { $0.alert } + .sheet(item: $sheet) { + if #available(iOS 16.0, *) { + $0.content + .presentationDetents([.fraction($0.fraction)]) } else { - return ActionSheet( - title: Text("Delete contact?\nThis cannot be undone!"), - buttons: [ - .destructive(Text("Delete")) { Task { await deleteChat(chat) } }, - .cancel() - ] - ) + $0.content } } } @@ -136,7 +163,7 @@ struct ChatListNavLink: View { switch (groupInfo.membership.memberStatus) { case .memInvited: ChatPreviewView(chat: chat, progressByTimeout: $progressByTimeout) - .frame(height: rowHeights[dynamicTypeSize]) + .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .trailing, allowsFullSwipe: true) { joinGroupButton() if groupInfo.canDelete { @@ -156,11 +183,12 @@ struct ChatListNavLink: View { .disabled(inProgress) case .memAccepted: ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) - .frame(height: rowHeights[dynamicTypeSize]) + .frameCompat(height: dynamicRowHeight) .onTapGesture { AlertManager.shared.showAlert(groupInvitationAcceptedAlert()) } .swipeActions(edge: .trailing) { + tagChatButton(chat) if (groupInfo.membership.memberCurrent) { leaveGroupChatButton(groupInfo) } @@ -170,31 +198,69 @@ struct ChatListNavLink: View { } default: NavLinkPlain( - tag: chat.chatInfo.id, + chatId: chat.chatInfo.id, selection: $chatModel.chatId, label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) }, disabled: !groupInfo.ready ) - .frame(height: rowHeights[dynamicTypeSize]) + .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .leading, allowsFullSwipe: true) { markReadButton() toggleFavoriteButton() - toggleNtfsButton(chat) + toggleNtfsButton(chat: chat) } .swipeActions(edge: .trailing, allowsFullSwipe: true) { - if !chat.chatItems.isEmpty { + tagChatButton(chat) + let showReportsButton = chat.chatStats.reportsCount > 0 && groupInfo.membership.memberRole >= .moderator + let showClearButton = !chat.chatItems.isEmpty + let showDeleteGroup = groupInfo.canDelete + let showLeaveGroup = groupInfo.membership.memberCurrent + let totalNumberOfButtons = 1 + (showReportsButton ? 1 : 0) + (showClearButton ? 1 : 0) + (showDeleteGroup ? 1 : 0) + (showLeaveGroup ? 1 : 0) + + if showClearButton && totalNumberOfButtons <= 3 { clearChatButton() } - if (groupInfo.membership.memberCurrent) { + + if showReportsButton && totalNumberOfButtons <= 3 { + archiveAllReportsButton() + } + + if showLeaveGroup { leaveGroupChatButton(groupInfo) } - if groupInfo.canDelete { + + if showDeleteGroup && totalNumberOfButtons <= 3 { deleteGroupChatButton(groupInfo) + } else if totalNumberOfButtons > 3 { + if showDeleteGroup && !groupInfo.membership.memberActive { + deleteGroupChatButton(groupInfo) + moreOptionsButton(false, chat, groupInfo) + } else { + moreOptionsButton(true, chat, groupInfo) + } } } } } + private func noteFolderNavLink(_ noteFolder: NoteFolder) -> some View { + NavLinkPlain( + chatId: chat.chatInfo.id, + selection: $chatModel.chatId, + label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) }, + disabled: !noteFolder.ready + ) + .frameCompat(height: dynamicRowHeight) + .swipeActions(edge: .leading, allowsFullSwipe: true) { + markReadButton() + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + if !chat.chatItems.isEmpty { + clearNoteFolderButton() + } + } + } + private func joinGroupButton() -> some View { Button { inProgress = true @@ -202,9 +268,9 @@ struct ChatListNavLink: View { await MainActor.run { inProgress = false } } } label: { - Label("Join", systemImage: chat.chatInfo.incognito ? "theatermasks" : "ipad.and.arrow.forward") + SwipeLabel(NSLocalizedString("Join", comment: "swipe action"), systemImage: chat.chatInfo.incognito ? "theatermasks" : "ipad.and.arrow.forward", inverted: oneHandUI) } - .tint(chat.chatInfo.incognito ? .indigo : .accentColor) + .tint(chat.chatInfo.incognito ? .indigo : theme.colors.primary) } @ViewBuilder private func markReadButton() -> some View { @@ -212,16 +278,16 @@ struct ChatListNavLink: View { Button { Task { await markChatRead(chat) } } label: { - Label("Read", systemImage: "checkmark") + SwipeLabel(NSLocalizedString("Read", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI) } - .tint(Color.accentColor) + .tint(theme.colors.primary) } else { Button { Task { await markChatUnread(chat) } } label: { - Label("Unread", systemImage: "circlebadge.fill") + SwipeLabel(NSLocalizedString("Unread", comment: "swipe action"), systemImage: "circlebadge.fill", inverted: oneHandUI) } - .tint(Color.accentColor) + .tint(theme.colors.primary) } } @@ -231,24 +297,118 @@ struct ChatListNavLink: View { Button { toggleChatFavorite(chat, favorite: false) } label: { - Label("Unfav.", systemImage: "star.slash") + SwipeLabel(NSLocalizedString("Unfav.", comment: "swipe action"), systemImage: "star.slash.fill", inverted: oneHandUI) } .tint(.green) } else { Button { toggleChatFavorite(chat, favorite: true) } label: { - Label("Favorite", systemImage: "star.fill") + SwipeLabel(NSLocalizedString("Favorite", comment: "swipe action"), systemImage: "star.fill", inverted: oneHandUI) } .tint(.green) } } + @ViewBuilder private func toggleNtfsButton(chat: Chat) -> some View { + if let nextMode = chat.chatInfo.nextNtfMode { + Button { + toggleNotifications(chat, enableNtfs: nextMode) + } label: { + SwipeLabel(nextMode.text(mentions: chat.chatInfo.hasMentions), systemImage: nextMode.iconFilled, inverted: oneHandUI) + } + } else { + EmptyView() + } + } + + private func archiveAllReportsButton() -> some View { + Button { + AlertManager.shared.showAlert(archiveAllReportsAlert()) + } label: { + SwipeLabel(NSLocalizedString("Archive reports", comment: "swipe action"), systemImage: "archivebox", inverted: oneHandUI) + } + } + private func clearChatButton() -> some View { Button { AlertManager.shared.showAlert(clearChatAlert()) } label: { - Label("Clear", systemImage: "gobackward") + SwipeLabel(NSLocalizedString("Clear", comment: "swipe action"), systemImage: "gobackward", inverted: oneHandUI) + } + .tint(Color.orange) + } + + private func tagChatButton(_ chat: Chat) -> some View { + Button { + setTagChatSheet(chat) + } label: { + SwipeLabel(NSLocalizedString("List", comment: "swipe action"), systemImage: "tag.fill", inverted: oneHandUI) + } + .tint(.mint) + } + + private func setTagChatSheet(_ chat: Chat) { + let screenHeight = UIScreen.main.bounds.height + let reservedSpace: Double = 4 * 44 // 2 for padding, 1 for "Create list" and another for extra tag + let tagsSpace = Double(max(chatTagsModel.userTags.count, 3)) * 44 + let fraction = min((reservedSpace + tagsSpace) / screenHeight, 0.62) + + parentSheet = SomeSheet( + content: { + AnyView( + NavigationView { + if chatTagsModel.userTags.isEmpty { + TagListEditor(chat: chat) + } else { + TagListView(chat: chat) + } + } + ) + }, + id: "lists sheet", + fraction: fraction + ) + } + + private func moreOptionsButton(_ canShowGroupDelete: Bool, _ chat: Chat, _ groupInfo: GroupInfo?) -> some View { + Button { + var buttons: [Alert.Button] = [] + buttons.append(.default(Text("Clear")) { + AlertManager.shared.showAlert(clearChatAlert()) + }) + + if let groupInfo, chat.chatStats.reportsCount > 0 && groupInfo.membership.memberRole >= .moderator && groupInfo.ready { + buttons.append(.default(Text("Archive reports")) { + AlertManager.shared.showAlert(archiveAllReportsAlert()) + }) + } + + if canShowGroupDelete, let gi = groupInfo, gi.canDelete { + buttons.append(.destructive(Text("Delete")) { + AlertManager.shared.showAlert(deleteGroupAlert(gi)) + }) + } + + buttons.append(.cancel()) + + actionSheet = SomeActionSheet( + actionSheet: ActionSheet( + title: canShowGroupDelete ? Text("Clear or delete group?") : Text("Clear group?"), + buttons: buttons + ), + id: "other options" + ) + } label: { + SwipeLabel(NSLocalizedString("More", comment: "swipe action"), systemImage: "ellipsis", inverted: oneHandUI) + } + } + + private func clearNoteFolderButton() -> some View { + Button { + AlertManager.shared.showAlert(clearNoteFolderAlert()) + } label: { + SwipeLabel(NSLocalizedString("Clear", comment: "swipe action"), systemImage: "gobackward", inverted: oneHandUI) } .tint(Color.orange) } @@ -257,7 +417,7 @@ struct ChatListNavLink: View { Button { AlertManager.shared.showAlert(leaveGroupAlert(groupInfo)) } label: { - Label("Leave", systemImage: "rectangle.portrait.and.arrow.right") + SwipeLabel(NSLocalizedString("Leave", comment: "swipe action"), systemImage: "rectangle.portrait.and.arrow.right.fill", inverted: oneHandUI) } .tint(Color.yellow) } @@ -266,32 +426,33 @@ struct ChatListNavLink: View { Button { AlertManager.shared.showAlert(deleteGroupAlert(groupInfo)) } label: { - Label("Delete", systemImage: "trash") + deleteLabel } .tint(.red) } private func contactRequestNavLink(_ contactRequest: UserContactRequest) -> some View { ContactRequestView(contactRequest: contactRequest, chat: chat) + .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button { Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } - } label: { Label("Accept", systemImage: "checkmark") } - .tint(.accentColor) + } label: { SwipeLabel(NSLocalizedString("Accept", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI) } + .tint(theme.colors.primary) Button { Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) } } label: { - Label("Accept incognito", systemImage: "theatermasks") + SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI) } .tint(.indigo) Button { AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequest)) } label: { - Label("Reject", systemImage: "multiply") + SwipeLabel(NSLocalizedString("Reject", comment: "swipe action"), systemImage: "multiply.fill", inverted: oneHandUI) } .tint(.red) } - .frame(height: rowHeights[dynamicTypeSize]) + .contentShape(Rectangle()) .onTapGesture { showContactRequestDialog = true } .confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) { Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } } @@ -302,38 +463,47 @@ struct ChatListNavLink: View { private func contactConnectionNavLink(_ contactConnection: PendingContactConnection) -> some View { ContactConnectionView(chat: chat) + .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button { AlertManager.shared.showAlert(deleteContactConnectionAlert(contactConnection) { a in AlertManager.shared.showAlertMsg(title: a.title, message: a.message) }) } label: { - Label("Delete", systemImage: "trash") + deleteLabel } .tint(.red) Button { showContactConnectionInfo = true } label: { - Label("Name", systemImage: "pencil") + SwipeLabel(NSLocalizedString("Name", comment: "swipe action"), systemImage: "pencil", inverted: oneHandUI) } - .tint(.accentColor) + .tint(theme.colors.primary) } - .frame(height: rowHeights[dynamicTypeSize]) - .sheet(isPresented: $showContactConnectionInfo) { - if case let .contactConnection(contactConnection) = chat.chatInfo { - ContactConnectionInfo(contactConnection: contactConnection) - .environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil) + .appSheet(isPresented: $showContactConnectionInfo) { + Group { + if case let .contactConnection(contactConnection) = chat.chatInfo { + ContactConnectionInfo(contactConnection: contactConnection) + .environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil) + .modifier(ThemedBackground(grouped: true)) + } } } + .contentShape(Rectangle()) .onTapGesture { showContactConnectionInfo = true } } + private var deleteLabel: some View { + SwipeLabel(NSLocalizedString("Delete", comment: "swipe action"), systemImage: "trash.fill", inverted: oneHandUI) + } + private func deleteGroupAlert(_ groupInfo: GroupInfo) -> Alert { - Alert( - title: Text("Delete group?"), + let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?" + return Alert( + title: Text(label), message: deleteGroupAlertMessage(groupInfo), primaryButton: .destructive(Text("Delete")) { Task { await deleteChat(chat) } @@ -342,8 +512,25 @@ struct ChatListNavLink: View { ) } - private func deleteGroupAlertMessage(_ groupInfo: GroupInfo) -> Text { - groupInfo.membership.memberCurrent ? Text("Group will be deleted for all members - this cannot be undone!") : Text("Group will be deleted for you - this cannot be undone!") + private func archiveAllReportsAlert() -> Alert { + Alert( + title: Text("Archive all reports?"), + message: Text("All reports will be archived for you."), + primaryButton: .destructive(Text("Archive")) { + Task { await archiveAllReportsForMe(chat.chatInfo.apiId) } + }, + secondaryButton: .cancel() + ) + } + + private func archiveAllReportsForMe(_ apiId: Int64) async { + do { + if case let .groupChatItemsDeleted(user, groupInfo, chatItemIDs, _, member) = try await apiArchiveReceivedReports(groupId: apiId) { + await groupChatItemsDeleted(user, groupInfo, chatItemIDs, member) + } + } catch { + logger.error("archiveAllReportsForMe error: \(responseError(error))") + } } private func clearChatAlert() -> Alert { @@ -357,10 +544,27 @@ struct ChatListNavLink: View { ) } - private func leaveGroupAlert(_ groupInfo: GroupInfo) -> Alert { + private func clearNoteFolderAlert() -> Alert { Alert( - title: Text("Leave group?"), - message: Text("You will stop receiving messages from this group. Chat history will be preserved."), + title: Text("Clear private notes?"), + message: Text("All messages will be deleted - this cannot be undone!"), + primaryButton: .destructive(Text("Clear")) { + Task { await clearChat(chat) } + }, + secondaryButton: .cancel() + ) + } + + private func leaveGroupAlert(_ groupInfo: GroupInfo) -> Alert { + let titleLabel: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group?" : "Leave chat?" + let messageLabel: LocalizedStringKey = ( + groupInfo.businessChat == nil + ? "You will stop receiving messages from this group. Chat history will be preserved." + : "You will stop receiving messages from this chat. Chat history will be preserved." + ) + return Alert( + title: Text(titleLabel), + message: Text(messageLabel), primaryButton: .destructive(Text("Leave")) { Task { await leaveGroup(groupInfo.groupId) } }, @@ -368,28 +572,6 @@ struct ChatListNavLink: View { ) } - private func rejectContactRequestAlert(_ contactRequest: UserContactRequest) -> Alert { - Alert( - title: Text("Reject contact request"), - message: Text("The sender will NOT be notified"), - primaryButton: .destructive(Text("Reject")) { - Task { await rejectContactRequest(contactRequest) } - }, - secondaryButton: .cancel() - ) - } - - private func pendingContactAlert(_ chat: Chat, _ contact: Contact) -> Alert { - Alert( - title: Text("Contact is not connected yet!"), - message: Text("Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)."), - primaryButton: .cancel(), - secondaryButton: .destructive(Text("Delete Contact")) { - removePendingContact(chat, contact) - } - ) - } - private func groupInvitationAcceptedAlert() -> Alert { Alert( title: Text("Joining group"), @@ -397,54 +579,59 @@ struct ChatListNavLink: View { ) } - private func deletePendingContactAlert(_ chat: Chat, _ contact: Contact) -> Alert { - Alert( - title: Text("Delete pending connection"), - message: Text("Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)."), - primaryButton: .destructive(Text("Delete")) { - removePendingContact(chat, contact) - }, - secondaryButton: .cancel() - ) - } - - private func removePendingContact(_ chat: Chat, _ contact: Contact) { - Task { - do { - try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId) - DispatchQueue.main.async { - chatModel.removeChat(contact.id) - } - } catch let error { - logger.error("ChatListNavLink.removePendingContact apiDeleteChat error: \(responseError(error))") - } - } - } - - private func invalidJSONPreview(_ json: String) -> some View { + private func invalidJSONPreview(_ json: Data?) -> some View { Text("invalid chat data") .foregroundColor(.red) .padding(4) - .frame(height: rowHeights[dynamicTypeSize]) + .frameCompat(height: dynamicRowHeight) .onTapGesture { showInvalidJSON = true } - .sheet(isPresented: $showInvalidJSON) { - invalidJSONView(json) + .appSheet(isPresented: $showInvalidJSON) { + invalidJSONView(dataToString(json)) .environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil) } } private func connectContactViaAddress_(_ contact: Contact, _ incognito: Bool) { Task { - let ok = await connectContactViaAddress(contact.contactId, incognito) + let ok = await connectContactViaAddress(contact.contactId, incognito, showAlert: { AlertManager.shared.showAlert($0) }) if ok { - await MainActor.run { - chatModel.chatId = contact.id + ItemsModel.shared.loadOpenChat(contact.id) { + AlertManager.shared.showAlert(connReqSentAlert(.contact)) } } } } } +extension View { + @inline(__always) + @ViewBuilder fileprivate func frameCompat(height: CGFloat) -> some View { + if #available(iOS 16, *) { + self.frame(height: height) + } else { + VStack(spacing: 0) { + Divider() + .padding(.leading, 16) + self + .frame(height: height) + .padding(.horizontal, 8) + .padding(.vertical, 8) + } + } + } +} + +func rejectContactRequestAlert(_ contactRequest: UserContactRequest) -> Alert { + Alert( + title: Text("Reject contact request"), + message: Text("The sender will NOT be notified"), + primaryButton: .destructive(Text("Reject")) { + Task { await rejectContactRequest(contactRequest) } + }, + secondaryButton: .cancel() + ) +} + func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, showError: @escaping (ErrorAlert) -> Void, success: @escaping () -> Void = {}) -> Alert { Alert( title: Text("Delete pending connection?"), @@ -471,15 +658,14 @@ func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, ) } -func connectContactViaAddress(_ contactId: Int64, _ incognito: Bool) async -> Bool { +func connectContactViaAddress(_ contactId: Int64, _ incognito: Bool, showAlert: (Alert) -> Void) async -> Bool { let (contact, alert) = await apiConnectContactViaAddress(incognito: incognito, contactId: contactId) if let alert = alert { - AlertManager.shared.showAlert(alert) + showAlert(alert) return false } else if let contact = contact { await MainActor.run { ChatModel.shared.updateContact(contact) - AlertManager.shared.showAlert(connReqSentAlert(.contact)) } return true } @@ -520,18 +706,11 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) { } } -struct ErrorAlert { - var title: LocalizedStringKey - var message: LocalizedStringKey -} - func getErrorAlert(_ error: Error, _ title: LocalizedStringKey) -> ErrorAlert { - switch error as? ChatResponse { - case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))): - return ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.") - case let .chatCmdError(_, .errorAgent(.BROKER(addr, .NETWORK))): - return ErrorAlert(title: "Connection error", message: "Please check your network connection with \(serverHostname(addr)) and try again.") - default: + if let r = error as? ChatError, + let alert = getNetworkErrorAlert(r) { + return alert + } else { return ErrorAlert(title: title, message: "Error: \(responseError(error))") } } @@ -543,15 +722,15 @@ struct ChatListNavLink_Previews: PreviewProvider { ChatListNavLink(chat: Chat( chatInfo: ChatInfo.sampleData.direct, chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")] - )) + ), parentSheet: .constant(nil)) ChatListNavLink(chat: Chat( chatInfo: ChatInfo.sampleData.direct, chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")] - )) + ), parentSheet: .constant(nil)) ChatListNavLink(chat: Chat( chatInfo: ChatInfo.sampleData.contactRequest, chatItems: [] - )) + ), parentSheet: .constant(nil)) } .previewLayout(.fixed(width: 360, height: 82)) } diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 1d86733206..f34f930c6f 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -9,15 +9,154 @@ import SwiftUI import SimpleXChat +enum UserPickerSheet: Identifiable { + case address + case chatPreferences + case chatProfiles + case currentProfile + case useFromDesktop + case settings + + var id: Self { self } + + var navigationTitle: LocalizedStringKey { + switch self { + case .address: "SimpleX address" + case .chatPreferences: "Your preferences" + case .chatProfiles: "Your chat profiles" + case .currentProfile: "Your current profile" + case .useFromDesktop: "Connect to desktop" + case .settings: "Your settings" + } + } +} + +enum PresetTag: Int, Identifiable, CaseIterable, Equatable { + case groupReports = 0 + case favorites = 1 + case contacts = 2 + case groups = 3 + case business = 4 + case notes = 5 + + var id: Int { rawValue } + + var сollapse: Bool { + self != .groupReports + } +} + +enum ActiveFilter: Identifiable, Equatable { + case presetTag(PresetTag) + case userTag(ChatTag) + case unread + + var id: String { + switch self { + case let .presetTag(tag): "preset \(tag.id)" + case let .userTag(tag): "user \(tag.chatTagId)" + case .unread: "unread" + } + } +} + +class SaveableSettings: ObservableObject { + @Published var servers: ServerSettings = ServerSettings(currUserServers: [], userServers: [], serverErrors: []) +} + +struct ServerSettings { + public var currUserServers: [UserOperatorServers] + public var userServers: [UserOperatorServers] + public var serverErrors: [UserServersError] +} + +struct UserPickerSheetView: View { + let sheet: UserPickerSheet + @EnvironmentObject var chatModel: ChatModel + @StateObject private var ss = SaveableSettings() + + @State private var loaded = false + + var body: some View { + NavigationView { + ZStack { + if loaded, let currentUser = chatModel.currentUser { + switch sheet { + case .address: + UserAddressView(shareViaProfile: currentUser.addressShared) + case .chatPreferences: + PreferencesView( + profile: currentUser.profile, + preferences: currentUser.fullPreferences, + currentPreferences: currentUser.fullPreferences + ) + case .chatProfiles: + UserProfilesView() + case .currentProfile: + UserProfile() + case .useFromDesktop: + ConnectDesktopView() + case .settings: + SettingsView() + } + } + Color.clear // Required for list background to be rendered during loading + } + .navigationTitle(sheet.navigationTitle) + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } + .overlay { + if let la = chatModel.laRequest { + LocalAuthView(authRequest: la) + } + } + .task { + withAnimation( + .easeOut(duration: 0.1), + { loaded = true } + ) + } + .onDisappear { + if serversCanBeSaved( + ss.servers.currUserServers, + ss.servers.userServers, + ss.servers.serverErrors + ) { + showAlert( + title: NSLocalizedString("Save servers?", comment: "alert title"), + buttonTitle: NSLocalizedString("Save", comment: "alert button"), + buttonAction: { saveServers($ss.servers.currUserServers, $ss.servers.userServers) }, + cancelButton: true + ) + } + } + .environmentObject(ss) + } +} + struct ChatListView: View { @EnvironmentObject var chatModel: ChatModel - @Binding var showSettings: Bool + @EnvironmentObject var theme: AppTheme + @Binding var activeUserPickerSheet: UserPickerSheet? + @State private var searchMode = false + @FocusState private var searchFocussed @State private var searchText = "" - @State private var showAddChat = false - @State private var userPickerVisible = false - @State private var showConnectDesktop = false - @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false + @State private var searchShowingSimplexLink = false + @State private var searchChatFilteredBySimplexLink: String? = nil + @State private var scrollToSearchBar = false + @State private var userPickerShown: Bool = false + @State private var sheet: SomeSheet<AnyView>? = nil + @StateObject private var chatTagsModel = ChatTagsModel.shared + // iOS 15 is required it to show/hide toolbar while chat is hidden/visible + @State private var viewOnScreen = true + + @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_ADDRESS_CREATION_CARD_SHOWN) private var addressCreationCardShown = false + @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial + var body: some View { if #available(iOS 16.0, *) { viewBody.scrollDismissesKeyboard(.immediately) @@ -25,50 +164,59 @@ struct ChatListView: View { viewBody } } - + private var viewBody: some View { - ZStack(alignment: .topLeading) { + ZStack(alignment: oneHandUI ? .bottomLeading : .topLeading) { NavStackCompat( isActive: Binding( get: { chatModel.chatId != nil }, - set: { _ in } + set: { active in + if !active { chatModel.chatId = nil } + } ), destination: chatView - ) { - VStack { - if chatModel.chats.isEmpty { - onboardingButtons() - } - chatListView + ) { chatListView } + } + .modifier( + Sheet(isPresented: $userPickerShown) { + UserPicker(userPickerShown: $userPickerShown, activeSheet: $activeUserPickerSheet) + } + ) + .appSheet( + item: $activeUserPickerSheet, + onDismiss: { chatModel.laRequest = nil }, + content: { UserPickerSheetView(sheet: $0) } + ) + .onChange(of: activeUserPickerSheet) { + if $0 != nil { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + userPickerShown = false } } - if userPickerVisible { - Rectangle().fill(.white.opacity(0.001)).onTapGesture { - withAnimation { - userPickerVisible.toggle() - } - } - } - UserPicker( - showSettings: $showSettings, - showConnectDesktop: $showConnectDesktop, - userPickerVisible: $userPickerVisible - ) - } - .sheet(isPresented: $showConnectDesktop) { - ConnectDesktopView() } + .environmentObject(chatTagsModel) } - + private var chatListView: some View { - VStack { - if chatModel.chats.count > 0 { - chatList.searchable(text: $searchText) - } else { - chatList + let tm = ToolbarMaterial.material(toolbarMaterial) + return withToolbar(tm) { + chatList + .background(theme.colors.background) + .navigationBarTitleDisplayMode(.inline) + .navigationBarHidden(searchMode || oneHandUI) + } + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + .onAppear { + if #unavailable(iOS 16.0), !viewOnScreen { + viewOnScreen = true + } + } + .onDisappear { + activeUserPickerSheet = nil + if #unavailable(iOS 16.0) { + viewOnScreen = false } } - .onDisappear() { withAnimation { userPickerVisible = false } } .refreshable { AlertManager.shared.showAlert(Alert( title: Text("Reconnect servers?"), @@ -85,166 +233,639 @@ struct ChatListView: View { secondaryButton: .cancel() )) } - .offset(x: -8) - .listStyle(.plain) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - let user = chatModel.currentUser ?? User.sampleData - ZStack(alignment: .topTrailing) { - ProfileImage(imageStr: user.image, color: Color(uiColor: .quaternaryLabel)) - .frame(width: 32, height: 32) - .padding(.trailing, 4) - let allRead = chatModel.users - .filter { u in !u.user.activeUser && !u.user.hidden } - .allSatisfy { u in u.unreadCount == 0 } - if !allRead { - unreadBadge(size: 12) + .safeAreaInset(edge: .top) { + if oneHandUI { Divider().background(tm) } + } + .safeAreaInset(edge: .bottom) { + if oneHandUI { + Divider().padding(.bottom, Self.hasHomeIndicator ? 0 : 8).background(tm) + } + } + .sheet(item: $sheet) { sheet in + if #available(iOS 16.0, *) { + sheet.content.presentationDetents([.fraction(sheet.fraction)]) + } else { + sheet.content + } + } + } + + static var hasHomeIndicator: Bool = { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first { + window.safeAreaInsets.bottom > 0 + } else { false } + }() + + @ViewBuilder func withToolbar(_ material: Material, content: () -> some View) -> some View { + if #available(iOS 16.0, *) { + if oneHandUI { + content() + .toolbarBackground(.hidden, for: .bottomBar) + .toolbar { bottomToolbar } + } else { + content() + .toolbarBackground(.automatic, for: .navigationBar) + .toolbarBackground(material) + .toolbar { topToolbar } + } + } else { + if oneHandUI { + content().toolbar { bottomToolbarGroup() } + } else { + content().toolbar { topToolbar } + } + } + } + + @ToolbarContentBuilder var topToolbar: some ToolbarContent { + ToolbarItem(placement: .topBarLeading) { leadingToolbarItem } + ToolbarItem(placement: .principal) { 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() + 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() + trailingToolbarItem.padding(.bottom, padding) + } + } + + @ViewBuilder var leadingToolbarItem: some View { + let user = chatModel.currentUser ?? User.sampleData + ZStack(alignment: .topTrailing) { + ProfileImage(imageStr: user.image, size: 32, color: Color(uiColor: .quaternaryLabel)) + .padding([.top, .trailing], 3) + let allRead = chatModel.users + .filter { u in !u.user.activeUser && !u.user.hidden } + .allSatisfy { u in u.unreadCount == 0 } + if !allRead { + unreadBadge(size: 12) + } + } + .onTapGesture { + userPickerShown = true + } + } + + @ViewBuilder var trailingToolbarItem: some View { + switch chatModel.chatRunning { + case .some(true): NewChatMenuButton() + case .some(false): chatStoppedIcon() + case .none: EmptyView() + } + } + + private var chatList: some View { + let cs = filteredChats() + return ZStack { + ScrollViewReader { scrollProxy in + List { + if !chatModel.chats.isEmpty { + ChatListSearchBar( + searchMode: $searchMode, + searchFocussed: $searchFocussed, + searchText: $searchText, + searchShowingSimplexLink: $searchShowingSimplexLink, + searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink, + parentSheet: $sheet + ) + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + .frame(maxWidth: .infinity) + .padding(.top, oneHandUI ? 8 : 0) + .id("searchBar") } - } - .onTapGesture { - if chatModel.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1 { - withAnimation { - userPickerVisible.toggle() + if #available(iOS 16.0, *) { + ForEach(cs, id: \.viewId) { chat in + ChatListNavLink(chat: chat, parentSheet: $sheet) + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + .padding(.trailing, -16) + .disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id)) + .listRowBackground(Color.clear) } + .offset(x: -8) } else { - showSettings = true + ForEach(cs, id: \.viewId) { chat in + ChatListNavLink(chat: chat, parentSheet: $sheet) + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets()) + .background { theme.colors.background } // Hides default list selection colour + .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() + .padding(.vertical, 6) + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + } + } + .listStyle(.plain) + .onChange(of: chatModel.chatId) { currentChatId in + if let chatId = chatModel.chatToTop, currentChatId != chatId { + chatModel.chatToTop = nil + chatModel.popChat(chatId) + } + stopAudioPlayer() + } + .onChange(of: chatModel.currentUser?.userId) { _ in + stopAudioPlayer() + } + .onChange(of: scrollToSearchBar) { scrollToSearchBar in + if scrollToSearchBar { + Task { self.scrollToSearchBar = false } + withAnimation { scrollProxy.scrollTo("searchBar") } } } } - ToolbarItem(placement: .principal) { + if cs.isEmpty && !chatModel.chats.isEmpty { + noChatsView() + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + .foregroundColor(.secondary) + } + } + } + + @ViewBuilder private func noChatsView() -> some View { + if searchString().isEmpty { + switch chatTagsModel.activeFilter { + case .presetTag: Text("No filtered chats") // this should not happen + case let .userTag(tag): Text("No chats in list \(tag.chatTagText)") + case .unread: + Button { + chatTagsModel.activeFilter = nil + } label: { + HStack { + Image(systemName: "line.3.horizontal.decrease") + Text("No unread chats") + } + } + case .none: Text("No chats") + } + } else { + Text("No chats found") + } + } + + + private func unreadBadge(size: CGFloat = 18) -> some View { + Circle() + .frame(width: size, height: size) + .foregroundColor(theme.colors.primary) + } + + @ViewBuilder private func chatView() -> some View { + if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) { + ChatView(chat: chat) + } + } + + func stopAudioPlayer() { + VoiceItemState.smallView.values.forEach { $0.audioPlayer?.stop() } + VoiceItemState.smallView = [:] + } + + private func filteredChats() -> [Chat] { + if let linkChatId = searchChatFilteredBySimplexLink { + return chatModel.chats.filter { $0.id == linkChatId } + } else { + let s = searchString() + return s == "" + ? chatModel.chats.filter { chat in + !chat.chatInfo.chatDeleted && !chat.chatInfo.contactCard && filtered(chat) + } + : chatModel.chats.filter { chat in + let cInfo = chat.chatInfo + return switch cInfo { + case let .direct(contact): + !contact.chatDeleted && !chat.chatInfo.contactCard && ( + ( viewNameContains(cInfo, s) || + contact.profile.displayName.localizedLowercase.contains(s) || + contact.fullName.localizedLowercase.contains(s) + ) + ) + case .group: viewNameContains(cInfo, s) + case .local: viewNameContains(cInfo, s) + case .contactRequest: viewNameContains(cInfo, s) + case let .contactConnection(conn): conn.localAlias.localizedLowercase.contains(s) + case .invalidJSON: false + } + } + } + + func filtered(_ chat: Chat) -> Bool { + switch chatTagsModel.activeFilter { + case let .presetTag(tag): presetTagMatchesChat(tag, chat.chatInfo, chat.chatStats) + case let .userTag(tag): chat.chatInfo.chatTags?.contains(tag.chatTagId) == true + case .unread: chat.unreadTag + case .none: true + } + } + + func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool { + cInfo.chatViewName.localizedLowercase.contains(s) + } + } + + func searchString() -> String { + searchShowingSimplexLink ? "" : searchText.trimmingCharacters(in: .whitespaces).localizedLowercase + } +} + +struct SubsStatusIndicator: View { + @State private var subs: SMPServerSubs = SMPServerSubs.newSMPServerSubs + @State private var hasSess: Bool = false + @State private var task: Task<Void, Never>? + @State private var showServersSummary = false + + @AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false + + var body: some View { + Button { + showServersSummary = true + } label: { + HStack(spacing: 4) { + Text("Chats").foregroundStyle(Color.primary).fixedSize().font(.headline) + SubscriptionStatusIndicatorView(subs: subs, hasSess: hasSess) + if showSubscriptionPercentage { + SubscriptionStatusPercentageView(subs: subs, hasSess: hasSess) + } + } + } + .disabled(ChatModel.shared.chatRunning != true) + .onAppear { + startTask() + } + .onDisappear { + stopTask() + } + .appSheet(isPresented: $showServersSummary) { + ServersSummaryView() + .environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil) + } + } + + private func startTask() { + task = Task { + while !Task.isCancelled { + if AppChatState.shared.value == .active, ChatModel.shared.chatRunning == true { + do { + let (subs, hasSess) = try await getAgentSubsTotal() + await MainActor.run { + self.subs = subs + self.hasSess = hasSess + } + } catch let error { + logger.error("getSubsTotal error: \(responseError(error))") + } + } + try? await Task.sleep(nanoseconds: 1_000_000_000) // Sleep for 1 second + } + } + } + + func stopTask() { + task?.cancel() + task = nil + } +} + +struct ChatListSearchBar: View { + @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme + @EnvironmentObject var chatTagsModel: ChatTagsModel + @Binding var searchMode: Bool + @FocusState.Binding var searchFocussed: Bool + @Binding var searchText: String + @Binding var searchShowingSimplexLink: Bool + @Binding var searchChatFilteredBySimplexLink: String? + @Binding var parentSheet: SomeSheet<AnyView>? + @State private var ignoreSearchTextChange = false + @State private var alert: PlanAndConnectAlert? + @State private var sheet: PlanAndConnectActionSheet? + + var body: some View { + VStack(spacing: 12) { + ScrollView([.horizontal], showsIndicators: false) { TagsView(parentSheet: $parentSheet, searchText: $searchText) } + HStack(spacing: 12) { HStack(spacing: 4) { - Text("Chats") - .font(.headline) - if chatModel.chats.count > 0 { - toggleFilterButton() + Image(systemName: "magnifyingglass") + TextField("Search or paste SimpleX link", text: $searchText) + .foregroundColor(searchShowingSimplexLink ? theme.colors.secondary : theme.colors.onBackground) + .disabled(searchShowingSimplexLink) + .focused($searchFocussed) + .frame(maxWidth: .infinity) + if !searchText.isEmpty { + Image(systemName: "xmark.circle.fill") + .onTapGesture { + searchText = "" + } } } - .frame(maxWidth: .infinity, alignment: .center) - } - ToolbarItem(placement: .navigationBarTrailing) { - switch chatModel.chatRunning { - case .some(true): NewChatButton(showAddChat: $showAddChat) - case .some(false): chatStoppedIcon() - case .none: EmptyView() + .padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7)) + .foregroundColor(theme.colors.secondary) + .background(Color(.tertiarySystemFill)) + .cornerRadius(10.0) + + if searchFocussed { + Text("Cancel") + .foregroundColor(theme.colors.primary) + .onTapGesture { + searchText = "" + searchFocussed = false + } + } else if m.chats.count > 0 { + toggleFilterButton() } } } + .onChange(of: searchFocussed) { sf in + withAnimation { searchMode = sf } + } + .onChange(of: searchText) { t in + if ignoreSearchTextChange { + ignoreSearchTextChange = false + } else { + if let link = strHasSingleSimplexLink(t.trimmingCharacters(in: .whitespaces)) { // if SimpleX link is pasted, show connection dialogue + searchFocussed = false + if case let .simplexLink(linkType, _, smpHosts) = link.format { + ignoreSearchTextChange = true + searchText = simplexLinkText(linkType, smpHosts) + } + searchShowingSimplexLink = true + searchChatFilteredBySimplexLink = nil + connect(link.text) + } else { + if t != "" { // if some other text is pasted, enter search mode + searchFocussed = true + } + searchShowingSimplexLink = false + searchChatFilteredBySimplexLink = nil + } + } + } + .onChange(of: chatTagsModel.activeFilter) { _ in + searchText = "" + } + .alert(item: $alert) { a in + planAndConnectAlert(a, dismiss: true, cleanup: { searchText = "" }) + } + .actionSheet(item: $sheet) { s in + planAndConnectActionSheet(s, dismiss: true, cleanup: { searchText = "" }) + } } private func toggleFilterButton() -> some View { + let showUnread = chatTagsModel.activeFilter == .unread + return ZStack { + Color.clear + .frame(width: 22, height: 22) + Image(systemName: showUnread ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease") + .resizable() + .scaledToFit() + .foregroundColor(showUnread ? theme.colors.primary : theme.colors.secondary) + .frame(width: showUnread ? 22 : 16, height: showUnread ? 22 : 16) + .onTapGesture { + if chatTagsModel.activeFilter == .unread { + chatTagsModel.activeFilter = nil + } else { + chatTagsModel.activeFilter = .unread + } + } + } + } + + private func connect(_ link: String) { + planAndConnect( + link, + showAlert: { alert = $0 }, + showActionSheet: { sheet = $0 }, + dismiss: false, + incognito: nil, + filterKnownContact: { searchChatFilteredBySimplexLink = $0.id }, + filterKnownGroup: { searchChatFilteredBySimplexLink = $0.id } + ) + } +} + +struct TagsView: View { + @EnvironmentObject var chatTagsModel: ChatTagsModel + @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @Binding var parentSheet: SomeSheet<AnyView>? + @Binding var searchText: String + + var body: some View { + HStack { + tagsView() + } + } + + @ViewBuilder private func tagsView() -> some View { + if chatTagsModel.presetTags.count > 1 { + if chatTagsModel.presetTags.count + chatTagsModel.userTags.count <= 3 { + expandedPresetTagsFiltersView() + } else { + collapsedTagsFilterView() + ForEach(PresetTag.allCases, id: \.id) { (tag: PresetTag) in + if !tag.сollapse && (chatTagsModel.presetTags[tag] ?? 0) > 0 { + expandedTagFilterView(tag) + } + } + } + } + let selectedTag: ChatTag? = if case let .userTag(tag) = chatTagsModel.activeFilter { + tag + } else { + nil + } + ForEach(chatTagsModel.userTags, id: \.id) { tag in + let current = tag == selectedTag + let color: Color = current ? .accentColor : .secondary + ZStack { + HStack(spacing: 4) { + if let emoji = tag.chatTagEmoji { + Text(emoji) + } else { + Image(systemName: current ? "tag.fill" : "tag") + .foregroundColor(color) + } + ZStack { + let badge = Text(verbatim: (chatTagsModel.unreadTags[tag.chatTagId] ?? 0) > 0 ? " ●" : "").font(.footnote) + (Text(tag.chatTagText).fontWeight(.semibold) + badge).foregroundColor(.clear) + Text(tag.chatTagText).fontWeight(current ? .semibold : .regular).foregroundColor(color) + badge.foregroundColor(theme.colors.primary) + } + } + .onTapGesture { + setActiveFilter(filter: .userTag(tag)) + } + .onLongPressGesture { + let screenHeight = UIScreen.main.bounds.height + let reservedSpace: Double = 4 * 44 // 2 for padding, 1 for "Create list" and another for extra tag + let tagsSpace = Double(max(chatTagsModel.userTags.count, 3)) * 44 + let fraction = min((reservedSpace + tagsSpace) / screenHeight, 0.62) + + parentSheet = SomeSheet( + content: { + AnyView( + NavigationView { + TagListView(chat: nil) + .modifier(ThemedBackground(grouped: true)) + } + ) + }, + id: "tag list", + fraction: fraction + ) + } + } + } + Button { - showUnreadAndFavorites = !showUnreadAndFavorites + parentSheet = SomeSheet( + content: { + AnyView( + NavigationView { + TagListEditor() + } + ) + }, + id: "tag create" + ) } label: { - Image(systemName: "line.3.horizontal.decrease.circle" + (showUnreadAndFavorites ? ".fill" : "")) - .foregroundColor(.accentColor) + if chatTagsModel.userTags.isEmpty { + HStack(spacing: 4) { + Image(systemName: "plus") + Text("Add list") + } + } else { + Image(systemName: "plus") + } + } + .foregroundColor(.secondary) + } + + @ViewBuilder private func expandedTagFilterView(_ tag: PresetTag) -> some View { + let selectedPresetTag: PresetTag? = if case let .presetTag(tag) = chatTagsModel.activeFilter { + tag + } else { + nil + } + let active = tag == selectedPresetTag + let (icon, text) = presetTagLabel(tag: tag, active: active) + let color: Color = active ? .accentColor : .secondary + + HStack(spacing: 4) { + Image(systemName: icon) + .foregroundColor(color) + ZStack { + Text(text).fontWeight(.semibold).foregroundColor(.clear) + Text(text).fontWeight(active ? .semibold : .regular).foregroundColor(color) + } + } + .onTapGesture { + setActiveFilter(filter: .presetTag(tag)) } } - @ViewBuilder private var chatList: some View { - let cs = filteredChats() - ZStack { - List { - ForEach(cs, id: \.viewId) { chat in - ChatListNavLink(chat: chat) - .padding(.trailing, -16) - .disabled(chatModel.chatRunning != true) + private func expandedPresetTagsFiltersView() -> some View { + ForEach(PresetTag.allCases, id: \.id) { tag in + if (chatTagsModel.presetTags[tag] ?? 0) > 0 { + expandedTagFilterView(tag) + } + } + } + + @ViewBuilder private func collapsedTagsFilterView() -> some View { + let selectedPresetTag: PresetTag? = if case let .presetTag(tag) = chatTagsModel.activeFilter { + tag + } else { + nil + } + Menu { + if chatTagsModel.activeFilter != nil || !searchText.isEmpty { + Button { + chatTagsModel.activeFilter = nil + searchText = "" + } label: { + HStack { + Image(systemName: "list.bullet") + Text("All") + } } } - .onChange(of: chatModel.chatId) { _ in - if chatModel.chatId == nil, let chatId = chatModel.chatToTop { - chatModel.chatToTop = nil - chatModel.popChat(chatId) + ForEach(PresetTag.allCases, id: \.id) { tag in + if (chatTagsModel.presetTags[tag] ?? 0) > 0 && tag.сollapse { + Button { + setActiveFilter(filter: .presetTag(tag)) + } label: { + let (systemName, text) = presetTagLabel(tag: tag, active: tag == selectedPresetTag) + HStack { + Image(systemName: systemName) + Text(text) + } + } } } - if cs.isEmpty && !chatModel.chats.isEmpty { - Text("No filtered chats").foregroundColor(.secondary) + } label: { + if let tag = selectedPresetTag, tag.сollapse { + let (systemName, _) = presetTagLabel(tag: tag, active: true) + Image(systemName: systemName) + .foregroundColor(.accentColor) + } else { + Image(systemName: "list.bullet") + .foregroundColor(.secondary) } } + .frame(minWidth: 28) + } + + private func presetTagLabel(tag: PresetTag, active: Bool) -> (String, 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") + } } - private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View { - Circle() - .frame(width: size, height: size) - .foregroundColor(.accentColor) - } - - private func onboardingButtons() -> some View { - VStack(alignment: .trailing, spacing: 0) { - Path { p in - p.move(to: CGPoint(x: 8, y: 0)) - p.addLine(to: CGPoint(x: 16, y: 10)) - p.addLine(to: CGPoint(x: 0, y: 10)) - p.addLine(to: CGPoint(x: 8, y: 0)) - } - .fill(Color.accentColor) - .frame(width: 20, height: 10) - .padding(.trailing, 12) - - connectButton("Tap to start a new chat") { - showAddChat = true - } - - Spacer() - Text("You have no chats") - .foregroundColor(.secondary) - .frame(maxWidth: .infinity) - } - .padding(.trailing, 6) - .frame(maxHeight: .infinity) - } - - private func connectButton(_ label: LocalizedStringKey, action: @escaping () -> Void) -> some View { - Button(action: action) { - Text(label) - .padding(.vertical, 10) - .padding(.horizontal, 20) - } - .background(Color.accentColor) - .foregroundColor(.white) - .clipShape(RoundedRectangle(cornerRadius: 16)) - } - - @ViewBuilder private func chatView() -> some View { - if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) { - ChatView(chat: chat).onAppear { - loadChat(chat: chat) - } - } - } - - private func filteredChats() -> [Chat] { - let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase - return s == "" && !showUnreadAndFavorites - ? chatModel.chats - : chatModel.chats.filter { chat in - let cInfo = chat.chatInfo - switch cInfo { - case let .direct(contact): - return s == "" - ? filtered(chat) - : (viewNameContains(cInfo, s) || - contact.profile.displayName.localizedLowercase.contains(s) || - contact.fullName.localizedLowercase.contains(s)) - case let .group(gInfo): - return s == "" - ? (filtered(chat) || gInfo.membership.memberStatus == .memInvited) - : viewNameContains(cInfo, s) - case .contactRequest: - return s == "" || viewNameContains(cInfo, s) - case let .contactConnection(conn): - return s != "" && conn.localAlias.localizedLowercase.contains(s) - case .invalidJSON: - return false - } - } - - func filtered(_ chat: Chat) -> Bool { - (chat.chatInfo.chatSettings?.favorite ?? false) || chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat - } - - func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool { - cInfo.chatViewName.localizedLowercase.contains(s) + private func setActiveFilter(filter: ActiveFilter) { + if filter != chatTagsModel.activeFilter { + chatTagsModel.activeFilter = filter + } else { + chatTagsModel.activeFilter = nil } } } @@ -260,28 +881,59 @@ func chatStoppedIcon() -> some View { } } +func presetTagMatchesChat(_ tag: PresetTag, _ chatInfo: ChatInfo, _ chatStats: ChatStats) -> Bool { + switch tag { + case .groupReports: + chatStats.reportsCount > 0 + case .favorites: + chatInfo.chatSettings?.favorite == true + case .contacts: + switch chatInfo { + case let .direct(contact): !(contact.activeConn == nil && contact.profile.contactLink != nil && contact.active) && !contact.chatDeleted + case .contactRequest: true + case .contactConnection: true + case let .group(groupInfo): groupInfo.businessChat?.chatType == .customer + default: false + } + case .groups: + switch chatInfo { + case let .group(groupInfo): groupInfo.businessChat == nil + default: false + } + case .business: + chatInfo.groupInfo?.businessChat?.chatType == .business + case .notes: + switch chatInfo { + case .local: true + default: false + } + } +} + struct ChatListView_Previews: PreviewProvider { + @State static var userPickerSheet: UserPickerSheet? = .none + static var previews: some View { let chatModel = ChatModel() - chatModel.chats = [ - Chat( + chatModel.updateChats([ + ChatData( chatInfo: ChatInfo.sampleData.direct, chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")] ), - Chat( + ChatData( chatInfo: ChatInfo.sampleData.group, chatItems: [ChatItem.getSample(1, .directSnd, .now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")] ), - Chat( + ChatData( chatInfo: ChatInfo.sampleData.contactRequest, chatItems: [] ) - ] + ]) return Group { - ChatListView(showSettings: Binding.constant(false)) + ChatListView(activeUserPickerSheet: $userPickerSheet) .environmentObject(chatModel) - ChatListView(showSettings: Binding.constant(false)) + ChatListView(activeUserPickerSheet: $userPickerSheet) .environmentObject(ChatModel()) } } diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 30068114f3..b8c8233e6e 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -11,50 +11,126 @@ import SimpleXChat struct ChatPreviewView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize @ObservedObject var chat: Chat @Binding var progressByTimeout: Bool - @Environment(\.colorScheme) var colorScheme + @State var deleting: Bool = false var darkGreen = Color(red: 0, green: 0.5, blue: 0) + @State private var activeContentPreview: ActiveContentPreview? = nil + @State private var showFullscreenGallery: Bool = false @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true + var dynamicMediaSize: CGFloat { dynamicSize(userFont).mediaSize } + var dynamicChatInfoSize: CGFloat { dynamicSize(userFont).chatInfoSize } + var body: some View { let cItem = chat.chatItems.last - return HStack(spacing: 8) { - ZStack(alignment: .bottomTrailing) { - ChatInfoImage(chat: chat) - .frame(width: 63, height: 63) - chatPreviewImageOverlayIcon() - .padding([.bottom, .trailing], 1) - } - .padding(.leading, 4) - - VStack(spacing: 0) { - HStack(alignment: .top) { - chatPreviewTitle() - Spacer() - (cItem?.timestampText ?? formatTimestampText(chat.chatInfo.updatedAt)) - .font(.subheadline) - .frame(minWidth: 60, alignment: .trailing) - .foregroundColor(.secondary) - .padding(.top, 4) + return ZStack { + HStack(spacing: 8) { + ZStack(alignment: .bottomTrailing) { + ChatInfoImage(chat: chat, size: dynamicSize(userFont).profileImageSize) + chatPreviewImageOverlayIcon() + .padding([.bottom, .trailing], 1) } - .padding(.bottom, 4) - .padding(.horizontal, 8) - - ZStack(alignment: .topTrailing) { - chatMessagePreview(cItem) - chatStatusImage() - .padding(.top, 26) - .frame(maxWidth: .infinity, alignment: .trailing) - } - .padding(.trailing, 8) + .padding(.leading, 4) - Spacer() + let chatTs = if let cItem { + cItem.meta.itemTs + } else { + chat.chatInfo.chatTs + } + VStack(spacing: 0) { + HStack(alignment: .top) { + chatPreviewTitle() + Spacer() + (formatTimestampText(chatTs)) + .font(.subheadline) + .frame(minWidth: 60, alignment: .trailing) + .foregroundColor(theme.colors.secondary) + .padding(.top, 4) + } + .padding(.bottom, 4) + .padding(.horizontal, 8) + + ZStack(alignment: .topTrailing) { + let chat = activeContentPreview?.chat ?? chat + let ci = activeContentPreview?.ci ?? chat.chatItems.last + let mc = ci?.content.msgContent + HStack(alignment: .top) { + let deleted = ci?.isDeletedContent == true || ci?.meta.itemDeleted != nil + let showContentPreview = (showChatPreviews && chatModel.draftChatId != chat.id && !deleted) || activeContentPreview != nil + if let ci, showContentPreview { + chatItemContentPreview(chat, ci) + } + let mcIsVoice = switch mc { case .voice: true; default: false } + if !mcIsVoice || !showContentPreview || mc?.text != "" || chatModel.draftChatId == chat.id { + let hasFilePreview = if case .file = mc { true } else { false } + chatMessagePreview(cItem, hasFilePreview) + } else { + Spacer() + chatInfoIcon(chat).frame(minWidth: 37, alignment: .trailing) + } + } + .onChange(of: chatModel.stopPreviousRecPlay?.path) { _ in + checkActiveContentPreview(chat, ci, mc) + } + .onChange(of: activeContentPreview) { _ in + checkActiveContentPreview(chat, ci, mc) + } + .onChange(of: showFullscreenGallery) { _ in + checkActiveContentPreview(chat, ci, mc) + } + chatStatusImage() + .padding(.top, dynamicChatInfoSize * 1.44) + .frame(maxWidth: .infinity, alignment: .trailing) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.trailing, 8) + + Spacer() + } + .frame(maxHeight: .infinity) + } + .opacity(deleting ? 0.4 : 1) + .padding(.bottom, -8) + + if deleting { + ProgressView() + .scaleEffect(2) + } + } + .onChange(of: chatModel.deletedChats.contains(chat.chatInfo.id)) { contains in + deleting = contains + // Stop voice when deleting the chat + if contains, let ci = activeContentPreview?.ci { + VoiceItemState.stopVoiceInSmallView(chat.chatInfo, ci) + } + } + + func checkActiveContentPreview(_ chat: Chat, _ ci: ChatItem?, _ mc: MsgContent?) { + let playing = chatModel.stopPreviousRecPlay + if case .voice = activeContentPreview?.mc, playing == nil { + activeContentPreview = nil + } else if activeContentPreview == nil { + if case .image = mc, let ci, let mc, showFullscreenGallery { + activeContentPreview = ActiveContentPreview(chat: chat, ci: ci, mc: mc) + } + if case .video = mc, let ci, let mc, showFullscreenGallery { + activeContentPreview = ActiveContentPreview(chat: chat, ci: ci, mc: mc) + } + if case .voice = mc, let ci, let mc, let fileSource = ci.file?.fileSource, playing?.path.hasSuffix(fileSource.filePath) == true { + activeContentPreview = ActiveContentPreview(chat: chat, ci: ci, mc: mc) + } + } else if case .voice = activeContentPreview?.mc { + if let playing, let fileSource = ci?.file?.fileSource, !playing.path.hasSuffix(fileSource.filePath) { + activeContentPreview = nil + } + } else if !showFullscreenGallery { + activeContentPreview = nil } - .frame(maxHeight: .infinity) } - .padding(.bottom, -8) } @ViewBuilder private func chatPreviewImageOverlayIcon() -> some View { @@ -67,6 +143,7 @@ struct ChatPreviewView: View { } case let .group(groupInfo): switch (groupInfo.membership.memberStatus) { + case .memRejected: inactiveIcon() case .memLeft: inactiveIcon() case .memRemoved: inactiveIcon() case .memGroupDeleted: inactiveIcon() @@ -77,7 +154,7 @@ struct ChatPreviewView: View { } } - @ViewBuilder private func inactiveIcon() -> some View { + private func inactiveIcon() -> some View { Image(systemName: "multiply.circle.fill") .foregroundColor(.secondary.opacity(0.65)) .background(Circle().foregroundColor(Color(uiColor: .systemBackground))) @@ -87,13 +164,13 @@ struct ChatPreviewView: View { let t = Text(chat.chatInfo.chatViewName).font(.title3).fontWeight(.bold) switch chat.chatInfo { case let .direct(contact): - previewTitle(contact.verified == true ? verifiedIcon + t : t) + previewTitle(contact.verified == true ? verifiedIcon + t : t).foregroundColor(deleting ? Color.secondary : nil) case let .group(groupInfo): let v = previewTitle(t) switch (groupInfo.membership.memberStatus) { - case .memInvited: v.foregroundColor(chat.chatInfo.incognito ? .indigo : .accentColor) - case .memAccepted: v.foregroundColor(.secondary) - default: v + case .memInvited: v.foregroundColor(deleting ? theme.colors.secondary : chat.chatInfo.incognito ? .indigo : theme.colors.primary) + case .memAccepted, .memRejected: v.foregroundColor(theme.colors.secondary) + default: if deleting { v.foregroundColor(theme.colors.secondary) } else { v } } default: previewTitle(t) } @@ -104,61 +181,100 @@ struct ChatPreviewView: View { } private var verifiedIcon: Text { - (Text(Image(systemName: "checkmark.shield")) + Text(" ")) - .foregroundColor(.secondary) + (Text(Image(systemName: "checkmark.shield")) + textSpace) + .foregroundColor(theme.colors.secondary) .baselineOffset(1) .kerning(-2) } - private func chatPreviewLayout(_ text: Text, draft: Bool = false) -> some View { + private func chatPreviewLayout(_ text: Text?, draft: Bool = false, hasFilePreview: Bool = false, hasSecrets: Bool) -> some View { ZStack(alignment: .topTrailing) { + let s = chat.chatStats + let mentionWidth: CGFloat = if s.unreadMentions > 0 && s.unreadCount > 1 { dynamicSize(userFont).unreadCorner } else { 0 } let t = text - .lineLimit(2) + .lineLimit(userFont <= .xxxLarge ? 2 : 1) .multilineTextAlignment(.leading) + .if(hasSecrets, transform: hiddenSecretsView) .frame(maxWidth: .infinity, alignment: .topLeading) - .padding(.leading, 8) - .padding(.trailing, 36) + .padding(.leading, hasFilePreview ? 0 : 8) + .padding(.trailing, mentionWidth + (hasFilePreview ? 38 : 36)) + .offset(x: hasFilePreview ? -2 : 0) + .fixedSize(horizontal: false, vertical: true) if !showChatPreviews && !draft { t.privacySensitive(true).redacted(reason: .privacy) } else { t } - let s = chat.chatStats - if s.unreadCount > 0 || s.unreadChat { - unreadCountText(s.unreadCount) - .font(.caption) - .foregroundColor(.white) - .padding(.horizontal, 4) - .frame(minWidth: 18, minHeight: 18) - .background(chat.chatInfo.ntfsEnabled ? Color.accentColor : Color.secondary) - .cornerRadius(10) - } else if !chat.chatInfo.ntfsEnabled { - Image(systemName: "speaker.slash.fill") - .foregroundColor(.secondary) - } else if chat.chatInfo.chatSettings?.favorite ?? false { - Image(systemName: "star.fill") - .resizable() - .scaledToFill() - .frame(width: 18, height: 18) - .padding(.trailing, 1) - .foregroundColor(.secondary.opacity(0.65)) - } + chatInfoIcon(chat).frame(minWidth: 37, alignment: .trailing) } } - private func messageDraft(_ draft: ComposeState) -> Text { + @ViewBuilder private func chatInfoIcon(_ chat: Chat) -> some View { + let s = chat.chatStats + if s.unreadCount > 0 || s.unreadChat { + let mentionColor = mentionColor(chat) + HStack(alignment: .center, spacing: 2) { + if s.unreadMentions > 0 && s.unreadCount > 1 { + Text("\(MENTION_START)") + .font(userFont <= .xxxLarge ? .body : .callout) + .foregroundColor(mentionColor) + .frame(minWidth: dynamicChatInfoSize, minHeight: dynamicChatInfoSize) + .cornerRadius(dynamicSize(userFont).unreadCorner) + .padding(.bottom, 1) + } + let singleUnreadIsMention = s.unreadMentions > 0 && s.unreadCount == 1 + (singleUnreadIsMention ? Text("\(MENTION_START)") : unreadCountText(s.unreadCount)) + .font(userFont <= .xxxLarge ? .caption : .caption2) + .foregroundColor(.white) + .padding(.horizontal, dynamicSize(userFont).unreadPadding) + .frame(minWidth: dynamicChatInfoSize, minHeight: dynamicChatInfoSize) + .background(singleUnreadIsMention ? mentionColor : chat.chatInfo.ntfsEnabled(false) || chat.chatInfo.chatType == .local ? theme.colors.primary : theme.colors.secondary) + .cornerRadius(dynamicSize(userFont).unreadCorner) + } + .frame(height: dynamicChatInfoSize) + } else if let ntfMode = chat.chatInfo.chatSettings?.enableNtfs, ntfMode != .all { + let iconSize = ntfMode == .mentions ? dynamicChatInfoSize * 0.8 : dynamicChatInfoSize + let iconColor = ntfMode == .mentions ? theme.colors.secondary.opacity(0.7) : theme.colors.secondary + Image(systemName: ntfMode.iconFilled) + .resizable() + .scaledToFill() + .frame(width: iconSize, height: iconSize) + .foregroundColor(iconColor) + } else if chat.chatInfo.chatSettings?.favorite ?? false { + Image(systemName: "star.fill") + .resizable() + .scaledToFill() + .frame(width: dynamicChatInfoSize, height: dynamicChatInfoSize) + .padding(.trailing, 1) + .foregroundColor(theme.colors.secondary.opacity(0.65)) + } else { + Color.clear.frame(width: 0) + } + } + + private func mentionColor(_ chat: Chat) -> Color { + switch chat.chatInfo.chatSettings?.enableNtfs { + case .all: theme.colors.primary + case .mentions: theme.colors.primary + default: theme.colors.secondary + } + } + + private func messageDraft(_ draft: ComposeState) -> (Text, Bool) { let msg = draft.message - return image("rectangle.and.pencil.and.ellipsis", color: .accentColor) - + attachment() - + messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true) + let r = messageText(msg, parseSimpleXMarkdown(msg), sender: nil, preview: true, mentions: draft.mentions, userMemberId: nil, showSecrets: nil, backgroundColor: UIColor(theme.colors.background)) + return (image("rectangle.and.pencil.and.ellipsis", color: theme.colors.primary) + + attachment() + + Text(AttributedString(r.string)), + r.hasSecrets) func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text { - Text(Image(systemName: s)).foregroundColor(color) + Text(" ") + Text(Image(systemName: s)).foregroundColor(color) + textSpace } func attachment() -> Text { switch draft.preview { - case let .filePreview(fileName, _): return image("doc.fill") + Text(fileName) + Text(" ") + case let .filePreview(fileName, _): return image("doc.fill") + Text(fileName) + textSpace case .mediaPreviews: return image("photo") case let .voicePreview(_, duration): return image("play.fill") + Text(durationText(duration)) default: return Text("") @@ -166,10 +282,26 @@ struct ChatPreviewView: View { } } - func chatItemPreview(_ cItem: ChatItem) -> Text { - let itemText = cItem.meta.itemDeleted == nil ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text") + func chatItemPreview(_ cItem: ChatItem) -> (Text, Bool) { + let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText() let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil - return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true) + let r = messageText(itemText, itemFormattedText, sender: 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) + + // 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 { + if cItem.meta.itemDeleted != nil, cItem.isReport { + "archived report" + } else { + switch cItem.meta.itemDeleted { + case let .moderated(_, byGroupMember): String.localizedStringWithFormat(NSLocalizedString("moderated by %@", comment: "marked deleted chat item preview text"), byGroupMember.displayName) + case .blocked: NSLocalizedString("blocked", comment: "marked deleted chat item preview text") + case .blockedByAdmin: NSLocalizedString("blocked by admin", comment: "marked deleted chat item preview text") + case .deleted, nil: NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text") + } + } + } func attachment() -> String? { switch cItem.content.msgContent { @@ -180,20 +312,29 @@ struct ChatPreviewView: View { default: return nil } } + + func prefix() -> NSAttributedString? { + switch cItem.content.msgContent { + case let .report(_, reason): reason.attrString + default: nil + } + } } - @ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?) -> some View { + @ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?, _ hasFilePreview: Bool = false) -> some View { if chatModel.draftChatId == chat.id, let draft = chatModel.draft { - chatPreviewLayout(messageDraft(draft), draft: true) + let (t, hasSecrets) = messageDraft(draft) + chatPreviewLayout(t, draft: true, hasFilePreview: hasFilePreview, hasSecrets: hasSecrets) } else if let cItem = cItem { - chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem)) + let (t, hasSecrets) = chatItemPreview(cItem) + chatPreviewLayout(itemStatusMark(cItem) + t, hasFilePreview: hasFilePreview, hasSecrets: hasSecrets) } else { switch (chat.chatInfo) { case let .direct(contact): - if contact.activeConn == nil && contact.profile.contactLink != nil { + if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active { chatPreviewInfoText("Tap to Connect") - .foregroundColor(.accentColor) - } else if !contact.ready && contact.activeConn != nil { + .foregroundColor(theme.colors.primary) + } else if !contact.sndReady && contact.activeConn != nil { if contact.nextSendGrpInv { chatPreviewInfoText("send direct message") } else if contact.active { @@ -202,6 +343,7 @@ struct ChatPreviewView: View { } case let .group(groupInfo): switch (groupInfo.membership.memberStatus) { + case .memRejected: chatPreviewInfoText("rejected") case .memInvited: groupInvitationPreviewText(groupInfo) case .memAccepted: chatPreviewInfoText("connecting…") default: EmptyView() @@ -211,13 +353,59 @@ struct ChatPreviewView: View { } } + @ViewBuilder func chatItemContentPreview(_ chat: Chat, _ ci: ChatItem) -> some View { + let mc = ci.content.msgContent + switch mc { + case let .link(_, preview): + smallContentPreview(size: dynamicMediaSize) { + ZStack(alignment: .topTrailing) { + Image(uiImage: imageFromBase64(preview.image) ?? UIImage(systemName: "arrow.up.right")!) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: dynamicMediaSize, height: dynamicMediaSize) + ZStack { + Image(systemName: "arrow.up.right") + .resizable() + .foregroundColor(Color.white) + .font(.system(size: 15, weight: .black)) + .frame(width: 8, height: 8) + } + .frame(width: 16, height: 16) + .background(Color.black.opacity(0.25)) + .cornerRadius(8) + } + .onTapGesture { + openBrowserAlert(uri: preview.uri) + } + } + case let .image(_, image): + smallContentPreview(size: dynamicMediaSize) { + CIImageView(chatItem: ci, preview: imageFromBase64(image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery) + } + case let .video(_,image, duration): + smallContentPreview(size: dynamicMediaSize) { + CIVideoView(chatItem: ci, preview: imageFromBase64(image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery) + } + case let .voice(_, duration): + smallContentPreviewVoice(size: dynamicMediaSize) { + CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: Binding.constant(true), smallViewSize: dynamicMediaSize) + } + case .file: + smallContentPreviewFile(size: dynamicMediaSize) { + CIFileView(file: ci.file, edited: ci.meta.itemEdited, smallViewSize: dynamicMediaSize) + } + default: EmptyView() + } + } + + @ViewBuilder private func groupInvitationPreviewText(_ groupInfo: GroupInfo) -> some View { groupInfo.membership.memberIncognito ? chatPreviewInfoText("join as \(groupInfo.membership.memberProfile.displayName)") : chatPreviewInfoText("you are invited to group") } - @ViewBuilder private func chatPreviewInfoText(_ text: LocalizedStringKey) -> some View { + private func chatPreviewInfoText(_ text: LocalizedStringKey) -> some View { Text(text) .frame(maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading) .padding([.leading, .trailing], 8) @@ -226,64 +414,124 @@ struct ChatPreviewView: View { private func itemStatusMark(_ cItem: ChatItem) -> Text { switch cItem.meta.itemStatus { - case .sndErrorAuth: + case .sndErrorAuth, .sndError: return Text(Image(systemName: "multiply")) .font(.caption) - .foregroundColor(.red) + Text(" ") - case .sndError: + .foregroundColor(.red) + textSpace + case .sndWarning: return Text(Image(systemName: "exclamationmark.triangle.fill")) .font(.caption) - .foregroundColor(.yellow) + Text(" ") + .foregroundColor(.orange) + textSpace default: return Text("") } } @ViewBuilder private func chatStatusImage() -> some View { + let size = dynamicSize(userFont).incognitoSize switch chat.chatInfo { case let .direct(contact): if contact.active && contact.activeConn != nil { - switch (chatModel.contactNetworkStatus(contact)) { - case .connected: incognitoIcon(chat.chatInfo.incognito) - case .error: - Image(systemName: "exclamationmark.circle") - .resizable() - .scaledToFit() - .frame(width: 17, height: 17) - .foregroundColor(.secondary) - default: - ProgressView() - } + NetworkStatusView(contact: contact, size: size) } else { - incognitoIcon(chat.chatInfo.incognito) + incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size) } case .group: if progressByTimeout { ProgressView() + } else if chat.chatStats.reportsCount > 0 { + groupReportsIcon(size: size * 0.8) } else { - incognitoIcon(chat.chatInfo.incognito) + incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size) } default: - incognitoIcon(chat.chatInfo.incognito) + incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size) + } + } + + struct NetworkStatusView: View { + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + @EnvironmentObject var theme: AppTheme + @ObservedObject var networkModel = NetworkModel.shared + + let contact: Contact + let size: CGFloat + + var body: some View { + let dynamicChatInfoSize = dynamicSize(userFont).chatInfoSize + switch (networkModel.contactNetworkStatus(contact)) { + case .connected: incognitoIcon(contact.contactConnIncognito, theme.colors.secondary, size: size) + case .error: + Image(systemName: "exclamationmark.circle") + .resizable() + .scaledToFit() + .frame(width: dynamicChatInfoSize, height: dynamicChatInfoSize) + .foregroundColor(theme.colors.secondary) + default: + ProgressView() + } } } } -@ViewBuilder func incognitoIcon(_ incognito: Bool) -> some View { +@ViewBuilder func incognitoIcon(_ incognito: Bool, _ secondaryColor: Color, size: CGFloat) -> some View { if incognito { Image(systemName: "theatermasks") .resizable() .scaledToFit() - .frame(width: 22, height: 22) - .foregroundColor(.secondary) + .frame(width: size, height: size) + .foregroundColor(secondaryColor) } else { EmptyView() } } +func groupReportsIcon(size: CGFloat) -> some View { + Image(systemName: "flag") + .resizable() + .scaledToFit() + .frame(width: size, height: size) + .foregroundColor(.red) +} + +func smallContentPreview(size: CGFloat, _ 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)) + .padding(.vertical, size / 6) + .padding(.leading, 3) + .offset(x: 6) +} + +func smallContentPreviewVoice(size: CGFloat, _ view: @escaping () -> some View) -> some View { + view() + .frame(height: voiceMessageSizeBasedOnSquareSize(size)) + .padding(.vertical, size / 6) + .padding(.leading, 8) +} + +func smallContentPreviewFile(size: CGFloat, _ view: @escaping () -> some View) -> some View { + view() + .frame(width: size, height: size) + .padding(.vertical, size / 7) + .padding(.leading, 5) +} + func unreadCountText(_ n: Int) -> Text { Text(n > 999 ? "\(n / 1000)k" : n > 0 ? "\(n)" : "") } +private struct ActiveContentPreview: Equatable { + var chat: Chat + var ci: ChatItem + var mc: MsgContent + + static func == (lhs: ActiveContentPreview, rhs: ActiveContentPreview) -> Bool { + lhs.chat.id == rhs.chat.id && lhs.ci.id == rhs.ci.id && lhs.mc == rhs.mc + } +} + struct ChatPreviewView_Previews: PreviewProvider { static var previews: some View { Group { diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift index 6d2fba99c6..b9f5b984e1 100644 --- a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift +++ b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift @@ -11,8 +11,10 @@ import SimpleXChat struct ContactConnectionInfo: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction @State var contactConnection: PendingContactConnection + @State private var showShortLink: Bool = true @State private var alert: CCInfoAlert? @State private var localAlias = "" @State private var showIncognitoSheet = false @@ -20,7 +22,7 @@ struct ContactConnectionInfo: View { enum CCInfoAlert: Identifiable { case deleteInvitationAlert - case error(title: LocalizedStringKey, error: LocalizedStringKey) + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -48,7 +50,7 @@ struct ContactConnectionInfo: View { Section { if contactConnection.groupLinkId == nil { - settingsRow("pencil") { + settingsRow("pencil", color: theme.colors.secondary) { TextField("Set contact name…", text: $localAlias) .autocapitalization(.none) .autocorrectionDisabled(true) @@ -60,17 +62,23 @@ struct ContactConnectionInfo: View { } if contactConnection.initiated, - let connReqInv = contactConnection.connReqInv { - SimpleXLinkQRCode(uri: simplexChatLink(connReqInv)) + let connLinkInv = contactConnection.connLinkInv { + SimpleXCreatedLinkQRCode(link: connLinkInv, short: $showShortLink) + .id("simplex-invitation-qrcode-\(connLinkInv.simplexChatUri(short: showShortLink))") incognitoEnabled() - shareLinkButton(connReqInv) + shareLinkButton(connLinkInv, short: showShortLink) oneTimeLinkLearnMoreButton() } else { incognitoEnabled() oneTimeLinkLearnMoreButton() } + } header: { + if let connLinkInv = contactConnection.connLinkInv, connLinkInv.connShortLink != nil { + ToggleShortLinkHeader(text: Text(""), link: connLinkInv, short: $showShortLink) + } } footer: { sharedProfileInfo(contactConnection.incognito) + .foregroundColor(theme.colors.secondary) } Section { @@ -82,6 +90,7 @@ struct ContactConnectionInfo: View { } } } + .modifier(ThemedBackground(grouped: true)) if #available(iOS 16, *) { v } else { @@ -99,7 +108,7 @@ struct ContactConnectionInfo: View { } success: { dismiss() } - case let .error(title, error): return Alert(title: Text(title), message: Text(error)) + case let .error(title, error): return mkAlert(title: title, message: error) } } .onAppear { @@ -149,7 +158,7 @@ struct ContactConnectionInfo: View { HStack(spacing: 6) { Text("Incognito") Image(systemName: "info.circle") - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) .font(.system(size: 14)) } .onTapGesture { @@ -164,6 +173,25 @@ struct ContactConnectionInfo: View { } } +private func shareLinkButton(_ connLinkInvitation: CreatedConnLink, short: Bool) -> some View { + Button { + showShareSheet(items: [connLinkInvitation.simplexChatUri(short: short)]) + } label: { + Label("Share 1-time link", systemImage: "square.and.arrow.up") + } +} + +private func oneTimeLinkLearnMoreButton() -> some View { + NavigationLink { + AddContactLearnMore(showTitle: false) + .navigationTitle("One-time invitation link") + .modifier(ThemedBackground()) + .navigationBarTitleDisplayMode(.large) + } label: { + Label("Learn more", systemImage: "info.circle") + } +} + struct ContactConnectionInfo_Previews: PreviewProvider { static var previews: some View { ContactConnectionInfo(contactConnection: PendingContactConnection.getSampleData()) diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift index d21f347881..f5156d86b8 100644 --- a/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift +++ b/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift @@ -12,9 +12,10 @@ import SimpleXChat struct ContactConnectionView: View { @EnvironmentObject var m: ChatModel @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize @State private var localAlias = "" @FocusState private var aliasTextFieldFocused: Bool - @State private var showContactConnectionInfo = false var body: some View { if case let .contactConnection(conn) = chat.chatInfo { @@ -29,8 +30,7 @@ struct ContactConnectionView: View { .resizable() .scaledToFill() .frame(width: 48, height: 48) - .foregroundColor(Color(uiColor: .secondarySystemBackground)) - .onTapGesture { showContactConnectionInfo = true } + .foregroundColor(Color(uiColor: .tertiarySystemGroupedBackground).asAnotherColorFromSecondaryVariant(theme)) } .frame(width: 63, height: 63) .padding(.leading, 4) @@ -41,7 +41,7 @@ struct ContactConnectionView: View { .font(.title3) .bold() .allowsTightening(false) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .padding(.horizontal, 8) .padding(.top, 1) .padding(.bottom, 0.5) @@ -54,14 +54,14 @@ struct ContactConnectionView: View { .padding(.trailing, 8) .padding(.vertical, 4) .frame(minWidth: 60, alignment: .trailing) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } .padding(.bottom, 2) ZStack(alignment: .topTrailing) { Text(contactConnection.description) .frame(maxWidth: .infinity, alignment: .leading) - incognitoIcon(contactConnection.incognito) + incognitoIcon(contactConnection.incognito, theme.colors.secondary, size: dynamicSize(userFont).incognitoSize) .padding(.top, 26) .frame(maxWidth: .infinity, alignment: .trailing) } @@ -70,9 +70,6 @@ struct ContactConnectionView: View { Spacer() } .frame(maxHeight: .infinity) - .appSheet(isPresented: $showContactConnectionInfo) { - ContactConnectionInfo(contactConnection: contactConnection) - } } } } diff --git a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift index c5c062a6ec..9276bbfc78 100644 --- a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift +++ b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift @@ -11,20 +11,21 @@ import SimpleXChat struct ContactRequestView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize var contactRequest: UserContactRequest @ObservedObject var chat: Chat var body: some View { HStack(spacing: 8) { - ChatInfoImage(chat: chat) - .frame(width: 63, height: 63) + ChatInfoImage(chat: chat, size: dynamicSize(userFont).profileImageSize) .padding(.leading, 4) VStack(alignment: .leading, spacing: 0) { HStack(alignment: .top) { Text(contactRequest.chatViewName) .font(.title3) .fontWeight(.bold) - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) .padding(.leading, 8) .frame(alignment: .topLeading) Spacer() @@ -33,7 +34,7 @@ struct ContactRequestView: View { .padding(.trailing, 8) .padding(.top, 4) .frame(minWidth: 60, alignment: .trailing) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } .padding(.bottom, 2) diff --git a/apps/ios/Shared/Views/ChatList/OneHandUICard.swift b/apps/ios/Shared/Views/ChatList/OneHandUICard.swift new file mode 100644 index 0000000000..059f24cc82 --- /dev/null +++ b/apps/ios/Shared/Views/ChatList/OneHandUICard.swift @@ -0,0 +1,51 @@ +// +// OneHandUICard.swift +// SimpleX (iOS) +// +// Created by EP on 06/08/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +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 + @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) + } + Image(systemName: "multiply") + .foregroundColor(theme.colors.secondary) + .onTapGesture { + showOneHandUIAlert = true + } + } + .padding() + .background(theme.appColors.sentMessage) + .cornerRadius(12) + .frame(height: dynamicSize(userFont).rowHeight) + .alert(isPresented: $showOneHandUIAlert) { + Alert( + title: Text("Reachable chat toolbar"), + message: Text("You can change it in Appearance settings."), + dismissButton: .default(Text("Ok")) { + withAnimation { + oneHandUICardShown = true + } + } + ) + } + } +} + +#Preview { + OneHandUICard() +} diff --git a/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift b/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift new file mode 100644 index 0000000000..8b0a8af888 --- /dev/null +++ b/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift @@ -0,0 +1,746 @@ +// +// ServersSummaryView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 25.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ServersSummaryView: View { + @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme + @State private var serversSummary: PresentedServersSummary? = nil + @State private var selectedUserCategory: PresentedUserCategory = .allUsers + @State private var selectedServerType: PresentedServerType = .smp + @State private var selectedSMPServer: String? = nil + @State private var selectedXFTPServer: String? = nil + @State private var timer: Timer? = nil + @State private var alert: SomeAlert? + + @AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false + + enum PresentedUserCategory { + case currentUser + case allUsers + } + + enum PresentedServerType { + case smp + case xftp + } + + var body: some View { + NavigationView { + viewBody() + .navigationTitle("Servers info") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + shareButton() + } + } + } + .onAppear { + if m.users.filter({ u in u.user.activeUser || !u.user.hidden }).count == 1 { + selectedUserCategory = .currentUser + } + getServersSummary() + startTimer() + } + .onDisappear { + stopTimer() + } + .alert(item: $alert) { $0.alert } + } + + private func startTimer() { + timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in + if AppChatState.shared.value == .active { + getServersSummary() + } + } + } + + private func getServersSummary() { + do { + serversSummary = try getAgentServersSummary() + } catch let error { + logger.error("getAgentServersSummary error: \(responseError(error))") + } + } + + private func stopTimer() { + timer?.invalidate() + timer = nil + } + + private func shareButton() -> some View { + Button { + if let serversSummary = serversSummary { + showShareSheet(items: [encodePrettyPrinted(serversSummary)]) + } + } label: { + Image(systemName: "square.and.arrow.up") + } + .disabled(serversSummary == nil) + } + + public func encodePrettyPrinted<T: Encodable>(_ value: T) -> String { + let encoder = jsonEncoder + encoder.outputFormatting = .prettyPrinted + let data = try! encoder.encode(value) + return String(decoding: data, as: UTF8.self) + } + + @ViewBuilder private func viewBody() -> some View { + if let summ = serversSummary { + List { + Group { + if m.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1 { + Picker("User selection", selection: $selectedUserCategory) { + Text("All profiles").tag(PresentedUserCategory.allUsers) + Text("Current profile").tag(PresentedUserCategory.currentUser) + } + .pickerStyle(.segmented) + } + + Picker("Server type", selection: $selectedServerType) { + Text("Messages").tag(PresentedServerType.smp) + Text("Files").tag(PresentedServerType.xftp) + } + .pickerStyle(.segmented) + } + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + + switch (selectedUserCategory, selectedServerType) { + case (.allUsers, .smp): + let smpSumm = summ.allUsersSMP + let (totals, curr, prev, prox) = (smpSumm.smpTotals, smpSumm.currentlyUsedSMPServers, smpSumm.previouslyUsedSMPServers, smpSumm.onlyProxiedSMPServers) + + SMPStatsView(stats: totals.stats, statsStartedAt: summ.statsStartedAt) + + smpSubsSection(totals) + + if curr.count > 0 { + smpServersListView(curr, summ.statsStartedAt, "Connected servers") + } + if prev.count > 0 { + smpServersListView(prev, summ.statsStartedAt, "Previously connected servers") + } + if prox.count > 0 { + smpServersListView(prox, summ.statsStartedAt, "Proxied servers", "You are not connected to these servers. Private routing is used to deliver messages to them.") + } + + ServerSessionsView(sess: totals.sessions) + case (.currentUser, .smp): + let smpSumm = summ.currentUserSMP + let (totals, curr, prev, prox) = (smpSumm.smpTotals, smpSumm.currentlyUsedSMPServers, smpSumm.previouslyUsedSMPServers, smpSumm.onlyProxiedSMPServers) + + SMPStatsView(stats: totals.stats, statsStartedAt: summ.statsStartedAt) + + smpSubsSection(totals) + + if curr.count > 0 { + smpServersListView(curr, summ.statsStartedAt, "Connected servers") + } + if prev.count > 0 { + smpServersListView(prev, summ.statsStartedAt, "Previously connected servers") + } + if prox.count > 0 { + smpServersListView(prox, summ.statsStartedAt, "Proxied servers", "You are not connected to these servers. Private routing is used to deliver messages to them.") + } + + ServerSessionsView(sess: totals.sessions) + case (.allUsers, .xftp): + let xftpSumm = summ.allUsersXFTP + let (totals, curr, prev) = (xftpSumm.xftpTotals, xftpSumm.currentlyUsedXFTPServers, xftpSumm.previouslyUsedXFTPServers) + + XFTPStatsView(stats: totals.stats, statsStartedAt: summ.statsStartedAt) + + if curr.count > 0 { + xftpServersListView(curr, summ.statsStartedAt, "Connected servers") + } + if prev.count > 0 { + xftpServersListView(prev, summ.statsStartedAt, "Previously connected servers") + } + + ServerSessionsView(sess: totals.sessions) + case (.currentUser, .xftp): + let xftpSumm = summ.currentUserXFTP + let (totals, curr, prev) = (xftpSumm.xftpTotals, xftpSumm.currentlyUsedXFTPServers, xftpSumm.previouslyUsedXFTPServers) + + XFTPStatsView(stats: totals.stats, statsStartedAt: summ.statsStartedAt) + + if curr.count > 0 { + xftpServersListView(curr, summ.statsStartedAt, "Connected servers") + } + if prev.count > 0 { + xftpServersListView(prev, summ.statsStartedAt, "Previously connected servers") + } + + ServerSessionsView(sess: totals.sessions) + } + + Section { + reconnectAllButton() + resetStatsButton() + } + } + } else { + Text("No info, try to reload") + .foregroundColor(theme.colors.secondary) + .background(theme.colors.background) + } + } + + private func smpSubsSection(_ totals: SMPTotals) -> some View { + Section { + infoRow("Active connections", numOrDash(totals.subs.ssActive)) + infoRow("Total", numOrDash(totals.subs.total)) + Toggle("Show percentage", isOn: $showSubscriptionPercentage) + } header: { + HStack { + Text("Message reception") + SubscriptionStatusIndicatorView(subs: totals.subs, hasSess: totals.sessions.hasSess) + if showSubscriptionPercentage { + SubscriptionStatusPercentageView(subs: totals.subs, hasSess: totals.sessions.hasSess) + } + } + } + } + + private func reconnectAllButton() -> some View { + Button { + alert = SomeAlert( + alert: Alert( + title: Text("Reconnect all servers?"), + message: Text("Reconnect all connected servers to force message delivery. It uses additional traffic."), + primaryButton: .default(Text("Ok")) { + Task { + do { + try await reconnectAllServers() + } catch let error { + alert = SomeAlert( + alert: mkAlert( + title: "Error reconnecting servers", + message: "\(responseError(error))" + ), + id: "error reconnecting servers" + ) + } + } + }, + secondaryButton: .cancel() + ), + id: "reconnect servers question" + ) + } label: { + Text("Reconnect all servers") + } + } + + private func smpServersListView( + _ servers: [SMPServerSummary], + _ statsStartedAt: Date, + _ header: LocalizedStringKey? = nil, + _ footer: LocalizedStringKey? = nil + ) -> some View { + let sortedServers = servers.sorted { + $0.hasSubs == $1.hasSubs + ? serverAddress($0.smpServer) < serverAddress($1.smpServer) + : $0.hasSubs && !$1.hasSubs + } + return Section { + ForEach(sortedServers) { server in + smpServerView(server, statsStartedAt) + } + } header: { + if let header = header { + Text(header) + } + } footer: { + if let footer = footer { + Text(footer) + } + } + } + + private func smpServerView(_ srvSumm: SMPServerSummary, _ statsStartedAt: Date) -> some View { + NavigationLink(tag: srvSumm.id, selection: $selectedSMPServer) { + SMPServerSummaryView( + summary: srvSumm, + statsStartedAt: statsStartedAt + ) + .navigationBarTitle("SMP server") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } label: { + HStack { + Text(serverAddress(srvSumm.smpServer)) + .lineLimit(1) + if let subs = srvSumm.subs { + Spacer() + if showSubscriptionPercentage { + SubscriptionStatusPercentageView(subs: subs, hasSess: srvSumm.sessionsOrNew.hasSess) + } + SubscriptionStatusIndicatorView(subs: subs, hasSess: srvSumm.sessionsOrNew.hasSess) + } else if let sess = srvSumm.sessions { + Spacer() + Image(systemName: "arrow.up.circle") + .symbolRenderingMode(.palette) + .foregroundStyle(sessIconColor(sess), Color.clear) + } + } + } + } + + private func serverAddress(_ server: String) -> String { + parseServerAddress(server)?.hostnames.first ?? server + } + + private func sessIconColor(_ sess: ServerSessions) -> Color { + let online = m.networkInfo.online + return ( + online && sess.ssConnected > 0 + ? sessionActiveColor + : Color(uiColor: .tertiaryLabel) + ) + } + + private var sessionActiveColor: Color { + let onionHosts = networkUseOnionHostsGroupDefault.get() + return onionHosts == .require ? .indigo : .accentColor + } + + private func xftpServersListView( + _ servers: [XFTPServerSummary], + _ statsStartedAt: Date, + _ header: LocalizedStringKey? = nil, + _ footer: LocalizedStringKey? = nil + ) -> some View { + let sortedServers = servers.sorted { serverAddress($0.xftpServer) < serverAddress($1.xftpServer) } + return Section { + ForEach(sortedServers) { server in + xftpServerView(server, statsStartedAt) + } + } header: { + if let header = header { + Text(header) + } + } footer: { + if let footer = footer { + Text(footer) + } + } + } + + private func xftpServerView(_ srvSumm: XFTPServerSummary, _ statsStartedAt: Date) -> some View { + NavigationLink(tag: srvSumm.id, selection: $selectedXFTPServer) { + XFTPServerSummaryView( + summary: srvSumm, + statsStartedAt: statsStartedAt + ) + .navigationBarTitle("XFTP server") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } label: { + HStack { + Text(serverAddress(srvSumm.xftpServer)) + .lineLimit(1) + if let inProgressIcon = inProgressIcon(srvSumm) { + Spacer() + Image(systemName: inProgressIcon) + .symbolRenderingMode(.palette) + .foregroundStyle(sessionActiveColor, Color.clear) + } + } + } + } + + private func inProgressIcon(_ srvSumm: XFTPServerSummary) -> String? { + switch (srvSumm.rcvInProgress, srvSumm.sndInProgress, srvSumm.delInProgress) { + case (false, false, false): nil + case (true, false, false): "arrow.down.circle" + case (false, true, false): "arrow.up.circle" + case (false, false, true): "trash.circle" + default: "arrow.up.arrow.down.circle" + } + } + + private func resetStatsButton() -> some View { + Button { + alert = SomeAlert( + alert: Alert( + title: Text("Reset all statistics?"), + message: Text("Servers statistics will be reset - this cannot be undone!"), + primaryButton: .destructive(Text("Reset")) { + Task { + do { + try await resetAgentServersStats() + getServersSummary() + } catch let error { + alert = SomeAlert( + alert: mkAlert( + title: "Error resetting statistics", + message: "\(responseError(error))" + ), + id: "error resetting statistics" + ) + } + } + }, + secondaryButton: .cancel() + ), + id: "reset statistics question" + ) + } label: { + Text("Reset all statistics") + } + } +} + +struct SubscriptionStatusIndicatorView: View { + @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme + var subs: SMPServerSubs + var hasSess: Bool + + var body: some View { + let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage( + online: m.networkInfo.online, + usesProxy: networkUseOnionHostsGroupDefault.get() != .no || groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) != nil, + subs: subs, + hasSess: hasSess, + primaryColor: theme.colors.primary + ) + if #available(iOS 16.0, *) { + Image(systemName: "dot.radiowaves.up.forward", variableValue: variableValue) + .foregroundColor(color) + } else { + Image(systemName: "dot.radiowaves.up.forward") + .foregroundColor(color.opacity(opacity)) + } + } +} + +struct SubscriptionStatusPercentageView: View { + @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme + var subs: SMPServerSubs + var hasSess: Bool + + var body: some View { + let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage( + online: m.networkInfo.online, + usesProxy: networkUseOnionHostsGroupDefault.get() != .no || groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) != nil, + subs: subs, + hasSess: hasSess, + primaryColor: theme.colors.primary + ) + Text(verbatim: "\(Int(floor(statusPercent * 100)))%") + .foregroundColor(.secondary) + .font(.caption) + } +} + +func subscriptionStatusColorAndPercentage(online: Bool, usesProxy: Bool, subs: SMPServerSubs, hasSess: Bool, primaryColor: Color) -> (Color, Double, Double, Double) { + func roundedToQuarter(_ n: Double) -> Double { + n >= 1 ? 1 + : n <= 0 ? 0 + : (n * 4).rounded() / 4 + } + + let activeColor: Color = usesProxy ? .indigo : primaryColor + let noConnColorAndPercent: (Color, Double, Double, Double) = (Color(uiColor: .tertiaryLabel), 1, 1, 0) + let activeSubsRounded = roundedToQuarter(subs.shareOfActive) + + return !online + ? noConnColorAndPercent + : ( + subs.total == 0 && !hasSess + ? (activeColor, 0, 0.33, 0) // On freshly installed app (without chats) and on app start + : ( + subs.ssActive == 0 + ? ( + hasSess ? (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) : noConnColorAndPercent + ) + : ( // ssActive > 0 + hasSess + ? (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) + : (.orange, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) // This would mean implementation error + ) + ) + ) +} + +struct SMPServerSummaryView: View { + var summary: SMPServerSummary + var statsStartedAt: Date + @State private var alert: SomeAlert? + + @AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false + + var body: some View { + List { + Section("Server address") { + Text(summary.smpServer) + .textSelection(.enabled) + } + + if let stats = summary.stats { + SMPStatsView(stats: stats, statsStartedAt: statsStartedAt) + } + + if let subs = summary.subs { + smpSubsSection(subs) + } + + if let sess = summary.sessions { + ServerSessionsView(sess: sess) + } + } + .alert(item: $alert) { $0.alert } + } + + private func smpSubsSection(_ subs: SMPServerSubs) -> some View { + Section { + infoRow("Active connections", numOrDash(subs.ssActive)) + infoRow("Pending", numOrDash(subs.ssPending)) + infoRow("Total", numOrDash(subs.total)) + reconnectButton() + } header: { + HStack { + Text("Message reception") + SubscriptionStatusIndicatorView(subs: subs, hasSess: summary.sessionsOrNew.hasSess) + if showSubscriptionPercentage { + SubscriptionStatusPercentageView(subs: subs, hasSess: summary.sessionsOrNew.hasSess) + } + } + } + } + + private func reconnectButton() -> some View { + Button { + alert = SomeAlert( + alert: Alert( + title: Text("Reconnect server?"), + message: Text("Reconnect server to force message delivery. It uses additional traffic."), + primaryButton: .default(Text("Ok")) { + Task { + do { + try await reconnectServer(smpServer: summary.smpServer) + } catch let error { + alert = SomeAlert( + alert: mkAlert( + title: "Error reconnecting server", + message: "\(responseError(error))" + ), + id: "error reconnecting server" + ) + } + } + }, + secondaryButton: .cancel() + ), + id: "reconnect server question" + ) + } label: { + Text("Reconnect") + } + } +} + +struct ServerSessionsView: View { + var sess: ServerSessions + + var body: some View { + Section("Transport sessions") { + infoRow("Connected", numOrDash(sess.ssConnected)) + infoRow("Errors", numOrDash(sess.ssErrors)) + infoRow("Connecting", numOrDash(sess.ssConnecting)) + } + } +} + +struct SMPStatsView: View { + var stats: AgentSMPServerStatsData + var statsStartedAt: Date + + var body: some View { + Section { + infoRow("Messages sent", numOrDash(stats._sentDirect + stats._sentViaProxy)) + infoRow("Messages received", numOrDash(stats._recvMsgs)) + NavigationLink { + DetailedSMPStatsView(stats: stats, statsStartedAt: statsStartedAt) + .navigationTitle("Detailed statistics") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } label: { + Text("Details") + } + } header: { + Text("Statistics") + } footer: { + Text("Starting from \(localTimestamp(statsStartedAt)).") + textNewLine + Text("All data is kept private on your device.") + } + } +} + +private func numOrDash(_ n: Int) -> String { + n == 0 ? "-" : "\(n)" +} + +struct DetailedSMPStatsView: View { + var stats: AgentSMPServerStatsData + var statsStartedAt: Date + + var body: some View { + List { + Section("Sent messages") { + infoRow("Sent total", numOrDash(stats._sentDirect + stats._sentViaProxy)) + infoRowTwoValues("Sent directly", "attempts", stats._sentDirect, stats._sentDirectAttempts) + infoRowTwoValues("Sent via proxy", "attempts", stats._sentViaProxy, stats._sentViaProxyAttempts) + infoRowTwoValues("Proxied", "attempts", stats._sentProxied, stats._sentProxiedAttempts) + Text("Send errors") + infoRow(Text(verbatim: "AUTH"), numOrDash(stats._sentAuthErrs)).padding(.leading, 24) + infoRow(Text(verbatim: "QUOTA"), numOrDash(stats._sentQuotaErrs)).padding(.leading, 24) + infoRow("expired", numOrDash(stats._sentExpiredErrs)).padding(.leading, 24) + infoRow("other", numOrDash(stats._sentOtherErrs)).padding(.leading, 24) + } + Section("Received messages") { + infoRow("Received total", numOrDash(stats._recvMsgs)) + Text("Receive errors") + infoRow("duplicates", numOrDash(stats._recvDuplicates)).padding(.leading, 24) + infoRow("decryption errors", numOrDash(stats._recvCryptoErrs)).padding(.leading, 24) + infoRow("other errors", numOrDash(stats._recvErrs)).padding(.leading, 24) + infoRowTwoValues("Acknowledged", "attempts", stats._ackMsgs, stats._ackAttempts) + Text("Acknowledgement errors") + infoRow(Text(verbatim: "NO_MSG errors"), numOrDash(stats._ackNoMsgErrs)).padding(.leading, 24) + infoRow("other errors", numOrDash(stats._ackOtherErrs)).padding(.leading, 24) + } + Section("Connections") { + infoRow("Created", numOrDash(stats._connCreated)) + infoRow("Secured", numOrDash(stats._connCreated)) + infoRow("Completed", numOrDash(stats._connCompleted)) + infoRowTwoValues("Deleted", "attempts", stats._connDeleted, stats._connDelAttempts) + infoRow("Deletion errors", numOrDash(stats._connDelErrs)) + infoRowTwoValues("Subscribed", "attempts", stats._connSubscribed, stats._connSubAttempts) + infoRow("Subscriptions ignored", numOrDash(stats._connSubIgnored)) + infoRow("Subscription errors", numOrDash(stats._connSubErrs)) + } + Section { + infoRowTwoValues("Enabled", "attempts", stats._ntfKey, stats._ntfKeyAttempts) + infoRowTwoValues("Disabled", "attempts", stats._ntfKeyDeleted, stats._ntfKeyDeleteAttempts) + } header: { + Text("Connection notifications") + } footer: { + Text("Starting from \(localTimestamp(statsStartedAt)).") + } + } + } +} + +private func infoRowTwoValues(_ title: LocalizedStringKey, _ title2: LocalizedStringKey, _ value: Int, _ value2: Int) -> some View { + HStack { + Text(title) + Text(verbatim: " / ").font(.caption2) + Text(title2).font(.caption2) + Spacer() + Group { + if value == 0 && value2 == 0 { + Text(verbatim: "-") + } else { + Text(numOrDash(value)) + Text(verbatim: " / ").font(.caption2) + Text(numOrDash(value2)).font(.caption2) + } + } + .foregroundStyle(.secondary) + } +} + +struct XFTPServerSummaryView: View { + var summary: XFTPServerSummary + var statsStartedAt: Date + + var body: some View { + List { + Section("Server address") { + Text(summary.xftpServer) + .textSelection(.enabled) + } + + if let stats = summary.stats { + XFTPStatsView(stats: stats, statsStartedAt: statsStartedAt) + } + + if let sess = summary.sessions { + ServerSessionsView(sess: sess) + } + } + } +} + +struct XFTPStatsView: View { + var stats: AgentXFTPServerStatsData + var statsStartedAt: Date + @State private var expanded = false + + var body: some View { + Section { + infoRow("Uploaded", prettySize(stats._uploadsSize)) + infoRow("Downloaded", prettySize(stats._downloadsSize)) + NavigationLink { + DetailedXFTPStatsView(stats: stats, statsStartedAt: statsStartedAt) + .navigationTitle("Detailed statistics") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } label: { + Text("Details") + } + } header: { + Text("Statistics") + } footer: { + Text("Starting from \(localTimestamp(statsStartedAt)).") + textNewLine + Text("All data is kept private on your device.") + } + } +} + +private func prettySize(_ sizeInKB: Int64) -> String { + let kb: Int64 = 1024 + return sizeInKB == 0 ? "-" : ByteCountFormatter.string(fromByteCount: sizeInKB * kb, countStyle: .binary) +} + +struct DetailedXFTPStatsView: View { + var stats: AgentXFTPServerStatsData + var statsStartedAt: Date + + var body: some View { + List { + Section("Uploaded files") { + infoRow("Size", prettySize(stats._uploadsSize)) + infoRowTwoValues("Chunks uploaded", "attempts", stats._uploads, stats._uploadAttempts) + infoRow("Upload errors", numOrDash(stats._uploadErrs)) + infoRowTwoValues("Chunks deleted", "attempts", stats._deletions, stats._deleteAttempts) + infoRow("Deletion errors", numOrDash(stats._deleteErrs)) + } + Section { + infoRow("Size", prettySize(stats._downloadsSize)) + infoRowTwoValues("Chunks downloaded", "attempts", stats._downloads, stats._downloadAttempts) + Text("Download errors") + infoRow(Text(verbatim: "AUTH"), numOrDash(stats._downloadAuthErrs)).padding(.leading, 24) + infoRow("other", numOrDash(stats._downloadErrs)).padding(.leading, 24) + } header: { + Text("Downloaded files") + } footer: { + Text("Starting from \(localTimestamp(statsStartedAt)).") + } + } + } +} + +#Preview { + ServersSummaryView() +} diff --git a/apps/ios/Shared/Views/ChatList/TagListView.swift b/apps/ios/Shared/Views/ChatList/TagListView.swift new file mode 100644 index 0000000000..2063fe15de --- /dev/null +++ b/apps/ios/Shared/Views/ChatList/TagListView.swift @@ -0,0 +1,408 @@ +// +// TagListView.swift +// SimpleX (iOS) +// +// Created by Diogo Cunha on 31/12/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat +import ElegantEmojiPicker + +struct TagEditorNavParams { + let chat: Chat? + let chatListTag: ChatTagData? + let tagId: Int64? +} + +struct TagListView: View { + var chat: Chat? = nil + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme + @EnvironmentObject var chatTagsModel: ChatTagsModel + @EnvironmentObject var m: ChatModel + @State private var editMode = EditMode.inactive + @State private var tagEditorNavParams: TagEditorNavParams? = nil + + var chatTagsIds: [Int64] { chat?.chatInfo.contact?.chatTags ?? chat?.chatInfo.groupInfo?.chatTags ?? [] } + + var body: some View { + List { + Section { + ForEach(chatTagsModel.userTags, id: \.id) { tag in + let text = tag.chatTagText + let emoji = tag.chatTagEmoji + let tagId = tag.chatTagId + let selected = chatTagsIds.contains(tagId) + + HStack { + if let emoji { + Text(emoji) + } else { + Image(systemName: "tag") + } + Text(text) + .padding(.leading, 12) + Spacer() + if chat != nil { + radioButton(selected: selected) + } + } + .contentShape(Rectangle()) + .onTapGesture { + if let c = chat { + setChatTag(tagId: selected ? nil : tagId, chat: c) { dismiss() } + } else { + tagEditorNavParams = TagEditorNavParams(chat: nil, chatListTag: ChatTagData(emoji: emoji, text: text), tagId: tagId) + } + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button { + showAlert( + NSLocalizedString("Delete list?", comment: "alert title"), + message: String.localizedStringWithFormat(NSLocalizedString("All chats will be removed from the list %@, and the list deleted.", comment: "alert message"), text), + actions: {[ + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "alert action"), + style: .default + ), + UIAlertAction( + title: NSLocalizedString("Delete", comment: "alert action"), + style: .destructive, + handler: { _ in + deleteTag(tagId) + } + ) + ]} + ) + } label: { + Label("Delete", systemImage: "trash.fill") + } + .tint(.red) + } + .swipeActions(edge: .leading, allowsFullSwipe: true) { + Button { + tagEditorNavParams = TagEditorNavParams(chat: nil, chatListTag: ChatTagData(emoji: emoji, text: text), tagId: tagId) + } label: { + Label("Edit", systemImage: "pencil") + } + .tint(theme.colors.primary) + } + .background( + // isActive required to navigate to edit view from any possible tag edited in swipe action + NavigationLink(isActive: Binding(get: { tagEditorNavParams != nil }, set: { _ in tagEditorNavParams = nil })) { + if let params = tagEditorNavParams { + TagListEditor( + chat: params.chat, + tagId: params.tagId, + emoji: params.chatListTag?.emoji, + name: params.chatListTag?.text ?? "" + ) + } + } label: { + EmptyView() + } + .opacity(0) + ) + } + .onMove(perform: moveItem) + + NavigationLink { + TagListEditor(chat: chat) + } label: { + Label("Create list", systemImage: "plus") + } + } header: { + if chat == nil { + editTagsButton() + .textCase(nil) + .frame(maxWidth: .infinity, alignment: .trailing) + } + } + } + .modifier(ThemedBackground(grouped: true)) + .environment(\.editMode, $editMode) + } + + private func editTagsButton() -> some View { + if editMode.isEditing { + Button("Done") { + editMode = .inactive + dismiss() + } + } else { + Button("Edit") { + editMode = .active + } + } + } + + private func radioButton(selected: Bool) -> some View { + Image(systemName: selected ? "checkmark.circle.fill" : "circle") + .imageScale(.large) + .foregroundStyle(selected ? Color.accentColor : Color(.tertiaryLabel)) + } + + private func moveItem(from source: IndexSet, to destination: Int) { + Task { + do { + var tags = chatTagsModel.userTags + tags.move(fromOffsets: source, toOffset: destination) + try await apiReorderChatTags(tagIds: tags.map { $0.chatTagId }) + + await MainActor.run { + chatTagsModel.userTags = tags + } + } catch let error { + showAlert( + NSLocalizedString("Error reordering lists", comment: "alert title"), + message: responseError(error) + ) + } + } + } + + private func deleteTag(_ tagId: Int64) { + Task { + try await apiDeleteChatTag(tagId: tagId) + + await MainActor.run { + chatTagsModel.userTags = chatTagsModel.userTags.filter { $0.chatTagId != tagId } + if case let .userTag(tag) = chatTagsModel.activeFilter, tagId == tag.chatTagId { + chatTagsModel.activeFilter = nil + } + m.chats.forEach { c in + if var contact = c.chatInfo.contact, contact.chatTags.contains(tagId) { + contact.chatTags = contact.chatTags.filter({ $0 != tagId }) + m.updateContact(contact) + } else if var group = c.chatInfo.groupInfo, group.chatTags.contains(tagId) { + group.chatTags = group.chatTags.filter({ $0 != tagId }) + m.updateGroup(group) + } + } + } + } + } +} + +private func setChatTag(tagId: Int64?, chat: Chat, closeSheet: @escaping () -> Void) { + Task { + do { + let tagIds: [Int64] = if let t = tagId { [t] } else {[]} + let (userTags, chatTags) = try await apiSetChatTags( + type: chat.chatInfo.chatType, + id: chat.chatInfo.apiId, + tagIds: tagIds + ) + + await MainActor.run { + let m = ChatModel.shared + let tm = ChatTagsModel.shared + tm.userTags = userTags + if chat.unreadTag, let tags = chat.chatInfo.chatTags { + tm.decTagsReadCount(tags) + } + if var contact = chat.chatInfo.contact { + contact.chatTags = chatTags + m.updateContact(contact) + } else if var group = chat.chatInfo.groupInfo { + group.chatTags = chatTags + m.updateGroup(group) + } + ChatTagsModel.shared.updateChatTagRead(chat, wasUnread: false) + closeSheet() + } + } catch let error { + showAlert( + NSLocalizedString("Error saving chat list", comment: "alert title"), + message: responseError(error) + ) + } + } +} + +struct EmojiPickerView: UIViewControllerRepresentable { + @Binding var selectedEmoji: String? + @Binding var showingPicker: Bool + @Environment(\.presentationMode) var presentationMode + + class Coordinator: NSObject, ElegantEmojiPickerDelegate, UIAdaptivePresentationControllerDelegate { + var parent: EmojiPickerView + + init(parent: EmojiPickerView) { + self.parent = parent + } + + func emojiPicker(_ picker: ElegantEmojiPicker, didSelectEmoji emoji: Emoji?) { + parent.selectedEmoji = emoji?.emoji + parent.showingPicker = false + picker.dismiss(animated: true) + } + + // Called when the picker is dismissed manually (without selection) + func presentationControllerWillDismiss(_ presentationController: UIPresentationController) { + parent.showingPicker = false + } + } + + func makeCoordinator() -> Coordinator { + return Coordinator(parent: self) + } + + func makeUIViewController(context: Context) -> UIViewController { + let config = ElegantConfiguration(showRandom: false, showReset: true, showClose: false) + let picker = ElegantEmojiPicker(delegate: context.coordinator, configuration: config) + + picker.presentationController?.delegate = context.coordinator + + let viewController = UIViewController() + DispatchQueue.main.async { + if let topVC = getTopViewController() { + topVC.present(picker, animated: true) + } + } + + return viewController + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + // No need to update the controller after creation + } +} + +struct TagListEditor: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var chatTagsModel: ChatTagsModel + @EnvironmentObject var theme: AppTheme + var chat: Chat? = nil + var tagId: Int64? = nil + var emoji: String? + var name: String = "" + @State private var newEmoji: String? + @State private var newName: String = "" + @State private var isPickerPresented = false + @State private var saving: Bool? + + var body: some View { + VStack { + List { + let isDuplicateEmojiOrName = chatTagsModel.userTags.contains { tag in + tag.chatTagId != tagId && + ((newEmoji != nil && tag.chatTagEmoji == newEmoji) || tag.chatTagText == trimmedName) + } + + Section { + HStack { + Button { + isPickerPresented = true + } label: { + if let newEmoji { + Text(newEmoji) + } else { + Image(systemName: "face.smiling") + .foregroundColor(.secondary) + } + } + TextField("List name...", text: $newName) + } + + Button { + saving = true + if let tId = tagId { + updateChatTag(tagId: tId, chatTagData: ChatTagData(emoji: newEmoji, text: trimmedName)) + } else { + createChatTag() + } + } label: { + Text( + chat != nil + ? "Add to list" + : "Save list" + ) + } + .disabled(saving != nil || (trimmedName == name && newEmoji == emoji) || trimmedName.isEmpty || isDuplicateEmojiOrName) + } footer: { + if isDuplicateEmojiOrName && saving != false { // if not saved already, to prevent flickering + HStack { + Image(systemName: "exclamationmark.circle") + .foregroundColor(.red) + Text("List name and emoji should be different for all lists.") + .foregroundColor(theme.colors.secondary) + } + } + } + } + + if isPickerPresented { + EmojiPickerView(selectedEmoji: $newEmoji, showingPicker: $isPickerPresented) + } + } + .modifier(ThemedBackground(grouped: true)) + .onAppear { + newEmoji = emoji + newName = name + } + } + + var trimmedName: String { + newName.trimmingCharacters(in: .whitespaces) + } + + private func createChatTag() { + Task { + do { + let text = trimmedName + let userTags = try await apiCreateChatTag( + tag: ChatTagData(emoji: newEmoji , text: text) + ) + await MainActor.run { + saving = false + chatTagsModel.userTags = userTags + } + if let chat, let tag = userTags.first(where: { $0.chatTagText == text && $0.chatTagEmoji == newEmoji}) { + setChatTag(tagId: tag.chatTagId, chat: chat) { dismiss() } + } else { + await MainActor.run { dismiss() } + } + } catch let error { + await MainActor.run { + saving = nil + showAlert( + NSLocalizedString("Error creating list", comment: "alert title"), + message: responseError(error) + ) + } + } + } + } + + private func updateChatTag(tagId: Int64, chatTagData: ChatTagData) { + Task { + do { + try await apiUpdateChatTag(tagId: tagId, tag: chatTagData) + await MainActor.run { + saving = false + for i in 0..<chatTagsModel.userTags.count { + if chatTagsModel.userTags[i].chatTagId == tagId { + chatTagsModel.userTags[i] = ChatTag( + chatTagId: tagId, + chatTagText: chatTagData.text, + chatTagEmoji: chatTagData.emoji + ) + } + } + dismiss() + } + } catch let error { + await MainActor.run { + saving = nil + showAlert( + NSLocalizedString("Error creating list", comment: "alert title"), + message: responseError(error) + ) + } + } + } + } +} diff --git a/apps/ios/Shared/Views/ChatList/UserPicker.swift b/apps/ios/Shared/Views/ChatList/UserPicker.swift index 741af6f08f..dbe10ad997 100644 --- a/apps/ios/Shared/Views/ChatList/UserPicker.swift +++ b/apps/ios/Shared/Views/ChatList/UserPicker.swift @@ -6,181 +6,283 @@ import SwiftUI import SimpleXChat -private let fillColorDark = Color(uiColor: UIColor(red: 0.11, green: 0.11, blue: 0.11, alpha: 255)) -private let fillColorLight = Color(uiColor: UIColor(red: 0.99, green: 0.99, blue: 0.99, alpha: 255)) - struct UserPicker: View { @EnvironmentObject var m: ChatModel - @Environment(\.colorScheme) var colorScheme - @Binding var showSettings: Bool - @Binding var showConnectDesktop: Bool - @Binding var userPickerVisible: Bool - @State var scrollViewContentSize: CGSize = .zero - @State var disableScrolling: Bool = true - private let menuButtonHeight: CGFloat = 68 - @State var chatViewNameWidth: CGFloat = 0 + @EnvironmentObject var theme: AppTheme + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + @Environment(\.scenePhase) private var scenePhase: ScenePhase + @Environment(\.colorScheme) private var colorScheme: ColorScheme + @Binding var userPickerShown: Bool + @Binding var activeSheet: UserPickerSheet? + @State private var currentUser: Int64? + @State private var switchingProfile = false + @State private var frameWidth: CGFloat = 0 + @State private var resetScroll = ResetScrollAction() - var fillColor: Color { - colorScheme == .dark ? fillColorDark : fillColorLight - } + // Inset grouped list dimensions + private let imageSize: CGFloat = 44 + private let rowPadding: CGFloat = 16 + private let rowVerticalPadding: CGFloat = 11 + private let sectionSpacing: CGFloat = 35 + private var sectionHorizontalPadding: CGFloat { frameWidth > 375 ? 20 : 16 } + private let sectionShape = RoundedRectangle(cornerRadius: 10, style: .continuous) var body: some View { - VStack { - Spacer().frame(height: 1) + let otherUsers: [UserInfo] = m.users + .filter { u in !u.user.hidden && u.user.userId != m.currentUser?.userId } + .sorted(using: KeyPathComparator<UserInfo>(\.user.activeOrder, order: .reverse)) + let sectionWidth = max(frameWidth - sectionHorizontalPadding * 2, 0) + let currentUserWidth = max(frameWidth - sectionHorizontalPadding - rowPadding * 2 - 14 - imageSize, 0) + let stopped = m.chatRunning != true + VStack(spacing: sectionSpacing) { + if let user = m.currentUser { + StickyScrollView(resetScroll: $resetScroll) { + HStack(spacing: rowPadding) { + HStack { + ProfileImage(imageStr: user.image, size: imageSize, color: Color(uiColor: .tertiarySystemGroupedBackground)) + .padding(.trailing, 6) + profileName(user).lineLimit(1) + } + .padding(rowPadding) + .frame(width: otherUsers.isEmpty ? sectionWidth : currentUserWidth, alignment: .leading) + .modifier(ListRow { activeSheet = .currentProfile }) + .clipShape(sectionShape) + .disabled(stopped) + .opacity(stopped ? 0.4 : 1) + ForEach(otherUsers) { u in + userView(u, size: imageSize) + .frame(maxWidth: sectionWidth * 0.618) + .fixedSize() + .disabled(stopped) + .opacity(stopped ? 0.4 : 1) + } + } + .padding(.horizontal, sectionHorizontalPadding) + } + .frame(height: 2 * rowPadding + imageSize) + .padding(.top, sectionSpacing) + .overlay(DetermineWidth()) + .onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 } + } VStack(spacing: 0) { - ScrollView { - ScrollViewReader { sp in - let users = m.users - .filter({ u in u.user.activeUser || !u.user.hidden }) - .sorted { u, _ in u.user.activeUser } - VStack(spacing: 0) { - ForEach(users) { u in - userView(u) - Divider() - if u.user.activeUser { Divider() } + openSheetOnTap("qrcode", title: m.userAddress == nil ? "Create SimpleX address" : "Your SimpleX address", sheet: .address, disabled: stopped) + openSheetOnTap("switch.2", title: "Chat preferences", sheet: .chatPreferences, disabled: stopped) + openSheetOnTap("person.crop.rectangle.stack", title: "Your chat profiles", sheet: .chatProfiles, disabled: stopped) + openSheetOnTap("desktopcomputer", title: "Use from desktop", sheet: .useFromDesktop, disabled: stopped) + ZStack(alignment: .trailing) { + openSheetOnTap("gearshape", title: "Settings", sheet: .settings, showDivider: false) + Image(systemName: colorScheme == .light ? "sun.max" : "moon.fill") + .resizable() + .scaledToFit() + .symbolRenderingMode(.monochrome) + .foregroundColor(theme.colors.secondary) + .frame(maxWidth: 20, maxHeight: .infinity) + .padding(.horizontal, rowPadding) + .background(Color(.systemBackground).opacity(0.01)) + .onTapGesture { + if (colorScheme == .light) { + ThemeManager.applyTheme(systemDarkThemeDefault.get()) + } else { + ThemeManager.applyTheme(DefaultTheme.LIGHT.themeName) } } - .overlay { - GeometryReader { geo -> Color in - DispatchQueue.main.async { - scrollViewContentSize = geo.size - let scenes = UIApplication.shared.connectedScenes - if let windowScene = scenes.first as? UIWindowScene { - let layoutFrame = windowScene.windows[0].safeAreaLayoutGuide.layoutFrame - disableScrolling = scrollViewContentSize.height + menuButtonHeight + 10 < layoutFrame.height - } - } - return Color.clear - } + .onLongPressGesture { + ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME) } - .onChange(of: userPickerVisible) { visible in - if visible, let u = users.first { - sp.scrollTo(u.id) - } - } - } - } - .simultaneousGesture(DragGesture(minimumDistance: disableScrolling ? 0 : 10000000)) - .frame(maxHeight: scrollViewContentSize.height) - - menuButton("Use from desktop", icon: "desktopcomputer") { - showConnectDesktop = true - withAnimation { - userPickerVisible.toggle() - } - } - Divider() - menuButton("Settings", icon: "gearshape") { - showSettings = true - withAnimation { - userPickerVisible.toggle() - } } } + .clipShape(sectionShape) + .padding(.horizontal, sectionHorizontalPadding) + .padding(.bottom, sectionSpacing) } - .clipShape(RoundedRectangle(cornerRadius: 16)) - .background( - Rectangle() - .fill(fillColor) - .cornerRadius(16) - .shadow(color: .black.opacity(0.12), radius: 24, x: 0, y: 0) - ) - .onPreferenceChange(DetermineWidth.Key.self) { chatViewNameWidth = $0 } - .frame(maxWidth: chatViewNameWidth > 0 ? min(300, chatViewNameWidth + 130) : 300) - .padding(8) - .opacity(userPickerVisible ? 1.0 : 0.0) .onAppear { - do { - m.users = try listUsers() - } catch let error { - logger.error("Error loading users \(responseError(error))") - } - } - } - - private func userView(_ u: UserInfo) -> some View { - let user = u.user - return Button(action: { - if user.activeUser { - showSettings = true - withAnimation { - userPickerVisible.toggle() - } - } else { + // This check prevents the call of listUsers after the app is suspended, and the database is closed. + if case .active = scenePhase { + currentUser = m.currentUser?.userId Task { do { - try await changeActiveUserAsync_(user.userId, viewPwd: nil) - await MainActor.run { userPickerVisible = false } - } catch { + let users = try await listUsersAsync() await MainActor.run { - AlertManager.shared.showAlertMsg( - title: "Error switching profile!", - message: "Error: \(responseError(error))" - ) + m.users = users + currentUser = m.currentUser?.userId } + } catch { + logger.error("Error loading users \(responseError(error))") } } } - }, label: { - HStack(spacing: 0) { - ProfileImage(imageStr: user.image, color: Color(uiColor: .tertiarySystemFill)) - .frame(width: 44, height: 44) - .padding(.trailing, 12) - Text(user.chatViewName) - .fontWeight(user.activeUser ? .medium : .regular) - .foregroundColor(.primary) - .overlay(DetermineWidth()) - Spacer() - if user.activeUser { - Image(systemName: "checkmark") - } else if u.unreadCount > 0 { - unreadCounter(u.unreadCount, color: user.showNtfs ? .accentColor : .secondary) - } else if !user.showNtfs { - Image(systemName: "speaker.slash") + } + .onChange(of: userPickerShown) { + if !$0 { resetScroll() } + } + .modifier(ThemedBackground(grouped: true)) + .disabled(switchingProfile) + } + + private func userView(_ u: UserInfo, size: CGFloat) -> some View { + HStack { + ZStack(alignment: .topTrailing) { + ProfileImage(imageStr: u.user.image, size: size, color: Color(uiColor: .tertiarySystemGroupedBackground)) + if (u.unreadCount > 0) { + UnreadBadge(userInfo: u).offset(x: 4, y: -4) } } - .padding(.trailing) - .padding([.leading, .vertical], 12) - }) - .buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill))) - } - - private func menuButton(_ title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View { - Button(action: action) { - HStack(spacing: 0) { - Text(title) - .overlay(DetermineWidth()) - Spacer() - Image(systemName: icon) - .symbolRenderingMode(.monochrome) - .foregroundColor(.secondary) - } - .padding(.horizontal) - .padding(.vertical, 22) - .frame(height: menuButtonHeight) + .padding(.trailing, 6) + Text(u.user.displayName).font(.title2).lineLimit(1) + } + .padding(rowPadding) + .modifier(ListRow { + switchingProfile = true + Task { + do { + try await changeActiveUserAsync_(u.user.userId, viewPwd: nil) + await MainActor.run { + switchingProfile = false + userPickerShown = false + } + } catch { + await MainActor.run { + switchingProfile = false + showAlert( + NSLocalizedString("Error switching profile!", comment: "alertTitle"), + message: String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "alert message"), responseError(error)) + ) + } + } + } + }) + .clipShape(sectionShape) + } + + private func openSheetOnTap(_ icon: String, title: LocalizedStringKey, sheet: UserPickerSheet, showDivider: Bool = true, disabled: Bool = false) -> some View { + ZStack(alignment: .bottom) { + settingsRow(icon, color: theme.colors.secondary) { + Text(title).foregroundColor(.primary).opacity(disabled ? 0.4 : 1) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, rowPadding) + .padding(.vertical, rowVerticalPadding) + .modifier(ListRow { activeSheet = sheet }) + .disabled(disabled) + if showDivider { + Divider().padding(.leading, 52) + } } - .buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill))) } } -private func unreadCounter(_ unread: Int, color: Color) -> some View { - unreadCountText(unread) - .font(.caption) - .foregroundColor(.white) - .padding(.horizontal, 4) - .frame(minWidth: 18, minHeight: 18) - .background(color) - .cornerRadius(10) +struct UnreadBadge: View { + var userInfo: UserInfo + @EnvironmentObject var theme: AppTheme + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + + var body: some View { + let size = dynamicSize(userFont).chatInfoSize + unreadCountText(userInfo.unreadCount) + .font(userFont <= .xxxLarge ? .caption : .caption2) + .foregroundColor(.white) + .padding(.horizontal, dynamicSize(userFont).unreadPadding) + .frame(minWidth: size, minHeight: size) + .background(userInfo.user.showNtfs ? theme.colors.primary : theme.colors.secondary) + .cornerRadius(dynamicSize(userFont).unreadCorner) + } } +struct ListRow: ViewModifier { + @Environment(\.colorScheme) private var colorScheme: ColorScheme + @State private var touchDown = false + let action: () -> Void + + func body(content: Content) -> some View { + ZStack { + elevatedSecondarySystemGroupedBackground + Color(.systemGray4).opacity(touchDown ? 1 : 0) + content + TouchOverlay(touchDown: $touchDown, action: action) + } + } + + var elevatedSecondarySystemGroupedBackground: Color { + switch colorScheme { + case .dark: Color(0xFF2C2C2E) + default: Color(0xFFFFFFFF) + } + } + + struct TouchOverlay: UIViewRepresentable { + @Binding var touchDown: Bool + let action: () -> Void + + func makeUIView(context: Context) -> TouchView { + let touchView = TouchView() + let gesture = UILongPressGestureRecognizer( + target: touchView, + action: #selector(touchView.longPress(gesture:)) + ) + gesture.delegate = touchView + gesture.minimumPressDuration = 0 + touchView.addGestureRecognizer(gesture) + return touchView + } + + func updateUIView(_ touchView: TouchView, context: Context) { + touchView.representer = self + } + + class TouchView: UIView, UIGestureRecognizerDelegate { + var representer: TouchOverlay? + private var startLocation: CGPoint? + private var task: Task<Void, Never>? + + @objc + func longPress(gesture: UILongPressGestureRecognizer) { + switch gesture.state { + case .began: + startLocation = gesture.location(in: nil) + task = Task { + do { + try await Task.sleep(nanoseconds: 200_000000) + await MainActor.run { representer?.touchDown = true } + } catch { } + } + case .ended: + if hitTest(gesture.location(in: self), with: nil) == self { + representer?.action() + } + task?.cancel() + representer?.touchDown = false + case .changed: + if let startLocation { + let location = gesture.location(in: nil) + let dx = location.x - startLocation.x + let dy = location.y - startLocation.y + if sqrt(pow(dx, 2) + pow(dy, 2)) > 10 { gesture.state = .failed } + } + case .cancelled, .failed: + task?.cancel() + representer?.touchDown = false + default: break + } + } + + func gestureRecognizer( + _: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith: UIGestureRecognizer + ) -> Bool { true } + } + } +} + + struct UserPicker_Previews: PreviewProvider { static var previews: some View { + @State var activeSheet: UserPickerSheet? + let m = ChatModel() m.users = [UserInfo.sampleData, UserInfo.sampleData] return UserPicker( - showSettings: Binding.constant(false), - showConnectDesktop: Binding.constant(false), - userPickerVisible: Binding.constant(true) + userPickerShown: .constant(true), + activeSheet: $activeSheet ) .environmentObject(m) } diff --git a/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift b/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift new file mode 100644 index 0000000000..456c46d318 --- /dev/null +++ b/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift @@ -0,0 +1,266 @@ +// +// ContactListNavLink.swift +// SimpleX (iOS) +// +// Created by Diogo Cunha on 01/08/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ContactListNavLink: View { + @EnvironmentObject var theme: AppTheme + @ObservedObject var chat: Chat + var showDeletedChatIcon: Bool + @State private var alert: SomeAlert? = nil + @State private var actionSheet: SomeActionSheet? = nil + @State private var sheet: SomeSheet<AnyView>? = nil + @State private var showConnectContactViaAddressDialog = false + @State private var showContactRequestDialog = false + + var body: some View { + let contactType = chatContactType(chat) + + Group { + switch (chat.chatInfo) { + case let .direct(contact): + switch contactType { + case .recent: + recentContactNavLink(contact) + case .chatDeleted: + deletedChatNavLink(contact) + case .card: + contactCardNavLink(contact) + default: + EmptyView() + } + case let .contactRequest(contactRequest): + contactRequestNavLink(contactRequest) + default: + EmptyView() + } + } + .alert(item: $alert) { $0.alert } + .actionSheet(item: $actionSheet) { $0.actionSheet } + .sheet(item: $sheet) { + if #available(iOS 16.0, *) { + $0.content + .presentationDetents([.fraction(0.4)]) + } else { + $0.content + } + } + } + + func recentContactNavLink(_ contact: Contact) -> some View { + Button { + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(contact.id) + } + } label: { + contactPreview(contact, titleColor: theme.colors.onBackground) + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button { + deleteContactDialog( + chat, + contact, + dismissToChatList: false, + showAlert: { alert = $0 }, + showActionSheet: { actionSheet = $0 }, + showSheetContent: { sheet = $0 } + ) + } label: { + Label("Delete", systemImage: "trash") + } + .tint(.red) + } + } + + func deletedChatNavLink(_ contact: Contact) -> some View { + Button { + Task { + await MainActor.run { + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(contact.id) + } + } + } + } label: { + contactPreview(contact, titleColor: theme.colors.onBackground) + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button { + deleteContactDialog( + chat, + contact, + dismissToChatList: false, + showAlert: { alert = $0 }, + showActionSheet: { actionSheet = $0 }, + showSheetContent: { sheet = $0 } + ) + } label: { + Label("Delete", systemImage: "trash") + } + .tint(.red) + } + } + + func contactPreview(_ contact: Contact, titleColor: Color) -> some View { + HStack{ + ProfileImage(imageStr: contact.image, size: 30) + + previewTitle(contact, titleColor: titleColor) + + Spacer() + + HStack { + if showDeletedChatIcon && contact.chatDeleted { + Image(systemName: "archivebox") + .resizable() + .scaledToFit() + .frame(width: 18, height: 18) + .foregroundColor(.secondary.opacity(0.65)) + } else if chat.chatInfo.chatSettings?.favorite ?? false { + Image(systemName: "star.fill") + .resizable() + .scaledToFill() + .frame(width: 18, height: 18) + .foregroundColor(.secondary.opacity(0.65)) + } + if contact.contactConnIncognito { + Image(systemName: "theatermasks") + .resizable() + .scaledToFit() + .frame(width: 22, height: 22) + .foregroundColor(.secondary) + } + } + } + } + + private func previewTitle(_ contact: Contact, titleColor: Color) -> some View { + let t = Text(chat.chatInfo.chatViewName).foregroundColor(titleColor) + return ( + contact.verified == true + ? verifiedIcon + t + : t + ) + .lineLimit(1) + } + + private var verifiedIcon: Text { + (Text(Image(systemName: "checkmark.shield")) + textSpace) + .foregroundColor(.secondary) + .baselineOffset(1) + .kerning(-2) + } + + func contactCardNavLink(_ contact: Contact) -> some View { + Button { + showConnectContactViaAddressDialog = true + } label: { + contactCardPreview(contact) + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button { + deleteContactDialog( + chat, + contact, + dismissToChatList: false, + showAlert: { alert = $0 }, + showActionSheet: { actionSheet = $0 }, + showSheetContent: { sheet = $0 } + ) + } label: { + Label("Delete", systemImage: "trash") + } + .tint(.red) + } + .confirmationDialog("Connect with \(contact.chatViewName)", isPresented: $showConnectContactViaAddressDialog, titleVisibility: .visible) { + Button("Use current profile") { connectContactViaAddress_(contact, false) } + Button("Use new incognito profile") { connectContactViaAddress_(contact, true) } + } + } + + private func connectContactViaAddress_(_ contact: Contact, _ incognito: Bool) { + Task { + let ok = await connectContactViaAddress(contact.contactId, incognito, showAlert: { alert = SomeAlert(alert: $0, id: "ContactListNavLink connectContactViaAddress") }) + if ok { + ItemsModel.shared.loadOpenChat(contact.id) { + dismissAllSheets(animated: true) { + AlertManager.shared.showAlert(connReqSentAlert(.contact)) + } + } + } + } + } + + func contactCardPreview(_ contact: Contact) -> some View { + HStack{ + ProfileImage(imageStr: contact.image, size: 30) + + Text(chat.chatInfo.chatViewName) + .foregroundColor(.accentColor) + .lineLimit(1) + + Spacer() + + Image(systemName: "envelope") + .resizable() + .scaledToFill() + .frame(width: 14, height: 14) + .foregroundColor(.accentColor) + } + } + + func contactRequestNavLink(_ contactRequest: UserContactRequest) -> some View { + Button { + showContactRequestDialog = true + } label: { + contactRequestPreview(contactRequest) + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button { + Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } + } label: { Label("Accept", systemImage: "checkmark") } + .tint(theme.colors.primary) + Button { + Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) } + } label: { + Label("Accept incognito", systemImage: "theatermasks") + } + .tint(.indigo) + Button { + alert = SomeAlert(alert: rejectContactRequestAlert(contactRequest), id: "rejectContactRequestAlert") + } label: { + Label("Reject", systemImage: "multiply") + } + .tint(.red) + } + .confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) { + Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } } + Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) } } + Button("Reject (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest) } } + } + } + + func contactRequestPreview(_ contactRequest: UserContactRequest) -> some View { + HStack{ + ProfileImage(imageStr: contactRequest.image, size: 30) + + Text(chat.chatInfo.chatViewName) + .foregroundColor(.accentColor) + .lineLimit(1) + + Spacer() + + Image(systemName: "checkmark") + .resizable() + .scaledToFill() + .frame(width: 14, height: 14) + .foregroundColor(.accentColor) + } + } +} diff --git a/apps/ios/Shared/Views/Database/ChatArchiveView.swift b/apps/ios/Shared/Views/Database/ChatArchiveView.swift deleted file mode 100644 index 65913343d5..0000000000 --- a/apps/ios/Shared/Views/Database/ChatArchiveView.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// ChatArchiveView.swift -// SimpleXChat -// -// Created by Evgeny on 23/06/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import SwiftUI -import SimpleXChat - -struct ChatArchiveView: View { - var archiveName: String - @AppStorage(DEFAULT_CHAT_ARCHIVE_NAME) private var chatArchiveName: String? - @AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0 - @State private var showDeleteAlert = false - - var body: some View { - let fileUrl = getDocumentsDirectory().appendingPathComponent(archiveName) - let fileTs = chatArchiveTimeDefault.get() - List { - Section { - settingsRow("square.and.arrow.up") { - Button { - showShareSheet(items: [fileUrl]) - } label: { - Text("Save archive") - } - } - settingsRow("trash") { - Button { - showDeleteAlert = true - } label: { - Text("Delete archive").foregroundColor(.red) - } - } - } header: { - Text("Chat archive") - } footer: { - Text("Created on \(fileTs)") - } - } - .alert(isPresented: $showDeleteAlert) { - Alert( - title: Text("Delete chat archive?"), - primaryButton: .destructive(Text("Delete")) { - do { - try FileManager.default.removeItem(atPath: fileUrl.path) - chatArchiveName = nil - chatArchiveTime = 0 - } catch let error { - logger.error("removeItem error \(String(describing: error))") - } - }, - secondaryButton: .cancel() - ) - } - } -} - -struct ChatArchiveView_Previews: PreviewProvider { - static var previews: some View { - ChatArchiveView(archiveName: "") - } -} diff --git a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift index 90cd17fbb3..441a164f8a 100644 --- a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift @@ -35,7 +35,9 @@ enum DatabaseEncryptionAlert: Identifiable { struct DatabaseEncryptionView: View { @EnvironmentObject private var m: ChatModel + @EnvironmentObject private var theme: AppTheme @Binding var useKeychain: Bool + var migration: Bool @State private var alert: DatabaseEncryptionAlert? = nil @State private var progressIndicator = false @State private var useKeychainToggle = storeDBPassphraseGroupDefault.get() @@ -46,9 +48,16 @@ struct DatabaseEncryptionView: View { @State private var confirmNewKey = "" @State private var currentKeyShown = false + let stopChatRunBlockStartChat: (Binding<Bool>, @escaping () async throws -> Bool) -> Void + var body: some View { ZStack { - databaseEncryptionView() + List { + if migration { + chatStoppedView() + } + databaseEncryptionView() + } if progressIndicator { ProgressView().scaleEffect(2) } @@ -56,107 +65,132 @@ struct DatabaseEncryptionView: View { } private func databaseEncryptionView() -> some View { - List { - Section { - settingsRow(storedKey ? "key.fill" : "key", color: storedKey ? .green : .secondary) { - Toggle("Save passphrase in Keychain", isOn: $useKeychainToggle) + Section { + settingsRow(storedKey ? "key.fill" : "key", color: storedKey ? .green : theme.colors.secondary) { + Toggle("Save passphrase in Keychain", isOn: $useKeychainToggle) .onChange(of: useKeychainToggle) { _ in if useKeychainToggle { setUseKeychain(true) - } else if storedKey { + } else if storedKey && !migration { + // Don't show in migration process since it will remove the key after successfull encryption alert = .keychainRemoveKey } else { setUseKeychain(false) } } - .disabled(initialRandomDBPassphrase) - } + .disabled(initialRandomDBPassphrase && !migration) + } - if !initialRandomDBPassphrase && m.chatDbEncrypted == true { - PassphraseField(key: $currentKey, placeholder: "Current passphrase…", valid: validKey(currentKey)) - } + if !initialRandomDBPassphrase && m.chatDbEncrypted == true { + PassphraseField(key: $currentKey, placeholder: "Current passphrase…", valid: validKey(currentKey)) + } - PassphraseField(key: $newKey, placeholder: "New passphrase…", valid: validKey(newKey), showStrength: true) - PassphraseField(key: $confirmNewKey, placeholder: "Confirm new passphrase…", valid: confirmNewKey == "" || newKey == confirmNewKey) + PassphraseField(key: $newKey, placeholder: "New passphrase…", valid: validKey(newKey), showStrength: true) + PassphraseField(key: $confirmNewKey, placeholder: "Confirm new passphrase…", valid: confirmNewKey == "" || newKey == confirmNewKey) - settingsRow("lock.rotation") { - Button("Update database passphrase") { - alert = currentKey == "" - ? (useKeychain ? .encryptDatabaseSaved : .encryptDatabase) - : (useKeychain ? .changeDatabaseKeySaved : .changeDatabaseKey) - } + settingsRow("lock.rotation", color: theme.colors.secondary) { + Button(migration ? "Set passphrase" : "Update database passphrase") { + alert = currentKey == "" + ? (useKeychain ? .encryptDatabaseSaved : .encryptDatabase) + : (useKeychain ? .changeDatabaseKeySaved : .changeDatabaseKey) } - .disabled( - (m.chatDbEncrypted == true && currentKey == "") || - currentKey == newKey || - newKey != confirmNewKey || - newKey == "" || - !validKey(currentKey) || - !validKey(newKey) - ) - } header: { - Text("") - } footer: { - VStack(alignment: .leading, spacing: 16) { - if m.chatDbEncrypted == false { - Text("Your chat database is not encrypted - set passphrase to encrypt it.") - } else if useKeychain { - if storedKey { - Text("iOS Keychain is used to securely store passphrase - it allows receiving push notifications.") - if initialRandomDBPassphrase { - Text("Database is encrypted using a random passphrase, you can change it.") - } else { - Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.") - } + } + .disabled( + (m.chatDbEncrypted == true && currentKey == "") || + currentKey == newKey || + newKey != confirmNewKey || + newKey == "" || + !validKey(currentKey) || + !validKey(newKey) + ) + } header: { + Text(migration ? "Database passphrase" : "") + .foregroundColor(theme.colors.secondary) + } footer: { + VStack(alignment: .leading, spacing: 16) { + if m.chatDbEncrypted == false { + Text("Your chat database is not encrypted - set passphrase to encrypt it.") + } else if useKeychain { + if storedKey { + Text("iOS Keychain is used to securely store passphrase - it allows receiving push notifications.") + if initialRandomDBPassphrase && !migration { + Text("Database is encrypted using a random passphrase, you can change it.") } else { - Text("iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.") + Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.") } } else { - Text("You have to enter passphrase every time the app starts - it is not stored on the device.") - Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.") - if m.notificationMode == .instant && m.notificationPreview != .hidden { - Text("**Warning**: Instant push notifications require passphrase saved in Keychain.") - } + Text("iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.") + } + } else { + Text("You have to enter passphrase every time the app starts - it is not stored on the device.") + Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.") + if m.notificationMode == .instant && m.notificationPreview != .hidden && !migration { + Text("**Warning**: Instant push notifications require passphrase saved in Keychain.") } } - .padding(.top, 1) - .font(.callout) } + .foregroundColor(theme.colors.secondary) + .padding(.top, 1) + .font(.callout) } .onAppear { if initialRandomDBPassphrase { currentKey = kcDatabasePassword.get() ?? "" } } - .disabled(m.chatRunning != false) + .disabled(progressIndicator) .alert(item: $alert) { item in databaseEncryptionAlert(item) } } - private func encryptDatabase() { - progressIndicator = true - Task { - do { - encryptionStartedDefault.set(true) - encryptionStartedAtDefault.set(Date.now) - try await apiStorageEncryption(currentKey: currentKey, newKey: newKey) - encryptionStartedDefault.set(false) - initialRandomDBPassphraseGroupDefault.set(false) - if useKeychain { - if kcDatabasePassword.set(newKey) { - await resetFormAfterEncryption(true) - await operationEnded(.databaseEncrypted) - } else { - await resetFormAfterEncryption() - await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain")) - } + private func encryptDatabaseAsync() async -> Bool { + await MainActor.run { + progressIndicator = true + } + do { + encryptionStartedDefault.set(true) + encryptionStartedAtDefault.set(Date.now) + if !m.chatDbChanged { + try apiSaveAppSettings(settings: AppSettings.current.prepareForExport()) + } + try await apiStorageEncryption(currentKey: currentKey, newKey: newKey) + encryptionStartedDefault.set(false) + initialRandomDBPassphraseGroupDefault.set(false) + if migration { + storeDBPassphraseGroupDefault.set(useKeychain) + } + if useKeychain { + if kcDatabasePassword.set(newKey) { + await resetFormAfterEncryption(true) + await operationEnded(.databaseEncrypted) } else { await resetFormAfterEncryption() - await operationEnded(.databaseEncrypted) + await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain")) } - } catch let error { - if case .chatCmdError(_, .errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse { - await operationEnded(.currentPassphraseError) - } else { - await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))")) + } else { + if migration { + removePassphraseFromKeyChain() } + await resetFormAfterEncryption() + await operationEnded(.databaseEncrypted) + } + return true + } catch let error { + if case .errorDatabase(.errorExport(.errorNotADatabase)) = error as? ChatError { + await operationEnded(.currentPassphraseError) + } else { + await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))")) + } + return false + } + } + + private func encryptDatabase() { + // it will try to stop and start the chat in case of: non-migration && successful encryption. In migration the chat will remain stopped + if migration { + Task { + await encryptDatabaseAsync() + } + } else { + stopChatRunBlockStartChat($progressIndicator) { + return await encryptDatabaseAsync() } } } @@ -174,7 +208,10 @@ struct DatabaseEncryptionView: View { private func setUseKeychain(_ value: Bool) { useKeychain = value - storeDBPassphraseGroupDefault.set(value) + // Postpone it when migrating to the end of encryption process + if !migration { + storeDBPassphraseGroupDefault.set(value) + } } private func databaseEncryptionAlert(_ alertItem: DatabaseEncryptionAlert) -> Alert { @@ -184,13 +221,7 @@ struct DatabaseEncryptionView: View { title: Text("Remove passphrase from keychain?"), message: Text("Instant push notifications will be hidden!\n") + storeSecurelyDanger(), primaryButton: .destructive(Text("Remove")) { - if kcDatabasePassword.remove() { - logger.debug("passphrase removed from keychain") - setUseKeychain(false) - storedKey = false - } else { - alert = .error(title: "Keychain error", error: "Failed to remove passphrase") - } + removePassphraseFromKeyChain() }, secondaryButton: .cancel() { withAnimation { useKeychainToggle = true } @@ -236,6 +267,16 @@ struct DatabaseEncryptionView: View { } } + private func removePassphraseFromKeyChain() { + if kcDatabasePassword.remove() { + logger.debug("passphrase removed from keychain") + setUseKeychain(false) + storedKey = false + } else { + alert = .error(title: "Keychain error", error: "Failed to remove passphrase") + } + } + private func storeSecurelySaved() -> Text { Text("Please store passphrase securely, you will NOT be able to change it if you lose it.") } @@ -256,6 +297,7 @@ struct DatabaseEncryptionView: View { struct PassphraseField: View { + @EnvironmentObject var theme: AppTheme @Binding var key: String var placeholder: LocalizedStringKey var valid: Bool @@ -266,7 +308,7 @@ struct PassphraseField: View { var body: some View { ZStack(alignment: .leading) { let iconColor = valid - ? (showStrength && key != "" ? PassphraseStrength(passphrase: key).color : .secondary) + ? (showStrength && key != "" ? PassphraseStrength(passphrase: key).color : theme.colors.secondary) : .red Image(systemName: valid ? (showKey ? "eye.slash" : "eye") : "exclamationmark.circle") .resizable() @@ -346,6 +388,6 @@ func validKey(_ s: String) -> Bool { struct DatabaseEncryptionView_Previews: PreviewProvider { static var previews: some View { - DatabaseEncryptionView(useKeychain: Binding.constant(true)) + DatabaseEncryptionView(useKeychain: Binding.constant(true), migration: false, stopChatRunBlockStartChat: { _, _ in true }) } } diff --git a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift index 04e377f3a5..02a1b87826 100644 --- a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct DatabaseErrorView: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @State var status: DBMigrationResult @State private var dbKey = "" @State private var storedDBKey = kcDatabasePassword.get() @@ -27,24 +28,40 @@ struct DatabaseErrorView: View { } } - @ViewBuilder private func databaseErrorView() -> some View { - VStack(alignment: .leading, spacing: 16) { + private func databaseErrorView() -> some View { + VStack(alignment: .center, spacing: 20) { switch status { case let .errorNotADatabase(dbFile): if useKeychain && storedDBKey != nil && storedDBKey != "" { titleText("Wrong database passphrase") Text("Database passphrase is different from saved in the keychain.") + .font(.callout) + .foregroundColor(theme.colors.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 25) + databaseKeyField(onSubmit: saveAndRunChat) - saveAndOpenButton() - fileNameText(dbFile) + Spacer() + VStack(spacing: 10) { + saveAndOpenButton() + fileNameText(dbFile) + } } else { titleText("Encrypted database") Text("Database passphrase is required to open chat.") + .font(.callout) + .foregroundColor(theme.colors.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 25) + .padding(.bottom, 5) + if useKeychain { databaseKeyField(onSubmit: saveAndRunChat) + Spacer() saveAndOpenButton() } else { databaseKeyField(onSubmit: { runChat() }) + Spacer() openChatButton() } } @@ -52,82 +69,105 @@ struct DatabaseErrorView: View { switch migrationError { case let .upgrade(upMigrations): titleText("Database upgrade") - Button("Upgrade and open chat") { runChat(confirmMigrations: .yesUp) } - fileNameText(dbFile) migrationsText(upMigrations.map(\.upName)) + Spacer() + VStack(spacing: 10) { + Button("Upgrade and open chat") { + runChat(confirmMigrations: .yesUp) + }.buttonStyle(OnboardingButtonStyle(isDisabled: false)) + fileNameText(dbFile) + } case let .downgrade(downMigrations): titleText("Database downgrade") - Text("Warning: you may lose some data!").bold() - Button("Downgrade and open chat") { runChat(confirmMigrations: .yesUpDown) } - fileNameText(dbFile) + Text("Warning: you may lose some data!") + .bold() + .padding(.horizontal, 25) + .multilineTextAlignment(.center) + migrationsText(downMigrations) + Spacer() + VStack(spacing: 10) { + Button("Downgrade and open chat") { + runChat(confirmMigrations: .yesUpDown) + }.buttonStyle(OnboardingButtonStyle(isDisabled: false)) + fileNameText(dbFile) + } case let .migrationError(mtrError): titleText("Incompatible database version") - fileNameText(dbFile) - Text("Error: ") + Text(mtrErrorDescription(mtrError)) + fileNameText(dbFile, font: .callout) + errorView(Text(mtrErrorDescription(mtrError))) } case let .errorSQL(dbFile, migrationSQLError): titleText("Database error") - fileNameText(dbFile) - Text("Error: \(migrationSQLError)") + fileNameText(dbFile, font: .callout) + errorView(Text("Error: \(migrationSQLError)")) case .errorKeychain: titleText("Keychain error") - Text("Cannot access keychain to save database password") + errorView(Text("Cannot access keychain to save database password")) case .invalidConfirmation: // this can only happen if incorrect parameter is passed - Text(String("Invalid migration confirmation")).font(.title) + titleText("Invalid migration confirmation") + errorView() + case let .unknown(json): titleText("Database error") - Text("Unknown database error: \(json)") + errorView(Text("Unknown database error: \(json)")) case .ok: EmptyView() } if showRestoreDbButton { - Spacer().frame(height: 10) + Spacer() Text("The attempt to change database passphrase was not completed.") + .multilineTextAlignment(.center) + .padding(.horizontal, 25) + .font(.footnote) + restoreDbButton() } } - .padding() + .padding(.horizontal, 25) + .padding(.top, 75) + .padding(.bottom, 25) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .onAppear() { showRestoreDbButton = shouldShowRestoreDbButton() } } - private func titleText(_ s: LocalizedStringKey) -> Text { - Text(s).font(.title) + private func titleText(_ s: LocalizedStringKey) -> some View { + Text(s).font(.largeTitle).bold().multilineTextAlignment(.center) } - private func fileNameText(_ f: String) -> Text { - Text("File: \((f as NSString).lastPathComponent)") + private func fileNameText(_ f: String, font: Font = .caption) -> Text { + Text("File: \((f as NSString).lastPathComponent)").font(font) } - private func migrationsText(_ ms: [String]) -> Text { - Text("Migrations: \(ms.joined(separator: ", "))") - } - - private func mtrErrorDescription(_ err: MTRError) -> LocalizedStringKey { - switch err { - case let .noDown(dbMigrations): - return "database version is newer than the app, but no down migration for: \(dbMigrations.joined(separator: ", "))" - case let .different(appMigration, dbMigration): - return "different migration in the app/database: \(appMigration) / \(dbMigration)" - } + private func migrationsText(_ ms: [String]) -> some View { + (Text("Migrations:").font(.subheadline) + textNewLine + Text(ms.joined(separator: "\n")).font(.caption)) + .multilineTextAlignment(.center) + .padding(.horizontal, 25) } private func databaseKeyField(onSubmit: @escaping () -> Void) -> some View { PassphraseField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey), onSubmit: onSubmit) + .padding(.vertical, 10) + .padding(.horizontal) + .background( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(Color(uiColor: .tertiarySystemFill)) + ) } private func saveAndOpenButton() -> some View { Button("Save passphrase and open chat") { saveAndRunChat() } + .buttonStyle(OnboardingButtonStyle(isDisabled: false)) } private func openChatButton() -> some View { Button("Open chat") { runChat() } + .buttonStyle(OnboardingButtonStyle(isDisabled: false)) } private func saveAndRunChat() { @@ -149,7 +189,7 @@ struct DatabaseErrorView: View { private func runChatSync(confirmMigrations: MigrationConfirmation? = nil) { do { resetChatCtrl() - try initializeChat(start: m.v3DBMigration.startChat, dbKey: useKeychain ? nil : dbKey, confirmMigrations: confirmMigrations) + try initializeChat(start: m.v3DBMigration.startChat, confirmStart: m.v3DBMigration.startChat && AppChatState.shared.value == .stopped, dbKey: useKeychain ? nil : dbKey, confirmMigrations: confirmMigrations) if let s = m.chatDbStatus { status = s let am = AlertManager.shared @@ -201,8 +241,9 @@ struct DatabaseErrorView: View { secondaryButton: .cancel() )) } label: { - Text("Restore database backup").foregroundColor(.red) + Text("Restore database backup") } + .buttonStyle(OnboardingButtonStyle(isDisabled: false)) } private func restoreDb() { @@ -217,6 +258,23 @@ struct DatabaseErrorView: View { )) } } + + private func errorView(_ s: Text? = nil) -> some View { + VStack(spacing: 35) { + Image(systemName: "exclamationmark.triangle.fill") + .resizable() + .frame(width: 50, height: 50) + .foregroundColor(.red) + + if let text = s { + text + .multilineTextAlignment(.center) + .font(.footnote) + } + } + .padding() + .frame(maxWidth: .infinity) + } } struct DatabaseErrorView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index 72515a1fac..59eee1338b 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -15,6 +15,7 @@ enum DatabaseAlert: Identifiable { case importArchive case archiveImported case archiveImportedWithErrors(archiveErrors: [ArchiveError]) + case archiveExportedWithErrors(archivePath: URL, archiveErrors: [ArchiveError]) case deleteChat case chatDeleted case deleteLegacyDatabase @@ -29,6 +30,7 @@ enum DatabaseAlert: Identifiable { case .importArchive: return "importArchive" case .archiveImported: return "archiveImported" case .archiveImportedWithErrors: return "archiveImportedWithErrors" + case .archiveExportedWithErrors: return "archiveExportedWithErrors" case .deleteChat: return "deleteChat" case .chatDeleted: return "chatDeleted" case .deleteLegacyDatabase: return "deleteLegacyDatabase" @@ -41,8 +43,10 @@ enum DatabaseAlert: Identifiable { struct DatabaseView: View { @EnvironmentObject var m: ChatModel - @Binding var showSettings: Bool + @EnvironmentObject var theme: AppTheme + let dismissSettingsSheet: DismissAction @State private var runChat = false + @State private var stoppingChat = false @State private var alert: DatabaseAlert? = nil @State private var showFileImporter = false @State private var importedArchivePath: URL? @@ -54,6 +58,8 @@ struct DatabaseView: View { @State private var useKeychain = storeDBPassphraseGroupDefault.get() @State private var appFilesCountAndSize: (Int, Int)? + @State private var showDatabaseEncryptionView = false + @State var chatItemTTL: ChatItemTTL @State private var currentChatItemTTL: ChatItemTTL = .none @@ -66,7 +72,20 @@ struct DatabaseView: View { } } + @ViewBuilder private func chatDatabaseView() -> some View { + NavigationLink(isActive: $showDatabaseEncryptionView) { + DatabaseEncryptionView(useKeychain: $useKeychain, migration: false, stopChatRunBlockStartChat: { progressIndicator, block in + stopChatRunBlockStartChat(false, progressIndicator, block) + }) + .navigationTitle("Database passphrase") + .modifier(ThemedBackground(grouped: true)) + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + List { let stopped = m.chatRunning == false Section { @@ -82,8 +101,10 @@ struct DatabaseView: View { .disabled(stopped || progressIndicator) } header: { Text("Messages") + .foregroundColor(theme.colors.secondary) } footer: { Text("This setting applies to messages in your current chat profile **\(m.currentUser?.displayName ?? "")**.") + .foregroundColor(theme.colors.secondary) } Section { @@ -96,78 +117,74 @@ struct DatabaseView: View { isOn: $runChat ) .onChange(of: runChat) { _ in - if (runChat) { - startChat() - } else { + if runChat { + DatabaseView.startChat($runChat, $progressIndicator) + } else if !stoppingChat { + stoppingChat = false alert = .stopChat } } } } header: { Text("Run chat") + .foregroundColor(theme.colors.secondary) } footer: { if case .documents = dbContainer { Text("Database will be migrated when the app restarts") + .foregroundColor(theme.colors.secondary) } } Section { let unencrypted = m.chatDbEncrypted == false - let color: Color = unencrypted ? .orange : .secondary + let color: Color = unencrypted ? .orange : theme.colors.secondary settingsRow(unencrypted ? "lock.open" : useKeychain ? "key" : "lock", color: color) { NavigationLink { - DatabaseEncryptionView(useKeychain: $useKeychain) + DatabaseEncryptionView(useKeychain: $useKeychain, migration: false, stopChatRunBlockStartChat: { progressIndicator, block in + stopChatRunBlockStartChat(false, progressIndicator, block) + }) .navigationTitle("Database passphrase") + .modifier(ThemedBackground(grouped: true)) } label: { Text("Database passphrase") } } - settingsRow("square.and.arrow.up") { + settingsRow("square.and.arrow.up", color: theme.colors.secondary) { Button("Export database") { if initialRandomDBPassphraseGroupDefault.get() && !unencrypted { - alert = .exportProhibited + showDatabaseEncryptionView = true + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + alert = .exportProhibited + } } else { - exportArchive() + stopChatRunBlockStartChat(stopped, $progressIndicator) { + await exportArchive() + } } } } - settingsRow("square.and.arrow.down") { + settingsRow("square.and.arrow.down", color: theme.colors.secondary) { Button("Import database", role: .destructive) { showFileImporter = true } } - if let archiveName = chatArchiveName { - let title: LocalizedStringKey = chatArchiveTimeDefault.get() < chatLastStartGroupDefault.get() - ? "Old database archive" - : "New database archive" - settingsRow("archivebox") { - NavigationLink { - ChatArchiveView(archiveName: archiveName) - .navigationTitle(title) - } label: { - Text(title) - } - } - } - settingsRow("trash.slash") { + settingsRow("trash.slash", color: theme.colors.secondary) { Button("Delete database", role: .destructive) { alert = .deleteChat } } } header: { Text("Chat database") + .foregroundColor(theme.colors.secondary) } footer: { - Text( - stopped - ? "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." - : "Stop chat to enable database actions" - ) + Text("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.") + .foregroundColor(theme.colors.secondary) } - .disabled(!stopped) + .disabled(progressIndicator) if case .group = dbContainer, legacyDatabase { - Section("Old database") { - settingsRow("trash") { + Section(header: Text("Old database").foregroundColor(theme.colors.secondary)) { + settingsRow("trash", color: theme.colors.secondary) { Button("Delete old database") { alert = .deleteLegacyDatabase } @@ -179,15 +196,18 @@ struct DatabaseView: View { Button(m.users.count > 1 ? "Delete files for all chat profiles" : "Delete all files", role: .destructive) { alert = .deleteFilesAndMedia } - .disabled(!stopped || appFilesCountAndSize?.0 == 0) + .disabled(progressIndicator || appFilesCountAndSize?.0 == 0) } header: { Text("Files & media") + .foregroundColor(theme.colors.secondary) } footer: { if let (fileCount, size) = appFilesCountAndSize { if fileCount == 0 { Text("No received or sent files") + .foregroundColor(theme.colors.secondary) } else { Text("\(fileCount) file(s) with total size of \(ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .binary))") + .foregroundColor(theme.colors.secondary) } } } @@ -241,7 +261,9 @@ struct DatabaseView: View { title: Text("Import chat database?"), message: Text("Your current chat database will be DELETED and REPLACED with the imported one.") + Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."), primaryButton: .destructive(Text("Import")) { - importArchive(fileURL) + stopChatRunBlockStartChat(m.chatRunning == false, $progressIndicator) { + await DatabaseView.importArchive(fileURL, $progressIndicator, $alert, false) + } }, secondaryButton: .cancel() ) @@ -249,29 +271,35 @@ struct DatabaseView: View { return Alert(title: Text("Error: no database file")) } case .archiveImported: + let (title, message) = archiveImportedAlertText() + return Alert(title: Text(title), message: Text(message)) + case let .archiveImportedWithErrors(errs): + let (title, message) = archiveImportedWithErrorsAlertText(errs: errs) + return Alert(title: Text(title), message: Text(message)) + case let .archiveExportedWithErrors(archivePath, errs): return Alert( - title: Text("Chat database imported"), - message: Text("Restart the app to use imported chat database") - ) - case .archiveImportedWithErrors: - return Alert( - title: Text("Chat database imported"), - message: Text("Restart the app to use imported chat database") + Text("\n") + Text("Some non-fatal errors occurred during import - you may see Chat console for more details.") + title: Text("Chat database exported"), + message: Text("You may save the exported archive.") + textNewLine + Text("Some file(s) were not exported:") + Text(archiveErrorsText(errs)), + dismissButton: .default(Text("Continue")) { + showShareSheet(items: [archivePath]) + } ) case .deleteChat: return Alert( title: Text("Delete chat profile?"), message: Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."), primaryButton: .destructive(Text("Delete")) { - deleteChat() + let wasStopped = m.chatRunning == false + stopChatRunBlockStartChat(wasStopped, $progressIndicator) { + _ = await deleteChat() + return true + } }, secondaryButton: .cancel() ) case .chatDeleted: - return Alert( - title: Text("Chat database deleted"), - message: Text("Restart the app to create a new chat profile") - ) + let (title, message) = chatDeletedAlertText() + return Alert(title: Text(title), message: Text(message)) case .deleteLegacyDatabase: return Alert( title: Text("Delete old database?"), @@ -286,7 +314,10 @@ struct DatabaseView: View { title: Text("Delete files and media?"), message: Text("This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain."), primaryButton: .destructive(Text("Delete")) { - deleteFiles() + stopChatRunBlockStartChat(m.chatRunning == false, $progressIndicator) { + deleteFiles() + return true + } }, secondaryButton: .cancel() ) @@ -306,87 +337,184 @@ struct DatabaseView: View { } } - private func authStopChat() { + private func authStopChat(_ onStop: (() -> Void)? = nil) { if UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) { authenticate(reason: NSLocalizedString("Stop SimpleX", comment: "authentication reason")) { laResult in switch laResult { - case .success: stopChat() - case .unavailable: stopChat() + case .success: stopChat(onStop) + case .unavailable: stopChat(onStop) case .failed: withAnimation { runChat = true } } } } else { - stopChat() + stopChat(onStop) } } - private func stopChat() { + private func stopChat(_ onStop: (() -> Void)? = nil) { Task { do { try await stopChatAsync() + onStop?() } catch let error { await MainActor.run { runChat = true - alert = .error(title: "Error stopping chat", error: responseError(error)) + showAlert("Error stopping chat", message: responseError(error)) } } } } - private func exportArchive() { - progressIndicator = true - Task { + func stopChatRunBlockStartChat( + _ stopped: Bool, + _ progressIndicator: Binding<Bool>, + _ block: @escaping () async throws -> Bool + ) { + // if the chat was running, the sequence is: stop chat, run block, start chat. + // Otherwise, just run block and do nothing - the toggle will be visible anyway and the user can start the chat or not + if stopped { + Task { + do { + _ = try await block() + } catch { + logger.error("Error while executing block: \(error)") + } + } + } else { + authStopChat { + stoppingChat = true + runChat = false + Task { + // if it throws, let's start chat again anyway + var canStart = false + do { + canStart = try await block() + } catch { + logger.error("Error executing block: \(error)") + canStart = true + } + if canStart { + await MainActor.run { + DatabaseView.startChat($runChat, $progressIndicator) + } + } + } + } + } + } + + static func startChat(_ runChat: Binding<Bool>, _ progressIndicator: Binding<Bool>) { + progressIndicator.wrappedValue = true + let m = ChatModel.shared + if m.chatDbChanged { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + resetChatCtrl() + do { + let hadDatabase = hasDatabase() + try initializeChat(start: true) + m.chatDbChanged = false + AppChatState.shared.set(.active) + if m.chatDbStatus != .ok || !hadDatabase { + // Hide current view and show `DatabaseErrorView` + dismissAllSheets(animated: true) + } + } catch let error { + fatalError("Error starting chat \(responseError(error))") + } + progressIndicator.wrappedValue = false + } + } else { do { - let archivePath = try await exportChatArchive() + _ = try apiStartChat() + runChat.wrappedValue = true + m.chatRunning = true + ChatReceiver.shared.start() + chatLastStartGroupDefault.set(Date.now) + AppChatState.shared.set(.active) + } catch let error { + runChat.wrappedValue = false + showAlert(NSLocalizedString("Error starting chat", comment: ""), message: responseError(error)) + } + progressIndicator.wrappedValue = false + } + } + + private func exportArchive() async -> Bool { + await MainActor.run { + progressIndicator = true + } + do { + let (archivePath, archiveErrors) = try await exportChatArchive() + if archiveErrors.isEmpty { showShareSheet(items: [archivePath]) await MainActor.run { progressIndicator = false } - } catch let error { + } else { await MainActor.run { - alert = .error(title: "Error exporting chat database", error: responseError(error)) + alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: archiveErrors) progressIndicator = false } } + } catch let error { + await MainActor.run { + alert = .error(title: "Error exporting chat database", error: responseError(error)) + progressIndicator = false + } } + return false } - private func importArchive(_ archivePath: URL) { + static func importArchive( + _ archivePath: URL, + _ progressIndicator: Binding<Bool>, + _ alert: Binding<DatabaseAlert?>, + _ migration: Bool + ) async -> Bool { if archivePath.startAccessingSecurityScopedResource() { - progressIndicator = true - Task { - do { - try await apiDeleteStorage() - do { - let config = ArchiveConfig(archivePath: archivePath.path) - let archiveErrors = try await apiImportArchive(config: config) - _ = kcDatabasePassword.remove() - if archiveErrors.isEmpty { - await operationEnded(.archiveImported) - } else { - await operationEnded(.archiveImportedWithErrors(archiveErrors: archiveErrors)) - } - } catch let error { - await operationEnded(.error(title: "Error importing chat database", error: responseError(error))) - } - } catch let error { - await operationEnded(.error(title: "Error deleting chat database", error: responseError(error))) - } + defer { archivePath.stopAccessingSecurityScopedResource() } + await MainActor.run { + progressIndicator.wrappedValue = true + } + do { + try await apiDeleteStorage() + try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) + do { + let config = ArchiveConfig(archivePath: archivePath.path) + let archiveErrors = try await apiImportArchive(config: config) + shouldImportAppSettingsDefault.set(true) + _ = kcDatabasePassword.remove() + if archiveErrors.isEmpty { + await operationEnded(.archiveImported, progressIndicator, alert) + return true + } else { + await operationEnded(.archiveImportedWithErrors(archiveErrors: archiveErrors), progressIndicator, alert) + return migration + } + } catch let error { + await operationEnded(.error(title: "Error importing chat database", error: responseError(error)), progressIndicator, alert) + } + } catch let error { + await operationEnded(.error(title: "Error deleting chat database", error: responseError(error)), progressIndicator, alert) + } } else { - alert = .error(title: "Error accessing database file") + showAlert("Error accessing database file") } + return false } - private func deleteChat() { - progressIndicator = true - Task { - do { - try await deleteChatAsync() - await operationEnded(.chatDeleted) - appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory()) - } catch let error { - await operationEnded(.error(title: "Error deleting database", error: responseError(error))) - } + private func deleteChat() async -> Bool { + await MainActor.run { + progressIndicator = true + } + do { + try await deleteChatAsync() + appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory()) + await DatabaseView.operationEnded(.chatDeleted, $progressIndicator, $alert) + return true + } catch let error { + await DatabaseView.operationEnded(.error(title: "Error deleting database", error: responseError(error)), $progressIndicator, $alert) + return false } } @@ -398,39 +526,30 @@ struct DatabaseView: View { } } - private func operationEnded(_ dbAlert: DatabaseAlert) async { + private static func operationEnded(_ dbAlert: DatabaseAlert, _ progressIndicator: Binding<Bool>, _ alert: Binding<DatabaseAlert?>) async { await MainActor.run { + let m = ChatModel.shared m.chatDbChanged = true m.chatInitialized = false - progressIndicator = false - alert = dbAlert + progressIndicator.wrappedValue = false } - } - - private func startChat() { - if m.chatDbChanged { - showSettings = false - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - resetChatCtrl() - do { - try initializeChat(start: true) - m.chatDbChanged = false - AppChatState.shared.set(.active) - } catch let error { - fatalError("Error starting chat \(responseError(error))") - } - } - } else { - do { - _ = try apiStartChat() - runChat = true - m.chatRunning = true - ChatReceiver.shared.start() - chatLastStartGroupDefault.set(Date.now) - AppChatState.shared.set(.active) - } catch let error { - runChat = false - alert = .error(title: "Error starting chat", error: responseError(error)) + await withCheckedContinuation { cont in + let okAlertActionWaiting = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default, handler: { _ in cont.resume() }) + // show these alerts globally so they are visible when all sheets will be hidden + if case .archiveImported = dbAlert { + let (title, message) = archiveImportedAlertText() + showAlert(title, message: message, actions: { [okAlertActionWaiting] }) + } else if case .archiveImportedWithErrors(let errs) = dbAlert { + let (title, message) = archiveImportedWithErrorsAlertText(errs: errs) + showAlert(title, message: message, actions: { [okAlertActionWaiting] }) + } else if case .chatDeleted = dbAlert { + let (title, message) = chatDeletedAlertText() + showAlert(title, message: message, actions: { [okAlertActionWaiting] }) + } else if case let .error(title, error) = dbAlert { + showAlert("\(title)", message: error, actions: { [okAlertActionWaiting] }) + } else { + alert.wrappedValue = dbAlert + cont.resume() } } } @@ -461,7 +580,7 @@ struct DatabaseView: View { appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory()) do { let chats = try apiGetChats() - m.updateChats(with: chats) + m.updateChats(chats) } catch let error { logger.error("apiGetChats: cannot update chats \(responseError(error))") } @@ -473,6 +592,37 @@ struct DatabaseView: View { } } +func archiveImportedAlertText() -> (String, String) { + ( + NSLocalizedString("Chat database imported", comment: ""), + NSLocalizedString("Restart the app to use imported chat database", comment: "") + ) +} +func archiveImportedWithErrorsAlertText(errs: [ArchiveError]) -> (String, String) { + ( + NSLocalizedString("Chat database imported", comment: ""), + NSLocalizedString("Restart the app to use imported chat database", comment: "") + "\n" + NSLocalizedString("Some non-fatal errors occurred during import:", comment: "") + archiveErrorsText(errs) + ) +} + +private func chatDeletedAlertText() -> (String, String) { + ( + NSLocalizedString("Chat database deleted", comment: ""), + NSLocalizedString("Restart the app to create a new chat profile", comment: "") + ) +} + +func archiveErrorsText(_ errs: [ArchiveError]) -> String { + return "\n" + errs.map(showArchiveError).joined(separator: "\n") + + func showArchiveError(_ err: ArchiveError) -> String { + switch err { + case let .import(importError): importError + case let .fileError(file, fileError): "\(file): \(fileError)" + } + } +} + func stopChatAsync() async throws { try await apiStopChat() ChatReceiver.shared.stop() @@ -484,10 +634,17 @@ func deleteChatAsync() async throws { try await apiDeleteStorage() _ = kcDatabasePassword.remove() storeDBPassphraseGroupDefault.set(true) + deleteAppDatabaseAndFiles() + // Clean state so when creating new user the app will start chat automatically (see CreateProfile:createProfile()) + DispatchQueue.main.async { + ChatModel.shared.users = [] + } } struct DatabaseView_Previews: PreviewProvider { + @Environment(\.dismiss) static var mockDismiss + static var previews: some View { - DatabaseView(showSettings: Binding.constant(false), chatItemTTL: .none) + DatabaseView(dismissSettingsSheet: mockDismiss, chatItemTTL: .none) } } diff --git a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift index 046929a9d0..79c0a42ae0 100644 --- a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift +++ b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift @@ -117,7 +117,7 @@ struct MigrateToAppGroupView: View { setV3DBMigration(.migration_error) migrationError = "Error starting chat: \(responseError(error))" } - deleteOldArchive() + deleteOldChatArchive() } label: { Text("Start chat") .font(.title) @@ -188,7 +188,9 @@ struct MigrateToAppGroupView: View { let config = ArchiveConfig(archivePath: getDocumentsDirectory().appendingPathComponent(archiveName).path) Task { do { - try await apiExportArchive(config: config) + try apiSaveAppSettings(settings: AppSettings.current.prepareForExport()) + try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) + _ = try await apiExportArchive(config: config) await MainActor.run { setV3DBMigration(.exported) } } catch let error { await MainActor.run { @@ -204,7 +206,11 @@ struct MigrateToAppGroupView: View { resetChatCtrl() try await MainActor.run { try initializeChat(start: false) } let _ = try await apiImportArchive(config: config) - await MainActor.run { setV3DBMigration(.migrated) } + let appSettings = try apiGetAppSettings(settings: AppSettings.current.prepareForExport()) + await MainActor.run { + appSettings.importIntoApp() + setV3DBMigration(.migrated) + } } catch let error { dbContainerGroupDefault.set(.documents) await MainActor.run { @@ -216,20 +222,29 @@ struct MigrateToAppGroupView: View { } } -func exportChatArchive() async throws -> URL { +func exportChatArchive(_ storagePath: URL? = nil) async throws -> (URL, [ArchiveError]) { let archiveTime = Date.now let ts = archiveTime.ISO8601Format(Date.ISO8601FormatStyle(timeSeparator: .omitted)) let archiveName = "simplex-chat.\(ts).zip" - let archivePath = getDocumentsDirectory().appendingPathComponent(archiveName) + let archivePath = (storagePath ?? getDocumentsDirectory()).appendingPathComponent(archiveName) let config = ArchiveConfig(archivePath: archivePath.path) - try await apiExportArchive(config: config) - deleteOldArchive() - UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME) - chatArchiveTimeDefault.set(archiveTime) - return archivePath + // Settings should be saved before changing a passphrase, otherwise the database needs to be migrated first + if !ChatModel.shared.chatDbChanged { + try apiSaveAppSettings(settings: AppSettings.current.prepareForExport()) + } + try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) + let errs = try await apiExportArchive(config: config) + if storagePath == nil { + deleteOldChatArchive() + UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME) + chatArchiveTimeDefault.set(archiveTime) + } + return (archivePath, errs) } -func deleteOldArchive() { +/// Deprecated. Remove in the end of 2025. All unused archives should be deleted for the most users til then. +/// Remove DEFAULT_CHAT_ARCHIVE_NAME and DEFAULT_CHAT_ARCHIVE_TIME as well +func deleteOldChatArchive() { let d = UserDefaults.standard if let archiveName = d.string(forKey: DEFAULT_CHAT_ARCHIVE_NAME) { do { diff --git a/apps/ios/Shared/Views/Helpers/AppSheet.swift b/apps/ios/Shared/Views/Helpers/AppSheet.swift index 0e64776ed6..1e334367e8 100644 --- a/apps/ios/Shared/Views/Helpers/AppSheet.swift +++ b/apps/ios/Shared/Views/Helpers/AppSheet.swift @@ -8,42 +8,24 @@ import SwiftUI -private struct SheetIsPresented<C>: ViewModifier where C: View { - var isPresented: Binding<Bool> - var onDismiss: (() -> Void)? - var sheetContent: () -> C - @Environment(\.scenePhase) var scenePhase +class AppSheetState: ObservableObject { + static let shared = AppSheetState() + @Published var scenePhaseActive: Bool = false - func body(content: Content) -> some View { - content.sheet(isPresented: isPresented, onDismiss: onDismiss) { - sheetContent().modifier(PrivacySensitive()) - } - } -} - -private struct SheetForItem<T, C>: ViewModifier where T: Identifiable, C: View { - var item: Binding<T?> - var onDismiss: (() -> Void)? - var sheetContent: (T) -> C - @Environment(\.scenePhase) var scenePhase - - func body(content: Content) -> some View { - content.sheet(item: item, onDismiss: onDismiss) { it in - sheetContent(it).modifier(PrivacySensitive()) - } + func redactionReasons(_ protectScreen: Bool) -> RedactionReasons { + !protectScreen || scenePhaseActive + ? RedactionReasons() + : RedactionReasons.placeholder } } private struct PrivacySensitive: ViewModifier { @AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false - @Environment(\.scenePhase) var scenePhase + // Screen protection doesn't work for appSheet on iOS 16 if @Environment(\.scenePhase) is used instead of global state + @ObservedObject var appSheetState: AppSheetState = AppSheetState.shared func body(content: Content) -> some View { - if case .active = scenePhase { - content - } else { - content.privacySensitive(protectScreen).redacted(reason: .privacy) - } + content.redacted(reason: appSheetState.redactionReasons(protectScreen)) } } @@ -53,7 +35,9 @@ extension View { onDismiss: (() -> Void)? = nil, content: @escaping () -> Content ) -> some View where Content: View { - modifier(SheetIsPresented(isPresented: isPresented, onDismiss: onDismiss, sheetContent: content)) + sheet(isPresented: isPresented, onDismiss: onDismiss) { + content().modifier(PrivacySensitive()) + } } func appSheet<T, Content>( @@ -61,6 +45,8 @@ extension View { onDismiss: (() -> Void)? = nil, content: @escaping (T) -> Content ) -> some View where T: Identifiable, Content: View { - modifier(SheetForItem(item: item, onDismiss: onDismiss, sheetContent: content)) + sheet(item: item, onDismiss: onDismiss) { it in + content(it).modifier(PrivacySensitive()) + } } } diff --git a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift index 1b344148c0..40d62e009b 100644 --- a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift +++ b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift @@ -10,21 +10,18 @@ import SwiftUI import SimpleXChat struct ChatInfoImage: View { + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat + var size: CGFloat var color = Color(uiColor: .tertiarySystemGroupedBackground) var body: some View { - var iconName: String - switch chat.chatInfo { - case .direct: iconName = "person.crop.circle.fill" - case .group: iconName = "person.2.circle.fill" - case .contactRequest: iconName = "person.crop.circle.fill" - default: iconName = "circle.fill" - } + let iconColor = if case .local = chat.chatInfo { theme.appColors.primaryVariant2 } else { color } return ProfileImage( imageStr: chat.chatInfo.image, - iconName: iconName, - color: color + iconName: chatIconName(chat.chatInfo), + size: size, + color: iconColor ) } } @@ -32,8 +29,9 @@ struct ChatInfoImage: View { struct ChatInfoImage_Previews: PreviewProvider { static var previews: some View { ChatInfoImage( - chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []) - , color: Color(red: 0.9, green: 0.9, blue: 0.9) + chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), + size: 63, + color: Color(red: 0.9, green: 0.9, blue: 0.9) ) .previewLayout(.fixed(width: 63, height: 63)) } diff --git a/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift b/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift new file mode 100644 index 0000000000..9aa6ac86cf --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift @@ -0,0 +1,175 @@ +// +// ChatItemClipShape.swift +// SimpleX (iOS) +// +// Created by Levitating Pineapple on 04/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +/// Modifier, which provides clipping mask for ``ChatItemWithMenu`` view +/// and it's previews: (drag interaction, context menu, etc.) +/// Supports [Dynamic Type](https://developer.apple.com/documentation/uikit/uifont/scaling_fonts_automatically) +/// by retaining pill shape, even when ``ChatItem``'s height is less that twice its corner radius +struct ChatItemClipped: ViewModifier { + @AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var roundness = defaultChatItemRoundness + @AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var tailEnabled = true + private let chatItem: (content: CIContent, chatDir: CIDirection)? + private let tailVisible: Bool + + init() { + self.chatItem = nil + self.tailVisible = false + } + + init(_ ci: ChatItem, tailVisible: Bool) { + self.chatItem = (ci.content, ci.chatDir) + self.tailVisible = tailVisible + } + + private func shapeStyle() -> ChatItemShape.Style { + if let ci = chatItem { + switch ci.content { + case + .sndMsgContent, + .rcvMsgContent, + .rcvDecryptionError, + .rcvIntegrityError, + .invalidJSON: + let tail = if let mc = ci.content.msgContent, mc.isImageOrVideo && mc.text.isEmpty { + false + } else { + tailVisible + } + return tailEnabled + ? .bubble( + padding: ci.chatDir.sent ? .trailing : .leading, + tailVisible: tail + ) + : .roundRect(radius: msgRectMaxRadius) + case .rcvGroupInvitation, .sndGroupInvitation: + return .roundRect(radius: msgRectMaxRadius) + default: return .roundRect(radius: 8) + } + } else { + return .roundRect(radius: msgRectMaxRadius) + } + } + + func body(content: Content) -> some View { + let clipShape = ChatItemShape( + roundness: roundness, + style: shapeStyle() + ) + content + .contentShape(.dragPreview, clipShape) + .contentShape(.contextMenuPreview, clipShape) + .clipShape(clipShape) + } +} + +struct ChatTailPadding: ViewModifier { + func body(content: Content) -> some View { + content.padding(.horizontal, -msgTailWidth) + } +} + +private let msgRectMaxRadius: Double = 18 +private let msgBubbleMaxRadius: Double = msgRectMaxRadius * 1.2 +private let msgTailWidth: Double = 9 +private let msgTailMinHeight: Double = msgTailWidth * 1.254 // ~56deg +private let msgTailMaxHeight: Double = msgTailWidth * 1.732 // 60deg + +struct ChatItemShape: Shape { + fileprivate enum Style { + case bubble(padding: HorizontalEdge, tailVisible: Bool) + case roundRect(radius: Double) + } + + fileprivate let roundness: Double + fileprivate let style: Style + + func path(in rect: CGRect) -> Path { + switch style { + case let .bubble(padding, tailVisible): + let w = rect.width + let h = rect.height + let rxMax = min(msgBubbleMaxRadius, w / 2) + let ryMax = min(msgBubbleMaxRadius, h / 2) + let rx = roundness * rxMax + let ry = roundness * ryMax + let tailHeight = min(msgTailMinHeight + roundness * (msgTailMaxHeight - msgTailMinHeight), h / 2) + var path = Path() + // top side + path.move(to: CGPoint(x: rx, y: 0)) + path.addLine(to: CGPoint(x: w - rx, y: 0)) + if roundness > 0 { + // top-right corner + path.addQuadCurve(to: CGPoint(x: w, y: ry), control: CGPoint(x: w, y: 0)) + } + if rect.height > 2 * ry { + // right side + path.addLine(to: CGPoint(x: w, y: h - ry)) + } + if roundness > 0 { + // bottom-right corner + path.addQuadCurve(to: CGPoint(x: w - rx, y: h), control: CGPoint(x: w, y: h)) + } + // bottom side + if tailVisible { + path.addLine(to: CGPoint(x: -msgTailWidth, y: h)) + if roundness > 0 { + // bottom-left tail + // distance of control point from touch point, calculated via ratios + let d = tailHeight - msgTailWidth * msgTailWidth / tailHeight + // tail control point + let tc = CGPoint(x: 0, y: h - tailHeight + d * sqrt(roundness)) + // bottom-left tail curve + path.addQuadCurve(to: CGPoint(x: 0, y: h - tailHeight), control: tc) + } else { + path.addLine(to: CGPoint(x: 0, y: h - tailHeight)) + } + if rect.height > ry + tailHeight { + // left side + path.addLine(to: CGPoint(x: 0, y: ry)) + } + } else { + path.addLine(to: CGPoint(x: rx, y: h)) + path.addQuadCurve(to: CGPoint(x: 0, y: h - ry), control: CGPoint(x: 0 , y: h)) + if rect.height > 2 * ry { + // left side + path.addLine(to: CGPoint(x: 0, y: ry)) + } + } + if roundness > 0 { + // top-left corner + path.addQuadCurve(to: CGPoint(x: rx, y: 0), control: CGPoint(x: 0, y: 0)) + } + path.closeSubpath() + return switch padding { + case .leading: path + case .trailing: path + .scale(x: -1, y: 1, anchor: .center) + .path(in: rect) + } + case let .roundRect(radius): + return Path(roundedRect: rect, cornerRadius: radius * roundness) + } + } + + var offset: Double? { + switch style { + case let .bubble(padding, isTailVisible): + if isTailVisible { + switch padding { + case .leading: -msgTailWidth + case .trailing: msgTailWidth + } + } else { 0 } + case .roundRect: 0 + } + } + +} diff --git a/apps/ios/Shared/Views/Helpers/ChatWallpaper.swift b/apps/ios/Shared/Views/Helpers/ChatWallpaper.swift new file mode 100644 index 0000000000..cc5be9e7bb --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/ChatWallpaper.swift @@ -0,0 +1,113 @@ +// +// ChatWallpaper.swift +// SimpleX (iOS) +// +// Created by Avently on 14.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI +import SimpleXChat + +struct ChatViewBackground: ViewModifier { + @Environment(\.colorScheme) var colorScheme + var image: Image + var imageType: WallpaperType + var background: Color + var tint: Color + + func body(content: Content) -> some View { + // Workaround a problem (SwiftUI bug?) when wallpaper is not updated when user changes global theme in iOS settings from dark to light and vice versa + if colorScheme == .light { + back(content) + } else { + back(content) + } + } + + func back(_ content: Content) -> some View { + content.background( + Canvas { context, size in + var image = context.resolve(image) + let rect = CGRectMake(0, 0, size.width, size.height) + func repeatDraw(_ imageScale: CGFloat) { + // Prevent range bounds crash and dividing by zero + if size.height == 0 || size.width == 0 || image.size.height == 0 || image.size.width == 0 { return } + image.shading = .color(tint) + let scale = imageScale * 2.5 // scale wallpaper for iOS + for h in 0 ... Int(size.height / image.size.height / scale) { + for w in 0 ... Int(size.width / image.size.width / scale) { + let rect = CGRectMake(CGFloat(w) * image.size.width * scale, CGFloat(h) * image.size.height * scale, image.size.width * scale, image.size.height * scale) + context.draw(image, in: rect, style: FillStyle()) + } + } + } + context.fill(Path(rect), with: .color(background)) + switch imageType { + case let WallpaperType.preset(filename, scale): + repeatDraw(CGFloat((scale ?? 1) * (PresetWallpaper.from(filename)?.scale ?? 1))) + case let WallpaperType.image(_, scale, scaleType): + let scaleType = scaleType ?? WallpaperScaleType.fill + switch scaleType { + case WallpaperScaleType.repeat: repeatDraw(CGFloat(scale ?? 1)) + case WallpaperScaleType.fill: fallthrough + case WallpaperScaleType.fit: + let scale = scaleType.computeScaleFactor(image.size, size) + let scaledWidth = (image.size.width * scale.0) + let scaledHeight = (image.size.height * scale.1) + context.draw(image, in: CGRectMake(((size.width - scaledWidth) / 2), ((size.height - scaledHeight) / 2), scaledWidth, scaledHeight), style: FillStyle()) + if case WallpaperScaleType.fit = scaleType { + if scaledWidth < size.width { + // has black lines at left and right sides + var x = (size.width - scaledWidth) / 2 + while x > 0 { + context.draw(image, in: CGRectMake((x - scaledWidth), ((size.height - scaledHeight) / 2), scaledWidth, scaledHeight), style: FillStyle()) + x -= scaledWidth + } + x = size.width - (size.width - scaledWidth) / 2 + while x < size.width { + context.draw(image, in: CGRectMake(x, ((size.height - scaledHeight) / 2), scaledWidth, scaledHeight), style: FillStyle()) + + x += scaledWidth + } + } else { + // has black lines at top and bottom sides + var y = (size.height - scaledHeight) / 2 + while y > 0 { + context.draw(image, in: CGRectMake(((size.width - scaledWidth) / 2), (y - scaledHeight), scaledWidth, scaledHeight), style: FillStyle()) + y -= scaledHeight + } + y = size.height - (size.height - scaledHeight) / 2 + while y < size.height { + context.draw(image, in: CGRectMake(((size.width - scaledWidth) / 2), y, scaledWidth, scaledHeight), style: FillStyle()) + y += scaledHeight + } + } + } + context.fill(Path(rect), with: .color(tint)) + } + case WallpaperType.empty: () + } + } + ).ignoresSafeArea(.all) + } +} + +extension PresetWallpaper { + public func toType(_ base: DefaultTheme, _ scale: Float? = nil) -> WallpaperType { + let scale = if let scale { + scale + } else if let type = ChatModel.shared.currentUser?.uiThemes?.preferredMode(base.mode == DefaultThemeMode.dark)?.wallpaper?.toAppWallpaper().type, type.sameType(WallpaperType.preset(filename, nil)) { + type.scale + } else if let scale = themeOverridesDefault.get().first(where: { $0.wallpaper != nil && $0.wallpaper!.preset == filename && $0.base == base })?.wallpaper?.scale { + scale + } else { + Float(1.0) + } + return WallpaperType.preset( + filename, + scale + ) + } +} diff --git a/apps/ios/Shared/Views/Helpers/ContextMenu.swift b/apps/ios/Shared/Views/Helpers/ContextMenu.swift deleted file mode 100644 index 287aba5262..0000000000 --- a/apps/ios/Shared/Views/Helpers/ContextMenu.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// ContextMenu2.swift -// SimpleX (iOS) -// -// Created by Evgeny on 09/08/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import Foundation -import UIKit -import SwiftUI - -extension View { - func uiKitContextMenu(menu: Binding<UIMenu>, allowMenu: Binding<Bool>) -> some View { - self.overlay { - if allowMenu.wrappedValue { - self.overlay(Color(uiColor: .systemBackground)).overlay(InteractionView(content: self, menu: menu)) - } - } - } -} - -private struct InteractionConfig<Content: View> { - let content: Content - let menu: UIMenu -} - -private struct InteractionView<Content: View>: UIViewRepresentable { - let content: Content - @Binding var menu: UIMenu - - func makeUIView(context: Context) -> UIView { - let view = UIView() - view.backgroundColor = .clear - let hostView = UIHostingController(rootView: content) - hostView.view.translatesAutoresizingMaskIntoConstraints = false - let constraints = [ - hostView.view.topAnchor.constraint(equalTo: view.topAnchor), - hostView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - hostView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), - hostView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), - hostView.view.widthAnchor.constraint(equalTo: view.widthAnchor), - hostView.view.heightAnchor.constraint(equalTo: view.heightAnchor) - ] - view.addSubview(hostView.view) - view.addConstraints(constraints) - let menuInteraction = UIContextMenuInteraction(delegate: context.coordinator) - view.addInteraction(menuInteraction) - return view - } - - func updateUIView(_ uiView: UIView, context: Context) {} - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - class Coordinator: NSObject, UIContextMenuInteractionDelegate { - let parent: InteractionView<Content> - - init(_ parent: InteractionView<Content>) { - self.parent = parent - } - - func contextMenuInteraction( - _ interaction: UIContextMenuInteraction, - configurationForMenuAtLocation location: CGPoint - ) -> UIContextMenuConfiguration? { - UIContextMenuConfiguration( - identifier: nil, - previewProvider: nil, - actionProvider: { [weak self] _ in - guard let self = self else { return nil } - return self.parent.menu - } - ) - } - - // func contextMenuInteraction( - // _ interaction: UIContextMenuInteraction, - // willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, - // animator: UIContextMenuInteractionCommitAnimating - // ) { - // animator.addCompletion { - // print("user tapped") - // } - // } - } -} diff --git a/apps/ios/Shared/Views/Helpers/DetermineWidth.swift b/apps/ios/Shared/Views/Helpers/DetermineWidth.swift index d2a0aaab1d..b05ab17089 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 DetermineWidthImageVideoItem: View { + typealias Key = MaximumWidthImageVideoPreferenceKey + var body: some View { + GeometryReader { proxy in + Color.clear + .preference( + key: MaximumWidthImageVideoPreferenceKey.self, + value: proxy.size.width + ) + } + } +} + struct MaximumWidthPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { @@ -28,6 +41,13 @@ struct MaximumWidthPreferenceKey: PreferenceKey { } } +struct MaximumWidthImageVideoPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = max(value, nextValue()) + } +} + struct DetermineWidth_Previews: PreviewProvider { static var previews: some View { DetermineWidth() diff --git a/apps/ios/Shared/Views/Helpers/ImagePicker.swift b/apps/ios/Shared/Views/Helpers/ImagePicker.swift index fe8d5bbdd4..d7525027e0 100644 --- a/apps/ios/Shared/Views/Helpers/ImagePicker.swift +++ b/apps/ios/Shared/Views/Helpers/ImagePicker.swift @@ -33,6 +33,7 @@ struct LibraryMediaListPicker: UIViewControllerRepresentable { typealias UIViewControllerType = PHPickerViewController var addMedia: (_ content: UploadContent) async -> Void var selectionLimit: Int + var filter: PHPickerFilter = .any(of: [.images, .videos]) var finishedPreprocessing: () -> Void = {} var didFinishPicking: (_ didSelectItems: Bool) async -> Void @@ -148,7 +149,7 @@ struct LibraryMediaListPicker: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> PHPickerViewController { var config = PHPickerConfiguration() - config.filter = .any(of: [.images, .videos]) + config.filter = filter config.selectionLimit = selectionLimit config.selection = .ordered config.preferredAssetRepresentationMode = .current diff --git a/apps/ios/Shared/Views/Helpers/InvertedForegroundStyle.swift b/apps/ios/Shared/Views/Helpers/InvertedForegroundStyle.swift new file mode 100644 index 0000000000..dca413dafe --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/InvertedForegroundStyle.swift @@ -0,0 +1,21 @@ +// +// Test.swift +// SimpleX (iOS) +// +// Created by Levitating Pineapple on 31/08/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +extension View { + @ViewBuilder + func invertedForegroundStyle(enabled: Bool = true) -> some View { + if enabled { + foregroundStyle(Material.ultraThin) + .environment(\.colorScheme, .dark) + .grayscale(1) + .contrast(-20) + } else { self } + } +} diff --git a/apps/ios/Shared/Views/Helpers/KeyboardPadding.swift b/apps/ios/Shared/Views/Helpers/KeyboardPadding.swift deleted file mode 100644 index 45d766ddfd..0000000000 --- a/apps/ios/Shared/Views/Helpers/KeyboardPadding.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// KeyboardPadding.swift -// SimpleX (iOS) -// -// Created by Evgeny on 10/07/2023. -// Copyright © 2023 SimpleX Chat. All rights reserved. -// - -import SwiftUI - -extension View { - @ViewBuilder func keyboardPadding() -> some View { - if #available(iOS 17.0, *) { - GeometryReader { g in - self.padding(.bottom, max(0, ChatModel.shared.keyboardHeight - g.safeAreaInsets.bottom)) - } - } else { - self - } - } -} diff --git a/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift b/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift index 2d5458b9d3..fdc3f2129f 100644 --- a/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift +++ b/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift @@ -7,16 +7,17 @@ // import SwiftUI +import SimpleXChat -struct NavLinkPlain<V: Hashable, Label: View>: View { - @State var tag: V - @Binding var selection: V? +struct NavLinkPlain<Label: View>: View { + let chatId: ChatId + @Binding var selection: ChatId? @ViewBuilder var label: () -> Label var disabled = false var body: some View { ZStack { - Button("") { DispatchQueue.main.async { selection = tag } } + Button("") { ItemsModel.shared.loadOpenChat(chatId) } .disabled(disabled) label() } diff --git a/apps/ios/Shared/Views/Helpers/NavStackCompat.swift b/apps/ios/Shared/Views/Helpers/NavStackCompat.swift index 6e3b89c9b8..e9383fc073 100644 --- a/apps/ios/Shared/Views/Helpers/NavStackCompat.swift +++ b/apps/ios/Shared/Views/Helpers/NavStackCompat.swift @@ -17,7 +17,9 @@ struct NavStackCompat <C: View, D: View>: View { if #available(iOS 16, *) { NavigationStack(path: Binding( get: { isActive.wrappedValue ? [true] : [] }, - set: { _ in } + set: { path in + if path.isEmpty { isActive.wrappedValue = false } + } )) { ZStack { NavigationLink(value: true) { EmptyView() } diff --git a/apps/ios/Shared/Views/Helpers/ProfileImage.swift b/apps/ios/Shared/Views/Helpers/ProfileImage.swift index cc4f09ae3b..3eedd56441 100644 --- a/apps/ios/Shared/Views/Helpers/ProfileImage.swift +++ b/apps/ios/Shared/Views/Helpers/ProfileImage.swift @@ -10,28 +10,54 @@ import SwiftUI import SimpleXChat struct ProfileImage: View { + @EnvironmentObject var theme: AppTheme var imageStr: String? = nil var iconName: String = "person.crop.circle.fill" + var size: CGFloat var color = Color(uiColor: .tertiarySystemGroupedBackground) - + var backgroundColor: Color? = nil + var blurred = false + @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner + var body: some View { - if let image = imageStr, - let data = Data(base64Encoded: dropImagePrefix(image)), - let uiImage = UIImage(data: data) { - Image(uiImage: uiImage) - .resizable() - .clipShape(Circle()) + if let uiImage = imageFromBase64(imageStr) { + clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius, blurred: blurred) } else { + let c = color.asAnotherColorFromSecondaryVariant(theme) Image(systemName: iconName) .resizable() - .foregroundColor(color) + .foregroundColor(c) + .frame(width: size, height: size) + .background(Circle().fill(backgroundColor != nil ? backgroundColor! : .clear)) + } + } +} + +extension Color { + func asAnotherColorFromSecondary(_ theme: AppTheme) -> Color { + return self + } + + func asAnotherColorFromSecondaryVariant(_ theme: AppTheme) -> Color { + let s = theme.colors.secondaryVariant + let l = theme.colors.isLight + return switch self { + case Color(uiColor: .tertiaryLabel): // ChatView title + l ? s.darker(0.05) : s.lighter(0.2) + case Color(uiColor: .tertiarySystemFill): // SettingsView, ChatInfoView + l ? s.darker(0.065) : s.lighter(0.085) + case Color(uiColor: .quaternaryLabel): // ChatListView user picker + l ? s.darker(0.1) : s.lighter(0.1) + case Color(uiColor: .tertiarySystemGroupedBackground): // ChatListView items, forward view + s.asGroupedBackground(theme.base.mode) + default: self } } } struct ProfileImage_Previews: PreviewProvider { static var previews: some View { - ProfileImage(imageStr: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAKHGlDQ1BJQ0MgUHJvZmlsZQAASImFVgdUVNcWve9Nb7QZeu9NehtAem/Sq6gMQ28OQxWxgAQjEFFEREARNFQFg1KjiIhiIQgoYA9IEFBisCAq6OQNJNH4//r/zDpz9ttzz7n73ffWmg0A6QCDxYqD+QCIT0hmezlYywQEBsngngEYCAIy0AC6DGYSy8rDwxUg8Xf9d7wbAxC33tHgzvrP3/9nCISFJzEBgIIRTGey2MkILkawT1oyi4tnEUxjI6IQvMLFkauYqxjQQtewwuoaHy8bBNMBwJMZDHYkAERbhJdJZUYic4hhCNZOCItOQDB3vjkzioFwxLsIXhcRl5IOAImrRzs+fivCk7QRrIL0shAcwNUW+tX8yH/tFfrPXgxG5D84Pi6F+dc9ck+HHJ7g641UMSQlQATQBHEgBaQDGcACbLAVYaIRJhx5Dv+9j77aZ4OsZIFtSEc0iARRIBnpt/9qlvfqpGSQBhjImnCEcUU+NtxnujZy4fbqVEiU/wuXdQyA9S0cDqfzC+e2F4DzyLkSB79wyi0A8KoBcL2GmcJOXePQ3C8MIAJeQAOiQArIAxXuWwMMgSmwBHbAGbgDHxAINgMmojceUZUGMkEWyAX54AA4DMpAJTgJ6sAZ0ALawQVwGVwDt8AQGAUPwQSYBi/AAngHliEIwkEUiAqJQtKQIqQO6UJ0yByyg1whLygQCoEioQQoBcqE9kD5UBFUBlVB9dBPUCd0GboBDUP3oUloDnoNfYRRMBmmwZKwEqwF02Er2AX2gTfBkXAinAHnwPvhUrgaPg23wZfhW/AoPAG/gBdRAEVCCaFkURooOsoG5Y4KQkWg2KidqDxUCaoa1YTqQvWj7qAmUPOoD2gsmoqWQWugTdGOaF80E52I3okuQJeh69Bt6D70HfQkegH9GUPBSGDUMSYYJ0wAJhKThsnFlGBqMK2Yq5hRzDTmHRaLFcIqY42wjthAbAx2O7YAewzbjO3BDmOnsIs4HE4Up44zw7njGLhkXC7uKO407hJuBDeNe48n4aXxunh7fBA+AZ+NL8E34LvxI/gZ/DKBj6BIMCG4E8II2wiFhFOELsJtwjRhmchPVCaaEX2IMcQsYimxiXiV+Ij4hkQiyZGMSZ6kaNJuUinpLOk6aZL0gSxAViPbkIPJKeT95FpyD/k++Q2FQlGiWFKCKMmU/ZR6yhXKE8p7HiqPJo8TTxjPLp5ynjaeEZ6XvAReRV4r3s28GbwlvOd4b/PO8xH4lPhs+Bh8O/nK+Tr5xvkW+an8Ovzu/PH8BfwN/Df4ZwVwAkoCdgJhAjkCJwWuCExRUVR5qg2VSd1DPUW9Sp2mYWnKNCdaDC2fdoY2SFsQFBDUF/QTTBcsF7woOCGEElISchKKEyoUahEaE/ooLClsJRwuvE+4SXhEeElEXMRSJFwkT6RZZFTko6iMqJ1orOhB0XbRx2JoMTUxT7E0seNiV8XmxWnipuJM8TzxFvEHErCEmoSXxHaJkxIDEouSUpIOkizJo5JXJOelhKQspWKkiqW6peakqdLm0tHSxdKXpJ/LCMpYycTJlMr0ySzISsg6yqbIVskOyi7LKcv5ymXLNcs9lifK0+Uj5Ivle+UXFKQV3BQyFRoVHigSFOmKUYpHFPsVl5SUlfyV9iq1K80qiyg7KWcoNyo/UqGoWKgkqlSr3FXFqtJVY1WPqQ6pwWoGalFq5Wq31WF1Q/Vo9WPqw+sw64zXJayrXjeuQdaw0kjVaNSY1BTSdNXM1mzXfKmloBWkdVCrX+uztoF2nPYp7Yc6AjrOOtk6XTqvddV0mbrlunf1KHr2erv0OvRe6avrh+sf179nQDVwM9hr0GvwydDIkG3YZDhnpGAUYlRhNE6n0T3oBfTrxhhja+NdxheMP5gYmiSbtJj8YaphGmvaYDq7Xnl9+PpT66fM5MwYZlVmE+Yy5iHmJ8wnLGQtGBbVFk8t5S3DLGssZ6xUrWKsTlu9tNa2Zlu3Wi/ZmNjssOmxRdk62ObZDtoJ2Pnaldk9sZezj7RvtF9wMHDY7tDjiHF0cTzoOO4k6cR0qndacDZy3uHc50J28XYpc3nqqubKdu1yg92c3Q65PdqguCFhQ7s7cHdyP+T+2EPZI9HjZ0+sp4dnueczLx2vTK9+b6r3Fu8G73c+1j6FPg99VXxTfHv9eP2C/er9lvxt/Yv8JwK0AnYE3AoUC4wO7AjCBfkF1QQtbrTbeHjjdLBBcG7w2CblTembbmwW2xy3+eIW3i2MLedCMCH+IQ0hKwx3RjVjMdQptCJ0gWnDPMJ8EWYZVhw2F24WXhQ+E2EWURQxG2kWeShyLsoiqiRqPtomuiz6VYxjTGXMUqx7bG0sJ84/rjkeHx8S35kgkBCb0LdVamv61mGWOiuXNZFokng4cYHtwq5JgpI2JXUk05A/0oEUlZTvUiZTzVPLU9+n+aWdS+dPT0gf2Ka2bd+2mQz7jB+3o7czt/dmymZmZU7usNpRtRPaGbqzd5f8rpxd07sddtdlEbNis37J1s4uyn67x39PV45kzu6cqe8cvmvM5cll547vNd1b+T36++jvB/fp7Tu673NeWN7NfO38kvyVAmbBzR90fij9gbM/Yv9goWHh8QPYAwkHxg5aHKwr4i/KKJo65HaorVimOK/47eEth2+U6JdUHiEeSTkyUepa2nFU4eiBoytlUWWj5dblzRUSFfsqlo6FHRs5bnm8qVKyMr/y44noE/eqHKraqpWqS05iT6aefHbK71T/j/Qf62vEavJrPtUm1E7UedX11RvV1zdINBQ2wo0pjXOng08PnbE909Gk0VTVLNScfxacTTn7/KeQn8ZaXFp6z9HPNZ1XPF/RSm3Na4PatrUttEe1T3QEdgx3Onf2dpl2tf6s+XPtBdkL5RcFLxZ2E7tzujmXMi4t9rB65i9HXp7q3dL78ErAlbt9nn2DV12uXr9mf+1Kv1X/petm1y/cMLnReZN+s/2W4a22AYOB1l8MfmkdNBxsu210u2PIeKhreP1w94jFyOU7tneu3XW6e2t0w+jwmO/YvfHg8Yl7Yfdm78fdf/Ug9cHyw92PMI/yHvM9Lnki8aT6V9VfmycMJy5O2k4OPPV++nCKOfXit6TfVqZznlGelcxIz9TP6s5emLOfG3q+8fn0C9aL5fnc3/l/r3ip8vL8H5Z/DCwELEy/Yr/ivC54I/qm9q3+295Fj8Un7+LfLS/lvRd9X/eB/qH/o//HmeW0FdxK6SfVT12fXT4/4sRzOCwGm7FqBVBIwhERALyuBYASCAB1CPEPG9f8119+BvrK2fyNwVndL5jhvubRVsMQgCakeCFp04OsQ1LJEgAe5NodqT6WANbT+yf/iqQIPd21PXgaAcDJcjivtwJAQHLFgcNZ9uBwPlUgYhHf1z37f7V9g9e8ITewiP88wfWIYET6HPg21nzjV2fybQVcxfrg2/onng/F50lD/ccAAAA4ZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAABigAwAEAAAAAQAAABgAAAAAwf1XlwAAAaNJREFUSA3FlT1LA0EQQBN/gYUYRTksJZVgEbCR/D+7QMr8ABtttBBCsLGzsLG2sxaxED/ie4d77u0dyaE5HHjczn7MzO7M7nU6/yXz+bwLhzCCjTQO+rZhDH3opuNLdRYN4RHe4RIKJ7R34Ro+4AEGSw2mE1iUwT18gpI74WvkGlccu4XNdH0jnYU7cAUacidn37qR23cOxc4aGU0nYUAn7iSWEHkz46w0ocdQu1X6B/AMQZ5o7KfBqNOfwRH8JB7FajGhnmcpKvQe3MEbvILiDm5gPXaCHnZr4vvFGMoEKudKn8YvQIOOe+YzCPop7dwJ3zRfJ7GDuso4YJGRa0yZgg4tUaNXdGrbuZWKKxzYYEJc2xp9AUUjGt8KC2jvgYadF8+10vJyDnNLXwbdiWUZi0fUK01Eoc+AZhCLZVzK4Vq6sDUdz+0dEcbbTTIOJmAyTVhx/WmvrExbv2jtPhWLKodjCtefZiEeZeVZWWSndgwj6fVf3XON8Qwq15++uoqrfYVrow6dGBpCq79ME291jaB0/Q2CPncyht/99MNO/vr9AqW/CGi8sJqbAAAAAElFTkSuQmCC") + ProfileImage(imageStr: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAKHGlDQ1BJQ0MgUHJvZmlsZQAASImFVgdUVNcWve9Nb7QZeu9NehtAem/Sq6gMQ28OQxWxgAQjEFFEREARNFQFg1KjiIhiIQgoYA9IEFBisCAq6OQNJNH4//r/zDpz9ttzz7n73ffWmg0A6QCDxYqD+QCIT0hmezlYywQEBsngngEYCAIy0AC6DGYSy8rDwxUg8Xf9d7wbAxC33tHgzvrP3/9nCISFJzEBgIIRTGey2MkILkawT1oyi4tnEUxjI6IQvMLFkauYqxjQQtewwuoaHy8bBNMBwJMZDHYkAERbhJdJZUYic4hhCNZOCItOQDB3vjkzioFwxLsIXhcRl5IOAImrRzs+fivCk7QRrIL0shAcwNUW+tX8yH/tFfrPXgxG5D84Pi6F+dc9ck+HHJ7g641UMSQlQATQBHEgBaQDGcACbLAVYaIRJhx5Dv+9j77aZ4OsZIFtSEc0iARRIBnpt/9qlvfqpGSQBhjImnCEcUU+NtxnujZy4fbqVEiU/wuXdQyA9S0cDqfzC+e2F4DzyLkSB79wyi0A8KoBcL2GmcJOXePQ3C8MIAJeQAOiQArIAxXuWwMMgSmwBHbAGbgDHxAINgMmojceUZUGMkEWyAX54AA4DMpAJTgJ6sAZ0ALawQVwGVwDt8AQGAUPwQSYBi/AAngHliEIwkEUiAqJQtKQIqQO6UJ0yByyg1whLygQCoEioQQoBcqE9kD5UBFUBlVB9dBPUCd0GboBDUP3oUloDnoNfYRRMBmmwZKwEqwF02Er2AX2gTfBkXAinAHnwPvhUrgaPg23wZfhW/AoPAG/gBdRAEVCCaFkURooOsoG5Y4KQkWg2KidqDxUCaoa1YTqQvWj7qAmUPOoD2gsmoqWQWugTdGOaF80E52I3okuQJeh69Bt6D70HfQkegH9GUPBSGDUMSYYJ0wAJhKThsnFlGBqMK2Yq5hRzDTmHRaLFcIqY42wjthAbAx2O7YAewzbjO3BDmOnsIs4HE4Up44zw7njGLhkXC7uKO407hJuBDeNe48n4aXxunh7fBA+AZ+NL8E34LvxI/gZ/DKBj6BIMCG4E8II2wiFhFOELsJtwjRhmchPVCaaEX2IMcQsYimxiXiV+Ij4hkQiyZGMSZ6kaNJuUinpLOk6aZL0gSxAViPbkIPJKeT95FpyD/k++Q2FQlGiWFKCKMmU/ZR6yhXKE8p7HiqPJo8TTxjPLp5ynjaeEZ6XvAReRV4r3s28GbwlvOd4b/PO8xH4lPhs+Bh8O/nK+Tr5xvkW+an8Ovzu/PH8BfwN/Df4ZwVwAkoCdgJhAjkCJwWuCExRUVR5qg2VSd1DPUW9Sp2mYWnKNCdaDC2fdoY2SFsQFBDUF/QTTBcsF7woOCGEElISchKKEyoUahEaE/ooLClsJRwuvE+4SXhEeElEXMRSJFwkT6RZZFTko6iMqJ1orOhB0XbRx2JoMTUxT7E0seNiV8XmxWnipuJM8TzxFvEHErCEmoSXxHaJkxIDEouSUpIOkizJo5JXJOelhKQspWKkiqW6peakqdLm0tHSxdKXpJ/LCMpYycTJlMr0ySzISsg6yqbIVskOyi7LKcv5ymXLNcs9lifK0+Uj5Ivle+UXFKQV3BQyFRoVHigSFOmKUYpHFPsVl5SUlfyV9iq1K80qiyg7KWcoNyo/UqGoWKgkqlSr3FXFqtJVY1WPqQ6pwWoGalFq5Wq31WF1Q/Vo9WPqw+sw64zXJayrXjeuQdaw0kjVaNSY1BTSdNXM1mzXfKmloBWkdVCrX+uztoF2nPYp7Yc6AjrOOtk6XTqvddV0mbrlunf1KHr2erv0OvRe6avrh+sf179nQDVwM9hr0GvwydDIkG3YZDhnpGAUYlRhNE6n0T3oBfTrxhhja+NdxheMP5gYmiSbtJj8YaphGmvaYDq7Xnl9+PpT66fM5MwYZlVmE+Yy5iHmJ8wnLGQtGBbVFk8t5S3DLGssZ6xUrWKsTlu9tNa2Zlu3Wi/ZmNjssOmxRdk62ObZDtoJ2Pnaldk9sZezj7RvtF9wMHDY7tDjiHF0cTzoOO4k6cR0qndacDZy3uHc50J28XYpc3nqqubKdu1yg92c3Q65PdqguCFhQ7s7cHdyP+T+2EPZI9HjZ0+sp4dnueczLx2vTK9+b6r3Fu8G73c+1j6FPg99VXxTfHv9eP2C/er9lvxt/Yv8JwK0AnYE3AoUC4wO7AjCBfkF1QQtbrTbeHjjdLBBcG7w2CblTembbmwW2xy3+eIW3i2MLedCMCH+IQ0hKwx3RjVjMdQptCJ0gWnDPMJ8EWYZVhw2F24WXhQ+E2EWURQxG2kWeShyLsoiqiRqPtomuiz6VYxjTGXMUqx7bG0sJ84/rjkeHx8S35kgkBCb0LdVamv61mGWOiuXNZFokng4cYHtwq5JgpI2JXUk05A/0oEUlZTvUiZTzVPLU9+n+aWdS+dPT0gf2Ka2bd+2mQz7jB+3o7czt/dmymZmZU7usNpRtRPaGbqzd5f8rpxd07sddtdlEbNis37J1s4uyn67x39PV45kzu6cqe8cvmvM5cll547vNd1b+T36++jvB/fp7Tu673NeWN7NfO38kvyVAmbBzR90fij9gbM/Yv9goWHh8QPYAwkHxg5aHKwr4i/KKJo65HaorVimOK/47eEth2+U6JdUHiEeSTkyUepa2nFU4eiBoytlUWWj5dblzRUSFfsqlo6FHRs5bnm8qVKyMr/y44noE/eqHKraqpWqS05iT6aefHbK71T/j/Qf62vEavJrPtUm1E7UedX11RvV1zdINBQ2wo0pjXOng08PnbE909Gk0VTVLNScfxacTTn7/KeQn8ZaXFp6z9HPNZ1XPF/RSm3Na4PatrUttEe1T3QEdgx3Onf2dpl2tf6s+XPtBdkL5RcFLxZ2E7tzujmXMi4t9rB65i9HXp7q3dL78ErAlbt9nn2DV12uXr9mf+1Kv1X/petm1y/cMLnReZN+s/2W4a22AYOB1l8MfmkdNBxsu210u2PIeKhreP1w94jFyOU7tneu3XW6e2t0w+jwmO/YvfHg8Yl7Yfdm78fdf/Ug9cHyw92PMI/yHvM9Lnki8aT6V9VfmycMJy5O2k4OPPV++nCKOfXit6TfVqZznlGelcxIz9TP6s5emLOfG3q+8fn0C9aL5fnc3/l/r3ip8vL8H5Z/DCwELEy/Yr/ivC54I/qm9q3+295Fj8Un7+LfLS/lvRd9X/eB/qH/o//HmeW0FdxK6SfVT12fXT4/4sRzOCwGm7FqBVBIwhERALyuBYASCAB1CPEPG9f8119+BvrK2fyNwVndL5jhvubRVsMQgCakeCFp04OsQ1LJEgAe5NodqT6WANbT+yf/iqQIPd21PXgaAcDJcjivtwJAQHLFgcNZ9uBwPlUgYhHf1z37f7V9g9e8ITewiP88wfWIYET6HPg21nzjV2fybQVcxfrg2/onng/F50lD/ccAAAA4ZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAABigAwAEAAAAAQAAABgAAAAAwf1XlwAAAaNJREFUSA3FlT1LA0EQQBN/gYUYRTksJZVgEbCR/D+7QMr8ABtttBBCsLGzsLG2sxaxED/ie4d77u0dyaE5HHjczn7MzO7M7nU6/yXz+bwLhzCCjTQO+rZhDH3opuNLdRYN4RHe4RIKJ7R34Ro+4AEGSw2mE1iUwT18gpI74WvkGlccu4XNdH0jnYU7cAUacidn37qR23cOxc4aGU0nYUAn7iSWEHkz46w0ocdQu1X6B/AMQZ5o7KfBqNOfwRH8JB7FajGhnmcpKvQe3MEbvILiDm5gPXaCHnZr4vvFGMoEKudKn8YvQIOOe+YzCPop7dwJ3zRfJ7GDuso4YJGRa0yZgg4tUaNXdGrbuZWKKxzYYEJc2xp9AUUjGt8KC2jvgYadF8+10vJyDnNLXwbdiWUZi0fUK01Eoc+AZhCLZVzK4Vq6sDUdz+0dEcbbTTIOJmAyTVhx/WmvrExbv2jtPhWLKodjCtefZiEeZeVZWWSndgwj6fVf3XON8Qwq15++uoqrfYVrow6dGBpCq79ME291jaB0/Q2CPncyht/99MNO/vr9AqW/CGi8sJqbAAAAAElFTkSuQmCC", size: 63) .previewLayout(.fixed(width: 63, height: 63)) .background(.black) } diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift index 936c6cb3ab..b8de0e4ceb 100644 --- a/apps/ios/Shared/Views/Helpers/ShareSheet.swift +++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift @@ -8,15 +8,63 @@ import SwiftUI -func showShareSheet(items: [Any], completed: (() -> Void)? = nil) { +func getTopViewController() -> UIViewController? { let keyWindowScene = UIApplication.shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene if let keyWindow = keyWindowScene?.windows.filter(\.isKeyWindow).first, - let presentedViewController = keyWindow.rootViewController?.presentedViewController ?? keyWindow.rootViewController { + let rootViewController = keyWindow.rootViewController { + // Find the top-most presented view controller + var topController = rootViewController + while let presentedViewController = topController.presentedViewController { + topController = presentedViewController + } + return topController + } + return nil +} + +func showShareSheet(items: [Any], completed: (() -> Void)? = nil) { + if let topController = getTopViewController() { let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil) if let completed = completed { - let handler: UIActivityViewController.CompletionWithItemsHandler = { _,_,_,_ in completed() } - activityViewController.completionWithItemsHandler = handler - } - presentedViewController.present(activityViewController, animated: true) + activityViewController.completionWithItemsHandler = { _, _, _, _ in + completed() + } + } + topController.present(activityViewController, animated: true) } } + +func showAlert( + title: String, + message: String? = nil, + buttonTitle: String, + buttonAction: @escaping () -> Void, + cancelButton: Bool +) -> Void { + if let topController = getTopViewController() { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: buttonTitle, style: .default) { _ in + buttonAction() + }) + if cancelButton { + alert.addAction(cancelAlertAction) + } + topController.present(alert, animated: true) + } +} + +func showAlert( + _ title: String, + message: String? = nil, + actions: () -> [UIAlertAction] = { [okAlertAction] } +) { + if let topController = getTopViewController() { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + for action in actions() { alert.addAction(action) } + topController.present(alert, animated: true) + } +} + +let okAlertAction = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default) + +let cancelAlertAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel) diff --git a/apps/ios/Shared/Views/Helpers/SheetRepresentable.swift b/apps/ios/Shared/Views/Helpers/SheetRepresentable.swift new file mode 100644 index 0000000000..841d5c7eda --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/SheetRepresentable.swift @@ -0,0 +1,188 @@ +// +// SwiftUISheet.swift +// SimpleX (iOS) +// +// Created by user on 23/09/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +private let sheetAnimationDuration: Double = 0.35 + +// Refrence: https://easings.net/ +private let easeOutCubic = UICubicTimingParameters( + controlPoint1: CGPoint(x: 0.215, y: 0.61), + controlPoint2: CGPoint(x: 0.355, y: 1) +) + +struct Sheet<SheetContent: View>: ViewModifier { + @Binding var isPresented: Bool + @ViewBuilder let sheetContent: () -> SheetContent + + func body(content: Content) -> some View { + ZStack { + content + SheetRepresentable(isPresented: $isPresented, content: sheetContent()) + .allowsHitTesting(isPresented) + .ignoresSafeArea() + } + } +} + +struct SheetRepresentable<Content: View>: UIViewControllerRepresentable { + @Binding var isPresented: Bool + let content: Content + + func makeUIViewController(context: Context) -> Controller<Content> { + Controller(content: content, representer: self) + } + + func updateUIViewController(_ sheetController: Controller<Content>, context: Context) { + sheetController.animate(isPresented: isPresented) + } + + class Controller<C: View>: UIViewController { + let hostingController: UIHostingController<C> + private let animator = UIViewPropertyAnimator( + duration: sheetAnimationDuration, + timingParameters: easeOutCubic + ) + private let representer: SheetRepresentable<C> + private var retainedFraction: CGFloat = 0 + private var sheetHeight: Double { hostingController.view.frame.height } + private var task: Task<Void, Never>? + + init(content: C, representer: SheetRepresentable<C>) { + self.representer = representer + self.hostingController = UIHostingController(rootView: content) + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError("init(coder:) missing") } + + deinit { + animator.stopAnimation(true) + animator.finishAnimation(at: .current) + } + + func animate(isPresented: Bool) { + let alreadyAnimating = animator.isRunning && isPresented != animator.isReversed + let sheetFullyDismissed = animator.fractionComplete == (animator.isReversed ? 1 : 0) + let sheetFullyPresented = animator.fractionComplete == (animator.isReversed ? 0 : 1) + + if !isPresented && sheetFullyDismissed || + isPresented && sheetFullyPresented || + alreadyAnimating { + return + } + + animator.pauseAnimation() + animator.isReversed = !isPresented + animator.continueAnimation( + withTimingParameters: isPresented + ? easeOutCubic + : UICubicTimingParameters(animationCurve: .easeIn), + durationFactor: 1 - animator.fractionComplete + ) + handleVisibility() + } + + func handleVisibility() { + if animator.isReversed { + task = Task { + do { + let sleepDuration = UInt64(sheetAnimationDuration * Double(NSEC_PER_SEC)) + try await Task.sleep(nanoseconds: sleepDuration) + view.isHidden = true + } catch { } + } + } else { + task?.cancel() + task = nil + view.isHidden = false + } + } + + override func viewDidLoad() { + view.isHidden = true + view.backgroundColor = .clear + view.addGestureRecognizer( + UITapGestureRecognizer(target: self, action: #selector(tap(gesture:))) + ) + addChild(hostingController) + hostingController.didMove(toParent: self) + if let sheet = hostingController.view { + sheet.isHidden = true + sheet.clipsToBounds = true + sheet.layer.cornerRadius = 10 + sheet.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner] + sheet.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(pan(gesture:)))) + sheet.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(sheet) + NSLayoutConstraint.activate([ + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + } + + override func viewDidAppear(_ animated: Bool) { + // Ensures animations are only setup once + // on some iOS version `viewDidAppear` can get called on each state change. + if hostingController.view.isHidden { + hostingController.view.transform = CGAffineTransform(translationX: 0, y: self.sheetHeight) + hostingController.view.isHidden = false + animator.pausesOnCompletion = true + animator.addAnimations { + self.hostingController.view.transform = .identity + self.view.backgroundColor = UIColor { + switch $0.userInterfaceStyle { + case .dark: .black.withAlphaComponent(0.290) + default: .black.withAlphaComponent(0.121) + } + } + } + animator.startAnimation() + animator.pauseAnimation() + } + } + + @objc + func pan(gesture: UIPanGestureRecognizer) { + switch gesture.state { + case .began: + animator.isReversed = false + animator.pauseAnimation() + retainedFraction = animator.fractionComplete + case .changed: + animator.fractionComplete = retainedFraction - gesture.translation(in: view).y / sheetHeight + case .ended, .cancelled: + let velocity = gesture.velocity(in: view).y + animator.isReversed = (velocity - (animator.fractionComplete - 0.5) * 100).sign == .plus + let defaultVelocity = sheetHeight / sheetAnimationDuration + let fractionRemaining = 1 - animator.fractionComplete + let durationFactor = min(max(fractionRemaining / (abs(velocity) / defaultVelocity), 0.5), 1) + animator.continueAnimation(withTimingParameters: nil, durationFactor: durationFactor * fractionRemaining) + handleVisibility() + DispatchQueue.main.asyncAfter(deadline: .now() + sheetAnimationDuration) { + self.representer.isPresented = !self.animator.isReversed + } + default: break + } + } + + @objc + func tap(gesture: UITapGestureRecognizer) { + switch gesture.state { + case .ended: + if gesture.location(in: view).y < view.frame.height - sheetHeight { + representer.isPresented = false + } + default: break + } + } + } +} diff --git a/apps/ios/Shared/Views/Helpers/StickyScrollView.swift b/apps/ios/Shared/Views/Helpers/StickyScrollView.swift new file mode 100644 index 0000000000..5799962778 --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/StickyScrollView.swift @@ -0,0 +1,61 @@ +// +// StickyScrollView.swift +// SimpleX (iOS) +// +// Created by user on 20/09/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct StickyScrollView<Content: View>: UIViewRepresentable { + @Binding var resetScroll: ResetScrollAction + @ViewBuilder let content: () -> Content + + func makeUIView(context: Context) -> UIScrollView { + let hc = context.coordinator.hostingController + hc.view.backgroundColor = .clear + let sv = UIScrollView() + sv.showsHorizontalScrollIndicator = false + sv.addSubview(hc.view) + sv.delegate = context.coordinator + DispatchQueue.main.async { + resetScroll = ResetScrollAction { sv.setContentOffset(.zero, animated: false) } + } + return sv + } + + func updateUIView(_ scrollView: UIScrollView, context: Context) { + let hc = context.coordinator.hostingController + hc.rootView = content() + hc.view.frame.size = hc.view.intrinsicContentSize + scrollView.contentSize = hc.view.intrinsicContentSize + } + + func makeCoordinator() -> Coordinator { + Coordinator(content: content()) + } + + class Coordinator: NSObject, UIScrollViewDelegate { + let hostingController: UIHostingController<Content> + + init(content: Content) { + self.hostingController = UIHostingController(rootView: content) + } + + func scrollViewWillEndDragging( + _ scrollView: UIScrollView, + withVelocity velocity: CGPoint, + targetContentOffset: UnsafeMutablePointer<CGPoint> + ) { + if targetContentOffset.pointee.x < 32 { + targetContentOffset.pointee.x = 0 + } + } + } +} + +struct ResetScrollAction { + var action = { } + func callAsFunction() { action() } +} diff --git a/apps/ios/Shared/Views/Helpers/SwipeLabel.swift b/apps/ios/Shared/Views/Helpers/SwipeLabel.swift new file mode 100644 index 0000000000..564589be6f --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/SwipeLabel.swift @@ -0,0 +1,80 @@ +// +// SwipeLabel.swift +// SimpleX (iOS) +// +// Created by Levitating Pineapple on 06/08/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct SwipeLabel: View { + private let text: String + private let systemImage: String + private let inverted: Bool + + init(_ text: String, systemImage: String, inverted: Bool) { + self.text = text + self.systemImage = systemImage + self.inverted = inverted + } + + var body: some View { + if inverted { + Image( + uiImage: SwipeActionView( + systemName: systemImage, + text: text + ).snapshot(inverted: inverted) + ) + } else { + Label(text, systemImage: systemImage) + } + } + + private class SwipeActionView: UIView { + private let imageView = UIImageView() + private let label = UILabel() + private let fontSize: CGFloat + + init(systemName: String, text: String) { + fontSize = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline).pointSize + super.init(frame: CGRect(x: 0, y: 0, width: 64, height: 32 + fontSize)) + imageView.image = UIImage(systemName: systemName) + imageView.contentMode = .scaleAspectFit + label.text = text + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: fontSize, weight: .medium) + addSubview(imageView) + addSubview(label) + } + + override func layoutSubviews() { + imageView.frame = CGRect( + x: 20, + y: 0, + width: 24, + height: 24 + ) + label.frame = CGRect( + x: 0, + y: 32, + width: 64, + height: fontSize + ) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError("not implemented") } + + func snapshot(inverted: Bool) -> UIImage { + UIGraphicsImageRenderer(bounds: bounds).image { context in + if inverted { + context.cgContext.scaleBy(x: 1, y: -1) + context.cgContext.translateBy(x: 0, y: -bounds.height) + } + layer.render(in: context.cgContext) + }.withRenderingMode(.alwaysTemplate) + } + } +} diff --git a/apps/ios/Shared/Views/Helpers/ThemeModeEditor.swift b/apps/ios/Shared/Views/Helpers/ThemeModeEditor.swift new file mode 100644 index 0000000000..9d5ae2e289 --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/ThemeModeEditor.swift @@ -0,0 +1,453 @@ +// +// ThemeModeEditor.swift +// SimpleX (iOS) +// +// Created by Avently on 20.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI +import SimpleXChat + +struct UserWallpaperEditor: View { + @EnvironmentObject var theme: AppTheme + var initialTheme: ThemeModeOverride + @State var themeModeOverride: ThemeModeOverride + @State var applyToMode: DefaultThemeMode? + @State var showMore: Bool = false + @State var showFileImporter: Bool = false + @Binding var globalThemeUsed: Bool + var save: (DefaultThemeMode?, ThemeModeOverride?) async -> Void + + @State private var showImageImporter: Bool = false + + var body: some View { + List { + let wallpaperType = theme.wallpaper.type + + WallpaperPresetSelector( + selectedWallpaper: wallpaperType, + currentColors: { type in + // If applying for : + // - all themes: no overrides needed + // - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected + let perUserOverride: ThemeModeOverrides? = wallpaperType.sameType(type) ? ChatModel.shared.currentUser?.uiThemes : nil + return ThemeManager.currentColors(type, nil, perUserOverride, themeOverridesDefault.get()) + }, + onChooseType: onChooseType + ) + .padding(.bottom, 10) + .listRowInsets(.init()) + .listRowBackground(Color.clear) + .modifier(WallpaperImporter(showImageImporter: $showImageImporter, onChooseImage: { image in + if let filename = saveWallpaperFile(image: image) { + _ = onTypeCopyFromSameTheme(WallpaperType.image(filename, 1, WallpaperScaleType.fill)) + } + })) + + WallpaperSetupView( + wallpaperType: themeModeOverride.type, + base: theme.base, + initialWallpaper: theme.wallpaper, + editColor: { name in editColor(name, theme) }, + onTypeChange: onTypeChange + ) + + Section { + if !globalThemeUsed { + ResetToGlobalThemeButton(true, theme.colors.primary) { + themeModeOverride = ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + globalThemeUsed = true + Task { + await save(applyToMode, nil) + await MainActor.run { + // Change accent color globally + ThemeManager.applyTheme(currentThemeDefault.get()) + } + } + } + } + + SetDefaultThemeButton(theme.colors.primary) { + globalThemeUsed = false + let lightBase = DefaultTheme.LIGHT + let darkBase = if theme.base != DefaultTheme.LIGHT { theme.base } else if systemDarkThemeDefault.get() == DefaultTheme.DARK.themeName { DefaultTheme.DARK } else if systemDarkThemeDefault.get() == DefaultTheme.BLACK.themeName { DefaultTheme.BLACK } else { DefaultTheme.SIMPLEX } + let mode = themeModeOverride.mode + Task { + // Saving for both modes in one place by changing mode once per save + if applyToMode == nil { + let oppositeMode = mode == DefaultThemeMode.light ? DefaultThemeMode.dark : DefaultThemeMode.light + await save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, oppositeMode == DefaultThemeMode.light ? lightBase : darkBase)) + } + await MainActor.run { + themeModeOverride = ThemeModeOverride.withFilledAppDefaults(mode, mode == DefaultThemeMode.light ? lightBase : darkBase) + } + await save(themeModeOverride.mode, themeModeOverride) + await MainActor.run { + // Change accent color globally + ThemeManager.applyTheme(currentThemeDefault.get()) + } + } + }.onChange(of: initialTheme.mode) { mode in + themeModeOverride = initialTheme + if applyToMode != nil { + applyToMode = mode + } + } + .onChange(of: theme) { _ in + // Applies updated global theme if current one tracks global theme + if globalThemeUsed { + themeModeOverride = ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + globalThemeUsed = true + } + } + } + + if showMore { + let values = [ + (nil, "All modes"), + (DefaultThemeMode.light, "Light mode"), + (DefaultThemeMode.dark, "Dark mode") + ] + Picker("Apply to", selection: $applyToMode) { + ForEach(values, id: \.0) { (_, text) in + Text(text) + } + } + .frame(height: 36) + .onChange(of: applyToMode) { mode in + if let mode, mode != theme.base.mode { + let lightBase = DefaultTheme.LIGHT + let darkBase = if theme.base != DefaultTheme.LIGHT { theme.base } else if systemDarkThemeDefault.get() == DefaultTheme.DARK.themeName { DefaultTheme.DARK } else if systemDarkThemeDefault.get() == DefaultTheme.BLACK.themeName { DefaultTheme.BLACK } else { DefaultTheme.SIMPLEX } + ThemeManager.applyTheme(mode == DefaultThemeMode.light ? lightBase.themeName : darkBase.themeName) + } + } + + CustomizeThemeColorsSection(editColor: { name in editColor(name, theme) }) + + ImportExportThemeSection(showFileImporter: $showFileImporter, perChat: nil, perUser: ChatModel.shared.currentUser?.uiThemes) + } else { + AdvancedSettingsButton(theme.colors.primary) { showMore = true } + } + } + .modifier( + ThemeImporter(isPresented: $showFileImporter) { imported in + let importedFromString = imported.wallpaper?.importFromString() + let importedType = importedFromString?.toAppWallpaper().type + let currentTheme = ThemeManager.currentColors(nil, nil, nil, themeOverridesDefault.get()) + let type: WallpaperType? = if importedType?.sameType(currentTheme.wallpaper.type) == true { nil } else { importedType } + let colors = ThemeManager.currentThemeOverridesForExport(type, nil, nil).colors + let res = ThemeModeOverride(mode: imported.base.mode, colors: imported.colors, wallpaper: importedFromString).removeSameColors(imported.base, colorsToCompare: colors) + Task { + await MainActor.run { + themeModeOverride = res + } + await save(applyToMode, res) + } + } + ) + } + + private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool { + _ = ThemeManager.copyFromSameThemeOverrides(type, nil, $themeModeOverride) + Task { + await save(applyToMode, themeModeOverride) + } + globalThemeUsed = false + return true + } + + private func preApplyGlobalIfNeeded(_ type: WallpaperType?) { + if globalThemeUsed { + _ = onTypeCopyFromSameTheme(type) + } + } + + private func onTypeChange(_ type: WallpaperType?) { + if globalThemeUsed { + preApplyGlobalIfNeeded(type) + // Saves copied static image instead of original from global theme + ThemeManager.applyWallpaper(themeModeOverride.type, $themeModeOverride) + } else { + ThemeManager.applyWallpaper(type, $themeModeOverride) + } + Task { + await save(applyToMode, themeModeOverride) + } + } + + private func currentColors(_ type: WallpaperType?) -> ThemeManager.ActiveTheme { + // If applying for : + // - all themes: no overrides needed + // - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected + let perUserOverride: ThemeModeOverrides? = theme.wallpaper.type.sameType(type) ? ChatModel.shared.currentUser?.uiThemes : nil + return ThemeManager.currentColors(type, nil, perUserOverride, themeOverridesDefault.get()) + } + + private func onChooseType(_ type: WallpaperType?) { + if let type, case WallpaperType.image = type { + if theme.wallpaper.type.isImage || currentColors(type).wallpaper.type.image == nil { + showImageImporter = true + } else { + _ = onTypeCopyFromSameTheme(currentColors(type).wallpaper.type) + } + } else if themeModeOverride.type != type || theme.wallpaper.type != type { + _ = onTypeCopyFromSameTheme(type) + } else { + onTypeChange(type) + } + } + + private func editColor(_ name: ThemeColor, _ currentTheme: AppTheme) -> Binding<Color> { + editColorBinding( + name: name, + wallpaperType: theme.wallpaper.type, + wallpaperImage: theme.wallpaper.type.image, + theme: currentTheme, + onColorChange: { color in + preApplyGlobalIfNeeded(themeModeOverride.type) + ThemeManager.applyThemeColor(name: name, color: color, pref: $themeModeOverride) + Task { await save(applyToMode, themeModeOverride) } + }) + } +} + +struct ChatWallpaperEditor: View { + @EnvironmentObject var theme: AppTheme + @State private var currentTheme: ThemeManager.ActiveTheme + var initialTheme: ThemeModeOverride + @State var themeModeOverride: ThemeModeOverride + @State var applyToMode: DefaultThemeMode? + @State var showMore: Bool = false + @State var showFileImporter: Bool = false + @Binding var globalThemeUsed: Bool + var save: (DefaultThemeMode?, ThemeModeOverride?) async -> Void + + @State private var showImageImporter: Bool = false + + init(initialTheme: ThemeModeOverride, themeModeOverride: ThemeModeOverride, applyToMode: DefaultThemeMode? = nil, globalThemeUsed: Binding<Bool>, save: @escaping (DefaultThemeMode?, ThemeModeOverride?) async -> Void) { + let cur = ThemeManager.currentColors(nil, globalThemeUsed.wrappedValue ? nil : themeModeOverride, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + self.currentTheme = cur + self.initialTheme = initialTheme + self.themeModeOverride = themeModeOverride + self.applyToMode = applyToMode + self._globalThemeUsed = globalThemeUsed + self.save = save + } + + var body: some View { + List { + WallpaperPresetSelector( + selectedWallpaper: currentTheme.wallpaper.type, + activeBackgroundColor: currentTheme.wallpaper.background, + activeTintColor: currentTheme.wallpaper.tint, + currentColors: currentColors, + onChooseType: onChooseType + ) + .padding(.bottom, 10) + .listRowInsets(.init()) + .listRowBackground(Color.clear) + .modifier(WallpaperImporter(showImageImporter: $showImageImporter, onChooseImage: { image in + if let filename = saveWallpaperFile(image: image) { + _ = onTypeCopyFromSameTheme(WallpaperType.image(filename, 1, WallpaperScaleType.fill)) + } + })) + + WallpaperSetupView( + wallpaperType: themeModeOverride.type, + base: currentTheme.base, + initialWallpaper: currentTheme.wallpaper, + editColor: editColor, + onTypeChange: onTypeChange + ) + + Section { + if !globalThemeUsed { + ResetToGlobalThemeButton(ChatModel.shared.currentUser?.uiThemes?.preferredMode(isInDarkTheme()) == nil, theme.colors.primary) { + themeModeOverride = ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + globalThemeUsed = true + Task { + await save(applyToMode, nil) + } + } + } + + SetDefaultThemeButton(theme.colors.primary) { + globalThemeUsed = false + let lightBase = DefaultTheme.LIGHT + let darkBase = if currentTheme.base != DefaultTheme.LIGHT { currentTheme.base } else if systemDarkThemeDefault.get() == DefaultTheme.DARK.themeName { DefaultTheme.DARK } else if systemDarkThemeDefault.get() == DefaultTheme.BLACK.themeName { DefaultTheme.BLACK } else { DefaultTheme.SIMPLEX } + let mode = themeModeOverride.mode + Task { + // Saving for both modes in one place by changing mode once per save + if applyToMode == nil { + let oppositeMode = mode == DefaultThemeMode.light ? DefaultThemeMode.dark : DefaultThemeMode.light + await save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, oppositeMode == DefaultThemeMode.light ? lightBase : darkBase)) + } + await MainActor.run { + themeModeOverride = ThemeModeOverride.withFilledAppDefaults(mode, mode == DefaultThemeMode.light ? lightBase : darkBase) + } + await save(themeModeOverride.mode, themeModeOverride) + } + } + .onChange(of: initialTheme) { initial in + if initial.mode != themeModeOverride.mode { + themeModeOverride = initial + currentTheme = ThemeManager.currentColors(nil, globalThemeUsed ? nil : themeModeOverride, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + if applyToMode != nil { + applyToMode = initial.mode + } + } + } + .onChange(of: currentTheme) { _ in + // Applies updated global theme if current one tracks global theme + if globalThemeUsed { + themeModeOverride = ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + globalThemeUsed = true + } + } + .onChange(of: themeModeOverride) { override in + currentTheme = ThemeManager.currentColors(nil, globalThemeUsed ? nil : override, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + } + } + + if showMore { + let values = [ + (nil, "All modes"), + (DefaultThemeMode.light, "Light mode"), + (DefaultThemeMode.dark, "Dark mode") + ] + Picker("Apply to", selection: $applyToMode) { + ForEach(values, id: \.0) { (_, text) in + Text(text) + } + } + .frame(height: 36) + .onChange(of: applyToMode) { mode in + if let mode, mode != currentTheme.base.mode { + let lightBase = DefaultTheme.LIGHT + let darkBase = if currentTheme.base != DefaultTheme.LIGHT { currentTheme.base } else if systemDarkThemeDefault.get() == DefaultTheme.DARK.themeName { DefaultTheme.DARK } else if systemDarkThemeDefault.get() == DefaultTheme.BLACK.themeName { DefaultTheme.BLACK } else { DefaultTheme.SIMPLEX } + ThemeManager.applyTheme(mode == DefaultThemeMode.light ? lightBase.themeName : darkBase.themeName) + } + } + + CustomizeThemeColorsSection(editColor: editColor) + + ImportExportThemeSection(showFileImporter: $showFileImporter, perChat: themeModeOverride, perUser: ChatModel.shared.currentUser?.uiThemes) + } else { + AdvancedSettingsButton(theme.colors.primary) { showMore = true } + } + } + .modifier( + ThemeImporter(isPresented: $showFileImporter) { imported in + let importedFromString = imported.wallpaper?.importFromString() + let importedType = importedFromString?.toAppWallpaper().type + let currentTheme = ThemeManager.currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + let type: WallpaperType? = if importedType?.sameType(currentTheme.wallpaper.type) == true { nil } else { importedType } + let colors = ThemeManager.currentThemeOverridesForExport(type, nil, ChatModel.shared.currentUser?.uiThemes).colors + let res = ThemeModeOverride(mode: imported.base.mode, colors: imported.colors, wallpaper: importedFromString).removeSameColors(imported.base, colorsToCompare: colors) + Task { + await MainActor.run { + themeModeOverride = res + } + await save(applyToMode, res) + } + } + ) + } + + private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool { + let success = ThemeManager.copyFromSameThemeOverrides(type, ChatModel.shared.currentUser?.uiThemes?.preferredMode(!currentTheme.colors.isLight), $themeModeOverride) + if success { + Task { + await save(applyToMode, themeModeOverride) + } + globalThemeUsed = false + } + return success + } + + private func preApplyGlobalIfNeeded(_ type: WallpaperType?) { + if globalThemeUsed { + _ = onTypeCopyFromSameTheme(type) + } + } + + private func onTypeChange(_ type: WallpaperType?) { + if globalThemeUsed { + preApplyGlobalIfNeeded(type) + // Saves copied static image instead of original from global theme + ThemeManager.applyWallpaper(themeModeOverride.type, $themeModeOverride) + } else { + ThemeManager.applyWallpaper(type, $themeModeOverride) + } + Task { + await save(applyToMode, themeModeOverride) + } + } + + private func currentColors(_ type: WallpaperType?) -> ThemeManager.ActiveTheme { + // If applying for : + // - all themes: no overrides needed + // - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected + let perChatOverride: ThemeModeOverride? = type?.sameType(themeModeOverride.type) == true ? themeModeOverride : nil + return ThemeManager.currentColors(type, perChatOverride, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + } + + private func onChooseType(_ type: WallpaperType?) { + if let type, case WallpaperType.image = type { + if (themeModeOverride.type?.isImage == true && !globalThemeUsed) || currentColors(type).wallpaper.type.image == nil { + showImageImporter = true + } else if !onTypeCopyFromSameTheme(currentColors(type).wallpaper.type) { + showImageImporter = true + } + } else if globalThemeUsed || themeModeOverride.type != type || themeModeOverride.type != type { + _ = onTypeCopyFromSameTheme(type) + } else { + onTypeChange(type) + } + } + + private func editColor(_ name: ThemeColor) -> Binding<Color> { + editColorBinding( + name: name, + wallpaperType: themeModeOverride.type, + wallpaperImage: themeModeOverride.type?.image, + theme: currentTheme.toAppTheme(), + onColorChange: { color in + preApplyGlobalIfNeeded(themeModeOverride.type) + ThemeManager.applyThemeColor(name: name, color: color, pref: $themeModeOverride) + currentTheme = ThemeManager.currentColors(nil, globalThemeUsed ? nil : themeModeOverride, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + Task { await save(applyToMode, themeModeOverride) } + }) + } +} + +private func ResetToGlobalThemeButton(_ app: Bool, _ primaryColor: Color, _ onClick: @escaping () -> Void) -> some View { + Button { + onClick() + } label: { + Text(app ? "Reset to app theme" : "Reset to user theme") + .foregroundColor(primaryColor) + } +} + +private func SetDefaultThemeButton(_ primaryColor: Color, _ onClick: @escaping () -> Void) -> some View { + Button { + onClick() + } label: { + Text("Set default theme") + .foregroundColor(primaryColor) + } +} + +private func AdvancedSettingsButton(_ primaryColor: Color, _ onClick: @escaping () -> Void) -> some View { + Button { + onClick() + } label: { + HStack { + Image(systemName: "chevron.down") + Text("Advanced settings") + }.foregroundColor(primaryColor) + } +} diff --git a/apps/ios/Shared/Views/Helpers/UserDefault.swift b/apps/ios/Shared/Views/Helpers/UserDefault.swift new file mode 100644 index 0000000000..5f18465d20 --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/UserDefault.swift @@ -0,0 +1,62 @@ +// +// UserDefault.swift +// SimpleX (iOS) +// +// Created by user on 14/10/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import Combine + +@propertyWrapper +public struct UserDefault<Value: Equatable>: DynamicProperty { + @StateObject private var observer = UserDefaultObserver() + let initialValue: Value + let key: String + let store: UserDefaults + + public init( + wrappedValue: Value, + _ key: String, + store: UserDefaults = .standard + ) { + self.initialValue = wrappedValue + self.key = key + self.store = store + } + + public var wrappedValue: Value { + get { + // Observer can only be accessed after the property wrapper is installed in view (runtime exception) + observer.subscribe(to: key) + return store.object(forKey: key) as? Value ?? initialValue + } + nonmutating set { + store.set(newValue, forKey: key) + } + } +} + +private class UserDefaultObserver: ObservableObject { + private var subscribed = false + + func subscribe(to key: String) { + if !subscribed { + NotificationCenter.default.addObserver( + self, + selector: #selector(userDefaultsDidChange), + name: UserDefaults.didChangeNotification, + object: nil + ) + subscribed = true + } + } + + @objc + private func userDefaultsDidChange(_ notification: Notification) { + Task { @MainActor in objectWillChange.send() } + } + + deinit { NotificationCenter.default.removeObserver(self) } +} diff --git a/apps/ios/Shared/Views/Helpers/VideoUtils.swift b/apps/ios/Shared/Views/Helpers/VideoUtils.swift deleted file mode 100644 index e13893de6e..0000000000 --- a/apps/ios/Shared/Views/Helpers/VideoUtils.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// VideoUtils.swift -// SimpleX (iOS) -// -// Created by Avently on 25.12.2023. -// Copyright © 2023 SimpleX Chat. All rights reserved. -// - -import AVFoundation -import Foundation -import SimpleXChat - -func makeVideoQualityLower(_ input: URL, outputUrl: URL) async -> Bool { - let asset: AVURLAsset = AVURLAsset(url: input, options: nil) - if let s = AVAssetExportSession(asset: asset, presetName: AVAssetExportPreset640x480) { - s.outputURL = outputUrl - s.outputFileType = .mp4 - s.metadataItemFilter = AVMetadataItemFilter.forSharing() - await s.export() - if let err = s.error { - logger.error("Failed to export video with error: \(err)") - } - return s.status == .completed - } - return false -} diff --git a/apps/ios/Shared/Views/Helpers/ViewModifiers.swift b/apps/ios/Shared/Views/Helpers/ViewModifiers.swift new file mode 100644 index 0000000000..85ef85c611 --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/ViewModifiers.swift @@ -0,0 +1,54 @@ +// +// ViewModifiers.swift +// SimpleX (iOS) +// +// Created by Avently on 12.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +extension View { + @inline(__always) + @ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View { + if condition { + transform(self) + } else { + self + } + } +} + +extension Notification.Name { + static let chatViewWillBeginScrolling = Notification.Name("chatWillBeginScrolling") +} + +struct PrivacyBlur: ViewModifier { + var enabled: Bool = true + @Binding var blurred: Bool + @AppStorage(DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) private var blurRadius: Int = 0 + + func body(content: Content) -> some View { + if blurRadius > 0 { + // parallel ifs are necessary here because otherwise some views flicker, + // e.g. when playing video + content + .blur(radius: blurred && enabled ? CGFloat(blurRadius) * 0.5 : 0) + .overlay { + if (blurred && enabled) { + Color.clear.contentShape(Rectangle()) + .simultaneousGesture(TapGesture().onEnded { + blurred = false + }) + } + } + .onReceive(NotificationCenter.default.publisher(for: .chatViewWillBeginScrolling)) { _ in + if !blurred { + blurred = true + } + } + } else { + content + } + } +} diff --git a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift index bdb5b03e8c..16ab26eff7 100644 --- a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift +++ b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift @@ -13,19 +13,28 @@ struct LocalAuthView: View { @EnvironmentObject var m: ChatModel var authRequest: LocalAuthRequest @State private var password = "" + @State private var allowToReact = true var body: some View { - PasscodeView(passcode: $password, title: authRequest.title ?? "Enter Passcode", reason: authRequest.reason, submitLabel: "Submit") { + PasscodeView(passcode: $password, title: authRequest.title ?? "Enter Passcode", reason: authRequest.reason, submitLabel: "Submit", + buttonsEnabled: $allowToReact) { if let sdPassword = kcSelfDestructPassword.get(), authRequest.selfDestruct && password == sdPassword { + allowToReact = false deleteStorageAndRestart(sdPassword) { r in m.laRequest = nil authRequest.completed(r) } return } - let r: LAResult = password == authRequest.password - ? .success - : .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry")) + let r: LAResult + if password == authRequest.password { + if authRequest.selfDestruct && kcSelfDestructPassword.get() != nil && !m.chatInitialized { + initChatAndMigrate() + } + r = .success + } else { + r = .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry")) + } m.laRequest = nil authRequest.completed(r) } cancel: { @@ -37,8 +46,28 @@ struct LocalAuthView: View { private func deleteStorageAndRestart(_ password: String, completed: @escaping (LAResult) -> Void) { Task { do { - try await stopChatAsync() - try await deleteChatAsync() + /** Waiting until [initializeChat] finishes */ + while (m.ctrlInitInProgress) { + try await Task.sleep(nanoseconds: 50_000000) + } + if m.chatRunning == true { + try await stopChatAsync() + } + if m.chatInitialized { + /** + * The following sequence can bring a user here: + * the user opened the app, entered app passcode, went to background, returned back, entered self-destruct code. + * In this case database should be closed to prevent possible situation when OS can deny database removal command + * */ + chatCloseStore() + } + deleteAppDatabaseAndFiles() + // Clear sensitive data on screen just in case app fails to hide its views while new database is created + m.chatId = nil + ItemsModel.shared.reversedChatItems = [] + ItemsModel.shared.chatState.clear() + m.updateChats([]) + m.users = [] _ = kcAppPassword.set(password) _ = kcSelfDestructPassword.remove() await NtfManager.shared.removeAllNotifications() @@ -53,7 +82,7 @@ struct LocalAuthView: View { try initializeChat(start: true) m.chatDbChanged = false AppChatState.shared.set(.active) - if m.currentUser != nil { return } + if m.currentUser != nil || !m.chatInitialized { return } var profile: Profile? = nil if let displayName = displayName, displayName != "" { profile = Profile(displayName: displayName, fullName: "") diff --git a/apps/ios/Shared/Views/LocalAuth/PasscodeEntry.swift b/apps/ios/Shared/Views/LocalAuth/PasscodeEntry.swift index 46ce66678a..4a6f8e7549 100644 --- a/apps/ios/Shared/Views/LocalAuth/PasscodeEntry.swift +++ b/apps/ios/Shared/Views/LocalAuth/PasscodeEntry.swift @@ -10,6 +10,7 @@ import SwiftUI struct PasscodeEntry: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme var width: CGFloat var height: CGFloat @Binding var password: String @@ -27,7 +28,7 @@ struct PasscodeEntry: View { } } - @ViewBuilder private func passwordView() -> some View { + private func passwordView() -> some View { Text( password == "" ? " " @@ -140,11 +141,11 @@ struct PasscodeEntry: View { ZStack { Circle() .frame(width: h, height: h) - .foregroundColor(Color(uiColor: .systemBackground)) + .foregroundColor(AppTheme.shared.colors.background) label() } } - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .frame(width: size, height: h) } } diff --git a/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift b/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift index c73ded2d28..ca30fa5ce8 100644 --- a/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift +++ b/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift @@ -14,6 +14,8 @@ struct PasscodeView: View { var reason: String? = nil var submitLabel: LocalizedStringKey var submitEnabled: ((String) -> Bool)? + @Binding var buttonsEnabled: Bool + var submit: () -> Void var cancel: () -> Void @@ -27,7 +29,7 @@ struct PasscodeView: View { } .padding(.horizontal, 40) .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(uiColor: .systemBackground)) + .background(AppTheme.shared.colors.background) } private func verticalPasscodeView(_ g: GeometryProxy) -> some View { @@ -70,11 +72,11 @@ struct PasscodeView: View { @ViewBuilder private func buttonsView() -> some View { Button(action: cancel) { Label("Cancel", systemImage: "multiply") - } + }.disabled(!buttonsEnabled) Button(action: submit) { Label(submitLabel, systemImage: "checkmark") } - .disabled(submitEnabled?(passcode) == false || passcode.count < 4) + .disabled(submitEnabled?(passcode) == false || passcode.count < 4 || !buttonsEnabled) } } @@ -85,6 +87,7 @@ struct PasscodeViewView_Previews: PreviewProvider { title: "Enter Passcode", reason: "Unlock app", submitLabel: "Submit", + buttonsEnabled: Binding.constant(true), submit: {}, cancel: {} ) diff --git a/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift b/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift index 76cd3e279a..7ec3ee1a42 100644 --- a/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift +++ b/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct SetAppPasscodeView: View { var passcodeKeychain: KeyChainItem = kcAppPassword + var prohibitedPasscodeKeychain: KeyChainItem = kcSelfDestructPassword var title: LocalizedStringKey = "New Passcode" var reason: String? var submit: () -> Void @@ -41,7 +42,10 @@ struct SetAppPasscodeView: View { } } } else { - setPasswordView(title: title, submitLabel: "Save") { + setPasswordView(title: title, + submitLabel: "Save", + // Do not allow to set app passcode == selfDestruct passcode + submitEnabled: { pwd in pwd != prohibitedPasscodeKeychain.get() }) { enteredPassword = passcode passcode = "" confirming = true @@ -54,7 +58,7 @@ struct SetAppPasscodeView: View { } private func setPasswordView(title: LocalizedStringKey, submitLabel: LocalizedStringKey, submitEnabled: (((String) -> Bool))? = nil, submit: @escaping () -> Void) -> some View { - PasscodeView(passcode: $passcode, title: title, reason: reason, submitLabel: submitLabel, submitEnabled: submitEnabled, submit: submit) { + PasscodeView(passcode: $passcode, title: title, reason: reason, submitLabel: submitLabel, submitEnabled: submitEnabled, buttonsEnabled: Binding.constant(true), submit: submit) { dismiss() cancel() } diff --git a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift new file mode 100644 index 0000000000..0af8fa7ad8 --- /dev/null +++ b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift @@ -0,0 +1,783 @@ +// +// MigrateFromDevice.swift +// SimpleX (iOS) +// +// Created by Avently on 14.02.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +private enum MigrationFromState: Equatable { + case chatStopInProgress + case chatStopFailed(reason: String) + case passphraseNotSet + case passphraseConfirmation + case uploadConfirmation + case archiving + case uploadProgress(uploadedBytes: Int64, totalBytes: Int64, fileId: Int64, archivePath: URL, ctrl: chat_ctrl?) + case uploadFailed(totalBytes: Int64, archivePath: URL) + case linkCreation + case linkShown(fileId: Int64, link: String, archivePath: URL, ctrl: chat_ctrl) + case finished(chatDeletion: Bool) +} + +private enum MigrateFromDeviceViewAlert: Identifiable { + case finishMigration(_ fileId: Int64, _ ctrl: chat_ctrl) + case deleteChat(_ title: LocalizedStringKey = "Delete chat profile?", _ text: LocalizedStringKey = "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.") + case startChat(_ title: LocalizedStringKey = "Start chat?", _ text: LocalizedStringKey = "Warning: starting chat on multiple devices is not supported and will cause message delivery failures") + + case wrongPassphrase(title: LocalizedStringKey = "Wrong passphrase!", message: LocalizedStringKey = "Enter correct passphrase.") + case invalidConfirmation(title: LocalizedStringKey = "Invalid migration confirmation") + case keychainError(_ title: LocalizedStringKey = "Keychain error") + case databaseError(_ title: LocalizedStringKey = "Database error", message: String) + case unknownError(_ title: LocalizedStringKey = "Unknown error", message: String) + case archiveExportedWithErrors(archivePath: URL, archiveErrors: [ArchiveError]) + + case error(title: LocalizedStringKey, error: String = "") + + var id: String { + switch self { + case .finishMigration: return "finishMigration" + case let .deleteChat(title, text): return "\(title) \(text)" + case let .startChat(title, text): return "\(title) \(text)" + + case .wrongPassphrase: return "wrongPassphrase" + case .invalidConfirmation: return "invalidConfirmation" + case .keychainError: return "keychainError" + case let .databaseError(title, message): return "\(title) \(message)" + case let .unknownError(title, message): return "\(title) \(message)" + case let .archiveExportedWithErrors(path, _): return "archiveExportedWithErrors \(path)" + + case let .error(title, _): return "error \(title)" + } + } +} + +struct MigrateFromDevice: View { + @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme + @Binding var showProgressOnSettings: Bool + @State private var migrationState: MigrationFromState = .chatStopInProgress + @State private var useKeychain = storeDBPassphraseGroupDefault.get() + @AppStorage(GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE, store: groupDefaults) private var initialRandomDBPassphrase: Bool = false + @State private var alert: MigrateFromDeviceViewAlert? + @State private var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) + private let tempDatabaseUrl = urlForTemporaryDatabase() + @State private var chatReceiver: MigrationChatReceiver? = nil + @State private var backDisabled: Bool = false + + var body: some View { + if authorized { + migrateView() + } else { + Button(action: runAuth) { Label("Unlock", systemImage: "lock") } + .onAppear(perform: runAuth) + } + } + + private func runAuth() { authorize(NSLocalizedString("Open migration to another device", comment: "authentication reason"), $authorized) } + + func migrateView() -> some View { + VStack { + switch migrationState { + case .chatStopInProgress: + chatStopInProgressView() + case let .chatStopFailed(reason): + chatStopFailedView(reason) + case .passphraseNotSet: + passphraseNotSetView() + case .passphraseConfirmation: + PassphraseConfirmationView(migrationState: $migrationState, alert: $alert) + case .uploadConfirmation: + uploadConfirmationView() + case .archiving: + archivingView() + case let .uploadProgress(uploaded, total, _, archivePath, _): + uploadProgressView(uploaded, totalBytes: total, archivePath) + case let .uploadFailed(total, archivePath): + uploadFailedView(totalBytes: total, archivePath) + case .linkCreation: + linkCreationView() + case let .linkShown(fileId, link, archivePath, ctrl): + linkShownView(fileId, link, archivePath, ctrl) + case let .finished(chatDeletion): + finishedView(chatDeletion) + } + } + .onChange(of: migrationState) { state in + backDisabled = switch migrationState { + case .chatStopInProgress, .archiving, .linkShown, .finished: true + case .chatStopFailed, .passphraseNotSet, .passphraseConfirmation, .uploadConfirmation, .uploadProgress, .uploadFailed, .linkCreation: false + } + } + .onAppear { + stopChat() + } + .onDisappear { + Task { + if !backDisabled { + await MainActor.run { + showProgressOnSettings = true + } + await startChatAndDismiss(false) + await MainActor.run { + showProgressOnSettings = false + } + } + if case let .uploadProgress(_, _, fileId, _, ctrl) = migrationState, let ctrl { + await cancelUploadedArchive(fileId, ctrl) + } + chatReceiver?.stopAndCleanUp() + try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory()) + } + } + .alert(item: $alert) { alert in + switch alert { + case let .finishMigration(fileId, ctrl): + return Alert( + title: Text("Remove archive?"), + message: Text("The uploaded database archive will be permanently removed from the servers."), + primaryButton: .destructive(Text("Continue")) { + finishMigration(fileId, ctrl) + }, + secondaryButton: .cancel() + ) + case let .startChat(title, text): + return Alert( + title: Text(title), + message: Text(text), + primaryButton: .destructive(Text("Start chat")) { + Task { + await startChatAndDismiss() + } + }, + secondaryButton: .cancel() + ) + case let .deleteChat(title, text): + return Alert( + title: Text(title), + message: Text(text), + primaryButton: .default(Text("Delete")) { + deleteChatAndDismiss() + }, + secondaryButton: .cancel() + ) + case let .wrongPassphrase(title, message): + return Alert(title: Text(title), message: Text(message)) + case let .invalidConfirmation(title): + return Alert(title: Text(title)) + case let .keychainError(title): + return Alert(title: Text(title)) + case let .databaseError(title, message): + return Alert(title: Text(title), message: Text(message)) + case let .unknownError(title, message): + return Alert(title: Text(title), message: Text(message)) + case let .archiveExportedWithErrors(archivePath, errs): + return Alert( + title: Text("Chat database exported"), + message: Text("You may migrate the exported database.") + textNewLine + Text("Some file(s) were not exported:") + Text(archiveErrorsText(errs)), + dismissButton: .default(Text("Continue")) { + Task { await uploadArchive(path: archivePath) } + } + ) + case let .error(title, error): + return Alert(title: Text(title), message: Text(error)) + } + } + .interactiveDismissDisabled(backDisabled) + } + + private func chatStopInProgressView() -> some View { + ZStack { + List { + Section {} header: { + Text("Stopping chat") + .foregroundColor(theme.colors.secondary) + } + } + progressView() + } + } + + private func chatStopFailedView(_ reason: String) -> some View { + List { + Section { + Text(reason) + Button(action: stopChat) { + settingsRow("stop.fill", color: theme.colors.secondary) { + Text("Stop chat").foregroundColor(.red) + } + } + } header: { + Text("Error stopping chat") + .foregroundColor(theme.colors.secondary) + } footer: { + Text("In order to continue, chat should be stopped.") + .foregroundColor(theme.colors.secondary) + .font(.callout) + } + } + } + + private func passphraseNotSetView() -> some View { + DatabaseEncryptionView(useKeychain: $useKeychain, migration: true, stopChatRunBlockStartChat: { _, _ in + }) + .onChange(of: initialRandomDBPassphrase) { initial in + if !initial { + migrationState = .uploadConfirmation + } + } + } + + private func uploadConfirmationView() -> some View { + List { + Section { + Button(action: { migrationState = .archiving }) { + settingsRow("tray.and.arrow.up", color: theme.colors.secondary) { + Text("Archive and upload").foregroundColor(theme.colors.primary) + } + } + } header: { + Text("Confirm upload") + .foregroundColor(theme.colors.secondary) + } footer: { + Text("All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.") + .foregroundColor(theme.colors.secondary) + .font(.callout) + } + } + } + + private func archivingView() -> some View { + ZStack { + List { + Section {} header: { + Text("Archiving database") + .foregroundColor(theme.colors.secondary) + } + } + progressView() + } + .onAppear { + exportArchive() + } + } + + private func uploadProgressView(_ uploadedBytes: Int64, totalBytes: Int64, _ archivePath: URL) -> some View { + ZStack { + List { + Section {} header: { + Text("Uploading archive") + .foregroundColor(theme.colors.secondary) + } + } + let ratio = Float(uploadedBytes) / Float(totalBytes) + MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: uploadedBytes, countStyle: .binary)) uploaded", theme.colors.primary) + } + .onAppear { + startUploading(totalBytes, archivePath) + } + } + + private func uploadFailedView(totalBytes: Int64, _ archivePath: URL) -> some View { + List { + Section { + Button(action: { + migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil) + }) { + settingsRow("tray.and.arrow.up", color: theme.colors.secondary) { + Text("Repeat upload").foregroundColor(theme.colors.primary) + } + } + } header: { + Text("Upload failed") + .foregroundColor(theme.colors.secondary) + } footer: { + Text("You can give another try.") + .foregroundColor(theme.colors.secondary) + .font(.callout) + } + } + .onAppear { + chatReceiver?.stopAndCleanUp() + } + } + + private func linkCreationView() -> some View { + ZStack { + List { + Section {} header: { + Text("Creating archive link") + .foregroundColor(theme.colors.secondary) + } + } + progressView() + } + } + + private func linkShownView(_ fileId: Int64, _ link: String, _ archivePath: URL, _ ctrl: chat_ctrl) -> some View { + List { + Section { + Button(action: { cancelMigration(fileId, ctrl) }) { + settingsRow("multiply", color: theme.colors.secondary) { + Text("Cancel migration").foregroundColor(.red) + } + } + Button(action: { alert = .finishMigration(fileId, ctrl) }) { + settingsRow("checkmark", color: theme.colors.secondary) { + Text("Finalize migration").foregroundColor(theme.colors.primary) + } + } + } footer: { + VStack(alignment: .leading, spacing: 16) { + Text("**Warning**: the archive will be removed.") + Text("Choose _Migrate from another device_ on the new device and scan QR code.") + } + .foregroundColor(theme.colors.secondary) + .font(.callout) + } + Section(header: Text("Show QR code").foregroundColor(theme.colors.secondary)) { + SimpleXLinkQRCode(uri: link) + .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)) + } + + Section(header: Text("Or securely share this file link").foregroundColor(theme.colors.secondary)) { + shareLinkView(link) + } + .listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 10)) + } + } + + private func finishedView(_ chatDeletion: Bool) -> some View { + ZStack { + List { + Section { + Button(action: { alert = .startChat() }) { + settingsRow("play.fill", color: theme.colors.secondary) { + Text("Start chat").foregroundColor(.red) + } + } + Button(action: { alert = .deleteChat() }) { + settingsRow("trash.fill", color: theme.colors.secondary) { + Text("Delete database from this device").foregroundColor(theme.colors.primary) + } + } + } header: { + Text("Migration complete") + .foregroundColor(theme.colors.secondary) + } footer: { + VStack(alignment: .leading, spacing: 16) { + Text("You **must not** use the same database on two devices.") + Text("**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.") + } + .foregroundColor(theme.colors.secondary) + .font(.callout) + } + } + if chatDeletion { + progressView() + } + } + } + + private func shareLinkView(_ link: String) -> some View { + HStack { + linkTextView(link) + Button { + showShareSheet(items: [link]) + } label: { + Image(systemName: "square.and.arrow.up") + .padding(.top, -7) + } + } + .frame(maxWidth: .infinity) + } + + private func linkTextView(_ link: String) -> some View { + Text(link) + .lineLimit(1) + .font(.caption) + .truncationMode(.middle) + } + + static func largeProgressView(_ value: Float, _ title: String, _ description: LocalizedStringKey, _ primaryColor: Color) -> some View { + ZStack { + VStack { + Text(description) + .font(.title3) + .hidden() + + Text(title) + .font(.system(size: 54)) + .bold() + .foregroundColor(primaryColor) + + Text(description) + .font(.title3) + } + + Circle() + .trim(from: 0, to: CGFloat(value)) + .stroke( + primaryColor, + style: StrokeStyle(lineWidth: 27) + ) + .rotationEffect(.degrees(180)) + .animation(.linear, value: value) + .frame(maxWidth: .infinity) + .padding(.horizontal) + .padding(.horizontal) + } + .frame(maxWidth: .infinity) + } + + private func stopChat() { + Task { + do { + try await stopChatAsync() + do { + try apiSaveAppSettings(settings: AppSettings.current.prepareForExport()) + await MainActor.run { + migrationState = initialRandomDBPassphraseGroupDefault.get() ? .passphraseNotSet : .passphraseConfirmation + } + } catch let error { + alert = .error(title: "Error saving settings", error: error.localizedDescription) + migrationState = .chatStopFailed(reason: NSLocalizedString("Error saving settings", comment: "when migrating")) + } + } catch let e { + await MainActor.run { + migrationState = .chatStopFailed(reason: e.localizedDescription) + } + } + } + } + + private func exportArchive() { + Task { + do { + try? FileManager.default.createDirectory(at: getMigrationTempFilesDirectory(), withIntermediateDirectories: true) + let (archivePath, errs) = try await exportChatArchive(getMigrationTempFilesDirectory()) + if errs.isEmpty { + await uploadArchive(path: archivePath) + } else { + await MainActor.run { + alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: errs) + migrationState = .uploadConfirmation + } + } + } catch let error { + await MainActor.run { + alert = .error(title: "Error exporting chat database", error: responseError(error)) + migrationState = .uploadConfirmation + } + } + } + } + + private func uploadArchive(path archivePath: URL) async { + if let attrs = try? FileManager.default.attributesOfItem(atPath: archivePath.path), + let totalBytes = attrs[.size] as? Int64 { + await MainActor.run { + migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil) + } + } else { + await MainActor.run { + alert = .error(title: "Exported file doesn't exist") + migrationState = .uploadConfirmation + } + } + } + + private func initTemporaryDatabase() -> (chat_ctrl, User)? { + let (status, ctrl) = chatInitTemporaryDatabase(url: tempDatabaseUrl) + showErrorOnMigrationIfNeeded(status, $alert) + do { + if let ctrl, let user = try startChatWithTemporaryDatabase(ctrl: ctrl) { + return (ctrl, user) + } + } catch let error { + logger.error("Error while starting chat in temporary database: \(error.localizedDescription)") + } + return nil + } + + private func startUploading(_ totalBytes: Int64, _ archivePath: URL) { + Task { + guard let ctrlAndUser = initTemporaryDatabase() else { + return migrationState = .uploadFailed(totalBytes: totalBytes, archivePath: archivePath) + } + let (ctrl, user) = ctrlAndUser + chatReceiver = MigrationChatReceiver(ctrl: ctrl, databaseUrl: tempDatabaseUrl) { msg in + await MainActor.run { + switch msg { + case let .result(.sndFileProgressXFTP(_, _, fileTransferMeta, sentSize, totalSize)): + if case let .uploadProgress(uploaded, total, _, _, _) = migrationState, uploaded != total { + migrationState = .uploadProgress(uploadedBytes: sentSize, totalBytes: totalSize, fileId: fileTransferMeta.fileId, archivePath: archivePath, ctrl: ctrl) + } + case .result(.sndFileRedirectStartXFTP): + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + migrationState = .linkCreation + } + case let .result(.sndStandaloneFileComplete(_, fileTransferMeta, rcvURIs)): + let cfg = getNetCfg() + let proxy: NetworkProxy? = if cfg.socksProxy == nil { + nil + } else { + networkProxyDefault.get() + } + let data = MigrationFileLinkData.init( + networkConfig: MigrationFileLinkData.NetworkConfig( + socksProxy: cfg.socksProxy, + networkProxy: proxy, + hostMode: cfg.hostMode, + requiredHostMode: cfg.requiredHostMode + ) + ) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + migrationState = .linkShown(fileId: fileTransferMeta.fileId, link: data.addToLink(link: rcvURIs[0]), archivePath: archivePath, ctrl: ctrl) + } + case .result(.sndFileError): + alert = .error(title: "Upload failed", error: "Check your internet connection and try again") + migrationState = .uploadFailed(totalBytes: totalBytes, archivePath: archivePath) + default: + logger.debug("unsupported event: \(msg.responseType)") + } + } + } + chatReceiver?.start() + + let (res, error) = await uploadStandaloneFile(user: user, file: CryptoFile.plain(archivePath.lastPathComponent), ctrl: ctrl) + await MainActor.run { + guard let res = res else { + migrationState = .uploadFailed(totalBytes: totalBytes, archivePath: archivePath) + return alert = .error(title: "Error uploading the archive", error: error ?? "") + } + migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: res.fileSize, fileId: res.fileId, archivePath: archivePath, ctrl: ctrl) + } + } + } + + private func cancelUploadedArchive(_ fileId: Int64, _ ctrl: chat_ctrl) async { + _ = await apiCancelFile(fileId: fileId, ctrl: ctrl) + } + + private func cancelMigration(_ fileId: Int64, _ ctrl: chat_ctrl) { + Task { + await cancelUploadedArchive(fileId, ctrl) + await startChatAndDismiss() + } + } + + private func finishMigration(_ fileId: Int64, _ ctrl: chat_ctrl) { + Task { + await cancelUploadedArchive(fileId, ctrl) + await MainActor.run { + migrationState = .finished(chatDeletion: false) + } + } + } + + private func deleteChatAndDismiss() { + Task { + do { + try await deleteChatAsync() + m.chatDbChanged = true + m.chatInitialized = false + migrationState = .finished(chatDeletion: true) + DispatchQueue.main.asyncAfter(deadline: .now()) { + resetChatCtrl() + do { + try initializeChat(start: false) + m.chatDbChanged = false + AppChatState.shared.set(.active) + } catch let error { + fatalError("Error starting chat \(responseError(error))") + } + dismissAllSheets(animated: true) + } + } catch let error { + alert = .error(title: "Error deleting database", error: responseError(error)) + } + } + } + + private func startChatAndDismiss(_ dismiss: Bool = true) async { + AppChatState.shared.set(.active) + do { + if m.chatDbChanged { + resetChatCtrl() + try initializeChat(start: true) + m.chatDbChanged = false + } else { + try startChat(refreshInvitations: true) + } + } catch let error { + alert = .error(title: "Error starting chat", error: responseError(error)) + } + // Hide settings anyway if chatDbStatus is not ok, probably passphrase needs to be entered + if dismiss || m.chatDbStatus != .ok { + dismissAllSheets(animated: true) + } + } + + private static func urlForTemporaryDatabase() -> URL { + URL(fileURLWithPath: generateNewFileName(getMigrationTempFilesDirectory().path + "/" + "migration", "db", fullPath: true)) + } +} + +private struct PassphraseConfirmationView: View { + @EnvironmentObject var theme: AppTheme + @Binding var migrationState: MigrationFromState + @State private var useKeychain = storeDBPassphraseGroupDefault.get() + @State private var currentKey: String = "" + @State private var verifyingPassphrase: Bool = false + @FocusState private var keyboardVisible: Bool + @Binding var alert: MigrateFromDeviceViewAlert? + + var body: some View { + ZStack { + List { + chatStoppedView() + Section { + PassphraseField(key: $currentKey, placeholder: "Current passphrase…", valid: validKey(currentKey)) + .focused($keyboardVisible) + Button(action: { + verifyingPassphrase = true + hideKeyboard() + Task { + await verifyDatabasePassphrase(currentKey) + verifyingPassphrase = false + } + }) { + settingsRow(useKeychain ? "key" : "lock", color: theme.colors.secondary) { + Text("Verify passphrase") + } + } + .disabled(verifyingPassphrase || currentKey.isEmpty) + } header: { + Text("Verify database passphrase") + .foregroundColor(theme.colors.secondary) + } footer: { + Text("Confirm that you remember database passphrase to migrate it.") + .foregroundColor(theme.colors.secondary) + .font(.callout) + } + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + keyboardVisible = true + } + } + } + if verifyingPassphrase { + progressView() + } + } + } + + private func verifyDatabasePassphrase(_ dbKey: String) async { + do { + try await testStorageEncryption(key: dbKey) + await MainActor.run { + migrationState = .uploadConfirmation + } + } catch let error { + if case .errorDatabase(.errorOpen(.errorNotADatabase)) = error as? ChatError { + showErrorOnMigrationIfNeeded(.errorNotADatabase(dbFile: ""), $alert) + } else { + alert = .error(title: "Error", error: NSLocalizedString("Error verifying passphrase:", comment: "") + " " + String(responseError(error))) + } + } + } +} + +private func showErrorOnMigrationIfNeeded(_ status: DBMigrationResult, _ alert: Binding<MigrateFromDeviceViewAlert?>) { + switch status { + case .invalidConfirmation: + alert.wrappedValue = .invalidConfirmation() + case .errorNotADatabase: + alert.wrappedValue = .wrongPassphrase() + case .errorKeychain: + alert.wrappedValue = .keychainError() + case let .errorSQL(_, error): + alert.wrappedValue = .databaseError(message: error) + case let .unknown(error): + alert.wrappedValue = .unknownError(message: error) + case .errorMigration: () + case .ok: () + } +} + +private func progressView() -> some View { + VStack { + ProgressView().scaleEffect(2) + } + .frame(maxWidth: .infinity, maxHeight: .infinity ) +} + +func chatStoppedView() -> some View { + settingsRow("exclamationmark.octagon.fill", color: .red) { + Text("Chat is stopped") + } +} + +private class MigrationChatReceiver { + let ctrl: chat_ctrl + let databaseUrl: URL + let processReceivedMsg: (APIResult<ChatEvent>) async -> Void + private var receiveLoop: Task<Void, Never>? + private var receiveMessages = true + + init(ctrl: chat_ctrl, databaseUrl: URL, _ processReceivedMsg: @escaping (APIResult<ChatEvent>) async -> Void) { + self.ctrl = ctrl + self.databaseUrl = databaseUrl + self.processReceivedMsg = processReceivedMsg + } + + func start() { + logger.debug("MigrationChatReceiver.start") + receiveMessages = true + if receiveLoop != nil { return } + receiveLoop = Task { await receiveMsgLoop() } + } + + func receiveMsgLoop() async { + // TODO use function that has timeout + if let msg: APIResult<ChatEvent> = await chatRecvMsg(ctrl) { + Task { + await TerminalItems.shared.addResult(msg) + } + logger.debug("processReceivedMsg: \(msg.responseType)") + await processReceivedMsg(msg) + } + if self.receiveMessages { + _ = try? await Task.sleep(nanoseconds: 7_500_000) + await receiveMsgLoop() + } + } + + func stopAndCleanUp() { + logger.debug("MigrationChatReceiver.stop") + receiveMessages = false + receiveLoop?.cancel() + receiveLoop = nil + chat_close_store(ctrl) + try? FileManager.default.removeItem(atPath: "\(databaseUrl.path)_chat.db") + try? FileManager.default.removeItem(atPath: "\(databaseUrl.path)_agent.db") + } +} + +struct MigrateFromDevice_Previews: PreviewProvider { + static var previews: some View { + MigrateFromDevice(showProgressOnSettings: Binding.constant(false)) + } +} diff --git a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift new file mode 100644 index 0000000000..93fe19cf33 --- /dev/null +++ b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift @@ -0,0 +1,801 @@ +// +// MigrateToDevice.swift +// SimpleX (iOS) +// +// Created by Avently on 23.02.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +enum MigrationToDeviceState: Codable, Equatable { + case downloadProgress(link: String, archiveName: String) + case archiveImport(archiveName: String) + case passphrase + + // Here we check whether it's needed to show migration process after app restart or not + // It's important to NOT show the process when archive was corrupted/not fully downloaded + static func makeMigrationState() -> MigrationToState? { + let state: MigrationToDeviceState? = UserDefaults.standard.string(forKey: DEFAULT_MIGRATION_TO_STAGE) != nil ? decodeJSON(UserDefaults.standard.string(forKey: DEFAULT_MIGRATION_TO_STAGE)!) : nil + var initial: MigrationToState? = .pasteOrScanLink + //logger.debug("Inited with migrationState: \(String(describing: state))") + switch state { + case nil: + initial = nil + case .downloadProgress: + // No migration happens at the moment actually since archive were not downloaded fully + logger.debug("MigrateToDevice: archive wasn't fully downloaded, removed broken file") + initial = nil + case let .archiveImport(archiveName): + let archivePath = getMigrationTempFilesDirectory().path + "/" + archiveName + initial = .archiveImportFailed(archivePath: archivePath) + case .passphrase: + initial = .passphrase(passphrase: "") + } + if initial == nil { + UserDefaults.standard.removeObject(forKey: DEFAULT_MIGRATION_TO_STAGE) + try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory()) + } + return initial + } + + static func save(_ state: MigrationToDeviceState?) { + if let state { + UserDefaults.standard.setValue(encodeJSON(state), forKey: DEFAULT_MIGRATION_TO_STAGE) + } else { + UserDefaults.standard.removeObject(forKey: DEFAULT_MIGRATION_TO_STAGE) + } + } +} + +enum MigrationToState: Equatable { + case pasteOrScanLink + case linkDownloading(link: String) + case downloadProgress(downloadedBytes: Int64, totalBytes: Int64, fileId: Int64, link: String, archivePath: String, ctrl: chat_ctrl?) + case downloadFailed(totalBytes: Int64, link: String, archivePath: String) + case archiveImport(archivePath: String) + case archiveImportFailed(archivePath: String) + case passphrase(passphrase: String) + case migrationConfirmation(status: DBMigrationResult, passphrase: String, useKeychain: Bool) + case migration(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Bool) + case onion(appSettings: AppSettings) +} + +private enum MigrateToDeviceViewAlert: Identifiable { + case chatImportedWithErrors(title: LocalizedStringKey = "Chat database imported", + text: LocalizedStringKey = "Some non-fatal errors occurred during import - you may see Chat console for more details.") + + case wrongPassphrase(title: LocalizedStringKey = "Wrong passphrase!", message: LocalizedStringKey = "Enter correct passphrase.") + case invalidConfirmation(title: LocalizedStringKey = "Invalid migration confirmation") + case keychainError(_ title: LocalizedStringKey = "Keychain error") + case databaseError(_ title: LocalizedStringKey = "Database error", message: String) + case unknownError(_ title: LocalizedStringKey = "Unknown error", message: String) + + case error(title: LocalizedStringKey, error: String = "") + + var id: String { + switch self { + case .chatImportedWithErrors: return "chatImportedWithErrors" + + case .wrongPassphrase: return "wrongPassphrase" + case .invalidConfirmation: return "invalidConfirmation" + case .keychainError: return "keychainError" + case let .databaseError(title, message): return "\(title) \(message)" + case let .unknownError(title, message): return "\(title) \(message)" + + case let .error(title, _): return "error \(title)" + } + } +} + +struct MigrateToDevice: View { + @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme + @Environment(\.dismiss) var dismiss: DismissAction + @Binding var migrationState: MigrationToState? + @State private var useKeychain = storeDBPassphraseGroupDefault.get() + @State private var alert: MigrateToDeviceViewAlert? + @State private var databaseAlert: DatabaseAlert? = nil + private let tempDatabaseUrl = urlForTemporaryDatabase() + @State private var chatReceiver: MigrationChatReceiver? = nil + // Prevent from hiding the view until migration is finished or app deleted + @State private var backDisabled: Bool = false + @State private var showQRCodeScanner: Bool = true + @State private var pasteboardHasStrings = UIPasteboard.general.hasStrings + + @State private var importingArchiveFromFileProgressIndicator = false + @State private var showFileImporter = false + + var body: some View { + VStack { + switch migrationState { + case nil: EmptyView() + case .pasteOrScanLink: + pasteOrScanLinkView() + case let .linkDownloading(link): + linkDownloadingView(link) + case let .downloadProgress(downloaded, total, _, _, _, _): + downloadProgressView(downloaded, totalBytes: total) + case let .downloadFailed(total, link, archivePath): + downloadFailedView(totalBytes: total, link, archivePath) + case let .archiveImport(archivePath): + archiveImportView(archivePath) + case let .archiveImportFailed(archivePath): + archiveImportFailedView(archivePath) + case let .passphrase(passphrase): + PassphraseEnteringView(migrationState: $migrationState, currentKey: passphrase, alert: $alert) + case let .migrationConfirmation(status, passphrase, useKeychain): + migrationConfirmationView(status, passphrase, useKeychain) + case let .migration(passphrase, confirmation, useKeychain): + migrationView(passphrase, confirmation, useKeychain) + case let .onion(appSettings): + OnionView(appSettings: appSettings, finishMigration: finishMigration) + } + } + .onAppear { + backDisabled = switch migrationState { + case nil, .pasteOrScanLink, .linkDownloading, .downloadProgress, .downloadFailed, .archiveImportFailed: false + case .archiveImport, .passphrase, .migrationConfirmation, .migration, .onion: true + } + } + .onChange(of: migrationState) { state in + backDisabled = switch state { + case nil, .pasteOrScanLink, .linkDownloading, .downloadProgress, .downloadFailed, .archiveImportFailed: false + case .archiveImport, .passphrase, .migrationConfirmation, .migration, .onion: true + } + } + .onDisappear { + Task { + if case .archiveImportFailed = migrationState { + // Original database is not exist, nothing is setup correctly for showing to a user yet. Return to clean state + deleteAppDatabaseAndFiles() + initChatAndMigrate() + } else if case let .downloadProgress(_, _, fileId, _, _, ctrl) = migrationState, let ctrl { + await stopArchiveDownloading(fileId, ctrl) + } + chatReceiver?.stopAndCleanUp() + if !backDisabled { + try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory()) + MigrationToDeviceState.save(nil) + } + } + } + .alert(item: $alert) { alert in + switch alert { + case let .chatImportedWithErrors(title, text): + return Alert(title: Text(title), message: Text(text)) + case let .wrongPassphrase(title, message): + return Alert(title: Text(title), message: Text(message)) + case let .invalidConfirmation(title): + return Alert(title: Text(title)) + case let .keychainError(title): + return Alert(title: Text(title)) + case let .databaseError(title, message): + return Alert(title: Text(title), message: Text(message)) + case let .unknownError(title, message): + return Alert(title: Text(title), message: Text(message)) + case let .error(title, error): + return Alert(title: Text(title), message: Text(error)) + } + } + .alert(item: $databaseAlert) { item in + switch item { + case .archiveImported: + let (title, message) = archiveImportedAlertText() + return Alert(title: Text(title), message: Text(message)) + case let .archiveImportedWithErrors(errs): + let (title, message) = archiveImportedWithErrorsAlertText(errs: errs) + return Alert(title: Text(title), message: Text(message)) + case let .error(title, error): + return Alert(title: Text(title), message: Text(error)) + default: // not expected this branch to be called because this alert is used only for importArchive purpose + return Alert(title: Text("Error")) + } + } + .interactiveDismissDisabled(backDisabled) + } + + private func pasteOrScanLinkView() -> some View { + ZStack { + List { + Section(header: Text("Scan QR code").foregroundColor(theme.colors.secondary)) { + ScannerInView(showQRCodeScanner: $showQRCodeScanner) { resp in + switch resp { + case let .success(r): + let link = r.string + if strHasSimplexFileLink(link.trimmingCharacters(in: .whitespaces)) { + migrationState = .linkDownloading(link: link.trimmingCharacters(in: .whitespaces)) + } else { + alert = .error(title: "Invalid link", error: "The text you pasted is not a SimpleX link.") + } + case let .failure(e): + logger.error("processQRCode QR code error: \(e.localizedDescription)") + alert = .error(title: "Invalid link", error: "The text you pasted is not a SimpleX link.") + } + } + } + Section(header: Text("Or paste archive link").foregroundColor(theme.colors.secondary)) { + pasteLinkView() + } + Section(header: Text("Or import archive file").foregroundColor(theme.colors.secondary)) { + archiveImportFromFileView() + } + } + if importingArchiveFromFileProgressIndicator { + progressView() + } + } + } + + private func pasteLinkView() -> some View { + Button { + if let str = UIPasteboard.general.string { + if strHasSimplexFileLink(str.trimmingCharacters(in: .whitespaces)) { + migrationState = .linkDownloading(link: str.trimmingCharacters(in: .whitespaces)) + } else { + alert = .error(title: "Invalid link", error: "The text you pasted is not a SimpleX link.") + } + } + } label: { + Text("Tap to paste link") + } + .disabled(!pasteboardHasStrings) + .frame(maxWidth: .infinity, alignment: .center) + } + + private func archiveImportFromFileView() -> some View { + Button { + showFileImporter = true + } label: { + Label("Import database", systemImage: "square.and.arrow.down") + } + .disabled(importingArchiveFromFileProgressIndicator) + .fileImporter( + isPresented: $showFileImporter, + allowedContentTypes: [.zip], + allowsMultipleSelection: false + ) { result in + if case let .success(files) = result, let fileURL = files.first { + Task { + let success = await DatabaseView.importArchive(fileURL, $importingArchiveFromFileProgressIndicator, $databaseAlert, true) + if success { + DatabaseView.startChat( + Binding.constant(false), + $importingArchiveFromFileProgressIndicator + ) + hideView() + } + } + } + } + } + + + private func linkDownloadingView(_ link: String) -> some View { + ZStack { + List { + Section {} header: { + Text("Downloading link details") + .foregroundColor(theme.colors.secondary) + } + } + progressView() + } + .onAppear { + downloadLinkDetails(link) + } + } + + private func downloadProgressView(_ downloadedBytes: Int64, totalBytes: Int64) -> some View { + ZStack { + List { + Section {} header: { + Text("Downloading archive") + .foregroundColor(theme.colors.secondary) + } + } + let ratio = Float(downloadedBytes) / Float(max(totalBytes, 1)) + MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: downloadedBytes, countStyle: .binary)) downloaded", theme.colors.primary) + } + } + + private func downloadFailedView(totalBytes: Int64, _ link: String, _ archivePath: String) -> some View { + List { + Section { + Button(action: { + try? FileManager.default.removeItem(atPath: archivePath) + migrationState = .linkDownloading(link: link) + }) { + settingsRow("tray.and.arrow.down", color: theme.colors.secondary) { + Text("Repeat download").foregroundColor(theme.colors.primary) + } + } + } header: { + Text("Download failed") + .foregroundColor(theme.colors.secondary) + } footer: { + Text("You can give another try.") + .foregroundColor(theme.colors.secondary) + .font(.callout) + } + } + .onAppear { + chatReceiver?.stopAndCleanUp() + try? FileManager.default.removeItem(atPath: archivePath) + MigrationToDeviceState.save(nil) + } + } + + private func archiveImportView(_ archivePath: String) -> some View { + ZStack { + List { + Section {} header: { + Text("Importing archive") + .foregroundColor(theme.colors.secondary) + } + } + progressView() + } + .onAppear { + importArchive(archivePath) + } + } + + private func archiveImportFailedView(_ archivePath: String) -> some View { + List { + Section { + Button(action: { + migrationState = .archiveImport(archivePath: archivePath) + }) { + settingsRow("square.and.arrow.down", color: theme.colors.secondary) { + Text("Repeat import").foregroundColor(theme.colors.primary) + } + } + } header: { + Text("Import failed") + .foregroundColor(theme.colors.secondary) + } footer: { + Text("You can give another try.") + .foregroundColor(theme.colors.secondary) + .font(.callout) + } + } + } + + private func migrationConfirmationView(_ status: DBMigrationResult, _ passphrase: String, _ useKeychain: Bool) -> some View { + List { + let (header, button, footer, confirmation): (LocalizedStringKey, LocalizedStringKey?, String, MigrationConfirmation?) = switch status { + case let .errorMigration(_, migrationError): + switch migrationError { + case .upgrade: + ("Database upgrade", + "Upgrade and open chat", + "", + .yesUp) + case .downgrade: + ("Database downgrade", + "Downgrade and open chat", + NSLocalizedString("Warning: you may lose some data!", comment: ""), + .yesUpDown) + case let .migrationError(mtrError): + ("Incompatible database version", + nil, + "\(NSLocalizedString("Error: ", comment: "")) \(mtrErrorDescription(mtrError))", + nil) + } + default: ("Error", nil, "Unknown error", nil) + } + Section { + if let button, let confirmation { + Button(action: { + migrationState = .migration(passphrase: passphrase, confirmation: confirmation, useKeychain: useKeychain) + }) { + settingsRow("square.and.arrow.down", color: theme.colors.secondary) { + Text(button).foregroundColor(theme.colors.primary) + } + } + } else { + EmptyView() + } + } header: { + Text(header) + .foregroundColor(theme.colors.secondary) + } footer: { + Text(footer) + .foregroundColor(theme.colors.secondary) + .font(.callout) + } + } + } + + private func migrationView(_ passphrase: String, _ confirmation: MigrationConfirmation, _ useKeychain: Bool) -> some View { + ZStack { + List { + Section {} header: { + Text("Migrating") + .foregroundColor(theme.colors.secondary) + } + } + progressView() + } + .onAppear { + startChat(passphrase, confirmation, useKeychain) + } + } + + struct OnionView: View { + @EnvironmentObject var theme: AppTheme + @State var appSettings: AppSettings + @State private var onionHosts: OnionHosts = .no + var finishMigration: (AppSettings) -> Void + + var body: some View { + List { + Section { + Button(action: { + var updated = appSettings.networkConfig! + let (hostMode, requiredHostMode) = onionHosts.hostMode + updated.hostMode = hostMode + updated.requiredHostMode = requiredHostMode + updated.socksProxy = nil + appSettings.networkConfig = updated + finishMigration(appSettings) + }) { + settingsRow("checkmark", color: theme.colors.secondary) { + Text("Apply").foregroundColor(theme.colors.primary) + } + } + } header: { + Text("Confirm network settings") + .foregroundColor(theme.colors.secondary) + } footer: { + Text("Please confirm that network settings are correct for this device.") + .foregroundColor(theme.colors.secondary) + .font(.callout) + } + + Section(header: Text("Network settings").foregroundColor(theme.colors.secondary)) { + Picker("Use .onion hosts", selection: $onionHosts) { + ForEach(OnionHosts.values, id: \.self) { Text($0.text) } + } + .frame(height: 36) + } + } + } + } + + private func downloadLinkDetails(_ link: String) { + let archiveTime = Date.now + let ts = archiveTime.ISO8601Format(Date.ISO8601FormatStyle(timeSeparator: .omitted)) + let archiveName = "simplex-chat.\(ts).zip" + let archivePath = getMigrationTempFilesDirectory().appendingPathComponent(archiveName) + + startDownloading(0, link, archivePath.path) + } + + private func initTemporaryDatabase() -> (chat_ctrl, User)? { + let (status, ctrl) = chatInitTemporaryDatabase(url: tempDatabaseUrl) + showErrorOnMigrationIfNeeded(status, $alert) + do { + if let ctrl, let user = try startChatWithTemporaryDatabase(ctrl: ctrl) { + return (ctrl, user) + } + } catch let error { + logger.error("Error while starting chat in temporary database: \(error.localizedDescription)") + } + return nil + } + + private func startDownloading(_ totalBytes: Int64, _ link: String, _ archivePath: String) { + Task { + guard let ctrlAndUser = initTemporaryDatabase() else { + return migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath) + } + let (ctrl, user) = ctrlAndUser + chatReceiver = MigrationChatReceiver(ctrl: ctrl, databaseUrl: tempDatabaseUrl) { msg in + await MainActor.run { + switch msg { + case let .result(.rcvFileProgressXFTP(_, _, receivedSize, totalSize, rcvFileTransfer)): + migrationState = .downloadProgress(downloadedBytes: receivedSize, totalBytes: totalSize, fileId: rcvFileTransfer.fileId, link: link, archivePath: archivePath, ctrl: ctrl) + MigrationToDeviceState.save(.downloadProgress(link: link, archiveName: URL(fileURLWithPath: archivePath).lastPathComponent)) + case .result(.rcvStandaloneFileComplete): + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + // User closed the whole screen before new state was saved + if migrationState == nil { + MigrationToDeviceState.save(nil) + } else { + migrationState = .archiveImport(archivePath: archivePath) + MigrationToDeviceState.save(.archiveImport(archiveName: URL(fileURLWithPath: archivePath).lastPathComponent)) + } + } + case .result(.rcvFileError): + alert = .error(title: "Download failed", error: "File was deleted or link is invalid") + migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath) + case .error(.error(.noRcvFileUser)): + alert = .error(title: "Download failed", error: "File was deleted or link is invalid") + migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath) + default: + logger.debug("unsupported event: \(msg.responseType)") + } + } + } + chatReceiver?.start() + + let (res, error) = await downloadStandaloneFile(user: user, url: link, file: CryptoFile.plain(URL(fileURLWithPath: archivePath).lastPathComponent), ctrl: ctrl) + if res == nil { + await MainActor.run { + migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath) + } + return alert = .error(title: "Error downloading the archive", error: error ?? "") + } + } + } + + private func importArchive(_ archivePath: String) { + Task { + do { + if !hasChatCtrl() { + chatInitControllerRemovingDatabases() + } else if ChatModel.shared.chatRunning == true { + // cannot delete storage if chat is running + try await stopChatAsync() + } + try await apiDeleteStorage() + try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) + do { + let config = ArchiveConfig(archivePath: archivePath) + let archiveErrors = try await apiImportArchive(config: config) + if !archiveErrors.isEmpty { + alert = .chatImportedWithErrors() + } + await MainActor.run { + migrationState = .passphrase(passphrase: "") + MigrationToDeviceState.save(.passphrase) + } + } catch let error { + await MainActor.run { + migrationState = .archiveImportFailed(archivePath: archivePath) + } + alert = .error(title: "Error importing chat database", error: responseError(error)) + } + } catch let error { + await MainActor.run { + migrationState = .archiveImportFailed(archivePath: archivePath) + } + alert = .error(title: "Error deleting chat database", error: responseError(error)) + } + } + } + + + private func stopArchiveDownloading(_ fileId: Int64, _ ctrl: chat_ctrl) async { + _ = await apiCancelFile(fileId: fileId, ctrl: ctrl) + } + + private func startChat(_ passphrase: String, _ confirmation: MigrationConfirmation, _ useKeychain: Bool) { + if useKeychain { + _ = kcDatabasePassword.set(passphrase) + } else { + _ = kcDatabasePassword.remove() + } + storeDBPassphraseGroupDefault.set(useKeychain) + initialRandomDBPassphraseGroupDefault.set(false) + AppChatState.shared.set(.active) + Task { + do { + resetChatCtrl() + try initializeChat(start: false, confirmStart: false, dbKey: passphrase, refreshInvitations: true, confirmMigrations: confirmation) + var appSettings = try apiGetAppSettings(settings: AppSettings.current.prepareForExport()) + let hasOnionConfigured = appSettings.networkConfig?.socksProxy != nil || appSettings.networkConfig?.hostMode == .onionHost + appSettings.networkConfig?.socksProxy = nil + appSettings.networkConfig?.hostMode = .publicHost + appSettings.networkConfig?.requiredHostMode = true + await MainActor.run { + if hasOnionConfigured { + migrationState = .onion(appSettings: appSettings) + } else { + finishMigration(appSettings) + } + } + } catch let error { + hideView() + AlertManager.shared.showAlert(Alert(title: Text("Error starting chat"), message: Text(responseError(error)))) + } + } + } + + private func finishMigration(_ appSettings: AppSettings) { + do { + try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory()) + MigrationToDeviceState.save(nil) + try ObjC.catchException { + appSettings.importIntoApp() + } + do { + try SimpleX.startChat(refreshInvitations: true) + AlertManager.shared.showAlertMsg(title: "Chat migrated!", message: "Finalize migration on another device.") + } catch let error { + AlertManager.shared.showAlert(Alert(title: Text("Error starting chat"), message: Text(responseError(error)))) + } + } catch let error { + logger.error("Error importing settings: \(error.localizedDescription)") + AlertManager.shared.showAlert( + Alert( + title: Text("Error migrating settings"), + message: Text ("Some app settings were not migrated.") + textNewLine + Text(responseError(error))) + ) + } + hideView() + } + + private func hideView() { + onboardingStageDefault.set(.onboardingComplete) + m.onboardingStage = .onboardingComplete + m.migrationState = nil + MigrationToDeviceState.save(nil) + dismiss() + } + + private func strHasSimplexFileLink(_ text: String) -> Bool { + text.starts(with: "simplex:/file") || text.starts(with: "https://simplex.chat/file") + } + + private static func urlForTemporaryDatabase() -> URL { + URL(fileURLWithPath: generateNewFileName(getMigrationTempFilesDirectory().path + "/" + "migration", "db", fullPath: true)) + } +} + +private struct PassphraseEnteringView: View { + @EnvironmentObject var theme: AppTheme + @Binding var migrationState: MigrationToState? + @State private var useKeychain = true + @State var currentKey: String + @State private var verifyingPassphrase: Bool = false + @FocusState private var keyboardVisible: Bool + @Binding var alert: MigrateToDeviceViewAlert? + + var body: some View { + ZStack { + List { + Section { + settingsRow("key", color: theme.colors.secondary) { + Toggle("Save passphrase in Keychain", isOn: $useKeychain) + } + + PassphraseField(key: $currentKey, placeholder: "Current passphrase…", valid: validKey(currentKey)) + .focused($keyboardVisible) + Button(action: { + verifyingPassphrase = true + hideKeyboard() + Task { + let (status, _) = chatInitTemporaryDatabase(url: getAppDatabasePath(), key: currentKey, confirmation: .yesUp) + let success = switch status { + case .ok, .invalidConfirmation: true + default: false + } + if success { + await MainActor.run { + migrationState = .migration(passphrase: currentKey, confirmation: .yesUp, useKeychain: useKeychain) + } + } else if case .errorMigration = status { + await MainActor.run { + migrationState = .migrationConfirmation(status: status, passphrase: currentKey, useKeychain: useKeychain) + } + } else { + showErrorOnMigrationIfNeeded(status, $alert) + } + verifyingPassphrase = false + } + }) { + settingsRow("key", color: theme.colors.secondary) { + Text("Open chat") + } + } + .disabled(verifyingPassphrase || currentKey.isEmpty) + } header: { + Text("Enter passphrase") + .foregroundColor(theme.colors.secondary) + } footer: { + VStack(alignment: .leading, spacing: 16) { + if useKeychain { + Text("iOS Keychain is used to securely store passphrase - it allows receiving push notifications.") + } else { + Text("You have to enter passphrase every time the app starts - it is not stored on the device.") + Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.") + Text("**Warning**: Instant push notifications require passphrase saved in Keychain.") + } + } + .foregroundColor(theme.colors.secondary) + .font(.callout) + .padding(.top, 1) + .onTapGesture { keyboardVisible = false } + } + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + keyboardVisible = true + } + } + } + if verifyingPassphrase { + progressView() + } + } + } +} + +private func showErrorOnMigrationIfNeeded(_ status: DBMigrationResult, _ alert: Binding<MigrateToDeviceViewAlert?>) { + switch status { + case .invalidConfirmation: + alert.wrappedValue = .invalidConfirmation() + case .errorNotADatabase: + alert.wrappedValue = .wrongPassphrase() + case .errorKeychain: + alert.wrappedValue = .keychainError() + case let .errorSQL(_, error): + alert.wrappedValue = .databaseError(message: error) + case let .unknown(error): + alert.wrappedValue = .unknownError(message: error) + case .errorMigration: () + case .ok: () + } +} + +private func progressView() -> some View { + VStack { + ProgressView().scaleEffect(2) + } + .frame(maxWidth: .infinity, maxHeight: .infinity ) +} + +private class MigrationChatReceiver { + let ctrl: chat_ctrl + let databaseUrl: URL + let processReceivedMsg: (APIResult<ChatEvent>) async -> Void + private var receiveLoop: Task<Void, Never>? + private var receiveMessages = true + + init(ctrl: chat_ctrl, databaseUrl: URL, _ processReceivedMsg: @escaping (APIResult<ChatEvent>) async -> Void) { + self.ctrl = ctrl + self.databaseUrl = databaseUrl + self.processReceivedMsg = processReceivedMsg + } + + func start() { + logger.debug("MigrationChatReceiver.start") + receiveMessages = true + if receiveLoop != nil { return } + receiveLoop = Task { await receiveMsgLoop() } + } + + func receiveMsgLoop() async { + // TODO use function that has timeout + if let msg = await chatRecvMsg(ctrl) { + Task { + await TerminalItems.shared.addResult(msg) + } + logger.debug("processReceivedMsg: \(msg.responseType)") + await processReceivedMsg(msg) + } + if self.receiveMessages { + _ = try? await Task.sleep(nanoseconds: 7_500_000) + await receiveMsgLoop() + } + } + + func stopAndCleanUp() { + logger.debug("MigrationChatReceiver.stop") + receiveMessages = false + receiveLoop?.cancel() + receiveLoop = nil + chat_close_store(ctrl) + try? FileManager.default.removeItem(atPath: "\(databaseUrl.path)_chat.db") + try? FileManager.default.removeItem(atPath: "\(databaseUrl.path)_agent.db") + } +} + +struct MigrateToDevice_Previews: PreviewProvider { + static var previews: some View { + MigrateToDevice(migrationState: Binding.constant(.pasteOrScanLink)) + } +} diff --git a/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift b/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift index 182149cbde..3a64a955c5 100644 --- a/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift +++ b/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift @@ -9,20 +9,35 @@ import SwiftUI struct AddContactLearnMore: View { + var showTitle: Bool + var body: some View { List { + if showTitle { + Text("One-time invitation link") + .font(.largeTitle) + .bold() + .fixedSize(horizontal: false, vertical: true) + .padding(.vertical) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + } 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).") } + .frame(maxWidth: .infinity, alignment: .leading) .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) } + .modifier(ThemedBackground(grouped: true)) } } struct AddContactLearnMore_Previews: PreviewProvider { static var previews: some View { - AddContactLearnMore() + AddContactLearnMore(showTitle: true) } } diff --git a/apps/ios/Shared/Views/NewChat/AddContactView.swift b/apps/ios/Shared/Views/NewChat/AddContactView.swift deleted file mode 100644 index de8e35d2a6..0000000000 --- a/apps/ios/Shared/Views/NewChat/AddContactView.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// AddContactView.swift -// SimpleX -// -// Created by Evgeny Poberezkin on 29/01/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import SwiftUI -import CoreImage.CIFilterBuiltins -import SimpleXChat - -struct AddContactView: View { - @EnvironmentObject private var chatModel: ChatModel - @Binding var contactConnection: PendingContactConnection? - var connReqInvitation: String - @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false - - var body: some View { - VStack { - List { - Section { - if connReqInvitation != "" { - SimpleXLinkQRCode(uri: connReqInvitation) - } else { - ProgressView() - .progressViewStyle(.circular) - .scaleEffect(2) - .frame(maxWidth: .infinity) - .padding(.vertical) - } - IncognitoToggle(incognitoEnabled: $incognitoDefault) - .disabled(contactConnection == nil) - shareLinkButton(connReqInvitation) - oneTimeLinkLearnMoreButton() - } header: { - Text("1-time link") - } footer: { - sharedProfileInfo(incognitoDefault) - } - } - } - .onAppear { chatModel.connReqInv = connReqInvitation } - .onChange(of: incognitoDefault) { incognito in - Task { - do { - if let contactConn = contactConnection, - let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) { - await MainActor.run { - contactConnection = conn - chatModel.updateContactConnection(conn) - } - } - } catch { - logger.error("apiSetConnectionIncognito error: \(responseError(error))") - } - } - } - } -} - -struct IncognitoToggle: View { - @Binding var incognitoEnabled: Bool - @State private var showIncognitoSheet = false - - var body: some View { - ZStack(alignment: .leading) { - Image(systemName: incognitoEnabled ? "theatermasks.fill" : "theatermasks") - .frame(maxWidth: 24, maxHeight: 24, alignment: .center) - .foregroundColor(incognitoEnabled ? Color.indigo : .secondary) - .font(.system(size: 14)) - Toggle(isOn: $incognitoEnabled) { - HStack(spacing: 6) { - Text("Incognito") - Image(systemName: "info.circle") - .foregroundColor(.accentColor) - .font(.system(size: 14)) - } - .onTapGesture { - showIncognitoSheet = true - } - } - .padding(.leading, 36) - } - .sheet(isPresented: $showIncognitoSheet) { - IncognitoHelp() - } - } -} - -func sharedProfileInfo(_ incognito: Bool) -> Text { - let name = ChatModel.shared.currentUser?.displayName ?? "" - return Text( - incognito - ? "A new random profile will be shared." - : "Your profile **\(name)** will be shared." - ) -} - -func shareLinkButton(_ connReqInvitation: String) -> some View { - Button { - showShareSheet(items: [simplexChatLink(connReqInvitation)]) - } label: { - settingsRow("square.and.arrow.up") { - Text("Share 1-time link") - } - } -} - -func oneTimeLinkLearnMoreButton() -> some View { - NavigationLink { - AddContactLearnMore() - .navigationTitle("One-time invitation link") - .navigationBarTitleDisplayMode(.large) - } label: { - settingsRow("info.circle") { - Text("Learn more") - } - } -} - -struct AddContactView_Previews: PreviewProvider { - static var previews: some View { - AddContactView( - contactConnection: Binding.constant(PendingContactConnection.getSampleData()), - connReqInvitation: "https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FFe5ICmvrm4wkrr6X1LTMii-lhBqLeB76%23MCowBQYDK2VuAyEAdhZZsHpuaAk3Hh1q0uNb_6hGTpuwBIrsp2z9U2T0oC0%3D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAcz6jJk71InuxA0bOX7OUhddfB8Ov7xwQIlIDeXBRZaOntUU4brU5Y3rBzroZBdQJi0FKdtt_D7I%3D%2CMEIwBQYDK2VvAzkA-hDvk1duBi1hlOr08VWSI-Ou4JNNSQjseY69QyKm7Kgg1zZjbpGfyBqSZ2eqys6xtoV4ZtoQUXQ%3D" - ) - } -} diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 6c7919669b..87c0b80372 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct AddGroupView: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false @State private var chat: Chat? @@ -22,7 +23,7 @@ struct AddGroupView: View { @State private var showTakePhoto = false @State private var chosenImage: UIImage? = nil @State private var showInvalidNameAlert = false - @State private var groupLink: String? + @State private var groupLink: CreatedConnLink? @State private var groupLinkMemberRole: GroupMemberRole = .member var body: some View { @@ -34,45 +35,40 @@ struct AddGroupView: View { creatingGroup: true, showFooterCounter: false ) { _ in - dismiss() DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - m.chatId = groupInfo.id + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(groupInfo.id) + } } } + .navigationBarTitleDisplayMode(.inline) } else { GroupLinkView( groupId: groupInfo.groupId, groupLink: $groupLink, groupLinkMemberRole: $groupLinkMemberRole, - showTitle: true, + showTitle: false, creatingGroup: true ) { - dismiss() DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - m.chatId = groupInfo.id + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(groupInfo.id) + } } } + .navigationBarTitle("Group link") } } else { - createGroupView().keyboardPadding() + createGroupView() } } func createGroupView() -> some View { List { Group { - Text("Create secret group") - .font(.largeTitle) - .bold() - .fixedSize(horizontal: false, vertical: true) - .padding(.bottom, 24) - .onTapGesture(perform: hideKeyboard) - ZStack(alignment: .center) { ZStack(alignment: .topTrailing) { - ProfileImage(imageStr: profile.image, color: Color(uiColor: .secondarySystemGroupedBackground)) - .aspectRatio(1, contentMode: .fit) - .frame(maxWidth: 128, maxHeight: 128) + ProfileImage(imageStr: profile.image, size: 128) if profile.image != nil { Button { profile.image = nil @@ -97,7 +93,7 @@ struct AddGroupView: View { Section { groupNameTextField() Button(action: createGroup) { - settingsRow("checkmark", color: .accentColor) { Text("Create group") } + settingsRow("checkmark", color: theme.colors.primary) { Text("Create group") } } .disabled(!canCreateProfile()) IncognitoToggle(incognitoEnabled: $incognitoDefault) @@ -106,6 +102,7 @@ struct AddGroupView: View { sharedGroupProfileInfo(incognitoDefault) Text("Fully decentralized – visible only to members.") } + .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity, alignment: .leading) .onTapGesture(perform: hideKeyboard) } @@ -140,12 +137,16 @@ struct AddGroupView: View { createInvalidNameAlert(mkValidName(profile.displayName), $profile.displayName) } .onChange(of: chosenImage) { image in - if let image = image { - profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500) - } else { - profile.image = nil + Task { + let resized: String? = if let image { + await resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500) + } else { + nil + } + await MainActor.run { profile.image = resized } } } + .modifier(ThemedBackground(grouped: true)) } func groupNameTextField() -> some View { @@ -158,7 +159,7 @@ struct AddGroupView: View { Image(systemName: "exclamationmark.circle").foregroundColor(.red) } } else { - Image(systemName: "pencil").foregroundColor(.secondary) + Image(systemName: "pencil").foregroundColor(theme.colors.secondary) } textField("Enter group name…", text: $profile.displayName) .focused($focusDisplayName) @@ -187,12 +188,10 @@ struct AddGroupView: View { hideKeyboard() do { profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces) + profile.groupPreferences = GroupPreferences(history: GroupPreference(enable: .on)) let gInfo = try apiNewGroup(incognito: incognitoDefault, groupProfile: profile) Task { - let groupMembers = await apiListMembers(gInfo.groupId) - await MainActor.run { - m.groupMembers = groupMembers.map { GMember.init($0) } - } + await m.loadGroupMembers(gInfo) } let c = Chat(chatInfo: .group(groupInfo: gInfo), chatItems: []) m.addChat(c) @@ -201,13 +200,14 @@ struct AddGroupView: View { chat = c } } catch { - dismiss() - AlertManager.shared.showAlert( - Alert( - title: Text("Error creating group"), - message: Text(responseError(error)) + dismissAllSheets(animated: true) { + AlertManager.shared.showAlert( + Alert( + title: Text("Error creating group"), + message: Text(responseError(error)) + ) ) - ) + } } } diff --git a/apps/ios/Shared/Views/NewChat/ConnectViaLinkView.swift b/apps/ios/Shared/Views/NewChat/ConnectViaLinkView.swift deleted file mode 100644 index 9df767485e..0000000000 --- a/apps/ios/Shared/Views/NewChat/ConnectViaLinkView.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// ConnectViaLinkView.swift -// SimpleX (iOS) -// -// Created by Evgeny on 21/09/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import SwiftUI - -enum ConnectViaLinkTab: String { - case scan - case paste -} - -struct ConnectViaLinkView: View { - @State private var selection: ConnectViaLinkTab = connectViaLinkTabDefault.get() - - var body: some View { - TabView(selection: $selection) { - ScanToConnectView() - .tabItem { - Label("Scan QR code", systemImage: "qrcode") - } - .tag(ConnectViaLinkTab.scan) - PasteToConnectView() - .tabItem { - Label("Paste received link", systemImage: "doc.plaintext") - } - .tag(ConnectViaLinkTab.paste) - } - .onChange(of: selection) { _ in - connectViaLinkTabDefault.set(selection) - } - } -} - -struct ConnectViaLinkView_Previews: PreviewProvider { - static var previews: some View { - ConnectViaLinkView() - } -} diff --git a/apps/ios/Shared/Views/NewChat/CreateLinkView.swift b/apps/ios/Shared/Views/NewChat/CreateLinkView.swift deleted file mode 100644 index 3be9e1c3b3..0000000000 --- a/apps/ios/Shared/Views/NewChat/CreateLinkView.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// CreateLinkView.swift -// SimpleX (iOS) -// -// Created by Evgeny on 21/09/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import SwiftUI -import SimpleXChat - -enum CreateLinkTab { - case oneTime - case longTerm - - var title: LocalizedStringKey { - switch self { - case .oneTime: return "One-time invitation link" - case .longTerm: return "Your SimpleX address" - } - } -} - -struct CreateLinkView: View { - @EnvironmentObject var m: ChatModel - @State var selection: CreateLinkTab - @State var connReqInvitation: String = "" - @State var contactConnection: PendingContactConnection? = nil - @State private var creatingConnReq = false - var viaNavLink = false - - var body: some View { - if viaNavLink { - createLinkView() - } else { - NavigationView { - createLinkView() - } - } - } - - private func createLinkView() -> some View { - TabView(selection: $selection) { - AddContactView(contactConnection: $contactConnection, connReqInvitation: connReqInvitation) - .tabItem { - Label( - connReqInvitation == "" - ? "Create one-time invitation link" - : "One-time invitation link", - systemImage: "1.circle" - ) - } - .tag(CreateLinkTab.oneTime) - UserAddressView(viaCreateLinkView: true) - .tabItem { - Label("Your SimpleX address", systemImage: "infinity.circle") - } - .tag(CreateLinkTab.longTerm) - } - .onChange(of: selection) { _ in - if case .oneTime = selection, connReqInvitation == "", contactConnection == nil && !creatingConnReq { - createInvitation() - } - } - .onAppear { m.connReqInv = connReqInvitation } - .onDisappear { m.connReqInv = nil } - .navigationTitle(selection.title) - .navigationBarTitleDisplayMode(.large) - } - - private func createInvitation() { - creatingConnReq = true - Task { - if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) { - await MainActor.run { - m.updateContactConnection(pcc) - connReqInvitation = connReq - contactConnection = pcc - m.connReqInv = connReq - } - } else { - await MainActor.run { - creatingConnReq = false - } - } - } - } -} - -struct CreateLinkView_Previews: PreviewProvider { - static var previews: some View { - CreateLinkView(selection: CreateLinkTab.oneTime) - } -} diff --git a/apps/ios/Shared/Views/NewChat/NewChatButton.swift b/apps/ios/Shared/Views/NewChat/NewChatButton.swift deleted file mode 100644 index 170805b488..0000000000 --- a/apps/ios/Shared/Views/NewChat/NewChatButton.swift +++ /dev/null @@ -1,466 +0,0 @@ -// -// NewChatButton.swift -// SimpleX -// -// Created by Evgeny Poberezkin on 31/01/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import SwiftUI -import SimpleXChat - -enum NewChatAction: Identifiable { - case createLink(link: String, connection: PendingContactConnection) - case connectViaLink - case createGroup - - var id: String { - switch self { - case let .createLink(link, _): return "createLink \(link)" - case .connectViaLink: return "connectViaLink" - case .createGroup: return "createGroup" - } - } -} - -struct NewChatButton: View { - @Binding var showAddChat: Bool - @State private var actionSheet: NewChatAction? - - var body: some View { - Button { showAddChat = true } label: { - Image(systemName: "square.and.pencil") - .resizable() - .scaledToFit() - .frame(width: 24, height: 24) - } - .confirmationDialog("Start a new chat", isPresented: $showAddChat, titleVisibility: .visible) { - Button("Share one-time invitation link") { addContactAction() } - Button("Connect via link / QR code") { actionSheet = .connectViaLink } - Button("Create secret group") { actionSheet = .createGroup } - } - .sheet(item: $actionSheet) { sheet in - switch sheet { - case let .createLink(link, pcc): - CreateLinkView(selection: .oneTime, connReqInvitation: link, contactConnection: pcc) - case .connectViaLink: ConnectViaLinkView() - case .createGroup: AddGroupView() - } - } - } - - func addContactAction() { - Task { - if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) { - await MainActor.run { - ChatModel.shared.updateContactConnection(pcc) - } - actionSheet = .createLink(link: connReq, connection: pcc) - } - } - } -} - -enum PlanAndConnectAlert: Identifiable { - case ownInvitationLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) - case invitationLinkConnecting(connectionLink: String) - case ownContactAddressConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) - case contactAddressConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) - case groupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) - case groupLinkConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) - case groupLinkConnecting(connectionLink: String, groupInfo: GroupInfo?) - - var id: String { - switch self { - case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink)" - case let .invitationLinkConnecting(connectionLink): return "invitationLinkConnecting \(connectionLink)" - case let .ownContactAddressConfirmConnect(connectionLink, _, _): return "ownContactAddressConfirmConnect \(connectionLink)" - case let .contactAddressConnectingConfirmReconnect(connectionLink, _, _): return "contactAddressConnectingConfirmReconnect \(connectionLink)" - case let .groupLinkConfirmConnect(connectionLink, _, _): return "groupLinkConfirmConnect \(connectionLink)" - case let .groupLinkConnectingConfirmReconnect(connectionLink, _, _): return "groupLinkConnectingConfirmReconnect \(connectionLink)" - case let .groupLinkConnecting(connectionLink, _): return "groupLinkConnecting \(connectionLink)" - } - } -} - -func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool) -> Alert { - switch alert { - case let .ownInvitationLinkConfirmConnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Connect to yourself?"), - message: Text("This is your own one-time link!"), - primaryButton: .destructive( - Text(incognito ? "Connect incognito" : "Connect"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) } - ), - secondaryButton: .cancel() - ) - case .invitationLinkConnecting: - return Alert( - title: Text("Already connecting!"), - message: Text("You are already connecting via this one-time link!") - ) - case let .ownContactAddressConfirmConnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Connect to yourself?"), - message: Text("This is your own SimpleX address!"), - primaryButton: .destructive( - Text(incognito ? "Connect incognito" : "Connect"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) } - ), - secondaryButton: .cancel() - ) - case let .contactAddressConnectingConfirmReconnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Repeat connection request?"), - message: Text("You have already requested connection via this address!"), - primaryButton: .destructive( - Text(incognito ? "Connect incognito" : "Connect"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) } - ), - secondaryButton: .cancel() - ) - case let .groupLinkConfirmConnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Join group?"), - message: Text("You will connect to all group members."), - primaryButton: .default( - Text(incognito ? "Join incognito" : "Join"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) } - ), - secondaryButton: .cancel() - ) - case let .groupLinkConnectingConfirmReconnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Repeat join request?"), - message: Text("You are already joining the group via this link!"), - primaryButton: .destructive( - Text(incognito ? "Join incognito" : "Join"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) } - ), - secondaryButton: .cancel() - ) - case let .groupLinkConnecting(_, groupInfo): - if let groupInfo = groupInfo { - return Alert( - title: Text("Group already exists!"), - message: Text("You are already joining the group \(groupInfo.displayName).") - ) - } else { - return Alert( - title: Text("Already joining the group!"), - message: Text("You are already joining the group via this link.") - ) - } - } -} - -enum PlanAndConnectActionSheet: Identifiable { - case askCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan?, title: LocalizedStringKey) - case askCurrentOrIncognitoProfileDestructive(connectionLink: String, connectionPlan: ConnectionPlan, title: LocalizedStringKey) - case askCurrentOrIncognitoProfileConnectContactViaAddress(contact: Contact) - case ownGroupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo) - - var id: String { - switch self { - case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink)" - case let .askCurrentOrIncognitoProfileDestructive(connectionLink, _, _): return "askCurrentOrIncognitoProfileDestructive \(connectionLink)" - case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): return "askCurrentOrIncognitoProfileConnectContactViaAddress \(contact.contactId)" - case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink)" - } - } -} - -func planAndConnectActionSheet(_ sheet: PlanAndConnectActionSheet, dismiss: Bool) -> ActionSheet { - switch sheet { - case let .askCurrentOrIncognitoProfile(connectionLink, connectionPlan, title): - return ActionSheet( - title: Text(title), - buttons: [ - .default(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false) }, - .default(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true) }, - .cancel() - ] - ) - case let .askCurrentOrIncognitoProfileDestructive(connectionLink, connectionPlan, title): - return ActionSheet( - title: Text(title), - buttons: [ - .destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false) }, - .destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true) }, - .cancel() - ] - ) - case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): - return ActionSheet( - title: Text("Connect with \(contact.chatViewName)"), - buttons: [ - .default(Text("Use current profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: false) }, - .default(Text("Use new incognito profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: true) }, - .cancel() - ] - ) - case let .ownGroupLinkConfirmConnect(connectionLink, connectionPlan, incognito, groupInfo): - if let incognito = incognito { - return ActionSheet( - title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"), - buttons: [ - .default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) }, - .destructive(Text(incognito ? "Join incognito" : "Join with current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }, - .cancel() - ] - ) - } else { - return ActionSheet( - title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"), - buttons: [ - .default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) }, - .destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false) }, - .destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true) }, - .cancel() - ] - ) - } - } -} - -func planAndConnect( - _ connectionLink: String, - showAlert: @escaping (PlanAndConnectAlert) -> Void, - showActionSheet: @escaping (PlanAndConnectActionSheet) -> Void, - dismiss: Bool, - incognito: Bool? -) { - Task { - do { - let connectionPlan = try await apiConnectPlan(connReq: connectionLink) - switch connectionPlan { - case let .invitationLink(ilp): - switch ilp { - case .ok: - logger.debug("planAndConnect, .invitationLink, .ok, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) - } else { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link")) - } - case .ownLink: - logger.debug("planAndConnect, .invitationLink, .ownLink, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!")) - } - case let .connecting(contact_): - logger.debug("planAndConnect, .invitationLink, .connecting, incognito=\(incognito?.description ?? "nil")") - if let contact = contact_ { - openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) } - } else { - showAlert(.invitationLinkConnecting(connectionLink: connectionLink)) - } - case let .known(contact): - logger.debug("planAndConnect, .invitationLink, .known, incognito=\(incognito?.description ?? "nil")") - openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) } - } - case let .contactAddress(cap): - switch cap { - case .ok: - logger.debug("planAndConnect, .contactAddress, .ok, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) - } else { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address")) - } - case .ownLink: - logger.debug("planAndConnect, .contactAddress, .ownLink, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!")) - } - case .connectingConfirmReconnect: - logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - showAlert(.contactAddressConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You have already requested connection!\nRepeat connection request?")) - } - case let .connectingProhibit(contact): - logger.debug("planAndConnect, .contactAddress, .connectingProhibit, incognito=\(incognito?.description ?? "nil")") - openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) } - case let .known(contact): - logger.debug("planAndConnect, .contactAddress, .known, incognito=\(incognito?.description ?? "nil")") - openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) } - case let .contactViaAddress(contact): - logger.debug("planAndConnect, .contactAddress, .contactViaAddress, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - connectContactViaAddress_(contact, dismiss: dismiss, incognito: incognito) - } else { - showActionSheet(.askCurrentOrIncognitoProfileConnectContactViaAddress(contact: contact)) - } - } - case let .groupLink(glp): - switch glp { - case .ok: - if let incognito = incognito { - showAlert(.groupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Join group")) - } - case let .ownLink(groupInfo): - logger.debug("planAndConnect, .groupLink, .ownLink, incognito=\(incognito?.description ?? "nil")") - showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo)) - case .connectingConfirmReconnect: - logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - showAlert(.groupLinkConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You are already joining the group!\nRepeat join request?")) - } - case let .connectingProhibit(groupInfo_): - logger.debug("planAndConnect, .groupLink, .connectingProhibit, incognito=\(incognito?.description ?? "nil")") - showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_)) - case let .known(groupInfo): - logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")") - openKnownGroup(groupInfo, dismiss: dismiss) { AlertManager.shared.showAlert(groupAlreadyExistsAlert(groupInfo)) } - } - } - } catch { - logger.debug("planAndConnect, plan error") - if let incognito = incognito { - connectViaLink(connectionLink, connectionPlan: nil, dismiss: dismiss, incognito: incognito) - } else { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: nil, title: "Connect via link")) - } - } - } -} - -private func connectContactViaAddress_(_ contact: Contact, dismiss: Bool, incognito: Bool) { - Task { - if dismiss { - DispatchQueue.main.async { - dismissAllSheets(animated: true) - } - } - _ = await connectContactViaAddress(contact.contactId, incognito) - } -} - -private func connectViaLink(_ connectionLink: String, connectionPlan: ConnectionPlan?, dismiss: Bool, incognito: Bool) { - Task { - if let (connReqType, pcc) = await apiConnect(incognito: incognito, connReq: connectionLink) { - await MainActor.run { - ChatModel.shared.updateContactConnection(pcc) - } - let crt: ConnReqType - if let plan = connectionPlan { - crt = planToConnReqType(plan) - } else { - crt = connReqType - } - DispatchQueue.main.async { - if dismiss { - dismissAllSheets(animated: true) { - AlertManager.shared.showAlert(connReqSentAlert(crt)) - } - } else { - AlertManager.shared.showAlert(connReqSentAlert(crt)) - } - } - } else { - if dismiss { - DispatchQueue.main.async { - dismissAllSheets(animated: true) - } - } - } - } -} - -func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) { - Task { - let m = ChatModel.shared - if let c = m.getContactChat(contact.contactId) { - DispatchQueue.main.async { - if dismiss { - dismissAllSheets(animated: true) { - m.chatId = c.id - showAlreadyExistsAlert?() - } - } else { - m.chatId = c.id - showAlreadyExistsAlert?() - } - } - } - } -} - -func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) { - Task { - let m = ChatModel.shared - if let g = m.getGroupChat(groupInfo.groupId) { - DispatchQueue.main.async { - if dismiss { - dismissAllSheets(animated: true) { - m.chatId = g.id - showAlreadyExistsAlert?() - } - } else { - m.chatId = g.id - showAlreadyExistsAlert?() - } - } - } - } -} - -func contactAlreadyConnectingAlert(_ contact: Contact) -> Alert { - mkAlert( - title: "Contact already exists", - message: "You are already connecting to \(contact.displayName)." - ) -} - -func groupAlreadyExistsAlert(_ groupInfo: GroupInfo) -> Alert { - mkAlert( - title: "Group already exists", - message: "You are already in group \(groupInfo.displayName)." - ) -} - -enum ConnReqType: Equatable { - case invitation - case contact - case groupLink - - var connReqSentText: LocalizedStringKey { - switch self { - case .invitation: return "You will be connected when your contact's device is online, please wait or check later!" - case .contact: return "You will be connected when your connection request is accepted, please wait or check later!" - case .groupLink: return "You will be connected when group link host's device is online, please wait or check later!" - } - } -} - -private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType { - switch connectionPlan { - case .invitationLink: return .invitation - case .contactAddress: return .contact - case .groupLink: return .groupLink - } -} - -func connReqSentAlert(_ type: ConnReqType) -> Alert { - return mkAlert( - title: "Connection request sent!", - message: type.connReqSentText - ) -} - -struct NewChatButton_Previews: PreviewProvider { - static var previews: some View { - NewChatButton(showAddChat: Binding.constant(false)) - } -} diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift new file mode 100644 index 0000000000..e5263813fa --- /dev/null +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -0,0 +1,497 @@ +// +// NewChatMenuButton.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 28.11.2023. +// Copyright © 2023 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +enum ContactType: Int { + case card, request, recent, chatDeleted, unlisted +} + +struct NewChatMenuButton: View { + // do not use chatModel here because it prevents showing AddGroupMembersView after group creation and QR code after link creation on iOS 16 +// @EnvironmentObject var chatModel: ChatModel + @State private var showNewChatSheet = false + @State private var alert: SomeAlert? = nil + + var body: some View { + Button { + showNewChatSheet = true + } label: { + Image(systemName: "square.and.pencil") + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + } + .appSheet(isPresented: $showNewChatSheet) { + NewChatSheet() + .environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil) + } + .alert(item: $alert) { a in + return a.alert + } + } +} + +private var indent: CGFloat = 36 + +struct NewChatSheet: View { + @EnvironmentObject var theme: AppTheme + @State private var baseContactTypes: [ContactType] = [.card, .request, .recent] + @EnvironmentObject var chatModel: ChatModel + @State private var searchMode = false + @FocusState var searchFocussed: Bool + @State private var searchText = "" + @State private var searchShowingSimplexLink = false + @State private var searchChatFilteredBySimplexLink: String? = nil + @State private var alert: SomeAlert? + + // Sheet height management + @State private var isAddContactActive = false + @State private var isScanPasteLinkActive = false + @State private var isLargeSheet = false + @State private var allowSmallSheet = true + + @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true + + var body: some View { + let showArchive = !filterContactTypes(chats: chatModel.chats, contactTypes: [.chatDeleted]).isEmpty + let v = NavigationView { + viewBody(showArchive) + .navigationTitle("New message") + .navigationBarTitleDisplayMode(.large) + .navigationBarHidden(searchMode) + .modifier(ThemedBackground(grouped: true)) + .alert(item: $alert) { a in + return a.alert + } + } + if #available(iOS 16.0, *), oneHandUI { + let sheetHeight: CGFloat = showArchive ? 575 : 500 + v.presentationDetents( + allowSmallSheet ? [.height(sheetHeight), .large] : [.large], + selection: Binding( + get: { isLargeSheet || !allowSmallSheet ? .large : .height(sheetHeight) }, + set: { isLargeSheet = $0 == .large } + ) + ) + } else { + v + } + } + + private func viewBody(_ showArchive: Bool) -> some View { + List { + HStack { + ContactsListSearchBar( + searchMode: $searchMode, + searchFocussed: $searchFocussed, + searchText: $searchText, + searchShowingSimplexLink: $searchShowingSimplexLink, + searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink + ) + .frame(maxWidth: .infinity) + } + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + + if (searchText.isEmpty) { + Section { + NavigationLink(isActive: $isAddContactActive) { + NewChatView(selection: .invite) + .navigationTitle("New chat") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + navigateOnTap(Label("Create 1-time link", systemImage: "link.badge.plus")) { + isAddContactActive = true + } + } + NavigationLink(isActive: $isScanPasteLinkActive) { + NewChatView(selection: .connect, showQRCodeScanner: true) + .navigationTitle("New chat") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + navigateOnTap(Label("Scan / Paste link", systemImage: "qrcode")) { + isScanPasteLinkActive = true + } + } + NavigationLink { + AddGroupView() + .navigationTitle("Create secret group") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + Label("Create group", systemImage: "person.2.circle.fill") + } + } + + if (showArchive) { + Section { + NavigationLink { + DeletedChats() + } label: { + newChatActionButton("archivebox", color: theme.colors.secondary) { Text("Archived contacts") } + } + } + } + } + + ContactsList( + baseContactTypes: $baseContactTypes, + searchMode: $searchMode, + searchText: $searchText, + header: "Your Contacts", + searchFocussed: $searchFocussed, + searchShowingSimplexLink: $searchShowingSimplexLink, + searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink, + showDeletedChatIcon: true + ) + } + } + + /// Extends label's tap area to match `.insetGrouped` list row insets + private func navigateOnTap<L: View>(_ label: L, setActive: @escaping () -> Void) -> some View { + label + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.leading, 16).padding(.vertical, 8).padding(.trailing, 32) + .contentShape(Rectangle()) + .onTapGesture { + isLargeSheet = true + DispatchQueue.main.async { + allowSmallSheet = false + setActive() + } + } + .padding(.leading, -16).padding(.vertical, -8).padding(.trailing, -32) + } + + func newChatActionButton<Content : View>(_ icon: String, color: Color/* = .secondary*/, content: @escaping () -> Content) -> some View { + ZStack(alignment: .leading) { + Image(systemName: icon) + .resizable() + .scaledToFit() + .frame(maxWidth: 24, maxHeight: 24, alignment: .center) + .symbolRenderingMode(.monochrome) + .foregroundColor(color) + content().foregroundColor(theme.colors.onBackground).padding(.leading, indent) + } + } +} + +func chatContactType(_ chat: Chat) -> ContactType { + switch chat.chatInfo { + case .contactRequest: + return .request + case let .direct(contact): + if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active { + return .card + } else if contact.chatDeleted { + return .chatDeleted + } else if contact.contactStatus == .active { + return .recent + } else { + return .unlisted + } + default: + return .unlisted + } +} + +private func filterContactTypes(chats: [Chat], contactTypes: [ContactType]) -> [Chat] { + return chats.filter { chat in + contactTypes.contains(chatContactType(chat)) + } +} + +struct ContactsList: View { + @EnvironmentObject var theme: AppTheme + @EnvironmentObject var chatModel: ChatModel + @Binding var baseContactTypes: [ContactType] + @Binding var searchMode: Bool + @Binding var searchText: String + var header: String? = nil + @FocusState.Binding var searchFocussed: Bool + @Binding var searchShowingSimplexLink: Bool + @Binding var searchChatFilteredBySimplexLink: String? + var showDeletedChatIcon: Bool + @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false + + var body: some View { + let contactTypes = contactTypesSearchTargets(baseContactTypes: baseContactTypes, searchEmpty: searchText.isEmpty) + let contactChats = filterContactTypes(chats: chatModel.chats, contactTypes: contactTypes) + let filteredContactChats = filteredContactChats( + showUnreadAndFavorites: showUnreadAndFavorites, + searchShowingSimplexLink: searchShowingSimplexLink, + searchChatFilteredBySimplexLink: searchChatFilteredBySimplexLink, + searchText: searchText, + contactChats: contactChats + ) + + if !filteredContactChats.isEmpty { + Section(header: Group { + if let header = header { + Text(header) + .textCase(.uppercase) + .foregroundColor(theme.colors.secondary) + } + } + ) { + ForEach(filteredContactChats, id: \.viewId) { chat in + ContactListNavLink(chat: chat, showDeletedChatIcon: showDeletedChatIcon) + .disabled(chatModel.chatRunning != true) + } + } + } + + if filteredContactChats.isEmpty && !contactChats.isEmpty { + noResultSection(text: "No filtered contacts") + } else if contactChats.isEmpty { + noResultSection(text: "No contacts") + } + } + + private func noResultSection(text: String) -> some View { + Section { + Text(text) + .foregroundColor(theme.colors.secondary) + .frame(maxWidth: .infinity, alignment: .center) + + } + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 7, leading: 0, bottom: 7, trailing: 0)) + } + + private func contactTypesSearchTargets(baseContactTypes: [ContactType], searchEmpty: Bool) -> [ContactType] { + if baseContactTypes.contains(.chatDeleted) || searchEmpty { + return baseContactTypes + } else { + return baseContactTypes + [.chatDeleted] + } + } + + private func chatsByTypeComparator(chat1: Chat, chat2: Chat) -> Bool { + let chat1Type = chatContactType(chat1) + let chat2Type = chatContactType(chat2) + + if chat1Type.rawValue < chat2Type.rawValue { + return true + } else if chat1Type.rawValue > chat2Type.rawValue { + return false + } else { + return chat2.chatInfo.chatTs < chat1.chatInfo.chatTs + } + } + + private func filterChat(chat: Chat, searchText: String, showUnreadAndFavorites: Bool) -> Bool { + var meetsPredicate = true + let s = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + let cInfo = chat.chatInfo + + if !searchText.isEmpty { + if (!cInfo.chatViewName.lowercased().contains(searchText.lowercased())) { + if case let .direct(contact) = cInfo { + meetsPredicate = contact.profile.displayName.lowercased().contains(s) || contact.fullName.lowercased().contains(s) + } else { + meetsPredicate = false + } + } + } + + if showUnreadAndFavorites { + meetsPredicate = meetsPredicate && (cInfo.chatSettings?.favorite ?? false) + } + + return meetsPredicate + } + + func filteredContactChats( + showUnreadAndFavorites: Bool, + searchShowingSimplexLink: Bool, + searchChatFilteredBySimplexLink: String?, + searchText: String, + contactChats: [Chat] + ) -> [Chat] { + let linkChatId = searchChatFilteredBySimplexLink + let s = searchShowingSimplexLink ? "" : searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + + let filteredChats: [Chat] + + if let linkChatId = linkChatId { + filteredChats = contactChats.filter { $0.id == linkChatId } + } else { + filteredChats = contactChats.filter { chat in + filterChat(chat: chat, searchText: s, showUnreadAndFavorites: showUnreadAndFavorites) + } + } + + return filteredChats.sorted(by: chatsByTypeComparator) + } +} + +struct ContactsListSearchBar: View { + @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme + @Binding var searchMode: Bool + @FocusState.Binding var searchFocussed: Bool + @Binding var searchText: String + @Binding var searchShowingSimplexLink: Bool + @Binding var searchChatFilteredBySimplexLink: String? + @State private var ignoreSearchTextChange = false + @State private var alert: PlanAndConnectAlert? + @State private var sheet: PlanAndConnectActionSheet? + @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false + + var body: some View { + HStack(spacing: 12) { + HStack(spacing: 4) { + Spacer() + .frame(width: 8) + Image(systemName: "magnifyingglass") + .resizable() + .scaledToFit() + .frame(width: 16, height: 16) + TextField("Search or paste SimpleX link", text: $searchText) + .foregroundColor(searchShowingSimplexLink ? theme.colors.secondary : theme.colors.onBackground) + .disabled(searchShowingSimplexLink) + .focused($searchFocussed) + .frame(maxWidth: .infinity) + if !searchText.isEmpty { + Image(systemName: "xmark.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 16, height: 16) + .onTapGesture { + searchText = "" + } + } + } + .padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7)) + .foregroundColor(theme.colors.secondary) + .background(Color(uiColor: .secondarySystemGroupedBackground)) + .cornerRadius(10.0) + + if searchFocussed { + Text("Cancel") + .foregroundColor(theme.colors.primary) + .onTapGesture { + searchText = "" + searchFocussed = false + } + } else if m.chats.count > 0 { + toggleFilterButton() + } + } + .padding(.top, 24) + .onChange(of: searchFocussed) { sf in + withAnimation { searchMode = sf } + } + .onChange(of: searchText) { t in + if ignoreSearchTextChange { + ignoreSearchTextChange = false + } else { + if let link = strHasSingleSimplexLink(t.trimmingCharacters(in: .whitespaces)) { // if SimpleX link is pasted, show connection dialogue + searchFocussed = false + if case let .simplexLink(linkType, _, smpHosts) = link.format { + ignoreSearchTextChange = true + searchText = simplexLinkText(linkType, smpHosts) + } + searchShowingSimplexLink = true + searchChatFilteredBySimplexLink = nil + connect(link.text) + } else { + if t != "" { // if some other text is pasted, enter search mode + searchFocussed = true + } + searchShowingSimplexLink = false + searchChatFilteredBySimplexLink = nil + } + } + } + .alert(item: $alert) { a in + planAndConnectAlert(a, dismiss: true, cleanup: { searchText = "" }) + } + .actionSheet(item: $sheet) { s in + planAndConnectActionSheet(s, dismiss: true, cleanup: { searchText = "" }) + } + } + + private func toggleFilterButton() -> some View { + ZStack { + Color.clear + .frame(width: 22, height: 22) + Image(systemName: showUnreadAndFavorites ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease") + .resizable() + .scaledToFit() + .foregroundColor(showUnreadAndFavorites ? theme.colors.primary : theme.colors.secondary) + .frame(width: showUnreadAndFavorites ? 22 : 16, height: showUnreadAndFavorites ? 22 : 16) + .onTapGesture { + showUnreadAndFavorites = !showUnreadAndFavorites + } + } + } + + private func connect(_ link: String) { + planAndConnect( + link, + showAlert: { alert = $0 }, + showActionSheet: { sheet = $0 }, + dismiss: true, + incognito: nil, + filterKnownContact: { searchChatFilteredBySimplexLink = $0.id } + ) + } +} + + +struct DeletedChats: View { + @State private var baseContactTypes: [ContactType] = [.chatDeleted] + @State private var searchMode = false + @FocusState var searchFocussed: Bool + @State private var searchText = "" + @State private var searchShowingSimplexLink = false + @State private var searchChatFilteredBySimplexLink: String? = nil + + var body: some View { + List { + ContactsListSearchBar( + searchMode: $searchMode, + searchFocussed: $searchFocussed, + searchText: $searchText, + searchShowingSimplexLink: $searchShowingSimplexLink, + searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink + ) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .frame(maxWidth: .infinity) + + ContactsList( + baseContactTypes: $baseContactTypes, + searchMode: $searchMode, + searchText: $searchText, + searchFocussed: $searchFocussed, + searchShowingSimplexLink: $searchShowingSimplexLink, + searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink, + showDeletedChatIcon: false + ) + } + .navigationTitle("Archived contacts") + .navigationBarTitleDisplayMode(.large) + .navigationBarHidden(searchMode) + .modifier(ThemedBackground(grouped: true)) + + } +} + +#Preview { + NewChatMenuButton() +} diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift new file mode 100644 index 0000000000..110eda7882 --- /dev/null +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -0,0 +1,1334 @@ +// +// NewChatView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 28.11.2023. +// Copyright © 2023 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat +import CodeScanner +import AVFoundation +import SimpleXChat + +struct SomeAlert: Identifiable { + var alert: Alert + var id: String +} + +struct SomeActionSheet: Identifiable { + var actionSheet: ActionSheet + var id: String +} + +struct SomeSheet<Content: View>: Identifiable { + @ViewBuilder var content: Content + var id: String + var fraction = 0.4 +} + +private enum NewChatViewAlert: Identifiable { + case planAndConnectAlert(alert: PlanAndConnectAlert) + case newChatSomeAlert(alert: SomeAlert) + var id: String { + switch self { + case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)" + case let .newChatSomeAlert(alert): return "newChatSomeAlert \(alert.id)" + } + } +} + +enum NewChatOption: Identifiable { + case invite + case connect + + var id: Self { self } +} + +func showKeepInvitationAlert() { + if let showingInvitation = ChatModel.shared.showingInvitation, + !showingInvitation.connChatUsed { + showAlert( + NSLocalizedString("Keep unused invitation?", comment: "alert title"), + message: NSLocalizedString("You can view invitation link again in connection details.", comment: "alert message"), + actions: {[ + UIAlertAction( + title: NSLocalizedString("Keep", comment: "alert action"), + style: .default + ), + UIAlertAction( + title: NSLocalizedString("Delete", comment: "alert action"), + style: .destructive, + handler: { _ in + Task { + await deleteChat(Chat( + chatInfo: .contactConnection(contactConnection: showingInvitation.pcc), + chatItems: [] + )) + } + } + ) + ]} + ) + } + ChatModel.shared.showingInvitation = nil +} + +struct NewChatView: View { + @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme + @State var selection: NewChatOption + @State var showQRCodeScanner = false + @State private var invitationUsed: Bool = false + @State private var connLinkInvitation: CreatedConnLink = CreatedConnLink(connFullLink: "", connShortLink: nil) + @State private var showShortLink = true + @State private var creatingConnReq = false + @State var choosingProfile = false + @State private var pastedLink: String = "" + @State private var alert: NewChatViewAlert? + @State private var contactConnection: PendingContactConnection? = nil + + 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 + } + } + + VStack { + // it seems there's a bug in iOS 15 if several views in switch (or if-else) statement have different transitions + // https://developer.apple.com/forums/thread/714977?answerId=731615022#731615022 + if case .invite = selection { + prepareAndInviteView() + .transition(.move(edge: .leading)) + .onAppear { + createInvitation() + } + } + if case .connect = selection { + ConnectView(showQRCodeScanner: $showQRCodeScanner, pastedLink: $pastedLink, alert: $alert) + .transition(.move(edge: .trailing)) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .modifier(ThemedBackground(grouped: true)) + .background( + // Rectangle is needed for swipe gesture to work on mostly empty views (creatingLinkProgressView and retryButton) + Rectangle() + .fill(theme.base == DefaultTheme.LIGHT ? theme.colors.background.asGroupedBackground(theme.base.mode) : theme.colors.background) + ) + .animation(.easeInOut(duration: 0.3333), value: selection) + .gesture(DragGesture(minimumDistance: 20.0, coordinateSpace: .local) + .onChanged { value in + switch(value.translation.width, value.translation.height) { + case (...0, -30...30): // left swipe + if selection == .invite { + selection = .connect + } + case (0..., -30...30): // right swipe + if selection == .connect { + selection = .invite + } + default: () + } + } + ) + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + InfoSheetButton { + AddContactLearnMore(showTitle: true) + } + } + } + .modifier(ThemedBackground(grouped: true)) + .onChange(of: invitationUsed) { used in + if used && !(m.showingInvitation?.connChatUsed ?? true) { + m.markShowingInvitationUsed() + } + } + .onDisappear { + if !choosingProfile { + showKeepInvitationAlert() + contactConnection = nil + } + } + .alert(item: $alert) { a in + switch(a) { + case let .planAndConnectAlert(alert): + return planAndConnectAlert(alert, dismiss: true, cleanup: { pastedLink = "" }) + case let .newChatSomeAlert(a): + return a.alert + } + } + } + + private func prepareAndInviteView() -> some View { + ZStack { // ZStack is needed for views to not make transitions between each other + if connLinkInvitation.connFullLink != "" { + InviteView( + invitationUsed: $invitationUsed, + contactConnection: $contactConnection, + connLinkInvitation: $connLinkInvitation, + showShortLink: $showShortLink, + choosingProfile: $choosingProfile + ) + } else if creatingConnReq { + creatingLinkProgressView() + } else { + retryButton() + } + } + } + + private func createInvitation() { + if connLinkInvitation.connFullLink == "" && contactConnection == nil && !creatingConnReq { + creatingConnReq = true + Task { + _ = try? await Task.sleep(nanoseconds: 250_000000) + let (r, apiAlert) = await apiAddContact(incognito: incognitoGroupDefault.get()) + if let (connLink, pcc) = r { + await MainActor.run { + m.updateContactConnection(pcc) + m.showingInvitation = ShowingInvitation(pcc: pcc, connChatUsed: false) + connLinkInvitation = connLink + contactConnection = pcc + } + } else { + await MainActor.run { + creatingConnReq = false + if let apiAlert = apiAlert { + alert = .newChatSomeAlert(alert: SomeAlert(alert: apiAlert, id: "createInvitation error")) + } + } + } + } + } + } + + // Rectangle here and in retryButton are needed for gesture to work + private func creatingLinkProgressView() -> some View { + ProgressView("Creating link…") + .progressViewStyle(.circular) + } + + private func retryButton() -> some View { + Button(action: createInvitation) { + VStack(spacing: 6) { + Image(systemName: "arrow.counterclockwise") + Text("Retry") + } + } + } +} + +private func incognitoProfileImage() -> some View { + Image(systemName: "theatermasks.fill") + .resizable() + .scaledToFit() + .frame(width: 30) + .foregroundColor(.indigo) +} + +private struct InviteView: View { + @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @Binding var invitationUsed: Bool + @Binding var contactConnection: PendingContactConnection? + @Binding var connLinkInvitation: CreatedConnLink + @Binding var showShortLink: Bool + @Binding var choosingProfile: Bool + + @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)) { + shareLinkView() + } + .listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 10)) + + qrCodeView() + if let selectedProfile = chatModel.currentUser { + Section { + NavigationLink { + ActiveProfilePicker( + contactConnection: $contactConnection, + connLinkInvitation: $connLinkInvitation, + incognitoEnabled: $incognitoDefault, + choosingProfile: $choosingProfile, + selectedProfile: selectedProfile + ) + } label: { + HStack { + if incognitoDefault { + incognitoProfileImage() + Text("Incognito") + } else { + ProfileImage(imageStr: chatModel.currentUser?.image, size: 30) + Text(chatModel.currentUser?.chatViewName ?? "") + } + } + } + } header: { + Text("Share profile").foregroundColor(theme.colors.secondary) + } footer: { + if incognitoDefault { + Text("A new random profile will be shared.") + } + } + } + } + .onChange(of: incognitoDefault) { incognito in + setInvitationUsed() + } + .onChange(of: chatModel.currentUser) { u in + setInvitationUsed() + } + } + + private func shareLinkView() -> some View { + HStack { + let link = connLinkInvitation.simplexChatUri(short: showShortLink) + linkTextView(link) + Button { + showShareSheet(items: [link]) + setInvitationUsed() + } label: { + Image(systemName: "square.and.arrow.up") + .padding(.top, -7) + } + } + .frame(maxWidth: .infinity) + } + + private func qrCodeView() -> some View { + Section { + SimpleXCreatedLinkQRCode(link: connLinkInvitation, short: $showShortLink, onShare: setInvitationUsed) + .id("simplex-qrcode-view-for-\(connLinkInvitation.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: { + ToggleShortLinkHeader(text: Text("Or show this code"), link: connLinkInvitation, short: $showShortLink) + } + } + + private func setInvitationUsed() { + if !invitationUsed { + invitationUsed = true + } + } +} + +private enum ProfileSwitchStatus { + case switchingUser + case switchingIncognito + case idle +} + +private struct ActiveProfilePicker: View { + @Environment(\.dismiss) var dismiss + @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @Binding var contactConnection: PendingContactConnection? + @Binding var connLinkInvitation: CreatedConnLink + @Binding var incognitoEnabled: Bool + @Binding var choosingProfile: Bool + @State private var alert: SomeAlert? + @State private var profileSwitchStatus: ProfileSwitchStatus = .idle + @State private var switchingProfileByTimeout = false + @State private var lastSwitchingProfileByTimeoutCall: Double? + @State private var profiles: [User] = [] + @State private var searchTextOrPassword = "" + @State private var showIncognitoSheet = false + @State private var incognitoFirst: Bool = false + @State var selectedProfile: User + var trimmedSearchTextOrPassword: String { searchTextOrPassword.trimmingCharacters(in: .whitespaces)} + + var body: some View { + viewBody() + .navigationTitle("Select chat profile") + .searchable(text: $searchTextOrPassword, placement: .navigationBarDrawer(displayMode: .always)) + .autocorrectionDisabled(true) + .navigationBarTitleDisplayMode(.large) + .onAppear { + profiles = chatModel.users + .map { $0.user } + .sorted { u, _ in u.activeUser } + } + .onChange(of: incognitoEnabled) { incognito in + if profileSwitchStatus != .switchingIncognito { + return + } + + Task { + do { + if let contactConn = contactConnection, + let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) { + await MainActor.run { + contactConnection = conn + chatModel.updateContactConnection(conn) + profileSwitchStatus = .idle + dismiss() + } + } + } catch { + profileSwitchStatus = .idle + incognitoEnabled = !incognito + logger.error("apiSetConnectionIncognito error: \(responseError(error))") + let err = getErrorAlert(error, "Error changing to incognito!") + + alert = SomeAlert( + alert: Alert( + title: Text(err.title), + message: Text(err.message ?? "Error: \(responseError(error))") + ), + id: "setConnectionIncognitoError" + ) + } + } + } + .onChange(of: profileSwitchStatus) { sp in + if sp != .idle { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + switchingProfileByTimeout = profileSwitchStatus != .idle + } + } else { + switchingProfileByTimeout = false + } + } + .onChange(of: selectedProfile) { profile in + if (profileSwitchStatus != .switchingUser) { + return + } + Task { + do { + if let contactConn = contactConnection { + let conn = try await apiChangeConnectionUser(connId: contactConn.pccConnId, userId: profile.userId) + await MainActor.run { + contactConnection = conn + connLinkInvitation = conn.connLinkInv ?? CreatedConnLink(connFullLink: "", connShortLink: nil) + incognitoEnabled = false + chatModel.updateContactConnection(conn) + } + do { + try await changeActiveUserAsync_(profile.userId, viewPwd: profile.hidden ? trimmedSearchTextOrPassword : nil ) + await MainActor.run { + profileSwitchStatus = .idle + dismiss() + } + } catch { + await MainActor.run { + profileSwitchStatus = .idle + alert = SomeAlert( + alert: Alert( + title: Text("Error switching profile"), + message: Text("Your connection was moved to \(profile.chatViewName) but an unexpected error occurred while redirecting you to the profile.") + ), + id: "switchingProfileError" + ) + } + } + } + } catch { + await MainActor.run { + profileSwitchStatus = .idle + if let currentUser = chatModel.currentUser { + selectedProfile = currentUser + } + let err = getErrorAlert(error, "Error changing connection profile") + alert = SomeAlert( + alert: Alert( + title: Text(err.title), + message: Text(err.message ?? "Error: \(responseError(error))") + ), + id: "changeConnectionUserError" + ) + } + } + } + } + .alert(item: $alert) { a in + a.alert + } + .onAppear { + incognitoFirst = incognitoEnabled + choosingProfile = true + } + .onDisappear { + choosingProfile = false + } + .sheet(isPresented: $showIncognitoSheet) { + IncognitoHelp() + } + } + + + @ViewBuilder private func viewBody() -> some View { + profilePicker() + .allowsHitTesting(!switchingProfileByTimeout) + .modifier(ThemedBackground(grouped: true)) + .overlay { + if switchingProfileByTimeout { + ProgressView() + .scaleEffect(2) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + } + + private func filteredProfiles() -> [User] { + let s = trimmedSearchTextOrPassword + let lower = s.localizedLowercase + + return profiles.filter { u in + if (u.activeUser || !u.hidden) && (s == "" || u.chatViewName.localizedLowercase.contains(lower)) { + return true + } + return correctPassword(u, s) + } + } + + private func profilerPickerUserOption(_ user: User) -> some View { + Button { + if selectedProfile == user && incognitoEnabled { + incognitoEnabled = false + profileSwitchStatus = .switchingIncognito + } else if selectedProfile != user { + selectedProfile = user + profileSwitchStatus = .switchingUser + } + } label: { + HStack { + ProfileImage(imageStr: user.image, size: 30) + .padding(.trailing, 2) + Text(user.chatViewName) + .foregroundColor(theme.colors.onBackground) + .lineLimit(1) + Spacer() + if selectedProfile == user, !incognitoEnabled { + Image(systemName: "checkmark") + .resizable().scaledToFit().frame(width: 16) + .foregroundColor(theme.colors.primary) + } + } + } + } + + @ViewBuilder private func profilePicker() -> some View { + let incognitoOption = Button { + if !incognitoEnabled { + incognitoEnabled = true + profileSwitchStatus = .switchingIncognito + } + } label : { + HStack { + incognitoProfileImage() + Text("Incognito") + .foregroundColor(theme.colors.onBackground) + Image(systemName: "info.circle") + .foregroundColor(theme.colors.primary) + .font(.system(size: 14)) + .onTapGesture { + showIncognitoSheet = true + } + Spacer() + if incognitoEnabled { + Image(systemName: "checkmark") + .resizable().scaledToFit().frame(width: 16) + .foregroundColor(theme.colors.primary) + } + } + } + + List { + let filteredProfiles = filteredProfiles() + let activeProfile = filteredProfiles.first { u in u.activeUser } + + if let selectedProfile = activeProfile { + let otherProfiles = filteredProfiles.filter { u in u.userId != activeProfile?.userId } + + if incognitoFirst { + incognitoOption + profilerPickerUserOption(selectedProfile) + } else { + profilerPickerUserOption(selectedProfile) + incognitoOption + } + + ForEach(otherProfiles) { p in + profilerPickerUserOption(p) + } + } else { + incognitoOption + ForEach(filteredProfiles) { p in + profilerPickerUserOption(p) + } + } + } + .opacity(switchingProfileByTimeout ? 0.4 : 1) + } +} + +private struct ConnectView: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme + @Binding var showQRCodeScanner: Bool + @Binding var pastedLink: String + @Binding var alert: NewChatViewAlert? + @State private var sheet: PlanAndConnectActionSheet? + @State private var pasteboardHasStrings = UIPasteboard.general.hasStrings + + var body: some View { + List { + Section(header: Text("Paste the link you received").foregroundColor(theme.colors.secondary)) { + pasteLinkView() + } + Section(header: Text("Or scan QR code").foregroundColor(theme.colors.secondary)) { + ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processQRCode) + } + } + .actionSheet(item: $sheet) { s in + planAndConnectActionSheet(s, dismiss: true, cleanup: { pastedLink = "" }) + } + } + + @ViewBuilder private func pasteLinkView() -> some View { + if pastedLink == "" { + Button { + if let str = UIPasteboard.general.string { + if let link = strHasSingleSimplexLink(str.trimmingCharacters(in: .whitespaces)) { + pastedLink = link.text + // It would be good to hide it, but right now it is not clear how to release camera in CodeScanner + // https://github.com/twostraws/CodeScanner/issues/121 + // No known tricks worked (changing view ID, wrapping it in another view, etc.) + // showQRCodeScanner = false + connect(pastedLink) + } else { + alert = .newChatSomeAlert(alert: SomeAlert( + alert: mkAlert(title: "Invalid link", message: "The text you pasted is not a SimpleX link."), + id: "pasteLinkView: code is not a SimpleX link" + )) + } + } + } label: { + Text("Tap to paste link") + } + .disabled(!pasteboardHasStrings) + .frame(maxWidth: .infinity, alignment: .center) + } else { + linkTextView(pastedLink) + } + } + + private func processQRCode(_ resp: Result<ScanResult, ScanError>) { + switch resp { + case let .success(r): + let link = r.string + if strIsSimplexLink(r.string) { + connect(link) + } else { + alert = .newChatSomeAlert(alert: SomeAlert( + alert: mkAlert(title: "Invalid QR code", message: "The code you scanned is not a SimpleX link QR code."), + id: "processQRCode: code is not a SimpleX link" + )) + } + case let .failure(e): + logger.error("processQRCode QR code error: \(e.localizedDescription)") + alert = .newChatSomeAlert(alert: SomeAlert( + alert: mkAlert(title: "Invalid QR code", message: "Error scanning code: \(e.localizedDescription)"), + id: "processQRCode: failure" + )) + } + } + + private func connect(_ link: String) { + planAndConnect( + link, + showAlert: { alert = .planAndConnectAlert(alert: $0) }, + showActionSheet: { sheet = $0 }, + dismiss: true, + incognito: nil + ) + } +} + +struct ScannerInView: View { + @Binding var showQRCodeScanner: Bool + let processQRCode: (_ resp: Result<ScanResult, ScanError>) -> Void + @State private var cameraAuthorizationStatus: AVAuthorizationStatus? + var scanMode: ScanMode = .continuous + + var body: some View { + Group { + if showQRCodeScanner, case .authorized = cameraAuthorizationStatus { + CodeScannerView(codeTypes: [.qr], scanMode: scanMode, completion: processQRCode) + .aspectRatio(1, contentMode: .fit) + .cornerRadius(12) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .padding(.horizontal) + } else { + Button { + switch cameraAuthorizationStatus { + case .notDetermined: askCameraAuthorization { showQRCodeScanner = true } + case .restricted: () + case .denied: UIApplication.shared.open(appSettingsURL) + case .authorized: showQRCodeScanner = true + default: askCameraAuthorization { showQRCodeScanner = true } + } + } label: { + ZStack { + Rectangle() + .aspectRatio(contentMode: .fill) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .foregroundColor(Color.clear) + switch cameraAuthorizationStatus { + case .authorized, nil: EmptyView() + case .restricted: Text("Camera not available") + case .denied: Label("Enable camera access", systemImage: "camera") + default: Label("Tap to scan", systemImage: "qrcode") + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .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)) + .disabled(cameraAuthorizationStatus == .restricted) + } + } + .task { + let status = AVCaptureDevice.authorizationStatus(for: .video) + cameraAuthorizationStatus = status + if showQRCodeScanner { + switch status { + case .notDetermined: await askCameraAuthorizationAsync() + case .restricted: showQRCodeScanner = false + case .denied: showQRCodeScanner = false + case .authorized: () + @unknown default: await askCameraAuthorizationAsync() + } + } + } + } + + func askCameraAuthorizationAsync() async { + await AVCaptureDevice.requestAccess(for: .video) + cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video) + } + + func askCameraAuthorization(_ cb: (() -> Void)? = nil) { + AVCaptureDevice.requestAccess(for: .video) { allowed in + cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video) + if allowed { cb?() } + } + } +} + + +private func linkTextView(_ link: String) -> some View { + Text(link) + .lineLimit(1) + .font(.caption) + .truncationMode(.middle) +} + +struct InfoSheetButton<Content: View>: View { + @ViewBuilder let content: Content + @State private var showInfoSheet = false + + var body: some View { + Button { + showInfoSheet = true + } label: { + Image(systemName: "info.circle") + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + } + .sheet(isPresented: $showInfoSheet) { + content + } + } +} + +func strIsSimplexLink(_ str: String) -> Bool { + if let parsedMd = parseSimpleXMarkdown(str), + parsedMd.count == 1, + case .simplexLink = parsedMd[0].format { + return true + } else { + return false + } +} + +func strHasSingleSimplexLink(_ str: String) -> FormattedText? { + if let parsedMd = parseSimpleXMarkdown(str) { + let parsedLinks = parsedMd.filter({ $0.format?.isSimplexLink ?? false }) + if parsedLinks.count == 1 { + return parsedLinks[0] + } else { + return nil + } + } else { + return nil + } +} + +struct IncognitoToggle: View { + @EnvironmentObject var theme: AppTheme + @Binding var incognitoEnabled: Bool + @State private var showIncognitoSheet = false + + var body: some View { + ZStack(alignment: .leading) { + Image(systemName: incognitoEnabled ? "theatermasks.fill" : "theatermasks") + .frame(maxWidth: 24, maxHeight: 24, alignment: .center) + .foregroundColor(incognitoEnabled ? Color.indigo : theme.colors.secondary) + .font(.system(size: 14)) + Toggle(isOn: $incognitoEnabled) { + HStack(spacing: 6) { + Text("Incognito") + Image(systemName: "info.circle") + .foregroundColor(theme.colors.primary) + .font(.system(size: 14)) + } + .onTapGesture { + showIncognitoSheet = true + } + } + .padding(.leading, 36) + } + .sheet(isPresented: $showIncognitoSheet) { + IncognitoHelp() + } + } +} + +func sharedProfileInfo(_ incognito: Bool) -> Text { + let name = ChatModel.shared.currentUser?.displayName ?? "" + return Text( + incognito + ? "A new random profile will be shared." + : "Your profile **\(name)** will be shared." + ) +} + +enum PlanAndConnectAlert: Identifiable { + case ownInvitationLinkConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) + case invitationLinkConnecting(connectionLink: CreatedConnLink) + case ownContactAddressConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) + case contactAddressConnectingConfirmReconnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) + case groupLinkConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) + case groupLinkConnectingConfirmReconnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) + case groupLinkConnecting(connectionLink: CreatedConnLink, groupInfo: GroupInfo?) + case error(shortOrFullLink: String, alert: Alert) + + var id: String { + switch self { + case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink.connFullLink)" + case let .invitationLinkConnecting(connectionLink): return "invitationLinkConnecting \(connectionLink.connFullLink)" + case let .ownContactAddressConfirmConnect(connectionLink, _, _): return "ownContactAddressConfirmConnect \(connectionLink.connFullLink)" + case let .contactAddressConnectingConfirmReconnect(connectionLink, _, _): return "contactAddressConnectingConfirmReconnect \(connectionLink.connFullLink)" + case let .groupLinkConfirmConnect(connectionLink, _, _): return "groupLinkConfirmConnect \(connectionLink.connFullLink)" + case let .groupLinkConnectingConfirmReconnect(connectionLink, _, _): return "groupLinkConnectingConfirmReconnect \(connectionLink.connFullLink)" + case let .groupLinkConnecting(connectionLink, _): return "groupLinkConnecting \(connectionLink.connFullLink)" + case let .error(shortOrFullLink, alert): return "error \(shortOrFullLink)" + } + } +} + +func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool, cleanup: (() -> Void)? = nil) -> Alert { + switch alert { + case let .ownInvitationLinkConfirmConnect(connectionLink, connectionPlan, incognito): + return Alert( + title: Text("Connect to yourself?"), + message: Text("This is your own one-time link!"), + primaryButton: .destructive( + Text(incognito ? "Connect incognito" : "Connect"), + action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } + ), + secondaryButton: .cancel() { cleanup?() } + ) + case .invitationLinkConnecting: + return Alert( + title: Text("Already connecting!"), + message: Text("You are already connecting via this one-time link!"), + dismissButton: .default(Text("OK")) { cleanup?() } + ) + case let .ownContactAddressConfirmConnect(connectionLink, connectionPlan, incognito): + return Alert( + title: Text("Connect to yourself?"), + message: Text("This is your own SimpleX address!"), + primaryButton: .destructive( + Text(incognito ? "Connect incognito" : "Connect"), + action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } + ), + secondaryButton: .cancel() { cleanup?() } + ) + case let .contactAddressConnectingConfirmReconnect(connectionLink, connectionPlan, incognito): + return Alert( + title: Text("Repeat connection request?"), + message: Text("You have already requested connection via this address!"), + primaryButton: .destructive( + Text(incognito ? "Connect incognito" : "Connect"), + action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } + ), + secondaryButton: .cancel() { cleanup?() } + ) + case let .groupLinkConfirmConnect(connectionLink, connectionPlan, incognito): + return Alert( + title: Text("Join group?"), + message: Text("You will connect to all group members."), + primaryButton: .default( + Text(incognito ? "Join incognito" : "Join"), + action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } + ), + secondaryButton: .cancel() { cleanup?() } + ) + case let .groupLinkConnectingConfirmReconnect(connectionLink, connectionPlan, incognito): + return Alert( + title: Text("Repeat join request?"), + message: Text("You are already joining the group via this link!"), + primaryButton: .destructive( + Text(incognito ? "Join incognito" : "Join"), + action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } + ), + secondaryButton: .cancel() { cleanup?() } + ) + case let .groupLinkConnecting(_, groupInfo): + if let groupInfo = groupInfo { + return groupInfo.businessChat == nil + ? Alert( + title: Text("Group already exists!"), + message: Text("You are already joining the group \(groupInfo.displayName)."), + dismissButton: .default(Text("OK")) { cleanup?() } + ) + : Alert( + title: Text("Chat already exists!"), + message: Text("You are already connecting to \(groupInfo.displayName)."), + dismissButton: .default(Text("OK")) { cleanup?() } + ) + } else { + return Alert( + title: Text("Already joining the group!"), + message: Text("You are already joining the group via this link."), + dismissButton: .default(Text("OK")) { cleanup?() } + ) + } + case let .error(_, alert): return alert + } +} + +enum PlanAndConnectActionSheet: Identifiable { + case askCurrentOrIncognitoProfile(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan?, title: LocalizedStringKey) + case askCurrentOrIncognitoProfileDestructive(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, title: LocalizedStringKey) + case askCurrentOrIncognitoProfileConnectContactViaAddress(contact: Contact) + case ownGroupLinkConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo) + + var id: String { + switch self { + case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink.connFullLink)" + case let .askCurrentOrIncognitoProfileDestructive(connectionLink, _, _): return "askCurrentOrIncognitoProfileDestructive \(connectionLink.connFullLink)" + case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): return "askCurrentOrIncognitoProfileConnectContactViaAddress \(contact.contactId)" + case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink.connFullLink)" + } + } +} + +func planAndConnectActionSheet(_ sheet: PlanAndConnectActionSheet, dismiss: Bool, cleanup: (() -> Void)? = nil) -> ActionSheet { + switch sheet { + case let .askCurrentOrIncognitoProfile(connectionLink, connectionPlan, title): + return ActionSheet( + title: Text(title), + buttons: [ + .default(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) }, + .default(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) }, + .cancel() { cleanup?() } + ] + ) + case let .askCurrentOrIncognitoProfileDestructive(connectionLink, connectionPlan, title): + return ActionSheet( + title: Text(title), + buttons: [ + .destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) }, + .destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) }, + .cancel() { cleanup?() } + ] + ) + case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): + return ActionSheet( + title: Text("Connect with \(contact.chatViewName)"), + buttons: [ + .default(Text("Use current profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: false, cleanup: cleanup) }, + .default(Text("Use new incognito profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: true, cleanup: cleanup) }, + .cancel() { cleanup?() } + ] + ) + case let .ownGroupLinkConfirmConnect(connectionLink, connectionPlan, incognito, groupInfo): + if let incognito = incognito { + return ActionSheet( + title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"), + buttons: [ + .default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) }, + .destructive(Text(incognito ? "Join incognito" : "Join with current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) }, + .cancel() { cleanup?() } + ] + ) + } else { + return ActionSheet( + title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"), + buttons: [ + .default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) }, + .destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) }, + .destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) }, + .cancel() { cleanup?() } + ] + ) + } + } +} + +func planAndConnect( + _ shortOrFullLink: String, + showAlert: @escaping (PlanAndConnectAlert) -> Void, + showActionSheet: @escaping (PlanAndConnectActionSheet) -> Void, + dismiss: Bool, + incognito: Bool?, + cleanup: (() -> Void)? = nil, + filterKnownContact: ((Contact) -> Void)? = nil, + filterKnownGroup: ((GroupInfo) -> Void)? = nil +) { + Task { + let (result, alert) = await apiConnectPlan(connLink: shortOrFullLink) + if let (connectionLink, connectionPlan) = result { + switch connectionPlan { + case let .invitationLink(ilp): + switch ilp { + case .ok: + logger.debug("planAndConnect, .invitationLink, .ok, incognito=\(incognito?.description ?? "nil")") + if let incognito = incognito { + connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) + } else { + await MainActor.run { + showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link")) + } + } + case .ownLink: + logger.debug("planAndConnect, .invitationLink, .ownLink, incognito=\(incognito?.description ?? "nil")") + await MainActor.run { + if let incognito = incognito { + showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) + } else { + showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!")) + } + } + case let .connecting(contact_): + logger.debug("planAndConnect, .invitationLink, .connecting, incognito=\(incognito?.description ?? "nil")") + await MainActor.run { + if let contact = contact_ { + if let f = filterKnownContact { + f(contact) + } else { + openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) } + } + } else { + showAlert(.invitationLinkConnecting(connectionLink: connectionLink)) + } + } + case let .known(contact): + logger.debug("planAndConnect, .invitationLink, .known, incognito=\(incognito?.description ?? "nil")") + await MainActor.run { + if let f = filterKnownContact { + f(contact) + } else { + openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) } + } + } + } + case let .contactAddress(cap): + switch cap { + case .ok: + logger.debug("planAndConnect, .contactAddress, .ok, incognito=\(incognito?.description ?? "nil")") + if let incognito = incognito { + connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) + } else { + await MainActor.run { + showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address")) + } + } + case .ownLink: + logger.debug("planAndConnect, .contactAddress, .ownLink, incognito=\(incognito?.description ?? "nil")") + await MainActor.run { + if let incognito = incognito { + showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) + } else { + showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!")) + } + } + case .connectingConfirmReconnect: + logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")") + await MainActor.run { + if let incognito = incognito { + showAlert(.contactAddressConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) + } else { + showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You have already requested connection!\nRepeat connection request?")) + } + } + case let .connectingProhibit(contact): + logger.debug("planAndConnect, .contactAddress, .connectingProhibit, incognito=\(incognito?.description ?? "nil")") + await MainActor.run { + if let f = filterKnownContact { + f(contact) + } else { + openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) } + } + } + case let .known(contact): + logger.debug("planAndConnect, .contactAddress, .known, incognito=\(incognito?.description ?? "nil")") + await MainActor.run { + if let f = filterKnownContact { + f(contact) + } else { + openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) } + } + } + case let .contactViaAddress(contact): + logger.debug("planAndConnect, .contactAddress, .contactViaAddress, incognito=\(incognito?.description ?? "nil")") + if let incognito = incognito { + connectContactViaAddress_(contact, dismiss: dismiss, incognito: incognito, cleanup: cleanup) + } else { + await MainActor.run { + showActionSheet(.askCurrentOrIncognitoProfileConnectContactViaAddress(contact: contact)) + } + } + } + case let .groupLink(glp): + switch glp { + case .ok: + await MainActor.run { + if let incognito = incognito { + showAlert(.groupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) + } else { + showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Join group")) + } + } + case let .ownLink(groupInfo): + logger.debug("planAndConnect, .groupLink, .ownLink, incognito=\(incognito?.description ?? "nil")") + await MainActor.run { + if let f = filterKnownGroup { + f(groupInfo) + } + showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo)) + } + case .connectingConfirmReconnect: + logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")") + await MainActor.run { + if let incognito = incognito { + showAlert(.groupLinkConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) + } else { + showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You are already joining the group!\nRepeat join request?")) + } + } + case let .connectingProhibit(groupInfo_): + logger.debug("planAndConnect, .groupLink, .connectingProhibit, incognito=\(incognito?.description ?? "nil")") + await MainActor.run { + showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_)) + } + case let .known(groupInfo): + logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")") + await MainActor.run { + if let f = filterKnownGroup { + f(groupInfo) + } else { + openKnownGroup(groupInfo, dismiss: dismiss) { AlertManager.shared.showAlert(groupAlreadyExistsAlert(groupInfo)) } + } + } + } + case let .error(chatError): + logger.debug("planAndConnect, .error \(chatErrorString(chatError))") + if let incognito = incognito { + connectViaLink(connectionLink, connectionPlan: nil, dismiss: dismiss, incognito: incognito, cleanup: cleanup) + } else { + showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: nil, title: "Connect via link")) + } + } + } else if let alert { + await MainActor.run { + showAlert(.error(shortOrFullLink: shortOrFullLink, alert: alert)) + } + } + } +} + +private func connectContactViaAddress_(_ contact: Contact, dismiss: Bool, incognito: Bool, cleanup: (() -> Void)? = nil) { + Task { + if dismiss { + DispatchQueue.main.async { + dismissAllSheets(animated: true) + } + } + let ok = await connectContactViaAddress(contact.contactId, incognito, showAlert: { AlertManager.shared.showAlert($0) }) + if ok { + AlertManager.shared.showAlert(connReqSentAlert(.contact)) + } + cleanup?() + } +} + +private func connectViaLink( + _ connectionLink: CreatedConnLink, + connectionPlan: ConnectionPlan?, + dismiss: Bool, + incognito: Bool, + cleanup: (() -> Void)? +) { + Task { + if let (connReqType, pcc) = await apiConnect(incognito: incognito, connLink: connectionLink) { + await MainActor.run { + ChatModel.shared.updateContactConnection(pcc) + } + let crt: ConnReqType + crt = if let plan = connectionPlan { + planToConnReqType(plan) ?? connReqType + } else { + connReqType + } + DispatchQueue.main.async { + if dismiss { + dismissAllSheets(animated: true) { + AlertManager.shared.showAlert(connReqSentAlert(crt)) + } + } else { + AlertManager.shared.showAlert(connReqSentAlert(crt)) + } + } + } else { + if dismiss { + DispatchQueue.main.async { + dismissAllSheets(animated: true) + } + } + } + cleanup?() + } +} + +func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) { + let m = ChatModel.shared + if let c = m.getContactChat(contact.contactId) { + if dismiss { + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(c.id) { + showAlreadyExistsAlert?() + } + } + } else { + ItemsModel.shared.loadOpenChat(c.id) { + showAlreadyExistsAlert?() + } + } + } +} + +func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) { + let m = ChatModel.shared + if let g = m.getGroupChat(groupInfo.groupId) { + if dismiss { + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(g.id) { + showAlreadyExistsAlert?() + } + } + } else { + ItemsModel.shared.loadOpenChat(g.id) { + showAlreadyExistsAlert?() + } + } + } +} + +func contactAlreadyConnectingAlert(_ contact: Contact) -> Alert { + mkAlert( + title: "Contact already exists", + message: "You are already connecting to \(contact.displayName)." + ) +} + +func groupAlreadyExistsAlert(_ groupInfo: GroupInfo) -> Alert { + groupInfo.businessChat == nil + ? mkAlert( + title: "Group already exists", + message: "You are already in group \(groupInfo.displayName)." + ) + : mkAlert( + title: "Chat already exists", + message: "You are already connected with \(groupInfo.displayName)." + ) +} + +enum ConnReqType: Equatable { + case invitation + case contact + case groupLink + + var connReqSentText: LocalizedStringKey { + switch self { + case .invitation: return "You will be connected when your contact's device is online, please wait or check later!" + case .contact: return "You will be connected when your connection request is accepted, please wait or check later!" + case .groupLink: return "You will be connected when group link host's device is online, please wait or check later!" + } + } +} + +private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType? { + switch connectionPlan { + case .invitationLink: .invitation + case .contactAddress: .contact + case .groupLink: .groupLink + case .error: nil + } +} + +func connReqSentAlert(_ type: ConnReqType) -> Alert { + return mkAlert( + title: "Connection request sent!", + message: type.connReqSentText + ) +} + +struct NewChatView_Previews: PreviewProvider { + static var previews: some View { + @State var parentAlert: SomeAlert? + @State var contactConnection: PendingContactConnection? = nil + + NewChatView( + selection: .invite + ) + } +} diff --git a/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift b/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift deleted file mode 100644 index 7c272fb631..0000000000 --- a/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// PasteToConnectView.swift -// SimpleX (iOS) -// -// Created by Ian Davies on 22/04/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import SwiftUI -import SimpleXChat - -struct PasteToConnectView: View { - @Environment(\.dismiss) var dismiss: DismissAction - @State private var connectionLink: String = "" - @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false - @FocusState private var linkEditorFocused: Bool - @State private var alert: PlanAndConnectAlert? - @State private var sheet: PlanAndConnectActionSheet? - - var body: some View { - List { - Text("Connect via link") - .font(.largeTitle) - .bold() - .fixedSize(horizontal: false, vertical: true) - .listRowBackground(Color.clear) - .listRowSeparator(.hidden) - .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - .onTapGesture { linkEditorFocused = false } - - Section { - linkEditor() - - Button { - if connectionLink == "" { - connectionLink = UIPasteboard.general.string ?? "" - } else { - connectionLink = "" - } - } label: { - if connectionLink == "" { - settingsRow("doc.plaintext") { Text("Paste") } - } else { - settingsRow("multiply") { Text("Clear") } - } - } - - Button { - connect() - } label: { - settingsRow("link") { Text("Connect") } - } - .disabled(connectionLink == "" || connectionLink.trimmingCharacters(in: .whitespaces).firstIndex(of: " ") != nil) - - IncognitoToggle(incognitoEnabled: $incognitoDefault) - } footer: { - VStack(alignment: .leading, spacing: 4) { - sharedProfileInfo(incognitoDefault) - Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.") - } - .frame(maxWidth: .infinity, alignment: .leading) - } - } - .alert(item: $alert) { a in planAndConnectAlert(a, dismiss: true) } - .actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) } - } - - private func linkEditor() -> some View { - ZStack { - Group { - if connectionLink.isEmpty { - TextEditor(text: Binding.constant(NSLocalizedString("Paste the link you received to connect with your contact.", comment: "placeholder"))) - .foregroundColor(.secondary) - .disabled(true) - } - TextEditor(text: $connectionLink) - .onSubmit(connect) - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - .focused($linkEditorFocused) - } - .allowsTightening(false) - .padding(.horizontal, -5) - .padding(.top, -8) - .frame(height: 180, alignment: .topLeading) - .frame(maxWidth: .infinity, alignment: .leading) - } - } - - private func connect() { - let link = connectionLink.trimmingCharacters(in: .whitespaces) - planAndConnect( - link, - showAlert: { alert = $0 }, - showActionSheet: { sheet = $0 }, - dismiss: true, - incognito: incognitoDefault - ) - } -} - -struct PasteToConnectView_Previews: PreviewProvider { - static var previews: some View { - PasteToConnectView() - } -} diff --git a/apps/ios/Shared/Views/NewChat/QRCode.swift b/apps/ios/Shared/Views/NewChat/QRCode.swift index 3ddb85079c..453149198b 100644 --- a/apps/ios/Shared/Views/NewChat/QRCode.swift +++ b/apps/ios/Shared/Views/NewChat/QRCode.swift @@ -8,6 +8,7 @@ import SwiftUI import CoreImage.CIFilterBuiltins +import SimpleXChat struct MutableQRCode: View { @Binding var uri: String @@ -20,26 +21,32 @@ struct MutableQRCode: View { } } +struct SimpleXCreatedLinkQRCode: View { + let link: CreatedConnLink + @Binding var short: Bool + var onShare: (() -> Void)? = nil + + var body: some View { + QRCode(uri: link.simplexChatUri(short: short), onShare: onShare) + } +} + struct SimpleXLinkQRCode: View { let uri: String var withLogo: Bool = true var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1) + var onShare: (() -> Void)? = nil var body: some View { - QRCode(uri: simplexChatLink(uri), withLogo: withLogo, tintColor: tintColor) + QRCode(uri: simplexChatLink(uri), withLogo: withLogo, tintColor: tintColor, onShare: onShare) } } -func simplexChatLink(_ uri: String) -> String { - uri.starts(with: "simplex:/") - ? uri.replacingOccurrences(of: "simplex:/", with: "https://simplex.chat/") - : uri -} - struct QRCode: View { let uri: String var withLogo: Bool = true var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1) + var onShare: (() -> Void)? = nil @State private var image: UIImage? = nil @State private var makeScreenshotFunc: () -> Void = {} @@ -47,33 +54,34 @@ struct QRCode: View { ZStack { if let image = image { qrCodeImage(image) - } - GeometryReader { geo in - ZStack { - if withLogo { - let w = geo.size.width - Image("icon-light") - .resizable() - .scaledToFit() - .frame(width: w * 0.16, height: w * 0.16) - .frame(width: w * 0.165, height: w * 0.165) - .background(.white) - .clipShape(Circle()) + GeometryReader { geo in + ZStack { + if withLogo { + let w = geo.size.width + Image("icon-light") + .resizable() + .scaledToFit() + .frame(width: w * 0.16, height: w * 0.16) + .frame(width: w * 0.165, height: w * 0.165) + .background(.white) + .clipShape(Circle()) + } } - } - .onAppear { - makeScreenshotFunc = { - let size = CGSizeMake(1024 / UIScreen.main.scale, 1024 / UIScreen.main.scale) - showShareSheet(items: [makeScreenshot(geo.frame(in: .local).origin, size)]) + .onAppear { + makeScreenshotFunc = { + let size = CGSizeMake(1024 / UIScreen.main.scale, 1024 / UIScreen.main.scale) + showShareSheet(items: [makeScreenshot(geo.frame(in: .local).origin, size)]) + onShare?() + } } + .frame(width: geo.size.width, height: geo.size.height) } - .frame(width: geo.size.width, height: geo.size.height) + } else { + Color.clear.aspectRatio(1, contentMode: .fit) } } .onTapGesture(perform: makeScreenshotFunc) - .onAppear { - image = image ?? generateImage(uri, tintColor: tintColor) - } + .task { image = await generateImage(uri, tintColor: tintColor) } .frame(maxWidth: .infinity, maxHeight: .infinity) } } @@ -86,7 +94,7 @@ private func qrCodeImage(_ image: UIImage) -> some View { .textSelection(.enabled) } -private func generateImage(_ uri: String, tintColor: UIColor) -> UIImage? { +private func generateImage(_ uri: String, tintColor: UIColor) async -> UIImage? { let context = CIContext() let filter = CIFilter.qrCodeGenerator() filter.message = Data(uri.utf8) diff --git a/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift b/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift deleted file mode 100644 index 7f3f5e02f8..0000000000 --- a/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// ConnectContactView.swift -// SimpleX -// -// Created by Evgeny Poberezkin on 29/01/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import SwiftUI -import SimpleXChat -import CodeScanner - -struct ScanToConnectView: View { - @Environment(\.dismiss) var dismiss: DismissAction - @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false - @State private var alert: PlanAndConnectAlert? - @State private var sheet: PlanAndConnectActionSheet? - - var body: some View { - ScrollView { - VStack(alignment: .leading) { - Text("Scan QR code") - .font(.largeTitle) - .bold() - .fixedSize(horizontal: false, vertical: true) - .padding(.vertical) - - CodeScannerView(codeTypes: [.qr], scanMode: .continuous, completion: processQRCode) - .aspectRatio(1, contentMode: .fit) - .cornerRadius(12) - - IncognitoToggle(incognitoEnabled: $incognitoDefault) - .padding(.horizontal) - .padding(.vertical, 6) - .background( - RoundedRectangle(cornerRadius: 12, style: .continuous) - .fill(Color(uiColor: .systemBackground)) - ) - .padding(.top) - - VStack(alignment: .leading, spacing: 4) { - sharedProfileInfo(incognitoDefault) - Text("If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.") - } - .frame(maxWidth: .infinity, alignment: .leading) - .font(.footnote) - .foregroundColor(.secondary) - .padding(.horizontal) - } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - } - .background(Color(.systemGroupedBackground)) - .alert(item: $alert) { a in planAndConnectAlert(a, dismiss: true) } - .actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) } - } - - func processQRCode(_ resp: Result<ScanResult, ScanError>) { - switch resp { - case let .success(r): - planAndConnect( - r.string, - showAlert: { alert = $0 }, - showActionSheet: { sheet = $0 }, - dismiss: true, - incognito: incognitoDefault - ) - case let .failure(e): - logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)") - dismiss() - } - } -} - -struct ConnectContactView_Previews: PreviewProvider { - static var previews: some View { - ScanToConnectView() - } -} diff --git a/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift b/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift new file mode 100644 index 0000000000..c8d0faafa7 --- /dev/null +++ b/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift @@ -0,0 +1,109 @@ +// +// AddressCreationCard.swift +// SimpleX (iOS) +// +// Created by Diogo Cunha on 13/11/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +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 new file mode 100644 index 0000000000..656cef4a04 --- /dev/null +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -0,0 +1,412 @@ +// +// ChooseServerOperators.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 31.10.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +let conditionsURL = URL(string: "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md")! + +struct OnboardingButtonStyle: ButtonStyle { + @EnvironmentObject var theme: AppTheme + var isDisabled: Bool = false + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.system(size: 17, weight: .semibold)) + .padding() + .frame(maxWidth: .infinity) + .background( + isDisabled + ? ( + theme.colors.isLight + ? .gray.opacity(0.17) + : .gray.opacity(0.27) + ) + : theme.colors.primary + ) + .foregroundColor( + isDisabled + ? ( + theme.colors.isLight + ? .gray.opacity(0.4) + : .white.opacity(0.2) + ) + : .white + ) + .cornerRadius(16) + .scaleEffect(configuration.isPressed ? 0.95 : 1.0) + } +} + +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<Int64>() + @State private var sheetItem: OnboardingConditionsViewSheet? = nil + @State private var notificationsModeNavLinkActive = false + @State private var justOpened = true + + var selectedOperators: [ServerOperator] { serverOperators.filter { selectedOperatorIds.contains($0.operatorId) } } + + 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) + + Spacer() + + 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) + + Button("Privacy policy and conditions of use.") { + sheetItem = .showConditions + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(.horizontal, 4) + + Spacer() + + VStack(spacing: 12) { + acceptConditionsButton() + + Button("Configure server operators") { + sheetItem = .configureOperators + } + .frame(minHeight: 40) + } + } + .padding(25) + .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 .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 + } + } + .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 { + ZStack { + button() + + NavigationLink(isActive: $notificationsModeNavLinkActive) { + notificationsModeDestinationView() + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } + } + + 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 acceptForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted } + let operatorIds = acceptForOperators.map { $0.operatorId } + let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: operatorIds) + await MainActor.run { + ChatModel.shared.conditions = r + } + 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 { + 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) + } + } + + private func enabledOperators(_ operators: [ServerOperator]) -> [ServerOperator]? { + var ops = operators + if !ops.isEmpty { + for i in 0..<ops.count { + var op = ops[i] + op.enabled = selectedOperatorIds.contains(op.operatorId) + ops[i] = op + } + let haveSMPStorage = ops.contains(where: { $0.enabled && $0.smpRoles.storage }) + let haveSMPProxy = ops.contains(where: { $0.enabled && $0.smpRoles.proxy }) + let haveXFTPStorage = ops.contains(where: { $0.enabled && $0.xftpRoles.storage }) + let haveXFTPProxy = ops.contains(where: { $0.enabled && $0.xftpRoles.proxy }) + if haveSMPStorage && haveSMPProxy && haveXFTPStorage && haveXFTPProxy { + return ops + } else if let firstEnabledIndex = ops.firstIndex(where: { $0.enabled }) { + var op = ops[firstEnabledIndex] + if !haveSMPStorage { op.smpRoles.storage = true } + if !haveSMPProxy { op.smpRoles.proxy = true } + if !haveXFTPStorage { op.xftpRoles.storage = true } + 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 + return nil + } + } else { + return nil + } + } +} + +private enum ChooseServerOperatorsSheet: Identifiable { + case showInfo + + var id: String { + switch self { + case .showInfo: return "showInfo" + } + } +} + +struct ChooseServerOperators: View { + @Environment(\.dismiss) var dismiss: DismissAction + @Environment(\.colorScheme) var colorScheme: ColorScheme + @EnvironmentObject var theme: AppTheme + var serverOperators: [ServerOperator] + @Binding var selectedOperatorIds: Set<Int64> + @State private var sheetItem: ChooseServerOperatorsSheet? = nil + + var body: some View { + GeometryReader { g in + ScrollView { + VStack(alignment: .leading, spacing: 20) { + Text("Server operators") + .font(.largeTitle) + .bold() + .frame(maxWidth: .infinity, alignment: .center) + .padding(.top, 25) + + infoText() + .frame(maxWidth: .infinity, alignment: .center) + + Spacer() + + ForEach(serverOperators) { srvOperator in + operatorCheckView(srvOperator) + } + VStack { + Text("SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.").padding(.bottom, 8) + Text("You can configure servers via settings.") + } + .font(.footnote) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.horizontal, 16) + + Spacer() + + VStack(spacing: 8) { + setOperatorsButton() + onboardingButtonPlaceholder() + } + } + .frame(minHeight: g.size.height) + } + .sheet(item: $sheetItem) { item in + switch item { + case .showInfo: + ChooseServerOperatorsInfoView() + } + } + .frame(maxHeight: .infinity, alignment: .top) + } + .frame(maxHeight: .infinity, alignment: .top) + .padding(25) + .interactiveDismissDisabled(selectedOperatorIds.isEmpty) + } + + private func infoText() -> some View { + Button { + sheetItem = .showInfo + } label: { + Label("How it helps privacy", systemImage: "info.circle") + .font(.headline) + } + } + + private func operatorCheckView(_ serverOperator: ServerOperator) -> some View { + let checked = selectedOperatorIds.contains(serverOperator.operatorId) + let icon = checked ? "checkmark.circle.fill" : "circle" + let iconColor = checked ? theme.colors.primary : Color(uiColor: .tertiaryLabel).asAnotherColorFromSecondary(theme) + return HStack(spacing: 10) { + Image(serverOperator.largeLogo(colorScheme)) + .resizable() + .scaledToFit() + .frame(height: 48) + Spacer() + Image(systemName: icon) + .resizable() + .scaledToFit() + .frame(width: 26, height: 26) + .foregroundColor(iconColor) + } + .background(theme.colors.background) + .padding() + .clipShape(RoundedRectangle(cornerRadius: 18)) + .overlay( + RoundedRectangle(cornerRadius: 18) + .stroke(Color(uiColor: .secondarySystemFill), lineWidth: 2) + ) + .padding(.horizontal, 2) + .onTapGesture { + if checked { + selectedOperatorIds.remove(serverOperator.operatorId) + } else { + selectedOperatorIds.insert(serverOperator.operatorId) + } + } + } + + private func setOperatorsButton() -> some View { + Button { + dismiss() + } label: { + Text("OK") + } + .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) + .disabled(selectedOperatorIds.isEmpty) + } +} + +let operatorsPostLink = URL(string: "https://simplex.chat/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.html")! + +struct ChooseServerOperatorsInfoView: View { + @Environment(\.colorScheme) var colorScheme: ColorScheme + @EnvironmentObject var theme: AppTheme + + var body: some View { + NavigationView { + List { + VStack(alignment: .leading, spacing: 12) { + Text("The app protects your privacy by using different operators in each conversation.") + Text("When more than one operator is enabled, none of them has metadata to learn who communicates with whom.") + Text("For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.") + } + .fixedSize(horizontal: false, vertical: true) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .padding(.top) + + Section { + ForEach(ChatModel.shared.conditions.serverOperators) { op in + operatorInfoNavLinkView(op) + } + } header: { + Text("About operators") + .foregroundColor(theme.colors.secondary) + } + } + .navigationTitle("Server operators") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } + } + + private func operatorInfoNavLinkView(_ op: ServerOperator) -> some View { + NavigationLink() { + OperatorInfoView(serverOperator: op) + .navigationBarTitle("Network operator") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + HStack { + Image(op.logo(colorScheme)) + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + Text(op.tradeName) + } + } + } +} + +#Preview { + OnboardingConditionsView() +} diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index f5db37dacf..ae72cb1be5 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -11,12 +11,14 @@ import SimpleXChat enum UserProfileAlert: Identifiable { case duplicateUserError + case invalidDisplayNameError case createUserError(error: LocalizedStringKey) case invalidNameError(validName: String) var id: String { switch self { case .duplicateUserError: return "duplicateUserError" + case .invalidDisplayNameError: return "invalidDisplayNameError" case .createUserError: return "createUserError" case let .invalidNameError(validName): return "invalidNameError \(validName)" } @@ -25,6 +27,7 @@ enum UserProfileAlert: Identifiable { struct CreateProfile: View { @Environment(\.dismiss) var dismiss + @EnvironmentObject var theme: AppTheme @State private var displayName: String = "" @FocusState private var focusDisplayName @State private var alert: UserProfileAlert? @@ -35,7 +38,7 @@ struct CreateProfile: View { TextField("Enter your name…", text: $displayName) .focused($focusDisplayName) Button { - createProfile(displayName, showAlert: { alert = $0 }, dismiss: dismiss) + createProfile() } label: { Label("Create profile", systemImage: "checkmark") } @@ -43,6 +46,8 @@ struct CreateProfile: View { } header: { HStack { Text("Your profile") + .foregroundColor(theme.colors.secondary) + let name = displayName.trimmingCharacters(in: .whitespaces) let validName = mkValidName(name) if name != validName { @@ -57,146 +62,210 @@ struct CreateProfile: View { .frame(height: 20) } footer: { VStack(alignment: .leading, spacing: 8) { - Text("Your profile, contacts and delivered messages are stored on your device.") - Text("The profile is only shared with your contacts.") + Text("Your profile is stored on your device and only shared with your contacts.") } + .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity, alignment: .leading) } } .navigationTitle("Create your profile") + .modifier(ThemedBackground(grouped: true)) .alert(item: $alert) { a in userProfileAlert(a, $displayName) } .onAppear() { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { focusDisplayName = true } } - .keyboardPadding() + } + + private func createProfile() { + hideKeyboard() + let profile = Profile( + displayName: displayName.trimmingCharacters(in: .whitespaces), + fullName: "" + ) + let m = ChatModel.shared + do { + AppChatState.shared.set(.active) + m.currentUser = try apiCreateActiveUser(profile) + // .isEmpty check is redundant here, but it makes it clearer what is going on + if m.users.isEmpty || m.users.allSatisfy({ $0.user.hidden }) { + try startChat() + withAnimation { + onboardingStageDefault.set(.step3_ChooseServerOperators) + m.onboardingStage = .step3_ChooseServerOperators + } + } else { + onboardingStageDefault.set(.onboardingComplete) + m.onboardingStage = .onboardingComplete + dismiss() + m.users = try listUsers() + try getUserChatData() + } + } catch let error { + showCreateProfileAlert(showAlert: { alert = $0 }, error) + } } } struct CreateFirstProfile: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss @State private var displayName: String = "" @FocusState private var focusDisplayName + @State private var nextStepNavLinkActive = false var body: some View { - VStack(alignment: .leading) { - Group { - Text("Create your profile") + let v = VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .center, spacing: 16) { + Text("Create profile") .font(.largeTitle) .bold() - Text("Your profile, contacts and delivered messages are stored on your device.") - .foregroundColor(.secondary) - Text("The profile is only shared with your contacts.") - .foregroundColor(.secondary) - .padding(.bottom) - } - .padding(.bottom) + .multilineTextAlignment(.center) - ZStack(alignment: .topLeading) { + 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) - if name != validName { - Button { - showAlert(.invalidNameError(validName: validName)) - } label: { - Image(systemName: "exclamationmark.circle").foregroundColor(.red) + 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)) + ) + if name != validName { + Button { + showAlert(.invalidNameError(validName: validName)) + } label: { + Image(systemName: "exclamationmark.circle") + .foregroundColor(.red) + .padding(.horizontal, 10) + } } - } else { - Image(systemName: "exclamationmark.circle").foregroundColor(.clear) } - TextField("Enter your name…", text: $displayName) - .focused($focusDisplayName) - .padding(.leading, 32) } - .padding(.bottom) + .padding(.top) + Spacer() - onboardingButtons() + + VStack(spacing: 10) { + createProfileButton() + if !focusDisplayName { + onboardingButtonPlaceholder() + } + } } .onAppear() { - focusDisplayName = true - setLastVersionDefault() + 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() + .padding(.horizontal, 25) + .padding(.bottom, 25) .frame(maxWidth: .infinity, alignment: .leading) - .keyboardPadding() + if #available(iOS 16, *) { + return v.padding(.top, 10) + } else { + return v.padding(.top, 75).ignoresSafeArea(.all, edges: .top) + } } - func onboardingButtons() -> some View { - HStack { + func createProfileButton() -> some View { + ZStack { Button { - hideKeyboard() - withAnimation { - m.onboardingStage = .step1_SimpleXInfo - } + createProfile() } label: { - HStack { - Image(systemName: "lessthan") - Text("About SimpleX") - } - } - - Spacer() - - Button { - createProfile(displayName, showAlert: showAlert, dismiss: dismiss) - } label: { - HStack { - Text("Create") - Image(systemName: "greaterthan") - } + Text("Create profile") } + .buttonStyle(OnboardingButtonStyle(isDisabled: !canCreateProfile(displayName))) .disabled(!canCreateProfile(displayName)) + + NavigationLink(isActive: $nextStepNavLinkActive) { + nextStepDestinationView() + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() } } private func showAlert(_ alert: UserProfileAlert) { AlertManager.shared.showAlert(userProfileAlert(alert, $displayName)) } + + private func nextStepDestinationView() -> some View { + OnboardingConditionsView() + .navigationBarBackButtonHidden(true) + .modifier(ThemedBackground()) + } + + private func createProfile() { + hideKeyboard() + let profile = Profile( + displayName: displayName.trimmingCharacters(in: .whitespaces), + fullName: "" + ) + let m = ChatModel.shared + do { + AppChatState.shared.set(.active) + m.currentUser = try apiCreateActiveUser(profile) + try startChat(onboarding: true) + onboardingStageDefault.set(.step3_ChooseServerOperators) + nextStepNavLinkActive = true + } catch let error { + showCreateProfileAlert(showAlert: showAlert, error) + } + } } -private func createProfile(_ displayName: String, showAlert: (UserProfileAlert) -> Void, dismiss: DismissAction) { - hideKeyboard() - let profile = Profile( - displayName: displayName.trimmingCharacters(in: .whitespaces), - fullName: "" - ) +private func showCreateProfileAlert( + showAlert: (UserProfileAlert) -> Void, + _ error: Error +) { let m = ChatModel.shared - do { - m.currentUser = try apiCreateActiveUser(profile) - if m.users.isEmpty { - try startChat() - withAnimation { - onboardingStageDefault.set(.step3_CreateSimpleXAddress) - m.onboardingStage = .step3_CreateSimpleXAddress - } + switch error as? ChatError { + case .errorStore(.duplicateName), + .error(.userExists): + if m.currentUser == nil { + AlertManager.shared.showAlert(duplicateUserAlert) } else { - onboardingStageDefault.set(.onboardingComplete) - m.onboardingStage = .onboardingComplete - dismiss() - m.users = try listUsers() - try getUserChatData() + showAlert(.duplicateUserError) } - } catch let error { - switch error as? ChatResponse { - case .chatCmdError(_, .errorStore(.duplicateName)), - .chatCmdError(_, .error(.userExists)): - if m.currentUser == nil { - AlertManager.shared.showAlert(duplicateUserAlert) - } else { - showAlert(.duplicateUserError) - } - default: - let err: LocalizedStringKey = "Error: \(responseError(error))" - if m.currentUser == nil { - AlertManager.shared.showAlert(creatUserErrorAlert(err)) - } else { - showAlert(.createUserError(error: err)) - } + case .error(.invalidDisplayName): + if m.currentUser == nil { + AlertManager.shared.showAlert(invalidDisplayNameAlert) + } else { + showAlert(.invalidDisplayNameError) + } + default: + let err: LocalizedStringKey = "Error: \(responseError(error))" + if m.currentUser == nil { + AlertManager.shared.showAlert(creatUserErrorAlert(err)) + } else { + showAlert(.createUserError(error: err)) } - logger.error("Failed to create user or start chat: \(responseError(error))") } + logger.error("Failed to create user or start chat: \(responseError(error))") } private func canCreateProfile(_ displayName: String) -> Bool { @@ -207,6 +276,7 @@ private func canCreateProfile(_ displayName: String) -> Bool { func userProfileAlert(_ alert: UserProfileAlert, _ displayName: Binding<String>) -> Alert { switch alert { case .duplicateUserError: return duplicateUserAlert + case .invalidDisplayNameError: return invalidDisplayNameAlert case let .createUserError(err): return creatUserErrorAlert(err) case let .invalidNameError(name): return createInvalidNameAlert(name, displayName) } @@ -219,6 +289,13 @@ private var duplicateUserAlert: Alert { ) } +private var invalidDisplayNameAlert: Alert { + Alert( + title: Text("Invalid display name!"), + message: Text("This display name is invalid. Please choose another name.") + ) +} + private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert { Alert( title: Text("Error creating profile!"), diff --git a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift index befb34b318..a2f5db7f03 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift @@ -31,7 +31,7 @@ struct CreateSimpleXAddress: View { Spacer() if let userAddress = m.userAddress { - SimpleXLinkQRCode(uri: userAddress.connReqContact) + SimpleXCreatedLinkQRCode(link: userAddress.connLinkContact, short: Binding.constant(false)) .frame(maxHeight: g.size.width) shareQRCodeButton(userAddress) .frame(maxWidth: .infinity) @@ -77,9 +77,9 @@ struct CreateSimpleXAddress: View { progressIndicator = true Task { do { - let connReqContact = try await apiCreateUserAddress() + let connLinkContact = try await apiCreateUserAddress(short: false) DispatchQueue.main.async { - m.userAddress = UserContactLink(connReqContact: connReqContact) + m.userAddress = UserContactLink(connLinkContact: connLinkContact) } await MainActor.run { progressIndicator = false } } catch let error { @@ -121,7 +121,7 @@ struct CreateSimpleXAddress: View { private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View { Button { - showShareSheet(items: [simplexChatLink(userAddress.connReqContact)]) + showShareSheet(items: [simplexChatLink(userAddress.connLinkContact.simplexChatUri(short: false))]) } label: { Label("Share", systemImage: "square.and.arrow.up") } @@ -189,7 +189,7 @@ struct SendAddressMailView: View { let messageBody = String(format: NSLocalizedString(""" <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> - """, comment: "email text"), simplexChatLink(userAddress.connReqContact)) + """, comment: "email text"), simplexChatLink(userAddress.connLinkContact.simplexChatUri(short: false))) MailView( isShowing: self.$showMailView, result: $mailViewResult, diff --git a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift index fdd73d2632..7452d74e91 100644 --- a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift +++ b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift @@ -9,24 +9,24 @@ import SwiftUI struct HowItWorks: View { + @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject var m: ChatModel var onboarding: Bool + @Binding var createProfileNavLinkActive: Bool var body: some View { VStack(alignment: .leading) { Text("How SimpleX works") .font(.largeTitle) + .bold() .padding(.vertical) ScrollView { VStack(alignment: .leading) { Group { - Text("Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*") - Text("To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.") - Text("You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.") - Text("Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.") - if onboarding { - Text("Read more in our GitHub repository.") - } else { + Text("To protect your privacy, SimpleX uses separate IDs for each of your contacts.") + 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).") } } @@ -37,18 +37,34 @@ struct HowItWorks: View { Spacer() if onboarding { - OnboardingActionButton() - .padding(.bottom, 8) + VStack(spacing: 10) { + createFirstProfileButton() + onboardingButtonPlaceholder() + } } } .lineLimit(10) - .padding() + .padding(onboarding ? 25 : 16) .frame(maxHeight: .infinity, alignment: .top) + .modifier(ThemedBackground()) + } + + private func createFirstProfileButton() -> some View { + Button { + dismiss() + createProfileNavLinkActive = true + } label: { + Text("Create your profile") + } + .buttonStyle(OnboardingButtonStyle(isDisabled: false)) } } struct HowItWorks_Previews: PreviewProvider { static var previews: some View { - HowItWorks(onboarding: true) + HowItWorks( + 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 438491b5f1..8f448dc508 100644 --- a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift +++ b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift @@ -12,20 +12,39 @@ struct OnboardingView: View { var onboarding: OnboardingStage var body: some View { - switch onboarding { - case .step1_SimpleXInfo: SimpleXInfo(onboarding: true) - case .step2_CreateProfile: CreateFirstProfile() - case .step3_CreateSimpleXAddress: CreateSimpleXAddress() - case .step4_SetNotificationsMode: SetNotificationsMode() - case .onboardingComplete: EmptyView() + NavigationView { + switch onboarding { + case .step1_SimpleXInfo: + SimpleXInfo(onboarding: true) + .modifier(ThemedBackground()) + case .step2_CreateProfile: // deprecated + CreateFirstProfile() + .modifier(ThemedBackground()) + case .step3_CreateSimpleXAddress: // deprecated + CreateSimpleXAddress() + case .step3_ChooseServerOperators: + OnboardingConditionsView() + .navigationBarBackButtonHidden(true) + .modifier(ThemedBackground()) + case .step4_SetNotificationsMode: + SetNotificationsMode() + .navigationBarBackButtonHidden(true) + .modifier(ThemedBackground()) + case .onboardingComplete: EmptyView() + } } } } +func onboardingButtonPlaceholder() -> some View { + Spacer().frame(height: 40) +} + enum OnboardingStage: String, Identifiable { case step1_SimpleXInfo - case step2_CreateProfile - case step3_CreateSimpleXAddress + case step2_CreateProfile // deprecated + case step3_CreateSimpleXAddress // deprecated + case step3_ChooseServerOperators // changed to simplified conditions case step4_SetNotificationsMode case onboardingComplete diff --git a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift index 3bbd7a5c94..31865e7af9 100644 --- a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift +++ b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift @@ -13,43 +13,61 @@ struct SetNotificationsMode: View { @EnvironmentObject var m: ChatModel @State private var notificationMode = NotificationsMode.instant @State private var showAlert: NotificationAlert? + @State private var showInfo: Bool = false var body: some View { - ScrollView { - VStack(alignment: .leading, spacing: 16) { - Text("Push notifications") - .font(.largeTitle) - .bold() - .frame(maxWidth: .infinity) + GeometryReader { g in + let v = ScrollView { + VStack(alignment: .center, spacing: 20) { + Text("Push notifications") + .font(.largeTitle) + .bold() + .padding(.top, 25) + + infoText() + + Spacer() - Text("Send notifications:") - ForEach(NotificationsMode.values) { mode in - NtfModeSelector(mode: mode, selection: $notificationMode) - } - - Spacer() - - Button { - if let token = m.deviceToken { - setNotificationsMode(token, notificationMode) - } else { - AlertManager.shared.showAlertMsg(title: "No device token!") + ForEach(NotificationsMode.values) { mode in + NtfModeSelector(mode: mode, selection: $notificationMode) } - onboardingStageDefault.set(.onboardingComplete) - m.onboardingStage = .onboardingComplete - } label: { - if case .off = notificationMode { - Text("Use chat") - } else { - Text("Enable notifications") + + 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 + } label: { + if case .off = notificationMode { + Text("Use chat") + } else { + Text("Enable notifications") + } + } + .buttonStyle(OnboardingButtonStyle()) + onboardingButtonPlaceholder() } } - .font(.title) - .frame(maxWidth: .infinity) + .padding(25) + .frame(minHeight: g.size.height) + } + if #available(iOS 16.4, *) { + v.scrollBounceBehavior(.basedOnSize) + } else { + v } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) } + .frame(maxHeight: .infinity) + .sheet(isPresented: $showInfo) { + NotificationsInfoView() + } + .navigationBarHidden(true) // necessary on iOS 15 } private func setNotificationsMode(_ token: DeviceToken, _ mode: NotificationsMode) { @@ -75,31 +93,51 @@ struct SetNotificationsMode: View { } } } + + private func infoText() -> some View { + Button { + showInfo = true + } label: { + Label("How it affects privacy", systemImage: "info.circle") + .font(.headline) + } + } } struct NtfModeSelector: View { + @EnvironmentObject var theme: AppTheme var mode: NotificationsMode @Binding var selection: NotificationsMode @State private var tapped = false var body: some View { ZStack { - VStack(alignment: .leading, spacing: 4) { - Text(mode.label) - .font(.headline) - .foregroundColor(selection == mode ? .accentColor : .secondary) - Text(ntfModeDescription(mode)) - .lineLimit(10) - .font(.subheadline) + HStack(spacing: 16) { + Image(systemName: mode.icon) + .resizable() + .scaledToFill() + .frame(width: mode.icon == "bolt" ? 14 : 18, height: 18) + .foregroundColor(selection == mode ? theme.colors.primary : theme.colors.secondary) + VStack(alignment: .leading, spacing: 4) { + Text(mode.label) + .font(.headline) + .foregroundColor(selection == mode ? theme.colors.primary : theme.colors.secondary) + Text(ntfModeShortDescription(mode)) + .lineLimit(2) + .font(.callout) + .fixedSize(horizontal: false, vertical: true) + } } - .padding(12) + .padding(.vertical, 12) + .padding(.trailing, 12) + .padding(.leading, 16) } .frame(maxWidth: .infinity, alignment: .leading) - .background(Color(uiColor: tapped ? .secondarySystemFill : .systemBackground)) + .background(tapped ? Color(uiColor: .secondarySystemFill) : theme.colors.background) .clipShape(RoundedRectangle(cornerRadius: 18)) .overlay( RoundedRectangle(cornerRadius: 18) - .stroke(selection == mode ? Color.accentColor : Color(uiColor: .secondarySystemFill), lineWidth: 2) + .stroke(selection == mode ? theme.colors.primary : Color(uiColor: .secondarySystemFill), lineWidth: 2) ) ._onButtonGesture { down in tapped = down @@ -108,6 +146,37 @@ struct NtfModeSelector: View { } } +struct NotificationsInfoView: View { + var body: some View { + VStack(alignment: .leading) { + Text("Notifications privacy") + .font(.largeTitle) + .bold() + .padding(.vertical) + ScrollView { + VStack(alignment: .leading) { + Group { + ForEach(NotificationsMode.values) { mode in + VStack(alignment: .leading, spacing: 4) { + (Text(Image(systemName: mode.icon)) + textSpace + Text(mode.label)) + .font(.headline) + .foregroundColor(.secondary) + Text(ntfModeDescription(mode)) + .lineLimit(10) + .font(.callout) + } + } + } + .padding(.bottom) + } + } + } + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .modifier(ThemedBackground()) + } +} + struct NotificationsModeView_Previews: PreviewProvider { static var previews: some View { SetNotificationsMode() diff --git a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift index ce1d727b10..9f41a37b1d 100644 --- a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift +++ b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift @@ -7,111 +7,147 @@ // import SwiftUI +import SimpleXChat struct SimpleXInfo: View { @EnvironmentObject var m: ChatModel @Environment(\.colorScheme) var colorScheme: ColorScheme @State private var showHowItWorks = false + @State private var createProfileNavLinkActive = false var onboarding: Bool var body: some View { GeometryReader { g in - ScrollView { + let v = ScrollView { VStack(alignment: .leading) { - Image(colorScheme == .light ? "logo" : "logo-light") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: g.size.width * 0.67) - .padding(.bottom, 8) - .frame(maxWidth: .infinity, minHeight: 48, alignment: .top) - - VStack(alignment: .leading) { - Text("The next generation of private messaging") - .font(.title2) - .padding(.bottom, 30) - .padding(.horizontal, 40) - .frame(maxWidth: .infinity) - .multilineTextAlignment(.center) - infoRow("privacy", "Privacy redefined", - "The 1st platform without any user identifiers – private by design.", width: 48) - infoRow("shield", "Immune to spam and abuse", - "People can connect to you only via the links you share.", width: 46) - infoRow(colorScheme == .light ? "decentralized" : "decentralized-light", "Decentralized", - "Open-source protocol and code – anybody can run the servers.", width: 44) + 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) + } } Spacer() - if onboarding { - OnboardingActionButton() - Spacer() - } - Button { - showHowItWorks = true - } label: { - Label("How it works", systemImage: "info.circle") - .font(.subheadline) + 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) + + Spacer() + + if onboarding { + VStack(spacing: 10) { + createFirstProfileButton() + + 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) + } } - .padding(.bottom, 8) - .frame(maxWidth: .infinity) } + .padding(.horizontal, 25) + .padding(.top, 75) + .padding(.bottom, 25) .frame(minHeight: g.size.height) } + .sheet(isPresented: Binding( + get: { m.migrationState != nil }, + set: { _ in + m.migrationState = nil + MigrationToDeviceState.save(nil) } + )) { + NavigationView { + VStack(alignment: .leading) { + MigrateToDevice(migrationState: $m.migrationState) + } + .navigationTitle("Migrate here") + .modifier(ThemedBackground(grouped: true)) + } + } .sheet(isPresented: $showHowItWorks) { - HowItWorks(onboarding: onboarding) + HowItWorks( + onboarding: onboarding, + createProfileNavLinkActive: $createProfileNavLinkActive + ) + } + if #available(iOS 16.4, *) { + v.scrollBounceBehavior(.basedOnSize) + } else { + v } } + .onAppear() { + setLastVersionDefault() + } .frame(maxHeight: .infinity) - .padding() + .navigationBarHidden(true) // necessary on iOS 15 } - private func infoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View { + 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(.top, 4) - .padding(.leading, 4) .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") + } + .buttonStyle(OnboardingButtonStyle(isDisabled: false)) + + NavigationLink(isActive: $createProfileNavLinkActive) { + CreateFirstProfile() + .modifier(ThemedBackground()) + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() } - .padding(.bottom, 20) - .padding(.trailing, 6) } } -struct OnboardingActionButton: View { - @EnvironmentObject var m: ChatModel +let textSpace = Text(verbatim: " ") - var body: some View { - if m.currentUser == nil { - actionButton("Create your profile", onboarding: .step2_CreateProfile) - } else { - actionButton("Make a private connection", onboarding: .onboardingComplete) - } - } - - private func actionButton(_ label: LocalizedStringKey, onboarding: OnboardingStage) -> some View { - Button { - withAnimation { - onboardingStageDefault.set(onboarding) - m.onboardingStage = onboarding - } - } label: { - HStack { - Text(label).font(.title2) - Image(systemName: "greaterthan") - } - } - .frame(maxWidth: .infinity) - .padding(.bottom) - } -} +let textNewLine = Text(verbatim: "\n") struct SimpleXInfo_Previews: PreviewProvider { static var previews: some View { diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index 59c2b25b6d..f65a21623a 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -7,17 +7,37 @@ // import SwiftUI +import SimpleXChat private struct VersionDescription { var version: String var post: URL? - var features: [FeatureDescription] + var features: [Feature] } -private struct FeatureDescription { - var icon: String - var title: LocalizedStringKey - var description: LocalizedStringKey +private enum Feature: Identifiable { + case feature(Description) + case view(FeatureView) + + var id: LocalizedStringKey { + switch self { + case let .feature(d): d.title + case let .view(v): v.title + } + } +} + +private struct Description { + let icon: String? + let title: LocalizedStringKey + let description: LocalizedStringKey? + var subfeatures: [(icon: String, description: LocalizedStringKey)] = [] +} + +private struct FeatureView { + let icon: String? + let title: LocalizedStringKey + let view: () -> any View } private let versionDescriptions: [VersionDescription] = [ @@ -25,171 +45,171 @@ private let versionDescriptions: [VersionDescription] = [ version: "v4.2", post: URL(string: "https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html"), features: [ - FeatureDescription( + .feature(Description( icon: "checkmark.shield", title: "Security assessment", description: "SimpleX Chat security was audited by Trail of Bits." - ), - FeatureDescription( + )), + .feature(Description( icon: "person.2", title: "Group links", description: "Admins can create the links to join groups." - ), - FeatureDescription( + )), + .feature(Description( icon: "checkmark", title: "Auto-accept contact requests", description: "With optional welcome message." - ), + )), ] ), VersionDescription( version: "v4.3", post: URL(string: "https://simplex.chat/blog/20221206-simplex-chat-v4.3-voice-messages.html"), features: [ - FeatureDescription( + .feature(Description( icon: "mic", title: "Voice messages", description: "Max 30 seconds, received instantly." - ), - FeatureDescription( + )), + .feature(Description( icon: "trash.slash", title: "Irreversible message deletion", description: "Your contacts can allow full message deletion." - ), - FeatureDescription( + )), + .feature(Description( icon: "externaldrive.connected.to.line.below", title: "Improved server configuration", description: "Add servers by scanning QR codes." - ), - FeatureDescription( + )), + .feature(Description( icon: "eye.slash", title: "Improved privacy and security", description: "Hide app screen in the recent apps." - ), + )), ] ), VersionDescription( version: "v4.4", post: URL(string: "https://simplex.chat/blog/20230103-simplex-chat-v4.4-disappearing-messages.html"), features: [ - FeatureDescription( + .feature(Description( icon: "stopwatch", title: "Disappearing messages", description: "Sent messages will be deleted after set time." - ), - FeatureDescription( + )), + .feature(Description( icon: "ellipsis.circle", title: "Live messages", description: "Recipients see updates as you type them." - ), - FeatureDescription( + )), + .feature(Description( icon: "checkmark.shield", title: "Verify connection security", description: "Compare security codes with your contacts." - ), - FeatureDescription( + )), + .feature(Description( icon: "camera", title: "GIFs and stickers", description: "Send them from gallery or custom keyboards." - ), - FeatureDescription( + )), + .feature(Description( icon: "character", title: "French interface", description: "Thanks to the users – contribute via Weblate!" - ) + )), ] ), VersionDescription( version: "v4.5", post: URL(string: "https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html"), features: [ - FeatureDescription( + .feature(Description( icon: "person.crop.rectangle.stack", title: "Multiple chat profiles", description: "Different names, avatars and transport isolation." - ), - FeatureDescription( + )), + .feature(Description( icon: "rectangle.and.pencil.and.ellipsis", title: "Message draft", description: "Preserve the last message draft, with attachments." - ), - FeatureDescription( + )), + .feature(Description( icon: "network.badge.shield.half.filled", title: "Transport isolation", description: "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." - ), - FeatureDescription( + )), + .feature(Description( icon: "lock.doc", title: "Private filenames", description: "To protect timezone, image/voice files use UTC." - ), - FeatureDescription( + )), + .feature(Description( icon: "battery.25", title: "Reduced battery usage", description: "More improvements are coming soon!" - ), - FeatureDescription( + )), + .feature(Description( icon: "character", title: "Italian interface", description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" - ) + )), ] ), VersionDescription( version: "v4.6", post: URL(string: "https://simplex.chat/blog/20230328-simplex-chat-v4-6-hidden-profiles.html"), features: [ - FeatureDescription( + .feature(Description( icon: "lock", title: "Hidden chat profiles", description: "Protect your chat profiles with a password!" - ), - FeatureDescription( + )), + .feature(Description( icon: "phone.arrow.up.right", title: "Audio and video calls", description: "Fully re-implemented - work in background!" - ), - FeatureDescription( + )), + .feature(Description( icon: "flag", title: "Group moderation", description: "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" - ), - FeatureDescription( + )), + .feature(Description( icon: "plus.message", title: "Group welcome message", description: "Set the message shown to new members!" - ), - FeatureDescription( + )), + .feature(Description( icon: "battery.50", title: "Further reduced battery usage", description: "More improvements are coming soon!" - ), - FeatureDescription( + )), + .feature(Description( icon: "character", title: "Chinese and Spanish interface", description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" - ), + )), ] ), VersionDescription( version: "v5.0", post: URL(string: "https://simplex.chat/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.html"), features: [ - FeatureDescription( + .feature(Description( icon: "arrow.up.doc", title: "Videos and files up to 1gb", description: "Fast and no wait until the sender is online!" - ), - FeatureDescription( + )), + .feature(Description( icon: "lock", title: "App passcode", description: "Set it instead of system authentication." - ), - FeatureDescription( + )), + .feature(Description( icon: "character", title: "Polish interface", description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" - ), + )), ] ), // Also @@ -199,119 +219,364 @@ private let versionDescriptions: [VersionDescription] = [ version: "v5.1", post: URL(string: "https://simplex.chat/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.html"), features: [ - FeatureDescription( + .feature(Description( icon: "face.smiling", title: "Message reactions", description: "Finally, we have them! 🚀" - ), - FeatureDescription( + )), + .feature(Description( icon: "arrow.up.message", title: "Better messages", description: "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." - ), - FeatureDescription( + )), + .feature(Description( icon: "lock", title: "Self-destruct passcode", description: "All data is erased when it is entered." - ), - FeatureDescription( + )), + .feature(Description( icon: "character", title: "Japanese interface", description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" - ), + )), ] ), VersionDescription( version: "v5.2", post: URL(string: "https://simplex.chat/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.html"), features: [ - FeatureDescription( + .feature(Description( icon: "checkmark", title: "Message delivery receipts!", description: "The second tick we missed! ✅" - ), - FeatureDescription( + )), + .feature(Description( icon: "star", title: "Find chats faster", description: "Filter unread and favorite chats." - ), - FeatureDescription( + )), + .feature(Description( icon: "exclamationmark.arrow.triangle.2.circlepath", title: "Keep your connections", description: "Fix encryption after restoring backups." - ), - FeatureDescription( + )), + .feature(Description( icon: "stopwatch", title: "Make one message disappear", description: "Even when disabled in the conversation." - ), - FeatureDescription( + )), + .feature(Description( icon: "gift", title: "A few more things", description: "- more stable message delivery.\n- a bit better groups.\n- and more!" - ), + )), ] ), VersionDescription( version: "v5.3", post: URL(string: "https://simplex.chat/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html"), features: [ - FeatureDescription( + .feature(Description( icon: "desktopcomputer", title: "New desktop app!", description: "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" - ), - FeatureDescription( + )), + .feature(Description( icon: "lock", title: "Encrypt stored files & media", description: "App encrypts new local files (except videos)." - ), - FeatureDescription( + )), + .feature(Description( icon: "magnifyingglass", title: "Discover and join groups", description: "- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." - ), - FeatureDescription( + )), + .feature(Description( icon: "theatermasks", title: "Simplified incognito mode", description: "Toggle incognito when connecting." - ), - FeatureDescription( + )), + .feature(Description( icon: "character", title: "\(4) new interface languages", description: "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" - ), + )), ] ), VersionDescription( version: "v5.4", post: URL(string: "https://simplex.chat/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.html"), features: [ - FeatureDescription( + .feature(Description( icon: "desktopcomputer", title: "Link mobile and desktop apps! 🔗", description: "Via secure quantum resistant protocol." - ), - FeatureDescription( + )), + .feature(Description( icon: "person.2", title: "Better groups", description: "Faster joining and more reliable messages." - ), - FeatureDescription( + )), + .feature(Description( icon: "theatermasks", title: "Incognito groups", description: "Create a group using a random profile." - ), - FeatureDescription( + )), + .feature(Description( icon: "hand.raised", title: "Block group members", description: "To hide unwanted messages." - ), - FeatureDescription( + )), + .feature(Description( icon: "gift", title: "A few more things", description: "- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" - ), + )), + ] + ), + VersionDescription( + version: "v5.5", + post: URL(string: "https://simplex.chat/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.html"), + features: [ + .feature(Description( + icon: "folder", + title: "Private notes", + description: "With encrypted files and media." + )), + .feature(Description( + icon: "link", + title: "Paste link to connect!", + description: "Search bar accepts invitation links." + )), + .feature(Description( + icon: "bubble.left.and.bubble.right", + title: "Join group conversations", + description: "Recent history and improved [directory bot](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)." + )), + .feature(Description( + icon: "battery.50", + title: "Improved message delivery", + description: "With reduced battery usage." + )), + .feature(Description( + icon: "character", + title: "Turkish interface", + description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" + )), + ] + ), + VersionDescription( + version: "v5.6", + post: URL(string: "https://simplex.chat/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.html"), + features: [ + .feature(Description( + icon: "key", + title: "Quantum resistant encryption", + description: "Enable in direct chats (BETA)!" + )), + .feature(Description( + icon: "tray.and.arrow.up", + title: "App data migration", + description: "Migrate to another device via QR code." + )), + .feature(Description( + icon: "phone", + title: "Picture-in-picture calls", + description: "Use the app while in the call." + )), + .feature(Description( + icon: "hand.raised", + title: "Safer groups", + description: "Admins can block a member for all." + )), + .feature(Description( + icon: "character", + title: "Hungarian interface", + description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" + )), + ] + ), + VersionDescription( + version: "v5.7", + post: URL(string: "https://simplex.chat/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.html"), + features: [ + .feature(Description( + icon: "key", + title: "Quantum resistant encryption", + description: "Will be enabled in direct chats!" + )), + .feature(Description( + icon: "arrowshape.turn.up.forward", + title: "Forward and save messages", + description: "Message source remains private." + )), + .feature(Description( + icon: "music.note", + title: "In-call sounds", + description: "When connecting audio and video calls." + )), + .feature(Description( + icon: "person.crop.square", + title: "Shape profile images", + description: "Square, circle, or anything in between." + )), + .feature(Description( + icon: "antenna.radiowaves.left.and.right", + title: "Network management", + description: "More reliable network connection." + )), + ] + ), + VersionDescription( + version: "v5.8", + post: URL(string: "https://simplex.chat/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html"), + features: [ + .feature(Description( + icon: "arrow.forward", + title: "Private message routing 🚀", + description: "Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." + )), + .feature(Description( + icon: "network.badge.shield.half.filled", + title: "Safely receive files", + description: "Confirm files from unknown servers." + )), + .feature(Description( + icon: "battery.50", + title: "Improved message delivery", + description: "With reduced battery usage." + )), + ] + ), + VersionDescription( + version: "v6.0", + post: URL(string: "https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html"), + features: [ + .feature(Description( + icon: nil, + title: "New chat experience 🎉", + description: nil, + subfeatures: [ + ("link.badge.plus", "Connect to your friends faster."), + ("archivebox", "Archive contacts to chat later."), + ("trash", "Delete up to 20 messages at once."), + ("platter.filled.bottom.and.arrow.down.iphone", "Use the app with one hand."), + ("paintpalette", "Color chats with the new themes."), + ] + )), + .feature(Description( + icon: nil, + title: "New media options", + description: nil, + subfeatures: [ + ("square.and.arrow.up", "Share from other apps."), + ("play.circle", "Play from the chat list."), + ("circle.filled.pattern.diagonalline.rectangle", "Blur for better privacy.") + ] + )), + .feature(Description( + icon: "arrow.forward", + title: "Private message routing 🚀", + description: "It protects your IP address and connections." + )), + .feature(Description( + icon: "network", + title: "Better networking", + description: "Connection and servers status." + )), + ] + ), + VersionDescription( + version: "v6.1", + post: URL(string: "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html"), + features: [ + .feature(Description( + icon: "checkmark.shield", + title: "Better security ✅", + description: "SimpleX protocols reviewed by Trail of Bits." + )), + .feature(Description( + icon: "video", + title: "Better calls", + description: "Switch audio and video during the call." + )), + .feature(Description( + icon: "bolt", + title: "Better notifications", + description: "Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" + )), + .feature(Description( + icon: nil, + title: "Better user experience", + description: nil, + subfeatures: [ + ("link", "Switch chat profile for 1-time invitations."), + ("message", "Customizable message shape."), + ("calendar", "Better message dates."), + ("arrowshape.turn.up.right", "Forward up to 20 messages at once."), + ("flag", "Delete or moderate up to 200 messages.") + ] + )), + ] + ), + VersionDescription( + version: "v6.2", + post: URL(string: "https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html"), + features: [ + .view(FeatureView( + icon: nil, + title: "Network decentralization", + view: { NewOperatorsView() } + )), + .feature(Description( + icon: "briefcase", + title: "Business chats", + description: "Privacy for your customers." + )), + .feature(Description( + icon: "bolt", + title: "More reliable notifications", + description: "Delivered even when Apple drops them." + )), + ] + ), + VersionDescription( + version: "v6.3", + post: URL(string: "https://simplex.chat/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html"), + features: [ + .feature(Description( + icon: "at", + title: "Mention members 👋", + description: "Get notified when mentioned." + )), + .feature(Description( + icon: "flag", + title: "Send private reports", + description: "Help admins moderating their groups." + )), + .feature(Description( + icon: "list.bullet", + title: "Organize chats into lists", + description: "Don't miss important messages." + )), + .feature(Description( + icon: nil, + title: "Better privacy and security", + description: nil, + subfeatures: [ + ("eye.slash", "Private media file names."), + ("trash", "Set message expiration in chats.") + ] + )), + .feature(Description( + icon: nil, + title: "Better groups performance", + description: nil, + subfeatures: [ + ("bolt", "Faster sending messages."), + ("person.2.slash", "Faster deletion of groups.") + ] + )), ] ), ] @@ -328,45 +593,100 @@ func shouldShowWhatsNew() -> Bool { return v != lastVersion } +fileprivate struct NewOperatorsView: View { + var body: some View { + VStack(alignment: .leading) { + Image((operatorsInfo[.flux] ?? ServerOperator.dummyOperatorInfo).largeLogo) + .resizable() + .scaledToFit() + .frame(height: 48) + Text("The second preset operator in the app!") + .multilineTextAlignment(.leading) + .lineLimit(10) + HStack { + Text("Enable Flux in Network & servers settings for better metadata privacy.") + } + } + } +} + +private enum WhatsNewViewSheet: Identifiable { + case showConditions + + var id: String { + switch self { + case .showConditions: return "showConditions" + } + } +} + struct WhatsNewView: View { @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme @State var currentVersion = versionDescriptions.count - 1 @State var currentVersionNav = versionDescriptions.count - 1 var viaSettings = false + var updatedConditions: Bool + @State private var sheetItem: WhatsNewViewSheet? = nil var body: some View { + whatsNewView() + .sheet(item: $sheetItem) { item in + switch item { + case .showConditions: + UsageConditionsView( + currUserServers: Binding.constant([]), + userServers: Binding.constant([]) + ) + .modifier(ThemedBackground(grouped: true)) + } + } + } + + private func whatsNewView() -> some View { VStack { TabView(selection: $currentVersion) { ForEach(Array(versionDescriptions.enumerated()), id: \.0) { (i, v) in - VStack(alignment: .leading, spacing: 16) { - Text("New in \(v.version)") - .font(.title) - .foregroundColor(.secondary) - .frame(maxWidth: .infinity) - .padding(.vertical) - ForEach(v.features, id: \.icon) { f in - featureDescription(f.icon, f.title, f.description) - .padding(.bottom, 8) - } - if let post = v.post { - Link(destination: post) { - HStack { - Text("Read more") - Image(systemName: "arrow.up.right.circle") + ScrollView { + VStack(alignment: .leading, spacing: 16) { + Text("New in \(v.version)") + .font(.title) + .foregroundColor(theme.colors.secondary) + .frame(maxWidth: .infinity) + .padding(.vertical) + ForEach(v.features) { f in + switch f { + case let .feature(d): featureDescription(d).padding(.bottom, 8) + case let .view(v): AnyView(v.view()).padding(.bottom, 8) } } - } - if !viaSettings { - Spacer() - Button("Ok") { - dismiss() + if let post = v.post { + Link(destination: post) { + HStack { + Text("Read more") + Image(systemName: "arrow.up.right.circle") + } + } + } + if updatedConditions { + Button("View updated conditions") { + sheetItem = .showConditions + } + } + if !viaSettings { + Spacer() + + Button("Ok") { + dismiss() + } + .font(.title3) + .frame(maxWidth: .infinity, alignment: .center) + + Spacer() } - .font(.title3) - .frame(maxWidth: .infinity, alignment: .center) - Spacer() } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .tag(i) } } @@ -379,19 +699,42 @@ struct WhatsNewView: View { currentVersionNav = currentVersion } } - - private func featureDescription(_ icon: String, _ title: LocalizedStringKey, _ description: LocalizedStringKey) -> some View { - VStack(alignment: .leading, spacing: 4) { + + @ViewBuilder private func featureHeader(_ icon: String?, _ title: LocalizedStringKey) -> some View { + if let icon { HStack(alignment: .center, spacing: 4) { Image(systemName: icon) .symbolRenderingMode(.monochrome) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .frame(minWidth: 30, alignment: .center) Text(title).font(.title3).bold() } - Text(description) - .multilineTextAlignment(.leading) - .lineLimit(10) + } else { + Text(title).font(.title3).bold() + } + } + + private func featureDescription(_ f: Description) -> some View { + VStack(alignment: .leading, spacing: 4) { + featureHeader(f.icon, f.title) + if let d = f.description { + Text(d) + .multilineTextAlignment(.leading) + .lineLimit(10) + } + if f.subfeatures.count > 0 { + ForEach(f.subfeatures, id: \.icon) { s in + HStack(alignment: .center, spacing: 4) { + Image(systemName: s.icon) + .symbolRenderingMode(.monochrome) + .foregroundColor(theme.colors.secondary) + .frame(minWidth: 30, alignment: .center) + Text(s.description) + .multilineTextAlignment(.leading) + .lineLimit(3) + } + } + } } } @@ -428,6 +771,6 @@ struct WhatsNewView: View { struct NewFeaturesView_Previews: PreviewProvider { static var previews: some View { - WhatsNewView() + WhatsNewView(updatedConditions: false) } } diff --git a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift index 6809dc1385..01b25baed8 100644 --- a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift +++ b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift @@ -12,8 +12,8 @@ import CodeScanner struct ConnectDesktopView: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction - var viaSettings = false @AppStorage(DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS) private var deviceName = UIDevice.current.name @AppStorage(DEFAULT_CONFIRM_REMOTE_SESSIONS) private var confirmRemoteSessions = false @AppStorage(DEFAULT_CONNECT_REMOTE_VIA_MULTICAST) private var connectRemoteViaMulticast = true @@ -36,7 +36,7 @@ struct ConnectDesktopView: View { case badInvitationError case badVersionError(version: String?) case desktopDisconnectedError - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -56,23 +56,6 @@ struct ConnectDesktopView: View { } var body: some View { - if viaSettings { - viewBody - .modifier(BackButton(label: "Back") { - if m.activeRemoteCtrl { - alert = .disconnectDesktop(action: .back) - } else { - dismiss() - } - }) - } else { - NavigationView { - viewBody - } - } - } - - var viewBody: some View { Group { let discovery = m.remoteCtrlSession?.discovery if discovery == true || (discovery == nil && !showConnectScreen) { @@ -159,7 +142,7 @@ struct ConnectDesktopView: View { case .desktopDisconnectedError: Alert(title: Text("Connection terminated")) case let .error(title, error): - Alert(title: Text(title), message: Text(error)) + mkAlert(title: title, message: error) } } .interactiveDismissDisabled(m.activeRemoteCtrl) @@ -167,7 +150,7 @@ struct ConnectDesktopView: View { private func connectDesktopView(showScanner: Bool = true) -> some View { List { - Section("This device name") { + Section(header: Text("This device name").foregroundColor(theme.colors.secondary)) { devicesView() } if showScanner { @@ -178,34 +161,40 @@ struct ConnectDesktopView: View { } } .navigationTitle("Connect to desktop") + .modifier(ThemedBackground(grouped: true)) } private func connectingDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> some View { - List { - Section("Connecting to desktop") { - ctrlDeviceNameText(session, rc) - ctrlDeviceVersionText(session) - } + ZStack { + List { + Section(header: Text("Connecting to desktop").foregroundColor(theme.colors.secondary)) { + ctrlDeviceNameText(session, rc) + ctrlDeviceVersionText(session) + } - if let sessCode = session.sessionCode { - Section("Session code") { - sessionCodeText(sessCode) + if let sessCode = session.sessionCode { + Section(header: Text("Session code").foregroundColor(theme.colors.secondary)) { + sessionCodeText(sessCode) + } + } + + Section { + disconnectButton() } } + .navigationTitle("Connecting to desktop") - Section { - disconnectButton() - } + ProgressView().scaleEffect(2) } - .navigationTitle("Connecting to desktop") + .modifier(ThemedBackground(grouped: true)) } private func searchingDesktopView() -> some View { List { - Section("This device name") { + Section(header: Text("This device name").foregroundColor(theme.colors.secondary)) { devicesView() } - Section("Found desktop") { + Section(header: Text("Found desktop").foregroundColor(theme.colors.secondary)) { Text("Waiting for desktop...").italic() Button { disconnectDesktop() @@ -215,14 +204,15 @@ struct ConnectDesktopView: View { } } .navigationTitle("Connecting to desktop") + .modifier(ThemedBackground(grouped: true)) } @ViewBuilder private func foundDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo, _ compatible: Bool) -> some View { let v = List { - Section("This device name") { + Section(header: Text("This device name").foregroundColor(theme.colors.secondary)) { devicesView() } - Section("Found desktop") { + Section(header: Text("Found desktop").foregroundColor(theme.colors.secondary)) { ctrlDeviceNameText(session, rc) ctrlDeviceVersionText(session) if !compatible { @@ -242,6 +232,7 @@ struct ConnectDesktopView: View { } } .navigationTitle("Found desktop") + .modifier(ThemedBackground(grouped: true)) if compatible && connectRemoteViaMulticastAuto { v.onAppear { confirmKnownDesktop(rc) } @@ -252,12 +243,12 @@ struct ConnectDesktopView: View { private func verifySessionView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?, _ sessCode: String) -> some View { List { - Section("Connected to desktop") { + Section(header: Text("Connected to desktop").foregroundColor(theme.colors.secondary)) { ctrlDeviceNameText(session, rc) ctrlDeviceVersionText(session) } - Section("Verify code with desktop") { + Section(header: Text("Verify code with desktop").foregroundColor(theme.colors.secondary)) { sessionCodeText(sessCode) Button { verifyDesktopSessionCode(sessCode) @@ -271,12 +262,13 @@ struct ConnectDesktopView: View { } } .navigationTitle("Verify connection") + .modifier(ThemedBackground(grouped: true)) } private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text { var t = Text(rc?.deviceViewName ?? session.ctrlAppInfo?.deviceName ?? "") if (rc == nil) { - t = t + Text(" ") + Text("(new)").italic() + t = t + textSpace + Text("(new)").italic() } return t } @@ -285,20 +277,20 @@ struct ConnectDesktopView: View { let v = session.ctrlAppInfo?.appVersionRange.maxVersion var t = Text("v\(v ?? "")") if v != session.appVersion { - t = t + Text(" ") + Text("(this device v\(session.appVersion))").italic() + t = t + textSpace + Text("(this device v\(session.appVersion))").italic() } return t } private func activeSessionView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo) -> some View { List { - Section("Connected desktop") { + Section(header: Text("Connected desktop").foregroundColor(theme.colors.secondary)) { Text(rc.deviceViewName) ctrlDeviceVersionText(session) } if let sessCode = session.sessionCode { - Section("Session code") { + Section(header: Text("Session code").foregroundColor(theme.colors.secondary)) { sessionCodeText(sessCode) } } @@ -308,9 +300,11 @@ struct ConnectDesktopView: View { } footer: { // This is specific to iOS Text("Keep the app open to use it from desktop") + .foregroundColor(theme.colors.secondary) } } .navigationTitle("Connected to desktop") + .modifier(ThemedBackground(grouped: true)) } private func sessionCodeText(_ code: String) -> some View { @@ -331,19 +325,13 @@ struct ConnectDesktopView: View { } private func scanDesctopAddressView() -> some View { - Section("Scan QR code from desktop") { - CodeScannerView(codeTypes: [.qr], scanMode: .oncePerCode, completion: processDesktopQRCode) - .aspectRatio(1, contentMode: .fit) - .cornerRadius(12) - .listRowBackground(Color.clear) - .listRowSeparator(.hidden) - .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - .padding(.horizontal) + Section(header: Text("Scan QR code from desktop").foregroundColor(theme.colors.secondary)) { + ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processDesktopQRCode, scanMode: .oncePerCode) } } private func desktopAddressView() -> some View { - Section("Desktop address") { + Section(header: Text("Desktop address").foregroundColor(theme.colors.secondary)) { if sessionAddress.isEmpty { Button { sessionAddress = UIPasteboard.general.string ?? "" @@ -356,7 +344,7 @@ struct ConnectDesktopView: View { Text(sessionAddress).lineLimit(1) Spacer() Image(systemName: "multiply.circle.fill") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .onTapGesture { sessionAddress = "" } } } @@ -371,7 +359,7 @@ struct ConnectDesktopView: View { private func linkedDesktopsView() -> some View { List { - Section("Desktop devices") { + Section(header: Text("Desktop devices").foregroundColor(theme.colors.secondary)) { ForEach(remoteCtrls, id: \.remoteCtrlId) { rc in remoteCtrlView(rc) } @@ -382,7 +370,7 @@ struct ConnectDesktopView: View { } } - Section("Linked desktop options") { + Section(header: Text("Linked desktop options").foregroundColor(theme.colors.secondary)) { Toggle("Verify connections", isOn: $confirmRemoteSessions) Toggle("Discover via local network", isOn: $connectRemoteViaMulticast) if connectRemoteViaMulticast { @@ -391,6 +379,7 @@ struct ConnectDesktopView: View { } } .navigationTitle("Linked desktops") + .modifier(ThemedBackground(grouped: true)) } private func remoteCtrlView(_ rc: RemoteCtrlInfo) -> some View { @@ -467,12 +456,12 @@ struct ConnectDesktopView: View { } } catch let e { await MainActor.run { - switch e as? ChatResponse { - case .chatCmdError(_, .errorRemoteCtrl(.badInvitation)): alert = .badInvitationError - case .chatCmdError(_, .error(.commandError)): alert = .badInvitationError - case let .chatCmdError(_, .errorRemoteCtrl(.badVersion(v))): alert = .badVersionError(version: v) - case .chatCmdError(_, .errorAgent(.RCP(.version))): alert = .badVersionError(version: nil) - case .chatCmdError(_, .errorAgent(.RCP(.ctrlAuth))): alert = .desktopDisconnectedError + switch e as? ChatError { + case .errorRemoteCtrl(.badInvitation): alert = .badInvitationError + case .error(.commandError): alert = .badInvitationError + case let .errorRemoteCtrl(.badVersion(v)): alert = .badVersionError(version: v) + case .errorAgent(.RCP(.version)): alert = .badVersionError(version: nil) + case .errorAgent(.RCP(.ctrlAuth)): alert = .desktopDisconnectedError default: errorAlert(e) } } diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift index 94a8937db6..554219eb69 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -18,7 +18,9 @@ struct TerminalView: View { @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @State var composeState: ComposeState = ComposeState() + @State var selectedRange = NSRange() @State private var keyboardVisible = false + @State private var keyboardHiddenDate = Date.now @State var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) @State private var terminalItem: TerminalItem? @State private var scrolled = false @@ -96,16 +98,24 @@ struct TerminalView: View { SendMessageView( composeState: $composeState, + selectedRange: $selectedRange, sendMessage: { _ in consoleSendMessage() }, showVoiceMessageButton: false, onMediaAdded: { _ in }, - keyboardVisible: $keyboardVisible + keyboardVisible: $keyboardVisible, + keyboardHiddenDate: $keyboardHiddenDate ) .padding(.horizontal, 12) } } .navigationViewStyle(.stack) - .navigationTitle("Chat console") + .toolbar { + // Redaction broken for `.navigationTitle` - using a toolbar item instead. + ToolbarItem(placement: .principal) { + Text("Chat console").font(.headline) + } + } + .modifier(ThemedBackground()) } func scrollToBottom(_ proxy: ScrollViewProxy, animation: Animation = .default) { @@ -121,6 +131,7 @@ struct TerminalView: View { return ScrollView { Text(s.prefix(maxItemSize)) .padding() + .frame(maxWidth: .infinity) } .toolbar { ToolbarItem(placement: .navigationBarTrailing) { @@ -130,21 +141,22 @@ struct TerminalView: View { } } .onDisappear { terminalItem = nil } + .modifier(ThemedBackground()) } func consoleSendMessage() { - let cmd = ChatCommand.string(composeState.message) if composeState.message.starts(with: "/sql") && (!prefPerformLA || !developerTools) { - let resp = ChatResponse.chatCmdError(user_: nil, chatError: ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty"))) + let resp: APIResult<ChatResponse2> = APIResult.error(ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty"))) Task { - await TerminalItems.shared.addCommand(.now, cmd, resp) + await TerminalItems.shared.addCommand(.now, .string(composeState.message), resp) } } else { + let cmd = composeState.message DispatchQueue.global().async { Task { - composeState.inProgress = true - _ = await chatSendCmd(cmd) - composeState.inProgress = false + await MainActor.run { composeState.inProgress = true } + await sendTerminalCmd(cmd) + await MainActor.run { composeState.inProgress = false } } } } @@ -152,12 +164,38 @@ struct TerminalView: View { } } +func sendTerminalCmd(_ cmd: String) async { + let start: Date = .now + await withCheckedContinuation { (cont: CheckedContinuation<Void, Never>) in + let d = sendSimpleXCmdStr(cmd) + Task { + guard let d else { + await TerminalItems.shared.addCommand(start, ChatCommand.string(cmd), APIResult<ChatResponse2>.error(.invalidJSON(json: nil))) + return + } + let r0: APIResult<ChatResponse0> = decodeAPIResult(d) + guard case .invalid = r0 else { + await TerminalItems.shared.addCommand(start, .string(cmd), r0) + return + } + let r1: APIResult<ChatResponse1> = decodeAPIResult(d) + guard case .invalid = r1 else { + await TerminalItems.shared.addCommand(start, .string(cmd), r1) + return + } + let r2: APIResult<ChatResponse2> = decodeAPIResult(d) + await TerminalItems.shared.addCommand(start, .string(cmd), r2) + } + cont.resume(returning: ()) + } +} + struct TerminalView_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() chatModel.terminalItems = [ - .resp(.now, ChatResponse.response(type: "contactSubscribed", json: "{}")), - .resp(.now, ChatResponse.response(type: "newChatItem", json: "{}")) + .err(.now, APIResult<ChatResponse2>.invalid(type: "contactSubscribed", json: "{}".data(using: .utf8)!).unexpected), + .err(.now, APIResult<ChatResponse2>.invalid(type: "newChatItems", json: "{}".data(using: .utf8)!).unexpected) ] return NavigationView { TerminalView() diff --git a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift b/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift deleted file mode 100644 index 9da3bac00b..0000000000 --- a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift +++ /dev/null @@ -1,170 +0,0 @@ -// -// AdvancedNetworkSettings.swift -// SimpleX (iOS) -// -// Created by Evgeny on 02/08/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import SwiftUI -import SimpleXChat - -private let secondsLabel = NSLocalizedString("sec", comment: "network option") - -enum NetworkSettingsAlert: Identifiable { - case update - case error(err: String) - - var id: String { - switch self { - case .update: return "update" - case let .error(err): return "error \(err)" - } - } -} - -struct AdvancedNetworkSettings: View { - @State private var netCfg = NetCfg.defaults - @State private var currentNetCfg = NetCfg.defaults - @State private var cfgLoaded = false - @State private var enableKeepAlive = true - @State private var keepAliveOpts = KeepAliveOpts.defaults - @State private var showSettingsAlert: NetworkSettingsAlert? - - var body: some View { - VStack { - List { - Section { - Button { - updateNetCfgView(NetCfg.defaults) - showSettingsAlert = .update - } label: { - Text("Reset to defaults") - } - .disabled(currentNetCfg == NetCfg.defaults) - - Button { - updateNetCfgView(NetCfg.proxyDefaults) - showSettingsAlert = .update - } label: { - Text("Set timeouts for proxy/VPN") - } - .disabled(currentNetCfg == NetCfg.proxyDefaults) - - timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [7_500000, 10_000000, 15_000000, 20_000000, 30_000000, 45_000000], label: secondsLabel) - timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel) - timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [15_000, 30_000, 45_000, 60_000, 90_000, 120_000], label: secondsLabel) - timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel) - intSettingPicker("PING count", selection: $netCfg.smpPingCount, values: [1, 2, 3, 5, 8], label: "") - Toggle("Enable TCP keep-alive", isOn: $enableKeepAlive) - - if enableKeepAlive { - intSettingPicker("TCP_KEEPIDLE", selection: $keepAliveOpts.keepIdle, values: [15, 30, 60, 120, 180], label: secondsLabel) - intSettingPicker("TCP_KEEPINTVL", selection: $keepAliveOpts.keepIntvl, values: [5, 10, 15, 30, 60], label: secondsLabel) - intSettingPicker("TCP_KEEPCNT", selection: $keepAliveOpts.keepCnt, values: [1, 2, 4, 6, 8], label: "") - } else { - Group { - Text("TCP_KEEPIDLE") - Text("TCP_KEEPINTVL") - Text("TCP_KEEPCNT") - } - .foregroundColor(.secondary) - } - } header: { - Text("") - } footer: { - HStack { - Button { - updateNetCfgView(currentNetCfg) - } label: { - Label("Revert", systemImage: "arrow.counterclockwise").font(.callout) - } - - Spacer() - - Button { - showSettingsAlert = .update - } label: { - Label("Save", systemImage: "checkmark").font(.callout) - } - } - .disabled(netCfg == currentNetCfg) - } - } - } - .onChange(of: keepAliveOpts) { opts in - netCfg.tcpKeepAlive = keepAliveOpts - } - .onChange(of: enableKeepAlive) { on in - netCfg.tcpKeepAlive = on ? (currentNetCfg.tcpKeepAlive ?? KeepAliveOpts.defaults) : nil - } - .onAppear { - if cfgLoaded { return } - cfgLoaded = true - currentNetCfg = getNetCfg() - updateNetCfgView(currentNetCfg) - } - .alert(item: $showSettingsAlert) { a in - switch a { - case .update: - return Alert( - title: Text("Update network settings?"), - message: Text("Updating settings will re-connect the client to all servers."), - primaryButton: .default(Text("Ok")) { - saveNetCfg() - }, - secondaryButton: .cancel() - ) - case let .error(err): - return Alert( - title: Text("Error updating settings"), - message: Text(err) - ) - } - } - } - - private func updateNetCfgView(_ cfg: NetCfg) { - netCfg = cfg - enableKeepAlive = netCfg.enableKeepAlive - keepAliveOpts = netCfg.tcpKeepAlive ?? KeepAliveOpts.defaults - } - - private func saveNetCfg() { - do { - try setNetworkConfig(netCfg) - currentNetCfg = netCfg - setNetCfg(netCfg) - } catch let error { - let err = responseError(error) - showSettingsAlert = .error(err: err) - logger.error("\(err)") - } - } - - private func intSettingPicker(_ title: LocalizedStringKey, selection: Binding<Int>, values: [Int], label: String) -> some View { - Picker(title, selection: selection) { - ForEach(values, id: \.self) { value in - Text("\(value) \(label)") - } - } - .frame(height: 36) - } - - private func timeoutSettingPicker(_ title: LocalizedStringKey, selection: Binding<Int>, values: [Int], label: String) -> some View { - Picker(title, selection: selection) { - let v = selection.wrappedValue - let vs = values.contains(v) ? values : values + [v] - ForEach(vs, id: \.self) { value in - Text("\(String(format: "%g", (Double(value) / 1000000))) \(secondsLabel)") - } - } - .frame(height: 36) - } -} - -struct AdvancedNetworkSettings_Previews: PreviewProvider { - static var previews: some View { - AdvancedNetworkSettings() - } -} diff --git a/apps/ios/Shared/Views/UserSettings/AppSettings.swift b/apps/ios/Shared/Views/UserSettings/AppSettings.swift new file mode 100644 index 0000000000..44e0b20958 --- /dev/null +++ b/apps/ios/Shared/Views/UserSettings/AppSettings.swift @@ -0,0 +1,107 @@ +// +// AppSettings.swift +// SimpleX (iOS) +// +// Created by Avently on 26.02.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SimpleXChat +import SwiftUI + +extension AppSettings { + public func importIntoApp() { + let def = UserDefaults.standard + if var val = networkConfig { + // migrating from Android/desktop BUT shouldn't be here ever because it should be changed in migration stage + if case .onionViaSocks = val.hostMode { + val.hostMode = .publicHost + val.requiredHostMode = true + } + if val.socksProxy != nil { + val.socksProxy = networkProxy?.toProxyString() + setNetCfg(val, networkProxy: networkProxy) + } else { + val.socksProxy = nil + setNetCfg(val, networkProxy: nil) + } + } + if let val = networkProxy { networkProxyDefault.set(val) } + if let val = privacyEncryptLocalFiles { privacyEncryptLocalFilesGroupDefault.set(val) } + if let val = privacyAskToApproveRelays { privacyAskToApproveRelaysGroupDefault.set(val) } + if let val = privacyAcceptImages { + privacyAcceptImagesGroupDefault.set(val) + def.setValue(val, forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES) + } + if let val = privacyLinkPreviews { + privacyLinkPreviewsGroupDefault.set(val) + def.setValue(val, forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) + } + if let val = privacyShowChatPreviews { def.setValue(val, forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) } + if let val = privacySaveLastDraft { def.setValue(val, forKey: DEFAULT_PRIVACY_SAVE_LAST_DRAFT) } + if let val = privacyProtectScreen { def.setValue(val, forKey: DEFAULT_PRIVACY_PROTECT_SCREEN) } + if let val = privacyMediaBlurRadius { def.setValue(val, forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) } + if let val = notificationMode { ChatModel.shared.notificationMode = val.toNotificationsMode() } + if let val = notificationPreviewMode { ntfPreviewModeGroupDefault.set(val) } + if let val = webrtcPolicyRelay { def.setValue(val, forKey: DEFAULT_WEBRTC_POLICY_RELAY) } + if let val = webrtcICEServers { def.setValue(val, forKey: DEFAULT_WEBRTC_ICE_SERVERS) } + if let val = confirmRemoteSessions { def.setValue(val, forKey: DEFAULT_CONFIRM_REMOTE_SESSIONS) } + if let val = connectRemoteViaMulticast { def.setValue(val, forKey: DEFAULT_CONNECT_REMOTE_VIA_MULTICAST) } + if let val = connectRemoteViaMulticastAuto { def.setValue(val, forKey: DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO) } + if let val = developerTools { def.setValue(val, forKey: DEFAULT_DEVELOPER_TOOLS) } + if let val = confirmDBUpgrades { confirmDBUpgradesGroupDefault.set(val) } + if let val = androidCallOnLockScreen { def.setValue(val.rawValue, forKey: ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN) } + if let val = iosCallKitEnabled { callKitEnabledGroupDefault.set(val) } + if let val = iosCallKitCallsInRecents { def.setValue(val, forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS) } + if let val = uiProfileImageCornerRadius { + profileImageCornerRadiusGroupDefault.set(val) + def.setValue(val, forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) + } + if let val = uiChatItemRoundness { def.setValue(val, forKey: DEFAULT_CHAT_ITEM_ROUNDNESS)} + if let val = uiChatItemTail { def.setValue(val, forKey: DEFAULT_CHAT_ITEM_TAIL)} + if let val = uiColorScheme { currentThemeDefault.set(val) } + if let val = uiDarkColorScheme { systemDarkThemeDefault.set(val) } + if let val = uiCurrentThemeIds { currentThemeIdsDefault.set(val) } + if let val = uiThemes { themeOverridesDefault.set(val.skipDuplicates()) } + if let val = oneHandUI { groupDefaults.setValue(val, forKey: GROUP_DEFAULT_ONE_HAND_UI) } + if let val = chatBottomBar { groupDefaults.setValue(val, forKey: GROUP_DEFAULT_CHAT_BOTTOM_BAR) } + } + + public static var current: AppSettings { + let def = UserDefaults.standard + var c = AppSettings.defaults + c.networkConfig = getNetCfg() + c.networkProxy = networkProxyDefault.get() + c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get() + c.privacyAskToApproveRelays = privacyAskToApproveRelaysGroupDefault.get() + c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get() + c.privacyLinkPreviews = def.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) + 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) + c.privacyMediaBlurRadius = def.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) + c.notificationMode = AppSettingsNotificationMode.from(ChatModel.shared.notificationMode) + c.notificationPreviewMode = ntfPreviewModeGroupDefault.get() + c.webrtcPolicyRelay = def.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY) + c.webrtcICEServers = def.stringArray(forKey: DEFAULT_WEBRTC_ICE_SERVERS) + c.confirmRemoteSessions = def.bool(forKey: DEFAULT_CONFIRM_REMOTE_SESSIONS) + c.connectRemoteViaMulticast = def.bool(forKey: DEFAULT_CONNECT_REMOTE_VIA_MULTICAST) + c.connectRemoteViaMulticastAuto = def.bool(forKey: DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO) + c.developerTools = def.bool(forKey: DEFAULT_DEVELOPER_TOOLS) + c.confirmDBUpgrades = confirmDBUpgradesGroupDefault.get() + c.androidCallOnLockScreen = AppSettingsLockScreenCalls(rawValue: def.string(forKey: ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN)!) + c.iosCallKitEnabled = callKitEnabledGroupDefault.get() + c.iosCallKitCallsInRecents = def.bool(forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS) + c.uiProfileImageCornerRadius = def.double(forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) + c.uiChatItemRoundness = def.double(forKey: DEFAULT_CHAT_ITEM_ROUNDNESS) + c.uiChatItemTail = def.bool(forKey: DEFAULT_CHAT_ITEM_TAIL) + c.uiColorScheme = currentThemeDefault.get() + c.uiDarkColorScheme = systemDarkThemeDefault.get() + c.uiCurrentThemeIds = currentThemeIdsDefault.get() + c.uiThemes = themeOverridesDefault.get() + c.oneHandUI = groupDefaults.bool(forKey: GROUP_DEFAULT_ONE_HAND_UI) + c.chatBottomBar = groupDefaults.bool(forKey: GROUP_DEFAULT_CHAT_BOTTOM_BAR) + return c + } +} diff --git a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift index 1f648b09dc..c6d0e27289 100644 --- a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift @@ -7,21 +7,51 @@ // import SwiftUI +import SimpleXChat +import Yams -let defaultAccentColor = CGColor.init(red: 0, green: 0.533, blue: 1, alpha: 1) +let colorModesLocalized: [LocalizedStringKey] = ["System", "Light", "Dark"] +let colorModesNames: [DefaultThemeMode?] = [nil, DefaultThemeMode.light, DefaultThemeMode.dark] -let interfaceStyles: [UIUserInterfaceStyle] = [.unspecified, .light, .dark] +let darkThemesLocalized: [LocalizedStringKey] = ["Dark", "SimpleX", "Black"] +let darkThemesNames: [String] = [DefaultTheme.DARK.themeName, DefaultTheme.SIMPLEX.themeName, DefaultTheme.BLACK.themeName] -let interfaceStyleNames: [LocalizedStringKey] = ["System", "Light", "Dark"] +let darkThemesWithoutBlackLocalized: [LocalizedStringKey] = ["Dark", "SimpleX"] +let darkThemesWithoutBlackNames: [String] = [DefaultTheme.DARK.themeName, DefaultTheme.SIMPLEX.themeName] let appSettingsURL = URL(string: UIApplication.openSettingsURLString)! struct AppearanceSettings: View { + @EnvironmentObject var m: ChatModel + @Environment(\.colorScheme) var colorScheme @EnvironmentObject var sceneDelegate: SceneDelegate + @EnvironmentObject var theme: AppTheme @State private var iconLightTapped = false @State private var iconDarkTapped = false - @State private var userInterfaceStyle = getUserInterfaceStyleDefault() - @State private var uiTintColor = getUIAccentColorDefault() + @State private var colorMode: DefaultThemeMode? = { + if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME { nil as DefaultThemeMode? } else { CurrentColors.base.mode } + }() + @State private var darkModeTheme: String = UserDefaults.standard.string(forKey: DEFAULT_SYSTEM_DARK_THEME) ?? DefaultTheme.DARK.themeName + @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileImageCornerRadius = defaultProfileImageCorner + @AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var chatItemRoundness = defaultChatItemRoundness + @AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var chatItemTail = true + @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true + @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial + + @State var themeUserDestination: (Int64, ThemeModeOverrides?)? = { + if let currentUser = ChatModel.shared.currentUser, let uiThemes = currentUser.uiThemes, uiThemes.preferredMode(!CurrentColors.colors.isLight) != nil { + (currentUser.userId, uiThemes) + } else { + nil + } + }() + + @State var perUserTheme: ThemeModeOverride = { + ChatModel.shared.currentUser?.uiThemes?.preferredMode(!CurrentColors.colors.isLight) ?? ThemeModeOverride(mode: CurrentColors.base.mode) + }() + + @State var showImageImporter: Bool = false + @State var customizeThemeIsOpen: Bool = false var body: some View { VStack{ @@ -36,42 +66,228 @@ struct AppearanceSettings: View { } } - Section("App icon") { + Section("Chat list") { + Toggle("Reachable chat toolbar", isOn: $oneHandUI) + Picker("Toolbar opacity", selection: $toolbarMaterial) { + ForEach(ToolbarMaterial.allCases, id: \.rawValue) { tm in + Text(tm.text).tag(tm.rawValue) + } + } + .frame(height: 36) + } + + Section { + ThemeDestinationPicker(themeUserDestination: $themeUserDestination, themeUserDest: themeUserDestination?.0, customizeThemeIsOpen: $customizeThemeIsOpen) + + WallpaperPresetSelector( + selectedWallpaper: theme.wallpaper.type, + currentColors: currentColors, + onChooseType: onChooseType + ) + .padding(.bottom, 10) + .listRowInsets(.init()) + .listRowBackground(Color.clear) + .modifier(WallpaperImporter(showImageImporter: $showImageImporter, onChooseImage: { image in + if let filename = saveWallpaperFile(image: image) { + if themeUserDestination == nil, case let WallpaperType.image(filename, _, _) = theme.wallpaper.type { + removeWallpaperFile(fileName: filename) + } else if let type = perUserTheme.type, case let WallpaperType.image(filename, _, _) = type { + removeWallpaperFile(fileName: filename) + } + onTypeChange(WallpaperType.image(filename, 1, WallpaperScaleType.fill)) + } + })) + + if case let WallpaperType.image(filename, _, _) = theme.wallpaper.type, (themeUserDestination == nil || perUserTheme.wallpaper?.imageFile != nil) { + Button { + if themeUserDestination == nil { + let defaultActiveTheme = ThemeManager.defaultActiveTheme(themeOverridesDefault.get()) + ThemeManager.saveAndApplyWallpaper(theme.base, nil, themeOverridesDefault) + ThemeManager.removeTheme(defaultActiveTheme?.themeId) + removeWallpaperFile(fileName: filename) + } else { + removeUserThemeModeOverrides($themeUserDestination, $perUserTheme) + } + saveThemeToDatabase(themeUserDestination) + } label: { + Text("Remove image") + .foregroundColor(theme.colors.primary) + } + .listRowBackground(Color.clear) + } + + Picker("Color mode", selection: $colorMode) { + ForEach(Array(colorModesNames.enumerated()), id: \.element) { index, mode in + Text(colorModesLocalized[index]) + } + } + .frame(height: 36) + Picker("Dark mode colors", selection: $darkModeTheme) { + if theme.base == .BLACK || themeOverridesDefault.get().contains(where: { $0.base == .BLACK }) { + ForEach(Array(darkThemesNames.enumerated()), id: \.element) { index, darkTheme in + Text(darkThemesLocalized[index]) + } + } else { + ForEach(Array(darkThemesWithoutBlackNames.enumerated()), id: \.element) { index, darkTheme in + Text(darkThemesLocalized[index]) + } + } + } + .frame(height: 36) + + NavigationLink { + let userId = themeUserDestination?.0 + if let userId { + UserWallpaperEditorSheet(userId: userId) + .onAppear { + customizeThemeIsOpen = true + } + } else { + CustomizeThemeView(onChooseType: onChooseType) + .navigationTitle("Customize theme") + .modifier(ThemedBackground(grouped: true)) + .onAppear { + customizeThemeIsOpen = true + } + } + } label: { + Text("Customize theme") + } + } header: { + Text("Themes") + .foregroundColor(theme.colors.secondary) + } + .onChange(of: profileImageCornerRadius) { cornerRadius in + profileImageCornerRadiusGroupDefault.set(cornerRadius) + saveThemeToDatabase(nil) + } + .onChange(of: colorMode) { mode in + guard let mode else { + ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME) + return + } + if case DefaultThemeMode.light = mode { + ThemeManager.applyTheme(DefaultTheme.LIGHT.themeName) + } else if case DefaultThemeMode.dark = mode { + ThemeManager.applyTheme(systemDarkThemeDefault.get()) + } + } + .onChange(of: darkModeTheme) { darkTheme in + ThemeManager.changeDarkTheme(darkTheme) + if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME { + ThemeManager.applyTheme(currentThemeDefault.get()) + } else if currentThemeDefault.get() != DefaultTheme.LIGHT.themeName { + ThemeManager.applyTheme(systemDarkThemeDefault.get()) + } + } + + Section(header: Text("Message shape").foregroundColor(theme.colors.secondary)) { + HStack { + Text("Corner") + Slider(value: $chatItemRoundness, in: 0...1, step: 0.05) + } + Toggle("Tail", isOn: $chatItemTail) + } + + Section(header: Text("Profile images").foregroundColor(theme.colors.secondary)) { + HStack(spacing: 16) { + if let img = m.currentUser?.image, img != "" { + ProfileImage(imageStr: img, size: 60) + } else { + clipProfileImage(Image(colorScheme == .light ? "icon-dark" : "icon-light"), size: 60, radius: profileImageCornerRadius) + } + + Slider( + value: $profileImageCornerRadius, + in: 0...50, + step: 2.5 + ) + } + .foregroundColor(theme.colors.secondary) + } + + Section(header: Text("App icon").foregroundColor(theme.colors.secondary)) { HStack { updateAppIcon(image: "icon-light", icon: nil, tapped: $iconLightTapped) Spacer().frame(width: 16) updateAppIcon(image: "icon-dark", icon: "DarkAppIcon", tapped: $iconDarkTapped) } } - - Section { - Picker("Theme", selection: $userInterfaceStyle) { - ForEach(interfaceStyles, id: \.self) { style in - Text(interfaceStyleNames[interfaceStyles.firstIndex(of: style) ?? 0]) - } - } - .frame(height: 36) - ColorPicker("Accent color", selection: $uiTintColor, supportsOpacity: false) - } header: { - Text("Colors") - } footer: { - Button { - uiTintColor = defaultAccentColor - setUIAccentColorDefault(defaultAccentColor) - } label: { - Text("Reset colors").font(.callout) - } - } - .onChange(of: userInterfaceStyle) { _ in - sceneDelegate.window?.overrideUserInterfaceStyle = userInterfaceStyle - setUserInterfaceStyleDefault(userInterfaceStyle) - } - .onChange(of: uiTintColor) { _ in - sceneDelegate.window?.tintColor = UIColor(cgColor: uiTintColor) - setUIAccentColorDefault(uiTintColor) - } } } + .onAppear { + customizeThemeIsOpen = false + } + } + + private func updateThemeUserDestination() { + if let dest = themeUserDestination { + var (userId, themes) = dest + themes = themes ?? ThemeModeOverrides() + if case DefaultThemeMode.light = perUserTheme.mode { + themes?.light = perUserTheme + } else { + themes?.dark = perUserTheme + } + themeUserDestination = (userId, themes) + } + } + + private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool { + if themeUserDestination == nil { + ThemeManager.saveAndApplyWallpaper(theme.base, type, themeOverridesDefault) + } else { + var wallpaperFiles = Set([perUserTheme.wallpaper?.imageFile]) + _ = ThemeManager.copyFromSameThemeOverrides(type, nil, $perUserTheme) + wallpaperFiles.remove(perUserTheme.wallpaper?.imageFile) + wallpaperFiles.forEach(removeWallpaperFile) + updateThemeUserDestination() + } + saveThemeToDatabase(themeUserDestination) + return true + } + + private func onTypeChange(_ type: WallpaperType?) { + if themeUserDestination == nil { + ThemeManager.saveAndApplyWallpaper(theme.base, type, themeOverridesDefault) + } else { + ThemeManager.applyWallpaper(type, $perUserTheme) + updateThemeUserDestination() + } + saveThemeToDatabase(themeUserDestination) + } + + private func currentColors(_ type: WallpaperType?) -> ThemeManager.ActiveTheme { + // If applying for : + // - all themes: no overrides needed + // - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected + let perUserOverride: ThemeModeOverrides? = themeUserDestination == nil + ? nil + : theme.wallpaper.type.sameType(type) + ? m.currentUser?.uiThemes + : nil + return ThemeManager.currentColors(type, nil, perUserOverride, themeOverridesDefault.get()) + } + + private func onChooseType(_ type: WallpaperType?) { + // don't have image in parent or already selected wallpaper with custom image + if let type, case WallpaperType.image = type { + if case WallpaperType.image = theme.wallpaper.type, themeUserDestination?.1 != nil { + showImageImporter = true + } else if currentColors(type).wallpaper.type.image == nil { + showImageImporter = true + } else if currentColors(type).wallpaper.type.image != nil, case WallpaperType.image = theme.wallpaper.type, themeUserDestination == nil { + showImageImporter = true + } else if themeUserDestination == nil { + onTypeChange(currentColors(type).wallpaper.type) + } else { + _ = onTypeCopyFromSameTheme(currentColors(type).wallpaper.type) + } + } else if (themeUserDestination != nil && themeUserDestination?.1?.preferredMode(!CurrentColors.colors.isLight)?.type != type) || theme.wallpaper.type != type { + _ = onTypeCopyFromSameTheme(type) + } else { + onTypeChange(type) + } } private var currentLanguage: String { @@ -93,10 +309,789 @@ struct AppearanceSettings: View { } ._onButtonGesture { tapped.wrappedValue = $0 } perform: {} .overlay(tapped.wrappedValue ? Color.secondary : Color.clear) - .cornerRadius(20) + .cornerRadius(13.5) } } +enum ToolbarMaterial: String, CaseIterable { + case bar + case ultraThin + case thin + case regular + case thick + case ultraThick + + static func material(_ s: String) -> Material { + ToolbarMaterial(rawValue: s)?.material ?? Material.bar + } + + static let defaultMaterial: String = ToolbarMaterial.regular.rawValue + + var material: Material { + switch self { + case .bar: .bar + case .ultraThin: .ultraThin + case .thin: .thin + case .regular: .regular + case .thick: .thick + case .ultraThick: .ultraThick + } + } + + var text: String { + switch self { + case .bar: "System" + case .ultraThin: "Ultra thin" + case .thin: "Thin" + case .regular: "Regular" + case .thick: "Thick" + case .ultraThick: "Ultra thick" + } + } +} + +struct ChatThemePreview: View { + @EnvironmentObject var theme: AppTheme + var base: DefaultTheme + var wallpaperType: WallpaperType? + var backgroundColor: Color? + var tintColor: Color? + var withMessages: Bool = true + + var body: some View { + let themeBackgroundColor = theme.colors.background + let backgroundColor = backgroundColor ?? wallpaperType?.defaultBackgroundColor(theme.base, theme.colors.background) + let tintColor = tintColor ?? wallpaperType?.defaultTintColor(theme.base) + let view = VStack { + if withMessages { + let alice = ChatItem.getSample(1, CIDirection.directRcv, Date.now, NSLocalizedString("Good afternoon!", comment: "message preview")) + let bob = ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir)) + HStack { + ChatItemView(chat: Chat.sampleData, chatItem: alice, scrollToItemId: { _ in }) + .modifier(ChatItemClipped(alice, tailVisible: true)) + Spacer() + } + HStack { + Spacer() + ChatItemView(chat: Chat.sampleData, chatItem: bob, scrollToItemId: { _ in }) + .modifier(ChatItemClipped(bob, tailVisible: true)) + .frame(alignment: .trailing) + } + } else { + Rectangle().fill(.clear) + } + } + .padding(.vertical, 10) + .padding(.horizontal, 16) + .frame(maxWidth: .infinity) + + if let wallpaperType, let wallpaperImage = wallpaperType.image, let backgroundColor, let tintColor { + view.modifier(ChatViewBackground(image: wallpaperImage, imageType: wallpaperType, background: backgroundColor, tint: tintColor)) + } else { + view.background(themeBackgroundColor) + } + } +} + +struct WallpaperPresetSelector: View { + @EnvironmentObject var theme: AppTheme + var selectedWallpaper: WallpaperType? + var activeBackgroundColor: Color? = nil + var activeTintColor: Color? = nil + var currentColors: (WallpaperType?) -> ThemeManager.ActiveTheme + var onChooseType: (WallpaperType?) -> Void + let width: Double = 80 + let height: Double = 80 + let backgrounds = PresetWallpaper.allCases + + private let cornerRadius: Double = 22.5 + + var baseTheme: DefaultTheme { theme.base } + + var body: some View { + VStack { + ChatThemePreview( + base: theme.base, + wallpaperType: selectedWallpaper, + backgroundColor: activeBackgroundColor ?? theme.wallpaper.background, + tintColor: activeTintColor ?? theme.wallpaper.tint + ) + .environmentObject(currentColors(selectedWallpaper).toAppTheme()) + ScrollView(.horizontal, showsIndicators: false) { + HStack { + BackgroundItem(nil) + ForEach(backgrounds, id: \.self) { background in + BackgroundItem(background) + } + OwnBackgroundItem(selectedWallpaper) + } + } + } + } + + func plus() -> some View { + Image(systemName: "plus") + .tint(theme.colors.primary) + .frame(width: 25, height: 25) + } + + func BackgroundItem(_ background: PresetWallpaper?) -> some View { + let checked = (background == nil && (selectedWallpaper == nil || selectedWallpaper?.isEmpty == true)) || selectedWallpaper?.samePreset(other: background) == true + let type = background?.toType(baseTheme, checked ? selectedWallpaper?.scale : nil) + let overrides = currentColors(type).toAppTheme() + return ZStack { + if let type { + ChatThemePreview( + base: baseTheme, + wallpaperType: type, + backgroundColor: checked ? activeBackgroundColor ?? overrides.wallpaper.background : overrides.wallpaper.background, + tintColor: checked ? activeTintColor ?? overrides.wallpaper.tint : overrides.wallpaper.tint, + withMessages: false + ) + .environmentObject(overrides) + } else { + Rectangle().fill(overrides.colors.background) + } + } + .frame(width: CGFloat(width), height: CGFloat(height)) + .clipShape(RoundedRectangle(cornerRadius: width / 100 * cornerRadius)) + .overlay(RoundedRectangle(cornerRadius: width / 100 * cornerRadius) + .strokeBorder(checked ? theme.colors.primary.opacity(0.8) : theme.colors.onBackground.opacity(isInDarkTheme() ? 0.2 : 0.1), lineWidth: 1) + ) + .onTapGesture { + onChooseType(background?.toType(baseTheme)) + } + } + + func OwnBackgroundItem(_ type: WallpaperType?) -> some View { + let overrides = currentColors(WallpaperType.image("", nil, nil)) + let appWallpaper = overrides.wallpaper + let backgroundColor = appWallpaper.background + let tintColor = appWallpaper.tint + let wallpaperImage = appWallpaper.type.image + let checked = if let type, case WallpaperType.image = type, wallpaperImage != nil { true } else { false } + let borderColor = if let type, case WallpaperType.image = type { theme.colors.primary.opacity(0.8) } else { theme.colors.onBackground.opacity(0.1) } + return ZStack { + if checked || wallpaperImage != nil { + ChatThemePreview( + base: baseTheme, + wallpaperType: checked ? type : appWallpaper.type, + backgroundColor: checked ? activeBackgroundColor ?? backgroundColor : backgroundColor, + tintColor: checked ? activeTintColor ?? tintColor : tintColor, + withMessages: false + ) + .environmentObject(currentColors(type).toAppTheme()) + } else { + plus() + } + } + .frame(width: width, height: height) + .clipShape(RoundedRectangle(cornerRadius: width / 100 * cornerRadius)) + .overlay(RoundedRectangle(cornerRadius: width / 100 * cornerRadius) + .strokeBorder(borderColor, lineWidth: 1) + ) + .onTapGesture { + onChooseType(WallpaperType.image("", nil, nil)) + } + } +} + +struct CustomizeThemeView: View { + @EnvironmentObject var theme: AppTheme + var onChooseType: (WallpaperType?) -> Void + @State private var showFileImporter = false + + var body: some View { + List { + let wallpaperImage = theme.wallpaper.type.image + let wallpaperType = theme.wallpaper.type + let baseTheme = theme.base + + let editColor: (ThemeColor) -> Binding<Color> = { name in + editColorBinding( + name: name, + wallpaperType: wallpaperType, + wallpaperImage: wallpaperImage, + theme: theme, + onColorChange: { color in + updateBackendTask.cancel() + updateBackendTask = Task { + if (try? await Task.sleep(nanoseconds: 200_000000)) != nil { + ThemeManager.saveAndApplyThemeColor(baseTheme, name, color) + saveThemeToDatabase(nil) + } + } + }) + } + WallpaperPresetSelector( + selectedWallpaper: wallpaperType, + currentColors: { type in + ThemeManager.currentColors(type, nil, nil, themeOverridesDefault.get()) + }, + onChooseType: onChooseType + ) + .listRowInsets(.init()) + .listRowBackground(Color.clear) + + if case let WallpaperType.image(filename, _, _) = theme.wallpaper.type { + Button { + let defaultActiveTheme = ThemeManager.defaultActiveTheme(themeOverridesDefault.get()) + ThemeManager.saveAndApplyWallpaper(baseTheme, nil, themeOverridesDefault) + ThemeManager.removeTheme(defaultActiveTheme?.themeId) + removeWallpaperFile(fileName: filename) + saveThemeToDatabase(nil) + } label: { + Text("Remove image") + .foregroundColor(theme.colors.primary) + } + .listRowBackground(Color.clear) + } + + Section { + WallpaperSetupView( + wallpaperType: wallpaperType, + base: baseTheme, + initialWallpaper: theme.wallpaper, + editColor: { name in + editColor(name) + }, + onTypeChange: { type in + ThemeManager.saveAndApplyWallpaper(baseTheme, type, themeOverridesDefault) + updateBackendTask.cancel() + updateBackendTask = Task { + if (try? await Task.sleep(nanoseconds: 200_000000)) != nil { + saveThemeToDatabase(nil) + } + } + } + ) + } header: { + Text("Chat colors") + .foregroundColor(theme.colors.secondary) + } + + CustomizeThemeColorsSection(editColor: editColor) + + let currentOverrides = ThemeManager.defaultActiveTheme(themeOverridesDefault.get()) + let canResetColors = theme.base.hasChangedAnyColor(currentOverrides) + if canResetColors { + Button { + ThemeManager.resetAllThemeColors() + saveThemeToDatabase(nil) + } label: { + Text("Reset colors").font(.callout).foregroundColor(theme.colors.primary) + } + } + + ImportExportThemeSection(showFileImporter: $showFileImporter, perChat: nil, perUser: nil) + } + .modifier( + ThemeImporter(isPresented: $showFileImporter) { theme in + ThemeManager.saveAndApplyThemeOverrides(theme) + saveThemeToDatabase(nil) + } + ) + /// When changing app theme, user overrides are hidden. User overrides will be returned back after closing Appearance screen, see ThemeDestinationPicker() + .interactiveDismissDisabled(true) + } +} + +struct ImportExportThemeSection: View { + @EnvironmentObject var theme: AppTheme + @Binding var showFileImporter: Bool + var perChat: ThemeModeOverride? + var perUser: ThemeModeOverrides? + + var body: some View { + Section { + Button { + let overrides = ThemeManager.currentThemeOverridesForExport(nil, perChat, perUser) + do { + let encoded = try encodeThemeOverrides(overrides) + var lines = encoded.split(separator: "\n") + // Removing theme id without using custom serializer or data class + lines.remove(at: 0) + let theme = lines.joined(separator: "\n") + let tempUrl = getTempFilesDirectory().appendingPathComponent("simplex.theme") + try? FileManager.default.removeItem(at: tempUrl) + if FileManager.default.createFile(atPath: tempUrl.path, contents: theme.data(using: .utf8)) { + showShareSheet(items: [tempUrl]) + } + } catch { + AlertManager.shared.showAlertMsg(title: "Error", message: "Error exporting theme: \(error.localizedDescription)") + } + } label: { + Text("Export theme").foregroundColor(theme.colors.primary) + } + Button { + showFileImporter = true + } label: { + Text("Import theme").foregroundColor(theme.colors.primary) + } + } + } +} + +struct ThemeImporter: ViewModifier { + @Binding var isPresented: Bool + var save: (ThemeOverrides) -> Void + + func body(content: Content) -> some View { + content.fileImporter( + isPresented: $isPresented, + allowedContentTypes: [.data/*.plainText*/], + allowsMultipleSelection: false + ) { result in + if case let .success(files) = result, let fileURL = files.first { + do { + var fileSize: Int? = nil + if fileURL.startAccessingSecurityScopedResource() { + let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey]) + fileSize = resourceValues.fileSize + } + if let fileSize = fileSize, + // Same as Android/desktop + fileSize <= 5_500_000 { + if let string = try? String(contentsOf: fileURL, encoding: .utf8), let theme: ThemeOverrides = decodeYAML("themeId: \(UUID().uuidString)\n" + string) { + save(theme) + logger.error("Saved theme from file") + } else { + logger.error("Error decoding theme file") + } + fileURL.stopAccessingSecurityScopedResource() + } else { + fileURL.stopAccessingSecurityScopedResource() + let prettyMaxFileSize = ByteCountFormatter.string(fromByteCount: 5_500_000, countStyle: .binary) + AlertManager.shared.showAlertMsg( + title: "Large file!", + message: "Currently maximum supported file size is \(prettyMaxFileSize)." + ) + } + } catch { + logger.error("Appearance fileImporter error \(error.localizedDescription)") + } + } + } + } +} + +struct UserWallpaperEditorSheet: View { + @Environment(\.dismiss) var dismiss + @EnvironmentObject var theme: AppTheme + @State var userId: Int64 + @State private var globalThemeUsed: Bool = false + + @State private var themes = ChatModel.shared.currentUser?.uiThemes ?? ThemeModeOverrides() + + var body: some View { + let preferred = themes.preferredMode(!theme.colors.isLight) + let initialTheme = preferred ?? ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + UserWallpaperEditor( + initialTheme: initialTheme, + themeModeOverride: initialTheme, + applyToMode: themes.light == themes.dark ? nil : initialTheme.mode, + globalThemeUsed: $globalThemeUsed, + save: { applyToMode, newTheme in + updateBackendTask.cancel() + updateBackendTask = Task { + let themes = ChatModel.shared.currentUser?.uiThemes ?? ThemeModeOverrides() + let initialTheme = themes.preferredMode(!theme.colors.isLight) ?? ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + + await save( + applyToMode, + newTheme, + themes, + userId, + realtimeUpdate: + initialTheme.wallpaper?.preset != newTheme?.wallpaper?.preset || + initialTheme.wallpaper?.imageFile != newTheme?.wallpaper?.imageFile || + initialTheme.wallpaper?.scale != newTheme?.wallpaper?.scale || + initialTheme.wallpaper?.scaleType != newTheme?.wallpaper?.scaleType + ) + } + } + ) + .navigationTitle("Profile theme") + .modifier(ThemedBackground(grouped: true)) + .onAppear { + globalThemeUsed = preferred == nil + } + .onChange(of: theme.base.mode) { _ in + globalThemeUsed = (ChatModel.shared.currentUser?.uiThemes ?? ThemeModeOverrides()).preferredMode(!theme.colors.isLight) == nil + } + .onChange(of: ChatModel.shared.currentUser?.userId) { _ in + dismiss() + } + } + + private func save( + _ applyToMode: DefaultThemeMode?, + _ newTheme: ThemeModeOverride?, + _ themes: ThemeModeOverrides?, + _ userId: Int64, + realtimeUpdate: Bool + ) async { + let unchangedThemes: ThemeModeOverrides = themes ?? ThemeModeOverrides() + var wallpaperFiles = Set([unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile]) + var changedThemes: ThemeModeOverrides? = unchangedThemes + let light: ThemeModeOverride? = if let newTheme { + ThemeModeOverride(mode: DefaultThemeMode.light, colors: newTheme.colors, wallpaper: newTheme.wallpaper?.withFilledWallpaperPath()) + } else { + nil + } + let dark: ThemeModeOverride? = if let newTheme { + ThemeModeOverride(mode: DefaultThemeMode.dark, colors: newTheme.colors, wallpaper: newTheme.wallpaper?.withFilledWallpaperPath()) + } else { + nil + } + + if let applyToMode { + switch applyToMode { + case DefaultThemeMode.light: + changedThemes?.light = light + case DefaultThemeMode.dark: + changedThemes?.dark = dark + } + } else { + changedThemes?.light = light + changedThemes?.dark = dark + } + if changedThemes?.light != nil || changedThemes?.dark != nil { + let light = changedThemes?.light + let dark = changedThemes?.dark + let currentMode = CurrentColors.base.mode + // same image file for both modes, copy image to make them as different files + if var light, var dark, let lightWallpaper = light.wallpaper, let darkWallpaper = dark.wallpaper, let lightImageFile = lightWallpaper.imageFile, let darkImageFile = darkWallpaper.imageFile, lightWallpaper.imageFile == darkWallpaper.imageFile { + let imageFile = if currentMode == DefaultThemeMode.light { + darkImageFile + } else { + lightImageFile + } + let filePath = saveWallpaperFile(url: getWallpaperFilePath(imageFile)) + if currentMode == DefaultThemeMode.light { + dark.wallpaper?.imageFile = filePath + changedThemes = ThemeModeOverrides(light: changedThemes?.light, dark: dark) + } else { + light.wallpaper?.imageFile = filePath + changedThemes = ThemeModeOverrides(light: light, dark: changedThemes?.dark) + } + } + } else { + changedThemes = nil + } + wallpaperFiles.remove(changedThemes?.light?.wallpaper?.imageFile) + wallpaperFiles.remove(changedThemes?.dark?.wallpaper?.imageFile) + wallpaperFiles.forEach(removeWallpaperFile) + + let oldThemes = ChatModel.shared.currentUser?.uiThemes + let changedThemesConstant = changedThemes + if realtimeUpdate { + await MainActor.run { + ChatModel.shared.updateCurrentUserUiThemes(uiThemes: changedThemesConstant) + } + } + do { + try await Task.sleep(nanoseconds: 200_000000) + } catch { + return + } + if !realtimeUpdate { + await MainActor.run { + ChatModel.shared.updateCurrentUserUiThemes(uiThemes: changedThemesConstant) + } + } + + if await !apiSetUserUIThemes(userId: userId, themes: changedThemesConstant) { + await MainActor.run { + // If failed to apply for some reason return the old themes + ChatModel.shared.updateCurrentUserUiThemes(uiThemes: oldThemes) + } + } + } +} + +struct ThemeDestinationPicker: View { + @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme + @Binding var themeUserDestination: (Int64, ThemeModeOverrides?)? + @State var themeUserDest: Int64? + @Binding var customizeThemeIsOpen: Bool + + var body: some View { + let values = [(nil, NSLocalizedString("All profiles", comment: "profile dropdown"))] + m.users.filter { $0.user.activeUser }.map { ($0.user.userId, $0.user.chatViewName)} + + if values.contains(where: { (userId, text) in userId == themeUserDestination?.0 }) { + Picker("Apply to", selection: $themeUserDest) { + ForEach(values, id: \.0) { (_, text) in + Text(text) + } + } + .frame(height: 36) + .onChange(of: themeUserDest) { userId in + themeUserDest = userId + if let userId { + themeUserDestination = (userId, m.users.first { $0.user.userId == userId }?.user.uiThemes) + } else { + themeUserDestination = nil + } + if let userId, userId != m.currentUser?.userId { + changeActiveUser(userId, viewPwd: nil) + } + } + .onChange(of: themeUserDestination == nil) { isNil in + if isNil { + // Easiest way to hide per-user customization. + // Otherwise, it would be needed to make global variable and to use it everywhere for making a decision to include these overrides into active theme constructing or not + m.currentUser?.uiThemes = nil + } else { + m.updateCurrentUserUiThemes(uiThemes: m.users.first(where: { $0.user.userId == m.currentUser?.userId })?.user.uiThemes) + } + } + .onDisappear { + // Skip when Appearance screen is not hidden yet + if customizeThemeIsOpen { return } + // Restore user overrides from stored list of users + m.updateCurrentUserUiThemes(uiThemes: m.users.first(where: { $0.user.userId == m.currentUser?.userId })?.user.uiThemes) + themeUserDestination = if let currentUser = m.currentUser, let uiThemes = currentUser.uiThemes { + (currentUser.userId, uiThemes) + } else { + nil + } + } + } else { + EmptyView() + .onAppear { + themeUserDestination = nil + themeUserDest = nil + } + } + } +} + +struct CustomizeThemeColorsSection: View { + @EnvironmentObject var theme: AppTheme + var editColor: (ThemeColor) -> Binding<Color> + + var body: some View { + Section { + picker(.primary, editColor) + picker(.primaryVariant, editColor) + picker(.secondary, editColor) + picker(.secondaryVariant, editColor) + picker(.background, editColor) + picker(.surface, editColor) + //picker(.title, editColor) + picker(.primaryVariant2, editColor) + } header: { + Text("Interface colors") + .foregroundColor(theme.colors.secondary) + } + } +} + +func editColorBinding(name: ThemeColor, wallpaperType: WallpaperType?, wallpaperImage: Image?, theme: AppTheme, onColorChange: @escaping (Color?) -> Void) -> Binding<Color> { + Binding(get: { + let baseTheme = theme.base + let wallpaperBackgroundColor = theme.wallpaper.background ?? wallpaperType?.defaultBackgroundColor(baseTheme, theme.colors.background) ?? Color.clear + let wallpaperTintColor = theme.wallpaper.tint ?? wallpaperType?.defaultTintColor(baseTheme) ?? Color.clear + return switch name { + case ThemeColor.wallpaperBackground: wallpaperBackgroundColor + case ThemeColor.wallpaperTint: wallpaperTintColor + case ThemeColor.primary: theme.colors.primary + case ThemeColor.primaryVariant: theme.colors.primaryVariant + case ThemeColor.secondary: theme.colors.secondary + case ThemeColor.secondaryVariant: theme.colors.secondaryVariant + case ThemeColor.background: theme.colors.background + case ThemeColor.surface: theme.colors.surface + case ThemeColor.title: theme.appColors.title + case ThemeColor.primaryVariant2: theme.appColors.primaryVariant2 + case ThemeColor.sentMessage: theme.appColors.sentMessage + case ThemeColor.sentQuote: theme.appColors.sentQuote + case ThemeColor.receivedMessage: theme.appColors.receivedMessage + case ThemeColor.receivedQuote: theme.appColors.receivedQuote + } + }, set: onColorChange) +} + +struct WallpaperSetupView: View { + var wallpaperType: WallpaperType? + var base: DefaultTheme + var initialWallpaper: AppWallpaper? + var editColor: (ThemeColor) -> Binding<Color> + var onTypeChange: (WallpaperType?) -> Void + + var body: some View { + if let wallpaperType, case let WallpaperType.image(_, _, scaleType) = wallpaperType { + let wallpaperScaleType = if let scaleType { + scaleType + } else if let initialWallpaper, case let WallpaperType.image(_, _, scaleType) = initialWallpaper.type, let scaleType { + scaleType + } else { + WallpaperScaleType.fill + } + WallpaperScaleTypeChooser(wallpaperScaleType: Binding.constant(wallpaperScaleType), wallpaperType: wallpaperType, onTypeChange: onTypeChange) + } + + + if let wallpaperType, wallpaperType.isPreset { + WallpaperScaleChooser(wallpaperScale: Binding.constant(initialWallpaper?.type.scale ?? 1), wallpaperType: wallpaperType, onTypeChange: onTypeChange) + } else if let wallpaperType, case let WallpaperType.image(_, _, scaleType) = wallpaperType, scaleType == WallpaperScaleType.repeat { + WallpaperScaleChooser(wallpaperScale: Binding.constant(initialWallpaper?.type.scale ?? 1), wallpaperType: wallpaperType, onTypeChange: onTypeChange) + } + + if wallpaperType?.isPreset == true || wallpaperType?.isImage == true { + picker(.wallpaperBackground, editColor) + picker(.wallpaperTint, editColor) + } + + picker(.sentMessage, editColor) + picker(.sentQuote, editColor) + picker(.receivedMessage, editColor) + picker(.receivedQuote, editColor) + + } + + private struct WallpaperScaleChooser: View { + @Binding var wallpaperScale: Float + var wallpaperType: WallpaperType? + var onTypeChange: (WallpaperType?) -> Void + + var body: some View { + HStack { + Text("\(wallpaperScale)".prefix(4)) + .frame(width: 40, height: 36, alignment: .leading) + Slider( + value: Binding(get: { wallpaperScale }, set: { scale in + if let wallpaperType, case let WallpaperType.preset(filename, _) = wallpaperType { + onTypeChange(WallpaperType.preset(filename, Float("\(scale)".prefix(9)))) + } else if let wallpaperType, case let WallpaperType.image(filename, _, scaleType) = wallpaperType { + onTypeChange(WallpaperType.image(filename, Float("\(scale)".prefix(9)), scaleType)) + } + }), + in: 0.5...2, + step: 0.0000001 + ) + .frame(height: 36) + } + } + } + + private struct WallpaperScaleTypeChooser: View { + @Binding var wallpaperScaleType: WallpaperScaleType + var wallpaperType: WallpaperType? + var onTypeChange: (WallpaperType?) -> Void + + var body: some View { + Picker("Scale", selection: Binding(get: { wallpaperScaleType }, set: { scaleType in + if let wallpaperType, case let WallpaperType.image(filename, scale, _) = wallpaperType { + onTypeChange(WallpaperType.image(filename, scale, scaleType)) + } + })) { + ForEach(Array(WallpaperScaleType.allCases), id: \.self) { type in + Text(type.text) + } + } + .frame(height: 36) + } + } +} + +private struct picker: View { + var name: ThemeColor + @State var color: Color + var editColor: (ThemeColor) -> Binding<Color> + // Prevent a race between setting a color here and applying externally changed color to the binding + @State private var lastColorUpdate: Date = .now + + init(_ name: ThemeColor, _ editColor: @escaping (ThemeColor) -> Binding<Color>) { + self.name = name + self.color = editColor(name).wrappedValue + self.editColor = editColor + } + + var body: some View { + ColorPickerView(name: name, selection: $color) + .onChange(of: color) { newColor in + let editedColor = editColor(name) + if editedColor.wrappedValue != newColor { + editedColor.wrappedValue = newColor + lastColorUpdate = .now + } + } + .onChange(of: editColor(name).wrappedValue) { newValue in + // Allows to update underlying color in the picker when color changed externally, for example, by reseting colors of a theme or changing the theme + if lastColorUpdate < Date.now - 1 && newValue != color { + color = newValue + } + } + } +} + +struct ColorPickerView: View { + var name: ThemeColor + @State var selection: Binding<Color> + + var body: some View { + let supportsOpacity = switch name { + case .wallpaperTint: true + case .sentMessage: true + case .sentQuote: true + case .receivedMessage: true + case .receivedQuote: true + default: UIColor(selection.wrappedValue).cgColor.alpha < 1 + } + ColorPicker(name.text, selection: selection, supportsOpacity: supportsOpacity) + } +} + +struct WallpaperImporter: ViewModifier { + @Binding var showImageImporter: Bool + var onChooseImage: (UIImage) -> Void + + func body(content: Content) -> some View { + content.sheet(isPresented: $showImageImporter) { + // LALAL TODO: limit by 5 mb + LibraryMediaListPicker(addMedia: { onChooseImage($0.uiImage) }, selectionLimit: 1, filter: .images, finishedPreprocessing: { }) { itemsSelected in + await MainActor.run { + showImageImporter = false + } + } + } + // content.fileImporter( + // isPresented: $showImageImporter, + // allowedContentTypes: [.image], + // allowsMultipleSelection: false + // ) { result in + // if case let .success(files) = result, let fileURL = files.first { + // do { + // var fileSize: Int? = nil + // if fileURL.startAccessingSecurityScopedResource() { + // let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey]) + // fileSize = resourceValues.fileSize + // } + // fileURL.stopAccessingSecurityScopedResource() + // if let fileSize = fileSize, + // // Same as Android/desktop + // fileSize <= 5_000_000, + // let image = UIImage(contentsOfFile: fileURL.path){ + // onChooseImage(image) + // } else { + // let prettyMaxFileSize = ByteCountFormatter.string(fromByteCount: 5_500_000, countStyle: .binary) + // AlertManager.shared.showAlertMsg( + // title: "Large file!", + // message: "Currently maximum supported file size is \(prettyMaxFileSize)." + // ) + // } + // } catch { + // logger.error("Appearance fileImporter error \(error.localizedDescription)") + // } + // } + // } + } +} + + +/// deprecated. Remove in 2025 func getUIAccentColorDefault() -> CGColor { let defs = UserDefaults.standard return CGColor( @@ -107,15 +1102,78 @@ func getUIAccentColorDefault() -> CGColor { ) } -func setUIAccentColorDefault(_ color: CGColor) { - if let cs = color.components { - let defs = UserDefaults.standard - defs.set(cs[0], forKey: DEFAULT_ACCENT_COLOR_RED) - defs.set(cs[1], forKey: DEFAULT_ACCENT_COLOR_GREEN) - defs.set(cs[2], forKey: DEFAULT_ACCENT_COLOR_BLUE) +private var updateBackendTask: Task = Task {} + +private func saveThemeToDatabase(_ themeUserDestination: (Int64, ThemeModeOverrides?)?) { + let m = ChatModel.shared + let oldThemes = m.currentUser?.uiThemes + if let themeUserDestination { + DispatchQueue.main.async { + // Update before save to make it work seamless + m.updateCurrentUserUiThemes(uiThemes: themeUserDestination.1) + } + } + Task { + if themeUserDestination == nil { + do { + try apiSaveAppSettings(settings: AppSettings.current.prepareForExport()) + } catch { + logger.error("Error saving settings: \(error)") + } + } else if let themeUserDestination, await !apiSetUserUIThemes(userId: themeUserDestination.0, themes: themeUserDestination.1) { + // If failed to apply for some reason return the old themes + m.updateCurrentUserUiThemes(uiThemes: oldThemes) + } } } +private func removeUserThemeModeOverrides(_ themeUserDestination: Binding<(Int64, ThemeModeOverrides?)?>, _ perUserTheme: Binding<ThemeModeOverride>) { + guard let dest = themeUserDestination.wrappedValue else { return } + perUserTheme.wrappedValue = ThemeModeOverride(mode: CurrentColors.base.mode) + themeUserDestination.wrappedValue = (dest.0, nil) + var wallpaperFilesToDelete: [String] = [] + if let type = ChatModel.shared.currentUser?.uiThemes?.light?.type, case let WallpaperType.image(filename, _, _) = type { + wallpaperFilesToDelete.append(filename) + } + if let type = ChatModel.shared.currentUser?.uiThemes?.dark?.type, case let WallpaperType.image(filename, _, _) = type { + wallpaperFilesToDelete.append(filename) + } + wallpaperFilesToDelete.forEach(removeWallpaperFile) +} + +private func decodeYAML<T: Decodable>(_ string: String) -> T? { + do { + return try YAMLDecoder().decode(T.self, from: string) + } catch { + logger.error("Error decoding YAML: \(error)") + return nil + } +} + +private func encodeThemeOverrides(_ value: ThemeOverrides) throws -> String { + let encoder = YAMLEncoder() + encoder.options = YAMLEncoder.Options(sequenceStyle: .block, mappingStyle: .block, newLineScalarStyle: .doubleQuoted) + + guard var node = try Yams.compose(yaml: try encoder.encode(value)) else { + throw RuntimeError("Error while composing a node from object") + } + node["base"]?.scalar?.style = .doubleQuoted + + ThemeColors.CodingKeys.allCases.forEach { key in + node["colors"]?[key.stringValue]?.scalar?.style = .doubleQuoted + } + + ThemeWallpaper.CodingKeys.allCases.forEach { key in + if case .scale = key { + // let number be without quotes + } else { + node["wallpaper"]?[key.stringValue]?.scalar?.style = .doubleQuoted + } + } + return try Yams.serialize(node: node) +} + +/// deprecated. Remove in 2025 func getUserInterfaceStyleDefault() -> UIUserInterfaceStyle { switch UserDefaults.standard.integer(forKey: DEFAULT_USER_INTERFACE_STYLE) { case 1: return .light @@ -124,17 +1182,6 @@ func getUserInterfaceStyleDefault() -> UIUserInterfaceStyle { } } -func setUserInterfaceStyleDefault(_ style: UIUserInterfaceStyle) { - var v: Int - switch style { - case .unspecified: v = 0 - case .light: v = 1 - case .dark: v = 2 - default: v = 0 - } - UserDefaults.standard.set(v, forKey: DEFAULT_USER_INTERFACE_STYLE) -} - struct AppearanceSettings_Previews: PreviewProvider { static var previews: some View { AppearanceSettings() diff --git a/apps/ios/Shared/Views/UserSettings/CallSettings.swift b/apps/ios/Shared/Views/UserSettings/CallSettings.swift index 3409e7ab0e..bae343ee88 100644 --- a/apps/ios/Shared/Views/UserSettings/CallSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/CallSettings.swift @@ -10,6 +10,7 @@ import SwiftUI import SimpleXChat struct CallSettings: View { + @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_WEBRTC_POLICY_RELAY) private var webrtcPolicyRelay = true @AppStorage(GROUP_DEFAULT_CALL_KIT_ENABLED, store: groupDefaults) private var callKitEnabled = true @AppStorage(DEFAULT_CALL_KIT_CALLS_IN_RECENTS) private var callKitCallsInRecents = false @@ -22,17 +23,21 @@ struct CallSettings: View { NavigationLink { RTCServers() .navigationTitle("Your ICE servers") + .modifier(ThemedBackground(grouped: true)) } label: { Text("WebRTC ICE servers") } Toggle("Always use relay", isOn: $webrtcPolicyRelay) } header: { Text("Settings") + .foregroundColor(theme.colors.secondary) } footer: { if webrtcPolicyRelay { Text("Relay server protects your IP address, but it can observe the duration of the call.") + .foregroundColor(theme.colors.secondary) } else { Text("Relay server is only used if necessary. Another party can observe your IP address.") + .foregroundColor(theme.colors.secondary) } } @@ -46,6 +51,7 @@ struct CallSettings: View { } } header: { Text("Interface") + .foregroundColor(theme.colors.secondary) } footer: { if callKitEnabled { Text("You can accept calls from lock screen, without device and app authentication.") @@ -55,7 +61,7 @@ struct CallSettings: View { } } - Section("Limitations") { + Section(header: Text("Limitations").foregroundColor(theme.colors.secondary)) { VStack(alignment: .leading, spacing: 8) { textListItem("1.", "Do NOT use SimpleX for emergency calls.") textListItem("2.", "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.") diff --git a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift index e99c6e3301..54454b7cef 100644 --- a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift +++ b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift @@ -10,8 +10,11 @@ import SwiftUI import SimpleXChat struct DeveloperView: View { + @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false + @State private var hintsUnchanged = hintDefaultsUnchanged() + @Environment(\.colorScheme) var colorScheme var body: some View { @@ -23,47 +26,63 @@ struct DeveloperView: View { .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) } NavigationLink { TerminalView() } label: { - settingsRow("terminal") { Text("Chat console") } + settingsRow("terminal", color: theme.colors.secondary) { Text("Chat console") } } - settingsRow("internaldrive") { - Toggle("Confirm database upgrades", isOn: $confirmDatabaseUpgrades) + settingsRow("lightbulb.max", color: theme.colors.secondary) { + Button("Reset all hints", action: resetHintDefaults) + .disabled(hintsUnchanged) } - settingsRow("chevron.left.forwardslash.chevron.right") { + settingsRow("chevron.left.forwardslash.chevron.right", color: theme.colors.secondary) { Toggle("Show developer options", isOn: $developerTools) } } header: { Text("") } footer: { - (developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option.") + ((developerTools ? Text("Show:") : Text("Hide:")) + textSpace + Text("Database IDs and Transport isolation option.")) + .foregroundColor(theme.colors.secondary) + } + + if developerTools { + Section { + settingsRow("internaldrive", color: theme.colors.secondary) { + Toggle("Confirm database upgrades", isOn: $confirmDatabaseUpgrades) + } + NavigationLink { + StorageView() + .navigationTitle("Storage") + .navigationBarTitleDisplayMode(.large) + } label: { + settingsRow("internaldrive", color: theme.colors.secondary) { Text("Storage") } + } + } header: { + Text("Developer options") + } } - -// Section { -// settingsRow("arrow.up.doc") { -// Toggle("Send videos and files via XFTP", isOn: $xftpSendEnabled) -// .onChange(of: xftpSendEnabled) { _ in -// do { -// try setXFTPConfig(getXFTPCfg()) -// } catch { -// logger.error("setXFTPConfig: cannot set XFTP config \(responseError(error))") -// } -// } -// } -// } header: { -// Text("Experimental") -// } footer: { -// if xftpSendEnabled { -// Text("v4.6.1+ is required to receive via XFTP.") -// } -// } } } } + + private func resetHintDefaults() { + for def in hintDefaults { + if let val = appDefaults[def] as? Bool { + UserDefaults.standard.set(val, forKey: def) + } + } + hintsUnchanged = true + } +} + +private func hintDefaultsUnchanged() -> Bool { + hintDefaults.allSatisfy { def in + appDefaults[def] as? Bool == UserDefaults.standard.bool(forKey: def) + } } struct DeveloperView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/UserSettings/HiddenProfileView.swift b/apps/ios/Shared/Views/UserSettings/HiddenProfileView.swift index 509874619f..5f20055b2b 100644 --- a/apps/ios/Shared/Views/UserSettings/HiddenProfileView.swift +++ b/apps/ios/Shared/Views/UserSettings/HiddenProfileView.swift @@ -13,6 +13,7 @@ struct HiddenProfileView: View { @State var user: User @Binding var profileHidden: Bool @EnvironmentObject private var m: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction @State private var hidePassword = "" @State private var confirmHidePassword = "" @@ -36,7 +37,7 @@ struct HiddenProfileView: View { PassphraseField(key: $hidePassword, placeholder: "Password to show", valid: passwordValid, showStrength: true) PassphraseField(key: $confirmHidePassword, placeholder: "Confirm password", valid: confirmValid) - settingsRow("lock") { + settingsRow("lock", color: theme.colors.secondary) { Button("Save profile password") { Task { do { @@ -58,8 +59,10 @@ struct HiddenProfileView: View { .disabled(saveDisabled) } header: { Text("Hidden profile password") + .foregroundColor(theme.colors.secondary) } footer: { Text("To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page.") + .foregroundColor(theme.colors.secondary) .font(.body) .padding(.top, 8) } @@ -70,6 +73,7 @@ struct HiddenProfileView: View { message: Text(savePasswordError ?? "") ) } + .modifier(ThemedBackground(grouped: true)) } var passwordValid: Bool { hidePassword == hidePassword.trimmingCharacters(in: .whitespaces) } diff --git a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift index 20dadb7954..d9862aaac8 100644 --- a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift +++ b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift @@ -10,24 +10,25 @@ import SwiftUI struct IncognitoHelp: View { var body: some View { - VStack(alignment: .leading) { + List { Text("Incognito mode") .font(.largeTitle) .bold() + .fixedSize(horizontal: false, vertical: true) .padding(.vertical) - ScrollView { - VStack(alignment: .leading) { - Group { - 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.") - } - .padding(.bottom) - } + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + VStack(alignment: .leading, spacing: 18) { + 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).") } + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) } - .frame(maxWidth: .infinity) - .padding() + .modifier(ThemedBackground()) } } diff --git a/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift b/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift index afb0af66c1..71c284e9ab 100644 --- a/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift +++ b/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift @@ -9,6 +9,8 @@ import SwiftUI struct MarkdownHelp: View { + @EnvironmentObject var theme: AppTheme + var body: some View { VStack(alignment: .leading, spacing: 8) { Text("You can use markdown to format messages:") @@ -17,11 +19,11 @@ struct MarkdownHelp: View { mdFormat("_italic_", Text("italic").italic()) mdFormat("~strike~", Text("strike").strikethrough()) mdFormat("`a + b`", Text("`a + b`").font(.body.monospaced())) - mdFormat("!1 colored!", Text("colored").foregroundColor(.red) + Text(" (") + color("1", .red) + color("2", .green) + color("3", .blue) + color("4", .yellow) + color("5", .cyan) + Text("6").foregroundColor(.purple) + Text(")")) + mdFormat("!1 colored!", Text("colored").foregroundColor(.red) + Text(verbatim: " (") + color("1", .red) + color("2", .green) + color("3", .blue) + color("4", .yellow) + color("5", .cyan) + Text("6").foregroundColor(.purple) + Text(verbatim: ")")) ( mdFormat("#secret#", Text("secret") .foregroundColor(.clear) - .underline(color: .primary) + Text(" (can be copied)")) + .underline(color: theme.colors.onBackground) + Text(" (can be copied)")) ) .textSelection(.enabled) } @@ -37,7 +39,7 @@ private func mdFormat(_ format: LocalizedStringKey, _ example: Text) -> some Vie } private func color(_ s: String, _ c: Color) -> Text { - Text(s).foregroundColor(c) + Text(", ") + Text(s).foregroundColor(c) + Text(verbatim: ", ") } struct MarkdownHelp_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift deleted file mode 100644 index d721cfad50..0000000000 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift +++ /dev/null @@ -1,182 +0,0 @@ -// -// NetworkServersView.swift -// SimpleX (iOS) -// -// Created by Evgeny on 02/08/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import SwiftUI -import SimpleXChat - -private enum NetworkAlert: Identifiable { - case updateOnionHosts(hosts: OnionHosts) - case updateSessionMode(mode: TransportSessionMode) - case error(err: String) - - var id: String { - switch self { - case let .updateOnionHosts(hosts): return "updateOnionHosts \(hosts)" - case let .updateSessionMode(mode): return "updateSessionMode \(mode)" - case let .error(err): return "error \(err)" - } - } -} - -struct NetworkAndServers: View { - @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false - @State private var cfgLoaded = false - @State private var currentNetCfg = NetCfg.defaults - @State private var netCfg = NetCfg.defaults - @State private var onionHosts: OnionHosts = .no - @State private var sessionMode: TransportSessionMode = .user - @State private var alert: NetworkAlert? - - var body: some View { - VStack { - List { - Section { - NavigationLink { - ProtocolServersView(serverProtocol: .smp) - .navigationTitle("Your SMP servers") - } label: { - Text("SMP servers") - } - - NavigationLink { - ProtocolServersView(serverProtocol: .xftp) - .navigationTitle("Your XFTP servers") - } label: { - Text("XFTP servers") - } - - Picker("Use .onion hosts", selection: $onionHosts) { - ForEach(OnionHosts.values, id: \.self) { Text($0.text) } - } - .frame(height: 36) - - if developerTools { - Picker("Transport isolation", selection: $sessionMode) { - ForEach(TransportSessionMode.values, id: \.self) { Text($0.text) } - } - .frame(height: 36) - } - - NavigationLink { - AdvancedNetworkSettings() - .navigationTitle("Network settings") - } label: { - Text("Advanced network settings") - } - } header: { - Text("Messages & files") - } footer: { - Text("Using .onion hosts requires compatible VPN provider.") - } - - Section("Calls") { - NavigationLink { - RTCServers() - .navigationTitle("Your ICE servers") - } label: { - Text("WebRTC ICE servers") - } - } - } - } - .onAppear { - if cfgLoaded { return } - cfgLoaded = true - currentNetCfg = getNetCfg() - resetNetCfgView() - } - .onChange(of: onionHosts) { _ in - if onionHosts != OnionHosts(netCfg: currentNetCfg) { - alert = .updateOnionHosts(hosts: onionHosts) - } - } - .onChange(of: sessionMode) { _ in - if sessionMode != netCfg.sessionMode { - alert = .updateSessionMode(mode: sessionMode) - } - } - .alert(item: $alert) { a in - switch a { - case let .updateOnionHosts(hosts): - return Alert( - title: Text("Update .onion hosts setting?"), - message: Text(onionHostsInfo(hosts)) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."), - primaryButton: .default(Text("Ok")) { - let (hostMode, requiredHostMode) = hosts.hostMode - netCfg.hostMode = hostMode - netCfg.requiredHostMode = requiredHostMode - saveNetCfg() - }, - secondaryButton: .cancel() { - resetNetCfgView() - } - ) - case let .updateSessionMode(mode): - return Alert( - title: Text("Update transport isolation mode?"), - message: Text(sessionModeInfo(mode)) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."), - primaryButton: .default(Text("Ok")) { - netCfg.sessionMode = mode - saveNetCfg() - }, - secondaryButton: .cancel() { - resetNetCfgView() - } - ) - case let .error(err): - return Alert( - title: Text("Error updating settings"), - message: Text(err) - ) - } - } - } - - private func saveNetCfg() { - do { - let def = netCfg.hostMode == .onionHost ? NetCfg.proxyDefaults : NetCfg.defaults - netCfg.tcpConnectTimeout = def.tcpConnectTimeout - netCfg.tcpTimeout = def.tcpTimeout - try setNetworkConfig(netCfg) - currentNetCfg = netCfg - setNetCfg(netCfg) - } catch let error { - let err = responseError(error) - resetNetCfgView() - alert = .error(err: err) - logger.error("\(err)") - } - } - - private func resetNetCfgView() { - netCfg = currentNetCfg - onionHosts = OnionHosts(netCfg: netCfg) - sessionMode = netCfg.sessionMode - } - - private func onionHostsInfo(_ hosts: OnionHosts) -> LocalizedStringKey { - switch hosts { - case .no: return "Onion hosts will not be used." - case .prefer: return "Onion hosts will be used when available. Requires enabling VPN." - case .require: return "Onion hosts will be required for connection. Requires enabling VPN." - } - } - - private func sessionModeInfo(_ mode: TransportSessionMode) -> LocalizedStringKey { - switch mode { - case .user: return "A separate TCP connection will be used **for each chat profile you have in the app**." - case .entity: return "A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." - } - } -} - -struct NetworkServersView_Previews: PreviewProvider { - static var previews: some View { - NetworkAndServers() - } -} diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift new file mode 100644 index 0000000000..fa698f8b7c --- /dev/null +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift @@ -0,0 +1,404 @@ +// +// AdvancedNetworkSettings.swift +// SimpleX (iOS) +// +// Created by Evgeny on 02/08/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +private let secondsLabel = NSLocalizedString("sec", comment: "network option") + +enum NetworkSettingsAlert: Identifiable { + case update + case error(err: String) + + var id: String { + switch self { + case .update: return "update" + case let .error(err): return "error \(err)" + } + } +} + +struct AdvancedNetworkSettings: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme + @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false + @State private var netCfg = NetCfg.defaults + @State private var currentNetCfg = NetCfg.defaults + @State private var cfgLoaded = false + @State private var enableKeepAlive = true + @State private var keepAliveOpts = KeepAliveOpts.defaults + @State private var showSettingsAlert: NetworkSettingsAlert? + @State private var onionHosts: OnionHosts = .no + @State private var showSaveDialog = false + @State private var netProxy = networkProxyDefault.get() + @State private var currentNetProxy = networkProxyDefault.get() + @State private var useNetProxy = false + @State private var netProxyAuth = false + + var body: some View { + VStack { + List { + Section { + NavigationLink { + List { + Section { + SelectionListView(list: SMPProxyMode.values, selection: $netCfg.smpProxyMode) { mode in + netCfg.smpProxyMode = mode + } + } footer: { + Text(proxyModeInfo(netCfg.smpProxyMode)) + .font(.callout) + .foregroundColor(theme.colors.secondary) + } + } + .navigationTitle("Private routing") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.inline) + } label: { + HStack { + Text("Private routing") + Spacer() + Text(netCfg.smpProxyMode.label) + } + } + + NavigationLink { + List { + Section { + SelectionListView(list: SMPProxyFallback.values, selection: $netCfg.smpProxyFallback) { mode in + netCfg.smpProxyFallback = mode + } + .disabled(netCfg.smpProxyMode == .never) + } footer: { + Text(proxyFallbackInfo(netCfg.smpProxyFallback)) + .font(.callout) + .foregroundColor(theme.colors.secondary) + } + } + .navigationTitle("Allow downgrade") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.inline) + } label: { + HStack { + Text("Allow downgrade") + Spacer() + Text(netCfg.smpProxyFallback.label) + } + } + + Toggle("Show message status", isOn: $showSentViaProxy) + } header: { + Text("Private message routing") + .foregroundColor(theme.colors.secondary) + } footer: { + VStack(alignment: .leading) { + Text("To protect your IP address, private routing uses your SMP servers to deliver messages.") + if showSentViaProxy { + Text("Show → on messages sent via private routing.") + } + } + .foregroundColor(theme.colors.secondary) + } + + Section { + Toggle("Use SOCKS proxy", isOn: $useNetProxy) + Group { + TextField("IP address", text: $netProxy.host) + TextField( + "Port", + text: Binding( + get: { netProxy.port > 0 ? "\(netProxy.port)" : "" }, + set: { s in + netProxy.port = if let port = Int(s), port > 0 { + port + } else { + 0 + } + } + ) + ) + Toggle("Proxy requires password", isOn: $netProxyAuth) + if netProxyAuth { + TextField("Username", text: $netProxy.username) + PassphraseField( + key: $netProxy.password, + placeholder: "Password", + valid: NetworkProxy.validCredential(netProxy.password) + ) + } + } + .if(!useNetProxy) { $0.foregroundColor(theme.colors.secondary) } + .disabled(!useNetProxy) + } header: { + HStack { + Text("SOCKS proxy").foregroundColor(theme.colors.secondary) + if useNetProxy && !netProxy.valid { + Spacer() + Image(systemName: "exclamationmark.circle.fill").foregroundColor(.red) + } + } + } footer: { + if netProxyAuth { + Text("Your credentials may be sent unencrypted.") + .foregroundColor(theme.colors.secondary) + } else { + Text("Do not use credentials with proxy.") + .foregroundColor(theme.colors.secondary) + } + } + .onChange(of: useNetProxy) { useNetProxy in + netCfg.socksProxy = useNetProxy && currentNetProxy.valid + ? currentNetProxy.toProxyString() + : nil + netProxy = currentNetProxy + netProxyAuth = netProxy.username != "" || netProxy.password != "" + } + .onChange(of: netProxyAuth) { netProxyAuth in + if netProxyAuth { + netProxy.auth = currentNetProxy.auth + netProxy.username = currentNetProxy.username + netProxy.password = currentNetProxy.password + } else { + netProxy.auth = .username + netProxy.username = "" + netProxy.password = "" + } + } + .onChange(of: netProxy) { netProxy in + netCfg.socksProxy = useNetProxy && netProxy.valid + ? netProxy.toProxyString() + : nil + } + + Section { + Picker("Use .onion hosts", selection: $onionHosts) { + ForEach(OnionHosts.values, id: \.self) { Text($0.text) } + } + .frame(height: 36) + } footer: { + Text(onionHostsInfo(onionHosts)) + .foregroundColor(theme.colors.secondary) + } + .onChange(of: onionHosts) { hosts in + if hosts != OnionHosts(netCfg: currentNetCfg) { + let (hostMode, requiredHostMode) = hosts.hostMode + netCfg.hostMode = hostMode + netCfg.requiredHostMode = requiredHostMode + } + } + + if developerTools { + Section { + Picker("Transport isolation", selection: $netCfg.sessionMode) { + let modes = TransportSessionMode.values.contains(netCfg.sessionMode) + ? TransportSessionMode.values + : TransportSessionMode.values + [netCfg.sessionMode] + ForEach(modes, id: \.self) { Text($0.text) } + } + .frame(height: 36) + } footer: { + sessionModeInfo(netCfg.sessionMode) + .foregroundColor(theme.colors.secondary) + } + } + + Section { + Picker("Use web port", selection: $netCfg.smpWebPortServers) { + ForEach(SMPWebPortServers.allCases, id: \.self) { Text($0.text) } + } + .frame(height: 36) + } header: { + Text("TCP port for messaging") + } footer: { + netCfg.smpWebPortServers == .preset + ? Text("Use TCP port 443 for preset servers only.") + : Text("Use TCP port \(netCfg.smpWebPortServers == .all ? "443" : "5223") when no port is specified.") + } + + Section("TCP connection") { + timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [10_000000, 15_000000, 20_000000, 30_000000, 45_000000, 60_000000, 90_000000], label: secondsLabel) + timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel) + timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [2_500, 5_000, 10_000, 15_000, 20_000, 30_000], label: secondsLabel) + // intSettingPicker("Receiving concurrency", selection: $netCfg.rcvConcurrency, values: [1, 2, 4, 8, 12, 16, 24], label: "") + timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel) + intSettingPicker("PING count", selection: $netCfg.smpPingCount, values: [1, 2, 3, 5, 8], label: "") + Toggle("Enable TCP keep-alive", isOn: $enableKeepAlive) + + if enableKeepAlive { + intSettingPicker("TCP_KEEPIDLE", selection: $keepAliveOpts.keepIdle, values: [15, 30, 60, 120, 180], label: secondsLabel) + intSettingPicker("TCP_KEEPINTVL", selection: $keepAliveOpts.keepIntvl, values: [5, 10, 15, 30, 60], label: secondsLabel) + intSettingPicker("TCP_KEEPCNT", selection: $keepAliveOpts.keepCnt, values: [1, 2, 4, 6, 8], label: "") + } else { + Group { + Text("TCP_KEEPIDLE") + Text("TCP_KEEPINTVL") + Text("TCP_KEEPCNT") + } + .foregroundColor(theme.colors.secondary) + } + } + + Section { + Button("Reset to defaults") { + updateNetCfgView(NetCfg.defaults, NetworkProxy.def) + } + .disabled(netCfg == NetCfg.defaults) + + Button("Set timeouts for proxy/VPN") { + updateNetCfgView(netCfg.withProxyTimeouts, netProxy) + } + .disabled(netCfg.hasProxyTimeouts) + + Button("Save and reconnect") { + showSettingsAlert = .update + } + .disabled(netCfg == currentNetCfg || (useNetProxy && !netProxy.valid)) + } + } + } + .onChange(of: keepAliveOpts) { opts in + netCfg.tcpKeepAlive = keepAliveOpts + } + .onChange(of: enableKeepAlive) { on in + netCfg.tcpKeepAlive = on ? (currentNetCfg.tcpKeepAlive ?? KeepAliveOpts.defaults) : nil + } + .onAppear { + if cfgLoaded { return } + cfgLoaded = true + currentNetCfg = getNetCfg() + currentNetProxy = networkProxyDefault.get() + updateNetCfgView(currentNetCfg, currentNetProxy) + } + .alert(item: $showSettingsAlert) { a in + switch a { + case .update: + return Alert( + title: Text("Update settings?"), + message: Text("Updating settings will re-connect the client to all servers."), + primaryButton: .default(Text("Ok")) { + _ = saveNetCfg() + }, + secondaryButton: .cancel() + ) + case let .error(err): + return Alert( + title: Text("Error updating settings"), + message: Text(err) + ) + } + } + .modifier(BackButton(disabled: Binding.constant(false)) { + if netCfg == currentNetCfg { + dismiss() + cfgLoaded = false + } else if !useNetProxy || netProxy.valid { + showSaveDialog = true + } + }) + .confirmationDialog("Update network settings?", isPresented: $showSaveDialog, titleVisibility: .visible) { + Button("Save and reconnect") { + if saveNetCfg() { + dismiss() + cfgLoaded = false + } + } + Button("Exit without saving") { dismiss() } + } + } + + private func updateNetCfgView(_ cfg: NetCfg, _ proxy: NetworkProxy) { + netCfg = cfg + netProxy = proxy + onionHosts = OnionHosts(netCfg: netCfg) + enableKeepAlive = netCfg.enableKeepAlive + keepAliveOpts = netCfg.tcpKeepAlive ?? KeepAliveOpts.defaults + useNetProxy = netCfg.socksProxy != nil + netProxyAuth = switch netProxy.auth { + case .username: netProxy.username != "" || netProxy.password != "" + case .isolate: false + } + } + + private func saveNetCfg() -> Bool { + do { + try setNetworkConfig(netCfg) + currentNetCfg = netCfg + setNetCfg(netCfg, networkProxy: useNetProxy ? netProxy : nil) + currentNetProxy = netProxy + networkProxyDefault.set(netProxy) + return true + } catch let error { + let err = responseError(error) + showSettingsAlert = .error(err: err) + logger.error("\(err)") + return false + } + } + + private func intSettingPicker(_ title: LocalizedStringKey, selection: Binding<Int>, values: [Int], label: String) -> some View { + Picker(title, selection: selection) { + ForEach(values, id: \.self) { value in + Text("\(value) \(label)") + } + } + .frame(height: 36) + } + + private func timeoutSettingPicker(_ title: LocalizedStringKey, selection: Binding<Int>, values: [Int], label: String) -> some View { + Picker(title, selection: selection) { + let v = selection.wrappedValue + let vs = values.contains(v) ? values : values + [v] + ForEach(vs, id: \.self) { value in + Text("\(String(format: "%g", (Double(value) / 1000000))) \(secondsLabel)") + } + } + .frame(height: 36) + } + + private func onionHostsInfo(_ hosts: OnionHosts) -> LocalizedStringKey { + switch hosts { + case .no: return "Onion hosts will not be used." + case .prefer: return "Onion hosts will be used when available.\nRequires compatible VPN." + case .require: return "Onion hosts will be **required** for connection.\nRequires compatible VPN." + } + } + + private func sessionModeInfo(_ mode: TransportSessionMode) -> Text { + let userMode = Text("A separate TCP connection will be used **for each chat profile you have in the app**.") + return switch mode { + case .user: userMode + case .session: userMode + textNewLine + Text("New SOCKS credentials will be used every time you start the app.") + case .server: userMode + textNewLine + Text("New SOCKS credentials will be used for each server.") + case .entity: Text("A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.") + } + } + + private func proxyModeInfo(_ mode: SMPProxyMode) -> LocalizedStringKey { + switch mode { + case .always: return "Always use private routing." + case .unknown: return "Use private routing with unknown servers." + case .unprotected: return "Use private routing with unknown servers when IP address is not protected." + case .never: return "Do NOT use private routing." + } + } + + private func proxyFallbackInfo(_ proxyFallback: SMPProxyFallback) -> LocalizedStringKey { + switch proxyFallback { + case .allow: return "Send messages directly when your or destination server does not support private routing." + case .allowProtected: return "Send messages directly when IP address is protected and your or destination server does not support private routing." + case .prohibit: return "Do NOT send messages directly, even if your or destination server does not support private routing." + } + } +} + +struct AdvancedNetworkSettings_Previews: PreviewProvider { + static var previews: some View { + AdvancedNetworkSettings() + } +} diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift new file mode 100644 index 0000000000..1e38b7d5ec --- /dev/null +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift @@ -0,0 +1,83 @@ +// +// ConditionsWebView.swift +// SimpleX (iOS) +// +// Created by Stanislav Dmitrenko on 26.11.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import WebKit + +struct ConditionsWebView: UIViewRepresentable { + @State var html: String + @EnvironmentObject var theme: AppTheme + @State var pageLoaded = false + + func makeUIView(context: Context) -> WKWebView { + let view = WKWebView() + view.backgroundColor = .clear + view.isOpaque = false + view.navigationDelegate = context.coordinator + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + // just to make sure that even if updateUIView will not be called for any reason, the page + // will be rendered anyway + if !pageLoaded { + loadPage(view) + } + } + return view + } + + func updateUIView(_ view: WKWebView, context: Context) { + loadPage(view) + } + + private func loadPage(_ webView: WKWebView) { + let styles = """ + <style> + body { + color: \(theme.colors.onBackground.toHTMLHex()); + font-family: Helvetica; + } + a { + color: \(theme.colors.primary.toHTMLHex()); + } + code, pre { + font-family: Menlo; + background: \(theme.colors.secondary.opacity(theme.colors.isLight ? 0.2 : 0.3).toHTMLHex()); + } + </style> + """ + let head = "<head><meta name='viewport' content='width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=no'>\(styles)</head>" + webView.loadHTMLString(head + html, baseURL: nil) + DispatchQueue.main.async { + pageLoaded = true + } + } + + func makeCoordinator() -> Cordinator { + Cordinator() + } + + class Cordinator: NSObject, WKNavigationDelegate { + func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + + guard let url = navigationAction.request.url else { return decisionHandler(.allow) } + + 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) + } + default: + decisionHandler(.allow) + } + } + } +} diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift new file mode 100644 index 0000000000..6f4710396a --- /dev/null +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift @@ -0,0 +1,480 @@ +// +// NetworkServersView.swift +// SimpleX (iOS) +// +// Created by Evgeny on 02/08/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +private enum NetworkAlert: Identifiable { + case error(err: String) + + var id: String { + switch self { + case let .error(err): return "error \(err)" + } + } +} + +private enum NetworkAndServersSheet: Identifiable { + case showConditions + + var id: String { + switch self { + case .showConditions: return "showConditions" + } + } +} + +struct NetworkAndServers: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var m: ChatModel + @Environment(\.colorScheme) var colorScheme: ColorScheme + @EnvironmentObject var theme: AppTheme + @EnvironmentObject var ss: SaveableSettings + @State private var sheetItem: NetworkAndServersSheet? = nil + @State private var justOpened = true + @State private var showSaveDialog = false + + var body: some View { + VStack { + List { + let conditionsAction = m.conditions.conditionsAction + let anyOperatorEnabled = ss.servers.userServers.contains(where: { $0.operator?.enabled ?? false }) + Section { + ForEach(ss.servers.userServers.enumerated().map { $0 }, id: \.element.id) { idx, userOperatorServers in + if let serverOperator = userOperatorServers.operator { + serverOperatorView(idx, serverOperator) + } else { + EmptyView() + } + } + + if let conditionsAction = conditionsAction, anyOperatorEnabled { + conditionsButton(conditionsAction) + } + } header: { + Text("Preset servers") + .foregroundColor(theme.colors.secondary) + } footer: { + switch conditionsAction { + case let .review(_, deadline, _): + if let deadline = deadline, anyOperatorEnabled { + Text("Conditions will be accepted on: \(conditionsTimestamp(deadline)).") + .foregroundColor(theme.colors.secondary) + } + default: + EmptyView() + } + } + + Section { + if let idx = ss.servers.userServers.firstIndex(where: { $0.operator == nil }) { + NavigationLink { + YourServersView( + userServers: $ss.servers.userServers, + serverErrors: $ss.servers.serverErrors, + operatorIndex: idx + ) + .navigationTitle("Your servers") + .modifier(ThemedBackground(grouped: true)) + } label: { + HStack { + Text("Your servers") + + if ss.servers.userServers[idx] != ss.servers.currUserServers[idx] { + Spacer() + unsavedChangesIndicator() + } + } + } + } + + NavigationLink { + AdvancedNetworkSettings() + .navigationTitle("Advanced settings") + .modifier(ThemedBackground(grouped: true)) + } label: { + Text("Advanced network settings") + } + } header: { + Text("Messages & files") + .foregroundColor(theme.colors.secondary) + } + + Section { + Button("Save servers", action: { saveServers($ss.servers.currUserServers, $ss.servers.userServers) }) + .disabled(!serversCanBeSaved(ss.servers.currUserServers, ss.servers.userServers, ss.servers.serverErrors)) + } footer: { + if let errStr = globalServersError(ss.servers.serverErrors) { + ServersErrorView(errStr: errStr) + } else if !ss.servers.serverErrors.isEmpty { + ServersErrorView(errStr: NSLocalizedString("Errors in servers configuration.", comment: "servers error")) + } + } + + Section(header: Text("Calls").foregroundColor(theme.colors.secondary)) { + NavigationLink { + RTCServers() + .navigationTitle("Your ICE servers") + .modifier(ThemedBackground(grouped: true)) + } label: { + Text("WebRTC ICE servers") + } + } + + Section(header: Text("Network connection").foregroundColor(theme.colors.secondary)) { + HStack { + Text(m.networkInfo.networkType.text) + Spacer() + Image(systemName: "circle.fill").foregroundColor(m.networkInfo.online ? .green : .red) + } + } + } + } + .task { + // this condition is needed to prevent re-setting the servers when exiting single server view + if justOpened { + do { + ss.servers.currUserServers = try await getUserServers() + ss.servers.userServers = ss.servers.currUserServers + ss.servers.serverErrors = [] + } catch let error { + await MainActor.run { + showAlert( + NSLocalizedString("Error loading servers", comment: "alert title"), + message: responseError(error) + ) + } + } + justOpened = false + } + } + .modifier(BackButton(disabled: Binding.constant(false)) { + if serversCanBeSaved(ss.servers.currUserServers, ss.servers.userServers, ss.servers.serverErrors) { + showSaveDialog = true + } else { + dismiss() + } + }) + .confirmationDialog("Save servers?", isPresented: $showSaveDialog, titleVisibility: .visible) { + Button("Save") { + saveServers($ss.servers.currUserServers, $ss.servers.userServers) + dismiss() + } + Button("Exit without saving") { dismiss() } + } + .sheet(item: $sheetItem) { item in + switch item { + case .showConditions: + UsageConditionsView( + currUserServers: $ss.servers.currUserServers, + userServers: $ss.servers.userServers + ) + .modifier(ThemedBackground(grouped: true)) + } + } + } + + private func serverOperatorView(_ operatorIndex: Int, _ serverOperator: ServerOperator) -> some View { + NavigationLink() { + OperatorView( + currUserServers: $ss.servers.currUserServers, + userServers: $ss.servers.userServers, + serverErrors: $ss.servers.serverErrors, + operatorIndex: operatorIndex, + useOperator: serverOperator.enabled + ) + .navigationBarTitle("\(serverOperator.tradeName) servers") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + HStack { + Image(serverOperator.logo(colorScheme)) + .resizable() + .scaledToFit() + .grayscale(serverOperator.enabled ? 0.0 : 1.0) + .frame(width: 24, height: 24) + Text(serverOperator.tradeName) + .foregroundColor(serverOperator.enabled ? theme.colors.onBackground : theme.colors.secondary) + + if ss.servers.userServers[operatorIndex] != ss.servers.currUserServers[operatorIndex] { + Spacer() + unsavedChangesIndicator() + } + } + } + } + + private func unsavedChangesIndicator() -> some View { + Image(systemName: "pencil") + .foregroundColor(theme.colors.secondary) + .symbolRenderingMode(.monochrome) + .frame(maxWidth: 24, maxHeight: 24, alignment: .center) + } + + private func conditionsButton(_ conditionsAction: UsageConditionsAction) -> some View { + Button { + sheetItem = .showConditions + } label: { + switch conditionsAction { + case .review: + Text("Review conditions") + case .accepted: + Text("Accepted conditions") + } + } + } +} + +struct UsageConditionsView: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme + @Binding var currUserServers: [UserOperatorServers] + @Binding var userServers: [UserOperatorServers] + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + switch ChatModel.shared.conditions.conditionsAction { + + case .none: + regularConditionsHeader() + .padding(.top) + .padding(.top) + ConditionsTextView() + .padding(.bottom) + .padding(.bottom) + + case let .review(operators, deadline, _): + HStack { + Text("Updated conditions").font(.largeTitle).bold() + } + .padding(.top) + .padding(.top) + + Text("Conditions will be accepted for the operator(s): **\(operators.map { $0.legalName_ }.joined(separator: ", "))**.") + ConditionsTextView() + VStack(spacing: 8) { + acceptConditionsButton(operators.map { $0.operatorId }) + if let deadline = deadline { + Text("Conditions will be automatically accepted for enabled operators on: \(conditionsTimestamp(deadline)).") + .foregroundColor(theme.colors.secondary) + .font(.footnote) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.horizontal, 32) + conditionsDiffButton(.footnote) + } else { + conditionsDiffButton() + .padding(.top) + } + } + .padding(.bottom) + .padding(.bottom) + + + case let .accepted(operators): + regularConditionsHeader() + .padding(.top) + .padding(.top) + Text("Conditions are accepted for the operator(s): **\(operators.map { $0.legalName_ }.joined(separator: ", "))**.") + ConditionsTextView() + .padding(.bottom) + .padding(.bottom) + } + } + .padding(.horizontal, 25) + .frame(maxHeight: .infinity) + } + + private func acceptConditionsButton(_ operatorIds: [Int64]) -> some View { + Button { + acceptForOperators(operatorIds) + } label: { + Text("Accept conditions") + } + .buttonStyle(OnboardingButtonStyle()) + } + + func acceptForOperators(_ operatorIds: [Int64]) { + Task { + do { + let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId + let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: operatorIds) + await MainActor.run { + ChatModel.shared.conditions = r + updateOperatorsConditionsAcceptance($currUserServers, r.serverOperators) + updateOperatorsConditionsAcceptance($userServers, r.serverOperators) + dismiss() + } + } catch let error { + await MainActor.run { + showAlert( + NSLocalizedString("Error accepting conditions", comment: "alert title"), + message: responseError(error) + ) + } + } + } + } + + @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) { + HStack { + Text("Open changes") + Image(systemName: "arrow.up.right.circle") + } + .font(font) + } + } + } +} + +private func regularConditionsHeader() -> some View { + HStack { + Text("Conditions of use").font(.largeTitle).bold() + Spacer() + conditionsLinkButton() + } +} + +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]>, _ serverErrors: Binding<[UserServersError]>) { + let userServersToValidate = userServers.wrappedValue + Task { + do { + let errs = try await validateServers(userServers: userServersToValidate) + await MainActor.run { + serverErrors.wrappedValue = errs + } + } catch let error { + logger.error("validateServers error: \(responseError(error))") + } + } +} + +func serversCanBeSaved( + _ currUserServers: [UserOperatorServers], + _ userServers: [UserOperatorServers], + _ serverErrors: [UserServersError] +) -> Bool { + return userServers != currUserServers && serverErrors.isEmpty +} + +struct ServersErrorView: View { + @EnvironmentObject var theme: AppTheme + var errStr: String + + var body: some View { + HStack { + Image(systemName: "exclamationmark.circle") + .foregroundColor(.red) + Text(errStr) + .foregroundColor(theme.colors.secondary) + } + } +} + +func globalServersError(_ serverErrors: [UserServersError]) -> String? { + for err in serverErrors { + if let errStr = err.globalError { + return errStr + } + } + return nil +} + +func globalSMPServersError(_ serverErrors: [UserServersError]) -> String? { + for err in serverErrors { + if let errStr = err.globalSMPError { + return errStr + } + } + return nil +} + +func globalXFTPServersError(_ serverErrors: [UserServersError]) -> String? { + for err in serverErrors { + if let errStr = err.globalXFTPError { + return errStr + } + } + return nil +} + +func findDuplicateHosts(_ serverErrors: [UserServersError]) -> Set<String> { + let duplicateHostsList = serverErrors.compactMap { err in + if case let .duplicateServer(_, _, duplicateHost) = err { + return duplicateHost + } else { + return nil + } + } + return Set(duplicateHostsList) +} + +func saveServers(_ currUserServers: Binding<[UserOperatorServers]>, _ userServers: Binding<[UserOperatorServers]>) { + let userServersToSave = userServers.wrappedValue + Task { + do { + try await setUserServers(userServers: userServersToSave) + // Get updated servers to learn new server ids (otherwise it messes up delete of newly added and saved servers) + do { + let updatedServers = try await getUserServers() + let updatedOperators = try await getServerOperators() + await MainActor.run { + ChatModel.shared.conditions = updatedOperators + currUserServers.wrappedValue = updatedServers + userServers.wrappedValue = updatedServers + } + } catch let error { + logger.error("saveServers getUserServers error: \(responseError(error))") + await MainActor.run { + currUserServers.wrappedValue = userServersToSave + } + } + } catch let error { + logger.error("saveServers setUserServers error: \(responseError(error))") + await MainActor.run { + showAlert( + NSLocalizedString("Error saving servers", comment: "alert title"), + message: responseError(error) + ) + } + } + } +} + +func updateOperatorsConditionsAcceptance(_ usvs: Binding<[UserOperatorServers]>, _ updatedOperators: [ServerOperator]) { + for i in 0..<usvs.wrappedValue.count { + if let updatedOperator = updatedOperators.first(where: { $0.operatorId == usvs.wrappedValue[i].operator?.operatorId }) { + usvs.wrappedValue[i].operator?.conditionsAcceptance = updatedOperator.conditionsAcceptance + } + } +} + +struct NetworkServersView_Previews: PreviewProvider { + static var previews: some View { + NetworkAndServers() + } +} diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NewServerView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NewServerView.swift new file mode 100644 index 0000000000..17a0ffdd1c --- /dev/null +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NewServerView.swift @@ -0,0 +1,156 @@ +// +// NewServerView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 13.11.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct NewServerView: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme + @Binding var userServers: [UserOperatorServers] + @Binding var serverErrors: [UserServersError] + @State private var serverToEdit: UserServer = .empty + @State private var showTestFailure = false + @State private var testing = false + @State private var testFailure: ProtocolTestFailure? + + var body: some View { + ZStack { + customServer() + if testing { + ProgressView().scaleEffect(2) + } + } + .modifier(BackButton(disabled: Binding.constant(false)) { + addServer(serverToEdit, $userServers, $serverErrors, dismiss) + }) + .alert(isPresented: $showTestFailure) { + Alert( + title: Text("Server test failed!"), + message: Text(testFailure?.localizedDescription ?? "") + ) + } + } + + // TODO Possibly refactor - similar functions in ProtocolServerView + private func customServer() -> some View { + VStack { + let serverAddress = parseServerAddress(serverToEdit.server) + let valid = serverAddress?.valid == true + List { + Section { + TextEditor(text: $serverToEdit.server) + .multilineTextAlignment(.leading) + .autocorrectionDisabled(true) + .autocapitalization(.none) + .allowsTightening(true) + .lineLimit(10) + .frame(height: 144) + .padding(-6) + } header: { + HStack { + Text("Your server address") + .foregroundColor(theme.colors.secondary) + if !valid { + Spacer() + Image(systemName: "exclamationmark.circle").foregroundColor(.red) + } + } + } + useServerSection(valid) + if valid { + Section(header: Text("Add to another device").foregroundColor(theme.colors.secondary)) { + MutableQRCode(uri: $serverToEdit.server) + .listRowInsets(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12)) + } + } + } + } + } + + private func useServerSection(_ valid: Bool) -> some View { + Section(header: Text("Use server").foregroundColor(theme.colors.secondary)) { + HStack { + Button("Test server") { + testing = true + serverToEdit.tested = nil + Task { + if let f = await testServerConnection(server: $serverToEdit) { + showTestFailure = true + testFailure = f + } + await MainActor.run { testing = false } + } + } + .disabled(!valid || testing) + Spacer() + showTestStatus(server: serverToEdit) + } + Toggle("Use for new connections", isOn: $serverToEdit.enabled) + } + } +} + +func serverProtocolAndOperator(_ server: UserServer, _ userServers: [UserOperatorServers]) -> (ServerProtocol, ServerOperator?)? { + if let serverAddress = parseServerAddress(server.server) { + let serverProtocol = serverAddress.serverProtocol + let hostnames = serverAddress.hostnames + let matchingOperator = userServers.compactMap { $0.operator }.first { op in + op.serverDomains.contains { domain in + hostnames.contains { hostname in + hostname.hasSuffix(domain) + } + } + } + return (serverProtocol, matchingOperator) + } else { + return nil + } +} + +func addServer( + _ server: UserServer, + _ userServers: Binding<[UserOperatorServers]>, + _ serverErrors: Binding<[UserServersError]>, + _ dismiss: DismissAction +) { + if let (serverProtocol, matchingOperator) = serverProtocolAndOperator(server, userServers.wrappedValue) { + if let i = userServers.wrappedValue.firstIndex(where: { $0.operator?.operatorId == matchingOperator?.operatorId }) { + switch serverProtocol { + case .smp: userServers[i].wrappedValue.smpServers.append(server) + case .xftp: userServers[i].wrappedValue.xftpServers.append(server) + } + validateServers_(userServers, serverErrors) + dismiss() + if let op = matchingOperator { + showAlert( + NSLocalizedString("Operator server", comment: "alert title"), + message: String.localizedStringWithFormat(NSLocalizedString("Server added to operator %@.", comment: "alert message"), op.tradeName) + ) + } + } else { // Shouldn't happen + dismiss() + showAlert(NSLocalizedString("Error adding server", comment: "alert title")) + } + } else { + dismiss() + if server.server.trimmingCharacters(in: .whitespaces) != "" { + showAlert( + NSLocalizedString("Invalid server address!", comment: "alert title"), + message: NSLocalizedString("Check server address and try again.", comment: "alert title") + ) + } + } +} + +#Preview { + NewServerView( + userServers: Binding.constant([UserOperatorServers.sampleDataNilOperator]), + serverErrors: Binding.constant([]) + ) +} diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift new file mode 100644 index 0000000000..afbccc109c --- /dev/null +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift @@ -0,0 +1,586 @@ +// +// OperatorView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 28.10.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat +import Ink + +struct OperatorView: View { + @Environment(\.dismiss) var dismiss: DismissAction + @Environment(\.colorScheme) var colorScheme: ColorScheme + @EnvironmentObject var theme: AppTheme + @Environment(\.editMode) private var editMode + @Binding var currUserServers: [UserOperatorServers] + @Binding var userServers: [UserOperatorServers] + @Binding var serverErrors: [UserServersError] + var operatorIndex: Int + @State var useOperator: Bool + @State private var useOperatorToggleReset: Bool = false + @State private var showConditionsSheet: Bool = false + @State private var selectedServer: String? = nil + @State private var testing = false + + var body: some View { + operatorView() + .opacity(testing ? 0.4 : 1) + .overlay { + if testing { + ProgressView() + .scaleEffect(2) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + .allowsHitTesting(!testing) + } + + private func operatorView() -> some View { + let duplicateHosts = findDuplicateHosts(serverErrors) + return VStack { + List { + Section { + infoViewLink() + useOperatorToggle() + } header: { + Text("Operator") + .foregroundColor(theme.colors.secondary) + } footer: { + if let errStr = globalServersError(serverErrors) { + ServersErrorView(errStr: errStr) + } else { + switch (userServers[operatorIndex].operator_.conditionsAcceptance) { + case let .accepted(acceptedAt, _): + if let acceptedAt = acceptedAt { + Text("Conditions accepted on: \(conditionsTimestamp(acceptedAt)).") + .foregroundColor(theme.colors.secondary) + } + case let .required(deadline): + if userServers[operatorIndex].operator_.enabled, let deadline = deadline { + Text("Conditions will be accepted on: \(conditionsTimestamp(deadline)).") + .foregroundColor(theme.colors.secondary) + } + } + } + } + + if userServers[operatorIndex].operator_.enabled { + if !userServers[operatorIndex].smpServers.filter({ !$0.deleted }).isEmpty { + Section { + Toggle("To receive", isOn: $userServers[operatorIndex].operator_.smpRoles.storage) + .onChange(of: userServers[operatorIndex].operator_.smpRoles.storage) { _ in + validateServers_($userServers, $serverErrors) + } + Toggle("For private routing", isOn: $userServers[operatorIndex].operator_.smpRoles.proxy) + .onChange(of: userServers[operatorIndex].operator_.smpRoles.proxy) { _ in + validateServers_($userServers, $serverErrors) + } + } header: { + Text("Use for messages") + .foregroundColor(theme.colors.secondary) + } footer: { + if let errStr = globalSMPServersError(serverErrors) { + ServersErrorView(errStr: errStr) + } + } + } + + // Preset servers can't be deleted + if !userServers[operatorIndex].smpServers.filter({ $0.preset }).isEmpty { + Section { + ForEach($userServers[operatorIndex].smpServers) { srv in + if srv.wrappedValue.preset { + ProtocolServerViewLink( + userServers: $userServers, + serverErrors: $serverErrors, + duplicateHosts: duplicateHosts, + server: srv, + serverProtocol: .smp, + backLabel: "\(userServers[operatorIndex].operator_.tradeName) servers", + selectedServer: $selectedServer + ) + } else { + EmptyView() + } + } + } header: { + Text("Message servers") + .foregroundColor(theme.colors.secondary) + } footer: { + if let errStr = globalSMPServersError(serverErrors) { + ServersErrorView(errStr: errStr) + } else { + Text("The servers for new connections of your current chat profile **\(ChatModel.shared.currentUser?.displayName ?? "")**.") + .foregroundColor(theme.colors.secondary) + .lineLimit(10) + } + } + } + + if !userServers[operatorIndex].smpServers.filter({ !$0.preset && !$0.deleted }).isEmpty { + Section { + ForEach($userServers[operatorIndex].smpServers) { srv in + if !srv.wrappedValue.preset && !srv.wrappedValue.deleted { + ProtocolServerViewLink( + userServers: $userServers, + serverErrors: $serverErrors, + duplicateHosts: duplicateHosts, + server: srv, + serverProtocol: .smp, + backLabel: "\(userServers[operatorIndex].operator_.tradeName) servers", + selectedServer: $selectedServer + ) + } else { + EmptyView() + } + } + .onDelete { indexSet in + deleteSMPServer($userServers, operatorIndex, indexSet) + validateServers_($userServers, $serverErrors) + } + } header: { + Text("Added message servers") + .foregroundColor(theme.colors.secondary) + } + } + + if !userServers[operatorIndex].xftpServers.filter({ !$0.deleted }).isEmpty { + Section { + Toggle("To send", isOn: $userServers[operatorIndex].operator_.xftpRoles.storage) + .onChange(of: userServers[operatorIndex].operator_.xftpRoles.storage) { _ in + validateServers_($userServers, $serverErrors) + } + } header: { + Text("Use for files") + .foregroundColor(theme.colors.secondary) + } footer: { + if let errStr = globalXFTPServersError(serverErrors) { + ServersErrorView(errStr: errStr) + } + } + } + + // Preset servers can't be deleted + if !userServers[operatorIndex].xftpServers.filter({ $0.preset }).isEmpty { + Section { + ForEach($userServers[operatorIndex].xftpServers) { srv in + if srv.wrappedValue.preset { + ProtocolServerViewLink( + userServers: $userServers, + serverErrors: $serverErrors, + duplicateHosts: duplicateHosts, + server: srv, + serverProtocol: .xftp, + backLabel: "\(userServers[operatorIndex].operator_.tradeName) servers", + selectedServer: $selectedServer + ) + } else { + EmptyView() + } + } + } header: { + Text("Media & file servers") + .foregroundColor(theme.colors.secondary) + } footer: { + if let errStr = globalXFTPServersError(serverErrors) { + ServersErrorView(errStr: errStr) + } else { + Text("The servers for new files of your current chat profile **\(ChatModel.shared.currentUser?.displayName ?? "")**.") + .foregroundColor(theme.colors.secondary) + .lineLimit(10) + } + } + } + + if !userServers[operatorIndex].xftpServers.filter({ !$0.preset && !$0.deleted }).isEmpty { + Section { + ForEach($userServers[operatorIndex].xftpServers) { srv in + if !srv.wrappedValue.preset && !srv.wrappedValue.deleted { + ProtocolServerViewLink( + userServers: $userServers, + serverErrors: $serverErrors, + duplicateHosts: duplicateHosts, + server: srv, + serverProtocol: .xftp, + backLabel: "\(userServers[operatorIndex].operator_.tradeName) servers", + selectedServer: $selectedServer + ) + } else { + EmptyView() + } + } + .onDelete { indexSet in + deleteXFTPServer($userServers, operatorIndex, indexSet) + validateServers_($userServers, $serverErrors) + } + } header: { + Text("Added media & file servers") + .foregroundColor(theme.colors.secondary) + } + } + + Section { + TestServersButton( + smpServers: $userServers[operatorIndex].smpServers, + xftpServers: $userServers[operatorIndex].xftpServers, + testing: $testing + ) + } + } + } + } + .toolbar { + if ( + !userServers[operatorIndex].smpServers.filter({ !$0.preset && !$0.deleted }).isEmpty || + !userServers[operatorIndex].xftpServers.filter({ !$0.preset && !$0.deleted }).isEmpty + ) { + EditButton() + } + } + .sheet(isPresented: $showConditionsSheet, onDismiss: onUseToggleSheetDismissed) { + SingleOperatorUsageConditionsView( + currUserServers: $currUserServers, + userServers: $userServers, + serverErrors: $serverErrors, + operatorIndex: operatorIndex + ) + .modifier(ThemedBackground(grouped: true)) + } + } + + private func infoViewLink() -> some View { + NavigationLink() { + OperatorInfoView(serverOperator: userServers[operatorIndex].operator_) + .navigationBarTitle("Network operator") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + Image(userServers[operatorIndex].operator_.largeLogo(colorScheme)) + .resizable() + .scaledToFit() + .grayscale(userServers[operatorIndex].operator_.enabled ? 0.0 : 1.0) + .frame(height: 40) + } + } + + private func useOperatorToggle() -> some View { + Toggle("Use servers", isOn: $useOperator) + .onChange(of: useOperator) { useOperatorToggle in + if useOperatorToggleReset { + useOperatorToggleReset = false + } else if useOperatorToggle { + switch userServers[operatorIndex].operator_.conditionsAcceptance { + case .accepted: + userServers[operatorIndex].operator_.enabled = true + validateServers_($userServers, $serverErrors) + case let .required(deadline): + if deadline == nil { + showConditionsSheet = true + } else { + userServers[operatorIndex].operator_.enabled = true + validateServers_($userServers, $serverErrors) + } + } + } else { + userServers[operatorIndex].operator_.enabled = false + validateServers_($userServers, $serverErrors) + } + } + } + + private func onUseToggleSheetDismissed() { + if useOperator && !userServers[operatorIndex].operator_.conditionsAcceptance.usageAllowed { + useOperatorToggleReset = true + useOperator = false + } + } +} + +func conditionsTimestamp(_ date: Date) -> String { + let localDateFormatter = DateFormatter() + localDateFormatter.dateStyle = .medium + localDateFormatter.timeStyle = .none + return localDateFormatter.string(from: date) +} + +struct OperatorInfoView: View { + @EnvironmentObject var theme: AppTheme + @Environment(\.colorScheme) var colorScheme: ColorScheme + var serverOperator: ServerOperator + + var body: some View { + VStack { + List { + Section { + VStack(alignment: .leading) { + Image(serverOperator.largeLogo(colorScheme)) + .resizable() + .scaledToFit() + .frame(height: 48) + if let legalName = serverOperator.legalName { + Text(legalName) + } + } + } + Section { + VStack(alignment: .leading, spacing: 12) { + ForEach(serverOperator.info.description, id: \.self) { d in + Text(d) + } + } + Link(serverOperator.info.website.absoluteString, destination: serverOperator.info.website) + } + if let selfhost = serverOperator.info.selfhost { + Section { + Link(selfhost.text, destination: selfhost.link) + } + } + } + } + } +} + +struct ConditionsTextView: View { + @State private var conditionsData: (UsageConditions, String?, UsageConditions?)? + @State private var failedToLoad: Bool = false + @State private var conditionsHTML: String? = nil + + let defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md" + + var body: some View { + viewBody() + .frame(maxWidth: .infinity, maxHeight: .infinity) + .task { + do { + let conditions = try await getUsageConditions() + let conditionsText = conditions.1 + let parentLink = "https://github.com/simplex-chat/simplex-chat/blob/\(conditions.0.conditionsCommit)" + let preparedText: String? + if let conditionsText { + let prepared = prepareMarkdown(conditionsText.trimmingCharacters(in: .whitespacesAndNewlines), parentLink) + conditionsHTML = MarkdownParser().html(from: prepared) + preparedText = prepared + } else { + preparedText = nil + } + conditionsData = (conditions.0, preparedText, conditions.2) + } catch let error { + logger.error("ConditionsTextView getUsageConditions error: \(responseError(error))") + failedToLoad = true + } + } + } + + // TODO Diff rendering + @ViewBuilder private func viewBody() -> some View { + if let (usageConditions, _, _) = conditionsData { + if let conditionsHTML { + ConditionsWebView(html: conditionsHTML) + .padding(6) + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color(uiColor: .secondarySystemGroupedBackground)) + ) + } else { + let conditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/\(usageConditions.conditionsCommit)/PRIVACY.md" + conditionsLinkView(conditionsLink) + } + } else if failedToLoad { + conditionsLinkView(defaultConditionsLink) + } else { + ProgressView() + .scaleEffect(2) + } + } + + 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)!) { + Text(conditionsLink) + .multilineTextAlignment(.leading) + } + } + } + + private func prepareMarkdown(_ text: String, _ parentLink: String) -> String { + let localLinkRegex = try! NSRegularExpression(pattern: "\\[([^\\(]*)\\]\\(#.*\\)") + let h1Regex = try! NSRegularExpression(pattern: "^# ") + var text = localLinkRegex.stringByReplacingMatches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count), withTemplate: "$1") + text = h1Regex.stringByReplacingMatches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count), withTemplate: "") + return text + .replacingOccurrences(of: "](/", with: "](\(parentLink)/") + .replacingOccurrences(of: "](./", with: "](\(parentLink)/") + } +} + +struct SingleOperatorUsageConditionsView: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme + @Binding var currUserServers: [UserOperatorServers] + @Binding var userServers: [UserOperatorServers] + @Binding var serverErrors: [UserServersError] + var operatorIndex: Int + + var body: some View { + viewBody() + } + + @ViewBuilder private func viewBody() -> some View { + let operatorsWithConditionsAccepted = ChatModel.shared.conditions.serverOperators.filter { $0.conditionsAcceptance.conditionsAccepted } + if case .accepted = userServers[operatorIndex].operator_.conditionsAcceptance { + + // In current UI implementation this branch doesn't get shown - as conditions can't be opened from inside operator once accepted + VStack(alignment: .leading, spacing: 20) { + viewHeader() + ConditionsTextView() + } + .padding(.bottom) + .padding(.bottom) + .padding(.horizontal) + .frame(maxHeight: .infinity) + + } else if !operatorsWithConditionsAccepted.isEmpty { + + NavigationView { + VStack(alignment: .leading, spacing: 20) { + viewHeader() + Text("Conditions are already accepted for these operator(s): **\(operatorsWithConditionsAccepted.map { $0.legalName_ }.joined(separator: ", "))**.") + Text("The same conditions will apply to operator **\(userServers[operatorIndex].operator_.legalName_)**.") + conditionsAppliedToOtherOperatorsText() + Spacer() + + acceptConditionsButton() + usageConditionsNavLinkButton() + } + .padding(.bottom) + .padding(.bottom) + .padding(.horizontal) + .frame(maxHeight: .infinity) + } + + } else { + + VStack(alignment: .leading, spacing: 20) { + viewHeader() + Text("To use the servers of **\(userServers[operatorIndex].operator_.legalName_)**, accept conditions of use.") + conditionsAppliedToOtherOperatorsText() + ConditionsTextView() + acceptConditionsButton() + .padding(.bottom) + .padding(.bottom) + } + .padding(.horizontal) + .frame(maxHeight: .infinity) + + } + } + + private func viewHeader() -> some View { + HStack { + Text("Use \(userServers[operatorIndex].operator_.tradeName)").font(.largeTitle).bold() + Spacer() + conditionsLinkButton() + } + .padding(.top) + .padding(.top) + } + + @ViewBuilder private func conditionsAppliedToOtherOperatorsText() -> some View { + let otherOperatorsToApply = ChatModel.shared.conditions.serverOperators.filter { + $0.enabled && + !$0.conditionsAcceptance.conditionsAccepted && + $0.operatorId != userServers[operatorIndex].operator_.operatorId + } + if !otherOperatorsToApply.isEmpty { + Text("These conditions will also apply for: **\(otherOperatorsToApply.map { $0.legalName_ }.joined(separator: ", "))**.") + } + } + + private func acceptConditionsButton() -> some View { + let operatorIds = ChatModel.shared.conditions.serverOperators + .filter { + $0.operatorId == userServers[operatorIndex].operator_.operatorId || // Opened operator + ($0.enabled && !$0.conditionsAcceptance.conditionsAccepted) // Other enabled operators with conditions not accepted + } + .map { $0.operatorId } + return Button { + acceptForOperators(operatorIds, operatorIndex) + } label: { + Text("Accept conditions") + } + .buttonStyle(OnboardingButtonStyle()) + } + + func acceptForOperators(_ operatorIds: [Int64], _ operatorIndexToEnable: Int) { + Task { + do { + let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId + let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: operatorIds) + await MainActor.run { + ChatModel.shared.conditions = r + updateOperatorsConditionsAcceptance($currUserServers, r.serverOperators) + updateOperatorsConditionsAcceptance($userServers, r.serverOperators) + userServers[operatorIndexToEnable].operator?.enabled = true + validateServers_($userServers, $serverErrors) + dismiss() + } + } catch let error { + await MainActor.run { + showAlert( + NSLocalizedString("Error accepting conditions", comment: "alert title"), + message: responseError(error) + ) + } + } + } + } + + private func usageConditionsNavLinkButton() -> some View { + NavigationLink("View conditions") { + ConditionsTextView() + .padding() + .navigationTitle("Conditions of use") + .navigationBarTitleDisplayMode(.large) + .toolbar { ToolbarItem(placement: .navigationBarTrailing, content: conditionsLinkButton) } + .modifier(ThemedBackground(grouped: true)) + } + .font(.callout) + .frame(maxWidth: .infinity, alignment: .center) + } +} + +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) { + Label("Open conditions", systemImage: "doc") + } + if let commitUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/commit/\(commit)") { + Link(destination: commitUrl) { + Label("Open changes", systemImage: "ellipsis") + } + } + } label: { + Image(systemName: "arrow.up.right.circle") + .resizable() + .scaledToFit() + .frame(width: 20) + .padding(2) + .contentShape(Circle()) + } +} + +#Preview { + OperatorView( + currUserServers: Binding.constant([UserOperatorServers.sampleData1, UserOperatorServers.sampleDataNilOperator]), + userServers: Binding.constant([UserOperatorServers.sampleData1, UserOperatorServers.sampleDataNilOperator]), + serverErrors: Binding.constant([]), + operatorIndex: 1, + useOperator: ServerOperator.sampleData1.enabled + ) +} diff --git a/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift similarity index 64% rename from apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift rename to apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift index 48d5a66970..13d01874ed 100644 --- a/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift @@ -11,15 +11,16 @@ import SimpleXChat struct ProtocolServerView: View { @Environment(\.dismiss) var dismiss: DismissAction - let serverProtocol: ServerProtocol - @Binding var server: ServerCfg - @State var serverToEdit: ServerCfg + @EnvironmentObject var theme: AppTheme + @Binding var userServers: [UserOperatorServers] + @Binding var serverErrors: [UserServersError] + @Binding var server: UserServer + @State var serverToEdit: UserServer + var backLabel: LocalizedStringKey @State private var showTestFailure = false @State private var testing = false @State private var testFailure: ProtocolTestFailure? - var proto: String { serverProtocol.rawValue.uppercased() } - var body: some View { ZStack { if server.preset { @@ -31,9 +32,33 @@ struct ProtocolServerView: View { ProgressView().scaleEffect(2) } } - .modifier(BackButton(label: "Your \(proto) servers") { - server = serverToEdit - dismiss() + .modifier(BackButton(label: backLabel, disabled: Binding.constant(false)) { + if let (serverToEditProtocol, serverToEditOperator) = serverProtocolAndOperator(serverToEdit, userServers), + let (serverProtocol, serverOperator) = serverProtocolAndOperator(server, userServers) { + if serverToEditProtocol != serverProtocol { + dismiss() + showAlert( + NSLocalizedString("Error updating server", comment: "alert title"), + message: NSLocalizedString("Server protocol changed.", comment: "alert title") + ) + } else if serverToEditOperator != serverOperator { + dismiss() + showAlert( + NSLocalizedString("Error updating server", comment: "alert title"), + message: NSLocalizedString("Server operator changed.", comment: "alert title") + ) + } else { + server = serverToEdit + validateServers_($userServers, $serverErrors) + dismiss() + } + } else { + dismiss() + showAlert( + NSLocalizedString("Invalid server address!", comment: "alert title"), + message: NSLocalizedString("Check server address and try again.", comment: "alert title") + ) + } }) .alert(isPresented: $showTestFailure) { Alert( @@ -49,7 +74,7 @@ struct ProtocolServerView: View { private func presetServer() -> some View { return VStack { List { - Section("Preset server address") { + Section(header: Text("Preset server address").foregroundColor(theme.colors.secondary)) { Text(serverToEdit.server) .textSelection(.enabled) } @@ -61,7 +86,7 @@ struct ProtocolServerView: View { private func customServer() -> some View { VStack { let serverAddress = parseServerAddress(serverToEdit.server) - let valid = serverAddress?.valid == true && serverAddress?.serverProtocol == serverProtocol + let valid = serverAddress?.valid == true List { Section { TextEditor(text: $serverToEdit.server) @@ -75,6 +100,7 @@ struct ProtocolServerView: View { } header: { HStack { Text("Your server address") + .foregroundColor(theme.colors.secondary) if !valid { Spacer() Image(systemName: "exclamationmark.circle").foregroundColor(.red) @@ -83,7 +109,7 @@ struct ProtocolServerView: View { } useServerSection(valid) if valid { - Section("Add to another device") { + Section(header: Text("Add to another device").foregroundColor(theme.colors.secondary)) { MutableQRCode(uri: $serverToEdit.server) .listRowInsets(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12)) } @@ -93,7 +119,7 @@ struct ProtocolServerView: View { } private func useServerSection(_ valid: Bool) -> some View { - Section("Use server") { + Section(header: Text("Use server").foregroundColor(theme.colors.secondary)) { HStack { Button("Test server") { testing = true @@ -117,6 +143,7 @@ struct ProtocolServerView: View { struct BackButton: ViewModifier { var label: LocalizedStringKey = "Back" + @Binding var disabled: Bool var action: () -> Void func body(content: Content) -> some View { @@ -130,12 +157,13 @@ struct BackButton: ViewModifier { Text(label) } } + .disabled(disabled) } } } } -@ViewBuilder func showTestStatus(server: ServerCfg) -> some View { +@ViewBuilder func showTestStatus(server: UserServer) -> some View { switch server.tested { case .some(true): Image(systemName: "checkmark") @@ -148,7 +176,7 @@ struct BackButton: ViewModifier { } } -func testServerConnection(server: Binding<ServerCfg>) async -> ProtocolTestFailure? { +func testServerConnection(server: Binding<UserServer>) async -> ProtocolTestFailure? { do { let r = try await testProtoServer(server: server.wrappedValue.server) switch r { @@ -168,16 +196,14 @@ func testServerConnection(server: Binding<ServerCfg>) async -> ProtocolTestFailu } } -func serverHostname(_ srv: String) -> String { - parseServerAddress(srv)?.hostnames.first ?? srv -} - struct ProtocolServerView_Previews: PreviewProvider { static var previews: some View { ProtocolServerView( - serverProtocol: .smp, - server: Binding.constant(ServerCfg.sampleData.custom), - serverToEdit: ServerCfg.sampleData.custom + userServers: Binding.constant([UserOperatorServers.sampleDataNilOperator]), + serverErrors: Binding.constant([]), + server: Binding.constant(UserServer.sampleData.custom), + serverToEdit: UserServer.sampleData.custom, + backLabel: "Your SMP servers" ) } } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift new file mode 100644 index 0000000000..b9737914ec --- /dev/null +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift @@ -0,0 +1,359 @@ +// +// ProtocolServersView.swift +// SimpleX (iOS) +// +// Created by Evgeny on 15/11/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +private let howToUrl = URL(string: "https://simplex.chat/docs/server.html")! + +struct YourServersView: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject private var m: ChatModel + @EnvironmentObject var theme: AppTheme + @Environment(\.editMode) private var editMode + @Binding var userServers: [UserOperatorServers] + @Binding var serverErrors: [UserServersError] + var operatorIndex: Int + @State private var selectedServer: String? = nil + @State private var showAddServer = false + @State private var newServerNavLinkActive = false + @State private var showScanProtoServer = false + @State private var testing = false + + var body: some View { + yourServersView() + .opacity(testing ? 0.4 : 1) + .overlay { + if testing { + ProgressView() + .scaleEffect(2) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + .allowsHitTesting(!testing) + } + + private func yourServersView() -> some View { + let duplicateHosts = findDuplicateHosts(serverErrors) + return List { + if !userServers[operatorIndex].smpServers.filter({ !$0.deleted }).isEmpty { + Section { + ForEach($userServers[operatorIndex].smpServers) { srv in + if !srv.wrappedValue.deleted { + ProtocolServerViewLink( + userServers: $userServers, + serverErrors: $serverErrors, + duplicateHosts: duplicateHosts, + server: srv, + serverProtocol: .smp, + backLabel: "Your servers", + selectedServer: $selectedServer + ) + } else { + EmptyView() + } + } + .onDelete { indexSet in + deleteSMPServer($userServers, operatorIndex, indexSet) + validateServers_($userServers, $serverErrors) + } + } header: { + Text("Message servers") + .foregroundColor(theme.colors.secondary) + } footer: { + if let errStr = globalSMPServersError(serverErrors) { + ServersErrorView(errStr: errStr) + } else { + Text("The servers for new connections of your current chat profile **\(m.currentUser?.displayName ?? "")**.") + .foregroundColor(theme.colors.secondary) + .lineLimit(10) + } + } + } + + if !userServers[operatorIndex].xftpServers.filter({ !$0.deleted }).isEmpty { + Section { + ForEach($userServers[operatorIndex].xftpServers) { srv in + if !srv.wrappedValue.deleted { + ProtocolServerViewLink( + userServers: $userServers, + serverErrors: $serverErrors, + duplicateHosts: duplicateHosts, + server: srv, + serverProtocol: .xftp, + backLabel: "Your servers", + selectedServer: $selectedServer + ) + } else { + EmptyView() + } + } + .onDelete { indexSet in + deleteXFTPServer($userServers, operatorIndex, indexSet) + validateServers_($userServers, $serverErrors) + } + } header: { + Text("Media & file servers") + .foregroundColor(theme.colors.secondary) + } footer: { + if let errStr = globalXFTPServersError(serverErrors) { + ServersErrorView(errStr: errStr) + } else { + Text("The servers for new files of your current chat profile **\(m.currentUser?.displayName ?? "")**.") + .foregroundColor(theme.colors.secondary) + .lineLimit(10) + } + } + } + + Section { + ZStack { + Button("Add server") { + showAddServer = true + } + + NavigationLink(isActive: $newServerNavLinkActive) { + newServerDestinationView() + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } + } footer: { + if let errStr = globalServersError(serverErrors) { + ServersErrorView(errStr: errStr) + } + } + + Section { + TestServersButton( + smpServers: $userServers[operatorIndex].smpServers, + xftpServers: $userServers[operatorIndex].xftpServers, + testing: $testing + ) + howToButton() + } + } + .toolbar { + if ( + !userServers[operatorIndex].smpServers.filter({ !$0.deleted }).isEmpty || + !userServers[operatorIndex].xftpServers.filter({ !$0.deleted }).isEmpty + ) { + EditButton() + } + } + .confirmationDialog("Add server", isPresented: $showAddServer, titleVisibility: .hidden) { + Button("Enter server manually") { newServerNavLinkActive = true } + Button("Scan server QR code") { showScanProtoServer = true } + } + .sheet(isPresented: $showScanProtoServer) { + ScanProtocolServer( + userServers: $userServers, + serverErrors: $serverErrors + ) + .modifier(ThemedBackground(grouped: true)) + } + } + + private func newServerDestinationView() -> some View { + NewServerView( + userServers: $userServers, + serverErrors: $serverErrors + ) + .navigationTitle("New server") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } + + func howToButton() -> some View { + Button { + DispatchQueue.main.async { + UIApplication.shared.open(howToUrl) + } + } label: { + HStack { + Text("How to use your servers") + Image(systemName: "arrow.up.right.circle") + } + } + } +} + +struct ProtocolServerViewLink: View { + @EnvironmentObject var theme: AppTheme + @Binding var userServers: [UserOperatorServers] + @Binding var serverErrors: [UserServersError] + var duplicateHosts: Set<String> + @Binding var server: UserServer + var serverProtocol: ServerProtocol + var backLabel: LocalizedStringKey + @Binding var selectedServer: String? + + var body: some View { + let proto = serverProtocol.rawValue.uppercased() + + NavigationLink(tag: server.id, selection: $selectedServer) { + ProtocolServerView( + userServers: $userServers, + serverErrors: $serverErrors, + server: $server, + serverToEdit: server, + backLabel: backLabel + ) + .navigationBarTitle("\(proto) server") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + let address = parseServerAddress(server.server) + HStack { + Group { + if let address = address { + if !address.valid || address.serverProtocol != serverProtocol { + invalidServer() + } else if address.hostnames.contains(where: duplicateHosts.contains) { + Image(systemName: "exclamationmark.circle").foregroundColor(.red) + } else if !server.enabled { + Image(systemName: "slash.circle").foregroundColor(theme.colors.secondary) + } else { + showTestStatus(server: server) + } + } else { + invalidServer() + } + } + .frame(width: 16, alignment: .center) + .padding(.trailing, 4) + + let v = Text(address?.hostnames.first ?? server.server).lineLimit(1) + if server.enabled { + v + } else { + v.foregroundColor(theme.colors.secondary) + } + } + } + } + + private func invalidServer() -> some View { + Image(systemName: "exclamationmark.circle").foregroundColor(.red) + } +} + +func deleteSMPServer( + _ userServers: Binding<[UserOperatorServers]>, + _ operatorServersIndex: Int, + _ serverIndexSet: IndexSet +) { + if let idx = serverIndexSet.first { + let server = userServers[operatorServersIndex].wrappedValue.smpServers[idx] + if server.serverId == nil { + userServers[operatorServersIndex].wrappedValue.smpServers.remove(at: idx) + } else { + var updatedServer = server + updatedServer.deleted = true + userServers[operatorServersIndex].wrappedValue.smpServers[idx] = updatedServer + } + } +} + +func deleteXFTPServer( + _ userServers: Binding<[UserOperatorServers]>, + _ operatorServersIndex: Int, + _ serverIndexSet: IndexSet +) { + if let idx = serverIndexSet.first { + let server = userServers[operatorServersIndex].wrappedValue.xftpServers[idx] + if server.serverId == nil { + userServers[operatorServersIndex].wrappedValue.xftpServers.remove(at: idx) + } else { + var updatedServer = server + updatedServer.deleted = true + userServers[operatorServersIndex].wrappedValue.xftpServers[idx] = updatedServer + } + } +} + +struct TestServersButton: View { + @Binding var smpServers: [UserServer] + @Binding var xftpServers: [UserServer] + @Binding var testing: Bool + + var body: some View { + Button("Test servers", action: testServers) + .disabled(testing || allServersDisabled) + } + + private var allServersDisabled: Bool { + smpServers.allSatisfy { !$0.enabled } && xftpServers.allSatisfy { !$0.enabled } + } + + private func testServers() { + resetTestStatus() + testing = true + Task { + let fs = await runServersTest() + await MainActor.run { + testing = false + if !fs.isEmpty { + let msg = fs.map { (srv, f) in + "\(srv): \(f.localizedDescription)" + }.joined(separator: "\n") + showAlert( + NSLocalizedString("Tests failed!", comment: "alert title"), + message: String.localizedStringWithFormat(NSLocalizedString("Some servers failed the test:\n%@", comment: "alert message"), msg) + ) + } + } + } + } + + private func resetTestStatus() { + for i in 0..<smpServers.count { + if smpServers[i].enabled { + smpServers[i].tested = nil + } + } + + for i in 0..<xftpServers.count { + if xftpServers[i].enabled { + xftpServers[i].tested = nil + } + } + } + + private func runServersTest() async -> [String: ProtocolTestFailure] { + var fs: [String: ProtocolTestFailure] = [:] + for i in 0..<smpServers.count { + if smpServers[i].enabled { + if let f = await testServerConnection(server: $smpServers[i]) { + fs[serverHostname(smpServers[i].server)] = f + } + } + } + + for i in 0..<xftpServers.count { + if xftpServers[i].enabled { + if let f = await testServerConnection(server: $xftpServers[i]) { + fs[serverHostname(xftpServers[i].server)] = f + } + } + } + return fs + } +} + +struct YourServersView_Previews: PreviewProvider { + static var previews: some View { + YourServersView( + userServers: Binding.constant([UserOperatorServers.sampleDataNilOperator]), + serverErrors: Binding.constant([]), + operatorIndex: 1 + ) + } +} diff --git a/apps/ios/Shared/Views/UserSettings/ScanProtocolServer.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift similarity index 66% rename from apps/ios/Shared/Views/UserSettings/ScanProtocolServer.swift rename to apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift index 33825fee62..b28b1a4d1e 100644 --- a/apps/ios/Shared/Views/UserSettings/ScanProtocolServer.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift @@ -12,8 +12,8 @@ import CodeScanner struct ScanProtocolServer: View { @Environment(\.dismiss) var dismiss: DismissAction - @Binding var servers: [ServerCfg] - @State private var showAddressError = false + @Binding var userServers: [UserOperatorServers] + @Binding var serverErrors: [UserServersError] var body: some View { VStack(alignment: .leading) { @@ -28,23 +28,14 @@ struct ScanProtocolServer: View { } .padding() .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .alert(isPresented: $showAddressError) { - Alert( - title: Text("Invalid server address!"), - message: Text("Check server address and try again.") - ) - } } func processQRCode(_ resp: Result<ScanResult, ScanError>) { switch resp { case let .success(r): - if parseServerAddress(r.string) != nil { - servers.append(ServerCfg(server: r.string, preset: false, tested: nil, enabled: true)) - dismiss() - } else { - showAddressError = true - } + var server: UserServer = .empty + server.server = r.string + addServer(server, $userServers, $serverErrors, dismiss) case let .failure(e): logger.error("ScanProtocolServer.processQRCode QR code error: \(e.localizedDescription)") dismiss() @@ -54,6 +45,9 @@ struct ScanProtocolServer: View { struct ScanProtocolServer_Previews: PreviewProvider { static var previews: some View { - ScanProtocolServer(servers: Binding.constant([])) + ScanProtocolServer( + userServers: Binding.constant([UserOperatorServers.sampleDataNilOperator]), + serverErrors: Binding.constant([]) + ) } } diff --git a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift index 04c02f0dd2..c4d0588987 100644 --- a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift +++ b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift @@ -11,36 +11,50 @@ import SimpleXChat struct NotificationsView: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @State private var notificationMode: NotificationsMode = ChatModel.shared.notificationMode - @State private var showAlert: NotificationAlert? + @State private var ntfAlert: NotificationAlert? @State private var legacyDatabase = dbContainerGroupDefault.get() == .documents + @State private var testing = false + @State private var testedSuccess: Bool? = nil var body: some View { + ZStack { + viewBody() + if testing { + ProgressView().scaleEffect(2) + } + } + .alert(item: $ntfAlert) { alert in + if let token = m.deviceToken { + return notificationAlert(alert, token) + } else { + return Alert(title: Text("No device token!")) + } + } + } + + private func viewBody() -> some View { List { Section { NavigationLink { List { Section { SelectionListView(list: NotificationsMode.values, selection: $notificationMode) { mode in - showAlert = .setMode(mode: mode) + ntfAlert = .setMode(mode: mode) } } footer: { VStack(alignment: .leading) { Text(ntfModeDescription(notificationMode)) + .foregroundColor(theme.colors.secondary) } .font(.callout) .padding(.top, 1) } } .navigationTitle("Send notifications") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.inline) - .alert(item: $showAlert) { alert in - if let token = m.deviceToken { - return notificationAlert(alert, token) - } else { - return Alert(title: Text("No device token!")) - } - } } label: { HStack { Text("Send notifications") @@ -59,6 +73,7 @@ struct NotificationsView: View { } footer: { VStack(alignment: .leading, spacing: 1) { Text("You can set lock screen notification preview via settings.") + .foregroundColor(theme.colors.secondary) Button("Open Settings") { DispatchQueue.main.async { UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) @@ -68,6 +83,7 @@ struct NotificationsView: View { } } .navigationTitle("Show preview") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.inline) } label: { HStack { @@ -76,17 +92,27 @@ struct NotificationsView: View { Text(m.notificationPreview.label) } } + + if let server = m.notificationServer { + smpServers("Push server", [server], theme.colors.secondary) + testTokenButton(server) + } } header: { Text("Push notifications") + .foregroundColor(theme.colors.secondary) } footer: { if legacyDatabase { Text("Please restart the app and migrate the database to enable push notifications.") + .foregroundColor(theme.colors.secondary) .font(.callout) .padding(.top, 1) } } } .disabled(legacyDatabase) + .onAppear { + (m.savedToken, m.tokenStatus, m.notificationMode, m.notificationServer) = apiGetNtfToken() + } } private func notificationAlert(_ alert: NotificationAlert, _ token: DeviceToken) -> Alert { @@ -102,6 +128,11 @@ struct NotificationsView: View { notificationMode = m.notificationMode } ) + case let .testFailure(testFailure): + return Alert( + title: Text("Server test failed!"), + message: Text(testFailure.localizedDescription) + ) case let .error(title, error): return Alert(title: Text(title), message: Text(error)) } @@ -125,43 +156,142 @@ struct NotificationsView: View { m.tokenStatus = .new notificationMode = .off m.notificationMode = .off + m.notificationServer = nil + testedSuccess = nil } } catch let error { await MainActor.run { let err = responseError(error) logger.error("apiDeleteToken error: \(err)") - showAlert = .error(title: "Error deleting token", error: err) + ntfAlert = .error(title: "Error deleting token", error: err) } } default: do { - let status = try await apiRegisterToken(token: token, notificationMode: mode) + let _ = try await apiRegisterToken(token: token, notificationMode: mode) + let (_, tknStatus, ntfMode, ntfServer) = apiGetNtfToken() await MainActor.run { - m.tokenStatus = status - notificationMode = mode - m.notificationMode = mode + m.tokenStatus = tknStatus + notificationMode = ntfMode + m.notificationMode = ntfMode + m.notificationServer = ntfServer + testedSuccess = nil } } catch let error { await MainActor.run { let err = responseError(error) logger.error("apiRegisterToken error: \(err)") - showAlert = .error(title: "Error enabling notifications", error: err) + ntfAlert = .error(title: "Error enabling notifications", error: err) } } } } } + + private func testTokenButton(_ server: String) -> some View { + HStack { + Button("Test notifications") { + testing = true + Task { + await testServerAndToken(server) + await MainActor.run { testing = false } + } + } + .disabled(testing) + if !testing { + Spacer() + showTestStatus() + } + } + } + + @ViewBuilder func showTestStatus() -> some View { + if testedSuccess == true { + Image(systemName: "checkmark") + .foregroundColor(.green) + } else if testedSuccess == false { + Image(systemName: "multiply") + .foregroundColor(.red) + } + } + + private func testServerAndToken(_ server: String) async { + do { + let r = try await testProtoServer(server: server) + switch r { + case .success: + if let token = m.deviceToken { + do { + let status = try await apiCheckToken(token: token) + await MainActor.run { + m.tokenStatus = status + testedSuccess = status.workingToken + if status.workingToken { + showAlert( + NSLocalizedString("Notifications status", comment: "alert title"), + message: tokenStatusInfo(status, register: false) + ) + } else { + showAlert( + title: NSLocalizedString("Notifications error", comment: "alert title"), + message: tokenStatusInfo(status, register: true), + buttonTitle: "Register", + buttonAction: { + reRegisterToken(token: token) + testedSuccess = nil + }, + cancelButton: true + ) + } + } + } catch let error { + await MainActor.run { + let err = responseError(error) + logger.error("apiCheckToken \(err)") + ntfAlert = .error(title: "Error checking token status", error: err) + } + } + } else { + await MainActor.run { + showAlert( + NSLocalizedString("No token!", comment: "alert title") + ) + } + } + case let .failure(f): + await MainActor.run { + ntfAlert = .testFailure(testFailure: f) + testedSuccess = false + } + } + } catch let error { + await MainActor.run { + let err = responseError(error) + logger.error("testServerConnection \(err)") + ntfAlert = .error(title: "Error testing server connection", error: err) + } + } + } } func ntfModeDescription(_ mode: NotificationsMode) -> LocalizedStringKey { switch mode { - case .off: return "**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." - case .periodic: return "**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." - case .instant: return "**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." + case .off: return "**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." + case .periodic: return "**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." + case .instant: return "**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." + } +} + +func ntfModeShortDescription(_ mode: NotificationsMode) -> LocalizedStringKey { + switch mode { + case .off: return "Check messages when allowed." + case .periodic: return "Check messages every 20 min." + case .instant: return "E2E encrypted notifications." } } struct SelectionListView<Item: SelectableItem>: View { + @EnvironmentObject var theme: AppTheme var list: [Item] @Binding var selection: Item var onSelection: ((Item) -> Void)? @@ -169,32 +299,24 @@ struct SelectionListView<Item: SelectableItem>: View { var body: some View { ForEach(list) { item in - HStack { - Text(item.label) - Spacer() - if selection == item { - Image(systemName: "checkmark") - .resizable().scaledToFit().frame(width: 16) - .foregroundColor(.accentColor) - } - } - .contentShape(Rectangle()) - .listRowBackground(Color(uiColor: tapped == item ? .secondarySystemFill : .systemBackground)) - .onTapGesture { + Button { if selection == item { return } if let f = onSelection { f(item) } else { selection = item } - } - ._onButtonGesture { down in - if down { - tapped = item - } else { - tapped = nil + } label: { + HStack { + Text(item.label).foregroundColor(theme.colors.onBackground) + Spacer() + if selection == item { + Image(systemName: "checkmark") + .resizable().scaledToFit().frame(width: 16) + .foregroundColor(theme.colors.primary) + } } - } perform: {} + } } .environment(\.editMode, .constant(.active)) } @@ -202,11 +324,13 @@ struct SelectionListView<Item: SelectableItem>: View { enum NotificationAlert: Identifiable { case setMode(mode: NotificationsMode) + case testFailure(testFailure: ProtocolTestFailure) case error(title: LocalizedStringKey, error: String) var id: String { switch self { case let .setMode(mode): return "enable \(mode.rawValue)" + case let .testFailure(testFailure): return "testFailure \(testFailure.testStep) \(testFailure.testError)" case let .error(title, error): return "error \(title): \(error)" } } diff --git a/apps/ios/Shared/Views/UserSettings/PreferencesView.swift b/apps/ios/Shared/Views/UserSettings/PreferencesView.swift index 960afb6d38..bd8171623a 100644 --- a/apps/ios/Shared/Views/UserSettings/PreferencesView.swift +++ b/apps/ios/Shared/Views/UserSettings/PreferencesView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct PreferencesView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @State var profile: LocalProfile @State var preferences: FullPreferences @State var currentPreferences: FullPreferences @@ -31,11 +32,22 @@ struct PreferencesView: View { .disabled(currentPreferences == preferences) } } + .onDisappear { + if currentPreferences != preferences { + showAlert( + title: NSLocalizedString("Your chat preferences", comment: "alert title"), + message: NSLocalizedString("Chat preferences were changed.", comment: "alert message"), + buttonTitle: NSLocalizedString("Save", comment: "alert button"), + buttonAction: savePreferences, + cancelButton: true + ) + } + } } private func featureSection(_ feature: ChatFeature, _ allowFeature: Binding<FeatureAllowed>) -> some View { Section { - settingsRow(feature.icon) { + settingsRow(feature.icon, color: theme.colors.secondary) { Picker(feature.text, selection: allowFeature) { ForEach(FeatureAllowed.values) { allow in Text(allow.text) @@ -44,7 +56,7 @@ struct PreferencesView: View { .frame(height: 36) } } - footer: { featureFooter(feature, allowFeature) } + footer: { featureFooter(feature, allowFeature).foregroundColor(theme.colors.secondary) } } @@ -54,16 +66,15 @@ struct PreferencesView: View { get: { allowFeature.wrappedValue == .always || allowFeature.wrappedValue == .yes }, set: { yes, _ in allowFeature.wrappedValue = yes ? .yes : .no } ) - settingsRow(ChatFeature.timedMessages.icon) { + settingsRow(ChatFeature.timedMessages.icon, color: theme.colors.secondary) { Toggle(ChatFeature.timedMessages.text, isOn: allow) } } - footer: { featureFooter(.timedMessages, allowFeature) } + footer: { featureFooter(.timedMessages, allowFeature).foregroundColor(theme.colors.secondary) } } private func featureFooter(_ feature: ChatFeature, _ allowFeature: Binding<FeatureAllowed>) -> some View { Text(feature.allowDescription(allowFeature.wrappedValue)) - .frame(height: 36, alignment: .topLeading) } private func savePreferences() { diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index d8ff2c2f89..eba7f8066a 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -11,15 +11,20 @@ import SimpleXChat 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(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true @AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true @AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true + @AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true @State private var simplexLinkMode = privacySimplexLinkModeDefault.get() + @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false + @AppStorage(DEFAULT_PRIVACY_SHORT_LINKS) private var shortSimplexLinks = false @AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @State private var currentLAMode = privacyLocalAuthModeDefault.get() + @AppStorage(DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) private var privacyMediaBlurRadius: Int = 0 @State private var contactReceipts = false @State private var contactReceiptsReset = false @State private var contactReceiptsOverrides = 0 @@ -43,46 +48,38 @@ struct PrivacySettings: View { var body: some View { VStack { List { - Section("Device") { + Section(header: Text("Device").foregroundColor(theme.colors.secondary)) { NavigationLink { SimplexLockView(prefPerformLA: $prefPerformLA, currentLAMode: $currentLAMode) .navigationTitle("SimpleX Lock") + .modifier(ThemedBackground(grouped: true)) } label: { if prefPerformLA { settingsRow("lock.fill", color: .green) { simplexLockRow(currentLAMode.text) } } else { - settingsRow("lock") { + settingsRow("lock", color: theme.colors.secondary) { simplexLockRow("Off") } } } - settingsRow("eye.slash") { + settingsRow("eye.slash", color: theme.colors.secondary) { Toggle("Protect app screen", isOn: $protectScreen) } } Section { - settingsRow("lock.doc") { - Toggle("Encrypt local files", isOn: $encryptLocalFiles) - .onChange(of: encryptLocalFiles) { - setEncryptLocalFiles($0) - } - } - settingsRow("photo") { - Toggle("Auto-accept images", isOn: $autoAcceptImages) - .onChange(of: autoAcceptImages) { - privacyAcceptImagesGroupDefault.set($0) - } - } - settingsRow("network") { + settingsRow("network", color: theme.colors.secondary) { Toggle("Send link previews", isOn: $useLinkPreviews) + .onChange(of: useLinkPreviews) { linkPreviews in + privacyLinkPreviewsGroupDefault.set(linkPreviews) + } } - settingsRow("message") { + settingsRow("message", color: theme.colors.secondary) { Toggle("Show last messages", isOn: $showChatPreviews) } - settingsRow("rectangle.and.pencil.and.ellipsis") { + settingsRow("rectangle.and.pencil.and.ellipsis", color: theme.colors.secondary) { Toggle("Message draft", isOn: $saveLastDraft) } .onChange(of: saveLastDraft) { saveDraft in @@ -91,7 +88,7 @@ struct PrivacySettings: View { m.draftChatId = nil } } - settingsRow("link") { + settingsRow("link", color: theme.colors.secondary) { Picker("SimpleX links", selection: $simplexLinkMode) { ForEach( SimpleXLinkMode.values + (SimpleXLinkMode.values.contains(simplexLinkMode) ? [] : [simplexLinkMode]) @@ -104,24 +101,77 @@ struct PrivacySettings: View { .onChange(of: simplexLinkMode) { mode in privacySimplexLinkModeDefault.set(mode) } + if developerTools { + settingsRow("link.badge.plus", color: theme.colors.secondary) { + Toggle("Use short links (BETA)", isOn: $shortSimplexLinks) + } + } } header: { Text("Chats") + .foregroundColor(theme.colors.secondary) } Section { - settingsRow("person") { + settingsRow("lock.doc", color: theme.colors.secondary) { + Toggle("Encrypt local files", isOn: $encryptLocalFiles) + .onChange(of: encryptLocalFiles) { + setEncryptLocalFiles($0) + } + } + settingsRow("photo", color: theme.colors.secondary) { + Toggle("Auto-accept images", isOn: $autoAcceptImages) + .onChange(of: autoAcceptImages) { + privacyAcceptImagesGroupDefault.set($0) + } + } + settingsRow("circle.filled.pattern.diagonalline.rectangle", color: theme.colors.secondary) { + Picker("Blur media", selection: $privacyMediaBlurRadius) { + let values = [0, 12, 24, 48] + ([0, 12, 24, 48].contains(privacyMediaBlurRadius) ? [] : [privacyMediaBlurRadius]) + ForEach(values, id: \.self) { radius in + let text: String = switch radius { + case 0: NSLocalizedString("Off", comment: "blur media") + case 12: NSLocalizedString("Soft", comment: "blur media") + case 24: NSLocalizedString("Medium", comment: "blur media") + case 48: NSLocalizedString("Strong", comment: "blur media") + default: "\(radius)" + } + Text(text) + } + } + } + .frame(height: 36) + settingsRow("network.badge.shield.half.filled", color: theme.colors.secondary) { + Toggle("Protect IP address", isOn: $askToApproveRelays) + } + } header: { + Text("Files") + .foregroundColor(theme.colors.secondary) + } footer: { + if askToApproveRelays { + Text("The app will ask to confirm downloads from unknown file servers (except .onion).") + .foregroundColor(theme.colors.secondary) + } else { + Text("Without Tor or VPN, your IP address will be visible to file servers.") + .foregroundColor(theme.colors.secondary) + } + } + + Section { + settingsRow("person", color: theme.colors.secondary) { Toggle("Contacts", isOn: $contactReceipts) } - settingsRow("person.2") { + settingsRow("person.2", color: theme.colors.secondary) { Toggle("Small groups (max 20)", isOn: $groupReceipts) } } header: { Text("Send delivery receipts to") + .foregroundColor(theme.colors.secondary) } footer: { VStack(alignment: .leading) { Text("These settings are for your current profile **\(m.currentUser?.displayName ?? "")**.") Text("They can be overridden in contact and group settings.") } + .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity, alignment: .leading) } .confirmationDialog(contactReceiptsDialogTitle, isPresented: $contactReceiptsDialogue, titleVisibility: .visible) { @@ -317,6 +367,7 @@ struct SimplexLockView: View { @Binding var prefPerformLA: Bool @Binding var currentLAMode: LAMode @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false @State private var laMode: LAMode = privacyLocalAuthModeDefault.get() @AppStorage(DEFAULT_LA_LOCK_DELAY) private var laLockDelay = 30 @@ -324,6 +375,7 @@ struct SimplexLockView: View { @State private var selfDestruct: Bool = UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT) @State private var currentSelfDestruct: Bool = UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT) @AppStorage(DEFAULT_LA_SELF_DESTRUCT_DISPLAY_NAME) private var selfDestructDisplayName = "" + @AppStorage(GROUP_DEFAULT_ALLOW_SHARE_EXTENSION, store: groupDefaults) private var allowShareExtension = false @State private var performLAToggleReset = false @State private var performLAModeReset = false @State private var performLASelfDestructReset = false @@ -395,13 +447,19 @@ struct SimplexLockView: View { } } + if performLA { + Section("Share to SimpleX") { + Toggle("Allow sharing", isOn: $allowShareExtension) + } + } + if performLA && laMode == .passcode { - Section("Self-destruct passcode") { + Section(header: Text("Self-destruct passcode").foregroundColor(theme.colors.secondary)) { Toggle(isOn: $selfDestruct) { HStack(spacing: 6) { Text("Enable self-destruct") Image(systemName: "info.circle") - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) .font(.system(size: 14)) } .onTapGesture { @@ -419,6 +477,7 @@ struct SimplexLockView: View { } } .onChange(of: performLA) { performLAToggle in + appLocalAuthEnabledGroupDefault.set(performLAToggle) prefLANoticeShown = true if performLAToggleReset { performLAToggleReset = false @@ -491,14 +550,23 @@ struct SimplexLockView: View { showLAAlert(.laPasscodeNotChangedAlert) } case .enableSelfDestruct: - SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, title: "Set passcode", reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view")) { + SetAppPasscodeView( + passcodeKeychain: kcSelfDestructPassword, + prohibitedPasscodeKeychain: kcAppPassword, + title: "Set passcode", + reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view") + ) { updateSelfDestruct() showLAAlert(.laSelfDestructPasscodeSetAlert) } cancel: { revertSelfDestruct() } case .changeSelfDestructPasscode: - SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view")) { + SetAppPasscodeView( + passcodeKeychain: kcSelfDestructPassword, + prohibitedPasscodeKeychain: kcAppPassword, + reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view") + ) { showLAAlert(.laSelfDestructPasscodeChangedAlert) } cancel: { showLAAlert(.laPasscodeNotChangedAlert) diff --git a/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift b/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift deleted file mode 100644 index 382eaffbef..0000000000 --- a/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift +++ /dev/null @@ -1,307 +0,0 @@ -// -// ProtocolServersView.swift -// SimpleX (iOS) -// -// Created by Evgeny on 15/11/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import SwiftUI -import SimpleXChat - -private let howToUrl = URL(string: "https://simplex.chat/docs/server.html")! - -struct ProtocolServersView: View { - @Environment(\.dismiss) var dismiss: DismissAction - @EnvironmentObject private var m: ChatModel - @Environment(\.editMode) private var editMode - let serverProtocol: ServerProtocol - @State private var currServers: [ServerCfg] = [] - @State private var presetServers: [String] = [] - @State private var servers: [ServerCfg] = [] - @State private var selectedServer: String? = nil - @State private var showAddServer = false - @State private var showScanProtoServer = false - @State private var justOpened = true - @State private var testing = false - @State private var alert: ServerAlert? = nil - @State private var showSaveDialog = false - - var proto: String { serverProtocol.rawValue.uppercased() } - - var body: some View { - ZStack { - protocolServersView() - if testing { - ProgressView().scaleEffect(2) - } - } - } - - enum ServerAlert: Identifiable { - case testsFailed(failures: [String: ProtocolTestFailure]) - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") - - var id: String { - switch self { - case .testsFailed: return "testsFailed" - case let .error(title, _): return "error \(title)" - } - } - } - - private func protocolServersView() -> some View { - List { - Section { - ForEach($servers) { srv in - protocolServerView(srv) - } - .onMove { indexSet, offset in - servers.move(fromOffsets: indexSet, toOffset: offset) - } - .onDelete { indexSet in - servers.remove(atOffsets: indexSet) - } - Button("Add server…") { - showAddServer = true - } - } header: { - Text("\(proto) servers") - } footer: { - Text("The servers for new connections of your current chat profile **\(m.currentUser?.displayName ?? "")**.") - .lineLimit(10) - } - - Section { - Button("Reset") { servers = currServers } - .disabled(servers == currServers || testing) - Button("Test servers", action: testServers) - .disabled(testing || allServersDisabled) - Button("Save servers", action: saveServers) - .disabled(saveDisabled) - howToButton() - } - } - .toolbar { EditButton() } - .confirmationDialog("Add server…", isPresented: $showAddServer, titleVisibility: .hidden) { - Button("Enter server manually") { - servers.append(ServerCfg.empty) - selectedServer = servers.last?.id - } - Button("Scan server QR code") { showScanProtoServer = true } - Button("Add preset servers", action: addAllPresets) - .disabled(hasAllPresets()) - } - .sheet(isPresented: $showScanProtoServer) { - ScanProtocolServer(servers: $servers) - } - .modifier(BackButton { - if saveDisabled { - dismiss() - justOpened = false - } else { - showSaveDialog = true - } - }) - .confirmationDialog("Save servers?", isPresented: $showSaveDialog) { - Button("Save") { - saveServers() - dismiss() - justOpened = false - } - Button("Exit without saving") { dismiss() } - } - .alert(item: $alert) { a in - switch a { - case let .testsFailed(fs): - let msg = fs.map { (srv, f) in - "\(srv): \(f.localizedDescription)" - }.joined(separator: "\n") - return Alert( - title: Text("Tests failed!"), - message: Text("Some servers failed the test:\n" + msg) - ) - case .error: - return Alert( - title: Text("Error") - ) - } - } - .onAppear { - // this condition is needed to prevent re-setting the servers when exiting single server view - if !justOpened { return } - do { - let r = try getUserProtoServers(serverProtocol) - currServers = r.protoServers - presetServers = r.presetServers - servers = currServers - } catch let error { - alert = .error( - title: "Error loading \(proto) servers", - error: "Error: \(responseError(error))" - ) - } - justOpened = false - } - } - - private var saveDisabled: Bool { - servers.isEmpty || - servers == currServers || - testing || - !servers.allSatisfy { srv in - if let address = parseServerAddress(srv.server) { - return uniqueAddress(srv, address) - } - return false - } || - allServersDisabled - } - - private var allServersDisabled: Bool { - servers.allSatisfy { !$0.enabled } - } - - private func protocolServerView(_ server: Binding<ServerCfg>) -> some View { - let srv = server.wrappedValue - return NavigationLink(tag: srv.id, selection: $selectedServer) { - ProtocolServerView( - serverProtocol: serverProtocol, - server: server, - serverToEdit: srv - ) - .navigationBarTitle(srv.preset ? "Preset server" : "Your server") - .navigationBarTitleDisplayMode(.large) - } label: { - let address = parseServerAddress(srv.server) - HStack { - Group { - if let address = address { - if !address.valid || address.serverProtocol != serverProtocol { - invalidServer() - } else if !uniqueAddress(srv, address) { - Image(systemName: "exclamationmark.circle").foregroundColor(.red) - } else if !srv.enabled { - Image(systemName: "slash.circle").foregroundColor(.secondary) - } else { - showTestStatus(server: srv) - } - } else { - invalidServer() - } - } - .frame(width: 16, alignment: .center) - .padding(.trailing, 4) - - let v = Text(address?.hostnames.first ?? srv.server).lineLimit(1) - if srv.enabled { - v - } else { - v.foregroundColor(.secondary) - } - } - } - } - - func howToButton() -> some View { - Button { - DispatchQueue.main.async { - UIApplication.shared.open(howToUrl) - } - } label: { - HStack { - Text("How to use your servers") - Image(systemName: "arrow.up.right.circle") - } - } - } - - private func invalidServer() -> some View { - Image(systemName: "exclamationmark.circle").foregroundColor(.red) - } - - private func uniqueAddress(_ s: ServerCfg, _ address: ServerAddress) -> Bool { - servers.allSatisfy { srv in - address.hostnames.allSatisfy { host in - srv.id == s.id || !srv.server.contains(host) - } - } - } - - private func hasAllPresets() -> Bool { - presetServers.allSatisfy { hasPreset($0) } - } - - private func addAllPresets() { - for srv in presetServers { - if !hasPreset(srv) { - servers.append(ServerCfg(server: srv, preset: true, tested: nil, enabled: true)) - } - } - } - - private func hasPreset(_ srv: String) -> Bool { - servers.contains(where: { $0.server == srv }) - } - - private func testServers() { - resetTestStatus() - testing = true - Task { - let fs = await runServersTest() - await MainActor.run { - testing = false - if !fs.isEmpty { - alert = .testsFailed(failures: fs) - } - } - } - } - - private func resetTestStatus() { - for i in 0..<servers.count { - if servers[i].enabled { - servers[i].tested = nil - } - } - } - - private func runServersTest() async -> [String: ProtocolTestFailure] { - var fs: [String: ProtocolTestFailure] = [:] - for i in 0..<servers.count { - if servers[i].enabled { - if let f = await testServerConnection(server: $servers[i]) { - fs[serverHostname(servers[i].server)] = f - } - } - } - return fs - } - - func saveServers() { - Task { - do { - try await setUserProtoServers(serverProtocol, servers: servers) - await MainActor.run { - currServers = servers - editMode?.wrappedValue = .inactive - } - } catch let error { - let err = responseError(error) - logger.error("saveServers setUserProtocolServers error: \(err)") - await MainActor.run { - alert = .error( - title: "Error saving \(proto) servers", - error: "Make sure \(proto) server addresses are in correct format, line separated and are not duplicated (\(responseError(error)))." - ) - } - } - } - } -} - -struct ProtocolServersView_Previews: PreviewProvider { - static var previews: some View { - ProtocolServersView(serverProtocol: .smp) - } -} diff --git a/apps/ios/Shared/Views/UserSettings/RTCServers.swift b/apps/ios/Shared/Views/UserSettings/RTCServers.swift index 1b4c65dc2e..ef891738cc 100644 --- a/apps/ios/Shared/Views/UserSettings/RTCServers.swift +++ b/apps/ios/Shared/Views/UserSettings/RTCServers.swift @@ -13,6 +13,7 @@ private let howToUrl = URL(string: "https://simplex.chat/docs/webrtc.html#config let serversFont = Font.custom("Menlo", size: 14) struct RTCServers: View { + @EnvironmentObject var theme: AppTheme @State private var userRTCServers: [String] = [] @State private var isUserRTCServers = false @State private var isUserRTCServersToggle = false @@ -53,6 +54,7 @@ struct RTCServers: View { } footer: { if !isUserRTCServers { Text("Using SimpleX Chat servers.") + .foregroundColor(theme.colors.secondary) } } @@ -80,6 +82,7 @@ struct RTCServers: View { } } header: { Text("ICE servers (one per line)") + .foregroundColor(theme.colors.secondary) } footer: { HStack(spacing: 20) { if editRTCServers { diff --git a/apps/ios/Shared/Views/UserSettings/SetDeliveryReceiptsView.swift b/apps/ios/Shared/Views/UserSettings/SetDeliveryReceiptsView.swift index 4925b7af8d..e03dace43d 100644 --- a/apps/ios/Shared/Views/UserSettings/SetDeliveryReceiptsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SetDeliveryReceiptsView.swift @@ -11,12 +11,13 @@ import SimpleXChat struct SetDeliveryReceiptsView: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme var body: some View { VStack(spacing: 16) { Text("Delivery receipts!") .font(.title) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .padding(.vertical) .multilineTextAlignment(.center) @@ -89,7 +90,7 @@ struct SetDeliveryReceiptsView: View { .padding() .padding(.horizontal) .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(uiColor: .systemBackground)) + .background(AppTheme.shared.colors.background) } } diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index f889d9c394..e06b1c4dd3 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -18,7 +18,7 @@ let appBuild = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? let DEFAULT_SHOW_LA_NOTICE = "showLocalAuthenticationNotice" let DEFAULT_LA_NOTICE_SHOWN = "localAuthenticationNoticeShown" -let DEFAULT_PERFORM_LA = "performLocalAuthentication" +let DEFAULT_PERFORM_LA = "performLocalAuthentication" // deprecated, moved to app group let DEFAULT_LA_MODE = "localAuthenticationMode" let DEFAULT_LA_LOCK_DELAY = "localAuthenticationLockDelay" let DEFAULT_LA_SELF_DESTRUCT = "localAuthenticationSelfDestruct" @@ -27,36 +27,62 @@ let DEFAULT_NOTIFICATION_ALERT_SHOWN = "notificationAlertShown" let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay" let DEFAULT_WEBRTC_ICE_SERVERS = "webrtcICEServers" let DEFAULT_CALL_KIT_CALLS_IN_RECENTS = "callKitCallsInRecents" -let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" -let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" +let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" // unused. Use GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES instead +let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" // deprecated, moved to app group let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode" let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews" let DEFAULT_PRIVACY_SAVE_LAST_DRAFT = "privacySaveLastDraft" +let DEFAULT_PRIVACY_SHORT_LINKS = "privacyShortLinks" let DEFAULT_PRIVACY_PROTECT_SCREEN = "privacyProtectScreen" let DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET = "privacyDeliveryReceiptsSet" +let DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS = "privacyMediaBlurRadius" let DEFAULT_EXPERIMENTAL_CALLS = "experimentalCalls" let DEFAULT_CHAT_ARCHIVE_NAME = "chatArchiveName" let DEFAULT_CHAT_ARCHIVE_TIME = "chatArchiveTime" let DEFAULT_CHAT_V3_DB_MIGRATION = "chatV3DBMigration" +let DEFAULT_SHOULD_IMPORT_APP_SETTINGS = "shouldImportAppSettings" let DEFAULT_DEVELOPER_TOOLS = "developerTools" let DEFAULT_ENCRYPTION_STARTED = "encryptionStarted" let DEFAULT_ENCRYPTION_STARTED_AT = "encryptionStartedAt" -let DEFAULT_ACCENT_COLOR_RED = "accentColorRed" -let DEFAULT_ACCENT_COLOR_GREEN = "accentColorGreen" -let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue" -let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle" +let DEFAULT_ACCENT_COLOR_RED = "accentColorRed" // deprecated, only used for migration +let DEFAULT_ACCENT_COLOR_GREEN = "accentColorGreen" // deprecated, only used for migration +let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue" // deprecated, only used for migration +let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle" // deprecated, only used for migration +let DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius" +let DEFAULT_CHAT_ITEM_ROUNDNESS = "chatItemRoundness" +let DEFAULT_CHAT_ITEM_TAIL = "chatItemTail" +let DEFAULT_ONE_HAND_UI_CARD_SHOWN = "oneHandUICardShown" +let DEFAULT_ADDRESS_CREATION_CARD_SHOWN = "addressCreationCardShown" +let DEFAULT_TOOLBAR_MATERIAL = "toolbarMaterial" let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab" let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown" let DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE = "showHiddenProfilesNotice" let DEFAULT_SHOW_MUTE_PROFILE_ALERT = "showMuteProfileAlert" let DEFAULT_WHATS_NEW_VERSION = "defaultWhatsNewVersion" let DEFAULT_ONBOARDING_STAGE = "onboardingStage" +let DEFAULT_MIGRATION_TO_STAGE = "migrationToStage" +let DEFAULT_MIGRATION_FROM_STAGE = "migrationFromStage" let DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME = "customDisappearingMessageTime" let DEFAULT_SHOW_UNREAD_AND_FAVORITES = "showUnreadAndFavorites" let DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS = "deviceNameForRemoteAccess" let DEFAULT_CONFIRM_REMOTE_SESSIONS = "confirmRemoteSessions" let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast" let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto" +let DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE = "showDeleteConversationNotice" +let DEFAULT_SHOW_DELETE_CONTACT_NOTICE = "showDeleteContactNotice" +let DEFAULT_SHOW_SENT_VIA_RPOXY = "showSentViaProxy" +let DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE = "showSubscriptionPercentage" + +let DEFAULT_CURRENT_THEME = "currentTheme" +let DEFAULT_SYSTEM_DARK_THEME = "systemDarkTheme" +let DEFAULT_CURRENT_THEME_IDS = "currentThemeIds" +let DEFAULT_THEME_OVERRIDES = "themeOverrides" + +let DEFAULT_NETWORK_PROXY = "networkProxy" + +let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen" + +let defaultChatItemRoundness: Double = 0.75 let appDefaults: [String: Any] = [ DEFAULT_SHOW_LA_NOTICE: false, @@ -73,16 +99,20 @@ let appDefaults: [String: Any] = [ DEFAULT_PRIVACY_SIMPLEX_LINK_MODE: SimpleXLinkMode.description.rawValue, DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS: true, DEFAULT_PRIVACY_SAVE_LAST_DRAFT: true, + DEFAULT_PRIVACY_SHORT_LINKS: false, DEFAULT_PRIVACY_PROTECT_SCREEN: false, DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET: false, + DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS: 0, DEFAULT_EXPERIMENTAL_CALLS: false, DEFAULT_CHAT_V3_DB_MIGRATION: V3DBMigrationState.offer.rawValue, DEFAULT_DEVELOPER_TOOLS: false, DEFAULT_ENCRYPTION_STARTED: false, - DEFAULT_ACCENT_COLOR_RED: 0.000, - DEFAULT_ACCENT_COLOR_GREEN: 0.533, - DEFAULT_ACCENT_COLOR_BLUE: 1.000, - DEFAULT_USER_INTERFACE_STYLE: 0, + DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner, + DEFAULT_CHAT_ITEM_ROUNDNESS: defaultChatItemRoundness, + DEFAULT_CHAT_ITEM_TAIL: true, + DEFAULT_ONE_HAND_UI_CARD_SHOWN: false, + DEFAULT_ADDRESS_CREATION_CARD_SHOWN: false, + DEFAULT_TOOLBAR_MATERIAL: ToolbarMaterial.defaultMaterial, DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue, DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false, DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE: true, @@ -93,8 +123,37 @@ let appDefaults: [String: Any] = [ DEFAULT_CONFIRM_REMOTE_SESSIONS: false, DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: true, DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true, + DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE: true, + DEFAULT_SHOW_DELETE_CONTACT_NOTICE: true, + DEFAULT_SHOW_SENT_VIA_RPOXY: false, + DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE: false, + ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue, + + DEFAULT_THEME_OVERRIDES: "{}", + DEFAULT_CURRENT_THEME: DefaultTheme.SYSTEM_THEME_NAME, + DEFAULT_SYSTEM_DARK_THEME: DefaultTheme.DARK.themeName, + DEFAULT_CURRENT_THEME_IDS: "{}" ] +// only Bool defaults can be used here, +// or hintDefaultsUnchanged and resetHintDefaults need to be changed +let hintDefaults = [ + DEFAULT_LA_NOTICE_SHOWN, + DEFAULT_ONE_HAND_UI_CARD_SHOWN, + DEFAULT_ADDRESS_CREATION_CARD_SHOWN, + DEFAULT_LIVE_MESSAGE_ALERT_SHOWN, + DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE, + DEFAULT_SHOW_MUTE_PROFILE_ALERT, + DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE, + DEFAULT_SHOW_DELETE_CONTACT_NOTICE +] + +// not used anymore +enum ConnectViaLinkTab: String { + case scan + case paste +} + enum SimpleXLinkMode: String, Identifiable { case description case full @@ -133,185 +192,243 @@ let onboardingStageDefault = EnumDefault<OnboardingStage>(defaults: UserDefaults let customDisappearingMessageTimeDefault = IntDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME) +let showDeleteConversationNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE) +let showDeleteContactNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONTACT_NOTICE) + +/// after importing new database, this flag will be set and unset only after importing app settings in `initializeChat` */ +let shouldImportAppSettingsDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOULD_IMPORT_APP_SETTINGS) +let currentThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME, withDefault: DefaultTheme.SYSTEM_THEME_NAME) +let systemDarkThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SYSTEM_DARK_THEME, withDefault: DefaultTheme.DARK.themeName) +let currentThemeIdsDefault = CodableDefault<[String: String]>(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME_IDS, withDefault: [:] ) +let themeOverridesDefault: CodableDefault<[ThemeOverrides]> = CodableDefault(defaults: UserDefaults.standard, forKey: DEFAULT_THEME_OVERRIDES, withDefault: []) + func setGroupDefaults() { privacyAcceptImagesGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES)) + appLocalAuthEnabledGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)) + privacyLinkPreviewsGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS)) + profileImageCornerRadiusGroupDefault.set(UserDefaults.standard.double(forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS)) } +public class StringDefault { + var defaults: UserDefaults + var key: String + var defaultValue: String + + public init(defaults: UserDefaults = UserDefaults.standard, forKey: String, withDefault: String) { + self.defaults = defaults + self.key = forKey + self.defaultValue = withDefault + } + + public func get() -> String { + defaults.string(forKey: key) ?? defaultValue + } + + public func set(_ value: String) { + defaults.set(value, forKey: key) + defaults.synchronize() + } +} + +public class CodableDefault<T: Codable> { + var defaults: UserDefaults + var key: String + var defaultValue: T + + public init(defaults: UserDefaults = UserDefaults.standard, forKey: String, withDefault: T) { + self.defaults = defaults + self.key = forKey + self.defaultValue = withDefault + } + + var cache: T? = nil + + public func get() -> T { + if let cache { + return cache + } else if let value = defaults.string(forKey: key) { + let res = decodeJSON(value) ?? defaultValue + cache = res + return res + } + return defaultValue + } + + public func set(_ value: T) { + defaults.set(encodeJSON(value), forKey: key) + cache = value + //defaults.synchronize() + } +} + +let networkProxyDefault: CodableDefault<NetworkProxy> = CodableDefault(defaults: UserDefaults.standard, forKey: DEFAULT_NETWORK_PROXY, withDefault: NetworkProxy.def) + struct SettingsView: View { @Environment(\.colorScheme) var colorScheme + @Environment(\.dismiss) var dismiss @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var sceneDelegate: SceneDelegate - @Binding var showSettings: Bool + @EnvironmentObject var theme: AppTheme + @State private var showProgress: Bool = false var body: some View { ZStack { settingsView() - if let la = chatModel.laRequest { - LocalAuthView(authRequest: la) + if showProgress { + progressView() } } } - @ViewBuilder func settingsView() -> some View { - let user: User = chatModel.currentUser! - NavigationView { - List { - Section("You") { - NavigationLink { - UserProfile() - .navigationTitle("Your current profile") - } label: { - ProfilePreview(profileOf: user) - .padding(.leading, -8) - } - - NavigationLink { - UserProfilesView() - } label: { - settingsRow("person.crop.rectangle.stack") { Text("Your chat profiles") } - } - - NavigationLink { - UserAddressView(shareViaProfile: chatModel.currentUser!.addressShared) - .navigationTitle("SimpleX address") - .navigationBarTitleDisplayMode(.large) - } label: { - settingsRow("qrcode") { Text("Your SimpleX address") } - } - - NavigationLink { - PreferencesView(profile: user.profile, preferences: user.fullPreferences, currentPreferences: user.fullPreferences) - .navigationTitle("Your preferences") - } label: { - settingsRow("switch.2") { Text("Chat preferences") } - } - - NavigationLink { - ConnectDesktopView(viaSettings: true) - } label: { - settingsRow("desktopcomputer") { Text("Use from desktop") } + func settingsView() -> some View { + List { + let user = chatModel.currentUser + Section(header: Text("Settings").foregroundColor(theme.colors.secondary)) { + NavigationLink { + NotificationsView() + .navigationTitle("Notifications") + .modifier(ThemedBackground(grouped: true)) + } label: { + HStack { + notificationsIcon() + Text("Notifications") } } .disabled(chatModel.chatRunning != true) - - Section("Settings") { - NavigationLink { - NotificationsView() - .navigationTitle("Notifications") - } label: { - HStack { - notificationsIcon() - Text("Notifications") - } - } - .disabled(chatModel.chatRunning != true) - - NavigationLink { - NetworkAndServers() - .navigationTitle("Network & servers") - } label: { - settingsRow("externaldrive.connected.to.line.below") { Text("Network & servers") } - } - .disabled(chatModel.chatRunning != true) - - NavigationLink { - CallSettings() - .navigationTitle("Your calls") - } label: { - settingsRow("video") { Text("Audio & video calls") } - } - .disabled(chatModel.chatRunning != true) - - NavigationLink { - PrivacySettings() - .navigationTitle("Your privacy") - } label: { - settingsRow("lock") { Text("Privacy & security") } - } - .disabled(chatModel.chatRunning != true) - - if UIApplication.shared.supportsAlternateIcons { - NavigationLink { - AppearanceSettings() - .navigationTitle("Appearance") - } label: { - settingsRow("sun.max") { Text("Appearance") } - } - .disabled(chatModel.chatRunning != true) - } - - chatDatabaseRow() - } - Section("Help") { + NavigationLink { + NetworkAndServers() + .navigationTitle("Network & servers") + .modifier(ThemedBackground(grouped: true)) + } label: { + settingsRow("externaldrive.connected.to.line.below", color: theme.colors.secondary) { Text("Network & servers") } + } + .disabled(chatModel.chatRunning != true) + + NavigationLink { + CallSettings() + .navigationTitle("Your calls") + .modifier(ThemedBackground(grouped: true)) + } label: { + settingsRow("video", color: theme.colors.secondary) { Text("Audio & video calls") } + } + .disabled(chatModel.chatRunning != true) + + NavigationLink { + PrivacySettings() + .navigationTitle("Your privacy") + .modifier(ThemedBackground(grouped: true)) + } label: { + settingsRow("lock", color: theme.colors.secondary) { Text("Privacy & security") } + } + .disabled(chatModel.chatRunning != true) + + if UIApplication.shared.supportsAlternateIcons { NavigationLink { - ChatHelp(showSettings: $showSettings) - .navigationTitle("Welcome \(user.displayName)!") - .frame(maxHeight: .infinity, alignment: .top) + AppearanceSettings() + .navigationTitle("Appearance") + .modifier(ThemedBackground(grouped: true)) } label: { - settingsRow("questionmark") { Text("How to use it") } - } - NavigationLink { - WhatsNewView(viaSettings: true) - .navigationBarTitleDisplayMode(.inline) - } label: { - settingsRow("plus") { Text("What's new") } - } - NavigationLink { - SimpleXInfo(onboarding: false) - .navigationBarTitle("", displayMode: .inline) - .frame(maxHeight: .infinity, alignment: .top) - } label: { - settingsRow("info") { Text("About SimpleX Chat") } - } - settingsRow("number") { - Button("Send questions and ideas") { - showSettings = false - DispatchQueue.main.async { - UIApplication.shared.open(simplexTeamURL) - } - } + settingsRow("sun.max", color: theme.colors.secondary) { Text("Appearance") } } .disabled(chatModel.chatRunning != true) - settingsRow("envelope") { Text("[Send us email](mailto:chat@simplex.chat)") } - } - - Section("Support SimpleX Chat") { - settingsRow("keyboard") { Text("[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)") } - settingsRow("star") { - Button("Rate the app") { - if let scene = sceneDelegate.windowScene { - SKStoreReviewController.requestReview(in: scene) - } - } - } - ZStack(alignment: .leading) { - Image(colorScheme == .dark ? "github_light" : "github") - .resizable() - .frame(width: 24, height: 24) - .opacity(0.5) - Text("[Star on GitHub](https://github.com/simplex-chat/simplex-chat)") - .padding(.leading, indent) - } - } - - Section("Develop") { - NavigationLink { - DeveloperView() - .navigationTitle("Developer tools") - } label: { - settingsRow("chevron.left.forwardslash.chevron.right") { Text("Developer tools") } - } - NavigationLink { - VersionView() - .navigationBarTitle("App version") - } label: { - Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))") - } } } - .navigationTitle("Your settings") + + Section(header: Text("Chat database").foregroundColor(theme.colors.secondary)) { + chatDatabaseRow() + NavigationLink { + MigrateFromDevice(showProgressOnSettings: $showProgress) + .toolbar { + // Redaction broken for `.navigationTitle` - using a toolbar item instead. + ToolbarItem(placement: .principal) { + Text("Migrate device").font(.headline) + } + } + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + settingsRow("tray.and.arrow.up", color: theme.colors.secondary) { Text("Migrate to another device") } + } + } + + Section(header: Text("Help").foregroundColor(theme.colors.secondary)) { + if let user = user { + NavigationLink { + ChatHelp(dismissSettingsSheet: dismiss) + .navigationTitle("Welcome \(user.displayName)!") + .modifier(ThemedBackground()) + .frame(maxHeight: .infinity, alignment: .top) + } label: { + settingsRow("questionmark", color: theme.colors.secondary) { Text("How to use it") } + } + } + NavigationLink { + WhatsNewView(viaSettings: true, updatedConditions: false) + .modifier(ThemedBackground()) + .navigationBarTitleDisplayMode(.inline) + } label: { + settingsRow("plus", color: theme.colors.secondary) { Text("What's new") } + } + NavigationLink { + SimpleXInfo(onboarding: false) + .navigationBarTitle("", displayMode: .inline) + .modifier(ThemedBackground()) + .frame(maxHeight: .infinity, alignment: .top) + } label: { + settingsRow("info", color: theme.colors.secondary) { Text("About SimpleX Chat") } + } + settingsRow("number", color: theme.colors.secondary) { + Button("Send questions and ideas") { + dismiss() + DispatchQueue.main.async { + UIApplication.shared.open(simplexTeamURL) + } + } + } + .disabled(chatModel.chatRunning != true) + settingsRow("envelope", color: theme.colors.secondary) { Text("[Send us email](mailto:chat@simplex.chat)") } + } + + 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("star", color: theme.colors.secondary) { + Button("Rate the app") { + if let scene = sceneDelegate.windowScene { + SKStoreReviewController.requestReview(in: scene) + } + } + } + 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) + } + } + + Section(header: Text("Develop").foregroundColor(theme.colors.secondary)) { + NavigationLink { + DeveloperView() + .navigationTitle("Developer tools") + .modifier(ThemedBackground(grouped: true)) + } label: { + settingsRow("chevron.left.forwardslash.chevron.right", color: theme.colors.secondary) { Text("Developer tools") } + } + NavigationLink { + VersionView() + .navigationBarTitle("App version") + .modifier(ThemedBackground()) + } label: { + Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))") + } + } } + .navigationTitle("Your settings") + .modifier(ThemedBackground(grouped: true)) .onDisappear { chatModel.showingTerminal = false chatModel.terminalItems = [] @@ -320,10 +437,11 @@ struct SettingsView: View { private func chatDatabaseRow() -> some View { NavigationLink { - DatabaseView(showSettings: $showSettings, chatItemTTL: chatModel.chatItemTTL) + DatabaseView(dismissSettingsSheet: dismiss, chatItemTTL: chatModel.chatItemTTL) .navigationTitle("Your chat database") + .modifier(ThemedBackground(grouped: true)) } label: { - let color: Color = chatModel.chatDbEncrypted == false ? .orange : .secondary + let color: Color = chatModel.chatDbEncrypted == false ? .orange : theme.colors.secondary settingsRow("internaldrive", color: color) { HStack { Text("Database passphrase & export") @@ -336,6 +454,13 @@ struct SettingsView: View { } } + private func progressView() -> some View { + VStack { + ProgressView().scaleEffect(2) + } + .frame(maxWidth: .infinity, maxHeight: .infinity ) + } + private enum NotificationAlert { case enable case error(LocalizedStringKey, String) @@ -347,13 +472,17 @@ struct SettingsView: View { switch (chatModel.tokenStatus) { case .new: icon = "bolt" - color = .secondary + color = theme.colors.secondary case .registered: icon = "bolt.fill" - color = .secondary - case .invalid: + color = theme.colors.secondary + case .invalid: fallthrough + case .invalidBad: fallthrough + case .invalidTopic: fallthrough + case .invalidExpired: fallthrough + case .invalidUnregistered: icon = "bolt.slash" - color = .secondary + color = theme.colors.secondary case .confirmed: icon = "bolt.fill" color = .yellow @@ -362,10 +491,10 @@ struct SettingsView: View { color = .green case .expired: icon = "bolt.slash.fill" - color = .secondary + color = theme.colors.secondary case .none: icon = "bolt" - color = .secondary + color = theme.colors.secondary } return Image(systemName: icon) .padding(.trailing, 9) @@ -373,7 +502,7 @@ struct SettingsView: View { } } -func settingsRow<Content : View>(_ icon: String, color: Color = .secondary, content: @escaping () -> Content) -> some View { +func settingsRow<Content : View>(_ icon: String, color: Color/* = .secondary*/, content: @escaping () -> Content) -> some View { ZStack(alignment: .leading) { Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center) .symbolRenderingMode(.monochrome) @@ -384,33 +513,31 @@ func settingsRow<Content : View>(_ icon: String, color: Color = .secondary, cont struct ProfilePreview: View { var profileOf: NamedChat - var color = Color(uiColor: .tertiarySystemFill) + var color = Color(uiColor: .tertiarySystemGroupedBackground) var body: some View { HStack { - ProfileImage(imageStr: profileOf.image, color: color) - .frame(width: 44, height: 44) + ProfileImage(imageStr: profileOf.image, size: 44, color: color) .padding(.trailing, 6) - .padding(.vertical, 6) - VStack(alignment: .leading) { - Text(profileOf.displayName) - .fontWeight(.bold) - .font(.title2) - if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName { - Text(profileOf.fullName) - } - } + profileName(profileOf).lineLimit(1) } } } +func profileName(_ profileOf: NamedChat) -> Text { + var t = Text(profileOf.displayName).fontWeight(.semibold).font(.title2) + if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName { + t = t + Text(verbatim: " (" + profileOf.fullName + ")") +// .font(.callout) + } + return t +} + struct SettingsView_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() chatModel.currentUser = User.sampleData - @State var showSettings = false - - return SettingsView(showSettings: $showSettings) + return SettingsView() .environmentObject(chatModel) } } diff --git a/apps/ios/Shared/Views/UserSettings/StorageView.swift b/apps/ios/Shared/Views/UserSettings/StorageView.swift new file mode 100644 index 0000000000..094c1cb3d6 --- /dev/null +++ b/apps/ios/Shared/Views/UserSettings/StorageView.swift @@ -0,0 +1,56 @@ +// +// StorageView.swift +// SimpleX (iOS) +// +// Created by Stanislav Dmitrenko on 13.01.2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct StorageView: View { + @State var appGroupFiles: [String: Int64] = [:] + @State var documentsFiles: [String: Int64] = [:] + + var body: some View { + ScrollView { + VStack(alignment: .leading) { + directoryView("App group:", appGroupFiles) + if !documentsFiles.isEmpty { + directoryView("Documents:", documentsFiles) + } + } + } + .padding() + .onAppear { + appGroupFiles = traverseFiles(in: getGroupContainerDirectory()) + documentsFiles = traverseFiles(in: getDocumentsDirectory()) + } + } + + @ViewBuilder + private func directoryView(_ name: LocalizedStringKey, _ contents: [String: Int64]) -> some View { + Text(name).font(.headline) + ForEach(Array(contents), id: \.key) { (key, value) in + Text(key).bold() + Text(verbatim: " ") + Text((ByteCountFormatter.string(fromByteCount: value, countStyle: .binary))) + } + } + + private func traverseFiles(in dir: URL) -> [String: Int64] { + var res: [String: Int64] = [:] + let fm = FileManager.default + do { + if let enumerator = fm.enumerator(at: dir, includingPropertiesForKeys: [.isDirectoryKey, .fileSizeKey, .fileAllocatedSizeKey]) { + for case let url as URL in enumerator { + let attrs = try url.resourceValues(forKeys: [/*.isDirectoryKey, .fileSizeKey,*/ .fileAllocatedSizeKey]) + let root = String(url.absoluteString.replacingOccurrences(of: dir.absoluteString, with: "").split(separator: "/")[0]) + res[root] = (res[root] ?? 0) + Int64(attrs.fileAllocatedSize ?? 0) + } + } + } catch { + logger.error("Error traversing files: \(error)") + } + return res + } +} diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift b/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift index 15f6a1c7d7..6c1ea8deb2 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift @@ -9,15 +9,94 @@ import SwiftUI struct UserAddressLearnMore: View { + @State var showCreateAddressButton = false + @State private var createAddressLinkActive = false + @State private var createOneTimeLinkActive = false + var body: some View { - List { - VStack(alignment: .leading, spacing: 18) { - Text("You can share your address as a link or QR code - anybody can connect to you.") - Text("You won't lose your contacts if you later delete your address.") - Text("When people request to connect, you can accept or reject it.") - Text("Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).") + VStack { + List { + VStack(alignment: .leading, spacing: 12) { + (Text(Image(systemName: "envelope")).foregroundColor(.secondary) + textSpace + Text("Share address publicly").bold().font(.title2)) + Text("Share SimpleX address on social media.") + Text("You won't lose your contacts if you later delete your address.") + + (Text(Image(systemName: "link.badge.plus")).foregroundColor(.secondary) + textSpace + Text("Share 1-time link with a friend").font(.title2).bold()) + .padding(.top) + Text("1-time link can be used *with one contact only* - share in person or via any messenger.") + Text("You can set connection name, to remember who the link was shared with.") + + if !showCreateAddressButton { + (Text(Image(systemName: "shield")).foregroundColor(.secondary) + textSpace + Text("Connection security").font(.title2).bold()) + .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).") + .padding(.top) + } + + } + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } - .listRowBackground(Color.clear) + .frame(maxHeight: .infinity, alignment: .top) + + Spacer() + + if showCreateAddressButton { + VStack { + addressCreationButton() + .padding(.bottom) + + createOneTimeLinkButton() + } + .padding() + } + } + .frame(maxHeight: .infinity, alignment: .top) + } + + private func addressCreationButton() -> some View { + ZStack { + Button { + createAddressLinkActive = true + } label: { + Text("Create SimpleX address") + } + .buttonStyle(OnboardingButtonStyle()) + + NavigationLink(isActive: $createAddressLinkActive) { + UserAddressView(autoCreate: true) + .navigationTitle("SimpleX address") + .navigationBarTitleDisplayMode(.large) + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } + } + + private func createOneTimeLinkButton() -> some View { + ZStack { + Button { + createOneTimeLinkActive = true + } label: { + Text("Create 1-time link") + .font(.callout) + } + + NavigationLink(isActive: $createOneTimeLinkActive) { + NewChatView(selection: .invite) + .navigationTitle("New chat") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() } } } diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index e9657961ef..4813edf96c 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -8,61 +8,40 @@ import SwiftUI import MessageUI -import SimpleXChat +@preconcurrency import SimpleXChat struct UserAddressView: View { @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject private var chatModel: ChatModel - @State var viaCreateLinkView = false + @EnvironmentObject var theme: AppTheme @State var shareViaProfile = false + @State var autoCreate = false + @State private var showShortLink = true @State private var aas = AutoAcceptState() @State private var savedAAS = AutoAcceptState() - @State private var ignoreShareViaProfileChange = false @State private var showMailView = false @State private var mailViewResult: Result<MFMailComposeResult, Error>? = nil @State private var alert: UserAddressAlert? - @State private var showSaveDialogue = false @State private var progressIndicator = false - @FocusState private var keyboardVisible: Bool private enum UserAddressAlert: Identifiable { case deleteAddress - case profileAddress(on: Bool) case shareOnCreate - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { case .deleteAddress: return "deleteAddress" - case let .profileAddress(on): return "profileAddress \(on)" case .shareOnCreate: return "shareOnCreate" case let .error(title, _): return "error \(title)" } } } - + var body: some View { ZStack { - if viaCreateLinkView { - userAddressScrollView() - } else { - userAddressScrollView() - .modifier(BackButton { - if savedAAS == aas { - dismiss() - } else { - keyboardVisible = false - showSaveDialogue = true - } - }) - .confirmationDialog("Save settings?", isPresented: $showSaveDialogue) { - Button("Save auto-accept settings") { - saveAAS() - dismiss() - } - Button("Exit without saving") { dismiss() } - } - } + userAddressView() + if progressIndicator { ZStack { if chatModel.userAddress != nil { @@ -75,22 +54,10 @@ struct UserAddressView: View { } } } - } - - @Namespace private var bottomID - - private func userAddressScrollView() -> some View { - ScrollViewReader { proxy in - userAddressView() - .onChange(of: keyboardVisible) { _ in - if keyboardVisible { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - withAnimation { - proxy.scrollTo(bottomID, anchor: .top) - } - } - } - } + .onAppear { + if chatModel.userAddress == nil, autoCreate { + createAddress() + } } } @@ -102,14 +69,19 @@ struct UserAddressView: View { aas = AutoAcceptState(userAddress: userAddress) savedAAS = aas } - .onChange(of: aas.enable) { _ in - if !aas.enable { aas = AutoAcceptState() } - } } else { Section { createAddressButton() - } footer: { - Text("Create an address to let people connect with you.") + } header: { + Text("For social media") + .foregroundColor(theme.colors.secondary) + } + + Section { + createOneTimeLinkButton() + } header: { + Text("Or to share privately") + .foregroundColor(theme.colors.secondary) } Section { @@ -124,8 +96,8 @@ struct UserAddressView: View { title: Text("Delete address?"), message: shareViaProfile - ? Text("All your contacts will remain connected. Profile update will be sent to your contacts.") - : Text("All your contacts will remain connected."), + ? Text("All your contacts will remain connected. Profile update will be sent to your contacts.") + : Text("All your contacts will remain connected."), primaryButton: .destructive(Text("Delete")) { progressIndicator = true Task { @@ -135,7 +107,6 @@ struct UserAddressView: View { chatModel.userAddress = nil chatModel.updateUser(u) if shareViaProfile { - ignoreShareViaProfileChange = true shareViaProfile = false } } @@ -148,96 +119,108 @@ struct UserAddressView: View { } }, secondaryButton: .cancel() ) - case let .profileAddress(on): - if on { - return Alert( - title: Text("Share address with contacts?"), - message: Text("Profile update will be sent to your contacts."), - primaryButton: .default(Text("Share")) { - setProfileAddress(on) - }, secondaryButton: .cancel() { - ignoreShareViaProfileChange = true - shareViaProfile = !on - } - ) - } else { - return Alert( - title: Text("Stop sharing address?"), - message: Text("Profile update will be sent to your contacts."), - primaryButton: .default(Text("Stop sharing")) { - setProfileAddress(on) - }, secondaryButton: .cancel() { - ignoreShareViaProfileChange = true - shareViaProfile = !on - } - ) - } 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."), primaryButton: .default(Text("Share")) { - setProfileAddress(true) - ignoreShareViaProfileChange = true + setProfileAddress($progressIndicator, true) shareViaProfile = true }, secondaryButton: .cancel() ) case let .error(title, error): - return Alert(title: Text(title), message: Text(error)) + return mkAlert(title: title, message: error) } } } @ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View { Section { - SimpleXLinkQRCode(uri: userAddress.connReqContact) - .id("simplex-contact-address-qrcode-\(userAddress.connReqContact)") + SimpleXCreatedLinkQRCode(link: userAddress.connLinkContact, short: $showShortLink) + .id("simplex-contact-address-qrcode-\(userAddress.connLinkContact.simplexChatUri(short: showShortLink))") shareQRCodeButton(userAddress) - if MFMailComposeViewController.canSendMail() { - shareViaEmailButton(userAddress) + // if MFMailComposeViewController.canSendMail() { + // shareViaEmailButton(userAddress) + // } + settingsRow("briefcase", color: theme.colors.secondary) { + Toggle("Business address", isOn: $aas.business) + .onChange(of: aas.business) { ba in + if ba { + aas.enable = true + aas.incognito = false + } + saveAAS($aas, $savedAAS) + } } - shareWithContactsButton() - autoAcceptToggle() - learnMoreButton() + addressSettingsButton(userAddress) } header: { - Text("Address") + ToggleShortLinkHeader(text: Text("For social media"), link: userAddress.connLinkContact, short: $showShortLink) + } footer: { + if aas.business { + Text("Add your team members to the conversations.") + .foregroundColor(theme.colors.secondary) + } } - if aas.enable { - autoAcceptSection() + Section { + createOneTimeLinkButton() + } header: { + Text("Or to share privately") + .foregroundColor(theme.colors.secondary) + } + + Section { + learnMoreButton() } Section { deleteAddressButton() } footer: { Text("Your contacts will remain connected.") + .foregroundColor(theme.colors.secondary) } - .id(bottomID) } private func createAddressButton() -> some View { Button { - progressIndicator = true - Task { - do { - let connReqContact = try await apiCreateUserAddress() - DispatchQueue.main.async { - chatModel.userAddress = UserContactLink(connReqContact: connReqContact) - alert = .shareOnCreate - progressIndicator = false - } - } catch let error { - logger.error("UserAddressView apiCreateUserAddress: \(responseError(error))") - let a = getErrorAlert(error, "Error creating address") - alert = .error(title: a.title, error: a.message) - await MainActor.run { progressIndicator = false } - } - } + createAddress() } label: { Label("Create SimpleX address", systemImage: "qrcode") } } + private func createAddress() { + progressIndicator = true + Task { + do { + let short = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS) + let connLinkContact = try await apiCreateUserAddress(short: short) + DispatchQueue.main.async { + chatModel.userAddress = UserContactLink(connLinkContact: connLinkContact) + alert = .shareOnCreate + progressIndicator = false + } + } catch let error { + logger.error("UserAddressView apiCreateUserAddress: \(responseError(error))") + let a = getErrorAlert(error, "Error creating address") + alert = .error(title: a.title, error: a.message) + await MainActor.run { progressIndicator = false } + } + } + } + + private func createOneTimeLinkButton() -> some View { + NavigationLink { + NewChatView(selection: .invite) + .navigationTitle("New chat") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } label: { + Label("Create 1-time link", systemImage: "link.badge.plus") + .foregroundColor(theme.colors.primary) + } + } + private func deleteAddressButton() -> some View { Button(role: .destructive) { alert = .deleteAddress @@ -249,9 +232,9 @@ struct UserAddressView: View { private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View { Button { - showShareSheet(items: [simplexChatLink(userAddress.connReqContact)]) + showShareSheet(items: [simplexChatLink(userAddress.connLinkContact.simplexChatUri(short: showShortLink))]) } label: { - settingsRow("square.and.arrow.up") { + settingsRow("square.and.arrow.up", color: theme.colors.secondary) { Text("Share address") } } @@ -261,7 +244,7 @@ struct UserAddressView: View { Button { showMailView = true } label: { - settingsRow("envelope") { + settingsRow("envelope", color: theme.colors.secondary) { Text("Invite friends") } } @@ -287,110 +270,250 @@ struct UserAddressView: View { } } - private func autoAcceptToggle() -> some View { - settingsRow("checkmark") { - Toggle("Auto-accept", isOn: $aas.enable) - .onChange(of: aas.enable) { _ in - saveAAS() - } + private func addressSettingsButton(_ userAddress: UserContactLink) -> some View { + NavigationLink { + UserAddressSettingsView(shareViaProfile: $shareViaProfile) + .navigationTitle("Address settings") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } label: { + Text("Address settings") } } private func learnMoreButton() -> some View { NavigationLink { UserAddressLearnMore() - .navigationTitle("SimpleX address") - .navigationBarTitleDisplayMode(.large) + .navigationTitle("Address or 1-time link?") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.inline) } label: { - settingsRow("info.circle") { - Text("About SimpleX address") + settingsRow("info.circle", color: theme.colors.secondary) { + Text("SimpleX address or 1-time link?") + } + } + } +} + +struct ToggleShortLinkHeader: View { + @EnvironmentObject var theme: AppTheme + let text: Text + var link: CreatedConnLink + @Binding var short: Bool + + var body: some View { + if link.connShortLink == nil { + text.foregroundColor(theme.colors.secondary) + } else { + HStack { + text.foregroundColor(theme.colors.secondary) + Spacer() + Text(short ? "Full link" : "Short link") + .textCase(.none) + .foregroundColor(theme.colors.primary) + .onTapGesture { short.toggle() } + } + } + } +} + +private struct AutoAcceptState: Equatable { + var enable = false + var incognito = false + var business = false + var welcomeText = "" + + init(enable: Bool = false, incognito: Bool = false, business: Bool = false, welcomeText: String = "") { + self.enable = enable + self.incognito = incognito + self.business = business + self.welcomeText = welcomeText + } + + init(userAddress: UserContactLink) { + if let aa = userAddress.autoAccept { + enable = true + incognito = aa.acceptIncognito + business = aa.businessAddress + if let msg = aa.autoReply { + welcomeText = msg.text + } else { + welcomeText = "" + } + } else { + enable = false + incognito = false + business = false + welcomeText = "" + } + } + + var autoAccept: AutoAccept? { + if enable { + var autoReply: MsgContent? = nil + let s = welcomeText.trimmingCharacters(in: .whitespacesAndNewlines) + if s != "" { autoReply = .text(s) } + return AutoAccept(businessAddress: business, acceptIncognito: incognito, autoReply: autoReply) + } + return nil + } +} + +private func setProfileAddress(_ progressIndicator: Binding<Bool>, _ on: Bool) { + progressIndicator.wrappedValue = true + Task { + do { + if let u = try await apiSetProfileAddress(on: on) { + DispatchQueue.main.async { + ChatModel.shared.updateUser(u) + } + } + await MainActor.run { progressIndicator.wrappedValue = false } + } catch let error { + logger.error("apiSetProfileAddress: \(responseError(error))") + await MainActor.run { progressIndicator.wrappedValue = false } + } + } +} + +struct UserAddressSettingsView: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme + @Binding var shareViaProfile: Bool + @State private var aas = AutoAcceptState() + @State private var savedAAS = AutoAcceptState() + @State private var ignoreShareViaProfileChange = false + @State private var progressIndicator = false + @FocusState private var keyboardVisible: Bool + + var body: some View { + ZStack { + if let userAddress = ChatModel.shared.userAddress { + userAddressSettingsView() + .onAppear { + aas = AutoAcceptState(userAddress: userAddress) + savedAAS = aas + } + .onChange(of: aas.enable) { aasEnabled in + if !aasEnabled { aas = AutoAcceptState() } + } + .onDisappear { + if savedAAS != aas { + showAlert( + title: NSLocalizedString("Auto-accept settings", comment: "alert title"), + message: NSLocalizedString("Settings were changed.", comment: "alert message"), + buttonTitle: NSLocalizedString("Save", comment: "alert button"), + buttonAction: { saveAAS($aas, $savedAAS) }, + cancelButton: true + ) + } + } + } else { + Text(String("Error opening address settings")) + } + if progressIndicator { + ProgressView().scaleEffect(2) + } + } + } + + private func userAddressSettingsView() -> some View { + List { + Section { + shareWithContactsButton() + autoAcceptToggle().disabled(aas.business) + } + + if aas.enable { + autoAcceptSection() } } } private func shareWithContactsButton() -> some View { - settingsRow("person") { + settingsRow("person", color: theme.colors.secondary) { Toggle("Share with contacts", isOn: $shareViaProfile) .onChange(of: shareViaProfile) { on in if ignoreShareViaProfileChange { ignoreShareViaProfileChange = false } else { - alert = .profileAddress(on: on) + if on { + showAlert( + NSLocalizedString("Share address with contacts?", comment: "alert title"), + message: NSLocalizedString("Profile update will be sent to your contacts.", comment: "alert message"), + actions: {[ + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "alert action"), + style: .default, + handler: { _ in + ignoreShareViaProfileChange = true + shareViaProfile = !on + } + ), + UIAlertAction( + title: NSLocalizedString("Share", comment: "alert action"), + style: .default, + handler: { _ in + setProfileAddress($progressIndicator, on) + } + ) + ]} + ) + } else { + showAlert( + NSLocalizedString("Stop sharing address?", comment: "alert title"), + message: NSLocalizedString("Profile update will be sent to your contacts.", comment: "alert message"), + actions: {[ + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "alert action"), + style: .default, + handler: { _ in + ignoreShareViaProfileChange = true + shareViaProfile = !on + } + ), + UIAlertAction( + title: NSLocalizedString("Stop sharing", comment: "alert action"), + style: .default, + handler: { _ in + setProfileAddress($progressIndicator, on) + } + ) + ]} + ) + } } } } } - private func setProfileAddress(_ on: Bool) { - progressIndicator = true - Task { - do { - if let u = try await apiSetProfileAddress(on: on) { - DispatchQueue.main.async { - chatModel.updateUser(u) - } + private func autoAcceptToggle() -> some View { + settingsRow("checkmark", color: theme.colors.secondary) { + Toggle("Auto-accept", isOn: $aas.enable) + .onChange(of: aas.enable) { _ in + saveAAS($aas, $savedAAS) } - await MainActor.run { progressIndicator = false } - } catch let error { - logger.error("UserAddressView apiSetProfileAddress: \(responseError(error))") - await MainActor.run { progressIndicator = false } - } } } - private struct AutoAcceptState: Equatable { - var enable = false - var incognito = false - var welcomeText = "" - - init(enable: Bool = false, incognito: Bool = false, welcomeText: String = "") { - self.enable = enable - self.incognito = incognito - self.welcomeText = welcomeText - } - - init(userAddress: UserContactLink) { - if let aa = userAddress.autoAccept { - enable = true - incognito = aa.acceptIncognito - if let msg = aa.autoReply { - welcomeText = msg.text - } else { - welcomeText = "" - } - } else { - enable = false - incognito = false - welcomeText = "" - } - } - - var autoAccept: AutoAccept? { - if enable { - var autoReply: MsgContent? = nil - let s = welcomeText.trimmingCharacters(in: .whitespacesAndNewlines) - if s != "" { autoReply = .text(s) } - return AutoAccept(acceptIncognito: incognito, autoReply: autoReply) - } - return nil - } - } - - @ViewBuilder private func autoAcceptSection() -> some View { + private func autoAcceptSection() -> some View { Section { - acceptIncognitoToggle() + if !aas.business { + acceptIncognitoToggle() + } welcomeMessageEditor() saveAASButton() .disabled(aas == savedAAS) } header: { Text("Auto-accept") + .foregroundColor(theme.colors.secondary) } } private func acceptIncognitoToggle() -> some View { settingsRow( aas.incognito ? "theatermasks.fill" : "theatermasks", - color: aas.incognito ? .indigo : .secondary + color: aas.incognito ? .indigo : theme.colors.secondary ) { Toggle("Accept incognito", isOn: $aas.incognito) } @@ -401,7 +524,7 @@ struct UserAddressView: View { Group { if aas.welcomeText.isEmpty { TextEditor(text: Binding.constant(NSLocalizedString("Enter welcome message… (optional)", comment: "placeholder"))) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .disabled(true) } TextEditor(text: $aas.welcomeText) @@ -417,22 +540,24 @@ struct UserAddressView: View { private func saveAASButton() -> some View { Button { keyboardVisible = false - saveAAS() + saveAAS($aas, $savedAAS) } label: { Text("Save") } } +} - private func saveAAS() { - Task { - do { - if let address = try await userAddressAutoAccept(aas.autoAccept) { - chatModel.userAddress = address - savedAAS = aas +private func saveAAS(_ aas: Binding<AutoAcceptState>, _ savedAAS: Binding<AutoAcceptState>) { + Task { + do { + if let address = try await userAddressAutoAccept(aas.wrappedValue.autoAccept) { + await MainActor.run { + ChatModel.shared.userAddress = address + savedAAS.wrappedValue = aas.wrappedValue } - } catch let error { - logger.error("userAddressAutoAccept error: \(responseError(error))") } + } catch let error { + logger.error("userAddressAutoAccept error: \(responseError(error))") } } } @@ -440,7 +565,9 @@ struct UserAddressView: View { struct UserAddressView_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() - chatModel.userAddress = UserContactLink(connReqContact: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D") + chatModel.userAddress = UserContactLink(connLinkContact: CreatedConnLink(connFullLink: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", connShortLink: nil)) + + return Group { UserAddressView() .environmentObject(chatModel) diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index e5ec23178d..9aa42930bf 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -11,8 +11,11 @@ import SimpleXChat struct UserProfile: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner @State private var profile = Profile(displayName: "", fullName: "") - @State private var editProfile = false + @State private var currentProfileHash: Int? + // Modals @State private var showChooseSource = false @State private var showImagePicker = false @State private var showTakePhoto = false @@ -21,85 +24,86 @@ struct UserProfile: View { @FocusState private var focusDisplayName var body: some View { - let user: User = chatModel.currentUser! - - return VStack(alignment: .leading) { - Text("Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile.") - .padding(.bottom) - - if editProfile { - ZStack(alignment: .center) { - ZStack(alignment: .topTrailing) { + List { + Group { + if profile.image != nil { + ZStack(alignment: .bottomTrailing) { + ZStack(alignment: .topTrailing) { + profileImageView(profile.image) + .onTapGesture { showChooseSource = true } + overlayButton("multiply", edge: .top) { profile.image = nil } + } + overlayButton("camera", edge: .bottom) { showChooseSource = true } + } + } else { + ZStack(alignment: .center) { profileImageView(profile.image) - if user.image != nil { - Button { - profile.image = nil - } label: { - Image(systemName: "multiply") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 12) - } + editImageButton { showChooseSource = true } + } + } + } + .frame(maxWidth: .infinity, alignment: .center) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .padding(.top) + .contentShape(Rectangle()) + + Section { + HStack { + TextField("Enter your name…", text: $profile.displayName) + .focused($focusDisplayName) + if !validDisplayName(profile.displayName) { + Button { + alert = .invalidNameError(validName: mkValidName(profile.displayName)) + } label: { + Image(systemName: "exclamationmark.circle").foregroundColor(.red) } } - - editImageButton { showChooseSource = true } } - .frame(maxWidth: .infinity, alignment: .center) - - VStack(alignment: .leading) { - ZStack(alignment: .leading) { - if !validNewProfileName(user) { - Button { - alert = .invalidNameError(validName: mkValidName(profile.displayName)) - } label: { - Image(systemName: "exclamationmark.circle").foregroundColor(.red) - } - } else { - Image(systemName: "exclamationmark.circle").foregroundColor(.clear) - } - profileNameTextEdit("Profile name", $profile.displayName) - .focused($focusDisplayName) - } - .padding(.bottom) - if showFullName(user) { - profileNameTextEdit("Full name (optional)", $profile.fullName) - .padding(.bottom) - } - HStack(spacing: 20) { - Button("Cancel") { editProfile = false } - Button("Save (and notify contacts)") { saveProfile() } - .disabled(!canSaveProfile(user)) - } + if let user = chatModel.currentUser, showFullName(user) { + TextField("Full name (optional)", text: $profile.fullName) } - .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) - } else { - ZStack(alignment: .center) { - profileImageView(user.image) - .onTapGesture { startEditingImage(user) } + } footer: { + Text("Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.") + } - if user.image == nil { - editImageButton { startEditingImage(user) } - } + Section { + Button(action: getCurrentProfile) { + Text("Reset") } - .frame(maxWidth: .infinity, alignment: .center) - - VStack(alignment: .leading) { - profileNameView("Profile name:", user.profile.displayName) - if showFullName(user) { - profileNameView("Full name:", user.profile.fullName) - } - Button("Edit") { - profile = fromLocalProfile(user.profile) - editProfile = true - focusDisplayName = true - } + .disabled(currentProfileHash == profile.hashValue) + Button(action: saveProfile) { + Text("Save (and notify contacts)") } - .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) + .disabled(!canSaveProfile) } } - .padding() - .frame(maxHeight: .infinity, alignment: .top) + // Lifecycle + .onAppear { + getCurrentProfile() + } + .onDisappear { + if canSaveProfile { + showAlert( + title: NSLocalizedString("Save your profile?", comment: "alert title"), + message: NSLocalizedString("Your profile was changed. If you save it, the updated profile will be sent to all your contacts.", comment: "alert message"), + buttonTitle: NSLocalizedString("Save (and notify contacts)", comment: "alert button"), + buttonAction: saveProfile, + cancelButton: true + ) + } + } + .onChange(of: chosenImage) { image in + Task { + let resized: String? = if let image { + await resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500) + } else { + nil + } + await MainActor.run { profile.image = resized } + } + } + // Modals .confirmationDialog("Profile image", isPresented: $showChooseSource, titleVisibility: .visible) { Button("Take picture") { showTakePhoto = true @@ -126,57 +130,48 @@ struct UserProfile: View { } } } - .onChange(of: chosenImage) { image in - if let image = image { - profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500) - } else { - profile.image = nil - } - } .alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) } } - func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding<String>) -> some View { - TextField(label, text: name) - .padding(.leading, 32) - } - - func profileNameView(_ label: LocalizedStringKey, _ name: String) -> some View { - HStack { - Text(label) - Text(name).fontWeight(.bold) - } - .padding(.bottom) - } - - func startEditingImage(_ user: User) { - profile = fromLocalProfile(user.profile) - editProfile = true - showChooseSource = true - } - - private func validNewProfileName(_ user: User) -> Bool { - profile.displayName == user.profile.displayName || validDisplayName(profile.displayName.trimmingCharacters(in: .whitespaces)) + private func overlayButton( + _ systemName: String, + edge: Edge.Set, + action: @escaping () -> Void + ) -> some View { + Image(systemName: systemName) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 12) + .foregroundColor(theme.colors.primary) + .padding(6) + .frame(width: 36, height: 36, alignment: .center) + .background(radius >= 20 ? Color.clear : theme.colors.background.opacity(0.5)) + .clipShape(Circle()) + .contentShape(Circle()) + .padding([.trailing, edge], -12) + .onTapGesture(perform: action) } private func showFullName(_ user: User) -> Bool { user.profile.fullName != "" && user.profile.fullName != user.profile.displayName } - - private func canSaveProfile(_ user: User) -> Bool { - profile.displayName.trimmingCharacters(in: .whitespaces) != "" && validNewProfileName(user) + + private var canSaveProfile: Bool { + currentProfileHash != profile.hashValue && + profile.displayName.trimmingCharacters(in: .whitespaces) != "" && + validDisplayName(profile.displayName) } - func saveProfile() { + private func saveProfile() { + focusDisplayName = false Task { do { profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces) if let (newProfile, _) = try await apiUpdateProfile(profile: profile) { - DispatchQueue.main.async { + await MainActor.run { chatModel.updateCurrentUser(newProfile) - profile = newProfile + getCurrentProfile() } - editProfile = false } else { alert = .duplicateUserError } @@ -185,12 +180,17 @@ struct UserProfile: View { } } } + + private func getCurrentProfile() { + if let user = chatModel.currentUser { + profile = fromLocalProfile(user.profile) + currentProfileHash = profile.hashValue + } + } } func profileImageView(_ imageStr: String?) -> some View { - ProfileImage(imageStr: imageStr) - .aspectRatio(1, contentMode: .fit) - .frame(maxWidth: 192, maxHeight: 192) + ProfileImage(imageStr: imageStr, size: 192) } func editImageButton(action: @escaping () -> Void) -> some View { @@ -203,19 +203,3 @@ func editImageButton(action: @escaping () -> Void) -> some View { .frame(width: 48) } } - -struct UserProfile_Previews: PreviewProvider { - static var previews: some View { - let chatModel1 = ChatModel() - chatModel1.currentUser = User.sampleData - let chatModel2 = ChatModel() - chatModel2.currentUser = User.sampleData - chatModel2.currentUser?.profile.image = "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBMRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAAqACAAQAAAABAAAAgKADAAQAAAABAAAAgAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/+ICNElDQ19QUk9GSUxFAAEBAAACJGFwcGwEAAAAbW50clJHQiBYWVogB+EABwAHAA0AFgAgYWNzcEFQUEwAAAAAQVBQTAAAAAAAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1hcHBsyhqVgiV/EE04mRPV0eoVggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKZGVzYwAAAPwAAABlY3BydAAAAWQAAAAjd3RwdAAAAYgAAAAUclhZWgAAAZwAAAAUZ1hZWgAAAbAAAAAUYlhZWgAAAcQAAAAUclRSQwAAAdgAAAAgY2hhZAAAAfgAAAAsYlRSQwAAAdgAAAAgZ1RSQwAAAdgAAAAgZGVzYwAAAAAAAAALRGlzcGxheSBQMwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB0ZXh0AAAAAENvcHlyaWdodCBBcHBsZSBJbmMuLCAyMDE3AABYWVogAAAAAAAA81EAAQAAAAEWzFhZWiAAAAAAAACD3wAAPb////+7WFlaIAAAAAAAAEq/AACxNwAACrlYWVogAAAAAAAAKDgAABELAADIuXBhcmEAAAAAAAMAAAACZmYAAPKnAAANWQAAE9AAAApbc2YzMgAAAAAAAQxCAAAF3v//8yYAAAeTAAD9kP//+6L///2jAAAD3AAAwG7/wAARCACAAIADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9sAQwABAQEBAQECAQECAwICAgMEAwMDAwQGBAQEBAQGBwYGBgYGBgcHBwcHBwcHCAgICAgICQkJCQkLCwsLCwsLCwsL/9sAQwECAgIDAwMFAwMFCwgGCAsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsL/90ABAAI/9oADAMBAAIRAxEAPwD4N1TV59SxpunRtBb/APPP/lo+eMsf4R+uKyxNa6Y32a3UTzjoi8Ip9/8AOfYV0tx4d1a8VlsojaWo6uThj+Pb6Cs2CCGyP2LQ4xPIMBpGIVVz7ngV+Ap31P2C1iSDQbnWXRtVYyMT8kSDkZ9B29zXXReD7ZVOkX0QlLgg2ycjBH8ZHXPoOK9O8L6LpljZidWMjyqMzAdc/wB3PJ+p4qjrPiuxs1a38LwLJIn35ScoP94jlm9hxW8ZKJm1fY/Gv4yeA/E37L3xf07xz4GuH0260+7i1bRLpDkwzQOHVfQ+WwAI7r1zmv7fv2Nv2nfCv7YH7PHh346+FwkD6nEYtRs1OTZ6jBhbiA98K/zJnrGynvX8u3x3+G6fFvwXcadcOZNTQebZyN1EgH3QB91W6H657VD/AMEYP2qdQ/Zb/aRuPgN8RpjZeFviJcJabJztWy1tPkgkOeFE3+ok9zGTwtfY5Nj1Vjyt6nzuZ4XlfMj+zamH5TupVYnhhgjsaRyMYNe8eEMC7jxxU+1SMYFQFyaevPWgRqaeuSVFb0SDgAZI/SsLS9w4kxux1HTNdTEAMDvQJst20UitvA4rotMh8ycbuAv6k1Rs3UgcHjrXc6Xb2iTKVIJPQEcZ96qKMW7nWabpNmzRyEE9wOlegtplzFCLiMbEcfKw5/XP51l6ZPK6b2SJsdd64A/Kr0t5fyRsqsPLU5baNo49P0q2I//Q8iuPD17eeTpVy32u2ufls5lAC5P8MmOA2O/Q/XIrHl+GWn+CGN7qyC9ugxkSID92nvz1+pwK/TKb9j34t3Pw/PjXXrpdR165L3F7pkiDz5RISzHzFIUzliXKBQCTgMGwD8P6zompRzR2V2xuLWV9sE7ggo4yPLlBxhgRgE8k8cHivyPPMl9g3iMMrw6r+X/gH6PlmZ+1tRrP3uj7/wDBPnjXdR1rXWDao5jtm4S3h43gf3jwSPyH1rW0Xw9f6uyw2MYSNAAT/Ag/qa9ii+GTWEv2nV8nfztH3m/+t/nirMsVtMPscGIYYuCqjj8fWvmo+9qz227aI5O38NeH/DeJIGE079ZW9fQf/W/Ovyx/ba+C1x/aR+K/h6FoLa5dUvDH8rRzj7kgI+7ux253DPev1yuINKtF3XriOMDlm+83+6O1eNePZoPH2h3ngWC032N7E0UhI7HuPcdQfWvQweJdKakjkxFFTjZn6+f8Eu/2yE/a+/Zss9R8TXCyeMvCpTSfECZ+eSZF/dXWPS5jG4n/AJ6Bx2r9JGbd0r+GX9jD476z/wAE5v20IL3xPM7eGdUZdK8QBeUewmYGO6A7tbviT127171/cfaXdve28d1aSJNFKqukiHcjqwyGUjggggg9xX6Dhq6q01JM+NxVF05tdCyRQCOvakY4GRTFYd66DmN2xk2sK6eE5+YVxlo5EwB4rrLZiTyePWgmSOmsAThCcZPFdxZ5KruJyprgrWQ5G3tXS21+FABzVrYyZ6ZZTTSqCR8vQ4rUudWgW1e3QMrBScj1/D+tcpp1+UXaOn09fWtKP7OAzNjK+tNiP//R/oYjkSW9NgqsWVA7HHyrk4AJ9Tzx6CvjL9qz4M+FrbRrn4q2s0Fjcs6R3ttKdsd+ZCFBUf8APx0xj/WAYOCA1fVF58Y/hbb/AAwPxlXWIH8OCHzhdKc57bAv3vM3fLsxu3cYzX58eGdH8f8A7b/xIHi/xOs2k+DNGkK28AOCgPVQejXMg++/IiU7RyefmI+Z79+qPl++0JpR/wATG7Z9M4WOQfeVv7srdT/snp+NeWa9bfZXez8KxCZQcGVhiJT/AOzH6fnX7K/Fn9mfwzf6N9r+GmnwWV3DF5UlmBiC8iAxtbPAkx0c/e6N/eH5s+IvDcuj2jWcUTJYwsYXDrtktHXgxuvBxngE9Oh9/is6yVUr4nDL3Oq7enl+R9Plmac9qNZ+90ff/gnybLoheT7XrM3nMo5JH8h2HtXJa9/aGoMYbAC0gTqwH7x1H8hXsHiWGDRUboqr/Eeck+nrXj9/d3twWmlzbQHnn77e/tXzaqXXuntuNtz4z/ay+Eul+NPAf9u+H4TLq2kqzEAfNLAeXU/T7w/Ed6/XL/giD+2n/wALr+Ck37Nnjq78zxV8PYkW0Z2+a60VjthbJ5LWzfuW/wBjyz3NfCGuJLLm30tSsT8OT/U1+b1v4w8VfsE/tXeHf2kfhqjz2Vvcl5rdDiO4tZflu7Q+zoSUz0baeq19RkWMUZexk/Q8LNMLzx51uf3yIxPXvTQuTkVw3wz+IfhH4seBNG+JngS7W+0XX7OG/sp1P34ZlDLn0Izhh2YEGu+LAHFfXo+XJ4P9cp6YNdbCWHFcerFSCK6OGcMBk0wOmtZMVswurDNcnHKB7VqxXbDGKaZEoncRXpt4iy8fWlN44XdM5+bGPauWbUAI9p5NeH/E39oTwF8OAdO1W6+06kfuWVuQ0vtvOcIPdiPalOrGC5pOyHToym7RV2f/0nXmiaPrF/ceJvC1hrUnhC11EyFGZsIN2Mtg+QLjy+A5GQcZI6V/QP8ABrWvhd4i+GmnXXwZeI6DAnkxRxgq0LL95JFb5hJnO7dyTz3qt4f8EeCPC3g5Pht4csYItKt4fKNngMpjfOd4PJLckk8k18FeKvBXj79kHxu/xW+ECte+F711XUtNdiVC54VvQj/lnL2+63FfNNqWh7rVtT9JdItdaitpV8QSxyy+a5VowVURE/KDnuB1PQ9a/OD4yfEbwv8AEP4rx6F8JNIfXb4QyQXMlqAwvmQgEBThSkQBUysQpyFBOBjE+NH7WWu/HtrH4QfACxvYpNZHl3bSr5M7kjLQqc/JGo5ml/u8DrX2X+z38A9C+B3hzyQUvNbvVX7dehcA7ekUQ/hiT+Fe/U81m1bVj1Px/wDiX4FXQ4b7WNItJXitXZLq3nU+fpzjqpQ87PQ88eowa+JdanuvP+03JzG3Kk87voP8a/pi+NPwStfiAo8V+GDHaeI7aPYsjj91dxj/AJYzjuOyv1X6V+Mfxk+By6eL7xPodhLE9kzDUNJYfvbSXqWUd4z147cjivjc3ybkviMMtOq7eaPo8tzXmtRrvXo/8z4aaC/1a3drrbDbr6nCgepPc+36V4T8Z/A/h7xz4KvPB8uGmcb4LhhxHKv3WUeh6HPY17TrMuo3dysUA3p0VUGEArCudFt7aH7bqjguOQP6V89SquLUk9T26lNNWZ7L/wAEJv2vNQ8L6xq/7BPxZma3ureafUPDHnHvy93Zg/X9/EO+XA7Cv6fFwRnNfwWftIWHi/wL4u0T9pX4Vu2ma74buobpJY+GEkDBo5CO4B+Vx3U4PFf2VfshftPeFf2tv2e/Dvx18LbYhq0G29tQcm0vovluID/uPkr6oVPev0TLsWq9FT69T43MMN7KpdbM+q1kA+WtuF8qCa5H7SD0qvrnjbw34L0KTxD4qvobCyhBLzTuFUY7DPU+wya7nNJXZwxu3ZHoqyqq5JxXnPxL+Nvw3+EemjUPHmqxWIbPlxcvNIR2WNcsfrjFflz8cf8AgpDJMZ/DvwKgwOVOq3S/rFGf0LV8MaZp/jf4j603ibxTdT3U053PdXRLu+eflB7fkK8PFZ5TheNHV/h/wT2cLlFSfvVNF+J+hnxI/ba8cfEa5fQfhnG+h6e5KCY/NeTD6jIjH0yfcV514W8HX2plrjUiWLEtIWbcSSOS7dST/k1x2g2PhrwdZhpyFbHzEnLk+5/oK6eDxRq2soYdPH2S0xjjh2H9K+erY+pVlzTdz3aWEhSjaCsf/9P+gafwFajxovjGKeVJSqrJEPuOVUoD7ZBGR32ivgn9pz9pHUfGOvP+zb8BIDrGr6kZLO/nhwUXH34UY/LwP9bJ91BxndxXyp41/ab/AGivht4c1D9mf+0La7vrOY6f/asUpe4WP7vlRzEhRnIHmMNyAkcEcfpB+zB+zBo37O/hQ3moBL3xLfxA312gyFA5EEOeRGp79Xb5j2x8wfQHyHZ/CP41fsg6lZ/GHT3tvEVvDC0WqxwIU8uGUqXXnnaCoIlHQj5vlOR+lPwv+Lngv4v+Gk8UeC7oTRBvLnib5ZYJcZKSL1B9D0YcgkU/QfEkXitbuzuLR7S5tGCTwS4bAfO3kcEEA5B/lg1+Yn7Qdtbfsd/E/TPiT8IdShs21jzDc6HIf3TRIQWyB0hYnCE8xt9044Ckr7k7H7AiUEf4V438U/hZa+O0TXNGkWy120XbDcEfJKn/ADxmA+8h7Hqp5HpWN8Efjv4N+OvhFfFHhOTy5otqXlnIR51tKRnaw7g9VccMOnOQPXZ71Yo2mdgiqMsWOAAOufasXoyrXPw++NX7P9zHdX174Q0wWOqW/wC81DSjjMe7J86HHDxtgnC5zzjkEV+Z3iOS20u7PlZupiT+9YYQH/ZWv6hvjRp3grXPAJ8c3t6lldabGZLC/j5be3KxY/jSUgAp+IwRkfzs/tYan4Vi+LM8nhzyo5bq2gnu4Iukd04PmDI6ZGGIHc18hnmW06K+s09LvVefkfRZTjZ1H7Cetlo/8z5d1bQk1m1ng1OMTRXCGOVX+7tbg5+tQf8ABPL9o/xV/wAE9vi/r3gDxhYahrPw18WSrMJbGMzvZXcYwkyxjn5k/dyr1OFI6VqBpJ8LdPiM9gOv0FWFTzJBFbJtzgADliT0H515uAzKphpNxV0z0sVhIVo8sj9rviP/AMFJPhxpuhJ/wqm2n1rUbhcqbmJreKLP95T8zEeg/GvzP8Y/Eb4vftA+Ije+Kb2XUWU/JCDstoAewH3Rj8TXmOi+HrJYTd63MII1OPLB+d8diev4DtXtWjeIrPTNNENtD9mjx8kY+V2H0/hH60YzNK2IdpPTsthYXL6VHWK17s2/C3gHQvDCLqPiKRZ7hei/wKfYdz7mu9/4TGa5lEGjREA8Z7/5+lec2Ntf65KLm+IjhXkZ4UCunt9X0zTONN56gu39K4k2dtlueh6Xpdxcz/a9UfMi84J4X+grv7fxNaaehi0oCWUDDSH7o+leNW99f30fls3l2+eT0z61oDVFgiEOngtgY3Y/kP61pEln/9T74+Ff/BPn4e6R8MnsPieWvfFF+haS+gkbbZM3RIQeHA/jLjMhznAwBufCz42+Mf2bPEsHwM/aNlMmiONmj6+cmIRg4Cuxz+7GQMn5oicNlcGvWf2ffiB418d/Dfwn4tvR9st9StTb3IVVUxSw8NK7E5O4qRgeo46msH9tXx78JfAfwS1CL4oQx30l8ki6XZ5Ama7VTtkQ9UWPIMjdNvynO4A/NHvnqP7Rn7Q/gX9nLwY3iXVGiudR1BS2n2aOA102PvkjpEowWfpjgcmviz9nH9njxT8afFEn7SX7TkJvJL8+bp+mXSfIUP3JJIyPljUf6qI9vmPOK+DfgboFl4V+LfhHxt+1DpWoW/he7iL6bJfRt9mLpgwOwbOYIyd23sSrFdvX+iZ7n7bY+fpkqHzU3RSj50IYZVuDhh34PIqG7bBufnr8Zv2fvF3wa8Vf8L8/ZgQ20sAJ1DR4lLRPF1fbGPvRHGWjHKn5kxjFe8fDD9qX4Q/FL4cXni/V7uHS2sIv+JpYXLgyQE/3RwZEc8Rso+bpwcive/E/irQPBOgXfizxTeJYafp8ZmnnkOFRR+pJPAA5J4GTX8uP7Uf7R3hHWPilqfjDwNpo02HVZ8wWqL84jAAaVlHAeUguVHAY/Unnq1oU6bnVdkuv6GtOlKclCmtWfQn7X37bl7qEqaB4HRbaCyXytOssgiBTgedL281hzg9Onrn8xl1eNpJNQ1C4M00zGSSV23M7HqST1Oa5K7Np44uf7Psmkubp3M0hCjcG9ZGzjn1r3fwR8LrDRokvNaIlmABw3IU/l1/yBXwWZY+eJnzS0itl/XU+tweEjh4WW73ZmaHpev8AiNhJCjW9vjh2+8w9hXqVnpukeGoFe4cqVIJdjyT2/X86W+8U2ljG1rpCiRxxu6jNeO+IrbX9amEzuwERy3rz9eB/M15jdztSPQhr7ahrEt/b/Ky8bXHIz0bn1HPP4CvW/CsEUKNqOqybQ3zZb77n2z/OvnvS2khv4r5wZLiLAUADbx6jvjtmvWNGinvbn7TqjlyRnGcjNNR0DmPTZtYuNSxb2KlY+w7fX3rd063toHDTAzSj+H/H0+lYulwz3Moislx2yOD+n9KzvF3xX8C/DCIwXbi+1NvuWsJzhj/fPRRxVRRV7ntNlp91eRm61F1hgUZOTtVawtT+JGiaQDYeF4hf3J+Uyn/VqT6dya+GNb+M3j74i339n3rx2ttG2PItwwT2yxALH6ce9e3eGLXyLFcofN24wf6nsPYU9gP/1fof9kb9uf4LeBf2QYLjxVctDrujNcIdJAImuJHkYoIiRjaejFsbMHI6Zf8As+/BTxt+1l4/X9qT9pSPdpW4NoukOCIpI0OYyUPS3Q8qDzK3zNkdfkv/AIJ4/s0ah+0xZWv7Q3xmjik8PCZvstqgwuoSQnYC3cwJtwSeZmBz8uc/vtp3iPQrm+k0LT50M9oMNCo27QuFIXgAheAdudp4ODXzeyPfbIviJ4C8I/FLwnceCPHFmLvTrkdOjxOPuyRt/A69iPocgkV+dehfEbxr+wf4ot/hz8W5ZtZ+Hd+7DS9VRCz2h67CvoM/PFnK/eTK5FfpHrviHR/DejXXiDxBdRWNhYxNPcXEzBI4o0GWZieAAK/mw/bP/bF1n9pvxTH4a8DxvD4X0mZjYRSAo88pBQ3Uw6jKkiOP+FSc/MxxhUqQpwc6jtFFU6cqk1GCu2W/26f269Y+Nutnwv4KElv4cs5M2ds/ytcOOPtE2O/9xP4R7kmvz00L4e614kvTqniKR087qf429h/dH616Zofg/S/D+dW16Xz7k/MXbr9AO3+ea2W1q8v/AN1pqeTE3AYj5iPb/P4V8DmWZzxU9NILZfq/M+uwWCjh495dWa2jWPh7wZaC10+FFfsqD5ifUnrn3/WpbibUtVI+0Psj/uA449z/AErPjtrTTI/tepybc8kE5Ymse78UXV0fL0hPIjHG89fw9K8u3c7W7Grd38WjOEt0Blx95v4c+i/41iW5ur+VmvHIG7IHTmqscK2ymaY5dhnLck/Qf41sWlqyqZp3EWevrRZCu2bdgoUiCIYOeT3zXp2hrp+nRfb9VmWCFerP1PsB3NeNz+K9O0eApYr58q/xN0B9f/1VzZ1q/wBQv/td07Mw6lvT2HRR+pockhpHp3jv4q6pdwnR/CObKBxgyf8ALZx7dxXz5p+i6tPqryW8WXYHLSgso7/Oe59s16Np9rNdXTG0Uh24Z++Pr2H5n6V6LZ22k+HoFudVcBs/LHjv7L1J9z+lRzGyiM8IeCI7fZfXKguFUGRjkcDnaD/WvQrrxNYaQo0rSYzLMR25wfUn/P0rift2ueJG2RB7S3PRV/1jD3PRRj/9ddh4b0C1iJKAY/MZPv8AxH9KhS1Lt3P/1v0M/YPkRP2ZNBhiARY3uVCqMAAStwAOwr6budO8L6Fe3PjW/dbUQRySzTSSlII12jzJGBIRTtQbnwOBya+Lf+CevizRdf8A2VNH1vS7lJbQT3hMmcBQshJ3Z+7t75xivy7/AG6/27G+OWpy/CP4WXTL4OgfE9wmQ2qyIeG7H7MrfcU48w4Y8bRXy9ScYRc5uyW59BGEpT5YrUs/tq/tm6r+0x4gPw3+G9xJa+CdPmDM/KNqMiHiVxwfKB5ijPX77c4C/GVlc2eip9h0SLz5z94noD/tH/J9hXJaTZXUkGxT5MA5YZxnPdm9/QV1j3WmeHoFkuPk4+Vf4mHsP4R7n8q+DzTMpYufLHSC2/zZ9XgcFHDxu/iZaj0i6uZDqGtThtvJzwoqrdeJY7RzbaYuSRw7Dt7f5xXE6h4kvNamG/5YgcqmcLj1Pc/X8qtLAwQGPDyPzk9B/n0ryuXsdzkW5LyS4k8+/kLsx4X/AB/wFdFYxXVwyxW6gMe55Ix6Cm6Z4et7JTqevzCJj1Zu/wBBUepeNba3t2svDcflL/FPJyT9BSsuormlcPYaJGHuGM0zcjJrk7vUbvUZwJD8vO1Rwo/Dv+Ncvda3AP3s7FpHOSzHLE+w7Utm+q6uTFZDyo8/Mx6/WomWkb+baDDTPlj0ReSPqRnFdBpukXeptv2iK3Xl3Y4RQPU1mWkFhpOQF+0XAwCO+TnAJ6L9OvtViJNV8RShdTcC2j5ESfLEvufU/Xn0rNstRPQI9QtwgsfCyiYr/wAvLjEQP+yv8X1P610mj+H0WcXWpO1xeMOWbl8fyQU3RbbMSiyG1EH+sbjgf3R2+tdbamytrc3KnbErANM3OWPOAP4iR0qGzdGotg2xbNBktjKJk/p1P48fSuziOn6DBtuj5twekYP3Sf7xH8q8/ttbvriUw6eGgSTv/wAtZB65/hH0P49qll1PS9FJF0RLP2jU5xn1qLiP/9k=" - return Group { - UserProfile() - .environmentObject(chatModel1) - UserProfile() - .environmentObject(chatModel2) - } - } -} diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift index f6c7bf37e8..887023b670 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift @@ -8,6 +8,7 @@ import SimpleXChat struct UserProfilesView: View { @EnvironmentObject private var m: ChatModel + @EnvironmentObject private var theme: AppTheme @Environment(\.editMode) private var editMode @AppStorage(DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE) private var showHiddenProfilesNotice = true @AppStorage(DEFAULT_SHOW_MUTE_PROFILE_ALERT) private var showMuteProfileAlert = true @@ -20,21 +21,20 @@ struct UserProfilesView: View { @State private var profileHidden = false @State private var profileAction: UserProfileAction? @State private var actionPassword = "" + @State private var navigateToProfileCreate = false var trimmedSearchTextOrPassword: String { searchTextOrPassword.trimmingCharacters(in: .whitespaces)} private enum UserProfilesAlert: Identifiable { case deleteUser(user: User, delSMPQueues: Bool) - case cantDeleteLastUser case hiddenProfilesNotice case muteProfileAlert case activateUserError(error: String) - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { case let .deleteUser(user, delSMPQueues): return "deleteUser \(user.userId) \(delSMPQueues)" - case .cantDeleteLastUser: return "cantDeleteLastUser" case .hiddenProfilesNotice: return "hiddenProfilesNotice" case .muteProfileAlert: return "muteProfileAlert" case let .activateUserError(err): return "activateUserError \(err)" @@ -56,17 +56,6 @@ struct UserProfilesView: View { } var body: some View { - if authorized { - userProfilesView() - } else { - Button(action: runAuth) { Label("Unlock", systemImage: "lock") } - .onAppear(perform: runAuth) - } - } - - private func runAuth() { authorize(NSLocalizedString("Open user profiles", comment: "authentication reason"), $authorized) } - - private func userProfilesView() -> some View { List { if profileHidden { Button { @@ -78,12 +67,14 @@ struct UserProfilesView: View { Section { let users = filteredUsers() let v = ForEach(users) { u in - userView(u.user, allowDelete: users.count > 1) + userView(u) } if #available(iOS 16, *) { v.onDelete { indexSet in if let i = indexSet.first { - confirmDeleteUser(users[i].user) + withAuth { + confirmDeleteUser(users[i].user) + } } } } else { @@ -91,16 +82,26 @@ struct UserProfilesView: View { } if trimmedSearchTextOrPassword == "" { - NavigationLink { - CreateProfile() - } label: { + NavigationLink( + destination: CreateProfile(), + isActive: $navigateToProfileCreate + ) { Label("Add profile", systemImage: "plus") + .frame(maxWidth: .infinity, alignment: .leading) + .frame(height: 38) + .padding(.leading, 16).padding(.vertical, 8).padding(.trailing, 32) + .contentShape(Rectangle()) + .onTapGesture { + withAuth { + self.navigateToProfileCreate = true + } + } + .padding(.leading, -16).padding(.vertical, -8).padding(.trailing, -32) } - .frame(height: 44) - .padding(.vertical, 4) } } footer: { Text("Tap to activate profile.") + .foregroundColor(theme.colors.secondary) .font(.body) .padding(.top, 8) @@ -112,6 +113,7 @@ struct UserProfilesView: View { } } .navigationTitle("Your chat profiles") + .modifier(ThemedBackground(grouped: true)) .searchable(text: $searchTextOrPassword, placement: .navigationBarDrawer(displayMode: .always)) .autocorrectionDisabled(true) .textInputAutocapitalization(.never) @@ -124,7 +126,7 @@ struct UserProfilesView: View { deleteModeButton("Profile and server connections", true) deleteModeButton("Local profile data only", false) } - .sheet(item: $selectedUser) { user in + .appSheet(item: $selectedUser) { user in HiddenProfileView(user: user, profileHidden: $profileHidden) } .onChange(of: profileHidden) { _ in @@ -132,7 +134,7 @@ struct UserProfilesView: View { withAnimation { profileHidden = false } } } - .sheet(item: $profileAction) { action in + .appSheet(item: $profileAction) { action in profileActionView(action) } .alert(item: $alert) { alert in @@ -146,13 +148,6 @@ struct UserProfilesView: View { }, secondaryButton: .cancel() ) - case .cantDeleteLastUser: - return Alert( - title: Text("Can't delete user profile!"), - message: m.users.count > 1 - ? Text("There should be at least one visible user profile.") - : Text("There should be at least one user profile.") - ) case .hiddenProfilesNotice: return Alert( title: Text("Make profile private!"), @@ -177,7 +172,7 @@ struct UserProfilesView: View { message: Text(err) ) case let .error(title, error): - return Alert(title: Text(title), message: Text(error)) + return mkAlert(title: title, message: error) } } } @@ -196,7 +191,25 @@ struct UserProfilesView: View { private var visibleUsersCount: Int { m.users.filter({ u in !u.user.hidden }).count } - + + private func withAuth(_ action: @escaping () -> Void) { + if authorized { + action() + } else { + authenticate( + reason: NSLocalizedString("Change chat profiles", comment: "authentication reason") + ) { laResult in + switch laResult { + case .success, .unavailable: + authorized = true + AppSheetState.shared.scenePhaseActive = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: action) + case .failed: authorized = false + } + } + } + } + private func correctPassword(_ user: User, _ pwd: String) -> Bool { if let ph = user.viewPwdHash { return pwd != "" && chatPasswordHash(pwd, ph.salt) == ph.hash @@ -208,26 +221,29 @@ struct UserProfilesView: View { !user.hidden ? nil : trimmedSearchTextOrPassword } - @ViewBuilder private func profileActionView(_ action: UserProfileAction) -> some View { + private func profileActionView(_ action: UserProfileAction) -> some View { let passwordValid = actionPassword == actionPassword.trimmingCharacters(in: .whitespaces) let passwordField = PassphraseField(key: $actionPassword, placeholder: "Profile password", valid: passwordValid) let actionEnabled: (User) -> Bool = { user in actionPassword != "" && passwordValid && correctPassword(user, actionPassword) } - List { + return List { switch action { case let .deleteUser(user, delSMPQueues): actionHeader("Delete profile", user) Section { passwordField - settingsRow("trash") { + settingsRow("trash", color: theme.colors.secondary) { Button("Delete chat profile", role: .destructive) { - profileAction = nil - Task { await removeUser(user, delSMPQueues, viewPwd: actionPassword) } + withAuth { + profileAction = nil + Task { await removeUser(user, delSMPQueues, viewPwd: actionPassword) } + } } .disabled(!actionEnabled(user)) } } footer: { if actionEnabled(user) { Text("All chats and messages will be deleted - this cannot be undone!") + .foregroundColor(theme.colors.secondary) .font(.callout) } } @@ -235,16 +251,19 @@ struct UserProfilesView: View { actionHeader("Unhide profile", user) Section { passwordField - settingsRow("lock.open") { + settingsRow("lock.open", color: theme.colors.secondary) { Button("Unhide chat profile") { - profileAction = nil - setUserPrivacy(user) { try await apiUnhideUser(user.userId, viewPwd: actionPassword) } + withAuth{ + profileAction = nil + setUserPrivacy(user) { try await apiUnhideUser(user.userId, viewPwd: actionPassword) } + } } .disabled(!actionEnabled(user)) } } } } + .modifier(ThemedBackground()) } @ViewBuilder func actionHeader(_ title: LocalizedStringKey, _ user: User) -> some View { @@ -260,11 +279,13 @@ struct UserProfilesView: View { private func deleteModeButton(_ title: LocalizedStringKey, _ delSMPQueues: Bool) -> some View { Button(title, role: .destructive) { - if let user = userToDelete { - if passwordEntryRequired(user) { - profileAction = .deleteUser(user: user, delSMPQueues: delSMPQueues) - } else { - alert = .deleteUser(user: user, delSMPQueues: delSMPQueues) + withAuth { + if let user = userToDelete { + if passwordEntryRequired(user) { + profileAction = .deleteUser(user: user, delSMPQueues: delSMPQueues) + } else { + alert = .deleteUser(user: user, delSMPQueues: delSMPQueues) + } } } } @@ -277,108 +298,130 @@ struct UserProfilesView: View { private func removeUser(_ user: User, _ delSMPQueues: Bool, viewPwd: String?) async { do { if user.activeUser { + ChatModel.shared.removeWallpaperFilesFromAllChats(user) if let newActive = m.users.first(where: { u in !u.user.activeUser && !u.user.hidden }) { try await changeActiveUserAsync_(newActive.user.userId, viewPwd: nil) try await deleteUser() + } else { + // Deleting the last visible user while having hidden one(s) + try await deleteUser() + try await changeActiveUserAsync_(nil, viewPwd: nil) + try? await stopChatAsync() + await MainActor.run { + onboardingStageDefault.set(.step1_SimpleXInfo) + m.onboardingStage = .step1_SimpleXInfo + dismissAllSheets() + } } } else { try await deleteUser() } } catch let error { + logger.error("Error deleting user profile: \(error)") let a = getErrorAlert(error, "Error deleting user profile") alert = .error(title: a.title, error: a.message) } func deleteUser() async throws { try await apiDeleteUser(user.userId, delSMPQueues, viewPwd: viewPwd) + removeWallpaperFilesFromTheme(user.uiThemes) await MainActor.run { withAnimation { m.removeUser(user) } } } } - @ViewBuilder private func userView(_ user: User, allowDelete: Bool) -> some View { + @ViewBuilder private func userView(_ userInfo: UserInfo) -> some View { + let user = userInfo.user let v = Button { Task { do { try await changeActiveUserAsync_(user.userId, viewPwd: userViewPassword(user)) + dismissAllSheets() } catch { await MainActor.run { alert = .activateUserError(error: responseError(error)) } } } } label: { HStack { - ProfileImage(imageStr: user.image, color: Color(uiColor: .tertiarySystemFill)) - .frame(width: 44, height: 44) - .padding(.vertical, 4) + ProfileImage(imageStr: user.image, size: 38) .padding(.trailing, 12) Text(user.chatViewName) Spacer() if user.activeUser { - Image(systemName: "checkmark").foregroundColor(.primary) - } else if user.hidden { - Image(systemName: "lock").foregroundColor(.secondary) - } else if !user.showNtfs { - Image(systemName: "speaker.slash").foregroundColor(.secondary) + Image(systemName: "checkmark").foregroundColor(theme.colors.onBackground) } else { - Image(systemName: "checkmark").foregroundColor(.clear) + if userInfo.unreadCount > 0 { + UnreadBadge(userInfo: userInfo) + } + if user.hidden { + Image(systemName: "lock").foregroundColor(theme.colors.secondary) + } else if userInfo.unreadCount == 0 { + if !user.showNtfs { + Image(systemName: "speaker.slash").foregroundColor(theme.colors.secondary) + } else { + Image(systemName: "checkmark").foregroundColor(.clear) + } + } } } } - .disabled(user.activeUser) - .foregroundColor(.primary) - .deleteDisabled(!allowDelete) + .foregroundColor(theme.colors.onBackground) .swipeActions(edge: .leading, allowsFullSwipe: true) { if user.hidden { Button("Unhide") { - if passwordEntryRequired(user) { - profileAction = .unhideUser(user: user) - } else { - setUserPrivacy(user) { try await apiUnhideUser(user.userId, viewPwd: trimmedSearchTextOrPassword) } + withAuth { + if passwordEntryRequired(user) { + profileAction = .unhideUser(user: user) + } else { + setUserPrivacy(user) { try await apiUnhideUser(user.userId, viewPwd: trimmedSearchTextOrPassword) } + } } } .tint(.green) } else { if visibleUsersCount > 1 { Button("Hide") { - selectedUser = user + withAuth { + selectedUser = user + } } .tint(.gray) } Group { if user.showNtfs { Button("Mute") { - setUserPrivacy(user, successAlert: showMuteProfileAlert ? .muteProfileAlert : nil) { - try await apiMuteUser(user.userId) + withAuth { + setUserPrivacy(user, successAlert: showMuteProfileAlert ? .muteProfileAlert : nil) { + try await apiMuteUser(user.userId) + } } } } else { Button("Unmute") { - setUserPrivacy(user) { try await apiUnmuteUser(user.userId) } + withAuth { + setUserPrivacy(user) { try await apiUnmuteUser(user.userId) } + } } } } - .tint(.accentColor) + .tint(theme.colors.primary) } } if #available(iOS 16, *) { v - } else if !allowDelete { - v } else { v.swipeActions(edge: .trailing, allowsFullSwipe: true) { Button("Delete", role: .destructive) { - confirmDeleteUser(user) + withAuth { + confirmDeleteUser(user) + } } } } } private func confirmDeleteUser(_ user: User) { - if m.users.count > 1 && (user.hidden || visibleUsersCount > 1) { - showDeleteConfirmation = true - userToDelete = user - } else { - alert = .cantDeleteLastUser - } + showDeleteConfirmation = true + userToDelete = user } private func setUserPrivacy(_ user: User, successAlert: UserProfilesAlert? = nil, _ api: @escaping () async throws -> User) { @@ -407,6 +450,13 @@ public func chatPasswordHash(_ pwd: String, _ salt: String) -> String { return hash } +public func correctPassword(_ user: User, _ pwd: String) -> Bool { + if let ph = user.viewPwdHash { + return pwd != "" && chatPasswordHash(pwd, ph.salt) == ph.hash + } + return false +} + struct UserProfilesView_Previews: PreviewProvider { static var previews: some View { UserProfilesView() diff --git a/apps/ios/SimpleX (iOS).entitlements b/apps/ios/SimpleX (iOS).entitlements index c78a7cb941..2ec32def0a 100644 --- a/apps/ios/SimpleX (iOS).entitlements +++ b/apps/ios/SimpleX (iOS).entitlements @@ -9,6 +9,10 @@ <string>applinks:simplex.chat</string> <string>applinks:www.simplex.chat</string> <string>applinks:simplex.chat?mode=developer</string> + <string>applinks:*.simplex.im</string> + <string>applinks:*.simplex.im?mode=developer</string> + <string>applinks:*.simplexonflux.com</string> + <string>applinks:*.simplexonflux.com?mode=developer</string> </array> <key>com.apple.security.application-groups</key> <array> diff --git a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff index e0477899be..e965e5a1a5 100644 --- a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff +++ b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff @@ -39,7 +39,7 @@ </trans-unit> <trans-unit id="!1 colored!" xml:space="preserve" approved="no"> <source>!1 colored!</source> - <target state="translated">! 1 ملون!</target> + <target state="translated">! 1 مُلوَّن!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="#secret#" xml:space="preserve" approved="no"> @@ -49,7 +49,7 @@ </trans-unit> <trans-unit id="%@" xml:space="preserve" approved="no"> <source>%@</source> - <target state="needs-translation">%@</target> + <target state="translated">%@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ %@" xml:space="preserve" approved="no"> @@ -69,7 +69,7 @@ </trans-unit> <trans-unit id="%@ is not verified" xml:space="preserve" approved="no"> <source>%@ is not verified</source> - <target state="translated">%@ لم يتم التحقق منها</target> + <target state="translated">%@ لم يتم التحقق منه</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ is verified" xml:space="preserve" approved="no"> @@ -107,8 +107,9 @@ <target state="translated">%d ثانية</target> <note>message ttl</note> </trans-unit> - <trans-unit id="%d skipped message(s)" xml:space="preserve"> + <trans-unit id="%d skipped message(s)" xml:space="preserve" approved="no"> <source>%d skipped message(s)</source> + <target state="translated">%d الرسائل المتخطية</target> <note>integrity error chat item</note> </trans-unit> <trans-unit id="%lld" xml:space="preserve" approved="no"> @@ -121,12 +122,14 @@ <target state="needs-translation">%lld %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld contact(s) selected" xml:space="preserve"> + <trans-unit id="%lld contact(s) selected" xml:space="preserve" approved="no"> <source>%lld contact(s) selected</source> + <target state="translated">%lld تم اختيار جهات الاتصال</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve"> + <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve" approved="no"> <source>%lld file(s) with total size of %@</source> + <target state="translated">%lld الملفات ذات الحجم الإجمالي %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld members" xml:space="preserve" approved="no"> @@ -134,8 +137,9 @@ <target state="translated">%lld أعضاء</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> + <trans-unit id="%lld second(s)" xml:space="preserve" approved="no"> <source>%lld second(s)</source> + <target state="translated">%lld ثوانى</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lldd" xml:space="preserve" approved="no"> @@ -183,24 +187,19 @@ <target state="needs-translation">)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target state="translated">** إضافة جهة اتصال جديدة **: لإنشاء رمز QR لمرة واحدة أو رابط جهة الاتصال الخاصة بك.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no"> <source>**Create link / QR code** for your contact to use.</source> <target state="translated">** أنشئ رابطًا / رمز QR ** لتستخدمه جهة الاتصال الخاصة بك.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve" approved="no"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> - <target state="translated">** المزيد من الخصوصية **: تحقق من الرسائل الجديدة كل 20 دقيقة. تتم مشاركة رمز الجهاز مع خادم SimpleX Chat ، ولكن ليس عدد جهات الاتصال أو الرسائل لديك.</target> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> + <target state="translated">** المزيد من الخصوصية **: تحققوا من الرسائل الجديدة كل 20 دقيقة. تتم مشاركة رمز الجهاز مع خادم SimpleX Chat ، ولكن ليس عدد جهات الاتصال أو الرسائل لديكم.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve" approved="no"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> - <target state="translated">** الأكثر خصوصية **: لا تستخدم خادم إشعارات SimpleX Chat ، وتحقق من الرسائل بشكل دوري في الخلفية (يعتمد على عدد مرات استخدامك للتطبيق).</target> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> + <target state="translated">** الأكثر خصوصية **: لا تستخدم خادم إشعارات SimpleX Chat ، وتحقق من الرسائل بشكل دوري في الخلفية (يعتمد على عدد مرات استخدامكم للتطبيق).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve" approved="no"> @@ -210,11 +209,11 @@ </trans-unit> <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve" approved="no"> <source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source> - <target state="translated">** يرجى ملاحظة **: لن تتمكن من استعادة أو تغيير عبارة المرور إذا فقدتها.</target> + <target state="translated">** يرجى ملاحظة **: لن تتمكنوا من استعادة أو تغيير عبارة المرور إذا فقدتموها.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve" approved="no"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> <target state="translated">** موصى به **: يتم إرسال رمز الجهاز والإشعارات إلى خادم إشعارات SimpleX Chat ، ولكن ليس محتوى الرسالة أو حجمها أو مصدرها.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -305,7 +304,7 @@ </trans-unit> <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve" approved="no"> <source>A separate TCP connection will be used **for each chat profile you have in the app**.</source> - <target state="translated">سيتم استخدام اتصال TCP منفصل ** لكل ملف تعريف دردشة لديك في التطبيق **.</target> + <target state="translated">سيتم استخدام اتصال TCP منفصل ** لكل ملف تعريف دردشة لديكم في التطبيق **.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." xml:space="preserve" approved="no"> @@ -355,312 +354,381 @@ <source>Accept requests</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> + <trans-unit id="Add preset servers" xml:space="preserve" approved="no"> <source>Add preset servers</source> + <target state="translated">إضافة خوادم محددة مسبقا</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add profile" xml:space="preserve"> + <trans-unit id="Add profile" xml:space="preserve" approved="no"> <source>Add profile</source> + <target state="translated">إضافة الملف الشخصي</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> + <trans-unit id="Add servers by scanning QR codes." xml:space="preserve" approved="no"> <source>Add servers by scanning QR codes.</source> + <target state="translated">إضافة خوادم عن طريق مسح رموز QR.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> + <trans-unit id="Add server" xml:space="preserve" approved="no"> + <source>Add server</source> + <target state="translated">أضف الخادم</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add to another device" xml:space="preserve"> + <trans-unit id="Add to another device" xml:space="preserve" approved="no"> <source>Add to another device</source> + <target state="translated">أضف إلى جهاز آخر</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> + <trans-unit id="Admins can create the links to join groups." xml:space="preserve" approved="no"> <source>Admins can create the links to join groups.</source> + <target state="translated">يمكن للمُدراء إنشاء روابط للانضمام إلى المجموعات.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Advanced network settings" xml:space="preserve"> + <trans-unit id="Advanced network settings" xml:space="preserve" approved="no"> <source>Advanced network settings</source> + <target state="translated">إعدادات الشبكة المتقدمة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve"> + <trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve" approved="no"> <source>All chats and messages will be deleted - this cannot be undone!</source> + <target state="translated">سيتم حذف جميع الدردشات والرسائل - لا يمكن التراجع عن هذا!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All group members will remain connected." xml:space="preserve"> + <trans-unit id="All group members will remain connected." xml:space="preserve" approved="no"> <source>All group members will remain connected.</source> + <target state="translated">سيبقى جميع أعضاء المجموعة على اتصال.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> + <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve" approved="no"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> + <target state="translated">سيتم حذف جميع الرسائل - لا يمكن التراجع عن هذا! سيتم حذف الرسائل فقط من أجلك.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All your contacts will remain connected" xml:space="preserve"> <source>All your contacts will remain connected</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow" xml:space="preserve"> + <trans-unit id="Allow" xml:space="preserve" approved="no"> <source>Allow</source> + <target state="translated">سماح</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> + <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve" approved="no"> <source>Allow disappearing messages only if your contact allows it to you.</source> + <target state="translated">السماح بالرسائل المختفية فقط إذا سمحت لك جهة الاتصال بذلك.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> <source>Allow irreversible message deletion only if your contact allows it to you.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow sending direct messages to members." xml:space="preserve"> + <trans-unit id="Allow sending direct messages to members." xml:space="preserve" approved="no"> <source>Allow sending direct messages to members.</source> + <target state="translated">السماح بإرسال رسائل مباشرة إلى الأعضاء.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow sending disappearing messages." xml:space="preserve"> + <trans-unit id="Allow sending disappearing messages." xml:space="preserve" approved="no"> <source>Allow sending disappearing messages.</source> + <target state="translated">السماح بإرسال الرسائل التي تختفي.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> <source>Allow to irreversibly delete sent messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to send voice messages." xml:space="preserve"> + <trans-unit id="Allow to send voice messages." xml:space="preserve" approved="no"> <source>Allow to send voice messages.</source> + <target state="translated">السماح بإرسال رسائل صوتية.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve"> + <trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve" approved="no"> <source>Allow voice messages only if your contact allows them.</source> + <target state="translated">اسمح بالرسائل الصوتية فقط إذا سمحت جهة اتصالك بذلك.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow voice messages?" xml:space="preserve"> + <trans-unit id="Allow voice messages?" xml:space="preserve" approved="no"> <source>Allow voice messages?</source> + <target state="translated">السماح بالرسائل الصوتية؟</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> <source>Allow your contacts to irreversibly delete sent messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> + <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve" approved="no"> <source>Allow your contacts to send disappearing messages.</source> + <target state="translated">السماح لجهات اتصالك بإرسال رسائل تختفي.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to send voice messages." xml:space="preserve"> + <trans-unit id="Allow your contacts to send voice messages." xml:space="preserve" approved="no"> <source>Allow your contacts to send voice messages.</source> + <target state="translated">اسمح لجهات اتصالك بإرسال رسائل صوتية.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Already connected?" xml:space="preserve"> + <trans-unit id="Already connected?" xml:space="preserve" approved="no"> <source>Already connected?</source> + <target state="translated">متصل بالفعل؟</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Answer call" xml:space="preserve"> + <trans-unit id="Answer call" xml:space="preserve" approved="no"> <source>Answer call</source> + <target state="translated">أجب الاتصال</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App build: %@" xml:space="preserve"> + <trans-unit id="App build: %@" xml:space="preserve" approved="no"> <source>App build: %@</source> + <target state="translated">إصدار التطبيق: %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App icon" xml:space="preserve"> + <trans-unit id="App icon" xml:space="preserve" approved="no"> <source>App icon</source> + <target state="translated">رمز التطبيق</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App version" xml:space="preserve"> + <trans-unit id="App version" xml:space="preserve" approved="no"> <source>App version</source> + <target state="translated">نسخة التطبيق</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App version: v%@" xml:space="preserve"> + <trans-unit id="App version: v%@" xml:space="preserve" approved="no"> <source>App version: v%@</source> + <target state="translated">نسخة التطبيق: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Appearance" xml:space="preserve"> + <trans-unit id="Appearance" xml:space="preserve" approved="no"> <source>Appearance</source> + <target state="translated">المظهر</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Attach" xml:space="preserve"> + <trans-unit id="Attach" xml:space="preserve" approved="no"> <source>Attach</source> + <target state="translated">إرفاق</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Audio & video calls" xml:space="preserve"> + <trans-unit id="Audio & video calls" xml:space="preserve" approved="no"> <source>Audio & video calls</source> + <target state="translated">مكالمات الصوت والفيديو</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Authentication failed" xml:space="preserve"> + <trans-unit id="Authentication failed" xml:space="preserve" approved="no"> <source>Authentication failed</source> + <target state="translated">فشلت المصادقة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Authentication unavailable" xml:space="preserve"> + <trans-unit id="Authentication unavailable" xml:space="preserve" approved="no"> <source>Authentication unavailable</source> + <target state="translated">المصادقة غير متاحة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Auto-accept contact requests" xml:space="preserve"> + <trans-unit id="Auto-accept contact requests" xml:space="preserve" approved="no"> <source>Auto-accept contact requests</source> + <target state="translated">قبول طلبات الاتصال تلقائيًا</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Auto-accept images" xml:space="preserve"> + <trans-unit id="Auto-accept images" xml:space="preserve" approved="no"> <source>Auto-accept images</source> + <target state="translated">قبول تلقائي للصور</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Automatically" xml:space="preserve"> <source>Automatically</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Back" xml:space="preserve"> + <trans-unit id="Back" xml:space="preserve" approved="no"> <source>Back</source> + <target state="translated">رجوع</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> <source>Both you and your contact can irreversibly delete sent messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can send disappearing messages." xml:space="preserve"> + <trans-unit id="Both you and your contact can send disappearing messages." xml:space="preserve" approved="no"> <source>Both you and your contact can send disappearing messages.</source> + <target state="translated">يمكنك أنت وجهة اتصالك إرسال رسائل تختفي.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can send voice messages." xml:space="preserve"> + <trans-unit id="Both you and your contact can send voice messages." xml:space="preserve" approved="no"> <source>Both you and your contact can send voice messages.</source> + <target state="translated">يمكنك أنت وجهة اتصالك إرسال رسائل صوتية.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> + <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve" approved="no"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> + <target state="translated">حسب ملف تعريف الدردشة (افتراضي) أو [حسب الاتصال] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Call already ended!" xml:space="preserve"> + <trans-unit id="Call already ended!" xml:space="preserve" approved="no"> <source>Call already ended!</source> + <target state="translated">انتهت المكالمة بالفعل!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Calls" xml:space="preserve"> + <trans-unit id="Calls" xml:space="preserve" approved="no"> <source>Calls</source> + <target state="translated">المكالمات</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't invite contact!" xml:space="preserve"> + <trans-unit id="Can't invite contact!" xml:space="preserve" approved="no"> <source>Can't invite contact!</source> + <target state="translated">لا يمكن دعوة جهة اتصال!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't invite contacts!" xml:space="preserve"> + <trans-unit id="Can't invite contacts!" xml:space="preserve" approved="no"> <source>Can't invite contacts!</source> + <target state="translated">لا يمكن دعوة جهات الاتصال!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Cancel" xml:space="preserve"> + <trans-unit id="Cancel" xml:space="preserve" approved="no"> <source>Cancel</source> + <target state="translated">إلغاء</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve" approved="no"> <source>Cannot access keychain to save database password</source> + <target state="translated">لا يمكن الوصول إلى سلسلة المفاتيح لحفظ كلمة مرور قاعدة البيانات</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Cannot receive file" xml:space="preserve"> + <trans-unit id="Cannot receive file" xml:space="preserve" approved="no"> <source>Cannot receive file</source> + <target state="translated">لا يمكن استلام الملف</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change" xml:space="preserve"> + <trans-unit id="Change" xml:space="preserve" approved="no"> <source>Change</source> + <target state="translated">تغير</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change database passphrase?" xml:space="preserve"> + <trans-unit id="Change database passphrase?" xml:space="preserve" approved="no"> <source>Change database passphrase?</source> + <target state="translated">تغيير عبارة مرور قاعدة البيانات؟</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change member role?" xml:space="preserve"> + <trans-unit id="Change member role?" xml:space="preserve" approved="no"> <source>Change member role?</source> + <target state="translated">تغيير دور العضو؟</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change receiving address" xml:space="preserve"> + <trans-unit id="Change receiving address" xml:space="preserve" approved="no"> <source>Change receiving address</source> + <target state="translated">تغيير عنوان الاستلام</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change receiving address?" xml:space="preserve"> + <trans-unit id="Change receiving address?" xml:space="preserve" approved="no"> <source>Change receiving address?</source> + <target state="translated">تغيير عنوان الاستلام؟</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change role" xml:space="preserve"> + <trans-unit id="Change role" xml:space="preserve" approved="no"> <source>Change role</source> + <target state="translated">تغيير الدور</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat archive" xml:space="preserve"> <source>Chat archive</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat console" xml:space="preserve"> + <trans-unit id="Chat console" xml:space="preserve" approved="no"> <source>Chat console</source> + <target state="translated">وحدة تحكم الدردشة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat database" xml:space="preserve"> + <trans-unit id="Chat database" xml:space="preserve" approved="no"> <source>Chat database</source> + <target state="translated">قاعدة بيانات الدردشة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat database deleted" xml:space="preserve"> + <trans-unit id="Chat database deleted" xml:space="preserve" approved="no"> <source>Chat database deleted</source> + <target state="translated">حُذفت قاعدة بيانات الدردشة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat database imported" xml:space="preserve"> + <trans-unit id="Chat database imported" xml:space="preserve" approved="no"> <source>Chat database imported</source> + <target state="translated">استُوردت قاعدة بيانات الدردشة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat is running" xml:space="preserve"> + <trans-unit id="Chat is running" xml:space="preserve" approved="no"> <source>Chat is running</source> + <target state="translated">الدردشة قيد التشغيل</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat is stopped" xml:space="preserve"> + <trans-unit id="Chat is stopped" xml:space="preserve" approved="no"> <source>Chat is stopped</source> + <target state="translated">توقفت الدردشة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat preferences" xml:space="preserve"> + <trans-unit id="Chat preferences" xml:space="preserve" approved="no"> <source>Chat preferences</source> + <target state="translated">تفضيلات الدردشة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chats" xml:space="preserve"> + <trans-unit id="Chats" xml:space="preserve" approved="no"> <source>Chats</source> + <target state="translated">الدردشات</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Check server address and try again." xml:space="preserve"> + <trans-unit id="Check server address and try again." xml:space="preserve" approved="no"> <source>Check server address and try again.</source> + <target state="translated">تحقق من عنوان الخادم وحاول مرة أخرى.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Choose file" xml:space="preserve"> + <trans-unit id="Choose file" xml:space="preserve" approved="no"> <source>Choose file</source> + <target state="translated">اختر الملف</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Choose from library" xml:space="preserve"> + <trans-unit id="Choose from library" xml:space="preserve" approved="no"> <source>Choose from library</source> + <target state="translated">اختر من المكتبة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Clear" xml:space="preserve"> + <trans-unit id="Clear" xml:space="preserve" approved="no"> <source>Clear</source> + <target state="translated">مسح</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Clear conversation" xml:space="preserve"> + <trans-unit id="Clear conversation" xml:space="preserve" approved="no"> <source>Clear conversation</source> + <target state="translated">مسح الدردشة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Clear conversation?" xml:space="preserve"> + <trans-unit id="Clear conversation?" xml:space="preserve" approved="no"> <source>Clear conversation?</source> + <target state="translated">مسح الدردشة؟</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Clear verification" xml:space="preserve"> + <trans-unit id="Clear verification" xml:space="preserve" approved="no"> <source>Clear verification</source> + <target state="translated">امسح التحقُّق</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Colors" xml:space="preserve"> <source>Colors</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Compare security codes with your contacts." xml:space="preserve"> + <trans-unit id="Compare security codes with your contacts." xml:space="preserve" approved="no"> <source>Compare security codes with your contacts.</source> + <target state="translated">قارن رموز الأمان مع جهات اتصالك.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Configure ICE servers" xml:space="preserve"> + <trans-unit id="Configure ICE servers" xml:space="preserve" approved="no"> <source>Configure ICE servers</source> + <target state="translated">ضبط خوادم ICE</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Confirm" xml:space="preserve"> + <trans-unit id="Confirm" xml:space="preserve" approved="no"> <source>Confirm</source> + <target state="translated">تأكيد</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Confirm new passphrase…" xml:space="preserve"> + <trans-unit id="Confirm new passphrase…" xml:space="preserve" approved="no"> <source>Confirm new passphrase…</source> + <target state="translated">تأكيد عبارة المرور الجديدة…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect" xml:space="preserve"> + <trans-unit id="Connect" xml:space="preserve" approved="no"> <source>Connect</source> + <target state="translated">اتصل</target> <note>server test step</note> </trans-unit> <trans-unit id="Connect via contact link?" xml:space="preserve"> @@ -671,8 +739,9 @@ <source>Connect via group link?</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link" xml:space="preserve"> + <trans-unit id="Connect via link" xml:space="preserve" approved="no"> <source>Connect via link</source> + <target state="translated">تواصل عبر الرابط</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via link / QR code" xml:space="preserve"> @@ -687,224 +756,273 @@ <source>Connect via relay</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connecting server…" xml:space="preserve"> + <trans-unit id="Connecting server…" xml:space="preserve" approved="no"> <source>Connecting to server…</source> + <target state="translated">جارِ الاتصال بالخادم…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connecting server… (error: %@)" xml:space="preserve"> + <trans-unit id="Connecting server… (error: %@)" xml:space="preserve" approved="no"> <source>Connecting to server… (error: %@)</source> + <target state="translated">الاتصال بالخادم... (الخطأ: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection" xml:space="preserve"> + <trans-unit id="Connection" xml:space="preserve" approved="no"> <source>Connection</source> + <target state="translated">الاتصال</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection error" xml:space="preserve"> + <trans-unit id="Connection error" xml:space="preserve" approved="no"> <source>Connection error</source> + <target state="translated">خطأ في الإتصال</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection error (AUTH)" xml:space="preserve"> + <trans-unit id="Connection error (AUTH)" xml:space="preserve" approved="no"> <source>Connection error (AUTH)</source> + <target state="translated">خطأ في الإتصال (المصادقة)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection request" xml:space="preserve"> <source>Connection request</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection request sent!" xml:space="preserve"> + <trans-unit id="Connection request sent!" xml:space="preserve" approved="no"> <source>Connection request sent!</source> + <target state="translated">أرسلت طلب الاتصال!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection timeout" xml:space="preserve"> + <trans-unit id="Connection timeout" xml:space="preserve" approved="no"> <source>Connection timeout</source> + <target state="translated">انتهت مهلة الاتصال</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contact allows" xml:space="preserve"> + <trans-unit id="Contact allows" xml:space="preserve" approved="no"> <source>Contact allows</source> + <target state="translated">تسمح جهة الاتصال</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contact already exists" xml:space="preserve"> + <trans-unit id="Contact already exists" xml:space="preserve" approved="no"> <source>Contact already exists</source> + <target state="translated">جهة الاتصال موجودة بالفعل</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact and all messages will be deleted - this cannot be undone!" xml:space="preserve"> <source>Contact and all messages will be deleted - this cannot be undone!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contact hidden:" xml:space="preserve"> + <trans-unit id="Contact hidden:" xml:space="preserve" approved="no"> <source>Contact hidden:</source> + <target state="translated">جهة الاتصال مخفية:</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is connected" xml:space="preserve"> + <trans-unit id="Contact is connected" xml:space="preserve" approved="no"> <source>Contact is connected</source> + <target state="translated">تم الاتصال</target> <note>notification</note> </trans-unit> <trans-unit id="Contact is not connected yet!" xml:space="preserve"> <source>Contact is not connected yet!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contact name" xml:space="preserve"> + <trans-unit id="Contact name" xml:space="preserve" approved="no"> <source>Contact name</source> + <target state="translated">اسم جهة الاتصال</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contact preferences" xml:space="preserve"> + <trans-unit id="Contact preferences" xml:space="preserve" approved="no"> <source>Contact preferences</source> + <target state="translated">تفضيلات جهة الاتصال</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact requests" xml:space="preserve"> <source>Contact requests</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve"> + <trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve" approved="no"> <source>Contacts can mark messages for deletion; you will be able to view them.</source> + <target state="translated">يمكن لجهات الاتصال تحديد الرسائل لحذفها؛ ستتمكن من مشاهدتها.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Copy" xml:space="preserve"> + <trans-unit id="Copy" xml:space="preserve" approved="no"> <source>Copy</source> + <target state="translated">نسخ</target> <note>chat item action</note> </trans-unit> <trans-unit id="Core built at: %@" xml:space="preserve"> <source>Core built at: %@</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Core version: v%@" xml:space="preserve"> + <trans-unit id="Core version: v%@" xml:space="preserve" approved="no"> <source>Core version: v%@</source> + <target state="translated">الإصدار الأساسي: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create" xml:space="preserve"> + <trans-unit id="Create" xml:space="preserve" approved="no"> <source>Create</source> + <target state="translated">إنشاء</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create address" xml:space="preserve"> <source>Create address</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create group link" xml:space="preserve"> + <trans-unit id="Create group link" xml:space="preserve" approved="no"> <source>Create group link</source> + <target state="translated">إنشاء رابط المجموعة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create link" xml:space="preserve"> + <trans-unit id="Create link" xml:space="preserve" approved="no"> <source>Create link</source> + <target state="translated">إنشاء رابط</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create one-time invitation link" xml:space="preserve"> <source>Create one-time invitation link</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create queue" xml:space="preserve"> + <trans-unit id="Create queue" xml:space="preserve" approved="no"> <source>Create queue</source> + <target state="translated">إنشاء قائمة انتظار</target> <note>server test step</note> </trans-unit> - <trans-unit id="Create secret group" xml:space="preserve"> + <trans-unit id="Create secret group" xml:space="preserve" approved="no"> <source>Create secret group</source> + <target state="translated">إنشاء مجموعة سرية</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create your profile" xml:space="preserve"> + <trans-unit id="Create your profile" xml:space="preserve" approved="no"> <source>Create your profile</source> + <target state="translated">أنشئ ملف تعريفك</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Created on %@" xml:space="preserve"> <source>Created on %@</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Current passphrase…" xml:space="preserve"> + <trans-unit id="Current passphrase…" xml:space="preserve" approved="no"> <source>Current passphrase…</source> + <target state="translated">عبارة المرور الحالية…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve" approved="no"> <source>Currently maximum supported file size is %@.</source> + <target state="translated">الحد الأقصى لحجم الملف المدعوم حاليًا هو %@.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Dark" xml:space="preserve"> + <trans-unit id="Dark" xml:space="preserve" approved="no"> <source>Dark</source> + <target state="translated">داكن</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database ID" xml:space="preserve"> + <trans-unit id="Database ID" xml:space="preserve" approved="no"> <source>Database ID</source> + <target state="translated">معرّف قاعدة البيانات</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database encrypted!" xml:space="preserve"> + <trans-unit id="Database encrypted!" xml:space="preserve" approved="no"> <source>Database encrypted!</source> + <target state="translated">قاعدة البيانات مُعمّاة!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database encryption passphrase will be updated and stored in the keychain. " xml:space="preserve"> + <trans-unit id="Database encryption passphrase will be updated and stored in the keychain. " xml:space="preserve" approved="no"> <source>Database encryption passphrase will be updated and stored in the keychain. </source> + <target state="translated">سيتم تحديث عبارة المرور الخاصة بتشفير قاعدة البيانات وتخزينها في سلسلة المفاتيح. +</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database encryption passphrase will be updated. " xml:space="preserve"> + <trans-unit id="Database encryption passphrase will be updated. " xml:space="preserve" approved="no"> <source>Database encryption passphrase will be updated. </source> + <target state="translated">سيتم تحديث عبارة مرور تعمية قاعدة البيانات. +</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database error" xml:space="preserve"> + <trans-unit id="Database error" xml:space="preserve" approved="no"> <source>Database error</source> + <target state="translated">خطأ في قاعدة البيانات</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database is encrypted using a random passphrase, you can change it." xml:space="preserve"> + <trans-unit id="Database is encrypted using a random passphrase, you can change it." xml:space="preserve" approved="no"> <source>Database is encrypted using a random passphrase, you can change it.</source> + <target state="translated">قاعدة البيانات مُعمّاة باستخدام عبارة مرور عشوائية، يمكنك تغييرها.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database is encrypted using a random passphrase. Please change it before exporting." xml:space="preserve"> + <trans-unit id="Database is encrypted using a random passphrase. Please change it before exporting." xml:space="preserve" approved="no"> <source>Database is encrypted using a random passphrase. Please change it before exporting.</source> + <target state="translated">قاعدة البيانات مُعمّاة باستخدام عبارة مرور عشوائية. يُرجى تغييره قبل التصدير.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database passphrase" xml:space="preserve"> + <trans-unit id="Database passphrase" xml:space="preserve" approved="no"> <source>Database passphrase</source> + <target state="translated">عبارة مرور قاعدة البيانات</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database passphrase & export" xml:space="preserve"> + <trans-unit id="Database passphrase & export" xml:space="preserve" approved="no"> <source>Database passphrase & export</source> + <target state="translated">عبارة مرور قاعدة البيانات وتصديرها</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve" approved="no"> <source>Database passphrase is different from saved in the keychain.</source> + <target state="translated">عبارة المرور الخاصة بقاعدة البيانات مختلفة عن تلك المحفوظة في سلسلة المفاتيح.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve" approved="no"> <source>Database passphrase is required to open chat.</source> + <target state="translated">عبارة مرور قاعدة البيانات مطلوبة لفتح الدردشة.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database will be encrypted and the passphrase stored in the keychain. " xml:space="preserve"> + <trans-unit id="Database will be encrypted and the passphrase stored in the keychain. " xml:space="preserve" approved="no"> <source>Database will be encrypted and the passphrase stored in the keychain. </source> + <target state="translated">سيتم تشفير قاعدة البيانات وتخزين عبارة المرور في سلسلة المفاتيح. +</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database will be encrypted. " xml:space="preserve"> + <trans-unit id="Database will be encrypted. " xml:space="preserve" approved="no"> <source>Database will be encrypted. </source> + <target state="translated">سيتم تعمية قاعدة البيانات. +</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database will be migrated when the app restarts" xml:space="preserve"> + <trans-unit id="Database will be migrated when the app restarts" xml:space="preserve" approved="no"> <source>Database will be migrated when the app restarts</source> + <target state="translated">سيتم نقل قاعدة البيانات عند إعادة تشغيل التطبيق</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Decentralized" xml:space="preserve"> + <trans-unit id="Decentralized" xml:space="preserve" approved="no"> <source>Decentralized</source> + <target state="translated">لامركزي</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete" xml:space="preserve"> + <trans-unit id="Delete" xml:space="preserve" approved="no"> <source>Delete</source> + <target state="translated">حذف</target> <note>chat item action</note> </trans-unit> <trans-unit id="Delete Contact" xml:space="preserve"> <source>Delete Contact</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete address" xml:space="preserve"> + <trans-unit id="Delete address" xml:space="preserve" approved="no"> <source>Delete address</source> + <target state="translated">حذف العنوان</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete address?" xml:space="preserve"> + <trans-unit id="Delete address?" xml:space="preserve" approved="no"> <source>Delete address?</source> + <target state="translated">حذف العنوان؟</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete after" xml:space="preserve"> + <trans-unit id="Delete after" xml:space="preserve" approved="no"> <source>Delete after</source> + <target state="translated">حذف بعد</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete all files" xml:space="preserve"> + <trans-unit id="Delete all files" xml:space="preserve" approved="no"> <source>Delete all files</source> + <target state="translated">حذف جميع الملفات</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete archive" xml:space="preserve"> @@ -915,152 +1033,188 @@ <source>Delete chat archive?</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat profile?" xml:space="preserve"> + <trans-unit id="Delete chat profile?" xml:space="preserve" approved="no"> <source>Delete chat profile?</source> + <target state="translated">حذف ملف تعريف الدردشة؟</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete connection" xml:space="preserve"> + <trans-unit id="Delete connection" xml:space="preserve" approved="no"> <source>Delete connection</source> + <target state="translated">حذف الاتصال</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact" xml:space="preserve"> + <trans-unit id="Delete contact" xml:space="preserve" approved="no"> <source>Delete contact</source> + <target state="translated">حذف جهة الاتصال</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact?" xml:space="preserve"> + <trans-unit id="Delete contact?" xml:space="preserve" approved="no"> <source>Delete contact?</source> + <target state="translated">حذف جهة الاتصال؟</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete database" xml:space="preserve"> + <trans-unit id="Delete database" xml:space="preserve" approved="no"> <source>Delete database</source> + <target state="translated">حذف قاعدة البيانات</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete files and media?" xml:space="preserve"> + <trans-unit id="Delete files and media?" xml:space="preserve" approved="no"> <source>Delete files and media?</source> + <target state="translated">حذف الملفات والوسائط؟</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete files for all chat profiles" xml:space="preserve"> + <trans-unit id="Delete files for all chat profiles" xml:space="preserve" approved="no"> <source>Delete files for all chat profiles</source> + <target state="translated">حذف الملفات لجميع ملفات تعريف الدردشة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete for everyone" xml:space="preserve"> + <trans-unit id="Delete for everyone" xml:space="preserve" approved="no"> <source>Delete for everyone</source> + <target state="translated">حذف للجميع</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Delete for me" xml:space="preserve"> + <trans-unit id="Delete for me" xml:space="preserve" approved="no"> <source>Delete for me</source> + <target state="translated">حذف بالنسبة لي</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete group" xml:space="preserve"> + <trans-unit id="Delete group" xml:space="preserve" approved="no"> <source>Delete group</source> + <target state="translated">حذف المجموعة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete group?" xml:space="preserve"> + <trans-unit id="Delete group?" xml:space="preserve" approved="no"> <source>Delete group?</source> + <target state="translated">حذف المجموعة؟</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete invitation" xml:space="preserve"> + <trans-unit id="Delete invitation" xml:space="preserve" approved="no"> <source>Delete invitation</source> + <target state="translated">حذف الدعوة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete link" xml:space="preserve"> + <trans-unit id="Delete link" xml:space="preserve" approved="no"> <source>Delete link</source> + <target state="translated">حذف الرابط</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete link?" xml:space="preserve"> + <trans-unit id="Delete link?" xml:space="preserve" approved="no"> <source>Delete link?</source> + <target state="translated">حذف الرابط؟</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete message?" xml:space="preserve"> + <trans-unit id="Delete message?" xml:space="preserve" approved="no"> <source>Delete message?</source> + <target state="translated">حذف الرسالة؟</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete messages" xml:space="preserve"> + <trans-unit id="Delete messages" xml:space="preserve" approved="no"> <source>Delete messages</source> + <target state="translated">حذف الرسائل</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete messages after" xml:space="preserve"> + <trans-unit id="Delete messages after" xml:space="preserve" approved="no"> <source>Delete messages after</source> + <target state="translated">حذف الرسائل بعد</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete old database" xml:space="preserve"> + <trans-unit id="Delete old database" xml:space="preserve" approved="no"> <source>Delete old database</source> + <target state="translated">حذف قاعدة البيانات القديمة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete old database?" xml:space="preserve"> + <trans-unit id="Delete old database?" xml:space="preserve" approved="no"> <source>Delete old database?</source> + <target state="translated">حذف قاعدة البيانات القديمة؟</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection" xml:space="preserve"> <source>Delete pending connection</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection?" xml:space="preserve"> + <trans-unit id="Delete pending connection?" xml:space="preserve" approved="no"> <source>Delete pending connection?</source> + <target state="translated">حذف الاتصال قيد الانتظار؟</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete queue" xml:space="preserve"> + <trans-unit id="Delete queue" xml:space="preserve" approved="no"> <source>Delete queue</source> + <target state="translated">حذف قائمة الانتظار</target> <note>server test step</note> </trans-unit> - <trans-unit id="Delete user profile?" xml:space="preserve"> + <trans-unit id="Delete user profile?" xml:space="preserve" approved="no"> <source>Delete user profile?</source> + <target state="translated">حذف ملف تعريف المستخدم؟</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Description" xml:space="preserve"> + <trans-unit id="Description" xml:space="preserve" approved="no"> <source>Description</source> + <target state="translated">الوصف</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Develop" xml:space="preserve"> + <trans-unit id="Develop" xml:space="preserve" approved="no"> <source>Develop</source> + <target state="translated">يطور</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Developer tools" xml:space="preserve"> + <trans-unit id="Developer tools" xml:space="preserve" approved="no"> <source>Developer tools</source> + <target state="translated">أدوات المطور</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Device" xml:space="preserve"> + <trans-unit id="Device" xml:space="preserve" approved="no"> <source>Device</source> + <target state="translated">الجهاز</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Device authentication is disabled. Turning off SimpleX Lock." xml:space="preserve"> + <trans-unit id="Device authentication is disabled. Turning off SimpleX Lock." xml:space="preserve" approved="no"> <source>Device authentication is disabled. Turning off SimpleX Lock.</source> + <target state="translated">استيثاق الجهاز مُعطَّل. جارِ إيقاف تشغيل قفل SimpleX.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." xml:space="preserve"> + <trans-unit id="Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." xml:space="preserve" approved="no"> <source>Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication.</source> + <target state="translated">مصادقة الجهاز غير مفعّلة. يمكنك تشغيل قفل SimpleX عبر الإعدادات، بمجرد تفعيل مصادقة الجهاز.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Different names, avatars and transport isolation." xml:space="preserve"> + <trans-unit id="Different names, avatars and transport isolation." xml:space="preserve" approved="no"> <source>Different names, avatars and transport isolation.</source> + <target state="translated">أسماء مختلفة، صور الأفاتار وعزل النقل.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Direct messages" xml:space="preserve"> + <trans-unit id="Direct messages" xml:space="preserve" approved="no"> <source>Direct messages</source> + <target state="translated">رسائل مباشرة</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve" approved="no"> + <source>Direct messages between members are prohibited.</source> + <target state="translated">الرسائل المباشرة بين الأعضاء ممنوعة.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> + <trans-unit id="Disable SimpleX Lock" xml:space="preserve" approved="no"> <source>Disable SimpleX Lock</source> + <target state="translated">تعطيل قفل SimpleX</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Disappearing messages" xml:space="preserve"> + <trans-unit id="Disappearing messages" xml:space="preserve" approved="no"> <source>Disappearing messages</source> + <target state="translated">الرسائل المختفية</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this chat." xml:space="preserve"> + <trans-unit id="Disappearing messages are prohibited in this chat." xml:space="preserve" approved="no"> <source>Disappearing messages are prohibited in this chat.</source> + <target state="translated">يُحظر اختفاء الرسائل في هذه الدردشة.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve" approved="no"> + <source>Disappearing messages are prohibited.</source> + <target state="translated">الرسائل المختفية ممنوعة.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disconnect" xml:space="preserve"> + <trans-unit id="Disconnect" xml:space="preserve" approved="no"> <source>Disconnect</source> + <target state="translated">قطع الاتصال</target> <note>server test step</note> </trans-unit> <trans-unit id="Display name" xml:space="preserve"> @@ -1071,124 +1225,153 @@ <source>Display name:</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> + <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve" approved="no"> <source>Do NOT use SimpleX for emergency calls.</source> + <target state="translated">لا تستخدم SimpleX لإجراء مكالمات الطوارئ.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Do it later" xml:space="preserve"> + <trans-unit id="Do it later" xml:space="preserve" approved="no"> <source>Do it later</source> + <target state="translated">افعل ذلك لاحقا</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Duplicate display name!" xml:space="preserve"> + <trans-unit id="Duplicate display name!" xml:space="preserve" approved="no"> <source>Duplicate display name!</source> + <target state="translated">اسم العرض مكرر!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Edit" xml:space="preserve"> + <trans-unit id="Edit" xml:space="preserve" approved="no"> <source>Edit</source> + <target state="translated">تحرير</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Edit group profile" xml:space="preserve"> + <trans-unit id="Edit group profile" xml:space="preserve" approved="no"> <source>Edit group profile</source> + <target state="translated">حرّر ملف تعريف المجموعة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enable" xml:space="preserve"> + <trans-unit id="Enable" xml:space="preserve" approved="no"> <source>Enable</source> + <target state="translated">تفعيل</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> + <trans-unit id="Enable SimpleX Lock" xml:space="preserve" approved="no"> <source>Enable SimpleX Lock</source> + <target state="translated">تفعيل قفل SimpleX</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Enable TCP keep-alive" xml:space="preserve"> + <trans-unit id="Enable TCP keep-alive" xml:space="preserve" approved="no"> <source>Enable TCP keep-alive</source> + <target state="translated">تفعيل أبقِ TCP على قيد الحياة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> + <trans-unit id="Enable automatic message deletion?" xml:space="preserve" approved="no"> <source>Enable automatic message deletion?</source> + <target state="translated">تفعيل الحذف التلقائي للرسائل؟</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enable instant notifications?" xml:space="preserve"> + <trans-unit id="Enable instant notifications?" xml:space="preserve" approved="no"> <source>Enable instant notifications?</source> + <target state="translated">تفعيل الإشعارات فورية؟</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enable notifications" xml:space="preserve"> + <trans-unit id="Enable notifications" xml:space="preserve" approved="no"> <source>Enable notifications</source> + <target state="translated">تفعيل الإشعارات</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enable periodic notifications?" xml:space="preserve"> + <trans-unit id="Enable periodic notifications?" xml:space="preserve" approved="no"> <source>Enable periodic notifications?</source> + <target state="translated">تفعيل الإشعارات دورية؟</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Encrypt" xml:space="preserve"> + <trans-unit id="Encrypt" xml:space="preserve" approved="no"> <source>Encrypt</source> + <target state="translated">التشفير</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Encrypt database?" xml:space="preserve"> + <trans-unit id="Encrypt database?" xml:space="preserve" approved="no"> <source>Encrypt database?</source> + <target state="translated">تشفير قاعدة البيانات؟</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Encrypted database" xml:space="preserve"> + <trans-unit id="Encrypted database" xml:space="preserve" approved="no"> <source>Encrypted database</source> + <target state="translated">قاعدة بيانات مشفرة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Encrypted message or another event" xml:space="preserve"> + <trans-unit id="Encrypted message or another event" xml:space="preserve" approved="no"> <source>Encrypted message or another event</source> + <target state="translated">رسالة مشفرة أو حدث آخر</target> <note>notification</note> </trans-unit> - <trans-unit id="Encrypted message: database error" xml:space="preserve"> + <trans-unit id="Encrypted message: database error" xml:space="preserve" approved="no"> <source>Encrypted message: database error</source> + <target state="translated">رسالة مشفرة: خطأ في قاعدة البيانات</target> <note>notification</note> </trans-unit> - <trans-unit id="Encrypted message: keychain error" xml:space="preserve"> + <trans-unit id="Encrypted message: keychain error" xml:space="preserve" approved="no"> <source>Encrypted message: keychain error</source> + <target state="translated">رسالة مشفرة: خطأ في سلسلة المفاتيح</target> <note>notification</note> </trans-unit> - <trans-unit id="Encrypted message: no passphrase" xml:space="preserve"> + <trans-unit id="Encrypted message: no passphrase" xml:space="preserve" approved="no"> <source>Encrypted message: no passphrase</source> + <target state="translated">الرسالة المشفرة: لا توجد عبارة مرور</target> <note>notification</note> </trans-unit> - <trans-unit id="Encrypted message: unexpected error" xml:space="preserve"> + <trans-unit id="Encrypted message: unexpected error" xml:space="preserve" approved="no"> <source>Encrypted message: unexpected error</source> + <target state="translated">رسالة مشفرة: خطأ غير متوقع</target> <note>notification</note> </trans-unit> - <trans-unit id="Enter correct passphrase." xml:space="preserve"> + <trans-unit id="Enter correct passphrase." xml:space="preserve" approved="no"> <source>Enter correct passphrase.</source> + <target state="translated">أدخل عبارة المرور الصحيحة.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enter passphrase…" xml:space="preserve"> + <trans-unit id="Enter passphrase…" xml:space="preserve" approved="no"> <source>Enter passphrase…</source> + <target state="translated">أدخل عبارة المرور…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enter server manually" xml:space="preserve"> + <trans-unit id="Enter server manually" xml:space="preserve" approved="no"> <source>Enter server manually</source> + <target state="translated">أدخل الخادم يدوياً</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error" xml:space="preserve"> + <trans-unit id="Error" xml:space="preserve" approved="no"> <source>Error</source> + <target state="translated">خطأ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error accepting contact request" xml:space="preserve"> + <trans-unit id="Error accepting contact request" xml:space="preserve" approved="no"> <source>Error accepting contact request</source> + <target state="translated">خطأ في قبول طلب الاتصال</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error accessing database file" xml:space="preserve"> <source>Error accessing database file</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error adding member(s)" xml:space="preserve"> + <trans-unit id="Error adding member(s)" xml:space="preserve" approved="no"> <source>Error adding member(s)</source> + <target state="translated">خطأ في إضافة عضو (أعضاء)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error changing address" xml:space="preserve"> + <trans-unit id="Error changing address" xml:space="preserve" approved="no"> <source>Error changing address</source> + <target state="translated">خطأ في تغيير العنوان</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error changing role" xml:space="preserve"> + <trans-unit id="Error changing role" xml:space="preserve" approved="no"> <source>Error changing role</source> + <target state="translated">خطأ في تغيير الدور المتغير</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error changing setting" xml:space="preserve"> + <trans-unit id="Error changing setting" xml:space="preserve" approved="no"> <source>Error changing setting</source> + <target state="translated">خطأ في تغيير الإعدادات</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> @@ -1419,16 +1602,16 @@ <source>Group members can irreversibly delete sent messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> @@ -1519,8 +1702,8 @@ <source>Image will be received when your contact is online, please wait or check later!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Import" xml:space="preserve"> @@ -1616,8 +1799,8 @@ <source>Irreversible message deletion is prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve"> @@ -1917,8 +2100,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Onion hosts will not be used.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> @@ -1969,8 +2152,9 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Open user profiles</source> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> + <trans-unit id="Anybody can host servers." xml:space="preserve" approved="no"> + <source>Anybody can host servers.</source> + <target state="translated">يمكن لأي شخص استضافة الخوادم.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve"> @@ -2001,8 +2185,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Paste the link you received into the box below to connect with your contact.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Periodically" xml:space="preserve"> @@ -2373,96 +2557,117 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Sent messages will be deleted after set time.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> + <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve" approved="no"> <source>Server requires authorization to create queues, check password</source> + <target state="translated">يتطلب الخادم إذنًا لإنشاء قوائم انتظار، تحقق من كلمة المرور</target> <note>server test error</note> </trans-unit> - <trans-unit id="Server test failed!" xml:space="preserve"> + <trans-unit id="Server test failed!" xml:space="preserve" approved="no"> <source>Server test failed!</source> + <target state="translated">فشلت تجربة الخادم!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Servers" xml:space="preserve"> + <trans-unit id="Servers" xml:space="preserve" approved="no"> <source>Servers</source> + <target state="translated">الخوادم</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Set 1 day" xml:space="preserve"> + <trans-unit id="Set 1 day" xml:space="preserve" approved="no"> <source>Set 1 day</source> + <target state="translated">تعيين يوم واحد</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Set contact name…" xml:space="preserve"> + <trans-unit id="Set contact name…" xml:space="preserve" approved="no"> <source>Set contact name…</source> + <target state="translated">تعيين اسم جهة الاتصال…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Set group preferences" xml:space="preserve"> + <trans-unit id="Set group preferences" xml:space="preserve" approved="no"> <source>Set group preferences</source> + <target state="translated">عيّن تفضيلات المجموعة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Set passphrase to export" xml:space="preserve"> + <trans-unit id="Set passphrase to export" xml:space="preserve" approved="no"> <source>Set passphrase to export</source> + <target state="translated">عيّن عبارة المرور للتصدير</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Set timeouts for proxy/VPN" xml:space="preserve"> + <trans-unit id="Set timeouts for proxy/VPN" xml:space="preserve" approved="no"> <source>Set timeouts for proxy/VPN</source> + <target state="translated">حدد مهلات للوسيط او شبكات افتراضية خاصة (Proxy/VPN timeouts)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Settings" xml:space="preserve"> + <trans-unit id="Settings" xml:space="preserve" approved="no"> <source>Settings</source> + <target state="translated">الإعدادات</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share" xml:space="preserve"> + <trans-unit id="Share" xml:space="preserve" approved="no"> <source>Share</source> + <target state="translated">مشاركة</target> <note>chat item action</note> </trans-unit> <trans-unit id="Share invitation link" xml:space="preserve"> <source>Share invitation link</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share link" xml:space="preserve"> + <trans-unit id="Share link" xml:space="preserve" approved="no"> <source>Share link</source> + <target state="translated">مشاركة الرابط</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share one-time invitation link" xml:space="preserve"> <source>Share one-time invitation link</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Show QR code" xml:space="preserve"> + <trans-unit id="Show QR code" xml:space="preserve" approved="no"> <source>Show QR code</source> + <target state="translated">عرض رمز QR</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Show preview" xml:space="preserve"> + <trans-unit id="Show preview" xml:space="preserve" approved="no"> <source>Show preview</source> + <target state="translated">عرض المعاينة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> + <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve" approved="no"> <source>SimpleX Chat security was audited by Trail of Bits.</source> + <target state="translated">تم تدقيق أمان SimpleX Chat بواسطة Trail of Bits.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SimpleX Lock" xml:space="preserve"> + <trans-unit id="SimpleX Lock" xml:space="preserve" approved="no"> <source>SimpleX Lock</source> + <target state="translated">قفل SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SimpleX Lock turned on" xml:space="preserve"> + <trans-unit id="SimpleX Lock turned on" xml:space="preserve" approved="no"> <source>SimpleX Lock turned on</source> + <target state="translated">تم تشغيل القفل SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SimpleX contact address" xml:space="preserve"> + <trans-unit id="SimpleX contact address" xml:space="preserve" approved="no"> <source>SimpleX contact address</source> + <target state="translated">عنوان جهة أتصال SimpleX</target> <note>simplex link type</note> </trans-unit> - <trans-unit id="SimpleX encrypted message or connection event" xml:space="preserve"> + <trans-unit id="SimpleX encrypted message or connection event" xml:space="preserve" approved="no"> <source>SimpleX encrypted message or connection event</source> + <target state="translated">حَدَثْ SimpleX لرسالة أو اتصال مشفر</target> <note>notification</note> </trans-unit> - <trans-unit id="SimpleX group link" xml:space="preserve"> + <trans-unit id="SimpleX group link" xml:space="preserve" approved="no"> <source>SimpleX group link</source> + <target state="translated">رابط مجموعة SimpleX</target> <note>simplex link type</note> </trans-unit> - <trans-unit id="SimpleX links" xml:space="preserve"> + <trans-unit id="SimpleX links" xml:space="preserve" approved="no"> <source>SimpleX links</source> + <target state="translated">روابط SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> + <trans-unit id="SimpleX one-time invitation" xml:space="preserve" approved="no"> <source>SimpleX one-time invitation</source> + <target state="translated">دعوة SimpleX لمرة واحدة</target> <note>simplex link type</note> </trans-unit> <trans-unit id="Skip" xml:space="preserve"> @@ -2581,8 +2786,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Thanks to the users – contribute via Weblate!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve"> @@ -2613,16 +2818,16 @@ We will be adding server redundancy to prevent lost messages.</source> <source>The microphone does not work when the app is in the background.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> <source>The old database was not removed during the migration, it can be deleted.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The sender will NOT be notified" xml:space="preserve"> @@ -2677,8 +2882,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>To prevent the call interruption, enable Do Not Disturb mode.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -2851,8 +3056,8 @@ To connect, please ask your contact to create another connection link and check <source>Voice messages are prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> @@ -2963,10 +3168,6 @@ To connect, please ask your contact to create another connection link and check <source>You can use markdown to format messages:</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <note>No comment provided by engineer.</note> @@ -3526,72 +3727,87 @@ SimpleX servers cannot see your profile.</source> <source>secret</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="starting…" xml:space="preserve"> + <trans-unit id="starting…" xml:space="preserve" approved="no"> <source>starting…</source> + <target state="translated">يبدأ…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="strike" xml:space="preserve"> + <trans-unit id="strike" xml:space="preserve" approved="no"> <source>strike</source> + <target state="translated">شطب</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="this contact" xml:space="preserve"> <source>this contact</source> <note>notification title</note> </trans-unit> - <trans-unit id="unknown" xml:space="preserve"> + <trans-unit id="unknown" xml:space="preserve" approved="no"> <source>unknown</source> + <target state="translated">غير معروف</target> <note>connection info</note> </trans-unit> - <trans-unit id="updated group profile" xml:space="preserve"> + <trans-unit id="updated group profile" xml:space="preserve" approved="no"> <source>updated group profile</source> + <target state="translated">حدثت ملف تعريف المجموعة</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="v%@ (%@)" xml:space="preserve"> <source>v%@ (%@)</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="via contact address link" xml:space="preserve"> + <trans-unit id="via contact address link" xml:space="preserve" approved="no"> <source>via contact address link</source> + <target state="translated">عبر رابط عنوان الاتصال</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="via group link" xml:space="preserve"> + <trans-unit id="via group link" xml:space="preserve" approved="no"> <source>via group link</source> + <target state="translated">عبر رابط المجموعة</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="via one-time link" xml:space="preserve"> + <trans-unit id="via one-time link" xml:space="preserve" approved="no"> <source>via one-time link</source> + <target state="translated">عبر رابط لمرة واحدة</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="via relay" xml:space="preserve"> + <trans-unit id="via relay" xml:space="preserve" approved="no"> <source>via relay</source> + <target state="translated">عبر المُرحل</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> + <trans-unit id="video call (not e2e encrypted)" xml:space="preserve" approved="no"> <source>video call (not e2e encrypted)</source> + <target state="translated">مكالمة الفيديو ليست مُعمّاة بين الطريفين</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="waiting for answer…" xml:space="preserve"> + <trans-unit id="waiting for answer…" xml:space="preserve" approved="no"> <source>waiting for answer…</source> + <target state="translated">بانتظار الرد…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="waiting for confirmation…" xml:space="preserve"> + <trans-unit id="waiting for confirmation…" xml:space="preserve" approved="no"> <source>waiting for confirmation…</source> + <target state="translated">في انتظار التأكيد…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="wants to connect to you!" xml:space="preserve"> + <trans-unit id="wants to connect to you!" xml:space="preserve" approved="no"> <source>wants to connect to you!</source> + <target state="translated">يريد الاتصال بك!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="yes" xml:space="preserve"> + <trans-unit id="yes" xml:space="preserve" approved="no"> <source>yes</source> + <target state="translated">نعم</target> <note>pref value</note> </trans-unit> - <trans-unit id="you are invited to group" xml:space="preserve"> + <trans-unit id="you are invited to group" xml:space="preserve" approved="no"> <source>you are invited to group</source> + <target state="translated">أنت مدعو إلى المجموعة</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="you changed address" xml:space="preserve"> + <trans-unit id="you changed address" xml:space="preserve" approved="no"> <source>you changed address</source> + <target state="translated">غيّرتَ العنوان</target> <note>chat item text</note> </trans-unit> <trans-unit id="you changed address for %@" xml:space="preserve"> @@ -3606,16 +3822,18 @@ SimpleX servers cannot see your profile.</source> <source>you changed role of %1$@ to %2$@</source> <note>snd group event chat item</note> </trans-unit> - <trans-unit id="you left" xml:space="preserve"> + <trans-unit id="you left" xml:space="preserve" approved="no"> <source>you left</source> + <target state="translated">غادرت</target> <note>snd group event chat item</note> </trans-unit> <trans-unit id="you removed %@" xml:space="preserve"> <source>you removed %@</source> <note>snd group event chat item</note> </trans-unit> - <trans-unit id="you shared one-time link" xml:space="preserve"> + <trans-unit id="you shared one-time link" xml:space="preserve" approved="no"> <source>you shared one-time link</source> + <target state="translated">لقد شاركت رابط لمرة واحدة</target> <note>chat list item description</note> </trans-unit> <trans-unit id="you shared one-time link incognito" xml:space="preserve"> @@ -3657,7 +3875,7 @@ SimpleX servers cannot see your profile.</source> </trans-unit> <trans-unit id="# %@" xml:space="preserve" approved="no"> <source># %@</source> - <target state="needs-translation"># %@</target> + <target state="translated"># %@</target> <note>copied message info title, # <title></note> </trans-unit> <trans-unit id="## History" xml:space="preserve" approved="no"> @@ -3667,7 +3885,7 @@ SimpleX servers cannot see your profile.</source> </trans-unit> <trans-unit id="## In reply to" xml:space="preserve" approved="no"> <source>## In reply to</source> - <target state="translated">## ردًا على</target> + <target state="translated">## ردًّا على</target> <note>copied message info</note> </trans-unit> <trans-unit id="%@ and %@ connected" xml:space="preserve" approved="no"> @@ -3675,6 +3893,1870 @@ SimpleX servers cannot see your profile.</source> <target state="translated">%@ و %@ متصل</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve" approved="no"> + <source>%@ downloaded</source> + <target state="translated">%@ تم التنزيل</target> + </trans-unit> + <trans-unit id="%@ and %@" xml:space="preserve" approved="no"> + <source>%@ and %@</source> + <target state="translated">%@ و %@</target> + </trans-unit> + <trans-unit id="%@ connected" xml:space="preserve" approved="no"> + <source>%@ connected</source> + <target state="translated">%@ متصل</target> + </trans-unit> + <trans-unit id="%lld minutes" xml:space="preserve" approved="no"> + <source>%lld minutes</source> + <target state="translated">%lld دقائق</target> + </trans-unit> + <trans-unit id="%@, %@ and %lld members" xml:space="preserve" approved="no"> + <source>%@, %@ and %lld members</source> + <target state="translated">%@, %@ و %lld أعضاء</target> + </trans-unit> + <trans-unit id="%d weeks" xml:space="preserve" approved="no"> + <source>%d weeks</source> + <target state="translated">%d أسابيع</target> + </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve" approved="no"> + <source>%@ uploaded</source> + <target state="translated">%@ تم الرفع</target> + </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve" approved="no"> + <source>%@, %@ and %lld other members connected</source> + <target state="translated">%@, %@ و %lld أعضاء آخرين متصلين</target> + </trans-unit> + <trans-unit id="%lld seconds" xml:space="preserve" approved="no"> + <source>%lld seconds</source> + <target state="translated">%lld ثواني</target> + </trans-unit> + <trans-unit id="%u messages failed to decrypt." xml:space="preserve" approved="no"> + <source>%u messages failed to decrypt.</source> + <target state="translated">%u فشلت عملية فك تشفير الرسائل.</target> + </trans-unit> + <trans-unit id="%lld messages marked deleted" xml:space="preserve" approved="no"> + <source>%lld messages marked deleted</source> + <target state="translated">%lld الرسائل معلمه بالحذف</target> + </trans-unit> + <trans-unit id="%lld messages moderated by %@" xml:space="preserve" approved="no"> + <source>%lld messages moderated by %@</source> + <target state="translated">%lld رسائل تمت إدارتها بواسطة %@</target> + </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve" approved="no"> + <source>%lld new interface languages</source> + <target state="translated">%lld لغات واجهة جديدة</target> + </trans-unit> + <trans-unit id="%lld group events" xml:space="preserve" approved="no"> + <source>%lld group events</source> + <target state="translated">%lld أحداث المجموعة</target> + </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve" approved="no"> + <source>%lld messages blocked by admin</source> + <target state="translated">%lld رسائل محظورة بواسطه المسؤول</target> + </trans-unit> + <trans-unit id="%lld messages blocked" xml:space="preserve" approved="no"> + <source>%lld messages blocked</source> + <target state="translated">%lld رسائل تم حظرها</target> + </trans-unit> + <trans-unit id="%u messages skipped." xml:space="preserve" approved="no"> + <source>%u messages skipped.</source> + <target state="translated">%u تم تخطي الرسائل.</target> + </trans-unit> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve" approved="no"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target state="translated">**إضافة جهة اتصال**: لإنشاء رابط دعوة جديد، أو الاتصال عبر الرابط الذي تلقيتوهم.</target> + </trans-unit> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve" approved="no"> + <source>**Create group**: to create a new group.</source> + <target state="translated">**إنشاء مجموعة**: لإنشاء مجموعة جديدة.</target> + </trans-unit> + <trans-unit id="(this device v%@)" xml:space="preserve" approved="no"> + <source>(this device v%@)</source> + <target state="translated">(هذا الجهاز v%@)</target> + </trans-unit> + <trans-unit id="(new)" xml:space="preserve" approved="no"> + <source>(new)</source> + <target state="translated">(جديد)</target> + </trans-unit> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve" approved="no"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> + <target state="translated">**يرجى الملاحظة**: سيؤدي استخدام نفس قاعدة البيانات على جهازين إلى كسر فك تشفير الرسائل من اتصالاتكم كحماية أمنية.</target> + </trans-unit> + <trans-unit id="A new random profile will be shared." xml:space="preserve" approved="no"> + <source>A new random profile will be shared.</source> + <target state="translated">سيتم مشاركة ملف تعريفي عشوائي جديد.</target> + </trans-unit> + <trans-unit id="30 seconds" xml:space="preserve" approved="no"> + <source>30 seconds</source> + <target state="translated">30 ثانيه</target> + </trans-unit> + <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve" approved="no"> + <source>- more stable message delivery. +- a bit better groups. +- and more!</source> + <target state="translated">- تسليم رسائل أكثر استقرارًا. +- مجموعات أفضل قليلاً. +- والمزيد!</target> + </trans-unit> + <trans-unit id="0 sec" xml:space="preserve" approved="no"> + <source>0 sec</source> + <target state="translated">0 ثانيه</target> + </trans-unit> + <trans-unit id="1 minute" xml:space="preserve" approved="no"> + <source>1 minute</source> + <target state="translated">1 دقيقة</target> + </trans-unit> + <trans-unit id="5 minutes" xml:space="preserve" approved="no"> + <source>5 minutes</source> + <target state="translated">5 دقائق</target> + </trans-unit> + <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve" approved="no"> + <source><p>Hi!</p> +<p><a href="%@">Connect to me via SimpleX Chat</a></p></source> + <target state="translated"><p>مرحبا!</p> +<p><a href="%@">أتصل بى من خلال SimpleX Chat</a></p></target> + </trans-unit> + <trans-unit id="0s" xml:space="preserve" approved="no"> + <source>0s</source> + <target state="translated">0 ث</target> + </trans-unit> + <trans-unit id="A few more things" xml:space="preserve" approved="no"> + <source>A few more things</source> + <target state="translated">بعض الأشياء الأخرى</target> + </trans-unit> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve" approved="no"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> + <target state="translated">- أتصل بـ [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- أسرع و أكثر اسْتِقْرارًا.</target> + </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve" approved="no"> + <source>**Warning**: the archive will be removed.</source> + <target state="translated">**تحذير**: سيتم إزالة الأرشيف.</target> + </trans-unit> + <trans-unit id="- optionally notify deleted contacts. - profile names with spaces. - and more!" xml:space="preserve" approved="no"> + <source>- optionally notify deleted contacts. +- profile names with spaces. +- and more!</source> + <target state="translated">- إخطار جهات الاتصال المحذوفة بشكل اختياري. +- أسماء الملفات الشخصية مع المسافات. +- والمزيد!</target> + </trans-unit> + <trans-unit id="- voice messages up to 5 minutes. - custom time to disappear. - editing history." xml:space="preserve" approved="no"> + <source>- voice messages up to 5 minutes. +- custom time to disappear. +- editing history.</source> + <target state="translated">- رسائل صوتية تصل مدتها إلى 5 دقائق. +- وقت مخصص للاختفاء. +- تعديل السجل.</target> + </trans-unit> + <trans-unit id="Add welcome message" xml:space="preserve" approved="no"> + <source>Add welcome message</source> + <target state="translated">إضافة رسالة ترحيب</target> + </trans-unit> + <trans-unit id="Abort changing address?" xml:space="preserve" approved="no"> + <source>Abort changing address?</source> + <target state="translated">هل تريد إلغاء تغيير العنوان؟</target> + </trans-unit> + <trans-unit id="Add contact" xml:space="preserve" approved="no"> + <source>Add contact</source> + <target state="translated">إضافة جهة اتصال</target> + </trans-unit> + <trans-unit id="Abort" xml:space="preserve" approved="no"> + <source>Abort</source> + <target state="translated">إحباط</target> + </trans-unit> + <trans-unit id="About SimpleX address" xml:space="preserve" approved="no"> + <source>About SimpleX address</source> + <target state="translated">حول عنوان SimpleX</target> + </trans-unit> + <trans-unit id="Accept connection request?" xml:space="preserve" approved="no"> + <source>Accept connection request?</source> + <target state="translated">قبول طلب الاتصال؟</target> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve" approved="no"> + <source>Acknowledged</source> + <target state="translated">معترف به</target> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve" approved="no"> + <source>Acknowledgement errors</source> + <target state="translated">أخطاء الإقرار</target> + </trans-unit> + <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve" approved="no"> + <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> + <target state="translated">أضف عنوانًا إلى ملفكم الشخصي، حتى تتمكن جهات الاتصال الخاصة بكم من مشاركته مع أشخاص اخرين. سيتم إرسال تحديث الملف الشخصي إلى جهات الاتصال الخاصة بكم.</target> + </trans-unit> + <trans-unit id="Abort changing address" xml:space="preserve" approved="no"> + <source>Abort changing address</source> + <target state="translated">إحباط تغيير العنوان</target> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve" approved="no"> + <source>Active connections</source> + <target state="translated">اتصالات نشطة</target> + </trans-unit> + <trans-unit id="Apply" xml:space="preserve" approved="no"> + <source>Apply</source> + <target state="translated">طبّق</target> + </trans-unit> + <trans-unit id="%@ server" xml:space="preserve" approved="no"> + <source>%@ server</source> + <target state="translated">%@ خادم</target> + </trans-unit> + <trans-unit id="Accept conditions" xml:space="preserve" approved="no"> + <source>Accept conditions</source> + <target state="translated">اقبل الشروط</target> + </trans-unit> + <trans-unit id="Share address" xml:space="preserve" approved="no"> + <source>Share address</source> + <target state="translated">مشاركة العنوان</target> + </trans-unit> + <trans-unit id="Already connecting!" xml:space="preserve" approved="no"> + <source>Already connecting!</source> + <target state="translated">جارٍ الاتصال بالفعل!</target> + </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve" approved="no"> + <source>%d file(s) are still being downloaded.</source> + <target state="translated">%d الملف(ات) لا تزال قيد التنزيل.</target> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve" approved="no"> + <source>%d file(s) failed to download.</source> + <target state="translated">%d الملف(ات) فشلت في التنزيل.</target> + </trans-unit> + <trans-unit id="All app data is deleted." xml:space="preserve" approved="no"> + <source>All app data is deleted.</source> + <target state="translated">حُذفت جميع بيانات التطبيق.</target> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve" approved="no"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target state="translated">السماح بحذف الرسائل بشكل لا رجوع فيه فقط إذا سمحت لك جهة الاتصال بذلك. (24 ساعة)</target> + </trans-unit> + <trans-unit id="Share profile" xml:space="preserve" approved="no"> + <source>Share profile</source> + <target state="translated">شارك ملف التعريف</target> + </trans-unit> + <trans-unit id="Always use relay" xml:space="preserve" approved="no"> + <source>Always use relay</source> + <target state="translated">استخدم الموجه دائمًا</target> + </trans-unit> + <trans-unit id="Address" xml:space="preserve" approved="no"> + <source>Address</source> + <target state="translated">عنوان</target> + </trans-unit> + <trans-unit id="All data is erased when it is entered." xml:space="preserve" approved="no"> + <source>All data is erased when it is entered.</source> + <target state="translated">يتم مسح جميع البيانات عند إدخالها.</target> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve" approved="no"> + <source>%d file(s) were deleted.</source> + <target state="translated">%d تم حذف الملف(ات).</target> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve" approved="no"> + <source>%d file(s) were not downloaded.</source> + <target state="translated">%d لم يتم تنزيل الملف(ات).</target> + </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve" approved="no"> + <source>%d messages not forwarded</source> + <target state="translated">%d الرسائل لم يتم تحويلها</target> + </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve" approved="no"> + <source>%d seconds(s)</source> + <target state="translated">%d ثواني</target> + </trans-unit> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve" approved="no"> + <source>**Scan / Paste link**: to connect via a link you received.</source> + <target state="translated">**امسح / ألصِق الرابط**: للاتصال عبر الرابط الذي تلقيته.</target> + </trans-unit> + <trans-unit id="1 year" xml:space="preserve" approved="no"> + <source>1 year</source> + <target state="translated">سنة واحدة</target> + </trans-unit> + <trans-unit id="1-time link" xml:space="preserve" approved="no"> + <source>1-time link</source> + <target state="translated">رابط لمرة واحدة</target> + </trans-unit> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve" approved="no"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> + <target state="translated">يمكن استعمال الرابط لمرة واحدة *مع جهة اتصال واحدة فقط* - شاركه شخصياً أو عبر أي تطبيق مراسلة.</target> + </trans-unit> + <trans-unit id="Accent" xml:space="preserve" approved="no"> + <source>Accent</source> + <target state="translated">لون تمييزي</target> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve" approved="no"> + <source>Accepted conditions</source> + <target state="translated">الشروط المتفق عليها</target> + </trans-unit> + <trans-unit id="All chats will be removed from the list (text), and the list deleted." xml:space="preserve" approved="no"> + <source>All chats will be removed from the list (text), and the list deleted.</source> + <target state="translated">سيتم إزالة جميع الدردشات من القائمة (النص)، وحذف القائمة.</target> + </trans-unit> + <trans-unit id="Allow message reactions." xml:space="preserve" approved="no"> + <source>Allow message reactions.</source> + <target state="translated">السماح بردود الفعل على الرسائل.</target> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target state="translated">السماح بحذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة)</target> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve" approved="no"> + <source>Allow to send SimpleX links.</source> + <target state="translated">السماح بإرسال روابط SimpleX.</target> + </trans-unit> + <trans-unit id="Already joining the group!" xml:space="preserve" approved="no"> + <source>Already joining the group!</source> + <target state="translated">جارٍ انضمام بالفعل إلى المجموعة!</target> + </trans-unit> + <trans-unit id="An empty chat profile with the provided name is created, and the app opens as usual." xml:space="preserve" approved="no"> + <source>An empty chat profile with the provided name is created, and the app opens as usual.</source> + <target state="translated">يتم إنشاء ملف تعريف دردشة فارغ بالاسم المقدم، ويفتح التطبيق كالمعتاد.</target> + </trans-unit> + <trans-unit id="Authentication cancelled" xml:space="preserve" approved="no"> + <source>Authentication cancelled</source> + <target state="translated">ألغيت المصادقة</target> + </trans-unit> + <trans-unit id="Audio/video calls are prohibited." xml:space="preserve" approved="no"> + <source>Audio/video calls are prohibited.</source> + <target state="translated">مكالمات الصوت/الفيديو محظورة.</target> + </trans-unit> + <trans-unit id="Better groups" xml:space="preserve" approved="no"> + <source>Better groups</source> + <target state="translated">مجموعات أفضل</target> + </trans-unit> + <trans-unit id="Background" xml:space="preserve" approved="no"> + <source>Background</source> + <target state="translated">الخلفية</target> + </trans-unit> + <trans-unit id="Better calls" xml:space="preserve" approved="no"> + <source>Better calls</source> + <target state="translated">مكالمات أفضل</target> + </trans-unit> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target state="translated">يمكنك أنت وجهة اتصالك حذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة)</target> + </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve" approved="no"> + <source>Block member for all?</source> + <target state="translated">حظر العضو للجميع؟</target> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve" approved="no"> + <source>Blur media</source> + <target state="translated">تمويه الوسائط</target> + </trans-unit> + <trans-unit id="Server type" xml:space="preserve" approved="no"> + <source>Server type</source> + <target state="translated">نوع الخادم</target> + </trans-unit> + <trans-unit id="Server requires authorization to upload, check password" xml:space="preserve" approved="no"> + <source>Server requires authorization to upload, check password</source> + <target state="translated">يتطلب الخادم إذنًا للرفع، تحقق من كلمة المرور</target> + </trans-unit> + <trans-unit id="Server version is incompatible with network settings." xml:space="preserve" approved="no"> + <source>Server version is incompatible with network settings.</source> + <target state="translated">إصدار الخادم غير متوافق مع إعدادات الشبكة.</target> + </trans-unit> + <trans-unit id="Share with contacts" xml:space="preserve" approved="no"> + <source>Share with contacts</source> + <target state="translated">مشاركة مع جهات الاتصال</target> + </trans-unit> + <trans-unit id="Show:" xml:space="preserve" approved="no"> + <source>Show:</source> + <target state="translated">عرض:</target> + </trans-unit> + <trans-unit id="SimpleX Address" xml:space="preserve" approved="no"> + <source>SimpleX Address</source> + <target state="translated">عنوان SimpleX</target> + </trans-unit> + <trans-unit id="SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." xml:space="preserve" approved="no"> + <source>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</source> + <target state="translated">توصلت SimpleX Chat وFlux إلى اتفاق لتضمين الخوادم التي تديرها Flux في التطبيق.</target> + </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve" approved="no"> + <source>Allow calls?</source> + <target state="translated">السماح بالمكالمات؟</target> + </trans-unit> + <trans-unit id="App passcode is replaced with self-destruct passcode." xml:space="preserve" approved="no"> + <source>App passcode is replaced with self-destruct passcode.</source> + <target state="translated">يتم استبدال رمز مرور التطبيق برمز مرور التدمير الذاتي.</target> + </trans-unit> + <trans-unit id="SimpleX Lock mode" xml:space="preserve" approved="no"> + <source>SimpleX Lock mode</source> + <target state="translated">SimpleX وضع القفل</target> + </trans-unit> + <trans-unit id="Audio and video calls" xml:space="preserve" approved="no"> + <source>Audio and video calls</source> + <target state="translated">مكالمات الصوت والفيديو</target> + </trans-unit> + <trans-unit id="App passcode" xml:space="preserve" approved="no"> + <source>App passcode</source> + <target state="translated">رمز مرور التطبيق</target> + </trans-unit> + <trans-unit id="Bad message ID" xml:space="preserve" approved="no"> + <source>Bad message ID</source> + <target state="translated">معرّف رسالة سيئ</target> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings." xml:space="preserve" approved="no"> + <source>Server address is incompatible with network settings.</source> + <target state="translated">عنوان الخادم غير متوافق مع إعدادات الشبكة.</target> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve" approved="no"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <target state="translated">سيتم تصفير إحصائيات الخوادم - لا يمكن التراجع عن هذا!</target> + </trans-unit> + <trans-unit id="Allow to send files and media." xml:space="preserve" approved="no"> + <source>Allow to send files and media.</source> + <target state="translated">السماح بإرسال الملفات والوسائط.</target> + </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve" approved="no"> + <source>App encrypts new local files (except videos).</source> + <target state="translated">يُعمِّي الملفات المحلية الجديدة (باستثناء مقاطع الفيديو).</target> + </trans-unit> + <trans-unit id="Better messages" xml:space="preserve" approved="no"> + <source>Better messages</source> + <target state="translated">رسائل أفضل</target> + </trans-unit> + <trans-unit id="Set passcode" xml:space="preserve" approved="no"> + <source>Set passcode</source> + <target state="translated">عيّن رمز المرور</target> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve" approved="no"> + <source>Additional accent 2</source> + <target state="translated">لون إضافي ثانوي 2</target> + </trans-unit> + <trans-unit id="Allow your contacts adding message reactions." xml:space="preserve" approved="no"> + <source>Allow your contacts adding message reactions.</source> + <target state="translated">السماح لجهات اتصالك بإضافة ردود الفعل للرسالة.</target> + </trans-unit> + <trans-unit id="Allow your contacts to call you." xml:space="preserve" approved="no"> + <source>Allow your contacts to call you.</source> + <target state="translated">السماح لجهات اتصالك بالاتصال بك.</target> + </trans-unit> + <trans-unit id="Audio/video calls" xml:space="preserve" approved="no"> + <source>Audio/video calls</source> + <target state="translated">مكالمات الصوت/الفيديو</target> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve" approved="no"> + <source>Better notifications</source> + <target state="translated">إشعارات أفضل</target> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve" approved="no"> + <source>Better user experience</source> + <target state="translated">تجربة مستخدم أفضل</target> + </trans-unit> + <trans-unit id="Block" xml:space="preserve" approved="no"> + <source>Block</source> + <target state="translated">حظر</target> + </trans-unit> + <trans-unit id="Black" xml:space="preserve" approved="no"> + <source>Black</source> + <target state="translated">أسود</target> + </trans-unit> + <trans-unit id="Block member?" xml:space="preserve" approved="no"> + <source>Block member?</source> + <target state="translated">حظر العضو؟</target> + </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve" approved="no"> + <source>Blocked by admin</source> + <target state="translated">محظور من قبل المُدير</target> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve" approved="no"> + <source>Blur for better privacy.</source> + <target state="translated">تمويه من أجل خصوصية أفضل.</target> + </trans-unit> + <trans-unit id="Show → on messages sent via private routing." xml:space="preserve" approved="no"> + <source>Show → on messages sent via private routing.</source> + <target state="translated">عرض ← على الرسائل المرسلة عبر التوجيه الخاص.</target> + </trans-unit> + <trans-unit id="Share from other apps." xml:space="preserve" approved="no"> + <source>Share from other apps.</source> + <target state="translated">المشاركة من التطبيقات الأخرى.</target> + </trans-unit> + <trans-unit id="Share this 1-time invite link" xml:space="preserve" approved="no"> + <source>Share this 1-time invite link</source> + <target state="translated">شارك رابط الدعوة هذا لمرة واحدة</target> + </trans-unit> + <trans-unit id="Set passphrase" xml:space="preserve" approved="no"> + <source>Set passphrase</source> + <target state="translated">عيّن عبارة المرور</target> + </trans-unit> + <trans-unit id="Share address with contacts?" xml:space="preserve" approved="no"> + <source>Share address with contacts?</source> + <target state="translated">مشاركة العنوان مع جهات الاتصال؟</target> + </trans-unit> + <trans-unit id="Allow downgrade" xml:space="preserve" approved="no"> + <source>Allow downgrade</source> + <target state="translated">السماح بالرجوع إلى إصدار سابق</target> + </trans-unit> + <trans-unit id="Bad desktop address" xml:space="preserve" approved="no"> + <source>Bad desktop address</source> + <target state="translated">عنوان سطح المكتب غير صالح</target> + </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve" approved="no"> + <source>%1$@, %2$@</source> + <target state="translated">%1$@, %2$@</target> + </trans-unit> + <trans-unit id="All profiles" xml:space="preserve" approved="no"> + <source>All profiles</source> + <target state="translated">جميع ملفات التعريف</target> + </trans-unit> + <trans-unit id="Authentication is required before the call is connected, but you may miss calls." xml:space="preserve" approved="no"> + <source>Authentication is required before the call is connected, but you may miss calls.</source> + <target state="translated">يتطلب التوثيق قبل الاتصال بالمكالمة، ولكن قد تفوتك المكالمات.</target> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve" approved="no"> + <source>Archiving database</source> + <target state="translated">جارِ أرشفة قاعدة البيانات</target> + </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve" approved="no"> + <source>Settings were changed.</source> + <target state="translated">تم تغيير الإعدادات.</target> + </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve" approved="no"> + <source>Better groups performance</source> + <target state="translated">أداء مجموعات أفضل</target> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve" approved="no"> + <source>Better privacy and security</source> + <target state="translated">خصوصية وأمان أفضل</target> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve" approved="no"> + <source>Better security ✅</source> + <target state="translated">أمان أفضل ✅</target> + </trans-unit> + <trans-unit id="Block for all" xml:space="preserve" approved="no"> + <source>Block for all</source> + <target state="translated">حظر للجميع</target> + </trans-unit> + <trans-unit id="Block group members" xml:space="preserve" approved="no"> + <source>Block group members</source> + <target state="translated">حظر أعضاء المجموعة</target> + </trans-unit> + <trans-unit id="Block member" xml:space="preserve" approved="no"> + <source>Block member</source> + <target state="translated">حظر العضو</target> + </trans-unit> + <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve" approved="no"> + <source>Both you and your contact can add message reactions.</source> + <target state="translated">يمكنك أنت وجهة اتصالك إضافة ردود فعل الرسائل.</target> + </trans-unit> + <trans-unit id="Both you and your contact can make calls." xml:space="preserve" approved="no"> + <source>Both you and your contact can make calls.</source> + <target state="translated">يمكنك أنت وجهة الاتصال إجراء مكالمات.</target> + </trans-unit> + <trans-unit id="Server" xml:space="preserve" approved="no"> + <source>Server</source> + <target state="translated">الخادم</target> + </trans-unit> + <trans-unit id="Server operators" xml:space="preserve" approved="no"> + <source>Server operators</source> + <target state="translated">مُشغلي الخادم</target> + </trans-unit> + <trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve" approved="no"> + <source>Server version is incompatible with your app: %@.</source> + <target state="translated">إصدار الخادم غير متوافق مع التطبيق لديك: %@.</target> + </trans-unit> + <trans-unit id="Servers info" xml:space="preserve" approved="no"> + <source>Servers info</source> + <target state="translated">معلومات الخوادم</target> + </trans-unit> + <trans-unit id="Set chat name…" xml:space="preserve" approved="no"> + <source>Set chat name…</source> + <target state="translated">عيّن اسم الدردشة…</target> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve" approved="no"> + <source>Shape profile images</source> + <target state="translated">شكّل الصور التعريفية</target> + </trans-unit> + <trans-unit id="Share address publicly" xml:space="preserve" approved="no"> + <source>Share address publicly</source> + <target state="translated">شارك العنوان علناً</target> + </trans-unit> + <trans-unit id="Show developer options" xml:space="preserve" approved="no"> + <source>Show developer options</source> + <target state="translated">عرض خيارات المطور</target> + </trans-unit> + <trans-unit id="SimpleX address" xml:space="preserve" approved="no"> + <source>SimpleX address</source> + <target state="translated">عنوان SimpleX</target> + </trans-unit> + <trans-unit id="SimpleX address or 1-time link?" xml:space="preserve" approved="no"> + <source>SimpleX address or 1-time link?</source> + <target state="translated">عنوان SimpleX أو رابط لمرة واحدة؟</target> + </trans-unit> + <trans-unit id="@'%@'" xml:space="preserve" approved="no"> + <source>@'%@'</source> + <target state="translated">@'%@'</target> + </trans-unit> + <trans-unit id="@%@" xml:space="preserve" approved="no"> + <source>@%@</source> + <target state="translated">@%@</target> + </trans-unit> + <trans-unit id="Active" xml:space="preserve" approved="no"> + <source>Active</source> + <target state="translated">نشط</target> + </trans-unit> + <trans-unit id="Add friends" xml:space="preserve" approved="no"> + <source>Add friends</source> + <target state="translated">أضف أصدقاء</target> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve" approved="no"> + <source>Add list</source> + <target state="translated">أضف القائمة</target> + </trans-unit> + <trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve" approved="no"> + <source>Address change will be aborted. Old receiving address will be used.</source> + <target state="translated">سيتم إحباط تغيير العنوان. سيتم استخدام عنوان الاستلام القديم.</target> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve" approved="no"> + <source>All messages will be deleted - this cannot be undone!</source> + <target state="translated">سيتم حذف كافة الرسائل - لا يمكن التراجع عن هذا!</target> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve" approved="no"> + <source>All reports will be archived for you.</source> + <target state="translated">سيتم أرشفة كافة البلاغات لك.</target> + </trans-unit> + <trans-unit id="All your contacts will remain connected." xml:space="preserve" approved="no"> + <source>All your contacts will remain connected.</source> + <target state="translated">ستبقى جميع جهات اتصالك متصلة.</target> + </trans-unit> + <trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve" approved="no"> + <source>All your contacts will remain connected. Profile update will be sent to your contacts.</source> + <target state="translated">ستبقى جميع جهات اتصالك متصلة. سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك.</target> + </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve" approved="no"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target state="translated">جميع جهات الاتصال، المحادثات والملفات الخاصة بك سيتم تشفيرها بأمان ورفعها على شكل أجزاء إلى موجهات XFTP المُعدة.</target> + </trans-unit> + <trans-unit id="Allow calls only if your contact allows them." xml:space="preserve" approved="no"> + <source>Allow calls only if your contact allows them.</source> + <target state="translated">السماح بالمكالمات فقط إذا سمحت جهة اتصالك بذلك.</target> + </trans-unit> + <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve" approved="no"> + <source>Allow message reactions only if your contact allows them.</source> + <target state="translated">اسمح بردود الفعل على الرسائل فقط إذا سمحت جهة اتصالك بذلك.</target> + </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve" approved="no"> + <source>Allow to report messsages to moderators.</source> + <target state="translated">السماح بالإبلاغ عن الرسائل إلى المشرفين.</target> + </trans-unit> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target state="translated">اسمح لجهات اتصالك بحذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة)</target> + </trans-unit> + <trans-unit id="Another reason" xml:space="preserve" approved="no"> + <source>Another reason</source> + <target state="translated">سبب آخر</target> + </trans-unit> + <trans-unit id="App group:" xml:space="preserve" approved="no"> + <source>App group:</source> + <target state="translated">مجموعة التطبيق:</target> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve" approved="no"> + <source>Apply to</source> + <target state="translated">طبّق لِ</target> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve" approved="no"> + <source>Archive</source> + <target state="translated">أرشف</target> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve" approved="no"> + <source>Archive %lld reports?</source> + <target state="translated">أرشف تقارير %lld؟</target> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve" approved="no"> + <source>Archive all reports?</source> + <target state="translated">أرشفة كافة البلاغات؟</target> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve" approved="no"> + <source>Archive and upload</source> + <target state="translated">أرشفة و رفع</target> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve" approved="no"> + <source>Archive report</source> + <target state="translated">أرشف البلاغ</target> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve" approved="no"> + <source>Archive report?</source> + <target state="translated">أرشف البلاغ؟</target> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve" approved="no"> + <source>Archive reports</source> + <target state="translated">أرشف البلاغات</target> + </trans-unit> + <trans-unit id="Ask" xml:space="preserve" approved="no"> + <source>Ask</source> + <target state="translated">اسأل</target> + </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve" approved="no"> + <source>Auto-accept settings</source> + <target state="translated">إعدادات القبول التلقائي</target> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve" approved="no"> + <source>Better message dates.</source> + <target state="translated">تواريخ أفضل للرسائل.</target> + </trans-unit> + <trans-unit id="Server added to operator %@." xml:space="preserve" approved="no"> + <source>Server added to operator %@.</source> + <target state="translated">تمت إضافة الخادم إلى المشغل %@.</target> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve" approved="no"> + <source>Server address is incompatible with network settings: %@.</source> + <target state="translated">عنوان الخادم غير متوافق مع إعدادات الشبكة: %@.</target> + </trans-unit> + <trans-unit id="Server protocol changed." xml:space="preserve" approved="no"> + <source>Server protocol changed.</source> + <target state="translated">تغيّر بروتوكول الخادم.</target> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve" approved="no"> + <source>SimpleX links are prohibited.</source> + <target state="translated">روابط SimpleX محظورة.</target> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve" approved="no"> + <source>Additional accent</source> + <target state="translated">لون إضافي ثانوي</target> + </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve" approved="no"> + <source>Always use private routing.</source> + <target state="translated">استخدم دائمًا التوجيه الخاص.</target> + </trans-unit> + <trans-unit id="About operators" xml:space="preserve" approved="no"> + <source>About operators</source> + <target state="translated">عن المُشغلين</target> + </trans-unit> + <trans-unit id="Add team members" xml:space="preserve" approved="no"> + <source>Add team members</source> + <target state="translated">أضف أعضاء الفريق</target> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve" approved="no"> + <source>Added media & file servers</source> + <target state="translated">أُضيفت خوادم الوسائط والملفات</target> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve" approved="no"> + <source>Added message servers</source> + <target state="translated">أُضيفت خوادم الرسائل</target> + </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve" approved="no"> + <source>Address or 1-time link?</source> + <target state="translated">عنوان أو رابط لمرة واحدة؟</target> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve" approved="no"> + <source>Address settings</source> + <target state="translated">إعدادات العنوان</target> + </trans-unit> + <trans-unit id="Allow sharing" xml:space="preserve" approved="no"> + <source>Allow sharing</source> + <target state="translated">السماح بالمشاركة</target> + </trans-unit> + <trans-unit id="App data migration" xml:space="preserve" approved="no"> + <source>App data migration</source> + <target state="translated">ترحيل بيانات التطبيق</target> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve" approved="no"> + <source>Archive contacts to chat later.</source> + <target state="translated">أرشفة جهات الاتصال للدردشة لاحقًا.</target> + </trans-unit> + <trans-unit id="Better networking" xml:space="preserve" approved="no"> + <source>Better networking</source> + <target state="translated">اتصال أفضل</target> + </trans-unit> + <trans-unit id="Session code" xml:space="preserve" approved="no"> + <source>Session code</source> + <target state="translated">رمز الجلسة</target> + </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve" approved="no"> + <source>Set default theme</source> + <target state="translated">تعيين السمة الافتراضية</target> + </trans-unit> + <trans-unit id="Set it instead of system authentication." xml:space="preserve" approved="no"> + <source>Set it instead of system authentication.</source> + <target state="translated">عيّنها بدلاً من استيثاق النظام.</target> + </trans-unit> + <trans-unit id="Set the message shown to new members!" xml:space="preserve" approved="no"> + <source>Set the message shown to new members!</source> + <target state="translated">تعيين رسالة تظهر للأعضاء الجدد!</target> + </trans-unit> + <trans-unit id="Share 1-time link" xml:space="preserve" approved="no"> + <source>Share 1-time link</source> + <target state="translated">مشاركة رابط ذو استخدام واحد</target> + </trans-unit> + <trans-unit id="Share 1-time link with a friend" xml:space="preserve" approved="no"> + <source>Share 1-time link with a friend</source> + <target state="translated">شارك رابطًا لمرة واحدة مع صديق</target> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve" approved="no"> + <source>Share SimpleX address on social media.</source> + <target state="translated">شارك عنوان SimpleX على وسائل التواصل الاجتماعي.</target> + </trans-unit> + <trans-unit id="Share to SimpleX" xml:space="preserve" approved="no"> + <source>Share to SimpleX</source> + <target state="translated">المشاركة لSimpleX</target> + </trans-unit> + <trans-unit id="Show calls in phone history" xml:space="preserve" approved="no"> + <source>Show calls in phone history</source> + <target state="translated">عرض المكالمات في سجل الهاتف</target> + </trans-unit> + <trans-unit id="Show percentage" xml:space="preserve" approved="no"> + <source>Show percentage</source> + <target state="translated">أظهِر النسبة المئوية</target> + </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve" approved="no"> + <source>SimpleX</source> + <target state="translated">SimpleX</target> + </trans-unit> + <trans-unit id="SimpleX Lock not enabled!" xml:space="preserve" approved="no"> + <source>SimpleX Lock not enabled!</source> + <target state="translated">قفل SimpleX غير مفعّل!</target> + </trans-unit> + <trans-unit id="Bad message hash" xml:space="preserve" approved="no"> + <source>Bad message hash</source> + <target state="translated">تجزئة رسالة سيئة</target> + </trans-unit> + <trans-unit id="App session" xml:space="preserve" approved="no"> + <source>App session</source> + <target state="translated">جلسة التطبيق</target> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve" approved="no"> + <source>SimpleX links not allowed</source> + <target state="translated">روابط SimpleX غير مسموح بها</target> + </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve" approved="no"> + <source>All data is kept private on your device.</source> + <target state="translated">جميع البيانات تُحفظ بشكل خاص على جهازك.</target> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve" approved="no"> + <source>Archived contacts</source> + <target state="translated">جهات الاتصال المؤرشفة</target> + </trans-unit> + <trans-unit id="Show message status" xml:space="preserve" approved="no"> + <source>Show message status</source> + <target state="translated">أظهِر حالة الرسالة</target> + </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve" approved="no"> + <source>Set message expiration in chats.</source> + <target state="translated">اضبط انتهاء صلاحية الرسالة في الدردشات.</target> + </trans-unit> + <trans-unit id="Server address" xml:space="preserve" approved="no"> + <source>Server address</source> + <target state="translated">عنوان الخادم</target> + </trans-unit> + <trans-unit id="Show last messages" xml:space="preserve" approved="no"> + <source>Show last messages</source> + <target state="translated">إظهار الرسائل الأخيرة</target> + </trans-unit> + <trans-unit id="Server operator changed." xml:space="preserve" approved="no"> + <source>Server operator changed.</source> + <target state="translated">تغيّر مُشغل الخادم.</target> + </trans-unit> + <trans-unit id="SimpleX address and 1-time links are safe to share via any messenger." xml:space="preserve" approved="no"> + <source>SimpleX address and 1-time links are safe to share via any messenger.</source> + <target state="translated">عنوان SimpleX والروابط لمرة واحدة آمنة للمشاركة عبر أي برنامج مُراسلة.</target> + </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve" approved="no"> + <source>Add your team members to the conversations.</source> + <target state="translated">أضف أعضاء فريقك إلى المحادثات.</target> + </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve" approved="no"> + <source>Advanced settings</source> + <target state="translated">إعدادات متقدّمة</target> + </trans-unit> + <trans-unit id="Add to list" xml:space="preserve" approved="no"> + <source>Add to list</source> + <target state="translated">أضف إلى القائمة</target> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve" approved="no"> + <source>Additional secondary</source> + <target state="translated">ثانوي إضافي</target> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve" approved="no"> + <source>Admins can block a member for all.</source> + <target state="translated">يمكن للمُدراء حظر عضو للجميع.</target> + </trans-unit> + <trans-unit id="All" xml:space="preserve" approved="no"> + <source>All</source> + <target state="translated">الكل</target> + </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve" approved="no"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <target state="translated">جميع الرسائل والملفات تُرسل **مشفرة من النهاية-إلى-النهاية**، مع أمان ما-بعد-الحوسبة-الكمية في الرسائل المباشرة.</target> + </trans-unit> + <trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve" approved="no"> + <source>All new messages from %@ will be hidden!</source> + <target state="translated">جميع الرسائل الجديدة من %@ سيتم إخفاؤها!</target> + </trans-unit> + <trans-unit id="Auto-accept" xml:space="preserve" approved="no"> + <source>Auto-accept</source> + <target state="translated">قبول تلقائي</target> + </trans-unit> + <trans-unit id="Change self-destruct mode" xml:space="preserve" approved="no"> + <source>Change self-destruct mode</source> + <target state="translated">تغيير وضع التدمير الذاتي</target> + </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve" approved="no"> + <source>Chat database exported</source> + <target state="translated">صُدرت قاعدة بيانات الدردشة</target> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve" approved="no"> + <source>Businesses</source> + <target state="translated">الشركات</target> + </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve" approved="no"> + <source>Change automatic message deletion?</source> + <target state="translated">تغيير حذف الرسائل التلقائي؟</target> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve" approved="no"> + <source>Can't call contact</source> + <target state="translated">لا يمكن مكالمة جهة الاتصال</target> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve" approved="no"> + <source>Chat list</source> + <target state="translated">قائمة الدردشات</target> + </trans-unit> + <trans-unit id="Calls prohibited!" xml:space="preserve" approved="no"> + <source>Calls prohibited!</source> + <target state="translated">المكالمات ممنوعة!</target> + </trans-unit> + <trans-unit id="Change lock mode" xml:space="preserve" approved="no"> + <source>Change lock mode</source> + <target state="translated">تغيير وضع القفل</target> + </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve" approved="no"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <target state="translated">توقفت الدردشة. إذا كنت قد استخدمت قاعدة البيانات هذه بالفعل على جهاز آخر، فيجب عليك نقلها مرة أخرى قبل بدء الدردشة.</target> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve" approved="no"> + <source>Cellular</source> + <target state="translated">خلوي</target> + </trans-unit> + <trans-unit id="Chat" xml:space="preserve" approved="no"> + <source>Chat</source> + <target state="translated">الدردشة</target> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve" approved="no"> + <source>Chat already exists!</source> + <target state="translated">الدردشة موجودة بالفعل!</target> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve" approved="no"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <target state="translated">سيتم حذف الدردشة لديك - لا يمكن التراجع عن هذا!</target> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve" approved="no"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <target state="translated">سيتم حذف الدردشة لجميع الأعضاء - لا يمكن التراجع عن هذا!</target> + </trans-unit> + <trans-unit id="Change self-destruct passcode" xml:space="preserve" approved="no"> + <source>Change self-destruct passcode</source> + <target state="translated">تغيير رمز المرور التدمير الذاتي</target> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve" approved="no"> + <source>Camera not available</source> + <target state="translated">الكاميرا غير متوفرة</target> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve" approved="no"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <target state="translated">تم تجاوز السعة - لم يتلق المُستلم الرسائل المُرسلة مسبقًا.</target> + </trans-unit> + <trans-unit id="Change passcode" xml:space="preserve" approved="no"> + <source>Change passcode</source> + <target state="translated">تغيير رمز المرور</target> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve" approved="no"> + <source>Chat colors</source> + <target state="translated">ألوان الدردشة</target> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve" approved="no"> + <source>Chat theme</source> + <target state="translated">سمة الدردشة</target> + </trans-unit> + <trans-unit id="Business address" xml:space="preserve" approved="no"> + <source>Business address</source> + <target state="translated">عنوان العمل التجاري</target> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve" approved="no"> + <source>Business chats</source> + <target state="translated">دردشات العمل التجاري</target> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve" approved="no"> + <source>Cancel migration</source> + <target state="translated">ألغِ الترحيل</target> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve" approved="no"> + <source>Change chat profiles</source> + <target state="translated">غيّر ملفات تعريف الدردشة</target> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve" approved="no"> + <source>Chat migrated!</source> + <target state="translated">رحّلت الدردشة!</target> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve" approved="no"> + <source>Chat profile</source> + <target state="translated">ملف تعريف الدردشة</target> + </trans-unit> + <trans-unit id="Contact deleted!" xml:space="preserve" approved="no"> + <source>Contact deleted!</source> + <target state="translated">حُذفت جهة الاتصال!</target> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve" approved="no"> + <source>Conditions of use</source> + <target state="translated">شروط الاستخدام</target> + </trans-unit> + <trans-unit id="Connecting" xml:space="preserve" approved="no"> + <source>Connecting</source> + <target state="translated">جارِ الاتصال</target> + </trans-unit> + <trans-unit id="Connect incognito" xml:space="preserve" approved="no"> + <source>Connect incognito</source> + <target state="translated">اتصال متخفي</target> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve" approved="no"> + <source>Created at</source> + <target state="translated">أُنشئ في</target> + </trans-unit> + <trans-unit id="Connect via contact address" xml:space="preserve" approved="no"> + <source>Connect via contact address</source> + <target state="translated">الاتصال عبر عنوان جهة الاتصال</target> + </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve" approved="no"> + <source>Connected servers</source> + <target state="translated">الخوادم المتصلة</target> + </trans-unit> + <trans-unit id="standard end-to-end encryption" xml:space="preserve" approved="no"> + <source>standard end-to-end encryption</source> + <target state="translated">التعمية القياسية بين الطرفين</target> + </trans-unit> + <trans-unit id="Delete up to 20 messages at once." xml:space="preserve" approved="no"> + <source>Delete up to 20 messages at once.</source> + <target state="translated">حذف ما يصل إلى 20 رسالة في آن واحد.</target> + </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve" approved="no"> + <source>Connect to your friends faster.</source> + <target state="translated">تواصل مع أصدقائك بشكل أسرع.</target> + </trans-unit> + <trans-unit id="Developer options" xml:space="preserve" approved="no"> + <source>Developer options</source> + <target state="translated">خيارات المطور</target> + </trans-unit> + <trans-unit id="Connect to yourself?" xml:space="preserve" approved="no"> + <source>Connect to yourself?</source> + <target state="translated">اتصل بنفسك؟</target> + </trans-unit> + <trans-unit id="Connect via one-time link" xml:space="preserve" approved="no"> + <source>Connect via one-time link</source> + <target state="translated">اتصال عبر رابط لمرة واحدة</target> + </trans-unit> + <trans-unit id="Connect to yourself? This is your own SimpleX address!" xml:space="preserve" approved="no"> + <source>Connect to yourself? +This is your own SimpleX address!</source> + <target state="translated">اتصل بنفسك؟ +هذا هو عنوان SimpleX الخاص بك!</target> + </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve" approved="no"> + <source>Connecting to contact, please wait or check later!</source> + <target state="translated">جارِ الاتصال بجهة الاتصال، يُرجى الانتظار أو التحقق لاحقًا!</target> + </trans-unit> + <trans-unit id="Database upgrade" xml:space="preserve" approved="no"> + <source>Database upgrade</source> + <target state="translated">ترقية قاعدة البيانات</target> + </trans-unit> + <trans-unit id="Create list" xml:space="preserve" approved="no"> + <source>Create list</source> + <target state="translated">أنشئ قائمة</target> + </trans-unit> + <trans-unit id="Create profile" xml:space="preserve" approved="no"> + <source>Create profile</source> + <target state="translated">إنشاء ملف تعريف</target> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve" approved="no"> + <source>Creating archive link</source> + <target state="translated">جارِ إنشاء رابط الأرشيف</target> + </trans-unit> + <trans-unit id="Details" xml:space="preserve" approved="no"> + <source>Details</source> + <target state="translated">التفاصيل</target> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve" approved="no"> + <source>Customize theme</source> + <target state="translated">تخصيص السمة</target> + </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve" approved="no"> + <source>Dark mode colors</source> + <target state="translated">ألوان الوضع الداكن</target> + </trans-unit> + <trans-unit id="Delete and notify contact" xml:space="preserve" approved="no"> + <source>Delete and notify contact</source> + <target state="translated">حذف وإشعار جهة الاتصال</target> + </trans-unit> + <trans-unit id="Deleted at: %@" xml:space="preserve" approved="no"> + <source>Deleted at: %@</source> + <target state="translated">حُذفت في: %@</target> + </trans-unit> + <trans-unit id="Detailed statistics" xml:space="preserve" approved="no"> + <source>Detailed statistics</source> + <target state="translated">إحصائيات مفصلة</target> + </trans-unit> + <trans-unit id="you are observer" xml:space="preserve" approved="no"> + <source>you are observer</source> + <target state="translated">أنت المراقب</target> + </trans-unit> + <trans-unit id="you" xml:space="preserve" approved="no"> + <source>you</source> + <target state="translated">أنت</target> + </trans-unit> + <trans-unit id="when IP hidden" xml:space="preserve" approved="no"> + <source>when IP hidden</source> + <target state="translated">عندما يكون IP مخفيًا</target> + </trans-unit> + <trans-unit id="video" xml:space="preserve" approved="no"> + <source>video</source> + <target state="translated">فيديو</target> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve" approved="no"> + <source>Clear or delete group?</source> + <target state="translated">مسح أو حذف المجموعة؟</target> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve" approved="no"> + <source>Clear private notes?</source> + <target state="translated">مسح الملاحظات الخاصة؟</target> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve" approved="no"> + <source>Community guidelines violation</source> + <target state="translated">انتهاك إرشادات المجتمع</target> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve" approved="no"> + <source>Connection not ready.</source> + <target state="translated">الاتصال غير جاهز.</target> + </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve" approved="no"> + <source>Connection requires encryption renegotiation.</source> + <target state="translated">يتطلب الاتصال إعادة التفاوض على التعمية.</target> + </trans-unit> + <trans-unit id="Contact is deleted." xml:space="preserve" approved="no"> + <source>Contact is deleted.</source> + <target state="translated">حُذفت جهة الاتصال.</target> + </trans-unit> + <trans-unit id="Contacts" xml:space="preserve" approved="no"> + <source>Contacts</source> + <target state="translated">جهات الاتصال</target> + </trans-unit> + <trans-unit id="Create SimpleX address" xml:space="preserve" approved="no"> + <source>Create SimpleX address</source> + <target state="translated">أنشئ عنوان SimpleX</target> + </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve" approved="no"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <target state="translated">لا يمكن تحميل نص الشروط الحالية، يمكنك مراجعة الشروط عبر هذا الرابط:</target> + </trans-unit> + <trans-unit id="Delete chat messages from your device." xml:space="preserve" approved="no"> + <source>Delete chat messages from your device.</source> + <target state="translated">احذف رسائل الدردشة من جهازك.</target> + </trans-unit> + <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve" approved="no"> + <source>Delete or moderate up to 200 messages.</source> + <target state="translated">حذف أو إشراف ما يصل إلى 200 رسالة.</target> + </trans-unit> + <trans-unit id="Delete profile" xml:space="preserve" approved="no"> + <source>Delete profile</source> + <target state="translated">حذف ملف التعريف</target> + </trans-unit> + <trans-unit id="Desktop devices" xml:space="preserve" approved="no"> + <source>Desktop devices</source> + <target state="translated">أجهزة سطح المكتب</target> + </trans-unit> + <trans-unit id="set new profile picture" xml:space="preserve" approved="no"> + <source>set new profile picture</source> + <target state="translated">عيّن صورة تعريفية جديدة</target> + </trans-unit> + <trans-unit id="weeks" xml:space="preserve" approved="no"> + <source>weeks</source> + <target state="translated">أسابيع</target> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve" approved="no"> + <source>Chunks uploaded</source> + <target state="translated">رُفع القطع</target> + </trans-unit> + <trans-unit id="Color mode" xml:space="preserve" approved="no"> + <source>Color mode</source> + <target state="translated">وضع اللون</target> + </trans-unit> + <trans-unit id="Created" xml:space="preserve" approved="no"> + <source>Created</source> + <target state="translated">أُنشئت</target> + </trans-unit> + <trans-unit id="Current Passcode" xml:space="preserve" approved="no"> + <source>Current Passcode</source> + <target state="translated">رمز المرور الحالي</target> + </trans-unit> + <trans-unit id="Custom time" xml:space="preserve" approved="no"> + <source>Custom time</source> + <target state="translated">وقت مخصّص</target> + </trans-unit> + <trans-unit id="Debug delivery" xml:space="preserve" approved="no"> + <source>Debug delivery</source> + <target state="translated">تسليم التصحيح</target> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve" approved="no"> + <source>Deleted</source> + <target state="translated">حُذفت</target> + </trans-unit> + <trans-unit id="Delete file" xml:space="preserve" approved="no"> + <source>Delete file</source> + <target state="translated">حذف الملف</target> + </trans-unit> + <trans-unit id="unknown status" xml:space="preserve" approved="no"> + <source>unknown status</source> + <target state="translated">حالة غير معروفة</target> + </trans-unit> + <trans-unit id="unknown servers" xml:space="preserve" approved="no"> + <source>unknown servers</source> + <target state="translated">خوادم غير معروفة</target> + </trans-unit> + <trans-unit id="Connect to yourself? This is your own one-time link!" xml:space="preserve" approved="no"> + <source>Connect to yourself? +This is your own one-time link!</source> + <target state="translated">اتصل بنفسك؟ +هذا هو الرابط الخاص بك لمرة واحدة!</target> + </trans-unit> + <trans-unit id="Connect with %@" xml:space="preserve" approved="no"> + <source>Connect with %@</source> + <target state="translated">الاتصال ب%@</target> + </trans-unit> + <trans-unit id="Connected desktop" xml:space="preserve" approved="no"> + <source>Connected desktop</source> + <target state="translated">سطح المكتب متصل</target> + </trans-unit> + <trans-unit id="Connected to desktop" xml:space="preserve" approved="no"> + <source>Connected to desktop</source> + <target state="translated">متصل بسطح المكتب</target> + </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve" approved="no"> + <source>Conversation deleted!</source> + <target state="translated">حُذفت المحادثة!</target> + </trans-unit> + <trans-unit id="Create a group using a random profile." xml:space="preserve" approved="no"> + <source>Create a group using a random profile.</source> + <target state="translated">أنشئ مجموعة باستخدام ملف تعريف عشوائي.</target> + </trans-unit> + <trans-unit id="Delete chat" xml:space="preserve" approved="no"> + <source>Delete chat</source> + <target state="translated">احذف الدردشة</target> + </trans-unit> + <trans-unit id="Delete chat profile" xml:space="preserve" approved="no"> + <source>Delete chat profile</source> + <target state="translated">حذف ملف تعريف الدردشة</target> + </trans-unit> + <trans-unit id="Delete chat?" xml:space="preserve" approved="no"> + <source>Delete chat?</source> + <target state="translated">حذف الدردشة؟</target> + </trans-unit> + <trans-unit id="Delete database from this device" xml:space="preserve" approved="no"> + <source>Delete database from this device</source> + <target state="translated">احذف قاعدة البيانات من هذا الجهاز</target> + </trans-unit> + <trans-unit id="Delivery" xml:space="preserve" approved="no"> + <source>Delivery</source> + <target state="translated">التوصيل</target> + </trans-unit> + <trans-unit id="Delivery receipts are disabled!" xml:space="preserve" approved="no"> + <source>Delivery receipts are disabled!</source> + <target state="translated">إيصالات التسليم مُعطَّلة!</target> + </trans-unit> + <trans-unit id="Connection terminated" xml:space="preserve" approved="no"> + <source>Connection terminated</source> + <target state="translated">انتهى الاتصال</target> + </trans-unit> + <trans-unit id="Create file" xml:space="preserve" approved="no"> + <source>Create file</source> + <target state="translated">إنشاء ملف</target> + </trans-unit> + <trans-unit id="Create group" xml:space="preserve" approved="no"> + <source>Create group</source> + <target state="translated">أنشئ مجموعة</target> + </trans-unit> + <trans-unit id="Database IDs and Transport isolation option." xml:space="preserve" approved="no"> + <source>Database IDs and Transport isolation option.</source> + <target state="translated">معرفات قاعدة البيانات وخيار عزل النقل.</target> + </trans-unit> + <trans-unit id="Database downgrade" xml:space="preserve" approved="no"> + <source>Database downgrade</source> + <target state="translated">الرجوع إلى إصدار سابق من قاعدة البيانات</target> + </trans-unit> + <trans-unit id="Delivery receipts!" xml:space="preserve" approved="no"> + <source>Delivery receipts!</source> + <target state="translated">إيصالات التسليم!</target> + </trans-unit> + <trans-unit id="Desktop address" xml:space="preserve" approved="no"> + <source>Desktop address</source> + <target state="translated">عنوان سطح المكتب</target> + </trans-unit> + <trans-unit id="updated profile" xml:space="preserve" approved="no"> + <source>updated profile</source> + <target state="translated">حدّثت ملف التعريف</target> + </trans-unit> + <trans-unit id="Connect to desktop" xml:space="preserve" approved="no"> + <source>Connect to desktop</source> + <target state="translated">اتصل بسطح المكتب</target> + </trans-unit> + <trans-unit id="Connecting to desktop" xml:space="preserve" approved="no"> + <source>Connecting to desktop</source> + <target state="translated">جار الاتصال بسطح المكتب</target> + </trans-unit> + <trans-unit id="Completed" xml:space="preserve" approved="no"> + <source>Completed</source> + <target state="translated">اكتملت</target> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve" approved="no"> + <source>Connection notifications</source> + <target state="translated">إشعارات الاتصال</target> + </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve" approved="no"> + <source>Connection and servers status.</source> + <target state="translated">حالة الاتصال والخوادم.</target> + </trans-unit> + <trans-unit id="Continue" xml:space="preserve" approved="no"> + <source>Continue</source> + <target state="translated">متابعة</target> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve" approved="no"> + <source>Connections</source> + <target state="translated">الاتصالات</target> + </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve" approved="no"> + <source>Content violates conditions of use</source> + <target state="translated">المحتوى ينتهك شروط الاستخدام</target> + </trans-unit> + <trans-unit id="Corner" xml:space="preserve" approved="no"> + <source>Corner</source> + <target state="translated">ركن</target> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve" approved="no"> + <source>Creating link…</source> + <target state="translated">جارِ إنشاء الرابط…</target> + </trans-unit> + <trans-unit id="Database ID: %d" xml:space="preserve" approved="no"> + <source>Database ID: %d</source> + <target state="translated">معرّف قاعدة البيانات: %d</target> + </trans-unit> + <trans-unit id="Decryption error" xml:space="preserve" approved="no"> + <source>Decryption error</source> + <target state="translated">خطأ في فك التعمية</target> + </trans-unit> + <trans-unit id="Delete report" xml:space="preserve" approved="no"> + <source>Delete report</source> + <target state="translated">احذف البلاغ</target> + </trans-unit> + <trans-unit id="Delete without notification" xml:space="preserve" approved="no"> + <source>Delete without notification</source> + <target state="translated">احذف دون إشعار</target> + </trans-unit> + <trans-unit id="Deleted at" xml:space="preserve" approved="no"> + <source>Deleted at</source> + <target state="translated">حُذفت في</target> + </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve" approved="no"> + <source>Clear group?</source> + <target state="translated">مسح المجموعة؟</target> + </trans-unit> + <trans-unit id="Compare file" xml:space="preserve" approved="no"> + <source>Compare file</source> + <target state="translated">قارن الملف</target> + </trans-unit> + <trans-unit id="Connect automatically" xml:space="preserve" approved="no"> + <source>Connect automatically</source> + <target state="translated">اتصل تلقائيًا</target> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve" approved="no"> + <source>Connection blocked</source> + <target state="translated">حُظر الاتصال</target> + </trans-unit> + <trans-unit id="unprotected" xml:space="preserve" approved="no"> + <source>unprotected</source> + <target state="translated">غير محمي</target> + </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve" approved="no"> + <source>Deletion errors</source> + <target state="translated">أخطاء الحذف</target> + </trans-unit> + <trans-unit id="Conditions will be accepted for enabled operators after 30 days." xml:space="preserve" approved="no"> + <source>Conditions will be accepted for enabled operators after 30 days.</source> + <target state="translated">سيتم قبول الشروط للمُشغلين المفعّلين بعد 30 يومًا.</target> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve" approved="no"> + <source>Connection security</source> + <target state="translated">أمان الاتصال</target> + </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve" approved="no"> + <source>Contact will be deleted - this cannot be undone!</source> + <target state="translated">سيتم حذف جهة الاتصال - لا يمكن التراجع عن هذا!</target> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve" approved="no"> + <source>Copy error</source> + <target state="translated">خطأ في النسخ</target> + </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve" approved="no"> + <source>Create 1-time link</source> + <target state="translated">أنشئ رابط لمرة واحدة</target> + </trans-unit> + <trans-unit id="Connected" xml:space="preserve" approved="no"> + <source>Connected</source> + <target state="translated">متصل</target> + </trans-unit> + <trans-unit id="Current profile" xml:space="preserve" approved="no"> + <source>Current profile</source> + <target state="translated">ملف التعريف الحالي</target> + </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve" approved="no"> + <source>Customizable message shape.</source> + <target state="translated">شكل الرسالة قابل للتخصيص.</target> + </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve" approved="no"> + <source>Chunks deleted</source> + <target state="translated">حُذفت القطع</target> + </trans-unit> + <trans-unit id="Chinese and Spanish interface" xml:space="preserve" approved="no"> + <source>Chinese and Spanish interface</source> + <target state="translated">الواجهة الصينية والاسبانية</target> + </trans-unit> + <trans-unit id="Download" xml:space="preserve" approved="no"> + <source>Download</source> + <target state="translated">نزّل</target> + </trans-unit> + <trans-unit id="Downloaded" xml:space="preserve" approved="no"> + <source>Downloaded</source> + <target state="translated">نُزّلت</target> + </trans-unit> + <trans-unit id="Downloaded files" xml:space="preserve" approved="no"> + <source>Downloaded files</source> + <target state="translated">الملفات التي نُزّلت</target> + </trans-unit> + <trans-unit id="Don't show again" xml:space="preserve" approved="no"> + <source>Don't show again</source> + <target state="translated">لا تُظهر مرة أخرى</target> + </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve" approved="no"> + <source>Confirm contact deletion?</source> + <target state="translated">تأكيد حذف جهة الاتصال؟</target> + </trans-unit> + <trans-unit id="Confirm database upgrades" xml:space="preserve" approved="no"> + <source>Confirm database upgrades</source> + <target state="translated">تأكيد ترقيات قاعدة البيانات</target> + </trans-unit> + <trans-unit id="Download failed" xml:space="preserve" approved="no"> + <source>Download failed</source> + <target state="translated">فشل التنزيل</target> + </trans-unit> + <trans-unit id="Download file" xml:space="preserve" approved="no"> + <source>Download file</source> + <target state="translated">نزّل الملف</target> + </trans-unit> + <trans-unit id="Downloading link details" xml:space="preserve" approved="no"> + <source>Downloading link details</source> + <target state="translated">جارِ تنزيل تفاصيل الرابط</target> + </trans-unit> + <trans-unit id="Downloading archive" xml:space="preserve" approved="no"> + <source>Downloading archive</source> + <target state="translated">جارِ تنزيل الأرشيف</target> + </trans-unit> + <trans-unit id="Don't enable" xml:space="preserve" approved="no"> + <source>Don't enable</source> + <target state="translated">لا تُفعل</target> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve" approved="no"> + <source>Confirm upload</source> + <target state="translated">أكّد الرفع</target> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve" approved="no"> + <source>Chunks downloaded</source> + <target state="translated">نُزّلت القطع</target> + </trans-unit> + <trans-unit id="Confirm Passcode" xml:space="preserve" approved="no"> + <source>Confirm Passcode</source> + <target state="translated">تأكيد رمز المرور</target> + </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve" approved="no"> + <source>Confirm files from unknown servers.</source> + <target state="translated">تأكيد الملفات من خوادم غير معروفة.</target> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve" approved="no"> + <source>Confirm network settings</source> + <target state="translated">أكّد إعدادات الشبكة</target> + </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve" approved="no"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <target state="translated">تأكد من أنك تتذكر عبارة مرور قاعدة البيانات لترحيلها.</target> + </trans-unit> + <trans-unit id="Downgrade and open chat" xml:space="preserve" approved="no"> + <source>Downgrade and open chat</source> + <target state="translated">الرجوع إلى إصدار سابق وفتح الدردشة</target> + </trans-unit> + <trans-unit id="Don't miss important messages." xml:space="preserve" approved="no"> + <source>Don't miss important messages.</source> + <target state="translated">لا تفوت رسائل مهمة.</target> + </trans-unit> + <trans-unit id="E2E encrypted notifications." xml:space="preserve" approved="no"> + <source>E2E encrypted notifications.</source> + <target state="translated">إشعارات مُشفرة بين الطرفين E2E</target> + </trans-unit> + <trans-unit id="Download errors" xml:space="preserve" approved="no"> + <source>Download errors</source> + <target state="translated">أخطاء التنزيل</target> + </trans-unit> + <trans-unit id="Download files" xml:space="preserve" approved="no"> + <source>Download files</source> + <target state="translated">نزّل الملفات</target> + </trans-unit> + <trans-unit id="Confirm password" xml:space="preserve" approved="no"> + <source>Confirm password</source> + <target state="translated">تأكيد كلمة المرور</target> + </trans-unit> + <trans-unit id="Enable self-destruct" xml:space="preserve" approved="no"> + <source>Enable self-destruct</source> + <target state="translated">تفعيل التدمير الذاتي</target> + </trans-unit> + <trans-unit id="Enable (keep overrides)" xml:space="preserve" approved="no"> + <source>Enable (keep overrides)</source> + <target state="translated">تفعيل (الاحتفاظ بالتجاوزات)</target> + </trans-unit> + <trans-unit id="Enable Flux" xml:space="preserve" approved="no"> + <source>Enable Flux</source> + <target state="translated">فعّل flux</target> + </trans-unit> + <trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve" approved="no"> + <source>Enable in direct chats (BETA)!</source> + <target state="translated">فعّل في الدردشات المباشرة (تجريبي)!</target> + </trans-unit> + <trans-unit id="Enable for all" xml:space="preserve" approved="no"> + <source>Enable for all</source> + <target state="translated">تفعيل للجميع</target> + </trans-unit> + <trans-unit id="Enable lock" xml:space="preserve" approved="no"> + <source>Enable lock</source> + <target state="translated">تفعيل القفل</target> + </trans-unit> + <trans-unit id="Enable camera access" xml:space="preserve" approved="no"> + <source>Enable camera access</source> + <target state="translated">فعّل الوصول إلى الكاميرا</target> + </trans-unit> + <trans-unit id="Enable self-destruct passcode" xml:space="preserve" approved="no"> + <source>Enable self-destruct passcode</source> + <target state="translated">تفعيل رمز التدمير الذاتي</target> + </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve" approved="no"> + <source>Can't message member</source> + <target state="translated">لا يمكن الاتصال بالعضو</target> + </trans-unit> + <trans-unit id="Color chats with the new themes." xml:space="preserve" approved="no"> + <source>Color chats with the new themes.</source> + <target state="translated">محادثات ملونة مع السمات الجديدة.</target> + </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve" approved="no"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <target state="translated">ستتم إزالة جميع الدردشات من القائمة %@، وسيتم حذف القائمة.</target> + </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve" approved="no"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target state="translated">البلغارية والفنلندية والتايلاندية والأوكرانية - شكرًا للمستخدمين و[Weblate] (https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> + </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve" approved="no"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <target state="translated">اختر _الترحيل من جهاز آخر_ على الجهاز الجديد وامسح رمز الاستجابة السريعة.</target> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve" approved="no"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <target state="translated">سيتم قبول شروط المشغل (المشغلين): **%@**.</target> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve" approved="no"> + <source>Conditions will be accepted on: %@.</source> + <target state="translated">سيتم قبول الشروط على: %@.</target> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve" approved="no"> + <source>Confirmed</source> + <target state="translated">تم التأكيد</target> + </trans-unit> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve" approved="no"> + <source>Connection is blocked by server operator: +%@</source> + <target state="translated">تم حظر الاتصال من قبل مشغل الخادم: +%@</target> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve" approved="no"> + <source>Can't call member</source> + <target state="translated">لا يمكن الاتصال بالعضو</target> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve" approved="no"> + <source>Chat already exists</source> + <target state="translated">الدردشة موجودة بالفعل</target> + </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve" approved="no"> + <source>Check messages every 20 min.</source> + <target state="translated">تحقق من الرسائل كل 20 دقيقة.</target> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve" approved="no"> + <source>Check messages when allowed.</source> + <target state="translated">تحقق من الرسائل عندما يُسمح بذلك.</target> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve" approved="no"> + <source>Cannot forward message</source> + <target state="translated">لا يمكن إعادة توجيه الرسالة</target> + </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve" approved="no"> + <source>Chat preferences were changed.</source> + <target state="translated">تم تغيير تفضيلات المحادثة.</target> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve" approved="no"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <target state="translated">الشروط مقبولة بالفعل لهذا المشغل (المشغلين): **%@**.</target> + </trans-unit> + <trans-unit id="Conditions will be accepted for operator(s): **%@**." xml:space="preserve" approved="no"> + <source>Conditions will be accepted for operator(s): **%@**.</source> + <target state="translated">سيتم قبول شروط المشغل (المشغلين): **%@**.</target> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve" approved="no"> + <source>Conditions accepted on: %@.</source> + <target state="translated">الشروط المقبولة على: %@.</target> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve" approved="no"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <target state="translated">يتم قبول شروط المشغل (المشغلين): **%@**.</target> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve" approved="no"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <target state="translated">سيتم قبول الشروط تلقائيًا للمشغلين الممكّنين على: %@.</target> + </trans-unit> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve" approved="no"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <target state="translated">أنشئ ملفًا شخصيًا جديدًا في [تطبيق سطح المكتب](https://simplex.chat/downloads/). 💻</target> + </trans-unit> + <trans-unit id="Error adding server" xml:space="preserve" approved="no"> + <source>Error adding server</source> + <target state="translated">خطأ في إضافة الخادم</target> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve" approved="no"> + <source>Created at: %@</source> + <target state="translated">تم الإنشاء في: %@</target> + </trans-unit> + <trans-unit id="Delete %lld messages of members?" xml:space="preserve" approved="no"> + <source>Delete %lld messages of members?</source> + <target state="translated">حذف %lld الرسائل القديمة للأعضاء؟</target> + </trans-unit> + <trans-unit id="Disappearing message" xml:space="preserve" approved="no"> + <source>Disappearing message</source> + <target state="translated">رسالة اختفاء</target> + </trans-unit> + <trans-unit id="Enabled" xml:space="preserve" approved="no"> + <source>Enabled</source> + <target state="translated">ممكّنة</target> + </trans-unit> + <trans-unit id="Encrypted message: database migration error" xml:space="preserve" approved="no"> + <source>Encrypted message: database migration error</source> + <target state="translated">رسالة مشفرة: خطأ في ترحيل قاعدة البيانات</target> + </trans-unit> + <trans-unit id="Delete list?" xml:space="preserve" approved="no"> + <source>Delete list?</source> + <target state="translated">Delete list?</target> + </trans-unit> + <trans-unit id="Delivered even when Apple drops them." xml:space="preserve" approved="no"> + <source>Delivered even when Apple drops them.</source> + <target state="translated">يتم تسليمها حتى عندما تسقطها شركة Apple.</target> + </trans-unit> + <trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve" approved="no"> + <source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source> + <target state="translated">عنوان خادم الوجهة %@ غير متوافق مع إعدادات خادم التوجيه %@.</target> + </trans-unit> + <trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve" approved="no"> + <source>Destination server version of %@ is incompatible with forwarding server %@.</source> + <target state="translated">إصدار خادم الوجهة لـ %@ غير متوافق مع خادم التوجيه %@.</target> + </trans-unit> + <trans-unit id="Don't create address" xml:space="preserve" approved="no"> + <source>Don't create address</source> + <target state="translated">لا تنشئ عنوان</target> + </trans-unit> + <trans-unit id="Done" xml:space="preserve" approved="no"> + <source>Done</source> + <target state="translated">تم</target> + </trans-unit> + <trans-unit id="Duration" xml:space="preserve" approved="no"> + <source>Duration</source> + <target state="translated">المدة</target> + </trans-unit> + <trans-unit id="Encrypt local files" xml:space="preserve" approved="no"> + <source>Encrypt local files</source> + <target state="translated">تشفير الملفات المحلية</target> + </trans-unit> + <trans-unit id="Encryption renegotiation in progress." xml:space="preserve" approved="no"> + <source>Encryption renegotiation in progress.</source> + <target state="translated">إعادة التفاوض على التشفير قيد التنفيذ.</target> + </trans-unit> + <trans-unit id="Enter Passcode" xml:space="preserve" approved="no"> + <source>Enter Passcode</source> + <target state="translated">أدخل رمز المرور</target> + </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve" approved="no"> + <source>Enter passphrase</source> + <target state="translated">قم بأدخل عبارة المرور</target> + </trans-unit> + <trans-unit id="Enter welcome message…" xml:space="preserve" approved="no"> + <source>Enter welcome message…</source> + <target state="translated">أدخل رسالة ترحيب…</target> + </trans-unit> + <trans-unit id="Enter your name…" xml:space="preserve" approved="no"> + <source>Enter your name…</source> + <target state="translated">أدخل اسمك…</target> + </trans-unit> + <trans-unit id="Error changing to incognito!" xml:space="preserve" approved="no"> + <source>Error changing to incognito!</source> + <target state="translated">خطأ في التغيير إلى التصفح المتخفي!</target> + </trans-unit> + <trans-unit id="Delete %lld messages?" xml:space="preserve" approved="no"> + <source>Delete %lld messages?</source> + <target state="translated">حذف %lld رسائل؟</target> + </trans-unit> + <trans-unit id="Error aborting address change" xml:space="preserve" approved="no"> + <source>Error aborting address change</source> + <target state="translated">خطأ في إجهاض تغيير العنوان</target> + </trans-unit> + <trans-unit id="Disappears at" xml:space="preserve" approved="no"> + <source>Disappears at</source> + <target state="translated">يختفي عند</target> + </trans-unit> + <trans-unit id="Do not use credentials with proxy." xml:space="preserve" approved="no"> + <source>Do not use credentials with proxy.</source> + <target state="translated">لا تستخدم بيانات الاعتماد مع البروكسي.</target> + </trans-unit> + <trans-unit id="Error accepting conditions" xml:space="preserve" approved="no"> + <source>Error accepting conditions</source> + <target state="translated">خطأ في قبول الشروط</target> + </trans-unit> + <trans-unit id="Enter password above to show!" xml:space="preserve" approved="no"> + <source>Enter password above to show!</source> + <target state="translated">أدخل كلمة المرور أعلاه للعرض!</target> + </trans-unit> + <trans-unit id="Error changing connection profile" xml:space="preserve" approved="no"> + <source>Error changing connection profile</source> + <target state="translated">خطأ في تغيير ملف تعريف الاتصال</target> + </trans-unit> + <trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve" approved="no"> + <source>Desktop app version %@ is not compatible with this app.</source> + <target state="translated">إصدار تطبيق سطح المكتب %@ غير متوافق مع هذا التطبيق.</target> + </trans-unit> + <trans-unit id="Encrypt stored files & media" xml:space="preserve" approved="no"> + <source>Encrypt stored files & media</source> + <target state="translated">تشفير الملفات والوسائط المخزنة</target> + </trans-unit> + <trans-unit id="Enter this device name…" xml:space="preserve" approved="no"> + <source>Enter this device name…</source> + <target state="translated">أدخل اسم الجهاز…</target> + </trans-unit> + <trans-unit id="Enter welcome message… (optional)" xml:space="preserve" approved="no"> + <source>Enter welcome message… (optional)</source> + <target state="translated">أدخل رسالة ترحيب... (اختياري)</target> + </trans-unit> + <trans-unit id="Correct name to %@?" xml:space="preserve" approved="no"> + <source>Correct name to %@?</source> + <target state="translated">الاسم الصحيح ل %@؟</target> + </trans-unit> + <trans-unit id="Delete member message?" xml:space="preserve" approved="no"> + <source>Delete member message?</source> + <target state="translated">حذف رسالة العضو؟</target> + </trans-unit> + <trans-unit id="Disable automatic message deletion?" xml:space="preserve" approved="no"> + <source>Disable automatic message deletion?</source> + <target state="translated">تعطيل حذف الرسائل التلقائي؟</target> + </trans-unit> + <trans-unit id="Disable delete messages" xml:space="preserve" approved="no"> + <source>Disable delete messages</source> + <target state="translated">تعطيل حذف الرسائل</target> + </trans-unit> + <trans-unit id="Disable for all" xml:space="preserve" approved="no"> + <source>Disable for all</source> + <target state="translated">تعطيل للجميع</target> + </trans-unit> + <trans-unit id="Disabled" xml:space="preserve" approved="no"> + <source>Disabled</source> + <target state="translated">عاجز</target> + </trans-unit> + <trans-unit id="Documents:" xml:space="preserve" approved="no"> + <source>Documents:</source> + <target state="translated">المستندات:</target> + </trans-unit> + <trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve" approved="no"> + <source>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</source> + <target state="translated">باستخدامك SimpleX Chat، فإنك توافق على: +- إرسال محتوى قانوني فقط في المجموعات العامة. +- احترام المستخدمين الآخرين - ممنوع إرسال رسائل مزعجة.</target> + </trans-unit> + <trans-unit id="Configure server operators" xml:space="preserve" approved="no"> + <source>Configure server operators</source> + <target state="translated">تكوين مشغلي الخادم</target> + </trans-unit> + <trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve" approved="no"> + <source>Enable Flux in Network & servers settings for better metadata privacy.</source> + <target state="translated">تمكين التدفق في إعدادات الشبكة والخوادم لتحسين خصوصية البيانات الوصفية.</target> + </trans-unit> + <trans-unit id="Discover and join groups" xml:space="preserve" approved="no"> + <source>Discover and join groups</source> + <target state="translated">اكتشف المجموعات وانضم إليها</target> + </trans-unit> + <trans-unit id="Discover via local network" xml:space="preserve" approved="no"> + <source>Discover via local network</source> + <target state="translated">اكتشف عبر الشبكة المحلية</target> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve" approved="no"> + <source>Enabled for</source> + <target state="translated">ممكّن ل</target> + </trans-unit> + <trans-unit id="Encrypted message: app is stopped" xml:space="preserve" approved="no"> + <source>Encrypted message: app is stopped</source> + <target state="translated">رسالة مشفرة: تم إيقاف التطبيق</target> + </trans-unit> + <trans-unit id="Enter group name…" xml:space="preserve" approved="no"> + <source>Enter group name…</source> + <target state="translated">أدخل اسم المجموعة…</target> + </trans-unit> + <trans-unit id="Do NOT use private routing." xml:space="preserve" approved="no"> + <source>Do NOT use private routing.</source> + <target state="translated">لا تستخدم التوجيه الخاص.</target> + </trans-unit> + <trans-unit id="Encryption re-negotiation error" xml:space="preserve" approved="no"> + <source>Encryption re-negotiation error</source> + <target state="translated">خطأ في إعادة تفاوض التشفير</target> + </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve" approved="no"> + <source>Connection with desktop stopped</source> + <target state="translated">تم إيقاف الاتصال بسطح المكتب</target> + </trans-unit> + <trans-unit id="Destination server error: %@" xml:space="preserve" approved="no"> + <source>Destination server error: %@</source> + <target state="translated">خطأ خادم الوجهة: %@</target> + </trans-unit> + <trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve" approved="no"> + <source>Do NOT send messages directly, even if your or destination server does not support private routing.</source> + <target state="translated">لا ترسل الرسائل بشكل مباشر، حتى لو كان خادمك أو خادم الوجهة لا يدعم التوجيه الخاص.</target> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve" approved="no"> + <source>Direct messages between members are prohibited in this chat.</source> + <target state="translated">يُحظر إرسال الرسائل المباشرة بين الأعضاء في هذه الدردشة.</target> + </trans-unit> + <trans-unit id="Disconnect desktop?" xml:space="preserve" approved="no"> + <source>Disconnect desktop?</source> + <target state="translated">فصل سطح المكتب؟</target> + </trans-unit> + <trans-unit id="Disable (keep overrides)" xml:space="preserve" approved="no"> + <source>Disable (keep overrides)</source> + <target state="translated">تعطيل (الاحتفاظ بالتجاوزات)</target> + </trans-unit> + <trans-unit id="Disappears at: %@" xml:space="preserve" approved="no"> + <source>Disappears at: %@</source> + <target state="translated">يختفي عند: %@</target> + </trans-unit> + <trans-unit id="Do not send history to new members." xml:space="preserve" approved="no"> + <source>Do not send history to new members.</source> + <target state="translated">لا ترسل التاريخ إلى الأعضاء الجدد.</target> + </trans-unit> + <trans-unit id="Encryption re-negotiation failed." xml:space="preserve" approved="no"> + <source>Encryption re-negotiation failed.</source> + <target state="translated">فشل إعادة التفاوض على التشفير.</target> + </trans-unit> </body> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="ar" datatype="plaintext"> @@ -3723,4 +5805,80 @@ SimpleX servers cannot see your profile.</source> </trans-unit> </body> </file> + <file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="ar" datatype="plaintext"> + <body> + <trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve" approved="no"> + <source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source> + <target state="needs-translation">يمكنك السماح بالمشاركة في إعدادات الخصوصية والأمان / اعدادات "SimpleX Lock"</target> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve" approved="no"> + <source>Keychain error</source> + <target state="translated">خطأ في Keychain</target> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve" approved="no"> + <source>Invalid migration confirmation</source> + <target state="translated">تأكيد الترحيل غير صالح</target> + </trans-unit> + <trans-unit id="%@" xml:space="preserve" approved="no"> + <source>%@</source> + <target state="translated">%@</target> + </trans-unit> + <trans-unit id="Share" xml:space="preserve" approved="no"> + <source>Share</source> + <target state="translated">مشاركة</target> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve" approved="no"> + <source>Incompatible database version</source> + <target state="translated">إصدار قاعدة بيانات غير متوافق</target> + </trans-unit> + <trans-unit id="File error" xml:space="preserve" approved="no"> + <source>File error</source> + <target state="translated">خطأ في الملف</target> + </trans-unit> + <trans-unit id="Database downgrade required" xml:space="preserve" approved="no"> + <source>Database downgrade required</source> + <target state="translated">مطلوب الرجوع إلى إصدار سابق من قاعدة البيانات‎</target> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve" approved="no"> + <source>Database encrypted!</source> + <target state="translated">قاعدة البيانات مُعمّاة!</target> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve" approved="no"> + <source>Wrong database passphrase</source> + <target state="translated">عبارة مرور قاعدة بيانات خاطئة</target> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve" approved="no"> + <source>Selected chat preferences prohibit this message.</source> + <target state="translated">تفضيلات الدردشة المحدّدة تحظر هذه الرسالة.</target> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve" approved="no"> + <source>Database error</source> + <target state="translated">خطأ في قاعدة البيانات</target> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve" approved="no"> + <source>Database passphrase is required to open chat.</source> + <target state="translated">عبارة مرور قاعدة البيانات مطلوبة لفتح الدردشة.</target> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve" approved="no"> + <source>Error: %@</source> + <target state="translated">خطأ: %@</target> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve" approved="no"> + <source>Cancel</source> + <target state="translated">إلغاء</target> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve" approved="no"> + <source>Large file!</source> + <target state="translated">الملف كبير!</target> + </trans-unit> + </body> + </file> + <file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="ar" datatype="plaintext"> + <body> + <trans-unit id="From: %@" xml:space="preserve" approved="no"> + <source>From: %@</source> + <target state="translated">من: %@</target> + </trans-unit> + </body> + </file> </xliff> 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 7a2afea082..776199ac1f 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -2,36 +2,9 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="bg" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> - <trans-unit id=" " xml:space="preserve"> - <source> -</source> - <target> -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (" xml:space="preserve"> - <source> (</source> - <target> (</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> <target> (може да се копира)</target> @@ -89,6 +62,7 @@ </trans-unit> <trans-unit id="%@ and %@" xml:space="preserve"> <source>%@ and %@</source> + <target>%@ и %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ and %@ connected" xml:space="preserve"> @@ -103,6 +77,12 @@ </trans-unit> <trans-unit id="%@ connected" xml:space="preserve"> <source>%@ connected</source> + <target>%@ свързан</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve"> + <source>%@ downloaded</source> + <target>%@ изтеглено</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ is connected!" xml:space="preserve"> @@ -120,9 +100,17 @@ <target>%@ е потвърдено</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ server" xml:space="preserve"> + <source>%@ server</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ servers" xml:space="preserve"> <source>%@ servers</source> - <target>%@ сървъри</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve"> + <source>%@ uploaded</source> + <target>%@ качено</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ wants to connect!" xml:space="preserve"> @@ -130,8 +118,14 @@ <target>%@ иска да се свърже!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve"> + <source>%1$@, %2$@</source> + <target>%1$@, %2$@</target> + <note>format for date separator in chat</note> + </trans-unit> <trans-unit id="%@, %@ and %lld members" xml:space="preserve"> <source>%@, %@ and %lld members</source> + <target>%@, %@ и %lld членове</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> @@ -149,11 +143,31 @@ <target>%d дни</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve"> + <source>%d file(s) are still being downloaded.</source> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve"> + <source>%d file(s) failed to download.</source> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve"> + <source>%d file(s) were deleted.</source> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve"> + <source>%d file(s) were not downloaded.</source> + <note>forward confirmation reason</note> + </trans-unit> <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> <target>%d часа</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve"> + <source>%d messages not forwarded</source> + <note>alert title</note> + </trans-unit> <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> <target>%d мин.</target> @@ -169,6 +183,10 @@ <target>%d сек.</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve"> + <source>%d seconds(s)</source> + <note>delete after time</note> + </trans-unit> <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> <target>%d пропуснато(и) съобщение(я)</target> @@ -201,6 +219,7 @@ </trans-unit> <trans-unit id="%lld group events" xml:space="preserve"> <source>%lld group events</source> + <target>%lld групови събития</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld members" xml:space="preserve"> @@ -210,14 +229,22 @@ </trans-unit> <trans-unit id="%lld messages blocked" xml:space="preserve"> <source>%lld messages blocked</source> + <target>%lld блокирани съобщения</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve"> + <source>%lld messages blocked by admin</source> + <target>%lld съобщения, блокирани от администратора</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld messages marked deleted" xml:space="preserve"> <source>%lld messages marked deleted</source> + <target>%lld съобщения, маркирани като изтрити</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld messages moderated by %@" xml:space="preserve"> <source>%lld messages moderated by %@</source> + <target>%lld съобщения, модерирани от %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld minutes" xml:space="preserve"> @@ -230,11 +257,6 @@ <target>%lld нови езици на интерфейса</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> - <source>%lld second(s)</source> - <target>%lld секунда(и)</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="%lld seconds" xml:space="preserve"> <source>%lld seconds</source> <target>%lld секунди</target> @@ -285,47 +307,39 @@ <target>%u пропуснати съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> - <source>(</source> - <target>(</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="(new)" xml:space="preserve"> <source>(new)</source> + <target>(ново)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="(this device v%@)" xml:space="preserve"> <source>(this device v%@)</source> + <target>(това устройство v%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve"> - <source>)</source> - <target>)</target> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target>**Добави контакт**: за създаване на нов линк.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target>**Добави нов контакт**: за да създадете своя еднократен QR код или линк за вашия контакт.</target> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve"> + <source>**Create group**: to create a new group.</source> + <target>**Създай група**: за създаване на нова група.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> - <source>**Create link / QR code** for your contact to use.</source> - <target>**Създай линк / QR код**, който вашият контакт да използва.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**По поверително**: проверявайте новите съобщения на всеки 20 минути. Токенът на устройството се споделя със сървъра за чат SimpleX, но не и колко контакти или съобщения имате.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Най-поверително**: не използвайте сървъра за известия SimpleX Chat, периодично проверявайте съобщенията във фонов режим (зависи от това колко често използвате приложението).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <target>**Поставете получения линк** или го отворете в браузъра и докоснете **Отваряне в мобилно приложение**.</target> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> + <target>**Моля, обърнете внимание**: използването на една и съща база данни на две устройства ще наруши декриптирането на съобщенията от вашите връзки като защита на сигурността.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> @@ -333,14 +347,13 @@ <target>**Моля, обърнете внимание**: НЯМА да можете да възстановите или промените паролата, ако я загубите.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Препоръчително**: токенът на устройството и известията се изпращат до сървъра за уведомяване на SimpleX Chat, но не и съдържанието, размерът на съобщението или от кого е.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target>**Сканирай QR код**: за да се свържете с вашия контакт лично или чрез видеообаждане.</target> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve"> + <source>**Scan / Paste link**: to connect via a link you received.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> @@ -348,6 +361,11 @@ <target>**Внимание**: Незабавните push известия изискват парола, запазена в Keychain.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve"> + <source>**Warning**: the archive will be removed.</source> + <target>**Внимание**: архивът ще бъде изтрит.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> <source>**e2e encrypted** audio call</source> <target>**e2e криптиран**аудио разговор</target> @@ -363,16 +381,11 @@ <target>\*удебелен*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=", " xml:space="preserve"> - <source>, </source> - <target>, </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable.</source> - <target>- свържете се с [директория за услуги](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjd LW3%23%2F%3Fv%3D1-2%26dh %3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (БЕТА)! + <target>- свържете се с [директория за услуги](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (БЕТА)! - потвърждениe за доставка (до 20 члена). - по-бързо и по-стабилно.</target> <note>No comment provided by engineer.</note> @@ -390,6 +403,9 @@ <source>- optionally notify deleted contacts. - profile names with spaces. - and more!</source> + <target>- по желание уведомете изтритите контакти. +- имена на профили с интервали. +- и още!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="- voice messages up to 5 minutes. - custom time to disappear. - editing history." xml:space="preserve"> @@ -401,13 +417,9 @@ - история на редактиране.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="." xml:space="preserve"> - <source>.</source> - <target>.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="0 sec" xml:space="preserve"> <source>0 sec</source> + <target>0 сек</target> <note>time to disappear</note> </trans-unit> <trans-unit id="0s" xml:space="preserve"> @@ -418,7 +430,8 @@ <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> <target>1 ден</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> @@ -433,18 +446,28 @@ <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> <target>1 месец</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> <target>1 седмица</target> - <note>time interval</note> + <note>delete after time +time interval</note> + </trans-unit> + <trans-unit id="1 year" xml:space="preserve"> + <source>1 year</source> + <note>delete after time</note> </trans-unit> <trans-unit id="1-time link" xml:space="preserve"> <source>1-time link</source> <target>Еднократен линк</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="5 minutes" xml:space="preserve"> <source>5 minutes</source> <target>5 минути</target> @@ -460,11 +483,6 @@ <target>30 секунди</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve"> - <source>: </source> - <target>: </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> @@ -514,31 +532,32 @@ <target>Откажи смяна на адрес?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve"> - <source>About SimpleX</source> - <target>За SimpleX</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve"> <source>About SimpleX Chat</source> <target>За SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve"> - <source>About SimpleX address</source> - <target>Повече за SimpleX адреса</target> + <trans-unit id="About operators" xml:space="preserve"> + <source>About operators</source> + <target>За операторите</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accent color" xml:space="preserve"> - <source>Accent color</source> - <target>Основен цвят</target> + <trans-unit id="Accent" xml:space="preserve"> + <source>Accent</source> + <target>Акцент</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept" xml:space="preserve"> <source>Accept</source> <target>Приеми</target> <note>accept contact request via notification - accept incoming call via notification</note> +accept incoming call via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accept conditions" xml:space="preserve"> + <source>Accept conditions</source> + <target>Приеми условията</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept connection request?" xml:space="preserve"> <source>Accept connection request?</source> @@ -553,16 +572,45 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Приеми инкогнито</target> - <note>accept contact request via notification</note> + <note>accept contact request via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve"> + <source>Accepted conditions</source> + <target>Приети условия</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve"> + <source>Acknowledged</source> + <target>Потвърден</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve"> + <source>Acknowledgement errors</source> + <target>Грешки при потвърждението</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Active" xml:space="preserve"> + <source>Active</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve"> + <source>Active connections</source> + <target>Активни връзки</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> <target>Добавете адрес към вашия профил, така че вашите контакти да могат да го споделят с други хора. Актуализацията на профила ще бъде изпратена до вашите контакти.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> - <source>Add preset servers</source> - <target>Добави предварително зададени сървъри</target> + <trans-unit id="Add friends" xml:space="preserve"> + <source>Add friends</source> + <target>Добави приятели</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve"> + <source>Add list</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add profile" xml:space="preserve"> @@ -570,14 +618,19 @@ <target>Добави профил</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> + <target>Добави сървър</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> <source>Add servers by scanning QR codes.</source> <target>Добави сървъри чрез сканиране на QR кодове.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> - <target>Добави сървър…</target> + <trans-unit id="Add team members" xml:space="preserve"> + <source>Add team members</source> + <target>Добави членове на екипа</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -585,11 +638,45 @@ <target>Добави към друго устройство</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add to list" xml:space="preserve"> + <source>Add to list</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> <target>Добави съобщение при посрещане</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve"> + <source>Add your team members to the conversations.</source> + <target>Добавете членовете на вашия екип към разговорите.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve"> + <source>Added media & file servers</source> + <target>Добавени медийни и файлови сървъри</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve"> + <source>Added message servers</source> + <target>Добавени сървъри за съобщения</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve"> + <source>Additional accent</source> + <target>Допълнителен акцент</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve"> + <source>Additional accent 2</source> + <target>Допълнителен акцент 2</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve"> + <source>Additional secondary</source> + <target>Допълнителен вторичен</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Address" xml:space="preserve"> <source>Address</source> <target>Адрес</target> @@ -600,6 +687,21 @@ <target>Промяната на адреса ще бъде прекъсната. Ще се използва старият адрес за получаване.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve"> + <source>Address or 1-time link?</source> + <target>Адрес или еднократен линк?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve"> + <source>Address settings</source> + <target>Настройки на адреса</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve"> + <source>Admins can block a member for all.</source> + <target>Администраторите могат да блокират член за всички.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> <source>Admins can create the links to join groups.</source> <target>Админите могат да създадат линкове за присъединяване към групи.</target> @@ -610,6 +712,15 @@ <target>Разширени мрежови настройки</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve"> + <source>Advanced settings</source> + <target>Разширени настройки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All" xml:space="preserve"> + <source>All</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All app data is deleted." xml:space="preserve"> <source>All app data is deleted.</source> <target>Всички данни от приложението бяха изтрити.</target> @@ -620,16 +731,34 @@ <target>Всички чатове и съобщения ще бъдат изтрити - това не може да бъде отменено!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> <target>Всички данни се изтриват при въвеждане.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve"> + <source>All data is kept private on your device.</source> + <target>Всички данни се съхраняват поверително на вашето устройство.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> <target>Всички членове на групата ще останат свързани.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone!</source> + <target>Всички съобщения ще бъдат изтрити - това не може да бъде отменено!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> <target>Всички съобщения ще бъдат изтрити - това не може да бъде отменено! Съобщенията ще бъдат изтрити САМО за вас.</target> @@ -637,6 +766,20 @@ </trans-unit> <trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve"> <source>All new messages from %@ will be hidden!</source> + <target>Всички нови съобщения от %@ ще бъдат скрити!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All profiles" xml:space="preserve"> + <source>All profiles</source> + <target>Всички профили</target> + <note>profile dropdown</note> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve"> + <source>All reports will be archived for you.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All servers" xml:space="preserve"> + <source>All servers</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All your contacts will remain connected." xml:space="preserve"> @@ -649,6 +792,11 @@ <target>Всички ваши контакти ще останат свързани. Актуализацията на профила ще бъде изпратена до вашите контакти.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target>Всички ваши контакти, разговори и файлове ще бъдат сигурно криптирани и качени на парчета в конфигурираните XFTP релета.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow" xml:space="preserve"> <source>Allow</source> <target>Позволи</target> @@ -659,14 +807,24 @@ <target>Позволи обаждания само ако вашият контакт ги разрешава.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve"> + <source>Allow calls?</source> + <target>Позволи обаждания?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> <target>Позволи изчезващи съобщения само ако вашият контакт ги разрешава.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <target>Позволи необратимо изтриване на съобщение само ако вашият контакт го рарешава.</target> + <trans-unit id="Allow downgrade" xml:space="preserve"> + <source>Allow downgrade</source> + <target>Позволи понижаване</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target>Позволи необратимо изтриване на съобщение само ако вашият контакт го рарешава. (24 часа)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> @@ -689,9 +847,23 @@ <target>Разреши изпращането на изчезващи съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow to irreversibly delete sent messages.</source> - <target>Позволи необратимо изтриване на изпратените съобщения.</target> + <trans-unit id="Allow sharing" xml:space="preserve"> + <source>Allow sharing</source> + <target>Позволи споделяне</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target>Позволи необратимо изтриване на изпратените съобщения. (24 часа)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve"> + <source>Allow to report messsages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve"> + <source>Allow to send SimpleX links.</source> + <target>Разрешаване на изпращане на SimpleX линкове.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send files and media." xml:space="preserve"> @@ -724,9 +896,9 @@ <target>Позволи на вашите контакти да ви се обаждат.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow your contacts to irreversibly delete sent messages.</source> - <target>Позволи на вашите контакти да изтриват необратимо изпратените съобщения.</target> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target>Позволи на вашите контакти да изтриват необратимо изпратените съобщения. (24 часа)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> @@ -746,10 +918,17 @@ </trans-unit> <trans-unit id="Already connecting!" xml:space="preserve"> <source>Already connecting!</source> + <target>В процес на свързване!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Already joining the group!" xml:space="preserve"> <source>Already joining the group!</source> + <target>Вече се присъединихте към групата!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve"> + <source>Always use private routing.</source> + <target>Винаги използвай поверително рутиране.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> @@ -762,21 +941,39 @@ <target>Създаен беше празен профил за чат с предоставеното име и приложението се отвари както обикновено.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Another reason" xml:space="preserve"> + <source>Another reason</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> <target>Отговор на повикване</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> + <target>Протокол и код с отворен код – всеки може да оперира собствени сървъри.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> <target>Компилация на приложението: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App data migration" xml:space="preserve"> + <source>App data migration</source> + <target>Миграция на данните от приложението</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> <source>App encrypts new local files (except videos).</source> <target>Приложението криптира нови локални файлове (с изключение на видеоклипове).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App group:" xml:space="preserve"> + <source>App group:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>Икона на приложението</target> @@ -792,6 +989,11 @@ <target>Кода за достъп до приложение се заменя с код за самоунищожение.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App session" xml:space="preserve"> + <source>App session</source> + <target>Сесия на приложението</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App version" xml:space="preserve"> <source>App version</source> <target>Версия на приложението</target> @@ -807,6 +1009,60 @@ <target>Изглед</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Apply" xml:space="preserve"> + <source>Apply</source> + <target>Приложи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve"> + <source>Apply to</source> + <target>Приложи към</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve"> + <source>Archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve"> + <source>Archive %lld reports?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve"> + <source>Archive all reports?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve"> + <source>Archive and upload</source> + <target>Архивиране и качване</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve"> + <source>Archive contacts to chat later.</source> + <target>Архивирайте контактите, за да разговаряте по-късно.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve"> + <source>Archive report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve"> + <source>Archive report?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve"> + <source>Archive reports</source> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve"> + <source>Archived contacts</source> + <target>Архивирани контакти</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve"> + <source>Archiving database</source> + <target>Архивиране на база данни</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Attach" xml:space="preserve"> <source>Attach</source> <target>Прикачи</target> @@ -867,13 +1123,24 @@ <target>Автоматично приемане на изображения</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve"> + <source>Auto-accept settings</source> + <target>Автоматично приемане на настройки</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Back" xml:space="preserve"> <source>Back</source> <target>Назад</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Background" xml:space="preserve"> + <source>Background</source> + <target>Фон</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Bad desktop address" xml:space="preserve"> <source>Bad desktop address</source> + <target>Грешен адрес на настолното устройство</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Bad message ID" xml:space="preserve"> @@ -886,8 +1153,23 @@ <target>Лош хеш на съобщението</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better calls" xml:space="preserve"> + <source>Better calls</source> + <target>По-добри обаждания</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better groups" xml:space="preserve"> <source>Better groups</source> + <target>По-добри групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve"> + <source>Better groups performance</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve"> + <source>Better message dates.</source> + <target>По-добри дати на съобщението.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Better messages" xml:space="preserve"> @@ -895,20 +1177,78 @@ <target>По-добри съобщения</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better networking" xml:space="preserve"> + <source>Better networking</source> + <target>Подобрена мрежа</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve"> + <source>Better notifications</source> + <target>Подобрени известия</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve"> + <source>Better privacy and security</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve"> + <source>Better security ✅</source> + <target>По-добра сигурност ✅</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve"> + <source>Better user experience</source> + <target>Подобрен интерфейс</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Black" xml:space="preserve"> + <source>Black</source> + <target>Черна</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block" xml:space="preserve"> <source>Block</source> + <target>Блокирай</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block for all" xml:space="preserve"> + <source>Block for all</source> + <target>Блокирай за всички</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Block group members" xml:space="preserve"> <source>Block group members</source> + <target>Блокиране на членове на групата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Block member" xml:space="preserve"> <source>Block member</source> + <target>Блокирай член</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve"> + <source>Block member for all?</source> + <target>Блокиране на член за всички?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Block member?" xml:space="preserve"> <source>Block member?</source> + <target>Блокирай члена?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve"> + <source>Blocked by admin</source> + <target>Блокиран от админ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve"> + <source>Blur for better privacy.</source> + <target>Размазване за по-добра поверителност.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve"> + <source>Blur media</source> + <target>Размазване на медия</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> @@ -916,9 +1256,9 @@ <target>И вие, и вашият контакт можете да добавяте реакции към съобщението.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> - <source>Both you and your contact can irreversibly delete sent messages.</source> - <target>И вие, и вашият контакт можете да изтриете необратимо изпратените съобщения.</target> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target>И вие, и вашият контакт можете да изтриете необратимо изпратените съобщения. (24 часа)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> @@ -941,11 +1281,31 @@ <target>Български, финландски, тайландски и украински - благодарение на потребителите и [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Business address" xml:space="preserve"> + <source>Business address</source> + <target>Бизнес адрес</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve"> + <source>Business chats</source> + <target>Бизнес чатове</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve"> + <source>Businesses</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>Чрез чат профил (по подразбиране) или [чрез връзка](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve"> + <source>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> <source>Call already ended!</source> <target>Разговорът вече приключи!</target> @@ -956,9 +1316,24 @@ <target>Обаждания</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve"> - <source>Can't delete user profile!</source> - <target>Потребителският профил не може да се изтрие!</target> + <trans-unit id="Calls prohibited!" xml:space="preserve"> + <source>Calls prohibited!</source> + <target>Обажданията са забранени!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve"> + <source>Camera not available</source> + <target>Камерата е неодстъпна</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve"> + <source>Can't call contact</source> + <target>Обаждането на контакта не е позволено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve"> + <source>Can't call member</source> + <target>Обаждането на члена не е позволено</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contact!" xml:space="preserve"> @@ -971,9 +1346,19 @@ <target>Не може да поканят контактите!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve"> + <source>Can't message member</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> <target>Отказ</target> + <note>alert action +alert button</note> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve"> + <source>Cancel migration</source> + <target>Отмени миграцията</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> @@ -981,9 +1366,23 @@ <target>Няма достъп до Keychain за запазване на паролата за базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> <target>Файлът не може да бъде получен</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <target>Капацитетът е надвишен - получателят не е получил предишно изпратените съобщения.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve"> + <source>Cellular</source> + <target>Мобилна мрежа</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change" xml:space="preserve"> @@ -991,6 +1390,15 @@ <target>Промени</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve"> + <source>Change automatic message deletion?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve"> + <source>Change chat profiles</source> + <target>Промени чат профилите</target> + <note>authentication reason</note> + </trans-unit> <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> <target>Промяна на паролата на базата данни?</target> @@ -1035,11 +1443,22 @@ <source>Change self-destruct passcode</source> <target>Промени кода за достъп за самоунищожение</target> <note>authentication reason - set passcode view</note> +set passcode view</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> - <source>Chat archive</source> - <target>Архив на чата</target> + <trans-unit id="Chat" xml:space="preserve"> + <source>Chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve"> + <source>Chat already exists</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve"> + <source>Chat already exists!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve"> + <source>Chat colors</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat console" xml:space="preserve"> @@ -1049,7 +1468,7 @@ </trans-unit> <trans-unit id="Chat database" xml:space="preserve"> <source>Chat database</source> - <target>База данни за чата</target> + <target>База данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat database deleted" xml:space="preserve"> @@ -1057,9 +1476,13 @@ <target>Базата данни на чата е изтрита</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve"> + <source>Chat database exported</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> - <target>Базата данни на чат е импортирана</target> + <target>Базата данни на е импортирана</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat is running" xml:space="preserve"> @@ -1072,26 +1495,74 @@ <target>Чатът е спрян</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <target>Чатът е спрян. Ако вече сте използвали тази база данни на друго устройство, трябва да я прехвърлите обратно, преди да стартирате чата отново.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve"> + <source>Chat list</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve"> + <source>Chat migrated!</source> + <target>Чатът е мигриран!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> <target>Чат настройки</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve"> + <source>Chat preferences were changed.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve"> + <source>Chat profile</source> + <target>Потребителски профил</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve"> + <source>Chat theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> <target>Чатове</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve"> + <source>Check messages every 20 min.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve"> + <source>Check messages when allowed.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> <target>Проверете адреса на сървъра и опитайте отново.</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> <target>Китайски и Испански интерфейс</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <target>Изберете _Мигриране от друго устройство_ на новото устройство и сканирайте QR кода.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> <target>Избери файл</target> @@ -1102,10 +1573,22 @@ <target>Избери от библиотеката</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve"> + <source>Chunks deleted</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve"> + <source>Chunks downloaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve"> + <source>Chunks uploaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> <target>Изчисти</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> @@ -1117,16 +1600,36 @@ <target>Изчисти разговора?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve"> + <source>Clear group?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve"> + <source>Clear or delete group?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve"> + <source>Clear private notes?</source> + <target>Изчистване на лични бележки?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear verification" xml:space="preserve"> <source>Clear verification</source> <target>Изчисти проверката</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> - <source>Colors</source> - <target>Цветове</target> + <trans-unit id="Color chats with the new themes." xml:space="preserve"> + <source>Color chats with the new themes.</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Color mode" xml:space="preserve"> + <source>Color mode</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve"> + <source>Community guidelines violation</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Compare file" xml:space="preserve"> <source>Compare file</source> <target>Сравни файл</target> @@ -1137,11 +1640,47 @@ <target>Сравнете кодовете за сигурност с вашите контакти.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Completed" xml:space="preserve"> + <source>Completed</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve"> + <source>Conditions accepted on: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve"> + <source>Conditions of use</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve"> + <source>Conditions will be accepted on: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> <target>Конфигурирай ICE сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Configure server operators" xml:space="preserve"> + <source>Configure server operators</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> <target>Потвърди</target> @@ -1152,11 +1691,24 @@ <target>Потвърди kодa за достъп</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve"> + <source>Confirm contact deletion?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm database upgrades" xml:space="preserve"> <source>Confirm database upgrades</source> <target>Потвърди актуализаациите на базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve"> + <source>Confirm files from unknown servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve"> + <source>Confirm network settings</source> + <target>Потвърди мрежовите настройки</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> <target>Потвърди новата парола…</target> @@ -1167,6 +1719,20 @@ <target>Потвърди парола</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <target>Потвърдете, че помните паролата на базата данни, преди да я мигрирате.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve"> + <source>Confirm upload</source> + <target>Потвърди качването</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve"> + <source>Confirmed</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> <target>Свързване</target> @@ -1174,6 +1740,7 @@ </trans-unit> <trans-unit id="Connect automatically" xml:space="preserve"> <source>Connect automatically</source> + <target>Автоматично свъзрване</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect incognito" xml:space="preserve"> @@ -1183,24 +1750,35 @@ </trans-unit> <trans-unit id="Connect to desktop" xml:space="preserve"> <source>Connect to desktop</source> + <target>Свързване с настолно устройство</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve"> + <source>Connect to your friends faster.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect to yourself?" xml:space="preserve"> <source>Connect to yourself?</source> + <target>Свърване със себе си?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect to yourself? This is your own SimpleX address!" xml:space="preserve"> <source>Connect to yourself? This is your own SimpleX address!</source> + <target>Свърване със себе си? +Това е вашият личен SimpleX адрес!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect to yourself? This is your own one-time link!" xml:space="preserve"> <source>Connect to yourself? This is your own one-time link!</source> + <target>Свърване със себе си? +Това е вашят еднократен линк за връзка!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via contact address" xml:space="preserve"> <source>Connect via contact address</source> + <target>Свързване чрез адрес за контакт</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via link" xml:space="preserve"> @@ -1208,11 +1786,6 @@ This is your own one-time link!</source> <target>Свърване чрез линк</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve"> - <source>Connect via link / QR code</source> - <target>Свърване чрез линк/QR код</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connect via one-time link" xml:space="preserve"> <source>Connect via one-time link</source> <target>Свързване чрез еднократен линк за връзка</target> @@ -1220,14 +1793,29 @@ This is your own one-time link!</source> </trans-unit> <trans-unit id="Connect with %@" xml:space="preserve"> <source>Connect with %@</source> + <target>Свързване с %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connected" xml:space="preserve"> + <source>Connected</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connected desktop" xml:space="preserve"> <source>Connected desktop</source> + <target>Свързано настолно устройство</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve"> + <source>Connected servers</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connected to desktop" xml:space="preserve"> <source>Connected to desktop</source> + <target>Свързан с настолно устройство</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connecting" xml:space="preserve"> + <source>Connecting</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> @@ -1240,8 +1828,13 @@ This is your own one-time link!</source> <target>Свързване със сървър…(грешка: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve"> + <source>Connecting to contact, please wait or check later!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting to desktop" xml:space="preserve"> <source>Connecting to desktop</source> + <target>Свързване с настолно устройство</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection" xml:space="preserve"> @@ -1249,6 +1842,14 @@ This is your own one-time link!</source> <target>Връзка</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve"> + <source>Connection and servers status.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve"> + <source>Connection blocked</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> <target>Грешка при свързване</target> @@ -1259,13 +1860,35 @@ This is your own one-time link!</source> <target>Грешка при свързване (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve"> + <source>Connection is blocked by server operator: +%@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve"> + <source>Connection not ready.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve"> + <source>Connection notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Заявката за връзка е изпратена!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve"> + <source>Connection requires encryption renegotiation.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve"> + <source>Connection security</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection terminated" xml:space="preserve"> <source>Connection terminated</source> + <target>Връзката е прекратена</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection timeout" xml:space="preserve"> @@ -1273,6 +1896,14 @@ This is your own one-time link!</source> <target>Времето на изчакване за установяване на връзката изтече</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve"> + <source>Connection with desktop stopped</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve"> + <source>Connections</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact allows" xml:space="preserve"> <source>Contact allows</source> <target>Контактът позволява</target> @@ -1283,6 +1914,10 @@ This is your own one-time link!</source> <target>Контактът вече съществува</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact deleted!" xml:space="preserve"> + <source>Contact deleted!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> <target>Контактът е скрит:</target> @@ -1293,9 +1928,8 @@ This is your own one-time link!</source> <target>Контактът е свързан</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve"> - <source>Contact is not connected yet!</source> - <target>Контактът все още не е свързан!</target> + <trans-unit id="Contact is deleted." xml:space="preserve"> + <source>Contact is deleted.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact name" xml:space="preserve"> @@ -1308,6 +1942,10 @@ This is your own one-time link!</source> <target>Настройки за контакт</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve"> + <source>Contact will be deleted - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> <target>Контакти</target> @@ -1318,51 +1956,70 @@ This is your own one-time link!</source> <target>Контактите могат да маркират съобщения за изтриване; ще можете да ги разглеждате.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve"> + <source>Content violates conditions of use</source> + <note>blocking reason</note> + </trans-unit> <trans-unit id="Continue" xml:space="preserve"> <source>Continue</source> <target>Продължи</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve"> + <source>Conversation deleted!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> <target>Копирай</target> - <note>chat item action</note> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve"> + <source>Copy error</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> <target>Версия на ядрото: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Corner" xml:space="preserve"> + <source>Corner</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Correct name to %@?" xml:space="preserve"> <source>Correct name to %@?</source> + <target>Поправи име на %@?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create" xml:space="preserve"> <source>Create</source> - <target>Създай</target> + <target>Създаване</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve"> + <source>Create 1-time link</source> + <target>Създаване на еднократна препратка</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> - <target>Създай SimpleX адрес</target> + <target>Създаване на адрес в SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create a group using a random profile." xml:space="preserve"> <source>Create a group using a random profile.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> - <source>Create an address to let people connect with you.</source> - <target>Създайте адрес, за да позволите на хората да се свързват с вас.</target> + <target>Създаване група с автоматично създаден профил.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create file" xml:space="preserve"> <source>Create file</source> - <target>Създай файл</target> + <target>Създаване на файл</target> <note>server test step</note> </trans-unit> <trans-unit id="Create group" xml:space="preserve"> <source>Create group</source> + <target>Създаване на група</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create group link" xml:space="preserve"> @@ -1375,18 +2032,18 @@ This is your own one-time link!</source> <target>Създай линк</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create list" xml:space="preserve"> + <source>Create list</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> <target>Създайте нов профил в [настолното приложение](https://simplex.chat/downloads/). 💻</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve"> - <source>Create one-time invitation link</source> - <target>Създай линк за еднократна покана</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create profile" xml:space="preserve"> <source>Create profile</source> + <target>Създай профил</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create queue" xml:space="preserve"> @@ -1404,9 +2061,28 @@ This is your own one-time link!</source> <target>Създай своя профил</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve"> - <source>Created on %@</source> - <target>Създаден на %@</target> + <trans-unit id="Created" xml:space="preserve"> + <source>Created</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve"> + <source>Created at</source> + <target>Създаден на</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve"> + <source>Created at: %@</source> + <target>Създаден на: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve"> + <source>Creating archive link</source> + <target>Създаване на архивен линк</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve"> + <source>Creating link…</source> + <target>Линкът се създава…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current Passcode" xml:space="preserve"> @@ -1414,11 +2090,19 @@ This is your own one-time link!</source> <target>Текущ kод за достъп</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> <target>Текуща парола…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current profile" xml:space="preserve"> + <source>Current profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> <target>В момента максималният поддържан размер на файла е %@.</target> @@ -1429,11 +2113,23 @@ This is your own one-time link!</source> <target>Персонализирано време</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve"> + <source>Customizable message shape.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve"> + <source>Customize theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> <target>Тъмна</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve"> + <source>Dark mode colors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> <target>ID в базата данни</target> @@ -1532,6 +2228,10 @@ This is your own one-time link!</source> <target>Базата данни ще бъде мигрирана, когато приложението се рестартира</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Debug delivery" xml:space="preserve"> + <source>Debug delivery</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> <target>Децентрализиран</target> @@ -1545,15 +2245,16 @@ This is your own one-time link!</source> <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> <target>Изтрий</target> - <note>chat item action</note> + <note>alert action +swipe action</note> + </trans-unit> + <trans-unit id="Delete %lld messages of members?" xml:space="preserve"> + <source>Delete %lld messages of members?</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete %lld messages?" xml:space="preserve"> <source>Delete %lld messages?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve"> - <source>Delete Contact</source> - <target>Изтрий контакт</target> + <target>Изтриване на %lld съобщения?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete address" xml:space="preserve"> @@ -1578,16 +2279,15 @@ This is your own one-time link!</source> </trans-unit> <trans-unit id="Delete and notify contact" xml:space="preserve"> <source>Delete and notify contact</source> + <target>Изтрий и уведоми контакт</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve"> - <source>Delete archive</source> - <target>Изтрий архив</target> + <trans-unit id="Delete chat" xml:space="preserve"> + <source>Delete chat</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve"> - <source>Delete chat archive?</source> - <target>Изтриване на архива на чата?</target> + <trans-unit id="Delete chat messages from your device." xml:space="preserve"> + <source>Delete chat messages from your device.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile" xml:space="preserve"> @@ -1600,6 +2300,10 @@ This is your own one-time link!</source> <target>Изтриване на чат профила?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete chat?" xml:space="preserve"> + <source>Delete chat?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> <source>Delete connection</source> <target>Изтрий връзката</target> @@ -1610,9 +2314,8 @@ This is your own one-time link!</source> <target>Изтрий контакт</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact? This cannot be undone!" xml:space="preserve"> - <source>Delete contact? -This cannot be undone!</source> + <trans-unit id="Delete contact?" xml:space="preserve"> + <source>Delete contact?</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete database" xml:space="preserve"> @@ -1620,6 +2323,11 @@ This cannot be undone!</source> <target>Изтрий базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete database from this device" xml:space="preserve"> + <source>Delete database from this device</source> + <target>Изтриване на базата данни от това устройство</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete file" xml:space="preserve"> <source>Delete file</source> <target>Изтрий файл</target> @@ -1670,6 +2378,10 @@ This cannot be undone!</source> <target>Изтрий линк?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete list?" xml:space="preserve"> + <source>Delete list?</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> <target>Изтрий съобщението на члена?</target> @@ -1683,7 +2395,7 @@ This cannot be undone!</source> <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> <target>Изтрий съобщенията</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> @@ -1700,9 +2412,8 @@ This cannot be undone!</source> <target>Изтрий старата база данни?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve"> - <source>Delete pending connection</source> - <target>Изтрий предстоящата връзка</target> + <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> + <source>Delete or moderate up to 200 messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection?" xml:space="preserve"> @@ -1720,11 +2431,27 @@ This cannot be undone!</source> <target>Изтрий опашка</target> <note>server test step</note> </trans-unit> + <trans-unit id="Delete report" xml:space="preserve"> + <source>Delete report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete up to 20 messages at once." xml:space="preserve"> + <source>Delete up to 20 messages at once.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> <target>Изтрий потребителския профил?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete without notification" xml:space="preserve"> + <source>Delete without notification</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve"> + <source>Deleted</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Deleted at" xml:space="preserve"> <source>Deleted at</source> <target>Изтрито на</target> @@ -1735,6 +2462,14 @@ This cannot be undone!</source> <target>Изтрито на: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve"> + <source>Deletion errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivered even when Apple drops them." xml:space="preserve"> + <source>Delivered even when Apple drops them.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery" xml:space="preserve"> <source>Delivery</source> <target>Доставка</target> @@ -1757,14 +2492,37 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Desktop address" xml:space="preserve"> <source>Desktop address</source> + <target>Адрес на настолно устройство</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve"> <source>Desktop app version %@ is not compatible with this app.</source> + <target>Версията на настолното приложение %@ не е съвместима с това приложение.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Desktop devices" xml:space="preserve"> <source>Desktop devices</source> + <target>Настолни устройства</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve"> + <source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server error: %@" xml:space="preserve"> + <source>Destination server error: %@</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve"> + <source>Destination server version of %@ is incompatible with forwarding server %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Detailed statistics" xml:space="preserve"> + <source>Detailed statistics</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Details" xml:space="preserve"> + <source>Details</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Develop" xml:space="preserve"> @@ -1772,6 +2530,10 @@ This cannot be undone!</source> <target>Разработване</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Developer options" xml:space="preserve"> + <source>Developer options</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> <target>Инструменти за разработчици</target> @@ -1802,8 +2564,12 @@ This cannot be undone!</source> <target>Лични съобщения</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve"> + <source>Direct messages between members are prohibited in this chat.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> <target>Личните съобщения между членовете са забранени в тази група.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1817,11 +2583,23 @@ This cannot be undone!</source> <target>Деактивирай SimpleX заключване</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Disable automatic message deletion?" xml:space="preserve"> + <source>Disable automatic message deletion?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Disable delete messages" xml:space="preserve"> + <source>Disable delete messages</source> + <note>alert button</note> + </trans-unit> <trans-unit id="Disable for all" xml:space="preserve"> <source>Disable for all</source> <target>Деактивиране за всички</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Disabled" xml:space="preserve"> + <source>Disabled</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> <source>Disappearing message</source> <target>Изчезващо съобщение</target> @@ -1837,8 +2615,8 @@ This cannot be undone!</source> <target>Изчезващите съобщения са забранени в този чат.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <target>Изчезващите съобщения са забранени в тази група.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1859,6 +2637,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Disconnect desktop?" xml:space="preserve"> <source>Disconnect desktop?</source> + <target>Прекъсни връзката с настолното устройство?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Discover and join groups" xml:space="preserve"> @@ -1868,6 +2647,11 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Discover via local network" xml:space="preserve"> <source>Discover via local network</source> + <target>Откриване през локалната мрежа</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve"> + <source>Do NOT send messages directly, even if your or destination server does not support private routing.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> @@ -1875,11 +2659,28 @@ This cannot be undone!</source> <target>НЕ използвайте SimpleX за спешни повиквания.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT use private routing." xml:space="preserve"> + <source>Do NOT use private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do it later" xml:space="preserve"> <source>Do it later</source> <target>Отложи</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do not send history to new members." xml:space="preserve"> + <source>Do not send history to new members.</source> + <target>Не изпращай история на нови членове.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not use credentials with proxy." xml:space="preserve"> + <source>Do not use credentials with proxy.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Documents:" xml:space="preserve"> + <source>Documents:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> <target>Не създавай адрес</target> @@ -1890,24 +2691,69 @@ This cannot be undone!</source> <target>Не активирай</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Don't miss important messages." xml:space="preserve"> + <source>Don't miss important messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> <target>Не показвай отново</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Done" xml:space="preserve"> + <source>Done</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Downgrade and open chat" xml:space="preserve"> <source>Downgrade and open chat</source> <target>Понижи версията и отвори чата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Download" xml:space="preserve"> + <source>Download</source> + <target>Изтегли</target> + <note>alert button +chat item action</note> + </trans-unit> + <trans-unit id="Download errors" xml:space="preserve"> + <source>Download errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download failed" xml:space="preserve"> + <source>Download failed</source> + <target>Неуспешно изтегляне</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Download file" xml:space="preserve"> <source>Download file</source> <target>Свали файл</target> <note>server test step</note> </trans-unit> + <trans-unit id="Download files" xml:space="preserve"> + <source>Download files</source> + <note>alert action</note> + </trans-unit> + <trans-unit id="Downloaded" xml:space="preserve"> + <source>Downloaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloaded files" xml:space="preserve"> + <source>Downloaded files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading archive" xml:space="preserve"> + <source>Downloading archive</source> + <target>Архива се изтегля</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading link details" xml:space="preserve"> + <source>Downloading link details</source> + <target>Подробности за линка се изтеглят</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> - <target>Дублирано показвано име!</target> + <target>Дублирано име!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Duration" xml:space="preserve"> @@ -1915,6 +2761,10 @@ This cannot be undone!</source> <target>Продължителност</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="E2E encrypted notifications." xml:space="preserve"> + <source>E2E encrypted notifications.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> <target>Редактирай</target> @@ -1935,6 +2785,10 @@ This cannot be undone!</source> <target>Активиране (запазване на промените)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve"> + <source>Enable Flux in Network & servers settings for better metadata privacy.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> <target>Активирай SimpleX заключване</target> @@ -1948,6 +2802,11 @@ This cannot be undone!</source> <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> <target>Активиране на автоматично изтриване на съобщения?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Enable camera access" xml:space="preserve"> + <source>Enable camera access</source> + <target>Разреши достъпа до камерата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable for all" xml:space="preserve"> @@ -1955,6 +2814,11 @@ This cannot be undone!</source> <target>Активиране за всички</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve"> + <source>Enable in direct chats (BETA)!</source> + <target>Активиране в личните чатове (БЕТА)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> <target>Активирай незабавни известия?</target> @@ -1985,6 +2849,15 @@ This cannot be undone!</source> <target>Активирай kод за достъп за самоунищожение</target> <note>set passcode view</note> </trans-unit> + <trans-unit id="Enabled" xml:space="preserve"> + <source>Enabled</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve"> + <source>Enabled for</source> + <target>Активирано за</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> <target>Криптирай</target> @@ -2015,6 +2888,11 @@ This cannot be undone!</source> <target>Криптирано съобщение или друго събитие</target> <note>notification</note> </trans-unit> + <trans-unit id="Encrypted message: app is stopped" xml:space="preserve"> + <source>Encrypted message: app is stopped</source> + <target>Криптирано съобщение: приложението е спряно</target> + <note>notification</note> + </trans-unit> <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> <target>Криптирано съобщение: грешка в базата данни</target> @@ -2042,10 +2920,16 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Encryption re-negotiation error" xml:space="preserve"> <source>Encryption re-negotiation error</source> + <target>Грешка при повторно договаряне на криптиране</target> <note>message decrypt error item</note> </trans-unit> <trans-unit id="Encryption re-negotiation failed." xml:space="preserve"> <source>Encryption re-negotiation failed.</source> + <target>Неуспешно повторно договаряне на криптирането.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encryption renegotiation in progress." xml:space="preserve"> + <source>Encryption renegotiation in progress.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter Passcode" xml:space="preserve"> @@ -2060,6 +2944,12 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Enter group name…" xml:space="preserve"> <source>Enter group name…</source> + <target>Въведи име на групата…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve"> + <source>Enter passphrase</source> + <target>Въведи парола</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter passphrase…" xml:space="preserve"> @@ -2079,6 +2969,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Enter this device name…" xml:space="preserve"> <source>Enter this device name…</source> + <target>Въведи името на това устройство…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter welcome message…" xml:space="preserve"> @@ -2093,6 +2984,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Enter your name…" xml:space="preserve"> <source>Enter your name…</source> + <target>Въведи своето име…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error" xml:space="preserve"> @@ -2105,26 +2997,33 @@ This cannot be undone!</source> <target>Грешка при отказване на промяна на адреса</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error accepting conditions" xml:space="preserve"> + <source>Error accepting conditions</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> <target>Грешка при приемане на заявка за контакт</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve"> - <source>Error accessing database file</source> - <target>Грешка при достъпа до файла с базата данни</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> <target>Грешка при добавяне на член(ове)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error adding server" xml:space="preserve"> + <source>Error adding server</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> <target>Грешка при промяна на адреса</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing connection profile" xml:space="preserve"> + <source>Error changing connection profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error changing role" xml:space="preserve"> <source>Error changing role</source> <target>Грешка при промяна на ролята</target> @@ -2135,6 +3034,18 @@ This cannot be undone!</source> <target>Грешка при промяна на настройката</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing to incognito!" xml:space="preserve"> + <source>Error changing to incognito!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error checking token status" xml:space="preserve"> + <source>Error checking token status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve"> + <source>Error connecting to forwarding server %@. Please try later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> <target>Грешка при създаване на адрес</target> @@ -2150,16 +3061,29 @@ This cannot be undone!</source> <target>Грешка при създаване на групов линк</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating list" xml:space="preserve"> + <source>Error creating list</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error creating member contact" xml:space="preserve"> <source>Error creating member contact</source> <target>Грешка при създаване на контакт с член</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating message" xml:space="preserve"> + <source>Error creating message</source> + <target>Грешка при създаване на съобщение</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>Грешка при създаване на профил!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating report" xml:space="preserve"> + <source>Error creating report</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error decrypting file" xml:space="preserve"> <source>Error decrypting file</source> <target>Грешка при декриптирането на файла</target> @@ -2167,7 +3091,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Error deleting chat database" xml:space="preserve"> <source>Error deleting chat database</source> - <target>Грешка при изтриване на чат базата данни</target> + <target>Грешка при изтриване на базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting chat!" xml:space="preserve"> @@ -2180,11 +3104,6 @@ This cannot be undone!</source> <target>Грешка при изтриване на връзката</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve"> - <source>Error deleting contact</source> - <target>Грешка при изтриване на контакт</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> <target>Грешка при изтриване на базата данни</target> @@ -2205,6 +3124,11 @@ This cannot be undone!</source> <target>Грешка при изтриване на потребителския профил</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error downloading the archive" xml:space="preserve"> + <source>Error downloading the archive</source> + <target>Грешка при изтеглянето на архива</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> <source>Error enabling delivery receipts!</source> <target>Грешка при активирането на потвърждениeто за доставка!</target> @@ -2222,12 +3146,16 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Error exporting chat database" xml:space="preserve"> <source>Error exporting chat database</source> - <target>Грешка при експортиране на чат базата данни</target> + <target>Грешка при експортиране на базата данни</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error exporting theme: %@" xml:space="preserve"> + <source>Error exporting theme: %@</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> - <target>Грешка при импортиране на чат базата данни</target> + <target>Грешка при импортиране на базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error joining group" xml:space="preserve"> @@ -2235,24 +3163,47 @@ This cannot be undone!</source> <target>Грешка при присъединяване към група</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve"> - <source>Error loading %@ servers</source> - <target>Грешка при зареждане на %@ сървъри</target> + <trans-unit id="Error loading servers" xml:space="preserve"> + <source>Error loading servers</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error migrating settings" xml:space="preserve"> + <source>Error migrating settings</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error opening chat" xml:space="preserve"> + <source>Error opening chat</source> + <target>Грешка при отваряне на чата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> <target>Грешка при получаване на файл</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error reconnecting server" xml:space="preserve"> + <source>Error reconnecting server</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error reconnecting servers" xml:space="preserve"> + <source>Error reconnecting servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error registering for notifications" xml:space="preserve"> + <source>Error registering for notifications</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> <target>Грешка при отстраняване на член</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve"> - <source>Error saving %@ servers</source> - <target>Грешка при запазване на %@ сървъра</target> + <trans-unit id="Error reordering lists" xml:space="preserve"> + <source>Error reordering lists</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error resetting statistics" xml:space="preserve"> + <source>Error resetting statistics</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving ICE servers" xml:space="preserve"> @@ -2260,6 +3211,10 @@ This cannot be undone!</source> <target>Грешка при запазване на ICE сървърите</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving chat list" xml:space="preserve"> + <source>Error saving chat list</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> <target>Грешка при запазване на профила на групата</target> @@ -2275,11 +3230,25 @@ This cannot be undone!</source> <target>Грешка при запазване на парола в Кeychain</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving servers" xml:space="preserve"> + <source>Error saving servers</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error saving settings" xml:space="preserve"> + <source>Error saving settings</source> + <target>Грешка при запазване на настройките</target> + <note>when migrating</note> + </trans-unit> <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> <target>Грешка при запазване на потребителска парола</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error scanning code: %@" xml:space="preserve"> + <source>Error scanning code: %@</source> + <target>Грешка при сканиране на кода: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending email" xml:space="preserve"> <source>Error sending email</source> <target>Грешка при изпращане на имейл</target> @@ -2310,16 +3279,24 @@ This cannot be undone!</source> <target>Грешка при спиране на чата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error switching profile" xml:space="preserve"> + <source>Error switching profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> <target>Грешка при смяна на профил!</target> - <note>No comment provided by engineer.</note> + <note>alertTitle</note> </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> <target>Грешка при синхронизиране на връзката</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error testing server connection" xml:space="preserve"> + <source>Error testing server connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> <target>Грешка при актуализиране на груповия линк</target> @@ -2330,6 +3307,10 @@ This cannot be undone!</source> <target>Грешка при актуализиране на съобщението</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error updating server" xml:space="preserve"> + <source>Error updating server</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> <target>Грешка при актуализиране на настройките</target> @@ -2340,6 +3321,16 @@ This cannot be undone!</source> <target>Грешка при актуализиране на поверителността на потребителя</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error uploading the archive" xml:space="preserve"> + <source>Error uploading the archive</source> + <target>Грешка при качването на архива</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error verifying passphrase:" xml:space="preserve"> + <source>Error verifying passphrase:</source> + <target>Грешка при проверката на паролата:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error: " xml:space="preserve"> <source>Error: </source> <target>Грешка: </target> @@ -2348,7 +3339,9 @@ This cannot be undone!</source> <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> <target>Грешка: %@</target> - <note>No comment provided by engineer.</note> + <note>alert message +file error text +snd error text</note> </trans-unit> <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> @@ -2360,6 +3353,14 @@ This cannot be undone!</source> <target>Грешка: няма файл с база данни</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Errors" xml:space="preserve"> + <source>Errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors in servers configuration." xml:space="preserve"> + <source>Errors in servers configuration.</source> + <note>servers error</note> + </trans-unit> <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> <source>Even when disabled in the conversation.</source> <target>Дори когато е деактивиран в разговора.</target> @@ -2372,8 +3373,13 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Expand" xml:space="preserve"> <source>Expand</source> + <target>Разшири</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Expired" xml:space="preserve"> + <source>Expired</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> <target>Експортирай база данни</target> @@ -2384,11 +3390,20 @@ This cannot be undone!</source> <target>Грешка при експортиране:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Export theme" xml:space="preserve"> + <source>Export theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> <target>Експортиран архив на базата данни.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Exported file doesn't exist" xml:space="preserve"> + <source>Exported file doesn't exist</source> + <target>Експортираният файл не съществува</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exporting database archive…" xml:space="preserve"> <source>Exporting database archive…</source> <target>Експортиране на архив на базата данни…</target> @@ -2404,15 +3419,58 @@ This cannot be undone!</source> <target>Бързо и без чакане, докато подателят е онлайн!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster deletion of groups." xml:space="preserve"> + <source>Faster deletion of groups.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Faster joining and more reliable messages." xml:space="preserve"> <source>Faster joining and more reliable messages.</source> + <target>По-бързо присъединяване и по-надеждни съобщения.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Faster sending messages." xml:space="preserve"> + <source>Faster sending messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Favorite" xml:space="preserve"> <source>Favorite</source> <target>Любим</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Favorites" xml:space="preserve"> + <source>Favorites</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <note>file error alert title</note> + </trans-unit> + <trans-unit id="File errors: %@" xml:space="preserve"> + <source>File errors: +%@</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="File is blocked by server operator: %@." xml:space="preserve"> + <source>File is blocked by server operator: +%@.</source> + <note>file error text</note> + </trans-unit> + <trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve"> + <source>File not found - most likely file was deleted or cancelled.</source> + <note>file error text</note> + </trans-unit> + <trans-unit id="File server error: %@" xml:space="preserve"> + <source>File server error: %@</source> + <note>file error text</note> + </trans-unit> + <trans-unit id="File status" xml:space="preserve"> + <source>File status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File status: %@" xml:space="preserve"> + <source>File status: %@</source> + <note>copied message info</note> + </trans-unit> <trans-unit id="File will be deleted from servers." xml:space="preserve"> <source>File will be deleted from servers.</source> <target>Файлът ще бъде изтрит от сървърите.</target> @@ -2433,6 +3491,10 @@ This cannot be undone!</source> <target>Файл: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files" xml:space="preserve"> + <source>Files</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> <target>Файлове и медия</target> @@ -2443,11 +3505,16 @@ This cannot be undone!</source> <target>Файлове и медия</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> - <source>Files and media are prohibited in this group.</source> + <trans-unit id="Files and media are prohibited." xml:space="preserve"> + <source>Files and media are prohibited.</source> <target>Файловете и медията са забранени в тази група.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files and media not allowed" xml:space="preserve"> + <source>Files and media not allowed</source> + <target>Файлове и медия не са разрешени</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files and media prohibited!" xml:space="preserve"> <source>Files and media prohibited!</source> <target>Файловете и медията са забранени!</target> @@ -2458,6 +3525,16 @@ This cannot be undone!</source> <target>Филтрирайте непрочетените и любимите чатове.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Finalize migration" xml:space="preserve"> + <source>Finalize migration</source> + <target>Завърши миграцията</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration on another device." xml:space="preserve"> + <source>Finalize migration on another device.</source> + <target>Завършете миграцията на другото устройство.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> <target>Най-накрая ги имаме! 🚀</target> @@ -2498,13 +3575,100 @@ This cannot be undone!</source> <target>Поправката не се поддържа от члена на групата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For all moderators" xml:space="preserve"> + <source>For all moderators</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For chat profile %@:" xml:space="preserve"> + <source>For chat profile %@:</source> + <note>servers error</note> + </trans-unit> <trans-unit id="For console" xml:space="preserve"> <source>For console</source> <target>За конзолата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." xml:space="preserve"> + <source>For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For me" xml:space="preserve"> + <source>For me</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For private routing" xml:space="preserve"> + <source>For private routing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For social media" xml:space="preserve"> + <source>For social media</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward" xml:space="preserve"> + <source>Forward</source> + <target>Препрати</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Forward %d message(s)?" xml:space="preserve"> + <source>Forward %d message(s)?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Forward and save messages" xml:space="preserve"> + <source>Forward and save messages</source> + <target>Препращане и запазване на съобщения</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward messages" xml:space="preserve"> + <source>Forward messages</source> + <note>alert action</note> + </trans-unit> + <trans-unit id="Forward messages without files?" xml:space="preserve"> + <source>Forward messages without files?</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> + <source>Forward up to 20 messages at once.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded" xml:space="preserve"> + <source>Forwarded</source> + <target>Препратено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded from" xml:space="preserve"> + <source>Forwarded from</source> + <target>Препратено от</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding %lld messages" xml:space="preserve"> + <source>Forwarding %lld messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve"> + <source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server address is incompatible with network settings: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server version is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server version is incompatible with network settings: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Destination server error: %2$@</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Error: %2$@</source> + <note>snd error text</note> + </trans-unit> <trans-unit id="Found desktop" xml:space="preserve"> <source>Found desktop</source> + <target>Намерено настолно устройство</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="French interface" xml:space="preserve"> @@ -2522,13 +3686,9 @@ This cannot be undone!</source> <target>Пълно име (незадължително)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full name:" xml:space="preserve"> - <source>Full name:</source> - <target>Пълно име:</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Fully decentralized – visible only to members." xml:space="preserve"> <source>Fully decentralized – visible only to members.</source> + <target>Напълно децентрализирана – видима е само за членовете.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fully re-implemented - work in background!" xml:space="preserve"> @@ -2546,6 +3706,18 @@ This cannot be undone!</source> <target>GIF файлове и стикери</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Get notified when mentioned." xml:space="preserve"> + <source>Get notified when mentioned.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Good afternoon!" xml:space="preserve"> + <source>Good afternoon!</source> + <note>message preview</note> + </trans-unit> + <trans-unit id="Good morning!" xml:space="preserve"> + <source>Good morning!</source> + <note>message preview</note> + </trans-unit> <trans-unit id="Group" xml:space="preserve"> <source>Group</source> <target>Група</target> @@ -2553,15 +3725,17 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Group already exists" xml:space="preserve"> <source>Group already exists</source> + <target>Групата вече съществува</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group already exists!" xml:space="preserve"> <source>Group already exists!</source> + <target>Групата вече съществува!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group display name" xml:space="preserve"> <source>Group display name</source> - <target>Показвано име на групата</target> + <target>Име на групата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group full name (optional)" xml:space="preserve"> @@ -2599,36 +3773,6 @@ This cannot be undone!</source> <target>Групови линкове</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve"> - <source>Group members can add message reactions.</source> - <target>Членовете на групата могат да добавят реакции към съобщенията.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> - <source>Group members can irreversibly delete sent messages.</source> - <target>Членовете на групата могат необратимо да изтриват изпратените съобщения.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> - <target>Членовете на групата могат да изпращат лични съобщения.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> - <target>Членовете на групата могат да изпращат изчезващи съобщения.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send files and media." xml:space="preserve"> - <source>Group members can send files and media.</source> - <target>Членовете на групата могат да изпращат файлове и медия.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> - <target>Членовете на групата могат да изпращат гласови съобщения.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> <target>Групово съобщение:</target> @@ -2669,11 +3813,19 @@ This cannot be undone!</source> <target>Групата ще бъде изтрита за вас - това не може да бъде отменено!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Groups" xml:space="preserve"> + <source>Groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Help" xml:space="preserve"> <source>Help</source> <target>Помощ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Help admins moderating their groups." xml:space="preserve"> + <source>Help admins moderating their groups.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> <target>Скрит</target> @@ -2714,15 +3866,27 @@ This cannot be undone!</source> <target>История</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="History is not sent to new members." xml:space="preserve"> + <source>History is not sent to new members.</source> + <target>Историята не се изпраща на нови членове.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> <target>Как работи SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="How it affects privacy" xml:space="preserve"> + <source>How it affects privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it helps privacy" xml:space="preserve"> + <source>How it helps privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> - <target>Как работи</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="How to" xml:space="preserve"> <source>How to</source> @@ -2739,21 +3903,25 @@ This cannot be undone!</source> <target>Как да използвате вашите сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Hungarian interface" xml:space="preserve"> + <source>Hungarian interface</source> + <target>Унгарски интерфейс</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> <target>ICE сървъри (по един на ред)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="IP address" xml:space="preserve"> + <source>IP address</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> <source>If you can't meet in person, show QR code in a video call, or share the link.</source> <target>Ако не можете да се срещнете лично, покажете QR код във видеоразговора или споделете линка.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <target>Ако не можете да се срещнете на живо, можете да **сканирате QR код във видеообаждането** или вашият контакт може да сподели линк за покана.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> <target>Ако въведете този kод за достъп, когато отваряте приложението, всички данни от приложението ще бъдат необратимо изтрити!</target> @@ -2789,8 +3957,8 @@ This cannot be undone!</source> <target>Веднага</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <target>Защитен от спам и злоупотреби</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2801,7 +3969,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Import chat database?" xml:space="preserve"> <source>Import chat database?</source> - <target>Импортиране на чат база данни?</target> + <target>Импортиране на база данни?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Import database" xml:space="preserve"> @@ -2809,6 +3977,30 @@ This cannot be undone!</source> <target>Импортиране на база данни</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Import failed" xml:space="preserve"> + <source>Import failed</source> + <target>Неуспешно импортиране</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import theme" xml:space="preserve"> + <source>Import theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Importing archive" xml:space="preserve"> + <source>Importing archive</source> + <target>Импортиране на архив</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved delivery, reduced traffic usage. More improvements are coming soon!" xml:space="preserve"> + <source>Improved delivery, reduced traffic usage. +More improvements are coming soon!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved message delivery" xml:space="preserve"> + <source>Improved message delivery</source> + <target>Подобрена доставка на съобщения</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> <target>Подобрена поверителност и сигурност</target> @@ -2819,11 +4011,29 @@ This cannot be undone!</source> <target>Подобрена конфигурация на сървъра</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In order to continue, chat should be stopped." xml:space="preserve"> + <source>In order to continue, chat should be stopped.</source> + <target>За да продължите, чатът трябва да бъде спрян.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>В отговор на</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In-call sounds" xml:space="preserve"> + <source>In-call sounds</source> + <target>Звуци по време на разговор</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Inappropriate content" xml:space="preserve"> + <source>Inappropriate content</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Inappropriate profile" xml:space="preserve"> + <source>Inappropriate profile</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> <target>Инкогнито</target> @@ -2831,6 +4041,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Incognito groups" xml:space="preserve"> <source>Incognito groups</source> + <target>Инкогнито групи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito mode" xml:space="preserve"> @@ -2865,6 +4076,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Incompatible version" xml:space="preserve"> <source>Incompatible version</source> + <target>Несъвместима версия</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incorrect passcode" xml:space="preserve"> @@ -2892,6 +4104,11 @@ This cannot be undone!</source> <target>Инсталирайте [SimpleX Chat за терминал](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Instant" xml:space="preserve"> + <source>Instant</source> + <target>Мигновено</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> @@ -2899,29 +4116,74 @@ This cannot be undone!</source> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Instantly" xml:space="preserve"> - <source>Instantly</source> - <target>Мигновено</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> <target>Интерфейс</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Interface colors" xml:space="preserve"> + <source>Interface colors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid" xml:space="preserve"> + <source>Invalid</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (bad token)" xml:space="preserve"> + <source>Invalid (bad token)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (expired)" xml:space="preserve"> + <source>Invalid (expired)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (unregistered)" xml:space="preserve"> + <source>Invalid (unregistered)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (wrong topic)" xml:space="preserve"> + <source>Invalid (wrong topic)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid QR code" xml:space="preserve"> + <source>Invalid QR code</source> + <target>Невалиден QR код</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> <target>Невалиден линк за връзка</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid display name!" xml:space="preserve"> + <source>Invalid display name!</source> + <target>Невалидно име!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid link" xml:space="preserve"> + <source>Invalid link</source> + <target>Невалиден линк</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Невалидно потвърждение за мигриране</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid name!" xml:space="preserve"> <source>Invalid name!</source> + <target>Невалидно име!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid response" xml:space="preserve"> + <source>Invalid response</source> + <target>Невалиден отговор</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> <target>Невалиден адрес на сървъра!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Invalid status" xml:space="preserve"> <source>Invalid status</source> @@ -2943,6 +4205,10 @@ This cannot be undone!</source> <target>Покани членове</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invite to chat" xml:space="preserve"> + <source>Invite to chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invite to group" xml:space="preserve"> <source>Invite to group</source> <target>Покани в групата</target> @@ -2958,8 +4224,8 @@ This cannot be undone!</source> <target>Необратимото изтриване на съобщения е забранено в този чат.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <target>Необратимото изтриване на съобщения е забранено в тази група.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2984,6 +4250,10 @@ This cannot be undone!</source> 3. Връзката е била компрометирана.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="It protects your IP address and connections." xml:space="preserve"> + <source>It protects your IP address and connections.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> <target>Изглежда, че вече сте свързани чрез този линк. Ако не е така, има грешка (%@).</target> @@ -3002,15 +4272,21 @@ This cannot be undone!</source> <trans-unit id="Join" xml:space="preserve"> <source>Join</source> <target>Присъединяване</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> <target>Влез в групата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Join group conversations" xml:space="preserve"> + <source>Join group conversations</source> + <target>Присъединяване към групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Join group?" xml:space="preserve"> <source>Join group?</source> + <target>Влез в групата?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Join incognito" xml:space="preserve"> @@ -3020,11 +4296,14 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Join with current profile" xml:space="preserve"> <source>Join with current profile</source> + <target>Присъединяване с текущия профил</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Join your group? This is your link for group %@!" xml:space="preserve"> <source>Join your group? This is your link for group %@!</source> + <target>Влез в твоята група? +Това е вашят линк за група %@!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Joining group" xml:space="preserve"> @@ -3032,10 +4311,25 @@ This is your link for group %@!</source> <target>Присъединяване към групата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep" xml:space="preserve"> + <source>Keep</source> + <target>Запази</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Keep conversation" xml:space="preserve"> + <source>Keep conversation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Keep the app open to use it from desktop" xml:space="preserve"> <source>Keep the app open to use it from desktop</source> + <target>Дръжте приложението отворено, за да го използвате от настолното устройство</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep unused invitation?" xml:space="preserve"> + <source>Keep unused invitation?</source> + <target>Запази неизползваната покана за връзка?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> <source>Keep your connections</source> <target>Запазете връзките си</target> @@ -3069,6 +4363,14 @@ This is your link for group %@!</source> <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> <target>Напусни</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Leave chat" xml:space="preserve"> + <source>Leave chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave chat?" xml:space="preserve"> + <source>Leave chat?</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group" xml:space="preserve"> @@ -3098,14 +4400,29 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Link mobile and desktop apps! 🔗" xml:space="preserve"> <source>Link mobile and desktop apps! 🔗</source> + <target>Свържете мобилни и настолни приложения! 🔗</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Linked desktop options" xml:space="preserve"> <source>Linked desktop options</source> + <target>Настройки на запомнени настолни устройства</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Linked desktops" xml:space="preserve"> <source>Linked desktops</source> + <target>Запомнени настолни устройства</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List" xml:space="preserve"> + <source>List</source> + <note>swipe action</note> + </trans-unit> + <trans-unit id="List name and emoji should be different for all lists." xml:space="preserve"> + <source>List name and emoji should be different for all lists.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List name..." xml:space="preserve"> + <source>List name...</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Live message!" xml:space="preserve"> @@ -3138,11 +4455,6 @@ This is your link for group %@!</source> <target>Режим на заключване</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> - <source>Make a private connection</source> - <target>Добави поверителна връзка</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make one message disappear" xml:space="preserve"> <source>Make one message disappear</source> <target>Накарайте едно съобщение да изчезне</target> @@ -3153,21 +4465,11 @@ This is your link for group %@!</source> <target>Направи профила поверителен!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> - <target>Уверете се, че %@ сървърните адреси са в правилен формат, разделени на редове и не се дублират (%@).</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> <target>Уверете се, че адресите на WebRTC ICE сървъра са в правилен формат, разделени на редове и не са дублирани.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <target>Много хора попитаха: *ако SimpleX няма потребителски идентификатори, как може да доставя съобщения?*</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> <source>Mark deleted for everyone</source> <target>Маркирай като изтрито за всички</target> @@ -3193,11 +4495,31 @@ This is your link for group %@!</source> <target>Макс. 30 секунди, получено незабавно.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Media & file servers" xml:space="preserve"> + <source>Media & file servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Medium" xml:space="preserve"> + <source>Medium</source> + <note>blur media</note> + </trans-unit> <trans-unit id="Member" xml:space="preserve"> <source>Member</source> <target>Член</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member inactive" xml:space="preserve"> + <source>Member inactive</source> + <note>item status text</note> + </trans-unit> + <trans-unit id="Member reports" xml:space="preserve"> + <source>Member reports</source> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All chat members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All chat members will be notified.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> <target>Ролята на члена ще бъде променена на "%@". Всички членове на групата ще бъдат уведомени.</target> @@ -3208,11 +4530,62 @@ This is your link for group %@!</source> <target>Ролята на члена ще бъде променена на "%@". Членът ще получи нова покана.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from chat - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> <target>Членът ще бъде премахнат от групата - това не може да бъде отменено!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Members can add message reactions." xml:space="preserve"> + <source>Members can add message reactions.</source> + <target>Членовете на групата могат да добавят реакции към съобщенията.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Members can irreversibly delete sent messages. (24 hours)</source> + <target>Членовете на групата могат необратимо да изтриват изпратените съобщения. (24 часа)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can report messsages to moderators." xml:space="preserve"> + <source>Members can report messsages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send SimpleX links." xml:space="preserve"> + <source>Members can send SimpleX links.</source> + <target>Членовете на групата могат да изпращат SimpleX линкове.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> + <target>Членовете на групата могат да изпращат лични съобщения.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> + <target>Членовете на групата могат да изпращат изчезващи съобщения.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send files and media." xml:space="preserve"> + <source>Members can send files and media.</source> + <target>Членовете на групата могат да изпращат файлове и медия.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> + <target>Членовете на групата могат да изпращат гласови съобщения.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mention members 👋" xml:space="preserve"> + <source>Mention members 👋</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Menus" xml:space="preserve"> + <source>Menus</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>Грешка при доставката на съобщението</target> @@ -3223,11 +4596,27 @@ This is your link for group %@!</source> <target>Потвърждениe за доставка на съобщения!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message delivery warning" xml:space="preserve"> + <source>Message delivery warning</source> + <note>item status text</note> + </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> <target>Чернова на съобщение</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message forwarded" xml:space="preserve"> + <source>Message forwarded</source> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message may be delivered later if member becomes active." xml:space="preserve"> + <source>Message may be delivered later if member becomes active.</source> + <note>item status description</note> + </trans-unit> + <trans-unit id="Message queue info" xml:space="preserve"> + <source>Message queue info</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message reactions" xml:space="preserve"> <source>Message reactions</source> <target>Реакции на съобщения</target> @@ -3238,16 +4627,46 @@ This is your link for group %@!</source> <target>Реакциите на съобщения са забранени в този чат.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> - <source>Message reactions are prohibited in this group.</source> + <trans-unit id="Message reactions are prohibited." xml:space="preserve"> + <source>Message reactions are prohibited.</source> <target>Реакциите на съобщения са забранени в тази група.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message reception" xml:space="preserve"> + <source>Message reception</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message servers" xml:space="preserve"> + <source>Message servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message shape" xml:space="preserve"> + <source>Message shape</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message source remains private." xml:space="preserve"> + <source>Message source remains private.</source> + <target>Източникът на съобщението остава скрит.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status" xml:space="preserve"> + <source>Message status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status: %@" xml:space="preserve"> + <source>Message status: %@</source> + <note>copied message info</note> + </trans-unit> <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> <target>Текст на съобщението</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message too large" xml:space="preserve"> + <source>Message too large</source> + <target>Съобщението е твърде голямо</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> <target>Съобщения</target> @@ -3260,6 +4679,63 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Messages from %@ will be shown!" xml:space="preserve"> <source>Messages from %@ will be shown!</source> + <target>Съобщенията от %@ ще бъдат показани!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages in this chat will never be deleted." xml:space="preserve"> + <source>Messages in this chat will never be deleted.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages received" xml:space="preserve"> + <source>Messages received</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages sent" xml:space="preserve"> + <source>Messages sent</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages were deleted after you selected them." xml:space="preserve"> + <source>Messages were deleted after you selected them.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Съобщенията, файловете и разговорите са защитени чрез **криптиране от край до край** с перфектна секретност при препращане, правдоподобно опровержение и възстановяване при взлом.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Съобщенията, файловете и разговорите са защитени чрез **квантово устойчиво e2e криптиране** с перфектна секретност при препращане, правдоподобно опровержение и възстановяване при взлом.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate device" xml:space="preserve"> + <source>Migrate device</source> + <target>Мигрирай устройството</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate from another device" xml:space="preserve"> + <source>Migrate from another device</source> + <target>Мигриране от друго устройство</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate here" xml:space="preserve"> + <source>Migrate here</source> + <target>Мигрирай тук</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device" xml:space="preserve"> + <source>Migrate to another device</source> + <target>Миграция към друго устройство</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device via QR code." xml:space="preserve"> + <source>Migrate to another device via QR code.</source> + <target>Мигрирайте към друго устройство чрез QR код.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating" xml:space="preserve"> + <source>Migrating</source> + <target>Мигриране</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Migrating database archive…" xml:space="preserve"> @@ -3267,6 +4743,11 @@ This is your link for group %@!</source> <target>Архивът на базата данни се мигрира…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Migration complete" xml:space="preserve"> + <source>Migration complete</source> + <target>Миграцията е завършена</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> <target>Грешка при мигриране:</target> @@ -3282,9 +4763,9 @@ This is your link for group %@!</source> <target>Миграцията е завършена</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> - <target>Миграции: %@</target> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> + <target>Миграции:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> @@ -3302,21 +4783,29 @@ This is your link for group %@!</source> <target>Модерирано в: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="More" xml:space="preserve"> + <source>More</source> + <note>swipe action</note> + </trans-unit> <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> <target>Очаквайте скоро още подобрения!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="More reliable network connection." xml:space="preserve"> + <source>More reliable network connection.</source> + <target>По-надеждна мрежова връзка.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="More reliable notifications" xml:space="preserve"> + <source>More reliable notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> <source>Most likely this connection is deleted.</source> <target>Най-вероятно тази връзка е изтрита.</target> <note>item status description</note> </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> - <source>Most likely this contact has deleted the connection with you.</source> - <target>Най-вероятно този контакт е изтрил връзката с вас.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> <target>Множество профили за чат</target> @@ -3325,7 +4814,11 @@ This is your link for group %@!</source> <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> <target>Без звук</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Mute all" xml:space="preserve"> + <source>Mute all</source> + <note>notification label action</note> </trans-unit> <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> @@ -3335,13 +4828,35 @@ This is your link for group %@!</source> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> <target>Име</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> <target>Мрежа и сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Network connection" xml:space="preserve"> + <source>Network connection</source> + <target>Мрежова връзка</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network decentralization" xml:space="preserve"> + <source>Network decentralization</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve"> + <source>Network issues - message expired after many attempts to send it.</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Network management" xml:space="preserve"> + <source>Network management</source> + <target>Управление на мрежата</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network operator" xml:space="preserve"> + <source>Network operator</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> <target>Мрежови настройки</target> @@ -3352,11 +4867,32 @@ This is your link for group %@!</source> <target>Състояние на мрежата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New" xml:space="preserve"> + <source>New</source> + <note>token status text</note> + </trans-unit> <trans-unit id="New Passcode" xml:space="preserve"> <source>New Passcode</source> <target>Нов kод за достъп</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve"> + <source>New SOCKS credentials will be used every time you start the app.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve"> + <source>New SOCKS credentials will be used for each server.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat" xml:space="preserve"> + <source>New chat</source> + <target>Нов чат</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat experience 🎉" xml:space="preserve"> + <source>New chat experience 🎉</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> <target>Нова заявка за контакт</target> @@ -3367,11 +4903,6 @@ This is your link for group %@!</source> <target>Нов контакт:</target> <note>notification</note> </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> - <source>New database archive</source> - <target>Нов архив на база данни</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="New desktop app!" xml:space="preserve"> <source>New desktop app!</source> <target>Ново настолно приложение!</target> @@ -3379,14 +4910,22 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="New display name" xml:space="preserve"> <source>New display name</source> - <target>Ново показвано име</target> + <target>Ново име</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <note>notification</note> + </trans-unit> <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> <target>Ново в %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New media options" xml:space="preserve"> + <source>New media options</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> <target>Нова членска роля</target> @@ -3402,6 +4941,10 @@ This is your link for group %@!</source> <target>Нова парола…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New server" xml:space="preserve"> + <source>New server</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No" xml:space="preserve"> <source>No</source> <target>Не</target> @@ -3412,6 +4955,18 @@ This is your link for group %@!</source> <target>Приложението няма kод за достъп</target> <note>Authentication unavailable</note> </trans-unit> + <trans-unit id="No chats" xml:space="preserve"> + <source>No chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats found" xml:space="preserve"> + <source>No chats found</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats in list %@" xml:space="preserve"> + <source>No chats in list %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> <target>Няма избрани контакти</target> @@ -3432,6 +4987,10 @@ This is your link for group %@!</source> <target>Няма токен за устройство!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No direct connection yet, message is forwarded by admin." xml:space="preserve"> + <source>No direct connection yet, message is forwarded by admin.</source> + <note>item status description</note> + </trans-unit> <trans-unit id="No filtered chats" xml:space="preserve"> <source>No filtered chats</source> <target>Няма филтрирани чатове</target> @@ -3447,20 +5006,96 @@ This is your link for group %@!</source> <target>Няма история</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No info, try to reload" xml:space="preserve"> + <source>No info, try to reload</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No media & file servers." xml:space="preserve"> + <source>No media & file servers.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No message" xml:space="preserve"> + <source>No message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No message servers." xml:space="preserve"> + <source>No message servers.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No network connection" xml:space="preserve"> + <source>No network connection</source> + <target>Няма мрежова връзка</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record speech" xml:space="preserve"> + <source>No permission to record speech</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record video" xml:space="preserve"> + <source>No permission to record video</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> <source>No permission to record voice message</source> <target>Няма разрешение за запис на гласово съобщение</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No push server" xml:space="preserve"> + <source>No push server</source> + <target>Локално</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> <target>Няма получени или изпратени файлове</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No servers for private message routing." xml:space="preserve"> + <source>No servers for private message routing.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive files." xml:space="preserve"> + <source>No servers to receive files.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive messages." xml:space="preserve"> + <source>No servers to receive messages.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to send files." xml:space="preserve"> + <source>No servers to send files.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No token!" xml:space="preserve"> + <source>No token!</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="No unread chats" xml:space="preserve"> + <source>No unread chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> + <target>Първата платформа без никакви потребителски идентификатори – поверителна по дизайн.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Not compatible!" xml:space="preserve"> <source>Not compatible!</source> + <target>Несъвместим!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notes" xml:space="preserve"> + <source>Notes</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing selected" xml:space="preserve"> + <source>Nothing selected</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing to forward!" xml:space="preserve"> + <source>Nothing to forward!</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> <target>Известия</target> @@ -3471,6 +5106,18 @@ This is your link for group %@!</source> <target>Известията са деактивирани!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notifications error" xml:space="preserve"> + <source>Notifications error</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Notifications privacy" xml:space="preserve"> + <source>Notifications privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications status" xml:space="preserve"> + <source>Notifications status</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. @@ -3480,44 +5127,43 @@ This is your link for group %@!</source> - да деактивират членове (роля "наблюдател")</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="OK" xml:space="preserve"> + <source>OK</source> + <target>ОК</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Off" xml:space="preserve"> <source>Off</source> <target>Изключено</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> - <source>Off (Local)</source> - <target>Изключено (Локално)</target> - <note>No comment provided by engineer.</note> + <note>blur media</note> </trans-unit> <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> <target>Ок</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> <target>Стара база данни</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> - <source>Old database archive</source> - <target>Стар архив на база данни</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> <target>Линк за еднократна покана</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> - <target>За свързване ще са необходими Onion хостове. Изисква се активиране на VPN.</target> + <trans-unit id="Onion hosts will be **required** for connection. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be **required** for connection. +Requires compatible VPN.</source> + <target>За свързване ще са **необходими** Onion хостове. +Изисква се активиране на VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> - <target>Ще се използват Onion хостове, когато са налични. Изисква се активиране на VPN.</target> + <trans-unit id="Onion hosts will be used when available. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be used when available. +Requires compatible VPN.</source> + <target>Ще се използват Onion хостове, когато са налични. +Изисква се активиране на VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will not be used." xml:space="preserve"> @@ -3525,11 +5171,19 @@ This is your link for group %@!</source> <target>Няма се използват Onion хостове.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only chat owners can change preferences." xml:space="preserve"> + <source>Only chat owners can change preferences.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <target>Само потребителските устройства съхраняват потребителски профили, контакти, групи и съобщения, изпратени с **двуслойно криптиране от край до край**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only delete conversation" xml:space="preserve"> + <source>Only delete conversation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> <source>Only group owners can change group preferences.</source> <target>Само собствениците на групата могат да променят груповите настройки.</target> @@ -3545,14 +5199,22 @@ This is your link for group %@!</source> <target>Само собствениците на групата могат да активират гласови съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only sender and moderators see it" xml:space="preserve"> + <source>Only sender and moderators see it</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you and moderators see it" xml:space="preserve"> + <source>Only you and moderators see it</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only you can add message reactions." xml:space="preserve"> <source>Only you can add message reactions.</source> <target>Само вие можете да добавяте реакции на съобщенията.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <target>Само вие можете необратимо да изтриете съобщения (вашият контакт може да ги маркира за изтриване).</target> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source> + <target>Само вие можете необратимо да изтриете съобщения (вашият контакт може да ги маркира за изтриване). (24 часа)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can make calls." xml:space="preserve"> @@ -3575,9 +5237,9 @@ This is your link for group %@!</source> <target>Само вашият контакт може да добавя реакции на съобщенията.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <target>Само вашият контакт може необратимо да изтрие съобщения (можете да ги маркирате за изтриване).</target> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source> + <target>Само вашият контакт може необратимо да изтрие съобщения (можете да ги маркирате за изтриване). (24 часа)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can make calls." xml:space="preserve"> @@ -3598,13 +5260,17 @@ This is your link for group %@!</source> <trans-unit id="Open" xml:space="preserve"> <source>Open</source> <target>Отвори</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Отвори настройки</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open changes" xml:space="preserve"> + <source>Open changes</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> <target>Отвори чат</target> @@ -3615,25 +5281,79 @@ This is your link for group %@!</source> <target>Отвори конзолата</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Open conditions" xml:space="preserve"> + <source>Open conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open group" xml:space="preserve"> <source>Open group</source> + <target>Отвори група</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve"> - <source>Open user profiles</source> - <target>Отвори потребителските профили</target> + <trans-unit id="Open link?" xml:space="preserve"> + <source>Open link?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Open migration to another device" xml:space="preserve"> + <source>Open migration to another device</source> + <target>Отвори миграцията към друго устройство</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> - <target>Протокол и код с отворен код – всеки може да оперира собствени сървъри.</target> + <trans-unit id="Opening app…" xml:space="preserve"> + <source>Opening app…</source> + <target>Приложението се отваря…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve"> - <source>Opening database…</source> - <target>Отваряне на база данни…</target> + <trans-unit id="Operator" xml:space="preserve"> + <source>Operator</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Operator server" xml:space="preserve"> + <source>Operator server</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Or import archive file" xml:space="preserve"> + <source>Or import archive file</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or paste archive link" xml:space="preserve"> + <source>Or paste archive link</source> + <target>Или постави архивен линк</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or scan QR code" xml:space="preserve"> + <source>Or scan QR code</source> + <target>Или сканирай QR код</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or securely share this file link" xml:space="preserve"> + <source>Or securely share this file link</source> + <target>Или сигурно споделете този линк към файла</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or show this code" xml:space="preserve"> + <source>Or show this code</source> + <target>Или покажи този код</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or to share privately" xml:space="preserve"> + <source>Or to share privately</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Organize chats into lists" xml:space="preserve"> + <source>Organize chats into lists</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other" xml:space="preserve"> + <source>Other</source> + <target>Други</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other file errors: %@" xml:space="preserve"> + <source>Other file errors: +%@</source> + <note>alert message</note> + </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> <target>PING бройка</target> @@ -3669,18 +5389,23 @@ This is your link for group %@!</source> <target>Кодът за достъп е зададен!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Password" xml:space="preserve"> + <source>Password</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> <target>Парола за показване</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste" xml:space="preserve"> - <source>Paste</source> - <target>Постави</target> - <note>No comment provided by engineer.</note> + <trans-unit id="Past member %@" xml:space="preserve"> + <source>Past member %@</source> + <target>Бивш член %@</target> + <note>past/unknown group member</note> </trans-unit> <trans-unit id="Paste desktop address" xml:space="preserve"> <source>Paste desktop address</source> + <target>Постави адрес на настолно устройство</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Paste image" xml:space="preserve"> @@ -3688,23 +5413,22 @@ This is your link for group %@!</source> <target>Постави изображение</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve"> - <source>Paste received link</source> + <trans-unit id="Paste link to connect!" xml:space="preserve"> + <source>Paste link to connect!</source> + <target>Поставете линк, за да се свържете!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Paste the link you received" xml:space="preserve"> + <source>Paste the link you received</source> <target>Постави получения линк</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> - <source>Paste the link you received to connect with your contact.</source> - <target>Поставете линка, който сте получили, за да се свържете с вашия контакт.</target> - <note>placeholder</note> - </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> - <target>Хората могат да се свържат с вас само чрез ликовете, които споделяте.</target> + <trans-unit id="Pending" xml:space="preserve"> + <source>Pending</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Periodically" xml:space="preserve"> - <source>Periodically</source> + <trans-unit id="Periodic" xml:space="preserve"> + <source>Periodic</source> <target>Периодично</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3713,11 +5437,29 @@ This is your link for group %@!</source> <target>Постоянна грешка при декриптиране</target> <note>message decrypt error item</note> </trans-unit> + <trans-unit id="Picture-in-picture calls" xml:space="preserve"> + <source>Picture-in-picture calls</source> + <target>Обаждания "картина в картина"</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Play from the chat list." xml:space="preserve"> + <source>Play from the chat list.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable calls." xml:space="preserve"> + <source>Please ask your contact to enable calls.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> <target>Моля, попитайте вашия контакт, за да активирате изпращане на гласови съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers." xml:space="preserve"> + <source>Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> <target>Моля, проверете дали сте използвали правилния линк или поискайте вашия контакт, за да ви изпрати друг.</target> @@ -3733,6 +5475,18 @@ This is your link for group %@!</source> <target>Моля, проверете вашите настройки и тези вашия за контакт.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve"> + <source>Please confirm that network settings are correct for this device.</source> + <target>Моля, потвърдете, че мрежовите настройки са правилни за това устройство.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact developers. Error: %@" xml:space="preserve"> + <source>Please contact developers. +Error: %@</source> + <target>Моля, свържете се с разработчиците. +Грешка: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> <target>Моля, свържете се с груповия администартор.</target> @@ -3773,11 +5527,27 @@ This is your link for group %@!</source> <target>Моля, съхранявайте паролата на сигурно място, НЯМА да можете да я промените, ако я загубите.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please try to disable and re-enable notfications." xml:space="preserve"> + <source>Please try to disable and re-enable notfications.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token activation to complete." xml:space="preserve"> + <source>Please wait for token activation to complete.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token to be registered." xml:space="preserve"> + <source>Please wait for token to be registered.</source> + <note>token info</note> + </trans-unit> <trans-unit id="Polish interface" xml:space="preserve"> <source>Polish interface</source> <target>Полски интерфейс</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Port" xml:space="preserve"> + <source>Port</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> <target>Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен</target> @@ -3788,36 +5558,76 @@ This is your link for group %@!</source> <target>Запазете последната чернова на съобщението с прикачени файлове.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Preset server" xml:space="preserve"> - <source>Preset server</source> - <target>Предварително зададен сървър</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> <target>Предварително зададен адрес на сървъра</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Preset servers" xml:space="preserve"> + <source>Preset servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Preview" xml:space="preserve"> <source>Preview</source> <target>Визуализация</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Previously connected servers" xml:space="preserve"> + <source>Previously connected servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> <target>Поверителност и сигурност</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Privacy for your customers." xml:space="preserve"> + <source>Privacy for your customers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy policy and conditions of use." xml:space="preserve"> + <source>Privacy policy and conditions of use.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy redefined" xml:space="preserve"> <source>Privacy redefined</source> <target>Поверителността преосмислена</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve"> + <source>Private chats, groups and your contacts are not accessible to server operators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> <target>Поверителни имена на файлове</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private media file names." xml:space="preserve"> + <source>Private media file names.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing" xml:space="preserve"> + <source>Private message routing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing 🚀" xml:space="preserve"> + <source>Private message routing 🚀</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private notes" xml:space="preserve"> + <source>Private notes</source> + <target>Лични бележки</target> + <note>name of notes to self</note> + </trans-unit> + <trans-unit id="Private routing" xml:space="preserve"> + <source>Private routing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private routing error" xml:space="preserve"> + <source>Private routing error</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> <target>Профилни и сървърни връзки</target> @@ -3828,12 +5638,9 @@ This is your link for group %@!</source> <target>Профилно изображение</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile name" xml:space="preserve"> - <source>Profile name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile name:" xml:space="preserve"> - <source>Profile name:</source> + <trans-unit id="Profile images" xml:space="preserve"> + <source>Profile images</source> + <target>Профилни изображения</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile password" xml:space="preserve"> @@ -3841,10 +5648,14 @@ This is your link for group %@!</source> <target>Профилна парола</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Profile theme" xml:space="preserve"> + <source>Profile theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> <source>Profile update will be sent to your contacts.</source> <target>Актуализацията на профила ще бъде изпратена до вашите контакти.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> @@ -3866,6 +5677,15 @@ This is your link for group %@!</source> <target>Забрани реакциите на съобщенията.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Prohibit reporting messages to moderators." xml:space="preserve"> + <source>Prohibit reporting messages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending SimpleX links." xml:space="preserve"> + <source>Prohibit sending SimpleX links.</source> + <target>Забранете изпращането на SimpleX линкове.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> <target>Забрани изпращането на лични съобщения до членовете.</target> @@ -3886,11 +5706,20 @@ This is your link for group %@!</source> <target>Забрани изпращането на гласови съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect IP address" xml:space="preserve"> + <source>Protect IP address</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> <target>Защити екрана на приложението</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve"> + <source>Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> <target>Защитете чат профилите с парола!</target> @@ -3906,16 +5735,42 @@ This is your link for group %@!</source> <target>Време за изчакване на протокола за KB</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Proxied" xml:space="preserve"> + <source>Proxied</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied servers" xml:space="preserve"> + <source>Proxied servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxy requires password" xml:space="preserve"> + <source>Proxy requires password</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> <target>Push известия</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Push server" xml:space="preserve"> + <source>Push server</source> + <target>Push сървър</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Quantum resistant encryption" xml:space="preserve"> + <source>Quantum resistant encryption</source> + <target>Квантово устойчиво криптиране</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> <source>Rate the app</source> <target>Оценете приложението</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reachable chat toolbar" xml:space="preserve"> + <source>Reachable chat toolbar</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> <target>Реагирай…</target> @@ -3924,16 +5779,21 @@ This is your link for group %@!</source> <trans-unit id="Read" xml:space="preserve"> <source>Read</source> <target>Прочетено</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Read more" xml:space="preserve"> <source>Read more</source> <target>Прочетете още</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> - <target>Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source> + <target>Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</source> + <target>Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> @@ -3941,11 +5801,6 @@ This is your link for group %@!</source> <target>Прочетете повече в [Ръководство на потребителя](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> - <source>Read more in our GitHub repository.</source> - <target>Прочетете повече в нашето хранилище в GitHub.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> <target>Прочетете повече в нашето [GitHub хранилище](https://github.com/simplex-chat/simplex-chat#readme).</target> @@ -3956,6 +5811,10 @@ This is your link for group %@!</source> <target>Потвърждениeто за доставка е деактивирано</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receive errors" xml:space="preserve"> + <source>Receive errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Получено в</target> @@ -3976,6 +5835,18 @@ This is your link for group %@!</source> <target>Получено съобщение</target> <note>message info title</note> </trans-unit> + <trans-unit id="Received messages" xml:space="preserve"> + <source>Received messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received reply" xml:space="preserve"> + <source>Received reply</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received total" xml:space="preserve"> + <source>Received total</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> <target>Получаващият адрес ще бъде променен към друг сървър. Промяната на адреса ще завърши, след като подателят е онлайн.</target> @@ -3991,16 +5862,46 @@ This is your link for group %@!</source> <target>Получаване чрез</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Recent history and improved [directory bot](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)." xml:space="preserve"> + <source>Recent history and improved [directory bot](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).</source> + <target>Скорошна история и подобрен [bot за директория за групи](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPd jdLW3%23%2F%3Fv%3D1-2% 26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve"> + <source>Recipient(s) can't see who this message is from.</source> + <target>Получателят(ите) не могат да видят от кого е това съобщение.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> <target>Получателите виждат актуализации, докато ги въвеждате.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect" xml:space="preserve"> + <source>Reconnect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> <target>Повторно се свържете с всички свързани сървъри, за да принудите доставката на съобщенията. Използва се допълнителен трафик.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect all servers" xml:space="preserve"> + <source>Reconnect all servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers?" xml:space="preserve"> + <source>Reconnect all servers?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect server to force message delivery. It uses additional traffic.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server?" xml:space="preserve"> + <source>Reconnect server?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> <target>Повторно свърване със сървърите?</target> @@ -4021,10 +5922,23 @@ This is your link for group %@!</source> <target>Намалена консумация на батерията</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Register" xml:space="preserve"> + <source>Register</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Register notification token?" xml:space="preserve"> + <source>Register notification token?</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Registered" xml:space="preserve"> + <source>Registered</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> <target>Отхвърляне</target> - <note>reject incoming call via notification</note> + <note>reject incoming call via notification +swipe action</note> </trans-unit> <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> <source>Reject (sender NOT notified)</source> @@ -4051,6 +5965,14 @@ This is your link for group %@!</source> <target>Премахване</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Remove archive?" xml:space="preserve"> + <source>Remove archive?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove image" xml:space="preserve"> + <source>Remove image</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> <target>Острани член</target> @@ -4083,10 +6005,27 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Repeat connection request?" xml:space="preserve"> <source>Repeat connection request?</source> + <target>Изпрати отново заявката за свързване?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat download" xml:space="preserve"> + <source>Repeat download</source> + <target>Повтори изтеглянето</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat import" xml:space="preserve"> + <source>Repeat import</source> + <target>Повтори импортирането</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Repeat join request?" xml:space="preserve"> <source>Repeat join request?</source> + <target>Изпрати отново заявката за присъединяване?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat upload" xml:space="preserve"> + <source>Repeat upload</source> + <target>Повтори качването</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reply" xml:space="preserve"> @@ -4094,6 +6033,46 @@ This is your link for group %@!</source> <target>Отговори</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Report" xml:space="preserve"> + <source>Report</source> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Report content: only group moderators will see it." xml:space="preserve"> + <source>Report content: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report member profile: only group moderators will see it." xml:space="preserve"> + <source>Report member profile: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report other: only group moderators will see it." xml:space="preserve"> + <source>Report other: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report reason?" xml:space="preserve"> + <source>Report reason?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Report spam: only group moderators will see it." xml:space="preserve"> + <source>Report spam: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report violation: only group moderators will see it." xml:space="preserve"> + <source>Report violation: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report: %@" xml:space="preserve"> + <source>Report: %@</source> + <note>report in notification</note> + </trans-unit> + <trans-unit id="Reporting messages to moderators is prohibited." xml:space="preserve"> + <source>Reporting messages to moderators is prohibited.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reports" xml:space="preserve"> + <source>Reports</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Required" xml:space="preserve"> <source>Required</source> <target>Задължително</target> @@ -4104,16 +6083,36 @@ This is your link for group %@!</source> <target>Нулиране</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset all hints" xml:space="preserve"> + <source>Reset all hints</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics" xml:space="preserve"> + <source>Reset all statistics</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics?" xml:space="preserve"> + <source>Reset all statistics?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> <target>Нулирай цветовете</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to app theme" xml:space="preserve"> + <source>Reset to app theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> <target>Възстановяване на настройките по подразбиране</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to user theme" xml:space="preserve"> + <source>Reset to user theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> <source>Restart the app to create a new chat profile</source> <target>Рестартирайте приложението, за да създадете нов чат профил</target> @@ -4144,14 +6143,18 @@ This is your link for group %@!</source> <target>Грешка при възстановяване на базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Retry" xml:space="preserve"> + <source>Retry</source> + <target>Опитай отново</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> <target>Покажи</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Revert" xml:space="preserve"> - <source>Revert</source> - <target>Отмени промените</target> + <trans-unit id="Review conditions" xml:space="preserve"> + <source>Review conditions</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke" xml:space="preserve"> @@ -4179,51 +6182,62 @@ This is your link for group %@!</source> <target>Стартиране на чат</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve"> - <source>SMP servers</source> - <target>SMP сървъри</target> + <trans-unit id="SMP server" xml:space="preserve"> + <source>SMP server</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SOCKS proxy" xml:space="preserve"> + <source>SOCKS proxy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safely receive files" xml:space="preserve"> + <source>Safely receive files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safer groups" xml:space="preserve"> + <source>Safer groups</source> + <target>По-безопасни групи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save" xml:space="preserve"> <source>Save</source> <target>Запази</target> - <note>chat item action</note> + <note>alert button +chat item action</note> </trans-unit> <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> <target>Запази (и уведоми контактите)</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> <target>Запази и уведоми контакта</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> <target>Запази и уведоми членовете на групата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save and reconnect" xml:space="preserve"> + <source>Save and reconnect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save and update group profile" xml:space="preserve"> <source>Save and update group profile</source> <target>Запази и актуализирай профила на групата</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save archive" xml:space="preserve"> - <source>Save archive</source> - <target>Запази архив</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve"> - <source>Save auto-accept settings</source> - <target>Запази настройките за автоматично приемане</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> <source>Save group profile</source> <target>Запази профила на групата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save list" xml:space="preserve"> + <source>Save list</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> <target>Запази паролата и отвори чата</target> @@ -4237,7 +6251,7 @@ This is your link for group %@!</source> <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> <target>Запази настройките?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> @@ -4252,23 +6266,49 @@ This is your link for group %@!</source> <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> <target>Запази сървърите?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve"> - <source>Save settings?</source> - <target>Запази настройките?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> <target>Запази съобщението при посрещане?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save your profile?" xml:space="preserve"> + <source>Save your profile?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Saved" xml:space="preserve"> + <source>Saved</source> + <target>Запазено</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> <source>Saved WebRTC ICE servers will be removed</source> <target>Запазените WebRTC ICE сървъри ще бъдат премахнати</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Saved from" xml:space="preserve"> + <source>Saved from</source> + <target>Запазено от</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved message" xml:space="preserve"> + <source>Saved message</source> + <target>Запазено съобщение</target> + <note>message info title</note> + </trans-unit> + <trans-unit id="Saving %lld messages" xml:space="preserve"> + <source>Saving %lld messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scale" xml:space="preserve"> + <source>Scale</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan / Paste link" xml:space="preserve"> + <source>Scan / Paste link</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> <target>Сканирай QR код</target> @@ -4276,6 +6316,7 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Scan QR code from desktop" xml:space="preserve"> <source>Scan QR code from desktop</source> + <target>Сканирай QR код от настолното устройство</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Scan code" xml:space="preserve"> @@ -4298,11 +6339,29 @@ This is your link for group %@!</source> <target>Търсене</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Search bar accepts invitation links." xml:space="preserve"> + <source>Search bar accepts invitation links.</source> + <target>Лентата за търсене приема линк за връзка.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search or paste SimpleX link" xml:space="preserve"> + <source>Search or paste SimpleX link</source> + <target>Търсене или поставяне на SimpleX линк</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secondary" xml:space="preserve"> + <source>Secondary</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> <source>Secure queue</source> <target>Сигурна опашка</target> <note>server test step</note> </trans-unit> + <trans-unit id="Secured" xml:space="preserve"> + <source>Secured</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> <target>Оценка на сигурността</target> @@ -4316,6 +6375,18 @@ This is your link for group %@!</source> <trans-unit id="Select" xml:space="preserve"> <source>Select</source> <target>Избери</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Select chat profile" xml:space="preserve"> + <source>Select chat profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected %lld" xml:space="preserve"> + <source>Selected %lld</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct" xml:space="preserve"> @@ -4353,11 +6424,6 @@ This is your link for group %@!</source> <target>Изпращайте потвърждениe за доставка на</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve"> - <source>Send direct message</source> - <target>Изпрати лично съобщение</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Send direct message to connect" xml:space="preserve"> <source>Send direct message to connect</source> <target>Изпрати лично съобщение за свързване</target> @@ -4368,6 +6434,10 @@ This is your link for group %@!</source> <target>Изпрати изчезващо съобщение</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send errors" xml:space="preserve"> + <source>Send errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> <target>Изпрати визуализация на линковете</target> @@ -4378,14 +6448,25 @@ This is your link for group %@!</source> <target>Изпрати съобщение на живо</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send message to enable calls." xml:space="preserve"> + <source>Send message to enable calls.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when your or destination server does not support private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> <target>Изпращай известия</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> - <source>Send notifications:</source> - <target>Изпратени известия:</target> + <trans-unit id="Send private reports" xml:space="preserve"> + <source>Send private reports</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve"> @@ -4403,10 +6484,15 @@ This is your link for group %@!</source> <target>Изпрати от галерия или персонализирани клавиатури.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve"> + <source>Send up to 100 last messages to new members.</source> + <target>Изпращане до последните 100 съобщения на нови членове.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> <target>Подателят отмени прехвърлянето на файла.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> @@ -4463,6 +6549,10 @@ This is your link for group %@!</source> <target>Изпратено на: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Sent directly" xml:space="preserve"> + <source>Sent directly</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> <target>Събитие за изпратен файл</target> @@ -4473,11 +6563,59 @@ This is your link for group %@!</source> <target>Изпратено съобщение</target> <note>message info title</note> </trans-unit> + <trans-unit id="Sent messages" xml:space="preserve"> + <source>Sent messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> <target>Изпратените съобщения ще бъдат изтрити след зададеното време.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sent reply" xml:space="preserve"> + <source>Sent reply</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent total" xml:space="preserve"> + <source>Sent total</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent via proxy" xml:space="preserve"> + <source>Sent via proxy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server" xml:space="preserve"> + <source>Server</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server added to operator %@." xml:space="preserve"> + <source>Server added to operator %@.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Server address" xml:space="preserve"> + <source>Server address</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings." xml:space="preserve"> + <source>Server address is incompatible with network settings.</source> + <note>srv error text.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Server address is incompatible with network settings: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server operator changed." xml:space="preserve"> + <source>Server operator changed.</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Server operators" xml:space="preserve"> + <source>Server operators</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server protocol changed." xml:space="preserve"> + <source>Server protocol changed.</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> <target>Сървърът изисква оторизация за създаване на опашки, проверете паролата</target> @@ -4493,13 +6631,34 @@ This is your link for group %@!</source> <target>Тестът на сървъра е неуспешен!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Server type" xml:space="preserve"> + <source>Server type</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server version is incompatible with network settings." xml:space="preserve"> + <source>Server version is incompatible with network settings.</source> + <note>srv error text</note> + </trans-unit> + <trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve"> + <source>Server version is incompatible with your app: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Servers" xml:space="preserve"> <source>Servers</source> <target>Сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Servers info" xml:space="preserve"> + <source>Servers info</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Session code" xml:space="preserve"> <source>Session code</source> + <target>Код на сесията</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set 1 day" xml:space="preserve"> @@ -4507,11 +6666,19 @@ This is your link for group %@!</source> <target>Задай 1 ден</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set chat name…" xml:space="preserve"> + <source>Set chat name…</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> <target>Задай име на контакт…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve"> + <source>Set default theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> <target>Задай групови настройки</target> @@ -4522,11 +6689,20 @@ This is your link for group %@!</source> <target>Задайте го вместо системната идентификация.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve"> + <source>Set message expiration in chats.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passcode" xml:space="preserve"> <source>Set passcode</source> <target>Задай kод за достъп</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set passphrase" xml:space="preserve"> + <source>Set passphrase</source> + <target>Задаване на парола</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> <target>Задай парола за експортиране</target> @@ -4547,24 +6723,50 @@ This is your link for group %@!</source> <target>Настройки</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve"> + <source>Settings were changed.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve"> + <source>Shape profile images</source> + <target>Променете формата на профилните изображения</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share" xml:space="preserve"> <source>Share</source> <target>Сподели</target> - <note>chat item action</note> + <note>alert action +chat item action</note> </trans-unit> <trans-unit id="Share 1-time link" xml:space="preserve"> <source>Share 1-time link</source> <target>Сподели еднократен линк</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share 1-time link with a friend" xml:space="preserve"> + <source>Share 1-time link with a friend</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve"> + <source>Share SimpleX address on social media.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address" xml:space="preserve"> <source>Share address</source> <target>Сподели адрес</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share address publicly" xml:space="preserve"> + <source>Share address publicly</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address with contacts?" xml:space="preserve"> <source>Share address with contacts?</source> <target>Сподели адреса с контактите?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Share from other apps." xml:space="preserve"> + <source>Share from other apps.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share link" xml:space="preserve"> @@ -4572,9 +6774,17 @@ This is your link for group %@!</source> <target>Сподели линк</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve"> - <source>Share one-time invitation link</source> - <target>Сподели линк за еднократна покана</target> + <trans-unit id="Share profile" xml:space="preserve"> + <source>Share profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share this 1-time invite link" xml:space="preserve"> + <source>Share this 1-time invite link</source> + <target>Сподели този еднократен линк за връзка</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share to SimpleX" xml:space="preserve"> + <source>Share to SimpleX</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share with contacts" xml:space="preserve"> @@ -4582,6 +6792,15 @@ This is your link for group %@!</source> <target>Сподели с контактите</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Short link" xml:space="preserve"> + <source>Short link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show QR code" xml:space="preserve"> + <source>Show QR code</source> + <target>Покажи QR код</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> <target>Показване на обажданията в хронологията на телефона</target> @@ -4597,21 +6816,41 @@ This is your link for group %@!</source> <target>Показване на последните съобщения в листа с чатовете</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show message status" xml:space="preserve"> + <source>Show message status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show percentage" xml:space="preserve"> + <source>Show percentage</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Показване на визуализация</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show → on messages sent via private routing." xml:space="preserve"> + <source>Show → on messages sent via private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show:" xml:space="preserve"> <source>Show:</source> <target>Покажи:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve"> + <source>SimpleX</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Address" xml:space="preserve"> <source>SimpleX Address</source> <target>SimpleX Адрес</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." xml:space="preserve"> + <source>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> <target>Сигурността на SimpleX Chat беше одитирана от Trail of Bits.</target> @@ -4642,6 +6881,18 @@ This is your link for group %@!</source> <target>SimpleX адрес</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX address and 1-time links are safe to share via any messenger." xml:space="preserve"> + <source>SimpleX address and 1-time links are safe to share via any messenger.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address or 1-time link?" xml:space="preserve"> + <source>SimpleX address or 1-time link?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX channel link" xml:space="preserve"> + <source>SimpleX channel link</source> + <note>simplex link type</note> + </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> <target>SimpleX адрес за контакт</target> @@ -4660,6 +6911,16 @@ This is your link for group %@!</source> <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> <target>SimpleX линкове</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve"> + <source>SimpleX links are prohibited.</source> + <target>SimpleX линкове са забранени в тази група.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve"> + <source>SimpleX links not allowed</source> + <target>SimpleX линковете не са разрешени</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> @@ -4667,11 +6928,19 @@ This is your link for group %@!</source> <target>Еднократна покана за SimpleX</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> + <source>SimpleX protocols reviewed by Trail of Bits.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Simplified incognito mode" xml:space="preserve"> <source>Simplified incognito mode</source> <target>Опростен режим инкогнито</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Size" xml:space="preserve"> + <source>Size</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Пропускане</target> @@ -4687,19 +6956,45 @@ This is your link for group %@!</source> <target>Малки групи (максимум 20)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Soft" xml:space="preserve"> + <source>Soft</source> + <note>blur media</note> + </trans-unit> + <trans-unit id="Some app settings were not migrated." xml:space="preserve"> + <source>Some app settings were not migrated.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some file(s) were not exported:" xml:space="preserve"> + <source>Some file(s) were not exported:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Някои не-фатални грешки са възникнали по време на импортиране - може да видите конзолата за повече подробности.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import:" xml:space="preserve"> + <source>Some non-fatal errors occurred during import:</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some servers failed the test: %@" xml:space="preserve"> + <source>Some servers failed the test: +%@</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> <target>Някой</target> <note>notification title</note> </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve"> - <source>Start a new chat</source> - <target>Започни нов чат</target> + <trans-unit id="Spam" xml:space="preserve"> + <source>Spam</source> + <note>blocking reason +report reason</note> + </trans-unit> + <trans-unit id="Square, circle, or anything in between." xml:space="preserve"> + <source>Square, circle, or anything in between.</source> + <target>Квадрат, кръг или нещо между тях.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start chat" xml:space="preserve"> @@ -4707,11 +7002,24 @@ This is your link for group %@!</source> <target>Започни чат</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Start chat?" xml:space="preserve"> + <source>Start chat?</source> + <target>Стартирай чата?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> <target>Започни миграция</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Starting from %@." xml:space="preserve"> + <source>Starting from %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Statistics" xml:space="preserve"> + <source>Statistics</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> <target>Спри</target> @@ -4722,9 +7030,9 @@ This is your link for group %@!</source> <target>Спри SimpleX</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> - <source>Stop chat to enable database actions</source> - <target>Спрете чата, за да активирате действията с базата данни</target> + <trans-unit id="Stop chat" xml:space="preserve"> + <source>Stop chat</source> + <target>Спри чата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> @@ -4755,23 +7063,56 @@ This is your link for group %@!</source> <trans-unit id="Stop sharing" xml:space="preserve"> <source>Stop sharing</source> <target>Спри споделянето</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Stop sharing address?" xml:space="preserve"> <source>Stop sharing address?</source> <target>Спри споделянето на адреса?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Stopping chat" xml:space="preserve"> + <source>Stopping chat</source> + <target>Спиране на чата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Storage" xml:space="preserve"> + <source>Storage</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Strong" xml:space="preserve"> + <source>Strong</source> + <note>blur media</note> + </trans-unit> <trans-unit id="Submit" xml:space="preserve"> <source>Submit</source> <target>Изпрати</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Subscribed" xml:space="preserve"> + <source>Subscribed</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscription errors" xml:space="preserve"> + <source>Subscription errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscriptions ignored" xml:space="preserve"> + <source>Subscriptions ignored</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Support SimpleX Chat" xml:space="preserve"> <source>Support SimpleX Chat</source> <target>Подкрепете SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Switch audio and video during the call." xml:space="preserve"> + <source>Switch audio and video during the call.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> + <source>Switch chat profile for 1-time invitations.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="System" xml:space="preserve"> <source>System</source> <target>Системен</target> @@ -4782,11 +7123,19 @@ This is your link for group %@!</source> <target>Системна идентификация</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP connection" xml:space="preserve"> + <source>TCP connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> <target>Времето на изчакване за установяване на TCP връзка</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP port for messaging" xml:space="preserve"> + <source>TCP port for messaging</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <source>TCP_KEEPCNT</source> <target>TCP_KEEPCNT</target> @@ -4802,11 +7151,19 @@ This is your link for group %@!</source> <target>TCP_KEEPINTVL</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tail" xml:space="preserve"> + <source>Tail</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> <target>Направи снимка</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap Create SimpleX address in the menu to create it later." xml:space="preserve"> + <source>Tap Create SimpleX address in the menu to create it later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> <target>Докосни бутона </target> @@ -4814,6 +7171,7 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Tap to Connect" xml:space="preserve"> <source>Tap to Connect</source> + <target>Докосни за свързване</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to activate profile." xml:space="preserve"> @@ -4831,16 +7189,29 @@ This is your link for group %@!</source> <target>Докосни за инкогнито вход</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve"> - <source>Tap to start a new chat</source> - <target>Докосни за започване на нов чат</target> + <trans-unit id="Tap to paste link" xml:space="preserve"> + <source>Tap to paste link</source> + <target>Докосни за поставяне на линк за връзка</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap to scan" xml:space="preserve"> + <source>Tap to scan</source> + <target>Докосни за сканиране</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Temporary file error" xml:space="preserve"> + <source>Temporary file error</source> + <note>file error alert title</note> + </trans-unit> <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> <target>Тестът е неуспешен на стъпка %@.</target> <note>server test failure</note> </trans-unit> + <trans-unit id="Test notifications" xml:space="preserve"> + <source>Test notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> <target>Тествай сървър</target> @@ -4854,7 +7225,7 @@ This is your link for group %@!</source> <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> <target>Тестовете са неуспешни!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> @@ -4871,11 +7242,6 @@ This is your link for group %@!</source> <target>Благодарение на потребителите – допринесете през Weblate!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> - <target>Първата платформа без никакви потребителски идентификатори – поверителна по дизайн.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <source>The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised.</source> @@ -4888,11 +7254,28 @@ It can happen because of some bug or when the connection is compromised.</source <target>Приложението може да ви уведоми, когато получите съобщения или заявки за контакт - моля, отворете настройките, за да активирате.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The app protects your privacy by using different operators in each conversation." xml:space="preserve"> + <source>The app protects your privacy by using different operators in each conversation.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve"> + <source>The app will ask to confirm downloads from unknown file servers (except .onion).</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> <source>The attempt to change database passphrase was not completed.</source> <target>Опитът за промяна на паролата на базата данни не беше завършен.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve"> + <source>The code you scanned is not a SimpleX link QR code.</source> + <target>QR кодът, който сканирахте, не е SimpleX линк за връзка.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The connection reached the limit of undelivered messages, your contact may be offline." xml:space="preserve"> + <source>The connection reached the limit of undelivered messages, your contact may be offline.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> <target>Връзката, която приехте, ще бъде отказана!</target> @@ -4913,6 +7296,11 @@ It can happen because of some bug or when the connection is compromised.</source <target>Криптирането работи и новото споразумение за криптиране не е необходимо. Това може да доведе до грешки при свързване!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> + <target>Ново поколение поверителни съобщения</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The hash of the previous message is different." xml:space="preserve"> <source>The hash of the previous message is different.</source> <target>Хешът на предишното съобщение е различен.</target> @@ -4928,9 +7316,12 @@ It can happen because of some bug or when the connection is compromised.</source <target>Съобщението ще бъде маркирано като модерирано за всички членове.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> - <target>Ново поколение поверителни съобщения</target> + <trans-unit id="The messages will be deleted for all members." xml:space="preserve"> + <source>The messages will be deleted for all members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve"> + <source>The messages will be marked as moderated for all members.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> @@ -4938,9 +7329,12 @@ It can happen because of some bug or when the connection is compromised.</source <target>Старата база данни не бе премахната по време на миграцията, тя може да бъде изтрита.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> - <target>Профилът се споделя само с вашите контакти.</target> + <trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve"> + <source>The same conditions will apply to operator **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second preset operator in the app!" xml:space="preserve"> + <source>The second preset operator in the app!</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> @@ -4958,19 +7352,25 @@ It can happen because of some bug or when the connection is compromised.</source <target>Сървърите за нови връзки на текущия ви чат профил **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Theme" xml:space="preserve"> - <source>Theme</source> - <target>Тема</target> + <trans-unit id="The servers for new files of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new files of your current chat profile **%@**.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve"> - <source>There should be at least one user profile.</source> - <target>Трябва да има поне един потребителски профил.</target> + <trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve"> + <source>The text you pasted is not a SimpleX link.</source> + <target>Текстът, който поставихте, не е SimpleX линк за връзка.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> - <source>There should be at least one visible user profile.</source> - <target>Трябва да има поне един видим потребителски профил.</target> + <trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve"> + <source>The uploaded database archive will be permanently removed from the servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Themes" xml:space="preserve"> + <source>Themes</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="These conditions will also apply for: **%@**." xml:space="preserve"> + <source>These conditions will also apply for: **%@**.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> @@ -4993,13 +7393,33 @@ It can happen because of some bug or when the connection is compromised.</source <target>Това действие не може да бъде отменено - съобщенията, изпратени и получени по-рано от избраното, ще бъдат изтрити. Може да отнеме няколко минути.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> <target>Това действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by end-to-end encryption.</source> + <target>Този чат е защитен чрез криптиране от край до край.</target> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by quantum resistant end-to-end encryption.</source> + <target>Този чат е защитен от квантово устойчиво криптиране от край до край.</target> + <note>E2EE info chat item</note> + </trans-unit> <trans-unit id="This device name" xml:space="preserve"> <source>This device name</source> + <target>Името на това устройство</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve"> + <source>This display name is invalid. Please choose another name.</source> + <target>Това име е невалидно. Моля, изберете друго име.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> @@ -5014,10 +7434,24 @@ It can happen because of some bug or when the connection is compromised.</source </trans-unit> <trans-unit id="This is your own SimpleX address!" xml:space="preserve"> <source>This is your own SimpleX address!</source> + <target>Това е вашият личен SimpleX адрес!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This is your own one-time link!" xml:space="preserve"> <source>This is your own one-time link!</source> + <target>Това е вашят еднократен линк за връзка!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve"> + <source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve"> + <source>This link was used with another mobile device, please create a new link on the desktop.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This message was deleted or not received yet." xml:space="preserve"> + <source>This message was deleted or not received yet.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> @@ -5025,6 +7459,10 @@ It can happen because of some bug or when the connection is compromised.</source <target>Тази настройка се прилага за съобщения в текущия ви профил **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Title" xml:space="preserve"> + <source>Title</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> <source>To ask any questions and to receive updates:</source> <target>За да задавате въпроси и да получавате актуализации:</target> @@ -5037,6 +7475,7 @@ It can happen because of some bug or when the connection is compromised.</source </trans-unit> <trans-unit id="To hide unwanted messages." xml:space="preserve"> <source>To hide unwanted messages.</source> + <target>Скриване на нежелани съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To make a new connection" xml:space="preserve"> @@ -5044,9 +7483,8 @@ It can happen because of some bug or when the connection is compromised.</source <target>За да направите нова връзка</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <target>За да се защити поверителността, вместо потребителски идентификатори, използвани от всички други платформи, SimpleX има идентификатори за опашки от съобщения, отделни за всеки от вашите контакти.</target> + <trans-unit id="To protect against your link being replaced, you can compare contact security codes." xml:space="preserve"> + <source>To protect against your link being replaced, you can compare contact security codes.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -5054,6 +7492,10 @@ It can happen because of some bug or when the connection is compromised.</source <target>За да не се разкрива часовата зона, файловете с изображения/глас използват UTC.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve"> + <source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> @@ -5061,6 +7503,23 @@ You will be prompted to complete authentication before this feature is enabled.< Ще бъдете подканени да извършите идентификация, преди тази функция да бъде активирана.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> + <target>За да се защити поверителността, вместо потребителски идентификатори, използвани от всички други платформи, SimpleX има идентификатори за опашки от съобщения, отделни за всеки от вашите контакти.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To receive" xml:space="preserve"> + <source>To receive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve"> + <source>To record speech please grant permission to use Microphone.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record video please grant permission to use Camera." xml:space="preserve"> + <source>To record video please grant permission to use Camera.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> <target>За да запишете гласово съобщение, моля, дайте разрешение за използване на микрофон.</target> @@ -5071,26 +7530,54 @@ You will be prompted to complete authentication before this feature is enabled.< <target>За да разкриете своя скрит профил, въведете пълна парола в полето за търсене на страницата **Вашите чат профили**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To send" xml:space="preserve"> + <source>To send</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> <source>To support instant push notifications the chat database has to be migrated.</source> <target>За поддръжка на незабавни push известия, базата данни за чат трябва да бъде мигрирана.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To use the servers of **%@**, accept conditions of use." xml:space="preserve"> + <source>To use the servers of **%@**, accept conditions of use.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> <target>За да проверите криптирането от край до край с вашия контакт, сравнете (или сканирайте) кода на вашите устройства.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle chat list:" xml:space="preserve"> + <source>Toggle chat list:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> <source>Toggle incognito when connecting.</source> <target>Избор на инкогнито при свързване.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Token status: %@." xml:space="preserve"> + <source>Token status: %@.</source> + <note>token status</note> + </trans-unit> + <trans-unit id="Toolbar opacity" xml:space="preserve"> + <source>Toolbar opacity</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Total" xml:space="preserve"> + <source>Total</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>Транспортна изолация</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Transport sessions" xml:space="preserve"> + <source>Transport sessions</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> <target>Опит за свързване със сървъра, използван за получаване на съобщения от този контакт (грешка: %@).</target> @@ -5101,16 +7588,16 @@ You will be prompted to complete authentication before this feature is enabled.< <target>Опит за свързване със сървъра, използван за получаване на съобщения от този контакт.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Turkish interface" xml:space="preserve"> + <source>Turkish interface</source> + <target>Турски интерфейс</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> <target>Изключи</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve"> - <source>Turn off notifications?</source> - <target>Изключи известията?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> <target>Включи</target> @@ -5123,20 +7610,32 @@ You will be prompted to complete authentication before this feature is enabled.< </trans-unit> <trans-unit id="Unblock" xml:space="preserve"> <source>Unblock</source> + <target>Отблокирай</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unblock for all" xml:space="preserve"> + <source>Unblock for all</source> + <target>Отблокирай за всички</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unblock member" xml:space="preserve"> <source>Unblock member</source> + <target>Отблокирай член</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unblock member for all?" xml:space="preserve"> + <source>Unblock member for all?</source> + <target>Отблокиране на член за всички?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unblock member?" xml:space="preserve"> <source>Unblock member?</source> + <target>Отблокирай член?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve"> - <source>Unexpected error: %@</source> - <target>Неочаквана грешка: %@</target> - <note>item status description</note> + <trans-unit id="Undelivered messages" xml:space="preserve"> + <source>Undelivered messages</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -5146,7 +7645,7 @@ You will be prompted to complete authentication before this feature is enabled.< <trans-unit id="Unfav." xml:space="preserve"> <source>Unfav.</source> <target>Премахни от любимите</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> @@ -5183,6 +7682,10 @@ You will be prompted to complete authentication before this feature is enabled.< <target>Непозната грешка</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unknown servers!" xml:space="preserve"> + <source>Unknown servers!</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> <target>Освен ако не използвате интерфейса за повикване на iOS, активирайте режима "Не безпокой", за да избегнете прекъсвания.</target> @@ -5197,10 +7700,12 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Unlink" xml:space="preserve"> <source>Unlink</source> + <target>Забрави</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unlink desktop?" xml:space="preserve"> <source>Unlink desktop?</source> + <target>Забрави настолно устройство?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unlock" xml:space="preserve"> @@ -5216,11 +7721,20 @@ To connect, please ask your contact to create another connection link and check <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> <target>Уведомявай</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> </trans-unit> <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> <target>Непрочетено</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Unsupported connection link" xml:space="preserve"> + <source>Unsupported connection link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve"> + <source>Up to 100 last messages are sent to new members.</source> + <target>На новите членове се изпращат до последните 100 съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update" xml:space="preserve"> @@ -5228,11 +7742,6 @@ To connect, please ask your contact to create another connection link and check <target>Актуализация</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> - <source>Update .onion hosts setting?</source> - <target>Актуализиране на настройката за .onion хостове?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> <target>Актуализирай паролата на базата данни</target> @@ -5243,9 +7752,12 @@ To connect, please ask your contact to create another connection link and check <target>Актуализиране на мрежовите настройки?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve"> - <source>Update transport isolation mode?</source> - <target>Актуализиране на режима на изолация на транспорта?</target> + <trans-unit id="Update settings?" xml:space="preserve"> + <source>Update settings?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updated conditions" xml:space="preserve"> + <source>Updated conditions</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> @@ -5253,31 +7765,64 @@ To connect, please ask your contact to create another connection link and check <target>Актуализирането на настройките ще свърже отново клиента към всички сървъри.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> - <target>Актуализирането на тази настройка ще свърже повторно клиента към всички сървъри.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Upgrade and open chat" xml:space="preserve"> <source>Upgrade and open chat</source> <target>Актуализирай и отвори чата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Upload errors" xml:space="preserve"> + <source>Upload errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload failed" xml:space="preserve"> + <source>Upload failed</source> + <target>Неуспешно качване</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Upload file" xml:space="preserve"> <source>Upload file</source> <target>Качи файл</target> <note>server test step</note> </trans-unit> + <trans-unit id="Uploaded" xml:space="preserve"> + <source>Uploaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploaded files" xml:space="preserve"> + <source>Uploaded files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploading archive" xml:space="preserve"> + <source>Uploading archive</source> + <target>Архивът се качва</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use %@" xml:space="preserve"> + <source>Use %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> <target>Използвай .onion хостове</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use SOCKS proxy" xml:space="preserve"> + <source>Use SOCKS proxy</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> <target>Използвай сървърите на SimpleX Chat?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use TCP port %@ when no port is specified." xml:space="preserve"> + <source>Use TCP port %@ when no port is specified.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve"> + <source>Use TCP port 443 for preset servers only.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> <target>Използвай чата</target> @@ -5288,6 +7833,14 @@ To connect, please ask your contact to create another connection link and check <target>Използвай текущия профил</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use for files" xml:space="preserve"> + <source>Use for files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for messages" xml:space="preserve"> + <source>Use for messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Използвай за нови връзки</target> @@ -5295,6 +7848,7 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Use from desktop" xml:space="preserve"> <source>Use from desktop</source> + <target>Използвай от настолно устройство</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use iOS call interface" xml:space="preserve"> @@ -5307,19 +7861,51 @@ To connect, please ask your contact to create another connection link and check <target>Използвай нов инкогнито профил</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use only local notifications?" xml:space="preserve"> + <source>Use only local notifications?</source> + <target>Използвай само локални известия?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve"> + <source>Use private routing with unknown servers when IP address is not protected.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers." xml:space="preserve"> + <source>Use private routing with unknown servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Използвай сървър</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> - <target>Потребителски профил</target> + <trans-unit id="Use servers" xml:space="preserve"> + <source>Use servers</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> - <target>Използването на .onion хостове изисква съвместим VPN доставчик.</target> + <trans-unit id="Use short links (BETA)" xml:space="preserve"> + <source>Use short links (BETA)</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app while in the call." xml:space="preserve"> + <source>Use the app while in the call.</source> + <target>Използвайте приложението по време на разговора.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app with one hand." xml:space="preserve"> + <source>Use the app with one hand.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use web port" xml:space="preserve"> + <source>Use web port</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="User selection" xml:space="preserve"> + <source>User selection</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Username" xml:space="preserve"> + <source>Username</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> @@ -5329,24 +7915,37 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Verify code with desktop" xml:space="preserve"> <source>Verify code with desktop</source> + <target>Потвърди кода с настолното устройство</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify connection" xml:space="preserve"> <source>Verify connection</source> + <target>Потвърди връзка</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify connection security" xml:space="preserve"> <source>Verify connection security</source> - <target>Потвръди сигурността на връзката</target> + <target>Потвърди сигурността на връзката</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify connections" xml:space="preserve"> <source>Verify connections</source> + <target>Потвърждение за свързване</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify database passphrase" xml:space="preserve"> + <source>Verify database passphrase</source> + <target>Проверете паролата на базата данни</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify passphrase" xml:space="preserve"> + <source>Verify passphrase</source> + <target>Провери паролата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify security code" xml:space="preserve"> <source>Verify security code</source> - <target>Потвръди кода за сигурност</target> + <target>Потвърди кода за сигурност</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Via browser" xml:space="preserve"> @@ -5356,6 +7955,7 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Via secure quantum resistant protocol." xml:space="preserve"> <source>Via secure quantum resistant protocol.</source> + <target>Чрез сигурен квантово устойчив протокол.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Video call" xml:space="preserve"> @@ -5378,11 +7978,24 @@ To connect, please ask your contact to create another connection link and check <target>Видео и файлове до 1gb</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View conditions" xml:space="preserve"> + <source>View conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> <target>Виж кода за сигурност</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View updated conditions" xml:space="preserve"> + <source>View updated conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Visible history" xml:space="preserve"> + <source>Visible history</source> + <target>Видима история</target> + <note>chat feature</note> + </trans-unit> <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> <target>Гласови съобщения</target> @@ -5393,11 +8006,16 @@ To connect, please ask your contact to create another connection link and check <target>Гласовите съобщения са забранени в този чат.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <target>Гласовите съобщения са забранени в тази група.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Voice messages not allowed" xml:space="preserve"> + <source>Voice messages not allowed</source> + <target>Гласовите съобщения не са разрешени</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> <source>Voice messages prohibited!</source> <target>Гласовите съобщения са забранени!</target> @@ -5410,6 +8028,7 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Waiting for desktop..." xml:space="preserve"> <source>Waiting for desktop...</source> + <target>Изчакване на настолно устройство…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Waiting for file" xml:space="preserve"> @@ -5427,6 +8046,19 @@ To connect, please ask your contact to create another connection link and check <target>Изчаква се получаването на видеото</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wallpaper accent" xml:space="preserve"> + <source>Wallpaper accent</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper background" xml:space="preserve"> + <source>Wallpaper background</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve"> + <source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source> + <target>Внимание: стартирането на чата на множество устройства не се поддържа и ще доведе до неуспешно изпращане на съобщения</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> <source>Warning: you may lose some data!</source> <target>Предупреждение: Може да загубите някои данни!</target> @@ -5447,6 +8079,11 @@ To connect, please ask your contact to create another connection link and check <target>Съобщение при посрещане</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Welcome message is too long" xml:space="preserve"> + <source>Welcome message is too long</source> + <target>Съобщението при посрещане е твърде дълго</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="What's new" xml:space="preserve"> <source>What's new</source> <target>Какво е новото</target> @@ -5457,9 +8094,13 @@ To connect, please ask your contact to create another connection link and check <target>Когато са налични</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> - <source>When people request to connect, you can accept or reject it.</source> - <target>Когато хората искат да се свържат с вас, можете да ги приемете или отхвърлите.</target> + <trans-unit id="When connecting audio and video calls." xml:space="preserve"> + <source>When connecting audio and video calls.</source> + <target>При свързване на аудио и видео разговори.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When more than one operator is enabled, none of them has metadata to learn who communicates with whom." xml:space="preserve"> + <source>When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> @@ -5467,29 +8108,69 @@ To connect, please ask your contact to create another connection link and check <target>Когато споделяте инкогнито профил с някого, този профил ще се използва за групите, в които той ви кани.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="WiFi" xml:space="preserve"> + <source>WiFi</source> + <target>WiFi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Will be enabled in direct chats!" xml:space="preserve"> + <source>Will be enabled in direct chats!</source> + <target>Ще бъде активирано в личните чатове!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wired ethernet" xml:space="preserve"> + <source>Wired ethernet</source> + <target>Кабелен Ethernet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With encrypted files and media." xml:space="preserve"> + <source>With encrypted files and media.</source> + <target>С криптирани файлове и медия.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> <target>С незадължително съобщение при посрещане.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="With reduced battery usage." xml:space="preserve"> + <source>With reduced battery usage.</source> + <target>С намален разход на батерията.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to file servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> <target>Грешна парола за базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve"> + <source>Wrong key or unknown connection - most likely this connection is deleted.</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Wrong key or unknown file chunk address - most likely file is deleted." xml:space="preserve"> + <source>Wrong key or unknown file chunk address - most likely file is deleted.</source> + <note>file error text</note> + </trans-unit> <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> <target>Грешна парола!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="XFTP servers" xml:space="preserve"> - <source>XFTP servers</source> - <target>XFTP сървъри</target> + <trans-unit id="XFTP server" xml:space="preserve"> + <source>XFTP server</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> - <target>Вие</target> + <trans-unit id="You **must not** use the same database on two devices." xml:space="preserve"> + <source>You **must not** use the same database on two devices.</source> + <target>**Не трябва** да използвате една и съща база данни на две устройства.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You accepted connection" xml:space="preserve"> @@ -5504,7 +8185,7 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="You already have a chat profile with the same display name. Please choose another name." xml:space="preserve"> <source>You already have a chat profile with the same display name. Please choose another name.</source> - <target>Вече имате чат профил със същото показвано име. Моля, изберете друго име.</target> + <target>Вече имате чат профил със същото име. Моля, изберете друго име.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already connected to %@." xml:space="preserve"> @@ -5512,33 +8193,45 @@ To connect, please ask your contact to create another connection link and check <target>Вече сте вече свързани с %@.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are already connected with %@." xml:space="preserve"> + <source>You are already connected with %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You are already connecting to %@." xml:space="preserve"> <source>You are already connecting to %@.</source> + <target>Вече се свързвате с %@.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already connecting via this one-time link!" xml:space="preserve"> <source>You are already connecting via this one-time link!</source> + <target>Вече се свързвате чрез този еднократен линк за връзка!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already in group %@." xml:space="preserve"> <source>You are already in group %@.</source> + <target>Вече сте в група %@.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already joining the group %@." xml:space="preserve"> <source>You are already joining the group %@.</source> + <target>Вече се присъединявате към групата %@.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already joining the group via this link!" xml:space="preserve"> <source>You are already joining the group via this link!</source> + <target>Вие вече се присъединявате към групата чрез този линк!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already joining the group via this link." xml:space="preserve"> <source>You are already joining the group via this link.</source> + <target>Вие вече се присъединявате към групата чрез този линк.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already joining the group! Repeat join request?" xml:space="preserve"> <source>You are already joining the group! Repeat join request?</source> + <target>Вече се присъединихте към групата! +Изпрати отново заявката за присъединяване?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve"> @@ -5551,14 +8244,21 @@ Repeat join request?</source> <target>Поканени сте в групата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are not connected to these servers. Private routing is used to deliver messages to them." xml:space="preserve"> + <source>You are not connected to these servers. Private routing is used to deliver messages to them.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> <source>You can accept calls from lock screen, without device and app authentication.</source> <target>Можете да приемате обаждания от заключен екран, без идентификация на устройство и приложението.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <target>Можете също да се свържете, като натиснете върху линка. Ако се отвори в браузъра, натиснете върху бутона **Отваряне в мобилно приложение**.</target> + <trans-unit id="You can change it in Appearance settings." xml:space="preserve"> + <source>You can change it in Appearance settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can configure servers via settings." xml:space="preserve"> + <source>You can configure servers via settings.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can create it later" xml:space="preserve"> @@ -5576,16 +8276,34 @@ Repeat join request?</source> <target>Можете да ги активирате по-късно през настройките за "Поверителност и сигурност" на приложението.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can give another try." xml:space="preserve"> + <source>You can give another try.</source> + <target>Можете да опитате още веднъж.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <source>You can hide or mute a user profile - swipe it to the right.</source> <target>Можете да скриете или заглушите известията за потребителски профил - плъзнете надясно.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> + <trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve"> + <source>You can make it visible to your SimpleX contacts via Settings.</source> + <target>Можете да го направите видим за вашите контакти в SimpleX чрез Настройки.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now chat with %@" xml:space="preserve"> + <source>You can now chat with %@</source> <target>Вече можете да изпращате съобщения до %@</target> <note>notification body</note> </trans-unit> + <trans-unit id="You can send messages to %@ from Archived contacts." xml:space="preserve"> + <source>You can send messages to %@ from Archived contacts.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can set connection name, to remember who the link was shared with." xml:space="preserve"> + <source>You can set connection name, to remember who the link was shared with.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> <target>Можете да зададете визуализация на известията на заключен екран през настройките.</target> @@ -5601,16 +8319,15 @@ Repeat join request?</source> <target>Можете да споделите този адрес с вашите контакти, за да им позволите да се свържат с **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> - <source>You can share your address as a link or QR code - anybody can connect to you.</source> - <target>Можете да споделите адреса си като линк или QR код - всеки може да се свърже с вас.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> <target>Можете да започнете чат през Настройки на приложението / База данни или като рестартирате приложението</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve"> + <source>You can still view conversation with %@ in the list of chats.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> <target>Можете да включите SimpleX заключване през Настройки.</target> @@ -5621,33 +8338,36 @@ Repeat join request?</source> <target>Можете да използвате markdown за форматиране на съобщенията:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can view invitation link again in connection details." xml:space="preserve"> + <source>You can view invitation link again in connection details.</source> + <target>Можете да видите отново линкът за покана в подробностите за връзката.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> <target>Не може да изпращате съобщения!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <target>Вие контролирате през кой сървър(и) **да получавате** съобщенията, вашите контакти – сървърите, които използвате, за да им изпращате съобщения.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <target>Не можахте да бъдете потвърдени; Моля, опитайте отново.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> + <target>Хората могат да се свържат с вас само чрез ликовете, които споделяте.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You have already requested connection via this address!" xml:space="preserve"> <source>You have already requested connection via this address!</source> + <target>Вече сте заявили връзка през този адрес!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You have already requested connection! Repeat connection request?" xml:space="preserve"> <source>You have already requested connection! Repeat connection request?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> - <target>Нямате чатове</target> + <target>Вече сте направили заявката за връзка! +Изпрати отново заявката за свързване?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> @@ -5670,11 +8390,23 @@ Repeat connection request?</source> <target>Вие се присъединихте към тази група. Свързване с поканващия член на групата.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You may migrate the exported database." xml:space="preserve"> + <source>You may migrate the exported database.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may save the exported archive." xml:space="preserve"> + <source>You may save the exported archive.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> <target>Трябва да използвате най-новата версия на вашата чат база данни САМО на едно устройство, в противен случай може да спрете да получавате съобщения от някои контакти.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You need to allow your contact to call to be able to call them." xml:space="preserve"> + <source>You need to allow your contact to call to be able to call them.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> <target>Трябва да разрешите на вашия контакт да изпраща гласови съобщения, за да можете да ги изпращате.</target> @@ -5690,6 +8422,10 @@ Repeat connection request?</source> <target>Изпратихте покана за групата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You should receive notifications." xml:space="preserve"> + <source>You should receive notifications.</source> + <note>token info</note> + </trans-unit> <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> <target>Ще бъдете свързани с групата, когато устройството на домакина на групата е онлайн, моля, изчакайте или проверете по-късно!</target> @@ -5697,6 +8433,7 @@ Repeat connection request?</source> </trans-unit> <trans-unit id="You will be connected when group link host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected when group link host's device is online, please wait or check later!</source> + <target>Ще бъдете свързани, когато устройството на хоста на груповата връзка е онлайн, моля, изчакайте или проверете по-късно!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve"> @@ -5716,6 +8453,7 @@ Repeat connection request?</source> </trans-unit> <trans-unit id="You will connect to all group members." xml:space="preserve"> <source>You will connect to all group members.</source> + <target>Ще се свържете с всички членове на групата.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve"> @@ -5723,6 +8461,10 @@ Repeat connection request?</source> <target>Все още ще получавате обаждания и известия от заглушени профили, когато са активни.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You will stop receiving messages from this chat. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this chat. Chat history will be preserved.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> <target>Ще спрете да получавате съобщения от тази група. Историята на чата ще бъде запазена.</target> @@ -5743,29 +8485,14 @@ Repeat connection request?</source> <target>Използвате инкогнито профил за тази група - за да се предотврати споделянето на основния ви профил, поканите на контакти не са разрешени</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your %@ servers" xml:space="preserve"> - <source>Your %@ servers</source> - <target>Вашите %@ сървъри</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your ICE servers" xml:space="preserve"> <source>Your ICE servers</source> <target>Вашите ICE сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> - <target>Вашите SMP сървъри</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your SimpleX address" xml:space="preserve"> <source>Your SimpleX address</source> - <target>Вашият SimpleX адрес</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your XFTP servers" xml:space="preserve"> - <source>Your XFTP servers</source> - <target>Вашите XFTP сървъри</target> + <target>Вашият адрес в SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your calls" xml:space="preserve"> @@ -5775,24 +8502,25 @@ Repeat connection request?</source> </trans-unit> <trans-unit id="Your chat database" xml:space="preserve"> <source>Your chat database</source> - <target>Вашата чат база данни</target> + <target>Вашата база данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your chat database is not encrypted - set passphrase to encrypt it." xml:space="preserve"> <source>Your chat database is not encrypted - set passphrase to encrypt it.</source> - <target>Вашата чат база данни не е криптирана - задайте парола, за да я криптирате.</target> + <target>Вашата база данни не е криптирана - задайте парола, за да я криптирате.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your chat preferences" xml:space="preserve"> + <source>Your chat preferences</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>Вашите чат профили</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> - <target>Вашият контакт трябва да бъде онлайн, за да осъществите връзката. -Можете да откажете тази връзка и да премахнете контакта (и да опитате по -късно с нов линк).</target> + <trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve"> + <source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> @@ -5805,21 +8533,18 @@ You can cancel this connection and remove the contact (and try later with a new <target>Вашите контакти могат да позволят пълното изтриване на съобщението.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> - <source>Your contacts in SimpleX will see it. -You can change it in Settings.</source> - <target>Вашите контакти в SimpleX ще го видят. -Можете да го промените в Настройки.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your contacts will remain connected." xml:space="preserve"> <source>Your contacts will remain connected.</source> <target>Вашите контакти ще останат свързани.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve"> + <source>Your credentials may be sent unencrypted.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> - <target>Вашата текуща чат база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната.</target> + <target>Вашата текуща база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your current profile" xml:space="preserve"> @@ -5839,6 +8564,7 @@ You can change it in Settings.</source> </trans-unit> <trans-unit id="Your profile" xml:space="preserve"> <source>Your profile</source> + <target>Вашият профил</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> @@ -5846,33 +8572,34 @@ You can change it in Settings.</source> <target>Вашият профил **%@** ще бъде споделен.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> - <target>Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти. -SimpleX сървърите не могат да видят вашия профил.</target> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> + <target>Профилът се споделя само с вашите контакти.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> - <target>Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство.</target> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> + <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> + <target>Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти. SimpleX сървърите не могат да видят вашия профил.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve"> + <source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> <target>Вашият автоматично генериран профил</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> - <target>Вашият сървър</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> <target>Вашият адрес на сървъра</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your servers" xml:space="preserve"> + <source>Your servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your settings" xml:space="preserve"> <source>Your settings</source> <target>Вашите настройки</target> @@ -5913,11 +8640,20 @@ SimpleX сървърите не могат да видят вашия профи <target>обаждането прието</target> <note>call status</note> </trans-unit> + <trans-unit id="accepted invitation" xml:space="preserve"> + <source>accepted invitation</source> + <note>chat list item title</note> + </trans-unit> <trans-unit id="admin" xml:space="preserve"> <source>admin</source> <target>админ</target> <note>member role</note> </trans-unit> + <trans-unit id="admins" xml:space="preserve"> + <source>admins</source> + <target>администратори</target> + <note>feature role</note> + </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> <target>съгласуване на криптиране за %@…</target> @@ -5928,6 +8664,11 @@ SimpleX сървърите не могат да видят вашия профи <target>съгласуване на криптиране…</target> <note>chat item text</note> </trans-unit> + <trans-unit id="all members" xml:space="preserve"> + <source>all members</source> + <target>всички членове</target> + <note>feature role</note> + </trans-unit> <trans-unit id="always" xml:space="preserve"> <source>always</source> <target>винаги</target> @@ -5935,6 +8676,15 @@ SimpleX сървърите не могат да видят вашия профи </trans-unit> <trans-unit id="and %lld other events" xml:space="preserve"> <source>and %lld other events</source> + <target>и %lld други събития</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="archived report" xml:space="preserve"> + <source>archived report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="attempts" xml:space="preserve"> + <source>attempts</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> @@ -5944,6 +8694,7 @@ SimpleX сървърите не могат да видят вашия профи </trans-unit> <trans-unit id="author" xml:space="preserve"> <source>author</source> + <target>автор</target> <note>member role</note> </trans-unit> <trans-unit id="bad message ID" xml:space="preserve"> @@ -5958,13 +8709,29 @@ SimpleX сървърите не могат да видят вашия профи </trans-unit> <trans-unit id="blocked" xml:space="preserve"> <source>blocked</source> - <note>No comment provided by engineer.</note> + <target>блокиран</target> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="blocked %@" xml:space="preserve"> + <source>blocked %@</source> + <target>блокиран %@</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="blocked by admin" xml:space="preserve"> + <source>blocked by admin</source> + <target>блокиран от админ</target> + <note>blocked chat item +marked deleted chat item preview text</note> </trans-unit> <trans-unit id="bold" xml:space="preserve"> <source>bold</source> <target>удебелен</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="call" xml:space="preserve"> + <source>call</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="call error" xml:space="preserve"> <source>call error</source> <target>грешка при повикване</target> @@ -5987,7 +8754,7 @@ SimpleX сървърите не могат да видят вашия профи </trans-unit> <trans-unit id="changed address for you" xml:space="preserve"> <source>changed address for you</source> - <target>променен е адреса за вас</target> + <target>адреса за изпращане е променен</target> <note>chat item text</note> </trans-unit> <trans-unit id="changed role of %@ to %@" xml:space="preserve"> @@ -6068,7 +8835,7 @@ SimpleX сървърите не могат да видят вашия профи <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> <target>свързване…</target> - <note>chat list item title</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> @@ -6080,6 +8847,11 @@ SimpleX сървърите не могат да видят вашия профи <target>връзка:%@</target> <note>connection information</note> </trans-unit> + <trans-unit id="contact %@ changed to %@" xml:space="preserve"> + <source>contact %1$@ changed to %2$@</source> + <target>името на контакта %1$@ е променено на %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> <target>контактът има e2e криптиране</target> @@ -6110,10 +8882,15 @@ SimpleX сървърите не могат да видят вашия профи <target>дни</target> <note>time unit</note> </trans-unit> + <trans-unit id="decryption errors" xml:space="preserve"> + <source>decryption errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> <target>по подразбиране (%@)</target> - <note>pref value</note> + <note>delete after time +pref value</note> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> @@ -6132,11 +8909,12 @@ SimpleX сървърите не могат да видят вашия профи </trans-unit> <trans-unit id="deleted contact" xml:space="preserve"> <source>deleted contact</source> + <target>изтрит контакт</target> <note>rcv direct event chat item</note> </trans-unit> <trans-unit id="deleted group" xml:space="preserve"> <source>deleted group</source> - <target>групата изтрита</target> + <target>групата е изтрита</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="different migration in the app/database: %@ / %@" xml:space="preserve"> @@ -6159,6 +8937,10 @@ SimpleX сървърите не могат да видят вашия профи <target>дублирано съобщение</target> <note>integrity error chat item</note> </trans-unit> + <trans-unit id="duplicates" xml:space="preserve"> + <source>duplicates</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> <target>e2e криптиран</target> @@ -6234,9 +9016,13 @@ SimpleX сървърите не могат да видят вашия профи <target>грешка</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="event happened" xml:space="preserve"> - <source>event happened</source> - <target>събитие се случи</target> + <trans-unit id="expired" xml:space="preserve"> + <source>expired</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="forwarded" xml:space="preserve"> + <source>forwarded</source> + <target>препратено</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> @@ -6264,6 +9050,10 @@ SimpleX сървърите не могат да видят вашия профи <target>iOS Keychain ще се използва за сигурно съхраняване на паролата, след като рестартирате приложението или промените паролата - това ще позволи получаването на push известия.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="inactive" xml:space="preserve"> + <source>inactive</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> <target>инкогнито чрез линк с адрес за контакт</target> @@ -6304,6 +9094,10 @@ SimpleX сървърите не могат да видят вашия профи <target>покана за група %@</target> <note>group name</note> </trans-unit> + <trans-unit id="invite" xml:space="preserve"> + <source>invite</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> <target>поканен</target> @@ -6349,11 +9143,20 @@ SimpleX сървърите не могат да видят вашия профи <target>член</target> <note>member role</note> </trans-unit> + <trans-unit id="member %@ changed to %@" xml:space="preserve"> + <source>member %1$@ changed to %2$@</source> + <target>името на члена %1$@ е променено на %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> <target>свързан</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="message" xml:space="preserve"> + <source>message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="message received" xml:space="preserve"> <source>message received</source> <target>получено съобщение</target> @@ -6377,7 +9180,11 @@ SimpleX сървърите не могат да видят вашия профи <trans-unit id="moderated by %@" xml:space="preserve"> <source>moderated by %@</source> <target>модерирано от %@</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="moderator" xml:space="preserve"> + <source>moderator</source> + <note>member role</note> </trans-unit> <trans-unit id="months" xml:space="preserve"> <source>months</source> @@ -6387,7 +9194,7 @@ SimpleX сървърите не могат да видят вашия профи <trans-unit id="never" xml:space="preserve"> <source>never</source> <target>никога</target> - <note>No comment provided by engineer.</note> + <note>delete after time</note> </trans-unit> <trans-unit id="new message" xml:space="preserve"> <source>new message</source> @@ -6418,8 +9225,8 @@ SimpleX сървърите не могат да видят вашия профи <source>off</source> <target>изключено</target> <note>enabled status - group pref value - time to disappear</note> +group pref value +time to disappear</note> </trans-unit> <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> @@ -6436,16 +9243,42 @@ SimpleX сървърите не могат да видят вашия профи <target>включено</target> <note>group pref value</note> </trans-unit> + <trans-unit id="other" xml:space="preserve"> + <source>other</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="other errors" xml:space="preserve"> + <source>other errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="owner" xml:space="preserve"> <source>owner</source> <target>собственик</target> <note>member role</note> </trans-unit> + <trans-unit id="owners" xml:space="preserve"> + <source>owners</source> + <target>собственици</target> + <note>feature role</note> + </trans-unit> <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> <target>peer-to-peer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="pending" xml:space="preserve"> + <source>pending</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="pending approval" xml:space="preserve"> + <source>pending approval</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="quantum resistant e2e encryption" xml:space="preserve"> + <source>quantum resistant e2e encryption</source> + <target>квантово устойчиво e2e криптиране</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> <target>получен отговор…</target> @@ -6456,6 +9289,10 @@ SimpleX сървърите не могат да видят вашия профи <target>получено потвърждение…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="rejected" xml:space="preserve"> + <source>rejected</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> <target>отхвърлено повикване</target> @@ -6471,11 +9308,39 @@ SimpleX сървърите не могат да видят вашия профи <target>отстранен %@</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="removed contact address" xml:space="preserve"> + <source>removed contact address</source> + <target>премахнат адрес за контакт</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="removed profile picture" xml:space="preserve"> + <source>removed profile picture</source> + <target>премахната профилна снимка</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> <target>ви острани</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="requested to connect" xml:space="preserve"> + <source>requested to connect</source> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="saved" xml:space="preserve"> + <source>saved</source> + <target>запазено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="saved from %@" xml:space="preserve"> + <source>saved from %@</source> + <target>запазено от %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="search" xml:space="preserve"> + <source>search</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="sec" xml:space="preserve"> <source>sec</source> <target>сек.</target> @@ -6501,6 +9366,27 @@ SimpleX сървърите не могат да видят вашия профи <target>изпрати лично съобщение</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve"> + <source>server queue info: %1$@ + +last received msg: %2$@</source> + <note>queue info</note> + </trans-unit> + <trans-unit id="set new contact address" xml:space="preserve"> + <source>set new contact address</source> + <target>зададен нов адрес за контакт</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="set new profile picture" xml:space="preserve"> + <source>set new profile picture</source> + <target>зададена нова профилна снимка</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="standard end-to-end encryption" xml:space="preserve"> + <source>standard end-to-end encryption</source> + <target>стандартно криптиране от край до край</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>стартиране…</target> @@ -6516,18 +9402,42 @@ SimpleX сървърите не могат да видят вашия профи <target>този контакт</target> <note>notification title</note> </trans-unit> + <trans-unit id="unblocked %@" xml:space="preserve"> + <source>unblocked %@</source> + <target>отблокиран %@</target> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="unknown" xml:space="preserve"> <source>unknown</source> <target>неизвестен</target> <note>connection info</note> </trans-unit> + <trans-unit id="unknown servers" xml:space="preserve"> + <source>unknown servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unknown status" xml:space="preserve"> + <source>unknown status</source> + <target>неизвестен статус</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unprotected" xml:space="preserve"> + <source>unprotected</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="updated group profile" xml:space="preserve"> <source>updated group profile</source> <target>актуализиран профил на групата</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="updated profile" xml:space="preserve"> + <source>updated profile</source> + <target>актуализиран профил</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="v%@" xml:space="preserve"> <source>v%@</source> + <target>v%@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="v%@ (%@)" xml:space="preserve"> @@ -6555,6 +9465,10 @@ SimpleX сървърите не могат да видят вашия профи <target>чрез реле</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="video" xml:space="preserve"> + <source>video</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> <source>video call (not e2e encrypted)</source> <target>видео разговор (не е e2e криптиран)</target> @@ -6580,11 +9494,20 @@ SimpleX сървърите не могат да видят вашия профи <target>седмици</target> <note>time unit</note> </trans-unit> + <trans-unit id="when IP hidden" xml:space="preserve"> + <source>when IP hidden</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="yes" xml:space="preserve"> <source>yes</source> <target>да</target> <note>pref value</note> </trans-unit> + <trans-unit id="you" xml:space="preserve"> + <source>you</source> + <target>вие</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> <target>вие сте поканени в групата</target> @@ -6595,14 +9518,19 @@ SimpleX сървърите не могат да видят вашия профи <target>вие сте наблюдател</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="you blocked %@" xml:space="preserve"> + <source>you blocked %@</source> + <target>вие блокирахте %@</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> - <target>променихте адреса</target> + <target>адреса за получаване е променен</target> <note>chat item text</note> </trans-unit> <trans-unit id="you changed address for %@" xml:space="preserve"> <source>you changed address for %@</source> - <target>променихте адреса за %@</target> + <target>променихте адреса получаване за %@</target> <note>chat item text</note> </trans-unit> <trans-unit id="you changed role for yourself to %@" xml:space="preserve"> @@ -6635,6 +9563,11 @@ SimpleX сървърите не могат да видят вашия профи <target>споделихте еднократен инкогнито линк за връзка</target> <note>chat list item description</note> </trans-unit> + <trans-unit id="you unblocked %@" xml:space="preserve"> + <source>you unblocked %@</source> + <target>вие отблокирахте %@</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you: " xml:space="preserve"> <source>you: </source> <target>вие: </target> @@ -6649,7 +9582,7 @@ SimpleX сървърите не могат да видят вашия профи </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="bg" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6669,6 +9602,7 @@ SimpleX сървърите не могат да видят вашия профи </trans-unit> <trans-unit id="NSLocalNetworkUsageDescription" xml:space="preserve"> <source>SimpleX uses local network access to allow using user chat profile via desktop app on the same network.</source> + <target>SimpleX използва достъп до локална мрежа, за да позволи използването на потребителския чат профил чрез настолно приложение в същата мрежа.</target> <note>Privacy - Local Network Usage Description</note> </trans-unit> <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> @@ -6685,7 +9619,7 @@ SimpleX сървърите не могат да видят вашия профи </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="bg" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> @@ -6705,4 +9639,205 @@ SimpleX сървърите не могат да видят вашия профи </trans-unit> </body> </file> + <file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="bg" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%d new events" xml:space="preserve"> + <source>%d new events</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From %d chat(s)" xml:space="preserve"> + <source>From %d chat(s)</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From: %@" xml:space="preserve"> + <source>From: %@</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <note>notification</note> + </trans-unit> + <trans-unit id="New messages" xml:space="preserve"> + <source>New messages</source> + <note>notification</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="bg" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> + <source>SimpleX SE</source> + <note>Bundle display name</note> + </trans-unit> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX SE</source> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> + <source>Copyright © 2024 SimpleX Chat. All rights reserved.</source> + <note>Copyright (human-readable)</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="bg" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App is locked!" xml:space="preserve"> + <source>App is locked!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Comment" xml:space="preserve"> + <source>Comment</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade required" xml:space="preserve"> + <source>Database downgrade required</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade required" xml:space="preserve"> + <source>Database upgrade required</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing file" xml:space="preserve"> + <source>Error preparing file</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing message" xml:space="preserve"> + <source>Error preparing message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No active profile" xml:space="preserve"> + <source>No active profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to downgrade the database." xml:space="preserve"> + <source>Open the app to downgrade the database.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to upgrade the database." xml:space="preserve"> + <source>Open the app to upgrade the database.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passphrase" xml:space="preserve"> + <source>Passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve"> + <source>Please create a profile in the SimpleX app</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending a message takes longer than expected." xml:space="preserve"> + <source>Sending a message takes longer than expected.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending message…" xml:space="preserve"> + <source>Sending message…</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Slow network?" xml:space="preserve"> + <source>Slow network?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unsupported format" xml:space="preserve"> + <source>Unsupported format</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wait" xml:space="preserve"> + <source>Wait</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve"> + <source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + </body> + </file> </xliff> diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/contents.json b/apps/ios/SimpleX Localizations/bg.xcloc/contents.json index 23e8239ce8..66d64e6539 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/bg.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "bg", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "16.2" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff b/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff index f599f9c300..bf7753675e 100644 --- a/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff +++ b/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff @@ -193,20 +193,16 @@ <source>)</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> <source>**Create link / QR code** for your contact to use.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> @@ -217,8 +213,8 @@ <source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> @@ -386,8 +382,8 @@ <source>Add servers by scanning QR codes.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -1251,8 +1247,8 @@ <source>Direct messages</source> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> @@ -1271,8 +1267,8 @@ <source>Disappearing messages are prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disappears at" xml:space="preserve"> @@ -1751,24 +1747,24 @@ <source>Group links</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve"> - <source>Group members can add message reactions.</source> + <trans-unit id="Members can add message reactions." xml:space="preserve"> + <source>Members can add message reactions.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> <source>Group members can irreversibly delete sent messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> @@ -1899,8 +1895,8 @@ <source>Immediately</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Import" xml:space="preserve"> @@ -2020,8 +2016,8 @@ <source>Irreversible message deletion is prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve"> @@ -2207,8 +2203,8 @@ <source>Message reactions are prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> - <source>Message reactions are prohibited in this group.</source> + <trans-unit id="Message reactions are prohibited." xml:space="preserve"> + <source>Message reactions are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Message text" xml:space="preserve"> @@ -2239,8 +2235,8 @@ <source>Migration is completed</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> @@ -2409,8 +2405,8 @@ <source>Onion hosts will not be used.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> @@ -2477,8 +2473,8 @@ <source>Open user profiles</source> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Opening database…" xml:space="preserve"> @@ -2537,8 +2533,8 @@ <source>Paste the link you received into the box below to connect with your contact.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Periodically" xml:space="preserve"> @@ -3373,8 +3369,8 @@ <source>Thanks to the users – contribute via Weblate!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> @@ -3418,16 +3414,16 @@ It can happen because of some bug or when the connection is compromised.</source <source>The message will be marked as moderated for all members.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> <source>The old database was not removed during the migration, it can be deleted.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The sender will NOT be notified" xml:space="preserve"> @@ -3490,8 +3486,8 @@ It can happen because of some bug or when the connection is compromised.</source <source>To make a new connection</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -3724,8 +3720,8 @@ To connect, please ask your contact to create another connection link and check <source>Voice messages are prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> @@ -3876,10 +3872,6 @@ To connect, please ask your contact to create another connection link and check <source>You can't send messages!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <note>No comment provided by engineer.</note> 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 be8b23658b..0400839cb0 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -2,36 +2,9 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="cs" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> - <trans-unit id=" " xml:space="preserve"> - <source> -</source> - <target> -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (" xml:space="preserve"> - <source> (</source> - <target> (</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> <target> (lze kopírovat)</target> @@ -89,6 +62,7 @@ </trans-unit> <trans-unit id="%@ and %@" xml:space="preserve"> <source>%@ and %@</source> + <target>%@ a %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ and %@ connected" xml:space="preserve"> @@ -103,6 +77,12 @@ </trans-unit> <trans-unit id="%@ connected" xml:space="preserve"> <source>%@ connected</source> + <target>%@ připojen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve"> + <source>%@ downloaded</source> + <target>%@ staženo</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ is connected!" xml:space="preserve"> @@ -120,16 +100,30 @@ <target>%@ je ověřený</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ server" xml:space="preserve"> + <source>%@ server</source> + <target>%@ server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ servers" xml:space="preserve"> <source>%@ servers</source> <target>%@ servery</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve"> + <source>%@ uploaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ wants to connect!" xml:space="preserve"> <source>%@ wants to connect!</source> <target>%@ se chce připojit!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve"> + <source>%1$@, %2$@</source> + <target>%1$@, %2$@</target> + <note>format for date separator in chat</note> + </trans-unit> <trans-unit id="%@, %@ and %lld members" xml:space="preserve"> <source>%@, %@ and %lld members</source> <note>No comment provided by engineer.</note> @@ -149,11 +143,35 @@ <target>%d dní</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve"> + <source>%d file(s) are still being downloaded.</source> + <target>%d soubor(y) stále stahován(y).</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve"> + <source>%d file(s) failed to download.</source> + <target>%d soubor(y) se nepodařilo stáhnout.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve"> + <source>%d file(s) were deleted.</source> + <target>%d soubor(y) smazán(y).</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve"> + <source>%d file(s) were not downloaded.</source> + <target>%d soubor(y) nestažen(y).</target> + <note>forward confirmation reason</note> + </trans-unit> <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> <target>%d hodin</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve"> + <source>%d messages not forwarded</source> + <note>alert title</note> + </trans-unit> <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> <target>%d minuty</target> @@ -169,6 +187,10 @@ <target>%d sek</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve"> + <source>%d seconds(s)</source> + <note>delete after time</note> + </trans-unit> <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> <target>%d přeskočené zprávy</target> @@ -210,10 +232,17 @@ </trans-unit> <trans-unit id="%lld messages blocked" xml:space="preserve"> <source>%lld messages blocked</source> + <target>%lld zprávy blokovaný</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve"> + <source>%lld messages blocked by admin</source> + <target>%lld zprávy blokovaný adminem</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld messages marked deleted" xml:space="preserve"> <source>%lld messages marked deleted</source> + <target>%lld zprávy označeno jako smazáno</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld messages moderated by %@" xml:space="preserve"> @@ -230,11 +259,6 @@ <target>%d nové jazyky rozhraní</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> - <source>%lld second(s)</source> - <target>%lld vteřin</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="%lld seconds" xml:space="preserve"> <source>%lld seconds</source> <target>%lld vteřin</target> @@ -285,47 +309,35 @@ <target>%u zpráv přeskočeno.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> - <source>(</source> - <target>(</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="(new)" xml:space="preserve"> <source>(new)</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="(this device v%@)" xml:space="preserve"> <source>(this device v%@)</source> + <target>(toto zařízení v%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve"> - <source>)</source> - <target>)</target> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target>**Přidat nový kontakt**: pro vytvoření jednorázového QR kódu nebo odkazu pro váš kontakt.</target> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve"> + <source>**Create group**: to create a new group.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> - <source>**Create link / QR code** for your contact to use.</source> - <target>**Vytvořte odkaz / QR kód** pro váš kontakt.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Soukromější**: kontrolovat nové zprávy každých 20 minut. Token zařízení je sdílen se serverem SimpleX Chat, ale ne kolik máte kontaktů nebo zpráv.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Nejsoukromější**: nepoužívejte server oznámení SimpleX Chat, pravidelně kontrolujte zprávy na pozadí (závisí na tom, jak často aplikaci používáte).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <target>**Vložte přijatý odkaz** nebo jej otevřete v prohlížeči a klepněte na **Otevřít v mobilní aplikaci**.</target> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> @@ -333,14 +345,13 @@ <target>**Upozornění**: Pokud heslo ztratíte, NEBUDETE jej moci obnovit ani změnit.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Doporučeno**: Token zařízení a oznámení se odesílají na oznamovací server SimpleX Chat, ale nikoli obsah, velikost nebo od koho jsou zprávy.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target>** Naskenujte QR kód**: pro připojení ke kontaktu osobně nebo prostřednictvím videohovoru.</target> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve"> + <source>**Scan / Paste link**: to connect via a link you received.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> @@ -348,6 +359,10 @@ <target>**Upozornění**: Okamžitě doručovaná oznámení vyžadují přístupové heslo uložené v Klíčence.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve"> + <source>**Warning**: the archive will be removed.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> <source>**e2e encrypted** audio call</source> <target>**e2e šifrovaný** audio hovor</target> @@ -363,16 +378,11 @@ <target>\*tučně*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=", " xml:space="preserve"> - <source>, </source> - <target>, </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable.</source> - <target>- připojit k [adresářová služba](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.cibule) (BETA)! + <target>- připojit k [adresářová služba](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)! - doručenky (až 20 členů). - Rychlejší a stabilnější.</target> <note>No comment provided by engineer.</note> @@ -401,11 +411,6 @@ - historie úprav.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="." xml:space="preserve"> - <source>.</source> - <target>.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="0 sec" xml:space="preserve"> <source>0 sec</source> <note>time to disappear</note> @@ -418,7 +423,8 @@ <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> <target>1 den</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> @@ -433,16 +439,25 @@ <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> <target>1 měsíc</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> <target>1 týden</target> - <note>time interval</note> + <note>delete after time +time interval</note> + </trans-unit> + <trans-unit id="1 year" xml:space="preserve"> + <source>1 year</source> + <note>delete after time</note> </trans-unit> <trans-unit id="1-time link" xml:space="preserve"> <source>1-time link</source> - <target>Jednorázový odkaz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="5 minutes" xml:space="preserve"> @@ -460,11 +475,6 @@ <target>30 vteřin</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve"> - <source>: </source> - <target>: </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> @@ -514,31 +524,29 @@ <target>Přerušit změnu adresy?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve"> - <source>About SimpleX</source> - <target>O SimpleX</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve"> <source>About SimpleX Chat</source> <target>O SimpleX chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve"> - <source>About SimpleX address</source> - <target>O SimpleX adrese</target> + <trans-unit id="About operators" xml:space="preserve"> + <source>About operators</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accent color" xml:space="preserve"> - <source>Accent color</source> - <target>Zbarvení</target> + <trans-unit id="Accent" xml:space="preserve"> + <source>Accent</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept" xml:space="preserve"> <source>Accept</source> <target>Přijmout</target> <note>accept contact request via notification - accept incoming call via notification</note> +accept incoming call via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accept conditions" xml:space="preserve"> + <source>Accept conditions</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept connection request?" xml:space="preserve"> <source>Accept connection request?</source> @@ -553,16 +561,40 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Přijmout inkognito</target> - <note>accept contact request via notification</note> + <note>accept contact request via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve"> + <source>Accepted conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve"> + <source>Acknowledged</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve"> + <source>Acknowledgement errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Active" xml:space="preserve"> + <source>Active</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve"> + <source>Active connections</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> <target>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.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> - <source>Add preset servers</source> - <target>Přidejte přednastavené servery</target> + <trans-unit id="Add friends" xml:space="preserve"> + <source>Add friends</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve"> + <source>Add list</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add profile" xml:space="preserve"> @@ -570,14 +602,18 @@ <target>Přidat profil</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> + <target>Přidat server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> <source>Add servers by scanning QR codes.</source> <target>Přidejte servery skenováním QR kódů.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> - <target>Přidat server…</target> + <trans-unit id="Add team members" xml:space="preserve"> + <source>Add team members</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -585,11 +621,39 @@ <target>Přidat do jiného zařízení</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add to list" xml:space="preserve"> + <source>Add to list</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> <target>Přidat uvítací zprávu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve"> + <source>Add your team members to the conversations.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve"> + <source>Added media & file servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve"> + <source>Added message servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve"> + <source>Additional accent</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve"> + <source>Additional accent 2</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve"> + <source>Additional secondary</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Address" xml:space="preserve"> <source>Address</source> <target>Adresa</target> @@ -600,6 +664,18 @@ <target>Změna adresy bude přerušena. Budou použity staré přijímací adresy.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve"> + <source>Address or 1-time link?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve"> + <source>Address settings</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve"> + <source>Admins can block a member for all.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> <source>Admins can create the links to join groups.</source> <target>Správci mohou vytvářet odkazy pro připojení ke skupinám.</target> @@ -610,6 +686,14 @@ <target>Pokročilá nastavení sítě</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve"> + <source>Advanced settings</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All" xml:space="preserve"> + <source>All</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All app data is deleted." xml:space="preserve"> <source>All app data is deleted.</source> <target>Všechna data aplikace jsou smazána.</target> @@ -620,16 +704,32 @@ <target>Všechny chaty a zprávy budou smazány – tuto akci nelze vrátit zpět!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> <target>Všechna data se při zadání vymažou.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve"> + <source>All data is kept private on your device.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> <target>Všichni členové skupiny zůstanou připojeni.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> <target>Všechny zprávy budou smazány – tuto akci nelze vrátit zpět! Zprávy budou smazány POUZE pro vás.</target> @@ -639,6 +739,18 @@ <source>All new messages from %@ will be hidden!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All profiles" xml:space="preserve"> + <source>All profiles</source> + <note>profile dropdown</note> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve"> + <source>All reports will be archived for you.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All servers" xml:space="preserve"> + <source>All servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All your contacts will remain connected." xml:space="preserve"> <source>All your contacts will remain connected.</source> <target>Všechny vaše kontakty zůstanou připojeny.</target> @@ -649,6 +761,10 @@ <target>Všechny vaše kontakty zůstanou připojeny. Aktualizace profilu bude odeslána vašim kontaktům.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow" xml:space="preserve"> <source>Allow</source> <target>Povolit</target> @@ -659,14 +775,22 @@ <target>Povolte hovory, pouze pokud je váš kontakt povolí.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve"> + <source>Allow calls?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> <target>Povolte mizící zprávy, pouze pokud vám to váš kontakt dovolí.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <target>Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí.</target> + <trans-unit id="Allow downgrade" xml:space="preserve"> + <source>Allow downgrade</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target>Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí. (24 hodin)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> @@ -689,9 +813,21 @@ <target>Povolit odesílání mizících zpráv.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow to irreversibly delete sent messages.</source> - <target>Povolit nevratné smazání odeslaných zpráv.</target> + <trans-unit id="Allow sharing" xml:space="preserve"> + <source>Allow sharing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target>Povolit nevratné smazání odeslaných zpráv. (24 hodin)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve"> + <source>Allow to report messsages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve"> + <source>Allow to send SimpleX links.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send files and media." xml:space="preserve"> @@ -724,9 +860,9 @@ <target>Povolte svým kontaktům vám volat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow your contacts to irreversibly delete sent messages.</source> - <target>Umožněte svým kontaktům nevratně odstranit odeslané zprávy.</target> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target>Umožněte svým kontaktům nevratně odstranit odeslané zprávy. (24 hodin)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> @@ -752,6 +888,10 @@ <source>Already joining the group!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve"> + <source>Always use private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> <source>Always use relay</source> <target>Spojení přes relé</target> @@ -762,21 +902,38 @@ <target>Vytvořit prázdný chat profil se zadaným názvem a otevřít aplikaci jako obvykle.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Another reason" xml:space="preserve"> + <source>Another reason</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> <target>Přijmout hovor</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> + <target>Servery může provozovat kdokoli.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> <target>Sestavení aplikace: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App data migration" xml:space="preserve"> + <source>App data migration</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> <source>App encrypts new local files (except videos).</source> <target>Aplikace šifruje nové místní soubory (s výjimkou videí).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App group:" xml:space="preserve"> + <source>App group:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>Ikona aplikace</target> @@ -792,6 +949,10 @@ <target>Přístupový kód aplikace je nahrazen sebedestrukčním přístupovým heslem.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App session" xml:space="preserve"> + <source>App session</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App version" xml:space="preserve"> <source>App version</source> <target>Verze aplikace</target> @@ -807,6 +968,54 @@ <target>Vzhled</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Apply" xml:space="preserve"> + <source>Apply</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve"> + <source>Apply to</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve"> + <source>Archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve"> + <source>Archive %lld reports?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve"> + <source>Archive all reports?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve"> + <source>Archive and upload</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve"> + <source>Archive contacts to chat later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve"> + <source>Archive report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve"> + <source>Archive report?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve"> + <source>Archive reports</source> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve"> + <source>Archived contacts</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve"> + <source>Archiving database</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Attach" xml:space="preserve"> <source>Attach</source> <target>Připojit</target> @@ -867,11 +1076,19 @@ <target>Automaticky přijímat obrázky</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve"> + <source>Auto-accept settings</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Back" xml:space="preserve"> <source>Back</source> <target>Zpět</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Background" xml:space="preserve"> + <source>Background</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Bad desktop address" xml:space="preserve"> <source>Bad desktop address</source> <note>No comment provided by engineer.</note> @@ -886,19 +1103,59 @@ <target>Špatný hash zprávy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better calls" xml:space="preserve"> + <source>Better calls</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better groups" xml:space="preserve"> <source>Better groups</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve"> + <source>Better groups performance</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve"> + <source>Better message dates.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better messages" xml:space="preserve"> <source>Better messages</source> <target>Lepší zprávy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better networking" xml:space="preserve"> + <source>Better networking</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve"> + <source>Better notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve"> + <source>Better privacy and security</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve"> + <source>Better security ✅</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve"> + <source>Better user experience</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Black" xml:space="preserve"> + <source>Black</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block" xml:space="preserve"> <source>Block</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block for all" xml:space="preserve"> + <source>Block for all</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block group members" xml:space="preserve"> <source>Block group members</source> <note>No comment provided by engineer.</note> @@ -907,18 +1164,34 @@ <source>Block member</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve"> + <source>Block member for all?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block member?" xml:space="preserve"> <source>Block member?</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve"> + <source>Blocked by admin</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve"> + <source>Blur for better privacy.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve"> + <source>Blur media</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <source>Both you and your contact can add message reactions.</source> <target>Vy i váš kontakt můžete přidávat reakce na zprávy.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> - <source>Both you and your contact can irreversibly delete sent messages.</source> - <target>Vy i váš kontakt můžete nevratně mazat odeslané zprávy.</target> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target>Vy i váš kontakt můžete nevratně mazat odeslané zprávy. (24 hodin)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> @@ -941,11 +1214,29 @@ <target>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)!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Business address" xml:space="preserve"> + <source>Business address</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve"> + <source>Business chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve"> + <source>Businesses</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>Podle chat profilu (výchozí) nebo [podle připojení](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve"> + <source>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> <source>Call already ended!</source> <target>Hovor již skončil!</target> @@ -956,9 +1247,20 @@ <target>Hovory</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve"> - <source>Can't delete user profile!</source> - <target>Nemohu smazat uživatelský profil!</target> + <trans-unit id="Calls prohibited!" xml:space="preserve"> + <source>Calls prohibited!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve"> + <source>Camera not available</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve"> + <source>Can't call contact</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve"> + <source>Can't call member</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contact!" xml:space="preserve"> @@ -971,9 +1273,18 @@ <target>Nelze pozvat kontakty!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve"> + <source>Can't message member</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> <target>Zrušit</target> + <note>alert action +alert button</note> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve"> + <source>Cancel migration</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> @@ -981,9 +1292,21 @@ <target>Nelze získat přístup ke klíčence pro uložení hesla databáze</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> <target>Nelze přijmout soubor</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve"> + <source>Cellular</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change" xml:space="preserve"> @@ -991,6 +1314,14 @@ <target>Změnit</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve"> + <source>Change automatic message deletion?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve"> + <source>Change chat profiles</source> + <note>authentication reason</note> + </trans-unit> <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> <target>Změnit přístupovou frázi databáze?</target> @@ -1035,11 +1366,22 @@ <source>Change self-destruct passcode</source> <target>Změnit sebedestrukční heslo</target> <note>authentication reason - set passcode view</note> +set passcode view</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> - <source>Chat archive</source> - <target>Chat se archivuje</target> + <trans-unit id="Chat" xml:space="preserve"> + <source>Chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve"> + <source>Chat already exists</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve"> + <source>Chat already exists!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve"> + <source>Chat colors</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat console" xml:space="preserve"> @@ -1057,6 +1399,10 @@ <target>Databáze chatu odstraněna</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve"> + <source>Chat database exported</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> <target>Importovaná databáze chatu</target> @@ -1072,26 +1418,71 @@ <target>Chat je zastaven</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve"> + <source>Chat list</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve"> + <source>Chat migrated!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> <target>Předvolby chatu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve"> + <source>Chat preferences were changed.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve"> + <source>Chat profile</source> + <target>Profil uživatele</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve"> + <source>Chat theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> <target>Chaty</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve"> + <source>Check messages every 20 min.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve"> + <source>Check messages when allowed.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> <target>Zkontrolujte adresu serveru a zkuste to znovu.</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> <target>Čínské a Španělské rozhranní</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> <target>Vybrat soubor</target> @@ -1102,10 +1493,22 @@ <target>Vybrat z knihovny</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve"> + <source>Chunks deleted</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve"> + <source>Chunks downloaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve"> + <source>Chunks uploaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> <target>Vyčistit</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> @@ -1117,16 +1520,35 @@ <target>Vyčistit konverzaci?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve"> + <source>Clear group?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve"> + <source>Clear or delete group?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve"> + <source>Clear private notes?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear verification" xml:space="preserve"> <source>Clear verification</source> <target>Zrušte ověření</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> - <source>Colors</source> - <target>Barvy</target> + <trans-unit id="Color chats with the new themes." xml:space="preserve"> + <source>Color chats with the new themes.</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Color mode" xml:space="preserve"> + <source>Color mode</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve"> + <source>Community guidelines violation</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Compare file" xml:space="preserve"> <source>Compare file</source> <target>Porovnat soubor</target> @@ -1137,11 +1559,47 @@ <target>Porovnejte bezpečnostní kódy se svými kontakty.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Completed" xml:space="preserve"> + <source>Completed</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve"> + <source>Conditions accepted on: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve"> + <source>Conditions of use</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve"> + <source>Conditions will be accepted on: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> <target>Konfigurace serverů ICE</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Configure server operators" xml:space="preserve"> + <source>Configure server operators</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> <target>Potvrdit</target> @@ -1152,11 +1610,23 @@ <target>Potvrdit heslo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve"> + <source>Confirm contact deletion?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm database upgrades" xml:space="preserve"> <source>Confirm database upgrades</source> <target>Potvrdit aktualizaci databáze</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve"> + <source>Confirm files from unknown servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve"> + <source>Confirm network settings</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> <target>Potvrdit novou heslovou frázi…</target> @@ -1167,6 +1637,18 @@ <target>Potvrdit heslo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve"> + <source>Confirm upload</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve"> + <source>Confirmed</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> <target>Připojit</target> @@ -1185,6 +1667,10 @@ <source>Connect to desktop</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve"> + <source>Connect to your friends faster.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connect to yourself?" xml:space="preserve"> <source>Connect to yourself?</source> <note>No comment provided by engineer.</note> @@ -1208,11 +1694,6 @@ This is your own one-time link!</source> <target>Připojte se prostřednictvím odkazu</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve"> - <source>Connect via link / QR code</source> - <target>Připojit se prostřednictvím odkazu / QR kódu</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connect via one-time link" xml:space="preserve"> <source>Connect via one-time link</source> <target>Připojit se jednorázovým odkazem</target> @@ -1222,14 +1703,26 @@ This is your own one-time link!</source> <source>Connect with %@</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected" xml:space="preserve"> + <source>Connected</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected desktop" xml:space="preserve"> <source>Connected desktop</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve"> + <source>Connected servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected to desktop" xml:space="preserve"> <source>Connected to desktop</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting" xml:space="preserve"> + <source>Connecting</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> <source>Connecting to server…</source> <target>Připojování k serveru…</target> @@ -1240,6 +1733,10 @@ This is your own one-time link!</source> <target>Připojování k serveru... (chyba: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve"> + <source>Connecting to contact, please wait or check later!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting to desktop" xml:space="preserve"> <source>Connecting to desktop</source> <note>No comment provided by engineer.</note> @@ -1249,6 +1746,14 @@ This is your own one-time link!</source> <target>Připojení</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve"> + <source>Connection and servers status.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve"> + <source>Connection blocked</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> <target>Chyba připojení</target> @@ -1259,11 +1764,32 @@ This is your own one-time link!</source> <target>Chyba spojení (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve"> + <source>Connection is blocked by server operator: +%@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve"> + <source>Connection not ready.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve"> + <source>Connection notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Požadavek na připojení byl odeslán!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve"> + <source>Connection requires encryption renegotiation.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve"> + <source>Connection security</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection terminated" xml:space="preserve"> <source>Connection terminated</source> <note>No comment provided by engineer.</note> @@ -1273,6 +1799,14 @@ This is your own one-time link!</source> <target>Časový limit připojení</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve"> + <source>Connection with desktop stopped</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve"> + <source>Connections</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact allows" xml:space="preserve"> <source>Contact allows</source> <target>Kontakt povolil</target> @@ -1283,6 +1817,10 @@ This is your own one-time link!</source> <target>Kontakt již existuje</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact deleted!" xml:space="preserve"> + <source>Contact deleted!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> <target>Skrytý kontakt:</target> @@ -1293,9 +1831,8 @@ This is your own one-time link!</source> <target>Kontakt je připojen</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve"> - <source>Contact is not connected yet!</source> - <target>Kontakt ještě není připojen!</target> + <trans-unit id="Contact is deleted." xml:space="preserve"> + <source>Contact is deleted.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact name" xml:space="preserve"> @@ -1308,6 +1845,10 @@ This is your own one-time link!</source> <target>Předvolby kontaktů</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve"> + <source>Contact will be deleted - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> <target>Kontakty</target> @@ -1318,21 +1859,37 @@ This is your own one-time link!</source> <target>Kontakty mohou označit zprávy ke smazání; vy je budete moci zobrazit.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve"> + <source>Content violates conditions of use</source> + <note>blocking reason</note> + </trans-unit> <trans-unit id="Continue" xml:space="preserve"> <source>Continue</source> <target>Pokračovat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve"> + <source>Conversation deleted!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> <target>Kopírovat</target> - <note>chat item action</note> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve"> + <source>Copy error</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> <target>Verze jádra: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Corner" xml:space="preserve"> + <source>Corner</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Correct name to %@?" xml:space="preserve"> <source>Correct name to %@?</source> <note>No comment provided by engineer.</note> @@ -1342,6 +1899,10 @@ This is your own one-time link!</source> <target>Vytvořit</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve"> + <source>Create 1-time link</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> <target>Vytvořit SimpleX adresu</target> @@ -1351,11 +1912,6 @@ This is your own one-time link!</source> <source>Create a group using a random profile.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> - <source>Create an address to let people connect with you.</source> - <target>Vytvořit adresu, aby se s vámi lidé mohli spojit.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create file" xml:space="preserve"> <source>Create file</source> <target>Vytvořit soubor</target> @@ -1375,18 +1931,18 @@ This is your own one-time link!</source> <target>Vytvořit odkaz</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create list" xml:space="preserve"> + <source>Create list</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> <target>Vytvořit nový profil v [desktop app](https://simplex.chat/downloads/). 💻</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve"> - <source>Create one-time invitation link</source> - <target>Vytvořit jednorázovou pozvánku</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create profile" xml:space="preserve"> <source>Create profile</source> + <target>Vytvořte si profil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create queue" xml:space="preserve"> @@ -1404,9 +1960,24 @@ This is your own one-time link!</source> <target>Vytvořte si profil</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve"> - <source>Created on %@</source> - <target>Vytvořeno na %@</target> + <trans-unit id="Created" xml:space="preserve"> + <source>Created</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve"> + <source>Created at</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve"> + <source>Created at: %@</source> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve"> + <source>Creating archive link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve"> + <source>Creating link…</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current Passcode" xml:space="preserve"> @@ -1414,11 +1985,19 @@ This is your own one-time link!</source> <target>Aktuální heslo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> <target>Aktuální přístupová fráze…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current profile" xml:space="preserve"> + <source>Current profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> <target>Aktuálně maximální podporovaná velikost souboru je %@.</target> @@ -1429,11 +2008,23 @@ This is your own one-time link!</source> <target>Vlastní čas</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve"> + <source>Customizable message shape.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve"> + <source>Customize theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> <target>Tmavý</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve"> + <source>Dark mode colors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> <target>ID databáze</target> @@ -1532,6 +2123,10 @@ This is your own one-time link!</source> <target>Databáze bude přenesena po restartu aplikace</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Debug delivery" xml:space="preserve"> + <source>Debug delivery</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> <target>Decentralizované</target> @@ -1545,17 +2140,17 @@ This is your own one-time link!</source> <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> <target>Smazat</target> - <note>chat item action</note> + <note>alert action +swipe action</note> + </trans-unit> + <trans-unit id="Delete %lld messages of members?" xml:space="preserve"> + <source>Delete %lld messages of members?</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete %lld messages?" xml:space="preserve"> <source>Delete %lld messages?</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve"> - <source>Delete Contact</source> - <target>Smazat kontakt</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Delete address" xml:space="preserve"> <source>Delete address</source> <target>Odstranit adresu</target> @@ -1580,14 +2175,12 @@ This is your own one-time link!</source> <source>Delete and notify contact</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve"> - <source>Delete archive</source> - <target>Smazat archiv</target> + <trans-unit id="Delete chat" xml:space="preserve"> + <source>Delete chat</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve"> - <source>Delete chat archive?</source> - <target>Smazat archiv chatu?</target> + <trans-unit id="Delete chat messages from your device." xml:space="preserve"> + <source>Delete chat messages from your device.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile" xml:space="preserve"> @@ -1600,6 +2193,10 @@ This is your own one-time link!</source> <target>Smazat chat profil?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete chat?" xml:space="preserve"> + <source>Delete chat?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> <source>Delete connection</source> <target>Smazat připojení</target> @@ -1610,9 +2207,8 @@ This is your own one-time link!</source> <target>Smazat kontakt</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact? This cannot be undone!" xml:space="preserve"> - <source>Delete contact? -This cannot be undone!</source> + <trans-unit id="Delete contact?" xml:space="preserve"> + <source>Delete contact?</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete database" xml:space="preserve"> @@ -1620,6 +2216,10 @@ This cannot be undone!</source> <target>Odstranění databáze</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete database from this device" xml:space="preserve"> + <source>Delete database from this device</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete file" xml:space="preserve"> <source>Delete file</source> <target>Smazat soubor</target> @@ -1670,6 +2270,10 @@ This cannot be undone!</source> <target>Smazat odkaz?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete list?" xml:space="preserve"> + <source>Delete list?</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> <target>Smazat zprávu člena?</target> @@ -1683,7 +2287,7 @@ This cannot be undone!</source> <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> <target>Smazat zprávy</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> @@ -1700,9 +2304,8 @@ This cannot be undone!</source> <target>Smazat starou databázi?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve"> - <source>Delete pending connection</source> - <target>Smazat čekající připojení</target> + <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> + <source>Delete or moderate up to 200 messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection?" xml:space="preserve"> @@ -1720,11 +2323,27 @@ This cannot be undone!</source> <target>Odstranit frontu</target> <note>server test step</note> </trans-unit> + <trans-unit id="Delete report" xml:space="preserve"> + <source>Delete report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete up to 20 messages at once." xml:space="preserve"> + <source>Delete up to 20 messages at once.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> <target>Smazat uživatelský profil?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete without notification" xml:space="preserve"> + <source>Delete without notification</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve"> + <source>Deleted</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Deleted at" xml:space="preserve"> <source>Deleted at</source> <target>Smazáno v</target> @@ -1735,6 +2354,14 @@ This cannot be undone!</source> <target>Smazáno v: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve"> + <source>Deletion errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivered even when Apple drops them." xml:space="preserve"> + <source>Delivered even when Apple drops them.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery" xml:space="preserve"> <source>Delivery</source> <target>Doručenka</target> @@ -1767,11 +2394,35 @@ This cannot be undone!</source> <source>Desktop devices</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve"> + <source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server error: %@" xml:space="preserve"> + <source>Destination server error: %@</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve"> + <source>Destination server version of %@ is incompatible with forwarding server %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Detailed statistics" xml:space="preserve"> + <source>Detailed statistics</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Details" xml:space="preserve"> + <source>Details</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Develop" xml:space="preserve"> <source>Develop</source> <target>Vyvinout</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Developer options" xml:space="preserve"> + <source>Developer options</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> <target>Nástroje pro vývojáře</target> @@ -1802,8 +2453,12 @@ This cannot be undone!</source> <target>Přímé zprávy</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve"> + <source>Direct messages between members are prohibited in this chat.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> <target>Přímé zprávy mezi členy jsou v této skupině zakázány.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1817,11 +2472,23 @@ This cannot be undone!</source> <target>Vypnutí zámku SimpleX</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Disable automatic message deletion?" xml:space="preserve"> + <source>Disable automatic message deletion?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Disable delete messages" xml:space="preserve"> + <source>Disable delete messages</source> + <note>alert button</note> + </trans-unit> <trans-unit id="Disable for all" xml:space="preserve"> <source>Disable for all</source> <target>Vypnout pro všechny</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Disabled" xml:space="preserve"> + <source>Disabled</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> <source>Disappearing message</source> <target>Mizící zpráva</target> @@ -1837,8 +2504,8 @@ This cannot be undone!</source> <target>Mizící zprávy jsou v tomto chatu zakázány.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <target>Mizící zprávy jsou v této skupině zakázány.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1870,16 +2537,36 @@ This cannot be undone!</source> <source>Discover via local network</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve"> + <source>Do NOT send messages directly, even if your or destination server does not support private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> <source>Do NOT use SimpleX for emergency calls.</source> <target>NEpoužívejte SimpleX pro tísňová volání.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT use private routing." xml:space="preserve"> + <source>Do NOT use private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do it later" xml:space="preserve"> <source>Do it later</source> <target>Udělat později</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do not send history to new members." xml:space="preserve"> + <source>Do not send history to new members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not use credentials with proxy." xml:space="preserve"> + <source>Do not use credentials with proxy.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Documents:" xml:space="preserve"> + <source>Documents:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> <target>Nevytvářet adresu</target> @@ -1890,21 +2577,62 @@ This cannot be undone!</source> <target>Nepovolovat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Don't miss important messages." xml:space="preserve"> + <source>Don't miss important messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> <target>Znovu neukazuj</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Done" xml:space="preserve"> + <source>Done</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Downgrade and open chat" xml:space="preserve"> <source>Downgrade and open chat</source> <target>Snížit a otevřít chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Download" xml:space="preserve"> + <source>Download</source> + <note>alert button +chat item action</note> + </trans-unit> + <trans-unit id="Download errors" xml:space="preserve"> + <source>Download errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download failed" xml:space="preserve"> + <source>Download failed</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Download file" xml:space="preserve"> <source>Download file</source> <target>Stáhnout soubor</target> <note>server test step</note> </trans-unit> + <trans-unit id="Download files" xml:space="preserve"> + <source>Download files</source> + <note>alert action</note> + </trans-unit> + <trans-unit id="Downloaded" xml:space="preserve"> + <source>Downloaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloaded files" xml:space="preserve"> + <source>Downloaded files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading archive" xml:space="preserve"> + <source>Downloading archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading link details" xml:space="preserve"> + <source>Downloading link details</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> <target>Duplicitní zobrazované jméno!</target> @@ -1915,6 +2643,10 @@ This cannot be undone!</source> <target>Trvání</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="E2E encrypted notifications." xml:space="preserve"> + <source>E2E encrypted notifications.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> <target>Upravit</target> @@ -1935,6 +2667,10 @@ This cannot be undone!</source> <target>Povolit (zachovat přepsání)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve"> + <source>Enable Flux in Network & servers settings for better metadata privacy.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> <target>Zapnutí zámku SimpleX</target> @@ -1948,6 +2684,10 @@ This cannot be undone!</source> <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> <target>Povolit automatické mazání zpráv?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Enable camera access" xml:space="preserve"> + <source>Enable camera access</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable for all" xml:space="preserve"> @@ -1955,6 +2695,10 @@ This cannot be undone!</source> <target>Povolit pro všechny</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve"> + <source>Enable in direct chats (BETA)!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> <target>Povolit okamžitá oznámení?</target> @@ -1985,6 +2729,14 @@ This cannot be undone!</source> <target>Povolit sebedestrukční heslo</target> <note>set passcode view</note> </trans-unit> + <trans-unit id="Enabled" xml:space="preserve"> + <source>Enabled</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve"> + <source>Enabled for</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> <target>Šifrovat</target> @@ -2015,6 +2767,10 @@ This cannot be undone!</source> <target>Šifrovaná zpráva nebo jiná událost</target> <note>notification</note> </trans-unit> + <trans-unit id="Encrypted message: app is stopped" xml:space="preserve"> + <source>Encrypted message: app is stopped</source> + <note>notification</note> + </trans-unit> <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> <target>Šifrovaná zpráva: chyba databáze</target> @@ -2048,6 +2804,10 @@ This cannot be undone!</source> <source>Encryption re-negotiation failed.</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encryption renegotiation in progress." xml:space="preserve"> + <source>Encryption renegotiation in progress.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter Passcode" xml:space="preserve"> <source>Enter Passcode</source> <target>Zadat heslo</target> @@ -2062,6 +2822,10 @@ This cannot be undone!</source> <source>Enter group name…</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve"> + <source>Enter passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter passphrase…" xml:space="preserve"> <source>Enter passphrase…</source> <target>Zadejte přístupovou frázi…</target> @@ -2105,26 +2869,33 @@ This cannot be undone!</source> <target>Chyba přerušení změny adresy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error accepting conditions" xml:space="preserve"> + <source>Error accepting conditions</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> <target>Chyba při přijímání žádosti o kontakt</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve"> - <source>Error accessing database file</source> - <target>Chyba přístupu k souboru databáze</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> <target>Chyba přidávání člena(ů)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error adding server" xml:space="preserve"> + <source>Error adding server</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> <target>Chuba změny adresy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing connection profile" xml:space="preserve"> + <source>Error changing connection profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error changing role" xml:space="preserve"> <source>Error changing role</source> <target>Chyba při změně role</target> @@ -2135,6 +2906,18 @@ This cannot be undone!</source> <target>Chyba změny nastavení</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing to incognito!" xml:space="preserve"> + <source>Error changing to incognito!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error checking token status" xml:space="preserve"> + <source>Error checking token status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve"> + <source>Error connecting to forwarding server %@. Please try later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> <target>Chyba při vytváření adresy</target> @@ -2150,16 +2933,28 @@ This cannot be undone!</source> <target>Chyba při vytváření odkazu skupiny</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating list" xml:space="preserve"> + <source>Error creating list</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error creating member contact" xml:space="preserve"> <source>Error creating member contact</source> <target>Chyba vytvoření kontaktu člena</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating message" xml:space="preserve"> + <source>Error creating message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>Chyba při vytváření profilu!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating report" xml:space="preserve"> + <source>Error creating report</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error decrypting file" xml:space="preserve"> <source>Error decrypting file</source> <target>Chyba dešifrování souboru</target> @@ -2180,11 +2975,6 @@ This cannot be undone!</source> <target>Chyba při mazání připojení</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve"> - <source>Error deleting contact</source> - <target>Chyba mazání kontaktu</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> <target>Chyba při mazání databáze</target> @@ -2205,6 +2995,10 @@ This cannot be undone!</source> <target>Chyba mazání uživatelského profilu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error downloading the archive" xml:space="preserve"> + <source>Error downloading the archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> <source>Error enabling delivery receipts!</source> <target>Chyba povolení potvrzení o doručení!</target> @@ -2225,6 +3019,10 @@ This cannot be undone!</source> <target>Chyba při exportu databáze chatu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error exporting theme: %@" xml:space="preserve"> + <source>Error exporting theme: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> <target>Chyba při importu databáze chatu</target> @@ -2235,24 +3033,46 @@ This cannot be undone!</source> <target>Chyba při připojování ke skupině</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve"> - <source>Error loading %@ servers</source> - <target>Chyba načítání %@ serverů</target> + <trans-unit id="Error loading servers" xml:space="preserve"> + <source>Error loading servers</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error migrating settings" xml:space="preserve"> + <source>Error migrating settings</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error opening chat" xml:space="preserve"> + <source>Error opening chat</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> <target>Chyba při příjmu souboru</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error reconnecting server" xml:space="preserve"> + <source>Error reconnecting server</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error reconnecting servers" xml:space="preserve"> + <source>Error reconnecting servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error registering for notifications" xml:space="preserve"> + <source>Error registering for notifications</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> <target>Chyba při odebrání člena</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve"> - <source>Error saving %@ servers</source> - <target>Chyba při ukládání serverů %@</target> + <trans-unit id="Error reordering lists" xml:space="preserve"> + <source>Error reordering lists</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error resetting statistics" xml:space="preserve"> + <source>Error resetting statistics</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving ICE servers" xml:space="preserve"> @@ -2260,6 +3080,10 @@ This cannot be undone!</source> <target>Chyba při ukládání serverů ICE</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving chat list" xml:space="preserve"> + <source>Error saving chat list</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> <target>Chyba při ukládání profilu skupiny</target> @@ -2275,11 +3099,23 @@ This cannot be undone!</source> <target>Při ukládání přístupové fráze do klíčenky došlo k chybě</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving servers" xml:space="preserve"> + <source>Error saving servers</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error saving settings" xml:space="preserve"> + <source>Error saving settings</source> + <note>when migrating</note> + </trans-unit> <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> <target>Chyba ukládání hesla uživatele</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error scanning code: %@" xml:space="preserve"> + <source>Error scanning code: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending email" xml:space="preserve"> <source>Error sending email</source> <target>Chyba odesílání e-mailu</target> @@ -2310,16 +3146,24 @@ This cannot be undone!</source> <target>Chyba při zastavení chatu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error switching profile" xml:space="preserve"> + <source>Error switching profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> <target>Chyba při přepínání profilu!</target> - <note>No comment provided by engineer.</note> + <note>alertTitle</note> </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> <target>Chyba synchronizace připojení</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error testing server connection" xml:space="preserve"> + <source>Error testing server connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> <target>Chyba aktualizace odkazu skupiny</target> @@ -2330,6 +3174,10 @@ This cannot be undone!</source> <target>Chyba aktualizace zprávy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error updating server" xml:space="preserve"> + <source>Error updating server</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> <target>Chyba při aktualizaci nastavení</target> @@ -2340,6 +3188,14 @@ This cannot be undone!</source> <target>Chyba aktualizace soukromí uživatele</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error uploading the archive" xml:space="preserve"> + <source>Error uploading the archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error verifying passphrase:" xml:space="preserve"> + <source>Error verifying passphrase:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error: " xml:space="preserve"> <source>Error: </source> <target>Chyba: </target> @@ -2348,7 +3204,9 @@ This cannot be undone!</source> <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> <target>Chyba: %@</target> - <note>No comment provided by engineer.</note> + <note>alert message +file error text +snd error text</note> </trans-unit> <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> @@ -2360,6 +3218,14 @@ This cannot be undone!</source> <target>Chyba: žádný soubor databáze</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Errors" xml:space="preserve"> + <source>Errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors in servers configuration." xml:space="preserve"> + <source>Errors in servers configuration.</source> + <note>servers error</note> + </trans-unit> <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> <source>Even when disabled in the conversation.</source> <target>I při vypnutí v konverzaci.</target> @@ -2374,6 +3240,10 @@ This cannot be undone!</source> <source>Expand</source> <note>chat item action</note> </trans-unit> + <trans-unit id="Expired" xml:space="preserve"> + <source>Expired</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> <target>Export databáze</target> @@ -2384,11 +3254,19 @@ This cannot be undone!</source> <target>Chyba exportu:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Export theme" xml:space="preserve"> + <source>Export theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> <target>Exportovaný archiv databáze.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Exported file doesn't exist" xml:space="preserve"> + <source>Exported file doesn't exist</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exporting database archive…" xml:space="preserve"> <source>Exporting database archive…</source> <target>Exportuji archiv databáze…</target> @@ -2404,15 +3282,57 @@ This cannot be undone!</source> <target>Rychle a bez čekání, než bude odesílatel online!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster deletion of groups." xml:space="preserve"> + <source>Faster deletion of groups.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Faster joining and more reliable messages." xml:space="preserve"> <source>Faster joining and more reliable messages.</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster sending messages." xml:space="preserve"> + <source>Faster sending messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Favorite" xml:space="preserve"> <source>Favorite</source> <target>Oblíbené</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Favorites" xml:space="preserve"> + <source>Favorites</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <note>file error alert title</note> + </trans-unit> + <trans-unit id="File errors: %@" xml:space="preserve"> + <source>File errors: +%@</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="File is blocked by server operator: %@." xml:space="preserve"> + <source>File is blocked by server operator: +%@.</source> + <note>file error text</note> + </trans-unit> + <trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve"> + <source>File not found - most likely file was deleted or cancelled.</source> + <note>file error text</note> + </trans-unit> + <trans-unit id="File server error: %@" xml:space="preserve"> + <source>File server error: %@</source> + <note>file error text</note> + </trans-unit> + <trans-unit id="File status" xml:space="preserve"> + <source>File status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File status: %@" xml:space="preserve"> + <source>File status: %@</source> + <note>copied message info</note> + </trans-unit> <trans-unit id="File will be deleted from servers." xml:space="preserve"> <source>File will be deleted from servers.</source> <target>Soubor bude smazán ze serverů.</target> @@ -2433,6 +3353,10 @@ This cannot be undone!</source> <target>Soubor: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files" xml:space="preserve"> + <source>Files</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> <target>Soubory a média</target> @@ -2443,11 +3367,15 @@ This cannot be undone!</source> <target>Soubory a média</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> - <source>Files and media are prohibited in this group.</source> + <trans-unit id="Files and media are prohibited." xml:space="preserve"> + <source>Files and media are prohibited.</source> <target>Soubory a média jsou zakázány v této skupině.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files and media not allowed" xml:space="preserve"> + <source>Files and media not allowed</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files and media prohibited!" xml:space="preserve"> <source>Files and media prohibited!</source> <target>Soubory a média jsou zakázány!</target> @@ -2458,6 +3386,14 @@ This cannot be undone!</source> <target>Filtrovat nepřečtené a oblíbené chaty.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Finalize migration" xml:space="preserve"> + <source>Finalize migration</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration on another device." xml:space="preserve"> + <source>Finalize migration on another device.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> <target>Konečně je máme! 🚀</target> @@ -2498,11 +3434,93 @@ This cannot be undone!</source> <target>Opravit nepodporované členem skupiny</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For all moderators" xml:space="preserve"> + <source>For all moderators</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For chat profile %@:" xml:space="preserve"> + <source>For chat profile %@:</source> + <note>servers error</note> + </trans-unit> <trans-unit id="For console" xml:space="preserve"> <source>For console</source> <target>Pro konzoli</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." xml:space="preserve"> + <source>For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For me" xml:space="preserve"> + <source>For me</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For private routing" xml:space="preserve"> + <source>For private routing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For social media" xml:space="preserve"> + <source>For social media</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward" xml:space="preserve"> + <source>Forward</source> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Forward %d message(s)?" xml:space="preserve"> + <source>Forward %d message(s)?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Forward and save messages" xml:space="preserve"> + <source>Forward and save messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward messages" xml:space="preserve"> + <source>Forward messages</source> + <note>alert action</note> + </trans-unit> + <trans-unit id="Forward messages without files?" xml:space="preserve"> + <source>Forward messages without files?</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> + <source>Forward up to 20 messages at once.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded" xml:space="preserve"> + <source>Forwarded</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded from" xml:space="preserve"> + <source>Forwarded from</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding %lld messages" xml:space="preserve"> + <source>Forwarding %lld messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve"> + <source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server address is incompatible with network settings: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server version is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server version is incompatible with network settings: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Destination server error: %2$@</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Error: %2$@</source> + <note>snd error text</note> + </trans-unit> <trans-unit id="Found desktop" xml:space="preserve"> <source>Found desktop</source> <note>No comment provided by engineer.</note> @@ -2522,11 +3540,6 @@ This cannot be undone!</source> <target>Celé jméno (volitelně)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full name:" xml:space="preserve"> - <source>Full name:</source> - <target>Celé jméno:</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Fully decentralized – visible only to members." xml:space="preserve"> <source>Fully decentralized – visible only to members.</source> <note>No comment provided by engineer.</note> @@ -2546,6 +3559,18 @@ This cannot be undone!</source> <target>GIFy a nálepky</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Get notified when mentioned." xml:space="preserve"> + <source>Get notified when mentioned.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Good afternoon!" xml:space="preserve"> + <source>Good afternoon!</source> + <note>message preview</note> + </trans-unit> + <trans-unit id="Good morning!" xml:space="preserve"> + <source>Good morning!</source> + <note>message preview</note> + </trans-unit> <trans-unit id="Group" xml:space="preserve"> <source>Group</source> <target>Skupina</target> @@ -2599,36 +3624,6 @@ This cannot be undone!</source> <target>Odkazy na skupiny</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve"> - <source>Group members can add message reactions.</source> - <target>Členové skupin mohou přidávat reakce na zprávy.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> - <source>Group members can irreversibly delete sent messages.</source> - <target>Členové skupiny mohou nevratně mazat odeslané zprávy.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> - <target>Členové skupiny mohou posílat přímé zprávy.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> - <target>Členové skupiny mohou posílat mizící zprávy.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send files and media." xml:space="preserve"> - <source>Group members can send files and media.</source> - <target>Členové skupiny mohou posílat soubory a média.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> - <target>Členové skupiny mohou posílat hlasové zprávy.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> <target>Skupinová zpráva:</target> @@ -2669,11 +3664,19 @@ This cannot be undone!</source> <target>Skupina bude smazána pro vás - toto nelze vzít zpět!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Groups" xml:space="preserve"> + <source>Groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Help" xml:space="preserve"> <source>Help</source> <target>Pomoc</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Help admins moderating their groups." xml:space="preserve"> + <source>Help admins moderating their groups.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> <target>Skryté</target> @@ -2714,15 +3717,26 @@ This cannot be undone!</source> <target>Historie</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="History is not sent to new members." xml:space="preserve"> + <source>History is not sent to new members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> <target>Jak SimpleX funguje</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="How it affects privacy" xml:space="preserve"> + <source>How it affects privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it helps privacy" xml:space="preserve"> + <source>How it helps privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> - <target>Jak to funguje</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="How to" xml:space="preserve"> <source>How to</source> @@ -2739,21 +3753,24 @@ This cannot be undone!</source> <target>Jak používat servery</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Hungarian interface" xml:space="preserve"> + <source>Hungarian interface</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> <target>Servery ICE (jeden na řádek)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="IP address" xml:space="preserve"> + <source>IP address</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> <source>If you can't meet in person, show QR code in a video call, or share the link.</source> <target>Pokud se nemůžete setkat osobně, zobrazte QR kód ve videohovoru nebo sdílejte odkaz.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <target>Pokud se nemůžete setkat osobně, můžete **naskenovat QR kód během videohovoru**, nebo váš kontakt může sdílet odkaz na pozvánku.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> <target>Pokud tento přístupový kód zadáte při otevření aplikace, všechna data budou nenávratně smazána!</target> @@ -2789,8 +3806,8 @@ This cannot be undone!</source> <target>Ihned</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <target>Odolná vůči spamu a zneužití</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2809,6 +3826,27 @@ This cannot be undone!</source> <target>Import databáze</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Import failed" xml:space="preserve"> + <source>Import failed</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import theme" xml:space="preserve"> + <source>Import theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Importing archive" xml:space="preserve"> + <source>Importing archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved delivery, reduced traffic usage. More improvements are coming soon!" xml:space="preserve"> + <source>Improved delivery, reduced traffic usage. +More improvements are coming soon!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved message delivery" xml:space="preserve"> + <source>Improved message delivery</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> <target>Vylepšená ochrana soukromí a zabezpečení</target> @@ -2819,11 +3857,27 @@ This cannot be undone!</source> <target>Vylepšená konfigurace serveru</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In order to continue, chat should be stopped." xml:space="preserve"> + <source>In order to continue, chat should be stopped.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>V odpovědi na</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In-call sounds" xml:space="preserve"> + <source>In-call sounds</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Inappropriate content" xml:space="preserve"> + <source>Inappropriate content</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Inappropriate profile" xml:space="preserve"> + <source>Inappropriate profile</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> <target>Inkognito</target> @@ -2892,6 +3946,11 @@ This cannot be undone!</source> <target>Nainstalujte [SimpleX Chat pro terminál](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Instant" xml:space="preserve"> + <source>Instant</source> + <target>Okamžitě</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> @@ -2899,29 +3958,68 @@ This cannot be undone!</source> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Instantly" xml:space="preserve"> - <source>Instantly</source> - <target>Okamžitě</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> <target>Rozhranní</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Interface colors" xml:space="preserve"> + <source>Interface colors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid" xml:space="preserve"> + <source>Invalid</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (bad token)" xml:space="preserve"> + <source>Invalid (bad token)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (expired)" xml:space="preserve"> + <source>Invalid (expired)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (unregistered)" xml:space="preserve"> + <source>Invalid (unregistered)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (wrong topic)" xml:space="preserve"> + <source>Invalid (wrong topic)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid QR code" xml:space="preserve"> + <source>Invalid QR code</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> <target>Neplatný odkaz na spojení</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid display name!" xml:space="preserve"> + <source>Invalid display name!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid link" xml:space="preserve"> + <source>Invalid link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid name!" xml:space="preserve"> <source>Invalid name!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid response" xml:space="preserve"> + <source>Invalid response</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> <target>Neplatná adresa serveru!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Invalid status" xml:space="preserve"> <source>Invalid status</source> @@ -2943,6 +4041,10 @@ This cannot be undone!</source> <target>Pozvat členy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invite to chat" xml:space="preserve"> + <source>Invite to chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invite to group" xml:space="preserve"> <source>Invite to group</source> <target>Pozvat do skupiny</target> @@ -2958,8 +4060,8 @@ This cannot be undone!</source> <target>Nevratné mazání zpráv je v tomto chatu zakázáno.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <target>Nevratné mazání zpráv je v této skupině zakázáno.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2984,6 +4086,10 @@ This cannot be undone!</source> 3. Spojení je kompromitováno.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="It protects your IP address and connections." xml:space="preserve"> + <source>It protects your IP address and connections.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> <target>Zdá se, že jste již připojeni prostřednictvím tohoto odkazu. Pokud tomu tak není, došlo k chybě (%@).</target> @@ -3002,13 +4108,17 @@ This cannot be undone!</source> <trans-unit id="Join" xml:space="preserve"> <source>Join</source> <target>Připojte se na</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> <target>Připojit ke skupině</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Join group conversations" xml:space="preserve"> + <source>Join group conversations</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Join group?" xml:space="preserve"> <source>Join group?</source> <note>No comment provided by engineer.</note> @@ -3032,10 +4142,22 @@ This is your link for group %@!</source> <target>Připojování ke skupině</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep" xml:space="preserve"> + <source>Keep</source> + <note>alert action</note> + </trans-unit> + <trans-unit id="Keep conversation" xml:space="preserve"> + <source>Keep conversation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Keep the app open to use it from desktop" xml:space="preserve"> <source>Keep the app open to use it from desktop</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep unused invitation?" xml:space="preserve"> + <source>Keep unused invitation?</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> <source>Keep your connections</source> <target>Zachovat vaše připojení</target> @@ -3069,6 +4191,14 @@ This is your link for group %@!</source> <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> <target>Opustit</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Leave chat" xml:space="preserve"> + <source>Leave chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave chat?" xml:space="preserve"> + <source>Leave chat?</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group" xml:space="preserve"> @@ -3108,6 +4238,18 @@ This is your link for group %@!</source> <source>Linked desktops</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="List" xml:space="preserve"> + <source>List</source> + <note>swipe action</note> + </trans-unit> + <trans-unit id="List name and emoji should be different for all lists." xml:space="preserve"> + <source>List name and emoji should be different for all lists.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List name..." xml:space="preserve"> + <source>List name...</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Live message!" xml:space="preserve"> <source>Live message!</source> <target>Živé zprávy!</target> @@ -3138,11 +4280,6 @@ This is your link for group %@!</source> <target>Režim zámku</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> - <source>Make a private connection</source> - <target>Vytvořte si soukromé připojení</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make one message disappear" xml:space="preserve"> <source>Make one message disappear</source> <target>Nechat jednu zprávu zmizet</target> @@ -3153,21 +4290,11 @@ This is your link for group %@!</source> <target>Změnit profil na soukromý!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> - <target>Ujistěte se, že adresy %@ serverů jsou ve správném formátu, oddělené řádky a nejsou duplicitní (%@).</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> <target>Ujistěte se, že adresy serverů WebRTC ICE jsou ve správném formátu, oddělené na řádcích a nejsou duplicitní.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <target>Mnoho lidí se ptalo: *Pokud SimpleX nemá žádné uživatelské identifikátory, jak může doručovat zprávy?*</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> <source>Mark deleted for everyone</source> <target>Označit jako smazané pro všechny</target> @@ -3193,11 +4320,31 @@ This is your link for group %@!</source> <target>Max 30 vteřin, přijato okamžitě.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Media & file servers" xml:space="preserve"> + <source>Media & file servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Medium" xml:space="preserve"> + <source>Medium</source> + <note>blur media</note> + </trans-unit> <trans-unit id="Member" xml:space="preserve"> <source>Member</source> <target>Člen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member inactive" xml:space="preserve"> + <source>Member inactive</source> + <note>item status text</note> + </trans-unit> + <trans-unit id="Member reports" xml:space="preserve"> + <source>Member reports</source> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All chat members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All chat members will be notified.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> <target>Role člena se změní na "%@". Všichni členové skupiny budou upozorněni.</target> @@ -3208,11 +4355,61 @@ This is your link for group %@!</source> <target>Role člena se změní na "%@". Člen obdrží novou pozvánku.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from chat - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> <target>Člen bude odstraněn ze skupiny - toto nelze vzít zpět!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Members can add message reactions." xml:space="preserve"> + <source>Members can add message reactions.</source> + <target>Členové skupin mohou přidávat reakce na zprávy.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Members can irreversibly delete sent messages. (24 hours)</source> + <target>Členové skupiny mohou nevratně mazat odeslané zprávy. (24 hodin)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can report messsages to moderators." xml:space="preserve"> + <source>Members can report messsages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send SimpleX links." xml:space="preserve"> + <source>Members can send SimpleX links.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> + <target>Členové skupiny mohou posílat přímé zprávy.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> + <target>Členové skupiny mohou posílat mizící zprávy.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send files and media." xml:space="preserve"> + <source>Members can send files and media.</source> + <target>Členové skupiny mohou posílat soubory a média.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> + <target>Členové skupiny mohou posílat hlasové zprávy.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mention members 👋" xml:space="preserve"> + <source>Mention members 👋</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Menus" xml:space="preserve"> + <source>Menus</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>Chyba doručení zprávy</target> @@ -3223,11 +4420,27 @@ This is your link for group %@!</source> <target>Potvrzení o doručení zprávy!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message delivery warning" xml:space="preserve"> + <source>Message delivery warning</source> + <note>item status text</note> + </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> <target>Návrh zprávy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message forwarded" xml:space="preserve"> + <source>Message forwarded</source> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message may be delivered later if member becomes active." xml:space="preserve"> + <source>Message may be delivered later if member becomes active.</source> + <note>item status description</note> + </trans-unit> + <trans-unit id="Message queue info" xml:space="preserve"> + <source>Message queue info</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message reactions" xml:space="preserve"> <source>Message reactions</source> <target>Reakce na zprávy</target> @@ -3238,16 +4451,44 @@ This is your link for group %@!</source> <target>Reakce na zprávy jsou v tomto chatu zakázány.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> - <source>Message reactions are prohibited in this group.</source> + <trans-unit id="Message reactions are prohibited." xml:space="preserve"> + <source>Message reactions are prohibited.</source> <target>Reakce na zprávy jsou v této skupině zakázány.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message reception" xml:space="preserve"> + <source>Message reception</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message servers" xml:space="preserve"> + <source>Message servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message shape" xml:space="preserve"> + <source>Message shape</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message source remains private." xml:space="preserve"> + <source>Message source remains private.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status" xml:space="preserve"> + <source>Message status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status: %@" xml:space="preserve"> + <source>Message status: %@</source> + <note>copied message info</note> + </trans-unit> <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> <target>Text zprávy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message too large" xml:space="preserve"> + <source>Message too large</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> <target>Zprávy</target> @@ -3262,11 +4503,63 @@ This is your link for group %@!</source> <source>Messages from %@ will be shown!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Messages in this chat will never be deleted." xml:space="preserve"> + <source>Messages in this chat will never be deleted.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages received" xml:space="preserve"> + <source>Messages received</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages sent" xml:space="preserve"> + <source>Messages sent</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages were deleted after you selected them." xml:space="preserve"> + <source>Messages were deleted after you selected them.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate device" xml:space="preserve"> + <source>Migrate device</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate from another device" xml:space="preserve"> + <source>Migrate from another device</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate here" xml:space="preserve"> + <source>Migrate here</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device" xml:space="preserve"> + <source>Migrate to another device</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device via QR code." xml:space="preserve"> + <source>Migrate to another device via QR code.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating" xml:space="preserve"> + <source>Migrating</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migrating database archive…" xml:space="preserve"> <source>Migrating database archive…</source> <target>Přenášení archivu databáze…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Migration complete" xml:space="preserve"> + <source>Migration complete</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> <target>Chyba přenášení:</target> @@ -3282,9 +4575,9 @@ This is your link for group %@!</source> <target>Přenesení dokončeno</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> - <target>Migrace: %@</target> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> + <target>Migrace:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> @@ -3302,21 +4595,28 @@ This is your link for group %@!</source> <target>Upraveno v: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="More" xml:space="preserve"> + <source>More</source> + <note>swipe action</note> + </trans-unit> <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> <target>Další vylepšení se chystají již brzy!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="More reliable network connection." xml:space="preserve"> + <source>More reliable network connection.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="More reliable notifications" xml:space="preserve"> + <source>More reliable notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> <source>Most likely this connection is deleted.</source> <target>Pravděpodobně je toto spojení smazáno.</target> <note>item status description</note> </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> - <source>Most likely this contact has deleted the connection with you.</source> - <target>Tento kontakt s největší pravděpodobností smazal spojení s vámi.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> <target>Více chatovacích profilů</target> @@ -3325,7 +4625,11 @@ This is your link for group %@!</source> <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> <target>Ztlumit</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Mute all" xml:space="preserve"> + <source>Mute all</source> + <note>notification label action</note> </trans-unit> <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> @@ -3335,13 +4639,33 @@ This is your link for group %@!</source> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> <target>Jméno</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> <target>Síť a servery</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Network connection" xml:space="preserve"> + <source>Network connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network decentralization" xml:space="preserve"> + <source>Network decentralization</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve"> + <source>Network issues - message expired after many attempts to send it.</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Network management" xml:space="preserve"> + <source>Network management</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network operator" xml:space="preserve"> + <source>Network operator</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> <target>Nastavení sítě</target> @@ -3352,11 +4676,31 @@ This is your link for group %@!</source> <target>Stav sítě</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New" xml:space="preserve"> + <source>New</source> + <note>token status text</note> + </trans-unit> <trans-unit id="New Passcode" xml:space="preserve"> <source>New Passcode</source> <target>Nové heslo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve"> + <source>New SOCKS credentials will be used every time you start the app.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve"> + <source>New SOCKS credentials will be used for each server.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat" xml:space="preserve"> + <source>New chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat experience 🎉" xml:space="preserve"> + <source>New chat experience 🎉</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> <target>Žádost o nový kontakt</target> @@ -3367,11 +4711,6 @@ This is your link for group %@!</source> <target>Nový kontakt:</target> <note>notification</note> </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> - <source>New database archive</source> - <target>Archiv nové databáze</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="New desktop app!" xml:space="preserve"> <source>New desktop app!</source> <target>Nová desktopová aplikace!</target> @@ -3382,11 +4721,19 @@ This is your link for group %@!</source> <target>Nově zobrazované jméno</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <note>notification</note> + </trans-unit> <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> <target>Nový V %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New media options" xml:space="preserve"> + <source>New media options</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> <target>Nová role člena</target> @@ -3402,6 +4749,10 @@ This is your link for group %@!</source> <target>Nová přístupová fráze…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New server" xml:space="preserve"> + <source>New server</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No" xml:space="preserve"> <source>No</source> <target>Ne</target> @@ -3412,6 +4763,18 @@ This is your link for group %@!</source> <target>Žádné heslo aplikace</target> <note>Authentication unavailable</note> </trans-unit> + <trans-unit id="No chats" xml:space="preserve"> + <source>No chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats found" xml:space="preserve"> + <source>No chats found</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats in list %@" xml:space="preserve"> + <source>No chats in list %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> <target>Nebyl vybrán žádný kontakt</target> @@ -3432,6 +4795,10 @@ This is your link for group %@!</source> <target>Žádný token zařízení!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No direct connection yet, message is forwarded by admin." xml:space="preserve"> + <source>No direct connection yet, message is forwarded by admin.</source> + <note>item status description</note> + </trans-unit> <trans-unit id="No filtered chats" xml:space="preserve"> <source>No filtered chats</source> <target>Žádné filtrované chaty</target> @@ -3447,20 +4814,94 @@ This is your link for group %@!</source> <target>Žádná historie</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No info, try to reload" xml:space="preserve"> + <source>No info, try to reload</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No media & file servers." xml:space="preserve"> + <source>No media & file servers.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No message" xml:space="preserve"> + <source>No message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No message servers." xml:space="preserve"> + <source>No message servers.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No network connection" xml:space="preserve"> + <source>No network connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record speech" xml:space="preserve"> + <source>No permission to record speech</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record video" xml:space="preserve"> + <source>No permission to record video</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> <source>No permission to record voice message</source> <target>Nemáte oprávnění nahrávat hlasové zprávy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No push server" xml:space="preserve"> + <source>No push server</source> + <target>Místní</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> <target>Žádné přijaté ani odeslané soubory</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No servers for private message routing." xml:space="preserve"> + <source>No servers for private message routing.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive files." xml:space="preserve"> + <source>No servers to receive files.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive messages." xml:space="preserve"> + <source>No servers to receive messages.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to send files." xml:space="preserve"> + <source>No servers to send files.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No token!" xml:space="preserve"> + <source>No token!</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="No unread chats" xml:space="preserve"> + <source>No unread chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> + <target>Bez uživatelských identifikátorů</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Not compatible!" xml:space="preserve"> <source>Not compatible!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notes" xml:space="preserve"> + <source>Notes</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing selected" xml:space="preserve"> + <source>Nothing selected</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing to forward!" xml:space="preserve"> + <source>Nothing to forward!</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> <target>Oznámení</target> @@ -3471,6 +4912,18 @@ This is your link for group %@!</source> <target>Oznámení jsou zakázána!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notifications error" xml:space="preserve"> + <source>Notifications error</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Notifications privacy" xml:space="preserve"> + <source>Notifications privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications status" xml:space="preserve"> + <source>Notifications status</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. @@ -3480,44 +4933,42 @@ This is your link for group %@!</source> - zakázat členy (role "pozorovatel")</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="OK" xml:space="preserve"> + <source>OK</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Off" xml:space="preserve"> <source>Off</source> <target>Vypnout</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> - <source>Off (Local)</source> - <target>Vypnuto (místní)</target> - <note>No comment provided by engineer.</note> + <note>blur media</note> </trans-unit> <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> <target>Ok</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> <target>Stará databáze</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> - <source>Old database archive</source> - <target>Archiv staré databáze</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> <target>Jednorázový zvací odkaz</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> - <target>Pro připojení budou vyžadováni Onion hostitelé. Vyžaduje povolení sítě VPN.</target> + <trans-unit id="Onion hosts will be **required** for connection. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be **required** for connection. +Requires compatible VPN.</source> + <target>Pro připojení budou vyžadováni Onion hostitelé. +Vyžaduje povolení sítě VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> - <target>Onion hostitelé budou použiti, pokud jsou k dispozici. Vyžaduje povolení sítě VPN.</target> + <trans-unit id="Onion hosts will be used when available. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be used when available. +Requires compatible VPN.</source> + <target>Onion hostitelé budou použiti, pokud jsou k dispozici. +Vyžaduje povolení sítě VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will not be used." xml:space="preserve"> @@ -3525,11 +4976,19 @@ This is your link for group %@!</source> <target>Onion hostitelé nebudou použiti.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only chat owners can change preferences." xml:space="preserve"> + <source>Only chat owners can change preferences.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <target>Pouze klientská zařízení ukládají uživatelské profily, kontakty, skupiny a zprávy odeslané s **2vrstvým šifrováním typu end-to-end**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only delete conversation" xml:space="preserve"> + <source>Only delete conversation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> <source>Only group owners can change group preferences.</source> <target>Předvolby skupiny mohou měnit pouze vlastníci skupiny.</target> @@ -3545,14 +5004,22 @@ This is your link for group %@!</source> <target>Pouze majitelé skupin mohou povolit zasílání hlasových zpráv.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only sender and moderators see it" xml:space="preserve"> + <source>Only sender and moderators see it</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you and moderators see it" xml:space="preserve"> + <source>Only you and moderators see it</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only you can add message reactions." xml:space="preserve"> <source>Only you can add message reactions.</source> <target>Reakce na zprávy můžete přidávat pouze vy.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <target>Nevratně mazat zprávy můžete pouze vy (váš kontakt je může označit ke smazání).</target> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source> + <target>Nevratně mazat zprávy můžete pouze vy (váš kontakt je může označit ke smazání). (24 hodin)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can make calls." xml:space="preserve"> @@ -3575,9 +5042,9 @@ This is your link for group %@!</source> <target>Reakce na zprávy může přidávat pouze váš kontakt.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <target>Nevratně mazat zprávy může pouze váš kontakt (vy je můžete označit ke smazání).</target> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source> + <target>Nevratně mazat zprávy může pouze váš kontakt (vy je můžete označit ke smazání). (24 hodin)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can make calls." xml:space="preserve"> @@ -3598,13 +5065,17 @@ This is your link for group %@!</source> <trans-unit id="Open" xml:space="preserve"> <source>Open</source> <target>Otevřít</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Otevřít nastavení</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open changes" xml:space="preserve"> + <source>Open changes</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> <target>Otevřete chat</target> @@ -3615,25 +5086,71 @@ This is your link for group %@!</source> <target>Otevřete konzolu chatu</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Open conditions" xml:space="preserve"> + <source>Open conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open group" xml:space="preserve"> <source>Open group</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve"> - <source>Open user profiles</source> - <target>Otevřít uživatelské profily</target> + <trans-unit id="Open link?" xml:space="preserve"> + <source>Open link?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Open migration to another device" xml:space="preserve"> + <source>Open migration to another device</source> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> - <target>Protokol a kód s otevřeným zdrojovým kódem - servery může provozovat kdokoli.</target> + <trans-unit id="Opening app…" xml:space="preserve"> + <source>Opening app…</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve"> - <source>Opening database…</source> - <target>Otvírání databáze…</target> + <trans-unit id="Operator" xml:space="preserve"> + <source>Operator</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Operator server" xml:space="preserve"> + <source>Operator server</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Or import archive file" xml:space="preserve"> + <source>Or import archive file</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or paste archive link" xml:space="preserve"> + <source>Or paste archive link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or scan QR code" xml:space="preserve"> + <source>Or scan QR code</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or securely share this file link" xml:space="preserve"> + <source>Or securely share this file link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or show this code" xml:space="preserve"> + <source>Or show this code</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or to share privately" xml:space="preserve"> + <source>Or to share privately</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Organize chats into lists" xml:space="preserve"> + <source>Organize chats into lists</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other" xml:space="preserve"> + <source>Other</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other file errors: %@" xml:space="preserve"> + <source>Other file errors: +%@</source> + <note>alert message</note> + </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> <target>Počet PING</target> @@ -3669,15 +5186,18 @@ This is your link for group %@!</source> <target>Heslo nastaveno!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Password" xml:space="preserve"> + <source>Password</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> <target>Heslo k zobrazení</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste" xml:space="preserve"> - <source>Paste</source> - <target>Vložit</target> - <note>No comment provided by engineer.</note> + <trans-unit id="Past member %@" xml:space="preserve"> + <source>Past member %@</source> + <note>past/unknown group member</note> </trans-unit> <trans-unit id="Paste desktop address" xml:space="preserve"> <source>Paste desktop address</source> @@ -3688,23 +5208,20 @@ This is your link for group %@!</source> <target>Vložit obrázek</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve"> - <source>Paste received link</source> - <target>Vložení přijatého odkazu</target> + <trans-unit id="Paste link to connect!" xml:space="preserve"> + <source>Paste link to connect!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> - <source>Paste the link you received to connect with your contact.</source> - <target>Vložte odkaz, který jste obdrželi, do pole níže a spojte se se svým kontaktem.</target> - <note>placeholder</note> - </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> - <target>Lidé se s vámi mohou spojit pouze prostřednictvím odkazů, které sdílíte.</target> + <trans-unit id="Paste the link you received" xml:space="preserve"> + <source>Paste the link you received</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Periodically" xml:space="preserve"> - <source>Periodically</source> + <trans-unit id="Pending" xml:space="preserve"> + <source>Pending</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Periodic" xml:space="preserve"> + <source>Periodic</source> <target>Pravidelně</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3713,11 +5230,28 @@ This is your link for group %@!</source> <target>Chyba dešifrování</target> <note>message decrypt error item</note> </trans-unit> + <trans-unit id="Picture-in-picture calls" xml:space="preserve"> + <source>Picture-in-picture calls</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Play from the chat list." xml:space="preserve"> + <source>Play from the chat list.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable calls." xml:space="preserve"> + <source>Please ask your contact to enable calls.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> <target>Prosím, požádejte kontaktní osobu, aby umožnila odesílání hlasových zpráv.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers." xml:space="preserve"> + <source>Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> <target>Zkontrolujte, zda jste použili správný odkaz, nebo požádejte kontakt, aby vám poslal jiný.</target> @@ -3733,6 +5267,15 @@ This is your link for group %@!</source> <target>Zkontrolujte prosím nastavení své i svého kontaktu.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve"> + <source>Please confirm that network settings are correct for this device.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact developers. Error: %@" xml:space="preserve"> + <source>Please contact developers. +Error: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> <target>Kontaktujte prosím správce skupiny.</target> @@ -3773,11 +5316,27 @@ This is your link for group %@!</source> <target>Heslo uložte bezpečně, v případě jeho ztráty jej NEBUDE možné změnit.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please try to disable and re-enable notfications." xml:space="preserve"> + <source>Please try to disable and re-enable notfications.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token activation to complete." xml:space="preserve"> + <source>Please wait for token activation to complete.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token to be registered." xml:space="preserve"> + <source>Please wait for token to be registered.</source> + <note>token info</note> + </trans-unit> <trans-unit id="Polish interface" xml:space="preserve"> <source>Polish interface</source> <target>Polské rozhraní</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Port" xml:space="preserve"> + <source>Port</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> <target>Je možné, že otisk certifikátu v adrese serveru je nesprávný</target> @@ -3788,36 +5347,75 @@ This is your link for group %@!</source> <target>Zachování posledního návrhu zprávy s přílohami.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Preset server" xml:space="preserve"> - <source>Preset server</source> - <target>Přednastavený server</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> <target>Přednastavená adresa serveru</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Preset servers" xml:space="preserve"> + <source>Preset servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Preview" xml:space="preserve"> <source>Preview</source> <target>Náhled</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Previously connected servers" xml:space="preserve"> + <source>Previously connected servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> <target>Ochrana osobních údajů a zabezpečení</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Privacy for your customers." xml:space="preserve"> + <source>Privacy for your customers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy policy and conditions of use." xml:space="preserve"> + <source>Privacy policy and conditions of use.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy redefined" xml:space="preserve"> <source>Privacy redefined</source> <target>Nové vymezení soukromí</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve"> + <source>Private chats, groups and your contacts are not accessible to server operators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> <target>Soukromé názvy souborů</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private media file names." xml:space="preserve"> + <source>Private media file names.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing" xml:space="preserve"> + <source>Private message routing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing 🚀" xml:space="preserve"> + <source>Private message routing 🚀</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private notes" xml:space="preserve"> + <source>Private notes</source> + <note>name of notes to self</note> + </trans-unit> + <trans-unit id="Private routing" xml:space="preserve"> + <source>Private routing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private routing error" xml:space="preserve"> + <source>Private routing error</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> <target>Profil a připojení k serveru</target> @@ -3828,12 +5426,8 @@ This is your link for group %@!</source> <target>Profilový obrázek</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile name" xml:space="preserve"> - <source>Profile name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile name:" xml:space="preserve"> - <source>Profile name:</source> + <trans-unit id="Profile images" xml:space="preserve"> + <source>Profile images</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile password" xml:space="preserve"> @@ -3841,10 +5435,14 @@ This is your link for group %@!</source> <target>Heslo profilu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Profile theme" xml:space="preserve"> + <source>Profile theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> <source>Profile update will be sent to your contacts.</source> <target>Aktualizace profilu bude zaslána vašim kontaktům.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> @@ -3866,6 +5464,14 @@ This is your link for group %@!</source> <target>Zakázat reakce na zprávy.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Prohibit reporting messages to moderators." xml:space="preserve"> + <source>Prohibit reporting messages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending SimpleX links." xml:space="preserve"> + <source>Prohibit sending SimpleX links.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> <target>Zakázat odesílání přímých zpráv členům.</target> @@ -3886,11 +5492,20 @@ This is your link for group %@!</source> <target>Zakázat odesílání hlasových zpráv.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect IP address" xml:space="preserve"> + <source>Protect IP address</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> <target>Ochrana obrazovky aplikace</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve"> + <source>Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> <target>Chraňte své chat profily heslem!</target> @@ -3906,16 +5521,40 @@ This is your link for group %@!</source> <target>Časový limit protokolu na KB</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Proxied" xml:space="preserve"> + <source>Proxied</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied servers" xml:space="preserve"> + <source>Proxied servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxy requires password" xml:space="preserve"> + <source>Proxy requires password</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> <target>Nabízená oznámení</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Push server" xml:space="preserve"> + <source>Push server</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Quantum resistant encryption" xml:space="preserve"> + <source>Quantum resistant encryption</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> <source>Rate the app</source> <target>Ohodnoťte aplikaci</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reachable chat toolbar" xml:space="preserve"> + <source>Reachable chat toolbar</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> <target>Reagovat…</target> @@ -3924,16 +5563,20 @@ This is your link for group %@!</source> <trans-unit id="Read" xml:space="preserve"> <source>Read</source> <target>Číst</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Read more" xml:space="preserve"> <source>Read more</source> <target>Přečíst více</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> - <target>Další informace naleznete v [Uživatelské příručce](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</source> + <target>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).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> @@ -3941,11 +5584,6 @@ This is your link for group %@!</source> <target>Přečtěte si více v [Uživatelské příručce](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> - <source>Read more in our GitHub repository.</source> - <target>Další informace najdete v našem repozitáři GitHub.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> <target>Přečtěte si více v našem [GitHub repozitáři](https://github.com/simplex-chat/simplex-chat#readme).</target> @@ -3956,6 +5594,10 @@ This is your link for group %@!</source> <target>Informace o dodání jsou zakázány</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receive errors" xml:space="preserve"> + <source>Receive errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Přijato v</target> @@ -3976,6 +5618,18 @@ This is your link for group %@!</source> <target>Přijatá zpráva</target> <note>message info title</note> </trans-unit> + <trans-unit id="Received messages" xml:space="preserve"> + <source>Received messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received reply" xml:space="preserve"> + <source>Received reply</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received total" xml:space="preserve"> + <source>Received total</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> <target>Přijímací adresa bude změněna na jiný server. Změna adresy bude dokončena po připojení odesílatele.</target> @@ -3991,16 +5645,44 @@ This is your link for group %@!</source> <target>Příjem přes</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Recent history and improved [directory bot](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)." xml:space="preserve"> + <source>Recent history and improved [directory bot](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).</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve"> + <source>Recipient(s) can't see who this message is from.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> <target>Příjemci uvidí aktualizace během jejich psaní.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect" xml:space="preserve"> + <source>Reconnect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> <target>Znovu připojte všechny připojené servery a vynuťte doručení zprávy. Využívá další provoz.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect all servers" xml:space="preserve"> + <source>Reconnect all servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers?" xml:space="preserve"> + <source>Reconnect all servers?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect server to force message delivery. It uses additional traffic.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server?" xml:space="preserve"> + <source>Reconnect server?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> <target>Znovu připojit servery?</target> @@ -4021,10 +5703,23 @@ This is your link for group %@!</source> <target>Snížení spotřeby baterie</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Register" xml:space="preserve"> + <source>Register</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Register notification token?" xml:space="preserve"> + <source>Register notification token?</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Registered" xml:space="preserve"> + <source>Registered</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> <target>Odmítnout</target> - <note>reject incoming call via notification</note> + <note>reject incoming call via notification +swipe action</note> </trans-unit> <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> <source>Reject (sender NOT notified)</source> @@ -4051,6 +5746,14 @@ This is your link for group %@!</source> <target>Odstranit</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Remove archive?" xml:space="preserve"> + <source>Remove archive?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove image" xml:space="preserve"> + <source>Remove image</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> <target>Odstranit člena</target> @@ -4085,15 +5788,67 @@ This is your link for group %@!</source> <source>Repeat connection request?</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat download" xml:space="preserve"> + <source>Repeat download</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat import" xml:space="preserve"> + <source>Repeat import</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Repeat join request?" xml:space="preserve"> <source>Repeat join request?</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat upload" xml:space="preserve"> + <source>Repeat upload</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reply" xml:space="preserve"> <source>Reply</source> <target>Odpověď</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Report" xml:space="preserve"> + <source>Report</source> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Report content: only group moderators will see it." xml:space="preserve"> + <source>Report content: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report member profile: only group moderators will see it." xml:space="preserve"> + <source>Report member profile: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report other: only group moderators will see it." xml:space="preserve"> + <source>Report other: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report reason?" xml:space="preserve"> + <source>Report reason?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Report spam: only group moderators will see it." xml:space="preserve"> + <source>Report spam: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report violation: only group moderators will see it." xml:space="preserve"> + <source>Report violation: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report: %@" xml:space="preserve"> + <source>Report: %@</source> + <note>report in notification</note> + </trans-unit> + <trans-unit id="Reporting messages to moderators is prohibited." xml:space="preserve"> + <source>Reporting messages to moderators is prohibited.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reports" xml:space="preserve"> + <source>Reports</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Required" xml:space="preserve"> <source>Required</source> <target>Povinné</target> @@ -4104,16 +5859,36 @@ This is your link for group %@!</source> <target>Obnovit</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset all hints" xml:space="preserve"> + <source>Reset all hints</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics" xml:space="preserve"> + <source>Reset all statistics</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics?" xml:space="preserve"> + <source>Reset all statistics?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> <target>Obnovení barev</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to app theme" xml:space="preserve"> + <source>Reset to app theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> <target>Obnovení výchozího nastavení</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to user theme" xml:space="preserve"> + <source>Reset to user theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> <source>Restart the app to create a new chat profile</source> <target>Restartujte aplikaci pro vytvoření nového chat profilu</target> @@ -4144,14 +5919,17 @@ This is your link for group %@!</source> <target>Chyba obnovení databáze</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Retry" xml:space="preserve"> + <source>Retry</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> <target>Odhalit</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Revert" xml:space="preserve"> - <source>Revert</source> - <target>Vrátit</target> + <trans-unit id="Review conditions" xml:space="preserve"> + <source>Review conditions</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke" xml:space="preserve"> @@ -4179,51 +5957,61 @@ This is your link for group %@!</source> <target>Spustit chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve"> - <source>SMP servers</source> - <target>SMP servery</target> + <trans-unit id="SMP server" xml:space="preserve"> + <source>SMP server</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SOCKS proxy" xml:space="preserve"> + <source>SOCKS proxy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safely receive files" xml:space="preserve"> + <source>Safely receive files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safer groups" xml:space="preserve"> + <source>Safer groups</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save" xml:space="preserve"> <source>Save</source> <target>Uložit</target> - <note>chat item action</note> + <note>alert button +chat item action</note> </trans-unit> <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> <target>Uložit (a informovat kontakty)</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> <target>Uložit a upozornit kontakt</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> <target>Uložit a upozornit členy skupiny</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save and reconnect" xml:space="preserve"> + <source>Save and reconnect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save and update group profile" xml:space="preserve"> <source>Save and update group profile</source> <target>Uložit a aktualizovat profil skupiny</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save archive" xml:space="preserve"> - <source>Save archive</source> - <target>Uložit archiv</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve"> - <source>Save auto-accept settings</source> - <target>Uložit nastavení automatického přijímání</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> <source>Save group profile</source> <target>Uložení profilu skupiny</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save list" xml:space="preserve"> + <source>Save list</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> <target>Uložte heslo a otevřete chat</target> @@ -4237,7 +6025,7 @@ This is your link for group %@!</source> <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> <target>Uložit předvolby?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> @@ -4252,23 +6040,46 @@ This is your link for group %@!</source> <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> <target>Uložit servery?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve"> - <source>Save settings?</source> - <target>Uložit nastavení?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> <target>Uložit uvítací zprávu?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save your profile?" xml:space="preserve"> + <source>Save your profile?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Saved" xml:space="preserve"> + <source>Saved</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> <source>Saved WebRTC ICE servers will be removed</source> <target>Uložené servery WebRTC ICE budou odstraněny</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Saved from" xml:space="preserve"> + <source>Saved from</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved message" xml:space="preserve"> + <source>Saved message</source> + <note>message info title</note> + </trans-unit> + <trans-unit id="Saving %lld messages" xml:space="preserve"> + <source>Saving %lld messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scale" xml:space="preserve"> + <source>Scale</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan / Paste link" xml:space="preserve"> + <source>Scan / Paste link</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> <target>Skenovat QR kód</target> @@ -4298,11 +6109,27 @@ This is your link for group %@!</source> <target>Hledat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Search bar accepts invitation links." xml:space="preserve"> + <source>Search bar accepts invitation links.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search or paste SimpleX link" xml:space="preserve"> + <source>Search or paste SimpleX link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secondary" xml:space="preserve"> + <source>Secondary</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> <source>Secure queue</source> <target>Zabezpečit frontu</target> <note>server test step</note> </trans-unit> + <trans-unit id="Secured" xml:space="preserve"> + <source>Secured</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> <target>Posouzení bezpečnosti</target> @@ -4316,6 +6143,18 @@ This is your link for group %@!</source> <trans-unit id="Select" xml:space="preserve"> <source>Select</source> <target>Vybrat</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Select chat profile" xml:space="preserve"> + <source>Select chat profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected %lld" xml:space="preserve"> + <source>Selected %lld</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct" xml:space="preserve"> @@ -4353,11 +6192,6 @@ This is your link for group %@!</source> <target>Potvrzení o doručení zasílat na</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve"> - <source>Send direct message</source> - <target>Odeslat přímou zprávu</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Send direct message to connect" xml:space="preserve"> <source>Send direct message to connect</source> <target>Odeslat přímou zprávu pro připojení</target> @@ -4368,6 +6202,10 @@ This is your link for group %@!</source> <target>Poslat mizící zprávu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send errors" xml:space="preserve"> + <source>Send errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> <target>Odesílání náhledů odkazů</target> @@ -4378,14 +6216,25 @@ This is your link for group %@!</source> <target>Odeslat živou zprávu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send message to enable calls." xml:space="preserve"> + <source>Send message to enable calls.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when your or destination server does not support private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> <target>Odeslat oznámení</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> - <source>Send notifications:</source> - <target>Odeslat oznámení:</target> + <trans-unit id="Send private reports" xml:space="preserve"> + <source>Send private reports</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve"> @@ -4403,10 +6252,14 @@ This is your link for group %@!</source> <target>Odeslat je z galerie nebo vlastní klávesnice.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve"> + <source>Send up to 100 last messages to new members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> <target>Odesílatel zrušil přenos souboru.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> @@ -4463,6 +6316,10 @@ This is your link for group %@!</source> <target>Posláno v: % @</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Sent directly" xml:space="preserve"> + <source>Sent directly</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> <target>Odeslaná událost souboru</target> @@ -4473,11 +6330,59 @@ This is your link for group %@!</source> <target>Poslaná zpráva</target> <note>message info title</note> </trans-unit> + <trans-unit id="Sent messages" xml:space="preserve"> + <source>Sent messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> <target>Odeslané zprávy se po uplynutí nastavené doby odstraní.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sent reply" xml:space="preserve"> + <source>Sent reply</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent total" xml:space="preserve"> + <source>Sent total</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent via proxy" xml:space="preserve"> + <source>Sent via proxy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server" xml:space="preserve"> + <source>Server</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server added to operator %@." xml:space="preserve"> + <source>Server added to operator %@.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Server address" xml:space="preserve"> + <source>Server address</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings." xml:space="preserve"> + <source>Server address is incompatible with network settings.</source> + <note>srv error text.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Server address is incompatible with network settings: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server operator changed." xml:space="preserve"> + <source>Server operator changed.</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Server operators" xml:space="preserve"> + <source>Server operators</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server protocol changed." xml:space="preserve"> + <source>Server protocol changed.</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> <target>Server vyžaduje autorizaci pro vytváření front, zkontrolujte heslo</target> @@ -4493,11 +6398,31 @@ This is your link for group %@!</source> <target>Test serveru se nezdařil!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Server type" xml:space="preserve"> + <source>Server type</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server version is incompatible with network settings." xml:space="preserve"> + <source>Server version is incompatible with network settings.</source> + <note>srv error text</note> + </trans-unit> + <trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve"> + <source>Server version is incompatible with your app: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Servers" xml:space="preserve"> <source>Servers</source> <target>Servery</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Servers info" xml:space="preserve"> + <source>Servers info</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Session code" xml:space="preserve"> <source>Session code</source> <note>No comment provided by engineer.</note> @@ -4507,11 +6432,19 @@ This is your link for group %@!</source> <target>Nastavit 1 den</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set chat name…" xml:space="preserve"> + <source>Set chat name…</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> <target>Nastavení jména kontaktu…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve"> + <source>Set default theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> <target>Nastavení skupinových předvoleb</target> @@ -4522,11 +6455,19 @@ This is your link for group %@!</source> <target>Nastavte jej namísto ověřování systému.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve"> + <source>Set message expiration in chats.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passcode" xml:space="preserve"> <source>Set passcode</source> <target>Nastavit heslo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set passphrase" xml:space="preserve"> + <source>Set passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> <target>Nastavení přístupové fráze pro export</target> @@ -4547,24 +6488,49 @@ This is your link for group %@!</source> <target>Nastavení</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve"> + <source>Settings were changed.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve"> + <source>Shape profile images</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share" xml:space="preserve"> <source>Share</source> <target>Sdílet</target> - <note>chat item action</note> + <note>alert action +chat item action</note> </trans-unit> <trans-unit id="Share 1-time link" xml:space="preserve"> <source>Share 1-time link</source> <target>Sdílet jednorázovou pozvánku</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share 1-time link with a friend" xml:space="preserve"> + <source>Share 1-time link with a friend</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve"> + <source>Share SimpleX address on social media.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address" xml:space="preserve"> <source>Share address</source> <target>Sdílet adresu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share address publicly" xml:space="preserve"> + <source>Share address publicly</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address with contacts?" xml:space="preserve"> <source>Share address with contacts?</source> <target>Sdílet adresu s kontakty?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Share from other apps." xml:space="preserve"> + <source>Share from other apps.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share link" xml:space="preserve"> @@ -4572,9 +6538,16 @@ This is your link for group %@!</source> <target>Sdílet odkaz</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve"> - <source>Share one-time invitation link</source> - <target>Jednorázový zvací odkaz</target> + <trans-unit id="Share profile" xml:space="preserve"> + <source>Share profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share this 1-time invite link" xml:space="preserve"> + <source>Share this 1-time invite link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share to SimpleX" xml:space="preserve"> + <source>Share to SimpleX</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share with contacts" xml:space="preserve"> @@ -4582,6 +6555,14 @@ This is your link for group %@!</source> <target>Sdílet s kontakty</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Short link" xml:space="preserve"> + <source>Short link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show QR code" xml:space="preserve"> + <source>Show QR code</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> <target>Ukaž hovory v historii telefonu</target> @@ -4597,21 +6578,41 @@ This is your link for group %@!</source> <target>Zobrazit poslední zprávy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show message status" xml:space="preserve"> + <source>Show message status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show percentage" xml:space="preserve"> + <source>Show percentage</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Zobrazení náhledu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show → on messages sent via private routing." xml:space="preserve"> + <source>Show → on messages sent via private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show:" xml:space="preserve"> <source>Show:</source> <target>Zobrazit:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve"> + <source>SimpleX</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Address" xml:space="preserve"> <source>SimpleX Address</source> <target>SimpleX Adresa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." xml:space="preserve"> + <source>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> <target>Zabezpečení SimpleX chatu bylo auditováno společností Trail of Bits.</target> @@ -4642,6 +6643,18 @@ This is your link for group %@!</source> <target>Adresa SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX address and 1-time links are safe to share via any messenger." xml:space="preserve"> + <source>SimpleX address and 1-time links are safe to share via any messenger.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address or 1-time link?" xml:space="preserve"> + <source>SimpleX address or 1-time link?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX channel link" xml:space="preserve"> + <source>SimpleX channel link</source> + <note>simplex link type</note> + </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> <target>SimpleX kontaktní adresa</target> @@ -4660,6 +6673,14 @@ This is your link for group %@!</source> <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> <target>Odkazy na SimpleX</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve"> + <source>SimpleX links are prohibited.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve"> + <source>SimpleX links not allowed</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> @@ -4667,11 +6688,19 @@ This is your link for group %@!</source> <target>Jednorázová pozvánka SimpleX</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> + <source>SimpleX protocols reviewed by Trail of Bits.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Simplified incognito mode" xml:space="preserve"> <source>Simplified incognito mode</source> <target>Zjednodušený inkognito režim</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Size" xml:space="preserve"> + <source>Size</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Přeskočit</target> @@ -4687,19 +6716,44 @@ This is your link for group %@!</source> <target>Malé skupiny (max. 20)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Soft" xml:space="preserve"> + <source>Soft</source> + <note>blur media</note> + </trans-unit> + <trans-unit id="Some app settings were not migrated." xml:space="preserve"> + <source>Some app settings were not migrated.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some file(s) were not exported:" xml:space="preserve"> + <source>Some file(s) were not exported:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Během importu došlo k nezávažným chybám - podrobnosti naleznete v chat konzoli.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import:" xml:space="preserve"> + <source>Some non-fatal errors occurred during import:</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some servers failed the test: %@" xml:space="preserve"> + <source>Some servers failed the test: +%@</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> <target>Někdo</target> <note>notification title</note> </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve"> - <source>Start a new chat</source> - <target>Začít nový chat</target> + <trans-unit id="Spam" xml:space="preserve"> + <source>Spam</source> + <note>blocking reason +report reason</note> + </trans-unit> + <trans-unit id="Square, circle, or anything in between." xml:space="preserve"> + <source>Square, circle, or anything in between.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start chat" xml:space="preserve"> @@ -4707,11 +6761,23 @@ This is your link for group %@!</source> <target>Začít chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Start chat?" xml:space="preserve"> + <source>Start chat?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> <target>Zahájit přenesení</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Starting from %@." xml:space="preserve"> + <source>Starting from %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Statistics" xml:space="preserve"> + <source>Statistics</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> <target>Zastavit</target> @@ -4722,9 +6788,8 @@ This is your link for group %@!</source> <target>Zastavit SimpleX</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> - <source>Stop chat to enable database actions</source> - <target>Zastavte chat pro povolení akcí databáze</target> + <trans-unit id="Stop chat" xml:space="preserve"> + <source>Stop chat</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> @@ -4755,23 +6820,55 @@ This is your link for group %@!</source> <trans-unit id="Stop sharing" xml:space="preserve"> <source>Stop sharing</source> <target>Přestat sdílet</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Stop sharing address?" xml:space="preserve"> <source>Stop sharing address?</source> <target>Přestat sdílet adresu?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Stopping chat" xml:space="preserve"> + <source>Stopping chat</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Storage" xml:space="preserve"> + <source>Storage</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Strong" xml:space="preserve"> + <source>Strong</source> + <note>blur media</note> + </trans-unit> <trans-unit id="Submit" xml:space="preserve"> <source>Submit</source> <target>Odeslat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Subscribed" xml:space="preserve"> + <source>Subscribed</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscription errors" xml:space="preserve"> + <source>Subscription errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscriptions ignored" xml:space="preserve"> + <source>Subscriptions ignored</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Support SimpleX Chat" xml:space="preserve"> <source>Support SimpleX Chat</source> <target>Podpořte SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Switch audio and video during the call." xml:space="preserve"> + <source>Switch audio and video during the call.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> + <source>Switch chat profile for 1-time invitations.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="System" xml:space="preserve"> <source>System</source> <target>Systém</target> @@ -4782,11 +6879,19 @@ This is your link for group %@!</source> <target>Ověření systému</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP connection" xml:space="preserve"> + <source>TCP connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> <target>Časový limit připojení TCP</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP port for messaging" xml:space="preserve"> + <source>TCP port for messaging</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <source>TCP_KEEPCNT</source> <target>TCP_KEEPCNT</target> @@ -4802,11 +6907,19 @@ This is your link for group %@!</source> <target>TCP_KEEPINTVL</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tail" xml:space="preserve"> + <source>Tail</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> <target>Vyfotit</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap Create SimpleX address in the menu to create it later." xml:space="preserve"> + <source>Tap Create SimpleX address in the menu to create it later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> <target>Klepněte na tlačítko </target> @@ -4831,16 +6944,27 @@ This is your link for group %@!</source> <target>Klepnutím se připojíte inkognito</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve"> - <source>Tap to start a new chat</source> - <target>Klepnutím na zahájíte nový chat</target> + <trans-unit id="Tap to paste link" xml:space="preserve"> + <source>Tap to paste link</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap to scan" xml:space="preserve"> + <source>Tap to scan</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Temporary file error" xml:space="preserve"> + <source>Temporary file error</source> + <note>file error alert title</note> + </trans-unit> <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> <target>Test selhal v kroku %@.</target> <note>server test failure</note> </trans-unit> + <trans-unit id="Test notifications" xml:space="preserve"> + <source>Test notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> <target>Testovací server</target> @@ -4854,7 +6978,7 @@ This is your link for group %@!</source> <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> <target>Testy selhaly!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> @@ -4871,11 +6995,6 @@ This is your link for group %@!</source> <target>Díky uživatelům - přispívejte prostřednictvím Weblate!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> - <target>1. Platforma bez identifikátorů uživatelů - soukromá už od záměru.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <source>The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised.</source> @@ -4888,11 +7007,27 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován <target>Aplikace vás může upozornit na přijaté zprávy nebo žádosti o kontakt - povolte to v nastavení.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The app protects your privacy by using different operators in each conversation." xml:space="preserve"> + <source>The app protects your privacy by using different operators in each conversation.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve"> + <source>The app will ask to confirm downloads from unknown file servers (except .onion).</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> <source>The attempt to change database passphrase was not completed.</source> <target>Pokus o změnu přístupové fráze databáze nebyl dokončen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve"> + <source>The code you scanned is not a SimpleX link QR code.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The connection reached the limit of undelivered messages, your contact may be offline." xml:space="preserve"> + <source>The connection reached the limit of undelivered messages, your contact may be offline.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> <target>Připojení, které jste přijali, bude zrušeno!</target> @@ -4913,6 +7048,11 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován <target>Šifrování funguje a nové povolení šifrování není vyžadováno. To může vyvolat chybu v připojení!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> + <target>Nová generace soukromých zpráv</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The hash of the previous message is different." xml:space="preserve"> <source>The hash of the previous message is different.</source> <target>Hash předchozí zprávy se liší.</target> @@ -4928,9 +7068,12 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován <target>Zpráva bude pro všechny členy označena jako moderovaná.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> - <target>Nová generace soukromých zpráv</target> + <trans-unit id="The messages will be deleted for all members." xml:space="preserve"> + <source>The messages will be deleted for all members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve"> + <source>The messages will be marked as moderated for all members.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> @@ -4938,9 +7081,12 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován <target>Stará databáze nebyla během přenášení odstraněna, lze ji smazat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> - <target>Profil je sdílen pouze s vašimi kontakty.</target> + <trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve"> + <source>The same conditions will apply to operator **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second preset operator in the app!" xml:space="preserve"> + <source>The second preset operator in the app!</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> @@ -4958,19 +7104,24 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován <target>Servery pro nová připojení vašeho aktuálního chat profilu **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Theme" xml:space="preserve"> - <source>Theme</source> - <target>Téma</target> + <trans-unit id="The servers for new files of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new files of your current chat profile **%@**.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve"> - <source>There should be at least one user profile.</source> - <target>Měl by tam být alespoň jeden uživatelský profil.</target> + <trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve"> + <source>The text you pasted is not a SimpleX link.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> - <source>There should be at least one visible user profile.</source> - <target>Měl by tam být alespoň jeden viditelný uživatelský profil.</target> + <trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve"> + <source>The uploaded database archive will be permanently removed from the servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Themes" xml:space="preserve"> + <source>Themes</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="These conditions will also apply for: **%@**." xml:space="preserve"> + <source>These conditions will also apply for: **%@**.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> @@ -4993,15 +7144,31 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován <target>Tuto akci nelze vzít zpět - zprávy odeslané a přijaté dříve, než bylo zvoleno, budou smazány. Může to trvat několik minut.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> <target>Tuto akci nelze vzít zpět - váš profil, kontakty, zprávy a soubory budou nenávratně ztraceny.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by end-to-end encryption.</source> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by quantum resistant end-to-end encryption.</source> + <note>E2EE info chat item</note> + </trans-unit> <trans-unit id="This device name" xml:space="preserve"> <source>This device name</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve"> + <source>This display name is invalid. Please choose another name.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> <source>This group has over %lld members, delivery receipts are not sent.</source> <target>Tato skupina má více než %lld členů, potvrzení o doručení nejsou odesílány.</target> @@ -5020,11 +7187,27 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován <source>This is your own one-time link!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve"> + <source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve"> + <source>This link was used with another mobile device, please create a new link on the desktop.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This message was deleted or not received yet." xml:space="preserve"> + <source>This message was deleted or not received yet.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <source>This setting applies to messages in your current chat profile **%@**.</source> <target>Toto nastavení platí pro zprávy ve vašem aktuálním chat profilu **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Title" xml:space="preserve"> + <source>Title</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> <source>To ask any questions and to receive updates:</source> <target>Chcete-li položit jakékoli dotazy a dostávat aktuality:</target> @@ -5044,9 +7227,8 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován <target>Vytvoření nového připojení</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <target>Pro ochranu soukromí namísto ID uživatelů používaných všemi ostatními platformami má SimpleX identifikátory pro fronty zpráv, oddělené pro každý z vašich kontaktů.</target> + <trans-unit id="To protect against your link being replaced, you can compare contact security codes." xml:space="preserve"> + <source>To protect against your link being replaced, you can compare contact security codes.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -5054,6 +7236,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován <target>K ochraně časového pásma používají obrazové/hlasové soubory UTC.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve"> + <source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> @@ -5061,6 +7247,23 @@ You will be prompted to complete authentication before this feature is enabled.< Před zapnutím této funkce budete vyzváni k dokončení ověření.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> + <target>Pro ochranu soukromí namísto ID uživatelů používaných všemi ostatními platformami má SimpleX identifikátory pro fronty zpráv, oddělené pro každý z vašich kontaktů.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To receive" xml:space="preserve"> + <source>To receive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve"> + <source>To record speech please grant permission to use Microphone.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record video please grant permission to use Camera." xml:space="preserve"> + <source>To record video please grant permission to use Camera.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> <target>Chcete-li nahrávat hlasové zprávy, udělte povolení k použití mikrofonu.</target> @@ -5071,26 +7274,54 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření.</target> <target>Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce **Chat profily**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To send" xml:space="preserve"> + <source>To send</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> <source>To support instant push notifications the chat database has to be migrated.</source> <target>Pro podporu doručování okamžitých upozornění musí být přenesena chat databáze.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To use the servers of **%@**, accept conditions of use." xml:space="preserve"> + <source>To use the servers of **%@**, accept conditions of use.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> <target>Chcete-li ověřit koncové šifrování u svého kontaktu, porovnejte (nebo naskenujte) kód na svých zařízeních.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle chat list:" xml:space="preserve"> + <source>Toggle chat list:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> <source>Toggle incognito when connecting.</source> <target>Změnit inkognito režim při připojení.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Token status: %@." xml:space="preserve"> + <source>Token status: %@.</source> + <note>token status</note> + </trans-unit> + <trans-unit id="Toolbar opacity" xml:space="preserve"> + <source>Toolbar opacity</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Total" xml:space="preserve"> + <source>Total</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>Izolace transportu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Transport sessions" xml:space="preserve"> + <source>Transport sessions</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> <target>Pokus o připojení k serveru používanému k přijímání zpráv od tohoto kontaktu (chyba: %@).</target> @@ -5101,16 +7332,15 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření.</target> <target>Pokus o připojení k serveru používanému pro příjem zpráv od tohoto kontaktu.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Turkish interface" xml:space="preserve"> + <source>Turkish interface</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> <target>Vypnout</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve"> - <source>Turn off notifications?</source> - <target>Vypnout upozornění?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> <target>Zapnout</target> @@ -5125,18 +7355,25 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření.</target> <source>Unblock</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock for all" xml:space="preserve"> + <source>Unblock for all</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member" xml:space="preserve"> <source>Unblock member</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock member for all?" xml:space="preserve"> + <source>Unblock member for all?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member?" xml:space="preserve"> <source>Unblock member?</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve"> - <source>Unexpected error: %@</source> - <target>Neočekávaná chyba: %@</target> - <note>item status description</note> + <trans-unit id="Undelivered messages" xml:space="preserve"> + <source>Undelivered messages</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -5146,7 +7383,7 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření.</target> <trans-unit id="Unfav." xml:space="preserve"> <source>Unfav.</source> <target>Odobl.</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> @@ -5183,6 +7420,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření.</target> <target>Neznámá chyba</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unknown servers!" xml:space="preserve"> + <source>Unknown servers!</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> <target>Při nepoužívání rozhraní volání iOS, povolte režim Nerušit, abyste se vyhnuli vyrušování.</target> @@ -5216,11 +7457,19 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> <target>Zrušit ztlumení</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> </trans-unit> <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> <target>Nepřečtený</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Unsupported connection link" xml:space="preserve"> + <source>Unsupported connection link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve"> + <source>Up to 100 last messages are sent to new members.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update" xml:space="preserve"> @@ -5228,11 +7477,6 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu <target>Aktualizovat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> - <source>Update .onion hosts setting?</source> - <target>Aktualizovat nastavení hostitelů .onion?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> <target>Aktualizovat přístupovou frázi databáze</target> @@ -5243,9 +7487,12 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu <target>Aktualizovat nastavení sítě?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve"> - <source>Update transport isolation mode?</source> - <target>Aktualizovat režim dopravní izolace?</target> + <trans-unit id="Update settings?" xml:space="preserve"> + <source>Update settings?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updated conditions" xml:space="preserve"> + <source>Updated conditions</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> @@ -5253,31 +7500,62 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu <target>Aktualizací nastavení se klient znovu připojí ke všem serverům.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> - <target>Aktualizace tohoto nastavení znovu připojí klienta ke všem serverům.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Upgrade and open chat" xml:space="preserve"> <source>Upgrade and open chat</source> <target>Zvýšit a otevřít chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Upload errors" xml:space="preserve"> + <source>Upload errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload failed" xml:space="preserve"> + <source>Upload failed</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Upload file" xml:space="preserve"> <source>Upload file</source> <target>Nahrát soubor</target> <note>server test step</note> </trans-unit> + <trans-unit id="Uploaded" xml:space="preserve"> + <source>Uploaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploaded files" xml:space="preserve"> + <source>Uploaded files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploading archive" xml:space="preserve"> + <source>Uploading archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use %@" xml:space="preserve"> + <source>Use %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> <target>Použít hostitele .onion</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use SOCKS proxy" xml:space="preserve"> + <source>Use SOCKS proxy</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> <target>Používat servery SimpleX Chat?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use TCP port %@ when no port is specified." xml:space="preserve"> + <source>Use TCP port %@ when no port is specified.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve"> + <source>Use TCP port 443 for preset servers only.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> <target>Použijte chat</target> @@ -5288,6 +7566,14 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu <target>Použít aktuální profil</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use for files" xml:space="preserve"> + <source>Use for files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for messages" xml:space="preserve"> + <source>Use for messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Použít pro nová připojení</target> @@ -5307,19 +7593,49 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu <target>Použít nový inkognito profil</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use only local notifications?" xml:space="preserve"> + <source>Use only local notifications?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve"> + <source>Use private routing with unknown servers when IP address is not protected.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers." xml:space="preserve"> + <source>Use private routing with unknown servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Použít server</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> - <target>Profil uživatele</target> + <trans-unit id="Use servers" xml:space="preserve"> + <source>Use servers</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> - <target>Použití hostitelů .onion vyžaduje kompatibilního poskytovatele VPN.</target> + <trans-unit id="Use short links (BETA)" xml:space="preserve"> + <source>Use short links (BETA)</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app while in the call." xml:space="preserve"> + <source>Use the app while in the call.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app with one hand." xml:space="preserve"> + <source>Use the app with one hand.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use web port" xml:space="preserve"> + <source>Use web port</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="User selection" xml:space="preserve"> + <source>User selection</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Username" xml:space="preserve"> + <source>Username</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> @@ -5344,6 +7660,14 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu <source>Verify connections</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Verify database passphrase" xml:space="preserve"> + <source>Verify database passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify passphrase" xml:space="preserve"> + <source>Verify passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Verify security code" xml:space="preserve"> <source>Verify security code</source> <target>Ověření bezpečnostního kódu</target> @@ -5378,11 +7702,23 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu <target>Videa a soubory až do velikosti 1 gb</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View conditions" xml:space="preserve"> + <source>View conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> <target>Zobrazení bezpečnostního kódu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View updated conditions" xml:space="preserve"> + <source>View updated conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Visible history" xml:space="preserve"> + <source>Visible history</source> + <note>chat feature</note> + </trans-unit> <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> <target>Hlasové zprávy</target> @@ -5393,11 +7729,15 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu <target>Hlasové zprávy jsou v tomto chatu zakázány.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <target>Hlasové zprávy jsou v této skupině zakázány.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Voice messages not allowed" xml:space="preserve"> + <source>Voice messages not allowed</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> <source>Voice messages prohibited!</source> <target>Hlasové zprávy jsou zakázány!</target> @@ -5427,6 +7767,18 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu <target>Čekám na video</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wallpaper accent" xml:space="preserve"> + <source>Wallpaper accent</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper background" xml:space="preserve"> + <source>Wallpaper background</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve"> + <source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> <source>Warning: you may lose some data!</source> <target>Upozornění: můžete ztratit nějaká data!</target> @@ -5447,6 +7799,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu <target>Uvítací zpráva</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Welcome message is too long" xml:space="preserve"> + <source>Welcome message is too long</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="What's new" xml:space="preserve"> <source>What's new</source> <target>Co je nového</target> @@ -5457,9 +7813,12 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu <target>Když je k dispozici</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> - <source>When people request to connect, you can accept or reject it.</source> - <target>Když někdo požádá o připojení, můžete žádost přijmout nebo odmítnout.</target> + <trans-unit id="When connecting audio and video calls." xml:space="preserve"> + <source>When connecting audio and video calls.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When more than one operator is enabled, none of them has metadata to learn who communicates with whom." xml:space="preserve"> + <source>When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> @@ -5467,29 +7826,63 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu <target>Pokud s někým sdílíte inkognito profil, bude tento profil použit pro skupiny, do kterých vás pozve.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="WiFi" xml:space="preserve"> + <source>WiFi</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Will be enabled in direct chats!" xml:space="preserve"> + <source>Will be enabled in direct chats!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wired ethernet" xml:space="preserve"> + <source>Wired ethernet</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With encrypted files and media." xml:space="preserve"> + <source>With encrypted files and media.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> <target>S volitelnou uvítací zprávou.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="With reduced battery usage." xml:space="preserve"> + <source>With reduced battery usage.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to file servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> <target>Špatná přístupová fráze k databázi</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve"> + <source>Wrong key or unknown connection - most likely this connection is deleted.</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Wrong key or unknown file chunk address - most likely file is deleted." xml:space="preserve"> + <source>Wrong key or unknown file chunk address - most likely file is deleted.</source> + <note>file error text</note> + </trans-unit> <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> <target>Špatná přístupová fráze!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="XFTP servers" xml:space="preserve"> - <source>XFTP servers</source> - <target>XFTP servery</target> + <trans-unit id="XFTP server" xml:space="preserve"> + <source>XFTP server</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> - <target>Vy</target> + <trans-unit id="You **must not** use the same database on two devices." xml:space="preserve"> + <source>You **must not** use the same database on two devices.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You accepted connection" xml:space="preserve"> @@ -5512,6 +7905,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu <target>Již jste připojeni k %@.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are already connected with %@." xml:space="preserve"> + <source>You are already connected with %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You are already connecting to %@." xml:space="preserve"> <source>You are already connecting to %@.</source> <note>No comment provided by engineer.</note> @@ -5551,14 +7948,21 @@ Repeat join request?</source> <target>Jste pozváni do skupiny</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are not connected to these servers. Private routing is used to deliver messages to them." xml:space="preserve"> + <source>You are not connected to these servers. Private routing is used to deliver messages to them.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> <source>You can accept calls from lock screen, without device and app authentication.</source> <target>Můžete přijímat hovory z obrazovky zámku, bez ověření zařízení a aplikace.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <target>Můžete se také připojit kliknutím na odkaz. Pokud se otevře v prohlížeči, klikněte na tlačítko **Otevřít v mobilní aplikaci**.</target> + <trans-unit id="You can change it in Appearance settings." xml:space="preserve"> + <source>You can change it in Appearance settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can configure servers via settings." xml:space="preserve"> + <source>You can configure servers via settings.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can create it later" xml:space="preserve"> @@ -5576,16 +7980,32 @@ Repeat join request?</source> <target>Můžete je povolit později v nastavení Soukromí & Bezpečnosti aplikace</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can give another try." xml:space="preserve"> + <source>You can give another try.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <source>You can hide or mute a user profile - swipe it to the right.</source> <target>Profil uživatele můžete skrýt nebo ztlumit - přejeďte prstem doprava.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> + <trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve"> + <source>You can make it visible to your SimpleX contacts via Settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now chat with %@" xml:space="preserve"> + <source>You can now chat with %@</source> <target>Nyní můžete posílat zprávy %@</target> <note>notification body</note> </trans-unit> + <trans-unit id="You can send messages to %@ from Archived contacts." xml:space="preserve"> + <source>You can send messages to %@ from Archived contacts.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can set connection name, to remember who the link was shared with." xml:space="preserve"> + <source>You can set connection name, to remember who the link was shared with.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> <target>Náhled oznámení na zamykací obrazovce můžete změnit v nastavení.</target> @@ -5601,16 +8021,15 @@ Repeat join request?</source> <target>Tuto adresu můžete sdílet s vašimi kontakty, abyse se mohli spojit s **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> - <source>You can share your address as a link or QR code - anybody can connect to you.</source> - <target>Můžete sdílet svou adresu jako odkaz nebo jako QR kód - kdokoli se k vám bude moci připojit.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> <target>Chat můžete zahájit prostřednictvím aplikace Nastavení / Databáze nebo restartováním aplikace</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve"> + <source>You can still view conversation with %@ in the list of chats.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> <target>Zámek SimpleX můžete zapnout v Nastavení.</target> @@ -5621,21 +8040,25 @@ Repeat join request?</source> <target>K formátování zpráv můžete použít markdown:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can view invitation link again in connection details." xml:space="preserve"> + <source>You can view invitation link again in connection details.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> <target>Nemůžete posílat zprávy!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <target>Sami řídíte, přes který server(y) **přijímat** zprávy, své kontakty – servery, které používáte k odesílání zpráv.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <target>Nemohli jste být ověřeni; Zkuste to prosím znovu.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> + <target>Lidé se s vámi mohou spojit pouze prostřednictvím odkazu, který sdílíte.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You have already requested connection via this address!" xml:space="preserve"> <source>You have already requested connection via this address!</source> <note>No comment provided by engineer.</note> @@ -5645,11 +8068,6 @@ Repeat join request?</source> Repeat connection request?</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> - <target>Nemáte žádné konverzace</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> <target>Musíte zadat přístupovou frázi při každém spuštění aplikace - není uložena v zařízení.</target> @@ -5670,11 +8088,23 @@ Repeat connection request?</source> <target>Připojili jste se k této skupině. Připojení k pozvání člena skupiny.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You may migrate the exported database." xml:space="preserve"> + <source>You may migrate the exported database.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may save the exported archive." xml:space="preserve"> + <source>You may save the exported archive.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> <target>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ů.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You need to allow your contact to call to be able to call them." xml:space="preserve"> + <source>You need to allow your contact to call to be able to call them.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> <target>Abyste mohli odesílat hlasové zprávy, musíte je povolit svému kontaktu.</target> @@ -5690,6 +8120,10 @@ Repeat connection request?</source> <target>Odeslali jste pozvánku do skupiny</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You should receive notifications." xml:space="preserve"> + <source>You should receive notifications.</source> + <note>token info</note> + </trans-unit> <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> <target>Ke skupině budete připojeni, až bude zařízení hostitele skupiny online, vyčkejte prosím nebo se podívejte později!</target> @@ -5723,6 +8157,10 @@ Repeat connection request?</source> <target>Stále budete přijímat volání a upozornění od umlčených profilů pokud budou aktivní.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You will stop receiving messages from this chat. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this chat. Chat history will be preserved.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> <target>Přestanete dostávat zprávy z této skupiny. Historie chatu bude zachována.</target> @@ -5743,31 +8181,16 @@ Repeat connection request?</source> <target>Pro tuto skupinu používáte inkognito profil - abyste zabránili sdílení svého hlavního profilu, není pozvání kontaktů povoleno</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your %@ servers" xml:space="preserve"> - <source>Your %@ servers</source> - <target>Vaše servery %@</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your ICE servers" xml:space="preserve"> <source>Your ICE servers</source> <target>Vaše servery ICE</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> - <target>Vaše servery SMP</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your SimpleX address" xml:space="preserve"> <source>Your SimpleX address</source> <target>Vaše SimpleX adresa</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your XFTP servers" xml:space="preserve"> - <source>Your XFTP servers</source> - <target>Vaše XFTP servery</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your calls" xml:space="preserve"> <source>Your calls</source> <target>Vaše hovory</target> @@ -5783,16 +8206,17 @@ Repeat connection request?</source> <target>Vaše chat databáze není šifrována – nastavte přístupovou frázi pro její šifrování.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your chat preferences" xml:space="preserve"> + <source>Your chat preferences</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>Vaše chat profily</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> - <target>K dokončení připojení, musí být váš kontakt online. -Toto připojení můžete zrušit a kontakt odebrat (a zkusit to později s novým odkazem).</target> + <trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve"> + <source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> @@ -5805,18 +8229,15 @@ Toto připojení můžete zrušit a kontakt odebrat (a zkusit to později s nov <target>Vaše kontakty mohou povolit úplné mazání zpráv.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> - <source>Your contacts in SimpleX will see it. -You can change it in Settings.</source> - <target>Vaše kontakty v SimpleX ji uvidí. -Můžete ji změnit v Nastavení.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your contacts will remain connected." xml:space="preserve"> <source>Your contacts will remain connected.</source> <target>Vaše kontakty zůstanou připojeny.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve"> + <source>Your credentials may be sent unencrypted.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> <target>Vaše aktuální chat databáze bude ODSTRANĚNA a NAHRAZENA importovanou.</target> @@ -5846,33 +8267,34 @@ Můžete ji změnit v Nastavení.</target> <target>Váš profil **%@** bude sdílen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> - <target>Váš profil je uložen ve vašem zařízení a sdílen pouze s vašimi kontakty. -Servery SimpleX nevidí váš profil.</target> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> + <target>Profil je sdílen pouze s vašimi kontakty.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> - <target>Váš profil, kontakty a doručené zprávy jsou uloženy ve vašem zařízení.</target> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> + <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> + <target>Váš profil je uložen ve vašem zařízení a sdílen pouze s vašimi kontakty. Servery SimpleX nevidí váš profil.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve"> + <source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> <target>Váš náhodný profil</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> - <target>Váš server</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> <target>Adresa vašeho serveru</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your servers" xml:space="preserve"> + <source>Your servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your settings" xml:space="preserve"> <source>Your settings</source> <target>Vaše nastavení</target> @@ -5913,11 +8335,19 @@ Servery SimpleX nevidí váš profil.</target> <target>přijatý hovor</target> <note>call status</note> </trans-unit> + <trans-unit id="accepted invitation" xml:space="preserve"> + <source>accepted invitation</source> + <note>chat list item title</note> + </trans-unit> <trans-unit id="admin" xml:space="preserve"> <source>admin</source> <target>správce</target> <note>member role</note> </trans-unit> + <trans-unit id="admins" xml:space="preserve"> + <source>admins</source> + <note>feature role</note> + </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> <target>povoluji šifrování pro %@…</target> @@ -5928,6 +8358,10 @@ Servery SimpleX nevidí váš profil.</target> <target>povoluji šifrování…</target> <note>chat item text</note> </trans-unit> + <trans-unit id="all members" xml:space="preserve"> + <source>all members</source> + <note>feature role</note> + </trans-unit> <trans-unit id="always" xml:space="preserve"> <source>always</source> <target>vždy</target> @@ -5937,6 +8371,14 @@ Servery SimpleX nevidí váš profil.</target> <source>and %lld other events</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="archived report" xml:space="preserve"> + <source>archived report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="attempts" xml:space="preserve"> + <source>attempts</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <source>audio call (not e2e encrypted)</source> <target>zvukový hovor (nešifrovaný e2e)</target> @@ -5958,13 +8400,26 @@ Servery SimpleX nevidí váš profil.</target> </trans-unit> <trans-unit id="blocked" xml:space="preserve"> <source>blocked</source> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="blocked %@" xml:space="preserve"> + <source>blocked %@</source> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="blocked by admin" xml:space="preserve"> + <source>blocked by admin</source> + <note>blocked chat item +marked deleted chat item preview text</note> </trans-unit> <trans-unit id="bold" xml:space="preserve"> <source>bold</source> <target>tučně</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="call" xml:space="preserve"> + <source>call</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="call error" xml:space="preserve"> <source>call error</source> <target>chyba volání</target> @@ -6068,7 +8523,7 @@ Servery SimpleX nevidí váš profil.</target> <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> <target>připojení…</target> - <note>chat list item title</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> @@ -6080,6 +8535,10 @@ Servery SimpleX nevidí váš profil.</target> <target>připojení:%@</target> <note>connection information</note> </trans-unit> + <trans-unit id="contact %@ changed to %@" xml:space="preserve"> + <source>contact %1$@ changed to %2$@</source> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> <target>kontakt má šifrování e2e</target> @@ -6110,10 +8569,15 @@ Servery SimpleX nevidí váš profil.</target> <target>dní</target> <note>time unit</note> </trans-unit> + <trans-unit id="decryption errors" xml:space="preserve"> + <source>decryption errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> <target>výchozí (%@)</target> - <note>pref value</note> + <note>delete after time +pref value</note> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> @@ -6159,6 +8623,10 @@ Servery SimpleX nevidí váš profil.</target> <target>duplicitní zpráva</target> <note>integrity error chat item</note> </trans-unit> + <trans-unit id="duplicates" xml:space="preserve"> + <source>duplicates</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> <target>e2e šifrované</target> @@ -6234,8 +8702,12 @@ Servery SimpleX nevidí váš profil.</target> <target>chyba</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="event happened" xml:space="preserve"> - <source>event happened</source> + <trans-unit id="expired" xml:space="preserve"> + <source>expired</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="forwarded" xml:space="preserve"> + <source>forwarded</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> @@ -6263,6 +8735,10 @@ Servery SimpleX nevidí váš profil.</target> <target>Klíčenka pro iOS bude použita k bezpečnému uložení přístupové fráze po restartování aplikace nebo změně přístupové fráze – umožní příjem oznámení push.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="inactive" xml:space="preserve"> + <source>inactive</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> <target>inkognito přes odkaz na kontaktní adresu</target> @@ -6303,6 +8779,10 @@ Servery SimpleX nevidí váš profil.</target> <target>pozvánka do skupiny %@</target> <note>group name</note> </trans-unit> + <trans-unit id="invite" xml:space="preserve"> + <source>invite</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> <target>pozvánka</target> @@ -6348,11 +8828,19 @@ Servery SimpleX nevidí váš profil.</target> <target>člen</target> <note>member role</note> </trans-unit> + <trans-unit id="member %@ changed to %@" xml:space="preserve"> + <source>member %1$@ changed to %2$@</source> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> <target>připojeno</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="message" xml:space="preserve"> + <source>message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="message received" xml:space="preserve"> <source>message received</source> <target>zpráva přijata</target> @@ -6376,7 +8864,11 @@ Servery SimpleX nevidí váš profil.</target> <trans-unit id="moderated by %@" xml:space="preserve"> <source>moderated by %@</source> <target>moderovaný %@</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="moderator" xml:space="preserve"> + <source>moderator</source> + <note>member role</note> </trans-unit> <trans-unit id="months" xml:space="preserve"> <source>months</source> @@ -6386,7 +8878,7 @@ Servery SimpleX nevidí váš profil.</target> <trans-unit id="never" xml:space="preserve"> <source>never</source> <target>nikdy</target> - <note>No comment provided by engineer.</note> + <note>delete after time</note> </trans-unit> <trans-unit id="new message" xml:space="preserve"> <source>new message</source> @@ -6417,8 +8909,8 @@ Servery SimpleX nevidí váš profil.</target> <source>off</source> <target>vypnuto</target> <note>enabled status - group pref value - time to disappear</note> +group pref value +time to disappear</note> </trans-unit> <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> @@ -6435,16 +8927,40 @@ Servery SimpleX nevidí váš profil.</target> <target>zapnuto</target> <note>group pref value</note> </trans-unit> + <trans-unit id="other" xml:space="preserve"> + <source>other</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="other errors" xml:space="preserve"> + <source>other errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="owner" xml:space="preserve"> <source>owner</source> <target>vlastník</target> <note>member role</note> </trans-unit> + <trans-unit id="owners" xml:space="preserve"> + <source>owners</source> + <note>feature role</note> + </trans-unit> <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> <target>peer-to-peer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="pending" xml:space="preserve"> + <source>pending</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="pending approval" xml:space="preserve"> + <source>pending approval</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="quantum resistant e2e encryption" xml:space="preserve"> + <source>quantum resistant e2e encryption</source> + <note>chat item text</note> + </trans-unit> <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> <target>obdržel odpověď…</target> @@ -6455,6 +8971,10 @@ Servery SimpleX nevidí váš profil.</target> <target>obdržel potvrzení…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="rejected" xml:space="preserve"> + <source>rejected</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> <target>odmítnutý hovor</target> @@ -6470,11 +8990,35 @@ Servery SimpleX nevidí váš profil.</target> <target>odstraněno %@</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="removed contact address" xml:space="preserve"> + <source>removed contact address</source> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="removed profile picture" xml:space="preserve"> + <source>removed profile picture</source> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> <target>odstranil vás</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="requested to connect" xml:space="preserve"> + <source>requested to connect</source> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="saved" xml:space="preserve"> + <source>saved</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="saved from %@" xml:space="preserve"> + <source>saved from %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="search" xml:space="preserve"> + <source>search</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="sec" xml:space="preserve"> <source>sec</source> <target>sek</target> @@ -6500,6 +9044,24 @@ Servery SimpleX nevidí váš profil.</target> <target>odeslat přímou zprávu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve"> + <source>server queue info: %1$@ + +last received msg: %2$@</source> + <note>queue info</note> + </trans-unit> + <trans-unit id="set new contact address" xml:space="preserve"> + <source>set new contact address</source> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="set new profile picture" xml:space="preserve"> + <source>set new profile picture</source> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="standard end-to-end encryption" xml:space="preserve"> + <source>standard end-to-end encryption</source> + <note>chat item text</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>začíná…</target> @@ -6515,16 +9077,36 @@ Servery SimpleX nevidí váš profil.</target> <target>tento kontakt</target> <note>notification title</note> </trans-unit> + <trans-unit id="unblocked %@" xml:space="preserve"> + <source>unblocked %@</source> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="unknown" xml:space="preserve"> <source>unknown</source> <target>neznámý</target> <note>connection info</note> </trans-unit> + <trans-unit id="unknown servers" xml:space="preserve"> + <source>unknown servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unknown status" xml:space="preserve"> + <source>unknown status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unprotected" xml:space="preserve"> + <source>unprotected</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="updated group profile" xml:space="preserve"> <source>updated group profile</source> <target>aktualizoval profil skupiny</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="updated profile" xml:space="preserve"> + <source>updated profile</source> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="v%@" xml:space="preserve"> <source>v%@</source> <note>No comment provided by engineer.</note> @@ -6554,6 +9136,10 @@ Servery SimpleX nevidí váš profil.</target> <target>přes relé</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="video" xml:space="preserve"> + <source>video</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> <source>video call (not e2e encrypted)</source> <target>videohovoru (nešifrovaného e2e)</target> @@ -6579,11 +9165,19 @@ Servery SimpleX nevidí váš profil.</target> <target>týdnů</target> <note>time unit</note> </trans-unit> + <trans-unit id="when IP hidden" xml:space="preserve"> + <source>when IP hidden</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="yes" xml:space="preserve"> <source>yes</source> <target>ano</target> <note>pref value</note> </trans-unit> + <trans-unit id="you" xml:space="preserve"> + <source>you</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> <target>jste pozváni do skupiny</target> @@ -6594,6 +9188,10 @@ Servery SimpleX nevidí váš profil.</target> <target>jste pozorovatel</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="you blocked %@" xml:space="preserve"> + <source>you blocked %@</source> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> <target>změnili jste adresu</target> @@ -6634,6 +9232,10 @@ Servery SimpleX nevidí váš profil.</target> <target>sdíleli jste jednorázový odkaz inkognito</target> <note>chat list item description</note> </trans-unit> + <trans-unit id="you unblocked %@" xml:space="preserve"> + <source>you unblocked %@</source> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you: " xml:space="preserve"> <source>you: </source> <target>vy: </target> @@ -6648,7 +9250,7 @@ Servery SimpleX nevidí váš profil.</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="cs" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6684,7 +9286,7 @@ Servery SimpleX nevidí váš profil.</target> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="cs" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> @@ -6704,4 +9306,205 @@ Servery SimpleX nevidí váš profil.</target> </trans-unit> </body> </file> + <file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="cs" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%d new events" xml:space="preserve"> + <source>%d new events</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From %d chat(s)" xml:space="preserve"> + <source>From %d chat(s)</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From: %@" xml:space="preserve"> + <source>From: %@</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <note>notification</note> + </trans-unit> + <trans-unit id="New messages" xml:space="preserve"> + <source>New messages</source> + <note>notification</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="cs" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> + <source>SimpleX SE</source> + <note>Bundle display name</note> + </trans-unit> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX SE</source> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> + <source>Copyright © 2024 SimpleX Chat. All rights reserved.</source> + <note>Copyright (human-readable)</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="cs" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App is locked!" xml:space="preserve"> + <source>App is locked!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Comment" xml:space="preserve"> + <source>Comment</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade required" xml:space="preserve"> + <source>Database downgrade required</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade required" xml:space="preserve"> + <source>Database upgrade required</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing file" xml:space="preserve"> + <source>Error preparing file</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing message" xml:space="preserve"> + <source>Error preparing message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No active profile" xml:space="preserve"> + <source>No active profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to downgrade the database." xml:space="preserve"> + <source>Open the app to downgrade the database.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to upgrade the database." xml:space="preserve"> + <source>Open the app to upgrade the database.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passphrase" xml:space="preserve"> + <source>Passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve"> + <source>Please create a profile in the SimpleX app</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending a message takes longer than expected." xml:space="preserve"> + <source>Sending a message takes longer than expected.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending message…" xml:space="preserve"> + <source>Sending message…</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Slow network?" xml:space="preserve"> + <source>Slow network?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unsupported format" xml:space="preserve"> + <source>Unsupported format</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wait" xml:space="preserve"> + <source>Wait</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve"> + <source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + </body> + </file> </xliff> diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/contents.json b/apps/ios/SimpleX Localizations/cs.xcloc/contents.json index 5c7c929ee3..9cd5922c24 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/cs.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "cs", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "16.2" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 75f70f7ad1..06fd7c5a1d 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -2,36 +2,9 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="de" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> - <trans-unit id=" " xml:space="preserve"> - <source> -</source> - <target> -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (" xml:space="preserve"> - <source> (</source> - <target> (</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> <target> (kann kopiert werden)</target> @@ -49,7 +22,7 @@ </trans-unit> <trans-unit id="## History" xml:space="preserve"> <source>## History</source> - <target>## Vergangenheit</target> + <target>## Nachrichtenverlauf</target> <note>copied message info</note> </trans-unit> <trans-unit id="## In reply to" xml:space="preserve"> @@ -107,6 +80,11 @@ <target>%@ wurde mit Ihnen verbunden</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve"> + <source>%@ downloaded</source> + <target>%@ heruntergeladen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ is connected!" xml:space="preserve"> <source>%@ is connected!</source> <target>%@ ist mit Ihnen verbunden!</target> @@ -122,9 +100,19 @@ <target>%@ wurde erfolgreich überprüft</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ server" xml:space="preserve"> + <source>%@ server</source> + <target>%@ Server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ servers" xml:space="preserve"> <source>%@ servers</source> - <target>%@-Server</target> + <target>%@ Server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve"> + <source>%@ uploaded</source> + <target>%@ hochgeladen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ wants to connect!" xml:space="preserve"> @@ -132,6 +120,11 @@ <target>%@ will sich mit Ihnen verbinden!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve"> + <source>%1$@, %2$@</source> + <target>%1$@, %2$@</target> + <note>format for date separator in chat</note> + </trans-unit> <trans-unit id="%@, %@ and %lld members" xml:space="preserve"> <source>%@, %@ and %lld members</source> <target>%@, %@ und %lld Mitglieder</target> @@ -152,11 +145,36 @@ <target>%d Tage</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve"> + <source>%d file(s) are still being downloaded.</source> + <target>%d Datei(en) wird/werden immer noch heruntergeladen.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve"> + <source>%d file(s) failed to download.</source> + <target>Bei %d Datei(en) ist das Herunterladen fehlgeschlagen.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve"> + <source>%d file(s) were deleted.</source> + <target>%d Datei(en) wurde(n) gelöscht.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve"> + <source>%d file(s) were not downloaded.</source> + <target>%d Datei(en) wurde(n) nicht heruntergeladen.</target> + <note>forward confirmation reason</note> + </trans-unit> <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> <target>%d Stunden</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve"> + <source>%d messages not forwarded</source> + <target>%d Nachrichten wurden nicht weitergeleitet</target> + <note>alert title</note> + </trans-unit> <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> <target>%d min</target> @@ -172,6 +190,11 @@ <target>%d s</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve"> + <source>%d seconds(s)</source> + <target>%d Sekunde(n)</target> + <note>delete after time</note> + </trans-unit> <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> <target>%d übersprungene Nachricht(en)</target> @@ -217,6 +240,11 @@ <target>%lld Nachrichten blockiert</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve"> + <source>%lld messages blocked by admin</source> + <target>%lld Nachrichten wurden vom Administrator blockiert</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld messages marked deleted" xml:space="preserve"> <source>%lld messages marked deleted</source> <target>%lld Nachrichten als gelöscht markiert</target> @@ -237,11 +265,6 @@ <target>%lld neue Sprachen für die Bedienoberfläche</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> - <source>%lld second(s)</source> - <target>%lld Sekunde(n)</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="%lld seconds" xml:space="preserve"> <source>%lld seconds</source> <target>%lld Sekunden</target> @@ -292,11 +315,6 @@ <target>%u übersprungene Nachrichten.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> - <source>(</source> - <target>(</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="(new)" xml:space="preserve"> <source>(new)</source> <target>(Neu)</target> @@ -307,34 +325,29 @@ <target>(Dieses Gerät hat v%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve"> - <source>)</source> - <target>)</target> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target>**Kontakt hinzufügen**: Um einen neuen Einladungslink zu erstellen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target>**Fügen Sie einen neuen Kontakt hinzu**: Erzeugen Sie einen Einmal-QR-Code oder -Link für Ihren Kontakt.</target> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve"> + <source>**Create group**: to create a new group.</source> + <target>**Gruppe erstellen**: Um eine neue Gruppe zu erstellen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> - <source>**Create link / QR code** for your contact to use.</source> - <target>**Generieren Sie einen Einladungs-Link / QR code** für Ihren Kontakt.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Mehr Privatsphäre**: Es wird alle 20 Minuten auf neue Nachrichten geprüft. Nur Ihr Geräte-Token wird dem SimpleX-Chat-Server mitgeteilt, aber nicht wie viele Kontakte Sie haben oder welche Nachrichten Sie empfangen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Beste Privatsphäre**: Es wird kein SimpleX-Chat-Benachrichtigungs-Server genutzt, Nachrichten werden in periodischen Abständen im Hintergrund geprüft (dies hängt davon ab, wie häufig Sie die App nutzen).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <target>**Fügen Sie den von Ihrem Kontakt erhaltenen Link ein** oder öffnen Sie ihn im Browser und tippen Sie auf **In mobiler App öffnen**.</target> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> + <target>**Bitte beachten Sie**: Aus Sicherheitsgründen wird die Nachrichtenentschlüsselung Ihrer Verbindungen abgebrochen, wenn Sie die gleiche Datenbank auf zwei Geräten nutzen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> @@ -342,14 +355,14 @@ <target>**Bitte beachten Sie**: Das Passwort kann NICHT wiederhergestellt oder geändert werden, wenn Sie es vergessen haben oder verlieren.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Empfohlen**: Nur Ihr Geräte-Token und ihre Benachrichtigungen werden an den SimpleX-Chat-Benachrichtigungs-Server gesendet, aber weder der Nachrichteninhalt noch deren Größe oder von wem sie gesendet wurde.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target>**Scannen Sie den QR-Code**, um sich während einem persönlichen Treffen oder per Videoanruf mit Ihrem Kontakt zu verbinden.</target> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve"> + <source>**Scan / Paste link**: to connect via a link you received.</source> + <target>**Link scannen / einfügen**: Um eine Verbindung über den Link herzustellen, den Sie erhalten haben.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> @@ -357,6 +370,11 @@ <target>**Warnung**: Sofortige Push-Benachrichtigungen erfordern die Eingabe eines Passworts, welches in Ihrem Schlüsselbund gespeichert ist.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve"> + <source>**Warning**: the archive will be removed.</source> + <target>**Warnung**: Das Archiv wird gelöscht.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> <source>**e2e encrypted** audio call</source> <target>**E2E-verschlüsselter** Audioanruf</target> @@ -372,11 +390,6 @@ <target>\*fett*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=", " xml:space="preserve"> - <source>, </source> - <target>, </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). @@ -408,14 +421,9 @@ <source>- voice messages up to 5 minutes. - custom time to disappear. - editing history.</source> - <target>- Bis zu 5 Minuten lange Sprachnachrichten. -- Zeitdauer für verschwindende Nachrichten anpassen. -- Nachrichten-Historie bearbeiten.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="." xml:space="preserve"> - <source>.</source> - <target>.</target> + <target>- Bis zu 5 Minuten lange Sprachnachrichten +- Zeitdauer für verschwindende Nachrichten anpassen +- Nachrichtenverlauf bearbeiten</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="0 sec" xml:space="preserve"> @@ -430,8 +438,9 @@ </trans-unit> <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> - <target>täglich</target> - <note>time interval</note> + <target>Älter als ein Tag</target> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> @@ -445,19 +454,31 @@ </trans-unit> <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> - <target>monatlich</target> - <note>time interval</note> + <target>Älter als ein Monat</target> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> - <target>wöchentlich</target> - <note>time interval</note> + <target>Älter als eine Woche</target> + <note>delete after time +time interval</note> + </trans-unit> + <trans-unit id="1 year" xml:space="preserve"> + <source>1 year</source> + <target>Älter als ein Jahr</target> + <note>delete after time</note> </trans-unit> <trans-unit id="1-time link" xml:space="preserve"> <source>1-time link</source> <target>Einmal-Link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> + <target>Ein Einmal-Link kann *nur mit einem Kontakt* genutzt werden - teilen Sie in nur persönlich oder über einen beliebigen Messenger.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="5 minutes" xml:space="preserve"> <source>5 minutes</source> <target>5 Minuten</target> @@ -473,11 +494,6 @@ <target>30 Sekunden</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve"> - <source>: </source> - <target>: </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> @@ -514,22 +530,17 @@ </trans-unit> <trans-unit id="Abort" xml:space="preserve"> <source>Abort</source> - <target>Abbrechen</target> + <target>Beenden</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Abort changing address" xml:space="preserve"> <source>Abort changing address</source> - <target>Wechsel der Adresse abbrechen</target> + <target>Wechsel der Empfängeradresse beenden</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Abort changing address?" xml:space="preserve"> <source>Abort changing address?</source> - <target>Wechsel der Adresse abbrechen?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve"> - <source>About SimpleX</source> - <target>Über SimpleX</target> + <target>Wechsel der Empfängeradresse beenden?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve"> @@ -537,21 +548,27 @@ <target>Über SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve"> - <source>About SimpleX address</source> - <target>Über die SimpleX-Adresse</target> + <trans-unit id="About operators" xml:space="preserve"> + <source>About operators</source> + <target>Über die Betreiber</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accent color" xml:space="preserve"> - <source>Accent color</source> - <target>Akzentfarbe</target> + <trans-unit id="Accent" xml:space="preserve"> + <source>Accent</source> + <target>Akzent</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept" xml:space="preserve"> <source>Accept</source> <target>Annehmen</target> <note>accept contact request via notification - accept incoming call via notification</note> +accept incoming call via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accept conditions" xml:space="preserve"> + <source>Accept conditions</source> + <target>Nutzungsbedingungen akzeptieren</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept connection request?" xml:space="preserve"> <source>Accept connection request?</source> @@ -566,16 +583,47 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Inkognito akzeptieren</target> - <note>accept contact request via notification</note> + <note>accept contact request via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve"> + <source>Accepted conditions</source> + <target>Akzeptierte Nutzungsbedingungen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve"> + <source>Acknowledged</source> + <target>Bestätigt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve"> + <source>Acknowledgement errors</source> + <target>Fehler bei der Bestätigung</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Active" xml:space="preserve"> + <source>Active</source> + <target>Aktiv</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve"> + <source>Active connections</source> + <target>Aktive Verbindungen</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> - <target>Fügen Sie die Adresse zu Ihrem Profil hinzu, damit Ihre Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet.</target> + <target>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.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> - <source>Add preset servers</source> - <target>Füge voreingestellte Server hinzu</target> + <trans-unit id="Add friends" xml:space="preserve"> + <source>Add friends</source> + <target>Freunde aufnehmen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve"> + <source>Add list</source> + <target>Liste hinzufügen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add profile" xml:space="preserve"> @@ -583,14 +631,19 @@ <target>Profil hinzufügen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> - <source>Add servers by scanning QR codes.</source> - <target>Fügen Sie Server durch Scannen der QR Codes hinzu.</target> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> + <target>Server hinzufügen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> - <target>Füge Server hinzu…</target> + <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> + <source>Add servers by scanning QR codes.</source> + <target>Server durch Scannen von QR Codes hinzufügen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add team members" xml:space="preserve"> + <source>Add team members</source> + <target>Team-Mitglieder aufnehmen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -598,11 +651,46 @@ <target>Einem anderen Gerät hinzufügen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add to list" xml:space="preserve"> + <source>Add to list</source> + <target>Zur Liste hinzufügen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> <target>Begrüßungsmeldung hinzufügen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve"> + <source>Add your team members to the conversations.</source> + <target>Nehmen Sie Team-Mitglieder in Ihre Unterhaltungen auf.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve"> + <source>Added media & file servers</source> + <target>Medien- und Dateiserver hinzugefügt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve"> + <source>Added message servers</source> + <target>Nachrichtenserver hinzugefügt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve"> + <source>Additional accent</source> + <target>Erste Akzentfarbe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve"> + <source>Additional accent 2</source> + <target>Zusätzlicher Akzent 2</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve"> + <source>Additional secondary</source> + <target>Zweite Akzentfarbe</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Address" xml:space="preserve"> <source>Address</source> <target>Adresse</target> @@ -610,7 +698,22 @@ </trans-unit> <trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve"> <source>Address change will be aborted. Old receiving address will be used.</source> - <target>Der Wechsel der Adresse wird abgebrochen. Die bisherige Adresse wird weiter verwendet.</target> + <target>Der Wechsel der Empfängeradresse wird beendet. Die bisherige Adresse wird weiter verwendet.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve"> + <source>Address or 1-time link?</source> + <target>Adress- oder Einmal-Link?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve"> + <source>Address settings</source> + <target>Adress-Einstellungen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve"> + <source>Admins can block a member for all.</source> + <target>Administratoren können ein Gruppenmitglied für Alle blockieren.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> @@ -623,6 +726,16 @@ <target>Erweiterte Netzwerkeinstellungen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve"> + <source>Advanced settings</source> + <target>Erweiterte Einstellungen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All" xml:space="preserve"> + <source>All</source> + <target>Alle</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All app data is deleted." xml:space="preserve"> <source>All app data is deleted.</source> <target>Werden die App-Daten komplett gelöscht.</target> @@ -630,27 +743,62 @@ </trans-unit> <trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve"> <source>All chats and messages will be deleted - this cannot be undone!</source> - <target>Alle Chats und Nachrichten werden gelöscht! Dies kann nicht rückgängig gemacht werden!</target> + <target>Es werden alle Chats und Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <target>Alle Chats werden von der Liste %@ entfernt und danach wird die Liste gelöscht.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> <target>Alle Daten werden gelöscht, sobald dieser eingegeben wird.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve"> + <source>All data is kept private on your device.</source> + <target>Alle Daten werden nur auf Ihrem Gerät gespeichert.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> <target>Alle Gruppenmitglieder bleiben verbunden.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <target>Alle Nachrichten und Dateien werden **Ende-zu-Ende verschlüsselt** versendet - in Direkt-Nachrichten mit Post-Quantum-Security.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone!</source> + <target>Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> - <target>Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht.</target> + <target>Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve"> <source>All new messages from %@ will be hidden!</source> - <target>Alle neuen Nachrichten von %@ werden verborgen!</target> + <target>Von %@ werden alle neuen Nachrichten ausgeblendet!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All profiles" xml:space="preserve"> + <source>All profiles</source> + <target>Alle Profile</target> + <note>profile dropdown</note> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve"> + <source>All reports will be archived for you.</source> + <target>Alle Meldungen werden für Sie archiviert.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All servers" xml:space="preserve"> + <source>All servers</source> + <target>Alle Server</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All your contacts will remain connected." xml:space="preserve"> @@ -663,6 +811,11 @@ <target>Alle Ihre Kontakte bleiben verbunden. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target>Alle Ihre Kontakte, Unterhaltungen und Dateien werden sicher verschlüsselt und in Daten-Paketen auf die konfigurierten XTFP-Relais hochgeladen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow" xml:space="preserve"> <source>Allow</source> <target>Erlauben</target> @@ -673,14 +826,24 @@ <target>Erlauben Sie Anrufe nur dann, wenn es Ihr Kontakt ebenfalls erlaubt.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> - <source>Allow disappearing messages only if your contact allows it to you.</source> - <target>Erlauben Sie verschwindende Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt.</target> + <trans-unit id="Allow calls?" xml:space="preserve"> + <source>Allow calls?</source> + <target>Anrufe erlauben?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <target>Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt.</target> + <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> + <source>Allow disappearing messages only if your contact allows it to you.</source> + <target>Erlauben Sie verschwindende Nachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow downgrade" xml:space="preserve"> + <source>Allow downgrade</source> + <target>Herabstufung erlauben</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target>Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt. (24 Stunden)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> @@ -703,9 +866,24 @@ <target>Das Senden von verschwindenden Nachrichten erlauben.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow to irreversibly delete sent messages.</source> - <target>Unwiederbringliches löschen von gesendeten Nachrichten erlauben.</target> + <trans-unit id="Allow sharing" xml:space="preserve"> + <source>Allow sharing</source> + <target>Teilen erlauben</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target>Unwiederbringliches löschen von gesendeten Nachrichten erlauben. (24 Stunden)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve"> + <source>Allow to report messsages to moderators.</source> + <target>Melden von Nachrichten an Moderatoren erlauben.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve"> + <source>Allow to send SimpleX links.</source> + <target>Das Senden von SimpleX-Links erlauben.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send files and media." xml:space="preserve"> @@ -738,9 +916,9 @@ <target>Erlaubt Ihren Kontakten Sie anzurufen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow your contacts to irreversibly delete sent messages.</source> - <target>Erlauben Sie Ihren Kontakten gesendete Nachrichten unwiederbringlich zu löschen.</target> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target>Erlauben Sie Ihren Kontakten gesendete Nachrichten unwiederbringlich zu löschen. (24 Stunden)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> @@ -768,6 +946,11 @@ <target>Sie sind bereits Mitglied der Gruppe!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve"> + <source>Always use private routing.</source> + <target>Sie nutzen immer privates Routing.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> <source>Always use relay</source> <target>Über ein Relais verbinden</target> @@ -778,21 +961,41 @@ <target>Es wurde ein leeres Chat-Profil mit dem eingegebenen Namen erstellt und die App öffnet wie gewohnt.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Another reason" xml:space="preserve"> + <source>Another reason</source> + <target>Anderer Grund</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> <target>Anruf annehmen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> + <target>Jeder kann seine eigenen Server aufsetzen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> <target>App Build: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App data migration" xml:space="preserve"> + <source>App data migration</source> + <target>App-Daten-Migration</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> <source>App encrypts new local files (except videos).</source> <target>Neue lokale Dateien (außer Video-Dateien) werden von der App verschlüsselt.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App group:" xml:space="preserve"> + <source>App group:</source> + <target>App-Gruppe:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>App-Icon</target> @@ -808,6 +1011,11 @@ <target>App-Zugangscode wurde durch den Selbstzerstörungs-Zugangscode ersetzt.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App session" xml:space="preserve"> + <source>App session</source> + <target>App-Sitzung</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App version" xml:space="preserve"> <source>App version</source> <target>App Version</target> @@ -823,6 +1031,66 @@ <target>Erscheinungsbild</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Apply" xml:space="preserve"> + <source>Apply</source> + <target>Anwenden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve"> + <source>Apply to</source> + <target>Anwenden auf</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve"> + <source>Archive</source> + <target>Archiv</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve"> + <source>Archive %lld reports?</source> + <target>Archiviere %lld Meldungen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve"> + <source>Archive all reports?</source> + <target>Alle Meldungen archivieren?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve"> + <source>Archive and upload</source> + <target>Archivieren und Hochladen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve"> + <source>Archive contacts to chat later.</source> + <target>Kontakte für spätere Chats archivieren.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve"> + <source>Archive report</source> + <target>Meldung archivieren</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve"> + <source>Archive report?</source> + <target>Meldung archivieren?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve"> + <source>Archive reports</source> + <target>Meldungen archivieren</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve"> + <source>Archived contacts</source> + <target>Archivierte Kontakte</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve"> + <source>Archiving database</source> + <target>Datenbank wird archiviert</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Attach" xml:space="preserve"> <source>Attach</source> <target>Anhängen</target> @@ -883,11 +1151,21 @@ <target>Bilder automatisch akzeptieren</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve"> + <source>Auto-accept settings</source> + <target>Einstellungen automatisch akzeptieren</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Back" xml:space="preserve"> <source>Back</source> <target>Zurück</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Background" xml:space="preserve"> + <source>Background</source> + <target>Hintergrund-Farbe</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Bad desktop address" xml:space="preserve"> <source>Bad desktop address</source> <target>Falsche Desktop-Adresse</target> @@ -903,21 +1181,71 @@ <target>Ungültiger Nachrichten-Hash</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better calls" xml:space="preserve"> + <source>Better calls</source> + <target>Verbesserte Anrufe</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better groups" xml:space="preserve"> <source>Better groups</source> <target>Bessere Gruppen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve"> + <source>Better groups performance</source> + <target>Bessere Leistung von Gruppen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve"> + <source>Better message dates.</source> + <target>Verbesserte Nachrichten-Datumsinformation</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better messages" xml:space="preserve"> <source>Better messages</source> <target>Verbesserungen bei Nachrichten</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better networking" xml:space="preserve"> + <source>Better networking</source> + <target>Kontrollieren Sie Ihr Netzwerk</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve"> + <source>Better notifications</source> + <target>Verbesserte Benachrichtigungen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve"> + <source>Better privacy and security</source> + <target>Bessere(r) Security und Datenschutz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve"> + <source>Better security ✅</source> + <target>Verbesserte Sicherheit ✅</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve"> + <source>Better user experience</source> + <target>Verbesserte Nutzer-Erfahrung</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Black" xml:space="preserve"> + <source>Black</source> + <target>Schwarz</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block" xml:space="preserve"> <source>Block</source> <target>Blockieren</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block for all" xml:space="preserve"> + <source>Block for all</source> + <target>Für Alle blockieren</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block group members" xml:space="preserve"> <source>Block group members</source> <target>Gruppenmitglieder blockieren</target> @@ -928,19 +1256,39 @@ <target>Mitglied blockieren</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve"> + <source>Block member for all?</source> + <target>Mitglied für Alle blockieren?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block member?" xml:space="preserve"> <source>Block member?</source> <target>Mitglied blockieren?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve"> + <source>Blocked by admin</source> + <target>wurde vom Administrator blockiert</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve"> + <source>Blur for better privacy.</source> + <target>Für bessere Privatsphäre verpixeln.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve"> + <source>Blur media</source> + <target>Medium verpixeln</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <source>Both you and your contact can add message reactions.</source> <target>Sowohl Sie, als auch Ihr Kontakt können Reaktionen auf Nachrichten geben.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> - <source>Both you and your contact can irreversibly delete sent messages.</source> - <target>Sowohl Ihr Kontakt, als auch Sie können gesendete Nachrichten unwiederbringlich löschen.</target> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target>Sowohl Ihr Kontakt, als auch Sie können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> @@ -963,11 +1311,35 @@ <target>Bulgarisch, Finnisch, Thailändisch und Ukrainisch - Dank der Nutzer und [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Business address" xml:space="preserve"> + <source>Business address</source> + <target>Geschäftliche Adresse</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve"> + <source>Business chats</source> + <target>Geschäftliche Chats</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve"> + <source>Businesses</source> + <target>Unternehmen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>Per Chat-Profil (Voreinstellung) oder [per Verbindung](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve"> + <source>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</source> + <target>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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> <source>Call already ended!</source> <target>Anruf ist bereits beendet!</target> @@ -978,9 +1350,24 @@ <target>Anrufe</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve"> - <source>Can't delete user profile!</source> - <target>Das Benutzerprofil kann nicht gelöscht werden!</target> + <trans-unit id="Calls prohibited!" xml:space="preserve"> + <source>Calls prohibited!</source> + <target>Anrufe nicht zugelassen!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve"> + <source>Camera not available</source> + <target>Kamera nicht verfügbar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve"> + <source>Can't call contact</source> + <target>Kontakt kann nicht angerufen werden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve"> + <source>Can't call member</source> + <target>Mitglied kann nicht angerufen werden</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contact!" xml:space="preserve"> @@ -993,9 +1380,20 @@ <target>Kontakte können nicht eingeladen werden!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve"> + <source>Can't message member</source> + <target>Mitglied kann nicht benachrichtigt werden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> <target>Abbrechen</target> + <note>alert action +alert button</note> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve"> + <source>Cancel migration</source> + <target>Migration abbrechen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> @@ -1003,9 +1401,24 @@ <target>Die App kann nicht auf den Schlüsselbund zugreifen, um das Datenbank-Passwort zu speichern</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Die Nachricht kann nicht weitergeleitet werden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> <target>Datei kann nicht empfangen werden</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <target>Kapazität überschritten - der Empfänger hat die zuvor gesendeten Nachrichten nicht empfangen.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve"> + <source>Cellular</source> + <target>Mobilfunknetz</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change" xml:space="preserve"> @@ -1013,6 +1426,16 @@ <target>Ändern</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve"> + <source>Change automatic message deletion?</source> + <target>Automatisches Löschen von Nachrichten ändern?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve"> + <source>Change chat profiles</source> + <target>Chat-Profile wechseln</target> + <note>authentication reason</note> + </trans-unit> <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> <target>Datenbank-Passwort ändern?</target> @@ -1057,11 +1480,26 @@ <source>Change self-destruct passcode</source> <target>Selbstzerstörungs-Zugangscode ändern</target> <note>authentication reason - set passcode view</note> +set passcode view</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> - <source>Chat archive</source> - <target>Datenbank Archiv</target> + <trans-unit id="Chat" xml:space="preserve"> + <source>Chat</source> + <target>Chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve"> + <source>Chat already exists</source> + <target>Chat besteht bereits</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve"> + <source>Chat already exists!</source> + <target>Chat besteht bereits!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve"> + <source>Chat colors</source> + <target>Chat-Farben</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat console" xml:space="preserve"> @@ -1079,6 +1517,11 @@ <target>Chat-Datenbank gelöscht</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve"> + <source>Chat database exported</source> + <target>Chat-Datenbank wurde exportiert</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> <target>Chat-Datenbank importiert</target> @@ -1094,26 +1537,81 @@ <target>Der Chat ist beendet</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <target>Der Chat ist angehalten. Wenn Sie diese Datenbank bereits auf einem anderen Gerät genutzt haben, sollten Sie diese vor dem Starten des Chats wieder zurückspielen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve"> + <source>Chat list</source> + <target>Chat-Liste</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve"> + <source>Chat migrated!</source> + <target>Chat wurde migriert!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> <target>Chat-Präferenzen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve"> + <source>Chat preferences were changed.</source> + <target>Die Chat-Präferenzen wurden geändert.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve"> + <source>Chat profile</source> + <target>Benutzerprofil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve"> + <source>Chat theme</source> + <target>Chat-Design</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <target>Der Chat wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <target>Der Chat wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> <target>Chats</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve"> + <source>Check messages every 20 min.</source> + <target>Alle 20min Nachrichten überprüfen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve"> + <source>Check messages when allowed.</source> + <target>Wenn es erlaubt ist, Nachrichten überprüfen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> <target>Überprüfen Sie die Serveradresse und versuchen Sie es nochmal.</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> <target>Chinesische und spanische Bedienoberfläche</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <target>Wählen Sie auf dem neuen Gerät _Von einem anderen Gerät migrieren_ und scannen Sie den QR-Code.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> <target>Datei auswählen</target> @@ -1124,19 +1622,49 @@ <target>Aus dem Fotoalbum auswählen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve"> + <source>Chunks deleted</source> + <target>Daten-Pakete gelöscht</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve"> + <source>Chunks downloaded</source> + <target>Daten-Pakete heruntergeladen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve"> + <source>Chunks uploaded</source> + <target>Daten-Pakete hochgeladen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> - <target>Löschen</target> - <note>No comment provided by engineer.</note> + <target>Entfernen</target> + <note>swipe action</note> </trans-unit> <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> - <target>Chatinhalte löschen</target> + <target>Chat-Inhalte entfernen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Clear conversation?" xml:space="preserve"> <source>Clear conversation?</source> - <target>Unterhaltung löschen?</target> + <target>Chat-Inhalte entfernen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve"> + <source>Clear group?</source> + <target>Gruppe entfernen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve"> + <source>Clear or delete group?</source> + <target>Gruppe entfernen oder löschen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve"> + <source>Clear private notes?</source> + <target>Private Notizen entfernen?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Clear verification" xml:space="preserve"> @@ -1144,11 +1672,21 @@ <target>Überprüfung zurücknehmen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> - <source>Colors</source> - <target>Farben</target> + <trans-unit id="Color chats with the new themes." xml:space="preserve"> + <source>Color chats with the new themes.</source> + <target>Farbige Chats mit neuen Designs.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Color mode" xml:space="preserve"> + <source>Color mode</source> + <target>Farbvariante</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve"> + <source>Community guidelines violation</source> + <target>Verstoß gegen die Gemeinschaftsrichtlinien</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Compare file" xml:space="preserve"> <source>Compare file</source> <target>Datei vergleichen</target> @@ -1159,11 +1697,56 @@ <target>Vergleichen Sie die Sicherheitscodes mit Ihren Kontakten.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Completed" xml:space="preserve"> + <source>Completed</source> + <target>Abgeschlossen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve"> + <source>Conditions accepted on: %@.</source> + <target>Die Nutzungsbedingungen wurden akzeptiert am: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <target>Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <target>Die Nutzungsbedingungen der/des folgenden Betreiber(s) wurden schon akzeptiert: **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve"> + <source>Conditions of use</source> + <target>Nutzungsbedingungen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <target>Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve"> + <source>Conditions will be accepted on: %@.</source> + <target>Die Nutzungsbedingungen werden akzeptiert am: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <target>Die Nutzungsbedingungen der aktivierten Betreiber werden automatisch akzeptiert am: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> <target>ICE-Server konfigurieren</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Configure server operators" xml:space="preserve"> + <source>Configure server operators</source> + <target>Server-Betreiber konfigurieren</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> <target>Bestätigen</target> @@ -1174,11 +1757,26 @@ <target>Zugangscode bestätigen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve"> + <source>Confirm contact deletion?</source> + <target>Löschen des Kontakts bestätigen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm database upgrades" xml:space="preserve"> <source>Confirm database upgrades</source> <target>Datenbank-Aktualisierungen bestätigen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve"> + <source>Confirm files from unknown servers.</source> + <target>Dateien von unbekannten Servern bestätigen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve"> + <source>Confirm network settings</source> + <target>Bestätigen Sie die Netzwerkeinstellungen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> <target>Neues Passwort bestätigen…</target> @@ -1189,6 +1787,21 @@ <target>Passwort bestätigen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <target>Bitte bestätigen Sie für die Migration, dass Sie sich an Ihr Datenbank-Passwort erinnern.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve"> + <source>Confirm upload</source> + <target>Hochladen bestätigen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve"> + <source>Confirmed</source> + <target>Bestätigt</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> <target>Verbinden</target> @@ -1209,6 +1822,11 @@ <target>Mit dem Desktop verbinden</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve"> + <source>Connect to your friends faster.</source> + <target>Schneller mit Ihren Freunden verbinden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connect to yourself?" xml:space="preserve"> <source>Connect to yourself?</source> <target>Mit Ihnen selbst verbinden?</target> @@ -1217,7 +1835,7 @@ <trans-unit id="Connect to yourself? This is your own SimpleX address!" xml:space="preserve"> <source>Connect to yourself? This is your own SimpleX address!</source> - <target>Mit Ihnen selbst verbinden? + <target>Sich mit Ihnen selbst verbinden? Das ist Ihre eigene SimpleX-Adresse!</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1238,11 +1856,6 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Über einen Link verbinden</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve"> - <source>Connect via link / QR code</source> - <target>Über einen Link / QR-Code verbinden</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connect via one-time link" xml:space="preserve"> <source>Connect via one-time link</source> <target>Über einen Einmal-Link verbinden</target> @@ -1253,16 +1866,31 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Mit %@ verbinden</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected" xml:space="preserve"> + <source>Connected</source> + <target>Verbunden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected desktop" xml:space="preserve"> <source>Connected desktop</source> <target>Verbundener Desktop</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve"> + <source>Connected servers</source> + <target>Verbundene Server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected to desktop" xml:space="preserve"> <source>Connected to desktop</source> <target>Mit dem Desktop verbunden</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting" xml:space="preserve"> + <source>Connecting</source> + <target>Verbinden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> <source>Connecting to server…</source> <target>Mit dem Server verbinden…</target> @@ -1273,6 +1901,11 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Mit dem Server verbinden… (Fehler: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve"> + <source>Connecting to contact, please wait or check later!</source> + <target>Verbinde mit Kontakt, bitte warten oder später erneut überprüfen!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting to desktop" xml:space="preserve"> <source>Connecting to desktop</source> <target>Mit dem Desktop verbinden</target> @@ -1283,6 +1916,16 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Verbindung</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve"> + <source>Connection and servers status.</source> + <target>Verbindungs- und Server-Status.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve"> + <source>Connection blocked</source> + <target>Verbindung blockiert</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> <target>Verbindungsfehler</target> @@ -1293,11 +1936,38 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Verbindungsfehler (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve"> + <source>Connection is blocked by server operator: +%@</source> + <target>Die Verbindung wurde vom Server-Betreiber blockiert: +%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve"> + <source>Connection not ready.</source> + <target>Verbindung noch nicht bereit.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve"> + <source>Connection notifications</source> + <target>Verbindungsbenachrichtigungen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Verbindungsanfrage wurde gesendet!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve"> + <source>Connection requires encryption renegotiation.</source> + <target>Die Verbindung erfordert eine Neuverhandlung der Verschlüsselung.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve"> + <source>Connection security</source> + <target>Verbindungs-Sicherheit</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection terminated" xml:space="preserve"> <source>Connection terminated</source> <target>Verbindung beendet</target> @@ -1308,6 +1978,16 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Verbindungszeitüberschreitung</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve"> + <source>Connection with desktop stopped</source> + <target>Die Verbindung mit dem Desktop wurde gestoppt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve"> + <source>Connections</source> + <target>Verbindungen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact allows" xml:space="preserve"> <source>Contact allows</source> <target>Der Kontakt erlaubt</target> @@ -1318,6 +1998,11 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Der Kontakt ist bereits vorhanden</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact deleted!" xml:space="preserve"> + <source>Contact deleted!</source> + <target>Kontakt gelöscht!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> <target>Kontakt verborgen:</target> @@ -1328,9 +2013,9 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Mit Ihrem Kontakt verbunden</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve"> - <source>Contact is not connected yet!</source> - <target>Ihr Kontakt ist noch nicht verbunden!</target> + <trans-unit id="Contact is deleted." xml:space="preserve"> + <source>Contact is deleted.</source> + <target>Kontakt wurde gelöscht.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact name" xml:space="preserve"> @@ -1343,6 +2028,11 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Kontakt-Präferenzen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve"> + <source>Contact will be deleted - this cannot be undone!</source> + <target>Kontakt wird gelöscht. Dies kann nicht rückgängig gemacht werden!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> <target>Kontakte</target> @@ -1353,21 +2043,41 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Ihre Kontakte können Nachrichten zum Löschen markieren. Sie können diese Nachrichten trotzdem anschauen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve"> + <source>Content violates conditions of use</source> + <target>Inhalt verletzt Nutzungsbedingungen</target> + <note>blocking reason</note> + </trans-unit> <trans-unit id="Continue" xml:space="preserve"> <source>Continue</source> <target>Weiter</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve"> + <source>Conversation deleted!</source> + <target>Chat-Inhalte entfernt!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> <target>Kopieren</target> - <note>chat item action</note> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve"> + <source>Copy error</source> + <target>Fehlermeldung kopieren</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> <target>Core Version: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Corner" xml:space="preserve"> + <source>Corner</source> + <target>Abrundung Ecken</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Correct name to %@?" xml:space="preserve"> <source>Correct name to %@?</source> <target>Richtiger Name für %@?</target> @@ -1378,6 +2088,11 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Erstellen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve"> + <source>Create 1-time link</source> + <target>Einmal-Link erstellen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> <target>SimpleX-Adresse erstellen</target> @@ -1388,11 +2103,6 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Erstellen Sie eine Gruppe mit einem zufälligen Profil.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> - <source>Create an address to let people connect with you.</source> - <target>Erstellen Sie eine Adresse, damit sich Personen mit Ihnen verbinden können.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create file" xml:space="preserve"> <source>Create file</source> <target>Datei erstellen</target> @@ -1413,16 +2123,16 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Link erzeugen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create list" xml:space="preserve"> + <source>Create list</source> + <target>Liste erstellen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> <target>Neues Profil in der [Desktop-App] erstellen (https://simplex.chat/downloads/). 💻</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve"> - <source>Create one-time invitation link</source> - <target>Einmal-Einladungslink erstellen</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create profile" xml:space="preserve"> <source>Create profile</source> <target>Profil erstellen</target> @@ -1443,9 +2153,29 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Erstellen Sie Ihr Profil</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve"> - <source>Created on %@</source> - <target>Erstellt am %@</target> + <trans-unit id="Created" xml:space="preserve"> + <source>Created</source> + <target>Erstellt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve"> + <source>Created at</source> + <target>Erstellt um</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve"> + <source>Created at: %@</source> + <target>Erstellt um: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve"> + <source>Creating archive link</source> + <target>Archiv-Link erzeugen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve"> + <source>Creating link…</source> + <target>Link wird erstellt…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current Passcode" xml:space="preserve"> @@ -1453,11 +2183,21 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Aktueller Zugangscode</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <target>Der Text der aktuellen Nutzungsbedingungen konnte nicht geladen werden. Sie können die Nutzungsbedingungen unter diesem Link einsehen:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> <target>Aktuelles Passwort…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current profile" xml:space="preserve"> + <source>Current profile</source> + <target>Aktuelles Profil</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> <target>Die derzeit maximal unterstützte Dateigröße beträgt %@.</target> @@ -1468,11 +2208,26 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Zeit anpassen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve"> + <source>Customizable message shape.</source> + <target>Anpassbares Format des Nachrichtenfelds</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve"> + <source>Customize theme</source> + <target>Design anpassen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> <target>Dunkel</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve"> + <source>Dark mode colors</source> + <target>Farben für die dunkle Variante</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> <target>Datenbank-ID</target> @@ -1571,6 +2326,11 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Die Datenbank wird beim nächsten Start der App migriert</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Debug delivery" xml:space="preserve"> + <source>Debug delivery</source> + <target>Debugging-Zustellung</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> <target>Dezentral</target> @@ -1584,18 +2344,19 @@ Das ist Ihr eigener Einmal-Link!</target> <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> <target>Löschen</target> - <note>chat item action</note> + <note>alert action +swipe action</note> + </trans-unit> + <trans-unit id="Delete %lld messages of members?" xml:space="preserve"> + <source>Delete %lld messages of members?</source> + <target>%lld Nachrichten der Mitglieder löschen?</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete %lld messages?" xml:space="preserve"> <source>Delete %lld messages?</source> <target>%lld Nachrichten löschen?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve"> - <source>Delete Contact</source> - <target>Kontakt löschen</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Delete address" xml:space="preserve"> <source>Delete address</source> <target>Adresse löschen</target> @@ -1621,14 +2382,14 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Kontakt löschen und benachrichtigen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve"> - <source>Delete archive</source> - <target>Archiv löschen</target> + <trans-unit id="Delete chat" xml:space="preserve"> + <source>Delete chat</source> + <target>Chat löschen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve"> - <source>Delete chat archive?</source> - <target>Chat Archiv löschen?</target> + <trans-unit id="Delete chat messages from your device." xml:space="preserve"> + <source>Delete chat messages from your device.</source> + <target>Chat-Nachrichten von Ihrem Gerät löschen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile" xml:space="preserve"> @@ -1641,6 +2402,11 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Chat-Profil löschen?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete chat?" xml:space="preserve"> + <source>Delete chat?</source> + <target>Chat löschen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> <source>Delete connection</source> <target>Verbindung löschen</target> @@ -1651,11 +2417,9 @@ Das ist Ihr eigener Einmal-Link!</target> <target>Kontakt löschen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact? This cannot be undone!" xml:space="preserve"> - <source>Delete contact? -This cannot be undone!</source> - <target>Kontakt löschen? -Das kann nicht rückgängig gemacht werden!</target> + <trans-unit id="Delete contact?" xml:space="preserve"> + <source>Delete contact?</source> + <target>Kontakt löschen?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete database" xml:space="preserve"> @@ -1663,6 +2427,11 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Datenbank löschen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete database from this device" xml:space="preserve"> + <source>Delete database from this device</source> + <target>Datenbank auf diesem Gerät löschen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete file" xml:space="preserve"> <source>Delete file</source> <target>Datei löschen</target> @@ -1713,6 +2482,11 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Link löschen?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete list?" xml:space="preserve"> + <source>Delete list?</source> + <target>Liste löschen?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> <target>Nachricht des Mitglieds löschen?</target> @@ -1726,11 +2500,11 @@ Das kann nicht rückgängig gemacht werden!</target> <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> <target>Nachrichten löschen</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> - <target>Löschen der Nachrichten</target> + <target>Nachrichten löschen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete old database" xml:space="preserve"> @@ -1743,14 +2517,14 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Alte Datenbank löschen?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve"> - <source>Delete pending connection</source> - <target>Ausstehende Verbindung löschen</target> + <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> + <source>Delete or moderate up to 200 messages.</source> + <target>Bis zu 200 Nachrichten löschen oder moderieren</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection?" xml:space="preserve"> <source>Delete pending connection?</source> - <target>Die ausstehende Verbindung löschen?</target> + <target>Ausstehende Verbindung löschen?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete profile" xml:space="preserve"> @@ -1763,11 +2537,31 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Lösche Warteschlange</target> <note>server test step</note> </trans-unit> + <trans-unit id="Delete report" xml:space="preserve"> + <source>Delete report</source> + <target>Meldung löschen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete up to 20 messages at once." xml:space="preserve"> + <source>Delete up to 20 messages at once.</source> + <target>Löschen Sie bis zu 20 Nachrichten auf einmal.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> <target>Benutzerprofil löschen?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete without notification" xml:space="preserve"> + <source>Delete without notification</source> + <target>Ohne Benachrichtigung löschen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve"> + <source>Deleted</source> + <target>Gelöscht</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Deleted at" xml:space="preserve"> <source>Deleted at</source> <target>Gelöscht um</target> @@ -1778,6 +2572,16 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Gelöscht um: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve"> + <source>Deletion errors</source> + <target>Fehler beim Löschen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivered even when Apple drops them." xml:space="preserve"> + <source>Delivered even when Apple drops them.</source> + <target>Auslieferung, selbst wenn Apple sie löscht.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery" xml:space="preserve"> <source>Delivery</source> <target>Zustellung</target> @@ -1813,11 +2617,41 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Desktop-Geräte</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve"> + <source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source> + <target>Adresse des Zielservers von %@ ist nicht kompatibel mit den Einstellungen des Weiterleitungsservers %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server error: %@" xml:space="preserve"> + <source>Destination server error: %@</source> + <target>Zielserver-Fehler: %@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve"> + <source>Destination server version of %@ is incompatible with forwarding server %@.</source> + <target>Die Version des Zielservers %@ ist nicht kompatibel mit dem Weiterleitungsserver %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Detailed statistics" xml:space="preserve"> + <source>Detailed statistics</source> + <target>Detaillierte Statistiken</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Details" xml:space="preserve"> + <source>Details</source> + <target>Details</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Develop" xml:space="preserve"> <source>Develop</source> <target>Entwicklung</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Developer options" xml:space="preserve"> + <source>Developer options</source> + <target>Optionen für Entwickler</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> <target>Entwicklertools</target> @@ -1848,8 +2682,13 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Direkte Nachrichten</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve"> + <source>Direct messages between members are prohibited in this chat.</source> + <target>In diesem Chat sind Direktnachrichten zwischen Mitgliedern nicht erlaubt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> <target>In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1863,11 +2702,26 @@ Das kann nicht rückgängig gemacht werden!</target> <target>SimpleX-Sperre deaktivieren</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Disable automatic message deletion?" xml:space="preserve"> + <source>Disable automatic message deletion?</source> + <target>Automatisches Löschen von Nachrichten deaktivieren?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Disable delete messages" xml:space="preserve"> + <source>Disable delete messages</source> + <target>Löschen von Nachrichten deaktivieren</target> + <note>alert button</note> + </trans-unit> <trans-unit id="Disable for all" xml:space="preserve"> <source>Disable for all</source> <target>Für Alle deaktivieren</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Disabled" xml:space="preserve"> + <source>Disabled</source> + <target>Deaktiviert</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> <source>Disappearing message</source> <target>Verschwindende Nachricht</target> @@ -1875,7 +2729,7 @@ Das kann nicht rückgängig gemacht werden!</target> </trans-unit> <trans-unit id="Disappearing messages" xml:space="preserve"> <source>Disappearing messages</source> - <target>verschwindende Nachrichten</target> + <target>Verschwindende Nachrichten</target> <note>chat feature</note> </trans-unit> <trans-unit id="Disappearing messages are prohibited in this chat." xml:space="preserve"> @@ -1883,8 +2737,8 @@ Das kann nicht rückgängig gemacht werden!</target> <target>In diesem Chat sind verschwindende Nachrichten nicht erlaubt.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <target>In dieser Gruppe sind verschwindende Nachrichten nicht erlaubt.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1918,9 +2772,19 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Lokales Netzwerk durchsuchen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve"> + <source>Do NOT send messages directly, even if your or destination server does not support private routing.</source> + <target>Nachrichten werden nicht direkt versendet, selbst wenn Ihr oder der Zielserver kein privates Routing unterstützt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> <source>Do NOT use SimpleX for emergency calls.</source> - <target>Nutzen Sie SimpleX nicht für Notrufe.</target> + <target>SimpleX NICHT für Notrufe nutzen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do NOT use private routing." xml:space="preserve"> + <source>Do NOT use private routing.</source> + <target>Sie nutzen KEIN privates Routing.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Do it later" xml:space="preserve"> @@ -1928,6 +2792,21 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Später wiederholen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do not send history to new members." xml:space="preserve"> + <source>Do not send history to new members.</source> + <target>Den Nachrichtenverlauf nicht an neue Mitglieder senden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not use credentials with proxy." xml:space="preserve"> + <source>Do not use credentials with proxy.</source> + <target>Verwenden Sie keine Anmeldeinformationen mit einem Proxy.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Documents:" xml:space="preserve"> + <source>Documents:</source> + <target>Dokumente:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> <target>Keine Adresse erstellt</target> @@ -1938,21 +2817,72 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Nicht aktivieren</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Don't miss important messages." xml:space="preserve"> + <source>Don't miss important messages.</source> + <target>Verpassen Sie keine wichtigen Nachrichten.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> <target>Nicht nochmals anzeigen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Done" xml:space="preserve"> + <source>Done</source> + <target>Fertig</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Downgrade and open chat" xml:space="preserve"> <source>Downgrade and open chat</source> <target>Datenbank herabstufen und den Chat öffnen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Download" xml:space="preserve"> + <source>Download</source> + <target>Herunterladen</target> + <note>alert button +chat item action</note> + </trans-unit> + <trans-unit id="Download errors" xml:space="preserve"> + <source>Download errors</source> + <target>Fehler beim Herunterladen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download failed" xml:space="preserve"> + <source>Download failed</source> + <target>Herunterladen fehlgeschlagen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Download file" xml:space="preserve"> <source>Download file</source> <target>Datei herunterladen</target> <note>server test step</note> </trans-unit> + <trans-unit id="Download files" xml:space="preserve"> + <source>Download files</source> + <target>Dateien herunterladen</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Downloaded" xml:space="preserve"> + <source>Downloaded</source> + <target>Heruntergeladen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloaded files" xml:space="preserve"> + <source>Downloaded files</source> + <target>Heruntergeladene Dateien</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading archive" xml:space="preserve"> + <source>Downloading archive</source> + <target>Archiv wird heruntergeladen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading link details" xml:space="preserve"> + <source>Downloading link details</source> + <target>Link-Details werden heruntergeladen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> <target>Doppelter Anzeigename!</target> @@ -1963,6 +2893,11 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Dauer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="E2E encrypted notifications." xml:space="preserve"> + <source>E2E encrypted notifications.</source> + <target>E2E-verschlüsselte Benachrichtigungen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> <target>Bearbeiten</target> @@ -1983,6 +2918,11 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Aktivieren (vorgenommene Einstellungen bleiben erhalten)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve"> + <source>Enable Flux in Network & servers settings for better metadata privacy.</source> + <target>Für einen besseren Metadatenschutz Flux in den Netzwerk- und Servereinstellungen aktivieren.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> <target>SimpleX-Sperre aktivieren</target> @@ -1996,6 +2936,11 @@ Das kann nicht rückgängig gemacht werden!</target> <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> <target>Automatisches Löschen von Nachrichten aktivieren?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Enable camera access" xml:space="preserve"> + <source>Enable camera access</source> + <target>Kamera-Zugriff aktivieren</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable for all" xml:space="preserve"> @@ -2003,6 +2948,11 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Für Alle aktivieren</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve"> + <source>Enable in direct chats (BETA)!</source> + <target>Kann in direkten Chats aktiviert werden (BETA)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> <target>Sofortige Benachrichtigungen aktivieren?</target> @@ -2033,6 +2983,16 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Selbstzerstörungs-Zugangscode aktivieren</target> <note>set passcode view</note> </trans-unit> + <trans-unit id="Enabled" xml:space="preserve"> + <source>Enabled</source> + <target>Aktiviert</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve"> + <source>Enabled for</source> + <target>Aktiviert für</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> <target>Verschlüsseln</target> @@ -2063,6 +3023,11 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Verschlüsselte Nachricht oder ein anderes Ereignis</target> <note>notification</note> </trans-unit> + <trans-unit id="Encrypted message: app is stopped" xml:space="preserve"> + <source>Encrypted message: app is stopped</source> + <target>Verschlüsselte Nachricht: Die App ist angehalten</target> + <note>notification</note> + </trans-unit> <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> <target>Verschlüsselte Nachricht: Datenbankfehler</target> @@ -2098,6 +3063,11 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Neuverhandlung der Verschlüsselung fehlgeschlagen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encryption renegotiation in progress." xml:space="preserve"> + <source>Encryption renegotiation in progress.</source> + <target>Die Neuverhandlung der Verschlüsselung läuft.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter Passcode" xml:space="preserve"> <source>Enter Passcode</source> <target>Zugangscode eingeben</target> @@ -2113,6 +3083,11 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Geben Sie den Gruppennamen ein…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve"> + <source>Enter passphrase</source> + <target>Passwort eingeben</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter passphrase…" xml:space="preserve"> <source>Enter passphrase…</source> <target>Passwort eingeben…</target> @@ -2155,27 +3130,37 @@ Das kann nicht rückgängig gemacht werden!</target> </trans-unit> <trans-unit id="Error aborting address change" xml:space="preserve"> <source>Error aborting address change</source> - <target>Fehler beim Abbrechen des Adresswechsels</target> + <target>Fehler beim Beenden des Adresswechsels</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error accepting conditions" xml:space="preserve"> + <source>Error accepting conditions</source> + <target>Fehler beim Akzeptieren der Nutzungsbedingungen</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> <target>Fehler beim Annehmen der Kontaktanfrage</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve"> - <source>Error accessing database file</source> - <target>Fehler beim Zugriff auf die Datenbankdatei</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> <target>Fehler beim Hinzufügen von Mitgliedern</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error adding server" xml:space="preserve"> + <source>Error adding server</source> + <target>Fehler beim Hinzufügen des Servers</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> - <target>Fehler beim Wechseln der Adresse</target> + <target>Fehler beim Wechseln der Empfängeradresse</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error changing connection profile" xml:space="preserve"> + <source>Error changing connection profile</source> + <target>Fehler beim Wechseln des Verbindungs-Profils</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error changing role" xml:space="preserve"> @@ -2188,6 +3173,21 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Fehler beim Ändern der Einstellung</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing to incognito!" xml:space="preserve"> + <source>Error changing to incognito!</source> + <target>Fehler beim Wechseln zum Inkognito-Profil!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error checking token status" xml:space="preserve"> + <source>Error checking token status</source> + <target>Fehler beim Überprüfen des Token-Status</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve"> + <source>Error connecting to forwarding server %@. Please try later.</source> + <target>Fehler beim Verbinden mit dem Weiterleitungsserver %@. Bitte versuchen Sie es später erneut.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> <target>Fehler beim Erstellen der Adresse</target> @@ -2203,16 +3203,31 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Fehler beim Erzeugen des Gruppen-Links</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating list" xml:space="preserve"> + <source>Error creating list</source> + <target>Fehler beim Erstellen der Liste</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error creating member contact" xml:space="preserve"> <source>Error creating member contact</source> <target>Fehler beim Anlegen eines Mitglied-Kontaktes</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating message" xml:space="preserve"> + <source>Error creating message</source> + <target>Fehler beim Erstellen der Nachricht</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>Fehler beim Erstellen des Profils!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating report" xml:space="preserve"> + <source>Error creating report</source> + <target>Fehler beim Erstellen der Meldung</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error decrypting file" xml:space="preserve"> <source>Error decrypting file</source> <target>Fehler beim Entschlüsseln der Datei</target> @@ -2233,11 +3248,6 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Fehler beim Löschen der Verbindung</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve"> - <source>Error deleting contact</source> - <target>Fehler beim Löschen des Kontakts</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> <target>Fehler beim Löschen der Datenbank</target> @@ -2258,6 +3268,11 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Fehler beim Löschen des Benutzerprofils</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error downloading the archive" xml:space="preserve"> + <source>Error downloading the archive</source> + <target>Fehler beim Herunterladen des Archivs</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> <source>Error enabling delivery receipts!</source> <target>Fehler beim Aktivieren von Empfangsbestätigungen!</target> @@ -2278,6 +3293,11 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Fehler beim Exportieren der Chat-Datenbank</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error exporting theme: %@" xml:space="preserve"> + <source>Error exporting theme: %@</source> + <target>Fehler beim Exportieren des Designs: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> <target>Fehler beim Importieren der Chat-Datenbank</target> @@ -2288,24 +3308,54 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Fehler beim Beitritt zur Gruppe</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve"> - <source>Error loading %@ servers</source> - <target>Fehler beim Laden von %@ Servern</target> + <trans-unit id="Error loading servers" xml:space="preserve"> + <source>Error loading servers</source> + <target>Fehler beim Laden der Server</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error migrating settings" xml:space="preserve"> + <source>Error migrating settings</source> + <target>Fehler beim Migrieren der Einstellungen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error opening chat" xml:space="preserve"> + <source>Error opening chat</source> + <target>Fehler beim Öffnen des Chats</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> - <target>Fehler beim Empfangen der Datei</target> + <target>Fehler beim Herunterladen der Datei</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error reconnecting server" xml:space="preserve"> + <source>Error reconnecting server</source> + <target>Fehler beim Wiederherstellen der Verbindung zum Server</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error reconnecting servers" xml:space="preserve"> + <source>Error reconnecting servers</source> + <target>Fehler beim Wiederherstellen der Verbindungen zu den Servern</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error registering for notifications" xml:space="preserve"> + <source>Error registering for notifications</source> + <target>Fehler beim Registrieren für Benachrichtigungen</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> <target>Fehler beim Entfernen des Mitglieds</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve"> - <source>Error saving %@ servers</source> - <target>Fehler beim Speichern der %@-Server</target> + <trans-unit id="Error reordering lists" xml:space="preserve"> + <source>Error reordering lists</source> + <target>Fehler beim Umsortieren der Listen</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error resetting statistics" xml:space="preserve"> + <source>Error resetting statistics</source> + <target>Fehler beim Zurücksetzen der Statistiken</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving ICE servers" xml:space="preserve"> @@ -2313,6 +3363,11 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Fehler beim Speichern der ICE-Server</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving chat list" xml:space="preserve"> + <source>Error saving chat list</source> + <target>Fehler beim Speichern der Chat-Liste</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> <target>Fehler beim Speichern des Gruppenprofils</target> @@ -2328,11 +3383,26 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Fehler beim Speichern des Passworts in den Schlüsselbund</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving servers" xml:space="preserve"> + <source>Error saving servers</source> + <target>Fehler beim Speichern der Server</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error saving settings" xml:space="preserve"> + <source>Error saving settings</source> + <target>Fehler beim Abspeichern der Einstellungen</target> + <note>when migrating</note> + </trans-unit> <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> <target>Fehler beim Speichern des Benutzer-Passworts</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error scanning code: %@" xml:space="preserve"> + <source>Error scanning code: %@</source> + <target>Fehler beim Scannen des Codes: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending email" xml:space="preserve"> <source>Error sending email</source> <target>Fehler beim Senden der eMail</target> @@ -2363,16 +3433,26 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Fehler beim Beenden des Chats</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error switching profile" xml:space="preserve"> + <source>Error switching profile</source> + <target>Fehler beim Wechseln des Profils</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> <target>Fehler beim Umschalten des Profils!</target> - <note>No comment provided by engineer.</note> + <note>alertTitle</note> </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> <target>Fehler beim Synchronisieren der Verbindung</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error testing server connection" xml:space="preserve"> + <source>Error testing server connection</source> + <target>Fehler beim Testen der Server-Verbindung</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> <target>Fehler beim Aktualisieren des Gruppen-Links</target> @@ -2383,6 +3463,11 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Fehler beim Aktualisieren der Nachricht</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error updating server" xml:space="preserve"> + <source>Error updating server</source> + <target>Fehler beim Aktualisieren des Servers</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> <target>Fehler beim Aktualisieren der Einstellungen</target> @@ -2393,6 +3478,16 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Fehler beim Aktualisieren der Benutzer-Privatsphäre</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error uploading the archive" xml:space="preserve"> + <source>Error uploading the archive</source> + <target>Fehler beim Hochladen des Archivs</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error verifying passphrase:" xml:space="preserve"> + <source>Error verifying passphrase:</source> + <target>Fehler bei der Überprüfung des Passworts:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error: " xml:space="preserve"> <source>Error: </source> <target>Fehler: </target> @@ -2401,7 +3496,9 @@ Das kann nicht rückgängig gemacht werden!</target> <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> <target>Fehler: %@</target> - <note>No comment provided by engineer.</note> + <note>alert message +file error text +snd error text</note> </trans-unit> <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> @@ -2413,6 +3510,16 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Fehler: Keine Datenbankdatei</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Errors" xml:space="preserve"> + <source>Errors</source> + <target>Fehler</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors in servers configuration." xml:space="preserve"> + <source>Errors in servers configuration.</source> + <target>Fehler in der Server-Konfiguration.</target> + <note>servers error</note> + </trans-unit> <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> <source>Even when disabled in the conversation.</source> <target>Auch wenn sie im Chat deaktiviert sind.</target> @@ -2428,6 +3535,11 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Erweitern</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Expired" xml:space="preserve"> + <source>Expired</source> + <target>Abgelaufen</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> <target>Datenbank exportieren</target> @@ -2438,11 +3550,21 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Fehler beim Export:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Export theme" xml:space="preserve"> + <source>Export theme</source> + <target>Design exportieren</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> <target>Exportiertes Datenbankarchiv.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Exported file doesn't exist" xml:space="preserve"> + <source>Exported file doesn't exist</source> + <target>Die exportierte Datei ist nicht vorhanden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exporting database archive…" xml:space="preserve"> <source>Exporting database archive…</source> <target>Exportieren des Datenbank-Archivs…</target> @@ -2458,16 +3580,70 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Schnell und ohne warten auf den Absender, bis er online ist!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster deletion of groups." xml:space="preserve"> + <source>Faster deletion of groups.</source> + <target>Schnelleres löschen von Gruppen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Faster joining and more reliable messages." xml:space="preserve"> <source>Faster joining and more reliable messages.</source> <target>Schnellerer Gruppenbeitritt und zuverlässigere Nachrichtenzustellung.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster sending messages." xml:space="preserve"> + <source>Faster sending messages.</source> + <target>Schnelleres versenden von Nachrichten.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Favorite" xml:space="preserve"> <source>Favorite</source> <target>Favorit</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Favorites" xml:space="preserve"> + <source>Favorites</source> + <target>Favoriten</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Datei-Fehler</target> + <note>file error alert title</note> + </trans-unit> + <trans-unit id="File errors: %@" xml:space="preserve"> + <source>File errors: +%@</source> + <target>Datei-Fehler: +%@</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="File is blocked by server operator: %@." xml:space="preserve"> + <source>File is blocked by server operator: +%@.</source> + <target>Datei wurde vom Server-Betreiber blockiert: +%@.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve"> + <source>File not found - most likely file was deleted or cancelled.</source> + <target>Datei nicht gefunden - höchstwahrscheinlich wurde die Datei gelöscht oder der Transfer abgebrochen.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File server error: %@" xml:space="preserve"> + <source>File server error: %@</source> + <target>Datei-Server Fehler: %@</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File status" xml:space="preserve"> + <source>File status</source> + <target>Datei-Status</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File status: %@" xml:space="preserve"> + <source>File status: %@</source> + <target>Datei-Status: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="File will be deleted from servers." xml:space="preserve"> <source>File will be deleted from servers.</source> <target>Die Datei wird von den Servern gelöscht.</target> @@ -2475,12 +3651,12 @@ Das kann nicht rückgängig gemacht werden!</target> </trans-unit> <trans-unit id="File will be received when your contact completes uploading it." xml:space="preserve"> <source>File will be received when your contact completes uploading it.</source> - <target>Die Datei wird empfangen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist.</target> + <target>Die Datei wird heruntergeladen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="File will be received when your contact is online, please wait or check later!" xml:space="preserve"> <source>File will be received when your contact is online, please wait or check later!</source> - <target>Die Datei wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</target> + <target>Die Datei wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="File: %@" xml:space="preserve"> @@ -2488,6 +3664,11 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Datei: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files" xml:space="preserve"> + <source>Files</source> + <target>Dateien</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> <target>Dateien & Medien</target> @@ -2498,11 +3679,16 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Dateien und Medien</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> - <source>Files and media are prohibited in this group.</source> + <trans-unit id="Files and media are prohibited." xml:space="preserve"> + <source>Files and media are prohibited.</source> <target>In dieser Gruppe sind Dateien und Medien nicht erlaubt.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files and media not allowed" xml:space="preserve"> + <source>Files and media not allowed</source> + <target>Dateien und Medien sind nicht erlaubt</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files and media prohibited!" xml:space="preserve"> <source>Files and media prohibited!</source> <target>Dateien und Medien sind nicht erlaubt!</target> @@ -2513,6 +3699,16 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Nach ungelesenen und favorisierten Chats filtern.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Finalize migration" xml:space="preserve"> + <source>Finalize migration</source> + <target>Die Migration wird abgeschlossen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration on another device." xml:space="preserve"> + <source>Finalize migration on another device.</source> + <target>Die Migration auf dem anderen Gerät wird abgeschlossen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> <target>Endlich haben wir sie! 🚀</target> @@ -2553,11 +3749,115 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Reparatur wird vom Gruppenmitglied nicht unterstützt</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For all moderators" xml:space="preserve"> + <source>For all moderators</source> + <target>Für alle Moderatoren</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For chat profile %@:" xml:space="preserve"> + <source>For chat profile %@:</source> + <target>Für das Chat-Profil %@:</target> + <note>servers error</note> + </trans-unit> <trans-unit id="For console" xml:space="preserve"> <source>For console</source> <target>Für Konsole</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." xml:space="preserve"> + <source>For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</source> + <target>Wenn Ihr Kontakt beispielsweise Nachrichten über einen SimpleX-Chatserver empfängt, wird Ihre App diese über einen der Server von Flux versenden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For me" xml:space="preserve"> + <source>For me</source> + <target>Für mich</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For private routing" xml:space="preserve"> + <source>For private routing</source> + <target>Für privates Routing</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For social media" xml:space="preserve"> + <source>For social media</source> + <target>Für soziale Medien</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward" xml:space="preserve"> + <source>Forward</source> + <target>Weiterleiten</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Forward %d message(s)?" xml:space="preserve"> + <source>Forward %d message(s)?</source> + <target>%d Nachricht(en) weiterleiten?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Forward and save messages" xml:space="preserve"> + <source>Forward and save messages</source> + <target>Nachrichten weiterleiten und speichern</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward messages" xml:space="preserve"> + <source>Forward messages</source> + <target>Nachrichten weiterleiten</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Forward messages without files?" xml:space="preserve"> + <source>Forward messages without files?</source> + <target>Nachrichten ohne Dateien weiterleiten?</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> + <source>Forward up to 20 messages at once.</source> + <target>Bis zu 20 Nachrichten auf einmal weiterleiten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded" xml:space="preserve"> + <source>Forwarded</source> + <target>Weitergeleitet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded from" xml:space="preserve"> + <source>Forwarded from</source> + <target>Weitergeleitet aus</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding %lld messages" xml:space="preserve"> + <source>Forwarding %lld messages</source> + <target>%lld Nachricht(en) wird/werden weitergeleitet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve"> + <source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source> + <target>Weiterleitungsserver %@ konnte sich nicht mit dem Zielserver %@ verbinden. Bitte versuchen Sie es später erneut.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server address is incompatible with network settings: %@.</source> + <target>Adresse des Weiterleitungsservers ist nicht kompatibel mit den Netzwerkeinstellungen: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server version is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server version is incompatible with network settings: %@.</source> + <target>Version des Weiterleitungsservers ist nicht kompatibel mit den Netzwerkeinstellungen: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Destination server error: %2$@</source> + <target>Weiterleitungsserver: %1$@ +Zielserver Fehler: %2$@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Error: %2$@</source> + <target>Weiterleitungsserver: %1$@ +Fehler: %2$@</target> + <note>snd error text</note> + </trans-unit> <trans-unit id="Found desktop" xml:space="preserve"> <source>Found desktop</source> <target>Gefundener Desktop</target> @@ -2578,11 +3878,6 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Vollständiger Name (optional)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full name:" xml:space="preserve"> - <source>Full name:</source> - <target>Vollständiger Name:</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Fully decentralized – visible only to members." xml:space="preserve"> <source>Fully decentralized – visible only to members.</source> <target>Vollständig dezentralisiert – nur für Mitglieder sichtbar.</target> @@ -2603,6 +3898,21 @@ Das kann nicht rückgängig gemacht werden!</target> <target>GIFs und Sticker</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Get notified when mentioned." xml:space="preserve"> + <source>Get notified when mentioned.</source> + <target>Bei Erwähnung benachrichtigt werden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Good afternoon!" xml:space="preserve"> + <source>Good afternoon!</source> + <target>Guten Nachmittag!</target> + <note>message preview</note> + </trans-unit> + <trans-unit id="Good morning!" xml:space="preserve"> + <source>Good morning!</source> + <target>Guten Morgen!</target> + <note>message preview</note> + </trans-unit> <trans-unit id="Group" xml:space="preserve"> <source>Group</source> <target>Gruppe</target> @@ -2658,36 +3968,6 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Gruppen-Links</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve"> - <source>Group members can add message reactions.</source> - <target>Gruppenmitglieder können eine Reaktion auf Nachrichten geben.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> - <source>Group members can irreversibly delete sent messages.</source> - <target>Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> - <target>Gruppenmitglieder können Direktnachrichten versenden.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> - <target>Gruppenmitglieder können verschwindende Nachrichten senden.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send files and media." xml:space="preserve"> - <source>Group members can send files and media.</source> - <target>Gruppenmitglieder können Dateien und Medien senden.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> - <target>Gruppenmitglieder können Sprachnachrichten versenden.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> <target>Grppennachricht:</target> @@ -2720,12 +4000,17 @@ Das kann nicht rückgängig gemacht werden!</target> </trans-unit> <trans-unit id="Group will be deleted for all members - this cannot be undone!" xml:space="preserve"> <source>Group will be deleted for all members - this cannot be undone!</source> - <target>Die Gruppe wird für alle Mitglieder gelöscht - dies kann nicht rückgängig gemacht werden!</target> + <target>Die Gruppe wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group will be deleted for you - this cannot be undone!" xml:space="preserve"> <source>Group will be deleted for you - this cannot be undone!</source> - <target>Die Gruppe wird für Sie gelöscht - dies kann nicht rückgängig gemacht werden!</target> + <target>Die Gruppe wird nur bei Ihnen gelöscht. Dies kann nicht rückgängig gemacht werden!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Groups" xml:space="preserve"> + <source>Groups</source> + <target>Gruppen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Help" xml:space="preserve"> @@ -2733,6 +4018,11 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Hilfe</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Help admins moderating their groups." xml:space="preserve"> + <source>Help admins moderating their groups.</source> + <target>Helfen Sie Administratoren bei der Moderation ihrer Gruppen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> <target>Verborgen</target> @@ -2770,7 +4060,12 @@ Das kann nicht rückgängig gemacht werden!</target> </trans-unit> <trans-unit id="History" xml:space="preserve"> <source>History</source> - <target>Vergangenheit</target> + <target>Nachrichtenverlauf</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="History is not sent to new members." xml:space="preserve"> + <source>History is not sent to new members.</source> + <target>Der Nachrichtenverlauf wird nicht an neue Gruppenmitglieder gesendet.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> @@ -2778,10 +4073,20 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Wie SimpleX funktioniert</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="How it affects privacy" xml:space="preserve"> + <source>How it affects privacy</source> + <target>Wie es die Privatsphäre beeinflusst</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it helps privacy" xml:space="preserve"> + <source>How it helps privacy</source> + <target>Wie es die Privatsphäre schützt</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> <target>Wie es funktioniert</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="How to" xml:space="preserve"> <source>How to</source> @@ -2798,21 +4103,26 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Wie Sie Ihre Server nutzen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Hungarian interface" xml:space="preserve"> + <source>Hungarian interface</source> + <target>Ungarische Bedienoberfläche</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> <target>ICE-Server (einer pro Zeile)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="IP address" xml:space="preserve"> + <source>IP address</source> + <target>IP-Adresse</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> <source>If you can't meet in person, show QR code in a video call, or share the link.</source> <target>Falls Sie sich nicht persönlich treffen können, zeigen Sie den QR-Code in einem Videoanruf oder teilen Sie den Link.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <target>Wenn Sie sich nicht persönlich treffen können, kann der **QR-Code während eines Videoanrufs gescannt werden**, oder Ihr Kontakt kann den Einladungslink über einen anderen Kanal mit Ihnen teilen.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> <target>Wenn Sie diesen Zugangscode während des Öffnens der App eingeben, werden alle App-Daten unwiederbringlich gelöscht!</target> @@ -2835,12 +4145,12 @@ Das kann nicht rückgängig gemacht werden!</target> </trans-unit> <trans-unit id="Image will be received when your contact completes uploading it." xml:space="preserve"> <source>Image will be received when your contact completes uploading it.</source> - <target>Das Bild wird empfangen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist.</target> + <target>Das Bild wird heruntergeladen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve"> <source>Image will be received when your contact is online, please wait or check later!</source> - <target>Das Bild wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</target> + <target>Das Bild wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Immediately" xml:space="preserve"> @@ -2848,8 +4158,8 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Sofort</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <target>Immun gegen Spam und Missbrauch</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2868,6 +4178,33 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Datenbank importieren</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Import failed" xml:space="preserve"> + <source>Import failed</source> + <target>Import ist fehlgeschlagen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import theme" xml:space="preserve"> + <source>Import theme</source> + <target>Design importieren</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Importing archive" xml:space="preserve"> + <source>Importing archive</source> + <target>Archiv wird importiert</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved delivery, reduced traffic usage. More improvements are coming soon!" xml:space="preserve"> + <source>Improved delivery, reduced traffic usage. +More improvements are coming soon!</source> + <target>Verbesserte Nachrichten-Auslieferung und verringerter Datenverbrauch. +Weitere Verbesserungen sind bald verfügbar!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved message delivery" xml:space="preserve"> + <source>Improved message delivery</source> + <target>Verbesserte Zustellung von Nachrichten</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> <target>Verbesserte Privatsphäre und Sicherheit</target> @@ -2878,11 +4215,31 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Verbesserte Serverkonfiguration</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In order to continue, chat should be stopped." xml:space="preserve"> + <source>In order to continue, chat should be stopped.</source> + <target>Um fortzufahren, sollte der Chat beendet werden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>Als Antwort auf</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In-call sounds" xml:space="preserve"> + <source>In-call sounds</source> + <target>Klingeltöne</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Inappropriate content" xml:space="preserve"> + <source>Inappropriate content</source> + <target>Unangemessener Inhalt</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Inappropriate profile" xml:space="preserve"> + <source>Inappropriate profile</source> + <target>Unangemessenes Profil</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> <target>Inkognito</target> @@ -2920,12 +4277,12 @@ Das kann nicht rückgängig gemacht werden!</target> </trans-unit> <trans-unit id="Incompatible database version" xml:space="preserve"> <source>Incompatible database version</source> - <target>Inkompatible Datenbank-Version</target> + <target>Datenbank-Version nicht kompatibel</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incompatible version" xml:space="preserve"> <source>Incompatible version</source> - <target>Inkompatible Version</target> + <target>Version nicht kompatibel</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incorrect passcode" xml:space="preserve"> @@ -2953,6 +4310,11 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Installieren Sie [SimpleX Chat als Terminalanwendung](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Instant" xml:space="preserve"> + <source>Instant</source> + <target>Sofort</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> @@ -2960,30 +4322,80 @@ Das kann nicht rückgängig gemacht werden!</target> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Instantly" xml:space="preserve"> - <source>Instantly</source> - <target>Sofort</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> <target>Schnittstelle</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Interface colors" xml:space="preserve"> + <source>Interface colors</source> + <target>Interface-Farben</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid" xml:space="preserve"> + <source>Invalid</source> + <target>Ungültig</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (bad token)" xml:space="preserve"> + <source>Invalid (bad token)</source> + <target>Ungültig (falsches Token)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (expired)" xml:space="preserve"> + <source>Invalid (expired)</source> + <target>Ungültig (abgelaufen)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (unregistered)" xml:space="preserve"> + <source>Invalid (unregistered)</source> + <target>Ungültig (nicht registriert)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (wrong topic)" xml:space="preserve"> + <source>Invalid (wrong topic)</source> + <target>Ungültig (falsches Thema)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid QR code" xml:space="preserve"> + <source>Invalid QR code</source> + <target>Ungültiger QR-Code</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> <target>Ungültiger Verbindungslink</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid display name!" xml:space="preserve"> + <source>Invalid display name!</source> + <target>Ungültiger Anzeigename!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid link" xml:space="preserve"> + <source>Invalid link</source> + <target>Ungültiger Link</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Migrations-Bestätigung ungültig</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid name!" xml:space="preserve"> <source>Invalid name!</source> <target>Ungültiger Name!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid response" xml:space="preserve"> + <source>Invalid response</source> + <target>Ungültige Reaktion</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> <target>Ungültige Serveradresse!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Invalid status" xml:space="preserve"> <source>Invalid status</source> @@ -3005,6 +4417,11 @@ Das kann nicht rückgängig gemacht werden!</target> <target>Mitglieder einladen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invite to chat" xml:space="preserve"> + <source>Invite to chat</source> + <target>Zum Chat einladen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invite to group" xml:space="preserve"> <source>Invite to group</source> <target>In Gruppe einladen</target> @@ -3020,8 +4437,8 @@ Das kann nicht rückgängig gemacht werden!</target> <target>In diesem Chat ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <target>In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3046,6 +4463,11 @@ Das kann nicht rückgängig gemacht werden!</target> 3. Die Verbindung wurde kompromittiert.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="It protects your IP address and connections." xml:space="preserve"> + <source>It protects your IP address and connections.</source> + <target>Ihre IP-Adresse und Verbindungen werden geschützt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> <target>Es sieht so aus, als ob Sie bereits über diesen Link verbunden sind. Wenn das nicht der Fall ist, gab es einen Fehler (%@).</target> @@ -3064,13 +4486,18 @@ Das kann nicht rückgängig gemacht werden!</target> <trans-unit id="Join" xml:space="preserve"> <source>Join</source> <target>Beitreten</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> <target>Treten Sie der Gruppe bei</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Join group conversations" xml:space="preserve"> + <source>Join group conversations</source> + <target>Gruppenunterhaltungen beitreten</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Join group?" xml:space="preserve"> <source>Join group?</source> <target>Der Gruppe beitreten?</target> @@ -3098,11 +4525,26 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Der Gruppe beitreten</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep" xml:space="preserve"> + <source>Keep</source> + <target>Behalten</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Keep conversation" xml:space="preserve"> + <source>Keep conversation</source> + <target>Chat-Inhalte beibehalten</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Keep the app open to use it from desktop" xml:space="preserve"> <source>Keep the app open to use it from desktop</source> <target>Die App muss geöffnet bleiben, um sie vom Desktop aus nutzen zu können</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep unused invitation?" xml:space="preserve"> + <source>Keep unused invitation?</source> + <target>Nicht genutzte Einladung behalten?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> <source>Keep your connections</source> <target>Ihre Verbindungen beibehalten</target> @@ -3136,6 +4578,16 @@ Das ist Ihr Link für die Gruppe %@!</target> <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> <target>Verlassen</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Leave chat" xml:space="preserve"> + <source>Leave chat</source> + <target>Chat verlassen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave chat?" xml:space="preserve"> + <source>Leave chat?</source> + <target>Chat verlassen?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group" xml:space="preserve"> @@ -3178,6 +4630,21 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Verknüpfte Desktops</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="List" xml:space="preserve"> + <source>List</source> + <target>Liste</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="List name and emoji should be different for all lists." xml:space="preserve"> + <source>List name and emoji should be different for all lists.</source> + <target>Der Listenname und das Emoji sollen für alle Listen unterschiedlich sein.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List name..." xml:space="preserve"> + <source>List name...</source> + <target>Listenname...</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Live message!" xml:space="preserve"> <source>Live message!</source> <target>Live Nachricht!</target> @@ -3208,11 +4675,6 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Sperr-Modus</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> - <source>Make a private connection</source> - <target>Stellen Sie eine private Verbindung her</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make one message disappear" xml:space="preserve"> <source>Make one message disappear</source> <target>Eine verschwindende Nachricht verfassen</target> @@ -3223,19 +4685,9 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Privates Profil erzeugen!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> - <target>Stellen Sie sicher, dass die %@-Server-Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind (%@).</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> - <target>Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht doppelt vorhanden sind.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <target>Viele Menschen haben gefragt: *Wie kann SimpleX Nachrichten zustellen, wenn es keine Benutzerkennungen gibt?*</target> + <target>Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> @@ -3263,11 +4715,36 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Max. 30 Sekunden, sofort erhalten.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Media & file servers" xml:space="preserve"> + <source>Media & file servers</source> + <target>Medien- und Datei-Server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Medium" xml:space="preserve"> + <source>Medium</source> + <target>Medium</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Member" xml:space="preserve"> <source>Member</source> <target>Mitglied</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member inactive" xml:space="preserve"> + <source>Member inactive</source> + <target>Mitglied inaktiv</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Member reports" xml:space="preserve"> + <source>Member reports</source> + <target>Mitglieder-Meldungen</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All chat members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All chat members will be notified.</source> + <target>Die Rolle des Mitglieds wird auf "%@" geändert. Alle Chat-Mitglieder werden darüber informiert.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> <target>Die Mitgliederrolle wird auf "%@" geändert. Alle Mitglieder der Gruppe werden benachrichtigt.</target> @@ -3278,9 +4755,64 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Die Mitgliederrolle wird auf "%@" geändert. Das Mitglied wird eine neue Einladung erhalten.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from chat - this cannot be undone!</source> + <target>Das Mitglied wird aus dem Chat entfernt. Dies kann nicht rückgängig gemacht werden!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> - <target>Das Mitglied wird aus der Gruppe entfernt - dies kann nicht rückgängig gemacht werden!</target> + <target>Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can add message reactions." xml:space="preserve"> + <source>Members can add message reactions.</source> + <target>Gruppenmitglieder können eine Reaktion auf Nachrichten geben.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Members can irreversibly delete sent messages. (24 hours)</source> + <target>Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can report messsages to moderators." xml:space="preserve"> + <source>Members can report messsages to moderators.</source> + <target>Mitglieder können Nachrichten an Moderatoren melden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send SimpleX links." xml:space="preserve"> + <source>Members can send SimpleX links.</source> + <target>Gruppenmitglieder können SimpleX-Links versenden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> + <target>Gruppenmitglieder können Direktnachrichten versenden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> + <target>Gruppenmitglieder können verschwindende Nachrichten versenden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send files and media." xml:space="preserve"> + <source>Members can send files and media.</source> + <target>Gruppenmitglieder können Dateien und Medien versenden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> + <target>Gruppenmitglieder können Sprachnachrichten versenden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mention members 👋" xml:space="preserve"> + <source>Mention members 👋</source> + <target>Erwähnung von Mitgliedern 👋</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Menus" xml:space="preserve"> + <source>Menus</source> + <target>Menüs</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Message delivery error" xml:space="preserve"> @@ -3293,11 +4825,31 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Empfangsbestätigungen für Nachrichten!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message delivery warning" xml:space="preserve"> + <source>Message delivery warning</source> + <target>Warnung bei der Nachrichtenzustellung</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> <target>Nachrichtenentwurf</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message forwarded" xml:space="preserve"> + <source>Message forwarded</source> + <target>Nachricht weitergeleitet</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message may be delivered later if member becomes active." xml:space="preserve"> + <source>Message may be delivered later if member becomes active.</source> + <target>Die Nachricht kann später zugestellt werden, wenn das Mitglied aktiv wird.</target> + <note>item status description</note> + </trans-unit> + <trans-unit id="Message queue info" xml:space="preserve"> + <source>Message queue info</source> + <target>Nachrichten-Warteschlangen-Information</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message reactions" xml:space="preserve"> <source>Message reactions</source> <target>Reaktionen auf Nachrichten</target> @@ -3308,16 +4860,51 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>In diesem Chat sind Reaktionen auf Nachrichten nicht erlaubt.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> - <source>Message reactions are prohibited in this group.</source> + <trans-unit id="Message reactions are prohibited." xml:space="preserve"> + <source>Message reactions are prohibited.</source> <target>In dieser Gruppe sind Reaktionen auf Nachrichten nicht erlaubt.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message reception" xml:space="preserve"> + <source>Message reception</source> + <target>Nachrichtenempfang</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message servers" xml:space="preserve"> + <source>Message servers</source> + <target>Nachrichten-Server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message shape" xml:space="preserve"> + <source>Message shape</source> + <target>Nachrichten-Form</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message source remains private." xml:space="preserve"> + <source>Message source remains private.</source> + <target>Die Nachrichtenquelle bleibt privat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status" xml:space="preserve"> + <source>Message status</source> + <target>Nachrichten-Status</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status: %@" xml:space="preserve"> + <source>Message status: %@</source> + <target>Nachrichten-Status: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> <target>Nachrichtentext</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message too large" xml:space="preserve"> + <source>Message too large</source> + <target>Die Nachricht ist zu lang</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> <target>Nachrichten</target> @@ -3333,11 +4920,76 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Die Nachrichten von %@ werden angezeigt!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Messages in this chat will never be deleted." xml:space="preserve"> + <source>Messages in this chat will never be deleted.</source> + <target>Nachrichten in diesem Chat werden nie gelöscht.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages received" xml:space="preserve"> + <source>Messages received</source> + <target>Empfangene Nachrichten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages sent" xml:space="preserve"> + <source>Messages sent</source> + <target>Gesendete Nachrichten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages were deleted after you selected them." xml:space="preserve"> + <source>Messages were deleted after you selected them.</source> + <target>Die Nachrichten wurden gelöscht, nachdem Sie sie ausgewählt hatten.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Nachrichten, Dateien und Anrufe sind durch **Ende-zu-Ende-Verschlüsselung** mit Perfect Forward Secrecy, Abstreitbarkeit und Wiederherstellung nach einer Kompromittierung geschützt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Nachrichten, Dateien und Anrufe sind durch **Quantum-resistente E2E-Verschlüsselung** mit Perfect Forward Secrecy, Abstreitbarkeit und Wiederherstellung nach einer Kompromittierung geschützt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate device" xml:space="preserve"> + <source>Migrate device</source> + <target>Gerät migrieren</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate from another device" xml:space="preserve"> + <source>Migrate from another device</source> + <target>Von einem anderen Gerät migrieren</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate here" xml:space="preserve"> + <source>Migrate here</source> + <target>Hierher migrieren</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device" xml:space="preserve"> + <source>Migrate to another device</source> + <target>Auf ein anderes Gerät migrieren</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device via QR code." xml:space="preserve"> + <source>Migrate to another device via QR code.</source> + <target>Daten können über einen QR-Code auf ein anderes Gerät migriert werden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating" xml:space="preserve"> + <source>Migrating</source> + <target>Migrieren</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migrating database archive…" xml:space="preserve"> <source>Migrating database archive…</source> <target>Datenbank-Archiv wird migriert…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Migration complete" xml:space="preserve"> + <source>Migration complete</source> + <target>Migration abgeschlossen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> <target>Fehler bei der Migration:</target> @@ -3353,9 +5005,9 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Die Migration wurde abgeschlossen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> - <target>Migrationen: %@</target> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> + <target>Migrationen:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> @@ -3373,21 +5025,31 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Moderiert um: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="More" xml:space="preserve"> + <source>More</source> + <target>Mehr</target> + <note>swipe action</note> + </trans-unit> <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> <target>Weitere Verbesserungen sind bald verfügbar!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="More reliable network connection." xml:space="preserve"> + <source>More reliable network connection.</source> + <target>Zuverlässigere Netzwerkverbindung.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="More reliable notifications" xml:space="preserve"> + <source>More reliable notifications</source> + <target>Zuverlässigere Benachrichtigungen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> <source>Most likely this connection is deleted.</source> <target>Wahrscheinlich ist diese Verbindung gelöscht worden.</target> <note>item status description</note> </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> - <source>Most likely this contact has deleted the connection with you.</source> - <target>Dieser Kontakt hat sehr wahrscheinlich die Verbindung mit Ihnen gelöscht.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> <target>Mehrere Chat-Profile</target> @@ -3396,7 +5058,12 @@ Das ist Ihr Link für die Gruppe %@!</target> <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> <target>Stummschalten</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Mute all" xml:space="preserve"> + <source>Mute all</source> + <target>Alle stummschalten</target> + <note>notification label action</note> </trans-unit> <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> @@ -3406,13 +5073,38 @@ Das ist Ihr Link für die Gruppe %@!</target> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> <target>Name</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> <target>Netzwerk & Server</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Network connection" xml:space="preserve"> + <source>Network connection</source> + <target>Netzwerkverbindung</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network decentralization" xml:space="preserve"> + <source>Network decentralization</source> + <target>Dezentralisiertes Netzwerk</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve"> + <source>Network issues - message expired after many attempts to send it.</source> + <target>Netzwerk-Fehler - die Nachricht ist nach vielen Sende-Versuchen abgelaufen.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Network management" xml:space="preserve"> + <source>Network management</source> + <target>Netzwerk-Verwaltung</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network operator" xml:space="preserve"> + <source>Network operator</source> + <target>Netzwerk-Betreiber</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> <target>Netzwerkeinstellungen</target> @@ -3423,11 +5115,36 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Netzwerkstatus</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New" xml:space="preserve"> + <source>New</source> + <target>Neu</target> + <note>token status text</note> + </trans-unit> <trans-unit id="New Passcode" xml:space="preserve"> <source>New Passcode</source> <target>Neuer Zugangscode</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve"> + <source>New SOCKS credentials will be used every time you start the app.</source> + <target>Jedes Mal wenn Sie die App starten, werden neue SOCKS-Anmeldeinformationen genutzt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve"> + <source>New SOCKS credentials will be used for each server.</source> + <target>Für jeden Server werden neue SOCKS-Anmeldeinformationen genutzt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat" xml:space="preserve"> + <source>New chat</source> + <target>Neuer Chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat experience 🎉" xml:space="preserve"> + <source>New chat experience 🎉</source> + <target>Neue Chat-Erfahrung 🎉</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> <target>Neue Kontaktanfrage</target> @@ -3438,11 +5155,6 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Neuer Kontakt:</target> <note>notification</note> </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> - <source>New database archive</source> - <target>Neues Datenbankarchiv</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="New desktop app!" xml:space="preserve"> <source>New desktop app!</source> <target>Neue Desktop-App!</target> @@ -3453,11 +5165,21 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Neuer Anzeigename</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>Neue Ereignisse</target> + <note>notification</note> + </trans-unit> <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> <target>Neu in %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New media options" xml:space="preserve"> + <source>New media options</source> + <target>Neue Medien-Optionen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> <target>Neue Mitgliedsrolle</target> @@ -3473,6 +5195,11 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Neues Passwort…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New server" xml:space="preserve"> + <source>New server</source> + <target>Neuer Server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No" xml:space="preserve"> <source>No</source> <target>Nein</target> @@ -3483,6 +5210,21 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Kein App-Passwort</target> <note>Authentication unavailable</note> </trans-unit> + <trans-unit id="No chats" xml:space="preserve"> + <source>No chats</source> + <target>Keine Chats</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats found" xml:space="preserve"> + <source>No chats found</source> + <target>Keine Chats gefunden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats in list %@" xml:space="preserve"> + <source>No chats in list %@</source> + <target>Keine Chats in der Liste %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> <target>Keine Kontakte ausgewählt</target> @@ -3503,6 +5245,11 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Kein Geräte-Token!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No direct connection yet, message is forwarded by admin." xml:space="preserve"> + <source>No direct connection yet, message is forwarded by admin.</source> + <target>Bisher keine direkte Verbindung. Nachricht wird von einem Admin weitergeleitet.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="No filtered chats" xml:space="preserve"> <source>No filtered chats</source> <target>Keine gefilterten Chats</target> @@ -3515,7 +5262,42 @@ Das ist Ihr Link für die Gruppe %@!</target> </trans-unit> <trans-unit id="No history" xml:space="preserve"> <source>No history</source> - <target>Keine Vergangenheit</target> + <target>Kein Nachrichtenverlauf</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No info, try to reload" xml:space="preserve"> + <source>No info, try to reload</source> + <target>Keine Information - es wird versucht neu zu laden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No media & file servers." xml:space="preserve"> + <source>No media & file servers.</source> + <target>Keine Medien- und Dateiserver.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No message" xml:space="preserve"> + <source>No message</source> + <target>Keine Nachricht</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No message servers." xml:space="preserve"> + <source>No message servers.</source> + <target>Keine Nachrichten-Server.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No network connection" xml:space="preserve"> + <source>No network connection</source> + <target>Keine Netzwerkverbindung</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record speech" xml:space="preserve"> + <source>No permission to record speech</source> + <target>Keine Genehmigung für Sprach-Aufnahmen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record video" xml:space="preserve"> + <source>No permission to record video</source> + <target>Keine Genehmigung für Video-Aufnahmen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> @@ -3523,9 +5305,49 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Keine Berechtigung für das Aufnehmen von Sprachnachrichten</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No push server" xml:space="preserve"> + <source>No push server</source> + <target>Lokal</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> - <target>Keine empfangenen oder gesendeten Dateien</target> + <target>Keine herunter- oder hochgeladenen Dateien</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No servers for private message routing." xml:space="preserve"> + <source>No servers for private message routing.</source> + <target>Keine Server für privates Nachrichten-Routing.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive files." xml:space="preserve"> + <source>No servers to receive files.</source> + <target>Keine Server für das Herunterladen von Dateien.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive messages." xml:space="preserve"> + <source>No servers to receive messages.</source> + <target>Keine Server für den Empfang von Nachrichten.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to send files." xml:space="preserve"> + <source>No servers to send files.</source> + <target>Keine Server für das Versenden von Dateien.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No token!" xml:space="preserve"> + <source>No token!</source> + <target>Kein Token!</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="No unread chats" xml:space="preserve"> + <source>No unread chats</source> + <target>Keine ungelesenen Chats</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> + <target>Keine Benutzerkennungen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Not compatible!" xml:space="preserve"> @@ -3533,6 +5355,21 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Nicht kompatibel!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notes" xml:space="preserve"> + <source>Notes</source> + <target>Anmerkungen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing selected" xml:space="preserve"> + <source>Nothing selected</source> + <target>Nichts ausgewählt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing to forward!" xml:space="preserve"> + <source>Nothing to forward!</source> + <target>Es gibt nichts zum Weiterleiten!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> <target>Benachrichtigungen</target> @@ -3543,6 +5380,21 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Benachrichtigungen sind deaktiviert!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notifications error" xml:space="preserve"> + <source>Notifications error</source> + <target>Benachrichtigungs-Fehler</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Notifications privacy" xml:space="preserve"> + <source>Notifications privacy</source> + <target>Datenschutz für Benachrichtigungen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications status" xml:space="preserve"> + <source>Notifications status</source> + <target>Benachrichtigungs-Status</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. @@ -3552,44 +5404,43 @@ Das ist Ihr Link für die Gruppe %@!</target> - Gruppenmitglieder deaktivieren ("Beobachter"-Rolle)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="OK" xml:space="preserve"> + <source>OK</source> + <target>OK</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Off" xml:space="preserve"> <source>Off</source> <target>Aus</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> - <source>Off (Local)</source> - <target>Aus (Lokal)</target> - <note>No comment provided by engineer.</note> + <note>blur media</note> </trans-unit> <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> <target>Ok</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> <target>Alte Datenbank</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> - <source>Old database archive</source> - <target>Altes Datenbankarchiv</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> <target>Einmal-Einladungslink</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> - <target>Für die Verbindung werden Onion-Hosts benötigt. Dies erfordert die Aktivierung eines VPNs.</target> + <trans-unit id="Onion hosts will be **required** for connection. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be **required** for connection. +Requires compatible VPN.</source> + <target>Für diese Verbindung werden Onion-Hosts benötigt. +Dies erfordert die Aktivierung eines VPNs.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> - <target>Onion-Hosts werden verwendet, sobald sie verfügbar sind. Dies erfordert die Aktivierung eines VPNs.</target> + <trans-unit id="Onion hosts will be used when available. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be used when available. +Requires compatible VPN.</source> + <target>Wenn Onion-Hosts verfügbar sind, werden sie verwendet. +Dies erfordert die Aktivierung eines VPNs.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will not be used." xml:space="preserve"> @@ -3597,11 +5448,21 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Onion-Hosts werden nicht verwendet.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only chat owners can change preferences." xml:space="preserve"> + <source>Only chat owners can change preferences.</source> + <target>Nur Chat-Eigentümer können die Präferenzen ändern.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <target>Nur die Endgeräte speichern die Benutzerprofile, Kontakte, Gruppen und Nachrichten, welche über eine **2-Schichten Ende-zu-Ende-Verschlüsselung** gesendet werden.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only delete conversation" xml:space="preserve"> + <source>Only delete conversation</source> + <target>Nur die Chat-Inhalte löschen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> <source>Only group owners can change group preferences.</source> <target>Gruppen-Präferenzen können nur von Gruppen-Eigentümern geändert werden.</target> @@ -3617,14 +5478,24 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Sprachnachrichten können nur von Gruppen-Eigentümern aktiviert werden.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only sender and moderators see it" xml:space="preserve"> + <source>Only sender and moderators see it</source> + <target>Nur Absender und Moderatoren sehen es</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you and moderators see it" xml:space="preserve"> + <source>Only you and moderators see it</source> + <target>Nur Sie und Moderatoren sehen es</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only you can add message reactions." xml:space="preserve"> <source>Only you can add message reactions.</source> <target>Nur Sie können Reaktionen auf Nachrichten geben.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <target>Nur Sie können Nachrichten unwiederbringlich löschen (Ihr Kontakt kann sie zum Löschen markieren).</target> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source> + <target>Nur Sie können Nachrichten unwiederbringlich löschen (Ihr Kontakt kann sie zum Löschen markieren). (24 Stunden)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can make calls." xml:space="preserve"> @@ -3647,9 +5518,9 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Nur Ihr Kontakt kann Reaktionen auf Nachrichten geben.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <target>Nur Ihr Kontakt kann Nachrichten unwiederbringlich löschen (Sie können sie zum Löschen markieren).</target> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source> + <target>Nur Ihr Kontakt kann Nachrichten unwiederbringlich löschen (Sie können sie zum Löschen markieren). (24 Stunden)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can make calls." xml:space="preserve"> @@ -3670,13 +5541,18 @@ Das ist Ihr Link für die Gruppe %@!</target> <trans-unit id="Open" xml:space="preserve"> <source>Open</source> <target>Öffnen</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Geräte-Einstellungen öffnen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open changes" xml:space="preserve"> + <source>Open changes</source> + <target>Änderungen öffnen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> <target>Chat öffnen</target> @@ -3687,26 +5563,87 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Chat-Konsole öffnen</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Open conditions" xml:space="preserve"> + <source>Open conditions</source> + <target>Nutzungsbedingungen öffnen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open group" xml:space="preserve"> <source>Open group</source> <target>Gruppe öffnen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve"> - <source>Open user profiles</source> - <target>Benutzerprofile öffnen</target> + <trans-unit id="Open link?" xml:space="preserve"> + <source>Open link?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Open migration to another device" xml:space="preserve"> + <source>Open migration to another device</source> + <target>Migration auf ein anderes Gerät öffnen</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> - <target>Open-Source-Protokoll und -Code – Jede Person kann ihre eigenen Server aufsetzen und nutzen.</target> + <trans-unit id="Opening app…" xml:space="preserve"> + <source>Opening app…</source> + <target>App wird geöffnet…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve"> - <source>Opening database…</source> - <target>Öffne Datenbank …</target> + <trans-unit id="Operator" xml:space="preserve"> + <source>Operator</source> + <target>Betreiber</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Operator server" xml:space="preserve"> + <source>Operator server</source> + <target>Betreiber-Server</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Or import archive file" xml:space="preserve"> + <source>Or import archive file</source> + <target>Oder importieren Sie eine Archiv-Datei</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or paste archive link" xml:space="preserve"> + <source>Or paste archive link</source> + <target>Oder fügen Sie den Archiv-Link ein</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or scan QR code" xml:space="preserve"> + <source>Or scan QR code</source> + <target>Oder den QR-Code scannen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or securely share this file link" xml:space="preserve"> + <source>Or securely share this file link</source> + <target>Oder teilen Sie diesen Datei-Link sicher</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or show this code" xml:space="preserve"> + <source>Or show this code</source> + <target>Oder diesen QR-Code anzeigen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or to share privately" xml:space="preserve"> + <source>Or to share privately</source> + <target>Oder zum privaten Teilen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Organize chats into lists" xml:space="preserve"> + <source>Organize chats into lists</source> + <target>Chats in Listen verwalten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other" xml:space="preserve"> + <source>Other</source> + <target>Andere</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other file errors: %@" xml:space="preserve"> + <source>Other file errors: +%@</source> + <target>Andere(r) Datei-Fehler: +%@</target> + <note>alert message</note> + </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> <target>PING-Zähler</target> @@ -3742,15 +5679,20 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Zugangscode eingestellt!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Password" xml:space="preserve"> + <source>Password</source> + <target>Passwort</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> <target>Passwort anzeigen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste" xml:space="preserve"> - <source>Paste</source> - <target>Einfügen</target> - <note>No comment provided by engineer.</note> + <trans-unit id="Past member %@" xml:space="preserve"> + <source>Past member %@</source> + <target>Ehemaliges Mitglied %@</target> + <note>past/unknown group member</note> </trans-unit> <trans-unit id="Paste desktop address" xml:space="preserve"> <source>Paste desktop address</source> @@ -3762,23 +5704,23 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Bild einfügen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve"> - <source>Paste received link</source> + <trans-unit id="Paste link to connect!" xml:space="preserve"> + <source>Paste link to connect!</source> + <target>Zum Verbinden den Link einfügen!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Paste the link you received" xml:space="preserve"> + <source>Paste the link you received</source> <target>Fügen Sie den erhaltenen Link ein</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> - <source>Paste the link you received to connect with your contact.</source> - <target>Um sich mit Ihrem Kontakt zu verbinden, fügen Sie den erhaltenen Link in das Feld unten ein.</target> - <note>placeholder</note> - </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> - <target>Verbindungen mit Kontakten sind nur über Links möglich, die Sie oder Ihre Kontakte untereinander teilen.</target> + <trans-unit id="Pending" xml:space="preserve"> + <source>Pending</source> + <target>Ausstehend</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Periodically" xml:space="preserve"> - <source>Periodically</source> + <trans-unit id="Periodic" xml:space="preserve"> + <source>Periodic</source> <target>Periodisch</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3787,11 +5729,33 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Entschlüsselungsfehler</target> <note>message decrypt error item</note> </trans-unit> + <trans-unit id="Picture-in-picture calls" xml:space="preserve"> + <source>Picture-in-picture calls</source> + <target>Bild-in-Bild-Anrufe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Play from the chat list." xml:space="preserve"> + <source>Play from the chat list.</source> + <target>Direkt aus der Chat-Liste abspielen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable calls." xml:space="preserve"> + <source>Please ask your contact to enable calls.</source> + <target>Bitten Sie Ihren Kontakt darum, Anrufe zu aktivieren.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> <target>Bitten Sie Ihren Kontakt darum, das Senden von Sprachnachrichten zu aktivieren.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers." xml:space="preserve"> + <source>Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers.</source> + <target>Bitte überprüfen Sie, ob sich das Mobiltelefon und die Desktop-App im gleichen lokalen Netzwerk befinden, und die Desktop-Firewall die Verbindung erlaubt. +Bitte teilen Sie weitere mögliche Probleme den Entwicklern mit.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> <target>Überprüfen Sie bitte, ob Sie den richtigen Link genutzt haben oder bitten Sie Ihren Kontakt nochmal darum, Ihnen einen Link zuzusenden.</target> @@ -3807,6 +5771,18 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Bitte überprüfen sie sowohl Ihre, als auch die Präferenzen Ihres Kontakts.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve"> + <source>Please confirm that network settings are correct for this device.</source> + <target>Bitte bestätigen Sie, dass die Netzwerkeinstellungen auf diesem Gerät richtig sind.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact developers. Error: %@" xml:space="preserve"> + <source>Please contact developers. +Error: %@</source> + <target>Bitte nehmen Sie Kontakt mit den Entwicklern auf. +Fehler: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> <target>Bitte kontaktieren Sie den Gruppen-Administrator.</target> @@ -3847,11 +5823,31 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Bitte bewahren Sie das Passwort sicher auf, Sie können es NICHT mehr ändern, wenn Sie es vergessen haben oder verlieren.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please try to disable and re-enable notfications." xml:space="preserve"> + <source>Please try to disable and re-enable notfications.</source> + <target>Bitte versuchen Sie, die Benachrichtigungen zu deaktivieren und wieder zu aktivieren.</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token activation to complete." xml:space="preserve"> + <source>Please wait for token activation to complete.</source> + <target>Bitte warten Sie, bis die Token-Aktivierung abgeschlossen ist.</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token to be registered." xml:space="preserve"> + <source>Please wait for token to be registered.</source> + <target>Bitte warten Sie auf die Registrierung des Tokens.</target> + <note>token info</note> + </trans-unit> <trans-unit id="Polish interface" xml:space="preserve"> <source>Polish interface</source> <target>Polnische Bedienoberfläche</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Port" xml:space="preserve"> + <source>Port</source> + <target>Port</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> <target>Der Fingerabdruck des Zertifikats in der Serveradresse ist wahrscheinlich ungültig</target> @@ -3862,36 +5858,86 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Preset server" xml:space="preserve"> - <source>Preset server</source> - <target>Voreingestellter Server</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> <target>Voreingestellte Serveradresse</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Preset servers" xml:space="preserve"> + <source>Preset servers</source> + <target>Voreingestellte Server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Preview" xml:space="preserve"> <source>Preview</source> <target>Vorschau</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Previously connected servers" xml:space="preserve"> + <source>Previously connected servers</source> + <target>Bisher verbundene Server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> <target>Datenschutz & Sicherheit</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Privacy for your customers." xml:space="preserve"> + <source>Privacy for your customers.</source> + <target>Schutz der Privatsphäre Ihrer Kunden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy policy and conditions of use." xml:space="preserve"> + <source>Privacy policy and conditions of use.</source> + <target>Datenschutz- und Nutzungsbedingungen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy redefined" xml:space="preserve"> <source>Privacy redefined</source> <target>Datenschutz neu definiert</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve"> + <source>Private chats, groups and your contacts are not accessible to server operators.</source> + <target>Private Chats, Gruppen und Ihre Kontakte sind für Server-Betreiber nicht zugänglich.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> <target>Neutrale Dateinamen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private media file names." xml:space="preserve"> + <source>Private media file names.</source> + <target>Medien mit anonymisierten Dateinamen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing" xml:space="preserve"> + <source>Private message routing</source> + <target>Privates Nachrichten-Routing</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing 🚀" xml:space="preserve"> + <source>Private message routing 🚀</source> + <target>Privates Nachrichten-Routing 🚀</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private notes" xml:space="preserve"> + <source>Private notes</source> + <target>Private Notizen</target> + <note>name of notes to self</note> + </trans-unit> + <trans-unit id="Private routing" xml:space="preserve"> + <source>Private routing</source> + <target>Privates Routing</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private routing error" xml:space="preserve"> + <source>Private routing error</source> + <target>Fehler beim privaten Routing</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> <target>Profil und Serververbindungen</target> @@ -3902,14 +5948,9 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Profilbild</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile name" xml:space="preserve"> - <source>Profile name</source> - <target>Profilname</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile name:" xml:space="preserve"> - <source>Profile name:</source> - <target>Profilname:</target> + <trans-unit id="Profile images" xml:space="preserve"> + <source>Profile images</source> + <target>Profil-Bilder</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile password" xml:space="preserve"> @@ -3917,10 +5958,15 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Passwort für Profil</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Profile theme" xml:space="preserve"> + <source>Profile theme</source> + <target>Profil-Design</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> <source>Profile update will be sent to your contacts.</source> <target>Profil-Aktualisierung wird an Ihre Kontakte gesendet.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> @@ -3942,6 +5988,16 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Reaktionen auf Nachrichten nicht erlauben.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Prohibit reporting messages to moderators." xml:space="preserve"> + <source>Prohibit reporting messages to moderators.</source> + <target>Melden von Nachrichten an Moderatoren nicht erlauben.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending SimpleX links." xml:space="preserve"> + <source>Prohibit sending SimpleX links.</source> + <target>Das Senden von SimpleX-Links nicht erlauben.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> <target>Das Senden von Direktnachrichten an Gruppenmitglieder nicht erlauben.</target> @@ -3949,7 +6005,7 @@ Das ist Ihr Link für die Gruppe %@!</target> </trans-unit> <trans-unit id="Prohibit sending disappearing messages." xml:space="preserve"> <source>Prohibit sending disappearing messages.</source> - <target>Das Senden von verschwindenden Nachrichten verbieten.</target> + <target>Das Senden von verschwindenden Nachrichten nicht erlauben.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit sending files and media." xml:space="preserve"> @@ -3962,11 +6018,23 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Das Senden von Sprachnachrichten nicht erlauben.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect IP address" xml:space="preserve"> + <source>Protect IP address</source> + <target>IP-Adresse schützen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> <target>App-Bildschirm schützen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve"> + <source>Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings.</source> + <target>Schützen Sie Ihre IP-Adresse vor den Nachrichten-Relais, die Ihre Kontakte ausgewählt haben. +Aktivieren Sie es in den *Netzwerk & Server* Einstellungen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> <target>Ihre Chat-Profile mit einem Passwort schützen!</target> @@ -3982,16 +6050,46 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Protokollzeitüberschreitung pro kB</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Proxied" xml:space="preserve"> + <source>Proxied</source> + <target>Proxied</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied servers" xml:space="preserve"> + <source>Proxied servers</source> + <target>Proxy-Server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxy requires password" xml:space="preserve"> + <source>Proxy requires password</source> + <target>Der Proxy benötigt ein Passwort</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> <target>Push-Benachrichtigungen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Push server" xml:space="preserve"> + <source>Push server</source> + <target>Push-Server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Quantum resistant encryption" xml:space="preserve"> + <source>Quantum resistant encryption</source> + <target>Quantum-resistente Verschlüsselung</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> <source>Rate the app</source> <target>Bewerten Sie die App</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reachable chat toolbar" xml:space="preserve"> + <source>Reachable chat toolbar</source> + <target>Chat-Symbolleiste unten</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> <target>Reagiere…</target> @@ -4000,16 +6098,21 @@ Das ist Ihr Link für die Gruppe %@!</target> <trans-unit id="Read" xml:space="preserve"> <source>Read</source> <target>Gelesen</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Read more" xml:space="preserve"> <source>Read more</source> <target>Mehr erfahren</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> - <target>Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address) lesen.</target> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source> + <target>Lesen Sie mehr dazu im [Benutzerhandbuch](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</source> + <target>Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses) lesen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> @@ -4017,11 +6120,6 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/readme.html#connect-to-friends) lesen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> - <source>Read more in our GitHub repository.</source> - <target>Erfahren Sie in unserem GitHub-Repository mehr dazu.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> <target>Erfahren Sie in unserem [GitHub-Repository](https://github.com/simplex-chat/simplex-chat#readme) mehr dazu.</target> @@ -4032,6 +6130,11 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Bestätigungen sind deaktiviert</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receive errors" xml:space="preserve"> + <source>Receive errors</source> + <target>Fehler beim Empfang</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Empfangen um</target> @@ -4052,6 +6155,21 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Empfangene Nachricht</target> <note>message info title</note> </trans-unit> + <trans-unit id="Received messages" xml:space="preserve"> + <source>Received messages</source> + <target>Empfangene Nachrichten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received reply" xml:space="preserve"> + <source>Received reply</source> + <target>Empfangene Antwort</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received total" xml:space="preserve"> + <source>Received total</source> + <target>Summe aller empfangenen Nachrichten</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> <target>Die Empfängeradresse wird auf einen anderen Server geändert. Der Adresswechsel wird abgeschlossen, wenn der Absender wieder online ist.</target> @@ -4059,7 +6177,7 @@ Das ist Ihr Link für die Gruppe %@!</target> </trans-unit> <trans-unit id="Receiving file will be stopped." xml:space="preserve"> <source>Receiving file will be stopped.</source> - <target>Der Empfang der Datei wird beendet.</target> + <target>Das Herunterladen der Datei wird beendet.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Receiving via" xml:space="preserve"> @@ -4067,16 +6185,51 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Empfangen über</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Recent history and improved [directory bot](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)." xml:space="preserve"> + <source>Recent history and improved [directory bot](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).</source> + <target>Aktueller Nachrichtenverlauf und verbesserter [Gruppenverzeichnis-Bot](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).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve"> + <source>Recipient(s) can't see who this message is from.</source> + <target>Empfänger können nicht sehen, von wem die Nachricht stammt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> <target>Die Empfänger sehen Nachrichtenaktualisierungen, während Sie sie eingeben.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect" xml:space="preserve"> + <source>Reconnect</source> + <target>Neu verbinden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> <target>Alle verbundenen Server werden neu verbunden, um die Zustellung der Nachricht zu erzwingen. Dies verursacht zusätzlichen Datenverkehr.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect all servers" xml:space="preserve"> + <source>Reconnect all servers</source> + <target>Alle Server neu verbinden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers?" xml:space="preserve"> + <source>Reconnect all servers?</source> + <target>Alle Server neu verbinden?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect server to force message delivery. It uses additional traffic.</source> + <target>Um die Auslieferung von Nachrichten zu erzwingen, wird der Server neu verbunden. Dafür wird weiterer Datenverkehr benötigt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server?" xml:space="preserve"> + <source>Reconnect server?</source> + <target>Server neu verbinden?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> <target>Die Server neu verbinden?</target> @@ -4097,10 +6250,26 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Reduzierter Batterieverbrauch</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Register" xml:space="preserve"> + <source>Register</source> + <target>Registrieren</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Register notification token?" xml:space="preserve"> + <source>Register notification token?</source> + <target>Benachrichtigungs-Token registrieren?</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Registered" xml:space="preserve"> + <source>Registered</source> + <target>Registriert</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> <target>Ablehnen</target> - <note>reject incoming call via notification</note> + <note>reject incoming call via notification +swipe action</note> </trans-unit> <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> <source>Reject (sender NOT notified)</source> @@ -4127,6 +6296,16 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Entfernen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Remove archive?" xml:space="preserve"> + <source>Remove archive?</source> + <target>Archiv entfernen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove image" xml:space="preserve"> + <source>Remove image</source> + <target>Bild entfernen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> <target>Mitglied entfernen</target> @@ -4162,16 +6341,81 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Verbindungsanfrage wiederholen?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat download" xml:space="preserve"> + <source>Repeat download</source> + <target>Herunterladen wiederholen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat import" xml:space="preserve"> + <source>Repeat import</source> + <target>Import wiederholen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Repeat join request?" xml:space="preserve"> <source>Repeat join request?</source> <target>Verbindungsanfrage wiederholen?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat upload" xml:space="preserve"> + <source>Repeat upload</source> + <target>Hochladen wiederholen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reply" xml:space="preserve"> <source>Reply</source> <target>Antwort</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Report" xml:space="preserve"> + <source>Report</source> + <target>Melden</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Report content: only group moderators will see it." xml:space="preserve"> + <source>Report content: only group moderators will see it.</source> + <target>Inhalt melden: Nur Gruppenmoderatoren werden es sehen.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report member profile: only group moderators will see it." xml:space="preserve"> + <source>Report member profile: only group moderators will see it.</source> + <target>Mitgliederprofil melden: Nur Gruppenmoderatoren werden es sehen.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report other: only group moderators will see it." xml:space="preserve"> + <source>Report other: only group moderators will see it.</source> + <target>Anderes melden: Nur Gruppenmoderatoren werden es sehen.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report reason?" xml:space="preserve"> + <source>Report reason?</source> + <target>Grund der Meldung?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Report spam: only group moderators will see it." xml:space="preserve"> + <source>Report spam: only group moderators will see it.</source> + <target>Spam melden: Nur Gruppenmoderatoren werden es sehen.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report violation: only group moderators will see it." xml:space="preserve"> + <source>Report violation: only group moderators will see it.</source> + <target>Verstoß melden: Nur Gruppenmoderatoren werden es sehen.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report: %@" xml:space="preserve"> + <source>Report: %@</source> + <target>Meldung: %@</target> + <note>report in notification</note> + </trans-unit> + <trans-unit id="Reporting messages to moderators is prohibited." xml:space="preserve"> + <source>Reporting messages to moderators is prohibited.</source> + <target>Melden von Nachrichten an Moderatoren ist nicht erlaubt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reports" xml:space="preserve"> + <source>Reports</source> + <target>Meldungen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Required" xml:space="preserve"> <source>Required</source> <target>Erforderlich</target> @@ -4182,16 +6426,41 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Zurücksetzen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset all hints" xml:space="preserve"> + <source>Reset all hints</source> + <target>Alle Hinweise zurücksetzen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics" xml:space="preserve"> + <source>Reset all statistics</source> + <target>Alle Statistiken zurücksetzen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics?" xml:space="preserve"> + <source>Reset all statistics?</source> + <target>Alle Statistiken zurücksetzen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> <target>Farben zurücksetzen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to app theme" xml:space="preserve"> + <source>Reset to app theme</source> + <target>Auf das App-Design zurücksetzen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> <target>Auf Voreinstellungen zurücksetzen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to user theme" xml:space="preserve"> + <source>Reset to user theme</source> + <target>Auf das Benutzer-spezifische Design zurücksetzen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> <source>Restart the app to create a new chat profile</source> <target>Um ein neues Chat-Profil zu erstellen, starten Sie die App neu</target> @@ -4222,14 +6491,19 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Fehler bei der Wiederherstellung der Datenbank</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Retry" xml:space="preserve"> + <source>Retry</source> + <target>Wiederholen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> <target>Aufdecken</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Revert" xml:space="preserve"> - <source>Revert</source> - <target>Zurückkehren</target> + <trans-unit id="Review conditions" xml:space="preserve"> + <source>Review conditions</source> + <target>Nutzungsbedingungen einsehen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke" xml:space="preserve"> @@ -4257,51 +6531,67 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Chat starten</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve"> - <source>SMP servers</source> + <trans-unit id="SMP server" xml:space="preserve"> + <source>SMP server</source> <target>SMP-Server</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SOCKS proxy" xml:space="preserve"> + <source>SOCKS proxy</source> + <target>SOCKS-Proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safely receive files" xml:space="preserve"> + <source>Safely receive files</source> + <target>Dateien sicher herunterladen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safer groups" xml:space="preserve"> + <source>Safer groups</source> + <target>Sicherere Gruppen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save" xml:space="preserve"> <source>Save</source> <target>Speichern</target> - <note>chat item action</note> + <note>alert button +chat item action</note> </trans-unit> <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> <target>Speichern (und Kontakte benachrichtigen)</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> <target>Speichern und Kontakt benachrichtigen</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> <target>Speichern und Gruppenmitglieder benachrichtigen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save and reconnect" xml:space="preserve"> + <source>Save and reconnect</source> + <target>Speichern und neu verbinden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save and update group profile" xml:space="preserve"> <source>Save and update group profile</source> <target>Gruppen-Profil sichern und aktualisieren</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save archive" xml:space="preserve"> - <source>Save archive</source> - <target>Archiv speichern</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve"> - <source>Save auto-accept settings</source> - <target>Einstellungen von "Automatisch akzeptieren" speichern</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> <source>Save group profile</source> <target>Gruppenprofil speichern</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save list" xml:space="preserve"> + <source>Save list</source> + <target>Liste speichern</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> <target>Passwort speichern und Chat öffnen</target> @@ -4315,7 +6605,7 @@ Das ist Ihr Link für die Gruppe %@!</target> <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> <target>Präferenzen speichern?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> @@ -4330,23 +6620,53 @@ Das ist Ihr Link für die Gruppe %@!</target> <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> <target>Alle Server speichern?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve"> - <source>Save settings?</source> - <target>Einstellungen speichern?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> <target>Begrüßungsmeldung speichern?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save your profile?" xml:space="preserve"> + <source>Save your profile?</source> + <target>Ihr Profil speichern?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Saved" xml:space="preserve"> + <source>Saved</source> + <target>Abgespeichert</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> <source>Saved WebRTC ICE servers will be removed</source> <target>Gespeicherte WebRTC ICE-Server werden entfernt</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Saved from" xml:space="preserve"> + <source>Saved from</source> + <target>Abgespeichert von</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved message" xml:space="preserve"> + <source>Saved message</source> + <target>Gespeicherte Nachricht</target> + <note>message info title</note> + </trans-unit> + <trans-unit id="Saving %lld messages" xml:space="preserve"> + <source>Saving %lld messages</source> + <target>Es wird/werden %lld Nachricht(en) gesichert</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scale" xml:space="preserve"> + <source>Scale</source> + <target>Skalieren</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan / Paste link" xml:space="preserve"> + <source>Scan / Paste link</source> + <target>Link scannen / einfügen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> <target>QR-Code scannen</target> @@ -4377,11 +6697,31 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Suche</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Search bar accepts invitation links." xml:space="preserve"> + <source>Search bar accepts invitation links.</source> + <target>In der Suchleiste werden nun auch Einladungslinks akzeptiert.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search or paste SimpleX link" xml:space="preserve"> + <source>Search or paste SimpleX link</source> + <target>Suchen oder SimpleX-Link einfügen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secondary" xml:space="preserve"> + <source>Secondary</source> + <target>Zweite Farbe</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> <source>Secure queue</source> <target>Sichere Warteschlange</target> <note>server test step</note> </trans-unit> + <trans-unit id="Secured" xml:space="preserve"> + <source>Secured</source> + <target>Abgesichert</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> <target>Sicherheits-Gutachten</target> @@ -4395,6 +6735,21 @@ Das ist Ihr Link für die Gruppe %@!</target> <trans-unit id="Select" xml:space="preserve"> <source>Select</source> <target>Auswählen</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Select chat profile" xml:space="preserve"> + <source>Select chat profile</source> + <target>Chat-Profil auswählen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected %lld" xml:space="preserve"> + <source>Selected %lld</source> + <target>%lld ausgewählt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Diese Nachricht ist wegen der gewählten Chat-Einstellungen nicht erlaubt.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct" xml:space="preserve"> @@ -4432,11 +6787,6 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Empfangsbestätigungen senden an</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve"> - <source>Send direct message</source> - <target>Direktnachricht senden</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Send direct message to connect" xml:space="preserve"> <source>Send direct message to connect</source> <target>Eine Direktnachricht zum Verbinden senden</target> @@ -4447,6 +6797,11 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Verschwindende Nachricht senden</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send errors" xml:space="preserve"> + <source>Send errors</source> + <target>Fehler beim Senden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> <target>Link-Vorschau senden</target> @@ -4457,14 +6812,29 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Live Nachricht senden</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send message to enable calls." xml:space="preserve"> + <source>Send message to enable calls.</source> + <target>Nachricht senden, um Anrufe zu aktivieren.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source> + <target>Nachrichten werden direkt versendet, wenn die IP-Adresse geschützt ist, und Ihr oder der Zielserver kein privates Routing unterstützt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when your or destination server does not support private routing.</source> + <target>Nachrichten werden direkt versendet, wenn Ihr oder der Zielserver kein privates Routing unterstützt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> <target>Benachrichtigungen senden</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> - <source>Send notifications:</source> - <target>Benachrichtigungen senden:</target> + <trans-unit id="Send private reports" xml:space="preserve"> + <source>Send private reports</source> + <target>Private Meldungen senden</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve"> @@ -4482,10 +6852,15 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Senden Sie diese aus dem Fotoalbum oder von individuellen Tastaturen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve"> + <source>Send up to 100 last messages to new members.</source> + <target>Bis zu 100 der letzten Nachrichten an neue Gruppenmitglieder senden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> <target>Der Absender hat die Dateiübertragung abgebrochen.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> @@ -4542,6 +6917,11 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Gesendet um: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Sent directly" xml:space="preserve"> + <source>Sent directly</source> + <target>Direkt gesendet</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> <target>Datei-Ereignis wurde gesendet</target> @@ -4552,11 +6932,71 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Gesendete Nachricht</target> <note>message info title</note> </trans-unit> + <trans-unit id="Sent messages" xml:space="preserve"> + <source>Sent messages</source> + <target>Gesendete Nachrichten</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> <target>Gesendete Nachrichten werden nach der eingestellten Zeit gelöscht.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sent reply" xml:space="preserve"> + <source>Sent reply</source> + <target>Gesendete Antwort</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent total" xml:space="preserve"> + <source>Sent total</source> + <target>Summe aller gesendeten Nachrichten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent via proxy" xml:space="preserve"> + <source>Sent via proxy</source> + <target>Über einen Proxy gesendet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server" xml:space="preserve"> + <source>Server</source> + <target>Server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server added to operator %@." xml:space="preserve"> + <source>Server added to operator %@.</source> + <target>Der Server wurde dem Betreiber %@ hinzugefügt.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Server address" xml:space="preserve"> + <source>Server address</source> + <target>Server-Adresse</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings." xml:space="preserve"> + <source>Server address is incompatible with network settings.</source> + <target>Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel.</target> + <note>srv error text.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Server address is incompatible with network settings: %@.</source> + <target>Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server operator changed." xml:space="preserve"> + <source>Server operator changed.</source> + <target>Der Server-Betreiber wurde geändert.</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Server operators" xml:space="preserve"> + <source>Server operators</source> + <target>Server-Betreiber</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server protocol changed." xml:space="preserve"> + <source>Server protocol changed.</source> + <target>Das Server-Protokoll wurde geändert.</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> <target>Um Warteschlangen zu erzeugen benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort</target> @@ -4572,11 +7012,36 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Server Test ist fehlgeschlagen!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Server type" xml:space="preserve"> + <source>Server type</source> + <target>Server-Typ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server version is incompatible with network settings." xml:space="preserve"> + <source>Server version is incompatible with network settings.</source> + <target>Die Server-Version ist nicht mit den Netzwerkeinstellungen kompatibel.</target> + <note>srv error text</note> + </trans-unit> + <trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve"> + <source>Server version is incompatible with your app: %@.</source> + <target>Die Server-Version ist nicht mit Ihrer App kompatibel: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Servers" xml:space="preserve"> <source>Servers</source> <target>Server</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Servers info" xml:space="preserve"> + <source>Servers info</source> + <target>Server-Informationen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <target>Die Serverstatistiken werden zurückgesetzt. Dies kann nicht rückgängig gemacht werden!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Session code" xml:space="preserve"> <source>Session code</source> <target>Sitzungscode</target> @@ -4587,11 +7052,21 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Einen Tag festlegen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set chat name…" xml:space="preserve"> + <source>Set chat name…</source> + <target>Chat-Name festlegen…</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> <target>Kontaktname festlegen…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve"> + <source>Set default theme</source> + <target>Default-Design einstellen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> <target>Gruppen-Präferenzen einstellen</target> @@ -4602,11 +7077,21 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Anstelle der System-Authentifizierung festlegen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve"> + <source>Set message expiration in chats.</source> + <target>Verfallsdatum von Nachrichten in Chats festlegen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passcode" xml:space="preserve"> <source>Set passcode</source> <target>Zugangscode einstellen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set passphrase" xml:space="preserve"> + <source>Set passphrase</source> + <target>Passwort festlegen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> <target>Passwort für den Export festlegen</target> @@ -4627,24 +7112,55 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Einstellungen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve"> + <source>Settings were changed.</source> + <target>Die Einstellungen wurden geändert.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve"> + <source>Shape profile images</source> + <target>Form der Profil-Bilder</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share" xml:space="preserve"> <source>Share</source> <target>Teilen</target> - <note>chat item action</note> + <note>alert action +chat item action</note> </trans-unit> <trans-unit id="Share 1-time link" xml:space="preserve"> <source>Share 1-time link</source> <target>Einmal-Link teilen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share 1-time link with a friend" xml:space="preserve"> + <source>Share 1-time link with a friend</source> + <target>Den Einmal-Einladungslink mit einem Freund teilen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve"> + <source>Share SimpleX address on social media.</source> + <target>Die SimpleX-Adresse auf sozialen Medien teilen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address" xml:space="preserve"> <source>Share address</source> <target>Adresse teilen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share address publicly" xml:space="preserve"> + <source>Share address publicly</source> + <target>Die Adresse öffentlich teilen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address with contacts?" xml:space="preserve"> <source>Share address with contacts?</source> <target>Die Adresse mit Kontakten teilen?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Share from other apps." xml:space="preserve"> + <source>Share from other apps.</source> + <target>Aus anderen Apps heraus teilen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share link" xml:space="preserve"> @@ -4652,9 +7168,19 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Link teilen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve"> - <source>Share one-time invitation link</source> - <target>Einmal-Einladungslink teilen</target> + <trans-unit id="Share profile" xml:space="preserve"> + <source>Share profile</source> + <target>Profil teilen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share this 1-time invite link" xml:space="preserve"> + <source>Share this 1-time invite link</source> + <target>Teilen Sie diesen Einmal-Einladungslink</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share to SimpleX" xml:space="preserve"> + <source>Share to SimpleX</source> + <target>Mit SimpleX teilen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share with contacts" xml:space="preserve"> @@ -4662,6 +7188,16 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Mit Kontakten teilen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Short link" xml:space="preserve"> + <source>Short link</source> + <target>Verkürzter Link</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show QR code" xml:space="preserve"> + <source>Show QR code</source> + <target>QR-Code anzeigen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> <target>Anrufliste anzeigen</target> @@ -4677,21 +7213,46 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Letzte Nachrichten anzeigen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show message status" xml:space="preserve"> + <source>Show message status</source> + <target>Nachrichtenstatus anzeigen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show percentage" xml:space="preserve"> + <source>Show percentage</source> + <target>Prozentualen Anteil anzeigen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Vorschau anzeigen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show → on messages sent via private routing." xml:space="preserve"> + <source>Show → on messages sent via private routing.</source> + <target>Bei Nachrichten, die über privates Routing versendet wurden, → anzeigen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show:" xml:space="preserve"> <source>Show:</source> <target>Anzeigen:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve"> + <source>SimpleX</source> + <target>SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Address" xml:space="preserve"> <source>SimpleX Address</source> <target>SimpleX-Adresse</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." xml:space="preserve"> + <source>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</source> + <target>SimpleX-Chat und Flux haben vereinbart, die von Flux betriebenen Server in die App aufzunehmen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> <target>Die Sicherheit von SimpleX Chat wurde von Trail of Bits überprüft.</target> @@ -4722,6 +7283,21 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>SimpleX-Adresse</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX address and 1-time links are safe to share via any messenger." xml:space="preserve"> + <source>SimpleX address and 1-time links are safe to share via any messenger.</source> + <target>Die SimpleX-Adresse und Einmal-Links können sicher über beliebige Messenger geteilt werden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address or 1-time link?" xml:space="preserve"> + <source>SimpleX address or 1-time link?</source> + <target>SimpleX-Adresse oder Einmal-Link?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX channel link" xml:space="preserve"> + <source>SimpleX channel link</source> + <target>SimpleX-Kanal-Link</target> + <note>simplex link type</note> + </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> <target>SimpleX-Kontaktadressen-Link</target> @@ -4740,6 +7316,16 @@ Das ist Ihr Link für die Gruppe %@!</target> <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> <target>SimpleX-Links</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve"> + <source>SimpleX links are prohibited.</source> + <target>In dieser Gruppe sind SimpleX-Links nicht erlaubt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve"> + <source>SimpleX links not allowed</source> + <target>SimpleX-Links sind nicht erlaubt</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> @@ -4747,11 +7333,21 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>SimpleX-Einmal-Einladung</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> + <source>SimpleX protocols reviewed by Trail of Bits.</source> + <target>Die SimpleX-Protokolle wurden von Trail of Bits überprüft.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Simplified incognito mode" xml:space="preserve"> <source>Simplified incognito mode</source> <target>Vereinfachter Inkognito-Modus</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Size" xml:space="preserve"> + <source>Size</source> + <target>Größe</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Überspringen</target> @@ -4767,19 +7363,52 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Kleine Gruppen (max. 20)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Soft" xml:space="preserve"> + <source>Soft</source> + <target>Weich</target> + <note>blur media</note> + </trans-unit> + <trans-unit id="Some app settings were not migrated." xml:space="preserve"> + <source>Some app settings were not migrated.</source> + <target>Einige App-Einstellungen wurden nicht migriert.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some file(s) were not exported:" xml:space="preserve"> + <source>Some file(s) were not exported:</source> + <target>Einzelne Datei(en) wurde(n) nicht exportiert:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Während des Imports sind einige nicht schwerwiegende Fehler aufgetreten - in der Chat-Konsole finden Sie weitere Einzelheiten.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import:" xml:space="preserve"> + <source>Some non-fatal errors occurred during import:</source> + <target>Während des Imports traten ein paar nicht schwerwiegende Fehler auf:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some servers failed the test: %@" xml:space="preserve"> + <source>Some servers failed the test: +%@</source> + <target>Einige Server haben den Test nicht bestanden: +%@</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> <target>Jemand</target> <note>notification title</note> </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve"> - <source>Start a new chat</source> - <target>Starten Sie einen neuen Chat</target> + <trans-unit id="Spam" xml:space="preserve"> + <source>Spam</source> + <target>Spam</target> + <note>blocking reason +report reason</note> + </trans-unit> + <trans-unit id="Square, circle, or anything in between." xml:space="preserve"> + <source>Square, circle, or anything in between.</source> + <target>Quadratisch, kreisförmig oder irgendetwas dazwischen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start chat" xml:space="preserve"> @@ -4787,11 +7416,26 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Starten Sie den Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Start chat?" xml:space="preserve"> + <source>Start chat?</source> + <target>Chat starten?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> <target>Starten Sie die Migration</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Starting from %@." xml:space="preserve"> + <source>Starting from %@.</source> + <target>Beginnend mit %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Statistics" xml:space="preserve"> + <source>Statistics</source> + <target>Statistiken</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> <target>Beenden</target> @@ -4802,9 +7446,9 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Stoppen Sie SimpleX</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> - <source>Stop chat to enable database actions</source> - <target>Chat beenden, um Datenbankaktionen zu erlauben</target> + <trans-unit id="Stop chat" xml:space="preserve"> + <source>Stop chat</source> + <target>Chat beenden</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> @@ -4819,39 +7463,79 @@ Das ist Ihr Link für die Gruppe %@!</target> </trans-unit> <trans-unit id="Stop file" xml:space="preserve"> <source>Stop file</source> - <target>Datei beenden</target> + <target>Herunterladen beenden</target> <note>cancel file action</note> </trans-unit> <trans-unit id="Stop receiving file?" xml:space="preserve"> <source>Stop receiving file?</source> - <target>Den Empfang der Datei beenden?</target> + <target>Das Herunterladen der Datei beenden?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop sending file?" xml:space="preserve"> <source>Stop sending file?</source> - <target>Das Senden der Datei beenden?</target> + <target>Das Hochladen der Datei beenden?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop sharing" xml:space="preserve"> <source>Stop sharing</source> <target>Teilen beenden</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Stop sharing address?" xml:space="preserve"> <source>Stop sharing address?</source> <target>Das Teilen der Adresse beenden?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Stopping chat" xml:space="preserve"> + <source>Stopping chat</source> + <target>Chat wird beendet</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Storage" xml:space="preserve"> + <source>Storage</source> + <target>Ablage</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Strong" xml:space="preserve"> + <source>Strong</source> + <target>Hart</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Submit" xml:space="preserve"> <source>Submit</source> <target>Bestätigen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Subscribed" xml:space="preserve"> + <source>Subscribed</source> + <target>Abonniert</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscription errors" xml:space="preserve"> + <source>Subscription errors</source> + <target>Fehler beim Abonnieren</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscriptions ignored" xml:space="preserve"> + <source>Subscriptions ignored</source> + <target>Nicht beachtete Abonnements</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Support SimpleX Chat" xml:space="preserve"> <source>Support SimpleX Chat</source> <target>Unterstützung von SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Switch audio and video during the call." xml:space="preserve"> + <source>Switch audio and video during the call.</source> + <target>Während des Anrufs zwischen Audio und Video wechseln</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> + <source>Switch chat profile for 1-time invitations.</source> + <target>Das Chat-Profil für Einmal-Einladungen wechseln</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="System" xml:space="preserve"> <source>System</source> <target>System</target> @@ -4862,11 +7546,21 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>System-Authentifizierung</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP connection" xml:space="preserve"> + <source>TCP connection</source> + <target>TCP-Verbindung</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> <target>Timeout der TCP-Verbindung</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP port for messaging" xml:space="preserve"> + <source>TCP port for messaging</source> + <target>TCP-Port für Nachrichtenübermittlung</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <source>TCP_KEEPCNT</source> <target>TCP_KEEPCNT</target> @@ -4882,11 +7576,21 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>TCP_KEEPINTVL</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tail" xml:space="preserve"> + <source>Tail</source> + <target>Sprechblasen-Format</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> <target>Machen Sie ein Foto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap Create SimpleX address in the menu to create it later." xml:space="preserve"> + <source>Tap Create SimpleX address in the menu to create it later.</source> + <target>Tippen Sie im Menü auf SimpleX-Adresse erstellen, um sie später zu erstellen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> <target>Schaltfläche antippen </target> @@ -4894,12 +7598,12 @@ Das ist Ihr Link für die Gruppe %@!</target> </trans-unit> <trans-unit id="Tap to Connect" xml:space="preserve"> <source>Tap to Connect</source> - <target>Zum Verbinden antippen</target> + <target>Zum Verbinden tippen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to activate profile." xml:space="preserve"> <source>Tap to activate profile.</source> - <target>Tippen Sie auf das Profil um es zu aktivieren.</target> + <target>Zum Aktivieren des Profils tippen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to join" xml:space="preserve"> @@ -4909,19 +7613,34 @@ Das ist Ihr Link für die Gruppe %@!</target> </trans-unit> <trans-unit id="Tap to join incognito" xml:space="preserve"> <source>Tap to join incognito</source> - <target>Tippen, um Inkognito beizutreten</target> + <target>Zum Inkognito beitreten tippen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve"> - <source>Tap to start a new chat</source> - <target>Tippen, um einen neuen Chat zu starten</target> + <trans-unit id="Tap to paste link" xml:space="preserve"> + <source>Tap to paste link</source> + <target>Zum Link einfügen tippen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap to scan" xml:space="preserve"> + <source>Tap to scan</source> + <target>Zum Scannen tippen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Temporary file error" xml:space="preserve"> + <source>Temporary file error</source> + <target>Temporärer Datei-Fehler</target> + <note>file error alert title</note> + </trans-unit> <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> <target>Der Test ist beim Schritt %@ fehlgeschlagen.</target> <note>server test failure</note> </trans-unit> + <trans-unit id="Test notifications" xml:space="preserve"> + <source>Test notifications</source> + <target>Benachrichtigungen testen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> <target>Teste Server</target> @@ -4935,7 +7654,7 @@ Das ist Ihr Link für die Gruppe %@!</target> <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> <target>Tests sind fehlgeschlagen!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> @@ -4952,11 +7671,6 @@ Das ist Ihr Link für die Gruppe %@!</target> <target>Dank der Nutzer - Tragen Sie per Weblate bei!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> - <target>Die erste Plattform ohne Benutzerkennungen – Privat per Design.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <source>The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised.</source> @@ -4969,11 +7683,31 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro <target>Wenn sie Nachrichten oder Kontaktanfragen empfangen, kann Sie die App benachrichtigen - Um dies zu aktivieren, öffnen Sie bitte die Einstellungen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The app protects your privacy by using different operators in each conversation." xml:space="preserve"> + <source>The app protects your privacy by using different operators in each conversation.</source> + <target>Durch Verwendung verschiedener Netzwerk-Betreiber für jede Unterhaltung schützt die App Ihre Privatsphäre.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve"> + <source>The app will ask to confirm downloads from unknown file servers (except .onion).</source> + <target>Die App wird eine Bestätigung bei Downloads von unbekannten Datei-Servern anfordern (außer bei .onion).</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> <source>The attempt to change database passphrase was not completed.</source> <target>Die Änderung des Datenbank-Passworts konnte nicht abgeschlossen werden.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve"> + <source>The code you scanned is not a SimpleX link QR code.</source> + <target>Der von Ihnen gescannte Code ist kein SimpleX-Link-QR-Code.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The connection reached the limit of undelivered messages, your contact may be offline." xml:space="preserve"> + <source>The connection reached the limit of undelivered messages, your contact may be offline.</source> + <target>Diese Verbindung hat das Limit der nicht ausgelieferten Nachrichten erreicht. Ihr Kontakt ist möglicherweise offline.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> <target>Die von Ihnen akzeptierte Verbindung wird abgebrochen!</target> @@ -4994,6 +7728,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro <target>Die Verschlüsselung funktioniert und ein neues Verschlüsselungsabkommen ist nicht erforderlich. Es kann zu Verbindungsfehlern kommen!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> + <target>Die nächste Generation von privatem Messaging</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The hash of the previous message is different." xml:space="preserve"> <source>The hash of the previous message is different.</source> <target>Der Hash der vorherigen Nachricht unterscheidet sich.</target> @@ -5009,9 +7748,14 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro <target>Diese Nachricht wird für alle Mitglieder als moderiert gekennzeichnet.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> - <target>Die nächste Generation von privatem Messaging</target> + <trans-unit id="The messages will be deleted for all members." xml:space="preserve"> + <source>The messages will be deleted for all members.</source> + <target>Die Nachrichten werden für alle Gruppenmitglieder gelöscht.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve"> + <source>The messages will be marked as moderated for all members.</source> + <target>Die Nachrichten werden für alle Mitglieder als moderiert gekennzeichnet werden.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> @@ -5019,9 +7763,14 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro <target>Die alte Datenbank wurde während der Migration nicht entfernt. Sie kann gelöscht werden.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> - <target>Das Profil wird nur mit Ihren Kontakten geteilt.</target> + <trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve"> + <source>The same conditions will apply to operator **%@**.</source> + <target>Dieselben Nutzungsbedingungen gelten auch für den Betreiber **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second preset operator in the app!" xml:space="preserve"> + <source>The second preset operator in the app!</source> + <target>Der zweite voreingestellte Netzwerk-Betreiber in der App!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> @@ -5036,22 +7785,32 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro </trans-unit> <trans-unit id="The servers for new connections of your current chat profile **%@**." xml:space="preserve"> <source>The servers for new connections of your current chat profile **%@**.</source> - <target>Server der neuen Verbindungen von Ihrem aktuellen Chat-Profil **%@**.</target> + <target>Nachrichten-Server für neue Verbindungen über Ihr aktuelles Chat-Profil **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Theme" xml:space="preserve"> - <source>Theme</source> + <trans-unit id="The servers for new files of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new files of your current chat profile **%@**.</source> + <target>Medien- und Datei-Server für neue Daten über Ihr aktuelles Chat-Profil **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve"> + <source>The text you pasted is not a SimpleX link.</source> + <target>Der von Ihnen eingefügte Text ist kein SimpleX-Link.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve"> + <source>The uploaded database archive will be permanently removed from the servers.</source> + <target>Das hochgeladene Datenbank-Archiv wird dauerhaft von den Servern entfernt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Themes" xml:space="preserve"> + <source>Themes</source> <target>Design</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve"> - <source>There should be at least one user profile.</source> - <target>Es muss mindestens ein Benutzer-Profil vorhanden sein.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> - <source>There should be at least one visible user profile.</source> - <target>Es muss mindestens ein sichtbares Benutzer-Profil vorhanden sein.</target> + <trans-unit id="These conditions will also apply for: **%@**." xml:space="preserve"> + <source>These conditions will also apply for: **%@**.</source> + <target>Diese Nutzungsbedingungen gelten auch für: **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> @@ -5066,24 +7825,44 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro </trans-unit> <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> <source>This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</source> - <target>Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten.</target> + <target>Es werden alle herunter- und hochgeladenen Dateien und Medien gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. Diese Aktion kann nicht rückgängig gemacht werden!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." xml:space="preserve"> <source>This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</source> - <target>Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dieser Vorgang kann mehrere Minuten dauern.</target> + <target>Es werden alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, gelöscht. Dieser Vorgang kann mehrere Minuten dauern. Diese Aktion kann nicht rückgängig gemacht werden!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</source> + <target>Die älteren als die ausgewählten gesendeten und empfangenen Nachrichten in diesem Chat werden gelöscht. Diese Aktion kann nicht rückgängig gemacht werden!</target> + <note>alert message</note> + </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> - <target>Diese Aktion kann nicht rückgängig gemacht werden! Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</target> + <target>Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. Diese Aktion kann nicht rückgängig gemacht werden!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by end-to-end encryption.</source> + <target>Dieser Chat ist durch Ende-zu-Ende-Verschlüsselung geschützt.</target> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by quantum resistant end-to-end encryption.</source> + <target>Dieser Chat ist durch Quantum-resistente Ende-zu-Ende-Verschlüsselung geschützt.</target> + <note>E2EE info chat item</note> + </trans-unit> <trans-unit id="This device name" xml:space="preserve"> <source>This device name</source> <target>Dieser Gerätename</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve"> + <source>This display name is invalid. Please choose another name.</source> + <target>Der Anzeigename ist ungültig. Bitte wählen Sie einen anderen Namen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> <source>This group has over %lld members, delivery receipts are not sent.</source> <target>Es werden keine Empfangsbestätigungen gesendet, da diese Gruppe über %lld Mitglieder hat.</target> @@ -5104,14 +7883,34 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro <target>Das ist Ihr eigener Einmal-Link!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve"> + <source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source> + <target>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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve"> + <source>This link was used with another mobile device, please create a new link on the desktop.</source> + <target>Dieser Link wurde schon mit einem anderen Mobiltelefon genutzt. Bitte erstellen sie einen neuen Link in der Desktop-App.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This message was deleted or not received yet." xml:space="preserve"> + <source>This message was deleted or not received yet.</source> + <target>Diese Nachricht wurde gelöscht oder bisher noch nicht empfangen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <source>This setting applies to messages in your current chat profile **%@**.</source> <target>Diese Einstellung gilt für Nachrichten in Ihrem aktuellen Chat-Profil **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Title" xml:space="preserve"> + <source>Title</source> + <target>Bezeichnung</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> <source>To ask any questions and to receive updates:</source> - <target>Um Fragen zu stellen und Aktualisierungen zu erhalten:</target> + <target>Um Fragen zu stellen und aktuelle Informationen zu erhalten:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To connect, your contact can scan QR code or use the link in the app." xml:space="preserve"> @@ -5129,9 +7928,9 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro <target>Um eine Verbindung mit einem neuen Kontakt zu erstellen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <target>Zum Schutz Ihrer Privatsphäre verwendet SimpleX an Stelle von Benutzerkennungen, die von allen anderen Plattformen verwendet werden, Kennungen für Nachrichtenwarteschlangen, die für jeden Ihrer Kontakte individuell sind.</target> + <trans-unit id="To protect against your link being replaced, you can compare contact security codes." xml:space="preserve"> + <source>To protect against your link being replaced, you can compare contact security codes.</source> + <target>Zum Schutz vor dem Austausch Ihres Links können Sie die Sicherheitscodes Ihrer Kontakte vergleichen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -5139,6 +7938,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro <target>Bild- und Sprachdateinamen enthalten UTC, um Informationen zur Zeitzone zu schützen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve"> + <source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source> + <target>Zum Schutz Ihrer IP-Adresse, wird für die Nachrichten-Auslieferung privates Routing über Ihre konfigurierten SMP-Server genutzt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> @@ -5146,6 +7950,26 @@ You will be prompted to complete authentication before this feature is enabled.< Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funktion aktiviert wird.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> + <target>Zum Schutz Ihrer Privatsphäre verwendet SimpleX an Stelle von Benutzerkennungen, die von allen anderen Plattformen verwendet werden, Kennungen für Nachrichtenwarteschlangen, die für jeden Ihrer Kontakte individuell sind.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To receive" xml:space="preserve"> + <source>To receive</source> + <target>Für den Empfang</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve"> + <source>To record speech please grant permission to use Microphone.</source> + <target>Bitte erteilen Sie für Sprach-Aufnahmen die Genehmigung das Mikrofon zu nutzen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record video please grant permission to use Camera." xml:space="preserve"> + <source>To record video please grant permission to use Camera.</source> + <target>Bitte erteilen Sie für Video-Aufnahmen die Genehmigung die Kamera zu nutzen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> <target>Bitte erlauben Sie die Nutzung des Mikrofons, um Sprachnachrichten aufnehmen zu können.</target> @@ -5156,26 +7980,61 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt <target>Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite **Ihre Chat-Profile** ein, um Ihr verborgenes Profil zu sehen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To send" xml:space="preserve"> + <source>To send</source> + <target>Für das Senden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> <source>To support instant push notifications the chat database has to be migrated.</source> <target>Um sofortige Push-Benachrichtigungen zu unterstützen, muss die Chat-Datenbank migriert werden.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To use the servers of **%@**, accept conditions of use." xml:space="preserve"> + <source>To use the servers of **%@**, accept conditions of use.</source> + <target>Um die Server von **%@** zu nutzen, müssen Sie dessen Nutzungsbedingungen akzeptieren.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> <target>Um die Ende-zu-Ende-Verschlüsselung mit Ihrem Kontakt zu überprüfen, müssen Sie den Sicherheitscode in Ihren Apps vergleichen oder scannen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle chat list:" xml:space="preserve"> + <source>Toggle chat list:</source> + <target>Chat-Liste umschalten:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> <source>Toggle incognito when connecting.</source> <target>Inkognito beim Verbinden einschalten.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Token status: %@." xml:space="preserve"> + <source>Token status: %@.</source> + <target>Token-Status: %@.</target> + <note>token status</note> + </trans-unit> + <trans-unit id="Toolbar opacity" xml:space="preserve"> + <source>Toolbar opacity</source> + <target>Deckkraft der Symbolleiste</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Total" xml:space="preserve"> + <source>Total</source> + <target>Summe aller Abonnements</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>Transport-Isolation</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Transport sessions" xml:space="preserve"> + <source>Transport sessions</source> + <target>Transport-Sitzungen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> <target>Beim Versuch die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: %@).</target> @@ -5186,16 +8045,16 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt <target>Versuche die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Turkish interface" xml:space="preserve"> + <source>Turkish interface</source> + <target>Türkische Bedienoberfläche</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> <target>Abschalten</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve"> - <source>Turn off notifications?</source> - <target>Benachrichtigungen abschalten?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> <target>Einschalten</target> @@ -5211,20 +8070,30 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt <target>Freigeben</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock for all" xml:space="preserve"> + <source>Unblock for all</source> + <target>Für Alle freigeben</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member" xml:space="preserve"> <source>Unblock member</source> <target>Mitglied freigeben</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock member for all?" xml:space="preserve"> + <source>Unblock member for all?</source> + <target>Mitglied für Alle freigeben?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member?" xml:space="preserve"> <source>Unblock member?</source> <target>Mitglied freigeben?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve"> - <source>Unexpected error: %@</source> - <target>Unerwarteter Fehler: %@</target> - <note>item status description</note> + <trans-unit id="Undelivered messages" xml:space="preserve"> + <source>Undelivered messages</source> + <target>Nicht ausgelieferte Nachrichten</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -5234,7 +8103,7 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt <trans-unit id="Unfav." xml:space="preserve"> <source>Unfav.</source> <target>Fav. entf.</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> @@ -5271,6 +8140,11 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt <target>Unbekannter Fehler</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unknown servers!" xml:space="preserve"> + <source>Unknown servers!</source> + <target>Unbekannte Server!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> <target>Aktivieren Sie den Modus "Bitte nicht stören", um Unterbrechungen zu vermeiden, es sei denn, Sie verwenden die iOS Anrufschnittstelle.</target> @@ -5306,11 +8180,21 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> <target>Stummschaltung aufheben</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> </trans-unit> <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> <target>Ungelesen</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Unsupported connection link" xml:space="preserve"> + <source>Unsupported connection link</source> + <target>Verbindungs-Link wird nicht unterstützt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve"> + <source>Up to 100 last messages are sent to new members.</source> + <target>Bis zu 100 der letzten Nachrichten werden an neue Mitglieder gesendet.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update" xml:space="preserve"> @@ -5318,11 +8202,6 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s <target>Aktualisieren</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> - <source>Update .onion hosts setting?</source> - <target>Einstellung für .onion-Hosts aktualisieren?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> <target>Datenbank-Passwort aktualisieren</target> @@ -5333,9 +8212,14 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s <target>Netzwerkeinstellungen aktualisieren?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve"> - <source>Update transport isolation mode?</source> - <target>Transport-Isolations-Modus aktualisieren?</target> + <trans-unit id="Update settings?" xml:space="preserve"> + <source>Update settings?</source> + <target>Einstellungen aktualisieren?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updated conditions" xml:space="preserve"> + <source>Updated conditions</source> + <target>Aktualisierte Nutzungsbedingungen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> @@ -5343,31 +8227,71 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s <target>Die Aktualisierung der Einstellungen wird den Client wieder mit allen Servern verbinden.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> - <target>Die Aktualisierung dieser Einstellung wird den Client wieder mit allen Servern verbinden.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Upgrade and open chat" xml:space="preserve"> <source>Upgrade and open chat</source> <target>Aktualisieren und den Chat öffnen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Upload errors" xml:space="preserve"> + <source>Upload errors</source> + <target>Fehler beim Hochladen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload failed" xml:space="preserve"> + <source>Upload failed</source> + <target>Hochladen fehlgeschlagen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Upload file" xml:space="preserve"> <source>Upload file</source> <target>Datei hochladen</target> <note>server test step</note> </trans-unit> + <trans-unit id="Uploaded" xml:space="preserve"> + <source>Uploaded</source> + <target>Hochgeladen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploaded files" xml:space="preserve"> + <source>Uploaded files</source> + <target>Hochgeladene Dateien</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploading archive" xml:space="preserve"> + <source>Uploading archive</source> + <target>Archiv wird hochgeladen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use %@" xml:space="preserve"> + <source>Use %@</source> + <target>Verwende %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> <target>Verwende .onion-Hosts</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use SOCKS proxy" xml:space="preserve"> + <source>Use SOCKS proxy</source> + <target>SOCKS-Proxy nutzen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> <target>Verwenden Sie SimpleX-Chat-Server?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use TCP port %@ when no port is specified." xml:space="preserve"> + <source>Use TCP port %@ when no port is specified.</source> + <target>Solange kein Port konfiguriert ist, wird TCP-Port %@ genutzt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve"> + <source>Use TCP port 443 for preset servers only.</source> + <target>TCP-Port 443 nur für voreingestellte Server verwenden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> <target>Verwenden Sie Chat</target> @@ -5375,7 +8299,17 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s </trans-unit> <trans-unit id="Use current profile" xml:space="preserve"> <source>Use current profile</source> - <target>Nutzen Sie das aktuelle Profil</target> + <target>Aktuelles Profil nutzen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for files" xml:space="preserve"> + <source>Use for files</source> + <target>Für Dateien verwenden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for messages" xml:space="preserve"> + <source>Use for messages</source> + <target>Für Nachrichten verwenden</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> @@ -5395,7 +8329,22 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s </trans-unit> <trans-unit id="Use new incognito profile" xml:space="preserve"> <source>Use new incognito profile</source> - <target>Nutzen Sie das neue Inkognito-Profil</target> + <target>Neues Inkognito-Profil nutzen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use only local notifications?" xml:space="preserve"> + <source>Use only local notifications?</source> + <target>Nur lokale Benachrichtigungen nutzen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve"> + <source>Use private routing with unknown servers when IP address is not protected.</source> + <target>Sie nutzen privates Routing mit unbekannten Servern, wenn Ihre IP-Adresse nicht geschützt ist.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers." xml:space="preserve"> + <source>Use private routing with unknown servers.</source> + <target>Sie nutzen privates Routing mit unbekannten Servern.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use server" xml:space="preserve"> @@ -5403,14 +8352,39 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s <target>Server nutzen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> - <target>Benutzerprofil</target> + <trans-unit id="Use servers" xml:space="preserve"> + <source>Use servers</source> + <target>Verwende Server</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> - <target>Für die Nutzung von .onion-Hosts sind kompatible VPN-Anbieter erforderlich.</target> + <trans-unit id="Use short links (BETA)" xml:space="preserve"> + <source>Use short links (BETA)</source> + <target>Kurze Links verwenden (BETA)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app while in the call." xml:space="preserve"> + <source>Use the app while in the call.</source> + <target>Die App kann während eines Anrufs genutzt werden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app with one hand." xml:space="preserve"> + <source>Use the app with one hand.</source> + <target>Die App mit einer Hand bedienen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use web port" xml:space="preserve"> + <source>Use web port</source> + <target>Web-Port nutzen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="User selection" xml:space="preserve"> + <source>User selection</source> + <target>Benutzer-Auswahl</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Username" xml:space="preserve"> + <source>Username</source> + <target>Benutzername</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> @@ -5438,6 +8412,16 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s <target>Verbindungen überprüfen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Verify database passphrase" xml:space="preserve"> + <source>Verify database passphrase</source> + <target>Überprüfen Sie das Datenbank-Passwort</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify passphrase" xml:space="preserve"> + <source>Verify passphrase</source> + <target>Überprüfen Sie das Passwort</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Verify security code" xml:space="preserve"> <source>Verify security code</source> <target>Sicherheitscode überprüfen</target> @@ -5460,12 +8444,12 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s </trans-unit> <trans-unit id="Video will be received when your contact completes uploading it." xml:space="preserve"> <source>Video will be received when your contact completes uploading it.</source> - <target>Das Video wird empfangen, sobald Ihr Kontakt das Hochladen beendet hat.</target> + <target>Das Video wird heruntergeladen, sobald Ihr Kontakt das Hochladen beendet hat.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Video will be received when your contact is online, please wait or check later!" xml:space="preserve"> <source>Video will be received when your contact is online, please wait or check later!</source> - <target>Das Video wird empfangen, wenn Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später!</target> + <target>Das Video wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Videos and files up to 1gb" xml:space="preserve"> @@ -5473,11 +8457,26 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s <target>Videos und Dateien bis zu 1GB</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View conditions" xml:space="preserve"> + <source>View conditions</source> + <target>Nutzungsbedingungen anschauen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> <target>Schauen Sie sich den Sicherheitscode an</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View updated conditions" xml:space="preserve"> + <source>View updated conditions</source> + <target>Aktualisierte Nutzungsbedingungen anschauen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Visible history" xml:space="preserve"> + <source>Visible history</source> + <target>Sichtbarer Nachrichtenverlauf</target> + <note>chat feature</note> + </trans-unit> <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> <target>Sprachnachrichten</target> @@ -5488,11 +8487,16 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s <target>In diesem Chat sind Sprachnachrichten nicht erlaubt.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <target>In dieser Gruppe sind Sprachnachrichten nicht erlaubt.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Voice messages not allowed" xml:space="preserve"> + <source>Voice messages not allowed</source> + <target>Sprachnachrichten sind nicht erlaubt</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> <source>Voice messages prohibited!</source> <target>Sprachnachrichten sind nicht erlaubt!</target> @@ -5523,6 +8527,21 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s <target>Auf das Video warten</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wallpaper accent" xml:space="preserve"> + <source>Wallpaper accent</source> + <target>Wallpaper-Akzent</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper background" xml:space="preserve"> + <source>Wallpaper background</source> + <target>Wallpaper-Hintergrund</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve"> + <source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source> + <target>Warnung: Das Starten des Chats auf mehreren Geräten wird nicht unterstützt und wird zu Fehlern bei der Nachrichtenübermittlung führen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> <source>Warning: you may lose some data!</source> <target>Warnung: Sie könnten einige Daten verlieren!</target> @@ -5543,6 +8562,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s <target>Begrüßungsmeldung</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Welcome message is too long" xml:space="preserve"> + <source>Welcome message is too long</source> + <target>Die Begrüßungsmeldung ist zu lang</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="What's new" xml:space="preserve"> <source>What's new</source> <target>Was ist neu</target> @@ -5553,9 +8577,14 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s <target>Wenn verfügbar</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> - <source>When people request to connect, you can accept or reject it.</source> - <target>Wenn Personen eine Verbindung anfordern, können Sie diese annehmen oder ablehnen.</target> + <trans-unit id="When connecting audio and video calls." xml:space="preserve"> + <source>When connecting audio and video calls.</source> + <target>Bei der Verbindung über Audio- und Video-Anrufe.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When more than one operator is enabled, none of them has metadata to learn who communicates with whom." xml:space="preserve"> + <source>When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</source> + <target>Wenn mehrere Netzwerk-Betreiber aktiviert sind, hat keiner von ihnen Metadaten, um zu erfahren, wer mit wem kommuniziert.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> @@ -5563,29 +8592,74 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s <target>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.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="WiFi" xml:space="preserve"> + <source>WiFi</source> + <target>WiFi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Will be enabled in direct chats!" xml:space="preserve"> + <source>Will be enabled in direct chats!</source> + <target>Wird in direkten Chats automatisch aktiviert!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wired ethernet" xml:space="preserve"> + <source>Wired ethernet</source> + <target>Kabelgebundenes Netzwerk</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With encrypted files and media." xml:space="preserve"> + <source>With encrypted files and media.</source> + <target>Mit verschlüsselten Dateien und Medien.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> <target>Mit optionaler Begrüßungsmeldung.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="With reduced battery usage." xml:space="preserve"> + <source>With reduced battery usage.</source> + <target>Mit reduziertem Akkuverbrauch.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to file servers.</source> + <target>Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für Datei-Server sichtbar sein.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source> + <target>Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für diese XFTP-Relais sichtbar sein: %@.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> <target>Falsches Datenbank-Passwort</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve"> + <source>Wrong key or unknown connection - most likely this connection is deleted.</source> + <target>Falscher Schlüssel oder unbekannte Verbindung - höchstwahrscheinlich ist diese Verbindung gelöscht worden.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Wrong key or unknown file chunk address - most likely file is deleted." xml:space="preserve"> + <source>Wrong key or unknown file chunk address - most likely file is deleted.</source> + <target>Falscher Schlüssel oder unbekannte Daten-Paketadresse der Datei - höchstwahrscheinlich wurde die Datei gelöscht.</target> + <note>file error text</note> + </trans-unit> <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> <target>Falsches Passwort!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="XFTP servers" xml:space="preserve"> - <source>XFTP servers</source> + <trans-unit id="XFTP server" xml:space="preserve"> + <source>XFTP server</source> <target>XFTP-Server</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> - <target>Ihre Daten</target> + <trans-unit id="You **must not** use the same database on two devices." xml:space="preserve"> + <source>You **must not** use the same database on two devices.</source> + <target>Sie dürfen die selbe Datenbank **nicht** auf zwei Geräten nutzen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You accepted connection" xml:space="preserve"> @@ -5608,6 +8682,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s <target>Sie sind bereits mit %@ verbunden.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are already connected with %@." xml:space="preserve"> + <source>You are already connected with %@.</source> + <target>Sie sind bereits mit %@ verbunden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You are already connecting to %@." xml:space="preserve"> <source>You are already connecting to %@.</source> <target>Sie sind bereits mit %@ verbunden.</target> @@ -5655,14 +8734,24 @@ Verbindungsanfrage wiederholen?</target> <target>Sie sind zu der Gruppe eingeladen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are not connected to these servers. Private routing is used to deliver messages to them." xml:space="preserve"> + <source>You are not connected to these servers. Private routing is used to deliver messages to them.</source> + <target>Sie sind nicht mit diesen Servern verbunden. Zur Auslieferung von Nachrichten an diese Server wird privates Routing genutzt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> <source>You can accept calls from lock screen, without device and app authentication.</source> <target>Sie können Anrufe ohne Geräte- und App-Authentifizierung vom Sperrbildschirm aus annehmen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <target>Sie können sich auch verbinden, indem Sie auf den Link klicken. Wenn er im Browser geöffnet wird, klicken Sie auf die Schaltfläche **In mobiler App öffnen**.</target> + <trans-unit id="You can change it in Appearance settings." xml:space="preserve"> + <source>You can change it in Appearance settings.</source> + <target>Kann von Ihnen in den Erscheinungsbild-Einstellungen geändert werden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can configure servers via settings." xml:space="preserve"> + <source>You can configure servers via settings.</source> + <target>Sie können die Server über die Einstellungen konfigurieren.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can create it later" xml:space="preserve"> @@ -5680,16 +8769,36 @@ Verbindungsanfrage wiederholen?</target> <target>Sie können diese später in den Datenschutz & Sicherheits-Einstellungen der App aktivieren.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can give another try." xml:space="preserve"> + <source>You can give another try.</source> + <target>Sie können es nochmal probieren.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <source>You can hide or mute a user profile - swipe it to the right.</source> <target>Sie können ein Benutzerprofil verbergen oder stummschalten - wischen Sie es nach rechts.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> + <trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve"> + <source>You can make it visible to your SimpleX contacts via Settings.</source> + <target>Sie können sie über Einstellungen für Ihre SimpleX-Kontakte sichtbar machen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now chat with %@" xml:space="preserve"> + <source>You can now chat with %@</source> <target>Sie können nun Nachrichten an %@ versenden</target> <note>notification body</note> </trans-unit> + <trans-unit id="You can send messages to %@ from Archived contacts." xml:space="preserve"> + <source>You can send messages to %@ from Archived contacts.</source> + <target>Sie können aus den archivierten Kontakten heraus Nachrichten an %@ versenden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can set connection name, to remember who the link was shared with." xml:space="preserve"> + <source>You can set connection name, to remember who the link was shared with.</source> + <target>Sie können einen Verbindungsnamen festlegen, um sich zu merken, mit wem der Link geteilt wurde.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> <target>Über die Geräte-Einstellungen können Sie die Benachrichtigungsvorschau im Sperrbildschirm erlauben.</target> @@ -5705,16 +8814,16 @@ Verbindungsanfrage wiederholen?</target> <target>Sie können diese Adresse mit Ihren Kontakten teilen, um sie mit **%@** verbinden zu lassen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> - <source>You can share your address as a link or QR code - anybody can connect to you.</source> - <target>Sie können Ihre Adresse als Link oder als QR-Code teilen – Jede Person kann sich darüber mit Ihnen verbinden.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> <target>Sie können den Chat über die App-Einstellungen / Datenbank oder durch Neustart der App starten</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve"> + <source>You can still view conversation with %@ in the list of chats.</source> + <target>Sie können in der Chat-Liste weiterhin die Unterhaltung mit %@ einsehen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> <target>Sie können die SimpleX-Sperre über die Einstellungen aktivieren.</target> @@ -5725,21 +8834,26 @@ Verbindungsanfrage wiederholen?</target> <target>Um Nachrichteninhalte zu formatieren, können Sie Markdowns verwenden:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can view invitation link again in connection details." xml:space="preserve"> + <source>You can view invitation link again in connection details.</source> + <target>Den Einladungslink können Sie in den Details der Verbindung nochmals sehen.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> <target>Sie können keine Nachrichten versenden!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <target>Sie können selbst festlegen, über welche Server Sie Ihre Nachrichten **empfangen** und an Ihre Kontakte **senden** wollen.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <target>Sie konnten nicht überprüft werden; bitte versuchen Sie es erneut.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> + <target>Sie entscheiden, wer sich mit Ihnen verbinden kann.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You have already requested connection via this address!" xml:space="preserve"> <source>You have already requested connection via this address!</source> <target>Sie haben über diese Adresse bereits eine Verbindung beantragt!</target> @@ -5752,11 +8866,6 @@ Repeat connection request?</source> Verbindungsanfrage wiederholen?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> - <target>Sie haben keine Chats</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> <target>Sie müssen das Passwort jedes Mal eingeben, wenn die App startet. Es wird nicht auf dem Gerät gespeichert.</target> @@ -5777,11 +8886,26 @@ Verbindungsanfrage wiederholen?</target> <target>Sie sind dieser Gruppe beigetreten. Sie werden mit dem einladenden Gruppenmitglied verbunden.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You may migrate the exported database." xml:space="preserve"> + <source>You may migrate the exported database.</source> + <target>Sie können die exportierte Datenbank migrieren.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may save the exported archive." xml:space="preserve"> + <source>You may save the exported archive.</source> + <target>Sie können das exportierte Archiv speichern.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> <target>Sie dürfen die neueste Version Ihrer Chat-Datenbank NUR auf einem Gerät verwenden, andernfalls erhalten Sie möglicherweise keine Nachrichten mehr von einigen Ihrer Kontakte.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You need to allow your contact to call to be able to call them." xml:space="preserve"> + <source>You need to allow your contact to call to be able to call them.</source> + <target>Sie müssen Ihrem Kontakt Anrufe zu Ihnen erlauben, bevor Sie ihn selbst anrufen können.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> <target>Sie müssen Ihrem Kontakt das Senden von Sprachnachrichten erlauben, um diese senden zu können.</target> @@ -5797,6 +8921,11 @@ Verbindungsanfrage wiederholen?</target> <target>Sie haben eine Gruppeneinladung gesendet</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You should receive notifications." xml:space="preserve"> + <source>You should receive notifications.</source> + <target>Sie sollten Benachrichtigungen erhalten.</target> + <note>token info</note> + </trans-unit> <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> <target>Sie werden mit der Gruppe verbunden, sobald das Endgerät des Gruppen-Hosts online ist. Bitte warten oder schauen Sie später nochmal nach!</target> @@ -5832,14 +8961,19 @@ Verbindungsanfrage wiederholen?</target> <target>Sie können Anrufe und Benachrichtigungen auch von stummgeschalteten Profilen empfangen, solange diese aktiv sind.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You will stop receiving messages from this chat. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this chat. Chat history will be preserved.</source> + <target>Sie werden von diesem Chat keine Nachrichten mehr erhalten. Der Nachrichtenverlauf bleibt erhalten.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> - <target>Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Chatverlauf wird beibehalten.</target> + <target>Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Nachrichtenverlauf wird beibehalten.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You won't lose your contacts if you later delete your address." xml:space="preserve"> <source>You won't lose your contacts if you later delete your address.</source> - <target>Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.</target> + <target>Sie werden Ihre damit verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" xml:space="preserve"> @@ -5852,31 +8986,16 @@ Verbindungsanfrage wiederholen?</target> <target>Sie verwenden ein Inkognito-Profil für diese Gruppe. Um zu verhindern, dass Sie Ihr Hauptprofil teilen, ist in diesem Fall das Einladen von Kontakten nicht erlaubt</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your %@ servers" xml:space="preserve"> - <source>Your %@ servers</source> - <target>Ihre %@-Server</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your ICE servers" xml:space="preserve"> <source>Your ICE servers</source> <target>Ihre ICE-Server</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> - <target>Ihre SMP-Server</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your SimpleX address" xml:space="preserve"> <source>Your SimpleX address</source> <target>Ihre SimpleX-Adresse</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your XFTP servers" xml:space="preserve"> - <source>Your XFTP servers</source> - <target>Ihre XFTP-Server</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your calls" xml:space="preserve"> <source>Your calls</source> <target>Anrufe</target> @@ -5892,16 +9011,19 @@ Verbindungsanfrage wiederholen?</target> <target>Ihre Chat-Datenbank ist nicht verschlüsselt. Bitte legen Sie ein Passwort fest, um sie zu schützen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your chat preferences" xml:space="preserve"> + <source>Your chat preferences</source> + <target>Ihre Chat-Präferenzen</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>Ihre Chat-Profile</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> - <target>Damit die Verbindung hergestellt werden kann, muss Ihr Kontakt online sein. -Sie können diese Verbindung abbrechen und den Kontakt entfernen (und es später nochmals mit einem neuen Link versuchen).</target> + <trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve"> + <source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source> + <target>Ihre Verbindung wurde auf %@ verschoben. Während Sie auf das Profil weitergeleitet wurden trat aber ein unerwarteter Fehler auf.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> @@ -5914,16 +9036,14 @@ Sie können diese Verbindung abbrechen und den Kontakt entfernen (und es später <target>Ihre Kontakte können die unwiederbringliche Löschung von Nachrichten erlauben.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> - <source>Your contacts in SimpleX will see it. -You can change it in Settings.</source> - <target>Ihre Kontakte in SimpleX werden es sehen. -Sie können es in den Einstellungen ändern.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your contacts will remain connected." xml:space="preserve"> <source>Your contacts will remain connected.</source> - <target>Ihre Kontakte bleiben verbunden.</target> + <target>Ihre Kontakte bleiben weiterhin verbunden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve"> + <source>Your credentials may be sent unencrypted.</source> + <target>Ihre Anmeldeinformationen können unverschlüsselt versendet werden.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> @@ -5943,7 +9063,7 @@ Sie können es in den Einstellungen ändern.</target> </trans-unit> <trans-unit id="Your privacy" xml:space="preserve"> <source>Your privacy</source> - <target>Ihre Privatsphäre</target> + <target>Privatsphäre</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your profile" xml:space="preserve"> @@ -5956,36 +9076,39 @@ Sie können es in den Einstellungen ändern.</target> <target>Ihr Profil **%@** wird geteilt.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> - <target>Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt. -SimpleX-Server können Ihr Profil nicht einsehen.</target> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> + <target>Das Profil wird nur mit Ihren Kontakten geteilt.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> - <target>Ihr Profil, Ihre Kontakte und zugestellten Nachrichten werden auf Ihrem Gerät gespeichert.</target> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> + <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> + <target>Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt. SimpleX-Server können Ihr Profil nicht einsehen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve"> + <source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source> + <target>Ihr Profil wurde geändert. Wenn Sie es speichern, wird das aktualisierte Profil an alle Ihre Kontakte gesendet.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> <target>Ihr Zufallsprofil</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> - <target>Ihr Server</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> <target>Ihre Serveradresse</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your servers" xml:space="preserve"> + <source>Your servers</source> + <target>Ihre Server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your settings" xml:space="preserve"> <source>Your settings</source> - <target>Ihre Einstellungen</target> + <target>Einstellungen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" xml:space="preserve"> @@ -6023,11 +9146,21 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Anruf angenommen</target> <note>call status</note> </trans-unit> + <trans-unit id="accepted invitation" xml:space="preserve"> + <source>accepted invitation</source> + <target>Einladung angenommen</target> + <note>chat list item title</note> + </trans-unit> <trans-unit id="admin" xml:space="preserve"> <source>admin</source> <target>Admin</target> <note>member role</note> </trans-unit> + <trans-unit id="admins" xml:space="preserve"> + <source>admins</source> + <target>Administratoren</target> + <note>feature role</note> + </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> <target>Verschlüsselung von %@ zustimmen…</target> @@ -6038,6 +9171,11 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Verschlüsselung zustimmen…</target> <note>chat item text</note> </trans-unit> + <trans-unit id="all members" xml:space="preserve"> + <source>all members</source> + <target>Alle Mitglieder</target> + <note>feature role</note> + </trans-unit> <trans-unit id="always" xml:space="preserve"> <source>always</source> <target>Immer</target> @@ -6048,6 +9186,16 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>und %lld weitere Ereignisse</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="archived report" xml:space="preserve"> + <source>archived report</source> + <target>Archivierte Meldung</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="attempts" xml:space="preserve"> + <source>attempts</source> + <target>Versuche</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <source>audio call (not e2e encrypted)</source> <target>Audioanruf (nicht E2E verschlüsselt)</target> @@ -6070,14 +9218,30 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> </trans-unit> <trans-unit id="blocked" xml:space="preserve"> <source>blocked</source> - <target>blockiert</target> - <note>No comment provided by engineer.</note> + <target>Blockiert</target> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="blocked %@" xml:space="preserve"> + <source>blocked %@</source> + <target>%@ wurde blockiert</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="blocked by admin" xml:space="preserve"> + <source>blocked by admin</source> + <target>wurde vom Administrator blockiert</target> + <note>blocked chat item +marked deleted chat item preview text</note> </trans-unit> <trans-unit id="bold" xml:space="preserve"> <source>bold</source> <target>fett</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="call" xml:space="preserve"> + <source>call</source> + <target>Anrufen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="call error" xml:space="preserve"> <source>call error</source> <target>Fehler bei Anruf</target> @@ -6100,7 +9264,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> </trans-unit> <trans-unit id="changed address for you" xml:space="preserve"> <source>changed address for you</source> - <target>wechselte die Adresse für Sie</target> + <target>Wechselte die Empfängeradresse von Ihnen</target> <note>chat item text</note> </trans-unit> <trans-unit id="changed role of %@ to %@" xml:space="preserve"> @@ -6115,12 +9279,12 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> </trans-unit> <trans-unit id="changing address for %@…" xml:space="preserve"> <source>changing address for %@…</source> - <target>Adresse von %@ wechseln…</target> + <target>Empfängeradresse für %@ wechseln wird gestartet…</target> <note>chat item text</note> </trans-unit> <trans-unit id="changing address…" xml:space="preserve"> <source>changing address…</source> - <target>Wechsel der Adresse…</target> + <target>Wechsel der Empfängeradresse wurde gestartet…</target> <note>chat item text</note> </trans-unit> <trans-unit id="colored" xml:space="preserve"> @@ -6181,7 +9345,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> <target>Verbinde…</target> - <note>chat list item title</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> @@ -6193,6 +9357,11 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Verbindung:%@</target> <note>connection information</note> </trans-unit> + <trans-unit id="contact %@ changed to %@" xml:space="preserve"> + <source>contact %1$@ changed to %2$@</source> + <target>Der Kontaktname wurde von %1$@ auf %2$@ geändert</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> <target>Kontakt nutzt E2E-Verschlüsselung</target> @@ -6223,10 +9392,16 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Tage</target> <note>time unit</note> </trans-unit> + <trans-unit id="decryption errors" xml:space="preserve"> + <source>decryption errors</source> + <target>Entschlüsselungs-Fehler</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> - <target>Voreinstellung (%@)</target> - <note>pref value</note> + <target>Default (%@)</target> + <note>delete after time +pref value</note> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> @@ -6273,6 +9448,11 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Doppelte Nachricht</target> <note>integrity error chat item</note> </trans-unit> + <trans-unit id="duplicates" xml:space="preserve"> + <source>duplicates</source> + <target>Duplikate</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> <target>E2E-verschlüsselt</target> @@ -6348,9 +9528,14 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Fehler</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="event happened" xml:space="preserve"> - <source>event happened</source> - <target>event happened</target> + <trans-unit id="expired" xml:space="preserve"> + <source>expired</source> + <target>Abgelaufen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="forwarded" xml:space="preserve"> + <source>forwarded</source> + <target>weitergeleitet</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> @@ -6378,6 +9563,11 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Für die sichere Speicherung des Passworts nach dem Neustart der App und dem Wechsel des Passworts wird der iOS Schlüsselbund verwendet - dies erlaubt den Empfang von Push-Benachrichtigungen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="inactive" xml:space="preserve"> + <source>inactive</source> + <target>Inaktiv</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> <target>Inkognito über einen Kontaktadressen-Link</target> @@ -6418,6 +9608,11 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Einladung zur Gruppe %@</target> <note>group name</note> </trans-unit> + <trans-unit id="invite" xml:space="preserve"> + <source>invite</source> + <target>Einladen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> <target>eingeladen</target> @@ -6463,11 +9658,21 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Mitglied</target> <note>member role</note> </trans-unit> + <trans-unit id="member %@ changed to %@" xml:space="preserve"> + <source>member %1$@ changed to %2$@</source> + <target>Der Mitgliedsname von %1$@ wurde auf %2$@ geändert</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> <target>ist der Gruppe beigetreten</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="message" xml:space="preserve"> + <source>message</source> + <target>Nachricht</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="message received" xml:space="preserve"> <source>message received</source> <target>Nachricht empfangen</target> @@ -6491,7 +9696,12 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <trans-unit id="moderated by %@" xml:space="preserve"> <source>moderated by %@</source> <target>Von %@ moderiert</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="moderator" xml:space="preserve"> + <source>moderator</source> + <target>Moderator</target> + <note>member role</note> </trans-unit> <trans-unit id="months" xml:space="preserve"> <source>months</source> @@ -6501,7 +9711,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <trans-unit id="never" xml:space="preserve"> <source>never</source> <target>nie</target> - <note>No comment provided by engineer.</note> + <note>delete after time</note> </trans-unit> <trans-unit id="new message" xml:space="preserve"> <source>new message</source> @@ -6532,8 +9742,8 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <source>off</source> <target>Aus</target> <note>enabled status - group pref value - time to disappear</note> +group pref value +time to disappear</note> </trans-unit> <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> @@ -6550,16 +9760,46 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Ein</target> <note>group pref value</note> </trans-unit> + <trans-unit id="other" xml:space="preserve"> + <source>other</source> + <target>Andere</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="other errors" xml:space="preserve"> + <source>other errors</source> + <target>Andere Fehler</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="owner" xml:space="preserve"> <source>owner</source> <target>Eigentümer</target> <note>member role</note> </trans-unit> + <trans-unit id="owners" xml:space="preserve"> + <source>owners</source> + <target>Eigentümer</target> + <note>feature role</note> + </trans-unit> <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> <target>Peer-to-Peer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="pending" xml:space="preserve"> + <source>pending</source> + <target>ausstehend</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="pending approval" xml:space="preserve"> + <source>pending approval</source> + <target>ausstehende Genehmigung</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="quantum resistant e2e encryption" xml:space="preserve"> + <source>quantum resistant e2e encryption</source> + <target>Quantum-resistente E2E-Verschlüsselung</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> <target>Antwort erhalten…</target> @@ -6570,6 +9810,11 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Bestätigung erhalten…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="rejected" xml:space="preserve"> + <source>rejected</source> + <target>abgelehnt</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> <target>Abgelehnter Anruf</target> @@ -6585,11 +9830,41 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>hat %@ aus der Gruppe entfernt</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="removed contact address" xml:space="preserve"> + <source>removed contact address</source> + <target>Die Kontaktadresse wurde entfernt</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="removed profile picture" xml:space="preserve"> + <source>removed profile picture</source> + <target>Das Profil-Bild wurde entfernt</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> <target>hat Sie aus der Gruppe entfernt</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="requested to connect" xml:space="preserve"> + <source>requested to connect</source> + <target>Zur Verbindung aufgefordert</target> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="saved" xml:space="preserve"> + <source>saved</source> + <target>abgespeichert</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="saved from %@" xml:space="preserve"> + <source>saved from %@</source> + <target>abgespeichert von %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="search" xml:space="preserve"> + <source>search</source> + <target>Suchen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="sec" xml:space="preserve"> <source>sec</source> <target>sek</target> @@ -6615,6 +9890,30 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Direktnachricht senden</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve"> + <source>server queue info: %1$@ + +last received msg: %2$@</source> + <target>Server-Warteschlangen-Information: %1$@ + +Zuletzt empfangene Nachricht: %2$@</target> + <note>queue info</note> + </trans-unit> + <trans-unit id="set new contact address" xml:space="preserve"> + <source>set new contact address</source> + <target>Es wurde eine neue Kontaktadresse festgelegt</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="set new profile picture" xml:space="preserve"> + <source>set new profile picture</source> + <target>Es wurde ein neues Profil-Bild festgelegt</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="standard end-to-end encryption" xml:space="preserve"> + <source>standard end-to-end encryption</source> + <target>Standard-Ende-zu-Ende-Verschlüsselung</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>Verbindung wird gestartet…</target> @@ -6630,16 +9929,41 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Dieser Kontakt</target> <note>notification title</note> </trans-unit> + <trans-unit id="unblocked %@" xml:space="preserve"> + <source>unblocked %@</source> + <target>%@ wurde freigegeben</target> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="unknown" xml:space="preserve"> <source>unknown</source> <target>Unbekannt</target> <note>connection info</note> </trans-unit> + <trans-unit id="unknown servers" xml:space="preserve"> + <source>unknown servers</source> + <target>Unbekannte Relais</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unknown status" xml:space="preserve"> + <source>unknown status</source> + <target>unbekannter Gruppenmitglieds-Status</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unprotected" xml:space="preserve"> + <source>unprotected</source> + <target>Ungeschützt</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="updated group profile" xml:space="preserve"> <source>updated group profile</source> <target>Aktualisiertes Gruppenprofil</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="updated profile" xml:space="preserve"> + <source>updated profile</source> + <target>Das Profil wurde aktualisiert</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="v%@" xml:space="preserve"> <source>v%@</source> <target>v%@</target> @@ -6670,6 +9994,11 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>über Relais</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="video" xml:space="preserve"> + <source>video</source> + <target>Video</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> <source>video call (not e2e encrypted)</source> <target>Videoanruf (nicht E2E verschlüsselt)</target> @@ -6695,11 +10024,21 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Wochen</target> <note>time unit</note> </trans-unit> + <trans-unit id="when IP hidden" xml:space="preserve"> + <source>when IP hidden</source> + <target>Wenn die IP-Adresse versteckt ist</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="yes" xml:space="preserve"> <source>yes</source> <target>Ja</target> <note>pref value</note> </trans-unit> + <trans-unit id="you" xml:space="preserve"> + <source>you</source> + <target>Profil</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> <target>Sie sind zu der Gruppe eingeladen</target> @@ -6710,14 +10049,19 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Sie sind Beobachter</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="you blocked %@" xml:space="preserve"> + <source>you blocked %@</source> + <target>Sie haben %@ blockiert</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> - <target>Sie haben die Adresse gewechselt</target> + <target>Die Empfängeradresse wurde gewechselt</target> <note>chat item text</note> </trans-unit> <trans-unit id="you changed address for %@" xml:space="preserve"> <source>you changed address for %@</source> - <target>Sie haben die Adresse für %@ gewechselt</target> + <target>Die Empfängeradresse für %@ wurde gewechselt</target> <note>chat item text</note> </trans-unit> <trans-unit id="you changed role for yourself to %@" xml:space="preserve"> @@ -6750,6 +10094,11 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Sie haben Inkognito einen Einmal-Link geteilt</target> <note>chat list item description</note> </trans-unit> + <trans-unit id="you unblocked %@" xml:space="preserve"> + <source>you unblocked %@</source> + <target>Sie haben %@ freigegeben</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you: " xml:space="preserve"> <source>you: </source> <target>Sie: </target> @@ -6764,7 +10113,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="de" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6774,7 +10123,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> </trans-unit> <trans-unit id="NSCameraUsageDescription" xml:space="preserve"> <source>SimpleX needs camera access to scan QR codes to connect to other users and for video calls.</source> - <target>SimpleX benötigt Zugriff auf die Kamera, um QR Codes für die Verbindung mit anderen Nutzern zu scannen und Videoanrufe durchzuführen.</target> + <target>SimpleX benötigt Zugriff auf die Kamera, um QR Codes für die Verbindung mit anderen Benutzern zu scannen und Videoanrufe durchzuführen.</target> <note>Privacy - Camera Usage Description</note> </trans-unit> <trans-unit id="NSFaceIDUsageDescription" xml:space="preserve"> @@ -6794,14 +10143,14 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> </trans-unit> <trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve"> <source>SimpleX needs access to Photo Library for saving captured and received media</source> - <target>SimpleX benötigt Zugriff auf das Fotoalbum, um selbst gemachte oder empfangene Bilder zu speichern</target> + <target>SimpleX benötigt Zugriff auf das Fotoalbum, um selbst gemachte oder heruntergeladene Bilder zu speichern</target> <note>Privacy - Photo Library Additions Usage Description</note> </trans-unit> </body> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="de" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> @@ -6816,9 +10165,255 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> </trans-unit> <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> <source>Copyright © 2022 SimpleX Chat. All rights reserved.</source> - <target>Copyright © 2022 SimpleX Chat. All rights reserved.</target> + <target>Copyright © 2024 SimpleX Chat. All rights reserved.</target> <note>Copyright (human-readable)</note> </trans-unit> </body> </file> + <file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="de" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%d new events" xml:space="preserve"> + <source>%d new events</source> + <target>%d neue Ereignisse</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="From %d chat(s)" xml:space="preserve"> + <source>From %d chat(s)</source> + <target>Von %d Chat(s)</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="From: %@" xml:space="preserve"> + <source>From: %@</source> + <target>Von: %@</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>Neue Ereignisse</target> + <note>notification</note> + </trans-unit> + <trans-unit id="New messages" xml:space="preserve"> + <source>New messages</source> + <target>Neue Nachrichten</target> + <note>notification</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="de" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle display name</note> + </trans-unit> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> + <source>Copyright © 2024 SimpleX Chat. All rights reserved.</source> + <target>Copyright © 2025 SimpleX Chat. Alle Rechte vorbehalten.</target> + <note>Copyright (human-readable)</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="de" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <target>%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App is locked!" xml:space="preserve"> + <source>App is locked!</source> + <target>Die App ist gesperrt!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <target>Abbrechen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <target>Es ist nicht möglich, auf den Schlüsselbund zuzugreifen, um das Datenbankpasswort zu speichern</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Nachricht kann nicht weitergeleitet werden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Comment" xml:space="preserve"> + <source>Comment</source> + <target>Kommentieren</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <target>Die maximal erlaubte Dateigröße beträgt aktuell %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade required" xml:space="preserve"> + <source>Database downgrade required</source> + <target>Datenbank-Herunterstufung ist erforderlich</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <target>Datenbank ist verschlüsselt!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <target>Datenbankfehler</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <target>Das Datenbank-Passwort unterscheidet sich vom im Schlüsselbund gespeicherten.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <target>Um den Chat zu öffnen, ist ein Datenbank-Passwort ist erforderlich.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade required" xml:space="preserve"> + <source>Database upgrade required</source> + <target>Datenbank-Aktualisierung erforderlich</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing file" xml:space="preserve"> + <source>Error preparing file</source> + <target>Fehler beim Vorbereiten der Datei</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing message" xml:space="preserve"> + <source>Error preparing message</source> + <target>Fehler beim Vorbereiten der Nachricht</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <target>Fehler: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Datei-Fehler</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <target>Datenbank-Version nicht kompatibel</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Migrations-Bestätigung ungültig</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <target>Schlüsselbund-Fehler</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <target>Große Datei!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No active profile" xml:space="preserve"> + <source>No active profile</source> + <target>Kein aktives Profil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <target>OK</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to downgrade the database." xml:space="preserve"> + <source>Open the app to downgrade the database.</source> + <target>Öffnen Sie die App, um die Datenbank herunterzustufen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to upgrade the database." xml:space="preserve"> + <source>Open the app to upgrade the database.</source> + <target>Öffnen Sie die App, um die Datenbank zu aktualisieren.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passphrase" xml:space="preserve"> + <source>Passphrase</source> + <target>Passwort</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve"> + <source>Please create a profile in the SimpleX app</source> + <target>Bitte erstellen Sie ein Profil in der SimpleX-App</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Diese Nachricht ist wegen der gewählten Chat-Einstellungen nicht erlaubt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending a message takes longer than expected." xml:space="preserve"> + <source>Sending a message takes longer than expected.</source> + <target>Das Senden einer Nachricht dauert länger als erwartet.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending message…" xml:space="preserve"> + <source>Sending message…</source> + <target>Nachricht wird gesendet…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <target>Teilen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Slow network?" xml:space="preserve"> + <source>Slow network?</source> + <target>Langsames Netzwerk?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <target>Unbekannter Datenbankfehler: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unsupported format" xml:space="preserve"> + <source>Unsupported format</source> + <target>Nicht unterstütztes Format</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wait" xml:space="preserve"> + <source>Wait</source> + <target>Warten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <target>Falsches Datenbank-Passwort</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve"> + <source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source> + <target>Sie können das Teilen in den Einstellungen zu Datenschutz & Sicherheit / SimpleX-Sperre erlauben.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + </body> + </file> </xliff> diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/de.xcloc/contents.json b/apps/ios/SimpleX Localizations/de.xcloc/contents.json index 11924b71f5..e8d71cf38c 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/de.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "de", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "16.2" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff index 7649b595cd..fc1846942c 100644 --- a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff +++ b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff @@ -31,48 +31,59 @@ Available in v5.1</source> <source> (</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" (can be copied)" xml:space="preserve"> + <trans-unit id=" (can be copied)" xml:space="preserve" approved="no"> <source> (can be copied)</source> + <target state="translated"> (μπορεί να αντιγραφή)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="!1 colored!" xml:space="preserve"> + <trans-unit id="!1 colored!" xml:space="preserve" approved="no"> <source>!1 colored!</source> + <target state="translated">!1 έγχρωμο!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="#secret#" xml:space="preserve"> + <trans-unit id="#secret#" xml:space="preserve" approved="no"> <source>#secret#</source> + <target state="translated">#μυστικό#</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@" xml:space="preserve"> + <trans-unit id="%@" xml:space="preserve" approved="no"> <source>%@</source> + <target state="translated">%@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ %@" xml:space="preserve"> + <trans-unit id="%@ %@" xml:space="preserve" approved="no"> <source>%@ %@</source> + <target state="translated">%@ %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ / %@" xml:space="preserve"> + <trans-unit id="%@ / %@" xml:space="preserve" approved="no"> <source>%@ / %@</source> + <target state="translated">%@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ is connected!" xml:space="preserve"> + <trans-unit id="%@ is connected!" xml:space="preserve" approved="no"> <source>%@ is connected!</source> + <target state="translated">%@ είναι συνδεδεμένο!</target> <note>notification title</note> </trans-unit> - <trans-unit id="%@ is not verified" xml:space="preserve"> + <trans-unit id="%@ is not verified" xml:space="preserve" approved="no"> <source>%@ is not verified</source> + <target state="translated">%@ δεν είναι επαληθευμένο</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ is verified" xml:space="preserve"> + <trans-unit id="%@ is verified" xml:space="preserve" approved="no"> <source>%@ is verified</source> + <target state="translated">%@ είναι επαληθευμένο</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ servers" xml:space="preserve"> + <trans-unit id="%@ servers" xml:space="preserve" approved="no"> <source>%@ servers</source> + <target state="translated">%@ διακομιστές</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ wants to connect!" xml:space="preserve"> + <trans-unit id="%@ wants to connect!" xml:space="preserve" approved="no"> <source>%@ wants to connect!</source> + <target state="translated">%@ θέλει να συνδεθεί!</target> <note>notification title</note> </trans-unit> <trans-unit id="%d days" xml:space="preserve"> @@ -175,20 +186,16 @@ Available in v5.1</source> <source>)</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> <source>**Create link / QR code** for your contact to use.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> @@ -199,8 +206,8 @@ Available in v5.1</source> <source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> @@ -325,8 +332,8 @@ Available in v5.1</source> <source>Add servers by scanning QR codes.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -1117,8 +1124,8 @@ Available in v5.1</source> <source>Direct messages</source> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> @@ -1133,8 +1140,8 @@ Available in v5.1</source> <source>Disappearing messages are prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disconnect" xml:space="preserve"> @@ -1569,16 +1576,16 @@ Available in v5.1</source> <source>Group members can irreversibly delete sent messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> @@ -1697,8 +1704,8 @@ Available in v5.1</source> <source>Immediately</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Import" xml:space="preserve"> @@ -1810,8 +1817,8 @@ Available in v5.1</source> <source>Irreversible message deletion is prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve"> @@ -2005,8 +2012,8 @@ Available in v5.1</source> <source>Migration is completed</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> @@ -2163,8 +2170,8 @@ Available in v5.1</source> <source>Onion hosts will not be used.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> @@ -2223,8 +2230,8 @@ Available in v5.1</source> <source>Open user profiles</source> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve"> @@ -2279,8 +2286,8 @@ Available in v5.1</source> <source>Paste the link you received into the box below to connect with your contact.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Periodically" xml:space="preserve"> @@ -2983,8 +2990,8 @@ Available in v5.1</source> <source>Thanks to the users – contribute via Weblate!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> @@ -3028,16 +3035,16 @@ It can happen because of some bug or when the connection is compromised.</source <source>The message will be marked as moderated for all members.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> <source>The old database was not removed during the migration, it can be deleted.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The sender will NOT be notified" xml:space="preserve"> @@ -3100,8 +3107,8 @@ It can happen because of some bug or when the connection is compromised.</source <source>To make a new connection</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -3326,8 +3333,8 @@ To connect, please ask your contact to create another connection link and check <source>Voice messages are prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> @@ -3467,10 +3474,6 @@ SimpleX Lock must be enabled.</source> <source>You can't send messages!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <note>No comment provided by engineer.</note> @@ -4162,6 +4165,66 @@ SimpleX servers cannot see your profile.</source> <source>\~strike~</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ connected" xml:space="preserve" approved="no"> + <source>%@ connected</source> + <target state="translated">%@ συνδεδεμένο</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="# %@" xml:space="preserve" approved="no"> + <source># %@</source> + <target state="translated"># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="%@ and %@" xml:space="preserve" approved="no"> + <source>%@ and %@</source> + <target state="translated">%@ και %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ at %@:" xml:space="preserve" approved="no"> + <source>%1$@ at %2$@:</source> + <target state="translated">%1$@ στις %2$@:</target> + <note>copied message info, <sender> at <time></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve" approved="no"> + <source>## History</source> + <target state="translated">## Ιστορικό</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve" approved="no"> + <source>## In reply to</source> + <target state="translated">## Ως απάντηση σε</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="%@ (current)" xml:space="preserve" approved="no"> + <source>%@ (current)</source> + <target state="translated">%@ (τωρινό)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ (current):" xml:space="preserve" approved="no"> + <source>%@ (current):</source> + <target state="translated">%@ (τωρινό):</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve" approved="no"> + <source>%@ and %@ connected</source> + <target state="translated">%@ και %@ συνδεδεμένο</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@:" xml:space="preserve" approved="no"> + <source>%@:</source> + <target state="translated">%@:</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="%@, %@ and %lld members" xml:space="preserve" approved="no"> + <source>%@, %@ and %lld members</source> + <target state="translated">%@, %@ και %lld μέλη</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve" approved="no"> + <source>%@, %@ and %lld other members connected</source> + <target state="translated">%@, %@ και %lld άλλα μέλη συνδέθηκαν</target> + <note>No comment provided by engineer.</note> + </trans-unit> </body> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="el" datatype="plaintext"> 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 23498b2128..fd71e0dee6 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -2,36 +2,9 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="en" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> - <trans-unit id=" " xml:space="preserve"> - <source> -</source> - <target> -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (" xml:space="preserve"> - <source> (</source> - <target> (</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> <target> (can be copied)</target> @@ -107,6 +80,11 @@ <target>%@ connected</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve"> + <source>%@ downloaded</source> + <target>%@ downloaded</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ is connected!" xml:space="preserve"> <source>%@ is connected!</source> <target>%@ is connected!</target> @@ -122,16 +100,31 @@ <target>%@ is verified</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ server" xml:space="preserve"> + <source>%@ server</source> + <target>%@ server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ servers" xml:space="preserve"> <source>%@ servers</source> <target>%@ servers</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve"> + <source>%@ uploaded</source> + <target>%@ uploaded</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ wants to connect!" xml:space="preserve"> <source>%@ wants to connect!</source> <target>%@ wants to connect!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve"> + <source>%1$@, %2$@</source> + <target>%1$@, %2$@</target> + <note>format for date separator in chat</note> + </trans-unit> <trans-unit id="%@, %@ and %lld members" xml:space="preserve"> <source>%@, %@ and %lld members</source> <target>%@, %@ and %lld members</target> @@ -152,11 +145,36 @@ <target>%d days</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve"> + <source>%d file(s) are still being downloaded.</source> + <target>%d file(s) are still being downloaded.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve"> + <source>%d file(s) failed to download.</source> + <target>%d file(s) failed to download.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve"> + <source>%d file(s) were deleted.</source> + <target>%d file(s) were deleted.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve"> + <source>%d file(s) were not downloaded.</source> + <target>%d file(s) were not downloaded.</target> + <note>forward confirmation reason</note> + </trans-unit> <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> <target>%d hours</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve"> + <source>%d messages not forwarded</source> + <target>%d messages not forwarded</target> + <note>alert title</note> + </trans-unit> <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> <target>%d min</target> @@ -172,6 +190,11 @@ <target>%d sec</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve"> + <source>%d seconds(s)</source> + <target>%d seconds(s)</target> + <note>delete after time</note> + </trans-unit> <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> <target>%d skipped message(s)</target> @@ -217,6 +240,11 @@ <target>%lld messages blocked</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve"> + <source>%lld messages blocked by admin</source> + <target>%lld messages blocked by admin</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld messages marked deleted" xml:space="preserve"> <source>%lld messages marked deleted</source> <target>%lld messages marked deleted</target> @@ -237,11 +265,6 @@ <target>%lld new interface languages</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> - <source>%lld second(s)</source> - <target>%lld second(s)</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="%lld seconds" xml:space="preserve"> <source>%lld seconds</source> <target>%lld seconds</target> @@ -292,11 +315,6 @@ <target>%u messages skipped.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> - <source>(</source> - <target>(</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="(new)" xml:space="preserve"> <source>(new)</source> <target>(new)</target> @@ -307,34 +325,29 @@ <target>(this device v%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve"> - <source>)</source> - <target>)</target> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target>**Create 1-time link**: to create and share a new invitation link.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target>**Add new contact**: to create your one-time QR Code or link for your contact.</target> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve"> + <source>**Create group**: to create a new group.</source> + <target>**Create group**: to create a new group.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> - <source>**Create link / QR code** for your contact to use.</source> - <target>**Create link / QR code** for your contact to use.</target> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> + <target>**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.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> - <target>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</target> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> + <target>**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.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> - <target>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <target>**Paste received link** or open it in the browser and tap **Open in mobile app**.</target> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> + <target>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> @@ -342,14 +355,14 @@ <target>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> - <target>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</target> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> + <target>**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.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target>**Scan QR code**: to connect to your contact in person or via video call.</target> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve"> + <source>**Scan / Paste link**: to connect via a link you received.</source> + <target>**Scan / Paste link**: to connect via a link you received.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> @@ -357,6 +370,11 @@ <target>**Warning**: Instant push notifications require passphrase saved in Keychain.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve"> + <source>**Warning**: the archive will be removed.</source> + <target>**Warning**: the archive will be removed.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> <source>**e2e encrypted** audio call</source> <target>**e2e encrypted** audio call</target> @@ -372,11 +390,6 @@ <target>\*bold*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=", " xml:space="preserve"> - <source>, </source> - <target>, </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). @@ -413,11 +426,6 @@ - editing history.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="." xml:space="preserve"> - <source>.</source> - <target>.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="0 sec" xml:space="preserve"> <source>0 sec</source> <target>0 sec</target> @@ -431,7 +439,8 @@ <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> <target>1 day</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> @@ -446,18 +455,30 @@ <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> <target>1 month</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> <target>1 week</target> - <note>time interval</note> + <note>delete after time +time interval</note> + </trans-unit> + <trans-unit id="1 year" xml:space="preserve"> + <source>1 year</source> + <target>1 year</target> + <note>delete after time</note> </trans-unit> <trans-unit id="1-time link" xml:space="preserve"> <source>1-time link</source> <target>1-time link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> + <target>1-time link can be used *with one contact only* - share in person or via any messenger.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="5 minutes" xml:space="preserve"> <source>5 minutes</source> <target>5 minutes</target> @@ -473,11 +494,6 @@ <target>30 seconds</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve"> - <source>: </source> - <target>: </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> @@ -527,31 +543,32 @@ <target>Abort changing address?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve"> - <source>About SimpleX</source> - <target>About SimpleX</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve"> <source>About SimpleX Chat</source> <target>About SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve"> - <source>About SimpleX address</source> - <target>About SimpleX address</target> + <trans-unit id="About operators" xml:space="preserve"> + <source>About operators</source> + <target>About operators</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accent color" xml:space="preserve"> - <source>Accent color</source> - <target>Accent color</target> + <trans-unit id="Accent" xml:space="preserve"> + <source>Accent</source> + <target>Accent</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept" xml:space="preserve"> <source>Accept</source> <target>Accept</target> <note>accept contact request via notification - accept incoming call via notification</note> +accept incoming call via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accept conditions" xml:space="preserve"> + <source>Accept conditions</source> + <target>Accept conditions</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept connection request?" xml:space="preserve"> <source>Accept connection request?</source> @@ -566,16 +583,47 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Accept incognito</target> - <note>accept contact request via notification</note> + <note>accept contact request via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve"> + <source>Accepted conditions</source> + <target>Accepted conditions</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve"> + <source>Acknowledged</source> + <target>Acknowledged</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve"> + <source>Acknowledgement errors</source> + <target>Acknowledgement errors</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Active" xml:space="preserve"> + <source>Active</source> + <target>Active</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve"> + <source>Active connections</source> + <target>Active connections</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> <target>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> - <source>Add preset servers</source> - <target>Add preset servers</target> + <trans-unit id="Add friends" xml:space="preserve"> + <source>Add friends</source> + <target>Add friends</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve"> + <source>Add list</source> + <target>Add list</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add profile" xml:space="preserve"> @@ -583,14 +631,19 @@ <target>Add profile</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> + <target>Add server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> <source>Add servers by scanning QR codes.</source> <target>Add servers by scanning QR codes.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> - <target>Add server…</target> + <trans-unit id="Add team members" xml:space="preserve"> + <source>Add team members</source> + <target>Add team members</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -598,11 +651,46 @@ <target>Add to another device</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add to list" xml:space="preserve"> + <source>Add to list</source> + <target>Add to list</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> <target>Add welcome message</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve"> + <source>Add your team members to the conversations.</source> + <target>Add your team members to the conversations.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve"> + <source>Added media & file servers</source> + <target>Added media & file servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve"> + <source>Added message servers</source> + <target>Added message servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve"> + <source>Additional accent</source> + <target>Additional accent</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve"> + <source>Additional accent 2</source> + <target>Additional accent 2</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve"> + <source>Additional secondary</source> + <target>Additional secondary</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Address" xml:space="preserve"> <source>Address</source> <target>Address</target> @@ -613,6 +701,21 @@ <target>Address change will be aborted. Old receiving address will be used.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve"> + <source>Address or 1-time link?</source> + <target>Address or 1-time link?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve"> + <source>Address settings</source> + <target>Address settings</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve"> + <source>Admins can block a member for all.</source> + <target>Admins can block a member for all.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> <source>Admins can create the links to join groups.</source> <target>Admins can create the links to join groups.</target> @@ -623,6 +726,16 @@ <target>Advanced network settings</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve"> + <source>Advanced settings</source> + <target>Advanced settings</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All" xml:space="preserve"> + <source>All</source> + <target>All</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All app data is deleted." xml:space="preserve"> <source>All app data is deleted.</source> <target>All app data is deleted.</target> @@ -633,16 +746,36 @@ <target>All chats and messages will be deleted - this cannot be undone!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <target>All chats will be removed from the list %@, and the list deleted.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> <target>All data is erased when it is entered.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve"> + <source>All data is kept private on your device.</source> + <target>All data is kept private on your device.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> <target>All group members will remain connected.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <target>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone!</source> + <target>All messages will be deleted - this cannot be undone!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> <target>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</target> @@ -653,6 +786,21 @@ <target>All new messages from %@ will be hidden!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All profiles" xml:space="preserve"> + <source>All profiles</source> + <target>All profiles</target> + <note>profile dropdown</note> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve"> + <source>All reports will be archived for you.</source> + <target>All reports will be archived for you.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All servers" xml:space="preserve"> + <source>All servers</source> + <target>All servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All your contacts will remain connected." xml:space="preserve"> <source>All your contacts will remain connected.</source> <target>All your contacts will remain connected.</target> @@ -663,6 +811,11 @@ <target>All your contacts will remain connected. Profile update will be sent to your contacts.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow" xml:space="preserve"> <source>Allow</source> <target>Allow</target> @@ -673,14 +826,24 @@ <target>Allow calls only if your contact allows them.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve"> + <source>Allow calls?</source> + <target>Allow calls?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> <target>Allow disappearing messages only if your contact allows it to you.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <target>Allow irreversible message deletion only if your contact allows it to you.</target> + <trans-unit id="Allow downgrade" xml:space="preserve"> + <source>Allow downgrade</source> + <target>Allow downgrade</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> @@ -703,9 +866,24 @@ <target>Allow sending disappearing messages.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow to irreversibly delete sent messages.</source> - <target>Allow to irreversibly delete sent messages.</target> + <trans-unit id="Allow sharing" xml:space="preserve"> + <source>Allow sharing</source> + <target>Allow sharing</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target>Allow to irreversibly delete sent messages. (24 hours)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve"> + <source>Allow to report messsages to moderators.</source> + <target>Allow to report messsages to moderators.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve"> + <source>Allow to send SimpleX links.</source> + <target>Allow to send SimpleX links.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send files and media." xml:space="preserve"> @@ -738,9 +916,9 @@ <target>Allow your contacts to call you.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow your contacts to irreversibly delete sent messages.</source> - <target>Allow your contacts to irreversibly delete sent messages.</target> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target>Allow your contacts to irreversibly delete sent messages. (24 hours)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> @@ -768,6 +946,11 @@ <target>Already joining the group!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve"> + <source>Always use private routing.</source> + <target>Always use private routing.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> <source>Always use relay</source> <target>Always use relay</target> @@ -778,21 +961,41 @@ <target>An empty chat profile with the provided name is created, and the app opens as usual.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Another reason" xml:space="preserve"> + <source>Another reason</source> + <target>Another reason</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> <target>Answer call</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> + <target>Anybody can host servers.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> <target>App build: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App data migration" xml:space="preserve"> + <source>App data migration</source> + <target>App data migration</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> <source>App encrypts new local files (except videos).</source> <target>App encrypts new local files (except videos).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App group:" xml:space="preserve"> + <source>App group:</source> + <target>App group:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>App icon</target> @@ -808,6 +1011,11 @@ <target>App passcode is replaced with self-destruct passcode.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App session" xml:space="preserve"> + <source>App session</source> + <target>App session</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App version" xml:space="preserve"> <source>App version</source> <target>App version</target> @@ -823,6 +1031,66 @@ <target>Appearance</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Apply" xml:space="preserve"> + <source>Apply</source> + <target>Apply</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve"> + <source>Apply to</source> + <target>Apply to</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve"> + <source>Archive</source> + <target>Archive</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve"> + <source>Archive %lld reports?</source> + <target>Archive %lld reports?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve"> + <source>Archive all reports?</source> + <target>Archive all reports?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve"> + <source>Archive and upload</source> + <target>Archive and upload</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve"> + <source>Archive contacts to chat later.</source> + <target>Archive contacts to chat later.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve"> + <source>Archive report</source> + <target>Archive report</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve"> + <source>Archive report?</source> + <target>Archive report?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve"> + <source>Archive reports</source> + <target>Archive reports</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve"> + <source>Archived contacts</source> + <target>Archived contacts</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve"> + <source>Archiving database</source> + <target>Archiving database</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Attach" xml:space="preserve"> <source>Attach</source> <target>Attach</target> @@ -883,11 +1151,21 @@ <target>Auto-accept images</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve"> + <source>Auto-accept settings</source> + <target>Auto-accept settings</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Back" xml:space="preserve"> <source>Back</source> <target>Back</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Background" xml:space="preserve"> + <source>Background</source> + <target>Background</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Bad desktop address" xml:space="preserve"> <source>Bad desktop address</source> <target>Bad desktop address</target> @@ -903,21 +1181,71 @@ <target>Bad message hash</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better calls" xml:space="preserve"> + <source>Better calls</source> + <target>Better calls</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better groups" xml:space="preserve"> <source>Better groups</source> <target>Better groups</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve"> + <source>Better groups performance</source> + <target>Better groups performance</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve"> + <source>Better message dates.</source> + <target>Better message dates.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better messages" xml:space="preserve"> <source>Better messages</source> <target>Better messages</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better networking" xml:space="preserve"> + <source>Better networking</source> + <target>Better networking</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve"> + <source>Better notifications</source> + <target>Better notifications</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve"> + <source>Better privacy and security</source> + <target>Better privacy and security</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve"> + <source>Better security ✅</source> + <target>Better security ✅</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve"> + <source>Better user experience</source> + <target>Better user experience</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Black" xml:space="preserve"> + <source>Black</source> + <target>Black</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block" xml:space="preserve"> <source>Block</source> <target>Block</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block for all" xml:space="preserve"> + <source>Block for all</source> + <target>Block for all</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block group members" xml:space="preserve"> <source>Block group members</source> <target>Block group members</target> @@ -928,19 +1256,39 @@ <target>Block member</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve"> + <source>Block member for all?</source> + <target>Block member for all?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block member?" xml:space="preserve"> <source>Block member?</source> <target>Block member?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve"> + <source>Blocked by admin</source> + <target>Blocked by admin</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve"> + <source>Blur for better privacy.</source> + <target>Blur for better privacy.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve"> + <source>Blur media</source> + <target>Blur media</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <source>Both you and your contact can add message reactions.</source> <target>Both you and your contact can add message reactions.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> - <source>Both you and your contact can irreversibly delete sent messages.</source> - <target>Both you and your contact can irreversibly delete sent messages.</target> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target>Both you and your contact can irreversibly delete sent messages. (24 hours)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> @@ -963,11 +1311,35 @@ <target>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Business address" xml:space="preserve"> + <source>Business address</source> + <target>Business address</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve"> + <source>Business chats</source> + <target>Business chats</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve"> + <source>Businesses</source> + <target>Businesses</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve"> + <source>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</source> + <target>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> <source>Call already ended!</source> <target>Call already ended!</target> @@ -978,9 +1350,24 @@ <target>Calls</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve"> - <source>Can't delete user profile!</source> - <target>Can't delete user profile!</target> + <trans-unit id="Calls prohibited!" xml:space="preserve"> + <source>Calls prohibited!</source> + <target>Calls prohibited!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve"> + <source>Camera not available</source> + <target>Camera not available</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve"> + <source>Can't call contact</source> + <target>Can't call contact</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve"> + <source>Can't call member</source> + <target>Can't call member</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contact!" xml:space="preserve"> @@ -993,9 +1380,20 @@ <target>Can't invite contacts!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve"> + <source>Can't message member</source> + <target>Can't message member</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> <target>Cancel</target> + <note>alert action +alert button</note> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve"> + <source>Cancel migration</source> + <target>Cancel migration</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> @@ -1003,9 +1401,24 @@ <target>Cannot access keychain to save database password</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Cannot forward message</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> <target>Cannot receive file</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <target>Capacity exceeded - recipient did not receive previously sent messages.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve"> + <source>Cellular</source> + <target>Cellular</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change" xml:space="preserve"> @@ -1013,6 +1426,16 @@ <target>Change</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve"> + <source>Change automatic message deletion?</source> + <target>Change automatic message deletion?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve"> + <source>Change chat profiles</source> + <target>Change chat profiles</target> + <note>authentication reason</note> + </trans-unit> <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> <target>Change database passphrase?</target> @@ -1057,11 +1480,26 @@ <source>Change self-destruct passcode</source> <target>Change self-destruct passcode</target> <note>authentication reason - set passcode view</note> +set passcode view</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> - <source>Chat archive</source> - <target>Chat archive</target> + <trans-unit id="Chat" xml:space="preserve"> + <source>Chat</source> + <target>Chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve"> + <source>Chat already exists</source> + <target>Chat already exists</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve"> + <source>Chat already exists!</source> + <target>Chat already exists!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve"> + <source>Chat colors</source> + <target>Chat colors</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat console" xml:space="preserve"> @@ -1079,6 +1517,11 @@ <target>Chat database deleted</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve"> + <source>Chat database exported</source> + <target>Chat database exported</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> <target>Chat database imported</target> @@ -1094,26 +1537,81 @@ <target>Chat is stopped</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <target>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve"> + <source>Chat list</source> + <target>Chat list</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve"> + <source>Chat migrated!</source> + <target>Chat migrated!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> <target>Chat preferences</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve"> + <source>Chat preferences were changed.</source> + <target>Chat preferences were changed.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve"> + <source>Chat profile</source> + <target>Chat profile</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve"> + <source>Chat theme</source> + <target>Chat theme</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <target>Chat will be deleted for all members - this cannot be undone!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <target>Chat will be deleted for you - this cannot be undone!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> <target>Chats</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve"> + <source>Check messages every 20 min.</source> + <target>Check messages every 20 min.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve"> + <source>Check messages when allowed.</source> + <target>Check messages when allowed.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> <target>Check server address and try again.</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> <target>Chinese and Spanish interface</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <target>Choose _Migrate from another device_ on the new device and scan QR code.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> <target>Choose file</target> @@ -1124,10 +1622,25 @@ <target>Choose from library</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve"> + <source>Chunks deleted</source> + <target>Chunks deleted</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve"> + <source>Chunks downloaded</source> + <target>Chunks downloaded</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve"> + <source>Chunks uploaded</source> + <target>Chunks uploaded</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> <target>Clear</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> @@ -1139,16 +1652,41 @@ <target>Clear conversation?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve"> + <source>Clear group?</source> + <target>Clear group?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve"> + <source>Clear or delete group?</source> + <target>Clear or delete group?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve"> + <source>Clear private notes?</source> + <target>Clear private notes?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear verification" xml:space="preserve"> <source>Clear verification</source> <target>Clear verification</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> - <source>Colors</source> - <target>Colors</target> + <trans-unit id="Color chats with the new themes." xml:space="preserve"> + <source>Color chats with the new themes.</source> + <target>Color chats with the new themes.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Color mode" xml:space="preserve"> + <source>Color mode</source> + <target>Color mode</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve"> + <source>Community guidelines violation</source> + <target>Community guidelines violation</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Compare file" xml:space="preserve"> <source>Compare file</source> <target>Compare file</target> @@ -1159,11 +1697,56 @@ <target>Compare security codes with your contacts.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Completed" xml:space="preserve"> + <source>Completed</source> + <target>Completed</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve"> + <source>Conditions accepted on: %@.</source> + <target>Conditions accepted on: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <target>Conditions are accepted for the operator(s): **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <target>Conditions are already accepted for these operator(s): **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve"> + <source>Conditions of use</source> + <target>Conditions of use</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <target>Conditions will be accepted for the operator(s): **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve"> + <source>Conditions will be accepted on: %@.</source> + <target>Conditions will be accepted on: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <target>Conditions will be automatically accepted for enabled operators on: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> <target>Configure ICE servers</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Configure server operators" xml:space="preserve"> + <source>Configure server operators</source> + <target>Configure server operators</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> <target>Confirm</target> @@ -1174,11 +1757,26 @@ <target>Confirm Passcode</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve"> + <source>Confirm contact deletion?</source> + <target>Confirm contact deletion?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm database upgrades" xml:space="preserve"> <source>Confirm database upgrades</source> <target>Confirm database upgrades</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve"> + <source>Confirm files from unknown servers.</source> + <target>Confirm files from unknown servers.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve"> + <source>Confirm network settings</source> + <target>Confirm network settings</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> <target>Confirm new passphrase…</target> @@ -1189,6 +1787,21 @@ <target>Confirm password</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <target>Confirm that you remember database passphrase to migrate it.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve"> + <source>Confirm upload</source> + <target>Confirm upload</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve"> + <source>Confirmed</source> + <target>Confirmed</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> <target>Connect</target> @@ -1209,6 +1822,11 @@ <target>Connect to desktop</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve"> + <source>Connect to your friends faster.</source> + <target>Connect to your friends faster.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connect to yourself?" xml:space="preserve"> <source>Connect to yourself?</source> <target>Connect to yourself?</target> @@ -1238,11 +1856,6 @@ This is your own one-time link!</target> <target>Connect via link</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve"> - <source>Connect via link / QR code</source> - <target>Connect via link / QR code</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connect via one-time link" xml:space="preserve"> <source>Connect via one-time link</source> <target>Connect via one-time link</target> @@ -1253,16 +1866,31 @@ This is your own one-time link!</target> <target>Connect with %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected" xml:space="preserve"> + <source>Connected</source> + <target>Connected</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected desktop" xml:space="preserve"> <source>Connected desktop</source> <target>Connected desktop</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve"> + <source>Connected servers</source> + <target>Connected servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected to desktop" xml:space="preserve"> <source>Connected to desktop</source> <target>Connected to desktop</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting" xml:space="preserve"> + <source>Connecting</source> + <target>Connecting</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> <source>Connecting to server…</source> <target>Connecting to server…</target> @@ -1273,6 +1901,11 @@ This is your own one-time link!</target> <target>Connecting to server… (error: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve"> + <source>Connecting to contact, please wait or check later!</source> + <target>Connecting to contact, please wait or check later!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting to desktop" xml:space="preserve"> <source>Connecting to desktop</source> <target>Connecting to desktop</target> @@ -1283,6 +1916,16 @@ This is your own one-time link!</target> <target>Connection</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve"> + <source>Connection and servers status.</source> + <target>Connection and servers status.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve"> + <source>Connection blocked</source> + <target>Connection blocked</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> <target>Connection error</target> @@ -1293,11 +1936,38 @@ This is your own one-time link!</target> <target>Connection error (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve"> + <source>Connection is blocked by server operator: +%@</source> + <target>Connection is blocked by server operator: +%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve"> + <source>Connection not ready.</source> + <target>Connection not ready.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve"> + <source>Connection notifications</source> + <target>Connection notifications</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Connection request sent!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve"> + <source>Connection requires encryption renegotiation.</source> + <target>Connection requires encryption renegotiation.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve"> + <source>Connection security</source> + <target>Connection security</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection terminated" xml:space="preserve"> <source>Connection terminated</source> <target>Connection terminated</target> @@ -1308,6 +1978,16 @@ This is your own one-time link!</target> <target>Connection timeout</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve"> + <source>Connection with desktop stopped</source> + <target>Connection with desktop stopped</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve"> + <source>Connections</source> + <target>Connections</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact allows" xml:space="preserve"> <source>Contact allows</source> <target>Contact allows</target> @@ -1318,6 +1998,11 @@ This is your own one-time link!</target> <target>Contact already exists</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact deleted!" xml:space="preserve"> + <source>Contact deleted!</source> + <target>Contact deleted!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> <target>Contact hidden:</target> @@ -1328,9 +2013,9 @@ This is your own one-time link!</target> <target>Contact is connected</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve"> - <source>Contact is not connected yet!</source> - <target>Contact is not connected yet!</target> + <trans-unit id="Contact is deleted." xml:space="preserve"> + <source>Contact is deleted.</source> + <target>Contact is deleted.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact name" xml:space="preserve"> @@ -1343,6 +2028,11 @@ This is your own one-time link!</target> <target>Contact preferences</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve"> + <source>Contact will be deleted - this cannot be undone!</source> + <target>Contact will be deleted - this cannot be undone!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> <target>Contacts</target> @@ -1353,21 +2043,41 @@ This is your own one-time link!</target> <target>Contacts can mark messages for deletion; you will be able to view them.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve"> + <source>Content violates conditions of use</source> + <target>Content violates conditions of use</target> + <note>blocking reason</note> + </trans-unit> <trans-unit id="Continue" xml:space="preserve"> <source>Continue</source> <target>Continue</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve"> + <source>Conversation deleted!</source> + <target>Conversation deleted!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> <target>Copy</target> - <note>chat item action</note> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve"> + <source>Copy error</source> + <target>Copy error</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> <target>Core version: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Corner" xml:space="preserve"> + <source>Corner</source> + <target>Corner</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Correct name to %@?" xml:space="preserve"> <source>Correct name to %@?</source> <target>Correct name to %@?</target> @@ -1378,6 +2088,11 @@ This is your own one-time link!</target> <target>Create</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve"> + <source>Create 1-time link</source> + <target>Create 1-time link</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> <target>Create SimpleX address</target> @@ -1388,11 +2103,6 @@ This is your own one-time link!</target> <target>Create a group using a random profile.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> - <source>Create an address to let people connect with you.</source> - <target>Create an address to let people connect with you.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create file" xml:space="preserve"> <source>Create file</source> <target>Create file</target> @@ -1413,16 +2123,16 @@ This is your own one-time link!</target> <target>Create link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create list" xml:space="preserve"> + <source>Create list</source> + <target>Create list</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> <target>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve"> - <source>Create one-time invitation link</source> - <target>Create one-time invitation link</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create profile" xml:space="preserve"> <source>Create profile</source> <target>Create profile</target> @@ -1443,9 +2153,29 @@ This is your own one-time link!</target> <target>Create your profile</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve"> - <source>Created on %@</source> - <target>Created on %@</target> + <trans-unit id="Created" xml:space="preserve"> + <source>Created</source> + <target>Created</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve"> + <source>Created at</source> + <target>Created at</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve"> + <source>Created at: %@</source> + <target>Created at: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve"> + <source>Creating archive link</source> + <target>Creating archive link</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve"> + <source>Creating link…</source> + <target>Creating link…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current Passcode" xml:space="preserve"> @@ -1453,11 +2183,21 @@ This is your own one-time link!</target> <target>Current Passcode</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <target>Current conditions text couldn't be loaded, you can review conditions via this link:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> <target>Current passphrase…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current profile" xml:space="preserve"> + <source>Current profile</source> + <target>Current profile</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> <target>Currently maximum supported file size is %@.</target> @@ -1468,11 +2208,26 @@ This is your own one-time link!</target> <target>Custom time</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve"> + <source>Customizable message shape.</source> + <target>Customizable message shape.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve"> + <source>Customize theme</source> + <target>Customize theme</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> <target>Dark</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve"> + <source>Dark mode colors</source> + <target>Dark mode colors</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> <target>Database ID</target> @@ -1571,6 +2326,11 @@ This is your own one-time link!</target> <target>Database will be migrated when the app restarts</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Debug delivery" xml:space="preserve"> + <source>Debug delivery</source> + <target>Debug delivery</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> <target>Decentralized</target> @@ -1584,18 +2344,19 @@ This is your own one-time link!</target> <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> <target>Delete</target> - <note>chat item action</note> + <note>alert action +swipe action</note> + </trans-unit> + <trans-unit id="Delete %lld messages of members?" xml:space="preserve"> + <source>Delete %lld messages of members?</source> + <target>Delete %lld messages of members?</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete %lld messages?" xml:space="preserve"> <source>Delete %lld messages?</source> <target>Delete %lld messages?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve"> - <source>Delete Contact</source> - <target>Delete Contact</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Delete address" xml:space="preserve"> <source>Delete address</source> <target>Delete address</target> @@ -1621,14 +2382,14 @@ This is your own one-time link!</target> <target>Delete and notify contact</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve"> - <source>Delete archive</source> - <target>Delete archive</target> + <trans-unit id="Delete chat" xml:space="preserve"> + <source>Delete chat</source> + <target>Delete chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve"> - <source>Delete chat archive?</source> - <target>Delete chat archive?</target> + <trans-unit id="Delete chat messages from your device." xml:space="preserve"> + <source>Delete chat messages from your device.</source> + <target>Delete chat messages from your device.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile" xml:space="preserve"> @@ -1641,6 +2402,11 @@ This is your own one-time link!</target> <target>Delete chat profile?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete chat?" xml:space="preserve"> + <source>Delete chat?</source> + <target>Delete chat?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> <source>Delete connection</source> <target>Delete connection</target> @@ -1651,11 +2417,9 @@ This is your own one-time link!</target> <target>Delete contact</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact? This cannot be undone!" xml:space="preserve"> - <source>Delete contact? -This cannot be undone!</source> - <target>Delete contact? -This cannot be undone!</target> + <trans-unit id="Delete contact?" xml:space="preserve"> + <source>Delete contact?</source> + <target>Delete contact?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete database" xml:space="preserve"> @@ -1663,6 +2427,11 @@ This cannot be undone!</target> <target>Delete database</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete database from this device" xml:space="preserve"> + <source>Delete database from this device</source> + <target>Delete database from this device</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete file" xml:space="preserve"> <source>Delete file</source> <target>Delete file</target> @@ -1713,6 +2482,11 @@ This cannot be undone!</target> <target>Delete link?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete list?" xml:space="preserve"> + <source>Delete list?</source> + <target>Delete list?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> <target>Delete member message?</target> @@ -1726,7 +2500,7 @@ This cannot be undone!</target> <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> <target>Delete messages</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> @@ -1743,9 +2517,9 @@ This cannot be undone!</target> <target>Delete old database?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve"> - <source>Delete pending connection</source> - <target>Delete pending connection</target> + <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> + <source>Delete or moderate up to 200 messages.</source> + <target>Delete or moderate up to 200 messages.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection?" xml:space="preserve"> @@ -1763,11 +2537,31 @@ This cannot be undone!</target> <target>Delete queue</target> <note>server test step</note> </trans-unit> + <trans-unit id="Delete report" xml:space="preserve"> + <source>Delete report</source> + <target>Delete report</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete up to 20 messages at once." xml:space="preserve"> + <source>Delete up to 20 messages at once.</source> + <target>Delete up to 20 messages at once.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> <target>Delete user profile?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete without notification" xml:space="preserve"> + <source>Delete without notification</source> + <target>Delete without notification</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve"> + <source>Deleted</source> + <target>Deleted</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Deleted at" xml:space="preserve"> <source>Deleted at</source> <target>Deleted at</target> @@ -1778,6 +2572,16 @@ This cannot be undone!</target> <target>Deleted at: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve"> + <source>Deletion errors</source> + <target>Deletion errors</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivered even when Apple drops them." xml:space="preserve"> + <source>Delivered even when Apple drops them.</source> + <target>Delivered even when Apple drops them.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery" xml:space="preserve"> <source>Delivery</source> <target>Delivery</target> @@ -1813,11 +2617,41 @@ This cannot be undone!</target> <target>Desktop devices</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve"> + <source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source> + <target>Destination server address of %@ is incompatible with forwarding server %@ settings.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server error: %@" xml:space="preserve"> + <source>Destination server error: %@</source> + <target>Destination server error: %@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve"> + <source>Destination server version of %@ is incompatible with forwarding server %@.</source> + <target>Destination server version of %@ is incompatible with forwarding server %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Detailed statistics" xml:space="preserve"> + <source>Detailed statistics</source> + <target>Detailed statistics</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Details" xml:space="preserve"> + <source>Details</source> + <target>Details</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Develop" xml:space="preserve"> <source>Develop</source> <target>Develop</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Developer options" xml:space="preserve"> + <source>Developer options</source> + <target>Developer options</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> <target>Developer tools</target> @@ -1848,9 +2682,14 @@ This cannot be undone!</target> <target>Direct messages</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> - <target>Direct messages between members are prohibited in this group.</target> + <trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve"> + <source>Direct messages between members are prohibited in this chat.</source> + <target>Direct messages between members are prohibited in this chat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> + <target>Direct messages between members are prohibited.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disable (keep overrides)" xml:space="preserve"> @@ -1863,11 +2702,26 @@ This cannot be undone!</target> <target>Disable SimpleX Lock</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Disable automatic message deletion?" xml:space="preserve"> + <source>Disable automatic message deletion?</source> + <target>Disable automatic message deletion?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Disable delete messages" xml:space="preserve"> + <source>Disable delete messages</source> + <target>Disable delete messages</target> + <note>alert button</note> + </trans-unit> <trans-unit id="Disable for all" xml:space="preserve"> <source>Disable for all</source> <target>Disable for all</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Disabled" xml:space="preserve"> + <source>Disabled</source> + <target>Disabled</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> <source>Disappearing message</source> <target>Disappearing message</target> @@ -1883,9 +2737,9 @@ This cannot be undone!</target> <target>Disappearing messages are prohibited in this chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> - <target>Disappearing messages are prohibited in this group.</target> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> + <target>Disappearing messages are prohibited.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disappears at" xml:space="preserve"> @@ -1918,16 +2772,41 @@ This cannot be undone!</target> <target>Discover via local network</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve"> + <source>Do NOT send messages directly, even if your or destination server does not support private routing.</source> + <target>Do NOT send messages directly, even if your or destination server does not support private routing.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> <source>Do NOT use SimpleX for emergency calls.</source> <target>Do NOT use SimpleX for emergency calls.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT use private routing." xml:space="preserve"> + <source>Do NOT use private routing.</source> + <target>Do NOT use private routing.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do it later" xml:space="preserve"> <source>Do it later</source> <target>Do it later</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do not send history to new members." xml:space="preserve"> + <source>Do not send history to new members.</source> + <target>Do not send history to new members.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not use credentials with proxy." xml:space="preserve"> + <source>Do not use credentials with proxy.</source> + <target>Do not use credentials with proxy.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Documents:" xml:space="preserve"> + <source>Documents:</source> + <target>Documents:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> <target>Don't create address</target> @@ -1938,21 +2817,72 @@ This cannot be undone!</target> <target>Don't enable</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Don't miss important messages." xml:space="preserve"> + <source>Don't miss important messages.</source> + <target>Don't miss important messages.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> <target>Don't show again</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Done" xml:space="preserve"> + <source>Done</source> + <target>Done</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Downgrade and open chat" xml:space="preserve"> <source>Downgrade and open chat</source> <target>Downgrade and open chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Download" xml:space="preserve"> + <source>Download</source> + <target>Download</target> + <note>alert button +chat item action</note> + </trans-unit> + <trans-unit id="Download errors" xml:space="preserve"> + <source>Download errors</source> + <target>Download errors</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download failed" xml:space="preserve"> + <source>Download failed</source> + <target>Download failed</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Download file" xml:space="preserve"> <source>Download file</source> <target>Download file</target> <note>server test step</note> </trans-unit> + <trans-unit id="Download files" xml:space="preserve"> + <source>Download files</source> + <target>Download files</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Downloaded" xml:space="preserve"> + <source>Downloaded</source> + <target>Downloaded</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloaded files" xml:space="preserve"> + <source>Downloaded files</source> + <target>Downloaded files</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading archive" xml:space="preserve"> + <source>Downloading archive</source> + <target>Downloading archive</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading link details" xml:space="preserve"> + <source>Downloading link details</source> + <target>Downloading link details</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> <target>Duplicate display name!</target> @@ -1963,6 +2893,11 @@ This cannot be undone!</target> <target>Duration</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="E2E encrypted notifications." xml:space="preserve"> + <source>E2E encrypted notifications.</source> + <target>E2E encrypted notifications.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> <target>Edit</target> @@ -1983,6 +2918,11 @@ This cannot be undone!</target> <target>Enable (keep overrides)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve"> + <source>Enable Flux in Network & servers settings for better metadata privacy.</source> + <target>Enable Flux in Network & servers settings for better metadata privacy.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> <target>Enable SimpleX Lock</target> @@ -1996,6 +2936,11 @@ This cannot be undone!</target> <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> <target>Enable automatic message deletion?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Enable camera access" xml:space="preserve"> + <source>Enable camera access</source> + <target>Enable camera access</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable for all" xml:space="preserve"> @@ -2003,6 +2948,11 @@ This cannot be undone!</target> <target>Enable for all</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve"> + <source>Enable in direct chats (BETA)!</source> + <target>Enable in direct chats (BETA)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> <target>Enable instant notifications?</target> @@ -2033,6 +2983,16 @@ This cannot be undone!</target> <target>Enable self-destruct passcode</target> <note>set passcode view</note> </trans-unit> + <trans-unit id="Enabled" xml:space="preserve"> + <source>Enabled</source> + <target>Enabled</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve"> + <source>Enabled for</source> + <target>Enabled for</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> <target>Encrypt</target> @@ -2063,6 +3023,11 @@ This cannot be undone!</target> <target>Encrypted message or another event</target> <note>notification</note> </trans-unit> + <trans-unit id="Encrypted message: app is stopped" xml:space="preserve"> + <source>Encrypted message: app is stopped</source> + <target>Encrypted message: app is stopped</target> + <note>notification</note> + </trans-unit> <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> <target>Encrypted message: database error</target> @@ -2098,6 +3063,11 @@ This cannot be undone!</target> <target>Encryption re-negotiation failed.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encryption renegotiation in progress." xml:space="preserve"> + <source>Encryption renegotiation in progress.</source> + <target>Encryption renegotiation in progress.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter Passcode" xml:space="preserve"> <source>Enter Passcode</source> <target>Enter Passcode</target> @@ -2113,6 +3083,11 @@ This cannot be undone!</target> <target>Enter group name…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve"> + <source>Enter passphrase</source> + <target>Enter passphrase</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter passphrase…" xml:space="preserve"> <source>Enter passphrase…</source> <target>Enter passphrase…</target> @@ -2158,26 +3133,36 @@ This cannot be undone!</target> <target>Error aborting address change</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error accepting conditions" xml:space="preserve"> + <source>Error accepting conditions</source> + <target>Error accepting conditions</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> <target>Error accepting contact request</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve"> - <source>Error accessing database file</source> - <target>Error accessing database file</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> <target>Error adding member(s)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error adding server" xml:space="preserve"> + <source>Error adding server</source> + <target>Error adding server</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> <target>Error changing address</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing connection profile" xml:space="preserve"> + <source>Error changing connection profile</source> + <target>Error changing connection profile</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error changing role" xml:space="preserve"> <source>Error changing role</source> <target>Error changing role</target> @@ -2188,6 +3173,21 @@ This cannot be undone!</target> <target>Error changing setting</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing to incognito!" xml:space="preserve"> + <source>Error changing to incognito!</source> + <target>Error changing to incognito!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error checking token status" xml:space="preserve"> + <source>Error checking token status</source> + <target>Error checking token status</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve"> + <source>Error connecting to forwarding server %@. Please try later.</source> + <target>Error connecting to forwarding server %@. Please try later.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> <target>Error creating address</target> @@ -2203,16 +3203,31 @@ This cannot be undone!</target> <target>Error creating group link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating list" xml:space="preserve"> + <source>Error creating list</source> + <target>Error creating list</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error creating member contact" xml:space="preserve"> <source>Error creating member contact</source> <target>Error creating member contact</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating message" xml:space="preserve"> + <source>Error creating message</source> + <target>Error creating message</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>Error creating profile!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating report" xml:space="preserve"> + <source>Error creating report</source> + <target>Error creating report</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error decrypting file" xml:space="preserve"> <source>Error decrypting file</source> <target>Error decrypting file</target> @@ -2233,11 +3248,6 @@ This cannot be undone!</target> <target>Error deleting connection</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve"> - <source>Error deleting contact</source> - <target>Error deleting contact</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> <target>Error deleting database</target> @@ -2258,6 +3268,11 @@ This cannot be undone!</target> <target>Error deleting user profile</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error downloading the archive" xml:space="preserve"> + <source>Error downloading the archive</source> + <target>Error downloading the archive</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> <source>Error enabling delivery receipts!</source> <target>Error enabling delivery receipts!</target> @@ -2278,6 +3293,11 @@ This cannot be undone!</target> <target>Error exporting chat database</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error exporting theme: %@" xml:space="preserve"> + <source>Error exporting theme: %@</source> + <target>Error exporting theme: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> <target>Error importing chat database</target> @@ -2288,24 +3308,54 @@ This cannot be undone!</target> <target>Error joining group</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve"> - <source>Error loading %@ servers</source> - <target>Error loading %@ servers</target> + <trans-unit id="Error loading servers" xml:space="preserve"> + <source>Error loading servers</source> + <target>Error loading servers</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error migrating settings" xml:space="preserve"> + <source>Error migrating settings</source> + <target>Error migrating settings</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error opening chat" xml:space="preserve"> + <source>Error opening chat</source> + <target>Error opening chat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> <target>Error receiving file</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error reconnecting server" xml:space="preserve"> + <source>Error reconnecting server</source> + <target>Error reconnecting server</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error reconnecting servers" xml:space="preserve"> + <source>Error reconnecting servers</source> + <target>Error reconnecting servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error registering for notifications" xml:space="preserve"> + <source>Error registering for notifications</source> + <target>Error registering for notifications</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> <target>Error removing member</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve"> - <source>Error saving %@ servers</source> - <target>Error saving %@ servers</target> + <trans-unit id="Error reordering lists" xml:space="preserve"> + <source>Error reordering lists</source> + <target>Error reordering lists</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error resetting statistics" xml:space="preserve"> + <source>Error resetting statistics</source> + <target>Error resetting statistics</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving ICE servers" xml:space="preserve"> @@ -2313,6 +3363,11 @@ This cannot be undone!</target> <target>Error saving ICE servers</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving chat list" xml:space="preserve"> + <source>Error saving chat list</source> + <target>Error saving chat list</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> <target>Error saving group profile</target> @@ -2328,11 +3383,26 @@ This cannot be undone!</target> <target>Error saving passphrase to keychain</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving servers" xml:space="preserve"> + <source>Error saving servers</source> + <target>Error saving servers</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error saving settings" xml:space="preserve"> + <source>Error saving settings</source> + <target>Error saving settings</target> + <note>when migrating</note> + </trans-unit> <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> <target>Error saving user password</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error scanning code: %@" xml:space="preserve"> + <source>Error scanning code: %@</source> + <target>Error scanning code: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending email" xml:space="preserve"> <source>Error sending email</source> <target>Error sending email</target> @@ -2363,16 +3433,26 @@ This cannot be undone!</target> <target>Error stopping chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error switching profile" xml:space="preserve"> + <source>Error switching profile</source> + <target>Error switching profile</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> <target>Error switching profile!</target> - <note>No comment provided by engineer.</note> + <note>alertTitle</note> </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> <target>Error synchronizing connection</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error testing server connection" xml:space="preserve"> + <source>Error testing server connection</source> + <target>Error testing server connection</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> <target>Error updating group link</target> @@ -2383,6 +3463,11 @@ This cannot be undone!</target> <target>Error updating message</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error updating server" xml:space="preserve"> + <source>Error updating server</source> + <target>Error updating server</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> <target>Error updating settings</target> @@ -2393,6 +3478,16 @@ This cannot be undone!</target> <target>Error updating user privacy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error uploading the archive" xml:space="preserve"> + <source>Error uploading the archive</source> + <target>Error uploading the archive</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error verifying passphrase:" xml:space="preserve"> + <source>Error verifying passphrase:</source> + <target>Error verifying passphrase:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error: " xml:space="preserve"> <source>Error: </source> <target>Error: </target> @@ -2401,7 +3496,9 @@ This cannot be undone!</target> <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> <target>Error: %@</target> - <note>No comment provided by engineer.</note> + <note>alert message +file error text +snd error text</note> </trans-unit> <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> @@ -2413,6 +3510,16 @@ This cannot be undone!</target> <target>Error: no database file</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Errors" xml:space="preserve"> + <source>Errors</source> + <target>Errors</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors in servers configuration." xml:space="preserve"> + <source>Errors in servers configuration.</source> + <target>Errors in servers configuration.</target> + <note>servers error</note> + </trans-unit> <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> <source>Even when disabled in the conversation.</source> <target>Even when disabled in the conversation.</target> @@ -2428,6 +3535,11 @@ This cannot be undone!</target> <target>Expand</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Expired" xml:space="preserve"> + <source>Expired</source> + <target>Expired</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> <target>Export database</target> @@ -2438,11 +3550,21 @@ This cannot be undone!</target> <target>Export error:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Export theme" xml:space="preserve"> + <source>Export theme</source> + <target>Export theme</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> <target>Exported database archive.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Exported file doesn't exist" xml:space="preserve"> + <source>Exported file doesn't exist</source> + <target>Exported file doesn't exist</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exporting database archive…" xml:space="preserve"> <source>Exporting database archive…</source> <target>Exporting database archive…</target> @@ -2458,16 +3580,70 @@ This cannot be undone!</target> <target>Fast and no wait until the sender is online!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster deletion of groups." xml:space="preserve"> + <source>Faster deletion of groups.</source> + <target>Faster deletion of groups.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Faster joining and more reliable messages." xml:space="preserve"> <source>Faster joining and more reliable messages.</source> <target>Faster joining and more reliable messages.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster sending messages." xml:space="preserve"> + <source>Faster sending messages.</source> + <target>Faster sending messages.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Favorite" xml:space="preserve"> <source>Favorite</source> <target>Favorite</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Favorites" xml:space="preserve"> + <source>Favorites</source> + <target>Favorites</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>File error</target> + <note>file error alert title</note> + </trans-unit> + <trans-unit id="File errors: %@" xml:space="preserve"> + <source>File errors: +%@</source> + <target>File errors: +%@</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="File is blocked by server operator: %@." xml:space="preserve"> + <source>File is blocked by server operator: +%@.</source> + <target>File is blocked by server operator: +%@.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve"> + <source>File not found - most likely file was deleted or cancelled.</source> + <target>File not found - most likely file was deleted or cancelled.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File server error: %@" xml:space="preserve"> + <source>File server error: %@</source> + <target>File server error: %@</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File status" xml:space="preserve"> + <source>File status</source> + <target>File status</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File status: %@" xml:space="preserve"> + <source>File status: %@</source> + <target>File status: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="File will be deleted from servers." xml:space="preserve"> <source>File will be deleted from servers.</source> <target>File will be deleted from servers.</target> @@ -2488,6 +3664,11 @@ This cannot be undone!</target> <target>File: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files" xml:space="preserve"> + <source>Files</source> + <target>Files</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> <target>Files & media</target> @@ -2498,9 +3679,14 @@ This cannot be undone!</target> <target>Files and media</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> - <source>Files and media are prohibited in this group.</source> - <target>Files and media are prohibited in this group.</target> + <trans-unit id="Files and media are prohibited." xml:space="preserve"> + <source>Files and media are prohibited.</source> + <target>Files and media are prohibited.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Files and media not allowed" xml:space="preserve"> + <source>Files and media not allowed</source> + <target>Files and media not allowed</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Files and media prohibited!" xml:space="preserve"> @@ -2513,6 +3699,16 @@ This cannot be undone!</target> <target>Filter unread and favorite chats.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Finalize migration" xml:space="preserve"> + <source>Finalize migration</source> + <target>Finalize migration</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration on another device." xml:space="preserve"> + <source>Finalize migration on another device.</source> + <target>Finalize migration on another device.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> <target>Finally, we have them! 🚀</target> @@ -2553,11 +3749,115 @@ This cannot be undone!</target> <target>Fix not supported by group member</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For all moderators" xml:space="preserve"> + <source>For all moderators</source> + <target>For all moderators</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For chat profile %@:" xml:space="preserve"> + <source>For chat profile %@:</source> + <target>For chat profile %@:</target> + <note>servers error</note> + </trans-unit> <trans-unit id="For console" xml:space="preserve"> <source>For console</source> <target>For console</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." xml:space="preserve"> + <source>For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</source> + <target>For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For me" xml:space="preserve"> + <source>For me</source> + <target>For me</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For private routing" xml:space="preserve"> + <source>For private routing</source> + <target>For private routing</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For social media" xml:space="preserve"> + <source>For social media</source> + <target>For social media</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward" xml:space="preserve"> + <source>Forward</source> + <target>Forward</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Forward %d message(s)?" xml:space="preserve"> + <source>Forward %d message(s)?</source> + <target>Forward %d message(s)?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Forward and save messages" xml:space="preserve"> + <source>Forward and save messages</source> + <target>Forward and save messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward messages" xml:space="preserve"> + <source>Forward messages</source> + <target>Forward messages</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Forward messages without files?" xml:space="preserve"> + <source>Forward messages without files?</source> + <target>Forward messages without files?</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> + <source>Forward up to 20 messages at once.</source> + <target>Forward up to 20 messages at once.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded" xml:space="preserve"> + <source>Forwarded</source> + <target>Forwarded</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded from" xml:space="preserve"> + <source>Forwarded from</source> + <target>Forwarded from</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding %lld messages" xml:space="preserve"> + <source>Forwarding %lld messages</source> + <target>Forwarding %lld messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve"> + <source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source> + <target>Forwarding server %@ failed to connect to destination server %@. Please try later.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server address is incompatible with network settings: %@.</source> + <target>Forwarding server address is incompatible with network settings: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server version is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server version is incompatible with network settings: %@.</source> + <target>Forwarding server version is incompatible with network settings: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Destination server error: %2$@</source> + <target>Forwarding server: %1$@ +Destination server error: %2$@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Error: %2$@</source> + <target>Forwarding server: %1$@ +Error: %2$@</target> + <note>snd error text</note> + </trans-unit> <trans-unit id="Found desktop" xml:space="preserve"> <source>Found desktop</source> <target>Found desktop</target> @@ -2578,11 +3878,6 @@ This cannot be undone!</target> <target>Full name (optional)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full name:" xml:space="preserve"> - <source>Full name:</source> - <target>Full name:</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Fully decentralized – visible only to members." xml:space="preserve"> <source>Fully decentralized – visible only to members.</source> <target>Fully decentralized – visible only to members.</target> @@ -2603,6 +3898,21 @@ This cannot be undone!</target> <target>GIFs and stickers</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Get notified when mentioned." xml:space="preserve"> + <source>Get notified when mentioned.</source> + <target>Get notified when mentioned.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Good afternoon!" xml:space="preserve"> + <source>Good afternoon!</source> + <target>Good afternoon!</target> + <note>message preview</note> + </trans-unit> + <trans-unit id="Good morning!" xml:space="preserve"> + <source>Good morning!</source> + <target>Good morning!</target> + <note>message preview</note> + </trans-unit> <trans-unit id="Group" xml:space="preserve"> <source>Group</source> <target>Group</target> @@ -2658,36 +3968,6 @@ This cannot be undone!</target> <target>Group links</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve"> - <source>Group members can add message reactions.</source> - <target>Group members can add message reactions.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> - <source>Group members can irreversibly delete sent messages.</source> - <target>Group members can irreversibly delete sent messages.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> - <target>Group members can send direct messages.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> - <target>Group members can send disappearing messages.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send files and media." xml:space="preserve"> - <source>Group members can send files and media.</source> - <target>Group members can send files and media.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> - <target>Group members can send voice messages.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> <target>Group message:</target> @@ -2728,11 +4008,21 @@ This cannot be undone!</target> <target>Group will be deleted for you - this cannot be undone!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Groups" xml:space="preserve"> + <source>Groups</source> + <target>Groups</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Help" xml:space="preserve"> <source>Help</source> <target>Help</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Help admins moderating their groups." xml:space="preserve"> + <source>Help admins moderating their groups.</source> + <target>Help admins moderating their groups.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> <target>Hidden</target> @@ -2773,15 +4063,30 @@ This cannot be undone!</target> <target>History</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="History is not sent to new members." xml:space="preserve"> + <source>History is not sent to new members.</source> + <target>History is not sent to new members.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> <target>How SimpleX works</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="How it affects privacy" xml:space="preserve"> + <source>How it affects privacy</source> + <target>How it affects privacy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it helps privacy" xml:space="preserve"> + <source>How it helps privacy</source> + <target>How it helps privacy</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> <target>How it works</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="How to" xml:space="preserve"> <source>How to</source> @@ -2798,21 +4103,26 @@ This cannot be undone!</target> <target>How to use your servers</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Hungarian interface" xml:space="preserve"> + <source>Hungarian interface</source> + <target>Hungarian interface</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> <target>ICE servers (one per line)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="IP address" xml:space="preserve"> + <source>IP address</source> + <target>IP address</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> <source>If you can't meet in person, show QR code in a video call, or share the link.</source> <target>If you can't meet in person, show QR code in a video call, or share the link.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <target>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> <target>If you enter this passcode when opening the app, all app data will be irreversibly removed!</target> @@ -2848,9 +4158,9 @@ This cannot be undone!</target> <target>Immediately</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> - <target>Immune to spam and abuse</target> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> + <target>Immune to spam</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Import" xml:space="preserve"> @@ -2868,6 +4178,33 @@ This cannot be undone!</target> <target>Import database</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Import failed" xml:space="preserve"> + <source>Import failed</source> + <target>Import failed</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import theme" xml:space="preserve"> + <source>Import theme</source> + <target>Import theme</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Importing archive" xml:space="preserve"> + <source>Importing archive</source> + <target>Importing archive</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved delivery, reduced traffic usage. More improvements are coming soon!" xml:space="preserve"> + <source>Improved delivery, reduced traffic usage. +More improvements are coming soon!</source> + <target>Improved delivery, reduced traffic usage. +More improvements are coming soon!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved message delivery" xml:space="preserve"> + <source>Improved message delivery</source> + <target>Improved message delivery</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> <target>Improved privacy and security</target> @@ -2878,11 +4215,31 @@ This cannot be undone!</target> <target>Improved server configuration</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In order to continue, chat should be stopped." xml:space="preserve"> + <source>In order to continue, chat should be stopped.</source> + <target>In order to continue, chat should be stopped.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>In reply to</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In-call sounds" xml:space="preserve"> + <source>In-call sounds</source> + <target>In-call sounds</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Inappropriate content" xml:space="preserve"> + <source>Inappropriate content</source> + <target>Inappropriate content</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Inappropriate profile" xml:space="preserve"> + <source>Inappropriate profile</source> + <target>Inappropriate profile</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> <target>Incognito</target> @@ -2953,6 +4310,11 @@ This cannot be undone!</target> <target>Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Instant" xml:space="preserve"> + <source>Instant</source> + <target>Instant</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> @@ -2960,30 +4322,80 @@ This cannot be undone!</target> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Instantly" xml:space="preserve"> - <source>Instantly</source> - <target>Instantly</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> <target>Interface</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Interface colors" xml:space="preserve"> + <source>Interface colors</source> + <target>Interface colors</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid" xml:space="preserve"> + <source>Invalid</source> + <target>Invalid</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (bad token)" xml:space="preserve"> + <source>Invalid (bad token)</source> + <target>Invalid (bad token)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (expired)" xml:space="preserve"> + <source>Invalid (expired)</source> + <target>Invalid (expired)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (unregistered)" xml:space="preserve"> + <source>Invalid (unregistered)</source> + <target>Invalid (unregistered)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (wrong topic)" xml:space="preserve"> + <source>Invalid (wrong topic)</source> + <target>Invalid (wrong topic)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid QR code" xml:space="preserve"> + <source>Invalid QR code</source> + <target>Invalid QR code</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> <target>Invalid connection link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid display name!" xml:space="preserve"> + <source>Invalid display name!</source> + <target>Invalid display name!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid link" xml:space="preserve"> + <source>Invalid link</source> + <target>Invalid link</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Invalid migration confirmation</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid name!" xml:space="preserve"> <source>Invalid name!</source> <target>Invalid name!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid response" xml:space="preserve"> + <source>Invalid response</source> + <target>Invalid response</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> <target>Invalid server address!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Invalid status" xml:space="preserve"> <source>Invalid status</source> @@ -3005,6 +4417,11 @@ This cannot be undone!</target> <target>Invite members</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invite to chat" xml:space="preserve"> + <source>Invite to chat</source> + <target>Invite to chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invite to group" xml:space="preserve"> <source>Invite to group</source> <target>Invite to group</target> @@ -3020,9 +4437,9 @@ This cannot be undone!</target> <target>Irreversible message deletion is prohibited in this chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> - <target>Irreversible message deletion is prohibited in this group.</target> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> + <target>Irreversible message deletion is prohibited.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve"> @@ -3046,6 +4463,11 @@ This cannot be undone!</target> 3. The connection was compromised.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="It protects your IP address and connections." xml:space="preserve"> + <source>It protects your IP address and connections.</source> + <target>It protects your IP address and connections.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> <target>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</target> @@ -3064,13 +4486,18 @@ This cannot be undone!</target> <trans-unit id="Join" xml:space="preserve"> <source>Join</source> <target>Join</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> <target>Join group</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Join group conversations" xml:space="preserve"> + <source>Join group conversations</source> + <target>Join group conversations</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Join group?" xml:space="preserve"> <source>Join group?</source> <target>Join group?</target> @@ -3098,11 +4525,26 @@ This is your link for group %@!</target> <target>Joining group</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep" xml:space="preserve"> + <source>Keep</source> + <target>Keep</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Keep conversation" xml:space="preserve"> + <source>Keep conversation</source> + <target>Keep conversation</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Keep the app open to use it from desktop" xml:space="preserve"> <source>Keep the app open to use it from desktop</source> <target>Keep the app open to use it from desktop</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep unused invitation?" xml:space="preserve"> + <source>Keep unused invitation?</source> + <target>Keep unused invitation?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> <source>Keep your connections</source> <target>Keep your connections</target> @@ -3136,6 +4578,16 @@ This is your link for group %@!</target> <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> <target>Leave</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Leave chat" xml:space="preserve"> + <source>Leave chat</source> + <target>Leave chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave chat?" xml:space="preserve"> + <source>Leave chat?</source> + <target>Leave chat?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group" xml:space="preserve"> @@ -3178,6 +4630,21 @@ This is your link for group %@!</target> <target>Linked desktops</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="List" xml:space="preserve"> + <source>List</source> + <target>List</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="List name and emoji should be different for all lists." xml:space="preserve"> + <source>List name and emoji should be different for all lists.</source> + <target>List name and emoji should be different for all lists.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List name..." xml:space="preserve"> + <source>List name...</source> + <target>List name...</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Live message!" xml:space="preserve"> <source>Live message!</source> <target>Live message!</target> @@ -3208,11 +4675,6 @@ This is your link for group %@!</target> <target>Lock mode</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> - <source>Make a private connection</source> - <target>Make a private connection</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make one message disappear" xml:space="preserve"> <source>Make one message disappear</source> <target>Make one message disappear</target> @@ -3223,21 +4685,11 @@ This is your link for group %@!</target> <target>Make profile private!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> - <target>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> <target>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <target>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> <source>Mark deleted for everyone</source> <target>Mark deleted for everyone</target> @@ -3263,11 +4715,36 @@ This is your link for group %@!</target> <target>Max 30 seconds, received instantly.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Media & file servers" xml:space="preserve"> + <source>Media & file servers</source> + <target>Media & file servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Medium" xml:space="preserve"> + <source>Medium</source> + <target>Medium</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Member" xml:space="preserve"> <source>Member</source> <target>Member</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member inactive" xml:space="preserve"> + <source>Member inactive</source> + <target>Member inactive</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Member reports" xml:space="preserve"> + <source>Member reports</source> + <target>Member reports</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All chat members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All chat members will be notified.</source> + <target>Member role will be changed to "%@". All chat members will be notified.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> <target>Member role will be changed to "%@". All group members will be notified.</target> @@ -3278,11 +4755,66 @@ This is your link for group %@!</target> <target>Member role will be changed to "%@". The member will receive a new invitation.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from chat - this cannot be undone!</source> + <target>Member will be removed from chat - this cannot be undone!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> <target>Member will be removed from group - this cannot be undone!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Members can add message reactions." xml:space="preserve"> + <source>Members can add message reactions.</source> + <target>Members can add message reactions.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Members can irreversibly delete sent messages. (24 hours)</source> + <target>Members can irreversibly delete sent messages. (24 hours)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can report messsages to moderators." xml:space="preserve"> + <source>Members can report messsages to moderators.</source> + <target>Members can report messsages to moderators.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send SimpleX links." xml:space="preserve"> + <source>Members can send SimpleX links.</source> + <target>Members can send SimpleX links.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> + <target>Members can send direct messages.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> + <target>Members can send disappearing messages.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send files and media." xml:space="preserve"> + <source>Members can send files and media.</source> + <target>Members can send files and media.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> + <target>Members can send voice messages.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mention members 👋" xml:space="preserve"> + <source>Mention members 👋</source> + <target>Mention members 👋</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Menus" xml:space="preserve"> + <source>Menus</source> + <target>Menus</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>Message delivery error</target> @@ -3293,11 +4825,31 @@ This is your link for group %@!</target> <target>Message delivery receipts!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message delivery warning" xml:space="preserve"> + <source>Message delivery warning</source> + <target>Message delivery warning</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> <target>Message draft</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message forwarded" xml:space="preserve"> + <source>Message forwarded</source> + <target>Message forwarded</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message may be delivered later if member becomes active." xml:space="preserve"> + <source>Message may be delivered later if member becomes active.</source> + <target>Message may be delivered later if member becomes active.</target> + <note>item status description</note> + </trans-unit> + <trans-unit id="Message queue info" xml:space="preserve"> + <source>Message queue info</source> + <target>Message queue info</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message reactions" xml:space="preserve"> <source>Message reactions</source> <target>Message reactions</target> @@ -3308,16 +4860,51 @@ This is your link for group %@!</target> <target>Message reactions are prohibited in this chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> - <source>Message reactions are prohibited in this group.</source> - <target>Message reactions are prohibited in this group.</target> + <trans-unit id="Message reactions are prohibited." xml:space="preserve"> + <source>Message reactions are prohibited.</source> + <target>Message reactions are prohibited.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message reception" xml:space="preserve"> + <source>Message reception</source> + <target>Message reception</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message servers" xml:space="preserve"> + <source>Message servers</source> + <target>Message servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message shape" xml:space="preserve"> + <source>Message shape</source> + <target>Message shape</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message source remains private." xml:space="preserve"> + <source>Message source remains private.</source> + <target>Message source remains private.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status" xml:space="preserve"> + <source>Message status</source> + <target>Message status</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status: %@" xml:space="preserve"> + <source>Message status: %@</source> + <target>Message status: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> <target>Message text</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message too large" xml:space="preserve"> + <source>Message too large</source> + <target>Message too large</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> <target>Messages</target> @@ -3333,11 +4920,76 @@ This is your link for group %@!</target> <target>Messages from %@ will be shown!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Messages in this chat will never be deleted." xml:space="preserve"> + <source>Messages in this chat will never be deleted.</source> + <target>Messages in this chat will never be deleted.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages received" xml:space="preserve"> + <source>Messages received</source> + <target>Messages received</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages sent" xml:space="preserve"> + <source>Messages sent</source> + <target>Messages sent</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages were deleted after you selected them." xml:space="preserve"> + <source>Messages were deleted after you selected them.</source> + <target>Messages were deleted after you selected them.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate device" xml:space="preserve"> + <source>Migrate device</source> + <target>Migrate device</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate from another device" xml:space="preserve"> + <source>Migrate from another device</source> + <target>Migrate from another device</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate here" xml:space="preserve"> + <source>Migrate here</source> + <target>Migrate here</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device" xml:space="preserve"> + <source>Migrate to another device</source> + <target>Migrate to another device</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device via QR code." xml:space="preserve"> + <source>Migrate to another device via QR code.</source> + <target>Migrate to another device via QR code.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating" xml:space="preserve"> + <source>Migrating</source> + <target>Migrating</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migrating database archive…" xml:space="preserve"> <source>Migrating database archive…</source> <target>Migrating database archive…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Migration complete" xml:space="preserve"> + <source>Migration complete</source> + <target>Migration complete</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> <target>Migration error:</target> @@ -3353,9 +5005,9 @@ This is your link for group %@!</target> <target>Migration is completed</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> - <target>Migrations: %@</target> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> + <target>Migrations:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> @@ -3373,21 +5025,31 @@ This is your link for group %@!</target> <target>Moderated at: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="More" xml:space="preserve"> + <source>More</source> + <target>More</target> + <note>swipe action</note> + </trans-unit> <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> <target>More improvements are coming soon!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="More reliable network connection." xml:space="preserve"> + <source>More reliable network connection.</source> + <target>More reliable network connection.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="More reliable notifications" xml:space="preserve"> + <source>More reliable notifications</source> + <target>More reliable notifications</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> <source>Most likely this connection is deleted.</source> <target>Most likely this connection is deleted.</target> <note>item status description</note> </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> - <source>Most likely this contact has deleted the connection with you.</source> - <target>Most likely this contact has deleted the connection with you.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> <target>Multiple chat profiles</target> @@ -3396,7 +5058,12 @@ This is your link for group %@!</target> <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> <target>Mute</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Mute all" xml:space="preserve"> + <source>Mute all</source> + <target>Mute all</target> + <note>notification label action</note> </trans-unit> <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> @@ -3406,13 +5073,38 @@ This is your link for group %@!</target> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> <target>Name</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> <target>Network & servers</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Network connection" xml:space="preserve"> + <source>Network connection</source> + <target>Network connection</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network decentralization" xml:space="preserve"> + <source>Network decentralization</source> + <target>Network decentralization</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve"> + <source>Network issues - message expired after many attempts to send it.</source> + <target>Network issues - message expired after many attempts to send it.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Network management" xml:space="preserve"> + <source>Network management</source> + <target>Network management</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network operator" xml:space="preserve"> + <source>Network operator</source> + <target>Network operator</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> <target>Network settings</target> @@ -3423,11 +5115,36 @@ This is your link for group %@!</target> <target>Network status</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New" xml:space="preserve"> + <source>New</source> + <target>New</target> + <note>token status text</note> + </trans-unit> <trans-unit id="New Passcode" xml:space="preserve"> <source>New Passcode</source> <target>New Passcode</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve"> + <source>New SOCKS credentials will be used every time you start the app.</source> + <target>New SOCKS credentials will be used every time you start the app.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve"> + <source>New SOCKS credentials will be used for each server.</source> + <target>New SOCKS credentials will be used for each server.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat" xml:space="preserve"> + <source>New chat</source> + <target>New chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat experience 🎉" xml:space="preserve"> + <source>New chat experience 🎉</source> + <target>New chat experience 🎉</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> <target>New contact request</target> @@ -3438,11 +5155,6 @@ This is your link for group %@!</target> <target>New contact:</target> <note>notification</note> </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> - <source>New database archive</source> - <target>New database archive</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="New desktop app!" xml:space="preserve"> <source>New desktop app!</source> <target>New desktop app!</target> @@ -3453,11 +5165,21 @@ This is your link for group %@!</target> <target>New display name</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>New events</target> + <note>notification</note> + </trans-unit> <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> <target>New in %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New media options" xml:space="preserve"> + <source>New media options</source> + <target>New media options</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> <target>New member role</target> @@ -3473,6 +5195,11 @@ This is your link for group %@!</target> <target>New passphrase…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New server" xml:space="preserve"> + <source>New server</source> + <target>New server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No" xml:space="preserve"> <source>No</source> <target>No</target> @@ -3483,6 +5210,21 @@ This is your link for group %@!</target> <target>No app password</target> <note>Authentication unavailable</note> </trans-unit> + <trans-unit id="No chats" xml:space="preserve"> + <source>No chats</source> + <target>No chats</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats found" xml:space="preserve"> + <source>No chats found</source> + <target>No chats found</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats in list %@" xml:space="preserve"> + <source>No chats in list %@</source> + <target>No chats in list %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> <target>No contacts selected</target> @@ -3503,6 +5245,11 @@ This is your link for group %@!</target> <target>No device token!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No direct connection yet, message is forwarded by admin." xml:space="preserve"> + <source>No direct connection yet, message is forwarded by admin.</source> + <target>No direct connection yet, message is forwarded by admin.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="No filtered chats" xml:space="preserve"> <source>No filtered chats</source> <target>No filtered chats</target> @@ -3518,21 +5265,111 @@ This is your link for group %@!</target> <target>No history</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No info, try to reload" xml:space="preserve"> + <source>No info, try to reload</source> + <target>No info, try to reload</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No media & file servers." xml:space="preserve"> + <source>No media & file servers.</source> + <target>No media & file servers.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No message" xml:space="preserve"> + <source>No message</source> + <target>No message</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No message servers." xml:space="preserve"> + <source>No message servers.</source> + <target>No message servers.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No network connection" xml:space="preserve"> + <source>No network connection</source> + <target>No network connection</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record speech" xml:space="preserve"> + <source>No permission to record speech</source> + <target>No permission to record speech</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record video" xml:space="preserve"> + <source>No permission to record video</source> + <target>No permission to record video</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> <source>No permission to record voice message</source> <target>No permission to record voice message</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No push server" xml:space="preserve"> + <source>No push server</source> + <target>No push server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> <target>No received or sent files</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No servers for private message routing." xml:space="preserve"> + <source>No servers for private message routing.</source> + <target>No servers for private message routing.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive files." xml:space="preserve"> + <source>No servers to receive files.</source> + <target>No servers to receive files.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive messages." xml:space="preserve"> + <source>No servers to receive messages.</source> + <target>No servers to receive messages.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to send files." xml:space="preserve"> + <source>No servers to send files.</source> + <target>No servers to send files.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No token!" xml:space="preserve"> + <source>No token!</source> + <target>No token!</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="No unread chats" xml:space="preserve"> + <source>No unread chats</source> + <target>No unread chats</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> + <target>No user identifiers.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Not compatible!" xml:space="preserve"> <source>Not compatible!</source> <target>Not compatible!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notes" xml:space="preserve"> + <source>Notes</source> + <target>Notes</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing selected" xml:space="preserve"> + <source>Nothing selected</source> + <target>Nothing selected</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing to forward!" xml:space="preserve"> + <source>Nothing to forward!</source> + <target>Nothing to forward!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> <target>Notifications</target> @@ -3543,6 +5380,21 @@ This is your link for group %@!</target> <target>Notifications are disabled!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notifications error" xml:space="preserve"> + <source>Notifications error</source> + <target>Notifications error</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Notifications privacy" xml:space="preserve"> + <source>Notifications privacy</source> + <target>Notifications privacy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications status" xml:space="preserve"> + <source>Notifications status</source> + <target>Notifications status</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. @@ -3552,44 +5404,43 @@ This is your link for group %@!</target> - disable members ("observer" role)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="OK" xml:space="preserve"> + <source>OK</source> + <target>OK</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Off" xml:space="preserve"> <source>Off</source> <target>Off</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> - <source>Off (Local)</source> - <target>Off (Local)</target> - <note>No comment provided by engineer.</note> + <note>blur media</note> </trans-unit> <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> <target>Ok</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> <target>Old database</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> - <source>Old database archive</source> - <target>Old database archive</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> <target>One-time invitation link</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> - <target>Onion hosts will be required for connection. Requires enabling VPN.</target> + <trans-unit id="Onion hosts will be **required** for connection. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be **required** for connection. +Requires compatible VPN.</source> + <target>Onion hosts will be **required** for connection. +Requires compatible VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> - <target>Onion hosts will be used when available. Requires enabling VPN.</target> + <trans-unit id="Onion hosts will be used when available. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be used when available. +Requires compatible VPN.</source> + <target>Onion hosts will be used when available. +Requires compatible VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will not be used." xml:space="preserve"> @@ -3597,9 +5448,19 @@ This is your link for group %@!</target> <target>Onion hosts will not be used.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> - <target>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</target> + <trans-unit id="Only chat owners can change preferences." xml:space="preserve"> + <source>Only chat owners can change preferences.</source> + <target>Only chat owners can change preferences.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> + <target>Only client devices store user profiles, contacts, groups, and messages.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only delete conversation" xml:space="preserve"> + <source>Only delete conversation</source> + <target>Only delete conversation</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> @@ -3617,14 +5478,24 @@ This is your link for group %@!</target> <target>Only group owners can enable voice messages.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only sender and moderators see it" xml:space="preserve"> + <source>Only sender and moderators see it</source> + <target>Only sender and moderators see it</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you and moderators see it" xml:space="preserve"> + <source>Only you and moderators see it</source> + <target>Only you and moderators see it</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only you can add message reactions." xml:space="preserve"> <source>Only you can add message reactions.</source> <target>Only you can add message reactions.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <target>Only you can irreversibly delete messages (your contact can mark them for deletion).</target> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source> + <target>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can make calls." xml:space="preserve"> @@ -3647,9 +5518,9 @@ This is your link for group %@!</target> <target>Only your contact can add message reactions.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <target>Only your contact can irreversibly delete messages (you can mark them for deletion).</target> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source> + <target>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can make calls." xml:space="preserve"> @@ -3670,13 +5541,18 @@ This is your link for group %@!</target> <trans-unit id="Open" xml:space="preserve"> <source>Open</source> <target>Open</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Open Settings</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open changes" xml:space="preserve"> + <source>Open changes</source> + <target>Open changes</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> <target>Open chat</target> @@ -3687,26 +5563,88 @@ This is your link for group %@!</target> <target>Open chat console</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Open conditions" xml:space="preserve"> + <source>Open conditions</source> + <target>Open conditions</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open group" xml:space="preserve"> <source>Open group</source> <target>Open group</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve"> - <source>Open user profiles</source> - <target>Open user profiles</target> + <trans-unit id="Open link?" xml:space="preserve"> + <source>Open link?</source> + <target>Open link?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Open migration to another device" xml:space="preserve"> + <source>Open migration to another device</source> + <target>Open migration to another device</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> - <target>Open-source protocol and code – anybody can run the servers.</target> + <trans-unit id="Opening app…" xml:space="preserve"> + <source>Opening app…</source> + <target>Opening app…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve"> - <source>Opening database…</source> - <target>Opening database…</target> + <trans-unit id="Operator" xml:space="preserve"> + <source>Operator</source> + <target>Operator</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Operator server" xml:space="preserve"> + <source>Operator server</source> + <target>Operator server</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Or import archive file" xml:space="preserve"> + <source>Or import archive file</source> + <target>Or import archive file</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or paste archive link" xml:space="preserve"> + <source>Or paste archive link</source> + <target>Or paste archive link</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or scan QR code" xml:space="preserve"> + <source>Or scan QR code</source> + <target>Or scan QR code</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or securely share this file link" xml:space="preserve"> + <source>Or securely share this file link</source> + <target>Or securely share this file link</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or show this code" xml:space="preserve"> + <source>Or show this code</source> + <target>Or show this code</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or to share privately" xml:space="preserve"> + <source>Or to share privately</source> + <target>Or to share privately</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Organize chats into lists" xml:space="preserve"> + <source>Organize chats into lists</source> + <target>Organize chats into lists</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other" xml:space="preserve"> + <source>Other</source> + <target>Other</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other file errors: %@" xml:space="preserve"> + <source>Other file errors: +%@</source> + <target>Other file errors: +%@</target> + <note>alert message</note> + </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> <target>PING count</target> @@ -3742,15 +5680,20 @@ This is your link for group %@!</target> <target>Passcode set!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Password" xml:space="preserve"> + <source>Password</source> + <target>Password</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> <target>Password to show</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste" xml:space="preserve"> - <source>Paste</source> - <target>Paste</target> - <note>No comment provided by engineer.</note> + <trans-unit id="Past member %@" xml:space="preserve"> + <source>Past member %@</source> + <target>Past member %@</target> + <note>past/unknown group member</note> </trans-unit> <trans-unit id="Paste desktop address" xml:space="preserve"> <source>Paste desktop address</source> @@ -3762,24 +5705,24 @@ This is your link for group %@!</target> <target>Paste image</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve"> - <source>Paste received link</source> - <target>Paste received link</target> + <trans-unit id="Paste link to connect!" xml:space="preserve"> + <source>Paste link to connect!</source> + <target>Paste link to connect!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> - <source>Paste the link you received to connect with your contact.</source> - <target>Paste the link you received to connect with your contact.</target> - <note>placeholder</note> - </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> - <target>People can connect to you only via the links you share.</target> + <trans-unit id="Paste the link you received" xml:space="preserve"> + <source>Paste the link you received</source> + <target>Paste the link you received</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Periodically" xml:space="preserve"> - <source>Periodically</source> - <target>Periodically</target> + <trans-unit id="Pending" xml:space="preserve"> + <source>Pending</source> + <target>Pending</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Periodic" xml:space="preserve"> + <source>Periodic</source> + <target>Periodic</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Permanent decryption error" xml:space="preserve"> @@ -3787,11 +5730,33 @@ This is your link for group %@!</target> <target>Permanent decryption error</target> <note>message decrypt error item</note> </trans-unit> + <trans-unit id="Picture-in-picture calls" xml:space="preserve"> + <source>Picture-in-picture calls</source> + <target>Picture-in-picture calls</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Play from the chat list." xml:space="preserve"> + <source>Play from the chat list.</source> + <target>Play from the chat list.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable calls." xml:space="preserve"> + <source>Please ask your contact to enable calls.</source> + <target>Please ask your contact to enable calls.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> <target>Please ask your contact to enable sending voice messages.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers." xml:space="preserve"> + <source>Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers.</source> + <target>Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> <target>Please check that you used the correct link or ask your contact to send you another one.</target> @@ -3807,6 +5772,18 @@ This is your link for group %@!</target> <target>Please check yours and your contact preferences.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve"> + <source>Please confirm that network settings are correct for this device.</source> + <target>Please confirm that network settings are correct for this device.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact developers. Error: %@" xml:space="preserve"> + <source>Please contact developers. +Error: %@</source> + <target>Please contact developers. +Error: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> <target>Please contact group admin.</target> @@ -3847,11 +5824,31 @@ This is your link for group %@!</target> <target>Please store passphrase securely, you will NOT be able to change it if you lose it.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please try to disable and re-enable notfications." xml:space="preserve"> + <source>Please try to disable and re-enable notfications.</source> + <target>Please try to disable and re-enable notfications.</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token activation to complete." xml:space="preserve"> + <source>Please wait for token activation to complete.</source> + <target>Please wait for token activation to complete.</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token to be registered." xml:space="preserve"> + <source>Please wait for token to be registered.</source> + <target>Please wait for token to be registered.</target> + <note>token info</note> + </trans-unit> <trans-unit id="Polish interface" xml:space="preserve"> <source>Polish interface</source> <target>Polish interface</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Port" xml:space="preserve"> + <source>Port</source> + <target>Port</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> <target>Possibly, certificate fingerprint in server address is incorrect</target> @@ -3862,36 +5859,86 @@ This is your link for group %@!</target> <target>Preserve the last message draft, with attachments.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Preset server" xml:space="preserve"> - <source>Preset server</source> - <target>Preset server</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> <target>Preset server address</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Preset servers" xml:space="preserve"> + <source>Preset servers</source> + <target>Preset servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Preview" xml:space="preserve"> <source>Preview</source> <target>Preview</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Previously connected servers" xml:space="preserve"> + <source>Previously connected servers</source> + <target>Previously connected servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> <target>Privacy & security</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Privacy for your customers." xml:space="preserve"> + <source>Privacy for your customers.</source> + <target>Privacy for your customers.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy policy and conditions of use." xml:space="preserve"> + <source>Privacy policy and conditions of use.</source> + <target>Privacy policy and conditions of use.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy redefined" xml:space="preserve"> <source>Privacy redefined</source> <target>Privacy redefined</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve"> + <source>Private chats, groups and your contacts are not accessible to server operators.</source> + <target>Private chats, groups and your contacts are not accessible to server operators.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> <target>Private filenames</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private media file names." xml:space="preserve"> + <source>Private media file names.</source> + <target>Private media file names.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing" xml:space="preserve"> + <source>Private message routing</source> + <target>Private message routing</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing 🚀" xml:space="preserve"> + <source>Private message routing 🚀</source> + <target>Private message routing 🚀</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private notes" xml:space="preserve"> + <source>Private notes</source> + <target>Private notes</target> + <note>name of notes to self</note> + </trans-unit> + <trans-unit id="Private routing" xml:space="preserve"> + <source>Private routing</source> + <target>Private routing</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private routing error" xml:space="preserve"> + <source>Private routing error</source> + <target>Private routing error</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> <target>Profile and server connections</target> @@ -3902,14 +5949,9 @@ This is your link for group %@!</target> <target>Profile image</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile name" xml:space="preserve"> - <source>Profile name</source> - <target>Profile name</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile name:" xml:space="preserve"> - <source>Profile name:</source> - <target>Profile name:</target> + <trans-unit id="Profile images" xml:space="preserve"> + <source>Profile images</source> + <target>Profile images</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile password" xml:space="preserve"> @@ -3917,10 +5959,15 @@ This is your link for group %@!</target> <target>Profile password</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Profile theme" xml:space="preserve"> + <source>Profile theme</source> + <target>Profile theme</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> <source>Profile update will be sent to your contacts.</source> <target>Profile update will be sent to your contacts.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> @@ -3942,6 +5989,16 @@ This is your link for group %@!</target> <target>Prohibit messages reactions.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Prohibit reporting messages to moderators." xml:space="preserve"> + <source>Prohibit reporting messages to moderators.</source> + <target>Prohibit reporting messages to moderators.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending SimpleX links." xml:space="preserve"> + <source>Prohibit sending SimpleX links.</source> + <target>Prohibit sending SimpleX links.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> <target>Prohibit sending direct messages to members.</target> @@ -3962,11 +6019,23 @@ This is your link for group %@!</target> <target>Prohibit sending voice messages.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect IP address" xml:space="preserve"> + <source>Protect IP address</source> + <target>Protect IP address</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> <target>Protect app screen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve"> + <source>Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings.</source> + <target>Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> <target>Protect your chat profiles with a password!</target> @@ -3982,16 +6051,46 @@ This is your link for group %@!</target> <target>Protocol timeout per KB</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Proxied" xml:space="preserve"> + <source>Proxied</source> + <target>Proxied</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied servers" xml:space="preserve"> + <source>Proxied servers</source> + <target>Proxied servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxy requires password" xml:space="preserve"> + <source>Proxy requires password</source> + <target>Proxy requires password</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> <target>Push notifications</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Push server" xml:space="preserve"> + <source>Push server</source> + <target>Push server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Quantum resistant encryption" xml:space="preserve"> + <source>Quantum resistant encryption</source> + <target>Quantum resistant encryption</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> <source>Rate the app</source> <target>Rate the app</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reachable chat toolbar" xml:space="preserve"> + <source>Reachable chat toolbar</source> + <target>Reachable chat toolbar</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> <target>React…</target> @@ -4000,16 +6099,21 @@ This is your link for group %@!</target> <trans-unit id="Read" xml:space="preserve"> <source>Read</source> <target>Read</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Read more" xml:space="preserve"> <source>Read more</source> <target>Read more</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> - <target>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source> + <target>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</source> + <target>Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> @@ -4017,11 +6121,6 @@ This is your link for group %@!</target> <target>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> - <source>Read more in our GitHub repository.</source> - <target>Read more in our GitHub repository.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> <target>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</target> @@ -4032,6 +6131,11 @@ This is your link for group %@!</target> <target>Receipts are disabled</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receive errors" xml:space="preserve"> + <source>Receive errors</source> + <target>Receive errors</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Received at</target> @@ -4052,6 +6156,21 @@ This is your link for group %@!</target> <target>Received message</target> <note>message info title</note> </trans-unit> + <trans-unit id="Received messages" xml:space="preserve"> + <source>Received messages</source> + <target>Received messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received reply" xml:space="preserve"> + <source>Received reply</source> + <target>Received reply</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received total" xml:space="preserve"> + <source>Received total</source> + <target>Received total</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> <target>Receiving address will be changed to a different server. Address change will complete after sender comes online.</target> @@ -4067,16 +6186,51 @@ This is your link for group %@!</target> <target>Receiving via</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Recent history and improved [directory bot](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)." xml:space="preserve"> + <source>Recent history and improved [directory bot](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).</source> + <target>Recent history and improved [directory bot](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).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve"> + <source>Recipient(s) can't see who this message is from.</source> + <target>Recipient(s) can't see who this message is from.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> <target>Recipients see updates as you type them.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect" xml:space="preserve"> + <source>Reconnect</source> + <target>Reconnect</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> <target>Reconnect all connected servers to force message delivery. It uses additional traffic.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect all servers" xml:space="preserve"> + <source>Reconnect all servers</source> + <target>Reconnect all servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers?" xml:space="preserve"> + <source>Reconnect all servers?</source> + <target>Reconnect all servers?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect server to force message delivery. It uses additional traffic.</source> + <target>Reconnect server to force message delivery. It uses additional traffic.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server?" xml:space="preserve"> + <source>Reconnect server?</source> + <target>Reconnect server?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> <target>Reconnect servers?</target> @@ -4097,10 +6251,26 @@ This is your link for group %@!</target> <target>Reduced battery usage</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Register" xml:space="preserve"> + <source>Register</source> + <target>Register</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Register notification token?" xml:space="preserve"> + <source>Register notification token?</source> + <target>Register notification token?</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Registered" xml:space="preserve"> + <source>Registered</source> + <target>Registered</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> <target>Reject</target> - <note>reject incoming call via notification</note> + <note>reject incoming call via notification +swipe action</note> </trans-unit> <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> <source>Reject (sender NOT notified)</source> @@ -4127,6 +6297,16 @@ This is your link for group %@!</target> <target>Remove</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Remove archive?" xml:space="preserve"> + <source>Remove archive?</source> + <target>Remove archive?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove image" xml:space="preserve"> + <source>Remove image</source> + <target>Remove image</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> <target>Remove member</target> @@ -4162,16 +6342,81 @@ This is your link for group %@!</target> <target>Repeat connection request?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat download" xml:space="preserve"> + <source>Repeat download</source> + <target>Repeat download</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat import" xml:space="preserve"> + <source>Repeat import</source> + <target>Repeat import</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Repeat join request?" xml:space="preserve"> <source>Repeat join request?</source> <target>Repeat join request?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat upload" xml:space="preserve"> + <source>Repeat upload</source> + <target>Repeat upload</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reply" xml:space="preserve"> <source>Reply</source> <target>Reply</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Report" xml:space="preserve"> + <source>Report</source> + <target>Report</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Report content: only group moderators will see it." xml:space="preserve"> + <source>Report content: only group moderators will see it.</source> + <target>Report content: only group moderators will see it.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report member profile: only group moderators will see it." xml:space="preserve"> + <source>Report member profile: only group moderators will see it.</source> + <target>Report member profile: only group moderators will see it.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report other: only group moderators will see it." xml:space="preserve"> + <source>Report other: only group moderators will see it.</source> + <target>Report other: only group moderators will see it.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report reason?" xml:space="preserve"> + <source>Report reason?</source> + <target>Report reason?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Report spam: only group moderators will see it." xml:space="preserve"> + <source>Report spam: only group moderators will see it.</source> + <target>Report spam: only group moderators will see it.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report violation: only group moderators will see it." xml:space="preserve"> + <source>Report violation: only group moderators will see it.</source> + <target>Report violation: only group moderators will see it.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report: %@" xml:space="preserve"> + <source>Report: %@</source> + <target>Report: %@</target> + <note>report in notification</note> + </trans-unit> + <trans-unit id="Reporting messages to moderators is prohibited." xml:space="preserve"> + <source>Reporting messages to moderators is prohibited.</source> + <target>Reporting messages to moderators is prohibited.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reports" xml:space="preserve"> + <source>Reports</source> + <target>Reports</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Required" xml:space="preserve"> <source>Required</source> <target>Required</target> @@ -4182,16 +6427,41 @@ This is your link for group %@!</target> <target>Reset</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset all hints" xml:space="preserve"> + <source>Reset all hints</source> + <target>Reset all hints</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics" xml:space="preserve"> + <source>Reset all statistics</source> + <target>Reset all statistics</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics?" xml:space="preserve"> + <source>Reset all statistics?</source> + <target>Reset all statistics?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> <target>Reset colors</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to app theme" xml:space="preserve"> + <source>Reset to app theme</source> + <target>Reset to app theme</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> <target>Reset to defaults</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to user theme" xml:space="preserve"> + <source>Reset to user theme</source> + <target>Reset to user theme</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> <source>Restart the app to create a new chat profile</source> <target>Restart the app to create a new chat profile</target> @@ -4222,14 +6492,19 @@ This is your link for group %@!</target> <target>Restore database error</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Retry" xml:space="preserve"> + <source>Retry</source> + <target>Retry</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> <target>Reveal</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Revert" xml:space="preserve"> - <source>Revert</source> - <target>Revert</target> + <trans-unit id="Review conditions" xml:space="preserve"> + <source>Review conditions</source> + <target>Review conditions</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke" xml:space="preserve"> @@ -4257,51 +6532,67 @@ This is your link for group %@!</target> <target>Run chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve"> - <source>SMP servers</source> - <target>SMP servers</target> + <trans-unit id="SMP server" xml:space="preserve"> + <source>SMP server</source> + <target>SMP server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SOCKS proxy" xml:space="preserve"> + <source>SOCKS proxy</source> + <target>SOCKS proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safely receive files" xml:space="preserve"> + <source>Safely receive files</source> + <target>Safely receive files</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safer groups" xml:space="preserve"> + <source>Safer groups</source> + <target>Safer groups</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save" xml:space="preserve"> <source>Save</source> <target>Save</target> - <note>chat item action</note> + <note>alert button +chat item action</note> </trans-unit> <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> <target>Save (and notify contacts)</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> <target>Save and notify contact</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> <target>Save and notify group members</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save and reconnect" xml:space="preserve"> + <source>Save and reconnect</source> + <target>Save and reconnect</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save and update group profile" xml:space="preserve"> <source>Save and update group profile</source> <target>Save and update group profile</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save archive" xml:space="preserve"> - <source>Save archive</source> - <target>Save archive</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve"> - <source>Save auto-accept settings</source> - <target>Save auto-accept settings</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> <source>Save group profile</source> <target>Save group profile</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save list" xml:space="preserve"> + <source>Save list</source> + <target>Save list</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> <target>Save passphrase and open chat</target> @@ -4315,7 +6606,7 @@ This is your link for group %@!</target> <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> <target>Save preferences?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> @@ -4330,23 +6621,53 @@ This is your link for group %@!</target> <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> <target>Save servers?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve"> - <source>Save settings?</source> - <target>Save settings?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> <target>Save welcome message?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save your profile?" xml:space="preserve"> + <source>Save your profile?</source> + <target>Save your profile?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Saved" xml:space="preserve"> + <source>Saved</source> + <target>Saved</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> <source>Saved WebRTC ICE servers will be removed</source> <target>Saved WebRTC ICE servers will be removed</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Saved from" xml:space="preserve"> + <source>Saved from</source> + <target>Saved from</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved message" xml:space="preserve"> + <source>Saved message</source> + <target>Saved message</target> + <note>message info title</note> + </trans-unit> + <trans-unit id="Saving %lld messages" xml:space="preserve"> + <source>Saving %lld messages</source> + <target>Saving %lld messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scale" xml:space="preserve"> + <source>Scale</source> + <target>Scale</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan / Paste link" xml:space="preserve"> + <source>Scan / Paste link</source> + <target>Scan / Paste link</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> <target>Scan QR code</target> @@ -4377,11 +6698,31 @@ This is your link for group %@!</target> <target>Search</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Search bar accepts invitation links." xml:space="preserve"> + <source>Search bar accepts invitation links.</source> + <target>Search bar accepts invitation links.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search or paste SimpleX link" xml:space="preserve"> + <source>Search or paste SimpleX link</source> + <target>Search or paste SimpleX link</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secondary" xml:space="preserve"> + <source>Secondary</source> + <target>Secondary</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> <source>Secure queue</source> <target>Secure queue</target> <note>server test step</note> </trans-unit> + <trans-unit id="Secured" xml:space="preserve"> + <source>Secured</source> + <target>Secured</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> <target>Security assessment</target> @@ -4395,6 +6736,21 @@ This is your link for group %@!</target> <trans-unit id="Select" xml:space="preserve"> <source>Select</source> <target>Select</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Select chat profile" xml:space="preserve"> + <source>Select chat profile</source> + <target>Select chat profile</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected %lld" xml:space="preserve"> + <source>Selected %lld</source> + <target>Selected %lld</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Selected chat preferences prohibit this message.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct" xml:space="preserve"> @@ -4432,11 +6788,6 @@ This is your link for group %@!</target> <target>Send delivery receipts to</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve"> - <source>Send direct message</source> - <target>Send direct message</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Send direct message to connect" xml:space="preserve"> <source>Send direct message to connect</source> <target>Send direct message to connect</target> @@ -4447,6 +6798,11 @@ This is your link for group %@!</target> <target>Send disappearing message</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send errors" xml:space="preserve"> + <source>Send errors</source> + <target>Send errors</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> <target>Send link previews</target> @@ -4457,14 +6813,29 @@ This is your link for group %@!</target> <target>Send live message</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send message to enable calls." xml:space="preserve"> + <source>Send message to enable calls.</source> + <target>Send message to enable calls.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source> + <target>Send messages directly when IP address is protected and your or destination server does not support private routing.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when your or destination server does not support private routing.</source> + <target>Send messages directly when your or destination server does not support private routing.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> <target>Send notifications</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> - <source>Send notifications:</source> - <target>Send notifications:</target> + <trans-unit id="Send private reports" xml:space="preserve"> + <source>Send private reports</source> + <target>Send private reports</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve"> @@ -4482,10 +6853,15 @@ This is your link for group %@!</target> <target>Send them from gallery or custom keyboards.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve"> + <source>Send up to 100 last messages to new members.</source> + <target>Send up to 100 last messages to new members.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> <target>Sender cancelled file transfer.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> @@ -4542,6 +6918,11 @@ This is your link for group %@!</target> <target>Sent at: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Sent directly" xml:space="preserve"> + <source>Sent directly</source> + <target>Sent directly</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> <target>Sent file event</target> @@ -4552,11 +6933,71 @@ This is your link for group %@!</target> <target>Sent message</target> <note>message info title</note> </trans-unit> + <trans-unit id="Sent messages" xml:space="preserve"> + <source>Sent messages</source> + <target>Sent messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> <target>Sent messages will be deleted after set time.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sent reply" xml:space="preserve"> + <source>Sent reply</source> + <target>Sent reply</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent total" xml:space="preserve"> + <source>Sent total</source> + <target>Sent total</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent via proxy" xml:space="preserve"> + <source>Sent via proxy</source> + <target>Sent via proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server" xml:space="preserve"> + <source>Server</source> + <target>Server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server added to operator %@." xml:space="preserve"> + <source>Server added to operator %@.</source> + <target>Server added to operator %@.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Server address" xml:space="preserve"> + <source>Server address</source> + <target>Server address</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings." xml:space="preserve"> + <source>Server address is incompatible with network settings.</source> + <target>Server address is incompatible with network settings.</target> + <note>srv error text.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Server address is incompatible with network settings: %@.</source> + <target>Server address is incompatible with network settings: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server operator changed." xml:space="preserve"> + <source>Server operator changed.</source> + <target>Server operator changed.</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Server operators" xml:space="preserve"> + <source>Server operators</source> + <target>Server operators</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server protocol changed." xml:space="preserve"> + <source>Server protocol changed.</source> + <target>Server protocol changed.</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> <target>Server requires authorization to create queues, check password</target> @@ -4572,11 +7013,36 @@ This is your link for group %@!</target> <target>Server test failed!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Server type" xml:space="preserve"> + <source>Server type</source> + <target>Server type</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server version is incompatible with network settings." xml:space="preserve"> + <source>Server version is incompatible with network settings.</source> + <target>Server version is incompatible with network settings.</target> + <note>srv error text</note> + </trans-unit> + <trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve"> + <source>Server version is incompatible with your app: %@.</source> + <target>Server version is incompatible with your app: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Servers" xml:space="preserve"> <source>Servers</source> <target>Servers</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Servers info" xml:space="preserve"> + <source>Servers info</source> + <target>Servers info</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <target>Servers statistics will be reset - this cannot be undone!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Session code" xml:space="preserve"> <source>Session code</source> <target>Session code</target> @@ -4587,11 +7053,21 @@ This is your link for group %@!</target> <target>Set 1 day</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set chat name…" xml:space="preserve"> + <source>Set chat name…</source> + <target>Set chat name…</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> <target>Set contact name…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve"> + <source>Set default theme</source> + <target>Set default theme</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> <target>Set group preferences</target> @@ -4602,11 +7078,21 @@ This is your link for group %@!</target> <target>Set it instead of system authentication.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve"> + <source>Set message expiration in chats.</source> + <target>Set message expiration in chats.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passcode" xml:space="preserve"> <source>Set passcode</source> <target>Set passcode</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set passphrase" xml:space="preserve"> + <source>Set passphrase</source> + <target>Set passphrase</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> <target>Set passphrase to export</target> @@ -4627,24 +7113,55 @@ This is your link for group %@!</target> <target>Settings</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve"> + <source>Settings were changed.</source> + <target>Settings were changed.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve"> + <source>Shape profile images</source> + <target>Shape profile images</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share" xml:space="preserve"> <source>Share</source> <target>Share</target> - <note>chat item action</note> + <note>alert action +chat item action</note> </trans-unit> <trans-unit id="Share 1-time link" xml:space="preserve"> <source>Share 1-time link</source> <target>Share 1-time link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share 1-time link with a friend" xml:space="preserve"> + <source>Share 1-time link with a friend</source> + <target>Share 1-time link with a friend</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve"> + <source>Share SimpleX address on social media.</source> + <target>Share SimpleX address on social media.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address" xml:space="preserve"> <source>Share address</source> <target>Share address</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share address publicly" xml:space="preserve"> + <source>Share address publicly</source> + <target>Share address publicly</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address with contacts?" xml:space="preserve"> <source>Share address with contacts?</source> <target>Share address with contacts?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Share from other apps." xml:space="preserve"> + <source>Share from other apps.</source> + <target>Share from other apps.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share link" xml:space="preserve"> @@ -4652,9 +7169,19 @@ This is your link for group %@!</target> <target>Share link</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve"> - <source>Share one-time invitation link</source> - <target>Share one-time invitation link</target> + <trans-unit id="Share profile" xml:space="preserve"> + <source>Share profile</source> + <target>Share profile</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share this 1-time invite link" xml:space="preserve"> + <source>Share this 1-time invite link</source> + <target>Share this 1-time invite link</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share to SimpleX" xml:space="preserve"> + <source>Share to SimpleX</source> + <target>Share to SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share with contacts" xml:space="preserve"> @@ -4662,6 +7189,16 @@ This is your link for group %@!</target> <target>Share with contacts</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Short link" xml:space="preserve"> + <source>Short link</source> + <target>Short link</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show QR code" xml:space="preserve"> + <source>Show QR code</source> + <target>Show QR code</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> <target>Show calls in phone history</target> @@ -4677,21 +7214,46 @@ This is your link for group %@!</target> <target>Show last messages</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show message status" xml:space="preserve"> + <source>Show message status</source> + <target>Show message status</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show percentage" xml:space="preserve"> + <source>Show percentage</source> + <target>Show percentage</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Show preview</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show → on messages sent via private routing." xml:space="preserve"> + <source>Show → on messages sent via private routing.</source> + <target>Show → on messages sent via private routing.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show:" xml:space="preserve"> <source>Show:</source> <target>Show:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve"> + <source>SimpleX</source> + <target>SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Address" xml:space="preserve"> <source>SimpleX Address</source> <target>SimpleX Address</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." xml:space="preserve"> + <source>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</source> + <target>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> <target>SimpleX Chat security was audited by Trail of Bits.</target> @@ -4722,6 +7284,21 @@ This is your link for group %@!</target> <target>SimpleX address</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX address and 1-time links are safe to share via any messenger." xml:space="preserve"> + <source>SimpleX address and 1-time links are safe to share via any messenger.</source> + <target>SimpleX address and 1-time links are safe to share via any messenger.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address or 1-time link?" xml:space="preserve"> + <source>SimpleX address or 1-time link?</source> + <target>SimpleX address or 1-time link?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX channel link" xml:space="preserve"> + <source>SimpleX channel link</source> + <target>SimpleX channel link</target> + <note>simplex link type</note> + </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> <target>SimpleX contact address</target> @@ -4740,6 +7317,16 @@ This is your link for group %@!</target> <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> <target>SimpleX links</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve"> + <source>SimpleX links are prohibited.</source> + <target>SimpleX links are prohibited.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve"> + <source>SimpleX links not allowed</source> + <target>SimpleX links not allowed</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> @@ -4747,11 +7334,21 @@ This is your link for group %@!</target> <target>SimpleX one-time invitation</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> + <source>SimpleX protocols reviewed by Trail of Bits.</source> + <target>SimpleX protocols reviewed by Trail of Bits.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Simplified incognito mode" xml:space="preserve"> <source>Simplified incognito mode</source> <target>Simplified incognito mode</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Size" xml:space="preserve"> + <source>Size</source> + <target>Size</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Skip</target> @@ -4767,19 +7364,52 @@ This is your link for group %@!</target> <target>Small groups (max 20)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Soft" xml:space="preserve"> + <source>Soft</source> + <target>Soft</target> + <note>blur media</note> + </trans-unit> + <trans-unit id="Some app settings were not migrated." xml:space="preserve"> + <source>Some app settings were not migrated.</source> + <target>Some app settings were not migrated.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some file(s) were not exported:" xml:space="preserve"> + <source>Some file(s) were not exported:</source> + <target>Some file(s) were not exported:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Some non-fatal errors occurred during import - you may see Chat console for more details.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import:" xml:space="preserve"> + <source>Some non-fatal errors occurred during import:</source> + <target>Some non-fatal errors occurred during import:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some servers failed the test: %@" xml:space="preserve"> + <source>Some servers failed the test: +%@</source> + <target>Some servers failed the test: +%@</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> <target>Somebody</target> <note>notification title</note> </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve"> - <source>Start a new chat</source> - <target>Start a new chat</target> + <trans-unit id="Spam" xml:space="preserve"> + <source>Spam</source> + <target>Spam</target> + <note>blocking reason +report reason</note> + </trans-unit> + <trans-unit id="Square, circle, or anything in between." xml:space="preserve"> + <source>Square, circle, or anything in between.</source> + <target>Square, circle, or anything in between.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start chat" xml:space="preserve"> @@ -4787,11 +7417,26 @@ This is your link for group %@!</target> <target>Start chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Start chat?" xml:space="preserve"> + <source>Start chat?</source> + <target>Start chat?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> <target>Start migration</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Starting from %@." xml:space="preserve"> + <source>Starting from %@.</source> + <target>Starting from %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Statistics" xml:space="preserve"> + <source>Statistics</source> + <target>Statistics</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> <target>Stop</target> @@ -4802,9 +7447,9 @@ This is your link for group %@!</target> <target>Stop SimpleX</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> - <source>Stop chat to enable database actions</source> - <target>Stop chat to enable database actions</target> + <trans-unit id="Stop chat" xml:space="preserve"> + <source>Stop chat</source> + <target>Stop chat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> @@ -4835,23 +7480,63 @@ This is your link for group %@!</target> <trans-unit id="Stop sharing" xml:space="preserve"> <source>Stop sharing</source> <target>Stop sharing</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Stop sharing address?" xml:space="preserve"> <source>Stop sharing address?</source> <target>Stop sharing address?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Stopping chat" xml:space="preserve"> + <source>Stopping chat</source> + <target>Stopping chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Storage" xml:space="preserve"> + <source>Storage</source> + <target>Storage</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Strong" xml:space="preserve"> + <source>Strong</source> + <target>Strong</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Submit" xml:space="preserve"> <source>Submit</source> <target>Submit</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Subscribed" xml:space="preserve"> + <source>Subscribed</source> + <target>Subscribed</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscription errors" xml:space="preserve"> + <source>Subscription errors</source> + <target>Subscription errors</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscriptions ignored" xml:space="preserve"> + <source>Subscriptions ignored</source> + <target>Subscriptions ignored</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Support SimpleX Chat" xml:space="preserve"> <source>Support SimpleX Chat</source> <target>Support SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Switch audio and video during the call." xml:space="preserve"> + <source>Switch audio and video during the call.</source> + <target>Switch audio and video during the call.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> + <source>Switch chat profile for 1-time invitations.</source> + <target>Switch chat profile for 1-time invitations.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="System" xml:space="preserve"> <source>System</source> <target>System</target> @@ -4862,11 +7547,21 @@ This is your link for group %@!</target> <target>System authentication</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP connection" xml:space="preserve"> + <source>TCP connection</source> + <target>TCP connection</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> <target>TCP connection timeout</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP port for messaging" xml:space="preserve"> + <source>TCP port for messaging</source> + <target>TCP port for messaging</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <source>TCP_KEEPCNT</source> <target>TCP_KEEPCNT</target> @@ -4882,11 +7577,21 @@ This is your link for group %@!</target> <target>TCP_KEEPINTVL</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tail" xml:space="preserve"> + <source>Tail</source> + <target>Tail</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> <target>Take picture</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap Create SimpleX address in the menu to create it later." xml:space="preserve"> + <source>Tap Create SimpleX address in the menu to create it later.</source> + <target>Tap Create SimpleX address in the menu to create it later.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> <target>Tap button </target> @@ -4912,16 +7617,31 @@ This is your link for group %@!</target> <target>Tap to join incognito</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve"> - <source>Tap to start a new chat</source> - <target>Tap to start a new chat</target> + <trans-unit id="Tap to paste link" xml:space="preserve"> + <source>Tap to paste link</source> + <target>Tap to paste link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap to scan" xml:space="preserve"> + <source>Tap to scan</source> + <target>Tap to scan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Temporary file error" xml:space="preserve"> + <source>Temporary file error</source> + <target>Temporary file error</target> + <note>file error alert title</note> + </trans-unit> <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> <target>Test failed at step %@.</target> <note>server test failure</note> </trans-unit> + <trans-unit id="Test notifications" xml:space="preserve"> + <source>Test notifications</source> + <target>Test notifications</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> <target>Test server</target> @@ -4935,7 +7655,7 @@ This is your link for group %@!</target> <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> <target>Tests failed!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> @@ -4952,11 +7672,6 @@ This is your link for group %@!</target> <target>Thanks to the users – contribute via Weblate!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> - <target>The 1st platform without any user identifiers – private by design.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <source>The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised.</source> @@ -4969,11 +7684,31 @@ It can happen because of some bug or when the connection is compromised.</target <target>The app can notify you when you receive messages or contact requests - please open settings to enable.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The app protects your privacy by using different operators in each conversation." xml:space="preserve"> + <source>The app protects your privacy by using different operators in each conversation.</source> + <target>The app protects your privacy by using different operators in each conversation.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve"> + <source>The app will ask to confirm downloads from unknown file servers (except .onion).</source> + <target>The app will ask to confirm downloads from unknown file servers (except .onion).</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> <source>The attempt to change database passphrase was not completed.</source> <target>The attempt to change database passphrase was not completed.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve"> + <source>The code you scanned is not a SimpleX link QR code.</source> + <target>The code you scanned is not a SimpleX link QR code.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The connection reached the limit of undelivered messages, your contact may be offline." xml:space="preserve"> + <source>The connection reached the limit of undelivered messages, your contact may be offline.</source> + <target>The connection reached the limit of undelivered messages, your contact may be offline.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> <target>The connection you accepted will be cancelled!</target> @@ -4994,6 +7729,11 @@ It can happen because of some bug or when the connection is compromised.</target <target>The encryption is working and the new encryption agreement is not required. It may result in connection errors!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> + <target>The future of messaging</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The hash of the previous message is different." xml:space="preserve"> <source>The hash of the previous message is different.</source> <target>The hash of the previous message is different.</target> @@ -5009,9 +7749,14 @@ It can happen because of some bug or when the connection is compromised.</target <target>The message will be marked as moderated for all members.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> - <target>The next generation of private messaging</target> + <trans-unit id="The messages will be deleted for all members." xml:space="preserve"> + <source>The messages will be deleted for all members.</source> + <target>The messages will be deleted for all members.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve"> + <source>The messages will be marked as moderated for all members.</source> + <target>The messages will be marked as moderated for all members.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> @@ -5019,9 +7764,14 @@ It can happen because of some bug or when the connection is compromised.</target <target>The old database was not removed during the migration, it can be deleted.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> - <target>The profile is only shared with your contacts.</target> + <trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve"> + <source>The same conditions will apply to operator **%@**.</source> + <target>The same conditions will apply to operator **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second preset operator in the app!" xml:space="preserve"> + <source>The second preset operator in the app!</source> + <target>The second preset operator in the app!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> @@ -5039,19 +7789,29 @@ It can happen because of some bug or when the connection is compromised.</target <target>The servers for new connections of your current chat profile **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Theme" xml:space="preserve"> - <source>Theme</source> - <target>Theme</target> + <trans-unit id="The servers for new files of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new files of your current chat profile **%@**.</source> + <target>The servers for new files of your current chat profile **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve"> - <source>There should be at least one user profile.</source> - <target>There should be at least one user profile.</target> + <trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve"> + <source>The text you pasted is not a SimpleX link.</source> + <target>The text you pasted is not a SimpleX link.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> - <source>There should be at least one visible user profile.</source> - <target>There should be at least one visible user profile.</target> + <trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve"> + <source>The uploaded database archive will be permanently removed from the servers.</source> + <target>The uploaded database archive will be permanently removed from the servers.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Themes" xml:space="preserve"> + <source>Themes</source> + <target>Themes</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="These conditions will also apply for: **%@**." xml:space="preserve"> + <source>These conditions will also apply for: **%@**.</source> + <target>These conditions will also apply for: **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> @@ -5074,16 +7834,36 @@ It can happen because of some bug or when the connection is compromised.</target <target>This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</source> + <target>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> <target>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by end-to-end encryption.</source> + <target>This chat is protected by end-to-end encryption.</target> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by quantum resistant end-to-end encryption.</source> + <target>This chat is protected by quantum resistant end-to-end encryption.</target> + <note>E2EE info chat item</note> + </trans-unit> <trans-unit id="This device name" xml:space="preserve"> <source>This device name</source> <target>This device name</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve"> + <source>This display name is invalid. Please choose another name.</source> + <target>This display name is invalid. Please choose another name.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> <source>This group has over %lld members, delivery receipts are not sent.</source> <target>This group has over %lld members, delivery receipts are not sent.</target> @@ -5104,11 +7884,31 @@ It can happen because of some bug or when the connection is compromised.</target <target>This is your own one-time link!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve"> + <source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source> + <target>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve"> + <source>This link was used with another mobile device, please create a new link on the desktop.</source> + <target>This link was used with another mobile device, please create a new link on the desktop.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This message was deleted or not received yet." xml:space="preserve"> + <source>This message was deleted or not received yet.</source> + <target>This message was deleted or not received yet.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <source>This setting applies to messages in your current chat profile **%@**.</source> <target>This setting applies to messages in your current chat profile **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Title" xml:space="preserve"> + <source>Title</source> + <target>Title</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> <source>To ask any questions and to receive updates:</source> <target>To ask any questions and to receive updates:</target> @@ -5129,9 +7929,9 @@ It can happen because of some bug or when the connection is compromised.</target <target>To make a new connection</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <target>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</target> + <trans-unit id="To protect against your link being replaced, you can compare contact security codes." xml:space="preserve"> + <source>To protect against your link being replaced, you can compare contact security codes.</source> + <target>To protect against your link being replaced, you can compare contact security codes.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -5139,6 +7939,11 @@ It can happen because of some bug or when the connection is compromised.</target <target>To protect timezone, image/voice files use UTC.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve"> + <source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source> + <target>To protect your IP address, private routing uses your SMP servers to deliver messages.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> @@ -5146,6 +7951,26 @@ You will be prompted to complete authentication before this feature is enabled.< You will be prompted to complete authentication before this feature is enabled.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> + <target>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To receive" xml:space="preserve"> + <source>To receive</source> + <target>To receive</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve"> + <source>To record speech please grant permission to use Microphone.</source> + <target>To record speech please grant permission to use Microphone.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record video please grant permission to use Camera." xml:space="preserve"> + <source>To record video please grant permission to use Camera.</source> + <target>To record video please grant permission to use Camera.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> <target>To record voice message please grant permission to use Microphone.</target> @@ -5156,26 +7981,61 @@ You will be prompted to complete authentication before this feature is enabled.< <target>To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To send" xml:space="preserve"> + <source>To send</source> + <target>To send</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> <source>To support instant push notifications the chat database has to be migrated.</source> <target>To support instant push notifications the chat database has to be migrated.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To use the servers of **%@**, accept conditions of use." xml:space="preserve"> + <source>To use the servers of **%@**, accept conditions of use.</source> + <target>To use the servers of **%@**, accept conditions of use.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> <target>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle chat list:" xml:space="preserve"> + <source>Toggle chat list:</source> + <target>Toggle chat list:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> <source>Toggle incognito when connecting.</source> <target>Toggle incognito when connecting.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Token status: %@." xml:space="preserve"> + <source>Token status: %@.</source> + <target>Token status: %@.</target> + <note>token status</note> + </trans-unit> + <trans-unit id="Toolbar opacity" xml:space="preserve"> + <source>Toolbar opacity</source> + <target>Toolbar opacity</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Total" xml:space="preserve"> + <source>Total</source> + <target>Total</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>Transport isolation</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Transport sessions" xml:space="preserve"> + <source>Transport sessions</source> + <target>Transport sessions</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> <target>Trying to connect to the server used to receive messages from this contact (error: %@).</target> @@ -5186,16 +8046,16 @@ You will be prompted to complete authentication before this feature is enabled.< <target>Trying to connect to the server used to receive messages from this contact.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Turkish interface" xml:space="preserve"> + <source>Turkish interface</source> + <target>Turkish interface</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> <target>Turn off</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve"> - <source>Turn off notifications?</source> - <target>Turn off notifications?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> <target>Turn on</target> @@ -5211,20 +8071,30 @@ You will be prompted to complete authentication before this feature is enabled.< <target>Unblock</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock for all" xml:space="preserve"> + <source>Unblock for all</source> + <target>Unblock for all</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member" xml:space="preserve"> <source>Unblock member</source> <target>Unblock member</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock member for all?" xml:space="preserve"> + <source>Unblock member for all?</source> + <target>Unblock member for all?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member?" xml:space="preserve"> <source>Unblock member?</source> <target>Unblock member?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve"> - <source>Unexpected error: %@</source> - <target>Unexpected error: %@</target> - <note>item status description</note> + <trans-unit id="Undelivered messages" xml:space="preserve"> + <source>Undelivered messages</source> + <target>Undelivered messages</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -5234,7 +8104,7 @@ You will be prompted to complete authentication before this feature is enabled.< <trans-unit id="Unfav." xml:space="preserve"> <source>Unfav.</source> <target>Unfav.</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> @@ -5271,6 +8141,11 @@ You will be prompted to complete authentication before this feature is enabled.< <target>Unknown error</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unknown servers!" xml:space="preserve"> + <source>Unknown servers!</source> + <target>Unknown servers!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> <target>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</target> @@ -5306,11 +8181,21 @@ To connect, please ask your contact to create another connection link and check <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> <target>Unmute</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> </trans-unit> <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> <target>Unread</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Unsupported connection link" xml:space="preserve"> + <source>Unsupported connection link</source> + <target>Unsupported connection link</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve"> + <source>Up to 100 last messages are sent to new members.</source> + <target>Up to 100 last messages are sent to new members.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update" xml:space="preserve"> @@ -5318,11 +8203,6 @@ To connect, please ask your contact to create another connection link and check <target>Update</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> - <source>Update .onion hosts setting?</source> - <target>Update .onion hosts setting?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> <target>Update database passphrase</target> @@ -5333,9 +8213,14 @@ To connect, please ask your contact to create another connection link and check <target>Update network settings?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve"> - <source>Update transport isolation mode?</source> - <target>Update transport isolation mode?</target> + <trans-unit id="Update settings?" xml:space="preserve"> + <source>Update settings?</source> + <target>Update settings?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updated conditions" xml:space="preserve"> + <source>Updated conditions</source> + <target>Updated conditions</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> @@ -5343,31 +8228,71 @@ To connect, please ask your contact to create another connection link and check <target>Updating settings will re-connect the client to all servers.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> - <target>Updating this setting will re-connect the client to all servers.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Upgrade and open chat" xml:space="preserve"> <source>Upgrade and open chat</source> <target>Upgrade and open chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Upload errors" xml:space="preserve"> + <source>Upload errors</source> + <target>Upload errors</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload failed" xml:space="preserve"> + <source>Upload failed</source> + <target>Upload failed</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Upload file" xml:space="preserve"> <source>Upload file</source> <target>Upload file</target> <note>server test step</note> </trans-unit> + <trans-unit id="Uploaded" xml:space="preserve"> + <source>Uploaded</source> + <target>Uploaded</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploaded files" xml:space="preserve"> + <source>Uploaded files</source> + <target>Uploaded files</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploading archive" xml:space="preserve"> + <source>Uploading archive</source> + <target>Uploading archive</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use %@" xml:space="preserve"> + <source>Use %@</source> + <target>Use %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> <target>Use .onion hosts</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use SOCKS proxy" xml:space="preserve"> + <source>Use SOCKS proxy</source> + <target>Use SOCKS proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> <target>Use SimpleX Chat servers?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use TCP port %@ when no port is specified." xml:space="preserve"> + <source>Use TCP port %@ when no port is specified.</source> + <target>Use TCP port %@ when no port is specified.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve"> + <source>Use TCP port 443 for preset servers only.</source> + <target>Use TCP port 443 for preset servers only.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> <target>Use chat</target> @@ -5378,6 +8303,16 @@ To connect, please ask your contact to create another connection link and check <target>Use current profile</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use for files" xml:space="preserve"> + <source>Use for files</source> + <target>Use for files</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for messages" xml:space="preserve"> + <source>Use for messages</source> + <target>Use for messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Use for new connections</target> @@ -5398,19 +8333,59 @@ To connect, please ask your contact to create another connection link and check <target>Use new incognito profile</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use only local notifications?" xml:space="preserve"> + <source>Use only local notifications?</source> + <target>Use only local notifications?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve"> + <source>Use private routing with unknown servers when IP address is not protected.</source> + <target>Use private routing with unknown servers when IP address is not protected.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers." xml:space="preserve"> + <source>Use private routing with unknown servers.</source> + <target>Use private routing with unknown servers.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Use server</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> - <target>User profile</target> + <trans-unit id="Use servers" xml:space="preserve"> + <source>Use servers</source> + <target>Use servers</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> - <target>Using .onion hosts requires compatible VPN provider.</target> + <trans-unit id="Use short links (BETA)" xml:space="preserve"> + <source>Use short links (BETA)</source> + <target>Use short links (BETA)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app while in the call." xml:space="preserve"> + <source>Use the app while in the call.</source> + <target>Use the app while in the call.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app with one hand." xml:space="preserve"> + <source>Use the app with one hand.</source> + <target>Use the app with one hand.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use web port" xml:space="preserve"> + <source>Use web port</source> + <target>Use web port</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="User selection" xml:space="preserve"> + <source>User selection</source> + <target>User selection</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Username" xml:space="preserve"> + <source>Username</source> + <target>Username</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> @@ -5438,6 +8413,16 @@ To connect, please ask your contact to create another connection link and check <target>Verify connections</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Verify database passphrase" xml:space="preserve"> + <source>Verify database passphrase</source> + <target>Verify database passphrase</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify passphrase" xml:space="preserve"> + <source>Verify passphrase</source> + <target>Verify passphrase</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Verify security code" xml:space="preserve"> <source>Verify security code</source> <target>Verify security code</target> @@ -5473,11 +8458,26 @@ To connect, please ask your contact to create another connection link and check <target>Videos and files up to 1gb</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View conditions" xml:space="preserve"> + <source>View conditions</source> + <target>View conditions</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> <target>View security code</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View updated conditions" xml:space="preserve"> + <source>View updated conditions</source> + <target>View updated conditions</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Visible history" xml:space="preserve"> + <source>Visible history</source> + <target>Visible history</target> + <note>chat feature</note> + </trans-unit> <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> <target>Voice messages</target> @@ -5488,9 +8488,14 @@ To connect, please ask your contact to create another connection link and check <target>Voice messages are prohibited in this chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> - <target>Voice messages are prohibited in this group.</target> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> + <target>Voice messages are prohibited.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Voice messages not allowed" xml:space="preserve"> + <source>Voice messages not allowed</source> + <target>Voice messages not allowed</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> @@ -5523,6 +8528,21 @@ To connect, please ask your contact to create another connection link and check <target>Waiting for video</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wallpaper accent" xml:space="preserve"> + <source>Wallpaper accent</source> + <target>Wallpaper accent</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper background" xml:space="preserve"> + <source>Wallpaper background</source> + <target>Wallpaper background</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve"> + <source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source> + <target>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> <source>Warning: you may lose some data!</source> <target>Warning: you may lose some data!</target> @@ -5543,6 +8563,11 @@ To connect, please ask your contact to create another connection link and check <target>Welcome message</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Welcome message is too long" xml:space="preserve"> + <source>Welcome message is too long</source> + <target>Welcome message is too long</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="What's new" xml:space="preserve"> <source>What's new</source> <target>What's new</target> @@ -5553,9 +8578,14 @@ To connect, please ask your contact to create another connection link and check <target>When available</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> - <source>When people request to connect, you can accept or reject it.</source> - <target>When people request to connect, you can accept or reject it.</target> + <trans-unit id="When connecting audio and video calls." xml:space="preserve"> + <source>When connecting audio and video calls.</source> + <target>When connecting audio and video calls.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When more than one operator is enabled, none of them has metadata to learn who communicates with whom." xml:space="preserve"> + <source>When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</source> + <target>When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> @@ -5563,29 +8593,74 @@ To connect, please ask your contact to create another connection link and check <target>When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="WiFi" xml:space="preserve"> + <source>WiFi</source> + <target>WiFi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Will be enabled in direct chats!" xml:space="preserve"> + <source>Will be enabled in direct chats!</source> + <target>Will be enabled in direct chats!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wired ethernet" xml:space="preserve"> + <source>Wired ethernet</source> + <target>Wired ethernet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With encrypted files and media." xml:space="preserve"> + <source>With encrypted files and media.</source> + <target>With encrypted files and media.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> <target>With optional welcome message.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="With reduced battery usage." xml:space="preserve"> + <source>With reduced battery usage.</source> + <target>With reduced battery usage.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to file servers.</source> + <target>Without Tor or VPN, your IP address will be visible to file servers.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source> + <target>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> <target>Wrong database passphrase</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve"> + <source>Wrong key or unknown connection - most likely this connection is deleted.</source> + <target>Wrong key or unknown connection - most likely this connection is deleted.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Wrong key or unknown file chunk address - most likely file is deleted." xml:space="preserve"> + <source>Wrong key or unknown file chunk address - most likely file is deleted.</source> + <target>Wrong key or unknown file chunk address - most likely file is deleted.</target> + <note>file error text</note> + </trans-unit> <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> <target>Wrong passphrase!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="XFTP servers" xml:space="preserve"> - <source>XFTP servers</source> - <target>XFTP servers</target> + <trans-unit id="XFTP server" xml:space="preserve"> + <source>XFTP server</source> + <target>XFTP server</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> - <target>You</target> + <trans-unit id="You **must not** use the same database on two devices." xml:space="preserve"> + <source>You **must not** use the same database on two devices.</source> + <target>You **must not** use the same database on two devices.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You accepted connection" xml:space="preserve"> @@ -5608,6 +8683,11 @@ To connect, please ask your contact to create another connection link and check <target>You are already connected to %@.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are already connected with %@." xml:space="preserve"> + <source>You are already connected with %@.</source> + <target>You are already connected with %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You are already connecting to %@." xml:space="preserve"> <source>You are already connecting to %@.</source> <target>You are already connecting to %@.</target> @@ -5655,14 +8735,24 @@ Repeat join request?</target> <target>You are invited to group</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are not connected to these servers. Private routing is used to deliver messages to them." xml:space="preserve"> + <source>You are not connected to these servers. Private routing is used to deliver messages to them.</source> + <target>You are not connected to these servers. Private routing is used to deliver messages to them.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> <source>You can accept calls from lock screen, without device and app authentication.</source> <target>You can accept calls from lock screen, without device and app authentication.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <target>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</target> + <trans-unit id="You can change it in Appearance settings." xml:space="preserve"> + <source>You can change it in Appearance settings.</source> + <target>You can change it in Appearance settings.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can configure servers via settings." xml:space="preserve"> + <source>You can configure servers via settings.</source> + <target>You can configure servers via settings.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can create it later" xml:space="preserve"> @@ -5680,16 +8770,36 @@ Repeat join request?</target> <target>You can enable them later via app Privacy & Security settings.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can give another try." xml:space="preserve"> + <source>You can give another try.</source> + <target>You can give another try.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <source>You can hide or mute a user profile - swipe it to the right.</source> <target>You can hide or mute a user profile - swipe it to the right.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> - <target>You can now send messages to %@</target> + <trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve"> + <source>You can make it visible to your SimpleX contacts via Settings.</source> + <target>You can make it visible to your SimpleX contacts via Settings.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now chat with %@" xml:space="preserve"> + <source>You can now chat with %@</source> + <target>You can now chat with %@</target> <note>notification body</note> </trans-unit> + <trans-unit id="You can send messages to %@ from Archived contacts." xml:space="preserve"> + <source>You can send messages to %@ from Archived contacts.</source> + <target>You can send messages to %@ from Archived contacts.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can set connection name, to remember who the link was shared with." xml:space="preserve"> + <source>You can set connection name, to remember who the link was shared with.</source> + <target>You can set connection name, to remember who the link was shared with.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> <target>You can set lock screen notification preview via settings.</target> @@ -5705,16 +8815,16 @@ Repeat join request?</target> <target>You can share this address with your contacts to let them connect with **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> - <source>You can share your address as a link or QR code - anybody can connect to you.</source> - <target>You can share your address as a link or QR code - anybody can connect to you.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> <target>You can start chat via app Settings / Database or by restarting the app</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve"> + <source>You can still view conversation with %@ in the list of chats.</source> + <target>You can still view conversation with %@ in the list of chats.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> <target>You can turn on SimpleX Lock via Settings.</target> @@ -5725,21 +8835,26 @@ Repeat join request?</target> <target>You can use markdown to format messages:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can view invitation link again in connection details." xml:space="preserve"> + <source>You can view invitation link again in connection details.</source> + <target>You can view invitation link again in connection details.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> <target>You can't send messages!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <target>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <target>You could not be verified; please try again.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> + <target>You decide who can connect.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You have already requested connection via this address!" xml:space="preserve"> <source>You have already requested connection via this address!</source> <target>You have already requested connection via this address!</target> @@ -5752,11 +8867,6 @@ Repeat connection request?</source> Repeat connection request?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> - <target>You have no chats</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> <target>You have to enter passphrase every time the app starts - it is not stored on the device.</target> @@ -5777,11 +8887,26 @@ Repeat connection request?</target> <target>You joined this group. Connecting to inviting group member.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You may migrate the exported database." xml:space="preserve"> + <source>You may migrate the exported database.</source> + <target>You may migrate the exported database.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may save the exported archive." xml:space="preserve"> + <source>You may save the exported archive.</source> + <target>You may save the exported archive.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> <target>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.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You need to allow your contact to call to be able to call them." xml:space="preserve"> + <source>You need to allow your contact to call to be able to call them.</source> + <target>You need to allow your contact to call to be able to call them.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> <target>You need to allow your contact to send voice messages to be able to send them.</target> @@ -5797,6 +8922,11 @@ Repeat connection request?</target> <target>You sent group invitation</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You should receive notifications." xml:space="preserve"> + <source>You should receive notifications.</source> + <target>You should receive notifications.</target> + <note>token info</note> + </trans-unit> <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> <target>You will be connected to group when the group host's device is online, please wait or check later!</target> @@ -5832,6 +8962,11 @@ Repeat connection request?</target> <target>You will still receive calls and notifications from muted profiles when they are active.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You will stop receiving messages from this chat. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this chat. Chat history will be preserved.</source> + <target>You will stop receiving messages from this chat. Chat history will be preserved.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> <target>You will stop receiving messages from this group. Chat history will be preserved.</target> @@ -5852,31 +8987,16 @@ Repeat connection request?</target> <target>You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your %@ servers" xml:space="preserve"> - <source>Your %@ servers</source> - <target>Your %@ servers</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your ICE servers" xml:space="preserve"> <source>Your ICE servers</source> <target>Your ICE servers</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> - <target>Your SMP servers</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your SimpleX address" xml:space="preserve"> <source>Your SimpleX address</source> <target>Your SimpleX address</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your XFTP servers" xml:space="preserve"> - <source>Your XFTP servers</source> - <target>Your XFTP servers</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your calls" xml:space="preserve"> <source>Your calls</source> <target>Your calls</target> @@ -5892,16 +9012,19 @@ Repeat connection request?</target> <target>Your chat database is not encrypted - set passphrase to encrypt it.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your chat preferences" xml:space="preserve"> + <source>Your chat preferences</source> + <target>Your chat preferences</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>Your chat profiles</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> - <target>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</target> + <trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve"> + <source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source> + <target>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> @@ -5914,18 +9037,16 @@ You can cancel this connection and remove the contact (and try later with a new <target>Your contacts can allow full message deletion.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> - <source>Your contacts in SimpleX will see it. -You can change it in Settings.</source> - <target>Your contacts in SimpleX will see it. -You can change it in Settings.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your contacts will remain connected." xml:space="preserve"> <source>Your contacts will remain connected.</source> <target>Your contacts will remain connected.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve"> + <source>Your credentials may be sent unencrypted.</source> + <target>Your credentials may be sent unencrypted.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> <target>Your current chat database will be DELETED and REPLACED with the imported one.</target> @@ -5956,33 +9077,36 @@ You can change it in Settings.</target> <target>Your profile **%@** will be shared.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> - <target>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</target> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> + <target>Your profile is stored on your device and only shared with your contacts.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> - <target>Your profile, contacts and delivered messages are stored on your device.</target> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> + <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> + <target>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve"> + <source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source> + <target>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> <target>Your random profile</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> - <target>Your server</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> <target>Your server address</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your servers" xml:space="preserve"> + <source>Your servers</source> + <target>Your servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your settings" xml:space="preserve"> <source>Your settings</source> <target>Your settings</target> @@ -6023,11 +9147,21 @@ SimpleX servers cannot see your profile.</target> <target>accepted call</target> <note>call status</note> </trans-unit> + <trans-unit id="accepted invitation" xml:space="preserve"> + <source>accepted invitation</source> + <target>accepted invitation</target> + <note>chat list item title</note> + </trans-unit> <trans-unit id="admin" xml:space="preserve"> <source>admin</source> <target>admin</target> <note>member role</note> </trans-unit> + <trans-unit id="admins" xml:space="preserve"> + <source>admins</source> + <target>admins</target> + <note>feature role</note> + </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> <target>agreeing encryption for %@…</target> @@ -6038,6 +9172,11 @@ SimpleX servers cannot see your profile.</target> <target>agreeing encryption…</target> <note>chat item text</note> </trans-unit> + <trans-unit id="all members" xml:space="preserve"> + <source>all members</source> + <target>all members</target> + <note>feature role</note> + </trans-unit> <trans-unit id="always" xml:space="preserve"> <source>always</source> <target>always</target> @@ -6048,6 +9187,16 @@ SimpleX servers cannot see your profile.</target> <target>and %lld other events</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="archived report" xml:space="preserve"> + <source>archived report</source> + <target>archived report</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="attempts" xml:space="preserve"> + <source>attempts</source> + <target>attempts</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <source>audio call (not e2e encrypted)</source> <target>audio call (not e2e encrypted)</target> @@ -6071,13 +9220,29 @@ SimpleX servers cannot see your profile.</target> <trans-unit id="blocked" xml:space="preserve"> <source>blocked</source> <target>blocked</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="blocked %@" xml:space="preserve"> + <source>blocked %@</source> + <target>blocked %@</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="blocked by admin" xml:space="preserve"> + <source>blocked by admin</source> + <target>blocked by admin</target> + <note>blocked chat item +marked deleted chat item preview text</note> </trans-unit> <trans-unit id="bold" xml:space="preserve"> <source>bold</source> <target>bold</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="call" xml:space="preserve"> + <source>call</source> + <target>call</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="call error" xml:space="preserve"> <source>call error</source> <target>call error</target> @@ -6181,7 +9346,7 @@ SimpleX servers cannot see your profile.</target> <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> <target>connecting…</target> - <note>chat list item title</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> @@ -6193,6 +9358,11 @@ SimpleX servers cannot see your profile.</target> <target>connection:%@</target> <note>connection information</note> </trans-unit> + <trans-unit id="contact %@ changed to %@" xml:space="preserve"> + <source>contact %1$@ changed to %2$@</source> + <target>contact %1$@ changed to %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> <target>contact has e2e encryption</target> @@ -6223,10 +9393,16 @@ SimpleX servers cannot see your profile.</target> <target>days</target> <note>time unit</note> </trans-unit> + <trans-unit id="decryption errors" xml:space="preserve"> + <source>decryption errors</source> + <target>decryption errors</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> <target>default (%@)</target> - <note>pref value</note> + <note>delete after time +pref value</note> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> @@ -6273,6 +9449,11 @@ SimpleX servers cannot see your profile.</target> <target>duplicate message</target> <note>integrity error chat item</note> </trans-unit> + <trans-unit id="duplicates" xml:space="preserve"> + <source>duplicates</source> + <target>duplicates</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> <target>e2e encrypted</target> @@ -6348,9 +9529,14 @@ SimpleX servers cannot see your profile.</target> <target>error</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="event happened" xml:space="preserve"> - <source>event happened</source> - <target>event happened</target> + <trans-unit id="expired" xml:space="preserve"> + <source>expired</source> + <target>expired</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="forwarded" xml:space="preserve"> + <source>forwarded</source> + <target>forwarded</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> @@ -6378,6 +9564,11 @@ SimpleX servers cannot see your profile.</target> <target>iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="inactive" xml:space="preserve"> + <source>inactive</source> + <target>inactive</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> <target>incognito via contact address link</target> @@ -6418,6 +9609,11 @@ SimpleX servers cannot see your profile.</target> <target>invitation to group %@</target> <note>group name</note> </trans-unit> + <trans-unit id="invite" xml:space="preserve"> + <source>invite</source> + <target>invite</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> <target>invited</target> @@ -6463,11 +9659,21 @@ SimpleX servers cannot see your profile.</target> <target>member</target> <note>member role</note> </trans-unit> + <trans-unit id="member %@ changed to %@" xml:space="preserve"> + <source>member %1$@ changed to %2$@</source> + <target>member %1$@ changed to %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> <target>connected</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="message" xml:space="preserve"> + <source>message</source> + <target>message</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="message received" xml:space="preserve"> <source>message received</source> <target>message received</target> @@ -6491,7 +9697,12 @@ SimpleX servers cannot see your profile.</target> <trans-unit id="moderated by %@" xml:space="preserve"> <source>moderated by %@</source> <target>moderated by %@</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="moderator" xml:space="preserve"> + <source>moderator</source> + <target>moderator</target> + <note>member role</note> </trans-unit> <trans-unit id="months" xml:space="preserve"> <source>months</source> @@ -6501,7 +9712,7 @@ SimpleX servers cannot see your profile.</target> <trans-unit id="never" xml:space="preserve"> <source>never</source> <target>never</target> - <note>No comment provided by engineer.</note> + <note>delete after time</note> </trans-unit> <trans-unit id="new message" xml:space="preserve"> <source>new message</source> @@ -6532,8 +9743,8 @@ SimpleX servers cannot see your profile.</target> <source>off</source> <target>off</target> <note>enabled status - group pref value - time to disappear</note> +group pref value +time to disappear</note> </trans-unit> <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> @@ -6550,16 +9761,46 @@ SimpleX servers cannot see your profile.</target> <target>on</target> <note>group pref value</note> </trans-unit> + <trans-unit id="other" xml:space="preserve"> + <source>other</source> + <target>other</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="other errors" xml:space="preserve"> + <source>other errors</source> + <target>other errors</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="owner" xml:space="preserve"> <source>owner</source> <target>owner</target> <note>member role</note> </trans-unit> + <trans-unit id="owners" xml:space="preserve"> + <source>owners</source> + <target>owners</target> + <note>feature role</note> + </trans-unit> <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> <target>peer-to-peer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="pending" xml:space="preserve"> + <source>pending</source> + <target>pending</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="pending approval" xml:space="preserve"> + <source>pending approval</source> + <target>pending approval</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="quantum resistant e2e encryption" xml:space="preserve"> + <source>quantum resistant e2e encryption</source> + <target>quantum resistant e2e encryption</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> <target>received answer…</target> @@ -6570,6 +9811,11 @@ SimpleX servers cannot see your profile.</target> <target>received confirmation…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="rejected" xml:space="preserve"> + <source>rejected</source> + <target>rejected</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> <target>rejected call</target> @@ -6585,11 +9831,41 @@ SimpleX servers cannot see your profile.</target> <target>removed %@</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="removed contact address" xml:space="preserve"> + <source>removed contact address</source> + <target>removed contact address</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="removed profile picture" xml:space="preserve"> + <source>removed profile picture</source> + <target>removed profile picture</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> <target>removed you</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="requested to connect" xml:space="preserve"> + <source>requested to connect</source> + <target>requested to connect</target> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="saved" xml:space="preserve"> + <source>saved</source> + <target>saved</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="saved from %@" xml:space="preserve"> + <source>saved from %@</source> + <target>saved from %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="search" xml:space="preserve"> + <source>search</source> + <target>search</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="sec" xml:space="preserve"> <source>sec</source> <target>sec</target> @@ -6615,6 +9891,30 @@ SimpleX servers cannot see your profile.</target> <target>send direct message</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve"> + <source>server queue info: %1$@ + +last received msg: %2$@</source> + <target>server queue info: %1$@ + +last received msg: %2$@</target> + <note>queue info</note> + </trans-unit> + <trans-unit id="set new contact address" xml:space="preserve"> + <source>set new contact address</source> + <target>set new contact address</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="set new profile picture" xml:space="preserve"> + <source>set new profile picture</source> + <target>set new profile picture</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="standard end-to-end encryption" xml:space="preserve"> + <source>standard end-to-end encryption</source> + <target>standard end-to-end encryption</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>starting…</target> @@ -6630,16 +9930,41 @@ SimpleX servers cannot see your profile.</target> <target>this contact</target> <note>notification title</note> </trans-unit> + <trans-unit id="unblocked %@" xml:space="preserve"> + <source>unblocked %@</source> + <target>unblocked %@</target> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="unknown" xml:space="preserve"> <source>unknown</source> <target>unknown</target> <note>connection info</note> </trans-unit> + <trans-unit id="unknown servers" xml:space="preserve"> + <source>unknown servers</source> + <target>unknown servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unknown status" xml:space="preserve"> + <source>unknown status</source> + <target>unknown status</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unprotected" xml:space="preserve"> + <source>unprotected</source> + <target>unprotected</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="updated group profile" xml:space="preserve"> <source>updated group profile</source> <target>updated group profile</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="updated profile" xml:space="preserve"> + <source>updated profile</source> + <target>updated profile</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="v%@" xml:space="preserve"> <source>v%@</source> <target>v%@</target> @@ -6670,6 +9995,11 @@ SimpleX servers cannot see your profile.</target> <target>via relay</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="video" xml:space="preserve"> + <source>video</source> + <target>video</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> <source>video call (not e2e encrypted)</source> <target>video call (not e2e encrypted)</target> @@ -6695,11 +10025,21 @@ SimpleX servers cannot see your profile.</target> <target>weeks</target> <note>time unit</note> </trans-unit> + <trans-unit id="when IP hidden" xml:space="preserve"> + <source>when IP hidden</source> + <target>when IP hidden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="yes" xml:space="preserve"> <source>yes</source> <target>yes</target> <note>pref value</note> </trans-unit> + <trans-unit id="you" xml:space="preserve"> + <source>you</source> + <target>you</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> <target>you are invited to group</target> @@ -6710,6 +10050,11 @@ SimpleX servers cannot see your profile.</target> <target>you are observer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="you blocked %@" xml:space="preserve"> + <source>you blocked %@</source> + <target>you blocked %@</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> <target>you changed address</target> @@ -6750,6 +10095,11 @@ SimpleX servers cannot see your profile.</target> <target>you shared one-time link incognito</target> <note>chat list item description</note> </trans-unit> + <trans-unit id="you unblocked %@" xml:space="preserve"> + <source>you unblocked %@</source> + <target>you unblocked %@</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you: " xml:space="preserve"> <source>you: </source> <target>you: </target> @@ -6764,7 +10114,7 @@ SimpleX servers cannot see your profile.</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="en" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6801,7 +10151,7 @@ SimpleX servers cannot see your profile.</target> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="en" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> @@ -6821,4 +10171,250 @@ SimpleX servers cannot see your profile.</target> </trans-unit> </body> </file> + <file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="en" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%d new events" xml:space="preserve"> + <source>%d new events</source> + <target>%d new events</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="From %d chat(s)" xml:space="preserve"> + <source>From %d chat(s)</source> + <target>From %d chat(s)</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="From: %@" xml:space="preserve"> + <source>From: %@</source> + <target>From: %@</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>New events</target> + <note>notification</note> + </trans-unit> + <trans-unit id="New messages" xml:space="preserve"> + <source>New messages</source> + <target>New messages</target> + <note>notification</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="en" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle display name</note> + </trans-unit> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> + <source>Copyright © 2024 SimpleX Chat. All rights reserved.</source> + <target>Copyright © 2024 SimpleX Chat. All rights reserved.</target> + <note>Copyright (human-readable)</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="en" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <target>%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App is locked!" xml:space="preserve"> + <source>App is locked!</source> + <target>App is locked!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <target>Cancel</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <target>Cannot access keychain to save database password</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Cannot forward message</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Comment" xml:space="preserve"> + <source>Comment</source> + <target>Comment</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <target>Currently maximum supported file size is %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade required" xml:space="preserve"> + <source>Database downgrade required</source> + <target>Database downgrade required</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <target>Database encrypted!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <target>Database error</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <target>Database passphrase is different from saved in the keychain.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <target>Database passphrase is required to open chat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade required" xml:space="preserve"> + <source>Database upgrade required</source> + <target>Database upgrade required</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing file" xml:space="preserve"> + <source>Error preparing file</source> + <target>Error preparing file</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing message" xml:space="preserve"> + <source>Error preparing message</source> + <target>Error preparing message</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <target>Error: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>File error</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <target>Incompatible database version</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Invalid migration confirmation</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <target>Keychain error</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <target>Large file!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No active profile" xml:space="preserve"> + <source>No active profile</source> + <target>No active profile</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <target>Ok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to downgrade the database." xml:space="preserve"> + <source>Open the app to downgrade the database.</source> + <target>Open the app to downgrade the database.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to upgrade the database." xml:space="preserve"> + <source>Open the app to upgrade the database.</source> + <target>Open the app to upgrade the database.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passphrase" xml:space="preserve"> + <source>Passphrase</source> + <target>Passphrase</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve"> + <source>Please create a profile in the SimpleX app</source> + <target>Please create a profile in the SimpleX app</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Selected chat preferences prohibit this message.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending a message takes longer than expected." xml:space="preserve"> + <source>Sending a message takes longer than expected.</source> + <target>Sending a message takes longer than expected.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending message…" xml:space="preserve"> + <source>Sending message…</source> + <target>Sending message…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <target>Share</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Slow network?" xml:space="preserve"> + <source>Slow network?</source> + <target>Slow network?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <target>Unknown database error: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unsupported format" xml:space="preserve"> + <source>Unsupported format</source> + <target>Unsupported format</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wait" xml:space="preserve"> + <source>Wait</source> + <target>Wait</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <target>Wrong database passphrase</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve"> + <source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source> + <target>You can allow sharing in Privacy & Security / SimpleX Lock settings.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + </body> + </file> </xliff> diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/en.xcloc/contents.json b/apps/ios/SimpleX Localizations/en.xcloc/contents.json index 7d429820ee..ec2accf27e 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/en.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "en", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "16.2" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index ccc7ee3446..d39fb61249 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -2,36 +2,9 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="es" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> - <trans-unit id=" " xml:space="preserve"> - <source> -</source> - <target> -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (" xml:space="preserve"> - <source> (</source> - <target> (</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> <target> (puede copiarse)</target> @@ -89,6 +62,7 @@ </trans-unit> <trans-unit id="%@ and %@" xml:space="preserve"> <source>%@ and %@</source> + <target>%@ y %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ and %@ connected" xml:space="preserve"> @@ -103,6 +77,12 @@ </trans-unit> <trans-unit id="%@ connected" xml:space="preserve"> <source>%@ connected</source> + <target>%@ conectado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve"> + <source>%@ downloaded</source> + <target>%@ descargado</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ is connected!" xml:space="preserve"> @@ -120,9 +100,19 @@ <target>%@ está verificado</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ server" xml:space="preserve"> + <source>%@ server</source> + <target>%@ servidor</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ servers" xml:space="preserve"> <source>%@ servers</source> - <target>Servidores %@</target> + <target>%@ servidores</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve"> + <source>%@ uploaded</source> + <target>%@ subido</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ wants to connect!" xml:space="preserve"> @@ -130,8 +120,14 @@ <target>¡ %@ quiere contactar!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve"> + <source>%1$@, %2$@</source> + <target>%1$@, %2$@</target> + <note>format for date separator in chat</note> + </trans-unit> <trans-unit id="%@, %@ and %lld members" xml:space="preserve"> <source>%@, %@ and %lld members</source> + <target>%@, %@ y %lld miembro(s) más</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> @@ -146,37 +142,67 @@ </trans-unit> <trans-unit id="%d days" xml:space="preserve"> <source>%d days</source> - <target>%d días</target> + <target>%d día(s)</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve"> + <source>%d file(s) are still being downloaded.</source> + <target>%d archivo(s) se está(n) descargando todavía.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve"> + <source>%d file(s) failed to download.</source> + <target>La descarga ha fallado para %d archivo(s).</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve"> + <source>%d file(s) were deleted.</source> + <target>%d archivo(s) ha(n) sido eliminado(s).</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve"> + <source>%d file(s) were not downloaded.</source> + <target>%d archivo(s) no se ha(n) descargado.</target> + <note>forward confirmation reason</note> + </trans-unit> <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> - <target>%d horas</target> + <target>%d hora(s)</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve"> + <source>%d messages not forwarded</source> + <target>%d mensaje(s) no enviado(s)</target> + <note>alert title</note> + </trans-unit> <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> - <target>%d minutos</target> + <target>%d minuto(s)</target> <note>time interval</note> </trans-unit> <trans-unit id="%d months" xml:space="preserve"> <source>%d months</source> - <target>%d meses</target> + <target>%d mes(es)</target> <note>time interval</note> </trans-unit> <trans-unit id="%d sec" xml:space="preserve"> <source>%d sec</source> - <target>%d segundos</target> + <target>%d segundo(s)</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve"> + <source>%d seconds(s)</source> + <target>%d segundos</target> + <note>delete after time</note> + </trans-unit> <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> - <target>%d mensaje(s) saltado(s</target> + <target>%d mensaje(s) omitido(s)</target> <note>integrity error chat item</note> </trans-unit> <trans-unit id="%d weeks" xml:space="preserve"> <source>%d weeks</source> - <target>%d semanas</target> + <target>%d semana(s)</target> <note>time interval</note> </trans-unit> <trans-unit id="%lld" xml:space="preserve"> @@ -201,6 +227,7 @@ </trans-unit> <trans-unit id="%lld group events" xml:space="preserve"> <source>%lld group events</source> + <target>%lld evento(s) de grupo</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld members" xml:space="preserve"> @@ -210,14 +237,22 @@ </trans-unit> <trans-unit id="%lld messages blocked" xml:space="preserve"> <source>%lld messages blocked</source> + <target>%lld mensaje(s) bloqueado(s)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve"> + <source>%lld messages blocked by admin</source> + <target>%lld mensajes bloqueados por el administrador</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld messages marked deleted" xml:space="preserve"> <source>%lld messages marked deleted</source> + <target>%lld mensaje(s) marcado(s) eliminado(s)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld messages moderated by %@" xml:space="preserve"> <source>%lld messages moderated by %@</source> + <target>%lld mensaje(s) moderado(s) por %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld minutes" xml:space="preserve"> @@ -230,11 +265,6 @@ <target>%lld idiomas de interfaz nuevos</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> - <source>%lld second(s)</source> - <target>%lld segundo(s)</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="%lld seconds" xml:space="preserve"> <source>%lld seconds</source> <target>%lld segundos</target> @@ -285,62 +315,54 @@ <target>%u mensaje(s) omitido(s).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> - <source>(</source> - <target>(</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="(new)" xml:space="preserve"> <source>(new)</source> + <target>(nuevo)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="(this device v%@)" xml:space="preserve"> <source>(this device v%@)</source> + <target>(este dispositivo v%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve"> - <source>)</source> - <target>)</target> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target>**Añadir contacto**: crea un enlace de invitación nuevo.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target>**Añadir nuevo contacto**: para crear tu código QR o enlace de un uso para tu contacto.</target> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve"> + <source>**Create group**: to create a new group.</source> + <target>**Crear grupo**: crea un grupo nuevo.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> - <source>**Create link / QR code** for your contact to use.</source> - <target>**Crea enlace / código QR** para que tu contacto lo use.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Más privado**: comprueba los mensajes nuevos cada 20 minutos. El token del dispositivo se comparte con el servidor de SimpleX Chat, pero no cuántos contactos o mensajes tienes.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Más privado**: no se usa el servidor de notificaciones de SimpleX Chat, los mensajes se comprueban periódicamente en segundo plano (dependiendo de la frecuencia con la que utilices la aplicación).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <target>**Pega el enlace recibido** o ábrelo en el navegador y pulsa **Abrir en aplicación móvil**.</target> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> + <target>**Recuarda**: usar la misma base de datos en dos dispositivos hará que falle el descifrado de mensajes como protección de seguridad.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> <source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source> - <target>**Atención**: NO podrás recuperar o cambiar la contraseña si la pierdes.</target> + <target>**Atención**: Si la pierdes NO podrás recuperar o cambiar la contraseña.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Recomendado**: el token del dispositivo y las notificaciones se envían al servidor de notificaciones de SimpleX Chat, pero no el contenido del mensaje, su tamaño o su procedencia.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target>**Escanear código QR**: en persona para conectarte con tu contacto, o por videollamada.</target> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve"> + <source>**Scan / Paste link**: to connect via a link you received.</source> + <target>**Escanear / Pegar enlace**: para conectar mediante un enlace recibido.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> @@ -348,6 +370,11 @@ <target>**Advertencia**: Las notificaciones automáticas instantáneas requieren una contraseña guardada en Keychain.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve"> + <source>**Warning**: the archive will be removed.</source> + <target>**Atención**: el archivo será eliminado.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> <source>**e2e encrypted** audio call</source> <target>Llamada con **cifrado de extremo a extremo **</target> @@ -363,11 +390,6 @@ <target>\*bold*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=", " xml:space="preserve"> - <source>, </source> - <target>, </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). @@ -390,6 +412,9 @@ <source>- optionally notify deleted contacts. - profile names with spaces. - and more!</source> + <target>- notificar opcionalmente a los contactos eliminados. +- nombres de perfil con espacios. +- ¡...y más!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="- voice messages up to 5 minutes. - custom time to disappear. - editing history." xml:space="preserve"> @@ -401,13 +426,9 @@ - historial de edición.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="." xml:space="preserve"> - <source>.</source> - <target>.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="0 sec" xml:space="preserve"> <source>0 sec</source> + <target>0 seg</target> <note>time to disappear</note> </trans-unit> <trans-unit id="0s" xml:space="preserve"> @@ -418,7 +439,8 @@ <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> <target>un dia</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> @@ -433,16 +455,28 @@ <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> <target>un mes</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> <target>una semana</target> - <note>time interval</note> + <note>delete after time +time interval</note> + </trans-unit> + <trans-unit id="1 year" xml:space="preserve"> + <source>1 year</source> + <target>1 año</target> + <note>delete after time</note> </trans-unit> <trans-unit id="1-time link" xml:space="preserve"> <source>1-time link</source> - <target>Enlace un uso</target> + <target>Enlace de un uso</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> + <target>Los enlaces de un uso pueden ser usados *solamente con un contacto* - compártelos en persona o mediante cualquier aplicación de mensajería.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="5 minutes" xml:space="preserve"> @@ -460,11 +494,6 @@ <target>30 segundos</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve"> - <source>: </source> - <target>: </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> @@ -484,7 +513,7 @@ </trans-unit> <trans-unit id="A new random profile will be shared." xml:space="preserve"> <source>A new random profile will be shared.</source> - <target>Se compartirá un perfil nuevo aleatorio.</target> + <target>Compartirás un perfil nuevo aleatorio.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> @@ -496,7 +525,7 @@ <source>A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</source> <target>Se usará una conexión TCP independiente **por cada contacto y miembro de grupo**. -**Atención**: si tienes muchas conexiones, tu consumo de batería y tráfico pueden ser sustancialmente mayores y algunas conexiones pueden fallar.</target> +**Atención**: si tienes muchas conexiones, tu consumo de batería y tráfico pueden aumentar bastante y algunas conexiones pueden fallar.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Abort" xml:space="preserve"> @@ -506,17 +535,12 @@ </trans-unit> <trans-unit id="Abort changing address" xml:space="preserve"> <source>Abort changing address</source> - <target>Cancelar cambio de dirección</target> + <target>Cancelar cambio de servidor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Abort changing address?" xml:space="preserve"> <source>Abort changing address?</source> - <target>¿Cancelar el cambio de dirección?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve"> - <source>About SimpleX</source> - <target>Acerca de SimpleX</target> + <target>¿Cancelar el cambio de servidor?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve"> @@ -524,13 +548,13 @@ <target>Sobre SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve"> - <source>About SimpleX address</source> - <target>Acerca de la dirección SimpleX</target> + <trans-unit id="About operators" xml:space="preserve"> + <source>About operators</source> + <target>Acerca de los operadores</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accent color" xml:space="preserve"> - <source>Accent color</source> + <trans-unit id="Accent" xml:space="preserve"> + <source>Accent</source> <target>Color</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -538,7 +562,13 @@ <source>Accept</source> <target>Aceptar</target> <note>accept contact request via notification - accept incoming call via notification</note> +accept incoming call via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accept conditions" xml:space="preserve"> + <source>Accept conditions</source> + <target>Aceptar condiciones</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept connection request?" xml:space="preserve"> <source>Accept connection request?</source> @@ -553,16 +583,47 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Aceptar incógnito</target> - <note>accept contact request via notification</note> + <note>accept contact request via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve"> + <source>Accepted conditions</source> + <target>Condiciones aceptadas</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve"> + <source>Acknowledged</source> + <target>Confirmaciones</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve"> + <source>Acknowledgement errors</source> + <target>Errores de confirmación</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Active" xml:space="preserve"> + <source>Active</source> + <target>Activo</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve"> + <source>Active connections</source> + <target>Conexiones activas</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> <target>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.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> - <source>Add preset servers</source> - <target>Añadir servidores predefinidos</target> + <trans-unit id="Add friends" xml:space="preserve"> + <source>Add friends</source> + <target>Añadir amigos</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve"> + <source>Add list</source> + <target>Añadir lista</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add profile" xml:space="preserve"> @@ -570,14 +631,19 @@ <target>Añadir perfil</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> + <target>Añadir servidor</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> <source>Add servers by scanning QR codes.</source> <target>Añadir servidores mediante el escaneo de códigos QR.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> - <target>Añadir servidor…</target> + <trans-unit id="Add team members" xml:space="preserve"> + <source>Add team members</source> + <target>Añadir miembros del equipo</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -585,11 +651,46 @@ <target>Añadir a otro dispositivo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add to list" xml:space="preserve"> + <source>Add to list</source> + <target>Añadir a la lista</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> <target>Añadir mensaje de bienvenida</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve"> + <source>Add your team members to the conversations.</source> + <target>Añade a miembros de tu equipo a las conversaciones.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve"> + <source>Added media & file servers</source> + <target>Servidores de archivos y multimedia añadidos</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve"> + <source>Added message servers</source> + <target>Servidores de mensajes añadidos</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve"> + <source>Additional accent</source> + <target>Acento adicional</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve"> + <source>Additional accent 2</source> + <target>Color adicional 2</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve"> + <source>Additional secondary</source> + <target>Secundario adicional</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Address" xml:space="preserve"> <source>Address</source> <target>Dirección</target> @@ -600,6 +701,21 @@ <target>El cambio de dirección se cancelará. Se usará la antigua dirección de recepción.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve"> + <source>Address or 1-time link?</source> + <target>¿Dirección o enlace de un uso?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve"> + <source>Address settings</source> + <target>Configurar dirección</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve"> + <source>Admins can block a member for all.</source> + <target>Los administradores pueden bloquear a un miembro para los demás.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> <source>Admins can create the links to join groups.</source> <target>Los administradores pueden crear enlaces para unirse a grupos.</target> @@ -610,6 +726,16 @@ <target>Configuración avanzada de red</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve"> + <source>Advanced settings</source> + <target>Configuración avanzada</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All" xml:space="preserve"> + <source>All</source> + <target>Todo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All app data is deleted." xml:space="preserve"> <source>All app data is deleted.</source> <target>Todos los datos de la aplicación se eliminarán.</target> @@ -617,26 +743,62 @@ </trans-unit> <trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve"> <source>All chats and messages will be deleted - this cannot be undone!</source> - <target>Se eliminarán todos los chats y mensajes. ¡No podrá deshacerse!</target> + <target>Se eliminarán todos los chats y mensajes. ¡No puede deshacerse!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <target>Todos los chats se quitarán de la lista %@ y esta será eliminada.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> <target>Al introducirlo todos los datos son eliminados.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve"> + <source>All data is kept private on your device.</source> + <target>Todos los datos son privados y están en tu dispositivo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> <target>Todos los miembros del grupo permanecerán conectados.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <target>Todos los mensajes y archivos son enviados **cifrados de extremo a extremo** y con seguridad de cifrado postcuántico en mensajes directos.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone!</source> + <target>Todos los mensajes serán eliminados. ¡No puede deshacerse!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> - <target>Se eliminarán todos los mensajes SOLO para tí. ¡No podrá deshacerse!</target> + <target>Se eliminarán todos los mensajes SOLO para tí. ¡No puede deshacerse!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve"> <source>All new messages from %@ will be hidden!</source> + <target>¡Los mensajes nuevos de %@ estarán ocultos!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All profiles" xml:space="preserve"> + <source>All profiles</source> + <target>Todos los perfiles</target> + <note>profile dropdown</note> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve"> + <source>All reports will be archived for you.</source> + <target>Todos los informes serán archivados para ti.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All servers" xml:space="preserve"> + <source>All servers</source> + <target>Todos los servidores</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All your contacts will remain connected." xml:space="preserve"> @@ -649,6 +811,11 @@ <target>Todos tus contactos permanecerán conectados. La actualización del perfil se enviará a tus contactos.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target>Todos tus contactos, conversaciones y archivos serán cifrados, divididos y subidos de forma segura a los servidores XFTP configurados.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow" xml:space="preserve"> <source>Allow</source> <target>Se permite</target> @@ -659,14 +826,24 @@ <target>Se permiten las llamadas pero sólo si tu contacto también las permite.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve"> + <source>Allow calls?</source> + <target>¿Permitir llamadas?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> <target>Se permiten los mensajes temporales pero sólo si tu contacto también los permite para tí.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <target>Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también la permite para tí.</target> + <trans-unit id="Allow downgrade" xml:space="preserve"> + <source>Allow downgrade</source> + <target>Permitir versión anterior</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target>Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también la permite para tí. (24 horas)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> @@ -676,7 +853,7 @@ </trans-unit> <trans-unit id="Allow message reactions." xml:space="preserve"> <source>Allow message reactions.</source> - <target>Permitir reacciones a los mensajes.</target> + <target>Se permiten reacciones a los mensajes.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow sending direct messages to members." xml:space="preserve"> @@ -689,9 +866,24 @@ <target>Permites el envío de mensajes temporales.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow to irreversibly delete sent messages.</source> - <target>Se permite la eliminación irreversible de mensajes.</target> + <trans-unit id="Allow sharing" xml:space="preserve"> + <source>Allow sharing</source> + <target>Permitir compartir</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target>Se permite la eliminación irreversible de mensajes. (24 horas)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve"> + <source>Allow to report messsages to moderators.</source> + <target>Permitir informar de mensajes a los moderadores.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve"> + <source>Allow to send SimpleX links.</source> + <target>Se permite enviar enlaces SimpleX.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send files and media." xml:space="preserve"> @@ -724,9 +916,9 @@ <target>Permites que tus contactos puedan llamarte.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow your contacts to irreversibly delete sent messages.</source> - <target>Permites a tus contactos eliminar irreversiblemente los mensajes enviados.</target> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target>Permites a tus contactos eliminar irreversiblemente los mensajes enviados. (24 horas)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> @@ -746,10 +938,17 @@ </trans-unit> <trans-unit id="Already connecting!" xml:space="preserve"> <source>Already connecting!</source> + <target>¡Ya en proceso de conexión!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Already joining the group!" xml:space="preserve"> <source>Already joining the group!</source> + <target>¡Ya en proceso de unirte al grupo!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve"> + <source>Always use private routing.</source> + <target>Usar siempre enrutamiento privado.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> @@ -762,24 +961,44 @@ <target>Se creará un perfil vacío con el nombre proporcionado, y la aplicación se abrirá como de costumbre.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Another reason" xml:space="preserve"> + <source>Another reason</source> + <target>Otro motivo</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> <target>Responder llamada</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> + <target>Cualquiera puede alojar servidores.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> <target>Compilación app: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App data migration" xml:space="preserve"> + <source>App data migration</source> + <target>Migrar datos de la aplicación</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> <source>App encrypts new local files (except videos).</source> <target>Cifrado de los nuevos archivos locales (excepto vídeos).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App group:" xml:space="preserve"> + <source>App group:</source> + <target>Grupo app:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> - <target>Icono aplicación</target> + <target>Icono de la aplicación</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App passcode" xml:space="preserve"> @@ -792,6 +1011,11 @@ <target>El código de acceso será reemplazado por código de autodestrucción.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App session" xml:space="preserve"> + <source>App session</source> + <target>por sesión</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App version" xml:space="preserve"> <source>App version</source> <target>Versión de la aplicación</target> @@ -807,6 +1031,66 @@ <target>Apariencia</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Apply" xml:space="preserve"> + <source>Apply</source> + <target>Aplicar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve"> + <source>Apply to</source> + <target>Aplicar a</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve"> + <source>Archive</source> + <target>Archivar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve"> + <source>Archive %lld reports?</source> + <target>¿Archivar %lld informes?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve"> + <source>Archive all reports?</source> + <target>¿Archivar todos los informes?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve"> + <source>Archive and upload</source> + <target>Archivar y subir</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve"> + <source>Archive contacts to chat later.</source> + <target>Archiva contactos para charlar más tarde.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve"> + <source>Archive report</source> + <target>Archivar informe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve"> + <source>Archive report?</source> + <target>¿Archivar informe?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve"> + <source>Archive reports</source> + <target>Archivar informes</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve"> + <source>Archived contacts</source> + <target>Contactos archivados</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve"> + <source>Archiving database</source> + <target>Archivando base de datos</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Attach" xml:space="preserve"> <source>Attach</source> <target>Adjuntar</target> @@ -867,13 +1151,24 @@ <target>Aceptar imágenes automáticamente</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve"> + <source>Auto-accept settings</source> + <target>Auto aceptar configuración</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Back" xml:space="preserve"> <source>Back</source> <target>Volver</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Background" xml:space="preserve"> + <source>Background</source> + <target>Fondo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Bad desktop address" xml:space="preserve"> <source>Bad desktop address</source> + <target>Dirección ordenador incorrecta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Bad message ID" xml:space="preserve"> @@ -886,8 +1181,24 @@ <target>Hash de mensaje incorrecto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better calls" xml:space="preserve"> + <source>Better calls</source> + <target>Llamadas mejoradas</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better groups" xml:space="preserve"> <source>Better groups</source> + <target>Grupos mejorados</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve"> + <source>Better groups performance</source> + <target>Rendimiento de grupos mejorado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve"> + <source>Better message dates.</source> + <target>Sistema de fechas mejorado.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Better messages" xml:space="preserve"> @@ -895,20 +1206,79 @@ <target>Mensajes mejorados</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better networking" xml:space="preserve"> + <source>Better networking</source> + <target>Uso de red mejorado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve"> + <source>Better notifications</source> + <target>Notificaciones mejoradas</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve"> + <source>Better privacy and security</source> + <target>Privacidad y seguridad mejoradas</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve"> + <source>Better security ✅</source> + <target>Seguridad mejorada ✅</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve"> + <source>Better user experience</source> + <target>Experiencia de usuario mejorada</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Black" xml:space="preserve"> + <source>Black</source> + <target>Negro</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block" xml:space="preserve"> <source>Block</source> + <target>Bloquear</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block for all" xml:space="preserve"> + <source>Block for all</source> + <target>Bloquear para todos</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Block group members" xml:space="preserve"> <source>Block group members</source> + <target>Bloquear miembros del grupo</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Block member" xml:space="preserve"> <source>Block member</source> + <target>Bloquear miembro</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve"> + <source>Block member for all?</source> + <target>¿Bloqear miembro para todos?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Block member?" xml:space="preserve"> <source>Block member?</source> + <target>¿Bloquear miembro?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve"> + <source>Blocked by admin</source> + <target>Bloqueado por administrador</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve"> + <source>Blur for better privacy.</source> + <target>Difumina para mayor privacidad.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve"> + <source>Blur media</source> + <target>Difuminar multimedia</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> @@ -916,9 +1286,9 @@ <target>Tanto tú como tu contacto podéis añadir reacciones a los mensajes.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> - <source>Both you and your contact can irreversibly delete sent messages.</source> - <target>Tanto tú como tu contacto podéis eliminar de forma irreversible los mensajes enviados.</target> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target>Tanto tú como tu contacto podéis eliminar de forma irreversible los mensajes enviados. (24 horas)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> @@ -941,9 +1311,33 @@ <target>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)!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Business address" xml:space="preserve"> + <source>Business address</source> + <target>Dirección empresarial</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve"> + <source>Business chats</source> + <target>Chats empresariales</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve"> + <source>Businesses</source> + <target>Empresas</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> - <target>Mediante perfil (por defecto) o [por conexión](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> + <target>Mediante perfil (predeterminado) o [por conexión](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve"> + <source>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</source> + <target>Al usar SimpleX Chat, aceptas: +- enviar únicamente contenido legal en los grupos públicos. +- respetar a los demás usuarios – spam prohibido.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> @@ -956,9 +1350,24 @@ <target>Llamadas</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve"> - <source>Can't delete user profile!</source> - <target>¡No se puede eliminar el perfil!</target> + <trans-unit id="Calls prohibited!" xml:space="preserve"> + <source>Calls prohibited!</source> + <target>¡Llamadas no permitidas!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve"> + <source>Camera not available</source> + <target>Cámara no disponible</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve"> + <source>Can't call contact</source> + <target>No se puede llamar al contacto</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve"> + <source>Can't call member</source> + <target>No se puede llamar al miembro</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contact!" xml:space="preserve"> @@ -968,12 +1377,23 @@ </trans-unit> <trans-unit id="Can't invite contacts!" xml:space="preserve"> <source>Can't invite contacts!</source> - <target>¡No se puede invitar a los contactos!</target> + <target>¡No se pueden invitar contactos!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve"> + <source>Can't message member</source> + <target>No se pueden enviar mensajes al miembro</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> <target>Cancelar</target> + <note>alert action +alert button</note> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve"> + <source>Cancel migration</source> + <target>Cancelar migración</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> @@ -981,9 +1401,24 @@ <target>Keychain inaccesible para guardar la contraseña de la base de datos</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>No se puede reenviar el mensaje</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> <target>No se puede recibir el archivo</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <target>Capacidad excedida - el destinatario no ha recibido los mensajes previos.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve"> + <source>Cellular</source> + <target>Móvil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change" xml:space="preserve"> @@ -991,6 +1426,16 @@ <target>Cambiar</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve"> + <source>Change automatic message deletion?</source> + <target>¿Modificar la eliminación automática de mensajes?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve"> + <source>Change chat profiles</source> + <target>Cambiar perfil de usuario</target> + <note>authentication reason</note> + </trans-unit> <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> <target>¿Cambiar contraseña de la base de datos?</target> @@ -1003,7 +1448,7 @@ </trans-unit> <trans-unit id="Change member role?" xml:space="preserve"> <source>Change member role?</source> - <target>¿Cambiar el rol del miembro?</target> + <target>¿Cambiar rol?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change passcode" xml:space="preserve"> @@ -1035,11 +1480,26 @@ <source>Change self-destruct passcode</source> <target>Cambiar código autodestrucción</target> <note>authentication reason - set passcode view</note> +set passcode view</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> - <source>Chat archive</source> - <target>Archivo del chat</target> + <trans-unit id="Chat" xml:space="preserve"> + <source>Chat</source> + <target>Chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve"> + <source>Chat already exists</source> + <target>El chat ya existe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve"> + <source>Chat already exists!</source> + <target>¡El chat ya existe!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve"> + <source>Chat colors</source> + <target>Colores del chat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat console" xml:space="preserve"> @@ -1049,7 +1509,7 @@ </trans-unit> <trans-unit id="Chat database" xml:space="preserve"> <source>Chat database</source> - <target>Base de datos del chat</target> + <target>Base de datos de SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat database deleted" xml:space="preserve"> @@ -1057,6 +1517,11 @@ <target>Base de datos eliminada</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve"> + <source>Chat database exported</source> + <target>Base de datos exportada</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> <target>Base de datos importada</target> @@ -1064,12 +1529,27 @@ </trans-unit> <trans-unit id="Chat is running" xml:space="preserve"> <source>Chat is running</source> - <target>Chat está en ejecución</target> + <target>SimpleX está en ejecución</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat is stopped" xml:space="preserve"> <source>Chat is stopped</source> - <target>Chat está detenido</target> + <target>SimpleX está parado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <target>SimpleX está parado. Si has usado esta base de datos en otro dispositivo, debes transferirla de vuelta antes de iniciar SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve"> + <source>Chat list</source> + <target>Lista de chats</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve"> + <source>Chat migrated!</source> + <target>¡Chat migrado!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> @@ -1077,21 +1557,61 @@ <target>Preferencias de Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve"> + <source>Chat preferences were changed.</source> + <target>Las preferencias del chat han sido modificadas.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve"> + <source>Chat profile</source> + <target>Perfil de usuario</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve"> + <source>Chat theme</source> + <target>Tema de chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <target>El chat será eliminado para todos los miembros. ¡No puede deshacerse!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <target>El chat será eliminado para tí. ¡No puede deshacerse!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> <target>Chats</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve"> + <source>Check messages every 20 min.</source> + <target>Comprobar mensajes cada 20 min.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve"> + <source>Check messages when allowed.</source> + <target>Comprobar mensajes cuando se permita.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> <target>Comprueba la dirección del servidor e inténtalo de nuevo.</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> <target>Interfaz en chino y español</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <target>En el nuevo dispositivo selecciona _Migrar desde otro dispositivo_ y escanéa el código QR.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> <target>Elije archivo</target> @@ -1102,10 +1622,25 @@ <target>Elige de la biblioteca</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve"> + <source>Chunks deleted</source> + <target>Bloques eliminados</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve"> + <source>Chunks downloaded</source> + <target>Bloques descargados</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve"> + <source>Chunks uploaded</source> + <target>Bloques subidos</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> <target>Vaciar</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> @@ -1117,16 +1652,41 @@ <target>¿Vaciar conversación?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve"> + <source>Clear group?</source> + <target>¿Vaciar grupo?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve"> + <source>Clear or delete group?</source> + <target>¿Vaciar o eliminar grupo?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve"> + <source>Clear private notes?</source> + <target>¿Eliminar notas privadas?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear verification" xml:space="preserve"> <source>Clear verification</source> <target>Eliminar verificación</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> - <source>Colors</source> - <target>Colores</target> + <trans-unit id="Color chats with the new themes." xml:space="preserve"> + <source>Color chats with the new themes.</source> + <target>Colorea los chats con los nuevos temas.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Color mode" xml:space="preserve"> + <source>Color mode</source> + <target>Modo de color</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve"> + <source>Community guidelines violation</source> + <target>Violación de las normas de la comunidad</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Compare file" xml:space="preserve"> <source>Compare file</source> <target>Comparar archivo</target> @@ -1137,11 +1697,56 @@ <target>Compara los códigos de seguridad con tus contactos.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Completed" xml:space="preserve"> + <source>Completed</source> + <target>Completadas</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve"> + <source>Conditions accepted on: %@.</source> + <target>Condiciones aceptadas el: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <target>Las condiciones se han aceptado para el(los) operador(s): **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <target>Las condiciones ya se han aceptado para el/los siguiente(s) operador(s): **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve"> + <source>Conditions of use</source> + <target>Condiciones de uso</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <target>Las condiciones serán aceptadas para el/los operador(es): **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve"> + <source>Conditions will be accepted on: %@.</source> + <target>Las condiciones serán aceptadas el: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <target>Las condiciones serán aceptadas automáticamente para los operadores habilitados el: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> <target>Configure servidores ICE</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Configure server operators" xml:space="preserve"> + <source>Configure server operators</source> + <target>Configurar operadores de servidores</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> <target>Confirmar</target> @@ -1152,11 +1757,26 @@ <target>Confirma Código</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve"> + <source>Confirm contact deletion?</source> + <target>¿Confirmas la eliminación del contacto?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm database upgrades" xml:space="preserve"> <source>Confirm database upgrades</source> <target>Confirmar actualizaciones de la bases de datos</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve"> + <source>Confirm files from unknown servers.</source> + <target>Confirma archivos de servidores desconocidos.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve"> + <source>Confirm network settings</source> + <target>Confirmar configuración de red</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> <target>Confirme nueva contraseña…</target> @@ -1167,6 +1787,21 @@ <target>Confirmar contraseña</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <target>Para migrar la base de datos confirma que recuerdas la frase de contraseña.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve"> + <source>Confirm upload</source> + <target>Confirmar subida</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve"> + <source>Confirmed</source> + <target>Confirmado</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> <target>Conectar</target> @@ -1174,6 +1809,7 @@ </trans-unit> <trans-unit id="Connect automatically" xml:space="preserve"> <source>Connect automatically</source> + <target>Conectar automáticamente</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect incognito" xml:space="preserve"> @@ -1183,24 +1819,36 @@ </trans-unit> <trans-unit id="Connect to desktop" xml:space="preserve"> <source>Connect to desktop</source> + <target>Conectar con ordenador</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve"> + <source>Connect to your friends faster.</source> + <target>Conecta más rápido con tus amigos.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect to yourself?" xml:space="preserve"> <source>Connect to yourself?</source> + <target>¿Conectarte a tí mismo?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect to yourself? This is your own SimpleX address!" xml:space="preserve"> <source>Connect to yourself? This is your own SimpleX address!</source> + <target>¿Conectarte a tí mismo? +¡Esta es tu propia dirección SimpleX!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect to yourself? This is your own one-time link!" xml:space="preserve"> <source>Connect to yourself? This is your own one-time link!</source> + <target>¿Conectarte a tí mismo? +¡Este es tu propio enlace de un solo uso!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via contact address" xml:space="preserve"> <source>Connect via contact address</source> + <target>Conectar mediante dirección de contacto</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via link" xml:space="preserve"> @@ -1208,11 +1856,6 @@ This is your own one-time link!</source> <target>Conectar mediante enlace</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve"> - <source>Connect via link / QR code</source> - <target>Conecta vía enlace / Código QR</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connect via one-time link" xml:space="preserve"> <source>Connect via one-time link</source> <target>Conectar mediante enlace de un sólo uso</target> @@ -1220,14 +1863,32 @@ This is your own one-time link!</source> </trans-unit> <trans-unit id="Connect with %@" xml:space="preserve"> <source>Connect with %@</source> + <target>Conectar con %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connected" xml:space="preserve"> + <source>Connected</source> + <target>Conectadas</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connected desktop" xml:space="preserve"> <source>Connected desktop</source> + <target>Ordenador conectado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve"> + <source>Connected servers</source> + <target>Servidores conectados</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connected to desktop" xml:space="preserve"> <source>Connected to desktop</source> + <target>Conectado con ordenador</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connecting" xml:space="preserve"> + <source>Connecting</source> + <target>Conectando</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> @@ -1240,8 +1901,14 @@ This is your own one-time link!</source> <target>Conectando con el servidor... (error: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve"> + <source>Connecting to contact, please wait or check later!</source> + <target>Conectando con el contacto, por favor espera o revisa más tarde.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting to desktop" xml:space="preserve"> <source>Connecting to desktop</source> + <target>Conectando con ordenador</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection" xml:space="preserve"> @@ -1249,6 +1916,16 @@ This is your own one-time link!</source> <target>Conexión</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve"> + <source>Connection and servers status.</source> + <target>Estado de tu conexión y servidores.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve"> + <source>Connection blocked</source> + <target>Conexión bloqueada</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> <target>Error conexión</target> @@ -1256,7 +1933,24 @@ This is your own one-time link!</source> </trans-unit> <trans-unit id="Connection error (AUTH)" xml:space="preserve"> <source>Connection error (AUTH)</source> - <target>Error conexión (Autenticación)</target> + <target>Error de conexión (Autenticación)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve"> + <source>Connection is blocked by server operator: +%@</source> + <target>Conexión bloqueada por el operador del servidor: +%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve"> + <source>Connection not ready.</source> + <target>Conexión no establecida.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve"> + <source>Connection notifications</source> + <target>Notificaciones de conexión</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> @@ -1264,13 +1958,34 @@ This is your own one-time link!</source> <target>¡Solicitud de conexión enviada!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve"> + <source>Connection requires encryption renegotiation.</source> + <target>La conexión requiere renegociar el cifrado.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve"> + <source>Connection security</source> + <target>Seguridad de conexión</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection terminated" xml:space="preserve"> <source>Connection terminated</source> + <target>Conexión finalizada</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection timeout" xml:space="preserve"> <source>Connection timeout</source> - <target>Tiempo de conexión expirado</target> + <target>Tiempo de conexión agotado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve"> + <source>Connection with desktop stopped</source> + <target>La conexión con el escritorio (desktop) se ha parado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve"> + <source>Connections</source> + <target>Conexiones</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact allows" xml:space="preserve"> @@ -1283,6 +1998,11 @@ This is your own one-time link!</source> <target>El contácto ya existe</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact deleted!" xml:space="preserve"> + <source>Contact deleted!</source> + <target>¡Contacto eliminado!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> <target>Contacto oculto:</target> @@ -1293,9 +2013,9 @@ This is your own one-time link!</source> <target>El contacto está en línea</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve"> - <source>Contact is not connected yet!</source> - <target>¡El contacto aun no se ha conectado!</target> + <trans-unit id="Contact is deleted." xml:space="preserve"> + <source>Contact is deleted.</source> + <target>El contacto está eliminado.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact name" xml:space="preserve"> @@ -1308,6 +2028,11 @@ This is your own one-time link!</source> <target>Preferencias de contacto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve"> + <source>Contact will be deleted - this cannot be undone!</source> + <target>El contacto será eliminado. ¡No puede deshacerse!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> <target>Contactos</target> @@ -1318,23 +2043,44 @@ This is your own one-time link!</source> <target>Tus contactos sólo pueden marcar los mensajes para eliminar. Tu podrás verlos.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve"> + <source>Content violates conditions of use</source> + <target>El contenido viola las condiciones de uso</target> + <note>blocking reason</note> + </trans-unit> <trans-unit id="Continue" xml:space="preserve"> <source>Continue</source> <target>Continuar</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve"> + <source>Conversation deleted!</source> + <target>¡Conversación eliminada!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> <target>Copiar</target> - <note>chat item action</note> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve"> + <source>Copy error</source> + <target>Copiar error</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> <target>Versión Core: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Corner" xml:space="preserve"> + <source>Corner</source> + <target>Esquina</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Correct name to %@?" xml:space="preserve"> <source>Correct name to %@?</source> + <target>¿Corregir el nombre a %@?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create" xml:space="preserve"> @@ -1342,18 +2088,19 @@ This is your own one-time link!</source> <target>Crear</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve"> + <source>Create 1-time link</source> + <target>Crear enlace de un uso</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> - <target>Crear tu dirección SimpleX</target> + <target>Crear dirección SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create a group using a random profile." xml:space="preserve"> <source>Create a group using a random profile.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> - <source>Create an address to let people connect with you.</source> - <target>Crea una dirección para que otras personas puedan conectar contigo.</target> + <target>Crear grupo usando perfil aleatorio.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create file" xml:space="preserve"> @@ -1363,6 +2110,7 @@ This is your own one-time link!</source> </trans-unit> <trans-unit id="Create group" xml:space="preserve"> <source>Create group</source> + <target>Crear grupo</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create group link" xml:space="preserve"> @@ -1375,18 +2123,19 @@ This is your own one-time link!</source> <target>Crear enlace</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create list" xml:space="preserve"> + <source>Create list</source> + <target>Crear lista</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> <target>Crea perfil nuevo en la [aplicación para PC](https://simplex.Descargas/de chat/). 💻</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve"> - <source>Create one-time invitation link</source> - <target>Crea enlace de invitación de un uso</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create profile" xml:space="preserve"> <source>Create profile</source> + <target>Crear perfil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create queue" xml:space="preserve"> @@ -1404,9 +2153,29 @@ This is your own one-time link!</source> <target>Crea tu perfil</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve"> - <source>Created on %@</source> - <target>Creado en %@</target> + <trans-unit id="Created" xml:space="preserve"> + <source>Created</source> + <target>Creadas</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve"> + <source>Created at</source> + <target>Creado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve"> + <source>Created at: %@</source> + <target>Creado: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve"> + <source>Creating archive link</source> + <target>Creando enlace al archivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve"> + <source>Creating link…</source> + <target>Creando enlace…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current Passcode" xml:space="preserve"> @@ -1414,11 +2183,21 @@ This is your own one-time link!</source> <target>Código de Acceso</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <target>El texto con las condiciones actuales no se ha podido cargar, puedes revisar las condiciones en el siguiente enlace:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> <target>Contraseña actual…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current profile" xml:space="preserve"> + <source>Current profile</source> + <target>Perfil actual</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> <target>El tamaño máximo de archivo admitido es %@.</target> @@ -1429,11 +2208,26 @@ This is your own one-time link!</source> <target>Tiempo personalizado</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve"> + <source>Customizable message shape.</source> + <target>Forma personalizable de los mensajes.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve"> + <source>Customize theme</source> + <target>Personalizar tema</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> <target>Oscuro</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve"> + <source>Dark mode colors</source> + <target>Colores en modo oscuro</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> <target>ID base de datos</target> @@ -1500,7 +2294,7 @@ This is your own one-time link!</source> </trans-unit> <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> <source>Database passphrase is different from saved in the keychain.</source> - <target>La contraseña es distinta a la almacenada en Keychain.</target> + <target>La contraseña es diferente a la almacenada en Keychain.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> @@ -1532,6 +2326,11 @@ This is your own one-time link!</source> <target>La base de datos migrará cuando se reinicie la aplicación</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Debug delivery" xml:space="preserve"> + <source>Debug delivery</source> + <target>Informe debug</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> <target>Descentralizada</target> @@ -1545,15 +2344,17 @@ This is your own one-time link!</source> <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> <target>Eliminar</target> - <note>chat item action</note> + <note>alert action +swipe action</note> + </trans-unit> + <trans-unit id="Delete %lld messages of members?" xml:space="preserve"> + <source>Delete %lld messages of members?</source> + <target>¿Eliminar %lld mensajes de miembros?</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete %lld messages?" xml:space="preserve"> <source>Delete %lld messages?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve"> - <source>Delete Contact</source> - <target>Eliminar contacto</target> + <target>¿Eliminar %lld mensajes?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete address" xml:space="preserve"> @@ -1578,16 +2379,17 @@ This is your own one-time link!</source> </trans-unit> <trans-unit id="Delete and notify contact" xml:space="preserve"> <source>Delete and notify contact</source> + <target>Eliminar y notificar contacto</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve"> - <source>Delete archive</source> - <target>Eliminar archivo</target> + <trans-unit id="Delete chat" xml:space="preserve"> + <source>Delete chat</source> + <target>Eliminar chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve"> - <source>Delete chat archive?</source> - <target>¿Eliminar archivo del chat?</target> + <trans-unit id="Delete chat messages from your device." xml:space="preserve"> + <source>Delete chat messages from your device.</source> + <target>Elimina los mensajes del dispositivo.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile" xml:space="preserve"> @@ -1597,7 +2399,12 @@ This is your own one-time link!</source> </trans-unit> <trans-unit id="Delete chat profile?" xml:space="preserve"> <source>Delete chat profile?</source> - <target>¿Eliminar el perfil?</target> + <target>¿Eliminar perfil?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete chat?" xml:space="preserve"> + <source>Delete chat?</source> + <target>¿Eliminar chat?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> @@ -1610,9 +2417,9 @@ This is your own one-time link!</source> <target>Eliminar contacto</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact? This cannot be undone!" xml:space="preserve"> - <source>Delete contact? -This cannot be undone!</source> + <trans-unit id="Delete contact?" xml:space="preserve"> + <source>Delete contact?</source> + <target>¿Eliminar contacto?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete database" xml:space="preserve"> @@ -1620,6 +2427,11 @@ This cannot be undone!</source> <target>Eliminar base de datos</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete database from this device" xml:space="preserve"> + <source>Delete database from this device</source> + <target>Eliminar base de datos de este dispositivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete file" xml:space="preserve"> <source>Delete file</source> <target>Eliminar archivo</target> @@ -1627,7 +2439,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Delete files and media?" xml:space="preserve"> <source>Delete files and media?</source> - <target>Eliminar archivos y multimedia?</target> + <target>¿Eliminar archivos y multimedia?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete files for all chat profiles" xml:space="preserve"> @@ -1670,6 +2482,11 @@ This cannot be undone!</source> <target>¿Eliminar enlace?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete list?" xml:space="preserve"> + <source>Delete list?</source> + <target>¿Eliminar lista?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> <target>¿Eliminar el mensaje de miembro?</target> @@ -1682,8 +2499,8 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> - <target>Eliminar mensaje</target> - <note>No comment provided by engineer.</note> + <target>Activar</target> + <note>alert button</note> </trans-unit> <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> @@ -1700,14 +2517,14 @@ This cannot be undone!</source> <target>¿Eliminar base de datos antigua?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve"> - <source>Delete pending connection</source> - <target>Eliminar conexión pendiente</target> + <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> + <source>Delete or moderate up to 200 messages.</source> + <target>Elimina o modera hasta 200 mensajes a la vez.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection?" xml:space="preserve"> <source>Delete pending connection?</source> - <target>¿Eliminar la conexion pendiente?</target> + <target>¿Eliminar conexión pendiente?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete profile" xml:space="preserve"> @@ -1720,11 +2537,31 @@ This cannot be undone!</source> <target>Eliminar cola</target> <note>server test step</note> </trans-unit> + <trans-unit id="Delete report" xml:space="preserve"> + <source>Delete report</source> + <target>Eliminar informe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete up to 20 messages at once." xml:space="preserve"> + <source>Delete up to 20 messages at once.</source> + <target>Elimina hasta 20 mensajes a la vez.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> <target>¿Eliminar perfil de usuario?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete without notification" xml:space="preserve"> + <source>Delete without notification</source> + <target>Elimina sin notificar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve"> + <source>Deleted</source> + <target>Eliminadas</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Deleted at" xml:space="preserve"> <source>Deleted at</source> <target>Eliminado</target> @@ -1735,6 +2572,16 @@ This cannot be undone!</source> <target>Eliminado: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve"> + <source>Deletion errors</source> + <target>Errores de eliminación</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivered even when Apple drops them." xml:space="preserve"> + <source>Delivered even when Apple drops them.</source> + <target>Entregados incluso cuando Apple los descarta.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery" xml:space="preserve"> <source>Delivery</source> <target>Entrega</target> @@ -1757,14 +2604,42 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Desktop address" xml:space="preserve"> <source>Desktop address</source> + <target>Dirección ordenador</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve"> <source>Desktop app version %@ is not compatible with this app.</source> + <target>La versión de aplicación del ordenador %" no es compatible con esta aplicación.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Desktop devices" xml:space="preserve"> <source>Desktop devices</source> + <target>Ordenadores</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve"> + <source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source> + <target>La dirección del servidor de destino de %@ es incompatible con la configuración del servidor de reenvío %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server error: %@" xml:space="preserve"> + <source>Destination server error: %@</source> + <target>Error del servidor de destino: %@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve"> + <source>Destination server version of %@ is incompatible with forwarding server %@.</source> + <target>La versión del servidor de destino de %@ es incompatible con el servidor de reenvío %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Detailed statistics" xml:space="preserve"> + <source>Detailed statistics</source> + <target>Estadísticas detalladas</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Details" xml:space="preserve"> + <source>Details</source> + <target>Detalles</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Develop" xml:space="preserve"> @@ -1772,6 +2647,11 @@ This cannot be undone!</source> <target>Desarrollo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Developer options" xml:space="preserve"> + <source>Developer options</source> + <target>Opciones desarrollador</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> <target>Herramientas desarrollo</target> @@ -1802,8 +2682,13 @@ This cannot be undone!</source> <target>Mensajes directos</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve"> + <source>Direct messages between members are prohibited in this chat.</source> + <target>Mensajes directos no permitidos entre miembros de este chat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> <target>Los mensajes directos entre miembros del grupo no están permitidos.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1817,11 +2702,26 @@ This cannot be undone!</source> <target>Desactivar Bloqueo SimpleX</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Disable automatic message deletion?" xml:space="preserve"> + <source>Disable automatic message deletion?</source> + <target>¿Desactivar la eliminación automática de mensajes?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Disable delete messages" xml:space="preserve"> + <source>Disable delete messages</source> + <target>Desactivar</target> + <note>alert button</note> + </trans-unit> <trans-unit id="Disable for all" xml:space="preserve"> <source>Disable for all</source> <target>Desactivar para todos</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Disabled" xml:space="preserve"> + <source>Disabled</source> + <target>Desactivado</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> <source>Disappearing message</source> <target>Mensaje temporal</target> @@ -1837,8 +2737,8 @@ This cannot be undone!</source> <target>Los mensajes temporales no están permitidos en este chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <target>Los mensajes temporales no están permitidos en este grupo.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1859,6 +2759,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Disconnect desktop?" xml:space="preserve"> <source>Disconnect desktop?</source> + <target>¿Desconectar del ordenador?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Discover and join groups" xml:space="preserve"> @@ -1868,6 +2769,12 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Discover via local network" xml:space="preserve"> <source>Discover via local network</source> + <target>Descubrir en red local</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve"> + <source>Do NOT send messages directly, even if your or destination server does not support private routing.</source> + <target>NO enviar mensajes directamente incluso si tu servidor o el de destino no soportan enrutamiento privado.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> @@ -1875,11 +2782,31 @@ This cannot be undone!</source> <target>NO uses SimpleX para llamadas de emergencia.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT use private routing." xml:space="preserve"> + <source>Do NOT use private routing.</source> + <target>NO usar enrutamiento privado.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do it later" xml:space="preserve"> <source>Do it later</source> <target>Hacer más tarde</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do not send history to new members." xml:space="preserve"> + <source>Do not send history to new members.</source> + <target>No se envía el historial a los miembros nuevos.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not use credentials with proxy." xml:space="preserve"> + <source>Do not use credentials with proxy.</source> + <target>No uses credenciales con proxy.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Documents:" xml:space="preserve"> + <source>Documents:</source> + <target>Documentos:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> <target>No crear dirección SimpleX</target> @@ -1890,9 +2817,19 @@ This cannot be undone!</source> <target>No activar</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Don't miss important messages." xml:space="preserve"> + <source>Don't miss important messages.</source> + <target>No pierdas los mensajes importantes.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> - <target>No mostrar de nuevo</target> + <target>No volver a mostrar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Done" xml:space="preserve"> + <source>Done</source> + <target>Hecho</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Downgrade and open chat" xml:space="preserve"> @@ -1900,11 +2837,52 @@ This cannot be undone!</source> <target>Degradar y abrir Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Download" xml:space="preserve"> + <source>Download</source> + <target>Descargar</target> + <note>alert button +chat item action</note> + </trans-unit> + <trans-unit id="Download errors" xml:space="preserve"> + <source>Download errors</source> + <target>Errores en la descarga</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download failed" xml:space="preserve"> + <source>Download failed</source> + <target>Descarga fallida</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Download file" xml:space="preserve"> <source>Download file</source> <target>Descargar archivo</target> <note>server test step</note> </trans-unit> + <trans-unit id="Download files" xml:space="preserve"> + <source>Download files</source> + <target>Descargar archivos</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Downloaded" xml:space="preserve"> + <source>Downloaded</source> + <target>Descargado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloaded files" xml:space="preserve"> + <source>Downloaded files</source> + <target>Archivos descargados</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading archive" xml:space="preserve"> + <source>Downloading archive</source> + <target>Descargando archivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading link details" xml:space="preserve"> + <source>Downloading link details</source> + <target>Descargando detalles del enlace</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> <target>¡Nombre mostrado duplicado!</target> @@ -1915,6 +2893,11 @@ This cannot be undone!</source> <target>Duración</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="E2E encrypted notifications." xml:space="preserve"> + <source>E2E encrypted notifications.</source> + <target>Notificaciones cifradas E2E.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> <target>Editar</target> @@ -1935,6 +2918,11 @@ This cannot be undone!</source> <target>Activar (conservar anulaciones)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve"> + <source>Enable Flux in Network & servers settings for better metadata privacy.</source> + <target>Habilitar Flux en la configuración de Red y servidores para mejorar la privacidad de los metadatos.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> <target>Activar Bloqueo SimpleX</target> @@ -1948,6 +2936,11 @@ This cannot be undone!</source> <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> <target>¿Activar eliminación automática de mensajes?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Enable camera access" xml:space="preserve"> + <source>Enable camera access</source> + <target>Permitir acceso a la cámara</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable for all" xml:space="preserve"> @@ -1955,6 +2948,11 @@ This cannot be undone!</source> <target>Activar para todos</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve"> + <source>Enable in direct chats (BETA)!</source> + <target>¡Activar en chats directos (BETA)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> <target>¿Activar notificación instantánea?</target> @@ -1985,6 +2983,16 @@ This cannot be undone!</source> <target>Activar código de autodestrucción</target> <note>set passcode view</note> </trans-unit> + <trans-unit id="Enabled" xml:space="preserve"> + <source>Enabled</source> + <target>Activado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve"> + <source>Enabled for</source> + <target>Activado para</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> <target>Cifrar</target> @@ -1997,7 +3005,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Encrypt local files" xml:space="preserve"> <source>Encrypt local files</source> - <target>Cifra archivos locales</target> + <target>Cifrar archivos locales</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Encrypt stored files & media" xml:space="preserve"> @@ -2015,6 +3023,11 @@ This cannot be undone!</source> <target>Mensaje cifrado u otro evento</target> <note>notification</note> </trans-unit> + <trans-unit id="Encrypted message: app is stopped" xml:space="preserve"> + <source>Encrypted message: app is stopped</source> + <target>Mensaje cifrado: la aplicación está parada</target> + <note>notification</note> + </trans-unit> <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> <target>Mensaje cifrado: error base de datos</target> @@ -2042,10 +3055,17 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Encryption re-negotiation error" xml:space="preserve"> <source>Encryption re-negotiation error</source> + <target>Error de renegociación de cifrado</target> <note>message decrypt error item</note> </trans-unit> <trans-unit id="Encryption re-negotiation failed." xml:space="preserve"> <source>Encryption re-negotiation failed.</source> + <target>Renegociación de cifrado fallida.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encryption renegotiation in progress." xml:space="preserve"> + <source>Encryption renegotiation in progress.</source> + <target>Renegociación de cifrado en curso.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter Passcode" xml:space="preserve"> @@ -2060,6 +3080,12 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Enter group name…" xml:space="preserve"> <source>Enter group name…</source> + <target>Nombre del grupo…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve"> + <source>Enter passphrase</source> + <target>Introduce la frase de contraseña</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter passphrase…" xml:space="preserve"> @@ -2074,11 +3100,12 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Enter server manually" xml:space="preserve"> <source>Enter server manually</source> - <target>Introduce el servidor manualmente</target> + <target>Añadir manualmente</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter this device name…" xml:space="preserve"> <source>Enter this device name…</source> + <target>Nombre de este dispositivo…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter welcome message…" xml:space="preserve"> @@ -2093,6 +3120,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Enter your name…" xml:space="preserve"> <source>Enter your name…</source> + <target>Introduce tu nombre…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error" xml:space="preserve"> @@ -2105,24 +3133,34 @@ This cannot be undone!</source> <target>Error al cancelar cambio de dirección</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error accepting conditions" xml:space="preserve"> + <source>Error accepting conditions</source> + <target>Error al aceptar las condiciones</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> <target>Error al aceptar solicitud del contacto</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve"> - <source>Error accessing database file</source> - <target>Error al acceder al archivo de la base de datos</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> <target>Error al añadir miembro(s)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error adding server" xml:space="preserve"> + <source>Error adding server</source> + <target>Error al añadir servidor</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> - <target>Error al cambiar dirección</target> + <target>Error al cambiar servidor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error changing connection profile" xml:space="preserve"> + <source>Error changing connection profile</source> + <target>Error al cambiar el perfil de conexión</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error changing role" xml:space="preserve"> @@ -2135,6 +3173,21 @@ This cannot be undone!</source> <target>Error cambiando configuración</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing to incognito!" xml:space="preserve"> + <source>Error changing to incognito!</source> + <target>¡Error al cambiar a incógnito!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error checking token status" xml:space="preserve"> + <source>Error checking token status</source> + <target>Error al verificar el estado del token</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve"> + <source>Error connecting to forwarding server %@. Please try later.</source> + <target>Error al conectar con el servidor de reenvío %@. Por favor, inténtalo más tarde.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> <target>Error al crear dirección</target> @@ -2150,16 +3203,31 @@ This cannot be undone!</source> <target>Error al crear enlace de grupo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating list" xml:space="preserve"> + <source>Error creating list</source> + <target>Error al crear lista</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error creating member contact" xml:space="preserve"> <source>Error creating member contact</source> <target>Error al establecer contacto con el miembro</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating message" xml:space="preserve"> + <source>Error creating message</source> + <target>Error al crear mensaje</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>¡Error al crear perfil!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating report" xml:space="preserve"> + <source>Error creating report</source> + <target>Error al crear informe</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error decrypting file" xml:space="preserve"> <source>Error decrypting file</source> <target>Error al descifrar el archivo</target> @@ -2180,11 +3248,6 @@ This cannot be undone!</source> <target>Error al eliminar conexión</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve"> - <source>Error deleting contact</source> - <target>Error al eliminar contacto</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> <target>Error al eliminar base de datos</target> @@ -2205,6 +3268,11 @@ This cannot be undone!</source> <target>Error al eliminar perfil</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error downloading the archive" xml:space="preserve"> + <source>Error downloading the archive</source> + <target>Error al descargar el archivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> <source>Error enabling delivery receipts!</source> <target>¡Error al activar confirmaciones de entrega!</target> @@ -2225,6 +3293,11 @@ This cannot be undone!</source> <target>Error al exportar base de datos</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error exporting theme: %@" xml:space="preserve"> + <source>Error exporting theme: %@</source> + <target>Error al exportar tema: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> <target>Error al importar base de datos</target> @@ -2232,27 +3305,57 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Error joining group" xml:space="preserve"> <source>Error joining group</source> - <target>Error al unirse al grupo</target> + <target>Error al unirte al grupo</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve"> - <source>Error loading %@ servers</source> - <target>Error al cargar servidores %@</target> + <trans-unit id="Error loading servers" xml:space="preserve"> + <source>Error loading servers</source> + <target>Error al cargar servidores</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error migrating settings" xml:space="preserve"> + <source>Error migrating settings</source> + <target>Error al migrar la configuración</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error opening chat" xml:space="preserve"> + <source>Error opening chat</source> + <target>Error al abrir chat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> <target>Error al recibir archivo</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error reconnecting server" xml:space="preserve"> + <source>Error reconnecting server</source> + <target>Error al reconectar con el servidor</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error reconnecting servers" xml:space="preserve"> + <source>Error reconnecting servers</source> + <target>Error al reconectar con los servidores</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error registering for notifications" xml:space="preserve"> + <source>Error registering for notifications</source> + <target>Error al registrarse para notificaciones</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> - <target>Error al eliminar miembro</target> + <target>Error al expulsar miembro</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve"> - <source>Error saving %@ servers</source> - <target>Error al guardar servidores %@</target> + <trans-unit id="Error reordering lists" xml:space="preserve"> + <source>Error reordering lists</source> + <target>Error al reorganizar listas</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error resetting statistics" xml:space="preserve"> + <source>Error resetting statistics</source> + <target>Error al restablecer las estadísticas</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving ICE servers" xml:space="preserve"> @@ -2260,6 +3363,11 @@ This cannot be undone!</source> <target>Error al guardar servidores ICE</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving chat list" xml:space="preserve"> + <source>Error saving chat list</source> + <target>Error al guardar listas</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> <target>Error al guardar perfil de grupo</target> @@ -2275,11 +3383,26 @@ This cannot be undone!</source> <target>Error al guardar contraseña en Keychain</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving servers" xml:space="preserve"> + <source>Error saving servers</source> + <target>Error al guardar servidores</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error saving settings" xml:space="preserve"> + <source>Error saving settings</source> + <target>Error al guardar ajustes</target> + <note>when migrating</note> + </trans-unit> <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> <target>Error al guardar contraseña de usuario</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error scanning code: %@" xml:space="preserve"> + <source>Error scanning code: %@</source> + <target>Error al escanear código: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending email" xml:space="preserve"> <source>Error sending email</source> <target>Error al enviar email</target> @@ -2307,19 +3430,29 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Error stopping chat" xml:space="preserve"> <source>Error stopping chat</source> - <target>Error al detener Chat</target> + <target>Error al parar SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error switching profile" xml:space="preserve"> + <source>Error switching profile</source> + <target>Error al cambiar perfil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> <target>¡Error al cambiar perfil!</target> - <note>No comment provided by engineer.</note> + <note>alertTitle</note> </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> <target>Error al sincronizar conexión</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error testing server connection" xml:space="preserve"> + <source>Error testing server connection</source> + <target>Error al testar la conexión al servidor</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> <target>Error al actualizar enlace de grupo</target> @@ -2330,6 +3463,11 @@ This cannot be undone!</source> <target>Error al actualizar mensaje</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error updating server" xml:space="preserve"> + <source>Error updating server</source> + <target>Error al actualizar el servidor</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> <target>Error al actualizar configuración</target> @@ -2340,6 +3478,16 @@ This cannot be undone!</source> <target>Error al actualizar privacidad de usuario</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error uploading the archive" xml:space="preserve"> + <source>Error uploading the archive</source> + <target>Error al subir el archivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error verifying passphrase:" xml:space="preserve"> + <source>Error verifying passphrase:</source> + <target>Error al verificar la frase de contraseña:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error: " xml:space="preserve"> <source>Error: </source> <target>Error: </target> @@ -2348,7 +3496,9 @@ This cannot be undone!</source> <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> <target>Error: %@</target> - <note>No comment provided by engineer.</note> + <note>alert message +file error text +snd error text</note> </trans-unit> <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> @@ -2360,6 +3510,16 @@ This cannot be undone!</source> <target>Error: sin archivo de base de datos</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Errors" xml:space="preserve"> + <source>Errors</source> + <target>Errores</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors in servers configuration." xml:space="preserve"> + <source>Errors in servers configuration.</source> + <target>Error en la configuración del servidor.</target> + <note>servers error</note> + </trans-unit> <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> <source>Even when disabled in the conversation.</source> <target>Incluso si está desactivado para la conversación.</target> @@ -2372,8 +3532,14 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Expand" xml:space="preserve"> <source>Expand</source> + <target>Expandir</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Expired" xml:space="preserve"> + <source>Expired</source> + <target>Expirado</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> <target>Exportar base de datos</target> @@ -2384,11 +3550,21 @@ This cannot be undone!</source> <target>Error al exportar:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Export theme" xml:space="preserve"> + <source>Export theme</source> + <target>Exportar tema</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> <target>Archivo de base de datos exportado.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Exported file doesn't exist" xml:space="preserve"> + <source>Exported file doesn't exist</source> + <target>El archivo exportado no existe</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exporting database archive…" xml:space="preserve"> <source>Exporting database archive…</source> <target>Exportando base de datos…</target> @@ -2404,15 +3580,70 @@ This cannot be undone!</source> <target>¡Rápido y sin necesidad de esperar a que el remitente esté en línea!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster deletion of groups." xml:space="preserve"> + <source>Faster deletion of groups.</source> + <target>Eliminación más rápida de grupos.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Faster joining and more reliable messages." xml:space="preserve"> <source>Faster joining and more reliable messages.</source> + <target>Mensajería más segura y conexión más rápida.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Faster sending messages." xml:space="preserve"> + <source>Faster sending messages.</source> + <target>Envío más rápido de mensajes.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Favorite" xml:space="preserve"> <source>Favorite</source> <target>Favoritos</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Favorites" xml:space="preserve"> + <source>Favorites</source> + <target>Favoritos</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Error de archivo</target> + <note>file error alert title</note> + </trans-unit> + <trans-unit id="File errors: %@" xml:space="preserve"> + <source>File errors: +%@</source> + <target>Error(es) de archivo +%@</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="File is blocked by server operator: %@." xml:space="preserve"> + <source>File is blocked by server operator: +%@.</source> + <target>Archivo bloqueado por el operador del servidor +%@.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve"> + <source>File not found - most likely file was deleted or cancelled.</source> + <target>Archivo no encontrado, probablemente haya sido eliminado o cancelado.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File server error: %@" xml:space="preserve"> + <source>File server error: %@</source> + <target>Error del servidor de archivos: %@</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File status" xml:space="preserve"> + <source>File status</source> + <target>Estado del archivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File status: %@" xml:space="preserve"> + <source>File status: %@</source> + <target>Estado del archivo: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="File will be deleted from servers." xml:space="preserve"> <source>File will be deleted from servers.</source> <target>El archivo será eliminado de los servidores.</target> @@ -2420,12 +3651,12 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="File will be received when your contact completes uploading it." xml:space="preserve"> <source>File will be received when your contact completes uploading it.</source> - <target>El archivo se recibirá cuando tu contacto termine de subirlo.</target> + <target>El archivo se recibirá cuando el contacto termine de subirlo.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="File will be received when your contact is online, please wait or check later!" xml:space="preserve"> <source>File will be received when your contact is online, please wait or check later!</source> - <target>El archivo se recibirá cuando tu contacto esté en línea, ¡por favor espera o compruébalo más tarde!</target> + <target>El archivo se recibirá cuando el contacto esté en línea, ¡por favor espera o revisa más tarde!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="File: %@" xml:space="preserve"> @@ -2433,6 +3664,11 @@ This cannot be undone!</source> <target>Archivo: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files" xml:space="preserve"> + <source>Files</source> + <target>Archivos</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> <target>Archivos y multimedia</target> @@ -2443,9 +3679,14 @@ This cannot be undone!</source> <target>Archivos y multimedia</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> - <source>Files and media are prohibited in this group.</source> - <target>No se permiten archivos y multimedia en este grupo.</target> + <trans-unit id="Files and media are prohibited." xml:space="preserve"> + <source>Files and media are prohibited.</source> + <target>Los archivos y multimedia no están permitidos en este grupo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Files and media not allowed" xml:space="preserve"> + <source>Files and media not allowed</source> + <target>Archivos y multimedia no permitidos</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Files and media prohibited!" xml:space="preserve"> @@ -2458,6 +3699,16 @@ This cannot be undone!</source> <target>Filtra chats no leídos y favoritos.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Finalize migration" xml:space="preserve"> + <source>Finalize migration</source> + <target>Finalizar migración</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration on another device." xml:space="preserve"> + <source>Finalize migration on another device.</source> + <target>Finalizar la migración en otro dispositivo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> <target>¡Por fin los tenemos! 🚀</target> @@ -2498,13 +3749,118 @@ This cannot be undone!</source> <target>Corrección no compatible con miembro del grupo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For all moderators" xml:space="preserve"> + <source>For all moderators</source> + <target>Para todos los moderadores</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For chat profile %@:" xml:space="preserve"> + <source>For chat profile %@:</source> + <target>Para el perfil de chat %@:</target> + <note>servers error</note> + </trans-unit> <trans-unit id="For console" xml:space="preserve"> <source>For console</source> <target>Para consola</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." xml:space="preserve"> + <source>For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</source> + <target>Si por ejemplo tu contacto recibe los mensajes a través de un servidor de SimpleX Chat, tu aplicación los entregará a través de un servidor de Flux.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For me" xml:space="preserve"> + <source>For me</source> + <target>para mí</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For private routing" xml:space="preserve"> + <source>For private routing</source> + <target>Para enrutamiento privado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For social media" xml:space="preserve"> + <source>For social media</source> + <target>Para redes sociales</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward" xml:space="preserve"> + <source>Forward</source> + <target>Reenviar</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Forward %d message(s)?" xml:space="preserve"> + <source>Forward %d message(s)?</source> + <target>¿Reenviar %d mensaje(s)?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Forward and save messages" xml:space="preserve"> + <source>Forward and save messages</source> + <target>Reenviar y guardar mensajes</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward messages" xml:space="preserve"> + <source>Forward messages</source> + <target>Reenviar mensajes</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Forward messages without files?" xml:space="preserve"> + <source>Forward messages without files?</source> + <target>¿Reenviar mensajes sin los archivos?</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> + <source>Forward up to 20 messages at once.</source> + <target>Desplazamiento de hasta 20 mensajes.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded" xml:space="preserve"> + <source>Forwarded</source> + <target>Reenviado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded from" xml:space="preserve"> + <source>Forwarded from</source> + <target>Reenviado por</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding %lld messages" xml:space="preserve"> + <source>Forwarding %lld messages</source> + <target>Reenviando %lld mensajes</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve"> + <source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source> + <target>El servidor de reenvío %@ no ha podido conectarse al servidor de destino %@. Por favor, intentalo más tarde.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server address is incompatible with network settings: %@.</source> + <target>La dirección del servidor de reenvío es incompatible con la configuración de red: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server version is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server version is incompatible with network settings: %@.</source> + <target>La versión del servidor de reenvío es incompatible con la configuración de red: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Destination server error: %2$@</source> + <target>Servidor de reenvío: %1$@ +Error del servidor de destino: %2$@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Error: %2$@</source> + <target>Servidor de reenvío: %1$@ +Error: %2$@</target> + <note>snd error text</note> + </trans-unit> <trans-unit id="Found desktop" xml:space="preserve"> <source>Found desktop</source> + <target>Ordenador encontrado</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="French interface" xml:space="preserve"> @@ -2522,18 +3878,14 @@ This cannot be undone!</source> <target>Nombre completo (opcional)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full name:" xml:space="preserve"> - <source>Full name:</source> - <target>Nombre completo:</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Fully decentralized – visible only to members." xml:space="preserve"> <source>Fully decentralized – visible only to members.</source> + <target>Totalmente descentralizado. Visible sólo para los miembros.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fully re-implemented - work in background!" xml:space="preserve"> <source>Fully re-implemented - work in background!</source> - <target>Completamente reimplementado: ¡funciona en segundo plano!</target> + <target>Totalmente revisado. ¡Funciona en segundo plano!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Further reduced battery usage" xml:space="preserve"> @@ -2546,6 +3898,21 @@ This cannot be undone!</source> <target>GIFs y stickers</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Get notified when mentioned." xml:space="preserve"> + <source>Get notified when mentioned.</source> + <target>Las menciones ahora se notifican.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Good afternoon!" xml:space="preserve"> + <source>Good afternoon!</source> + <target>¡Buenas tardes!</target> + <note>message preview</note> + </trans-unit> + <trans-unit id="Good morning!" xml:space="preserve"> + <source>Good morning!</source> + <target>¡Buenos días!</target> + <note>message preview</note> + </trans-unit> <trans-unit id="Group" xml:space="preserve"> <source>Group</source> <target>Grupo</target> @@ -2553,10 +3920,12 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Group already exists" xml:space="preserve"> <source>Group already exists</source> + <target>El grupo ya existe</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group already exists!" xml:space="preserve"> <source>Group already exists!</source> + <target>¡El grupo ya existe!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group display name" xml:space="preserve"> @@ -2599,36 +3968,6 @@ This cannot be undone!</source> <target>Enlaces de grupo</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve"> - <source>Group members can add message reactions.</source> - <target>Los miembros pueden añadir reacciones a los mensajes.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> - <source>Group members can irreversibly delete sent messages.</source> - <target>Los miembros del grupo pueden eliminar mensajes de forma irreversible.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> - <target>Los miembros del grupo pueden enviar mensajes directos.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> - <target>Los miembros del grupo pueden enviar mensajes temporales.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send files and media." xml:space="preserve"> - <source>Group members can send files and media.</source> - <target>Los miembros del grupo pueden enviar archivos y multimedia.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> - <target>Los miembros del grupo pueden enviar mensajes de voz.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> <target>Mensaje de grupo:</target> @@ -2641,7 +3980,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Group preferences" xml:space="preserve"> <source>Group preferences</source> - <target>Preferencias de grupo</target> + <target>Preferencias del grupo</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group profile" xml:space="preserve"> @@ -2661,12 +4000,17 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Group will be deleted for all members - this cannot be undone!" xml:space="preserve"> <source>Group will be deleted for all members - this cannot be undone!</source> - <target>El grupo será eliminado para todos los miembros. ¡No podrá deshacerse!</target> + <target>El grupo será eliminado para todos los miembros. ¡No puede deshacerse!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group will be deleted for you - this cannot be undone!" xml:space="preserve"> <source>Group will be deleted for you - this cannot be undone!</source> - <target>El grupo será eliminado para tí. ¡No podrá deshacerse!</target> + <target>El grupo será eliminado para tí. ¡No puede deshacerse!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Groups" xml:space="preserve"> + <source>Groups</source> + <target>Grupos</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Help" xml:space="preserve"> @@ -2674,6 +4018,11 @@ This cannot be undone!</source> <target>Ayuda</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Help admins moderating their groups." xml:space="preserve"> + <source>Help admins moderating their groups.</source> + <target>Ayuda a los admins a moderar sus grupos.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> <target>Oculto</target> @@ -2714,15 +4063,30 @@ This cannot be undone!</source> <target>Historial</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="History is not sent to new members." xml:space="preserve"> + <source>History is not sent to new members.</source> + <target>El historial no se envía a miembros nuevos.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> <target>Cómo funciona SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="How it affects privacy" xml:space="preserve"> + <source>How it affects privacy</source> + <target>Cómo afecta a la privacidad</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it helps privacy" xml:space="preserve"> + <source>How it helps privacy</source> + <target>Cómo ayuda a la privacidad</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> <target>Cómo funciona</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="How to" xml:space="preserve"> <source>How to</source> @@ -2739,19 +4103,24 @@ This cannot be undone!</source> <target>Cómo usar los servidores</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Hungarian interface" xml:space="preserve"> + <source>Hungarian interface</source> + <target>Interfaz en húngaro</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> <target>Servidores ICE (uno por línea)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> - <source>If you can't meet in person, show QR code in a video call, or share the link.</source> - <target>Si no puedes reunirte en persona, muestra el código QR por videollamada, o comparte el enlace.</target> + <trans-unit id="IP address" xml:space="preserve"> + <source>IP address</source> + <target>Dirección IP</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <target>Si no puedes reunirte en persona, puedes **escanear el código QR por videollamada**, o tu contacto puede compartir un enlace de invitación.</target> + <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> + <source>If you can't meet in person, show QR code in a video call, or share the link.</source> + <target>Si no puedes reunirte en persona, muestra el código QR por videollamada o comparte el enlace.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> @@ -2776,12 +4145,12 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Image will be received when your contact completes uploading it." xml:space="preserve"> <source>Image will be received when your contact completes uploading it.</source> - <target>La imagen se recibirá cuando tu contacto termine de subirla.</target> + <target>La imagen se recibirá cuando el contacto termine de subirla.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve"> <source>Image will be received when your contact is online, please wait or check later!</source> - <target>La imagen se recibirá cuando tu contacto esté en línea, ¡por favor espera o compruébalo más tarde!</target> + <target>La imagen se recibirá cuando el contacto esté en línea, ¡por favor espera o revisa más tarde!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Immediately" xml:space="preserve"> @@ -2789,8 +4158,8 @@ This cannot be undone!</source> <target>Inmediatamente</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <target>Inmune a spam y abuso</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2809,6 +4178,33 @@ This cannot be undone!</source> <target>Importar base de datos</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Import failed" xml:space="preserve"> + <source>Import failed</source> + <target>Error de importación</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import theme" xml:space="preserve"> + <source>Import theme</source> + <target>Importar tema</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Importing archive" xml:space="preserve"> + <source>Importing archive</source> + <target>Importando archivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved delivery, reduced traffic usage. More improvements are coming soon!" xml:space="preserve"> + <source>Improved delivery, reduced traffic usage. +More improvements are coming soon!</source> + <target>Reducción del tráfico y entrega mejorada. +¡Pronto habrá nuevas mejoras!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved message delivery" xml:space="preserve"> + <source>Improved message delivery</source> + <target>Entrega de mensajes mejorada</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> <target>Seguridad y privacidad mejoradas</target> @@ -2819,11 +4215,31 @@ This cannot be undone!</source> <target>Configuración del servidor mejorada</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In order to continue, chat should be stopped." xml:space="preserve"> + <source>In order to continue, chat should be stopped.</source> + <target>Para continuar, SimpleX debe estar parado.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>En respuesta a</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In-call sounds" xml:space="preserve"> + <source>In-call sounds</source> + <target>Sonido de llamada</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Inappropriate content" xml:space="preserve"> + <source>Inappropriate content</source> + <target>Contenido inapropiado</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Inappropriate profile" xml:space="preserve"> + <source>Inappropriate profile</source> + <target>Perfil inapropiado</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> <target>Incógnito</target> @@ -2831,6 +4247,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Incognito groups" xml:space="preserve"> <source>Incognito groups</source> + <target>Grupos incógnito</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito mode" xml:space="preserve"> @@ -2865,6 +4282,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Incompatible version" xml:space="preserve"> <source>Incompatible version</source> + <target>Versión incompatible</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incorrect passcode" xml:space="preserve"> @@ -2892,6 +4310,11 @@ This cannot be undone!</source> <target>Instalar terminal para [SimpleX Chat](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Instant" xml:space="preserve"> + <source>Instant</source> + <target>Al instante</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> @@ -2899,29 +4322,80 @@ This cannot be undone!</source> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Instantly" xml:space="preserve"> - <source>Instantly</source> - <target>Al instante</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> <target>Interfaz</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Interface colors" xml:space="preserve"> + <source>Interface colors</source> + <target>Colores del interfaz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid" xml:space="preserve"> + <source>Invalid</source> + <target>No válido</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (bad token)" xml:space="preserve"> + <source>Invalid (bad token)</source> + <target>No válido (token incorrecto)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (expired)" xml:space="preserve"> + <source>Invalid (expired)</source> + <target>No válido (expirado)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (unregistered)" xml:space="preserve"> + <source>Invalid (unregistered)</source> + <target>No válido (no registrado)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (wrong topic)" xml:space="preserve"> + <source>Invalid (wrong topic)</source> + <target>No válido (tópico incorrecto)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid QR code" xml:space="preserve"> + <source>Invalid QR code</source> + <target>Código QR no válido</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> <target>Enlace de conexión no válido</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid display name!" xml:space="preserve"> + <source>Invalid display name!</source> + <target>¡Nombre mostrado no válido!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid link" xml:space="preserve"> + <source>Invalid link</source> + <target>Enlace no válido</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Confirmación de migración no válida</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid name!" xml:space="preserve"> <source>Invalid name!</source> + <target>¡Nombre no válido!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid response" xml:space="preserve"> + <source>Invalid response</source> + <target>Respuesta no válida</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> <target>¡Dirección de servidor no válida!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Invalid status" xml:space="preserve"> <source>Invalid status</source> @@ -2943,6 +4417,11 @@ This cannot be undone!</source> <target>Invitar miembros</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invite to chat" xml:space="preserve"> + <source>Invite to chat</source> + <target>Invitar al chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invite to group" xml:space="preserve"> <source>Invite to group</source> <target>Invitar al grupo</target> @@ -2958,8 +4437,8 @@ This cannot be undone!</source> <target>La eliminación irreversible de mensajes no está permitida en este chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <target>La eliminación irreversible de mensajes no está permitida en este grupo.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2979,11 +4458,16 @@ This cannot be undone!</source> 2. Message decryption failed, because you or your contact used old database backup. 3. The connection was compromised.</source> <target>Esto puede suceder cuando: -1. Los mensajes caducan en el cliente saliente tras 2 días o en el servidor tras 30 días. +1. Los mensajes caducan tras 2 días en el cliente saliente o tras 30 días en el servidor. 2. El descifrado ha fallado porque tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos. 3. La conexión ha sido comprometida.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="It protects your IP address and connections." xml:space="preserve"> + <source>It protects your IP address and connections.</source> + <target>Protege tu dirección IP y tus conexiones.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> <target>Parece que ya estás conectado mediante este enlace. Si no es así ha habido un error (%@).</target> @@ -3001,30 +4485,39 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Join" xml:space="preserve"> <source>Join</source> - <target>Únete</target> - <note>No comment provided by engineer.</note> + <target>Unirte</target> + <note>swipe action</note> </trans-unit> <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> - <target>Únete al grupo</target> + <target>Unirte al grupo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Join group conversations" xml:space="preserve"> + <source>Join group conversations</source> + <target>Unirse a la conversación del grupo</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Join group?" xml:space="preserve"> <source>Join group?</source> + <target>¿Unirte al grupo?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Join incognito" xml:space="preserve"> <source>Join incognito</source> - <target>Únete en modo incógnito</target> + <target>Unirte en modo incógnito</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Join with current profile" xml:space="preserve"> <source>Join with current profile</source> + <target>Unirte con el perfil actual</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Join your group? This is your link for group %@!" xml:space="preserve"> <source>Join your group? This is your link for group %@!</source> + <target>¿Unirse a tu grupo? +¡Este es tu enlace para el grupo %@!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Joining group" xml:space="preserve"> @@ -3032,10 +4525,26 @@ This is your link for group %@!</source> <target>Entrando al grupo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep" xml:space="preserve"> + <source>Keep</source> + <target>Guardar</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Keep conversation" xml:space="preserve"> + <source>Keep conversation</source> + <target>Conservar conversación</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Keep the app open to use it from desktop" xml:space="preserve"> <source>Keep the app open to use it from desktop</source> + <target>Mantén la aplicación abierta para usarla desde el ordenador</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep unused invitation?" xml:space="preserve"> + <source>Keep unused invitation?</source> + <target>¿Guardar invitación no usada?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> <source>Keep your connections</source> <target>Conserva tus conexiones</target> @@ -3069,6 +4578,16 @@ This is your link for group %@!</source> <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> <target>Salir</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Leave chat" xml:space="preserve"> + <source>Leave chat</source> + <target>Salir del chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave chat?" xml:space="preserve"> + <source>Leave chat?</source> + <target>¿Salir del chat?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group" xml:space="preserve"> @@ -3098,14 +4617,32 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Link mobile and desktop apps! 🔗" xml:space="preserve"> <source>Link mobile and desktop apps! 🔗</source> + <target>¡Enlazar aplicación móvil con ordenador! 🔗</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Linked desktop options" xml:space="preserve"> <source>Linked desktop options</source> + <target>Opciones ordenador enlazado</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Linked desktops" xml:space="preserve"> <source>Linked desktops</source> + <target>Ordenadores enlazados</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List" xml:space="preserve"> + <source>List</source> + <target>Lista</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="List name and emoji should be different for all lists." xml:space="preserve"> + <source>List name and emoji should be different for all lists.</source> + <target>El nombre y el emoji deben ser diferentes en todas las listas.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List name..." xml:space="preserve"> + <source>List name...</source> + <target>Nombre de la lista...</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Live message!" xml:space="preserve"> @@ -3125,7 +4662,7 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Local profile data only" xml:space="preserve"> <source>Local profile data only</source> - <target>Sólo datos del perfil local</target> + <target>Eliminar sólo el perfil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Lock after" xml:space="preserve"> @@ -3138,11 +4675,6 @@ This is your link for group %@!</source> <target>Modo bloqueo</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> - <source>Make a private connection</source> - <target>Establecer una conexión privada</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make one message disappear" xml:space="preserve"> <source>Make one message disappear</source> <target>Escribir un mensaje temporal</target> @@ -3153,21 +4685,11 @@ This is your link for group %@!</source> <target>¡Hacer perfil privado!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> - <target>Asegúrate de que las direcciones del servidor %@ tienen el formato correcto, están separadas por líneas y no duplicadas (%@).</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> <target>Asegúrate de que las direcciones del servidor WebRTC ICE tienen el formato correcto, están separadas por líneas y no duplicadas.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <target>Muchos se preguntarán: *si SimpleX no tiene identificadores de usuario, ¿cómo puede entregar los mensajes?*</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> <source>Mark deleted for everyone</source> <target>Marcar como eliminado para todos</target> @@ -3193,24 +4715,104 @@ This is your link for group %@!</source> <target>Máximo 30 segundos, recibido al instante.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Media & file servers" xml:space="preserve"> + <source>Media & file servers</source> + <target>Servidores de archivos y multimedia</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Medium" xml:space="preserve"> + <source>Medium</source> + <target>Medio</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Member" xml:space="preserve"> <source>Member</source> <target>Miembro</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member inactive" xml:space="preserve"> + <source>Member inactive</source> + <target>Miembro inactivo</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Member reports" xml:space="preserve"> + <source>Member reports</source> + <target>Informes de miembros</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All chat members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All chat members will be notified.</source> + <target>El rol del miembro cambiará a "%@" y todos serán notificados.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> - <target>El rol de miembro cambiará a "%@". Todos los miembros del grupo serán notificados.</target> + <target>El rol del miembro cambiará a "%@" y se notificará al grupo.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Member role will be changed to "%@". The member will receive a new invitation." xml:space="preserve"> <source>Member role will be changed to "%@". The member will receive a new invitation.</source> - <target>El rol del miembro cambiará a "%@". El miembro recibirá una invitación nueva.</target> + <target>El rol del miembro cambiará a "%@" y recibirá una invitación nueva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from chat - this cannot be undone!</source> + <target>El miembro será eliminado del chat. ¡No puede deshacerse!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> - <target>El miembro será expulsado del grupo. ¡No podrá deshacerse!</target> + <target>El miembro será expulsado del grupo. ¡No puede deshacerse!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can add message reactions." xml:space="preserve"> + <source>Members can add message reactions.</source> + <target>Los miembros pueden añadir reacciones a los mensajes.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Members can irreversibly delete sent messages. (24 hours)</source> + <target>Los miembros del grupo pueden eliminar mensajes de forma irreversible. (24 horas)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can report messsages to moderators." xml:space="preserve"> + <source>Members can report messsages to moderators.</source> + <target>Los miembros pueden informar de mensajes a los moderadores.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send SimpleX links." xml:space="preserve"> + <source>Members can send SimpleX links.</source> + <target>Los miembros del grupo pueden enviar enlaces SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> + <target>Los miembros del grupo pueden enviar mensajes directos.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> + <target>Los miembros del grupo pueden enviar mensajes temporales.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send files and media." xml:space="preserve"> + <source>Members can send files and media.</source> + <target>Los miembros del grupo pueden enviar archivos y multimedia.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> + <target>Los miembros del grupo pueden enviar mensajes de voz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mention members 👋" xml:space="preserve"> + <source>Mention members 👋</source> + <target>Menciona a miembros 👋</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Menus" xml:space="preserve"> + <source>Menus</source> + <target>Menus</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Message delivery error" xml:space="preserve"> @@ -3223,14 +4825,34 @@ This is your link for group %@!</source> <target>¡Confirmación de entrega de mensajes!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message delivery warning" xml:space="preserve"> + <source>Message delivery warning</source> + <target>Aviso de entrega de mensaje</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> <target>Borrador de mensaje</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message forwarded" xml:space="preserve"> + <source>Message forwarded</source> + <target>Mensaje reenviado</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message may be delivered later if member becomes active." xml:space="preserve"> + <source>Message may be delivered later if member becomes active.</source> + <target>El mensaje podría ser entregado más tarde si el miembro vuelve a estar activo.</target> + <note>item status description</note> + </trans-unit> + <trans-unit id="Message queue info" xml:space="preserve"> + <source>Message queue info</source> + <target>Información cola de mensajes</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message reactions" xml:space="preserve"> <source>Message reactions</source> - <target>Reacciones a los mensajes</target> + <target>Reacciones a mensajes</target> <note>chat feature</note> </trans-unit> <trans-unit id="Message reactions are prohibited in this chat." xml:space="preserve"> @@ -3238,16 +4860,51 @@ This is your link for group %@!</source> <target>Las reacciones a los mensajes no están permitidas en este chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> - <source>Message reactions are prohibited in this group.</source> + <trans-unit id="Message reactions are prohibited." xml:space="preserve"> + <source>Message reactions are prohibited.</source> <target>Las reacciones a los mensajes no están permitidas en este grupo.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message reception" xml:space="preserve"> + <source>Message reception</source> + <target>Recepción de mensaje</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message servers" xml:space="preserve"> + <source>Message servers</source> + <target>Servidores de mensajes</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message shape" xml:space="preserve"> + <source>Message shape</source> + <target>Forma del mensaje</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message source remains private." xml:space="preserve"> + <source>Message source remains private.</source> + <target>El autor del mensaje se mantiene privado.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status" xml:space="preserve"> + <source>Message status</source> + <target>Estado del mensaje</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status: %@" xml:space="preserve"> + <source>Message status: %@</source> + <target>Estado del mensaje: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> <target>Contacto y texto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message too large" xml:space="preserve"> + <source>Message too large</source> + <target>Mensaje demasiado largo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> <target>Mensajes</target> @@ -3260,6 +4917,67 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Messages from %@ will be shown!" xml:space="preserve"> <source>Messages from %@ will be shown!</source> + <target>¡Los mensajes de %@ serán mostrados!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages in this chat will never be deleted." xml:space="preserve"> + <source>Messages in this chat will never be deleted.</source> + <target>Los mensajes de esta conversación nunca se eliminan.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages received" xml:space="preserve"> + <source>Messages received</source> + <target>Mensajes recibidos</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages sent" xml:space="preserve"> + <source>Messages sent</source> + <target>Mensajes enviados</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages were deleted after you selected them." xml:space="preserve"> + <source>Messages were deleted after you selected them.</source> + <target>Los mensajes han sido eliminados después de seleccionarlos.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Los mensajes, archivos y llamadas están protegidos mediante **cifrado de extremo a extremo** con secreto perfecto hacía adelante, repudio y recuperación tras ataque.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate device" xml:space="preserve"> + <source>Migrate device</source> + <target>Migrar dispositivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate from another device" xml:space="preserve"> + <source>Migrate from another device</source> + <target>Migrar desde otro dispositivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate here" xml:space="preserve"> + <source>Migrate here</source> + <target>Migrar aquí</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device" xml:space="preserve"> + <source>Migrate to another device</source> + <target>Migrar a otro dispositivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device via QR code." xml:space="preserve"> + <source>Migrate to another device via QR code.</source> + <target>Migrar a otro dispositivo mediante código QR.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating" xml:space="preserve"> + <source>Migrating</source> + <target>Migrando</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Migrating database archive…" xml:space="preserve"> @@ -3267,6 +4985,11 @@ This is your link for group %@!</source> <target>Migrando base de datos…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Migration complete" xml:space="preserve"> + <source>Migration complete</source> + <target>Migración completada</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> <target>Error de migración:</target> @@ -3282,9 +5005,9 @@ This is your link for group %@!</source> <target>Migración completada</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> - <target>Migraciones: %@</target> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> + <target>Migraciones:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> @@ -3302,21 +5025,31 @@ This is your link for group %@!</source> <target>Moderado: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="More" xml:space="preserve"> + <source>More</source> + <target>Más</target> + <note>swipe action</note> + </trans-unit> <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> <target>¡Pronto habrá más mejoras!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="More reliable network connection." xml:space="preserve"> + <source>More reliable network connection.</source> + <target>Conexión de red más fiable.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="More reliable notifications" xml:space="preserve"> + <source>More reliable notifications</source> + <target>Notificaciones más fiables</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> <source>Most likely this connection is deleted.</source> <target>Probablemente la conexión ha sido eliminada.</target> <note>item status description</note> </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> - <source>Most likely this contact has deleted the connection with you.</source> - <target>Lo más probable es que este contacto haya eliminado la conexión contigo.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> <target>Múltiples perfiles</target> @@ -3325,7 +5058,12 @@ This is your link for group %@!</source> <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> <target>Silenciar</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Mute all" xml:space="preserve"> + <source>Mute all</source> + <target>Silenciar todo</target> + <note>notification label action</note> </trans-unit> <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> @@ -3335,13 +5073,38 @@ This is your link for group %@!</source> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> <target>Nombre</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> <target>Servidores y Redes</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Network connection" xml:space="preserve"> + <source>Network connection</source> + <target>Conexión de red</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network decentralization" xml:space="preserve"> + <source>Network decentralization</source> + <target>Descentralización de la red</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve"> + <source>Network issues - message expired after many attempts to send it.</source> + <target>Problema en la red - el mensaje ha expirado tras muchos intentos de envío.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Network management" xml:space="preserve"> + <source>Network management</source> + <target>Gestión de la red</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network operator" xml:space="preserve"> + <source>Network operator</source> + <target>Operador de red</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> <target>Configuración de red</target> @@ -3352,11 +5115,36 @@ This is your link for group %@!</source> <target>Estado de la red</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New" xml:space="preserve"> + <source>New</source> + <target>Nuevo</target> + <note>token status text</note> + </trans-unit> <trans-unit id="New Passcode" xml:space="preserve"> <source>New Passcode</source> <target>Código Nuevo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve"> + <source>New SOCKS credentials will be used every time you start the app.</source> + <target>Se usarán credenciales SOCKS nuevas cada vez que inicies la aplicación.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve"> + <source>New SOCKS credentials will be used for each server.</source> + <target>Se usarán credenciales SOCKS nuevas para cada servidor.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat" xml:space="preserve"> + <source>New chat</source> + <target>Nuevo chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat experience 🎉" xml:space="preserve"> + <source>New chat experience 🎉</source> + <target>Nueva experiencia de chat 🎉</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> <target>Nueva solicitud de contacto</target> @@ -3367,11 +5155,6 @@ This is your link for group %@!</source> <target>Contacto nuevo:</target> <note>notification</note> </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> - <source>New database archive</source> - <target>Nuevo archivo de bases de datos</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="New desktop app!" xml:space="preserve"> <source>New desktop app!</source> <target>Nueva aplicación para PC!</target> @@ -3382,11 +5165,21 @@ This is your link for group %@!</source> <target>Nuevo nombre mostrado</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>Eventos nuevos</target> + <note>notification</note> + </trans-unit> <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> <target>Nuevo en %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New media options" xml:space="preserve"> + <source>New media options</source> + <target>Nuevas opciones multimedia</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> <target>Nuevo rol de miembro</target> @@ -3402,6 +5195,11 @@ This is your link for group %@!</source> <target>Contraseña nueva…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New server" xml:space="preserve"> + <source>New server</source> + <target>Servidor nuevo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No" xml:space="preserve"> <source>No</source> <target>No</target> @@ -3412,6 +5210,21 @@ This is your link for group %@!</source> <target>Sin contraseña de la aplicación</target> <note>Authentication unavailable</note> </trans-unit> + <trans-unit id="No chats" xml:space="preserve"> + <source>No chats</source> + <target>Sin chats</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats found" xml:space="preserve"> + <source>No chats found</source> + <target>Ningún chat encontrado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats in list %@" xml:space="preserve"> + <source>No chats in list %@</source> + <target>Sin chats en la lista %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> <target>Ningún contacto seleccionado</target> @@ -3432,6 +5245,11 @@ This is your link for group %@!</source> <target>¡Sin dispositivo token!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No direct connection yet, message is forwarded by admin." xml:space="preserve"> + <source>No direct connection yet, message is forwarded by admin.</source> + <target>Aún no hay conexión directa con este miembro, el mensaje es reenviado por el administrador.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="No filtered chats" xml:space="preserve"> <source>No filtered chats</source> <target>Sin chats filtrados</target> @@ -3447,20 +5265,111 @@ This is your link for group %@!</source> <target>Sin historial</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No info, try to reload" xml:space="preserve"> + <source>No info, try to reload</source> + <target>No hay información, intenta recargar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No media & file servers." xml:space="preserve"> + <source>No media & file servers.</source> + <target>Sin servidores para archivos y multimedia.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No message" xml:space="preserve"> + <source>No message</source> + <target>Ningún mensaje</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No message servers." xml:space="preserve"> + <source>No message servers.</source> + <target>Sin servidores para mensajes.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No network connection" xml:space="preserve"> + <source>No network connection</source> + <target>Sin conexión de red</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record speech" xml:space="preserve"> + <source>No permission to record speech</source> + <target>Sin permiso para grabación de voz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record video" xml:space="preserve"> + <source>No permission to record video</source> + <target>Sin permiso para grabación de vídeo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> <source>No permission to record voice message</source> <target>Sin permiso para grabar mensajes de voz</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No push server" xml:space="preserve"> + <source>No push server</source> + <target>Sin servidores push</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> <target>Sin archivos recibidos o enviados</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No servers for private message routing." xml:space="preserve"> + <source>No servers for private message routing.</source> + <target>Sin servidores para enrutamiento privado.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive files." xml:space="preserve"> + <source>No servers to receive files.</source> + <target>Sin servidores para recibir archivos.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive messages." xml:space="preserve"> + <source>No servers to receive messages.</source> + <target>Sin servidores para recibir mensajes.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to send files." xml:space="preserve"> + <source>No servers to send files.</source> + <target>Sin servidores para enviar archivos.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No token!" xml:space="preserve"> + <source>No token!</source> + <target>¡Sin token!</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="No unread chats" xml:space="preserve"> + <source>No unread chats</source> + <target>Ningún chat sin leer</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> + <target>Sin identificadores de usuario.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Not compatible!" xml:space="preserve"> <source>Not compatible!</source> + <target>¡No compatible!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notes" xml:space="preserve"> + <source>Notes</source> + <target>Notas</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing selected" xml:space="preserve"> + <source>Nothing selected</source> + <target>Nada seleccionado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing to forward!" xml:space="preserve"> + <source>Nothing to forward!</source> + <target>¡Nada para reenviar!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> <target>Notificaciones</target> @@ -3471,6 +5380,21 @@ This is your link for group %@!</source> <target>¡Las notificaciones están desactivadas!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notifications error" xml:space="preserve"> + <source>Notifications error</source> + <target>Error en notificaciones</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Notifications privacy" xml:space="preserve"> + <source>Notifications privacy</source> + <target>Privacidad en las notificaciones</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications status" xml:space="preserve"> + <source>Notifications status</source> + <target>Estado notificaciones</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. @@ -3480,44 +5404,43 @@ This is your link for group %@!</source> - desactivar el rol miembro (a rol "observador")</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="OK" xml:space="preserve"> + <source>OK</source> + <target>OK</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Off" xml:space="preserve"> <source>Off</source> <target>Desactivado</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> - <source>Off (Local)</source> - <target>Desactivado (Local)</target> - <note>No comment provided by engineer.</note> + <note>blur media</note> </trans-unit> <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> <target>Ok</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> <target>Base de datos antigua</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> - <source>Old database archive</source> - <target>Archivo de bases de datos antiguas</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> - <target>Enlace único de invitación de un uso</target> + <target>Enlace de invitación de un solo uso</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> - <target>Se requieren hosts .onion para la conexión. Requiere activación de la VPN.</target> + <trans-unit id="Onion hosts will be **required** for connection. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be **required** for connection. +Requires compatible VPN.</source> + <target>Se **requieren** hosts .onion para la conexión. +Requiere activación de la VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> - <target>Se usarán hosts .onion si están disponibles. Requiere activación de la VPN.</target> + <trans-unit id="Onion hosts will be used when available. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be used when available. +Requires compatible VPN.</source> + <target>Se usarán hosts .onion si están disponibles. +Requiere activación de la VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will not be used." xml:space="preserve"> @@ -3525,14 +5448,24 @@ This is your link for group %@!</source> <target>No se usarán hosts .onion.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only chat owners can change preferences." xml:space="preserve"> + <source>Only chat owners can change preferences.</source> + <target>Sólo los propietarios del chat pueden cambiar las preferencias.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <target>Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes enviados con **cifrado de extremo a extremo de 2 capas**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only delete conversation" xml:space="preserve"> + <source>Only delete conversation</source> + <target>Eliminar sólo la conversación</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> <source>Only group owners can change group preferences.</source> - <target>Sólo los propietarios pueden modificar las preferencias de grupo.</target> + <target>Sólo los propietarios pueden modificar las preferencias del grupo.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can enable files and media." xml:space="preserve"> @@ -3545,14 +5478,24 @@ This is your link for group %@!</source> <target>Sólo los propietarios del grupo pueden activar los mensajes de voz.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only sender and moderators see it" xml:space="preserve"> + <source>Only sender and moderators see it</source> + <target>Solo el remitente y el moderador pueden verlo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you and moderators see it" xml:space="preserve"> + <source>Only you and moderators see it</source> + <target>Solo tú y los moderadores podéis verlo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only you can add message reactions." xml:space="preserve"> <source>Only you can add message reactions.</source> <target>Sólo tú puedes añadir reacciones a los mensajes.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <target>Sólo tú puedes eliminar mensajes de forma irreversible (tu contacto puede marcarlos para eliminar).</target> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source> + <target>Sólo tú puedes eliminar mensajes de forma irreversible (tu contacto puede marcarlos para eliminar). (24 horas)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can make calls." xml:space="preserve"> @@ -3575,9 +5518,9 @@ This is your link for group %@!</source> <target>Sólo tu contacto puede añadir reacciones a los mensajes.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <target>Sólo tu contacto puede eliminar mensajes de forma irreversible (tu puedes marcarlos para eliminar).</target> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source> + <target>Sólo tu contacto puede eliminar mensajes de forma irreversible (tu puedes marcarlos para eliminar). (24 horas)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can make calls." xml:space="preserve"> @@ -3598,13 +5541,18 @@ This is your link for group %@!</source> <trans-unit id="Open" xml:space="preserve"> <source>Open</source> <target>Abrir</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Abrir Configuración</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open changes" xml:space="preserve"> + <source>Open changes</source> + <target>Abrir cambios</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> <target>Abrir chat</target> @@ -3615,25 +5563,87 @@ This is your link for group %@!</source> <target>Abrir consola de Chat</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Open conditions" xml:space="preserve"> + <source>Open conditions</source> + <target>Abrir condiciones</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open group" xml:space="preserve"> <source>Open group</source> + <target>Grupo abierto</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve"> - <source>Open user profiles</source> - <target>Abrir perfil de usuario</target> + <trans-unit id="Open link?" xml:space="preserve"> + <source>Open link?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Open migration to another device" xml:space="preserve"> + <source>Open migration to another device</source> + <target>Abrir menú migración a otro dispositivo</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> - <target>Protocolo y código abiertos: cualquiera puede usar los servidores.</target> + <trans-unit id="Opening app…" xml:space="preserve"> + <source>Opening app…</source> + <target>Iniciando aplicación…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve"> - <source>Opening database…</source> - <target>Abriendo base de datos…</target> + <trans-unit id="Operator" xml:space="preserve"> + <source>Operator</source> + <target>Operador</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Operator server" xml:space="preserve"> + <source>Operator server</source> + <target>Servidor del operador</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Or import archive file" xml:space="preserve"> + <source>Or import archive file</source> + <target>O importa desde un archivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or paste archive link" xml:space="preserve"> + <source>Or paste archive link</source> + <target>O pegar enlace del archivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or scan QR code" xml:space="preserve"> + <source>Or scan QR code</source> + <target>O escanea el código QR</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or securely share this file link" xml:space="preserve"> + <source>Or securely share this file link</source> + <target>O comparte de forma segura este enlace al archivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or show this code" xml:space="preserve"> + <source>Or show this code</source> + <target>O muestra el código QR</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or to share privately" xml:space="preserve"> + <source>Or to share privately</source> + <target>O para compartir en privado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Organize chats into lists" xml:space="preserve"> + <source>Organize chats into lists</source> + <target>Organiza tus chats en listas</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other" xml:space="preserve"> + <source>Other</source> + <target>Otro</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other file errors: %@" xml:space="preserve"> + <source>Other file errors: +%@</source> + <target>Otro(s) error(es) de archivo. +%@</target> + <note>alert message</note> + </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> <target>Contador PING</target> @@ -3669,18 +5679,24 @@ This is your link for group %@!</source> <target>¡Código de acceso guardado!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Password" xml:space="preserve"> + <source>Password</source> + <target>Contraseña</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> <target>Contraseña para hacerlo visible</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste" xml:space="preserve"> - <source>Paste</source> - <target>Pegar</target> - <note>No comment provided by engineer.</note> + <trans-unit id="Past member %@" xml:space="preserve"> + <source>Past member %@</source> + <target>Miembro pasado %@</target> + <note>past/unknown group member</note> </trans-unit> <trans-unit id="Paste desktop address" xml:space="preserve"> <source>Paste desktop address</source> + <target>Pegar dirección de ordenador</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Paste image" xml:space="preserve"> @@ -3688,24 +5704,24 @@ This is your link for group %@!</source> <target>Pegar imagen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve"> - <source>Paste received link</source> - <target>Pegar enlace recibido</target> + <trans-unit id="Paste link to connect!" xml:space="preserve"> + <source>Paste link to connect!</source> + <target>Pegar enlace para conectar!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> - <source>Paste the link you received to connect with your contact.</source> - <target>Pega el enlace que has recibido en el recuadro para conectar con tu contacto.</target> - <note>placeholder</note> - </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> - <target>Las personas pueden conectarse contigo solo mediante los enlaces que compartes.</target> + <trans-unit id="Paste the link you received" xml:space="preserve"> + <source>Paste the link you received</source> + <target>Pega el enlace recibido</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Periodically" xml:space="preserve"> - <source>Periodically</source> - <target>Periódico</target> + <trans-unit id="Pending" xml:space="preserve"> + <source>Pending</source> + <target>Pendientes</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Periodic" xml:space="preserve"> + <source>Periodic</source> + <target>Periódicamente</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Permanent decryption error" xml:space="preserve"> @@ -3713,11 +5729,33 @@ This is your link for group %@!</source> <target>Error permanente descifrado</target> <note>message decrypt error item</note> </trans-unit> + <trans-unit id="Picture-in-picture calls" xml:space="preserve"> + <source>Picture-in-picture calls</source> + <target>Llamadas picture-in-picture</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Play from the chat list." xml:space="preserve"> + <source>Play from the chat list.</source> + <target>Reproduce desde la lista de chats.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable calls." xml:space="preserve"> + <source>Please ask your contact to enable calls.</source> + <target>Por favor, pide a tu contacto que active las llamadas.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> <target>Solicita que tu contacto habilite el envío de mensajes de voz.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers." xml:space="preserve"> + <source>Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers.</source> + <target>Comprueba que el móvil y el ordenador están conectados a la misma red local y que el cortafuegos del ordenador permite la conexión. +Por favor, comparte cualquier otro problema con los desarrolladores.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> <target>Comprueba que has usado el enlace correcto o pide a tu contacto que te envíe otro.</target> @@ -3733,9 +5771,21 @@ This is your link for group %@!</source> <target>Comprueba tus preferencias y las de tu contacto.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve"> + <source>Please confirm that network settings are correct for this device.</source> + <target>Por favor, confirma que la configuración de red es correcta para este dispositivo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact developers. Error: %@" xml:space="preserve"> + <source>Please contact developers. +Error: %@</source> + <target>Por favor, contacta con los desarrolladores. +Error: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> - <target>Póngase en contacto con el administrador del grupo.</target> + <target>Ponte en contacto con el administrador del grupo.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please enter correct current passphrase." xml:space="preserve"> @@ -3773,14 +5823,34 @@ This is your link for group %@!</source> <target>Guarda la contraseña de forma segura, NO podrás cambiarla si la pierdes.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please try to disable and re-enable notfications." xml:space="preserve"> + <source>Please try to disable and re-enable notfications.</source> + <target>Por favor, intenta desactivar y reactivar las notificaciones.</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token activation to complete." xml:space="preserve"> + <source>Please wait for token activation to complete.</source> + <target>Por favor, espera a que el token de activación se complete.</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token to be registered." xml:space="preserve"> + <source>Please wait for token to be registered.</source> + <target>Por favor, espera a que el token se registre.</target> + <note>token info</note> + </trans-unit> <trans-unit id="Polish interface" xml:space="preserve"> <source>Polish interface</source> <target>Interfaz en polaco</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Port" xml:space="preserve"> + <source>Port</source> + <target>Puerto</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> - <target>Posiblemente la huella digital del certificado en la dirección del servidor es incorrecta</target> + <target>Posiblemente la huella del certificado en la dirección del servidor es incorrecta</target> <note>server test error</note> </trans-unit> <trans-unit id="Preserve the last message draft, with attachments." xml:space="preserve"> @@ -3788,14 +5858,14 @@ This is your link for group %@!</source> <target>Conserva el último borrador del mensaje con los datos adjuntos.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Preset server" xml:space="preserve"> - <source>Preset server</source> - <target>Servidor predefinido</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> - <target>Dirección del servidor predefinida</target> + <target>Dirección predefinida del servidor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Preset servers" xml:space="preserve"> + <source>Preset servers</source> + <target>Servidores predefinidos</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Preview" xml:space="preserve"> @@ -3803,9 +5873,24 @@ This is your link for group %@!</source> <target>Vista previa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Previously connected servers" xml:space="preserve"> + <source>Previously connected servers</source> + <target>Servidores conectados previamente</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> - <target>Privacidad y Seguridad</target> + <target>Seguridad y Privacidad</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy for your customers." xml:space="preserve"> + <source>Privacy for your customers.</source> + <target>Privacidad para tus clientes.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy policy and conditions of use." xml:space="preserve"> + <source>Privacy policy and conditions of use.</source> + <target>Política de privacidad y condiciones de uso.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Privacy redefined" xml:space="preserve"> @@ -3813,14 +5898,49 @@ This is your link for group %@!</source> <target>Privacidad redefinida</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve"> + <source>Private chats, groups and your contacts are not accessible to server operators.</source> + <target>Los chats privados, los grupos y tus contactos no son accesibles para los operadores de servidores.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> <target>Nombres de archivos privados</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private media file names." xml:space="preserve"> + <source>Private media file names.</source> + <target>Nombres privados en archivos de media.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing" xml:space="preserve"> + <source>Private message routing</source> + <target>Enrutamiento privado de mensajes</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing 🚀" xml:space="preserve"> + <source>Private message routing 🚀</source> + <target>Enrutamiento privado de mensajes 🚀</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private notes" xml:space="preserve"> + <source>Private notes</source> + <target>Notas privadas</target> + <note>name of notes to self</note> + </trans-unit> + <trans-unit id="Private routing" xml:space="preserve"> + <source>Private routing</source> + <target>Enrutamiento privado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private routing error" xml:space="preserve"> + <source>Private routing error</source> + <target>Error de enrutamiento privado</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> - <target>Perfil y conexiones de servidor</target> + <target>Eliminar perfil y conexiones</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile image" xml:space="preserve"> @@ -3828,12 +5948,9 @@ This is your link for group %@!</source> <target>Imagen del perfil</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile name" xml:space="preserve"> - <source>Profile name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile name:" xml:space="preserve"> - <source>Profile name:</source> + <trans-unit id="Profile images" xml:space="preserve"> + <source>Profile images</source> + <target>Forma de los perfiles</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile password" xml:space="preserve"> @@ -3841,10 +5958,15 @@ This is your link for group %@!</source> <target>Contraseña del perfil</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Profile theme" xml:space="preserve"> + <source>Profile theme</source> + <target>Tema del perfil</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> <source>Profile update will be sent to your contacts.</source> <target>La actualización del perfil se enviará a tus contactos.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> @@ -3866,6 +5988,16 @@ This is your link for group %@!</source> <target>No se permiten reacciones a los mensajes.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Prohibit reporting messages to moderators." xml:space="preserve"> + <source>Prohibit reporting messages to moderators.</source> + <target>No se permite informar de mensajes a los moderadores.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending SimpleX links." xml:space="preserve"> + <source>Prohibit sending SimpleX links.</source> + <target>No se permite enviar enlaces SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> <target>No se permiten mensajes directos entre miembros.</target> @@ -3886,9 +6018,21 @@ This is your link for group %@!</source> <target>No se permiten mensajes de voz.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect IP address" xml:space="preserve"> + <source>Protect IP address</source> + <target>Proteger dirección IP</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> - <target>Proteger la pantalla de la aplicación</target> + <target>Proteger la pantalla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve"> + <source>Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings.</source> + <target>Protege tu dirección IP de los servidores de retransmisión elegidos por tus contactos. +Actívalo en ajustes de *Servidores y Redes*.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> @@ -3898,17 +6042,42 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Protocol timeout" xml:space="preserve"> <source>Protocol timeout</source> - <target>Tiempo de espera del protocolo</target> + <target>Timeout protocolo</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Protocol timeout per KB" xml:space="preserve"> <source>Protocol timeout per KB</source> - <target>Límite de espera del protocolo por KB</target> + <target>Timeout protocolo por KB</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied" xml:space="preserve"> + <source>Proxied</source> + <target>Como proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied servers" xml:space="preserve"> + <source>Proxied servers</source> + <target>Servidores con proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxy requires password" xml:space="preserve"> + <source>Proxy requires password</source> + <target>El proxy requiere contraseña</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> - <target>Notificaciones automáticas</target> + <target>Notificaciones push</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Push server" xml:space="preserve"> + <source>Push server</source> + <target>Servidor push</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Quantum resistant encryption" xml:space="preserve"> + <source>Quantum resistant encryption</source> + <target>Cifrado resistente a tecnología cuántica</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> @@ -3916,6 +6085,11 @@ This is your link for group %@!</source> <target>Valora la aplicación</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reachable chat toolbar" xml:space="preserve"> + <source>Reachable chat toolbar</source> + <target>Barra de menú accesible</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> <target>Reacciona…</target> @@ -3924,31 +6098,31 @@ This is your link for group %@!</source> <trans-unit id="Read" xml:space="preserve"> <source>Read</source> <target>Leer</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Read more" xml:space="preserve"> <source>Read more</source> <target>Saber más</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> - <target>Más información en el [Manual de usuario](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source> + <target>Conoce más en la [Guía del Usuario](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</source> + <target>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).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> <source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source> - <target>Más información en el [Manual de usuario](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> - <source>Read more in our GitHub repository.</source> - <target>Más información en nuestro repositorio GitHub.</target> + <target>Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> - <target>Más información en nuestro [repositorio GitHub](https://github.com/simplex-chat/simplex-chat#readme).</target> + <target>Conoce más en nuestro [repositorio GitHub](https://github.com/simplex-chat/simplex-chat#readme).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Receipts are disabled" xml:space="preserve"> @@ -3956,6 +6130,11 @@ This is your link for group %@!</source> <target>Las confirmaciones están desactivadas</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receive errors" xml:space="preserve"> + <source>Receive errors</source> + <target>Errores de recepción</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Recibido a las</target> @@ -3976,6 +6155,21 @@ This is your link for group %@!</source> <target>Mensaje entrante</target> <note>message info title</note> </trans-unit> + <trans-unit id="Received messages" xml:space="preserve"> + <source>Received messages</source> + <target>Mensajes recibidos</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received reply" xml:space="preserve"> + <source>Received reply</source> + <target>Respuesta recibida</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received total" xml:space="preserve"> + <source>Received total</source> + <target>Total recibidos</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> <target>La dirección de recepción pasará a otro servidor. El cambio se completará cuando el remitente esté en línea.</target> @@ -3991,16 +6185,51 @@ This is your link for group %@!</source> <target>Recibiendo vía</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Recent history and improved [directory bot](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)." xml:space="preserve"> + <source>Recent history and improved [directory bot](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).</source> + <target>Historial reciente y [bot del directorio](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) mejorados.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve"> + <source>Recipient(s) can't see who this message is from.</source> + <target>Los destinatarios no ven de quién procede este mensaje.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> <target>Los destinatarios ven actualizarse mientras escribes.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect" xml:space="preserve"> + <source>Reconnect</source> + <target>Reconectar</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> <target>Reconectar todos los servidores conectados para forzar la entrega del mensaje. Se usa tráfico adicional.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect all servers" xml:space="preserve"> + <source>Reconnect all servers</source> + <target>Reconectar todos los servidores</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers?" xml:space="preserve"> + <source>Reconnect all servers?</source> + <target>¿Reconectar todos los servidores?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect server to force message delivery. It uses additional traffic.</source> + <target>Reconectar con el servidor para forzar la entrega de mensajes. Se usa tráfico adicional.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server?" xml:space="preserve"> + <source>Reconnect server?</source> + <target>¿Reconectar servidor?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> <target>¿Reconectar servidores?</target> @@ -4021,10 +6250,26 @@ This is your link for group %@!</source> <target>Reducción del uso de batería</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Register" xml:space="preserve"> + <source>Register</source> + <target>Registrar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Register notification token?" xml:space="preserve"> + <source>Register notification token?</source> + <target>¿Registrar el token de notificaciones?</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Registered" xml:space="preserve"> + <source>Registered</source> + <target>Registrado</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> <target>Rechazar</target> - <note>reject incoming call via notification</note> + <note>reject incoming call via notification +swipe action</note> </trans-unit> <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> <source>Reject (sender NOT notified)</source> @@ -4038,7 +6283,7 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Relay server is only used if necessary. Another party can observe your IP address." xml:space="preserve"> <source>Relay server is only used if necessary. Another party can observe your IP address.</source> - <target>El retransmisor sólo se usa en caso de necesidad. Un tercero podría ver tu IP.</target> + <target>El servidor de retransmisión sólo se usa en caso de necesidad. Un tercero podría ver tu IP.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Relay server protects your IP address, but it can observe the duration of the call." xml:space="preserve"> @@ -4051,6 +6296,16 @@ This is your link for group %@!</source> <target>Eliminar</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Remove archive?" xml:space="preserve"> + <source>Remove archive?</source> + <target>¿Eliminar archivo?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove image" xml:space="preserve"> + <source>Remove image</source> + <target>Eliminar imagen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> <target>Expulsar miembro</target> @@ -4083,10 +6338,27 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Repeat connection request?" xml:space="preserve"> <source>Repeat connection request?</source> + <target>¿Repetir solicitud de conexión?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat download" xml:space="preserve"> + <source>Repeat download</source> + <target>Repetir descarga</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat import" xml:space="preserve"> + <source>Repeat import</source> + <target>Repetir importación</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Repeat join request?" xml:space="preserve"> <source>Repeat join request?</source> + <target>¿Repetir solicitud de admisión?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat upload" xml:space="preserve"> + <source>Repeat upload</source> + <target>Repetir subida</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reply" xml:space="preserve"> @@ -4094,6 +6366,56 @@ This is your link for group %@!</source> <target>Responder</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Report" xml:space="preserve"> + <source>Report</source> + <target>Informe</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Report content: only group moderators will see it." xml:space="preserve"> + <source>Report content: only group moderators will see it.</source> + <target>Informar de contenido: sólo los moderadores del grupo lo verán.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report member profile: only group moderators will see it." xml:space="preserve"> + <source>Report member profile: only group moderators will see it.</source> + <target>Informar del perfil de un miembro: sólo los moderadores del grupo lo verán.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report other: only group moderators will see it." xml:space="preserve"> + <source>Report other: only group moderators will see it.</source> + <target>Informar de otros: sólo los moderadores del grupo lo verán.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report reason?" xml:space="preserve"> + <source>Report reason?</source> + <target>¿Motivo del informe?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Report spam: only group moderators will see it." xml:space="preserve"> + <source>Report spam: only group moderators will see it.</source> + <target>Informar de spam: sólo los moderadores del grupo lo verán.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report violation: only group moderators will see it." xml:space="preserve"> + <source>Report violation: only group moderators will see it.</source> + <target>Informar de violación: sólo los moderadores del grupo lo verán.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report: %@" xml:space="preserve"> + <source>Report: %@</source> + <target>Informe: %@</target> + <note>report in notification</note> + </trans-unit> + <trans-unit id="Reporting messages to moderators is prohibited." xml:space="preserve"> + <source>Reporting messages to moderators is prohibited.</source> + <target>No se permite informar de mensajes a los moderadores.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reports" xml:space="preserve"> + <source>Reports</source> + <target>Informes</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Required" xml:space="preserve"> <source>Required</source> <target>Obligatorio</target> @@ -4104,14 +6426,39 @@ This is your link for group %@!</source> <target>Restablecer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset all hints" xml:space="preserve"> + <source>Reset all hints</source> + <target>Reiniciar todas las pistas</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics" xml:space="preserve"> + <source>Reset all statistics</source> + <target>Restablecer todas las estadísticas</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics?" xml:space="preserve"> + <source>Reset all statistics?</source> + <target>¿Restablecer todas las estadísticas?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> <target>Restablecer colores</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to app theme" xml:space="preserve"> + <source>Reset to app theme</source> + <target>Restablecer al tema de la aplicación</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> - <target>Restablecer valores por defecto</target> + <target>Restablecer valores predetarminados</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset to user theme" xml:space="preserve"> + <source>Reset to user theme</source> + <target>Restablecer al tema del usuario</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> @@ -4144,14 +6491,19 @@ This is your link for group %@!</source> <target>Error al restaurar base de datos</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Retry" xml:space="preserve"> + <source>Retry</source> + <target>Reintentar</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> <target>Revelar</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Revert" xml:space="preserve"> - <source>Revert</source> - <target>Revertir</target> + <trans-unit id="Review conditions" xml:space="preserve"> + <source>Review conditions</source> + <target>Revisar condiciones</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke" xml:space="preserve"> @@ -4176,47 +6528,58 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Run chat" xml:space="preserve"> <source>Run chat</source> - <target>Ejecutar chat</target> + <target>Ejecutar SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve"> - <source>SMP servers</source> - <target>Servidores SMP</target> + <trans-unit id="SMP server" xml:space="preserve"> + <source>SMP server</source> + <target>Servidor SMP</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SOCKS proxy" xml:space="preserve"> + <source>SOCKS proxy</source> + <target>Proxy SOCKS</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safely receive files" xml:space="preserve"> + <source>Safely receive files</source> + <target>Recibe archivos de forma segura</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safer groups" xml:space="preserve"> + <source>Safer groups</source> + <target>Grupos más seguros</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save" xml:space="preserve"> <source>Save</source> <target>Guardar</target> - <note>chat item action</note> + <note>alert button +chat item action</note> </trans-unit> <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> <target>Guardar (y notificar contactos)</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> <target>Guardar y notificar contacto</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> - <target>Guardar y notificar a los miembros del grupo</target> + <target>Guardar y notificar grupo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save and reconnect" xml:space="preserve"> + <source>Save and reconnect</source> + <target>Guardar y reconectar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save and update group profile" xml:space="preserve"> <source>Save and update group profile</source> - <target>Guardar y actualizar perfil de grupo</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save archive" xml:space="preserve"> - <source>Save archive</source> - <target>Guardar archivo</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve"> - <source>Save auto-accept settings</source> - <target>Guardar configuración de auto aceptar</target> + <target>Guardar y actualizar perfil del grupo</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> @@ -4224,6 +6587,11 @@ This is your link for group %@!</source> <target>Guardar perfil de grupo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save list" xml:space="preserve"> + <source>Save list</source> + <target>Guardar lista</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> <target>Guardar contraseña y abrir el chat</target> @@ -4237,7 +6605,7 @@ This is your link for group %@!</source> <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> <target>¿Guardar preferencias?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> @@ -4252,23 +6620,53 @@ This is your link for group %@!</source> <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> <target>¿Guardar servidores?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve"> - <source>Save settings?</source> - <target>¿Guardar configuración?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> <target>¿Guardar mensaje de bienvenida?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save your profile?" xml:space="preserve"> + <source>Save your profile?</source> + <target>¿Guardar tu perfil?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Saved" xml:space="preserve"> + <source>Saved</source> + <target>Guardado</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> <source>Saved WebRTC ICE servers will be removed</source> <target>Los servidores WebRTC ICE guardados serán eliminados</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Saved from" xml:space="preserve"> + <source>Saved from</source> + <target>Guardado desde</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved message" xml:space="preserve"> + <source>Saved message</source> + <target>Mensaje guardado</target> + <note>message info title</note> + </trans-unit> + <trans-unit id="Saving %lld messages" xml:space="preserve"> + <source>Saving %lld messages</source> + <target>Guardando %lld mensajes</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scale" xml:space="preserve"> + <source>Scale</source> + <target>Escala</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan / Paste link" xml:space="preserve"> + <source>Scan / Paste link</source> + <target>Escanear / Pegar enlace</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> <target>Escanear código QR</target> @@ -4276,6 +6674,7 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Scan QR code from desktop" xml:space="preserve"> <source>Scan QR code from desktop</source> + <target>Escanear código QR desde ordenador</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Scan code" xml:space="preserve"> @@ -4290,7 +6689,7 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Scan server QR code" xml:space="preserve"> <source>Scan server QR code</source> - <target>Escanear código QR del servidor</target> + <target>Escanear código QR</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Search" xml:space="preserve"> @@ -4298,11 +6697,31 @@ This is your link for group %@!</source> <target>Buscar</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Search bar accepts invitation links." xml:space="preserve"> + <source>Search bar accepts invitation links.</source> + <target>La barra de búsqueda acepta enlaces de invitación.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search or paste SimpleX link" xml:space="preserve"> + <source>Search or paste SimpleX link</source> + <target>Buscar o pegar enlace SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secondary" xml:space="preserve"> + <source>Secondary</source> + <target>Secundario</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> <source>Secure queue</source> <target>Cola segura</target> <note>server test step</note> </trans-unit> + <trans-unit id="Secured" xml:space="preserve"> + <source>Secured</source> + <target>Aseguradas</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> <target>Evaluación de la seguridad</target> @@ -4316,6 +6735,21 @@ This is your link for group %@!</source> <trans-unit id="Select" xml:space="preserve"> <source>Select</source> <target>Seleccionar</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Select chat profile" xml:space="preserve"> + <source>Select chat profile</source> + <target>Selecciona perfil de chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected %lld" xml:space="preserve"> + <source>Selected %lld</source> + <target>Seleccionados %lld</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Las preferencias seleccionadas no permiten este mensaje.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct" xml:space="preserve"> @@ -4345,7 +6779,7 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Send a live message - it will update for the recipient(s) as you type it" xml:space="preserve"> <source>Send a live message - it will update for the recipient(s) as you type it</source> - <target>Envía un mensaje en vivo: se actualizará para el(los) destinatario(s) a medida que se escribe</target> + <target>Envía un mensaje en vivo: se actualizará para el (los) destinatario(s) a medida que se escribe</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send delivery receipts to" xml:space="preserve"> @@ -4353,14 +6787,9 @@ This is your link for group %@!</source> <target>Enviar confirmaciones de entrega a</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve"> - <source>Send direct message</source> - <target>Enviar mensaje directo</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Send direct message to connect" xml:space="preserve"> <source>Send direct message to connect</source> - <target>Enviar mensaje directo para conectar</target> + <target>Envía un mensaje para conectar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send disappearing message" xml:space="preserve"> @@ -4368,6 +6797,11 @@ This is your link for group %@!</source> <target>Enviar mensaje temporal</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send errors" xml:space="preserve"> + <source>Send errors</source> + <target>Errores de envío</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> <target>Enviar previsualizacion de enlaces</target> @@ -4378,14 +6812,29 @@ This is your link for group %@!</source> <target>Mensaje en vivo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send message to enable calls." xml:space="preserve"> + <source>Send message to enable calls.</source> + <target>Enviar mensaje para activar llamadas.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source> + <target>Enviar mensajes directamente cuando tu dirección IP está protegida y tu servidor o el de destino no admitan enrutamiento privado.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when your or destination server does not support private routing.</source> + <target>Enviar mensajes directamente cuando tu servidor o el de destino no admitan enrutamiento privado.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> <target>Enviar notificaciones</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> - <source>Send notifications:</source> - <target>Enviar notificaciones:</target> + <trans-unit id="Send private reports" xml:space="preserve"> + <source>Send private reports</source> + <target>Envía informes privados</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve"> @@ -4403,10 +6852,15 @@ This is your link for group %@!</source> <target>Envíalos desde la galería o desde teclados personalizados.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve"> + <source>Send up to 100 last messages to new members.</source> + <target>Se envían hasta 100 mensajes más recientes a los miembros nuevos.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> <target>El remitente ha cancelado la transferencia de archivos.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> @@ -4463,6 +6917,11 @@ This is your link for group %@!</source> <target>Enviado: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Sent directly" xml:space="preserve"> + <source>Sent directly</source> + <target>Directamente</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> <target>Evento de archivo enviado</target> @@ -4473,11 +6932,71 @@ This is your link for group %@!</source> <target>Mensaje saliente</target> <note>message info title</note> </trans-unit> + <trans-unit id="Sent messages" xml:space="preserve"> + <source>Sent messages</source> + <target>Mensajes enviados</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> <target>Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sent reply" xml:space="preserve"> + <source>Sent reply</source> + <target>Respuesta enviada</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent total" xml:space="preserve"> + <source>Sent total</source> + <target>Total enviados</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent via proxy" xml:space="preserve"> + <source>Sent via proxy</source> + <target>Mediante proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server" xml:space="preserve"> + <source>Server</source> + <target>Servidor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server added to operator %@." xml:space="preserve"> + <source>Server added to operator %@.</source> + <target>Servidor añadido al operador %@.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Server address" xml:space="preserve"> + <source>Server address</source> + <target>Dirección del servidor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings." xml:space="preserve"> + <source>Server address is incompatible with network settings.</source> + <target>La dirección del servidor es incompatible con la configuración de la red.</target> + <note>srv error text.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Server address is incompatible with network settings: %@.</source> + <target>La dirección del servidor es incompatible con la configuración de la red: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server operator changed." xml:space="preserve"> + <source>Server operator changed.</source> + <target>El operador del servidor ha cambiado.</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Server operators" xml:space="preserve"> + <source>Server operators</source> + <target>Operadores de servidores</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server protocol changed." xml:space="preserve"> + <source>Server protocol changed.</source> + <target>El protocolo del servidor ha cambiado.</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> <target>El servidor requiere autorización para crear colas, comprueba la contraseña</target> @@ -4490,7 +7009,22 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Server test failed!" xml:space="preserve"> <source>Server test failed!</source> - <target>¡Error en prueba del servidor!</target> + <target>¡Prueba no superada!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server type" xml:space="preserve"> + <source>Server type</source> + <target>Tipo de servidor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server version is incompatible with network settings." xml:space="preserve"> + <source>Server version is incompatible with network settings.</source> + <target>La versión del servidor es incompatible con la configuración de red.</target> + <note>srv error text</note> + </trans-unit> + <trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve"> + <source>Server version is incompatible with your app: %@.</source> + <target>La versión del servidor es incompatible con tu aplicación: %@.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Servers" xml:space="preserve"> @@ -4498,8 +7032,19 @@ This is your link for group %@!</source> <target>Servidores</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Servers info" xml:space="preserve"> + <source>Servers info</source> + <target>Info servidores</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <target>Las estadísticas de los servidores serán restablecidas. ¡No puede deshacerse!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Session code" xml:space="preserve"> <source>Session code</source> + <target>Código de sesión</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set 1 day" xml:space="preserve"> @@ -4507,11 +7052,21 @@ This is your link for group %@!</source> <target>Establecer 1 día</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set chat name…" xml:space="preserve"> + <source>Set chat name…</source> + <target>Nombre para el chat…</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> <target>Escribe el nombre del contacto…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve"> + <source>Set default theme</source> + <target>Establecer tema predeterminado</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> <target>Establecer preferencias de grupo</target> @@ -4522,11 +7077,21 @@ This is your link for group %@!</source> <target>Úsalo en lugar de la autenticación del sistema.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve"> + <source>Set message expiration in chats.</source> + <target>Establece el vencimiento para los mensajes en los chats.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passcode" xml:space="preserve"> <source>Set passcode</source> <target>Código autodestrucción</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set passphrase" xml:space="preserve"> + <source>Set passphrase</source> + <target>Definir frase de contraseña</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> <target>Escribe la contraseña para exportar</target> @@ -4547,24 +7112,55 @@ This is your link for group %@!</source> <target>Configuración</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve"> + <source>Settings were changed.</source> + <target>La configuración ha sido modificada.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve"> + <source>Shape profile images</source> + <target>Dar forma a las imágenes de perfil</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share" xml:space="preserve"> <source>Share</source> <target>Compartir</target> - <note>chat item action</note> + <note>alert action +chat item action</note> </trans-unit> <trans-unit id="Share 1-time link" xml:space="preserve"> <source>Share 1-time link</source> <target>Compartir enlace de un uso</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share 1-time link with a friend" xml:space="preserve"> + <source>Share 1-time link with a friend</source> + <target>Compartir enlace de un uso con un amigo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve"> + <source>Share SimpleX address on social media.</source> + <target>Comparte tu dirección SimpleX en redes sociales.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address" xml:space="preserve"> <source>Share address</source> <target>Compartir dirección</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share address publicly" xml:space="preserve"> + <source>Share address publicly</source> + <target>Campartir dirección públicamente</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address with contacts?" xml:space="preserve"> <source>Share address with contacts?</source> <target>¿Compartir la dirección con los contactos?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Share from other apps." xml:space="preserve"> + <source>Share from other apps.</source> + <target>Comparte desde otras aplicaciones.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share link" xml:space="preserve"> @@ -4572,9 +7168,19 @@ This is your link for group %@!</source> <target>Compartir enlace</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve"> - <source>Share one-time invitation link</source> - <target>Compartir enlace de invitación de un uso</target> + <trans-unit id="Share profile" xml:space="preserve"> + <source>Share profile</source> + <target>Perfil a compartir</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share this 1-time invite link" xml:space="preserve"> + <source>Share this 1-time invite link</source> + <target>Comparte este enlace de un solo uso</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share to SimpleX" xml:space="preserve"> + <source>Share to SimpleX</source> + <target>Compartir con Simplex</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share with contacts" xml:space="preserve"> @@ -4582,6 +7188,16 @@ This is your link for group %@!</source> <target>Compartir con contactos</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Short link" xml:space="preserve"> + <source>Short link</source> + <target>Enlace corto</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show QR code" xml:space="preserve"> + <source>Show QR code</source> + <target>Mostrar código QR</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> <target>Mostrar llamadas en el historial del teléfono</target> @@ -4597,21 +7213,46 @@ This is your link for group %@!</source> <target>Mostrar último mensaje</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show message status" xml:space="preserve"> + <source>Show message status</source> + <target>Estado del mensaje</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show percentage" xml:space="preserve"> + <source>Show percentage</source> + <target>Mostrar porcentajes</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Mostrar vista previa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show → on messages sent via private routing." xml:space="preserve"> + <source>Show → on messages sent via private routing.</source> + <target>Mostrar → en mensajes con enrutamiento privado.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show:" xml:space="preserve"> <source>Show:</source> <target>Mostrar:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve"> + <source>SimpleX</source> + <target>SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Address" xml:space="preserve"> <source>SimpleX Address</source> <target>Dirección SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." xml:space="preserve"> + <source>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</source> + <target>Simplex Chat y Flux han acordado incluir en la aplicación servidores operados por Flux.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> <target>La seguridad de SimpleX Chat ha sido auditada por Trail of Bits.</target> @@ -4642,6 +7283,21 @@ This is your link for group %@!</source> <target>Dirección SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX address and 1-time links are safe to share via any messenger." xml:space="preserve"> + <source>SimpleX address and 1-time links are safe to share via any messenger.</source> + <target>Compartir enlaces de un solo uso y direcciones SimpleX es seguro a través de cualquier medio.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address or 1-time link?" xml:space="preserve"> + <source>SimpleX address or 1-time link?</source> + <target>¿Dirección SimpleX o enlace de un uso?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX channel link" xml:space="preserve"> + <source>SimpleX channel link</source> + <target>Enlace de canal SimpleX</target> + <note>simplex link type</note> + </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> <target>Dirección de contacto SimpleX</target> @@ -4660,6 +7316,16 @@ This is your link for group %@!</source> <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> <target>Enlaces SimpleX</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve"> + <source>SimpleX links are prohibited.</source> + <target>Los enlaces SimpleX no se permiten en este grupo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve"> + <source>SimpleX links not allowed</source> + <target>Enlaces SimpleX no permitidos</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> @@ -4667,11 +7333,21 @@ This is your link for group %@!</source> <target>Invitación SimpleX de un uso</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> + <source>SimpleX protocols reviewed by Trail of Bits.</source> + <target>Protocolos de SimpleX auditados por Trail of Bits.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Simplified incognito mode" xml:space="preserve"> <source>Simplified incognito mode</source> <target>Modo incógnito simplificado</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Size" xml:space="preserve"> + <source>Size</source> + <target>Tamaño</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Omitir</target> @@ -4684,7 +7360,22 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Small groups (max 20)" xml:space="preserve"> <source>Small groups (max 20)</source> - <target>Grupos pequeños (máx. 20)</target> + <target>Grupos pequeños (max. 20)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Soft" xml:space="preserve"> + <source>Soft</source> + <target>Suave</target> + <note>blur media</note> + </trans-unit> + <trans-unit id="Some app settings were not migrated." xml:space="preserve"> + <source>Some app settings were not migrated.</source> + <target>Algunas configuraciones de la app no han sido migradas.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some file(s) were not exported:" xml:space="preserve"> + <source>Some file(s) were not exported:</source> + <target>Algunos archivos no han sido exportados:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> @@ -4692,14 +7383,32 @@ This is your link for group %@!</source> <target>Algunos errores no críticos ocurrieron durante la importación - para más detalles puedes ver la consola de Chat.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import:" xml:space="preserve"> + <source>Some non-fatal errors occurred during import:</source> + <target>Han ocurrido algunos errores no críticos durante la importación:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some servers failed the test: %@" xml:space="preserve"> + <source>Some servers failed the test: +%@</source> + <target>Algunos servidores no han superado la prueba: +%@</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> <target>Alguien</target> <note>notification title</note> </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve"> - <source>Start a new chat</source> - <target>Iniciar chat nuevo</target> + <trans-unit id="Spam" xml:space="preserve"> + <source>Spam</source> + <target>Spam</target> + <note>blocking reason +report reason</note> + </trans-unit> + <trans-unit id="Square, circle, or anything in between." xml:space="preserve"> + <source>Square, circle, or anything in between.</source> + <target>Cuadrada, circular o cualquier forma intermedia.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start chat" xml:space="preserve"> @@ -4707,34 +7416,49 @@ This is your link for group %@!</source> <target>Iniciar chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Start chat?" xml:space="preserve"> + <source>Start chat?</source> + <target>¿Iniciar chat?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> <target>Iniciar migración</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Starting from %@." xml:space="preserve"> + <source>Starting from %@.</source> + <target>Iniciado el %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Statistics" xml:space="preserve"> + <source>Statistics</source> + <target>Estadísticas</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> - <target>Detener</target> + <target>Parar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop SimpleX" xml:space="preserve"> <source>Stop SimpleX</source> - <target>Detener SimpleX</target> + <target>Parar SimpleX</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> - <source>Stop chat to enable database actions</source> - <target>Detén SimpleX para habilitar las acciones sobre la base de datos</target> + <trans-unit id="Stop chat" xml:space="preserve"> + <source>Stop chat</source> + <target>Parar SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> <source>Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped.</source> - <target>Para poder exportar, importar o eliminar la base de datos primero debes detener Chat. Durante el tiempo que esté detenido no podrás recibir ni enviar mensajes.</target> + <target>Para exportar, importar o eliminar la base de datos debes parar SimpleX. Mientra tanto no podrás enviar ni recibir mensajes.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat?" xml:space="preserve"> <source>Stop chat?</source> - <target>¿Detener Chat?</target> + <target>¿Parar Chat?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop file" xml:space="preserve"> @@ -4755,23 +7479,63 @@ This is your link for group %@!</source> <trans-unit id="Stop sharing" xml:space="preserve"> <source>Stop sharing</source> <target>Dejar de compartir</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Stop sharing address?" xml:space="preserve"> <source>Stop sharing address?</source> <target>¿Dejar de compartir la dirección?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Stopping chat" xml:space="preserve"> + <source>Stopping chat</source> + <target>Parando chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Storage" xml:space="preserve"> + <source>Storage</source> + <target>Almacenamiento</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Strong" xml:space="preserve"> + <source>Strong</source> + <target>Fuerte</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Submit" xml:space="preserve"> <source>Submit</source> <target>Enviar</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Subscribed" xml:space="preserve"> + <source>Subscribed</source> + <target>Suscritas</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscription errors" xml:space="preserve"> + <source>Subscription errors</source> + <target>Errores de suscripción</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscriptions ignored" xml:space="preserve"> + <source>Subscriptions ignored</source> + <target>Suscripciones ignoradas</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Support SimpleX Chat" xml:space="preserve"> <source>Support SimpleX Chat</source> <target>Soporte SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Switch audio and video during the call." xml:space="preserve"> + <source>Switch audio and video during the call.</source> + <target>Intercambia audio y video durante la llamada.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> + <source>Switch chat profile for 1-time invitations.</source> + <target>Cambia el perfil de chat para invitaciones de un solo uso.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="System" xml:space="preserve"> <source>System</source> <target>Sistema</target> @@ -4782,9 +7546,19 @@ This is your link for group %@!</source> <target>Autenticación del sistema</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP connection" xml:space="preserve"> + <source>TCP connection</source> + <target>Conexión TCP</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> - <target>Tiempo de espera de la conexión TCP agotado</target> + <target>Timeout de la conexión TCP</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="TCP port for messaging" xml:space="preserve"> + <source>TCP port for messaging</source> + <target>Puerto TCP para mensajes</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> @@ -4802,11 +7576,21 @@ This is your link for group %@!</source> <target>TCP_KEEPINTVL</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tail" xml:space="preserve"> + <source>Tail</source> + <target>Cola</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> <target>Tomar foto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap Create SimpleX address in the menu to create it later." xml:space="preserve"> + <source>Tap Create SimpleX address in the menu to create it later.</source> + <target>Pulsa Crear dirección SimpleX en el menú para crearla más tarde.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> <target>Pulsa el botón </target> @@ -4814,6 +7598,7 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Tap to Connect" xml:space="preserve"> <source>Tap to Connect</source> + <target>Pulsa para conectar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to activate profile." xml:space="preserve"> @@ -4823,7 +7608,7 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Tap to join" xml:space="preserve"> <source>Tap to join</source> - <target>Pulsa para unirse</target> + <target>Pulsa para unirte</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to join incognito" xml:space="preserve"> @@ -4831,16 +7616,31 @@ This is your link for group %@!</source> <target>Pulsa para unirte en modo incógnito</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve"> - <source>Tap to start a new chat</source> - <target>Pulsa para iniciar chat nuevo</target> + <trans-unit id="Tap to paste link" xml:space="preserve"> + <source>Tap to paste link</source> + <target>Pulsa para pegar el enlacePulsa para pegar enlace</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap to scan" xml:space="preserve"> + <source>Tap to scan</source> + <target>Pulsa para escanear</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Temporary file error" xml:space="preserve"> + <source>Temporary file error</source> + <target>Error en archivo temporal</target> + <note>file error alert title</note> + </trans-unit> <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> - <target>La prueba ha fallado en el paso %@.</target> + <target>Prueba no superada en el paso %@.</target> <note>server test failure</note> </trans-unit> + <trans-unit id="Test notifications" xml:space="preserve"> + <source>Test notifications</source> + <target>Probar notificaciones</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> <target>Probar servidor</target> @@ -4853,8 +7653,8 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> - <target>¡Pruebas fallidas!</target> - <note>No comment provided by engineer.</note> + <target>¡Pruebas no superadas!</target> + <note>alert title</note> </trans-unit> <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> @@ -4863,17 +7663,12 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> <source>Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> - <target>Gracias a los usuarios: [contribuye vía Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#traducir-el-aplicaciones)!</target> + <target>¡Nuestro agradecimiento a todos los colaboradores, [puedes contribuir a través de Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#traducir-el-aplicaciones)!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Thanks to the users – contribute via Weblate!" xml:space="preserve"> <source>Thanks to the users – contribute via Weblate!</source> - <target>¡Gracias a los colaboradores! Contribuye a través de Weblate.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> - <target>La primera plataforma sin identificadores de usuario: diseñada para la privacidad.</target> + <target>¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> @@ -4885,7 +7680,17 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target> </trans-unit> <trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve"> <source>The app can notify you when you receive messages or contact requests - please open settings to enable.</source> - <target>La aplicación puede notificarte cuando recibas mensajes o solicitudes de contacto: abre la configuración para habilitar.</target> + <target>La aplicación puede notificarte cuando recibas mensajes o solicitudes de contacto: por favor, abre la configuración para activarlo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app protects your privacy by using different operators in each conversation." xml:space="preserve"> + <source>The app protects your privacy by using different operators in each conversation.</source> + <target>La aplicación protege tu privacidad mediante el uso de diferentes operadores en cada conversación.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve"> + <source>The app will ask to confirm downloads from unknown file servers (except .onion).</source> + <target>La aplicación pedirá que confirmes las descargas desde servidores de archivos desconocidos (excepto si son .onion).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> @@ -4893,6 +7698,16 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target> <target>El intento de cambiar la contraseña de la base de datos no se ha completado.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve"> + <source>The code you scanned is not a SimpleX link QR code.</source> + <target>El código QR escaneado no es un enlace de SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The connection reached the limit of undelivered messages, your contact may be offline." xml:space="preserve"> + <source>The connection reached the limit of undelivered messages, your contact may be offline.</source> + <target>La conexión ha alcanzado el límite de mensajes no entregados. es posible que tu contacto esté desconectado.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> <target>¡La conexión que has aceptado se cancelará!</target> @@ -4913,6 +7728,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target> <target>El cifrado funciona y un cifrado nuevo no es necesario. ¡Podría dar lugar a errores de conexión!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> + <target>La nueva generación de mensajería privada</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The hash of the previous message is different." xml:space="preserve"> <source>The hash of the previous message is different.</source> <target>El hash del mensaje anterior es diferente.</target> @@ -4928,9 +7748,14 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target> <target>El mensaje será marcado como moderado para todos los miembros.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> - <target>La nueva generación de mensajería privada</target> + <trans-unit id="The messages will be deleted for all members." xml:space="preserve"> + <source>The messages will be deleted for all members.</source> + <target>Los mensajes serán eliminados para todos los miembros.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve"> + <source>The messages will be marked as moderated for all members.</source> + <target>Los mensajes serán marcados como moderados para todos los miembros.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> @@ -4938,9 +7763,14 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target> <target>La base de datos antigua no se eliminó durante la migración, puede eliminarse.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> - <target>El perfil sólo se comparte con tus contactos.</target> + <trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve"> + <source>The same conditions will apply to operator **%@**.</source> + <target>Las mismas condiciones se aplicarán al operador **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second preset operator in the app!" xml:space="preserve"> + <source>The second preset operator in the app!</source> + <target>¡Segundo operador predefinido!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> @@ -4955,22 +7785,32 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target> </trans-unit> <trans-unit id="The servers for new connections of your current chat profile **%@**." xml:space="preserve"> <source>The servers for new connections of your current chat profile **%@**.</source> - <target>Lista de servidores para las conexiones nuevas de tu perfil actual **%@**.</target> + <target>Servidores para conexiones nuevas en tu perfil **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Theme" xml:space="preserve"> - <source>Theme</source> - <target>Tema</target> + <trans-unit id="The servers for new files of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new files of your current chat profile **%@**.</source> + <target>Servidores para enviar archivos en tu perfil **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve"> - <source>There should be at least one user profile.</source> - <target>Debe haber al menos un perfil.</target> + <trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve"> + <source>The text you pasted is not a SimpleX link.</source> + <target>El texto pegado no es un enlace de SimpleX.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> - <source>There should be at least one visible user profile.</source> - <target>Debe haber al menos un perfil visible.</target> + <trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve"> + <source>The uploaded database archive will be permanently removed from the servers.</source> + <target>El archivo de bases de datos subido será eliminado permanentemente de los servidores.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Themes" xml:space="preserve"> + <source>Themes</source> + <target>Temas</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="These conditions will also apply for: **%@**." xml:space="preserve"> + <source>These conditions will also apply for: **%@**.</source> + <target>Estas condiciones también se aplican para: **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> @@ -4980,26 +7820,47 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target> </trans-unit> <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve"> <source>They can be overridden in contact and group settings.</source> - <target>Se pueden anular en la configuración de contactos.</target> + <target>Se puede modificar desde la configuración particular de cada grupo y contacto.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> <source>This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</source> - <target>Esta acción no se puede deshacer. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán.</target> + <target>Esta acción es irreversible. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." xml:space="preserve"> <source>This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</source> - <target>Esta acción no se puede deshacer. Se eliminarán los mensajes enviados y recibidos anteriores a la selección. Puede tardar varios minutos.</target> + <target>Esta acción es irreversible. Se eliminarán los mensajes enviados y recibidos anteriores a la selección. Podría tardar varios minutos.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</source> + <target>Todos los mensajes previos al período seleccionado serán eliminados del chat. ¡No puede deshacerse!</target> + <note>alert message</note> + </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> - <target>Esta acción no se puede deshacer. Tu perfil, contactos, mensajes y archivos se perderán irreversiblemente.</target> + <target>Esta acción es irreversible. Tu perfil, contactos, mensajes y archivos se perderán irreversiblemente.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by end-to-end encryption.</source> + <target>Este chat está protegido por cifrado de extremo a extremo.</target> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by quantum resistant end-to-end encryption.</source> + <target>Este chat está protegido por cifrado de extremo a extremo resistente a tecnología cuántica.</target> + <note>E2EE info chat item</note> + </trans-unit> <trans-unit id="This device name" xml:space="preserve"> <source>This device name</source> + <target>Nombre del dispositivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve"> + <source>This display name is invalid. Please choose another name.</source> + <target>Éste nombre mostrado no es válido. Por favor, elije otro nombre.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> @@ -5014,10 +7875,27 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target> </trans-unit> <trans-unit id="This is your own SimpleX address!" xml:space="preserve"> <source>This is your own SimpleX address!</source> + <target>¡Esta es tu propia dirección SimpleX!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This is your own one-time link!" xml:space="preserve"> <source>This is your own one-time link!</source> + <target>¡Este es tu propio enlace de un solo uso!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve"> + <source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source> + <target>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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve"> + <source>This link was used with another mobile device, please create a new link on the desktop.</source> + <target>Este enlace ha sido usado en otro dispositivo móvil, por favor crea un enlace nuevo en el ordenador.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This message was deleted or not received yet." xml:space="preserve"> + <source>This message was deleted or not received yet.</source> + <target>El mensaje ha sido eliminado o aún no se ha recibido.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> @@ -5025,6 +7903,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target> <target>Esta configuración se aplica a los mensajes del perfil actual **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Title" xml:space="preserve"> + <source>Title</source> + <target>Título</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> <source>To ask any questions and to receive updates:</source> <target>Para consultar cualquier duda y recibir actualizaciones:</target> @@ -5037,6 +7920,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target> </trans-unit> <trans-unit id="To hide unwanted messages." xml:space="preserve"> <source>To hide unwanted messages.</source> + <target>Para ocultar mensajes no deseados.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To make a new connection" xml:space="preserve"> @@ -5044,9 +7928,9 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target> <target>Para hacer una conexión nueva</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <target>Para proteger la privacidad, en lugar de los identificadores de usuario que usan el resto de plataformas, SimpleX dispone de identificadores para las colas de mensajes, independientes para cada uno de tus contactos.</target> + <trans-unit id="To protect against your link being replaced, you can compare contact security codes." xml:space="preserve"> + <source>To protect against your link being replaced, you can compare contact security codes.</source> + <target>Para protegerte contra una sustitución del enlace, puedes comparar los códigos de seguridad con tu contacto.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -5054,13 +7938,38 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target> <target>Para proteger la zona horaria, los archivos de imagen/voz usan la hora UTC.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve"> + <source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source> + <target>Para proteger tu dirección IP, el enrutamiento privado usa tu lista de servidores SMP para enviar mensajes.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> - <target>Para proteger tu información, activa Bloqueo SimpleX. + <target>Para proteger tu información, activa el Bloqueo SimpleX. Se te pedirá que completes la autenticación antes de activar esta función.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> + <target>Para proteger tu privacidad, SimpleX usa identificadores distintos para cada uno de tus contactos.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To receive" xml:space="preserve"> + <source>To receive</source> + <target>Para recibir</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve"> + <source>To record speech please grant permission to use Microphone.</source> + <target>Para grabación de voz, por favor concede el permiso para usar el micrófono.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record video please grant permission to use Camera." xml:space="preserve"> + <source>To record video please grant permission to use Camera.</source> + <target>Para grabación de vídeo, por favor concede el permiso para usar la cámara.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> <target>Para grabar el mensaje de voz concede permiso para usar el micrófono.</target> @@ -5071,14 +7980,29 @@ Se te pedirá que completes la autenticación antes de activar esta función.</t <target>Para hacer visible tu perfil oculto, introduce la contraseña en el campo de búsqueda del menú **Mis perfiles**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To send" xml:space="preserve"> + <source>To send</source> + <target>Para enviar</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> <source>To support instant push notifications the chat database has to be migrated.</source> <target>Para permitir las notificaciones automáticas instantáneas, la base de datos se debe migrar.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To use the servers of **%@**, accept conditions of use." xml:space="preserve"> + <source>To use the servers of **%@**, accept conditions of use.</source> + <target>Para usar los servidores de **%@**, debes aceptar las condiciones de uso.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> - <target>Para comprobar el cifrado de extremo a extremo con tu contacto compara (o escanea) el código en tus dispositivos.</target> + <target>Para verificar el cifrado de extremo a extremo con tu contacto, compara (o escanea) el código en ambos dispositivos.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Toggle chat list:" xml:space="preserve"> + <source>Toggle chat list:</source> + <target>Alternar lista de chats:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> @@ -5086,11 +8010,31 @@ Se te pedirá que completes la autenticación antes de activar esta función.</t <target>Activa incógnito al conectar.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Token status: %@." xml:space="preserve"> + <source>Token status: %@.</source> + <target>Estado token: %@.</target> + <note>token status</note> + </trans-unit> + <trans-unit id="Toolbar opacity" xml:space="preserve"> + <source>Toolbar opacity</source> + <target>Opacidad barra</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Total" xml:space="preserve"> + <source>Total</source> + <target>Total</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>Aislamiento de transporte</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Transport sessions" xml:space="preserve"> + <source>Transport sessions</source> + <target>Sesiones de transporte</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> <target>Intentando conectar con el servidor usado para recibir mensajes de este contacto (error: %@).</target> @@ -5101,16 +8045,16 @@ Se te pedirá que completes la autenticación antes de activar esta función.</t <target>Intentando conectar con el servidor usado para recibir mensajes de este contacto.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Turkish interface" xml:space="preserve"> + <source>Turkish interface</source> + <target>Interfaz en turco</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> <target>Desactivar</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve"> - <source>Turn off notifications?</source> - <target>¿Desactivar notificaciones?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> <target>Activar</target> @@ -5123,20 +8067,33 @@ Se te pedirá que completes la autenticación antes de activar esta función.</t </trans-unit> <trans-unit id="Unblock" xml:space="preserve"> <source>Unblock</source> + <target>Desbloquear</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unblock for all" xml:space="preserve"> + <source>Unblock for all</source> + <target>Desbloquear para todos</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unblock member" xml:space="preserve"> <source>Unblock member</source> + <target>Desbloquear miembro</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unblock member for all?" xml:space="preserve"> + <source>Unblock member for all?</source> + <target>¿Desbloquear el miembro para todos?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unblock member?" xml:space="preserve"> <source>Unblock member?</source> + <target>¿Desbloquear miembro?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve"> - <source>Unexpected error: %@</source> - <target>Error inesperado: %@</target> - <note>item status description</note> + <trans-unit id="Undelivered messages" xml:space="preserve"> + <source>Undelivered messages</source> + <target>Mensajes no entregados</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -5146,7 +8103,7 @@ Se te pedirá que completes la autenticación antes de activar esta función.</t <trans-unit id="Unfav." xml:space="preserve"> <source>Unfav.</source> <target>No fav.</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> @@ -5183,6 +8140,11 @@ Se te pedirá que completes la autenticación antes de activar esta función.</t <target>Error desconocido</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unknown servers!" xml:space="preserve"> + <source>Unknown servers!</source> + <target>¡Servidores desconocidos!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> <target>A menos que utilices la interfaz de llamadas de iOS, activa el modo No molestar para evitar interrupciones.</target> @@ -5191,17 +8153,18 @@ Se te pedirá que completes la autenticación antes de activar esta función.</t <trans-unit id="Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection." xml:space="preserve"> <source>Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection.</source> - <target>A menos que tu contacto haya eliminado la conexión o -que este enlace ya se haya usado, podría ser un error. Por favor, notifícalo. -Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueba que tienes buena conexión de red.</target> + <target>A menos que tu contacto haya eliminado la conexión o el enlace se haya usado, podría ser un error. Por favor, notifícalo. +Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión de red.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unlink" xml:space="preserve"> <source>Unlink</source> + <target>Desenlazar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unlink desktop?" xml:space="preserve"> <source>Unlink desktop?</source> + <target>¿Desenlazar ordenador?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unlock" xml:space="preserve"> @@ -5217,11 +8180,21 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> <target>Activar audio</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> </trans-unit> <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> <target>No leído</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Unsupported connection link" xml:space="preserve"> + <source>Unsupported connection link</source> + <target>Enlace de conexión no compatible</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve"> + <source>Up to 100 last messages are sent to new members.</source> + <target>Hasta 100 últimos mensajes son enviados a los miembros nuevos.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update" xml:space="preserve"> @@ -5229,11 +8202,6 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb <target>Actualizar</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> - <source>Update .onion hosts setting?</source> - <target>¿Actualizar la configuración de los hosts .onion?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> <target>Actualizar contraseña de la base de datos</target> @@ -5244,19 +8212,19 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb <target>¿Actualizar la configuración de red?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve"> - <source>Update transport isolation mode?</source> - <target>¿Actualizar el modo de aislamiento de transporte?</target> + <trans-unit id="Update settings?" xml:space="preserve"> + <source>Update settings?</source> + <target>¿Actualizar configuración?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updated conditions" xml:space="preserve"> + <source>Updated conditions</source> + <target>Condiciones actualizadas</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> <source>Updating settings will re-connect the client to all servers.</source> - <target>Al actualizar la configuración el cliente se reconectará a todos los servidores.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> - <target>Al actualizar esta configuración el cliente se reconectará a todos los servidores.</target> + <target>Para actualizar la configuración el cliente se reconectará a todos los servidores.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Upgrade and open chat" xml:space="preserve"> @@ -5264,21 +8232,66 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb <target>Actualizar y abrir Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Upload errors" xml:space="preserve"> + <source>Upload errors</source> + <target>Errores en subida</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload failed" xml:space="preserve"> + <source>Upload failed</source> + <target>Error de subida</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Upload file" xml:space="preserve"> <source>Upload file</source> <target>Subir archivo</target> <note>server test step</note> </trans-unit> + <trans-unit id="Uploaded" xml:space="preserve"> + <source>Uploaded</source> + <target>Subido</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploaded files" xml:space="preserve"> + <source>Uploaded files</source> + <target>Archivos subidos</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploading archive" xml:space="preserve"> + <source>Uploading archive</source> + <target>Subiendo archivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use %@" xml:space="preserve"> + <source>Use %@</source> + <target>Usar %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> <target>Usar hosts .onion</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use SOCKS proxy" xml:space="preserve"> + <source>Use SOCKS proxy</source> + <target>Usar proxy SOCKS</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> <target>¿Usar servidores SimpleX Chat?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use TCP port %@ when no port is specified." xml:space="preserve"> + <source>Use TCP port %@ when no port is specified.</source> + <target>Se usa el puerto TCP %@ cuando no se ha especificado otro.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve"> + <source>Use TCP port 443 for preset servers only.</source> + <target>Usar puerto TCP 443 solo en servidores predefinidos.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> <target>Usar Chat</target> @@ -5289,13 +8302,24 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb <target>Usar perfil actual</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use for files" xml:space="preserve"> + <source>Use for files</source> + <target>Uso para archivos</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for messages" xml:space="preserve"> + <source>Use for messages</source> + <target>Uso para mensajes</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> - <target>Usar para conexiones nuevas</target> + <target>Para conexiones nuevas</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use from desktop" xml:space="preserve"> <source>Use from desktop</source> + <target>Usar desde ordenador</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use iOS call interface" xml:space="preserve"> @@ -5308,19 +8332,59 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb <target>Usar nuevo perfil incógnito</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use only local notifications?" xml:space="preserve"> + <source>Use only local notifications?</source> + <target>¿Usar sólo notificaciones locales?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve"> + <source>Use private routing with unknown servers when IP address is not protected.</source> + <target>Usar enrutamiento privado con servidores desconocidos cuando tu dirección IP no está protegida.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers." xml:space="preserve"> + <source>Use private routing with unknown servers.</source> + <target>Usar enrutamiento privado con servidores de mensaje desconocidos.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Usar servidor</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> - <target>Perfil de usuario</target> + <trans-unit id="Use servers" xml:space="preserve"> + <source>Use servers</source> + <target>Usar servidores</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> - <target>Usar hosts .onion requiere un proveedor VPN compatible.</target> + <trans-unit id="Use short links (BETA)" xml:space="preserve"> + <source>Use short links (BETA)</source> + <target>Usar enlaces cortos (BETA)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app while in the call." xml:space="preserve"> + <source>Use the app while in the call.</source> + <target>Usar la aplicación durante la llamada.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app with one hand." xml:space="preserve"> + <source>Use the app with one hand.</source> + <target>Usa la aplicación con una sola mano.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use web port" xml:space="preserve"> + <source>Use web port</source> + <target>Usar puerto web</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="User selection" xml:space="preserve"> + <source>User selection</source> + <target>Selección de usuarios</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Username" xml:space="preserve"> + <source>Username</source> + <target>Nombre de usuario</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> @@ -5330,10 +8394,12 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb </trans-unit> <trans-unit id="Verify code with desktop" xml:space="preserve"> <source>Verify code with desktop</source> + <target>Verificar código con ordenador</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify connection" xml:space="preserve"> <source>Verify connection</source> + <target>Verificar conexión</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify connection security" xml:space="preserve"> @@ -5343,6 +8409,17 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb </trans-unit> <trans-unit id="Verify connections" xml:space="preserve"> <source>Verify connections</source> + <target>Verificar conexiones</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify database passphrase" xml:space="preserve"> + <source>Verify database passphrase</source> + <target>Verificar la contraseña de la base de datos</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify passphrase" xml:space="preserve"> + <source>Verify passphrase</source> + <target>Verificar frase de contraseña</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify security code" xml:space="preserve"> @@ -5357,6 +8434,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb </trans-unit> <trans-unit id="Via secure quantum resistant protocol." xml:space="preserve"> <source>Via secure quantum resistant protocol.</source> + <target>Mediante protocolo seguro de resistencia cuántica.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Video call" xml:space="preserve"> @@ -5366,12 +8444,12 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb </trans-unit> <trans-unit id="Video will be received when your contact completes uploading it." xml:space="preserve"> <source>Video will be received when your contact completes uploading it.</source> - <target>El video se recibirá cuando tu contacto termine de subirlo.</target> + <target>El video se recibirá cuando el contacto termine de subirlo.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Video will be received when your contact is online, please wait or check later!" xml:space="preserve"> <source>Video will be received when your contact is online, please wait or check later!</source> - <target>El vídeo se recibirá cuando tu contacto esté en línea, por favor espera o compruébalo más tarde.</target> + <target>El vídeo se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Videos and files up to 1gb" xml:space="preserve"> @@ -5379,11 +8457,26 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb <target>Vídeos y archivos de hasta 1Gb</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View conditions" xml:space="preserve"> + <source>View conditions</source> + <target>Ver condiciones</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> <target>Mostrar código de seguridad</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View updated conditions" xml:space="preserve"> + <source>View updated conditions</source> + <target>Ver condiciones actualizadas</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Visible history" xml:space="preserve"> + <source>Visible history</source> + <target>Historial visible</target> + <note>chat feature</note> + </trans-unit> <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> <target>Mensajes de voz</target> @@ -5394,11 +8487,16 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb <target>Los mensajes de voz no están permitidos en este chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <target>Los mensajes de voz no están permitidos en este grupo.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Voice messages not allowed" xml:space="preserve"> + <source>Voice messages not allowed</source> + <target>Mensajes de voz no permitidos</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> <source>Voice messages prohibited!</source> <target>¡Mensajes de voz no permitidos!</target> @@ -5411,6 +8509,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb </trans-unit> <trans-unit id="Waiting for desktop..." xml:space="preserve"> <source>Waiting for desktop...</source> + <target>Esperando ordenador...</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Waiting for file" xml:space="preserve"> @@ -5428,6 +8527,21 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb <target>Esperando el vídeo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wallpaper accent" xml:space="preserve"> + <source>Wallpaper accent</source> + <target>Color imagen de fondo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper background" xml:space="preserve"> + <source>Wallpaper background</source> + <target>Color de fondo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve"> + <source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source> + <target>Atención: el inicio del chat en varios dispositivos es incompatible y provocará fallos en la entrega de mensajes</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> <source>Warning: you may lose some data!</source> <target>Atención: ¡puedes perder algunos datos!</target> @@ -5448,6 +8562,11 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb <target>Mensaje de bienvenida</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Welcome message is too long" xml:space="preserve"> + <source>Welcome message is too long</source> + <target>Mensaje de bienvenida demasiado largo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="What's new" xml:space="preserve"> <source>What's new</source> <target>Novedades</target> @@ -5458,9 +8577,14 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb <target>Si disponibles</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> - <source>When people request to connect, you can accept or reject it.</source> - <target>Cuando alguien solicite conectarse podrás aceptar o rechazar la solicitud.</target> + <trans-unit id="When connecting audio and video calls." xml:space="preserve"> + <source>When connecting audio and video calls.</source> + <target>Al iniciar llamadas de audio y vídeo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When more than one operator is enabled, none of them has metadata to learn who communicates with whom." xml:space="preserve"> + <source>When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</source> + <target>Cuando está habilitado más de un operador, ninguno dispone de los metadatos para conocer quién se comunica con quién.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> @@ -5468,29 +8592,74 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb <target>Cuando compartes un perfil incógnito con alguien, este perfil también se usará para los grupos a los que te inviten.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="WiFi" xml:space="preserve"> + <source>WiFi</source> + <target>WiFi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Will be enabled in direct chats!" xml:space="preserve"> + <source>Will be enabled in direct chats!</source> + <target>¡Será habilitado en los chats directos!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wired ethernet" xml:space="preserve"> + <source>Wired ethernet</source> + <target>Ethernet por cable</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With encrypted files and media." xml:space="preserve"> + <source>With encrypted files and media.</source> + <target>Con cifrado de archivos y multimedia.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> <target>Con mensaje de bienvenida opcional.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="With reduced battery usage." xml:space="preserve"> + <source>With reduced battery usage.</source> + <target>Con uso reducido de batería.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to file servers.</source> + <target>Sin Tor o VPN, tu dirección IP será visible para los servidores de archivos.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source> + <target>Sin Tor o VPN, tu dirección IP será visible para estos servidores XFTP: %@.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> <target>Contraseña de base de datos incorrecta</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve"> + <source>Wrong key or unknown connection - most likely this connection is deleted.</source> + <target>Clave incorrecta o conexión desconocida - probablemente esta conexión fue eliminada.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Wrong key or unknown file chunk address - most likely file is deleted." xml:space="preserve"> + <source>Wrong key or unknown file chunk address - most likely file is deleted.</source> + <target>Clave incorrecta o dirección del bloque del archivo desconocida. Es probable que el archivo se haya eliminado.</target> + <note>file error text</note> + </trans-unit> <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> <target>¡Contraseña incorrecta!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="XFTP servers" xml:space="preserve"> - <source>XFTP servers</source> - <target>Servidores XFTP</target> + <trans-unit id="XFTP server" xml:space="preserve"> + <source>XFTP server</source> + <target>Servidor XFTP</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> - <target>Tú</target> + <trans-unit id="You **must not** use the same database on two devices." xml:space="preserve"> + <source>You **must not** use the same database on two devices.</source> + <target>**No debes** usar la misma base de datos en dos dispositivos.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You accepted connection" xml:space="preserve"> @@ -5510,36 +8679,49 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb </trans-unit> <trans-unit id="You are already connected to %@." xml:space="preserve"> <source>You are already connected to %@.</source> - <target>Ya estás conectado a %@.</target> + <target>Ya estás conectado con %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already connected with %@." xml:space="preserve"> + <source>You are already connected with %@.</source> + <target>Ya estás conectado con %@.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already connecting to %@." xml:space="preserve"> <source>You are already connecting to %@.</source> + <target>Ya estás conectando con %@.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already connecting via this one-time link!" xml:space="preserve"> <source>You are already connecting via this one-time link!</source> + <target>¡Ya estás conectando mediante este enlace de un solo uso!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already in group %@." xml:space="preserve"> <source>You are already in group %@.</source> + <target>Ya estás en el grupo %@.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already joining the group %@." xml:space="preserve"> <source>You are already joining the group %@.</source> + <target>Ya estás uniéndote al grupo %@.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already joining the group via this link!" xml:space="preserve"> <source>You are already joining the group via this link!</source> + <target>¡Ya estás uniéndote al grupo mediante este enlace!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already joining the group via this link." xml:space="preserve"> <source>You are already joining the group via this link.</source> + <target>Ya estás uniéndote al grupo mediante este enlace.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already joining the group! Repeat join request?" xml:space="preserve"> <source>You are already joining the group! Repeat join request?</source> + <target>¡En proceso de unirte al grupo! +¿Repetir solicitud de admisión?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve"> @@ -5549,7 +8731,12 @@ Repeat join request?</source> </trans-unit> <trans-unit id="You are invited to group" xml:space="preserve"> <source>You are invited to group</source> - <target>Has sido invitado al grupo</target> + <target>Has sido invitado a un grupo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are not connected to these servers. Private routing is used to deliver messages to them." xml:space="preserve"> + <source>You are not connected to these servers. Private routing is used to deliver messages to them.</source> + <target>No tienes conexión directa a estos servidores. Los mensajes destinados a estos usan enrutamiento privado.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> @@ -5557,9 +8744,14 @@ Repeat join request?</source> <target>Puede aceptar llamadas desde la pantalla de bloqueo, sin autenticación de dispositivos y aplicaciones.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <target>También puedes conectarte haciendo clic en el enlace. Si se abre en el navegador, haz clic en el botón **Abrir en aplicación móvil**.</target> + <trans-unit id="You can change it in Appearance settings." xml:space="preserve"> + <source>You can change it in Appearance settings.</source> + <target>Puedes cambiar la posición de la barra desde el menú Apariencia.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can configure servers via settings." xml:space="preserve"> + <source>You can configure servers via settings.</source> + <target>Puedes configurar los servidores a través de su configuración.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can create it later" xml:space="preserve"> @@ -5577,16 +8769,36 @@ Repeat join request?</source> <target>Puedes activarlos más tarde en la configuración de Privacidad y Seguridad.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can give another try." xml:space="preserve"> + <source>You can give another try.</source> + <target>Puedes intentarlo de nuevo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <source>You can hide or mute a user profile - swipe it to the right.</source> <target>Puedes ocultar o silenciar un perfil deslizándolo a la derecha.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> - <target>Ya puedes enviar mensajes a %@</target> + <trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve"> + <source>You can make it visible to your SimpleX contacts via Settings.</source> + <target>Puedes hacerlo visible para tus contactos de SimpleX en Configuración.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now chat with %@" xml:space="preserve"> + <source>You can now chat with %@</source> + <target>Ya puedes chatear con %@</target> <note>notification body</note> </trans-unit> + <trans-unit id="You can send messages to %@ from Archived contacts." xml:space="preserve"> + <source>You can send messages to %@ from Archived contacts.</source> + <target>Puedes enviar mensajes a %@ desde Contactos archivados.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can set connection name, to remember who the link was shared with." xml:space="preserve"> + <source>You can set connection name, to remember who the link was shared with.</source> + <target>Puedes añadir un nombre a la conexión para recordar a quién corresponde.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> <target>Puedes configurar las notificaciones de la pantalla de bloqueo desde Configuración.</target> @@ -5594,7 +8806,7 @@ Repeat join request?</source> </trans-unit> <trans-unit id="You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." xml:space="preserve"> <source>You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.</source> - <target>Puedes compartir un enlace o un código QR: cualquiera podrá unirse al grupo. Si lo eliminas más tarde los miembros del grupo no se perderán.</target> + <target>Puedes compartir un enlace o código QR para que cualquiera pueda unirse al grupo. Si decides eliminarlo más tarde, los miembros del grupo se mantendrán.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can share this address with your contacts to let them connect with **%@**." xml:space="preserve"> @@ -5602,16 +8814,16 @@ Repeat join request?</source> <target>Puedes compartir esta dirección con tus contactos para que puedan conectar con **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> - <source>You can share your address as a link or QR code - anybody can connect to you.</source> - <target>Puedes compartir tu dirección como enlace o como código QR: cualquiera podrá conectarse contigo.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> <target>Puede iniciar Chat a través de la Configuración / Base de datos de la aplicación o reiniciando la aplicación</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve"> + <source>You can still view conversation with %@ in the list of chats.</source> + <target>Aún puedes ver la conversación con %@ en la lista de chats.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> <target>Puedes activar el Bloqueo SimpleX a través de Configuración.</target> @@ -5622,33 +8834,36 @@ Repeat join request?</source> <target>Puedes usar la sintaxis markdown para dar formato a tus mensajes:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can view invitation link again in connection details." xml:space="preserve"> + <source>You can view invitation link again in connection details.</source> + <target>Podrás ver el enlace de invitación en detalles de conexión.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> <target>¡No puedes enviar mensajes!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <target>Tú controlas a través de qué servidor(es) **recibes** los mensajes. Tus contactos controlan a través de qué servidor(es) **envías** tus mensajes.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <target>No has podido ser autenticado. Inténtalo de nuevo.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> + <target>Tu decides quién se conecta.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You have already requested connection via this address!" xml:space="preserve"> <source>You have already requested connection via this address!</source> + <target>¡Ya has solicitado la conexión mediante esta dirección!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You have already requested connection! Repeat connection request?" xml:space="preserve"> <source>You have already requested connection! Repeat connection request?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> - <target>No tienes chats</target> + <target>Ya has solicitado la conexión +¿Repetir solicitud?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> @@ -5671,11 +8886,26 @@ Repeat connection request?</source> <target>Te has unido a este grupo. Conectando con el emisor de la invitacíon.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You may migrate the exported database." xml:space="preserve"> + <source>You may migrate the exported database.</source> + <target>Puedes migrar la base de datos exportada.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may save the exported archive." xml:space="preserve"> + <source>You may save the exported archive.</source> + <target>Puedes guardar el archivo exportado.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> <target>Debes usar la versión más reciente de tu base de datos ÚNICAMENTE en un dispositivo, de lo contrario podrías dejar de recibir mensajes de algunos contactos.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You need to allow your contact to call to be able to call them." xml:space="preserve"> + <source>You need to allow your contact to call to be able to call them.</source> + <target>Debes permitir que tus contacto te llamen para poder llamarles.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> <target>Para poder enviar mensajes de voz antes debes permitir que tu contacto pueda enviarlos.</target> @@ -5691,32 +8921,39 @@ Repeat connection request?</source> <target>Has enviado una invitación de grupo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You should receive notifications." xml:space="preserve"> + <source>You should receive notifications.</source> + <target>Deberías recibir notificaciones.</target> + <note>token info</note> + </trans-unit> <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> - <target>Te conectarás al grupo cuando el dispositivo del anfitrión esté en línea, por favor espera o compruébalo más tarde.</target> + <target>Te conectarás al grupo cuando el dispositivo del anfitrión esté en línea, por favor espera o revisa más tarde.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be connected when group link host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected when group link host's device is online, please wait or check later!</source> + <target>Te conectarás cuando el dispositivo propietario del grupo esté en línea, por favor espera o revisa más tarde.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve"> <source>You will be connected when your connection request is accepted, please wait or check later!</source> - <target>Te conectarás cuando tu solicitud se acepte, por favor espera o compruébalo más tarde.</target> + <target>Te conectarás cuando tu solicitud se acepte, por favor espera o revisa más tarde.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be connected when your contact's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected when your contact's device is online, please wait or check later!</source> - <target>Te conectarás cuando el dispositivo de tu contacto esté en línea, por favor espera o compruébalo más tarde.</target> + <target>Te conectarás cuando el dispositivo del contacto esté en línea, por favor espera o revisa más tarde.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be required to authenticate when you start or resume the app after 30 seconds in background." xml:space="preserve"> <source>You will be required to authenticate when you start or resume the app after 30 seconds in background.</source> - <target>Se te pedirá identificarte cuándo inicies o continues usando la aplicación tras 30 segundos en segundo plano.</target> + <target>Se te pedirá autenticarte cuando inicies la aplicación o sigas usándola tras 30 segundos en segundo plano.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will connect to all group members." xml:space="preserve"> <source>You will connect to all group members.</source> + <target>Te conectarás con todos los miembros del grupo.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve"> @@ -5724,6 +8961,11 @@ Repeat connection request?</source> <target>Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You will stop receiving messages from this chat. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this chat. Chat history will be preserved.</source> + <target>Dejarás de recibir mensajes de este chat. El historial del chat se conserva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> <target>Dejarás de recibir mensajes de este grupo. El historial del chat se conservará.</target> @@ -5741,12 +8983,7 @@ Repeat connection request?</source> </trans-unit> <trans-unit id="You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" xml:space="preserve"> <source>You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed</source> - <target>Estás usando un perfil incógnito para este grupo, por tanto para evitar compartir tu perfil principal no se permite invitar a contactos</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your %@ servers" xml:space="preserve"> - <source>Your %@ servers</source> - <target>Mis servidores %@</target> + <target>Estás usando un perfil incógnito en este grupo. Para evitar descubrir tu perfil principal no se permite invitar contactos</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your ICE servers" xml:space="preserve"> @@ -5754,21 +8991,11 @@ Repeat connection request?</source> <target>Servidores ICE</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> - <target>Servidores SMP</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your SimpleX address" xml:space="preserve"> <source>Your SimpleX address</source> <target>Mi dirección SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your XFTP servers" xml:space="preserve"> - <source>Your XFTP servers</source> - <target>Servidores XFTP</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your calls" xml:space="preserve"> <source>Your calls</source> <target>Llamadas</target> @@ -5784,16 +9011,19 @@ Repeat connection request?</source> <target>La base de datos no está cifrada - establece una contraseña para cifrarla.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your chat preferences" xml:space="preserve"> + <source>Your chat preferences</source> + <target>Tus preferencias de chat</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>Mis perfiles</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> - <target>Tu contacto debe estar en línea para que se complete la conexión. -Puedes cancelar esta conexión y eliminar el contacto (e intentarlo más tarde con un enlace nuevo).</target> + <trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve"> + <source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source> + <target>Tu conexión ha sido trasladada a %@ pero ha ocurrido un error inesperado al redirigirte al perfil.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> @@ -5806,18 +9036,16 @@ Puedes cancelar esta conexión y eliminar el contacto (e intentarlo más tarde c <target>Tus contactos pueden permitir la eliminación completa de mensajes.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> - <source>Your contacts in SimpleX will see it. -You can change it in Settings.</source> - <target>Tus contactos en SimpleX lo verán. -Puedes cambiarlo en Configuración.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your contacts will remain connected." xml:space="preserve"> <source>Your contacts will remain connected.</source> <target>Tus contactos permanecerán conectados.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve"> + <source>Your credentials may be sent unencrypted.</source> + <target>Tus credenciales podrían ser enviadas sin cifrar.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> <target>La base de datos actual será ELIMINADA y SUSTITUIDA por la importada.</target> @@ -5840,38 +9068,42 @@ Puedes cambiarlo en Configuración.</target> </trans-unit> <trans-unit id="Your profile" xml:space="preserve"> <source>Your profile</source> + <target>Tu perfil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> <source>Your profile **%@** will be shared.</source> - <target>Tu perfil **%@** será compartido.</target> + <target>El perfil **%@** será compartido.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> - <target>Tu perfil se almacena en tu dispositivo y sólo se comparte con tus contactos. -Los servidores de SimpleX no pueden ver tu perfil.</target> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> + <target>El perfil sólo se comparte con tus contactos.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> - <target>Tu perfil, contactos y mensajes se almacenan en tu dispositivo.</target> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> + <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> + <target>Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos. Los servidores SimpleX no pueden ver tu perfil.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve"> + <source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source> + <target>Tu perfil ha sido modificado. Si lo guardas la actualización será enviada a todos tus contactos.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> <target>Tu perfil aleatorio</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> - <target>Tu servidor</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> - <target>Dirección de tu servidor</target> + <target>Dirección del servidor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your servers" xml:space="preserve"> + <source>Your servers</source> + <target>Tus servidores</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your settings" xml:space="preserve"> @@ -5914,11 +9146,21 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>llamada aceptada</target> <note>call status</note> </trans-unit> + <trans-unit id="accepted invitation" xml:space="preserve"> + <source>accepted invitation</source> + <target>invitación aceptada</target> + <note>chat list item title</note> + </trans-unit> <trans-unit id="admin" xml:space="preserve"> <source>admin</source> <target>administrador</target> <note>member role</note> </trans-unit> + <trans-unit id="admins" xml:space="preserve"> + <source>admins</source> + <target>administradores</target> + <note>feature role</note> + </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> <target>acordando cifrado para %@…</target> @@ -5929,6 +9171,11 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>acordando cifrado…</target> <note>chat item text</note> </trans-unit> + <trans-unit id="all members" xml:space="preserve"> + <source>all members</source> + <target>todos los miembros</target> + <note>feature role</note> + </trans-unit> <trans-unit id="always" xml:space="preserve"> <source>always</source> <target>siempre</target> @@ -5936,6 +9183,17 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </trans-unit> <trans-unit id="and %lld other events" xml:space="preserve"> <source>and %lld other events</source> + <target>y %lld evento(s) más</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="archived report" xml:space="preserve"> + <source>archived report</source> + <target>informes archivados</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="attempts" xml:space="preserve"> + <source>attempts</source> + <target>intentos</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> @@ -5945,6 +9203,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </trans-unit> <trans-unit id="author" xml:space="preserve"> <source>author</source> + <target>autor</target> <note>member role</note> </trans-unit> <trans-unit id="bad message ID" xml:space="preserve"> @@ -5959,13 +9218,30 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </trans-unit> <trans-unit id="blocked" xml:space="preserve"> <source>blocked</source> - <note>No comment provided by engineer.</note> + <target>bloqueado</target> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="blocked %@" xml:space="preserve"> + <source>blocked %@</source> + <target>ha bloqueado a %@</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="blocked by admin" xml:space="preserve"> + <source>blocked by admin</source> + <target>bloqueado por administrador</target> + <note>blocked chat item +marked deleted chat item preview text</note> </trans-unit> <trans-unit id="bold" xml:space="preserve"> <source>bold</source> <target>negrita</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="call" xml:space="preserve"> + <source>call</source> + <target>llamada</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="call error" xml:space="preserve"> <source>call error</source> <target>error en llamada</target> @@ -5988,7 +9264,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </trans-unit> <trans-unit id="changed address for you" xml:space="preserve"> <source>changed address for you</source> - <target>el servidor de envío ha cambiado para tí</target> + <target>ha cambiado tu servidor de envío</target> <note>chat item text</note> </trans-unit> <trans-unit id="changed role of %@ to %@" xml:space="preserve"> @@ -6003,12 +9279,12 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </trans-unit> <trans-unit id="changing address for %@…" xml:space="preserve"> <source>changing address for %@…</source> - <target>cambiando dirección para %@…</target> + <target>cambiando el servidor para %@…</target> <note>chat item text</note> </trans-unit> <trans-unit id="changing address…" xml:space="preserve"> <source>changing address…</source> - <target>cambiando dirección…</target> + <target>cambiando de servidor…</target> <note>chat item text</note> </trans-unit> <trans-unit id="colored" xml:space="preserve"> @@ -6038,7 +9314,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </trans-unit> <trans-unit id="connecting" xml:space="preserve"> <source>connecting</source> - <target>conectando</target> + <target>conectando...</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connecting (accepted)" xml:space="preserve"> @@ -6069,7 +9345,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> <target>conectando…</target> - <note>chat list item title</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> @@ -6081,6 +9357,11 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>conexión: % @</target> <note>connection information</note> </trans-unit> + <trans-unit id="contact %@ changed to %@" xml:space="preserve"> + <source>contact %1$@ changed to %2$@</source> + <target>el contacto %1$@ ha cambiado a %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> <target>el contacto dispone de cifrado de extremo a extremo</target> @@ -6111,19 +9392,25 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>días</target> <note>time unit</note> </trans-unit> + <trans-unit id="decryption errors" xml:space="preserve"> + <source>decryption errors</source> + <target>errores de descifrado</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> - <target>por defecto (%@)</target> - <note>pref value</note> + <target>predeterminado (%@)</target> + <note>delete after time +pref value</note> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> - <target>por defecto (no)</target> + <target>predeterminado (no)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="default (yes)" xml:space="preserve"> <source>default (yes)</source> - <target>por defecto (sí)</target> + <target>predeterminado (sí)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="deleted" xml:space="preserve"> @@ -6133,11 +9420,12 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </trans-unit> <trans-unit id="deleted contact" xml:space="preserve"> <source>deleted contact</source> + <target>contacto eliminado</target> <note>rcv direct event chat item</note> </trans-unit> <trans-unit id="deleted group" xml:space="preserve"> <source>deleted group</source> - <target>grupo eliminado</target> + <target>ha eliminado el grupo</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="different migration in the app/database: %@ / %@" xml:space="preserve"> @@ -6160,6 +9448,11 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>mensaje duplicado</target> <note>integrity error chat item</note> </trans-unit> + <trans-unit id="duplicates" xml:space="preserve"> + <source>duplicates</source> + <target>duplicados</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> <target>cifrado de extremo a extremo</target> @@ -6235,9 +9528,14 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>error</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="event happened" xml:space="preserve"> - <source>event happened</source> - <target>evento ocurrido</target> + <trans-unit id="expired" xml:space="preserve"> + <source>expired</source> + <target>expirados</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="forwarded" xml:space="preserve"> + <source>forwarded</source> + <target>reenviado</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> @@ -6265,6 +9563,11 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>iOS Keychain se usará para almacenar la contraseña de forma segura después de reiniciar la aplicación o cambiar la contraseña. Esto permitirá recibir notificaciones automáticas.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="inactive" xml:space="preserve"> + <source>inactive</source> + <target>inactivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> <target>en modo incógnito mediante enlace de dirección del contacto</target> @@ -6305,6 +9608,11 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>invitación al grupo %@</target> <note>group name</note> </trans-unit> + <trans-unit id="invite" xml:space="preserve"> + <source>invite</source> + <target>Invitar</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> <target>ha sido invitado</target> @@ -6317,7 +9625,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </trans-unit> <trans-unit id="invited to connect" xml:space="preserve"> <source>invited to connect</source> - <target>invitado a conectarse</target> + <target>invitación a conectarse</target> <note>chat list item title</note> </trans-unit> <trans-unit id="invited via your group link" xml:space="preserve"> @@ -6350,11 +9658,21 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>miembro</target> <note>member role</note> </trans-unit> + <trans-unit id="member %@ changed to %@" xml:space="preserve"> + <source>member %1$@ changed to %2$@</source> + <target>el miembro %1$@ ha cambiado a %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> <target>conectado</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="message" xml:space="preserve"> + <source>message</source> + <target>mensaje</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="message received" xml:space="preserve"> <source>message received</source> <target>mensaje recibido</target> @@ -6378,7 +9696,12 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <trans-unit id="moderated by %@" xml:space="preserve"> <source>moderated by %@</source> <target>moderado por %@</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="moderator" xml:space="preserve"> + <source>moderator</source> + <target>moderador</target> + <note>member role</note> </trans-unit> <trans-unit id="months" xml:space="preserve"> <source>months</source> @@ -6388,7 +9711,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <trans-unit id="never" xml:space="preserve"> <source>never</source> <target>nunca</target> - <note>No comment provided by engineer.</note> + <note>delete after time</note> </trans-unit> <trans-unit id="new message" xml:space="preserve"> <source>new message</source> @@ -6419,8 +9742,8 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <source>off</source> <target>desactivado</target> <note>enabled status - group pref value - time to disappear</note> +group pref value +time to disappear</note> </trans-unit> <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> @@ -6437,16 +9760,46 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>Activado</target> <note>group pref value</note> </trans-unit> + <trans-unit id="other" xml:space="preserve"> + <source>other</source> + <target>otros</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="other errors" xml:space="preserve"> + <source>other errors</source> + <target>otros errores</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="owner" xml:space="preserve"> <source>owner</source> <target>propietario</target> <note>member role</note> </trans-unit> + <trans-unit id="owners" xml:space="preserve"> + <source>owners</source> + <target>propietarios</target> + <note>feature role</note> + </trans-unit> <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> <target>p2p</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="pending" xml:space="preserve"> + <source>pending</source> + <target>pendiente</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="pending approval" xml:space="preserve"> + <source>pending approval</source> + <target>pendiente de aprobación</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="quantum resistant e2e encryption" xml:space="preserve"> + <source>quantum resistant e2e encryption</source> + <target>cifrado e2e resistente a tecnología cuántica</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> <target>respuesta recibida…</target> @@ -6457,6 +9810,11 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>confirmación recibida…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="rejected" xml:space="preserve"> + <source>rejected</source> + <target>rechazado</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> <target>llamada rechazada</target> @@ -6472,11 +9830,41 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>ha expulsado a %@</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="removed contact address" xml:space="preserve"> + <source>removed contact address</source> + <target>dirección de contacto eliminada</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="removed profile picture" xml:space="preserve"> + <source>removed profile picture</source> + <target>ha eliminado la imagen del perfil</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> <target>te ha expulsado</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="requested to connect" xml:space="preserve"> + <source>requested to connect</source> + <target>solicitado para conectar</target> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="saved" xml:space="preserve"> + <source>saved</source> + <target>guardado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="saved from %@" xml:space="preserve"> + <source>saved from %@</source> + <target>Guardado desde %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="search" xml:space="preserve"> + <source>search</source> + <target>buscar</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="sec" xml:space="preserve"> <source>sec</source> <target>seg</target> @@ -6502,6 +9890,30 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>Enviar mensaje directo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve"> + <source>server queue info: %1$@ + +last received msg: %2$@</source> + <target>información cola del servidor: %1$@ + +último mensaje recibido: %2$@</target> + <note>queue info</note> + </trans-unit> + <trans-unit id="set new contact address" xml:space="preserve"> + <source>set new contact address</source> + <target>nueva dirección de contacto</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="set new profile picture" xml:space="preserve"> + <source>set new profile picture</source> + <target>tiene nueva imagen del perfil</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="standard end-to-end encryption" xml:space="preserve"> + <source>standard end-to-end encryption</source> + <target>cifrado estándar de extremo a extremo</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>inicializando…</target> @@ -6517,18 +9929,44 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>este contacto</target> <note>notification title</note> </trans-unit> + <trans-unit id="unblocked %@" xml:space="preserve"> + <source>unblocked %@</source> + <target>ha desbloqueado a %@</target> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="unknown" xml:space="preserve"> <source>unknown</source> <target>desconocido</target> <note>connection info</note> </trans-unit> + <trans-unit id="unknown servers" xml:space="preserve"> + <source>unknown servers</source> + <target>con servidores desconocidos</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unknown status" xml:space="preserve"> + <source>unknown status</source> + <target>estado desconocido</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unprotected" xml:space="preserve"> + <source>unprotected</source> + <target>con IP desprotegida</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="updated group profile" xml:space="preserve"> <source>updated group profile</source> <target>ha actualizado el perfil del grupo</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="updated profile" xml:space="preserve"> + <source>updated profile</source> + <target>perfil actualizado</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="v%@" xml:space="preserve"> <source>v%@</source> + <target>v%@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="v%@ (%@)" xml:space="preserve"> @@ -6556,6 +9994,11 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>mediante retransmisor</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="video" xml:space="preserve"> + <source>video</source> + <target>video</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> <source>video call (not e2e encrypted)</source> <target>videollamada (sin cifrar)</target> @@ -6581,14 +10024,24 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>semanas</target> <note>time unit</note> </trans-unit> + <trans-unit id="when IP hidden" xml:space="preserve"> + <source>when IP hidden</source> + <target>con IP oculta</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="yes" xml:space="preserve"> <source>yes</source> <target>sí</target> <note>pref value</note> </trans-unit> + <trans-unit id="you" xml:space="preserve"> + <source>you</source> + <target>tu</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> - <target>has sido invitado al grupo</target> + <target>has sido invitado a un grupo</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="you are observer" xml:space="preserve"> @@ -6596,6 +10049,11 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>Tu rol es observador</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="you blocked %@" xml:space="preserve"> + <source>you blocked %@</source> + <target>has bloqueado a %@</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> <target>has cambiado de servidor</target> @@ -6628,7 +10086,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </trans-unit> <trans-unit id="you shared one-time link" xml:space="preserve"> <source>you shared one-time link</source> - <target>has compartido un enlace de un uso</target> + <target>enlace de un solo uso</target> <note>chat list item description</note> </trans-unit> <trans-unit id="you shared one-time link incognito" xml:space="preserve"> @@ -6636,6 +10094,11 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>has compartido enlace de un solo uso en modo incógnito</target> <note>chat list item description</note> </trans-unit> + <trans-unit id="you unblocked %@" xml:space="preserve"> + <source>you unblocked %@</source> + <target>has desbloqueado a %@</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you: " xml:space="preserve"> <source>you: </source> <target>tú: </target> @@ -6650,7 +10113,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="es" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6670,6 +10133,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </trans-unit> <trans-unit id="NSLocalNetworkUsageDescription" xml:space="preserve"> <source>SimpleX uses local network access to allow using user chat profile via desktop app on the same network.</source> + <target>SimpleX utiliza el acceso a la red local para abrir el perfil de chat en la aplicación de ordenador en la misma red.</target> <note>Privacy - Local Network Usage Description</note> </trans-unit> <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> @@ -6686,7 +10150,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="es" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> @@ -6706,4 +10170,250 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </trans-unit> </body> </file> + <file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="es" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%d new events" xml:space="preserve"> + <source>%d new events</source> + <target>%d evento(s) nuevo(s)</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="From %d chat(s)" xml:space="preserve"> + <source>From %d chat(s)</source> + <target>De %d chat(s)</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="From: %@" xml:space="preserve"> + <source>From: %@</source> + <target>De: %@</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>Eventos nuevos</target> + <note>notification</note> + </trans-unit> + <trans-unit id="New messages" xml:space="preserve"> + <source>New messages</source> + <target>Mensajes nuevos</target> + <note>notification</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="es" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle display name</note> + </trans-unit> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> + <source>Copyright © 2024 SimpleX Chat. All rights reserved.</source> + <target>Copyright © 2024 SimpleX Chat. Todos los derechos reservados.</target> + <note>Copyright (human-readable)</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="es" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <target>%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App is locked!" xml:space="preserve"> + <source>App is locked!</source> + <target>¡Aplicación bloqueada!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <target>Cancelar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <target>Keychain inaccesible para guardar la contraseña de la base de datos</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>No se puede reenviar el mensaje</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Comment" xml:space="preserve"> + <source>Comment</source> + <target>Comentario</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <target>El tamaño máximo de archivo admitido es %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade required" xml:space="preserve"> + <source>Database downgrade required</source> + <target>Se requiere volver a versión anterior de la base de datos</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <target>¡Base de datos cifrada!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <target>Error en base de datos</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <target>La contraseña de la base de datos es diferente a la almacenada en keychain.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <target>Se requiere la contraseña de la base de datos para abrir la aplicación.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade required" xml:space="preserve"> + <source>Database upgrade required</source> + <target>Se requiere actualizar la base de datos</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing file" xml:space="preserve"> + <source>Error preparing file</source> + <target>Error al preparar el archivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing message" xml:space="preserve"> + <source>Error preparing message</source> + <target>Error al preparar el mensaje</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <target>Error: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Error de archivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <target>Versión de base de datos incompatible</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Confirmación de migración no válida</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <target>Error en keychain</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <target>¡Archivo grande!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No active profile" xml:space="preserve"> + <source>No active profile</source> + <target>Ningún perfil activo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <target>Ok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to downgrade the database." xml:space="preserve"> + <source>Open the app to downgrade the database.</source> + <target>Abre la aplicación para volver a versión anterior de la base de datos.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to upgrade the database." xml:space="preserve"> + <source>Open the app to upgrade the database.</source> + <target>Abre la aplicación para actualizar la base de datos.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passphrase" xml:space="preserve"> + <source>Passphrase</source> + <target>Frase de contraseña</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve"> + <source>Please create a profile in the SimpleX app</source> + <target>Por favor, crea un perfil en SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Las preferencias seleccionadas no permiten este mensaje.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending a message takes longer than expected." xml:space="preserve"> + <source>Sending a message takes longer than expected.</source> + <target>Enviar el mensaje lleva más tiempo del esperado.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending message…" xml:space="preserve"> + <source>Sending message…</source> + <target>Enviando mensaje…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <target>Compartir</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Slow network?" xml:space="preserve"> + <source>Slow network?</source> + <target>¿Red lenta?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <target>Error desconocido en la base de datos: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unsupported format" xml:space="preserve"> + <source>Unsupported format</source> + <target>Formato sin soporte</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wait" xml:space="preserve"> + <source>Wait</source> + <target>Espera</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <target>Contraseña incorrecta de la base de datos</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve"> + <source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source> + <target>Puedes dar permiso para compartir en Privacidad y Seguridad / Bloque SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + </body> + </file> </xliff> diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/es.xcloc/contents.json b/apps/ios/SimpleX Localizations/es.xcloc/contents.json index c7d2c05ffa..80cffac8d2 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/es.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "es", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "16.2" }, "version" : "1.0" } \ No newline at end of file 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 cf161efae4..a54666bb10 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -2,36 +2,9 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="fi" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> - <trans-unit id=" " xml:space="preserve"> - <source> -</source> - <target> -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (" xml:space="preserve"> - <source> (</source> - <target> (</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> <target> (voidaan kopioida)</target> @@ -105,6 +78,10 @@ <source>%@ connected</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve"> + <source>%@ downloaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ is connected!" xml:space="preserve"> <source>%@ is connected!</source> <target>%@ on yhdistetty!</target> @@ -120,9 +97,16 @@ <target>%@ on vahvistettu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ server" xml:space="preserve"> + <source>%@ server</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ servers" xml:space="preserve"> <source>%@ servers</source> - <target>%@ palvelimet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve"> + <source>%@ uploaded</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ wants to connect!" xml:space="preserve"> @@ -130,6 +114,10 @@ <target>%@ haluaa muodostaa yhteyden!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve"> + <source>%1$@, %2$@</source> + <note>format for date separator in chat</note> + </trans-unit> <trans-unit id="%@, %@ and %lld members" xml:space="preserve"> <source>%@, %@ and %lld members</source> <note>No comment provided by engineer.</note> @@ -149,11 +137,31 @@ <target>%d päivää</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve"> + <source>%d file(s) are still being downloaded.</source> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve"> + <source>%d file(s) failed to download.</source> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve"> + <source>%d file(s) were deleted.</source> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve"> + <source>%d file(s) were not downloaded.</source> + <note>forward confirmation reason</note> + </trans-unit> <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> <target>%d tuntia</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve"> + <source>%d messages not forwarded</source> + <note>alert title</note> + </trans-unit> <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> <target>%d min</target> @@ -169,6 +177,10 @@ <target>%d sek</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve"> + <source>%d seconds(s)</source> + <note>delete after time</note> + </trans-unit> <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> <target>%d ohitettua viestiä</target> @@ -212,6 +224,10 @@ <source>%lld messages blocked</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve"> + <source>%lld messages blocked by admin</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld messages marked deleted" xml:space="preserve"> <source>%lld messages marked deleted</source> <note>No comment provided by engineer.</note> @@ -230,11 +246,6 @@ <target>%lld uutta käyttöliittymän kieltä</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> - <source>%lld second(s)</source> - <target>%lld sekunti(a)</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="%lld seconds" xml:space="preserve"> <source>%lld seconds</source> <target>%lld sekuntia</target> @@ -285,11 +296,6 @@ <target>%u viestit ohitettu.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> - <source>(</source> - <target>(</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="(new)" xml:space="preserve"> <source>(new)</source> <note>No comment provided by engineer.</note> @@ -298,34 +304,26 @@ <source>(this device v%@)</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve"> - <source>)</source> - <target>)</target> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target>**Lisää uusi kontakti**: luo kertakäyttöinen QR-koodi tai linkki kontaktille.</target> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve"> + <source>**Create group**: to create a new group.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> - <source>**Create link / QR code** for your contact to use.</source> - <target>**Luo linkki / QR-koodi* kontaktille.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Yksityisempi**: tarkista uudet viestit 20 minuutin välein. Laitetunnus jaetaan SimpleX Chat -palvelimen kanssa, mutta ei sitä, kuinka monta yhteystietoa tai viestiä sinulla on.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Yksityisin**: älä käytä SimpleX Chat -ilmoituspalvelinta, tarkista viestit ajoittain taustalla (riippuu siitä, kuinka usein käytät sovellusta).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <target>**Liitä vastaanotettu linkki** tai avaa se selaimessa ja napauta **Avaa mobiilisovelluksessa**.</target> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> @@ -333,14 +331,13 @@ <target>**Huomaa**: et voi palauttaa tai muuttaa tunnuslausetta, jos kadotat sen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Suositus**: laitetunnus ja ilmoitukset lähetetään SimpleX Chat -ilmoituspalvelimelle, mutta ei viestin sisältöä, kokoa tai sitä, keneltä se on peräisin.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target>**Skannaa QR-koodi**: muodosta yhteys kontaktiisi henkilökohtaisesti tai videopuhelun kautta.</target> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve"> + <source>**Scan / Paste link**: to connect via a link you received.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> @@ -348,6 +345,10 @@ <target>**Varoitus**: Välittömät push-ilmoitukset vaativat tunnuslauseen, joka on tallennettu Keychainiin.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve"> + <source>**Warning**: the archive will be removed.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> <source>**e2e encrypted** audio call</source> <target>**e2e-salattu** äänipuhelu</target> @@ -363,11 +364,6 @@ <target>\*bold*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=", " xml:space="preserve"> - <source>, </source> - <target>, </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). @@ -398,11 +394,6 @@ - historian muokkaaminen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="." xml:space="preserve"> - <source>.</source> - <target>.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="0 sec" xml:space="preserve"> <source>0 sec</source> <note>time to disappear</note> @@ -415,7 +406,8 @@ <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> <target>1 päivä</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> @@ -430,16 +422,25 @@ <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> <target>1 kuukausi</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> <target>1 viikko</target> - <note>time interval</note> + <note>delete after time +time interval</note> + </trans-unit> + <trans-unit id="1 year" xml:space="preserve"> + <source>1 year</source> + <note>delete after time</note> </trans-unit> <trans-unit id="1-time link" xml:space="preserve"> <source>1-time link</source> - <target>Kertakäyttölinkki</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="5 minutes" xml:space="preserve"> @@ -457,11 +458,6 @@ <target>30 sekuntia</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve"> - <source>: </source> - <target>: </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> @@ -511,31 +507,29 @@ <target>Keskeytä osoitteenvaihto?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve"> - <source>About SimpleX</source> - <target>Tietoja SimpleX:stä</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve"> <source>About SimpleX Chat</source> <target>Tietoja SimpleX Chatistä</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve"> - <source>About SimpleX address</source> - <target>Tietoja SimpleX osoitteesta</target> + <trans-unit id="About operators" xml:space="preserve"> + <source>About operators</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accent color" xml:space="preserve"> - <source>Accent color</source> - <target>Korostusväri</target> + <trans-unit id="Accent" xml:space="preserve"> + <source>Accent</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept" xml:space="preserve"> <source>Accept</source> <target>Hyväksy</target> <note>accept contact request via notification - accept incoming call via notification</note> +accept incoming call via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accept conditions" xml:space="preserve"> + <source>Accept conditions</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept connection request?" xml:space="preserve"> <source>Accept connection request?</source> @@ -550,16 +544,40 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Hyväksy tuntematon</target> - <note>accept contact request via notification</note> + <note>accept contact request via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve"> + <source>Accepted conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve"> + <source>Acknowledged</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve"> + <source>Acknowledgement errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Active" xml:space="preserve"> + <source>Active</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve"> + <source>Active connections</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> <target>Lisää osoite profiiliisi, jotta kontaktisi voivat jakaa sen muiden kanssa. Profiilipäivitys lähetetään kontakteillesi.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> - <source>Add preset servers</source> - <target>Lisää esiasetettuja palvelimia</target> + <trans-unit id="Add friends" xml:space="preserve"> + <source>Add friends</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve"> + <source>Add list</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add profile" xml:space="preserve"> @@ -567,14 +585,18 @@ <target>Lisää profiili</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> + <target>Lisää palvelin</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> <source>Add servers by scanning QR codes.</source> <target>Lisää palvelimia skannaamalla QR-koodeja.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> - <target>Lisää palvelin…</target> + <trans-unit id="Add team members" xml:space="preserve"> + <source>Add team members</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -582,11 +604,39 @@ <target>Lisää toiseen laitteeseen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add to list" xml:space="preserve"> + <source>Add to list</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> <target>Lisää tervetuloviesti</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve"> + <source>Add your team members to the conversations.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve"> + <source>Added media & file servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve"> + <source>Added message servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve"> + <source>Additional accent</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve"> + <source>Additional accent 2</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve"> + <source>Additional secondary</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Address" xml:space="preserve"> <source>Address</source> <target>Osoite</target> @@ -597,6 +647,18 @@ <target>Osoitteenmuutos keskeytetään. Käytetään vanhaa vastaanotto-osoitetta.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve"> + <source>Address or 1-time link?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve"> + <source>Address settings</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve"> + <source>Admins can block a member for all.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> <source>Admins can create the links to join groups.</source> <target>Ylläpitäjät voivat luoda linkkejä ryhmiin liittymiseen.</target> @@ -607,6 +669,14 @@ <target>Verkon lisäasetukset</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve"> + <source>Advanced settings</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All" xml:space="preserve"> + <source>All</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All app data is deleted." xml:space="preserve"> <source>All app data is deleted.</source> <target>Kaikki sovelluksen tiedot poistetaan.</target> @@ -617,16 +687,32 @@ <target>Kaikki keskustelut ja viestit poistetaan - tätä ei voi kumota!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> <target>Kaikki tiedot poistetaan, kun se syötetään.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve"> + <source>All data is kept private on your device.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> <target>Kaikki ryhmän jäsenet pysyvät yhteydessä.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> <target>Kaikki viestit poistetaan - tätä ei voi kumota! Viestit poistuvat VAIN sinulta.</target> @@ -636,6 +722,18 @@ <source>All new messages from %@ will be hidden!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All profiles" xml:space="preserve"> + <source>All profiles</source> + <note>profile dropdown</note> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve"> + <source>All reports will be archived for you.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All servers" xml:space="preserve"> + <source>All servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All your contacts will remain connected." xml:space="preserve"> <source>All your contacts will remain connected.</source> <target>Kaikki kontaktisi pysyvät yhteydessä.</target> @@ -646,6 +744,10 @@ <target>Kaikki kontaktisi pysyvät yhteydessä. Profiilipäivitys lähetetään kontakteillesi.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow" xml:space="preserve"> <source>Allow</source> <target>Salli</target> @@ -656,14 +758,22 @@ <target>Salli puhelut vain, jos kontaktisi sallii ne.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve"> + <source>Allow calls?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> <target>Salli katoavat viestit vain, jos kontaktisi sallii sen sinulle.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <target>Salli peruuttamaton viestien poisto vain, jos kontaktisi sallii ne sinulle.</target> + <trans-unit id="Allow downgrade" xml:space="preserve"> + <source>Allow downgrade</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target>Salli peruuttamaton viestien poisto vain, jos kontaktisi sallii ne sinulle. (24 tuntia)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> @@ -686,9 +796,21 @@ <target>Salli katoavien viestien lähettäminen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow to irreversibly delete sent messages.</source> - <target>Salli lähetettyjen viestien peruuttamaton poistaminen.</target> + <trans-unit id="Allow sharing" xml:space="preserve"> + <source>Allow sharing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target>Salli lähetettyjen viestien peruuttamaton poistaminen. (24 tuntia)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve"> + <source>Allow to report messsages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve"> + <source>Allow to send SimpleX links.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send files and media." xml:space="preserve"> @@ -721,9 +843,9 @@ <target>Salli kontaktiesi soittaa sinulle.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow your contacts to irreversibly delete sent messages.</source> - <target>Salli kontaktiesi poistaa lähetetyt viestit peruuttamattomasti.</target> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target>Salli kontaktiesi poistaa lähetetyt viestit peruuttamattomasti. (24 tuntia)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> @@ -749,6 +871,10 @@ <source>Already joining the group!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve"> + <source>Always use private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> <source>Always use relay</source> <target>Käytä aina relettä</target> @@ -759,20 +885,37 @@ <target>Luodaan tyhjä chat-profiili annetulla nimellä, ja sovellus avautuu normaalisti.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Another reason" xml:space="preserve"> + <source>Another reason</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> <target>Vastaa puheluun</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> + <target>Avoimen lähdekoodin protokolla ja koodi - kuka tahansa voi käyttää palvelimia.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> <target>Sovellusversio: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App data migration" xml:space="preserve"> + <source>App data migration</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> <source>App encrypts new local files (except videos).</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App group:" xml:space="preserve"> + <source>App group:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>Sovelluksen kuvake</target> @@ -788,6 +931,10 @@ <target>Sovelluksen pääsykoodi korvataan itsetuhoutuvalla pääsykoodilla.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App session" xml:space="preserve"> + <source>App session</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App version" xml:space="preserve"> <source>App version</source> <target>Sovellusversio</target> @@ -803,6 +950,54 @@ <target>Ulkonäkö</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Apply" xml:space="preserve"> + <source>Apply</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve"> + <source>Apply to</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve"> + <source>Archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve"> + <source>Archive %lld reports?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve"> + <source>Archive all reports?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve"> + <source>Archive and upload</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve"> + <source>Archive contacts to chat later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve"> + <source>Archive report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve"> + <source>Archive report?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve"> + <source>Archive reports</source> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve"> + <source>Archived contacts</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve"> + <source>Archiving database</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Attach" xml:space="preserve"> <source>Attach</source> <target>Liitä</target> @@ -863,11 +1058,19 @@ <target>Hyväksy kuvat automaattisesti</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve"> + <source>Auto-accept settings</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Back" xml:space="preserve"> <source>Back</source> <target>Takaisin</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Background" xml:space="preserve"> + <source>Background</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Bad desktop address" xml:space="preserve"> <source>Bad desktop address</source> <note>No comment provided by engineer.</note> @@ -882,19 +1085,59 @@ <target>Virheellinen viestin tarkiste</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better calls" xml:space="preserve"> + <source>Better calls</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better groups" xml:space="preserve"> <source>Better groups</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve"> + <source>Better groups performance</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve"> + <source>Better message dates.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better messages" xml:space="preserve"> <source>Better messages</source> <target>Parempia viestejä</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better networking" xml:space="preserve"> + <source>Better networking</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve"> + <source>Better notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve"> + <source>Better privacy and security</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve"> + <source>Better security ✅</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve"> + <source>Better user experience</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Black" xml:space="preserve"> + <source>Black</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block" xml:space="preserve"> <source>Block</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block for all" xml:space="preserve"> + <source>Block for all</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block group members" xml:space="preserve"> <source>Block group members</source> <note>No comment provided by engineer.</note> @@ -903,18 +1146,34 @@ <source>Block member</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve"> + <source>Block member for all?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block member?" xml:space="preserve"> <source>Block member?</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve"> + <source>Blocked by admin</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve"> + <source>Blur for better privacy.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve"> + <source>Blur media</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <source>Both you and your contact can add message reactions.</source> <target>Sekä sinä että kontaktisi voivat käyttää viestireaktioita.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> - <source>Both you and your contact can irreversibly delete sent messages.</source> - <target>Sekä sinä että kontaktisi voitte peruuttamattomasti poistaa lähetetyt viestit.</target> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target>Sekä sinä että kontaktisi voitte peruuttamattomasti poistaa lähetetyt viestit. (24 tuntia)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> @@ -936,11 +1195,29 @@ <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Business address" xml:space="preserve"> + <source>Business address</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve"> + <source>Business chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve"> + <source>Businesses</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>Chat-profiilin mukaan (oletus) tai [yhteyden mukaan](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve"> + <source>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> <source>Call already ended!</source> <target>Puhelu on jo päättynyt!</target> @@ -951,9 +1228,20 @@ <target>Puhelut</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve"> - <source>Can't delete user profile!</source> - <target>Käyttäjäprofiilia ei voi poistaa!</target> + <trans-unit id="Calls prohibited!" xml:space="preserve"> + <source>Calls prohibited!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve"> + <source>Camera not available</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve"> + <source>Can't call contact</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve"> + <source>Can't call member</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contact!" xml:space="preserve"> @@ -966,9 +1254,18 @@ <target>Kontakteja ei voi kutsua!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve"> + <source>Can't message member</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> <target>Peruuta</target> + <note>alert action +alert button</note> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve"> + <source>Cancel migration</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> @@ -976,9 +1273,21 @@ <target>Ei pääsyä avainnippuun tietokannan salasanan tallentamiseksi</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> <target>Tiedostoa ei voi vastaanottaa</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve"> + <source>Cellular</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change" xml:space="preserve"> @@ -986,6 +1295,14 @@ <target>Muuta</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve"> + <source>Change automatic message deletion?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve"> + <source>Change chat profiles</source> + <note>authentication reason</note> + </trans-unit> <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> <target>Muutetaanko tietokannan tunnuslause?</target> @@ -1030,11 +1347,22 @@ <source>Change self-destruct passcode</source> <target>Vaihda itsetuhoutuva pääsykoodi</target> <note>authentication reason - set passcode view</note> +set passcode view</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> - <source>Chat archive</source> - <target>Chat-arkisto</target> + <trans-unit id="Chat" xml:space="preserve"> + <source>Chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve"> + <source>Chat already exists</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve"> + <source>Chat already exists!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve"> + <source>Chat colors</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat console" xml:space="preserve"> @@ -1052,6 +1380,10 @@ <target>Chat-tietokanta poistettu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve"> + <source>Chat database exported</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> <target>Chat-tietokanta tuotu</target> @@ -1067,26 +1399,71 @@ <target>Chat on pysäytetty</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve"> + <source>Chat list</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve"> + <source>Chat migrated!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> <target>Chat-asetukset</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve"> + <source>Chat preferences were changed.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve"> + <source>Chat profile</source> + <target>Käyttäjäprofiili</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve"> + <source>Chat theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> <target>Keskustelut</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve"> + <source>Check messages every 20 min.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve"> + <source>Check messages when allowed.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> <target>Tarkista palvelimen osoite ja yritä uudelleen.</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> <target>Kiinalainen ja espanjalainen käyttöliittymä</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> <target>Valitse tiedosto</target> @@ -1097,10 +1474,22 @@ <target>Valitse kirjastosta</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve"> + <source>Chunks deleted</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve"> + <source>Chunks downloaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve"> + <source>Chunks uploaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> <target>Tyhjennä</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> @@ -1112,16 +1501,35 @@ <target>Tyhjennä keskustelu?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve"> + <source>Clear group?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve"> + <source>Clear or delete group?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve"> + <source>Clear private notes?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear verification" xml:space="preserve"> <source>Clear verification</source> <target>Tyhjennä vahvistus</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> - <source>Colors</source> - <target>Värit</target> + <trans-unit id="Color chats with the new themes." xml:space="preserve"> + <source>Color chats with the new themes.</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Color mode" xml:space="preserve"> + <source>Color mode</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve"> + <source>Community guidelines violation</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Compare file" xml:space="preserve"> <source>Compare file</source> <target>Vertaa tiedostoa</target> @@ -1132,11 +1540,47 @@ <target>Vertaa turvakoodeja kontaktiesi kanssa.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Completed" xml:space="preserve"> + <source>Completed</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve"> + <source>Conditions accepted on: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve"> + <source>Conditions of use</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve"> + <source>Conditions will be accepted on: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> <target>Määritä ICE-palvelimet</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Configure server operators" xml:space="preserve"> + <source>Configure server operators</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> <target>Vahvista</target> @@ -1147,11 +1591,23 @@ <target>Vahvista pääsykoodi</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve"> + <source>Confirm contact deletion?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm database upgrades" xml:space="preserve"> <source>Confirm database upgrades</source> <target>Vahvista tietokannan päivitykset</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve"> + <source>Confirm files from unknown servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve"> + <source>Confirm network settings</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> <target>Vahvista uusi tunnuslause…</target> @@ -1162,6 +1618,18 @@ <target>Vahvista salasana</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve"> + <source>Confirm upload</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve"> + <source>Confirmed</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> <target>Yhdistä</target> @@ -1180,6 +1648,10 @@ <source>Connect to desktop</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve"> + <source>Connect to your friends faster.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connect to yourself?" xml:space="preserve"> <source>Connect to yourself?</source> <note>No comment provided by engineer.</note> @@ -1203,11 +1675,6 @@ This is your own one-time link!</source> <target>Yhdistä linkin kautta</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve"> - <source>Connect via link / QR code</source> - <target>Yhdistä linkillä / QR-koodilla</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connect via one-time link" xml:space="preserve"> <source>Connect via one-time link</source> <target>Yhdistä kertalinkillä</target> @@ -1217,14 +1684,26 @@ This is your own one-time link!</source> <source>Connect with %@</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected" xml:space="preserve"> + <source>Connected</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected desktop" xml:space="preserve"> <source>Connected desktop</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve"> + <source>Connected servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected to desktop" xml:space="preserve"> <source>Connected to desktop</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting" xml:space="preserve"> + <source>Connecting</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> <source>Connecting to server…</source> <target>Yhteyden muodostaminen palvelimeen…</target> @@ -1235,6 +1714,10 @@ This is your own one-time link!</source> <target>Yhteyden muodostaminen palvelimeen... (virhe: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve"> + <source>Connecting to contact, please wait or check later!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting to desktop" xml:space="preserve"> <source>Connecting to desktop</source> <note>No comment provided by engineer.</note> @@ -1244,6 +1727,14 @@ This is your own one-time link!</source> <target>Yhteys</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve"> + <source>Connection and servers status.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve"> + <source>Connection blocked</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> <target>Yhteysvirhe</target> @@ -1254,11 +1745,32 @@ This is your own one-time link!</source> <target>Yhteysvirhe (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve"> + <source>Connection is blocked by server operator: +%@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve"> + <source>Connection not ready.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve"> + <source>Connection notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Yhteyspyyntö lähetetty!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve"> + <source>Connection requires encryption renegotiation.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve"> + <source>Connection security</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection terminated" xml:space="preserve"> <source>Connection terminated</source> <note>No comment provided by engineer.</note> @@ -1268,6 +1780,14 @@ This is your own one-time link!</source> <target>Yhteyden aikakatkaisu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve"> + <source>Connection with desktop stopped</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve"> + <source>Connections</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact allows" xml:space="preserve"> <source>Contact allows</source> <target>Kontakti sallii</target> @@ -1278,6 +1798,10 @@ This is your own one-time link!</source> <target>Kontakti on jo olemassa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact deleted!" xml:space="preserve"> + <source>Contact deleted!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> <target>Kontakti piilotettu:</target> @@ -1288,9 +1812,8 @@ This is your own one-time link!</source> <target>Kontakti on yhdistetty</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve"> - <source>Contact is not connected yet!</source> - <target>Kontaktia ei ole vielä yhdistetty!</target> + <trans-unit id="Contact is deleted." xml:space="preserve"> + <source>Contact is deleted.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact name" xml:space="preserve"> @@ -1303,6 +1826,10 @@ This is your own one-time link!</source> <target>Kontaktin asetukset</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve"> + <source>Contact will be deleted - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> <target>Kontaktit</target> @@ -1313,21 +1840,37 @@ This is your own one-time link!</source> <target>Kontaktit voivat merkitä viestit poistettaviksi; voit katsella niitä.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve"> + <source>Content violates conditions of use</source> + <note>blocking reason</note> + </trans-unit> <trans-unit id="Continue" xml:space="preserve"> <source>Continue</source> <target>Jatka</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve"> + <source>Conversation deleted!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> <target>Kopioi</target> - <note>chat item action</note> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve"> + <source>Copy error</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> <target>Ydinversio: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Corner" xml:space="preserve"> + <source>Corner</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Correct name to %@?" xml:space="preserve"> <source>Correct name to %@?</source> <note>No comment provided by engineer.</note> @@ -1337,6 +1880,10 @@ This is your own one-time link!</source> <target>Luo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve"> + <source>Create 1-time link</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> <target>Luo SimpleX-osoite</target> @@ -1346,11 +1893,6 @@ This is your own one-time link!</source> <source>Create a group using a random profile.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> - <source>Create an address to let people connect with you.</source> - <target>Luo osoite, jolla ihmiset voivat ottaa sinuun yhteyttä.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create file" xml:space="preserve"> <source>Create file</source> <target>Luo tiedosto</target> @@ -1370,18 +1912,18 @@ This is your own one-time link!</source> <target>Luo linkki</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create list" xml:space="preserve"> + <source>Create list</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> <target>Luo uusi profiili [työpöytäsovelluksessa](https://simplex.chat/downloads/). 💻</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve"> - <source>Create one-time invitation link</source> - <target>Luo kertakutsulinkki</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create profile" xml:space="preserve"> <source>Create profile</source> + <target>Luo profiilisi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create queue" xml:space="preserve"> @@ -1399,9 +1941,24 @@ This is your own one-time link!</source> <target>Luo profiilisi</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve"> - <source>Created on %@</source> - <target>Luotu %@</target> + <trans-unit id="Created" xml:space="preserve"> + <source>Created</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve"> + <source>Created at</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve"> + <source>Created at: %@</source> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve"> + <source>Creating archive link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve"> + <source>Creating link…</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current Passcode" xml:space="preserve"> @@ -1409,11 +1966,19 @@ This is your own one-time link!</source> <target>Nykyinen pääsykoodi</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> <target>Nykyinen tunnuslause…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current profile" xml:space="preserve"> + <source>Current profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> <target>Nykyinen tuettu enimmäistiedostokoko on %@.</target> @@ -1424,11 +1989,23 @@ This is your own one-time link!</source> <target>Mukautettu aika</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve"> + <source>Customizable message shape.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve"> + <source>Customize theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> <target>Tumma</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve"> + <source>Dark mode colors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> <target>Tietokannan tunnus</target> @@ -1527,6 +2104,10 @@ This is your own one-time link!</source> <target>Tietokanta siirretään, kun sovellus käynnistyy uudelleen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Debug delivery" xml:space="preserve"> + <source>Debug delivery</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> <target>Hajautettu</target> @@ -1540,17 +2121,17 @@ This is your own one-time link!</source> <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> <target>Poista</target> - <note>chat item action</note> + <note>alert action +swipe action</note> + </trans-unit> + <trans-unit id="Delete %lld messages of members?" xml:space="preserve"> + <source>Delete %lld messages of members?</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete %lld messages?" xml:space="preserve"> <source>Delete %lld messages?</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve"> - <source>Delete Contact</source> - <target>Poista kontakti</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Delete address" xml:space="preserve"> <source>Delete address</source> <target>Poista osoite</target> @@ -1575,14 +2156,12 @@ This is your own one-time link!</source> <source>Delete and notify contact</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve"> - <source>Delete archive</source> - <target>Poista arkisto</target> + <trans-unit id="Delete chat" xml:space="preserve"> + <source>Delete chat</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve"> - <source>Delete chat archive?</source> - <target>Poista keskusteluarkisto?</target> + <trans-unit id="Delete chat messages from your device." xml:space="preserve"> + <source>Delete chat messages from your device.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile" xml:space="preserve"> @@ -1595,6 +2174,10 @@ This is your own one-time link!</source> <target>Poista keskusteluprofiili?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete chat?" xml:space="preserve"> + <source>Delete chat?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> <source>Delete connection</source> <target>Poista yhteys</target> @@ -1605,9 +2188,8 @@ This is your own one-time link!</source> <target>Poista kontakti</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact? This cannot be undone!" xml:space="preserve"> - <source>Delete contact? -This cannot be undone!</source> + <trans-unit id="Delete contact?" xml:space="preserve"> + <source>Delete contact?</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete database" xml:space="preserve"> @@ -1615,6 +2197,10 @@ This cannot be undone!</source> <target>Poista tietokanta</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete database from this device" xml:space="preserve"> + <source>Delete database from this device</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete file" xml:space="preserve"> <source>Delete file</source> <target>Poista tiedosto</target> @@ -1665,6 +2251,10 @@ This cannot be undone!</source> <target>Poista linkki?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete list?" xml:space="preserve"> + <source>Delete list?</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> <target>Poista jäsenviesti?</target> @@ -1678,7 +2268,7 @@ This cannot be undone!</source> <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> <target>Poista viestit</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> @@ -1695,9 +2285,8 @@ This cannot be undone!</source> <target>Poista vanha tietokanta?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve"> - <source>Delete pending connection</source> - <target>Poista vireillä oleva yhteys</target> + <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> + <source>Delete or moderate up to 200 messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection?" xml:space="preserve"> @@ -1715,11 +2304,27 @@ This cannot be undone!</source> <target>Poista jono</target> <note>server test step</note> </trans-unit> + <trans-unit id="Delete report" xml:space="preserve"> + <source>Delete report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete up to 20 messages at once." xml:space="preserve"> + <source>Delete up to 20 messages at once.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> <target>Poista käyttäjäprofiili?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete without notification" xml:space="preserve"> + <source>Delete without notification</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve"> + <source>Deleted</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Deleted at" xml:space="preserve"> <source>Deleted at</source> <target>Poistettu klo</target> @@ -1730,6 +2335,14 @@ This cannot be undone!</source> <target>Poistettu klo: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve"> + <source>Deletion errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivered even when Apple drops them." xml:space="preserve"> + <source>Delivered even when Apple drops them.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery" xml:space="preserve"> <source>Delivery</source> <target>Toimitus</target> @@ -1762,11 +2375,35 @@ This cannot be undone!</source> <source>Desktop devices</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve"> + <source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server error: %@" xml:space="preserve"> + <source>Destination server error: %@</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve"> + <source>Destination server version of %@ is incompatible with forwarding server %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Detailed statistics" xml:space="preserve"> + <source>Detailed statistics</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Details" xml:space="preserve"> + <source>Details</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Develop" xml:space="preserve"> <source>Develop</source> <target>Kehitä</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Developer options" xml:space="preserve"> + <source>Developer options</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> <target>Kehittäjätyökalut</target> @@ -1797,8 +2434,12 @@ This cannot be undone!</source> <target>Yksityisviestit</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve"> + <source>Direct messages between members are prohibited in this chat.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> <target>Yksityisviestit jäsenten välillä ovat kiellettyjä tässä ryhmässä.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1812,11 +2453,23 @@ This cannot be undone!</source> <target>Poista SimpleX Lock käytöstä</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Disable automatic message deletion?" xml:space="preserve"> + <source>Disable automatic message deletion?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Disable delete messages" xml:space="preserve"> + <source>Disable delete messages</source> + <note>alert button</note> + </trans-unit> <trans-unit id="Disable for all" xml:space="preserve"> <source>Disable for all</source> <target>Poista käytöstä kaikilta</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Disabled" xml:space="preserve"> + <source>Disabled</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> <source>Disappearing message</source> <target>Tuhoutuva viesti</target> @@ -1832,8 +2485,8 @@ This cannot be undone!</source> <target>Katoavat viestit ovat kiellettyjä tässä keskustelussa.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <target>Katoavat viestit ovat kiellettyjä tässä ryhmässä.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1865,16 +2518,36 @@ This cannot be undone!</source> <source>Discover via local network</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve"> + <source>Do NOT send messages directly, even if your or destination server does not support private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> <source>Do NOT use SimpleX for emergency calls.</source> <target>Älä käytä SimpleX-sovellusta hätäpuheluihin.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT use private routing." xml:space="preserve"> + <source>Do NOT use private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do it later" xml:space="preserve"> <source>Do it later</source> <target>Tee myöhemmin</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do not send history to new members." xml:space="preserve"> + <source>Do not send history to new members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not use credentials with proxy." xml:space="preserve"> + <source>Do not use credentials with proxy.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Documents:" xml:space="preserve"> + <source>Documents:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> <target>Älä luo osoitetta</target> @@ -1885,21 +2558,62 @@ This cannot be undone!</source> <target>Älä salli</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Don't miss important messages." xml:space="preserve"> + <source>Don't miss important messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> <target>Älä näytä uudelleen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Done" xml:space="preserve"> + <source>Done</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Downgrade and open chat" xml:space="preserve"> <source>Downgrade and open chat</source> <target>Alenna ja avaa keskustelu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Download" xml:space="preserve"> + <source>Download</source> + <note>alert button +chat item action</note> + </trans-unit> + <trans-unit id="Download errors" xml:space="preserve"> + <source>Download errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download failed" xml:space="preserve"> + <source>Download failed</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Download file" xml:space="preserve"> <source>Download file</source> <target>Lataa tiedosto</target> <note>server test step</note> </trans-unit> + <trans-unit id="Download files" xml:space="preserve"> + <source>Download files</source> + <note>alert action</note> + </trans-unit> + <trans-unit id="Downloaded" xml:space="preserve"> + <source>Downloaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloaded files" xml:space="preserve"> + <source>Downloaded files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading archive" xml:space="preserve"> + <source>Downloading archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading link details" xml:space="preserve"> + <source>Downloading link details</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> <target>Päällekkäinen näyttönimi!</target> @@ -1910,6 +2624,10 @@ This cannot be undone!</source> <target>Kesto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="E2E encrypted notifications." xml:space="preserve"> + <source>E2E encrypted notifications.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> <target>Muokkaa</target> @@ -1930,6 +2648,10 @@ This cannot be undone!</source> <target>Salli (pidä ohitukset)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve"> + <source>Enable Flux in Network & servers settings for better metadata privacy.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> <target>Ota SimpleX Lock käyttöön</target> @@ -1943,6 +2665,10 @@ This cannot be undone!</source> <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> <target>Ota automaattinen viestien poisto käyttöön?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Enable camera access" xml:space="preserve"> + <source>Enable camera access</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable for all" xml:space="preserve"> @@ -1950,6 +2676,10 @@ This cannot be undone!</source> <target>Salli kaikille</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve"> + <source>Enable in direct chats (BETA)!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> <target>Salli välittömät ilmoitukset?</target> @@ -1980,6 +2710,14 @@ This cannot be undone!</source> <target>Ota itsetuhoava pääsykoodi käyttöön</target> <note>set passcode view</note> </trans-unit> + <trans-unit id="Enabled" xml:space="preserve"> + <source>Enabled</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve"> + <source>Enabled for</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> <target>Salaa</target> @@ -2009,6 +2747,10 @@ This cannot be undone!</source> <target>Salattu viesti tai muu tapahtuma</target> <note>notification</note> </trans-unit> + <trans-unit id="Encrypted message: app is stopped" xml:space="preserve"> + <source>Encrypted message: app is stopped</source> + <note>notification</note> + </trans-unit> <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> <target>Salattu viesti: tietokantavirhe</target> @@ -2042,6 +2784,10 @@ This cannot be undone!</source> <source>Encryption re-negotiation failed.</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encryption renegotiation in progress." xml:space="preserve"> + <source>Encryption renegotiation in progress.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter Passcode" xml:space="preserve"> <source>Enter Passcode</source> <target>Syötä pääsykoodi</target> @@ -2056,6 +2802,10 @@ This cannot be undone!</source> <source>Enter group name…</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve"> + <source>Enter passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter passphrase…" xml:space="preserve"> <source>Enter passphrase…</source> <target>Syötä tunnuslause…</target> @@ -2099,26 +2849,33 @@ This cannot be undone!</source> <target>Virhe osoitteenmuutoksen keskeytyksessä</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error accepting conditions" xml:space="preserve"> + <source>Error accepting conditions</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> <target>Virhe kontaktipyynnön hyväksymisessä</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve"> - <source>Error accessing database file</source> - <target>Virhe tietokantatiedoston käyttämisessä</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> <target>Virhe lisättäessä jäseniä</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error adding server" xml:space="preserve"> + <source>Error adding server</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> <target>Virhe osoitteenvaihdossa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing connection profile" xml:space="preserve"> + <source>Error changing connection profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error changing role" xml:space="preserve"> <source>Error changing role</source> <target>Virhe roolin vaihdossa</target> @@ -2129,6 +2886,18 @@ This cannot be undone!</source> <target>Virhe asetuksen muuttamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing to incognito!" xml:space="preserve"> + <source>Error changing to incognito!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error checking token status" xml:space="preserve"> + <source>Error checking token status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve"> + <source>Error connecting to forwarding server %@. Please try later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> <target>Virhe osoitteen luomisessa</target> @@ -2144,15 +2913,27 @@ This cannot be undone!</source> <target>Virhe ryhmälinkin luomisessa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating list" xml:space="preserve"> + <source>Error creating list</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error creating member contact" xml:space="preserve"> <source>Error creating member contact</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating message" xml:space="preserve"> + <source>Error creating message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>Virhe profiilin luomisessa!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating report" xml:space="preserve"> + <source>Error creating report</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error decrypting file" xml:space="preserve"> <source>Error decrypting file</source> <target>Virhe tiedoston salauksen purussa</target> @@ -2173,11 +2954,6 @@ This cannot be undone!</source> <target>Virhe yhteyden poistamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve"> - <source>Error deleting contact</source> - <target>Virhe kontaktin poistamisessa</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> <target>Virhe tietokannan poistamisessa</target> @@ -2198,6 +2974,10 @@ This cannot be undone!</source> <target>Virhe käyttäjäprofiilin poistamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error downloading the archive" xml:space="preserve"> + <source>Error downloading the archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> <source>Error enabling delivery receipts!</source> <target>Virhe toimituskuittauksien sallimisessa!</target> @@ -2218,6 +2998,10 @@ This cannot be undone!</source> <target>Virhe vietäessä keskustelujen tietokantaa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error exporting theme: %@" xml:space="preserve"> + <source>Error exporting theme: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> <target>Virhe keskustelujen tietokannan tuonnissa</target> @@ -2228,24 +3012,46 @@ This cannot be undone!</source> <target>Virhe ryhmään liittymisessä</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve"> - <source>Error loading %@ servers</source> - <target>Virhe %@-palvelimien lataamisessa</target> + <trans-unit id="Error loading servers" xml:space="preserve"> + <source>Error loading servers</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error migrating settings" xml:space="preserve"> + <source>Error migrating settings</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error opening chat" xml:space="preserve"> + <source>Error opening chat</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> <target>Virhe tiedoston vastaanottamisessa</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error reconnecting server" xml:space="preserve"> + <source>Error reconnecting server</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error reconnecting servers" xml:space="preserve"> + <source>Error reconnecting servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error registering for notifications" xml:space="preserve"> + <source>Error registering for notifications</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> <target>Virhe poistettaessa jäsentä</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve"> - <source>Error saving %@ servers</source> - <target>Virhe %@ palvelimien tallentamisessa</target> + <trans-unit id="Error reordering lists" xml:space="preserve"> + <source>Error reordering lists</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error resetting statistics" xml:space="preserve"> + <source>Error resetting statistics</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving ICE servers" xml:space="preserve"> @@ -2253,6 +3059,10 @@ This cannot be undone!</source> <target>Virhe ICE-palvelimien tallentamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving chat list" xml:space="preserve"> + <source>Error saving chat list</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> <target>Virhe ryhmäprofiilin tallentamisessa</target> @@ -2268,11 +3078,23 @@ This cannot be undone!</source> <target>Virhe tunnuslauseen tallentamisessa avainnippuun</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving servers" xml:space="preserve"> + <source>Error saving servers</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error saving settings" xml:space="preserve"> + <source>Error saving settings</source> + <note>when migrating</note> + </trans-unit> <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> <target>Virhe käyttäjän salasanan tallentamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error scanning code: %@" xml:space="preserve"> + <source>Error scanning code: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending email" xml:space="preserve"> <source>Error sending email</source> <target>Virhe sähköpostin lähettämisessä</target> @@ -2302,16 +3124,24 @@ This cannot be undone!</source> <target>Virhe keskustelun lopettamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error switching profile" xml:space="preserve"> + <source>Error switching profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> <target>Virhe profiilin vaihdossa!</target> - <note>No comment provided by engineer.</note> + <note>alertTitle</note> </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> <target>Virhe yhteyden synkronoinnissa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error testing server connection" xml:space="preserve"> + <source>Error testing server connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> <target>Virhe ryhmälinkin päivittämisessä</target> @@ -2322,6 +3152,10 @@ This cannot be undone!</source> <target>Virhe viestin päivityksessä</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error updating server" xml:space="preserve"> + <source>Error updating server</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> <target>Virhe asetusten päivittämisessä</target> @@ -2332,6 +3166,14 @@ This cannot be undone!</source> <target>Virhe päivitettäessä käyttäjän tietosuojaa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error uploading the archive" xml:space="preserve"> + <source>Error uploading the archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error verifying passphrase:" xml:space="preserve"> + <source>Error verifying passphrase:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error: " xml:space="preserve"> <source>Error: </source> <target>Virhe: </target> @@ -2340,7 +3182,9 @@ This cannot be undone!</source> <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> <target>Virhe: %@</target> - <note>No comment provided by engineer.</note> + <note>alert message +file error text +snd error text</note> </trans-unit> <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> @@ -2352,6 +3196,14 @@ This cannot be undone!</source> <target>Virhe: ei tietokantatiedostoa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Errors" xml:space="preserve"> + <source>Errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors in servers configuration." xml:space="preserve"> + <source>Errors in servers configuration.</source> + <note>servers error</note> + </trans-unit> <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> <source>Even when disabled in the conversation.</source> <target>Jopa kun ei käytössä keskustelussa.</target> @@ -2366,6 +3218,10 @@ This cannot be undone!</source> <source>Expand</source> <note>chat item action</note> </trans-unit> + <trans-unit id="Expired" xml:space="preserve"> + <source>Expired</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> <target>Vie tietokanta</target> @@ -2376,11 +3232,19 @@ This cannot be undone!</source> <target>Vientivirhe:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Export theme" xml:space="preserve"> + <source>Export theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> <target>Viety tietokanta-arkisto.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Exported file doesn't exist" xml:space="preserve"> + <source>Exported file doesn't exist</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exporting database archive…" xml:space="preserve"> <source>Exporting database archive…</source> <target>Tietokanta-arkiston vienti…</target> @@ -2396,15 +3260,57 @@ This cannot be undone!</source> <target>Nopea ja ei odotusta, kunnes lähettäjä on online-tilassa!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster deletion of groups." xml:space="preserve"> + <source>Faster deletion of groups.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Faster joining and more reliable messages." xml:space="preserve"> <source>Faster joining and more reliable messages.</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster sending messages." xml:space="preserve"> + <source>Faster sending messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Favorite" xml:space="preserve"> <source>Favorite</source> <target>Suosikki</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Favorites" xml:space="preserve"> + <source>Favorites</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <note>file error alert title</note> + </trans-unit> + <trans-unit id="File errors: %@" xml:space="preserve"> + <source>File errors: +%@</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="File is blocked by server operator: %@." xml:space="preserve"> + <source>File is blocked by server operator: +%@.</source> + <note>file error text</note> + </trans-unit> + <trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve"> + <source>File not found - most likely file was deleted or cancelled.</source> + <note>file error text</note> + </trans-unit> + <trans-unit id="File server error: %@" xml:space="preserve"> + <source>File server error: %@</source> + <note>file error text</note> + </trans-unit> + <trans-unit id="File status" xml:space="preserve"> + <source>File status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File status: %@" xml:space="preserve"> + <source>File status: %@</source> + <note>copied message info</note> + </trans-unit> <trans-unit id="File will be deleted from servers." xml:space="preserve"> <source>File will be deleted from servers.</source> <target>Tiedosto poistetaan palvelimilta.</target> @@ -2425,6 +3331,10 @@ This cannot be undone!</source> <target>Tiedosto: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files" xml:space="preserve"> + <source>Files</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> <target>Tiedostot & media</target> @@ -2435,11 +3345,15 @@ This cannot be undone!</source> <target>Tiedostot ja media</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> - <source>Files and media are prohibited in this group.</source> + <trans-unit id="Files and media are prohibited." xml:space="preserve"> + <source>Files and media are prohibited.</source> <target>Tiedostot ja media ovat tässä ryhmässä kiellettyjä.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files and media not allowed" xml:space="preserve"> + <source>Files and media not allowed</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files and media prohibited!" xml:space="preserve"> <source>Files and media prohibited!</source> <target>Tiedostot ja media kielletty!</target> @@ -2450,6 +3364,14 @@ This cannot be undone!</source> <target>Suodata lukemattomia- ja suosikkikeskusteluja.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Finalize migration" xml:space="preserve"> + <source>Finalize migration</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration on another device." xml:space="preserve"> + <source>Finalize migration on another device.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> <target>Vihdoinkin meillä! 🚀</target> @@ -2490,11 +3412,93 @@ This cannot be undone!</source> <target>Ryhmän jäsen ei tue korjausta</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For all moderators" xml:space="preserve"> + <source>For all moderators</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For chat profile %@:" xml:space="preserve"> + <source>For chat profile %@:</source> + <note>servers error</note> + </trans-unit> <trans-unit id="For console" xml:space="preserve"> <source>For console</source> <target>Konsoliin</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." xml:space="preserve"> + <source>For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For me" xml:space="preserve"> + <source>For me</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For private routing" xml:space="preserve"> + <source>For private routing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For social media" xml:space="preserve"> + <source>For social media</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward" xml:space="preserve"> + <source>Forward</source> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Forward %d message(s)?" xml:space="preserve"> + <source>Forward %d message(s)?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Forward and save messages" xml:space="preserve"> + <source>Forward and save messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward messages" xml:space="preserve"> + <source>Forward messages</source> + <note>alert action</note> + </trans-unit> + <trans-unit id="Forward messages without files?" xml:space="preserve"> + <source>Forward messages without files?</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> + <source>Forward up to 20 messages at once.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded" xml:space="preserve"> + <source>Forwarded</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded from" xml:space="preserve"> + <source>Forwarded from</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding %lld messages" xml:space="preserve"> + <source>Forwarding %lld messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve"> + <source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server address is incompatible with network settings: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server version is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server version is incompatible with network settings: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Destination server error: %2$@</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Error: %2$@</source> + <note>snd error text</note> + </trans-unit> <trans-unit id="Found desktop" xml:space="preserve"> <source>Found desktop</source> <note>No comment provided by engineer.</note> @@ -2514,11 +3518,6 @@ This cannot be undone!</source> <target>Koko nimi (valinnainen)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full name:" xml:space="preserve"> - <source>Full name:</source> - <target>Koko nimi:</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Fully decentralized – visible only to members." xml:space="preserve"> <source>Fully decentralized – visible only to members.</source> <note>No comment provided by engineer.</note> @@ -2538,6 +3537,18 @@ This cannot be undone!</source> <target>GIFit ja tarrat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Get notified when mentioned." xml:space="preserve"> + <source>Get notified when mentioned.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Good afternoon!" xml:space="preserve"> + <source>Good afternoon!</source> + <note>message preview</note> + </trans-unit> + <trans-unit id="Good morning!" xml:space="preserve"> + <source>Good morning!</source> + <note>message preview</note> + </trans-unit> <trans-unit id="Group" xml:space="preserve"> <source>Group</source> <target>Ryhmä</target> @@ -2591,36 +3602,6 @@ This cannot be undone!</source> <target>Ryhmälinkit</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve"> - <source>Group members can add message reactions.</source> - <target>Ryhmän jäsenet voivat lisätä viestireaktioita.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> - <source>Group members can irreversibly delete sent messages.</source> - <target>Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> - <target>Ryhmän jäsenet voivat lähettää suoraviestejä.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> - <target>Ryhmän jäsenet voivat lähettää katoavia viestejä.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send files and media." xml:space="preserve"> - <source>Group members can send files and media.</source> - <target>Ryhmän jäsenet voivat lähettää tiedostoja ja mediaa.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> - <target>Ryhmän jäsenet voivat lähettää ääniviestejä.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> <target>Ryhmäviesti:</target> @@ -2661,11 +3642,19 @@ This cannot be undone!</source> <target>Ryhmä poistetaan sinulta - tätä ei voi perua!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Groups" xml:space="preserve"> + <source>Groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Help" xml:space="preserve"> <source>Help</source> <target>Apua</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Help admins moderating their groups." xml:space="preserve"> + <source>Help admins moderating their groups.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> <target>Piilotettu</target> @@ -2706,15 +3695,26 @@ This cannot be undone!</source> <target>Historia</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="History is not sent to new members." xml:space="preserve"> + <source>History is not sent to new members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> <target>Miten SimpleX toimii</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="How it affects privacy" xml:space="preserve"> + <source>How it affects privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it helps privacy" xml:space="preserve"> + <source>How it helps privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> - <target>Kuinka se toimii</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="How to" xml:space="preserve"> <source>How to</source> @@ -2731,21 +3731,24 @@ This cannot be undone!</source> <target>Miten käytät palvelimiasi</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Hungarian interface" xml:space="preserve"> + <source>Hungarian interface</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> <target>ICE-palvelimet (yksi per rivi)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="IP address" xml:space="preserve"> + <source>IP address</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> <source>If you can't meet in person, show QR code in a video call, or share the link.</source> <target>Jos et voi tavata henkilökohtaisesti, näytä QR-koodi videopuhelussa tai jaa linkki.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <target>Jos et voi tavata henkilökohtaisesti, voit **skannata QR-koodin videopuhelussa** tai kontaktisi voi jakaa kutsulinkin.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> <target>Jos syötät tämän pääsykoodin sovellusta avatessasi, kaikki sovelluksen tiedot poistetaan peruuttamattomasti!</target> @@ -2781,8 +3784,8 @@ This cannot be undone!</source> <target>Heti</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <target>Immuuni roskapostille ja väärinkäytöksille</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2801,6 +3804,27 @@ This cannot be undone!</source> <target>Tuo tietokanta</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Import failed" xml:space="preserve"> + <source>Import failed</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import theme" xml:space="preserve"> + <source>Import theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Importing archive" xml:space="preserve"> + <source>Importing archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved delivery, reduced traffic usage. More improvements are coming soon!" xml:space="preserve"> + <source>Improved delivery, reduced traffic usage. +More improvements are coming soon!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved message delivery" xml:space="preserve"> + <source>Improved message delivery</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> <target>Parannettu yksityisyys ja turvallisuus</target> @@ -2811,11 +3835,27 @@ This cannot be undone!</source> <target>Parannettu palvelimen kokoonpano</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In order to continue, chat should be stopped." xml:space="preserve"> + <source>In order to continue, chat should be stopped.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>Vastauksena</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In-call sounds" xml:space="preserve"> + <source>In-call sounds</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Inappropriate content" xml:space="preserve"> + <source>Inappropriate content</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Inappropriate profile" xml:space="preserve"> + <source>Inappropriate profile</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> <target>Incognito</target> @@ -2884,6 +3924,11 @@ This cannot be undone!</source> <target>Asenna [SimpleX Chat terminaalille](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Instant" xml:space="preserve"> + <source>Instant</source> + <target>Heti</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> @@ -2891,29 +3936,68 @@ This cannot be undone!</source> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Instantly" xml:space="preserve"> - <source>Instantly</source> - <target>Heti</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> <target>Käyttöliittymä</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Interface colors" xml:space="preserve"> + <source>Interface colors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid" xml:space="preserve"> + <source>Invalid</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (bad token)" xml:space="preserve"> + <source>Invalid (bad token)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (expired)" xml:space="preserve"> + <source>Invalid (expired)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (unregistered)" xml:space="preserve"> + <source>Invalid (unregistered)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (wrong topic)" xml:space="preserve"> + <source>Invalid (wrong topic)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid QR code" xml:space="preserve"> + <source>Invalid QR code</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> <target>Virheellinen yhteyslinkki</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid display name!" xml:space="preserve"> + <source>Invalid display name!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid link" xml:space="preserve"> + <source>Invalid link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid name!" xml:space="preserve"> <source>Invalid name!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid response" xml:space="preserve"> + <source>Invalid response</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> <target>Virheellinen palvelinosoite!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Invalid status" xml:space="preserve"> <source>Invalid status</source> @@ -2935,6 +4019,10 @@ This cannot be undone!</source> <target>Kutsu jäseniä</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invite to chat" xml:space="preserve"> + <source>Invite to chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invite to group" xml:space="preserve"> <source>Invite to group</source> <target>Kutsu ryhmään</target> @@ -2950,8 +4038,8 @@ This cannot be undone!</source> <target>Viestien peruuttamaton poisto on kielletty tässä keskustelussa.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <target>Viestien peruuttamaton poisto on kielletty tässä ryhmässä.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2976,6 +4064,10 @@ This cannot be undone!</source> 3. Yhteys vaarantui.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="It protects your IP address and connections." xml:space="preserve"> + <source>It protects your IP address and connections.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> <target>Näyttäisi, että olet jo yhteydessä tämän linkin kautta. Jos näin ei ole, tapahtui virhe (%@).</target> @@ -2994,13 +4086,17 @@ This cannot be undone!</source> <trans-unit id="Join" xml:space="preserve"> <source>Join</source> <target>Liity</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> <target>Liity ryhmään</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Join group conversations" xml:space="preserve"> + <source>Join group conversations</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Join group?" xml:space="preserve"> <source>Join group?</source> <note>No comment provided by engineer.</note> @@ -3024,10 +4120,22 @@ This is your link for group %@!</source> <target>Liittyy ryhmään</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep" xml:space="preserve"> + <source>Keep</source> + <note>alert action</note> + </trans-unit> + <trans-unit id="Keep conversation" xml:space="preserve"> + <source>Keep conversation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Keep the app open to use it from desktop" xml:space="preserve"> <source>Keep the app open to use it from desktop</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep unused invitation?" xml:space="preserve"> + <source>Keep unused invitation?</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> <source>Keep your connections</source> <target>Pidä kontaktisi</target> @@ -3061,6 +4169,14 @@ This is your link for group %@!</source> <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> <target>Poistu</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Leave chat" xml:space="preserve"> + <source>Leave chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave chat?" xml:space="preserve"> + <source>Leave chat?</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group" xml:space="preserve"> @@ -3100,6 +4216,18 @@ This is your link for group %@!</source> <source>Linked desktops</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="List" xml:space="preserve"> + <source>List</source> + <note>swipe action</note> + </trans-unit> + <trans-unit id="List name and emoji should be different for all lists." xml:space="preserve"> + <source>List name and emoji should be different for all lists.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List name..." xml:space="preserve"> + <source>List name...</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Live message!" xml:space="preserve"> <source>Live message!</source> <target>Live-viesti!</target> @@ -3130,11 +4258,6 @@ This is your link for group %@!</source> <target>Lukitustila</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> - <source>Make a private connection</source> - <target>Luo yksityinen yhteys</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make one message disappear" xml:space="preserve"> <source>Make one message disappear</source> <target>Hävitä yksi viesti</target> @@ -3145,21 +4268,11 @@ This is your link for group %@!</source> <target>Tee profiilista yksityinen!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> - <target>Varmista, että %@-palvelinosoitteet ovat oikeassa muodossa, että ne on erotettu toisistaan riveittäin ja että ne eivät ole päällekkäisiä (%@).</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> <target>Varmista, että WebRTC ICE -palvelinosoitteet ovat oikeassa muodossa, rivieroteltuina ja että ne eivät ole päällekkäisiä.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <target>Monet ihmiset kysyivät: *Jos SimpleX:llä ei ole käyttäjätunnuksia, miten se voi toimittaa viestejä?*</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> <source>Mark deleted for everyone</source> <target>Merkitse poistetuksi kaikilta</target> @@ -3185,11 +4298,31 @@ This is your link for group %@!</source> <target>Enintään 30 sekuntia, vastaanotetaan välittömästi.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Media & file servers" xml:space="preserve"> + <source>Media & file servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Medium" xml:space="preserve"> + <source>Medium</source> + <note>blur media</note> + </trans-unit> <trans-unit id="Member" xml:space="preserve"> <source>Member</source> <target>Jäsen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member inactive" xml:space="preserve"> + <source>Member inactive</source> + <note>item status text</note> + </trans-unit> + <trans-unit id="Member reports" xml:space="preserve"> + <source>Member reports</source> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All chat members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All chat members will be notified.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> <target>Jäsenen rooli muuttuu muotoon "%@". Kaikille ryhmän jäsenille ilmoitetaan asiasta.</target> @@ -3200,11 +4333,61 @@ This is your link for group %@!</source> <target>Jäsenen rooli muutetaan muotoon "%@". Jäsen saa uuden kutsun.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from chat - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> <target>Jäsen poistetaan ryhmästä - tätä ei voi perua!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Members can add message reactions." xml:space="preserve"> + <source>Members can add message reactions.</source> + <target>Ryhmän jäsenet voivat lisätä viestireaktioita.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Members can irreversibly delete sent messages. (24 hours)</source> + <target>Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti. (24 tuntia)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can report messsages to moderators." xml:space="preserve"> + <source>Members can report messsages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send SimpleX links." xml:space="preserve"> + <source>Members can send SimpleX links.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> + <target>Ryhmän jäsenet voivat lähettää suoraviestejä.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> + <target>Ryhmän jäsenet voivat lähettää katoavia viestejä.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send files and media." xml:space="preserve"> + <source>Members can send files and media.</source> + <target>Ryhmän jäsenet voivat lähettää tiedostoja ja mediaa.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> + <target>Ryhmän jäsenet voivat lähettää ääniviestejä.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mention members 👋" xml:space="preserve"> + <source>Mention members 👋</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Menus" xml:space="preserve"> + <source>Menus</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>Viestin toimitusvirhe</target> @@ -3215,11 +4398,27 @@ This is your link for group %@!</source> <target>Viestien toimituskuittaukset!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message delivery warning" xml:space="preserve"> + <source>Message delivery warning</source> + <note>item status text</note> + </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> <target>Viestiluonnos</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message forwarded" xml:space="preserve"> + <source>Message forwarded</source> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message may be delivered later if member becomes active." xml:space="preserve"> + <source>Message may be delivered later if member becomes active.</source> + <note>item status description</note> + </trans-unit> + <trans-unit id="Message queue info" xml:space="preserve"> + <source>Message queue info</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message reactions" xml:space="preserve"> <source>Message reactions</source> <target>Viestireaktiot</target> @@ -3230,16 +4429,44 @@ This is your link for group %@!</source> <target>Viestireaktiot ovat kiellettyjä tässä keskustelussa.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> - <source>Message reactions are prohibited in this group.</source> + <trans-unit id="Message reactions are prohibited." xml:space="preserve"> + <source>Message reactions are prohibited.</source> <target>Viestireaktiot ovat kiellettyjä tässä ryhmässä.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message reception" xml:space="preserve"> + <source>Message reception</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message servers" xml:space="preserve"> + <source>Message servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message shape" xml:space="preserve"> + <source>Message shape</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message source remains private." xml:space="preserve"> + <source>Message source remains private.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status" xml:space="preserve"> + <source>Message status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status: %@" xml:space="preserve"> + <source>Message status: %@</source> + <note>copied message info</note> + </trans-unit> <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> <target>Viestin teksti</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message too large" xml:space="preserve"> + <source>Message too large</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> <target>Viestit</target> @@ -3254,11 +4481,63 @@ This is your link for group %@!</source> <source>Messages from %@ will be shown!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Messages in this chat will never be deleted." xml:space="preserve"> + <source>Messages in this chat will never be deleted.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages received" xml:space="preserve"> + <source>Messages received</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages sent" xml:space="preserve"> + <source>Messages sent</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages were deleted after you selected them." xml:space="preserve"> + <source>Messages were deleted after you selected them.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate device" xml:space="preserve"> + <source>Migrate device</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate from another device" xml:space="preserve"> + <source>Migrate from another device</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate here" xml:space="preserve"> + <source>Migrate here</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device" xml:space="preserve"> + <source>Migrate to another device</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device via QR code." xml:space="preserve"> + <source>Migrate to another device via QR code.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating" xml:space="preserve"> + <source>Migrating</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migrating database archive…" xml:space="preserve"> <source>Migrating database archive…</source> <target>Siirretään tietokannan arkistoa…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Migration complete" xml:space="preserve"> + <source>Migration complete</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> <target>Siirtovirhe:</target> @@ -3274,9 +4553,9 @@ This is your link for group %@!</source> <target>Siirto on valmis</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> - <target>Siirrot: %@</target> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> + <target>Siirrot:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> @@ -3294,21 +4573,28 @@ This is your link for group %@!</source> <target>Moderoitu klo: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="More" xml:space="preserve"> + <source>More</source> + <note>swipe action</note> + </trans-unit> <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> <target>Lisää parannuksia on tulossa pian!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="More reliable network connection." xml:space="preserve"> + <source>More reliable network connection.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="More reliable notifications" xml:space="preserve"> + <source>More reliable notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> <source>Most likely this connection is deleted.</source> <target>Todennäköisesti tämä yhteys on poistettu.</target> <note>item status description</note> </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> - <source>Most likely this contact has deleted the connection with you.</source> - <target>Todennäköisesti tämä kontakti on poistanut yhteyden sinuun.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> <target>Useita keskusteluprofiileja</target> @@ -3317,7 +4603,11 @@ This is your link for group %@!</source> <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> <target>Mykistä</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Mute all" xml:space="preserve"> + <source>Mute all</source> + <note>notification label action</note> </trans-unit> <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> @@ -3327,13 +4617,33 @@ This is your link for group %@!</source> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> <target>Nimi</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> <target>Verkko ja palvelimet</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Network connection" xml:space="preserve"> + <source>Network connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network decentralization" xml:space="preserve"> + <source>Network decentralization</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve"> + <source>Network issues - message expired after many attempts to send it.</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Network management" xml:space="preserve"> + <source>Network management</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network operator" xml:space="preserve"> + <source>Network operator</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> <target>Verkkoasetukset</target> @@ -3344,11 +4654,31 @@ This is your link for group %@!</source> <target>Verkon tila</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New" xml:space="preserve"> + <source>New</source> + <note>token status text</note> + </trans-unit> <trans-unit id="New Passcode" xml:space="preserve"> <source>New Passcode</source> <target>Uusi pääsykoodi</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve"> + <source>New SOCKS credentials will be used every time you start the app.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve"> + <source>New SOCKS credentials will be used for each server.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat" xml:space="preserve"> + <source>New chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat experience 🎉" xml:space="preserve"> + <source>New chat experience 🎉</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> <target>Uusi kontaktipyyntö</target> @@ -3359,11 +4689,6 @@ This is your link for group %@!</source> <target>Uusi kontakti:</target> <note>notification</note> </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> - <source>New database archive</source> - <target>Uusi tietokanta-arkisto</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="New desktop app!" xml:space="preserve"> <source>New desktop app!</source> <note>No comment provided by engineer.</note> @@ -3373,11 +4698,19 @@ This is your link for group %@!</source> <target>Uusi näyttönimi</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <note>notification</note> + </trans-unit> <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> <target>Uutta %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New media options" xml:space="preserve"> + <source>New media options</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> <target>Uusi jäsenrooli</target> @@ -3393,6 +4726,10 @@ This is your link for group %@!</source> <target>Uusi tunnuslause…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New server" xml:space="preserve"> + <source>New server</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No" xml:space="preserve"> <source>No</source> <target>Ei</target> @@ -3403,6 +4740,18 @@ This is your link for group %@!</source> <target>Ei sovelluksen salasanaa</target> <note>Authentication unavailable</note> </trans-unit> + <trans-unit id="No chats" xml:space="preserve"> + <source>No chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats found" xml:space="preserve"> + <source>No chats found</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats in list %@" xml:space="preserve"> + <source>No chats in list %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> <target>Kontakteja ei ole valittu</target> @@ -3423,6 +4772,10 @@ This is your link for group %@!</source> <target>Ei laitetunnusta!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No direct connection yet, message is forwarded by admin." xml:space="preserve"> + <source>No direct connection yet, message is forwarded by admin.</source> + <note>item status description</note> + </trans-unit> <trans-unit id="No filtered chats" xml:space="preserve"> <source>No filtered chats</source> <target>Ei suodatettuja keskusteluja</target> @@ -3438,20 +4791,94 @@ This is your link for group %@!</source> <target>Ei historiaa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No info, try to reload" xml:space="preserve"> + <source>No info, try to reload</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No media & file servers." xml:space="preserve"> + <source>No media & file servers.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No message" xml:space="preserve"> + <source>No message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No message servers." xml:space="preserve"> + <source>No message servers.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No network connection" xml:space="preserve"> + <source>No network connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record speech" xml:space="preserve"> + <source>No permission to record speech</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record video" xml:space="preserve"> + <source>No permission to record video</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> <source>No permission to record voice message</source> <target>Ei lupaa ääniviestin tallentamiseen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No push server" xml:space="preserve"> + <source>No push server</source> + <target>Paikallinen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> <target>Ei vastaanotettuja tai lähetettyjä tiedostoja</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No servers for private message routing." xml:space="preserve"> + <source>No servers for private message routing.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive files." xml:space="preserve"> + <source>No servers to receive files.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive messages." xml:space="preserve"> + <source>No servers to receive messages.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to send files." xml:space="preserve"> + <source>No servers to send files.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No token!" xml:space="preserve"> + <source>No token!</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="No unread chats" xml:space="preserve"> + <source>No unread chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> + <target>Ensimmäinen alusta ilman käyttäjätunnisteita – suunniteltu yksityiseksi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Not compatible!" xml:space="preserve"> <source>Not compatible!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notes" xml:space="preserve"> + <source>Notes</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing selected" xml:space="preserve"> + <source>Nothing selected</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing to forward!" xml:space="preserve"> + <source>Nothing to forward!</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> <target>Ilmoitukset</target> @@ -3462,6 +4889,18 @@ This is your link for group %@!</source> <target>Ilmoitukset on poistettu käytöstä!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notifications error" xml:space="preserve"> + <source>Notifications error</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Notifications privacy" xml:space="preserve"> + <source>Notifications privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications status" xml:space="preserve"> + <source>Notifications status</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. @@ -3471,44 +4910,42 @@ This is your link for group %@!</source> - poista jäsenet käytöstä ("tarkkailija" rooli)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="OK" xml:space="preserve"> + <source>OK</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Off" xml:space="preserve"> <source>Off</source> <target>Pois</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> - <source>Off (Local)</source> - <target>Pois (Paikallinen)</target> - <note>No comment provided by engineer.</note> + <note>blur media</note> </trans-unit> <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> <target>Ok</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> <target>Vanha tietokanta</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> - <source>Old database archive</source> - <target>Vanha tietokanta-arkisto</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> <target>Kertakutsulinkki</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> - <target>Yhteyden muodostamiseen tarvitaan Onion-isäntiä. Edellyttää VPN:n sallimista.</target> + <trans-unit id="Onion hosts will be **required** for connection. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be **required** for connection. +Requires compatible VPN.</source> + <target>Yhteyden muodostamiseen tarvitaan Onion-isäntiä. +Edellyttää VPN:n sallimista.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> - <target>Onion-isäntiä käytetään, kun niitä on saatavilla. Edellyttää VPN:n sallimista.</target> + <trans-unit id="Onion hosts will be used when available. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be used when available. +Requires compatible VPN.</source> + <target>Onion-isäntiä käytetään, kun niitä on saatavilla. +Edellyttää VPN:n sallimista.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will not be used." xml:space="preserve"> @@ -3516,11 +4953,19 @@ This is your link for group %@!</source> <target>Onion-isäntiä ei käytetä.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only chat owners can change preferences." xml:space="preserve"> + <source>Only chat owners can change preferences.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <target>Vain asiakaslaitteet tallentavat käyttäjäprofiileja, yhteystietoja, ryhmiä ja viestejä, jotka on lähetetty **kaksinkertaisella päästä päähän -salauksella**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only delete conversation" xml:space="preserve"> + <source>Only delete conversation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> <source>Only group owners can change group preferences.</source> <target>Vain ryhmän omistajat voivat muuttaa ryhmän asetuksia.</target> @@ -3536,14 +4981,22 @@ This is your link for group %@!</source> <target>Vain ryhmän omistajat voivat ottaa ääniviestit käyttöön.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only sender and moderators see it" xml:space="preserve"> + <source>Only sender and moderators see it</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you and moderators see it" xml:space="preserve"> + <source>Only you and moderators see it</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only you can add message reactions." xml:space="preserve"> <source>Only you can add message reactions.</source> <target>Vain sinä voit lisätä viestireaktioita.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <target>Vain sinä voit poistaa viestejä peruuttamattomasti (kontaktisi voi merkitä ne poistettavaksi).</target> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source> + <target>Vain sinä voit poistaa viestejä peruuttamattomasti (kontaktisi voi merkitä ne poistettavaksi). (24 tuntia)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can make calls." xml:space="preserve"> @@ -3566,9 +5019,9 @@ This is your link for group %@!</source> <target>Vain kontaktisi voi lisätä viestireaktioita.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <target>Vain kontaktisi voi poistaa viestejä peruuttamattomasti (voit merkitä ne poistettavaksi).</target> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source> + <target>Vain kontaktisi voi poistaa viestejä peruuttamattomasti (voit merkitä ne poistettavaksi). (24 tuntia)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can make calls." xml:space="preserve"> @@ -3588,13 +5041,17 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Open" xml:space="preserve"> <source>Open</source> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Avaa Asetukset</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open changes" xml:space="preserve"> + <source>Open changes</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> <target>Avaa keskustelu</target> @@ -3605,25 +5062,71 @@ This is your link for group %@!</source> <target>Avaa keskustelukonsoli</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Open conditions" xml:space="preserve"> + <source>Open conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open group" xml:space="preserve"> <source>Open group</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve"> - <source>Open user profiles</source> - <target>Avaa käyttäjäprofiilit</target> + <trans-unit id="Open link?" xml:space="preserve"> + <source>Open link?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Open migration to another device" xml:space="preserve"> + <source>Open migration to another device</source> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> - <target>Avoimen lähdekoodin protokolla ja koodi - kuka tahansa voi käyttää palvelimia.</target> + <trans-unit id="Opening app…" xml:space="preserve"> + <source>Opening app…</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve"> - <source>Opening database…</source> - <target>Avataan tietokantaa…</target> + <trans-unit id="Operator" xml:space="preserve"> + <source>Operator</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Operator server" xml:space="preserve"> + <source>Operator server</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Or import archive file" xml:space="preserve"> + <source>Or import archive file</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or paste archive link" xml:space="preserve"> + <source>Or paste archive link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or scan QR code" xml:space="preserve"> + <source>Or scan QR code</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or securely share this file link" xml:space="preserve"> + <source>Or securely share this file link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or show this code" xml:space="preserve"> + <source>Or show this code</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or to share privately" xml:space="preserve"> + <source>Or to share privately</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Organize chats into lists" xml:space="preserve"> + <source>Organize chats into lists</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other" xml:space="preserve"> + <source>Other</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other file errors: %@" xml:space="preserve"> + <source>Other file errors: +%@</source> + <note>alert message</note> + </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> <target>PING-määrä</target> @@ -3659,15 +5162,18 @@ This is your link for group %@!</source> <target>Pääsykoodi asetettu!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Password" xml:space="preserve"> + <source>Password</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> <target>Salasana näytettäväksi</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste" xml:space="preserve"> - <source>Paste</source> - <target>Liitä</target> - <note>No comment provided by engineer.</note> + <trans-unit id="Past member %@" xml:space="preserve"> + <source>Past member %@</source> + <note>past/unknown group member</note> </trans-unit> <trans-unit id="Paste desktop address" xml:space="preserve"> <source>Paste desktop address</source> @@ -3678,23 +5184,20 @@ This is your link for group %@!</source> <target>Liitä kuva</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve"> - <source>Paste received link</source> - <target>Liitä vastaanotettu linkki</target> + <trans-unit id="Paste link to connect!" xml:space="preserve"> + <source>Paste link to connect!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> - <source>Paste the link you received to connect with your contact.</source> - <target>Liitä saamasi linkki, jonka avulla voit muodostaa yhteyden kontaktiisi.</target> - <note>placeholder</note> - </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> - <target>Ihmiset voivat ottaa sinuun yhteyttä vain jakamiesi linkkien kautta.</target> + <trans-unit id="Paste the link you received" xml:space="preserve"> + <source>Paste the link you received</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Periodically" xml:space="preserve"> - <source>Periodically</source> + <trans-unit id="Pending" xml:space="preserve"> + <source>Pending</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Periodic" xml:space="preserve"> + <source>Periodic</source> <target>Ajoittain</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3703,11 +5206,28 @@ This is your link for group %@!</source> <target>Pysyvä salauksen purkuvirhe</target> <note>message decrypt error item</note> </trans-unit> + <trans-unit id="Picture-in-picture calls" xml:space="preserve"> + <source>Picture-in-picture calls</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Play from the chat list." xml:space="preserve"> + <source>Play from the chat list.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable calls." xml:space="preserve"> + <source>Please ask your contact to enable calls.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> <target>Pyydä kontaktiasi sallimaan ääniviestien lähettäminen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers." xml:space="preserve"> + <source>Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> <target>Tarkista, että käytit oikeaa linkkiä tai pyydä kontaktiasi lähettämään sinulle uusi linkki.</target> @@ -3723,6 +5243,15 @@ This is your link for group %@!</source> <target>Tarkista omasi ja kontaktin asetukset.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve"> + <source>Please confirm that network settings are correct for this device.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact developers. Error: %@" xml:space="preserve"> + <source>Please contact developers. +Error: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> <target>Ota yhteyttä ryhmän ylläpitäjään.</target> @@ -3763,11 +5292,27 @@ This is your link for group %@!</source> <target>Säilytä tunnuslause turvallisesti, ET voi muuttaa sitä, jos kadotat sen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please try to disable and re-enable notfications." xml:space="preserve"> + <source>Please try to disable and re-enable notfications.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token activation to complete." xml:space="preserve"> + <source>Please wait for token activation to complete.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token to be registered." xml:space="preserve"> + <source>Please wait for token to be registered.</source> + <note>token info</note> + </trans-unit> <trans-unit id="Polish interface" xml:space="preserve"> <source>Polish interface</source> <target>Puolalainen käyttöliittymä</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Port" xml:space="preserve"> + <source>Port</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> <target>Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen</target> @@ -3778,36 +5323,75 @@ This is your link for group %@!</source> <target>Säilytä viimeinen viestiluonnos liitteineen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Preset server" xml:space="preserve"> - <source>Preset server</source> - <target>Esiasetettu palvelin</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> <target>Esiasetettu palvelimen osoite</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Preset servers" xml:space="preserve"> + <source>Preset servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Preview" xml:space="preserve"> <source>Preview</source> <target>Esikatselu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Previously connected servers" xml:space="preserve"> + <source>Previously connected servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> <target>Yksityisyys ja turvallisuus</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Privacy for your customers." xml:space="preserve"> + <source>Privacy for your customers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy policy and conditions of use." xml:space="preserve"> + <source>Privacy policy and conditions of use.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy redefined" xml:space="preserve"> <source>Privacy redefined</source> <target>Yksityisyys uudelleen määritettynä</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve"> + <source>Private chats, groups and your contacts are not accessible to server operators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> <target>Yksityiset tiedostonimet</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private media file names." xml:space="preserve"> + <source>Private media file names.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing" xml:space="preserve"> + <source>Private message routing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing 🚀" xml:space="preserve"> + <source>Private message routing 🚀</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private notes" xml:space="preserve"> + <source>Private notes</source> + <note>name of notes to self</note> + </trans-unit> + <trans-unit id="Private routing" xml:space="preserve"> + <source>Private routing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private routing error" xml:space="preserve"> + <source>Private routing error</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> <target>Profiili- ja palvelinyhteydet</target> @@ -3818,12 +5402,8 @@ This is your link for group %@!</source> <target>Profiilikuva</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile name" xml:space="preserve"> - <source>Profile name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile name:" xml:space="preserve"> - <source>Profile name:</source> + <trans-unit id="Profile images" xml:space="preserve"> + <source>Profile images</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile password" xml:space="preserve"> @@ -3831,10 +5411,14 @@ This is your link for group %@!</source> <target>Profiilin salasana</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Profile theme" xml:space="preserve"> + <source>Profile theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> <source>Profile update will be sent to your contacts.</source> <target>Profiilipäivitys lähetetään kontakteillesi.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> @@ -3856,6 +5440,14 @@ This is your link for group %@!</source> <target>Estä viestireaktiot.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Prohibit reporting messages to moderators." xml:space="preserve"> + <source>Prohibit reporting messages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending SimpleX links." xml:space="preserve"> + <source>Prohibit sending SimpleX links.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> <target>Estä suorien viestien lähettäminen jäsenille.</target> @@ -3876,11 +5468,20 @@ This is your link for group %@!</source> <target>Estä ääniviestien lähettäminen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect IP address" xml:space="preserve"> + <source>Protect IP address</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> <target>Suojaa sovellusnäyttö</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve"> + <source>Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> <target>Suojaa keskusteluprofiilisi salasanalla!</target> @@ -3896,16 +5497,40 @@ This is your link for group %@!</source> <target>Protokollan aikakatkaisu per KB</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Proxied" xml:space="preserve"> + <source>Proxied</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied servers" xml:space="preserve"> + <source>Proxied servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxy requires password" xml:space="preserve"> + <source>Proxy requires password</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> <target>Push-ilmoitukset</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Push server" xml:space="preserve"> + <source>Push server</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Quantum resistant encryption" xml:space="preserve"> + <source>Quantum resistant encryption</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> <source>Rate the app</source> <target>Arvioi sovellus</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reachable chat toolbar" xml:space="preserve"> + <source>Reachable chat toolbar</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> <target>Reagoi…</target> @@ -3914,16 +5539,20 @@ This is your link for group %@!</source> <trans-unit id="Read" xml:space="preserve"> <source>Read</source> <target>Lue</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Read more" xml:space="preserve"> <source>Read more</source> <target>Lue lisää</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> - <target>Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</source> + <target>Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> @@ -3931,11 +5560,6 @@ This is your link for group %@!</source> <target>Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> - <source>Read more in our GitHub repository.</source> - <target>Lue lisää GitHub-tietovarastostamme.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> <target>Lue lisää [GitHub-arkistosta](https://github.com/simplex-chat/simplex-chat#readme).</target> @@ -3946,6 +5570,10 @@ This is your link for group %@!</source> <target>Kuittaukset pois käytöstä</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receive errors" xml:space="preserve"> + <source>Receive errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Vastaanotettu klo</target> @@ -3966,6 +5594,18 @@ This is your link for group %@!</source> <target>Vastaanotettu viesti</target> <note>message info title</note> </trans-unit> + <trans-unit id="Received messages" xml:space="preserve"> + <source>Received messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received reply" xml:space="preserve"> + <source>Received reply</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received total" xml:space="preserve"> + <source>Received total</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> <target>Vastaanotto-osoite vaihdetaan toiseen palvelimeen. Osoitteenmuutos tehdään sen jälkeen, kun lähettäjä tulee verkkoon.</target> @@ -3981,16 +5621,44 @@ This is your link for group %@!</source> <target>Vastaanotto kautta</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Recent history and improved [directory bot](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)." xml:space="preserve"> + <source>Recent history and improved [directory bot](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).</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve"> + <source>Recipient(s) can't see who this message is from.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> <target>Vastaanottajat näkevät päivitykset, kun kirjoitat niitä.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect" xml:space="preserve"> + <source>Reconnect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> <target>Yhdistä kaikki yhdistetyt palvelimet uudelleen pakottaaksesi viestin toimituksen. Tämä käyttää ylimääräistä liikennettä.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect all servers" xml:space="preserve"> + <source>Reconnect all servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers?" xml:space="preserve"> + <source>Reconnect all servers?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect server to force message delivery. It uses additional traffic.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server?" xml:space="preserve"> + <source>Reconnect server?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> <target>Yhdistä palvelimet uudelleen?</target> @@ -4011,10 +5679,23 @@ This is your link for group %@!</source> <target>Pienempi akun käyttö</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Register" xml:space="preserve"> + <source>Register</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Register notification token?" xml:space="preserve"> + <source>Register notification token?</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Registered" xml:space="preserve"> + <source>Registered</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> <target>Hylkää</target> - <note>reject incoming call via notification</note> + <note>reject incoming call via notification +swipe action</note> </trans-unit> <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> <source>Reject (sender NOT notified)</source> @@ -4041,6 +5722,14 @@ This is your link for group %@!</source> <target>Poista</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Remove archive?" xml:space="preserve"> + <source>Remove archive?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove image" xml:space="preserve"> + <source>Remove image</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> <target>Poista jäsen</target> @@ -4075,15 +5764,67 @@ This is your link for group %@!</source> <source>Repeat connection request?</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat download" xml:space="preserve"> + <source>Repeat download</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat import" xml:space="preserve"> + <source>Repeat import</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Repeat join request?" xml:space="preserve"> <source>Repeat join request?</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat upload" xml:space="preserve"> + <source>Repeat upload</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reply" xml:space="preserve"> <source>Reply</source> <target>Vastaa</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Report" xml:space="preserve"> + <source>Report</source> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Report content: only group moderators will see it." xml:space="preserve"> + <source>Report content: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report member profile: only group moderators will see it." xml:space="preserve"> + <source>Report member profile: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report other: only group moderators will see it." xml:space="preserve"> + <source>Report other: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report reason?" xml:space="preserve"> + <source>Report reason?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Report spam: only group moderators will see it." xml:space="preserve"> + <source>Report spam: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report violation: only group moderators will see it." xml:space="preserve"> + <source>Report violation: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report: %@" xml:space="preserve"> + <source>Report: %@</source> + <note>report in notification</note> + </trans-unit> + <trans-unit id="Reporting messages to moderators is prohibited." xml:space="preserve"> + <source>Reporting messages to moderators is prohibited.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reports" xml:space="preserve"> + <source>Reports</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Required" xml:space="preserve"> <source>Required</source> <target>Pakollinen</target> @@ -4094,16 +5835,36 @@ This is your link for group %@!</source> <target>Oletustilaan</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset all hints" xml:space="preserve"> + <source>Reset all hints</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics" xml:space="preserve"> + <source>Reset all statistics</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics?" xml:space="preserve"> + <source>Reset all statistics?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> <target>Oletusvärit</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to app theme" xml:space="preserve"> + <source>Reset to app theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> <target>Palauta oletusasetukset</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to user theme" xml:space="preserve"> + <source>Reset to user theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> <source>Restart the app to create a new chat profile</source> <target>Käynnistä sovellus uudelleen uuden keskusteluprofiilin luomiseksi</target> @@ -4134,14 +5895,17 @@ This is your link for group %@!</source> <target>Virhe tietokannan palauttamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Retry" xml:space="preserve"> + <source>Retry</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> <target>Paljasta</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Revert" xml:space="preserve"> - <source>Revert</source> - <target>Palauta</target> + <trans-unit id="Review conditions" xml:space="preserve"> + <source>Review conditions</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke" xml:space="preserve"> @@ -4169,51 +5933,61 @@ This is your link for group %@!</source> <target>Käynnistä chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve"> - <source>SMP servers</source> - <target>SMP-palvelimet</target> + <trans-unit id="SMP server" xml:space="preserve"> + <source>SMP server</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SOCKS proxy" xml:space="preserve"> + <source>SOCKS proxy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safely receive files" xml:space="preserve"> + <source>Safely receive files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safer groups" xml:space="preserve"> + <source>Safer groups</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save" xml:space="preserve"> <source>Save</source> <target>Tallenna</target> - <note>chat item action</note> + <note>alert button +chat item action</note> </trans-unit> <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> <target>Tallenna (ja ilmoita kontakteille)</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> <target>Tallenna ja ilmoita kontaktille</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> <target>Tallenna ja ilmoita ryhmän jäsenille</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save and reconnect" xml:space="preserve"> + <source>Save and reconnect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save and update group profile" xml:space="preserve"> <source>Save and update group profile</source> <target>Tallenna ja päivitä ryhmäprofiili</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save archive" xml:space="preserve"> - <source>Save archive</source> - <target>Tallenna arkisto</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve"> - <source>Save auto-accept settings</source> - <target>Tallenna automaattisen hyväksynnän asetukset</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> <source>Save group profile</source> <target>Tallenna ryhmäprofiili</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save list" xml:space="preserve"> + <source>Save list</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> <target>Tallenna tunnuslause ja avaa keskustelu</target> @@ -4227,7 +6001,7 @@ This is your link for group %@!</source> <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> <target>Tallenna asetukset?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> @@ -4242,23 +6016,46 @@ This is your link for group %@!</source> <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> <target>Tallenna palvelimet?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve"> - <source>Save settings?</source> - <target>Tallenna asetukset?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> <target>Tallenna tervetuloviesti?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save your profile?" xml:space="preserve"> + <source>Save your profile?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Saved" xml:space="preserve"> + <source>Saved</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> <source>Saved WebRTC ICE servers will be removed</source> <target>Tallennetut WebRTC ICE -palvelimet poistetaan</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Saved from" xml:space="preserve"> + <source>Saved from</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved message" xml:space="preserve"> + <source>Saved message</source> + <note>message info title</note> + </trans-unit> + <trans-unit id="Saving %lld messages" xml:space="preserve"> + <source>Saving %lld messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scale" xml:space="preserve"> + <source>Scale</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan / Paste link" xml:space="preserve"> + <source>Scan / Paste link</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> <target>Skannaa QR-koodi</target> @@ -4288,11 +6085,27 @@ This is your link for group %@!</source> <target>Haku</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Search bar accepts invitation links." xml:space="preserve"> + <source>Search bar accepts invitation links.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search or paste SimpleX link" xml:space="preserve"> + <source>Search or paste SimpleX link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secondary" xml:space="preserve"> + <source>Secondary</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> <source>Secure queue</source> <target>Turvallinen jono</target> <note>server test step</note> </trans-unit> + <trans-unit id="Secured" xml:space="preserve"> + <source>Secured</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> <target>Turvallisuusarviointi</target> @@ -4306,6 +6119,18 @@ This is your link for group %@!</source> <trans-unit id="Select" xml:space="preserve"> <source>Select</source> <target>Valitse</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Select chat profile" xml:space="preserve"> + <source>Select chat profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected %lld" xml:space="preserve"> + <source>Selected %lld</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct" xml:space="preserve"> @@ -4343,11 +6168,6 @@ This is your link for group %@!</source> <target>Lähetä toimituskuittaukset vastaanottajalle</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve"> - <source>Send direct message</source> - <target>Lähetä yksityisviesti</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Send direct message to connect" xml:space="preserve"> <source>Send direct message to connect</source> <note>No comment provided by engineer.</note> @@ -4357,6 +6177,10 @@ This is your link for group %@!</source> <target>Lähetä katoava viesti</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send errors" xml:space="preserve"> + <source>Send errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> <target>Lähetä linkkien esikatselu</target> @@ -4367,14 +6191,25 @@ This is your link for group %@!</source> <target>Lähetä live-viesti</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send message to enable calls." xml:space="preserve"> + <source>Send message to enable calls.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when your or destination server does not support private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> <target>Lähetys ilmoitukset</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> - <source>Send notifications:</source> - <target>Lähetys ilmoitukset:</target> + <trans-unit id="Send private reports" xml:space="preserve"> + <source>Send private reports</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve"> @@ -4392,10 +6227,14 @@ This is your link for group %@!</source> <target>Lähetä ne galleriasta tai mukautetuista näppäimistöistä.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve"> + <source>Send up to 100 last messages to new members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> <target>Lähettäjä peruutti tiedoston siirron.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> @@ -4452,6 +6291,10 @@ This is your link for group %@!</source> <target>Lähetetty klo: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Sent directly" xml:space="preserve"> + <source>Sent directly</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> <target>Lähetetty tiedosto tapahtuma</target> @@ -4462,11 +6305,59 @@ This is your link for group %@!</source> <target>Lähetetty viesti</target> <note>message info title</note> </trans-unit> + <trans-unit id="Sent messages" xml:space="preserve"> + <source>Sent messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> <target>Lähetetyt viestit poistetaan asetetun ajan kuluttua.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sent reply" xml:space="preserve"> + <source>Sent reply</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent total" xml:space="preserve"> + <source>Sent total</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent via proxy" xml:space="preserve"> + <source>Sent via proxy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server" xml:space="preserve"> + <source>Server</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server added to operator %@." xml:space="preserve"> + <source>Server added to operator %@.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Server address" xml:space="preserve"> + <source>Server address</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings." xml:space="preserve"> + <source>Server address is incompatible with network settings.</source> + <note>srv error text.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Server address is incompatible with network settings: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server operator changed." xml:space="preserve"> + <source>Server operator changed.</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Server operators" xml:space="preserve"> + <source>Server operators</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server protocol changed." xml:space="preserve"> + <source>Server protocol changed.</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> <target>Palvelin vaatii valtuutuksen jonojen luomiseen, tarkista salasana</target> @@ -4482,11 +6373,31 @@ This is your link for group %@!</source> <target>Palvelintesti epäonnistui!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Server type" xml:space="preserve"> + <source>Server type</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server version is incompatible with network settings." xml:space="preserve"> + <source>Server version is incompatible with network settings.</source> + <note>srv error text</note> + </trans-unit> + <trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve"> + <source>Server version is incompatible with your app: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Servers" xml:space="preserve"> <source>Servers</source> <target>Palvelimet</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Servers info" xml:space="preserve"> + <source>Servers info</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Session code" xml:space="preserve"> <source>Session code</source> <note>No comment provided by engineer.</note> @@ -4496,11 +6407,19 @@ This is your link for group %@!</source> <target>Aseta 1 päivä</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set chat name…" xml:space="preserve"> + <source>Set chat name…</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> <target>Aseta kontaktin nimi…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve"> + <source>Set default theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> <target>Aseta ryhmän asetukset</target> @@ -4511,11 +6430,19 @@ This is your link for group %@!</source> <target>Aseta se järjestelmän todennuksen sijaan.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve"> + <source>Set message expiration in chats.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passcode" xml:space="preserve"> <source>Set passcode</source> <target>Aseta pääsykoodi</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set passphrase" xml:space="preserve"> + <source>Set passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> <target>Aseta tunnuslause vientiä varten</target> @@ -4536,24 +6463,49 @@ This is your link for group %@!</source> <target>Asetukset</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve"> + <source>Settings were changed.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve"> + <source>Shape profile images</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share" xml:space="preserve"> <source>Share</source> <target>Jaa</target> - <note>chat item action</note> + <note>alert action +chat item action</note> </trans-unit> <trans-unit id="Share 1-time link" xml:space="preserve"> <source>Share 1-time link</source> <target>Jaa kertakäyttölinkki</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share 1-time link with a friend" xml:space="preserve"> + <source>Share 1-time link with a friend</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve"> + <source>Share SimpleX address on social media.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address" xml:space="preserve"> <source>Share address</source> <target>Jaa osoite</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share address publicly" xml:space="preserve"> + <source>Share address publicly</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address with contacts?" xml:space="preserve"> <source>Share address with contacts?</source> <target>Jaa osoite kontakteille?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Share from other apps." xml:space="preserve"> + <source>Share from other apps.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share link" xml:space="preserve"> @@ -4561,9 +6513,16 @@ This is your link for group %@!</source> <target>Jaa linkki</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve"> - <source>Share one-time invitation link</source> - <target>Jaa kertakutsulinkki</target> + <trans-unit id="Share profile" xml:space="preserve"> + <source>Share profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share this 1-time invite link" xml:space="preserve"> + <source>Share this 1-time invite link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share to SimpleX" xml:space="preserve"> + <source>Share to SimpleX</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share with contacts" xml:space="preserve"> @@ -4571,6 +6530,14 @@ This is your link for group %@!</source> <target>Jaa kontaktien kanssa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Short link" xml:space="preserve"> + <source>Short link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show QR code" xml:space="preserve"> + <source>Show QR code</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> <target>Näytä puhelut puhelinhistoriassa</target> @@ -4586,21 +6553,41 @@ This is your link for group %@!</source> <target>Näytä viimeiset viestit</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show message status" xml:space="preserve"> + <source>Show message status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show percentage" xml:space="preserve"> + <source>Show percentage</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Näytä esikatselu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show → on messages sent via private routing." xml:space="preserve"> + <source>Show → on messages sent via private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show:" xml:space="preserve"> <source>Show:</source> <target>Näytä:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve"> + <source>SimpleX</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Address" xml:space="preserve"> <source>SimpleX Address</source> <target>SimpleX-osoite</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." xml:space="preserve"> + <source>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> <target>Trail of Bits on tarkastanut SimpleX Chatin tietoturvan.</target> @@ -4631,6 +6618,18 @@ This is your link for group %@!</source> <target>SimpleX-osoite</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX address and 1-time links are safe to share via any messenger." xml:space="preserve"> + <source>SimpleX address and 1-time links are safe to share via any messenger.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address or 1-time link?" xml:space="preserve"> + <source>SimpleX address or 1-time link?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX channel link" xml:space="preserve"> + <source>SimpleX channel link</source> + <note>simplex link type</note> + </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> <target>SimpleX-yhteystiedot</target> @@ -4649,6 +6648,14 @@ This is your link for group %@!</source> <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> <target>SimpleX-linkit</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve"> + <source>SimpleX links are prohibited.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve"> + <source>SimpleX links not allowed</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> @@ -4656,10 +6663,18 @@ This is your link for group %@!</source> <target>SimpleX-kertakutsu</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> + <source>SimpleX protocols reviewed by Trail of Bits.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Simplified incognito mode" xml:space="preserve"> <source>Simplified incognito mode</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Size" xml:space="preserve"> + <source>Size</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Ohita</target> @@ -4675,19 +6690,44 @@ This is your link for group %@!</source> <target>Pienryhmät (max 20)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Soft" xml:space="preserve"> + <source>Soft</source> + <note>blur media</note> + </trans-unit> + <trans-unit id="Some app settings were not migrated." xml:space="preserve"> + <source>Some app settings were not migrated.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some file(s) were not exported:" xml:space="preserve"> + <source>Some file(s) were not exported:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Tuonnin aikana tapahtui joitakin ei-vakavia virheitä – saatat nähdä Chat-konsolissa lisätietoja.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import:" xml:space="preserve"> + <source>Some non-fatal errors occurred during import:</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some servers failed the test: %@" xml:space="preserve"> + <source>Some servers failed the test: +%@</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> <target>Joku</target> <note>notification title</note> </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve"> - <source>Start a new chat</source> - <target>Aloita uusi keskustelu</target> + <trans-unit id="Spam" xml:space="preserve"> + <source>Spam</source> + <note>blocking reason +report reason</note> + </trans-unit> + <trans-unit id="Square, circle, or anything in between." xml:space="preserve"> + <source>Square, circle, or anything in between.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start chat" xml:space="preserve"> @@ -4695,11 +6735,23 @@ This is your link for group %@!</source> <target>Aloita keskustelu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Start chat?" xml:space="preserve"> + <source>Start chat?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> <target>Aloita siirto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Starting from %@." xml:space="preserve"> + <source>Starting from %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Statistics" xml:space="preserve"> + <source>Statistics</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> <target>Lopeta</target> @@ -4710,9 +6762,8 @@ This is your link for group %@!</source> <target>Lopeta SimpleX</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> - <source>Stop chat to enable database actions</source> - <target>Pysäytä keskustelu tietokantatoimien mahdollistamiseksi</target> + <trans-unit id="Stop chat" xml:space="preserve"> + <source>Stop chat</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> @@ -4743,23 +6794,55 @@ This is your link for group %@!</source> <trans-unit id="Stop sharing" xml:space="preserve"> <source>Stop sharing</source> <target>Lopeta jakaminen</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Stop sharing address?" xml:space="preserve"> <source>Stop sharing address?</source> <target>Lopeta osoitteen jakaminen?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Stopping chat" xml:space="preserve"> + <source>Stopping chat</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Storage" xml:space="preserve"> + <source>Storage</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Strong" xml:space="preserve"> + <source>Strong</source> + <note>blur media</note> + </trans-unit> <trans-unit id="Submit" xml:space="preserve"> <source>Submit</source> <target>Lähetä</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Subscribed" xml:space="preserve"> + <source>Subscribed</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscription errors" xml:space="preserve"> + <source>Subscription errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscriptions ignored" xml:space="preserve"> + <source>Subscriptions ignored</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Support SimpleX Chat" xml:space="preserve"> <source>Support SimpleX Chat</source> <target>SimpleX Chat tuki</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Switch audio and video during the call." xml:space="preserve"> + <source>Switch audio and video during the call.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> + <source>Switch chat profile for 1-time invitations.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="System" xml:space="preserve"> <source>System</source> <target>Järjestelmä</target> @@ -4770,11 +6853,19 @@ This is your link for group %@!</source> <target>Järjestelmän todennus</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP connection" xml:space="preserve"> + <source>TCP connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> <target>TCP-yhteyden aikakatkaisu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP port for messaging" xml:space="preserve"> + <source>TCP port for messaging</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <source>TCP_KEEPCNT</source> <target>TCP_KEEPCNT</target> @@ -4790,11 +6881,19 @@ This is your link for group %@!</source> <target>TCP_KEEPINTVL</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tail" xml:space="preserve"> + <source>Tail</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> <target>Ota kuva</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap Create SimpleX address in the menu to create it later." xml:space="preserve"> + <source>Tap Create SimpleX address in the menu to create it later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> <target>Napauta painiketta </target> @@ -4819,16 +6918,27 @@ This is your link for group %@!</source> <target>Napauta liittyäksesi incognito-tilassa</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve"> - <source>Tap to start a new chat</source> - <target>Aloita uusi keskustelu napauttamalla</target> + <trans-unit id="Tap to paste link" xml:space="preserve"> + <source>Tap to paste link</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap to scan" xml:space="preserve"> + <source>Tap to scan</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Temporary file error" xml:space="preserve"> + <source>Temporary file error</source> + <note>file error alert title</note> + </trans-unit> <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> <target>Testi epäonnistui vaiheessa %@.</target> <note>server test failure</note> </trans-unit> + <trans-unit id="Test notifications" xml:space="preserve"> + <source>Test notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> <target>Testipalvelin</target> @@ -4842,7 +6952,7 @@ This is your link for group %@!</source> <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> <target>Testit epäonnistuivat!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> @@ -4859,11 +6969,6 @@ This is your link for group %@!</source> <target>Kiitokset käyttäjille – osallistu Weblaten kautta!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> - <target>Ensimmäinen alusta ilman käyttäjätunnisteita – suunniteltu yksityiseksi.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <source>The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised.</source> @@ -4876,11 +6981,27 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t <target>Sovellus voi ilmoittaa sinulle, kun saat viestejä tai yhteydenottopyyntöjä - avaa asetukset ottaaksesi ne käyttöön.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The app protects your privacy by using different operators in each conversation." xml:space="preserve"> + <source>The app protects your privacy by using different operators in each conversation.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve"> + <source>The app will ask to confirm downloads from unknown file servers (except .onion).</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> <source>The attempt to change database passphrase was not completed.</source> <target>Tietokannan tunnuslauseen muuttamista ei suoritettu loppuun.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve"> + <source>The code you scanned is not a SimpleX link QR code.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The connection reached the limit of undelivered messages, your contact may be offline." xml:space="preserve"> + <source>The connection reached the limit of undelivered messages, your contact may be offline.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> <target>Hyväksymäsi yhteys peruuntuu!</target> @@ -4901,6 +7022,11 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t <target>Salaus toimii ja uutta salaussopimusta ei tarvita. Tämä voi johtaa yhteysvirheisiin!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> + <target>Seuraavan sukupolven yksityisviestit</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The hash of the previous message is different." xml:space="preserve"> <source>The hash of the previous message is different.</source> <target>Edellisen viestin tarkiste on erilainen.</target> @@ -4916,9 +7042,12 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t <target>Viesti merkitään moderoiduksi kaikille jäsenille.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> - <target>Seuraavan sukupolven yksityisviestit</target> + <trans-unit id="The messages will be deleted for all members." xml:space="preserve"> + <source>The messages will be deleted for all members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve"> + <source>The messages will be marked as moderated for all members.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> @@ -4926,9 +7055,12 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t <target>Vanhaa tietokantaa ei poistettu siirron aikana, se voidaan kuitenkin poistaa.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> - <target>Profiili jaetaan vain kontaktiesi kanssa.</target> + <trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve"> + <source>The same conditions will apply to operator **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second preset operator in the app!" xml:space="preserve"> + <source>The second preset operator in the app!</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> @@ -4946,19 +7078,24 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t <target>Palvelimet nykyisen keskusteluprofiilisi uusille yhteyksille **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Theme" xml:space="preserve"> - <source>Theme</source> - <target>Teema</target> + <trans-unit id="The servers for new files of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new files of your current chat profile **%@**.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve"> - <source>There should be at least one user profile.</source> - <target>Käyttäjäprofiileja tulee olla vähintään yksi.</target> + <trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve"> + <source>The text you pasted is not a SimpleX link.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> - <source>There should be at least one visible user profile.</source> - <target>Näkyviä käyttäjäprofiileja tulee olla vähintään yksi.</target> + <trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve"> + <source>The uploaded database archive will be permanently removed from the servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Themes" xml:space="preserve"> + <source>Themes</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="These conditions will also apply for: **%@**." xml:space="preserve"> + <source>These conditions will also apply for: **%@**.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> @@ -4981,15 +7118,31 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t <target>Tätä toimintoa ei voi kumota - valittua aikaisemmin lähetetyt ja vastaanotetut viestit poistetaan. Tämä voi kestää useita minuutteja.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> <target>Tätä toimintoa ei voi kumota - profiilisi, kontaktisi, viestisi ja tiedostosi poistuvat peruuttamattomasti.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by end-to-end encryption.</source> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by quantum resistant end-to-end encryption.</source> + <note>E2EE info chat item</note> + </trans-unit> <trans-unit id="This device name" xml:space="preserve"> <source>This device name</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve"> + <source>This display name is invalid. Please choose another name.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> <source>This group has over %lld members, delivery receipts are not sent.</source> <target>Tässä ryhmässä on yli %lld jäsentä, lähetyskuittauksia ei lähetetä.</target> @@ -5008,11 +7161,27 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t <source>This is your own one-time link!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve"> + <source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve"> + <source>This link was used with another mobile device, please create a new link on the desktop.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This message was deleted or not received yet." xml:space="preserve"> + <source>This message was deleted or not received yet.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <source>This setting applies to messages in your current chat profile **%@**.</source> <target>Tämä asetus koskee nykyisen keskusteluprofiilisi viestejä *%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Title" xml:space="preserve"> + <source>Title</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> <source>To ask any questions and to receive updates:</source> <target>Voit esittää kysymyksiä ja saada päivityksiä:</target> @@ -5032,9 +7201,8 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t <target>Uuden yhteyden luominen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <target>Yksityisyyden suojaamiseksi kaikkien muiden alustojen käyttämien käyttäjätunnusten sijaan SimpleX käyttää viestijonojen tunnisteita, jotka ovat kaikille kontakteille erillisiä.</target> + <trans-unit id="To protect against your link being replaced, you can compare contact security codes." xml:space="preserve"> + <source>To protect against your link being replaced, you can compare contact security codes.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -5042,6 +7210,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t <target>Aikavyöhykkeen suojaamiseksi kuva-/äänitiedostot käyttävät UTC:tä.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve"> + <source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> @@ -5049,6 +7221,23 @@ You will be prompted to complete authentication before this feature is enabled.< Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus otetaan käyttöön.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> + <target>Yksityisyyden suojaamiseksi kaikkien muiden alustojen käyttämien käyttäjätunnusten sijaan SimpleX käyttää viestijonojen tunnisteita, jotka ovat kaikille kontakteille erillisiä.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To receive" xml:space="preserve"> + <source>To receive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve"> + <source>To record speech please grant permission to use Microphone.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record video please grant permission to use Camera." xml:space="preserve"> + <source>To record video please grant permission to use Camera.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> <target>Jos haluat nauhoittaa ääniviestin, anna lupa käyttää mikrofonia.</target> @@ -5059,25 +7248,53 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote <target>Voit paljastaa piilotetun profiilisi syöttämällä koko salasanan hakukenttään **Keskusteluprofiilisi** -sivulla.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To send" xml:space="preserve"> + <source>To send</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> <source>To support instant push notifications the chat database has to be migrated.</source> <target>Keskustelujen-tietokanta on siirrettävä välittömien push-ilmoitusten tukemiseksi.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To use the servers of **%@**, accept conditions of use." xml:space="preserve"> + <source>To use the servers of **%@**, accept conditions of use.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> <target>Voit tarkistaa päästä päähän -salauksen kontaktisi kanssa vertaamalla (tai skannaamalla) laitteidenne koodia.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle chat list:" xml:space="preserve"> + <source>Toggle chat list:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> <source>Toggle incognito when connecting.</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Token status: %@." xml:space="preserve"> + <source>Token status: %@.</source> + <note>token status</note> + </trans-unit> + <trans-unit id="Toolbar opacity" xml:space="preserve"> + <source>Toolbar opacity</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Total" xml:space="preserve"> + <source>Total</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>Kuljetuksen eristäminen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Transport sessions" xml:space="preserve"> + <source>Transport sessions</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> <target>Yritetään muodostaa yhteyttä palvelimeen, jota käytetään tämän kontaktin viestien vastaanottamiseen (virhe: %@).</target> @@ -5088,16 +7305,15 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote <target>Yritetään muodostaa yhteys palvelimeen, jota käytetään viestien vastaanottamiseen tältä kontaktilta.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Turkish interface" xml:space="preserve"> + <source>Turkish interface</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> <target>Sammuta</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve"> - <source>Turn off notifications?</source> - <target>Kytke ilmoitukset pois päältä?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> <target>Kytke päälle</target> @@ -5112,18 +7328,25 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote <source>Unblock</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock for all" xml:space="preserve"> + <source>Unblock for all</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member" xml:space="preserve"> <source>Unblock member</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock member for all?" xml:space="preserve"> + <source>Unblock member for all?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member?" xml:space="preserve"> <source>Unblock member?</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve"> - <source>Unexpected error: %@</source> - <target>Odottamaton virhe: %@</target> - <note>item status description</note> + <trans-unit id="Undelivered messages" xml:space="preserve"> + <source>Undelivered messages</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -5133,7 +7356,7 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote <trans-unit id="Unfav." xml:space="preserve"> <source>Unfav.</source> <target>Epäsuotuisa.</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> @@ -5170,6 +7393,10 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote <target>Tuntematon virhe</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unknown servers!" xml:space="preserve"> + <source>Unknown servers!</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> <target>Ellet käytä iOS:n puhelinkäyttöliittymää, ota Älä häiritse -tila käyttöön keskeytysten välttämiseksi.</target> @@ -5203,11 +7430,19 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> <target>Poista mykistys</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> </trans-unit> <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> <target>Lukematon</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Unsupported connection link" xml:space="preserve"> + <source>Unsupported connection link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve"> + <source>Up to 100 last messages are sent to new members.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update" xml:space="preserve"> @@ -5215,11 +7450,6 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja <target>Päivitä</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> - <source>Update .onion hosts setting?</source> - <target>Päivitä .onion-isäntien asetus?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> <target>Päivitä tietokannan tunnuslause</target> @@ -5230,9 +7460,12 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja <target>Päivitä verkkoasetukset?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve"> - <source>Update transport isolation mode?</source> - <target>Päivitä kuljetuksen eristystila?</target> + <trans-unit id="Update settings?" xml:space="preserve"> + <source>Update settings?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updated conditions" xml:space="preserve"> + <source>Updated conditions</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> @@ -5240,31 +7473,62 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja <target>Asetusten päivittäminen yhdistää asiakkaan uudelleen kaikkiin palvelimiin.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> - <target>Tämän asetuksen päivittäminen yhdistää asiakkaan uudelleen kaikkiin palvelimiin.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Upgrade and open chat" xml:space="preserve"> <source>Upgrade and open chat</source> <target>Päivitä ja avaa keskustelu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Upload errors" xml:space="preserve"> + <source>Upload errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload failed" xml:space="preserve"> + <source>Upload failed</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Upload file" xml:space="preserve"> <source>Upload file</source> <target>Lataa tiedosto</target> <note>server test step</note> </trans-unit> + <trans-unit id="Uploaded" xml:space="preserve"> + <source>Uploaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploaded files" xml:space="preserve"> + <source>Uploaded files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploading archive" xml:space="preserve"> + <source>Uploading archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use %@" xml:space="preserve"> + <source>Use %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> <target>Käytä .onion-isäntiä</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use SOCKS proxy" xml:space="preserve"> + <source>Use SOCKS proxy</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> <target>Käytä SimpleX Chat palvelimia?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use TCP port %@ when no port is specified." xml:space="preserve"> + <source>Use TCP port %@ when no port is specified.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve"> + <source>Use TCP port 443 for preset servers only.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> <target>Käytä chattia</target> @@ -5275,6 +7539,14 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja <target>Käytä nykyistä profiilia</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use for files" xml:space="preserve"> + <source>Use for files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for messages" xml:space="preserve"> + <source>Use for messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Käytä uusiin yhteyksiin</target> @@ -5294,19 +7566,49 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja <target>Käytä uutta incognito-profiilia</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use only local notifications?" xml:space="preserve"> + <source>Use only local notifications?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve"> + <source>Use private routing with unknown servers when IP address is not protected.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers." xml:space="preserve"> + <source>Use private routing with unknown servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Käytä palvelinta</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> - <target>Käyttäjäprofiili</target> + <trans-unit id="Use servers" xml:space="preserve"> + <source>Use servers</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> - <target>.onion-isäntien käyttäminen vaatii yhteensopivan VPN-palveluntarjoajan.</target> + <trans-unit id="Use short links (BETA)" xml:space="preserve"> + <source>Use short links (BETA)</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app while in the call." xml:space="preserve"> + <source>Use the app while in the call.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app with one hand." xml:space="preserve"> + <source>Use the app with one hand.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use web port" xml:space="preserve"> + <source>Use web port</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="User selection" xml:space="preserve"> + <source>User selection</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Username" xml:space="preserve"> + <source>Username</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> @@ -5331,6 +7633,14 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja <source>Verify connections</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Verify database passphrase" xml:space="preserve"> + <source>Verify database passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify passphrase" xml:space="preserve"> + <source>Verify passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Verify security code" xml:space="preserve"> <source>Verify security code</source> <target>Tarkista turvakoodi</target> @@ -5365,11 +7675,23 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja <target>Videot ja tiedostot 1 Gt asti</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View conditions" xml:space="preserve"> + <source>View conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> <target>Näytä turvakoodi</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View updated conditions" xml:space="preserve"> + <source>View updated conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Visible history" xml:space="preserve"> + <source>Visible history</source> + <note>chat feature</note> + </trans-unit> <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> <target>Ääniviestit</target> @@ -5380,11 +7702,15 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja <target>Ääniviestit ovat kiellettyjä tässä keskustelussa.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <target>Ääniviestit ovat kiellettyjä tässä ryhmässä.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Voice messages not allowed" xml:space="preserve"> + <source>Voice messages not allowed</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> <source>Voice messages prohibited!</source> <target>Ääniviestit kielletty!</target> @@ -5414,6 +7740,18 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja <target>Odottaa videota</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wallpaper accent" xml:space="preserve"> + <source>Wallpaper accent</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper background" xml:space="preserve"> + <source>Wallpaper background</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve"> + <source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> <source>Warning: you may lose some data!</source> <target>Varoitus: saatat menettää joitain tietoja!</target> @@ -5434,6 +7772,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja <target>Tervetuloviesti</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Welcome message is too long" xml:space="preserve"> + <source>Welcome message is too long</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="What's new" xml:space="preserve"> <source>What's new</source> <target>Uusimmat</target> @@ -5444,9 +7786,12 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja <target>Kun saatavilla</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> - <source>When people request to connect, you can accept or reject it.</source> - <target>Kun ihmiset pyytävät yhteyden muodostamista, voit hyväksyä tai hylätä sen.</target> + <trans-unit id="When connecting audio and video calls." xml:space="preserve"> + <source>When connecting audio and video calls.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When more than one operator is enabled, none of them has metadata to learn who communicates with whom." xml:space="preserve"> + <source>When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> @@ -5454,29 +7799,63 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja <target>Kun jaat inkognitoprofiilin jonkun kanssa, tätä profiilia käytetään ryhmissä, joihin tämä sinut kutsuu.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="WiFi" xml:space="preserve"> + <source>WiFi</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Will be enabled in direct chats!" xml:space="preserve"> + <source>Will be enabled in direct chats!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wired ethernet" xml:space="preserve"> + <source>Wired ethernet</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With encrypted files and media." xml:space="preserve"> + <source>With encrypted files and media.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> <target>Valinnaisella tervetuloviestillä.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="With reduced battery usage." xml:space="preserve"> + <source>With reduced battery usage.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to file servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> <target>Väärä tietokannan tunnuslause</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve"> + <source>Wrong key or unknown connection - most likely this connection is deleted.</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Wrong key or unknown file chunk address - most likely file is deleted." xml:space="preserve"> + <source>Wrong key or unknown file chunk address - most likely file is deleted.</source> + <note>file error text</note> + </trans-unit> <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> <target>Väärä tunnuslause!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="XFTP servers" xml:space="preserve"> - <source>XFTP servers</source> - <target>XFTP-palvelimet</target> + <trans-unit id="XFTP server" xml:space="preserve"> + <source>XFTP server</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> - <target>Sinä</target> + <trans-unit id="You **must not** use the same database on two devices." xml:space="preserve"> + <source>You **must not** use the same database on two devices.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You accepted connection" xml:space="preserve"> @@ -5499,6 +7878,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja <target>Olet jo muodostanut yhteyden %@:n kanssa.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are already connected with %@." xml:space="preserve"> + <source>You are already connected with %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You are already connecting to %@." xml:space="preserve"> <source>You are already connecting to %@.</source> <note>No comment provided by engineer.</note> @@ -5538,14 +7921,21 @@ Repeat join request?</source> <target>Sinut on kutsuttu ryhmään</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are not connected to these servers. Private routing is used to deliver messages to them." xml:space="preserve"> + <source>You are not connected to these servers. Private routing is used to deliver messages to them.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> <source>You can accept calls from lock screen, without device and app authentication.</source> <target>Voit vastaanottaa puheluita lukitusnäytöltä ilman laitteen ja sovelluksen todennusta.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <target>Voit myös muodostaa yhteyden klikkaamalla linkkiä. Jos se avautuu selaimessa, napsauta **Avaa mobiilisovelluksessa**-painiketta.</target> + <trans-unit id="You can change it in Appearance settings." xml:space="preserve"> + <source>You can change it in Appearance settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can configure servers via settings." xml:space="preserve"> + <source>You can configure servers via settings.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can create it later" xml:space="preserve"> @@ -5563,16 +7953,32 @@ Repeat join request?</source> <target>Voit ottaa ne käyttöön myöhemmin sovelluksen Yksityisyys & Turvallisuus -asetuksista.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can give another try." xml:space="preserve"> + <source>You can give another try.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <source>You can hide or mute a user profile - swipe it to the right.</source> <target>Voit piilottaa tai mykistää käyttäjäprofiilin pyyhkäisemällä sitä oikealle.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> + <trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve"> + <source>You can make it visible to your SimpleX contacts via Settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now chat with %@" xml:space="preserve"> + <source>You can now chat with %@</source> <target>Voit nyt lähettää viestejä %@:lle</target> <note>notification body</note> </trans-unit> + <trans-unit id="You can send messages to %@ from Archived contacts." xml:space="preserve"> + <source>You can send messages to %@ from Archived contacts.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can set connection name, to remember who the link was shared with." xml:space="preserve"> + <source>You can set connection name, to remember who the link was shared with.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> <target>Voit määrittää lukitusnäytön ilmoituksen esikatselun asetuksista.</target> @@ -5588,16 +7994,15 @@ Repeat join request?</source> <target>Voit jakaa tämän osoitteen kontaktiesi kanssa, jotta ne voivat muodostaa yhteyden **%@** kanssa.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> - <source>You can share your address as a link or QR code - anybody can connect to you.</source> - <target>Voit jakaa osoitteesi linkkinä tai QR-koodina - kuka tahansa voi muodostaa yhteyden sinuun.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> <target>Voit aloittaa keskustelun sovelluksen Asetukset / Tietokanta kautta tai käynnistämällä sovelluksen uudelleen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve"> + <source>You can still view conversation with %@ in the list of chats.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> <target>Voit ottaa SimpleX Lockin käyttöön Asetusten kautta.</target> @@ -5608,21 +8013,25 @@ Repeat join request?</source> <target>Voit käyttää markdownia viestien muotoiluun:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can view invitation link again in connection details." xml:space="preserve"> + <source>You can view invitation link again in connection details.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> <target>Et voi lähettää viestejä!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <target>Sinä hallitset, minkä palvelim(i)en kautta **viestit vastaanotetaan**, kontaktisi - palvelimet, joita käytät viestien lähettämiseen niille.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <target>Sinua ei voitu todentaa; yritä uudelleen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> + <target>Kimin bağlanabileceğine siz karar verirsiniz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You have already requested connection via this address!" xml:space="preserve"> <source>You have already requested connection via this address!</source> <note>No comment provided by engineer.</note> @@ -5632,11 +8041,6 @@ Repeat join request?</source> Repeat connection request?</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> - <target>Sinulla ei ole keskusteluja</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> <target>Sinun on annettava tunnuslause aina, kun sovellus käynnistyy - sitä ei tallenneta laitteeseen.</target> @@ -5657,11 +8061,23 @@ Repeat connection request?</source> <target>Liityit tähän ryhmään. Muodostetaan yhteyttä ryhmän jäsenten kutsumiseksi.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You may migrate the exported database." xml:space="preserve"> + <source>You may migrate the exported database.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may save the exported archive." xml:space="preserve"> + <source>You may save the exported archive.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> <target>Sinun tulee käyttää keskustelujen-tietokannan uusinta versiota AINOSTAAN yhdessä laitteessa, muuten saatat lakata vastaanottamasta viestejä joiltakin kontakteilta.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You need to allow your contact to call to be able to call them." xml:space="preserve"> + <source>You need to allow your contact to call to be able to call them.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> <target>Sinun on sallittava kontaktiesi lähettää ääniviestejä, jotta voit lähettää niitä.</target> @@ -5677,6 +8093,10 @@ Repeat connection request?</source> <target>Lähetit ryhmäkutsun</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You should receive notifications." xml:space="preserve"> + <source>You should receive notifications.</source> + <note>token info</note> + </trans-unit> <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> <target>Sinut yhdistetään ryhmään, kun ryhmän isännän laite on online-tilassa, odota tai tarkista myöhemmin!</target> @@ -5710,6 +8130,10 @@ Repeat connection request?</source> <target>Saat edelleen puheluita ja ilmoituksia mykistetyiltä profiileilta, kun ne ovat aktiivisia.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You will stop receiving messages from this chat. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this chat. Chat history will be preserved.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> <target>Et enää saa viestejä tästä ryhmästä. Keskusteluhistoria säilytetään.</target> @@ -5730,31 +8154,16 @@ Repeat connection request?</source> <target>Käytät tässä ryhmässä incognito-profiilia. Kontaktien kutsuminen ei ole sallittua, jotta pääprofiilisi ei tule jaetuksi</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your %@ servers" xml:space="preserve"> - <source>Your %@ servers</source> - <target>%@-palvelimesi</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your ICE servers" xml:space="preserve"> <source>Your ICE servers</source> <target>ICE-palvelimesi</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> - <target>SMP-palvelimesi</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your SimpleX address" xml:space="preserve"> <source>Your SimpleX address</source> <target>SimpleX-osoitteesi</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your XFTP servers" xml:space="preserve"> - <source>Your XFTP servers</source> - <target>XFTP-palvelimesi</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your calls" xml:space="preserve"> <source>Your calls</source> <target>Puhelusi</target> @@ -5770,16 +8179,17 @@ Repeat connection request?</source> <target>Keskustelut-tietokantasi ei ole salattu - aseta tunnuslause sen salaamiseksi.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your chat preferences" xml:space="preserve"> + <source>Your chat preferences</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>Keskusteluprofiilisi</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> - <target>Kontaktin tulee olla online-tilassa, jotta yhteys voidaan muodostaa. -Voit peruuttaa tämän yhteyden ja poistaa kontaktin (ja yrittää myöhemmin uudella linkillä).</target> + <trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve"> + <source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> @@ -5792,18 +8202,15 @@ Voit peruuttaa tämän yhteyden ja poistaa kontaktin (ja yrittää myöhemmin uu <target>Kontaktisi voivat sallia viestien täydellisen poistamisen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> - <source>Your contacts in SimpleX will see it. -You can change it in Settings.</source> - <target>Kontaktisi SimpleX:ssä näkevät sen. -Voit muuttaa sitä Asetuksista.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your contacts will remain connected." xml:space="preserve"> <source>Your contacts will remain connected.</source> <target>Kontaktisi pysyvät yhdistettyinä.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve"> + <source>Your credentials may be sent unencrypted.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> <target>Nykyinen keskustelut-tietokantasi poistetaan ja korvataan tuodulla tietokannalla.</target> @@ -5833,33 +8240,34 @@ Voit muuttaa sitä Asetuksista.</target> <target>Profiilisi **%@** jaetaan.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> - <target>Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa. -SimpleX-palvelimet eivät näe profiiliasi.</target> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> + <target>Profiili jaetaan vain kontaktiesi kanssa.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> - <target>Profiilisi, kontaktisi ja toimitetut viestit tallennetaan laitteellesi.</target> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> + <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> + <target>Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa. SimpleX-palvelimet eivät näe profiiliasi.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve"> + <source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> <target>Satunnainen profiilisi</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> - <target>Palvelimesi</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> <target>Palvelimesi osoite</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your servers" xml:space="preserve"> + <source>Your servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your settings" xml:space="preserve"> <source>Your settings</source> <target>Asetuksesi</target> @@ -5900,11 +8308,19 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <target>hyväksytty puhelu</target> <note>call status</note> </trans-unit> + <trans-unit id="accepted invitation" xml:space="preserve"> + <source>accepted invitation</source> + <note>chat list item title</note> + </trans-unit> <trans-unit id="admin" xml:space="preserve"> <source>admin</source> <target>ylläpitäjä</target> <note>member role</note> </trans-unit> + <trans-unit id="admins" xml:space="preserve"> + <source>admins</source> + <note>feature role</note> + </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> <target>salauksesta sovitaan %@:lle…</target> @@ -5915,6 +8331,10 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <target>hyväksyy salausta…</target> <note>chat item text</note> </trans-unit> + <trans-unit id="all members" xml:space="preserve"> + <source>all members</source> + <note>feature role</note> + </trans-unit> <trans-unit id="always" xml:space="preserve"> <source>always</source> <target>aina</target> @@ -5924,6 +8344,14 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <source>and %lld other events</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="archived report" xml:space="preserve"> + <source>archived report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="attempts" xml:space="preserve"> + <source>attempts</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <source>audio call (not e2e encrypted)</source> <target>äänipuhelu (ei e2e-salattu)</target> @@ -5945,13 +8373,26 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> </trans-unit> <trans-unit id="blocked" xml:space="preserve"> <source>blocked</source> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="blocked %@" xml:space="preserve"> + <source>blocked %@</source> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="blocked by admin" xml:space="preserve"> + <source>blocked by admin</source> + <note>blocked chat item +marked deleted chat item preview text</note> </trans-unit> <trans-unit id="bold" xml:space="preserve"> <source>bold</source> <target>lihavoitu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="call" xml:space="preserve"> + <source>call</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="call error" xml:space="preserve"> <source>call error</source> <target>soittovirhe</target> @@ -6054,7 +8495,7 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> <target>yhdistää…</target> - <note>chat list item title</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> @@ -6066,6 +8507,10 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <target>yhteys:%@</target> <note>connection information</note> </trans-unit> + <trans-unit id="contact %@ changed to %@" xml:space="preserve"> + <source>contact %1$@ changed to %2$@</source> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> <target>kontaktilla on e2e-salaus</target> @@ -6096,10 +8541,15 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <target>päivää</target> <note>time unit</note> </trans-unit> + <trans-unit id="decryption errors" xml:space="preserve"> + <source>decryption errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> <target>oletusarvo (%@)</target> - <note>pref value</note> + <note>delete after time +pref value</note> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> @@ -6145,6 +8595,10 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <target>päällekkäinen viesti</target> <note>integrity error chat item</note> </trans-unit> + <trans-unit id="duplicates" xml:space="preserve"> + <source>duplicates</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> <target>e2e-salattu</target> @@ -6220,9 +8674,12 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <target>virhe</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="event happened" xml:space="preserve"> - <source>event happened</source> - <target>tapahtuma tapahtui</target> + <trans-unit id="expired" xml:space="preserve"> + <source>expired</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="forwarded" xml:space="preserve"> + <source>forwarded</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> @@ -6250,6 +8707,10 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <target>iOS-Avainnippua käytetään tunnuslauseen turvalliseen tallentamiseen sen muuttamisen tai sovelluksen uudelleen käynnistämisen jälkeen - se mahdollistaa push-ilmoitusten vastaanottamisen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="inactive" xml:space="preserve"> + <source>inactive</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> <target>incognito kontaktilinkin kautta</target> @@ -6290,6 +8751,10 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <target>kutsu ryhmään %@</target> <note>group name</note> </trans-unit> + <trans-unit id="invite" xml:space="preserve"> + <source>invite</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> <target>kutsuttu</target> @@ -6335,11 +8800,19 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <target>jäsen</target> <note>member role</note> </trans-unit> + <trans-unit id="member %@ changed to %@" xml:space="preserve"> + <source>member %1$@ changed to %2$@</source> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> <target>yhdistetty</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="message" xml:space="preserve"> + <source>message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="message received" xml:space="preserve"> <source>message received</source> <target>viesti vastaanotettu</target> @@ -6363,7 +8836,11 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <trans-unit id="moderated by %@" xml:space="preserve"> <source>moderated by %@</source> <target>%@ moderoi</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="moderator" xml:space="preserve"> + <source>moderator</source> + <note>member role</note> </trans-unit> <trans-unit id="months" xml:space="preserve"> <source>months</source> @@ -6373,7 +8850,7 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <trans-unit id="never" xml:space="preserve"> <source>never</source> <target>ei koskaan</target> - <note>No comment provided by engineer.</note> + <note>delete after time</note> </trans-unit> <trans-unit id="new message" xml:space="preserve"> <source>new message</source> @@ -6404,8 +8881,8 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <source>off</source> <target>pois</target> <note>enabled status - group pref value - time to disappear</note> +group pref value +time to disappear</note> </trans-unit> <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> @@ -6422,16 +8899,40 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <target>päällä</target> <note>group pref value</note> </trans-unit> + <trans-unit id="other" xml:space="preserve"> + <source>other</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="other errors" xml:space="preserve"> + <source>other errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="owner" xml:space="preserve"> <source>owner</source> <target>omistaja</target> <note>member role</note> </trans-unit> + <trans-unit id="owners" xml:space="preserve"> + <source>owners</source> + <note>feature role</note> + </trans-unit> <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> <target>vertais</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="pending" xml:space="preserve"> + <source>pending</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="pending approval" xml:space="preserve"> + <source>pending approval</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="quantum resistant e2e encryption" xml:space="preserve"> + <source>quantum resistant e2e encryption</source> + <note>chat item text</note> + </trans-unit> <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> <target>vastaus saatu…</target> @@ -6442,6 +8943,10 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <target>vahvistus saatu…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="rejected" xml:space="preserve"> + <source>rejected</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> <target>hylätty puhelu</target> @@ -6457,11 +8962,35 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <target>%@ poistettu</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="removed contact address" xml:space="preserve"> + <source>removed contact address</source> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="removed profile picture" xml:space="preserve"> + <source>removed profile picture</source> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> <target>poisti sinut</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="requested to connect" xml:space="preserve"> + <source>requested to connect</source> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="saved" xml:space="preserve"> + <source>saved</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="saved from %@" xml:space="preserve"> + <source>saved from %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="search" xml:space="preserve"> + <source>search</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="sec" xml:space="preserve"> <source>sec</source> <target>sek</target> @@ -6486,6 +9015,24 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <source>send direct message</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve"> + <source>server queue info: %1$@ + +last received msg: %2$@</source> + <note>queue info</note> + </trans-unit> + <trans-unit id="set new contact address" xml:space="preserve"> + <source>set new contact address</source> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="set new profile picture" xml:space="preserve"> + <source>set new profile picture</source> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="standard end-to-end encryption" xml:space="preserve"> + <source>standard end-to-end encryption</source> + <note>chat item text</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>alkaa…</target> @@ -6501,16 +9048,36 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <target>tämä kontakti</target> <note>notification title</note> </trans-unit> + <trans-unit id="unblocked %@" xml:space="preserve"> + <source>unblocked %@</source> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="unknown" xml:space="preserve"> <source>unknown</source> <target>tuntematon</target> <note>connection info</note> </trans-unit> + <trans-unit id="unknown servers" xml:space="preserve"> + <source>unknown servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unknown status" xml:space="preserve"> + <source>unknown status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unprotected" xml:space="preserve"> + <source>unprotected</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="updated group profile" xml:space="preserve"> <source>updated group profile</source> <target>päivitetty ryhmäprofiili</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="updated profile" xml:space="preserve"> + <source>updated profile</source> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="v%@" xml:space="preserve"> <source>v%@</source> <note>No comment provided by engineer.</note> @@ -6540,6 +9107,10 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <target>releellä</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="video" xml:space="preserve"> + <source>video</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> <source>video call (not e2e encrypted)</source> <target>videopuhelu (ei e2e-salattu)</target> @@ -6565,11 +9136,19 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <target>viikkoa</target> <note>time unit</note> </trans-unit> + <trans-unit id="when IP hidden" xml:space="preserve"> + <source>when IP hidden</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="yes" xml:space="preserve"> <source>yes</source> <target>kyllä</target> <note>pref value</note> </trans-unit> + <trans-unit id="you" xml:space="preserve"> + <source>you</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> <target>sinut on kutsuttu ryhmään</target> @@ -6580,6 +9159,10 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <target>olet tarkkailija</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="you blocked %@" xml:space="preserve"> + <source>you blocked %@</source> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> <target>muutit osoitetta</target> @@ -6620,6 +9203,10 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> <target>jaoit kertalinkin incognito-tilassa</target> <note>chat list item description</note> </trans-unit> + <trans-unit id="you unblocked %@" xml:space="preserve"> + <source>you unblocked %@</source> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you: " xml:space="preserve"> <source>you: </source> <target>sinä: </target> @@ -6634,7 +9221,7 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="fi" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6670,7 +9257,7 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="fi" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> @@ -6690,4 +9277,205 @@ SimpleX-palvelimet eivät näe profiiliasi.</target> </trans-unit> </body> </file> + <file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="fi" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%d new events" xml:space="preserve"> + <source>%d new events</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From %d chat(s)" xml:space="preserve"> + <source>From %d chat(s)</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From: %@" xml:space="preserve"> + <source>From: %@</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <note>notification</note> + </trans-unit> + <trans-unit id="New messages" xml:space="preserve"> + <source>New messages</source> + <note>notification</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="fi" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> + <source>SimpleX SE</source> + <note>Bundle display name</note> + </trans-unit> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX SE</source> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> + <source>Copyright © 2024 SimpleX Chat. All rights reserved.</source> + <note>Copyright (human-readable)</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="fi" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App is locked!" xml:space="preserve"> + <source>App is locked!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Comment" xml:space="preserve"> + <source>Comment</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade required" xml:space="preserve"> + <source>Database downgrade required</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade required" xml:space="preserve"> + <source>Database upgrade required</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing file" xml:space="preserve"> + <source>Error preparing file</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing message" xml:space="preserve"> + <source>Error preparing message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No active profile" xml:space="preserve"> + <source>No active profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to downgrade the database." xml:space="preserve"> + <source>Open the app to downgrade the database.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to upgrade the database." xml:space="preserve"> + <source>Open the app to upgrade the database.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passphrase" xml:space="preserve"> + <source>Passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve"> + <source>Please create a profile in the SimpleX app</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending a message takes longer than expected." xml:space="preserve"> + <source>Sending a message takes longer than expected.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending message…" xml:space="preserve"> + <source>Sending message…</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Slow network?" xml:space="preserve"> + <source>Slow network?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unsupported format" xml:space="preserve"> + <source>Unsupported format</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wait" xml:space="preserve"> + <source>Wait</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve"> + <source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + </body> + </file> </xliff> diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/contents.json b/apps/ios/SimpleX Localizations/fi.xcloc/contents.json index 0e3ae6dc56..11f7a4861c 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/fi.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "fi", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "16.2" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 153d98be3c..59bde0650e 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -2,36 +2,9 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="fr" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> - <trans-unit id=" " xml:space="preserve"> - <source> -</source> - <target> -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (" xml:space="preserve"> - <source> (</source> - <target> (</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> <target> (peut être copié)</target> @@ -107,6 +80,11 @@ <target>%@ connecté(e)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve"> + <source>%@ downloaded</source> + <target>%@ téléchargé</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ is connected!" xml:space="preserve"> <source>%@ is connected!</source> <target>%@ est connecté·e !</target> @@ -122,16 +100,31 @@ <target>%@ est vérifié·e</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ server" xml:space="preserve"> + <source>%@ server</source> + <target>Serveur %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ servers" xml:space="preserve"> <source>%@ servers</source> <target>Serveurs %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve"> + <source>%@ uploaded</source> + <target>%@ envoyé</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ wants to connect!" xml:space="preserve"> <source>%@ wants to connect!</source> <target>%@ veut se connecter !</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve"> + <source>%1$@, %2$@</source> + <target>%1$@, %2$@</target> + <note>format for date separator in chat</note> + </trans-unit> <trans-unit id="%@, %@ and %lld members" xml:space="preserve"> <source>%@, %@ and %lld members</source> <target>%@, %@ et %lld membres</target> @@ -152,11 +145,36 @@ <target>%d jours</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve"> + <source>%d file(s) are still being downloaded.</source> + <target>%d fichier(s) en cours de téléchargement.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve"> + <source>%d file(s) failed to download.</source> + <target>Le téléchargement de %d fichier(s) a échoué.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve"> + <source>%d file(s) were deleted.</source> + <target>Le(s) fichier(s) %d a(ont) été supprimé(s).</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve"> + <source>%d file(s) were not downloaded.</source> + <target>Le(s) fichier(s) %d n'a (n'ont) pas été téléchargé(s).</target> + <note>forward confirmation reason</note> + </trans-unit> <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> <target>%d heures</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve"> + <source>%d messages not forwarded</source> + <target>%d messages non transférés</target> + <note>alert title</note> + </trans-unit> <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> <target>%d min</target> @@ -172,6 +190,11 @@ <target>%d sec</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve"> + <source>%d seconds(s)</source> + <target>%d seconde(s)</target> + <note>delete after time</note> + </trans-unit> <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> <target>%d message·s sauté·s</target> @@ -217,6 +240,11 @@ <target>%lld messages bloqués</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve"> + <source>%lld messages blocked by admin</source> + <target>%lld messages bloqués par l'administrateur</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld messages marked deleted" xml:space="preserve"> <source>%lld messages marked deleted</source> <target>%lld messages marqués comme supprimés</target> @@ -237,11 +265,6 @@ <target>%lld nouvelles langues d'interface</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> - <source>%lld second(s)</source> - <target>%lld seconde·s</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="%lld seconds" xml:space="preserve"> <source>%lld seconds</source> <target>%lld secondes</target> @@ -292,11 +315,6 @@ <target>%u messages sautés.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> - <source>(</source> - <target>(</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="(new)" xml:space="preserve"> <source>(new)</source> <target>(nouveau)</target> @@ -307,34 +325,29 @@ <target>(cet appareil v%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve"> - <source>)</source> - <target>)</target> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target>**Ajouter un contact** : pour créer un nouveau lien d'invitation.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target>**Ajouter un nouveau contact** : pour créer un lien ou code QR unique pour votre contact.</target> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve"> + <source>**Create group**: to create a new group.</source> + <target>**Créer un groupe** : pour créer un nouveau groupe.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> - <source>**Create link / QR code** for your contact to use.</source> - <target>**Créer un lien / code QR** que votre contact pourra utiliser.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Vie privée** : vérification de nouveaux messages toute les 20 minutes. Le token de l'appareil est partagé avec le serveur SimpleX, mais pas le nombre de messages ou de contacts.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Confidentiel** : ne pas utiliser le serveur de notifications SimpleX, vérification de nouveaux messages periodiquement en arrière plan (dépend de l'utilisation de l'app).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <target>**Collez le lien reçu** ou ouvrez-le dans votre navigateur et appuyez sur **Open in mobile app**.</target> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> + <target>**Remarque** : l'utilisation de la même base de données sur deux appareils interrompt le déchiffrement des messages provenant de vos connexions, par mesure de sécurité.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> @@ -342,14 +355,14 @@ <target>**Veuillez noter** : vous NE pourrez PAS récupérer ou modifier votre phrase secrète si vous la perdez.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Recommandé** : le token de l'appareil et les notifications sont envoyés au serveur de notifications SimpleX, mais pas le contenu du message, sa taille ou son auteur.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target>**Scanner le code QR** : pour vous connecter à votre contact en personne ou par appel vidéo.</target> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve"> + <source>**Scan / Paste link**: to connect via a link you received.</source> + <target>**Scanner / Coller** : pour vous connecter via un lien que vous avez reçu.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> @@ -357,6 +370,11 @@ <target>**Avertissement** : les notifications push instantanées nécessitent une phrase secrète enregistrée dans la keychain.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve"> + <source>**Warning**: the archive will be removed.</source> + <target>**Avertissement** : l'archive sera supprimée.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> <source>**e2e encrypted** audio call</source> <target>appel audio **chiffré de bout en bout**</target> @@ -372,11 +390,6 @@ <target>\*gras*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=", " xml:space="preserve"> - <source>, </source> - <target>, </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). @@ -413,11 +426,6 @@ - l'historique de modification.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="." xml:space="preserve"> - <source>.</source> - <target>.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="0 sec" xml:space="preserve"> <source>0 sec</source> <target>0 sec</target> @@ -431,7 +439,8 @@ <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> <target>1 jour</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> @@ -446,16 +455,28 @@ <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> <target>1 mois</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> <target>1 semaine</target> - <note>time interval</note> + <note>delete after time +time interval</note> + </trans-unit> + <trans-unit id="1 year" xml:space="preserve"> + <source>1 year</source> + <target>1 an</target> + <note>delete after time</note> </trans-unit> <trans-unit id="1-time link" xml:space="preserve"> <source>1-time link</source> - <target>Lien à usage unique</target> + <target>Lien unique</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> + <target>Le lien unique peut être utilisé *avec un seul contact* - partagez le en personne ou via n'importe quelle messagerie.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="5 minutes" xml:space="preserve"> @@ -473,11 +494,6 @@ <target>30 secondes</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve"> - <source>: </source> - <target>: </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> @@ -514,7 +530,7 @@ </trans-unit> <trans-unit id="Abort" xml:space="preserve"> <source>Abort</source> - <target>Annuler</target> + <target>Abandonner</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Abort changing address" xml:space="preserve"> @@ -527,31 +543,32 @@ <target>Abandonner le changement d'adresse ?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve"> - <source>About SimpleX</source> - <target>À propos de SimpleX</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve"> <source>About SimpleX Chat</source> <target>À propos de SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve"> - <source>About SimpleX address</source> - <target>À propos de l'adresse SimpleX</target> + <trans-unit id="About operators" xml:space="preserve"> + <source>About operators</source> + <target>À propos des opérateurs</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accent color" xml:space="preserve"> - <source>Accent color</source> - <target>Couleur principale</target> + <trans-unit id="Accent" xml:space="preserve"> + <source>Accent</source> + <target>Principale</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept" xml:space="preserve"> <source>Accept</source> <target>Accepter</target> <note>accept contact request via notification - accept incoming call via notification</note> +accept incoming call via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accept conditions" xml:space="preserve"> + <source>Accept conditions</source> + <target>Accepter les conditions</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept connection request?" xml:space="preserve"> <source>Accept connection request?</source> @@ -566,16 +583,47 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Accepter en incognito</target> - <note>accept contact request via notification</note> + <note>accept contact request via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve"> + <source>Accepted conditions</source> + <target>Conditions acceptées</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve"> + <source>Acknowledged</source> + <target>Reçu avec accusé de réception</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve"> + <source>Acknowledgement errors</source> + <target>Erreur d'accusé de réception</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Active" xml:space="preserve"> + <source>Active</source> + <target>Actif</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve"> + <source>Active connections</source> + <target>Connections actives</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> <target>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.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> - <source>Add preset servers</source> - <target>Ajouter des serveurs prédéfinis</target> + <trans-unit id="Add friends" xml:space="preserve"> + <source>Add friends</source> + <target>Ajouter des amis</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve"> + <source>Add list</source> + <target>Ajouter une liste</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add profile" xml:space="preserve"> @@ -583,14 +631,19 @@ <target>Ajouter un profil</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> + <target>Ajouter un serveur</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> <source>Add servers by scanning QR codes.</source> <target>Ajoutez des serveurs en scannant des codes QR.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> - <target>Ajouter un serveur…</target> + <trans-unit id="Add team members" xml:space="preserve"> + <source>Add team members</source> + <target>Ajouter des membres à l'équipe</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -598,11 +651,46 @@ <target>Ajouter à un autre appareil</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add to list" xml:space="preserve"> + <source>Add to list</source> + <target>Ajouter à la liste</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> <target>Ajouter un message d'accueil</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve"> + <source>Add your team members to the conversations.</source> + <target>Ajoutez les membres de votre équipe aux conversations.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve"> + <source>Added media & file servers</source> + <target>Ajout de serveurs de médias et de fichiers</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve"> + <source>Added message servers</source> + <target>Ajout de serveurs de messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve"> + <source>Additional accent</source> + <target>Accent additionnel</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve"> + <source>Additional accent 2</source> + <target>Accent additionnel 2</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve"> + <source>Additional secondary</source> + <target>Accent secondaire</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Address" xml:space="preserve"> <source>Address</source> <target>Adresse</target> @@ -613,6 +701,21 @@ <target>Le changement d'adresse sera annulé. L'ancienne adresse de réception sera utilisée.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve"> + <source>Address or 1-time link?</source> + <target>Adresse ou lien unique ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve"> + <source>Address settings</source> + <target>Paramètres de l'adresse</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve"> + <source>Admins can block a member for all.</source> + <target>Les admins peuvent bloquer un membre pour tous.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> <source>Admins can create the links to join groups.</source> <target>Les admins peuvent créer les liens qui permettent de rejoindre les groupes.</target> @@ -623,6 +726,16 @@ <target>Paramètres réseau avancés</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve"> + <source>Advanced settings</source> + <target>Paramètres avancés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All" xml:space="preserve"> + <source>All</source> + <target>Tout</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All app data is deleted." xml:space="preserve"> <source>All app data is deleted.</source> <target>Toutes les données de l'application sont supprimées.</target> @@ -633,16 +746,36 @@ <target>Toutes les discussions et tous les messages seront supprimés - il est impossible de revenir en arrière !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <target>Tous les chats seront supprimés de la liste %@, et la liste sera supprimée.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> <target>Toutes les données sont effacées lorsqu'il est saisi.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve"> + <source>All data is kept private on your device.</source> + <target>Toutes les données restent confinées dans votre appareil.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> <target>Tous les membres du groupe resteront connectés.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <target>Tous les messages et fichiers sont envoyés **chiffrés de bout en bout**, avec une sécurité post-quantique dans les messages directs.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone!</source> + <target>Tous les messages seront supprimés - il n'est pas possible de revenir en arrière !</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> <target>Tous les messages seront supprimés - impossible de revenir en arrière ! Les messages seront supprimés UNIQUEMENT pour vous.</target> @@ -653,6 +786,20 @@ <target>Tous les nouveaux messages de %@ seront cachés !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All profiles" xml:space="preserve"> + <source>All profiles</source> + <target>Tous les profiles</target> + <note>profile dropdown</note> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve"> + <source>All reports will be archived for you.</source> + <target>Tous les rapports seront archivés pour vous.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All servers" xml:space="preserve"> + <source>All servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All your contacts will remain connected." xml:space="preserve"> <source>All your contacts will remain connected.</source> <target>Tous vos contacts resteront connectés.</target> @@ -663,6 +810,11 @@ <target>Tous vos contacts resteront connectés. La mise à jour du profil sera envoyée à vos contacts.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target>Tous vos contacts, conversations et fichiers seront chiffrés en toute sécurité et transférés par morceaux vers les relais XFTP configurés.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow" xml:space="preserve"> <source>Allow</source> <target>Autoriser</target> @@ -673,14 +825,24 @@ <target>Autoriser les appels que si votre contact les autorise.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve"> + <source>Allow calls?</source> + <target>Autoriser les appels ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> <target>Autorise les messages éphémères seulement si votre contact vous l’autorise.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <target>Autoriser la suppression irréversible des messages uniquement si votre contact vous l'autorise.</target> + <trans-unit id="Allow downgrade" xml:space="preserve"> + <source>Allow downgrade</source> + <target>Autoriser la rétrogradation</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target>Autoriser la suppression irréversible des messages uniquement si votre contact vous l'autorise. (24 heures)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> @@ -703,9 +865,24 @@ <target>Autorise l’envoi de messages éphémères.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow to irreversibly delete sent messages.</source> - <target>Autoriser la suppression irréversible de messages envoyés.</target> + <trans-unit id="Allow sharing" xml:space="preserve"> + <source>Allow sharing</source> + <target>Autoriser le partage</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target>Autoriser la suppression irréversible de messages envoyés. (24 heures)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve"> + <source>Allow to report messsages to moderators.</source> + <target>Permettre de signaler des messages aux modérateurs.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve"> + <source>Allow to send SimpleX links.</source> + <target>Autorise l'envoi de liens SimpleX.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send files and media." xml:space="preserve"> @@ -738,9 +915,9 @@ <target>Autorise vos contacts à vous appeler.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow your contacts to irreversibly delete sent messages.</source> - <target>Autorise vos contacts à supprimer de manière irréversible les messages envoyés.</target> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target>Autorise vos contacts à supprimer de manière irréversible les messages envoyés. (24 heures)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> @@ -768,6 +945,11 @@ <target>Groupe déjà rejoint !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve"> + <source>Always use private routing.</source> + <target>Toujours utiliser le routage privé.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> <source>Always use relay</source> <target>Se connecter via relais</target> @@ -778,21 +960,40 @@ <target>Un profil de chat vierge portant le nom fourni est créé et l'application s'ouvre normalement.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Another reason" xml:space="preserve"> + <source>Another reason</source> + <target>Autre raison</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> <target>Répondre à l'appel</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> + <target>N'importe qui peut heberger un serveur.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> <target>Build de l'app : %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App data migration" xml:space="preserve"> + <source>App data migration</source> + <target>Transfert des données de l'application</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> <source>App encrypts new local files (except videos).</source> <target>L'application chiffre les nouveaux fichiers locaux (sauf les vidéos).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App group:" xml:space="preserve"> + <source>App group:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>Icône de l'app</target> @@ -808,6 +1009,11 @@ <target>Le code d'accès de l'application est remplacé par un code d'autodestruction.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App session" xml:space="preserve"> + <source>App session</source> + <target>Session de l'app</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App version" xml:space="preserve"> <source>App version</source> <target>Version de l'app</target> @@ -823,6 +1029,66 @@ <target>Apparence</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Apply" xml:space="preserve"> + <source>Apply</source> + <target>Appliquer</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve"> + <source>Apply to</source> + <target>Appliquer à</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve"> + <source>Archive</source> + <target>Archiver</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve"> + <source>Archive %lld reports?</source> + <target>Archiver les rapports %lld ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve"> + <source>Archive all reports?</source> + <target>Archiver tous les rapports ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve"> + <source>Archive and upload</source> + <target>Archiver et téléverser</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve"> + <source>Archive contacts to chat later.</source> + <target>Archiver les contacts pour discuter plus tard.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve"> + <source>Archive report</source> + <target>Archiver le rapport</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve"> + <source>Archive report?</source> + <target>Archiver le rapport ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve"> + <source>Archive reports</source> + <target>Archiver les rapports</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve"> + <source>Archived contacts</source> + <target>Contacts archivés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve"> + <source>Archiving database</source> + <target>Archivage de la base de données</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Attach" xml:space="preserve"> <source>Attach</source> <target>Attacher</target> @@ -883,11 +1149,21 @@ <target>Images auto-acceptées</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve"> + <source>Auto-accept settings</source> + <target>Paramètres de réception automatique</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Back" xml:space="preserve"> <source>Back</source> <target>Retour</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Background" xml:space="preserve"> + <source>Background</source> + <target>Fond</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Bad desktop address" xml:space="preserve"> <source>Bad desktop address</source> <target>Mauvaise adresse de bureau</target> @@ -903,21 +1179,71 @@ <target>Mauvais hash de message</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better calls" xml:space="preserve"> + <source>Better calls</source> + <target>Appels améliorés</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better groups" xml:space="preserve"> <source>Better groups</source> <target>Des groupes plus performants</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve"> + <source>Better groups performance</source> + <target>Meilleure performance des groupes</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve"> + <source>Better message dates.</source> + <target>Meilleures dates de messages.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better messages" xml:space="preserve"> <source>Better messages</source> <target>Meilleurs messages</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better networking" xml:space="preserve"> + <source>Better networking</source> + <target>Meilleure gestion de réseau</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve"> + <source>Better notifications</source> + <target>Notifications améliorées</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve"> + <source>Better privacy and security</source> + <target>Meilleure protection de la privacité et de la sécurité</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve"> + <source>Better security ✅</source> + <target>Sécurité accrue ✅</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve"> + <source>Better user experience</source> + <target>Une meilleure expérience pour l'utilisateur</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Black" xml:space="preserve"> + <source>Black</source> + <target>Noir</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block" xml:space="preserve"> <source>Block</source> <target>Bloquer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block for all" xml:space="preserve"> + <source>Block for all</source> + <target>Bloqué pour tous</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block group members" xml:space="preserve"> <source>Block group members</source> <target>Bloquer des membres d'un groupe</target> @@ -928,19 +1254,39 @@ <target>Bloquer ce membre</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve"> + <source>Block member for all?</source> + <target>Bloquer le membre pour tous ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block member?" xml:space="preserve"> <source>Block member?</source> <target>Bloquer ce membre ?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve"> + <source>Blocked by admin</source> + <target>Bloqué par l'administrateur</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve"> + <source>Blur for better privacy.</source> + <target>Rendez les images floues et protégez-les contre les regards indiscrets.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve"> + <source>Blur media</source> + <target>Flouter les médias</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <source>Both you and your contact can add message reactions.</source> <target>Vous et votre contact pouvez ajouter des réactions aux messages.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> - <source>Both you and your contact can irreversibly delete sent messages.</source> - <target>Vous et votre contact êtes tous deux en mesure de supprimer de manière irréversible les messages envoyés.</target> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target>Vous et votre contact êtes tous deux en mesure de supprimer de manière irréversible les messages envoyés. (24 heures)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> @@ -963,11 +1309,35 @@ <target>Bulgare, finnois, thaïlandais et ukrainien - grâce aux utilisateurs et à [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat) !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Business address" xml:space="preserve"> + <source>Business address</source> + <target>Adresse professionnelle</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve"> + <source>Business chats</source> + <target>Discussions professionnelles</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve"> + <source>Businesses</source> + <target>Entreprises</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>Par profil de chat (par défaut) ou [par connexion](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve"> + <source>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</source> + <target>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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> <source>Call already ended!</source> <target>Appel déjà terminé !</target> @@ -978,9 +1348,24 @@ <target>Appels</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve"> - <source>Can't delete user profile!</source> - <target>Impossible de supprimer le profil d'utilisateur !</target> + <trans-unit id="Calls prohibited!" xml:space="preserve"> + <source>Calls prohibited!</source> + <target>Les appels ne sont pas autorisés !</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve"> + <source>Camera not available</source> + <target>Caméra non disponible</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve"> + <source>Can't call contact</source> + <target>Impossible d'appeler le contact</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve"> + <source>Can't call member</source> + <target>Impossible d'appeler le membre</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contact!" xml:space="preserve"> @@ -993,9 +1378,20 @@ <target>Impossible d'inviter les contacts !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve"> + <source>Can't message member</source> + <target>Impossible d'envoyer un message à ce membre</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> <target>Annuler</target> + <note>alert action +alert button</note> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve"> + <source>Cancel migration</source> + <target>Annuler le transfert</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> @@ -1003,9 +1399,24 @@ <target>Impossible d'accéder à la keychain pour enregistrer le mot de passe de la base de données</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Impossible de transférer le message</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> <target>Impossible de recevoir le fichier</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <target>Capacité dépassée - le destinataire n'a pas pu recevoir les messages envoyés précédemment.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve"> + <source>Cellular</source> + <target>Cellulaire</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change" xml:space="preserve"> @@ -1013,6 +1424,16 @@ <target>Changer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve"> + <source>Change automatic message deletion?</source> + <target>Modifier la suppression automatique des messages ?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve"> + <source>Change chat profiles</source> + <target>Changer de profil de discussion</target> + <note>authentication reason</note> + </trans-unit> <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> <target>Changer la phrase secrète de la base de données ?</target> @@ -1057,11 +1478,26 @@ <source>Change self-destruct passcode</source> <target>Modifier le code d'autodestruction</target> <note>authentication reason - set passcode view</note> +set passcode view</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> - <source>Chat archive</source> - <target>Archives du chat</target> + <trans-unit id="Chat" xml:space="preserve"> + <source>Chat</source> + <target>Discussions</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve"> + <source>Chat already exists</source> + <target>La discussion existe déjà</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve"> + <source>Chat already exists!</source> + <target>La discussion existe déjà !</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve"> + <source>Chat colors</source> + <target>Couleurs de chat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat console" xml:space="preserve"> @@ -1079,6 +1515,11 @@ <target>Base de données du chat supprimée</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve"> + <source>Chat database exported</source> + <target>Exportation de la base de données des discussions</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> <target>Base de données du chat importée</target> @@ -1094,26 +1535,81 @@ <target>Le chat est arrêté</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <target>Le chat est arrêté. Si vous avez déjà utilisé cette base de données sur un autre appareil, vous devez la transférer à nouveau avant de démarrer le chat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve"> + <source>Chat list</source> + <target>Liste de discussion</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve"> + <source>Chat migrated!</source> + <target>Messagerie transférée !</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> <target>Préférences de chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve"> + <source>Chat preferences were changed.</source> + <target>Les préférences de discussion ont été modifiées.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve"> + <source>Chat profile</source> + <target>Profil d'utilisateur</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve"> + <source>Chat theme</source> + <target>Thème de chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <target>La discussion sera supprimé pour tous les membres - cela ne peut pas être annulé !</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <target>Le discussion sera supprimé pour vous - il n'est pas possible de revenir en arrière !</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> - <target>Chats</target> + <target>Discussions</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve"> + <source>Check messages every 20 min.</source> + <target>Consulter les messages toutes les 20 minutes.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve"> + <source>Check messages when allowed.</source> + <target>Consulter les messages quand c'est possible.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> <target>Vérifiez l'adresse du serveur et réessayez.</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> <target>Interface en chinois et en espagnol</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <target>Choisissez _Transferer depuis un autre appareil_ sur le nouvel appareil et scannez le code QR.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> <target>Choisir le fichier</target> @@ -1124,10 +1620,25 @@ <target>Choisir dans la photothèque</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve"> + <source>Chunks deleted</source> + <target>Chunks supprimés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve"> + <source>Chunks downloaded</source> + <target>Chunks téléchargés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve"> + <source>Chunks uploaded</source> + <target>Chunks téléversés</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> <target>Effacer</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> @@ -1139,16 +1650,41 @@ <target>Effacer la conversation ?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve"> + <source>Clear group?</source> + <target>Vider le groupe ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve"> + <source>Clear or delete group?</source> + <target>Vider ou supprimer le groupe ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve"> + <source>Clear private notes?</source> + <target>Effacer les notes privées ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear verification" xml:space="preserve"> <source>Clear verification</source> <target>Retirer la vérification</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> - <source>Colors</source> - <target>Couleurs</target> + <trans-unit id="Color chats with the new themes." xml:space="preserve"> + <source>Color chats with the new themes.</source> + <target>Colorez vos discussions avec les nouveaux thèmes.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Color mode" xml:space="preserve"> + <source>Color mode</source> + <target>Mode de couleur</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve"> + <source>Community guidelines violation</source> + <target>Infraction aux règles communautaires</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Compare file" xml:space="preserve"> <source>Compare file</source> <target>Comparer le fichier</target> @@ -1159,11 +1695,56 @@ <target>Comparez les codes de sécurité avec vos contacts.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Completed" xml:space="preserve"> + <source>Completed</source> + <target>Complétées</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve"> + <source>Conditions accepted on: %@.</source> + <target>Conditions acceptées le : %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <target>Les conditions sont acceptées pour le(s) opérateur(s) : **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <target>Les conditions sont déjà acceptées pour ces opérateurs : **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve"> + <source>Conditions of use</source> + <target>Conditions d'utilisation</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <target>Les conditions seront acceptées pour le(s) opérateur(s) : **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve"> + <source>Conditions will be accepted on: %@.</source> + <target>Les conditions seront acceptées le : %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <target>Les conditions seront automatiquement acceptées pour les opérateurs activés le : %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> <target>Configurer les serveurs ICE</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Configure server operators" xml:space="preserve"> + <source>Configure server operators</source> + <target>Configurer les opérateurs de serveur</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> <target>Confirmer</target> @@ -1174,11 +1755,26 @@ <target>Confirmer le code d'accès</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve"> + <source>Confirm contact deletion?</source> + <target>Confirmer la suppression du contact ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm database upgrades" xml:space="preserve"> <source>Confirm database upgrades</source> <target>Confirmer la mise à niveau de la base de données</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve"> + <source>Confirm files from unknown servers.</source> + <target>Confirmer les fichiers provenant de serveurs inconnus.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve"> + <source>Confirm network settings</source> + <target>Confirmer les paramètres réseau</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> <target>Confirmer la nouvelle phrase secrète…</target> @@ -1189,6 +1785,21 @@ <target>Confirmer le mot de passe</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <target>Confirmer que vous vous souvenez de la phrase secrète de la base de données pour la transférer.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve"> + <source>Confirm upload</source> + <target>Confirmer la transmission</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve"> + <source>Confirmed</source> + <target>Confirmé</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> <target>Se connecter</target> @@ -1206,7 +1817,12 @@ </trans-unit> <trans-unit id="Connect to desktop" xml:space="preserve"> <source>Connect to desktop</source> - <target>Se connecter au bureau</target> + <target>Connexion au bureau</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve"> + <source>Connect to your friends faster.</source> + <target>Connectez-vous à vos amis plus rapidement.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect to yourself?" xml:space="preserve"> @@ -1238,11 +1854,6 @@ Il s'agit de votre propre lien unique !</target> <target>Se connecter via un lien</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve"> - <source>Connect via link / QR code</source> - <target>Se connecter via un lien / code QR</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connect via one-time link" xml:space="preserve"> <source>Connect via one-time link</source> <target>Se connecter via un lien unique</target> @@ -1253,16 +1864,31 @@ Il s'agit de votre propre lien unique !</target> <target>Se connecter avec %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected" xml:space="preserve"> + <source>Connected</source> + <target>Connecté</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected desktop" xml:space="preserve"> <source>Connected desktop</source> <target>Bureau connecté</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve"> + <source>Connected servers</source> + <target>Serveurs connectés</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected to desktop" xml:space="preserve"> <source>Connected to desktop</source> <target>Connecté au bureau</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting" xml:space="preserve"> + <source>Connecting</source> + <target>Connexion</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> <source>Connecting to server…</source> <target>Connexion au serveur…</target> @@ -1273,6 +1899,11 @@ Il s'agit de votre propre lien unique !</target> <target>Connexion au serveur… (erreur : %@)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve"> + <source>Connecting to contact, please wait or check later!</source> + <target>Connexion au contact, veuillez patienter ou vérifier plus tard !</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting to desktop" xml:space="preserve"> <source>Connecting to desktop</source> <target>Connexion au bureau</target> @@ -1283,6 +1914,16 @@ Il s'agit de votre propre lien unique !</target> <target>Connexion</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve"> + <source>Connection and servers status.</source> + <target>État de la connexion et des serveurs.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve"> + <source>Connection blocked</source> + <target>Connexion bloquée</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> <target>Erreur de connexion</target> @@ -1293,11 +1934,38 @@ Il s'agit de votre propre lien unique !</target> <target>Erreur de connexion (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve"> + <source>Connection is blocked by server operator: +%@</source> + <target>La connexion est bloquée par l'opérateur du serveur : +%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve"> + <source>Connection not ready.</source> + <target>La connexion n'est pas prête.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve"> + <source>Connection notifications</source> + <target>Notifications de connexion</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Demande de connexion envoyée !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve"> + <source>Connection requires encryption renegotiation.</source> + <target>La connexion nécessite une renégociation du cryptage.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve"> + <source>Connection security</source> + <target>Sécurité des connexions</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection terminated" xml:space="preserve"> <source>Connection terminated</source> <target>Connexion terminée</target> @@ -1308,6 +1976,16 @@ Il s'agit de votre propre lien unique !</target> <target>Délai de connexion</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve"> + <source>Connection with desktop stopped</source> + <target>La connexion avec le bureau s'est arrêtée</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve"> + <source>Connections</source> + <target>Connexions</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact allows" xml:space="preserve"> <source>Contact allows</source> <target>Votre contact autorise</target> @@ -1318,6 +1996,11 @@ Il s'agit de votre propre lien unique !</target> <target>Contact déjà existant</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact deleted!" xml:space="preserve"> + <source>Contact deleted!</source> + <target>Contact supprimé !</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> <target>Contact masqué :</target> @@ -1328,9 +2011,9 @@ Il s'agit de votre propre lien unique !</target> <target>Le contact est connecté</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve"> - <source>Contact is not connected yet!</source> - <target>Le contact n'est pas encore connecté !</target> + <trans-unit id="Contact is deleted." xml:space="preserve"> + <source>Contact is deleted.</source> + <target>Le contact est supprimé.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact name" xml:space="preserve"> @@ -1343,6 +2026,11 @@ Il s'agit de votre propre lien unique !</target> <target>Préférences de contact</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve"> + <source>Contact will be deleted - this cannot be undone!</source> + <target>Le contact sera supprimé - il n'est pas possible de revenir en arrière !</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> <target>Contacts</target> @@ -1353,21 +2041,41 @@ Il s'agit de votre propre lien unique !</target> <target>Vos contacts peuvent marquer les messages pour les supprimer ; vous pourrez les consulter.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve"> + <source>Content violates conditions of use</source> + <target>Le contenu enfreint les conditions d'utilisation</target> + <note>blocking reason</note> + </trans-unit> <trans-unit id="Continue" xml:space="preserve"> <source>Continue</source> <target>Continuer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve"> + <source>Conversation deleted!</source> + <target>Conversation supprimée !</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> <target>Copier</target> - <note>chat item action</note> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve"> + <source>Copy error</source> + <target>Erreur de copie</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> <target>Version du cœur : v%@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Corner" xml:space="preserve"> + <source>Corner</source> + <target>Coin</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Correct name to %@?" xml:space="preserve"> <source>Correct name to %@?</source> <target>Corriger le nom pour %@ ?</target> @@ -1378,6 +2086,11 @@ Il s'agit de votre propre lien unique !</target> <target>Créer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve"> + <source>Create 1-time link</source> + <target>Créer un lien unique</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> <target>Créer une adresse SimpleX</target> @@ -1388,11 +2101,6 @@ Il s'agit de votre propre lien unique !</target> <target>Création de groupes via un profil aléatoire.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> - <source>Create an address to let people connect with you.</source> - <target>Créez une adresse pour permettre aux gens de vous contacter.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create file" xml:space="preserve"> <source>Create file</source> <target>Créer un fichier</target> @@ -1413,16 +2121,16 @@ Il s'agit de votre propre lien unique !</target> <target>Créer un lien</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create list" xml:space="preserve"> + <source>Create list</source> + <target>Créer une liste</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> <target>Créer un nouveau profil sur [l'application de bureau](https://simplex.chat/downloads/). 💻</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve"> - <source>Create one-time invitation link</source> - <target>Créer un lien d'invitation unique</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create profile" xml:space="preserve"> <source>Create profile</source> <target>Créer le profil</target> @@ -1443,9 +2151,29 @@ Il s'agit de votre propre lien unique !</target> <target>Créez votre profil</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve"> - <source>Created on %@</source> - <target>Créé le %@</target> + <trans-unit id="Created" xml:space="preserve"> + <source>Created</source> + <target>Créées</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve"> + <source>Created at</source> + <target>Créé à</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve"> + <source>Created at: %@</source> + <target>Créé à : %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve"> + <source>Creating archive link</source> + <target>Création d'un lien d'archive</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve"> + <source>Creating link…</source> + <target>Création d'un lien…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current Passcode" xml:space="preserve"> @@ -1453,11 +2181,21 @@ Il s'agit de votre propre lien unique !</target> <target>Code d'accès actuel</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <target>Le texte sur les conditions actuelles n'a pas pu être chargé. Vous pouvez consulter les conditions en cliquant sur ce lien :</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> <target>Phrase secrète actuelle…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current profile" xml:space="preserve"> + <source>Current profile</source> + <target>Profil actuel</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> <target>Actuellement, la taille maximale des fichiers supportés est de %@.</target> @@ -1468,11 +2206,26 @@ Il s'agit de votre propre lien unique !</target> <target>Délai personnalisé</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve"> + <source>Customizable message shape.</source> + <target>Forme des messages personnalisable.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve"> + <source>Customize theme</source> + <target>Personnaliser le thème</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> <target>Sombre</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve"> + <source>Dark mode colors</source> + <target>Couleurs en mode sombre</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> <target>ID de base de données</target> @@ -1571,6 +2324,11 @@ Il s'agit de votre propre lien unique !</target> <target>La base de données sera migrée lors du redémarrage de l'app</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Debug delivery" xml:space="preserve"> + <source>Debug delivery</source> + <target>Livraison de débogage</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> <target>Décentralisé</target> @@ -1584,18 +2342,19 @@ Il s'agit de votre propre lien unique !</target> <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> <target>Supprimer</target> - <note>chat item action</note> + <note>alert action +swipe action</note> + </trans-unit> + <trans-unit id="Delete %lld messages of members?" xml:space="preserve"> + <source>Delete %lld messages of members?</source> + <target>Supprimer %lld messages de membres ?</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete %lld messages?" xml:space="preserve"> <source>Delete %lld messages?</source> <target>Supprimer %lld messages ?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve"> - <source>Delete Contact</source> - <target>Supprimer le contact</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Delete address" xml:space="preserve"> <source>Delete address</source> <target>Supprimer l'adresse</target> @@ -1621,14 +2380,14 @@ Il s'agit de votre propre lien unique !</target> <target>Supprimer et en informer le contact</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve"> - <source>Delete archive</source> - <target>Supprimer l'archive</target> + <trans-unit id="Delete chat" xml:space="preserve"> + <source>Delete chat</source> + <target>Supprimer la discussion</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve"> - <source>Delete chat archive?</source> - <target>Supprimer l'archive du chat ?</target> + <trans-unit id="Delete chat messages from your device." xml:space="preserve"> + <source>Delete chat messages from your device.</source> + <target>Supprimer les messages de chat de votre appareil.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile" xml:space="preserve"> @@ -1641,6 +2400,11 @@ Il s'agit de votre propre lien unique !</target> <target>Supprimer le profil du chat ?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete chat?" xml:space="preserve"> + <source>Delete chat?</source> + <target>Supprimer la discussion ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> <source>Delete connection</source> <target>Supprimer la connexion</target> @@ -1651,11 +2415,9 @@ Il s'agit de votre propre lien unique !</target> <target>Supprimer le contact</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact? This cannot be undone!" xml:space="preserve"> - <source>Delete contact? -This cannot be undone!</source> - <target>Supprimer le contact ? -Cette opération ne peut être annulée !</target> + <trans-unit id="Delete contact?" xml:space="preserve"> + <source>Delete contact?</source> + <target>Supprimer le contact ?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete database" xml:space="preserve"> @@ -1663,6 +2425,11 @@ Cette opération ne peut être annulée !</target> <target>Supprimer la base de données</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete database from this device" xml:space="preserve"> + <source>Delete database from this device</source> + <target>Supprimer la base de données de cet appareil</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete file" xml:space="preserve"> <source>Delete file</source> <target>Supprimer le fichier</target> @@ -1713,6 +2480,11 @@ Cette opération ne peut être annulée !</target> <target>Supprimer le lien ?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete list?" xml:space="preserve"> + <source>Delete list?</source> + <target>Supprimer la liste ?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> <target>Supprimer le message de ce membre ?</target> @@ -1726,7 +2498,7 @@ Cette opération ne peut être annulée !</target> <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> <target>Supprimer les messages</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> @@ -1743,9 +2515,9 @@ Cette opération ne peut être annulée !</target> <target>Supprimer l'ancienne base de données ?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve"> - <source>Delete pending connection</source> - <target>Supprimer la connexion en attente</target> + <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> + <source>Delete or moderate up to 200 messages.</source> + <target>Supprimer ou modérer jusqu'à 200 messages.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection?" xml:space="preserve"> @@ -1763,11 +2535,31 @@ Cette opération ne peut être annulée !</target> <target>Supprimer la file d'attente</target> <note>server test step</note> </trans-unit> + <trans-unit id="Delete report" xml:space="preserve"> + <source>Delete report</source> + <target>Supprimer le rapport</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete up to 20 messages at once." xml:space="preserve"> + <source>Delete up to 20 messages at once.</source> + <target>Supprimez jusqu'à 20 messages à la fois.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> <target>Supprimer le profil utilisateur ?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete without notification" xml:space="preserve"> + <source>Delete without notification</source> + <target>Supprimer sans notification</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve"> + <source>Deleted</source> + <target>Supprimées</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Deleted at" xml:space="preserve"> <source>Deleted at</source> <target>Supprimé à</target> @@ -1778,6 +2570,16 @@ Cette opération ne peut être annulée !</target> <target>Supprimé à : %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve"> + <source>Deletion errors</source> + <target>Erreurs de suppression</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivered even when Apple drops them." xml:space="preserve"> + <source>Delivered even when Apple drops them.</source> + <target>Distribués même quand Apple les oublie.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery" xml:space="preserve"> <source>Delivery</source> <target>Distribution</target> @@ -1790,7 +2592,7 @@ Cette opération ne peut être annulée !</target> </trans-unit> <trans-unit id="Delivery receipts!" xml:space="preserve"> <source>Delivery receipts!</source> - <target>Justificatifs de réception!</target> + <target>Justificatifs de réception !</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Description" xml:space="preserve"> @@ -1813,11 +2615,41 @@ Cette opération ne peut être annulée !</target> <target>Appareils de bureau</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve"> + <source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source> + <target>L'adresse du serveur de destination %@ est incompatible avec les paramètres du serveur de redirection %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server error: %@" xml:space="preserve"> + <source>Destination server error: %@</source> + <target>Erreur du serveur de destination : %@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve"> + <source>Destination server version of %@ is incompatible with forwarding server %@.</source> + <target>La version du serveur de destination %@ est incompatible avec le serveur de redirection %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Detailed statistics" xml:space="preserve"> + <source>Detailed statistics</source> + <target>Statistiques détaillées</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Details" xml:space="preserve"> + <source>Details</source> + <target>Détails</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Develop" xml:space="preserve"> <source>Develop</source> <target>Développer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Developer options" xml:space="preserve"> + <source>Developer options</source> + <target>Options pour les développeurs</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> <target>Outils du développeur</target> @@ -1848,8 +2680,13 @@ Cette opération ne peut être annulée !</target> <target>Messages directs</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve"> + <source>Direct messages between members are prohibited in this chat.</source> + <target>Les messages directs entre membres sont interdits dans cette discussion.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> <target>Les messages directs entre membres sont interdits dans ce groupe.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1863,11 +2700,26 @@ Cette opération ne peut être annulée !</target> <target>Désactiver SimpleX Lock</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Disable automatic message deletion?" xml:space="preserve"> + <source>Disable automatic message deletion?</source> + <target>Désactiver la suppression automatique des messages ?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Disable delete messages" xml:space="preserve"> + <source>Disable delete messages</source> + <target>Désactiver la suppression des messages</target> + <note>alert button</note> + </trans-unit> <trans-unit id="Disable for all" xml:space="preserve"> <source>Disable for all</source> <target>Désactiver pour tous</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Disabled" xml:space="preserve"> + <source>Disabled</source> + <target>Désactivé</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> <source>Disappearing message</source> <target>Message éphémère</target> @@ -1883,8 +2735,8 @@ Cette opération ne peut être annulée !</target> <target>Les messages éphémères sont interdits dans cette discussion.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <target>Les messages éphémères sont interdits dans ce groupe.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1918,16 +2770,41 @@ Cette opération ne peut être annulée !</target> <target>Rechercher sur le réseau</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve"> + <source>Do NOT send messages directly, even if your or destination server does not support private routing.</source> + <target>Ne pas envoyer de messages directement, même si votre serveur ou le serveur de destination ne prend pas en charge le routage privé.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> <source>Do NOT use SimpleX for emergency calls.</source> <target>N'utilisez PAS SimpleX pour les appels d'urgence.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT use private routing." xml:space="preserve"> + <source>Do NOT use private routing.</source> + <target>Ne pas utiliser de routage privé.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do it later" xml:space="preserve"> <source>Do it later</source> <target>Faites-le plus tard</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do not send history to new members." xml:space="preserve"> + <source>Do not send history to new members.</source> + <target>Ne pas envoyer d'historique aux nouveaux membres.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not use credentials with proxy." xml:space="preserve"> + <source>Do not use credentials with proxy.</source> + <target>Ne pas utiliser d'identifiants avec le proxy.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Documents:" xml:space="preserve"> + <source>Documents:</source> + <target>Documents:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> <target>Ne pas créer d'adresse</target> @@ -1938,21 +2815,72 @@ Cette opération ne peut être annulée !</target> <target>Ne pas activer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Don't miss important messages." xml:space="preserve"> + <source>Don't miss important messages.</source> + <target>Ne manquez pas les messages importants.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> <target>Ne plus afficher</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Done" xml:space="preserve"> + <source>Done</source> + <target>Terminé</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Downgrade and open chat" xml:space="preserve"> <source>Downgrade and open chat</source> <target>Rétrograder et ouvrir le chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Download" xml:space="preserve"> + <source>Download</source> + <target>Télécharger</target> + <note>alert button +chat item action</note> + </trans-unit> + <trans-unit id="Download errors" xml:space="preserve"> + <source>Download errors</source> + <target>Erreurs de téléchargement</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download failed" xml:space="preserve"> + <source>Download failed</source> + <target>Échec du téléchargement</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Download file" xml:space="preserve"> <source>Download file</source> <target>Télécharger le fichier</target> <note>server test step</note> </trans-unit> + <trans-unit id="Download files" xml:space="preserve"> + <source>Download files</source> + <target>Télécharger les fichiers</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Downloaded" xml:space="preserve"> + <source>Downloaded</source> + <target>Téléchargé</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloaded files" xml:space="preserve"> + <source>Downloaded files</source> + <target>Fichiers téléchargés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading archive" xml:space="preserve"> + <source>Downloading archive</source> + <target>Téléchargement de l'archive</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading link details" xml:space="preserve"> + <source>Downloading link details</source> + <target>Téléchargement des détails du lien</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> <target>Nom d'affichage en double !</target> @@ -1963,6 +2891,11 @@ Cette opération ne peut être annulée !</target> <target>Durée</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="E2E encrypted notifications." xml:space="preserve"> + <source>E2E encrypted notifications.</source> + <target>Notifications chiffrées E2E.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> <target>Modifier</target> @@ -1983,6 +2916,11 @@ Cette opération ne peut être annulée !</target> <target>Activer (conserver les remplacements)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve"> + <source>Enable Flux in Network & servers settings for better metadata privacy.</source> + <target>Activez Flux dans les paramètres du réseau et des serveurs pour une meilleure confidentialité des métadonnées.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> <target>Activer SimpleX Lock</target> @@ -1996,6 +2934,11 @@ Cette opération ne peut être annulée !</target> <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> <target>Activer la suppression automatique des messages ?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Enable camera access" xml:space="preserve"> + <source>Enable camera access</source> + <target>Autoriser l'accès à la caméra</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable for all" xml:space="preserve"> @@ -2003,6 +2946,11 @@ Cette opération ne peut être annulée !</target> <target>Activer pour tous</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve"> + <source>Enable in direct chats (BETA)!</source> + <target>Activer dans les conversations directes (BETA) !</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> <target>Activer les notifications instantanées ?</target> @@ -2033,6 +2981,16 @@ Cette opération ne peut être annulée !</target> <target>Activer le code d'autodestruction</target> <note>set passcode view</note> </trans-unit> + <trans-unit id="Enabled" xml:space="preserve"> + <source>Enabled</source> + <target>Activé</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve"> + <source>Enabled for</source> + <target>Activé pour</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> <target>Chiffrer</target> @@ -2063,6 +3021,11 @@ Cette opération ne peut être annulée !</target> <target>Message chiffrée ou autre événement</target> <note>notification</note> </trans-unit> + <trans-unit id="Encrypted message: app is stopped" xml:space="preserve"> + <source>Encrypted message: app is stopped</source> + <target>Message chiffré : l'application est arrêtée</target> + <note>notification</note> + </trans-unit> <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> <target>Message chiffrée : erreur de base de données</target> @@ -2098,6 +3061,11 @@ Cette opération ne peut être annulée !</target> <target>La renégociation du chiffrement a échoué.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encryption renegotiation in progress." xml:space="preserve"> + <source>Encryption renegotiation in progress.</source> + <target>Renégociation du chiffrement en cours.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter Passcode" xml:space="preserve"> <source>Enter Passcode</source> <target>Entrer le code d'accès</target> @@ -2113,6 +3081,11 @@ Cette opération ne peut être annulée !</target> <target>Entrer un nom de groupe…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve"> + <source>Enter passphrase</source> + <target>Entrer la phrase secrète</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter passphrase…" xml:space="preserve"> <source>Enter passphrase…</source> <target>Entrez la phrase secrète…</target> @@ -2158,26 +3131,36 @@ Cette opération ne peut être annulée !</target> <target>Erreur lors de l'annulation du changement d'adresse</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error accepting conditions" xml:space="preserve"> + <source>Error accepting conditions</source> + <target>Erreur lors de la validation des conditions</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> <target>Erreur de validation de la demande de contact</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve"> - <source>Error accessing database file</source> - <target>Erreur d'accès au fichier de la base de données</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> <target>Erreur lors de l'ajout de membre·s</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error adding server" xml:space="preserve"> + <source>Error adding server</source> + <target>Erreur lors de l'ajout du serveur</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> <target>Erreur de changement d'adresse</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing connection profile" xml:space="preserve"> + <source>Error changing connection profile</source> + <target>Erreur lors du changement de profil de connexion</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error changing role" xml:space="preserve"> <source>Error changing role</source> <target>Erreur lors du changement de rôle</target> @@ -2188,6 +3171,21 @@ Cette opération ne peut être annulée !</target> <target>Erreur de changement de paramètre</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing to incognito!" xml:space="preserve"> + <source>Error changing to incognito!</source> + <target>Erreur lors du passage en mode incognito !</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error checking token status" xml:space="preserve"> + <source>Error checking token status</source> + <target>Erreur lors de la vérification de l'état du jeton (token)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve"> + <source>Error connecting to forwarding server %@. Please try later.</source> + <target>Erreur de connexion au serveur de redirection %@. Veuillez réessayer plus tard.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> <target>Erreur lors de la création de l'adresse</target> @@ -2203,16 +3201,31 @@ Cette opération ne peut être annulée !</target> <target>Erreur lors de la création du lien du groupe</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating list" xml:space="preserve"> + <source>Error creating list</source> + <target>Erreur lors de la création de la liste</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error creating member contact" xml:space="preserve"> <source>Error creating member contact</source> <target>Erreur lors de la création du contact du membre</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating message" xml:space="preserve"> + <source>Error creating message</source> + <target>Erreur lors de la création du message</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>Erreur lors de la création du profil !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating report" xml:space="preserve"> + <source>Error creating report</source> + <target>Erreur lors de la création du rapport</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error decrypting file" xml:space="preserve"> <source>Error decrypting file</source> <target>Erreur lors du déchiffrement du fichier</target> @@ -2233,11 +3246,6 @@ Cette opération ne peut être annulée !</target> <target>Erreur lors de la suppression de la connexion</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve"> - <source>Error deleting contact</source> - <target>Erreur lors de la suppression du contact</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> <target>Erreur lors de la suppression de la base de données</target> @@ -2258,6 +3266,11 @@ Cette opération ne peut être annulée !</target> <target>Erreur lors de la suppression du profil utilisateur</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error downloading the archive" xml:space="preserve"> + <source>Error downloading the archive</source> + <target>Erreur lors du téléchargement de l'archive</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> <source>Error enabling delivery receipts!</source> <target>Erreur lors de l'activation des accusés de réception !</target> @@ -2278,6 +3291,11 @@ Cette opération ne peut être annulée !</target> <target>Erreur lors de l'exportation de la base de données du chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error exporting theme: %@" xml:space="preserve"> + <source>Error exporting theme: %@</source> + <target>Erreur d'exportation du thème : %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> <target>Erreur lors de l'importation de la base de données du chat</target> @@ -2288,24 +3306,54 @@ Cette opération ne peut être annulée !</target> <target>Erreur lors de la liaison avec le groupe</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve"> - <source>Error loading %@ servers</source> - <target>Erreur lors du chargement des serveurs %@</target> + <trans-unit id="Error loading servers" xml:space="preserve"> + <source>Error loading servers</source> + <target>Erreur de chargement des serveurs</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error migrating settings" xml:space="preserve"> + <source>Error migrating settings</source> + <target>Erreur lors de la migration des paramètres</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error opening chat" xml:space="preserve"> + <source>Error opening chat</source> + <target>Erreur lors de l'ouverture du chat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> <target>Erreur lors de la réception du fichier</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error reconnecting server" xml:space="preserve"> + <source>Error reconnecting server</source> + <target>Erreur de reconnexion du serveur</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error reconnecting servers" xml:space="preserve"> + <source>Error reconnecting servers</source> + <target>Erreur de reconnexion des serveurs</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error registering for notifications" xml:space="preserve"> + <source>Error registering for notifications</source> + <target>Erreur lors de l'inscription aux notifications</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> <target>Erreur lors de la suppression d'un membre</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve"> - <source>Error saving %@ servers</source> - <target>Erreur lors de la sauvegarde des serveurs %@</target> + <trans-unit id="Error reordering lists" xml:space="preserve"> + <source>Error reordering lists</source> + <target>Erreur lors de la réorganisation des listes</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error resetting statistics" xml:space="preserve"> + <source>Error resetting statistics</source> + <target>Erreur de réinitialisation des statistiques</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving ICE servers" xml:space="preserve"> @@ -2313,6 +3361,11 @@ Cette opération ne peut être annulée !</target> <target>Erreur lors de la sauvegarde des serveurs ICE</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving chat list" xml:space="preserve"> + <source>Error saving chat list</source> + <target>Erreur lors de l'enregistrement de la liste des chats</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> <target>Erreur lors de la sauvegarde du profil de groupe</target> @@ -2328,11 +3381,26 @@ Cette opération ne peut être annulée !</target> <target>Erreur lors de l'enregistrement de la phrase de passe dans la keychain</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving servers" xml:space="preserve"> + <source>Error saving servers</source> + <target>Erreur d'enregistrement des serveurs</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error saving settings" xml:space="preserve"> + <source>Error saving settings</source> + <target>Erreur lors de l'enregistrement des paramètres</target> + <note>when migrating</note> + </trans-unit> <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> <target>Erreur d'enregistrement du mot de passe de l'utilisateur</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error scanning code: %@" xml:space="preserve"> + <source>Error scanning code: %@</source> + <target>Erreur lors du scan du code : %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending email" xml:space="preserve"> <source>Error sending email</source> <target>Erreur lors de l'envoi de l'e-mail</target> @@ -2363,16 +3431,26 @@ Cette opération ne peut être annulée !</target> <target>Erreur lors de l'arrêt du chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error switching profile" xml:space="preserve"> + <source>Error switching profile</source> + <target>Erreur lors du changement de profil</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> <target>Erreur lors du changement de profil !</target> - <note>No comment provided by engineer.</note> + <note>alertTitle</note> </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> <target>Erreur de synchronisation de connexion</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error testing server connection" xml:space="preserve"> + <source>Error testing server connection</source> + <target>Erreur lors du test de connexion au serveur</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> <target>Erreur lors de la mise à jour du lien de groupe</target> @@ -2383,6 +3461,11 @@ Cette opération ne peut être annulée !</target> <target>Erreur lors de la mise à jour du message</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error updating server" xml:space="preserve"> + <source>Error updating server</source> + <target>Erreur de mise à jour du serveur</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> <target>Erreur lors de la mise à jour des paramètres</target> @@ -2393,6 +3476,16 @@ Cette opération ne peut être annulée !</target> <target>Erreur de mise à jour de la confidentialité de l'utilisateur</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error uploading the archive" xml:space="preserve"> + <source>Error uploading the archive</source> + <target>Erreur lors de l'envoi de l'archive</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error verifying passphrase:" xml:space="preserve"> + <source>Error verifying passphrase:</source> + <target>Erreur lors de la vérification de la phrase secrète :</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error: " xml:space="preserve"> <source>Error: </source> <target>Erreur : </target> @@ -2401,7 +3494,9 @@ Cette opération ne peut être annulée !</target> <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> <target>Erreur : %@</target> - <note>No comment provided by engineer.</note> + <note>alert message +file error text +snd error text</note> </trans-unit> <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> @@ -2413,6 +3508,16 @@ Cette opération ne peut être annulée !</target> <target>Erreur : pas de fichier de base de données</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Errors" xml:space="preserve"> + <source>Errors</source> + <target>Erreurs</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors in servers configuration." xml:space="preserve"> + <source>Errors in servers configuration.</source> + <target>Erreurs dans la configuration des serveurs.</target> + <note>servers error</note> + </trans-unit> <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> <source>Even when disabled in the conversation.</source> <target>Même s'il est désactivé dans la conversation.</target> @@ -2420,14 +3525,19 @@ Cette opération ne peut être annulée !</target> </trans-unit> <trans-unit id="Exit without saving" xml:space="preserve"> <source>Exit without saving</source> - <target>Quitter sans sauvegarder</target> + <target>Quitter sans enregistrer</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Expand" xml:space="preserve"> <source>Expand</source> - <target>Développer</target> + <target>Étendre</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Expired" xml:space="preserve"> + <source>Expired</source> + <target>Expiré</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> <target>Exporter la base de données</target> @@ -2438,11 +3548,21 @@ Cette opération ne peut être annulée !</target> <target>Erreur lors de l'exportation :</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Export theme" xml:space="preserve"> + <source>Export theme</source> + <target>Exporter le thème</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> <target>Archive de la base de données exportée.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Exported file doesn't exist" xml:space="preserve"> + <source>Exported file doesn't exist</source> + <target>Le fichier exporté n'existe pas</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exporting database archive…" xml:space="preserve"> <source>Exporting database archive…</source> <target>Exportation de l'archive de la base de données…</target> @@ -2458,16 +3578,70 @@ Cette opération ne peut être annulée !</target> <target>Rapide et ne nécessitant pas d'attendre que l'expéditeur soit en ligne !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster deletion of groups." xml:space="preserve"> + <source>Faster deletion of groups.</source> + <target>Suppression plus rapide des groupes.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Faster joining and more reliable messages." xml:space="preserve"> <source>Faster joining and more reliable messages.</source> <target>Connexion plus rapide et messages plus fiables.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster sending messages." xml:space="preserve"> + <source>Faster sending messages.</source> + <target>Envoi plus rapide des messages.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Favorite" xml:space="preserve"> <source>Favorite</source> <target>Favoris</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Favorites" xml:space="preserve"> + <source>Favorites</source> + <target>Favoris</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Erreur de fichier</target> + <note>file error alert title</note> + </trans-unit> + <trans-unit id="File errors: %@" xml:space="preserve"> + <source>File errors: +%@</source> + <target>Erreurs de fichier : +%@</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="File is blocked by server operator: %@." xml:space="preserve"> + <source>File is blocked by server operator: +%@.</source> + <target>Le fichier est bloqué par l'opérateur du serveur : +%@.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve"> + <source>File not found - most likely file was deleted or cancelled.</source> + <target>Fichier introuvable - le fichier a probablement été supprimé ou annulé.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File server error: %@" xml:space="preserve"> + <source>File server error: %@</source> + <target>Erreur de serveur de fichiers : %@</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File status" xml:space="preserve"> + <source>File status</source> + <target>Statut du fichier</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File status: %@" xml:space="preserve"> + <source>File status: %@</source> + <target>Statut du fichier : %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="File will be deleted from servers." xml:space="preserve"> <source>File will be deleted from servers.</source> <target>Le fichier sera supprimé des serveurs.</target> @@ -2488,6 +3662,11 @@ Cette opération ne peut être annulée !</target> <target>Fichier : %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files" xml:space="preserve"> + <source>Files</source> + <target>Fichiers</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> <target>Fichiers & médias</target> @@ -2498,11 +3677,16 @@ Cette opération ne peut être annulée !</target> <target>Fichiers et médias</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> - <source>Files and media are prohibited in this group.</source> + <trans-unit id="Files and media are prohibited." xml:space="preserve"> + <source>Files and media are prohibited.</source> <target>Les fichiers et les médias sont interdits dans ce groupe.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files and media not allowed" xml:space="preserve"> + <source>Files and media not allowed</source> + <target>Fichiers et médias non autorisés</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files and media prohibited!" xml:space="preserve"> <source>Files and media prohibited!</source> <target>Fichiers et médias interdits !</target> @@ -2513,6 +3697,16 @@ Cette opération ne peut être annulée !</target> <target>Filtrer les messages non lus et favoris.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Finalize migration" xml:space="preserve"> + <source>Finalize migration</source> + <target>Finaliser le transfert</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration on another device." xml:space="preserve"> + <source>Finalize migration on another device.</source> + <target>Finalisez le transfert sur l'autre appareil.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> <target>Enfin, les voilà ! 🚀</target> @@ -2520,7 +3714,7 @@ Cette opération ne peut être annulée !</target> </trans-unit> <trans-unit id="Find chats faster" xml:space="preserve"> <source>Find chats faster</source> - <target>Trouver des messages plus rapidement</target> + <target>Recherche de message plus rapide</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix" xml:space="preserve"> @@ -2535,7 +3729,7 @@ Cette opération ne peut être annulée !</target> </trans-unit> <trans-unit id="Fix connection?" xml:space="preserve"> <source>Fix connection?</source> - <target>Réparer la connexion?</target> + <target>Réparer la connexion ?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix encryption after restoring backups." xml:space="preserve"> @@ -2553,11 +3747,113 @@ Cette opération ne peut être annulée !</target> <target>Correction non prise en charge par un membre du groupe</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For all moderators" xml:space="preserve"> + <source>For all moderators</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For chat profile %@:" xml:space="preserve"> + <source>For chat profile %@:</source> + <target>Pour le profil de discussion %@ :</target> + <note>servers error</note> + </trans-unit> <trans-unit id="For console" xml:space="preserve"> <source>For console</source> <target>Pour la console</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." xml:space="preserve"> + <source>For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</source> + <target>Par exemple, si votre contact reçoit des messages via un serveur SimpleX Chat, votre application les transmettra via un serveur Flux.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For me" xml:space="preserve"> + <source>For me</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For private routing" xml:space="preserve"> + <source>For private routing</source> + <target>Pour le routage privé</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For social media" xml:space="preserve"> + <source>For social media</source> + <target>Pour les réseaux sociaux</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward" xml:space="preserve"> + <source>Forward</source> + <target>Transférer</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Forward %d message(s)?" xml:space="preserve"> + <source>Forward %d message(s)?</source> + <target>Transférer %d message(s) ?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Forward and save messages" xml:space="preserve"> + <source>Forward and save messages</source> + <target>Transférer et sauvegarder des messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward messages" xml:space="preserve"> + <source>Forward messages</source> + <target>Transférer les messages</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Forward messages without files?" xml:space="preserve"> + <source>Forward messages without files?</source> + <target>Transférer les messages sans les fichiers ?</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> + <source>Forward up to 20 messages at once.</source> + <target>Transférez jusqu'à 20 messages à la fois.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded" xml:space="preserve"> + <source>Forwarded</source> + <target>Transféré</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded from" xml:space="preserve"> + <source>Forwarded from</source> + <target>Transféré depuis</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding %lld messages" xml:space="preserve"> + <source>Forwarding %lld messages</source> + <target>Transfert des %lld messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve"> + <source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source> + <target>Le serveur de redirection %@ n'a pas réussi à se connecter au serveur de destination %@. Veuillez réessayer plus tard.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server address is incompatible with network settings: %@.</source> + <target>L'adresse du serveur de redirection est incompatible avec les paramètres du réseau : %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server version is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server version is incompatible with network settings: %@.</source> + <target>La version du serveur de redirection est incompatible avec les paramètres du réseau : %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Destination server error: %2$@</source> + <target>Serveur de transfert : %1$@ +Erreur du serveur de destination : %2$@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Error: %2$@</source> + <target>Serveur de transfert : %1$@ +Erreur : %2$@</target> + <note>snd error text</note> + </trans-unit> <trans-unit id="Found desktop" xml:space="preserve"> <source>Found desktop</source> <target>Bureau trouvé</target> @@ -2578,11 +3874,6 @@ Cette opération ne peut être annulée !</target> <target>Nom complet (optionnel)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full name:" xml:space="preserve"> - <source>Full name:</source> - <target>Nom complet :</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Fully decentralized – visible only to members." xml:space="preserve"> <source>Fully decentralized – visible only to members.</source> <target>Entièrement décentralisé – visible que par ses membres.</target> @@ -2603,6 +3894,20 @@ Cette opération ne peut être annulée !</target> <target>GIFs et stickers</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Get notified when mentioned." xml:space="preserve"> + <source>Get notified when mentioned.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Good afternoon!" xml:space="preserve"> + <source>Good afternoon!</source> + <target>Bonjour !</target> + <note>message preview</note> + </trans-unit> + <trans-unit id="Good morning!" xml:space="preserve"> + <source>Good morning!</source> + <target>Bonjour !</target> + <note>message preview</note> + </trans-unit> <trans-unit id="Group" xml:space="preserve"> <source>Group</source> <target>Groupe</target> @@ -2658,36 +3963,6 @@ Cette opération ne peut être annulée !</target> <target>Liens de groupe</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve"> - <source>Group members can add message reactions.</source> - <target>Les membres du groupe peuvent ajouter des réactions aux messages.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> - <source>Group members can irreversibly delete sent messages.</source> - <target>Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> - <target>Les membres du groupe peuvent envoyer des messages directs.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> - <target>Les membres du groupes peuvent envoyer des messages éphémères.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send files and media." xml:space="preserve"> - <source>Group members can send files and media.</source> - <target>Les membres du groupe peuvent envoyer des fichiers et des médias.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> - <target>Les membres du groupe peuvent envoyer des messages vocaux.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> <target>Message du groupe :</target> @@ -2728,11 +4003,19 @@ Cette opération ne peut être annulée !</target> <target>Le groupe va être supprimé pour vous - impossible de revenir en arrière !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Groups" xml:space="preserve"> + <source>Groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Help" xml:space="preserve"> <source>Help</source> <target>Aide</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Help admins moderating their groups." xml:space="preserve"> + <source>Help admins moderating their groups.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> <target>Caché</target> @@ -2773,15 +4056,29 @@ Cette opération ne peut être annulée !</target> <target>Historique</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="History is not sent to new members." xml:space="preserve"> + <source>History is not sent to new members.</source> + <target>L'historique n'est pas envoyé aux nouveaux membres.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> <target>Comment SimpleX fonctionne</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="How it affects privacy" xml:space="preserve"> + <source>How it affects privacy</source> + <target>L'impact sur la vie privée</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it helps privacy" xml:space="preserve"> + <source>How it helps privacy</source> + <target>Comment il contribue à la protection de la vie privée</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> - <target>Comment ça fonctionne</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="How to" xml:space="preserve"> <source>How to</source> @@ -2798,21 +4095,26 @@ Cette opération ne peut être annulée !</target> <target>Comment utiliser vos serveurs</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Hungarian interface" xml:space="preserve"> + <source>Hungarian interface</source> + <target>Interface en hongrois</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> <target>Serveurs ICE (un par ligne)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="IP address" xml:space="preserve"> + <source>IP address</source> + <target>Adresse IP</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> <source>If you can't meet in person, show QR code in a video call, or share the link.</source> <target>Si vous ne pouvez pas vous rencontrer en personne, montrez le code QR lors d'un appel vidéo ou partagez le lien.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <target>Si vous ne pouvez pas voir la personne, vous pouvez **scanner le code QR dans un appel vidéo**, ou votre contact peut vous partager un lien d'invitation.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> <target>Si vous saisissez ce code à l'ouverture de l'application, toutes les données de l'application seront irréversiblement supprimées !</target> @@ -2848,8 +4150,8 @@ Cette opération ne peut être annulée !</target> <target>Immédiatement</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <target>Protégé du spam et des abus</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2868,6 +4170,33 @@ Cette opération ne peut être annulée !</target> <target>Importer la base de données</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Import failed" xml:space="preserve"> + <source>Import failed</source> + <target>Échec de l'importation</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import theme" xml:space="preserve"> + <source>Import theme</source> + <target>Importer un thème</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Importing archive" xml:space="preserve"> + <source>Importing archive</source> + <target>Importation de l'archive</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved delivery, reduced traffic usage. More improvements are coming soon!" xml:space="preserve"> + <source>Improved delivery, reduced traffic usage. +More improvements are coming soon!</source> + <target>Amélioration de la distribution, réduction de l'utilisation du trafic. +D'autres améliorations sont à venir !</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved message delivery" xml:space="preserve"> + <source>Improved message delivery</source> + <target>Amélioration de la transmission des messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> <target>Une meilleure sécurité et protection de la vie privée</target> @@ -2878,11 +4207,29 @@ Cette opération ne peut être annulée !</target> <target>Configuration de serveur améliorée</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In order to continue, chat should be stopped." xml:space="preserve"> + <source>In order to continue, chat should be stopped.</source> + <target>Pour continuer, le chat doit être interrompu.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>En réponse à</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In-call sounds" xml:space="preserve"> + <source>In-call sounds</source> + <target>Sons d'appel</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Inappropriate content" xml:space="preserve"> + <source>Inappropriate content</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Inappropriate profile" xml:space="preserve"> + <source>Inappropriate profile</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> <target>Incognito</target> @@ -2953,6 +4300,11 @@ Cette opération ne peut être annulée !</target> <target>Installer [SimpleX Chat pour terminal](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Instant" xml:space="preserve"> + <source>Instant</source> + <target>Instantané</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> @@ -2960,30 +4312,75 @@ Cette opération ne peut être annulée !</target> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Instantly" xml:space="preserve"> - <source>Instantly</source> - <target>Instantané</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> <target>Interface</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Interface colors" xml:space="preserve"> + <source>Interface colors</source> + <target>Couleurs d'interface</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid" xml:space="preserve"> + <source>Invalid</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (bad token)" xml:space="preserve"> + <source>Invalid (bad token)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (expired)" xml:space="preserve"> + <source>Invalid (expired)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (unregistered)" xml:space="preserve"> + <source>Invalid (unregistered)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (wrong topic)" xml:space="preserve"> + <source>Invalid (wrong topic)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid QR code" xml:space="preserve"> + <source>Invalid QR code</source> + <target>Code QR invalide</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> <target>Lien de connection invalide</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid display name!" xml:space="preserve"> + <source>Invalid display name!</source> + <target>Nom d'affichage invalide !</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid link" xml:space="preserve"> + <source>Invalid link</source> + <target>Lien invalide</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Confirmation de migration invalide</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid name!" xml:space="preserve"> <source>Invalid name!</source> <target>Nom invalide !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid response" xml:space="preserve"> + <source>Invalid response</source> + <target>Réponse invalide</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> <target>Adresse de serveur invalide !</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Invalid status" xml:space="preserve"> <source>Invalid status</source> @@ -3005,6 +4402,11 @@ Cette opération ne peut être annulée !</target> <target>Inviter des membres</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invite to chat" xml:space="preserve"> + <source>Invite to chat</source> + <target>Inviter à discuter</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invite to group" xml:space="preserve"> <source>Invite to group</source> <target>Inviter au groupe</target> @@ -3020,8 +4422,8 @@ Cette opération ne peut être annulée !</target> <target>La suppression irréversible de message est interdite dans ce chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <target>La suppression irréversible de messages est interdite dans ce groupe.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3046,6 +4448,11 @@ Cette opération ne peut être annulée !</target> 3. La connexion a été compromise.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="It protects your IP address and connections." xml:space="preserve"> + <source>It protects your IP address and connections.</source> + <target>Il protège votre adresse IP et vos connexions.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> <target>Il semblerait que vous êtes déjà connecté via ce lien. Si ce n'est pas le cas, il y a eu une erreur (%@).</target> @@ -3064,13 +4471,18 @@ Cette opération ne peut être annulée !</target> <trans-unit id="Join" xml:space="preserve"> <source>Join</source> <target>Rejoindre</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> <target>Rejoindre le groupe</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Join group conversations" xml:space="preserve"> + <source>Join group conversations</source> + <target>Participez aux conversations de groupe</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Join group?" xml:space="preserve"> <source>Join group?</source> <target>Rejoindre le groupe ?</target> @@ -3098,11 +4510,26 @@ Voici votre lien pour le groupe %@ !</target> <target>Entrain de rejoindre le groupe</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep" xml:space="preserve"> + <source>Keep</source> + <target>Conserver</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Keep conversation" xml:space="preserve"> + <source>Keep conversation</source> + <target>Garder la conversation</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Keep the app open to use it from desktop" xml:space="preserve"> <source>Keep the app open to use it from desktop</source> <target>Garder l'application ouverte pour l'utiliser depuis le bureau</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep unused invitation?" xml:space="preserve"> + <source>Keep unused invitation?</source> + <target>Conserver l'invitation inutilisée ?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> <source>Keep your connections</source> <target>Conserver vos connexions</target> @@ -3136,6 +4563,16 @@ Voici votre lien pour le groupe %@ !</target> <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> <target>Quitter</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Leave chat" xml:space="preserve"> + <source>Leave chat</source> + <target>Quitter la discussion</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave chat?" xml:space="preserve"> + <source>Leave chat?</source> + <target>Quitter la discussion ?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group" xml:space="preserve"> @@ -3178,6 +4615,18 @@ Voici votre lien pour le groupe %@ !</target> <target>Bureaux liés</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="List" xml:space="preserve"> + <source>List</source> + <note>swipe action</note> + </trans-unit> + <trans-unit id="List name and emoji should be different for all lists." xml:space="preserve"> + <source>List name and emoji should be different for all lists.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List name..." xml:space="preserve"> + <source>List name...</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Live message!" xml:space="preserve"> <source>Live message!</source> <target>Message dynamique !</target> @@ -3208,11 +4657,6 @@ Voici votre lien pour le groupe %@ !</target> <target>Mode de verrouillage</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> - <source>Make a private connection</source> - <target>Établir une connexion privée</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make one message disappear" xml:space="preserve"> <source>Make one message disappear</source> <target>Rendre un message éphémère</target> @@ -3223,21 +4667,11 @@ Voici votre lien pour le groupe %@ !</target> <target>Rendre un profil privé !</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> - <target>Assurez-vous que les adresses des serveurs %@ sont au bon format et ne sont pas dupliquées, un par ligne (%@).</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> <target>Assurez-vous que les adresses des serveurs WebRTC ICE sont au bon format et ne sont pas dupliquées, un par ligne.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <target>Beaucoup se demandent : *si SimpleX n'a pas d'identifiant d'utilisateur, comment peut-il délivrer des messages ?*</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> <source>Mark deleted for everyone</source> <target>Marquer comme supprimé pour tout le monde</target> @@ -3263,11 +4697,35 @@ Voici votre lien pour le groupe %@ !</target> <target>Max 30 secondes, réception immédiate.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Media & file servers" xml:space="preserve"> + <source>Media & file servers</source> + <target>Serveurs de fichiers et de médias</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Medium" xml:space="preserve"> + <source>Medium</source> + <target>Modéré</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Member" xml:space="preserve"> <source>Member</source> <target>Membre</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member inactive" xml:space="preserve"> + <source>Member inactive</source> + <target>Membre inactif</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Member reports" xml:space="preserve"> + <source>Member reports</source> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All chat members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All chat members will be notified.</source> + <target>Le rôle du membre sera modifié pour « %@ ». Tous les membres du chat seront notifiés.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> <target>Le rôle du membre sera changé pour "%@". Tous les membres du groupe en seront informés.</target> @@ -3278,11 +4736,64 @@ Voici votre lien pour le groupe %@ !</target> <target>Le rôle du membre sera changé pour "%@". Ce membre recevra une nouvelle invitation.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from chat - this cannot be undone!</source> + <target>Le membre sera retiré de la discussion - cela ne peut pas être annulé !</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> <target>Ce membre sera retiré du groupe - impossible de revenir en arrière !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Members can add message reactions." xml:space="preserve"> + <source>Members can add message reactions.</source> + <target>Les membres du groupe peuvent ajouter des réactions aux messages.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Members can irreversibly delete sent messages. (24 hours)</source> + <target>Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés. (24 heures)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can report messsages to moderators." xml:space="preserve"> + <source>Members can report messsages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send SimpleX links." xml:space="preserve"> + <source>Members can send SimpleX links.</source> + <target>Les membres du groupe peuvent envoyer des liens SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> + <target>Les membres du groupe peuvent envoyer des messages directs.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> + <target>Les membres du groupes peuvent envoyer des messages éphémères.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send files and media." xml:space="preserve"> + <source>Members can send files and media.</source> + <target>Les membres du groupe peuvent envoyer des fichiers et des médias.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> + <target>Les membres du groupe peuvent envoyer des messages vocaux.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mention members 👋" xml:space="preserve"> + <source>Mention members 👋</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Menus" xml:space="preserve"> + <source>Menus</source> + <target>Menus</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>Erreur de distribution du message</target> @@ -3293,11 +4804,31 @@ Voici votre lien pour le groupe %@ !</target> <target>Accusés de réception des messages !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message delivery warning" xml:space="preserve"> + <source>Message delivery warning</source> + <target>Avertissement sur la distribution des messages</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> <target>Brouillon de message</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message forwarded" xml:space="preserve"> + <source>Message forwarded</source> + <target>Message transféré</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message may be delivered later if member becomes active." xml:space="preserve"> + <source>Message may be delivered later if member becomes active.</source> + <target>Le message peut être transmis plus tard si le membre devient actif.</target> + <note>item status description</note> + </trans-unit> + <trans-unit id="Message queue info" xml:space="preserve"> + <source>Message queue info</source> + <target>Informations sur la file d'attente des messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message reactions" xml:space="preserve"> <source>Message reactions</source> <target>Réactions aux messages</target> @@ -3308,16 +4839,51 @@ Voici votre lien pour le groupe %@ !</target> <target>Les réactions aux messages sont interdites dans ce chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> - <source>Message reactions are prohibited in this group.</source> + <trans-unit id="Message reactions are prohibited." xml:space="preserve"> + <source>Message reactions are prohibited.</source> <target>Les réactions aux messages sont interdites dans ce groupe.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message reception" xml:space="preserve"> + <source>Message reception</source> + <target>Réception de message</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message servers" xml:space="preserve"> + <source>Message servers</source> + <target>Serveurs de messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message shape" xml:space="preserve"> + <source>Message shape</source> + <target>Forme du message</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message source remains private." xml:space="preserve"> + <source>Message source remains private.</source> + <target>La source du message reste privée.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status" xml:space="preserve"> + <source>Message status</source> + <target>Statut du message</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status: %@" xml:space="preserve"> + <source>Message status: %@</source> + <target>Statut du message : %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> <target>Texte du message</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message too large" xml:space="preserve"> + <source>Message too large</source> + <target>Message trop volumineux</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> <target>Messages</target> @@ -3333,11 +4899,75 @@ Voici votre lien pour le groupe %@ !</target> <target>Les messages de %@ seront affichés !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Messages in this chat will never be deleted." xml:space="preserve"> + <source>Messages in this chat will never be deleted.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages received" xml:space="preserve"> + <source>Messages received</source> + <target>Messages reçus</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages sent" xml:space="preserve"> + <source>Messages sent</source> + <target>Messages envoyés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages were deleted after you selected them." xml:space="preserve"> + <source>Messages were deleted after you selected them.</source> + <target>Les messages ont été supprimés après avoir été sélectionnés.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Les messages, fichiers et appels sont protégés par un chiffrement **de bout en bout** avec une confidentialité persistante, une répudiation et une récupération en cas d'effraction.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate device" xml:space="preserve"> + <source>Migrate device</source> + <target>Transférer l'appareil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate from another device" xml:space="preserve"> + <source>Migrate from another device</source> + <target>Transférer depuis un autre appareil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate here" xml:space="preserve"> + <source>Migrate here</source> + <target>Transférer ici</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device" xml:space="preserve"> + <source>Migrate to another device</source> + <target>Transférer vers un autre appareil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device via QR code." xml:space="preserve"> + <source>Migrate to another device via QR code.</source> + <target>Transférer vers un autre appareil via un code QR.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating" xml:space="preserve"> + <source>Migrating</source> + <target>Transfert</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migrating database archive…" xml:space="preserve"> <source>Migrating database archive…</source> <target>Migration de l'archive de la base de données…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Migration complete" xml:space="preserve"> + <source>Migration complete</source> + <target>Transfert terminé</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> <target>Erreur de migration :</target> @@ -3353,9 +4983,9 @@ Voici votre lien pour le groupe %@ !</target> <target>La migration est terminée</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> - <target>Migrations : %@</target> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> + <target>Migrations :</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> @@ -3373,21 +5003,30 @@ Voici votre lien pour le groupe %@ !</target> <target>Modéré à : %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="More" xml:space="preserve"> + <source>More</source> + <note>swipe action</note> + </trans-unit> <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> <target>Plus d'améliorations à venir !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="More reliable network connection." xml:space="preserve"> + <source>More reliable network connection.</source> + <target>Connexion réseau plus fiable.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="More reliable notifications" xml:space="preserve"> + <source>More reliable notifications</source> + <target>Notifications plus fiables</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> <source>Most likely this connection is deleted.</source> <target>Connexion probablement supprimée.</target> <note>item status description</note> </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> - <source>Most likely this contact has deleted the connection with you.</source> - <target>Il est fort probable que ce contact ait supprimé la connexion avec vous.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> <target>Différents profils de chat</target> @@ -3396,7 +5035,11 @@ Voici votre lien pour le groupe %@ !</target> <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> <target>Muet</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Mute all" xml:space="preserve"> + <source>Mute all</source> + <note>notification label action</note> </trans-unit> <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> @@ -3406,13 +5049,38 @@ Voici votre lien pour le groupe %@ !</target> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> <target>Nom</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> <target>Réseau et serveurs</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Network connection" xml:space="preserve"> + <source>Network connection</source> + <target>Connexion au réseau</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network decentralization" xml:space="preserve"> + <source>Network decentralization</source> + <target>Décentralisation du réseau</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve"> + <source>Network issues - message expired after many attempts to send it.</source> + <target>Problèmes de réseau - le message a expiré après plusieurs tentatives d'envoi.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Network management" xml:space="preserve"> + <source>Network management</source> + <target>Gestion du réseau</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network operator" xml:space="preserve"> + <source>Network operator</source> + <target>Opérateur de réseau</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> <target>Paramètres réseau</target> @@ -3423,11 +5091,35 @@ Voici votre lien pour le groupe %@ !</target> <target>État du réseau</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New" xml:space="preserve"> + <source>New</source> + <note>token status text</note> + </trans-unit> <trans-unit id="New Passcode" xml:space="preserve"> <source>New Passcode</source> <target>Nouveau code d'accès</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve"> + <source>New SOCKS credentials will be used every time you start the app.</source> + <target>De nouveaux identifiants SOCKS seront utilisés chaque fois que vous démarrerez l'application.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve"> + <source>New SOCKS credentials will be used for each server.</source> + <target>De nouveaux identifiants SOCKS seront utilisées pour chaque serveur.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat" xml:space="preserve"> + <source>New chat</source> + <target>Nouveau chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat experience 🎉" xml:space="preserve"> + <source>New chat experience 🎉</source> + <target>Nouvelle expérience de discussion 🎉</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> <target>Nouvelle demande de contact</target> @@ -3438,11 +5130,6 @@ Voici votre lien pour le groupe %@ !</target> <target>Nouveau contact :</target> <note>notification</note> </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> - <source>New database archive</source> - <target>Nouvelle archive de base de données</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="New desktop app!" xml:space="preserve"> <source>New desktop app!</source> <target>Nouvelle application de bureau !</target> @@ -3453,11 +5140,21 @@ Voici votre lien pour le groupe %@ !</target> <target>Nouveau nom d'affichage</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>Nouveaux événements</target> + <note>notification</note> + </trans-unit> <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> <target>Nouveautés de la %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New media options" xml:space="preserve"> + <source>New media options</source> + <target>Nouvelles options de médias</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> <target>Nouveau rôle</target> @@ -3473,6 +5170,11 @@ Voici votre lien pour le groupe %@ !</target> <target>Nouvelle phrase secrète…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New server" xml:space="preserve"> + <source>New server</source> + <target>Nouveau serveur</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No" xml:space="preserve"> <source>No</source> <target>Non</target> @@ -3483,6 +5185,18 @@ Voici votre lien pour le groupe %@ !</target> <target>Pas de mot de passe pour l'app</target> <note>Authentication unavailable</note> </trans-unit> + <trans-unit id="No chats" xml:space="preserve"> + <source>No chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats found" xml:space="preserve"> + <source>No chats found</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats in list %@" xml:space="preserve"> + <source>No chats in list %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> <target>Aucun contact sélectionné</target> @@ -3503,9 +5217,14 @@ Voici votre lien pour le groupe %@ !</target> <target>Pas de token d'appareil !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No direct connection yet, message is forwarded by admin." xml:space="preserve"> + <source>No direct connection yet, message is forwarded by admin.</source> + <target>Pas de connexion directe pour l'instant, le message est transmis par l'administrateur.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="No filtered chats" xml:space="preserve"> <source>No filtered chats</source> - <target>Pas de chats filtrés</target> + <target>Aucune discussion filtrés</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No group!" xml:space="preserve"> @@ -3518,21 +5237,107 @@ Voici votre lien pour le groupe %@ !</target> <target>Aucun historique</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No info, try to reload" xml:space="preserve"> + <source>No info, try to reload</source> + <target>Pas d'info, essayez de recharger</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No media & file servers." xml:space="preserve"> + <source>No media & file servers.</source> + <target>Pas de serveurs de médias et de fichiers.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No message" xml:space="preserve"> + <source>No message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No message servers." xml:space="preserve"> + <source>No message servers.</source> + <target>Pas de serveurs de messages.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No network connection" xml:space="preserve"> + <source>No network connection</source> + <target>Pas de connexion au réseau</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record speech" xml:space="preserve"> + <source>No permission to record speech</source> + <target>Enregistrement des conversations non autorisé</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record video" xml:space="preserve"> + <source>No permission to record video</source> + <target>Enregistrement de la vidéo non autorisé</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> <source>No permission to record voice message</source> <target>Pas l'autorisation d'enregistrer un message vocal</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No push server" xml:space="preserve"> + <source>No push server</source> + <target>No push server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> <target>Aucun fichier reçu ou envoyé</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No servers for private message routing." xml:space="preserve"> + <source>No servers for private message routing.</source> + <target>Pas de serveurs pour le routage privé des messages.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive files." xml:space="preserve"> + <source>No servers to receive files.</source> + <target>Pas de serveurs pour recevoir des fichiers.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive messages." xml:space="preserve"> + <source>No servers to receive messages.</source> + <target>Pas de serveurs pour recevoir des messages.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to send files." xml:space="preserve"> + <source>No servers to send files.</source> + <target>Pas de serveurs pour envoyer des fichiers.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No token!" xml:space="preserve"> + <source>No token!</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="No unread chats" xml:space="preserve"> + <source>No unread chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> + <target>Aucun identifiant d'utilisateur.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Not compatible!" xml:space="preserve"> <source>Not compatible!</source> <target>Non compatible !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notes" xml:space="preserve"> + <source>Notes</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing selected" xml:space="preserve"> + <source>Nothing selected</source> + <target>Aucune sélection</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing to forward!" xml:space="preserve"> + <source>Nothing to forward!</source> + <target>Rien à transférer !</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> <target>Notifications</target> @@ -3543,6 +5348,19 @@ Voici votre lien pour le groupe %@ !</target> <target>Les notifications sont désactivées !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notifications error" xml:space="preserve"> + <source>Notifications error</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Notifications privacy" xml:space="preserve"> + <source>Notifications privacy</source> + <target>Notifications sécurisées</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications status" xml:space="preserve"> + <source>Notifications status</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. @@ -3552,44 +5370,43 @@ Voici votre lien pour le groupe %@ !</target> - désactiver des membres (rôle "observateur")</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="OK" xml:space="preserve"> + <source>OK</source> + <target>OK</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Off" xml:space="preserve"> <source>Off</source> <target>Off</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> - <source>Off (Local)</source> - <target>Off (Local)</target> - <note>No comment provided by engineer.</note> + <note>blur media</note> </trans-unit> <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> <target>Ok</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> <target>Ancienne base de données</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> - <source>Old database archive</source> - <target>Archives de l'ancienne base de données</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> <target>Lien d'invitation unique</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> - <target>Les hôtes .onion seront nécessaires pour la connexion. Nécessite l'activation d'un VPN.</target> + <trans-unit id="Onion hosts will be **required** for connection. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be **required** for connection. +Requires compatible VPN.</source> + <target>Les hôtes .onion seront **nécessaires** pour la connexion. +Nécessite l'activation d'un VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> - <target>Les hôtes .onion seront utilisés dès que possible. Nécessite l'activation d'un VPN.</target> + <trans-unit id="Onion hosts will be used when available. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be used when available. +Requires compatible VPN.</source> + <target>Les hôtes .onion seront utilisés dès que possible. +Nécessite l'activation d'un VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will not be used." xml:space="preserve"> @@ -3597,11 +5414,21 @@ Voici votre lien pour le groupe %@ !</target> <target>Les hôtes .onion ne seront pas utilisés.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only chat owners can change preferences." xml:space="preserve"> + <source>Only chat owners can change preferences.</source> + <target>Seuls les propriétaires peuvent modifier les préférences.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <target>Seuls les appareils clients stockent les profils des utilisateurs, les contacts, les groupes et les messages envoyés avec un **chiffrement de bout en bout à deux couches**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only delete conversation" xml:space="preserve"> + <source>Only delete conversation</source> + <target>Ne supprimer que la conversation</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> <source>Only group owners can change group preferences.</source> <target>Seuls les propriétaires du groupe peuvent modifier les préférences du groupe.</target> @@ -3617,14 +5444,22 @@ Voici votre lien pour le groupe %@ !</target> <target>Seuls les propriétaires de groupes peuvent activer les messages vocaux.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only sender and moderators see it" xml:space="preserve"> + <source>Only sender and moderators see it</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you and moderators see it" xml:space="preserve"> + <source>Only you and moderators see it</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only you can add message reactions." xml:space="preserve"> <source>Only you can add message reactions.</source> <target>Vous seul pouvez ajouter des réactions aux messages.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <target>Vous êtes le seul à pouvoir supprimer des messages de manière irréversible (votre contact peut les marquer comme supprimé).</target> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source> + <target>Vous êtes le seul à pouvoir supprimer des messages de manière irréversible (votre contact peut les marquer comme supprimé). (24 heures)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can make calls." xml:space="preserve"> @@ -3647,9 +5482,9 @@ Voici votre lien pour le groupe %@ !</target> <target>Seul votre contact peut ajouter des réactions aux messages.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <target>Seul votre contact peut supprimer de manière irréversible des messages (vous pouvez les marquer comme supprimé).</target> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source> + <target>Seul votre contact peut supprimer de manière irréversible des messages (vous pouvez les marquer comme supprimé). (24 heures)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can make calls." xml:space="preserve"> @@ -3670,13 +5505,18 @@ Voici votre lien pour le groupe %@ !</target> <trans-unit id="Open" xml:space="preserve"> <source>Open</source> <target>Ouvrir</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Ouvrir les Paramètres</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open changes" xml:space="preserve"> + <source>Open changes</source> + <target>Ouvrir les modifications</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> <target>Ouvrir le chat</target> @@ -3687,26 +5527,86 @@ Voici votre lien pour le groupe %@ !</target> <target>Ouvrir la console du chat</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Open conditions" xml:space="preserve"> + <source>Open conditions</source> + <target>Ouvrir les conditions</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open group" xml:space="preserve"> <source>Open group</source> <target>Ouvrir le groupe</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve"> - <source>Open user profiles</source> - <target>Ouvrir les profils d'utilisateurs</target> + <trans-unit id="Open link?" xml:space="preserve"> + <source>Open link?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Open migration to another device" xml:space="preserve"> + <source>Open migration to another device</source> + <target>Ouvrir le transfert vers un autre appareil</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> - <target>Protocole et code open-source – n'importe qui peut heberger un serveur.</target> + <trans-unit id="Opening app…" xml:space="preserve"> + <source>Opening app…</source> + <target>Ouverture de l'app…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve"> - <source>Opening database…</source> - <target>Ouverture de la base de données…</target> + <trans-unit id="Operator" xml:space="preserve"> + <source>Operator</source> + <target>Opérateur</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Operator server" xml:space="preserve"> + <source>Operator server</source> + <target>Serveur de l'opérateur</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Or import archive file" xml:space="preserve"> + <source>Or import archive file</source> + <target>Ou importer un fichier d'archive</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or paste archive link" xml:space="preserve"> + <source>Or paste archive link</source> + <target>Ou coller le lien de l'archive</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or scan QR code" xml:space="preserve"> + <source>Or scan QR code</source> + <target>Ou scanner le code QR</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or securely share this file link" xml:space="preserve"> + <source>Or securely share this file link</source> + <target>Ou partagez en toute sécurité le lien de ce fichier</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or show this code" xml:space="preserve"> + <source>Or show this code</source> + <target>Ou montrez ce code</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or to share privately" xml:space="preserve"> + <source>Or to share privately</source> + <target>Ou à partager en privé</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Organize chats into lists" xml:space="preserve"> + <source>Organize chats into lists</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other" xml:space="preserve"> + <source>Other</source> + <target>Autres</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other file errors: %@" xml:space="preserve"> + <source>Other file errors: +%@</source> + <target>Autres erreurs de fichiers : +%@</target> + <note>alert message</note> + </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> <target>Nombre de PING</target> @@ -3742,15 +5642,20 @@ Voici votre lien pour le groupe %@ !</target> <target>Code d'accès défini !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Password" xml:space="preserve"> + <source>Password</source> + <target>Mot de passe</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> <target>Mot de passe à entrer</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste" xml:space="preserve"> - <source>Paste</source> - <target>Coller</target> - <note>No comment provided by engineer.</note> + <trans-unit id="Past member %@" xml:space="preserve"> + <source>Past member %@</source> + <target>Ancien membre %@</target> + <note>past/unknown group member</note> </trans-unit> <trans-unit id="Paste desktop address" xml:space="preserve"> <source>Paste desktop address</source> @@ -3762,23 +5667,23 @@ Voici votre lien pour le groupe %@ !</target> <target>Coller l'image</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve"> - <source>Paste received link</source> - <target>Coller le lien reçu</target> + <trans-unit id="Paste link to connect!" xml:space="preserve"> + <source>Paste link to connect!</source> + <target>Collez le lien pour vous connecter !</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> - <source>Paste the link you received to connect with your contact.</source> - <target>Collez le lien que vous avez reçu dans le cadre ci-dessous pour vous connecter avec votre contact.</target> - <note>placeholder</note> - </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> - <target>On ne peut se connecter à vous qu’avec les liens que vous partagez.</target> + <trans-unit id="Paste the link you received" xml:space="preserve"> + <source>Paste the link you received</source> + <target>Collez le lien que vous avez reçu</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Periodically" xml:space="preserve"> - <source>Periodically</source> + <trans-unit id="Pending" xml:space="preserve"> + <source>Pending</source> + <target>En attente</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Periodic" xml:space="preserve"> + <source>Periodic</source> <target>Périodique</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3787,11 +5692,33 @@ Voici votre lien pour le groupe %@ !</target> <target>Erreur de déchiffrement</target> <note>message decrypt error item</note> </trans-unit> + <trans-unit id="Picture-in-picture calls" xml:space="preserve"> + <source>Picture-in-picture calls</source> + <target>Appels picture-in-picture</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Play from the chat list." xml:space="preserve"> + <source>Play from the chat list.</source> + <target>Aperçu depuis la liste de conversation.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable calls." xml:space="preserve"> + <source>Please ask your contact to enable calls.</source> + <target>Veuillez demander à votre contact d'autoriser les appels.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> <target>Veuillez demander à votre contact de permettre l'envoi de messages vocaux.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers." xml:space="preserve"> + <source>Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers.</source> + <target>Veuillez vérifier que le téléphone portable et l'ordinateur de bureau sont connectés au même réseau local et que le pare-feu de l'ordinateur de bureau autorise la connexion. +Veuillez faire part de tout autre problème aux développeurs.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> <target>Veuillez vérifier que vous avez utilisé le bon lien ou demandez à votre contact de vous en envoyer un autre.</target> @@ -3807,6 +5734,18 @@ Voici votre lien pour le groupe %@ !</target> <target>Veuillez vérifier vos préférences ainsi que celles de votre contact.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve"> + <source>Please confirm that network settings are correct for this device.</source> + <target>Veuillez confirmer que les paramètres réseau de cet appareil sont corrects.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact developers. Error: %@" xml:space="preserve"> + <source>Please contact developers. +Error: %@</source> + <target>Veuillez contacter les développeurs. +Erreur : %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> <target>Veuillez contacter l'administrateur du groupe.</target> @@ -3847,11 +5786,28 @@ Voici votre lien pour le groupe %@ !</target> <target>Veuillez conserver votre phrase secrète en lieu sûr, vous NE pourrez PAS la changer si vous la perdez.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please try to disable and re-enable notfications." xml:space="preserve"> + <source>Please try to disable and re-enable notfications.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token activation to complete." xml:space="preserve"> + <source>Please wait for token activation to complete.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token to be registered." xml:space="preserve"> + <source>Please wait for token to be registered.</source> + <note>token info</note> + </trans-unit> <trans-unit id="Polish interface" xml:space="preserve"> <source>Polish interface</source> <target>Interface en polonais</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Port" xml:space="preserve"> + <source>Port</source> + <target>Port</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> <target>Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte</target> @@ -3862,36 +5818,83 @@ Voici votre lien pour le groupe %@ !</target> <target>Conserver le brouillon du dernier message, avec les pièces jointes.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Preset server" xml:space="preserve"> - <source>Preset server</source> - <target>Serveur prédéfini</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> <target>Adresse du serveur prédéfinie</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Preset servers" xml:space="preserve"> + <source>Preset servers</source> + <target>Serveurs prédéfinis</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Preview" xml:space="preserve"> <source>Preview</source> <target>Aperçu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Previously connected servers" xml:space="preserve"> + <source>Previously connected servers</source> + <target>Serveurs précédemment connectés</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> <target>Vie privée et sécurité</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Privacy for your customers." xml:space="preserve"> + <source>Privacy for your customers.</source> + <target>Respect de la vie privée de vos clients.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy policy and conditions of use." xml:space="preserve"> + <source>Privacy policy and conditions of use.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy redefined" xml:space="preserve"> <source>Privacy redefined</source> <target>La vie privée redéfinie</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve"> + <source>Private chats, groups and your contacts are not accessible to server operators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> <target>Noms de fichiers privés</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private media file names." xml:space="preserve"> + <source>Private media file names.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing" xml:space="preserve"> + <source>Private message routing</source> + <target>Routage privé des messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing 🚀" xml:space="preserve"> + <source>Private message routing 🚀</source> + <target>Routage privé des messages 🚀</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private notes" xml:space="preserve"> + <source>Private notes</source> + <target>Notes privées</target> + <note>name of notes to self</note> + </trans-unit> + <trans-unit id="Private routing" xml:space="preserve"> + <source>Private routing</source> + <target>Routage privé</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private routing error" xml:space="preserve"> + <source>Private routing error</source> + <target>Erreur de routage privé</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> <target>Profil et connexions au serveur</target> @@ -3902,14 +5905,9 @@ Voici votre lien pour le groupe %@ !</target> <target>Image de profil</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile name" xml:space="preserve"> - <source>Profile name</source> - <target>Nom du profil</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile name:" xml:space="preserve"> - <source>Profile name:</source> - <target>Nom du profil :</target> + <trans-unit id="Profile images" xml:space="preserve"> + <source>Profile images</source> + <target>Images de profil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile password" xml:space="preserve"> @@ -3917,10 +5915,15 @@ Voici votre lien pour le groupe %@ !</target> <target>Mot de passe de profil</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Profile theme" xml:space="preserve"> + <source>Profile theme</source> + <target>Thème de profil</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> <source>Profile update will be sent to your contacts.</source> <target>La mise à jour du profil sera envoyée à vos contacts.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> @@ -3942,6 +5945,15 @@ Voici votre lien pour le groupe %@ !</target> <target>Interdire les réactions aux messages.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Prohibit reporting messages to moderators." xml:space="preserve"> + <source>Prohibit reporting messages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending SimpleX links." xml:space="preserve"> + <source>Prohibit sending SimpleX links.</source> + <target>Interdire l'envoi de liens SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> <target>Interdire l'envoi de messages directs aux membres.</target> @@ -3962,11 +5974,23 @@ Voici votre lien pour le groupe %@ !</target> <target>Interdire l'envoi de messages vocaux.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect IP address" xml:space="preserve"> + <source>Protect IP address</source> + <target>Protéger l'adresse IP</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> <target>Protéger l'écran de l'app</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve"> + <source>Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings.</source> + <target>Protégez votre adresse IP des relais de messagerie choisis par vos contacts. +Activez-le dans les paramètres *Réseau et serveurs*.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> <target>Protégez vos profils de chat par un mot de passe !</target> @@ -3982,16 +6006,46 @@ Voici votre lien pour le groupe %@ !</target> <target>Délai d'attente du protocole par KB</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Proxied" xml:space="preserve"> + <source>Proxied</source> + <target>Routé via un proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied servers" xml:space="preserve"> + <source>Proxied servers</source> + <target>Serveurs routés via des proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxy requires password" xml:space="preserve"> + <source>Proxy requires password</source> + <target>Le proxy est protégé par un mot de passe</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> <target>Notifications push</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Push server" xml:space="preserve"> + <source>Push server</source> + <target>Serveur Push</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Quantum resistant encryption" xml:space="preserve"> + <source>Quantum resistant encryption</source> + <target>Chiffrement résistant post-quantique</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> <source>Rate the app</source> <target>Évaluer l'app</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reachable chat toolbar" xml:space="preserve"> + <source>Reachable chat toolbar</source> + <target>Barre d'outils accessible</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> <target>Réagissez…</target> @@ -4000,16 +6054,21 @@ Voici votre lien pour le groupe %@ !</target> <trans-unit id="Read" xml:space="preserve"> <source>Read</source> <target>Lire</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Read more" xml:space="preserve"> <source>Read more</source> <target>En savoir plus</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> - <target>Pour en savoir plus, consultez le [Guide de l'utilisateur](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source> + <target>Pour en savoir plus, consultez le [Guide de l'utilisateur](https ://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</source> + <target>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).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> @@ -4017,11 +6076,6 @@ Voici votre lien pour le groupe %@ !</target> <target>Pour en savoir plus, consultez le [Guide de l'utilisateur](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> - <source>Read more in our GitHub repository.</source> - <target>Plus d'informations sur notre GitHub.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> <target>Pour en savoir plus, consultez notre [dépôt GitHub](https://github.com/simplex-chat/simplex-chat#readme).</target> @@ -4032,6 +6086,11 @@ Voici votre lien pour le groupe %@ !</target> <target>Les accusés de réception sont désactivés</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receive errors" xml:space="preserve"> + <source>Receive errors</source> + <target>Erreurs reçues</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Reçu à</target> @@ -4052,6 +6111,21 @@ Voici votre lien pour le groupe %@ !</target> <target>Message reçu</target> <note>message info title</note> </trans-unit> + <trans-unit id="Received messages" xml:space="preserve"> + <source>Received messages</source> + <target>Messages reçus</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received reply" xml:space="preserve"> + <source>Received reply</source> + <target>Réponse reçue</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received total" xml:space="preserve"> + <source>Received total</source> + <target>Total reçu</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> <target>L'adresse de réception sera changée pour un autre serveur. Le changement d'adresse sera terminé lorsque l'expéditeur sera en ligne.</target> @@ -4067,19 +6141,54 @@ Voici votre lien pour le groupe %@ !</target> <target>Réception via</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Recent history and improved [directory bot](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)." xml:space="preserve"> + <source>Recent history and improved [directory bot](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).</source> + <target>Historique récent et amélioration du [bot annuaire](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve"> + <source>Recipient(s) can't see who this message is from.</source> + <target>Le(s) destinataire(s) ne peut(vent) pas voir de qui provient ce message.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> <target>Les destinataires voient les mises à jour au fur et à mesure que vous leur écrivez.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect" xml:space="preserve"> + <source>Reconnect</source> + <target>Reconnecter</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> <target>Reconnecter tous les serveurs connectés pour forcer la livraison des messages. Cette méthode utilise du trafic supplémentaire.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect all servers" xml:space="preserve"> + <source>Reconnect all servers</source> + <target>Reconnecter tous les serveurs</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers?" xml:space="preserve"> + <source>Reconnect all servers?</source> + <target>Reconnecter tous les serveurs ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect server to force message delivery. It uses additional traffic.</source> + <target>Reconnecter le serveur pour forcer la livraison des messages. Utilise du trafic supplémentaire.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server?" xml:space="preserve"> + <source>Reconnect server?</source> + <target>Reconnecter le serveur ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> - <target>Reconnecter les serveurs?</target> + <target>Reconnecter les serveurs ?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Record updated at" xml:space="preserve"> @@ -4097,10 +6206,23 @@ Voici votre lien pour le groupe %@ !</target> <target>Réduction de la consommation de batterie</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Register" xml:space="preserve"> + <source>Register</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Register notification token?" xml:space="preserve"> + <source>Register notification token?</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Registered" xml:space="preserve"> + <source>Registered</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> <target>Rejeter</target> - <note>reject incoming call via notification</note> + <note>reject incoming call via notification +swipe action</note> </trans-unit> <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> <source>Reject (sender NOT notified)</source> @@ -4127,6 +6249,16 @@ Voici votre lien pour le groupe %@ !</target> <target>Supprimer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Remove archive?" xml:space="preserve"> + <source>Remove archive?</source> + <target>Supprimer l'archive ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove image" xml:space="preserve"> + <source>Remove image</source> + <target>Enlever l'image</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> <target>Retirer le membre</target> @@ -4154,7 +6286,7 @@ Voici votre lien pour le groupe %@ !</target> </trans-unit> <trans-unit id="Renegotiate encryption?" xml:space="preserve"> <source>Renegotiate encryption?</source> - <target>Renégocier le chiffrement?</target> + <target>Renégocier le chiffrement ?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Repeat connection request?" xml:space="preserve"> @@ -4162,16 +6294,71 @@ Voici votre lien pour le groupe %@ !</target> <target>Répéter la demande de connexion ?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat download" xml:space="preserve"> + <source>Repeat download</source> + <target>Répéter le téléchargement</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat import" xml:space="preserve"> + <source>Repeat import</source> + <target>Répéter l'importation</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Repeat join request?" xml:space="preserve"> <source>Repeat join request?</source> <target>Répéter la requête d'adhésion ?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat upload" xml:space="preserve"> + <source>Repeat upload</source> + <target>Répéter l'envoi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reply" xml:space="preserve"> <source>Reply</source> <target>Répondre</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Report" xml:space="preserve"> + <source>Report</source> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Report content: only group moderators will see it." xml:space="preserve"> + <source>Report content: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report member profile: only group moderators will see it." xml:space="preserve"> + <source>Report member profile: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report other: only group moderators will see it." xml:space="preserve"> + <source>Report other: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report reason?" xml:space="preserve"> + <source>Report reason?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Report spam: only group moderators will see it." xml:space="preserve"> + <source>Report spam: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report violation: only group moderators will see it." xml:space="preserve"> + <source>Report violation: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report: %@" xml:space="preserve"> + <source>Report: %@</source> + <note>report in notification</note> + </trans-unit> + <trans-unit id="Reporting messages to moderators is prohibited." xml:space="preserve"> + <source>Reporting messages to moderators is prohibited.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reports" xml:space="preserve"> + <source>Reports</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Required" xml:space="preserve"> <source>Required</source> <target>Requis</target> @@ -4182,16 +6369,41 @@ Voici votre lien pour le groupe %@ !</target> <target>Réinitialisation</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset all hints" xml:space="preserve"> + <source>Reset all hints</source> + <target>Rétablir tous les conseils</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics" xml:space="preserve"> + <source>Reset all statistics</source> + <target>Réinitialiser toutes les statistiques</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics?" xml:space="preserve"> + <source>Reset all statistics?</source> + <target>Réinitialiser toutes les statistiques ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> <target>Réinitialisation des couleurs</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to app theme" xml:space="preserve"> + <source>Reset to app theme</source> + <target>Réinitialisation au thème de l'appli</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> <target>Réinitialisation des valeurs par défaut</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to user theme" xml:space="preserve"> + <source>Reset to user theme</source> + <target>Réinitialisation au thème de l'utilisateur</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> <source>Restart the app to create a new chat profile</source> <target>Redémarrez l'application pour créer un nouveau profil de chat</target> @@ -4222,14 +6434,19 @@ Voici votre lien pour le groupe %@ !</target> <target>Erreur de restauration de la base de données</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Retry" xml:space="preserve"> + <source>Retry</source> + <target>Réessayer</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> <target>Révéler</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Revert" xml:space="preserve"> - <source>Revert</source> - <target>Revenir en arrière</target> + <trans-unit id="Review conditions" xml:space="preserve"> + <source>Review conditions</source> + <target>Vérifier les conditions</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke" xml:space="preserve"> @@ -4257,65 +6474,80 @@ Voici votre lien pour le groupe %@ !</target> <target>Exécuter le chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve"> - <source>SMP servers</source> - <target>Serveurs SMP</target> + <trans-unit id="SMP server" xml:space="preserve"> + <source>SMP server</source> + <target>Serveur SMP</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SOCKS proxy" xml:space="preserve"> + <source>SOCKS proxy</source> + <target>proxy SOCKS</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safely receive files" xml:space="preserve"> + <source>Safely receive files</source> + <target>Réception de fichiers en toute sécurité</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safer groups" xml:space="preserve"> + <source>Safer groups</source> + <target>Groupes plus sûrs</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save" xml:space="preserve"> <source>Save</source> - <target>Sauvegarder</target> - <note>chat item action</note> + <target>Enregistrer</target> + <note>alert button +chat item action</note> </trans-unit> <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> - <target>Sauvegarder (et en informer les contacts)</target> - <note>No comment provided by engineer.</note> + <target>Enregistrer (et en informer les contacts)</target> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> - <target>Sauvegarder et en informer les contacts</target> - <note>No comment provided by engineer.</note> + <target>Enregistrer et en informer le contact</target> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> - <target>Sauvegarder et en informer les membres du groupe</target> + <target>Enregistrer et en informer les membres du groupe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save and reconnect" xml:space="preserve"> + <source>Save and reconnect</source> + <target>Sauvegarder et se reconnecter</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save and update group profile" xml:space="preserve"> <source>Save and update group profile</source> - <target>Sauvegarder et mettre à jour le profil du groupe</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save archive" xml:space="preserve"> - <source>Save archive</source> - <target>Sauvegarder l'archive</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve"> - <source>Save auto-accept settings</source> - <target>Sauvegarder les paramètres d'acceptation automatique</target> + <target>Enregistrer et mettre à jour le profil du groupe</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> <source>Save group profile</source> - <target>Sauvegarder le profil du groupe</target> + <target>Enregistrer le profil du groupe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save list" xml:space="preserve"> + <source>Save list</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> - <target>Sauvegarder la phrase secrète et ouvrir le chat</target> + <target>Enregistrer la phrase secrète et ouvrir le chat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save passphrase in Keychain" xml:space="preserve"> <source>Save passphrase in Keychain</source> - <target>Sauvegarder la phrase secrète dans la keychain</target> + <target>Enregistrer la phrase secrète dans la Keychain</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> - <target>Sauvegarder les préférences ?</target> - <note>No comment provided by engineer.</note> + <target>Enregistrer les préférences ?</target> + <note>alert title</note> </trans-unit> <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> @@ -4324,22 +6556,27 @@ Voici votre lien pour le groupe %@ !</target> </trans-unit> <trans-unit id="Save servers" xml:space="preserve"> <source>Save servers</source> - <target>Sauvegarder les serveurs</target> + <target>Enregistrer les serveurs</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> - <target>Sauvegarder les serveurs ?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve"> - <source>Save settings?</source> - <target>Sauvegarder les paramètres ?</target> - <note>No comment provided by engineer.</note> + <target>Enregistrer les serveurs ?</target> + <note>alert title</note> </trans-unit> <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> - <target>Sauvegarder le message d'accueil ?</target> + <target>Enregistrer le message d'accueil ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save your profile?" xml:space="preserve"> + <source>Save your profile?</source> + <target>Sauvegarder votre profil ?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Saved" xml:space="preserve"> + <source>Saved</source> + <target>Enregistré</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> @@ -4347,6 +6584,31 @@ Voici votre lien pour le groupe %@ !</target> <target>Les serveurs WebRTC ICE sauvegardés seront supprimés</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Saved from" xml:space="preserve"> + <source>Saved from</source> + <target>Enregistré depuis</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved message" xml:space="preserve"> + <source>Saved message</source> + <target>Message enregistré</target> + <note>message info title</note> + </trans-unit> + <trans-unit id="Saving %lld messages" xml:space="preserve"> + <source>Saving %lld messages</source> + <target>Sauvegarde de %lld messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scale" xml:space="preserve"> + <source>Scale</source> + <target>Échelle</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan / Paste link" xml:space="preserve"> + <source>Scan / Paste link</source> + <target>Scanner / Coller un lien</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> <target>Scanner un code QR</target> @@ -4354,7 +6616,7 @@ Voici votre lien pour le groupe %@ !</target> </trans-unit> <trans-unit id="Scan QR code from desktop" xml:space="preserve"> <source>Scan QR code from desktop</source> - <target>Scanner le code QR du bureau</target> + <target>Scannez le code QR du bureau</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Scan code" xml:space="preserve"> @@ -4374,7 +6636,22 @@ Voici votre lien pour le groupe %@ !</target> </trans-unit> <trans-unit id="Search" xml:space="preserve"> <source>Search</source> - <target>Recherche</target> + <target>Rechercher</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search bar accepts invitation links." xml:space="preserve"> + <source>Search bar accepts invitation links.</source> + <target>La barre de recherche accepte les liens d'invitation.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search or paste SimpleX link" xml:space="preserve"> + <source>Search or paste SimpleX link</source> + <target>Rechercher ou coller un lien SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secondary" xml:space="preserve"> + <source>Secondary</source> + <target>Secondaire</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> @@ -4382,6 +6659,11 @@ Voici votre lien pour le groupe %@ !</target> <target>File d'attente sécurisée</target> <note>server test step</note> </trans-unit> + <trans-unit id="Secured" xml:space="preserve"> + <source>Secured</source> + <target>Sécurisées</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> <target>Évaluation de sécurité</target> @@ -4395,6 +6677,21 @@ Voici votre lien pour le groupe %@ !</target> <trans-unit id="Select" xml:space="preserve"> <source>Select</source> <target>Choisir</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Select chat profile" xml:space="preserve"> + <source>Select chat profile</source> + <target>Sélectionner un profil de discussion</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected %lld" xml:space="preserve"> + <source>Selected %lld</source> + <target>%lld sélectionné(s)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Les préférences de chat sélectionnées interdisent ce message.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct" xml:space="preserve"> @@ -4432,11 +6729,6 @@ Voici votre lien pour le groupe %@ !</target> <target>Envoyer les accusés de réception à</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve"> - <source>Send direct message</source> - <target>Envoyer un message direct</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Send direct message to connect" xml:space="preserve"> <source>Send direct message to connect</source> <target>Envoyer un message direct pour vous connecter</target> @@ -4447,9 +6739,14 @@ Voici votre lien pour le groupe %@ !</target> <target>Envoyer un message éphémère</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send errors" xml:space="preserve"> + <source>Send errors</source> + <target>Erreurs d'envoi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> - <target>Envoi d'aperçus de liens</target> + <target>Aperçu des liens</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send live message" xml:space="preserve"> @@ -4457,14 +6754,28 @@ Voici votre lien pour le groupe %@ !</target> <target>Envoyer un message dynamique</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send message to enable calls." xml:space="preserve"> + <source>Send message to enable calls.</source> + <target>Envoyer un message pour activer les appels.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source> + <target>Envoyer les messages de manière directe lorsque l'adresse IP est protégée et que votre serveur ou le serveur de destination ne prend pas en charge le routage privé.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when your or destination server does not support private routing.</source> + <target>Envoyez les messages de manière directe lorsque votre serveur ou le serveur de destination ne prend pas en charge le routage privé.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> <target>Envoi de notifications</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> - <source>Send notifications:</source> - <target>Envoi de notifications :</target> + <trans-unit id="Send private reports" xml:space="preserve"> + <source>Send private reports</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve"> @@ -4474,7 +6785,7 @@ Voici votre lien pour le groupe %@ !</target> </trans-unit> <trans-unit id="Send receipts" xml:space="preserve"> <source>Send receipts</source> - <target>Envoyer les justificatifs</target> + <target>Envoi de justificatifs</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve"> @@ -4482,10 +6793,15 @@ Voici votre lien pour le groupe %@ !</target> <target>Envoyez-les depuis la phototèque ou des claviers personnalisés.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve"> + <source>Send up to 100 last messages to new members.</source> + <target>Envoi des 100 derniers messages aux nouveaux membres.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> <target>L'expéditeur a annulé le transfert de fichiers.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> @@ -4542,6 +6858,11 @@ Voici votre lien pour le groupe %@ !</target> <target>Envoyé le : %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Sent directly" xml:space="preserve"> + <source>Sent directly</source> + <target>Envoyé directement</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> <target>Événement de fichier envoyé</target> @@ -4552,11 +6873,71 @@ Voici votre lien pour le groupe %@ !</target> <target>Message envoyé</target> <note>message info title</note> </trans-unit> + <trans-unit id="Sent messages" xml:space="preserve"> + <source>Sent messages</source> + <target>Messages envoyés</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> <target>Les messages envoyés seront supprimés après une durée déterminée.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sent reply" xml:space="preserve"> + <source>Sent reply</source> + <target>Réponse envoyée</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent total" xml:space="preserve"> + <source>Sent total</source> + <target>Total envoyé</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent via proxy" xml:space="preserve"> + <source>Sent via proxy</source> + <target>Envoyé via le proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server" xml:space="preserve"> + <source>Server</source> + <target>Serveur</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server added to operator %@." xml:space="preserve"> + <source>Server added to operator %@.</source> + <target>Serveur ajouté à l'opérateur %@.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Server address" xml:space="preserve"> + <source>Server address</source> + <target>Adresse du serveur</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings." xml:space="preserve"> + <source>Server address is incompatible with network settings.</source> + <target>L'adresse du serveur est incompatible avec les paramètres du réseau.</target> + <note>srv error text.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Server address is incompatible with network settings: %@.</source> + <target>L'adresse du serveur est incompatible avec les paramètres réseau : %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server operator changed." xml:space="preserve"> + <source>Server operator changed.</source> + <target>L'opérateur du serveur a changé.</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Server operators" xml:space="preserve"> + <source>Server operators</source> + <target>Opérateurs de serveur</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server protocol changed." xml:space="preserve"> + <source>Server protocol changed.</source> + <target>Le protocole du serveur a été modifié.</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> <target>Le serveur requiert une autorisation pour créer des files d'attente, vérifiez le mot de passe</target> @@ -4564,7 +6945,7 @@ Voici votre lien pour le groupe %@ !</target> </trans-unit> <trans-unit id="Server requires authorization to upload, check password" xml:space="preserve"> <source>Server requires authorization to upload, check password</source> - <target>Le serveur requiert une autorisation pour uploader, vérifiez le mot de passe</target> + <target>Le serveur requiert une autorisation pour téléverser, vérifiez le mot de passe</target> <note>server test error</note> </trans-unit> <trans-unit id="Server test failed!" xml:space="preserve"> @@ -4572,11 +6953,36 @@ Voici votre lien pour le groupe %@ !</target> <target>Échec du test du serveur !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Server type" xml:space="preserve"> + <source>Server type</source> + <target>Type de serveur</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server version is incompatible with network settings." xml:space="preserve"> + <source>Server version is incompatible with network settings.</source> + <target>La version du serveur est incompatible avec les paramètres du réseau.</target> + <note>srv error text</note> + </trans-unit> + <trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve"> + <source>Server version is incompatible with your app: %@.</source> + <target>La version du serveur est incompatible avec votre appli : %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Servers" xml:space="preserve"> <source>Servers</source> <target>Serveurs</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Servers info" xml:space="preserve"> + <source>Servers info</source> + <target>Infos serveurs</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <target>Les statistiques des serveurs seront réinitialisées - il n'est pas possible de revenir en arrière !</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Session code" xml:space="preserve"> <source>Session code</source> <target>Code de session</target> @@ -4587,11 +6993,20 @@ Voici votre lien pour le groupe %@ !</target> <target>Définir 1 jour</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set chat name…" xml:space="preserve"> + <source>Set chat name…</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> <target>Définir le nom du contact…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve"> + <source>Set default theme</source> + <target>Définir le thème par défaut</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> <target>Définir les préférences du groupe</target> @@ -4602,11 +7017,20 @@ Voici votre lien pour le groupe %@ !</target> <target>Il permet de remplacer l'authentification du système.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve"> + <source>Set message expiration in chats.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passcode" xml:space="preserve"> <source>Set passcode</source> <target>Définir le code d'accès</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set passphrase" xml:space="preserve"> + <source>Set passphrase</source> + <target>Définir une phrase secrète</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> <target>Définir la phrase secrète pour l'export</target> @@ -4627,24 +7051,55 @@ Voici votre lien pour le groupe %@ !</target> <target>Paramètres</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve"> + <source>Settings were changed.</source> + <target>Les paramètres ont été modifiés.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve"> + <source>Shape profile images</source> + <target>Images de profil modelable</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share" xml:space="preserve"> <source>Share</source> <target>Partager</target> - <note>chat item action</note> + <note>alert action +chat item action</note> </trans-unit> <trans-unit id="Share 1-time link" xml:space="preserve"> <source>Share 1-time link</source> <target>Partager un lien unique</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share 1-time link with a friend" xml:space="preserve"> + <source>Share 1-time link with a friend</source> + <target>Partager un lien unique avec un ami</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve"> + <source>Share SimpleX address on social media.</source> + <target>Partagez votre adresse SimpleX sur les réseaux sociaux.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address" xml:space="preserve"> <source>Share address</source> <target>Partager l'adresse</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share address publicly" xml:space="preserve"> + <source>Share address publicly</source> + <target>Partager publiquement votre adresse</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address with contacts?" xml:space="preserve"> <source>Share address with contacts?</source> <target>Partager l'adresse avec vos contacts ?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Share from other apps." xml:space="preserve"> + <source>Share from other apps.</source> + <target>Partager depuis d'autres applications.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share link" xml:space="preserve"> @@ -4652,9 +7107,19 @@ Voici votre lien pour le groupe %@ !</target> <target>Partager le lien</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve"> - <source>Share one-time invitation link</source> - <target>Partager un lien d'invitation unique</target> + <trans-unit id="Share profile" xml:space="preserve"> + <source>Share profile</source> + <target>Partager le profil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share this 1-time invite link" xml:space="preserve"> + <source>Share this 1-time invite link</source> + <target>Partagez ce lien d'invitation unique</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share to SimpleX" xml:space="preserve"> + <source>Share to SimpleX</source> + <target>Partager sur SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share with contacts" xml:space="preserve"> @@ -4662,6 +7127,15 @@ Voici votre lien pour le groupe %@ !</target> <target>Partager avec vos contacts</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Short link" xml:space="preserve"> + <source>Short link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show QR code" xml:space="preserve"> + <source>Show QR code</source> + <target>Afficher le code QR</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> <target>Afficher les appels dans l'historique du téléphone</target> @@ -4674,12 +7148,27 @@ Voici votre lien pour le groupe %@ !</target> </trans-unit> <trans-unit id="Show last messages" xml:space="preserve"> <source>Show last messages</source> - <target>Voir les derniers messages</target> + <target>Aperçu des derniers messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show message status" xml:space="preserve"> + <source>Show message status</source> + <target>Afficher le statut du message</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show percentage" xml:space="preserve"> + <source>Show percentage</source> + <target>Afficher le pourcentage</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> - <target>Montrer l'aperçu</target> + <target>Aperçu affiché</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show → on messages sent via private routing." xml:space="preserve"> + <source>Show → on messages sent via private routing.</source> + <target>Afficher → sur les messages envoyés via le routage privé.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Show:" xml:space="preserve"> @@ -4687,11 +7176,21 @@ Voici votre lien pour le groupe %@ !</target> <target>Afficher :</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve"> + <source>SimpleX</source> + <target>SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Address" xml:space="preserve"> <source>SimpleX Address</source> <target>Adresse SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." xml:space="preserve"> + <source>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</source> + <target>SimpleX Chat et Flux ont conclu un accord pour inclure les serveurs exploités par Flux dans l'application.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> <target>La sécurité de SimpleX Chat a été auditée par Trail of Bits.</target> @@ -4722,6 +7221,20 @@ Voici votre lien pour le groupe %@ !</target> <target>Adresse SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX address and 1-time links are safe to share via any messenger." xml:space="preserve"> + <source>SimpleX address and 1-time links are safe to share via any messenger.</source> + <target>Les adresses SimpleX et les liens à usage unique peuvent être partagés en toute sécurité via n'importe quelle messagerie.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address or 1-time link?" xml:space="preserve"> + <source>SimpleX address or 1-time link?</source> + <target>Adresse SimpleX ou lien unique ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX channel link" xml:space="preserve"> + <source>SimpleX channel link</source> + <note>simplex link type</note> + </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> <target>Adresse de contact SimpleX</target> @@ -4740,6 +7253,16 @@ Voici votre lien pour le groupe %@ !</target> <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> <target>Liens SimpleX</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve"> + <source>SimpleX links are prohibited.</source> + <target>Les liens SimpleX sont interdits dans ce groupe.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve"> + <source>SimpleX links not allowed</source> + <target>Les liens SimpleX ne sont pas autorisés</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> @@ -4747,11 +7270,21 @@ Voici votre lien pour le groupe %@ !</target> <target>Invitation unique SimpleX</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> + <source>SimpleX protocols reviewed by Trail of Bits.</source> + <target>Protocoles SimpleX audité par Trail of Bits.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Simplified incognito mode" xml:space="preserve"> <source>Simplified incognito mode</source> <target>Mode incognito simplifié</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Size" xml:space="preserve"> + <source>Size</source> + <target>Taille</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Passer</target> @@ -4767,19 +7300,51 @@ Voici votre lien pour le groupe %@ !</target> <target>Petits groupes (max 20)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Soft" xml:space="preserve"> + <source>Soft</source> + <target>Léger</target> + <note>blur media</note> + </trans-unit> + <trans-unit id="Some app settings were not migrated." xml:space="preserve"> + <source>Some app settings were not migrated.</source> + <target>Certains paramètres de l'application n'ont pas été migrés.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some file(s) were not exported:" xml:space="preserve"> + <source>Some file(s) were not exported:</source> + <target>Certains fichiers n'ont pas été exportés :</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Des erreurs non fatales se sont produites lors de l'importation - vous pouvez consulter la console de chat pour plus de détails.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import:" xml:space="preserve"> + <source>Some non-fatal errors occurred during import:</source> + <target>L'importation a entraîné des erreurs non fatales :</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some servers failed the test: %@" xml:space="preserve"> + <source>Some servers failed the test: +%@</source> + <target>Certains serveurs ont échoué le test : +%@</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> <target>Quelqu'un</target> <note>notification title</note> </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve"> - <source>Start a new chat</source> - <target>Commencer une nouvelle conversation</target> + <trans-unit id="Spam" xml:space="preserve"> + <source>Spam</source> + <note>blocking reason +report reason</note> + </trans-unit> + <trans-unit id="Square, circle, or anything in between." xml:space="preserve"> + <source>Square, circle, or anything in between.</source> + <target>Carré, circulaire, ou toute autre forme intermédiaire.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start chat" xml:space="preserve"> @@ -4787,11 +7352,26 @@ Voici votre lien pour le groupe %@ !</target> <target>Démarrer le chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Start chat?" xml:space="preserve"> + <source>Start chat?</source> + <target>Lancer le chat ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> <target>Démarrer la migration</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Starting from %@." xml:space="preserve"> + <source>Starting from %@.</source> + <target>À partir de %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Statistics" xml:space="preserve"> + <source>Statistics</source> + <target>Statistiques</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> <target>Arrêter</target> @@ -4802,9 +7382,9 @@ Voici votre lien pour le groupe %@ !</target> <target>Arrêter SimpleX</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> - <source>Stop chat to enable database actions</source> - <target>Arrêter le chat pour permettre des actions sur la base de données</target> + <trans-unit id="Stop chat" xml:space="preserve"> + <source>Stop chat</source> + <target>Arrêter le chat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> @@ -4835,23 +7415,62 @@ Voici votre lien pour le groupe %@ !</target> <trans-unit id="Stop sharing" xml:space="preserve"> <source>Stop sharing</source> <target>Cesser le partage</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Stop sharing address?" xml:space="preserve"> <source>Stop sharing address?</source> <target>Cesser le partage d'adresse ?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Stopping chat" xml:space="preserve"> + <source>Stopping chat</source> + <target>Arrêt du chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Storage" xml:space="preserve"> + <source>Storage</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Strong" xml:space="preserve"> + <source>Strong</source> + <target>Fort</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Submit" xml:space="preserve"> <source>Submit</source> <target>Soumettre</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Subscribed" xml:space="preserve"> + <source>Subscribed</source> + <target>Inscriptions</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscription errors" xml:space="preserve"> + <source>Subscription errors</source> + <target>Erreurs d'inscription</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscriptions ignored" xml:space="preserve"> + <source>Subscriptions ignored</source> + <target>Inscriptions ignorées</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Support SimpleX Chat" xml:space="preserve"> <source>Support SimpleX Chat</source> <target>Supporter SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Switch audio and video during the call." xml:space="preserve"> + <source>Switch audio and video during the call.</source> + <target>Passer de l'audio à la vidéo pendant l'appel.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> + <source>Switch chat profile for 1-time invitations.</source> + <target>Changer de profil de chat pour les invitations à usage unique.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="System" xml:space="preserve"> <source>System</source> <target>Système</target> @@ -4862,11 +7481,20 @@ Voici votre lien pour le groupe %@ !</target> <target>Authentification du système</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP connection" xml:space="preserve"> + <source>TCP connection</source> + <target>Connexion TCP</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> <target>Délai de connexion TCP</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP port for messaging" xml:space="preserve"> + <source>TCP port for messaging</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <source>TCP_KEEPCNT</source> <target>TCP_KEEPCNT</target> @@ -4882,11 +7510,21 @@ Voici votre lien pour le groupe %@ !</target> <target>TCP_KEEPINTVL</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tail" xml:space="preserve"> + <source>Tail</source> + <target>Queue</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> <target>Prendre une photo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap Create SimpleX address in the menu to create it later." xml:space="preserve"> + <source>Tap Create SimpleX address in the menu to create it later.</source> + <target>Appuyez sur Créer une adresse SimpleX dans le menu pour la créer ultérieurement.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> <target>Appuyez sur le bouton </target> @@ -4912,16 +7550,30 @@ Voici votre lien pour le groupe %@ !</target> <target>Appuyez pour rejoindre incognito</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve"> - <source>Tap to start a new chat</source> - <target>Appuyez ici pour démarrer une nouvelle discussion</target> + <trans-unit id="Tap to paste link" xml:space="preserve"> + <source>Tap to paste link</source> + <target>Appuyez pour coller le lien</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap to scan" xml:space="preserve"> + <source>Tap to scan</source> + <target>Appuyez pour scanner</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Temporary file error" xml:space="preserve"> + <source>Temporary file error</source> + <target>Erreur de fichier temporaire</target> + <note>file error alert title</note> + </trans-unit> <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> <target>Échec du test à l'étape %@.</target> <note>server test failure</note> </trans-unit> + <trans-unit id="Test notifications" xml:space="preserve"> + <source>Test notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> <target>Tester le serveur</target> @@ -4935,7 +7587,7 @@ Voici votre lien pour le groupe %@ !</target> <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> <target>Échec des tests !</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> @@ -4952,11 +7604,6 @@ Voici votre lien pour le groupe %@ !</target> <target>Merci aux utilisateurs - contribuez via Weblate !</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> - <target>La 1ère plateforme sans aucun identifiant d'utilisateur – privée par design.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <source>The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised.</source> @@ -4969,11 +7616,31 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. <target>L'application peut vous avertir lorsque vous recevez des messages ou des demandes de contact - veuillez ouvrir les paramètres pour les activer.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The app protects your privacy by using different operators in each conversation." xml:space="preserve"> + <source>The app protects your privacy by using different operators in each conversation.</source> + <target>L'application protège votre vie privée en utilisant des opérateurs différents pour chaque conversation.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve"> + <source>The app will ask to confirm downloads from unknown file servers (except .onion).</source> + <target>L'application demandera de confirmer les téléchargements à partir de serveurs de fichiers inconnus (sauf .onion).</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> <source>The attempt to change database passphrase was not completed.</source> <target>La tentative de modification de la phrase secrète de la base de données n'a pas abouti.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve"> + <source>The code you scanned is not a SimpleX link QR code.</source> + <target>Le code scanné n'est pas un code QR de lien SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The connection reached the limit of undelivered messages, your contact may be offline." xml:space="preserve"> + <source>The connection reached the limit of undelivered messages, your contact may be offline.</source> + <target>La connexion a atteint la limite des messages non délivrés, votre contact est peut-être hors ligne.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> <target>La connexion que vous avez acceptée sera annulée !</target> @@ -4994,6 +7661,11 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. <target>Le chiffrement fonctionne et le nouvel accord de chiffrement n'est pas nécessaire. Cela peut provoquer des erreurs de connexion !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> + <target>La nouvelle génération de messagerie privée</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The hash of the previous message is different." xml:space="preserve"> <source>The hash of the previous message is different.</source> <target>Le hash du message précédent est différent.</target> @@ -5009,9 +7681,14 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. <target>Le message sera marqué comme modéré pour tous les membres.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> - <target>La nouvelle génération de messagerie privée</target> + <trans-unit id="The messages will be deleted for all members." xml:space="preserve"> + <source>The messages will be deleted for all members.</source> + <target>Les messages seront supprimés pour tous les membres.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve"> + <source>The messages will be marked as moderated for all members.</source> + <target>Les messages seront marqués comme modérés pour tous les membres.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> @@ -5019,9 +7696,14 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. <target>L'ancienne base de données n'a pas été supprimée lors de la migration, elle peut être supprimée.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> - <target>Le profil n'est partagé qu'avec vos contacts.</target> + <trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve"> + <source>The same conditions will apply to operator **%@**.</source> + <target>Les mêmes conditions s'appliquent à l'opérateur **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second preset operator in the app!" xml:space="preserve"> + <source>The second preset operator in the app!</source> + <target>Le deuxième opérateur prédéfini de l'application !</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> @@ -5039,19 +7721,29 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. <target>Les serveurs pour les nouvelles connexions de votre profil de chat actuel **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Theme" xml:space="preserve"> - <source>Theme</source> - <target>Thème</target> + <trans-unit id="The servers for new files of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new files of your current chat profile **%@**.</source> + <target>Les serveurs pour les nouveaux fichiers de votre profil de chat actuel **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve"> - <source>There should be at least one user profile.</source> - <target>Il doit y avoir au moins un profil d'utilisateur.</target> + <trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve"> + <source>The text you pasted is not a SimpleX link.</source> + <target>Le texte collé n'est pas un lien SimpleX.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> - <source>There should be at least one visible user profile.</source> - <target>Il doit y avoir au moins un profil d'utilisateur visible.</target> + <trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve"> + <source>The uploaded database archive will be permanently removed from the servers.</source> + <target>L'archive de la base de données envoyée sera définitivement supprimée des serveurs.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Themes" xml:space="preserve"> + <source>Themes</source> + <target>Thèmes</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="These conditions will also apply for: **%@**." xml:space="preserve"> + <source>These conditions will also apply for: **%@**.</source> + <target>Ces conditions s'appliquent également aux : **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> @@ -5074,14 +7766,33 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. <target>Cette action ne peut être annulée - les messages envoyés et reçus avant la date sélectionnée seront supprimés. Cela peut prendre plusieurs minutes.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> <target>Cette action ne peut être annulée - votre profil, vos contacts, vos messages et vos fichiers seront irréversiblement perdus.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by end-to-end encryption.</source> + <target>Cette discussion est protégée par un chiffrement de bout en bout.</target> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by quantum resistant end-to-end encryption.</source> + <target>Cette discussion est protégée par un chiffrement de bout en bout résistant aux technologies quantiques.</target> + <note>E2EE info chat item</note> + </trans-unit> <trans-unit id="This device name" xml:space="preserve"> <source>This device name</source> - <target>Ce nom d'appareil</target> + <target>Nom de cet appareil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve"> + <source>This display name is invalid. Please choose another name.</source> + <target>Ce nom d'affichage est invalide. Veuillez choisir un autre nom.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> @@ -5104,11 +7815,29 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. <target>Voici votre propre lien unique !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve"> + <source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve"> + <source>This link was used with another mobile device, please create a new link on the desktop.</source> + <target>Ce lien a été utilisé avec un autre appareil mobile, veuillez créer un nouveau lien sur le bureau.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This message was deleted or not received yet." xml:space="preserve"> + <source>This message was deleted or not received yet.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <source>This setting applies to messages in your current chat profile **%@**.</source> <target>Ce paramètre s'applique aux messages de votre profil de chat actuel **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Title" xml:space="preserve"> + <source>Title</source> + <target>Titre</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> <source>To ask any questions and to receive updates:</source> <target>Si vous avez des questions et que vous souhaitez des réponses :</target> @@ -5116,7 +7845,7 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. </trans-unit> <trans-unit id="To connect, your contact can scan QR code or use the link in the app." xml:space="preserve"> <source>To connect, your contact can scan QR code or use the link in the app.</source> - <target>Pour se connecter, votre contact peut scanner le code QR ou utiliser le lien dans l'application.</target> + <target>Pour se connecter, votre contact peut scanner un code QR ou utiliser un lien dans l'app.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To hide unwanted messages." xml:space="preserve"> @@ -5129,9 +7858,9 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. <target>Pour établir une nouvelle connexion</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <target>Pour protéger votre vie privée, au lieu d’IDs utilisés par toutes les autres plateformes, SimpleX a des IDs pour les queues de messages, distinctes pour chacun de vos contacts.</target> + <trans-unit id="To protect against your link being replaced, you can compare contact security codes." xml:space="preserve"> + <source>To protect against your link being replaced, you can compare contact security codes.</source> + <target>Pour vous protéger contre le remplacement de votre lien, vous pouvez comparer les codes de sécurité des contacts.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -5139,6 +7868,11 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. <target>Pour préserver le fuseau horaire, les fichiers image/voix utilisent le système UTC.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve"> + <source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source> + <target>Pour protéger votre adresse IP, le routage privé utilise vos serveurs SMP pour délivrer les messages.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> @@ -5146,6 +7880,26 @@ You will be prompted to complete authentication before this feature is enabled.< Vous serez invité à confirmer l'authentification avant que cette fonction ne soit activée.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> + <target>Pour protéger votre vie privée, au lieu d’IDs utilisés par toutes les autres plateformes, SimpleX a des IDs pour les queues de messages, distinctes pour chacun de vos contacts.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To receive" xml:space="preserve"> + <source>To receive</source> + <target>Pour recevoir</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve"> + <source>To record speech please grant permission to use Microphone.</source> + <target>Si vous souhaitez enregistrer une conversation, veuillez autoriser l'utilisation du microphone.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record video please grant permission to use Camera." xml:space="preserve"> + <source>To record video please grant permission to use Camera.</source> + <target>Si vous souhaitez enregistrer une vidéo, veuillez autoriser l'utilisation de la caméra.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> <target>Pour enregistrer un message vocal, veuillez accorder la permission d'utiliser le microphone.</target> @@ -5156,26 +7910,60 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s <target>Pour révéler votre profil caché, entrez le mot de passe dans le champ de recherche de la page **Vos profils de chat**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To send" xml:space="preserve"> + <source>To send</source> + <target>Pour envoyer</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> <source>To support instant push notifications the chat database has to be migrated.</source> <target>Pour prendre en charge les notifications push instantanées, la base de données du chat doit être migrée.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To use the servers of **%@**, accept conditions of use." xml:space="preserve"> + <source>To use the servers of **%@**, accept conditions of use.</source> + <target>Pour utiliser les serveurs de **%@**, acceptez les conditions d'utilisation.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> <target>Pour vérifier le chiffrement de bout en bout avec votre contact, comparez (ou scannez) le code sur vos appareils.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle chat list:" xml:space="preserve"> + <source>Toggle chat list:</source> + <target>Afficher la liste des conversations :</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> <source>Toggle incognito when connecting.</source> <target>Basculer en mode incognito lors de la connexion.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Token status: %@." xml:space="preserve"> + <source>Token status: %@.</source> + <note>token status</note> + </trans-unit> + <trans-unit id="Toolbar opacity" xml:space="preserve"> + <source>Toolbar opacity</source> + <target>Opacité de la barre d'outils</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Total" xml:space="preserve"> + <source>Total</source> + <target>Total</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>Transport isolé</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Transport sessions" xml:space="preserve"> + <source>Transport sessions</source> + <target>Sessions de transport</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> <target>Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact (erreur : %@).</target> @@ -5186,16 +7974,16 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s <target>Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Turkish interface" xml:space="preserve"> + <source>Turkish interface</source> + <target>Interface en turc</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> <target>Désactiver</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve"> - <source>Turn off notifications?</source> - <target>Désactiver les notifications ?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> <target>Activer</target> @@ -5211,20 +7999,30 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s <target>Débloquer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock for all" xml:space="preserve"> + <source>Unblock for all</source> + <target>Débloquer pour tous</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member" xml:space="preserve"> <source>Unblock member</source> <target>Débloquer ce membre</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock member for all?" xml:space="preserve"> + <source>Unblock member for all?</source> + <target>Débloquer le membre pour tous ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member?" xml:space="preserve"> <source>Unblock member?</source> <target>Débloquer ce membre ?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve"> - <source>Unexpected error: %@</source> - <target>Erreur inattendue : %@</target> - <note>item status description</note> + <trans-unit id="Undelivered messages" xml:space="preserve"> + <source>Undelivered messages</source> + <target>Messages non distribués</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -5234,7 +8032,7 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s <trans-unit id="Unfav." xml:space="preserve"> <source>Unfav.</source> <target>Unfav.</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> @@ -5271,6 +8069,11 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s <target>Erreur inconnue</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unknown servers!" xml:space="preserve"> + <source>Unknown servers!</source> + <target>Serveurs inconnus !</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> <target>À moins que vous utilisiez l'interface d'appel d'iOS, activez le mode "Ne pas déranger" pour éviter les interruptions.</target> @@ -5306,11 +8109,20 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> <target>Démute</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> </trans-unit> <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> <target>Non lu</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Unsupported connection link" xml:space="preserve"> + <source>Unsupported connection link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve"> + <source>Up to 100 last messages are sent to new members.</source> + <target>Les 100 derniers messages sont envoyés aux nouveaux membres.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update" xml:space="preserve"> @@ -5318,11 +8130,6 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien <target>Mise à jour</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> - <source>Update .onion hosts setting?</source> - <target>Mettre à jour le paramètre des hôtes .onion ?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> <target>Mise à jour de la phrase secrète de la base de données</target> @@ -5333,9 +8140,13 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien <target>Mettre à jour les paramètres réseau ?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve"> - <source>Update transport isolation mode?</source> - <target>Mettre à jour le mode d'isolement du transport ?</target> + <trans-unit id="Update settings?" xml:space="preserve"> + <source>Update settings?</source> + <target>Mettre à jour les paramètres ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updated conditions" xml:space="preserve"> + <source>Updated conditions</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> @@ -5343,31 +8154,69 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien <target>La mise à jour des ces paramètres reconnectera le client à tous les serveurs.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> - <target>La mise à jour de ce paramètre reconnectera le client à tous les serveurs.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Upgrade and open chat" xml:space="preserve"> <source>Upgrade and open chat</source> <target>Mettre à niveau et ouvrir le chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Upload errors" xml:space="preserve"> + <source>Upload errors</source> + <target>Erreurs de téléversement</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload failed" xml:space="preserve"> + <source>Upload failed</source> + <target>Échec de l'envoi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Upload file" xml:space="preserve"> <source>Upload file</source> - <target>Transférer le fichier</target> + <target>Téléverser le fichier</target> <note>server test step</note> </trans-unit> + <trans-unit id="Uploaded" xml:space="preserve"> + <source>Uploaded</source> + <target>Téléversé</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploaded files" xml:space="preserve"> + <source>Uploaded files</source> + <target>Fichiers téléversés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploading archive" xml:space="preserve"> + <source>Uploading archive</source> + <target>Envoi de l'archive</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use %@" xml:space="preserve"> + <source>Use %@</source> + <target>Utiliser %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> <target>Utiliser les hôtes .onions</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use SOCKS proxy" xml:space="preserve"> + <source>Use SOCKS proxy</source> + <target>Utiliser un proxy SOCKS</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> <target>Utiliser les serveurs SimpleX Chat ?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use TCP port %@ when no port is specified." xml:space="preserve"> + <source>Use TCP port %@ when no port is specified.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve"> + <source>Use TCP port 443 for preset servers only.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> <target>Utiliser le chat</target> @@ -5378,6 +8227,16 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien <target>Utiliser le profil actuel</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use for files" xml:space="preserve"> + <source>Use for files</source> + <target>Utiliser pour les fichiers</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for messages" xml:space="preserve"> + <source>Use for messages</source> + <target>Utiliser pour les messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Utiliser pour les nouvelles connexions</target> @@ -5385,7 +8244,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien </trans-unit> <trans-unit id="Use from desktop" xml:space="preserve"> <source>Use from desktop</source> - <target>Utilisation depuis le bureau</target> + <target>Accès au bureau</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use iOS call interface" xml:space="preserve"> @@ -5398,24 +8257,62 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien <target>Utiliser un nouveau profil incognito</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use only local notifications?" xml:space="preserve"> + <source>Use only local notifications?</source> + <target>Utilisation de notifications locales uniquement ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve"> + <source>Use private routing with unknown servers when IP address is not protected.</source> + <target>Utiliser le routage privé avec des serveurs inconnus lorsque l'adresse IP n'est pas protégée.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers." xml:space="preserve"> + <source>Use private routing with unknown servers.</source> + <target>Utiliser le routage privé avec des serveurs inconnus.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Utiliser ce serveur</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> - <target>Profil d'utilisateur</target> + <trans-unit id="Use servers" xml:space="preserve"> + <source>Use servers</source> + <target>Utiliser les serveurs</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> - <target>L'utilisation des hôtes .onion nécessite un fournisseur VPN compatible.</target> + <trans-unit id="Use short links (BETA)" xml:space="preserve"> + <source>Use short links (BETA)</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app while in the call." xml:space="preserve"> + <source>Use the app while in the call.</source> + <target>Utiliser l'application pendant l'appel.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app with one hand." xml:space="preserve"> + <source>Use the app with one hand.</source> + <target>Utiliser l'application d'une main.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use web port" xml:space="preserve"> + <source>Use web port</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="User selection" xml:space="preserve"> + <source>User selection</source> + <target>Sélection de l'utilisateur</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Username" xml:space="preserve"> + <source>Username</source> + <target>Nom d'utilisateur</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> <source>Using SimpleX Chat servers.</source> - <target>Utilisation des serveurs SimpleX Chat.</target> + <target>Vous utilisez les serveurs SimpleX.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify code with desktop" xml:space="preserve"> @@ -5438,6 +8335,16 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien <target>Vérifier les connexions</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Verify database passphrase" xml:space="preserve"> + <source>Verify database passphrase</source> + <target>Vérifier la phrase secrète de la base de données</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify passphrase" xml:space="preserve"> + <source>Verify passphrase</source> + <target>Vérifier la phrase secrète</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Verify security code" xml:space="preserve"> <source>Verify security code</source> <target>Vérifier le code de sécurité</target> @@ -5460,7 +8367,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien </trans-unit> <trans-unit id="Video will be received when your contact completes uploading it." xml:space="preserve"> <source>Video will be received when your contact completes uploading it.</source> - <target>La vidéo ne sera reçue que lorsque votre contact aura fini de la transférer.</target> + <target>La vidéo ne sera reçue que lorsque votre contact aura fini la mettre en ligne.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Video will be received when your contact is online, please wait or check later!" xml:space="preserve"> @@ -5473,11 +8380,26 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien <target>Vidéos et fichiers jusqu'à 1Go</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View conditions" xml:space="preserve"> + <source>View conditions</source> + <target>Voir les conditions</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> <target>Afficher le code de sécurité</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View updated conditions" xml:space="preserve"> + <source>View updated conditions</source> + <target>Voir les conditions mises à jour</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Visible history" xml:space="preserve"> + <source>Visible history</source> + <target>Historique visible</target> + <note>chat feature</note> + </trans-unit> <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> <target>Messages vocaux</target> @@ -5488,11 +8410,16 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien <target>Les messages vocaux sont interdits dans ce chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <target>Les messages vocaux sont interdits dans ce groupe.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Voice messages not allowed" xml:space="preserve"> + <source>Voice messages not allowed</source> + <target>Les messages vocaux ne sont pas autorisés</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> <source>Voice messages prohibited!</source> <target>Messages vocaux interdits !</target> @@ -5523,6 +8450,21 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien <target>En attente de la vidéo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wallpaper accent" xml:space="preserve"> + <source>Wallpaper accent</source> + <target>Accentuation du papier-peint</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper background" xml:space="preserve"> + <source>Wallpaper background</source> + <target>Fond d'écran</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve"> + <source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source> + <target>Attention : démarrer une session de chat sur plusieurs appareils n'est pas pris en charge et entraînera des dysfonctionnements au niveau de la transmission des messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> <source>Warning: you may lose some data!</source> <target>Attention : vous risquez de perdre des données !</target> @@ -5543,6 +8485,11 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien <target>Message de bienvenue</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Welcome message is too long" xml:space="preserve"> + <source>Welcome message is too long</source> + <target>Le message de bienvenue est trop long</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="What's new" xml:space="preserve"> <source>What's new</source> <target>Quoi de neuf ?</target> @@ -5553,9 +8500,14 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien <target>Quand disponible</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> - <source>When people request to connect, you can accept or reject it.</source> - <target>Lorsque des personnes demandent à se connecter, vous pouvez les accepter ou les refuser.</target> + <trans-unit id="When connecting audio and video calls." xml:space="preserve"> + <source>When connecting audio and video calls.</source> + <target>Lors des appels audio et vidéo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When more than one operator is enabled, none of them has metadata to learn who communicates with whom." xml:space="preserve"> + <source>When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</source> + <target>Lorsque plusieurs opérateurs sont activés, aucun d'entre eux ne dispose de métadonnées permettant de savoir qui communique avec qui.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> @@ -5563,29 +8515,74 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien <target>Lorsque vous partagez un profil incognito avec quelqu'un, ce profil sera utilisé pour les groupes auxquels il vous invite.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="WiFi" xml:space="preserve"> + <source>WiFi</source> + <target>WiFi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Will be enabled in direct chats!" xml:space="preserve"> + <source>Will be enabled in direct chats!</source> + <target>Activé dans les discussions directes !</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wired ethernet" xml:space="preserve"> + <source>Wired ethernet</source> + <target>Ethernet câblé</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With encrypted files and media." xml:space="preserve"> + <source>With encrypted files and media.</source> + <target>Avec les fichiers et les médias chiffrés.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> <target>Avec message de bienvenue facultatif.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="With reduced battery usage." xml:space="preserve"> + <source>With reduced battery usage.</source> + <target>Consommation réduite de la batterie.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to file servers.</source> + <target>Sans Tor ou un VPN, votre adresse IP sera visible par les serveurs de fichiers.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source> + <target>Sans Tor ni VPN, votre adresse IP sera visible par ces relais XFTP : %@.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> <target>Mauvaise phrase secrète pour la base de données</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve"> + <source>Wrong key or unknown connection - most likely this connection is deleted.</source> + <target>Clé erronée ou connexion non identifiée - il est très probable que cette connexion soit supprimée.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Wrong key or unknown file chunk address - most likely file is deleted." xml:space="preserve"> + <source>Wrong key or unknown file chunk address - most likely file is deleted.</source> + <target>Mauvaise clé ou adresse inconnue du bloc de données du fichier - le fichier est probablement supprimé.</target> + <note>file error text</note> + </trans-unit> <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> <target>Mauvaise phrase secrète !</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="XFTP servers" xml:space="preserve"> - <source>XFTP servers</source> - <target>Serveurs XFTP</target> + <trans-unit id="XFTP server" xml:space="preserve"> + <source>XFTP server</source> + <target>Serveur XFTP</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> - <target>Vous</target> + <trans-unit id="You **must not** use the same database on two devices." xml:space="preserve"> + <source>You **must not** use the same database on two devices.</source> + <target>Vous **ne devez pas** utiliser la même base de données sur deux appareils.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You accepted connection" xml:space="preserve"> @@ -5608,6 +8605,11 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien <target>Vous êtes déjà connecté·e à %@ via ce lien.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are already connected with %@." xml:space="preserve"> + <source>You are already connected with %@.</source> + <target>Vous êtes déjà connecté avec %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You are already connecting to %@." xml:space="preserve"> <source>You are already connecting to %@.</source> <target>Vous êtes déjà en train de vous connecter à %@.</target> @@ -5655,14 +8657,24 @@ Répéter la demande d'adhésion ?</target> <target>Vous êtes invité·e au groupe</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are not connected to these servers. Private routing is used to deliver messages to them." xml:space="preserve"> + <source>You are not connected to these servers. Private routing is used to deliver messages to them.</source> + <target>Vous n'êtes pas connecté à ces serveurs. Le routage privé est utilisé pour leur délivrer des messages.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> <source>You can accept calls from lock screen, without device and app authentication.</source> <target>Vous pouvez accepter des appels à partir de l'écran de verrouillage, sans authentification de l'appareil ou de l'application.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <target>Vous pouvez également vous connecter en cliquant sur le lien. S'il s'ouvre dans le navigateur, cliquez sur le bouton **Open in mobile app**.</target> + <trans-unit id="You can change it in Appearance settings." xml:space="preserve"> + <source>You can change it in Appearance settings.</source> + <target>Vous pouvez choisir de le modifier dans les paramètres d'apparence.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can configure servers via settings." xml:space="preserve"> + <source>You can configure servers via settings.</source> + <target>Vous pouvez configurer les serveurs via les paramètres.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can create it later" xml:space="preserve"> @@ -5680,16 +8692,36 @@ Répéter la demande d'adhésion ?</target> <target>Vous pouvez les activer ultérieurement via les paramètres de Confidentialité et Sécurité de l'application.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can give another try." xml:space="preserve"> + <source>You can give another try.</source> + <target>Vous pouvez faire un nouvel essai.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <source>You can hide or mute a user profile - swipe it to the right.</source> <target>Vous pouvez masquer ou mettre en sourdine un profil d'utilisateur - faites-le glisser vers la droite.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> + <trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve"> + <source>You can make it visible to your SimpleX contacts via Settings.</source> + <target>Vous pouvez le rendre visible à vos contacts SimpleX via Paramètres.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now chat with %@" xml:space="preserve"> + <source>You can now chat with %@</source> <target>Vous pouvez maintenant envoyer des messages à %@</target> <note>notification body</note> </trans-unit> + <trans-unit id="You can send messages to %@ from Archived contacts." xml:space="preserve"> + <source>You can send messages to %@ from Archived contacts.</source> + <target>Vous pouvez envoyer des messages à %@ à partir des contacts archivés.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can set connection name, to remember who the link was shared with." xml:space="preserve"> + <source>You can set connection name, to remember who the link was shared with.</source> + <target>Vous pouvez définir un nom de connexion pour vous rappeler avec qui le lien a été partagé.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> <target>Vous pouvez configurer l'aperçu des notifications sur l'écran de verrouillage via les paramètres.</target> @@ -5705,16 +8737,16 @@ Répéter la demande d'adhésion ?</target> <target>Vous pouvez partager cette adresse avec vos contacts pour leur permettre de se connecter avec **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> - <source>You can share your address as a link or QR code - anybody can connect to you.</source> - <target>Vous pouvez partager votre adresse sous forme de lien ou de code QR - n'importe qui pourra se connecter à vous.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> <target>Vous pouvez lancer le chat via Paramètres / Base de données ou en redémarrant l'app</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve"> + <source>You can still view conversation with %@ in the list of chats.</source> + <target>Vous pouvez toujours voir la conversation avec %@ dans la liste des discussions.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> <target>Vous pouvez activer SimpleX Lock dans les Paramètres.</target> @@ -5725,21 +8757,26 @@ Répéter la demande d'adhésion ?</target> <target>Vous pouvez utiliser le format markdown pour mettre en forme les messages :</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can view invitation link again in connection details." xml:space="preserve"> + <source>You can view invitation link again in connection details.</source> + <target>Vous pouvez à nouveau consulter le lien d'invitation dans les détails de la connexion.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> <target>Vous ne pouvez pas envoyer de messages !</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <target>Vous contrôlez par quel·s serveur·s vous pouvez **transmettre** ainsi que par quel·s serveur·s vous pouvez **recevoir** les messages de vos contacts.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <target>Vous n'avez pas pu être vérifié·e ; veuillez réessayer.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> + <target>Vous choisissez qui peut se connecter.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You have already requested connection via this address!" xml:space="preserve"> <source>You have already requested connection via this address!</source> <target>Vous avez déjà demandé une connexion via cette adresse !</target> @@ -5752,11 +8789,6 @@ Repeat connection request?</source> Répéter la demande de connexion ?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> - <target>Vous n'avez aucune discussion</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> <target>Vous devez saisir la phrase secrète à chaque fois que l'application démarre - elle n'est pas stockée sur l'appareil.</target> @@ -5777,11 +8809,26 @@ Répéter la demande de connexion ?</target> <target>Vous avez rejoint ce groupe. Connexion à l'invitation d'un membre du groupe.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You may migrate the exported database." xml:space="preserve"> + <source>You may migrate the exported database.</source> + <target>Vous pouvez migrer la base de données exportée.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may save the exported archive." xml:space="preserve"> + <source>You may save the exported archive.</source> + <target>Vous pouvez enregistrer l'archive exportée.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> <target>Vous devez utiliser la version la plus récente de votre base de données de chat sur un seul appareil UNIQUEMENT, sinon vous risquez de ne plus recevoir les messages de certains contacts.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You need to allow your contact to call to be able to call them." xml:space="preserve"> + <source>You need to allow your contact to call to be able to call them.</source> + <target>Vous devez autoriser votre contact à appeler pour pouvoir l'appeler.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> <target>Vous devez autoriser votre contact à envoyer des messages vocaux pour pouvoir en envoyer.</target> @@ -5797,6 +8844,10 @@ Répéter la demande de connexion ?</target> <target>Vous avez envoyé une invitation de groupe</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You should receive notifications." xml:space="preserve"> + <source>You should receive notifications.</source> + <note>token info</note> + </trans-unit> <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> <target>Vous serez connecté·e au groupe lorsque l'appareil de l'hôte sera en ligne, veuillez attendre ou vérifier plus tard !</target> @@ -5832,6 +8883,11 @@ Répéter la demande de connexion ?</target> <target>Vous continuerez à recevoir des appels et des notifications des profils mis en sourdine lorsqu'ils sont actifs.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You will stop receiving messages from this chat. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this chat. Chat history will be preserved.</source> + <target>Vous ne recevrez plus de messages de cette discussion. L'historique sera préservé.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> <target>Vous ne recevrez plus de messages de ce groupe. L'historique du chat sera conservé.</target> @@ -5852,31 +8908,16 @@ Répéter la demande de connexion ?</target> <target>Vous utilisez un profil incognito pour ce groupe - pour éviter de partager votre profil principal ; inviter des contacts n'est pas possible</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your %@ servers" xml:space="preserve"> - <source>Your %@ servers</source> - <target>Vos serveurs %@</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your ICE servers" xml:space="preserve"> <source>Your ICE servers</source> <target>Vos serveurs ICE</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> - <target>Vos serveurs SMP</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your SimpleX address" xml:space="preserve"> <source>Your SimpleX address</source> <target>Votre adresse SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your XFTP servers" xml:space="preserve"> - <source>Your XFTP servers</source> - <target>Vos serveurs XFTP</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your calls" xml:space="preserve"> <source>Your calls</source> <target>Vos appels</target> @@ -5892,16 +8933,19 @@ Répéter la demande de connexion ?</target> <target>Votre base de données de chat n'est pas chiffrée - définisez une phrase secrète.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your chat preferences" xml:space="preserve"> + <source>Your chat preferences</source> + <target>Vos préférences de discussion</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>Vos profils de chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> - <target>Votre contact a besoin d'être en ligne pour completer la connexion. -Vous pouvez annuler la connexion et supprimer le contact (et réessayer plus tard avec un autre lien).</target> + <trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve"> + <source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source> + <target>Votre connexion a été déplacée vers %@ mais une erreur inattendue s'est produite lors de la redirection vers le profil.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> @@ -5914,18 +8958,16 @@ Vous pouvez annuler la connexion et supprimer le contact (et réessayer plus tar <target>Vos contacts peuvent autoriser la suppression complète des messages.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> - <source>Your contacts in SimpleX will see it. -You can change it in Settings.</source> - <target>Vos contacts dans SimpleX la verront. -Vous pouvez modifier ce choix dans les Paramètres.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your contacts will remain connected." xml:space="preserve"> <source>Your contacts will remain connected.</source> <target>Vos contacts resteront connectés.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve"> + <source>Your credentials may be sent unencrypted.</source> + <target>Vos informations d'identification peuvent être envoyées non chiffrées.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> <target>Votre base de données de chat actuelle va être SUPPRIMEE et REMPLACEE par celle importée.</target> @@ -5956,33 +8998,36 @@ Vous pouvez modifier ce choix dans les Paramètres.</target> <target>Votre profil **%@** sera partagé.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> - <target>Votre profil est stocké sur votre appareil et est seulement partagé avec vos contacts. -Les serveurs SimpleX ne peuvent pas voir votre profil.</target> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> + <target>Le profil n'est partagé qu'avec vos contacts.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> - <target>Votre profil, vos contacts et les messages reçus sont stockés sur votre appareil.</target> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> + <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> + <target>Votre profil est stocké sur votre appareil et est seulement partagé avec vos contacts. Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve"> + <source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source> + <target>Votre profil a été modifié. Si vous l'enregistrez, le profil mis à jour sera envoyé à tous vos contacts.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> <target>Votre profil aléatoire</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> - <target>Votre serveur</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> <target>Votre adresse de serveur</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your servers" xml:space="preserve"> + <source>Your servers</source> + <target>Vos serveurs</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your settings" xml:space="preserve"> <source>Your settings</source> <target>Vos paramètres</target> @@ -6023,21 +9068,36 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>appel accepté</target> <note>call status</note> </trans-unit> + <trans-unit id="accepted invitation" xml:space="preserve"> + <source>accepted invitation</source> + <target>invitation acceptée</target> + <note>chat list item title</note> + </trans-unit> <trans-unit id="admin" xml:space="preserve"> <source>admin</source> <target>admin</target> <note>member role</note> </trans-unit> + <trans-unit id="admins" xml:space="preserve"> + <source>admins</source> + <target>admins</target> + <note>feature role</note> + </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> - <target>accord sur le chiffrement pour %@…</target> + <target>négociation du chiffrement avec %@…</target> <note>chat item text</note> </trans-unit> <trans-unit id="agreeing encryption…" xml:space="preserve"> <source>agreeing encryption…</source> - <target>accord sur le chiffrement…</target> + <target>négociation du chiffrement…</target> <note>chat item text</note> </trans-unit> + <trans-unit id="all members" xml:space="preserve"> + <source>all members</source> + <target>tous les membres</target> + <note>feature role</note> + </trans-unit> <trans-unit id="always" xml:space="preserve"> <source>always</source> <target>toujours</target> @@ -6048,6 +9108,15 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>et %lld autres événements</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="archived report" xml:space="preserve"> + <source>archived report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="attempts" xml:space="preserve"> + <source>attempts</source> + <target>tentatives</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <source>audio call (not e2e encrypted)</source> <target>appel audio (sans chiffrement)</target> @@ -6071,13 +9140,29 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <trans-unit id="blocked" xml:space="preserve"> <source>blocked</source> <target>blocké</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="blocked %@" xml:space="preserve"> + <source>blocked %@</source> + <target>%@ bloqué</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="blocked by admin" xml:space="preserve"> + <source>blocked by admin</source> + <target>bloqué par l'administrateur</target> + <note>blocked chat item +marked deleted chat item preview text</note> </trans-unit> <trans-unit id="bold" xml:space="preserve"> <source>bold</source> <target>gras</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="call" xml:space="preserve"> + <source>call</source> + <target>appeler</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="call error" xml:space="preserve"> <source>call error</source> <target>erreur d'appel</target> @@ -6100,7 +9185,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> </trans-unit> <trans-unit id="changed address for you" xml:space="preserve"> <source>changed address for you</source> - <target>adresse modifiée pour vous</target> + <target>changement de l'adresse du contact</target> <note>chat item text</note> </trans-unit> <trans-unit id="changed role of %@ to %@" xml:space="preserve"> @@ -6181,7 +9266,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> <target>connexion…</target> - <note>chat list item title</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> @@ -6193,6 +9278,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>connexion : %@</target> <note>connection information</note> </trans-unit> + <trans-unit id="contact %@ changed to %@" xml:space="preserve"> + <source>contact %1$@ changed to %2$@</source> + <target>le contact %1$@ est devenu %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> <target>Ce contact a le chiffrement de bout en bout</target> @@ -6223,10 +9313,16 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>jours</target> <note>time unit</note> </trans-unit> + <trans-unit id="decryption errors" xml:space="preserve"> + <source>decryption errors</source> + <target>Erreurs de déchiffrement</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> <target>défaut (%@)</target> - <note>pref value</note> + <note>delete after time +pref value</note> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> @@ -6273,6 +9369,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>message dupliqué</target> <note>integrity error chat item</note> </trans-unit> + <trans-unit id="duplicates" xml:space="preserve"> + <source>duplicates</source> + <target>doublons</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> <target>chiffré de bout en bout</target> @@ -6348,9 +9449,14 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>erreur</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="event happened" xml:space="preserve"> - <source>event happened</source> - <target>event happened</target> + <trans-unit id="expired" xml:space="preserve"> + <source>expired</source> + <target>expiré</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="forwarded" xml:space="preserve"> + <source>forwarded</source> + <target>transféré</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> @@ -6378,6 +9484,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>La keychain d'iOS sera utilisée pour stocker en toute sécurité la phrase secrète après le redémarrage de l'app ou la modification de la phrase secrète - il permettra de recevoir les notifications push.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="inactive" xml:space="preserve"> + <source>inactive</source> + <target>inactif</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> <target>mode incognito via le lien d'adresse du contact</target> @@ -6418,6 +9529,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>invitation au groupe %@</target> <note>group name</note> </trans-unit> + <trans-unit id="invite" xml:space="preserve"> + <source>invite</source> + <target>inviter</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> <target>invité·e</target> @@ -6463,11 +9579,21 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>membre</target> <note>member role</note> </trans-unit> + <trans-unit id="member %@ changed to %@" xml:space="preserve"> + <source>member %1$@ changed to %2$@</source> + <target>le membre %1$@ est devenu %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> <target>est connecté·e</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="message" xml:space="preserve"> + <source>message</source> + <target>message</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="message received" xml:space="preserve"> <source>message received</source> <target>message reçu</target> @@ -6491,7 +9617,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <trans-unit id="moderated by %@" xml:space="preserve"> <source>moderated by %@</source> <target>modéré par %@</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="moderator" xml:space="preserve"> + <source>moderator</source> + <note>member role</note> </trans-unit> <trans-unit id="months" xml:space="preserve"> <source>months</source> @@ -6501,7 +9631,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <trans-unit id="never" xml:space="preserve"> <source>never</source> <target>jamais</target> - <note>No comment provided by engineer.</note> + <note>delete after time</note> </trans-unit> <trans-unit id="new message" xml:space="preserve"> <source>new message</source> @@ -6532,17 +9662,17 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <source>off</source> <target>off</target> <note>enabled status - group pref value - time to disappear</note> +group pref value +time to disappear</note> </trans-unit> <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> - <target>offert %@</target> + <target>propose %@</target> <note>feature offered item</note> </trans-unit> <trans-unit id="offered %@: %@" xml:space="preserve"> <source>offered %1$@: %2$@</source> - <target>offert %1$@ : %2$@</target> + <target>propose %1$@ : %2$@</target> <note>feature offered item</note> </trans-unit> <trans-unit id="on" xml:space="preserve"> @@ -6550,16 +9680,44 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>on</target> <note>group pref value</note> </trans-unit> + <trans-unit id="other" xml:space="preserve"> + <source>other</source> + <target>autre</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="other errors" xml:space="preserve"> + <source>other errors</source> + <target>autres erreurs</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="owner" xml:space="preserve"> <source>owner</source> <target>propriétaire</target> <note>member role</note> </trans-unit> + <trans-unit id="owners" xml:space="preserve"> + <source>owners</source> + <target>propriétaires</target> + <note>feature role</note> + </trans-unit> <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> <target>pair-à-pair</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="pending" xml:space="preserve"> + <source>pending</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="pending approval" xml:space="preserve"> + <source>pending approval</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="quantum resistant e2e encryption" xml:space="preserve"> + <source>quantum resistant e2e encryption</source> + <target>chiffrement e2e résistant post-quantique</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> <target>réponse reçu…</target> @@ -6570,6 +9728,10 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>confimation reçu…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="rejected" xml:space="preserve"> + <source>rejected</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> <target>appel rejeté</target> @@ -6585,11 +9747,41 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>a retiré %@</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="removed contact address" xml:space="preserve"> + <source>removed contact address</source> + <target>suppression de l'adresse de contact</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="removed profile picture" xml:space="preserve"> + <source>removed profile picture</source> + <target>suppression de la photo de profil</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> <target>vous a retiré</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="requested to connect" xml:space="preserve"> + <source>requested to connect</source> + <target>demande à se connecter</target> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="saved" xml:space="preserve"> + <source>saved</source> + <target>enregistré</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="saved from %@" xml:space="preserve"> + <source>saved from %@</source> + <target>enregistré à partir de %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="search" xml:space="preserve"> + <source>search</source> + <target>rechercher</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="sec" xml:space="preserve"> <source>sec</source> <target>sec</target> @@ -6615,6 +9807,30 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>envoyer un message direct</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve"> + <source>server queue info: %1$@ + +last received msg: %2$@</source> + <target>info sur la file d'attente du serveur : %1$@ + +dernier message reçu : %2$@</target> + <note>queue info</note> + </trans-unit> + <trans-unit id="set new contact address" xml:space="preserve"> + <source>set new contact address</source> + <target>a changé d'adresse de contact</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="set new profile picture" xml:space="preserve"> + <source>set new profile picture</source> + <target>a changé d'image de profil</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="standard end-to-end encryption" xml:space="preserve"> + <source>standard end-to-end encryption</source> + <target>chiffrement de bout en bout standard</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>lancement…</target> @@ -6630,16 +9846,41 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>ce contact</target> <note>notification title</note> </trans-unit> + <trans-unit id="unblocked %@" xml:space="preserve"> + <source>unblocked %@</source> + <target>%@ débloqué</target> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="unknown" xml:space="preserve"> <source>unknown</source> <target>inconnu</target> <note>connection info</note> </trans-unit> + <trans-unit id="unknown servers" xml:space="preserve"> + <source>unknown servers</source> + <target>relais inconnus</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unknown status" xml:space="preserve"> + <source>unknown status</source> + <target>statut inconnu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unprotected" xml:space="preserve"> + <source>unprotected</source> + <target>non protégé</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="updated group profile" xml:space="preserve"> <source>updated group profile</source> <target>mise à jour du profil de groupe</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="updated profile" xml:space="preserve"> + <source>updated profile</source> + <target>profil mis à jour</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="v%@" xml:space="preserve"> <source>v%@</source> <target>v%@</target> @@ -6670,6 +9911,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>via relais</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="video" xml:space="preserve"> + <source>video</source> + <target>vidéo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> <source>video call (not e2e encrypted)</source> <target>appel vidéo (sans chiffrement)</target> @@ -6695,11 +9941,21 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>semaines</target> <note>time unit</note> </trans-unit> + <trans-unit id="when IP hidden" xml:space="preserve"> + <source>when IP hidden</source> + <target>lorsque l'IP est masquée</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="yes" xml:space="preserve"> <source>yes</source> <target>oui</target> <note>pref value</note> </trans-unit> + <trans-unit id="you" xml:space="preserve"> + <source>you</source> + <target>vous</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> <target>vous êtes invité·e au groupe</target> @@ -6710,6 +9966,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>vous êtes observateur</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="you blocked %@" xml:space="preserve"> + <source>you blocked %@</source> + <target>vous avez bloqué %@</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> <target>vous avez changé d'adresse</target> @@ -6750,6 +10011,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>vous avez partagé un lien unique en incognito</target> <note>chat list item description</note> </trans-unit> + <trans-unit id="you unblocked %@" xml:space="preserve"> + <source>you unblocked %@</source> + <target>vous avez débloqué %@</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you: " xml:space="preserve"> <source>you: </source> <target>vous : </target> @@ -6764,7 +10030,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="fr" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6801,7 +10067,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="fr" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> @@ -6821,4 +10087,249 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> </trans-unit> </body> </file> + <file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="fr" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%d new events" xml:space="preserve"> + <source>%d new events</source> + <target>%d nouveaux événements</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="From %d chat(s)" xml:space="preserve"> + <source>From %d chat(s)</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From: %@" xml:space="preserve"> + <source>From: %@</source> + <target>De : %@</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>Nouveaux événements</target> + <note>notification</note> + </trans-unit> + <trans-unit id="New messages" xml:space="preserve"> + <source>New messages</source> + <target>Nouveaux messages</target> + <note>notification</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="fr" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle display name</note> + </trans-unit> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> + <source>Copyright © 2024 SimpleX Chat. All rights reserved.</source> + <target>Copyright © 2024 SimpleX Chat. Tous droits réservés.</target> + <note>Copyright (human-readable)</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="fr" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <target>%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App is locked!" xml:space="preserve"> + <source>App is locked!</source> + <target>L'app est verrouillée !</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <target>Annuler</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <target>Impossible d'accéder à la keychain pour enregistrer le mot de passe de la base de données</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Impossible de transférer le message</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Comment" xml:space="preserve"> + <source>Comment</source> + <target>Commenter</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <target>Actuellement, la taille maximale des fichiers supportés est de %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade required" xml:space="preserve"> + <source>Database downgrade required</source> + <target>Mise à jour de la base de données nécessaire</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <target>Base de données chiffrée !</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <target>Erreur de base de données</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <target>La phrase secrète de la base de données est différente de celle enregistrée dans la keychain.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <target>La phrase secrète de la base de données est nécessaire pour ouvrir le chat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade required" xml:space="preserve"> + <source>Database upgrade required</source> + <target>Mise à niveau de la base de données nécessaire</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing file" xml:space="preserve"> + <source>Error preparing file</source> + <target>Erreur lors de la préparation du fichier</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing message" xml:space="preserve"> + <source>Error preparing message</source> + <target>Erreur lors de la préparation du message</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <target>Erreur : %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Erreur de fichier</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <target>Version de la base de données incompatible</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Confirmation de migration invalide</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <target>Erreur de la keychain</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <target>Fichier trop lourd !</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No active profile" xml:space="preserve"> + <source>No active profile</source> + <target>Pas de profil actif</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <target>Ok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to downgrade the database." xml:space="preserve"> + <source>Open the app to downgrade the database.</source> + <target>Ouvrez l'app pour rétrograder la base de données.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to upgrade the database." xml:space="preserve"> + <source>Open the app to upgrade the database.</source> + <target>Ouvrez l'app pour mettre à jour la base de données.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passphrase" xml:space="preserve"> + <source>Passphrase</source> + <target>Phrase secrète</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve"> + <source>Please create a profile in the SimpleX app</source> + <target>Veuillez créer un profil dans l'app SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Les paramètres de chat sélectionnés ne permettent pas l'envoi de ce message.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending a message takes longer than expected." xml:space="preserve"> + <source>Sending a message takes longer than expected.</source> + <target>L'envoi d'un message prend plus de temps que prévu.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending message…" xml:space="preserve"> + <source>Sending message…</source> + <target>Envoi du message…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <target>Partager</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Slow network?" xml:space="preserve"> + <source>Slow network?</source> + <target>Réseau lent ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <target>Erreur inconnue de la base de données : %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unsupported format" xml:space="preserve"> + <source>Unsupported format</source> + <target>Format non pris en charge</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wait" xml:space="preserve"> + <source>Wait</source> + <target>Attendez</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <target>Mauvaise phrase secrète pour la base de données</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve"> + <source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source> + <target>Vous pouvez autoriser le partage dans les paramètres Confidentialité et sécurité / SimpleX Lock.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + </body> + </file> </xliff> diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/contents.json b/apps/ios/SimpleX Localizations/fr.xcloc/contents.json index 7df7c8ed26..d026c874ec 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/fr.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "fr", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "16.2" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff index ac71ed26bc..f76d7eba1e 100644 --- a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff +++ b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff @@ -217,23 +217,18 @@ Available in v5.1</source> <target state="translated">)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target state="translated">**הוסיפו איש קשר חדש**: ליצירת קוד QR או קישור חד־פעמיים עבור איש הקשר שלכם.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no"> <source>**Create link / QR code** for your contact to use.</source> <target state="translated">**צור קישור / קוד QR** לשימוש איש הקשר שלך.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve" approved="no"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> <target state="translated">**יותר פרטי**: בדוק הודעות חדשות כל 20 דקות. אסימון המכשיר משותף עם שרת SimpleX Chat, אך לא כמה אנשי קשר או הודעות יש לך.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve" approved="no"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> <target state="translated">**הכי פרטי**: אל תשתמש בשרת ההתראות של SimpleX Chat, בדוק הודעות מעת לעת ברקע (תלוי בתדירות השימוש באפליקציה).</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -247,8 +242,8 @@ Available in v5.1</source> <target state="translated">**שימו לב**: לא ניתן יהיה לשחזר או לשנות את הסיסמה אם תאבדו אותה.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve" approved="no"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> <target state="translated">**מומלץ**: אסימון מכשיר והתראות נשלחים לשרת ההתראות של SimpleX Chat, אך לא תוכן ההודעה, גודלה או ממי היא.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -403,9 +398,9 @@ Available in v5.1</source> <target state="translated">הוספת שרתים על ידי סריקת קוד QR.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve" approved="no"> - <source>Add server…</source> - <target state="translated">הוסף שרת…</target> + <trans-unit id="Add server" xml:space="preserve" approved="no"> + <source>Add server</source> + <target state="translated">הוסף שרת</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve" approved="no"> @@ -1391,8 +1386,8 @@ Available in v5.1</source> <target state="translated">הודעות ישירות</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve" approved="no"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve" approved="no"> + <source>Direct messages between members are prohibited.</source> <target state="translated">הודעות ישירות בין חברי קבוצה אסורות בקבוצה זו.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1411,8 +1406,8 @@ Available in v5.1</source> <target state="translated">הודעות נעלמות אסורות בצ׳אט זה.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve" approved="no"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve" approved="no"> + <source>Disappearing messages are prohibited.</source> <target state="translated">הודעות נעלמות אסורות בקבוצה זו.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1956,18 +1951,18 @@ Available in v5.1</source> <target state="translated">חברי הקבוצה יכולים למחוק באופן בלתי הפיך הודעות שנשלחו.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve" approved="no"> - <source>Group members can send direct messages.</source> + <trans-unit id="Members can send direct messages." xml:space="preserve" approved="no"> + <source>Members can send direct messages.</source> <target state="translated">חברי הקבוצה יכולים לשלוח הודעות ישירות.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve" approved="no"> - <source>Group members can send disappearing messages.</source> + <trans-unit id="Members can send disappearing messages." xml:space="preserve" approved="no"> + <source>Members can send disappearing messages.</source> <target state="translated">חברי הקבוצה יכולים לשלוח הודעות נעלמות.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve" approved="no"> - <source>Group members can send voice messages.</source> + <trans-unit id="Members can send voice messages." xml:space="preserve" approved="no"> + <source>Members can send voice messages.</source> <target state="translated">חברי הקבוצה יכולים לשלוח הודעות קוליות.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2115,8 +2110,8 @@ Available in v5.1</source> <target state="translated">מיד</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve" approved="no"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve" approved="no"> + <source>Immune to spam</source> <target state="translated">חסין מפני ספאם ושימוש לרעה</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2257,8 +2252,8 @@ Available in v5.1</source> <target state="translated">מחיקה בלתי הפיכה של הודעות אסורה בצ׳אט זה.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve" approved="no"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve" approved="no"> + <source>Irreversible message deletion is prohibited.</source> <target state="translated">מחיקה בלתי הפיכה של הודעות אסורה בקבוצה זו.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2502,9 +2497,9 @@ Available in v5.1</source> <target state="translated">ההעברה הושלמה</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve" approved="no"> - <source>Migrations: %@</source> - <target state="translated">העברות: %@</target> + <trans-unit id="Migrations:" xml:space="preserve" approved="no"> + <source>Migrations:</source> + <target state="translated">העברות:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve" approved="no"> @@ -2701,8 +2696,8 @@ Available in v5.1</source> <target state="translated">לא ייעשה שימוש במארחי Onion.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> @@ -2761,8 +2756,8 @@ Available in v5.1</source> <source>Open user profiles</source> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve"> @@ -2817,8 +2812,8 @@ Available in v5.1</source> <source>Paste the link you received into the box below to connect with your contact.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Periodically" xml:space="preserve"> @@ -3521,8 +3516,8 @@ Available in v5.1</source> <source>Thanks to the users – contribute via Weblate!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> @@ -3566,16 +3561,16 @@ It can happen because of some bug or when the connection is compromised.</source <source>The message will be marked as moderated for all members.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> <source>The old database was not removed during the migration, it can be deleted.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The sender will NOT be notified" xml:space="preserve"> @@ -3638,8 +3633,8 @@ It can happen because of some bug or when the connection is compromised.</source <source>To make a new connection</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -3864,8 +3859,8 @@ To connect, please ask your contact to create another connection link and check <source>Voice messages are prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> @@ -4005,10 +4000,6 @@ SimpleX Lock must be enabled.</source> <source>You can't send messages!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <note>No comment provided by engineer.</note> @@ -4967,8 +4958,8 @@ SimpleX servers cannot see your profile.</source> <target state="translated">נמחק</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Files and media are prohibited in this group." xml:space="preserve" approved="no"> - <source>Files and media are prohibited in this group.</source> + <trans-unit id="Files and media are prohibited." xml:space="preserve" approved="no"> + <source>Files and media are prohibited.</source> <target state="translated">קבצים ומדיה אסורים בקבוצה זו.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -5027,13 +5018,13 @@ SimpleX servers cannot see your profile.</source> <target state="translated">הזמן חברים</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve" approved="no"> - <source>Group members can add message reactions.</source> + <trans-unit id="Members can add message reactions." xml:space="preserve" approved="no"> + <source>Members can add message reactions.</source> <target state="translated">חברי הקבוצה יכולים להוסיף תגובות אמוג׳י להודעות.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send files and media." xml:space="preserve" approved="no"> - <source>Group members can send files and media.</source> + <trans-unit id="Members can send files and media." xml:space="preserve" approved="no"> + <source>Members can send files and media.</source> <target state="translated">חברי הקבוצה יכולים לשלוח קבצים ומדיה.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -5231,8 +5222,8 @@ SimpleX servers cannot see your profile.</source> <target state="translated">תגובות אמוג׳י להודעות אסורות בצ׳אט זה.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve" approved="no"> - <source>Message reactions are prohibited in this group.</source> + <trans-unit id="Message reactions are prohibited." xml:space="preserve" approved="no"> + <source>Message reactions are prohibited.</source> <target state="translated">תגובות אמוג׳י להודעות אסורות בקבוצה זו.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -5311,6 +5302,283 @@ SimpleX servers cannot see your profile.</source> <target state="translated">ללא היסטוריה</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ and %@" xml:space="preserve" approved="no"> + <source>%@ and %@</source> + <target state="translated">%@ ו-%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect automatically" xml:space="preserve" approved="no"> + <source>Connect automatically</source> + <target state="translated">התבר אוטומטי</target> + </trans-unit> + <trans-unit id="Create profile" xml:space="preserve" approved="no"> + <source>Create profile</source> + <target state="translated">צור פרופיל</target> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve" approved="no"> + <source>Created at: %@</source> + <target state="translated">נוצר ב:%@</target> + </trans-unit> + <trans-unit id="Desktop devices" xml:space="preserve" approved="no"> + <source>Desktop devices</source> + <target state="translated">מכשירי מחשב</target> + </trans-unit> + <trans-unit id="Discover via local network" xml:space="preserve" approved="no"> + <source>Discover via local network</source> + <target state="translated">גלה באמצעות הרשת המקומית</target> + </trans-unit> + <trans-unit id="Forward" xml:space="preserve" approved="no"> + <source>Forward</source> + <target state="translated">העבר</target> + </trans-unit> + <trans-unit id="Group already exists" xml:space="preserve" approved="no"> + <source>Group already exists</source> + <target state="translated">קבוצה כבר קיימת</target> + </trans-unit> + <trans-unit id="Connected to desktop" xml:space="preserve" approved="no"> + <source>Connected to desktop</source> + <target state="translated">מחובר למחשב</target> + </trans-unit> + <trans-unit id="Group already exists!" xml:space="preserve" approved="no"> + <source>Group already exists!</source> + <target state="translated">קבוצה כבר קיימת!</target> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve" approved="no"> + <source>Confirm upload</source> + <target state="translated">אשר ההעלאה</target> + </trans-unit> + <trans-unit id="Block for all" xml:space="preserve" approved="no"> + <source>Block for all</source> + <target state="translated">חסום לכולם</target> + </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve" approved="no"> + <source>Blocked by admin</source> + <target state="translated">נחסם ע"י מנהל</target> + </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve" approved="no"> + <source>Block member for all?</source> + <target state="translated">לחסום את החבר לכולם?</target> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve" approved="no"> + <source>Camera not available</source> + <target state="translated">מצלמה לא זמינה</target> + </trans-unit> + <trans-unit id="Connect to desktop" xml:space="preserve" approved="no"> + <source>Connect to desktop</source> + <target state="translated">חבר למחשב</target> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve" approved="no"> + <source>Created at</source> + <target state="translated">נוצר ב</target> + </trans-unit> + <trans-unit id="(new)" xml:space="preserve" approved="no"> + <source>(new)</source> + <target state="translated">(חדש)</target> + </trans-unit> + <trans-unit id="Block member" xml:space="preserve" approved="no"> + <source>Block member</source> + <target state="translated">חבר חסום</target> + </trans-unit> + <trans-unit id="Block member?" xml:space="preserve" approved="no"> + <source>Block member?</source> + <target state="translated">לחסום חבר?</target> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve" approved="no"> + <source>Creating link…</source> + <target state="translated">יוצר קישור…</target> + </trans-unit> + <trans-unit id="Files" xml:space="preserve" approved="no"> + <source>Files</source> + <target state="translated">קבצים</target> + </trans-unit> + <trans-unit id="Disabled" xml:space="preserve" approved="no"> + <source>Disabled</source> + <target state="translated">מושבת</target> + </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve" approved="no"> + <source>Enter passphrase</source> + <target state="translated">הכנס סיסמא</target> + </trans-unit> + <trans-unit id="Apply" xml:space="preserve" approved="no"> + <source>Apply</source> + <target state="translated">החל</target> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve" approved="no"> + <source>Apply to</source> + <target state="translated">החל ל</target> + </trans-unit> + <trans-unit id="Background" xml:space="preserve" approved="no"> + <source>Background</source> + <target state="translated">ברקע</target> + </trans-unit> + <trans-unit id="Black" xml:space="preserve" approved="no"> + <source>Black</source> + <target state="translated">שחור</target> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve" approved="no"> + <source>Blur media</source> + <target state="translated">טשטש מדיה</target> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve" approved="no"> + <source>Chat theme</source> + <target state="translated">צבע ערכת נושא</target> + </trans-unit> + <trans-unit id="Completed" xml:space="preserve" approved="no"> + <source>Completed</source> + <target state="translated">הושלם</target> + </trans-unit> + <trans-unit id="Connected" xml:space="preserve" approved="no"> + <source>Connected</source> + <target state="translated">מחובר</target> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve" approved="no"> + <source>Connection notifications</source> + <target state="translated">התראות חיבור</target> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve" approved="no"> + <source>Connections</source> + <target state="translated">חיבורים</target> + </trans-unit> + <trans-unit id="Current profile" xml:space="preserve" approved="no"> + <source>Current profile</source> + <target state="translated">פרופיל נוכחי</target> + </trans-unit> + <trans-unit id="Disconnect desktop?" xml:space="preserve" approved="no"> + <source>Disconnect desktop?</source> + <target state="translated">להתנתק מהמחשב?</target> + </trans-unit> + <trans-unit id="Discover and join groups" xml:space="preserve" approved="no"> + <source>Discover and join groups</source> + <target state="translated">גלה והצטרף לקבוצות</target> + </trans-unit> + <trans-unit id="Enabled" xml:space="preserve" approved="no"> + <source>Enabled</source> + <target state="translated">מופעל</target> + </trans-unit> + <trans-unit id="Error opening chat" xml:space="preserve" approved="no"> + <source>Error opening chat</source> + <target state="translated">שגיאה בפתיחת הצ'אט</target> + </trans-unit> + <trans-unit id="Good morning!" xml:space="preserve" approved="no"> + <source>Good morning!</source> + <target state="translated">בוקר טוב!</target> + </trans-unit> + <trans-unit id="Connect to yourself? This is your own SimpleX address!" xml:space="preserve" approved="no"> + <source>Connect to yourself? +This is your own SimpleX address!</source> + <target state="translated">להתחבר אליך? +זו כתובת הSimpleX שלך!</target> + </trans-unit> + <trans-unit id="Connect to yourself?" xml:space="preserve" approved="no"> + <source>Connect to yourself?</source> + <target state="translated">להתחבר אליך?</target> + </trans-unit> + <trans-unit id="Connect to yourself? This is your own one-time link!" xml:space="preserve" approved="no"> + <source>Connect to yourself? +This is your own one-time link!</source> + <target state="translated">להתחבר אליך? +זו כתובת ההזמנה החד-פעמי שלך!</target> + </trans-unit> + <trans-unit id="Connected desktop" xml:space="preserve" approved="no"> + <source>Connected desktop</source> + <target state="translated">מחשב מחובר</target> + </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve" approved="no"> + <source>Connected servers</source> + <target state="translated">שרתים מחוברים</target> + </trans-unit> + <trans-unit id="Enter group name…" xml:space="preserve" approved="no"> + <source>Enter group name…</source> + <target state="translated">הכנס שם לקבוצה…</target> + </trans-unit> + <trans-unit id="Enter this device name…" xml:space="preserve" approved="no"> + <source>Enter this device name…</source> + <target state="translated">הכנס שם למכשיר הזה…</target> + </trans-unit> + <trans-unit id="Enter your name…" xml:space="preserve" approved="no"> + <source>Enter your name…</source> + <target state="translated">הכנס את השם שלך…</target> + </trans-unit> + <trans-unit id="Error decrypting file" xml:space="preserve" approved="no"> + <source>Error decrypting file</source> + <target state="translated">שגיאה בפענוח הקובץ</target> + </trans-unit> + <trans-unit id="Errors" xml:space="preserve" approved="no"> + <source>Errors</source> + <target state="translated">שגיאות</target> + </trans-unit> + <trans-unit id="File status" xml:space="preserve" approved="no"> + <source>File status</source> + <target state="translated">מצב הקובץ</target> + </trans-unit> + <trans-unit id="Connecting" xml:space="preserve" approved="no"> + <source>Connecting</source> + <target state="translated">מתחבר</target> + </trans-unit> + <trans-unit id="Connecting to desktop" xml:space="preserve" approved="no"> + <source>Connecting to desktop</source> + <target state="translated">מתחבר למחשב</target> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve" approved="no"> + <source>Deleted</source> + <target state="translated">נמחק</target> + </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve" approved="no"> + <source>Deletion errors</source> + <target state="translated">שגיאות במחיקה</target> + </trans-unit> + <trans-unit id="Details" xml:space="preserve" approved="no"> + <source>Details</source> + <target state="translated">פרטים</target> + </trans-unit> + <trans-unit id="Forwarded" xml:space="preserve" approved="no"> + <source>Forwarded</source> + <target state="translated">הועבר</target> + </trans-unit> + <trans-unit id="Found desktop" xml:space="preserve" approved="no"> + <source>Found desktop</source> + <target state="translated">נמצא מחשב</target> + </trans-unit> + <trans-unit id="Good afternoon!" xml:space="preserve" approved="no"> + <source>Good afternoon!</source> + <target state="translated">אחר צהריים טובים!</target> + </trans-unit> + <trans-unit id="Desktop address" xml:space="preserve" approved="no"> + <source>Desktop address</source> + <target state="translated">כתובת מחשב</target> + </trans-unit> + <trans-unit id="Forwarded from" xml:space="preserve" approved="no"> + <source>Forwarded from</source> + <target state="translated">הועבר מ</target> + </trans-unit> + <trans-unit id="History is not sent to new members." xml:space="preserve" approved="no"> + <source>History is not sent to new members.</source> + <target state="translated">היסטוריה לא נשלחת לחברים חדשים.</target> + </trans-unit> + <trans-unit id="Created" xml:space="preserve" approved="no"> + <source>Created</source> + <target state="translated">נוצר</target> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve" approved="no"> + <source>Copy error</source> + <target state="translated">שגיאת העתקה</target> + </trans-unit> + <trans-unit id="Create group" xml:space="preserve" approved="no"> + <source>Create group</source> + <target state="translated">צור קבוצה</target> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve" approved="no"> + <source>Enabled for</source> + <target state="translated">מופעל עבור</target> + </trans-unit> + <trans-unit id="Error creating message" xml:space="preserve" approved="no"> + <source>Error creating message</source> + <target state="translated">שגיאה ביצירת הודעה</target> + </trans-unit> + <trans-unit id="File error" xml:space="preserve" approved="no"> + <source>File error</source> + <target state="translated">שגיאה בקובץ</target> + </trans-unit> </body> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="he" datatype="plaintext"> diff --git a/apps/ios/SimpleX Localizations/hi.xcloc/Localized Contents/hi.xliff b/apps/ios/SimpleX Localizations/hi.xcloc/Localized Contents/hi.xliff deleted file mode 100644 index 31746eccd9..0000000000 --- a/apps/ios/SimpleX Localizations/hi.xcloc/Localized Contents/hi.xliff +++ /dev/null @@ -1,3554 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> - <file original="en.lproj/Localizable.strings" source-language="en" target-language="hi" datatype="plaintext"> - <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.0" build-num="14A309"/> - </header> - <body> - <trans-unit id=" " xml:space="preserve"> - <source> -</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (" xml:space="preserve"> - <source> (</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (can be copied)" xml:space="preserve"> - <source> (can be copied)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="!1 colored!" xml:space="preserve"> - <source>!1 colored!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="#secret#" xml:space="preserve"> - <source>#secret#</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@" xml:space="preserve"> - <source>%@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ %@" xml:space="preserve"> - <source>%@ %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ / %@" xml:space="preserve"> - <source>%@ / %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ is connected!" xml:space="preserve"> - <source>%@ is connected!</source> - <note>notification title</note> - </trans-unit> - <trans-unit id="%@ is not verified" xml:space="preserve"> - <source>%@ is not verified</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ is verified" xml:space="preserve"> - <source>%@ is verified</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ wants to connect!" xml:space="preserve"> - <source>%@ wants to connect!</source> - <note>notification title</note> - </trans-unit> - <trans-unit id="%d days" xml:space="preserve"> - <source>%d days</source> - <note>message ttl</note> - </trans-unit> - <trans-unit id="%d hours" xml:space="preserve"> - <source>%d hours</source> - <note>message ttl</note> - </trans-unit> - <trans-unit id="%d min" xml:space="preserve"> - <source>%d min</source> - <note>message ttl</note> - </trans-unit> - <trans-unit id="%d months" xml:space="preserve"> - <source>%d months</source> - <note>message ttl</note> - </trans-unit> - <trans-unit id="%d sec" xml:space="preserve"> - <source>%d sec</source> - <note>message ttl</note> - </trans-unit> - <trans-unit id="%d skipped message(s)" xml:space="preserve"> - <source>%d skipped message(s)</source> - <note>integrity error chat item</note> - </trans-unit> - <trans-unit id="%lld" xml:space="preserve"> - <source>%lld</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld %@" xml:space="preserve"> - <source>%lld %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld contact(s) selected" xml:space="preserve"> - <source>%lld contact(s) selected</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve"> - <source>%lld file(s) with total size of %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld members" xml:space="preserve"> - <source>%lld members</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> - <source>%lld second(s)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lldd" xml:space="preserve"> - <source>%lldd</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lldh" xml:space="preserve"> - <source>%lldh</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lldk" xml:space="preserve"> - <source>%lldk</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lldm" xml:space="preserve"> - <source>%lldm</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lldmth" xml:space="preserve"> - <source>%lldmth</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%llds" xml:space="preserve"> - <source>%llds</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lldw" xml:space="preserve"> - <source>%lldw</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="(" xml:space="preserve"> - <source>(</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=")" xml:space="preserve"> - <source>)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> - <source>**Create link / QR code** for your contact to use.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> - <source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> - <source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> - <source>**e2e encrypted** audio call</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**e2e encrypted** video call" xml:space="preserve"> - <source>**e2e encrypted** video call</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="*bold*" xml:space="preserve"> - <source>\*bold*</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=", " xml:space="preserve"> - <source>, </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="." xml:space="preserve"> - <source>.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="1 day" xml:space="preserve"> - <source>1 day</source> - <note>message ttl</note> - </trans-unit> - <trans-unit id="1 hour" xml:space="preserve"> - <source>1 hour</source> - <note>message ttl</note> - </trans-unit> - <trans-unit id="1 month" xml:space="preserve"> - <source>1 month</source> - <note>message ttl</note> - </trans-unit> - <trans-unit id="1 week" xml:space="preserve"> - <source>1 week</source> - <note>message ttl</note> - </trans-unit> - <trans-unit id="2 weeks" xml:space="preserve"> - <source>2 weeks</source> - <note>message ttl</note> - </trans-unit> - <trans-unit id="6" xml:space="preserve"> - <source>6</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=": " xml:space="preserve"> - <source>: </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A new contact" xml:space="preserve"> - <source>A new contact</source> - <note>notification title</note> - </trans-unit> - <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>A random profile will be sent to the contact that you received this link from</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A random profile will be sent to your contact" xml:space="preserve"> - <source>A random profile will be sent to your contact</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> - <source>A separate TCP connection will be used **for each chat profile you have in the app**.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." xml:space="preserve"> - <source>A separate TCP connection will be used **for each contact and group member**. -**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve"> - <source>About SimpleX</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="About SimpleX Chat" xml:space="preserve"> - <source>About SimpleX Chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Accent color" xml:space="preserve"> - <source>Accent color</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Accept" xml:space="preserve"> - <source>Accept</source> - <note>accept contact request via notification - accept incoming call via notification</note> - </trans-unit> - <trans-unit id="Accept contact" xml:space="preserve"> - <source>Accept contact</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Accept contact request from %@?" xml:space="preserve"> - <source>Accept contact request from %@?</source> - <note>notification body</note> - </trans-unit> - <trans-unit id="Accept incognito" xml:space="preserve"> - <source>Accept incognito</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Accept requests" xml:space="preserve"> - <source>Accept requests</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> - <source>Add preset servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Add profile" xml:space="preserve"> - <source>Add profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> - <source>Add servers by scanning QR codes.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Add to another device" xml:space="preserve"> - <source>Add to another device</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> - <source>Admins can create the links to join groups.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Advanced network settings" xml:space="preserve"> - <source>Advanced network settings</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve"> - <source>All chats and messages will be deleted - this cannot be undone!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="All group members will remain connected." xml:space="preserve"> - <source>All group members will remain connected.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> - <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="All your contacts will remain connected" xml:space="preserve"> - <source>All your contacts will remain connected</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow" xml:space="preserve"> - <source>Allow</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> - <source>Allow disappearing messages only if your contact allows it to you.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow sending direct messages to members." xml:space="preserve"> - <source>Allow sending direct messages to members.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow sending disappearing messages." xml:space="preserve"> - <source>Allow sending disappearing messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow to irreversibly delete sent messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow to send voice messages." xml:space="preserve"> - <source>Allow to send voice messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve"> - <source>Allow voice messages only if your contact allows them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow voice messages?" xml:space="preserve"> - <source>Allow voice messages?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow your contacts to irreversibly delete sent messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> - <source>Allow your contacts to send disappearing messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow your contacts to send voice messages." xml:space="preserve"> - <source>Allow your contacts to send voice messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Already connected?" xml:space="preserve"> - <source>Already connected?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Answer call" xml:space="preserve"> - <source>Answer call</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="App build: %@" xml:space="preserve"> - <source>App build: %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="App icon" xml:space="preserve"> - <source>App icon</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="App version" xml:space="preserve"> - <source>App version</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="App version: v%@" xml:space="preserve"> - <source>App version: v%@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Appearance" xml:space="preserve"> - <source>Appearance</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Attach" xml:space="preserve"> - <source>Attach</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Audio & video calls" xml:space="preserve"> - <source>Audio & video calls</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Authentication failed" xml:space="preserve"> - <source>Authentication failed</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Authentication unavailable" xml:space="preserve"> - <source>Authentication unavailable</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Auto-accept contact requests" xml:space="preserve"> - <source>Auto-accept contact requests</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Auto-accept images" xml:space="preserve"> - <source>Auto-accept images</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Automatically" xml:space="preserve"> - <source>Automatically</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Back" xml:space="preserve"> - <source>Back</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> - <source>Both you and your contact can irreversibly delete sent messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Both you and your contact can send disappearing messages." xml:space="preserve"> - <source>Both you and your contact can send disappearing messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Both you and your contact can send voice messages." xml:space="preserve"> - <source>Both you and your contact can send voice messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Call already ended!" xml:space="preserve"> - <source>Call already ended!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Calls" xml:space="preserve"> - <source>Calls</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Can't invite contact!" xml:space="preserve"> - <source>Can't invite contact!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Can't invite contacts!" xml:space="preserve"> - <source>Can't invite contacts!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Cancel" xml:space="preserve"> - <source>Cancel</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> - <source>Cannot access keychain to save database password</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Cannot receive file" xml:space="preserve"> - <source>Cannot receive file</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change" xml:space="preserve"> - <source>Change</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change database passphrase?" xml:space="preserve"> - <source>Change database passphrase?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change member role?" xml:space="preserve"> - <source>Change member role?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change receiving address" xml:space="preserve"> - <source>Change receiving address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change receiving address?" xml:space="preserve"> - <source>Change receiving address?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change role" xml:space="preserve"> - <source>Change role</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> - <source>Chat archive</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat console" xml:space="preserve"> - <source>Chat console</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat database" xml:space="preserve"> - <source>Chat database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat database deleted" xml:space="preserve"> - <source>Chat database deleted</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat database imported" xml:space="preserve"> - <source>Chat database imported</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat is running" xml:space="preserve"> - <source>Chat is running</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat is stopped" xml:space="preserve"> - <source>Chat is stopped</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat preferences" xml:space="preserve"> - <source>Chat preferences</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chats" xml:space="preserve"> - <source>Chats</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Check server address and try again." xml:space="preserve"> - <source>Check server address and try again.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Choose file" xml:space="preserve"> - <source>Choose file</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Choose from library" xml:space="preserve"> - <source>Choose from library</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Clear" xml:space="preserve"> - <source>Clear</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Clear conversation" xml:space="preserve"> - <source>Clear conversation</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Clear conversation?" xml:space="preserve"> - <source>Clear conversation?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Clear verification" xml:space="preserve"> - <source>Clear verification</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> - <source>Colors</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Compare security codes with your contacts." xml:space="preserve"> - <source>Compare security codes with your contacts.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Configure ICE servers" xml:space="preserve"> - <source>Configure ICE servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Confirm" xml:space="preserve"> - <source>Confirm</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Confirm new passphrase…" xml:space="preserve"> - <source>Confirm new passphrase…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connect" xml:space="preserve"> - <source>Connect</source> - <note>server test step</note> - </trans-unit> - <trans-unit id="Connect via contact link?" xml:space="preserve"> - <source>Connect via contact link?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connect via group link?" xml:space="preserve"> - <source>Connect via group link?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connect via link" xml:space="preserve"> - <source>Connect via link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve"> - <source>Connect via link / QR code</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connect via one-time link?" xml:space="preserve"> - <source>Connect via one-time link?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connect via relay" xml:space="preserve"> - <source>Connect via relay</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connecting server…" xml:space="preserve"> - <source>Connecting to server…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connecting server… (error: %@)" xml:space="preserve"> - <source>Connecting to server… (error: %@)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection" xml:space="preserve"> - <source>Connection</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection error" xml:space="preserve"> - <source>Connection error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection error (AUTH)" xml:space="preserve"> - <source>Connection error (AUTH)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection request" xml:space="preserve"> - <source>Connection request</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection request sent!" xml:space="preserve"> - <source>Connection request sent!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection timeout" xml:space="preserve"> - <source>Connection timeout</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact allows" xml:space="preserve"> - <source>Contact allows</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact already exists" xml:space="preserve"> - <source>Contact already exists</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact and all messages will be deleted - this cannot be undone!" xml:space="preserve"> - <source>Contact and all messages will be deleted - this cannot be undone!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact hidden:" xml:space="preserve"> - <source>Contact hidden:</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Contact is connected" xml:space="preserve"> - <source>Contact is connected</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve"> - <source>Contact is not connected yet!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact name" xml:space="preserve"> - <source>Contact name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact preferences" xml:space="preserve"> - <source>Contact preferences</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact requests" xml:space="preserve"> - <source>Contact requests</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve"> - <source>Contacts can mark messages for deletion; you will be able to view them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Copy" xml:space="preserve"> - <source>Copy</source> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Core built at: %@" xml:space="preserve"> - <source>Core built at: %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Core version: v%@" xml:space="preserve"> - <source>Core version: v%@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create" xml:space="preserve"> - <source>Create</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create address" xml:space="preserve"> - <source>Create address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create group link" xml:space="preserve"> - <source>Create group link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create link" xml:space="preserve"> - <source>Create link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve"> - <source>Create one-time invitation link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create queue" xml:space="preserve"> - <source>Create queue</source> - <note>server test step</note> - </trans-unit> - <trans-unit id="Create secret group" xml:space="preserve"> - <source>Create secret group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create your profile" xml:space="preserve"> - <source>Create your profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve"> - <source>Created on %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Current passphrase…" xml:space="preserve"> - <source>Current passphrase…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> - <source>Currently maximum supported file size is %@.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Dark" xml:space="preserve"> - <source>Dark</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Data" xml:space="preserve"> - <source>Data</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database ID" xml:space="preserve"> - <source>Database ID</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database encrypted!" xml:space="preserve"> - <source>Database encrypted!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database encryption passphrase will be updated and stored in the keychain. " xml:space="preserve"> - <source>Database encryption passphrase will be updated and stored in the keychain. -</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database encryption passphrase will be updated. " xml:space="preserve"> - <source>Database encryption passphrase will be updated. -</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database error" xml:space="preserve"> - <source>Database error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database is encrypted using a random passphrase, you can change it." xml:space="preserve"> - <source>Database is encrypted using a random passphrase, you can change it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database is encrypted using a random passphrase. Please change it before exporting." xml:space="preserve"> - <source>Database is encrypted using a random passphrase. Please change it before exporting.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database passphrase" xml:space="preserve"> - <source>Database passphrase</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database passphrase & export" xml:space="preserve"> - <source>Database passphrase & export</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> - <source>Database passphrase is different from saved in the keychain.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> - <source>Database passphrase is required to open chat.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database will be encrypted and the passphrase stored in the keychain. " xml:space="preserve"> - <source>Database will be encrypted and the passphrase stored in the keychain. -</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database will be encrypted. " xml:space="preserve"> - <source>Database will be encrypted. -</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database will be migrated when the app restarts" xml:space="preserve"> - <source>Database will be migrated when the app restarts</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Decentralized" xml:space="preserve"> - <source>Decentralized</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete" xml:space="preserve"> - <source>Delete</source> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve"> - <source>Delete Contact</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete address" xml:space="preserve"> - <source>Delete address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete address?" xml:space="preserve"> - <source>Delete address?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete after" xml:space="preserve"> - <source>Delete after</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete all files" xml:space="preserve"> - <source>Delete all files</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve"> - <source>Delete archive</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve"> - <source>Delete chat archive?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete chat profile?" xml:space="preserve"> - <source>Delete chat profile?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete connection" xml:space="preserve"> - <source>Delete connection</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete contact" xml:space="preserve"> - <source>Delete contact</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete contact?" xml:space="preserve"> - <source>Delete contact?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete database" xml:space="preserve"> - <source>Delete database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete files & media" xml:space="preserve"> - <source>Delete files & media</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete files and media?" xml:space="preserve"> - <source>Delete files and media?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete files for all chat profiles" xml:space="preserve"> - <source>Delete files for all chat profiles</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete for everyone" xml:space="preserve"> - <source>Delete for everyone</source> - <note>chat feature</note> - </trans-unit> - <trans-unit id="Delete for me" xml:space="preserve"> - <source>Delete for me</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete group" xml:space="preserve"> - <source>Delete group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete group?" xml:space="preserve"> - <source>Delete group?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete invitation" xml:space="preserve"> - <source>Delete invitation</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete link" xml:space="preserve"> - <source>Delete link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete link?" xml:space="preserve"> - <source>Delete link?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete message?" xml:space="preserve"> - <source>Delete message?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete messages" xml:space="preserve"> - <source>Delete messages</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete messages after" xml:space="preserve"> - <source>Delete messages after</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete old database" xml:space="preserve"> - <source>Delete old database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete old database?" xml:space="preserve"> - <source>Delete old database?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve"> - <source>Delete pending connection</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete pending connection?" xml:space="preserve"> - <source>Delete pending connection?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete queue" xml:space="preserve"> - <source>Delete queue</source> - <note>server test step</note> - </trans-unit> - <trans-unit id="Delete user profile?" xml:space="preserve"> - <source>Delete user profile?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Description" xml:space="preserve"> - <source>Description</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Develop" xml:space="preserve"> - <source>Develop</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Developer tools" xml:space="preserve"> - <source>Developer tools</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Device" xml:space="preserve"> - <source>Device</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Device authentication is disabled. Turning off SimpleX Lock." xml:space="preserve"> - <source>Device authentication is disabled. Turning off SimpleX Lock.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." xml:space="preserve"> - <source>Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Direct messages" xml:space="preserve"> - <source>Direct messages</source> - <note>chat feature</note> - </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> - <source>Disable SimpleX Lock</source> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Disappearing messages" xml:space="preserve"> - <source>Disappearing messages</source> - <note>chat feature</note> - </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this chat." xml:space="preserve"> - <source>Disappearing messages are prohibited in this chat.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Disconnect" xml:space="preserve"> - <source>Disconnect</source> - <note>server test step</note> - </trans-unit> - <trans-unit id="Display name" xml:space="preserve"> - <source>Display name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Display name:" xml:space="preserve"> - <source>Display name:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> - <source>Do NOT use SimpleX for emergency calls.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Do it later" xml:space="preserve"> - <source>Do it later</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Edit" xml:space="preserve"> - <source>Edit</source> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Edit group profile" xml:space="preserve"> - <source>Edit group profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable" xml:space="preserve"> - <source>Enable</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> - <source>Enable SimpleX Lock</source> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Enable TCP keep-alive" xml:space="preserve"> - <source>Enable TCP keep-alive</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> - <source>Enable automatic message deletion?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable instant notifications?" xml:space="preserve"> - <source>Enable instant notifications?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable notifications" xml:space="preserve"> - <source>Enable notifications</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable periodic notifications?" xml:space="preserve"> - <source>Enable periodic notifications?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Encrypt" xml:space="preserve"> - <source>Encrypt</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Encrypt database?" xml:space="preserve"> - <source>Encrypt database?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Encrypted database" xml:space="preserve"> - <source>Encrypted database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Encrypted message or another event" xml:space="preserve"> - <source>Encrypted message or another event</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Encrypted message: database error" xml:space="preserve"> - <source>Encrypted message: database error</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Encrypted message: keychain error" xml:space="preserve"> - <source>Encrypted message: keychain error</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Encrypted message: no passphrase" xml:space="preserve"> - <source>Encrypted message: no passphrase</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Encrypted message: unexpected error" xml:space="preserve"> - <source>Encrypted message: unexpected error</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Enter correct passphrase." xml:space="preserve"> - <source>Enter correct passphrase.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enter passphrase…" xml:space="preserve"> - <source>Enter passphrase…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enter server manually" xml:space="preserve"> - <source>Enter server manually</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error" xml:space="preserve"> - <source>Error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error accepting contact request" xml:space="preserve"> - <source>Error accepting contact request</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve"> - <source>Error accessing database file</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error adding member(s)" xml:space="preserve"> - <source>Error adding member(s)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error changing address" xml:space="preserve"> - <source>Error changing address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error changing role" xml:space="preserve"> - <source>Error changing role</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error changing setting" xml:space="preserve"> - <source>Error changing setting</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error creating address" xml:space="preserve"> - <source>Error creating address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error creating group" xml:space="preserve"> - <source>Error creating group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error creating group link" xml:space="preserve"> - <source>Error creating group link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting chat database" xml:space="preserve"> - <source>Error deleting chat database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting chat!" xml:space="preserve"> - <source>Error deleting chat!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting connection" xml:space="preserve"> - <source>Error deleting connection</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve"> - <source>Error deleting contact</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting database" xml:space="preserve"> - <source>Error deleting database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting old database" xml:space="preserve"> - <source>Error deleting old database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting token" xml:space="preserve"> - <source>Error deleting token</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting user profile" xml:space="preserve"> - <source>Error deleting user profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error enabling notifications" xml:space="preserve"> - <source>Error enabling notifications</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error encrypting database" xml:space="preserve"> - <source>Error encrypting database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error exporting chat database" xml:space="preserve"> - <source>Error exporting chat database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error importing chat database" xml:space="preserve"> - <source>Error importing chat database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error joining group" xml:space="preserve"> - <source>Error joining group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error receiving file" xml:space="preserve"> - <source>Error receiving file</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error removing member" xml:space="preserve"> - <source>Error removing member</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error saving ICE servers" xml:space="preserve"> - <source>Error saving ICE servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error saving SMP servers" xml:space="preserve"> - <source>Error saving SMP servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error saving group profile" xml:space="preserve"> - <source>Error saving group profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error saving passphrase to keychain" xml:space="preserve"> - <source>Error saving passphrase to keychain</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error sending message" xml:space="preserve"> - <source>Error sending message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error starting chat" xml:space="preserve"> - <source>Error starting chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error stopping chat" xml:space="preserve"> - <source>Error stopping chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error updating message" xml:space="preserve"> - <source>Error updating message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error updating settings" xml:space="preserve"> - <source>Error updating settings</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error: %@" xml:space="preserve"> - <source>Error: %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error: URL is invalid" xml:space="preserve"> - <source>Error: URL is invalid</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error: no database file" xml:space="preserve"> - <source>Error: no database file</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Exit without saving" xml:space="preserve"> - <source>Exit without saving</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Export database" xml:space="preserve"> - <source>Export database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Export error:" xml:space="preserve"> - <source>Export error:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Exported database archive." xml:space="preserve"> - <source>Exported database archive.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Exporting database archive..." xml:space="preserve"> - <source>Exporting database archive...</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Failed to remove passphrase" xml:space="preserve"> - <source>Failed to remove passphrase</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="File will be received when your contact is online, please wait or check later!" xml:space="preserve"> - <source>File will be received when your contact is online, please wait or check later!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="File: %@" xml:space="preserve"> - <source>File: %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Files & media" xml:space="preserve"> - <source>Files & media</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="For console" xml:space="preserve"> - <source>For console</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Full link" xml:space="preserve"> - <source>Full link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Full name (optional)" xml:space="preserve"> - <source>Full name (optional)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Full name:" xml:space="preserve"> - <source>Full name:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="GIFs and stickers" xml:space="preserve"> - <source>GIFs and stickers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group" xml:space="preserve"> - <source>Group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group display name" xml:space="preserve"> - <source>Group display name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group full name (optional)" xml:space="preserve"> - <source>Group full name (optional)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group image" xml:space="preserve"> - <source>Group image</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group invitation" xml:space="preserve"> - <source>Group invitation</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group invitation expired" xml:space="preserve"> - <source>Group invitation expired</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group invitation is no longer valid, it was removed by sender." xml:space="preserve"> - <source>Group invitation is no longer valid, it was removed by sender.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group link" xml:space="preserve"> - <source>Group link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group links" xml:space="preserve"> - <source>Group links</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> - <source>Group members can irreversibly delete sent messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group message:" xml:space="preserve"> - <source>Group message:</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Group preferences" xml:space="preserve"> - <source>Group preferences</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group profile" xml:space="preserve"> - <source>Group profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group profile is stored on members' devices, not on the servers." xml:space="preserve"> - <source>Group profile is stored on members' devices, not on the servers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group will be deleted for all members - this cannot be undone!" xml:space="preserve"> - <source>Group will be deleted for all members - this cannot be undone!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group will be deleted for you - this cannot be undone!" xml:space="preserve"> - <source>Group will be deleted for you - this cannot be undone!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Help" xml:space="preserve"> - <source>Help</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Hidden" xml:space="preserve"> - <source>Hidden</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Hide" xml:space="preserve"> - <source>Hide</source> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Hide app screen in the recent apps." xml:space="preserve"> - <source>Hide app screen in the recent apps.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="How SimpleX works" xml:space="preserve"> - <source>How SimpleX works</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="How it works" xml:space="preserve"> - <source>How it works</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="How to" xml:space="preserve"> - <source>How to</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="How to use it" xml:space="preserve"> - <source>How to use it</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="How to use your servers" xml:space="preserve"> - <source>How to use your servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="ICE servers (one per line)" xml:space="preserve"> - <source>ICE servers (one per line)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="If the video fails to connect, flip the camera to resolve it." xml:space="preserve"> - <source>If the video fails to connect, flip the camera to resolve it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="If you can't meet in person, **show QR code in the video call**, or share the link." xml:space="preserve"> - <source>If you can't meet in person, **show QR code in the video call**, or share the link.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." xml:space="preserve"> - <source>If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Ignore" xml:space="preserve"> - <source>Ignore</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve"> - <source>Image will be received when your contact is online, please wait or check later!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Import" xml:space="preserve"> - <source>Import</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Import chat database?" xml:space="preserve"> - <source>Import chat database?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Import database" xml:space="preserve"> - <source>Import database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Improved privacy and security" xml:space="preserve"> - <source>Improved privacy and security</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Improved server configuration" xml:space="preserve"> - <source>Improved server configuration</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito" xml:space="preserve"> - <source>Incognito</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode" xml:space="preserve"> - <source>Incognito mode</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve"> - <source>Incognito mode is not supported here - your main profile will be sent to group members</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve"> - <source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incoming audio call" xml:space="preserve"> - <source>Incoming audio call</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Incoming call" xml:space="preserve"> - <source>Incoming call</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Incoming video call" xml:space="preserve"> - <source>Incoming video call</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Incorrect security code!" xml:space="preserve"> - <source>Incorrect security code!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> - <source>Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> - <source>Instant push notifications will be hidden! -</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Instantly" xml:space="preserve"> - <source>Instantly</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Invalid connection link" xml:space="preserve"> - <source>Invalid connection link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Invalid server address!" xml:space="preserve"> - <source>Invalid server address!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Invitation expired!" xml:space="preserve"> - <source>Invitation expired!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Invite members" xml:space="preserve"> - <source>Invite members</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Invite to group" xml:space="preserve"> - <source>Invite to group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Irreversible message deletion" xml:space="preserve"> - <source>Irreversible message deletion</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this chat." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this chat.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve"> - <source>It allows having many anonymous connections without any shared data between them in a single chat profile.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="It can happen when: 1. The messages expire on the server if they were not received for 30 days, 2. The server you use to receive the messages from this contact was updated and restarted. 3. The connection is compromised. Please connect to the developers via Settings to receive the updates about the servers. We will be adding server redundancy to prevent lost messages." xml:space="preserve"> - <source>It can happen when: -1. The messages expire on the server if they were not received for 30 days, -2. The server you use to receive the messages from this contact was updated and restarted. -3. The connection is compromised. -Please connect to the developers via Settings to receive the updates about the servers. -We will be adding server redundancy to prevent lost messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> - <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Join" xml:space="preserve"> - <source>Join</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Join group" xml:space="preserve"> - <source>Join group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Join incognito" xml:space="preserve"> - <source>Join incognito</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Joining group" xml:space="preserve"> - <source>Joining group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Keychain error" xml:space="preserve"> - <source>Keychain error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="LIVE" xml:space="preserve"> - <source>LIVE</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Large file!" xml:space="preserve"> - <source>Large file!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Leave" xml:space="preserve"> - <source>Leave</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Leave group" xml:space="preserve"> - <source>Leave group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Leave group?" xml:space="preserve"> - <source>Leave group?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Light" xml:space="preserve"> - <source>Light</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Limitations" xml:space="preserve"> - <source>Limitations</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Live message!" xml:space="preserve"> - <source>Live message!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Live messages" xml:space="preserve"> - <source>Live messages</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Local name" xml:space="preserve"> - <source>Local name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Local profile data only" xml:space="preserve"> - <source>Local profile data only</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> - <source>Make a private connection</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Make sure SMP server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure SMP server addresses are in correct format, line separated and are not duplicated (%@).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> - <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Mark deleted for everyone" xml:space="preserve"> - <source>Mark deleted for everyone</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Mark read" xml:space="preserve"> - <source>Mark read</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Mark verified" xml:space="preserve"> - <source>Mark verified</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Markdown in messages" xml:space="preserve"> - <source>Markdown in messages</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Max 30 seconds, received instantly." xml:space="preserve"> - <source>Max 30 seconds, received instantly.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Member" xml:space="preserve"> - <source>Member</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> - <source>Member role will be changed to "%@". All group members will be notified.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Member role will be changed to "%@". The member will receive a new invitation." xml:space="preserve"> - <source>Member role will be changed to "%@". The member will receive a new invitation.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> - <source>Member will be removed from group - this cannot be undone!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Message delivery error" xml:space="preserve"> - <source>Message delivery error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Message text" xml:space="preserve"> - <source>Message text</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Messages" xml:space="preserve"> - <source>Messages</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Migrating database archive..." xml:space="preserve"> - <source>Migrating database archive...</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Migration error:" xml:space="preserve"> - <source>Migration error:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." xml:space="preserve"> - <source>Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Migration is completed" xml:space="preserve"> - <source>Migration is completed</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> - <source>Most likely this contact has deleted the connection with you.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Mute" xml:space="preserve"> - <source>Mute</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Name" xml:space="preserve"> - <source>Name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Network & servers" xml:space="preserve"> - <source>Network & servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Network settings" xml:space="preserve"> - <source>Network settings</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Network status" xml:space="preserve"> - <source>Network status</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="New contact request" xml:space="preserve"> - <source>New contact request</source> - <note>notification</note> - </trans-unit> - <trans-unit id="New contact:" xml:space="preserve"> - <source>New contact:</source> - <note>notification</note> - </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> - <source>New database archive</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="New in %@" xml:space="preserve"> - <source>New in %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="New member role" xml:space="preserve"> - <source>New member role</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="New message" xml:space="preserve"> - <source>New message</source> - <note>notification</note> - </trans-unit> - <trans-unit id="New passphrase…" xml:space="preserve"> - <source>New passphrase…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No" xml:space="preserve"> - <source>No</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No contacts selected" xml:space="preserve"> - <source>No contacts selected</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No contacts to add" xml:space="preserve"> - <source>No contacts to add</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No device token!" xml:space="preserve"> - <source>No device token!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No group!" xml:space="preserve"> - <source>Group not found!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No permission to record voice message" xml:space="preserve"> - <source>No permission to record voice message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No received or sent files" xml:space="preserve"> - <source>No received or sent files</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Notifications" xml:space="preserve"> - <source>Notifications</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Notifications are disabled!" xml:space="preserve"> - <source>Notifications are disabled!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> - <source>Off (Local)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Ok" xml:space="preserve"> - <source>Ok</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Old database" xml:space="preserve"> - <source>Old database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> - <source>Old database archive</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="One-time invitation link" xml:space="preserve"> - <source>One-time invitation link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Onion hosts will not be used." xml:space="preserve"> - <source>Onion hosts will not be used.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> - <source>Only group owners can change group preferences.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only group owners can enable voice messages." xml:space="preserve"> - <source>Only group owners can enable voice messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only you can send disappearing messages." xml:space="preserve"> - <source>Only you can send disappearing messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only you can send voice messages." xml:space="preserve"> - <source>Only you can send voice messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only your contact can send disappearing messages." xml:space="preserve"> - <source>Only your contact can send disappearing messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only your contact can send voice messages." xml:space="preserve"> - <source>Only your contact can send voice messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Open Settings" xml:space="preserve"> - <source>Open Settings</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Open chat" xml:space="preserve"> - <source>Open chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Open chat console" xml:space="preserve"> - <source>Open chat console</source> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve"> - <source>Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="PING count" xml:space="preserve"> - <source>PING count</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="PING interval" xml:space="preserve"> - <source>PING interval</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Paste" xml:space="preserve"> - <source>Paste</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Paste image" xml:space="preserve"> - <source>Paste image</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve"> - <source>Paste received link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve"> - <source>Paste the link you received into the box below to connect with your contact.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Periodically" xml:space="preserve"> - <source>Periodically</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> - <source>Please ask your contact to enable sending voice messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> - <source>Please check that you used the correct link or ask your contact to send you another one.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please check your network connection with %@ and try again." xml:space="preserve"> - <source>Please check your network connection with %@ and try again.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please check yours and your contact preferences." xml:space="preserve"> - <source>Please check yours and your contact preferences.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please enter correct current passphrase." xml:space="preserve"> - <source>Please enter correct current passphrase.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please enter the previous password after restoring database backup. This action can not be undone." xml:space="preserve"> - <source>Please enter the previous password after restoring database backup. This action can not be undone.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please restart the app and migrate the database to enable push notifications." xml:space="preserve"> - <source>Please restart the app and migrate the database to enable push notifications.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please store passphrase securely, you will NOT be able to access chat if you lose it." xml:space="preserve"> - <source>Please store passphrase securely, you will NOT be able to access chat if you lose it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please store passphrase securely, you will NOT be able to change it if you lose it." xml:space="preserve"> - <source>Please store passphrase securely, you will NOT be able to change it if you lose it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> - <source>Possibly, certificate fingerprint in server address is incorrect</source> - <note>server test error</note> - </trans-unit> - <trans-unit id="Preset server" xml:space="preserve"> - <source>Preset server</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Preset server address" xml:space="preserve"> - <source>Preset server address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Privacy & security" xml:space="preserve"> - <source>Privacy & security</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Privacy redefined" xml:space="preserve"> - <source>Privacy redefined</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile and server connections" xml:space="preserve"> - <source>Profile and server connections</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile image" xml:space="preserve"> - <source>Profile image</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Prohibit irreversible message deletion." xml:space="preserve"> - <source>Prohibit irreversible message deletion.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> - <source>Prohibit sending direct messages to members.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Prohibit sending disappearing messages." xml:space="preserve"> - <source>Prohibit sending disappearing messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Prohibit sending voice messages." xml:space="preserve"> - <source>Prohibit sending voice messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Protect app screen" xml:space="preserve"> - <source>Protect app screen</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Protocol timeout" xml:space="preserve"> - <source>Protocol timeout</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Push notifications" xml:space="preserve"> - <source>Push notifications</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Rate the app" xml:space="preserve"> - <source>Rate the app</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Read" xml:space="preserve"> - <source>Read</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> - <source>Read more in our GitHub repository.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> - <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Received file event" xml:space="preserve"> - <source>Received file event</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Receiving via" xml:space="preserve"> - <source>Receiving via</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> - <source>Recipients see updates as you type them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reject" xml:space="preserve"> - <source>Reject</source> - <note>reject incoming call via notification</note> - </trans-unit> - <trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve"> - <source>Reject contact (sender NOT notified)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reject contact request" xml:space="preserve"> - <source>Reject contact request</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Relay server is only used if necessary. Another party can observe your IP address." xml:space="preserve"> - <source>Relay server is only used if necessary. Another party can observe your IP address.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Relay server protects your IP address, but it can observe the duration of the call." xml:space="preserve"> - <source>Relay server protects your IP address, but it can observe the duration of the call.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Remove" xml:space="preserve"> - <source>Remove</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Remove member" xml:space="preserve"> - <source>Remove member</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Remove member?" xml:space="preserve"> - <source>Remove member?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Remove passphrase from keychain?" xml:space="preserve"> - <source>Remove passphrase from keychain?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reply" xml:space="preserve"> - <source>Reply</source> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Required" xml:space="preserve"> - <source>Required</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reset" xml:space="preserve"> - <source>Reset</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reset colors" xml:space="preserve"> - <source>Reset colors</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reset to defaults" xml:space="preserve"> - <source>Reset to defaults</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> - <source>Restart the app to create a new chat profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Restart the app to use imported chat database" xml:space="preserve"> - <source>Restart the app to use imported chat database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Restore" xml:space="preserve"> - <source>Restore</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Restore database backup" xml:space="preserve"> - <source>Restore database backup</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Restore database backup?" xml:space="preserve"> - <source>Restore database backup?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Restore database error" xml:space="preserve"> - <source>Restore database error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reveal" xml:space="preserve"> - <source>Reveal</source> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Revert" xml:space="preserve"> - <source>Revert</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Role" xml:space="preserve"> - <source>Role</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Run chat" xml:space="preserve"> - <source>Run chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve"> - <source>SMP servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save" xml:space="preserve"> - <source>Save</source> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Save (and notify contacts)" xml:space="preserve"> - <source>Save (and notify contacts)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save and notify contact" xml:space="preserve"> - <source>Save and notify contact</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save and notify group members" xml:space="preserve"> - <source>Save and notify group members</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save archive" xml:space="preserve"> - <source>Save archive</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save group profile" xml:space="preserve"> - <source>Save group profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save passphrase and open chat" xml:space="preserve"> - <source>Save passphrase and open chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save passphrase in Keychain" xml:space="preserve"> - <source>Save passphrase in Keychain</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save preferences?" xml:space="preserve"> - <source>Save preferences?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save servers" xml:space="preserve"> - <source>Save servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> - <source>Saved WebRTC ICE servers will be removed</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Scan QR code" xml:space="preserve"> - <source>Scan QR code</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Scan code" xml:space="preserve"> - <source>Scan code</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Scan security code from your contact's app." xml:space="preserve"> - <source>Scan security code from your contact's app.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Scan server QR code" xml:space="preserve"> - <source>Scan server QR code</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Search" xml:space="preserve"> - <source>Search</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Secure queue" xml:space="preserve"> - <source>Secure queue</source> - <note>server test step</note> - </trans-unit> - <trans-unit id="Security assessment" xml:space="preserve"> - <source>Security assessment</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Security code" xml:space="preserve"> - <source>Security code</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send" xml:space="preserve"> - <source>Send</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send a live message - it will update for the recipient(s) as you type it" xml:space="preserve"> - <source>Send a live message - it will update for the recipient(s) as you type it</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve"> - <source>Send direct message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send link previews" xml:space="preserve"> - <source>Send link previews</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send live message" xml:space="preserve"> - <source>Send live message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send notifications" xml:space="preserve"> - <source>Send notifications</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> - <source>Send notifications:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send questions and ideas" xml:space="preserve"> - <source>Send questions and ideas</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve"> - <source>Send them from gallery or custom keyboards.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> - <source>Sender cancelled file transfer.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> - <source>Sender may have deleted the connection request.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Sending via" xml:space="preserve"> - <source>Sending via</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Sent file event" xml:space="preserve"> - <source>Sent file event</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> - <source>Sent messages will be deleted after set time.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> - <source>Server requires authorization to create queues, check password</source> - <note>server test error</note> - </trans-unit> - <trans-unit id="Server test failed!" xml:space="preserve"> - <source>Server test failed!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Servers" xml:space="preserve"> - <source>Servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Set 1 day" xml:space="preserve"> - <source>Set 1 day</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Set contact name…" xml:space="preserve"> - <source>Set contact name…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Set group preferences" xml:space="preserve"> - <source>Set group preferences</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Set passphrase to export" xml:space="preserve"> - <source>Set passphrase to export</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Set timeouts for proxy/VPN" xml:space="preserve"> - <source>Set timeouts for proxy/VPN</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Settings" xml:space="preserve"> - <source>Settings</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Share" xml:space="preserve"> - <source>Share</source> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Share invitation link" xml:space="preserve"> - <source>Share invitation link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Share link" xml:space="preserve"> - <source>Share link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve"> - <source>Share one-time invitation link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Show QR code" xml:space="preserve"> - <source>Show QR code</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Show preview" xml:space="preserve"> - <source>Show preview</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> - <source>SimpleX Chat security was audited by Trail of Bits.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX Lock" xml:space="preserve"> - <source>SimpleX Lock</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX Lock turned on" xml:space="preserve"> - <source>SimpleX Lock turned on</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX contact address" xml:space="preserve"> - <source>SimpleX contact address</source> - <note>simplex link type</note> - </trans-unit> - <trans-unit id="SimpleX encrypted message or connection event" xml:space="preserve"> - <source>SimpleX encrypted message or connection event</source> - <note>notification</note> - </trans-unit> - <trans-unit id="SimpleX group link" xml:space="preserve"> - <source>SimpleX group link</source> - <note>simplex link type</note> - </trans-unit> - <trans-unit id="SimpleX links" xml:space="preserve"> - <source>SimpleX links</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> - <source>SimpleX one-time invitation</source> - <note>simplex link type</note> - </trans-unit> - <trans-unit id="Skip" xml:space="preserve"> - <source>Skip</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Skipped messages" xml:space="preserve"> - <source>Skipped messages</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Somebody" xml:space="preserve"> - <source>Somebody</source> - <note>notification title</note> - </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve"> - <source>Start a new chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Start chat" xml:space="preserve"> - <source>Start chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Start migration" xml:space="preserve"> - <source>Start migration</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Stop" xml:space="preserve"> - <source>Stop</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Stop SimpleX" xml:space="preserve"> - <source>Stop SimpleX</source> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> - <source>Stop chat to enable database actions</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> - <source>Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Stop chat?" xml:space="preserve"> - <source>Stop chat?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Support SimpleX Chat" xml:space="preserve"> - <source>Support SimpleX Chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="System" xml:space="preserve"> - <source>System</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="TCP connection timeout" xml:space="preserve"> - <source>TCP connection timeout</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> - <source>TCP_KEEPCNT</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="TCP_KEEPIDLE" xml:space="preserve"> - <source>TCP_KEEPIDLE</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="TCP_KEEPINTVL" xml:space="preserve"> - <source>TCP_KEEPINTVL</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Take picture" xml:space="preserve"> - <source>Take picture</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Tap button " xml:space="preserve"> - <source>Tap button </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Tap to join" xml:space="preserve"> - <source>Tap to join</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Tap to join incognito" xml:space="preserve"> - <source>Tap to join incognito</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve"> - <source>Tap to start a new chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Test failed at step %@." xml:space="preserve"> - <source>Test failed at step %@.</source> - <note>server test failure</note> - </trans-unit> - <trans-unit id="Test server" xml:space="preserve"> - <source>Test server</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Test servers" xml:space="preserve"> - <source>Test servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Tests failed!" xml:space="preserve"> - <source>Tests failed!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> - <source>Thank you for installing SimpleX Chat!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve"> - <source>The app can notify you when you receive messages or contact requests - please open settings to enable.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> - <source>The attempt to change database passphrase was not completed.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> - <source>The connection you accepted will be cancelled!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The contact you shared this link with will NOT be able to connect!" xml:space="preserve"> - <source>The contact you shared this link with will NOT be able to connect!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The created archive is available via app Settings / Database / Old database archive." xml:space="preserve"> - <source>The created archive is available via app Settings / Database / Old database archive.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The group is fully decentralized – it is visible only to the members." xml:space="preserve"> - <source>The group is fully decentralized – it is visible only to the members.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The microphone does not work when the app is in the background." xml:space="preserve"> - <source>The microphone does not work when the app is in the background.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> - <source>The old database was not removed during the migration, it can be deleted.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The sender will NOT be notified" xml:space="preserve"> - <source>The sender will NOT be notified</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The servers for new connections of your current chat profile **%@**." xml:space="preserve"> - <source>The servers for new connections of your current chat profile **%@**.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Theme" xml:space="preserve"> - <source>Theme</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> - <source>This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." xml:space="preserve"> - <source>This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> - <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member)." xml:space="preserve"> - <source>This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This group no longer exists." xml:space="preserve"> - <source>This group no longer exists.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> - <source>This setting applies to messages in your current chat profile **%@**.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> - <source>To ask any questions and to receive updates:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve"> - <source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To make a new connection" xml:space="preserve"> - <source>To make a new connection</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To prevent the call interruption, enable Do Not Disturb mode." xml:space="preserve"> - <source>To prevent the call interruption, enable Do Not Disturb mode.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> - <source>To protect your information, turn on SimpleX Lock. -You will be prompted to complete authentication before this feature is enabled.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> - <source>To record voice message please grant permission to use Microphone.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> - <source>To support instant push notifications the chat database has to be migrated.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> - <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Transfer images faster" xml:space="preserve"> - <source>Transfer images faster</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Transport isolation" xml:space="preserve"> - <source>Transport isolation</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> - <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Trying to connect to the server used to receive messages from this contact." xml:space="preserve"> - <source>Trying to connect to the server used to receive messages from this contact.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Turn off" xml:space="preserve"> - <source>Turn off</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve"> - <source>Turn off notifications?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Turn on" xml:space="preserve"> - <source>Turn on</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unable to record voice message" xml:space="preserve"> - <source>Unable to record voice message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve"> - <source>Unexpected error: %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unexpected migration state" xml:space="preserve"> - <source>Unexpected migration state</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unknown database error: %@" xml:space="preserve"> - <source>Unknown database error: %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unknown error" xml:space="preserve"> - <source>Unknown error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection." xml:space="preserve"> - <source>Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unlock" xml:space="preserve"> - <source>Unlock</source> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Unmute" xml:space="preserve"> - <source>Unmute</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unread" xml:space="preserve"> - <source>Unread</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Update" xml:space="preserve"> - <source>Update</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> - <source>Update .onion hosts setting?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Update database passphrase" xml:space="preserve"> - <source>Update database passphrase</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Update network settings?" xml:space="preserve"> - <source>Update network settings?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve"> - <source>Update transport isolation mode?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> - <source>Updating settings will re-connect the client to all servers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Use .onion hosts" xml:space="preserve"> - <source>Use .onion hosts</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> - <source>Use SimpleX Chat servers?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Use chat" xml:space="preserve"> - <source>Use chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Use for new connections" xml:space="preserve"> - <source>Use for new connections</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Use server" xml:space="preserve"> - <source>Use server</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> - <source>Using SimpleX Chat servers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Verify connection security" xml:space="preserve"> - <source>Verify connection security</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Verify security code" xml:space="preserve"> - <source>Verify security code</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Via browser" xml:space="preserve"> - <source>Via browser</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Video call" xml:space="preserve"> - <source>Video call</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="View security code" xml:space="preserve"> - <source>View security code</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Voice messages" xml:space="preserve"> - <source>Voice messages</source> - <note>chat feature</note> - </trans-unit> - <trans-unit id="Voice messages are prohibited in this chat." xml:space="preserve"> - <source>Voice messages are prohibited in this chat.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Voice messages prohibited!" xml:space="preserve"> - <source>Voice messages prohibited!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Voice message…" xml:space="preserve"> - <source>Voice message…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Waiting for file" xml:space="preserve"> - <source>Waiting for file</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Waiting for image" xml:space="preserve"> - <source>Waiting for image</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="WebRTC ICE servers" xml:space="preserve"> - <source>WebRTC ICE servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Welcome %@!" xml:space="preserve"> - <source>Welcome %@!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Welcome message" xml:space="preserve"> - <source>Welcome message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="What's new" xml:space="preserve"> - <source>What's new</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="When available" xml:space="preserve"> - <source>When available</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> - <source>When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="With optional welcome message." xml:space="preserve"> - <source>With optional welcome message.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Wrong database passphrase" xml:space="preserve"> - <source>Wrong database passphrase</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Wrong passphrase!" xml:space="preserve"> - <source>Wrong passphrase!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You accepted connection" xml:space="preserve"> - <source>You accepted connection</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You allow" xml:space="preserve"> - <source>You allow</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You are already connected to %@." xml:space="preserve"> - <source>You are already connected to %@.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve"> - <source>You are connected to the server used to receive messages from this contact.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You are invited to group" xml:space="preserve"> - <source>You are invited to group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> - <note>notification body</note> - </trans-unit> - <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> - <source>You can set lock screen notification preview via settings.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." xml:space="preserve"> - <source>You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it." xml:space="preserve"> - <source>You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> - <source>You can start chat via app Settings / Database or by restarting the app</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can use markdown to format messages:" xml:space="preserve"> - <source>You can use markdown to format messages:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You could not be verified; please try again." xml:space="preserve"> - <source>You could not be verified; please try again.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> - <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You invited your contact" xml:space="preserve"> - <source>You invited your contact</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You joined this group" xml:space="preserve"> - <source>You joined this group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You joined this group. Connecting to inviting group member." xml:space="preserve"> - <source>You joined this group. Connecting to inviting group member.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> - <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> - <source>You need to allow your contact to send voice messages to be able to send them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You rejected group invitation" xml:space="preserve"> - <source>You rejected group invitation</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You sent group invitation" xml:space="preserve"> - <source>You sent group invitation</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> - <source>You will be connected to group when the group host's device is online, please wait or check later!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve"> - <source>You will be connected when your connection request is accepted, please wait or check later!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will be connected when your contact's device is online, please wait or check later!" xml:space="preserve"> - <source>You will be connected when your contact's device is online, please wait or check later!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will be required to authenticate when you start or resume the app after 30 seconds in background." xml:space="preserve"> - <source>You will be required to authenticate when you start or resume the app after 30 seconds in background.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve"> - <source>You will join a group this link refers to and connect to its group members.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> - <source>You will stop receiving messages from this group. Chat history will be preserved.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" xml:space="preserve"> - <source>You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" xml:space="preserve"> - <source>You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your ICE servers" xml:space="preserve"> - <source>Your ICE servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your SimpleX contact address" xml:space="preserve"> - <source>Your SimpleX contact address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your calls" xml:space="preserve"> - <source>Your calls</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat database" xml:space="preserve"> - <source>Your chat database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat database is not encrypted - set passphrase to encrypt it." xml:space="preserve"> - <source>Your chat database is not encrypted - set passphrase to encrypt it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat profile" xml:space="preserve"> - <source>Your chat profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat profile will be sent to group members" xml:space="preserve"> - <source>Your chat profile will be sent to group members</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve"> - <source>Your chat profile will be sent to your contact</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat profiles" xml:space="preserve"> - <source>Your chat profiles</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat profiles are stored locally, only on your device." xml:space="preserve"> - <source>Your chat profiles are stored locally, only on your device.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chats" xml:space="preserve"> - <source>Your chats</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contact address" xml:space="preserve"> - <source>Your contact address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contact can scan it from the app." xml:space="preserve"> - <source>Your contact can scan it from the app.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> - <source>Your contact sent a file that is larger than currently supported maximum size (%@).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contacts can allow full message deletion." xml:space="preserve"> - <source>Your contacts can allow full message deletion.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> - <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your current profile" xml:space="preserve"> - <source>Your current profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your preferences" xml:space="preserve"> - <source>Your preferences</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your privacy" xml:space="preserve"> - <source>Your privacy</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>Your profile will be sent to the contact that you received this link from</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your random profile" xml:space="preserve"> - <source>Your random profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your server address" xml:space="preserve"> - <source>Your server address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your settings" xml:space="preserve"> - <source>Your settings</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" xml:space="preserve"> - <source>[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="[Send us email](mailto:chat@simplex.chat)" xml:space="preserve"> - <source>[Send us email](mailto:chat@simplex.chat)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> - <source>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="_italic_" xml:space="preserve"> - <source>\_italic_</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="`a + b`" xml:space="preserve"> - <source>\`a + b`</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="above, then choose:" xml:space="preserve"> - <source>above, then choose:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="accepted call" xml:space="preserve"> - <source>accepted call</source> - <note>call status</note> - </trans-unit> - <trans-unit id="admin" xml:space="preserve"> - <source>admin</source> - <note>member role</note> - </trans-unit> - <trans-unit id="always" xml:space="preserve"> - <source>always</source> - <note>pref value</note> - </trans-unit> - <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> - <source>audio call (not e2e encrypted)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="bad message ID" xml:space="preserve"> - <source>bad message ID</source> - <note>integrity error chat item</note> - </trans-unit> - <trans-unit id="bad message hash" xml:space="preserve"> - <source>bad message hash</source> - <note>integrity error chat item</note> - </trans-unit> - <trans-unit id="bold" xml:space="preserve"> - <source>bold</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="call error" xml:space="preserve"> - <source>call error</source> - <note>call status</note> - </trans-unit> - <trans-unit id="call in progress" xml:space="preserve"> - <source>call in progress</source> - <note>call status</note> - </trans-unit> - <trans-unit id="calling…" xml:space="preserve"> - <source>calling…</source> - <note>call status</note> - </trans-unit> - <trans-unit id="cancelled %@" xml:space="preserve"> - <source>cancelled %@</source> - <note>feature offered item</note> - </trans-unit> - <trans-unit id="changed address for you" xml:space="preserve"> - <source>changed address for you</source> - <note>chat item text</note> - </trans-unit> - <trans-unit id="changed role of %@ to %@" xml:space="preserve"> - <source>changed role of %1$@ to %2$@</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="changed your role to %@" xml:space="preserve"> - <source>changed your role to %@</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="changing address for %@..." xml:space="preserve"> - <source>changing address for %@...</source> - <note>chat item text</note> - </trans-unit> - <trans-unit id="changing address..." xml:space="preserve"> - <source>changing address...</source> - <note>chat item text</note> - </trans-unit> - <trans-unit id="colored" xml:space="preserve"> - <source>colored</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="complete" xml:space="preserve"> - <source>complete</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connect to SimpleX Chat developers." xml:space="preserve"> - <source>connect to SimpleX Chat developers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connected" xml:space="preserve"> - <source>connected</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connecting" xml:space="preserve"> - <source>connecting</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connecting (accepted)" xml:space="preserve"> - <source>connecting (accepted)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connecting (announced)" xml:space="preserve"> - <source>connecting (announced)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connecting (introduced)" xml:space="preserve"> - <source>connecting (introduced)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connecting (introduction invitation)" xml:space="preserve"> - <source>connecting (introduction invitation)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connecting call" xml:space="preserve"> - <source>connecting call…</source> - <note>call status</note> - </trans-unit> - <trans-unit id="connecting…" xml:space="preserve"> - <source>connecting…</source> - <note>chat list item title</note> - </trans-unit> - <trans-unit id="connection established" xml:space="preserve"> - <source>connection established</source> - <note>chat list item title (it should not be shown</note> - </trans-unit> - <trans-unit id="connection:%@" xml:space="preserve"> - <source>connection:%@</source> - <note>connection information</note> - </trans-unit> - <trans-unit id="contact has e2e encryption" xml:space="preserve"> - <source>contact has e2e encryption</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="contact has no e2e encryption" xml:space="preserve"> - <source>contact has no e2e encryption</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="creator" xml:space="preserve"> - <source>creator</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="default (%@)" xml:space="preserve"> - <source>default (%@)</source> - <note>pref value</note> - </trans-unit> - <trans-unit id="deleted" xml:space="preserve"> - <source>deleted</source> - <note>deleted chat item</note> - </trans-unit> - <trans-unit id="deleted group" xml:space="preserve"> - <source>deleted group</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="direct" xml:space="preserve"> - <source>direct</source> - <note>connection level description</note> - </trans-unit> - <trans-unit id="duplicate message" xml:space="preserve"> - <source>duplicate message</source> - <note>integrity error chat item</note> - </trans-unit> - <trans-unit id="e2e encrypted" xml:space="preserve"> - <source>e2e encrypted</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="enabled" xml:space="preserve"> - <source>enabled</source> - <note>enabled status</note> - </trans-unit> - <trans-unit id="enabled for contact" xml:space="preserve"> - <source>enabled for contact</source> - <note>enabled status</note> - </trans-unit> - <trans-unit id="enabled for you" xml:space="preserve"> - <source>enabled for you</source> - <note>enabled status</note> - </trans-unit> - <trans-unit id="ended" xml:space="preserve"> - <source>ended</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="ended call %@" xml:space="preserve"> - <source>ended call %@</source> - <note>call status</note> - </trans-unit> - <trans-unit id="error" xml:space="preserve"> - <source>error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="group deleted" xml:space="preserve"> - <source>group deleted</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="group profile updated" xml:space="preserve"> - <source>group profile updated</source> - <note>snd group event chat item</note> - </trans-unit> - <trans-unit id="iOS Keychain is used to securely store passphrase - it allows receiving push notifications." xml:space="preserve"> - <source>iOS Keychain is used to securely store passphrase - it allows receiving push notifications.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." xml:space="preserve"> - <source>iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="incognito via contact address link" xml:space="preserve"> - <source>incognito via contact address link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="incognito via group link" xml:space="preserve"> - <source>incognito via group link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="incognito via one-time link" xml:space="preserve"> - <source>incognito via one-time link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="indirect (%d)" xml:space="preserve"> - <source>indirect (%d)</source> - <note>connection level description</note> - </trans-unit> - <trans-unit id="invalid chat" xml:space="preserve"> - <source>invalid chat</source> - <note>invalid chat data</note> - </trans-unit> - <trans-unit id="invalid chat data" xml:space="preserve"> - <source>invalid chat data</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="invalid data" xml:space="preserve"> - <source>invalid data</source> - <note>invalid chat item</note> - </trans-unit> - <trans-unit id="invitation to group %@" xml:space="preserve"> - <source>invitation to group %@</source> - <note>group name</note> - </trans-unit> - <trans-unit id="invited" xml:space="preserve"> - <source>invited</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="invited %@" xml:space="preserve"> - <source>invited %@</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="invited to connect" xml:space="preserve"> - <source>invited to connect</source> - <note>chat list item title</note> - </trans-unit> - <trans-unit id="invited via your group link" xml:space="preserve"> - <source>invited via your group link</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="italic" xml:space="preserve"> - <source>italic</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="join as %@" xml:space="preserve"> - <source>join as %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="left" xml:space="preserve"> - <source>left</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="marked deleted" xml:space="preserve"> - <source>marked deleted</source> - <note>marked deleted chat item preview text</note> - </trans-unit> - <trans-unit id="member" xml:space="preserve"> - <source>member</source> - <note>member role</note> - </trans-unit> - <trans-unit id="member connected" xml:space="preserve"> - <source>connected</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="message received" xml:space="preserve"> - <source>message received</source> - <note>notification</note> - </trans-unit> - <trans-unit id="missed call" xml:space="preserve"> - <source>missed call</source> - <note>call status</note> - </trans-unit> - <trans-unit id="never" xml:space="preserve"> - <source>never</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="new message" xml:space="preserve"> - <source>new message</source> - <note>notification</note> - </trans-unit> - <trans-unit id="no" xml:space="preserve"> - <source>no</source> - <note>pref value</note> - </trans-unit> - <trans-unit id="no e2e encryption" xml:space="preserve"> - <source>no e2e encryption</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="off" xml:space="preserve"> - <source>off</source> - <note>enabled status - group pref value</note> - </trans-unit> - <trans-unit id="offered %@" xml:space="preserve"> - <source>offered %@</source> - <note>feature offered item</note> - </trans-unit> - <trans-unit id="offered %@: %@" xml:space="preserve"> - <source>offered %1$@: %2$@</source> - <note>feature offered item</note> - </trans-unit> - <trans-unit id="on" xml:space="preserve"> - <source>on</source> - <note>group pref value</note> - </trans-unit> - <trans-unit id="or chat with the developers" xml:space="preserve"> - <source>or chat with the developers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="owner" xml:space="preserve"> - <source>owner</source> - <note>member role</note> - </trans-unit> - <trans-unit id="peer-to-peer" xml:space="preserve"> - <source>peer-to-peer</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="received answer…" xml:space="preserve"> - <source>received answer…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="received confirmation…" xml:space="preserve"> - <source>received confirmation…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="rejected call" xml:space="preserve"> - <source>rejected call</source> - <note>call status</note> - </trans-unit> - <trans-unit id="removed" xml:space="preserve"> - <source>removed</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="removed %@" xml:space="preserve"> - <source>removed %@</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="removed you" xml:space="preserve"> - <source>removed you</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="sec" xml:space="preserve"> - <source>sec</source> - <note>network option</note> - </trans-unit> - <trans-unit id="secret" xml:space="preserve"> - <source>secret</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="starting…" xml:space="preserve"> - <source>starting…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="strike" xml:space="preserve"> - <source>strike</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="this contact" xml:space="preserve"> - <source>this contact</source> - <note>notification title</note> - </trans-unit> - <trans-unit id="unknown" xml:space="preserve"> - <source>unknown</source> - <note>connection info</note> - </trans-unit> - <trans-unit id="updated group profile" xml:space="preserve"> - <source>updated group profile</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="v%@ (%@)" xml:space="preserve"> - <source>v%@ (%@)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="via contact address link" xml:space="preserve"> - <source>via contact address link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="via group link" xml:space="preserve"> - <source>via group link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="via one-time link" xml:space="preserve"> - <source>via one-time link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="via relay" xml:space="preserve"> - <source>via relay</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> - <source>video call (not e2e encrypted)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="waiting for answer…" xml:space="preserve"> - <source>waiting for answer…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="waiting for confirmation…" xml:space="preserve"> - <source>waiting for confirmation…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="wants to connect to you!" xml:space="preserve"> - <source>wants to connect to you!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="yes" xml:space="preserve"> - <source>yes</source> - <note>pref value</note> - </trans-unit> - <trans-unit id="you are invited to group" xml:space="preserve"> - <source>you are invited to group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="you changed address" xml:space="preserve"> - <source>you changed address</source> - <note>chat item text</note> - </trans-unit> - <trans-unit id="you changed address for %@" xml:space="preserve"> - <source>you changed address for %@</source> - <note>chat item text</note> - </trans-unit> - <trans-unit id="you changed role for yourself to %@" xml:space="preserve"> - <source>you changed role for yourself to %@</source> - <note>snd group event chat item</note> - </trans-unit> - <trans-unit id="you changed role of %@ to %@" xml:space="preserve"> - <source>you changed role of %1$@ to %2$@</source> - <note>snd group event chat item</note> - </trans-unit> - <trans-unit id="you left" xml:space="preserve"> - <source>you left</source> - <note>snd group event chat item</note> - </trans-unit> - <trans-unit id="you removed %@" xml:space="preserve"> - <source>you removed %@</source> - <note>snd group event chat item</note> - </trans-unit> - <trans-unit id="you shared one-time link" xml:space="preserve"> - <source>you shared one-time link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="you shared one-time link incognito" xml:space="preserve"> - <source>you shared one-time link incognito</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="you: " xml:space="preserve"> - <source>you: </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="~strike~" xml:space="preserve"> - <source>\~strike~</source> - <note>No comment provided by engineer.</note> - </trans-unit> - </body> - </file> - <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="hi" datatype="plaintext"> - <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.0" build-num="14A309"/> - </header> - <body> - <trans-unit id="CFBundleName" xml:space="preserve"> - <source>SimpleX</source> - <note>Bundle name</note> - </trans-unit> - <trans-unit id="NSCameraUsageDescription" xml:space="preserve"> - <source>SimpleX needs camera access to scan QR codes to connect to other users and for video calls.</source> - <note>Privacy - Camera Usage Description</note> - </trans-unit> - <trans-unit id="NSFaceIDUsageDescription" xml:space="preserve"> - <source>SimpleX uses Face ID for local authentication</source> - <note>Privacy - Face ID Usage Description</note> - </trans-unit> - <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> - <source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source> - <note>Privacy - Microphone Usage Description</note> - </trans-unit> - <trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve"> - <source>SimpleX needs access to Photo Library for saving captured and received media</source> - <note>Privacy - Photo Library Additions Usage Description</note> - </trans-unit> - </body> - </file> - <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="hi" datatype="plaintext"> - <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.0" build-num="14A309"/> - </header> - <body> - <trans-unit id="CFBundleDisplayName" xml:space="preserve"> - <source>SimpleX NSE</source> - <note>Bundle display name</note> - </trans-unit> - <trans-unit id="CFBundleName" xml:space="preserve"> - <source>SimpleX NSE</source> - <note>Bundle name</note> - </trans-unit> - <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> - <source>Copyright © 2022 SimpleX Chat. All rights reserved.</source> - <note>Copyright (human-readable)</note> - </trans-unit> - </body> - </file> -</xliff> diff --git a/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff b/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff index abf15ee42d..6ad4d159c7 100644 --- a/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff +++ b/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff @@ -114,12 +114,12 @@ </trans-unit> <trans-unit id="%lld" xml:space="preserve"> <source>%lld</source> - <target></target> + <target/> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld %@" xml:space="preserve"> <source>%lld %@</source> - <target></target> + <target/> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld contact(s) selected" xml:space="preserve" approved="no"> @@ -144,12 +144,12 @@ </trans-unit> <trans-unit id="%lldd" xml:space="preserve"> <source>%lldd</source> - <target></target> + <target/> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lldh" xml:space="preserve"> <source>%lldh</source> - <target></target> + <target/> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lldk" xml:space="preserve"> @@ -158,7 +158,7 @@ </trans-unit> <trans-unit id="%lldm" xml:space="preserve"> <source>%lldm</source> - <target></target> + <target/> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lldmth" xml:space="preserve"> @@ -173,17 +173,14 @@ <source>%lldw</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> + <trans-unit id="(" xml:space="preserve" approved="no"> <source>(</source> + <target state="translated">(</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve"> + <trans-unit id=")" xml:space="preserve" approved="no"> <source>)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target state="needs-translation">**Dodajte novi kontakt**: da biste stvorili svoj jednokratni QR kôd ili vezu za svoj kontakt.</target> + <target state="translated">)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no"> @@ -191,13 +188,13 @@ <target state="needs-translation">**Stvorite vezu / QR kôd** za vaš kontakt.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve" approved="no"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> <target state="needs-translation">**Privatnije**: provjeravajte nove poruke svakih 20 minuta. Token uređaja dijeli se s SimpleX Chat poslužiteljem, ali ne i s brojem kontakata ili poruka koje imate.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve" approved="no"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> <target state="needs-translation">**Najprivatniji**: nemojte koristiti SimpleX Chat poslužitelj obavijesti, povremeno provjeravajte poruke u pozadini (ovisi o tome koliko često koristite aplikaciju).</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -211,8 +208,8 @@ <target state="needs-translation">**Imajte na umu**: NEĆETE moći oporaviti ili promijeniti pristupni izraz ako ga izgubite.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve" approved="no"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> <target state="needs-translation">**Preporučeno**: token uređaja i obavijesti šalju se na poslužitelj obavijesti SimpleX Chata, ali ne i sadržaj poruke, veličinu ili od koga je.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -253,22 +250,22 @@ </trans-unit> <trans-unit id="1 day" xml:space="preserve" approved="no"> <source>1 day</source> - <target state="needs-translation">1 dan</target> + <target state="translated">1 dan</target> <note>message ttl</note> </trans-unit> <trans-unit id="1 hour" xml:space="preserve" approved="no"> <source>1 hour</source> - <target state="needs-translation">1 sat</target> + <target state="translated">1 sat</target> <note>message ttl</note> </trans-unit> <trans-unit id="1 month" xml:space="preserve" approved="no"> <source>1 month</source> - <target state="needs-translation">1 mjesec</target> + <target state="translated">1 mesec</target> <note>message ttl</note> </trans-unit> <trans-unit id="1 week" xml:space="preserve" approved="no"> <source>1 week</source> - <target state="needs-translation">1 tjedan</target> + <target state="translated">1 nedelja</target> <note>message ttl</note> </trans-unit> <trans-unit id="2 weeks" xml:space="preserve" approved="no"> @@ -367,8 +364,8 @@ <source>Add servers by scanning QR codes.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -1039,8 +1036,8 @@ <source>Direct messages</source> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> @@ -1055,8 +1052,8 @@ <source>Disappearing messages are prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disconnect" xml:space="preserve"> @@ -1419,16 +1416,16 @@ <source>Group members can irreversibly delete sent messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> @@ -1519,20 +1516,23 @@ <source>Image will be received when your contact is online, please wait or check later!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Import" xml:space="preserve"> + <trans-unit id="Import" xml:space="preserve" approved="no"> <source>Import</source> + <target state="translated">Uvesti</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Import chat database?" xml:space="preserve"> + <trans-unit id="Import chat database?" xml:space="preserve" approved="no"> <source>Import chat database?</source> + <target state="translated">Uvesti data bazu razgovora?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Import database" xml:space="preserve"> + <trans-unit id="Import database" xml:space="preserve" approved="no"> <source>Import database</source> + <target state="translated">Uvesti data bazu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> @@ -1616,8 +1616,8 @@ <source>Irreversible message deletion is prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve"> @@ -1693,12 +1693,14 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Live message!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Live messages" xml:space="preserve"> + <trans-unit id="Live messages" xml:space="preserve" approved="no"> <source>Live messages</source> + <target state="translated">Žive poruke</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Local name" xml:space="preserve"> + <trans-unit id="Local name" xml:space="preserve" approved="no"> <source>Local name</source> + <target state="translated">Lokalno ime</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Local profile data only" xml:space="preserve"> @@ -1917,8 +1919,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Onion hosts will not be used.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> @@ -1965,8 +1967,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Open chat console</source> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve"> @@ -1997,8 +1999,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Paste the link you received into the box below to connect with your contact.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Periodically" xml:space="preserve"> @@ -2577,8 +2579,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Thanks to the users – contribute via Weblate!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve"> @@ -2609,16 +2611,16 @@ We will be adding server redundancy to prevent lost messages.</source> <source>The microphone does not work when the app is in the background.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> <source>The old database was not removed during the migration, it can be deleted.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The sender will NOT be notified" xml:space="preserve"> @@ -2673,8 +2675,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>To prevent the call interruption, enable Do Not Disturb mode.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -2847,8 +2849,8 @@ To connect, please ask your contact to create another connection link and check <source>Voice messages are prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> @@ -2959,10 +2961,6 @@ To connect, please ask your contact to create another connection link and check <source>You can use markdown to format messages:</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <note>No comment provided by engineer.</note> @@ -3161,8 +3159,9 @@ SimpleX servers cannot see your profile.</source> <source>\_italic_</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="`a + b`" xml:space="preserve"> + <trans-unit id="`a + b`" xml:space="preserve" approved="no"> <source>\`a + b`</source> + <target state="translated">\`a + b`</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="above, then choose:" xml:space="preserve"> @@ -3622,6 +3621,110 @@ SimpleX servers cannot see your profile.</source> <source>\~strike~</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="# %@" xml:space="preserve" approved="no"> + <source># %@</source> + <target state="translated"># %@</target> + </trans-unit> + <trans-unit id="%@ server" xml:space="preserve" approved="no"> + <source>%@ server</source> + <target state="translated">%@ server</target> + </trans-unit> + <trans-unit id="%@ servers" xml:space="preserve" approved="no"> + <source>%@ servers</source> + <target state="translated">%@ serveri</target> + </trans-unit> + <trans-unit id="Import failed" xml:space="preserve" approved="no"> + <source>Import failed</source> + <target state="translated">Uvoz neuspešan</target> + </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve" approved="no"> + <source>%@ downloaded</source> + <target state="translated">%@ preuzeto</target> + </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve" approved="no"> + <source>%@ uploaded</source> + <target state="translated">%@ otpremljeno</target> + </trans-unit> + <trans-unit id="1 minute" xml:space="preserve" approved="no"> + <source>1 minute</source> + <target state="translated">1 minut</target> + </trans-unit> + <trans-unit id="Password" xml:space="preserve" approved="no"> + <source>Password</source> + <target state="translated">Šifra</target> + </trans-unit> + <trans-unit id="## History" xml:space="preserve" approved="no"> + <source>## History</source> + <target state="translated">## Istorija</target> + </trans-unit> + <trans-unit id="%@ (current)" xml:space="preserve" approved="no"> + <source>%@ (current)</source> + <target state="translated">%@ (trenutan)</target> + </trans-unit> + <trans-unit id="%@ and %@" xml:space="preserve" approved="no"> + <source>%@ and %@</source> + <target state="translated">%@ i %@</target> + </trans-unit> + <trans-unit id="%@ connected" xml:space="preserve" approved="no"> + <source>%@ connected</source> + <target state="translated">%@ povezan</target> + </trans-unit> + <trans-unit id="0 sec" xml:space="preserve" approved="no"> + <source>0 sec</source> + <target state="translated">0 sek</target> + </trans-unit> + <trans-unit id="5 minutes" xml:space="preserve" approved="no"> + <source>5 minutes</source> + <target state="translated">5 minuta</target> + </trans-unit> + <trans-unit id="%@ (current):" xml:space="preserve" approved="no"> + <source>%@ (current):</source> + <target state="translated">%@ (trenutan):</target> + </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve" approved="no"> + <source>%@ and %@ connected</source> + <target state="translated">%@ i %@ su povezani</target> + </trans-unit> + <trans-unit id="%@:" xml:space="preserve" approved="no"> + <source>%@:</source> + <target state="translated">%@:</target> + </trans-unit> + <trans-unit id="%@ at %@:" xml:space="preserve" approved="no"> + <source>%1$@ at %2$@:</source> + <target state="translated">%1$@ u %2$@:</target> + </trans-unit> + <trans-unit id="30 seconds" xml:space="preserve" approved="no"> + <source>30 seconds</source> + <target state="translated">30 sekundi</target> + </trans-unit> + <trans-unit id="Password to show" xml:space="preserve" approved="no"> + <source>Password to show</source> + <target state="translated">Prikazati šifru</target> + </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve" approved="no"> + <source>%1$@, %2$@</source> + <target state="translated">%1$@, %2$@</target> + </trans-unit> + <trans-unit id="0s" xml:space="preserve" approved="no"> + <source>0s</source> + <target state="translated">0s</target> + </trans-unit> + <trans-unit id="Import theme" xml:space="preserve" approved="no"> + <source>Import theme</source> + <target state="translated">Uvesti temu</target> + </trans-unit> + <trans-unit id="Immediately" xml:space="preserve" approved="no"> + <source>Immediately</source> + <target state="translated">Odmah</target> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve" approved="no"> + <source>Address settings</source> + <target state="translated">Podešavanje adrese</target> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve" approved="no"> + <source>Admins can block a member for all.</source> + <target state="needs-translation">Administratori mogu da blokiraju</target> + </trans-unit> </body> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="hr" datatype="plaintext"> diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..84d4c2f1c9 --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,15 @@ +{ + "colors" : [ + { + "idiom" : "universal", + "locale" : "hu" + } + ], + "properties" : { + "localizable" : true + }, + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff new file mode 100644 index 0000000000..78bee138e4 --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -0,0 +1,10419 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> + <file original="en.lproj/Localizable.strings" source-language="en" target-language="hu" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id=" (can be copied)" xml:space="preserve"> + <source> (can be copied)</source> + <target> (másolható)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="!1 colored!" xml:space="preserve"> + <source>!1 colored!</source> + <target>!1 színezett!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="# %@" xml:space="preserve"> + <source># %@</source> + <target># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve"> + <source>## History</source> + <target>## Előzmények</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve"> + <source>## In reply to</source> + <target>## Válaszul erre</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="#secret#" xml:space="preserve"> + <source>#secret#</source> + <target>#titok#</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <target>%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ %@" xml:space="preserve"> + <source>%@ %@</source> + <target>%@ %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ (current)" xml:space="preserve"> + <source>%@ (current)</source> + <target>%@ (jelenlegi)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ (current):" xml:space="preserve"> + <source>%@ (current):</source> + <target>%@ (jelenlegi):</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="%@ / %@" xml:space="preserve"> + <source>%@ / %@</source> + <target>%@ / %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ and %@" xml:space="preserve"> + <source>%@ and %@</source> + <target>%@ és %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve"> + <source>%@ and %@ connected</source> + <target>%@ és %@ kapcsolódott</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ at %@:" xml:space="preserve"> + <source>%1$@ at %2$@:</source> + <target>%1$@ ekkor: %2$@:</target> + <note>copied message info, <sender> at <time></note> + </trans-unit> + <trans-unit id="%@ connected" xml:space="preserve"> + <source>%@ connected</source> + <target>%@ kapcsolódott</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve"> + <source>%@ downloaded</source> + <target>%@ letöltve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ is connected!" xml:space="preserve"> + <source>%@ is connected!</source> + <target>%@ kapcsolódott!</target> + <note>notification title</note> + </trans-unit> + <trans-unit id="%@ is not verified" xml:space="preserve"> + <source>%@ is not verified</source> + <target>%@ nincs hitelesítve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ is verified" xml:space="preserve"> + <source>%@ is verified</source> + <target>%@ hitelesítve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ server" xml:space="preserve"> + <source>%@ server</source> + <target>%@ kiszolgáló</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ servers" xml:space="preserve"> + <source>%@ servers</source> + <target>%@ kiszolgáló</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve"> + <source>%@ uploaded</source> + <target>%@ feltöltve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ wants to connect!" xml:space="preserve"> + <source>%@ wants to connect!</source> + <target>%@ kapcsolódni szeretne!</target> + <note>notification title</note> + </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve"> + <source>%1$@, %2$@</source> + <target>%1$@, %2$@</target> + <note>format for date separator in chat</note> + </trans-unit> + <trans-unit id="%@, %@ and %lld members" xml:space="preserve"> + <source>%@, %@ and %lld members</source> + <target>%@, %@ és további %lld tag</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> + <source>%@, %@ and %lld other members connected</source> + <target>%@, %@ és további %lld tag kapcsolódott</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@:" xml:space="preserve"> + <source>%@:</source> + <target>%@:</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="%d days" xml:space="preserve"> + <source>%d days</source> + <target>%d nap</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve"> + <source>%d file(s) are still being downloaded.</source> + <target>%d fájl letöltése még folyamatban van.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve"> + <source>%d file(s) failed to download.</source> + <target>Nem sikerült letölteni %d fájlt.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve"> + <source>%d file(s) were deleted.</source> + <target>%d fájl törölve lett.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve"> + <source>%d file(s) were not downloaded.</source> + <target>%d fájl nem lett letöltve.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d hours" xml:space="preserve"> + <source>%d hours</source> + <target>%d óra</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve"> + <source>%d messages not forwarded</source> + <target>%d üzenet nem lett továbbítva</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="%d min" xml:space="preserve"> + <source>%d min</source> + <target>%d perc</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="%d months" xml:space="preserve"> + <source>%d months</source> + <target>%d hónap</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="%d sec" xml:space="preserve"> + <source>%d sec</source> + <target>%d mp</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve"> + <source>%d seconds(s)</source> + <target>%d másodperc</target> + <note>delete after time</note> + </trans-unit> + <trans-unit id="%d skipped message(s)" xml:space="preserve"> + <source>%d skipped message(s)</source> + <target>%d üzenet kihagyva</target> + <note>integrity error chat item</note> + </trans-unit> + <trans-unit id="%d weeks" xml:space="preserve"> + <source>%d weeks</source> + <target>%d hét</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="%lld" xml:space="preserve"> + <source>%lld</source> + <target>%lld</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld %@" xml:space="preserve"> + <source>%lld %@</source> + <target>%lld %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld contact(s) selected" xml:space="preserve"> + <source>%lld contact(s) selected</source> + <target>%lld partner kijelölve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve"> + <source>%lld file(s) with total size of %@</source> + <target>%lld fájl, %@ összméretben</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld group events" xml:space="preserve"> + <source>%lld group events</source> + <target>%lld csoportesemény</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld members" xml:space="preserve"> + <source>%lld members</source> + <target>%lld tag</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld messages blocked" xml:space="preserve"> + <source>%lld messages blocked</source> + <target>%lld üzenet letiltva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve"> + <source>%lld messages blocked by admin</source> + <target>%lld üzenetet letiltott az adminisztrátor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld messages marked deleted" xml:space="preserve"> + <source>%lld messages marked deleted</source> + <target>%lld üzenet megjelölve törlésre</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld messages moderated by %@" xml:space="preserve"> + <source>%lld messages moderated by %@</source> + <target>%@ %lld üzenetet moderált</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld minutes" xml:space="preserve"> + <source>%lld minutes</source> + <target>%lld perc</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve"> + <source>%lld new interface languages</source> + <target>%lld új kezelőfelületi nyelv</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld seconds" xml:space="preserve"> + <source>%lld seconds</source> + <target>%lld mp</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lldd" xml:space="preserve"> + <source>%lldd</source> + <target>%lldn</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lldh" xml:space="preserve"> + <source>%lldh</source> + <target>%lldó</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lldk" xml:space="preserve"> + <source>%lldk</source> + <target>%lldk</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lldm" xml:space="preserve"> + <source>%lldm</source> + <target>%lldp</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lldmth" xml:space="preserve"> + <source>%lldmth</source> + <target>%lldh</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%llds" xml:space="preserve"> + <source>%llds</source> + <target>%lldmp</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lldw" xml:space="preserve"> + <source>%lldw</source> + <target>%lldhét</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%u messages failed to decrypt." xml:space="preserve"> + <source>%u messages failed to decrypt.</source> + <target>Nem sikerült visszafejteni %u üzenetet.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%u messages skipped." xml:space="preserve"> + <source>%u messages skipped.</source> + <target>%u üzenet kihagyva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="(new)" xml:space="preserve"> + <source>(new)</source> + <target>(új)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="(this device v%@)" xml:space="preserve"> + <source>(this device v%@)</source> + <target>(ez az eszköz: v%@)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target>**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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve"> + <source>**Create group**: to create a new group.</source> + <target>**Csoport létrehozása:** új csoport létrehozásához.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> + <target>**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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> + <target>**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).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> + <target>**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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> + <source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source> + <target>**Megjegyzés:** NEM fogja tudni helyreállítani, vagy módosítani a jelmondatot abban az esetben, ha elveszíti.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> + <target>**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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve"> + <source>**Scan / Paste link**: to connect via a link you received.</source> + <target>**Hivatkozás beolvasása / beillesztése**: egy kapott hivatkozáson keresztüli kapcsolódáshoz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> + <source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source> + <target>**Figyelmeztetés:** Az azonnali push-értesítésekhez a kulcstartóban tárolt jelmondat megadása szükséges.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve"> + <source>**Warning**: the archive will be removed.</source> + <target>**Figyelmeztetés:** az archívum el lesz távolítva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> + <source>**e2e encrypted** audio call</source> + <target>**e2e titkosított** hanghívás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="**e2e encrypted** video call" xml:space="preserve"> + <source>**e2e encrypted** video call</source> + <target>**e2e titkosított** videóhívás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="*bold*" xml:space="preserve"> + <source>\*bold*</source> + <target>\*félkövér*</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> + <target>- kapcsolódás a [könyvtár szolgáltatáshoz](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)! +- kézbesítési jelentések (legfeljebb 20 tag). +- gyorsabb és stabilabb.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> + <source>- more stable message delivery. +- a bit better groups. +- and more!</source> + <target>- stabilabb üzenetkézbesítés. +- picit továbbfejlesztett csoportok. +- és még sok más!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="- optionally notify deleted contacts. - profile names with spaces. - and more!" xml:space="preserve"> + <source>- optionally notify deleted contacts. +- profile names with spaces. +- and more!</source> + <target>- partnerek értesítése a törlésről (nem kötelező) +- profilnevek szóközökkel +- és még sok más!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="- voice messages up to 5 minutes. - custom time to disappear. - editing history." xml:space="preserve"> + <source>- voice messages up to 5 minutes. +- custom time to disappear. +- editing history.</source> + <target>- legfeljebb 5 perc hosszúságú hangüzenetek. +- egyéni üzenet-eltűnési időkorlát. +- előzmények szerkesztése.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="0 sec" xml:space="preserve"> + <source>0 sec</source> + <target>0 mp</target> + <note>time to disappear</note> + </trans-unit> + <trans-unit id="0s" xml:space="preserve"> + <source>0s</source> + <target>0s</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="1 day" xml:space="preserve"> + <source>1 day</source> + <target>1 nap</target> + <note>delete after time +time interval</note> + </trans-unit> + <trans-unit id="1 hour" xml:space="preserve"> + <source>1 hour</source> + <target>1 óra</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="1 minute" xml:space="preserve"> + <source>1 minute</source> + <target>1 perc</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="1 month" xml:space="preserve"> + <source>1 month</source> + <target>1 hónap</target> + <note>delete after time +time interval</note> + </trans-unit> + <trans-unit id="1 week" xml:space="preserve"> + <source>1 week</source> + <target>1 hét</target> + <note>delete after time +time interval</note> + </trans-unit> + <trans-unit id="1 year" xml:space="preserve"> + <source>1 year</source> + <target>1 év</target> + <note>delete after time</note> + </trans-unit> + <trans-unit id="1-time link" xml:space="preserve"> + <source>1-time link</source> + <target>Egyszer használható meghívó</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> + <target>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ó.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="5 minutes" xml:space="preserve"> + <source>5 minutes</source> + <target>5 perc</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="6" xml:space="preserve"> + <source>6</source> + <target>6</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="30 seconds" xml:space="preserve"> + <source>30 seconds</source> + <target>30 másodperc</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> + <source><p>Hi!</p> +<p><a href="%@">Connect to me via SimpleX Chat</a></p></source> + <target><p>Üdvözlöm!</p> +<p><a href="%@">Csatlakozzon hozzám a SimpleX Chaten keresztül</a></p></target> + <note>email text</note> + </trans-unit> + <trans-unit id="A few more things" xml:space="preserve"> + <source>A few more things</source> + <target>Néhány további dolog</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="A new contact" xml:space="preserve"> + <source>A new contact</source> + <target>Egy új partner</target> + <note>notification title</note> + </trans-unit> + <trans-unit id="A new random profile will be shared." xml:space="preserve"> + <source>A new random profile will be shared.</source> + <target>Egy új, véletlenszerű profil lesz megosztva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> + <source>A separate TCP connection will be used **for each chat profile you have in the app**.</source> + <target>**Az összes csevegési profiljához az alkalmazásban** külön TCP-kapcsolat (és SOCKS-hitelesítőadat) lesz használva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." xml:space="preserve"> + <source>A separate TCP connection will be used **for each contact and group member**. +**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</source> + <target>**Az összes partneréhez és csoporttaghoz** külön TCP-kapcsolat (és SOCKS-hitelesítőadat) lesz használva. +**Megjegyzés:** ha sok kapcsolata van, az akkumulátor-használat és az adatforgalom jelentősen megnövekedhet, és néhány kapcsolódási kísérlet sikertelen lehet.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Abort" xml:space="preserve"> + <source>Abort</source> + <target>Megszakítás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Abort changing address" xml:space="preserve"> + <source>Abort changing address</source> + <target>Cím módosításának megszakítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Abort changing address?" xml:space="preserve"> + <source>Abort changing address?</source> + <target>Megszakítja a cím módosítását?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="About SimpleX Chat" xml:space="preserve"> + <source>About SimpleX Chat</source> + <target>A SimpleX Chat névjegye</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="About operators" xml:space="preserve"> + <source>About operators</source> + <target>Az üzemeltetőkről</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Accent" xml:space="preserve"> + <source>Accent</source> + <target>Kiemelőszín</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Accept" xml:space="preserve"> + <source>Accept</source> + <target>Elfogadás</target> + <note>accept contact request via notification +accept incoming call via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accept conditions" xml:space="preserve"> + <source>Accept conditions</source> + <target>Feltételek elfogadása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Accept connection request?" xml:space="preserve"> + <source>Accept connection request?</source> + <target>Elfogadja a meghívási kérést?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Accept contact request from %@?" xml:space="preserve"> + <source>Accept contact request from %@?</source> + <target>Elfogadja %@ meghívási kérését?</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="Accept incognito" xml:space="preserve"> + <source>Accept incognito</source> + <target>Elfogadás inkognitóban</target> + <note>accept contact request via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve"> + <source>Accepted conditions</source> + <target>Elfogadott feltételek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve"> + <source>Acknowledged</source> + <target>Visszaigazolt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve"> + <source>Acknowledgement errors</source> + <target>Visszaigazolási hibák</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Active" xml:space="preserve"> + <source>Active</source> + <target>Aktív</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve"> + <source>Active connections</source> + <target>Aktív kapcsolatok száma</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> + <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> + <target>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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add friends" xml:space="preserve"> + <source>Add friends</source> + <target>Barátok hozzáadása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve"> + <source>Add list</source> + <target>Lista hozzáadása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add profile" xml:space="preserve"> + <source>Add profile</source> + <target>Profil hozzáadása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> + <target>Kiszolgáló hozzáadása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> + <source>Add servers by scanning QR codes.</source> + <target>Kiszolgáló hozzáadása QR-kód beolvasásával.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add team members" xml:space="preserve"> + <source>Add team members</source> + <target>Munkatársak hozzáadása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add to another device" xml:space="preserve"> + <source>Add to another device</source> + <target>Hozzáadás egy másik eszközhöz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add to list" xml:space="preserve"> + <source>Add to list</source> + <target>Hozzáadás listához</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add welcome message" xml:space="preserve"> + <source>Add welcome message</source> + <target>Üdvözlőüzenet hozzáadása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve"> + <source>Add your team members to the conversations.</source> + <target>Adja hozzá a munkatársait a beszélgetésekhez.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve"> + <source>Added media & file servers</source> + <target>Hozzáadott média- és fájlkiszolgálók</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve"> + <source>Added message servers</source> + <target>Hozzáadott üzenetkiszolgálók</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve"> + <source>Additional accent</source> + <target>További kiemelőszín</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve"> + <source>Additional accent 2</source> + <target>További kiemelőszín 2</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve"> + <source>Additional secondary</source> + <target>További másodlagos szín</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address" xml:space="preserve"> + <source>Address</source> + <target>Cím</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve"> + <source>Address change will be aborted. Old receiving address will be used.</source> + <target>A cím módosítása meg fog szakadni. A régi fogadási cím lesz használva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve"> + <source>Address or 1-time link?</source> + <target>Cím vagy egyszer használható meghívó?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve"> + <source>Address settings</source> + <target>Címbeállítások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve"> + <source>Admins can block a member for all.</source> + <target>Az adminisztrátorok egy tagot a csoport összes tagja számára letilthatnak.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> + <source>Admins can create the links to join groups.</source> + <target>Az adminisztrátorok hivatkozásokat hozhatnak létre a csoportokhoz való csatlakozáshoz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Advanced network settings" xml:space="preserve"> + <source>Advanced network settings</source> + <target>Speciális hálózati beállítások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve"> + <source>Advanced settings</source> + <target>Speciális beállítások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All" xml:space="preserve"> + <source>All</source> + <target>Összes</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All app data is deleted." xml:space="preserve"> + <source>All app data is deleted.</source> + <target>Az összes alkalmazásadat törölve.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All chats and messages will be deleted - this cannot be undone!</source> + <target>Az összes csevegés és üzenet törölve lesz – ez a művelet nem vonható vissza!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <target>Az összes csevegés el lesz távolítva a(z) %@ nevű listáról, és a lista is törölve lesz.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="All data is erased when it is entered." xml:space="preserve"> + <source>All data is erased when it is entered.</source> + <target>A jelkód megadása után az összes adat törölve lesz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve"> + <source>All data is kept private on your device.</source> + <target>Az összes adat privát módon van tárolva az eszközén.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All group members will remain connected." xml:space="preserve"> + <source>All group members will remain connected.</source> + <target>Az összes csoporttag kapcsolatban marad.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <target>Az összes üzenet és fájl **végpontok közötti titkosítással**, a közvetlen üzenetek továbbá kvantumbiztos titkosítással is rendelkeznek.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone!</source> + <target>Az összes üzenet törölve lesz – ez a művelet nem vonható vissza!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> + <target>Az összes üzenet törölve lesz – ez a művelet nem vonható vissza! Az üzenetek CSAK az Ön számára törlődnek.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve"> + <source>All new messages from %@ will be hidden!</source> + <target>%@ összes új üzenete el lesz rejtve!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All profiles" xml:space="preserve"> + <source>All profiles</source> + <target>Összes profil</target> + <note>profile dropdown</note> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve"> + <source>All reports will be archived for you.</source> + <target>Az összes jelentés archiválva lesz az Ön számára.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All servers" xml:space="preserve"> + <source>All servers</source> + <target>Összes kiszolgáló</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All your contacts will remain connected." xml:space="preserve"> + <source>All your contacts will remain connected.</source> + <target>Az összes partnerével kapcsolatban marad.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve"> + <source>All your contacts will remain connected. Profile update will be sent to your contacts.</source> + <target>A partnereivel kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target>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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow" xml:space="preserve"> + <source>Allow</source> + <target>Engedélyezés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow calls only if your contact allows them." xml:space="preserve"> + <source>Allow calls only if your contact allows them.</source> + <target>A hívások kezdeményezése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve"> + <source>Allow calls?</source> + <target>Engedélyezi a hívásokat?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> + <source>Allow disappearing messages only if your contact allows it to you.</source> + <target>Az eltűnő üzenetek küldése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi az Ön számára.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow downgrade" xml:space="preserve"> + <source>Allow downgrade</source> + <target>Visszafejlesztés engedélyezése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target>Az üzenetek végleges törlése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. (24 óra)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> + <source>Allow message reactions only if your contact allows them.</source> + <target>A reakciók hozzáadása az üzenetekhez csak abban az esetben van engedélyezve, ha a partnere is engedélyezi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow message reactions." xml:space="preserve"> + <source>Allow message reactions.</source> + <target>A reakciók hozzáadása az üzenetekhez engedélyezve van.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow sending direct messages to members." xml:space="preserve"> + <source>Allow sending direct messages to members.</source> + <target>A közvetlen üzenetek küldése a tagok között engedélyezve van.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow sending disappearing messages." xml:space="preserve"> + <source>Allow sending disappearing messages.</source> + <target>Az eltűnő üzenetek küldése engedélyezve van.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow sharing" xml:space="preserve"> + <source>Allow sharing</source> + <target>Megosztás engedélyezése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target>Az elküldött üzenetek végleges törlése engedélyezve van. (24 óra)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve"> + <source>Allow to report messsages to moderators.</source> + <target>Az üzenetek jelentése a moderátorok felé engedélyezve van.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve"> + <source>Allow to send SimpleX links.</source> + <target>A SimpleX-hivatkozások küldése engedélyezve van.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send files and media." xml:space="preserve"> + <source>Allow to send files and media.</source> + <target>A fájlok- és a médiatartalmak küldése engedélyezve van.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send voice messages." xml:space="preserve"> + <source>Allow to send voice messages.</source> + <target>A hangüzenetek küldése engedélyezve van.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve"> + <source>Allow voice messages only if your contact allows them.</source> + <target>A hangüzenetek küldése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow voice messages?" xml:space="preserve"> + <source>Allow voice messages?</source> + <target>Engedélyezi a hangüzeneteket?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow your contacts adding message reactions." xml:space="preserve"> + <source>Allow your contacts adding message reactions.</source> + <target>A reakciók hozzáadása az üzenetekhez engedélyezve van a partnerei számára.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow your contacts to call you." xml:space="preserve"> + <source>Allow your contacts to call you.</source> + <target>A hívások kezdeményezése engedélyezve van a partnerei számára.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target>Az elküldött üzenetek végleges törlése engedélyezve van a partnerei számára. (24 óra)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> + <source>Allow your contacts to send disappearing messages.</source> + <target>Az eltűnő üzenetek küldésének engedélyezése a partnerei számára.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow your contacts to send voice messages." xml:space="preserve"> + <source>Allow your contacts to send voice messages.</source> + <target>A hangüzenetek küldése engedélyezve van a partnerei számára.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Already connected?" xml:space="preserve"> + <source>Already connected?</source> + <target>Már kapcsolódott?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Already connecting!" xml:space="preserve"> + <source>Already connecting!</source> + <target>Kapcsolódás folyamatban!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Already joining the group!" xml:space="preserve"> + <source>Already joining the group!</source> + <target>A csatlakozás folyamatban van a csoporthoz!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve"> + <source>Always use private routing.</source> + <target>Mindig használjon privát útválasztást.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Always use relay" xml:space="preserve"> + <source>Always use relay</source> + <target>Mindig használjon továbbítókiszolgálót</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="An empty chat profile with the provided name is created, and the app opens as usual." xml:space="preserve"> + <source>An empty chat profile with the provided name is created, and the app opens as usual.</source> + <target>Egy üres csevegési profil lesz létrehozva a megadott névvel, és az alkalmazás a szokásos módon megnyílik.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Another reason" xml:space="preserve"> + <source>Another reason</source> + <target>Egyéb indoklás</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Answer call" xml:space="preserve"> + <source>Answer call</source> + <target>Hívás fogadása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> + <target>Bárki üzemeltethet kiszolgálókat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App build: %@" xml:space="preserve"> + <source>App build: %@</source> + <target>Az alkalmazás összeállítási száma: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App data migration" xml:space="preserve"> + <source>App data migration</source> + <target>Alkalmazásadatok átköltöztetése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> + <source>App encrypts new local files (except videos).</source> + <target>Az alkalmazás titkosítja a helyi fájlokat (a videók kivételével).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App group:" xml:space="preserve"> + <source>App group:</source> + <target>Alkalmazáscsoport:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App icon" xml:space="preserve"> + <source>App icon</source> + <target>Alkalmazásikon</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App passcode" xml:space="preserve"> + <source>App passcode</source> + <target>Alkalmazás jelkód</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App passcode is replaced with self-destruct passcode." xml:space="preserve"> + <source>App passcode is replaced with self-destruct passcode.</source> + <target>Az alkalmazás-jelkód helyettesítve lesz egy önmegsemmisítő-jelkóddal.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App session" xml:space="preserve"> + <source>App session</source> + <target>Alkalmazás munkamenete</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App version" xml:space="preserve"> + <source>App version</source> + <target>Az alkalmazás verziója</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App version: v%@" xml:space="preserve"> + <source>App version: v%@</source> + <target>Az alkalmazás verziója: v%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Appearance" xml:space="preserve"> + <source>Appearance</source> + <target>Megjelenés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply" xml:space="preserve"> + <source>Apply</source> + <target>Alkalmaz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve"> + <source>Apply to</source> + <target>Alkalmazás erre</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve"> + <source>Archive</source> + <target>Archívum</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve"> + <source>Archive %lld reports?</source> + <target>Archivál %lld jelentést?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve"> + <source>Archive all reports?</source> + <target>Archiválja az összes jelentést?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve"> + <source>Archive and upload</source> + <target>Archiválás és feltöltés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve"> + <source>Archive contacts to chat later.</source> + <target>A partnerek archiválása a későbbi csevegéshez.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve"> + <source>Archive report</source> + <target>Jelentés archiválása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve"> + <source>Archive report?</source> + <target>Archiválja a jelentést?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve"> + <source>Archive reports</source> + <target>Jelentések archiválása</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve"> + <source>Archived contacts</source> + <target>Archivált partnerek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve"> + <source>Archiving database</source> + <target>Adatbázis archiválása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Attach" xml:space="preserve"> + <source>Attach</source> + <target>Mellékelés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Audio & video calls" xml:space="preserve"> + <source>Audio & video calls</source> + <target>Hang- és videóhívások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Audio and video calls" xml:space="preserve"> + <source>Audio and video calls</source> + <target>Hang- és videóhívások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Audio/video calls" xml:space="preserve"> + <source>Audio/video calls</source> + <target>Hang- és videóhívások</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Audio/video calls are prohibited." xml:space="preserve"> + <source>Audio/video calls are prohibited.</source> + <target>A hívások kezdeményezése le van tiltva ebben a csevegésben.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Authentication cancelled" xml:space="preserve"> + <source>Authentication cancelled</source> + <target>Hitelesítés visszavonva</target> + <note>PIN entry</note> + </trans-unit> + <trans-unit id="Authentication failed" xml:space="preserve"> + <source>Authentication failed</source> + <target>Sikertelen hitelesítés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Authentication is required before the call is connected, but you may miss calls." xml:space="preserve"> + <source>Authentication is required before the call is connected, but you may miss calls.</source> + <target>A hívás összekapcsolása előtt hitelesítésre van szükség, de előfordulhat, hogy lemarad a hívásokról.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Authentication unavailable" xml:space="preserve"> + <source>Authentication unavailable</source> + <target>Hitelesítés elérhetetlen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Auto-accept" xml:space="preserve"> + <source>Auto-accept</source> + <target>Automatikus elfogadás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Auto-accept contact requests" xml:space="preserve"> + <source>Auto-accept contact requests</source> + <target>Meghívási kérések automatikus elfogadása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Auto-accept images" xml:space="preserve"> + <source>Auto-accept images</source> + <target>Képek automatikus elfogadása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve"> + <source>Auto-accept settings</source> + <target>Beállítások automatikus elfogadása</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Back" xml:space="preserve"> + <source>Back</source> + <target>Vissza</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Background" xml:space="preserve"> + <source>Background</source> + <target>Háttér</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Bad desktop address" xml:space="preserve"> + <source>Bad desktop address</source> + <target>Érvénytelen számítógépcím</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Bad message ID" xml:space="preserve"> + <source>Bad message ID</source> + <target>Téves üzenet ID</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Bad message hash" xml:space="preserve"> + <source>Bad message hash</source> + <target>Érvénytelen az üzenet hasítóértéke</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better calls" xml:space="preserve"> + <source>Better calls</source> + <target>Továbbfejlesztett hívásélmény</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better groups" xml:space="preserve"> + <source>Better groups</source> + <target>Továbbfejlesztett csoportok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve"> + <source>Better groups performance</source> + <target>Továbbfejlesztett, gyorsabb csoportok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve"> + <source>Better message dates.</source> + <target>Továbbfejlesztett üzenetdátumok.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better messages" xml:space="preserve"> + <source>Better messages</source> + <target>Továbbfejlesztett üzenetek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better networking" xml:space="preserve"> + <source>Better networking</source> + <target>Jobb hálózatkezelés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve"> + <source>Better notifications</source> + <target>Továbbfejlesztett értesítések</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve"> + <source>Better privacy and security</source> + <target>Továbbfejlesztett adatvédelem és biztonság</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve"> + <source>Better security ✅</source> + <target>Továbbfejlesztett biztonság ✅</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve"> + <source>Better user experience</source> + <target>Továbbfejlesztett felhasználói élmény</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Black" xml:space="preserve"> + <source>Black</source> + <target>Fekete</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block" xml:space="preserve"> + <source>Block</source> + <target>Letiltás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block for all" xml:space="preserve"> + <source>Block for all</source> + <target>Letiltás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block group members" xml:space="preserve"> + <source>Block group members</source> + <target>Csoporttagok letiltása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block member" xml:space="preserve"> + <source>Block member</source> + <target>Letiltás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve"> + <source>Block member for all?</source> + <target>Az összes tag számára letiltja a tagot?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block member?" xml:space="preserve"> + <source>Block member?</source> + <target>Letiltja a tagot?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve"> + <source>Blocked by admin</source> + <target>Letiltva az adminisztrátor által</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve"> + <source>Blur for better privacy.</source> + <target>Elhomályosítás a jobb adatvédelemért.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve"> + <source>Blur media</source> + <target>Médiatartalom elhomályosítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> + <source>Both you and your contact can add message reactions.</source> + <target>Mindkét fél hozzáadhat az üzenetekhez reakciókat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target>Mindkét fél véglegesen törölheti az elküldött üzeneteket. (24 óra)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> + <source>Both you and your contact can make calls.</source> + <target>Mindkét fél tud hívásokat kezdeményezni.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Both you and your contact can send disappearing messages." xml:space="preserve"> + <source>Both you and your contact can send disappearing messages.</source> + <target>Mindkét fél küldhet eltűnő üzeneteket.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Both you and your contact can send voice messages." xml:space="preserve"> + <source>Both you and your contact can send voice messages.</source> + <target>Mindkét fél küldhet hangüzeneteket.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target>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)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business address" xml:space="preserve"> + <source>Business address</source> + <target>Üzleti cím</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve"> + <source>Business chats</source> + <target>Üzleti csevegések</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve"> + <source>Businesses</source> + <target>Üzleti</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> + <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> + <target>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).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve"> + <source>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</source> + <target>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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Call already ended!" xml:space="preserve"> + <source>Call already ended!</source> + <target>A hívás már befejeződött!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Calls" xml:space="preserve"> + <source>Calls</source> + <target>Hívások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Calls prohibited!" xml:space="preserve"> + <source>Calls prohibited!</source> + <target>A hívások le vannak tiltva!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve"> + <source>Camera not available</source> + <target>A kamera nem elérhető</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve"> + <source>Can't call contact</source> + <target>Nem lehet felhívni a partnert</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve"> + <source>Can't call member</source> + <target>Nem lehet felhívni a tagot</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't invite contact!" xml:space="preserve"> + <source>Can't invite contact!</source> + <target>Nem lehet meghívni a partnert!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't invite contacts!" xml:space="preserve"> + <source>Can't invite contacts!</source> + <target>Nem lehet meghívni a partnereket!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve"> + <source>Can't message member</source> + <target>Nem lehet üzenetet küldeni a tagnak</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <target>Mégse</target> + <note>alert action +alert button</note> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve"> + <source>Cancel migration</source> + <target>Átköltöztetés visszavonása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <target>Nem lehet hozzáférni a kulcstartóhoz az adatbázisjelszó mentéséhez</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Nem lehet továbbítani az üzenetet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot receive file" xml:space="preserve"> + <source>Cannot receive file</source> + <target>Nem lehet fogadni a fájlt</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <target>Kapacitás túllépés – a címzett nem kapta meg a korábban elküldött üzeneteket.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve"> + <source>Cellular</source> + <target>Mobilhálózat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Change" xml:space="preserve"> + <source>Change</source> + <target>Módosítás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve"> + <source>Change automatic message deletion?</source> + <target>Módosítja az automatikus üzenettörlést?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve"> + <source>Change chat profiles</source> + <target>Csevegési profilok módosítása</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Change database passphrase?" xml:space="preserve"> + <source>Change database passphrase?</source> + <target>Módosítja az adatbázis jelmondatát?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Change lock mode" xml:space="preserve"> + <source>Change lock mode</source> + <target>Zárolási mód módosítása</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Change member role?" xml:space="preserve"> + <source>Change member role?</source> + <target>Módosítja a tag szerepkörét?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Change passcode" xml:space="preserve"> + <source>Change passcode</source> + <target>Jelkód módosítása</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Change receiving address" xml:space="preserve"> + <source>Change receiving address</source> + <target>Fogadási cím módosítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Change receiving address?" xml:space="preserve"> + <source>Change receiving address?</source> + <target>Módosítja a fogadási címet?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Change role" xml:space="preserve"> + <source>Change role</source> + <target>Szerepkör módosítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Change self-destruct mode" xml:space="preserve"> + <source>Change self-destruct mode</source> + <target>Önmegsemmisítő-mód módosítása</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Change self-destruct passcode" xml:space="preserve"> + <source>Change self-destruct passcode</source> + <target>Önmegsemmisítő-jelkód módosítása</target> + <note>authentication reason +set passcode view</note> + </trans-unit> + <trans-unit id="Chat" xml:space="preserve"> + <source>Chat</source> + <target>Csevegés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve"> + <source>Chat already exists</source> + <target>A csevegés már létezik</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve"> + <source>Chat already exists!</source> + <target>A csevegés már létezik!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve"> + <source>Chat colors</source> + <target>Csevegés színei</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat console" xml:space="preserve"> + <source>Chat console</source> + <target>Csevegési konzol</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat database" xml:space="preserve"> + <source>Chat database</source> + <target>Csevegési adatbázis</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat database deleted" xml:space="preserve"> + <source>Chat database deleted</source> + <target>Csevegési adatbázis törölve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve"> + <source>Chat database exported</source> + <target>Csevegési adatbázis exportálva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat database imported" xml:space="preserve"> + <source>Chat database imported</source> + <target>Csevegési adatbázis importálva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat is running" xml:space="preserve"> + <source>Chat is running</source> + <target>A csevegés fut</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat is stopped" xml:space="preserve"> + <source>Chat is stopped</source> + <target>A csevegés megállt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <target>A csevegés megállt. Ha már használta ezt az adatbázist egy másik eszközön, úgy visszaállítás szükséges a csevegés elindítása előtt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve"> + <source>Chat list</source> + <target>Csevegési lista</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve"> + <source>Chat migrated!</source> + <target>A csevegés átköltöztetve!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat preferences" xml:space="preserve"> + <source>Chat preferences</source> + <target>Csevegési beállítások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve"> + <source>Chat preferences were changed.</source> + <target>A csevegési beállítások módosultak.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve"> + <source>Chat profile</source> + <target>Csevegési profil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve"> + <source>Chat theme</source> + <target>Csevegés témája</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <target>A csevegés minden tag számára törölve lesz – ez a művelet nem vonható vissza!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <target>A csevegés törölve lesz az Ön számára – ez a művelet nem vonható vissza!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chats" xml:space="preserve"> + <source>Chats</source> + <target>Csevegések</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve"> + <source>Check messages every 20 min.</source> + <target>Üzenetek ellenőrzése 20 percenként.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve"> + <source>Check messages when allowed.</source> + <target>Üzenetek ellenőrzése, amikor engedélyezett.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check server address and try again." xml:space="preserve"> + <source>Check server address and try again.</source> + <target>Kiszolgáló címének ellenőrzése és újrapróbálkozás.</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> + <source>Chinese and Spanish interface</source> + <target>Kínai és spanyol kezelőfelület</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <target>Válassza az _Átköltöztetés egy másik eszközről_ opciót az új eszközén és olvassa be a QR-kódot.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Choose file" xml:space="preserve"> + <source>Choose file</source> + <target>Fájl kiválasztása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Choose from library" xml:space="preserve"> + <source>Choose from library</source> + <target>Választás a könyvtárból</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve"> + <source>Chunks deleted</source> + <target>Törölt töredékek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve"> + <source>Chunks downloaded</source> + <target>Letöltött töredékek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve"> + <source>Chunks uploaded</source> + <target>Feltöltött töredékek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear" xml:space="preserve"> + <source>Clear</source> + <target>Kiürítés</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Clear conversation" xml:space="preserve"> + <source>Clear conversation</source> + <target>Üzenetek kiürítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear conversation?" xml:space="preserve"> + <source>Clear conversation?</source> + <target>Kiüríti az üzeneteket?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve"> + <source>Clear group?</source> + <target>Kiüríti a csoportot?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve"> + <source>Clear or delete group?</source> + <target>Csoport kiürítése vagy törlése?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve"> + <source>Clear private notes?</source> + <target>Kiüríti a privát jegyzeteket?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear verification" xml:space="preserve"> + <source>Clear verification</source> + <target>Hitelesítés törlése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Color chats with the new themes." xml:space="preserve"> + <source>Color chats with the new themes.</source> + <target>Csevegések színezése új témákkal.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Color mode" xml:space="preserve"> + <source>Color mode</source> + <target>Színmód</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve"> + <source>Community guidelines violation</source> + <target>Közösségi irányelvek megsértése</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Compare file" xml:space="preserve"> + <source>Compare file</source> + <target>Fájl-összehasonlítás</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Compare security codes with your contacts." xml:space="preserve"> + <source>Compare security codes with your contacts.</source> + <target>Biztonsági kódok összehasonlítása a partnerekével.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Completed" xml:space="preserve"> + <source>Completed</source> + <target>Elkészült</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve"> + <source>Conditions accepted on: %@.</source> + <target>Feltételek elfogadásának ideje: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <target>A következő üzemeltető(k) számára elfogadott feltételek: **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <target>A feltételek már el lettek fogadva a következő üzemeltető(k) számára: **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve"> + <source>Conditions of use</source> + <target>Használati feltételek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <target>A feltételek el lesznek fogadva a következő üzemeltető(k) számára: **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve"> + <source>Conditions will be accepted on: %@.</source> + <target>A feltételek el lesznek fogadva a következő időpontban: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <target>A feltételek automatikusan el lesznek fogadva az engedélyezett üzemeltetők számára a következő időpontban: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Configure ICE servers" xml:space="preserve"> + <source>Configure ICE servers</source> + <target>ICE-kiszolgálók beállítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Configure server operators" xml:space="preserve"> + <source>Configure server operators</source> + <target>Kiszolgálóüzemeltetők beállítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm" xml:space="preserve"> + <source>Confirm</source> + <target>Megerősítés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm Passcode" xml:space="preserve"> + <source>Confirm Passcode</source> + <target>Jelkód megerősítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve"> + <source>Confirm contact deletion?</source> + <target>Biztosan törli a partnert?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm database upgrades" xml:space="preserve"> + <source>Confirm database upgrades</source> + <target>Adatbázis fejlesztésének megerősítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve"> + <source>Confirm files from unknown servers.</source> + <target>Ismeretlen kiszolgálókról származó fájlok megerősítése.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve"> + <source>Confirm network settings</source> + <target>Hálózati beállítások megerősítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm new passphrase…" xml:space="preserve"> + <source>Confirm new passphrase…</source> + <target>Új jelmondat megerősítése…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm password" xml:space="preserve"> + <source>Confirm password</source> + <target>Jelszó megerősítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <target>Az átköltöztetéshez erősítse meg, hogy emlékszik az adatbázis jelmondatára.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve"> + <source>Confirm upload</source> + <target>Feltöltés megerősítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve"> + <source>Confirmed</source> + <target>Megerősítve</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Connect" xml:space="preserve"> + <source>Connect</source> + <target>Kapcsolódás</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Connect automatically" xml:space="preserve"> + <source>Connect automatically</source> + <target>Kapcsolódás automatikusan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect incognito" xml:space="preserve"> + <source>Connect incognito</source> + <target>Kapcsolódás inkognitóban</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect to desktop" xml:space="preserve"> + <source>Connect to desktop</source> + <target>Társítás számítógéppel</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve"> + <source>Connect to your friends faster.</source> + <target>Kapcsolódjon gyorsabban a partnereihez.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect to yourself?" xml:space="preserve"> + <source>Connect to yourself?</source> + <target>Kapcsolódik saját magához?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect to yourself? This is your own SimpleX address!" xml:space="preserve"> + <source>Connect to yourself? +This is your own SimpleX address!</source> + <target>Kapcsolódik saját magához? +Ez a saját SimpleX-címe!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect to yourself? This is your own one-time link!" xml:space="preserve"> + <source>Connect to yourself? +This is your own one-time link!</source> + <target>Kapcsolódik saját magához? +Ez a saját egyszer használható meghívója!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via contact address" xml:space="preserve"> + <source>Connect via contact address</source> + <target>Kapcsolódás a kapcsolattartási címen keresztül</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via link" xml:space="preserve"> + <source>Connect via link</source> + <target>Kapcsolódás egy hivatkozáson keresztül</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via one-time link" xml:space="preserve"> + <source>Connect via one-time link</source> + <target>Kapcsolódás egyszer használható meghívón keresztül</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect with %@" xml:space="preserve"> + <source>Connect with %@</source> + <target>Kapcsolódás a következővel: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connected" xml:space="preserve"> + <source>Connected</source> + <target>Kapcsolódott</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connected desktop" xml:space="preserve"> + <source>Connected desktop</source> + <target>Társított számítógép</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve"> + <source>Connected servers</source> + <target>Kapcsolódott kiszolgálók</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connected to desktop" xml:space="preserve"> + <source>Connected to desktop</source> + <target>Kapcsolódva a számítógéphez</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connecting" xml:space="preserve"> + <source>Connecting</source> + <target>Kapcsolódás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connecting server…" xml:space="preserve"> + <source>Connecting to server…</source> + <target>Kapcsolódás a kiszolgálóhoz…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connecting server… (error: %@)" xml:space="preserve"> + <source>Connecting to server… (error: %@)</source> + <target>Kapcsolódás a kiszolgálóhoz… (hiba: %@)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve"> + <source>Connecting to contact, please wait or check later!</source> + <target>Kapcsolódás a partnerhez, várjon vagy ellenőrizze később!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connecting to desktop" xml:space="preserve"> + <source>Connecting to desktop</source> + <target>Kapcsolódás a számítógéphez</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection" xml:space="preserve"> + <source>Connection</source> + <target>Kapcsolat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve"> + <source>Connection and servers status.</source> + <target>Kapcsolatok- és kiszolgálók állapotának megjelenítése.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve"> + <source>Connection blocked</source> + <target>A kapcsolat le van tiltva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection error" xml:space="preserve"> + <source>Connection error</source> + <target>Kapcsolódási hiba</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection error (AUTH)" xml:space="preserve"> + <source>Connection error (AUTH)</source> + <target>Kapcsolódási hiba (AUTH)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve"> + <source>Connection is blocked by server operator: +%@</source> + <target>A kiszolgáló üzemeltetője letiltotta a kapcsolatot: +%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve"> + <source>Connection not ready.</source> + <target>A kapcsolat nem áll készen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve"> + <source>Connection notifications</source> + <target>Kapcsolódási értesítések</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection request sent!" xml:space="preserve"> + <source>Connection request sent!</source> + <target>Meghívási kérés elküldve!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve"> + <source>Connection requires encryption renegotiation.</source> + <target>A kapcsolat titkosítása újraegyeztetést igényel.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve"> + <source>Connection security</source> + <target>Kapcsolatbiztonság</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection terminated" xml:space="preserve"> + <source>Connection terminated</source> + <target>Kapcsolat megszakítva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection timeout" xml:space="preserve"> + <source>Connection timeout</source> + <target>Időtúllépés kapcsolódáskor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve"> + <source>Connection with desktop stopped</source> + <target>A kapcsolat a számítógéppel megszakadt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve"> + <source>Connections</source> + <target>Kapcsolatok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contact allows" xml:space="preserve"> + <source>Contact allows</source> + <target>Partner engedélyezi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contact already exists" xml:space="preserve"> + <source>Contact already exists</source> + <target>A partner már létezik</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contact deleted!" xml:space="preserve"> + <source>Contact deleted!</source> + <target>Partner törölve!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contact hidden:" xml:space="preserve"> + <source>Contact hidden:</source> + <target>Rejtett név:</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Contact is connected" xml:space="preserve"> + <source>Contact is connected</source> + <target>Partnere kapcsolódott</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Contact is deleted." xml:space="preserve"> + <source>Contact is deleted.</source> + <target>Törölt partner.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contact name" xml:space="preserve"> + <source>Contact name</source> + <target>Csak név</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contact preferences" xml:space="preserve"> + <source>Contact preferences</source> + <target>Partnerbeállítások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve"> + <source>Contact will be deleted - this cannot be undone!</source> + <target>A partner törölve lesz – ez a művelet nem vonható vissza!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contacts" xml:space="preserve"> + <source>Contacts</source> + <target>Partnerek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve"> + <source>Contacts can mark messages for deletion; you will be able to view them.</source> + <target>A partnerei törlésre jelölhetnek üzeneteket; Ön majd meg tudja nézni azokat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve"> + <source>Content violates conditions of use</source> + <target>A tartalom sérti a használati feltételeket</target> + <note>blocking reason</note> + </trans-unit> + <trans-unit id="Continue" xml:space="preserve"> + <source>Continue</source> + <target>Folytatás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve"> + <source>Conversation deleted!</source> + <target>Beszélgetés törölve!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy" xml:space="preserve"> + <source>Copy</source> + <target>Másolás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve"> + <source>Copy error</source> + <target>Másolási hiba</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Core version: v%@" xml:space="preserve"> + <source>Core version: v%@</source> + <target>Fő verzió: v%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Corner" xml:space="preserve"> + <source>Corner</source> + <target>Sarok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Correct name to %@?" xml:space="preserve"> + <source>Correct name to %@?</source> + <target>Helyesbíti a nevet a következőre: %@?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create" xml:space="preserve"> + <source>Create</source> + <target>Létrehozás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve"> + <source>Create 1-time link</source> + <target>Egyszer használható meghívó létrehozása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create SimpleX address" xml:space="preserve"> + <source>Create SimpleX address</source> + <target>SimpleX-cím létrehozása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create a group using a random profile." xml:space="preserve"> + <source>Create a group using a random profile.</source> + <target>Csoport létrehozása véletlenszerű profillal.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create file" xml:space="preserve"> + <source>Create file</source> + <target>Fájl létrehozása</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Create group" xml:space="preserve"> + <source>Create group</source> + <target>Csoport létrehozása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create group link" xml:space="preserve"> + <source>Create group link</source> + <target>Csoporthivatkozás létrehozása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create link" xml:space="preserve"> + <source>Create link</source> + <target>Hivatkozás létrehozása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create list" xml:space="preserve"> + <source>Create list</source> + <target>Lista létrehozása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <target>Új profil létrehozása a [számítógép-alkalmazásban](https://simplex.chat/downloads/). 💻</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create profile" xml:space="preserve"> + <source>Create profile</source> + <target>Profil létrehozása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create queue" xml:space="preserve"> + <source>Create queue</source> + <target>Sorba állítás létrehozása</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Create secret group" xml:space="preserve"> + <source>Create secret group</source> + <target>Titkos csoport létrehozása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create your profile" xml:space="preserve"> + <source>Create your profile</source> + <target>Saját profil létrehozása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created" xml:space="preserve"> + <source>Created</source> + <target>Létrehozva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve"> + <source>Created at</source> + <target>Létrehozva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve"> + <source>Created at: %@</source> + <target>Létrehozva: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve"> + <source>Creating archive link</source> + <target>Archívum hivatkozás létrehozása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve"> + <source>Creating link…</source> + <target>Hivatkozás létrehozása…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Current Passcode" xml:space="preserve"> + <source>Current Passcode</source> + <target>Jelenlegi jelkód</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <target>A jelenlegi feltételek szövegét nem lehetett betölteni, a feltételeket a következő hivatkozáson keresztül vizsgálhatja felül:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Current passphrase…" xml:space="preserve"> + <source>Current passphrase…</source> + <target>Jelenlegi jelmondat…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Current profile" xml:space="preserve"> + <source>Current profile</source> + <target>Jelenlegi profil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <target>Jelenleg támogatott legnagyobb fájl méret: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Custom time" xml:space="preserve"> + <source>Custom time</source> + <target>Egyéni időköz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve"> + <source>Customizable message shape.</source> + <target>Személyre szabható üzenetbuborékok.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve"> + <source>Customize theme</source> + <target>Téma személyre szabása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Dark" xml:space="preserve"> + <source>Dark</source> + <target>Sötét</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve"> + <source>Dark mode colors</source> + <target>Sötét mód színei</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database ID" xml:space="preserve"> + <source>Database ID</source> + <target>Adatbázis-azonosító</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database ID: %d" xml:space="preserve"> + <source>Database ID: %d</source> + <target>Adatbázis-azonosító: %d</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Database IDs and Transport isolation option." xml:space="preserve"> + <source>Database IDs and Transport isolation option.</source> + <target>Adatbázis-azonosítók és átvitel-izolációs beállítások.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade" xml:space="preserve"> + <source>Database downgrade</source> + <target>Adatbázis visszafejlesztése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <target>Adatbázis titkosítva!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encryption passphrase will be updated and stored in the keychain. " xml:space="preserve"> + <source>Database encryption passphrase will be updated and stored in the keychain. +</source> + <target>Az adatbázis titkosítási jelmondata frissülni fog és a kulcstartóban lesz tárolva. +</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encryption passphrase will be updated. " xml:space="preserve"> + <source>Database encryption passphrase will be updated. +</source> + <target>Az adatbázis titkosítási jelmondata frissítve lesz. +</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <target>Adatbázishiba</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database is encrypted using a random passphrase, you can change it." xml:space="preserve"> + <source>Database is encrypted using a random passphrase, you can change it.</source> + <target>Az adatbázis egy véletlenszerű jelmondattal van titkosítva, amelyet szabadon módosíthat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database is encrypted using a random passphrase. Please change it before exporting." xml:space="preserve"> + <source>Database is encrypted using a random passphrase. Please change it before exporting.</source> + <target>Az adatbázis egy véletlenszerű jelmondattal van titkosítva. Exportálás előtt módosítsa.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase" xml:space="preserve"> + <source>Database passphrase</source> + <target>Adatbázis-jelmondat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase & export" xml:space="preserve"> + <source>Database passphrase & export</source> + <target>Adatbázis-jelmondat és -exportálás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <target>Az adatbázis jelmondata nem egyezik a kulcstartóba mentettől.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <target>A csevegés megnyitásához adja meg az adatbázis jelmondatát.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade" xml:space="preserve"> + <source>Database upgrade</source> + <target>Adatbázis fejlesztése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database will be encrypted and the passphrase stored in the keychain. " xml:space="preserve"> + <source>Database will be encrypted and the passphrase stored in the keychain. +</source> + <target>Az adatbázis titkosítva lesz, a jelmondat pedig a kulcstartóban lesz tárolva. +</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database will be encrypted. " xml:space="preserve"> + <source>Database will be encrypted. +</source> + <target>Az adatbázis titkosítva lesz. +</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database will be migrated when the app restarts" xml:space="preserve"> + <source>Database will be migrated when the app restarts</source> + <target>Az adatbázis az alkalmazás újraindításakor lesz átköltöztetve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Debug delivery" xml:space="preserve"> + <source>Debug delivery</source> + <target>Kézbesítési hibák felderítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Decentralized" xml:space="preserve"> + <source>Decentralized</source> + <target>Decentralizált</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Decryption error" xml:space="preserve"> + <source>Decryption error</source> + <target>Titkosítás visszafejtési hiba</target> + <note>message decrypt error item</note> + </trans-unit> + <trans-unit id="Delete" xml:space="preserve"> + <source>Delete</source> + <target>Törlés</target> + <note>alert action +swipe action</note> + </trans-unit> + <trans-unit id="Delete %lld messages of members?" xml:space="preserve"> + <source>Delete %lld messages of members?</source> + <target>Törli a tagok %lld üzenetét?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete %lld messages?" xml:space="preserve"> + <source>Delete %lld messages?</source> + <target>Töröl %lld üzenetet?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete address" xml:space="preserve"> + <source>Delete address</source> + <target>Cím törlése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete address?" xml:space="preserve"> + <source>Delete address?</source> + <target>Törli a címet?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete after" xml:space="preserve"> + <source>Delete after</source> + <target>Törlés ennyi idő után</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete all files" xml:space="preserve"> + <source>Delete all files</source> + <target>Az összes fájl törlése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete and notify contact" xml:space="preserve"> + <source>Delete and notify contact</source> + <target>Törlés, és a partner értesítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete chat" xml:space="preserve"> + <source>Delete chat</source> + <target>Csevegés törlése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete chat messages from your device." xml:space="preserve"> + <source>Delete chat messages from your device.</source> + <target>Csevegési üzenetek törlése a saját eszközéről.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete chat profile" xml:space="preserve"> + <source>Delete chat profile</source> + <target>Csevegési profil törlése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete chat profile?" xml:space="preserve"> + <source>Delete chat profile?</source> + <target>Törli a csevegési profilt?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete chat?" xml:space="preserve"> + <source>Delete chat?</source> + <target>Törli a csevegést?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete connection" xml:space="preserve"> + <source>Delete connection</source> + <target>Kapcsolat törlése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete contact" xml:space="preserve"> + <source>Delete contact</source> + <target>Partner törlése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete contact?" xml:space="preserve"> + <source>Delete contact?</source> + <target>Törli a partnert?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete database" xml:space="preserve"> + <source>Delete database</source> + <target>Adatbázis törlése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete database from this device" xml:space="preserve"> + <source>Delete database from this device</source> + <target>Adatbázis törlése erről az eszközről</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete file" xml:space="preserve"> + <source>Delete file</source> + <target>Fájl törlése</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Delete files and media?" xml:space="preserve"> + <source>Delete files and media?</source> + <target>Törli a fájl- és a médiatartalmakat?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete files for all chat profiles" xml:space="preserve"> + <source>Delete files for all chat profiles</source> + <target>Fájlok törlése az összes csevegési profilból</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete for everyone" xml:space="preserve"> + <source>Delete for everyone</source> + <target>Törlés az összes tagnál</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Delete for me" xml:space="preserve"> + <source>Delete for me</source> + <target>Csak nálam</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete group" xml:space="preserve"> + <source>Delete group</source> + <target>Csoport törlése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete group?" xml:space="preserve"> + <source>Delete group?</source> + <target>Törli a csoportot?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete invitation" xml:space="preserve"> + <source>Delete invitation</source> + <target>Meghívó törlése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete link" xml:space="preserve"> + <source>Delete link</source> + <target>Törlés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete link?" xml:space="preserve"> + <source>Delete link?</source> + <target>Törli a hivatkozást?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete list?" xml:space="preserve"> + <source>Delete list?</source> + <target>Törli a listát?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Delete member message?" xml:space="preserve"> + <source>Delete member message?</source> + <target>Törli a tag üzenetét?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete message?" xml:space="preserve"> + <source>Delete message?</source> + <target>Törli az üzenetet?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete messages" xml:space="preserve"> + <source>Delete messages</source> + <target>Üzenetek törlése</target> + <note>alert button</note> + </trans-unit> + <trans-unit id="Delete messages after" xml:space="preserve"> + <source>Delete messages after</source> + <target>Üzenetek törlése ennyi idő után</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete old database" xml:space="preserve"> + <source>Delete old database</source> + <target>Régi adatbázis törlése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete old database?" xml:space="preserve"> + <source>Delete old database?</source> + <target>Törli a régi adatbázist?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> + <source>Delete or moderate up to 200 messages.</source> + <target>Legfeljebb 200 üzenet egyszerre való törlése, vagy moderálása.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete pending connection?" xml:space="preserve"> + <source>Delete pending connection?</source> + <target>Törli a függőben lévő meghívót?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete profile" xml:space="preserve"> + <source>Delete profile</source> + <target>Profil törlése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete queue" xml:space="preserve"> + <source>Delete queue</source> + <target>Sorba állítás törlése</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Delete report" xml:space="preserve"> + <source>Delete report</source> + <target>Jelentés törlése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete up to 20 messages at once." xml:space="preserve"> + <source>Delete up to 20 messages at once.</source> + <target>Legfeljebb 20 üzenet egyszerre való törlése.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete user profile?" xml:space="preserve"> + <source>Delete user profile?</source> + <target>Törli a felhasználói profilt?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete without notification" xml:space="preserve"> + <source>Delete without notification</source> + <target>Törlés értesítés nélkül</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve"> + <source>Deleted</source> + <target>Törölve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted at" xml:space="preserve"> + <source>Deleted at</source> + <target>Törölve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted at: %@" xml:space="preserve"> + <source>Deleted at: %@</source> + <target>Törölve: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve"> + <source>Deletion errors</source> + <target>Törlési hibák</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivered even when Apple drops them." xml:space="preserve"> + <source>Delivered even when Apple drops them.</source> + <target>Kézbesítés akkor is, amikor az Apple eldobja őket.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivery" xml:space="preserve"> + <source>Delivery</source> + <target>Kézbesítés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivery receipts are disabled!" xml:space="preserve"> + <source>Delivery receipts are disabled!</source> + <target>A kézbesítési jelentések le vannak tiltva!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivery receipts!" xml:space="preserve"> + <source>Delivery receipts!</source> + <target>Kézbesítési jelentések!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Description" xml:space="preserve"> + <source>Description</source> + <target>Leírás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Desktop address" xml:space="preserve"> + <source>Desktop address</source> + <target>Számítógép címe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve"> + <source>Desktop app version %@ is not compatible with this app.</source> + <target>A számítógép-alkalmazás verziója (%@) nem kompatibilis ezzel az alkalmazással.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Desktop devices" xml:space="preserve"> + <source>Desktop devices</source> + <target>Számítógépek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve"> + <source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source> + <target>A(z) %@ célkiszolgáló címe nem kompatibilis a(z) %@ továbbítókiszolgáló beállításaival.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server error: %@" xml:space="preserve"> + <source>Destination server error: %@</source> + <target>Célkiszolgáló-hiba: %@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve"> + <source>Destination server version of %@ is incompatible with forwarding server %@.</source> + <target>A(z) %@ célkiszolgáló verziója nem kompatibilis a(z) %@ továbbítókiszolgálóval.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Detailed statistics" xml:space="preserve"> + <source>Detailed statistics</source> + <target>Részletes statisztikák</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Details" xml:space="preserve"> + <source>Details</source> + <target>További részletek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Develop" xml:space="preserve"> + <source>Develop</source> + <target>Fejlesztés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Developer options" xml:space="preserve"> + <source>Developer options</source> + <target>Fejlesztői beállítások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Developer tools" xml:space="preserve"> + <source>Developer tools</source> + <target>Fejlesztői eszközök</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Device" xml:space="preserve"> + <source>Device</source> + <target>Eszköz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Device authentication is disabled. Turning off SimpleX Lock." xml:space="preserve"> + <source>Device authentication is disabled. Turning off SimpleX Lock.</source> + <target>Az eszközön nincs beállítva a képernyőzár. A SimpleX-zár ki van kapcsolva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." xml:space="preserve"> + <source>Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication.</source> + <target>Az eszközön nincs beállítva a képernyőzár. A SimpleX-zár az „Adatvédelem és biztonság” menüben kapcsolható be, miután beállította a képernyőzárat az eszközén.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Different names, avatars and transport isolation." xml:space="preserve"> + <source>Different names, avatars and transport isolation.</source> + <target>Különböző nevek, profilképek és átvitel-izoláció.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages" xml:space="preserve"> + <source>Direct messages</source> + <target>Közvetlen üzenetek</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve"> + <source>Direct messages between members are prohibited in this chat.</source> + <target>A tagok közötti közvetlen üzenetek le vannak tiltva ebben a csevegésben.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> + <target>A tagok közötti közvetlen üzenetek le vannak tiltva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disable (keep overrides)" xml:space="preserve"> + <source>Disable (keep overrides)</source> + <target>Letiltás (felülírások megtartásával)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> + <source>Disable SimpleX Lock</source> + <target>SimpleX-zár kikapcsolása</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Disable automatic message deletion?" xml:space="preserve"> + <source>Disable automatic message deletion?</source> + <target>Letiltja az automatikus üzenettörlést?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Disable delete messages" xml:space="preserve"> + <source>Disable delete messages</source> + <target>Üzenetek törlésének letiltása</target> + <note>alert button</note> + </trans-unit> + <trans-unit id="Disable for all" xml:space="preserve"> + <source>Disable for all</source> + <target>Letiltás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disabled" xml:space="preserve"> + <source>Disabled</source> + <target>Letiltva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disappearing message" xml:space="preserve"> + <source>Disappearing message</source> + <target>Eltűnő üzenet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disappearing messages" xml:space="preserve"> + <source>Disappearing messages</source> + <target>Eltűnő üzenetek</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Disappearing messages are prohibited in this chat." xml:space="preserve"> + <source>Disappearing messages are prohibited in this chat.</source> + <target>Az eltűnő üzenetek küldése le van tiltva ebben a csevegésben.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> + <target>Az eltűnő üzenetek küldése le van tiltva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disappears at" xml:space="preserve"> + <source>Disappears at</source> + <target>Eltűnik</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disappears at: %@" xml:space="preserve"> + <source>Disappears at: %@</source> + <target>Eltűnik: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Disconnect" xml:space="preserve"> + <source>Disconnect</source> + <target>Kapcsolat bontása</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Disconnect desktop?" xml:space="preserve"> + <source>Disconnect desktop?</source> + <target>Leválasztja a számítógépet?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Discover and join groups" xml:space="preserve"> + <source>Discover and join groups</source> + <target>Csoportok felfedezése és csatlakozás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Discover via local network" xml:space="preserve"> + <source>Discover via local network</source> + <target>Felfedezés helyi hálózaton keresztül</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve"> + <source>Do NOT send messages directly, even if your or destination server does not support private routing.</source> + <target>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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> + <source>Do NOT use SimpleX for emergency calls.</source> + <target>NE használja a SimpleXet segélyhívásokhoz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do NOT use private routing." xml:space="preserve"> + <source>Do NOT use private routing.</source> + <target>NE használjon privát útválasztást.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do it later" xml:space="preserve"> + <source>Do it later</source> + <target>Befejezés később</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not send history to new members." xml:space="preserve"> + <source>Do not send history to new members.</source> + <target>Az előzmények ne legyenek elküldve az új tagok számára.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not use credentials with proxy." xml:space="preserve"> + <source>Do not use credentials with proxy.</source> + <target>Ne használja a hitelesítőadatokat proxyval.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Documents:" xml:space="preserve"> + <source>Documents:</source> + <target>Dokumentumok:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Don't create address" xml:space="preserve"> + <source>Don't create address</source> + <target>Ne hozzon létre címet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Don't enable" xml:space="preserve"> + <source>Don't enable</source> + <target>Ne engedélyezze</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Don't miss important messages." xml:space="preserve"> + <source>Don't miss important messages.</source> + <target>Ne maradjon le a fontos üzenetekről.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Don't show again" xml:space="preserve"> + <source>Don't show again</source> + <target>Ne mutasd újra</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Done" xml:space="preserve"> + <source>Done</source> + <target>Kész</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downgrade and open chat" xml:space="preserve"> + <source>Downgrade and open chat</source> + <target>Visszafejlesztés és a csevegés megnyitása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download" xml:space="preserve"> + <source>Download</source> + <target>Letöltés</target> + <note>alert button +chat item action</note> + </trans-unit> + <trans-unit id="Download errors" xml:space="preserve"> + <source>Download errors</source> + <target>Letöltési hibák</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download failed" xml:space="preserve"> + <source>Download failed</source> + <target>Sikertelen letöltés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download file" xml:space="preserve"> + <source>Download file</source> + <target>Fájl letöltése</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Download files" xml:space="preserve"> + <source>Download files</source> + <target>Fájlok letöltése</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Downloaded" xml:space="preserve"> + <source>Downloaded</source> + <target>Letöltve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloaded files" xml:space="preserve"> + <source>Downloaded files</source> + <target>Letöltött fájlok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading archive" xml:space="preserve"> + <source>Downloading archive</source> + <target>Archívum letöltése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading link details" xml:space="preserve"> + <source>Downloading link details</source> + <target>Letöltési hivatkozás részletei</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Duplicate display name!" xml:space="preserve"> + <source>Duplicate display name!</source> + <target>Duplikált megjelenítendő név!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Duration" xml:space="preserve"> + <source>Duration</source> + <target>Időtartam</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="E2E encrypted notifications." xml:space="preserve"> + <source>E2E encrypted notifications.</source> + <target>Végpontok közötti titkosított értesítések.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Edit" xml:space="preserve"> + <source>Edit</source> + <target>Szerkesztés</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Edit group profile" xml:space="preserve"> + <source>Edit group profile</source> + <target>Csoportprofil szerkesztése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable" xml:space="preserve"> + <source>Enable</source> + <target>Engedélyezés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable (keep overrides)" xml:space="preserve"> + <source>Enable (keep overrides)</source> + <target>Engedélyezés (felülírások megtartásával)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve"> + <source>Enable Flux in Network & servers settings for better metadata privacy.</source> + <target>A Flux kiszolgálókat engedélyezheti a beállításokban, a „Hálózat és kiszolgálók” menüben, a metaadatok jobb védelme érdekében.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> + <source>Enable SimpleX Lock</source> + <target>SimpleX-zár bekapcsolása</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Enable TCP keep-alive" xml:space="preserve"> + <source>Enable TCP keep-alive</source> + <target>TCP életben tartása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> + <source>Enable automatic message deletion?</source> + <target>Engedélyezi az automatikus üzenettörlést?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Enable camera access" xml:space="preserve"> + <source>Enable camera access</source> + <target>Kamera hozzáférés engedélyezése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable for all" xml:space="preserve"> + <source>Enable for all</source> + <target>Engedélyezés az összes tag számára</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve"> + <source>Enable in direct chats (BETA)!</source> + <target>Engedélyezés a közvetlen csevegésekben (BÉTA)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable instant notifications?" xml:space="preserve"> + <source>Enable instant notifications?</source> + <target>Engedélyezi az azonnali értesítéseket?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable lock" xml:space="preserve"> + <source>Enable lock</source> + <target>Zárolás engedélyezése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable notifications" xml:space="preserve"> + <source>Enable notifications</source> + <target>Értesítések engedélyezése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable periodic notifications?" xml:space="preserve"> + <source>Enable periodic notifications?</source> + <target>Engedélyezi az időszakos értesítéseket?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable self-destruct" xml:space="preserve"> + <source>Enable self-destruct</source> + <target>Önmegsemmisítés engedélyezése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable self-destruct passcode" xml:space="preserve"> + <source>Enable self-destruct passcode</source> + <target>Önmegsemmisítő-jelkód engedélyezése</target> + <note>set passcode view</note> + </trans-unit> + <trans-unit id="Enabled" xml:space="preserve"> + <source>Enabled</source> + <target>Engedélyezve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve"> + <source>Enabled for</source> + <target>Számukra engedélyezve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt" xml:space="preserve"> + <source>Encrypt</source> + <target>Titkosít</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt database?" xml:space="preserve"> + <source>Encrypt database?</source> + <target>Titkosítja az adatbázist?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt local files" xml:space="preserve"> + <source>Encrypt local files</source> + <target>Helyi fájlok titkosítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt stored files & media" xml:space="preserve"> + <source>Encrypt stored files & media</source> + <target>A tárolt fájlok- és a médiatartalmak titkosítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypted database" xml:space="preserve"> + <source>Encrypted database</source> + <target>Titkosított adatbázis</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypted message or another event" xml:space="preserve"> + <source>Encrypted message or another event</source> + <target>Titkosított üzenet vagy más esemény</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Encrypted message: app is stopped" xml:space="preserve"> + <source>Encrypted message: app is stopped</source> + <target>Titkosított üzenet: az alkalmazás megállt</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Encrypted message: database error" xml:space="preserve"> + <source>Encrypted message: database error</source> + <target>Titkosított üzenet: adatbázishiba</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Encrypted message: database migration error" xml:space="preserve"> + <source>Encrypted message: database migration error</source> + <target>Titkosított üzenet: adatbázis-átköltöztetési hiba</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Encrypted message: keychain error" xml:space="preserve"> + <source>Encrypted message: keychain error</source> + <target>Titkosított üzenet: kulcstartó hiba</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Encrypted message: no passphrase" xml:space="preserve"> + <source>Encrypted message: no passphrase</source> + <target>Titkosított üzenet: nincs jelmondat</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Encrypted message: unexpected error" xml:space="preserve"> + <source>Encrypted message: unexpected error</source> + <target>Titkosított üzenet: váratlan hiba</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Encryption re-negotiation error" xml:space="preserve"> + <source>Encryption re-negotiation error</source> + <target>Hiba történt a titkosítás újraegyeztetésekor</target> + <note>message decrypt error item</note> + </trans-unit> + <trans-unit id="Encryption re-negotiation failed." xml:space="preserve"> + <source>Encryption re-negotiation failed.</source> + <target>Nem sikerült a titkosítást újraegyeztetni.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encryption renegotiation in progress." xml:space="preserve"> + <source>Encryption renegotiation in progress.</source> + <target>A titkosítás újraegyeztetése folyamatban van.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter Passcode" xml:space="preserve"> + <source>Enter Passcode</source> + <target>Adja meg a jelkódot</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter correct passphrase." xml:space="preserve"> + <source>Enter correct passphrase.</source> + <target>Adja meg a helyes jelmondatot.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter group name…" xml:space="preserve"> + <source>Enter group name…</source> + <target>Adja meg a csoport nevét…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve"> + <source>Enter passphrase</source> + <target>Adja meg a jelmondatot</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter passphrase…" xml:space="preserve"> + <source>Enter passphrase…</source> + <target>Adja meg a jelmondatot…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter password above to show!" xml:space="preserve"> + <source>Enter password above to show!</source> + <target>Adja meg a jelszót fentebb a megjelenítéshez!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter server manually" xml:space="preserve"> + <source>Enter server manually</source> + <target>Adja meg a kiszolgálót kézzel</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter this device name…" xml:space="preserve"> + <source>Enter this device name…</source> + <target>Adja meg ennek az eszköznek a nevét…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter welcome message…" xml:space="preserve"> + <source>Enter welcome message…</source> + <target>Adja meg az üdvözlőüzenetet…</target> + <note>placeholder</note> + </trans-unit> + <trans-unit id="Enter welcome message… (optional)" xml:space="preserve"> + <source>Enter welcome message… (optional)</source> + <target>Adja meg az üdvözlőüzenetet… (nem kötelező)</target> + <note>placeholder</note> + </trans-unit> + <trans-unit id="Enter your name…" xml:space="preserve"> + <source>Enter your name…</source> + <target>Adjon meg egy nevet…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error" xml:space="preserve"> + <source>Error</source> + <target>Hiba</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error aborting address change" xml:space="preserve"> + <source>Error aborting address change</source> + <target>Hiba történt a cím módosításának megszakításakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error accepting conditions" xml:space="preserve"> + <source>Error accepting conditions</source> + <target>Hiba történt a feltételek elfogadásakor</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error accepting contact request" xml:space="preserve"> + <source>Error accepting contact request</source> + <target>Hiba történt a meghívási kérés elfogadásakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error adding member(s)" xml:space="preserve"> + <source>Error adding member(s)</source> + <target>Hiba történt a tag(ok) hozzáadásakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error adding server" xml:space="preserve"> + <source>Error adding server</source> + <target>Hiba történt a kiszolgáló hozzáadásakor</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error changing address" xml:space="preserve"> + <source>Error changing address</source> + <target>Hiba történt a cím módosításakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error changing connection profile" xml:space="preserve"> + <source>Error changing connection profile</source> + <target>Hiba történt a kapcsolati profilra való váltáskor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error changing role" xml:space="preserve"> + <source>Error changing role</source> + <target>Hiba történt a szerepkör módosításakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error changing setting" xml:space="preserve"> + <source>Error changing setting</source> + <target>Hiba történt a beállítás módosításakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error changing to incognito!" xml:space="preserve"> + <source>Error changing to incognito!</source> + <target>Hiba történt az inkognitóprofilra való váltáskor!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error checking token status" xml:space="preserve"> + <source>Error checking token status</source> + <target>Hiba történt a token állapotának ellenőrzésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve"> + <source>Error connecting to forwarding server %@. Please try later.</source> + <target>Hiba történt a(z) %@ továbbítókiszolgálóhoz való kapcsolódáskor. Próbálja meg később.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating address" xml:space="preserve"> + <source>Error creating address</source> + <target>Hiba történt a cím létrehozásakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating group" xml:space="preserve"> + <source>Error creating group</source> + <target>Hiba történt a csoport létrehozásakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating group link" xml:space="preserve"> + <source>Error creating group link</source> + <target>Hiba történt a csoporthivatkozás létrehozásakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating list" xml:space="preserve"> + <source>Error creating list</source> + <target>Hiba történt a lista létrehozásakor</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error creating member contact" xml:space="preserve"> + <source>Error creating member contact</source> + <target>Hiba történt a partnerrel történő kapcsolat létrehozásában</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating message" xml:space="preserve"> + <source>Error creating message</source> + <target>Hiba történt az üzenet létrehozásakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating profile!" xml:space="preserve"> + <source>Error creating profile!</source> + <target>Hiba történt a profil létrehozásakor!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating report" xml:space="preserve"> + <source>Error creating report</source> + <target>Hiba történt a jelentés létrehozásakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error decrypting file" xml:space="preserve"> + <source>Error decrypting file</source> + <target>Hiba történt a fájl visszafejtésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error deleting chat database" xml:space="preserve"> + <source>Error deleting chat database</source> + <target>Hiba történt a csevegési adatbázis törlésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error deleting chat!" xml:space="preserve"> + <source>Error deleting chat!</source> + <target>Hiba történt a csevegés törlésekor!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error deleting connection" xml:space="preserve"> + <source>Error deleting connection</source> + <target>Hiba történt a kapcsolat törlésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error deleting database" xml:space="preserve"> + <source>Error deleting database</source> + <target>Hiba történt az adatbázis törlésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error deleting old database" xml:space="preserve"> + <source>Error deleting old database</source> + <target>Hiba történt a régi adatbázis törlésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error deleting token" xml:space="preserve"> + <source>Error deleting token</source> + <target>Hiba történt a token törlésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error deleting user profile" xml:space="preserve"> + <source>Error deleting user profile</source> + <target>Hiba történt a felhasználó-profil törlésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error downloading the archive" xml:space="preserve"> + <source>Error downloading the archive</source> + <target>Hiba történt az archívum letöltésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> + <source>Error enabling delivery receipts!</source> + <target>Hiba történt a kézbesítési jelentések engedélyezésekor!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error enabling notifications" xml:space="preserve"> + <source>Error enabling notifications</source> + <target>Hiba történt az értesítések engedélyezésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error encrypting database" xml:space="preserve"> + <source>Error encrypting database</source> + <target>Hiba történt az adatbázis titkosításakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error exporting chat database" xml:space="preserve"> + <source>Error exporting chat database</source> + <target>Hiba történt a csevegési adatbázis exportálásakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error exporting theme: %@" xml:space="preserve"> + <source>Error exporting theme: %@</source> + <target>Hiba történt a téma exportálásakor: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error importing chat database" xml:space="preserve"> + <source>Error importing chat database</source> + <target>Hiba történt a csevegési adatbázis importálásakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error joining group" xml:space="preserve"> + <source>Error joining group</source> + <target>Hiba történt a csoporthoz való csatlakozáskor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error loading servers" xml:space="preserve"> + <source>Error loading servers</source> + <target>Hiba történt a kiszolgálók betöltésekor</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error migrating settings" xml:space="preserve"> + <source>Error migrating settings</source> + <target>Hiba történt a beállítások átköltöztetésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error opening chat" xml:space="preserve"> + <source>Error opening chat</source> + <target>Hiba történt a csevegés megnyitásakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error receiving file" xml:space="preserve"> + <source>Error receiving file</source> + <target>Hiba történt a fájl fogadásakor</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error reconnecting server" xml:space="preserve"> + <source>Error reconnecting server</source> + <target>Hiba történt a kiszolgálóhoz való újrakapcsolódáskor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error reconnecting servers" xml:space="preserve"> + <source>Error reconnecting servers</source> + <target>Hiba történt a kiszolgálókhoz való újrakapcsolódáskor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error registering for notifications" xml:space="preserve"> + <source>Error registering for notifications</source> + <target>Hiba történt az értesítések regisztrálásakor</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error removing member" xml:space="preserve"> + <source>Error removing member</source> + <target>Hiba történt a tag eltávolításakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error reordering lists" xml:space="preserve"> + <source>Error reordering lists</source> + <target>Hiba történt a listák újrarendezésekor</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error resetting statistics" xml:space="preserve"> + <source>Error resetting statistics</source> + <target>Hiba történt a statisztikák visszaállításakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error saving ICE servers" xml:space="preserve"> + <source>Error saving ICE servers</source> + <target>Hiba történt az ICE-kiszolgálók mentésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error saving chat list" xml:space="preserve"> + <source>Error saving chat list</source> + <target>Hiba történt a csevegési lista mentésekor</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error saving group profile" xml:space="preserve"> + <source>Error saving group profile</source> + <target>Hiba történt a csoportprofil mentésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error saving passcode" xml:space="preserve"> + <source>Error saving passcode</source> + <target>Hiba történt a jelkód mentésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error saving passphrase to keychain" xml:space="preserve"> + <source>Error saving passphrase to keychain</source> + <target>Hiba történt a jelmondat kulcstartóba történő mentésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error saving servers" xml:space="preserve"> + <source>Error saving servers</source> + <target>Hiba történt a kiszolgálók mentésekor</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error saving settings" xml:space="preserve"> + <source>Error saving settings</source> + <target>Hiba történt a beállítások mentésekor</target> + <note>when migrating</note> + </trans-unit> + <trans-unit id="Error saving user password" xml:space="preserve"> + <source>Error saving user password</source> + <target>Hiba történt a felhasználó jelszavának mentésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error scanning code: %@" xml:space="preserve"> + <source>Error scanning code: %@</source> + <target>Hiba történt a kód beolvasásakor: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error sending email" xml:space="preserve"> + <source>Error sending email</source> + <target>Hiba történt az e-mail elküldésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error sending member contact invitation" xml:space="preserve"> + <source>Error sending member contact invitation</source> + <target>Hiba történt a tag kapcsolatfelvételi meghívójának elküldésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error sending message" xml:space="preserve"> + <source>Error sending message</source> + <target>Hiba történt az üzenet elküldésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error setting delivery receipts!" xml:space="preserve"> + <source>Error setting delivery receipts!</source> + <target>Hiba történt a kézbesítési jelentések beállításakor!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error starting chat" xml:space="preserve"> + <source>Error starting chat</source> + <target>Hiba történt a csevegés elindításakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error stopping chat" xml:space="preserve"> + <source>Error stopping chat</source> + <target>Hiba történt a csevegés megállításakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error switching profile" xml:space="preserve"> + <source>Error switching profile</source> + <target>Hiba történt a profilváltáskor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error switching profile!" xml:space="preserve"> + <source>Error switching profile!</source> + <target>Hiba történt a profilváltáskor!</target> + <note>alertTitle</note> + </trans-unit> + <trans-unit id="Error synchronizing connection" xml:space="preserve"> + <source>Error synchronizing connection</source> + <target>Hiba történt a kapcsolat szinkronizálásakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error testing server connection" xml:space="preserve"> + <source>Error testing server connection</source> + <target>Hiba történt a kiszolgáló kapcsolatának tesztelésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error updating group link" xml:space="preserve"> + <source>Error updating group link</source> + <target>Hiba történt a csoporthivatkozás frissítésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error updating message" xml:space="preserve"> + <source>Error updating message</source> + <target>Hiba történt az üzenet frissítésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error updating server" xml:space="preserve"> + <source>Error updating server</source> + <target>Hiba történt a kiszolgáló frissítésekor</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error updating settings" xml:space="preserve"> + <source>Error updating settings</source> + <target>Hiba történt a beállítások frissítésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error updating user privacy" xml:space="preserve"> + <source>Error updating user privacy</source> + <target>Hiba történt a felhasználói adatvédelem frissítésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error uploading the archive" xml:space="preserve"> + <source>Error uploading the archive</source> + <target>Hiba történt az archívum feltöltésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error verifying passphrase:" xml:space="preserve"> + <source>Error verifying passphrase:</source> + <target>Hiba történt a jelmondat hitelesítésekor:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: " xml:space="preserve"> + <source>Error: </source> + <target>Hiba: </target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <target>Hiba: %@</target> + <note>alert message +file error text +snd error text</note> + </trans-unit> + <trans-unit id="Error: URL is invalid" xml:space="preserve"> + <source>Error: URL is invalid</source> + <target>Hiba: a webcím érvénytelen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: no database file" xml:space="preserve"> + <source>Error: no database file</source> + <target>Hiba: nincs adatbázisfájl</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors" xml:space="preserve"> + <source>Errors</source> + <target>Hibák</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors in servers configuration." xml:space="preserve"> + <source>Errors in servers configuration.</source> + <target>Hibák a kiszolgálók konfigurációjában.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> + <source>Even when disabled in the conversation.</source> + <target>Akkor is, ha le van tiltva a beszélgetésben.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Exit without saving" xml:space="preserve"> + <source>Exit without saving</source> + <target>Kilépés mentés nélkül</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Expand" xml:space="preserve"> + <source>Expand</source> + <target>Kibontás</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Expired" xml:space="preserve"> + <source>Expired</source> + <target>Lejárt</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Export database" xml:space="preserve"> + <source>Export database</source> + <target>Adatbázis exportálása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Export error:" xml:space="preserve"> + <source>Export error:</source> + <target>Exportálási hiba:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Export theme" xml:space="preserve"> + <source>Export theme</source> + <target>Téma exportálása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Exported database archive." xml:space="preserve"> + <source>Exported database archive.</source> + <target>Exportált adatbázis-archívum.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Exported file doesn't exist" xml:space="preserve"> + <source>Exported file doesn't exist</source> + <target>Az exportált fájl nem létezik</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Exporting database archive…" xml:space="preserve"> + <source>Exporting database archive…</source> + <target>Adatbázis-archívum exportálása…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Failed to remove passphrase" xml:space="preserve"> + <source>Failed to remove passphrase</source> + <target>Nem sikerült eltávolítani a jelmondatot</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fast and no wait until the sender is online!" xml:space="preserve"> + <source>Fast and no wait until the sender is online!</source> + <target>Gyors és nem kell várni, amíg a feladó online lesz!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Faster deletion of groups." xml:space="preserve"> + <source>Faster deletion of groups.</source> + <target>Gyorsabb csoporttörlés.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Faster joining and more reliable messages." xml:space="preserve"> + <source>Faster joining and more reliable messages.</source> + <target>Gyorsabb csatlakozás és megbízhatóbb üzenetkézbesítés.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Faster sending messages." xml:space="preserve"> + <source>Faster sending messages.</source> + <target>Gyorsabb üzenetküldés.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Favorite" xml:space="preserve"> + <source>Favorite</source> + <target>Kedvenc</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Favorites" xml:space="preserve"> + <source>Favorites</source> + <target>Kedvencek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Fájlhiba</target> + <note>file error alert title</note> + </trans-unit> + <trans-unit id="File errors: %@" xml:space="preserve"> + <source>File errors: +%@</source> + <target>Fájlhiba: +%@</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="File is blocked by server operator: %@." xml:space="preserve"> + <source>File is blocked by server operator: +%@.</source> + <target>A kiszolgáló üzemeltetője letiltotta a fájlt: +%@.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve"> + <source>File not found - most likely file was deleted or cancelled.</source> + <target>A fájl nem található – valószínűleg a fájlt törölték vagy visszavonták.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File server error: %@" xml:space="preserve"> + <source>File server error: %@</source> + <target>Fájlkiszolgáló-hiba: %@</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File status" xml:space="preserve"> + <source>File status</source> + <target>Fájl állapota</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File status: %@" xml:space="preserve"> + <source>File status: %@</source> + <target>Fájl állapota: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="File will be deleted from servers." xml:space="preserve"> + <source>File will be deleted from servers.</source> + <target>A fájl törölve lesz a kiszolgálókról.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File will be received when your contact completes uploading it." xml:space="preserve"> + <source>File will be received when your contact completes uploading it.</source> + <target>A fájl akkor érkezik meg, amikor a küldője befejezte annak feltöltését.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File will be received when your contact is online, please wait or check later!" xml:space="preserve"> + <source>File will be received when your contact is online, please wait or check later!</source> + <target>A fájl akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File: %@" xml:space="preserve"> + <source>File: %@</source> + <target>Fájl: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Files" xml:space="preserve"> + <source>Files</source> + <target>Fájlok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Files & media" xml:space="preserve"> + <source>Files & media</source> + <target>Fájlok és médiatartalmak</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Files and media" xml:space="preserve"> + <source>Files and media</source> + <target>Fájlok és médiatartalmak</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Files and media are prohibited." xml:space="preserve"> + <source>Files and media are prohibited.</source> + <target>A fájlok- és a médiatartalmak küldése le van tiltva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Files and media not allowed" xml:space="preserve"> + <source>Files and media not allowed</source> + <target>A fájlok- és médiatartalmak nincsenek engedélyezve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Files and media prohibited!" xml:space="preserve"> + <source>Files and media prohibited!</source> + <target>A fájlok- és a médiatartalmak küldése le van tiltva!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Filter unread and favorite chats." xml:space="preserve"> + <source>Filter unread and favorite chats.</source> + <target>Olvasatlan és kedvenc csevegésekre való szűrés.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration" xml:space="preserve"> + <source>Finalize migration</source> + <target>Átköltöztetés véglegesítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration on another device." xml:space="preserve"> + <source>Finalize migration on another device.</source> + <target>Átköltöztetés véglegesítése egy másik eszközön.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> + <source>Finally, we have them! 🚀</source> + <target>Végre, megvannak! 🚀</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Find chats faster" xml:space="preserve"> + <source>Find chats faster</source> + <target>Csevegési üzenetek gyorsabb megtalálása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix" xml:space="preserve"> + <source>Fix</source> + <target>Javítás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix connection" xml:space="preserve"> + <source>Fix connection</source> + <target>Kapcsolat javítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix connection?" xml:space="preserve"> + <source>Fix connection?</source> + <target>Kapcsolat javítása?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix encryption after restoring backups." xml:space="preserve"> + <source>Fix encryption after restoring backups.</source> + <target>Titkosítás javítása az adatmentések helyreállítása után.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix not supported by contact" xml:space="preserve"> + <source>Fix not supported by contact</source> + <target>Partner általi javítás nem támogatott</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix not supported by group member" xml:space="preserve"> + <source>Fix not supported by group member</source> + <target>Csoporttag általi javítás nem támogatott</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For all moderators" xml:space="preserve"> + <source>For all moderators</source> + <target>Az összes moderátor számára</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For chat profile %@:" xml:space="preserve"> + <source>For chat profile %@:</source> + <target>A(z) %@ nevű csevegési profilhoz:</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="For console" xml:space="preserve"> + <source>For console</source> + <target>Konzolhoz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." xml:space="preserve"> + <source>For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</source> + <target>Például, ha a partnere egy SimpleX Chat-kiszolgálón keresztül fogadja az üzeneteket, akkor az Ön alkalmazása egy Flux-kiszolgálón keresztül fogja azokat kézbesíteni.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For me" xml:space="preserve"> + <source>For me</source> + <target>Csak magamnak</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For private routing" xml:space="preserve"> + <source>For private routing</source> + <target>A privát útválasztáshoz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For social media" xml:space="preserve"> + <source>For social media</source> + <target>A közösségi médiához</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward" xml:space="preserve"> + <source>Forward</source> + <target>Továbbítás</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Forward %d message(s)?" xml:space="preserve"> + <source>Forward %d message(s)?</source> + <target>Továbbít %d üzenetet?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Forward and save messages" xml:space="preserve"> + <source>Forward and save messages</source> + <target>Üzenetek továbbítása és mentése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward messages" xml:space="preserve"> + <source>Forward messages</source> + <target>Üzenetek továbbítása</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Forward messages without files?" xml:space="preserve"> + <source>Forward messages without files?</source> + <target>Továbbítja az üzeneteket fájlok nélkül?</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> + <source>Forward up to 20 messages at once.</source> + <target>Legfeljebb 20 üzenet egyszerre való továbbítása.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded" xml:space="preserve"> + <source>Forwarded</source> + <target>Továbbított</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded from" xml:space="preserve"> + <source>Forwarded from</source> + <target>Továbbítva innen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding %lld messages" xml:space="preserve"> + <source>Forwarding %lld messages</source> + <target>%lld üzenet továbbítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve"> + <source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source> + <target>A(z) %@ továbbítókiszolgáló nem tudott kapcsolódni a(z) %@ célkiszolgálóhoz. Próbálja meg később.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server address is incompatible with network settings: %@.</source> + <target>A továbbítókiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server version is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server version is incompatible with network settings: %@.</source> + <target>A továbbítókiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Destination server error: %2$@</source> + <target>Továbbítókiszolgáló: %1$@ +Célkiszolgáló-hiba: %2$@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Error: %2$@</source> + <target>Továbbítókiszolgáló: %1$@ +Hiba: %2$@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Found desktop" xml:space="preserve"> + <source>Found desktop</source> + <target>Megtalált számítógép</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="French interface" xml:space="preserve"> + <source>French interface</source> + <target>Francia kezelőfelület</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Full link" xml:space="preserve"> + <source>Full link</source> + <target>Teljes hivatkozás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Full name (optional)" xml:space="preserve"> + <source>Full name (optional)</source> + <target>Teljes név (nem kötelező)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fully decentralized – visible only to members." xml:space="preserve"> + <source>Fully decentralized – visible only to members.</source> + <target>Teljesen decentralizált – csak a tagok számára látható.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fully re-implemented - work in background!" xml:space="preserve"> + <source>Fully re-implemented - work in background!</source> + <target>Teljesen újra implementálva – háttérben történő működés!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Further reduced battery usage" xml:space="preserve"> + <source>Further reduced battery usage</source> + <target>Tovább csökkentett akkumulátor-használat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="GIFs and stickers" xml:space="preserve"> + <source>GIFs and stickers</source> + <target>GIF-ek és matricák</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Get notified when mentioned." xml:space="preserve"> + <source>Get notified when mentioned.</source> + <target>Kapjon értesítést, ha megemlítik.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Good afternoon!" xml:space="preserve"> + <source>Good afternoon!</source> + <target>Jó napot!</target> + <note>message preview</note> + </trans-unit> + <trans-unit id="Good morning!" xml:space="preserve"> + <source>Good morning!</source> + <target>Jó reggelt!</target> + <note>message preview</note> + </trans-unit> + <trans-unit id="Group" xml:space="preserve"> + <source>Group</source> + <target>Csoport</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group already exists" xml:space="preserve"> + <source>Group already exists</source> + <target>A csoport már létezik</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group already exists!" xml:space="preserve"> + <source>Group already exists!</source> + <target>A csoport már létezik!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group display name" xml:space="preserve"> + <source>Group display name</source> + <target>A csoport megjelenített neve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group full name (optional)" xml:space="preserve"> + <source>Group full name (optional)</source> + <target>A csoport teljes neve (nem kötelező)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group image" xml:space="preserve"> + <source>Group image</source> + <target>Csoport profilképe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group invitation" xml:space="preserve"> + <source>Group invitation</source> + <target>Csoportmeghívó</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group invitation expired" xml:space="preserve"> + <source>Group invitation expired</source> + <target>A csoportmeghívó lejárt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group invitation is no longer valid, it was removed by sender." xml:space="preserve"> + <source>Group invitation is no longer valid, it was removed by sender.</source> + <target>A csoportmeghívó már nem érvényes, a küldője eltávolította.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group link" xml:space="preserve"> + <source>Group link</source> + <target>Csoporthivatkozás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group links" xml:space="preserve"> + <source>Group links</source> + <target>Csoporthivatkozások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group message:" xml:space="preserve"> + <source>Group message:</source> + <target>Csoport üzenet:</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Group moderation" xml:space="preserve"> + <source>Group moderation</source> + <target>Csoport moderálása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group preferences" xml:space="preserve"> + <source>Group preferences</source> + <target>Csoportbeállítások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group profile" xml:space="preserve"> + <source>Group profile</source> + <target>Csoportprofil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group profile is stored on members' devices, not on the servers." xml:space="preserve"> + <source>Group profile is stored on members' devices, not on the servers.</source> + <target>A csoport profilja a tagok eszközein tárolódik, nem a kiszolgálókon.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group welcome message" xml:space="preserve"> + <source>Group welcome message</source> + <target>A csoport üdvözlőüzenete</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Group will be deleted for all members - this cannot be undone!</source> + <target>A csoport törölve lesz az összes tag számára – ez a művelet nem vonható vissza!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Group will be deleted for you - this cannot be undone!</source> + <target>A csoport törölve lesz az Ön számára – ez a művelet nem vonható vissza!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Groups" xml:space="preserve"> + <source>Groups</source> + <target>Csoportok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Help" xml:space="preserve"> + <source>Help</source> + <target>Súgó</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Help admins moderating their groups." xml:space="preserve"> + <source>Help admins moderating their groups.</source> + <target>Segítsen az adminisztrátoroknak a csoportjaik moderálásában.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Hidden" xml:space="preserve"> + <source>Hidden</source> + <target>Se név, se üzenet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Hidden chat profiles" xml:space="preserve"> + <source>Hidden chat profiles</source> + <target>Rejtett csevegési profilok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Hidden profile password" xml:space="preserve"> + <source>Hidden profile password</source> + <target>Rejtett profiljelszó</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Hide" xml:space="preserve"> + <source>Hide</source> + <target>Összecsukás</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Hide app screen in the recent apps." xml:space="preserve"> + <source>Hide app screen in the recent apps.</source> + <target>Alkalmazás képernyőjének elrejtése a gyakran használt alkalmazások között.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Hide profile" xml:space="preserve"> + <source>Hide profile</source> + <target>Profil elrejtése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Hide:" xml:space="preserve"> + <source>Hide:</source> + <target>Elrejtve:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="History" xml:space="preserve"> + <source>History</source> + <target>Előzmények</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="History is not sent to new members." xml:space="preserve"> + <source>History is not sent to new members.</source> + <target>Az előzmények nem lesznek elküldve az új tagok számára.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How SimpleX works" xml:space="preserve"> + <source>How SimpleX works</source> + <target>Hogyan működik a SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it affects privacy" xml:space="preserve"> + <source>How it affects privacy</source> + <target>Hogyan érinti az adatvédelmet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it helps privacy" xml:space="preserve"> + <source>How it helps privacy</source> + <target>Hogyan segíti az adatvédelmet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it works" xml:space="preserve"> + <source>How it works</source> + <target>Hogyan működik</target> + <note>alert button</note> + </trans-unit> + <trans-unit id="How to" xml:space="preserve"> + <source>How to</source> + <target>Hogyan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How to use it" xml:space="preserve"> + <source>How to use it</source> + <target>Használati útmutató</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How to use your servers" xml:space="preserve"> + <source>How to use your servers</source> + <target>Hogyan használja a saját kiszolgálóit</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Hungarian interface" xml:space="preserve"> + <source>Hungarian interface</source> + <target>Magyar kezelőfelület</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="ICE servers (one per line)" xml:space="preserve"> + <source>ICE servers (one per line)</source> + <target>ICE-kiszolgálók (soronként egy)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="IP address" xml:space="preserve"> + <source>IP address</source> + <target>IP-cím</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> + <source>If you can't meet in person, show QR code in a video call, or share the link.</source> + <target>Ha nem tud személyesen találkozni, mutassa meg a QR-kódot egy videohívás közben, vagy ossza meg a hivatkozást.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> + <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> + <target>Ha az alkalmazás megnyitásakor megadja ezt a jelkódot, az összes alkalmazásadat véglegesen el lesz távolítva!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="If you enter your self-destruct passcode while opening the app:" xml:space="preserve"> + <source>If you enter your self-destruct passcode while opening the app:</source> + <target>Ha az alkalmazás megnyitásakor megadja az önmegsemmisítő-jelkódot:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." xml:space="preserve"> + <source>If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app).</source> + <target>Ha most kell használnia a csevegést, koppintson alább a **Befejezés később** lehetőségre (az alkalmazás újraindításakor fel lesz ajánlva az adatbázis átköltöztetése).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ignore" xml:space="preserve"> + <source>Ignore</source> + <target>Mellőzés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Image will be received when your contact completes uploading it." xml:space="preserve"> + <source>Image will be received when your contact completes uploading it.</source> + <target>A kép akkor érkezik meg, amikor a küldője befejezte annak feltöltését.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve"> + <source>Image will be received when your contact is online, please wait or check later!</source> + <target>A kép akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Immediately" xml:space="preserve"> + <source>Immediately</source> + <target>Azonnal</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> + <target>Védett a kéretlen tartalommal szemben</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import" xml:space="preserve"> + <source>Import</source> + <target>Importálás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import chat database?" xml:space="preserve"> + <source>Import chat database?</source> + <target>Importálja a csevegési adatbázist?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import database" xml:space="preserve"> + <source>Import database</source> + <target>Adatbázis importálása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import failed" xml:space="preserve"> + <source>Import failed</source> + <target>Sikertelen importálás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import theme" xml:space="preserve"> + <source>Import theme</source> + <target>Téma importálása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Importing archive" xml:space="preserve"> + <source>Importing archive</source> + <target>Archívum importálása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved delivery, reduced traffic usage. More improvements are coming soon!" xml:space="preserve"> + <source>Improved delivery, reduced traffic usage. +More improvements are coming soon!</source> + <target>Továbbfejlesztett kézbesítés, csökkentett adatforgalom-használat. +További fejlesztések hamarosan!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved message delivery" xml:space="preserve"> + <source>Improved message delivery</source> + <target>Továbbfejlesztett üzenetkézbesítés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved privacy and security" xml:space="preserve"> + <source>Improved privacy and security</source> + <target>Fejlesztett adatvédelem és biztonság</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved server configuration" xml:space="preserve"> + <source>Improved server configuration</source> + <target>Javított kiszolgáló konfiguráció</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="In order to continue, chat should be stopped." xml:space="preserve"> + <source>In order to continue, chat should be stopped.</source> + <target>A folytatáshoz a csevegést meg kell szakítani.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="In reply to" xml:space="preserve"> + <source>In reply to</source> + <target>Válaszul erre</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="In-call sounds" xml:space="preserve"> + <source>In-call sounds</source> + <target>Bejövő hívás csengőhangja</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Inappropriate content" xml:space="preserve"> + <source>Inappropriate content</source> + <target>Kifogásolt tartalom</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Inappropriate profile" xml:space="preserve"> + <source>Inappropriate profile</source> + <target>Kifogásolt profil</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Incognito" xml:space="preserve"> + <source>Incognito</source> + <target>Inkognitó</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incognito groups" xml:space="preserve"> + <source>Incognito groups</source> + <target>Inkognitócsoportok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incognito mode" xml:space="preserve"> + <source>Incognito mode</source> + <target>Inkognitómód</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve"> + <source>Incognito mode protects your privacy by using a new random profile for each contact.</source> + <target>Az inkognitómód úgy védi a személyes adatait, hogy az összes partneréhez új, véletlenszerű profilt használ.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incoming audio call" xml:space="preserve"> + <source>Incoming audio call</source> + <target>Bejövő hanghívás</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Incoming call" xml:space="preserve"> + <source>Incoming call</source> + <target>Bejövő hívás</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Incoming video call" xml:space="preserve"> + <source>Incoming video call</source> + <target>Bejövő videóhívás</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <target>Nem kompatibilis adatbázis-verzió</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible version" xml:space="preserve"> + <source>Incompatible version</source> + <target>Nem kompatibilis verzió</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incorrect passcode" xml:space="preserve"> + <source>Incorrect passcode</source> + <target>Téves jelkód</target> + <note>PIN entry</note> + </trans-unit> + <trans-unit id="Incorrect security code!" xml:space="preserve"> + <source>Incorrect security code!</source> + <target>Helytelen biztonsági kód!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Info" xml:space="preserve"> + <source>Info</source> + <target>Információ</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Initial role" xml:space="preserve"> + <source>Initial role</source> + <target>Kezdeti szerepkör</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> + <source>Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)</source> + <target>A [SimpleX Chat terminálhoz] telepítése (https://github.com/simplex-chat/simplex-chat)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Instant" xml:space="preserve"> + <source>Instant</source> + <target>Azonnali</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> + <source>Instant push notifications will be hidden! +</source> + <target>Az azonnali push-értesítések el lesznek rejtve! +</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Interface" xml:space="preserve"> + <source>Interface</source> + <target>Kezelőfelület</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Interface colors" xml:space="preserve"> + <source>Interface colors</source> + <target>Kezelőfelület színei</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid" xml:space="preserve"> + <source>Invalid</source> + <target>Érvénytelen</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (bad token)" xml:space="preserve"> + <source>Invalid (bad token)</source> + <target>Érvénytelen (hibás token)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (expired)" xml:space="preserve"> + <source>Invalid (expired)</source> + <target>Érvénytelen (lejárt)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (unregistered)" xml:space="preserve"> + <source>Invalid (unregistered)</source> + <target>Érvénytelen (nincs regisztrálva)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (wrong topic)" xml:space="preserve"> + <source>Invalid (wrong topic)</source> + <target>Érvénytelen (rossz topic)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid QR code" xml:space="preserve"> + <source>Invalid QR code</source> + <target>Érvénytelen QR-kód</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid connection link" xml:space="preserve"> + <source>Invalid connection link</source> + <target>Érvénytelen kapcsolattartási hivatkozás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid display name!" xml:space="preserve"> + <source>Invalid display name!</source> + <target>Érvénytelen megjelenítendő név!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid link" xml:space="preserve"> + <source>Invalid link</source> + <target>Érvénytelen hivatkozás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Érvénytelen átköltöztetési visszaigazolás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid name!" xml:space="preserve"> + <source>Invalid name!</source> + <target>Érvénytelen név!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid response" xml:space="preserve"> + <source>Invalid response</source> + <target>Érvénytelen válasz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid server address!" xml:space="preserve"> + <source>Invalid server address!</source> + <target>Érvénytelen kiszolgálócím!</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Invalid status" xml:space="preserve"> + <source>Invalid status</source> + <target>Érvénytelen állapot</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Invitation expired!" xml:space="preserve"> + <source>Invitation expired!</source> + <target>A meghívó lejárt!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invite friends" xml:space="preserve"> + <source>Invite friends</source> + <target>Barátok meghívása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invite members" xml:space="preserve"> + <source>Invite members</source> + <target>Tagok meghívása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invite to chat" xml:space="preserve"> + <source>Invite to chat</source> + <target>Meghívás a csevegésbe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invite to group" xml:space="preserve"> + <source>Invite to group</source> + <target>Meghívás a csoportba</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Irreversible message deletion" xml:space="preserve"> + <source>Irreversible message deletion</source> + <target>Végleges üzenettörlés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Irreversible message deletion is prohibited in this chat." xml:space="preserve"> + <source>Irreversible message deletion is prohibited in this chat.</source> + <target>Az üzenetek végleges törlése le van tiltva ebben a csevegésben.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> + <target>Az üzenetek végleges törlése le van tiltva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve"> + <source>It allows having many anonymous connections without any shared data between them in a single chat profile.</source> + <target>Lehetővé teszi, hogy egyetlen csevegési profilon belül több névtelen kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="It can happen when you or your connection used the old database backup." xml:space="preserve"> + <source>It can happen when you or your connection used the old database backup.</source> + <target>Ez akkor fordulhat elő, ha Ön vagy a partnere régi adatbázis biztonsági mentést használt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="It can happen when: 1. The messages expired in the sending client after 2 days or on the server after 30 days. 2. Message decryption failed, because you or your contact used old database backup. 3. The connection was compromised." xml:space="preserve"> + <source>It can happen when: +1. The messages expired in the sending client after 2 days or on the server after 30 days. +2. Message decryption failed, because you or your contact used old database backup. +3. The connection was compromised.</source> + <target>Ez akkor fordulhat elő, ha: +1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak. +2. Nem sikerült az üzenetet visszafejteni, mert Ön, vagy a partnere régebbi adatbázis biztonsági mentést használt. +3. A kapcsolat sérült.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="It protects your IP address and connections." xml:space="preserve"> + <source>It protects your IP address and connections.</source> + <target>Védi az IP-címét és a kapcsolatait.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> + <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> + <target>Úgy tűnik, már kapcsolódott ezen a hivatkozáson keresztül. Ha ez nem így van, akkor hiba történt (%@).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Italian interface" xml:space="preserve"> + <source>Italian interface</source> + <target>Olasz kezelőfelület</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Japanese interface" xml:space="preserve"> + <source>Japanese interface</source> + <target>Japán kezelőfelület</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Join" xml:space="preserve"> + <source>Join</source> + <target>Csatlakozás</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Join group" xml:space="preserve"> + <source>Join group</source> + <target>Csatlakozás csoporthoz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Join group conversations" xml:space="preserve"> + <source>Join group conversations</source> + <target>Csatlakozás csoportos beszélgetésekhez</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Join group?" xml:space="preserve"> + <source>Join group?</source> + <target>Csatlakozik a csoporthoz?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Join incognito" xml:space="preserve"> + <source>Join incognito</source> + <target>Csatlakozás inkognitóban</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Join with current profile" xml:space="preserve"> + <source>Join with current profile</source> + <target>Csatlakozás a jelenlegi profillal</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Join your group? This is your link for group %@!" xml:space="preserve"> + <source>Join your group? +This is your link for group %@!</source> + <target>Csatlakozik a csoportjához? +Ez a saját hivatkozása a(z) %@ nevű csoporthoz!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Joining group" xml:space="preserve"> + <source>Joining group</source> + <target>Csatlakozás a csoporthoz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keep" xml:space="preserve"> + <source>Keep</source> + <target>Megtartás</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Keep conversation" xml:space="preserve"> + <source>Keep conversation</source> + <target>Beszélgetés megtartása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keep the app open to use it from desktop" xml:space="preserve"> + <source>Keep the app open to use it from desktop</source> + <target>A számítógépről való használathoz tartsd nyitva az alkalmazást</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keep unused invitation?" xml:space="preserve"> + <source>Keep unused invitation?</source> + <target>Megtartja a fel nem használt meghívót?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Keep your connections" xml:space="preserve"> + <source>Keep your connections</source> + <target>Kapcsolatok megtartása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="KeyChain error" xml:space="preserve"> + <source>KeyChain error</source> + <target>Kulcstartóhiba</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <target>Kulcstartóhiba</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="LIVE" xml:space="preserve"> + <source>LIVE</source> + <target>ÉLŐ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <target>Nagy fájl!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Learn more" xml:space="preserve"> + <source>Learn more</source> + <target>Tudjon meg többet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave" xml:space="preserve"> + <source>Leave</source> + <target>Elhagyás</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Leave chat" xml:space="preserve"> + <source>Leave chat</source> + <target>Csevegés elhagyása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave chat?" xml:space="preserve"> + <source>Leave chat?</source> + <target>Elhagyja a csevegést?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave group" xml:space="preserve"> + <source>Leave group</source> + <target>Csoport elhagyása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave group?" xml:space="preserve"> + <source>Leave group?</source> + <target>Elhagyja a csoportot?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Let's talk in SimpleX Chat" xml:space="preserve"> + <source>Let's talk in SimpleX Chat</source> + <target>Beszélgessünk a SimpleX Chatben</target> + <note>email subject</note> + </trans-unit> + <trans-unit id="Light" xml:space="preserve"> + <source>Light</source> + <target>Világos</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Limitations" xml:space="preserve"> + <source>Limitations</source> + <target>Korlátozások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Link mobile and desktop apps! 🔗" xml:space="preserve"> + <source>Link mobile and desktop apps! 🔗</source> + <target>Társítsa össze a hordozható eszköz- és számítógépes alkalmazásokat! 🔗</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Linked desktop options" xml:space="preserve"> + <source>Linked desktop options</source> + <target>Társított számítógép beállítások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Linked desktops" xml:space="preserve"> + <source>Linked desktops</source> + <target>Társított számítógépek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List" xml:space="preserve"> + <source>List</source> + <target>Lista</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="List name and emoji should be different for all lists." xml:space="preserve"> + <source>List name and emoji should be different for all lists.</source> + <target>Az összes lista nevének és emodzsijának különbözőnek kell lennie.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List name..." xml:space="preserve"> + <source>List name...</source> + <target>Lista neve…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Live message!" xml:space="preserve"> + <source>Live message!</source> + <target>Élő üzenet!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Live messages" xml:space="preserve"> + <source>Live messages</source> + <target>Élő üzenetek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Local name" xml:space="preserve"> + <source>Local name</source> + <target>Helyi név</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Local profile data only" xml:space="preserve"> + <source>Local profile data only</source> + <target>Csak helyi profiladatok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Lock after" xml:space="preserve"> + <source>Lock after</source> + <target>Zárolás miután</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Lock mode" xml:space="preserve"> + <source>Lock mode</source> + <target>Zárolási mód</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Make one message disappear" xml:space="preserve"> + <source>Make one message disappear</source> + <target>Egy üzenet eltüntetése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Make profile private!" xml:space="preserve"> + <source>Make profile private!</source> + <target>Tegye priváttá a profilját!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> + <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> + <target>Győződjön meg arról, hogy a megadott WebRTC ICE-kiszolgálók címei megfelelő formátumúak, soronként elkülönítettek, és nincsenek duplikálva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mark deleted for everyone" xml:space="preserve"> + <source>Mark deleted for everyone</source> + <target>Jelölje meg az összes tag számára töröltként</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mark read" xml:space="preserve"> + <source>Mark read</source> + <target>Megjelölés olvasottként</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mark verified" xml:space="preserve"> + <source>Mark verified</source> + <target>Hitelesítés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Markdown in messages" xml:space="preserve"> + <source>Markdown in messages</source> + <target>Markdown az üzenetekben</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Max 30 seconds, received instantly." xml:space="preserve"> + <source>Max 30 seconds, received instantly.</source> + <target>Max. 30 másodperc, azonnal érkezett.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Media & file servers" xml:space="preserve"> + <source>Media & file servers</source> + <target>Média- és fájlkiszolgálók</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Medium" xml:space="preserve"> + <source>Medium</source> + <target>Közepes</target> + <note>blur media</note> + </trans-unit> + <trans-unit id="Member" xml:space="preserve"> + <source>Member</source> + <target>Tag</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Member inactive" xml:space="preserve"> + <source>Member inactive</source> + <target>Inaktív tag</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Member reports" xml:space="preserve"> + <source>Member reports</source> + <target>Tagok jelentései</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All chat members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All chat members will be notified.</source> + <target>A tag szerepköre a következőre fog módosulni: „%@”. A csevegés összes tagja értesítést fog kapni.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All group members will be notified.</source> + <target>A tag szerepköre a következőre fog módosulni: „%@”. A csoport az összes tagja értesítést fog kapni.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". The member will receive a new invitation." xml:space="preserve"> + <source>Member role will be changed to "%@". The member will receive a new invitation.</source> + <target>A tag szerepköre a következőre fog módosulni: „%@”. A tag új meghívást fog kapni.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from chat - this cannot be undone!</source> + <target>A tag el lesz távolítva a csevegésből – ez a művelet nem vonható vissza!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from group - this cannot be undone!</source> + <target>A tag el lesz távolítva a csoportból – ez a művelet nem vonható vissza!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can add message reactions." xml:space="preserve"> + <source>Members can add message reactions.</source> + <target>A tagok reakciókat adhatnak hozzá az üzenetekhez.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Members can irreversibly delete sent messages. (24 hours)</source> + <target>A tagok véglegesen törölhetik az elküldött üzeneteiket. (24 óra)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can report messsages to moderators." xml:space="preserve"> + <source>Members can report messsages to moderators.</source> + <target>A tagok jelenthetik az üzeneteket a moderátorok felé.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send SimpleX links." xml:space="preserve"> + <source>Members can send SimpleX links.</source> + <target>A tagok küldhetnek SimpleX-hivatkozásokat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> + <target>A tagok küldhetnek egymásnak közvetlen üzeneteket.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> + <target>A tagok küldhetnek eltűnő üzeneteket.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send files and media." xml:space="preserve"> + <source>Members can send files and media.</source> + <target>A tagok küldhetnek fájlokat és médiatartalmakat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> + <target>A tagok küldhetnek hangüzeneteket.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mention members 👋" xml:space="preserve"> + <source>Mention members 👋</source> + <target>Tagok említése 👋</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Menus" xml:space="preserve"> + <source>Menus</source> + <target>Menük</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message delivery error" xml:space="preserve"> + <source>Message delivery error</source> + <target>Üzenetkézbesítési hiba</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message delivery receipts!" xml:space="preserve"> + <source>Message delivery receipts!</source> + <target>Üzenetkézbesítési jelentések!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message delivery warning" xml:space="preserve"> + <source>Message delivery warning</source> + <target>Üzenetkézbesítési figyelmeztetés</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message draft" xml:space="preserve"> + <source>Message draft</source> + <target>Üzenetvázlat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message forwarded" xml:space="preserve"> + <source>Message forwarded</source> + <target>Továbbított üzenet</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message may be delivered later if member becomes active." xml:space="preserve"> + <source>Message may be delivered later if member becomes active.</source> + <target>Az üzenet később is kézbesíthető, ha a tag aktívvá válik.</target> + <note>item status description</note> + </trans-unit> + <trans-unit id="Message queue info" xml:space="preserve"> + <source>Message queue info</source> + <target>Üzenetsorbaállítási információ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message reactions" xml:space="preserve"> + <source>Message reactions</source> + <target>Üzenetreakciók</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Message reactions are prohibited in this chat." xml:space="preserve"> + <source>Message reactions are prohibited in this chat.</source> + <target>A reakciók hozzáadása az üzenetekhez le van tiltva ebben a csevegésben.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message reactions are prohibited." xml:space="preserve"> + <source>Message reactions are prohibited.</source> + <target>A reakciók hozzáadása az üzenetekhez le van tiltva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message reception" xml:space="preserve"> + <source>Message reception</source> + <target>Üzenetjelentés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message servers" xml:space="preserve"> + <source>Message servers</source> + <target>Üzenetkiszolgálók</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message shape" xml:space="preserve"> + <source>Message shape</source> + <target>Üzenetbuborék alakja</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message source remains private." xml:space="preserve"> + <source>Message source remains private.</source> + <target>Az üzenet forrása titokban marad.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status" xml:space="preserve"> + <source>Message status</source> + <target>Üzenet állapota</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status: %@" xml:space="preserve"> + <source>Message status: %@</source> + <target>Üzenet állapota: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Message text" xml:space="preserve"> + <source>Message text</source> + <target>Név és üzenet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message too large" xml:space="preserve"> + <source>Message too large</source> + <target>Az üzenet túl nagy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages" xml:space="preserve"> + <source>Messages</source> + <target>Üzenetek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages & files" xml:space="preserve"> + <source>Messages & files</source> + <target>Üzenetek és fájlok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages from %@ will be shown!" xml:space="preserve"> + <source>Messages from %@ will be shown!</source> + <target>%@ összes üzenete meg fog jelenni!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages in this chat will never be deleted." xml:space="preserve"> + <source>Messages in this chat will never be deleted.</source> + <target>Az ebben a csevegésben lévő üzenetek soha nem lesznek törölve.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages received" xml:space="preserve"> + <source>Messages received</source> + <target>Fogadott üzenetek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages sent" xml:space="preserve"> + <source>Messages sent</source> + <target>Elküldött üzenetek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages were deleted after you selected them." xml:space="preserve"> + <source>Messages were deleted after you selected them.</source> + <target>Az üzeneteket törölték miután kijelölte őket.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Az üzenetek, a fájlok és a hívások **végpontok közötti titkosítással**, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Az üzenetek, a fájlok és a hívások **végpontok közötti kvantumbiztos titkosítással**, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate device" xml:space="preserve"> + <source>Migrate device</source> + <target>Eszköz átköltöztetése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate from another device" xml:space="preserve"> + <source>Migrate from another device</source> + <target>Átköltöztetés egy másik eszközről</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate here" xml:space="preserve"> + <source>Migrate here</source> + <target>Átköltöztetés ide</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device" xml:space="preserve"> + <source>Migrate to another device</source> + <target>Átköltöztetés egy másik eszközre</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device via QR code." xml:space="preserve"> + <source>Migrate to another device via QR code.</source> + <target>Átköltöztetés egy másik eszközre QR-kód használatával.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating" xml:space="preserve"> + <source>Migrating</source> + <target>Átköltöztetés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating database archive…" xml:space="preserve"> + <source>Migrating database archive…</source> + <target>Adatbázis-archívum átköltöztetése…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migration complete" xml:space="preserve"> + <source>Migration complete</source> + <target>Átköltöztetés befejezve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migration error:" xml:space="preserve"> + <source>Migration error:</source> + <target>Átköltöztetési hiba:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." xml:space="preserve"> + <source>Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat).</source> + <target>Sikertelen átköltöztetés. Koppintson a **Kihagyás** lehetőségre a jelenlegi adatbázis használatának folytatásához. Jelentse a problémát az alkalmazás fejlesztőinek csevegésben vagy e-mailben [chat@simplex.chat](mailto:chat@simplex.chat).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migration is completed" xml:space="preserve"> + <source>Migration is completed</source> + <target>Az átköltöztetés befejeződött</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> + <target>Átköltöztetések:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Moderate" xml:space="preserve"> + <source>Moderate</source> + <target>Moderálás</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Moderated at" xml:space="preserve"> + <source>Moderated at</source> + <target>Moderálva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Moderated at: %@" xml:space="preserve"> + <source>Moderated at: %@</source> + <target>Moderálva: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="More" xml:space="preserve"> + <source>More</source> + <target>Továbbiak</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="More improvements are coming soon!" xml:space="preserve"> + <source>More improvements are coming soon!</source> + <target>Hamarosan további fejlesztések érkeznek!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="More reliable network connection." xml:space="preserve"> + <source>More reliable network connection.</source> + <target>Megbízhatóbb hálózati kapcsolat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="More reliable notifications" xml:space="preserve"> + <source>More reliable notifications</source> + <target>Megbízhatóbb értesítések</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> + <source>Most likely this connection is deleted.</source> + <target>Valószínűleg ez a kapcsolat törölve lett.</target> + <note>item status description</note> + </trans-unit> + <trans-unit id="Multiple chat profiles" xml:space="preserve"> + <source>Multiple chat profiles</source> + <target>Több csevegési profil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mute" xml:space="preserve"> + <source>Mute</source> + <target>Némítás</target> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Mute all" xml:space="preserve"> + <source>Mute all</source> + <target>Összes némítása</target> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Muted when inactive!" xml:space="preserve"> + <source>Muted when inactive!</source> + <target>Némítás, ha inaktív!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Name" xml:space="preserve"> + <source>Name</source> + <target>Név</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Network & servers" xml:space="preserve"> + <source>Network & servers</source> + <target>Hálózat és kiszolgálók</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network connection" xml:space="preserve"> + <source>Network connection</source> + <target>Hálózati kapcsolat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network decentralization" xml:space="preserve"> + <source>Network decentralization</source> + <target>Hálózati decentralizáció</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve"> + <source>Network issues - message expired after many attempts to send it.</source> + <target>Hálózati problémák – az üzenet többszöri elküldési kísérlet után lejárt.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Network management" xml:space="preserve"> + <source>Network management</source> + <target>Hálózatkezelés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network operator" xml:space="preserve"> + <source>Network operator</source> + <target>Hálózatüzemeltető</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network settings" xml:space="preserve"> + <source>Network settings</source> + <target>Hálózati beállítások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network status" xml:space="preserve"> + <source>Network status</source> + <target>Hálózat állapota</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New" xml:space="preserve"> + <source>New</source> + <target>Új</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="New Passcode" xml:space="preserve"> + <source>New Passcode</source> + <target>Új jelkód</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve"> + <source>New SOCKS credentials will be used every time you start the app.</source> + <target>Minden alkalommal, amikor elindítja az alkalmazást, új SOCKS-hitelesítő-adatokat fog használni.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve"> + <source>New SOCKS credentials will be used for each server.</source> + <target>Az összes kiszolgálóhoz új, SOCKS-hitelesítő-adatok legyenek használva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat" xml:space="preserve"> + <source>New chat</source> + <target>Új csevegés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat experience 🎉" xml:space="preserve"> + <source>New chat experience 🎉</source> + <target>Új csevegési élmény 🎉</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New contact request" xml:space="preserve"> + <source>New contact request</source> + <target>Új meghívási kérés</target> + <note>notification</note> + </trans-unit> + <trans-unit id="New contact:" xml:space="preserve"> + <source>New contact:</source> + <target>Új kapcsolat:</target> + <note>notification</note> + </trans-unit> + <trans-unit id="New desktop app!" xml:space="preserve"> + <source>New desktop app!</source> + <target>Új számítógép-alkalmazás!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New display name" xml:space="preserve"> + <source>New display name</source> + <target>Új megjelenítendő név</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>Új események</target> + <note>notification</note> + </trans-unit> + <trans-unit id="New in %@" xml:space="preserve"> + <source>New in %@</source> + <target>Újdonságok a(z) %@ verzióban</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New media options" xml:space="preserve"> + <source>New media options</source> + <target>Új médiabeállítások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New member role" xml:space="preserve"> + <source>New member role</source> + <target>Új tag szerepköre</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New message" xml:space="preserve"> + <source>New message</source> + <target>Új üzenet</target> + <note>notification</note> + </trans-unit> + <trans-unit id="New passphrase…" xml:space="preserve"> + <source>New passphrase…</source> + <target>Új jelmondat…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New server" xml:space="preserve"> + <source>New server</source> + <target>Új kiszolgáló</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No" xml:space="preserve"> + <source>No</source> + <target>Nem</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No app password" xml:space="preserve"> + <source>No app password</source> + <target>Nincs alkalmazás jelszó</target> + <note>Authentication unavailable</note> + </trans-unit> + <trans-unit id="No chats" xml:space="preserve"> + <source>No chats</source> + <target>Nincsenek csevegések</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats found" xml:space="preserve"> + <source>No chats found</source> + <target>Nem találhatók csevegések</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats in list %@" xml:space="preserve"> + <source>No chats in list %@</source> + <target>Nincsenek csevegések a(z) %@ nevű listában</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No contacts selected" xml:space="preserve"> + <source>No contacts selected</source> + <target>Nincs partner kijelölve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No contacts to add" xml:space="preserve"> + <source>No contacts to add</source> + <target>Nincs hozzáadandó partner</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No delivery information" xml:space="preserve"> + <source>No delivery information</source> + <target>Nincs kézbesítési információ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No device token!" xml:space="preserve"> + <source>No device token!</source> + <target>Nincs készüléktoken!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No direct connection yet, message is forwarded by admin." xml:space="preserve"> + <source>No direct connection yet, message is forwarded by admin.</source> + <target>Még nincs közvetlen kapcsolat, az üzenetet az adminisztrátor továbbítja.</target> + <note>item status description</note> + </trans-unit> + <trans-unit id="No filtered chats" xml:space="preserve"> + <source>No filtered chats</source> + <target>Nincsenek szűrt csevegések</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No group!" xml:space="preserve"> + <source>Group not found!</source> + <target>Csoport nem található!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No history" xml:space="preserve"> + <source>No history</source> + <target>Nincsenek előzmények</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No info, try to reload" xml:space="preserve"> + <source>No info, try to reload</source> + <target>Nincs információ, próbálja meg újratölteni</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No media & file servers." xml:space="preserve"> + <source>No media & file servers.</source> + <target>Nincsenek média- és fájlkiszolgálók.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No message" xml:space="preserve"> + <source>No message</source> + <target>Nincs üzenet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No message servers." xml:space="preserve"> + <source>No message servers.</source> + <target>Nincsenek üzenet-kiszolgálók.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No network connection" xml:space="preserve"> + <source>No network connection</source> + <target>Nincs hálózati kapcsolat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record speech" xml:space="preserve"> + <source>No permission to record speech</source> + <target>Nincs jogosultság megadva a beszéd rögzítéséhez</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record video" xml:space="preserve"> + <source>No permission to record video</source> + <target>Nincs jogosultság megadva a videó rögzítéséhez</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record voice message" xml:space="preserve"> + <source>No permission to record voice message</source> + <target>Nincs engedély a hangüzenet rögzítésére</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No push server" xml:space="preserve"> + <source>No push server</source> + <target>Helyi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No received or sent files" xml:space="preserve"> + <source>No received or sent files</source> + <target>Nincsenek fogadott vagy küldött fájlok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No servers for private message routing." xml:space="preserve"> + <source>No servers for private message routing.</source> + <target>Nincsenek kiszolgálók a privát üzenet-útválasztáshoz.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive files." xml:space="preserve"> + <source>No servers to receive files.</source> + <target>Nincsenek fájlfogadási kiszolgálók.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive messages." xml:space="preserve"> + <source>No servers to receive messages.</source> + <target>Nincsenek üzenetfogadási kiszolgálók.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to send files." xml:space="preserve"> + <source>No servers to send files.</source> + <target>Nincsenek fájlküldő-kiszolgálók.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No token!" xml:space="preserve"> + <source>No token!</source> + <target>Nincs token!</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="No unread chats" xml:space="preserve"> + <source>No unread chats</source> + <target>Nincsenek olvasatlan csevegések</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> + <target>Nincsenek felhasználó-azonosítók.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Not compatible!" xml:space="preserve"> + <source>Not compatible!</source> + <target>Nem kompatibilis!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notes" xml:space="preserve"> + <source>Notes</source> + <target>Jegyzetek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing selected" xml:space="preserve"> + <source>Nothing selected</source> + <target>Nincs semmi kijelölve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing to forward!" xml:space="preserve"> + <source>Nothing to forward!</source> + <target>Nincs mit továbbítani!</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Notifications" xml:space="preserve"> + <source>Notifications</source> + <target>Értesítések</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications are disabled!" xml:space="preserve"> + <source>Notifications are disabled!</source> + <target>Az értesítések le vannak tiltva!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications error" xml:space="preserve"> + <source>Notifications error</source> + <target>Értesítési hiba</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Notifications privacy" xml:space="preserve"> + <source>Notifications privacy</source> + <target>Értesítési adatvédelem</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications status" xml:space="preserve"> + <source>Notifications status</source> + <target>Értesítések állapota</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> + <source>Now admins can: +- delete members' messages. +- disable members ("observer" role)</source> + <target>Most már az adminisztrátorok is: +- törölhetik a tagok üzeneteit. +- letilthatnak tagokat („megfigyelő” szerepkör)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="OK" xml:space="preserve"> + <source>OK</source> + <target>Rendben</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Off" xml:space="preserve"> + <source>Off</source> + <target>Kikapcsolva</target> + <note>blur media</note> + </trans-unit> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <target>Rendben</target> + <note>alert button</note> + </trans-unit> + <trans-unit id="Old database" xml:space="preserve"> + <source>Old database</source> + <target>Régi adatbázis</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="One-time invitation link" xml:space="preserve"> + <source>One-time invitation link</source> + <target>Egyszer használható meghívó</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Onion hosts will be **required** for connection. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be **required** for connection. +Requires compatible VPN.</source> + <target>Onion-kiszolgálók **szükségesek** a kapcsolódáshoz. +Kompatibilis VPN szükséges.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Onion hosts will be used when available. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be used when available. +Requires compatible VPN.</source> + <target>Onion-kiszolgálók használata, ha azok rendelkezésre állnak. +VPN engedélyezése szükséges.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Onion hosts will not be used." xml:space="preserve"> + <source>Onion hosts will not be used.</source> + <target>Az onion-kiszolgálók nem lesznek használva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only chat owners can change preferences." xml:space="preserve"> + <source>Only chat owners can change preferences.</source> + <target>Csak a csevegés tulajdonosai módosíthatják a csevegési beállításokat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> + <target>A felhasználói profilok, partnerek, csoportok és üzenetek csak az eszközön vannak tárolva a kliensen belül.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only delete conversation" xml:space="preserve"> + <source>Only delete conversation</source> + <target>Csak a beszélgetés törlése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> + <source>Only group owners can change group preferences.</source> + <target>Csak a csoport tulajdonosai módosíthatják a csoportbeállításokat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only group owners can enable files and media." xml:space="preserve"> + <source>Only group owners can enable files and media.</source> + <target>Csak a csoport tulajdonosai engedélyezhetik a fájlok- és a médiatartalmak küldését.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only group owners can enable voice messages." xml:space="preserve"> + <source>Only group owners can enable voice messages.</source> + <target>Csak a csoport tulajdonosai engedélyezhetik a hangüzenetek küldését.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only sender and moderators see it" xml:space="preserve"> + <source>Only sender and moderators see it</source> + <target>Csak a küldő és a moderátorok látják</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you and moderators see it" xml:space="preserve"> + <source>Only you and moderators see it</source> + <target>Csak Ön és a moderátorok látják</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you can add message reactions." xml:space="preserve"> + <source>Only you can add message reactions.</source> + <target>Csak Ön adhat hozzá reakciókat az üzenetekhez.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source> + <target>Véglegesen csak Ön törölhet üzeneteket (partnere csak törlésre jelölheti meg őket ). (24 óra)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you can make calls." xml:space="preserve"> + <source>Only you can make calls.</source> + <target>Csak Ön tud hívásokat indítani.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you can send disappearing messages." xml:space="preserve"> + <source>Only you can send disappearing messages.</source> + <target>Csak Ön tud eltűnő üzeneteket küldeni.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you can send voice messages." xml:space="preserve"> + <source>Only you can send voice messages.</source> + <target>Csak Ön tud hangüzeneteket küldeni.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only your contact can add message reactions." xml:space="preserve"> + <source>Only your contact can add message reactions.</source> + <target>Csak a partnere adhat hozzá reakciókat az üzenetekhez.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source> + <target>Csak a partnere tudja az üzeneteket véglegesen törölni (Ön csak törlésre jelölheti meg azokat). (24 óra)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only your contact can make calls." xml:space="preserve"> + <source>Only your contact can make calls.</source> + <target>Csak a partnere tud hívást indítani.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only your contact can send disappearing messages." xml:space="preserve"> + <source>Only your contact can send disappearing messages.</source> + <target>Csak a partnere tud eltűnő üzeneteket küldeni.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only your contact can send voice messages." xml:space="preserve"> + <source>Only your contact can send voice messages.</source> + <target>Csak a partnere tud hangüzeneteket küldeni.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open" xml:space="preserve"> + <source>Open</source> + <target>Megnyitás</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Open Settings" xml:space="preserve"> + <source>Open Settings</source> + <target>Beállítások megnyitása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open changes" xml:space="preserve"> + <source>Open changes</source> + <target>Módosítások megtekintése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open chat" xml:space="preserve"> + <source>Open chat</source> + <target>Csevegés megnyitása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open chat console" xml:space="preserve"> + <source>Open chat console</source> + <target>Csevegési konzol megnyitása</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Open conditions" xml:space="preserve"> + <source>Open conditions</source> + <target>Feltételek megnyitása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open group" xml:space="preserve"> + <source>Open group</source> + <target>Csoport megnyitása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open link?" xml:space="preserve"> + <source>Open link?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Open migration to another device" xml:space="preserve"> + <source>Open migration to another device</source> + <target>Átköltöztetés indítása egy másik eszközre</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Opening app…" xml:space="preserve"> + <source>Opening app…</source> + <target>Az alkalmazás megnyitása…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Operator" xml:space="preserve"> + <source>Operator</source> + <target>Üzemeltető</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Operator server" xml:space="preserve"> + <source>Operator server</source> + <target>Kiszolgáló-üzemeltető</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Or import archive file" xml:space="preserve"> + <source>Or import archive file</source> + <target>Vagy archívumfájl importálása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or paste archive link" xml:space="preserve"> + <source>Or paste archive link</source> + <target>Vagy az archívum hivatkozásának beillesztése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or scan QR code" xml:space="preserve"> + <source>Or scan QR code</source> + <target>Vagy QR-kód beolvasása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or securely share this file link" xml:space="preserve"> + <source>Or securely share this file link</source> + <target>Vagy ossza meg biztonságosan ezt a fájlhivatkozást</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or show this code" xml:space="preserve"> + <source>Or show this code</source> + <target>Vagy mutassa meg ezt a kódot</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or to share privately" xml:space="preserve"> + <source>Or to share privately</source> + <target>Vagy a privát megosztáshoz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Organize chats into lists" xml:space="preserve"> + <source>Organize chats into lists</source> + <target>Csevegések listákba szervezése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other" xml:space="preserve"> + <source>Other</source> + <target>További</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other file errors: %@" xml:space="preserve"> + <source>Other file errors: +%@</source> + <target>Egyéb fájlhiba: +%@</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="PING count" xml:space="preserve"> + <source>PING count</source> + <target>PING-ek száma</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="PING interval" xml:space="preserve"> + <source>PING interval</source> + <target>Időtartam a PING-ek között</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passcode" xml:space="preserve"> + <source>Passcode</source> + <target>Jelkód</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passcode changed!" xml:space="preserve"> + <source>Passcode changed!</source> + <target>A jelkód módosult!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passcode entry" xml:space="preserve"> + <source>Passcode entry</source> + <target>Jelkód bevitele</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passcode not changed!" xml:space="preserve"> + <source>Passcode not changed!</source> + <target>A jelkód nem módosult!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passcode set!" xml:space="preserve"> + <source>Passcode set!</source> + <target>A jelkód beállítva!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Password" xml:space="preserve"> + <source>Password</source> + <target>Jelszó</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Password to show" xml:space="preserve"> + <source>Password to show</source> + <target>Jelszó a megjelenítéshez</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Past member %@" xml:space="preserve"> + <source>Past member %@</source> + <target>(Már nem tag) %@</target> + <note>past/unknown group member</note> + </trans-unit> + <trans-unit id="Paste desktop address" xml:space="preserve"> + <source>Paste desktop address</source> + <target>Számítógép címének beillesztése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Paste image" xml:space="preserve"> + <source>Paste image</source> + <target>Kép beillesztése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Paste link to connect!" xml:space="preserve"> + <source>Paste link to connect!</source> + <target>Hivatkozás beillesztése a kapcsolódáshoz!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Paste the link you received" xml:space="preserve"> + <source>Paste the link you received</source> + <target>Kapott hivatkozás beillesztése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Pending" xml:space="preserve"> + <source>Pending</source> + <target>Függőben</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Periodic" xml:space="preserve"> + <source>Periodic</source> + <target>Időszakos</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Permanent decryption error" xml:space="preserve"> + <source>Permanent decryption error</source> + <target>Végleges visszafejtési hiba</target> + <note>message decrypt error item</note> + </trans-unit> + <trans-unit id="Picture-in-picture calls" xml:space="preserve"> + <source>Picture-in-picture calls</source> + <target>Kép a képben hívások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Play from the chat list." xml:space="preserve"> + <source>Play from the chat list.</source> + <target>Lejátszás a csevegési listából.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable calls." xml:space="preserve"> + <source>Please ask your contact to enable calls.</source> + <target>Kérje meg a partnerét, hogy engedélyezze a hívásokat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> + <source>Please ask your contact to enable sending voice messages.</source> + <target>Kérje meg a partnerét, hogy engedélyezze a hangüzenetek küldését.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers." xml:space="preserve"> + <source>Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers.</source> + <target>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. +Minden további problémát osszon meg a fejlesztőkkel.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> + <source>Please check that you used the correct link or ask your contact to send you another one.</source> + <target>Ellenőrizze, hogy a megfelelő hivatkozást használta-e, vagy kérje meg a partnerét, hogy küldjön egy másikat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please check your network connection with %@ and try again." xml:space="preserve"> + <source>Please check your network connection with %@ and try again.</source> + <target>Ellenőrizze a hálózati kapcsolatát a vele: %@, és próbálja újra.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please check yours and your contact preferences." xml:space="preserve"> + <source>Please check yours and your contact preferences.</source> + <target>Ellenőrizze a saját- és a partnere beállításait.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve"> + <source>Please confirm that network settings are correct for this device.</source> + <target>Ellenőrizze, hogy a hálózati beállítások megfelelők-e ehhez az eszközhöz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact developers. Error: %@" xml:space="preserve"> + <source>Please contact developers. +Error: %@</source> + <target>Lépjen kapcsolatba a fejlesztőkkel. +Hiba: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact group admin." xml:space="preserve"> + <source>Please contact group admin.</source> + <target>Lépjen kapcsolatba a csoport adminisztrátorával.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please enter correct current passphrase." xml:space="preserve"> + <source>Please enter correct current passphrase.</source> + <target>Adja meg a helyes, jelenlegi jelmondatot.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please enter the previous password after restoring database backup. This action can not be undone." xml:space="preserve"> + <source>Please enter the previous password after restoring database backup. This action can not be undone.</source> + <target>Adja meg a korábbi jelszót az adatbázis biztonsági mentésének visszaállítása után. Ez a művelet nem vonható vissza.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please remember or store it securely - there is no way to recover a lost passcode!" xml:space="preserve"> + <source>Please remember or store it securely - there is no way to recover a lost passcode!</source> + <target>Jegyezze fel vagy tárolja el biztonságosan – az elveszett jelkódot nem lehet visszaállítani!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please report it to the developers." xml:space="preserve"> + <source>Please report it to the developers.</source> + <target>Jelentse a fejlesztőknek.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please restart the app and migrate the database to enable push notifications." xml:space="preserve"> + <source>Please restart the app and migrate the database to enable push notifications.</source> + <target>Indítsa újra az alkalmazást az adatbázis-átköltöztetéséhez szükséges push-értesítések engedélyezéséhez.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please store passphrase securely, you will NOT be able to access chat if you lose it." xml:space="preserve"> + <source>Please store passphrase securely, you will NOT be able to access chat if you lose it.</source> + <target>Tárolja el biztonságosan jelmondát, mert ha elveszti azt, akkor NEM férhet hozzá a csevegéshez.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please store passphrase securely, you will NOT be able to change it if you lose it." xml:space="preserve"> + <source>Please store passphrase securely, you will NOT be able to change it if you lose it.</source> + <target>Tárolja el biztonságosan jelmondatát, mert ha elveszíti azt, NEM tudja módosítani.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please try to disable and re-enable notfications." xml:space="preserve"> + <source>Please try to disable and re-enable notfications.</source> + <target>Próbálja meg letiltani és újra engedélyezni az értesítéseket.</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token activation to complete." xml:space="preserve"> + <source>Please wait for token activation to complete.</source> + <target>Várjon, amíg a token aktiválása befejeződik.</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token to be registered." xml:space="preserve"> + <source>Please wait for token to be registered.</source> + <target>Várjon a token regisztrálására.</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Polish interface" xml:space="preserve"> + <source>Polish interface</source> + <target>Lengyel kezelőfelület</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Port" xml:space="preserve"> + <source>Port</source> + <target>Port</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> + <source>Possibly, certificate fingerprint in server address is incorrect</source> + <target>Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen</target> + <note>server test error</note> + </trans-unit> + <trans-unit id="Preserve the last message draft, with attachments." xml:space="preserve"> + <source>Preserve the last message draft, with attachments.</source> + <target>Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Preset server address" xml:space="preserve"> + <source>Preset server address</source> + <target>Az előre beállított kiszolgáló címe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Preset servers" xml:space="preserve"> + <source>Preset servers</source> + <target>Előre beállított kiszolgálók</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Preview" xml:space="preserve"> + <source>Preview</source> + <target>Előnézet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Previously connected servers" xml:space="preserve"> + <source>Previously connected servers</source> + <target>Korábban kapcsolódott kiszolgálók</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy & security" xml:space="preserve"> + <source>Privacy & security</source> + <target>Adatvédelem és biztonság</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy for your customers." xml:space="preserve"> + <source>Privacy for your customers.</source> + <target>Saját ügyfeleinek adatvédelme.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy policy and conditions of use." xml:space="preserve"> + <source>Privacy policy and conditions of use.</source> + <target>Adatvédelmi szabályzat és felhasználási feltételek.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy redefined" xml:space="preserve"> + <source>Privacy redefined</source> + <target>Újraértelmezett adatvédelem</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve"> + <source>Private chats, groups and your contacts are not accessible to server operators.</source> + <target>A privát csevegések, a csoportok és a partnerek nem érhetők el a szerver üzemeltetői számára.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private filenames" xml:space="preserve"> + <source>Private filenames</source> + <target>Privát fájlnevek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private media file names." xml:space="preserve"> + <source>Private media file names.</source> + <target>Privát nevek a médiafájlokhoz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing" xml:space="preserve"> + <source>Private message routing</source> + <target>Privát üzenet-útválasztás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing 🚀" xml:space="preserve"> + <source>Private message routing 🚀</source> + <target>Privát üzenet-útválasztás 🚀</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private notes" xml:space="preserve"> + <source>Private notes</source> + <target>Privát jegyzetek</target> + <note>name of notes to self</note> + </trans-unit> + <trans-unit id="Private routing" xml:space="preserve"> + <source>Private routing</source> + <target>Privát útválasztás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private routing error" xml:space="preserve"> + <source>Private routing error</source> + <target>Privát útválasztási hiba</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Profile and server connections" xml:space="preserve"> + <source>Profile and server connections</source> + <target>Profil és kiszolgálókapcsolatok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Profile image" xml:space="preserve"> + <source>Profile image</source> + <target>Profilkép</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Profile images" xml:space="preserve"> + <source>Profile images</source> + <target>Profilképek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Profile password" xml:space="preserve"> + <source>Profile password</source> + <target>Profiljelszó</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Profile theme" xml:space="preserve"> + <source>Profile theme</source> + <target>Profiltéma</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> + <source>Profile update will be sent to your contacts.</source> + <target>A profilfrissítés el lesz küldve a partnerei számára.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> + <source>Prohibit audio/video calls.</source> + <target>A hívások kezdeményezése le van tiltva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit irreversible message deletion." xml:space="preserve"> + <source>Prohibit irreversible message deletion.</source> + <target>Az elküldött üzenetek végleges törlése le van tiltva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit message reactions." xml:space="preserve"> + <source>Prohibit message reactions.</source> + <target>A reakciók hozzáadása az üzenethez le van tiltva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit messages reactions." xml:space="preserve"> + <source>Prohibit messages reactions.</source> + <target>A reakciók hozzáadása az üzenetekhez le van tiltva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit reporting messages to moderators." xml:space="preserve"> + <source>Prohibit reporting messages to moderators.</source> + <target>Az üzenetek a moderátorok felé történő jelentésének megtiltása.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending SimpleX links." xml:space="preserve"> + <source>Prohibit sending SimpleX links.</source> + <target>A SimpleX-hivatkozások küldése le van tiltva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> + <source>Prohibit sending direct messages to members.</source> + <target>A közvetlen üzenetek küldése a tagok között le van tiltva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending disappearing messages." xml:space="preserve"> + <source>Prohibit sending disappearing messages.</source> + <target>Az eltűnő üzenetek küldése le van tiltva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending files and media." xml:space="preserve"> + <source>Prohibit sending files and media.</source> + <target>A fájlok- és a médiatartalmak küldése le van tiltva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending voice messages." xml:space="preserve"> + <source>Prohibit sending voice messages.</source> + <target>A hangüzenetek küldése le van tiltva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Protect IP address" xml:space="preserve"> + <source>Protect IP address</source> + <target>IP-cím védelme</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Protect app screen" xml:space="preserve"> + <source>Protect app screen</source> + <target>Alkalmazás képernyőjének védelme</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve"> + <source>Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings.</source> + <target>Védje az IP-címét a partnerei által kiválasztott üzenetváltási továbbítókiszolgálókkal szemben. +Engedélyezze a *Hálózat és kiszolgálók* menüben.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> + <source>Protect your chat profiles with a password!</source> + <target>Védje meg a csevegési profiljait egy jelszóval!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Protocol timeout" xml:space="preserve"> + <source>Protocol timeout</source> + <target>Protokoll időtúllépése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Protocol timeout per KB" xml:space="preserve"> + <source>Protocol timeout per KB</source> + <target>Protokoll időtúllépése kB-onként</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied" xml:space="preserve"> + <source>Proxied</source> + <target>Proxyzott</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied servers" xml:space="preserve"> + <source>Proxied servers</source> + <target>Proxyzott kiszolgálók</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxy requires password" xml:space="preserve"> + <source>Proxy requires password</source> + <target>A proxy jelszót igényel</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Push notifications" xml:space="preserve"> + <source>Push notifications</source> + <target>Push-értesítések</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Push server" xml:space="preserve"> + <source>Push server</source> + <target>Push-kiszolgáló</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Quantum resistant encryption" xml:space="preserve"> + <source>Quantum resistant encryption</source> + <target>Kvantumbiztos titkosítás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Rate the app" xml:space="preserve"> + <source>Rate the app</source> + <target>Értékelje az alkalmazást</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reachable chat toolbar" xml:space="preserve"> + <source>Reachable chat toolbar</source> + <target>Könnyen elérhető eszköztár</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="React…" xml:space="preserve"> + <source>React…</source> + <target>Reagálj…</target> + <note>chat item menu</note> + </trans-unit> + <trans-unit id="Read" xml:space="preserve"> + <source>Read</source> + <target>Olvasott</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Read more" xml:space="preserve"> + <source>Read more</source> + <target>Tudjon meg többet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source> + <target>További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</source> + <target>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).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source> + <target>További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> + <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> + <target>További információ a [GitHub tárolóban](https://github.com/simplex-chat/simplex-chat#readme).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Receipts are disabled" xml:space="preserve"> + <source>Receipts are disabled</source> + <target>A kézbesítési jelentések le vannak tiltva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Receive errors" xml:space="preserve"> + <source>Receive errors</source> + <target>Üzenetfogadási hibák</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received at" xml:space="preserve"> + <source>Received at</source> + <target>Fogadva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received at: %@" xml:space="preserve"> + <source>Received at: %@</source> + <target>Fogadva: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Received file event" xml:space="preserve"> + <source>Received file event</source> + <target>Fogadott fájlesemény</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Received message" xml:space="preserve"> + <source>Received message</source> + <target>Fogadott üzenetbuborék színe</target> + <note>message info title</note> + </trans-unit> + <trans-unit id="Received messages" xml:space="preserve"> + <source>Received messages</source> + <target>Fogadott üzenetek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received reply" xml:space="preserve"> + <source>Received reply</source> + <target>Fogadott válaszüzenet-buborék színe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received total" xml:space="preserve"> + <source>Received total</source> + <target>Összes fogadott üzenet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> + <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> + <target>A fogadási cím egy másik kiszolgálóra fog módosulni. A cím módosítása a feladó online állapotba kerülése után fejeződik be.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Receiving file will be stopped." xml:space="preserve"> + <source>Receiving file will be stopped.</source> + <target>A fájl fogadása le fog állni.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Receiving via" xml:space="preserve"> + <source>Receiving via</source> + <target>Fogadás a következőn keresztül:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recent history and improved [directory bot](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)." xml:space="preserve"> + <source>Recent history and improved [directory bot](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).</source> + <target>Legutóbbi előzmények és továbbfejlesztett [könyvtárbot](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).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve"> + <source>Recipient(s) can't see who this message is from.</source> + <target>A címzett(ek) nem látja(k), hogy kitől származik ez az üzenet.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> + <source>Recipients see updates as you type them.</source> + <target>A címzettek a beírás közben látják a szövegváltozásokat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect" xml:space="preserve"> + <source>Reconnect</source> + <target>Újrakapcsolódás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> + <target>Az összes kiszolgálóhoz való újrakapcsolódás az üzenetkézbesítési jelentések kikényszerítéséhez. Ez további adatforgalmat használ.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers" xml:space="preserve"> + <source>Reconnect all servers</source> + <target>Újrakapcsolódás az összes kiszolgálóhoz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers?" xml:space="preserve"> + <source>Reconnect all servers?</source> + <target>Újrakapcsolódik az összes kiszolgálóhoz?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect server to force message delivery. It uses additional traffic.</source> + <target>A kiszolgálóhoz való újrakapcsolódás az üzenetkézbesítési jelentések kikényszerítéséhez. Ez további adatforgalmat használ.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server?" xml:space="preserve"> + <source>Reconnect server?</source> + <target>Újrakapcsolódik a kiszolgálóhoz?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect servers?" xml:space="preserve"> + <source>Reconnect servers?</source> + <target>Újrakapcsolódik a kiszolgálókhoz?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Record updated at" xml:space="preserve"> + <source>Record updated at</source> + <target>Bejegyzés frissítve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Record updated at: %@" xml:space="preserve"> + <source>Record updated at: %@</source> + <target>Bejegyzés frissítve: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Reduced battery usage" xml:space="preserve"> + <source>Reduced battery usage</source> + <target>Csökkentett akkumulátor-használat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Register" xml:space="preserve"> + <source>Register</source> + <target>Regisztrálás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Register notification token?" xml:space="preserve"> + <source>Register notification token?</source> + <target>Regisztrálja az értesítési tokent?</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Registered" xml:space="preserve"> + <source>Registered</source> + <target>Regisztrálva</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Reject" xml:space="preserve"> + <source>Reject</source> + <target>Elutasítás</target> + <note>reject incoming call via notification +swipe action</note> + </trans-unit> + <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> + <source>Reject (sender NOT notified)</source> + <target>Elutasítás (a feladó NEM kap értesítést)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reject contact request" xml:space="preserve"> + <source>Reject contact request</source> + <target>Meghívási kérés elutasítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Relay server is only used if necessary. Another party can observe your IP address." xml:space="preserve"> + <source>Relay server is only used if necessary. Another party can observe your IP address.</source> + <target>A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címet.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Relay server protects your IP address, but it can observe the duration of the call." xml:space="preserve"> + <source>Relay server protects your IP address, but it can observe the duration of the call.</source> + <target>A továbbítókiszolgáló megvédi az Ön IP-címét, de megfigyelheti a hívás időtartamát.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove" xml:space="preserve"> + <source>Remove</source> + <target>Eltávolítás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove archive?" xml:space="preserve"> + <source>Remove archive?</source> + <target>Eltávolítja az archívumot?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove image" xml:space="preserve"> + <source>Remove image</source> + <target>Kép eltávolítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove member" xml:space="preserve"> + <source>Remove member</source> + <target>Eltávolítás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove member?" xml:space="preserve"> + <source>Remove member?</source> + <target>Eltávolítja a tagot?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove passphrase from keychain?" xml:space="preserve"> + <source>Remove passphrase from keychain?</source> + <target>Eltávolítja a jelmondatot a kulcstartóból?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Renegotiate" xml:space="preserve"> + <source>Renegotiate</source> + <target>Újraegyeztetés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Renegotiate encryption" xml:space="preserve"> + <source>Renegotiate encryption</source> + <target>Titkosítás újraegyeztetése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Renegotiate encryption?" xml:space="preserve"> + <source>Renegotiate encryption?</source> + <target>Újraegyezteti a titkosítást?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat connection request?" xml:space="preserve"> + <source>Repeat connection request?</source> + <target>Megismétli a meghívási kérést?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat download" xml:space="preserve"> + <source>Repeat download</source> + <target>Letöltés ismét</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat import" xml:space="preserve"> + <source>Repeat import</source> + <target>Importálás ismét</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat join request?" xml:space="preserve"> + <source>Repeat join request?</source> + <target>Megismétli a meghívási kérést?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat upload" xml:space="preserve"> + <source>Repeat upload</source> + <target>Feltöltés ismét</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reply" xml:space="preserve"> + <source>Reply</source> + <target>Válasz</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Report" xml:space="preserve"> + <source>Report</source> + <target>Jelentés</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Report content: only group moderators will see it." xml:space="preserve"> + <source>Report content: only group moderators will see it.</source> + <target>Tartalom jelentése: csak a csoport moderátorai látják.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report member profile: only group moderators will see it." xml:space="preserve"> + <source>Report member profile: only group moderators will see it.</source> + <target>Tag profiljának jelentése: csak a csoport moderátorai látják.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report other: only group moderators will see it." xml:space="preserve"> + <source>Report other: only group moderators will see it.</source> + <target>Egyéb jelentés: csak a csoport moderátorai látják.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report reason?" xml:space="preserve"> + <source>Report reason?</source> + <target>Jelentés indoklása?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Report spam: only group moderators will see it." xml:space="preserve"> + <source>Report spam: only group moderators will see it.</source> + <target>Kéretlen tartalom jelentése: csak a csoport moderátorai látják.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report violation: only group moderators will see it." xml:space="preserve"> + <source>Report violation: only group moderators will see it.</source> + <target>Szabálysértés jelentése: csak a csoport moderátorai látják.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report: %@" xml:space="preserve"> + <source>Report: %@</source> + <target>Jelentés: %@</target> + <note>report in notification</note> + </trans-unit> + <trans-unit id="Reporting messages to moderators is prohibited." xml:space="preserve"> + <source>Reporting messages to moderators is prohibited.</source> + <target>Az üzenetek jelentése a moderátorok felé le van tiltva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reports" xml:space="preserve"> + <source>Reports</source> + <target>Jelentések</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Required" xml:space="preserve"> + <source>Required</source> + <target>Szükséges</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset" xml:space="preserve"> + <source>Reset</source> + <target>Visszaállítás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all hints" xml:space="preserve"> + <source>Reset all hints</source> + <target>Tippek visszaállítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics" xml:space="preserve"> + <source>Reset all statistics</source> + <target>Az összes statisztika visszaállítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics?" xml:space="preserve"> + <source>Reset all statistics?</source> + <target>Visszaállítja az összes statisztikát?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset colors" xml:space="preserve"> + <source>Reset colors</source> + <target>Színek visszaállítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset to app theme" xml:space="preserve"> + <source>Reset to app theme</source> + <target>Alkalmazás témájának visszaállítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset to defaults" xml:space="preserve"> + <source>Reset to defaults</source> + <target>Visszaállítás alapértelmezettre</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset to user theme" xml:space="preserve"> + <source>Reset to user theme</source> + <target>Felhasználó által létrehozott téma visszaállítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> + <source>Restart the app to create a new chat profile</source> + <target>Új csevegési profil létrehozásához indítsa újra az alkalmazást</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Restart the app to use imported chat database" xml:space="preserve"> + <source>Restart the app to use imported chat database</source> + <target>Az importált csevegési adatbázis használatához indítsa újra az alkalmazást</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Restore" xml:space="preserve"> + <source>Restore</source> + <target>Visszaállítás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Restore database backup" xml:space="preserve"> + <source>Restore database backup</source> + <target>Adatbázismentés visszaállítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Restore database backup?" xml:space="preserve"> + <source>Restore database backup?</source> + <target>Visszaállítja az adatbázismentést?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Restore database error" xml:space="preserve"> + <source>Restore database error</source> + <target>Hiba történt az adatbázis visszaállításakor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Retry" xml:space="preserve"> + <source>Retry</source> + <target>Újrapróbálkozás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reveal" xml:space="preserve"> + <source>Reveal</source> + <target>Felfedés</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Review conditions" xml:space="preserve"> + <source>Review conditions</source> + <target>Feltételek felülvizsgálata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Revoke" xml:space="preserve"> + <source>Revoke</source> + <target>Visszavonás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Revoke file" xml:space="preserve"> + <source>Revoke file</source> + <target>Fájl visszavonása</target> + <note>cancel file action</note> + </trans-unit> + <trans-unit id="Revoke file?" xml:space="preserve"> + <source>Revoke file?</source> + <target>Visszavonja a fájlt?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Role" xml:space="preserve"> + <source>Role</source> + <target>Szerepkör</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Run chat" xml:space="preserve"> + <source>Run chat</source> + <target>Csevegési szolgáltatás indítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SMP server" xml:space="preserve"> + <source>SMP server</source> + <target>SMP-kiszolgáló</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SOCKS proxy" xml:space="preserve"> + <source>SOCKS proxy</source> + <target>SOCKS-proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safely receive files" xml:space="preserve"> + <source>Safely receive files</source> + <target>Fájlok biztonságos fogadása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safer groups" xml:space="preserve"> + <source>Safer groups</source> + <target>Biztonságosabb csoportok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save" xml:space="preserve"> + <source>Save</source> + <target>Mentés</target> + <note>alert button +chat item action</note> + </trans-unit> + <trans-unit id="Save (and notify contacts)" xml:space="preserve"> + <source>Save (and notify contacts)</source> + <target>Mentés (és a partnerek értesítése)</target> + <note>alert button</note> + </trans-unit> + <trans-unit id="Save and notify contact" xml:space="preserve"> + <source>Save and notify contact</source> + <target>Mentés és a partner értesítése</target> + <note>alert button</note> + </trans-unit> + <trans-unit id="Save and notify group members" xml:space="preserve"> + <source>Save and notify group members</source> + <target>Mentés és a csoporttagok értesítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save and reconnect" xml:space="preserve"> + <source>Save and reconnect</source> + <target>Mentés és újrakapcsolódás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save and update group profile" xml:space="preserve"> + <source>Save and update group profile</source> + <target>Mentés és a csoportprofil frissítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save group profile" xml:space="preserve"> + <source>Save group profile</source> + <target>Csoportprofil mentése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save list" xml:space="preserve"> + <source>Save list</source> + <target>Lista mentése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save passphrase and open chat" xml:space="preserve"> + <source>Save passphrase and open chat</source> + <target>Jelmondat mentése és a csevegés megnyitása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save passphrase in Keychain" xml:space="preserve"> + <source>Save passphrase in Keychain</source> + <target>Jelmondat mentése a kulcstartóba</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save preferences?" xml:space="preserve"> + <source>Save preferences?</source> + <target>Menti a beállításokat?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Save profile password" xml:space="preserve"> + <source>Save profile password</source> + <target>Profiljelszó mentése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save servers" xml:space="preserve"> + <source>Save servers</source> + <target>Kiszolgálók mentése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save servers?" xml:space="preserve"> + <source>Save servers?</source> + <target>Menti a kiszolgálókat?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Save welcome message?" xml:space="preserve"> + <source>Save welcome message?</source> + <target>Menti az üdvözlőüzenetet?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save your profile?" xml:space="preserve"> + <source>Save your profile?</source> + <target>Menti a profilt?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Saved" xml:space="preserve"> + <source>Saved</source> + <target>Mentett</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> + <source>Saved WebRTC ICE servers will be removed</source> + <target>A mentett WebRTC ICE-kiszolgálók el lesznek távolítva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved from" xml:space="preserve"> + <source>Saved from</source> + <target>Elmentve innen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved message" xml:space="preserve"> + <source>Saved message</source> + <target>Mentett üzenet</target> + <note>message info title</note> + </trans-unit> + <trans-unit id="Saving %lld messages" xml:space="preserve"> + <source>Saving %lld messages</source> + <target>%lld üzenet mentése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scale" xml:space="preserve"> + <source>Scale</source> + <target>Méretezés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan / Paste link" xml:space="preserve"> + <source>Scan / Paste link</source> + <target>Hivatkozás beolvasása / beillesztése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan QR code" xml:space="preserve"> + <source>Scan QR code</source> + <target>QR-kód beolvasása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan QR code from desktop" xml:space="preserve"> + <source>Scan QR code from desktop</source> + <target>QR-kód beolvasása a számítógépről</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan code" xml:space="preserve"> + <source>Scan code</source> + <target>Beolvasás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan security code from your contact's app." xml:space="preserve"> + <source>Scan security code from your contact's app.</source> + <target>Biztonsági kód beolvasása a partnere alkalmazásából.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan server QR code" xml:space="preserve"> + <source>Scan server QR code</source> + <target>A kiszolgáló QR-kódjának beolvasása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search" xml:space="preserve"> + <source>Search</source> + <target>Keresés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search bar accepts invitation links." xml:space="preserve"> + <source>Search bar accepts invitation links.</source> + <target>A keresősáv elfogadja a meghívási hivatkozásokat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search or paste SimpleX link" xml:space="preserve"> + <source>Search or paste SimpleX link</source> + <target>Keresés vagy SimpleX-hivatkozás beillesztése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secondary" xml:space="preserve"> + <source>Secondary</source> + <target>Másodlagos szín</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secure queue" xml:space="preserve"> + <source>Secure queue</source> + <target>Biztonságos sorba állítás</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Secured" xml:space="preserve"> + <source>Secured</source> + <target>Biztosítva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Security assessment" xml:space="preserve"> + <source>Security assessment</source> + <target>Biztonsági kiértékelés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Security code" xml:space="preserve"> + <source>Security code</source> + <target>Biztonsági kód</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Select" xml:space="preserve"> + <source>Select</source> + <target>Kijelölés</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Select chat profile" xml:space="preserve"> + <source>Select chat profile</source> + <target>Csevegési profil kijelölése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected %lld" xml:space="preserve"> + <source>Selected %lld</source> + <target>%lld kijelölve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>A kijelölt csevegési beállítások tiltják ezt az üzenetet.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Self-destruct" xml:space="preserve"> + <source>Self-destruct</source> + <target>Önmegsemmisítés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Self-destruct passcode" xml:space="preserve"> + <source>Self-destruct passcode</source> + <target>Önmegsemmisítő-jelkód</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Self-destruct passcode changed!" xml:space="preserve"> + <source>Self-destruct passcode changed!</source> + <target>Az önmegsemmisítő-jelkód módosult!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Self-destruct passcode enabled!" xml:space="preserve"> + <source>Self-destruct passcode enabled!</source> + <target>Az önmegsemmisítő-jelkód engedélyezve!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send" xml:space="preserve"> + <source>Send</source> + <target>Küldés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send a live message - it will update for the recipient(s) as you type it" xml:space="preserve"> + <source>Send a live message - it will update for the recipient(s) as you type it</source> + <target>Élő üzenet küldése – az üzenet a címzett(ek) számára valós időben frissül, ahogy Ön beírja az üzenetet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send delivery receipts to" xml:space="preserve"> + <source>Send delivery receipts to</source> + <target>A kézbesítési jelentéseket a következő címre kell küldeni</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send direct message to connect" xml:space="preserve"> + <source>Send direct message to connect</source> + <target>Közvetlen üzenet küldése a kapcsolódáshoz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send disappearing message" xml:space="preserve"> + <source>Send disappearing message</source> + <target>Eltűnő üzenet küldése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send errors" xml:space="preserve"> + <source>Send errors</source> + <target>Üzenetküldési hibák</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send link previews" xml:space="preserve"> + <source>Send link previews</source> + <target>Hivatkozás előnézete</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send live message" xml:space="preserve"> + <source>Send live message</source> + <target>Élő üzenet küldése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send message to enable calls." xml:space="preserve"> + <source>Send message to enable calls.</source> + <target>Üzenet küldése a hívások engedélyezéséhez.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source> + <target>Közvetlen üzenetküldés, ha az IP-cím védett és a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when your or destination server does not support private routing.</source> + <target>Közvetlen üzenetküldés, ha a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send notifications" xml:space="preserve"> + <source>Send notifications</source> + <target>Értesítések küldése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send private reports" xml:space="preserve"> + <source>Send private reports</source> + <target>Privát jelentések küldése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send questions and ideas" xml:space="preserve"> + <source>Send questions and ideas</source> + <target>Ötletek és javaslatok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send receipts" xml:space="preserve"> + <source>Send receipts</source> + <target>Kézbesítési jelentések küldése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve"> + <source>Send them from gallery or custom keyboards.</source> + <target>Küldje el őket a galériából vagy az egyéni billentyűzetekről.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve"> + <source>Send up to 100 last messages to new members.</source> + <target>Legfeljebb az utolsó 100 üzenet elküldése az új tagok számára.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> + <source>Sender cancelled file transfer.</source> + <target>A fájl küldője visszavonta az átvitelt.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> + <source>Sender may have deleted the connection request.</source> + <target>A küldője törölhette a meghívási kérést.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending delivery receipts will be enabled for all contacts in all visible chat profiles." xml:space="preserve"> + <source>Sending delivery receipts will be enabled for all contacts in all visible chat profiles.</source> + <target>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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending delivery receipts will be enabled for all contacts." xml:space="preserve"> + <source>Sending delivery receipts will be enabled for all contacts.</source> + <target>A kézbesítési jelentések küldése az összes partnere számára engedélyezve lesz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending file will be stopped." xml:space="preserve"> + <source>Sending file will be stopped.</source> + <target>A fájl küldése le fog állni.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld contacts" xml:space="preserve"> + <source>Sending receipts is disabled for %lld contacts</source> + <target>A kézbesítési jelentések le vannak tiltva %lld partnernél</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is disabled for %lld groups</source> + <target>A kézbesítési jelentések le vannak tiltva %lld csoportban</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve"> + <source>Sending receipts is enabled for %lld contacts</source> + <target>A kézbesítési jelentések engedélyezve vannak %lld partnernél</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is enabled for %lld groups</source> + <target>A kézbesítési jelentések engedélyezve vannak %lld csoportban</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending via" xml:space="preserve"> + <source>Sending via</source> + <target>Küldés a következőn keresztül:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent at" xml:space="preserve"> + <source>Sent at</source> + <target>Elküldve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent at: %@" xml:space="preserve"> + <source>Sent at: %@</source> + <target>Elküldve: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Sent directly" xml:space="preserve"> + <source>Sent directly</source> + <target>Közvetlenül küldött</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent file event" xml:space="preserve"> + <source>Sent file event</source> + <target>Elküldött fájlesemény</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Sent message" xml:space="preserve"> + <source>Sent message</source> + <target>Üzenetbuborék színe</target> + <note>message info title</note> + </trans-unit> + <trans-unit id="Sent messages" xml:space="preserve"> + <source>Sent messages</source> + <target>Elküldött üzenetek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> + <source>Sent messages will be deleted after set time.</source> + <target>Az elküldött üzenetek törölve lesznek a beállított idő után.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent reply" xml:space="preserve"> + <source>Sent reply</source> + <target>Válaszüzenet-buborék színe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent total" xml:space="preserve"> + <source>Sent total</source> + <target>Összes elküldött üzenet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent via proxy" xml:space="preserve"> + <source>Sent via proxy</source> + <target>Proxyn keresztül küldött</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server" xml:space="preserve"> + <source>Server</source> + <target>Kiszolgáló</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server added to operator %@." xml:space="preserve"> + <source>Server added to operator %@.</source> + <target>Kiszolgáló hozzáadva a következő üzemeltetőhöz: %@.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Server address" xml:space="preserve"> + <source>Server address</source> + <target>Kiszolgáló címe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings." xml:space="preserve"> + <source>Server address is incompatible with network settings.</source> + <target>A kiszolgáló címe nem kompatibilis a hálózati beállításokkal.</target> + <note>srv error text.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Server address is incompatible with network settings: %@.</source> + <target>A kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server operator changed." xml:space="preserve"> + <source>Server operator changed.</source> + <target>A kiszolgáló üzemeltetője módosult.</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Server operators" xml:space="preserve"> + <source>Server operators</source> + <target>Kiszolgálóüzemeltetők</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server protocol changed." xml:space="preserve"> + <source>Server protocol changed.</source> + <target>A kiszolgáló-protokoll módosult.</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> + <source>Server requires authorization to create queues, check password</source> + <target>A kiszolgálónak engedélyre van szüksége a sorba állítás létrehozásához, ellenőrizze a jelszavát</target> + <note>server test error</note> + </trans-unit> + <trans-unit id="Server requires authorization to upload, check password" xml:space="preserve"> + <source>Server requires authorization to upload, check password</source> + <target>A kiszolgálónak hitelesítésre van szüksége a feltöltéshez, ellenőrizze jelszavát</target> + <note>server test error</note> + </trans-unit> + <trans-unit id="Server test failed!" xml:space="preserve"> + <source>Server test failed!</source> + <target>Sikertelen kiszolgáló teszt!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server type" xml:space="preserve"> + <source>Server type</source> + <target>Kiszolgáló típusa</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server version is incompatible with network settings." xml:space="preserve"> + <source>Server version is incompatible with network settings.</source> + <target>A kiszolgáló verziója nem kompatibilis a hálózati beállításokkal.</target> + <note>srv error text</note> + </trans-unit> + <trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve"> + <source>Server version is incompatible with your app: %@.</source> + <target>A kiszolgáló verziója nem kompatibilis az alkalmazással: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers" xml:space="preserve"> + <source>Servers</source> + <target>Kiszolgálók</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers info" xml:space="preserve"> + <source>Servers info</source> + <target>Információk a kiszolgálókról</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <target>A kiszolgálók statisztikái visszaállnak – ez a művelet nem vonható vissza!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Session code" xml:space="preserve"> + <source>Session code</source> + <target>Munkamenet kód</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set 1 day" xml:space="preserve"> + <source>Set 1 day</source> + <target>Beállítva 1 nap</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set chat name…" xml:space="preserve"> + <source>Set chat name…</source> + <target>Csevegés nevének beállítása…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set contact name…" xml:space="preserve"> + <source>Set contact name…</source> + <target>Partner nevének beállítása…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve"> + <source>Set default theme</source> + <target>Alapértelmezett téma beállítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set group preferences" xml:space="preserve"> + <source>Set group preferences</source> + <target>Csoportbeállítások megadása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set it instead of system authentication." xml:space="preserve"> + <source>Set it instead of system authentication.</source> + <target>Beállítás a rendszer-hitelesítés helyett.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve"> + <source>Set message expiration in chats.</source> + <target>Üzenetek eltűnési idejének módosítása a csevegésekben.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set passcode" xml:space="preserve"> + <source>Set passcode</source> + <target>Jelkód beállítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set passphrase" xml:space="preserve"> + <source>Set passphrase</source> + <target>Jelmondat beállítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set passphrase to export" xml:space="preserve"> + <source>Set passphrase to export</source> + <target>Jelmondat beállítása az exportáláshoz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set the message shown to new members!" xml:space="preserve"> + <source>Set the message shown to new members!</source> + <target>Megjelenítendő üzenet beállítása az új tagok számára!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set timeouts for proxy/VPN" xml:space="preserve"> + <source>Set timeouts for proxy/VPN</source> + <target>Időtúllépések beállítása a proxy/VPN számára</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Settings" xml:space="preserve"> + <source>Settings</source> + <target>Beállítások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve"> + <source>Settings were changed.</source> + <target>A beállítások módosultak.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve"> + <source>Shape profile images</source> + <target>Profilkép alakzata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <target>Megosztás</target> + <note>alert action +chat item action</note> + </trans-unit> + <trans-unit id="Share 1-time link" xml:space="preserve"> + <source>Share 1-time link</source> + <target>Egyszer használható meghívó megosztása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share 1-time link with a friend" xml:space="preserve"> + <source>Share 1-time link with a friend</source> + <target>Egyszer használható meghívó megosztása egy baráttal</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve"> + <source>Share SimpleX address on social media.</source> + <target>SimpleX-cím megosztása a közösségi médiában.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share address" xml:space="preserve"> + <source>Share address</source> + <target>Cím megosztása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share address publicly" xml:space="preserve"> + <source>Share address publicly</source> + <target>Cím nyilvános megosztása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share address with contacts?" xml:space="preserve"> + <source>Share address with contacts?</source> + <target>Megosztja a címet a partnereivel?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Share from other apps." xml:space="preserve"> + <source>Share from other apps.</source> + <target>Megosztás más alkalmazásokból.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share link" xml:space="preserve"> + <source>Share link</source> + <target>Megosztás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share profile" xml:space="preserve"> + <source>Share profile</source> + <target>Profil megosztása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share this 1-time invite link" xml:space="preserve"> + <source>Share this 1-time invite link</source> + <target>Ennek az egyszer használható meghívónak a megosztása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share to SimpleX" xml:space="preserve"> + <source>Share to SimpleX</source> + <target>Megosztás a SimpleXben</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share with contacts" xml:space="preserve"> + <source>Share with contacts</source> + <target>Megosztás a partnerekkel</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Short link" xml:space="preserve"> + <source>Short link</source> + <target>Rövid hivatkozás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show QR code" xml:space="preserve"> + <source>Show QR code</source> + <target>QR-kód megjelenítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show calls in phone history" xml:space="preserve"> + <source>Show calls in phone history</source> + <target>Hívások megjelenítése a híváslistában</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show developer options" xml:space="preserve"> + <source>Show developer options</source> + <target>Fejlesztői beállítások megjelenítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show last messages" xml:space="preserve"> + <source>Show last messages</source> + <target>Legutóbbi üzenet előnézetének megjelenítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show message status" xml:space="preserve"> + <source>Show message status</source> + <target>Üzenet állapotának megjelenítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show percentage" xml:space="preserve"> + <source>Show percentage</source> + <target>Százalék megjelenítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show preview" xml:space="preserve"> + <source>Show preview</source> + <target>Értesítés előnézete</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show → on messages sent via private routing." xml:space="preserve"> + <source>Show → on messages sent via private routing.</source> + <target>Egy „→” jel megjelenítése a privát útválasztáson keresztül küldött üzeneteknél.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show:" xml:space="preserve"> + <source>Show:</source> + <target>Megjelenítve:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve"> + <source>SimpleX</source> + <target>SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX Address" xml:space="preserve"> + <source>SimpleX Address</source> + <target>SimpleX-cím</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." xml:space="preserve"> + <source>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</source> + <target>A SimpleX Chat és a Flux megállapodást kötött arról, hogy a Flux által üzemeltetett kiszolgálókat beépítik az alkalmazásba.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> + <source>SimpleX Chat security was audited by Trail of Bits.</source> + <target>A SimpleX Chat biztonsága a Trail of Bits által lett auditálva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX Lock" xml:space="preserve"> + <source>SimpleX Lock</source> + <target>SimpleX-zár</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX Lock mode" xml:space="preserve"> + <source>SimpleX Lock mode</source> + <target>Zárolási mód</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX Lock not enabled!" xml:space="preserve"> + <source>SimpleX Lock not enabled!</source> + <target>A SimpleX-zár nincs bekapcsolva!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX Lock turned on" xml:space="preserve"> + <source>SimpleX Lock turned on</source> + <target>SimpleX-zár bekapcsolva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address" xml:space="preserve"> + <source>SimpleX address</source> + <target>SimpleX-cím</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address and 1-time links are safe to share via any messenger." xml:space="preserve"> + <source>SimpleX address and 1-time links are safe to share via any messenger.</source> + <target>A SimpleX-cím és az egyszer használható meghívó biztonságosan megosztható bármilyen üzenetváltó-alkalmazáson keresztül.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address or 1-time link?" xml:space="preserve"> + <source>SimpleX address or 1-time link?</source> + <target>SimpleX-cím vagy egyszer használható meghívó?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX channel link" xml:space="preserve"> + <source>SimpleX channel link</source> + <target>SimpleX-csatornahivatkozás</target> + <note>simplex link type</note> + </trans-unit> + <trans-unit id="SimpleX contact address" xml:space="preserve"> + <source>SimpleX contact address</source> + <target>SimpleX kapcsolattartási cím</target> + <note>simplex link type</note> + </trans-unit> + <trans-unit id="SimpleX encrypted message or connection event" xml:space="preserve"> + <source>SimpleX encrypted message or connection event</source> + <target>SimpleX titkosított üzenet vagy kapcsolati esemény</target> + <note>notification</note> + </trans-unit> + <trans-unit id="SimpleX group link" xml:space="preserve"> + <source>SimpleX group link</source> + <target>SimpleX-csoporthivatkozás</target> + <note>simplex link type</note> + </trans-unit> + <trans-unit id="SimpleX links" xml:space="preserve"> + <source>SimpleX links</source> + <target>SimpleX-hivatkozások</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve"> + <source>SimpleX links are prohibited.</source> + <target>A SimpleX-hivatkozások küldése le van tiltva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve"> + <source>SimpleX links not allowed</source> + <target>A SimpleX-hivatkozások küldése le van tiltva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> + <source>SimpleX one-time invitation</source> + <target>Egyszer használható SimpleX-meghívó</target> + <note>simplex link type</note> + </trans-unit> + <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> + <source>SimpleX protocols reviewed by Trail of Bits.</source> + <target>A SimpleX Chat biztonsága a Trail of Bits által lett felülvizsgálva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Simplified incognito mode" xml:space="preserve"> + <source>Simplified incognito mode</source> + <target>Egyszerűsített inkognitómód</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Size" xml:space="preserve"> + <source>Size</source> + <target>Méret</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Skip" xml:space="preserve"> + <source>Skip</source> + <target>Kihagyás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Skipped messages" xml:space="preserve"> + <source>Skipped messages</source> + <target>Kihagyott üzenetek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Small groups (max 20)" xml:space="preserve"> + <source>Small groups (max 20)</source> + <target>Kis csoportok (max. 20 tag)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Soft" xml:space="preserve"> + <source>Soft</source> + <target>Enyhe</target> + <note>blur media</note> + </trans-unit> + <trans-unit id="Some app settings were not migrated." xml:space="preserve"> + <source>Some app settings were not migrated.</source> + <target>Egyes alkalmazásbeállítások nem lettek átköltöztetve.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some file(s) were not exported:" xml:space="preserve"> + <source>Some file(s) were not exported:</source> + <target>Néhány fájl nem lett exportálva:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> + <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> + <target>Néhány nem végzetes hiba történt az importáláskor – további részleteket a csevegési konzolban olvashat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import:" xml:space="preserve"> + <source>Some non-fatal errors occurred during import:</source> + <target>Néhány nem végzetes hiba történt az importáláskor:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some servers failed the test: %@" xml:space="preserve"> + <source>Some servers failed the test: +%@</source> + <target>Néhány kiszolgáló megbukott a teszten: +%@</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Somebody" xml:space="preserve"> + <source>Somebody</source> + <target>Valaki</target> + <note>notification title</note> + </trans-unit> + <trans-unit id="Spam" xml:space="preserve"> + <source>Spam</source> + <target>Kéretlen tartalom</target> + <note>blocking reason +report reason</note> + </trans-unit> + <trans-unit id="Square, circle, or anything in between." xml:space="preserve"> + <source>Square, circle, or anything in between.</source> + <target>Négyzet, kör vagy bármi a kettő között.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Start chat" xml:space="preserve"> + <source>Start chat</source> + <target>Csevegés indítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Start chat?" xml:space="preserve"> + <source>Start chat?</source> + <target>Elindítja a csevegést?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Start migration" xml:space="preserve"> + <source>Start migration</source> + <target>Átköltöztetés indítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Starting from %@." xml:space="preserve"> + <source>Starting from %@.</source> + <target>Statisztikagyűjtés kezdete: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Statistics" xml:space="preserve"> + <source>Statistics</source> + <target>Statisztikák</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop" xml:space="preserve"> + <source>Stop</source> + <target>Megállítás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop SimpleX" xml:space="preserve"> + <source>Stop SimpleX</source> + <target>SimpleX megállítása</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Stop chat" xml:space="preserve"> + <source>Stop chat</source> + <target>Csevegési szolgáltatás megállítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> + <source>Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped.</source> + <target>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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop chat?" xml:space="preserve"> + <source>Stop chat?</source> + <target>Megállítja a csevegést?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop file" xml:space="preserve"> + <source>Stop file</source> + <target>Fájl megállítása</target> + <note>cancel file action</note> + </trans-unit> + <trans-unit id="Stop receiving file?" xml:space="preserve"> + <source>Stop receiving file?</source> + <target>Megállítja a fájlfogadást?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop sending file?" xml:space="preserve"> + <source>Stop sending file?</source> + <target>Megállítja a fájlküldést?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop sharing" xml:space="preserve"> + <source>Stop sharing</source> + <target>Megosztás megállítása</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Stop sharing address?" xml:space="preserve"> + <source>Stop sharing address?</source> + <target>Megállítja a címmegosztást?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Stopping chat" xml:space="preserve"> + <source>Stopping chat</source> + <target>Csevegés megállítása folyamatban</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Storage" xml:space="preserve"> + <source>Storage</source> + <target>Tárhely</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Strong" xml:space="preserve"> + <source>Strong</source> + <target>Erős</target> + <note>blur media</note> + </trans-unit> + <trans-unit id="Submit" xml:space="preserve"> + <source>Submit</source> + <target>Elküldés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscribed" xml:space="preserve"> + <source>Subscribed</source> + <target>Feliratkozva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscription errors" xml:space="preserve"> + <source>Subscription errors</source> + <target>Feliratkozási hibák</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscriptions ignored" xml:space="preserve"> + <source>Subscriptions ignored</source> + <target>Mellőzött feliratkozások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Support SimpleX Chat" xml:space="preserve"> + <source>Support SimpleX Chat</source> + <target>SimpleX Chat támogatása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch audio and video during the call." xml:space="preserve"> + <source>Switch audio and video during the call.</source> + <target>Hang/Videó váltása hívás közben.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> + <source>Switch chat profile for 1-time invitations.</source> + <target>Csevegési profilváltás az egyszer használható meghívókhoz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="System" xml:space="preserve"> + <source>System</source> + <target>Rendszer</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="System authentication" xml:space="preserve"> + <source>System authentication</source> + <target>Rendszer-hitelesítés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="TCP connection" xml:space="preserve"> + <source>TCP connection</source> + <target>TCP-kapcsolat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="TCP connection timeout" xml:space="preserve"> + <source>TCP connection timeout</source> + <target>TCP-kapcsolat időtúllépése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="TCP port for messaging" xml:space="preserve"> + <source>TCP port for messaging</source> + <target>TCP-port az üzenetváltáshoz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> + <source>TCP_KEEPCNT</source> + <target>TCP_KEEPCNT</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="TCP_KEEPIDLE" xml:space="preserve"> + <source>TCP_KEEPIDLE</source> + <target>TCP_KEEPIDLE</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="TCP_KEEPINTVL" xml:space="preserve"> + <source>TCP_KEEPINTVL</source> + <target>TCP_KEEPINTVL</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tail" xml:space="preserve"> + <source>Tail</source> + <target>Farok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Take picture" xml:space="preserve"> + <source>Take picture</source> + <target>Kép készítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tap Create SimpleX address in the menu to create it later." xml:space="preserve"> + <source>Tap Create SimpleX address in the menu to create it later.</source> + <target>Koppintson a SimpleX-cím létrehozása menüpontra a későbbi létrehozáshoz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tap button " xml:space="preserve"> + <source>Tap button </source> + <target>Koppintson a </target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tap to Connect" xml:space="preserve"> + <source>Tap to Connect</source> + <target>Koppintson ide a kapcsolódáshoz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tap to activate profile." xml:space="preserve"> + <source>Tap to activate profile.</source> + <target>A profil aktiválásához koppintson az ikonra.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tap to join" xml:space="preserve"> + <source>Tap to join</source> + <target>Koppintson ide a csatlakozáshoz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tap to join incognito" xml:space="preserve"> + <source>Tap to join incognito</source> + <target>Koppintson ide az inkognitóban való kapcsolódáshoz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tap to paste link" xml:space="preserve"> + <source>Tap to paste link</source> + <target>Koppintson ide a hivatkozás beillesztéséhez</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tap to scan" xml:space="preserve"> + <source>Tap to scan</source> + <target>Koppintson ide a QR-kód beolvasásához</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Temporary file error" xml:space="preserve"> + <source>Temporary file error</source> + <target>Ideiglenes fájlhiba</target> + <note>file error alert title</note> + </trans-unit> + <trans-unit id="Test failed at step %@." xml:space="preserve"> + <source>Test failed at step %@.</source> + <target>A teszt a(z) %@ lépésnél sikertelen volt.</target> + <note>server test failure</note> + </trans-unit> + <trans-unit id="Test notifications" xml:space="preserve"> + <source>Test notifications</source> + <target>Értesítések tesztelése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Test server" xml:space="preserve"> + <source>Test server</source> + <target>Kiszolgáló tesztelése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Test servers" xml:space="preserve"> + <source>Test servers</source> + <target>Kiszolgálók tesztelése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tests failed!" xml:space="preserve"> + <source>Tests failed!</source> + <target>Sikertelen tesztek!</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> + <source>Thank you for installing SimpleX Chat!</source> + <target>Köszönjük, hogy telepítette a SimpleX Chatet!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target>Köszönet a felhasználóknak [a Weblate-en való közreműködésért](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Thanks to the users – contribute via Weblate!" xml:space="preserve"> + <source>Thanks to the users – contribute via Weblate!</source> + <target>Köszönet a felhasználóknak a Weblate-en való közreműködésért!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> + <source>The ID of the next message is incorrect (less or equal to the previous). +It can happen because of some bug or when the connection is compromised.</source> + <target>A következő üzenet azonosítója érvénytelen (kisebb vagy egyenlő az előzővel). +Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve"> + <source>The app can notify you when you receive messages or contact requests - please open settings to enable.</source> + <target>Az alkalmazás értesíteni fogja, amikor üzeneteket vagy meghívási kéréseket kap – ezt a beállítások menüben engedélyezheti.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app protects your privacy by using different operators in each conversation." xml:space="preserve"> + <source>The app protects your privacy by using different operators in each conversation.</source> + <target>Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetéshez más-más üzemeltetőt használ.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve"> + <source>The app will ask to confirm downloads from unknown file servers (except .onion).</source> + <target>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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> + <source>The attempt to change database passphrase was not completed.</source> + <target>Az adatbázis jelmondatának módosítására tett kísérlet nem fejeződött be.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve"> + <source>The code you scanned is not a SimpleX link QR code.</source> + <target>A beolvasott QR-kód nem egy SimpleX-QR-kód-hivatkozás.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The connection reached the limit of undelivered messages, your contact may be offline." xml:space="preserve"> + <source>The connection reached the limit of undelivered messages, your contact may be offline.</source> + <target>A kapcsolat elérte a kézbesítetlen üzenetek számának határát, a partnere lehet, hogy offline állapotban van.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> + <source>The connection you accepted will be cancelled!</source> + <target>Az Ön által elfogadott kérelem vissza lesz vonva!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The contact you shared this link with will NOT be able to connect!" xml:space="preserve"> + <source>The contact you shared this link with will NOT be able to connect!</source> + <target>A partnere, akivel megosztotta ezt a hivatkozást, NEM fog tudni kapcsolódni!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The created archive is available via app Settings / Database / Old database archive." xml:space="preserve"> + <source>The created archive is available via app Settings / Database / Old database archive.</source> + <target>A létrehozott archívum a „Beállítások / Adatbázis / Régi adatbázis-archívum” menüben érhető el.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The encryption is working and the new encryption agreement is not required. It may result in connection errors!" xml:space="preserve"> + <source>The encryption is working and the new encryption agreement is not required. It may result in connection errors!</source> + <target>A titkosítás működik, és új titkosítási egyezményre nincs szükség. Ez kapcsolati hibákat eredményezhet!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> + <target>Az üzenetváltás jövője</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The hash of the previous message is different." xml:space="preserve"> + <source>The hash of the previous message is different.</source> + <target>Az előző üzenet hasítóértéke különbözik.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The message will be deleted for all members." xml:space="preserve"> + <source>The message will be deleted for all members.</source> + <target>Az üzenet az összes tag számára törölve lesz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The message will be marked as moderated for all members." xml:space="preserve"> + <source>The message will be marked as moderated for all members.</source> + <target>Az üzenet az összes tag számára moderáltként lesz megjelölve.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The messages will be deleted for all members." xml:space="preserve"> + <source>The messages will be deleted for all members.</source> + <target>Az üzenetek az összes tag számára törölve lesznek.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve"> + <source>The messages will be marked as moderated for all members.</source> + <target>Az üzenetek az összes tag számára moderáltként lesznek megjelölve.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> + <source>The old database was not removed during the migration, it can be deleted.</source> + <target>A régi adatbázis nem lett eltávolítva az átköltöztetéskor, ezért törölhető.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve"> + <source>The same conditions will apply to operator **%@**.</source> + <target>Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető számára is: **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second preset operator in the app!" xml:space="preserve"> + <source>The second preset operator in the app!</source> + <target>A második előre beállított üzemeltető az alkalmazásban!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> + <source>The second tick we missed! ✅</source> + <target>A második jelölés, amit kihagytunk! ✅</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The sender will NOT be notified" xml:space="preserve"> + <source>The sender will NOT be notified</source> + <target>A feladó NEM fog értesítést kapni</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The servers for new connections of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new connections of your current chat profile **%@**.</source> + <target>A jelenlegi **%@** nevű csevegési profiljához tartozó új kapcsolatok kiszolgálói.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The servers for new files of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new files of your current chat profile **%@**.</source> + <target>A jelenlegi **%@** nevű csevegési profiljához tartozó új fájlok kiszolgálói.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve"> + <source>The text you pasted is not a SimpleX link.</source> + <target>A beillesztett szöveg nem egy SimpleX-hivatkozás.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve"> + <source>The uploaded database archive will be permanently removed from the servers.</source> + <target>A feltöltött adatbázis-archívum véglegesen el lesz távolítva a kiszolgálókról.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Themes" xml:space="preserve"> + <source>Themes</source> + <target>Témák</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="These conditions will also apply for: **%@**." xml:space="preserve"> + <source>These conditions will also apply for: **%@**.</source> + <target>Ezek a feltételek lesznek elfogadva a következő számára is: **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> + <source>These settings are for your current profile **%@**.</source> + <target>Ezek a beállítások csak a jelenlegi **%@** nevű csevegési profiljára vonatkoznak.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve"> + <source>They can be overridden in contact and group settings.</source> + <target>Ezek felülbírálhatók a partner- és csoportbeállításokban.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> + <source>This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</source> + <target>Ez a művelet nem vonható vissza – az összes fogadott és küldött fájl a médiatartalmakkal együtt törölve lesznek. Az alacsony felbontású képek viszont megmaradnak.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</source> + <target>Ez a művelet nem vonható vissza – a kijelöltnél korábban küldött és fogadott üzenetek törölve lesznek. Ez több percet is igénybe vehet.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</source> + <target>Ez a művelet nem vonható vissza – a kijelölt üzenettől korábban küldött és fogadott üzenetek törölve lesznek a csevegésből.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> + <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> + <target>Ez a művelet nem vonható vissza – profiljai, partnerei, üzenetei és fájljai véglegesen törölve lesznek.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by end-to-end encryption.</source> + <target>Ez a csevegés végpontok közötti titkosítással védett.</target> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by quantum resistant end-to-end encryption.</source> + <target>Ez a csevegés végpontok közötti kvantumbiztos titkosítással védett.</target> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This device name" xml:space="preserve"> + <source>This device name</source> + <target>Ennek az eszköznek a neve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve"> + <source>This display name is invalid. Please choose another name.</source> + <target>Ez a megjelenítendő név érvénytelen. Válasszon egy másik nevet.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> + <source>This group has over %lld members, delivery receipts are not sent.</source> + <target>Ennek a csoportnak több mint %lld tagja van, a kézbesítési jelentések nem lesznek elküldve.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This group no longer exists." xml:space="preserve"> + <source>This group no longer exists.</source> + <target>Ez a csoport már nem létezik.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This is your own SimpleX address!" xml:space="preserve"> + <source>This is your own SimpleX address!</source> + <target>Ez a saját SimpleX-címe!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This is your own one-time link!" xml:space="preserve"> + <source>This is your own one-time link!</source> + <target>Ez a saját egyszer használható meghívója!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve"> + <source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source> + <target>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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve"> + <source>This link was used with another mobile device, please create a new link on the desktop.</source> + <target>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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This message was deleted or not received yet." xml:space="preserve"> + <source>This message was deleted or not received yet.</source> + <target>Ez az üzenet törölve lett vagy még nem érkezett meg.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> + <source>This setting applies to messages in your current chat profile **%@**.</source> + <target>Ez a beállítás csak az Ön jelenlegi **%@** nevű csevegési profiljában lévő üzenetekre vonatkozik.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Title" xml:space="preserve"> + <source>Title</source> + <target>Cím</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> + <source>To ask any questions and to receive updates:</source> + <target>Bármilyen kérdés feltevéséhez és a frissítésekért:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To connect, your contact can scan QR code or use the link in the app." xml:space="preserve"> + <source>To connect, your contact can scan QR code or use the link in the app.</source> + <target>A kapcsolódáshoz a partnere beolvashatja a QR-kódot, vagy használhatja az alkalmazásban található hivatkozást.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To hide unwanted messages." xml:space="preserve"> + <source>To hide unwanted messages.</source> + <target>Kéretlen üzenetek elrejtése.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To make a new connection" xml:space="preserve"> + <source>To make a new connection</source> + <target>Új kapcsolat létrehozásához</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To protect against your link being replaced, you can compare contact security codes." xml:space="preserve"> + <source>To protect against your link being replaced, you can compare contact security codes.</source> + <target>A hivatkozás cseréje elleni védelem érdekében összehasonlíthatja a biztonsági kódokat a partnerével.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> + <source>To protect timezone, image/voice files use UTC.</source> + <target>Az időzóna védelmének érdekében a kép-/hangfájlok UTC-t használnak.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve"> + <source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source> + <target>Az IP-cím védelmének érdekében a privát útválasztás az SMP-kiszolgálókat használja az üzenetek kézbesítéséhez.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> + <source>To protect your information, turn on SimpleX Lock. +You will be prompted to complete authentication before this feature is enabled.</source> + <target>A biztonsága érdekében kapcsolja be a SimpleX-zár funkciót. +A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beállítására az eszközén.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> + <target>Adatainak védelme érdekében a SimpleX külön üzenet-azonosítókat használ minden egyes kapcsolatához.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To receive" xml:space="preserve"> + <source>To receive</source> + <target>A fogadáshoz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve"> + <source>To record speech please grant permission to use Microphone.</source> + <target>A beszéd rögzítéséhez adjon engedélyt a Mikrofon használatára.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record video please grant permission to use Camera." xml:space="preserve"> + <source>To record video please grant permission to use Camera.</source> + <target>A videó rögzítéséhez adjon engedélyt a Kamera használatára.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> + <source>To record voice message please grant permission to use Microphone.</source> + <target>Hangüzenet rögzítéséhez adjon engedélyt a mikrofon használathoz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." xml:space="preserve"> + <source>To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page.</source> + <target>Rejtett profilja felfedéséhez adja meg a teljes jelszót a keresőmezőben, a **Csevegési profilok** menüben.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To send" xml:space="preserve"> + <source>To send</source> + <target>A küldéshez</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> + <source>To support instant push notifications the chat database has to be migrated.</source> + <target>Az azonnali push-értesítések támogatásához a csevegési adatbázis átköltöztetése szükséges.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To use the servers of **%@**, accept conditions of use." xml:space="preserve"> + <source>To use the servers of **%@**, accept conditions of use.</source> + <target>A(z) **%@** kiszolgálóinak használatához fogadja el a használati feltételeket.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> + <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> + <target>A végpontok közötti titkosítás hitelesítéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) a partnere eszközén lévő kóddal.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Toggle chat list:" xml:space="preserve"> + <source>Toggle chat list:</source> + <target>Csevegési lista átváltása:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> + <source>Toggle incognito when connecting.</source> + <target>Inkognitóra váltás kapcsolódáskor.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Token status: %@." xml:space="preserve"> + <source>Token status: %@.</source> + <target>Token állapota: %@.</target> + <note>token status</note> + </trans-unit> + <trans-unit id="Toolbar opacity" xml:space="preserve"> + <source>Toolbar opacity</source> + <target>Eszköztár átlátszatlansága</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Total" xml:space="preserve"> + <source>Total</source> + <target>Összes kapcsolat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Transport isolation" xml:space="preserve"> + <source>Transport isolation</source> + <target>Átvitel-izoláció</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Transport sessions" xml:space="preserve"> + <source>Transport sessions</source> + <target>Munkamenetek átvitele</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> + <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> + <target>Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (hiba: %@).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Trying to connect to the server used to receive messages from this contact." xml:space="preserve"> + <source>Trying to connect to the server used to receive messages from this contact.</source> + <target>Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Turkish interface" xml:space="preserve"> + <source>Turkish interface</source> + <target>Török kezelőfelület</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Turn off" xml:space="preserve"> + <source>Turn off</source> + <target>Kikapcsolás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Turn on" xml:space="preserve"> + <source>Turn on</source> + <target>Bekapcsolás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unable to record voice message" xml:space="preserve"> + <source>Unable to record voice message</source> + <target>Hangüzenet rögzítése nem lehetséges</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unblock" xml:space="preserve"> + <source>Unblock</source> + <target>Feloldás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unblock for all" xml:space="preserve"> + <source>Unblock for all</source> + <target>Feloldás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unblock member" xml:space="preserve"> + <source>Unblock member</source> + <target>Tag feloldása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unblock member for all?" xml:space="preserve"> + <source>Unblock member for all?</source> + <target>Az összes tag számára feloldja a tag letiltását?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unblock member?" xml:space="preserve"> + <source>Unblock member?</source> + <target>Feloldja a tag letiltását?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Undelivered messages" xml:space="preserve"> + <source>Undelivered messages</source> + <target>Kézbesítetlen üzenetek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unexpected migration state" xml:space="preserve"> + <source>Unexpected migration state</source> + <target>Váratlan átköltöztetési állapot</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unfav." xml:space="preserve"> + <source>Unfav.</source> + <target>Kedvenc megszüntetése</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Unhide" xml:space="preserve"> + <source>Unhide</source> + <target>Felfedés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unhide chat profile" xml:space="preserve"> + <source>Unhide chat profile</source> + <target>Csevegési profil felfedése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unhide profile" xml:space="preserve"> + <source>Unhide profile</source> + <target>Profil felfedése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unit" xml:space="preserve"> + <source>Unit</source> + <target>Egység</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown caller" xml:space="preserve"> + <source>Unknown caller</source> + <target>Ismeretlen hívó</target> + <note>callkit banner</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <target>Ismeretlen adatbázishiba: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown error" xml:space="preserve"> + <source>Unknown error</source> + <target>Ismeretlen hiba</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown servers!" xml:space="preserve"> + <source>Unknown servers!</source> + <target>Ismeretlen kiszolgálók!</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> + <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> + <target>Hacsak nem az iOS hívási felületét használja, engedélyezze a Ne zavarjanak módot a megszakítások elkerülése érdekében.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection." xml:space="preserve"> + <source>Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. +To connect, please ask your contact to create another connection link and check that you have a stable network connection.</source> + <target>Hacsak a partnere nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt egyszer, lehet hogy ez egy hiba – jelentse a problémát. +A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcsolattartási hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unlink" xml:space="preserve"> + <source>Unlink</source> + <target>Szétkapcsolás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unlink desktop?" xml:space="preserve"> + <source>Unlink desktop?</source> + <target>Leválasztja a számítógépet?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unlock" xml:space="preserve"> + <source>Unlock</source> + <target>Feloldás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unlock app" xml:space="preserve"> + <source>Unlock app</source> + <target>Alkalmazás feloldása</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Unmute" xml:space="preserve"> + <source>Unmute</source> + <target>Némítás megszüntetése</target> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Unread" xml:space="preserve"> + <source>Unread</source> + <target>Olvasatlan</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Unsupported connection link" xml:space="preserve"> + <source>Unsupported connection link</source> + <target>Nem támogatott kapcsolattartási hivatkozás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve"> + <source>Up to 100 last messages are sent to new members.</source> + <target>Legfeljebb az utolsó 100 üzenet lesz elküldve az új tagok számára.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Update" xml:space="preserve"> + <source>Update</source> + <target>Frissítés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Update database passphrase" xml:space="preserve"> + <source>Update database passphrase</source> + <target>Az adatbázis jelmondatának módosítása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Update network settings?" xml:space="preserve"> + <source>Update network settings?</source> + <target>Módosítja a hálózati beállításokat?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Update settings?" xml:space="preserve"> + <source>Update settings?</source> + <target>Frissíti a beállításokat?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updated conditions" xml:space="preserve"> + <source>Updated conditions</source> + <target>Frissített feltételek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> + <source>Updating settings will re-connect the client to all servers.</source> + <target>A beállítások frissítése a kiszolgálókhoz való újra kapcsolódással jár.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upgrade and open chat" xml:space="preserve"> + <source>Upgrade and open chat</source> + <target>Fejlesztés és a csevegés megnyitása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload errors" xml:space="preserve"> + <source>Upload errors</source> + <target>Feltöltési hibák</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload failed" xml:space="preserve"> + <source>Upload failed</source> + <target>Sikertelen feltöltés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload file" xml:space="preserve"> + <source>Upload file</source> + <target>Fájl feltöltése</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Uploaded" xml:space="preserve"> + <source>Uploaded</source> + <target>Feltöltve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploaded files" xml:space="preserve"> + <source>Uploaded files</source> + <target>Feltöltött fájlok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploading archive" xml:space="preserve"> + <source>Uploading archive</source> + <target>Archívum feltöltése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use %@" xml:space="preserve"> + <source>Use %@</source> + <target>%@ használata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use .onion hosts" xml:space="preserve"> + <source>Use .onion hosts</source> + <target>Onion-kiszolgálók használata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use SOCKS proxy" xml:space="preserve"> + <source>Use SOCKS proxy</source> + <target>SOCKS-proxy használata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> + <source>Use SimpleX Chat servers?</source> + <target>SimpleX Chat-kiszolgálók használata?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port %@ when no port is specified." xml:space="preserve"> + <source>Use TCP port %@ when no port is specified.</source> + <target>A következő TCP-port használata, amikor nincs port megadva: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve"> + <source>Use TCP port 443 for preset servers only.</source> + <target>A 443-as TCP-port használata kizárólag az előre beállított kiszolgálokhoz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use chat" xml:space="preserve"> + <source>Use chat</source> + <target>SimpleX Chat használata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use current profile" xml:space="preserve"> + <source>Use current profile</source> + <target>Jelenlegi profil használata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for files" xml:space="preserve"> + <source>Use for files</source> + <target>Használat a fájlokhoz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for messages" xml:space="preserve"> + <source>Use for messages</source> + <target>Használat az üzenetekhez</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for new connections" xml:space="preserve"> + <source>Use for new connections</source> + <target>Alkalmazás új kapcsolatokhoz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use from desktop" xml:space="preserve"> + <source>Use from desktop</source> + <target>Társítás számítógéppel</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use iOS call interface" xml:space="preserve"> + <source>Use iOS call interface</source> + <target>Az iOS hívási felületét használata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use new incognito profile" xml:space="preserve"> + <source>Use new incognito profile</source> + <target>Új inkognitóprofil használata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use only local notifications?" xml:space="preserve"> + <source>Use only local notifications?</source> + <target>Csak helyi értesítések használata?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve"> + <source>Use private routing with unknown servers when IP address is not protected.</source> + <target>Használjon privát útválasztást ismeretlen kiszolgálókkal, ha az IP-cím nem védett.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers." xml:space="preserve"> + <source>Use private routing with unknown servers.</source> + <target>Használjon privát útválasztást ismeretlen kiszolgálókkal.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use server" xml:space="preserve"> + <source>Use server</source> + <target>Kiszolgáló használata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use servers" xml:space="preserve"> + <source>Use servers</source> + <target>Kiszolgálók használata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use short links (BETA)" xml:space="preserve"> + <source>Use short links (BETA)</source> + <target>Rövid hivatkozások használata (béta)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app while in the call." xml:space="preserve"> + <source>Use the app while in the call.</source> + <target>Használja az alkalmazást hívás közben.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app with one hand." xml:space="preserve"> + <source>Use the app with one hand.</source> + <target>Használja az alkalmazást egy kézzel.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use web port" xml:space="preserve"> + <source>Use web port</source> + <target>Webport használata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="User selection" xml:space="preserve"> + <source>User selection</source> + <target>Felhasználó kijelölése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Username" xml:space="preserve"> + <source>Username</source> + <target>Felhasználónév</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> + <source>Using SimpleX Chat servers.</source> + <target>SimpleX Chat-kiszolgálók használatban.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify code with desktop" xml:space="preserve"> + <source>Verify code with desktop</source> + <target>Kód hitelesítése a számítógépen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify connection" xml:space="preserve"> + <source>Verify connection</source> + <target>Kapcsolat hitelesítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify connection security" xml:space="preserve"> + <source>Verify connection security</source> + <target>Biztonságos kapcsolat hitelesítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify connections" xml:space="preserve"> + <source>Verify connections</source> + <target>Kapcsolatok hitelesítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify database passphrase" xml:space="preserve"> + <source>Verify database passphrase</source> + <target>Az adatbázis jelmondatának hitelesítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify passphrase" xml:space="preserve"> + <source>Verify passphrase</source> + <target>Jelmondat hitelesítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify security code" xml:space="preserve"> + <source>Verify security code</source> + <target>Biztonsági kód hitelesítése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Via browser" xml:space="preserve"> + <source>Via browser</source> + <target>Böngészőn keresztül</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Via secure quantum resistant protocol." xml:space="preserve"> + <source>Via secure quantum resistant protocol.</source> + <target>Biztonságos kvantumbiztos protokollon keresztül.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Video call" xml:space="preserve"> + <source>Video call</source> + <target>Videóhívás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Video will be received when your contact completes uploading it." xml:space="preserve"> + <source>Video will be received when your contact completes uploading it.</source> + <target>A videó akkor érkezik meg, amikor a küldője befejezte annak feltöltését.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Video will be received when your contact is online, please wait or check later!" xml:space="preserve"> + <source>Video will be received when your contact is online, please wait or check later!</source> + <target>A videó akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Videos and files up to 1gb" xml:space="preserve"> + <source>Videos and files up to 1gb</source> + <target>Videók és fájlok legfeljebb 1GB méretig</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="View conditions" xml:space="preserve"> + <source>View conditions</source> + <target>Feltételek megtekintése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="View security code" xml:space="preserve"> + <source>View security code</source> + <target>Biztonsági kód megtekintése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="View updated conditions" xml:space="preserve"> + <source>View updated conditions</source> + <target>Frissített feltételek megtekintése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Visible history" xml:space="preserve"> + <source>Visible history</source> + <target>Látható előzmények</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Voice messages" xml:space="preserve"> + <source>Voice messages</source> + <target>Hangüzenetek</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Voice messages are prohibited in this chat." xml:space="preserve"> + <source>Voice messages are prohibited in this chat.</source> + <target>A hangüzenetek küldése le van tiltva ebben a csevegésben.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> + <target>A hangüzenetek küldése le van tiltva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Voice messages not allowed" xml:space="preserve"> + <source>Voice messages not allowed</source> + <target>A hangüzenetek küldése le van tiltva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Voice messages prohibited!" xml:space="preserve"> + <source>Voice messages prohibited!</source> + <target>A hangüzenetek le vannak tiltva!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Voice message…" xml:space="preserve"> + <source>Voice message…</source> + <target>Hangüzenet…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Waiting for desktop..." xml:space="preserve"> + <source>Waiting for desktop...</source> + <target>Várakozás a számítógép-alkalmazásra…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Waiting for file" xml:space="preserve"> + <source>Waiting for file</source> + <target>Várakozás a fájlra</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Waiting for image" xml:space="preserve"> + <source>Waiting for image</source> + <target>Várakozás a képre</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Waiting for video" xml:space="preserve"> + <source>Waiting for video</source> + <target>Várakozás a videóra</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper accent" xml:space="preserve"> + <source>Wallpaper accent</source> + <target>Háttérkép kiemelőszíne</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper background" xml:space="preserve"> + <source>Wallpaper background</source> + <target>Háttérkép háttérszíne</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve"> + <source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source> + <target>Figyelmeztetés: a csevegés elindítása egyszerre több eszközön nem támogatott, mert üzenetkézbesítési hibákat okoz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> + <source>Warning: you may lose some data!</source> + <target>Figyelmeztetés: néhány adat elveszhet!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="WebRTC ICE servers" xml:space="preserve"> + <source>WebRTC ICE servers</source> + <target>WebRTC ICE-kiszolgálók</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Welcome %@!" xml:space="preserve"> + <source>Welcome %@!</source> + <target>Üdvözöljük %@!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Welcome message" xml:space="preserve"> + <source>Welcome message</source> + <target>Üdvözlőüzenet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Welcome message is too long" xml:space="preserve"> + <source>Welcome message is too long</source> + <target>Az üdvözlőüzenet túl hosszú</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="What's new" xml:space="preserve"> + <source>What's new</source> + <target>Újdonságok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When available" xml:space="preserve"> + <source>When available</source> + <target>Amikor elérhető</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When connecting audio and video calls." xml:space="preserve"> + <source>When connecting audio and video calls.</source> + <target>Amikor egy bejövő hang- vagy videóhívás érkezik.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When more than one operator is enabled, none of them has metadata to learn who communicates with whom." xml:space="preserve"> + <source>When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</source> + <target>Amikor egynél több üzemeltető van engedélyezve, akkor egyik sem rendelkezik olyan metaadatokkal, amelyekből megtudható, hogy ki kivel kommunikál.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> + <source>When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</source> + <target>Inkognitóprofil megosztása esetén a rendszer azt a profilt fogja használni azokhoz a csoportokhoz, amelyekbe meghívást kapott.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="WiFi" xml:space="preserve"> + <source>WiFi</source> + <target>Wi-Fi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Will be enabled in direct chats!" xml:space="preserve"> + <source>Will be enabled in direct chats!</source> + <target>A közvetlen csevegésekben engedélyezve lesz!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wired ethernet" xml:space="preserve"> + <source>Wired ethernet</source> + <target>Vezetékes Ethernet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With encrypted files and media." xml:space="preserve"> + <source>With encrypted files and media.</source> + <target>Titkosított fájlokkal és médiatartalmakkal.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With optional welcome message." xml:space="preserve"> + <source>With optional welcome message.</source> + <target>Nem kötelező üdvözlőüzenettel.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With reduced battery usage." xml:space="preserve"> + <source>With reduced battery usage.</source> + <target>Csökkentett akkumulátor-használattal.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to file servers.</source> + <target>Tor vagy VPN nélkül az Ön IP-címe látható lesz a fájlkiszolgálók számára.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source> + <target>Tor vagy VPN nélkül az Ön IP-címe látható lesz a következő XFTP-továbbítókiszolgálók számára: %@.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <target>Érvénytelen adatbázis-jelmondat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve"> + <source>Wrong key or unknown connection - most likely this connection is deleted.</source> + <target>Érvénytelen kulcs vagy ismeretlen kapcsolat – valószínűleg ez a kapcsolat törlődött.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Wrong key or unknown file chunk address - most likely file is deleted." xml:space="preserve"> + <source>Wrong key or unknown file chunk address - most likely file is deleted.</source> + <target>Érvénytelen kulcs vagy ismeretlen fájltöredékcím – valószínűleg a fájl törlődött.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="Wrong passphrase!" xml:space="preserve"> + <source>Wrong passphrase!</source> + <target>Érvénytelen jelmondat!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="XFTP server" xml:space="preserve"> + <source>XFTP server</source> + <target>XFTP-kiszolgáló</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You **must not** use the same database on two devices." xml:space="preserve"> + <source>You **must not** use the same database on two devices.</source> + <target>**Nem szabad** ugyanazt az adatbázist használni egyszerre két eszközön.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You accepted connection" xml:space="preserve"> + <source>You accepted connection</source> + <target>Kapcsolat létrehozása</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You allow" xml:space="preserve"> + <source>You allow</source> + <target>Ön engedélyezi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You already have a chat profile with the same display name. Please choose another name." xml:space="preserve"> + <source>You already have a chat profile with the same display name. Please choose another name.</source> + <target>Már van egy csevegési profil ugyanezzel a megjelenítendő névvel. Válasszon egy másik nevet.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already connected to %@." xml:space="preserve"> + <source>You are already connected to %@.</source> + <target>Ön már kapcsolódott a következőhöz: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already connected with %@." xml:space="preserve"> + <source>You are already connected with %@.</source> + <target>Ön már kapcsolódva van vele: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already connecting to %@." xml:space="preserve"> + <source>You are already connecting to %@.</source> + <target>A kapcsolódás már folyamatban van a következőhöz: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already connecting via this one-time link!" xml:space="preserve"> + <source>You are already connecting via this one-time link!</source> + <target>A kapcsolódás már folyamatban van ezen az egyszer használható meghívón keresztül!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already in group %@." xml:space="preserve"> + <source>You are already in group %@.</source> + <target>Ön már a(z) %@ nevű csoport tagja.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already joining the group %@." xml:space="preserve"> + <source>You are already joining the group %@.</source> + <target>A csatlakozás már folyamatban van a(z) %@ nevű csoporthoz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already joining the group via this link!" xml:space="preserve"> + <source>You are already joining the group via this link!</source> + <target>A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already joining the group via this link." xml:space="preserve"> + <source>You are already joining the group via this link.</source> + <target>A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already joining the group! Repeat join request?" xml:space="preserve"> + <source>You are already joining the group! +Repeat join request?</source> + <target>A csatlakozás már folyamatban van a csoporthoz! +Megismétli a meghívási kérést?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve"> + <source>You are connected to the server used to receive messages from this contact.</source> + <target>Ön már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are invited to group" xml:space="preserve"> + <source>You are invited to group</source> + <target>Ön meghívást kapott a csoportba</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are not connected to these servers. Private routing is used to deliver messages to them." xml:space="preserve"> + <source>You are not connected to these servers. Private routing is used to deliver messages to them.</source> + <target>Ön nem kapcsolódik ezekhez a kiszolgálókhoz. A privát útválasztás az üzenetek kézbesítésére szolgál.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> + <source>You can accept calls from lock screen, without device and app authentication.</source> + <target>Hívásokat fogadhat a lezárási képernyőről, eszköz- és alkalmazás-hitelesítés nélkül.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can change it in Appearance settings." xml:space="preserve"> + <source>You can change it in Appearance settings.</source> + <target>Ezt a „Megjelenés” menüben módosíthatja.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can configure servers via settings." xml:space="preserve"> + <source>You can configure servers via settings.</source> + <target>A kiszolgálókat a „Hálózat és kiszolgálók” menüben konfigurálhatja.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can create it later" xml:space="preserve"> + <source>You can create it later</source> + <target>Létrehozás később</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can enable later via Settings" xml:space="preserve"> + <source>You can enable later via Settings</source> + <target>Később engedélyezheti a „Beállításokban”</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can enable them later via app Privacy & Security settings." xml:space="preserve"> + <source>You can enable them later via app Privacy & Security settings.</source> + <target>Később engedélyezheti őket az „Adatvédelem és biztonság” menüben.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can give another try." xml:space="preserve"> + <source>You can give another try.</source> + <target>Megpróbálhatja még egyszer.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> + <source>You can hide or mute a user profile - swipe it to the right.</source> + <target>Elrejtheti vagy lenémíthatja a felhasználó -profiljait – csúsztassa jobbra a profilt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve"> + <source>You can make it visible to your SimpleX contacts via Settings.</source> + <target>Láthatóvá teheti a SimpleXbeli partnerei számára a „Beállításokban”.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now chat with %@" xml:space="preserve"> + <source>You can now chat with %@</source> + <target>Mostantól küldhet üzeneteket %@ számára</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="You can send messages to %@ from Archived contacts." xml:space="preserve"> + <source>You can send messages to %@ from Archived contacts.</source> + <target>Az „Archivált partnerekből” továbbra is küldhet üzeneteket neki: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can set connection name, to remember who the link was shared with." xml:space="preserve"> + <source>You can set connection name, to remember who the link was shared with.</source> + <target>Beállíthatja a partner nevét, hogy emlékezzen arra, hogy kivel osztotta meg a hivatkozást.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> + <source>You can set lock screen notification preview via settings.</source> + <target>A lezárási képernyő értesítési előnézetét az „Értesítések” menüben állíthatja be.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." xml:space="preserve"> + <source>You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.</source> + <target>Megoszthat egy hivatkozást vagy QR-kódot – így bárki csatlakozhat a csoporthoz. Ha a csoportot Ön később törli, akkor nem fogja elveszíteni annak tagjait.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can share this address with your contacts to let them connect with **%@**." xml:space="preserve"> + <source>You can share this address with your contacts to let them connect with **%@**.</source> + <target>Megoszthatja ezt a SimpleX-címet a partnereivel, hogy kapcsolatba léphessenek vele: **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> + <source>You can start chat via app Settings / Database or by restarting the app</source> + <target>A csevegést az alkalmazás „Beállítások / Adatbázis” menüben vagy az alkalmazás újraindításával indíthatja el</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve"> + <source>You can still view conversation with %@ in the list of chats.</source> + <target>A(z) %@ nevű partnerével folytatott beszélgetéseit továbbra is megtekintheti a csevegések listájában.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> + <source>You can turn on SimpleX Lock via Settings.</source> + <target>A SimpleX-zár az „Adatvédelem és biztonság” menüben kapcsolható be.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can use markdown to format messages:" xml:space="preserve"> + <source>You can use markdown to format messages:</source> + <target>Üzenetek formázása a szövegbe szúrt speciális karakterekkel:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can view invitation link again in connection details." xml:space="preserve"> + <source>You can view invitation link again in connection details.</source> + <target>A meghívási hivatkozást újra megtekintheti a kapcsolat részleteinél.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="You can't send messages!" xml:space="preserve"> + <source>You can't send messages!</source> + <target>Nem lehet üzeneteket küldeni!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You could not be verified; please try again." xml:space="preserve"> + <source>You could not be verified; please try again.</source> + <target>Nem sikerült hitelesíteni; próbálja meg újra.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> + <target>Ön dönti el, hogy kivel beszélget.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You have already requested connection via this address!" xml:space="preserve"> + <source>You have already requested connection via this address!</source> + <target>Már küldött egy meghívási kérést ezen a címen keresztül!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You have already requested connection! Repeat connection request?" xml:space="preserve"> + <source>You have already requested connection! +Repeat connection request?</source> + <target>Ön már küldött egy meghívási kérést! +Megismétli a meghívási kérést?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> + <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> + <target>A jelmondatot minden alkalommal meg kell adnia, amikor az alkalmazás elindul – nem az eszközön van tárolva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You invited a contact" xml:space="preserve"> + <source>You invited a contact</source> + <target>Ön meghívta egy partnerét</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You joined this group" xml:space="preserve"> + <source>You joined this group</source> + <target>Ön csatlakozott ehhez a csoporthoz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You joined this group. Connecting to inviting group member." xml:space="preserve"> + <source>You joined this group. Connecting to inviting group member.</source> + <target>Ön csatlakozott ehhez a csoporthoz. Kapcsolódás a meghívó csoporttaghoz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may migrate the exported database." xml:space="preserve"> + <source>You may migrate the exported database.</source> + <target>Az exportált adatbázist átköltöztetheti.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may save the exported archive." xml:space="preserve"> + <source>You may save the exported archive.</source> + <target>Az exportált archívumot elmentheti.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> + <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> + <target>A csevegési adatbázis legfrissebb verzióját CSAK egy eszközön kell használnia, ellenkező esetben előfordulhat, hogy az üzeneteket nem fogja megkapni valamennyi partnerétől.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You need to allow your contact to call to be able to call them." xml:space="preserve"> + <source>You need to allow your contact to call to be able to call them.</source> + <target>Engedélyeznie kell a hívásokat a partnere számára, hogy fel tudják hívni egymást.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> + <source>You need to allow your contact to send voice messages to be able to send them.</source> + <target>Engedélyeznie kell a hangüzenetek küldését a partnere számára, hogy hangüzeneteket küldhessenek egymásnak.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You rejected group invitation" xml:space="preserve"> + <source>You rejected group invitation</source> + <target>Csoportmeghívó elutasítva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You sent group invitation" xml:space="preserve"> + <source>You sent group invitation</source> + <target>Csoportmeghívó elküldve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You should receive notifications." xml:space="preserve"> + <source>You should receive notifications.</source> + <target>Ön megkapja az értesítéseket.</target> + <note>token info</note> + </trans-unit> + <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> + <source>You will be connected to group when the group host's device is online, please wait or check later!</source> + <target>Akkor lesz kapcsolódva a csoporthoz, amikor a csoport tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You will be connected when group link host's device is online, please wait or check later!" xml:space="preserve"> + <source>You will be connected when group link host's device is online, please wait or check later!</source> + <target>Akkor lesz kapcsolódva, amikor a csoporthivatkozás tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve"> + <source>You will be connected when your connection request is accepted, please wait or check later!</source> + <target>Akkor lesz kapcsolódva, ha a meghívási kérése el lesz fogadva, várjon, vagy ellenőrizze később!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You will be connected when your contact's device is online, please wait or check later!" xml:space="preserve"> + <source>You will be connected when your contact's device is online, please wait or check later!</source> + <target>Akkor lesz kapcsolódva, amikor a partnerének az eszköze online lesz, várjon, vagy ellenőrizze később!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You will be required to authenticate when you start or resume the app after 30 seconds in background." xml:space="preserve"> + <source>You will be required to authenticate when you start or resume the app after 30 seconds in background.</source> + <target>Az alkalmazás elindításához vagy 30 másodpercnyi háttérben töltött idő után, az alkalmazáshoz való visszatéréshez hitelesítésre lesz szükség.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You will connect to all group members." xml:space="preserve"> + <source>You will connect to all group members.</source> + <target>Kapcsolódni fog a csoport összes tagjához.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve"> + <source>You will still receive calls and notifications from muted profiles when they are active.</source> + <target>Továbbra is kap hívásokat és értesítéseket a némított profiloktól, ha azok aktívak.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You will stop receiving messages from this chat. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this chat. Chat history will be preserved.</source> + <target>Ön nem fog több üzenetet kapni ebből a csevegésből, de a csevegés előzményei megmaradnak.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this group. Chat history will be preserved.</source> + <target>Ettől a csoporttól nem fog értesítéseket kapni. A csevegési előzmények megmaradnak.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You won't lose your contacts if you later delete your address." xml:space="preserve"> + <source>You won't lose your contacts if you later delete your address.</source> + <target>Nem veszíti el a partnereit, ha később törli a címét.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" xml:space="preserve"> + <source>You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile</source> + <target>Egy olyan partnerét próbálja meghívni, akivel inkognitóprofilt osztott meg abban a csoportban, amelyben a fő profilja van használatban</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" xml:space="preserve"> + <source>You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed</source> + <target>Inkognitóprofilt használ ehhez a csoporthoz – fő profilja megosztásának elkerülése érdekében a meghívók küldése le van tiltva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your ICE servers" xml:space="preserve"> + <source>Your ICE servers</source> + <target>Saját ICE-kiszolgálók</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your SimpleX address" xml:space="preserve"> + <source>Your SimpleX address</source> + <target>Profil SimpleX-címe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your calls" xml:space="preserve"> + <source>Your calls</source> + <target>Hívások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your chat database" xml:space="preserve"> + <source>Your chat database</source> + <target>Csevegési adatbázis</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your chat database is not encrypted - set passphrase to encrypt it." xml:space="preserve"> + <source>Your chat database is not encrypted - set passphrase to encrypt it.</source> + <target>A csevegési adatbázis nincs titkosítva – adjon meg egy jelmondatot a titkosításhoz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your chat preferences" xml:space="preserve"> + <source>Your chat preferences</source> + <target>Az Ön csevegési beállításai</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Your chat profiles" xml:space="preserve"> + <source>Your chat profiles</source> + <target>Csevegési profilok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve"> + <source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source> + <target>A kapcsolata át lett helyezve ide: %@, de egy váratlan hiba történt a profilra való átirányításkor.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> + <source>Your contact sent a file that is larger than currently supported maximum size (%@).</source> + <target>A partnere a jelenleg megengedett maximális méretű (%@) fájlnál nagyobbat küldött.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your contacts can allow full message deletion." xml:space="preserve"> + <source>Your contacts can allow full message deletion.</source> + <target>A partnerei engedélyezhetik a teljes üzenet törlését.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your contacts will remain connected." xml:space="preserve"> + <source>Your contacts will remain connected.</source> + <target>A partnerei továbbra is kapcsolódva maradnak.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve"> + <source>Your credentials may be sent unencrypted.</source> + <target>A hitelesítőadatai titkosítatlanul is elküldhetők.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> + <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> + <target>A jelenlegi csevegési adatbázis TÖRÖLVE és CSERÉLVE lesz az importáltra.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your current profile" xml:space="preserve"> + <source>Your current profile</source> + <target>Jelenlegi profil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your preferences" xml:space="preserve"> + <source>Your preferences</source> + <target>Beállítások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your privacy" xml:space="preserve"> + <source>Your privacy</source> + <target>Adatvédelem</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your profile" xml:space="preserve"> + <source>Your profile</source> + <target>Profil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> + <source>Your profile **%@** will be shared.</source> + <target>A(z) **%@** nevű profilja meg lesz osztva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> + <target>A profilja csak a partnereivel van megosztva.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> + <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> + <target>A profilja az eszközén van tárolva és csak a partnereivel van megosztva. A SimpleX-kiszolgálók nem láthatják a profilját.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve"> + <source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source> + <target>A profilja módosult. Ha elmenti, a profilfrissítés el lesz küldve a partnerei számára.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Your random profile" xml:space="preserve"> + <source>Your random profile</source> + <target>Véletlenszerű profil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your server address" xml:space="preserve"> + <source>Your server address</source> + <target>Saját SMP-kiszolgálójának címe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your servers" xml:space="preserve"> + <source>Your servers</source> + <target>Saját kiszolgálók</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your settings" xml:space="preserve"> + <source>Your settings</source> + <target>Beállítások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" xml:space="preserve"> + <source>[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)</source> + <target>[Közreműködés](https://github.com/simplex-chat/simplex-chat#contribute)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="[Send us email](mailto:chat@simplex.chat)" xml:space="preserve"> + <source>[Send us email](mailto:chat@simplex.chat)</source> + <target>[Küldjön nekünk e-mailt](mailto:chat@simplex.chat)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> + <source>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</source> + <target>[Csillagozás a GitHubon](https://github.com/simplex-chat/simplex-chat)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="_italic_" xml:space="preserve"> + <source>\_italic_</source> + <target>\_dőlt_</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="`a + b`" xml:space="preserve"> + <source>\`a + b`</source> + <target>a + b</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="above, then choose:" xml:space="preserve"> + <source>above, then choose:</source> + <target>gombra fent, majd válassza ki:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="accepted call" xml:space="preserve"> + <source>accepted call</source> + <target>fogadott hívás</target> + <note>call status</note> + </trans-unit> + <trans-unit id="accepted invitation" xml:space="preserve"> + <source>accepted invitation</source> + <target>elfogadott meghívó</target> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="admin" xml:space="preserve"> + <source>admin</source> + <target>adminisztrátor</target> + <note>member role</note> + </trans-unit> + <trans-unit id="admins" xml:space="preserve"> + <source>admins</source> + <target>adminisztrátorok</target> + <note>feature role</note> + </trans-unit> + <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> + <source>agreeing encryption for %@…</source> + <target>titkosítás elfogadása %@ számára…</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="agreeing encryption…" xml:space="preserve"> + <source>agreeing encryption…</source> + <target>titkosítás elfogadása…</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="all members" xml:space="preserve"> + <source>all members</source> + <target>összes tag</target> + <note>feature role</note> + </trans-unit> + <trans-unit id="always" xml:space="preserve"> + <source>always</source> + <target>mindig</target> + <note>pref value</note> + </trans-unit> + <trans-unit id="and %lld other events" xml:space="preserve"> + <source>and %lld other events</source> + <target>és további %lld esemény</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="archived report" xml:space="preserve"> + <source>archived report</source> + <target>archivált jelentés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="attempts" xml:space="preserve"> + <source>attempts</source> + <target>próbálkozások</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> + <source>audio call (not e2e encrypted)</source> + <target>hanghívás (nem e2e titkosított)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="author" xml:space="preserve"> + <source>author</source> + <target>szerző</target> + <note>member role</note> + </trans-unit> + <trans-unit id="bad message ID" xml:space="preserve"> + <source>bad message ID</source> + <target>téves üzenet ID</target> + <note>integrity error chat item</note> + </trans-unit> + <trans-unit id="bad message hash" xml:space="preserve"> + <source>bad message hash</source> + <target>érvénytelen az üzenet hasítóértéke</target> + <note>integrity error chat item</note> + </trans-unit> + <trans-unit id="blocked" xml:space="preserve"> + <source>blocked</source> + <target>letiltva</target> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="blocked %@" xml:space="preserve"> + <source>blocked %@</source> + <target>letiltotta őt: %@</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="blocked by admin" xml:space="preserve"> + <source>blocked by admin</source> + <target>letiltva az adminisztrátor által</target> + <note>blocked chat item +marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="bold" xml:space="preserve"> + <source>bold</source> + <target>félkövér</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="call" xml:space="preserve"> + <source>call</source> + <target>hívás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="call error" xml:space="preserve"> + <source>call error</source> + <target>híváshiba</target> + <note>call status</note> + </trans-unit> + <trans-unit id="call in progress" xml:space="preserve"> + <source>call in progress</source> + <target>hívás folyamatban</target> + <note>call status</note> + </trans-unit> + <trans-unit id="calling…" xml:space="preserve"> + <source>calling…</source> + <target>hívás…</target> + <note>call status</note> + </trans-unit> + <trans-unit id="cancelled %@" xml:space="preserve"> + <source>cancelled %@</source> + <target>%@ visszavonva</target> + <note>feature offered item</note> + </trans-unit> + <trans-unit id="changed address for you" xml:space="preserve"> + <source>changed address for you</source> + <target>módosította a címet az Ön számára</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="changed role of %@ to %@" xml:space="preserve"> + <source>changed role of %1$@ to %2$@</source> + <target>a következőre módosította %1$@ szerepkörét: „%2$@”</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="changed your role to %@" xml:space="preserve"> + <source>changed your role to %@</source> + <target>a következőre módosította az Ön szerepkörét: „%@”</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="changing address for %@…" xml:space="preserve"> + <source>changing address for %@…</source> + <target>cím módosítása %@ számára…</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="changing address…" xml:space="preserve"> + <source>changing address…</source> + <target>cím módosítása…</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="colored" xml:space="preserve"> + <source>colored</source> + <target>színezett</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="complete" xml:space="preserve"> + <source>complete</source> + <target>befejezett</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="connect to SimpleX Chat developers." xml:space="preserve"> + <source>connect to SimpleX Chat developers.</source> + <target>kapcsolódás a SimpleX Chat fejlesztőkhöz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="connected" xml:space="preserve"> + <source>connected</source> + <target>kapcsolódott</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="connected directly" xml:space="preserve"> + <source>connected directly</source> + <target>közvetlenül kapcsolódott</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="connecting" xml:space="preserve"> + <source>connecting</source> + <target>kapcsolódás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="connecting (accepted)" xml:space="preserve"> + <source>connecting (accepted)</source> + <target>kapcsolódás (elfogadva)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="connecting (announced)" xml:space="preserve"> + <source>connecting (announced)</source> + <target>kapcsolódás (bejelentve)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="connecting (introduced)" xml:space="preserve"> + <source>connecting (introduced)</source> + <target>kapcsolódás (bemutatkozva)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="connecting (introduction invitation)" xml:space="preserve"> + <source>connecting (introduction invitation)</source> + <target>kapcsolódás (bemutatkozó meghívó)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="connecting call" xml:space="preserve"> + <source>connecting call…</source> + <target>kapcsolódási hívás…</target> + <note>call status</note> + </trans-unit> + <trans-unit id="connecting…" xml:space="preserve"> + <source>connecting…</source> + <target>kapcsolódás…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="connection established" xml:space="preserve"> + <source>connection established</source> + <target>kapcsolat létrehozva</target> + <note>chat list item title (it should not be shown</note> + </trans-unit> + <trans-unit id="connection:%@" xml:space="preserve"> + <source>connection:%@</source> + <target>kapcsolat: %@</target> + <note>connection information</note> + </trans-unit> + <trans-unit id="contact %@ changed to %@" xml:space="preserve"> + <source>contact %1$@ changed to %2$@</source> + <target>%1$@ a következőre módosította a nevét: %2$@</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="contact has e2e encryption" xml:space="preserve"> + <source>contact has e2e encryption</source> + <target>a partner e2e titkosítással rendelkezik</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="contact has no e2e encryption" xml:space="preserve"> + <source>contact has no e2e encryption</source> + <target>a partner nem rendelkezik e2e titkosítással</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="creator" xml:space="preserve"> + <source>creator</source> + <target>készítő</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="custom" xml:space="preserve"> + <source>custom</source> + <target>egyéni</target> + <note>dropdown time picker choice</note> + </trans-unit> + <trans-unit id="database version is newer than the app, but no down migration for: %@" xml:space="preserve"> + <source>database version is newer than the app, but no down migration for: %@</source> + <target>az adatbázis verziója újabb, mint az alkalmazásé, de a visszafelé történő átköltöztetés viszont nem lehetséges a következőhöz: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="days" xml:space="preserve"> + <source>days</source> + <target>nap</target> + <note>time unit</note> + </trans-unit> + <trans-unit id="decryption errors" xml:space="preserve"> + <source>decryption errors</source> + <target>visszafejtési hibák</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="default (%@)" xml:space="preserve"> + <source>default (%@)</source> + <target>alapértelmezett (%@)</target> + <note>delete after time +pref value</note> + </trans-unit> + <trans-unit id="default (no)" xml:space="preserve"> + <source>default (no)</source> + <target>alapértelmezett (nem)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="default (yes)" xml:space="preserve"> + <source>default (yes)</source> + <target>alapértelmezett (igen)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="deleted" xml:space="preserve"> + <source>deleted</source> + <target>törölve</target> + <note>deleted chat item</note> + </trans-unit> + <trans-unit id="deleted contact" xml:space="preserve"> + <source>deleted contact</source> + <target>törölt partner</target> + <note>rcv direct event chat item</note> + </trans-unit> + <trans-unit id="deleted group" xml:space="preserve"> + <source>deleted group</source> + <target>törölt csoport</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="different migration in the app/database: %@ / %@" xml:space="preserve"> + <source>different migration in the app/database: %@ / %@</source> + <target>különböző átköltöztetés az alkalmazásban/adatbázisban: %@ / %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="direct" xml:space="preserve"> + <source>direct</source> + <target>közvetlen</target> + <note>connection level description</note> + </trans-unit> + <trans-unit id="disabled" xml:space="preserve"> + <source>disabled</source> + <target>letiltva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="duplicate message" xml:space="preserve"> + <source>duplicate message</source> + <target>duplikált üzenet</target> + <note>integrity error chat item</note> + </trans-unit> + <trans-unit id="duplicates" xml:space="preserve"> + <source>duplicates</source> + <target>duplikációk</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="e2e encrypted" xml:space="preserve"> + <source>e2e encrypted</source> + <target>e2e titkosított</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="enabled" xml:space="preserve"> + <source>enabled</source> + <target>engedélyezve</target> + <note>enabled status</note> + </trans-unit> + <trans-unit id="enabled for contact" xml:space="preserve"> + <source>enabled for contact</source> + <target>engedélyezve a partner számára</target> + <note>enabled status</note> + </trans-unit> + <trans-unit id="enabled for you" xml:space="preserve"> + <source>enabled for you</source> + <target>engedélyezve az Ön számára</target> + <note>enabled status</note> + </trans-unit> + <trans-unit id="encryption agreed" xml:space="preserve"> + <source>encryption agreed</source> + <target>titkosítás elfogadva</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption agreed for %@" xml:space="preserve"> + <source>encryption agreed for %@</source> + <target>titkosítás elfogadva %@ számára</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption ok" xml:space="preserve"> + <source>encryption ok</source> + <target>titkosítás rendben</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption ok for %@" xml:space="preserve"> + <source>encryption ok for %@</source> + <target>titkosítás rendben %@ számára</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption re-negotiation allowed" xml:space="preserve"> + <source>encryption re-negotiation allowed</source> + <target>a titkosítás újraegyeztetése engedélyezve van</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption re-negotiation allowed for %@" xml:space="preserve"> + <source>encryption re-negotiation allowed for %@</source> + <target>a titkosítás újraegyeztetése engedélyezve van %@ számára</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption re-negotiation required" xml:space="preserve"> + <source>encryption re-negotiation required</source> + <target>a titkosítás újraegyeztetése szükséges</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption re-negotiation required for %@" xml:space="preserve"> + <source>encryption re-negotiation required for %@</source> + <target>a titkosítás újraegyeztetése szükséges %@ számára</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="ended" xml:space="preserve"> + <source>ended</source> + <target>befejeződött</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="ended call %@" xml:space="preserve"> + <source>ended call %@</source> + <target>%@ hívása befejeződött</target> + <note>call status</note> + </trans-unit> + <trans-unit id="error" xml:space="preserve"> + <source>error</source> + <target>hiba</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="expired" xml:space="preserve"> + <source>expired</source> + <target>lejárt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="forwarded" xml:space="preserve"> + <source>forwarded</source> + <target>továbbított</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="group deleted" xml:space="preserve"> + <source>group deleted</source> + <target>a csoport törölve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="group profile updated" xml:space="preserve"> + <source>group profile updated</source> + <target>csoportprofil frissítve</target> + <note>snd group event chat item</note> + </trans-unit> + <trans-unit id="hours" xml:space="preserve"> + <source>hours</source> + <target>óra</target> + <note>time unit</note> + </trans-unit> + <trans-unit id="iOS Keychain is used to securely store passphrase - it allows receiving push notifications." xml:space="preserve"> + <source>iOS Keychain is used to securely store passphrase - it allows receiving push notifications.</source> + <target>Az iOS kulcstartó a jelmondat biztonságos tárolására szolgál – lehetővé teszi a push-értesítések fogadását.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." xml:space="preserve"> + <source>iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.</source> + <target>Az iOS kulcstartó biztonságosan fogja tárolni a jelmondatot az alkalmazás újraindítása, vagy a jelmondat módosítása után – lehetővé teszi a push-értesítések fogadását.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="inactive" xml:space="preserve"> + <source>inactive</source> + <target>inaktív</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="incognito via contact address link" xml:space="preserve"> + <source>incognito via contact address link</source> + <target>inkognitó a kapcsolattartási címhivatkozáson keresztül</target> + <note>chat list item description</note> + </trans-unit> + <trans-unit id="incognito via group link" xml:space="preserve"> + <source>incognito via group link</source> + <target>inkognitó a csoporthivatkozáson keresztül</target> + <note>chat list item description</note> + </trans-unit> + <trans-unit id="incognito via one-time link" xml:space="preserve"> + <source>incognito via one-time link</source> + <target>inkognitó egy egyszer használható meghívón keresztül</target> + <note>chat list item description</note> + </trans-unit> + <trans-unit id="indirect (%d)" xml:space="preserve"> + <source>indirect (%d)</source> + <target>közvetett (%d)</target> + <note>connection level description</note> + </trans-unit> + <trans-unit id="invalid chat" xml:space="preserve"> + <source>invalid chat</source> + <target>érvénytelen csevegés</target> + <note>invalid chat data</note> + </trans-unit> + <trans-unit id="invalid chat data" xml:space="preserve"> + <source>invalid chat data</source> + <target>érvénytelen csevegésadat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="invalid data" xml:space="preserve"> + <source>invalid data</source> + <target>érvénytelen adat</target> + <note>invalid chat item</note> + </trans-unit> + <trans-unit id="invitation to group %@" xml:space="preserve"> + <source>invitation to group %@</source> + <target>meghívás a(z) %@ csoportba</target> + <note>group name</note> + </trans-unit> + <trans-unit id="invite" xml:space="preserve"> + <source>invite</source> + <target>meghívás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="invited" xml:space="preserve"> + <source>invited</source> + <target>meghíva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="invited %@" xml:space="preserve"> + <source>invited %@</source> + <target>meghívta őt: %@</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="invited to connect" xml:space="preserve"> + <source>invited to connect</source> + <target>Függőben lévő meghívó</target> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="invited via your group link" xml:space="preserve"> + <source>invited via your group link</source> + <target>meghíva a saját csoporthivatkozásán keresztül</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="italic" xml:space="preserve"> + <source>italic</source> + <target>dőlt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="join as %@" xml:space="preserve"> + <source>join as %@</source> + <target>csatlakozás mint %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="left" xml:space="preserve"> + <source>left</source> + <target>elhagyta a csoportot</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="marked deleted" xml:space="preserve"> + <source>marked deleted</source> + <target>törlésre jelölve</target> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="member" xml:space="preserve"> + <source>member</source> + <target>tag</target> + <note>member role</note> + </trans-unit> + <trans-unit id="member %@ changed to %@" xml:space="preserve"> + <source>member %1$@ changed to %2$@</source> + <target>%1$@ a következőre módosította a nevét: %2$@</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="member connected" xml:space="preserve"> + <source>connected</source> + <target>kapcsolódott</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="message" xml:space="preserve"> + <source>message</source> + <target>üzenet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="message received" xml:space="preserve"> + <source>message received</source> + <target>üzenet érkezett</target> + <note>notification</note> + </trans-unit> + <trans-unit id="minutes" xml:space="preserve"> + <source>minutes</source> + <target>perc</target> + <note>time unit</note> + </trans-unit> + <trans-unit id="missed call" xml:space="preserve"> + <source>missed call</source> + <target>nem fogadott hívás</target> + <note>call status</note> + </trans-unit> + <trans-unit id="moderated" xml:space="preserve"> + <source>moderated</source> + <target>moderált</target> + <note>moderated chat item</note> + </trans-unit> + <trans-unit id="moderated by %@" xml:space="preserve"> + <source>moderated by %@</source> + <target>moderálva lett %@ által</target> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="moderator" xml:space="preserve"> + <source>moderator</source> + <target>moderátor</target> + <note>member role</note> + </trans-unit> + <trans-unit id="months" xml:space="preserve"> + <source>months</source> + <target>hónap</target> + <note>time unit</note> + </trans-unit> + <trans-unit id="never" xml:space="preserve"> + <source>never</source> + <target>soha</target> + <note>delete after time</note> + </trans-unit> + <trans-unit id="new message" xml:space="preserve"> + <source>new message</source> + <target>új üzenet</target> + <note>notification</note> + </trans-unit> + <trans-unit id="no" xml:space="preserve"> + <source>no</source> + <target>nem</target> + <note>pref value</note> + </trans-unit> + <trans-unit id="no e2e encryption" xml:space="preserve"> + <source>no e2e encryption</source> + <target>nincs e2e titkosítás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="no text" xml:space="preserve"> + <source>no text</source> + <target>nincs szöveg</target> + <note>copied message info in history</note> + </trans-unit> + <trans-unit id="observer" xml:space="preserve"> + <source>observer</source> + <target>megfigyelő</target> + <note>member role</note> + </trans-unit> + <trans-unit id="off" xml:space="preserve"> + <source>off</source> + <target>kikapcsolva</target> + <note>enabled status +group pref value +time to disappear</note> + </trans-unit> + <trans-unit id="offered %@" xml:space="preserve"> + <source>offered %@</source> + <target>%@ ajánlotta</target> + <note>feature offered item</note> + </trans-unit> + <trans-unit id="offered %@: %@" xml:space="preserve"> + <source>offered %1$@: %2$@</source> + <target>ajánlotta: %1$@, ekkor: %2$@</target> + <note>feature offered item</note> + </trans-unit> + <trans-unit id="on" xml:space="preserve"> + <source>on</source> + <target>bekapcsolva</target> + <note>group pref value</note> + </trans-unit> + <trans-unit id="other" xml:space="preserve"> + <source>other</source> + <target>egyéb</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="other errors" xml:space="preserve"> + <source>other errors</source> + <target>egyéb hibák</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="owner" xml:space="preserve"> + <source>owner</source> + <target>tulajdonos</target> + <note>member role</note> + </trans-unit> + <trans-unit id="owners" xml:space="preserve"> + <source>owners</source> + <target>tulajdonosok</target> + <note>feature role</note> + </trans-unit> + <trans-unit id="peer-to-peer" xml:space="preserve"> + <source>peer-to-peer</source> + <target>egyenrangú</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="pending" xml:space="preserve"> + <source>pending</source> + <target>függőben</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="pending approval" xml:space="preserve"> + <source>pending approval</source> + <target>jóváhagyásra vár</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="quantum resistant e2e encryption" xml:space="preserve"> + <source>quantum resistant e2e encryption</source> + <target>végpontok közötti kvantumbiztos titkosítás</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="received answer…" xml:space="preserve"> + <source>received answer…</source> + <target>válasz fogadása…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="received confirmation…" xml:space="preserve"> + <source>received confirmation…</source> + <target>visszaigazolás fogadása…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="rejected" xml:space="preserve"> + <source>rejected</source> + <target>elutasítva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="rejected call" xml:space="preserve"> + <source>rejected call</source> + <target>elutasított hívás</target> + <note>call status</note> + </trans-unit> + <trans-unit id="removed" xml:space="preserve"> + <source>removed</source> + <target>eltávolítva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="removed %@" xml:space="preserve"> + <source>removed %@</source> + <target>eltávolította őt: %@</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="removed contact address" xml:space="preserve"> + <source>removed contact address</source> + <target>eltávolította a kapcsolattartási címet</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="removed profile picture" xml:space="preserve"> + <source>removed profile picture</source> + <target>eltávolította a profilképét</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="removed you" xml:space="preserve"> + <source>removed you</source> + <target>eltávolította Önt</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="requested to connect" xml:space="preserve"> + <source>requested to connect</source> + <target>Függőben lévő meghívási kérelem</target> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="saved" xml:space="preserve"> + <source>saved</source> + <target>mentett</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="saved from %@" xml:space="preserve"> + <source>saved from %@</source> + <target>elmentve innen: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="search" xml:space="preserve"> + <source>search</source> + <target>keresés</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="sec" xml:space="preserve"> + <source>sec</source> + <target>mp</target> + <note>network option</note> + </trans-unit> + <trans-unit id="seconds" xml:space="preserve"> + <source>seconds</source> + <target>másodperc</target> + <note>time unit</note> + </trans-unit> + <trans-unit id="secret" xml:space="preserve"> + <source>secret</source> + <target>titok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="security code changed" xml:space="preserve"> + <source>security code changed</source> + <target>a biztonsági kód módosult</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="send direct message" xml:space="preserve"> + <source>send direct message</source> + <target>közvetlen üzenet küldése</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve"> + <source>server queue info: %1$@ + +last received msg: %2$@</source> + <target>a kiszolgáló sorbaállítási információi: %1$@ + +utoljára fogadott üzenet: %2$@</target> + <note>queue info</note> + </trans-unit> + <trans-unit id="set new contact address" xml:space="preserve"> + <source>set new contact address</source> + <target>új kapcsolattartási címet állított be</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="set new profile picture" xml:space="preserve"> + <source>set new profile picture</source> + <target>új profilképet állított be</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="standard end-to-end encryption" xml:space="preserve"> + <source>standard end-to-end encryption</source> + <target>szabványos végpontok közötti titkosítás</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="starting…" xml:space="preserve"> + <source>starting…</source> + <target>indítás…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="strike" xml:space="preserve"> + <source>strike</source> + <target>áthúzott</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="this contact" xml:space="preserve"> + <source>this contact</source> + <target>ez a partner</target> + <note>notification title</note> + </trans-unit> + <trans-unit id="unblocked %@" xml:space="preserve"> + <source>unblocked %@</source> + <target>feloldotta %@ letiltását</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="unknown" xml:space="preserve"> + <source>unknown</source> + <target>ismeretlen</target> + <note>connection info</note> + </trans-unit> + <trans-unit id="unknown servers" xml:space="preserve"> + <source>unknown servers</source> + <target>ismeretlen átjátszók</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unknown status" xml:space="preserve"> + <source>unknown status</source> + <target>ismeretlen állapot</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unprotected" xml:space="preserve"> + <source>unprotected</source> + <target>nem védett</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="updated group profile" xml:space="preserve"> + <source>updated group profile</source> + <target>frissítette a csoport profilját</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="updated profile" xml:space="preserve"> + <source>updated profile</source> + <target>frissített profil</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="v%@" xml:space="preserve"> + <source>v%@</source> + <target>v%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="v%@ (%@)" xml:space="preserve"> + <source>v%@ (%@)</source> + <target>v%@ (%@)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="via contact address link" xml:space="preserve"> + <source>via contact address link</source> + <target>a kapcsolattartási címhivatkozáson keresztül</target> + <note>chat list item description</note> + </trans-unit> + <trans-unit id="via group link" xml:space="preserve"> + <source>via group link</source> + <target>a csoporthivatkozáson keresztül</target> + <note>chat list item description</note> + </trans-unit> + <trans-unit id="via one-time link" xml:space="preserve"> + <source>via one-time link</source> + <target>egy egyszer használható meghívón keresztül</target> + <note>chat list item description</note> + </trans-unit> + <trans-unit id="via relay" xml:space="preserve"> + <source>via relay</source> + <target>egy továbbítókiszolgálón keresztül</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="video" xml:space="preserve"> + <source>video</source> + <target>videó</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> + <source>video call (not e2e encrypted)</source> + <target>videóhívás (nem e2e titkosított)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="waiting for answer…" xml:space="preserve"> + <source>waiting for answer…</source> + <target>várakozás a válaszra…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="waiting for confirmation…" xml:space="preserve"> + <source>waiting for confirmation…</source> + <target>várakozás a visszaigazolásra…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="wants to connect to you!" xml:space="preserve"> + <source>wants to connect to you!</source> + <target>kapcsolatba akar lépni Önnel!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="weeks" xml:space="preserve"> + <source>weeks</source> + <target>hét</target> + <note>time unit</note> + </trans-unit> + <trans-unit id="when IP hidden" xml:space="preserve"> + <source>when IP hidden</source> + <target>ha az IP-cím rejtett</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="yes" xml:space="preserve"> + <source>yes</source> + <target>igen</target> + <note>pref value</note> + </trans-unit> + <trans-unit id="you" xml:space="preserve"> + <source>you</source> + <target>Ön</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="you are invited to group" xml:space="preserve"> + <source>you are invited to group</source> + <target>Ön meghívást kapott a csoportba</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="you are observer" xml:space="preserve"> + <source>you are observer</source> + <target>Ön megfigyelő</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="you blocked %@" xml:space="preserve"> + <source>you blocked %@</source> + <target>Ön letiltotta őt: %@</target> + <note>snd group event chat item</note> + </trans-unit> + <trans-unit id="you changed address" xml:space="preserve"> + <source>you changed address</source> + <target>Ön módosította a címet</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="you changed address for %@" xml:space="preserve"> + <source>you changed address for %@</source> + <target>Ön módosította a címet %@ számára</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="you changed role for yourself to %@" xml:space="preserve"> + <source>you changed role for yourself to %@</source> + <target>Ön a következőre módosította a saját szerepkörét: „%@”</target> + <note>snd group event chat item</note> + </trans-unit> + <trans-unit id="you changed role of %@ to %@" xml:space="preserve"> + <source>you changed role of %1$@ to %2$@</source> + <target>Ön a következőre módosította %1$@ szerepkörét: „%2$@”</target> + <note>snd group event chat item</note> + </trans-unit> + <trans-unit id="you left" xml:space="preserve"> + <source>you left</source> + <target>Ön elhagyta a csoportot</target> + <note>snd group event chat item</note> + </trans-unit> + <trans-unit id="you removed %@" xml:space="preserve"> + <source>you removed %@</source> + <target>Ön eltávolította őt: %@</target> + <note>snd group event chat item</note> + </trans-unit> + <trans-unit id="you shared one-time link" xml:space="preserve"> + <source>you shared one-time link</source> + <target>Ön egy egyszer használható meghívót osztott meg</target> + <note>chat list item description</note> + </trans-unit> + <trans-unit id="you shared one-time link incognito" xml:space="preserve"> + <source>you shared one-time link incognito</source> + <target>Ön egy egyszer használható meghívót osztott meg inkognitóban</target> + <note>chat list item description</note> + </trans-unit> + <trans-unit id="you unblocked %@" xml:space="preserve"> + <source>you unblocked %@</source> + <target>Ön feloldotta %@ letiltását</target> + <note>snd group event chat item</note> + </trans-unit> + <trans-unit id="you: " xml:space="preserve"> + <source>you: </source> + <target>Ön: </target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="~strike~" xml:space="preserve"> + <source>\~strike~</source> + <target>\~áthúzott~</target> + <note>No comment provided by engineer.</note> + </trans-unit> + </body> + </file> + <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="hu" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX</source> + <target>SimpleX</target> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSCameraUsageDescription" xml:space="preserve"> + <source>SimpleX needs camera access to scan QR codes to connect to other users and for video calls.</source> + <target>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.</target> + <note>Privacy - Camera Usage Description</note> + </trans-unit> + <trans-unit id="NSFaceIDUsageDescription" xml:space="preserve"> + <source>SimpleX uses Face ID for local authentication</source> + <target>A SimpleX Face ID-t használ a helyi hitelesítéshez</target> + <note>Privacy - Face ID Usage Description</note> + </trans-unit> + <trans-unit id="NSLocalNetworkUsageDescription" xml:space="preserve"> + <source>SimpleX uses local network access to allow using user chat profile via desktop app on the same network.</source> + <target>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.</target> + <note>Privacy - Local Network Usage Description</note> + </trans-unit> + <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> + <source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source> + <target>A SimpleXnek mikrofon-hozzáférésre van szüksége hang- és videohívásokhoz, valamint hangüzenetek rögzítéséhez.</target> + <note>Privacy - Microphone Usage Description</note> + </trans-unit> + <trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve"> + <source>SimpleX needs access to Photo Library for saving captured and received media</source> + <target>A SimpleXnek galéria-hozzáférésre van szüksége a rögzített és fogadott média mentéséhez</target> + <note>Privacy - Photo Library Additions Usage Description</note> + </trans-unit> + </body> + </file> + <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="hu" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> + <source>SimpleX NSE</source> + <target>SimpleX NSE</target> + <note>Bundle display name</note> + </trans-unit> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX NSE</source> + <target>SimpleX NSE</target> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> + <source>Copyright © 2022 SimpleX Chat. All rights reserved.</source> + <target>Copyright © 2022 SimpleX Chat. Minden jog fenntartva.</target> + <note>Copyright (human-readable)</note> + </trans-unit> + </body> + </file> + <file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="hu" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%d new events" xml:space="preserve"> + <source>%d new events</source> + <target>%d új esemény</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="From %d chat(s)" xml:space="preserve"> + <source>From %d chat(s)</source> + <target>%d csevegésből</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="From: %@" xml:space="preserve"> + <source>From: %@</source> + <target>Tőle: %@</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>Új események</target> + <note>notification</note> + </trans-unit> + <trans-unit id="New messages" xml:space="preserve"> + <source>New messages</source> + <target>Új üzenetek</target> + <note>notification</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="hu" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle display name</note> + </trans-unit> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> + <source>Copyright © 2024 SimpleX Chat. All rights reserved.</source> + <target>Copyright © 2024 SimpleX Chat. Minden jog fenntartva.</target> + <note>Copyright (human-readable)</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="hu" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <target>%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App is locked!" xml:space="preserve"> + <source>App is locked!</source> + <target>Az alkalmazás zárolva van!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <target>Mégse</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <target>Nem lehet hozzáférni a kulcstartóhoz az adatbázis jelszavának mentéséhez</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Nem lehet továbbítani az üzenetet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Comment" xml:space="preserve"> + <source>Comment</source> + <target>Hozzászólás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <target>Jelenleg támogatott legnagyobb fájl méret: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade required" xml:space="preserve"> + <source>Database downgrade required</source> + <target>Adatbázis visszafejlesztése szükséges</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <target>Adatbázis titkosítva!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <target>Adatbázishiba</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <target>Az adatbázis jelmondata nem egyezik a kulcstartóba mentettől.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <target>A csevegés megnyitásához adja meg az adatbázis jelmondatát.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade required" xml:space="preserve"> + <source>Database upgrade required</source> + <target>Adatbázis fejlesztése szükséges</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing file" xml:space="preserve"> + <source>Error preparing file</source> + <target>Hiba történt a fájl előkészítésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing message" xml:space="preserve"> + <source>Error preparing message</source> + <target>Hiba történt az üzenet előkészítésekor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <target>Hiba: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Fájlhiba</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <target>Nem kompatibilis adatbázis-verzió</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Érvénytelen átköltöztetési visszaigazolás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <target>Kulcstartóhiba</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <target>Nagy fájl!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No active profile" xml:space="preserve"> + <source>No active profile</source> + <target>Nincs aktív profil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <target>Rendben</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to downgrade the database." xml:space="preserve"> + <source>Open the app to downgrade the database.</source> + <target>Nyissa meg az alkalmazást az adatbázis visszafejlesztéséhez.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to upgrade the database." xml:space="preserve"> + <source>Open the app to upgrade the database.</source> + <target>Nyissa meg az alkalmazást az adatbázis fejlesztéséhez.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passphrase" xml:space="preserve"> + <source>Passphrase</source> + <target>Jelmondat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve"> + <source>Please create a profile in the SimpleX app</source> + <target>Hozzon létre egy profilt a SimpleX alkalmazásban</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>A kijelölt csevegési beállítások tiltják ezt az üzenetet.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending a message takes longer than expected." xml:space="preserve"> + <source>Sending a message takes longer than expected.</source> + <target>Az üzenet elküldése a vártnál tovább tart.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending message…" xml:space="preserve"> + <source>Sending message…</source> + <target>Üzenet küldése…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <target>Megosztás</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Slow network?" xml:space="preserve"> + <source>Slow network?</source> + <target>Lassú a hálózata?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <target>Ismeretlen adatbázishiba: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unsupported format" xml:space="preserve"> + <source>Unsupported format</source> + <target>Nem támogatott formátum</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wait" xml:space="preserve"> + <source>Wait</source> + <target>Várjon</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <target>Érvénytelen adatbázis-jelmondat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve"> + <source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source> + <target>A megosztást az Adatvédelem és biztonság / SimpleX-zár menüben engedélyezheti.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + </body> + </file> +</xliff> diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..aaa7f79bc8 --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,23 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.000", + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.533" + } + }, + "idiom" : "universal" + } + ], + "properties" : { + "localizable" : true + }, + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..9c675514f4 --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/en.lproj/Localizable.strings new file mode 100644 index 0000000000..cb83427195 --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/en.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* No comment provided by engineer. */ +"_italic_" = "\\_italic_"; + +/* No comment provided by engineer. */ +"*bold*" = "\\*bold*"; + +/* No comment provided by engineer. */ +"`a + b`" = "\\`a + b`"; + +/* No comment provided by engineer. */ +"~strike~" = "\\~strike~"; + +/* call status */ +"connecting call" = "connecting call…"; + +/* No comment provided by engineer. */ +"Connecting server…" = "Connecting to server…"; + +/* No comment provided by engineer. */ +"Connecting server… (error: %@)" = "Connecting to server… (error: %@)"; + +/* rcv group event chat item */ +"member connected" = "connected"; + +/* No comment provided by engineer. */ +"No group!" = "Group not found!"; diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings new file mode 100644 index 0000000000..d34eb67fc7 --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -0,0 +1,12 @@ +/* Bundle name */ +"CFBundleName" = "SimpleX"; +/* Privacy - Camera Usage Description */ +"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; +/* Privacy - Face ID Usage Description */ +"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; +/* Privacy - Local Network Usage Description */ +"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; +/* Privacy - Photo Library Additions Usage Description */ +"NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/contents.json b/apps/ios/SimpleX Localizations/hu.xcloc/contents.json new file mode 100644 index 0000000000..c07ec0f900 --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/contents.json @@ -0,0 +1,12 @@ +{ + "developmentRegion" : "en", + "project" : "SimpleX.xcodeproj", + "targetLocale" : "hu", + "toolInfo" : { + "toolBuildNumber" : "16C5032a", + "toolID" : "com.apple.dt.xcode", + "toolName" : "Xcode", + "toolVersion" : "16.2" + }, + "version" : "1.0" +} \ No newline at end of file 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 bf1b1ee386..cf5f61918f 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -2,36 +2,9 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="it" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> - <trans-unit id=" " xml:space="preserve"> - <source> -</source> - <target> -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (" xml:space="preserve"> - <source> (</source> - <target> (</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> <target> (può essere copiato)</target> @@ -94,7 +67,7 @@ </trans-unit> <trans-unit id="%@ and %@ connected" xml:space="preserve"> <source>%@ and %@ connected</source> - <target>%@ e %@ sono connessi/e</target> + <target>%@ e %@ si sono connessi/e</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ at %@:" xml:space="preserve"> @@ -104,7 +77,12 @@ </trans-unit> <trans-unit id="%@ connected" xml:space="preserve"> <source>%@ connected</source> - <target>%@ si è connesso/a</target> + <target>%@ connesso/a</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve"> + <source>%@ downloaded</source> + <target>%@ scaricati</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ is connected!" xml:space="preserve"> @@ -122,9 +100,19 @@ <target>%@ è verificato/a</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ server" xml:space="preserve"> + <source>%@ server</source> + <target>%@ server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ servers" xml:space="preserve"> <source>%@ servers</source> - <target>Server %@</target> + <target>%@ server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve"> + <source>%@ uploaded</source> + <target>%@ caricati</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ wants to connect!" xml:space="preserve"> @@ -132,6 +120,11 @@ <target>%@ si vuole connettere!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve"> + <source>%1$@, %2$@</source> + <target>%1$@, %2$@</target> + <note>format for date separator in chat</note> + </trans-unit> <trans-unit id="%@, %@ and %lld members" xml:space="preserve"> <source>%@, %@ and %lld members</source> <target>%@, %@ e %lld membri</target> @@ -139,7 +132,7 @@ </trans-unit> <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> <source>%@, %@ and %lld other members connected</source> - <target>%@, %@ e altri %lld membri sono connessi</target> + <target>%@, %@ e altri %lld membri si sono connessi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@:" xml:space="preserve"> @@ -152,11 +145,36 @@ <target>%d giorni</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve"> + <source>%d file(s) are still being downloaded.</source> + <target>%d file è/sono ancora in scaricamento.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve"> + <source>%d file(s) failed to download.</source> + <target>%d file ha/hanno fallito lo scaricamento.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve"> + <source>%d file(s) were deleted.</source> + <target>%d file è/sono stato/i eliminato/i.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve"> + <source>%d file(s) were not downloaded.</source> + <target>%d file non è/sono stato/i scaricato/i.</target> + <note>forward confirmation reason</note> + </trans-unit> <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> <target>%d ore</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve"> + <source>%d messages not forwarded</source> + <target>%d messaggi non inoltrati</target> + <note>alert title</note> + </trans-unit> <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> <target>%d min</target> @@ -172,6 +190,11 @@ <target>%d sec</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve"> + <source>%d seconds(s)</source> + <target>%d secondo/i</target> + <note>delete after time</note> + </trans-unit> <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> <target>%d messaggio/i saltato/i</target> @@ -217,6 +240,11 @@ <target>%lld messaggi bloccati</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve"> + <source>%lld messages blocked by admin</source> + <target>%lld messaggi bloccati dall'amministratore</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld messages marked deleted" xml:space="preserve"> <source>%lld messages marked deleted</source> <target>%lld messaggi contrassegnati eliminati</target> @@ -237,11 +265,6 @@ <target>%lld nuove lingue dell'interfaccia</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> - <source>%lld second(s)</source> - <target>%lld secondo/i</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="%lld seconds" xml:space="preserve"> <source>%lld seconds</source> <target>%lld secondi</target> @@ -292,11 +315,6 @@ <target>%u messaggi saltati.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> - <source>(</source> - <target>(</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="(new)" xml:space="preserve"> <source>(new)</source> <target>(nuovo)</target> @@ -307,34 +325,29 @@ <target>(questo dispositivo v%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve"> - <source>)</source> - <target>)</target> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target>**Aggiungi contatto**: per creare un nuovo link di invito.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target>**Aggiungi un contatto**: per creare il tuo codice QR o link una tantum per il tuo contatto.</target> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve"> + <source>**Create group**: to create a new group.</source> + <target>**Crea gruppo**: per creare un nuovo gruppo.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> - <source>**Create link / QR code** for your contact to use.</source> - <target>**Crea link / codice QR** da usare per il tuo contatto.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Più privato**: controlla messaggi nuovi ogni 20 minuti. Viene condiviso il token del dispositivo con il server di SimpleX Chat, ma non quanti contatti o messaggi hai.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Il più privato**: non usare il server di notifica di SimpleX Chat, controlla i messaggi periodicamente in secondo piano (dipende da quanto spesso usi l'app).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <target>**Incolla il link ricevuto** o aprilo nel browser e tocca **Apri in app mobile**.</target> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> + <target>**Nota bene**: usare lo stesso database su due dispositivi bloccherà la decifrazione dei messaggi dalle tue connessioni, come misura di sicurezza.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> @@ -342,14 +355,14 @@ <target>**Nota bene**: NON potrai recuperare o cambiare la password se la perdi.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Consigliato**: vengono inviati il token del dispositivo e le notifiche al server di notifica di SimpleX Chat, ma non il contenuto del messaggio,la sua dimensione o il suo mittente.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target>**Scansiona codice QR**: per connetterti al contatto di persona o via videochiamata.</target> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve"> + <source>**Scan / Paste link**: to connect via a link you received.</source> + <target>**Scansiona / Incolla link**: per connetterti tramite un link che hai ricevuto.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> @@ -357,6 +370,11 @@ <target>**Attenzione**: le notifiche push istantanee richiedono una password salvata nel portachiavi.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve"> + <source>**Warning**: the archive will be removed.</source> + <target>**Attenzione**: l'archivio verrà rimosso.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> <source>**e2e encrypted** audio call</source> <target>Chiamata **crittografata e2e**</target> @@ -372,11 +390,6 @@ <target>\*grassetto*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=", " xml:space="preserve"> - <source>, </source> - <target>, </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). @@ -413,11 +426,6 @@ - cronologia delle modifiche.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="." xml:space="preserve"> - <source>.</source> - <target>.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="0 sec" xml:space="preserve"> <source>0 sec</source> <target>0 sec</target> @@ -431,7 +439,8 @@ <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> <target>1 giorno</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> @@ -446,18 +455,30 @@ <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> <target>1 mese</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> <target>1 settimana</target> - <note>time interval</note> + <note>delete after time +time interval</note> + </trans-unit> + <trans-unit id="1 year" xml:space="preserve"> + <source>1 year</source> + <target>1 anno</target> + <note>delete after time</note> </trans-unit> <trans-unit id="1-time link" xml:space="preserve"> <source>1-time link</source> <target>Link una tantum</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> + <target>Il link una tantum può essere usato *con un solo contatto* - condividilo di persona o tramite qualsiasi messenger.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="5 minutes" xml:space="preserve"> <source>5 minutes</source> <target>5 minuti</target> @@ -473,11 +494,6 @@ <target>30 secondi</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve"> - <source>: </source> - <target>: </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> @@ -527,31 +543,32 @@ <target>Interrompere il cambio di indirizzo?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve"> - <source>About SimpleX</source> - <target>Riguardo SimpleX</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve"> <source>About SimpleX Chat</source> <target>Riguardo SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve"> - <source>About SimpleX address</source> - <target>Info sull'indirizzo SimpleX</target> + <trans-unit id="About operators" xml:space="preserve"> + <source>About operators</source> + <target>Info sugli operatori</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accent color" xml:space="preserve"> - <source>Accent color</source> - <target>Colore principale</target> + <trans-unit id="Accent" xml:space="preserve"> + <source>Accent</source> + <target>Principale</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept" xml:space="preserve"> <source>Accept</source> <target>Accetta</target> <note>accept contact request via notification - accept incoming call via notification</note> +accept incoming call via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accept conditions" xml:space="preserve"> + <source>Accept conditions</source> + <target>Accetta le condizioni</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept connection request?" xml:space="preserve"> <source>Accept connection request?</source> @@ -566,16 +583,47 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Accetta in incognito</target> - <note>accept contact request via notification</note> + <note>accept contact request via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve"> + <source>Accepted conditions</source> + <target>Condizioni accettate</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve"> + <source>Acknowledged</source> + <target>Riconosciuto</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve"> + <source>Acknowledgement errors</source> + <target>Errori di riconoscimento</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Active" xml:space="preserve"> + <source>Active</source> + <target>Attivo</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve"> + <source>Active connections</source> + <target>Connessioni attive</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> <target>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.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> - <source>Add preset servers</source> - <target>Aggiungi server preimpostati</target> + <trans-unit id="Add friends" xml:space="preserve"> + <source>Add friends</source> + <target>Aggiungi amici</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve"> + <source>Add list</source> + <target>Aggiungi elenco</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add profile" xml:space="preserve"> @@ -583,14 +631,19 @@ <target>Aggiungi profilo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> + <target>Aggiungi server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> <source>Add servers by scanning QR codes.</source> <target>Aggiungi server scansionando codici QR.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> - <target>Aggiungi server…</target> + <trans-unit id="Add team members" xml:space="preserve"> + <source>Add team members</source> + <target>Aggiungi membri del team</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -598,11 +651,46 @@ <target>Aggiungi ad un altro dispositivo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add to list" xml:space="preserve"> + <source>Add to list</source> + <target>Aggiungi ad un elenco</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> <target>Aggiungi messaggio di benvenuto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve"> + <source>Add your team members to the conversations.</source> + <target>Aggiungi i membri del tuo team alle conversazioni.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve"> + <source>Added media & file servers</source> + <target>Server di multimediali e file aggiunti</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve"> + <source>Added message servers</source> + <target>Server dei messaggi aggiunti</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve"> + <source>Additional accent</source> + <target>Principale aggiuntivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve"> + <source>Additional accent 2</source> + <target>Principale aggiuntivo 2</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve"> + <source>Additional secondary</source> + <target>Secondario aggiuntivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Address" xml:space="preserve"> <source>Address</source> <target>Indirizzo</target> @@ -613,6 +701,21 @@ <target>Il cambio di indirizzo verrà interrotto. Verrà usato il vecchio indirizzo di ricezione.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve"> + <source>Address or 1-time link?</source> + <target>Indirizzo o link una tantum?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve"> + <source>Address settings</source> + <target>Impostazioni dell'indirizzo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve"> + <source>Admins can block a member for all.</source> + <target>Gli amministratori possono bloccare un membro per tutti.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> <source>Admins can create the links to join groups.</source> <target>Gli amministratori possono creare i link per entrare nei gruppi.</target> @@ -623,6 +726,16 @@ <target>Impostazioni di rete avanzate</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve"> + <source>Advanced settings</source> + <target>Impostazioni avanzate</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All" xml:space="preserve"> + <source>All</source> + <target>Tutte</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All app data is deleted." xml:space="preserve"> <source>All app data is deleted.</source> <target>Tutti i dati dell'app vengono eliminati.</target> @@ -633,16 +746,36 @@ <target>Tutte le chat e i messaggi verranno eliminati. Non è reversibile!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <target>Tutte le chat verranno rimosse dall'elenco %@ e l'elenco eliminato.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> <target>Tutti i dati vengono cancellati quando inserito.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve"> + <source>All data is kept private on your device.</source> + <target>Tutti i dati sono privati, nel tuo dispositivo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> <target>Tutti i membri del gruppo resteranno connessi.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <target>Tutti i messaggi e i file vengono inviati **crittografati end-to-end**, con sicurezza resistenti alla quantistica nei messaggi diretti.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone!</source> + <target>Tutti i messaggi verranno eliminati, non è reversibile!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> <target>Tutti i messaggi verranno eliminati, non è reversibile! I messaggi verranno eliminati SOLO per te.</target> @@ -653,6 +786,21 @@ <target>Tutti i nuovi messaggi da %@ verrranno nascosti!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All profiles" xml:space="preserve"> + <source>All profiles</source> + <target>Tutti gli profili</target> + <note>profile dropdown</note> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve"> + <source>All reports will be archived for you.</source> + <target>Tutte le segnalazioni verranno archiviate per te.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All servers" xml:space="preserve"> + <source>All servers</source> + <target>Tutti i server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All your contacts will remain connected." xml:space="preserve"> <source>All your contacts will remain connected.</source> <target>Tutti i tuoi contatti resteranno connessi.</target> @@ -663,6 +811,11 @@ <target>Tutti i tuoi contatti resteranno connessi. L'aggiornamento del profilo verrà inviato ai tuoi contatti.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target>Tutti i tuoi contatti, le conversazioni e i file verranno criptati in modo sicuro e caricati in blocchi sui relay XFTP configurati.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow" xml:space="preserve"> <source>Allow</source> <target>Consenti</target> @@ -673,14 +826,24 @@ <target>Consenti le chiamate solo se il tuo contatto le consente.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve"> + <source>Allow calls?</source> + <target>Consentire le chiamate?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> <target>Consenti i messaggi a tempo solo se il contatto li consente a te.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <target>Consenti l'eliminazione irreversibile dei messaggi solo se il contatto la consente a te.</target> + <trans-unit id="Allow downgrade" xml:space="preserve"> + <source>Allow downgrade</source> + <target>Consenti downgrade</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target>Consenti l'eliminazione irreversibile dei messaggi solo se il contatto la consente a te. (24 ore)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> @@ -703,9 +866,24 @@ <target>Permetti l'invio di messaggi a tempo.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow to irreversibly delete sent messages.</source> - <target>Permetti di eliminare irreversibilmente i messaggi inviati.</target> + <trans-unit id="Allow sharing" xml:space="preserve"> + <source>Allow sharing</source> + <target>Consenti la condivisione</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target>Permetti di eliminare irreversibilmente i messaggi inviati. (24 ore)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve"> + <source>Allow to report messsages to moderators.</source> + <target>Consenti di segnalare messaggi ai moderatori.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve"> + <source>Allow to send SimpleX links.</source> + <target>Consenti di inviare link di SimpleX.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send files and media." xml:space="preserve"> @@ -738,9 +916,9 @@ <target>Consenti ai tuoi contatti di chiamarti.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow your contacts to irreversibly delete sent messages.</source> - <target>Permetti ai tuoi contatti di eliminare irreversibilmente i messaggi inviati.</target> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target>Permetti ai tuoi contatti di eliminare irreversibilmente i messaggi inviati. (24 ore)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> @@ -768,6 +946,11 @@ <target>Già in ingresso nel gruppo!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve"> + <source>Always use private routing.</source> + <target>Usa sempre l'instradamento privato.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> <source>Always use relay</source> <target>Connetti via relay</target> @@ -778,21 +961,41 @@ <target>Viene creato un profilo di chat vuoto con il nome scelto e l'app si apre come al solito.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Another reason" xml:space="preserve"> + <source>Another reason</source> + <target>Altro motivo</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> <target>Rispondi alla chiamata</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> + <target>Chiunque può installare i server.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> <target>Build dell'app: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App data migration" xml:space="preserve"> + <source>App data migration</source> + <target>Migrazione dati dell'app</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> <source>App encrypts new local files (except videos).</source> <target>L'app cripta i nuovi file locali (eccetto i video).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App group:" xml:space="preserve"> + <source>App group:</source> + <target>Gruppo app:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>Icona app</target> @@ -808,6 +1011,11 @@ <target>Il codice di accesso dell'app viene sostituito da un codice di autodistruzione.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App session" xml:space="preserve"> + <source>App session</source> + <target>Sessione dell'app</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App version" xml:space="preserve"> <source>App version</source> <target>Versione dell'app</target> @@ -823,6 +1031,66 @@ <target>Aspetto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Apply" xml:space="preserve"> + <source>Apply</source> + <target>Applica</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve"> + <source>Apply to</source> + <target>Applica a</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve"> + <source>Archive</source> + <target>Archivia</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve"> + <source>Archive %lld reports?</source> + <target>Archiviare %lld segnalazioni?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve"> + <source>Archive all reports?</source> + <target>Archiviare tutte le segnalazioni?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve"> + <source>Archive and upload</source> + <target>Archivia e carica</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve"> + <source>Archive contacts to chat later.</source> + <target>Archivia contatti per chattare più tardi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve"> + <source>Archive report</source> + <target>Archivia la segnalazione</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve"> + <source>Archive report?</source> + <target>Archiviare la segnalazione?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve"> + <source>Archive reports</source> + <target>Archivia segnalazioni</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve"> + <source>Archived contacts</source> + <target>Contatti archiviati</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve"> + <source>Archiving database</source> + <target>Archiviazione del database</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Attach" xml:space="preserve"> <source>Attach</source> <target>Allega</target> @@ -875,19 +1143,29 @@ </trans-unit> <trans-unit id="Auto-accept contact requests" xml:space="preserve"> <source>Auto-accept contact requests</source> - <target>Auto-accetta richieste di contatto</target> + <target>Auto-accetta le richieste di contatto</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Auto-accept images" xml:space="preserve"> <source>Auto-accept images</source> - <target>Auto-accetta immagini</target> + <target>Auto-accetta le immagini</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve"> + <source>Auto-accept settings</source> + <target>Accetta automaticamente le impostazioni</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Back" xml:space="preserve"> <source>Back</source> <target>Indietro</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Background" xml:space="preserve"> + <source>Background</source> + <target>Sfondo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Bad desktop address" xml:space="preserve"> <source>Bad desktop address</source> <target>Indirizzo desktop errato</target> @@ -903,21 +1181,71 @@ <target>Hash del messaggio errato</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better calls" xml:space="preserve"> + <source>Better calls</source> + <target>Chiamate migliorate</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better groups" xml:space="preserve"> <source>Better groups</source> <target>Gruppi migliorati</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve"> + <source>Better groups performance</source> + <target>Prestazioni dei gruppi migliorate</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve"> + <source>Better message dates.</source> + <target>Date dei messaggi migliorate.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better messages" xml:space="preserve"> <source>Better messages</source> <target>Messaggi migliorati</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better networking" xml:space="preserve"> + <source>Better networking</source> + <target>Rete migliorata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve"> + <source>Better notifications</source> + <target>Notifiche migliorate</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve"> + <source>Better privacy and security</source> + <target>Privacy e sicurezza migliori</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve"> + <source>Better security ✅</source> + <target>Sicurezza migliorata ✅</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve"> + <source>Better user experience</source> + <target>Esperienza utente migliorata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Black" xml:space="preserve"> + <source>Black</source> + <target>Nero</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block" xml:space="preserve"> <source>Block</source> <target>Blocca</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block for all" xml:space="preserve"> + <source>Block for all</source> + <target>Blocca per tutti</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block group members" xml:space="preserve"> <source>Block group members</source> <target>Blocca i membri dei gruppi</target> @@ -928,19 +1256,39 @@ <target>Blocca membro</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve"> + <source>Block member for all?</source> + <target>Bloccare il membro per tutti?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block member?" xml:space="preserve"> <source>Block member?</source> <target>Bloccare il membro?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve"> + <source>Blocked by admin</source> + <target>Bloccato dall'amministratore</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve"> + <source>Blur for better privacy.</source> + <target>Sfoca per una privacy maggiore.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve"> + <source>Blur media</source> + <target>Sfocatura dei file multimediali</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <source>Both you and your contact can add message reactions.</source> <target>Sia tu che il tuo contatto potete aggiungere reazioni ai messaggi.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> - <source>Both you and your contact can irreversibly delete sent messages.</source> - <target>Sia tu che il tuo contatto potete eliminare irreversibilmente i messaggi inviati.</target> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target>Sia tu che il tuo contatto potete eliminare irreversibilmente i messaggi inviati. (24 ore)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> @@ -963,11 +1311,35 @@ <target>Bulgaro, finlandese, tailandese e ucraino - grazie agli utenti e a [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Business address" xml:space="preserve"> + <source>Business address</source> + <target>Indirizzo di lavoro</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve"> + <source>Business chats</source> + <target>Chat di lavoro</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve"> + <source>Businesses</source> + <target>Lavorative</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>Per profilo di chat (predefinito) o [per connessione](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve"> + <source>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</source> + <target>Usando SimpleX Chat accetti di: +- inviare solo contenuto legale nei gruppi pubblici. +- rispettare gli altri utenti - niente spam.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> <source>Call already ended!</source> <target>Chiamata già terminata!</target> @@ -978,9 +1350,24 @@ <target>Chiamate</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve"> - <source>Can't delete user profile!</source> - <target>Impossibile eliminare il profilo utente!</target> + <trans-unit id="Calls prohibited!" xml:space="preserve"> + <source>Calls prohibited!</source> + <target>Chiamate proibite!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve"> + <source>Camera not available</source> + <target>Fotocamera non disponibile</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve"> + <source>Can't call contact</source> + <target>Impossibile chiamare il contatto</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve"> + <source>Can't call member</source> + <target>Impossibile chiamare il membro</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contact!" xml:space="preserve"> @@ -993,9 +1380,20 @@ <target>Impossibile invitare i contatti!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve"> + <source>Can't message member</source> + <target>Impossibile inviare un messaggio al membro</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> <target>Annulla</target> + <note>alert action +alert button</note> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve"> + <source>Cancel migration</source> + <target>Annulla migrazione</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> @@ -1003,9 +1401,24 @@ <target>Impossibile accedere al portachiavi per salvare la password del database</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Impossibile inoltrare il messaggio</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> <target>Impossibile ricevere il file</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <target>Quota superata - il destinatario non ha ricevuto i messaggi precedentemente inviati.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve"> + <source>Cellular</source> + <target>Mobile</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change" xml:space="preserve"> @@ -1013,6 +1426,16 @@ <target>Cambia</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve"> + <source>Change automatic message deletion?</source> + <target>Cambiare l'eliminazione automatica dei messaggi?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve"> + <source>Change chat profiles</source> + <target>Modifica profili utente</target> + <note>authentication reason</note> + </trans-unit> <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> <target>Cambiare password del database?</target> @@ -1057,11 +1480,26 @@ <source>Change self-destruct passcode</source> <target>Cambia codice di autodistruzione</target> <note>authentication reason - set passcode view</note> +set passcode view</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> - <source>Chat archive</source> - <target>Archivio chat</target> + <trans-unit id="Chat" xml:space="preserve"> + <source>Chat</source> + <target>Chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve"> + <source>Chat already exists</source> + <target>La chat esiste già</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve"> + <source>Chat already exists!</source> + <target>La chat esiste già!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve"> + <source>Chat colors</source> + <target>Colori della chat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat console" xml:space="preserve"> @@ -1079,6 +1517,11 @@ <target>Database della chat eliminato</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve"> + <source>Chat database exported</source> + <target>Database della chat esportato</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> <target>Database della chat importato</target> @@ -1094,26 +1537,81 @@ <target>Chat fermata</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <target>La chat è ferma. Se hai già usato questo database su un altro dispositivo, dovresti trasferirlo prima di avviare la chat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve"> + <source>Chat list</source> + <target>Elenco delle chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve"> + <source>Chat migrated!</source> + <target>Chat migrata!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> <target>Preferenze della chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve"> + <source>Chat preferences were changed.</source> + <target>Le preferenze della chat sono state cambiate.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve"> + <source>Chat profile</source> + <target>Profilo utente</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve"> + <source>Chat theme</source> + <target>Tema della chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <target>La chat verrà eliminata per tutti i membri, non è reversibile!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <target>La chat verrà eliminata solo per te, non è reversibile!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> <target>Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve"> + <source>Check messages every 20 min.</source> + <target>Controlla i messaggi ogni 20 min.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve"> + <source>Check messages when allowed.</source> + <target>Controlla i messaggi quando consentito.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> <target>Controlla l'indirizzo del server e riprova.</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> <target>Interfaccia cinese e spagnola</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <target>Scegli _Migra da un altro dispositivo_ sul nuovo dispositivo e scansione il codice QR.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> <target>Scegli file</target> @@ -1124,10 +1622,25 @@ <target>Scegli dalla libreria</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve"> + <source>Chunks deleted</source> + <target>Blocchi eliminati</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve"> + <source>Chunks downloaded</source> + <target>Blocchi scaricati</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve"> + <source>Chunks uploaded</source> + <target>Blocchi inviati</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> <target>Svuota</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> @@ -1139,16 +1652,41 @@ <target>Svuotare la conversazione?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve"> + <source>Clear group?</source> + <target>Svuotare il gruppo?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve"> + <source>Clear or delete group?</source> + <target>Svuotare o eliminare il gruppo?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve"> + <source>Clear private notes?</source> + <target>Svuotare le note private?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear verification" xml:space="preserve"> <source>Clear verification</source> <target>Annulla la verifica</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> - <source>Colors</source> - <target>Colori</target> + <trans-unit id="Color chats with the new themes." xml:space="preserve"> + <source>Color chats with the new themes.</source> + <target>Colora le chat con i nuovi temi.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Color mode" xml:space="preserve"> + <source>Color mode</source> + <target>Modalità di colore</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve"> + <source>Community guidelines violation</source> + <target>Violazione delle linee guida della comunità</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Compare file" xml:space="preserve"> <source>Compare file</source> <target>Confronta file</target> @@ -1159,11 +1697,56 @@ <target>Confronta i codici di sicurezza con i tuoi contatti.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Completed" xml:space="preserve"> + <source>Completed</source> + <target>Completato</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve"> + <source>Conditions accepted on: %@.</source> + <target>Condizioni accettate il: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <target>Le condizioni sono state accettate per gli operatori: **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <target>Le condizioni sono già state accettate per i seguenti operatori: **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve"> + <source>Conditions of use</source> + <target>Condizioni d'uso</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <target>Le condizioni verranno accettate per gli operatori: **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve"> + <source>Conditions will be accepted on: %@.</source> + <target>Le condizioni verranno accettate il: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <target>Le condizioni verranno accettate automaticamente per gli operatori attivi il: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> <target>Configura server ICE</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Configure server operators" xml:space="preserve"> + <source>Configure server operators</source> + <target>Configura gli operatori dei server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> <target>Conferma</target> @@ -1174,14 +1757,29 @@ <target>Conferma il codice di accesso</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve"> + <source>Confirm contact deletion?</source> + <target>Confermare l'eliminazione del contatto?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm database upgrades" xml:space="preserve"> <source>Confirm database upgrades</source> <target>Conferma aggiornamenti database</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve"> + <source>Confirm files from unknown servers.</source> + <target>Conferma i file da server sconosciuti.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve"> + <source>Confirm network settings</source> + <target>Conferma le impostazioni di rete</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> - <target>Conferma password nuova…</target> + <target>Conferma nuova password…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Confirm password" xml:space="preserve"> @@ -1189,6 +1787,21 @@ <target>Conferma password</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <target>Conferma che ricordi la password del database da migrare.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve"> + <source>Confirm upload</source> + <target>Conferma caricamento</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve"> + <source>Confirmed</source> + <target>Confermato</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> <target>Connetti</target> @@ -1196,6 +1809,7 @@ </trans-unit> <trans-unit id="Connect automatically" xml:space="preserve"> <source>Connect automatically</source> + <target>Connetti automaticamente</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect incognito" xml:space="preserve"> @@ -1208,6 +1822,11 @@ <target>Connetti al desktop</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve"> + <source>Connect to your friends faster.</source> + <target>Connettiti più velocemente ai tuoi amici.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connect to yourself?" xml:space="preserve"> <source>Connect to yourself?</source> <target>Connettersi a te stesso?</target> @@ -1237,11 +1856,6 @@ Questo è il tuo link una tantum!</target> <target>Connetti via link</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve"> - <source>Connect via link / QR code</source> - <target>Connetti via link / codice QR</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connect via one-time link" xml:space="preserve"> <source>Connect via one-time link</source> <target>Connetti via link una tantum</target> @@ -1252,16 +1866,31 @@ Questo è il tuo link una tantum!</target> <target>Connettersi con %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected" xml:space="preserve"> + <source>Connected</source> + <target>Connesso</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected desktop" xml:space="preserve"> <source>Connected desktop</source> <target>Desktop connesso</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve"> + <source>Connected servers</source> + <target>Server connessi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected to desktop" xml:space="preserve"> <source>Connected to desktop</source> <target>Connesso al desktop</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting" xml:space="preserve"> + <source>Connecting</source> + <target>In connessione</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> <source>Connecting to server…</source> <target>Connessione al server…</target> @@ -1272,6 +1901,11 @@ Questo è il tuo link una tantum!</target> <target>Connessione al server… (errore: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve"> + <source>Connecting to contact, please wait or check later!</source> + <target>In collegamento con il contatto, attendi o controlla più tardi!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting to desktop" xml:space="preserve"> <source>Connecting to desktop</source> <target>Connessione al desktop</target> @@ -1282,6 +1916,16 @@ Questo è il tuo link una tantum!</target> <target>Connessione</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve"> + <source>Connection and servers status.</source> + <target>Stato della connessione e dei server.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve"> + <source>Connection blocked</source> + <target>Connessione bloccata</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> <target>Errore di connessione</target> @@ -1292,11 +1936,38 @@ Questo è il tuo link una tantum!</target> <target>Errore di connessione (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve"> + <source>Connection is blocked by server operator: +%@</source> + <target>La connessione è bloccata dall'operatore del server: +%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve"> + <source>Connection not ready.</source> + <target>Connessione non pronta.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve"> + <source>Connection notifications</source> + <target>Notifiche di connessione</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Richiesta di connessione inviata!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve"> + <source>Connection requires encryption renegotiation.</source> + <target>La connessione richiede la rinegoziazione della crittografia.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve"> + <source>Connection security</source> + <target>Sicurezza della connessione</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection terminated" xml:space="preserve"> <source>Connection terminated</source> <target>Connessione terminata</target> @@ -1307,6 +1978,16 @@ Questo è il tuo link una tantum!</target> <target>Connessione scaduta</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve"> + <source>Connection with desktop stopped</source> + <target>Connessione con il desktop fermata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve"> + <source>Connections</source> + <target>Connessioni</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact allows" xml:space="preserve"> <source>Contact allows</source> <target>Il contatto lo consente</target> @@ -1317,6 +1998,11 @@ Questo è il tuo link una tantum!</target> <target>Il contatto esiste già</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact deleted!" xml:space="preserve"> + <source>Contact deleted!</source> + <target>Contatto eliminato!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> <target>Contatto nascosto:</target> @@ -1327,9 +2013,9 @@ Questo è il tuo link una tantum!</target> <target>Il contatto è connesso</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve"> - <source>Contact is not connected yet!</source> - <target>Il contatto non è ancora connesso!</target> + <trans-unit id="Contact is deleted." xml:space="preserve"> + <source>Contact is deleted.</source> + <target>Il contatto è stato eliminato.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact name" xml:space="preserve"> @@ -1342,6 +2028,11 @@ Questo è il tuo link una tantum!</target> <target>Preferenze del contatto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve"> + <source>Contact will be deleted - this cannot be undone!</source> + <target>Il contatto verrà eliminato - non è reversibile!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> <target>Contatti</target> @@ -1352,21 +2043,41 @@ Questo è il tuo link una tantum!</target> <target>I contatti possono contrassegnare i messaggi per l'eliminazione; potrai vederli.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve"> + <source>Content violates conditions of use</source> + <target>Il contenuto viola le condizioni di utilizzo</target> + <note>blocking reason</note> + </trans-unit> <trans-unit id="Continue" xml:space="preserve"> <source>Continue</source> <target>Continua</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve"> + <source>Conversation deleted!</source> + <target>Conversazione eliminata!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> <target>Copia</target> - <note>chat item action</note> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve"> + <source>Copy error</source> + <target>Copia errore</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> <target>Versione core: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Corner" xml:space="preserve"> + <source>Corner</source> + <target>Angolo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Correct name to %@?" xml:space="preserve"> <source>Correct name to %@?</source> <target>Correggere il nome a %@?</target> @@ -1377,6 +2088,11 @@ Questo è il tuo link una tantum!</target> <target>Crea</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve"> + <source>Create 1-time link</source> + <target>Crea link una tantum</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> <target>Crea indirizzo SimpleX</target> @@ -1387,11 +2103,6 @@ Questo è il tuo link una tantum!</target> <target>Crea un gruppo usando un profilo casuale.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> - <source>Create an address to let people connect with you.</source> - <target>Crea un indirizzo per consentire alle persone di connettersi con te.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create file" xml:space="preserve"> <source>Create file</source> <target>Crea file</target> @@ -1412,16 +2123,16 @@ Questo è il tuo link una tantum!</target> <target>Crea link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create list" xml:space="preserve"> + <source>Create list</source> + <target>Crea elenco</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> <target>Crea un nuovo profilo nell'[app desktop](https://simplex.chat/downloads/). 💻</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve"> - <source>Create one-time invitation link</source> - <target>Crea link di invito una tantum</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create profile" xml:space="preserve"> <source>Create profile</source> <target>Crea profilo</target> @@ -1442,9 +2153,29 @@ Questo è il tuo link una tantum!</target> <target>Crea il tuo profilo</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve"> - <source>Created on %@</source> - <target>Creato il %@</target> + <trans-unit id="Created" xml:space="preserve"> + <source>Created</source> + <target>Creato</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve"> + <source>Created at</source> + <target>Creato il</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve"> + <source>Created at: %@</source> + <target>Creato il: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve"> + <source>Creating archive link</source> + <target>Creazione link dell'archivio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve"> + <source>Creating link…</source> + <target>Creazione link…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current Passcode" xml:space="preserve"> @@ -1452,11 +2183,21 @@ Questo è il tuo link una tantum!</target> <target>Codice di accesso attuale</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <target>Il testo delle condizioni attuali testo non è stato caricato, puoi consultare le condizioni tramite questo link:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> <target>Password attuale…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current profile" xml:space="preserve"> + <source>Current profile</source> + <target>Profilo attuale</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> <target>Attualmente la dimensione massima supportata è di %@.</target> @@ -1467,11 +2208,26 @@ Questo è il tuo link una tantum!</target> <target>Tempo personalizzato</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve"> + <source>Customizable message shape.</source> + <target>Forma dei messaggi personalizzabile.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve"> + <source>Customize theme</source> + <target>Personalizza il tema</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> <target>Scuro</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve"> + <source>Dark mode colors</source> + <target>Colori modalità scura</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> <target>ID database</target> @@ -1570,6 +2326,11 @@ Questo è il tuo link una tantum!</target> <target>Il database verrà migrato al riavvio dell'app</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Debug delivery" xml:space="preserve"> + <source>Debug delivery</source> + <target>Debug della consegna</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> <target>Decentralizzato</target> @@ -1583,18 +2344,19 @@ Questo è il tuo link una tantum!</target> <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> <target>Elimina</target> - <note>chat item action</note> + <note>alert action +swipe action</note> + </trans-unit> + <trans-unit id="Delete %lld messages of members?" xml:space="preserve"> + <source>Delete %lld messages of members?</source> + <target>Eliminare %lld messaggi dei membri?</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete %lld messages?" xml:space="preserve"> <source>Delete %lld messages?</source> <target>Eliminare %lld messaggi?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve"> - <source>Delete Contact</source> - <target>Elimina contatto</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Delete address" xml:space="preserve"> <source>Delete address</source> <target>Elimina indirizzo</target> @@ -1620,14 +2382,14 @@ Questo è il tuo link una tantum!</target> <target>Elimina e avvisa il contatto</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve"> - <source>Delete archive</source> - <target>Elimina archivio</target> + <trans-unit id="Delete chat" xml:space="preserve"> + <source>Delete chat</source> + <target>Elimina chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve"> - <source>Delete chat archive?</source> - <target>Eliminare l'archivio della chat?</target> + <trans-unit id="Delete chat messages from your device." xml:space="preserve"> + <source>Delete chat messages from your device.</source> + <target>Elimina i messaggi di chat dal tuo dispositivo.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile" xml:space="preserve"> @@ -1640,6 +2402,11 @@ Questo è il tuo link una tantum!</target> <target>Eliminare il profilo di chat?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete chat?" xml:space="preserve"> + <source>Delete chat?</source> + <target>Eliminare la chat?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> <source>Delete connection</source> <target>Elimina connessione</target> @@ -1650,11 +2417,9 @@ Questo è il tuo link una tantum!</target> <target>Elimina contatto</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact? This cannot be undone!" xml:space="preserve"> - <source>Delete contact? -This cannot be undone!</source> - <target>Eliminare il contatto? -Non è reversibile!</target> + <trans-unit id="Delete contact?" xml:space="preserve"> + <source>Delete contact?</source> + <target>Eliminare il contatto?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete database" xml:space="preserve"> @@ -1662,6 +2427,11 @@ Non è reversibile!</target> <target>Elimina database</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete database from this device" xml:space="preserve"> + <source>Delete database from this device</source> + <target>Elimina il database da questo dispositivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete file" xml:space="preserve"> <source>Delete file</source> <target>Elimina file</target> @@ -1712,6 +2482,11 @@ Non è reversibile!</target> <target>Eliminare il link?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete list?" xml:space="preserve"> + <source>Delete list?</source> + <target>Eliminare l'elenco?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> <target>Eliminare il messaggio del membro?</target> @@ -1725,7 +2500,7 @@ Non è reversibile!</target> <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> <target>Elimina messaggi</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> @@ -1742,9 +2517,9 @@ Non è reversibile!</target> <target>Eliminare il database vecchio?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve"> - <source>Delete pending connection</source> - <target>Elimina connessione in attesa</target> + <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> + <source>Delete or moderate up to 200 messages.</source> + <target>Elimina o modera fino a 200 messaggi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection?" xml:space="preserve"> @@ -1762,11 +2537,31 @@ Non è reversibile!</target> <target>Elimina coda</target> <note>server test step</note> </trans-unit> + <trans-unit id="Delete report" xml:space="preserve"> + <source>Delete report</source> + <target>Elimina la segnalazione</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete up to 20 messages at once." xml:space="preserve"> + <source>Delete up to 20 messages at once.</source> + <target>Elimina fino a 20 messaggi contemporaneamente.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> <target>Eliminare il profilo utente?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete without notification" xml:space="preserve"> + <source>Delete without notification</source> + <target>Elimina senza avvisare</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve"> + <source>Deleted</source> + <target>Eliminato</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Deleted at" xml:space="preserve"> <source>Deleted at</source> <target>Eliminato il</target> @@ -1777,6 +2572,16 @@ Non è reversibile!</target> <target>Eliminato il: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve"> + <source>Deletion errors</source> + <target>Errori di eliminazione</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivered even when Apple drops them." xml:space="preserve"> + <source>Delivered even when Apple drops them.</source> + <target>Consegnati anche quando Apple li scarta.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery" xml:space="preserve"> <source>Delivery</source> <target>Consegna</target> @@ -1812,11 +2617,41 @@ Non è reversibile!</target> <target>Dispositivi desktop</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve"> + <source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source> + <target>L'indirizzo del server di destinazione di %@ è incompatibile con le impostazioni del server di inoltro %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server error: %@" xml:space="preserve"> + <source>Destination server error: %@</source> + <target>Errore del server di destinazione: %@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve"> + <source>Destination server version of %@ is incompatible with forwarding server %@.</source> + <target>La versione del server di destinazione di %@ è incompatibile con il server di inoltro %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Detailed statistics" xml:space="preserve"> + <source>Detailed statistics</source> + <target>Statistiche dettagliate</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Details" xml:space="preserve"> + <source>Details</source> + <target>Dettagli</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Develop" xml:space="preserve"> <source>Develop</source> <target>Sviluppa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Developer options" xml:space="preserve"> + <source>Developer options</source> + <target>Opzioni sviluppatore</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> <target>Strumenti di sviluppo</target> @@ -1847,8 +2682,13 @@ Non è reversibile!</target> <target>Messaggi diretti</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve"> + <source>Direct messages between members are prohibited in this chat.</source> + <target>I messaggi diretti tra i membri sono vietati in questa chat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> <target>I messaggi diretti tra i membri sono vietati in questo gruppo.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1862,11 +2702,26 @@ Non è reversibile!</target> <target>Disattiva SimpleX Lock</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Disable automatic message deletion?" xml:space="preserve"> + <source>Disable automatic message deletion?</source> + <target>Disattivare l'eliminazione automatica dei messaggi?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Disable delete messages" xml:space="preserve"> + <source>Disable delete messages</source> + <target>Disattiva eliminazione messaggi</target> + <note>alert button</note> + </trans-unit> <trans-unit id="Disable for all" xml:space="preserve"> <source>Disable for all</source> <target>Disattiva per tutti</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Disabled" xml:space="preserve"> + <source>Disabled</source> + <target>Disattivato</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> <source>Disappearing message</source> <target>Messaggio a tempo</target> @@ -1882,8 +2737,8 @@ Non è reversibile!</target> <target>I messaggi a tempo sono vietati in questa chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <target>I messaggi a tempo sono vietati in questo gruppo.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1914,6 +2769,12 @@ Non è reversibile!</target> </trans-unit> <trans-unit id="Discover via local network" xml:space="preserve"> <source>Discover via local network</source> + <target>Individua via rete locale</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve"> + <source>Do NOT send messages directly, even if your or destination server does not support private routing.</source> + <target>NON inviare messaggi direttamente, anche se il tuo server o quello di destinazione non supporta l'instradamento privato.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> @@ -1921,11 +2782,31 @@ Non è reversibile!</target> <target>NON usare SimpleX per chiamate di emergenza.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT use private routing." xml:space="preserve"> + <source>Do NOT use private routing.</source> + <target>NON usare l'instradamento privato.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do it later" xml:space="preserve"> <source>Do it later</source> <target>Fallo dopo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do not send history to new members." xml:space="preserve"> + <source>Do not send history to new members.</source> + <target>Non inviare la cronologia ai nuovi membri.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not use credentials with proxy." xml:space="preserve"> + <source>Do not use credentials with proxy.</source> + <target>Non usare credenziali con proxy.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Documents:" xml:space="preserve"> + <source>Documents:</source> + <target>Documenti:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> <target>Non creare un indirizzo</target> @@ -1936,21 +2817,72 @@ Non è reversibile!</target> <target>Non attivare</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Don't miss important messages." xml:space="preserve"> + <source>Don't miss important messages.</source> + <target>Non perdere messaggi importanti.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> <target>Non mostrare più</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Done" xml:space="preserve"> + <source>Done</source> + <target>Fatto</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Downgrade and open chat" xml:space="preserve"> <source>Downgrade and open chat</source> <target>Esegui downgrade e apri chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Download" xml:space="preserve"> + <source>Download</source> + <target>Scarica</target> + <note>alert button +chat item action</note> + </trans-unit> + <trans-unit id="Download errors" xml:space="preserve"> + <source>Download errors</source> + <target>Errori di scaricamento</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download failed" xml:space="preserve"> + <source>Download failed</source> + <target>Scaricamento fallito</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Download file" xml:space="preserve"> <source>Download file</source> <target>Scarica file</target> <note>server test step</note> </trans-unit> + <trans-unit id="Download files" xml:space="preserve"> + <source>Download files</source> + <target>Scarica i file</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Downloaded" xml:space="preserve"> + <source>Downloaded</source> + <target>Scaricato</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloaded files" xml:space="preserve"> + <source>Downloaded files</source> + <target>File scaricati</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading archive" xml:space="preserve"> + <source>Downloading archive</source> + <target>Scaricamento archivio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading link details" xml:space="preserve"> + <source>Downloading link details</source> + <target>Scaricamento dettagli del link</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> <target>Nome da mostrare doppio!</target> @@ -1961,6 +2893,11 @@ Non è reversibile!</target> <target>Durata</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="E2E encrypted notifications." xml:space="preserve"> + <source>E2E encrypted notifications.</source> + <target>Notifiche crittografate E2E.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> <target>Modifica</target> @@ -1981,6 +2918,11 @@ Non è reversibile!</target> <target>Attiva (mantieni sostituzioni)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve"> + <source>Enable Flux in Network & servers settings for better metadata privacy.</source> + <target>Attiva Flux nelle impostazioni "Rete e server" per una migliore privacy dei metadati.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> <target>Attiva SimpleX Lock</target> @@ -1994,6 +2936,11 @@ Non è reversibile!</target> <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> <target>Attivare l'eliminazione automatica dei messaggi?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Enable camera access" xml:space="preserve"> + <source>Enable camera access</source> + <target>Attiva l'accesso alla fotocamera</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable for all" xml:space="preserve"> @@ -2001,6 +2948,11 @@ Non è reversibile!</target> <target>Attiva per tutti</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve"> + <source>Enable in direct chats (BETA)!</source> + <target>Attivala nelle chat dirette (BETA)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> <target>Attivare le notifiche istantanee?</target> @@ -2031,6 +2983,16 @@ Non è reversibile!</target> <target>Attiva il codice di autodistruzione</target> <note>set passcode view</note> </trans-unit> + <trans-unit id="Enabled" xml:space="preserve"> + <source>Enabled</source> + <target>Attivato</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve"> + <source>Enabled for</source> + <target>Attivo per</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> <target>Crittografare</target> @@ -2061,6 +3023,11 @@ Non è reversibile!</target> <target>Messaggio crittografato o altro evento</target> <note>notification</note> </trans-unit> + <trans-unit id="Encrypted message: app is stopped" xml:space="preserve"> + <source>Encrypted message: app is stopped</source> + <target>Messaggio cifrato: l'app è ferma</target> + <note>notification</note> + </trans-unit> <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> <target>Messaggio crittografato: errore del database</target> @@ -2096,6 +3063,11 @@ Non è reversibile!</target> <target>Rinegoziazione crittografia fallita.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encryption renegotiation in progress." xml:space="preserve"> + <source>Encryption renegotiation in progress.</source> + <target>Rinegoziazione della crittografia in corso.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter Passcode" xml:space="preserve"> <source>Enter Passcode</source> <target>Inserisci il codice di accesso</target> @@ -2111,6 +3083,11 @@ Non è reversibile!</target> <target>Inserisci il nome del gruppo…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve"> + <source>Enter passphrase</source> + <target>Inserisci password</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter passphrase…" xml:space="preserve"> <source>Enter passphrase…</source> <target>Inserisci la password…</target> @@ -2156,26 +3133,36 @@ Non è reversibile!</target> <target>Errore nell'interruzione del cambio di indirizzo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error accepting conditions" xml:space="preserve"> + <source>Error accepting conditions</source> + <target>Errore di accettazione delle condizioni</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> <target>Errore nell'accettazione della richiesta di contatto</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve"> - <source>Error accessing database file</source> - <target>Errore nell'accesso al file del database</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> <target>Errore di aggiunta membro/i</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error adding server" xml:space="preserve"> + <source>Error adding server</source> + <target>Errore di aggiunta del server</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> <target>Errore nella modifica dell'indirizzo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing connection profile" xml:space="preserve"> + <source>Error changing connection profile</source> + <target>Errore nel cambio di profilo di connessione</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error changing role" xml:space="preserve"> <source>Error changing role</source> <target>Errore nel cambio di ruolo</target> @@ -2186,6 +3173,21 @@ Non è reversibile!</target> <target>Errore nella modifica dell'impostazione</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing to incognito!" xml:space="preserve"> + <source>Error changing to incognito!</source> + <target>Errore nel passaggio a incognito!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error checking token status" xml:space="preserve"> + <source>Error checking token status</source> + <target>Errore di controllo dello stato del token</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve"> + <source>Error connecting to forwarding server %@. Please try later.</source> + <target>Errore di connessione al server di inoltro %@. Riprova più tardi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> <target>Errore nella creazione dell'indirizzo</target> @@ -2201,16 +3203,31 @@ Non è reversibile!</target> <target>Errore nella creazione del link del gruppo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating list" xml:space="preserve"> + <source>Error creating list</source> + <target>Errore nella creazione dell'elenco</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error creating member contact" xml:space="preserve"> <source>Error creating member contact</source> <target>Errore di creazione del contatto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating message" xml:space="preserve"> + <source>Error creating message</source> + <target>Errore di creazione del messaggio</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>Errore nella creazione del profilo!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating report" xml:space="preserve"> + <source>Error creating report</source> + <target>Errore nella creazione del resoconto</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error decrypting file" xml:space="preserve"> <source>Error decrypting file</source> <target>Errore decifrando il file</target> @@ -2231,11 +3248,6 @@ Non è reversibile!</target> <target>Errore nell'eliminazione della connessione</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve"> - <source>Error deleting contact</source> - <target>Errore nell'eliminazione del contatto</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> <target>Errore nell'eliminazione del database</target> @@ -2256,6 +3268,11 @@ Non è reversibile!</target> <target>Errore nell'eliminazione del profilo utente</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error downloading the archive" xml:space="preserve"> + <source>Error downloading the archive</source> + <target>Errore di scaricamento dell'archivio</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> <source>Error enabling delivery receipts!</source> <target>Errore nell'attivazione delle ricevute di consegna!</target> @@ -2276,6 +3293,11 @@ Non è reversibile!</target> <target>Errore nell'esportazione del database della chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error exporting theme: %@" xml:space="preserve"> + <source>Error exporting theme: %@</source> + <target>Errore di esportazione del tema: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> <target>Errore nell'importazione del database della chat</target> @@ -2286,24 +3308,54 @@ Non è reversibile!</target> <target>Errore di ingresso nel gruppo</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve"> - <source>Error loading %@ servers</source> - <target>Errore nel caricamento dei server %@</target> + <trans-unit id="Error loading servers" xml:space="preserve"> + <source>Error loading servers</source> + <target>Errore nel caricamento dei server</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error migrating settings" xml:space="preserve"> + <source>Error migrating settings</source> + <target>Errore nella migrazione delle impostazioni</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error opening chat" xml:space="preserve"> + <source>Error opening chat</source> + <target>Errore di apertura della chat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> <target>Errore nella ricezione del file</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error reconnecting server" xml:space="preserve"> + <source>Error reconnecting server</source> + <target>Errore di riconnessione al server</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error reconnecting servers" xml:space="preserve"> + <source>Error reconnecting servers</source> + <target>Errore di riconnessione ai server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error registering for notifications" xml:space="preserve"> + <source>Error registering for notifications</source> + <target>Errore di registrazione per le notifiche</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> <target>Errore nella rimozione del membro</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve"> - <source>Error saving %@ servers</source> - <target>Errore nel salvataggio dei server %@</target> + <trans-unit id="Error reordering lists" xml:space="preserve"> + <source>Error reordering lists</source> + <target>Errore riordinando gli elenchi</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error resetting statistics" xml:space="preserve"> + <source>Error resetting statistics</source> + <target>Errore di azzeramento statistiche</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving ICE servers" xml:space="preserve"> @@ -2311,6 +3363,11 @@ Non è reversibile!</target> <target>Errore nel salvataggio dei server ICE</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving chat list" xml:space="preserve"> + <source>Error saving chat list</source> + <target>Errore nel salvataggio dell'elenco di chat</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> <target>Errore nel salvataggio del profilo del gruppo</target> @@ -2326,11 +3383,26 @@ Non è reversibile!</target> <target>Errore nel salvataggio della password nel portachiavi</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving servers" xml:space="preserve"> + <source>Error saving servers</source> + <target>Errore di salvataggio dei server</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error saving settings" xml:space="preserve"> + <source>Error saving settings</source> + <target>Errore di salvataggio delle impostazioni</target> + <note>when migrating</note> + </trans-unit> <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> <target>Errore nel salvataggio della password utente</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error scanning code: %@" xml:space="preserve"> + <source>Error scanning code: %@</source> + <target>Errore di scansione del codice: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending email" xml:space="preserve"> <source>Error sending email</source> <target>Errore nell'invio dell'email</target> @@ -2361,16 +3433,26 @@ Non è reversibile!</target> <target>Errore nell'interruzione della chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error switching profile" xml:space="preserve"> + <source>Error switching profile</source> + <target>Errore nel cambio di profilo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> <target>Errore nel cambio di profilo!</target> - <note>No comment provided by engineer.</note> + <note>alertTitle</note> </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> <target>Errore nella sincronizzazione della connessione</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error testing server connection" xml:space="preserve"> + <source>Error testing server connection</source> + <target>Errore provando la connessione al server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> <target>Errore nell'aggiornamento del link del gruppo</target> @@ -2381,6 +3463,11 @@ Non è reversibile!</target> <target>Errore nell'aggiornamento del messaggio</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error updating server" xml:space="preserve"> + <source>Error updating server</source> + <target>Errore di aggiornamento del server</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> <target>Errore nell'aggiornamento delle impostazioni</target> @@ -2391,6 +3478,16 @@ Non è reversibile!</target> <target>Errore nell'aggiornamento della privacy dell'utente</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error uploading the archive" xml:space="preserve"> + <source>Error uploading the archive</source> + <target>Errore di invio dell'archivio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error verifying passphrase:" xml:space="preserve"> + <source>Error verifying passphrase:</source> + <target>Errore di verifica della password:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error: " xml:space="preserve"> <source>Error: </source> <target>Errore: </target> @@ -2399,7 +3496,9 @@ Non è reversibile!</target> <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> <target>Errore: %@</target> - <note>No comment provided by engineer.</note> + <note>alert message +file error text +snd error text</note> </trans-unit> <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> @@ -2411,6 +3510,16 @@ Non è reversibile!</target> <target>Errore: nessun file di database</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Errors" xml:space="preserve"> + <source>Errors</source> + <target>Errori</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors in servers configuration." xml:space="preserve"> + <source>Errors in servers configuration.</source> + <target>Errori nella configurazione dei server.</target> + <note>servers error</note> + </trans-unit> <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> <source>Even when disabled in the conversation.</source> <target>Anche quando disattivato nella conversazione.</target> @@ -2426,6 +3535,11 @@ Non è reversibile!</target> <target>Espandi</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Expired" xml:space="preserve"> + <source>Expired</source> + <target>Scaduto</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> <target>Esporta database</target> @@ -2436,11 +3550,21 @@ Non è reversibile!</target> <target>Errore di esportazione:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Export theme" xml:space="preserve"> + <source>Export theme</source> + <target>Esporta tema</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> <target>Archivio database esportato.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Exported file doesn't exist" xml:space="preserve"> + <source>Exported file doesn't exist</source> + <target>Il file esportato non esiste</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exporting database archive…" xml:space="preserve"> <source>Exporting database archive…</source> <target>Esportazione archivio database…</target> @@ -2456,16 +3580,70 @@ Non è reversibile!</target> <target>Veloce e senza aspettare che il mittente sia in linea!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster deletion of groups." xml:space="preserve"> + <source>Faster deletion of groups.</source> + <target>Eliminazione dei gruppi più veloce.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Faster joining and more reliable messages." xml:space="preserve"> <source>Faster joining and more reliable messages.</source> <target>Ingresso più veloce e messaggi più affidabili.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster sending messages." xml:space="preserve"> + <source>Faster sending messages.</source> + <target>Invio dei messaggi più veloce.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Favorite" xml:space="preserve"> <source>Favorite</source> <target>Preferito</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Favorites" xml:space="preserve"> + <source>Favorites</source> + <target>Preferite</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Errore del file</target> + <note>file error alert title</note> + </trans-unit> + <trans-unit id="File errors: %@" xml:space="preserve"> + <source>File errors: +%@</source> + <target>Errori di file: +%@</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="File is blocked by server operator: %@." xml:space="preserve"> + <source>File is blocked by server operator: +%@.</source> + <target>Il file è bloccato dall'operatore del server: +%@.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve"> + <source>File not found - most likely file was deleted or cancelled.</source> + <target>File non trovato - probabilmente è stato eliminato o annullato.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File server error: %@" xml:space="preserve"> + <source>File server error: %@</source> + <target>Errore del server dei file: %@</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File status" xml:space="preserve"> + <source>File status</source> + <target>Stato del file</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File status: %@" xml:space="preserve"> + <source>File status: %@</source> + <target>Stato del file: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="File will be deleted from servers." xml:space="preserve"> <source>File will be deleted from servers.</source> <target>Il file verrà eliminato dai server.</target> @@ -2486,6 +3664,11 @@ Non è reversibile!</target> <target>File: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files" xml:space="preserve"> + <source>Files</source> + <target>File</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> <target>File e multimediali</target> @@ -2496,11 +3679,16 @@ Non è reversibile!</target> <target>File e multimediali</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> - <source>Files and media are prohibited in this group.</source> + <trans-unit id="Files and media are prohibited." xml:space="preserve"> + <source>Files and media are prohibited.</source> <target>File e contenuti multimediali sono vietati in questo gruppo.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files and media not allowed" xml:space="preserve"> + <source>Files and media not allowed</source> + <target>File e multimediali non consentiti</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files and media prohibited!" xml:space="preserve"> <source>Files and media prohibited!</source> <target>File e contenuti multimediali vietati!</target> @@ -2511,6 +3699,16 @@ Non è reversibile!</target> <target>Filtra le chat non lette e preferite.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Finalize migration" xml:space="preserve"> + <source>Finalize migration</source> + <target>Finalizza la migrazione</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration on another device." xml:space="preserve"> + <source>Finalize migration on another device.</source> + <target>Finalizza la migrazione su un altro dispositivo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> <target>Finalmente le abbiamo! 🚀</target> @@ -2551,13 +3749,118 @@ Non è reversibile!</target> <target>Correzione non supportata dal membro del gruppo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For all moderators" xml:space="preserve"> + <source>For all moderators</source> + <target>Per tutti i moderatori</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For chat profile %@:" xml:space="preserve"> + <source>For chat profile %@:</source> + <target>Per il profilo di chat %@:</target> + <note>servers error</note> + </trans-unit> <trans-unit id="For console" xml:space="preserve"> <source>For console</source> <target>Per console</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." xml:space="preserve"> + <source>For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</source> + <target>Ad esempio, se il tuo contatto riceve messaggi tramite un server di SimpleX Chat, la tua app li consegnerà tramite un server Flux.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For me" xml:space="preserve"> + <source>For me</source> + <target>Per me</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For private routing" xml:space="preserve"> + <source>For private routing</source> + <target>Per l'instradamento privato</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For social media" xml:space="preserve"> + <source>For social media</source> + <target>Per i social media</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward" xml:space="preserve"> + <source>Forward</source> + <target>Inoltra</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Forward %d message(s)?" xml:space="preserve"> + <source>Forward %d message(s)?</source> + <target>Inoltrare %d messaggio/i?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Forward and save messages" xml:space="preserve"> + <source>Forward and save messages</source> + <target>Inoltra e salva i messaggi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward messages" xml:space="preserve"> + <source>Forward messages</source> + <target>Inoltra i messaggi</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Forward messages without files?" xml:space="preserve"> + <source>Forward messages without files?</source> + <target>Inoltrare i messaggi senza file?</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> + <source>Forward up to 20 messages at once.</source> + <target>Inoltra fino a 20 messaggi alla volta.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded" xml:space="preserve"> + <source>Forwarded</source> + <target>Inoltrato</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded from" xml:space="preserve"> + <source>Forwarded from</source> + <target>Inoltrato da</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding %lld messages" xml:space="preserve"> + <source>Forwarding %lld messages</source> + <target>Inoltro di %lld messaggi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve"> + <source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source> + <target>Il server di inoltro %@ non è riuscito a connettersi al server di destinazione %@. Riprova più tardi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server address is incompatible with network settings: %@.</source> + <target>L'indirizzo del server di inoltro è incompatibile con le impostazioni di rete: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server version is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server version is incompatible with network settings: %@.</source> + <target>La versione del server di inoltro è incompatibile con le impostazioni di rete: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Destination server error: %2$@</source> + <target>Server di inoltro: %1$@ +Errore del server di destinazione: %2$@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Error: %2$@</source> + <target>Server di inoltro: %1$@ +Errore: %2$@</target> + <note>snd error text</note> + </trans-unit> <trans-unit id="Found desktop" xml:space="preserve"> <source>Found desktop</source> + <target>Desktop trovato</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="French interface" xml:space="preserve"> @@ -2575,11 +3878,6 @@ Non è reversibile!</target> <target>Nome completo (facoltativo)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full name:" xml:space="preserve"> - <source>Full name:</source> - <target>Nome completo:</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Fully decentralized – visible only to members." xml:space="preserve"> <source>Fully decentralized – visible only to members.</source> <target>Completamente decentralizzato: visibile solo ai membri.</target> @@ -2600,6 +3898,21 @@ Non è reversibile!</target> <target>GIF e adesivi</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Get notified when mentioned." xml:space="preserve"> + <source>Get notified when mentioned.</source> + <target>Ricevi una notifica quando menzionato.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Good afternoon!" xml:space="preserve"> + <source>Good afternoon!</source> + <target>Buon pomeriggio!</target> + <note>message preview</note> + </trans-unit> + <trans-unit id="Good morning!" xml:space="preserve"> + <source>Good morning!</source> + <target>Buongiorno!</target> + <note>message preview</note> + </trans-unit> <trans-unit id="Group" xml:space="preserve"> <source>Group</source> <target>Gruppo</target> @@ -2655,36 +3968,6 @@ Non è reversibile!</target> <target>Link del gruppo</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve"> - <source>Group members can add message reactions.</source> - <target>I membri del gruppo possono aggiungere reazioni ai messaggi.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> - <source>Group members can irreversibly delete sent messages.</source> - <target>I membri del gruppo possono eliminare irreversibilmente i messaggi inviati.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> - <target>I membri del gruppo possono inviare messaggi diretti.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> - <target>I membri del gruppo possono inviare messaggi a tempo.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send files and media." xml:space="preserve"> - <source>Group members can send files and media.</source> - <target>I membri del gruppo possono inviare file e contenuti multimediali.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> - <target>I membri del gruppo possono inviare messaggi vocali.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> <target>Messaggio del gruppo:</target> @@ -2725,11 +4008,21 @@ Non è reversibile!</target> <target>Il gruppo verrà eliminato per te. Non è reversibile!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Groups" xml:space="preserve"> + <source>Groups</source> + <target>Gruppi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Help" xml:space="preserve"> <source>Help</source> <target>Aiuto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Help admins moderating their groups." xml:space="preserve"> + <source>Help admins moderating their groups.</source> + <target>Aiuta gli amministratori a moderare i loro gruppi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> <target>Nascosta</target> @@ -2770,15 +4063,30 @@ Non è reversibile!</target> <target>Cronologia</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="History is not sent to new members." xml:space="preserve"> + <source>History is not sent to new members.</source> + <target>La cronologia non viene inviata ai nuovi membri.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> <target>Come funziona SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="How it affects privacy" xml:space="preserve"> + <source>How it affects privacy</source> + <target>Come influisce sulla privacy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it helps privacy" xml:space="preserve"> + <source>How it helps privacy</source> + <target>Come aiuta la privacy</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> <target>Come funziona</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="How to" xml:space="preserve"> <source>How to</source> @@ -2795,21 +4103,26 @@ Non è reversibile!</target> <target>Come usare i tuoi server</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Hungarian interface" xml:space="preserve"> + <source>Hungarian interface</source> + <target>Interfaccia in ungherese</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> <target>Server ICE (uno per riga)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="IP address" xml:space="preserve"> + <source>IP address</source> + <target>Indirizzo IP</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> <source>If you can't meet in person, show QR code in a video call, or share the link.</source> <target>Se non potete incontrarvi di persona, mostra il codice QR in una videochiamata o condividi il link.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <target>Se non potete incontrarvi di persona, puoi **scansionare il codice QR durante la videochiamata** oppure il tuo contatto può condividere un link di invito.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> <target>Se inserisci questo codice all'apertura dell'app, tutti i dati di essa verranno rimossi in modo irreversibile!</target> @@ -2845,8 +4158,8 @@ Non è reversibile!</target> <target>Immediatamente</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <target>Immune a spam e abusi</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2865,6 +4178,33 @@ Non è reversibile!</target> <target>Importa database</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Import failed" xml:space="preserve"> + <source>Import failed</source> + <target>Importazione fallita</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import theme" xml:space="preserve"> + <source>Import theme</source> + <target>Importa tema</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Importing archive" xml:space="preserve"> + <source>Importing archive</source> + <target>Importazione archivio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved delivery, reduced traffic usage. More improvements are coming soon!" xml:space="preserve"> + <source>Improved delivery, reduced traffic usage. +More improvements are coming soon!</source> + <target>Consegna migliorata, utilizzo di traffico ridotto. +Altri miglioramenti sono in arrivo!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved message delivery" xml:space="preserve"> + <source>Improved message delivery</source> + <target>Consegna dei messaggi migliorata</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> <target>Privacy e sicurezza migliorate</target> @@ -2875,11 +4215,31 @@ Non è reversibile!</target> <target>Configurazione del server migliorata</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In order to continue, chat should be stopped." xml:space="preserve"> + <source>In order to continue, chat should be stopped.</source> + <target>Per continuare, la chat deve essere fermata.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>In risposta a</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In-call sounds" xml:space="preserve"> + <source>In-call sounds</source> + <target>Suoni nelle chiamate</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Inappropriate content" xml:space="preserve"> + <source>Inappropriate content</source> + <target>Contenuto inappropriato</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Inappropriate profile" xml:space="preserve"> + <source>Inappropriate profile</source> + <target>Profilo inappropriato</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> <target>Incognito</target> @@ -2950,6 +4310,11 @@ Non è reversibile!</target> <target>Installa [Simplex Chat per terminale](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Instant" xml:space="preserve"> + <source>Instant</source> + <target>Istantaneamente</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> @@ -2957,30 +4322,80 @@ Non è reversibile!</target> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Instantly" xml:space="preserve"> - <source>Instantly</source> - <target>Istantaneamente</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> <target>Interfaccia</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Interface colors" xml:space="preserve"> + <source>Interface colors</source> + <target>Colori dell'interfaccia</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid" xml:space="preserve"> + <source>Invalid</source> + <target>Non valido</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (bad token)" xml:space="preserve"> + <source>Invalid (bad token)</source> + <target>Non valido (token corrotto)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (expired)" xml:space="preserve"> + <source>Invalid (expired)</source> + <target>Non valido (scaduto)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (unregistered)" xml:space="preserve"> + <source>Invalid (unregistered)</source> + <target>Non valido (non registrato)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (wrong topic)" xml:space="preserve"> + <source>Invalid (wrong topic)</source> + <target>Non valido (argomento sbagliato)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid QR code" xml:space="preserve"> + <source>Invalid QR code</source> + <target>Codice QR non valido</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> <target>Link di connessione non valido</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid display name!" xml:space="preserve"> + <source>Invalid display name!</source> + <target>Nome da mostrare non valido!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid link" xml:space="preserve"> + <source>Invalid link</source> + <target>Link non valido</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Conferma di migrazione non valida</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid name!" xml:space="preserve"> <source>Invalid name!</source> <target>Nome non valido!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid response" xml:space="preserve"> + <source>Invalid response</source> + <target>Risposta non valida</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> <target>Indirizzo del server non valido!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Invalid status" xml:space="preserve"> <source>Invalid status</source> @@ -3002,6 +4417,11 @@ Non è reversibile!</target> <target>Invita membri</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invite to chat" xml:space="preserve"> + <source>Invite to chat</source> + <target>Invita in chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invite to group" xml:space="preserve"> <source>Invite to group</source> <target>Invita al gruppo</target> @@ -3017,8 +4437,8 @@ Non è reversibile!</target> <target>L'eliminazione irreversibile dei messaggi è vietata in questa chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <target>L'eliminazione irreversibile dei messaggi è vietata in questo gruppo.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3043,6 +4463,11 @@ Non è reversibile!</target> 3. La connessione è stata compromessa.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="It protects your IP address and connections." xml:space="preserve"> + <source>It protects your IP address and connections.</source> + <target>Protegge il tuo indirizzo IP e le connessioni.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> <target>Sembra che tu sia già connesso tramite questo link. In caso contrario, c'è stato un errore (%@).</target> @@ -3061,13 +4486,18 @@ Non è reversibile!</target> <trans-unit id="Join" xml:space="preserve"> <source>Join</source> <target>Entra</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> <target>Entra nel gruppo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Join group conversations" xml:space="preserve"> + <source>Join group conversations</source> + <target>Entra in conversazioni di gruppo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Join group?" xml:space="preserve"> <source>Join group?</source> <target>Entrare nel gruppo?</target> @@ -3095,11 +4525,26 @@ Questo è il tuo link per il gruppo %@!</target> <target>Ingresso nel gruppo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep" xml:space="preserve"> + <source>Keep</source> + <target>Tieni</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Keep conversation" xml:space="preserve"> + <source>Keep conversation</source> + <target>Tieni la conversazione</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Keep the app open to use it from desktop" xml:space="preserve"> <source>Keep the app open to use it from desktop</source> <target>Tieni aperta l'app per usarla dal desktop</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep unused invitation?" xml:space="preserve"> + <source>Keep unused invitation?</source> + <target>Tenere l'invito inutilizzato?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> <source>Keep your connections</source> <target>Mantieni le tue connessioni</target> @@ -3133,6 +4578,16 @@ Questo è il tuo link per il gruppo %@!</target> <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> <target>Esci</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Leave chat" xml:space="preserve"> + <source>Leave chat</source> + <target>Esci dalla chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave chat?" xml:space="preserve"> + <source>Leave chat?</source> + <target>Uscire dalla chat?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group" xml:space="preserve"> @@ -3175,6 +4630,21 @@ Questo è il tuo link per il gruppo %@!</target> <target>Desktop collegati</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="List" xml:space="preserve"> + <source>List</source> + <target>Elenco</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="List name and emoji should be different for all lists." xml:space="preserve"> + <source>List name and emoji should be different for all lists.</source> + <target>Il nome dell'elenco e l'emoji dovrebbero essere diversi per tutte le liste.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List name..." xml:space="preserve"> + <source>List name...</source> + <target>Nome elenco...</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Live message!" xml:space="preserve"> <source>Live message!</source> <target>Messaggio in diretta!</target> @@ -3205,11 +4675,6 @@ Questo è il tuo link per il gruppo %@!</target> <target>Modalità di blocco</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> - <source>Make a private connection</source> - <target>Crea una connessione privata</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make one message disappear" xml:space="preserve"> <source>Make one message disappear</source> <target>Fai sparire un messaggio</target> @@ -3220,21 +4685,11 @@ Questo è il tuo link per il gruppo %@!</target> <target>Rendi privato il profilo!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> - <target>Assicurati che gli indirizzi dei server %@ siano nel formato corretto, uno per riga e non doppi (%@).</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> <target>Assicurati che gli indirizzi dei server WebRTC ICE siano nel formato corretto, uno per riga e non doppi.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <target>Molte persone hanno chiesto: *se SimpleX non ha identificatori utente, come può recapitare i messaggi?*</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> <source>Mark deleted for everyone</source> <target>Contrassegna eliminato per tutti</target> @@ -3260,11 +4715,36 @@ Questo è il tuo link per il gruppo %@!</target> <target>Max 30 secondi, ricevuto istantaneamente.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Media & file servers" xml:space="preserve"> + <source>Media & file servers</source> + <target>Server di multimediali e file</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Medium" xml:space="preserve"> + <source>Medium</source> + <target>Media</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Member" xml:space="preserve"> <source>Member</source> <target>Membro</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member inactive" xml:space="preserve"> + <source>Member inactive</source> + <target>Membro inattivo</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Member reports" xml:space="preserve"> + <source>Member reports</source> + <target>Segnalazioni dei membri</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All chat members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All chat members will be notified.</source> + <target>Il ruolo del membro verrà cambiato in "%@". Verranno notificati tutti i membri della chat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> <target>Il ruolo del membro verrà cambiato in "%@". Tutti i membri del gruppo verranno avvisati.</target> @@ -3275,11 +4755,66 @@ Questo è il tuo link per il gruppo %@!</target> <target>Il ruolo del membro verrà cambiato in "%@". Il membro riceverà un invito nuovo.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from chat - this cannot be undone!</source> + <target>Il membro verrà rimosso dalla chat, non è reversibile!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> <target>Il membro verrà rimosso dal gruppo, non è reversibile!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Members can add message reactions." xml:space="preserve"> + <source>Members can add message reactions.</source> + <target>I membri del gruppo possono aggiungere reazioni ai messaggi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Members can irreversibly delete sent messages. (24 hours)</source> + <target>I membri del gruppo possono eliminare irreversibilmente i messaggi inviati. (24 ore)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can report messsages to moderators." xml:space="preserve"> + <source>Members can report messsages to moderators.</source> + <target>I membri possono segnalare messaggi ai moderatori.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send SimpleX links." xml:space="preserve"> + <source>Members can send SimpleX links.</source> + <target>I membri del gruppo possono inviare link di Simplex.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> + <target>I membri del gruppo possono inviare messaggi diretti.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> + <target>I membri del gruppo possono inviare messaggi a tempo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send files and media." xml:space="preserve"> + <source>Members can send files and media.</source> + <target>I membri del gruppo possono inviare file e contenuti multimediali.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> + <target>I membri del gruppo possono inviare messaggi vocali.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mention members 👋" xml:space="preserve"> + <source>Mention members 👋</source> + <target>Menziona i membri 👋</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Menus" xml:space="preserve"> + <source>Menus</source> + <target>Menu</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>Errore di recapito del messaggio</target> @@ -3290,9 +4825,29 @@ Questo è il tuo link per il gruppo %@!</target> <target>Ricevute di consegna dei messaggi!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message delivery warning" xml:space="preserve"> + <source>Message delivery warning</source> + <target>Avviso di consegna del messaggio</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> - <target>Bozza dei messaggi</target> + <target>Bozza del messaggio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message forwarded" xml:space="preserve"> + <source>Message forwarded</source> + <target>Messaggio inoltrato</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message may be delivered later if member becomes active." xml:space="preserve"> + <source>Message may be delivered later if member becomes active.</source> + <target>Il messaggio può essere consegnato più tardi se il membro diventa attivo.</target> + <note>item status description</note> + </trans-unit> + <trans-unit id="Message queue info" xml:space="preserve"> + <source>Message queue info</source> + <target>Info coda messaggi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Message reactions" xml:space="preserve"> @@ -3305,16 +4860,51 @@ Questo è il tuo link per il gruppo %@!</target> <target>Le reazioni ai messaggi sono vietate in questa chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> - <source>Message reactions are prohibited in this group.</source> + <trans-unit id="Message reactions are prohibited." xml:space="preserve"> + <source>Message reactions are prohibited.</source> <target>Le reazioni ai messaggi sono vietate in questo gruppo.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message reception" xml:space="preserve"> + <source>Message reception</source> + <target>Ricezione messaggi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message servers" xml:space="preserve"> + <source>Message servers</source> + <target>Server dei messaggi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message shape" xml:space="preserve"> + <source>Message shape</source> + <target>Forma del messaggio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message source remains private." xml:space="preserve"> + <source>Message source remains private.</source> + <target>La fonte del messaggio resta privata.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status" xml:space="preserve"> + <source>Message status</source> + <target>Stato del messaggio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status: %@" xml:space="preserve"> + <source>Message status: %@</source> + <target>Stato del messaggio: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> <target>Testo del messaggio</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message too large" xml:space="preserve"> + <source>Message too large</source> + <target>Messaggio troppo grande</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> <target>Messaggi</target> @@ -3330,11 +4920,76 @@ Questo è il tuo link per il gruppo %@!</target> <target>I messaggi da %@ verranno mostrati!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Messages in this chat will never be deleted." xml:space="preserve"> + <source>Messages in this chat will never be deleted.</source> + <target>I messaggi in questa chat non verranno mai eliminati.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages received" xml:space="preserve"> + <source>Messages received</source> + <target>Messaggi ricevuti</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages sent" xml:space="preserve"> + <source>Messages sent</source> + <target>Messaggi inviati</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages were deleted after you selected them." xml:space="preserve"> + <source>Messages were deleted after you selected them.</source> + <target>I messaggi sono stati eliminati dopo che li hai selezionati.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>I messaggi, i file e le chiamate sono protetti da **crittografia end-to-end** con perfect forward secrecy, ripudio e recupero da intrusione.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>I messaggi, i file e le chiamate sono protetti da **crittografia e2e resistente alla quantistica** con perfect forward secrecy, ripudio e recupero da intrusione.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate device" xml:space="preserve"> + <source>Migrate device</source> + <target>Migra dispositivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate from another device" xml:space="preserve"> + <source>Migrate from another device</source> + <target>Migra da un altro dispositivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate here" xml:space="preserve"> + <source>Migrate here</source> + <target>Migra qui</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device" xml:space="preserve"> + <source>Migrate to another device</source> + <target>Migra ad un altro dispositivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device via QR code." xml:space="preserve"> + <source>Migrate to another device via QR code.</source> + <target>Migra ad un altro dispositivo via codice QR.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating" xml:space="preserve"> + <source>Migrating</source> + <target>Migrazione</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migrating database archive…" xml:space="preserve"> <source>Migrating database archive…</source> <target>Migrazione archivio del database…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Migration complete" xml:space="preserve"> + <source>Migration complete</source> + <target>Migrazione completata</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> <target>Errore di migrazione:</target> @@ -3350,9 +5005,9 @@ Questo è il tuo link per il gruppo %@!</target> <target>La migrazione è completata</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> - <target>Migrazioni: %@</target> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> + <target>Migrazioni:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> @@ -3370,21 +5025,31 @@ Questo è il tuo link per il gruppo %@!</target> <target>Moderato il: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="More" xml:space="preserve"> + <source>More</source> + <target>Altro</target> + <note>swipe action</note> + </trans-unit> <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> <target>Altri miglioramenti sono in arrivo!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="More reliable network connection." xml:space="preserve"> + <source>More reliable network connection.</source> + <target>Connessione di rete più affidabile.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="More reliable notifications" xml:space="preserve"> + <source>More reliable notifications</source> + <target>Notifiche più affidabili</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> <source>Most likely this connection is deleted.</source> <target>Probabilmente questa connessione è stata eliminata.</target> <note>item status description</note> </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> - <source>Most likely this contact has deleted the connection with you.</source> - <target>Probabilmente questo contatto ha eliminato la connessione con te.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> <target>Profili di chat multipli</target> @@ -3393,7 +5058,12 @@ Questo è il tuo link per il gruppo %@!</target> <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> <target>Silenzia</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Mute all" xml:space="preserve"> + <source>Mute all</source> + <target>Silenzia tutto</target> + <note>notification label action</note> </trans-unit> <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> @@ -3403,13 +5073,38 @@ Questo è il tuo link per il gruppo %@!</target> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> <target>Nome</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> <target>Rete e server</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Network connection" xml:space="preserve"> + <source>Network connection</source> + <target>Connessione di rete</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network decentralization" xml:space="preserve"> + <source>Network decentralization</source> + <target>Decentralizzazione della rete</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve"> + <source>Network issues - message expired after many attempts to send it.</source> + <target>Problemi di rete - messaggio scaduto dopo molti tentativi di inviarlo.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Network management" xml:space="preserve"> + <source>Network management</source> + <target>Gestione della rete</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network operator" xml:space="preserve"> + <source>Network operator</source> + <target>Operatore di rete</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> <target>Impostazioni di rete</target> @@ -3420,11 +5115,36 @@ Questo è il tuo link per il gruppo %@!</target> <target>Stato della rete</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New" xml:space="preserve"> + <source>New</source> + <target>Nuovo</target> + <note>token status text</note> + </trans-unit> <trans-unit id="New Passcode" xml:space="preserve"> <source>New Passcode</source> <target>Nuovo codice di accesso</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve"> + <source>New SOCKS credentials will be used every time you start the app.</source> + <target>Le nuove credenziali SOCKS verranno usate ogni volta che avvii l'app.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve"> + <source>New SOCKS credentials will be used for each server.</source> + <target>Le nuove credenziali SOCKS verranno usate per ogni server.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat" xml:space="preserve"> + <source>New chat</source> + <target>Nuova chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat experience 🎉" xml:space="preserve"> + <source>New chat experience 🎉</source> + <target>Una nuova esperienza di chat 🎉</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> <target>Nuova richiesta di contatto</target> @@ -3435,11 +5155,6 @@ Questo è il tuo link per il gruppo %@!</target> <target>Nuovo contatto:</target> <note>notification</note> </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> - <source>New database archive</source> - <target>Nuovo archivio database</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="New desktop app!" xml:space="preserve"> <source>New desktop app!</source> <target>Nuova app desktop!</target> @@ -3450,11 +5165,21 @@ Questo è il tuo link per il gruppo %@!</target> <target>Nuovo nome da mostrare</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>Nuovi eventi</target> + <note>notification</note> + </trans-unit> <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> <target>Novità nella %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New media options" xml:space="preserve"> + <source>New media options</source> + <target>Nuove opzioni multimediali</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> <target>Nuovo ruolo del membro</target> @@ -3470,6 +5195,11 @@ Questo è il tuo link per il gruppo %@!</target> <target>Nuova password…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New server" xml:space="preserve"> + <source>New server</source> + <target>Nuovo server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No" xml:space="preserve"> <source>No</source> <target>No</target> @@ -3480,6 +5210,21 @@ Questo è il tuo link per il gruppo %@!</target> <target>Nessuna password dell'app</target> <note>Authentication unavailable</note> </trans-unit> + <trans-unit id="No chats" xml:space="preserve"> + <source>No chats</source> + <target>Nessuna chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats found" xml:space="preserve"> + <source>No chats found</source> + <target>Nessuna chat trovata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats in list %@" xml:space="preserve"> + <source>No chats in list %@</source> + <target>Nessuna chat nell'elenco %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> <target>Nessun contatto selezionato</target> @@ -3500,6 +5245,11 @@ Questo è il tuo link per il gruppo %@!</target> <target>Nessun token del dispositivo!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No direct connection yet, message is forwarded by admin." xml:space="preserve"> + <source>No direct connection yet, message is forwarded by admin.</source> + <target>Ancora nessuna connessione diretta, il messaggio viene inoltrato dall'amministratore.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="No filtered chats" xml:space="preserve"> <source>No filtered chats</source> <target>Nessuna chat filtrata</target> @@ -3515,20 +5265,111 @@ Questo è il tuo link per il gruppo %@!</target> <target>Nessuna cronologia</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No info, try to reload" xml:space="preserve"> + <source>No info, try to reload</source> + <target>Nessuna informazione, prova a ricaricare</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No media & file servers." xml:space="preserve"> + <source>No media & file servers.</source> + <target>Nessun server di multimediali e file.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No message" xml:space="preserve"> + <source>No message</source> + <target>Nessun messaggio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No message servers." xml:space="preserve"> + <source>No message servers.</source> + <target>Nessun server dei messaggi.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No network connection" xml:space="preserve"> + <source>No network connection</source> + <target>Nessuna connessione di rete</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record speech" xml:space="preserve"> + <source>No permission to record speech</source> + <target>Nessuna autorizzazione per registrare l'audio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record video" xml:space="preserve"> + <source>No permission to record video</source> + <target>Nessuna autorizzazione per registrare il video</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> <source>No permission to record voice message</source> <target>Nessuna autorizzazione per registrare messaggi vocali</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No push server" xml:space="preserve"> + <source>No push server</source> + <target>Locale</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> <target>Nessun file ricevuto o inviato</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No servers for private message routing." xml:space="preserve"> + <source>No servers for private message routing.</source> + <target>Nessun server per l'instradamento dei messaggi privati.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive files." xml:space="preserve"> + <source>No servers to receive files.</source> + <target>Nessun server per ricevere file.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive messages." xml:space="preserve"> + <source>No servers to receive messages.</source> + <target>Nessun server per ricevere messaggi.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to send files." xml:space="preserve"> + <source>No servers to send files.</source> + <target>Nessun server per inviare file.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No token!" xml:space="preserve"> + <source>No token!</source> + <target>Nessun token!</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="No unread chats" xml:space="preserve"> + <source>No unread chats</source> + <target>Nessuna chat non letta</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> + <target>Nessun identificatore utente.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Not compatible!" xml:space="preserve"> <source>Not compatible!</source> + <target>Non compatibile!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notes" xml:space="preserve"> + <source>Notes</source> + <target>Note</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing selected" xml:space="preserve"> + <source>Nothing selected</source> + <target>Nessuna selezione</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing to forward!" xml:space="preserve"> + <source>Nothing to forward!</source> + <target>Niente da inoltrare!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> <target>Notifiche</target> @@ -3539,6 +5380,21 @@ Questo è il tuo link per il gruppo %@!</target> <target>Le notifiche sono disattivate!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notifications error" xml:space="preserve"> + <source>Notifications error</source> + <target>Errore delle notifiche</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Notifications privacy" xml:space="preserve"> + <source>Notifications privacy</source> + <target>Privacy delle notifiche</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications status" xml:space="preserve"> + <source>Notifications status</source> + <target>Stato delle notifiche</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. @@ -3548,44 +5404,43 @@ Questo è il tuo link per il gruppo %@!</target> - disattivare i membri (ruolo "osservatore")</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="OK" xml:space="preserve"> + <source>OK</source> + <target>OK</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Off" xml:space="preserve"> <source>Off</source> <target>Off</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> - <source>Off (Local)</source> - <target>Off (Locale)</target> - <note>No comment provided by engineer.</note> + <note>blur media</note> </trans-unit> <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> <target>Ok</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> <target>Database vecchio</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> - <source>Old database archive</source> - <target>Vecchio archivio del database</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> <target>Link di invito una tantum</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> - <target>Gli host Onion saranno necessari per la connessione. Richiede l'attivazione della VPN.</target> + <trans-unit id="Onion hosts will be **required** for connection. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be **required** for connection. +Requires compatible VPN.</source> + <target>Gli host Onion saranno **necessari** per la connessione. +Richiede l'attivazione della VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> - <target>Gli host Onion verranno usati quando disponibili. Richiede l'attivazione della VPN.</target> + <trans-unit id="Onion hosts will be used when available. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be used when available. +Requires compatible VPN.</source> + <target>Gli host Onion verranno usati quando disponibili. +Richiede l'attivazione della VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will not be used." xml:space="preserve"> @@ -3593,11 +5448,21 @@ Questo è il tuo link per il gruppo %@!</target> <target>Gli host Onion non verranno usati.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only chat owners can change preferences." xml:space="preserve"> + <source>Only chat owners can change preferences.</source> + <target>Solo i proprietari della chat possono modificarne le preferenze.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <target>Solo i dispositivi client archiviano profili utente, i contatti, i gruppi e i messaggi inviati con la **crittografia end-to-end a 2 livelli**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only delete conversation" xml:space="preserve"> + <source>Only delete conversation</source> + <target>Elimina solo la conversazione</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> <source>Only group owners can change group preferences.</source> <target>Solo i proprietari del gruppo possono modificarne le preferenze.</target> @@ -3613,14 +5478,24 @@ Questo è il tuo link per il gruppo %@!</target> <target>Solo i proprietari del gruppo possono attivare i messaggi vocali.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only sender and moderators see it" xml:space="preserve"> + <source>Only sender and moderators see it</source> + <target>Solo il mittente e i moderatori lo vedono</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you and moderators see it" xml:space="preserve"> + <source>Only you and moderators see it</source> + <target>Solo tu e i moderatori lo vedete</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only you can add message reactions." xml:space="preserve"> <source>Only you can add message reactions.</source> <target>Solo tu puoi aggiungere reazioni ai messaggi.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <target>Solo tu puoi eliminare irreversibilmente i messaggi (il tuo contatto può contrassegnarli per l'eliminazione).</target> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source> + <target>Solo tu puoi eliminare irreversibilmente i messaggi (il tuo contatto può contrassegnarli per l'eliminazione). (24 ore)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can make calls." xml:space="preserve"> @@ -3643,9 +5518,9 @@ Questo è il tuo link per il gruppo %@!</target> <target>Solo il tuo contatto può aggiungere reazioni ai messaggi.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <target>Solo il tuo contatto può eliminare irreversibilmente i messaggi (tu puoi contrassegnarli per l'eliminazione).</target> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source> + <target>Solo il tuo contatto può eliminare irreversibilmente i messaggi (tu puoi contrassegnarli per l'eliminazione). (24 ore)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can make calls." xml:space="preserve"> @@ -3666,13 +5541,18 @@ Questo è il tuo link per il gruppo %@!</target> <trans-unit id="Open" xml:space="preserve"> <source>Open</source> <target>Apri</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Apri le impostazioni</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open changes" xml:space="preserve"> + <source>Open changes</source> + <target>Apri le modifiche</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> <target>Apri chat</target> @@ -3683,26 +5563,87 @@ Questo è il tuo link per il gruppo %@!</target> <target>Apri la console della chat</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Open conditions" xml:space="preserve"> + <source>Open conditions</source> + <target>Apri le condizioni</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open group" xml:space="preserve"> <source>Open group</source> <target>Apri gruppo</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve"> - <source>Open user profiles</source> - <target>Apri i profili utente</target> + <trans-unit id="Open link?" xml:space="preserve"> + <source>Open link?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Open migration to another device" xml:space="preserve"> + <source>Open migration to another device</source> + <target>Apri migrazione ad un altro dispositivo</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> - <target>Protocollo e codice open source: chiunque può gestire i server.</target> + <trans-unit id="Opening app…" xml:space="preserve"> + <source>Opening app…</source> + <target>Apertura dell'app…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve"> - <source>Opening database…</source> - <target>Apertura del database…</target> + <trans-unit id="Operator" xml:space="preserve"> + <source>Operator</source> + <target>Operatore</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Operator server" xml:space="preserve"> + <source>Operator server</source> + <target>Server dell'operatore</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Or import archive file" xml:space="preserve"> + <source>Or import archive file</source> + <target>O importa file archivio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or paste archive link" xml:space="preserve"> + <source>Or paste archive link</source> + <target>O incolla il link dell'archivio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or scan QR code" xml:space="preserve"> + <source>Or scan QR code</source> + <target>O scansiona il codice QR</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or securely share this file link" xml:space="preserve"> + <source>Or securely share this file link</source> + <target>O condividi in modo sicuro questo link del file</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or show this code" xml:space="preserve"> + <source>Or show this code</source> + <target>O mostra questo codice</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or to share privately" xml:space="preserve"> + <source>Or to share privately</source> + <target>O per condividere in modo privato</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Organize chats into lists" xml:space="preserve"> + <source>Organize chats into lists</source> + <target>Organizza le chat in elenchi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other" xml:space="preserve"> + <source>Other</source> + <target>Altro</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other file errors: %@" xml:space="preserve"> + <source>Other file errors: +%@</source> + <target>Altri errori di file: +%@</target> + <note>alert message</note> + </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> <target>Conteggio PING</target> @@ -3738,15 +5679,20 @@ Questo è il tuo link per il gruppo %@!</target> <target>Codice di accesso impostato!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Password" xml:space="preserve"> + <source>Password</source> + <target>Password</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> <target>Password per mostrare</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste" xml:space="preserve"> - <source>Paste</source> - <target>Incolla</target> - <note>No comment provided by engineer.</note> + <trans-unit id="Past member %@" xml:space="preserve"> + <source>Past member %@</source> + <target>Membro passato %@</target> + <note>past/unknown group member</note> </trans-unit> <trans-unit id="Paste desktop address" xml:space="preserve"> <source>Paste desktop address</source> @@ -3758,23 +5704,23 @@ Questo è il tuo link per il gruppo %@!</target> <target>Incolla immagine</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve"> - <source>Paste received link</source> - <target>Incolla il link ricevuto</target> + <trans-unit id="Paste link to connect!" xml:space="preserve"> + <source>Paste link to connect!</source> + <target>Incolla un link per connettere!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> - <source>Paste the link you received to connect with your contact.</source> - <target>Incolla il link che hai ricevuto nella casella sottostante per connetterti con il tuo contatto.</target> - <note>placeholder</note> - </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> - <target>Le persone possono connettersi a te solo tramite i link che condividi.</target> + <trans-unit id="Paste the link you received" xml:space="preserve"> + <source>Paste the link you received</source> + <target>Incolla il link che hai ricevuto</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Periodically" xml:space="preserve"> - <source>Periodically</source> + <trans-unit id="Pending" xml:space="preserve"> + <source>Pending</source> + <target>In attesa</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Periodic" xml:space="preserve"> + <source>Periodic</source> <target>Periodicamente</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3783,11 +5729,33 @@ Questo è il tuo link per il gruppo %@!</target> <target>Errore di decifrazione</target> <note>message decrypt error item</note> </trans-unit> + <trans-unit id="Picture-in-picture calls" xml:space="preserve"> + <source>Picture-in-picture calls</source> + <target>Chiamate picture-in-picture</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Play from the chat list." xml:space="preserve"> + <source>Play from the chat list.</source> + <target>Riproduci dall'elenco delle chat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable calls." xml:space="preserve"> + <source>Please ask your contact to enable calls.</source> + <target>Chiedi al contatto di attivare le chiamate.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> <target>Chiedi al tuo contatto di attivare l'invio dei messaggi vocali.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers." xml:space="preserve"> + <source>Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers.</source> + <target>Controlla che mobile e desktop siano collegati alla stessa rete locale e che il firewall del desktop consenta la connessione. +Si prega di condividere qualsiasi altro problema con gli sviluppatori.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> <target>Controlla di aver usato il link giusto o chiedi al tuo contatto di inviartene un altro.</target> @@ -3803,6 +5771,18 @@ Questo è il tuo link per il gruppo %@!</target> <target>Controlla le preferenze tue e del tuo contatto.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve"> + <source>Please confirm that network settings are correct for this device.</source> + <target>Conferma che le impostazioni di rete sono corrette per questo dispositivo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact developers. Error: %@" xml:space="preserve"> + <source>Please contact developers. +Error: %@</source> + <target>Contatta gli sviluppatori. +Errore: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> <target>Contatta l'amministratore del gruppo.</target> @@ -3843,11 +5823,31 @@ Questo è il tuo link per il gruppo %@!</target> <target>Conserva la password in modo sicuro, NON potrai cambiarla se la perdi.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please try to disable and re-enable notfications." xml:space="preserve"> + <source>Please try to disable and re-enable notfications.</source> + <target>Prova a disattivare e riattivare le notifiche.</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token activation to complete." xml:space="preserve"> + <source>Please wait for token activation to complete.</source> + <target>Attendi il completamento dell'attivazione del token.</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token to be registered." xml:space="preserve"> + <source>Please wait for token to be registered.</source> + <target>Attendi la registrazione del token.</target> + <note>token info</note> + </trans-unit> <trans-unit id="Polish interface" xml:space="preserve"> <source>Polish interface</source> <target>Interfaccia polacca</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Port" xml:space="preserve"> + <source>Port</source> + <target>Porta</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> <target>Probabilmente l'impronta del certificato nell'indirizzo del server è sbagliata</target> @@ -3858,36 +5858,86 @@ Questo è il tuo link per il gruppo %@!</target> <target>Conserva la bozza dell'ultimo messaggio, con gli allegati.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Preset server" xml:space="preserve"> - <source>Preset server</source> - <target>Server preimpostato</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> <target>Indirizzo server preimpostato</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Preset servers" xml:space="preserve"> + <source>Preset servers</source> + <target>Server preimpostati</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Preview" xml:space="preserve"> <source>Preview</source> <target>Anteprima</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Previously connected servers" xml:space="preserve"> + <source>Previously connected servers</source> + <target>Server precedentemente connessi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> <target>Privacy e sicurezza</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Privacy for your customers." xml:space="preserve"> + <source>Privacy for your customers.</source> + <target>Privacy per i tuoi clienti.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy policy and conditions of use." xml:space="preserve"> + <source>Privacy policy and conditions of use.</source> + <target>Informativa sulla privacy e condizioni d'uso.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy redefined" xml:space="preserve"> <source>Privacy redefined</source> <target>Privacy ridefinita</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve"> + <source>Private chats, groups and your contacts are not accessible to server operators.</source> + <target>Le chat private, i gruppi e i tuoi contatti non sono accessibili agli operatori dei server.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> <target>Nomi di file privati</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private media file names." xml:space="preserve"> + <source>Private media file names.</source> + <target>Nomi privati dei file multimediali.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing" xml:space="preserve"> + <source>Private message routing</source> + <target>Instradamento privato dei messaggi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing 🚀" xml:space="preserve"> + <source>Private message routing 🚀</source> + <target>Instradamento privato dei messaggi 🚀</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private notes" xml:space="preserve"> + <source>Private notes</source> + <target>Note private</target> + <note>name of notes to self</note> + </trans-unit> + <trans-unit id="Private routing" xml:space="preserve"> + <source>Private routing</source> + <target>Instradamento privato</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private routing error" xml:space="preserve"> + <source>Private routing error</source> + <target>Errore di instradamento privato</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> <target>Profilo e connessioni al server</target> @@ -3898,14 +5948,9 @@ Questo è il tuo link per il gruppo %@!</target> <target>Immagine del profilo</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile name" xml:space="preserve"> - <source>Profile name</source> - <target>Nome del profilo</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile name:" xml:space="preserve"> - <source>Profile name:</source> - <target>Nome del profilo:</target> + <trans-unit id="Profile images" xml:space="preserve"> + <source>Profile images</source> + <target>Immagini del profilo</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile password" xml:space="preserve"> @@ -3913,10 +5958,15 @@ Questo è il tuo link per il gruppo %@!</target> <target>Password del profilo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Profile theme" xml:space="preserve"> + <source>Profile theme</source> + <target>Tema del profilo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> <source>Profile update will be sent to your contacts.</source> <target>L'aggiornamento del profilo verrà inviato ai tuoi contatti.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> @@ -3938,6 +5988,16 @@ Questo è il tuo link per il gruppo %@!</target> <target>Proibisci le reazioni ai messaggi.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Prohibit reporting messages to moderators." xml:space="preserve"> + <source>Prohibit reporting messages to moderators.</source> + <target>Vieta di segnalare messaggi ai moderatori.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending SimpleX links." xml:space="preserve"> + <source>Prohibit sending SimpleX links.</source> + <target>Vieta l'invio di link di SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> <target>Proibisci l'invio di messaggi diretti ai membri.</target> @@ -3958,11 +6018,23 @@ Questo è il tuo link per il gruppo %@!</target> <target>Proibisci l'invio di messaggi vocali.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect IP address" xml:space="preserve"> + <source>Protect IP address</source> + <target>Proteggi l'indirizzo IP</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> <target>Proteggi la schermata dell'app</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve"> + <source>Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings.</source> + <target>Proteggi il tuo indirizzo IP dai relay di messaggistica scelti dai tuoi contatti. +Attivalo nelle impostazioni *Rete e server*.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> <target>Proteggi i tuoi profili di chat con una password!</target> @@ -3978,16 +6050,46 @@ Questo è il tuo link per il gruppo %@!</target> <target>Scadenza del protocollo per KB</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Proxied" xml:space="preserve"> + <source>Proxied</source> + <target>Via proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied servers" xml:space="preserve"> + <source>Proxied servers</source> + <target>Server via proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxy requires password" xml:space="preserve"> + <source>Proxy requires password</source> + <target>Il proxy richiede una password</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> <target>Notifiche push</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Push server" xml:space="preserve"> + <source>Push server</source> + <target>Server push</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Quantum resistant encryption" xml:space="preserve"> + <source>Quantum resistant encryption</source> + <target>Crittografia resistente alla quantistica</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> <source>Rate the app</source> <target>Valuta l'app</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reachable chat toolbar" xml:space="preserve"> + <source>Reachable chat toolbar</source> + <target>Barra degli strumenti di chat accessibile</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> <target>Reagisci…</target> @@ -3996,16 +6098,21 @@ Questo è il tuo link per il gruppo %@!</target> <trans-unit id="Read" xml:space="preserve"> <source>Read</source> <target>Leggi</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Read more" xml:space="preserve"> <source>Read more</source> <target>Leggi tutto</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> - <target>Maggiori informazioni nella [Guida per l'utente](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source> + <target>Leggi di più nella [Guida utente](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</source> + <target>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).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> @@ -4013,11 +6120,6 @@ Questo è il tuo link per il gruppo %@!</target> <target>Maggiori informazioni nella [Guida per l'utente](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> - <source>Read more in our GitHub repository.</source> - <target>Maggiori informazioni nel nostro repository GitHub.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> <target>Maggiori informazioni nel nostro [repository GitHub](https://github.com/simplex-chat/simplex-chat#readme).</target> @@ -4028,6 +6130,11 @@ Questo è il tuo link per il gruppo %@!</target> <target>Le ricevute sono disattivate</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receive errors" xml:space="preserve"> + <source>Receive errors</source> + <target>Errori di ricezione</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Ricevuto il</target> @@ -4048,6 +6155,21 @@ Questo è il tuo link per il gruppo %@!</target> <target>Messaggio ricevuto</target> <note>message info title</note> </trans-unit> + <trans-unit id="Received messages" xml:space="preserve"> + <source>Received messages</source> + <target>Messaggi ricevuti</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received reply" xml:space="preserve"> + <source>Received reply</source> + <target>Risposta ricevuta</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received total" xml:space="preserve"> + <source>Received total</source> + <target>Totale ricevuto</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> <target>L'indirizzo di ricezione verrà cambiato in un server diverso. La modifica dell'indirizzo verrà completata dopo che il mittente sarà in linea.</target> @@ -4063,16 +6185,51 @@ Questo è il tuo link per il gruppo %@!</target> <target>Ricezione via</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Recent history and improved [directory bot](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)." xml:space="preserve"> + <source>Recent history and improved [directory bot](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).</source> + <target>Cronologia recente e [bot della directory](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) migliorato.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve"> + <source>Recipient(s) can't see who this message is from.</source> + <target>I destinatari non possono vedere da chi proviene questo messaggio.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> <target>I destinatari vedono gli aggiornamenti mentre li digiti.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect" xml:space="preserve"> + <source>Reconnect</source> + <target>Riconnetti</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> <target>Riconnetti tutti i server connessi per imporre il recapito dei messaggi. Utilizza traffico aggiuntivo.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect all servers" xml:space="preserve"> + <source>Reconnect all servers</source> + <target>Riconnetti tutti i server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers?" xml:space="preserve"> + <source>Reconnect all servers?</source> + <target>Riconnettere tutti i server?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect server to force message delivery. It uses additional traffic.</source> + <target>Riconnetti il server per forzare la consegna dei messaggi. Usa traffico aggiuntivo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server?" xml:space="preserve"> + <source>Reconnect server?</source> + <target>Riconnettere il server?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> <target>Riconnettere i server?</target> @@ -4093,10 +6250,26 @@ Questo è il tuo link per il gruppo %@!</target> <target>Consumo di batteria ridotto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Register" xml:space="preserve"> + <source>Register</source> + <target>Registra</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Register notification token?" xml:space="preserve"> + <source>Register notification token?</source> + <target>Registrare il token di notifica?</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Registered" xml:space="preserve"> + <source>Registered</source> + <target>Registrato</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> <target>Rifiuta</target> - <note>reject incoming call via notification</note> + <note>reject incoming call via notification +swipe action</note> </trans-unit> <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> <source>Reject (sender NOT notified)</source> @@ -4123,6 +6296,16 @@ Questo è il tuo link per il gruppo %@!</target> <target>Rimuovi</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Remove archive?" xml:space="preserve"> + <source>Remove archive?</source> + <target>Rimuovere l'archivio?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove image" xml:space="preserve"> + <source>Remove image</source> + <target>Rimuovi immagine</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> <target>Rimuovi membro</target> @@ -4158,16 +6341,81 @@ Questo è il tuo link per il gruppo %@!</target> <target>Ripetere la richiesta di connessione?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat download" xml:space="preserve"> + <source>Repeat download</source> + <target>Ripeti scaricamento</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat import" xml:space="preserve"> + <source>Repeat import</source> + <target>Ripeti importazione</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Repeat join request?" xml:space="preserve"> <source>Repeat join request?</source> <target>Ripetere la richiesta di ingresso?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat upload" xml:space="preserve"> + <source>Repeat upload</source> + <target>Ripeti caricamento</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reply" xml:space="preserve"> <source>Reply</source> <target>Rispondi</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Report" xml:space="preserve"> + <source>Report</source> + <target>Segnala</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Report content: only group moderators will see it." xml:space="preserve"> + <source>Report content: only group moderators will see it.</source> + <target>Segnala contenuto: solo i moderatori del gruppo lo vedranno.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report member profile: only group moderators will see it." xml:space="preserve"> + <source>Report member profile: only group moderators will see it.</source> + <target>Segnala profilo: solo i moderatori del gruppo lo vedranno.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report other: only group moderators will see it." xml:space="preserve"> + <source>Report other: only group moderators will see it.</source> + <target>Segnala altro: solo i moderatori del gruppo lo vedranno.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report reason?" xml:space="preserve"> + <source>Report reason?</source> + <target>Motivo della segnalazione?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Report spam: only group moderators will see it." xml:space="preserve"> + <source>Report spam: only group moderators will see it.</source> + <target>Segnala spam: solo i moderatori del gruppo lo vedranno.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report violation: only group moderators will see it." xml:space="preserve"> + <source>Report violation: only group moderators will see it.</source> + <target>Segnala violazione: solo i moderatori del gruppo lo vedranno.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report: %@" xml:space="preserve"> + <source>Report: %@</source> + <target>Segnalazione: %@</target> + <note>report in notification</note> + </trans-unit> + <trans-unit id="Reporting messages to moderators is prohibited." xml:space="preserve"> + <source>Reporting messages to moderators is prohibited.</source> + <target>È vietato segnalare messaggi ai moderatori.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reports" xml:space="preserve"> + <source>Reports</source> + <target>Segnalazioni</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Required" xml:space="preserve"> <source>Required</source> <target>Obbligatorio</target> @@ -4178,16 +6426,41 @@ Questo è il tuo link per il gruppo %@!</target> <target>Ripristina</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset all hints" xml:space="preserve"> + <source>Reset all hints</source> + <target>Ripristina tutti i suggerimenti</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics" xml:space="preserve"> + <source>Reset all statistics</source> + <target>Azzera tutte le statistiche</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics?" xml:space="preserve"> + <source>Reset all statistics?</source> + <target>Azzerare tutte le statistiche?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> <target>Ripristina i colori</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to app theme" xml:space="preserve"> + <source>Reset to app theme</source> + <target>Ripristina al tema dell'app</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> <target>Ripristina i predefiniti</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to user theme" xml:space="preserve"> + <source>Reset to user theme</source> + <target>Ripristina al tema dell'utente</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> <source>Restart the app to create a new chat profile</source> <target>Riavvia l'app per creare un nuovo profilo di chat</target> @@ -4218,14 +6491,19 @@ Questo è il tuo link per il gruppo %@!</target> <target>Errore di ripristino del database</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Retry" xml:space="preserve"> + <source>Retry</source> + <target>Riprova</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> <target>Rivela</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Revert" xml:space="preserve"> - <source>Revert</source> - <target>Ripristina</target> + <trans-unit id="Review conditions" xml:space="preserve"> + <source>Review conditions</source> + <target>Leggi le condizioni</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke" xml:space="preserve"> @@ -4253,51 +6531,67 @@ Questo è il tuo link per il gruppo %@!</target> <target>Avvia chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve"> - <source>SMP servers</source> + <trans-unit id="SMP server" xml:space="preserve"> + <source>SMP server</source> <target>Server SMP</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SOCKS proxy" xml:space="preserve"> + <source>SOCKS proxy</source> + <target>Proxy SOCKS</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safely receive files" xml:space="preserve"> + <source>Safely receive files</source> + <target>Ricevi i file in sicurezza</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safer groups" xml:space="preserve"> + <source>Safer groups</source> + <target>Gruppi più sicuri</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save" xml:space="preserve"> <source>Save</source> <target>Salva</target> - <note>chat item action</note> + <note>alert button +chat item action</note> </trans-unit> <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> <target>Salva (e avvisa i contatti)</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> <target>Salva e avvisa il contatto</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> <target>Salva e avvisa i membri del gruppo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save and reconnect" xml:space="preserve"> + <source>Save and reconnect</source> + <target>Salva e riconnetti</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save and update group profile" xml:space="preserve"> <source>Save and update group profile</source> <target>Salva e aggiorna il profilo del gruppo</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save archive" xml:space="preserve"> - <source>Save archive</source> - <target>Salva archivio</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve"> - <source>Save auto-accept settings</source> - <target>Salva le impostazioni di accettazione automatica</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> <source>Save group profile</source> <target>Salva il profilo del gruppo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save list" xml:space="preserve"> + <source>Save list</source> + <target>Salva elenco</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> <target>Salva la password e apri la chat</target> @@ -4311,7 +6605,7 @@ Questo è il tuo link per il gruppo %@!</target> <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> <target>Salvare le preferenze?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> @@ -4326,23 +6620,53 @@ Questo è il tuo link per il gruppo %@!</target> <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> <target>Salvare i server?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve"> - <source>Save settings?</source> - <target>Salvare le impostazioni?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> <target>Salvare il messaggio di benvenuto?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save your profile?" xml:space="preserve"> + <source>Save your profile?</source> + <target>Salvare il profilo?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Saved" xml:space="preserve"> + <source>Saved</source> + <target>Salvato</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> <source>Saved WebRTC ICE servers will be removed</source> <target>I server WebRTC ICE salvati verranno rimossi</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Saved from" xml:space="preserve"> + <source>Saved from</source> + <target>Salvato da</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved message" xml:space="preserve"> + <source>Saved message</source> + <target>Messaggio salvato</target> + <note>message info title</note> + </trans-unit> + <trans-unit id="Saving %lld messages" xml:space="preserve"> + <source>Saving %lld messages</source> + <target>Salvataggio di %lld messaggi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scale" xml:space="preserve"> + <source>Scale</source> + <target>Scala</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan / Paste link" xml:space="preserve"> + <source>Scan / Paste link</source> + <target>Scansiona / Incolla link</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> <target>Scansiona codice QR</target> @@ -4373,11 +6697,31 @@ Questo è il tuo link per il gruppo %@!</target> <target>Cerca</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Search bar accepts invitation links." xml:space="preserve"> + <source>Search bar accepts invitation links.</source> + <target>La barra di ricerca accetta i link di invito.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search or paste SimpleX link" xml:space="preserve"> + <source>Search or paste SimpleX link</source> + <target>Cerca o incolla un link SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secondary" xml:space="preserve"> + <source>Secondary</source> + <target>Secondario</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> <source>Secure queue</source> <target>Coda sicura</target> <note>server test step</note> </trans-unit> + <trans-unit id="Secured" xml:space="preserve"> + <source>Secured</source> + <target>Protetto</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> <target>Valutazione della sicurezza</target> @@ -4391,6 +6735,21 @@ Questo è il tuo link per il gruppo %@!</target> <trans-unit id="Select" xml:space="preserve"> <source>Select</source> <target>Seleziona</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Select chat profile" xml:space="preserve"> + <source>Select chat profile</source> + <target>Seleziona il profilo di chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected %lld" xml:space="preserve"> + <source>Selected %lld</source> + <target>%lld selezionato</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Le preferenze della chat selezionata vietano questo messaggio.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct" xml:space="preserve"> @@ -4428,11 +6787,6 @@ Questo è il tuo link per il gruppo %@!</target> <target>Invia ricevute di consegna a</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve"> - <source>Send direct message</source> - <target>Invia messaggio diretto</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Send direct message to connect" xml:space="preserve"> <source>Send direct message to connect</source> <target>Invia messaggio diretto per connetterti</target> @@ -4443,9 +6797,14 @@ Questo è il tuo link per il gruppo %@!</target> <target>Invia messaggio a tempo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send errors" xml:space="preserve"> + <source>Send errors</source> + <target>Errori di invio</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> - <target>Invia anteprime dei link</target> + <target>Invia le anteprime dei link</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send live message" xml:space="preserve"> @@ -4453,14 +6812,29 @@ Questo è il tuo link per il gruppo %@!</target> <target>Invia messaggio in diretta</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send message to enable calls." xml:space="preserve"> + <source>Send message to enable calls.</source> + <target>Invia un messaggio per attivare le chiamate.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source> + <target>Invia messaggi direttamente quando l'indirizzo IP è protetto e il tuo server o quello di destinazione non supporta l'instradamento privato.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when your or destination server does not support private routing.</source> + <target>Invia messaggi direttamente quando il tuo server o quello di destinazione non supporta l'instradamento privato.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> <target>Invia notifiche</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> - <source>Send notifications:</source> - <target>Invia notifiche:</target> + <trans-unit id="Send private reports" xml:space="preserve"> + <source>Send private reports</source> + <target>Invia segnalazioni private</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve"> @@ -4478,10 +6852,15 @@ Questo è il tuo link per il gruppo %@!</target> <target>Inviali dalla galleria o dalle tastiere personalizzate.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve"> + <source>Send up to 100 last messages to new members.</source> + <target>Invia fino a 100 ultimi messaggi ai nuovi membri.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> <target>Il mittente ha annullato il trasferimento del file.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> @@ -4538,6 +6917,11 @@ Questo è il tuo link per il gruppo %@!</target> <target>Inviato il: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Sent directly" xml:space="preserve"> + <source>Sent directly</source> + <target>Inviato direttamente</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> <target>Evento file inviato</target> @@ -4548,11 +6932,71 @@ Questo è il tuo link per il gruppo %@!</target> <target>Messaggio inviato</target> <note>message info title</note> </trans-unit> + <trans-unit id="Sent messages" xml:space="preserve"> + <source>Sent messages</source> + <target>Messaggi inviati</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> <target>I messaggi inviati verranno eliminati dopo il tempo impostato.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sent reply" xml:space="preserve"> + <source>Sent reply</source> + <target>Risposta inviata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent total" xml:space="preserve"> + <source>Sent total</source> + <target>Totale inviato</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent via proxy" xml:space="preserve"> + <source>Sent via proxy</source> + <target>Inviato via proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server" xml:space="preserve"> + <source>Server</source> + <target>Server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server added to operator %@." xml:space="preserve"> + <source>Server added to operator %@.</source> + <target>Server aggiunto all'operatore %@.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Server address" xml:space="preserve"> + <source>Server address</source> + <target>Indirizzo server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings." xml:space="preserve"> + <source>Server address is incompatible with network settings.</source> + <target>L'indirizzo del server non è compatibile con le impostazioni di rete.</target> + <note>srv error text.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Server address is incompatible with network settings: %@.</source> + <target>L'indirizzo del server è incompatibile con le impostazioni di rete: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server operator changed." xml:space="preserve"> + <source>Server operator changed.</source> + <target>L'operatore del server è cambiato.</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Server operators" xml:space="preserve"> + <source>Server operators</source> + <target>Operatori server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server protocol changed." xml:space="preserve"> + <source>Server protocol changed.</source> + <target>Il protocollo del server è cambiato.</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> <target>Il server richiede l'autorizzazione di creare code, controlla la password</target> @@ -4568,11 +7012,36 @@ Questo è il tuo link per il gruppo %@!</target> <target>Test del server fallito!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Server type" xml:space="preserve"> + <source>Server type</source> + <target>Tipo server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server version is incompatible with network settings." xml:space="preserve"> + <source>Server version is incompatible with network settings.</source> + <target>La versione del server non è compatibile con le impostazioni di rete.</target> + <note>srv error text</note> + </trans-unit> + <trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve"> + <source>Server version is incompatible with your app: %@.</source> + <target>La versione del server è incompatibile con la tua app: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Servers" xml:space="preserve"> <source>Servers</source> <target>Server</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Servers info" xml:space="preserve"> + <source>Servers info</source> + <target>Info dei server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <target>Le statistiche dei server verranno azzerate - è irreversibile!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Session code" xml:space="preserve"> <source>Session code</source> <target>Codice di sessione</target> @@ -4583,11 +7052,21 @@ Questo è il tuo link per il gruppo %@!</target> <target>Imposta 1 giorno</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set chat name…" xml:space="preserve"> + <source>Set chat name…</source> + <target>Imposta il nome della chat…</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> <target>Imposta nome del contatto…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve"> + <source>Set default theme</source> + <target>Imposta tema predefinito</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> <target>Imposta le preferenze del gruppo</target> @@ -4598,11 +7077,21 @@ Questo è il tuo link per il gruppo %@!</target> <target>Impostalo al posto dell'autenticazione di sistema.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve"> + <source>Set message expiration in chats.</source> + <target>Imposta la scadenza dei messaggi nelle chat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passcode" xml:space="preserve"> <source>Set passcode</source> <target>Imposta codice</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set passphrase" xml:space="preserve"> + <source>Set passphrase</source> + <target>Imposta password</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> <target>Imposta la password per esportare</target> @@ -4623,24 +7112,55 @@ Questo è il tuo link per il gruppo %@!</target> <target>Impostazioni</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve"> + <source>Settings were changed.</source> + <target>Le impostazioni sono state cambiate.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve"> + <source>Shape profile images</source> + <target>Forma delle immagini del profilo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share" xml:space="preserve"> <source>Share</source> <target>Condividi</target> - <note>chat item action</note> + <note>alert action +chat item action</note> </trans-unit> <trans-unit id="Share 1-time link" xml:space="preserve"> <source>Share 1-time link</source> <target>Condividi link una tantum</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share 1-time link with a friend" xml:space="preserve"> + <source>Share 1-time link with a friend</source> + <target>Condividi link una tantum con un amico</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve"> + <source>Share SimpleX address on social media.</source> + <target>Condividi l'indirizzo SimpleX sui social media.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address" xml:space="preserve"> <source>Share address</source> <target>Condividi indirizzo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share address publicly" xml:space="preserve"> + <source>Share address publicly</source> + <target>Condividi indirizzo pubblicamente</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address with contacts?" xml:space="preserve"> <source>Share address with contacts?</source> <target>Condividere l'indirizzo con i contatti?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Share from other apps." xml:space="preserve"> + <source>Share from other apps.</source> + <target>Condividi da altre app.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share link" xml:space="preserve"> @@ -4648,9 +7168,19 @@ Questo è il tuo link per il gruppo %@!</target> <target>Condividi link</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve"> - <source>Share one-time invitation link</source> - <target>Condividi link di invito una tantum</target> + <trans-unit id="Share profile" xml:space="preserve"> + <source>Share profile</source> + <target>Condividi il profilo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share this 1-time invite link" xml:space="preserve"> + <source>Share this 1-time invite link</source> + <target>Condividi questo link di invito una tantum</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share to SimpleX" xml:space="preserve"> + <source>Share to SimpleX</source> + <target>Condividi in SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share with contacts" xml:space="preserve"> @@ -4658,6 +7188,16 @@ Questo è il tuo link per il gruppo %@!</target> <target>Condividi con i contatti</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Short link" xml:space="preserve"> + <source>Short link</source> + <target>Link breve</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show QR code" xml:space="preserve"> + <source>Show QR code</source> + <target>Mostra codice QR</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> <target>Mostra le chiamate nella cronologia del telefono</target> @@ -4673,21 +7213,46 @@ Questo è il tuo link per il gruppo %@!</target> <target>Mostra ultimi messaggi</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show message status" xml:space="preserve"> + <source>Show message status</source> + <target>Mostra stato del messaggio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show percentage" xml:space="preserve"> + <source>Show percentage</source> + <target>Mostra percentuale</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Mostra anteprima</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show → on messages sent via private routing." xml:space="preserve"> + <source>Show → on messages sent via private routing.</source> + <target>Mostra → nei messaggi inviati via instradamento privato.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show:" xml:space="preserve"> <source>Show:</source> <target>Mostra:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve"> + <source>SimpleX</source> + <target>SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Address" xml:space="preserve"> <source>SimpleX Address</source> <target>Indirizzo SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." xml:space="preserve"> + <source>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</source> + <target>SimpleX Chat e Flux hanno concluso un accordo per includere server gestiti da Flux nell'app.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> <target>La sicurezza di SimpleX Chat è stata verificata da Trail of Bits.</target> @@ -4718,9 +7283,24 @@ Questo è il tuo link per il gruppo %@!</target> <target>Indirizzo SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX address and 1-time links are safe to share via any messenger." xml:space="preserve"> + <source>SimpleX address and 1-time links are safe to share via any messenger.</source> + <target>L'indirizzo SimpleX e i link una tantum sono sicuri da condividere tramite qualsiasi messenger.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address or 1-time link?" xml:space="preserve"> + <source>SimpleX address or 1-time link?</source> + <target>Indirizzo SimpleX o link una tantum?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX channel link" xml:space="preserve"> + <source>SimpleX channel link</source> + <target>Link del canale SimpleX</target> + <note>simplex link type</note> + </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> - <target>Indirizzo del contatto SimpleX</target> + <target>Indirizzo di contatto SimpleX</target> <note>simplex link type</note> </trans-unit> <trans-unit id="SimpleX encrypted message or connection event" xml:space="preserve"> @@ -4736,6 +7316,16 @@ Questo è il tuo link per il gruppo %@!</target> <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> <target>Link di SimpleX</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve"> + <source>SimpleX links are prohibited.</source> + <target>I link di SimpleX sono vietati in questo gruppo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve"> + <source>SimpleX links not allowed</source> + <target>Link di SimpleX non consentiti</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> @@ -4743,11 +7333,21 @@ Questo è il tuo link per il gruppo %@!</target> <target>Invito SimpleX una tantum</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> + <source>SimpleX protocols reviewed by Trail of Bits.</source> + <target>Protocolli di SimpleX esaminati da Trail of Bits.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Simplified incognito mode" xml:space="preserve"> <source>Simplified incognito mode</source> <target>Modalità incognito semplificata</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Size" xml:space="preserve"> + <source>Size</source> + <target>Dimensione</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Salta</target> @@ -4763,19 +7363,52 @@ Questo è il tuo link per il gruppo %@!</target> <target>Piccoli gruppi (max 20)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Soft" xml:space="preserve"> + <source>Soft</source> + <target>Leggera</target> + <note>blur media</note> + </trans-unit> + <trans-unit id="Some app settings were not migrated." xml:space="preserve"> + <source>Some app settings were not migrated.</source> + <target>Alcune impostazioni dell'app non sono state migrate.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some file(s) were not exported:" xml:space="preserve"> + <source>Some file(s) were not exported:</source> + <target>Alcuni file non sono stati esportati:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Si sono verificati alcuni errori non gravi durante l'importazione: vedi la console della chat per i dettagli.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import:" xml:space="preserve"> + <source>Some non-fatal errors occurred during import:</source> + <target>Si sono verificati alcuni errori non fatali durante l'importazione:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some servers failed the test: %@" xml:space="preserve"> + <source>Some servers failed the test: +%@</source> + <target>Alcuni server hanno fallito il test: +%@</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> <target>Qualcuno</target> <note>notification title</note> </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve"> - <source>Start a new chat</source> - <target>Inizia una nuova chat</target> + <trans-unit id="Spam" xml:space="preserve"> + <source>Spam</source> + <target>Spam</target> + <note>blocking reason +report reason</note> + </trans-unit> + <trans-unit id="Square, circle, or anything in between." xml:space="preserve"> + <source>Square, circle, or anything in between.</source> + <target>Quadrata, circolare o qualsiasi forma tra le due.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start chat" xml:space="preserve"> @@ -4783,11 +7416,26 @@ Questo è il tuo link per il gruppo %@!</target> <target>Avvia chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Start chat?" xml:space="preserve"> + <source>Start chat?</source> + <target>Avviare la chat?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> <target>Avvia la migrazione</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Starting from %@." xml:space="preserve"> + <source>Starting from %@.</source> + <target>Inizio da %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Statistics" xml:space="preserve"> + <source>Statistics</source> + <target>Statistiche</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> <target>Ferma</target> @@ -4798,9 +7446,9 @@ Questo è il tuo link per il gruppo %@!</target> <target>Ferma SimpleX</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> - <source>Stop chat to enable database actions</source> - <target>Ferma la chat per attivare le azioni del database</target> + <trans-unit id="Stop chat" xml:space="preserve"> + <source>Stop chat</source> + <target>Ferma la chat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> @@ -4831,23 +7479,63 @@ Questo è il tuo link per il gruppo %@!</target> <trans-unit id="Stop sharing" xml:space="preserve"> <source>Stop sharing</source> <target>Smetti di condividere</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Stop sharing address?" xml:space="preserve"> <source>Stop sharing address?</source> <target>Smettere di condividere l'indirizzo?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Stopping chat" xml:space="preserve"> + <source>Stopping chat</source> + <target>Arresto della chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Storage" xml:space="preserve"> + <source>Storage</source> + <target>Archiviazione</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Strong" xml:space="preserve"> + <source>Strong</source> + <target>Forte</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Submit" xml:space="preserve"> <source>Submit</source> <target>Invia</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Subscribed" xml:space="preserve"> + <source>Subscribed</source> + <target>Iscritto</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscription errors" xml:space="preserve"> + <source>Subscription errors</source> + <target>Errori di iscrizione</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscriptions ignored" xml:space="preserve"> + <source>Subscriptions ignored</source> + <target>Iscrizioni ignorate</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Support SimpleX Chat" xml:space="preserve"> <source>Support SimpleX Chat</source> <target>Supporta SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Switch audio and video during the call." xml:space="preserve"> + <source>Switch audio and video during the call.</source> + <target>Cambia tra audio e video durante la chiamata.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> + <source>Switch chat profile for 1-time invitations.</source> + <target>Cambia profilo di chat per inviti una tantum.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="System" xml:space="preserve"> <source>System</source> <target>Sistema</target> @@ -4858,11 +7546,21 @@ Questo è il tuo link per il gruppo %@!</target> <target>Autenticazione di sistema</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP connection" xml:space="preserve"> + <source>TCP connection</source> + <target>Connessione TCP</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> <target>Scadenza connessione TCP</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP port for messaging" xml:space="preserve"> + <source>TCP port for messaging</source> + <target>Porta TCP per i messaggi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <source>TCP_KEEPCNT</source> <target>TCP_KEEPCNT</target> @@ -4878,11 +7576,21 @@ Questo è il tuo link per il gruppo %@!</target> <target>TCP_KEEPINTVL</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tail" xml:space="preserve"> + <source>Tail</source> + <target>Coda</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> <target>Scatta foto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap Create SimpleX address in the menu to create it later." xml:space="preserve"> + <source>Tap Create SimpleX address in the menu to create it later.</source> + <target>Tocca Crea indirizzo SimpleX nel menu per crearlo più tardi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> <target>Tocca il pulsante </target> @@ -4908,16 +7616,31 @@ Questo è il tuo link per il gruppo %@!</target> <target>Toccare per entrare in incognito</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve"> - <source>Tap to start a new chat</source> - <target>Tocca per iniziare una chat</target> + <trans-unit id="Tap to paste link" xml:space="preserve"> + <source>Tap to paste link</source> + <target>Tocca per incollare il link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap to scan" xml:space="preserve"> + <source>Tap to scan</source> + <target>Tocca per scansionare</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Temporary file error" xml:space="preserve"> + <source>Temporary file error</source> + <target>Errore del file temporaneo</target> + <note>file error alert title</note> + </trans-unit> <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> <target>Test fallito al passo %@.</target> <note>server test failure</note> </trans-unit> + <trans-unit id="Test notifications" xml:space="preserve"> + <source>Test notifications</source> + <target>Prova le notifiche</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> <target>Prova server</target> @@ -4931,7 +7654,7 @@ Questo è il tuo link per il gruppo %@!</target> <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> <target>Test falliti!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> @@ -4948,11 +7671,6 @@ Questo è il tuo link per il gruppo %@!</target> <target>Grazie agli utenti – contribuite via Weblate!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> - <target>La prima piattaforma senza alcun identificatore utente – privata by design.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <source>The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised.</source> @@ -4965,11 +7683,31 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.</ta <target>L'app può avvisarti quando ricevi messaggi o richieste di contatto: apri le impostazioni per attivare.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The app protects your privacy by using different operators in each conversation." xml:space="preserve"> + <source>The app protects your privacy by using different operators in each conversation.</source> + <target>L'app protegge la tua privacy usando diversi operatori in ogni conversazione.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve"> + <source>The app will ask to confirm downloads from unknown file servers (except .onion).</source> + <target>L'app chiederà di confermare i download da server di file sconosciuti (eccetto .onion).</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> <source>The attempt to change database passphrase was not completed.</source> <target>Il tentativo di cambiare la password del database non è stato completato.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve"> + <source>The code you scanned is not a SimpleX link QR code.</source> + <target>Il codice che hai scansionato non è un codice QR di link SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The connection reached the limit of undelivered messages, your contact may be offline." xml:space="preserve"> + <source>The connection reached the limit of undelivered messages, your contact may be offline.</source> + <target>La connessione ha raggiunto il limite di messaggi non consegnati, il contatto potrebbe essere offline.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> <target>La connessione che hai accettato verrà annullata!</target> @@ -4990,6 +7728,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.</ta <target>La crittografia funziona e il nuovo accordo sulla crittografia non è richiesto. Potrebbero verificarsi errori di connessione!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> + <target>La nuova generazione di messaggistica privata</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The hash of the previous message is different." xml:space="preserve"> <source>The hash of the previous message is different.</source> <target>L'hash del messaggio precedente è diverso.</target> @@ -5005,9 +7748,14 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.</ta <target>Il messaggio sarà segnato come moderato per tutti i membri.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> - <target>La nuova generazione di messaggistica privata</target> + <trans-unit id="The messages will be deleted for all members." xml:space="preserve"> + <source>The messages will be deleted for all members.</source> + <target>I messaggi verranno eliminati per tutti i membri.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve"> + <source>The messages will be marked as moderated for all members.</source> + <target>I messaggi verranno contrassegnati come moderati per tutti i membri.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> @@ -5015,9 +7763,14 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.</ta <target>Il database vecchio non è stato rimosso durante la migrazione, può essere eliminato.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> - <target>Il profilo è condiviso solo con i tuoi contatti.</target> + <trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve"> + <source>The same conditions will apply to operator **%@**.</source> + <target>Le stesse condizioni si applicheranno all'operatore **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second preset operator in the app!" xml:space="preserve"> + <source>The second preset operator in the app!</source> + <target>Il secondo operatore preimpostato nell'app!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> @@ -5035,19 +7788,29 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.</ta <target>I server per le nuove connessioni del profilo di chat attuale **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Theme" xml:space="preserve"> - <source>Theme</source> - <target>Tema</target> + <trans-unit id="The servers for new files of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new files of your current chat profile **%@**.</source> + <target>I server per nuovi file del tuo profilo di chat attuale **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve"> - <source>There should be at least one user profile.</source> - <target>Deve esserci almeno un profilo utente.</target> + <trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve"> + <source>The text you pasted is not a SimpleX link.</source> + <target>Il testo che hai incollato non è un link SimpleX.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> - <source>There should be at least one visible user profile.</source> - <target>Deve esserci almeno un profilo utente visibile.</target> + <trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve"> + <source>The uploaded database archive will be permanently removed from the servers.</source> + <target>L'archivio del database caricato verrà rimosso definitivamente dai server.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Themes" xml:space="preserve"> + <source>Themes</source> + <target>Temi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="These conditions will also apply for: **%@**." xml:space="preserve"> + <source>These conditions will also apply for: **%@**.</source> + <target>Queste condizioni si applicheranno anche per: **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> @@ -5070,16 +7833,36 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.</ta <target>Questa azione non può essere annullata: i messaggi inviati e ricevuti prima di quanto selezionato verranno eliminati. Potrebbe richiedere diversi minuti.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</source> + <target>Questa azione non è reversibile: i messaggi inviati e ricevuti in questa chat prima della selezione verranno eliminati.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> <target>Questa azione non può essere annullata: il tuo profilo, i contatti, i messaggi e i file andranno persi in modo irreversibile.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by end-to-end encryption.</source> + <target>Questa chat è protetta da crittografia end-to-end.</target> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by quantum resistant end-to-end encryption.</source> + <target>Questa chat è protetta da crittografia end-to-end resistente alla quantistica.</target> + <note>E2EE info chat item</note> + </trans-unit> <trans-unit id="This device name" xml:space="preserve"> <source>This device name</source> <target>Il nome di questo dispositivo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve"> + <source>This display name is invalid. Please choose another name.</source> + <target>Questo nome da mostrare non è valido. Scegline un altro.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> <source>This group has over %lld members, delivery receipts are not sent.</source> <target>Questo gruppo ha più di %lld membri, le ricevute di consegna non vengono inviate.</target> @@ -5100,11 +7883,31 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.</ta <target>Questo è il tuo link una tantum!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve"> + <source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source> + <target>Questo link richiede una versione più recente dell'app. Aggiornala o chiedi al tuo contatto di inviare un link compatibile.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve"> + <source>This link was used with another mobile device, please create a new link on the desktop.</source> + <target>Questo link è stato usato con un altro dispositivo mobile, creane uno nuovo sul desktop.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This message was deleted or not received yet." xml:space="preserve"> + <source>This message was deleted or not received yet.</source> + <target>Questo messaggio è stato eliminato o non ancora ricevuto.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <source>This setting applies to messages in your current chat profile **%@**.</source> <target>Questa impostazione si applica ai messaggi del profilo di chat attuale **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Title" xml:space="preserve"> + <source>Title</source> + <target>Titoli</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> <source>To ask any questions and to receive updates:</source> <target>Per porre domande e ricevere aggiornamenti:</target> @@ -5125,9 +7928,9 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.</ta <target>Per creare una nuova connessione</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <target>Per proteggere la privacy, invece degli ID utente utilizzati da tutte le altre piattaforme, SimpleX ha identificatori per le code di messaggi, separati per ciascuno dei tuoi contatti.</target> + <trans-unit id="To protect against your link being replaced, you can compare contact security codes." xml:space="preserve"> + <source>To protect against your link being replaced, you can compare contact security codes.</source> + <target>Per proteggerti dalla sostituzione del tuo link, puoi confrontare i codici di sicurezza del contatto.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -5135,6 +7938,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.</ta <target>Per proteggere il fuso orario, i file immagine/vocali usano UTC.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve"> + <source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source> + <target>Per proteggere il tuo indirizzo IP, l'instradamento privato usa i tuoi server SMP per consegnare i messaggi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> @@ -5142,6 +7950,26 @@ You will be prompted to complete authentication before this feature is enabled.< Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzionalità.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> + <target>Per proteggere la privacy, invece degli ID utente utilizzati da tutte le altre piattaforme, SimpleX ha identificatori per le code di messaggi, separati per ciascuno dei tuoi contatti.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To receive" xml:space="preserve"> + <source>To receive</source> + <target>Per ricevere</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve"> + <source>To record speech please grant permission to use Microphone.</source> + <target>Per registrare l'audio, concedi l'autorizzazione di usare il microfono.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record video please grant permission to use Camera." xml:space="preserve"> + <source>To record video please grant permission to use Camera.</source> + <target>Per registrare il video, concedi l'autorizzazione di usare la fotocamera.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> <target>Per registrare un messaggio vocale, concedi l'autorizzazione all'uso del microfono.</target> @@ -5152,26 +7980,61 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio <target>Per rivelare il tuo profilo nascosto, inserisci una password completa in un campo di ricerca nella pagina **I tuoi profili di chat**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To send" xml:space="preserve"> + <source>To send</source> + <target>Per inviare</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> <source>To support instant push notifications the chat database has to be migrated.</source> <target>Per supportare le notifiche push istantanee, il database della chat deve essere migrato.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To use the servers of **%@**, accept conditions of use." xml:space="preserve"> + <source>To use the servers of **%@**, accept conditions of use.</source> + <target>Per usare i server di **%@**, accetta le condizioni d'uso.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> <target>Per verificare la crittografia end-to-end con il tuo contatto, confrontate (o scansionate) il codice sui vostri dispositivi.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle chat list:" xml:space="preserve"> + <source>Toggle chat list:</source> + <target>Cambia l'elenco delle chat:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> <source>Toggle incognito when connecting.</source> <target>Attiva/disattiva l'incognito quando ti colleghi.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Token status: %@." xml:space="preserve"> + <source>Token status: %@.</source> + <target>Stato del token: %@.</target> + <note>token status</note> + </trans-unit> + <trans-unit id="Toolbar opacity" xml:space="preserve"> + <source>Toolbar opacity</source> + <target>Opacità barra degli strumenti</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Total" xml:space="preserve"> + <source>Total</source> + <target>Totale</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>Isolamento del trasporto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Transport sessions" xml:space="preserve"> + <source>Transport sessions</source> + <target>Sessioni di trasporto</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> <target>Tentativo di connessione al server usato per ricevere messaggi da questo contatto (errore: %@).</target> @@ -5182,16 +8045,16 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio <target>Tentativo di connessione al server usato per ricevere messaggi da questo contatto.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Turkish interface" xml:space="preserve"> + <source>Turkish interface</source> + <target>Interfaccia in turco</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> <target>Spegni</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve"> - <source>Turn off notifications?</source> - <target>Spegnere le notifiche?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> <target>Attiva</target> @@ -5207,20 +8070,30 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio <target>Sblocca</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock for all" xml:space="preserve"> + <source>Unblock for all</source> + <target>Sblocca per tutti</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member" xml:space="preserve"> <source>Unblock member</source> <target>Sblocca membro</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock member for all?" xml:space="preserve"> + <source>Unblock member for all?</source> + <target>Sbloccare il membro per tutti?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member?" xml:space="preserve"> <source>Unblock member?</source> <target>Sbloccare il membro?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve"> - <source>Unexpected error: %@</source> - <target>Errore imprevisto: % @</target> - <note>item status description</note> + <trans-unit id="Undelivered messages" xml:space="preserve"> + <source>Undelivered messages</source> + <target>Messaggi non consegnati</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -5230,7 +8103,7 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio <trans-unit id="Unfav." xml:space="preserve"> <source>Unfav.</source> <target>Non pref.</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> @@ -5267,6 +8140,11 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio <target>Errore sconosciuto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unknown servers!" xml:space="preserve"> + <source>Unknown servers!</source> + <target>Server sconosciuti!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> <target>A meno che non utilizzi l'interfaccia di chiamata iOS, attiva la modalità Non disturbare per evitare interruzioni.</target> @@ -5302,11 +8180,21 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> <target>Riattiva notifiche</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> </trans-unit> <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> <target>Non letto</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Unsupported connection link" xml:space="preserve"> + <source>Unsupported connection link</source> + <target>Link di connessione non supportato</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve"> + <source>Up to 100 last messages are sent to new members.</source> + <target>Vengono inviati ai nuovi membri fino a 100 ultimi messaggi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update" xml:space="preserve"> @@ -5314,11 +8202,6 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e <target>Aggiorna</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> - <source>Update .onion hosts setting?</source> - <target>Aggiornare l'impostazione degli host .onion?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> <target>Aggiorna la password del database</target> @@ -5329,9 +8212,14 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e <target>Aggiornare le impostazioni di rete?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve"> - <source>Update transport isolation mode?</source> - <target>Aggiornare la modalità di isolamento del trasporto?</target> + <trans-unit id="Update settings?" xml:space="preserve"> + <source>Update settings?</source> + <target>Aggiornare le impostazioni?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updated conditions" xml:space="preserve"> + <source>Updated conditions</source> + <target>Condizioni aggiornate</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> @@ -5339,31 +8227,71 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e <target>L'aggiornamento delle impostazioni riconnetterà il client a tutti i server.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> - <target>L'aggiornamento di questa impostazione riconnetterà il client a tutti i server.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Upgrade and open chat" xml:space="preserve"> <source>Upgrade and open chat</source> <target>Aggiorna e apri chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Upload errors" xml:space="preserve"> + <source>Upload errors</source> + <target>Errori di invio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload failed" xml:space="preserve"> + <source>Upload failed</source> + <target>Invio fallito</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Upload file" xml:space="preserve"> <source>Upload file</source> <target>Invia file</target> <note>server test step</note> </trans-unit> + <trans-unit id="Uploaded" xml:space="preserve"> + <source>Uploaded</source> + <target>Inviato</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploaded files" xml:space="preserve"> + <source>Uploaded files</source> + <target>File inviati</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploading archive" xml:space="preserve"> + <source>Uploading archive</source> + <target>Invio dell'archivio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use %@" xml:space="preserve"> + <source>Use %@</source> + <target>Usa %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> <target>Usa gli host .onion</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use SOCKS proxy" xml:space="preserve"> + <source>Use SOCKS proxy</source> + <target>Usa proxy SOCKS</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> <target>Usare i server di SimpleX Chat?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use TCP port %@ when no port is specified." xml:space="preserve"> + <source>Use TCP port %@ when no port is specified.</source> + <target>Usa la porta TCP %@ quando non è specificata alcuna porta.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve"> + <source>Use TCP port 443 for preset servers only.</source> + <target>Usa la porta TCP 443 solo per i server preimpostati.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> <target>Usa la chat</target> @@ -5374,6 +8302,16 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e <target>Usa il profilo attuale</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use for files" xml:space="preserve"> + <source>Use for files</source> + <target>Usa per i file</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for messages" xml:space="preserve"> + <source>Use for messages</source> + <target>Usa per i messaggi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Usa per connessioni nuove</target> @@ -5394,19 +8332,59 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e <target>Usa nuovo profilo in incognito</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use only local notifications?" xml:space="preserve"> + <source>Use only local notifications?</source> + <target>Usare solo notifiche locali?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve"> + <source>Use private routing with unknown servers when IP address is not protected.</source> + <target>Usa l'instradamento privato con server sconosciuti quando l'indirizzo IP non è protetto.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers." xml:space="preserve"> + <source>Use private routing with unknown servers.</source> + <target>Usa l'instradamento privato con server sconosciuti.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Usa il server</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> - <target>Profilo utente</target> + <trans-unit id="Use servers" xml:space="preserve"> + <source>Use servers</source> + <target>Usa i server</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> - <target>L'uso di host .onion richiede un fornitore di VPN compatibile.</target> + <trans-unit id="Use short links (BETA)" xml:space="preserve"> + <source>Use short links (BETA)</source> + <target>Usa link brevi (BETA)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app while in the call." xml:space="preserve"> + <source>Use the app while in the call.</source> + <target>Usa l'app mentre sei in chiamata.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app with one hand." xml:space="preserve"> + <source>Use the app with one hand.</source> + <target>Usa l'app con una mano sola.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use web port" xml:space="preserve"> + <source>Use web port</source> + <target>Usa porta web</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="User selection" xml:space="preserve"> + <source>User selection</source> + <target>Selezione utente</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Username" xml:space="preserve"> + <source>Username</source> + <target>Nome utente</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> @@ -5434,6 +8412,16 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e <target>Verifica le connessioni</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Verify database passphrase" xml:space="preserve"> + <source>Verify database passphrase</source> + <target>Verifica password del database</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify passphrase" xml:space="preserve"> + <source>Verify passphrase</source> + <target>Verifica password</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Verify security code" xml:space="preserve"> <source>Verify security code</source> <target>Verifica codice di sicurezza</target> @@ -5469,11 +8457,26 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e <target>Video e file fino a 1 GB</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View conditions" xml:space="preserve"> + <source>View conditions</source> + <target>Vedi le condizioni</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> <target>Vedi codice di sicurezza</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View updated conditions" xml:space="preserve"> + <source>View updated conditions</source> + <target>Vedi le condizioni aggiornate</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Visible history" xml:space="preserve"> + <source>Visible history</source> + <target>Cronologia visibile</target> + <note>chat feature</note> + </trans-unit> <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> <target>Messaggi vocali</target> @@ -5484,11 +8487,16 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e <target>I messaggi vocali sono vietati in questa chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <target>I messaggi vocali sono vietati in questo gruppo.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Voice messages not allowed" xml:space="preserve"> + <source>Voice messages not allowed</source> + <target>Messaggi vocali non consentiti</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> <source>Voice messages prohibited!</source> <target>Messaggi vocali vietati!</target> @@ -5501,6 +8509,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e </trans-unit> <trans-unit id="Waiting for desktop..." xml:space="preserve"> <source>Waiting for desktop...</source> + <target>In attesa del desktop...</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Waiting for file" xml:space="preserve"> @@ -5518,6 +8527,21 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e <target>In attesa del video</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wallpaper accent" xml:space="preserve"> + <source>Wallpaper accent</source> + <target>Tinta dello sfondo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper background" xml:space="preserve"> + <source>Wallpaper background</source> + <target>Retro dello sfondo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve"> + <source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source> + <target>Attenzione: avviare la chat su più dispositivi non è supportato e provocherà problemi di recapito dei messaggi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> <source>Warning: you may lose some data!</source> <target>Attenzione: potresti perdere alcuni dati!</target> @@ -5538,6 +8562,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e <target>Messaggio di benvenuto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Welcome message is too long" xml:space="preserve"> + <source>Welcome message is too long</source> + <target>Il messaggio di benvenuto è troppo lungo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="What's new" xml:space="preserve"> <source>What's new</source> <target>Novità</target> @@ -5548,9 +8577,14 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e <target>Quando disponibili</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> - <source>When people request to connect, you can accept or reject it.</source> - <target>Quando le persone chiedono di connettersi, puoi accettare o rifiutare.</target> + <trans-unit id="When connecting audio and video calls." xml:space="preserve"> + <source>When connecting audio and video calls.</source> + <target>Quando si connettono le chiamate audio e video.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When more than one operator is enabled, none of them has metadata to learn who communicates with whom." xml:space="preserve"> + <source>When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</source> + <target>Quando più di un operatore è attivato, nessuno di essi ha metadati per scoprire chi comunica con chi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> @@ -5558,29 +8592,74 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e <target>Quando condividi un profilo in incognito con qualcuno, questo profilo verrà utilizzato per i gruppi a cui ti invitano.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="WiFi" xml:space="preserve"> + <source>WiFi</source> + <target>WiFi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Will be enabled in direct chats!" xml:space="preserve"> + <source>Will be enabled in direct chats!</source> + <target>Viene attivata nelle chat dirette!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wired ethernet" xml:space="preserve"> + <source>Wired ethernet</source> + <target>Cavo ethernet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With encrypted files and media." xml:space="preserve"> + <source>With encrypted files and media.</source> + <target>Con file e multimediali criptati.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> <target>Con messaggio di benvenuto facoltativo.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="With reduced battery usage." xml:space="preserve"> + <source>With reduced battery usage.</source> + <target>Con consumo di batteria ridotto.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to file servers.</source> + <target>Senza Tor o VPN, il tuo indirizzo IP sarà visibile ai server di file.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source> + <target>Senza Tor o VPN, il tuo indirizzo IP sarà visibile a questi relay XFTP: %@.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> <target>Password del database sbagliata</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve"> + <source>Wrong key or unknown connection - most likely this connection is deleted.</source> + <target>Chiave sbagliata o connessione sconosciuta - molto probabilmente questa connessione è stata eliminata.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Wrong key or unknown file chunk address - most likely file is deleted." xml:space="preserve"> + <source>Wrong key or unknown file chunk address - most likely file is deleted.</source> + <target>Chiave sbagliata o indirizzo sconosciuto per frammento del file - probabilmente il file è stato eliminato.</target> + <note>file error text</note> + </trans-unit> <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> <target>Password sbagliata!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="XFTP servers" xml:space="preserve"> - <source>XFTP servers</source> + <trans-unit id="XFTP server" xml:space="preserve"> + <source>XFTP server</source> <target>Server XFTP</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> - <target>Tu</target> + <trans-unit id="You **must not** use the same database on two devices." xml:space="preserve"> + <source>You **must not** use the same database on two devices.</source> + <target>**Non devi** usare lo stesso database su due dispositivi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You accepted connection" xml:space="preserve"> @@ -5603,6 +8682,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e <target>Sei già connesso/a a %@.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are already connected with %@." xml:space="preserve"> + <source>You are already connected with %@.</source> + <target>Sei già connesso/a con %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You are already connecting to %@." xml:space="preserve"> <source>You are already connecting to %@.</source> <target>Ti stai già connettendo a %@.</target> @@ -5650,14 +8734,24 @@ Ripetere la richiesta di ingresso?</target> <target>Sei stato/a invitato/a al gruppo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are not connected to these servers. Private routing is used to deliver messages to them." xml:space="preserve"> + <source>You are not connected to these servers. Private routing is used to deliver messages to them.</source> + <target>Non sei connesso/a a questi server. L'instradamento privato è usato per consegnare loro i messaggi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> <source>You can accept calls from lock screen, without device and app authentication.</source> <target>Puoi accettare chiamate dalla schermata di blocco, senza l'autenticazione del dispositivo e dell'app.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <target>Puoi anche connetterti cliccando il link. Se si apre nel browser, clicca il pulsante **Apri nell'app mobile**.</target> + <trans-unit id="You can change it in Appearance settings." xml:space="preserve"> + <source>You can change it in Appearance settings.</source> + <target>Puoi cambiarlo nelle impostazioni dell'aspetto.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can configure servers via settings." xml:space="preserve"> + <source>You can configure servers via settings.</source> + <target>Puoi configurare i server nelle impostazioni.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can create it later" xml:space="preserve"> @@ -5675,16 +8769,36 @@ Ripetere la richiesta di ingresso?</target> <target>Puoi attivarle più tardi nelle impostazioni di privacy e sicurezza dell'app.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can give another try." xml:space="preserve"> + <source>You can give another try.</source> + <target>Puoi fare un altro tentativo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <source>You can hide or mute a user profile - swipe it to the right.</source> <target>Puoi nascondere o silenziare un profilo utente - scorrilo verso destra.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> + <trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve"> + <source>You can make it visible to your SimpleX contacts via Settings.</source> + <target>Puoi renderlo visibile ai tuoi contatti SimpleX nelle impostazioni.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now chat with %@" xml:space="preserve"> + <source>You can now chat with %@</source> <target>Ora puoi inviare messaggi a %@</target> <note>notification body</note> </trans-unit> + <trans-unit id="You can send messages to %@ from Archived contacts." xml:space="preserve"> + <source>You can send messages to %@ from Archived contacts.</source> + <target>Puoi inviare messaggi a %@ dai contatti archiviati.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can set connection name, to remember who the link was shared with." xml:space="preserve"> + <source>You can set connection name, to remember who the link was shared with.</source> + <target>Puoi impostare il nome della connessione per ricordare con chi è stato condiviso il link.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> <target>Puoi impostare l'anteprima della notifica nella schermata di blocco tramite le impostazioni.</target> @@ -5700,16 +8814,16 @@ Ripetere la richiesta di ingresso?</target> <target>Puoi condividere questo indirizzo con i tuoi contatti per consentire loro di connettersi con **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> - <source>You can share your address as a link or QR code - anybody can connect to you.</source> - <target>Puoi condividere il tuo indirizzo come link o come codice QR: chiunque potrà connettersi a te.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> <target>Puoi avviare la chat via Impostazioni / Database o riavviando l'app</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve"> + <source>You can still view conversation with %@ in the list of chats.</source> + <target>Puoi ancora vedere la conversazione con %@ nell'elenco delle chat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> <target>Puoi attivare SimpleX Lock tramite le impostazioni.</target> @@ -5720,21 +8834,26 @@ Ripetere la richiesta di ingresso?</target> <target>Puoi usare il markdown per formattare i messaggi:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can view invitation link again in connection details." xml:space="preserve"> + <source>You can view invitation link again in connection details.</source> + <target>Puoi vedere di nuovo il link di invito nei dettagli di connessione.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> <target>Non puoi inviare messaggi!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <target>Puoi controllare attraverso quale/i server **ricevere** i messaggi, i tuoi contatti – i server che usi per inviare loro i messaggi.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <target>Non è stato possibile verificarti, riprova.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> + <target>Sei tu a decidere chi può connettersi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You have already requested connection via this address!" xml:space="preserve"> <source>You have already requested connection via this address!</source> <target>Hai già richiesto la connessione tramite questo indirizzo!</target> @@ -5747,11 +8866,6 @@ Repeat connection request?</source> Ripetere la richiesta di connessione?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> - <target>Non hai chat</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> <target>Devi inserire la password ogni volta che si avvia l'app: non viene memorizzata sul dispositivo.</target> @@ -5772,11 +8886,26 @@ Ripetere la richiesta di connessione?</target> <target>Sei entrato/a in questo gruppo. Connessione al membro del gruppo invitante.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You may migrate the exported database." xml:space="preserve"> + <source>You may migrate the exported database.</source> + <target>Puoi migrare il database esportato.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may save the exported archive." xml:space="preserve"> + <source>You may save the exported archive.</source> + <target>Puoi salvare l'archivio esportato.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> <target>Devi usare la versione più recente del tuo database della chat SOLO su un dispositivo, altrimenti potresti non ricevere più i messaggi da alcuni contatti.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You need to allow your contact to call to be able to call them." xml:space="preserve"> + <source>You need to allow your contact to call to be able to call them.</source> + <target>Devi consentire le chiamate al tuo contatto per poterlo chiamare.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> <target>Devi consentire al tuo contatto di inviare messaggi vocali per poterli inviare anche tu.</target> @@ -5792,6 +8921,11 @@ Ripetere la richiesta di connessione?</target> <target>Hai inviato un invito al gruppo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You should receive notifications." xml:space="preserve"> + <source>You should receive notifications.</source> + <target>Dovresti ricevere le notifiche.</target> + <note>token info</note> + </trans-unit> <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> <target>Verrai connesso/a al gruppo quando il dispositivo dell'host del gruppo sarà in linea, attendi o controlla più tardi!</target> @@ -5827,6 +8961,11 @@ Ripetere la richiesta di connessione?</target> <target>Continuerai a ricevere chiamate e notifiche da profili silenziati quando sono attivi.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You will stop receiving messages from this chat. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this chat. Chat history will be preserved.</source> + <target>Non riceverai più messaggi da questa chat. La cronologia della chat verrà conservata.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> <target>Non riceverai più messaggi da questo gruppo. La cronologia della chat verrà conservata.</target> @@ -5847,31 +8986,16 @@ Ripetere la richiesta di connessione?</target> <target>Stai usando un profilo in incognito per questo gruppo: per impedire la condivisione del tuo profilo principale non è consentito invitare contatti</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your %@ servers" xml:space="preserve"> - <source>Your %@ servers</source> - <target>I tuoi server %@</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your ICE servers" xml:space="preserve"> <source>Your ICE servers</source> <target>I tuoi server ICE</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> - <target>I tuoi server SMP</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your SimpleX address" xml:space="preserve"> <source>Your SimpleX address</source> <target>Il tuo indirizzo SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your XFTP servers" xml:space="preserve"> - <source>Your XFTP servers</source> - <target>I tuoi server XFTP</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your calls" xml:space="preserve"> <source>Your calls</source> <target>Le tue chiamate</target> @@ -5887,16 +9011,19 @@ Ripetere la richiesta di connessione?</target> <target>Il tuo database della chat non è crittografato: imposta la password per crittografarlo.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your chat preferences" xml:space="preserve"> + <source>Your chat preferences</source> + <target>Le tue preferenze della chat</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>I tuoi profili di chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> - <target>Il tuo contatto deve essere in linea per completare la connessione. -Puoi annullare questa connessione e rimuovere il contatto (e riprovare più tardi con un link nuovo).</target> + <trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve"> + <source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source> + <target>La tua connessione è stata spostata a %@, ma si è verificato un errore imprevisto durante il reindirizzamento al profilo.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> @@ -5909,18 +9036,16 @@ Puoi annullare questa connessione e rimuovere il contatto (e riprovare più tard <target>I tuoi contatti possono consentire l'eliminazione completa dei messaggi.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> - <source>Your contacts in SimpleX will see it. -You can change it in Settings.</source> - <target>I tuoi contatti in SimpleX lo vedranno. -Puoi modificarlo nelle impostazioni.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your contacts will remain connected." xml:space="preserve"> <source>Your contacts will remain connected.</source> <target>I tuoi contatti resteranno connessi.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve"> + <source>Your credentials may be sent unencrypted.</source> + <target>Le credenziali potrebbero essere inviate in chiaro.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> <target>Il tuo attuale database della chat verrà ELIMINATO e SOSTITUITO con quello importato.</target> @@ -5948,36 +9073,39 @@ Puoi modificarlo nelle impostazioni.</target> </trans-unit> <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> <source>Your profile **%@** will be shared.</source> - <target>Il tuo profilo **%@** verrà condiviso.</target> + <target>Verrà condiviso il tuo profilo **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> - <target>Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti. -I server di SimpleX non possono vedere il tuo profilo.</target> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> + <target>Il profilo è condiviso solo con i tuoi contatti.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> - <target>Il tuo profilo, i contatti e i messaggi recapitati sono memorizzati sul tuo dispositivo.</target> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> + <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> + <target>Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti. I server di SimpleX non possono vedere il tuo profilo.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve"> + <source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source> + <target>Il tuo profilo è stato cambiato. Se lo salvi, il profilo aggiornato verrà inviato a tutti i tuoi contatti.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> <target>Il tuo profilo casuale</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> - <target>Il tuo server</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> <target>L'indirizzo del tuo server</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your servers" xml:space="preserve"> + <source>Your servers</source> + <target>I tuoi server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your settings" xml:space="preserve"> <source>Your settings</source> <target>Le tue impostazioni</target> @@ -5995,7 +9123,7 @@ I server di SimpleX non possono vedere il tuo profilo.</target> </trans-unit> <trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> <source>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</source> - <target>[Stella su GitHub](https://github.com/simplex-chat/simplex-chat)</target> + <target>[Dai una stella su GitHub](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="_italic_" xml:space="preserve"> @@ -6018,11 +9146,21 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>chiamata accettata</target> <note>call status</note> </trans-unit> + <trans-unit id="accepted invitation" xml:space="preserve"> + <source>accepted invitation</source> + <target>invito accettato</target> + <note>chat list item title</note> + </trans-unit> <trans-unit id="admin" xml:space="preserve"> <source>admin</source> <target>amministratore</target> <note>member role</note> </trans-unit> + <trans-unit id="admins" xml:space="preserve"> + <source>admins</source> + <target>amministratori</target> + <note>feature role</note> + </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> <target>concordando la crittografia per %@…</target> @@ -6033,6 +9171,11 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>concordando la crittografia…</target> <note>chat item text</note> </trans-unit> + <trans-unit id="all members" xml:space="preserve"> + <source>all members</source> + <target>tutti i membri</target> + <note>feature role</note> + </trans-unit> <trans-unit id="always" xml:space="preserve"> <source>always</source> <target>sempre</target> @@ -6043,6 +9186,16 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>e altri %lld eventi</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="archived report" xml:space="preserve"> + <source>archived report</source> + <target>segnalazione archiviata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="attempts" xml:space="preserve"> + <source>attempts</source> + <target>tentativi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <source>audio call (not e2e encrypted)</source> <target>chiamata audio (non crittografata e2e)</target> @@ -6050,6 +9203,7 @@ I server di SimpleX non possono vedere il tuo profilo.</target> </trans-unit> <trans-unit id="author" xml:space="preserve"> <source>author</source> + <target>autore</target> <note>member role</note> </trans-unit> <trans-unit id="bad message ID" xml:space="preserve"> @@ -6065,13 +9219,29 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <trans-unit id="blocked" xml:space="preserve"> <source>blocked</source> <target>bloccato</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="blocked %@" xml:space="preserve"> + <source>blocked %@</source> + <target>ha bloccato %@</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="blocked by admin" xml:space="preserve"> + <source>blocked by admin</source> + <target>bloccato dall'amministratore</target> + <note>blocked chat item +marked deleted chat item preview text</note> </trans-unit> <trans-unit id="bold" xml:space="preserve"> <source>bold</source> <target>grassetto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="call" xml:space="preserve"> + <source>call</source> + <target>chiama</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="call error" xml:space="preserve"> <source>call error</source> <target>errore di chiamata</target> @@ -6104,7 +9274,7 @@ I server di SimpleX non possono vedere il tuo profilo.</target> </trans-unit> <trans-unit id="changed your role to %@" xml:space="preserve"> <source>changed your role to %@</source> - <target>cambiato il tuo ruolo in %@</target> + <target>ha cambiato il tuo ruolo in %@</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="changing address for %@…" xml:space="preserve"> @@ -6175,7 +9345,7 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> <target>in connessione…</target> - <note>chat list item title</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> @@ -6187,6 +9357,11 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>connessione:% @</target> <note>connection information</note> </trans-unit> + <trans-unit id="contact %@ changed to %@" xml:space="preserve"> + <source>contact %1$@ changed to %2$@</source> + <target>contatto %1$@ cambiato in %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> <target>il contatto ha la crittografia e2e</target> @@ -6217,10 +9392,16 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>giorni</target> <note>time unit</note> </trans-unit> + <trans-unit id="decryption errors" xml:space="preserve"> + <source>decryption errors</source> + <target>errori di decifrazione</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> <target>predefinito (%@)</target> - <note>pref value</note> + <note>delete after time +pref value</note> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> @@ -6267,6 +9448,11 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>messaggio duplicato</target> <note>integrity error chat item</note> </trans-unit> + <trans-unit id="duplicates" xml:space="preserve"> + <source>duplicates</source> + <target>doppi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> <target>crittografato e2e</target> @@ -6342,9 +9528,14 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>errore</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="event happened" xml:space="preserve"> - <source>event happened</source> - <target>evento accaduto</target> + <trans-unit id="expired" xml:space="preserve"> + <source>expired</source> + <target>scaduto</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="forwarded" xml:space="preserve"> + <source>forwarded</source> + <target>inoltrato</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> @@ -6372,6 +9563,11 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>Il portachiavi di iOS verrà usato per archiviare in modo sicuro la password dopo il riavvio dell'app o la modifica della password; consentirà di ricevere notifiche push.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="inactive" xml:space="preserve"> + <source>inactive</source> + <target>inattivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> <target>incognito via link indirizzo del contatto</target> @@ -6412,6 +9608,11 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>invito al gruppo %@</target> <note>group name</note> </trans-unit> + <trans-unit id="invite" xml:space="preserve"> + <source>invite</source> + <target>invita</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> <target>ha invitato</target> @@ -6457,11 +9658,21 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>membro</target> <note>member role</note> </trans-unit> + <trans-unit id="member %@ changed to %@" xml:space="preserve"> + <source>member %1$@ changed to %2$@</source> + <target>il membro %1$@ è diventato %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> <target>si è connesso/a</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="message" xml:space="preserve"> + <source>message</source> + <target>messaggio</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="message received" xml:space="preserve"> <source>message received</source> <target>messaggio ricevuto</target> @@ -6485,7 +9696,12 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <trans-unit id="moderated by %@" xml:space="preserve"> <source>moderated by %@</source> <target>moderato da %@</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="moderator" xml:space="preserve"> + <source>moderator</source> + <target>moderatore</target> + <note>member role</note> </trans-unit> <trans-unit id="months" xml:space="preserve"> <source>months</source> @@ -6495,7 +9711,7 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <trans-unit id="never" xml:space="preserve"> <source>never</source> <target>mai</target> - <note>No comment provided by engineer.</note> + <note>delete after time</note> </trans-unit> <trans-unit id="new message" xml:space="preserve"> <source>new message</source> @@ -6526,8 +9742,8 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <source>off</source> <target>off</target> <note>enabled status - group pref value - time to disappear</note> +group pref value +time to disappear</note> </trans-unit> <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> @@ -6544,16 +9760,46 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>on</target> <note>group pref value</note> </trans-unit> + <trans-unit id="other" xml:space="preserve"> + <source>other</source> + <target>altro</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="other errors" xml:space="preserve"> + <source>other errors</source> + <target>altri errori</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="owner" xml:space="preserve"> <source>owner</source> <target>proprietario</target> <note>member role</note> </trans-unit> + <trans-unit id="owners" xml:space="preserve"> + <source>owners</source> + <target>proprietari</target> + <note>feature role</note> + </trans-unit> <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> <target>peer-to-peer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="pending" xml:space="preserve"> + <source>pending</source> + <target>in attesa</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="pending approval" xml:space="preserve"> + <source>pending approval</source> + <target>in attesa di approvazione</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="quantum resistant e2e encryption" xml:space="preserve"> + <source>quantum resistant e2e encryption</source> + <target>crittografia e2e resistente alla quantistica</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> <target>risposta ricevuta…</target> @@ -6564,6 +9810,11 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>conferma ricevuta…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="rejected" xml:space="preserve"> + <source>rejected</source> + <target>rifiutato</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> <target>chiamata rifiutata</target> @@ -6579,11 +9830,41 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>ha rimosso %@</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="removed contact address" xml:space="preserve"> + <source>removed contact address</source> + <target>indirizzo di contatto rimosso</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="removed profile picture" xml:space="preserve"> + <source>removed profile picture</source> + <target>immagine del profilo rimossa</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> <target>ti ha rimosso/a</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="requested to connect" xml:space="preserve"> + <source>requested to connect</source> + <target>richiesto di connettersi</target> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="saved" xml:space="preserve"> + <source>saved</source> + <target>salvato</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="saved from %@" xml:space="preserve"> + <source>saved from %@</source> + <target>salvato da %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="search" xml:space="preserve"> + <source>search</source> + <target>cerca</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="sec" xml:space="preserve"> <source>sec</source> <target>sec</target> @@ -6609,6 +9890,30 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>invia messaggio diretto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve"> + <source>server queue info: %1$@ + +last received msg: %2$@</source> + <target>info coda server: %1$@ + +ultimo msg ricevuto: %2$@</target> + <note>queue info</note> + </trans-unit> + <trans-unit id="set new contact address" xml:space="preserve"> + <source>set new contact address</source> + <target>impostato nuovo indirizzo di contatto</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="set new profile picture" xml:space="preserve"> + <source>set new profile picture</source> + <target>ha impostato una nuova immagine del profilo</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="standard end-to-end encryption" xml:space="preserve"> + <source>standard end-to-end encryption</source> + <target>crittografia end-to-end standard</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>avvio…</target> @@ -6624,16 +9929,41 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>questo contatto</target> <note>notification title</note> </trans-unit> + <trans-unit id="unblocked %@" xml:space="preserve"> + <source>unblocked %@</source> + <target>ha sbloccato %@</target> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="unknown" xml:space="preserve"> <source>unknown</source> <target>sconosciuto</target> <note>connection info</note> </trans-unit> + <trans-unit id="unknown servers" xml:space="preserve"> + <source>unknown servers</source> + <target>relay sconosciuti</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unknown status" xml:space="preserve"> + <source>unknown status</source> + <target>stato sconosciuto</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unprotected" xml:space="preserve"> + <source>unprotected</source> + <target>non protetto</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="updated group profile" xml:space="preserve"> <source>updated group profile</source> - <target>profilo del gruppo aggiornato</target> + <target>ha aggiornato il profilo del gruppo</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="updated profile" xml:space="preserve"> + <source>updated profile</source> + <target>profilo aggiornato</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="v%@" xml:space="preserve"> <source>v%@</source> <target>v%@</target> @@ -6664,6 +9994,11 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>via relay</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="video" xml:space="preserve"> + <source>video</source> + <target>video</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> <source>video call (not e2e encrypted)</source> <target>videochiamata (non crittografata e2e)</target> @@ -6689,11 +10024,21 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>settimane</target> <note>time unit</note> </trans-unit> + <trans-unit id="when IP hidden" xml:space="preserve"> + <source>when IP hidden</source> + <target>quando l'IP è nascosto</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="yes" xml:space="preserve"> <source>yes</source> <target>sì</target> <note>pref value</note> </trans-unit> + <trans-unit id="you" xml:space="preserve"> + <source>you</source> + <target>tu</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> <target>sei stato/a invitato/a al gruppo</target> @@ -6704,6 +10049,11 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>sei un osservatore</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="you blocked %@" xml:space="preserve"> + <source>you blocked %@</source> + <target>hai bloccato %@</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> <target>hai cambiato indirizzo</target> @@ -6744,6 +10094,11 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>hai condiviso un link incognito una tantum</target> <note>chat list item description</note> </trans-unit> + <trans-unit id="you unblocked %@" xml:space="preserve"> + <source>you unblocked %@</source> + <target>hai sbloccato %@</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you: " xml:space="preserve"> <source>you: </source> <target>tu: </target> @@ -6758,7 +10113,7 @@ I server di SimpleX non possono vedere il tuo profilo.</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="it" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6795,7 +10150,7 @@ I server di SimpleX non possono vedere il tuo profilo.</target> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="it" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> @@ -6815,4 +10170,250 @@ I server di SimpleX non possono vedere il tuo profilo.</target> </trans-unit> </body> </file> + <file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="it" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%d new events" xml:space="preserve"> + <source>%d new events</source> + <target>%d nuovi eventi</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="From %d chat(s)" xml:space="preserve"> + <source>From %d chat(s)</source> + <target>Da %d chat</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="From: %@" xml:space="preserve"> + <source>From: %@</source> + <target>Da: %@</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>Nuovi eventi</target> + <note>notification</note> + </trans-unit> + <trans-unit id="New messages" xml:space="preserve"> + <source>New messages</source> + <target>Nuovi messaggi</target> + <note>notification</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="it" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle display name</note> + </trans-unit> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> + <source>Copyright © 2024 SimpleX Chat. All rights reserved.</source> + <target>Copyright © 2024 SimpleX Chat. Tutti i diritti riservati.</target> + <note>Copyright (human-readable)</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="it" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <target>%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App is locked!" xml:space="preserve"> + <source>App is locked!</source> + <target>L'app è bloccata!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <target>Annulla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <target>Impossibile accedere al portachiavi per salvare la password del database</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Impossibile inoltrare il messaggio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Comment" xml:space="preserve"> + <source>Comment</source> + <target>Commento</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <target>Attualmente la dimensione massima supportata è di %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade required" xml:space="preserve"> + <source>Database downgrade required</source> + <target>Downgrade del database necessario</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <target>Database crittografato!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <target>Errore del database</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <target>La password del database è diversa da quella salvata nel portachiavi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <target>La password del database è necessaria per aprire la chat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade required" xml:space="preserve"> + <source>Database upgrade required</source> + <target>Aggiornamento del database necessario</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing file" xml:space="preserve"> + <source>Error preparing file</source> + <target>Errore nella preparazione del file</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing message" xml:space="preserve"> + <source>Error preparing message</source> + <target>Errore nella preparazione del messaggio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <target>Errore: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Errore del file</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <target>Versione del database incompatibile</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Conferma di migrazione non valida</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <target>Errore del portachiavi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <target>File grande!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No active profile" xml:space="preserve"> + <source>No active profile</source> + <target>Nessun profilo attivo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <target>Ok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to downgrade the database." xml:space="preserve"> + <source>Open the app to downgrade the database.</source> + <target>Apri l'app per eseguire il downgrade del database.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to upgrade the database." xml:space="preserve"> + <source>Open the app to upgrade the database.</source> + <target>Apri l'app per aggiornare il database.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passphrase" xml:space="preserve"> + <source>Passphrase</source> + <target>Password</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve"> + <source>Please create a profile in the SimpleX app</source> + <target>Crea un profilo nell'app SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Le preferenze della chat selezionata vietano questo messaggio.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending a message takes longer than expected." xml:space="preserve"> + <source>Sending a message takes longer than expected.</source> + <target>L'invio di un messaggio richiede più tempo del previsto.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending message…" xml:space="preserve"> + <source>Sending message…</source> + <target>Invio messaggio…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <target>Condividi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Slow network?" xml:space="preserve"> + <source>Slow network?</source> + <target>Rete lenta?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <target>Errore del database sconosciuto: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unsupported format" xml:space="preserve"> + <source>Unsupported format</source> + <target>Formato non supportato</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wait" xml:space="preserve"> + <source>Wait</source> + <target>Attendi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <target>Password del database sbagliata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve"> + <source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source> + <target>Puoi consentire la condivisione in Privacy e sicurezza / impostazioni di SimpleX Lock.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + </body> + </file> </xliff> diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/it.xcloc/contents.json b/apps/ios/SimpleX Localizations/it.xcloc/contents.json index 2ad653d36f..a42f254bd9 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/it.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "it", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "16.2" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 11ca09ba3b..27134216a7 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -2,36 +2,9 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="ja" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> - <trans-unit id=" " xml:space="preserve"> - <source> -</source> - <target> -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (" xml:space="preserve"> - <source> (</source> - <target> (</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> <target> (コピー可能)</target> @@ -74,7 +47,7 @@ </trans-unit> <trans-unit id="%@ (current)" xml:space="preserve"> <source>%@ (current)</source> - <target>%@ (現在)</target> + <target>%@ (現在)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ (current):" xml:space="preserve"> @@ -89,6 +62,7 @@ </trans-unit> <trans-unit id="%@ and %@" xml:space="preserve"> <source>%@ and %@</source> + <target>%@ と %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ and %@ connected" xml:space="preserve"> @@ -103,6 +77,12 @@ </trans-unit> <trans-unit id="%@ connected" xml:space="preserve"> <source>%@ connected</source> + <target>%@ 接続中</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve"> + <source>%@ downloaded</source> + <target>%@ ダウンロード済</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ is connected!" xml:space="preserve"> @@ -120,18 +100,34 @@ <target>%@ は検証されています</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ server" xml:space="preserve"> + <source>%@ server</source> + <target>%@ サーバー</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ servers" xml:space="preserve"> <source>%@ servers</source> <target>%@ サーバー</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve"> + <source>%@ uploaded</source> + <target>%@ アップロード済</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ wants to connect!" xml:space="preserve"> <source>%@ wants to connect!</source> <target>%@ が接続を希望しています!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve"> + <source>%1$@, %2$@</source> + <target>%1$@, %2$@</target> + <note>format for date separator in chat</note> + </trans-unit> <trans-unit id="%@, %@ and %lld members" xml:space="preserve"> <source>%@, %@ and %lld members</source> + <target>%@や%@など%lld人のメンバー</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> @@ -149,11 +145,36 @@ <target>%d 日</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve"> + <source>%d file(s) are still being downloaded.</source> + <target>%d 個のファイルをダウンロードしています。</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve"> + <source>%d file(s) failed to download.</source> + <target>%d 個のファイルがダウンロードに失敗しました。</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve"> + <source>%d file(s) were deleted.</source> + <target>%d 個のファイルが削除されました。</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve"> + <source>%d file(s) were not downloaded.</source> + <target>%d 個のファイルがダウンロードされていません。</target> + <note>forward confirmation reason</note> + </trans-unit> <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> <target>%d 時</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve"> + <source>%d messages not forwarded</source> + <target>%d 個のメッセージが未転送</target> + <note>alert title</note> + </trans-unit> <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> <target>%d 分</target> @@ -169,6 +190,10 @@ <target>%d 秒</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve"> + <source>%d seconds(s)</source> + <note>delete after time</note> + </trans-unit> <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> <target>%d 件のスキップされたメッセージ</target> @@ -181,7 +206,7 @@ </trans-unit> <trans-unit id="%lld" xml:space="preserve"> <source>%lld</source> - <target>%lld</target> + <target/> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld %@" xml:space="preserve"> @@ -201,6 +226,7 @@ </trans-unit> <trans-unit id="%lld group events" xml:space="preserve"> <source>%lld group events</source> + <target>%lld件のグループイベント</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld members" xml:space="preserve"> @@ -210,14 +236,22 @@ </trans-unit> <trans-unit id="%lld messages blocked" xml:space="preserve"> <source>%lld messages blocked</source> + <target>%lld件のメッセージをブロック</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve"> + <source>%lld messages blocked by admin</source> + <target>%lldのメッセージが管理者によりブロック済</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld messages marked deleted" xml:space="preserve"> <source>%lld messages marked deleted</source> + <target>%lld 件のメッセージが削除されました</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld messages moderated by %@" xml:space="preserve"> <source>%lld messages moderated by %@</source> + <target>%@ により%lld 件のメッセージが検閲されました</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld minutes" xml:space="preserve"> @@ -230,11 +264,6 @@ <target>%lldつの新しいインターフェース言語</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> - <source>%lld second(s)</source> - <target>%lld 秒</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="%lld seconds" xml:space="preserve"> <source>%lld seconds</source> <target>%lld 秒</target> @@ -285,47 +314,39 @@ <target>%u 件のメッセージがスキップされました。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> - <source>(</source> - <target>(</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="(new)" xml:space="preserve"> <source>(new)</source> + <target>(新規)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="(this device v%@)" xml:space="preserve"> <source>(this device v%@)</source> + <target>(このデバイス v%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve"> - <source>)</source> - <target>)</target> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target>**コンタクトの追加**: 新しい招待リンクを作成するか、受け取ったリンクから接続します。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target>**新しい連絡先を追加**: 連絡先のワンタイム QR コードまたはリンクを作成します。</target> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve"> + <source>**Create group**: to create a new group.</source> + <target>**グループ作成**: 新しいグループを作成する。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> - <source>**Create link / QR code** for your contact to use.</source> - <target>連絡先が使用する **リンク/QR コードを作成します**。</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**よりプライベート**: 20 分ごとに新しいメッセージを確認します。 デバイス トークンは SimpleX Chat サーバーと共有されますが、連絡先やメッセージの数は共有されません。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**最もプライベート**: SimpleX Chat 通知サーバーを使用せず、バックグラウンドで定期的にメッセージをチェックします (アプリの使用頻度によって異なります)。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <target>**受信したリンク**を貼り付けるか、ブラウザーで開いて [**モバイル アプリで開く**] をタップします。</target> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> + <target>**注意**: 2つの端末で同じデータベースを使用すると、セキュリティ保護として、あなたが接続しているメッセージの復号化が解除されます。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> @@ -333,14 +354,14 @@ <target>**注意**: パスフレーズを紛失すると、パスフレーズを復元または変更できなくなります。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**推奨**: デバイス トークンと通知は SimpleX Chat 通知サーバーに送信されますが、メッセージの内容、サイズ、送信者は送信されません。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target>**QR コードをスキャン**: 直接またはビデオ通話で連絡先に接続します。</target> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve"> + <source>**Scan / Paste link**: to connect via a link you received.</source> + <target>**QRスキャン / リンクの貼り付け**: 受け取ったリンクで接続する。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> @@ -348,30 +369,33 @@ <target>**警告**: 即時の プッシュ通知には、キーチェーンに保存されたパスフレーズが必要です。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve"> + <source>**Warning**: the archive will be removed.</source> + <target>**警告**: アーカイブデータは削除されます。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> <source>**e2e encrypted** audio call</source> - <target>**e2e 暗号化**された音声通話</target> + <target>**エンドツーエンド暗号化済み**の音声通話</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**e2e encrypted** video call" xml:space="preserve"> <source>**e2e encrypted** video call</source> - <target>**エンドツーエンド暗号化済み**のビデオ通話</target> + <target>**エンドツーエンド暗号化済み**の テレビ電話 通話</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="*bold*" xml:space="preserve"> <source>\*bold*</source> - <target>\*太文字*</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=", " xml:space="preserve"> - <source>, </source> - <target>, </target> + <target>\*太字*</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable.</source> + <target>- [ディレクトリサービス](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) に接続 (ベータ)! +- 配信証明を送信する (最大 20 人まで)。 +- より速く、より安定。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> @@ -387,6 +411,9 @@ <source>- optionally notify deleted contacts. - profile names with spaces. - and more!</source> + <target>- 任意で削除された連絡先へ通知します。 +- プロフィール名に空白を含めることができます。 +- and more!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="- voice messages up to 5 minutes. - custom time to disappear. - editing history." xml:space="preserve"> @@ -398,13 +425,9 @@ - 編集履歴。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="." xml:space="preserve"> - <source>.</source> - <target>.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="0 sec" xml:space="preserve"> <source>0 sec</source> + <target>0 秒</target> <note>time to disappear</note> </trans-unit> <trans-unit id="0s" xml:space="preserve"> @@ -415,7 +438,8 @@ <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> <target>1日</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> @@ -430,16 +454,27 @@ <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> <target>1ヶ月</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> <target>1週間</target> - <note>time interval</note> + <note>delete after time +time interval</note> + </trans-unit> + <trans-unit id="1 year" xml:space="preserve"> + <source>1 year</source> + <note>delete after time</note> </trans-unit> <trans-unit id="1-time link" xml:space="preserve"> <source>1-time link</source> - <target>使い捨てのリンク</target> + <target>使い捨てリンク</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> + <target>使い捨てリンクは、*ひとつの連絡先にのみ* 使用できます - 対面または任意のチャットで共有してください。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="5 minutes" xml:space="preserve"> @@ -457,11 +492,6 @@ <target>30秒</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve"> - <source>: </source> - <target>: </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> @@ -511,35 +541,33 @@ <target>アドレス変更を中止しますか?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve"> - <source>About SimpleX</source> - <target>SimpleXについて</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve"> <source>About SimpleX Chat</source> <target>SimpleX Chat について</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve"> - <source>About SimpleX address</source> - <target>SimpleXアドレスについて</target> + <trans-unit id="About operators" xml:space="preserve"> + <source>About operators</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accent color" xml:space="preserve"> - <source>Accent color</source> - <target>アクセントカラー</target> + <trans-unit id="Accent" xml:space="preserve"> + <source>Accent</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept" xml:space="preserve"> <source>Accept</source> <target>承諾</target> <note>accept contact request via notification - accept incoming call via notification</note> +accept incoming call via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accept conditions" xml:space="preserve"> + <source>Accept conditions</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept connection request?" xml:space="preserve"> <source>Accept connection request?</source> - <target>連絡を受け入れる</target> + <target>接続要求を承認?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept contact request from %@?" xml:space="preserve"> @@ -550,16 +578,40 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>シークレットモードで承諾</target> - <note>accept contact request via notification</note> + <note>accept contact request via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve"> + <source>Accepted conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve"> + <source>Acknowledged</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve"> + <source>Acknowledgement errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Active" xml:space="preserve"> + <source>Active</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve"> + <source>Active connections</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> <target>プロフィールにアドレスを追加し、連絡先があなたのアドレスを他の人と共有できるようにします。プロフィールの更新は連絡先に送信されます。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> - <source>Add preset servers</source> - <target>既存サーバを追加</target> + <trans-unit id="Add friends" xml:space="preserve"> + <source>Add friends</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve"> + <source>Add list</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add profile" xml:space="preserve"> @@ -567,14 +619,18 @@ <target>プロフィールを追加</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> + <target>サーバを追加</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> <source>Add servers by scanning QR codes.</source> <target>QRコードでサーバを追加する。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> - <target>サーバを追加…</target> + <trans-unit id="Add team members" xml:space="preserve"> + <source>Add team members</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -582,11 +638,41 @@ <target>別の端末に追加</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add to list" xml:space="preserve"> + <source>Add to list</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> <target>ウェルカムメッセージを追加</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve"> + <source>Add your team members to the conversations.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve"> + <source>Added media & file servers</source> + <target>追加されたメディア & ファイルサーバー</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve"> + <source>Added message servers</source> + <target>追加されたメッセージサーバー</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve"> + <source>Additional accent</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve"> + <source>Additional accent 2</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve"> + <source>Additional secondary</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Address" xml:space="preserve"> <source>Address</source> <target>アドレス</target> @@ -597,6 +683,18 @@ <target>アドレス変更は中止されます。古い受信アドレスが使用されます。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve"> + <source>Address or 1-time link?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve"> + <source>Address settings</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve"> + <source>Admins can block a member for all.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> <source>Admins can create the links to join groups.</source> <target>管理者はグループの参加リンクを生成できます。</target> @@ -607,6 +705,15 @@ <target>ネットワーク詳細設定</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve"> + <source>Advanced settings</source> + <target>詳細設定</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All" xml:space="preserve"> + <source>All</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All app data is deleted." xml:space="preserve"> <source>All app data is deleted.</source> <target>すべてのアプリデータが削除されます。</target> @@ -617,16 +724,32 @@ <target>全チャットとメッセージが削除されます(※元に戻せません※)!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> <target>入力するとすべてのデータが消去されます。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve"> + <source>All data is kept private on your device.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> <target>グループ全員の接続が継続します。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> <target>全てのメッセージが削除されます(※注意:元に戻せません!※)。削除されるのは片方あなたのメッセージのみ。</target> @@ -636,6 +759,19 @@ <source>All new messages from %@ will be hidden!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All profiles" xml:space="preserve"> + <source>All profiles</source> + <target>すべてのプロフィール</target> + <note>profile dropdown</note> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve"> + <source>All reports will be archived for you.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All servers" xml:space="preserve"> + <source>All servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All your contacts will remain connected." xml:space="preserve"> <source>All your contacts will remain connected.</source> <target>あなたの連絡先が繋がったまま継続します。</target> @@ -643,7 +779,12 @@ </trans-unit> <trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve"> <source>All your contacts will remain connected. Profile update will be sent to your contacts.</source> - <target>あなたの連絡先が繋がったまま継続します。</target> + <target>すべての連絡先は維持されます。連絡先に更新されたプロフィールを送信します。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target>すべての連絡先、会話、ファイルは安全に暗号化され、設定されたXFTPリレーに分割でアップロードされます。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow" xml:space="preserve"> @@ -656,14 +797,22 @@ <target>連絡先が通話を許可している場合のみ通話を許可する。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve"> + <source>Allow calls?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> <target>連絡先が許可している場合のみ消えるメッセージを許可する。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <target>送信相手も永久メッセージ削除を許可する時のみに許可する。</target> + <trans-unit id="Allow downgrade" xml:space="preserve"> + <source>Allow downgrade</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target>送信相手も永久メッセージ削除を許可する時のみに許可する。(24時間)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> @@ -686,9 +835,23 @@ <target>消えるメッセージの送信を許可する。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow to irreversibly delete sent messages.</source> - <target>送信済みメッセージの永久削除を許可する。</target> + <trans-unit id="Allow sharing" xml:space="preserve"> + <source>Allow sharing</source> + <target>共有を許可</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target>送信済みメッセージの永久削除を許可する。(24時間)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve"> + <source>Allow to report messsages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve"> + <source>Allow to send SimpleX links.</source> + <target>SimpleXリンクの送信を許可。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send files and media." xml:space="preserve"> @@ -721,9 +884,9 @@ <target>連絡先からの通話を許可する。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow your contacts to irreversibly delete sent messages.</source> - <target>送信相手が永久メッセージ削除するのを許可する。</target> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target>送信相手が永久メッセージ削除するのを許可する。(24時間)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> @@ -743,10 +906,17 @@ </trans-unit> <trans-unit id="Already connecting!" xml:space="preserve"> <source>Already connecting!</source> + <target>既に接続中です!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Already joining the group!" xml:space="preserve"> <source>Already joining the group!</source> + <target>すでにグループに参加しています!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve"> + <source>Always use private routing.</source> + <target>プライベートルーティングを常に使用する。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> @@ -759,21 +929,39 @@ <target>指定された名前の空のチャット プロファイルが作成され、アプリが通常どおり開きます。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Another reason" xml:space="preserve"> + <source>Another reason</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> <target>通話に応答</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> + <target>プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> <target>アプリのビルド: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App data migration" xml:space="preserve"> + <source>App data migration</source> + <target>アプリデータの移行</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> <source>App encrypts new local files (except videos).</source> <target>アプリは新しいローカルファイル(ビデオを除く)を暗号化します。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App group:" xml:space="preserve"> + <source>App group:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>アプリのアイコン</target> @@ -789,6 +977,10 @@ <target>アプリのパスコードは自己破壊パスコードに置き換えられます。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App session" xml:space="preserve"> + <source>App session</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App version" xml:space="preserve"> <source>App version</source> <target>アプリのバージョン</target> @@ -801,7 +993,58 @@ </trans-unit> <trans-unit id="Appearance" xml:space="preserve"> <source>Appearance</source> - <target>見た目</target> + <target>アピアランス</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply" xml:space="preserve"> + <source>Apply</source> + <target>適用</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve"> + <source>Apply to</source> + <target>に適用する</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve"> + <source>Archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve"> + <source>Archive %lld reports?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve"> + <source>Archive all reports?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve"> + <source>Archive and upload</source> + <target>アーカイブとアップロード</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve"> + <source>Archive contacts to chat later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve"> + <source>Archive report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve"> + <source>Archive report?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve"> + <source>Archive reports</source> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve"> + <source>Archived contacts</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve"> + <source>Archiving database</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Attach" xml:space="preserve"> @@ -864,11 +1107,19 @@ <target>画像を自動的に受信</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve"> + <source>Auto-accept settings</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Back" xml:space="preserve"> <source>Back</source> <target>戻る</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Background" xml:space="preserve"> + <source>Background</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Bad desktop address" xml:space="preserve"> <source>Bad desktop address</source> <note>No comment provided by engineer.</note> @@ -883,19 +1134,59 @@ <target>メッセージのハッシュ値問題</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better calls" xml:space="preserve"> + <source>Better calls</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better groups" xml:space="preserve"> <source>Better groups</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve"> + <source>Better groups performance</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve"> + <source>Better message dates.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better messages" xml:space="preserve"> <source>Better messages</source> <target>より良いメッセージ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better networking" xml:space="preserve"> + <source>Better networking</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve"> + <source>Better notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve"> + <source>Better privacy and security</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve"> + <source>Better security ✅</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve"> + <source>Better user experience</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Black" xml:space="preserve"> + <source>Black</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block" xml:space="preserve"> <source>Block</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block for all" xml:space="preserve"> + <source>Block for all</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block group members" xml:space="preserve"> <source>Block group members</source> <note>No comment provided by engineer.</note> @@ -904,18 +1195,34 @@ <source>Block member</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve"> + <source>Block member for all?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block member?" xml:space="preserve"> <source>Block member?</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve"> + <source>Blocked by admin</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve"> + <source>Blur for better privacy.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve"> + <source>Blur media</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <source>Both you and your contact can add message reactions.</source> <target>自分も相手もメッセージへのリアクションを追加できます。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> - <source>Both you and your contact can irreversibly delete sent messages.</source> - <target>あなたと連絡相手が送信済みメッセージを永久削除できます。</target> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target>あなたと連絡相手が送信済みメッセージを永久削除できます。(24時間)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> @@ -938,11 +1245,29 @@ <target>ブルガリア語、フィンランド語、タイ語、ウクライナ語 - ユーザーと [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)に感謝します!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Business address" xml:space="preserve"> + <source>Business address</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve"> + <source>Business chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve"> + <source>Businesses</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>チャット プロファイル経由 (デフォルト) または [接続経由](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve"> + <source>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> <source>Call already ended!</source> <target>通話は既に終了してます!</target> @@ -953,9 +1278,20 @@ <target>通話</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve"> - <source>Can't delete user profile!</source> - <target>ユーザープロフィールが削除できません!</target> + <trans-unit id="Calls prohibited!" xml:space="preserve"> + <source>Calls prohibited!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve"> + <source>Camera not available</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve"> + <source>Can't call contact</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve"> + <source>Can't call member</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contact!" xml:space="preserve"> @@ -968,9 +1304,18 @@ <target>連絡先を招待できません!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve"> + <source>Can't message member</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> <target>中止</target> + <note>alert action +alert button</note> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve"> + <source>Cancel migration</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> @@ -978,9 +1323,21 @@ <target>データベースのパスワードを保存するためのキーチェーンにアクセスできません</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> <target>ファイル受信ができません</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve"> + <source>Cellular</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change" xml:space="preserve"> @@ -988,6 +1345,14 @@ <target>変更</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve"> + <source>Change automatic message deletion?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve"> + <source>Change chat profiles</source> + <note>authentication reason</note> + </trans-unit> <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> <target>データベースのパスフレーズを更新しますか?</target> @@ -1032,11 +1397,22 @@ <source>Change self-destruct passcode</source> <target>自己破壊パスコードを変更する</target> <note>authentication reason - set passcode view</note> +set passcode view</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> - <source>Chat archive</source> - <target>チャットのアーカイブ</target> + <trans-unit id="Chat" xml:space="preserve"> + <source>Chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve"> + <source>Chat already exists</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve"> + <source>Chat already exists!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve"> + <source>Chat colors</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat console" xml:space="preserve"> @@ -1054,6 +1430,10 @@ <target>チャットのデータベースが削除されました</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve"> + <source>Chat database exported</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> <target>チャットのデータベースが読み込まれました</target> @@ -1069,26 +1449,72 @@ <target>チャットが停止してます</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve"> + <source>Chat list</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve"> + <source>Chat migrated!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> <target>チャット設定</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve"> + <source>Chat preferences were changed.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve"> + <source>Chat profile</source> + <target>ユーザープロフィール</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve"> + <source>Chat theme</source> + <target>チャットテーマ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> <target>チャット</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve"> + <source>Check messages every 20 min.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve"> + <source>Check messages when allowed.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> <target>サーバのアドレスを確認してから再度試してください。</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> <target>中国語とスペイン語UI</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> <target>ファイルを選択</target> @@ -1099,10 +1525,24 @@ <target>ライブラリから選択</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve"> + <source>Chunks deleted</source> + <target>チャンクが削除されました</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve"> + <source>Chunks downloaded</source> + <target>チャンクがダウンロードされました</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve"> + <source>Chunks uploaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> <target>消す</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> @@ -1114,16 +1554,37 @@ <target>ダイアログのクリアしますか?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve"> + <source>Clear group?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve"> + <source>Clear or delete group?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve"> + <source>Clear private notes?</source> + <target>プライベートノートを消しますか?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear verification" xml:space="preserve"> <source>Clear verification</source> <target>検証を消す</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> - <source>Colors</source> - <target>色</target> + <trans-unit id="Color chats with the new themes." xml:space="preserve"> + <source>Color chats with the new themes.</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Color mode" xml:space="preserve"> + <source>Color mode</source> + <target>色設定</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve"> + <source>Community guidelines violation</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Compare file" xml:space="preserve"> <source>Compare file</source> <target>ファイルを比較</target> @@ -1134,11 +1595,48 @@ <target>連絡先とセキュリティコードを確認する。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Completed" xml:space="preserve"> + <source>Completed</source> + <target>完了</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve"> + <source>Conditions accepted on: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve"> + <source>Conditions of use</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve"> + <source>Conditions will be accepted on: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> <target>ICEサーバを設定</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Configure server operators" xml:space="preserve"> + <source>Configure server operators</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> <target>確認</target> @@ -1149,11 +1647,23 @@ <target>パスコードを確認</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve"> + <source>Confirm contact deletion?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm database upgrades" xml:space="preserve"> <source>Confirm database upgrades</source> <target>データベースのアップグレードを確認</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve"> + <source>Confirm files from unknown servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve"> + <source>Confirm network settings</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> <target>新しいパスフレーズを確認…</target> @@ -1164,6 +1674,18 @@ <target>パスワードを確認</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve"> + <source>Confirm upload</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve"> + <source>Confirmed</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> <target>接続</target> @@ -1180,6 +1702,12 @@ </trans-unit> <trans-unit id="Connect to desktop" xml:space="preserve"> <source>Connect to desktop</source> + <target>デスクトップに接続</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve"> + <source>Connect to your friends faster.</source> + <target>友達ともっと速くつながりましょう。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect to yourself?" xml:space="preserve"> @@ -1205,26 +1733,38 @@ This is your own one-time link!</source> <target>リンク経由で接続</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve"> - <source>Connect via link / QR code</source> - <target>リンク・QRコード経由で接続</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connect via one-time link" xml:space="preserve"> <source>Connect via one-time link</source> - <target>使い捨てリンク経由で接続しますか?</target> + <target>ワンタイムリンクで接続</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect with %@" xml:space="preserve"> <source>Connect with %@</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected" xml:space="preserve"> + <source>Connected</source> + <target>接続中</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected desktop" xml:space="preserve"> <source>Connected desktop</source> + <target>デスクトップに接続済</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve"> + <source>Connected servers</source> + <target>接続中のサーバ</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connected to desktop" xml:space="preserve"> <source>Connected to desktop</source> + <target>デスクトップに接続済</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connecting" xml:space="preserve"> + <source>Connecting</source> + <target>接続待ち</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> @@ -1237,8 +1777,14 @@ This is your own one-time link!</source> <target>サーバーに接続中… (エラー: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve"> + <source>Connecting to contact, please wait or check later!</source> + <target>連絡先に接続中です。しばらくお待ちいただくか、後で確認してください!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting to desktop" xml:space="preserve"> <source>Connecting to desktop</source> + <target>デスクトップに接続中</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection" xml:space="preserve"> @@ -1246,6 +1792,15 @@ This is your own one-time link!</source> <target>接続</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve"> + <source>Connection and servers status.</source> + <target>接続とサーバーのステータス。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve"> + <source>Connection blocked</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> <target>接続エラー</target> @@ -1256,13 +1811,35 @@ This is your own one-time link!</source> <target>接続エラー (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve"> + <source>Connection is blocked by server operator: +%@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve"> + <source>Connection not ready.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve"> + <source>Connection notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>接続リクエストを送信しました!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve"> + <source>Connection requires encryption renegotiation.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve"> + <source>Connection security</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection terminated" xml:space="preserve"> <source>Connection terminated</source> + <target>接続停止</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection timeout" xml:space="preserve"> @@ -1270,6 +1847,14 @@ This is your own one-time link!</source> <target>接続タイムアウト</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve"> + <source>Connection with desktop stopped</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve"> + <source>Connections</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact allows" xml:space="preserve"> <source>Contact allows</source> <target>連絡先の許可</target> @@ -1280,6 +1865,10 @@ This is your own one-time link!</source> <target>連絡先に既に存在します</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact deleted!" xml:space="preserve"> + <source>Contact deleted!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> <target>連絡先が非表示:</target> @@ -1290,9 +1879,8 @@ This is your own one-time link!</source> <target>連絡先は接続中</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve"> - <source>Contact is not connected yet!</source> - <target>連絡先がまだ繋がってません!</target> + <trans-unit id="Contact is deleted." xml:space="preserve"> + <source>Contact is deleted.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact name" xml:space="preserve"> @@ -1305,6 +1893,10 @@ This is your own one-time link!</source> <target>連絡先の設定</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve"> + <source>Contact will be deleted - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> <target>連絡先</target> @@ -1315,21 +1907,37 @@ This is your own one-time link!</source> <target>連絡先はメッセージを削除対象とすることができます。あなたには閲覧可能です。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve"> + <source>Content violates conditions of use</source> + <note>blocking reason</note> + </trans-unit> <trans-unit id="Continue" xml:space="preserve"> <source>Continue</source> <target>続ける</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve"> + <source>Conversation deleted!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> <target>コピー</target> - <note>chat item action</note> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve"> + <source>Copy error</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> <target>コアのバージョン: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Corner" xml:space="preserve"> + <source>Corner</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Correct name to %@?" xml:space="preserve"> <source>Correct name to %@?</source> <note>No comment provided by engineer.</note> @@ -1339,6 +1947,10 @@ This is your own one-time link!</source> <target>作成</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve"> + <source>Create 1-time link</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> <target>SimpleXアドレスの作成</target> @@ -1348,11 +1960,6 @@ This is your own one-time link!</source> <source>Create a group using a random profile.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> - <source>Create an address to let people connect with you.</source> - <target>人とつながるためのアドレスを作成する。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create file" xml:space="preserve"> <source>Create file</source> <target>ファイルを作成</target> @@ -1372,18 +1979,18 @@ This is your own one-time link!</source> <target>リンクを生成する</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create list" xml:space="preserve"> + <source>Create list</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> <target>[デスクトップアプリ](https://simplex.chat/downloads/)で新しいプロファイルを作成します。 💻</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve"> - <source>Create one-time invitation link</source> - <target>使い捨ての招待リンクを生成する</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create profile" xml:space="preserve"> <source>Create profile</source> + <target>プロフィールを作成する</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create queue" xml:space="preserve"> @@ -1401,9 +2008,24 @@ This is your own one-time link!</source> <target>プロフィールを作成する</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve"> - <source>Created on %@</source> - <target>%@ によって作成されました</target> + <trans-unit id="Created" xml:space="preserve"> + <source>Created</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve"> + <source>Created at</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve"> + <source>Created at: %@</source> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve"> + <source>Creating archive link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve"> + <source>Creating link…</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current Passcode" xml:space="preserve"> @@ -1411,11 +2033,19 @@ This is your own one-time link!</source> <target>現在のパスコード</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> <target>現在の暗証フレーズ…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current profile" xml:space="preserve"> + <source>Current profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> <target>現在サポートされている最大ファイルサイズは %@.</target> @@ -1426,11 +2056,25 @@ This is your own one-time link!</source> <target>カスタム時間</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve"> + <source>Customizable message shape.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve"> + <source>Customize theme</source> + <target>カスタムテーマ</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> <target>ダークモード</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve"> + <source>Dark mode colors</source> + <target>ダークモードカラー</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> <target>データベースID</target> @@ -1529,6 +2173,11 @@ This is your own one-time link!</source> <target>データベースはアプリ再起動時に移行されます</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Debug delivery" xml:space="preserve"> + <source>Debug delivery</source> + <target>配信のデバッグ</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> <target>分散型</target> @@ -1542,17 +2191,17 @@ This is your own one-time link!</source> <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> <target>削除</target> - <note>chat item action</note> + <note>alert action +swipe action</note> + </trans-unit> + <trans-unit id="Delete %lld messages of members?" xml:space="preserve"> + <source>Delete %lld messages of members?</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete %lld messages?" xml:space="preserve"> <source>Delete %lld messages?</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve"> - <source>Delete Contact</source> - <target>連絡先を削除</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Delete address" xml:space="preserve"> <source>Delete address</source> <target>アドレスを削除</target> @@ -1577,14 +2226,12 @@ This is your own one-time link!</source> <source>Delete and notify contact</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve"> - <source>Delete archive</source> - <target>アーカイブを削除</target> + <trans-unit id="Delete chat" xml:space="preserve"> + <source>Delete chat</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve"> - <source>Delete chat archive?</source> - <target>チャットのアーカイブを削除しますか?</target> + <trans-unit id="Delete chat messages from your device." xml:space="preserve"> + <source>Delete chat messages from your device.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile" xml:space="preserve"> @@ -1597,6 +2244,10 @@ This is your own one-time link!</source> <target>チャットのプロフィールを削除しますか?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete chat?" xml:space="preserve"> + <source>Delete chat?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> <source>Delete connection</source> <target>接続を削除する</target> @@ -1607,9 +2258,8 @@ This is your own one-time link!</source> <target>連絡先を削除</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact? This cannot be undone!" xml:space="preserve"> - <source>Delete contact? -This cannot be undone!</source> + <trans-unit id="Delete contact?" xml:space="preserve"> + <source>Delete contact?</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete database" xml:space="preserve"> @@ -1617,6 +2267,10 @@ This cannot be undone!</source> <target>データベースを削除</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete database from this device" xml:space="preserve"> + <source>Delete database from this device</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete file" xml:space="preserve"> <source>Delete file</source> <target>ファイルの削除</target> @@ -1667,6 +2321,10 @@ This cannot be undone!</source> <target>リンクを削除しますか?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete list?" xml:space="preserve"> + <source>Delete list?</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> <target>メンバーのメッセージを削除しますか?</target> @@ -1680,7 +2338,7 @@ This cannot be undone!</source> <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> <target>メッセージを削除</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> @@ -1697,9 +2355,8 @@ This cannot be undone!</source> <target>古いデータベースを削除しますか?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve"> - <source>Delete pending connection</source> - <target>確認待ちの接続を削除</target> + <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> + <source>Delete or moderate up to 200 messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection?" xml:space="preserve"> @@ -1717,11 +2374,27 @@ This cannot be undone!</source> <target>待ち行列を削除</target> <note>server test step</note> </trans-unit> + <trans-unit id="Delete report" xml:space="preserve"> + <source>Delete report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete up to 20 messages at once." xml:space="preserve"> + <source>Delete up to 20 messages at once.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> <target>ユーザープロフィールを削除しますか?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete without notification" xml:space="preserve"> + <source>Delete without notification</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve"> + <source>Deleted</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Deleted at" xml:space="preserve"> <source>Deleted at</source> <target>削除完了</target> @@ -1732,14 +2405,22 @@ This cannot be undone!</source> <target>削除完了: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve"> + <source>Deletion errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivered even when Apple drops them." xml:space="preserve"> + <source>Delivered even when Apple drops them.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery" xml:space="preserve"> <source>Delivery</source> - <target>Delivery</target> + <target>配信</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delivery receipts are disabled!" xml:space="preserve"> <source>Delivery receipts are disabled!</source> - <target>Delivery receipts are disabled!</target> + <target>配信通知の停止!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delivery receipts!" xml:space="preserve"> @@ -1762,6 +2443,27 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Desktop devices" xml:space="preserve"> <source>Desktop devices</source> + <target>デスクトップ機器</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve"> + <source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server error: %@" xml:space="preserve"> + <source>Destination server error: %@</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve"> + <source>Destination server version of %@ is incompatible with forwarding server %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Detailed statistics" xml:space="preserve"> + <source>Detailed statistics</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Details" xml:space="preserve"> + <source>Details</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Develop" xml:space="preserve"> @@ -1769,6 +2471,11 @@ This cannot be undone!</source> <target>開発</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Developer options" xml:space="preserve"> + <source>Developer options</source> + <target>開発者向けの設定</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> <target>開発ツール</target> @@ -1799,8 +2506,12 @@ This cannot be undone!</source> <target>ダイレクトメッセージ</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve"> + <source>Direct messages between members are prohibited in this chat.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> <target>このグループではメンバー間のダイレクトメッセージが使用禁止です。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1814,11 +2525,23 @@ This cannot be undone!</source> <target>SimpleXロックを無効にする</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Disable automatic message deletion?" xml:space="preserve"> + <source>Disable automatic message deletion?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Disable delete messages" xml:space="preserve"> + <source>Disable delete messages</source> + <note>alert button</note> + </trans-unit> <trans-unit id="Disable for all" xml:space="preserve"> <source>Disable for all</source> <target>すべて無効</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Disabled" xml:space="preserve"> + <source>Disabled</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> <source>Disappearing message</source> <target>消えるメッセージ</target> @@ -1834,8 +2557,8 @@ This cannot be undone!</source> <target>このチャットでは消えるメッセージが使用禁止です。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <target>このグループでは消えるメッセージが使用禁止です。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1867,19 +2590,39 @@ This cannot be undone!</source> <source>Discover via local network</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve"> + <source>Do NOT send messages directly, even if your or destination server does not support private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> <source>Do NOT use SimpleX for emergency calls.</source> <target>緊急通報にSimpleXを使用しないでください。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT use private routing." xml:space="preserve"> + <source>Do NOT use private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do it later" xml:space="preserve"> <source>Do it later</source> <target>後で行う</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do not send history to new members." xml:space="preserve"> + <source>Do not send history to new members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not use credentials with proxy." xml:space="preserve"> + <source>Do not use credentials with proxy.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Documents:" xml:space="preserve"> + <source>Documents:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> - <target>アドレスを作成しないでください</target> + <target>アドレスを作成しない</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Don't enable" xml:space="preserve"> @@ -1887,21 +2630,62 @@ This cannot be undone!</source> <target>有効にしない</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Don't miss important messages." xml:space="preserve"> + <source>Don't miss important messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> <target>次から表示しない</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Done" xml:space="preserve"> + <source>Done</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Downgrade and open chat" xml:space="preserve"> <source>Downgrade and open chat</source> <target>ダウングレードしてチャットを開く</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Download" xml:space="preserve"> + <source>Download</source> + <note>alert button +chat item action</note> + </trans-unit> + <trans-unit id="Download errors" xml:space="preserve"> + <source>Download errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download failed" xml:space="preserve"> + <source>Download failed</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Download file" xml:space="preserve"> <source>Download file</source> <target>ファイルをダウンロード</target> <note>server test step</note> </trans-unit> + <trans-unit id="Download files" xml:space="preserve"> + <source>Download files</source> + <note>alert action</note> + </trans-unit> + <trans-unit id="Downloaded" xml:space="preserve"> + <source>Downloaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloaded files" xml:space="preserve"> + <source>Downloaded files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading archive" xml:space="preserve"> + <source>Downloading archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading link details" xml:space="preserve"> + <source>Downloading link details</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> <target>表示の名前が重複してます!</target> @@ -1912,6 +2696,10 @@ This cannot be undone!</source> <target>間隔</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="E2E encrypted notifications." xml:space="preserve"> + <source>E2E encrypted notifications.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> <target>編集する</target> @@ -1932,6 +2720,10 @@ This cannot be undone!</source> <target>有効にする(設定の優先を維持)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve"> + <source>Enable Flux in Network & servers settings for better metadata privacy.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> <target>SimpleXロックを有効にする</target> @@ -1945,6 +2737,10 @@ This cannot be undone!</source> <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> <target>自動メッセージ削除を有効にしますか?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Enable camera access" xml:space="preserve"> + <source>Enable camera access</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable for all" xml:space="preserve"> @@ -1952,6 +2748,10 @@ This cannot be undone!</source> <target>すべて有効</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve"> + <source>Enable in direct chats (BETA)!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> <target>即時通知を有効にしますか?</target> @@ -1982,6 +2782,14 @@ This cannot be undone!</source> <target>自己破壊パスコードを有効にする</target> <note>set passcode view</note> </trans-unit> + <trans-unit id="Enabled" xml:space="preserve"> + <source>Enabled</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve"> + <source>Enabled for</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> <target>暗号化する</target> @@ -2012,6 +2820,10 @@ This cannot be undone!</source> <target>暗号化されたメッセージまたは別のイベント</target> <note>notification</note> </trans-unit> + <trans-unit id="Encrypted message: app is stopped" xml:space="preserve"> + <source>Encrypted message: app is stopped</source> + <note>notification</note> + </trans-unit> <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> <target>暗号化されたメッセージ : データベースエラー</target> @@ -2045,6 +2857,10 @@ This cannot be undone!</source> <source>Encryption re-negotiation failed.</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encryption renegotiation in progress." xml:space="preserve"> + <source>Encryption renegotiation in progress.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter Passcode" xml:space="preserve"> <source>Enter Passcode</source> <target>パスコードを入力</target> @@ -2059,6 +2875,10 @@ This cannot be undone!</source> <source>Enter group name…</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve"> + <source>Enter passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter passphrase…" xml:space="preserve"> <source>Enter passphrase…</source> <target>暗証フレーズを入力…</target> @@ -2102,26 +2922,33 @@ This cannot be undone!</source> <target>アドレス変更中止エラー</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error accepting conditions" xml:space="preserve"> + <source>Error accepting conditions</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> <target>連絡先リクエストの承諾にエラー発生</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve"> - <source>Error accessing database file</source> - <target>データベースファイルへのアクセスエラー</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> <target>メンバー追加にエラー発生</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error adding server" xml:space="preserve"> + <source>Error adding server</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> <target>アドレス変更にエラー発生</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing connection profile" xml:space="preserve"> + <source>Error changing connection profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error changing role" xml:space="preserve"> <source>Error changing role</source> <target>役割変更にエラー発生</target> @@ -2132,6 +2959,18 @@ This cannot be undone!</source> <target>設定変更にエラー発生</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing to incognito!" xml:space="preserve"> + <source>Error changing to incognito!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error checking token status" xml:space="preserve"> + <source>Error checking token status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve"> + <source>Error connecting to forwarding server %@. Please try later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> <target>アドレス作成にエラー発生</target> @@ -2147,16 +2986,28 @@ This cannot be undone!</source> <target>グループリンク生成にエラー発生</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating list" xml:space="preserve"> + <source>Error creating list</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error creating member contact" xml:space="preserve"> <source>Error creating member contact</source> <target>メンバー連絡先の作成中にエラーが発生</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating message" xml:space="preserve"> + <source>Error creating message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>プロフィール作成にエラー発生!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating report" xml:space="preserve"> + <source>Error creating report</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error decrypting file" xml:space="preserve"> <source>Error decrypting file</source> <target>ファイルの復号エラー</target> @@ -2177,11 +3028,6 @@ This cannot be undone!</source> <target>接続の削除エラー</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve"> - <source>Error deleting contact</source> - <target>連絡先の削除にエラー発生</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> <target>データベースの削除にエラー発生</target> @@ -2202,6 +3048,10 @@ This cannot be undone!</source> <target>ユーザのプロフィール削除にエラー発生</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error downloading the archive" xml:space="preserve"> + <source>Error downloading the archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> <source>Error enabling delivery receipts!</source> <note>No comment provided by engineer.</note> @@ -2221,6 +3071,10 @@ This cannot be undone!</source> <target>チャットデータベースのエキスポートにエラー発生</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error exporting theme: %@" xml:space="preserve"> + <source>Error exporting theme: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> <target>チャットデータベースのインポートにエラー発生</target> @@ -2231,24 +3085,46 @@ This cannot be undone!</source> <target>グループ参加にエラー発生</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve"> - <source>Error loading %@ servers</source> - <target>%@ サーバーのロード中にエラーが発生</target> + <trans-unit id="Error loading servers" xml:space="preserve"> + <source>Error loading servers</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error migrating settings" xml:space="preserve"> + <source>Error migrating settings</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error opening chat" xml:space="preserve"> + <source>Error opening chat</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> <target>ファイル受信にエラー発生</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error reconnecting server" xml:space="preserve"> + <source>Error reconnecting server</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error reconnecting servers" xml:space="preserve"> + <source>Error reconnecting servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error registering for notifications" xml:space="preserve"> + <source>Error registering for notifications</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> <target>メンバー除名にエラー発生</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve"> - <source>Error saving %@ servers</source> - <target>%@ サーバの保存エラー</target> + <trans-unit id="Error reordering lists" xml:space="preserve"> + <source>Error reordering lists</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error resetting statistics" xml:space="preserve"> + <source>Error resetting statistics</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving ICE servers" xml:space="preserve"> @@ -2256,6 +3132,10 @@ This cannot be undone!</source> <target>ICEサーバ保存にエラー発生</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving chat list" xml:space="preserve"> + <source>Error saving chat list</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> <target>グループのプロフィール保存にエラー発生</target> @@ -2271,11 +3151,23 @@ This cannot be undone!</source> <target>キーチェーンにパスフレーズを保存にエラー発生</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving servers" xml:space="preserve"> + <source>Error saving servers</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error saving settings" xml:space="preserve"> + <source>Error saving settings</source> + <note>when migrating</note> + </trans-unit> <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> <target>ユーザーパスワード保存エラー</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error scanning code: %@" xml:space="preserve"> + <source>Error scanning code: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending email" xml:space="preserve"> <source>Error sending email</source> <target>メールの送信にエラー発生</target> @@ -2305,16 +3197,24 @@ This cannot be undone!</source> <target>チャット停止にエラー発生</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error switching profile" xml:space="preserve"> + <source>Error switching profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> <target>プロフィール切り替えにエラー発生!</target> - <note>No comment provided by engineer.</note> + <note>alertTitle</note> </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> <target>接続の同期エラー</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error testing server connection" xml:space="preserve"> + <source>Error testing server connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> <target>グループのリンクのアップデートにエラー発生</target> @@ -2325,6 +3225,10 @@ This cannot be undone!</source> <target>メッセージの更新にエラー発生</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error updating server" xml:space="preserve"> + <source>Error updating server</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> <target>設定の更新にエラー発生</target> @@ -2335,6 +3239,14 @@ This cannot be undone!</source> <target>ユーザープライバシーの更新のエラー</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error uploading the archive" xml:space="preserve"> + <source>Error uploading the archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error verifying passphrase:" xml:space="preserve"> + <source>Error verifying passphrase:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error: " xml:space="preserve"> <source>Error: </source> <target>エラー : </target> @@ -2343,7 +3255,9 @@ This cannot be undone!</source> <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> <target>エラー : %@</target> - <note>No comment provided by engineer.</note> + <note>alert message +file error text +snd error text</note> </trans-unit> <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> @@ -2355,6 +3269,14 @@ This cannot be undone!</source> <target>エラー: データベースが存在しません</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Errors" xml:space="preserve"> + <source>Errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors in servers configuration." xml:space="preserve"> + <source>Errors in servers configuration.</source> + <note>servers error</note> + </trans-unit> <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> <source>Even when disabled in the conversation.</source> <target>会話中に無効になっている場合でも。</target> @@ -2369,6 +3291,10 @@ This cannot be undone!</source> <source>Expand</source> <note>chat item action</note> </trans-unit> + <trans-unit id="Expired" xml:space="preserve"> + <source>Expired</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> <target>データベースをエキスポート</target> @@ -2379,11 +3305,19 @@ This cannot be undone!</source> <target>エクスポートエラー:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Export theme" xml:space="preserve"> + <source>Export theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> <target>データベースのアーカイブをエクスポートします。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Exported file doesn't exist" xml:space="preserve"> + <source>Exported file doesn't exist</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exporting database archive…" xml:space="preserve"> <source>Exporting database archive…</source> <target>データベース アーカイブをエクスポートしています…</target> @@ -2399,15 +3333,57 @@ This cannot be undone!</source> <target>送信者がオンラインになるまでの待ち時間がなく、速い!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster deletion of groups." xml:space="preserve"> + <source>Faster deletion of groups.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Faster joining and more reliable messages." xml:space="preserve"> <source>Faster joining and more reliable messages.</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster sending messages." xml:space="preserve"> + <source>Faster sending messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Favorite" xml:space="preserve"> <source>Favorite</source> <target>お気に入り</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Favorites" xml:space="preserve"> + <source>Favorites</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <note>file error alert title</note> + </trans-unit> + <trans-unit id="File errors: %@" xml:space="preserve"> + <source>File errors: +%@</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="File is blocked by server operator: %@." xml:space="preserve"> + <source>File is blocked by server operator: +%@.</source> + <note>file error text</note> + </trans-unit> + <trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve"> + <source>File not found - most likely file was deleted or cancelled.</source> + <note>file error text</note> + </trans-unit> + <trans-unit id="File server error: %@" xml:space="preserve"> + <source>File server error: %@</source> + <note>file error text</note> + </trans-unit> + <trans-unit id="File status" xml:space="preserve"> + <source>File status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File status: %@" xml:space="preserve"> + <source>File status: %@</source> + <note>copied message info</note> + </trans-unit> <trans-unit id="File will be deleted from servers." xml:space="preserve"> <source>File will be deleted from servers.</source> <target>ファイルはサーバーから削除されます。</target> @@ -2428,6 +3404,10 @@ This cannot be undone!</source> <target>ファイル: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files" xml:space="preserve"> + <source>Files</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> <target>ファイルとメディア</target> @@ -2438,11 +3418,15 @@ This cannot be undone!</source> <target>ファイルとメディア</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> - <source>Files and media are prohibited in this group.</source> + <trans-unit id="Files and media are prohibited." xml:space="preserve"> + <source>Files and media are prohibited.</source> <target>このグループでは、ファイルとメディアは禁止されています。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files and media not allowed" xml:space="preserve"> + <source>Files and media not allowed</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files and media prohibited!" xml:space="preserve"> <source>Files and media prohibited!</source> <target>ファイルとメディアは禁止されています!</target> @@ -2453,6 +3437,14 @@ This cannot be undone!</source> <target>未読とお気に入りをフィルターします。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Finalize migration" xml:space="preserve"> + <source>Finalize migration</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration on another device." xml:space="preserve"> + <source>Finalize migration on another device.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> <target>ついに、私たちはそれらを手に入れました! 🚀</target> @@ -2493,11 +3485,93 @@ This cannot be undone!</source> <target>グループメンバーによる修正はサポートされていません</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For all moderators" xml:space="preserve"> + <source>For all moderators</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For chat profile %@:" xml:space="preserve"> + <source>For chat profile %@:</source> + <note>servers error</note> + </trans-unit> <trans-unit id="For console" xml:space="preserve"> <source>For console</source> <target>コンソール</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." xml:space="preserve"> + <source>For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For me" xml:space="preserve"> + <source>For me</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For private routing" xml:space="preserve"> + <source>For private routing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For social media" xml:space="preserve"> + <source>For social media</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward" xml:space="preserve"> + <source>Forward</source> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Forward %d message(s)?" xml:space="preserve"> + <source>Forward %d message(s)?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Forward and save messages" xml:space="preserve"> + <source>Forward and save messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward messages" xml:space="preserve"> + <source>Forward messages</source> + <note>alert action</note> + </trans-unit> + <trans-unit id="Forward messages without files?" xml:space="preserve"> + <source>Forward messages without files?</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> + <source>Forward up to 20 messages at once.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded" xml:space="preserve"> + <source>Forwarded</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded from" xml:space="preserve"> + <source>Forwarded from</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding %lld messages" xml:space="preserve"> + <source>Forwarding %lld messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve"> + <source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server address is incompatible with network settings: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server version is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server version is incompatible with network settings: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Destination server error: %2$@</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Error: %2$@</source> + <note>snd error text</note> + </trans-unit> <trans-unit id="Found desktop" xml:space="preserve"> <source>Found desktop</source> <note>No comment provided by engineer.</note> @@ -2517,11 +3591,6 @@ This cannot be undone!</source> <target>フルネーム (任意):</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full name:" xml:space="preserve"> - <source>Full name:</source> - <target>フルネーム:</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Fully decentralized – visible only to members." xml:space="preserve"> <source>Fully decentralized – visible only to members.</source> <note>No comment provided by engineer.</note> @@ -2541,6 +3610,18 @@ This cannot be undone!</source> <target>GIFとステッカー</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Get notified when mentioned." xml:space="preserve"> + <source>Get notified when mentioned.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Good afternoon!" xml:space="preserve"> + <source>Good afternoon!</source> + <note>message preview</note> + </trans-unit> + <trans-unit id="Good morning!" xml:space="preserve"> + <source>Good morning!</source> + <note>message preview</note> + </trans-unit> <trans-unit id="Group" xml:space="preserve"> <source>Group</source> <target>グループ</target> @@ -2594,36 +3675,6 @@ This cannot be undone!</source> <target>グループのリンク</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve"> - <source>Group members can add message reactions.</source> - <target>グループメンバーはメッセージへのリアクションを追加できます。</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> - <source>Group members can irreversibly delete sent messages.</source> - <target>グループのメンバーがメッセージを完全削除することができます。</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> - <target>グループのメンバーがダイレクトメッセージを送信できます。</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> - <target>グループのメンバーが消えるメッセージを送信できます。</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send files and media." xml:space="preserve"> - <source>Group members can send files and media.</source> - <target>グループメンバーはファイルやメディアを送信できます。</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> - <target>グループのメンバーが音声メッセージを送信できます。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> <target>グループメッセージ:</target> @@ -2664,11 +3715,19 @@ This cannot be undone!</source> <target>あなたにとってグループが削除されます (※元に戻せません※)!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Groups" xml:space="preserve"> + <source>Groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Help" xml:space="preserve"> <source>Help</source> <target>ヘルプ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Help admins moderating their groups." xml:space="preserve"> + <source>Help admins moderating their groups.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> <target>プライベート</target> @@ -2709,15 +3768,26 @@ This cannot be undone!</source> <target>履歴</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="History is not sent to new members." xml:space="preserve"> + <source>History is not sent to new members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> <target>SimpleX の仕組み</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="How it affects privacy" xml:space="preserve"> + <source>How it affects privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it helps privacy" xml:space="preserve"> + <source>How it helps privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> - <target>技術の説明</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="How to" xml:space="preserve"> <source>How to</source> @@ -2734,21 +3804,24 @@ This cannot be undone!</source> <target>自分のサーバの使い方</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Hungarian interface" xml:space="preserve"> + <source>Hungarian interface</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> <target>ICEサーバ (1行に1サーバ)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="IP address" xml:space="preserve"> + <source>IP address</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> <source>If you can't meet in person, show QR code in a video call, or share the link.</source> <target>直接会えない場合は、ビデオ通話で QR コードを表示するか、リンクを共有してください。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <target>直接会えない場合は、**ビデオ通話で QR コードを表示する**か、リンクを共有してください。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> <target>アプリを開くときにこのパスコードを入力すると、アプリのすべてのデータが元に戻せないように削除されます!</target> @@ -2784,8 +3857,8 @@ This cannot be undone!</source> <target>即座に</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <target>スパムや悪質送信を防止</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2804,6 +3877,27 @@ This cannot be undone!</source> <target>データベースを読み込む</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Import failed" xml:space="preserve"> + <source>Import failed</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import theme" xml:space="preserve"> + <source>Import theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Importing archive" xml:space="preserve"> + <source>Importing archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved delivery, reduced traffic usage. More improvements are coming soon!" xml:space="preserve"> + <source>Improved delivery, reduced traffic usage. +More improvements are coming soon!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved message delivery" xml:space="preserve"> + <source>Improved message delivery</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> <target>プライバシーとセキュリティ強化</target> @@ -2814,11 +3908,27 @@ This cannot be undone!</source> <target>サーバ設定の向上</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In order to continue, chat should be stopped." xml:space="preserve"> + <source>In order to continue, chat should be stopped.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>返信先</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In-call sounds" xml:space="preserve"> + <source>In-call sounds</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Inappropriate content" xml:space="preserve"> + <source>Inappropriate content</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Inappropriate profile" xml:space="preserve"> + <source>Inappropriate profile</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> <target>シークレットモード</target> @@ -2887,6 +3997,11 @@ This cannot be undone!</source> <target>インストール [ターミナル用SimpleX Chat](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Instant" xml:space="preserve"> + <source>Instant</source> + <target>即時</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> @@ -2894,29 +4009,68 @@ This cannot be undone!</source> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Instantly" xml:space="preserve"> - <source>Instantly</source> - <target>すぐに</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> <target>インターフェース</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Interface colors" xml:space="preserve"> + <source>Interface colors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid" xml:space="preserve"> + <source>Invalid</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (bad token)" xml:space="preserve"> + <source>Invalid (bad token)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (expired)" xml:space="preserve"> + <source>Invalid (expired)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (unregistered)" xml:space="preserve"> + <source>Invalid (unregistered)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (wrong topic)" xml:space="preserve"> + <source>Invalid (wrong topic)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid QR code" xml:space="preserve"> + <source>Invalid QR code</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> <target>無効な接続リンク</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid display name!" xml:space="preserve"> + <source>Invalid display name!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid link" xml:space="preserve"> + <source>Invalid link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid name!" xml:space="preserve"> <source>Invalid name!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid response" xml:space="preserve"> + <source>Invalid response</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> <target>無効なサーバアドレス!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Invalid status" xml:space="preserve"> <source>Invalid status</source> @@ -2938,6 +4092,10 @@ This cannot be undone!</source> <target>メンバーを招待する</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invite to chat" xml:space="preserve"> + <source>Invite to chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invite to group" xml:space="preserve"> <source>Invite to group</source> <target>グループに招待する</target> @@ -2953,8 +4111,8 @@ This cannot be undone!</source> <target>このチャットではメッセージの完全削除が使用禁止です。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <target>このグループではメッセージの完全削除が使用禁止です。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2979,6 +4137,10 @@ This cannot be undone!</source> 3. 接続に問題があった。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="It protects your IP address and connections." xml:space="preserve"> + <source>It protects your IP address and connections.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> <target>このリンクからすでに接続されているようです。そうでない場合は、エラー(%@)が発生しました。</target> @@ -2997,13 +4159,17 @@ This cannot be undone!</source> <trans-unit id="Join" xml:space="preserve"> <source>Join</source> <target>参加</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> <target>グループに参加</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Join group conversations" xml:space="preserve"> + <source>Join group conversations</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Join group?" xml:space="preserve"> <source>Join group?</source> <note>No comment provided by engineer.</note> @@ -3027,10 +4193,22 @@ This is your link for group %@!</source> <target>グループに参加</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep" xml:space="preserve"> + <source>Keep</source> + <note>alert action</note> + </trans-unit> + <trans-unit id="Keep conversation" xml:space="preserve"> + <source>Keep conversation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Keep the app open to use it from desktop" xml:space="preserve"> <source>Keep the app open to use it from desktop</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep unused invitation?" xml:space="preserve"> + <source>Keep unused invitation?</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> <source>Keep your connections</source> <target>接続を維持</target> @@ -3064,6 +4242,14 @@ This is your link for group %@!</source> <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> <target>脱退</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Leave chat" xml:space="preserve"> + <source>Leave chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave chat?" xml:space="preserve"> + <source>Leave chat?</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group" xml:space="preserve"> @@ -3103,6 +4289,18 @@ This is your link for group %@!</source> <source>Linked desktops</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="List" xml:space="preserve"> + <source>List</source> + <note>swipe action</note> + </trans-unit> + <trans-unit id="List name and emoji should be different for all lists." xml:space="preserve"> + <source>List name and emoji should be different for all lists.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List name..." xml:space="preserve"> + <source>List name...</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Live message!" xml:space="preserve"> <source>Live message!</source> <target>ライブメッセージ!</target> @@ -3133,11 +4331,6 @@ This is your link for group %@!</source> <target>ロックモード</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> - <source>Make a private connection</source> - <target>プライベートな接続をする</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make one message disappear" xml:space="preserve"> <source>Make one message disappear</source> <target>メッセージを1つ消す</target> @@ -3148,21 +4341,11 @@ This is your link for group %@!</source> <target>プロフィールを非表示にできます!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> - <target>%@ サーバー アドレスが正しい形式で、行が区切られており、重複していないことを確認してください (%@)。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> <target>WebRTC ICEサーバのアドレスを正しく1行ずつに分けて、重複しないように、形式もご確認ください。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <target>多くの人が次のような質問をしました: *SimpleX にユーザー識別子がない場合、どうやってメッセージを配信できるのですか?*</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> <source>Mark deleted for everyone</source> <target>全員に対して削除済みマークを付ける</target> @@ -3188,11 +4371,31 @@ This is your link for group %@!</source> <target>最大 30 秒で即時受信します。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Media & file servers" xml:space="preserve"> + <source>Media & file servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Medium" xml:space="preserve"> + <source>Medium</source> + <note>blur media</note> + </trans-unit> <trans-unit id="Member" xml:space="preserve"> <source>Member</source> <target>メンバー</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member inactive" xml:space="preserve"> + <source>Member inactive</source> + <note>item status text</note> + </trans-unit> + <trans-unit id="Member reports" xml:space="preserve"> + <source>Member reports</source> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All chat members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All chat members will be notified.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> <target>メンバーの役割が "%@" に変更されます。 グループメンバー全員に通知されます。</target> @@ -3203,11 +4406,61 @@ This is your link for group %@!</source> <target>メンバーの役割が "%@" に変更されます。 メンバーは新たな招待を受け取ります。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from chat - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> <target>メンバーをグループから除名する (※元に戻せません※)!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Members can add message reactions." xml:space="preserve"> + <source>Members can add message reactions.</source> + <target>グループメンバーはメッセージへのリアクションを追加できます。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Members can irreversibly delete sent messages. (24 hours)</source> + <target>グループのメンバーがメッセージを完全削除することができます。(24時間)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can report messsages to moderators." xml:space="preserve"> + <source>Members can report messsages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send SimpleX links." xml:space="preserve"> + <source>Members can send SimpleX links.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> + <target>グループのメンバーがダイレクトメッセージを送信できます。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> + <target>グループのメンバーが消えるメッセージを送信できます。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send files and media." xml:space="preserve"> + <source>Members can send files and media.</source> + <target>グループメンバーはファイルやメディアを送信できます。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> + <target>グループのメンバーが音声メッセージを送信できます。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mention members 👋" xml:space="preserve"> + <source>Mention members 👋</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Menus" xml:space="preserve"> + <source>Menus</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>メッセージ送信エラー</target> @@ -3217,11 +4470,27 @@ This is your link for group %@!</source> <source>Message delivery receipts!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message delivery warning" xml:space="preserve"> + <source>Message delivery warning</source> + <note>item status text</note> + </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> <target>メッセージの下書き</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message forwarded" xml:space="preserve"> + <source>Message forwarded</source> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message may be delivered later if member becomes active." xml:space="preserve"> + <source>Message may be delivered later if member becomes active.</source> + <note>item status description</note> + </trans-unit> + <trans-unit id="Message queue info" xml:space="preserve"> + <source>Message queue info</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message reactions" xml:space="preserve"> <source>Message reactions</source> <target>メッセージへのリアクション</target> @@ -3232,16 +4501,44 @@ This is your link for group %@!</source> <target>このチャットではメッセージへのリアクションは禁止されています。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> - <source>Message reactions are prohibited in this group.</source> + <trans-unit id="Message reactions are prohibited." xml:space="preserve"> + <source>Message reactions are prohibited.</source> <target>このグループではメッセージへのリアクションは禁止されています。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message reception" xml:space="preserve"> + <source>Message reception</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message servers" xml:space="preserve"> + <source>Message servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message shape" xml:space="preserve"> + <source>Message shape</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message source remains private." xml:space="preserve"> + <source>Message source remains private.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status" xml:space="preserve"> + <source>Message status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status: %@" xml:space="preserve"> + <source>Message status: %@</source> + <note>copied message info</note> + </trans-unit> <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> <target>メッセージ内容</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message too large" xml:space="preserve"> + <source>Message too large</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> <target>メッセージ</target> @@ -3256,11 +4553,66 @@ This is your link for group %@!</source> <source>Messages from %@ will be shown!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Messages in this chat will never be deleted." xml:space="preserve"> + <source>Messages in this chat will never be deleted.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages received" xml:space="preserve"> + <source>Messages received</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages sent" xml:space="preserve"> + <source>Messages sent</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages were deleted after you selected them." xml:space="preserve"> + <source>Messages were deleted after you selected them.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>メッセージ、ファイル、通話は、前方秘匿性、否認可能性および侵入復元性を備えた**エンドツーエンドの暗号化**によって保護されます。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>メッセージ、ファイル、通話は、前方秘匿性、否認可能性および侵入復元性を備えた**耐量子E2E暗号化**によって保護されます。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate device" xml:space="preserve"> + <source>Migrate device</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate from another device" xml:space="preserve"> + <source>Migrate from another device</source> + <target>別の端末から移行</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate here" xml:space="preserve"> + <source>Migrate here</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device" xml:space="preserve"> + <source>Migrate to another device</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device via QR code." xml:space="preserve"> + <source>Migrate to another device via QR code.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating" xml:space="preserve"> + <source>Migrating</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migrating database archive…" xml:space="preserve"> <source>Migrating database archive…</source> <target>データベースのアーカイブを移行しています…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Migration complete" xml:space="preserve"> + <source>Migration complete</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> <target>移行エラー:</target> @@ -3276,9 +4628,9 @@ This is your link for group %@!</source> <target>移行が完了しました</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> - <target>移行: %@</target> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> + <target>移行:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> @@ -3296,21 +4648,28 @@ This is your link for group %@!</source> <target>モデレーターによって介入済み: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="More" xml:space="preserve"> + <source>More</source> + <note>swipe action</note> + </trans-unit> <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> <target>まだまだ改善してまいります!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="More reliable network connection." xml:space="preserve"> + <source>More reliable network connection.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="More reliable notifications" xml:space="preserve"> + <source>More reliable notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> <source>Most likely this connection is deleted.</source> <target>おそらく、この接続は削除されています。</target> <note>item status description</note> </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> - <source>Most likely this contact has deleted the connection with you.</source> - <target>恐らくこの連絡先があなたとの接続を削除しました。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> <target>複数チャットのプロフィール</target> @@ -3319,7 +4678,11 @@ This is your link for group %@!</source> <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> <target>ミュート</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Mute all" xml:space="preserve"> + <source>Mute all</source> + <note>notification label action</note> </trans-unit> <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> @@ -3329,13 +4692,33 @@ This is your link for group %@!</source> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> <target>名前</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> <target>ネットワークとサーバ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Network connection" xml:space="preserve"> + <source>Network connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network decentralization" xml:space="preserve"> + <source>Network decentralization</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve"> + <source>Network issues - message expired after many attempts to send it.</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Network management" xml:space="preserve"> + <source>Network management</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network operator" xml:space="preserve"> + <source>Network operator</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> <target>ネットワーク設定</target> @@ -3346,11 +4729,31 @@ This is your link for group %@!</source> <target>ネットワーク状況</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New" xml:space="preserve"> + <source>New</source> + <note>token status text</note> + </trans-unit> <trans-unit id="New Passcode" xml:space="preserve"> <source>New Passcode</source> <target>新しいパスコード</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve"> + <source>New SOCKS credentials will be used every time you start the app.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve"> + <source>New SOCKS credentials will be used for each server.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat" xml:space="preserve"> + <source>New chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat experience 🎉" xml:space="preserve"> + <source>New chat experience 🎉</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> <target>新しい繋がりのリクエスト</target> @@ -3361,11 +4764,6 @@ This is your link for group %@!</source> <target>新しい連絡先:</target> <note>notification</note> </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> - <source>New database archive</source> - <target>新しいデータベースのアーカイブ</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="New desktop app!" xml:space="preserve"> <source>New desktop app!</source> <target>新しいデスクトップアプリ!</target> @@ -3376,11 +4774,19 @@ This is your link for group %@!</source> <target>新たな表示名</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <note>notification</note> + </trans-unit> <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> <target>%@ の新機能</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New media options" xml:space="preserve"> + <source>New media options</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> <target>新しいメンバーの役割</target> @@ -3396,6 +4802,10 @@ This is your link for group %@!</source> <target>新しいパスフレーズ…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New server" xml:space="preserve"> + <source>New server</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No" xml:space="preserve"> <source>No</source> <target>いいえ</target> @@ -3406,6 +4816,18 @@ This is your link for group %@!</source> <target>アプリのパスワードはありません</target> <note>Authentication unavailable</note> </trans-unit> + <trans-unit id="No chats" xml:space="preserve"> + <source>No chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats found" xml:space="preserve"> + <source>No chats found</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats in list %@" xml:space="preserve"> + <source>No chats in list %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> <target>連絡先が選択されてません</target> @@ -3426,6 +4848,10 @@ This is your link for group %@!</source> <target>デバイストークンがありません!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No direct connection yet, message is forwarded by admin." xml:space="preserve"> + <source>No direct connection yet, message is forwarded by admin.</source> + <note>item status description</note> + </trans-unit> <trans-unit id="No filtered chats" xml:space="preserve"> <source>No filtered chats</source> <target>フィルタされたチャットはありません</target> @@ -3441,20 +4867,94 @@ This is your link for group %@!</source> <target>履歴はありません</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No info, try to reload" xml:space="preserve"> + <source>No info, try to reload</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No media & file servers." xml:space="preserve"> + <source>No media & file servers.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No message" xml:space="preserve"> + <source>No message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No message servers." xml:space="preserve"> + <source>No message servers.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No network connection" xml:space="preserve"> + <source>No network connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record speech" xml:space="preserve"> + <source>No permission to record speech</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record video" xml:space="preserve"> + <source>No permission to record video</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> <source>No permission to record voice message</source> <target>音声メッセージを録音する権限がありません</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No push server" xml:space="preserve"> + <source>No push server</source> + <target>自分のみ</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> <target>送受信済みのファイルがありません</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No servers for private message routing." xml:space="preserve"> + <source>No servers for private message routing.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive files." xml:space="preserve"> + <source>No servers to receive files.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive messages." xml:space="preserve"> + <source>No servers to receive messages.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to send files." xml:space="preserve"> + <source>No servers to send files.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No token!" xml:space="preserve"> + <source>No token!</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="No unread chats" xml:space="preserve"> + <source>No unread chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> + <target>世界初のユーザーIDのないプラットフォーム|設計も元からプライベート。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Not compatible!" xml:space="preserve"> <source>Not compatible!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notes" xml:space="preserve"> + <source>Notes</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing selected" xml:space="preserve"> + <source>Nothing selected</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing to forward!" xml:space="preserve"> + <source>Nothing to forward!</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> <target>通知</target> @@ -3465,6 +4965,18 @@ This is your link for group %@!</source> <target>通知が無効になっています!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notifications error" xml:space="preserve"> + <source>Notifications error</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Notifications privacy" xml:space="preserve"> + <source>Notifications privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications status" xml:space="preserve"> + <source>Notifications status</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. @@ -3474,44 +4986,42 @@ This is your link for group %@!</source> - メンバーを無効にする (メッセージの送信不可)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="OK" xml:space="preserve"> + <source>OK</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Off" xml:space="preserve"> <source>Off</source> <target>オフ</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> - <source>Off (Local)</source> - <target>オフ(自分のみ)</target> - <note>No comment provided by engineer.</note> + <note>blur media</note> </trans-unit> <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> <target>OK</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> <target>古いデータベース</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> - <source>Old database archive</source> - <target>過去のデータベースアーカイブ</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> <target>使い捨ての招待リンク</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> - <target>接続にオニオンのホストが必要となります。VPN を有効にする必要があります。</target> + <trans-unit id="Onion hosts will be **required** for connection. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be **required** for connection. +Requires compatible VPN.</source> + <target>接続にオニオンのホストが必要となります。 +VPN を有効にする必要があります。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> - <target>オニオンのホストが利用可能時に使われます。VPN を有効にする必要があります。</target> + <trans-unit id="Onion hosts will be used when available. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be used when available. +Requires compatible VPN.</source> + <target>オニオンのホストが利用可能時に使われます。 +VPN を有効にする必要があります。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will not be used." xml:space="preserve"> @@ -3519,11 +5029,19 @@ This is your link for group %@!</source> <target>オニオンのホストが使われません。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only chat owners can change preferences." xml:space="preserve"> + <source>Only chat owners can change preferences.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <target>**2 レイヤーのエンドツーエンド暗号化**を使用して送信されたユーザー プロファイル、連絡先、グループ、メッセージを保存できるのはクライアント デバイスのみです。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only delete conversation" xml:space="preserve"> + <source>Only delete conversation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> <source>Only group owners can change group preferences.</source> <target>グループ設定を変えられるのはグループのオーナーだけです。</target> @@ -3539,14 +5057,22 @@ This is your link for group %@!</source> <target>音声メッセージを利用可能に設定できるのはグループのオーナーだけです。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only sender and moderators see it" xml:space="preserve"> + <source>Only sender and moderators see it</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you and moderators see it" xml:space="preserve"> + <source>Only you and moderators see it</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only you can add message reactions." xml:space="preserve"> <source>Only you can add message reactions.</source> <target>メッセージへのリアクションを追加できるのは、あなただけです。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <target>メッセージの完全削除はあなたにしかできません (あなたの連絡先は削除対象とすることができます)。</target> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source> + <target>メッセージの完全削除はあなたにしかできません (あなたの連絡先は削除対象とすることができます)。(24時間)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can make calls." xml:space="preserve"> @@ -3569,9 +5095,9 @@ This is your link for group %@!</source> <target>メッセージへのリアクションを追加できるのは連絡先だけです。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <target>メッセージを完全削除できるのはあなたの連絡相手だけです (あなたは削除対象とすることができます)。</target> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source> + <target>メッセージを完全削除できるのはあなたの連絡相手だけです (あなたは削除対象とすることができます)。(24時間)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can make calls." xml:space="preserve"> @@ -3592,13 +5118,17 @@ This is your link for group %@!</source> <trans-unit id="Open" xml:space="preserve"> <source>Open</source> <target>開く</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>設定を開く</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open changes" xml:space="preserve"> + <source>Open changes</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> <target>チャットを開く</target> @@ -3609,25 +5139,71 @@ This is your link for group %@!</source> <target>チャットのコンソールを開く</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Open conditions" xml:space="preserve"> + <source>Open conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open group" xml:space="preserve"> <source>Open group</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve"> - <source>Open user profiles</source> - <target>ユーザープロフィールを開く</target> + <trans-unit id="Open link?" xml:space="preserve"> + <source>Open link?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Open migration to another device" xml:space="preserve"> + <source>Open migration to another device</source> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> - <target>プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。</target> + <trans-unit id="Opening app…" xml:space="preserve"> + <source>Opening app…</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve"> - <source>Opening database…</source> - <target>データベースを開いています…</target> + <trans-unit id="Operator" xml:space="preserve"> + <source>Operator</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Operator server" xml:space="preserve"> + <source>Operator server</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Or import archive file" xml:space="preserve"> + <source>Or import archive file</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or paste archive link" xml:space="preserve"> + <source>Or paste archive link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or scan QR code" xml:space="preserve"> + <source>Or scan QR code</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or securely share this file link" xml:space="preserve"> + <source>Or securely share this file link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or show this code" xml:space="preserve"> + <source>Or show this code</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or to share privately" xml:space="preserve"> + <source>Or to share privately</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Organize chats into lists" xml:space="preserve"> + <source>Organize chats into lists</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other" xml:space="preserve"> + <source>Other</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other file errors: %@" xml:space="preserve"> + <source>Other file errors: +%@</source> + <note>alert message</note> + </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> <target>PING回数</target> @@ -3663,15 +5239,18 @@ This is your link for group %@!</source> <target>パスコードを設定しました!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Password" xml:space="preserve"> + <source>Password</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> <target>パスワードを表示する</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste" xml:space="preserve"> - <source>Paste</source> - <target>貼り付け</target> - <note>No comment provided by engineer.</note> + <trans-unit id="Past member %@" xml:space="preserve"> + <source>Past member %@</source> + <note>past/unknown group member</note> </trans-unit> <trans-unit id="Paste desktop address" xml:space="preserve"> <source>Paste desktop address</source> @@ -3682,23 +5261,20 @@ This is your link for group %@!</source> <target>画像の貼り付け</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve"> - <source>Paste received link</source> - <target>頂いたリンクを貼り付ける</target> + <trans-unit id="Paste link to connect!" xml:space="preserve"> + <source>Paste link to connect!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> - <source>Paste the link you received to connect with your contact.</source> - <target>連絡相手から頂いたリンクを以下の入力欄に貼り付けて繋がります。</target> - <note>placeholder</note> - </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> - <target>あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。</target> + <trans-unit id="Paste the link you received" xml:space="preserve"> + <source>Paste the link you received</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Periodically" xml:space="preserve"> - <source>Periodically</source> + <trans-unit id="Pending" xml:space="preserve"> + <source>Pending</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Periodic" xml:space="preserve"> + <source>Periodic</source> <target>定期的に</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3707,11 +5283,28 @@ This is your link for group %@!</source> <target>永続的な復号化エラー</target> <note>message decrypt error item</note> </trans-unit> + <trans-unit id="Picture-in-picture calls" xml:space="preserve"> + <source>Picture-in-picture calls</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Play from the chat list." xml:space="preserve"> + <source>Play from the chat list.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable calls." xml:space="preserve"> + <source>Please ask your contact to enable calls.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> <target>音声メッセージを有効にするように連絡相手に要求してください。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers." xml:space="preserve"> + <source>Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> <target>リンクが正しいかどうかご確認ください。または、連絡相手にもう一度リンクをお求めください。</target> @@ -3727,6 +5320,15 @@ This is your link for group %@!</source> <target>あなたと連絡先の設定を確認してください。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve"> + <source>Please confirm that network settings are correct for this device.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact developers. Error: %@" xml:space="preserve"> + <source>Please contact developers. +Error: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> <target>グループの管理者に連絡してください。</target> @@ -3767,11 +5369,27 @@ This is your link for group %@!</source> <target>パスフレーズを失くさないように保管してください。失くすと変更できなくなります。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please try to disable and re-enable notfications." xml:space="preserve"> + <source>Please try to disable and re-enable notfications.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token activation to complete." xml:space="preserve"> + <source>Please wait for token activation to complete.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token to be registered." xml:space="preserve"> + <source>Please wait for token to be registered.</source> + <note>token info</note> + </trans-unit> <trans-unit id="Polish interface" xml:space="preserve"> <source>Polish interface</source> <target>ポーランド語UI</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Port" xml:space="preserve"> + <source>Port</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> <target>サーバアドレスの証明証IDが正しくないかもしれません</target> @@ -3782,36 +5400,76 @@ This is your link for group %@!</source> <target>添付を含めて、下書きを保存する。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Preset server" xml:space="preserve"> - <source>Preset server</source> - <target>プレセットサーバ</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> <target>プレセットサーバのアドレス</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Preset servers" xml:space="preserve"> + <source>Preset servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Preview" xml:space="preserve"> <source>Preview</source> <target>プレビュー</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Previously connected servers" xml:space="preserve"> + <source>Previously connected servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> <target>プライバシーとセキュリティ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Privacy for your customers." xml:space="preserve"> + <source>Privacy for your customers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy policy and conditions of use." xml:space="preserve"> + <source>Privacy policy and conditions of use.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy redefined" xml:space="preserve"> <source>Privacy redefined</source> <target>プライバシーの基準を新境地に</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve"> + <source>Private chats, groups and your contacts are not accessible to server operators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> <target>プライベートなファイル名</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private media file names." xml:space="preserve"> + <source>Private media file names.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing" xml:space="preserve"> + <source>Private message routing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing 🚀" xml:space="preserve"> + <source>Private message routing 🚀</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private notes" xml:space="preserve"> + <source>Private notes</source> + <target>プライベートノート</target> + <note>name of notes to self</note> + </trans-unit> + <trans-unit id="Private routing" xml:space="preserve"> + <source>Private routing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private routing error" xml:space="preserve"> + <source>Private routing error</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> <target>プロフィールとサーバ接続</target> @@ -3822,12 +5480,8 @@ This is your link for group %@!</source> <target>プロフィール画像</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile name" xml:space="preserve"> - <source>Profile name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile name:" xml:space="preserve"> - <source>Profile name:</source> + <trans-unit id="Profile images" xml:space="preserve"> + <source>Profile images</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile password" xml:space="preserve"> @@ -3835,10 +5489,14 @@ This is your link for group %@!</source> <target>プロフィールのパスワード</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Profile theme" xml:space="preserve"> + <source>Profile theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> <source>Profile update will be sent to your contacts.</source> <target>連絡先にプロフィール更新のお知らせが届きます。</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> @@ -3860,6 +5518,14 @@ This is your link for group %@!</source> <target>メッセージへのリアクションは禁止されています。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Prohibit reporting messages to moderators." xml:space="preserve"> + <source>Prohibit reporting messages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending SimpleX links." xml:space="preserve"> + <source>Prohibit sending SimpleX links.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> <target>メンバー間のダイレクトメッセージを使用禁止にする。</target> @@ -3880,11 +5546,20 @@ This is your link for group %@!</source> <target>音声メッセージを使用禁止にする。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect IP address" xml:space="preserve"> + <source>Protect IP address</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> <target>アプリ画面を守る</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve"> + <source>Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> <target>チャットのプロフィールをパスワードで保護します!</target> @@ -3900,16 +5575,40 @@ This is your link for group %@!</source> <target>KB あたりのプロトコル タイムアウト</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Proxied" xml:space="preserve"> + <source>Proxied</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied servers" xml:space="preserve"> + <source>Proxied servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxy requires password" xml:space="preserve"> + <source>Proxy requires password</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> <target>プッシュ通知</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Push server" xml:space="preserve"> + <source>Push server</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Quantum resistant encryption" xml:space="preserve"> + <source>Quantum resistant encryption</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> <source>Rate the app</source> <target>アプリを評価</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reachable chat toolbar" xml:space="preserve"> + <source>Reachable chat toolbar</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> <target>反応する…</target> @@ -3918,16 +5617,20 @@ This is your link for group %@!</source> <trans-unit id="Read" xml:space="preserve"> <source>Read</source> <target>読む</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Read more" xml:space="preserve"> <source>Read more</source> <target>続きを読む</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> - <target>詳しくは[ユーザーガイド](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)をご覧ください。</target> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</source> + <target>詳しくは[ユーザーガイド](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)をご覧ください。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> @@ -3935,20 +5638,19 @@ This is your link for group %@!</source> <target>詳しくは[ユーザーガイド](https://simplex.chat/docs/guide/readme.html#connect-to-friends)をご覧ください。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> - <source>Read more in our GitHub repository.</source> - <target>GitHubリポジトリで詳細をご確認ください。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> - <target>詳しくは[GitHubリポジトリ](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)をご覧ください。</target> + <target>詳しくは[GitHubリポジトリ](https://github.com/simplex-chat/simplex-chat#readme)をご覧ください。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Receipts are disabled" xml:space="preserve"> <source>Receipts are disabled</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receive errors" xml:space="preserve"> + <source>Receive errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>受信</target> @@ -3969,6 +5671,18 @@ This is your link for group %@!</source> <target>受信したメッセージ</target> <note>message info title</note> </trans-unit> + <trans-unit id="Received messages" xml:space="preserve"> + <source>Received messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received reply" xml:space="preserve"> + <source>Received reply</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received total" xml:space="preserve"> + <source>Received total</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> <target>開発中の機能です!相手のクライアントが4.2でなければ機能しません。アドレス変更が完了すると、会話にメッセージが出ます。連絡相手 (またはグループのメンバー) からメッセージを受信できないかをご確認ください。</target> @@ -3984,16 +5698,44 @@ This is your link for group %@!</source> <target>経由で受信</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Recent history and improved [directory bot](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)." xml:space="preserve"> + <source>Recent history and improved [directory bot](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).</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve"> + <source>Recipient(s) can't see who this message is from.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> <target>受信者には、入力時に更新内容が表示されます。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect" xml:space="preserve"> + <source>Reconnect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> <target>接続されているすべてのサーバーを再接続して、メッセージを強制的に配信します。 追加のトラフィックを使用します。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect all servers" xml:space="preserve"> + <source>Reconnect all servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers?" xml:space="preserve"> + <source>Reconnect all servers?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect server to force message delivery. It uses additional traffic.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server?" xml:space="preserve"> + <source>Reconnect server?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> <target>サーバーに再接続しますか?</target> @@ -4014,10 +5756,23 @@ This is your link for group %@!</source> <target>電池使用量低減</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Register" xml:space="preserve"> + <source>Register</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Register notification token?" xml:space="preserve"> + <source>Register notification token?</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Registered" xml:space="preserve"> + <source>Registered</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> <target>拒否</target> - <note>reject incoming call via notification</note> + <note>reject incoming call via notification +swipe action</note> </trans-unit> <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> <source>Reject (sender NOT notified)</source> @@ -4044,6 +5799,14 @@ This is your link for group %@!</source> <target>削除</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Remove archive?" xml:space="preserve"> + <source>Remove archive?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove image" xml:space="preserve"> + <source>Remove image</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> <target>メンバーを除名する</target> @@ -4078,15 +5841,67 @@ This is your link for group %@!</source> <source>Repeat connection request?</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat download" xml:space="preserve"> + <source>Repeat download</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat import" xml:space="preserve"> + <source>Repeat import</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Repeat join request?" xml:space="preserve"> <source>Repeat join request?</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat upload" xml:space="preserve"> + <source>Repeat upload</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reply" xml:space="preserve"> <source>Reply</source> <target>返信</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Report" xml:space="preserve"> + <source>Report</source> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Report content: only group moderators will see it." xml:space="preserve"> + <source>Report content: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report member profile: only group moderators will see it." xml:space="preserve"> + <source>Report member profile: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report other: only group moderators will see it." xml:space="preserve"> + <source>Report other: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report reason?" xml:space="preserve"> + <source>Report reason?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Report spam: only group moderators will see it." xml:space="preserve"> + <source>Report spam: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report violation: only group moderators will see it." xml:space="preserve"> + <source>Report violation: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report: %@" xml:space="preserve"> + <source>Report: %@</source> + <note>report in notification</note> + </trans-unit> + <trans-unit id="Reporting messages to moderators is prohibited." xml:space="preserve"> + <source>Reporting messages to moderators is prohibited.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reports" xml:space="preserve"> + <source>Reports</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Required" xml:space="preserve"> <source>Required</source> <target>必須</target> @@ -4097,16 +5912,36 @@ This is your link for group %@!</source> <target>戻す</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset all hints" xml:space="preserve"> + <source>Reset all hints</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics" xml:space="preserve"> + <source>Reset all statistics</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics?" xml:space="preserve"> + <source>Reset all statistics?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> <target>既定の色に戻す</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to app theme" xml:space="preserve"> + <source>Reset to app theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> <target>既定に戻す</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to user theme" xml:space="preserve"> + <source>Reset to user theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> <source>Restart the app to create a new chat profile</source> <target>新しいチャットプロファイルを作成するためにアプリを再起動する</target> @@ -4137,14 +5972,17 @@ This is your link for group %@!</source> <target>データベース復元エラー</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Retry" xml:space="preserve"> + <source>Retry</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> <target>開示する</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Revert" xml:space="preserve"> - <source>Revert</source> - <target>元に戻す</target> + <trans-unit id="Review conditions" xml:space="preserve"> + <source>Review conditions</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke" xml:space="preserve"> @@ -4172,51 +6010,61 @@ This is your link for group %@!</source> <target>チャット起動</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve"> - <source>SMP servers</source> - <target>SMPサーバ</target> + <trans-unit id="SMP server" xml:space="preserve"> + <source>SMP server</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SOCKS proxy" xml:space="preserve"> + <source>SOCKS proxy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safely receive files" xml:space="preserve"> + <source>Safely receive files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safer groups" xml:space="preserve"> + <source>Safer groups</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save" xml:space="preserve"> <source>Save</source> <target>保存</target> - <note>chat item action</note> + <note>alert button +chat item action</note> </trans-unit> <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> <target>保存(連絡先に通知)</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> <target>保存して、連絡先にに知らせる</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> <target>保存して、グループのメンバーにに知らせる</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save and reconnect" xml:space="preserve"> + <source>Save and reconnect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save and update group profile" xml:space="preserve"> <source>Save and update group profile</source> <target>グループプロファイルの保存と更新</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save archive" xml:space="preserve"> - <source>Save archive</source> - <target>アーカイブを保存</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve"> - <source>Save auto-accept settings</source> - <target>自動受け入れ設定を保存する</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> <source>Save group profile</source> <target>グループプロフィールの保存</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save list" xml:space="preserve"> + <source>Save list</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> <target>パスフレーズをを保存して、チャットを開始</target> @@ -4230,7 +6078,7 @@ This is your link for group %@!</source> <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> <target>この設定でよろしいですか?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> @@ -4245,23 +6093,46 @@ This is your link for group %@!</source> <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> <target>サーバを保存しますか?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve"> - <source>Save settings?</source> - <target>設定を保存しますか?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> <target>ウェルカムメッセージを保存しますか?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save your profile?" xml:space="preserve"> + <source>Save your profile?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Saved" xml:space="preserve"> + <source>Saved</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> <source>Saved WebRTC ICE servers will be removed</source> <target>保存されたWebRTC ICEサーバは削除されます</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Saved from" xml:space="preserve"> + <source>Saved from</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved message" xml:space="preserve"> + <source>Saved message</source> + <note>message info title</note> + </trans-unit> + <trans-unit id="Saving %lld messages" xml:space="preserve"> + <source>Saving %lld messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scale" xml:space="preserve"> + <source>Scale</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan / Paste link" xml:space="preserve"> + <source>Scan / Paste link</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> <target>QRコードを読み込む</target> @@ -4291,11 +6162,27 @@ This is your link for group %@!</source> <target>検索</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Search bar accepts invitation links." xml:space="preserve"> + <source>Search bar accepts invitation links.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search or paste SimpleX link" xml:space="preserve"> + <source>Search or paste SimpleX link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secondary" xml:space="preserve"> + <source>Secondary</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> <source>Secure queue</source> <target>待ち行列セキュリティ確認</target> <note>server test step</note> </trans-unit> + <trans-unit id="Secured" xml:space="preserve"> + <source>Secured</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> <target>セキュリティ評価</target> @@ -4309,6 +6196,18 @@ This is your link for group %@!</source> <trans-unit id="Select" xml:space="preserve"> <source>Select</source> <target>選択</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Select chat profile" xml:space="preserve"> + <source>Select chat profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected %lld" xml:space="preserve"> + <source>Selected %lld</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct" xml:space="preserve"> @@ -4345,11 +6244,6 @@ This is your link for group %@!</source> <source>Send delivery receipts to</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve"> - <source>Send direct message</source> - <target>ダイレクトメッセージを送信</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Send direct message to connect" xml:space="preserve"> <source>Send direct message to connect</source> <target>ダイレクトメッセージを送信して接続する</target> @@ -4360,6 +6254,10 @@ This is your link for group %@!</source> <target>消えるメッセージを送信</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send errors" xml:space="preserve"> + <source>Send errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> <target>リンクのプレビューを送信</target> @@ -4370,14 +6268,25 @@ This is your link for group %@!</source> <target>ライブメッセージを送信</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send message to enable calls." xml:space="preserve"> + <source>Send message to enable calls.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when your or destination server does not support private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> <target>通知を送信する</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> - <source>Send notifications:</source> - <target>通知を送信する:</target> + <trans-unit id="Send private reports" xml:space="preserve"> + <source>Send private reports</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve"> @@ -4394,10 +6303,14 @@ This is your link for group %@!</source> <target>ギャラリーまたはカスタム キーボードから送信します。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve"> + <source>Send up to 100 last messages to new members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> <target>送信者がファイル転送をキャンセルしました。</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> @@ -4448,6 +6361,10 @@ This is your link for group %@!</source> <target>送信日時: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Sent directly" xml:space="preserve"> + <source>Sent directly</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> <target>送信済みファイルイベント</target> @@ -4458,11 +6375,59 @@ This is your link for group %@!</source> <target>送信</target> <note>message info title</note> </trans-unit> + <trans-unit id="Sent messages" xml:space="preserve"> + <source>Sent messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> <target>一定時間が経ったら送信されたメッセージが削除されます。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sent reply" xml:space="preserve"> + <source>Sent reply</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent total" xml:space="preserve"> + <source>Sent total</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent via proxy" xml:space="preserve"> + <source>Sent via proxy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server" xml:space="preserve"> + <source>Server</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server added to operator %@." xml:space="preserve"> + <source>Server added to operator %@.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Server address" xml:space="preserve"> + <source>Server address</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings." xml:space="preserve"> + <source>Server address is incompatible with network settings.</source> + <note>srv error text.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Server address is incompatible with network settings: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server operator changed." xml:space="preserve"> + <source>Server operator changed.</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Server operators" xml:space="preserve"> + <source>Server operators</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server protocol changed." xml:space="preserve"> + <source>Server protocol changed.</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> <target>キューを作成するにはサーバーの認証が必要です。パスワードを確認してください</target> @@ -4478,11 +6443,31 @@ This is your link for group %@!</source> <target>サーバテスト失敗!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Server type" xml:space="preserve"> + <source>Server type</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server version is incompatible with network settings." xml:space="preserve"> + <source>Server version is incompatible with network settings.</source> + <note>srv error text</note> + </trans-unit> + <trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve"> + <source>Server version is incompatible with your app: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Servers" xml:space="preserve"> <source>Servers</source> <target>サーバ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Servers info" xml:space="preserve"> + <source>Servers info</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Session code" xml:space="preserve"> <source>Session code</source> <note>No comment provided by engineer.</note> @@ -4492,11 +6477,19 @@ This is your link for group %@!</source> <target>1日に設定</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set chat name…" xml:space="preserve"> + <source>Set chat name…</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> <target>連絡先の名前を設定…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve"> + <source>Set default theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> <target>グループの設定を行う</target> @@ -4507,11 +6500,19 @@ This is your link for group %@!</source> <target>システム認証の代わりに設定します。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve"> + <source>Set message expiration in chats.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passcode" xml:space="preserve"> <source>Set passcode</source> <target>パスコードを設定する</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set passphrase" xml:space="preserve"> + <source>Set passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> <target>暗証フレーズを設定してからエクスポート</target> @@ -4532,24 +6533,49 @@ This is your link for group %@!</source> <target>設定</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve"> + <source>Settings were changed.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve"> + <source>Shape profile images</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share" xml:space="preserve"> <source>Share</source> <target>共有する</target> - <note>chat item action</note> + <note>alert action +chat item action</note> </trans-unit> <trans-unit id="Share 1-time link" xml:space="preserve"> <source>Share 1-time link</source> <target>使い捨てのリンクを共有</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share 1-time link with a friend" xml:space="preserve"> + <source>Share 1-time link with a friend</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve"> + <source>Share SimpleX address on social media.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address" xml:space="preserve"> <source>Share address</source> <target>アドレスを共有する</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share address publicly" xml:space="preserve"> + <source>Share address publicly</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address with contacts?" xml:space="preserve"> <source>Share address with contacts?</source> <target>アドレスを連絡先と共有しますか?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Share from other apps." xml:space="preserve"> + <source>Share from other apps.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share link" xml:space="preserve"> @@ -4557,9 +6583,16 @@ This is your link for group %@!</source> <target>リンクを送る</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve"> - <source>Share one-time invitation link</source> - <target>使い捨ての招待リンクを共有</target> + <trans-unit id="Share profile" xml:space="preserve"> + <source>Share profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share this 1-time invite link" xml:space="preserve"> + <source>Share this 1-time invite link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share to SimpleX" xml:space="preserve"> + <source>Share to SimpleX</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share with contacts" xml:space="preserve"> @@ -4567,6 +6600,14 @@ This is your link for group %@!</source> <target>連絡先と共有する</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Short link" xml:space="preserve"> + <source>Short link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show QR code" xml:space="preserve"> + <source>Show QR code</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> <target>通話履歴を表示</target> @@ -4582,21 +6623,41 @@ This is your link for group %@!</source> <target>最新のメッセージを表示</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show message status" xml:space="preserve"> + <source>Show message status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show percentage" xml:space="preserve"> + <source>Show percentage</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>プレビューを表示</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show → on messages sent via private routing." xml:space="preserve"> + <source>Show → on messages sent via private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show:" xml:space="preserve"> <source>Show:</source> <target>表示する:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve"> + <source>SimpleX</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Address" xml:space="preserve"> <source>SimpleX Address</source> <target>SimpleXアドレス</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." xml:space="preserve"> + <source>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> <target>SimpleX Chat のセキュリティは Trail of Bits によって監査されました。</target> @@ -4627,6 +6688,18 @@ This is your link for group %@!</source> <target>SimpleXアドレス</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX address and 1-time links are safe to share via any messenger." xml:space="preserve"> + <source>SimpleX address and 1-time links are safe to share via any messenger.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address or 1-time link?" xml:space="preserve"> + <source>SimpleX address or 1-time link?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX channel link" xml:space="preserve"> + <source>SimpleX channel link</source> + <note>simplex link type</note> + </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> <target>SimpleX連絡先アドレス</target> @@ -4645,6 +6718,14 @@ This is your link for group %@!</source> <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> <target>SimpleXリンク</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve"> + <source>SimpleX links are prohibited.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve"> + <source>SimpleX links not allowed</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> @@ -4652,11 +6733,19 @@ This is your link for group %@!</source> <target>SimpleX使い捨て招待リンク</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> + <source>SimpleX protocols reviewed by Trail of Bits.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Simplified incognito mode" xml:space="preserve"> <source>Simplified incognito mode</source> <target>シークレットモードの簡素化</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Size" xml:space="preserve"> + <source>Size</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>スキップ</target> @@ -4672,19 +6761,44 @@ This is your link for group %@!</source> <target>小グループ(最大20名)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Soft" xml:space="preserve"> + <source>Soft</source> + <note>blur media</note> + </trans-unit> + <trans-unit id="Some app settings were not migrated." xml:space="preserve"> + <source>Some app settings were not migrated.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some file(s) were not exported:" xml:space="preserve"> + <source>Some file(s) were not exported:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>インポート中に致命的でないエラーが発生しました - 詳細はチャットコンソールを参照してください。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import:" xml:space="preserve"> + <source>Some non-fatal errors occurred during import:</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some servers failed the test: %@" xml:space="preserve"> + <source>Some servers failed the test: +%@</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> <target>誰か</target> <note>notification title</note> </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve"> - <source>Start a new chat</source> - <target>新しいチャットを開始する</target> + <trans-unit id="Spam" xml:space="preserve"> + <source>Spam</source> + <note>blocking reason +report reason</note> + </trans-unit> + <trans-unit id="Square, circle, or anything in between." xml:space="preserve"> + <source>Square, circle, or anything in between.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start chat" xml:space="preserve"> @@ -4692,11 +6806,23 @@ This is your link for group %@!</source> <target>チャットを開始する</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Start chat?" xml:space="preserve"> + <source>Start chat?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> <target>移行の開始</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Starting from %@." xml:space="preserve"> + <source>Starting from %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Statistics" xml:space="preserve"> + <source>Statistics</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> <target>停止</target> @@ -4707,9 +6833,8 @@ This is your link for group %@!</source> <target>SimpleX を停止</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> - <source>Stop chat to enable database actions</source> - <target>チャットを停止してデータベースアクションを有効にします</target> + <trans-unit id="Stop chat" xml:space="preserve"> + <source>Stop chat</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> @@ -4740,23 +6865,55 @@ This is your link for group %@!</source> <trans-unit id="Stop sharing" xml:space="preserve"> <source>Stop sharing</source> <target>共有を停止</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Stop sharing address?" xml:space="preserve"> <source>Stop sharing address?</source> <target>アドレスの共有を停止しますか?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Stopping chat" xml:space="preserve"> + <source>Stopping chat</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Storage" xml:space="preserve"> + <source>Storage</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Strong" xml:space="preserve"> + <source>Strong</source> + <note>blur media</note> + </trans-unit> <trans-unit id="Submit" xml:space="preserve"> <source>Submit</source> <target>送信</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Subscribed" xml:space="preserve"> + <source>Subscribed</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscription errors" xml:space="preserve"> + <source>Subscription errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscriptions ignored" xml:space="preserve"> + <source>Subscriptions ignored</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Support SimpleX Chat" xml:space="preserve"> <source>Support SimpleX Chat</source> <target>Simplex Chatを支援</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Switch audio and video during the call." xml:space="preserve"> + <source>Switch audio and video during the call.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> + <source>Switch chat profile for 1-time invitations.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="System" xml:space="preserve"> <source>System</source> <target>システム</target> @@ -4767,11 +6924,19 @@ This is your link for group %@!</source> <target>システム認証</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP connection" xml:space="preserve"> + <source>TCP connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> <target>TCP接続タイムアウト</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP port for messaging" xml:space="preserve"> + <source>TCP port for messaging</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <source>TCP_KEEPCNT</source> <target>TCP_KEEPCNT</target> @@ -4787,11 +6952,19 @@ This is your link for group %@!</source> <target>TCP_KEEPINTVL</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tail" xml:space="preserve"> + <source>Tail</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> <target>写真を撮影</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap Create SimpleX address in the menu to create it later." xml:space="preserve"> + <source>Tap Create SimpleX address in the menu to create it later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> <target>ボタンをタップ </target> @@ -4816,16 +6989,27 @@ This is your link for group %@!</source> <target>タップしてシークレットモードで参加</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve"> - <source>Tap to start a new chat</source> - <target>タップして新しいチャットを始める</target> + <trans-unit id="Tap to paste link" xml:space="preserve"> + <source>Tap to paste link</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap to scan" xml:space="preserve"> + <source>Tap to scan</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Temporary file error" xml:space="preserve"> + <source>Temporary file error</source> + <note>file error alert title</note> + </trans-unit> <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> <target>テストはステップ %@ で失敗しました。</target> <note>server test failure</note> </trans-unit> + <trans-unit id="Test notifications" xml:space="preserve"> + <source>Test notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> <target>テストサーバ</target> @@ -4839,7 +7023,7 @@ This is your link for group %@!</source> <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> <target>テストは失敗しました!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> @@ -4856,11 +7040,6 @@ This is your link for group %@!</source> <target>ユーザーに感謝します – Weblate 経由で貢献してください!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> - <target>世界初のユーザーIDのないプラットフォーム|設計も元からプライベート。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <source>The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised.</source> @@ -4873,11 +7052,27 @@ It can happen because of some bug or when the connection is compromised.</source <target>アプリは、メッセージや連絡先のリクエストを受信したときに通知することができます - 設定を開いて有効にしてください。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The app protects your privacy by using different operators in each conversation." xml:space="preserve"> + <source>The app protects your privacy by using different operators in each conversation.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve"> + <source>The app will ask to confirm downloads from unknown file servers (except .onion).</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> <source>The attempt to change database passphrase was not completed.</source> <target>データベースのパスフレーズ変更が完了してません。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve"> + <source>The code you scanned is not a SimpleX link QR code.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The connection reached the limit of undelivered messages, your contact may be offline." xml:space="preserve"> + <source>The connection reached the limit of undelivered messages, your contact may be offline.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> <target>承認済の接続がキャンセルされます!</target> @@ -4898,6 +7093,11 @@ It can happen because of some bug or when the connection is compromised.</source <target>暗号化は機能しており、新しい暗号化への同意は必要ありません。接続エラーが発生する可能性があります!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> + <target>次世代のプライバシー・メッセンジャー</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The hash of the previous message is different." xml:space="preserve"> <source>The hash of the previous message is different.</source> <target>以前のメッセージとハッシュ値が異なります。</target> @@ -4913,9 +7113,12 @@ It can happen because of some bug or when the connection is compromised.</source <target>メッセージは、すべてのメンバーに対してモデレートされたものとして表示されます。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> - <target>次世代のプライバシー・メッセンジャー</target> + <trans-unit id="The messages will be deleted for all members." xml:space="preserve"> + <source>The messages will be deleted for all members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve"> + <source>The messages will be marked as moderated for all members.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> @@ -4923,9 +7126,12 @@ It can happen because of some bug or when the connection is compromised.</source <target>古いデータベースは移行時に削除されなかったので、削除することができます。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> - <target>プロフィールは連絡先にしか共有されません。</target> + <trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve"> + <source>The same conditions will apply to operator **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second preset operator in the app!" xml:space="preserve"> + <source>The second preset operator in the app!</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> @@ -4943,19 +7149,24 @@ It can happen because of some bug or when the connection is compromised.</source <target>現在のチャットプロフィールの新しい接続のサーバ **%@**。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Theme" xml:space="preserve"> - <source>Theme</source> - <target>テーマ</target> + <trans-unit id="The servers for new files of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new files of your current chat profile **%@**.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve"> - <source>There should be at least one user profile.</source> - <target>少なくとも1つのユーザープロファイルが必要です。</target> + <trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve"> + <source>The text you pasted is not a SimpleX link.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> - <source>There should be at least one visible user profile.</source> - <target>少なくとも1つのユーザープロフィールが表示されている必要があります。</target> + <trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve"> + <source>The uploaded database archive will be permanently removed from the servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Themes" xml:space="preserve"> + <source>Themes</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="These conditions will also apply for: **%@**." xml:space="preserve"> + <source>These conditions will also apply for: **%@**.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> @@ -4978,15 +7189,31 @@ It can happen because of some bug or when the connection is compromised.</source <target>選択中の以前の送受信したメッセージが削除されます (※元に戻せません※)。数分かかります。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> <target>あなたのプロフィール、連絡先、メッセージ、ファイルが完全削除されます (※元に戻せません※)。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by end-to-end encryption.</source> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by quantum resistant end-to-end encryption.</source> + <note>E2EE info chat item</note> + </trans-unit> <trans-unit id="This device name" xml:space="preserve"> <source>This device name</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve"> + <source>This display name is invalid. Please choose another name.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> <source>This group has over %lld members, delivery receipts are not sent.</source> <note>No comment provided by engineer.</note> @@ -5004,11 +7231,27 @@ It can happen because of some bug or when the connection is compromised.</source <source>This is your own one-time link!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve"> + <source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve"> + <source>This link was used with another mobile device, please create a new link on the desktop.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This message was deleted or not received yet." xml:space="preserve"> + <source>This message was deleted or not received yet.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <source>This setting applies to messages in your current chat profile **%@**.</source> <target>この設定は現在のチャットプロフィール **%@** のメッセージに適用されます。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Title" xml:space="preserve"> + <source>Title</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> <source>To ask any questions and to receive updates:</source> <target>質問や最新情報を受け取るには:</target> @@ -5028,9 +7271,8 @@ It can happen because of some bug or when the connection is compromised.</source <target>新規に接続する場合</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <target>プライバシーを保護するために、SimpleX には、他のすべてのプラットフォームで使用されるユーザー ID の代わりに、連絡先ごとに個別のメッセージ キューの識別子があります。</target> + <trans-unit id="To protect against your link being replaced, you can compare contact security codes." xml:space="preserve"> + <source>To protect against your link being replaced, you can compare contact security codes.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -5038,6 +7280,10 @@ It can happen because of some bug or when the connection is compromised.</source <target>時間帯を漏らさないために、画像と音声ファイルはUTCを使います。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve"> + <source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> @@ -5045,6 +7291,23 @@ You will be prompted to complete authentication before this feature is enabled.< オンにするには、認証ステップが行われます。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> + <target>プライバシーを保護するために、SimpleX には、他のすべてのプラットフォームで使用されるユーザー ID の代わりに、連絡先ごとに個別のメッセージ キューの識別子があります。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To receive" xml:space="preserve"> + <source>To receive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve"> + <source>To record speech please grant permission to use Microphone.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record video please grant permission to use Camera." xml:space="preserve"> + <source>To record video please grant permission to use Camera.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> <target>音声メッセージを録音する場合は、マイクの使用を許可してください。</target> @@ -5055,25 +7318,53 @@ You will be prompted to complete authentication before this feature is enabled.< <target>非表示のプロフィールを表示するには、**チャット プロフィール** ページの検索フィールドに完全なパスワードを入力します。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To send" xml:space="preserve"> + <source>To send</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> <source>To support instant push notifications the chat database has to be migrated.</source> <target>インスタント プッシュ通知をサポートするには、チャット データベースを移行する必要があります。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To use the servers of **%@**, accept conditions of use." xml:space="preserve"> + <source>To use the servers of **%@**, accept conditions of use.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> <target>エンドツーエンド暗号化を確認するには、ご自分の端末と連絡先の端末のコードを比べます (スキャンします)。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle chat list:" xml:space="preserve"> + <source>Toggle chat list:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> <source>Toggle incognito when connecting.</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Token status: %@." xml:space="preserve"> + <source>Token status: %@.</source> + <note>token status</note> + </trans-unit> + <trans-unit id="Toolbar opacity" xml:space="preserve"> + <source>Toolbar opacity</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Total" xml:space="preserve"> + <source>Total</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>トランスポート隔離</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Transport sessions" xml:space="preserve"> + <source>Transport sessions</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> <target>この連絡先からのメッセージの受信に使用されるサーバーに接続しようとしています (エラー: %@)。</target> @@ -5084,16 +7375,15 @@ You will be prompted to complete authentication before this feature is enabled.< <target>このコンタクトから受信するメッセージのサーバに接続しようとしてます。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Turkish interface" xml:space="preserve"> + <source>Turkish interface</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> <target>オフにする</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve"> - <source>Turn off notifications?</source> - <target>通知をオフにしますか?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> <target>オンにする</target> @@ -5108,18 +7398,25 @@ You will be prompted to complete authentication before this feature is enabled.< <source>Unblock</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock for all" xml:space="preserve"> + <source>Unblock for all</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member" xml:space="preserve"> <source>Unblock member</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock member for all?" xml:space="preserve"> + <source>Unblock member for all?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member?" xml:space="preserve"> <source>Unblock member?</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve"> - <source>Unexpected error: %@</source> - <target>予期しないエラー: %@</target> - <note>item status description</note> + <trans-unit id="Undelivered messages" xml:space="preserve"> + <source>Undelivered messages</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -5129,7 +7426,7 @@ You will be prompted to complete authentication before this feature is enabled.< <trans-unit id="Unfav." xml:space="preserve"> <source>Unfav.</source> <target>お気に入りを取り消す。</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> @@ -5166,6 +7463,10 @@ You will be prompted to complete authentication before this feature is enabled.< <target>不明なエラー</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unknown servers!" xml:space="preserve"> + <source>Unknown servers!</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> <target>iOS 通話インターフェイスを使用しない場合は、中断を避けるために「おやすみモード」を有効にしてください。</target> @@ -5199,11 +7500,19 @@ To connect, please ask your contact to create another connection link and check <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> <target>ミュート解除</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> </trans-unit> <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> <target>未読</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Unsupported connection link" xml:space="preserve"> + <source>Unsupported connection link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve"> + <source>Up to 100 last messages are sent to new members.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update" xml:space="preserve"> @@ -5211,11 +7520,6 @@ To connect, please ask your contact to create another connection link and check <target>更新</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> - <source>Update .onion hosts setting?</source> - <target>.onionのホスト設定を更新しますか?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> <target>データベースのパスフレーズを更新</target> @@ -5226,9 +7530,12 @@ To connect, please ask your contact to create another connection link and check <target>ネットワーク設定を更新しますか?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve"> - <source>Update transport isolation mode?</source> - <target>トランスポート隔離モードを更新しますか?</target> + <trans-unit id="Update settings?" xml:space="preserve"> + <source>Update settings?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updated conditions" xml:space="preserve"> + <source>Updated conditions</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> @@ -5236,31 +7543,62 @@ To connect, please ask your contact to create another connection link and check <target>設定を更新すると、全サーバにクライントの再接続が行われます。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> - <target>設定を更新すると、全サーバにクライントの再接続が行われます。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Upgrade and open chat" xml:space="preserve"> <source>Upgrade and open chat</source> <target>アップグレードしてチャットを開く</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Upload errors" xml:space="preserve"> + <source>Upload errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload failed" xml:space="preserve"> + <source>Upload failed</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Upload file" xml:space="preserve"> <source>Upload file</source> <target>ファイルをアップロードする</target> <note>server test step</note> </trans-unit> + <trans-unit id="Uploaded" xml:space="preserve"> + <source>Uploaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploaded files" xml:space="preserve"> + <source>Uploaded files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploading archive" xml:space="preserve"> + <source>Uploading archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use %@" xml:space="preserve"> + <source>Use %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> <target>.onionホストを使う</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use SOCKS proxy" xml:space="preserve"> + <source>Use SOCKS proxy</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> <target>SimpleX チャット サーバーを使用しますか?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use TCP port %@ when no port is specified." xml:space="preserve"> + <source>Use TCP port %@ when no port is specified.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve"> + <source>Use TCP port 443 for preset servers only.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> <target>チャット</target> @@ -5271,6 +7609,14 @@ To connect, please ask your contact to create another connection link and check <target>現在のプロファイルを使用する</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use for files" xml:space="preserve"> + <source>Use for files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for messages" xml:space="preserve"> + <source>Use for messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>新しい接続に使う</target> @@ -5290,19 +7636,49 @@ To connect, please ask your contact to create another connection link and check <target>新しいシークレットプロファイルを使用する</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use only local notifications?" xml:space="preserve"> + <source>Use only local notifications?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve"> + <source>Use private routing with unknown servers when IP address is not protected.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers." xml:space="preserve"> + <source>Use private routing with unknown servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>サーバを使う</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> - <target>ユーザープロフィール</target> + <trans-unit id="Use servers" xml:space="preserve"> + <source>Use servers</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> - <target>.onionホストを使用するには、互換性のあるVPNプロバイダーが必要です。</target> + <trans-unit id="Use short links (BETA)" xml:space="preserve"> + <source>Use short links (BETA)</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app while in the call." xml:space="preserve"> + <source>Use the app while in the call.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app with one hand." xml:space="preserve"> + <source>Use the app with one hand.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use web port" xml:space="preserve"> + <source>Use web port</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="User selection" xml:space="preserve"> + <source>User selection</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Username" xml:space="preserve"> + <source>Username</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> @@ -5327,6 +7703,14 @@ To connect, please ask your contact to create another connection link and check <source>Verify connections</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Verify database passphrase" xml:space="preserve"> + <source>Verify database passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify passphrase" xml:space="preserve"> + <source>Verify passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Verify security code" xml:space="preserve"> <source>Verify security code</source> <target>セキュリティコードを確認</target> @@ -5361,11 +7745,23 @@ To connect, please ask your contact to create another connection link and check <target>1GBまでのビデオとファイル</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View conditions" xml:space="preserve"> + <source>View conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> <target>セキュリティコードを確認</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View updated conditions" xml:space="preserve"> + <source>View updated conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Visible history" xml:space="preserve"> + <source>Visible history</source> + <note>chat feature</note> + </trans-unit> <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> <target>音声メッセージ</target> @@ -5376,11 +7772,15 @@ To connect, please ask your contact to create another connection link and check <target>このチャットでは音声メッセージが使用禁止です。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <target>このグループでは音声メッセージが使用禁止です。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Voice messages not allowed" xml:space="preserve"> + <source>Voice messages not allowed</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> <source>Voice messages prohibited!</source> <target>音声メッセージは使用禁止です!</target> @@ -5410,6 +7810,18 @@ To connect, please ask your contact to create another connection link and check <target>ビデオ待機中</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wallpaper accent" xml:space="preserve"> + <source>Wallpaper accent</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper background" xml:space="preserve"> + <source>Wallpaper background</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve"> + <source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> <source>Warning: you may lose some data!</source> <target>警告: 一部のデータが失われる可能性があります!</target> @@ -5430,6 +7842,10 @@ To connect, please ask your contact to create another connection link and check <target>ウェルカムメッセージ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Welcome message is too long" xml:space="preserve"> + <source>Welcome message is too long</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="What's new" xml:space="preserve"> <source>What's new</source> <target>新着情報</target> @@ -5440,9 +7856,12 @@ To connect, please ask your contact to create another connection link and check <target>利用可能時に</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> - <source>When people request to connect, you can accept or reject it.</source> - <target>接続が要求されたら、それを受け入れるか拒否するかを選択できます。</target> + <trans-unit id="When connecting audio and video calls." xml:space="preserve"> + <source>When connecting audio and video calls.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When more than one operator is enabled, none of them has metadata to learn who communicates with whom." xml:space="preserve"> + <source>When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> @@ -5450,29 +7869,63 @@ To connect, please ask your contact to create another connection link and check <target>連絡相手にシークレットモードのプロフィールを共有すると、その連絡相手に招待されたグループでも同じプロフィールが使われます。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="WiFi" xml:space="preserve"> + <source>WiFi</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Will be enabled in direct chats!" xml:space="preserve"> + <source>Will be enabled in direct chats!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wired ethernet" xml:space="preserve"> + <source>Wired ethernet</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With encrypted files and media." xml:space="preserve"> + <source>With encrypted files and media.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> <target>任意のウェルカムメッセージ付き。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="With reduced battery usage." xml:space="preserve"> + <source>With reduced battery usage.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to file servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> <target>データベースのパスフレーズが違います</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve"> + <source>Wrong key or unknown connection - most likely this connection is deleted.</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Wrong key or unknown file chunk address - most likely file is deleted." xml:space="preserve"> + <source>Wrong key or unknown file chunk address - most likely file is deleted.</source> + <note>file error text</note> + </trans-unit> <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> <target>パスフレーズが違います!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="XFTP servers" xml:space="preserve"> - <source>XFTP servers</source> - <target>XFTPサーバ</target> + <trans-unit id="XFTP server" xml:space="preserve"> + <source>XFTP server</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> - <target>あなた</target> + <trans-unit id="You **must not** use the same database on two devices." xml:space="preserve"> + <source>You **must not** use the same database on two devices.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You accepted connection" xml:space="preserve"> @@ -5495,6 +7948,10 @@ To connect, please ask your contact to create another connection link and check <target>すでに %@ に接続されています。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are already connected with %@." xml:space="preserve"> + <source>You are already connected with %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You are already connecting to %@." xml:space="preserve"> <source>You are already connecting to %@.</source> <note>No comment provided by engineer.</note> @@ -5534,14 +7991,21 @@ Repeat join request?</source> <target>グループ招待が届きました</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are not connected to these servers. Private routing is used to deliver messages to them." xml:space="preserve"> + <source>You are not connected to these servers. Private routing is used to deliver messages to them.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> <source>You can accept calls from lock screen, without device and app authentication.</source> <target>デバイスやアプリの認証を行わずに、ロック画面から通話を受けることができます。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <target>リンクをクリックすることでも接続できます。ブラウザで開いた場合は、**モバイルアプリで開く**ボタンをクリックしてください。</target> + <trans-unit id="You can change it in Appearance settings." xml:space="preserve"> + <source>You can change it in Appearance settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can configure servers via settings." xml:space="preserve"> + <source>You can configure servers via settings.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can create it later" xml:space="preserve"> @@ -5559,16 +8023,33 @@ Repeat join request?</source> <target>あとでアプリのプライバシーとセキュリティの設定から有効にすることができます。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can give another try." xml:space="preserve"> + <source>You can give another try.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <source>You can hide or mute a user profile - swipe it to the right.</source> <target>ユーザープロファイルを右にスワイプすると、非表示またはミュートにすることができます。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> + <trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve"> + <source>You can make it visible to your SimpleX contacts via Settings.</source> + <target>設定でSimpleXの連絡先に表示させることができます。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now chat with %@" xml:space="preserve"> + <source>You can now chat with %@</source> <target>%@ にメッセージを送信できるようになりました</target> <note>notification body</note> </trans-unit> + <trans-unit id="You can send messages to %@ from Archived contacts." xml:space="preserve"> + <source>You can send messages to %@ from Archived contacts.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can set connection name, to remember who the link was shared with." xml:space="preserve"> + <source>You can set connection name, to remember who the link was shared with.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> <target>設定からロック画面の通知プレビューを設定できます。</target> @@ -5584,16 +8065,15 @@ Repeat join request?</source> <target>このアドレスを連絡先と共有して、**%@** に接続できるようにすることができます。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> - <source>You can share your address as a link or QR code - anybody can connect to you.</source> - <target>アドレスをリンクやQRコードとして共有することで、誰でも接続することができます。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> <target>アプリの設定/データベースから、またはアプリを再起動することでチャットを開始できます</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve"> + <source>You can still view conversation with %@ in the list of chats.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> <target>設定からSimpleXのロックをオンにすることができます。</target> @@ -5604,21 +8084,25 @@ Repeat join request?</source> <target>メッセージの書式にmarkdownを使用することができます:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can view invitation link again in connection details." xml:space="preserve"> + <source>You can view invitation link again in connection details.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> <target>メッセージを送信できませんでした!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <target>あなたはメッセージの受信に使用するサーバーを制御し、連絡先はあなたがメッセージの送信に使用するサーバーを使用することができます。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <target>確認できませんでした。 もう一度お試しください。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> + <target>あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You have already requested connection via this address!" xml:space="preserve"> <source>You have already requested connection via this address!</source> <note>No comment provided by engineer.</note> @@ -5628,11 +8112,6 @@ Repeat join request?</source> Repeat connection request?</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> - <target>あなたはチャットがありません</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> <target>アプリ起動時にパスフレーズを入力しなければなりません。端末に保存されてません。</target> @@ -5653,11 +8132,23 @@ Repeat connection request?</source> <target>グループに参加しました。招待をくれたメンバーに接続してます。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You may migrate the exported database." xml:space="preserve"> + <source>You may migrate the exported database.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may save the exported archive." xml:space="preserve"> + <source>You may save the exported archive.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> <target>あなたの最新データベースを1つの端末にしか使わなければ、一部の連絡先からメッセージが届きかねます。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You need to allow your contact to call to be able to call them." xml:space="preserve"> + <source>You need to allow your contact to call to be able to call them.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> <target>音声メッセージを送るには、連絡相手からの音声メッセージを許可しなければなりません。</target> @@ -5673,6 +8164,10 @@ Repeat connection request?</source> <target>グループの招待を送りました</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You should receive notifications." xml:space="preserve"> + <source>You should receive notifications.</source> + <note>token info</note> + </trans-unit> <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> <target>グループのホスト端末がオンラインになったら、接続されます。後でチェックするか、しばらくお待ちください!</target> @@ -5706,6 +8201,10 @@ Repeat connection request?</source> <target>ミュートされたプロフィールがアクティブな場合でも、そのプロフィールからの通話や通知は引き続き受信します。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You will stop receiving messages from this chat. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this chat. Chat history will be preserved.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> <target>このグループからのメッセージが届かなくなります。チャットの履歴が残ります。</target> @@ -5726,31 +8225,16 @@ Repeat connection request?</source> <target>シークレットモードのプロフィールでこのグループに参加しています。メインのプロフィールを守るために、招待することができません</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your %@ servers" xml:space="preserve"> - <source>Your %@ servers</source> - <target>あなたの %@ サーバー</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your ICE servers" xml:space="preserve"> <source>Your ICE servers</source> <target>あなたのICEサーバ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> - <target>あなたのSMPサーバ</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your SimpleX address" xml:space="preserve"> <source>Your SimpleX address</source> <target>あなたのSimpleXアドレス</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your XFTP servers" xml:space="preserve"> - <source>Your XFTP servers</source> - <target>あなたのXFTPサーバ</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your calls" xml:space="preserve"> <source>Your calls</source> <target>あなたの通話</target> @@ -5766,16 +8250,17 @@ Repeat connection request?</source> <target>チャット データベースは暗号化されていません - 暗号化するにはパスフレーズを設定してください。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your chat preferences" xml:space="preserve"> + <source>Your chat preferences</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>あなたのチャットプロフィール</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> - <target>接続を完了するには、連絡相手がオンラインになる必要があります。 -この接続をキャンセルして、連絡先を削除をすることもできます (後でやり直すこともできます)。</target> + <trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve"> + <source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> @@ -5788,18 +8273,15 @@ You can cancel this connection and remove the contact (and try later with a new <target>連絡先がメッセージの完全削除を許可できます。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> - <source>Your contacts in SimpleX will see it. -You can change it in Settings.</source> - <target>SimpleX の連絡先に表示されます。 -設定で変更できます。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your contacts will remain connected." xml:space="preserve"> <source>Your contacts will remain connected.</source> <target>連絡先は接続されたままになります。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve"> + <source>Your credentials may be sent unencrypted.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> <target>現在のチャット データベースは削除され、インポートされたデータベースに置き換えられます。</target> @@ -5829,33 +8311,34 @@ You can change it in Settings.</source> <target>あなたのプロファイル **%@** が共有されます。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> - <target>プロフィールはデバイスに保存され、連絡先とのみ共有されます。 -SimpleX サーバーはあなたのプロファイルを参照できません。</target> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> + <target>プロフィールは連絡先にしか共有されません。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> - <target>あなたのプロフィール、連絡先、送信したメッセージがご自分の端末に保存されます。</target> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> + <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> + <target>プロフィールはデバイスに保存され、連絡先とのみ共有されます。 SimpleX サーバーはあなたのプロファイルを参照できません。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve"> + <source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> <target>あなたのランダム・プロフィール</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> - <target>あなたのサーバ</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> <target>あなたのサーバアドレス</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your servers" xml:space="preserve"> + <source>Your servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your settings" xml:space="preserve"> <source>Your settings</source> <target>あなたの設定</target> @@ -5896,11 +8379,19 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>受けた通話</target> <note>call status</note> </trans-unit> + <trans-unit id="accepted invitation" xml:space="preserve"> + <source>accepted invitation</source> + <note>chat list item title</note> + </trans-unit> <trans-unit id="admin" xml:space="preserve"> <source>admin</source> <target>管理者</target> <note>member role</note> </trans-unit> + <trans-unit id="admins" xml:space="preserve"> + <source>admins</source> + <note>feature role</note> + </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> <target>%@の暗号化に同意しています…</target> @@ -5911,6 +8402,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>暗号化に同意しています…</target> <note>chat item text</note> </trans-unit> + <trans-unit id="all members" xml:space="preserve"> + <source>all members</source> + <note>feature role</note> + </trans-unit> <trans-unit id="always" xml:space="preserve"> <source>always</source> <target>常に</target> @@ -5920,6 +8415,14 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <source>and %lld other events</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="archived report" xml:space="preserve"> + <source>archived report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="attempts" xml:space="preserve"> + <source>attempts</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <source>audio call (not e2e encrypted)</source> <target>音声通話 (エンドツーエンド暗号化なし)</target> @@ -5941,13 +8444,26 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 </trans-unit> <trans-unit id="blocked" xml:space="preserve"> <source>blocked</source> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="blocked %@" xml:space="preserve"> + <source>blocked %@</source> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="blocked by admin" xml:space="preserve"> + <source>blocked by admin</source> + <note>blocked chat item +marked deleted chat item preview text</note> </trans-unit> <trans-unit id="bold" xml:space="preserve"> <source>bold</source> <target>太文字</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="call" xml:space="preserve"> + <source>call</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="call error" xml:space="preserve"> <source>call error</source> <target>通話エラー</target> @@ -6050,7 +8566,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> <target>接続待ち…</target> - <note>chat list item title</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> @@ -6062,6 +8578,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>接続:%@</target> <note>connection information</note> </trans-unit> + <trans-unit id="contact %@ changed to %@" xml:space="preserve"> + <source>contact %1$@ changed to %2$@</source> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> <target>連絡先はエンドツーエンド暗号化があります</target> @@ -6092,10 +8612,15 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>日</target> <note>time unit</note> </trans-unit> + <trans-unit id="decryption errors" xml:space="preserve"> + <source>decryption errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> <target>デフォルト (%@)</target> - <note>pref value</note> + <note>delete after time +pref value</note> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> @@ -6141,6 +8666,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>重複メッセージ</target> <note>integrity error chat item</note> </trans-unit> + <trans-unit id="duplicates" xml:space="preserve"> + <source>duplicates</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> <target>エンドツーエンド暗号化済み</target> @@ -6216,9 +8745,12 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>エラー</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="event happened" xml:space="preserve"> - <source>event happened</source> - <target>イベント発生</target> + <trans-unit id="expired" xml:space="preserve"> + <source>expired</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="forwarded" xml:space="preserve"> + <source>forwarded</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> @@ -6246,6 +8778,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>iOS キーチェーンは、アプリを再起動するかパスフレーズを変更した後にパスフレーズを安全に保存するために使用され、プッシュ通知を受信できるようになります。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="inactive" xml:space="preserve"> + <source>inactive</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> <target>連絡先リンク経由でシークレットモード</target> @@ -6286,6 +8822,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>グループ %@ への招待</target> <note>group name</note> </trans-unit> + <trans-unit id="invite" xml:space="preserve"> + <source>invite</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> <target>招待済み</target> @@ -6331,11 +8871,19 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>メンバー</target> <note>member role</note> </trans-unit> + <trans-unit id="member %@ changed to %@" xml:space="preserve"> + <source>member %1$@ changed to %2$@</source> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> <target>接続中</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="message" xml:space="preserve"> + <source>message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="message received" xml:space="preserve"> <source>message received</source> <target>メッセージを受信</target> @@ -6359,7 +8907,11 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <trans-unit id="moderated by %@" xml:space="preserve"> <source>moderated by %@</source> <target>%@ によってモデレートされた</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="moderator" xml:space="preserve"> + <source>moderator</source> + <note>member role</note> </trans-unit> <trans-unit id="months" xml:space="preserve"> <source>months</source> @@ -6369,7 +8921,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <trans-unit id="never" xml:space="preserve"> <source>never</source> <target>一度も</target> - <note>No comment provided by engineer.</note> + <note>delete after time</note> </trans-unit> <trans-unit id="new message" xml:space="preserve"> <source>new message</source> @@ -6400,8 +8952,8 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <source>off</source> <target>オフ</target> <note>enabled status - group pref value - time to disappear</note> +group pref value +time to disappear</note> </trans-unit> <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> @@ -6418,16 +8970,40 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>オン</target> <note>group pref value</note> </trans-unit> + <trans-unit id="other" xml:space="preserve"> + <source>other</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="other errors" xml:space="preserve"> + <source>other errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="owner" xml:space="preserve"> <source>owner</source> <target>オーナー</target> <note>member role</note> </trans-unit> + <trans-unit id="owners" xml:space="preserve"> + <source>owners</source> + <note>feature role</note> + </trans-unit> <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> <target>P2P</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="pending" xml:space="preserve"> + <source>pending</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="pending approval" xml:space="preserve"> + <source>pending approval</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="quantum resistant e2e encryption" xml:space="preserve"> + <source>quantum resistant e2e encryption</source> + <note>chat item text</note> + </trans-unit> <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> <target>回答を受け取りました…</target> @@ -6438,6 +9014,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>確認を受け取りました…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="rejected" xml:space="preserve"> + <source>rejected</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> <target>拒否した通話</target> @@ -6453,11 +9033,35 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>%@ を除名されました</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="removed contact address" xml:space="preserve"> + <source>removed contact address</source> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="removed profile picture" xml:space="preserve"> + <source>removed profile picture</source> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> <target>あなたを除名しました</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="requested to connect" xml:space="preserve"> + <source>requested to connect</source> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="saved" xml:space="preserve"> + <source>saved</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="saved from %@" xml:space="preserve"> + <source>saved from %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="search" xml:space="preserve"> + <source>search</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="sec" xml:space="preserve"> <source>sec</source> <target>秒</target> @@ -6482,6 +9086,24 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <source>send direct message</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve"> + <source>server queue info: %1$@ + +last received msg: %2$@</source> + <note>queue info</note> + </trans-unit> + <trans-unit id="set new contact address" xml:space="preserve"> + <source>set new contact address</source> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="set new profile picture" xml:space="preserve"> + <source>set new profile picture</source> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="standard end-to-end encryption" xml:space="preserve"> + <source>standard end-to-end encryption</source> + <note>chat item text</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>接続中…</target> @@ -6497,16 +9119,36 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>この連絡先</target> <note>notification title</note> </trans-unit> + <trans-unit id="unblocked %@" xml:space="preserve"> + <source>unblocked %@</source> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="unknown" xml:space="preserve"> <source>unknown</source> <target>不明</target> <note>connection info</note> </trans-unit> + <trans-unit id="unknown servers" xml:space="preserve"> + <source>unknown servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unknown status" xml:space="preserve"> + <source>unknown status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unprotected" xml:space="preserve"> + <source>unprotected</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="updated group profile" xml:space="preserve"> <source>updated group profile</source> <target>グループプロフィールを更新しました</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="updated profile" xml:space="preserve"> + <source>updated profile</source> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="v%@" xml:space="preserve"> <source>v%@</source> <note>No comment provided by engineer.</note> @@ -6536,6 +9178,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>リレー経由</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="video" xml:space="preserve"> + <source>video</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> <source>video call (not e2e encrypted)</source> <target>ビデオ通話 (非エンドツーエンド暗号化)</target> @@ -6561,11 +9207,19 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>週</target> <note>time unit</note> </trans-unit> + <trans-unit id="when IP hidden" xml:space="preserve"> + <source>when IP hidden</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="yes" xml:space="preserve"> <source>yes</source> <target>はい</target> <note>pref value</note> </trans-unit> + <trans-unit id="you" xml:space="preserve"> + <source>you</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> <target>グループ招待が届きました</target> @@ -6576,6 +9230,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>あなたはオブザーバーです</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="you blocked %@" xml:space="preserve"> + <source>you blocked %@</source> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> <target>アドレスを変えました</target> @@ -6616,6 +9274,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>シークレットモードで使い捨てリンクを送りました</target> <note>chat list item description</note> </trans-unit> + <trans-unit id="you unblocked %@" xml:space="preserve"> + <source>you unblocked %@</source> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you: " xml:space="preserve"> <source>you: </source> <target>あなた: </target> @@ -6630,7 +9292,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="ja" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6640,12 +9302,12 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 </trans-unit> <trans-unit id="NSCameraUsageDescription" xml:space="preserve"> <source>SimpleX needs camera access to scan QR codes to connect to other users and for video calls.</source> - <target>SimpleX は、他のユーザーに接続したりビデオ通話を行うために QR コードをスキャンするためにカメラにアクセスする必要があります。</target> + <target>SimpleXは他のユーザーに接続したりビデオ通話する際にQRコード読み取りのためにカメラにアクセスする必要があります。</target> <note>Privacy - Camera Usage Description</note> </trans-unit> <trans-unit id="NSFaceIDUsageDescription" xml:space="preserve"> <source>SimpleX uses Face ID for local authentication</source> - <target>SimpleX はローカル認証に Face ID を使用します</target> + <target>SimpleXはローカル認証にFace IDを使用します</target> <note>Privacy - Face ID Usage Description</note> </trans-unit> <trans-unit id="NSLocalNetworkUsageDescription" xml:space="preserve"> @@ -6654,19 +9316,19 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 </trans-unit> <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> <source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source> - <target>SimpleX では、音声通話やビデオ通話、および音声メッセージの録音のためにマイクへのアクセスが必要です。</target> + <target>SimpleXは音声通話やビデオ通話および音声メッセージの録音のためにマイクにアクセスする必要があります。</target> <note>Privacy - Microphone Usage Description</note> </trans-unit> <trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve"> <source>SimpleX needs access to Photo Library for saving captured and received media</source> - <target>SimpleX は、キャプチャおよび受信したメディアを保存するためにフォト ライブラリにアクセスする必要があります</target> + <target>SimpleXはキャプチャおよび受信したメディアを保存するためにフォトライブラリにアクセスする必要があります</target> <note>Privacy - Photo Library Additions Usage Description</note> </trans-unit> </body> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="ja" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> @@ -6686,4 +9348,205 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 </trans-unit> </body> </file> + <file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="ja" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%d new events" xml:space="preserve"> + <source>%d new events</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From %d chat(s)" xml:space="preserve"> + <source>From %d chat(s)</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From: %@" xml:space="preserve"> + <source>From: %@</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <note>notification</note> + </trans-unit> + <trans-unit id="New messages" xml:space="preserve"> + <source>New messages</source> + <note>notification</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="ja" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> + <source>SimpleX SE</source> + <note>Bundle display name</note> + </trans-unit> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX SE</source> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> + <source>Copyright © 2024 SimpleX Chat. All rights reserved.</source> + <note>Copyright (human-readable)</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="ja" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App is locked!" xml:space="preserve"> + <source>App is locked!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Comment" xml:space="preserve"> + <source>Comment</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade required" xml:space="preserve"> + <source>Database downgrade required</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade required" xml:space="preserve"> + <source>Database upgrade required</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing file" xml:space="preserve"> + <source>Error preparing file</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing message" xml:space="preserve"> + <source>Error preparing message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No active profile" xml:space="preserve"> + <source>No active profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to downgrade the database." xml:space="preserve"> + <source>Open the app to downgrade the database.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to upgrade the database." xml:space="preserve"> + <source>Open the app to upgrade the database.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passphrase" xml:space="preserve"> + <source>Passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve"> + <source>Please create a profile in the SimpleX app</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending a message takes longer than expected." xml:space="preserve"> + <source>Sending a message takes longer than expected.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending message…" xml:space="preserve"> + <source>Sending message…</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Slow network?" xml:space="preserve"> + <source>Slow network?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unsupported format" xml:space="preserve"> + <source>Unsupported format</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wait" xml:space="preserve"> + <source>Wait</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve"> + <source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + </body> + </file> </xliff> diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/contents.json b/apps/ios/SimpleX Localizations/ja.xcloc/contents.json index 7d3c224e68..ce6052fc44 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/ja.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "ja", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "16.2" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff b/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff index cc7b5522e6..019f63cbc0 100644 --- a/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff +++ b/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff @@ -5,9 +5,11 @@ <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/> </header> <body> - <trans-unit id=" " xml:space="preserve"> + <trans-unit id=" " xml:space="preserve" approved="no"> <source> </source> + <target state="translated"> +</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id=" " xml:space="preserve"> @@ -22,220 +24,264 @@ <source> </source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" (" xml:space="preserve"> + <trans-unit id=" (" xml:space="preserve" approved="no"> <source> (</source> + <target state="translated"> (</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" (can be copied)" xml:space="preserve"> + <trans-unit id=" (can be copied)" xml:space="preserve" approved="no"> <source> (can be copied)</source> + <target state="translated"> (복사 가능)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="!1 colored!" xml:space="preserve"> + <trans-unit id="!1 colored!" xml:space="preserve" approved="no"> <source>!1 colored!</source> + <target state="translated">!1 색상 적용됨!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="#secret#" xml:space="preserve"> + <trans-unit id="#secret#" xml:space="preserve" approved="no"> <source>#secret#</source> + <target state="translated">#비밀#</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@" xml:space="preserve"> + <trans-unit id="%@" xml:space="preserve" approved="no"> <source>%@</source> + <target state="translated">%@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ %@" xml:space="preserve"> + <trans-unit id="%@ %@" xml:space="preserve" approved="no"> <source>%@ %@</source> + <target state="translated">%@ %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ / %@" xml:space="preserve"> + <trans-unit id="%@ / %@" xml:space="preserve" approved="no"> <source>%@ / %@</source> + <target state="translated">%@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ is connected!" xml:space="preserve"> + <trans-unit id="%@ is connected!" xml:space="preserve" approved="no"> <source>%@ is connected!</source> + <target state="translated">%@이(가) 연결되었습니다!</target> <note>notification title</note> </trans-unit> - <trans-unit id="%@ is not verified" xml:space="preserve"> + <trans-unit id="%@ is not verified" xml:space="preserve" approved="no"> <source>%@ is not verified</source> + <target state="translated">%@은(는) 인증되지 않았습니다</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ is verified" xml:space="preserve"> + <trans-unit id="%@ is verified" xml:space="preserve" approved="no"> <source>%@ is verified</source> + <target state="translated">%@ 은(는) 인증되었습니다</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ wants to connect!" xml:space="preserve"> + <trans-unit id="%@ wants to connect!" xml:space="preserve" approved="no"> <source>%@ wants to connect!</source> + <target state="translated">%@ 연결을 원함!</target> <note>notification title</note> </trans-unit> - <trans-unit id="%d days" xml:space="preserve"> + <trans-unit id="%d days" xml:space="preserve" approved="no"> <source>%d days</source> + <target state="translated">%d 일</target> <note>message ttl</note> </trans-unit> - <trans-unit id="%d hours" xml:space="preserve"> + <trans-unit id="%d hours" xml:space="preserve" approved="no"> <source>%d hours</source> + <target state="translated">%d 시간</target> <note>message ttl</note> </trans-unit> - <trans-unit id="%d min" xml:space="preserve"> + <trans-unit id="%d min" xml:space="preserve" approved="no"> <source>%d min</source> + <target state="translated">%d 분</target> <note>message ttl</note> </trans-unit> - <trans-unit id="%d months" xml:space="preserve"> + <trans-unit id="%d months" xml:space="preserve" approved="no"> <source>%d months</source> + <target state="translated">%d 개월</target> <note>message ttl</note> </trans-unit> - <trans-unit id="%d sec" xml:space="preserve"> + <trans-unit id="%d sec" xml:space="preserve" approved="no"> <source>%d sec</source> + <target state="translated">%d 초</target> <note>message ttl</note> </trans-unit> - <trans-unit id="%d skipped message(s)" xml:space="preserve"> + <trans-unit id="%d skipped message(s)" xml:space="preserve" approved="no"> <source>%d skipped message(s)</source> + <target state="translated">건너뛰기 메시지 %d개</target> <note>integrity error chat item</note> </trans-unit> - <trans-unit id="%lld" xml:space="preserve"> + <trans-unit id="%lld" xml:space="preserve" approved="no"> <source>%lld</source> + <target state="translated">%lld</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld %@" xml:space="preserve"> + <trans-unit id="%lld %@" xml:space="preserve" approved="no"> <source>%lld %@</source> + <target state="translated">%lld %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld contact(s) selected" xml:space="preserve"> + <trans-unit id="%lld contact(s) selected" xml:space="preserve" approved="no"> <source>%lld contact(s) selected</source> + <target state="translated">%lld명의 연락처 선택됨</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve"> + <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve" approved="no"> <source>%lld file(s) with total size of %@</source> + <target state="translated">총 크기가 %@인 파일 %lld 개</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld members" xml:space="preserve"> + <trans-unit id="%lld members" xml:space="preserve" approved="no"> <source>%lld members</source> + <target state="translated">%lld명의 멤버</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> + <trans-unit id="%lld second(s)" xml:space="preserve" approved="no"> <source>%lld second(s)</source> + <target state="translated">%lld 초</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldd" xml:space="preserve"> + <trans-unit id="%lldd" xml:space="preserve" approved="no"> <source>%lldd</source> + <target state="translated">%lldd</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldh" xml:space="preserve"> + <trans-unit id="%lldh" xml:space="preserve" approved="no"> <source>%lldh</source> + <target state="translated">%lldh</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldk" xml:space="preserve"> + <trans-unit id="%lldk" xml:space="preserve" approved="no"> <source>%lldk</source> + <target state="translated">%lldk</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldm" xml:space="preserve"> + <trans-unit id="%lldm" xml:space="preserve" approved="no"> <source>%lldm</source> + <target state="translated">%lldm</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldmth" xml:space="preserve"> + <trans-unit id="%lldmth" xml:space="preserve" approved="no"> <source>%lldmth</source> + <target state="translated">%lldmth</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%llds" xml:space="preserve"> <source>%llds</source> + <target></target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lldw" xml:space="preserve"> <source>%lldw</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> + <trans-unit id="(" xml:space="preserve" approved="no"> <source>(</source> + <target state="translated">(</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve"> + <trans-unit id=")" xml:space="preserve" approved="no"> <source>)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> + <target state="translated">)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> <source>**Create link / QR code** for your contact to use.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> + <target state="translated">**비공개**: 20분마다 새로운 메시지를 확인합니다. 푸시 서버에는 장치 토큰만 공유됩니다. 연락처 수나 메세지 메타데이터가 표시되지 않습니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> + <target state="translated">**비공개**: SimpleX 채팅 푸시 서버를 사용하지 마세요. 앱은 사용 빈도에 따라 시스템이 허용하는 백그라운드에서 메세지를 확인합니다.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> + <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve" approved="no"> <source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source> + <target state="translated">**참고**: 비밀번호를 분실하면 복구하거나 변경할 수 없습니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> + <target state="translated">**권장**: 디바이스 토큰과 종단 간 암호화 알림이 SimpleX 채팅 푸시 서버로 전송되지만 메세지 내용, 크기 또는 발신자가 표시되지 않습니다.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> <source>**Scan QR code**: to connect to your contact in person or via video call.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> + <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve" approved="no"> <source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source> + <target state="translated">**경고**: 즉각적인 푸시 알림은 암호문을 키체인에 저장해야 합니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> + <trans-unit id="**e2e encrypted** audio call" xml:space="preserve" approved="no"> <source>**e2e encrypted** audio call</source> + <target state="translated">**e2e** 오디오 통화</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**e2e encrypted** video call" xml:space="preserve"> + <trans-unit id="**e2e encrypted** video call" xml:space="preserve" approved="no"> <source>**e2e encrypted** video call</source> + <target state="translated">**e2e 암호화** 영상 통화</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="*bold*" xml:space="preserve"> <source>\*bold*</source> + <target></target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=", " xml:space="preserve"> + <trans-unit id=", " xml:space="preserve" approved="no"> <source>, </source> + <target state="translated">, </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="." xml:space="preserve"> + <trans-unit id="." xml:space="preserve" approved="no"> <source>.</source> + <target state="translated">.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="1 day" xml:space="preserve"> + <trans-unit id="1 day" xml:space="preserve" approved="no"> <source>1 day</source> + <target state="translated">1일</target> <note>message ttl</note> </trans-unit> - <trans-unit id="1 hour" xml:space="preserve"> + <trans-unit id="1 hour" xml:space="preserve" approved="no"> <source>1 hour</source> + <target state="translated">1시간</target> <note>message ttl</note> </trans-unit> - <trans-unit id="1 month" xml:space="preserve"> + <trans-unit id="1 month" xml:space="preserve" approved="no"> <source>1 month</source> + <target state="translated">1개월</target> <note>message ttl</note> </trans-unit> - <trans-unit id="1 week" xml:space="preserve"> + <trans-unit id="1 week" xml:space="preserve" approved="no"> <source>1 week</source> + <target state="translated">1주</target> <note>message ttl</note> </trans-unit> <trans-unit id="2 weeks" xml:space="preserve"> <source>2 weeks</source> <note>message ttl</note> </trans-unit> - <trans-unit id="6" xml:space="preserve"> + <trans-unit id="6" xml:space="preserve" approved="no"> <source>6</source> + <target state="translated">6</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve"> + <trans-unit id=": " xml:space="preserve" approved="no"> <source>: </source> + <target state="translated">: </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="A new contact" xml:space="preserve"> + <trans-unit id="A new contact" xml:space="preserve" approved="no"> <source>A new contact</source> + <target state="translated">새로운 연결</target> <note>notification title</note> </trans-unit> <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve"> @@ -246,29 +292,34 @@ <source>A random profile will be sent to your contact</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> + <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve" approved="no"> <source>A separate TCP connection will be used **for each chat profile you have in the app**.</source> + <target state="translated">앱에 있는 각 채팅 프로필**마다 별도의 TCP 연결이 사용됩니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." xml:space="preserve"> + <trans-unit id="A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." xml:space="preserve" approved="no"> <source>A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</source> + <target state="translated">각 연락처 및 그룹 구성원**마다 별도의 TCP 연결이 사용됩니다. +**참고**: 연결이 많으면 배터리와 트래픽 소비가 상당히 증가하고 일부 연결이 실패할 수 있습니다.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="About SimpleX" xml:space="preserve"> <source>About SimpleX</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX Chat" xml:space="preserve"> + <trans-unit id="About SimpleX Chat" xml:space="preserve" approved="no"> <source>About SimpleX Chat</source> + <target state="translated">SimpleX Chat에 대하여</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accent color" xml:space="preserve"> <source>Accent color</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accept" xml:space="preserve"> + <trans-unit id="Accept" xml:space="preserve" approved="no"> <source>Accept</source> + <target state="translated">승인</target> <note>accept contact request via notification accept incoming call via notification</note> </trans-unit> @@ -276,12 +327,14 @@ <source>Accept contact</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accept contact request from %@?" xml:space="preserve"> + <trans-unit id="Accept contact request from %@?" xml:space="preserve" approved="no"> <source>Accept contact request from %@?</source> + <target state="translated">%@의 연락 요청을 수락하시겠습니까?</target> <note>notification body</note> </trans-unit> - <trans-unit id="Accept incognito" xml:space="preserve"> + <trans-unit id="Accept incognito" xml:space="preserve" approved="no"> <source>Accept incognito</source> + <target state="translated">인정하지 않음</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept requests" xml:space="preserve"> @@ -292,337 +345,411 @@ <source>Add preset servers</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add profile" xml:space="preserve"> + <trans-unit id="Add profile" xml:space="preserve" approved="no"> <source>Add profile</source> + <target state="translated">프로필 추가하기</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> + <trans-unit id="Add servers by scanning QR codes." xml:space="preserve" approved="no"> <source>Add servers by scanning QR codes.</source> + <target state="translated">QR 코드를 스캔하여 서버를 추가합니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> + <trans-unit id="Add server" xml:space="preserve" approved="no"> + <source>Add server</source> + <target state="translated">서버 추가하기</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add to another device" xml:space="preserve"> + <trans-unit id="Add to another device" xml:space="preserve" approved="no"> <source>Add to another device</source> + <target state="translated">다른 장치에 추가하기</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add welcome message" xml:space="preserve"> + <trans-unit id="Add welcome message" xml:space="preserve" approved="no"> <source>Add welcome message</source> + <target state="translated">환영 메세지 추가하기</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> + <trans-unit id="Admins can create the links to join groups." xml:space="preserve" approved="no"> <source>Admins can create the links to join groups.</source> + <target state="translated">관리자는 그룹에 가입할 수 있는 링크를 만들 수 있습니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Advanced network settings" xml:space="preserve"> + <trans-unit id="Advanced network settings" xml:space="preserve" approved="no"> <source>Advanced network settings</source> + <target state="translated">고급 네트워크 설정</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve"> + <trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve" approved="no"> <source>All chats and messages will be deleted - this cannot be undone!</source> + <target state="translated">모든 채팅과 메세지가 삭제됩니다. - 수정 불가능!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All group members will remain connected." xml:space="preserve"> + <trans-unit id="All group members will remain connected." xml:space="preserve" approved="no"> <source>All group members will remain connected.</source> + <target state="translated">모든 그룹 구성원은 연결 상태를 유지합니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> + <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve" approved="no"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> + <target state="translated">모든 메세지가 삭제됩니다 - 수정할 수 없습니다! 메세지는 오직 당신만을 위해 삭제될 것입니다.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All your contacts will remain connected" xml:space="preserve"> <source>All your contacts will remain connected</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow" xml:space="preserve"> + <trans-unit id="Allow" xml:space="preserve" approved="no"> <source>Allow</source> + <target state="translated">승인</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> + <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve" approved="no"> <source>Allow disappearing messages only if your contact allows it to you.</source> + <target state="translated">연락처가 메세지를 허용하는 경우에만 메세지 삭제를 허용합니다.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> <source>Allow irreversible message deletion only if your contact allows it to you.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow sending direct messages to members." xml:space="preserve"> + <trans-unit id="Allow sending direct messages to members." xml:space="preserve" approved="no"> <source>Allow sending direct messages to members.</source> + <target state="translated">회원에게 직접 메시지를 보낼 수 있습니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow sending disappearing messages." xml:space="preserve"> + <trans-unit id="Allow sending disappearing messages." xml:space="preserve" approved="no"> <source>Allow sending disappearing messages.</source> + <target state="translated">사라지는 메시지를 보내는 것을 허용합니다.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> <source>Allow to irreversibly delete sent messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to send voice messages." xml:space="preserve"> + <trans-unit id="Allow to send voice messages." xml:space="preserve" approved="no"> <source>Allow to send voice messages.</source> + <target state="translated">음성 메세지를 보낼 수 있습니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve"> + <trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve" approved="no"> <source>Allow voice messages only if your contact allows them.</source> + <target state="translated">연락처가 음성 메세지를 허용하는 경우에만 음성 메세지를 허용합니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow voice messages?" xml:space="preserve"> + <trans-unit id="Allow voice messages?" xml:space="preserve" approved="no"> <source>Allow voice messages?</source> + <target state="translated">음성 메세지를 허용 하겠습니까?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> <source>Allow your contacts to irreversibly delete sent messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> + <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve" approved="no"> <source>Allow your contacts to send disappearing messages.</source> + <target state="translated">연락처가 사라지는 메시지를 보낼 수 있도록 허용합니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to send voice messages." xml:space="preserve"> + <trans-unit id="Allow your contacts to send voice messages." xml:space="preserve" approved="no"> <source>Allow your contacts to send voice messages.</source> + <target state="translated">연락처가 음성 메시지를 보낼 수 있도록 허용합니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Already connected?" xml:space="preserve"> + <trans-unit id="Already connected?" xml:space="preserve" approved="no"> <source>Already connected?</source> + <target state="translated">이미 연결되었나요?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Always use relay" xml:space="preserve"> + <trans-unit id="Always use relay" xml:space="preserve" approved="no"> <source>Always use relay</source> + <target state="translated">항상 릴레이 사용</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Answer call" xml:space="preserve"> + <trans-unit id="Answer call" xml:space="preserve" approved="no"> <source>Answer call</source> + <target state="translated">응답 전화</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App build: %@" xml:space="preserve"> + <trans-unit id="App build: %@" xml:space="preserve" approved="no"> <source>App build: %@</source> + <target state="translated">앱 빌드: %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App icon" xml:space="preserve"> + <trans-unit id="App icon" xml:space="preserve" approved="no"> <source>App icon</source> + <target state="translated">앱 아이콘</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App version" xml:space="preserve"> + <trans-unit id="App version" xml:space="preserve" approved="no"> <source>App version</source> + <target state="translated">앱 버전</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App version: v%@" xml:space="preserve"> + <trans-unit id="App version: v%@" xml:space="preserve" approved="no"> <source>App version: v%@</source> + <target state="translated">앱 버전: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Appearance" xml:space="preserve"> + <trans-unit id="Appearance" xml:space="preserve" approved="no"> <source>Appearance</source> + <target state="translated">출석</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Attach" xml:space="preserve"> + <trans-unit id="Attach" xml:space="preserve" approved="no"> <source>Attach</source> + <target state="translated">첨부</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Audio & video calls" xml:space="preserve"> + <trans-unit id="Audio & video calls" xml:space="preserve" approved="no"> <source>Audio & video calls</source> + <target state="translated">음성 & 영상 통화</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Audio and video calls" xml:space="preserve"> + <trans-unit id="Audio and video calls" xml:space="preserve" approved="no"> <source>Audio and video calls</source> + <target state="translated">음성 및 영상 통화</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Authentication failed" xml:space="preserve"> + <trans-unit id="Authentication failed" xml:space="preserve" approved="no"> <source>Authentication failed</source> + <target state="translated">인증 실패</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Authentication is required before the call is connected, but you may miss calls." xml:space="preserve"> + <trans-unit id="Authentication is required before the call is connected, but you may miss calls." xml:space="preserve" approved="no"> <source>Authentication is required before the call is connected, but you may miss calls.</source> + <target state="translated">통화가 연결되기 전에 인증이 필요하지만, 통화를 놓칠 수 있습니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Authentication unavailable" xml:space="preserve"> + <trans-unit id="Authentication unavailable" xml:space="preserve" approved="no"> <source>Authentication unavailable</source> + <target state="translated">인증 사용 불가</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Auto-accept contact requests" xml:space="preserve"> + <trans-unit id="Auto-accept contact requests" xml:space="preserve" approved="no"> <source>Auto-accept contact requests</source> + <target state="translated">연락처 요청 자동 수락</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Auto-accept images" xml:space="preserve"> + <trans-unit id="Auto-accept images" xml:space="preserve" approved="no"> <source>Auto-accept images</source> + <target state="translated">이미지 자동 수락</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Automatically" xml:space="preserve"> <source>Automatically</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Back" xml:space="preserve"> + <trans-unit id="Back" xml:space="preserve" approved="no"> <source>Back</source> + <target state="translated">뒤로가기</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> <source>Both you and your contact can irreversibly delete sent messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can send disappearing messages." xml:space="preserve"> + <trans-unit id="Both you and your contact can send disappearing messages." xml:space="preserve" approved="no"> <source>Both you and your contact can send disappearing messages.</source> + <target state="translated">당신과 당신의 연락처 모두 사라지는 메시지를 보낼 수 있습니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can send voice messages." xml:space="preserve"> + <trans-unit id="Both you and your contact can send voice messages." xml:space="preserve" approved="no"> <source>Both you and your contact can send voice messages.</source> + <target state="translated">당신과 당신의 연락처 모두 음성 메시지를 보낼 수 있습니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> + <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve" approved="no"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> + <target state="translated">채팅 프로필(기본값) 또는 [연결](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Call already ended!" xml:space="preserve"> + <trans-unit id="Call already ended!" xml:space="preserve" approved="no"> <source>Call already ended!</source> + <target state="translated">통화가 이미 종료되었습니다!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Calls" xml:space="preserve"> + <trans-unit id="Calls" xml:space="preserve" approved="no"> <source>Calls</source> + <target state="translated">통화</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't delete user profile!" xml:space="preserve"> <source>Can't delete user profile!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't invite contact!" xml:space="preserve"> + <trans-unit id="Can't invite contact!" xml:space="preserve" approved="no"> <source>Can't invite contact!</source> + <target state="translated">주소를 초대할 수 없습니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't invite contacts!" xml:space="preserve"> + <trans-unit id="Can't invite contacts!" xml:space="preserve" approved="no"> <source>Can't invite contacts!</source> + <target state="translated">연락처를 초대할 수 없습니다!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Cancel" xml:space="preserve"> + <trans-unit id="Cancel" xml:space="preserve" approved="no"> <source>Cancel</source> + <target state="translated">취소</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve" approved="no"> <source>Cannot access keychain to save database password</source> + <target state="translated">데이터베이스 암호를 저장하는 키체인에 접근 할 수 없습니다</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Cannot receive file" xml:space="preserve"> + <trans-unit id="Cannot receive file" xml:space="preserve" approved="no"> <source>Cannot receive file</source> + <target state="translated">파일을 받을 수 없습니다</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change" xml:space="preserve"> + <trans-unit id="Change" xml:space="preserve" approved="no"> <source>Change</source> + <target state="translated">변경</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change database passphrase?" xml:space="preserve"> + <trans-unit id="Change database passphrase?" xml:space="preserve" approved="no"> <source>Change database passphrase?</source> + <target state="translated">데이터베이스 암호 변경?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change member role?" xml:space="preserve"> + <trans-unit id="Change member role?" xml:space="preserve" approved="no"> <source>Change member role?</source> + <target state="translated">멤버 역할을 변경하시겠습니까?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change receiving address" xml:space="preserve"> + <trans-unit id="Change receiving address" xml:space="preserve" approved="no"> <source>Change receiving address</source> + <target state="translated">수신 주소 변경</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change receiving address?" xml:space="preserve" approved="no"> <source>Change receiving address?</source> - <target state="translated">修改接收地址?</target> + <target state="translated">수신 주소를 변경하시겠습니까?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change role" xml:space="preserve"> + <trans-unit id="Change role" xml:space="preserve" approved="no"> <source>Change role</source> + <target state="translated">역할 변경</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> + <trans-unit id="Chat archive" xml:space="preserve" approved="no"> <source>Chat archive</source> + <target state="translated">채팅 기록 보관함</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat console" xml:space="preserve"> + <trans-unit id="Chat console" xml:space="preserve" approved="no"> <source>Chat console</source> + <target state="translated">채팅 콘솔</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat database" xml:space="preserve"> + <trans-unit id="Chat database" xml:space="preserve" approved="no"> <source>Chat database</source> + <target state="translated">채팅 데이터베이스</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat database deleted" xml:space="preserve"> + <trans-unit id="Chat database deleted" xml:space="preserve" approved="no"> <source>Chat database deleted</source> + <target state="translated">채팅 데이터베이스 삭제</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat database imported" xml:space="preserve"> + <trans-unit id="Chat database imported" xml:space="preserve" approved="no"> <source>Chat database imported</source> + <target state="translated">채팅 데이터베이스를 가져옴</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat is running" xml:space="preserve"> + <trans-unit id="Chat is running" xml:space="preserve" approved="no"> <source>Chat is running</source> + <target state="translated">채팅이 실행 중입니다</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat is stopped" xml:space="preserve"> + <trans-unit id="Chat is stopped" xml:space="preserve" approved="no"> <source>Chat is stopped</source> + <target state="translated">채팅이 중단되었습니다</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat preferences" xml:space="preserve"> + <trans-unit id="Chat preferences" xml:space="preserve" approved="no"> <source>Chat preferences</source> + <target state="translated">채팅 환경설정</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chats" xml:space="preserve"> + <trans-unit id="Chats" xml:space="preserve" approved="no"> <source>Chats</source> + <target state="translated">채팅</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Check server address and try again." xml:space="preserve"> + <trans-unit id="Check server address and try again." xml:space="preserve" approved="no"> <source>Check server address and try again.</source> + <target state="translated">서버 주소를 확인한 후 다시 시도합니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> + <trans-unit id="Chinese and Spanish interface" xml:space="preserve" approved="no"> <source>Chinese and Spanish interface</source> + <target state="translated">중국어 및 스페인어 환경</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Choose file" xml:space="preserve"> + <trans-unit id="Choose file" xml:space="preserve" approved="no"> <source>Choose file</source> + <target state="translated">파일 선택</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Choose from library" xml:space="preserve"> + <trans-unit id="Choose from library" xml:space="preserve" approved="no"> <source>Choose from library</source> + <target state="translated">라이브러리에서 선택</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Clear" xml:space="preserve"> + <trans-unit id="Clear" xml:space="preserve" approved="no"> <source>Clear</source> + <target state="translated">정리</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Clear conversation" xml:space="preserve"> + <trans-unit id="Clear conversation" xml:space="preserve" approved="no"> <source>Clear conversation</source> + <target state="translated">대화 삭제</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Clear conversation?" xml:space="preserve"> + <trans-unit id="Clear conversation?" xml:space="preserve" approved="no"> <source>Clear conversation?</source> + <target state="translated">대화 삭제?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Clear verification" xml:space="preserve"> + <trans-unit id="Clear verification" xml:space="preserve" approved="no"> <source>Clear verification</source> + <target state="translated">인증 삭제</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Colors" xml:space="preserve"> <source>Colors</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Compare security codes with your contacts." xml:space="preserve"> + <trans-unit id="Compare security codes with your contacts." xml:space="preserve" approved="no"> <source>Compare security codes with your contacts.</source> + <target state="translated">보안 코드를 연락처와 비교합니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Configure ICE servers" xml:space="preserve"> + <trans-unit id="Configure ICE servers" xml:space="preserve" approved="no"> <source>Configure ICE servers</source> + <target state="translated">ICE 서버 구성</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Confirm" xml:space="preserve"> + <trans-unit id="Confirm" xml:space="preserve" approved="no"> <source>Confirm</source> + <target state="translated">확인</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Confirm new passphrase…" xml:space="preserve"> + <trans-unit id="Confirm new passphrase…" xml:space="preserve" approved="no"> <source>Confirm new passphrase…</source> + <target state="translated">새 암호 확인…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Confirm password" xml:space="preserve"> + <trans-unit id="Confirm password" xml:space="preserve" approved="no"> <source>Confirm password</source> + <target state="translated">비밀번호 확인</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect" xml:space="preserve"> + <trans-unit id="Connect" xml:space="preserve" approved="no"> <source>Connect</source> + <target state="translated">연결</target> <note>server test step</note> </trans-unit> <trans-unit id="Connect via contact link?" xml:space="preserve"> @@ -633,8 +760,9 @@ <source>Connect via group link?</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link" xml:space="preserve"> + <trans-unit id="Connect via link" xml:space="preserve" approved="no"> <source>Connect via link</source> + <target state="translated">링크를 통해 연결</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via link / QR code" xml:space="preserve"> @@ -645,174 +773,210 @@ <source>Connect via one-time link?</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connecting server…" xml:space="preserve"> + <trans-unit id="Connecting server…" xml:space="preserve" approved="no"> <source>Connecting to server…</source> + <target state="translated">서버에 연결중…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connecting server… (error: %@)" xml:space="preserve"> + <trans-unit id="Connecting server… (error: %@)" xml:space="preserve" approved="no"> <source>Connecting to server… (error: %@)</source> + <target state="translated">서버에 연결중...(오류: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection" xml:space="preserve"> + <trans-unit id="Connection" xml:space="preserve" approved="no"> <source>Connection</source> + <target state="translated">연결</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection error" xml:space="preserve"> + <trans-unit id="Connection error" xml:space="preserve" approved="no"> <source>Connection error</source> + <target state="translated">연결 오류</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection error (AUTH)" xml:space="preserve"> + <trans-unit id="Connection error (AUTH)" xml:space="preserve" approved="no"> <source>Connection error (AUTH)</source> + <target state="translated">연결 에러 (인증)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection request" xml:space="preserve"> <source>Connection request</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection request sent!" xml:space="preserve"> + <trans-unit id="Connection request sent!" xml:space="preserve" approved="no"> <source>Connection request sent!</source> + <target state="translated">연결 요청이 전송되었습니다!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection timeout" xml:space="preserve"> + <trans-unit id="Connection timeout" xml:space="preserve" approved="no"> <source>Connection timeout</source> + <target state="translated">연결 시간초과</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contact allows" xml:space="preserve"> + <trans-unit id="Contact allows" xml:space="preserve" approved="no"> <source>Contact allows</source> + <target state="translated">연락 가능</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contact already exists" xml:space="preserve"> + <trans-unit id="Contact already exists" xml:space="preserve" approved="no"> <source>Contact already exists</source> + <target state="translated">연결이 이미 존재</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact and all messages will be deleted - this cannot be undone!" xml:space="preserve"> <source>Contact and all messages will be deleted - this cannot be undone!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contact hidden:" xml:space="preserve"> + <trans-unit id="Contact hidden:" xml:space="preserve" approved="no"> <source>Contact hidden:</source> + <target state="translated">숨겨진 연락처:</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is connected" xml:space="preserve"> + <trans-unit id="Contact is connected" xml:space="preserve" approved="no"> <source>Contact is connected</source> + <target state="translated">연락처가 연결되었습니다</target> <note>notification</note> </trans-unit> <trans-unit id="Contact is not connected yet!" xml:space="preserve"> <source>Contact is not connected yet!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contact name" xml:space="preserve"> + <trans-unit id="Contact name" xml:space="preserve" approved="no"> <source>Contact name</source> + <target state="translated">연락처 이름</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contact preferences" xml:space="preserve"> + <trans-unit id="Contact preferences" xml:space="preserve" approved="no"> <source>Contact preferences</source> + <target state="translated">연락처 선호도</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact requests" xml:space="preserve"> <source>Contact requests</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve"> + <trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve" approved="no"> <source>Contacts can mark messages for deletion; you will be able to view them.</source> + <target state="translated">연락처는 메세지를 삭제하도록 표시할 수 있으며, 이를 확인할 수 있습니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Copy" xml:space="preserve"> + <trans-unit id="Copy" xml:space="preserve" approved="no"> <source>Copy</source> + <target state="translated">복사</target> <note>chat item action</note> </trans-unit> <trans-unit id="Core built at: %@" xml:space="preserve"> <source>Core built at: %@</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Core version: v%@" xml:space="preserve"> + <trans-unit id="Core version: v%@" xml:space="preserve" approved="no"> <source>Core version: v%@</source> + <target state="translated">코어 버전: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create" xml:space="preserve"> + <trans-unit id="Create" xml:space="preserve" approved="no"> <source>Create</source> + <target state="translated">생성</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create address" xml:space="preserve"> <source>Create address</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create group link" xml:space="preserve"> + <trans-unit id="Create group link" xml:space="preserve" approved="no"> <source>Create group link</source> + <target state="translated">그룹 링크 생성</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create link" xml:space="preserve"> + <trans-unit id="Create link" xml:space="preserve" approved="no"> <source>Create link</source> + <target state="translated">링크 생성</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create one-time invitation link" xml:space="preserve"> <source>Create one-time invitation link</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create queue" xml:space="preserve"> + <trans-unit id="Create queue" xml:space="preserve" approved="no"> <source>Create queue</source> + <target state="translated">큐 생성</target> <note>server test step</note> </trans-unit> - <trans-unit id="Create secret group" xml:space="preserve"> + <trans-unit id="Create secret group" xml:space="preserve" approved="no"> <source>Create secret group</source> + <target state="translated">비밀 그룹 생성</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create your profile" xml:space="preserve"> + <trans-unit id="Create your profile" xml:space="preserve" approved="no"> <source>Create your profile</source> + <target state="translated">프로필 생성</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Created on %@" xml:space="preserve"> <source>Created on %@</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Current passphrase…" xml:space="preserve"> + <trans-unit id="Current passphrase…" xml:space="preserve" approved="no"> <source>Current passphrase…</source> + <target state="translated">현재 암호…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve" approved="no"> <source>Currently maximum supported file size is %@.</source> + <target state="translated">현재 지원되는 최대 파일 크기는 %@입니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Dark" xml:space="preserve"> + <trans-unit id="Dark" xml:space="preserve" approved="no"> <source>Dark</source> + <target state="translated">다크</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database ID" xml:space="preserve"> + <trans-unit id="Database ID" xml:space="preserve" approved="no"> <source>Database ID</source> + <target state="translated">데이터베이스 아이디</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database encrypted!" xml:space="preserve"> + <trans-unit id="Database encrypted!" xml:space="preserve" approved="no"> <source>Database encrypted!</source> + <target state="translated">데이터베이스 암호화됨!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database encryption passphrase will be updated and stored in the keychain. " xml:space="preserve"> + <trans-unit id="Database encryption passphrase will be updated and stored in the keychain. " xml:space="preserve" approved="no"> <source>Database encryption passphrase will be updated and stored in the keychain. </source> + <target state="translated">데이터베이스 암호화 키가 키체인에 저장됩니다. +</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database encryption passphrase will be updated. " xml:space="preserve"> + <trans-unit id="Database encryption passphrase will be updated. " xml:space="preserve" approved="no"> <source>Database encryption passphrase will be updated. </source> + <target state="translated">데이터베이스 암호화 키가 업데이트됩니다. +</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database error" xml:space="preserve"> + <trans-unit id="Database error" xml:space="preserve" approved="no"> <source>Database error</source> + <target state="translated">데이터베이스 오류</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database is encrypted using a random passphrase, you can change it." xml:space="preserve"> + <trans-unit id="Database is encrypted using a random passphrase, you can change it." xml:space="preserve" approved="no"> <source>Database is encrypted using a random passphrase, you can change it.</source> + <target state="translated">데이터베이스는 임의의 암호를 사용하여 암호화되므로 변경할 수 있습니다.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database is encrypted using a random passphrase. Please change it before exporting." xml:space="preserve"> + <trans-unit id="Database is encrypted using a random passphrase. Please change it before exporting." xml:space="preserve" approved="no"> <source>Database is encrypted using a random passphrase. Please change it before exporting.</source> + <target state="translated">데이터베이스는 임의의 암호를 사용하여 암호화됩니다. 내보내기 전에 변경하십시오.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database passphrase" xml:space="preserve"> + <trans-unit id="Database passphrase" xml:space="preserve" approved="no"> <source>Database passphrase</source> + <target state="translated">데이터베이스 암호화 키</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database passphrase & export" xml:space="preserve"> + <trans-unit id="Database passphrase & export" xml:space="preserve" approved="no"> <source>Database passphrase & export</source> + <target state="translated">데이터베이스 암호화 키 & 내보내기</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> @@ -1001,8 +1165,8 @@ <source>Direct messages</source> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> @@ -1017,8 +1181,8 @@ <source>Disappearing messages are prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disconnect" xml:space="preserve"> @@ -1409,16 +1573,16 @@ <source>Group members can irreversibly delete sent messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> @@ -1525,8 +1689,8 @@ <source>Image will be received when your contact is online, please wait or check later!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Import" xml:space="preserve"> @@ -1630,8 +1794,8 @@ <source>Irreversible message deletion is prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve"> @@ -1949,8 +2113,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Onion hosts will not be used.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> @@ -2001,8 +2165,9 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Open user profiles</source> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> + <trans-unit id="Anybody can host servers." xml:space="preserve" approved="no"> + <source>Anybody can host servers.</source> + <target state="translated">누구나 서버를 호스팅할 수 있습니다.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve"> @@ -2037,8 +2202,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Paste the link you received into the box below to connect with your contact.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Periodically" xml:space="preserve"> @@ -2397,24 +2562,29 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Send live message</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications" xml:space="preserve"> + <trans-unit id="Send notifications" xml:space="preserve" approved="no"> <source>Send notifications</source> + <target state="translated">알림 전송</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> + <trans-unit id="Send notifications:" xml:space="preserve" approved="no"> <source>Send notifications:</source> + <target state="translated">알림 전송:</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send questions and ideas" xml:space="preserve"> + <trans-unit id="Send questions and ideas" xml:space="preserve" approved="no"> <source>Send questions and ideas</source> + <target state="translated">질문이나 아이디어 보내기</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve"> + <trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve" approved="no"> <source>Send them from gallery or custom keyboards.</source> + <target state="needs-translation">갤러리 또는 사용자 정의 키보드에서 그들을 보내십시오.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> + <trans-unit id="Sender cancelled file transfer." xml:space="preserve" approved="no"> <source>Sender cancelled file transfer.</source> + <target state="translated">상대방이 파일 전송을 취소했습니다.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> @@ -2653,8 +2823,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Thanks to the users – contribute via Weblate!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve"> @@ -2689,16 +2859,16 @@ We will be adding server redundancy to prevent lost messages.</source> <source>The message will be marked as moderated for all members.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> <source>The old database was not removed during the migration, it can be deleted.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The sender will NOT be notified" xml:space="preserve"> @@ -2757,8 +2927,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>To make a new connection</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -2951,8 +3121,8 @@ To connect, please ask your contact to create another connection link and check <source>Voice messages are prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> @@ -3076,10 +3246,6 @@ SimpleX Lock must be enabled.</source> <source>You can't send messages!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <note>No comment provided by engineer.</note> @@ -3755,6 +3921,1206 @@ SimpleX servers cannot see your profile.</source> <source>\~strike~</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change passcode" xml:space="preserve" approved="no"> + <source>Change passcode</source> + <target state="translated">패스코드 변경</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve" approved="no"> + <source>Cellular</source> + <target state="translated">셀룰러</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve" approved="no"> + <source>Send messages directly when your or destination server does not support private routing.</source> + <target state="needs-translation">이 서버 또는 도착 서버가 비밀 라우팅을 지원하지 않을 때 직통 메시지 보내기.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve" approved="no"> + <source>Send up to 100 last messages to new members.</source> + <target state="translated">새로운 멤버에게 최대 100개의 마지막 메시지 보내기.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve" approved="no"> + <source>## History</source> + <target state="translated">## 기록</target> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve" approved="no"> + <source>## In reply to</source> + <target state="translated">## 에 대한 답변</target> + </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve" approved="no"> + <source>%@ downloaded</source> + <target state="translated">%@ 다운로드됨</target> + </trans-unit> + <trans-unit id="# %@" xml:space="preserve" approved="no"> + <source># %@</source> + <target state="translated"># %@</target> + </trans-unit> + <trans-unit id="%@ and %@" xml:space="preserve" approved="no"> + <source>%@ and %@</source> + <target state="translated">%@ 그리고 %@</target> + </trans-unit> + <trans-unit id="%@ at %@:" xml:space="preserve" approved="no"> + <source>%1$@ at %2$@:</source> + <target state="translated">%2$@의 %1$@:</target> + </trans-unit> + <trans-unit id="%@ connected" xml:space="preserve" approved="no"> + <source>%@ connected</source> + <target state="translated">%@ 연결됨</target> + </trans-unit> + <trans-unit id="%@ (current):" xml:space="preserve" approved="no"> + <source>%@ (current):</source> + <target state="translated">%@ (현재):</target> + </trans-unit> + <trans-unit id="%@ (current)" xml:space="preserve" approved="no"> + <source>%@ (current)</source> + <target state="translated">%@ (현재)</target> + </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve" approved="no"> + <source>%@ and %@ connected</source> + <target state="translated">%@ 및 %@이(가) 연결되었습니다</target> + </trans-unit> + <trans-unit id="%@ server" xml:space="preserve" approved="no"> + <source>%@ server</source> + <target state="translated">%@서버</target> + </trans-unit> + <trans-unit id="%@ servers" xml:space="preserve" approved="no"> + <source>%@ servers</source> + <target state="translated">%@서버들</target> + </trans-unit> + <trans-unit id="%@, %@ and %lld members" xml:space="preserve" approved="no"> + <source>%@, %@ and %lld members</source> + <target state="translated">%@, %@ 과 %lld 멤버들</target> + </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve" approved="no"> + <source>%d file(s) are still being downloaded.</source> + <target state="translated">%d 개의 파일 다운로드중.</target> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve" approved="no"> + <source>%d file(s) were deleted.</source> + <target state="translated">%d개의 파일이 삭제됨.</target> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve" approved="no"> + <source>%d file(s) were not downloaded.</source> + <target state="translated">%d개의 파일이 다운로드 되지 않음.</target> + </trans-unit> + <trans-unit id="%d weeks" xml:space="preserve" approved="no"> + <source>%d weeks</source> + <target state="translated">%d 주</target> + </trans-unit> + <trans-unit id="%lld seconds" xml:space="preserve" approved="no"> + <source>%lld seconds</source> + <target state="translated">%lld 초</target> + </trans-unit> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve" approved="no"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target state="translated">**1회 링크 생성** : 새 초대 링크를 생성하고 공유합니다.</target> + </trans-unit> + <trans-unit id="1-time link" xml:space="preserve" approved="no"> + <source>1-time link</source> + <target state="translated">일회성 링크</target> + </trans-unit> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve" approved="no"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> + <target state="translated">일회용 링크는 *한 번의 연락처로만* 사용할 수 있으며, 대면 또는 메신저를 통해 공유할 수 있습니다.</target> + </trans-unit> + <trans-unit id="A few more things" xml:space="preserve" approved="no"> + <source>A few more things</source> + <target state="translated">몇 가지 더</target> + </trans-unit> + <trans-unit id="Accept conditions" xml:space="preserve" approved="no"> + <source>Accept conditions</source> + <target state="translated">조건 수락</target> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve" approved="no"> + <source>Accepted conditions</source> + <target state="translated">수락된 조건</target> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve" approved="no"> + <source>Active connections</source> + <target state="translated">연결 활성화</target> + </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve" approved="no"> + <source>%@ uploaded</source> + <target state="translated">%@업로드됨</target> + </trans-unit> + <trans-unit id="Accept connection request?" xml:space="preserve" approved="no"> + <source>Accept connection request?</source> + <target state="translated">연결 요청을 수락하시겠습니까?</target> + </trans-unit> + <trans-unit id="%lld minutes" xml:space="preserve" approved="no"> + <source>%lld minutes</source> + <target state="translated">%lld 분</target> + </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve" approved="no"> + <source>**Warning**: the archive will be removed.</source> + <target state="translated">**경고**: 보관물이 제거됩니다.</target> + </trans-unit> + <trans-unit id="5 minutes" xml:space="preserve" approved="no"> + <source>5 minutes</source> + <target state="translated">5 분</target> + </trans-unit> + <trans-unit id="Abort changing address" xml:space="preserve" approved="no"> + <source>Abort changing address</source> + <target state="translated">주소 변경 중단</target> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve" approved="no"> + <source>Acknowledgement errors</source> + <target state="translated">확인 오류</target> + </trans-unit> + <trans-unit id="Abort" xml:space="preserve" approved="no"> + <source>Abort</source> + <target state="translated">중단</target> + </trans-unit> + <trans-unit id="%u messages failed to decrypt." xml:space="preserve" approved="no"> + <source>%u messages failed to decrypt.</source> + <target state="translated">%u개의 메세지를 번역하는데 실패함.</target> + </trans-unit> + <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve" approved="no"> + <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> + <target state="translated">연락처가 다른 사람과 공유할 수 있도록 프로필에 주소를 추가합니다. 프로필 업데이트가 연락처로 전송됩니다.</target> + </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve" approved="no"> + <source>%lld messages blocked by admin</source> + <target state="translated">관리자에 의해 차단된 %lld개의 메세지</target> + </trans-unit> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve" approved="no"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> + <target state="translated">**참고**: 두 장치에서 동일한 데이터베이스를 사용하면 보안 보호를 위해 연결에서 메세지를 해독할 수 있습니다.</target> + </trans-unit> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve" approved="no"> + <source>**Create group**: to create a new group.</source> + <target state="translated">**그룹 생성** : 새로운 그룹을 생성합니다.</target> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve" approved="no"> + <source>%d file(s) failed to download.</source> + <target state="translated">%d개의 파일을 다운로드하는데 실패함.</target> + </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve" approved="no"> + <source>%d messages not forwarded</source> + <target state="translated">%d개의 메세지가 전달되지 않음</target> + </trans-unit> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve" approved="no"> + <source>**Scan / Paste link**: to connect via a link you received.</source> + <target state="translated">**스캔/붙여넣기 링크**: 받은 링크를 통해 연결합니다.</target> + </trans-unit> + <trans-unit id="About operators" xml:space="preserve" approved="no"> + <source>About operators</source> + <target state="translated">연산자 정보</target> + </trans-unit> + <trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve" approved="no"> + <source>Address change will be aborted. Old receiving address will be used.</source> + <target state="translated">주소 변경이 중단됩니다. 이전 수신 주소가 사용됩니다.</target> + </trans-unit> + <trans-unit id="%@:" xml:space="preserve" approved="no"> + <source>%@:</source> + <target state="translated">%@:</target> + </trans-unit> + <trans-unit id="%lld messages blocked" xml:space="preserve" approved="no"> + <source>%lld messages blocked</source> + <target state="translated">%lld개의 메세지가 차단됨</target> + </trans-unit> + <trans-unit id="%lld messages marked deleted" xml:space="preserve" approved="no"> + <source>%lld messages marked deleted</source> + <target state="translated">삭제된 메세지 %lld 개</target> + </trans-unit> + <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve" approved="no"> + <source>- more stable message delivery. +- a bit better groups. +- and more!</source> + <target state="translated">- 보다 안정적인 메세지 전달. +- 조금 더 나은 그룹. +- 그리고 더!</target> + </trans-unit> + <trans-unit id="0s" xml:space="preserve" approved="no"> + <source>0s</source> + <target state="translated">0초</target> + </trans-unit> + <trans-unit id="1 minute" xml:space="preserve" approved="no"> + <source>1 minute</source> + <target state="translated">1분</target> + </trans-unit> + <trans-unit id="Abort changing address?" xml:space="preserve" approved="no"> + <source>Abort changing address?</source> + <target state="translated">주소 변경을 중단하시겠습니까?</target> + </trans-unit> + <trans-unit id="30 seconds" xml:space="preserve" approved="no"> + <source>30 seconds</source> + <target state="translated">30초</target> + </trans-unit> + <trans-unit id="- voice messages up to 5 minutes. - custom time to disappear. - editing history." xml:space="preserve" approved="no"> + <source>- voice messages up to 5 minutes. +- custom time to disappear. +- editing history.</source> + <target state="translated">- 음성 메세지 최대 5분. +- 사라지는 맞춤형 시간. +- 편집 기록.</target> + </trans-unit> + <trans-unit id="Add friends" xml:space="preserve" approved="no"> + <source>Add friends</source> + <target state="translated">친구 추가</target> + </trans-unit> + <trans-unit id="Add team members" xml:space="preserve" approved="no"> + <source>Add team members</source> + <target state="translated">팀원 추가하기</target> + </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve" approved="no"> + <source>Add your team members to the conversations.</source> + <target state="translated">대화에 팀원을 추가하세요.</target> + </trans-unit> + <trans-unit id="%u messages skipped." xml:space="preserve" approved="no"> + <source>%u messages skipped.</source> + <target state="translated">메세지 %u개를 건너뜀.</target> + </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve" approved="no"> + <source>%@, %@ and %lld other members connected</source> + <target state="translated">%@, %@ 그리고 %lld 다른 멤버들이 연결됨</target> + </trans-unit> + <trans-unit id="%lld messages moderated by %@" xml:space="preserve" approved="no"> + <source>%lld messages moderated by %@</source> + <target state="translated">%@ 에 의해 중재된 %lld 개의 메세지</target> + </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve" approved="no"> + <source>%lld new interface languages</source> + <target state="translated">%lld개의 새로운 인터페이스 언어</target> + </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve" approved="no"> + <source>%1$@, %2$@</source> + <target state="translated">%1$@, %2$@</target> + </trans-unit> + <trans-unit id="- optionally notify deleted contacts. - profile names with spaces. - and more!" xml:space="preserve" approved="no"> + <source>- optionally notify deleted contacts. +- profile names with spaces. +- and more!</source> + <target state="translated">- 선택적으로 삭제된 연락처를 통지합니다. +- 공백이 있는 프로필 이름. +- 그리고 더!</target> + </trans-unit> + <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve" approved="no"> + <source><p>Hi!</p> +<p><a href="%@">Connect to me via SimpleX Chat</a></p></source> + <target state="translated"><p>안녕하세요!/p> +<p><a href="%@">SimpleX 채팅을 통해 저에게 연결하세요 </a></p></target> + </trans-unit> + <trans-unit id="A new random profile will be shared." xml:space="preserve" approved="no"> + <source>A new random profile will be shared.</source> + <target state="translated">새로운 랜덤 프로필이 공유될 것입니다.</target> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve" approved="no"> + <source>Acknowledged</source> + <target state="translated">인정된</target> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve" approved="no"> + <source>Additional accent 2</source> + <target state="translated">추가 악센트2</target> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve" approved="no"> + <source>Added media & file servers</source> + <target state="translated">미디어 및 파일 서버 추가</target> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve" approved="no"> + <source>Added message servers</source> + <target state="translated">추가된 메세지 서버</target> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve" approved="no"> + <source>Additional accent</source> + <target state="translated">추가 악센트</target> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve" approved="no"> + <source>Additional secondary</source> + <target state="translated">추가적 보조</target> + </trans-unit> + <trans-unit id="Address" xml:space="preserve" approved="no"> + <source>Address</source> + <target state="translated">주소</target> + </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve" approved="no"> + <source>Address or 1-time link?</source> + <target state="translated">주소 또는 일회성 링크?</target> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve" approved="no"> + <source>Address settings</source> + <target state="translated">주소 세팅</target> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve" approved="no"> + <source>Admins can block a member for all.</source> + <target state="translated">관리자는 모두를 위해 회원을 차단할 수 있습니다.</target> + </trans-unit> + <trans-unit id="%lld group events" xml:space="preserve" approved="no"> + <source>%lld group events</source> + <target state="translated">%lld개의 그룹 이벤트</target> + </trans-unit> + <trans-unit id="All app data is deleted." xml:space="preserve" approved="no"> + <source>All app data is deleted.</source> + <target state="translated">모든 앱 데이터가 삭제됩니다.</target> + </trans-unit> + <trans-unit id="All data is erased when it is entered." xml:space="preserve" approved="no"> + <source>All data is erased when it is entered.</source> + <target state="translated">입력하면 모든 데이터가 삭제됩니다.</target> + </trans-unit> + <trans-unit id="0 sec" xml:space="preserve" approved="no"> + <source>0 sec</source> + <target state="translated">0 초</target> + </trans-unit> + <trans-unit id="(this device v%@)" xml:space="preserve" approved="no"> + <source>(this device v%@)</source> + <target state="translated">(이 장치 v%@)</target> + </trans-unit> + <trans-unit id="(new)" xml:space="preserve" approved="no"> + <source>(new)</source> + <target state="translated">(새로운)</target> + </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve" approved="no"> + <source>Advanced settings</source> + <target state="translated">고급 설정</target> + </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve" approved="no"> + <source>All data is kept private on your device.</source> + <target state="translated">모든 데이터는 기기에서 비공개로 유지됩니다.</target> + </trans-unit> + <trans-unit id="All profiles" xml:space="preserve" approved="no"> + <source>All profiles</source> + <target state="translated">전체 프로필</target> + </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve" approved="no"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target state="translated">모든 연락처, 대화 및 파일은 안전하게 암호화되어 구성된 XFTP 릴레이에 청크로 업로드됩니다.</target> + </trans-unit> + <trans-unit id="Allow calls only if your contact allows them." xml:space="preserve" approved="no"> + <source>Allow calls only if your contact allows them.</source> + <target state="translated">허용된 연락처만 통화가 가능합니다.</target> + </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve" approved="no"> + <source>Allow calls?</source> + <target state="translated">통화 허용?</target> + </trans-unit> + <trans-unit id="Allow downgrade" xml:space="preserve" approved="no"> + <source>Allow downgrade</source> + <target state="translated">강등 허용</target> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve" approved="no"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target state="translated">연락처가 허용하는 경우에만 수정 불가능한 메세지 삭제를 허용합니다. (24시간)</target> + </trans-unit> + <trans-unit id="Allow to send files and media." xml:space="preserve" approved="no"> + <source>Allow to send files and media.</source> + <target state="translated">파일과 미디어를 전송할 수 있습니다.</target> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve" approved="no"> + <source>Archiving database</source> + <target state="translated">보관된 데이터베이스</target> + </trans-unit> + <trans-unit id="Better calls" xml:space="preserve" approved="no"> + <source>Better calls</source> + <target state="translated">더 나은 통화</target> + </trans-unit> + <trans-unit id="Block" xml:space="preserve" approved="no"> + <source>Block</source> + <target state="translated">차단</target> + </trans-unit> + <trans-unit id="Conditions will be accepted for enabled operators after 30 days." xml:space="preserve" approved="no"> + <source>Conditions will be accepted for enabled operators after 30 days.</source> + <target state="translated">30일 후에 활성화된 운영자에 대한 조건이 수락될 것입니다.</target> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve" approved="no"> + <source>Conditions will be accepted on: %@.</source> + <target state="translated">조건은 다음과 같습니다: %@.</target> + </trans-unit> + <trans-unit id="Connect via one-time link" xml:space="preserve" approved="no"> + <source>Connect via one-time link</source> + <target state="translated">일회성 링크를 통해 연결</target> + </trans-unit> + <trans-unit id="Connected desktop" xml:space="preserve" approved="no"> + <source>Connected desktop</source> + <target state="translated">데스크톱과 연결됨</target> + </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve" approved="no"> + <source>Connected servers</source> + <target state="translated">연결된 서버</target> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve" approved="no"> + <source>Connection security</source> + <target state="translated">연결 보안</target> + </trans-unit> + <trans-unit id="Connection terminated" xml:space="preserve" approved="no"> + <source>Connection terminated</source> + <target state="translated">종료된 연결</target> + </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve" approved="no"> + <source>Connection with desktop stopped</source> + <target state="translated">데스크톱과의 연결이 중지됨</target> + </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve" approved="no"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <target state="translated">현재 조건 텍스트를 로드할 수 없습니다. 이 링크를 통해 조건을 검토할 수 있습니다:</target> + </trans-unit> + <trans-unit id="Bad desktop address" xml:space="preserve" approved="no"> + <source>Bad desktop address</source> + <target state="translated">잘못된 데스크톱 주소</target> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve" approved="no"> + <source>Camera not available</source> + <target state="translated">카메라가 사용 불가능합니다</target> + </trans-unit> + <trans-unit id="Custom time" xml:space="preserve" approved="no"> + <source>Custom time</source> + <target state="translated">사용자 지정 시간</target> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target state="translated">보낸 메시지를 되돌릴 수 없도록 삭제합니다. (24시간)</target> + </trans-unit> + <trans-unit id="Allow message reactions." xml:space="preserve" approved="no"> + <source>Allow message reactions.</source> + <target state="translated">메세지 응답 허용.</target> + </trans-unit> + <trans-unit id="Allow your contacts adding message reactions." xml:space="preserve" approved="no"> + <source>Allow your contacts adding message reactions.</source> + <target state="translated">연락처가 메세지 응답을 추가하도록 허용합니다.</target> + </trans-unit> + <trans-unit id="Already connecting!" xml:space="preserve" approved="no"> + <source>Already connecting!</source> + <target state="translated">이미 연결 중입니다!</target> + </trans-unit> + <trans-unit id="Already joining the group!" xml:space="preserve" approved="no"> + <source>Already joining the group!</source> + <target state="translated">그룹에 참가하는 중입니다!</target> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve" approved="no"> + <source>Archive and upload</source> + <target state="translated">기록 및 업로드</target> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve" approved="no"> + <source>Chat colors</source> + <target state="translated">채팅 색깔</target> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve" approved="no"> + <source>Chat list</source> + <target state="translated">채팅 목록</target> + </trans-unit> + <trans-unit id="Completed" xml:space="preserve" approved="no"> + <source>Completed</source> + <target state="translated">완료됨</target> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve" approved="no"> + <source>Copy error</source> + <target state="translated">복사 오류</target> + </trans-unit> + <trans-unit id="Create SimpleX address" xml:space="preserve" approved="no"> + <source>Create SimpleX address</source> + <target state="translated">SimpleX 주소 생성</target> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve" approved="no"> + <source>Creating link…</source> + <target state="translated">생성 링크…</target> + </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve" approved="no"> + <source>Blocked by admin</source> + <target state="translated">관리자에 의해 차단됨</target> + </trans-unit> + <trans-unit id="Connect to desktop" xml:space="preserve" approved="no"> + <source>Connect to desktop</source> + <target state="translated">데스크톱에 연결</target> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve" approved="no"> + <source>Created at</source> + <target state="translated">에 생성됨</target> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve" approved="no"> + <source>Created at: %@</source> + <target state="translated">생성 위치: %@</target> + </trans-unit> + <trans-unit id="Change self-destruct passcode" xml:space="preserve" approved="no"> + <source>Change self-destruct passcode</source> + <target state="translated">자기-파괴 비밀번호 변경</target> + </trans-unit> + <trans-unit id="Create file" xml:space="preserve" approved="no"> + <source>Create file</source> + <target state="translated">파일 생성</target> + </trans-unit> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target state="translated">연락처가 보낸 메세지를 되돌릴 수 없도록 삭제할 수 있도록 허용합니다. (24시간)</target> + </trans-unit> + <trans-unit id="App data migration" xml:space="preserve" approved="no"> + <source>App data migration</source> + <target state="translated">앱 데이터 이동</target> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve" approved="no"> + <source>Apply to</source> + <target state="translated">적용 대상</target> + </trans-unit> + <trans-unit id="Block for all" xml:space="preserve" approved="no"> + <source>Block for all</source> + <target state="translated">모두를 위한 차단</target> + </trans-unit> + <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve" approved="no"> + <source>Both you and your contact can add message reactions.</source> + <target state="translated">당신과 당신의 연락처 모두 메세지 반응을 추가할 수 있습니다.</target> + </trans-unit> + <trans-unit id="Calls prohibited!" xml:space="preserve" approved="no"> + <source>Calls prohibited!</source> + <target state="translated">통화 금지!</target> + </trans-unit> + <trans-unit id="Change self-destruct mode" xml:space="preserve" approved="no"> + <source>Change self-destruct mode</source> + <target state="translated">자기-파괴 모드 변경</target> + </trans-unit> + <trans-unit id="Contacts" xml:space="preserve" approved="no"> + <source>Contacts</source> + <target state="translated">연락처</target> + </trans-unit> + <trans-unit id="Create group" xml:space="preserve" approved="no"> + <source>Create group</source> + <target state="translated">그룹 생성</target> + </trans-unit> + <trans-unit id="Both you and your contact can make calls." xml:space="preserve" approved="no"> + <source>Both you and your contact can make calls.</source> + <target state="translated">당신과 당신의 연락처 모두 전화를 걸 수 있습니다.</target> + </trans-unit> + <trans-unit id="App passcode" xml:space="preserve" approved="no"> + <source>App passcode</source> + <target state="translated">앱 비밀번호</target> + </trans-unit> + <trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve" approved="no"> + <source>All your contacts will remain connected. Profile update will be sent to your contacts.</source> + <target state="translated">당신의 모든 연락은 연결되어 있습니다. 프로필 업데이트가 모든 연락으로 전송됩니다.</target> + </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve" approved="no"> + <source>App encrypts new local files (except videos).</source> + <target state="translated">앱은 새로운 로컬 파일을 암호화합니다 (동영상 제외).</target> + </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve" approved="no"> + <source>Chat preferences were changed.</source> + <target state="translated">채팅 환경설정이 변경되었습니다.</target> + </trans-unit> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve" approved="no"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <target state="translated">[데스크톱 앱]에 새로운 프로필 생성(https://simplex.chat/downloads/).💻</target> + </trans-unit> + <trans-unit id="Contact is deleted." xml:space="preserve" approved="no"> + <source>Contact is deleted.</source> + <target state="translated">연락처가 삭제되었습니다.</target> + </trans-unit> + <trans-unit id="Continue" xml:space="preserve" approved="no"> + <source>Continue</source> + <target state="translated">계속</target> + </trans-unit> + <trans-unit id="Current Passcode" xml:space="preserve" approved="no"> + <source>Current Passcode</source> + <target state="translated">현재 비밀번호</target> + </trans-unit> + <trans-unit id="An empty chat profile with the provided name is created, and the app opens as usual." xml:space="preserve" approved="no"> + <source>An empty chat profile with the provided name is created, and the app opens as usual.</source> + <target state="translated">제공된 이름으로 빈 채팅 프로필이 생성되고 앱이 정상적으로 열립니다.</target> + </trans-unit> + <trans-unit id="Allow your contacts to call you." xml:space="preserve" approved="no"> + <source>Allow your contacts to call you.</source> + <target state="translated">연락처가 전화할 수 있도록 허용합니다.</target> + </trans-unit> + <trans-unit id="Allow sharing" xml:space="preserve" approved="no"> + <source>Allow sharing</source> + <target state="translated">공유 허용</target> + </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve" approved="no"> + <source>Always use private routing.</source> + <target state="translated">항상 개인 경로를 사용합니다.</target> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve" approved="no"> + <source>Better user experience</source> + <target state="translated">더 나은 사용자 경험</target> + </trans-unit> + <trans-unit id="Change lock mode" xml:space="preserve" approved="no"> + <source>Change lock mode</source> + <target state="translated">잠금 모드 변경</target> + </trans-unit> + <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve" approved="no"> + <source>Allow message reactions only if your contact allows them.</source> + <target state="translated">연락처가 메세지 응답을 허용하는 경우에만 메세지 응답을 허용합니다.</target> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve" approved="no"> + <source>Better security ✅</source> + <target state="translated">더 나은 안전✅</target> + </trans-unit> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target state="translated">당신과 당신의 연락처 모두 보낸 메세지를 되돌릴 수 없습니다. (24시간)</target> + </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve" approved="no"> + <source>Confirm contact deletion?</source> + <target state="translated">연락처 삭제를 확인하시겠습니까?</target> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve" approved="no"> + <source>Can't call contact</source> + <target state="translated">연락처에 전화할 수 없습니다</target> + </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve" approved="no"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target state="translated">불가리아어, 핀란드어, 태국어, 우크라이나어 - 사용자 여러분과 [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)에 감사드립니다!</target> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve" approved="no"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <target state="translated">용량 초과 - 수신자가 이전에 보낸 메세지를 받지 못했습니다.</target> + </trans-unit> + <trans-unit id="Chat" xml:space="preserve" approved="no"> + <source>Chat</source> + <target state="translated">채팅</target> + </trans-unit> + <trans-unit id="Connect to yourself? This is your own one-time link!" xml:space="preserve" approved="no"> + <source>Connect to yourself? +This is your own one-time link!</source> + <target state="translated">자신에게 연결할까요? +이것은 당신만의 일회성 링크입니다!</target> + </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve" approved="no"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <target state="translated">새 기기에서 _다른 기기에서 이동_을 선택하고 QR 코드를 스캔합니다.</target> + </trans-unit> + <trans-unit id="Connecting to desktop" xml:space="preserve" approved="no"> + <source>Connecting to desktop</source> + <target state="translated">데스크톱에 연결중</target> + </trans-unit> + <trans-unit id="Connect with %@" xml:space="preserve" approved="no"> + <source>Connect with %@</source> + <target state="translated">%@와 연결</target> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve" approved="no"> + <source>Archived contacts</source> + <target state="translated">보관된 연락처</target> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve" approved="no"> + <source>Better message dates.</source> + <target state="translated">더 나은 메세지 날짜.</target> + </trans-unit> + <trans-unit id="Better networking" xml:space="preserve" approved="no"> + <source>Better networking</source> + <target state="translated">더 나은 네트워킹</target> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve" approved="no"> + <source>Check messages when allowed.</source> + <target state="translated">허용될 때 메시지를 확인합니다.</target> + </trans-unit> + <trans-unit id="Compare file" xml:space="preserve" approved="no"> + <source>Compare file</source> + <target state="translated">파일 비교</target> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve" approved="no"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <target state="translated">다음 조건은 활성화된 운영자에 대해 자동으로 수락됩니다: %@.</target> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve" approved="no"> + <source>Confirm upload</source> + <target state="translated">업로드 확인</target> + </trans-unit> + <trans-unit id="Connect incognito" xml:space="preserve" approved="no"> + <source>Connect incognito</source> + <target state="translated">비밀 연결</target> + </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve" approved="no"> + <source>Connect to your friends faster.</source> + <target state="translated">친구들과 더 빨리 연결하세요.</target> + </trans-unit> + <trans-unit id="Connect to yourself?" xml:space="preserve" approved="no"> + <source>Connect to yourself?</source> + <target state="translated">자신과 연결할까요?</target> + </trans-unit> + <trans-unit id="Created" xml:space="preserve" approved="no"> + <source>Created</source> + <target state="translated">생성됨</target> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve" approved="no"> + <source>Creating archive link</source> + <target state="translated">기록 링크 생성하기</target> + </trans-unit> + <trans-unit id="Auto-accept" xml:space="preserve" approved="no"> + <source>Auto-accept</source> + <target state="translated">자동 수락</target> + </trans-unit> + <trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve" approved="no"> + <source>All new messages from %@ will be hidden!</source> + <target state="translated">%@로부터의 모든 새 메세지가 숨겨집니다!</target> + </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve" approved="no"> + <source>Auto-accept settings</source> + <target state="translated">자동-수락 설정</target> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve" approved="no"> + <source>Archive contacts to chat later.</source> + <target state="translated">나중에 채팅할 연락처를 보관합니다.</target> + </trans-unit> + <trans-unit id="Background" xml:space="preserve" approved="no"> + <source>Background</source> + <target state="translated">배경</target> + </trans-unit> + <trans-unit id="Bad message hash" xml:space="preserve" approved="no"> + <source>Bad message hash</source> + <target state="translated">잘못된 메세지 hash</target> + </trans-unit> + <trans-unit id="Better groups" xml:space="preserve" approved="no"> + <source>Better groups</source> + <target state="translated">더 나은 그룹</target> + </trans-unit> + <trans-unit id="Better messages" xml:space="preserve" approved="no"> + <source>Better messages</source> + <target state="translated">더 나은 메세지</target> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve" approved="no"> + <source>Chunks downloaded</source> + <target state="translated">다운로드된 청크</target> + </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve" approved="no"> + <source>Chunks deleted</source> + <target state="translated">삭제된 청크</target> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve" approved="no"> + <source>Chunks uploaded</source> + <target state="translated">업로드 된 청크</target> + </trans-unit> + <trans-unit id="Corner" xml:space="preserve" approved="no"> + <source>Corner</source> + <target state="translated">코너</target> + </trans-unit> + <trans-unit id="Correct name to %@?" xml:space="preserve" approved="no"> + <source>Correct name to %@?</source> + <target state="translated">%@의 정확한 이름은?</target> + </trans-unit> + <trans-unit id="Create a group using a random profile." xml:space="preserve" approved="no"> + <source>Create a group using a random profile.</source> + <target state="translated">랜덤 프로필을 사용하여 그룹을 만듭니다.</target> + </trans-unit> + <trans-unit id="Authentication cancelled" xml:space="preserve" approved="no"> + <source>Authentication cancelled</source> + <target state="translated">인증 취소</target> + </trans-unit> + <trans-unit id="Confirm Passcode" xml:space="preserve" approved="no"> + <source>Confirm Passcode</source> + <target state="translated">비밀번호 확인</target> + </trans-unit> + <trans-unit id="Confirm database upgrades" xml:space="preserve" approved="no"> + <source>Confirm database upgrades</source> + <target state="translated">데이터베이스 업그레이드 확인</target> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve" approved="no"> + <source>Blur media</source> + <target state="translated">가려진 미디어</target> + </trans-unit> + <trans-unit id="Block group members" xml:space="preserve" approved="no"> + <source>Block group members</source> + <target state="translated">그룹 구성원 차단</target> + </trans-unit> + <trans-unit id="Connected" xml:space="preserve" approved="no"> + <source>Connected</source> + <target state="translated">연결됨</target> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve" approved="no"> + <source>All messages will be deleted - this cannot be undone!</source> + <target state="translated">모든 메세지가 삭제됩니다 - 수정할 수 없습니다!</target> + </trans-unit> + <trans-unit id="All your contacts will remain connected." xml:space="preserve" approved="no"> + <source>All your contacts will remain connected.</source> + <target state="translated">당신의 모든 연락은 계속 연결되어 있습니다.</target> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve" approved="no"> + <source>Allow to send SimpleX links.</source> + <target state="translated">SinpleX 링크 전송 허용.</target> + </trans-unit> + <trans-unit id="Bad message ID" xml:space="preserve" approved="no"> + <source>Bad message ID</source> + <target state="translated">잘못된 메세지 ID</target> + </trans-unit> + <trans-unit id="Black" xml:space="preserve" approved="no"> + <source>Black</source> + <target state="translated">블랙</target> + </trans-unit> + <trans-unit id="Block member" xml:space="preserve" approved="no"> + <source>Block member</source> + <target state="translated">차단 구성원</target> + </trans-unit> + <trans-unit id="Connected to desktop" xml:space="preserve" approved="no"> + <source>Connected to desktop</source> + <target state="translated">데스크톱과 연결됨</target> + </trans-unit> + <trans-unit id="App passcode is replaced with self-destruct passcode." xml:space="preserve" approved="no"> + <source>App passcode is replaced with self-destruct passcode.</source> + <target state="translated">앱 비밀번호는 자체-파괴 비밀번호로 대체됩니다.</target> + </trans-unit> + <trans-unit id="Apply" xml:space="preserve" approved="no"> + <source>Apply</source> + <target state="translated">적용</target> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve" approved="no"> + <source>Better notifications</source> + <target state="translated">더 나은 공지</target> + </trans-unit> + <trans-unit id="Block member?" xml:space="preserve" approved="no"> + <source>Block member?</source> + <target state="translated">차단 멤버?</target> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve" approved="no"> + <source>Blur for better privacy.</source> + <target state="translated">더 나은 개인정보를 위해 흐림.</target> + </trans-unit> + <trans-unit id="Business address" xml:space="preserve" approved="no"> + <source>Business address</source> + <target state="translated">사업체 주소</target> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve" approved="no"> + <source>Business chats</source> + <target state="translated">비즈니스 채팅</target> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve" approved="no"> + <source>Can't call member</source> + <target state="translated">회원에게 전화할 수 없습니다</target> + </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve" approved="no"> + <source>Can't message member</source> + <target state="translated">멤버에게 메세지를 보낼 수 없습니다</target> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve" approved="no"> + <source>Cancel migration</source> + <target state="translated">이동 취소</target> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve" approved="no"> + <source>Change chat profiles</source> + <target state="translated">채팅 프로필 변경</target> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve" approved="no"> + <source>Chat already exists</source> + <target state="translated">채팅이 이미 존재합니다</target> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve" approved="no"> + <source>Chat already exists!</source> + <target state="translated">채팅이 이미 존재합니다!</target> + </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve" approved="no"> + <source>Chat database exported</source> + <target state="translated">채팅 데이터베이스 내보내기</target> + </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve" approved="no"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <target state="translated">채팅이 중지되었습니다. 이미 다른 장치에서 이 데이터베이스를 사용하고 있다면 채팅을 시작하기 전에 다시 전송해야 합니다.</target> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve" approved="no"> + <source>Chat migrated!</source> + <target state="translated">채팅 이동!</target> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve" approved="no"> + <source>Chat theme</source> + <target state="translated">채팅 테마</target> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve" approved="no"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <target state="translated">채팅은 모든 회원에게 삭제됩니다 - 이는 되돌릴 수 없습니다!</target> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve" approved="no"> + <source>Chat profile</source> + <target state="translated">채팅 프로필</target> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve" approved="no"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <target state="translated">채팅은 삭제됩니다 - 되돌릴 수 없습니다!</target> + </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve" approved="no"> + <source>Check messages every 20 min.</source> + <target state="translated">20분마다 메시지를 확인합니다.</target> + </trans-unit> + <trans-unit id="Color chats with the new themes." xml:space="preserve" approved="no"> + <source>Color chats with the new themes.</source> + <target state="translated">새로운 테마로 채팅을 색칠하세요.</target> + </trans-unit> + <trans-unit id="Color mode" xml:space="preserve" approved="no"> + <source>Color mode</source> + <target state="translated">색깔 모드</target> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve" approved="no"> + <source>Clear private notes?</source> + <target state="translated">개인 메모를 지우시겠습니까?</target> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve" approved="no"> + <source>Conditions accepted on: %@.</source> + <target state="translated">조건이 수락됨: %@.</target> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve" approved="no"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <target state="translated">운영자의 조건이 허용됩니다: **%@**.</target> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve" approved="no"> + <source>Conditions of use</source> + <target state="translated">이용 조건</target> + </trans-unit> + <trans-unit id="Conditions will be accepted for operator(s): **%@**." xml:space="preserve" approved="no"> + <source>Conditions will be accepted for operator(s): **%@**.</source> + <target state="translated">운영자 조건이 수락됩니다: **%@**.</target> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve" approved="no"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <target state="translated">운영자 조건이 수락됩니다.: **%@**.</target> + </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve" approved="no"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <target state="translated">이동하는데에 필요한 데이터베이스 비밀번호를 기억하는지 확인합니다.</target> + </trans-unit> + <trans-unit id="Connect to yourself? This is your own SimpleX address!" xml:space="preserve" approved="no"> + <source>Connect to yourself? +This is your own SimpleX address!</source> + <target state="translated">자신과 연결할까요? +이것은 당신의 SimpleX 주소입니다!</target> + </trans-unit> + <trans-unit id="Connect via contact address" xml:space="preserve" approved="no"> + <source>Connect via contact address</source> + <target state="translated">연락처 주소로 연결</target> + </trans-unit> + <trans-unit id="Connecting" xml:space="preserve" approved="no"> + <source>Connecting</source> + <target state="translated">연결중</target> + </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve" approved="no"> + <source>Connecting to contact, please wait or check later!</source> + <target state="translated">연락처에 연결 중이니 기다려 주시거나 나중에 확인해 주세요!</target> + </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve" approved="no"> + <source>Connection and servers status.</source> + <target state="translated">연결 및 서버 상태.</target> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve" approved="no"> + <source>Connection notifications</source> + <target state="translated">연결 공지</target> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve" approved="no"> + <source>Connections</source> + <target state="translated">연결</target> + </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve" approved="no"> + <source>Contact will be deleted - this cannot be undone!</source> + <target state="translated">연락처가 삭제됩니다 - 취소할 수 없습니다!</target> + </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve" approved="no"> + <source>Conversation deleted!</source> + <target state="translated">대화가 삭제되었습니다!</target> + </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve" approved="no"> + <source>Create 1-time link</source> + <target state="translated">일회성 링크 생성</target> + </trans-unit> + <trans-unit id="Create profile" xml:space="preserve" approved="no"> + <source>Create profile</source> + <target state="translated">프로필 생성</target> + </trans-unit> + <trans-unit id="Audio/video calls" xml:space="preserve" approved="no"> + <source>Audio/video calls</source> + <target state="translated">음성/영상 통화</target> + </trans-unit> + <trans-unit id="Audio/video calls are prohibited." xml:space="preserve" approved="no"> + <source>Audio/video calls are prohibited.</source> + <target state="translated">음성/영상 통화는 금지되어 있습니다.</target> + </trans-unit> + <trans-unit id="App session" xml:space="preserve" approved="no"> + <source>App session</source> + <target state="translated">앱 세션</target> + </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve" approved="no"> + <source>Block member for all?</source> + <target state="translated">모두를 위한 차단 멤버?</target> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve" approved="no"> + <source>Cannot forward message</source> + <target state="translated">메세지를 전달할 수 없습니다</target> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve" approved="no"> + <source>Confirm network settings</source> + <target state="translated">네트워크 설정 확인</target> + </trans-unit> + <trans-unit id="Connect automatically" xml:space="preserve" approved="no"> + <source>Connect automatically</source> + <target state="translated">자동으로 연결</target> + </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve" approved="no"> + <source>Confirm files from unknown servers.</source> + <target state="translated">알 수 없는 서버에서 파일을 확인합니다.</target> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve" approved="no"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <target state="translated">이 운영자들에 대한 조건은 이미 받아들여지고 있습니다: **%@**.</target> + </trans-unit> + <trans-unit id="Contact deleted!" xml:space="preserve" approved="no"> + <source>Contact deleted!</source> + <target state="translated">연락처 삭제!</target> + </trans-unit> + <trans-unit id="Current profile" xml:space="preserve" approved="no"> + <source>Current profile</source> + <target state="translated">현재 프로필</target> + </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve" approved="no"> + <source>Customizable message shape.</source> + <target state="translated">사용자 지정 가능한 메세지 형태.</target> + </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve" approved="no"> + <source>%d seconds(s)</source> + <target state="translated">%d 초</target> + </trans-unit> + <trans-unit id="1 year" xml:space="preserve" approved="no"> + <source>1 year</source> + <target state="translated">1년</target> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve" approved="no"> + <source>Add list</source> + <target state="translated">리스트 추가</target> + </trans-unit> + <trans-unit id="Add to list" xml:space="preserve" approved="no"> + <source>Add to list</source> + <target state="translated">리스트에 추가</target> + </trans-unit> + <trans-unit id="All" xml:space="preserve" approved="no"> + <source>All</source> + <target state="translated">모두</target> + </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve" approved="no"> + <source>Allow to report messsages to moderators.</source> + <target state="translated">메시지를 신고하는것을 허용합니다.</target> + </trans-unit> + <trans-unit id="Another reason" xml:space="preserve" approved="no"> + <source>Another reason</source> + <target state="translated">다른 이유</target> + </trans-unit> + <trans-unit id="App group:" xml:space="preserve" approved="no"> + <source>App group:</source> + <target state="translated">앱 그룹:</target> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve" approved="no"> + <source>Archive</source> + <target state="translated">아카이브</target> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve" approved="no"> + <source>Archive report</source> + <target state="translated">신고 아카이브</target> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve" approved="no"> + <source>Archive report?</source> + <target state="translated">신고를 아카이브할까요?</target> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve" approved="no"> + <source>Archive reports</source> + <target state="translated">신고 아카이브</target> + </trans-unit> + <trans-unit id="Ask" xml:space="preserve" approved="no"> + <source>Ask</source> + <target state="translated">묻기</target> + </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve" approved="no"> + <source>Clear group?</source> + <target state="translated">그룹을 비울까요?</target> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve" approved="no"> + <source>Clear or delete group?</source> + <target state="translated">그룹을 비우거나 삭제할까요?</target> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve" approved="no"> + <source>Community guidelines violation</source> + <target state="translated">커뮤니티 지침 위반</target> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve" approved="no"> + <source>Connection blocked</source> + <target state="translated">연결 차단됨</target> + </trans-unit> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve" approved="no"> + <source>Connection is blocked by server operator: +%@</source> + <target state="translated">서버 관리자에 의해 연결이 차단되었습니다: +%@</target> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve" approved="no"> + <source>Connection not ready.</source> + <target state="translated">연결 준비되지 않음.</target> + </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve" approved="no"> + <source>Connection requires encryption renegotiation.</source> + <target state="translated">연결에는 암호화 재협상이 필요합니다.</target> + </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve" approved="no"> + <source>Content violates conditions of use</source> + <target state="translated">내용은 사용 규정을 위반합니다</target> + </trans-unit> + <trans-unit id="Create list" xml:space="preserve" approved="no"> + <source>Create list</source> + <target state="translated">리스트 추가</target> + </trans-unit> + <trans-unit id="Database ID: %d" xml:space="preserve" approved="no"> + <source>Database ID: %d</source> + <target state="translated">데이터베이스 아이디: %d</target> + </trans-unit> + <trans-unit id="Database IDs and Transport isolation option." xml:space="preserve" approved="no"> + <source>Database IDs and Transport isolation option.</source> + <target state="translated">데이터베이스 ID 및 전송 격리 옵션.</target> + </trans-unit> + <trans-unit id="Database downgrade" xml:space="preserve" approved="no"> + <source>Database downgrade</source> + <target state="translated">데이터베이스 다운그레이드</target> + </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve" approved="no"> + <source>Better groups performance</source> + <target state="translated">더 나은 그룹 성능</target> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve" approved="no"> + <source>Confirmed</source> + <target state="translated">확인함</target> + </trans-unit> + <trans-unit id="Active" xml:space="preserve" approved="no"> + <source>Active</source> + <target state="translated">활성화됨</target> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve" approved="no"> + <source>Archive all reports?</source> + <target state="translated">모든 신고를 아카이브할까요?</target> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve" approved="no"> + <source>Businesses</source> + <target state="translated">비즈니스</target> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve" approved="no"> + <source>Better privacy and security</source> + <target state="translated">더 나은 프라이버시 및 보안</target> + </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve" approved="no"> + <source>Change automatic message deletion?</source> + <target state="translated">자동 메시지 삭제를 변경할까요?</target> + </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve" approved="no"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <target state="translated">모든 채팅은 %@ 리스트에서 제거되고 리스트는 삭제됩니다.</target> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve" approved="no"> + <source>All reports will be archived for you.</source> + <target state="translated">모든 보고서는 사용자를 위해 보관됩니다.</target> + </trans-unit> + <trans-unit id="Accent" xml:space="preserve" approved="no"> + <source>Accent</source> + <target state="translated">강조</target> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve" approved="no"> + <source>Archive %lld reports?</source> + <target state="translated">%lld 신고를 아카이브할까요?</target> + </trans-unit> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve" approved="no"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> + <target state="translated">- [경로 서비스](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA) 에 연결중! +- 전달 확인 (최대 20 명의 멤버). +- 더 빠르고 안정적입니다.</target> + </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve" approved="no"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <target state="translated">모든 메시지와 파일은 **종간단 암호화 (E2EE)**되며, 개인 메시지는 양자 보안이 적용됩니다.</target> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve" approved="no"> + <source>Customize theme</source> + <target state="translated">테마 사용자 지정</target> + </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve" approved="no"> + <source>Dark mode colors</source> + <target state="translated">다크 모드 색상들</target> + </trans-unit> </body> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="ko" datatype="plaintext"> @@ -3778,8 +5144,9 @@ SimpleX servers cannot see your profile.</source> <source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source> <note>Privacy - Microphone Usage Description</note> </trans-unit> - <trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve"> + <trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve" approved="no"> <source>SimpleX needs access to Photo Library for saving captured and received media</source> + <target state="needs-translation">SimpleX는 캡처 및 수신 된 미디어를 저장하기 위해 사진 라이브러리에 접근이 필요합니다</target> <note>Privacy - Photo Library Additions Usage Description</note> </trans-unit> </body> @@ -3793,8 +5160,9 @@ SimpleX servers cannot see your profile.</source> <source>SimpleX NSE</source> <note>Bundle display name</note> </trans-unit> - <trans-unit id="CFBundleName" xml:space="preserve"> + <trans-unit id="CFBundleName" xml:space="preserve" approved="no"> <source>SimpleX NSE</source> + <target state="translated">SimpleX NSE</target> <note>Bundle name</note> </trans-unit> <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> diff --git a/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff b/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff index feb1e177f1..0f795170c6 100644 --- a/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff +++ b/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff @@ -162,20 +162,16 @@ <target state="translated">)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> <source>**Create link / QR code** for your contact to use.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> @@ -187,8 +183,8 @@ <target state="translated">**Turėkite omenyje**: jeigu prarasite slaptafrazę, NEBEGALĖSITE jos atkurti ar pakeisti.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> @@ -329,9 +325,9 @@ <target state="translated">Pridėti serverius skenuojant QR kodus.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve" approved="no"> - <source>Add server…</source> - <target state="translated">Pridėti serverį…</target> + <trans-unit id="Add server" xml:space="preserve" approved="no"> + <source>Add server</source> + <target state="translated">Pridėti serverį</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -1033,8 +1029,8 @@ <source>Direct messages</source> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> @@ -1049,8 +1045,8 @@ <source>Disappearing messages are prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disconnect" xml:space="preserve"> @@ -1417,16 +1413,16 @@ <source>Group members can irreversibly delete sent messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> @@ -1513,8 +1509,8 @@ <source>Image will be received when your contact is online, please wait or check later!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Import" xml:space="preserve"> @@ -1614,8 +1610,8 @@ <source>Irreversible message deletion is prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve"> @@ -1919,8 +1915,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Onion hosts will not be used.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> @@ -1971,8 +1967,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Open user profiles</source> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve"> @@ -2003,8 +1999,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Paste the link you received into the box below to connect with your contact.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Periodically" xml:space="preserve"> @@ -2591,8 +2587,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Thanks to the users – contribute via Weblate!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve"> @@ -2627,16 +2623,16 @@ We will be adding server redundancy to prevent lost messages.</source> <source>The message will be marked as moderated for all members.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> <source>The old database was not removed during the migration, it can be deleted.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The sender will NOT be notified" xml:space="preserve"> @@ -2687,8 +2683,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>To make a new connection</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -2873,8 +2869,8 @@ To connect, please ask your contact to create another connection link and check <source>Voice messages are prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> @@ -2993,10 +2989,6 @@ To connect, please ask your contact to create another connection link and check <source>You can't send messages!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <note>No comment provided by engineer.</note> diff --git a/apps/ios/SimpleX Localizations/ml.xcloc/Localized Contents/ml.xliff b/apps/ios/SimpleX Localizations/ml.xcloc/Localized Contents/ml.xliff deleted file mode 100644 index f4a1a815ea..0000000000 --- a/apps/ios/SimpleX Localizations/ml.xcloc/Localized Contents/ml.xliff +++ /dev/null @@ -1,4624 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> - <file original="en.lproj/Localizable.strings" source-language="en" target-language="ml" datatype="plaintext"> - <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/> - </header> - <body> - <trans-unit id=" " xml:space="preserve"> - <source> -</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (" xml:space="preserve"> - <source> (</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (can be copied)" xml:space="preserve"> - <source> (can be copied)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="!1 colored!" xml:space="preserve"> - <source>!1 colored!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="#secret#" xml:space="preserve"> - <source>#secret#</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@" xml:space="preserve"> - <source>%@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ %@" xml:space="preserve"> - <source>%@ %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ (current)" xml:space="preserve"> - <source>%@ (current)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ (current):" xml:space="preserve"> - <source>%@ (current):</source> - <note>copied message info</note> - </trans-unit> - <trans-unit id="%@ / %@" xml:space="preserve"> - <source>%@ / %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ is connected!" xml:space="preserve"> - <source>%@ is connected!</source> - <note>notification title</note> - </trans-unit> - <trans-unit id="%@ is not verified" xml:space="preserve"> - <source>%@ is not verified</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ is verified" xml:space="preserve"> - <source>%@ is verified</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ servers" xml:space="preserve"> - <source>%@ servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ wants to connect!" xml:space="preserve"> - <source>%@ wants to connect!</source> - <note>notification title</note> - </trans-unit> - <trans-unit id="%@:" xml:space="preserve"> - <source>%@:</source> - <note>copied message info</note> - </trans-unit> - <trans-unit id="%d days" xml:space="preserve"> - <source>%d days</source> - <note>time interval</note> - </trans-unit> - <trans-unit id="%d hours" xml:space="preserve"> - <source>%d hours</source> - <note>time interval</note> - </trans-unit> - <trans-unit id="%d min" xml:space="preserve"> - <source>%d min</source> - <note>time interval</note> - </trans-unit> - <trans-unit id="%d months" xml:space="preserve"> - <source>%d months</source> - <note>time interval</note> - </trans-unit> - <trans-unit id="%d sec" xml:space="preserve"> - <source>%d sec</source> - <note>time interval</note> - </trans-unit> - <trans-unit id="%d skipped message(s)" xml:space="preserve"> - <source>%d skipped message(s)</source> - <note>integrity error chat item</note> - </trans-unit> - <trans-unit id="%d weeks" xml:space="preserve"> - <source>%d weeks</source> - <note>time interval</note> - </trans-unit> - <trans-unit id="%lld" xml:space="preserve"> - <source>%lld</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld %@" xml:space="preserve"> - <source>%lld %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld contact(s) selected" xml:space="preserve"> - <source>%lld contact(s) selected</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve"> - <source>%lld file(s) with total size of %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld members" xml:space="preserve"> - <source>%lld members</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld minutes" xml:space="preserve"> - <source>%lld minutes</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> - <source>%lld second(s)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld seconds" xml:space="preserve"> - <source>%lld seconds</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lldd" xml:space="preserve"> - <source>%lldd</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lldh" xml:space="preserve"> - <source>%lldh</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lldk" xml:space="preserve"> - <source>%lldk</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lldm" xml:space="preserve"> - <source>%lldm</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lldmth" xml:space="preserve"> - <source>%lldmth</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%llds" xml:space="preserve"> - <source>%llds</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lldw" xml:space="preserve"> - <source>%lldw</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%u messages failed to decrypt." xml:space="preserve"> - <source>%u messages failed to decrypt.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%u messages skipped." xml:space="preserve"> - <source>%u messages skipped.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="(" xml:space="preserve"> - <source>(</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=")" xml:space="preserve"> - <source>)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> - <source>**Create link / QR code** for your contact to use.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> - <source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> - <source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> - <source>**e2e encrypted** audio call</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**e2e encrypted** video call" xml:space="preserve"> - <source>**e2e encrypted** video call</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="*bold*" xml:space="preserve"> - <source>\*bold*</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=", " xml:space="preserve"> - <source>, </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="- voice messages up to 5 minutes. - custom time to disappear. - editing history." xml:space="preserve"> - <source>- voice messages up to 5 minutes. -- custom time to disappear. -- editing history.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="." xml:space="preserve"> - <source>.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="0s" xml:space="preserve"> - <source>0s</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="1 day" xml:space="preserve"> - <source>1 day</source> - <note>time interval</note> - </trans-unit> - <trans-unit id="1 hour" xml:space="preserve"> - <source>1 hour</source> - <note>time interval</note> - </trans-unit> - <trans-unit id="1 minute" xml:space="preserve"> - <source>1 minute</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="1 month" xml:space="preserve"> - <source>1 month</source> - <note>time interval</note> - </trans-unit> - <trans-unit id="1 week" xml:space="preserve"> - <source>1 week</source> - <note>time interval</note> - </trans-unit> - <trans-unit id="1-time link" xml:space="preserve"> - <source>1-time link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="5 minutes" xml:space="preserve"> - <source>5 minutes</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="6" xml:space="preserve"> - <source>6</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="30 seconds" xml:space="preserve"> - <source>30 seconds</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=": " xml:space="preserve"> - <source>: </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> - <source><p>Hi!</p> -<p><a href="%@">Connect to me via SimpleX Chat</a></p></source> - <note>email text</note> - </trans-unit> - <trans-unit id="A new contact" xml:space="preserve"> - <source>A new contact</source> - <note>notification title</note> - </trans-unit> - <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>A random profile will be sent to the contact that you received this link from</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A random profile will be sent to your contact" xml:space="preserve"> - <source>A random profile will be sent to your contact</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> - <source>A separate TCP connection will be used **for each chat profile you have in the app**.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." xml:space="preserve"> - <source>A separate TCP connection will be used **for each contact and group member**. -**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve"> - <source>About SimpleX</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="About SimpleX Chat" xml:space="preserve"> - <source>About SimpleX Chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve"> - <source>About SimpleX address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Accent color" xml:space="preserve"> - <source>Accent color</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Accept" xml:space="preserve"> - <source>Accept</source> - <note>accept contact request via notification - accept incoming call via notification</note> - </trans-unit> - <trans-unit id="Accept contact" xml:space="preserve"> - <source>Accept contact</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Accept contact request from %@?" xml:space="preserve"> - <source>Accept contact request from %@?</source> - <note>notification body</note> - </trans-unit> - <trans-unit id="Accept incognito" xml:space="preserve"> - <source>Accept incognito</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> - <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> - <source>Add preset servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Add profile" xml:space="preserve"> - <source>Add profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> - <source>Add servers by scanning QR codes.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Add to another device" xml:space="preserve"> - <source>Add to another device</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Add welcome message" xml:space="preserve"> - <source>Add welcome message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Address" xml:space="preserve"> - <source>Address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> - <source>Admins can create the links to join groups.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Advanced network settings" xml:space="preserve"> - <source>Advanced network settings</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="All app data is deleted." xml:space="preserve"> - <source>All app data is deleted.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve"> - <source>All chats and messages will be deleted - this cannot be undone!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="All data is erased when it is entered." xml:space="preserve"> - <source>All data is erased when it is entered.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="All group members will remain connected." xml:space="preserve"> - <source>All group members will remain connected.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> - <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="All your contacts will remain connected." xml:space="preserve"> - <source>All your contacts will remain connected.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve"> - <source>All your contacts will remain connected. Profile update will be sent to your contacts.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow" xml:space="preserve"> - <source>Allow</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow calls only if your contact allows them." xml:space="preserve"> - <source>Allow calls only if your contact allows them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> - <source>Allow disappearing messages only if your contact allows it to you.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> - <source>Allow message reactions only if your contact allows them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow message reactions." xml:space="preserve"> - <source>Allow message reactions.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow sending direct messages to members." xml:space="preserve"> - <source>Allow sending direct messages to members.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow sending disappearing messages." xml:space="preserve"> - <source>Allow sending disappearing messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow to irreversibly delete sent messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow to send voice messages." xml:space="preserve"> - <source>Allow to send voice messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve"> - <source>Allow voice messages only if your contact allows them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow voice messages?" xml:space="preserve"> - <source>Allow voice messages?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow your contacts adding message reactions." xml:space="preserve"> - <source>Allow your contacts adding message reactions.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow your contacts to call you." xml:space="preserve"> - <source>Allow your contacts to call you.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow your contacts to irreversibly delete sent messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> - <source>Allow your contacts to send disappearing messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow your contacts to send voice messages." xml:space="preserve"> - <source>Allow your contacts to send voice messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Already connected?" xml:space="preserve"> - <source>Already connected?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Always use relay" xml:space="preserve"> - <source>Always use relay</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="An empty chat profile with the provided name is created, and the app opens as usual." xml:space="preserve"> - <source>An empty chat profile with the provided name is created, and the app opens as usual.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Answer call" xml:space="preserve"> - <source>Answer call</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="App build: %@" xml:space="preserve"> - <source>App build: %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="App icon" xml:space="preserve"> - <source>App icon</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="App passcode" xml:space="preserve"> - <source>App passcode</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="App passcode is replaced with self-destruct passcode." xml:space="preserve"> - <source>App passcode is replaced with self-destruct passcode.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="App version" xml:space="preserve"> - <source>App version</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="App version: v%@" xml:space="preserve"> - <source>App version: v%@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Appearance" xml:space="preserve"> - <source>Appearance</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Attach" xml:space="preserve"> - <source>Attach</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Audio & video calls" xml:space="preserve"> - <source>Audio & video calls</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Audio and video calls" xml:space="preserve"> - <source>Audio and video calls</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Audio/video calls" xml:space="preserve"> - <source>Audio/video calls</source> - <note>chat feature</note> - </trans-unit> - <trans-unit id="Audio/video calls are prohibited." xml:space="preserve"> - <source>Audio/video calls are prohibited.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Authentication cancelled" xml:space="preserve"> - <source>Authentication cancelled</source> - <note>PIN entry</note> - </trans-unit> - <trans-unit id="Authentication failed" xml:space="preserve"> - <source>Authentication failed</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Authentication is required before the call is connected, but you may miss calls." xml:space="preserve"> - <source>Authentication is required before the call is connected, but you may miss calls.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Authentication unavailable" xml:space="preserve"> - <source>Authentication unavailable</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Auto-accept" xml:space="preserve"> - <source>Auto-accept</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Auto-accept contact requests" xml:space="preserve"> - <source>Auto-accept contact requests</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Auto-accept images" xml:space="preserve"> - <source>Auto-accept images</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Back" xml:space="preserve"> - <source>Back</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Bad message ID" xml:space="preserve"> - <source>Bad message ID</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Bad message hash" xml:space="preserve"> - <source>Bad message hash</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Better messages" xml:space="preserve"> - <source>Better messages</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> - <source>Both you and your contact can add message reactions.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> - <source>Both you and your contact can irreversibly delete sent messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> - <source>Both you and your contact can make calls.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Both you and your contact can send disappearing messages." xml:space="preserve"> - <source>Both you and your contact can send disappearing messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Both you and your contact can send voice messages." xml:space="preserve"> - <source>Both you and your contact can send voice messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> - <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Call already ended!" xml:space="preserve"> - <source>Call already ended!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Calls" xml:space="preserve"> - <source>Calls</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve"> - <source>Can't delete user profile!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Can't invite contact!" xml:space="preserve"> - <source>Can't invite contact!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Can't invite contacts!" xml:space="preserve"> - <source>Can't invite contacts!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Cancel" xml:space="preserve"> - <source>Cancel</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> - <source>Cannot access keychain to save database password</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Cannot receive file" xml:space="preserve"> - <source>Cannot receive file</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change" xml:space="preserve"> - <source>Change</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change database passphrase?" xml:space="preserve"> - <source>Change database passphrase?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change lock mode" xml:space="preserve"> - <source>Change lock mode</source> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Change member role?" xml:space="preserve"> - <source>Change member role?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change passcode" xml:space="preserve"> - <source>Change passcode</source> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Change receiving address" xml:space="preserve"> - <source>Change receiving address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change receiving address?" xml:space="preserve"> - <source>Change receiving address?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change role" xml:space="preserve"> - <source>Change role</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change self-destruct mode" xml:space="preserve"> - <source>Change self-destruct mode</source> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Change self-destruct passcode" xml:space="preserve"> - <source>Change self-destruct passcode</source> - <note>authentication reason - set passcode view</note> - </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> - <source>Chat archive</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat console" xml:space="preserve"> - <source>Chat console</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat database" xml:space="preserve"> - <source>Chat database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat database deleted" xml:space="preserve"> - <source>Chat database deleted</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat database imported" xml:space="preserve"> - <source>Chat database imported</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat is running" xml:space="preserve"> - <source>Chat is running</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat is stopped" xml:space="preserve"> - <source>Chat is stopped</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat preferences" xml:space="preserve"> - <source>Chat preferences</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chats" xml:space="preserve"> - <source>Chats</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Check server address and try again." xml:space="preserve"> - <source>Check server address and try again.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> - <source>Chinese and Spanish interface</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Choose file" xml:space="preserve"> - <source>Choose file</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Choose from library" xml:space="preserve"> - <source>Choose from library</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Clear" xml:space="preserve"> - <source>Clear</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Clear conversation" xml:space="preserve"> - <source>Clear conversation</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Clear conversation?" xml:space="preserve"> - <source>Clear conversation?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Clear verification" xml:space="preserve"> - <source>Clear verification</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> - <source>Colors</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Compare file" xml:space="preserve"> - <source>Compare file</source> - <note>server test step</note> - </trans-unit> - <trans-unit id="Compare security codes with your contacts." xml:space="preserve"> - <source>Compare security codes with your contacts.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Configure ICE servers" xml:space="preserve"> - <source>Configure ICE servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Confirm" xml:space="preserve"> - <source>Confirm</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Confirm Passcode" xml:space="preserve"> - <source>Confirm Passcode</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Confirm database upgrades" xml:space="preserve"> - <source>Confirm database upgrades</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Confirm new passphrase…" xml:space="preserve"> - <source>Confirm new passphrase…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Confirm password" xml:space="preserve"> - <source>Confirm password</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connect" xml:space="preserve"> - <source>Connect</source> - <note>server test step</note> - </trans-unit> - <trans-unit id="Connect via contact link?" xml:space="preserve"> - <source>Connect via contact link?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connect via group link?" xml:space="preserve"> - <source>Connect via group link?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connect via link" xml:space="preserve"> - <source>Connect via link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve"> - <source>Connect via link / QR code</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connect via one-time link?" xml:space="preserve"> - <source>Connect via one-time link?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connecting server…" xml:space="preserve"> - <source>Connecting to server…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connecting server… (error: %@)" xml:space="preserve"> - <source>Connecting to server… (error: %@)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection" xml:space="preserve"> - <source>Connection</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection error" xml:space="preserve"> - <source>Connection error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection error (AUTH)" xml:space="preserve"> - <source>Connection error (AUTH)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection request" xml:space="preserve"> - <source>Connection request</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection request sent!" xml:space="preserve"> - <source>Connection request sent!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection timeout" xml:space="preserve"> - <source>Connection timeout</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact allows" xml:space="preserve"> - <source>Contact allows</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact already exists" xml:space="preserve"> - <source>Contact already exists</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact and all messages will be deleted - this cannot be undone!" xml:space="preserve"> - <source>Contact and all messages will be deleted - this cannot be undone!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact hidden:" xml:space="preserve"> - <source>Contact hidden:</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Contact is connected" xml:space="preserve"> - <source>Contact is connected</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve"> - <source>Contact is not connected yet!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact name" xml:space="preserve"> - <source>Contact name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact preferences" xml:space="preserve"> - <source>Contact preferences</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve"> - <source>Contacts can mark messages for deletion; you will be able to view them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Continue" xml:space="preserve"> - <source>Continue</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Copy" xml:space="preserve"> - <source>Copy</source> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Core version: v%@" xml:space="preserve"> - <source>Core version: v%@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create" xml:space="preserve"> - <source>Create</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create SimpleX address" xml:space="preserve"> - <source>Create SimpleX address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> - <source>Create an address to let people connect with you.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create file" xml:space="preserve"> - <source>Create file</source> - <note>server test step</note> - </trans-unit> - <trans-unit id="Create group link" xml:space="preserve"> - <source>Create group link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create link" xml:space="preserve"> - <source>Create link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve"> - <source>Create one-time invitation link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create queue" xml:space="preserve"> - <source>Create queue</source> - <note>server test step</note> - </trans-unit> - <trans-unit id="Create secret group" xml:space="preserve"> - <source>Create secret group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create your profile" xml:space="preserve"> - <source>Create your profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve"> - <source>Created on %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Current Passcode" xml:space="preserve"> - <source>Current Passcode</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Current passphrase…" xml:space="preserve"> - <source>Current passphrase…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> - <source>Currently maximum supported file size is %@.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Custom time" xml:space="preserve"> - <source>Custom time</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Dark" xml:space="preserve"> - <source>Dark</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database ID" xml:space="preserve"> - <source>Database ID</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database ID: %d" xml:space="preserve"> - <source>Database ID: %d</source> - <note>copied message info</note> - </trans-unit> - <trans-unit id="Database IDs and Transport isolation option." xml:space="preserve"> - <source>Database IDs and Transport isolation option.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database downgrade" xml:space="preserve"> - <source>Database downgrade</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database encrypted!" xml:space="preserve"> - <source>Database encrypted!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database encryption passphrase will be updated and stored in the keychain. " xml:space="preserve"> - <source>Database encryption passphrase will be updated and stored in the keychain. -</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database encryption passphrase will be updated. " xml:space="preserve"> - <source>Database encryption passphrase will be updated. -</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database error" xml:space="preserve"> - <source>Database error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database is encrypted using a random passphrase, you can change it." xml:space="preserve"> - <source>Database is encrypted using a random passphrase, you can change it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database is encrypted using a random passphrase. Please change it before exporting." xml:space="preserve"> - <source>Database is encrypted using a random passphrase. Please change it before exporting.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database passphrase" xml:space="preserve"> - <source>Database passphrase</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database passphrase & export" xml:space="preserve"> - <source>Database passphrase & export</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> - <source>Database passphrase is different from saved in the keychain.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> - <source>Database passphrase is required to open chat.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database upgrade" xml:space="preserve"> - <source>Database upgrade</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database will be encrypted and the passphrase stored in the keychain. " xml:space="preserve"> - <source>Database will be encrypted and the passphrase stored in the keychain. -</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database will be encrypted. " xml:space="preserve"> - <source>Database will be encrypted. -</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database will be migrated when the app restarts" xml:space="preserve"> - <source>Database will be migrated when the app restarts</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Decentralized" xml:space="preserve"> - <source>Decentralized</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Decryption error" xml:space="preserve"> - <source>Decryption error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete" xml:space="preserve"> - <source>Delete</source> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve"> - <source>Delete Contact</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete address" xml:space="preserve"> - <source>Delete address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete address?" xml:space="preserve"> - <source>Delete address?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete after" xml:space="preserve"> - <source>Delete after</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete all files" xml:space="preserve"> - <source>Delete all files</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve"> - <source>Delete archive</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve"> - <source>Delete chat archive?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete chat profile" xml:space="preserve"> - <source>Delete chat profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete chat profile?" xml:space="preserve"> - <source>Delete chat profile?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete connection" xml:space="preserve"> - <source>Delete connection</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete contact" xml:space="preserve"> - <source>Delete contact</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete contact?" xml:space="preserve"> - <source>Delete contact?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete database" xml:space="preserve"> - <source>Delete database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete file" xml:space="preserve"> - <source>Delete file</source> - <note>server test step</note> - </trans-unit> - <trans-unit id="Delete files and media?" xml:space="preserve"> - <source>Delete files and media?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete files for all chat profiles" xml:space="preserve"> - <source>Delete files for all chat profiles</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete for everyone" xml:space="preserve"> - <source>Delete for everyone</source> - <note>chat feature</note> - </trans-unit> - <trans-unit id="Delete for me" xml:space="preserve"> - <source>Delete for me</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete group" xml:space="preserve"> - <source>Delete group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete group?" xml:space="preserve"> - <source>Delete group?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete invitation" xml:space="preserve"> - <source>Delete invitation</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete link" xml:space="preserve"> - <source>Delete link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete link?" xml:space="preserve"> - <source>Delete link?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete member message?" xml:space="preserve"> - <source>Delete member message?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete message?" xml:space="preserve"> - <source>Delete message?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete messages" xml:space="preserve"> - <source>Delete messages</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete messages after" xml:space="preserve"> - <source>Delete messages after</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete old database" xml:space="preserve"> - <source>Delete old database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete old database?" xml:space="preserve"> - <source>Delete old database?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve"> - <source>Delete pending connection</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete pending connection?" xml:space="preserve"> - <source>Delete pending connection?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete profile" xml:space="preserve"> - <source>Delete profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete queue" xml:space="preserve"> - <source>Delete queue</source> - <note>server test step</note> - </trans-unit> - <trans-unit id="Delete user profile?" xml:space="preserve"> - <source>Delete user profile?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Deleted at" xml:space="preserve"> - <source>Deleted at</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Deleted at: %@" xml:space="preserve"> - <source>Deleted at: %@</source> - <note>copied message info</note> - </trans-unit> - <trans-unit id="Description" xml:space="preserve"> - <source>Description</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Develop" xml:space="preserve"> - <source>Develop</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Developer tools" xml:space="preserve"> - <source>Developer tools</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Device" xml:space="preserve"> - <source>Device</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Device authentication is disabled. Turning off SimpleX Lock." xml:space="preserve"> - <source>Device authentication is disabled. Turning off SimpleX Lock.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." xml:space="preserve"> - <source>Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Different names, avatars and transport isolation." xml:space="preserve"> - <source>Different names, avatars and transport isolation.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Direct messages" xml:space="preserve"> - <source>Direct messages</source> - <note>chat feature</note> - </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> - <source>Disable SimpleX Lock</source> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Disappearing message" xml:space="preserve"> - <source>Disappearing message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Disappearing messages" xml:space="preserve"> - <source>Disappearing messages</source> - <note>chat feature</note> - </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this chat." xml:space="preserve"> - <source>Disappearing messages are prohibited in this chat.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Disappears at" xml:space="preserve"> - <source>Disappears at</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Disappears at: %@" xml:space="preserve"> - <source>Disappears at: %@</source> - <note>copied message info</note> - </trans-unit> - <trans-unit id="Disconnect" xml:space="preserve"> - <source>Disconnect</source> - <note>server test step</note> - </trans-unit> - <trans-unit id="Display name" xml:space="preserve"> - <source>Display name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Display name:" xml:space="preserve"> - <source>Display name:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> - <source>Do NOT use SimpleX for emergency calls.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Do it later" xml:space="preserve"> - <source>Do it later</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Don't create address" xml:space="preserve"> - <source>Don't create address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Don't show again" xml:space="preserve"> - <source>Don't show again</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Downgrade and open chat" xml:space="preserve"> - <source>Downgrade and open chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Download file" xml:space="preserve"> - <source>Download file</source> - <note>server test step</note> - </trans-unit> - <trans-unit id="Duplicate display name!" xml:space="preserve"> - <source>Duplicate display name!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Duration" xml:space="preserve"> - <source>Duration</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Edit" xml:space="preserve"> - <source>Edit</source> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Edit group profile" xml:space="preserve"> - <source>Edit group profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable" xml:space="preserve"> - <source>Enable</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> - <source>Enable SimpleX Lock</source> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Enable TCP keep-alive" xml:space="preserve"> - <source>Enable TCP keep-alive</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> - <source>Enable automatic message deletion?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable instant notifications?" xml:space="preserve"> - <source>Enable instant notifications?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable lock" xml:space="preserve"> - <source>Enable lock</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable notifications" xml:space="preserve"> - <source>Enable notifications</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable periodic notifications?" xml:space="preserve"> - <source>Enable periodic notifications?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable self-destruct" xml:space="preserve"> - <source>Enable self-destruct</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable self-destruct passcode" xml:space="preserve"> - <source>Enable self-destruct passcode</source> - <note>set passcode view</note> - </trans-unit> - <trans-unit id="Encrypt" xml:space="preserve"> - <source>Encrypt</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Encrypt database?" xml:space="preserve"> - <source>Encrypt database?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Encrypted database" xml:space="preserve"> - <source>Encrypted database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Encrypted message or another event" xml:space="preserve"> - <source>Encrypted message or another event</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Encrypted message: database error" xml:space="preserve"> - <source>Encrypted message: database error</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Encrypted message: database migration error" xml:space="preserve"> - <source>Encrypted message: database migration error</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Encrypted message: keychain error" xml:space="preserve"> - <source>Encrypted message: keychain error</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Encrypted message: no passphrase" xml:space="preserve"> - <source>Encrypted message: no passphrase</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Encrypted message: unexpected error" xml:space="preserve"> - <source>Encrypted message: unexpected error</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Enter Passcode" xml:space="preserve"> - <source>Enter Passcode</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enter correct passphrase." xml:space="preserve"> - <source>Enter correct passphrase.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enter passphrase…" xml:space="preserve"> - <source>Enter passphrase…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enter password above to show!" xml:space="preserve"> - <source>Enter password above to show!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enter server manually" xml:space="preserve"> - <source>Enter server manually</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enter welcome message…" xml:space="preserve"> - <source>Enter welcome message…</source> - <note>placeholder</note> - </trans-unit> - <trans-unit id="Enter welcome message… (optional)" xml:space="preserve"> - <source>Enter welcome message… (optional)</source> - <note>placeholder</note> - </trans-unit> - <trans-unit id="Error" xml:space="preserve"> - <source>Error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error accepting contact request" xml:space="preserve"> - <source>Error accepting contact request</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve"> - <source>Error accessing database file</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error adding member(s)" xml:space="preserve"> - <source>Error adding member(s)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error changing address" xml:space="preserve"> - <source>Error changing address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error changing role" xml:space="preserve"> - <source>Error changing role</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error changing setting" xml:space="preserve"> - <source>Error changing setting</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error creating address" xml:space="preserve"> - <source>Error creating address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error creating group" xml:space="preserve"> - <source>Error creating group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error creating group link" xml:space="preserve"> - <source>Error creating group link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error creating profile!" xml:space="preserve"> - <source>Error creating profile!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting chat database" xml:space="preserve"> - <source>Error deleting chat database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting chat!" xml:space="preserve"> - <source>Error deleting chat!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting connection" xml:space="preserve"> - <source>Error deleting connection</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve"> - <source>Error deleting contact</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting database" xml:space="preserve"> - <source>Error deleting database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting old database" xml:space="preserve"> - <source>Error deleting old database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting token" xml:space="preserve"> - <source>Error deleting token</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting user profile" xml:space="preserve"> - <source>Error deleting user profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error enabling notifications" xml:space="preserve"> - <source>Error enabling notifications</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error encrypting database" xml:space="preserve"> - <source>Error encrypting database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error exporting chat database" xml:space="preserve"> - <source>Error exporting chat database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error importing chat database" xml:space="preserve"> - <source>Error importing chat database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error joining group" xml:space="preserve"> - <source>Error joining group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve"> - <source>Error loading %@ servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error receiving file" xml:space="preserve"> - <source>Error receiving file</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error removing member" xml:space="preserve"> - <source>Error removing member</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve"> - <source>Error saving %@ servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error saving ICE servers" xml:space="preserve"> - <source>Error saving ICE servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error saving group profile" xml:space="preserve"> - <source>Error saving group profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error saving passcode" xml:space="preserve"> - <source>Error saving passcode</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error saving passphrase to keychain" xml:space="preserve"> - <source>Error saving passphrase to keychain</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error saving user password" xml:space="preserve"> - <source>Error saving user password</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error sending email" xml:space="preserve"> - <source>Error sending email</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error sending message" xml:space="preserve"> - <source>Error sending message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error starting chat" xml:space="preserve"> - <source>Error starting chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error stopping chat" xml:space="preserve"> - <source>Error stopping chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error switching profile!" xml:space="preserve"> - <source>Error switching profile!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error updating group link" xml:space="preserve"> - <source>Error updating group link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error updating message" xml:space="preserve"> - <source>Error updating message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error updating settings" xml:space="preserve"> - <source>Error updating settings</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error updating user privacy" xml:space="preserve"> - <source>Error updating user privacy</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error: " xml:space="preserve"> - <source>Error: </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error: %@" xml:space="preserve"> - <source>Error: %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error: URL is invalid" xml:space="preserve"> - <source>Error: URL is invalid</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error: no database file" xml:space="preserve"> - <source>Error: no database file</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Exit without saving" xml:space="preserve"> - <source>Exit without saving</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Export database" xml:space="preserve"> - <source>Export database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Export error:" xml:space="preserve"> - <source>Export error:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Exported database archive." xml:space="preserve"> - <source>Exported database archive.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Exporting database archive..." xml:space="preserve"> - <source>Exporting database archive...</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Failed to remove passphrase" xml:space="preserve"> - <source>Failed to remove passphrase</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Fast and no wait until the sender is online!" xml:space="preserve"> - <source>Fast and no wait until the sender is online!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="File will be deleted from servers." xml:space="preserve"> - <source>File will be deleted from servers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="File will be received when your contact completes uploading it." xml:space="preserve"> - <source>File will be received when your contact completes uploading it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="File will be received when your contact is online, please wait or check later!" xml:space="preserve"> - <source>File will be received when your contact is online, please wait or check later!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="File: %@" xml:space="preserve"> - <source>File: %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Files & media" xml:space="preserve"> - <source>Files & media</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> - <source>Finally, we have them! 🚀</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="For console" xml:space="preserve"> - <source>For console</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="French interface" xml:space="preserve"> - <source>French interface</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Full link" xml:space="preserve"> - <source>Full link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Full name (optional)" xml:space="preserve"> - <source>Full name (optional)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Full name:" xml:space="preserve"> - <source>Full name:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Fully re-implemented - work in background!" xml:space="preserve"> - <source>Fully re-implemented - work in background!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Further reduced battery usage" xml:space="preserve"> - <source>Further reduced battery usage</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="GIFs and stickers" xml:space="preserve"> - <source>GIFs and stickers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group" xml:space="preserve"> - <source>Group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group display name" xml:space="preserve"> - <source>Group display name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group full name (optional)" xml:space="preserve"> - <source>Group full name (optional)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group image" xml:space="preserve"> - <source>Group image</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group invitation" xml:space="preserve"> - <source>Group invitation</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group invitation expired" xml:space="preserve"> - <source>Group invitation expired</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group invitation is no longer valid, it was removed by sender." xml:space="preserve"> - <source>Group invitation is no longer valid, it was removed by sender.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group link" xml:space="preserve"> - <source>Group link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group links" xml:space="preserve"> - <source>Group links</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve"> - <source>Group members can add message reactions.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> - <source>Group members can irreversibly delete sent messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group message:" xml:space="preserve"> - <source>Group message:</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Group moderation" xml:space="preserve"> - <source>Group moderation</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group preferences" xml:space="preserve"> - <source>Group preferences</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group profile" xml:space="preserve"> - <source>Group profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group profile is stored on members' devices, not on the servers." xml:space="preserve"> - <source>Group profile is stored on members' devices, not on the servers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group welcome message" xml:space="preserve"> - <source>Group welcome message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group will be deleted for all members - this cannot be undone!" xml:space="preserve"> - <source>Group will be deleted for all members - this cannot be undone!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group will be deleted for you - this cannot be undone!" xml:space="preserve"> - <source>Group will be deleted for you - this cannot be undone!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Help" xml:space="preserve"> - <source>Help</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Hidden" xml:space="preserve"> - <source>Hidden</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Hidden chat profiles" xml:space="preserve"> - <source>Hidden chat profiles</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Hidden profile password" xml:space="preserve"> - <source>Hidden profile password</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Hide" xml:space="preserve"> - <source>Hide</source> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Hide app screen in the recent apps." xml:space="preserve"> - <source>Hide app screen in the recent apps.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Hide profile" xml:space="preserve"> - <source>Hide profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Hide:" xml:space="preserve"> - <source>Hide:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="History" xml:space="preserve"> - <source>History</source> - <note>copied message info</note> - </trans-unit> - <trans-unit id="How SimpleX works" xml:space="preserve"> - <source>How SimpleX works</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="How it works" xml:space="preserve"> - <source>How it works</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="How to" xml:space="preserve"> - <source>How to</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="How to use it" xml:space="preserve"> - <source>How to use it</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="How to use your servers" xml:space="preserve"> - <source>How to use your servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="ICE servers (one per line)" xml:space="preserve"> - <source>ICE servers (one per line)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> - <source>If you can't meet in person, show QR code in a video call, or share the link.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> - <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="If you enter your self-destruct passcode while opening the app:" xml:space="preserve"> - <source>If you enter your self-destruct passcode while opening the app:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." xml:space="preserve"> - <source>If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Ignore" xml:space="preserve"> - <source>Ignore</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Image will be received when your contact completes uploading it." xml:space="preserve"> - <source>Image will be received when your contact completes uploading it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve"> - <source>Image will be received when your contact is online, please wait or check later!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Immediately" xml:space="preserve"> - <source>Immediately</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Import" xml:space="preserve"> - <source>Import</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Import chat database?" xml:space="preserve"> - <source>Import chat database?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Import database" xml:space="preserve"> - <source>Import database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Improved privacy and security" xml:space="preserve"> - <source>Improved privacy and security</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Improved server configuration" xml:space="preserve"> - <source>Improved server configuration</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito" xml:space="preserve"> - <source>Incognito</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode" xml:space="preserve"> - <source>Incognito mode</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve"> - <source>Incognito mode is not supported here - your main profile will be sent to group members</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve"> - <source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incoming audio call" xml:space="preserve"> - <source>Incoming audio call</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Incoming call" xml:space="preserve"> - <source>Incoming call</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Incoming video call" xml:space="preserve"> - <source>Incoming video call</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Incompatible database version" xml:space="preserve"> - <source>Incompatible database version</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incorrect passcode" xml:space="preserve"> - <source>Incorrect passcode</source> - <note>PIN entry</note> - </trans-unit> - <trans-unit id="Incorrect security code!" xml:space="preserve"> - <source>Incorrect security code!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Info" xml:space="preserve"> - <source>Info</source> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Initial role" xml:space="preserve"> - <source>Initial role</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> - <source>Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> - <source>Instant push notifications will be hidden! -</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Instantly" xml:space="preserve"> - <source>Instantly</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Interface" xml:space="preserve"> - <source>Interface</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Invalid connection link" xml:space="preserve"> - <source>Invalid connection link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Invalid server address!" xml:space="preserve"> - <source>Invalid server address!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Invitation expired!" xml:space="preserve"> - <source>Invitation expired!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Invite friends" xml:space="preserve"> - <source>Invite friends</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Invite members" xml:space="preserve"> - <source>Invite members</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Invite to group" xml:space="preserve"> - <source>Invite to group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Irreversible message deletion" xml:space="preserve"> - <source>Irreversible message deletion</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this chat." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this chat.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve"> - <source>It allows having many anonymous connections without any shared data between them in a single chat profile.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="It can happen when you or your connection used the old database backup." xml:space="preserve"> - <source>It can happen when you or your connection used the old database backup.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="It can happen when: 1. The messages expired in the sending client after 2 days or on the server after 30 days. 2. Message decryption failed, because you or your contact used old database backup. 3. The connection was compromised." xml:space="preserve"> - <source>It can happen when: -1. The messages expired in the sending client after 2 days or on the server after 30 days. -2. Message decryption failed, because you or your contact used old database backup. -3. The connection was compromised.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> - <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Italian interface" xml:space="preserve"> - <source>Italian interface</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Japanese interface" xml:space="preserve"> - <source>Japanese interface</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Join" xml:space="preserve"> - <source>Join</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Join group" xml:space="preserve"> - <source>Join group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Join incognito" xml:space="preserve"> - <source>Join incognito</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Joining group" xml:space="preserve"> - <source>Joining group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="KeyChain error" xml:space="preserve"> - <source>KeyChain error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Keychain error" xml:space="preserve"> - <source>Keychain error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="LIVE" xml:space="preserve"> - <source>LIVE</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Large file!" xml:space="preserve"> - <source>Large file!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Learn more" xml:space="preserve"> - <source>Learn more</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Leave" xml:space="preserve"> - <source>Leave</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Leave group" xml:space="preserve"> - <source>Leave group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Leave group?" xml:space="preserve"> - <source>Leave group?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Let's talk in SimpleX Chat" xml:space="preserve"> - <source>Let's talk in SimpleX Chat</source> - <note>email subject</note> - </trans-unit> - <trans-unit id="Light" xml:space="preserve"> - <source>Light</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Limitations" xml:space="preserve"> - <source>Limitations</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Live message!" xml:space="preserve"> - <source>Live message!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Live messages" xml:space="preserve"> - <source>Live messages</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Local name" xml:space="preserve"> - <source>Local name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Local profile data only" xml:space="preserve"> - <source>Local profile data only</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Lock after" xml:space="preserve"> - <source>Lock after</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Lock mode" xml:space="preserve"> - <source>Lock mode</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> - <source>Make a private connection</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Make profile private!" xml:space="preserve"> - <source>Make profile private!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> - <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Mark deleted for everyone" xml:space="preserve"> - <source>Mark deleted for everyone</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Mark read" xml:space="preserve"> - <source>Mark read</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Mark verified" xml:space="preserve"> - <source>Mark verified</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Markdown in messages" xml:space="preserve"> - <source>Markdown in messages</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Max 30 seconds, received instantly." xml:space="preserve"> - <source>Max 30 seconds, received instantly.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Member" xml:space="preserve"> - <source>Member</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> - <source>Member role will be changed to "%@". All group members will be notified.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Member role will be changed to "%@". The member will receive a new invitation." xml:space="preserve"> - <source>Member role will be changed to "%@". The member will receive a new invitation.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> - <source>Member will be removed from group - this cannot be undone!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Message delivery error" xml:space="preserve"> - <source>Message delivery error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Message draft" xml:space="preserve"> - <source>Message draft</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Message reactions" xml:space="preserve"> - <source>Message reactions</source> - <note>chat feature</note> - </trans-unit> - <trans-unit id="Message reactions are prohibited in this chat." xml:space="preserve"> - <source>Message reactions are prohibited in this chat.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> - <source>Message reactions are prohibited in this group.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Message text" xml:space="preserve"> - <source>Message text</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Messages" xml:space="preserve"> - <source>Messages</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Messages & files" xml:space="preserve"> - <source>Messages & files</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Migrating database archive..." xml:space="preserve"> - <source>Migrating database archive...</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Migration error:" xml:space="preserve"> - <source>Migration error:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." xml:space="preserve"> - <source>Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Migration is completed" xml:space="preserve"> - <source>Migration is completed</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Moderate" xml:space="preserve"> - <source>Moderate</source> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Moderated at" xml:space="preserve"> - <source>Moderated at</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Moderated at: %@" xml:space="preserve"> - <source>Moderated at: %@</source> - <note>copied message info</note> - </trans-unit> - <trans-unit id="More improvements are coming soon!" xml:space="preserve"> - <source>More improvements are coming soon!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> - <source>Most likely this contact has deleted the connection with you.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Multiple chat profiles" xml:space="preserve"> - <source>Multiple chat profiles</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Mute" xml:space="preserve"> - <source>Mute</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Muted when inactive!" xml:space="preserve"> - <source>Muted when inactive!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Name" xml:space="preserve"> - <source>Name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Network & servers" xml:space="preserve"> - <source>Network & servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Network settings" xml:space="preserve"> - <source>Network settings</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Network status" xml:space="preserve"> - <source>Network status</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="New Passcode" xml:space="preserve"> - <source>New Passcode</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="New contact request" xml:space="preserve"> - <source>New contact request</source> - <note>notification</note> - </trans-unit> - <trans-unit id="New contact:" xml:space="preserve"> - <source>New contact:</source> - <note>notification</note> - </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> - <source>New database archive</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="New display name" xml:space="preserve"> - <source>New display name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="New in %@" xml:space="preserve"> - <source>New in %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="New member role" xml:space="preserve"> - <source>New member role</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="New message" xml:space="preserve"> - <source>New message</source> - <note>notification</note> - </trans-unit> - <trans-unit id="New passphrase…" xml:space="preserve"> - <source>New passphrase…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No" xml:space="preserve"> - <source>No</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No app password" xml:space="preserve"> - <source>No app password</source> - <note>Authentication unavailable</note> - </trans-unit> - <trans-unit id="No contacts selected" xml:space="preserve"> - <source>No contacts selected</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No contacts to add" xml:space="preserve"> - <source>No contacts to add</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No device token!" xml:space="preserve"> - <source>No device token!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No group!" xml:space="preserve"> - <source>Group not found!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No permission to record voice message" xml:space="preserve"> - <source>No permission to record voice message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No received or sent files" xml:space="preserve"> - <source>No received or sent files</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Notifications" xml:space="preserve"> - <source>Notifications</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Notifications are disabled!" xml:space="preserve"> - <source>Notifications are disabled!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> - <source>Now admins can: -- delete members' messages. -- disable members ("observer" role)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off" xml:space="preserve"> - <source>Off</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> - <source>Off (Local)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Ok" xml:space="preserve"> - <source>Ok</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Old database" xml:space="preserve"> - <source>Old database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> - <source>Old database archive</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="One-time invitation link" xml:space="preserve"> - <source>One-time invitation link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Onion hosts will not be used." xml:space="preserve"> - <source>Onion hosts will not be used.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> - <source>Only group owners can change group preferences.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only group owners can enable voice messages." xml:space="preserve"> - <source>Only group owners can enable voice messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only you can add message reactions." xml:space="preserve"> - <source>Only you can add message reactions.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only you can make calls." xml:space="preserve"> - <source>Only you can make calls.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only you can send disappearing messages." xml:space="preserve"> - <source>Only you can send disappearing messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only you can send voice messages." xml:space="preserve"> - <source>Only you can send voice messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only your contact can add message reactions." xml:space="preserve"> - <source>Only your contact can add message reactions.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only your contact can make calls." xml:space="preserve"> - <source>Only your contact can make calls.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only your contact can send disappearing messages." xml:space="preserve"> - <source>Only your contact can send disappearing messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only your contact can send voice messages." xml:space="preserve"> - <source>Only your contact can send voice messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Open Settings" xml:space="preserve"> - <source>Open Settings</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Open chat" xml:space="preserve"> - <source>Open chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Open chat console" xml:space="preserve"> - <source>Open chat console</source> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve"> - <source>Open user profiles</source> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve"> - <source>Opening database…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve"> - <source>Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="PING count" xml:space="preserve"> - <source>PING count</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="PING interval" xml:space="preserve"> - <source>PING interval</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Passcode" xml:space="preserve"> - <source>Passcode</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Passcode changed!" xml:space="preserve"> - <source>Passcode changed!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Passcode entry" xml:space="preserve"> - <source>Passcode entry</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Passcode not changed!" xml:space="preserve"> - <source>Passcode not changed!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Passcode set!" xml:space="preserve"> - <source>Passcode set!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Password to show" xml:space="preserve"> - <source>Password to show</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Paste" xml:space="preserve"> - <source>Paste</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Paste image" xml:space="preserve"> - <source>Paste image</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve"> - <source>Paste received link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve"> - <source>Paste the link you received into the box below to connect with your contact.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Periodically" xml:space="preserve"> - <source>Periodically</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Permanent decryption error" xml:space="preserve"> - <source>Permanent decryption error</source> - <note>message decrypt error item</note> - </trans-unit> - <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> - <source>Please ask your contact to enable sending voice messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> - <source>Please check that you used the correct link or ask your contact to send you another one.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please check your network connection with %@ and try again." xml:space="preserve"> - <source>Please check your network connection with %@ and try again.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please check yours and your contact preferences." xml:space="preserve"> - <source>Please check yours and your contact preferences.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please contact group admin." xml:space="preserve"> - <source>Please contact group admin.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please enter correct current passphrase." xml:space="preserve"> - <source>Please enter correct current passphrase.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please enter the previous password after restoring database backup. This action can not be undone." xml:space="preserve"> - <source>Please enter the previous password after restoring database backup. This action can not be undone.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please remember or store it securely - there is no way to recover a lost passcode!" xml:space="preserve"> - <source>Please remember or store it securely - there is no way to recover a lost passcode!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please report it to the developers." xml:space="preserve"> - <source>Please report it to the developers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please restart the app and migrate the database to enable push notifications." xml:space="preserve"> - <source>Please restart the app and migrate the database to enable push notifications.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please store passphrase securely, you will NOT be able to access chat if you lose it." xml:space="preserve"> - <source>Please store passphrase securely, you will NOT be able to access chat if you lose it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please store passphrase securely, you will NOT be able to change it if you lose it." xml:space="preserve"> - <source>Please store passphrase securely, you will NOT be able to change it if you lose it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Polish interface" xml:space="preserve"> - <source>Polish interface</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> - <source>Possibly, certificate fingerprint in server address is incorrect</source> - <note>server test error</note> - </trans-unit> - <trans-unit id="Preserve the last message draft, with attachments." xml:space="preserve"> - <source>Preserve the last message draft, with attachments.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Preset server" xml:space="preserve"> - <source>Preset server</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Preset server address" xml:space="preserve"> - <source>Preset server address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Preview" xml:space="preserve"> - <source>Preview</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Privacy & security" xml:space="preserve"> - <source>Privacy & security</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Privacy redefined" xml:space="preserve"> - <source>Privacy redefined</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Private filenames" xml:space="preserve"> - <source>Private filenames</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile and server connections" xml:space="preserve"> - <source>Profile and server connections</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile image" xml:space="preserve"> - <source>Profile image</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile password" xml:space="preserve"> - <source>Profile password</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> - <source>Profile update will be sent to your contacts.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> - <source>Prohibit audio/video calls.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Prohibit irreversible message deletion." xml:space="preserve"> - <source>Prohibit irreversible message deletion.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Prohibit message reactions." xml:space="preserve"> - <source>Prohibit message reactions.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Prohibit messages reactions." xml:space="preserve"> - <source>Prohibit messages reactions.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> - <source>Prohibit sending direct messages to members.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Prohibit sending disappearing messages." xml:space="preserve"> - <source>Prohibit sending disappearing messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Prohibit sending voice messages." xml:space="preserve"> - <source>Prohibit sending voice messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Protect app screen" xml:space="preserve"> - <source>Protect app screen</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> - <source>Protect your chat profiles with a password!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Protocol timeout" xml:space="preserve"> - <source>Protocol timeout</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Push notifications" xml:space="preserve"> - <source>Push notifications</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Rate the app" xml:space="preserve"> - <source>Rate the app</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="React..." xml:space="preserve"> - <source>React...</source> - <note>chat item menu</note> - </trans-unit> - <trans-unit id="Read" xml:space="preserve"> - <source>Read</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Read more" xml:space="preserve"> - <source>Read more</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> - <source>Read more in our GitHub repository.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> - <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Received at" xml:space="preserve"> - <source>Received at</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Received at: %@" xml:space="preserve"> - <source>Received at: %@</source> - <note>copied message info</note> - </trans-unit> - <trans-unit id="Received file event" xml:space="preserve"> - <source>Received file event</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Received message" xml:space="preserve"> - <source>Received message</source> - <note>message info title</note> - </trans-unit> - <trans-unit id="Receiving file will be stopped." xml:space="preserve"> - <source>Receiving file will be stopped.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Receiving via" xml:space="preserve"> - <source>Receiving via</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> - <source>Recipients see updates as you type them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Record updated at" xml:space="preserve"> - <source>Record updated at</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Record updated at: %@" xml:space="preserve"> - <source>Record updated at: %@</source> - <note>copied message info</note> - </trans-unit> - <trans-unit id="Reduced battery usage" xml:space="preserve"> - <source>Reduced battery usage</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reject" xml:space="preserve"> - <source>Reject</source> - <note>reject incoming call via notification</note> - </trans-unit> - <trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve"> - <source>Reject contact (sender NOT notified)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reject contact request" xml:space="preserve"> - <source>Reject contact request</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Relay server is only used if necessary. Another party can observe your IP address." xml:space="preserve"> - <source>Relay server is only used if necessary. Another party can observe your IP address.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Relay server protects your IP address, but it can observe the duration of the call." xml:space="preserve"> - <source>Relay server protects your IP address, but it can observe the duration of the call.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Remove" xml:space="preserve"> - <source>Remove</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Remove member" xml:space="preserve"> - <source>Remove member</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Remove member?" xml:space="preserve"> - <source>Remove member?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Remove passphrase from keychain?" xml:space="preserve"> - <source>Remove passphrase from keychain?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reply" xml:space="preserve"> - <source>Reply</source> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Required" xml:space="preserve"> - <source>Required</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reset" xml:space="preserve"> - <source>Reset</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reset colors" xml:space="preserve"> - <source>Reset colors</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reset to defaults" xml:space="preserve"> - <source>Reset to defaults</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> - <source>Restart the app to create a new chat profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Restart the app to use imported chat database" xml:space="preserve"> - <source>Restart the app to use imported chat database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Restore" xml:space="preserve"> - <source>Restore</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Restore database backup" xml:space="preserve"> - <source>Restore database backup</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Restore database backup?" xml:space="preserve"> - <source>Restore database backup?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Restore database error" xml:space="preserve"> - <source>Restore database error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reveal" xml:space="preserve"> - <source>Reveal</source> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Revert" xml:space="preserve"> - <source>Revert</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Revoke" xml:space="preserve"> - <source>Revoke</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Revoke file" xml:space="preserve"> - <source>Revoke file</source> - <note>cancel file action</note> - </trans-unit> - <trans-unit id="Revoke file?" xml:space="preserve"> - <source>Revoke file?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Role" xml:space="preserve"> - <source>Role</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Run chat" xml:space="preserve"> - <source>Run chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve"> - <source>SMP servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save" xml:space="preserve"> - <source>Save</source> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Save (and notify contacts)" xml:space="preserve"> - <source>Save (and notify contacts)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save and notify contact" xml:space="preserve"> - <source>Save and notify contact</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save and notify group members" xml:space="preserve"> - <source>Save and notify group members</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save and update group profile" xml:space="preserve"> - <source>Save and update group profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save archive" xml:space="preserve"> - <source>Save archive</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve"> - <source>Save auto-accept settings</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save group profile" xml:space="preserve"> - <source>Save group profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save passphrase and open chat" xml:space="preserve"> - <source>Save passphrase and open chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save passphrase in Keychain" xml:space="preserve"> - <source>Save passphrase in Keychain</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save preferences?" xml:space="preserve"> - <source>Save preferences?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save profile password" xml:space="preserve"> - <source>Save profile password</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save servers" xml:space="preserve"> - <source>Save servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save servers?" xml:space="preserve"> - <source>Save servers?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve"> - <source>Save settings?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save welcome message?" xml:space="preserve"> - <source>Save welcome message?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> - <source>Saved WebRTC ICE servers will be removed</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Scan QR code" xml:space="preserve"> - <source>Scan QR code</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Scan code" xml:space="preserve"> - <source>Scan code</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Scan security code from your contact's app." xml:space="preserve"> - <source>Scan security code from your contact's app.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Scan server QR code" xml:space="preserve"> - <source>Scan server QR code</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Search" xml:space="preserve"> - <source>Search</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Secure queue" xml:space="preserve"> - <source>Secure queue</source> - <note>server test step</note> - </trans-unit> - <trans-unit id="Security assessment" xml:space="preserve"> - <source>Security assessment</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Security code" xml:space="preserve"> - <source>Security code</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Select" xml:space="preserve"> - <source>Select</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Self-destruct" xml:space="preserve"> - <source>Self-destruct</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Self-destruct passcode" xml:space="preserve"> - <source>Self-destruct passcode</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Self-destruct passcode changed!" xml:space="preserve"> - <source>Self-destruct passcode changed!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Self-destruct passcode enabled!" xml:space="preserve"> - <source>Self-destruct passcode enabled!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send" xml:space="preserve"> - <source>Send</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send a live message - it will update for the recipient(s) as you type it" xml:space="preserve"> - <source>Send a live message - it will update for the recipient(s) as you type it</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve"> - <source>Send direct message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send disappearing message" xml:space="preserve"> - <source>Send disappearing message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send link previews" xml:space="preserve"> - <source>Send link previews</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send live message" xml:space="preserve"> - <source>Send live message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send notifications" xml:space="preserve"> - <source>Send notifications</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> - <source>Send notifications:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send questions and ideas" xml:space="preserve"> - <source>Send questions and ideas</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve"> - <source>Send them from gallery or custom keyboards.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> - <source>Sender cancelled file transfer.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> - <source>Sender may have deleted the connection request.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Sending file will be stopped." xml:space="preserve"> - <source>Sending file will be stopped.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Sending via" xml:space="preserve"> - <source>Sending via</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Sent at" xml:space="preserve"> - <source>Sent at</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Sent at: %@" xml:space="preserve"> - <source>Sent at: %@</source> - <note>copied message info</note> - </trans-unit> - <trans-unit id="Sent file event" xml:space="preserve"> - <source>Sent file event</source> - <note>notification</note> - </trans-unit> - <trans-unit id="Sent message" xml:space="preserve"> - <source>Sent message</source> - <note>message info title</note> - </trans-unit> - <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> - <source>Sent messages will be deleted after set time.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> - <source>Server requires authorization to create queues, check password</source> - <note>server test error</note> - </trans-unit> - <trans-unit id="Server requires authorization to upload, check password" xml:space="preserve"> - <source>Server requires authorization to upload, check password</source> - <note>server test error</note> - </trans-unit> - <trans-unit id="Server test failed!" xml:space="preserve"> - <source>Server test failed!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Servers" xml:space="preserve"> - <source>Servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Set 1 day" xml:space="preserve"> - <source>Set 1 day</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Set contact name…" xml:space="preserve"> - <source>Set contact name…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Set group preferences" xml:space="preserve"> - <source>Set group preferences</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Set it instead of system authentication." xml:space="preserve"> - <source>Set it instead of system authentication.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Set passcode" xml:space="preserve"> - <source>Set passcode</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Set passphrase to export" xml:space="preserve"> - <source>Set passphrase to export</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Set the message shown to new members!" xml:space="preserve"> - <source>Set the message shown to new members!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Set timeouts for proxy/VPN" xml:space="preserve"> - <source>Set timeouts for proxy/VPN</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Settings" xml:space="preserve"> - <source>Settings</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Share" xml:space="preserve"> - <source>Share</source> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Share 1-time link" xml:space="preserve"> - <source>Share 1-time link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Share address" xml:space="preserve"> - <source>Share address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Share address with contacts?" xml:space="preserve"> - <source>Share address with contacts?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Share link" xml:space="preserve"> - <source>Share link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve"> - <source>Share one-time invitation link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Share with contacts" xml:space="preserve"> - <source>Share with contacts</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Show calls in phone history" xml:space="preserve"> - <source>Show calls in phone history</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Show developer options" xml:space="preserve"> - <source>Show developer options</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Show preview" xml:space="preserve"> - <source>Show preview</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Show:" xml:space="preserve"> - <source>Show:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX Address" xml:space="preserve"> - <source>SimpleX Address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> - <source>SimpleX Chat security was audited by Trail of Bits.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX Lock" xml:space="preserve"> - <source>SimpleX Lock</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX Lock mode" xml:space="preserve"> - <source>SimpleX Lock mode</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX Lock not enabled!" xml:space="preserve"> - <source>SimpleX Lock not enabled!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX Lock turned on" xml:space="preserve"> - <source>SimpleX Lock turned on</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX address" xml:space="preserve"> - <source>SimpleX address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX contact address" xml:space="preserve"> - <source>SimpleX contact address</source> - <note>simplex link type</note> - </trans-unit> - <trans-unit id="SimpleX encrypted message or connection event" xml:space="preserve"> - <source>SimpleX encrypted message or connection event</source> - <note>notification</note> - </trans-unit> - <trans-unit id="SimpleX group link" xml:space="preserve"> - <source>SimpleX group link</source> - <note>simplex link type</note> - </trans-unit> - <trans-unit id="SimpleX links" xml:space="preserve"> - <source>SimpleX links</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> - <source>SimpleX one-time invitation</source> - <note>simplex link type</note> - </trans-unit> - <trans-unit id="Skip" xml:space="preserve"> - <source>Skip</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Skipped messages" xml:space="preserve"> - <source>Skipped messages</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> - <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Somebody" xml:space="preserve"> - <source>Somebody</source> - <note>notification title</note> - </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve"> - <source>Start a new chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Start chat" xml:space="preserve"> - <source>Start chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Start migration" xml:space="preserve"> - <source>Start migration</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Stop" xml:space="preserve"> - <source>Stop</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Stop SimpleX" xml:space="preserve"> - <source>Stop SimpleX</source> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> - <source>Stop chat to enable database actions</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> - <source>Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Stop chat?" xml:space="preserve"> - <source>Stop chat?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Stop file" xml:space="preserve"> - <source>Stop file</source> - <note>cancel file action</note> - </trans-unit> - <trans-unit id="Stop receiving file?" xml:space="preserve"> - <source>Stop receiving file?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Stop sending file?" xml:space="preserve"> - <source>Stop sending file?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Stop sharing" xml:space="preserve"> - <source>Stop sharing</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Stop sharing address?" xml:space="preserve"> - <source>Stop sharing address?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Submit" xml:space="preserve"> - <source>Submit</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Support SimpleX Chat" xml:space="preserve"> - <source>Support SimpleX Chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="System" xml:space="preserve"> - <source>System</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="System authentication" xml:space="preserve"> - <source>System authentication</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="TCP connection timeout" xml:space="preserve"> - <source>TCP connection timeout</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> - <source>TCP_KEEPCNT</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="TCP_KEEPIDLE" xml:space="preserve"> - <source>TCP_KEEPIDLE</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="TCP_KEEPINTVL" xml:space="preserve"> - <source>TCP_KEEPINTVL</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Take picture" xml:space="preserve"> - <source>Take picture</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Tap button " xml:space="preserve"> - <source>Tap button </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Tap to activate profile." xml:space="preserve"> - <source>Tap to activate profile.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Tap to join" xml:space="preserve"> - <source>Tap to join</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Tap to join incognito" xml:space="preserve"> - <source>Tap to join incognito</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve"> - <source>Tap to start a new chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Test failed at step %@." xml:space="preserve"> - <source>Test failed at step %@.</source> - <note>server test failure</note> - </trans-unit> - <trans-unit id="Test server" xml:space="preserve"> - <source>Test server</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Test servers" xml:space="preserve"> - <source>Test servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Tests failed!" xml:space="preserve"> - <source>Tests failed!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> - <source>Thank you for installing SimpleX Chat!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> - <source>Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Thanks to the users – contribute via Weblate!" xml:space="preserve"> - <source>Thanks to the users – contribute via Weblate!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> - <source>The ID of the next message is incorrect (less or equal to the previous). -It can happen because of some bug or when the connection is compromised.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve"> - <source>The app can notify you when you receive messages or contact requests - please open settings to enable.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> - <source>The attempt to change database passphrase was not completed.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> - <source>The connection you accepted will be cancelled!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The contact you shared this link with will NOT be able to connect!" xml:space="preserve"> - <source>The contact you shared this link with will NOT be able to connect!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The created archive is available via app Settings / Database / Old database archive." xml:space="preserve"> - <source>The created archive is available via app Settings / Database / Old database archive.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The group is fully decentralized – it is visible only to the members." xml:space="preserve"> - <source>The group is fully decentralized – it is visible only to the members.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The hash of the previous message is different." xml:space="preserve"> - <source>The hash of the previous message is different.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The message will be deleted for all members." xml:space="preserve"> - <source>The message will be deleted for all members.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The message will be marked as moderated for all members." xml:space="preserve"> - <source>The message will be marked as moderated for all members.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> - <source>The old database was not removed during the migration, it can be deleted.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The sender will NOT be notified" xml:space="preserve"> - <source>The sender will NOT be notified</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The servers for new connections of your current chat profile **%@**." xml:space="preserve"> - <source>The servers for new connections of your current chat profile **%@**.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Theme" xml:space="preserve"> - <source>Theme</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve"> - <source>There should be at least one user profile.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> - <source>There should be at least one visible user profile.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> - <source>This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." xml:space="preserve"> - <source>This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> - <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This error is permanent for this connection, please re-connect." xml:space="preserve"> - <source>This error is permanent for this connection, please re-connect.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member)." xml:space="preserve"> - <source>This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This group no longer exists." xml:space="preserve"> - <source>This group no longer exists.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> - <source>This setting applies to messages in your current chat profile **%@**.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> - <source>To ask any questions and to receive updates:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To connect, your contact can scan QR code or use the link in the app." xml:space="preserve"> - <source>To connect, your contact can scan QR code or use the link in the app.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve"> - <source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To make a new connection" xml:space="preserve"> - <source>To make a new connection</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> - <source>To protect timezone, image/voice files use UTC.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> - <source>To protect your information, turn on SimpleX Lock. -You will be prompted to complete authentication before this feature is enabled.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> - <source>To record voice message please grant permission to use Microphone.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." xml:space="preserve"> - <source>To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> - <source>To support instant push notifications the chat database has to be migrated.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> - <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Transport isolation" xml:space="preserve"> - <source>Transport isolation</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> - <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Trying to connect to the server used to receive messages from this contact." xml:space="preserve"> - <source>Trying to connect to the server used to receive messages from this contact.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Turn off" xml:space="preserve"> - <source>Turn off</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve"> - <source>Turn off notifications?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Turn on" xml:space="preserve"> - <source>Turn on</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unable to record voice message" xml:space="preserve"> - <source>Unable to record voice message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve"> - <source>Unexpected error: %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unexpected migration state" xml:space="preserve"> - <source>Unexpected migration state</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unhide" xml:space="preserve"> - <source>Unhide</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unhide chat profile" xml:space="preserve"> - <source>Unhide chat profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unhide profile" xml:space="preserve"> - <source>Unhide profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unit" xml:space="preserve"> - <source>Unit</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unknown caller" xml:space="preserve"> - <source>Unknown caller</source> - <note>callkit banner</note> - </trans-unit> - <trans-unit id="Unknown database error: %@" xml:space="preserve"> - <source>Unknown database error: %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unknown error" xml:space="preserve"> - <source>Unknown error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> - <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection." xml:space="preserve"> - <source>Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unlock" xml:space="preserve"> - <source>Unlock</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unlock app" xml:space="preserve"> - <source>Unlock app</source> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Unmute" xml:space="preserve"> - <source>Unmute</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unread" xml:space="preserve"> - <source>Unread</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Update" xml:space="preserve"> - <source>Update</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> - <source>Update .onion hosts setting?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Update database passphrase" xml:space="preserve"> - <source>Update database passphrase</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Update network settings?" xml:space="preserve"> - <source>Update network settings?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve"> - <source>Update transport isolation mode?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> - <source>Updating settings will re-connect the client to all servers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Upgrade and open chat" xml:space="preserve"> - <source>Upgrade and open chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Upload file" xml:space="preserve"> - <source>Upload file</source> - <note>server test step</note> - </trans-unit> - <trans-unit id="Use .onion hosts" xml:space="preserve"> - <source>Use .onion hosts</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> - <source>Use SimpleX Chat servers?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Use chat" xml:space="preserve"> - <source>Use chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Use for new connections" xml:space="preserve"> - <source>Use for new connections</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Use iOS call interface" xml:space="preserve"> - <source>Use iOS call interface</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Use server" xml:space="preserve"> - <source>Use server</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> - <source>Using SimpleX Chat servers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Verify connection security" xml:space="preserve"> - <source>Verify connection security</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Verify security code" xml:space="preserve"> - <source>Verify security code</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Via browser" xml:space="preserve"> - <source>Via browser</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Video call" xml:space="preserve"> - <source>Video call</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Video will be received when your contact completes uploading it." xml:space="preserve"> - <source>Video will be received when your contact completes uploading it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Video will be received when your contact is online, please wait or check later!" xml:space="preserve"> - <source>Video will be received when your contact is online, please wait or check later!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Videos and files up to 1gb" xml:space="preserve"> - <source>Videos and files up to 1gb</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="View security code" xml:space="preserve"> - <source>View security code</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Voice messages" xml:space="preserve"> - <source>Voice messages</source> - <note>chat feature</note> - </trans-unit> - <trans-unit id="Voice messages are prohibited in this chat." xml:space="preserve"> - <source>Voice messages are prohibited in this chat.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Voice messages prohibited!" xml:space="preserve"> - <source>Voice messages prohibited!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Voice message…" xml:space="preserve"> - <source>Voice message…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Waiting for file" xml:space="preserve"> - <source>Waiting for file</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Waiting for image" xml:space="preserve"> - <source>Waiting for image</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Waiting for video" xml:space="preserve"> - <source>Waiting for video</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> - <source>Warning: you may lose some data!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="WebRTC ICE servers" xml:space="preserve"> - <source>WebRTC ICE servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Welcome %@!" xml:space="preserve"> - <source>Welcome %@!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Welcome message" xml:space="preserve"> - <source>Welcome message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="What's new" xml:space="preserve"> - <source>What's new</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="When available" xml:space="preserve"> - <source>When available</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> - <source>When people request to connect, you can accept or reject it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> - <source>When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="With optional welcome message." xml:space="preserve"> - <source>With optional welcome message.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Wrong database passphrase" xml:space="preserve"> - <source>Wrong database passphrase</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Wrong passphrase!" xml:space="preserve"> - <source>Wrong passphrase!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="XFTP servers" xml:space="preserve"> - <source>XFTP servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You accepted connection" xml:space="preserve"> - <source>You accepted connection</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You allow" xml:space="preserve"> - <source>You allow</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You already have a chat profile with the same display name. Please choose another name." xml:space="preserve"> - <source>You already have a chat profile with the same display name. Please choose another name.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You are already connected to %@." xml:space="preserve"> - <source>You are already connected to %@.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve"> - <source>You are connected to the server used to receive messages from this contact.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You are invited to group" xml:space="preserve"> - <source>You are invited to group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> - <source>You can accept calls from lock screen, without device and app authentication.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can create it later" xml:space="preserve"> - <source>You can create it later</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> - <source>You can hide or mute a user profile - swipe it to the right.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> - <note>notification body</note> - </trans-unit> - <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> - <source>You can set lock screen notification preview via settings.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." xml:space="preserve"> - <source>You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can share this address with your contacts to let them connect with **%@**." xml:space="preserve"> - <source>You can share this address with your contacts to let them connect with **%@**.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> - <source>You can share your address as a link or QR code - anybody can connect to you.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> - <source>You can start chat via app Settings / Database or by restarting the app</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> - <source>You can turn on SimpleX Lock via Settings.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can use markdown to format messages:" xml:space="preserve"> - <source>You can use markdown to format messages:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can't send messages!" xml:space="preserve"> - <source>You can't send messages!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You could not be verified; please try again." xml:space="preserve"> - <source>You could not be verified; please try again.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> - <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You invited your contact" xml:space="preserve"> - <source>You invited your contact</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You joined this group" xml:space="preserve"> - <source>You joined this group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You joined this group. Connecting to inviting group member." xml:space="preserve"> - <source>You joined this group. Connecting to inviting group member.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> - <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> - <source>You need to allow your contact to send voice messages to be able to send them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You rejected group invitation" xml:space="preserve"> - <source>You rejected group invitation</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You sent group invitation" xml:space="preserve"> - <source>You sent group invitation</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> - <source>You will be connected to group when the group host's device is online, please wait or check later!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve"> - <source>You will be connected when your connection request is accepted, please wait or check later!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will be connected when your contact's device is online, please wait or check later!" xml:space="preserve"> - <source>You will be connected when your contact's device is online, please wait or check later!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will be required to authenticate when you start or resume the app after 30 seconds in background." xml:space="preserve"> - <source>You will be required to authenticate when you start or resume the app after 30 seconds in background.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve"> - <source>You will join a group this link refers to and connect to its group members.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve"> - <source>You will still receive calls and notifications from muted profiles when they are active.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> - <source>You will stop receiving messages from this group. Chat history will be preserved.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You won't lose your contacts if you later delete your address." xml:space="preserve"> - <source>You won't lose your contacts if you later delete your address.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" xml:space="preserve"> - <source>You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" xml:space="preserve"> - <source>You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your %@ servers" xml:space="preserve"> - <source>Your %@ servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your ICE servers" xml:space="preserve"> - <source>Your ICE servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your SimpleX address" xml:space="preserve"> - <source>Your SimpleX address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your XFTP servers" xml:space="preserve"> - <source>Your XFTP servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your calls" xml:space="preserve"> - <source>Your calls</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat database" xml:space="preserve"> - <source>Your chat database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat database is not encrypted - set passphrase to encrypt it." xml:space="preserve"> - <source>Your chat database is not encrypted - set passphrase to encrypt it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat profile will be sent to group members" xml:space="preserve"> - <source>Your chat profile will be sent to group members</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve"> - <source>Your chat profile will be sent to your contact</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat profiles" xml:space="preserve"> - <source>Your chat profiles</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chats" xml:space="preserve"> - <source>Your chats</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> - <source>Your contact sent a file that is larger than currently supported maximum size (%@).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contacts can allow full message deletion." xml:space="preserve"> - <source>Your contacts can allow full message deletion.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> - <source>Your contacts in SimpleX will see it. -You can change it in Settings.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contacts will remain connected." xml:space="preserve"> - <source>Your contacts will remain connected.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> - <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your current profile" xml:space="preserve"> - <source>Your current profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your preferences" xml:space="preserve"> - <source>Your preferences</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your privacy" xml:space="preserve"> - <source>Your privacy</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>Your profile will be sent to the contact that you received this link from</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your random profile" xml:space="preserve"> - <source>Your random profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your server address" xml:space="preserve"> - <source>Your server address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your settings" xml:space="preserve"> - <source>Your settings</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" xml:space="preserve"> - <source>[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="[Send us email](mailto:chat@simplex.chat)" xml:space="preserve"> - <source>[Send us email](mailto:chat@simplex.chat)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> - <source>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="_italic_" xml:space="preserve"> - <source>\_italic_</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="`a + b`" xml:space="preserve"> - <source>\`a + b`</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="above, then choose:" xml:space="preserve"> - <source>above, then choose:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="accepted call" xml:space="preserve"> - <source>accepted call</source> - <note>call status</note> - </trans-unit> - <trans-unit id="admin" xml:space="preserve"> - <source>admin</source> - <note>member role</note> - </trans-unit> - <trans-unit id="always" xml:space="preserve"> - <source>always</source> - <note>pref value</note> - </trans-unit> - <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> - <source>audio call (not e2e encrypted)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="bad message ID" xml:space="preserve"> - <source>bad message ID</source> - <note>integrity error chat item</note> - </trans-unit> - <trans-unit id="bad message hash" xml:space="preserve"> - <source>bad message hash</source> - <note>integrity error chat item</note> - </trans-unit> - <trans-unit id="bold" xml:space="preserve"> - <source>bold</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="call error" xml:space="preserve"> - <source>call error</source> - <note>call status</note> - </trans-unit> - <trans-unit id="call in progress" xml:space="preserve"> - <source>call in progress</source> - <note>call status</note> - </trans-unit> - <trans-unit id="calling…" xml:space="preserve"> - <source>calling…</source> - <note>call status</note> - </trans-unit> - <trans-unit id="cancelled %@" xml:space="preserve"> - <source>cancelled %@</source> - <note>feature offered item</note> - </trans-unit> - <trans-unit id="changed address for you" xml:space="preserve"> - <source>changed address for you</source> - <note>chat item text</note> - </trans-unit> - <trans-unit id="changed role of %@ to %@" xml:space="preserve"> - <source>changed role of %1$@ to %2$@</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="changed your role to %@" xml:space="preserve"> - <source>changed your role to %@</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="changing address for %@..." xml:space="preserve"> - <source>changing address for %@...</source> - <note>chat item text</note> - </trans-unit> - <trans-unit id="changing address..." xml:space="preserve"> - <source>changing address...</source> - <note>chat item text</note> - </trans-unit> - <trans-unit id="colored" xml:space="preserve"> - <source>colored</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="complete" xml:space="preserve"> - <source>complete</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connect to SimpleX Chat developers." xml:space="preserve"> - <source>connect to SimpleX Chat developers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connected" xml:space="preserve"> - <source>connected</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connecting" xml:space="preserve"> - <source>connecting</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connecting (accepted)" xml:space="preserve"> - <source>connecting (accepted)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connecting (announced)" xml:space="preserve"> - <source>connecting (announced)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connecting (introduced)" xml:space="preserve"> - <source>connecting (introduced)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connecting (introduction invitation)" xml:space="preserve"> - <source>connecting (introduction invitation)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connecting call" xml:space="preserve"> - <source>connecting call…</source> - <note>call status</note> - </trans-unit> - <trans-unit id="connecting…" xml:space="preserve"> - <source>connecting…</source> - <note>chat list item title</note> - </trans-unit> - <trans-unit id="connection established" xml:space="preserve"> - <source>connection established</source> - <note>chat list item title (it should not be shown</note> - </trans-unit> - <trans-unit id="connection:%@" xml:space="preserve"> - <source>connection:%@</source> - <note>connection information</note> - </trans-unit> - <trans-unit id="contact has e2e encryption" xml:space="preserve"> - <source>contact has e2e encryption</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="contact has no e2e encryption" xml:space="preserve"> - <source>contact has no e2e encryption</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="creator" xml:space="preserve"> - <source>creator</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="custom" xml:space="preserve"> - <source>custom</source> - <note>dropdown time picker choice</note> - </trans-unit> - <trans-unit id="database version is newer than the app, but no down migration for: %@" xml:space="preserve"> - <source>database version is newer than the app, but no down migration for: %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="days" xml:space="preserve"> - <source>days</source> - <note>time unit</note> - </trans-unit> - <trans-unit id="default (%@)" xml:space="preserve"> - <source>default (%@)</source> - <note>pref value</note> - </trans-unit> - <trans-unit id="deleted" xml:space="preserve"> - <source>deleted</source> - <note>deleted chat item</note> - </trans-unit> - <trans-unit id="deleted group" xml:space="preserve"> - <source>deleted group</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="different migration in the app/database: %@ / %@" xml:space="preserve"> - <source>different migration in the app/database: %@ / %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="direct" xml:space="preserve"> - <source>direct</source> - <note>connection level description</note> - </trans-unit> - <trans-unit id="duplicate message" xml:space="preserve"> - <source>duplicate message</source> - <note>integrity error chat item</note> - </trans-unit> - <trans-unit id="e2e encrypted" xml:space="preserve"> - <source>e2e encrypted</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="enabled" xml:space="preserve"> - <source>enabled</source> - <note>enabled status</note> - </trans-unit> - <trans-unit id="enabled for contact" xml:space="preserve"> - <source>enabled for contact</source> - <note>enabled status</note> - </trans-unit> - <trans-unit id="enabled for you" xml:space="preserve"> - <source>enabled for you</source> - <note>enabled status</note> - </trans-unit> - <trans-unit id="ended" xml:space="preserve"> - <source>ended</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="ended call %@" xml:space="preserve"> - <source>ended call %@</source> - <note>call status</note> - </trans-unit> - <trans-unit id="error" xml:space="preserve"> - <source>error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="group deleted" xml:space="preserve"> - <source>group deleted</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="group profile updated" xml:space="preserve"> - <source>group profile updated</source> - <note>snd group event chat item</note> - </trans-unit> - <trans-unit id="hours" xml:space="preserve"> - <source>hours</source> - <note>time unit</note> - </trans-unit> - <trans-unit id="iOS Keychain is used to securely store passphrase - it allows receiving push notifications." xml:space="preserve"> - <source>iOS Keychain is used to securely store passphrase - it allows receiving push notifications.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." xml:space="preserve"> - <source>iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="incognito via contact address link" xml:space="preserve"> - <source>incognito via contact address link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="incognito via group link" xml:space="preserve"> - <source>incognito via group link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="incognito via one-time link" xml:space="preserve"> - <source>incognito via one-time link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="indirect (%d)" xml:space="preserve"> - <source>indirect (%d)</source> - <note>connection level description</note> - </trans-unit> - <trans-unit id="invalid chat" xml:space="preserve"> - <source>invalid chat</source> - <note>invalid chat data</note> - </trans-unit> - <trans-unit id="invalid chat data" xml:space="preserve"> - <source>invalid chat data</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="invalid data" xml:space="preserve"> - <source>invalid data</source> - <note>invalid chat item</note> - </trans-unit> - <trans-unit id="invitation to group %@" xml:space="preserve"> - <source>invitation to group %@</source> - <note>group name</note> - </trans-unit> - <trans-unit id="invited" xml:space="preserve"> - <source>invited</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="invited %@" xml:space="preserve"> - <source>invited %@</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="invited to connect" xml:space="preserve"> - <source>invited to connect</source> - <note>chat list item title</note> - </trans-unit> - <trans-unit id="invited via your group link" xml:space="preserve"> - <source>invited via your group link</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="italic" xml:space="preserve"> - <source>italic</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="join as %@" xml:space="preserve"> - <source>join as %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="left" xml:space="preserve"> - <source>left</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="marked deleted" xml:space="preserve"> - <source>marked deleted</source> - <note>marked deleted chat item preview text</note> - </trans-unit> - <trans-unit id="member" xml:space="preserve"> - <source>member</source> - <note>member role</note> - </trans-unit> - <trans-unit id="member connected" xml:space="preserve"> - <source>connected</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="message received" xml:space="preserve"> - <source>message received</source> - <note>notification</note> - </trans-unit> - <trans-unit id="minutes" xml:space="preserve"> - <source>minutes</source> - <note>time unit</note> - </trans-unit> - <trans-unit id="missed call" xml:space="preserve"> - <source>missed call</source> - <note>call status</note> - </trans-unit> - <trans-unit id="moderated" xml:space="preserve"> - <source>moderated</source> - <note>moderated chat item</note> - </trans-unit> - <trans-unit id="moderated by %@" xml:space="preserve"> - <source>moderated by %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="months" xml:space="preserve"> - <source>months</source> - <note>time unit</note> - </trans-unit> - <trans-unit id="never" xml:space="preserve"> - <source>never</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="new message" xml:space="preserve"> - <source>new message</source> - <note>notification</note> - </trans-unit> - <trans-unit id="no" xml:space="preserve"> - <source>no</source> - <note>pref value</note> - </trans-unit> - <trans-unit id="no e2e encryption" xml:space="preserve"> - <source>no e2e encryption</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="no text" xml:space="preserve"> - <source>no text</source> - <note>copied message info in history</note> - </trans-unit> - <trans-unit id="observer" xml:space="preserve"> - <source>observer</source> - <note>member role</note> - </trans-unit> - <trans-unit id="off" xml:space="preserve"> - <source>off</source> - <note>enabled status - group pref value</note> - </trans-unit> - <trans-unit id="offered %@" xml:space="preserve"> - <source>offered %@</source> - <note>feature offered item</note> - </trans-unit> - <trans-unit id="offered %@: %@" xml:space="preserve"> - <source>offered %1$@: %2$@</source> - <note>feature offered item</note> - </trans-unit> - <trans-unit id="on" xml:space="preserve"> - <source>on</source> - <note>group pref value</note> - </trans-unit> - <trans-unit id="or chat with the developers" xml:space="preserve"> - <source>or chat with the developers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="owner" xml:space="preserve"> - <source>owner</source> - <note>member role</note> - </trans-unit> - <trans-unit id="peer-to-peer" xml:space="preserve"> - <source>peer-to-peer</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="received answer…" xml:space="preserve"> - <source>received answer…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="received confirmation…" xml:space="preserve"> - <source>received confirmation…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="rejected call" xml:space="preserve"> - <source>rejected call</source> - <note>call status</note> - </trans-unit> - <trans-unit id="removed" xml:space="preserve"> - <source>removed</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="removed %@" xml:space="preserve"> - <source>removed %@</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="removed you" xml:space="preserve"> - <source>removed you</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="sec" xml:space="preserve"> - <source>sec</source> - <note>network option</note> - </trans-unit> - <trans-unit id="seconds" xml:space="preserve"> - <source>seconds</source> - <note>time unit</note> - </trans-unit> - <trans-unit id="secret" xml:space="preserve"> - <source>secret</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="starting…" xml:space="preserve"> - <source>starting…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="strike" xml:space="preserve"> - <source>strike</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="this contact" xml:space="preserve"> - <source>this contact</source> - <note>notification title</note> - </trans-unit> - <trans-unit id="unknown" xml:space="preserve"> - <source>unknown</source> - <note>connection info</note> - </trans-unit> - <trans-unit id="updated group profile" xml:space="preserve"> - <source>updated group profile</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="v%@ (%@)" xml:space="preserve"> - <source>v%@ (%@)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="via contact address link" xml:space="preserve"> - <source>via contact address link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="via group link" xml:space="preserve"> - <source>via group link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="via one-time link" xml:space="preserve"> - <source>via one-time link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="via relay" xml:space="preserve"> - <source>via relay</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> - <source>video call (not e2e encrypted)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="waiting for answer…" xml:space="preserve"> - <source>waiting for answer…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="waiting for confirmation…" xml:space="preserve"> - <source>waiting for confirmation…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="wants to connect to you!" xml:space="preserve"> - <source>wants to connect to you!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="weeks" xml:space="preserve"> - <source>weeks</source> - <note>time unit</note> - </trans-unit> - <trans-unit id="yes" xml:space="preserve"> - <source>yes</source> - <note>pref value</note> - </trans-unit> - <trans-unit id="you are invited to group" xml:space="preserve"> - <source>you are invited to group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="you are observer" xml:space="preserve"> - <source>you are observer</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="you changed address" xml:space="preserve"> - <source>you changed address</source> - <note>chat item text</note> - </trans-unit> - <trans-unit id="you changed address for %@" xml:space="preserve"> - <source>you changed address for %@</source> - <note>chat item text</note> - </trans-unit> - <trans-unit id="you changed role for yourself to %@" xml:space="preserve"> - <source>you changed role for yourself to %@</source> - <note>snd group event chat item</note> - </trans-unit> - <trans-unit id="you changed role of %@ to %@" xml:space="preserve"> - <source>you changed role of %1$@ to %2$@</source> - <note>snd group event chat item</note> - </trans-unit> - <trans-unit id="you left" xml:space="preserve"> - <source>you left</source> - <note>snd group event chat item</note> - </trans-unit> - <trans-unit id="you removed %@" xml:space="preserve"> - <source>you removed %@</source> - <note>snd group event chat item</note> - </trans-unit> - <trans-unit id="you shared one-time link" xml:space="preserve"> - <source>you shared one-time link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="you shared one-time link incognito" xml:space="preserve"> - <source>you shared one-time link incognito</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="you: " xml:space="preserve"> - <source>you: </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="~strike~" xml:space="preserve"> - <source>\~strike~</source> - <note>No comment provided by engineer.</note> - </trans-unit> - </body> - </file> - <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="ml" datatype="plaintext"> - <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/> - </header> - <body> - <trans-unit id="CFBundleName" xml:space="preserve"> - <source>SimpleX</source> - <note>Bundle name</note> - </trans-unit> - <trans-unit id="NSCameraUsageDescription" xml:space="preserve"> - <source>SimpleX needs camera access to scan QR codes to connect to other users and for video calls.</source> - <note>Privacy - Camera Usage Description</note> - </trans-unit> - <trans-unit id="NSFaceIDUsageDescription" xml:space="preserve"> - <source>SimpleX uses Face ID for local authentication</source> - <note>Privacy - Face ID Usage Description</note> - </trans-unit> - <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> - <source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source> - <note>Privacy - Microphone Usage Description</note> - </trans-unit> - <trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve"> - <source>SimpleX needs access to Photo Library for saving captured and received media</source> - <note>Privacy - Photo Library Additions Usage Description</note> - </trans-unit> - </body> - </file> - <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="ml" datatype="plaintext"> - <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/> - </header> - <body> - <trans-unit id="CFBundleDisplayName" xml:space="preserve"> - <source>SimpleX NSE</source> - <note>Bundle display name</note> - </trans-unit> - <trans-unit id="CFBundleName" xml:space="preserve"> - <source>SimpleX NSE</source> - <note>Bundle name</note> - </trans-unit> - <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> - <source>Copyright © 2022 SimpleX Chat. All rights reserved.</source> - <note>Copyright (human-readable)</note> - </trans-unit> - </body> - </file> -</xliff> 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 094a30677a..4008c57ac0 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -2,36 +2,9 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="nl" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> - <trans-unit id=" " xml:space="preserve"> - <source> -</source> - <target> -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (" xml:space="preserve"> - <source> (</source> - <target> (</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> <target> (kan gekopieerd worden)</target> @@ -107,6 +80,11 @@ <target>%@ verbonden</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve"> + <source>%@ downloaded</source> + <target>%@ gedownload</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ is connected!" xml:space="preserve"> <source>%@ is connected!</source> <target>%@ is verbonden!</target> @@ -122,16 +100,31 @@ <target>%@ is geverifieerd</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ server" xml:space="preserve"> + <source>%@ server</source> + <target>%@ server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ servers" xml:space="preserve"> <source>%@ servers</source> <target>%@ servers</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve"> + <source>%@ uploaded</source> + <target>%@ geüpload</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ wants to connect!" xml:space="preserve"> <source>%@ wants to connect!</source> <target>%@ wil verbinding maken!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve"> + <source>%1$@, %2$@</source> + <target>%1$@, %2$@</target> + <note>format for date separator in chat</note> + </trans-unit> <trans-unit id="%@, %@ and %lld members" xml:space="preserve"> <source>%@, %@ and %lld members</source> <target>%@, %@ en %lld leden</target> @@ -152,11 +145,36 @@ <target>%d dagen</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve"> + <source>%d file(s) are still being downloaded.</source> + <target>%d bestand(en) worden nog gedownload.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve"> + <source>%d file(s) failed to download.</source> + <target>%d bestand(en) konden niet worden gedownload.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve"> + <source>%d file(s) were deleted.</source> + <target>%d bestand(en) zijn verwijderd.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve"> + <source>%d file(s) were not downloaded.</source> + <target>%d bestand(en) zijn niet gedownload.</target> + <note>forward confirmation reason</note> + </trans-unit> <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> <target>%d uren</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve"> + <source>%d messages not forwarded</source> + <target>%d berichten niet doorgestuurd</target> + <note>alert title</note> + </trans-unit> <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> <target>%d min</target> @@ -172,6 +190,11 @@ <target>%d sec</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve"> + <source>%d seconds(s)</source> + <target>%d seconden</target> + <note>delete after time</note> + </trans-unit> <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> <target>%d overgeslagen bericht(en)</target> @@ -217,6 +240,11 @@ <target>%lld berichten geblokkeerd</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve"> + <source>%lld messages blocked by admin</source> + <target>%lld berichten geblokkeerd door beheerder</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld messages marked deleted" xml:space="preserve"> <source>%lld messages marked deleted</source> <target>%lld berichten gemarkeerd als verwijderd</target> @@ -237,11 +265,6 @@ <target>%lld nieuwe interface-talen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> - <source>%lld second(s)</source> - <target>%lld seconde(n)</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="%lld seconds" xml:space="preserve"> <source>%lld seconds</source> <target>%lld seconden</target> @@ -292,11 +315,6 @@ <target>%u berichten zijn overgeslagen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> - <source>(</source> - <target>(</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="(new)" xml:space="preserve"> <source>(new)</source> <target>(nieuw)</target> @@ -307,34 +325,29 @@ <target>(dit apparaat v%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve"> - <source>)</source> - <target>)</target> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target>**Contact toevoegen**: om een nieuwe uitnodigingslink aan te maken, of verbinding te maken via een link die u heeft ontvangen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target>**Nieuw contact toevoegen**: om uw eenmalige QR-code of link voor uw contact te maken.</target> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve"> + <source>**Create group**: to create a new group.</source> + <target>**Groep aanmaken**: om een nieuwe groep aan te maken.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> - <source>**Create link / QR code** for your contact to use.</source> - <target>**Maak een link / QR-code aan** die uw contact kan gebruiken.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Meer privé**: bekijk elke 20 minuten nieuwe berichten. Apparaattoken wordt gedeeld met de SimpleX Chat-server, maar niet hoeveel contacten of berichten u heeft.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Meest privé**: gebruik geen SimpleX Chat-notificatie server, controleer berichten regelmatig op de achtergrond (afhankelijk van hoe vaak u de app gebruikt).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <target>**Plak de ontvangen link** of open deze in de browser en tik op **Openen in mobiele app**.</target> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> + <target>**Let op**: als u dezelfde database op twee apparaten gebruikt, wordt de decodering van berichten van uw verbindingen verbroken, als veiligheidsmaatregel.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> @@ -342,14 +355,14 @@ <target>**Let op**: u kunt het wachtwoord NIET herstellen of wijzigen als u het kwijtraakt.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Aanbevolen**: apparaattoken en meldingen worden naar de SimpleX Chat-meldingsserver gestuurd, maar niet de berichtinhoud, -grootte of van wie het afkomstig is.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target>**Scan QR-code**: om persoonlijk of via een video gesprek verbinding te maken met uw contact.</target> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve"> + <source>**Scan / Paste link**: to connect via a link you received.</source> + <target>**Link scannen/plakken**: om verbinding te maken via een link die u hebt ontvangen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> @@ -357,6 +370,11 @@ <target>**Waarschuwing**: voor directe push meldingen is een wachtwoord vereist dat is opgeslagen in de Keychain.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve"> + <source>**Warning**: the archive will be removed.</source> + <target>**Waarschuwing**: het archief wordt verwijderd.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> <source>**e2e encrypted** audio call</source> <target>**e2e versleuteld** audio gesprek</target> @@ -372,11 +390,6 @@ <target>\*vetgedrukt*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=", " xml:space="preserve"> - <source>, </source> - <target>, </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). @@ -413,11 +426,6 @@ - bewerkingsgeschiedenis.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="." xml:space="preserve"> - <source>.</source> - <target>.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="0 sec" xml:space="preserve"> <source>0 sec</source> <target>0 sec</target> @@ -431,7 +439,8 @@ <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> <target>1 dag</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> @@ -446,18 +455,30 @@ <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> <target>1 maand</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> <target>1 week</target> - <note>time interval</note> + <note>delete after time +time interval</note> + </trans-unit> + <trans-unit id="1 year" xml:space="preserve"> + <source>1 year</source> + <target>1 jaar</target> + <note>delete after time</note> </trans-unit> <trans-unit id="1-time link" xml:space="preserve"> <source>1-time link</source> <target>Eenmalige link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> + <target>Eenmalige link die *slechts met één contactpersoon* kan worden gebruikt - deel persoonlijk of via een messenger.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="5 minutes" xml:space="preserve"> <source>5 minutes</source> <target>5 minuten</target> @@ -473,11 +494,6 @@ <target>30 seconden</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve"> - <source>: </source> - <target>: </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> @@ -502,7 +518,7 @@ </trans-unit> <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> <source>A separate TCP connection will be used **for each chat profile you have in the app**.</source> - <target>Er wordt een aparte TCP-verbinding gebruikt **voor elk chat profiel dat je in de app hebt**.</target> + <target>Er wordt een aparte TCP-verbinding gebruikt **voor elk chatprofiel dat je in de app hebt**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." xml:space="preserve"> @@ -527,31 +543,32 @@ <target>Adres wijziging afbreken?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve"> - <source>About SimpleX</source> - <target>Over SimpleX</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve"> <source>About SimpleX Chat</source> <target>Over SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve"> - <source>About SimpleX address</source> - <target>Over SimpleX adres</target> + <trans-unit id="About operators" xml:space="preserve"> + <source>About operators</source> + <target>Over operatoren</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accent color" xml:space="preserve"> - <source>Accent color</source> - <target>Accent kleur</target> + <trans-unit id="Accent" xml:space="preserve"> + <source>Accent</source> + <target>Accent</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept" xml:space="preserve"> <source>Accept</source> <target>Accepteer</target> <note>accept contact request via notification - accept incoming call via notification</note> +accept incoming call via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accept conditions" xml:space="preserve"> + <source>Accept conditions</source> + <target>Accepteer voorwaarden</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept connection request?" xml:space="preserve"> <source>Accept connection request?</source> @@ -566,16 +583,47 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Accepteer incognito</target> - <note>accept contact request via notification</note> + <note>accept contact request via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve"> + <source>Accepted conditions</source> + <target>Geaccepteerde voorwaarden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve"> + <source>Acknowledged</source> + <target>Erkend</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve"> + <source>Acknowledgement errors</source> + <target>Bevestigingsfouten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Active" xml:space="preserve"> + <source>Active</source> + <target>actief</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve"> + <source>Active connections</source> + <target>Actieve verbindingen</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> <target>Voeg een adres toe aan uw profiel, zodat uw contacten het met andere mensen kunnen delen. Profiel update wordt naar uw contacten verzonden.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> - <source>Add preset servers</source> - <target>Vooraf ingestelde servers toevoegen</target> + <trans-unit id="Add friends" xml:space="preserve"> + <source>Add friends</source> + <target>Vrienden toevoegen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve"> + <source>Add list</source> + <target>Lijst toevoegen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add profile" xml:space="preserve"> @@ -583,14 +631,19 @@ <target>Profiel toevoegen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> + <target>Server toevoegen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> <source>Add servers by scanning QR codes.</source> <target>Servers toevoegen door QR-codes te scannen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> - <target>Server toevoegen…</target> + <trans-unit id="Add team members" xml:space="preserve"> + <source>Add team members</source> + <target>Teamleden toevoegen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -598,9 +651,44 @@ <target>Toevoegen aan een ander apparaat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add to list" xml:space="preserve"> + <source>Add to list</source> + <target>Toevoegen aan lijst</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> - <target>Welkomst bericht toevoegen</target> + <target>Welkom bericht toevoegen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve"> + <source>Add your team members to the conversations.</source> + <target>Voeg uw teamleden toe aan de gesprekken.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve"> + <source>Added media & file servers</source> + <target>Media- en bestandsservers toegevoegd</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve"> + <source>Added message servers</source> + <target>Berichtservers toegevoegd</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve"> + <source>Additional accent</source> + <target>Extra accent</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve"> + <source>Additional accent 2</source> + <target>Extra accent 2</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve"> + <source>Additional secondary</source> + <target>Extra secundair</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Address" xml:space="preserve"> @@ -613,6 +701,21 @@ <target>Adres wijziging wordt afgebroken. Het oude ontvangstadres wordt gebruikt.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve"> + <source>Address or 1-time link?</source> + <target>Adres of eenmalige link?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve"> + <source>Address settings</source> + <target>Adres instellingen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve"> + <source>Admins can block a member for all.</source> + <target>Beheerders kunnen een lid voor iedereen blokkeren.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> <source>Admins can create the links to join groups.</source> <target>Beheerders kunnen de uitnodiging links naar groepen aanmaken.</target> @@ -623,6 +726,16 @@ <target>Geavanceerde netwerk instellingen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve"> + <source>Advanced settings</source> + <target>Geavanceerde instellingen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All" xml:space="preserve"> + <source>All</source> + <target>alle</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All app data is deleted." xml:space="preserve"> <source>All app data is deleted.</source> <target>Alle app-gegevens worden verwijderd.</target> @@ -633,16 +746,36 @@ <target>Alle chats en berichten worden verwijderd, dit kan niet ongedaan worden gemaakt!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <target>Alle chats worden uit de lijst %@ verwijderd en de lijst wordt verwijderd.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> <target>Alle gegevens worden bij het invoeren gewist.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve"> + <source>All data is kept private on your device.</source> + <target>Alle gegevens zijn privé op uw apparaat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> <target>Alle groepsleden blijven verbonden.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <target>Alle berichten en bestanden worden **end-to-end versleuteld** verzonden, met post-quantumbeveiliging in directe berichten.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone!</source> + <target>Alle berichten worden verwijderd. Dit kan niet ongedaan worden gemaakt!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> <target>Alle berichten worden verwijderd, dit kan niet ongedaan worden gemaakt! De berichten worden ALLEEN voor jou verwijderd.</target> @@ -653,6 +786,20 @@ <target>Alle nieuwe berichten van %@ worden verborgen!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All profiles" xml:space="preserve"> + <source>All profiles</source> + <target>Alle profielen</target> + <note>profile dropdown</note> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve"> + <source>All reports will be archived for you.</source> + <target>Alle rapporten worden voor u gearchiveerd.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All servers" xml:space="preserve"> + <source>All servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All your contacts will remain connected." xml:space="preserve"> <source>All your contacts will remain connected.</source> <target>Al uw contacten blijven verbonden.</target> @@ -663,6 +810,11 @@ <target>Al uw contacten blijven verbonden. Profiel update wordt naar uw contacten verzonden.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target>Al uw contacten, gesprekken en bestanden worden veilig gecodeerd en in delen geüpload naar geconfigureerde XFTP-relays.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow" xml:space="preserve"> <source>Allow</source> <target>Toestaan</target> @@ -673,24 +825,34 @@ <target>Sta oproepen alleen toe als uw contact dit toestaat.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve"> + <source>Allow calls?</source> + <target>Oproepen toestaan?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> <target>Sta verdwijnende berichten alleen toe als uw contact dit toestaat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <target>Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contact dit toestaat.</target> + <trans-unit id="Allow downgrade" xml:space="preserve"> + <source>Allow downgrade</source> + <target>Downgraden toestaan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target>Sta het definitief verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> <source>Allow message reactions only if your contact allows them.</source> - <target>Sta berichtreacties alleen toe als uw contact dit toestaat.</target> + <target>Sta bericht reacties alleen toe als uw contact dit toestaat.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions." xml:space="preserve"> <source>Allow message reactions.</source> - <target>Sta berichtreacties toe.</target> + <target>Sta bericht reacties toe.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow sending direct messages to members." xml:space="preserve"> @@ -703,9 +865,24 @@ <target>Toestaan dat verdwijnende berichten worden verzonden.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow to irreversibly delete sent messages.</source> - <target>Sta toe om verzonden berichten onomkeerbaar te verwijderen.</target> + <trans-unit id="Allow sharing" xml:space="preserve"> + <source>Allow sharing</source> + <target>Delen toestaan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target>Sta toe om verzonden berichten definitief te verwijderen. (24 uur)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve"> + <source>Allow to report messsages to moderators.</source> + <target>Hiermee kunt u berichten rapporteren aan moderators.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve"> + <source>Allow to send SimpleX links.</source> + <target>Sta toe dat SimpleX-links worden verzonden.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send files and media." xml:space="preserve"> @@ -730,7 +907,7 @@ </trans-unit> <trans-unit id="Allow your contacts adding message reactions." xml:space="preserve"> <source>Allow your contacts adding message reactions.</source> - <target>Sta uw contactpersonen toe om berichtreacties toe te voegen.</target> + <target>Sta uw contactpersonen toe om bericht reacties toe te voegen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to call you." xml:space="preserve"> @@ -738,9 +915,9 @@ <target>Sta toe dat uw contacten u bellen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow your contacts to irreversibly delete sent messages.</source> - <target>Laat uw contacten verzonden berichten onomkeerbaar verwijderen.</target> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target>Laat uw contacten verzonden berichten definitief verwijderen. (24 uur)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> @@ -768,9 +945,14 @@ <target>Al lid van de groep!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve"> + <source>Always use private routing.</source> + <target>Gebruik altijd privéroutering.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> <source>Always use relay</source> - <target>Verbinden via relais</target> + <target>Altijd relay gebruiken</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="An empty chat profile with the provided name is created, and the app opens as usual." xml:space="preserve"> @@ -778,21 +960,41 @@ <target>Er wordt een leeg chatprofiel met de opgegeven naam gemaakt en de app wordt zoals gewoonlijk geopend.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Another reason" xml:space="preserve"> + <source>Another reason</source> + <target>Een andere reden</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> <target>Beantwoord oproep</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> + <target>Iedereen kan servers hosten.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> <target>App build: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App data migration" xml:space="preserve"> + <source>App data migration</source> + <target>Migratie van app-gegevens</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> <source>App encrypts new local files (except videos).</source> <target>App versleutelt nieuwe lokale bestanden (behalve video's).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App group:" xml:space="preserve"> + <source>App group:</source> + <target>App-groep:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>App icon</target> @@ -808,6 +1010,11 @@ <target>De app-toegangscode wordt vervangen door een zelfvernietigings wachtwoord.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App session" xml:space="preserve"> + <source>App session</source> + <target>Appsessie</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App version" xml:space="preserve"> <source>App version</source> <target>App versie</target> @@ -823,6 +1030,66 @@ <target>Uiterlijk</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Apply" xml:space="preserve"> + <source>Apply</source> + <target>Toepassen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve"> + <source>Apply to</source> + <target>Toepassen op</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve"> + <source>Archive</source> + <target>Archief</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve"> + <source>Archive %lld reports?</source> + <target>%lld rapporten archiveren?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve"> + <source>Archive all reports?</source> + <target>Alle rapporten archiveren?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve"> + <source>Archive and upload</source> + <target>Archiveren en uploaden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve"> + <source>Archive contacts to chat later.</source> + <target>Archiveer contacten om later te chatten.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve"> + <source>Archive report</source> + <target>Rapport archiveren</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve"> + <source>Archive report?</source> + <target>Rapport archiveren?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve"> + <source>Archive reports</source> + <target>Rapporten archiveren</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve"> + <source>Archived contacts</source> + <target>Gearchiveerde contacten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve"> + <source>Archiving database</source> + <target>Database archiveren</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Attach" xml:space="preserve"> <source>Attach</source> <target>Bijvoegen</target> @@ -845,7 +1112,7 @@ </trans-unit> <trans-unit id="Audio/video calls are prohibited." xml:space="preserve"> <source>Audio/video calls are prohibited.</source> - <target>Audio/video gesprekken zijn verboden.</target> + <target>Audio/video gesprekken zijn niet toegestaan.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Authentication cancelled" xml:space="preserve"> @@ -883,11 +1150,21 @@ <target>Afbeeldingen automatisch accepteren</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve"> + <source>Auto-accept settings</source> + <target>Instellingen automatisch accepteren</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Back" xml:space="preserve"> <source>Back</source> <target>Terug</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Background" xml:space="preserve"> + <source>Background</source> + <target>Achtergrond</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Bad desktop address" xml:space="preserve"> <source>Bad desktop address</source> <target>Onjuist desktopadres</target> @@ -903,21 +1180,71 @@ <target>Onjuiste bericht hash</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better calls" xml:space="preserve"> + <source>Better calls</source> + <target>Betere gesprekken</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better groups" xml:space="preserve"> <source>Better groups</source> <target>Betere groepen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve"> + <source>Better groups performance</source> + <target>Betere prestaties van groepen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve"> + <source>Better message dates.</source> + <target>Betere datums voor berichten.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better messages" xml:space="preserve"> <source>Better messages</source> <target>Betere berichten</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better networking" xml:space="preserve"> + <source>Better networking</source> + <target>Beter netwerk</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve"> + <source>Better notifications</source> + <target>Betere meldingen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve"> + <source>Better privacy and security</source> + <target>Betere privacy en veiligheid</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve"> + <source>Better security ✅</source> + <target>Betere beveiliging ✅</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve"> + <source>Better user experience</source> + <target>Betere gebruikerservaring</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Black" xml:space="preserve"> + <source>Black</source> + <target>Zwart</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block" xml:space="preserve"> <source>Block</source> <target>Blokkeren</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block for all" xml:space="preserve"> + <source>Block for all</source> + <target>Blokkeren voor iedereen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block group members" xml:space="preserve"> <source>Block group members</source> <target>Groepsleden blokkeren</target> @@ -928,19 +1255,39 @@ <target>Lid blokkeren</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve"> + <source>Block member for all?</source> + <target>Lid voor iedereen blokkeren?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block member?" xml:space="preserve"> <source>Block member?</source> <target>Lid blokkeren?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> - <source>Both you and your contact can add message reactions.</source> - <target>Zowel u als uw contact kunnen berichtreacties toevoegen.</target> + <trans-unit id="Blocked by admin" xml:space="preserve"> + <source>Blocked by admin</source> + <target>Geblokkeerd door beheerder</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> - <source>Both you and your contact can irreversibly delete sent messages.</source> - <target>Zowel jij als je contact kunnen verzonden berichten onherroepelijk verwijderen.</target> + <trans-unit id="Blur for better privacy." xml:space="preserve"> + <source>Blur for better privacy.</source> + <target>Vervagen voor betere privacy.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve"> + <source>Blur media</source> + <target>Vervaag media</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> + <source>Both you and your contact can add message reactions.</source> + <target>Zowel u als uw contact kunnen bericht reacties toevoegen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target>Zowel jij als je contact kunnen verzonden berichten onherroepelijk verwijderen. (24 uur)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> @@ -963,9 +1310,33 @@ <target>Bulgaars, Fins, Thais en Oekraïens - dankzij de gebruikers en [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Business address" xml:space="preserve"> + <source>Business address</source> + <target>Zakelijk adres</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve"> + <source>Business chats</source> + <target>Zakelijke chats</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve"> + <source>Businesses</source> + <target>bedrijven</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> - <target>Via chat profiel (standaard) of [via verbinding](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> + <target>Via chatprofiel (standaard) of [via verbinding](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve"> + <source>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</source> + <target>Door SimpleX Chat te gebruiken, gaat u ermee akkoord: +- alleen legale content te versturen in openbare groepen. +- andere gebruikers te respecteren – geen spam.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> @@ -978,9 +1349,24 @@ <target>Oproepen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve"> - <source>Can't delete user profile!</source> - <target>Kan gebruikers profiel niet verwijderen!</target> + <trans-unit id="Calls prohibited!" xml:space="preserve"> + <source>Calls prohibited!</source> + <target>Bellen niet toegestaan!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve"> + <source>Camera not available</source> + <target>Camera niet beschikbaar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve"> + <source>Can't call contact</source> + <target>Kan contact niet bellen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve"> + <source>Can't call member</source> + <target>Kan lid niet bellen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contact!" xml:space="preserve"> @@ -993,9 +1379,20 @@ <target>Kan geen contacten uitnodigen!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve"> + <source>Can't message member</source> + <target>Kan geen bericht sturen naar lid</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> <target>Annuleren</target> + <note>alert action +alert button</note> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve"> + <source>Cancel migration</source> + <target>Migratie annuleren</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> @@ -1003,9 +1400,24 @@ <target>Geen toegang tot de keychain om database wachtwoord op te slaan</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Kan bericht niet doorsturen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> <target>Kan bestand niet ontvangen</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <target>Capaciteit overschreden - ontvanger heeft eerder verzonden berichten niet ontvangen.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve"> + <source>Cellular</source> + <target>Mobiel</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change" xml:space="preserve"> @@ -1013,6 +1425,16 @@ <target>Veranderen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve"> + <source>Change automatic message deletion?</source> + <target>Automatisch verwijderen van berichten wijzigen?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve"> + <source>Change chat profiles</source> + <target>Gebruikersprofielen wijzigen</target> + <note>authentication reason</note> + </trans-unit> <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> <target>Wachtwoord database wijzigen?</target> @@ -1057,11 +1479,26 @@ <source>Change self-destruct passcode</source> <target>Zelfvernietigings code wijzigen</target> <note>authentication reason - set passcode view</note> +set passcode view</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> - <source>Chat archive</source> - <target>Gesprek archief</target> + <trans-unit id="Chat" xml:space="preserve"> + <source>Chat</source> + <target>Chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve"> + <source>Chat already exists</source> + <target>Chat bestaat al</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve"> + <source>Chat already exists!</source> + <target>Chat bestaat al!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve"> + <source>Chat colors</source> + <target>Chat kleuren</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat console" xml:space="preserve"> @@ -1079,6 +1516,11 @@ <target>Chat database verwijderd</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve"> + <source>Chat database exported</source> + <target>Chat database geëxporteerd</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> <target>Chat database geïmporteerd</target> @@ -1094,26 +1536,81 @@ <target>Chat is gestopt</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <target>Chat is gestopt. Als je deze database al op een ander apparaat hebt gebruikt, moet je deze terugzetten voordat je met chatten begint.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve"> + <source>Chat list</source> + <target>Chatlijst</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve"> + <source>Chat migrated!</source> + <target>Chat gemigreerd!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> <target>Gesprek voorkeuren</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve"> + <source>Chat preferences were changed.</source> + <target>Chatvoorkeuren zijn gewijzigd.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve"> + <source>Chat profile</source> + <target>Gebruikers profiel</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve"> + <source>Chat theme</source> + <target>Chat thema</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <target>De chat wordt voor alle leden verwijderd - dit kan niet ongedaan worden gemaakt!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <target>De chat wordt voor je verwijderd - dit kan niet ongedaan worden gemaakt!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> - <target>Gesprekken</target> + <target>Chats</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve"> + <source>Check messages every 20 min.</source> + <target>Controleer uw berichten elke 20 minuten.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve"> + <source>Check messages when allowed.</source> + <target>Controleer berichten indien toegestaan.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> <target>Controleer het server adres en probeer het opnieuw.</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> <target>Chinese en Spaanse interface</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <target>Kies _Migreren vanaf een ander apparaat_ op het nieuwe apparaat en scan de QR-code.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> <target>Kies bestand</target> @@ -1124,10 +1621,25 @@ <target>Kies uit bibliotheek</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve"> + <source>Chunks deleted</source> + <target>Stukken verwijderd</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve"> + <source>Chunks downloaded</source> + <target>Stukken gedownload</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve"> + <source>Chunks uploaded</source> + <target>Stukken geüpload</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> <target>Wissen</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> @@ -1139,16 +1651,41 @@ <target>Gesprek wissen?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve"> + <source>Clear group?</source> + <target>Groep wissen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve"> + <source>Clear or delete group?</source> + <target>Groep wissen of verwijderen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve"> + <source>Clear private notes?</source> + <target>Privénotities verwijderen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear verification" xml:space="preserve"> <source>Clear verification</source> <target>Verwijderd verificatie</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> - <source>Colors</source> - <target>Kleuren</target> + <trans-unit id="Color chats with the new themes." xml:space="preserve"> + <source>Color chats with the new themes.</source> + <target>Kleurchats met de nieuwe thema's.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Color mode" xml:space="preserve"> + <source>Color mode</source> + <target>Kleur mode</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve"> + <source>Community guidelines violation</source> + <target>Schending van de communityrichtlijnen</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Compare file" xml:space="preserve"> <source>Compare file</source> <target>Bestand vergelijken</target> @@ -1159,11 +1696,56 @@ <target>Vergelijk beveiligingscodes met je contacten.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Completed" xml:space="preserve"> + <source>Completed</source> + <target>Voltooid</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve"> + <source>Conditions accepted on: %@.</source> + <target>Voorwaarden geaccepteerd op: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <target>Voorwaarden worden geaccepteerd voor de operator(s): **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <target>Voorwaarden zijn reeds geaccepteerd voor de volgende operator(s): **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve"> + <source>Conditions of use</source> + <target>Gebruiksvoorwaarden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <target>Voorwaarden worden geaccepteerd voor de operator(s): **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve"> + <source>Conditions will be accepted on: %@.</source> + <target>Voorwaarden worden geaccepteerd op: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <target>Voorwaarden worden automatisch geaccepteerd voor ingeschakelde operators op: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> <target>ICE servers configureren</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Configure server operators" xml:space="preserve"> + <source>Configure server operators</source> + <target>Serveroperators configureren</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> <target>Bevestigen</target> @@ -1174,11 +1756,26 @@ <target>Bevestig toegangscode</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve"> + <source>Confirm contact deletion?</source> + <target>Contact verwijderen bevestigen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm database upgrades" xml:space="preserve"> <source>Confirm database upgrades</source> <target>Bevestig database upgrades</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve"> + <source>Confirm files from unknown servers.</source> + <target>Bevestig bestanden van onbekende servers.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve"> + <source>Confirm network settings</source> + <target>Bevestig netwerk instellingen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> <target>Bevestig nieuw wachtwoord…</target> @@ -1189,6 +1786,21 @@ <target>Bevestig wachtwoord</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <target>Bevestig dat u het wachtwoord voor de database onthoudt om deze te migreren.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve"> + <source>Confirm upload</source> + <target>Bevestig het uploaden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve"> + <source>Confirmed</source> + <target>Bevestigd</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> <target>Verbind</target> @@ -1209,6 +1821,11 @@ <target>Verbinden met desktop</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve"> + <source>Connect to your friends faster.</source> + <target>Maak sneller verbinding met je vrienden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connect to yourself?" xml:space="preserve"> <source>Connect to yourself?</source> <target>Verbinding maken met jezelf?</target> @@ -1238,11 +1855,6 @@ Dit is uw eigen eenmalige link!</target> <target>Maak verbinding via link</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve"> - <source>Connect via link / QR code</source> - <target>Maak verbinding via link / QR-code</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connect via one-time link" xml:space="preserve"> <source>Connect via one-time link</source> <target>Verbinden via een eenmalige link?</target> @@ -1253,16 +1865,31 @@ Dit is uw eigen eenmalige link!</target> <target>Verbonden met %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected" xml:space="preserve"> + <source>Connected</source> + <target>Verbonden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected desktop" xml:space="preserve"> <source>Connected desktop</source> <target>Verbonden desktop</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve"> + <source>Connected servers</source> + <target>Verbonden servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected to desktop" xml:space="preserve"> <source>Connected to desktop</source> <target>Verbonden met desktop</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting" xml:space="preserve"> + <source>Connecting</source> + <target>Verbinden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> <source>Connecting to server…</source> <target>Verbinden met de server…</target> @@ -1273,6 +1900,11 @@ Dit is uw eigen eenmalige link!</target> <target>Verbinden met server... (fout: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve"> + <source>Connecting to contact, please wait or check later!</source> + <target>Er wordt verbinding gemaakt met het contact. Even geduld of controleer het later!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting to desktop" xml:space="preserve"> <source>Connecting to desktop</source> <target>Verbinding maken met desktop</target> @@ -1283,6 +1915,16 @@ Dit is uw eigen eenmalige link!</target> <target>Verbinding</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve"> + <source>Connection and servers status.</source> + <target>Verbindings- en serverstatus.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve"> + <source>Connection blocked</source> + <target>Verbinding geblokkeerd</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> <target>Verbindingsfout</target> @@ -1293,11 +1935,38 @@ Dit is uw eigen eenmalige link!</target> <target>Verbindingsfout (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve"> + <source>Connection is blocked by server operator: +%@</source> + <target>Verbinding is geblokkeerd door serveroperator: +%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve"> + <source>Connection not ready.</source> + <target>Verbinding nog niet klaar.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve"> + <source>Connection notifications</source> + <target>Verbindingsmeldingen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Verbindingsverzoek verzonden!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve"> + <source>Connection requires encryption renegotiation.</source> + <target>Verbinding vereist heronderhandeling over encryptie.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve"> + <source>Connection security</source> + <target>Beveiliging van de verbinding</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection terminated" xml:space="preserve"> <source>Connection terminated</source> <target>Verbinding beëindigd</target> @@ -1308,6 +1977,16 @@ Dit is uw eigen eenmalige link!</target> <target>Timeout verbinding</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve"> + <source>Connection with desktop stopped</source> + <target>Verbinding met desktop is gestopt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve"> + <source>Connections</source> + <target>Verbindingen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact allows" xml:space="preserve"> <source>Contact allows</source> <target>Contact maakt het mogelijk</target> @@ -1318,6 +1997,11 @@ Dit is uw eigen eenmalige link!</target> <target>Contact bestaat al</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact deleted!" xml:space="preserve"> + <source>Contact deleted!</source> + <target>Contact verwijderd!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> <target>Contact verborgen:</target> @@ -1328,9 +2012,9 @@ Dit is uw eigen eenmalige link!</target> <target>Contact is verbonden</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve"> - <source>Contact is not connected yet!</source> - <target>Contact is nog niet verbonden!</target> + <trans-unit id="Contact is deleted." xml:space="preserve"> + <source>Contact is deleted.</source> + <target>Contact is verwijderd.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact name" xml:space="preserve"> @@ -1343,6 +2027,11 @@ Dit is uw eigen eenmalige link!</target> <target>Contact voorkeuren</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve"> + <source>Contact will be deleted - this cannot be undone!</source> + <target>Het contact wordt verwijderd. Dit kan niet ongedaan worden gemaakt!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> <target>Contacten</target> @@ -1353,21 +2042,41 @@ Dit is uw eigen eenmalige link!</target> <target>Contact personen kunnen berichten markeren voor verwijdering; u kunt ze wel bekijken.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve"> + <source>Content violates conditions of use</source> + <target>Inhoud schendt de gebruiksvoorwaarden</target> + <note>blocking reason</note> + </trans-unit> <trans-unit id="Continue" xml:space="preserve"> <source>Continue</source> <target>Doorgaan</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve"> + <source>Conversation deleted!</source> + <target>Gesprek verwijderd!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> <target>Kopiëren</target> - <note>chat item action</note> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve"> + <source>Copy error</source> + <target>Kopieerfout</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> <target>Core versie: v% @</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Corner" xml:space="preserve"> + <source>Corner</source> + <target>Hoek</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Correct name to %@?" xml:space="preserve"> <source>Correct name to %@?</source> <target>Juiste naam voor %@?</target> @@ -1378,6 +2087,11 @@ Dit is uw eigen eenmalige link!</target> <target>Maak</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve"> + <source>Create 1-time link</source> + <target>Eenmalige link maken</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> <target>Maak een SimpleX adres aan</target> @@ -1388,11 +2102,6 @@ Dit is uw eigen eenmalige link!</target> <target>Maak een groep met een willekeurig profiel.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> - <source>Create an address to let people connect with you.</source> - <target>Maak een adres aan zodat mensen contact met je kunnen opnemen.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create file" xml:space="preserve"> <source>Create file</source> <target>Bestand maken</target> @@ -1413,16 +2122,16 @@ Dit is uw eigen eenmalige link!</target> <target>Maak link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create list" xml:space="preserve"> + <source>Create list</source> + <target>Maak een lijst</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> <target>Maak een nieuw profiel aan in [desktop-app](https://simplex.chat/downloads/). 💻</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve"> - <source>Create one-time invitation link</source> - <target>Maak een eenmalige uitnodiging link</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create profile" xml:space="preserve"> <source>Create profile</source> <target>Maak een profiel aan</target> @@ -1443,9 +2152,29 @@ Dit is uw eigen eenmalige link!</target> <target>Maak je profiel aan</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve"> - <source>Created on %@</source> - <target>Gemaakt op %@</target> + <trans-unit id="Created" xml:space="preserve"> + <source>Created</source> + <target>Gemaakt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve"> + <source>Created at</source> + <target>Gemaakt op</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve"> + <source>Created at: %@</source> + <target>Aangemaakt op: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve"> + <source>Creating archive link</source> + <target>Archief link maken</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve"> + <source>Creating link…</source> + <target>Link maken…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current Passcode" xml:space="preserve"> @@ -1453,11 +2182,21 @@ Dit is uw eigen eenmalige link!</target> <target>Huidige toegangscode</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <target>De tekst van de huidige voorwaarden kon niet worden geladen. U kunt de voorwaarden bekijken via deze link:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> <target>Huidige wachtwoord…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current profile" xml:space="preserve"> + <source>Current profile</source> + <target>Huidig profiel</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> <target>De momenteel maximaal ondersteunde bestandsgrootte is %@.</target> @@ -1468,11 +2207,26 @@ Dit is uw eigen eenmalige link!</target> <target>Aangepaste tijd</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve"> + <source>Customizable message shape.</source> + <target>Aanpasbare berichtvorm.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve"> + <source>Customize theme</source> + <target>Thema aanpassen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> <target>Donker</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve"> + <source>Dark mode colors</source> + <target>Kleuren in donkere modus</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> <target>Database-ID</target> @@ -1544,7 +2298,7 @@ Dit is uw eigen eenmalige link!</target> </trans-unit> <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> <source>Database passphrase is required to open chat.</source> - <target>Database wachtwoord is vereist om je gesprekken te openen.</target> + <target>Database wachtwoord is vereist om je chats te openen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database upgrade" xml:space="preserve"> @@ -1571,6 +2325,11 @@ Dit is uw eigen eenmalige link!</target> <target>De database wordt gemigreerd wanneer de app opnieuw wordt opgestart</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Debug delivery" xml:space="preserve"> + <source>Debug delivery</source> + <target>Foutopsporing bezorging</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> <target>Gedecentraliseerd</target> @@ -1584,18 +2343,19 @@ Dit is uw eigen eenmalige link!</target> <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> <target>Verwijderen</target> - <note>chat item action</note> + <note>alert action +swipe action</note> + </trans-unit> + <trans-unit id="Delete %lld messages of members?" xml:space="preserve"> + <source>Delete %lld messages of members?</source> + <target>%lld berichten van leden verwijderen?</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete %lld messages?" xml:space="preserve"> <source>Delete %lld messages?</source> <target>%lld berichten verwijderen?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve"> - <source>Delete Contact</source> - <target>Verwijder contact</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Delete address" xml:space="preserve"> <source>Delete address</source> <target>Adres verwijderen</target> @@ -1618,27 +2378,32 @@ Dit is uw eigen eenmalige link!</target> </trans-unit> <trans-unit id="Delete and notify contact" xml:space="preserve"> <source>Delete and notify contact</source> - <target>Contact verwijderen en op de hoogte stellen</target> + <target>Verwijderen en contact op de hoogte stellen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve"> - <source>Delete archive</source> - <target>Archief verwijderen</target> + <trans-unit id="Delete chat" xml:space="preserve"> + <source>Delete chat</source> + <target>Chat verwijderen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve"> - <source>Delete chat archive?</source> - <target>Chat archief verwijderen?</target> + <trans-unit id="Delete chat messages from your device." xml:space="preserve"> + <source>Delete chat messages from your device.</source> + <target>Verwijder chatberichten van uw apparaat.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile" xml:space="preserve"> <source>Delete chat profile</source> - <target>Chat profiel verwijderen</target> + <target>Chatprofiel verwijderen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile?" xml:space="preserve"> <source>Delete chat profile?</source> - <target>Chat profiel verwijderen?</target> + <target>Chatprofiel verwijderen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete chat?" xml:space="preserve"> + <source>Delete chat?</source> + <target>Chat verwijderen?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> @@ -1651,11 +2416,9 @@ Dit is uw eigen eenmalige link!</target> <target>Verwijder contact</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact? This cannot be undone!" xml:space="preserve"> - <source>Delete contact? -This cannot be undone!</source> - <target>Verwijder contact? -Dit kan niet ongedaan gemaakt worden!</target> + <trans-unit id="Delete contact?" xml:space="preserve"> + <source>Delete contact?</source> + <target>Verwijder contact?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete database" xml:space="preserve"> @@ -1663,6 +2426,11 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Database verwijderen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete database from this device" xml:space="preserve"> + <source>Delete database from this device</source> + <target>Verwijder de database van dit apparaat</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete file" xml:space="preserve"> <source>Delete file</source> <target>Verwijder bestand</target> @@ -1675,7 +2443,7 @@ Dit kan niet ongedaan gemaakt worden!</target> </trans-unit> <trans-unit id="Delete files for all chat profiles" xml:space="preserve"> <source>Delete files for all chat profiles</source> - <target>Verwijder bestanden voor alle chat profielen</target> + <target>Verwijder bestanden voor alle chatprofielen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete for everyone" xml:space="preserve"> @@ -1713,6 +2481,11 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Link verwijderen?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete list?" xml:space="preserve"> + <source>Delete list?</source> + <target>Lijst verwijderen?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> <target>Bericht van lid verwijderen?</target> @@ -1726,7 +2499,7 @@ Dit kan niet ongedaan gemaakt worden!</target> <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> <target>Verwijder berichten</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> @@ -1743,9 +2516,9 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Oude database verwijderen?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve"> - <source>Delete pending connection</source> - <target>Wachtende verbinding verwijderen</target> + <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> + <source>Delete or moderate up to 200 messages.</source> + <target>Maximaal 200 berichten verwijderen of modereren.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection?" xml:space="preserve"> @@ -1763,11 +2536,31 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Wachtrij verwijderen</target> <note>server test step</note> </trans-unit> + <trans-unit id="Delete report" xml:space="preserve"> + <source>Delete report</source> + <target>Rapport verwijderen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete up to 20 messages at once." xml:space="preserve"> + <source>Delete up to 20 messages at once.</source> + <target>Verwijder maximaal 20 berichten tegelijk.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> <target>Gebruikers profiel verwijderen?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete without notification" xml:space="preserve"> + <source>Delete without notification</source> + <target>Verwijderen zonder melding</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve"> + <source>Deleted</source> + <target>Verwijderd</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Deleted at" xml:space="preserve"> <source>Deleted at</source> <target>Verwijderd om</target> @@ -1778,6 +2571,16 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Verwijderd om: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve"> + <source>Deletion errors</source> + <target>Verwijderingsfouten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivered even when Apple drops them." xml:space="preserve"> + <source>Delivered even when Apple drops them.</source> + <target>Geleverd ook als Apple ze verliest</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery" xml:space="preserve"> <source>Delivery</source> <target>Bezorging</target> @@ -1813,11 +2616,41 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Desktop apparaten</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve"> + <source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source> + <target>Het bestemmingsserveradres van %@ is niet compatibel met de doorstuurserverinstellingen %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server error: %@" xml:space="preserve"> + <source>Destination server error: %@</source> + <target>Bestemmingsserverfout: %@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve"> + <source>Destination server version of %@ is incompatible with forwarding server %@.</source> + <target>De versie van de bestemmingsserver %@ is niet compatibel met de doorstuurserver %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Detailed statistics" xml:space="preserve"> + <source>Detailed statistics</source> + <target>Gedetailleerde statistieken</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Details" xml:space="preserve"> + <source>Details</source> + <target>Details</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Develop" xml:space="preserve"> <source>Develop</source> <target>Ontwikkelen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Developer options" xml:space="preserve"> + <source>Developer options</source> + <target>Ontwikkelaars opties</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> <target>Ontwikkel gereedschap</target> @@ -1848,9 +2681,14 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Directe berichten</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> - <target>Directe berichten tussen leden zijn verboden in deze groep.</target> + <trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve"> + <source>Direct messages between members are prohibited in this chat.</source> + <target>Directe berichten tussen leden zijn in deze chat niet toegestaan.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> + <target>Directe berichten tussen leden zijn niet toegestaan.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disable (keep overrides)" xml:space="preserve"> @@ -1863,11 +2701,26 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>SimpleX Vergrendelen uitschakelen</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Disable automatic message deletion?" xml:space="preserve"> + <source>Disable automatic message deletion?</source> + <target>Automatisch verwijderen van berichten uitschakelen?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Disable delete messages" xml:space="preserve"> + <source>Disable delete messages</source> + <target>Berichten verwijderen uitschakelen</target> + <note>alert button</note> + </trans-unit> <trans-unit id="Disable for all" xml:space="preserve"> <source>Disable for all</source> <target>Uitschakelen voor iedereen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Disabled" xml:space="preserve"> + <source>Disabled</source> + <target>Uitgeschakeld</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> <source>Disappearing message</source> <target>Verdwijnend bericht</target> @@ -1880,12 +2733,12 @@ Dit kan niet ongedaan gemaakt worden!</target> </trans-unit> <trans-unit id="Disappearing messages are prohibited in this chat." xml:space="preserve"> <source>Disappearing messages are prohibited in this chat.</source> - <target>Verdwijnende berichten zijn verboden in dit gesprek.</target> + <target>Verdwijnende berichten zijn niet toegestaan in dit gesprek.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> - <target>Verdwijnende berichten zijn verboden in deze groep.</target> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> + <target>Verdwijnende berichten zijn niet toegestaan.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disappears at" xml:space="preserve"> @@ -1918,16 +2771,41 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Ontdek via het lokale netwerk</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve"> + <source>Do NOT send messages directly, even if your or destination server does not support private routing.</source> + <target>Stuur GEEN berichten rechtstreeks, zelfs als uw of de bestemmingsserver geen privéroutering ondersteunt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> <source>Do NOT use SimpleX for emergency calls.</source> <target>Gebruik SimpleX NIET voor noodoproepen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT use private routing." xml:space="preserve"> + <source>Do NOT use private routing.</source> + <target>Gebruik GEEN privéroutering.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do it later" xml:space="preserve"> <source>Do it later</source> <target>Doe het later</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do not send history to new members." xml:space="preserve"> + <source>Do not send history to new members.</source> + <target>Stuur geen geschiedenis naar nieuwe leden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not use credentials with proxy." xml:space="preserve"> + <source>Do not use credentials with proxy.</source> + <target>Gebruik geen inloggegevens met proxy.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Documents:" xml:space="preserve"> + <source>Documents:</source> + <target>Documenten:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> <target>Maak geen adres aan</target> @@ -1938,21 +2816,72 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Niet inschakelen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Don't miss important messages." xml:space="preserve"> + <source>Don't miss important messages.</source> + <target>Mis geen belangrijke berichten.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> <target>Niet meer weergeven</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Done" xml:space="preserve"> + <source>Done</source> + <target>Klaar</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Downgrade and open chat" xml:space="preserve"> <source>Downgrade and open chat</source> <target>Downgraden en chat openen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Download" xml:space="preserve"> + <source>Download</source> + <target>Downloaden</target> + <note>alert button +chat item action</note> + </trans-unit> + <trans-unit id="Download errors" xml:space="preserve"> + <source>Download errors</source> + <target>Downloadfouten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download failed" xml:space="preserve"> + <source>Download failed</source> + <target>Download mislukt</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Download file" xml:space="preserve"> <source>Download file</source> <target>Download bestand</target> <note>server test step</note> </trans-unit> + <trans-unit id="Download files" xml:space="preserve"> + <source>Download files</source> + <target>‐Bestanden downloaden</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Downloaded" xml:space="preserve"> + <source>Downloaded</source> + <target>Gedownload</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloaded files" xml:space="preserve"> + <source>Downloaded files</source> + <target>Gedownloade bestanden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading archive" xml:space="preserve"> + <source>Downloading archive</source> + <target>Archief downloaden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading link details" xml:space="preserve"> + <source>Downloading link details</source> + <target>Link gegevens downloaden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> <target>Dubbele weergavenaam!</target> @@ -1963,6 +2892,11 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Duur</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="E2E encrypted notifications." xml:space="preserve"> + <source>E2E encrypted notifications.</source> + <target>E2E versleutelde meldingen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> <target>Bewerk</target> @@ -1983,6 +2917,11 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Inschakelen (overschrijvingen behouden)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve"> + <source>Enable Flux in Network & servers settings for better metadata privacy.</source> + <target>Schakel Flux in bij Netwerk- en serverinstellingen voor betere privacy van metagegevens.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> <target>SimpleX Vergrendelen inschakelen</target> @@ -1996,6 +2935,11 @@ Dit kan niet ongedaan gemaakt worden!</target> <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> <target>Automatisch verwijderen van berichten aanzetten?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Enable camera access" xml:space="preserve"> + <source>Enable camera access</source> + <target>Schakel cameratoegang in</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable for all" xml:space="preserve"> @@ -2003,6 +2947,11 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Inschakelen voor iedereen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve"> + <source>Enable in direct chats (BETA)!</source> + <target>Activeer in directe chats (BETA)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> <target>Onmiddellijke meldingen inschakelen?</target> @@ -2033,6 +2982,16 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Zelfvernietigings wachtwoord inschakelen</target> <note>set passcode view</note> </trans-unit> + <trans-unit id="Enabled" xml:space="preserve"> + <source>Enabled</source> + <target>Ingeschakeld</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve"> + <source>Enabled for</source> + <target>Ingeschakeld voor</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> <target>Versleutelen</target> @@ -2063,6 +3022,11 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Versleuteld bericht of een andere gebeurtenis</target> <note>notification</note> </trans-unit> + <trans-unit id="Encrypted message: app is stopped" xml:space="preserve"> + <source>Encrypted message: app is stopped</source> + <target>Versleuteld bericht: app is gestopt</target> + <note>notification</note> + </trans-unit> <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> <target>Versleuteld bericht: database fout</target> @@ -2098,6 +3062,11 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Opnieuw onderhandelen over de codering is mislukt.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encryption renegotiation in progress." xml:space="preserve"> + <source>Encryption renegotiation in progress.</source> + <target>Er wordt opnieuw onderhandeld over de encryptie.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter Passcode" xml:space="preserve"> <source>Enter Passcode</source> <target>Voer toegangscode in</target> @@ -2113,6 +3082,11 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Groep naam invoeren…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve"> + <source>Enter passphrase</source> + <target>Voer het wachtwoord in</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter passphrase…" xml:space="preserve"> <source>Enter passphrase…</source> <target>Voer wachtwoord in…</target> @@ -2135,12 +3109,12 @@ Dit kan niet ongedaan gemaakt worden!</target> </trans-unit> <trans-unit id="Enter welcome message…" xml:space="preserve"> <source>Enter welcome message…</source> - <target>Welkomst bericht invoeren…</target> + <target>Welkom bericht invoeren…</target> <note>placeholder</note> </trans-unit> <trans-unit id="Enter welcome message… (optional)" xml:space="preserve"> <source>Enter welcome message… (optional)</source> - <target>Voer welkomst bericht in... (optioneel)</target> + <target>Voer welkom bericht in... (optioneel)</target> <note>placeholder</note> </trans-unit> <trans-unit id="Enter your name…" xml:space="preserve"> @@ -2158,26 +3132,36 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Fout bij het afbreken van adres wijziging</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error accepting conditions" xml:space="preserve"> + <source>Error accepting conditions</source> + <target>Fout bij het accepteren van voorwaarden</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> <target>Fout bij het accepteren van een contactverzoek</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve"> - <source>Error accessing database file</source> - <target>Fout bij toegang tot database bestand</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> <target>Fout bij het toevoegen van leden</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error adding server" xml:space="preserve"> + <source>Error adding server</source> + <target>Fout bij toevoegen server</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> <target>Fout bij wijzigen van adres</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing connection profile" xml:space="preserve"> + <source>Error changing connection profile</source> + <target>Fout bij wijzigen van verbindingsprofiel</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error changing role" xml:space="preserve"> <source>Error changing role</source> <target>Fout bij wisselen van rol</target> @@ -2188,6 +3172,21 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Fout bij wijzigen van instelling</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing to incognito!" xml:space="preserve"> + <source>Error changing to incognito!</source> + <target>Fout bij het overschakelen naar incognito!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error checking token status" xml:space="preserve"> + <source>Error checking token status</source> + <target>Fout bij het controleren van de tokenstatus</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve"> + <source>Error connecting to forwarding server %@. Please try later.</source> + <target>Fout bij het verbinden met doorstuurserver %@. Probeer het later opnieuw.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> <target>Fout bij aanmaken van adres</target> @@ -2203,16 +3202,31 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Fout bij maken van groep link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating list" xml:space="preserve"> + <source>Error creating list</source> + <target>Fout bij het aanmaken van de lijst</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error creating member contact" xml:space="preserve"> <source>Error creating member contact</source> <target>Fout bij aanmaken contact</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating message" xml:space="preserve"> + <source>Error creating message</source> + <target>Fout bij het maken van een bericht</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>Fout bij aanmaken van profiel!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating report" xml:space="preserve"> + <source>Error creating report</source> + <target>Fout bij het rapporteren</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error decrypting file" xml:space="preserve"> <source>Error decrypting file</source> <target>Fout bij het ontsleutelen van bestand</target> @@ -2233,11 +3247,6 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Fout bij verwijderen van verbinding</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve"> - <source>Error deleting contact</source> - <target>Fout bij het verwijderen van contact</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> <target>Fout bij het verwijderen van de database</target> @@ -2258,6 +3267,11 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Fout bij het verwijderen van gebruikers profiel</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error downloading the archive" xml:space="preserve"> + <source>Error downloading the archive</source> + <target>Fout bij het downloaden van het archief</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> <source>Error enabling delivery receipts!</source> <target>Fout bij het inschakelen van ontvangst bevestiging!</target> @@ -2278,6 +3292,11 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Fout bij het exporteren van de chat database</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error exporting theme: %@" xml:space="preserve"> + <source>Error exporting theme: %@</source> + <target>Fout bij exporteren van thema: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> <target>Fout bij het importeren van de chat database</target> @@ -2288,24 +3307,54 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Fout bij lid worden van groep</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve"> - <source>Error loading %@ servers</source> - <target>Fout bij het laden van %@ servers</target> + <trans-unit id="Error loading servers" xml:space="preserve"> + <source>Error loading servers</source> + <target>Fout bij het laden van servers</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error migrating settings" xml:space="preserve"> + <source>Error migrating settings</source> + <target>Fout bij migreren van instellingen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error opening chat" xml:space="preserve"> + <source>Error opening chat</source> + <target>Fout bij het openen van de chat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> <target>Fout bij ontvangen van bestand</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error reconnecting server" xml:space="preserve"> + <source>Error reconnecting server</source> + <target>Fout bij opnieuw verbinding maken met de server</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error reconnecting servers" xml:space="preserve"> + <source>Error reconnecting servers</source> + <target>Fout bij opnieuw verbinden van servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error registering for notifications" xml:space="preserve"> + <source>Error registering for notifications</source> + <target>Fout bij registreren voor meldingen</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> <target>Fout bij verwijderen van lid</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve"> - <source>Error saving %@ servers</source> - <target>Fout bij opslaan van %@ servers</target> + <trans-unit id="Error reordering lists" xml:space="preserve"> + <source>Error reordering lists</source> + <target>Fout bij het opnieuw ordenen van lijsten</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error resetting statistics" xml:space="preserve"> + <source>Error resetting statistics</source> + <target>Fout bij het resetten van statistieken</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving ICE servers" xml:space="preserve"> @@ -2313,6 +3362,11 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Fout bij opslaan van ICE servers</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving chat list" xml:space="preserve"> + <source>Error saving chat list</source> + <target>Fout bij het opslaan van chatlijst</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> <target>Fout bij opslaan van groep profiel</target> @@ -2328,11 +3382,26 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Fout bij opslaan van wachtwoord in de keychain</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving servers" xml:space="preserve"> + <source>Error saving servers</source> + <target>Fout bij het opslaan van servers</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error saving settings" xml:space="preserve"> + <source>Error saving settings</source> + <target>Fout bij opslaan van instellingen</target> + <note>when migrating</note> + </trans-unit> <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> <target>Fout bij opslaan gebruikers wachtwoord</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error scanning code: %@" xml:space="preserve"> + <source>Error scanning code: %@</source> + <target>Fout bij het scannen van code: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending email" xml:space="preserve"> <source>Error sending email</source> <target>Fout bij het verzenden van e-mail</target> @@ -2363,16 +3432,26 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Fout bij het stoppen van de chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error switching profile" xml:space="preserve"> + <source>Error switching profile</source> + <target>Fout bij wisselen van profiel</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> <target>Fout bij wisselen van profiel!</target> - <note>No comment provided by engineer.</note> + <note>alertTitle</note> </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> <target>Fout bij het synchroniseren van de verbinding</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error testing server connection" xml:space="preserve"> + <source>Error testing server connection</source> + <target>Fout bij het testen van de serververbinding</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> <target>Fout bij bijwerken van groep link</target> @@ -2383,6 +3462,11 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Fout bij updaten van bericht</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error updating server" xml:space="preserve"> + <source>Error updating server</source> + <target>Fout bij het updaten van de server</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> <target>Fout bij bijwerken van instellingen</target> @@ -2393,6 +3477,16 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Fout bij updaten van gebruikers privacy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error uploading the archive" xml:space="preserve"> + <source>Error uploading the archive</source> + <target>Fout bij het uploaden van het archief</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error verifying passphrase:" xml:space="preserve"> + <source>Error verifying passphrase:</source> + <target>Fout bij het verifiëren van het wachtwoord:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error: " xml:space="preserve"> <source>Error: </source> <target>Fout: </target> @@ -2401,7 +3495,9 @@ Dit kan niet ongedaan gemaakt worden!</target> <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> <target>Fout: %@</target> - <note>No comment provided by engineer.</note> + <note>alert message +file error text +snd error text</note> </trans-unit> <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> @@ -2413,6 +3509,16 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Fout: geen database bestand</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Errors" xml:space="preserve"> + <source>Errors</source> + <target>Fouten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors in servers configuration." xml:space="preserve"> + <source>Errors in servers configuration.</source> + <target>Fouten in de serverconfiguratie.</target> + <note>servers error</note> + </trans-unit> <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> <source>Even when disabled in the conversation.</source> <target>Zelfs wanneer uitgeschakeld in het gesprek.</target> @@ -2425,9 +3531,14 @@ Dit kan niet ongedaan gemaakt worden!</target> </trans-unit> <trans-unit id="Expand" xml:space="preserve"> <source>Expand</source> - <target>Uitbreiden</target> + <target>Uitklappen</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Expired" xml:space="preserve"> + <source>Expired</source> + <target>Verlopen</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> <target>Database exporteren</target> @@ -2438,11 +3549,21 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Exportfout:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Export theme" xml:space="preserve"> + <source>Export theme</source> + <target>Exporteer thema</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> <target>Geëxporteerd database archief.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Exported file doesn't exist" xml:space="preserve"> + <source>Exported file doesn't exist</source> + <target>Geëxporteerd bestand bestaat niet</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exporting database archive…" xml:space="preserve"> <source>Exporting database archive…</source> <target>Database archief exporteren…</target> @@ -2458,16 +3579,70 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Snel en niet wachten tot de afzender online is!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster deletion of groups." xml:space="preserve"> + <source>Faster deletion of groups.</source> + <target>Sneller verwijderen van groepen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Faster joining and more reliable messages." xml:space="preserve"> <source>Faster joining and more reliable messages.</source> <target>Snellere deelname en betrouwbaardere berichten.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster sending messages." xml:space="preserve"> + <source>Faster sending messages.</source> + <target>Sneller verzenden van berichten.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Favorite" xml:space="preserve"> <source>Favorite</source> <target>Favoriet</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Favorites" xml:space="preserve"> + <source>Favorites</source> + <target>Favorieten</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Bestandsfout</target> + <note>file error alert title</note> + </trans-unit> + <trans-unit id="File errors: %@" xml:space="preserve"> + <source>File errors: +%@</source> + <target>Bestandsfouten: +%@</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="File is blocked by server operator: %@." xml:space="preserve"> + <source>File is blocked by server operator: +%@.</source> + <target>Bestand is geblokkeerd door serveroperator: +%@.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve"> + <source>File not found - most likely file was deleted or cancelled.</source> + <target>Bestand niet gevonden - hoogstwaarschijnlijk is het bestand verwijderd of geannuleerd.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File server error: %@" xml:space="preserve"> + <source>File server error: %@</source> + <target>Bestandsserverfout: %@</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File status" xml:space="preserve"> + <source>File status</source> + <target>Bestandsstatus</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File status: %@" xml:space="preserve"> + <source>File status: %@</source> + <target>Bestandsstatus: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="File will be deleted from servers." xml:space="preserve"> <source>File will be deleted from servers.</source> <target>Het bestand wordt van de servers verwijderd.</target> @@ -2488,6 +3663,11 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Bestand: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files" xml:space="preserve"> + <source>Files</source> + <target>Bestanden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> <target>Bestanden en media</target> @@ -2498,14 +3678,19 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Bestanden en media</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> - <source>Files and media are prohibited in this group.</source> - <target>Bestanden en media zijn verboden in deze groep.</target> + <trans-unit id="Files and media are prohibited." xml:space="preserve"> + <source>Files and media are prohibited.</source> + <target>Bestanden en media zijn niet toegestaan.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Files and media not allowed" xml:space="preserve"> + <source>Files and media not allowed</source> + <target>Bestanden en media niet toegestaan</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Files and media prohibited!" xml:space="preserve"> <source>Files and media prohibited!</source> - <target>Bestanden en media verboden!</target> + <target>Bestanden en media niet toegestaan!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Filter unread and favorite chats." xml:space="preserve"> @@ -2513,6 +3698,16 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Filter ongelezen en favoriete chats.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Finalize migration" xml:space="preserve"> + <source>Finalize migration</source> + <target>Voltooi de migratie</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration on another device." xml:space="preserve"> + <source>Finalize migration on another device.</source> + <target>Voltooi de migratie op een ander apparaat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> <target>Eindelijk, we hebben ze! 🚀</target> @@ -2520,7 +3715,7 @@ Dit kan niet ongedaan gemaakt worden!</target> </trans-unit> <trans-unit id="Find chats faster" xml:space="preserve"> <source>Find chats faster</source> - <target>Vind gesprekken sneller</target> + <target>Vind chats sneller</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix" xml:space="preserve"> @@ -2553,11 +3748,115 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Herstel wordt niet ondersteund door groepslid</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For all moderators" xml:space="preserve"> + <source>For all moderators</source> + <target>Voor alle moderators</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For chat profile %@:" xml:space="preserve"> + <source>For chat profile %@:</source> + <target>Voor chatprofiel %@:</target> + <note>servers error</note> + </trans-unit> <trans-unit id="For console" xml:space="preserve"> <source>For console</source> <target>Voor console</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." xml:space="preserve"> + <source>For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</source> + <target>Als uw contactpersoon bijvoorbeeld berichten ontvangt via een SimpleX Chat-server, worden deze door uw app via een Flux-server verzonden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For me" xml:space="preserve"> + <source>For me</source> + <target>Voor mij</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For private routing" xml:space="preserve"> + <source>For private routing</source> + <target>Voor privé-routering</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For social media" xml:space="preserve"> + <source>For social media</source> + <target>Voor social media</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward" xml:space="preserve"> + <source>Forward</source> + <target>Doorsturen</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Forward %d message(s)?" xml:space="preserve"> + <source>Forward %d message(s)?</source> + <target>%d bericht(en) doorsturen?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Forward and save messages" xml:space="preserve"> + <source>Forward and save messages</source> + <target>Berichten doorsturen en opslaan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward messages" xml:space="preserve"> + <source>Forward messages</source> + <target>Berichten doorsturen</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Forward messages without files?" xml:space="preserve"> + <source>Forward messages without files?</source> + <target>Berichten doorsturen zonder bestanden?</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> + <source>Forward up to 20 messages at once.</source> + <target>Stuur maximaal 20 berichten tegelijk door.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded" xml:space="preserve"> + <source>Forwarded</source> + <target>Doorgestuurd</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded from" xml:space="preserve"> + <source>Forwarded from</source> + <target>Doorgestuurd vanuit</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding %lld messages" xml:space="preserve"> + <source>Forwarding %lld messages</source> + <target>%lld berichten doorsturen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve"> + <source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source> + <target>De doorstuurserver %@ kon geen verbinding maken met de bestemmingsserver %@. Probeer het later opnieuw.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server address is incompatible with network settings: %@.</source> + <target>Het adres van de doorstuurserver is niet compatibel met de netwerkinstellingen: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server version is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server version is incompatible with network settings: %@.</source> + <target>De doorstuurserverversie is niet compatibel met de netwerkinstellingen: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Destination server error: %2$@</source> + <target>Doorstuurserver: %1$@ +Bestemmingsserverfout: %2$@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Error: %2$@</source> + <target>Doorstuurserver: %1$@ +Fout: %2$@</target> + <note>snd error text</note> + </trans-unit> <trans-unit id="Found desktop" xml:space="preserve"> <source>Found desktop</source> <target>Desktop gevonden</target> @@ -2578,11 +3877,6 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Volledige naam (optioneel)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full name:" xml:space="preserve"> - <source>Full name:</source> - <target>Volledige naam:</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Fully decentralized – visible only to members." xml:space="preserve"> <source>Fully decentralized – visible only to members.</source> <target>Volledig gedecentraliseerd – alleen zichtbaar voor leden.</target> @@ -2603,6 +3897,21 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>GIF's en stickers</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Get notified when mentioned." xml:space="preserve"> + <source>Get notified when mentioned.</source> + <target>Ontvang een melding als u vermeld wordt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Good afternoon!" xml:space="preserve"> + <source>Good afternoon!</source> + <target>Goedemiddag!</target> + <note>message preview</note> + </trans-unit> + <trans-unit id="Good morning!" xml:space="preserve"> + <source>Good morning!</source> + <target>Goedemorgen!</target> + <note>message preview</note> + </trans-unit> <trans-unit id="Group" xml:space="preserve"> <source>Group</source> <target>Groep</target> @@ -2658,36 +3967,6 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Groep links</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve"> - <source>Group members can add message reactions.</source> - <target>Groepsleden kunnen berichtreacties toevoegen.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> - <source>Group members can irreversibly delete sent messages.</source> - <target>Groepsleden kunnen verzonden berichten onherroepelijk verwijderen.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> - <target>Groepsleden kunnen directe berichten sturen.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> - <target>Groepsleden kunnen verdwijnende berichten sturen.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send files and media." xml:space="preserve"> - <source>Group members can send files and media.</source> - <target>Groepsleden kunnen bestanden en media verzenden.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> - <target>Groepsleden kunnen spraak berichten verzenden.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> <target>Groep bericht:</target> @@ -2715,7 +3994,7 @@ Dit kan niet ongedaan gemaakt worden!</target> </trans-unit> <trans-unit id="Group welcome message" xml:space="preserve"> <source>Group welcome message</source> - <target>Groep welkomst bericht</target> + <target>Groep welkom bericht</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group will be deleted for all members - this cannot be undone!" xml:space="preserve"> @@ -2728,11 +4007,21 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>De groep wordt voor u verwijderd, dit kan niet ongedaan worden gemaakt!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Groups" xml:space="preserve"> + <source>Groups</source> + <target>Groepen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Help" xml:space="preserve"> <source>Help</source> <target>Help</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Help admins moderating their groups." xml:space="preserve"> + <source>Help admins moderating their groups.</source> + <target>Help beheerders bij het modereren van hun groepen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> <target>Verborgen</target> @@ -2740,7 +4029,7 @@ Dit kan niet ongedaan gemaakt worden!</target> </trans-unit> <trans-unit id="Hidden chat profiles" xml:space="preserve"> <source>Hidden chat profiles</source> - <target>Verborgen chat profielen</target> + <target>Verborgen chatprofielen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hidden profile password" xml:space="preserve"> @@ -2773,15 +4062,30 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Geschiedenis</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="History is not sent to new members." xml:space="preserve"> + <source>History is not sent to new members.</source> + <target>Geschiedenis wordt niet naar nieuwe leden gestuurd.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> <target>Hoe SimpleX werkt</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="How it affects privacy" xml:space="preserve"> + <source>How it affects privacy</source> + <target>Hoe het de privacy beïnvloedt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it helps privacy" xml:space="preserve"> + <source>How it helps privacy</source> + <target>Hoe het de privacy helpt</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> <target>Hoe het werkt</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="How to" xml:space="preserve"> <source>How to</source> @@ -2798,24 +4102,29 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Hoe u uw servers gebruikt</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Hungarian interface" xml:space="preserve"> + <source>Hungarian interface</source> + <target>Hongaarse interface</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> <target>ICE servers (één per lijn)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="IP address" xml:space="preserve"> + <source>IP address</source> + <target>IP-adres</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> <source>If you can't meet in person, show QR code in a video call, or share the link.</source> <target>Als je elkaar niet persoonlijk kunt ontmoeten, laat dan de QR-code zien in een videogesprek of deel de link.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <target>Als u elkaar niet persoonlijk kunt ontmoeten, kunt u **de QR-code scannen in het video gesprek**, of uw contact kan een uitnodiging link delen.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> - <target>Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens onomkeerbaar verwijderd!</target> + <target>Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens definitief verwijderd!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="If you enter your self-destruct passcode while opening the app:" xml:space="preserve"> @@ -2848,8 +4157,8 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Onmiddellijk</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <target>Immuun voor spam en misbruik</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2868,6 +4177,33 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Database importeren</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Import failed" xml:space="preserve"> + <source>Import failed</source> + <target>Importeren is mislukt</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import theme" xml:space="preserve"> + <source>Import theme</source> + <target>Thema importeren</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Importing archive" xml:space="preserve"> + <source>Importing archive</source> + <target>Archief importeren</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved delivery, reduced traffic usage. More improvements are coming soon!" xml:space="preserve"> + <source>Improved delivery, reduced traffic usage. +More improvements are coming soon!</source> + <target>Verbeterde levering, minder data gebruik. +Binnenkort meer verbeteringen!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved message delivery" xml:space="preserve"> + <source>Improved message delivery</source> + <target>Verbeterde berichtbezorging</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> <target>Verbeterde privacy en veiligheid</target> @@ -2878,11 +4214,31 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Verbeterde serverconfiguratie</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In order to continue, chat should be stopped." xml:space="preserve"> + <source>In order to continue, chat should be stopped.</source> + <target>Om verder te kunnen gaan, moet de chat worden gestopt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>In antwoord op</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In-call sounds" xml:space="preserve"> + <source>In-call sounds</source> + <target>Geluiden tijdens het bellen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Inappropriate content" xml:space="preserve"> + <source>Inappropriate content</source> + <target>Ongepaste inhoud</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Inappropriate profile" xml:space="preserve"> + <source>Inappropriate profile</source> + <target>Ongepast profiel</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> <target>Incognito</target> @@ -2953,6 +4309,11 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Installeer [SimpleX Chat voor terminal](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Instant" xml:space="preserve"> + <source>Instant</source> + <target>Direct</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> @@ -2960,30 +4321,80 @@ Dit kan niet ongedaan gemaakt worden!</target> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Instantly" xml:space="preserve"> - <source>Instantly</source> - <target>Meteen</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> <target>Interface</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Interface colors" xml:space="preserve"> + <source>Interface colors</source> + <target>Interface kleuren</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid" xml:space="preserve"> + <source>Invalid</source> + <target>Ongeldig</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (bad token)" xml:space="preserve"> + <source>Invalid (bad token)</source> + <target>Ongeldig (ongeldig token)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (expired)" xml:space="preserve"> + <source>Invalid (expired)</source> + <target>Ongeldig (verlopen)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (unregistered)" xml:space="preserve"> + <source>Invalid (unregistered)</source> + <target>Ongeldig (niet geregistreerd)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (wrong topic)" xml:space="preserve"> + <source>Invalid (wrong topic)</source> + <target>Ongeldig (verkeerd onderwerp)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid QR code" xml:space="preserve"> + <source>Invalid QR code</source> + <target>Ongeldige QR-code</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> <target>Ongeldige verbinding link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid display name!" xml:space="preserve"> + <source>Invalid display name!</source> + <target>Ongeldige weergavenaam!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid link" xml:space="preserve"> + <source>Invalid link</source> + <target>Ongeldige link</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Ongeldige migratie bevestiging</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid name!" xml:space="preserve"> <source>Invalid name!</source> <target>Ongeldige naam!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid response" xml:space="preserve"> + <source>Invalid response</source> + <target>Ongeldig antwoord</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> <target>Ongeldig server adres!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Invalid status" xml:space="preserve"> <source>Invalid status</source> @@ -3005,6 +4416,11 @@ Dit kan niet ongedaan gemaakt worden!</target> <target>Nodig leden uit</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invite to chat" xml:space="preserve"> + <source>Invite to chat</source> + <target>Uitnodigen voor een chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invite to group" xml:space="preserve"> <source>Invite to group</source> <target>Uitnodigen voor groep</target> @@ -3017,17 +4433,17 @@ Dit kan niet ongedaan gemaakt worden!</target> </trans-unit> <trans-unit id="Irreversible message deletion is prohibited in this chat." xml:space="preserve"> <source>Irreversible message deletion is prohibited in this chat.</source> - <target>Het onomkeerbaar verwijderen van berichten is verboden in dit gesprek.</target> + <target>Het definitief verwijderen van berichten is niet toegestaan in dit gesprek.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> - <target>Het onomkeerbaar verwijderen van berichten is verboden in deze groep.</target> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> + <target>Het definitief verwijderen van berichten is verbHet definitief verwijderen van berichten is niet toegestaan..</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve"> <source>It allows having many anonymous connections without any shared data between them in a single chat profile.</source> - <target>Het maakt het mogelijk om veel anonieme verbindingen te hebben zonder enige gedeelde gegevens tussen hen in een enkel chat profiel.</target> + <target>Het maakt het mogelijk om veel anonieme verbindingen te hebben zonder enige gedeelde gegevens tussen hen in een enkel chatprofiel.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It can happen when you or your connection used the old database backup." xml:space="preserve"> @@ -3046,6 +4462,11 @@ Dit kan niet ongedaan gemaakt worden!</target> 3. De verbinding is verbroken.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="It protects your IP address and connections." xml:space="preserve"> + <source>It protects your IP address and connections.</source> + <target>Het beschermt uw IP-adres en verbindingen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> <target>Het lijkt erop dat u al bent verbonden via deze link. Als dit niet het geval is, is er een fout opgetreden (%@).</target> @@ -3063,14 +4484,19 @@ Dit kan niet ongedaan gemaakt worden!</target> </trans-unit> <trans-unit id="Join" xml:space="preserve"> <source>Join</source> - <target>Word lid van</target> - <note>No comment provided by engineer.</note> + <target>Word lid</target> + <note>swipe action</note> </trans-unit> <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> <target>Word lid van groep</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Join group conversations" xml:space="preserve"> + <source>Join group conversations</source> + <target>Neem deel aan groepsgesprekken</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Join group?" xml:space="preserve"> <source>Join group?</source> <target>Deelnemen aan groep?</target> @@ -3098,11 +4524,26 @@ Dit is jouw link voor groep %@!</target> <target>Deel nemen aan groep</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep" xml:space="preserve"> + <source>Keep</source> + <target>Bewaar</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Keep conversation" xml:space="preserve"> + <source>Keep conversation</source> + <target>Behoud het gesprek</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Keep the app open to use it from desktop" xml:space="preserve"> <source>Keep the app open to use it from desktop</source> <target>Houd de app geopend om deze vanaf de desktop te gebruiken</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep unused invitation?" xml:space="preserve"> + <source>Keep unused invitation?</source> + <target>Ongebruikte uitnodiging bewaren?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> <source>Keep your connections</source> <target>Behoud uw verbindingen</target> @@ -3136,6 +4577,16 @@ Dit is jouw link voor groep %@!</target> <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> <target>Verlaten</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Leave chat" xml:space="preserve"> + <source>Leave chat</source> + <target>Chat verlaten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave chat?" xml:space="preserve"> + <source>Leave chat?</source> + <target>Chat verlaten?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group" xml:space="preserve"> @@ -3178,6 +4629,21 @@ Dit is jouw link voor groep %@!</target> <target>Gelinkte desktops</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="List" xml:space="preserve"> + <source>List</source> + <target>Lijst</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="List name and emoji should be different for all lists." xml:space="preserve"> + <source>List name and emoji should be different for all lists.</source> + <target>De naam en emoji van de lijst moeten voor alle lijsten verschillend zijn.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List name..." xml:space="preserve"> + <source>List name...</source> + <target>Naam van lijst...</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Live message!" xml:space="preserve"> <source>Live message!</source> <target>Live bericht!</target> @@ -3208,11 +4674,6 @@ Dit is jouw link voor groep %@!</target> <target>Vergrendeling modus</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> - <source>Make a private connection</source> - <target>Maak een privéverbinding</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make one message disappear" xml:space="preserve"> <source>Make one message disappear</source> <target>Eén bericht laten verdwijnen</target> @@ -3223,21 +4684,11 @@ Dit is jouw link voor groep %@!</target> <target>Profiel privé maken!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> - <target>Zorg ervoor dat %@ server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn (%@).</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> <target>Zorg ervoor dat WebRTC ICE server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <target>Veel mensen vroegen: *als SimpleX geen gebruikers-ID's heeft, hoe kan het dan berichten bezorgen?*</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> <source>Mark deleted for everyone</source> <target>Markeer verwijderd voor iedereen</target> @@ -3263,11 +4714,36 @@ Dit is jouw link voor groep %@!</target> <target>Max 30 seconden, direct ontvangen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Media & file servers" xml:space="preserve"> + <source>Media & file servers</source> + <target>Media- en bestandsservers</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Medium" xml:space="preserve"> + <source>Medium</source> + <target>Medium</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Member" xml:space="preserve"> <source>Member</source> <target>Lid</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member inactive" xml:space="preserve"> + <source>Member inactive</source> + <target>Lid inactief</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Member reports" xml:space="preserve"> + <source>Member reports</source> + <target>Ledenrapporten</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All chat members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All chat members will be notified.</source> + <target>De rol van het lid wordt gewijzigd naar "%@". Alle chatleden worden op de hoogte gebracht.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> <target>De rol van lid wordt gewijzigd in "%@". Alle groepsleden worden op de hoogte gebracht.</target> @@ -3278,11 +4754,66 @@ Dit is jouw link voor groep %@!</target> <target>De rol van lid wordt gewijzigd in "%@". Het lid ontvangt een nieuwe uitnodiging.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from chat - this cannot be undone!</source> + <target>Lid wordt verwijderd uit de chat - dit kan niet ongedaan worden gemaakt!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> <target>Lid wordt uit de groep verwijderd, dit kan niet ongedaan worden gemaakt!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Members can add message reactions." xml:space="preserve"> + <source>Members can add message reactions.</source> + <target>Groepsleden kunnen bericht reacties toevoegen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Members can irreversibly delete sent messages. (24 hours)</source> + <target>Groepsleden kunnen verzonden berichten onherroepelijk verwijderen. (24 uur)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can report messsages to moderators." xml:space="preserve"> + <source>Members can report messsages to moderators.</source> + <target>Leden kunnen berichten melden bij moderators.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send SimpleX links." xml:space="preserve"> + <source>Members can send SimpleX links.</source> + <target>Groepsleden kunnen SimpleX-links verzenden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> + <target>Groepsleden kunnen directe berichten sturen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> + <target>Groepsleden kunnen verdwijnende berichten sturen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send files and media." xml:space="preserve"> + <source>Members can send files and media.</source> + <target>Groepsleden kunnen bestanden en media verzenden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> + <target>Groepsleden kunnen spraak berichten verzenden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mention members 👋" xml:space="preserve"> + <source>Mention members 👋</source> + <target>Vermeld leden 👋</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Menus" xml:space="preserve"> + <source>Menus</source> + <target>Menu's</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>Fout bij bezorging van bericht</target> @@ -3293,11 +4824,31 @@ Dit is jouw link voor groep %@!</target> <target>Ontvangst bevestiging voor berichten!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message delivery warning" xml:space="preserve"> + <source>Message delivery warning</source> + <target>Waarschuwing voor berichtbezorging</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> <target>Concept bericht</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message forwarded" xml:space="preserve"> + <source>Message forwarded</source> + <target>Bericht doorgestuurd</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message may be delivered later if member becomes active." xml:space="preserve"> + <source>Message may be delivered later if member becomes active.</source> + <target>Het bericht kan later worden bezorgd als het lid actief wordt.</target> + <note>item status description</note> + </trans-unit> + <trans-unit id="Message queue info" xml:space="preserve"> + <source>Message queue info</source> + <target>Informatie over berichtenwachtrij</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message reactions" xml:space="preserve"> <source>Message reactions</source> <target>Reacties op berichten</target> @@ -3305,19 +4856,54 @@ Dit is jouw link voor groep %@!</target> </trans-unit> <trans-unit id="Message reactions are prohibited in this chat." xml:space="preserve"> <source>Message reactions are prohibited in this chat.</source> - <target>Reacties op berichten zijn verboden in deze chat.</target> + <target>Reacties op berichten zijn niet toegestaan in deze chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> - <source>Message reactions are prohibited in this group.</source> - <target>Reacties op berichten zijn verboden in deze groep.</target> + <trans-unit id="Message reactions are prohibited." xml:space="preserve"> + <source>Message reactions are prohibited.</source> + <target>Reacties op berichten zijn niet toegestaan.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message reception" xml:space="preserve"> + <source>Message reception</source> + <target>Bericht ontvangst</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message servers" xml:space="preserve"> + <source>Message servers</source> + <target>Berichtservers</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message shape" xml:space="preserve"> + <source>Message shape</source> + <target>Berichtvorm</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message source remains private." xml:space="preserve"> + <source>Message source remains private.</source> + <target>Berichtbron blijft privé.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status" xml:space="preserve"> + <source>Message status</source> + <target>Berichtstatus</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status: %@" xml:space="preserve"> + <source>Message status: %@</source> + <target>Berichtstatus: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> <target>Bericht tekst</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message too large" xml:space="preserve"> + <source>Message too large</source> + <target>Bericht te groot</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> <target>Berichten</target> @@ -3333,11 +4919,76 @@ Dit is jouw link voor groep %@!</target> <target>Berichten van %@ worden getoond!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Messages in this chat will never be deleted." xml:space="preserve"> + <source>Messages in this chat will never be deleted.</source> + <target>Berichten in deze chat zullen nooit worden verwijderd.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages received" xml:space="preserve"> + <source>Messages received</source> + <target>Berichten ontvangen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages sent" xml:space="preserve"> + <source>Messages sent</source> + <target>Berichten verzonden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages were deleted after you selected them." xml:space="preserve"> + <source>Messages were deleted after you selected them.</source> + <target>Berichten zijn verwijderd nadat u ze had geselecteerd.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Berichten, bestanden en oproepen worden beschermd door **end-to-end codering** met perfecte voorwaartse geheimhouding, afwijzing en inbraakherstel.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Berichten, bestanden en oproepen worden beschermd door **kwantumbestendige e2e encryptie** met perfecte voorwaartse geheimhouding, afwijzing en inbraakherstel.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate device" xml:space="preserve"> + <source>Migrate device</source> + <target>Apparaat migreren</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate from another device" xml:space="preserve"> + <source>Migrate from another device</source> + <target>Migreer vanaf een ander apparaat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate here" xml:space="preserve"> + <source>Migrate here</source> + <target>Migreer hierheen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device" xml:space="preserve"> + <source>Migrate to another device</source> + <target>Migreer naar een ander apparaat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device via QR code." xml:space="preserve"> + <source>Migrate to another device via QR code.</source> + <target>Migreer naar een ander apparaat via QR-code.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating" xml:space="preserve"> + <source>Migrating</source> + <target>Migreren</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migrating database archive…" xml:space="preserve"> <source>Migrating database archive…</source> <target>Database archief migreren…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Migration complete" xml:space="preserve"> + <source>Migration complete</source> + <target>Migratie voltooid</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> <target>Migratiefout:</target> @@ -3345,7 +4996,7 @@ Dit is jouw link voor groep %@!</target> </trans-unit> <trans-unit id="Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." xml:space="preserve"> <source>Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat).</source> - <target>Migratie mislukt. Tik hieronder op **Overslaan** om de huidige database te blijven gebruiken. Meld het probleem aan de app-ontwikkelaars via chat of e-mail [chat@simplex.chat](mailto:chat@simplex.chat).</target> + <target>Migratie mislukt. Tik hier onder op **Overslaan** om de huidige database te blijven gebruiken. Meld het probleem aan de app-ontwikkelaars via chat of e-mail [chat@simplex.chat](mailto:chat@simplex.chat).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Migration is completed" xml:space="preserve"> @@ -3353,9 +5004,9 @@ Dit is jouw link voor groep %@!</target> <target>Migratie is voltooid</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> - <target>Migraties: %@</target> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> + <target>Migraties:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> @@ -3373,30 +5024,45 @@ Dit is jouw link voor groep %@!</target> <target>Gemodereerd op: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="More" xml:space="preserve"> + <source>More</source> + <target>Meer</target> + <note>swipe action</note> + </trans-unit> <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> <target>Meer verbeteringen volgen snel!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="More reliable network connection." xml:space="preserve"> + <source>More reliable network connection.</source> + <target>Betrouwbaardere netwerkverbinding.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="More reliable notifications" xml:space="preserve"> + <source>More reliable notifications</source> + <target>Betrouwbaardere meldingen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> <source>Most likely this connection is deleted.</source> <target>Hoogstwaarschijnlijk is deze verbinding verwijderd.</target> <note>item status description</note> </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> - <source>Most likely this contact has deleted the connection with you.</source> - <target>Hoogstwaarschijnlijk heeft dit contact de verbinding met jou verwijderd.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> - <target>Meerdere chat profielen</target> + <target>Meerdere chatprofielen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> <target>Dempen</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Mute all" xml:space="preserve"> + <source>Mute all</source> + <target>Alles dempen</target> + <note>notification label action</note> </trans-unit> <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> @@ -3406,13 +5072,38 @@ Dit is jouw link voor groep %@!</target> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> <target>Naam</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> <target>Netwerk & servers</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Network connection" xml:space="preserve"> + <source>Network connection</source> + <target>Netwerkverbinding</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network decentralization" xml:space="preserve"> + <source>Network decentralization</source> + <target>Netwerk decentralisatie</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve"> + <source>Network issues - message expired after many attempts to send it.</source> + <target>Netwerkproblemen - bericht is verlopen na vele pogingen om het te verzenden.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Network management" xml:space="preserve"> + <source>Network management</source> + <target>Netwerkbeheer</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network operator" xml:space="preserve"> + <source>Network operator</source> + <target>Netwerkbeheerder</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> <target>Netwerk instellingen</target> @@ -3423,11 +5114,36 @@ Dit is jouw link voor groep %@!</target> <target>Netwerk status</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New" xml:space="preserve"> + <source>New</source> + <target>Nieuw</target> + <note>token status text</note> + </trans-unit> <trans-unit id="New Passcode" xml:space="preserve"> <source>New Passcode</source> <target>Nieuwe toegangscode</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve"> + <source>New SOCKS credentials will be used every time you start the app.</source> + <target>Elke keer dat u de app start, worden er nieuwe SOCKS-inloggegevens gebruikt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve"> + <source>New SOCKS credentials will be used for each server.</source> + <target>Voor elke server worden nieuwe SOCKS-inloggegevens gebruikt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat" xml:space="preserve"> + <source>New chat</source> + <target>Nieuw gesprek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat experience 🎉" xml:space="preserve"> + <source>New chat experience 🎉</source> + <target>Nieuwe chatervaring 🎉</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> <target>Nieuw contactverzoek</target> @@ -3438,11 +5154,6 @@ Dit is jouw link voor groep %@!</target> <target>Nieuw contact:</target> <note>notification</note> </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> - <source>New database archive</source> - <target>Nieuw database archief</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="New desktop app!" xml:space="preserve"> <source>New desktop app!</source> <target>Nieuwe desktop app!</target> @@ -3453,11 +5164,21 @@ Dit is jouw link voor groep %@!</target> <target>Nieuwe weergavenaam</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>Nieuwe gebeurtenissen</target> + <note>notification</note> + </trans-unit> <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> <target>Nieuw in %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New media options" xml:space="preserve"> + <source>New media options</source> + <target>Nieuwe media-opties</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> <target>Nieuwe leden rol</target> @@ -3473,6 +5194,11 @@ Dit is jouw link voor groep %@!</target> <target>Nieuw wachtwoord…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New server" xml:space="preserve"> + <source>New server</source> + <target>Nieuwe server</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No" xml:space="preserve"> <source>No</source> <target>Nee</target> @@ -3483,6 +5209,21 @@ Dit is jouw link voor groep %@!</target> <target>Geen app wachtwoord</target> <note>Authentication unavailable</note> </trans-unit> + <trans-unit id="No chats" xml:space="preserve"> + <source>No chats</source> + <target>Geen chats</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats found" xml:space="preserve"> + <source>No chats found</source> + <target>Geen chats gevonden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats in list %@" xml:space="preserve"> + <source>No chats in list %@</source> + <target>Geen chats in lijst %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> <target>Geen contacten geselecteerd</target> @@ -3503,9 +5244,14 @@ Dit is jouw link voor groep %@!</target> <target>Geen apparaattoken!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No direct connection yet, message is forwarded by admin." xml:space="preserve"> + <source>No direct connection yet, message is forwarded by admin.</source> + <target>Nog geen directe verbinding, bericht wordt doorgestuurd door beheerder.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="No filtered chats" xml:space="preserve"> <source>No filtered chats</source> - <target>Geen gefilterde gesprekken</target> + <target>Geen gefilterde chats</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No group!" xml:space="preserve"> @@ -3518,21 +5264,111 @@ Dit is jouw link voor groep %@!</target> <target>Geen geschiedenis</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No info, try to reload" xml:space="preserve"> + <source>No info, try to reload</source> + <target>Geen info, probeer opnieuw te laden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No media & file servers." xml:space="preserve"> + <source>No media & file servers.</source> + <target>Geen media- en bestandsservers.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No message" xml:space="preserve"> + <source>No message</source> + <target>Geen bericht</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No message servers." xml:space="preserve"> + <source>No message servers.</source> + <target>Geen berichtenservers.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No network connection" xml:space="preserve"> + <source>No network connection</source> + <target>Geen netwerkverbinding</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record speech" xml:space="preserve"> + <source>No permission to record speech</source> + <target>Geen toestemming om spraak op te nemen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record video" xml:space="preserve"> + <source>No permission to record video</source> + <target>Geen toestemming om video op te nemen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> <source>No permission to record voice message</source> <target>Geen toestemming om spraakbericht op te nemen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No push server" xml:space="preserve"> + <source>No push server</source> + <target>Lokaal</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> <target>Geen ontvangen of verzonden bestanden</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No servers for private message routing." xml:space="preserve"> + <source>No servers for private message routing.</source> + <target>Geen servers voor het routeren van privéberichten.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive files." xml:space="preserve"> + <source>No servers to receive files.</source> + <target>Geen servers om bestanden te ontvangen.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive messages." xml:space="preserve"> + <source>No servers to receive messages.</source> + <target>Geen servers om berichten te ontvangen.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to send files." xml:space="preserve"> + <source>No servers to send files.</source> + <target>Geen servers om bestanden te verzenden.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No token!" xml:space="preserve"> + <source>No token!</source> + <target>Geen token!</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="No unread chats" xml:space="preserve"> + <source>No unread chats</source> + <target>Geen ongelezen chats</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> + <target>Geen gebruikers-ID's.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Not compatible!" xml:space="preserve"> <source>Not compatible!</source> <target>Niet compatibel!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notes" xml:space="preserve"> + <source>Notes</source> + <target>Notities</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing selected" xml:space="preserve"> + <source>Nothing selected</source> + <target>Niets geselecteerd</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing to forward!" xml:space="preserve"> + <source>Nothing to forward!</source> + <target>Niets om door te sturen!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> <target>Meldingen</target> @@ -3543,6 +5379,21 @@ Dit is jouw link voor groep %@!</target> <target>Meldingen zijn uitgeschakeld!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notifications error" xml:space="preserve"> + <source>Notifications error</source> + <target>Meldingsfout</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Notifications privacy" xml:space="preserve"> + <source>Notifications privacy</source> + <target>Privacy van meldingen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications status" xml:space="preserve"> + <source>Notifications status</source> + <target>Meldingsstatus</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. @@ -3552,44 +5403,43 @@ Dit is jouw link voor groep %@!</target> - schakel leden uit ("waarnemer" rol)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="OK" xml:space="preserve"> + <source>OK</source> + <target>OK</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Off" xml:space="preserve"> <source>Off</source> <target>Uit</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> - <source>Off (Local)</source> - <target>Uit (lokaal)</target> - <note>No comment provided by engineer.</note> + <note>blur media</note> </trans-unit> <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> <target>OK</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> <target>Oude database</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> - <source>Old database archive</source> - <target>Oud database archief</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> <target>Eenmalige uitnodiging link</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> - <target>Onion hosts zullen nodig zijn voor verbinding. Vereist het inschakelen van VPN.</target> + <trans-unit id="Onion hosts will be **required** for connection. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be **required** for connection. +Requires compatible VPN.</source> + <target>Onion hosts zullen nodig zijn voor verbinding. +Vereist het inschakelen van VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> - <target>Onion hosts worden gebruikt indien beschikbaar. Vereist het inschakelen van VPN.</target> + <trans-unit id="Onion hosts will be used when available. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be used when available. +Requires compatible VPN.</source> + <target>Onion hosts worden gebruikt indien beschikbaar. +Vereist het inschakelen van VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will not be used." xml:space="preserve"> @@ -3597,11 +5447,21 @@ Dit is jouw link voor groep %@!</target> <target>Onion hosts worden niet gebruikt.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only chat owners can change preferences." xml:space="preserve"> + <source>Only chat owners can change preferences.</source> + <target>Alleen chateigenaren kunnen voorkeuren wijzigen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <target>Alleen client apparaten slaan gebruikers profielen, contacten, groepen en berichten op die zijn verzonden met **2-laags end-to-end-codering**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only delete conversation" xml:space="preserve"> + <source>Only delete conversation</source> + <target>Alleen conversatie verwijderen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> <source>Only group owners can change group preferences.</source> <target>Alleen groep eigenaren kunnen groep voorkeuren wijzigen.</target> @@ -3617,14 +5477,24 @@ Dit is jouw link voor groep %@!</target> <target>Alleen groep eigenaren kunnen spraak berichten inschakelen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can add message reactions." xml:space="preserve"> - <source>Only you can add message reactions.</source> - <target>Alleen jij kunt berichtreacties toevoegen.</target> + <trans-unit id="Only sender and moderators see it" xml:space="preserve"> + <source>Only sender and moderators see it</source> + <target>Alleen de verzender en moderators zien het</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <target>Alleen jij kunt berichten onomkeerbaar verwijderen (je contact kan ze markeren voor verwijdering).</target> + <trans-unit id="Only you and moderators see it" xml:space="preserve"> + <source>Only you and moderators see it</source> + <target>Alleen jij en moderators zien het</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you can add message reactions." xml:space="preserve"> + <source>Only you can add message reactions.</source> + <target>Alleen jij kunt bericht reacties toevoegen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source> + <target>Alleen jij kunt berichten definitief verwijderen (je contact kan ze markeren voor verwijdering). (24 uur)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can make calls." xml:space="preserve"> @@ -3644,12 +5514,12 @@ Dit is jouw link voor groep %@!</target> </trans-unit> <trans-unit id="Only your contact can add message reactions." xml:space="preserve"> <source>Only your contact can add message reactions.</source> - <target>Alleen uw contact kan berichtreacties toevoegen.</target> + <target>Alleen uw contact kan bericht reacties toevoegen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <target>Alleen uw contact kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering).</target> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source> + <target>Alleen uw contact kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering). (24 uur)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can make calls." xml:space="preserve"> @@ -3670,16 +5540,21 @@ Dit is jouw link voor groep %@!</target> <trans-unit id="Open" xml:space="preserve"> <source>Open</source> <target>Open</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Open instellingen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open changes" xml:space="preserve"> + <source>Open changes</source> + <target>Wijzigingen openen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> - <target>Gesprekken openen</target> + <target>Chat openen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Open chat console" xml:space="preserve"> @@ -3687,26 +5562,87 @@ Dit is jouw link voor groep %@!</target> <target>Chat console openen</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Open conditions" xml:space="preserve"> + <source>Open conditions</source> + <target>Open voorwaarden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open group" xml:space="preserve"> <source>Open group</source> <target>Open groep</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve"> - <source>Open user profiles</source> - <target>Gebruikers profielen openen</target> + <trans-unit id="Open link?" xml:space="preserve"> + <source>Open link?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Open migration to another device" xml:space="preserve"> + <source>Open migration to another device</source> + <target>Open de migratie naar een ander apparaat</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> - <target>Open-source protocol en code. Iedereen kan de servers draaien.</target> + <trans-unit id="Opening app…" xml:space="preserve"> + <source>Opening app…</source> + <target>App openen…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve"> - <source>Opening database…</source> - <target>Database openen…</target> + <trans-unit id="Operator" xml:space="preserve"> + <source>Operator</source> + <target>Operator</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Operator server" xml:space="preserve"> + <source>Operator server</source> + <target>Operatorserver</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Or import archive file" xml:space="preserve"> + <source>Or import archive file</source> + <target>Of importeer archiefbestand</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or paste archive link" xml:space="preserve"> + <source>Or paste archive link</source> + <target>Of plak de archief link</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or scan QR code" xml:space="preserve"> + <source>Or scan QR code</source> + <target>Of scan de QR-code</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or securely share this file link" xml:space="preserve"> + <source>Or securely share this file link</source> + <target>Of deel deze bestands link veilig</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or show this code" xml:space="preserve"> + <source>Or show this code</source> + <target>Of laat deze code zien</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or to share privately" xml:space="preserve"> + <source>Or to share privately</source> + <target>Of om privé te delen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Organize chats into lists" xml:space="preserve"> + <source>Organize chats into lists</source> + <target>Organiseer chats in lijsten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other" xml:space="preserve"> + <source>Other</source> + <target>Ander</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other file errors: %@" xml:space="preserve"> + <source>Other file errors: +%@</source> + <target>Andere bestandsfouten: +%@</target> + <note>alert message</note> + </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> <target>PING count</target> @@ -3742,15 +5678,20 @@ Dit is jouw link voor groep %@!</target> <target>Toegangscode ingesteld!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Password" xml:space="preserve"> + <source>Password</source> + <target>Wachtwoord</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> <target>Wachtwoord om weer te geven</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste" xml:space="preserve"> - <source>Paste</source> - <target>Plakken</target> - <note>No comment provided by engineer.</note> + <trans-unit id="Past member %@" xml:space="preserve"> + <source>Past member %@</source> + <target>Voormalig lid %@</target> + <note>past/unknown group member</note> </trans-unit> <trans-unit id="Paste desktop address" xml:space="preserve"> <source>Paste desktop address</source> @@ -3762,23 +5703,23 @@ Dit is jouw link voor groep %@!</target> <target>Afbeelding plakken</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve"> - <source>Paste received link</source> - <target>Plak de ontvangen link</target> + <trans-unit id="Paste link to connect!" xml:space="preserve"> + <source>Paste link to connect!</source> + <target>Plak een link om te verbinden!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> - <source>Paste the link you received to connect with your contact.</source> - <target>Plak de link die je hebt ontvangen in het vak hieronder om verbinding te maken met je contact.</target> - <note>placeholder</note> - </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> - <target>Mensen kunnen alleen verbinding met u maken via de links die u deelt.</target> + <trans-unit id="Paste the link you received" xml:space="preserve"> + <source>Paste the link you received</source> + <target>Plak de link die je hebt ontvangen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Periodically" xml:space="preserve"> - <source>Periodically</source> + <trans-unit id="Pending" xml:space="preserve"> + <source>Pending</source> + <target>in behandeling</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Periodic" xml:space="preserve"> + <source>Periodic</source> <target>Periodiek</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3787,11 +5728,33 @@ Dit is jouw link voor groep %@!</target> <target>Decodering fout</target> <note>message decrypt error item</note> </trans-unit> + <trans-unit id="Picture-in-picture calls" xml:space="preserve"> + <source>Picture-in-picture calls</source> + <target>Beeld-in-beeld oproepen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Play from the chat list." xml:space="preserve"> + <source>Play from the chat list.</source> + <target>Afspelen via de chat lijst.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable calls." xml:space="preserve"> + <source>Please ask your contact to enable calls.</source> + <target>Vraag uw contactpersoon om oproepen in te schakelen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> <target>Vraag uw contact om het verzenden van spraak berichten in te schakelen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers." xml:space="preserve"> + <source>Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers.</source> + <target>Controleer of mobiel en desktop met hetzelfde lokale netwerk zijn verbonden en of de desktopfirewall de verbinding toestaat. +Deel eventuele andere problemen met de ontwikkelaars.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> <target>Controleer of u de juiste link heeft gebruikt of vraag uw contact om u een andere te sturen.</target> @@ -3807,6 +5770,18 @@ Dit is jouw link voor groep %@!</target> <target>Controleer de uwe en uw contact voorkeuren.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve"> + <source>Please confirm that network settings are correct for this device.</source> + <target>Controleer of de netwerk instellingen correct zijn voor dit apparaat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact developers. Error: %@" xml:space="preserve"> + <source>Please contact developers. +Error: %@</source> + <target>Neem contact op met ontwikkelaars. +Fout: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> <target>Neem contact op met de groep beheerder.</target> @@ -3839,7 +5814,7 @@ Dit is jouw link voor groep %@!</target> </trans-unit> <trans-unit id="Please store passphrase securely, you will NOT be able to access chat if you lose it." xml:space="preserve"> <source>Please store passphrase securely, you will NOT be able to access chat if you lose it.</source> - <target>Sla het wachtwoord veilig op. Als u deze kwijtraakt, heeft u GEEN toegang tot de gesprekken.</target> + <target>Sla het wachtwoord veilig op. Als u deze kwijtraakt, heeft u GEEN toegang tot de chats.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please store passphrase securely, you will NOT be able to change it if you lose it." xml:space="preserve"> @@ -3847,11 +5822,31 @@ Dit is jouw link voor groep %@!</target> <target>Bewaar het wachtwoord veilig, u kunt deze NIET wijzigen als u het kwijtraakt.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please try to disable and re-enable notfications." xml:space="preserve"> + <source>Please try to disable and re-enable notfications.</source> + <target>Probeer meldingen uit en weer in te schakelen.</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token activation to complete." xml:space="preserve"> + <source>Please wait for token activation to complete.</source> + <target>Wacht tot de tokenactivering voltooid is.</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token to be registered." xml:space="preserve"> + <source>Please wait for token to be registered.</source> + <target>Wacht tot het token is geregistreerd.</target> + <note>token info</note> + </trans-unit> <trans-unit id="Polish interface" xml:space="preserve"> <source>Polish interface</source> <target>Poolse interface</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Port" xml:space="preserve"> + <source>Port</source> + <target>Poort</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> <target>Mogelijk is de certificaat vingerafdruk in het server adres onjuist</target> @@ -3862,36 +5857,86 @@ Dit is jouw link voor groep %@!</target> <target>Bewaar het laatste berichtconcept, met bijlagen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Preset server" xml:space="preserve"> - <source>Preset server</source> - <target>Vooraf ingestelde server</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> <target>Vooraf ingesteld server adres</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Preset servers" xml:space="preserve"> + <source>Preset servers</source> + <target>Vooraf ingestelde servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Preview" xml:space="preserve"> <source>Preview</source> <target>Voorbeeld</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Previously connected servers" xml:space="preserve"> + <source>Previously connected servers</source> + <target>Eerder verbonden servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> <target>Privacy en beveiliging</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Privacy for your customers." xml:space="preserve"> + <source>Privacy for your customers.</source> + <target>Privacy voor uw klanten.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy policy and conditions of use." xml:space="preserve"> + <source>Privacy policy and conditions of use.</source> + <target>Privacybeleid en gebruiksvoorwaarden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy redefined" xml:space="preserve"> <source>Privacy redefined</source> <target>Privacy opnieuw gedefinieerd</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve"> + <source>Private chats, groups and your contacts are not accessible to server operators.</source> + <target>Privéchats, groepen en uw contacten zijn niet toegankelijk voor serverbeheerders.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> <target>Privé bestandsnamen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private media file names." xml:space="preserve"> + <source>Private media file names.</source> + <target>Namen van persoonlijke mediabestanden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing" xml:space="preserve"> + <source>Private message routing</source> + <target>Routering van privéberichten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing 🚀" xml:space="preserve"> + <source>Private message routing 🚀</source> + <target>Routing van privéberichten🚀</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private notes" xml:space="preserve"> + <source>Private notes</source> + <target>Privé notities</target> + <note>name of notes to self</note> + </trans-unit> + <trans-unit id="Private routing" xml:space="preserve"> + <source>Private routing</source> + <target>Privéroutering</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private routing error" xml:space="preserve"> + <source>Private routing error</source> + <target>Fout in privéroutering</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> <target>Profiel- en serververbindingen</target> @@ -3902,14 +5947,9 @@ Dit is jouw link voor groep %@!</target> <target>profielfoto</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile name" xml:space="preserve"> - <source>Profile name</source> - <target>Profielnaam</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile name:" xml:space="preserve"> - <source>Profile name:</source> - <target>Profielnaam:</target> + <trans-unit id="Profile images" xml:space="preserve"> + <source>Profile images</source> + <target>Profiel afbeeldingen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile password" xml:space="preserve"> @@ -3917,10 +5957,15 @@ Dit is jouw link voor groep %@!</target> <target>Profiel wachtwoord</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Profile theme" xml:space="preserve"> + <source>Profile theme</source> + <target>Profiel thema</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> <source>Profile update will be sent to your contacts.</source> <target>Profiel update wordt naar uw contacten verzonden.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> @@ -3929,12 +5974,12 @@ Dit is jouw link voor groep %@!</target> </trans-unit> <trans-unit id="Prohibit irreversible message deletion." xml:space="preserve"> <source>Prohibit irreversible message deletion.</source> - <target>Verbied het onomkeerbaar verwijderen van berichten.</target> + <target>Verbied het definitief verwijderen van berichten.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit message reactions." xml:space="preserve"> <source>Prohibit message reactions.</source> - <target>Berichtreacties verbieden.</target> + <target>Bericht reacties verbieden.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit messages reactions." xml:space="preserve"> @@ -3942,6 +5987,16 @@ Dit is jouw link voor groep %@!</target> <target>Berichten reacties verbieden.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Prohibit reporting messages to moderators." xml:space="preserve"> + <source>Prohibit reporting messages to moderators.</source> + <target>Het melden van berichten aan moderators is niet toegestaan.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending SimpleX links." xml:space="preserve"> + <source>Prohibit sending SimpleX links.</source> + <target>Verbied het verzenden van SimpleX-links</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> <target>Verbied het sturen van directe berichten naar leden.</target> @@ -3962,14 +6017,26 @@ Dit is jouw link voor groep %@!</target> <target>Verbieden het verzenden van spraak berichten.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect IP address" xml:space="preserve"> + <source>Protect IP address</source> + <target>Bescherm het IP-adres</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> <target>App scherm verbergen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve"> + <source>Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings.</source> + <target>Bescherm uw IP-adres tegen de berichtenrelais die door uw contacten zijn gekozen. +Schakel dit in in *Netwerk en servers*-instellingen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> - <target>Bescherm je chat profielen met een wachtwoord!</target> + <target>Bescherm je chatprofielen met een wachtwoord!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Protocol timeout" xml:space="preserve"> @@ -3982,16 +6049,46 @@ Dit is jouw link voor groep %@!</target> <target>Protocol timeout per KB</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Proxied" xml:space="preserve"> + <source>Proxied</source> + <target>Proxied</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied servers" xml:space="preserve"> + <source>Proxied servers</source> + <target>Proxied servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxy requires password" xml:space="preserve"> + <source>Proxy requires password</source> + <target>Proxy vereist wachtwoord</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> <target>Push meldingen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Push server" xml:space="preserve"> + <source>Push server</source> + <target>Push server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Quantum resistant encryption" xml:space="preserve"> + <source>Quantum resistant encryption</source> + <target>quantum bestendige encryptie</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> <source>Rate the app</source> <target>Beoordeel de app</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reachable chat toolbar" xml:space="preserve"> + <source>Reachable chat toolbar</source> + <target>Toegankelijke chatwerkbalk</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> <target>Reageer…</target> @@ -4000,15 +6097,20 @@ Dit is jouw link voor groep %@!</target> <trans-unit id="Read" xml:space="preserve"> <source>Read</source> <target>Lees</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Read more" xml:space="preserve"> <source>Read more</source> <target>Lees meer</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source> + <target>Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</source> <target>Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/app-settings.html#uw-simplex-contactadres).</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -4017,11 +6119,6 @@ Dit is jouw link voor groep %@!</target> <target>Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> - <source>Read more in our GitHub repository.</source> - <target>Lees meer in onze GitHub repository.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> <target>Lees meer in onze [GitHub-repository](https://github.com/simplex-chat/simplex-chat#readme).</target> @@ -4032,6 +6129,11 @@ Dit is jouw link voor groep %@!</target> <target>Bevestigingen zijn uitgeschakeld</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receive errors" xml:space="preserve"> + <source>Receive errors</source> + <target>Fouten ontvangen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Ontvangen op</target> @@ -4052,6 +6154,21 @@ Dit is jouw link voor groep %@!</target> <target>Ontvangen bericht</target> <note>message info title</note> </trans-unit> + <trans-unit id="Received messages" xml:space="preserve"> + <source>Received messages</source> + <target>Ontvangen berichten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received reply" xml:space="preserve"> + <source>Received reply</source> + <target>Antwoord ontvangen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received total" xml:space="preserve"> + <source>Received total</source> + <target>Totaal ontvangen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> <target>Het ontvangstadres wordt gewijzigd naar een andere server. Adres wijziging wordt voltooid nadat de afzender online is.</target> @@ -4067,16 +6184,51 @@ Dit is jouw link voor groep %@!</target> <target>Ontvang via</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Recent history and improved [directory bot](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)." xml:space="preserve"> + <source>Recent history and improved [directory bot](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).</source> + <target>Recente geschiedenis en verbeterde [directory bot](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).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve"> + <source>Recipient(s) can't see who this message is from.</source> + <target>Ontvanger(s) kunnen niet zien van wie dit bericht afkomstig is.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> <target>Ontvangers zien updates terwijl u ze typt.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect" xml:space="preserve"> + <source>Reconnect</source> + <target>opnieuw verbinden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> <target>Verbind alle verbonden servers opnieuw om de bezorging van berichten af te dwingen. Het maakt gebruik van extra data.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect all servers" xml:space="preserve"> + <source>Reconnect all servers</source> + <target>Maak opnieuw verbinding met alle servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers?" xml:space="preserve"> + <source>Reconnect all servers?</source> + <target>Alle servers opnieuw verbinden?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect server to force message delivery. It uses additional traffic.</source> + <target>Maak opnieuw verbinding met de server om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra verkeer.Maak opnieuw verbinding met de server om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra data.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server?" xml:space="preserve"> + <source>Reconnect server?</source> + <target>Server opnieuw verbinden?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> <target>Servers opnieuw verbinden?</target> @@ -4097,10 +6249,26 @@ Dit is jouw link voor groep %@!</target> <target>Verminderd batterijgebruik</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Register" xml:space="preserve"> + <source>Register</source> + <target>Register</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Register notification token?" xml:space="preserve"> + <source>Register notification token?</source> + <target>Meldingstoken registreren?</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Registered" xml:space="preserve"> + <source>Registered</source> + <target>Geregistreerd</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> <target>Afwijzen</target> - <note>reject incoming call via notification</note> + <note>reject incoming call via notification +swipe action</note> </trans-unit> <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> <source>Reject (sender NOT notified)</source> @@ -4127,6 +6295,16 @@ Dit is jouw link voor groep %@!</target> <target>Verwijderen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Remove archive?" xml:space="preserve"> + <source>Remove archive?</source> + <target>Archief verwijderen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove image" xml:space="preserve"> + <source>Remove image</source> + <target>Verwijder afbeelding</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> <target>Lid verwijderen</target> @@ -4162,16 +6340,81 @@ Dit is jouw link voor groep %@!</target> <target>Verbindingsverzoek herhalen?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat download" xml:space="preserve"> + <source>Repeat download</source> + <target>Herhaal het downloaden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat import" xml:space="preserve"> + <source>Repeat import</source> + <target>Herhaal import</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Repeat join request?" xml:space="preserve"> <source>Repeat join request?</source> <target>Deelnameverzoek herhalen?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat upload" xml:space="preserve"> + <source>Repeat upload</source> + <target>Herhaal het uploaden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reply" xml:space="preserve"> <source>Reply</source> <target>Antwoord</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Report" xml:space="preserve"> + <source>Report</source> + <target>rapporteren</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Report content: only group moderators will see it." xml:space="preserve"> + <source>Report content: only group moderators will see it.</source> + <target>Inhoud melden: alleen groepsmoderators kunnen dit zien.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report member profile: only group moderators will see it." xml:space="preserve"> + <source>Report member profile: only group moderators will see it.</source> + <target>Rapporteer ledenprofiel: alleen groepsmoderators kunnen dit zien.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report other: only group moderators will see it." xml:space="preserve"> + <source>Report other: only group moderators will see it.</source> + <target>Anders melden: alleen groepsmoderators kunnen het zien.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report reason?" xml:space="preserve"> + <source>Report reason?</source> + <target>Reden melding?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Report spam: only group moderators will see it." xml:space="preserve"> + <source>Report spam: only group moderators will see it.</source> + <target>Spam melden: alleen groepsmoderators kunnen het zien.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report violation: only group moderators will see it." xml:space="preserve"> + <source>Report violation: only group moderators will see it.</source> + <target>Rapporteer overtreding: alleen groepsmoderators kunnen dit zien.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report: %@" xml:space="preserve"> + <source>Report: %@</source> + <target>rapporteer: %@</target> + <note>report in notification</note> + </trans-unit> + <trans-unit id="Reporting messages to moderators is prohibited." xml:space="preserve"> + <source>Reporting messages to moderators is prohibited.</source> + <target>Het is niet toegestaan om berichten aan moderators te melden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reports" xml:space="preserve"> + <source>Reports</source> + <target>Rapporten</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Required" xml:space="preserve"> <source>Required</source> <target>Vereist</target> @@ -4182,19 +6425,44 @@ Dit is jouw link voor groep %@!</target> <target>Resetten</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset all hints" xml:space="preserve"> + <source>Reset all hints</source> + <target>Alle hints resetten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics" xml:space="preserve"> + <source>Reset all statistics</source> + <target>Reset alle statistieken</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics?" xml:space="preserve"> + <source>Reset all statistics?</source> + <target>Alle statistieken resetten?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> <target>Kleuren resetten</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to app theme" xml:space="preserve"> + <source>Reset to app theme</source> + <target>Terugzetten naar app thema</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> <target>Resetten naar standaardwaarden</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to user theme" xml:space="preserve"> + <source>Reset to user theme</source> + <target>Terugzetten naar gebruikersthema</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> <source>Restart the app to create a new chat profile</source> - <target>Start de app opnieuw om een nieuw chat profiel aan te maken</target> + <target>Start de app opnieuw om een nieuw chatprofiel aan te maken</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restart the app to use imported chat database" xml:space="preserve"> @@ -4222,14 +6490,19 @@ Dit is jouw link voor groep %@!</target> <target>Database fout herstellen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Retry" xml:space="preserve"> + <source>Retry</source> + <target>Opnieuw proberen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> <target>Onthullen</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Revert" xml:space="preserve"> - <source>Revert</source> - <target>Terugdraaien</target> + <trans-unit id="Review conditions" xml:space="preserve"> + <source>Review conditions</source> + <target>Voorwaarden bekijken</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke" xml:space="preserve"> @@ -4257,54 +6530,70 @@ Dit is jouw link voor groep %@!</target> <target>Chat uitvoeren</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve"> - <source>SMP servers</source> - <target>SMP servers</target> + <trans-unit id="SMP server" xml:space="preserve"> + <source>SMP server</source> + <target>SMP server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SOCKS proxy" xml:space="preserve"> + <source>SOCKS proxy</source> + <target>SOCKS proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safely receive files" xml:space="preserve"> + <source>Safely receive files</source> + <target>Veilig bestanden ontvangen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safer groups" xml:space="preserve"> + <source>Safer groups</source> + <target>Veiligere groepen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save" xml:space="preserve"> <source>Save</source> <target>Opslaan</target> - <note>chat item action</note> + <note>alert button +chat item action</note> </trans-unit> <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> <target>Bewaar (en informeer contacten)</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> <target>Opslaan en Contact melden</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> <target>Opslaan en groep leden melden</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save and reconnect" xml:space="preserve"> + <source>Save and reconnect</source> + <target>Opslaan en opnieuw verbinden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save and update group profile" xml:space="preserve"> <source>Save and update group profile</source> <target>Groep profiel opslaan en bijwerken</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save archive" xml:space="preserve"> - <source>Save archive</source> - <target>Bewaar archief</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve"> - <source>Save auto-accept settings</source> - <target>Sla instellingen voor automatisch accepteren op</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> <source>Save group profile</source> <target>Groep profiel opslaan</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save list" xml:space="preserve"> + <source>Save list</source> + <target>Lijst opslaan</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> - <target>Bewaar het wachtwoord en open je gesprekken</target> + <target>Wachtwoord opslaan en open je chats</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save passphrase in Keychain" xml:space="preserve"> @@ -4315,7 +6604,7 @@ Dit is jouw link voor groep %@!</target> <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> <target>Voorkeuren opslaan?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> @@ -4324,22 +6613,27 @@ Dit is jouw link voor groep %@!</target> </trans-unit> <trans-unit id="Save servers" xml:space="preserve"> <source>Save servers</source> - <target>Bewaar servers</target> + <target>Servers opslaan</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> <target>Servers opslaan?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve"> - <source>Save settings?</source> - <target>Instellingen opslaan?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> - <target>Welkomst bericht opslaan?</target> + <target>Welkom bericht opslaan?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save your profile?" xml:space="preserve"> + <source>Save your profile?</source> + <target>Uw profiel opslaan?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Saved" xml:space="preserve"> + <source>Saved</source> + <target>Opgeslagen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> @@ -4347,6 +6641,31 @@ Dit is jouw link voor groep %@!</target> <target>Opgeslagen WebRTC ICE servers worden verwijderd</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Saved from" xml:space="preserve"> + <source>Saved from</source> + <target>Opgeslagen van</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved message" xml:space="preserve"> + <source>Saved message</source> + <target>Opgeslagen bericht</target> + <note>message info title</note> + </trans-unit> + <trans-unit id="Saving %lld messages" xml:space="preserve"> + <source>Saving %lld messages</source> + <target>%lld berichten opslaan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scale" xml:space="preserve"> + <source>Scale</source> + <target>Schaal</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan / Paste link" xml:space="preserve"> + <source>Scan / Paste link</source> + <target>Link scannen/plakken</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> <target>Scan QR-code</target> @@ -4377,11 +6696,31 @@ Dit is jouw link voor groep %@!</target> <target>Zoeken</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Search bar accepts invitation links." xml:space="preserve"> + <source>Search bar accepts invitation links.</source> + <target>Zoekbalk accepteert uitnodigingslinks.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search or paste SimpleX link" xml:space="preserve"> + <source>Search or paste SimpleX link</source> + <target>Zoeken of plak een SimpleX link</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secondary" xml:space="preserve"> + <source>Secondary</source> + <target>Secundair</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> <source>Secure queue</source> <target>Veilige wachtrij</target> <note>server test step</note> </trans-unit> + <trans-unit id="Secured" xml:space="preserve"> + <source>Secured</source> + <target>Beveiligd</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> <target>Beveiligingsbeoordeling</target> @@ -4395,6 +6734,21 @@ Dit is jouw link voor groep %@!</target> <trans-unit id="Select" xml:space="preserve"> <source>Select</source> <target>Selecteer</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Select chat profile" xml:space="preserve"> + <source>Select chat profile</source> + <target>Selecteer chatprofiel</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected %lld" xml:space="preserve"> + <source>Selected %lld</source> + <target>%lld geselecteerd</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Geselecteerde chat voorkeuren verbieden dit bericht.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct" xml:space="preserve"> @@ -4432,11 +6786,6 @@ Dit is jouw link voor groep %@!</target> <target>Stuur ontvangstbewijzen naar</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve"> - <source>Send direct message</source> - <target>Direct bericht sturen</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Send direct message to connect" xml:space="preserve"> <source>Send direct message to connect</source> <target>Stuur een direct bericht om verbinding te maken</target> @@ -4447,6 +6796,11 @@ Dit is jouw link voor groep %@!</target> <target>Stuur een verdwijnend bericht</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send errors" xml:space="preserve"> + <source>Send errors</source> + <target>Verzend fouten</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> <target>Link voorbeelden verzenden</target> @@ -4457,14 +6811,29 @@ Dit is jouw link voor groep %@!</target> <target>Stuur een livebericht</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send message to enable calls." xml:space="preserve"> + <source>Send message to enable calls.</source> + <target>Stuur een bericht om oproepen mogelijk te maken.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source> + <target>Stuur berichten rechtstreeks als het IP-adres beschermd is en uw of bestemmingsserver geen privéroutering ondersteunt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when your or destination server does not support private routing.</source> + <target>Stuur berichten rechtstreeks wanneer uw of de doelserver geen privéroutering ondersteunt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> <target>Meldingen verzenden</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> - <source>Send notifications:</source> - <target>Meldingen verzenden:</target> + <trans-unit id="Send private reports" xml:space="preserve"> + <source>Send private reports</source> + <target>Rapporteer privé</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve"> @@ -4482,10 +6851,15 @@ Dit is jouw link voor groep %@!</target> <target>Stuur ze vanuit de galerij of aangepaste toetsenborden.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve"> + <source>Send up to 100 last messages to new members.</source> + <target>Stuur tot 100 laatste berichten naar nieuwe leden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> <target>Afzender heeft bestandsoverdracht geannuleerd.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> @@ -4542,6 +6916,11 @@ Dit is jouw link voor groep %@!</target> <target>Verzonden op: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Sent directly" xml:space="preserve"> + <source>Sent directly</source> + <target>Direct verzonden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> <target>Verzonden bestandsgebeurtenis</target> @@ -4552,11 +6931,71 @@ Dit is jouw link voor groep %@!</target> <target>Verzonden bericht</target> <note>message info title</note> </trans-unit> + <trans-unit id="Sent messages" xml:space="preserve"> + <source>Sent messages</source> + <target>Verzonden berichten</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> <target>Verzonden berichten worden na ingestelde tijd verwijderd.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sent reply" xml:space="preserve"> + <source>Sent reply</source> + <target>Antwoord verzonden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent total" xml:space="preserve"> + <source>Sent total</source> + <target>Totaal verzonden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent via proxy" xml:space="preserve"> + <source>Sent via proxy</source> + <target>Verzonden via proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server" xml:space="preserve"> + <source>Server</source> + <target>Server</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server added to operator %@." xml:space="preserve"> + <source>Server added to operator %@.</source> + <target>Server toegevoegd aan operator %@.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Server address" xml:space="preserve"> + <source>Server address</source> + <target>Server adres</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings." xml:space="preserve"> + <source>Server address is incompatible with network settings.</source> + <target>Serveradres is niet compatibel met netwerkinstellingen.</target> + <note>srv error text.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Server address is incompatible with network settings: %@.</source> + <target>Serveradres is incompatibel met netwerkinstellingen: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server operator changed." xml:space="preserve"> + <source>Server operator changed.</source> + <target>Serveroperator gewijzigd.</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Server operators" xml:space="preserve"> + <source>Server operators</source> + <target>Serverbeheerders</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server protocol changed." xml:space="preserve"> + <source>Server protocol changed.</source> + <target>Serverprotocol gewijzigd.</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> <target>Server vereist autorisatie om wachtrijen te maken, controleer wachtwoord</target> @@ -4572,11 +7011,36 @@ Dit is jouw link voor groep %@!</target> <target>Servertest mislukt!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Server type" xml:space="preserve"> + <source>Server type</source> + <target>Server type</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server version is incompatible with network settings." xml:space="preserve"> + <source>Server version is incompatible with network settings.</source> + <target>Serverversie is incompatibel met netwerkinstellingen.</target> + <note>srv error text</note> + </trans-unit> + <trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve"> + <source>Server version is incompatible with your app: %@.</source> + <target>Serverversie is incompatibel met uw app: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Servers" xml:space="preserve"> <source>Servers</source> <target>Servers</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Servers info" xml:space="preserve"> + <source>Servers info</source> + <target>Server informatie</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <target>Serverstatistieken worden gereset - dit kan niet ongedaan worden gemaakt!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Session code" xml:space="preserve"> <source>Session code</source> <target>Sessie code</target> @@ -4587,11 +7051,21 @@ Dit is jouw link voor groep %@!</target> <target>Stel 1 dag in</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set chat name…" xml:space="preserve"> + <source>Set chat name…</source> + <target>Stel chatnaam in…</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> <target>Contactnaam instellen…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve"> + <source>Set default theme</source> + <target>Stel het standaard thema in</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> <target>Groep voorkeuren instellen</target> @@ -4602,11 +7076,21 @@ Dit is jouw link voor groep %@!</target> <target>Stel het in in plaats van systeemverificatie.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve"> + <source>Set message expiration in chats.</source> + <target>Stel de berichtvervaldatum in chats in.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passcode" xml:space="preserve"> <source>Set passcode</source> <target>Toegangscode instellen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set passphrase" xml:space="preserve"> + <source>Set passphrase</source> + <target>Wachtwoord instellen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> <target>Wachtwoord instellen om te exporteren</target> @@ -4627,24 +7111,55 @@ Dit is jouw link voor groep %@!</target> <target>Instellingen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve"> + <source>Settings were changed.</source> + <target>Instellingen zijn gewijzigd.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve"> + <source>Shape profile images</source> + <target>Vorm profiel afbeeldingen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share" xml:space="preserve"> <source>Share</source> <target>Deel</target> - <note>chat item action</note> + <note>alert action +chat item action</note> </trans-unit> <trans-unit id="Share 1-time link" xml:space="preserve"> <source>Share 1-time link</source> <target>Eenmalige link delen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share 1-time link with a friend" xml:space="preserve"> + <source>Share 1-time link with a friend</source> + <target>Deel eenmalig een link met een vriend</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve"> + <source>Share SimpleX address on social media.</source> + <target>Deel het SimpleX-adres op sociale media.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address" xml:space="preserve"> <source>Share address</source> <target>Adres delen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share address publicly" xml:space="preserve"> + <source>Share address publicly</source> + <target>Adres openbaar delen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address with contacts?" xml:space="preserve"> <source>Share address with contacts?</source> <target>Adres delen met contacten?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Share from other apps." xml:space="preserve"> + <source>Share from other apps.</source> + <target>Delen vanuit andere apps.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share link" xml:space="preserve"> @@ -4652,9 +7167,19 @@ Dit is jouw link voor groep %@!</target> <target>Deel link</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve"> - <source>Share one-time invitation link</source> - <target>Eenmalige uitnodiging link delen</target> + <trans-unit id="Share profile" xml:space="preserve"> + <source>Share profile</source> + <target>Profiel delen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share this 1-time invite link" xml:space="preserve"> + <source>Share this 1-time invite link</source> + <target>Deel deze eenmalige uitnodigingslink</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share to SimpleX" xml:space="preserve"> + <source>Share to SimpleX</source> + <target>Delen op SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share with contacts" xml:space="preserve"> @@ -4662,6 +7187,15 @@ Dit is jouw link voor groep %@!</target> <target>Delen met contacten</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Short link" xml:space="preserve"> + <source>Short link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show QR code" xml:space="preserve"> + <source>Show QR code</source> + <target>Toon QR-code</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> <target>Toon oproepen in de telefoongeschiedenis</target> @@ -4677,21 +7211,46 @@ Dit is jouw link voor groep %@!</target> <target>Laat laatste berichten zien</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show message status" xml:space="preserve"> + <source>Show message status</source> + <target>Toon berichtstatus</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show percentage" xml:space="preserve"> + <source>Show percentage</source> + <target>Percentage weergeven</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Toon voorbeeld</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show → on messages sent via private routing." xml:space="preserve"> + <source>Show → on messages sent via private routing.</source> + <target>Toon → bij berichten verzonden via privéroutering.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show:" xml:space="preserve"> <source>Show:</source> <target>Toon:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve"> + <source>SimpleX</source> + <target>SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Address" xml:space="preserve"> <source>SimpleX Address</source> <target>SimpleX adres</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." xml:space="preserve"> + <source>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</source> + <target>Simplex-chat en flux hebben een overeenkomst gemaakt om door flux geëxploiteerde servers in de app op te nemen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> <target>De beveiliging van SimpleX Chat is gecontroleerd door Trail of Bits.</target> @@ -4722,6 +7281,20 @@ Dit is jouw link voor groep %@!</target> <target>SimpleX adres</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX address and 1-time links are safe to share via any messenger." xml:space="preserve"> + <source>SimpleX address and 1-time links are safe to share via any messenger.</source> + <target>SimpleX-adressen en eenmalige links kunnen veilig worden gedeeld via elke messenger.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address or 1-time link?" xml:space="preserve"> + <source>SimpleX address or 1-time link?</source> + <target>SimpleX adres of eenmalige link?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX channel link" xml:space="preserve"> + <source>SimpleX channel link</source> + <note>simplex link type</note> + </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> <target>SimpleX contact adres</target> @@ -4740,6 +7313,16 @@ Dit is jouw link voor groep %@!</target> <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> <target>SimpleX links</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve"> + <source>SimpleX links are prohibited.</source> + <target>SimpleX-links zijn niet toegestaan.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve"> + <source>SimpleX links not allowed</source> + <target>SimpleX-links zijn niet toegestaan</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> @@ -4747,11 +7330,21 @@ Dit is jouw link voor groep %@!</target> <target>Eenmalige SimpleX uitnodiging</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> + <source>SimpleX protocols reviewed by Trail of Bits.</source> + <target>SimpleX-protocollen beoordeeld door Trail of Bits.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Simplified incognito mode" xml:space="preserve"> <source>Simplified incognito mode</source> <target>Vereenvoudigde incognitomodus</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Size" xml:space="preserve"> + <source>Size</source> + <target>Maat</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Overslaan</target> @@ -4767,19 +7360,52 @@ Dit is jouw link voor groep %@!</target> <target>Kleine groepen (max 20)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Soft" xml:space="preserve"> + <source>Soft</source> + <target>Soft</target> + <note>blur media</note> + </trans-unit> + <trans-unit id="Some app settings were not migrated." xml:space="preserve"> + <source>Some app settings were not migrated.</source> + <target>Sommige app-instellingen zijn niet gemigreerd.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some file(s) were not exported:" xml:space="preserve"> + <source>Some file(s) were not exported:</source> + <target>Sommige bestanden zijn niet geëxporteerd:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren - u kunt de Chat console raadplegen voor meer details.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import:" xml:space="preserve"> + <source>Some non-fatal errors occurred during import:</source> + <target>Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some servers failed the test: %@" xml:space="preserve"> + <source>Some servers failed the test: +%@</source> + <target>Sommige servers zijn niet geslaagd voor de test: +%@</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> <target>Iemand</target> <note>notification title</note> </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve"> - <source>Start a new chat</source> - <target>Begin een nieuw gesprek</target> + <trans-unit id="Spam" xml:space="preserve"> + <source>Spam</source> + <target>Spam</target> + <note>blocking reason +report reason</note> + </trans-unit> + <trans-unit id="Square, circle, or anything in between." xml:space="preserve"> + <source>Square, circle, or anything in between.</source> + <target>Vierkant, cirkel of iets daartussenin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start chat" xml:space="preserve"> @@ -4787,11 +7413,26 @@ Dit is jouw link voor groep %@!</target> <target>Begin gesprek</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Start chat?" xml:space="preserve"> + <source>Start chat?</source> + <target>Begin chat?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> <target>Start migratie</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Starting from %@." xml:space="preserve"> + <source>Starting from %@.</source> + <target>Beginnend vanaf %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Statistics" xml:space="preserve"> + <source>Statistics</source> + <target>Statistieken</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> <target>Stop</target> @@ -4802,9 +7443,9 @@ Dit is jouw link voor groep %@!</target> <target>Stop SimpleX</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> - <source>Stop chat to enable database actions</source> - <target>Stop de chat om database acties mogelijk te maken</target> + <trans-unit id="Stop chat" xml:space="preserve"> + <source>Stop chat</source> + <target>Stop chat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> @@ -4835,16 +7476,46 @@ Dit is jouw link voor groep %@!</target> <trans-unit id="Stop sharing" xml:space="preserve"> <source>Stop sharing</source> <target>Stop met delen</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Stop sharing address?" xml:space="preserve"> <source>Stop sharing address?</source> <target>Stop met het delen van adres?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Stopping chat" xml:space="preserve"> + <source>Stopping chat</source> + <target>Chat stoppen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Storage" xml:space="preserve"> + <source>Storage</source> + <target>Opslag</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Strong" xml:space="preserve"> + <source>Strong</source> + <target>Krachtig</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Submit" xml:space="preserve"> <source>Submit</source> - <target>Indienen</target> + <target>Bevestigen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscribed" xml:space="preserve"> + <source>Subscribed</source> + <target>Subscribed</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscription errors" xml:space="preserve"> + <source>Subscription errors</source> + <target>Subscription fouten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscriptions ignored" xml:space="preserve"> + <source>Subscriptions ignored</source> + <target>Subscriptions genegeerd</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Support SimpleX Chat" xml:space="preserve"> @@ -4852,6 +7523,16 @@ Dit is jouw link voor groep %@!</target> <target>Ondersteuning van SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Switch audio and video during the call." xml:space="preserve"> + <source>Switch audio and video during the call.</source> + <target>Wisselen tussen audio en video tijdens het gesprek.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> + <source>Switch chat profile for 1-time invitations.</source> + <target>Wijzig chatprofiel voor eenmalige uitnodigingen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="System" xml:space="preserve"> <source>System</source> <target>Systeem</target> @@ -4862,11 +7543,21 @@ Dit is jouw link voor groep %@!</target> <target>Systeem authenticatie</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP connection" xml:space="preserve"> + <source>TCP connection</source> + <target>TCP verbinding</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> <target>Timeout van TCP-verbinding</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP port for messaging" xml:space="preserve"> + <source>TCP port for messaging</source> + <target>TCP-poort voor berichtenuitwisseling</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <source>TCP_KEEPCNT</source> <target>TCP_KEEPCNT</target> @@ -4882,11 +7573,21 @@ Dit is jouw link voor groep %@!</target> <target>TCP_KEEPINTVL</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tail" xml:space="preserve"> + <source>Tail</source> + <target>Staart</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> <target>Foto nemen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap Create SimpleX address in the menu to create it later." xml:space="preserve"> + <source>Tap Create SimpleX address in the menu to create it later.</source> + <target>Tik op SimpleX-adres maken in het menu om het later te maken.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> <target>Tik op de knop </target> @@ -4894,34 +7595,49 @@ Dit is jouw link voor groep %@!</target> </trans-unit> <trans-unit id="Tap to Connect" xml:space="preserve"> <source>Tap to Connect</source> - <target>Tik om verbinding te maken</target> + <target>Tik hier om verbinding te maken</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to activate profile." xml:space="preserve"> <source>Tap to activate profile.</source> - <target>Tik om profiel te activeren.</target> + <target>Tik hier om profiel te activeren.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to join" xml:space="preserve"> <source>Tap to join</source> - <target>Tik om lid te worden</target> + <target>Tik hier om lid te worden</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to join incognito" xml:space="preserve"> <source>Tap to join incognito</source> - <target>Tik om incognito lid te worden</target> + <target>Tik hier om incognito lid te worden</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve"> - <source>Tap to start a new chat</source> - <target>Tik om een nieuw gesprek te starten</target> + <trans-unit id="Tap to paste link" xml:space="preserve"> + <source>Tap to paste link</source> + <target>Tik hier om de link te plakken</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap to scan" xml:space="preserve"> + <source>Tap to scan</source> + <target>Tik hier om te scannen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Temporary file error" xml:space="preserve"> + <source>Temporary file error</source> + <target>Tijdelijke bestandsfout</target> + <note>file error alert title</note> + </trans-unit> <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> <target>Test mislukt bij stap %@.</target> <note>server test failure</note> </trans-unit> + <trans-unit id="Test notifications" xml:space="preserve"> + <source>Test notifications</source> + <target>Testmeldingen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> <target>Server test</target> @@ -4935,7 +7651,7 @@ Dit is jouw link voor groep %@!</target> <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> <target>Testen mislukt!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> @@ -4952,11 +7668,6 @@ Dit is jouw link voor groep %@!</target> <target>Dank aan de gebruikers – draag bij via Weblate!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> - <target>Het eerste platform zonder gebruikers-ID's, privé door ontwerp.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <source>The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised.</source> @@ -4969,11 +7680,31 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</target> <target>De app kan u op de hoogte stellen wanneer u berichten of contact verzoeken ontvangt - open de instellingen om dit in te schakelen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The app protects your privacy by using different operators in each conversation." xml:space="preserve"> + <source>The app protects your privacy by using different operators in each conversation.</source> + <target>De app beschermt uw privacy door in elk gesprek andere operatoren te gebruiken.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve"> + <source>The app will ask to confirm downloads from unknown file servers (except .onion).</source> + <target>De app vraagt om downloads van onbekende bestandsservers (behalve .onion) te bevestigen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> <source>The attempt to change database passphrase was not completed.</source> <target>De poging om het wachtwoord van de database te wijzigen is niet voltooid.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve"> + <source>The code you scanned is not a SimpleX link QR code.</source> + <target>De code die u heeft gescand is geen SimpleX link QR-code.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The connection reached the limit of undelivered messages, your contact may be offline." xml:space="preserve"> + <source>The connection reached the limit of undelivered messages, your contact may be offline.</source> + <target>De verbinding heeft de limiet van niet-afgeleverde berichten bereikt. Uw contactpersoon is mogelijk offline.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> <target>De door u geaccepteerde verbinding wordt geannuleerd!</target> @@ -4994,6 +7725,11 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</target> <target>De versleuteling werkt en de nieuwe versleutelingsovereenkomst is niet vereist. Dit kan leiden tot verbindingsfouten!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> + <target>De volgende generatie privéberichten</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The hash of the previous message is different." xml:space="preserve"> <source>The hash of the previous message is different.</source> <target>De hash van het vorige bericht is anders.</target> @@ -5009,9 +7745,14 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</target> <target>Het bericht wordt gemarkeerd als gemodereerd voor alle leden.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> - <target>De volgende generatie privéberichten</target> + <trans-unit id="The messages will be deleted for all members." xml:space="preserve"> + <source>The messages will be deleted for all members.</source> + <target>De berichten worden voor alle leden verwijderd.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve"> + <source>The messages will be marked as moderated for all members.</source> + <target>De berichten worden voor alle leden als gemodereerd gemarkeerd.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> @@ -5019,9 +7760,14 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</target> <target>De oude database is niet verwijderd tijdens de migratie, deze kan worden verwijderd.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> - <target>Het profiel wordt alleen gedeeld met uw contacten.</target> + <trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve"> + <source>The same conditions will apply to operator **%@**.</source> + <target>Dezelfde voorwaarden gelden voor operator **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second preset operator in the app!" xml:space="preserve"> + <source>The second preset operator in the app!</source> + <target>De tweede vooraf ingestelde operator in de app!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> @@ -5036,22 +7782,32 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</target> </trans-unit> <trans-unit id="The servers for new connections of your current chat profile **%@**." xml:space="preserve"> <source>The servers for new connections of your current chat profile **%@**.</source> - <target>De servers voor nieuwe verbindingen van uw huidige chat profiel **%@**.</target> + <target>De servers voor nieuwe verbindingen van uw huidige chatprofiel **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Theme" xml:space="preserve"> - <source>Theme</source> - <target>Thema</target> + <trans-unit id="The servers for new files of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new files of your current chat profile **%@**.</source> + <target>De servers voor nieuwe bestanden van uw huidige chatprofiel **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve"> - <source>There should be at least one user profile.</source> - <target>Er moet ten minste één gebruikers profiel zijn.</target> + <trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve"> + <source>The text you pasted is not a SimpleX link.</source> + <target>De tekst die u hebt geplakt is geen SimpleX link.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> - <source>There should be at least one visible user profile.</source> - <target>Er moet ten minste één zichtbaar gebruikers profiel zijn.</target> + <trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve"> + <source>The uploaded database archive will be permanently removed from the servers.</source> + <target>Het geüploade databasearchief wordt permanent van de servers verwijderd.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Themes" xml:space="preserve"> + <source>Themes</source> + <target>Thema's</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="These conditions will also apply for: **%@**." xml:space="preserve"> + <source>These conditions will also apply for: **%@**.</source> + <target>Deze voorwaarden zijn ook van toepassing op: **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> @@ -5074,14 +7830,34 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</target> <target>Deze actie kan niet ongedaan worden gemaakt, de berichten die eerder zijn verzonden en ontvangen dan geselecteerd, worden verwijderd. Het kan enkele minuten duren.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</source> + <target>Deze actie kan niet ongedaan worden gemaakt. De berichten die eerder in deze chat zijn verzonden en ontvangen dan geselecteerd, worden verwijderd.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> - <target>Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren.</target> + <target>Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan definitief verloren.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by end-to-end encryption.</source> + <target>Deze chat is beveiligd met end-to-end codering.</target> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by quantum resistant end-to-end encryption.</source> + <target>Deze chat wordt beschermd door quantum bestendige end-to-end codering.</target> + <note>E2EE info chat item</note> + </trans-unit> <trans-unit id="This device name" xml:space="preserve"> <source>This device name</source> - <target>Deze apparaatnaam</target> + <target>Naam van dit apparaat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve"> + <source>This display name is invalid. Please choose another name.</source> + <target>Deze weergavenaam is ongeldig. Kies een andere naam.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> @@ -5104,9 +7880,28 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</target> <target>Dit is uw eigen eenmalige link!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve"> + <source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve"> + <source>This link was used with another mobile device, please create a new link on the desktop.</source> + <target>Deze link is gebruikt met een ander mobiel apparaat. Maak een nieuwe link op de desktop.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This message was deleted or not received yet." xml:space="preserve"> + <source>This message was deleted or not received yet.</source> + <target>Dit bericht is verwijderd of nog niet ontvangen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <source>This setting applies to messages in your current chat profile **%@**.</source> - <target>Deze instelling is van toepassing op berichten in je huidige chat profiel **%@**.</target> + <target>Deze instelling is van toepassing op berichten in je huidige chatprofiel **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Title" xml:space="preserve"> + <source>Title</source> + <target>Titel</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> @@ -5129,9 +7924,9 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</target> <target>Om een nieuwe verbinding te maken</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <target>Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers-ID's die door alle andere platforms worden gebruikt, ID's voor berichten wachtrijen, afzonderlijk voor elk van uw contacten.</target> + <trans-unit id="To protect against your link being replaced, you can compare contact security codes." xml:space="preserve"> + <source>To protect against your link being replaced, you can compare contact security codes.</source> + <target>Om te voorkomen dat uw link wordt vervangen, kunt u contactbeveiligingscodes vergelijken.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -5139,6 +7934,11 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</target> <target>Om de tijdzone te beschermen, gebruiken afbeeldings-/spraakbestanden UTC.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve"> + <source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source> + <target>Om uw IP-adres te beschermen, gebruikt privéroutering uw SMP-servers om berichten te bezorgen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> @@ -5146,6 +7946,26 @@ You will be prompted to complete authentication before this feature is enabled.< U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingeschakeld.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> + <target>Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers-ID's die door alle andere platforms worden gebruikt, ID's voor berichten wachtrijen, afzonderlijk voor elk van uw contacten.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To receive" xml:space="preserve"> + <source>To receive</source> + <target>Om te ontvangen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve"> + <source>To record speech please grant permission to use Microphone.</source> + <target>Geef toestemming om de microfoon te gebruiken om spraak op te nemen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record video please grant permission to use Camera." xml:space="preserve"> + <source>To record video please grant permission to use Camera.</source> + <target>Om video op te nemen, dient u toestemming te geven om de camera te gebruiken.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> <target>Geef toestemming om de microfoon te gebruiken om een spraakbericht op te nemen.</target> @@ -5153,7 +7973,12 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc </trans-unit> <trans-unit id="To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." xml:space="preserve"> <source>To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page.</source> - <target>Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoek veld in op de pagina **Uw chat profielen**.</target> + <target>Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoek veld in op de pagina **Uw chatprofielen**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To send" xml:space="preserve"> + <source>To send</source> + <target>Om te verzenden</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> @@ -5161,21 +7986,51 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc <target>Om directe push meldingen te ondersteunen, moet de chat database worden gemigreerd.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To use the servers of **%@**, accept conditions of use." xml:space="preserve"> + <source>To use the servers of **%@**, accept conditions of use.</source> + <target>Om de servers van **%@** te gebruiken, moet u de gebruiksvoorwaarden accepteren.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> <target>Vergelijk (of scan) de code op uw apparaten om end-to-end-codering met uw contact te verifiëren.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle chat list:" xml:space="preserve"> + <source>Toggle chat list:</source> + <target>Chatlijst wisselen:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> <source>Toggle incognito when connecting.</source> <target>Schakel incognito in tijdens het verbinden.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Token status: %@." xml:space="preserve"> + <source>Token status: %@.</source> + <target>Tokenstatus: %@.</target> + <note>token status</note> + </trans-unit> + <trans-unit id="Toolbar opacity" xml:space="preserve"> + <source>Toolbar opacity</source> + <target>De transparantie van de werkbalk</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Total" xml:space="preserve"> + <source>Total</source> + <target>Totaal</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>Transport isolation</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Transport sessions" xml:space="preserve"> + <source>Transport sessions</source> + <target>Transportsessies</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> <target>Proberen verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen (fout: %@).</target> @@ -5186,16 +8041,16 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc <target>Proberen verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Turkish interface" xml:space="preserve"> + <source>Turkish interface</source> + <target>Turkse interface</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> <target>Uitschakelen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve"> - <source>Turn off notifications?</source> - <target>Schakel meldingen uit?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> <target>Zet aan</target> @@ -5211,20 +8066,30 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc <target>Deblokkeren</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock for all" xml:space="preserve"> + <source>Unblock for all</source> + <target>Deblokkeer voor iedereen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member" xml:space="preserve"> <source>Unblock member</source> <target>Lid deblokkeren</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock member for all?" xml:space="preserve"> + <source>Unblock member for all?</source> + <target>Lid voor iedereen deblokkeren?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member?" xml:space="preserve"> <source>Unblock member?</source> <target>Lid deblokkeren?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve"> - <source>Unexpected error: %@</source> - <target>Onverwachte fout: %@</target> - <note>item status description</note> + <trans-unit id="Undelivered messages" xml:space="preserve"> + <source>Undelivered messages</source> + <target>Niet afgeleverde berichten</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -5234,7 +8099,7 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc <trans-unit id="Unfav." xml:space="preserve"> <source>Unfav.</source> <target>Niet fav.</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> @@ -5243,7 +8108,7 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc </trans-unit> <trans-unit id="Unhide chat profile" xml:space="preserve"> <source>Unhide chat profile</source> - <target>Chat profiel zichtbaar maken</target> + <target>Chatprofiel zichtbaar maken</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unhide profile" xml:space="preserve"> @@ -5271,6 +8136,11 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc <target>Onbekende fout</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unknown servers!" xml:space="preserve"> + <source>Unknown servers!</source> + <target>Onbekende servers!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> <target>Schakel de modus Niet storen in om onderbrekingen te voorkomen, tenzij u de iOS-oproepinterface gebruikt.</target> @@ -5306,11 +8176,20 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> <target>Dempen opheffen</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> </trans-unit> <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> <target>Ongelezen</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Unsupported connection link" xml:space="preserve"> + <source>Unsupported connection link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve"> + <source>Up to 100 last messages are sent to new members.</source> + <target>Er worden maximaal 100 laatste berichten naar nieuwe leden verzonden.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update" xml:space="preserve"> @@ -5318,11 +8197,6 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak <target>Update</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> - <source>Update .onion hosts setting?</source> - <target>.onion hosts-instelling updaten?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> <target>Database wachtwoord bijwerken</target> @@ -5333,9 +8207,14 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak <target>Netwerk instellingen bijwerken?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve"> - <source>Update transport isolation mode?</source> - <target>Transportisolatiemodus updaten?</target> + <trans-unit id="Update settings?" xml:space="preserve"> + <source>Update settings?</source> + <target>Instellingen actualiseren?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updated conditions" xml:space="preserve"> + <source>Updated conditions</source> + <target>Bijgewerkte voorwaarden</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> @@ -5343,31 +8222,70 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak <target>Door de instellingen bij te werken, wordt de client opnieuw verbonden met alle servers.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> - <target>Als u deze instelling bijwerkt, wordt de client opnieuw verbonden met alle servers.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Upgrade and open chat" xml:space="preserve"> <source>Upgrade and open chat</source> <target>Upgrade en open chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Upload errors" xml:space="preserve"> + <source>Upload errors</source> + <target>Upload fouten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload failed" xml:space="preserve"> + <source>Upload failed</source> + <target>Upload mislukt</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Upload file" xml:space="preserve"> <source>Upload file</source> <target>Upload bestand</target> <note>server test step</note> </trans-unit> + <trans-unit id="Uploaded" xml:space="preserve"> + <source>Uploaded</source> + <target>Geüpload</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploaded files" xml:space="preserve"> + <source>Uploaded files</source> + <target>Geüploade bestanden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploading archive" xml:space="preserve"> + <source>Uploading archive</source> + <target>Archief uploaden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use %@" xml:space="preserve"> + <source>Use %@</source> + <target>Gebruik %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> <target>Gebruik .onion-hosts</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use SOCKS proxy" xml:space="preserve"> + <source>Use SOCKS proxy</source> + <target>Gebruik SOCKS proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> <target>SimpleX Chat servers gebruiken?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use TCP port %@ when no port is specified." xml:space="preserve"> + <source>Use TCP port %@ when no port is specified.</source> + <target>Gebruik TCP-poort %@ als er geen poort is opgegeven.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve"> + <source>Use TCP port 443 for preset servers only.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> <target>Gebruik chat</target> @@ -5378,6 +8296,16 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak <target>Gebruik het huidige profiel</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use for files" xml:space="preserve"> + <source>Use for files</source> + <target>Gebruik voor bestanden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for messages" xml:space="preserve"> + <source>Use for messages</source> + <target>Gebruik voor berichten</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Gebruik voor nieuwe verbindingen</target> @@ -5398,24 +8326,63 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak <target>Gebruik een nieuw incognitoprofiel</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use only local notifications?" xml:space="preserve"> + <source>Use only local notifications?</source> + <target>Alleen lokale meldingen gebruiken?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve"> + <source>Use private routing with unknown servers when IP address is not protected.</source> + <target>Gebruik privéroutering met onbekende servers wanneer het IP-adres niet beveiligd is.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers." xml:space="preserve"> + <source>Use private routing with unknown servers.</source> + <target>Gebruik privéroutering met onbekende servers.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Gebruik server</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> - <target>Gebruikers profiel</target> + <trans-unit id="Use servers" xml:space="preserve"> + <source>Use servers</source> + <target>Gebruik servers</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> - <target>Het gebruik van .onion-hosts vereist een compatibele VPN-provider.</target> + <trans-unit id="Use short links (BETA)" xml:space="preserve"> + <source>Use short links (BETA)</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app while in the call." xml:space="preserve"> + <source>Use the app while in the call.</source> + <target>Gebruik de app tijdens het gesprek.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app with one hand." xml:space="preserve"> + <source>Use the app with one hand.</source> + <target>Gebruik de app met één hand.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use web port" xml:space="preserve"> + <source>Use web port</source> + <target>Gebruik een webpoort</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="User selection" xml:space="preserve"> + <source>User selection</source> + <target>Gebruikersselectie</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Username" xml:space="preserve"> + <source>Username</source> + <target>Gebruikersnaam</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> <source>Using SimpleX Chat servers.</source> - <target>SimpleX Chat servers gebruiken.</target> + <target>Gebruik SimpleX Chat servers.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify code with desktop" xml:space="preserve"> @@ -5438,6 +8405,16 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak <target>Controleer verbindingen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Verify database passphrase" xml:space="preserve"> + <source>Verify database passphrase</source> + <target>Controleer het wachtwoord van de database</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify passphrase" xml:space="preserve"> + <source>Verify passphrase</source> + <target>Controleer het wachtwoord</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Verify security code" xml:space="preserve"> <source>Verify security code</source> <target>Controleer de beveiligingscode</target> @@ -5473,11 +8450,26 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak <target>Video's en bestanden tot 1 GB</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View conditions" xml:space="preserve"> + <source>View conditions</source> + <target>Bekijk voorwaarden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> <target>Beveiligingscode bekijken</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View updated conditions" xml:space="preserve"> + <source>View updated conditions</source> + <target>Bekijk de bijgewerkte voorwaarden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Visible history" xml:space="preserve"> + <source>Visible history</source> + <target>Zichtbare geschiedenis</target> + <note>chat feature</note> + </trans-unit> <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> <target>Spraak berichten</target> @@ -5485,17 +8477,22 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak </trans-unit> <trans-unit id="Voice messages are prohibited in this chat." xml:space="preserve"> <source>Voice messages are prohibited in this chat.</source> - <target>Spraak berichten zijn verboden in deze chat.</target> + <target>Spraak berichten zijn niet toegestaan in dit gesprek.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> - <target>Spraak berichten zijn verboden in deze groep.</target> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> + <target>Spraak berichten zijn niet toegestaan.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Voice messages not allowed" xml:space="preserve"> + <source>Voice messages not allowed</source> + <target>Spraakberichten niet toegestaan</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> <source>Voice messages prohibited!</source> - <target>Spraak berichten verboden!</target> + <target>Spraak berichten niet toegestaan!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice message…" xml:space="preserve"> @@ -5523,6 +8520,21 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak <target>Wachten op video</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wallpaper accent" xml:space="preserve"> + <source>Wallpaper accent</source> + <target>Achtergrond accent</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper background" xml:space="preserve"> + <source>Wallpaper background</source> + <target>Wallpaper achtergrond</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve"> + <source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source> + <target>Waarschuwing: het starten van de chat op meerdere apparaten wordt niet ondersteund en zal leiden tot mislukte bezorging van berichten</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> <source>Warning: you may lose some data!</source> <target>Waarschuwing: u kunt sommige gegevens verliezen!</target> @@ -5540,7 +8552,12 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak </trans-unit> <trans-unit id="Welcome message" xml:space="preserve"> <source>Welcome message</source> - <target>Welkomst bericht</target> + <target>Welkom bericht</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Welcome message is too long" xml:space="preserve"> + <source>Welcome message is too long</source> + <target>Welkom bericht is te lang</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="What's new" xml:space="preserve"> @@ -5553,9 +8570,14 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak <target>Wanneer beschikbaar</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> - <source>When people request to connect, you can accept or reject it.</source> - <target>Wanneer mensen vragen om verbinding te maken, kunt u dit accepteren of weigeren.</target> + <trans-unit id="When connecting audio and video calls." xml:space="preserve"> + <source>When connecting audio and video calls.</source> + <target>Bij het verbinden van audio- en video-oproepen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When more than one operator is enabled, none of them has metadata to learn who communicates with whom." xml:space="preserve"> + <source>When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</source> + <target>Wanneer er meer dan één operator is ingeschakeld, beschikt geen enkele operator over metagegevens om te achterhalen wie met wie communiceert.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> @@ -5563,29 +8585,74 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak <target>Wanneer je een incognito profiel met iemand deelt, wordt dit profiel gebruikt voor de groepen waarvoor ze je uitnodigen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="WiFi" xml:space="preserve"> + <source>WiFi</source> + <target>Wifi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Will be enabled in direct chats!" xml:space="preserve"> + <source>Will be enabled in direct chats!</source> + <target>Wordt ingeschakeld in directe chats!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wired ethernet" xml:space="preserve"> + <source>Wired ethernet</source> + <target>Bekabeld Ethernet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With encrypted files and media." xml:space="preserve"> + <source>With encrypted files and media.</source> + <target>‐Met versleutelde bestanden en media.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> - <target>Met optioneel welkomst bericht.</target> + <target>Met optioneel welkom bericht.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="With reduced battery usage." xml:space="preserve"> + <source>With reduced battery usage.</source> + <target>Met verminderd batterijgebruik.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to file servers.</source> + <target>Zonder Tor of VPN is uw IP-adres zichtbaar voor bestandsservers.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source> + <target>Zonder Tor of VPN zal uw IP-adres zichtbaar zijn voor deze XFTP-relays: %@.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> <target>Verkeerd wachtwoord voor de database</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve"> + <source>Wrong key or unknown connection - most likely this connection is deleted.</source> + <target>Verkeerde sleutel of onbekende verbinding - hoogstwaarschijnlijk is deze verbinding verwijderd.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Wrong key or unknown file chunk address - most likely file is deleted." xml:space="preserve"> + <source>Wrong key or unknown file chunk address - most likely file is deleted.</source> + <target>Verkeerde sleutel of onbekend bestanddeeladres - hoogstwaarschijnlijk is het bestand verwijderd.</target> + <note>file error text</note> + </trans-unit> <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> <target>Verkeerd wachtwoord!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="XFTP servers" xml:space="preserve"> - <source>XFTP servers</source> - <target>XFTP servers</target> + <trans-unit id="XFTP server" xml:space="preserve"> + <source>XFTP server</source> + <target>XFTP server</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> - <target>Jij</target> + <trans-unit id="You **must not** use the same database on two devices." xml:space="preserve"> + <source>You **must not** use the same database on two devices.</source> + <target>U **mag** niet dezelfde database op twee apparaten gebruiken.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You accepted connection" xml:space="preserve"> @@ -5600,7 +8667,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak </trans-unit> <trans-unit id="You already have a chat profile with the same display name. Please choose another name." xml:space="preserve"> <source>You already have a chat profile with the same display name. Please choose another name.</source> - <target>Je hebt al een chat profiel met dezelfde weergave naam. Kies een andere naam.</target> + <target>Je hebt al een chatprofiel met dezelfde weergave naam. Kies een andere naam.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already connected to %@." xml:space="preserve"> @@ -5608,6 +8675,11 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak <target>U bent al verbonden met %@.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are already connected with %@." xml:space="preserve"> + <source>You are already connected with %@.</source> + <target>U bent al verbonden met %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You are already connecting to %@." xml:space="preserve"> <source>You are already connecting to %@.</source> <target>U maakt al verbinding met %@.</target> @@ -5655,14 +8727,24 @@ Deelnameverzoek herhalen?</target> <target>Je bent uitgenodigd voor de groep</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are not connected to these servers. Private routing is used to deliver messages to them." xml:space="preserve"> + <source>You are not connected to these servers. Private routing is used to deliver messages to them.</source> + <target>U bent niet verbonden met deze servers. Privéroutering wordt gebruikt om berichten bij hen af te leveren.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> <source>You can accept calls from lock screen, without device and app authentication.</source> <target>U kunt oproepen van het vergrendelingsscherm accepteren, zonder apparaat- en app-verificatie.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <target>U kunt ook verbinding maken door op de link te klikken. Als het in de browser wordt geopend, klikt u op de knop **Openen in mobiele app**.</target> + <trans-unit id="You can change it in Appearance settings." xml:space="preserve"> + <source>You can change it in Appearance settings.</source> + <target>U kunt dit wijzigen in de instellingen onder uiterlijk.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can configure servers via settings." xml:space="preserve"> + <source>You can configure servers via settings.</source> + <target>U kunt servers configureren via instellingen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can create it later" xml:space="preserve"> @@ -5680,16 +8762,36 @@ Deelnameverzoek herhalen?</target> <target>U kunt ze later inschakelen via de privacy- en beveiligingsinstellingen van de app.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can give another try." xml:space="preserve"> + <source>You can give another try.</source> + <target>Je kunt het nog een keer proberen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <source>You can hide or mute a user profile - swipe it to the right.</source> <target>U kunt een gebruikers profiel verbergen of dempen - veeg het naar rechts.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> + <trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve"> + <source>You can make it visible to your SimpleX contacts via Settings.</source> + <target>Je kunt het via Instellingen zichtbaar maken voor je SimpleX contacten.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now chat with %@" xml:space="preserve"> + <source>You can now chat with %@</source> <target>Je kunt nu berichten sturen naar %@</target> <note>notification body</note> </trans-unit> + <trans-unit id="You can send messages to %@ from Archived contacts." xml:space="preserve"> + <source>You can send messages to %@ from Archived contacts.</source> + <target>U kunt berichten naar %@ sturen vanuit gearchiveerde contacten.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can set connection name, to remember who the link was shared with." xml:space="preserve"> + <source>You can set connection name, to remember who the link was shared with.</source> + <target>U kunt een verbindingsnaam instellen, zodat u kunt onthouden met wie de link is gedeeld.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> <target>U kunt een voorbeeld van een melding op het vergrendeld scherm instellen via instellingen.</target> @@ -5705,16 +8807,16 @@ Deelnameverzoek herhalen?</target> <target>U kunt dit adres delen met uw contacten om hen verbinding te laten maken met **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> - <source>You can share your address as a link or QR code - anybody can connect to you.</source> - <target>U kunt uw adres delen als een link of als een QR-code. Iedereen kan verbinding met u maken.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> <target>U kunt de chat starten via app Instellingen / Database of door de app opnieuw op te starten</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve"> + <source>You can still view conversation with %@ in the list of chats.</source> + <target>Je kunt het gesprek met %@ nog steeds bekijken in de lijst met chats.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> <target>Je kunt SimpleX Vergrendeling aanzetten via Instellingen.</target> @@ -5725,21 +8827,26 @@ Deelnameverzoek herhalen?</target> <target>U kunt markdown gebruiken voor opmaak in berichten:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can view invitation link again in connection details." xml:space="preserve"> + <source>You can view invitation link again in connection details.</source> + <target>U kunt de uitnodigingslink opnieuw bekijken in de verbindingsdetails.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> <target>Je kunt geen berichten versturen!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <target>U bepaalt via welke server(s) de berichten **ontvangen**, uw contacten de servers die u gebruikt om ze berichten te sturen.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <target>U kon niet worden geverifieerd; probeer het opnieuw.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> + <target>Jij bepaalt wie er verbinding mag maken.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You have already requested connection via this address!" xml:space="preserve"> <source>You have already requested connection via this address!</source> <target>U heeft al een verbinding aangevraagd via dit adres!</target> @@ -5752,11 +8859,6 @@ Repeat connection request?</source> Verbindingsverzoek herhalen?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> - <target>Je hebt geen gesprekken</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> <target>U moet elke keer dat de app start het wachtwoord invoeren, deze wordt niet op het apparaat opgeslagen.</target> @@ -5777,11 +8879,26 @@ Verbindingsverzoek herhalen?</target> <target>Je bent lid geworden van deze groep. Verbinding maken met uitnodigend groepslid.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You may migrate the exported database." xml:space="preserve"> + <source>You may migrate the exported database.</source> + <target>U kunt de geëxporteerde database migreren.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may save the exported archive." xml:space="preserve"> + <source>You may save the exported archive.</source> + <target>U kunt het geëxporteerde archief opslaan.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> <target>U mag ALLEEN de meest recente versie van uw chat database op één apparaat gebruiken, anders ontvangt u mogelijk geen berichten meer van sommige contacten.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You need to allow your contact to call to be able to call them." xml:space="preserve"> + <source>You need to allow your contact to call to be able to call them.</source> + <target>U moet uw contactpersoon toestemming geven om te bellen, zodat hij/zij je kan bellen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> <target>U moet uw contact toestemming geven om spraak berichten te verzenden om ze te kunnen verzenden.</target> @@ -5797,6 +8914,11 @@ Verbindingsverzoek herhalen?</target> <target>Je hebt een groep uitnodiging verzonden</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You should receive notifications." xml:space="preserve"> + <source>You should receive notifications.</source> + <target>U zou meldingen moeten ontvangen.</target> + <note>token info</note> + </trans-unit> <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> <target>Je wordt verbonden met de groep wanneer het apparaat van de groep host online is, even geduld a.u.b. of controleer het later!</target> @@ -5832,6 +8954,11 @@ Verbindingsverzoek herhalen?</target> <target>U ontvangt nog steeds oproepen en meldingen van gedempte profielen wanneer deze actief zijn.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You will stop receiving messages from this chat. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this chat. Chat history will be preserved.</source> + <target>U ontvangt geen berichten meer van deze chat. De chatgeschiedenis blijft bewaard.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> <target>Je ontvangt geen berichten meer van deze groep. Je gesprek geschiedenis blijft behouden.</target> @@ -5852,31 +8979,16 @@ Verbindingsverzoek herhalen?</target> <target>Je gebruikt een incognito profiel voor deze groep. Om te voorkomen dat je je hoofdprofiel deelt, is het niet toegestaan om contacten uit te nodigen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your %@ servers" xml:space="preserve"> - <source>Your %@ servers</source> - <target>Uw %@ servers</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your ICE servers" xml:space="preserve"> <source>Your ICE servers</source> <target>Uw ICE servers</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> - <target>Uw SMP servers</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your SimpleX address" xml:space="preserve"> <source>Your SimpleX address</source> <target>Uw SimpleX adres</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your XFTP servers" xml:space="preserve"> - <source>Your XFTP servers</source> - <target>Uw XFTP servers</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your calls" xml:space="preserve"> <source>Your calls</source> <target>Uw oproepen</target> @@ -5892,16 +9004,19 @@ Verbindingsverzoek herhalen?</target> <target>Uw chat database is niet versleuteld, stel een wachtwoord in om deze te versleutelen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your chat preferences" xml:space="preserve"> + <source>Your chat preferences</source> + <target>Uw chat voorkeuren</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>Uw chat profielen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> - <target>Uw contact moet online zijn om de verbinding te voltooien. -U kunt deze verbinding verbreken en het contact verwijderen en later proberen met een nieuwe link.</target> + <trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve"> + <source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source> + <target>Uw verbinding is verplaatst naar %@, maar er is een onverwachte fout opgetreden tijdens het omleiden naar het profiel.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> @@ -5914,18 +9029,16 @@ U kunt deze verbinding verbreken en het contact verwijderen en later proberen me <target>Uw contacten kunnen volledige verwijdering van berichten toestaan.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> - <source>Your contacts in SimpleX will see it. -You can change it in Settings.</source> - <target>Uw contacten in SimpleX kunnen het zien. -U kunt dit wijzigen in Instellingen.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your contacts will remain connected." xml:space="preserve"> <source>Your contacts will remain connected.</source> <target>Uw contacten blijven verbonden.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve"> + <source>Your credentials may be sent unencrypted.</source> + <target>Uw inloggegevens worden mogelijk niet-versleuteld verzonden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> <target>Uw huidige chat database wordt VERWIJDERD en VERVANGEN door de geïmporteerde.</target> @@ -5956,33 +9069,36 @@ U kunt dit wijzigen in Instellingen.</target> <target>Uw profiel **%@** wordt gedeeld.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> - <target>Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten. -SimpleX servers kunnen uw profiel niet zien.</target> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> + <target>Het profiel wordt alleen gedeeld met uw contacten.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> - <target>Uw profiel, contacten en afgeleverde berichten worden op uw apparaat opgeslagen.</target> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> + <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> + <target>Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten. SimpleX servers kunnen uw profiel niet zien.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve"> + <source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source> + <target>Je profiel is gewijzigd. Als je het opslaat, wordt het bijgewerkte profiel naar al je contacten verzonden.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> <target>Je willekeurige profiel</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> - <target>Uw server</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> <target>Uw server adres</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your servers" xml:space="preserve"> + <source>Your servers</source> + <target>Uw servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your settings" xml:space="preserve"> <source>Your settings</source> <target>Uw instellingen</target> @@ -6023,11 +9139,21 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>geaccepteerde oproep</target> <note>call status</note> </trans-unit> + <trans-unit id="accepted invitation" xml:space="preserve"> + <source>accepted invitation</source> + <target>geaccepteerde uitnodiging</target> + <note>chat list item title</note> + </trans-unit> <trans-unit id="admin" xml:space="preserve"> <source>admin</source> <target>Beheerder</target> <note>member role</note> </trans-unit> + <trans-unit id="admins" xml:space="preserve"> + <source>admins</source> + <target>beheerders</target> + <note>feature role</note> + </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> <target>versleuteling overeenkomen voor %@…</target> @@ -6038,6 +9164,11 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>versleuteling overeenkomen…</target> <note>chat item text</note> </trans-unit> + <trans-unit id="all members" xml:space="preserve"> + <source>all members</source> + <target>alle leden</target> + <note>feature role</note> + </trans-unit> <trans-unit id="always" xml:space="preserve"> <source>always</source> <target>altijd</target> @@ -6048,6 +9179,16 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>en %lld andere gebeurtenissen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="archived report" xml:space="preserve"> + <source>archived report</source> + <target>gearchiveerd rapport</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="attempts" xml:space="preserve"> + <source>attempts</source> + <target>pogingen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <source>audio call (not e2e encrypted)</source> <target>audio oproep (niet e2e versleuteld)</target> @@ -6071,13 +9212,29 @@ SimpleX servers kunnen uw profiel niet zien.</target> <trans-unit id="blocked" xml:space="preserve"> <source>blocked</source> <target>geblokkeerd</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="blocked %@" xml:space="preserve"> + <source>blocked %@</source> + <target>blokkeerde %@</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="blocked by admin" xml:space="preserve"> + <source>blocked by admin</source> + <target>geblokkeerd door beheerder</target> + <note>blocked chat item +marked deleted chat item preview text</note> </trans-unit> <trans-unit id="bold" xml:space="preserve"> <source>bold</source> <target>vetgedrukt</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="call" xml:space="preserve"> + <source>call</source> + <target>bellen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="call error" xml:space="preserve"> <source>call error</source> <target>oproepfout</target> @@ -6181,7 +9338,7 @@ SimpleX servers kunnen uw profiel niet zien.</target> <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> <target>Verbinden…</target> - <note>chat list item title</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> @@ -6193,6 +9350,11 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>verbinding:%@</target> <note>connection information</note> </trans-unit> + <trans-unit id="contact %@ changed to %@" xml:space="preserve"> + <source>contact %1$@ changed to %2$@</source> + <target>contactpersoon %1$@ gewijzigd in %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> <target>contact heeft e2e-codering</target> @@ -6223,10 +9385,16 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>dagen</target> <note>time unit</note> </trans-unit> + <trans-unit id="decryption errors" xml:space="preserve"> + <source>decryption errors</source> + <target>decoderingsfouten</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> <target>standaard (%@)</target> - <note>pref value</note> + <note>delete after time +pref value</note> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> @@ -6273,6 +9441,11 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>dubbel bericht</target> <note>integrity error chat item</note> </trans-unit> + <trans-unit id="duplicates" xml:space="preserve"> + <source>duplicates</source> + <target>duplicaten</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> <target>e2e versleuteld</target> @@ -6348,9 +9521,14 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>fout</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="event happened" xml:space="preserve"> - <source>event happened</source> - <target>gebeurtenis gebeurd</target> + <trans-unit id="expired" xml:space="preserve"> + <source>expired</source> + <target>verlopen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="forwarded" xml:space="preserve"> + <source>forwarded</source> + <target>doorgestuurd</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> @@ -6378,9 +9556,14 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>iOS-keychain wordt gebruikt om het wachtwoord veilig op te slaan nadat u de app opnieuw hebt opgestart of het wachtwoord hebt gewijzigd, hiermee kunt u push meldingen ontvangen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="inactive" xml:space="preserve"> + <source>inactive</source> + <target>inactief</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> - <target>incognito via contactadres link</target> + <target>incognito via contact adres link</target> <note>chat list item description</note> </trans-unit> <trans-unit id="incognito via group link" xml:space="preserve"> @@ -6418,6 +9601,11 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>uitnodiging voor groep %@</target> <note>group name</note> </trans-unit> + <trans-unit id="invite" xml:space="preserve"> + <source>invite</source> + <target>uitnodiging</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> <target>uitgenodigd</target> @@ -6450,7 +9638,7 @@ SimpleX servers kunnen uw profiel niet zien.</target> </trans-unit> <trans-unit id="left" xml:space="preserve"> <source>left</source> - <target>verlaten</target> + <target>is vertrokken</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="marked deleted" xml:space="preserve"> @@ -6463,11 +9651,21 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>lid</target> <note>member role</note> </trans-unit> + <trans-unit id="member %@ changed to %@" xml:space="preserve"> + <source>member %1$@ changed to %2$@</source> + <target>lid %1$@ gewijzigd in %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> <target>is toegetreden</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="message" xml:space="preserve"> + <source>message</source> + <target>bericht</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="message received" xml:space="preserve"> <source>message received</source> <target>bericht ontvangen</target> @@ -6491,7 +9689,12 @@ SimpleX servers kunnen uw profiel niet zien.</target> <trans-unit id="moderated by %@" xml:space="preserve"> <source>moderated by %@</source> <target>gemodereerd door %@</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="moderator" xml:space="preserve"> + <source>moderator</source> + <target>moderator</target> + <note>member role</note> </trans-unit> <trans-unit id="months" xml:space="preserve"> <source>months</source> @@ -6501,7 +9704,7 @@ SimpleX servers kunnen uw profiel niet zien.</target> <trans-unit id="never" xml:space="preserve"> <source>never</source> <target>nooit</target> - <note>No comment provided by engineer.</note> + <note>delete after time</note> </trans-unit> <trans-unit id="new message" xml:space="preserve"> <source>new message</source> @@ -6532,8 +9735,8 @@ SimpleX servers kunnen uw profiel niet zien.</target> <source>off</source> <target>uit</target> <note>enabled status - group pref value - time to disappear</note> +group pref value +time to disappear</note> </trans-unit> <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> @@ -6550,16 +9753,46 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>aan</target> <note>group pref value</note> </trans-unit> + <trans-unit id="other" xml:space="preserve"> + <source>other</source> + <target>overig</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="other errors" xml:space="preserve"> + <source>other errors</source> + <target>overige fouten</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="owner" xml:space="preserve"> <source>owner</source> <target>Eigenaar</target> <note>member role</note> </trans-unit> + <trans-unit id="owners" xml:space="preserve"> + <source>owners</source> + <target>eigenaren</target> + <note>feature role</note> + </trans-unit> <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> <target>peer-to-peer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="pending" xml:space="preserve"> + <source>pending</source> + <target>In behandeling</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="pending approval" xml:space="preserve"> + <source>pending approval</source> + <target>in afwachting van goedkeuring</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="quantum resistant e2e encryption" xml:space="preserve"> + <source>quantum resistant e2e encryption</source> + <target>quantum bestendige e2e-codering</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> <target>antwoord gekregen…</target> @@ -6570,6 +9803,11 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>bevestiging ontvangen…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="rejected" xml:space="preserve"> + <source>rejected</source> + <target>afgewezen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> <target>geweigerde oproep</target> @@ -6585,11 +9823,41 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>verwijderd %@</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="removed contact address" xml:space="preserve"> + <source>removed contact address</source> + <target>contactadres verwijderd</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="removed profile picture" xml:space="preserve"> + <source>removed profile picture</source> + <target>profielfoto verwijderd</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> <target>heeft je verwijderd</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="requested to connect" xml:space="preserve"> + <source>requested to connect</source> + <target>verzocht om verbinding te maken</target> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="saved" xml:space="preserve"> + <source>saved</source> + <target>opgeslagen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="saved from %@" xml:space="preserve"> + <source>saved from %@</source> + <target>opgeslagen van %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="search" xml:space="preserve"> + <source>search</source> + <target>zoekopdracht</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="sec" xml:space="preserve"> <source>sec</source> <target>sec</target> @@ -6615,6 +9883,30 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>stuur een direct bericht</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve"> + <source>server queue info: %1$@ + +last received msg: %2$@</source> + <target>informatie over serverwachtrij: %1$@ + +laatst ontvangen bericht: %2$@</target> + <note>queue info</note> + </trans-unit> + <trans-unit id="set new contact address" xml:space="preserve"> + <source>set new contact address</source> + <target>nieuw contactadres instellen</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="set new profile picture" xml:space="preserve"> + <source>set new profile picture</source> + <target>nieuwe profielfoto</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="standard end-to-end encryption" xml:space="preserve"> + <source>standard end-to-end encryption</source> + <target>standaard end-to-end encryptie</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>beginnen…</target> @@ -6630,16 +9922,41 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>dit contact</target> <note>notification title</note> </trans-unit> + <trans-unit id="unblocked %@" xml:space="preserve"> + <source>unblocked %@</source> + <target>gedeblokkeerd %@</target> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="unknown" xml:space="preserve"> <source>unknown</source> <target>onbekend</target> <note>connection info</note> </trans-unit> + <trans-unit id="unknown servers" xml:space="preserve"> + <source>unknown servers</source> + <target>onbekende relays</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unknown status" xml:space="preserve"> + <source>unknown status</source> + <target>onbekende status</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unprotected" xml:space="preserve"> + <source>unprotected</source> + <target>onbeschermd</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="updated group profile" xml:space="preserve"> <source>updated group profile</source> <target>bijgewerkt groep profiel</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="updated profile" xml:space="preserve"> + <source>updated profile</source> + <target>bijgewerkt profiel</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="v%@" xml:space="preserve"> <source>v%@</source> <target>v%@</target> @@ -6652,7 +9969,7 @@ SimpleX servers kunnen uw profiel niet zien.</target> </trans-unit> <trans-unit id="via contact address link" xml:space="preserve"> <source>via contact address link</source> - <target>via contactadres link</target> + <target>via contact adres link</target> <note>chat list item description</note> </trans-unit> <trans-unit id="via group link" xml:space="preserve"> @@ -6667,7 +9984,12 @@ SimpleX servers kunnen uw profiel niet zien.</target> </trans-unit> <trans-unit id="via relay" xml:space="preserve"> <source>via relay</source> - <target>via relais</target> + <target>via relay</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="video" xml:space="preserve"> + <source>video</source> + <target>video</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> @@ -6695,11 +10017,21 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>weken</target> <note>time unit</note> </trans-unit> + <trans-unit id="when IP hidden" xml:space="preserve"> + <source>when IP hidden</source> + <target>wanneer IP verborgen is</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="yes" xml:space="preserve"> <source>yes</source> <target>Ja</target> <note>pref value</note> </trans-unit> + <trans-unit id="you" xml:space="preserve"> + <source>you</source> + <target>jij</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> <target>je bent uitgenodigd voor de groep</target> @@ -6707,9 +10039,14 @@ SimpleX servers kunnen uw profiel niet zien.</target> </trans-unit> <trans-unit id="you are observer" xml:space="preserve"> <source>you are observer</source> - <target>jij bent waarnemer</target> + <target>je bent waarnemer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="you blocked %@" xml:space="preserve"> + <source>you blocked %@</source> + <target>je hebt %@ geblokkeerd</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> <target>je bent van adres veranderd</target> @@ -6732,7 +10069,7 @@ SimpleX servers kunnen uw profiel niet zien.</target> </trans-unit> <trans-unit id="you left" xml:space="preserve"> <source>you left</source> - <target>jij bent vertrokken</target> + <target>je bent vertrokken</target> <note>snd group event chat item</note> </trans-unit> <trans-unit id="you removed %@" xml:space="preserve"> @@ -6750,6 +10087,11 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>je hebt een eenmalige link incognito gedeeld</target> <note>chat list item description</note> </trans-unit> + <trans-unit id="you unblocked %@" xml:space="preserve"> + <source>you unblocked %@</source> + <target>je hebt %@ gedeblokkeerd</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you: " xml:space="preserve"> <source>you: </source> <target>Jij: </target> @@ -6764,7 +10106,7 @@ SimpleX servers kunnen uw profiel niet zien.</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="nl" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6801,7 +10143,7 @@ SimpleX servers kunnen uw profiel niet zien.</target> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="nl" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> @@ -6821,4 +10163,249 @@ SimpleX servers kunnen uw profiel niet zien.</target> </trans-unit> </body> </file> + <file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="nl" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%d new events" xml:space="preserve"> + <source>%d new events</source> + <target>‐%d nieuwe gebeurtenissen</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="From %d chat(s)" xml:space="preserve"> + <source>From %d chat(s)</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From: %@" xml:space="preserve"> + <source>From: %@</source> + <target>Van: %@</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>Nieuwe gebeurtenissen</target> + <note>notification</note> + </trans-unit> + <trans-unit id="New messages" xml:space="preserve"> + <source>New messages</source> + <target>Nieuwe berichten</target> + <note>notification</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="nl" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle display name</note> + </trans-unit> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> + <source>Copyright © 2024 SimpleX Chat. All rights reserved.</source> + <target>Copyright © 2024 SimpleX Chat. All rights reserved.</target> + <note>Copyright (human-readable)</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="nl" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <target>%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App is locked!" xml:space="preserve"> + <source>App is locked!</source> + <target>App is vergrendeld!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <target>Annuleren</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <target>Kan geen toegang krijgen tot de keychain om het database wachtwoord op te slaan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Kan bericht niet doorsturen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Comment" xml:space="preserve"> + <source>Comment</source> + <target>Opmerking</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <target>De momenteel maximaal ondersteunde bestandsgrootte is %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade required" xml:space="preserve"> + <source>Database downgrade required</source> + <target>Database downgrade vereist</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <target>Database versleuteld!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <target>Database fout</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <target>Het wachtwoord van de database verschilt van het wachtwoord die in de keychain is opgeslagen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <target>Database wachtwoord is vereist om je chats te openen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade required" xml:space="preserve"> + <source>Database upgrade required</source> + <target>Database upgrade vereist</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing file" xml:space="preserve"> + <source>Error preparing file</source> + <target>Fout bij voorbereiden bestand</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing message" xml:space="preserve"> + <source>Error preparing message</source> + <target>Fout bij het voorbereiden van bericht</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <target>Fout: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Bestandsfout</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <target>Incompatibele database versie</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Ongeldige migratie bevestiging</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <target>Keychain fout</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <target>Groot bestand!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No active profile" xml:space="preserve"> + <source>No active profile</source> + <target>Geen actief profiel</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <target>Ok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to downgrade the database." xml:space="preserve"> + <source>Open the app to downgrade the database.</source> + <target>Open de app om de database te downgraden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to upgrade the database." xml:space="preserve"> + <source>Open the app to upgrade the database.</source> + <target>Open de app om de database te upgraden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passphrase" xml:space="preserve"> + <source>Passphrase</source> + <target>Wachtwoord</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve"> + <source>Please create a profile in the SimpleX app</source> + <target>Maak een profiel aan in de SimpleX app</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Geselecteerde chat voorkeuren verbieden dit bericht.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending a message takes longer than expected." xml:space="preserve"> + <source>Sending a message takes longer than expected.</source> + <target>Het verzenden van een bericht duurt langer dan verwacht.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending message…" xml:space="preserve"> + <source>Sending message…</source> + <target>Bericht versturen…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <target>Deel</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Slow network?" xml:space="preserve"> + <source>Slow network?</source> + <target>Traag netwerk?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <target>Onbekende database fout: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unsupported format" xml:space="preserve"> + <source>Unsupported format</source> + <target>Niet ondersteund formaat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wait" xml:space="preserve"> + <source>Wait</source> + <target>wachten</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <target>Verkeerde database wachtwoord</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve"> + <source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source> + <target>U kunt delen toestaan in de instellingen voor Privacy en beveiliging / SimpleX Lock.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + </body> + </file> </xliff> diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/contents.json b/apps/ios/SimpleX Localizations/nl.xcloc/contents.json index 20246f53d4..4b8d468de2 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/nl.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "nl", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "16.2" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index ad1924f4c4..175c8b4112 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -2,36 +2,9 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="pl" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> - <trans-unit id=" " xml:space="preserve"> - <source> -</source> - <target> -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (" xml:space="preserve"> - <source> (</source> - <target> (</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> <target> (można skopiować)</target> @@ -39,7 +12,7 @@ </trans-unit> <trans-unit id="!1 colored!" xml:space="preserve"> <source>!1 colored!</source> - <target>!1 kolorowy!</target> + <target>!1 pokolorowany!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="# %@" xml:space="preserve"> @@ -107,6 +80,11 @@ <target>%@ połączony</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve"> + <source>%@ downloaded</source> + <target>%@ pobrane</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ is connected!" xml:space="preserve"> <source>%@ is connected!</source> <target>%@ jest połączony!</target> @@ -122,9 +100,19 @@ <target>%@ jest zweryfikowany</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ server" xml:space="preserve"> + <source>%@ server</source> + <target>%@ serwer</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ servers" xml:space="preserve"> <source>%@ servers</source> - <target>%@ serwery</target> + <target>%@ serwery/ów</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve"> + <source>%@ uploaded</source> + <target>%@ wgrane</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ wants to connect!" xml:space="preserve"> @@ -132,6 +120,11 @@ <target>%@ chce się połączyć!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve"> + <source>%1$@, %2$@</source> + <target>%1$@, %2$@</target> + <note>format for date separator in chat</note> + </trans-unit> <trans-unit id="%@, %@ and %lld members" xml:space="preserve"> <source>%@, %@ and %lld members</source> <target>%@, %@ i %lld członków</target> @@ -152,11 +145,36 @@ <target>%d dni</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve"> + <source>%d file(s) are still being downloaded.</source> + <target>%d plik(ów) jest dalej pobieranych.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve"> + <source>%d file(s) failed to download.</source> + <target>%d plik(ów) nie udało się pobrać.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve"> + <source>%d file(s) were deleted.</source> + <target>%d plik(ów) zostało usuniętych.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve"> + <source>%d file(s) were not downloaded.</source> + <target>%d plik(ów) nie zostało pobranych.</target> + <note>forward confirmation reason</note> + </trans-unit> <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> <target>%d godzin</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve"> + <source>%d messages not forwarded</source> + <target>%d wiadomości nie przekazanych</target> + <note>alert title</note> + </trans-unit> <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> <target>%d min</target> @@ -172,6 +190,11 @@ <target>%d sek</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve"> + <source>%d seconds(s)</source> + <target>%d sekundach</target> + <note>delete after time</note> + </trans-unit> <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> <target>%d pominięte wiadomość(i)</target> @@ -217,6 +240,11 @@ <target>%lld wiadomości zablokowanych</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve"> + <source>%lld messages blocked by admin</source> + <target>%lld wiadomości zablokowanych przez admina</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld messages marked deleted" xml:space="preserve"> <source>%lld messages marked deleted</source> <target>%lld wiadomości oznaczonych do usunięcia</target> @@ -237,11 +265,6 @@ <target>%lld nowe języki interfejsu</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> - <source>%lld second(s)</source> - <target>%lld sekund(y)</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="%lld seconds" xml:space="preserve"> <source>%lld seconds</source> <target>%lld sekund</target> @@ -292,11 +315,6 @@ <target>%u pominiętych wiadomości.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> - <source>(</source> - <target>(</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="(new)" xml:space="preserve"> <source>(new)</source> <target>(nowy)</target> @@ -307,54 +325,54 @@ <target>(to urządzenie v%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve"> - <source>)</source> - <target>)</target> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target>**Dodaj kontakt**: aby utworzyć nowy link z zaproszeniem lub połączyć się za pomocą otrzymanego linku.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target>**Dodaj nowy kontakt**: aby stworzyć swój jednorazowy kod QR lub link dla kontaktu.</target> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve"> + <source>**Create group**: to create a new group.</source> + <target>**Utwórz grupę**: aby utworzyć nową grupę.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> - <source>**Create link / QR code** for your contact to use.</source> - <target>**Utwórz link / kod QR**, aby Twój kontakt mógł z niego skorzystać.</target> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> + <target>**Bardziej prywatny**: sprawdzanie nowych wiadomości odbywa się co 20 minut. Współdzielony z serwerem SimpleX Chat jest token urządzenia, lecz nie informacje o liczbie kontaktów lub wiadomości.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> - <target>**Bardziej prywatny**: sprawdzanie nowych wiadomości co 20 minut. Token urządzenia jest współdzielony z serwerem SimpleX Chat, ale nie informacje o liczbie kontaktów lub wiadomości.</target> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> + <target>**Najbardziej prywatny**: nie korzystaj z serwera powiadomień SimpleX Chat, wiadomości sprawdzane są co jakiś czas w tle (zależne od tego jak często korzystasz z aplikacji).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> - <target>**Najbardziej prywatny**: nie korzystaj z serwera powiadomień SimpleX Chat, sprawdzaj wiadomości okresowo w tle (zależy jak często korzystasz z aplikacji).</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <target>**Wklej otrzymany link** lub otwórz go w przeglądarce i dotknij **Otwórz w aplikacji mobilnej**.</target> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> + <target>*Uwaga*: w celach bezpieczeństwa użycie tej samej bazy danych na dwóch różnych urządzeniach spowoduje brak możliwości odszyfrowywania wiadomości z Twoich połączeń.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> <source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source> - <target>**Uwaga**: NIE będziesz w stanie odzyskać lub zmienić hasła, jeśli je stracisz.</target> + <target>**Uwaga**: NIE będziesz w stanie odzyskać lub zmienić kodu dostępu, jeśli go stracisz.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> - <target>**Zalecane**: token urządzenia i powiadomienia są wysyłane do serwera powiadomień SimpleX Chat, ale nie treść wiadomości, rozmiar lub od kogo jest.</target> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> + <target>**Zalecane**: do serwera powiadomień SimpleX Chat wysyłany jest token urządzenia i powiadomienia, lecz nie treść wiadomości, jej rozmiar lub od kogo ona jest.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target>**Skanuj kod QR**: aby połączyć się z kontaktem osobiście lub za pomocą połączenia wideo.</target> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve"> + <source>**Scan / Paste link**: to connect via a link you received.</source> + <target>**Zeskanuj / Wklej link**: aby połączyć się za pomocą otrzymanego linku.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> <source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source> - <target>**Uwaga**: Natychmiastowe powiadomienia push wymagają hasła zapisanego w Keychain.</target> + <target>**Uwaga**: Natychmiastowe powiadomienia push wymagają zapisania kodu dostępu w Keychain.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve"> + <source>**Warning**: the archive will be removed.</source> + <target>**Ostrzeżenie**: archiwum zostanie usunięte.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> @@ -372,11 +390,6 @@ <target>\*pogrubiony*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=", " xml:space="preserve"> - <source>, </source> - <target>, </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). @@ -413,11 +426,6 @@ - historia edycji.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="." xml:space="preserve"> - <source>.</source> - <target>.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="0 sec" xml:space="preserve"> <source>0 sec</source> <target>0 sek</target> @@ -431,7 +439,8 @@ <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> <target>1 dzień</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> @@ -446,16 +455,28 @@ <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> <target>1 miesiąc</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> <target>1 tydzień</target> - <note>time interval</note> + <note>delete after time +time interval</note> + </trans-unit> + <trans-unit id="1 year" xml:space="preserve"> + <source>1 year</source> + <target>1 roku</target> + <note>delete after time</note> </trans-unit> <trans-unit id="1-time link" xml:space="preserve"> <source>1-time link</source> - <target>1-razowy link</target> + <target>link jednorazowy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> + <target>Link jednorazowy może być użyty *tylko z jednym kontaktem* - udostępnij go osobiście lub przez dowolny komunikator.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="5 minutes" xml:space="preserve"> @@ -473,11 +494,6 @@ <target>30 sekund</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve"> - <source>: </source> - <target>: </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> @@ -527,31 +543,32 @@ <target>Przerwać zmianę adresu?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve"> - <source>About SimpleX</source> - <target>O SimpleX</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve"> <source>About SimpleX Chat</source> <target>O SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve"> - <source>About SimpleX address</source> - <target>O adresie SimpleX</target> + <trans-unit id="About operators" xml:space="preserve"> + <source>About operators</source> + <target>O operatorach</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accent color" xml:space="preserve"> - <source>Accent color</source> - <target>Kolor akcentu</target> + <trans-unit id="Accent" xml:space="preserve"> + <source>Accent</source> + <target>Akcent</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept" xml:space="preserve"> <source>Accept</source> <target>Akceptuj</target> <note>accept contact request via notification - accept incoming call via notification</note> +accept incoming call via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accept conditions" xml:space="preserve"> + <source>Accept conditions</source> + <target>Zaakceptuj warunki</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept connection request?" xml:space="preserve"> <source>Accept connection request?</source> @@ -566,16 +583,47 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Akceptuj incognito</target> - <note>accept contact request via notification</note> + <note>accept contact request via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve"> + <source>Accepted conditions</source> + <target>Zaakceptowano warunki</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve"> + <source>Acknowledged</source> + <target>Potwierdzono</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve"> + <source>Acknowledgement errors</source> + <target>Błędy potwierdzenia</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Active" xml:space="preserve"> + <source>Active</source> + <target>Aktywne</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve"> + <source>Active connections</source> + <target>Aktywne połączenia</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> <target>Dodaj adres do swojego profilu, aby Twoje kontakty mogły go udostępnić innym osobom. Aktualizacja profilu zostanie wysłana do Twoich kontaktów.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> - <source>Add preset servers</source> - <target>Dodaj gotowe serwery</target> + <trans-unit id="Add friends" xml:space="preserve"> + <source>Add friends</source> + <target>Dodaj znajomych</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve"> + <source>Add list</source> + <target>Dodaj listę</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add profile" xml:space="preserve"> @@ -583,14 +631,19 @@ <target>Dodaj profil</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> + <target>Dodaj serwer</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> <source>Add servers by scanning QR codes.</source> <target>Dodaj serwery, skanując kody QR.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> - <target>Dodaj serwer…</target> + <trans-unit id="Add team members" xml:space="preserve"> + <source>Add team members</source> + <target>Dodaj członków zespołu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -598,11 +651,46 @@ <target>Dodaj do innego urządzenia</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add to list" xml:space="preserve"> + <source>Add to list</source> + <target>Dodaj do listy</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> <target>Dodaj wiadomość powitalną</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve"> + <source>Add your team members to the conversations.</source> + <target>Dodaj członków zespołu do konwersacji.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve"> + <source>Added media & file servers</source> + <target>Dodano serwery multimediów i plików</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve"> + <source>Added message servers</source> + <target>Dodano serwery wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve"> + <source>Additional accent</source> + <target>Dodatkowy akcent</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve"> + <source>Additional accent 2</source> + <target>Dodatkowy akcent 2</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve"> + <source>Additional secondary</source> + <target>Dodatkowy drugorzędny</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Address" xml:space="preserve"> <source>Address</source> <target>Adres</target> @@ -613,6 +701,21 @@ <target>Zmiana adresu zostanie przerwana. Użyty zostanie stary adres odbiorczy.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve"> + <source>Address or 1-time link?</source> + <target>Adres czy jednorazowy link?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve"> + <source>Address settings</source> + <target>Ustawienia adresu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve"> + <source>Admins can block a member for all.</source> + <target>Administratorzy mogą blokować członka dla wszystkich.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> <source>Admins can create the links to join groups.</source> <target>Administratorzy mogą tworzyć linki do dołączania do grup.</target> @@ -623,6 +726,16 @@ <target>Zaawansowane ustawienia sieci</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve"> + <source>Advanced settings</source> + <target>Zaawansowane ustawienia</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All" xml:space="preserve"> + <source>All</source> + <target>Wszystko</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All app data is deleted." xml:space="preserve"> <source>All app data is deleted.</source> <target>Wszystkie dane aplikacji są usunięte.</target> @@ -633,16 +746,36 @@ <target>Wszystkie czaty i wiadomości zostaną usunięte - nie można tego cofnąć!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <target>Wszystkie rozmowy zostaną usunięte z listy %@, a lista usunięta.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> <target>Wszystkie dane są usuwane po jego wprowadzeniu.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve"> + <source>All data is kept private on your device.</source> + <target>Wszystkie dane są prywatne na Twoim urządzeniu.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> <target>Wszyscy członkowie grupy pozostaną połączeni.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <target>Wszystkie wiadomości i pliki są wysyłane **z szyfrowaniem end-to-end**, z bezpieczeństwem postkwantowym w wiadomościach bezpośrednich.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone!</source> + <target>Wszystkie wiadomości zostaną usunięte – nie można tego cofnąć!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> <target>Wszystkie wiadomości zostaną usunięte - nie można tego cofnąć! Wiadomości zostaną usunięte TYLKO dla Ciebie.</target> @@ -653,6 +786,20 @@ <target>Wszystkie nowe wiadomości z %@ zostaną ukryte!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All profiles" xml:space="preserve"> + <source>All profiles</source> + <target>Wszystkie profile</target> + <note>profile dropdown</note> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve"> + <source>All reports will be archived for you.</source> + <target>Wszystkie raporty zostaną dla Ciebie zarchiwizowane.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All servers" xml:space="preserve"> + <source>All servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All your contacts will remain connected." xml:space="preserve"> <source>All your contacts will remain connected.</source> <target>Wszystkie Twoje kontakty pozostaną połączone.</target> @@ -663,6 +810,11 @@ <target>Wszystkie Twoje kontakty pozostaną połączone. Aktualizacja profilu zostanie wysłana do Twoich kontaktów.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target>Wszystkie twoje kontakty, konwersacje i pliki będą bezpiecznie szyfrowane i wgrywane w kawałkach do skonfigurowanych przekaźników XFTP.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow" xml:space="preserve"> <source>Allow</source> <target>Pozwól</target> @@ -673,14 +825,24 @@ <target>Zezwalaj na połączenia tylko wtedy, gdy Twój kontakt na to pozwala.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve"> + <source>Allow calls?</source> + <target>Zezwolić na połączenia?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> <target>Zezwól na znikające wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <target>Zezwalaj na nieodwracalne usuwanie wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli.</target> + <trans-unit id="Allow downgrade" xml:space="preserve"> + <source>Allow downgrade</source> + <target>Zezwól na obniżenie wersji</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target>Zezwalaj na nieodwracalne usuwanie wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli. (24 godziny)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> @@ -703,9 +865,24 @@ <target>Zezwól na wysyłanie znikających wiadomości.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow to irreversibly delete sent messages.</source> - <target>Zezwól na nieodwracalne usunięcie wysłanych wiadomości.</target> + <trans-unit id="Allow sharing" xml:space="preserve"> + <source>Allow sharing</source> + <target>Zezwól na udostępnianie</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target>Zezwól na nieodwracalne usunięcie wysłanych wiadomości. (24 godziny)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve"> + <source>Allow to report messsages to moderators.</source> + <target>Zezwól na zgłaszanie wiadomości moderatorom.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve"> + <source>Allow to send SimpleX links.</source> + <target>Zezwól na wysyłanie linków SimpleX.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send files and media." xml:space="preserve"> @@ -738,9 +915,9 @@ <target>Zezwól swoim kontaktom na połączenia do Ciebie.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow your contacts to irreversibly delete sent messages.</source> - <target>Zezwól swoim kontaktom na nieodwracalne usuwanie wysłanych wiadomości.</target> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target>Zezwól swoim kontaktom na nieodwracalne usuwanie wysłanych wiadomości. (24 godziny)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> @@ -768,6 +945,11 @@ <target>Już dołączono do grupy!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve"> + <source>Always use private routing.</source> + <target>Zawsze używaj prywatnego trasowania.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> <source>Always use relay</source> <target>Zawsze używaj przekaźnika</target> @@ -778,21 +960,41 @@ <target>Tworzony jest pusty profil czatu o podanej nazwie, a aplikacja otwiera się jak zwykle.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Another reason" xml:space="preserve"> + <source>Another reason</source> + <target>Inny powód</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> <target>Odbierz połączenie</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> + <target>Każdy może hostować serwery.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> <target>Kompilacja aplikacji: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App data migration" xml:space="preserve"> + <source>App data migration</source> + <target>Migracja danych aplikacji</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> <source>App encrypts new local files (except videos).</source> <target>Aplikacja szyfruje nowe lokalne pliki (bez filmów).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App group:" xml:space="preserve"> + <source>App group:</source> + <target>Grupa aplikacji:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>Ikona aplikacji</target> @@ -808,6 +1010,11 @@ <target>Pin aplikacji został zastąpiony pinem samozniszczenia.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App session" xml:space="preserve"> + <source>App session</source> + <target>Sesja aplikacji</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App version" xml:space="preserve"> <source>App version</source> <target>Wersja aplikacji</target> @@ -823,6 +1030,66 @@ <target>Wygląd</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Apply" xml:space="preserve"> + <source>Apply</source> + <target>Zastosuj</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve"> + <source>Apply to</source> + <target>Zastosuj dla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve"> + <source>Archive</source> + <target>Archiwizuj</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve"> + <source>Archive %lld reports?</source> + <target>Archiwizować %lld reports?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve"> + <source>Archive all reports?</source> + <target>Archiwizować wszystkie zgłoszenia?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve"> + <source>Archive and upload</source> + <target>Archiwizuj i prześlij</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve"> + <source>Archive contacts to chat later.</source> + <target>Archiwizuj kontakty aby porozmawiać później.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve"> + <source>Archive report</source> + <target>Archiwizuj zgłoszenie</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve"> + <source>Archive report?</source> + <target>Archiwizować zgłoszenie?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve"> + <source>Archive reports</source> + <target>Archiwizuj zgłoszenia</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve"> + <source>Archived contacts</source> + <target>Zarchiwizowane kontakty</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve"> + <source>Archiving database</source> + <target>Archiwizowanie bazy danych</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Attach" xml:space="preserve"> <source>Attach</source> <target>Dołącz</target> @@ -883,11 +1150,21 @@ <target>Automatyczne akceptowanie obrazów</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve"> + <source>Auto-accept settings</source> + <target>Ustawienia automatycznej akceptacji</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Back" xml:space="preserve"> <source>Back</source> <target>Wstecz</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Background" xml:space="preserve"> + <source>Background</source> + <target>Tło</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Bad desktop address" xml:space="preserve"> <source>Bad desktop address</source> <target>Zły adres komputera</target> @@ -903,21 +1180,69 @@ <target>Zły hash wiadomości</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better calls" xml:space="preserve"> + <source>Better calls</source> + <target>Lepsze połączenia</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better groups" xml:space="preserve"> <source>Better groups</source> <target>Lepsze grupy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve"> + <source>Better groups performance</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve"> + <source>Better message dates.</source> + <target>Lepsze daty wiadomości.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better messages" xml:space="preserve"> <source>Better messages</source> <target>Lepsze wiadomości</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better networking" xml:space="preserve"> + <source>Better networking</source> + <target>Lepsze sieciowanie</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve"> + <source>Better notifications</source> + <target>Lepsze powiadomienia</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve"> + <source>Better privacy and security</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve"> + <source>Better security ✅</source> + <target>Lepsze zabezpieczenia ✅</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve"> + <source>Better user experience</source> + <target>Lepszy interfejs użytkownika</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Black" xml:space="preserve"> + <source>Black</source> + <target>Czarny</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block" xml:space="preserve"> <source>Block</source> <target>Zablokuj</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block for all" xml:space="preserve"> + <source>Block for all</source> + <target>Zablokuj dla wszystkich</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block group members" xml:space="preserve"> <source>Block group members</source> <target>Blokuj członków grupy</target> @@ -928,19 +1253,39 @@ <target>Zablokuj członka</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve"> + <source>Block member for all?</source> + <target>Zablokować członka dla wszystkich?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block member?" xml:space="preserve"> <source>Block member?</source> <target>Zablokować członka?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve"> + <source>Blocked by admin</source> + <target>Zablokowany przez admina</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve"> + <source>Blur for better privacy.</source> + <target>Rozmycie dla lepszej prywatności.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve"> + <source>Blur media</source> + <target>Rozmycie mediów</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <source>Both you and your contact can add message reactions.</source> <target>Zarówno Ty, jak i Twój kontakt możecie dodawać reakcje wiadomości.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> - <source>Both you and your contact can irreversibly delete sent messages.</source> - <target>Zarówno Ty, jak i Twój kontakt możecie nieodwracalnie usunąć wysłane wiadomości.</target> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target>Zarówno Ty, jak i Twój kontakt możecie nieodwracalnie usunąć wysłane wiadomości. (24 godziny)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> @@ -963,11 +1308,32 @@ <target>Bułgarski, fiński, tajski i ukraiński – dzięki użytkownikom i [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Business address" xml:space="preserve"> + <source>Business address</source> + <target>Adres firmowy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve"> + <source>Business chats</source> + <target>Czaty biznesowe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve"> + <source>Businesses</source> + <target>Firmy</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>Według profilu czatu (domyślnie) lub [według połączenia](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve"> + <source>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> <source>Call already ended!</source> <target>Połączenie już zakończone!</target> @@ -978,9 +1344,24 @@ <target>Połączenia</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve"> - <source>Can't delete user profile!</source> - <target>Nie można usunąć profilu użytkownika!</target> + <trans-unit id="Calls prohibited!" xml:space="preserve"> + <source>Calls prohibited!</source> + <target>Połączenia zakazane!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve"> + <source>Camera not available</source> + <target>Kamera nie dostępna</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve"> + <source>Can't call contact</source> + <target>Nie można zadzwonić do kontaktu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve"> + <source>Can't call member</source> + <target>Nie można zadzwonić do członka</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contact!" xml:space="preserve"> @@ -993,9 +1374,20 @@ <target>Nie można zaprosić kontaktów!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve"> + <source>Can't message member</source> + <target>Nie można wysłać wiadomości do członka</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> <target>Anuluj</target> + <note>alert action +alert button</note> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve"> + <source>Cancel migration</source> + <target>Anuluj migrację</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> @@ -1003,9 +1395,24 @@ <target>Nie można uzyskać dostępu do pęku kluczy, aby zapisać hasło do bazy danych</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Nie można przekazać wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> <target>Nie można odebrać pliku</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <target>Przekroczono pojemność - odbiorca nie otrzymał wcześniej wysłanych wiadomości.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve"> + <source>Cellular</source> + <target>Sieć komórkowa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change" xml:space="preserve"> @@ -1013,6 +1420,15 @@ <target>Zmień</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve"> + <source>Change automatic message deletion?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve"> + <source>Change chat profiles</source> + <target>Zmień profil czatu</target> + <note>authentication reason</note> + </trans-unit> <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> <target>Zmienić hasło bazy danych?</target> @@ -1057,11 +1473,26 @@ <source>Change self-destruct passcode</source> <target>Zmień pin samozniszczenia</target> <note>authentication reason - set passcode view</note> +set passcode view</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> - <source>Chat archive</source> - <target>Archiwum czatu</target> + <trans-unit id="Chat" xml:space="preserve"> + <source>Chat</source> + <target>Czat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve"> + <source>Chat already exists</source> + <target>Czat już istnieje</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve"> + <source>Chat already exists!</source> + <target>Czat już istnieje!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve"> + <source>Chat colors</source> + <target>Kolory czatu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat console" xml:space="preserve"> @@ -1079,9 +1510,14 @@ <target>Baza danych czatu usunięta</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve"> + <source>Chat database exported</source> + <target>Wyeksportowano bazę danych czatów</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> - <target>Zaimportowano bazę danych czatu</target> + <target>Zaimportowano bazę danych czatów</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat is running" xml:space="preserve"> @@ -1094,26 +1530,81 @@ <target>Czat jest zatrzymany</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <target>Czat został zatrzymany. Jeśli korzystałeś już z tej bazy danych na innym urządzeniu, powinieneś przenieść ją z powrotem przed rozpoczęciem czatu.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve"> + <source>Chat list</source> + <target>Lista czatów</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve"> + <source>Chat migrated!</source> + <target>Czat zmigrowany!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> <target>Preferencje czatu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve"> + <source>Chat preferences were changed.</source> + <target>Preferencje czatu zostały zmienione.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve"> + <source>Chat profile</source> + <target>Profil użytkownika</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve"> + <source>Chat theme</source> + <target>Motyw czatu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <target>Czat zostanie usunięty dla wszystkich członków – tej operacji nie można cofnąć!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <target>Czat zostanie usunięty dla Ciebie – tej operacji nie można cofnąć!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> <target>Czaty</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve"> + <source>Check messages every 20 min.</source> + <target>Sprawdzaj wiadomości co 20 min.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve"> + <source>Check messages when allowed.</source> + <target>Sprawdź wiadomości, gdy będzie to dopuszczone.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> <target>Sprawdź adres serwera i spróbuj ponownie.</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> <target>Chiński i hiszpański interfejs</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <target>Wybierz _Zmigruj z innego urządzenia_ na nowym urządzeniu i zeskanuj kod QR.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> <target>Wybierz plik</target> @@ -1124,10 +1615,25 @@ <target>Wybierz z biblioteki</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve"> + <source>Chunks deleted</source> + <target>Fragmenty usunięte</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve"> + <source>Chunks downloaded</source> + <target>Fragmenty pobrane</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve"> + <source>Chunks uploaded</source> + <target>Fragmenty przesłane</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> <target>Wyczyść</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> @@ -1139,16 +1645,38 @@ <target>Wyczyścić rozmowę?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve"> + <source>Clear group?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve"> + <source>Clear or delete group?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve"> + <source>Clear private notes?</source> + <target>Wyczyścić prywatne notatki?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear verification" xml:space="preserve"> <source>Clear verification</source> <target>Wyczyść weryfikację</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> - <source>Colors</source> - <target>Kolory</target> + <trans-unit id="Color chats with the new themes." xml:space="preserve"> + <source>Color chats with the new themes.</source> + <target>Koloruj czaty z nowymi motywami.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Color mode" xml:space="preserve"> + <source>Color mode</source> + <target>Tryb koloru</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve"> + <source>Community guidelines violation</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Compare file" xml:space="preserve"> <source>Compare file</source> <target>Porównaj plik</target> @@ -1159,11 +1687,52 @@ <target>Porównaj kody bezpieczeństwa ze swoimi kontaktami.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Completed" xml:space="preserve"> + <source>Completed</source> + <target>Zakończono</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve"> + <source>Conditions accepted on: %@.</source> + <target>Warunki zaakceptowane dnia: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <target>Warunki zostały zaakceptowane przez operatora(-ów): **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <target>Warunki zostały już zaakceptowane przez tego(-ych) operatora(-ów): **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve"> + <source>Conditions of use</source> + <target>Warunki użytkowania</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve"> + <source>Conditions will be accepted on: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> <target>Skonfiguruj serwery ICE</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Configure server operators" xml:space="preserve"> + <source>Configure server operators</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> <target>Potwierdź</target> @@ -1174,11 +1743,26 @@ <target>Potwierdź Pin</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve"> + <source>Confirm contact deletion?</source> + <target>Potwierdzić usunięcie kontaktu?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm database upgrades" xml:space="preserve"> <source>Confirm database upgrades</source> <target>Potwierdź aktualizacje bazy danych</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve"> + <source>Confirm files from unknown servers.</source> + <target>Potwierdzaj pliki z nieznanych serwerów.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve"> + <source>Confirm network settings</source> + <target>Potwierdź ustawienia sieciowe</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> <target>Potwierdź nowe hasło…</target> @@ -1189,6 +1773,20 @@ <target>Potwierdź hasło</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <target>Potwierdź, że pamiętasz hasło do bazy danych, aby ją zmigrować.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve"> + <source>Confirm upload</source> + <target>Potwierdź wgranie</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve"> + <source>Confirmed</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> <target>Połącz</target> @@ -1209,6 +1807,11 @@ <target>Połącz do komputera</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve"> + <source>Connect to your friends faster.</source> + <target>Szybciej łącz się ze znajomymi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connect to yourself?" xml:space="preserve"> <source>Connect to yourself?</source> <target>Połączyć się ze sobą?</target> @@ -1238,11 +1841,6 @@ To jest twój jednorazowy link!</target> <target>Połącz się przez link</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve"> - <source>Connect via link / QR code</source> - <target>Połącz się przez link / kod QR</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connect via one-time link" xml:space="preserve"> <source>Connect via one-time link</source> <target>Połącz przez jednorazowy link</target> @@ -1253,16 +1851,31 @@ To jest twój jednorazowy link!</target> <target>Połącz z %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected" xml:space="preserve"> + <source>Connected</source> + <target>Połączony</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected desktop" xml:space="preserve"> <source>Connected desktop</source> <target>Połączony komputer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve"> + <source>Connected servers</source> + <target>Połączone serwery</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected to desktop" xml:space="preserve"> <source>Connected to desktop</source> <target>Połączony do komputera</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting" xml:space="preserve"> + <source>Connecting</source> + <target>Łączenie</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> <source>Connecting to server…</source> <target>Łączenie z serwerem…</target> @@ -1273,6 +1886,11 @@ To jest twój jednorazowy link!</target> <target>Łączenie z serwerem... (błąd: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve"> + <source>Connecting to contact, please wait or check later!</source> + <target>Łączenie z kontaktem, poczekaj lub sprawdź później!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting to desktop" xml:space="preserve"> <source>Connecting to desktop</source> <target>Łączenie z komputerem</target> @@ -1283,6 +1901,15 @@ To jest twój jednorazowy link!</target> <target>Połączenie</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve"> + <source>Connection and servers status.</source> + <target>Stan połączenia i serwerów.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve"> + <source>Connection blocked</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> <target>Błąd połączenia</target> @@ -1293,11 +1920,33 @@ To jest twój jednorazowy link!</target> <target>Błąd połączenia (UWIERZYTELNIANIE)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve"> + <source>Connection is blocked by server operator: +%@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve"> + <source>Connection not ready.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve"> + <source>Connection notifications</source> + <target>Powiadomienia o połączeniu</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Prośba o połączenie wysłana!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve"> + <source>Connection requires encryption renegotiation.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve"> + <source>Connection security</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection terminated" xml:space="preserve"> <source>Connection terminated</source> <target>Połączenie zakończone</target> @@ -1308,6 +1957,16 @@ To jest twój jednorazowy link!</target> <target>Czas połączenia minął</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve"> + <source>Connection with desktop stopped</source> + <target>Połączenie z komputerem zakończone</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve"> + <source>Connections</source> + <target>Połączenia</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact allows" xml:space="preserve"> <source>Contact allows</source> <target>Kontakt pozwala</target> @@ -1318,6 +1977,11 @@ To jest twój jednorazowy link!</target> <target>Kontakt już istnieje</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact deleted!" xml:space="preserve"> + <source>Contact deleted!</source> + <target>Kontakt usunięty!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> <target>Kontakt ukryty:</target> @@ -1328,9 +1992,9 @@ To jest twój jednorazowy link!</target> <target>Kontakt jest połączony</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve"> - <source>Contact is not connected yet!</source> - <target>Kontakt nie jest jeszcze połączony!</target> + <trans-unit id="Contact is deleted." xml:space="preserve"> + <source>Contact is deleted.</source> + <target>Kontakt jest usunięty.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact name" xml:space="preserve"> @@ -1343,6 +2007,11 @@ To jest twój jednorazowy link!</target> <target>Preferencje kontaktu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve"> + <source>Contact will be deleted - this cannot be undone!</source> + <target>Kontakt zostanie usunięty – nie można tego cofnąć!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> <target>Kontakty</target> @@ -1353,21 +2022,40 @@ To jest twój jednorazowy link!</target> <target>Kontakty mogą oznaczać wiadomości do usunięcia; będziesz mógł je zobaczyć.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve"> + <source>Content violates conditions of use</source> + <note>blocking reason</note> + </trans-unit> <trans-unit id="Continue" xml:space="preserve"> <source>Continue</source> <target>Kontynuuj</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve"> + <source>Conversation deleted!</source> + <target>Rozmowa usunięta!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> <target>Kopiuj</target> - <note>chat item action</note> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve"> + <source>Copy error</source> + <target>Kopiuj błąd</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> <target>Wersja rdzenia: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Corner" xml:space="preserve"> + <source>Corner</source> + <target>Róg</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Correct name to %@?" xml:space="preserve"> <source>Correct name to %@?</source> <target>Poprawić imię na %@?</target> @@ -1378,6 +2066,10 @@ To jest twój jednorazowy link!</target> <target>Utwórz</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve"> + <source>Create 1-time link</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> <target>Utwórz adres SimpleX</target> @@ -1388,11 +2080,6 @@ To jest twój jednorazowy link!</target> <target>Utwórz grupę używając losowego profilu.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> - <source>Create an address to let people connect with you.</source> - <target>Utwórz adres, aby ludzie mogli się z Tobą połączyć.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create file" xml:space="preserve"> <source>Create file</source> <target>Utwórz plik</target> @@ -1413,16 +2100,15 @@ To jest twój jednorazowy link!</target> <target>Utwórz link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create list" xml:space="preserve"> + <source>Create list</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> <target>Utwórz nowy profil w [aplikacji desktopowej](https://simplex.chat/downloads/). 💻</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve"> - <source>Create one-time invitation link</source> - <target>Utwórz jednorazowy link do zaproszenia</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create profile" xml:space="preserve"> <source>Create profile</source> <target>Utwórz profil</target> @@ -1443,9 +2129,29 @@ To jest twój jednorazowy link!</target> <target>Utwórz swój profil</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve"> - <source>Created on %@</source> - <target>Utworzony w dniu %@</target> + <trans-unit id="Created" xml:space="preserve"> + <source>Created</source> + <target>Utworzono</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve"> + <source>Created at</source> + <target>Utworzony o</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve"> + <source>Created at: %@</source> + <target>Utworzony o: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve"> + <source>Creating archive link</source> + <target>Tworzenie linku archiwum</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve"> + <source>Creating link…</source> + <target>Tworzenie linku…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current Passcode" xml:space="preserve"> @@ -1453,11 +2159,20 @@ To jest twój jednorazowy link!</target> <target>Aktualny Pin</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> <target>Obecne hasło…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current profile" xml:space="preserve"> + <source>Current profile</source> + <target>Bieżący profil</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> <target>Obecnie maksymalna obsługiwana wielkość pliku wynosi %@.</target> @@ -1468,11 +2183,25 @@ To jest twój jednorazowy link!</target> <target>Niestandardowy czas</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve"> + <source>Customizable message shape.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve"> + <source>Customize theme</source> + <target>Dostosuj motyw</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> <target>Ciemny</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve"> + <source>Dark mode colors</source> + <target>Kolory ciemnego trybu</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> <target>ID bazy danych</target> @@ -1571,6 +2300,11 @@ To jest twój jednorazowy link!</target> <target>Baza danych zostanie zmigrowana po ponownym uruchomieniu aplikacji</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Debug delivery" xml:space="preserve"> + <source>Debug delivery</source> + <target>Dostarczenie debugowania</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> <target>Zdecentralizowane</target> @@ -1584,18 +2318,19 @@ To jest twój jednorazowy link!</target> <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> <target>Usuń</target> - <note>chat item action</note> + <note>alert action +swipe action</note> + </trans-unit> + <trans-unit id="Delete %lld messages of members?" xml:space="preserve"> + <source>Delete %lld messages of members?</source> + <target>Usunąć %lld wiadomości członków?</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete %lld messages?" xml:space="preserve"> <source>Delete %lld messages?</source> <target>Usunąć %lld wiadomości?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve"> - <source>Delete Contact</source> - <target>Usuń Kontakt</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Delete address" xml:space="preserve"> <source>Delete address</source> <target>Usuń adres</target> @@ -1621,14 +2356,12 @@ To jest twój jednorazowy link!</target> <target>Usuń i powiadom kontakt</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve"> - <source>Delete archive</source> - <target>Usuń archiwum</target> + <trans-unit id="Delete chat" xml:space="preserve"> + <source>Delete chat</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve"> - <source>Delete chat archive?</source> - <target>Usunąć archiwum czatu?</target> + <trans-unit id="Delete chat messages from your device." xml:space="preserve"> + <source>Delete chat messages from your device.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile" xml:space="preserve"> @@ -1641,6 +2374,10 @@ To jest twój jednorazowy link!</target> <target>Usunąć profil czatu?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete chat?" xml:space="preserve"> + <source>Delete chat?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> <source>Delete connection</source> <target>Usuń połączenie</target> @@ -1651,11 +2388,9 @@ To jest twój jednorazowy link!</target> <target>Usuń kontakt</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact? This cannot be undone!" xml:space="preserve"> - <source>Delete contact? -This cannot be undone!</source> - <target>Usunąć kontakt? -To nie może być cofnięte!</target> + <trans-unit id="Delete contact?" xml:space="preserve"> + <source>Delete contact?</source> + <target>Usunąć kontakt?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete database" xml:space="preserve"> @@ -1663,6 +2398,11 @@ To nie może być cofnięte!</target> <target>Usuń bazę danych</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete database from this device" xml:space="preserve"> + <source>Delete database from this device</source> + <target>Usuń bazę danych z tego urządzenia</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete file" xml:space="preserve"> <source>Delete file</source> <target>Usuń plik</target> @@ -1713,6 +2453,10 @@ To nie może być cofnięte!</target> <target>Usunąć link?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete list?" xml:space="preserve"> + <source>Delete list?</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> <target>Usunąć wiadomość członka?</target> @@ -1726,7 +2470,7 @@ To nie może być cofnięte!</target> <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> <target>Usuń wiadomości</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> @@ -1743,9 +2487,8 @@ To nie może być cofnięte!</target> <target>Usunąć starą bazę danych?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve"> - <source>Delete pending connection</source> - <target>Usuń oczekujące połączenie</target> + <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> + <source>Delete or moderate up to 200 messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection?" xml:space="preserve"> @@ -1763,11 +2506,30 @@ To nie może być cofnięte!</target> <target>Usuń kolejkę</target> <note>server test step</note> </trans-unit> + <trans-unit id="Delete report" xml:space="preserve"> + <source>Delete report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete up to 20 messages at once." xml:space="preserve"> + <source>Delete up to 20 messages at once.</source> + <target>Usuń do 20 wiadomości na raz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> <target>Usunąć profil użytkownika?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete without notification" xml:space="preserve"> + <source>Delete without notification</source> + <target>Usuń bez powiadomienia</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve"> + <source>Deleted</source> + <target>Usunięto</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Deleted at" xml:space="preserve"> <source>Deleted at</source> <target>Usunięto o</target> @@ -1778,6 +2540,15 @@ To nie może być cofnięte!</target> <target>Usunięto o: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve"> + <source>Deletion errors</source> + <target>Błędy usuwania</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivered even when Apple drops them." xml:space="preserve"> + <source>Delivered even when Apple drops them.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery" xml:space="preserve"> <source>Delivery</source> <target>Dostarczenie</target> @@ -1813,11 +2584,41 @@ To nie może być cofnięte!</target> <target>Urządzenia komputerowe</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve"> + <source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source> + <target>Adres serwera docelowego %@ jest niekompatybilny z ustawieniami serwera przekazującego %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server error: %@" xml:space="preserve"> + <source>Destination server error: %@</source> + <target>Błąd docelowego serwera: %@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve"> + <source>Destination server version of %@ is incompatible with forwarding server %@.</source> + <target>Wersja serwera docelowego %@ jest niekompatybilna z serwerem przekierowującym %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Detailed statistics" xml:space="preserve"> + <source>Detailed statistics</source> + <target>Szczegółowe statystyki</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Details" xml:space="preserve"> + <source>Details</source> + <target>Szczegóły</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Develop" xml:space="preserve"> <source>Develop</source> <target>Deweloperskie</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Developer options" xml:space="preserve"> + <source>Developer options</source> + <target>Opcje deweloperskie</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> <target>Narzędzia deweloperskie</target> @@ -1848,8 +2649,12 @@ To nie może być cofnięte!</target> <target>Bezpośrednie wiadomości</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve"> + <source>Direct messages between members are prohibited in this chat.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> <target>Bezpośrednie wiadomości między członkami są zabronione w tej grupie.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1863,11 +2668,24 @@ To nie może być cofnięte!</target> <target>Wyłącz blokadę SimpleX</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Disable automatic message deletion?" xml:space="preserve"> + <source>Disable automatic message deletion?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Disable delete messages" xml:space="preserve"> + <source>Disable delete messages</source> + <note>alert button</note> + </trans-unit> <trans-unit id="Disable for all" xml:space="preserve"> <source>Disable for all</source> <target>Wyłącz dla wszystkich</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Disabled" xml:space="preserve"> + <source>Disabled</source> + <target>Wyłączony</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> <source>Disappearing message</source> <target>Znikająca wiadomość</target> @@ -1883,8 +2701,8 @@ To nie może być cofnięte!</target> <target>Znikające wiadomości są zabronione na tym czacie.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <target>Znikające wiadomości są zabronione w tej grupie.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1918,16 +2736,40 @@ To nie może być cofnięte!</target> <target>Odkryj przez sieć lokalną</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve"> + <source>Do NOT send messages directly, even if your or destination server does not support private routing.</source> + <target>NIE wysyłaj wiadomości bezpośrednio, nawet jeśli serwer docelowy nie obsługuje prywatnego trasowania.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> <source>Do NOT use SimpleX for emergency calls.</source> <target>NIE używaj SimpleX do połączeń alarmowych.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT use private routing." xml:space="preserve"> + <source>Do NOT use private routing.</source> + <target>NIE używaj prywatnego trasowania.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do it later" xml:space="preserve"> <source>Do it later</source> <target>Zrób to później</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do not send history to new members." xml:space="preserve"> + <source>Do not send history to new members.</source> + <target>Nie wysyłaj historii do nowych członków.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not use credentials with proxy." xml:space="preserve"> + <source>Do not use credentials with proxy.</source> + <target>Nie używaj danych logowania do proxy.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Documents:" xml:space="preserve"> + <source>Documents:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> <target>Nie twórz adresu</target> @@ -1938,21 +2780,70 @@ To nie może być cofnięte!</target> <target>Nie włączaj</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Don't miss important messages." xml:space="preserve"> + <source>Don't miss important messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> <target>Nie pokazuj ponownie</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Done" xml:space="preserve"> + <source>Done</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Downgrade and open chat" xml:space="preserve"> <source>Downgrade and open chat</source> <target>Obniż wersję i otwórz czat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Download" xml:space="preserve"> + <source>Download</source> + <target>Pobierz</target> + <note>alert button +chat item action</note> + </trans-unit> + <trans-unit id="Download errors" xml:space="preserve"> + <source>Download errors</source> + <target>Błędy pobierania</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download failed" xml:space="preserve"> + <source>Download failed</source> + <target>Pobieranie nie udane</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Download file" xml:space="preserve"> <source>Download file</source> <target>Pobierz plik</target> <note>server test step</note> </trans-unit> + <trans-unit id="Download files" xml:space="preserve"> + <source>Download files</source> + <target>Pobierz pliki</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Downloaded" xml:space="preserve"> + <source>Downloaded</source> + <target>Pobrane</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloaded files" xml:space="preserve"> + <source>Downloaded files</source> + <target>Pobrane pliki</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading archive" xml:space="preserve"> + <source>Downloading archive</source> + <target>Pobieranie archiwum</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading link details" xml:space="preserve"> + <source>Downloading link details</source> + <target>Pobieranie szczegółów linku</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> <target>Zduplikowana wyświetlana nazwa!</target> @@ -1963,6 +2854,10 @@ To nie może być cofnięte!</target> <target>Czas trwania</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="E2E encrypted notifications." xml:space="preserve"> + <source>E2E encrypted notifications.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> <target>Edytuj</target> @@ -1983,6 +2878,10 @@ To nie może być cofnięte!</target> <target>Włącz (zachowaj nadpisania)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve"> + <source>Enable Flux in Network & servers settings for better metadata privacy.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> <target>Włącz blokadę SimpleX</target> @@ -1996,6 +2895,11 @@ To nie może być cofnięte!</target> <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> <target>Czy włączyć automatyczne usuwanie wiadomości?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Enable camera access" xml:space="preserve"> + <source>Enable camera access</source> + <target>Włącz dostęp do kamery</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable for all" xml:space="preserve"> @@ -2003,6 +2907,11 @@ To nie może być cofnięte!</target> <target>Włącz dla wszystkich</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve"> + <source>Enable in direct chats (BETA)!</source> + <target>Włącz w czatach bezpośrednich (BETA)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> <target>Włączyć natychmiastowe powiadomienia?</target> @@ -2033,6 +2942,16 @@ To nie może być cofnięte!</target> <target>Włącz pin samodestrukcji</target> <note>set passcode view</note> </trans-unit> + <trans-unit id="Enabled" xml:space="preserve"> + <source>Enabled</source> + <target>Włączony</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve"> + <source>Enabled for</source> + <target>Włączony dla</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> <target>Szyfruj</target> @@ -2063,6 +2982,11 @@ To nie może być cofnięte!</target> <target>Zaszyfrowana wiadomość lub inne zdarzenie</target> <note>notification</note> </trans-unit> + <trans-unit id="Encrypted message: app is stopped" xml:space="preserve"> + <source>Encrypted message: app is stopped</source> + <target>Zaszyfrowana wiadomość: aplikacja jest zatrzymana</target> + <note>notification</note> + </trans-unit> <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> <target>Zaszyfrowana wiadomość: błąd bazy danych</target> @@ -2098,6 +3022,10 @@ To nie może być cofnięte!</target> <target>Renegocjacja szyfrowania nie powiodła się.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encryption renegotiation in progress." xml:space="preserve"> + <source>Encryption renegotiation in progress.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter Passcode" xml:space="preserve"> <source>Enter Passcode</source> <target>Wprowadź Pin</target> @@ -2113,6 +3041,11 @@ To nie może być cofnięte!</target> <target>Wpisz nazwę grupy…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve"> + <source>Enter passphrase</source> + <target>Wprowadź hasło</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter passphrase…" xml:space="preserve"> <source>Enter passphrase…</source> <target>Wprowadź hasło…</target> @@ -2158,26 +3091,34 @@ To nie może być cofnięte!</target> <target>Błąd przerwania zmiany adresu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error accepting conditions" xml:space="preserve"> + <source>Error accepting conditions</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> <target>Błąd przyjmowania prośby o kontakt</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve"> - <source>Error accessing database file</source> - <target>Błąd dostępu do pliku bazy danych</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> <target>Błąd dodawania członka(ów)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error adding server" xml:space="preserve"> + <source>Error adding server</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> <target>Błąd zmiany adresu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing connection profile" xml:space="preserve"> + <source>Error changing connection profile</source> + <target>Błąd zmiany połączenia profilu</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error changing role" xml:space="preserve"> <source>Error changing role</source> <target>Błąd zmiany roli</target> @@ -2188,6 +3129,20 @@ To nie może być cofnięte!</target> <target>Błąd zmiany ustawienia</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing to incognito!" xml:space="preserve"> + <source>Error changing to incognito!</source> + <target>Błąd zmiany na incognito!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error checking token status" xml:space="preserve"> + <source>Error checking token status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve"> + <source>Error connecting to forwarding server %@. Please try later.</source> + <target>Błąd połączenia z serwerem przekierowania %@. Spróbuj ponownie później.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> <target>Błąd tworzenia adresu</target> @@ -2203,16 +3158,29 @@ To nie może być cofnięte!</target> <target>Błąd tworzenia linku grupy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating list" xml:space="preserve"> + <source>Error creating list</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error creating member contact" xml:space="preserve"> <source>Error creating member contact</source> <target>Błąd tworzenia kontaktu członka</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating message" xml:space="preserve"> + <source>Error creating message</source> + <target>Błąd tworzenia wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>Błąd tworzenia profilu!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating report" xml:space="preserve"> + <source>Error creating report</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error decrypting file" xml:space="preserve"> <source>Error decrypting file</source> <target>Błąd odszyfrowania pliku</target> @@ -2233,11 +3201,6 @@ To nie może być cofnięte!</target> <target>Błąd usuwania połączenia</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve"> - <source>Error deleting contact</source> - <target>Błąd usuwania kontaktu</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> <target>Błąd usuwania bazy danych</target> @@ -2258,6 +3221,11 @@ To nie może być cofnięte!</target> <target>Błąd usuwania profilu użytkownika</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error downloading the archive" xml:space="preserve"> + <source>Error downloading the archive</source> + <target>Błąd pobierania archiwum</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> <source>Error enabling delivery receipts!</source> <target>Błąd włączania potwierdzeń dostawy!</target> @@ -2278,6 +3246,11 @@ To nie może być cofnięte!</target> <target>Błąd eksportu bazy danych czatu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error exporting theme: %@" xml:space="preserve"> + <source>Error exporting theme: %@</source> + <target>Błąd eksportowania motywu: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> <target>Błąd importu bazy danych czatu</target> @@ -2288,24 +3261,51 @@ To nie może być cofnięte!</target> <target>Błąd dołączenia do grupy</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve"> - <source>Error loading %@ servers</source> - <target>Błąd ładowania %@ serwerów</target> + <trans-unit id="Error loading servers" xml:space="preserve"> + <source>Error loading servers</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error migrating settings" xml:space="preserve"> + <source>Error migrating settings</source> + <target>Błąd migracji ustawień</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error opening chat" xml:space="preserve"> + <source>Error opening chat</source> + <target>Błąd otwierania czatu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> <target>Błąd odbioru pliku</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error reconnecting server" xml:space="preserve"> + <source>Error reconnecting server</source> + <target>Błąd ponownego łączenia z serwerem</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error reconnecting servers" xml:space="preserve"> + <source>Error reconnecting servers</source> + <target>Błąd ponownego łączenia serwerów</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error registering for notifications" xml:space="preserve"> + <source>Error registering for notifications</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> <target>Błąd usuwania członka</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve"> - <source>Error saving %@ servers</source> - <target>Błąd zapisu %@ serwerów</target> + <trans-unit id="Error reordering lists" xml:space="preserve"> + <source>Error reordering lists</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error resetting statistics" xml:space="preserve"> + <source>Error resetting statistics</source> + <target>Błąd resetowania statystyk</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving ICE servers" xml:space="preserve"> @@ -2313,6 +3313,10 @@ To nie może być cofnięte!</target> <target>Błąd zapisu serwerów ICE</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving chat list" xml:space="preserve"> + <source>Error saving chat list</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> <target>Błąd zapisu profilu grupy</target> @@ -2328,11 +3332,25 @@ To nie może być cofnięte!</target> <target>Błąd zapisu hasła do pęku kluczy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving servers" xml:space="preserve"> + <source>Error saving servers</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error saving settings" xml:space="preserve"> + <source>Error saving settings</source> + <target>Błąd zapisywania ustawień</target> + <note>when migrating</note> + </trans-unit> <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> <target>Błąd zapisu hasła użytkownika</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error scanning code: %@" xml:space="preserve"> + <source>Error scanning code: %@</source> + <target>Błąd skanowanie kodu: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending email" xml:space="preserve"> <source>Error sending email</source> <target>Błąd wysyłania e-mail</target> @@ -2363,16 +3381,25 @@ To nie może być cofnięte!</target> <target>Błąd zatrzymania czatu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error switching profile" xml:space="preserve"> + <source>Error switching profile</source> + <target>Błąd zmiany profilu</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> <target>Błąd przełączania profilu!</target> - <note>No comment provided by engineer.</note> + <note>alertTitle</note> </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> <target>Błąd synchronizacji połączenia</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error testing server connection" xml:space="preserve"> + <source>Error testing server connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> <target>Błąd aktualizacji linku grupy</target> @@ -2383,6 +3410,10 @@ To nie może być cofnięte!</target> <target>Błąd aktualizacji wiadomości</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error updating server" xml:space="preserve"> + <source>Error updating server</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> <target>Błąd aktualizacji ustawień</target> @@ -2393,6 +3424,16 @@ To nie może być cofnięte!</target> <target>Błąd aktualizacji prywatności użytkownika</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error uploading the archive" xml:space="preserve"> + <source>Error uploading the archive</source> + <target>Błąd wgrywania archiwum</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error verifying passphrase:" xml:space="preserve"> + <source>Error verifying passphrase:</source> + <target>Błąd weryfikowania hasła:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error: " xml:space="preserve"> <source>Error: </source> <target>Błąd: </target> @@ -2401,7 +3442,9 @@ To nie może być cofnięte!</target> <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> <target>Błąd: %@</target> - <note>No comment provided by engineer.</note> + <note>alert message +file error text +snd error text</note> </trans-unit> <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> @@ -2413,6 +3456,15 @@ To nie może być cofnięte!</target> <target>Błąd: brak pliku bazy danych</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Errors" xml:space="preserve"> + <source>Errors</source> + <target>Błędy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors in servers configuration." xml:space="preserve"> + <source>Errors in servers configuration.</source> + <note>servers error</note> + </trans-unit> <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> <source>Even when disabled in the conversation.</source> <target>Nawet po wyłączeniu w rozmowie.</target> @@ -2428,6 +3480,10 @@ To nie może być cofnięte!</target> <target>Rozszerz</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Expired" xml:space="preserve"> + <source>Expired</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> <target>Eksportuj bazę danych</target> @@ -2438,11 +3494,21 @@ To nie może być cofnięte!</target> <target>Błąd eksportu:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Export theme" xml:space="preserve"> + <source>Export theme</source> + <target>Eksportuj motyw</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> <target>Wyeksportowane archiwum bazy danych.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Exported file doesn't exist" xml:space="preserve"> + <source>Exported file doesn't exist</source> + <target>Wyeksportowany plik nie istnieje</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exporting database archive…" xml:space="preserve"> <source>Exporting database archive…</source> <target>Eksportowanie archiwum bazy danych…</target> @@ -2458,16 +3524,65 @@ To nie może być cofnięte!</target> <target>Szybko i bez czekania aż nadawca będzie online!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster deletion of groups." xml:space="preserve"> + <source>Faster deletion of groups.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Faster joining and more reliable messages." xml:space="preserve"> <source>Faster joining and more reliable messages.</source> <target>Szybsze dołączenie i bardziej niezawodne wiadomości.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster sending messages." xml:space="preserve"> + <source>Faster sending messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Favorite" xml:space="preserve"> <source>Favorite</source> <target>Ulubione</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Favorites" xml:space="preserve"> + <source>Favorites</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Błąd pliku</target> + <note>file error alert title</note> + </trans-unit> + <trans-unit id="File errors: %@" xml:space="preserve"> + <source>File errors: +%@</source> + <target>Błędy pliku: +%@</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="File is blocked by server operator: %@." xml:space="preserve"> + <source>File is blocked by server operator: +%@.</source> + <note>file error text</note> + </trans-unit> + <trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve"> + <source>File not found - most likely file was deleted or cancelled.</source> + <target>Nie odnaleziono pliku - najprawdopodobniej plik został usunięty lub anulowany.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File server error: %@" xml:space="preserve"> + <source>File server error: %@</source> + <target>Błąd serwera plików: %@</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File status" xml:space="preserve"> + <source>File status</source> + <target>Status pliku</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File status: %@" xml:space="preserve"> + <source>File status: %@</source> + <target>Status pliku: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="File will be deleted from servers." xml:space="preserve"> <source>File will be deleted from servers.</source> <target>Plik zostanie usunięty z serwerów.</target> @@ -2488,6 +3603,11 @@ To nie może być cofnięte!</target> <target>Plik: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files" xml:space="preserve"> + <source>Files</source> + <target>Pliki</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> <target>Pliki i media</target> @@ -2498,11 +3618,16 @@ To nie może być cofnięte!</target> <target>Pliki i media</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> - <source>Files and media are prohibited in this group.</source> + <trans-unit id="Files and media are prohibited." xml:space="preserve"> + <source>Files and media are prohibited.</source> <target>Pliki i media są zabronione w tej grupie.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files and media not allowed" xml:space="preserve"> + <source>Files and media not allowed</source> + <target>Pliki i multimedia nie są dozwolone</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files and media prohibited!" xml:space="preserve"> <source>Files and media prohibited!</source> <target>Pliki i media zabronione!</target> @@ -2513,6 +3638,16 @@ To nie może być cofnięte!</target> <target>Filtruj nieprzeczytane i ulubione czaty.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Finalize migration" xml:space="preserve"> + <source>Finalize migration</source> + <target>Dokończ migrację</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration on another device." xml:space="preserve"> + <source>Finalize migration on another device.</source> + <target>Dokończ migrację na innym urządzeniu.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> <target>W końcu je mamy! 🚀</target> @@ -2553,11 +3688,108 @@ To nie może być cofnięte!</target> <target>Naprawa nie jest obsługiwana przez członka grupy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For all moderators" xml:space="preserve"> + <source>For all moderators</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For chat profile %@:" xml:space="preserve"> + <source>For chat profile %@:</source> + <note>servers error</note> + </trans-unit> <trans-unit id="For console" xml:space="preserve"> <source>For console</source> <target>Dla konsoli</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." xml:space="preserve"> + <source>For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For me" xml:space="preserve"> + <source>For me</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For private routing" xml:space="preserve"> + <source>For private routing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For social media" xml:space="preserve"> + <source>For social media</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward" xml:space="preserve"> + <source>Forward</source> + <target>Przekaż dalej</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Forward %d message(s)?" xml:space="preserve"> + <source>Forward %d message(s)?</source> + <target>Przekazać %d wiadomość(i)?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Forward and save messages" xml:space="preserve"> + <source>Forward and save messages</source> + <target>Przesyłaj dalej i zapisuj wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward messages" xml:space="preserve"> + <source>Forward messages</source> + <target>Przekaż wiadomości</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Forward messages without files?" xml:space="preserve"> + <source>Forward messages without files?</source> + <target>Przekazać wiadomości bez plików?</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> + <source>Forward up to 20 messages at once.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded" xml:space="preserve"> + <source>Forwarded</source> + <target>Przekazane dalej</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded from" xml:space="preserve"> + <source>Forwarded from</source> + <target>Przekazane dalej od</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding %lld messages" xml:space="preserve"> + <source>Forwarding %lld messages</source> + <target>Przekazywanie %lld wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve"> + <source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source> + <target>Serwer przekazujący %@ nie mógł połączyć się z serwerem docelowym %@. Spróbuj ponownie później.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server address is incompatible with network settings: %@.</source> + <target>Adres serwera przekierowującego jest niekompatybilny z ustawieniami sieciowymi: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server version is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server version is incompatible with network settings: %@.</source> + <target>Wersja serwera przekierowującego jest niekompatybilna z ustawieniami sieciowymi: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Destination server error: %2$@</source> + <target>Serwer przekazujący: %1$@ +Błąd serwera docelowego: %2$@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Error: %2$@</source> + <target>Serwer przekazujący: %1$@ +Błąd: %2$@</target> + <note>snd error text</note> + </trans-unit> <trans-unit id="Found desktop" xml:space="preserve"> <source>Found desktop</source> <target>Znaleziono komputer</target> @@ -2578,11 +3810,6 @@ To nie może być cofnięte!</target> <target>Pełna nazwa (opcjonalna)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full name:" xml:space="preserve"> - <source>Full name:</source> - <target>Pełna nazwa:</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Fully decentralized – visible only to members." xml:space="preserve"> <source>Fully decentralized – visible only to members.</source> <target>W pełni zdecentralizowana – widoczna tylko dla członków.</target> @@ -2603,6 +3830,20 @@ To nie może być cofnięte!</target> <target>GIF-y i naklejki</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Get notified when mentioned." xml:space="preserve"> + <source>Get notified when mentioned.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Good afternoon!" xml:space="preserve"> + <source>Good afternoon!</source> + <target>Dzień dobry!</target> + <note>message preview</note> + </trans-unit> + <trans-unit id="Good morning!" xml:space="preserve"> + <source>Good morning!</source> + <target>Dzień dobry!</target> + <note>message preview</note> + </trans-unit> <trans-unit id="Group" xml:space="preserve"> <source>Group</source> <target>Grupa</target> @@ -2658,36 +3899,6 @@ To nie może być cofnięte!</target> <target>Linki grupowe</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve"> - <source>Group members can add message reactions.</source> - <target>Członkowie grupy mogą dodawać reakcje wiadomości.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> - <source>Group members can irreversibly delete sent messages.</source> - <target>Członkowie grupy mogą nieodwracalnie usuwać wysłane wiadomości.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> - <target>Członkowie grupy mogą wysyłać bezpośrednie wiadomości.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> - <target>Członkowie grupy mogą wysyłać znikające wiadomości.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send files and media." xml:space="preserve"> - <source>Group members can send files and media.</source> - <target>Członkowie grupy mogą wysyłać pliki i media.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> - <target>Członkowie grupy mogą wysyłać wiadomości głosowe.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> <target>Wiadomość grupowa:</target> @@ -2728,11 +3939,19 @@ To nie może być cofnięte!</target> <target>Grupa zostanie usunięta dla Ciebie - nie można tego cofnąć!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Groups" xml:space="preserve"> + <source>Groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Help" xml:space="preserve"> <source>Help</source> <target>Pomoc</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Help admins moderating their groups." xml:space="preserve"> + <source>Help admins moderating their groups.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> <target>Ukryte</target> @@ -2773,15 +3992,27 @@ To nie może być cofnięte!</target> <target>Historia</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="History is not sent to new members." xml:space="preserve"> + <source>History is not sent to new members.</source> + <target>Historia nie jest wysyłana do nowych członków.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> <target>Jak działa SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="How it affects privacy" xml:space="preserve"> + <source>How it affects privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it helps privacy" xml:space="preserve"> + <source>How it helps privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> - <target>Jak to działa</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="How to" xml:space="preserve"> <source>How to</source> @@ -2798,21 +4029,26 @@ To nie może być cofnięte!</target> <target>Jak korzystać z Twoich serwerów</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Hungarian interface" xml:space="preserve"> + <source>Hungarian interface</source> + <target>Węgierski interfejs</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> <target>Serwery ICE (po jednym na linię)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="IP address" xml:space="preserve"> + <source>IP address</source> + <target>Adres IP</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> <source>If you can't meet in person, show QR code in a video call, or share the link.</source> <target>Jeśli nie możesz spotkać się osobiście, pokaż kod QR w rozmowie wideo lub udostępnij link.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <target>Jeśli nie możesz spotkać się osobiście, możesz **zeskanować kod QR w rozmowie wideo** lub Twój kontakt może udostępnić link z zaproszeniem.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> <target>Jeśli wprowadzisz ten pin podczas otwierania aplikacji, wszystkie dane aplikacji zostaną nieodwracalnie usunięte!</target> @@ -2848,8 +4084,8 @@ To nie może być cofnięte!</target> <target>Natychmiast</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <target>Odporność na spam i nadużycia</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2868,6 +4104,31 @@ To nie może być cofnięte!</target> <target>Importuj bazę danych</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Import failed" xml:space="preserve"> + <source>Import failed</source> + <target>Import nie udał się</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import theme" xml:space="preserve"> + <source>Import theme</source> + <target>Importuj motyw</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Importing archive" xml:space="preserve"> + <source>Importing archive</source> + <target>Importowanie archiwum</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved delivery, reduced traffic usage. More improvements are coming soon!" xml:space="preserve"> + <source>Improved delivery, reduced traffic usage. +More improvements are coming soon!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved message delivery" xml:space="preserve"> + <source>Improved message delivery</source> + <target>Ulepszona dostawa wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> <target>Zwiększona prywatność i bezpieczeństwo</target> @@ -2878,11 +4139,29 @@ To nie może być cofnięte!</target> <target>Ulepszona konfiguracja serwera</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In order to continue, chat should be stopped." xml:space="preserve"> + <source>In order to continue, chat should be stopped.</source> + <target>Aby konturować, czat musi zostać zatrzymany.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>W odpowiedzi na</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In-call sounds" xml:space="preserve"> + <source>In-call sounds</source> + <target>Dźwięki w rozmowie</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Inappropriate content" xml:space="preserve"> + <source>Inappropriate content</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Inappropriate profile" xml:space="preserve"> + <source>Inappropriate profile</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> <target>Incognito</target> @@ -2953,6 +4232,11 @@ To nie może być cofnięte!</target> <target>Zainstaluj [SimpleX Chat na terminal](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Instant" xml:space="preserve"> + <source>Instant</source> + <target>Natychmiastowo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> @@ -2960,30 +4244,75 @@ To nie może być cofnięte!</target> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Instantly" xml:space="preserve"> - <source>Instantly</source> - <target>Natychmiastowo</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> <target>Interfejs</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Interface colors" xml:space="preserve"> + <source>Interface colors</source> + <target>Kolory interfejsu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid" xml:space="preserve"> + <source>Invalid</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (bad token)" xml:space="preserve"> + <source>Invalid (bad token)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (expired)" xml:space="preserve"> + <source>Invalid (expired)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (unregistered)" xml:space="preserve"> + <source>Invalid (unregistered)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (wrong topic)" xml:space="preserve"> + <source>Invalid (wrong topic)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid QR code" xml:space="preserve"> + <source>Invalid QR code</source> + <target>Nieprawidłowy kod QR</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> <target>Nieprawidłowy link połączenia</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid display name!" xml:space="preserve"> + <source>Invalid display name!</source> + <target>Nieprawidłowa nazwa wyświetlana!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid link" xml:space="preserve"> + <source>Invalid link</source> + <target>Nieprawidłowy link</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Nieprawidłowe potwierdzenie migracji</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid name!" xml:space="preserve"> <source>Invalid name!</source> <target>Nieprawidłowa nazwa!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid response" xml:space="preserve"> + <source>Invalid response</source> + <target>Nieprawidłowa odpowiedź</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> <target>Nieprawidłowy adres serwera!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Invalid status" xml:space="preserve"> <source>Invalid status</source> @@ -3005,6 +4334,10 @@ To nie może być cofnięte!</target> <target>Zaproś członków</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invite to chat" xml:space="preserve"> + <source>Invite to chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invite to group" xml:space="preserve"> <source>Invite to group</source> <target>Zaproś do grupy</target> @@ -3020,8 +4353,8 @@ To nie może być cofnięte!</target> <target>Nieodwracalne usuwanie wiadomości jest na tym czacie zabronione.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <target>Nieodwracalne usuwanie wiadomości jest w tej grupie zabronione.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3046,6 +4379,11 @@ To nie może być cofnięte!</target> 3. Połączenie zostało skompromitowane.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="It protects your IP address and connections." xml:space="preserve"> + <source>It protects your IP address and connections.</source> + <target>Chroni Twój adres IP i połączenia.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> <target>Wygląda na to, że jesteś już połączony przez ten link. Jeśli tak nie jest, wystąpił błąd (%@).</target> @@ -3064,13 +4402,18 @@ To nie może być cofnięte!</target> <trans-unit id="Join" xml:space="preserve"> <source>Join</source> <target>Dołącz</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> <target>Dołącz do grupy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Join group conversations" xml:space="preserve"> + <source>Join group conversations</source> + <target>Dołącz do grupowej rozmowy</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Join group?" xml:space="preserve"> <source>Join group?</source> <target>Dołączyć do grupy?</target> @@ -3098,11 +4441,26 @@ To jest twój link do grupy %@!</target> <target>Dołączanie do grupy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep" xml:space="preserve"> + <source>Keep</source> + <target>Zachowaj</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Keep conversation" xml:space="preserve"> + <source>Keep conversation</source> + <target>Zachowaj rozmowę</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Keep the app open to use it from desktop" xml:space="preserve"> <source>Keep the app open to use it from desktop</source> <target>Zostaw aplikację otwartą i używaj ją z komputera</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep unused invitation?" xml:space="preserve"> + <source>Keep unused invitation?</source> + <target>Zachować nieużyte zaproszenie?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> <source>Keep your connections</source> <target>Zachowaj swoje połączenia</target> @@ -3136,6 +4494,14 @@ To jest twój link do grupy %@!</target> <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> <target>Opuść</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Leave chat" xml:space="preserve"> + <source>Leave chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave chat?" xml:space="preserve"> + <source>Leave chat?</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group" xml:space="preserve"> @@ -3178,6 +4544,18 @@ To jest twój link do grupy %@!</target> <target>Połączone komputery</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="List" xml:space="preserve"> + <source>List</source> + <note>swipe action</note> + </trans-unit> + <trans-unit id="List name and emoji should be different for all lists." xml:space="preserve"> + <source>List name and emoji should be different for all lists.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List name..." xml:space="preserve"> + <source>List name...</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Live message!" xml:space="preserve"> <source>Live message!</source> <target>Wiadomość na żywo!</target> @@ -3208,11 +4586,6 @@ To jest twój link do grupy %@!</target> <target>Tryb blokady</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> - <source>Make a private connection</source> - <target>Nawiąż prywatne połączenie</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make one message disappear" xml:space="preserve"> <source>Make one message disappear</source> <target>Spraw, aby jedna wiadomość zniknęła</target> @@ -3223,21 +4596,11 @@ To jest twój link do grupy %@!</target> <target>Ustaw profil jako prywatny!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> - <target>Upewnij się, że adresy serwerów %@ są w poprawnym formacie, rozdzielone liniami i nie są zduplikowane (%@).</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> <target>Upewnij się, że adresy serwerów WebRTC ICE są w poprawnym formacie, rozdzielone liniami i nie są zduplikowane.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <target>Wiele osób pytało: *jeśli SimpleX nie ma identyfikatora użytkownika, jak może dostarczać wiadomości?*</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> <source>Mark deleted for everyone</source> <target>Oznacz jako usunięty dla wszystkich</target> @@ -3263,11 +4626,34 @@ To jest twój link do grupy %@!</target> <target>Maksymalnie 30 sekund, odbierane natychmiast.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Media & file servers" xml:space="preserve"> + <source>Media & file servers</source> + <target>Serwery mediów i plików</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Medium" xml:space="preserve"> + <source>Medium</source> + <target>Średni</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Member" xml:space="preserve"> <source>Member</source> <target>Członek</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member inactive" xml:space="preserve"> + <source>Member inactive</source> + <target>Członek nieaktywny</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Member reports" xml:space="preserve"> + <source>Member reports</source> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All chat members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All chat members will be notified.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> <target>Rola członka grupy zostanie zmieniona na "%@". Wszyscy członkowie grupy zostaną powiadomieni.</target> @@ -3278,11 +4664,63 @@ To jest twój link do grupy %@!</target> <target>Rola członka zostanie zmieniona na "%@". Członek otrzyma nowe zaproszenie.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from chat - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> <target>Członek zostanie usunięty z grupy - nie można tego cofnąć!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Members can add message reactions." xml:space="preserve"> + <source>Members can add message reactions.</source> + <target>Członkowie grupy mogą dodawać reakcje wiadomości.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Members can irreversibly delete sent messages. (24 hours)</source> + <target>Członkowie grupy mogą nieodwracalnie usuwać wysłane wiadomości. (24 godziny)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can report messsages to moderators." xml:space="preserve"> + <source>Members can report messsages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send SimpleX links." xml:space="preserve"> + <source>Members can send SimpleX links.</source> + <target>Członkowie grupy mogą wysyłać linki SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> + <target>Członkowie grupy mogą wysyłać bezpośrednie wiadomości.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> + <target>Członkowie grupy mogą wysyłać znikające wiadomości.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send files and media." xml:space="preserve"> + <source>Members can send files and media.</source> + <target>Członkowie grupy mogą wysyłać pliki i media.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> + <target>Członkowie grupy mogą wysyłać wiadomości głosowe.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mention members 👋" xml:space="preserve"> + <source>Mention members 👋</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Menus" xml:space="preserve"> + <source>Menus</source> + <target>Menu</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>Błąd dostarczenia wiadomości</target> @@ -3293,11 +4731,31 @@ To jest twój link do grupy %@!</target> <target>Potwierdzenia dostarczenia wiadomości!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message delivery warning" xml:space="preserve"> + <source>Message delivery warning</source> + <target>Ostrzeżenie dostarczenia wiadomości</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> <target>Wersja robocza wiadomości</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message forwarded" xml:space="preserve"> + <source>Message forwarded</source> + <target>Wiadomość przekazana</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message may be delivered later if member becomes active." xml:space="preserve"> + <source>Message may be delivered later if member becomes active.</source> + <target>Wiadomość może zostać dostarczona później jeśli członek stanie się aktywny.</target> + <note>item status description</note> + </trans-unit> + <trans-unit id="Message queue info" xml:space="preserve"> + <source>Message queue info</source> + <target>Informacje kolejki wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message reactions" xml:space="preserve"> <source>Message reactions</source> <target>Reakcje wiadomości</target> @@ -3308,16 +4766,51 @@ To jest twój link do grupy %@!</target> <target>Reakcje wiadomości są zabronione na tym czacie.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> - <source>Message reactions are prohibited in this group.</source> + <trans-unit id="Message reactions are prohibited." xml:space="preserve"> + <source>Message reactions are prohibited.</source> <target>Reakcje wiadomości są zabronione w tej grupie.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message reception" xml:space="preserve"> + <source>Message reception</source> + <target>Odebranie wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message servers" xml:space="preserve"> + <source>Message servers</source> + <target>Serwery wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message shape" xml:space="preserve"> + <source>Message shape</source> + <target>Kształt wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message source remains private." xml:space="preserve"> + <source>Message source remains private.</source> + <target>Źródło wiadomości pozostaje prywatne.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status" xml:space="preserve"> + <source>Message status</source> + <target>Status wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status: %@" xml:space="preserve"> + <source>Message status: %@</source> + <target>Status wiadomości: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> <target>Tekst wiadomości</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message too large" xml:space="preserve"> + <source>Message too large</source> + <target>Wiadomość jest zbyt duża</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> <target>Wiadomości</target> @@ -3333,11 +4826,75 @@ To jest twój link do grupy %@!</target> <target>Wiadomości od %@ zostaną pokazane!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Messages in this chat will never be deleted." xml:space="preserve"> + <source>Messages in this chat will never be deleted.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages received" xml:space="preserve"> + <source>Messages received</source> + <target>Otrzymane wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages sent" xml:space="preserve"> + <source>Messages sent</source> + <target>Wysłane wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages were deleted after you selected them." xml:space="preserve"> + <source>Messages were deleted after you selected them.</source> + <target>Wiadomości zostały usunięte po wybraniu ich.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Wiadomości, pliki i połączenia są chronione przez **szyfrowanie end-to-end** z doskonałym utajnianiem z wyprzedzeniem i odzyskiem po złamaniu.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>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.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate device" xml:space="preserve"> + <source>Migrate device</source> + <target>Zmigruj urządzenie</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate from another device" xml:space="preserve"> + <source>Migrate from another device</source> + <target>Zmigruj z innego urządzenia</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate here" xml:space="preserve"> + <source>Migrate here</source> + <target>Zmigruj tutaj</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device" xml:space="preserve"> + <source>Migrate to another device</source> + <target>Zmigruj do innego urządzenia</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device via QR code." xml:space="preserve"> + <source>Migrate to another device via QR code.</source> + <target>Zmigruj do innego urządzenia przez kod QR.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating" xml:space="preserve"> + <source>Migrating</source> + <target>Migrowanie</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migrating database archive…" xml:space="preserve"> <source>Migrating database archive…</source> <target>Migrowanie archiwum bazy danych…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Migration complete" xml:space="preserve"> + <source>Migration complete</source> + <target>Migracja zakończona</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> <target>Błąd migracji:</target> @@ -3353,9 +4910,9 @@ To jest twój link do grupy %@!</target> <target>Migracja została zakończona</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> - <target>Migracje: %@</target> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> + <target>Migracje:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> @@ -3373,21 +4930,29 @@ To jest twój link do grupy %@!</target> <target>Moderowany o: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="More" xml:space="preserve"> + <source>More</source> + <note>swipe action</note> + </trans-unit> <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> <target>Więcej ulepszeń już wkrótce!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="More reliable network connection." xml:space="preserve"> + <source>More reliable network connection.</source> + <target>Bardziej niezawodne połączenia sieciowe.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="More reliable notifications" xml:space="preserve"> + <source>More reliable notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> <source>Most likely this connection is deleted.</source> <target>Najprawdopodobniej to połączenie jest usunięte.</target> <note>item status description</note> </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> - <source>Most likely this contact has deleted the connection with you.</source> - <target>Najprawdopodobniej ten kontakt usunął połączenie z Tobą.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> <target>Wiele profili czatu</target> @@ -3396,7 +4961,11 @@ To jest twój link do grupy %@!</target> <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> <target>Wycisz</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Mute all" xml:space="preserve"> + <source>Mute all</source> + <note>notification label action</note> </trans-unit> <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> @@ -3406,13 +4975,36 @@ To jest twój link do grupy %@!</target> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> <target>Nazwa</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> <target>Sieć i serwery</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Network connection" xml:space="preserve"> + <source>Network connection</source> + <target>Połączenie z siecią</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network decentralization" xml:space="preserve"> + <source>Network decentralization</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve"> + <source>Network issues - message expired after many attempts to send it.</source> + <target>Błąd sieciowy - wiadomość wygasła po wielu próbach wysłania jej.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Network management" xml:space="preserve"> + <source>Network management</source> + <target>Zarządzenie sieciowe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network operator" xml:space="preserve"> + <source>Network operator</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> <target>Ustawienia sieci</target> @@ -3423,11 +5015,35 @@ To jest twój link do grupy %@!</target> <target>Status sieci</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New" xml:space="preserve"> + <source>New</source> + <note>token status text</note> + </trans-unit> <trans-unit id="New Passcode" xml:space="preserve"> <source>New Passcode</source> <target>Nowy Pin</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve"> + <source>New SOCKS credentials will be used every time you start the app.</source> + <target>Nowe poświadczenia SOCKS będą używane przy każdym uruchomieniu aplikacji.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve"> + <source>New SOCKS credentials will be used for each server.</source> + <target>Dla każdego serwera zostaną użyte nowe poświadczenia SOCKS.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat" xml:space="preserve"> + <source>New chat</source> + <target>Nowy czat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat experience 🎉" xml:space="preserve"> + <source>New chat experience 🎉</source> + <target>Nowe możliwości czatu 🎉</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> <target>Nowa prośba o kontakt</target> @@ -3438,11 +5054,6 @@ To jest twój link do grupy %@!</target> <target>Nowy kontakt:</target> <note>notification</note> </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> - <source>New database archive</source> - <target>Nowe archiwum bazy danych</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="New desktop app!" xml:space="preserve"> <source>New desktop app!</source> <target>Nowa aplikacja desktopowa!</target> @@ -3453,11 +5064,20 @@ To jest twój link do grupy %@!</target> <target>Nowa wyświetlana nazwa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <note>notification</note> + </trans-unit> <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> <target>Nowość w %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New media options" xml:space="preserve"> + <source>New media options</source> + <target>Nowe opcje mediów</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> <target>Nowa rola członka</target> @@ -3473,6 +5093,10 @@ To jest twój link do grupy %@!</target> <target>Nowe hasło…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New server" xml:space="preserve"> + <source>New server</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No" xml:space="preserve"> <source>No</source> <target>Nie</target> @@ -3483,6 +5107,18 @@ To jest twój link do grupy %@!</target> <target>Brak hasła aplikacji</target> <note>Authentication unavailable</note> </trans-unit> + <trans-unit id="No chats" xml:space="preserve"> + <source>No chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats found" xml:space="preserve"> + <source>No chats found</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats in list %@" xml:space="preserve"> + <source>No chats in list %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> <target>Nie wybrano kontaktów</target> @@ -3503,6 +5139,11 @@ To jest twój link do grupy %@!</target> <target>Brak tokenu urządzenia!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No direct connection yet, message is forwarded by admin." xml:space="preserve"> + <source>No direct connection yet, message is forwarded by admin.</source> + <target>Brak bezpośredniego połączenia, wiadomość została przekazana przez administratora.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="No filtered chats" xml:space="preserve"> <source>No filtered chats</source> <target>Brak filtrowanych czatów</target> @@ -3518,21 +5159,101 @@ To jest twój link do grupy %@!</target> <target>Brak historii</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No info, try to reload" xml:space="preserve"> + <source>No info, try to reload</source> + <target>Brak informacji, spróbuj przeładować</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No media & file servers." xml:space="preserve"> + <source>No media & file servers.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No message" xml:space="preserve"> + <source>No message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No message servers." xml:space="preserve"> + <source>No message servers.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No network connection" xml:space="preserve"> + <source>No network connection</source> + <target>Brak połączenia z siecią</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record speech" xml:space="preserve"> + <source>No permission to record speech</source> + <target>Brak zezwoleń do nagrania rozmowy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record video" xml:space="preserve"> + <source>No permission to record video</source> + <target>Brak zezwoleń do nagrania wideo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> <source>No permission to record voice message</source> <target>Brak uprawnień do nagrywania wiadomości głosowej</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No push server" xml:space="preserve"> + <source>No push server</source> + <target>Lokalnie</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> <target>Brak odebranych lub wysłanych plików</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No servers for private message routing." xml:space="preserve"> + <source>No servers for private message routing.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive files." xml:space="preserve"> + <source>No servers to receive files.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive messages." xml:space="preserve"> + <source>No servers to receive messages.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to send files." xml:space="preserve"> + <source>No servers to send files.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No token!" xml:space="preserve"> + <source>No token!</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="No unread chats" xml:space="preserve"> + <source>No unread chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> + <target>Brak identyfikatorów użytkownika.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Not compatible!" xml:space="preserve"> <source>Not compatible!</source> <target>Nie kompatybilny!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notes" xml:space="preserve"> + <source>Notes</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing selected" xml:space="preserve"> + <source>Nothing selected</source> + <target>Nic nie jest zaznaczone</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing to forward!" xml:space="preserve"> + <source>Nothing to forward!</source> + <target>Nic do przekazania!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> <target>Powiadomienia</target> @@ -3543,6 +5264,18 @@ To jest twój link do grupy %@!</target> <target>Powiadomienia są wyłączone!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notifications error" xml:space="preserve"> + <source>Notifications error</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Notifications privacy" xml:space="preserve"> + <source>Notifications privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications status" xml:space="preserve"> + <source>Notifications status</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. @@ -3552,44 +5285,43 @@ To jest twój link do grupy %@!</target> - wyłączyć członków (rola "obserwatora")</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="OK" xml:space="preserve"> + <source>OK</source> + <target>OK</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Off" xml:space="preserve"> <source>Off</source> <target>Wyłączony</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> - <source>Off (Local)</source> - <target>Wyłączony (Lokalnie)</target> - <note>No comment provided by engineer.</note> + <note>blur media</note> </trans-unit> <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> <target>Ok</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> <target>Stara baza danych</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> - <source>Old database archive</source> - <target>Stare archiwum bazy danych</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> <target>Jednorazowy link zaproszenia</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> - <target>Hosty onion będą wymagane do połączenia. Wymaga włączenia VPN.</target> + <trans-unit id="Onion hosts will be **required** for connection. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be **required** for connection. +Requires compatible VPN.</source> + <target>Hosty onion będą wymagane do połączenia. +Wymaga włączenia VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> - <target>Hosty onion będą używane, gdy będą dostępne. Wymaga włączenia VPN.</target> + <trans-unit id="Onion hosts will be used when available. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be used when available. +Requires compatible VPN.</source> + <target>Hosty onion będą używane, gdy będą dostępne. +Wymaga włączenia VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will not be used." xml:space="preserve"> @@ -3597,11 +5329,20 @@ To jest twój link do grupy %@!</target> <target>Hosty onion nie będą używane.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only chat owners can change preferences." xml:space="preserve"> + <source>Only chat owners can change preferences.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <target>Tylko urządzenia klienckie przechowują profile użytkowników, kontakty, grupy i wiadomości wysyłane za pomocą **2-warstwowego szyfrowania end-to-end**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only delete conversation" xml:space="preserve"> + <source>Only delete conversation</source> + <target>Usuń tylko rozmowę</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> <source>Only group owners can change group preferences.</source> <target>Tylko właściciele grup mogą zmieniać preferencje grupy.</target> @@ -3617,14 +5358,22 @@ To jest twój link do grupy %@!</target> <target>Tylko właściciele grup mogą włączyć wiadomości głosowe.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only sender and moderators see it" xml:space="preserve"> + <source>Only sender and moderators see it</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you and moderators see it" xml:space="preserve"> + <source>Only you and moderators see it</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only you can add message reactions." xml:space="preserve"> <source>Only you can add message reactions.</source> <target>Tylko Ty możesz dodawać reakcje wiadomości.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <target>Tylko Ty możesz nieodwracalnie usunąć wiadomości (Twój kontakt może oznaczyć je do usunięcia).</target> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source> + <target>Tylko Ty możesz nieodwracalnie usunąć wiadomości (Twój kontakt może oznaczyć je do usunięcia). (24 godziny)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can make calls." xml:space="preserve"> @@ -3647,9 +5396,9 @@ To jest twój link do grupy %@!</target> <target>Tylko Twój kontakt może dodawać reakcje wiadomości.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <target>Tylko Twój kontakt może nieodwracalnie usunąć wiadomości (możesz oznaczyć je do usunięcia).</target> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source> + <target>Tylko Twój kontakt może nieodwracalnie usunąć wiadomości (możesz oznaczyć je do usunięcia). (24 godziny)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can make calls." xml:space="preserve"> @@ -3670,13 +5419,17 @@ To jest twój link do grupy %@!</target> <trans-unit id="Open" xml:space="preserve"> <source>Open</source> <target>Otwórz</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Otwórz Ustawienia</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open changes" xml:space="preserve"> + <source>Open changes</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> <target>Otwórz czat</target> @@ -3687,26 +5440,81 @@ To jest twój link do grupy %@!</target> <target>Otwórz konsolę czatu</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Open conditions" xml:space="preserve"> + <source>Open conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open group" xml:space="preserve"> <source>Open group</source> <target>Grupa otwarta</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve"> - <source>Open user profiles</source> - <target>Otwórz profile użytkownika</target> + <trans-unit id="Open link?" xml:space="preserve"> + <source>Open link?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Open migration to another device" xml:space="preserve"> + <source>Open migration to another device</source> + <target>Otwórz migrację na innym urządzeniu</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> - <target>Otwarto źródłowy protokół i kod - każdy może uruchomić serwery.</target> + <trans-unit id="Opening app…" xml:space="preserve"> + <source>Opening app…</source> + <target>Otwieranie aplikacji…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve"> - <source>Opening database…</source> - <target>Otwieranie bazy danych…</target> + <trans-unit id="Operator" xml:space="preserve"> + <source>Operator</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Operator server" xml:space="preserve"> + <source>Operator server</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Or import archive file" xml:space="preserve"> + <source>Or import archive file</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or paste archive link" xml:space="preserve"> + <source>Or paste archive link</source> + <target>Lub wklej link archiwum</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or scan QR code" xml:space="preserve"> + <source>Or scan QR code</source> + <target>Lub zeskanuj kod QR</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or securely share this file link" xml:space="preserve"> + <source>Or securely share this file link</source> + <target>Lub bezpiecznie udostępnij ten link pliku</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or show this code" xml:space="preserve"> + <source>Or show this code</source> + <target>Lub pokaż ten kod</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or to share privately" xml:space="preserve"> + <source>Or to share privately</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Organize chats into lists" xml:space="preserve"> + <source>Organize chats into lists</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other" xml:space="preserve"> + <source>Other</source> + <target>Inne</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other file errors: %@" xml:space="preserve"> + <source>Other file errors: +%@</source> + <target>Inne błędy pliku: +%@</target> + <note>alert message</note> + </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> <target>Liczba PINGÓW</target> @@ -3742,15 +5550,20 @@ To jest twój link do grupy %@!</target> <target>Pin ustawiony!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Password" xml:space="preserve"> + <source>Password</source> + <target>Hasło</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> <target>Hasło do wyświetlenia</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste" xml:space="preserve"> - <source>Paste</source> - <target>Wklej</target> - <note>No comment provided by engineer.</note> + <trans-unit id="Past member %@" xml:space="preserve"> + <source>Past member %@</source> + <target>Były członek %@</target> + <note>past/unknown group member</note> </trans-unit> <trans-unit id="Paste desktop address" xml:space="preserve"> <source>Paste desktop address</source> @@ -3762,23 +5575,23 @@ To jest twój link do grupy %@!</target> <target>Wklej obraz</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve"> - <source>Paste received link</source> - <target>Wklej otrzymany link</target> + <trans-unit id="Paste link to connect!" xml:space="preserve"> + <source>Paste link to connect!</source> + <target>Wklej link, aby połączyć!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> - <source>Paste the link you received to connect with your contact.</source> - <target>Wklej otrzymany link w pole poniżej, aby połączyć się z kontaktem.</target> - <note>placeholder</note> - </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> - <target>Ludzie mogą się z Tobą połączyć tylko poprzez linki, które udostępniasz.</target> + <trans-unit id="Paste the link you received" xml:space="preserve"> + <source>Paste the link you received</source> + <target>Wklej link, który otrzymałeś</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Periodically" xml:space="preserve"> - <source>Periodically</source> + <trans-unit id="Pending" xml:space="preserve"> + <source>Pending</source> + <target>Oczekujące</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Periodic" xml:space="preserve"> + <source>Periodic</source> <target>Okresowo</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3787,11 +5600,33 @@ To jest twój link do grupy %@!</target> <target>Stały błąd odszyfrowania</target> <note>message decrypt error item</note> </trans-unit> + <trans-unit id="Picture-in-picture calls" xml:space="preserve"> + <source>Picture-in-picture calls</source> + <target>Połączenia obraz-w-obrazie</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Play from the chat list." xml:space="preserve"> + <source>Play from the chat list.</source> + <target>Odtwórz z listy czatów.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable calls." xml:space="preserve"> + <source>Please ask your contact to enable calls.</source> + <target>Poproś kontakt o włącznie połączeń.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> <target>Poproś Twój kontakt o włączenie wysyłania wiadomości głosowych.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers." xml:space="preserve"> + <source>Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers.</source> + <target>Sprawdź, czy telefon i komputer są podłączone do tej samej sieci lokalnej i czy zapora sieciowa komputera umożliwia połączenie. +Proszę podzielić się innymi problemami z deweloperami.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> <target>Sprawdź, czy użyłeś prawidłowego linku lub poproś Twój kontakt o przesłanie innego.</target> @@ -3807,6 +5642,18 @@ To jest twój link do grupy %@!</target> <target>Proszę sprawdzić preferencje Twoje i Twojego kontaktu.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve"> + <source>Please confirm that network settings are correct for this device.</source> + <target>Proszę potwierdzić, że ustawienia sieciowe są prawidłowe dla tego urządzenia.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact developers. Error: %@" xml:space="preserve"> + <source>Please contact developers. +Error: %@</source> + <target>Proszę skontaktować się z deweloperami. +Błąd: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> <target>Skontaktuj się z administratorem grupy.</target> @@ -3814,12 +5661,12 @@ To jest twój link do grupy %@!</target> </trans-unit> <trans-unit id="Please enter correct current passphrase." xml:space="preserve"> <source>Please enter correct current passphrase.</source> - <target>Wprowadź poprawne aktualne hasło.</target> + <target>Wprowadź poprawny obecny kod dostępu.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please enter the previous password after restoring database backup. This action can not be undone." xml:space="preserve"> <source>Please enter the previous password after restoring database backup. This action can not be undone.</source> - <target>Proszę podać poprzednie hasło po przywróceniu kopii zapasowej bazy danych. Tej czynności nie można cofnąć.</target> + <target>Proszę podać poprzedni kod dostępu po przywróceniu kopii zapasowej bazy danych. Tej czynności nie można cofnąć.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please remember or store it securely - there is no way to recover a lost passcode!" xml:space="preserve"> @@ -3839,19 +5686,36 @@ To jest twój link do grupy %@!</target> </trans-unit> <trans-unit id="Please store passphrase securely, you will NOT be able to access chat if you lose it." xml:space="preserve"> <source>Please store passphrase securely, you will NOT be able to access chat if you lose it.</source> - <target>Prosimy o bezpieczne przechowywanie hasła, w przypadku jego utraty NIE będzie można uzyskać dostępu do czatu.</target> + <target>Przechowuj kod dostępu w bezpieczny sposób, w przypadku jego utraty NIE będzie można uzyskać dostępu do czatu.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please store passphrase securely, you will NOT be able to change it if you lose it." xml:space="preserve"> <source>Please store passphrase securely, you will NOT be able to change it if you lose it.</source> - <target>Prosimy o bezpieczne przechowywanie hasła, w przypadku jego utraty NIE będzie można go zmienić.</target> + <target>Przechowuj kod dostępu w bezpieczny sposób, w przypadku jego utraty NIE będzie można go zmienić.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please try to disable and re-enable notfications." xml:space="preserve"> + <source>Please try to disable and re-enable notfications.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token activation to complete." xml:space="preserve"> + <source>Please wait for token activation to complete.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token to be registered." xml:space="preserve"> + <source>Please wait for token to be registered.</source> + <note>token info</note> + </trans-unit> <trans-unit id="Polish interface" xml:space="preserve"> <source>Polish interface</source> <target>Polski interfejs</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Port" xml:space="preserve"> + <source>Port</source> + <target>Port</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> <target>Możliwe, że odcisk palca certyfikatu w adresie serwera jest nieprawidłowy</target> @@ -3862,36 +5726,81 @@ To jest twój link do grupy %@!</target> <target>Zachowaj ostatnią wersję roboczą wiadomości wraz z załącznikami.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Preset server" xml:space="preserve"> - <source>Preset server</source> - <target>Wstępnie ustawiony serwer</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> <target>Wstępnie ustawiony adres serwera</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Preset servers" xml:space="preserve"> + <source>Preset servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Preview" xml:space="preserve"> <source>Preview</source> <target>Podgląd</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Previously connected servers" xml:space="preserve"> + <source>Previously connected servers</source> + <target>Wcześniej połączone serwery</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> <target>Prywatność i bezpieczeństwo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Privacy for your customers." xml:space="preserve"> + <source>Privacy for your customers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy policy and conditions of use." xml:space="preserve"> + <source>Privacy policy and conditions of use.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy redefined" xml:space="preserve"> <source>Privacy redefined</source> <target>Redefinicja prywatności</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve"> + <source>Private chats, groups and your contacts are not accessible to server operators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> <target>Prywatne nazwy plików</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private media file names." xml:space="preserve"> + <source>Private media file names.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing" xml:space="preserve"> + <source>Private message routing</source> + <target>Trasowanie prywatnych wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing 🚀" xml:space="preserve"> + <source>Private message routing 🚀</source> + <target>Trasowanie prywatnych wiadomości🚀</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private notes" xml:space="preserve"> + <source>Private notes</source> + <target>Prywatne notatki</target> + <note>name of notes to self</note> + </trans-unit> + <trans-unit id="Private routing" xml:space="preserve"> + <source>Private routing</source> + <target>Prywatne trasowanie</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private routing error" xml:space="preserve"> + <source>Private routing error</source> + <target>Błąd prywatnego trasowania</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> <target>Profil i połączenia z serwerem</target> @@ -3902,14 +5811,9 @@ To jest twój link do grupy %@!</target> <target>Zdjęcie profilowe</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile name" xml:space="preserve"> - <source>Profile name</source> - <target>Nazwa profilu</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile name:" xml:space="preserve"> - <source>Profile name:</source> - <target>Nazwa profilu:</target> + <trans-unit id="Profile images" xml:space="preserve"> + <source>Profile images</source> + <target>Zdjęcia profilowe</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile password" xml:space="preserve"> @@ -3917,10 +5821,15 @@ To jest twój link do grupy %@!</target> <target>Hasło profilu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Profile theme" xml:space="preserve"> + <source>Profile theme</source> + <target>Motyw profilu</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> <source>Profile update will be sent to your contacts.</source> <target>Aktualizacja profilu zostanie wysłana do Twoich kontaktów.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> @@ -3942,6 +5851,15 @@ To jest twój link do grupy %@!</target> <target>Zabroń reakcje wiadomości.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Prohibit reporting messages to moderators." xml:space="preserve"> + <source>Prohibit reporting messages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending SimpleX links." xml:space="preserve"> + <source>Prohibit sending SimpleX links.</source> + <target>Zabroń wysyłania linków SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> <target>Zabroń wysyłania bezpośrednich wiadomości do członków.</target> @@ -3962,11 +5880,23 @@ To jest twój link do grupy %@!</target> <target>Zabroń wysyłania wiadomości głosowych.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect IP address" xml:space="preserve"> + <source>Protect IP address</source> + <target>Chroń adres IP</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> <target>Chroń ekran aplikacji</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve"> + <source>Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings.</source> + <target>Chroni Twój adres IP przed przekaźnikami wiadomości wybranych przez Twoje kontakty. +Włącz w ustawianiach *Sieć i serwery* .</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> <target>Chroń swoje profile czatu hasłem!</target> @@ -3982,16 +5912,46 @@ To jest twój link do grupy %@!</target> <target>Limit czasu protokołu na KB</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Proxied" xml:space="preserve"> + <source>Proxied</source> + <target>Trasowane przez proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied servers" xml:space="preserve"> + <source>Proxied servers</source> + <target>Serwery trasowane przez proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxy requires password" xml:space="preserve"> + <source>Proxy requires password</source> + <target>Proxy wymaga hasła</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> <target>Powiadomienia push</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Push server" xml:space="preserve"> + <source>Push server</source> + <target>Serwer Push</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Quantum resistant encryption" xml:space="preserve"> + <source>Quantum resistant encryption</source> + <target>Kwantowo odporne szyfrowanie</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> <source>Rate the app</source> <target>Oceń aplikację</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reachable chat toolbar" xml:space="preserve"> + <source>Reachable chat toolbar</source> + <target>Osiągalny pasek narzędzi czatu</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> <target>Reaguj…</target> @@ -4000,16 +5960,21 @@ To jest twój link do grupy %@!</target> <trans-unit id="Read" xml:space="preserve"> <source>Read</source> <target>Czytaj</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Read more" xml:space="preserve"> <source>Read more</source> <target>Przeczytaj więcej</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> - <target>Przeczytaj więcej w [Podręczniku Użytkownika](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source> + <target>Przeczytaj więcej w [Poradniku Użytkownika](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</source> + <target>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).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> @@ -4017,11 +5982,6 @@ To jest twój link do grupy %@!</target> <target>Przeczytaj więcej w [Podręczniku Użytkownika](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> - <source>Read more in our GitHub repository.</source> - <target>Przeczytaj więcej na naszym repozytorium GitHub.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> <target>Przeczytaj więcej na naszym [repozytorium GitHub](https://github.com/simplex-chat/simplex-chat#readme).</target> @@ -4032,6 +5992,11 @@ To jest twój link do grupy %@!</target> <target>Potwierdzenia są wyłączone</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receive errors" xml:space="preserve"> + <source>Receive errors</source> + <target>Błędy otrzymania</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Otrzymane o</target> @@ -4052,6 +6017,21 @@ To jest twój link do grupy %@!</target> <target>Otrzymano wiadomość</target> <note>message info title</note> </trans-unit> + <trans-unit id="Received messages" xml:space="preserve"> + <source>Received messages</source> + <target>Otrzymane wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received reply" xml:space="preserve"> + <source>Received reply</source> + <target>Otrzymano odpowiedź</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received total" xml:space="preserve"> + <source>Received total</source> + <target>Otrzymano łącznie</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> <target>Adres odbiorczy zostanie zmieniony na inny serwer. Zmiana adresu zostanie zakończona gdy nadawca będzie online.</target> @@ -4067,16 +6047,51 @@ To jest twój link do grupy %@!</target> <target>Odbieranie przez</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Recent history and improved [directory bot](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)." xml:space="preserve"> + <source>Recent history and improved [directory bot](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).</source> + <target>Ostania historia i ulepszony [bot adresowy](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).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve"> + <source>Recipient(s) can't see who this message is from.</source> + <target>Odbiorca/y nie mogą zobaczyć od kogo jest ta wiadomość.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> <target>Odbiorcy widzą aktualizacje podczas ich wpisywania.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect" xml:space="preserve"> + <source>Reconnect</source> + <target>Połącz ponownie</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> <target>Połącz ponownie wszystkie połączone serwery, aby wymusić dostarczanie wiadomości. Wykorzystuje dodatkowy ruch.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect all servers" xml:space="preserve"> + <source>Reconnect all servers</source> + <target>Połącz ponownie wszystkie serwery</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers?" xml:space="preserve"> + <source>Reconnect all servers?</source> + <target>Połączyć ponownie wszystkie serwery?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect server to force message delivery. It uses additional traffic.</source> + <target>Ponownie połącz z serwerem w celu wymuszenia dostarczenia wiadomości. Wykorzystuje to dodatkowy ruch.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server?" xml:space="preserve"> + <source>Reconnect server?</source> + <target>Połączyć ponownie serwer?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> <target>Ponownie połączyć serwery?</target> @@ -4097,10 +6112,23 @@ To jest twój link do grupy %@!</target> <target>Zmniejszone zużycie baterii</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Register" xml:space="preserve"> + <source>Register</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Register notification token?" xml:space="preserve"> + <source>Register notification token?</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Registered" xml:space="preserve"> + <source>Registered</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> <target>Odrzuć</target> - <note>reject incoming call via notification</note> + <note>reject incoming call via notification +swipe action</note> </trans-unit> <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> <source>Reject (sender NOT notified)</source> @@ -4127,6 +6155,16 @@ To jest twój link do grupy %@!</target> <target>Usuń</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Remove archive?" xml:space="preserve"> + <source>Remove archive?</source> + <target>Usunąć archiwum?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove image" xml:space="preserve"> + <source>Remove image</source> + <target>Usuń obraz</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> <target>Usuń członka</target> @@ -4162,16 +6200,71 @@ To jest twój link do grupy %@!</target> <target>Powtórzyć prośbę połączenia?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat download" xml:space="preserve"> + <source>Repeat download</source> + <target>Powtórz pobieranie</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat import" xml:space="preserve"> + <source>Repeat import</source> + <target>Powtórz importowanie</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Repeat join request?" xml:space="preserve"> <source>Repeat join request?</source> <target>Powtórzyć prośbę dołączenia?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat upload" xml:space="preserve"> + <source>Repeat upload</source> + <target>Powtórz wgrywanie</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reply" xml:space="preserve"> <source>Reply</source> <target>Odpowiedz</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Report" xml:space="preserve"> + <source>Report</source> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Report content: only group moderators will see it." xml:space="preserve"> + <source>Report content: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report member profile: only group moderators will see it." xml:space="preserve"> + <source>Report member profile: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report other: only group moderators will see it." xml:space="preserve"> + <source>Report other: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report reason?" xml:space="preserve"> + <source>Report reason?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Report spam: only group moderators will see it." xml:space="preserve"> + <source>Report spam: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report violation: only group moderators will see it." xml:space="preserve"> + <source>Report violation: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report: %@" xml:space="preserve"> + <source>Report: %@</source> + <note>report in notification</note> + </trans-unit> + <trans-unit id="Reporting messages to moderators is prohibited." xml:space="preserve"> + <source>Reporting messages to moderators is prohibited.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reports" xml:space="preserve"> + <source>Reports</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Required" xml:space="preserve"> <source>Required</source> <target>Wymagane</target> @@ -4182,16 +6275,41 @@ To jest twój link do grupy %@!</target> <target>Resetuj</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset all hints" xml:space="preserve"> + <source>Reset all hints</source> + <target>Zresetuj wszystkie wskazówki</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics" xml:space="preserve"> + <source>Reset all statistics</source> + <target>Resetuj wszystkie statystyki</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics?" xml:space="preserve"> + <source>Reset all statistics?</source> + <target>Zresetować wszystkie statystyki?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> <target>Resetuj kolory</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to app theme" xml:space="preserve"> + <source>Reset to app theme</source> + <target>Zresetuj do motywu aplikacji</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> <target>Przywróć wartości domyślne</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to user theme" xml:space="preserve"> + <source>Reset to user theme</source> + <target>Zresetuj do motywu użytkownika</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> <source>Restart the app to create a new chat profile</source> <target>Uruchom ponownie aplikację, aby utworzyć nowy profil czatu</target> @@ -4222,14 +6340,18 @@ To jest twój link do grupy %@!</target> <target>Błąd przywracania bazy danych</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Retry" xml:space="preserve"> + <source>Retry</source> + <target>Ponów</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> <target>Ujawnij</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Revert" xml:space="preserve"> - <source>Revert</source> - <target>Przywrócić</target> + <trans-unit id="Review conditions" xml:space="preserve"> + <source>Review conditions</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke" xml:space="preserve"> @@ -4257,51 +6379,66 @@ To jest twój link do grupy %@!</target> <target>Uruchom czat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve"> - <source>SMP servers</source> - <target>Serwery SMP</target> + <trans-unit id="SMP server" xml:space="preserve"> + <source>SMP server</source> + <target>Serwer SMP</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SOCKS proxy" xml:space="preserve"> + <source>SOCKS proxy</source> + <target>Proxy SOCKS</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safely receive files" xml:space="preserve"> + <source>Safely receive files</source> + <target>Bezpiecznie otrzymuj pliki</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safer groups" xml:space="preserve"> + <source>Safer groups</source> + <target>Bezpieczniejsze grupy</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save" xml:space="preserve"> <source>Save</source> <target>Zapisz</target> - <note>chat item action</note> + <note>alert button +chat item action</note> </trans-unit> <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> <target>Zapisz (i powiadom kontakty)</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> <target>Zapisz i powiadom kontakt</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> <target>Zapisz i powiadom członków grupy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save and reconnect" xml:space="preserve"> + <source>Save and reconnect</source> + <target>Zapisz i połącz ponownie</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save and update group profile" xml:space="preserve"> <source>Save and update group profile</source> <target>Zapisz i zaktualizuj profil grupowy</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save archive" xml:space="preserve"> - <source>Save archive</source> - <target>Zapisz archiwum</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve"> - <source>Save auto-accept settings</source> - <target>Zapisz ustawienia automatycznej akceptacji</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> <source>Save group profile</source> <target>Zapisz profil grupy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save list" xml:space="preserve"> + <source>Save list</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> <target>Zapisz hasło i otwórz czat</target> @@ -4315,7 +6452,7 @@ To jest twój link do grupy %@!</target> <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> <target>Zapisać preferencje?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> @@ -4330,23 +6467,53 @@ To jest twój link do grupy %@!</target> <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> <target>Zapisać serwery?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve"> - <source>Save settings?</source> - <target>Zapisać ustawienia?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> <target>Zapisać wiadomość powitalną?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save your profile?" xml:space="preserve"> + <source>Save your profile?</source> + <target>Zapisać Twój profil?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Saved" xml:space="preserve"> + <source>Saved</source> + <target>Zapisane</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> <source>Saved WebRTC ICE servers will be removed</source> <target>Zapisane serwery WebRTC ICE zostaną usunięte</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Saved from" xml:space="preserve"> + <source>Saved from</source> + <target>Zapisane od</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved message" xml:space="preserve"> + <source>Saved message</source> + <target>Zachowano wiadomość</target> + <note>message info title</note> + </trans-unit> + <trans-unit id="Saving %lld messages" xml:space="preserve"> + <source>Saving %lld messages</source> + <target>Zapisywanie %lld wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scale" xml:space="preserve"> + <source>Scale</source> + <target>Skaluj</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan / Paste link" xml:space="preserve"> + <source>Scan / Paste link</source> + <target>Skanuj / Wklej link</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> <target>Zeskanuj kod QR</target> @@ -4377,11 +6544,31 @@ To jest twój link do grupy %@!</target> <target>Szukaj</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Search bar accepts invitation links." xml:space="preserve"> + <source>Search bar accepts invitation links.</source> + <target>Pasek wyszukiwania akceptuje linki zaproszenia.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search or paste SimpleX link" xml:space="preserve"> + <source>Search or paste SimpleX link</source> + <target>Wyszukaj lub wklej link SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secondary" xml:space="preserve"> + <source>Secondary</source> + <target>Drugorzędny</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> <source>Secure queue</source> <target>Bezpieczna kolejka</target> <note>server test step</note> </trans-unit> + <trans-unit id="Secured" xml:space="preserve"> + <source>Secured</source> + <target>Zabezpieczone</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> <target>Ocena bezpieczeństwa</target> @@ -4395,6 +6582,21 @@ To jest twój link do grupy %@!</target> <trans-unit id="Select" xml:space="preserve"> <source>Select</source> <target>Wybierz</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Select chat profile" xml:space="preserve"> + <source>Select chat profile</source> + <target>Wybierz profil czatu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected %lld" xml:space="preserve"> + <source>Selected %lld</source> + <target>Zaznaczono %lld</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Wybrane preferencje czatu zabraniają tej wiadomości.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct" xml:space="preserve"> @@ -4432,11 +6634,6 @@ To jest twój link do grupy %@!</target> <target>Wyślij potwierdzenia dostawy do</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve"> - <source>Send direct message</source> - <target>Wyślij wiadomość bezpośrednią</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Send direct message to connect" xml:space="preserve"> <source>Send direct message to connect</source> <target>Wyślij wiadomość bezpośrednią aby połączyć</target> @@ -4447,6 +6644,11 @@ To jest twój link do grupy %@!</target> <target>Wyślij znikającą wiadomość</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send errors" xml:space="preserve"> + <source>Send errors</source> + <target>Wyślij błędy</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> <target>Wyślij podgląd linku</target> @@ -4457,14 +6659,28 @@ To jest twój link do grupy %@!</target> <target>Wyślij wiadomość na żywo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send message to enable calls." xml:space="preserve"> + <source>Send message to enable calls.</source> + <target>Wyślij wiadomość aby włączyć połączenia.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source> + <target>Wysyłaj wiadomości bezpośrednio, gdy adres IP jest chroniony i Twój lub docelowy serwer nie obsługuje prywatnego trasowania.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when your or destination server does not support private routing.</source> + <target>Wysyłaj wiadomości bezpośrednio, gdy Twój lub docelowy serwer nie obsługuje prywatnego trasowania.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> <target>Wyślij powiadomienia</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> - <source>Send notifications:</source> - <target>Wyślij powiadomienia:</target> + <trans-unit id="Send private reports" xml:space="preserve"> + <source>Send private reports</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve"> @@ -4482,10 +6698,15 @@ To jest twój link do grupy %@!</target> <target>Wyślij je z galerii lub niestandardowych klawiatur.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve"> + <source>Send up to 100 last messages to new members.</source> + <target>Wysyłaj do 100 ostatnich wiadomości do nowych członków.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> <target>Nadawca anulował transfer pliku.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> @@ -4542,6 +6763,11 @@ To jest twój link do grupy %@!</target> <target>Wysłano o: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Sent directly" xml:space="preserve"> + <source>Sent directly</source> + <target>Wysłano bezpośrednio</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> <target>Wyślij zdarzenie pliku</target> @@ -4552,11 +6778,67 @@ To jest twój link do grupy %@!</target> <target>Wyślij wiadomość</target> <note>message info title</note> </trans-unit> + <trans-unit id="Sent messages" xml:space="preserve"> + <source>Sent messages</source> + <target>Wysłane wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> <target>Wysłane wiadomości zostaną usunięte po ustawionym czasie.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sent reply" xml:space="preserve"> + <source>Sent reply</source> + <target>Wyślij odpowiedź</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent total" xml:space="preserve"> + <source>Sent total</source> + <target>Wysłano łącznie</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent via proxy" xml:space="preserve"> + <source>Sent via proxy</source> + <target>Wysłano przez proxy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server" xml:space="preserve"> + <source>Server</source> + <target>Serwer</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server added to operator %@." xml:space="preserve"> + <source>Server added to operator %@.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Server address" xml:space="preserve"> + <source>Server address</source> + <target>Adres serwera</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings." xml:space="preserve"> + <source>Server address is incompatible with network settings.</source> + <target>Adres serwera jest niekompatybilny z ustawieniami sieciowymi.</target> + <note>srv error text.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Server address is incompatible with network settings: %@.</source> + <target>Adres serwera jest niekompatybilny z ustawieniami sieci: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server operator changed." xml:space="preserve"> + <source>Server operator changed.</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Server operators" xml:space="preserve"> + <source>Server operators</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server protocol changed." xml:space="preserve"> + <source>Server protocol changed.</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> <target>Serwer wymaga autoryzacji do tworzenia kolejek, sprawdź hasło</target> @@ -4572,11 +6854,36 @@ To jest twój link do grupy %@!</target> <target>Test serwera nie powiódł się!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Server type" xml:space="preserve"> + <source>Server type</source> + <target>Typ serwera</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server version is incompatible with network settings." xml:space="preserve"> + <source>Server version is incompatible with network settings.</source> + <target>Wersja serwera jest niekompatybilna z ustawieniami sieciowymi.</target> + <note>srv error text</note> + </trans-unit> + <trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve"> + <source>Server version is incompatible with your app: %@.</source> + <target>Wersja serwera jest niekompatybilna z aplikacją: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Servers" xml:space="preserve"> <source>Servers</source> <target>Serwery</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Servers info" xml:space="preserve"> + <source>Servers info</source> + <target>Informacje o serwerach</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <target>Statystyki serwerów zostaną zresetowane - nie można tego cofnąć!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Session code" xml:space="preserve"> <source>Session code</source> <target>Kod sesji</target> @@ -4587,11 +6894,20 @@ To jest twój link do grupy %@!</target> <target>Ustaw 1 dzień</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set chat name…" xml:space="preserve"> + <source>Set chat name…</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> <target>Ustaw nazwę kontaktu…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve"> + <source>Set default theme</source> + <target>Ustaw domyślny motyw</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> <target>Ustaw preferencje grupy</target> @@ -4602,11 +6918,20 @@ To jest twój link do grupy %@!</target> <target>Ustaw go zamiast uwierzytelniania systemowego.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve"> + <source>Set message expiration in chats.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passcode" xml:space="preserve"> <source>Set passcode</source> <target>Ustaw pin</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set passphrase" xml:space="preserve"> + <source>Set passphrase</source> + <target>Ustaw hasło</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> <target>Ustaw hasło do eksportu</target> @@ -4627,24 +6952,52 @@ To jest twój link do grupy %@!</target> <target>Ustawienia</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve"> + <source>Settings were changed.</source> + <target>Ustawienia zostały zmienione.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve"> + <source>Shape profile images</source> + <target>Kształtuj obrazy profilowe</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share" xml:space="preserve"> <source>Share</source> <target>Udostępnij</target> - <note>chat item action</note> + <note>alert action +chat item action</note> </trans-unit> <trans-unit id="Share 1-time link" xml:space="preserve"> <source>Share 1-time link</source> <target>Udostępnij 1-razowy link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share 1-time link with a friend" xml:space="preserve"> + <source>Share 1-time link with a friend</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve"> + <source>Share SimpleX address on social media.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address" xml:space="preserve"> <source>Share address</source> <target>Udostępnij adres</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share address publicly" xml:space="preserve"> + <source>Share address publicly</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address with contacts?" xml:space="preserve"> <source>Share address with contacts?</source> <target>Udostępnić adres kontaktom?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Share from other apps." xml:space="preserve"> + <source>Share from other apps.</source> + <target>Udostępnij z innych aplikacji.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share link" xml:space="preserve"> @@ -4652,9 +7005,19 @@ To jest twój link do grupy %@!</target> <target>Udostępnij link</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve"> - <source>Share one-time invitation link</source> - <target>Jednorazowy link zaproszenia</target> + <trans-unit id="Share profile" xml:space="preserve"> + <source>Share profile</source> + <target>Udostępnij profil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share this 1-time invite link" xml:space="preserve"> + <source>Share this 1-time invite link</source> + <target>Udostępnij ten jednorazowy link</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share to SimpleX" xml:space="preserve"> + <source>Share to SimpleX</source> + <target>Udostępnij do SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share with contacts" xml:space="preserve"> @@ -4662,6 +7025,15 @@ To jest twój link do grupy %@!</target> <target>Udostępnij kontaktom</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Short link" xml:space="preserve"> + <source>Short link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show QR code" xml:space="preserve"> + <source>Show QR code</source> + <target>Pokaż kod QR</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> <target>Pokaż połączenia w historii telefonu</target> @@ -4677,21 +7049,45 @@ To jest twój link do grupy %@!</target> <target>Pokaż ostatnie wiadomości</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show message status" xml:space="preserve"> + <source>Show message status</source> + <target>Pokaż status wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show percentage" xml:space="preserve"> + <source>Show percentage</source> + <target>Pokaż procent</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Pokaż podgląd</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show → on messages sent via private routing." xml:space="preserve"> + <source>Show → on messages sent via private routing.</source> + <target>Pokaż → na wiadomościach wysłanych przez prywatne trasowanie.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show:" xml:space="preserve"> <source>Show:</source> <target>Pokaż:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve"> + <source>SimpleX</source> + <target>SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Address" xml:space="preserve"> <source>SimpleX Address</source> <target>Adres SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." xml:space="preserve"> + <source>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> <target>Bezpieczeństwo SimpleX Chat zostało zaudytowane przez Trail of Bits.</target> @@ -4722,6 +7118,18 @@ To jest twój link do grupy %@!</target> <target>Adres SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX address and 1-time links are safe to share via any messenger." xml:space="preserve"> + <source>SimpleX address and 1-time links are safe to share via any messenger.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address or 1-time link?" xml:space="preserve"> + <source>SimpleX address or 1-time link?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX channel link" xml:space="preserve"> + <source>SimpleX channel link</source> + <note>simplex link type</note> + </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> <target>Adres kontaktowy SimpleX</target> @@ -4740,6 +7148,16 @@ To jest twój link do grupy %@!</target> <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> <target>Linki SimpleX</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve"> + <source>SimpleX links are prohibited.</source> + <target>Linki SimpleX są zablokowane na tej grupie.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve"> + <source>SimpleX links not allowed</source> + <target>Linki SimpleX są niedozwolone</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> @@ -4747,11 +7165,20 @@ To jest twój link do grupy %@!</target> <target>Zaproszenie jednorazowe SimpleX</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> + <source>SimpleX protocols reviewed by Trail of Bits.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Simplified incognito mode" xml:space="preserve"> <source>Simplified incognito mode</source> <target>Uproszczony tryb incognito</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Size" xml:space="preserve"> + <source>Size</source> + <target>Rozmiar</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Pomiń</target> @@ -4767,19 +7194,49 @@ To jest twój link do grupy %@!</target> <target>Małe grupy (maks. 20)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Soft" xml:space="preserve"> + <source>Soft</source> + <target>Łagodny</target> + <note>blur media</note> + </trans-unit> + <trans-unit id="Some app settings were not migrated." xml:space="preserve"> + <source>Some app settings were not migrated.</source> + <target>Niektóre ustawienia aplikacji nie zostały zmigrowane.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some file(s) were not exported:" xml:space="preserve"> + <source>Some file(s) were not exported:</source> + <target>Niektóre plik(i) nie zostały wyeksportowane:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Podczas importu wystąpiły niekrytyczne błędy - więcej szczegółów można znaleźć w konsoli czatu.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import:" xml:space="preserve"> + <source>Some non-fatal errors occurred during import:</source> + <target>Podczas importu wystąpiły niekrytyczne błędy:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some servers failed the test: %@" xml:space="preserve"> + <source>Some servers failed the test: +%@</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> <target>Ktoś</target> <note>notification title</note> </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve"> - <source>Start a new chat</source> - <target>Rozpocznij nowy czat</target> + <trans-unit id="Spam" xml:space="preserve"> + <source>Spam</source> + <note>blocking reason +report reason</note> + </trans-unit> + <trans-unit id="Square, circle, or anything in between." xml:space="preserve"> + <source>Square, circle, or anything in between.</source> + <target>Kwadrat, okrąg lub cokolwiek pomiędzy.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start chat" xml:space="preserve"> @@ -4787,11 +7244,26 @@ To jest twój link do grupy %@!</target> <target>Rozpocznij czat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Start chat?" xml:space="preserve"> + <source>Start chat?</source> + <target>Rozpocząć czat?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> <target>Rozpocznij migrację</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Starting from %@." xml:space="preserve"> + <source>Starting from %@.</source> + <target>Zaczynanie od %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Statistics" xml:space="preserve"> + <source>Statistics</source> + <target>Statystyki</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> <target>Zatrzymaj</target> @@ -4802,9 +7274,9 @@ To jest twój link do grupy %@!</target> <target>Zatrzymaj SimpleX</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> - <source>Stop chat to enable database actions</source> - <target>Zatrzymaj czat, aby umożliwić działania na bazie danych</target> + <trans-unit id="Stop chat" xml:space="preserve"> + <source>Stop chat</source> + <target>Zatrzymaj czat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> @@ -4835,23 +7307,60 @@ To jest twój link do grupy %@!</target> <trans-unit id="Stop sharing" xml:space="preserve"> <source>Stop sharing</source> <target>Przestań udostępniać</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Stop sharing address?" xml:space="preserve"> <source>Stop sharing address?</source> <target>Przestać udostępniać adres?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Stopping chat" xml:space="preserve"> + <source>Stopping chat</source> + <target>Zatrzymywanie czatu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Storage" xml:space="preserve"> + <source>Storage</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Strong" xml:space="preserve"> + <source>Strong</source> + <target>Silne</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Submit" xml:space="preserve"> <source>Submit</source> <target>Zatwierdź</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Subscribed" xml:space="preserve"> + <source>Subscribed</source> + <target>Zasubskrybowano</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscription errors" xml:space="preserve"> + <source>Subscription errors</source> + <target>Błędy subskrypcji</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscriptions ignored" xml:space="preserve"> + <source>Subscriptions ignored</source> + <target>Subskrypcje zignorowane</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Support SimpleX Chat" xml:space="preserve"> <source>Support SimpleX Chat</source> <target>Wspieraj SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Switch audio and video during the call." xml:space="preserve"> + <source>Switch audio and video during the call.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> + <source>Switch chat profile for 1-time invitations.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="System" xml:space="preserve"> <source>System</source> <target>System</target> @@ -4862,11 +7371,20 @@ To jest twój link do grupy %@!</target> <target>Uwierzytelnianie systemu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP connection" xml:space="preserve"> + <source>TCP connection</source> + <target>Połączenie TCP</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> <target>Limit czasu połączenia TCP</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP port for messaging" xml:space="preserve"> + <source>TCP port for messaging</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <source>TCP_KEEPCNT</source> <target>TCP_KEEPCNT</target> @@ -4882,11 +7400,20 @@ To jest twój link do grupy %@!</target> <target>TCP_KEEPINTVL</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tail" xml:space="preserve"> + <source>Tail</source> + <target>Ogon</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> <target>Zrób zdjęcie</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap Create SimpleX address in the menu to create it later." xml:space="preserve"> + <source>Tap Create SimpleX address in the menu to create it later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> <target>Naciśnij przycisk </target> @@ -4912,16 +7439,30 @@ To jest twój link do grupy %@!</target> <target>Dotnij, aby dołączyć w trybie incognito</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve"> - <source>Tap to start a new chat</source> - <target>Dotknij, aby rozpocząć nowy czat</target> + <trans-unit id="Tap to paste link" xml:space="preserve"> + <source>Tap to paste link</source> + <target>Dotknij, aby wkleić link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap to scan" xml:space="preserve"> + <source>Tap to scan</source> + <target>Dotknij, aby zeskanować</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Temporary file error" xml:space="preserve"> + <source>Temporary file error</source> + <target>Tymczasowy błąd pliku</target> + <note>file error alert title</note> + </trans-unit> <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> <target>Test nie powiódł się na etapie %@.</target> <note>server test failure</note> </trans-unit> + <trans-unit id="Test notifications" xml:space="preserve"> + <source>Test notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> <target>Przetestuj serwer</target> @@ -4935,7 +7476,7 @@ To jest twój link do grupy %@!</target> <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> <target>Testy nie powiodły się!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> @@ -4952,11 +7493,6 @@ To jest twój link do grupy %@!</target> <target>Podziękowania dla użytkowników - wkład za pośrednictwem Weblate!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> - <target>Pierwsza platforma bez żadnych identyfikatorów użytkowników – z założenia prywatna.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <source>The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised.</source> @@ -4969,11 +7505,29 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom <target>Aplikacja może powiadamiać Cię, gdy otrzymujesz wiadomości lub prośby o kontakt — otwórz ustawienia, aby włączyć.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The app protects your privacy by using different operators in each conversation." xml:space="preserve"> + <source>The app protects your privacy by using different operators in each conversation.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve"> + <source>The app will ask to confirm downloads from unknown file servers (except .onion).</source> + <target>Aplikacja zapyta o potwierdzenie pobierania od nieznanych serwerów plików (poza .onion).</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> <source>The attempt to change database passphrase was not completed.</source> <target>Próba zmiany hasła bazy danych nie została zakończona.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve"> + <source>The code you scanned is not a SimpleX link QR code.</source> + <target>Kod, który zeskanowałeś nie jest kodem QR linku SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The connection reached the limit of undelivered messages, your contact may be offline." xml:space="preserve"> + <source>The connection reached the limit of undelivered messages, your contact may be offline.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> <target>Zaakceptowane przez Ciebie połączenie zostanie anulowane!</target> @@ -4994,6 +7548,11 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom <target>Szyfrowanie działa, a nowe uzgodnienie szyfrowania nie jest wymagane. Może to spowodować błędy w połączeniu!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> + <target>Następna generacja prywatnych wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The hash of the previous message is different." xml:space="preserve"> <source>The hash of the previous message is different.</source> <target>Hash poprzedniej wiadomości jest inny.</target> @@ -5009,9 +7568,14 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom <target>Wiadomość zostanie oznaczona jako moderowana dla wszystkich członków.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> - <target>Następna generacja prywatnych wiadomości</target> + <trans-unit id="The messages will be deleted for all members." xml:space="preserve"> + <source>The messages will be deleted for all members.</source> + <target>Wiadomości zostaną usunięte dla wszystkich członków.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve"> + <source>The messages will be marked as moderated for all members.</source> + <target>Wiadomości zostaną oznaczone jako moderowane dla wszystkich członków.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> @@ -5019,9 +7583,12 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom <target>Stara baza danych nie została usunięta podczas migracji, można ją usunąć.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> - <target>Profil jest udostępniany tylko Twoim kontaktom.</target> + <trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve"> + <source>The same conditions will apply to operator **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second preset operator in the app!" xml:space="preserve"> + <source>The second preset operator in the app!</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> @@ -5039,19 +7606,27 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom <target>Serwery dla nowych połączeń bieżącego profilu czatu **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Theme" xml:space="preserve"> - <source>Theme</source> - <target>Motyw</target> + <trans-unit id="The servers for new files of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new files of your current chat profile **%@**.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve"> - <source>There should be at least one user profile.</source> - <target>Powinien istnieć co najmniej jeden profil użytkownika.</target> + <trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve"> + <source>The text you pasted is not a SimpleX link.</source> + <target>Tekst, który wkleiłeś nie jest linkiem SimpleX.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> - <source>There should be at least one visible user profile.</source> - <target>Powinien istnieć co najmniej jeden widoczny profil użytkownika.</target> + <trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve"> + <source>The uploaded database archive will be permanently removed from the servers.</source> + <target>Przesłane archiwum bazy danych zostanie trwale usunięte z serwerów.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Themes" xml:space="preserve"> + <source>Themes</source> + <target>Motywy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="These conditions will also apply for: **%@**." xml:space="preserve"> + <source>These conditions will also apply for: **%@**.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> @@ -5074,16 +7649,35 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom <target>Tego działania nie można cofnąć - wiadomości wysłane i odebrane wcześniej niż wybrane zostaną usunięte. Może to potrwać kilka minut.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> <target>Tego działania nie można cofnąć - Twój profil, kontakty, wiadomości i pliki zostaną nieodwracalnie utracone.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by end-to-end encryption.</source> + <target>Ten czat jest chroniony przez szyfrowanie end-to-end.</target> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by quantum resistant end-to-end encryption.</source> + <target>Ten czat jest chroniony przez kwantowo odporne szyfrowanie end-to-end.</target> + <note>E2EE info chat item</note> + </trans-unit> <trans-unit id="This device name" xml:space="preserve"> <source>This device name</source> <target>Nazwa tego urządzenia</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve"> + <source>This display name is invalid. Please choose another name.</source> + <target>Nazwa wyświetlana jest nieprawidłowa. Proszę wybrać inną nazwę.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> <source>This group has over %lld members, delivery receipts are not sent.</source> <target>Ta grupa ma ponad %lld członków, potwierdzenia dostawy nie są wysyłane.</target> @@ -5104,11 +7698,29 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom <target>To jest twój jednorazowy link!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve"> + <source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve"> + <source>This link was used with another mobile device, please create a new link on the desktop.</source> + <target>Ten link dostał użyty z innym urządzeniem mobilnym, proszę stworzyć nowy link na komputerze.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This message was deleted or not received yet." xml:space="preserve"> + <source>This message was deleted or not received yet.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <source>This setting applies to messages in your current chat profile **%@**.</source> <target>To ustawienie dotyczy wiadomości Twojego bieżącego profilu czatu **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Title" xml:space="preserve"> + <source>Title</source> + <target>Tytuł</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> <source>To ask any questions and to receive updates:</source> <target>Aby zadać wszelkie pytania i otrzymywać aktualizacje:</target> @@ -5129,9 +7741,8 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom <target>Aby nawiązać nowe połączenie</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <target>Aby chronić prywatność, zamiast identyfikatorów użytkowników używanych przez wszystkie inne platformy, SimpleX ma identyfikatory dla kolejek wiadomości, oddzielne dla każdego z Twoich kontaktów.</target> + <trans-unit id="To protect against your link being replaced, you can compare contact security codes." xml:space="preserve"> + <source>To protect against your link being replaced, you can compare contact security codes.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -5139,6 +7750,11 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom <target>Aby chronić strefę czasową, pliki obrazów/głosów używają UTC.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve"> + <source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source> + <target>Aby chronić Twój adres IP, prywatne trasowanie używa Twoich serwerów SMP, aby dostarczyć wiadomości.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> @@ -5146,6 +7762,25 @@ You will be prompted to complete authentication before this feature is enabled.< Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> + <target>Aby chronić prywatność, zamiast identyfikatorów użytkowników używanych przez wszystkie inne platformy, SimpleX ma identyfikatory dla kolejek wiadomości, oddzielne dla każdego z Twoich kontaktów.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To receive" xml:space="preserve"> + <source>To receive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve"> + <source>To record speech please grant permission to use Microphone.</source> + <target>Aby nagrać rozmowę, proszę zezwolić na użycie Mikrofonu.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record video please grant permission to use Camera." xml:space="preserve"> + <source>To record video please grant permission to use Camera.</source> + <target>Aby nagrać wideo, proszę zezwolić na użycie Aparatu.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> <target>Aby nagrać wiadomość głosową należy udzielić zgody na użycie Mikrofonu.</target> @@ -5156,26 +7791,58 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta <target>Aby ujawnić Twój ukryty profil, wprowadź pełne hasło w pole wyszukiwania na stronie **Twoich profili czatu**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To send" xml:space="preserve"> + <source>To send</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> <source>To support instant push notifications the chat database has to be migrated.</source> <target>Aby obsługiwać natychmiastowe powiadomienia push, należy zmigrować bazę danych czatu.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To use the servers of **%@**, accept conditions of use." xml:space="preserve"> + <source>To use the servers of **%@**, accept conditions of use.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> <target>Aby zweryfikować szyfrowanie end-to-end z Twoim kontaktem porównaj (lub zeskanuj) kod na waszych urządzeniach.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle chat list:" xml:space="preserve"> + <source>Toggle chat list:</source> + <target>Przełącz listę czatów:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> <source>Toggle incognito when connecting.</source> <target>Przełącz incognito przy połączeniu.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Token status: %@." xml:space="preserve"> + <source>Token status: %@.</source> + <note>token status</note> + </trans-unit> + <trans-unit id="Toolbar opacity" xml:space="preserve"> + <source>Toolbar opacity</source> + <target>Nieprzezroczystość paska narzędzi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Total" xml:space="preserve"> + <source>Total</source> + <target>Łącznie</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>Izolacja transportu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Transport sessions" xml:space="preserve"> + <source>Transport sessions</source> + <target>Sesje transportowe</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> <target>Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu (błąd: %@).</target> @@ -5186,16 +7853,16 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta <target>Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Turkish interface" xml:space="preserve"> + <source>Turkish interface</source> + <target>Turecki interfejs</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> <target>Wyłącz</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve"> - <source>Turn off notifications?</source> - <target>Wyłączyć powiadomienia?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> <target>Włącz</target> @@ -5211,20 +7878,29 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta <target>Odblokuj</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock for all" xml:space="preserve"> + <source>Unblock for all</source> + <target>Odblokuj dla wszystkich</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member" xml:space="preserve"> <source>Unblock member</source> <target>Odblokuj członka</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock member for all?" xml:space="preserve"> + <source>Unblock member for all?</source> + <target>Odblokować członka dla wszystkich?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member?" xml:space="preserve"> <source>Unblock member?</source> <target>Odblokować członka?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve"> - <source>Unexpected error: %@</source> - <target>Nieoczekiwany błąd: %@</target> - <note>item status description</note> + <trans-unit id="Undelivered messages" xml:space="preserve"> + <source>Undelivered messages</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -5234,7 +7910,7 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta <trans-unit id="Unfav." xml:space="preserve"> <source>Unfav.</source> <target>Nie ulub.</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> @@ -5271,6 +7947,11 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta <target>Nieznany błąd</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unknown servers!" xml:space="preserve"> + <source>Unknown servers!</source> + <target>Nieznane serwery!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> <target>O ile nie korzystasz z interfejsu połączeń systemu iOS, włącz tryb Nie przeszkadzać, aby uniknąć przerywania.</target> @@ -5306,11 +7987,20 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> <target>Wyłącz wyciszenie</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> </trans-unit> <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> <target>Nieprzeczytane</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Unsupported connection link" xml:space="preserve"> + <source>Unsupported connection link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve"> + <source>Up to 100 last messages are sent to new members.</source> + <target>Do nowych członków wysyłanych jest do 100 ostatnich wiadomości.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update" xml:space="preserve"> @@ -5318,11 +8008,6 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc <target>Aktualizuj</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> - <source>Update .onion hosts setting?</source> - <target>Zaktualizować ustawienie hostów .onion?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> <target>Aktualizuj hasło do bazy danych</target> @@ -5333,9 +8018,13 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc <target>Zaktualizować ustawienia sieci?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve"> - <source>Update transport isolation mode?</source> - <target>Zaktualizować tryb izolacji transportu?</target> + <trans-unit id="Update settings?" xml:space="preserve"> + <source>Update settings?</source> + <target>Zaktualizować ustawienia?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updated conditions" xml:space="preserve"> + <source>Updated conditions</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> @@ -5343,31 +8032,68 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc <target>Aktualizacja ustawień spowoduje ponowne połączenie klienta ze wszystkimi serwerami.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> - <target>Aktualizacja tych ustawień spowoduje ponowne połączenie klienta ze wszystkimi serwerami.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Upgrade and open chat" xml:space="preserve"> <source>Upgrade and open chat</source> <target>Zaktualizuj i otwórz czat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Upload errors" xml:space="preserve"> + <source>Upload errors</source> + <target>Błędy przesłania</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload failed" xml:space="preserve"> + <source>Upload failed</source> + <target>Wgrywanie nie udane</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Upload file" xml:space="preserve"> <source>Upload file</source> <target>Prześlij plik</target> <note>server test step</note> </trans-unit> + <trans-unit id="Uploaded" xml:space="preserve"> + <source>Uploaded</source> + <target>Przesłane</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploaded files" xml:space="preserve"> + <source>Uploaded files</source> + <target>Przesłane pliki</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploading archive" xml:space="preserve"> + <source>Uploading archive</source> + <target>Wgrywanie archiwum</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use %@" xml:space="preserve"> + <source>Use %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> <target>Użyj hostów .onion</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use SOCKS proxy" xml:space="preserve"> + <source>Use SOCKS proxy</source> + <target>Użyj proxy SOCKS</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> <target>Użyć serwerów SimpleX Chat?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use TCP port %@ when no port is specified." xml:space="preserve"> + <source>Use TCP port %@ when no port is specified.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve"> + <source>Use TCP port 443 for preset servers only.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> <target>Użyj czatu</target> @@ -5378,6 +8104,14 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc <target>Użyj obecnego profilu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use for files" xml:space="preserve"> + <source>Use for files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for messages" xml:space="preserve"> + <source>Use for messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Użyj dla nowych połączeń</target> @@ -5398,19 +8132,56 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc <target>Użyj nowego profilu incognito</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use only local notifications?" xml:space="preserve"> + <source>Use only local notifications?</source> + <target>Używać tylko lokalnych powiadomień?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve"> + <source>Use private routing with unknown servers when IP address is not protected.</source> + <target>Używaj prywatnego trasowania z nieznanymi serwerami, gdy adres IP nie jest chroniony.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers." xml:space="preserve"> + <source>Use private routing with unknown servers.</source> + <target>Używaj prywatnego trasowania z nieznanymi serwerami.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Użyj serwera</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> - <target>Profil użytkownika</target> + <trans-unit id="Use servers" xml:space="preserve"> + <source>Use servers</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> - <target>Używanie hostów .onion wymaga kompatybilnego dostawcy VPN.</target> + <trans-unit id="Use short links (BETA)" xml:space="preserve"> + <source>Use short links (BETA)</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app while in the call." xml:space="preserve"> + <source>Use the app while in the call.</source> + <target>Używaj aplikacji podczas połączenia.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app with one hand." xml:space="preserve"> + <source>Use the app with one hand.</source> + <target>Korzystaj z aplikacji jedną ręką.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use web port" xml:space="preserve"> + <source>Use web port</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="User selection" xml:space="preserve"> + <source>User selection</source> + <target>Wybór użytkownika</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Username" xml:space="preserve"> + <source>Username</source> + <target>Nazwa użytkownika</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> @@ -5438,6 +8209,16 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc <target>Zweryfikuj połączenia</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Verify database passphrase" xml:space="preserve"> + <source>Verify database passphrase</source> + <target>Zweryfikuj hasło bazy danych</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify passphrase" xml:space="preserve"> + <source>Verify passphrase</source> + <target>Zweryfikuj hasło</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Verify security code" xml:space="preserve"> <source>Verify security code</source> <target>Weryfikuj kod bezpieczeństwa</target> @@ -5473,11 +8254,24 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc <target>Filmy i pliki do 1gb</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View conditions" xml:space="preserve"> + <source>View conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> <target>Pokaż kod bezpieczeństwa</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View updated conditions" xml:space="preserve"> + <source>View updated conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Visible history" xml:space="preserve"> + <source>Visible history</source> + <target>Widoczna historia</target> + <note>chat feature</note> + </trans-unit> <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> <target>Wiadomości głosowe</target> @@ -5488,11 +8282,16 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc <target>Wiadomości głosowe są zabronione na tym czacie.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <target>Wiadomości głosowe są zabronione w tej grupie.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Voice messages not allowed" xml:space="preserve"> + <source>Voice messages not allowed</source> + <target>Wiadomości głosowe są niedozwolone</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> <source>Voice messages prohibited!</source> <target>Wiadomości głosowe zabronione!</target> @@ -5523,6 +8322,21 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc <target>Oczekiwanie na film</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wallpaper accent" xml:space="preserve"> + <source>Wallpaper accent</source> + <target>Akcent tapety</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper background" xml:space="preserve"> + <source>Wallpaper background</source> + <target>Tło tapety</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve"> + <source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source> + <target>Ostrzeżenie: rozpoczęcie czatu na wielu urządzeniach nie jest wspierane i spowoduje niepowodzenia dostarczania wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> <source>Warning: you may lose some data!</source> <target>Uwaga: możesz stracić niektóre dane!</target> @@ -5543,6 +8357,11 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc <target>Wiadomość powitalna</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Welcome message is too long" xml:space="preserve"> + <source>Welcome message is too long</source> + <target>Wiadomość powitalna jest zbyt długa</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="What's new" xml:space="preserve"> <source>What's new</source> <target>Co nowego</target> @@ -5553,9 +8372,13 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc <target>Gdy dostępny</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> - <source>When people request to connect, you can accept or reject it.</source> - <target>Kiedy ludzie proszą o połączenie, możesz je zaakceptować lub odrzucić.</target> + <trans-unit id="When connecting audio and video calls." xml:space="preserve"> + <source>When connecting audio and video calls.</source> + <target>Podczas łączenia połączeń audio i wideo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When more than one operator is enabled, none of them has metadata to learn who communicates with whom." xml:space="preserve"> + <source>When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> @@ -5563,29 +8386,74 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc <target>Gdy udostępnisz komuś profil incognito, będzie on używany w grupach, do których Cię zaprosi.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="WiFi" xml:space="preserve"> + <source>WiFi</source> + <target>WiFi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Will be enabled in direct chats!" xml:space="preserve"> + <source>Will be enabled in direct chats!</source> + <target>Zostanie włączone w czatach bezpośrednich!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wired ethernet" xml:space="preserve"> + <source>Wired ethernet</source> + <target>Połączenie ethernet (po kablu)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With encrypted files and media." xml:space="preserve"> + <source>With encrypted files and media.</source> + <target>Z zaszyfrowanymi plikami i multimediami.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> <target>Z opcjonalną wiadomością powitalną.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="With reduced battery usage." xml:space="preserve"> + <source>With reduced battery usage.</source> + <target>Ze zmniejszonym zużyciem baterii.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to file servers.</source> + <target>Bez Tor lub VPN, Twój adres IP będzie widoczny do serwerów plików.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source> + <target>Bez Tor lub VPN, Twój adres IP będzie widoczny dla tych przekaźników XFTP: %@.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> <target>Nieprawidłowe hasło bazy danych</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve"> + <source>Wrong key or unknown connection - most likely this connection is deleted.</source> + <target>Zły klucz lub nieznane połączenie - najprawdopodobniej to połączenie jest usunięte.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Wrong key or unknown file chunk address - most likely file is deleted." xml:space="preserve"> + <source>Wrong key or unknown file chunk address - most likely file is deleted.</source> + <target>Zły klucz lub nieznany adres fragmentu pliku - najprawdopodobniej plik został usunięty.</target> + <note>file error text</note> + </trans-unit> <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> <target>Nieprawidłowe hasło!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="XFTP servers" xml:space="preserve"> - <source>XFTP servers</source> - <target>Serwery XFTP</target> + <trans-unit id="XFTP server" xml:space="preserve"> + <source>XFTP server</source> + <target>Serwer XFTP</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> - <target>Ty</target> + <trans-unit id="You **must not** use the same database on two devices." xml:space="preserve"> + <source>You **must not** use the same database on two devices.</source> + <target>**Nie możesz** używać tej samej bazy na dwóch urządzeniach.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You accepted connection" xml:space="preserve"> @@ -5608,6 +8476,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc <target>Jesteś już połączony z %@.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are already connected with %@." xml:space="preserve"> + <source>You are already connected with %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You are already connecting to %@." xml:space="preserve"> <source>You are already connecting to %@.</source> <target>Już się łączysz z %@.</target> @@ -5655,14 +8527,23 @@ Powtórzyć prośbę dołączenia?</target> <target>Jesteś zaproszony do grupy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are not connected to these servers. Private routing is used to deliver messages to them." xml:space="preserve"> + <source>You are not connected to these servers. Private routing is used to deliver messages to them.</source> + <target>Nie jesteś połączony z tymi serwerami. Prywatne trasowanie jest używane do dostarczania do nich wiadomości.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> <source>You can accept calls from lock screen, without device and app authentication.</source> <target>Możesz przyjmować połączenia z ekranu blokady, bez uwierzytelniania urządzenia i aplikacji.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <target>Możesz też połączyć się klikając w link. Jeśli otworzy się on w przeglądarce, kliknij przycisk **Otwórz w aplikacji mobilnej**.</target> + <trans-unit id="You can change it in Appearance settings." xml:space="preserve"> + <source>You can change it in Appearance settings.</source> + <target>Możesz to zmienić w ustawieniach wyglądu.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can configure servers via settings." xml:space="preserve"> + <source>You can configure servers via settings.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can create it later" xml:space="preserve"> @@ -5680,16 +8561,35 @@ Powtórzyć prośbę dołączenia?</target> <target>Możesz je włączyć później w ustawieniach Prywatności i Bezpieczeństwa aplikacji.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can give another try." xml:space="preserve"> + <source>You can give another try.</source> + <target>Możesz spróbować ponownie.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <source>You can hide or mute a user profile - swipe it to the right.</source> <target>Możesz ukryć lub wyciszyć profil użytkownika - przesuń palcem w prawo.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> + <trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve"> + <source>You can make it visible to your SimpleX contacts via Settings.</source> + <target>Możesz ustawić go jako widoczny dla swoich kontaktów SimpleX w Ustawieniach.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now chat with %@" xml:space="preserve"> + <source>You can now chat with %@</source> <target>Możesz teraz wysyłać wiadomości do %@</target> <note>notification body</note> </trans-unit> + <trans-unit id="You can send messages to %@ from Archived contacts." xml:space="preserve"> + <source>You can send messages to %@ from Archived contacts.</source> + <target>Możesz wysyłać wiadomości do %@ ze zarchiwizowanych kontaktów.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can set connection name, to remember who the link was shared with." xml:space="preserve"> + <source>You can set connection name, to remember who the link was shared with.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> <target>Podgląd powiadomień na ekranie blokady można ustawić w ustawieniach.</target> @@ -5705,16 +8605,16 @@ Powtórzyć prośbę dołączenia?</target> <target>Możesz udostępnić ten adres Twoim kontaktom, aby umożliwić im połączenie z **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> - <source>You can share your address as a link or QR code - anybody can connect to you.</source> - <target>Możesz udostępnić swój adres jako link lub jako kod QR - każdy będzie mógł się z Tobą połączyć.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> <target>Możesz rozpocząć czat poprzez Ustawienia aplikacji / Baza danych lub poprzez ponowne uruchomienie aplikacji</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve"> + <source>You can still view conversation with %@ in the list of chats.</source> + <target>Nadal możesz przeglądać rozmowę z %@ na liście czatów.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> <target>Możesz włączyć blokadę SimpleX poprzez Ustawienia.</target> @@ -5725,21 +8625,26 @@ Powtórzyć prośbę dołączenia?</target> <target>Możesz używać markdown do formatowania wiadomości:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can view invitation link again in connection details." xml:space="preserve"> + <source>You can view invitation link again in connection details.</source> + <target>Możesz zobaczyć link zaproszenia ponownie w szczegółach połączenia.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> <target>Nie możesz wysyłać wiadomości!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <target>Kontrolujesz przez który serwer(y) **odbierać** wiadomości, Twoje kontakty - serwery, których używasz do wysyłania im wiadomości.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <target>Nie można zweryfikować użytkownika; proszę spróbować ponownie.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> + <target>Ty decydujesz, kto może się połączyć.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You have already requested connection via this address!" xml:space="preserve"> <source>You have already requested connection via this address!</source> <target>Już prosiłeś o połączenie na ten adres!</target> @@ -5752,11 +8657,6 @@ Repeat connection request?</source> Powtórzyć prośbę połączenia?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> - <target>Nie masz czatów</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> <target>Musisz wprowadzić hasło przy każdym uruchomieniu aplikacji - nie jest one przechowywane na urządzeniu.</target> @@ -5777,11 +8677,26 @@ Powtórzyć prośbę połączenia?</target> <target>Dołączyłeś do tej grupy. Łączenie z zapraszającym członkiem grupy.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You may migrate the exported database." xml:space="preserve"> + <source>You may migrate the exported database.</source> + <target>Możesz zmigrować wyeksportowaną bazy danych.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may save the exported archive." xml:space="preserve"> + <source>You may save the exported archive.</source> + <target>Możesz zapisać wyeksportowane archiwum.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> <target>Musisz używać najnowszej wersji bazy danych czatu TYLKO na jednym urządzeniu, w przeciwnym razie możesz przestać otrzymywać wiadomości od niektórych kontaktów.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You need to allow your contact to call to be able to call them." xml:space="preserve"> + <source>You need to allow your contact to call to be able to call them.</source> + <target>Aby móc dzwonić, musisz zezwolić kontaktowi na połączenia.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> <target>Musisz zezwolić Twojemu kontaktowi na wysyłanie wiadomości głosowych, aby móc je wysyłać.</target> @@ -5797,6 +8712,10 @@ Powtórzyć prośbę połączenia?</target> <target>Wysłałeś zaproszenie do grupy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You should receive notifications." xml:space="preserve"> + <source>You should receive notifications.</source> + <note>token info</note> + </trans-unit> <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> <target>Zostaniesz połączony do grupy, gdy urządzenie gospodarza grupy będzie online, proszę czekać lub sprawdzić później!</target> @@ -5832,6 +8751,10 @@ Powtórzyć prośbę połączenia?</target> <target>Nadal będziesz otrzymywać połączenia i powiadomienia z wyciszonych profili, gdy są one aktywne.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You will stop receiving messages from this chat. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this chat. Chat history will be preserved.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> <target>Przestaniesz otrzymywać wiadomości od tej grupy. Historia czatu zostanie zachowana.</target> @@ -5852,31 +8775,16 @@ Powtórzyć prośbę połączenia?</target> <target>Używasz profilu incognito dla tej grupy - aby zapobiec udostępnianiu głównego profilu zapraszanie kontaktów jest zabronione</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your %@ servers" xml:space="preserve"> - <source>Your %@ servers</source> - <target>Twoje serwery %@</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your ICE servers" xml:space="preserve"> <source>Your ICE servers</source> <target>Twoje serwery ICE</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> - <target>Twoje serwery SMP</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your SimpleX address" xml:space="preserve"> <source>Your SimpleX address</source> <target>Twój adres SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your XFTP servers" xml:space="preserve"> - <source>Your XFTP servers</source> - <target>Twoje serwery XFTP</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your calls" xml:space="preserve"> <source>Your calls</source> <target>Twoje połączenia</target> @@ -5892,16 +8800,19 @@ Powtórzyć prośbę połączenia?</target> <target>Baza danych czatu nie jest szyfrowana - ustaw hasło, aby ją zaszyfrować.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your chat preferences" xml:space="preserve"> + <source>Your chat preferences</source> + <target>Twoje preferencje czatu</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>Twoje profile czatu</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> - <target>Twój kontakt musi być online, aby połączenie zostało zakończone. -Możesz anulować to połączenie i usunąć kontakt (i spróbować później z nowym linkiem).</target> + <trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve"> + <source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source> + <target>Twoje połączenie zostało przeniesione do %@, ale podczas przekierowywania do profilu wystąpił nieoczekiwany błąd.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> @@ -5914,18 +8825,16 @@ Możesz anulować to połączenie i usunąć kontakt (i spróbować później z <target>Twoje kontakty mogą zezwolić na pełne usunięcie wiadomości.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> - <source>Your contacts in SimpleX will see it. -You can change it in Settings.</source> - <target>Twoje kontakty w SimpleX będą to widzieć. -Możesz to zmienić w Ustawieniach.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your contacts will remain connected." xml:space="preserve"> <source>Your contacts will remain connected.</source> <target>Twoje kontakty pozostaną połączone.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve"> + <source>Your credentials may be sent unencrypted.</source> + <target>Twoje poświadczenia mogą zostać wysłane niezaszyfrowane.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> <target>Twoja obecna baza danych czatu zostanie usunięta i zastąpiona zaimportowaną.</target> @@ -5956,33 +8865,36 @@ Możesz to zmienić w Ustawieniach.</target> <target>Twój profil **%@** zostanie udostępniony.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> - <target>Twój profil jest przechowywany na urządzeniu i udostępniany tylko Twoim kontaktom. -Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> + <target>Profil jest udostępniany tylko Twoim kontaktom.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> - <target>Twój profil, kontakty i dostarczone wiadomości są przechowywane na Twoim urządzeniu.</target> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> + <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> + <target>Twój profil jest przechowywany na urządzeniu i udostępniany tylko Twoim kontaktom. Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve"> + <source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source> + <target>Twój profil został zmieniony. Jeśli go zapiszesz, zaktualizowany profil zostanie wysłany do wszystkich kontaktów.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> <target>Twój losowy profil</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> - <target>Twój serwer</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> <target>Twój adres serwera</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your servers" xml:space="preserve"> + <source>Your servers</source> + <target>Twoje serwery</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your settings" xml:space="preserve"> <source>Your settings</source> <target>Twoje ustawienia</target> @@ -6023,11 +8935,20 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>zaakceptowane połączenie</target> <note>call status</note> </trans-unit> + <trans-unit id="accepted invitation" xml:space="preserve"> + <source>accepted invitation</source> + <note>chat list item title</note> + </trans-unit> <trans-unit id="admin" xml:space="preserve"> <source>admin</source> <target>administrator</target> <note>member role</note> </trans-unit> + <trans-unit id="admins" xml:space="preserve"> + <source>admins</source> + <target>administratorzy</target> + <note>feature role</note> + </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> <target>uzgadnianie szyfrowania dla %@…</target> @@ -6038,6 +8959,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>uzgadnianie szyfrowania…</target> <note>chat item text</note> </trans-unit> + <trans-unit id="all members" xml:space="preserve"> + <source>all members</source> + <target>wszyscy członkowie</target> + <note>feature role</note> + </trans-unit> <trans-unit id="always" xml:space="preserve"> <source>always</source> <target>zawsze</target> @@ -6048,6 +8974,15 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>i %lld innych wydarzeń</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="archived report" xml:space="preserve"> + <source>archived report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="attempts" xml:space="preserve"> + <source>attempts</source> + <target>próby</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <source>audio call (not e2e encrypted)</source> <target>połączenie audio (nie szyfrowane e2e)</target> @@ -6071,13 +9006,29 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <trans-unit id="blocked" xml:space="preserve"> <source>blocked</source> <target>zablokowany</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="blocked %@" xml:space="preserve"> + <source>blocked %@</source> + <target>zablokowany %@</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="blocked by admin" xml:space="preserve"> + <source>blocked by admin</source> + <target>zablokowany przez admina</target> + <note>blocked chat item +marked deleted chat item preview text</note> </trans-unit> <trans-unit id="bold" xml:space="preserve"> <source>bold</source> <target>pogrubiona</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="call" xml:space="preserve"> + <source>call</source> + <target>zadzwoń</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="call error" xml:space="preserve"> <source>call error</source> <target>błąd połączenia</target> @@ -6181,7 +9132,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> <target>łączenie…</target> - <note>chat list item title</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> @@ -6193,6 +9144,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>połączenie: %@</target> <note>connection information</note> </trans-unit> + <trans-unit id="contact %@ changed to %@" xml:space="preserve"> + <source>contact %1$@ changed to %2$@</source> + <target>kontakt %1$@ zmieniony na %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> <target>kontakt posiada szyfrowanie e2e</target> @@ -6223,10 +9179,16 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>dni</target> <note>time unit</note> </trans-unit> + <trans-unit id="decryption errors" xml:space="preserve"> + <source>decryption errors</source> + <target>błąd odszyfrowywania</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> <target>domyślne (%@)</target> - <note>pref value</note> + <note>delete after time +pref value</note> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> @@ -6273,6 +9235,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>zduplikowana wiadomość</target> <note>integrity error chat item</note> </trans-unit> + <trans-unit id="duplicates" xml:space="preserve"> + <source>duplicates</source> + <target>duplikaty</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> <target>zaszyfrowany e2e</target> @@ -6348,9 +9315,14 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>błąd</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="event happened" xml:space="preserve"> - <source>event happened</source> - <target>nowe wydarzenie</target> + <trans-unit id="expired" xml:space="preserve"> + <source>expired</source> + <target>wygasły</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="forwarded" xml:space="preserve"> + <source>forwarded</source> + <target>przekazane dalej</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> @@ -6378,6 +9350,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>iOS Keychain będzie używany do bezpiecznego przechowywania hasła po ponownym uruchomieniu aplikacji lub zmianie hasła - pozwoli to na otrzymywanie powiadomień push.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="inactive" xml:space="preserve"> + <source>inactive</source> + <target>nieaktywny</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> <target>incognito poprzez link adresu kontaktowego</target> @@ -6418,6 +9395,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>zaproszenie do grupy %@</target> <note>group name</note> </trans-unit> + <trans-unit id="invite" xml:space="preserve"> + <source>invite</source> + <target>zaproś</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> <target>zaproszony</target> @@ -6463,11 +9445,21 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>członek</target> <note>member role</note> </trans-unit> + <trans-unit id="member %@ changed to %@" xml:space="preserve"> + <source>member %1$@ changed to %2$@</source> + <target>członek %1$@ zmieniony na %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> <target>połączony</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="message" xml:space="preserve"> + <source>message</source> + <target>wiadomość</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="message received" xml:space="preserve"> <source>message received</source> <target>wiadomość otrzymana</target> @@ -6491,7 +9483,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <trans-unit id="moderated by %@" xml:space="preserve"> <source>moderated by %@</source> <target>moderowany przez %@</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="moderator" xml:space="preserve"> + <source>moderator</source> + <note>member role</note> </trans-unit> <trans-unit id="months" xml:space="preserve"> <source>months</source> @@ -6501,7 +9497,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <trans-unit id="never" xml:space="preserve"> <source>never</source> <target>nigdy</target> - <note>No comment provided by engineer.</note> + <note>delete after time</note> </trans-unit> <trans-unit id="new message" xml:space="preserve"> <source>new message</source> @@ -6532,8 +9528,8 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <source>off</source> <target>wyłączony</target> <note>enabled status - group pref value - time to disappear</note> +group pref value +time to disappear</note> </trans-unit> <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> @@ -6550,16 +9546,44 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>włączone</target> <note>group pref value</note> </trans-unit> + <trans-unit id="other" xml:space="preserve"> + <source>other</source> + <target>inne</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="other errors" xml:space="preserve"> + <source>other errors</source> + <target>inne błędy</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="owner" xml:space="preserve"> <source>owner</source> <target>właściciel</target> <note>member role</note> </trans-unit> + <trans-unit id="owners" xml:space="preserve"> + <source>owners</source> + <target>właściciele</target> + <note>feature role</note> + </trans-unit> <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> <target>peer-to-peer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="pending" xml:space="preserve"> + <source>pending</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="pending approval" xml:space="preserve"> + <source>pending approval</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="quantum resistant e2e encryption" xml:space="preserve"> + <source>quantum resistant e2e encryption</source> + <target>kwantowo odporne szyfrowanie e2e</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> <target>otrzymano odpowiedź…</target> @@ -6570,6 +9594,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>otrzymano potwierdzenie…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="rejected" xml:space="preserve"> + <source>rejected</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> <target>odrzucone połączenie</target> @@ -6585,11 +9613,40 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>usunięto %@</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="removed contact address" xml:space="preserve"> + <source>removed contact address</source> + <target>usunięto adres kontaktu</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="removed profile picture" xml:space="preserve"> + <source>removed profile picture</source> + <target>usunięto zdjęcie profilu</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> <target>usunął cię</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="requested to connect" xml:space="preserve"> + <source>requested to connect</source> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="saved" xml:space="preserve"> + <source>saved</source> + <target>zapisane</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="saved from %@" xml:space="preserve"> + <source>saved from %@</source> + <target>zapisane od %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="search" xml:space="preserve"> + <source>search</source> + <target>szukaj</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="sec" xml:space="preserve"> <source>sec</source> <target>sek</target> @@ -6615,6 +9672,30 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>wyślij wiadomość bezpośrednią</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve"> + <source>server queue info: %1$@ + +last received msg: %2$@</source> + <target>Informacje kolejki serwera: %1$@ + +ostatnia otrzymana wiadomość: %2$@</target> + <note>queue info</note> + </trans-unit> + <trans-unit id="set new contact address" xml:space="preserve"> + <source>set new contact address</source> + <target>ustaw nowy adres kontaktu</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="set new profile picture" xml:space="preserve"> + <source>set new profile picture</source> + <target>ustaw nowe zdjęcie profilu</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="standard end-to-end encryption" xml:space="preserve"> + <source>standard end-to-end encryption</source> + <target>standardowe szyfrowanie end-to-end</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>uruchamianie…</target> @@ -6630,16 +9711,41 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>ten kontakt</target> <note>notification title</note> </trans-unit> + <trans-unit id="unblocked %@" xml:space="preserve"> + <source>unblocked %@</source> + <target>odblokowano %@</target> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="unknown" xml:space="preserve"> <source>unknown</source> <target>nieznany</target> <note>connection info</note> </trans-unit> + <trans-unit id="unknown servers" xml:space="preserve"> + <source>unknown servers</source> + <target>nieznane przekaźniki</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unknown status" xml:space="preserve"> + <source>unknown status</source> + <target>nieznany status</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unprotected" xml:space="preserve"> + <source>unprotected</source> + <target>niezabezpieczony</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="updated group profile" xml:space="preserve"> <source>updated group profile</source> <target>zaktualizowano profil grupy</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="updated profile" xml:space="preserve"> + <source>updated profile</source> + <target>zaktualizowano profil</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="v%@" xml:space="preserve"> <source>v%@</source> <target>v%@</target> @@ -6670,6 +9776,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>przez przekaźnik</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="video" xml:space="preserve"> + <source>video</source> + <target>wideo</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> <source>video call (not e2e encrypted)</source> <target>połączenie wideo (bez szyfrowania e2e)</target> @@ -6695,11 +9806,21 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>tygodnie</target> <note>time unit</note> </trans-unit> + <trans-unit id="when IP hidden" xml:space="preserve"> + <source>when IP hidden</source> + <target>gdy IP ukryty</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="yes" xml:space="preserve"> <source>yes</source> <target>tak</target> <note>pref value</note> </trans-unit> + <trans-unit id="you" xml:space="preserve"> + <source>you</source> + <target>Ty</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> <target>jesteś zaproszony do grupy</target> @@ -6710,6 +9831,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>jesteś obserwatorem</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="you blocked %@" xml:space="preserve"> + <source>you blocked %@</source> + <target>zablokowałeś %@</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> <target>zmieniłeś adres</target> @@ -6750,6 +9876,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>udostępniłeś jednorazowy link incognito</target> <note>chat list item description</note> </trans-unit> + <trans-unit id="you unblocked %@" xml:space="preserve"> + <source>you unblocked %@</source> + <target>odblokowałeś %@</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you: " xml:space="preserve"> <source>you: </source> <target>ty: </target> @@ -6764,7 +9895,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="pl" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6801,7 +9932,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="pl" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> @@ -6821,4 +9952,245 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> </trans-unit> </body> </file> + <file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="pl" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%d new events" xml:space="preserve"> + <source>%d new events</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From %d chat(s)" xml:space="preserve"> + <source>From %d chat(s)</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From: %@" xml:space="preserve"> + <source>From: %@</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <note>notification</note> + </trans-unit> + <trans-unit id="New messages" xml:space="preserve"> + <source>New messages</source> + <note>notification</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="pl" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle display name</note> + </trans-unit> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> + <source>Copyright © 2024 SimpleX Chat. All rights reserved.</source> + <target>Copyright © 2024 SimpleX Chat. Wszelkie prawa zastrzeżone.</target> + <note>Copyright (human-readable)</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="pl" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <target>%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App is locked!" xml:space="preserve"> + <source>App is locked!</source> + <target>Aplikacja zablokowana!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <target>Anuluj</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <target>Nie można uzyskać dostępu do pęku kluczy aby zapisać hasło do bazy danych</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Nie można przekazać wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Comment" xml:space="preserve"> + <source>Comment</source> + <target>Komentarz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <target>Obecnie maksymalny obsługiwany rozmiar pliku to %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade required" xml:space="preserve"> + <source>Database downgrade required</source> + <target>Wymagane obniżenie wersji bazy danych</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <target>Baza danych zaszyfrowana!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <target>Błąd bazy danych</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <target>Hasło bazy danych jest inne niż zapisane w pęku kluczy.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <target>Hasło do bazy danych jest wymagane do otwarcia czatu.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade required" xml:space="preserve"> + <source>Database upgrade required</source> + <target>Wymagana aktualizacja bazy danych</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing file" xml:space="preserve"> + <source>Error preparing file</source> + <target>Błąd przygotowania pliku</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing message" xml:space="preserve"> + <source>Error preparing message</source> + <target>Błąd przygotowania wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <target>Błąd: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Błąd pliku</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <target>Niekompatybilna wersja bazy danych</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Nieprawidłowe potwierdzenie migracji</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <target>Błąd pęku kluczy</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <target>Duży plik!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No active profile" xml:space="preserve"> + <source>No active profile</source> + <target>Brak aktywnego profilu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <target>Ok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to downgrade the database." xml:space="preserve"> + <source>Open the app to downgrade the database.</source> + <target>Otwórz aplikację aby obniżyć wersję bazy danych.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to upgrade the database." xml:space="preserve"> + <source>Open the app to upgrade the database.</source> + <target>Otwórz aplikację aby zaktualizować bazę danych.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passphrase" xml:space="preserve"> + <source>Passphrase</source> + <target>Hasło</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve"> + <source>Please create a profile in the SimpleX app</source> + <target>Proszę utworzyć profil w aplikacji SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Wybrane preferencje czatu zabraniają tej wiadomości.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending a message takes longer than expected." xml:space="preserve"> + <source>Sending a message takes longer than expected.</source> + <target>Wysłanie wiadomości trwa dłużej niż oczekiwano.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending message…" xml:space="preserve"> + <source>Sending message…</source> + <target>Wysyłanie wiadomości…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <target>Udostępnij</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Slow network?" xml:space="preserve"> + <source>Slow network?</source> + <target>Wolna sieć?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <target>Nieznany błąd bazy danych: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unsupported format" xml:space="preserve"> + <source>Unsupported format</source> + <target>Niewspierany format</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wait" xml:space="preserve"> + <source>Wait</source> + <target>Czekaj</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <target>Nieprawidłowe hasło bazy danych</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve"> + <source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source> + <target>Możesz zezwolić na udostępnianie w ustawieniach Prywatność i bezpieczeństwo / Blokada SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + </body> + </file> </xliff> diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/contents.json b/apps/ios/SimpleX Localizations/pl.xcloc/contents.json index 22043b831d..c79fba1c1e 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/pl.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "pl", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "16.2" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff index 540a4eada4..bbb6c7d22a 100644 --- a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff +++ b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff @@ -69,7 +69,7 @@ </trans-unit> <trans-unit id="%@ is not verified" xml:space="preserve" approved="no"> <source>%@ is not verified</source> - <target state="translated">%@ não foi verificado</target> + <target state="translated">%@ não foi verificado(a)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ is verified" xml:space="preserve" approved="no"> @@ -84,12 +84,12 @@ </trans-unit> <trans-unit id="%d days" xml:space="preserve" approved="no"> <source>%d days</source> - <target state="translated">%d dia(s)</target> + <target state="translated">%d dias</target> <note>message ttl</note> </trans-unit> <trans-unit id="%d hours" xml:space="preserve" approved="no"> <source>%d hours</source> - <target state="translated">%d hora(s)</target> + <target state="translated">%d horas</target> <note>message ttl</note> </trans-unit> <trans-unit id="%d min" xml:space="preserve" approved="no"> @@ -99,7 +99,7 @@ </trans-unit> <trans-unit id="%d months" xml:space="preserve" approved="no"> <source>%d months</source> - <target state="translated">%d mês(es)</target> + <target state="translated">%d meses</target> <note>message ttl</note> </trans-unit> <trans-unit id="%d sec" xml:space="preserve" approved="no"> @@ -109,7 +109,7 @@ </trans-unit> <trans-unit id="%d skipped message(s)" xml:space="preserve" approved="no"> <source>%d skipped message(s)</source> - <target state="translated">%d mensagem(ns) ignorada(s)</target> + <target state="translated">%d mensagem(ns) omitida(s)</target> <note>integrity error chat item</note> </trans-unit> <trans-unit id="%lld" xml:space="preserve" approved="no"> @@ -187,23 +187,18 @@ <target state="translated">)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target state="translated">**Adicionar novo contato**: para criar seu QR Code ou link único para seu contato.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no"> <source>**Create link / QR code** for your contact to use.</source> <target state="translated">**Crie um link / QR code** para seu contato usar.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve" approved="no"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> <target state="translated">**Mais privado**: verifique as novas mensagens a cada 20 minutos. O token do dispositivo é compartilhado com o servidor do SimpleX Chat, mas não quantos contatos ou mensagens você tem.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve" approved="no"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> <target state="translated">**Mais privado**: não use o servidor de notificações do SimpleX Chat, verifique as mensagens periodicamente em segundo plano (depende da frequência com que você usa o aplicativo).</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -217,8 +212,8 @@ <target state="translated">**Observação**: NÃO será possível recuperar ou alterar a frase secreta se você a perder.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve" approved="no"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> <target state="translated">**Recomendado**: o token do dispositivo e as notificações são enviados para o servidor de notificações do SimpleX Chat, mas não o conteúdo, o tamanho ou o remetente da mensagem.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -374,9 +369,9 @@ <target state="translated">Adicione servidores escaneando o QR code.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve" approved="no"> - <source>Add server…</source> - <target state="translated">Adicionar servidor…</target> + <trans-unit id="Add server" xml:space="preserve" approved="no"> + <source>Add server</source> + <target state="translated">Adicionar servidor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve" approved="no"> @@ -1209,8 +1204,8 @@ <target state="translated">Mensagens diretas</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve" approved="no"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve" approved="no"> + <source>Direct messages between members are prohibited.</source> <target state="translated">Mensagens diretas entre membros são proibidas neste grupo.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1229,8 +1224,8 @@ <target state="translated">Mensagens temporárias são proibidas nesse chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve" approved="no"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve" approved="no"> + <source>Disappearing messages are prohibited.</source> <target state="translated">Mensagens que temporárias são proibidas neste grupo.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1643,18 +1638,18 @@ <target state="translated">Os membros do grupo podem excluir mensagens enviadas de forma irreversível.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve" approved="no"> - <source>Group members can send direct messages.</source> + <trans-unit id="Members can send direct messages." xml:space="preserve" approved="no"> + <source>Members can send direct messages.</source> <target state="translated">Os membros do grupo podem enviar DMs.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve" approved="no"> - <source>Group members can send disappearing messages.</source> + <trans-unit id="Members can send disappearing messages." xml:space="preserve" approved="no"> + <source>Members can send disappearing messages.</source> <target state="translated">Os membros do grupo podem enviar mensagens que desaparecem.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve" approved="no"> - <source>Group members can send voice messages.</source> + <trans-unit id="Members can send voice messages." xml:space="preserve" approved="no"> + <source>Members can send voice messages.</source> <target state="translated">Os membros do grupo podem enviar mensagens de voz.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1761,8 +1756,8 @@ <target state="translated">A imagem será recebida quando seu contato estiver online, aguarde ou verifique mais tarde!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve" approved="no"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve" approved="no"> + <source>Immune to spam</source> <target state="translated">Imune a spam e abuso</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1878,8 +1873,8 @@ <target state="translated">A exclusão irreversível de mensagens é proibida neste chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve" approved="no"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve" approved="no"> + <source>Irreversible message deletion is prohibited.</source> <target state="translated">A exclusão irreversível de mensagens é proibida neste grupo.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2209,8 +2204,8 @@ We will be adding server redundancy to prevent lost messages.</source> <target state="translated">Hosts Onion não serão usados.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve" approved="no"> @@ -2267,8 +2262,8 @@ We will be adding server redundancy to prevent lost messages.</source> <target state="translated">Abrir console de chat</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve" approved="no"> - <source>Open-source protocol and code – anybody can run the servers.</source> + <trans-unit id="Anybody can host servers." xml:space="preserve" approved="no"> + <source>Anybody can host servers.</source> <target state="translated">Protocolo de código aberto – qualquer um pode executar os servidores.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2306,8 +2301,8 @@ We will be adding server redundancy to prevent lost messages.</source> <target state="translated">Cole o link que você recebeu na caixa abaixo para conectar com o seu contato.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve" approved="no"> - <source>People can connect to you only via the links you share.</source> + <trans-unit id="You decide who can connect." xml:space="preserve" approved="no"> + <source>You decide who can connect.</source> <target state="translated">Pessoas podem se conectar com você somente via links compartilhados.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2961,8 +2956,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>Thank you for installing SimpleX Chat!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve" approved="no"> - <source>The 1st platform without any user identifiers – private by design.</source> + <trans-unit id="No user identifiers." xml:space="preserve" approved="no"> + <source>No user identifiers.</source> <target state="translated">A 1ª plataforma sem nenhum identificador de usuário – privada por design.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2998,8 +2993,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>The microphone does not work when the app is in the background.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve" approved="no"> - <source>The next generation of private messaging</source> + <trans-unit id="The future of messaging" xml:space="preserve" approved="no"> + <source>The future of messaging</source> <target state="translated">A próxima geração de mensageiros privados</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3007,8 +3002,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>The old database was not removed during the migration, it can be deleted.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve" approved="no"> - <source>The profile is only shared with your contacts.</source> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve" approved="no"> + <source>Your profile is stored on your device and only shared with your contacts.</source> <target state="translated">O perfil é compartilhado apenas com seus contatos.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3071,8 +3066,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>To prevent the call interruption, enable Do Not Disturb mode.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve" approved="no"> @@ -3274,8 +3269,8 @@ Para se conectar, peça ao seu contato para criar outro link de conexão e verif <target state="translated">Mensagens de voz são proibidas neste chat.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve" approved="no"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve" approved="no"> + <source>Voice messages are prohibited.</source> <target state="translated">Mensagens de voz são proibidas neste grupo.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3402,10 +3397,6 @@ Para se conectar, peça ao seu contato para criar outro link de conexão e verif <target state="translated">Você pode usar markdown para formatar mensagens:</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve" approved="no"> <source>You could not be verified; please try again.</source> <target state="translated">Você não pôde ser verificado; por favor, tente novamente.</target> @@ -5189,6 +5180,471 @@ Isso pode acontecer por causa de algum bug ou quando a conexão está comprometi <target state="translated">Desaparecerá: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="# %@" xml:space="preserve" approved="no"> + <source># %@</source> + <target state="translated"># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve" approved="no"> + <source>## History</source> + <target state="translated">## Histórico</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve" approved="no"> + <source>## In reply to</source> + <target state="translated">## Em resposta a</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="%@ connected" xml:space="preserve" approved="no"> + <source>%@ connected</source> + <target state="translated">%@ conectado(a)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@, %@ and %lld members" xml:space="preserve" approved="no"> + <source>%@, %@ and %lld members</source> + <target state="translated">%@, %@ e %lld membros</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve" approved="no"> + <source>%@ and %@ connected</source> + <target state="translated">%@ e %@ conectados</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve" approved="no"> + <source>%@, %@ and %lld other members connected</source> + <target state="translated">%@, %@ e %lld e outros membros se conectaram</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ and %@" xml:space="preserve" approved="no"> + <source>%@ and %@</source> + <target state="translated">%@ e %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ at %@:" xml:space="preserve" approved="no"> + <source>%1$@ at %2$@:</source> + <target state="translated">%1$@ em %2$@:</target> + <note>copied message info, <sender> at <time></note> + </trans-unit> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target state="translated">Permitir que seus contatos deletem mensagens enviadas de maneira irreversível. (24 horas)</target> + </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve" approved="no"> + <source>%@ downloaded</source> + <target state="translated">baixado</target> + </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve" approved="no"> + <source>%@ uploaded</source> + <target state="translated">transferido</target> + </trans-unit> + <trans-unit id="A new random profile will be shared." xml:space="preserve" approved="no"> + <source>A new random profile will be shared.</source> + <target state="translated">Um novo perfil aleatório será compartilhado.</target> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve" approved="no"> + <source>Camera not available</source> + <target state="translated">Câmera indisponível</target> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve" approved="no"> + <source>Admins can block a member for all.</source> + <target state="translated">Administradores podem bloquear um membro para todos.</target> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target state="translated">Permitir que mensagens enviadas sejam deletadas de maneira irreversível. (24 horas)</target> + </trans-unit> + <trans-unit id="Apply" xml:space="preserve" approved="no"> + <source>Apply</source> + <target state="translated">Aplicar</target> + </trans-unit> + <trans-unit id="Accent" xml:space="preserve" approved="no"> + <source>Accent</source> + <target state="translated">Esquema</target> + </trans-unit> + <trans-unit id="Accept connection request?" xml:space="preserve" approved="no"> + <source>Accept connection request?</source> + <target state="translated">Aceitar solicitação de conexão?</target> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve" approved="no"> + <source>Active connections</source> + <target state="translated">Conexões ativas</target> + </trans-unit> + <trans-unit id="Add contact" xml:space="preserve" approved="no"> + <source>Add contact</source> + <target state="translated">Adicionar contato</target> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve" approved="no"> + <source>Additional accent</source> + <target state="translated">Esquema adicional</target> + </trans-unit> + <trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve" approved="no"> + <source>All new messages from %@ will be hidden!</source> + <target state="translated">Todas as novas mensagens de %@ serão ocultas!</target> + </trans-unit> + <trans-unit id="All profiles" xml:space="preserve" approved="no"> + <source>All profiles</source> + <target state="translated">Todos perfis</target> + </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve" approved="no"> + <source>Allow calls?</source> + <target state="translated">Permitir chamadas?</target> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve" approved="no"> + <source>Archive contacts to chat later.</source> + <target state="translated">Arquivar contatos para conversar depois.</target> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve" approved="no"> + <source>Blur media</source> + <target state="translated">Censurar mídia</target> + </trans-unit> + <trans-unit id="Calls prohibited!" xml:space="preserve" approved="no"> + <source>Calls prohibited!</source> + <target state="translated">Chamadas proibidas!</target> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve" approved="no"> + <source>Can't call contact</source> + <target state="translated">Não foi possível ligar para o contato</target> + </trans-unit> + <trans-unit id="%lld messages marked deleted" xml:space="preserve" approved="no"> + <source>%lld messages marked deleted</source> + <target state="translated">mensagens deletadas</target> + </trans-unit> + <trans-unit id="0 sec" xml:space="preserve" approved="no"> + <source>0 sec</source> + <target state="translated">0 seg</target> + </trans-unit> + <trans-unit id="%lld messages blocked" xml:space="preserve" approved="no"> + <source>%lld messages blocked</source> + <target state="translated">mensagens bloqueadas</target> + </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve" approved="no"> + <source>%lld messages blocked by admin</source> + <target state="translated">mensagens bloqueadas pelo administrador</target> + </trans-unit> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve" approved="no"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> + <target state="translated">**Nota**: usar o mesmo banco de dados em dois dispositivos irá quebrar a desencriptação das mensagens de suas conexões como uma medida de segurança.</target> + </trans-unit> + <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve" approved="no"> + <source>- more stable message delivery. +- a bit better groups. +- and more!</source> + <target state="translated">- entrega de mensagens mais estável. +- grupos melhorados. +- e muito mais!</target> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve" approved="no"> + <source>All messages will be deleted - this cannot be undone!</source> + <target state="translated">Todas as mensagens serão deletadas - isto não pode ser desfeito!</target> + </trans-unit> + <trans-unit id="Allow to send files and media." xml:space="preserve" approved="no"> + <source>Allow to send files and media.</source> + <target state="translated">Permitir o envio de arquivos e mídia.</target> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve" approved="no"> + <source>Allow to send SimpleX links.</source> + <target state="translated">Permitir envio de links SimpleX.</target> + </trans-unit> + <trans-unit id="Block for all" xml:space="preserve" approved="no"> + <source>Block for all</source> + <target state="translated">Bloquear para todos</target> + </trans-unit> + <trans-unit id="Block member" xml:space="preserve" approved="no"> + <source>Block member</source> + <target state="translated">Bloquear membro</target> + </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve" approved="no"> + <source>Blocked by admin</source> + <target state="translated">Bloqueado por um administrador</target> + </trans-unit> + <trans-unit id="Block group members" xml:space="preserve" approved="no"> + <source>Block group members</source> + <target state="translated">Bloquear membros de grupo</target> + </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve" approved="no"> + <source>Block member for all?</source> + <target state="translated">Bloquear membro para todos?</target> + </trans-unit> + <trans-unit id="Block member?" xml:space="preserve" approved="no"> + <source>Block member?</source> + <target state="translated">Bloquear membro?</target> + </trans-unit> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target state="translated">Você e seu contato podem apagar mensagens enviadas de maneira irreversível. (24 horas)</target> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve" approved="no"> + <source>Can't call member</source> + <target state="translated">Não foi possível ligar para este membro</target> + </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve" approved="no"> + <source>Can't message member</source> + <target state="translated">Não foi possível enviar mensagem para este membro</target> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve" approved="no"> + <source>Cancel migration</source> + <target state="translated">Cancelar migração</target> + </trans-unit> + <trans-unit id="Abort" xml:space="preserve" approved="no"> + <source>Abort</source> + <target state="translated">Abortar</target> + </trans-unit> + <trans-unit id="Abort changing address" xml:space="preserve" approved="no"> + <source>Abort changing address</source> + <target state="translated">Abortar troca de endereço</target> + </trans-unit> + <trans-unit id="Abort changing address?" xml:space="preserve" approved="no"> + <source>Abort changing address?</source> + <target state="translated">Abortar troca de endereço?</target> + </trans-unit> + <trans-unit id="- optionally notify deleted contacts. - profile names with spaces. - and more!" xml:space="preserve" approved="no"> + <source>- optionally notify deleted contacts. +- profile names with spaces. +- and more!</source> + <target state="translated">- notificar contatos apagados de maneira opcional. +- nome de perfil com espaços. +- e muito mais!</target> + </trans-unit> + <trans-unit id="Allow sharing" xml:space="preserve" approved="no"> + <source>Allow sharing</source> + <target state="translated">Permitir compartilhamento</target> + </trans-unit> + <trans-unit id="Block" xml:space="preserve" approved="no"> + <source>Block</source> + <target state="translated">Bloquear</target> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve" approved="no"> + <source>Additional accent 2</source> + <target state="translated">Esquema adicional 2</target> + </trans-unit> + <trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve" approved="no"> + <source>Address change will be aborted. Old receiving address will be used.</source> + <target state="translated">Alteração de endereço será abortada. O endereço antigo será utilizado.</target> + </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve" approved="no"> + <source>Advanced settings</source> + <target state="translated">Configurações avançadas</target> + </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve" approved="no"> + <source>All data is kept private on your device.</source> + <target state="translated">Toda informação é privada em seu dispositivo.</target> + </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve" approved="no"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target state="translated">Todos os seus contatos, conversas e arquivos serão encriptados e enviados em pedaços para nós XFTP.</target> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve" approved="no"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target state="translated">Permitir deletar mensagens de maneira irreversível apenas se seu contato permitir para você. (24 horas)</target> + </trans-unit> + <trans-unit id="Already connecting!" xml:space="preserve" approved="no"> + <source>Already connecting!</source> + <target state="translated">Já está conectando!</target> + </trans-unit> + <trans-unit id="Already joining the group!" xml:space="preserve" approved="no"> + <source>Already joining the group!</source> + <target state="translated">Já está entrando no grupo!</target> + </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve" approved="no"> + <source>Always use private routing.</source> + <target state="translated">Sempre use rotas privadas.</target> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve" approved="no"> + <source>Apply to</source> + <target state="translated">Aplicar em</target> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve" approved="no"> + <source>Archiving database</source> + <target state="translated">Arquivando banco de dados</target> + </trans-unit> + <trans-unit id="Black" xml:space="preserve" approved="no"> + <source>Black</source> + <target state="translated">Preto</target> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve" approved="no"> + <source>Cannot forward message</source> + <target state="translated">Não é possível encaminhar mensagem</target> + </trans-unit> + <trans-unit id="(new)" xml:space="preserve" approved="no"> + <source>(new)</source> + <target state="translated">(novo)</target> + </trans-unit> + <trans-unit id="(this device v%@)" xml:space="preserve" approved="no"> + <source>(this device v%@)</source> + <target state="translated">este dispositivo</target> + </trans-unit> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve" approved="no"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target state="translated">**Adicionar contato**: criar um novo link de convite ou conectar via um link que você recebeu.</target> + </trans-unit> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve" approved="no"> + <source>**Create group**: to create a new group.</source> + <target state="translated">**Criar grupo**: criar um novo grupo.</target> + </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve" approved="no"> + <source>**Warning**: the archive will be removed.</source> + <target state="translated">**Aviso**: o arquivo será removido.</target> + </trans-unit> + <trans-unit id="A few more things" xml:space="preserve" approved="no"> + <source>A few more things</source> + <target state="translated">E mais algumas coisas</target> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve" approved="no"> + <source>Archived contacts</source> + <target state="translated">Contatos arquivados</target> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve" approved="no"> + <source>Cellular</source> + <target state="translated">Rede móvel</target> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve" approved="no"> + <source>%d file(s) failed to download.</source> + <target state="translated">%d arquivo(s) falharam ao ser baixados.</target> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve" approved="no"> + <source>%d file(s) were deleted.</source> + <target state="translated">%d arquivo(s) foram apagados.</target> + </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve" approved="no"> + <source>%d messages not forwarded</source> + <target state="translated">%d mensagens não encaminhadas</target> + </trans-unit> + <trans-unit id="Bad desktop address" xml:space="preserve" approved="no"> + <source>Bad desktop address</source> + <target state="translated">Endereço de desktop incorreto</target> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve" approved="no"> + <source>Blur for better privacy.</source> + <target state="translated">Borrar para melhor privacidade.</target> + </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve" approved="no"> + <source>%d file(s) are still being downloaded.</source> + <target state="translated">%d arquivo(s) ainda estão sendo baixados.</target> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve" approved="no"> + <source>%d file(s) were not downloaded.</source> + <target state="translated">%d arquivo(s) não foram baixados.</target> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve" approved="no"> + <source>Chat colors</source> + <target state="translated">Cores do chat</target> + </trans-unit> + <trans-unit id="%lld group events" xml:space="preserve" approved="no"> + <source>%lld group events</source> + <target state="translated">%lld eventos do grupo</target> + </trans-unit> + <trans-unit id="%lld messages moderated by %@" xml:space="preserve" approved="no"> + <source>%lld messages moderated by %@</source> + <target state="translated">%lld mensagens moderadas por %@</target> + </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve" approved="no"> + <source>%1$@, %2$@</source> + <target state="translated">%1$@, %2$@</target> + </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve" approved="no"> + <source>%lld new interface languages</source> + <target state="translated">%lld novos idiomas de interface</target> + </trans-unit> + <trans-unit id="Better networking" xml:space="preserve" approved="no"> + <source>Better networking</source> + <target state="translated">Melhores redes</target> + </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve" approved="no"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target state="translated">Búlgaro, Finlandês, Tailandês e Ucraniano - obrigado aos usuários e [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> + </trans-unit> + <trans-unit id="Better groups" xml:space="preserve" approved="no"> + <source>Better groups</source> + <target state="translated">Melhores grupos</target> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve" approved="no"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <target state="translated">Capacidade excedida - o destinatário não recebeu as mensagens enviadas anteriormente.</target> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve" approved="no"> + <source>Chat migrated!</source> + <target state="translated">Conversa migrada!</target> + </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve" approved="no"> + <source>Auto-accept settings</source> + <target state="translated">Aceitar automaticamente configurações</target> + </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve" approved="no"> + <source>App encrypts new local files (except videos).</source> + <target state="translated">O aplicativo criptografa novos arquivos locais (exceto videos).</target> + </trans-unit> + <trans-unit id="App session" xml:space="preserve" approved="no"> + <source>App session</source> + <target state="translated">Sessão do aplicativo</target> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve" approved="no"> + <source>Acknowledged</source> + <target state="translated">Reconhecido</target> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve" approved="no"> + <source>Acknowledgement errors</source> + <target state="translated">Erros conhecidos</target> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve" approved="no"> + <source>Chat list</source> + <target state="translated">Lista de conversas</target> + </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve" approved="no"> + <source>Chat database exported</source> + <target state="translated">Banco de dados da conversa exportado</target> + </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve" approved="no"> + <source>Chat preferences were changed.</source> + <target state="translated">As preferências de bate-papo foram alteradas.</target> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve" approved="no"> + <source>Chat theme</source> + <target state="translated">Tema da conversa</target> + </trans-unit> + <trans-unit id="Better calls" xml:space="preserve" approved="no"> + <source>Better calls</source> + <target state="translated">Chamadas melhores</target> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve" approved="no"> + <source>Better user experience</source> + <target state="translated">Melhor experiência do usuário</target> + </trans-unit> + <trans-unit id="Allow downgrade" xml:space="preserve" approved="no"> + <source>Allow downgrade</source> + <target state="translated">Permitir redução</target> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve" approved="no"> + <source>Additional secondary</source> + <target state="translated">Secundária adicional</target> + </trans-unit> + <trans-unit id="App data migration" xml:space="preserve" approved="no"> + <source>App data migration</source> + <target state="translated">Migração de dados do aplicativo</target> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve" approved="no"> + <source>Archive and upload</source> + <target state="translated">Arquivar e enviar</target> + </trans-unit> + <trans-unit id="Background" xml:space="preserve" approved="no"> + <source>Background</source> + <target state="translated">Fundo</target> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve" approved="no"> + <source>Better message dates.</source> + <target state="translated">Datas de mensagens melhores.</target> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve" approved="no"> + <source>Better notifications</source> + <target state="translated">Notificações melhores</target> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve" approved="no"> + <source>Better security ✅</source> + <target state="translated">Melhor segurança ✅</target> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve" approved="no"> + <source>Chat profile</source> + <target state="translated">Perfil da conversa</target> + </trans-unit> </body> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="pt-BR" datatype="plaintext"> diff --git a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff index 71c523ca4e..bc8bf79da1 100644 --- a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff +++ b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff @@ -5,9 +5,11 @@ <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/> </header> <body> - <trans-unit id=" " xml:space="preserve"> + <trans-unit id=" " xml:space="preserve" approved="no"> <source> </source> + <target state="needs-translation"> +</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id=" Available in v5.1" xml:space="preserve"> @@ -15,140 +17,174 @@ Available in v5.1</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" " xml:space="preserve"> + <trans-unit id=" " xml:space="preserve" approved="no"> <source> </source> + <target state="translated"> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" " xml:space="preserve"> + <trans-unit id=" " xml:space="preserve" approved="no"> <source> </source> + <target state="translated"> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" " xml:space="preserve"> + <trans-unit id=" " xml:space="preserve" approved="no"> <source> </source> + <target state="translated"> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" (" xml:space="preserve"> + <trans-unit id=" (" xml:space="preserve" approved="no"> <source> (</source> + <target state="translated"> (</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" (can be copied)" xml:space="preserve"> + <trans-unit id=" (can be copied)" xml:space="preserve" approved="no"> <source> (can be copied)</source> + <target state="translated"> .(pode ser copiado)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="!1 colored!" xml:space="preserve"> + <trans-unit id="!1 colored!" xml:space="preserve" approved="no"> <source>!1 colored!</source> + <target state="translated">!1 colorido!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="#secret#" xml:space="preserve"> + <trans-unit id="#secret#" xml:space="preserve" approved="no"> <source>#secret#</source> + <target state="translated">#secreto#</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@" xml:space="preserve"> + <trans-unit id="%@" xml:space="preserve" approved="no"> <source>%@</source> + <target state="needs-translation">%@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ %@" xml:space="preserve"> + <trans-unit id="%@ %@" xml:space="preserve" approved="no"> <source>%@ %@</source> + <target state="needs-translation">%@ %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ / %@" xml:space="preserve"> + <trans-unit id="%@ / %@" xml:space="preserve" approved="no"> <source>%@ / %@</source> + <target state="needs-translation">%@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ is connected!" xml:space="preserve"> + <trans-unit id="%@ is connected!" xml:space="preserve" approved="no"> <source>%@ is connected!</source> + <target state="translated">%@ está conectado!</target> <note>notification title</note> </trans-unit> - <trans-unit id="%@ is not verified" xml:space="preserve"> + <trans-unit id="%@ is not verified" xml:space="preserve" approved="no"> <source>%@ is not verified</source> + <target state="translated">%@ não foi verificado</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ is verified" xml:space="preserve"> + <trans-unit id="%@ is verified" xml:space="preserve" approved="no"> <source>%@ is verified</source> + <target state="translated">%@ foi verificado</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ servers" xml:space="preserve"> + <trans-unit id="%@ servers" xml:space="preserve" approved="no"> <source>%@ servers</source> + <target state="translated">%@ servidores</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ wants to connect!" xml:space="preserve"> + <trans-unit id="%@ wants to connect!" xml:space="preserve" approved="no"> <source>%@ wants to connect!</source> + <target state="translated">%@ quer se conectar!</target> <note>notification title</note> </trans-unit> - <trans-unit id="%d days" xml:space="preserve"> + <trans-unit id="%d days" xml:space="preserve" approved="no"> <source>%d days</source> + <target state="translated">%d dias</target> <note>message ttl</note> </trans-unit> - <trans-unit id="%d hours" xml:space="preserve"> + <trans-unit id="%d hours" xml:space="preserve" approved="no"> <source>%d hours</source> + <target state="translated">%d horas</target> <note>message ttl</note> </trans-unit> - <trans-unit id="%d min" xml:space="preserve"> + <trans-unit id="%d min" xml:space="preserve" approved="no"> <source>%d min</source> + <target state="translated">%d minuto</target> <note>message ttl</note> </trans-unit> - <trans-unit id="%d months" xml:space="preserve"> + <trans-unit id="%d months" xml:space="preserve" approved="no"> <source>%d months</source> + <target state="translated">%d meses</target> <note>message ttl</note> </trans-unit> - <trans-unit id="%d sec" xml:space="preserve"> + <trans-unit id="%d sec" xml:space="preserve" approved="no"> <source>%d sec</source> + <target state="translated">%d segundo</target> <note>message ttl</note> </trans-unit> - <trans-unit id="%d skipped message(s)" xml:space="preserve"> + <trans-unit id="%d skipped message(s)" xml:space="preserve" approved="no"> <source>%d skipped message(s)</source> + <target state="translated">%d mensagem(s) ignorada(s)</target> <note>integrity error chat item</note> </trans-unit> - <trans-unit id="%lld" xml:space="preserve"> + <trans-unit id="%lld" xml:space="preserve" approved="no"> <source>%lld</source> + <target state="needs-translation">%lld</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld %@" xml:space="preserve"> + <trans-unit id="%lld %@" xml:space="preserve" approved="no"> <source>%lld %@</source> + <target state="needs-translation">%lld %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld contact(s) selected" xml:space="preserve"> + <trans-unit id="%lld contact(s) selected" xml:space="preserve" approved="no"> <source>%lld contact(s) selected</source> + <target state="translated">%lld contato(s) selecionado(s)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve"> + <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve" approved="no"> <source>%lld file(s) with total size of %@</source> + <target state="translated">%lld arquivo(s) com tamanho total de %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld members" xml:space="preserve"> + <trans-unit id="%lld members" xml:space="preserve" approved="no"> <source>%lld members</source> + <target state="translated">%lld membros</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld minutes" xml:space="preserve"> + <trans-unit id="%lld minutes" xml:space="preserve" approved="no"> <source>%lld minutes</source> + <target state="translated">%lld minutos</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> + <trans-unit id="%lld second(s)" xml:space="preserve" approved="no"> <source>%lld second(s)</source> + <target state="translated">%lld segundo(s)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld seconds" xml:space="preserve"> + <trans-unit id="%lld seconds" xml:space="preserve" approved="no"> <source>%lld seconds</source> + <target state="translated">%lld segundos</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldd" xml:space="preserve"> + <trans-unit id="%lldd" xml:space="preserve" approved="no"> <source>%lldd</source> + <target state="needs-translation">%lldd</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldh" xml:space="preserve"> + <trans-unit id="%lldh" xml:space="preserve" approved="no"> <source>%lldh</source> + <target state="needs-translation">%lldh</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldk" xml:space="preserve"> + <trans-unit id="%lldk" xml:space="preserve" approved="no"> <source>%lldk</source> + <target state="needs-translation">%lldk</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldm" xml:space="preserve"> + <trans-unit id="%lldm" xml:space="preserve" approved="no"> <source>%lldm</source> + <target state="needs-translation">%lldm</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldmth" xml:space="preserve"> + <trans-unit id="%lldmth" xml:space="preserve" approved="no"> <source>%lldmth</source> + <target state="needs-translation">%lldmth</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%llds" xml:space="preserve"> @@ -159,64 +195,70 @@ Available in v5.1</source> <source>%lldw</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%u messages failed to decrypt." xml:space="preserve"> + <trans-unit id="%u messages failed to decrypt." xml:space="preserve" approved="no"> <source>%u messages failed to decrypt.</source> + <target state="translated">%u mensagens não foram descriptografadas.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%u messages skipped." xml:space="preserve"> + <trans-unit id="%u messages skipped." xml:space="preserve" approved="no"> <source>%u messages skipped.</source> + <target state="translated">%u mensagens ignoradas.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> + <trans-unit id="(" xml:space="preserve" approved="no"> <source>(</source> + <target state="needs-translation">(</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id=")" xml:space="preserve"> <source>)</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> <source>**Create link / QR code** for your contact to use.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> + <target state="translated">**Mais privado**: verifique novas mensagens a cada 20 minutos. O token do dispositivo é compartilhado com o servidor SimpleX Chat, mas não com quantos contatos ou mensagens você possui.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> + <target state="translated">**Totalmente privado**: não use o servidor de notificações do SimpleX Chat, verifique as mensagens periodicamente em segundo plano (depende da frequência com que você usa o aplicativo).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> + <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve" approved="no"> <source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source> + <target state="translated">**Atenção**: Você NÃO poderá recuperar ou alterar a senha caso a perca.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> + <target state="translated">**Recomendado**: O token do dispositivo e as notificações são enviados ao servidor de notificação do SimpleX Chat, mas não o conteúdo, o tamanho da mensagem ou de quem ela é.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> <source>**Scan QR code**: to connect to your contact in person or via video call.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> + <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve" approved="no"> <source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source> + <target state="translated">**Aviso**: As notificações push instantâneas exigem uma senha salva nas Chaves.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> + <trans-unit id="**e2e encrypted** audio call" xml:space="preserve" approved="no"> <source>**e2e encrypted** audio call</source> + <target state="translated">** Criptografado e2e** chamada de áudio</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**e2e encrypted** video call" xml:space="preserve"> + <trans-unit id="**e2e encrypted** video call" xml:space="preserve" approved="no"> <source>**e2e encrypted** video call</source> + <target state="translated">**Criptografado e2e** chamada de vídeo</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="*bold*" xml:space="preserve"> @@ -325,8 +367,8 @@ Available in v5.1</source> <source>Add servers by scanning QR codes.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -353,144 +395,175 @@ Available in v5.1</source> <source>All group members will remain connected.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> + <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve" approved="no"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> + <target state="translated">Todas as mensagens serão apagadas – isso não pode ser desfeito! As mensagens serão apagadas SOMENTE para você.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All your contacts will remain connected" xml:space="preserve"> <source>All your contacts will remain connected</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow" xml:space="preserve"> + <trans-unit id="Allow" xml:space="preserve" approved="no"> <source>Allow</source> + <target state="translated">Permitir</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow calls only if your contact allows them." xml:space="preserve"> + <trans-unit id="Allow calls only if your contact allows them." xml:space="preserve" approved="no"> <source>Allow calls only if your contact allows them.</source> + <target state="translated">Permita chamadas somente se seu contato permitir.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> + <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve" approved="no"> <source>Allow disappearing messages only if your contact allows it to you.</source> + <target state="translated">Permita o desaparecimento de mensagens somente se o seu contato permitir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> <source>Allow irreversible message deletion only if your contact allows it to you.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow sending direct messages to members." xml:space="preserve"> + <trans-unit id="Allow sending direct messages to members." xml:space="preserve" approved="no"> <source>Allow sending direct messages to members.</source> + <target state="translated">Permitir o envio de mensagens diretas aos membros.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow sending disappearing messages." xml:space="preserve"> + <trans-unit id="Allow sending disappearing messages." xml:space="preserve" approved="no"> <source>Allow sending disappearing messages.</source> + <target state="translated">Permitir o envio de mensagens que desaparecem.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> <source>Allow to irreversibly delete sent messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to send voice messages." xml:space="preserve"> + <trans-unit id="Allow to send voice messages." xml:space="preserve" approved="no"> <source>Allow to send voice messages.</source> + <target state="translated">Permitir enviar mensagens de voz.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve"> + <trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve" approved="no"> <source>Allow voice messages only if your contact allows them.</source> + <target state="translated">Permita mensagens de voz apenas se o seu contato permitir.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow voice messages?" xml:space="preserve"> + <trans-unit id="Allow voice messages?" xml:space="preserve" approved="no"> <source>Allow voice messages?</source> + <target state="translated">Permitir mensagens de voz?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to call you." xml:space="preserve"> + <trans-unit id="Allow your contacts to call you." xml:space="preserve" approved="no"> <source>Allow your contacts to call you.</source> + <target state="translated">Permita que seus contatos liguem para você.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> <source>Allow your contacts to irreversibly delete sent messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> + <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve" approved="no"> <source>Allow your contacts to send disappearing messages.</source> + <target state="translated">Permita que seus contatos enviem mensagens que desaparecem.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to send voice messages." xml:space="preserve"> + <trans-unit id="Allow your contacts to send voice messages." xml:space="preserve" approved="no"> <source>Allow your contacts to send voice messages.</source> + <target state="translated">Permita que seus contatos enviem mensagens de voz.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Already connected?" xml:space="preserve"> + <trans-unit id="Already connected?" xml:space="preserve" approved="no"> <source>Already connected?</source> + <target state="translated">Já conectado?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Always use relay" xml:space="preserve"> + <trans-unit id="Always use relay" xml:space="preserve" approved="no"> <source>Always use relay</source> + <target state="translated">Sempre usar retransmissão</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Answer call" xml:space="preserve"> + <trans-unit id="Answer call" xml:space="preserve" approved="no"> <source>Answer call</source> + <target state="translated">Atender chamada</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App build: %@" xml:space="preserve"> + <trans-unit id="App build: %@" xml:space="preserve" approved="no"> <source>App build: %@</source> + <target state="translated">Versão do Aplicativo: %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App icon" xml:space="preserve"> + <trans-unit id="App icon" xml:space="preserve" approved="no"> <source>App icon</source> + <target state="translated">Ícone do Aplicativo</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App passcode" xml:space="preserve"> + <trans-unit id="App passcode" xml:space="preserve" approved="no"> <source>App passcode</source> + <target state="translated">Senha do Aplicativo</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App version" xml:space="preserve"> + <trans-unit id="App version" xml:space="preserve" approved="no"> <source>App version</source> + <target state="translated">Versão do Aplicativo</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App version: v%@" xml:space="preserve"> + <trans-unit id="App version: v%@" xml:space="preserve" approved="no"> <source>App version: v%@</source> + <target state="translated">Versão do Aplicativo: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Appearance" xml:space="preserve"> + <trans-unit id="Appearance" xml:space="preserve" approved="no"> <source>Appearance</source> + <target state="translated">Aparência</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Attach" xml:space="preserve"> + <trans-unit id="Attach" xml:space="preserve" approved="no"> <source>Attach</source> + <target state="translated">Anexar</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Audio & video calls" xml:space="preserve"> + <trans-unit id="Audio & video calls" xml:space="preserve" approved="no"> <source>Audio & video calls</source> + <target state="translated">Chamadas de áudio e vídeo</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Audio and video calls" xml:space="preserve"> + <trans-unit id="Audio and video calls" xml:space="preserve" approved="no"> <source>Audio and video calls</source> + <target state="translated">Chamadas de áudio e vídeo</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Audio/video calls" xml:space="preserve"> + <trans-unit id="Audio/video calls" xml:space="preserve" approved="no"> <source>Audio/video calls</source> + <target state="translated">Chamadas de áudio/vídeo</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Audio/video calls are prohibited." xml:space="preserve"> + <trans-unit id="Audio/video calls are prohibited." xml:space="preserve" approved="no"> <source>Audio/video calls are prohibited.</source> + <target state="translated">Chamadas de áudio/vídeo são proibidas.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Authentication cancelled" xml:space="preserve"> + <trans-unit id="Authentication cancelled" xml:space="preserve" approved="no"> <source>Authentication cancelled</source> + <target state="translated">Autenticação cancelada</target> <note>PIN entry</note> </trans-unit> - <trans-unit id="Authentication failed" xml:space="preserve"> + <trans-unit id="Authentication failed" xml:space="preserve" approved="no"> <source>Authentication failed</source> + <target state="translated">Falha na autenticação</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Authentication is required before the call is connected, but you may miss calls." xml:space="preserve"> + <trans-unit id="Authentication is required before the call is connected, but you may miss calls." xml:space="preserve" approved="no"> <source>Authentication is required before the call is connected, but you may miss calls.</source> + <target state="translated">A autenticação é necessária antes que a chamada seja conectada, mas você pode perder chamadas.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Authentication unavailable" xml:space="preserve"> + <trans-unit id="Authentication unavailable" xml:space="preserve" approved="no"> <source>Authentication unavailable</source> + <target state="translated">Autenticação indisponível</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Auto-accept contact requests" xml:space="preserve"> + <trans-unit id="Auto-accept contact requests" xml:space="preserve" approved="no"> <source>Auto-accept contact requests</source> + <target state="translated">Aceitar solicitações de contato automaticamente</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Auto-accept images" xml:space="preserve"> @@ -545,160 +618,197 @@ Available in v5.1</source> <source>Can't delete user profile!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't invite contact!" xml:space="preserve"> + <trans-unit id="Can't invite contact!" xml:space="preserve" approved="no"> <source>Can't invite contact!</source> + <target state="translated">Não é possível convidar contato!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't invite contacts!" xml:space="preserve"> + <trans-unit id="Can't invite contacts!" xml:space="preserve" approved="no"> <source>Can't invite contacts!</source> + <target state="translated">Não é possível convidar contatos!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Cancel" xml:space="preserve"> + <trans-unit id="Cancel" xml:space="preserve" approved="no"> <source>Cancel</source> + <target state="translated">Cancelar</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve" approved="no"> <source>Cannot access keychain to save database password</source> + <target state="translated">Não é possível acessar as chaves para salvar a senha do banco de dados</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Cannot receive file" xml:space="preserve"> + <trans-unit id="Cannot receive file" xml:space="preserve" approved="no"> <source>Cannot receive file</source> + <target state="translated">Não é possível receber o arquivo</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change" xml:space="preserve"> + <trans-unit id="Change" xml:space="preserve" approved="no"> <source>Change</source> + <target state="translated">Mudar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change passcode" xml:space="preserve"> <source>Change passcode</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change database passphrase?" xml:space="preserve"> + <trans-unit id="Change database passphrase?" xml:space="preserve" approved="no"> <source>Change database passphrase?</source> + <target state="translated">Alterar a senha do banco de dados?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change lock mode" xml:space="preserve"> + <trans-unit id="Change lock mode" xml:space="preserve" approved="no"> <source>Change lock mode</source> + <target state="translated">Alterar modo de bloqueio</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Change member role?" xml:space="preserve"> + <trans-unit id="Change member role?" xml:space="preserve" approved="no"> <source>Change member role?</source> + <target state="translated">Alterar a função do membro?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change passcode" xml:space="preserve"> + <trans-unit id="Change passcode" xml:space="preserve" approved="no"> <source>Change passcode</source> + <target state="translated">Alterar senha</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Change receiving address" xml:space="preserve"> + <trans-unit id="Change receiving address" xml:space="preserve" approved="no"> <source>Change receiving address</source> + <target state="translated">Alterar endereço de recebimento</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change receiving address?" xml:space="preserve"> + <trans-unit id="Change receiving address?" xml:space="preserve" approved="no"> <source>Change receiving address?</source> + <target state="translated">Alterar endereço de recebimento?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change role" xml:space="preserve"> + <trans-unit id="Change role" xml:space="preserve" approved="no"> <source>Change role</source> + <target state="translated">Mudar função</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> + <trans-unit id="Chat archive" xml:space="preserve" approved="no"> <source>Chat archive</source> + <target state="translated">Arquivar conversa</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat console" xml:space="preserve"> + <trans-unit id="Chat console" xml:space="preserve" approved="no"> <source>Chat console</source> + <target state="translated">Console da conversa</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat database" xml:space="preserve"> + <trans-unit id="Chat database" xml:space="preserve" approved="no"> <source>Chat database</source> + <target state="translated">Banco de Dados da conversa</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat database deleted" xml:space="preserve"> + <trans-unit id="Chat database deleted" xml:space="preserve" approved="no"> <source>Chat database deleted</source> + <target state="translated">Banco de Dados da conversa apagado</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat database imported" xml:space="preserve"> + <trans-unit id="Chat database imported" xml:space="preserve" approved="no"> <source>Chat database imported</source> + <target state="translated">Banco de Dados da conversa importado</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat is running" xml:space="preserve"> + <trans-unit id="Chat is running" xml:space="preserve" approved="no"> <source>Chat is running</source> + <target state="translated">A conversa está em execução</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat is stopped" xml:space="preserve"> <source>Chat is stopped</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat preferences" xml:space="preserve"> + <trans-unit id="Chat preferences" xml:space="preserve" approved="no"> <source>Chat preferences</source> + <target state="translated">Preferências da conversa</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chats" xml:space="preserve"> + <trans-unit id="Chats" xml:space="preserve" approved="no"> <source>Chats</source> + <target state="translated">Conversas</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Check server address and try again." xml:space="preserve"> + <trans-unit id="Check server address and try again." xml:space="preserve" approved="no"> <source>Check server address and try again.</source> + <target state="translated">Verifique o endereço do servidor e tente novamente.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> + <trans-unit id="Chinese and Spanish interface" xml:space="preserve" approved="no"> <source>Chinese and Spanish interface</source> + <target state="translated">Interface em Chinês e Espanhol</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Choose file" xml:space="preserve"> + <trans-unit id="Choose file" xml:space="preserve" approved="no"> <source>Choose file</source> + <target state="translated">Escolher arquivo</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Choose from library" xml:space="preserve"> + <trans-unit id="Choose from library" xml:space="preserve" approved="no"> <source>Choose from library</source> + <target state="translated">Escolha da biblioteca</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Clear" xml:space="preserve"> + <trans-unit id="Clear" xml:space="preserve" approved="no"> <source>Clear</source> + <target state="translated">Limpar</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Clear conversation" xml:space="preserve"> + <trans-unit id="Clear conversation" xml:space="preserve" approved="no"> <source>Clear conversation</source> + <target state="translated">Limpar conversa</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Clear conversation?" xml:space="preserve"> + <trans-unit id="Clear conversation?" xml:space="preserve" approved="no"> <source>Clear conversation?</source> + <target state="translated">Limpar conversa?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Clear verification" xml:space="preserve"> + <trans-unit id="Clear verification" xml:space="preserve" approved="no"> <source>Clear verification</source> + <target state="translated">Limpar verificação</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> + <trans-unit id="Colors" xml:space="preserve" approved="no"> <source>Colors</source> + <target state="translated">Cores</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Compare file" xml:space="preserve"> + <trans-unit id="Compare file" xml:space="preserve" approved="no"> <source>Compare file</source> + <target state="translated">Comparar arquivo</target> <note>server test step</note> </trans-unit> - <trans-unit id="Compare security codes with your contacts." xml:space="preserve"> + <trans-unit id="Compare security codes with your contacts." xml:space="preserve" approved="no"> <source>Compare security codes with your contacts.</source> + <target state="translated">Compare os códigos de segurança com seus contatos.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Configure ICE servers" xml:space="preserve"> + <trans-unit id="Configure ICE servers" xml:space="preserve" approved="no"> <source>Configure ICE servers</source> + <target state="translated">Configurar servidores ICE</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Confirm" xml:space="preserve"> + <trans-unit id="Confirm" xml:space="preserve" approved="no"> <source>Confirm</source> + <target state="translated">Confirme</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Confirm Passcode" xml:space="preserve"> + <trans-unit id="Confirm Passcode" xml:space="preserve" approved="no"> <source>Confirm Passcode</source> + <target state="translated">Confirmar senha</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Confirm database upgrades" xml:space="preserve"> + <trans-unit id="Confirm database upgrades" xml:space="preserve" approved="no"> <source>Confirm database upgrades</source> + <target state="translated">Confirmar atualizações do banco de dados</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Confirm new passphrase…" xml:space="preserve"> + <trans-unit id="Confirm new passphrase…" xml:space="preserve" approved="no"> <source>Confirm new passphrase…</source> + <target state="translated">Confirme a nova senha…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Confirm password" xml:space="preserve"> @@ -1117,8 +1227,8 @@ Available in v5.1</source> <source>Direct messages</source> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> @@ -1133,8 +1243,8 @@ Available in v5.1</source> <source>Disappearing messages are prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disconnect" xml:space="preserve"> @@ -1569,16 +1679,16 @@ Available in v5.1</source> <source>Group members can irreversibly delete sent messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> @@ -1697,8 +1807,8 @@ Available in v5.1</source> <source>Immediately</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Import" xml:space="preserve"> @@ -1810,8 +1920,8 @@ Available in v5.1</source> <source>Irreversible message deletion is prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve"> @@ -2005,8 +2115,8 @@ Available in v5.1</source> <source>Migration is completed</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> @@ -2163,8 +2273,8 @@ Available in v5.1</source> <source>Onion hosts will not be used.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> @@ -2223,8 +2333,8 @@ Available in v5.1</source> <source>Open user profiles</source> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve"> @@ -2279,8 +2389,8 @@ Available in v5.1</source> <source>Paste the link you received into the box below to connect with your contact.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Periodically" xml:space="preserve"> @@ -2983,8 +3093,8 @@ Available in v5.1</source> <source>Thanks to the users – contribute via Weblate!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> @@ -3028,16 +3138,16 @@ It can happen because of some bug or when the connection is compromised.</source <source>The message will be marked as moderated for all members.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> <source>The old database was not removed during the migration, it can be deleted.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The sender will NOT be notified" xml:space="preserve"> @@ -3100,8 +3210,8 @@ It can happen because of some bug or when the connection is compromised.</source <source>To make a new connection</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -3326,8 +3436,8 @@ To connect, please ask your contact to create another connection link and check <source>Voice messages are prohibited in this chat.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> @@ -3467,10 +3577,6 @@ SimpleX Lock must be enabled.</source> <source>You can't send messages!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <note>No comment provided by engineer.</note> @@ -4162,6 +4268,307 @@ SimpleX servers cannot see your profile.</source> <source>\~strike~</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve" approved="no"> + <source>## In reply to</source> + <target state="translated">## Em resposta a</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve" approved="no"> + <source>%@ uploaded</source> + <target state="translated">%@ enviado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%d weeks" xml:space="preserve" approved="no"> + <source>%d weeks</source> + <target state="translated">%d semanas</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve" approved="no"> + <source>%lld messages blocked by admin</source> + <target state="translated">%lld mensagens bloqueadas pelo administrador</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve" approved="no"> + <source>%lld new interface languages</source> + <target state="translated">%lld novas interface de idiomas</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve" approved="no"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target state="translated">**Adicionar contato**: para criar um novo link de convite ou conectar-se por meio de um link que você recebeu.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve" approved="no"> + <source>**Create group**: to create a new group.</source> + <target state="translated">**Criar grupo**: para criar um novo grupo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve" approved="no"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> + <target state="translated">**Observação**: usar o mesmo banco de dados em dois dispositivos interromperá a descriptografia de mensagens de suas conexões, como proteção de segurança.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve" approved="no"> + <source>## History</source> + <target state="translated">## Histórico</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve" approved="no"> + <source>**Warning**: the archive will be removed.</source> + <target state="translated">**Atenção**: O arquivo será removido.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ and %@" xml:space="preserve" approved="no"> + <source>%@ and %@</source> + <target state="translated">%@ e %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve" approved="no"> + <source>%@ and %@ connected</source> + <target state="translated">%@ e %@ conectado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ at %@:" xml:space="preserve" approved="no"> + <source>%1$@ at %2$@:</source> + <target state="translated">%1$@ em %2$@:</target> + <note>copied message info, <sender> at <time></note> + </trans-unit> + <trans-unit id="%@ connected" xml:space="preserve" approved="no"> + <source>%@ connected</source> + <target state="translated">%@ conectado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@, %@ and %lld members" xml:space="preserve" approved="no"> + <source>%@, %@ and %lld members</source> + <target state="translated">%@, %@ e %lld membros</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve" approved="no"> + <source>%@, %@ and %lld other members connected</source> + <target state="translated">%@, %@ e %lld outros membros conectados</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld group events" xml:space="preserve" approved="no"> + <source>%lld group events</source> + <target state="translated">%lld eventos de grupo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld messages marked deleted" xml:space="preserve" approved="no"> + <source>%lld messages marked deleted</source> + <target state="translated">%lld mensagens marcadas como excluídas</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld messages blocked" xml:space="preserve" approved="no"> + <source>%lld messages blocked</source> + <target state="translated">%lld mensagens bloqueadas</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld messages moderated by %@" xml:space="preserve" approved="no"> + <source>%lld messages moderated by %@</source> + <target state="translated">%lld mensagens moderadas por %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="(this device v%@)" xml:space="preserve" approved="no"> + <source>(this device v%@)</source> + <target state="translated">(este dispositivo v%@)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All data is erased when it is entered." xml:space="preserve" approved="no"> + <source>All data is erased when it is entered.</source> + <target state="translated">Todos os dados são apagados quando são inseridos.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve" approved="no"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target state="translated">Permita a exclusão irreversível de mensagens somente se o seu contato permitir. (24 horas)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply" xml:space="preserve" approved="no"> + <source>Apply</source> + <target state="translated">Aplicar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve" approved="no"> + <source>Archive and upload</source> + <target state="translated">Arquivar e fazer envio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve" approved="no"> + <source>All new messages from %@ will be hidden!</source> + <target state="translated">Todas as novas mensagens de %@ ficarão ocultas!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Already connecting!" xml:space="preserve" approved="no"> + <source>Already connecting!</source> + <target state="translated">Já conectando!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve" approved="no"> + <source>App encrypts new local files (except videos).</source> + <target state="translated">O aplicativo criptografa novos arquivos locais (exceto vídeos).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App passcode is replaced with self-destruct passcode." xml:space="preserve" approved="no"> + <source>App passcode is replaced with self-destruct passcode.</source> + <target state="translated">A senha do Aplicativo é substituída pela senha de autodestruição.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All your contacts will remain connected." xml:space="preserve" approved="no"> + <source>All your contacts will remain connected.</source> + <target state="translated">Todos os seus contatos permanecerão conectados.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve" approved="no"> + <source>All your contacts will remain connected. Profile update will be sent to your contacts.</source> + <target state="translated">Todos os seus contatos permanecerão conectados. A atualização do perfil será enviada para seus contatos.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send files and media." xml:space="preserve" approved="no"> + <source>Allow to send files and media.</source> + <target state="translated">Permitir o envio de arquivos e mídia.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App data migration" xml:space="preserve" approved="no"> + <source>App data migration</source> + <target state="translated">Migração de dados de aplicativos</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve" approved="no"> + <source>Archiving database</source> + <target state="translated">Arquivando banco de dados</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve" approved="no"> + <source>All messages will be deleted - this cannot be undone!</source> + <target state="translated">Todas as mensagens serão apagadas – isso não pode ser desfeito!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve" approved="no"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target state="translated">Todos os seus contatos, conversas e arquivos serão criptografados com segurança e enviados em partes para retransmissões XFTP configuradas.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve" approved="no"> + <source>Allow message reactions only if your contact allows them.</source> + <target state="translated">Permita reações às mensagens somente se o seu contato permitir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow message reactions." xml:space="preserve" approved="no"> + <source>Allow message reactions.</source> + <target state="translated">Permitir reações às mensagens.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target state="translated">Permita que seus contatos apaguem irreversivelmente as mensagens enviadas. (24 horas)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Already joining the group!" xml:space="preserve" approved="no"> + <source>Already joining the group!</source> + <target state="translated">Entrando no grupo!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="An empty chat profile with the provided name is created, and the app opens as usual." xml:space="preserve" approved="no"> + <source>An empty chat profile with the provided name is created, and the app opens as usual.</source> + <target state="translated">Um perfil de conversa vazio com o nome fornecido é criado e o aplicativo abre normalmente.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Auto-accept" xml:space="preserve" approved="no"> + <source>Auto-accept</source> + <target state="translated">Aceitar automaticamente</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow your contacts adding message reactions." xml:space="preserve" approved="no"> + <source>Allow your contacts adding message reactions.</source> + <target state="translated">Permita que seus contatos adicionem reações às mensagens.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target state="translated">Permitir apagar irreversivelmente as mensagens enviadas. (24 horas)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve" approved="no"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <target state="translated">Escolha _Migrar de outro dispositivo_ no novo dispositivo e leia o código QR.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve" approved="no"> + <source>Cancel migration</source> + <target state="translated">Cancelar migração</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve" approved="no"> + <source>Camera not available</source> + <target state="translated">Câmera não disponível</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Change self-destruct passcode" xml:space="preserve" approved="no"> + <source>Change self-destruct passcode</source> + <target state="translated">Alterar a senha de autodestruição</target> + <note>authentication reason + set passcode view</note> + </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve" approved="no"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <target state="translated">A conversa foi interrompida. Se você já usou esse banco de dados em outro dispositivo, deverá transferi-lo de volta antes de iniciar a conversa.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve" approved="no"> + <source>Chat migrated!</source> + <target state="translated">Conversa migrada!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve" approved="no"> + <source>Clear private notes?</source> + <target state="translated">Limpar notas privadas?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve" approved="no"> + <source>Confirm network settings</source> + <target state="translated">Confirme as configurações de rede</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve" approved="no"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <target state="translated">Confirme se você se lembra da senha do banco de dados para migrá-lo.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Change self-destruct mode" xml:space="preserve" approved="no"> + <source>Change self-destruct mode</source> + <target state="translated">Alterar modo de autodestruição</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve" approved="no"> + <source>Confirm upload</source> + <target state="translated">Confirmar envio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve" approved="no"> + <source>%@ downloaded</source> + <target state="translated">%@ baixado</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="# %@" xml:space="preserve" approved="no"> + <source># %@</source> + <target state="needs-translation"># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="%@:" xml:space="preserve" approved="no"> + <source>%@:</source> + <target state="needs-translation">%@:</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="%@ (current)" xml:space="preserve" approved="no"> + <source>%@ (current)</source> + <target state="translated">%@(atual)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ (current):" xml:space="preserve" approved="no"> + <source>%@ (current):</source> + <target state="translated">%@ (atual):</target> + <note>copied message info</note> + </trans-unit> </body> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="pt" datatype="plaintext"> 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 f4970446ae..419fa75375 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -2,36 +2,9 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="ru" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> - <trans-unit id=" " xml:space="preserve"> - <source> -</source> - <target> -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (" xml:space="preserve"> - <source> (</source> - <target> (</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> <target> (можно скопировать)</target> @@ -107,6 +80,11 @@ <target>%@ соединен(а)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve"> + <source>%@ downloaded</source> + <target>%@ загружено</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ is connected!" xml:space="preserve"> <source>%@ is connected!</source> <target>Установлено соединение с %@!</target> @@ -122,16 +100,31 @@ <target>%@ подтверждён</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ server" xml:space="preserve"> + <source>%@ server</source> + <target>%@ сервер</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ servers" xml:space="preserve"> <source>%@ servers</source> <target>%@ серверы</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve"> + <source>%@ uploaded</source> + <target>%@ загружено</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ wants to connect!" xml:space="preserve"> <source>%@ wants to connect!</source> <target>%@ хочет соединиться!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve"> + <source>%1$@, %2$@</source> + <target>%1$@, %2$@</target> + <note>format for date separator in chat</note> + </trans-unit> <trans-unit id="%@, %@ and %lld members" xml:space="preserve"> <source>%@, %@ and %lld members</source> <target>%@, %@ и %lld членов группы</target> @@ -139,7 +132,7 @@ </trans-unit> <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> <source>%@, %@ and %lld other members connected</source> - <target>%@, %@ и %lld других членов соединены</target> + <target>установлено соединение с %@, %@ и %lld другими членами группы</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@:" xml:space="preserve"> @@ -152,11 +145,36 @@ <target>%d дней</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve"> + <source>%d file(s) are still being downloaded.</source> + <target>%d файл(ов) загружаются.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve"> + <source>%d file(s) failed to download.</source> + <target>%d файл(ов) не удалось загрузить.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve"> + <source>%d file(s) were deleted.</source> + <target>%d файлов было удалено.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve"> + <source>%d file(s) were not downloaded.</source> + <target>%d файлов не было загружено.</target> + <note>forward confirmation reason</note> + </trans-unit> <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> <target>%d ч.</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve"> + <source>%d messages not forwarded</source> + <target>%d сообщений не переслано</target> + <note>alert title</note> + </trans-unit> <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> <target>%d мин</target> @@ -172,6 +190,11 @@ <target>%d сек</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve"> + <source>%d seconds(s)</source> + <target>%d секунд</target> + <note>delete after time</note> + </trans-unit> <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> <target>%d пропущенных сообщение(й)</target> @@ -209,7 +232,7 @@ </trans-unit> <trans-unit id="%lld members" xml:space="preserve"> <source>%lld members</source> - <target>Членов группы: %lld</target> + <target>%lld членов</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld messages blocked" xml:space="preserve"> @@ -217,6 +240,11 @@ <target>%lld сообщений заблокировано</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve"> + <source>%lld messages blocked by admin</source> + <target>%lld сообщений заблокировано администратором</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld messages marked deleted" xml:space="preserve"> <source>%lld messages marked deleted</source> <target>%lld сообщений помечено удалёнными</target> @@ -237,11 +265,6 @@ <target>%lld новых языков интерфейса</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> - <source>%lld second(s)</source> - <target>%lld секунд</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="%lld seconds" xml:space="preserve"> <source>%lld seconds</source> <target>%lld секунд</target> @@ -292,11 +315,6 @@ <target>%u сообщений пропущено.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> - <source>(</source> - <target>(</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="(new)" xml:space="preserve"> <source>(new)</source> <target>(новое)</target> @@ -307,34 +325,29 @@ <target>(это устройство v%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve"> - <source>)</source> - <target>)</target> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target>**Добавить контакт**: создать и поделиться новой ссылкой-приглашением.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target>**Добавить новый контакт**: чтобы создать одноразовый QR код или ссылку для Вашего контакта.</target> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve"> + <source>**Create group**: to create a new group.</source> + <target>**Создать группу**: создать новую группу.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> - <source>**Create link / QR code** for your contact to use.</source> - <target>**Создать ссылку / QR код** для Вашего контакта.</target> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> + <target>**Более конфиденциально**: проверять новые сообщения каждые 20 минут. Только токен устройства будет отправлен на сервер уведомлений SimpleX Chat, но у сервера не будет информации о количестве контактов и какой либо информации о сообщениях.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> - <target>**Более конфиденциально**: проверять новые сообщения каждые 20 минут. Токен устройства будет отправлен на сервер уведомлений SimpleX Chat, но у сервера не будет информации о количестве контактов и сообщений.</target> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> + <target>**Самый конфиденциальный**: не использовать сервер уведомлений SimpleX Chat. Сообщения проверяются в фоновом режиме, когда система позволяет, в зависимости от того, как часто Вы используете приложение.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> - <target>**Самый конфиденциальный**: не использовать сервер уведомлений SimpleX Chat, проверять сообщения периодически в фоновом режиме (зависит от того насколько часто Вы используете приложение).</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <target>**Вставить полученную ссылку**, или откройте её в браузере и нажмите **Open in mobile app**.</target> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> + <target>**Обратите внимание**: использование одной и той же базы данных на двух устройствах нарушит расшифровку сообщений от ваших контактов, как свойство защиты соединений.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> @@ -342,14 +355,14 @@ <target>**Внимание**: Вы не сможете восстановить или поменять пароль, если Вы его потеряете.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Рекомендовано**: токен устройства и уведомления отправляются на сервер SimpleX Chat, но сервер не получает сами сообщения, их размер или от кого они.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target>**Сканировать QR код**: соединиться с Вашим контактом при встрече или во время видеозвонка.</target> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve"> + <source>**Scan / Paste link**: to connect via a link you received.</source> + <target>**Сканировать / Вставить ссылку**: чтобы соединиться через полученную ссылку.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> @@ -357,6 +370,11 @@ <target>**Внимание**: для работы мгновенных уведомлений пароль должен быть сохранен в Keychain.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve"> + <source>**Warning**: the archive will be removed.</source> + <target>**Внимание**: архив будет удален.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> <source>**e2e encrypted** audio call</source> <target>**e2e зашифрованный** аудиозвонок</target> @@ -372,11 +390,6 @@ <target>\*жирный*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=", " xml:space="preserve"> - <source>, </source> - <target>, </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). @@ -413,11 +426,6 @@ - история редактирования.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="." xml:space="preserve"> - <source>.</source> - <target>.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="0 sec" xml:space="preserve"> <source>0 sec</source> <target>0 сек</target> @@ -431,7 +439,8 @@ <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> <target>1 день</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> @@ -446,18 +455,30 @@ <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> <target>1 месяц</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> <target>1 неделю</target> - <note>time interval</note> + <note>delete after time +time interval</note> + </trans-unit> + <trans-unit id="1 year" xml:space="preserve"> + <source>1 year</source> + <target>1 год</target> + <note>delete after time</note> </trans-unit> <trans-unit id="1-time link" xml:space="preserve"> <source>1-time link</source> <target>Одноразовая ссылка</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> + <target>Одноразовая ссылка может быть использована *только с одним контактом* - поделитесь при встрече или через любой мессенджер.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="5 minutes" xml:space="preserve"> <source>5 minutes</source> <target>5 минут</target> @@ -473,11 +494,6 @@ <target>30 секунд</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve"> - <source>: </source> - <target>: </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> @@ -508,8 +524,8 @@ <trans-unit id="A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." xml:space="preserve"> <source>A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</source> - <target>Отдельное TCP-соединение (и авторизация SOCKS) будет использоваться **для каждого контакта и члена группы**. -**Обратите внимание**: если у Вас много контактов, потребление батареи и трафика может быть значительно выше, и некоторые соединения могут не работать.</target> + <target>Будет использовано отдельное TCP соединение **для каждого контакта и члена группы**. +**Примечание**: Чем больше подключений, тем быстрее разряжается батарея и расходуется трафик, а некоторые соединения могут отваливаться.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Abort" xml:space="preserve"> @@ -527,31 +543,32 @@ <target>Прекратить изменение адреса?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve"> - <source>About SimpleX</source> - <target>О SimpleX</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve"> <source>About SimpleX Chat</source> <target>Информация о SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve"> - <source>About SimpleX address</source> - <target>Об адресе SimpleX</target> + <trans-unit id="About operators" xml:space="preserve"> + <source>About operators</source> + <target>Об операторах</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accent color" xml:space="preserve"> - <source>Accent color</source> - <target>Основной цвет</target> + <trans-unit id="Accent" xml:space="preserve"> + <source>Accent</source> + <target>Акцент</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept" xml:space="preserve"> <source>Accept</source> <target>Принять</target> <note>accept contact request via notification - accept incoming call via notification</note> +accept incoming call via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accept conditions" xml:space="preserve"> + <source>Accept conditions</source> + <target>Принять условия</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept connection request?" xml:space="preserve"> <source>Accept connection request?</source> @@ -566,16 +583,47 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Принять инкогнито</target> - <note>accept contact request via notification</note> + <note>accept contact request via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve"> + <source>Accepted conditions</source> + <target>Принятые условия</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve"> + <source>Acknowledged</source> + <target>Подтверждено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve"> + <source>Acknowledgement errors</source> + <target>Ошибки подтверждения</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Active" xml:space="preserve"> + <source>Active</source> + <target>Активный</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve"> + <source>Active connections</source> + <target>Активные соединения</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> <target>Добавьте адрес в свой профиль, чтобы Ваши контакты могли поделиться им. Профиль будет отправлен Вашим контактам.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> - <source>Add preset servers</source> - <target>Добавить серверы по умолчанию</target> + <trans-unit id="Add friends" xml:space="preserve"> + <source>Add friends</source> + <target>Добавить друзей</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve"> + <source>Add list</source> + <target>Добавить список</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add profile" xml:space="preserve"> @@ -583,14 +631,19 @@ <target>Добавить профиль</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> + <target>Добавить сервер</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> <source>Add servers by scanning QR codes.</source> <target>Добавить серверы через QR код.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> - <target>Добавить сервер…</target> + <trans-unit id="Add team members" xml:space="preserve"> + <source>Add team members</source> + <target>Добавить сотрудников</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -598,11 +651,46 @@ <target>Добавить на другое устройство</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add to list" xml:space="preserve"> + <source>Add to list</source> + <target>Добавить в список</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> <target>Добавить приветственное сообщение</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve"> + <source>Add your team members to the conversations.</source> + <target>Добавьте сотрудников в разговор.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve"> + <source>Added media & file servers</source> + <target>Дополнительные серверы файлов и медиа</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve"> + <source>Added message servers</source> + <target>Дополнительные серверы сообщений</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve"> + <source>Additional accent</source> + <target>Дополнительный акцент</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve"> + <source>Additional accent 2</source> + <target>Дополнительный акцент 2</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve"> + <source>Additional secondary</source> + <target>Вторичный 2</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Address" xml:space="preserve"> <source>Address</source> <target>Адрес</target> @@ -613,6 +701,21 @@ <target>Изменение адреса будет прекращено. Будет использоваться старый адрес.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve"> + <source>Address or 1-time link?</source> + <target>Адрес или одноразовая ссылка?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve"> + <source>Address settings</source> + <target>Настройки адреса</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve"> + <source>Admins can block a member for all.</source> + <target>Админы могут заблокировать члена группы.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> <source>Admins can create the links to join groups.</source> <target>Админы могут создать ссылки для вступления в группу.</target> @@ -623,6 +726,16 @@ <target>Настройки сети</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve"> + <source>Advanced settings</source> + <target>Настройки сети</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All" xml:space="preserve"> + <source>All</source> + <target>Все</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All app data is deleted." xml:space="preserve"> <source>All app data is deleted.</source> <target>Все данные приложения будут удалены.</target> @@ -633,14 +746,34 @@ <target>Все чаты и сообщения будут удалены - это нельзя отменить!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <target>Все чаты будут удалены из списка %@, и список удален.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> <target>Все данные удаляются при его вводе.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve"> + <source>All data is kept private on your device.</source> + <target>Все данные хранятся только на вашем устройстве.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> - <target>Все члены группы, которые соединились через эту ссылку, останутся в группе.</target> + <target>Все члены группы останутся соединены.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <target>Все сообщения и файлы отправляются с **end-to-end шифрованием**, с постквантовой безопасностью в прямых разговорах.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone!</source> + <target>Все сообщения будут удалены - это нельзя отменить!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> @@ -653,6 +786,20 @@ <target>Все новые сообщения от %@ будут скрыты!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All profiles" xml:space="preserve"> + <source>All profiles</source> + <target>Все профили</target> + <note>profile dropdown</note> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve"> + <source>All reports will be archived for you.</source> + <target>Все сообщения о нарушениях будут заархивированы для вас.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All servers" xml:space="preserve"> + <source>All servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All your contacts will remain connected." xml:space="preserve"> <source>All your contacts will remain connected.</source> <target>Все контакты, которые соединились через этот адрес, сохранятся.</target> @@ -663,6 +810,11 @@ <target>Все Ваши контакты сохранятся. Обновленный профиль будет отправлен Вашим контактам.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target>Все ваши контакты, разговоры и файлы будут надежно зашифрованы и загружены на выбранные XFTP серверы.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow" xml:space="preserve"> <source>Allow</source> <target>Разрешить</target> @@ -673,14 +825,24 @@ <target>Разрешить звонки, только если их разрешает Ваш контакт.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve"> + <source>Allow calls?</source> + <target>Разрешить звонки?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> <target>Разрешить исчезающие сообщения, только если Ваш контакт разрешает их Вам.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <target>Разрешить необратимое удаление сообщений, только если Ваш контакт разрешает это Вам.</target> + <trans-unit id="Allow downgrade" xml:space="preserve"> + <source>Allow downgrade</source> + <target>Разрешить прямую доставку</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target>Разрешить необратимое удаление сообщений, только если Ваш контакт разрешает это Вам. (24 часа)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> @@ -695,7 +857,7 @@ </trans-unit> <trans-unit id="Allow sending direct messages to members." xml:space="preserve"> <source>Allow sending direct messages to members.</source> - <target>Разрешить посылать прямые сообщения членам группы.</target> + <target>Разрешить личные сообщения членам группы.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow sending disappearing messages." xml:space="preserve"> @@ -703,9 +865,24 @@ <target>Разрешить посылать исчезающие сообщения.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow to irreversibly delete sent messages.</source> - <target>Разрешить необратимо удалять отправленные сообщения.</target> + <trans-unit id="Allow sharing" xml:space="preserve"> + <source>Allow sharing</source> + <target>Разрешить поделиться</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target>Разрешить необратимо удалять отправленные сообщения. (24 часа)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve"> + <source>Allow to report messsages to moderators.</source> + <target>Разрешить отправлять сообщения о нарушениях модераторам.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve"> + <source>Allow to send SimpleX links.</source> + <target>Разрешить отправлять ссылки SimpleX.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send files and media." xml:space="preserve"> @@ -738,9 +915,9 @@ <target>Разрешить Вашим контактам звонить Вам.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow your contacts to irreversibly delete sent messages.</source> - <target>Разрешить Вашим контактам необратимо удалять отправленные сообщения.</target> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target>Разрешить Вашим контактам необратимо удалять отправленные сообщения. (24 часа)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> @@ -768,6 +945,11 @@ <target>Вступление в группу уже начато!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve"> + <source>Always use private routing.</source> + <target>Всегда использовать конфиденциальную доставку.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> <source>Always use relay</source> <target>Всегда соединяться через relay</target> @@ -778,21 +960,41 @@ <target>Будет создан пустой профиль чата с указанным именем, и приложение откроется в обычном режиме.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Another reason" xml:space="preserve"> + <source>Another reason</source> + <target>Другая причина</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> <target>Принять звонок</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> + <target>Кто угодно может запустить сервер.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> <target>Сборка приложения: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App data migration" xml:space="preserve"> + <source>App data migration</source> + <target>Миграция данных</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> <source>App encrypts new local files (except videos).</source> <target>Приложение шифрует новые локальные файлы (кроме видео).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App group:" xml:space="preserve"> + <source>App group:</source> + <target>Группа приложения:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>Иконка</target> @@ -808,6 +1010,11 @@ <target>Код доступа в приложение будет заменен кодом самоуничтожения.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App session" xml:space="preserve"> + <source>App session</source> + <target>Сессия приложения</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App version" xml:space="preserve"> <source>App version</source> <target>Версия приложения</target> @@ -823,6 +1030,66 @@ <target>Интерфейс</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Apply" xml:space="preserve"> + <source>Apply</source> + <target>Применить</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve"> + <source>Apply to</source> + <target>Применить к</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve"> + <source>Archive</source> + <target>Архивировать</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve"> + <source>Archive %lld reports?</source> + <target>Архивировать %lld сообщений о нарушениях?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve"> + <source>Archive all reports?</source> + <target>Архивировать все сообщения о нарушениях?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve"> + <source>Archive and upload</source> + <target>Архивировать и загрузить</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve"> + <source>Archive contacts to chat later.</source> + <target>Архивируйте контакты чтобы продолжить переписку.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve"> + <source>Archive report</source> + <target>Архивировать сообщение о нарушении</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve"> + <source>Archive report?</source> + <target>Архивировать сообщение о нарушении?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve"> + <source>Archive reports</source> + <target>Архивировать сообщения о нарушениях</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve"> + <source>Archived contacts</source> + <target>Архивированные контакты</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve"> + <source>Archiving database</source> + <target>Подготовка архива</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Attach" xml:space="preserve"> <source>Attach</source> <target>Прикрепить</target> @@ -883,11 +1150,21 @@ <target>Автоприем изображений</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve"> + <source>Auto-accept settings</source> + <target>Настройки автоприема</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Back" xml:space="preserve"> <source>Back</source> <target>Назад</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Background" xml:space="preserve"> + <source>Background</source> + <target>Фон</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Bad desktop address" xml:space="preserve"> <source>Bad desktop address</source> <target>Неверный адрес компьютера</target> @@ -903,24 +1180,74 @@ <target>Ошибка хэш сообщения</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better calls" xml:space="preserve"> + <source>Better calls</source> + <target>Улучшенные звонки</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better groups" xml:space="preserve"> <source>Better groups</source> <target>Улучшенные группы</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve"> + <source>Better groups performance</source> + <target>Улучшенная производительность групп</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve"> + <source>Better message dates.</source> + <target>Улучшенные даты сообщений.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better messages" xml:space="preserve"> <source>Better messages</source> <target>Улучшенные сообщения</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better networking" xml:space="preserve"> + <source>Better networking</source> + <target>Улучшенные сетевые функции</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve"> + <source>Better notifications</source> + <target>Улучшенные уведомления</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve"> + <source>Better privacy and security</source> + <target>Улучшенная конфиденциальность и безопасность</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve"> + <source>Better security ✅</source> + <target>Улучшенная безопасность ✅</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve"> + <source>Better user experience</source> + <target>Улучшенный интерфейс</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Black" xml:space="preserve"> + <source>Black</source> + <target>Черная</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block" xml:space="preserve"> <source>Block</source> <target>Заблокировать</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block for all" xml:space="preserve"> + <source>Block for all</source> + <target>Заблокировать для всех</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block group members" xml:space="preserve"> <source>Block group members</source> - <target>Блокируйте членов группы</target> + <target>Заблокировать членов группы</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Block member" xml:space="preserve"> @@ -928,19 +1255,39 @@ <target>Заблокировать члена группы</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve"> + <source>Block member for all?</source> + <target>Заблокировать для всех?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block member?" xml:space="preserve"> <source>Block member?</source> <target>Заблокировать члена группы?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve"> + <source>Blocked by admin</source> + <target>Заблокирован администратором</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve"> + <source>Blur for better privacy.</source> + <target>Размыть для конфиденциальности.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve"> + <source>Blur media</source> + <target>Размытие изображений</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <source>Both you and your contact can add message reactions.</source> <target>И Вы, и Ваш контакт можете добавлять реакции на сообщения.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> - <source>Both you and your contact can irreversibly delete sent messages.</source> - <target>Вы и Ваш контакт можете необратимо удалять отправленные сообщения.</target> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target>Вы и Ваш контакт можете необратимо удалять отправленные сообщения. (24 часа)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> @@ -963,11 +1310,35 @@ <target>Болгарский, финский, тайский и украинский - благодаря пользователям и [Weblate] (https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Business address" xml:space="preserve"> + <source>Business address</source> + <target>Бизнес адрес</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve"> + <source>Business chats</source> + <target>Бизнес разговоры</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve"> + <source>Businesses</source> + <target>Бизнесы</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>По профилю чата или [по соединению](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve"> + <source>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</source> + <target>Используя SimpleX Chat, Вы согласны: +- отправлять только законные сообщения в публичных группах. +- уважать других пользователей – не отправлять спам.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> <source>Call already ended!</source> <target>Звонок уже завершен!</target> @@ -978,9 +1349,24 @@ <target>Звонки</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve"> - <source>Can't delete user profile!</source> - <target>Нельзя удалить профиль пользователя!</target> + <trans-unit id="Calls prohibited!" xml:space="preserve"> + <source>Calls prohibited!</source> + <target>Звонки запрещены!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve"> + <source>Camera not available</source> + <target>Камера недоступна</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve"> + <source>Can't call contact</source> + <target>Не удается позвонить контакту</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve"> + <source>Can't call member</source> + <target>Не удаётся позвонить члену группы</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contact!" xml:space="preserve"> @@ -993,9 +1379,20 @@ <target>Нельзя пригласить контакты!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve"> + <source>Can't message member</source> + <target>Не удаётся отправить сообщение члену группы</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> <target>Отменить</target> + <note>alert action +alert button</note> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve"> + <source>Cancel migration</source> + <target>Отменить миграцию</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> @@ -1003,9 +1400,24 @@ <target>Ошибка доступа к Keychain при сохранении пароля</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Невозможно переслать сообщение</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> <target>Невозможно получить файл</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <target>Превышено количество сообщений - предыдущие сообщения не доставлены.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve"> + <source>Cellular</source> + <target>Мобильная сеть</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change" xml:space="preserve"> @@ -1013,6 +1425,16 @@ <target>Поменять</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve"> + <source>Change automatic message deletion?</source> + <target>Измененить автоматическое удаление сообщений?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve"> + <source>Change chat profiles</source> + <target>Поменять профили</target> + <note>authentication reason</note> + </trans-unit> <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> <target>Поменять пароль базы данных?</target> @@ -1057,11 +1479,26 @@ <source>Change self-destruct passcode</source> <target>Изменить код самоуничтожения</target> <note>authentication reason - set passcode view</note> +set passcode view</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> - <source>Chat archive</source> - <target>Архив чата</target> + <trans-unit id="Chat" xml:space="preserve"> + <source>Chat</source> + <target>Разговор</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve"> + <source>Chat already exists</source> + <target>Разговор уже существует</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve"> + <source>Chat already exists!</source> + <target>Разговор уже существует!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve"> + <source>Chat colors</source> + <target>Цвета чата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat console" xml:space="preserve"> @@ -1079,6 +1516,11 @@ <target>Данные чата удалены</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve"> + <source>Chat database exported</source> + <target>Данные чата экспортированы</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> <target>Архив чата импортирован</target> @@ -1094,26 +1536,81 @@ <target>Чат остановлен</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <target>Чат остановлен. Если вы уже использовали эту базу данных на другом устройстве, перенесите ее обратно до запуска чата.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve"> + <source>Chat list</source> + <target>Список чатов</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve"> + <source>Chat migrated!</source> + <target>Чат мигрирован!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> <target>Предпочтения</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve"> + <source>Chat preferences were changed.</source> + <target>Настройки чата были изменены.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve"> + <source>Chat profile</source> + <target>Профиль чата</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve"> + <source>Chat theme</source> + <target>Тема чата</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <target>Разговор будет удален для всех участников - это действие нельзя отменить!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <target>Разговор будет удален для Вас - это действие нельзя отменить!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> <target>Чаты</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve"> + <source>Check messages every 20 min.</source> + <target>Проверять сообщения каждые 20 минут.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve"> + <source>Check messages when allowed.</source> + <target>Проверять сообщения по возможности.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> <target>Проверьте адрес сервера и попробуйте снова.</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> <target>Китайский и Испанский интерфейс</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <target>Выберите _Мигрировать с другого устройства_ на новом устройстве и сосканируйте QR код.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> <target>Выбрать файл</target> @@ -1124,10 +1621,25 @@ <target>Выбрать из библиотеки</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve"> + <source>Chunks deleted</source> + <target>Блоков удалено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve"> + <source>Chunks downloaded</source> + <target>Блоков принято</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve"> + <source>Chunks uploaded</source> + <target>Блоков загружено</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> <target>Очистить</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> @@ -1139,16 +1651,41 @@ <target>Очистить разговор?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve"> + <source>Clear group?</source> + <target>Очистить группу?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve"> + <source>Clear or delete group?</source> + <target>Очистить или удалить группу?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve"> + <source>Clear private notes?</source> + <target>Очистить личные заметки?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear verification" xml:space="preserve"> <source>Clear verification</source> <target>Сбросить подтверждение</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> - <source>Colors</source> - <target>Цвета</target> + <trans-unit id="Color chats with the new themes." xml:space="preserve"> + <source>Color chats with the new themes.</source> + <target>Добавьте цвета к чатам в настройках.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Color mode" xml:space="preserve"> + <source>Color mode</source> + <target>Режим цветов</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve"> + <source>Community guidelines violation</source> + <target>Нарушение правил группы</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Compare file" xml:space="preserve"> <source>Compare file</source> <target>Сравнение файла</target> @@ -1159,11 +1696,56 @@ <target>Сравните код безопасности с Вашими контактами.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Completed" xml:space="preserve"> + <source>Completed</source> + <target>Готово</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve"> + <source>Conditions accepted on: %@.</source> + <target>Условия приняты: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <target>Условия приняты для оператора(ов): **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <target>Условия уже приняты для следующих оператора(ов): **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve"> + <source>Conditions of use</source> + <target>Условия использования</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <target>Условия будут приняты для оператора(ов): **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve"> + <source>Conditions will be accepted on: %@.</source> + <target>Условия будут приняты: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <target>Условия будут автоматически приняты для включенных операторов: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> <target>Настройка ICE серверов</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Configure server operators" xml:space="preserve"> + <source>Configure server operators</source> + <target>Настроить операторов серверов</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> <target>Подтвердить</target> @@ -1174,11 +1756,26 @@ <target>Подтвердить Код</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve"> + <source>Confirm contact deletion?</source> + <target>Потвердить удаление контакта?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm database upgrades" xml:space="preserve"> <source>Confirm database upgrades</source> <target>Подтвердить обновление базы данных</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve"> + <source>Confirm files from unknown servers.</source> + <target>Подтверждать файлы с неизвестных серверов.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve"> + <source>Confirm network settings</source> + <target>Подтвердите настройки сети</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> <target>Подтвердите новый пароль…</target> @@ -1189,6 +1786,21 @@ <target>Подтвердить пароль</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <target>Подтвердите, что Вы помните пароль базы данных для ее миграции.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve"> + <source>Confirm upload</source> + <target>Подтвердить загрузку</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve"> + <source>Confirmed</source> + <target>Подтвержденный</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> <target>Соединиться</target> @@ -1209,6 +1821,11 @@ <target>Подключиться к компьютеру</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve"> + <source>Connect to your friends faster.</source> + <target>Соединяйтесь с друзьями быстрее.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connect to yourself?" xml:space="preserve"> <source>Connect to yourself?</source> <target>Соединиться с самим собой?</target> @@ -1238,11 +1855,6 @@ This is your own one-time link!</source> <target>Соединиться через ссылку</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve"> - <source>Connect via link / QR code</source> - <target>Соединиться через ссылку / QR код</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connect via one-time link" xml:space="preserve"> <source>Connect via one-time link</source> <target>Соединиться через одноразовую ссылку</target> @@ -1253,16 +1865,31 @@ This is your own one-time link!</source> <target>Соединиться с %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected" xml:space="preserve"> + <source>Connected</source> + <target>Соединено</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected desktop" xml:space="preserve"> <source>Connected desktop</source> <target>Подключенный компьютер</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve"> + <source>Connected servers</source> + <target>Подключенные серверы</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected to desktop" xml:space="preserve"> <source>Connected to desktop</source> <target>Компьютер подключен</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting" xml:space="preserve"> + <source>Connecting</source> + <target>Соединяется</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> <source>Connecting to server…</source> <target>Устанавливается соединение с сервером…</target> @@ -1273,6 +1900,11 @@ This is your own one-time link!</source> <target>Устанавливается соединение с сервером… (ошибка: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve"> + <source>Connecting to contact, please wait or check later!</source> + <target>Контакт соединяется, подождите или проверьте позже!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting to desktop" xml:space="preserve"> <source>Connecting to desktop</source> <target>Подключение к компьютеру</target> @@ -1283,6 +1915,16 @@ This is your own one-time link!</source> <target>Соединение</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve"> + <source>Connection and servers status.</source> + <target>Состояние соединения и серверов.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve"> + <source>Connection blocked</source> + <target>Соединение заблокировано</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> <target>Ошибка соединения</target> @@ -1293,11 +1935,38 @@ This is your own one-time link!</source> <target>Ошибка соединения (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve"> + <source>Connection is blocked by server operator: +%@</source> + <target>Соединение заблокировано сервером оператора: +%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve"> + <source>Connection not ready.</source> + <target>Соединение не готово.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve"> + <source>Connection notifications</source> + <target>Уведомления по соединениям</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Запрос на соединение отправлен!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve"> + <source>Connection requires encryption renegotiation.</source> + <target>Соединение требует повторного согласования шифрования.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve"> + <source>Connection security</source> + <target>Безопасность соединения</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection terminated" xml:space="preserve"> <source>Connection terminated</source> <target>Подключение прервано</target> @@ -1308,6 +1977,16 @@ This is your own one-time link!</source> <target>Превышено время соединения</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve"> + <source>Connection with desktop stopped</source> + <target>Соединение с компьютером остановлено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve"> + <source>Connections</source> + <target>Соединения</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact allows" xml:space="preserve"> <source>Contact allows</source> <target>Контакт разрешает</target> @@ -1318,6 +1997,11 @@ This is your own one-time link!</source> <target>Существующий контакт</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact deleted!" xml:space="preserve"> + <source>Contact deleted!</source> + <target>Контакт удален!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> <target>Контакт скрыт:</target> @@ -1328,9 +2012,9 @@ This is your own one-time link!</source> <target>Соединение с контактом установлено</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve"> - <source>Contact is not connected yet!</source> - <target>Соединение еще не установлено!</target> + <trans-unit id="Contact is deleted." xml:space="preserve"> + <source>Contact is deleted.</source> + <target>Контакт удален.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact name" xml:space="preserve"> @@ -1343,6 +2027,11 @@ This is your own one-time link!</source> <target>Предпочтения контакта</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve"> + <source>Contact will be deleted - this cannot be undone!</source> + <target>Контакт будет удален — это нельзя отменить!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> <target>Контакты</target> @@ -1353,21 +2042,41 @@ This is your own one-time link!</source> <target>Контакты могут помечать сообщения для удаления; Вы сможете просмотреть их.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve"> + <source>Content violates conditions of use</source> + <target>Содержание нарушает условия использования</target> + <note>blocking reason</note> + </trans-unit> <trans-unit id="Continue" xml:space="preserve"> <source>Continue</source> <target>Продолжить</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve"> + <source>Conversation deleted!</source> + <target>Разговор удален!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> <target>Скопировать</target> - <note>chat item action</note> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve"> + <source>Copy error</source> + <target>Ошибка копирования</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> <target>Версия ядра: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Corner" xml:space="preserve"> + <source>Corner</source> + <target>Угол</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Correct name to %@?" xml:space="preserve"> <source>Correct name to %@?</source> <target>Исправить имя на %@?</target> @@ -1378,6 +2087,11 @@ This is your own one-time link!</source> <target>Создать</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve"> + <source>Create 1-time link</source> + <target>Создать одноразовую ссылку</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> <target>Создать адрес SimpleX</target> @@ -1388,11 +2102,6 @@ This is your own one-time link!</source> <target>Создайте группу, используя случайный профиль.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> - <source>Create an address to let people connect with you.</source> - <target>Создайте адрес, чтобы можно было соединиться с вами.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create file" xml:space="preserve"> <source>Create file</source> <target>Создание файла</target> @@ -1413,16 +2122,16 @@ This is your own one-time link!</source> <target>Создать ссылку</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create list" xml:space="preserve"> + <source>Create list</source> + <target>Создать список</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> <target>Создайте новый профиль в [приложении для компьютера](https://simplex.chat/downloads/). 💻</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve"> - <source>Create one-time invitation link</source> - <target>Создать ссылку-приглашение</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create profile" xml:space="preserve"> <source>Create profile</source> <target>Создать профиль</target> @@ -1443,9 +2152,29 @@ This is your own one-time link!</source> <target>Создать профиль</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve"> - <source>Created on %@</source> - <target>Дата создания %@</target> + <trans-unit id="Created" xml:space="preserve"> + <source>Created</source> + <target>Создано</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve"> + <source>Created at</source> + <target>Создано</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve"> + <source>Created at: %@</source> + <target>Создано: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve"> + <source>Creating archive link</source> + <target>Создание ссылки на архив</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve"> + <source>Creating link…</source> + <target>Создаётся ссылка…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current Passcode" xml:space="preserve"> @@ -1453,11 +2182,21 @@ This is your own one-time link!</source> <target>Текущий Код</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <target>Текст условий использования не может быть показан, вы можете посмотреть их через ссылку:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> <target>Текущий пароль…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current profile" xml:space="preserve"> + <source>Current profile</source> + <target>Текущий профиль</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> <target>Максимальный размер файла - %@.</target> @@ -1468,11 +2207,26 @@ This is your own one-time link!</source> <target>Пользовательское время</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve"> + <source>Customizable message shape.</source> + <target>Настраиваемая форма сообщений.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve"> + <source>Customize theme</source> + <target>Настроить тему</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> <target>Тёмная</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve"> + <source>Dark mode colors</source> + <target>Цвета тёмного режима</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> <target>ID базы данных</target> @@ -1571,6 +2325,11 @@ This is your own one-time link!</source> <target>Данные чата будут мигрированы при перезапуске</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Debug delivery" xml:space="preserve"> + <source>Debug delivery</source> + <target>Отладка доставки</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> <target>Децентрализованный</target> @@ -1584,18 +2343,19 @@ This is your own one-time link!</source> <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> <target>Удалить</target> - <note>chat item action</note> + <note>alert action +swipe action</note> + </trans-unit> + <trans-unit id="Delete %lld messages of members?" xml:space="preserve"> + <source>Delete %lld messages of members?</source> + <target>Удалить %lld сообщений членов группы?</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete %lld messages?" xml:space="preserve"> <source>Delete %lld messages?</source> <target>Удалить %lld сообщений?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve"> - <source>Delete Contact</source> - <target>Удалить контакт</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Delete address" xml:space="preserve"> <source>Delete address</source> <target>Удалить адрес</target> @@ -1621,14 +2381,14 @@ This is your own one-time link!</source> <target>Удалить и уведомить контакт</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve"> - <source>Delete archive</source> - <target>Удалить архив</target> + <trans-unit id="Delete chat" xml:space="preserve"> + <source>Delete chat</source> + <target>Удалить разговор</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve"> - <source>Delete chat archive?</source> - <target>Удалить архив чата?</target> + <trans-unit id="Delete chat messages from your device." xml:space="preserve"> + <source>Delete chat messages from your device.</source> + <target>Удалить сообщения с вашего устройства.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile" xml:space="preserve"> @@ -1641,6 +2401,11 @@ This is your own one-time link!</source> <target>Удалить профиль?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete chat?" xml:space="preserve"> + <source>Delete chat?</source> + <target>Удалить разговор?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> <source>Delete connection</source> <target>Удалить соединение</target> @@ -1651,11 +2416,9 @@ This is your own one-time link!</source> <target>Удалить контакт</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact? This cannot be undone!" xml:space="preserve"> - <source>Delete contact? -This cannot be undone!</source> - <target>Удалить контакт? -Это не может быть отменено!</target> + <trans-unit id="Delete contact?" xml:space="preserve"> + <source>Delete contact?</source> + <target>Удалить контакт?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete database" xml:space="preserve"> @@ -1663,6 +2426,11 @@ This cannot be undone!</source> <target>Удалить данные чата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete database from this device" xml:space="preserve"> + <source>Delete database from this device</source> + <target>Удалить базу данных с этого устройства</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete file" xml:space="preserve"> <source>Delete file</source> <target>Удалить файл</target> @@ -1713,6 +2481,11 @@ This cannot be undone!</source> <target>Удалить ссылку?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete list?" xml:space="preserve"> + <source>Delete list?</source> + <target>Удалить список?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> <target>Удалить сообщение участника?</target> @@ -1726,7 +2499,7 @@ This cannot be undone!</source> <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> <target>Удалить сообщения</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> @@ -1743,9 +2516,9 @@ This cannot be undone!</source> <target>Удалить предыдущую версию данных?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve"> - <source>Delete pending connection</source> - <target>Удалить соединение</target> + <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> + <source>Delete or moderate up to 200 messages.</source> + <target>Удаляйте или модерируйте до 200 сообщений.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection?" xml:space="preserve"> @@ -1763,11 +2536,31 @@ This cannot be undone!</source> <target>Удаление очереди</target> <note>server test step</note> </trans-unit> + <trans-unit id="Delete report" xml:space="preserve"> + <source>Delete report</source> + <target>Удалить сообщение о нарушении</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete up to 20 messages at once." xml:space="preserve"> + <source>Delete up to 20 messages at once.</source> + <target>Удаляйте до 20 сообщений за раз.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> <target>Удалить профиль пользователя?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete without notification" xml:space="preserve"> + <source>Delete without notification</source> + <target>Удалить без уведомления</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve"> + <source>Deleted</source> + <target>Удалено</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Deleted at" xml:space="preserve"> <source>Deleted at</source> <target>Удалено</target> @@ -1778,6 +2571,16 @@ This cannot be undone!</source> <target>Удалено: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve"> + <source>Deletion errors</source> + <target>Ошибки удаления</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivered even when Apple drops them." xml:space="preserve"> + <source>Delivered even when Apple drops them.</source> + <target>Доставляются даже тогда, когда Apple их теряет.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery" xml:space="preserve"> <source>Delivery</source> <target>Доставка</target> @@ -1813,11 +2616,41 @@ This cannot be undone!</source> <target>Компьютеры</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve"> + <source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source> + <target>Адрес сервера назначения %@ несовместим с настройками пересылающего сервера %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server error: %@" xml:space="preserve"> + <source>Destination server error: %@</source> + <target>Ошибка сервера получателя: %@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve"> + <source>Destination server version of %@ is incompatible with forwarding server %@.</source> + <target>Версия сервера назначения %@ несовместима с пересылающим сервером %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Detailed statistics" xml:space="preserve"> + <source>Detailed statistics</source> + <target>Подробная статистика</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Details" xml:space="preserve"> + <source>Details</source> + <target>Подробности</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Develop" xml:space="preserve"> <source>Develop</source> <target>Для разработчиков</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Developer options" xml:space="preserve"> + <source>Developer options</source> + <target>Опции разработчика</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> <target>Инструменты разработчика</target> @@ -1848,9 +2681,13 @@ This cannot be undone!</source> <target>Прямые сообщения</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> - <target>Прямые сообщения между членами группы запрещены.</target> + <trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve"> + <source>Direct messages between members are prohibited in this chat.</source> + <target>Личные сообщения запрещены в этой группе.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disable (keep overrides)" xml:space="preserve"> @@ -1863,11 +2700,26 @@ This cannot be undone!</source> <target>Отключить блокировку SimpleX</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Disable automatic message deletion?" xml:space="preserve"> + <source>Disable automatic message deletion?</source> + <target>Отключить автоматическое удаление сообщений?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Disable delete messages" xml:space="preserve"> + <source>Disable delete messages</source> + <target>Отключить удаление сообщений</target> + <note>alert button</note> + </trans-unit> <trans-unit id="Disable for all" xml:space="preserve"> <source>Disable for all</source> <target>Выключить для всех</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Disabled" xml:space="preserve"> + <source>Disabled</source> + <target>Выключено</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> <source>Disappearing message</source> <target>Исчезающее сообщение</target> @@ -1883,8 +2735,8 @@ This cannot be undone!</source> <target>Исчезающие сообщения запрещены в этом чате.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <target>Исчезающие сообщения запрещены в этой группе.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1918,16 +2770,40 @@ This cannot be undone!</source> <target>Обнаружение по локальной сети</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve"> + <source>Do NOT send messages directly, even if your or destination server does not support private routing.</source> + <target>Не отправлять сообщения напрямую, даже если сервер получателя не поддерживает конфиденциальную доставку.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> <source>Do NOT use SimpleX for emergency calls.</source> <target>Не используйте SimpleX для экстренных звонков.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT use private routing." xml:space="preserve"> + <source>Do NOT use private routing.</source> + <target>Не использовать конфиденциальную доставку.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do it later" xml:space="preserve"> <source>Do it later</source> <target>Отложить</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do not send history to new members." xml:space="preserve"> + <source>Do not send history to new members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not use credentials with proxy." xml:space="preserve"> + <source>Do not use credentials with proxy.</source> + <target>Не использовать учетные данные с прокси.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Documents:" xml:space="preserve"> + <source>Documents:</source> + <target>Документы:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> <target>Не создавать адрес</target> @@ -1938,21 +2814,72 @@ This cannot be undone!</source> <target>Не включать</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Don't miss important messages." xml:space="preserve"> + <source>Don't miss important messages.</source> + <target>Не пропустите важные сообщения.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> <target>Не показывать</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Done" xml:space="preserve"> + <source>Done</source> + <target>Готово</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Downgrade and open chat" xml:space="preserve"> <source>Downgrade and open chat</source> <target>Откатить версию и открыть чат</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Download" xml:space="preserve"> + <source>Download</source> + <target>Загрузить</target> + <note>alert button +chat item action</note> + </trans-unit> + <trans-unit id="Download errors" xml:space="preserve"> + <source>Download errors</source> + <target>Ошибки приема</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download failed" xml:space="preserve"> + <source>Download failed</source> + <target>Ошибка загрузки</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Download file" xml:space="preserve"> <source>Download file</source> <target>Загрузка файла</target> <note>server test step</note> </trans-unit> + <trans-unit id="Download files" xml:space="preserve"> + <source>Download files</source> + <target>Загрузить файлы</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Downloaded" xml:space="preserve"> + <source>Downloaded</source> + <target>Принято</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloaded files" xml:space="preserve"> + <source>Downloaded files</source> + <target>Принятые файлы</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading archive" xml:space="preserve"> + <source>Downloading archive</source> + <target>Загрузка архива</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading link details" xml:space="preserve"> + <source>Downloading link details</source> + <target>Загрузка ссылки архива</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> <target>Имя профиля уже используется!</target> @@ -1963,6 +2890,11 @@ This cannot be undone!</source> <target>Длительность</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="E2E encrypted notifications." xml:space="preserve"> + <source>E2E encrypted notifications.</source> + <target>E2E зашифрованные нотификации.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> <target>Редактировать</target> @@ -1983,6 +2915,10 @@ This cannot be undone!</source> <target>Включить (кроме исключений)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve"> + <source>Enable Flux in Network & servers settings for better metadata privacy.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> <target>Включить блокировку SimpleX</target> @@ -1996,6 +2932,11 @@ This cannot be undone!</source> <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> <target>Включить автоматическое удаление сообщений?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Enable camera access" xml:space="preserve"> + <source>Enable camera access</source> + <target>Включить доступ к камере</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable for all" xml:space="preserve"> @@ -2003,6 +2944,11 @@ This cannot be undone!</source> <target>Включить для всех</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve"> + <source>Enable in direct chats (BETA)!</source> + <target>Включите для контактов (BETA)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> <target>Включить мгновенные уведомления?</target> @@ -2033,6 +2979,16 @@ This cannot be undone!</source> <target>Включить код самоуничтожения</target> <note>set passcode view</note> </trans-unit> + <trans-unit id="Enabled" xml:space="preserve"> + <source>Enabled</source> + <target>Включено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve"> + <source>Enabled for</source> + <target>Включено для</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> <target>Зашифровать</target> @@ -2063,6 +3019,11 @@ This cannot be undone!</source> <target>Зашифрованное сообщение или событие чата</target> <note>notification</note> </trans-unit> + <trans-unit id="Encrypted message: app is stopped" xml:space="preserve"> + <source>Encrypted message: app is stopped</source> + <target>Зашифрованное сообщение: приложение остановлено</target> + <note>notification</note> + </trans-unit> <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> <target>Зашифрованное сообщение: ошибка базы данных</target> @@ -2098,6 +3059,11 @@ This cannot be undone!</source> <target>Ошибка нового соглашения о шифровании.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encryption renegotiation in progress." xml:space="preserve"> + <source>Encryption renegotiation in progress.</source> + <target>Выполняется повторное согласование шифрования.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter Passcode" xml:space="preserve"> <source>Enter Passcode</source> <target>Введите Код</target> @@ -2113,6 +3079,11 @@ This cannot be undone!</source> <target>Введите имя группы…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve"> + <source>Enter passphrase</source> + <target>Введите пароль</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter passphrase…" xml:space="preserve"> <source>Enter passphrase…</source> <target>Введите пароль…</target> @@ -2158,26 +3129,35 @@ This cannot be undone!</source> <target>Ошибка при прекращении изменения адреса</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error accepting conditions" xml:space="preserve"> + <source>Error accepting conditions</source> + <target>Ошибка приема условий</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> <target>Ошибка при принятии запроса на соединение</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve"> - <source>Error accessing database file</source> - <target>Ошибка при доступе к данным чата</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> - <target>Ошибка при добавлении членов группы</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error adding server" xml:space="preserve"> + <source>Error adding server</source> + <target>Ошибка добавления сервера</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> <target>Ошибка при изменении адреса</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing connection profile" xml:space="preserve"> + <source>Error changing connection profile</source> + <target>Ошибка при изменении профиля соединения</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error changing role" xml:space="preserve"> <source>Error changing role</source> <target>Ошибка при изменении роли</target> @@ -2188,6 +3168,21 @@ This cannot be undone!</source> <target>Ошибка при изменении настройки</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing to incognito!" xml:space="preserve"> + <source>Error changing to incognito!</source> + <target>Ошибка при смене на Инкогнито!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error checking token status" xml:space="preserve"> + <source>Error checking token status</source> + <target>Ошибка проверки статуса токена</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve"> + <source>Error connecting to forwarding server %@. Please try later.</source> + <target>Ошибка подключения к пересылающему серверу %@. Попробуйте позже.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> <target>Ошибка при создании адреса</target> @@ -2203,9 +3198,18 @@ This cannot be undone!</source> <target>Ошибка при создании ссылки группы</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating list" xml:space="preserve"> + <source>Error creating list</source> + <target>Ошибка создания списка</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error creating member contact" xml:space="preserve"> <source>Error creating member contact</source> - <target>Ошибка создания контакта с членом группы</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating message" xml:space="preserve"> + <source>Error creating message</source> + <target>Ошибка создания сообщения</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> @@ -2213,6 +3217,11 @@ This cannot be undone!</source> <target>Ошибка создания профиля!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating report" xml:space="preserve"> + <source>Error creating report</source> + <target>Ошибка создания сообщения о нарушении</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error decrypting file" xml:space="preserve"> <source>Error decrypting file</source> <target>Ошибка расшифровки файла</target> @@ -2233,11 +3242,6 @@ This cannot be undone!</source> <target>Ошибка при удалении соединения</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve"> - <source>Error deleting contact</source> - <target>Ошибка при удалении контакта</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> <target>Ошибка при удалении данных чата</target> @@ -2258,6 +3262,11 @@ This cannot be undone!</source> <target>Ошибка удаления профиля пользователя</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error downloading the archive" xml:space="preserve"> + <source>Error downloading the archive</source> + <target>Ошибка загрузки архива</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> <source>Error enabling delivery receipts!</source> <target>Ошибка при включении отчётов о доставке!</target> @@ -2278,6 +3287,11 @@ This cannot be undone!</source> <target>Ошибка при экспорте архива чата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error exporting theme: %@" xml:space="preserve"> + <source>Error exporting theme: %@</source> + <target>Ошибка экспорта темы: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> <target>Ошибка при импорте архива чата</target> @@ -2288,24 +3302,53 @@ This cannot be undone!</source> <target>Ошибка при вступлении в группу</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve"> - <source>Error loading %@ servers</source> - <target>Ошибка загрузки %@ серверов</target> + <trans-unit id="Error loading servers" xml:space="preserve"> + <source>Error loading servers</source> + <target>Ошибка загрузки серверов</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error migrating settings" xml:space="preserve"> + <source>Error migrating settings</source> + <target>Ошибка миграции настроек</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error opening chat" xml:space="preserve"> + <source>Error opening chat</source> + <target>Ошибка доступа к чату</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> <target>Ошибка при получении файла</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error reconnecting server" xml:space="preserve"> + <source>Error reconnecting server</source> + <target>Ошибка переподключения к серверу</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error reconnecting servers" xml:space="preserve"> + <source>Error reconnecting servers</source> + <target>Ошибка переподключения к серверам</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error registering for notifications" xml:space="preserve"> + <source>Error registering for notifications</source> + <target>Ошибка регистрации для уведомлений</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> - <target>Ошибка при удалении члена группы</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve"> - <source>Error saving %@ servers</source> - <target>Ошибка при сохранении %@ серверов</target> + <trans-unit id="Error reordering lists" xml:space="preserve"> + <source>Error reordering lists</source> + <target>Ошибка сортировки списков</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error resetting statistics" xml:space="preserve"> + <source>Error resetting statistics</source> + <target>Ошибка сброса статистики</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving ICE servers" xml:space="preserve"> @@ -2313,6 +3356,11 @@ This cannot be undone!</source> <target>Ошибка при сохранении ICE серверов</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving chat list" xml:space="preserve"> + <source>Error saving chat list</source> + <target>Ошибка сохранения списка чатов</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> <target>Ошибка при сохранении профиля группы</target> @@ -2328,11 +3376,26 @@ This cannot be undone!</source> <target>Ошибка сохранения пароля в Keychain</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving servers" xml:space="preserve"> + <source>Error saving servers</source> + <target>Ошибка сохранения серверов</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error saving settings" xml:space="preserve"> + <source>Error saving settings</source> + <target>Ошибка сохранения настроек</target> + <note>when migrating</note> + </trans-unit> <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> <target>Ошибка при сохранении пароля пользователя</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error scanning code: %@" xml:space="preserve"> + <source>Error scanning code: %@</source> + <target>Ошибка сканирования кода: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending email" xml:space="preserve"> <source>Error sending email</source> <target>Ошибка отправки email</target> @@ -2340,7 +3403,6 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Error sending member contact invitation" xml:space="preserve"> <source>Error sending member contact invitation</source> - <target>Ошибка отправки приглашения члену группы</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error sending message" xml:space="preserve"> @@ -2363,16 +3425,26 @@ This cannot be undone!</source> <target>Ошибка при остановке чата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error switching profile" xml:space="preserve"> + <source>Error switching profile</source> + <target>Ошибка переключения профиля</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> <target>Ошибка выбора профиля!</target> - <note>No comment provided by engineer.</note> + <note>alertTitle</note> </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> <target>Ошибка синхронизации соединения</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error testing server connection" xml:space="preserve"> + <source>Error testing server connection</source> + <target>Ошибка проверки соединения с сервером</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> <target>Ошибка обновления ссылки группы</target> @@ -2383,6 +3455,11 @@ This cannot be undone!</source> <target>Ошибка при обновлении сообщения</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error updating server" xml:space="preserve"> + <source>Error updating server</source> + <target>Ошибка сохранения сервера</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> <target>Ошибка при сохранении настроек сети</target> @@ -2393,6 +3470,16 @@ This cannot be undone!</source> <target>Ошибка при обновлении конфиденциальности</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error uploading the archive" xml:space="preserve"> + <source>Error uploading the archive</source> + <target>Ошибка загрузки архива</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error verifying passphrase:" xml:space="preserve"> + <source>Error verifying passphrase:</source> + <target>Ошибка подтверждения пароля:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error: " xml:space="preserve"> <source>Error: </source> <target>Ошибка: </target> @@ -2401,7 +3488,9 @@ This cannot be undone!</source> <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> <target>Ошибка: %@</target> - <note>No comment provided by engineer.</note> + <note>alert message +file error text +snd error text</note> </trans-unit> <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> @@ -2413,6 +3502,16 @@ This cannot be undone!</source> <target>Ошибка: данные чата не найдены</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Errors" xml:space="preserve"> + <source>Errors</source> + <target>Ошибки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors in servers configuration." xml:space="preserve"> + <source>Errors in servers configuration.</source> + <target>Ошибки в настройках серверов.</target> + <note>servers error</note> + </trans-unit> <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> <source>Even when disabled in the conversation.</source> <target>Даже когда они выключены в разговоре.</target> @@ -2428,6 +3527,11 @@ This cannot be undone!</source> <target>Раскрыть</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Expired" xml:space="preserve"> + <source>Expired</source> + <target>Истекший</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> <target>Экспорт архива чата</target> @@ -2438,11 +3542,21 @@ This cannot be undone!</source> <target>Ошибка при экспорте:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Export theme" xml:space="preserve"> + <source>Export theme</source> + <target>Экспорт темы</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> <target>Архив чата экспортирован.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Exported file doesn't exist" xml:space="preserve"> + <source>Exported file doesn't exist</source> + <target>Экспортированный файл не существует</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exporting database archive…" xml:space="preserve"> <source>Exporting database archive…</source> <target>Архив чата экспортируется…</target> @@ -2458,16 +3572,70 @@ This cannot be undone!</source> <target>Быстрые и не нужно ждать, когда отправитель онлайн!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster deletion of groups." xml:space="preserve"> + <source>Faster deletion of groups.</source> + <target>Ускорено удаление групп.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Faster joining and more reliable messages." xml:space="preserve"> <source>Faster joining and more reliable messages.</source> <target>Быстрое вступление и надежная доставка сообщений.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster sending messages." xml:space="preserve"> + <source>Faster sending messages.</source> + <target>Ускорена отправка сообщений.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Favorite" xml:space="preserve"> <source>Favorite</source> <target>Избранный</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Favorites" xml:space="preserve"> + <source>Favorites</source> + <target>Избранное</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Ошибка файла</target> + <note>file error alert title</note> + </trans-unit> + <trans-unit id="File errors: %@" xml:space="preserve"> + <source>File errors: +%@</source> + <target>Ошибки файлов: +%@</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="File is blocked by server operator: %@." xml:space="preserve"> + <source>File is blocked by server operator: +%@.</source> + <target>Файл заблокирован оператором сервера: +%@.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve"> + <source>File not found - most likely file was deleted or cancelled.</source> + <target>Файл не найден - скорее всего, файл был удален или отменен.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File server error: %@" xml:space="preserve"> + <source>File server error: %@</source> + <target>Ошибка сервера файлов: %@</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File status" xml:space="preserve"> + <source>File status</source> + <target>Статус файла</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File status: %@" xml:space="preserve"> + <source>File status: %@</source> + <target>Статус файла: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="File will be deleted from servers." xml:space="preserve"> <source>File will be deleted from servers.</source> <target>Файл будет удалён с серверов.</target> @@ -2488,6 +3656,11 @@ This cannot be undone!</source> <target>Файл: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files" xml:space="preserve"> + <source>Files</source> + <target>Файлы</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> <target>Файлы и медиа</target> @@ -2498,11 +3671,16 @@ This cannot be undone!</source> <target>Файлы и медиа</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> - <source>Files and media are prohibited in this group.</source> + <trans-unit id="Files and media are prohibited." xml:space="preserve"> + <source>Files and media are prohibited.</source> <target>Файлы и медиа запрещены в этой группе.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files and media not allowed" xml:space="preserve"> + <source>Files and media not allowed</source> + <target>Файлы и медиа не разрешены</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files and media prohibited!" xml:space="preserve"> <source>Files and media prohibited!</source> <target>Файлы и медиа запрещены!</target> @@ -2513,6 +3691,16 @@ This cannot be undone!</source> <target>Фильтровать непрочитанные и избранные чаты.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Finalize migration" xml:space="preserve"> + <source>Finalize migration</source> + <target>Завершить миграцию</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration on another device." xml:space="preserve"> + <source>Finalize migration on another device.</source> + <target>Завершите миграцию на другом устройстве.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> <target>Наконец-то, мы их добавили! 🚀</target> @@ -2550,14 +3738,117 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Fix not supported by group member" xml:space="preserve"> <source>Fix not supported by group member</source> - <target>Починка не поддерживается членом группы</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For all moderators" xml:space="preserve"> + <source>For all moderators</source> + <target>Для всех модераторов</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For chat profile %@:" xml:space="preserve"> + <source>For chat profile %@:</source> + <target>Для профиля чата %@:</target> + <note>servers error</note> + </trans-unit> <trans-unit id="For console" xml:space="preserve"> <source>For console</source> <target>Для консоли</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." xml:space="preserve"> + <source>For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</source> + <target>Например, если Ваш контакт получает сообщения через сервер SimpleX Chat, Ваше приложение доставит их через сервер Flux.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For me" xml:space="preserve"> + <source>For me</source> + <target>Для меня</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For private routing" xml:space="preserve"> + <source>For private routing</source> + <target>Для доставки сообщений</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For social media" xml:space="preserve"> + <source>For social media</source> + <target>Для социальных сетей</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward" xml:space="preserve"> + <source>Forward</source> + <target>Переслать</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Forward %d message(s)?" xml:space="preserve"> + <source>Forward %d message(s)?</source> + <target>Переслать %d сообщение(й)?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Forward and save messages" xml:space="preserve"> + <source>Forward and save messages</source> + <target>Переслать и сохранить сообщение</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward messages" xml:space="preserve"> + <source>Forward messages</source> + <target>Переслать сообщения</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Forward messages without files?" xml:space="preserve"> + <source>Forward messages without files?</source> + <target>Переслать сообщения без файлов?</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> + <source>Forward up to 20 messages at once.</source> + <target>Пересылайте до 20 сообщений за раз.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded" xml:space="preserve"> + <source>Forwarded</source> + <target>Переслано</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded from" xml:space="preserve"> + <source>Forwarded from</source> + <target>Переслано из</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding %lld messages" xml:space="preserve"> + <source>Forwarding %lld messages</source> + <target>Пересылка %lld сообщений</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve"> + <source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source> + <target>Пересылающий сервер %@ не смог подключиться к серверу назначения %@. Попробуйте позже.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server address is incompatible with network settings: %@.</source> + <target>Адрес пересылающего сервера несовместим с настройками сети: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server version is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server version is incompatible with network settings: %@.</source> + <target>Версия пересылающего сервера несовместима с настройками сети: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Destination server error: %2$@</source> + <target>Пересылающий сервер: %1$@ +Ошибка сервера получателя: %2$@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Error: %2$@</source> + <target>Пересылающий сервер: %1$@ +Ошибка: %2$@</target> + <note>snd error text</note> + </trans-unit> <trans-unit id="Found desktop" xml:space="preserve"> <source>Found desktop</source> <target>Компьютер найден</target> @@ -2578,14 +3869,8 @@ This cannot be undone!</source> <target>Полное имя (не обязательно)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full name:" xml:space="preserve"> - <source>Full name:</source> - <target>Полное имя:</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Fully decentralized – visible only to members." xml:space="preserve"> <source>Fully decentralized – visible only to members.</source> - <target>Группа полностью децентрализована – она видна только членам.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fully re-implemented - work in background!" xml:space="preserve"> @@ -2603,6 +3888,21 @@ This cannot be undone!</source> <target>ГИФ файлы и стикеры</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Get notified when mentioned." xml:space="preserve"> + <source>Get notified when mentioned.</source> + <target>Уведомления, когда Вас упомянули.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Good afternoon!" xml:space="preserve"> + <source>Good afternoon!</source> + <target>Добрый день!</target> + <note>message preview</note> + </trans-unit> + <trans-unit id="Good morning!" xml:space="preserve"> + <source>Good morning!</source> + <target>Доброе утро!</target> + <note>message preview</note> + </trans-unit> <trans-unit id="Group" xml:space="preserve"> <source>Group</source> <target>Группа</target> @@ -2630,7 +3930,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Group image" xml:space="preserve"> <source>Group image</source> - <target>Аватар группы</target> + <target>Картинка группы</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group invitation" xml:space="preserve"> @@ -2658,36 +3958,6 @@ This cannot be undone!</source> <target>Ссылки групп</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve"> - <source>Group members can add message reactions.</source> - <target>Члены группы могут добавлять реакции на сообщения.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> - <source>Group members can irreversibly delete sent messages.</source> - <target>Члены группы могут необратимо удалять отправленные сообщения.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> - <target>Члены группы могут посылать прямые сообщения.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> - <target>Члены группы могут посылать исчезающие сообщения.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send files and media." xml:space="preserve"> - <source>Group members can send files and media.</source> - <target>Члены группы могут слать файлы и медиа.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> - <target>Члены группы могут отправлять голосовые сообщения.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> <target>Групповое сообщение:</target> @@ -2710,7 +3980,6 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Group profile is stored on members' devices, not on the servers." xml:space="preserve"> <source>Group profile is stored on members' devices, not on the servers.</source> - <target>Профиль группы хранится на устройствах членов, а не на серверах.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group welcome message" xml:space="preserve"> @@ -2720,7 +3989,6 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Group will be deleted for all members - this cannot be undone!" xml:space="preserve"> <source>Group will be deleted for all members - this cannot be undone!</source> - <target>Группа будет удалена для всех членов - это действие нельзя отменить!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group will be deleted for you - this cannot be undone!" xml:space="preserve"> @@ -2728,11 +3996,21 @@ This cannot be undone!</source> <target>Группа будет удалена для Вас - это действие нельзя отменить!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Groups" xml:space="preserve"> + <source>Groups</source> + <target>Группы</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Help" xml:space="preserve"> <source>Help</source> <target>Помощь</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Help admins moderating their groups." xml:space="preserve"> + <source>Help admins moderating their groups.</source> + <target>Помогайте администраторам модерировать их группы.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> <target>Скрытое</target> @@ -2773,15 +4051,29 @@ This cannot be undone!</source> <target>История</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="History is not sent to new members." xml:space="preserve"> + <source>History is not sent to new members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> <target>Как SimpleX работает</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="How it affects privacy" xml:space="preserve"> + <source>How it affects privacy</source> + <target>Как это влияет на конфиденциальность</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it helps privacy" xml:space="preserve"> + <source>How it helps privacy</source> + <target>Как это улучшает конфиденциальность</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> <target>Как это работает</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="How to" xml:space="preserve"> <source>How to</source> @@ -2790,7 +4082,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="How to use it" xml:space="preserve"> <source>How to use it</source> - <target>Как использовать</target> + <target>Про адрес</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How to use your servers" xml:space="preserve"> @@ -2798,21 +4090,26 @@ This cannot be undone!</source> <target>Как использовать серверы</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Hungarian interface" xml:space="preserve"> + <source>Hungarian interface</source> + <target>Венгерский интерфейс</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> <target>ICE серверы (один на строке)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="IP address" xml:space="preserve"> + <source>IP address</source> + <target>IP адрес</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> <source>If you can't meet in person, show QR code in a video call, or share the link.</source> <target>Если Вы не можете встретиться лично, покажите QR-код во время видеозвонка или поделитесь ссылкой.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <target>Если Вы не можете встретиться лично, Вы можете **сосканировать QR код во время видеозвонка**, или Ваш контакт может отправить Вам ссылку.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> <target>Если Вы введете этот код при открытии приложения, все данные приложения будут безвозвратно удалены!</target> @@ -2848,8 +4145,8 @@ This cannot be undone!</source> <target>Сразу</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <target>Защищен от спама</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2868,6 +4165,32 @@ This cannot be undone!</source> <target>Импорт архива чата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Import failed" xml:space="preserve"> + <source>Import failed</source> + <target>Ошибка импорта</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import theme" xml:space="preserve"> + <source>Import theme</source> + <target>Импорт темы</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Importing archive" xml:space="preserve"> + <source>Importing archive</source> + <target>Импорт архива</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved delivery, reduced traffic usage. More improvements are coming soon!" xml:space="preserve"> + <source>Improved delivery, reduced traffic usage. +More improvements are coming soon!</source> + <target>Улучшенная доставка, меньше трафик.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved message delivery" xml:space="preserve"> + <source>Improved message delivery</source> + <target>Улучшенная доставка сообщений</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> <target>Улучшенная безопасность</target> @@ -2878,11 +4201,31 @@ This cannot be undone!</source> <target>Улучшенная конфигурация серверов</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In order to continue, chat should be stopped." xml:space="preserve"> + <source>In order to continue, chat should be stopped.</source> + <target>Чтобы продолжить, чат должен быть остановлен.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>В ответ на</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In-call sounds" xml:space="preserve"> + <source>In-call sounds</source> + <target>Звуки во время звонков</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Inappropriate content" xml:space="preserve"> + <source>Inappropriate content</source> + <target>Неприемлемый контент</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Inappropriate profile" xml:space="preserve"> + <source>Inappropriate profile</source> + <target>Неприемлемый профиль</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> <target>Инкогнито</target> @@ -2953,6 +4296,11 @@ This cannot be undone!</source> <target>[SimpleX Chat для терминала](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Instant" xml:space="preserve"> + <source>Instant</source> + <target>Мгновенно</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> @@ -2960,30 +4308,80 @@ This cannot be undone!</source> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Instantly" xml:space="preserve"> - <source>Instantly</source> - <target>Мгновенно</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> <target>Интерфейс</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Interface colors" xml:space="preserve"> + <source>Interface colors</source> + <target>Цвета интерфейса</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid" xml:space="preserve"> + <source>Invalid</source> + <target>Недействительный</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (bad token)" xml:space="preserve"> + <source>Invalid (bad token)</source> + <target>Недействительный (плохой токен)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (expired)" xml:space="preserve"> + <source>Invalid (expired)</source> + <target>Недействительный (истекший)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (unregistered)" xml:space="preserve"> + <source>Invalid (unregistered)</source> + <target>Недействительный (незарегистрированный)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (wrong topic)" xml:space="preserve"> + <source>Invalid (wrong topic)</source> + <target>Недействительный (плохой заголовок)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid QR code" xml:space="preserve"> + <source>Invalid QR code</source> + <target>Неверный QR код</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> <target>Ошибка в ссылке контакта</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid display name!" xml:space="preserve"> + <source>Invalid display name!</source> + <target>Ошибка имени!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid link" xml:space="preserve"> + <source>Invalid link</source> + <target>Ошибка ссылки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Ошибка подтверждения миграции</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid name!" xml:space="preserve"> <source>Invalid name!</source> <target>Неверное имя!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid response" xml:space="preserve"> + <source>Invalid response</source> + <target>Ошибка ответа</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> <target>Ошибка в адресе сервера!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Invalid status" xml:space="preserve"> <source>Invalid status</source> @@ -3002,7 +4400,11 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Invite members" xml:space="preserve"> <source>Invite members</source> - <target>Пригласить членов группы</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invite to chat" xml:space="preserve"> + <source>Invite to chat</source> + <target>Пригласить в разговор</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invite to group" xml:space="preserve"> @@ -3020,8 +4422,8 @@ This cannot be undone!</source> <target>Необратимое удаление сообщений запрещено в этом чате.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <target>Необратимое удаление сообщений запрещено в этой группе.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3046,6 +4448,11 @@ This cannot be undone!</source> 3. Соединение компроментировано.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="It protects your IP address and connections." xml:space="preserve"> + <source>It protects your IP address and connections.</source> + <target>Защищает ваш IP адрес и соединения.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> <target>Возможно, Вы уже соединились через эту ссылку. Если это не так, то это ошибка (%@).</target> @@ -3064,13 +4471,18 @@ This cannot be undone!</source> <trans-unit id="Join" xml:space="preserve"> <source>Join</source> <target>Вступить</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> <target>Вступить в группу</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Join group conversations" xml:space="preserve"> + <source>Join group conversations</source> + <target>Присоединяйтесь к разговорам в группах</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Join group?" xml:space="preserve"> <source>Join group?</source> <target>Вступить в группу?</target> @@ -3098,11 +4510,26 @@ This is your link for group %@!</source> <target>Вступление в группу</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep" xml:space="preserve"> + <source>Keep</source> + <target>Оставить</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Keep conversation" xml:space="preserve"> + <source>Keep conversation</source> + <target>Оставить разговор</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Keep the app open to use it from desktop" xml:space="preserve"> <source>Keep the app open to use it from desktop</source> <target>Оставьте приложение открытым, чтобы использовать его с компьютера</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep unused invitation?" xml:space="preserve"> + <source>Keep unused invitation?</source> + <target>Оставить неиспользованное приглашение?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> <source>Keep your connections</source> <target>Сохраните Ваши соединения</target> @@ -3136,6 +4563,16 @@ This is your link for group %@!</source> <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> <target>Выйти</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Leave chat" xml:space="preserve"> + <source>Leave chat</source> + <target>Покинуть разговор</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave chat?" xml:space="preserve"> + <source>Leave chat?</source> + <target>Покинуть разговор?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group" xml:space="preserve"> @@ -3178,6 +4615,21 @@ This is your link for group %@!</source> <target>Связанные компьютеры</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="List" xml:space="preserve"> + <source>List</source> + <target>Список</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="List name and emoji should be different for all lists." xml:space="preserve"> + <source>List name and emoji should be different for all lists.</source> + <target>Название списка и эмодзи должны быть разными для всех списков.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List name..." xml:space="preserve"> + <source>List name...</source> + <target>Имя списка...</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Live message!" xml:space="preserve"> <source>Live message!</source> <target>Живое сообщение!</target> @@ -3208,11 +4660,6 @@ This is your link for group %@!</source> <target>Режим блокировки</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> - <source>Make a private connection</source> - <target>Добавьте контакт</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make one message disappear" xml:space="preserve"> <source>Make one message disappear</source> <target>Одно исчезающее сообщение</target> @@ -3223,21 +4670,11 @@ This is your link for group %@!</source> <target>Сделайте профиль скрытым!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> - <target>Пожалуйста, проверьте, что адреса %@ серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется (%@).</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> <target>Пожалуйста, проверьте, что адреса WebRTC ICE серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <target>Много пользователей спросили: *как SimpleX доставляет сообщения без идентификаторов пользователей?*</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> <source>Mark deleted for everyone</source> <target>Пометить как удаленное для всех</target> @@ -3263,24 +4700,89 @@ This is your link for group %@!</source> <target>Макс. 30 секунд, доставляются мгновенно.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Media & file servers" xml:space="preserve"> + <source>Media & file servers</source> + <target>Серверы файлов и медиа</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Medium" xml:space="preserve"> + <source>Medium</source> + <target>Среднее</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Member" xml:space="preserve"> <source>Member</source> - <target>Член группы</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Member inactive" xml:space="preserve"> + <source>Member inactive</source> + <note>item status text</note> + </trans-unit> + <trans-unit id="Member reports" xml:space="preserve"> + <source>Member reports</source> + <target>Сообщения о нарушениях</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All chat members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All chat members will be notified.</source> + <target>Роль участника будет изменена на "%@". Все участники разговора получат уведомление.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> - <target>Роль члена группы будет изменена на "%@". Все члены группы получат сообщение.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Member role will be changed to "%@". The member will receive a new invitation." xml:space="preserve"> <source>Member role will be changed to "%@". The member will receive a new invitation.</source> - <target>Роль члена группы будет изменена на "%@". Будет отправлено новое приглашение.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from chat - this cannot be undone!</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> - <target>Член группы будет удален - это действие нельзя отменить!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can add message reactions." xml:space="preserve"> + <source>Members can add message reactions.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Members can irreversibly delete sent messages. (24 hours)</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can report messsages to moderators." xml:space="preserve"> + <source>Members can report messsages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send SimpleX links." xml:space="preserve"> + <source>Members can send SimpleX links.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send files and media." xml:space="preserve"> + <source>Members can send files and media.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mention members 👋" xml:space="preserve"> + <source>Mention members 👋</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Menus" xml:space="preserve"> + <source>Menus</source> + <target>Меню</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Message delivery error" xml:space="preserve"> @@ -3293,11 +4795,30 @@ This is your link for group %@!</source> <target>Отчеты о доставке сообщений!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message delivery warning" xml:space="preserve"> + <source>Message delivery warning</source> + <target>Предупреждение доставки сообщения</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> <target>Черновик сообщения</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message forwarded" xml:space="preserve"> + <source>Message forwarded</source> + <target>Сообщение переслано</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message may be delivered later if member becomes active." xml:space="preserve"> + <source>Message may be delivered later if member becomes active.</source> + <note>item status description</note> + </trans-unit> + <trans-unit id="Message queue info" xml:space="preserve"> + <source>Message queue info</source> + <target>Информация об очереди сообщений</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message reactions" xml:space="preserve"> <source>Message reactions</source> <target>Реакции на сообщения</target> @@ -3308,16 +4829,51 @@ This is your link for group %@!</source> <target>Реакции на сообщения в этом чате запрещены.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> - <source>Message reactions are prohibited in this group.</source> + <trans-unit id="Message reactions are prohibited." xml:space="preserve"> + <source>Message reactions are prohibited.</source> <target>Реакции на сообщения запрещены в этой группе.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message reception" xml:space="preserve"> + <source>Message reception</source> + <target>Прием сообщений</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message servers" xml:space="preserve"> + <source>Message servers</source> + <target>Серверы сообщений</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message shape" xml:space="preserve"> + <source>Message shape</source> + <target>Форма сообщений</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message source remains private." xml:space="preserve"> + <source>Message source remains private.</source> + <target>Источник сообщения остаётся конфиденциальным.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status" xml:space="preserve"> + <source>Message status</source> + <target>Статус сообщения</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status: %@" xml:space="preserve"> + <source>Message status: %@</source> + <target>Статус сообщения: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> <target>Текст сообщения</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message too large" xml:space="preserve"> + <source>Message too large</source> + <target>Сообщение слишком большое</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> <target>Сообщения</target> @@ -3333,11 +4889,76 @@ This is your link for group %@!</source> <target>Сообщения от %@ будут показаны!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Messages in this chat will never be deleted." xml:space="preserve"> + <source>Messages in this chat will never be deleted.</source> + <target>Сообщения в этом чате никогда не будут удалены.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages received" xml:space="preserve"> + <source>Messages received</source> + <target>Получено сообщений</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages sent" xml:space="preserve"> + <source>Messages sent</source> + <target>Сообщений отправлено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages were deleted after you selected them." xml:space="preserve"> + <source>Messages were deleted after you selected them.</source> + <target>Сообщения были удалены после того, как вы их выбрали.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Сообщения, файлы и звонки защищены **end-to-end шифрованием** с прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Сообщения, файлы и звонки защищены **квантово-устойчивым end-to-end шифрованием** с прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate device" xml:space="preserve"> + <source>Migrate device</source> + <target>Мигрировать устройство</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate from another device" xml:space="preserve"> + <source>Migrate from another device</source> + <target>Миграция с другого устройства</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate here" xml:space="preserve"> + <source>Migrate here</source> + <target>Мигрировать сюда</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device" xml:space="preserve"> + <source>Migrate to another device</source> + <target>Мигрировать на другое устройство</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device via QR code." xml:space="preserve"> + <source>Migrate to another device via QR code.</source> + <target>Мигрируйте на другое устройство через QR код.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating" xml:space="preserve"> + <source>Migrating</source> + <target>Миграция</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migrating database archive…" xml:space="preserve"> <source>Migrating database archive…</source> <target>Данные чата перемещаются…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Migration complete" xml:space="preserve"> + <source>Migration complete</source> + <target>Миграция завершена</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> <target>Ошибка при перемещении данных:</target> @@ -3353,9 +4974,9 @@ This is your link for group %@!</source> <target>Перемещение данных завершено</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> - <target>Миграции: %@</target> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> + <target>Миграции:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> @@ -3373,21 +4994,31 @@ This is your link for group %@!</source> <target>Модерировано: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="More" xml:space="preserve"> + <source>More</source> + <target>Больше</target> + <note>swipe action</note> + </trans-unit> <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> <target>Дополнительные улучшения скоро!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="More reliable network connection." xml:space="preserve"> + <source>More reliable network connection.</source> + <target>Более надежное соединение с сетью.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="More reliable notifications" xml:space="preserve"> + <source>More reliable notifications</source> + <target>Более надежные уведомления</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> <source>Most likely this connection is deleted.</source> <target>Скорее всего, соединение удалено.</target> <note>item status description</note> </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> - <source>Most likely this contact has deleted the connection with you.</source> - <target>Скорее всего, этот контакт удалил соединение с Вами.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> <target>Много профилей чата</target> @@ -3396,7 +5027,12 @@ This is your link for group %@!</source> <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> <target>Без звука</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Mute all" xml:space="preserve"> + <source>Mute all</source> + <target>Все без звука</target> + <note>notification label action</note> </trans-unit> <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> @@ -3406,13 +5042,38 @@ This is your link for group %@!</source> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> <target>Имя</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> <target>Сеть & серверы</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Network connection" xml:space="preserve"> + <source>Network connection</source> + <target>Интернет-соединение</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network decentralization" xml:space="preserve"> + <source>Network decentralization</source> + <target>Децентрализация сети</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve"> + <source>Network issues - message expired after many attempts to send it.</source> + <target>Ошибка сети - сообщение не было отправлено после многократных попыток.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Network management" xml:space="preserve"> + <source>Network management</source> + <target>Статус сети</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network operator" xml:space="preserve"> + <source>Network operator</source> + <target>Оператор сети</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> <target>Настройки сети</target> @@ -3423,11 +5084,36 @@ This is your link for group %@!</source> <target>Состояние сети</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New" xml:space="preserve"> + <source>New</source> + <target>Новый</target> + <note>token status text</note> + </trans-unit> <trans-unit id="New Passcode" xml:space="preserve"> <source>New Passcode</source> <target>Новый Код</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve"> + <source>New SOCKS credentials will be used every time you start the app.</source> + <target>Новые учетные данные SOCKS будут использоваться при каждом запуске приложения.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve"> + <source>New SOCKS credentials will be used for each server.</source> + <target>Новые учетные данные SOCKS будут использоваться для каждого сервера.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat" xml:space="preserve"> + <source>New chat</source> + <target>Новый чат</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat experience 🎉" xml:space="preserve"> + <source>New chat experience 🎉</source> + <target>Новый интерфейс 🎉</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> <target>Новый запрос на соединение</target> @@ -3438,11 +5124,6 @@ This is your link for group %@!</source> <target>Новый контакт:</target> <note>notification</note> </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> - <source>New database archive</source> - <target>Новый архив чата</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="New desktop app!" xml:space="preserve"> <source>New desktop app!</source> <target>Приложение для компьютера!</target> @@ -3453,14 +5134,23 @@ This is your link for group %@!</source> <target>Новое имя</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>Новые события</target> + <note>notification</note> + </trans-unit> <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> <target>Новое в %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New media options" xml:space="preserve"> + <source>New media options</source> + <target>Новые медиа-опции</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> - <target>Роль члена группы</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="New message" xml:space="preserve"> @@ -3473,6 +5163,11 @@ This is your link for group %@!</source> <target>Новый пароль…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New server" xml:space="preserve"> + <source>New server</source> + <target>Новый сервер</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No" xml:space="preserve"> <source>No</source> <target>Нет</target> @@ -3483,6 +5178,21 @@ This is your link for group %@!</source> <target>Нет кода доступа</target> <note>Authentication unavailable</note> </trans-unit> + <trans-unit id="No chats" xml:space="preserve"> + <source>No chats</source> + <target>Нет чатов</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats found" xml:space="preserve"> + <source>No chats found</source> + <target>Чаты не найдены</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats in list %@" xml:space="preserve"> + <source>No chats in list %@</source> + <target>Нет чатов в списке %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> <target>Контакты не выбраны</target> @@ -3503,6 +5213,11 @@ This is your link for group %@!</source> <target>Отсутствует токен устройства!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No direct connection yet, message is forwarded by admin." xml:space="preserve"> + <source>No direct connection yet, message is forwarded by admin.</source> + <target>Прямого соединения пока нет, сообщение переслано или будет переслано админом.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="No filtered chats" xml:space="preserve"> <source>No filtered chats</source> <target>Нет отфильтрованных разговоров</target> @@ -3518,21 +5233,111 @@ This is your link for group %@!</source> <target>Нет истории</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No info, try to reload" xml:space="preserve"> + <source>No info, try to reload</source> + <target>Нет информации, попробуйте перезагрузить</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No media & file servers." xml:space="preserve"> + <source>No media & file servers.</source> + <target>Нет серверов файлов и медиа.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No message" xml:space="preserve"> + <source>No message</source> + <target>Нет сообщения</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No message servers." xml:space="preserve"> + <source>No message servers.</source> + <target>Нет серверов сообщений.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No network connection" xml:space="preserve"> + <source>No network connection</source> + <target>Нет интернет-соединения</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record speech" xml:space="preserve"> + <source>No permission to record speech</source> + <target>Нет разрешения на запись речи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record video" xml:space="preserve"> + <source>No permission to record video</source> + <target>Нет разрешения на запись видео</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> <source>No permission to record voice message</source> <target>Нет разрешения для записи голосового сообщения</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No push server" xml:space="preserve"> + <source>No push server</source> + <target>Без сервера нотификаций</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> <target>Нет полученных или отправленных файлов</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No servers for private message routing." xml:space="preserve"> + <source>No servers for private message routing.</source> + <target>Нет серверов для доставки сообщений.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive files." xml:space="preserve"> + <source>No servers to receive files.</source> + <target>Нет серверов для приема файлов.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive messages." xml:space="preserve"> + <source>No servers to receive messages.</source> + <target>Нет серверов для приема сообщений.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to send files." xml:space="preserve"> + <source>No servers to send files.</source> + <target>Нет серверов для отправки файлов.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No token!" xml:space="preserve"> + <source>No token!</source> + <target>Нет токена!</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="No unread chats" xml:space="preserve"> + <source>No unread chats</source> + <target>Нет непрочитанных чатов</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> + <target>Без идентификаторов пользователей.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Not compatible!" xml:space="preserve"> <source>Not compatible!</source> <target>Несовместимая версия!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notes" xml:space="preserve"> + <source>Notes</source> + <target>Заметки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing selected" xml:space="preserve"> + <source>Nothing selected</source> + <target>Ничего не выбрано</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing to forward!" xml:space="preserve"> + <source>Nothing to forward!</source> + <target>Нет сообщений, которые можно переслать!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> <target>Уведомления</target> @@ -3543,53 +5348,64 @@ This is your link for group %@!</source> <target>Уведомления выключены</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notifications error" xml:space="preserve"> + <source>Notifications error</source> + <target>Ошибка уведомлений</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Notifications privacy" xml:space="preserve"> + <source>Notifications privacy</source> + <target>Конфиденциальность уведомлений</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications status" xml:space="preserve"> + <source>Notifications status</source> + <target>Статус уведомлений</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. - disable members ("observer" role)</source> - <target>Теперь админы могут: -- удалять сообщения членов. -- приостанавливать членов (роль "наблюдатель")</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="OK" xml:space="preserve"> + <source>OK</source> + <target>OK</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Off" xml:space="preserve"> <source>Off</source> <target>Выключено</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> - <source>Off (Local)</source> - <target>Выключить (Локальные)</target> - <note>No comment provided by engineer.</note> + <note>blur media</note> </trans-unit> <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> <target>Ок</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> <target>Предыдущая версия данных чата</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> - <source>Old database archive</source> - <target>Старый архив чата</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> <target>Одноразовая ссылка</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> - <target>Подключаться только к onion хостам. Требуется включенный VPN.</target> + <trans-unit id="Onion hosts will be **required** for connection. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be **required** for connection. +Requires compatible VPN.</source> + <target>Подключаться только к **onion** хостам. +Требуется совместимый VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> - <target>Onion хосты используются, если возможно. Требуется включенный VPN.</target> + <trans-unit id="Onion hosts will be used when available. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be used when available. +Requires compatible VPN.</source> + <target>Onion хосты используются, если возможно. +Требуется совместимый VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will not be used." xml:space="preserve"> @@ -3597,9 +5413,19 @@ This is your link for group %@!</source> <target>Onion хосты не используются.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> - <target>Только пользовательские устройства хранят контакты, группы и сообщения, которые отправляются **с двухуровневым end-to-end шифрованием**.</target> + <trans-unit id="Only chat owners can change preferences." xml:space="preserve"> + <source>Only chat owners can change preferences.</source> + <target>Только владельцы разговора могут поменять предпочтения.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> + <target>Только пользовательские устройства хранят контакты, группы и сообщения.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only delete conversation" xml:space="preserve"> + <source>Only delete conversation</source> + <target>Удалить только разговор</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> @@ -3617,14 +5443,24 @@ This is your link for group %@!</source> <target>Только владельцы группы могут разрешить голосовые сообщения.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only sender and moderators see it" xml:space="preserve"> + <source>Only sender and moderators see it</source> + <target>Только отправитель и модераторы видят это</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you and moderators see it" xml:space="preserve"> + <source>Only you and moderators see it</source> + <target>Только вы и модераторы видят это</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only you can add message reactions." xml:space="preserve"> <source>Only you can add message reactions.</source> <target>Только Вы можете добавлять реакции на сообщения.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <target>Только Вы можете необратимо удалять сообщения (Ваш контакт может помечать их на удаление).</target> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source> + <target>Только Вы можете необратимо удалять сообщения (Ваш контакт может помечать их на удаление). (24 часа)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can make calls." xml:space="preserve"> @@ -3647,9 +5483,9 @@ This is your link for group %@!</source> <target>Только Ваш контакт может добавлять реакции на сообщения.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <target>Только Ваш контакт может необратимо удалять сообщения (Вы можете помечать их на удаление).</target> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source> + <target>Только Ваш контакт может необратимо удалять сообщения (Вы можете помечать их на удаление). (24 часа)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can make calls." xml:space="preserve"> @@ -3670,13 +5506,18 @@ This is your link for group %@!</source> <trans-unit id="Open" xml:space="preserve"> <source>Open</source> <target>Открыть</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Открыть Настройки</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open changes" xml:space="preserve"> + <source>Open changes</source> + <target>Открыть изменения</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> <target>Открыть чат</target> @@ -3687,26 +5528,87 @@ This is your link for group %@!</source> <target>Открыть консоль</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Open conditions" xml:space="preserve"> + <source>Open conditions</source> + <target>Открыть условия</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open group" xml:space="preserve"> <source>Open group</source> <target>Открыть группу</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve"> - <source>Open user profiles</source> - <target>Открыть профили пользователя</target> + <trans-unit id="Open link?" xml:space="preserve"> + <source>Open link?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Open migration to another device" xml:space="preserve"> + <source>Open migration to another device</source> + <target>Открытие миграции на другое устройство</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> - <target>Открытый протокол и код - кто угодно может запустить сервер.</target> + <trans-unit id="Opening app…" xml:space="preserve"> + <source>Opening app…</source> + <target>Приложение отрывается…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve"> - <source>Opening database…</source> - <target>Открытие базы данных…</target> + <trans-unit id="Operator" xml:space="preserve"> + <source>Operator</source> + <target>Оператор</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Operator server" xml:space="preserve"> + <source>Operator server</source> + <target>Сервер оператора</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Or import archive file" xml:space="preserve"> + <source>Or import archive file</source> + <target>Или импортировать файл архива</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or paste archive link" xml:space="preserve"> + <source>Or paste archive link</source> + <target>Или вставьте ссылку архива</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or scan QR code" xml:space="preserve"> + <source>Or scan QR code</source> + <target>Или отсканируйте QR код</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or securely share this file link" xml:space="preserve"> + <source>Or securely share this file link</source> + <target>Или передайте эту ссылку</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or show this code" xml:space="preserve"> + <source>Or show this code</source> + <target>Или покажите этот код</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or to share privately" xml:space="preserve"> + <source>Or to share privately</source> + <target>Или поделиться конфиденциально</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Organize chats into lists" xml:space="preserve"> + <source>Organize chats into lists</source> + <target>Организуйте чаты в списки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other" xml:space="preserve"> + <source>Other</source> + <target>Другaя сеть</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other file errors: %@" xml:space="preserve"> + <source>Other file errors: +%@</source> + <target>Другие ошибки файлов: +%@</target> + <note>alert message</note> + </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> <target>Количество PING</target> @@ -3742,15 +5644,19 @@ This is your link for group %@!</source> <target>Код доступа установлен!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Password" xml:space="preserve"> + <source>Password</source> + <target>Пароль</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> <target>Пароль чтобы раскрыть</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste" xml:space="preserve"> - <source>Paste</source> - <target>Вставить</target> - <note>No comment provided by engineer.</note> + <trans-unit id="Past member %@" xml:space="preserve"> + <source>Past member %@</source> + <note>past/unknown group member</note> </trans-unit> <trans-unit id="Paste desktop address" xml:space="preserve"> <source>Paste desktop address</source> @@ -3762,23 +5668,23 @@ This is your link for group %@!</source> <target>Вставить изображение</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve"> - <source>Paste received link</source> - <target>Вставить полученную ссылку</target> + <trans-unit id="Paste link to connect!" xml:space="preserve"> + <source>Paste link to connect!</source> + <target>Вставьте ссылку, чтобы соединиться!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> - <source>Paste the link you received to connect with your contact.</source> - <target>Чтобы соединиться, вставьте ссылку, полученную от Вашего контакта.</target> - <note>placeholder</note> - </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> - <target>С Вами можно соединиться только через созданные Вами ссылки.</target> + <trans-unit id="Paste the link you received" xml:space="preserve"> + <source>Paste the link you received</source> + <target>Вставьте полученную ссылку</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Periodically" xml:space="preserve"> - <source>Periodically</source> + <trans-unit id="Pending" xml:space="preserve"> + <source>Pending</source> + <target>Ожидает</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Periodic" xml:space="preserve"> + <source>Periodic</source> <target>Периодически</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3787,11 +5693,33 @@ This is your link for group %@!</source> <target>Ошибка расшифровки</target> <note>message decrypt error item</note> </trans-unit> + <trans-unit id="Picture-in-picture calls" xml:space="preserve"> + <source>Picture-in-picture calls</source> + <target>Звонки с картинкой-в-картинке</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Play from the chat list." xml:space="preserve"> + <source>Play from the chat list.</source> + <target>Открыть из списка чатов.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable calls." xml:space="preserve"> + <source>Please ask your contact to enable calls.</source> + <target>Попросите Вашего контакта разрешить звонки.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> <target>Попросите у Вашего контакта разрешить отправку голосовых сообщений.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers." xml:space="preserve"> + <source>Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers.</source> + <target>Пожалуйста, проверьте, что мобильный и компьютер находятся в одной и той же локальной сети, и что брандмауэр компьютера разрешает подключение. +Пожалуйста, поделитесь любыми другими ошибками с разработчиками.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> <target>Пожалуйста, проверьте, что Вы использовали правильную ссылку или попросите, чтобы Ваш контакт отправил Вам другую ссылку.</target> @@ -3807,6 +5735,18 @@ This is your link for group %@!</source> <target>Проверьте предпочтения Вашего контакта.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve"> + <source>Please confirm that network settings are correct for this device.</source> + <target>Пожалуйста, подтвердите, что настройки сети верны для этого устройства.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact developers. Error: %@" xml:space="preserve"> + <source>Please contact developers. +Error: %@</source> + <target>Пожалуйста, сообщите разработчикам. +Ошибка: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> <target>Пожалуйста, свяжитесь с админом группы.</target> @@ -3847,11 +5787,31 @@ This is your link for group %@!</source> <target>Пожалуйста, надежно сохраните пароль, Вы НЕ сможете его поменять, если потеряете.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please try to disable and re-enable notfications." xml:space="preserve"> + <source>Please try to disable and re-enable notfications.</source> + <target>Попробуйте выключить и снова включить уведомления.</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token activation to complete." xml:space="preserve"> + <source>Please wait for token activation to complete.</source> + <target>Пожалуйста, дождитесь завершения активации токена.</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token to be registered." xml:space="preserve"> + <source>Please wait for token to be registered.</source> + <target>Пожалуйста, дождитесь регистрации токена.</target> + <note>token info</note> + </trans-unit> <trans-unit id="Polish interface" xml:space="preserve"> <source>Polish interface</source> <target>Польский интерфейс</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Port" xml:space="preserve"> + <source>Port</source> + <target>Порт</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> <target>Возможно, хэш сертификата в адресе сервера неверный</target> @@ -3862,36 +5822,86 @@ This is your link for group %@!</source> <target>Сохранить последний черновик, вместе с вложениями.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Preset server" xml:space="preserve"> - <source>Preset server</source> - <target>Сервер по умолчанию</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> <target>Адрес сервера по умолчанию</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Preset servers" xml:space="preserve"> + <source>Preset servers</source> + <target>Серверы по умолчанию</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Preview" xml:space="preserve"> <source>Preview</source> <target>Просмотр</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Previously connected servers" xml:space="preserve"> + <source>Previously connected servers</source> + <target>Ранее подключенные серверы</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> <target>Конфиденциальность</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Privacy for your customers." xml:space="preserve"> + <source>Privacy for your customers.</source> + <target>Конфиденциальность для ваших покупателей.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy policy and conditions of use." xml:space="preserve"> + <source>Privacy policy and conditions of use.</source> + <target>Политика конфиденциальности и условия использования.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy redefined" xml:space="preserve"> <source>Privacy redefined</source> <target>Более конфиденциальный</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve"> + <source>Private chats, groups and your contacts are not accessible to server operators.</source> + <target>Частные разговоры, группы и Ваши контакты недоступны для операторов серверов.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> <target>Защищенные имена файлов</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private media file names." xml:space="preserve"> + <source>Private media file names.</source> + <target>Конфиденциальные названия медиафайлов.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing" xml:space="preserve"> + <source>Private message routing</source> + <target>Конфиденциальная доставка сообщений</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing 🚀" xml:space="preserve"> + <source>Private message routing 🚀</source> + <target>Конфиденциальная доставка 🚀</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private notes" xml:space="preserve"> + <source>Private notes</source> + <target>Личные заметки</target> + <note>name of notes to self</note> + </trans-unit> + <trans-unit id="Private routing" xml:space="preserve"> + <source>Private routing</source> + <target>Конфиденциальная доставка</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private routing error" xml:space="preserve"> + <source>Private routing error</source> + <target>Ошибка конфиденциальной доставки</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> <target>Профиль и соединения на сервере</target> @@ -3899,17 +5909,12 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Profile image" xml:space="preserve"> <source>Profile image</source> - <target>Аватар</target> + <target>Картинка профиля</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile name" xml:space="preserve"> - <source>Profile name</source> - <target>Имя профиля</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile name:" xml:space="preserve"> - <source>Profile name:</source> - <target>Имя профиля:</target> + <trans-unit id="Profile images" xml:space="preserve"> + <source>Profile images</source> + <target>Картинки профилей</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile password" xml:space="preserve"> @@ -3917,10 +5922,15 @@ This is your link for group %@!</source> <target>Пароль профиля</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Profile theme" xml:space="preserve"> + <source>Profile theme</source> + <target>Тема профиля</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> <source>Profile update will be sent to your contacts.</source> <target>Обновлённый профиль будет отправлен Вашим контактам.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> @@ -3942,9 +5952,18 @@ This is your link for group %@!</source> <target>Запретить реакции на сообщения.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Prohibit reporting messages to moderators." xml:space="preserve"> + <source>Prohibit reporting messages to moderators.</source> + <target>Запретить жаловаться модераторам группы.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending SimpleX links." xml:space="preserve"> + <source>Prohibit sending SimpleX links.</source> + <target>Запретить отправку ссылок SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> - <target>Запретить посылать прямые сообщения членам группы.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit sending disappearing messages." xml:space="preserve"> @@ -3962,11 +5981,23 @@ This is your link for group %@!</source> <target>Запретить отправлять голосовые сообщений.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect IP address" xml:space="preserve"> + <source>Protect IP address</source> + <target>Защитить IP адрес</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> <target>Защитить экран приложения</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve"> + <source>Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings.</source> + <target>Защитите ваш IP адрес от серверов сообщений, выбранных Вашими контактами. +Включите в настройках *Сети и серверов*.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> <target>Защитите Ваши профили чата паролем!</target> @@ -3982,16 +6013,46 @@ This is your link for group %@!</source> <target>Таймаут протокола на KB</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Proxied" xml:space="preserve"> + <source>Proxied</source> + <target>Проксировано</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied servers" xml:space="preserve"> + <source>Proxied servers</source> + <target>Проксированные серверы</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxy requires password" xml:space="preserve"> + <source>Proxy requires password</source> + <target>Прокси требует пароль</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> <target>Доставка уведомлений</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Push server" xml:space="preserve"> + <source>Push server</source> + <target>Сервер уведомлений</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Quantum resistant encryption" xml:space="preserve"> + <source>Quantum resistant encryption</source> + <target>Квантово-устойчивое шифрование</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> <source>Rate the app</source> <target>Оценить приложение</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reachable chat toolbar" xml:space="preserve"> + <source>Reachable chat toolbar</source> + <target>Доступная панель чата</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> <target>Реакция…</target> @@ -4000,16 +6061,21 @@ This is your link for group %@!</source> <trans-unit id="Read" xml:space="preserve"> <source>Read</source> <target>Прочитано</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Read more" xml:space="preserve"> <source>Read more</source> <target>Узнать больше</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> - <target>Узнать больше в [Руководстве пользователя](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source> + <target>Дополнительная информация в [Руководстве пользователя](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</source> + <target>Узнать больше в [Руководстве пользователя](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> @@ -4017,11 +6083,6 @@ This is your link for group %@!</source> <target>Узнать больше в [Руководстве пользователя](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> - <source>Read more in our GitHub repository.</source> - <target>Узнайте больше из нашего GitHub репозитория.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> <target>Узнайте больше из нашего [GitHub репозитория](https://github.com/simplex-chat/simplex-chat#readme).</target> @@ -4032,6 +6093,11 @@ This is your link for group %@!</source> <target>Отчёты о доставке выключены</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receive errors" xml:space="preserve"> + <source>Receive errors</source> + <target>Ошибки приема</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Получено</target> @@ -4052,6 +6118,21 @@ This is your link for group %@!</source> <target>Полученное сообщение</target> <note>message info title</note> </trans-unit> + <trans-unit id="Received messages" xml:space="preserve"> + <source>Received messages</source> + <target>Полученные сообщения</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received reply" xml:space="preserve"> + <source>Received reply</source> + <target>Полученный ответ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received total" xml:space="preserve"> + <source>Received total</source> + <target>Всего получено</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> <target>Адрес получения сообщений будет перемещён на другой сервер. Изменение адреса завершится после того как отправитель будет онлайн.</target> @@ -4067,16 +6148,51 @@ This is your link for group %@!</source> <target>Получение через</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Recent history and improved [directory bot](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)." xml:space="preserve"> + <source>Recent history and improved [directory bot](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).</source> + <target>История сообщений и улучшенный [каталог групп](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve"> + <source>Recipient(s) can't see who this message is from.</source> + <target>Получатели не видят от кого это сообщение.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> <target>Получатели видят их в то время как Вы их набираете.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect" xml:space="preserve"> + <source>Reconnect</source> + <target>Переподключить</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> <target>Повторно подключите все серверы, чтобы принудительно доставить сообщения. Используется дополнительный трафик.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect all servers" xml:space="preserve"> + <source>Reconnect all servers</source> + <target>Переподключить все серверы</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers?" xml:space="preserve"> + <source>Reconnect all servers?</source> + <target>Переподключить все серверы?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect server to force message delivery. It uses additional traffic.</source> + <target>Переподключить сервер для устранения неполадок доставки сообщений. Это использует дополнительный трафик.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server?" xml:space="preserve"> + <source>Reconnect server?</source> + <target>Переподключить сервер?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> <target>Переподключить серверы?</target> @@ -4097,10 +6213,26 @@ This is your link for group %@!</source> <target>Уменьшенное потребление батареи</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Register" xml:space="preserve"> + <source>Register</source> + <target>Зарегистрировать</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Register notification token?" xml:space="preserve"> + <source>Register notification token?</source> + <target>Зарегистрировать токен уведомлений?</target> + <note>token info</note> + </trans-unit> + <trans-unit id="Registered" xml:space="preserve"> + <source>Registered</source> + <target>Зарегистрирован</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> <target>Отклонить</target> - <note>reject incoming call via notification</note> + <note>reject incoming call via notification +swipe action</note> </trans-unit> <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> <source>Reject (sender NOT notified)</source> @@ -4127,14 +6259,22 @@ This is your link for group %@!</source> <target>Удалить</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Remove archive?" xml:space="preserve"> + <source>Remove archive?</source> + <target>Удалить архив?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove image" xml:space="preserve"> + <source>Remove image</source> + <target>Удалить изображение</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> - <target>Удалить члена группы</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Remove member?" xml:space="preserve"> <source>Remove member?</source> - <target>Удалить члена группы?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Remove passphrase from keychain?" xml:space="preserve"> @@ -4162,16 +6302,81 @@ This is your link for group %@!</source> <target>Повторить запрос на соединение?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat download" xml:space="preserve"> + <source>Repeat download</source> + <target>Повторить загрузку</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat import" xml:space="preserve"> + <source>Repeat import</source> + <target>Повторить импорт</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Repeat join request?" xml:space="preserve"> <source>Repeat join request?</source> <target>Повторить запрос на вступление?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat upload" xml:space="preserve"> + <source>Repeat upload</source> + <target>Повторить загрузку</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reply" xml:space="preserve"> <source>Reply</source> <target>Ответить</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Report" xml:space="preserve"> + <source>Report</source> + <target>Пожаловаться</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Report content: only group moderators will see it." xml:space="preserve"> + <source>Report content: only group moderators will see it.</source> + <target>Пожаловаться на сообщение: увидят только модераторы группы.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report member profile: only group moderators will see it." xml:space="preserve"> + <source>Report member profile: only group moderators will see it.</source> + <target>Пожаловаться на профиль: увидят только модераторы группы.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report other: only group moderators will see it." xml:space="preserve"> + <source>Report other: only group moderators will see it.</source> + <target>Пожаловаться: увидят только модераторы группы.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report reason?" xml:space="preserve"> + <source>Report reason?</source> + <target>Причина сообщения?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Report spam: only group moderators will see it." xml:space="preserve"> + <source>Report spam: only group moderators will see it.</source> + <target>Пожаловаться на спам: увидят только модераторы группы.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report violation: only group moderators will see it." xml:space="preserve"> + <source>Report violation: only group moderators will see it.</source> + <target>Пожаловаться на нарушение: увидят только модераторы группы.</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report: %@" xml:space="preserve"> + <source>Report: %@</source> + <target>Сообщение о нарушении: %@</target> + <note>report in notification</note> + </trans-unit> + <trans-unit id="Reporting messages to moderators is prohibited." xml:space="preserve"> + <source>Reporting messages to moderators is prohibited.</source> + <target>Сообщения о нарушениях запрещены в этой группе.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reports" xml:space="preserve"> + <source>Reports</source> + <target>Сообщения о нарушениях</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Required" xml:space="preserve"> <source>Required</source> <target>Обязательно</target> @@ -4182,16 +6387,41 @@ This is your link for group %@!</source> <target>Сбросить</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset all hints" xml:space="preserve"> + <source>Reset all hints</source> + <target>Сбросить все подсказки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics" xml:space="preserve"> + <source>Reset all statistics</source> + <target>Сбросить всю статистику</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics?" xml:space="preserve"> + <source>Reset all statistics?</source> + <target>Сбросить всю статистику?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> <target>Сбросить цвета</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to app theme" xml:space="preserve"> + <source>Reset to app theme</source> + <target>Сбросить на тему приложения</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> <target>Сбросить настройки</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to user theme" xml:space="preserve"> + <source>Reset to user theme</source> + <target>Сбросить на тему пользователя</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> <source>Restart the app to create a new chat profile</source> <target>Перезапустите приложение, чтобы создать новый профиль.</target> @@ -4222,14 +6452,19 @@ This is your link for group %@!</source> <target>Ошибка при восстановлении базы данных</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Retry" xml:space="preserve"> + <source>Retry</source> + <target>Повторить</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> <target>Показать</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Revert" xml:space="preserve"> - <source>Revert</source> - <target>Отменить изменения</target> + <trans-unit id="Review conditions" xml:space="preserve"> + <source>Review conditions</source> + <target>Посмотреть условия</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke" xml:space="preserve"> @@ -4257,29 +6492,49 @@ This is your link for group %@!</source> <target>Запустить chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve"> - <source>SMP servers</source> - <target>SMP серверы</target> + <trans-unit id="SMP server" xml:space="preserve"> + <source>SMP server</source> + <target>SMP сервер</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SOCKS proxy" xml:space="preserve"> + <source>SOCKS proxy</source> + <target>SOCKS прокси</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safely receive files" xml:space="preserve"> + <source>Safely receive files</source> + <target>Получайте файлы безопасно</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safer groups" xml:space="preserve"> + <source>Safer groups</source> + <target>Более безопасные группы</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save" xml:space="preserve"> <source>Save</source> <target>Сохранить</target> - <note>chat item action</note> + <note>alert button +chat item action</note> </trans-unit> <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> <target>Сохранить (и уведомить контакты)</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> <target>Сохранить и уведомить контакт</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> - <target>Сохранить и уведомить членов группы</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save and reconnect" xml:space="preserve"> + <source>Save and reconnect</source> + <target>Сохранить и переподключиться</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save and update group profile" xml:space="preserve"> @@ -4287,21 +6542,16 @@ This is your link for group %@!</source> <target>Сохранить сообщение и обновить группу</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save archive" xml:space="preserve"> - <source>Save archive</source> - <target>Сохранить архив</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve"> - <source>Save auto-accept settings</source> - <target>Сохранить настройки автоприема</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> <source>Save group profile</source> <target>Сохранить профиль группы</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save list" xml:space="preserve"> + <source>Save list</source> + <target>Сохранить список</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> <target>Сохранить пароль и открыть чат</target> @@ -4315,7 +6565,7 @@ This is your link for group %@!</source> <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> <target>Сохранить предпочтения?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> @@ -4330,23 +6580,53 @@ This is your link for group %@!</source> <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> <target>Сохранить серверы?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve"> - <source>Save settings?</source> - <target>Сохранить настройки?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> <target>Сохранить приветственное сообщение?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save your profile?" xml:space="preserve"> + <source>Save your profile?</source> + <target>Сохранить ваш профиль?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Saved" xml:space="preserve"> + <source>Saved</source> + <target>Сохранено</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> <source>Saved WebRTC ICE servers will be removed</source> <target>Сохраненные WebRTC ICE серверы будут удалены</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Saved from" xml:space="preserve"> + <source>Saved from</source> + <target>Сохранено из</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved message" xml:space="preserve"> + <source>Saved message</source> + <target>Сохраненное сообщение</target> + <note>message info title</note> + </trans-unit> + <trans-unit id="Saving %lld messages" xml:space="preserve"> + <source>Saving %lld messages</source> + <target>Сохранение %lld сообщений</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scale" xml:space="preserve"> + <source>Scale</source> + <target>Масштаб</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan / Paste link" xml:space="preserve"> + <source>Scan / Paste link</source> + <target>Сканировать / Вставить ссылку</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> <target>Сканировать QR код</target> @@ -4377,11 +6657,31 @@ This is your link for group %@!</source> <target>Поиск</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Search bar accepts invitation links." xml:space="preserve"> + <source>Search bar accepts invitation links.</source> + <target>Поле поиска поддерживает ссылки-приглашения.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search or paste SimpleX link" xml:space="preserve"> + <source>Search or paste SimpleX link</source> + <target>Искать или вставьте ссылку SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secondary" xml:space="preserve"> + <source>Secondary</source> + <target>Вторичный</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> <source>Secure queue</source> <target>Защита очереди</target> <note>server test step</note> </trans-unit> + <trans-unit id="Secured" xml:space="preserve"> + <source>Secured</source> + <target>Защищено</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> <target>Аудит безопасности</target> @@ -4395,6 +6695,21 @@ This is your link for group %@!</source> <trans-unit id="Select" xml:space="preserve"> <source>Select</source> <target>Выбрать</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Select chat profile" xml:space="preserve"> + <source>Select chat profile</source> + <target>Выберите профиль чата</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected %lld" xml:space="preserve"> + <source>Selected %lld</source> + <target>Выбрано %lld</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Выбранные настройки чата запрещают это сообщение.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct" xml:space="preserve"> @@ -4432,11 +6747,6 @@ This is your link for group %@!</source> <target>Отправка отчётов о доставке</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve"> - <source>Send direct message</source> - <target>Отправить сообщение</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Send direct message to connect" xml:space="preserve"> <source>Send direct message to connect</source> <target>Отправьте сообщение чтобы соединиться</target> @@ -4447,6 +6757,11 @@ This is your link for group %@!</source> <target>Отправить исчезающее сообщение</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send errors" xml:space="preserve"> + <source>Send errors</source> + <target>Ошибки отправки</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> <target>Отправлять картинки ссылок</target> @@ -4457,14 +6772,29 @@ This is your link for group %@!</source> <target>Отправить живое сообщение</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send message to enable calls." xml:space="preserve"> + <source>Send message to enable calls.</source> + <target>Отправьте сообщение, чтобы включить звонки.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source> + <target>Отправлять сообщения напрямую, когда IP адрес защищен, и Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when your or destination server does not support private routing.</source> + <target>Отправлять сообщения напрямую, когда Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> <target>Отправлять уведомления</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> - <source>Send notifications:</source> - <target>Отправлять уведомления:</target> + <trans-unit id="Send private reports" xml:space="preserve"> + <source>Send private reports</source> + <target>Вы можете сообщить о нарушениях</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve"> @@ -4482,10 +6812,14 @@ This is your link for group %@!</source> <target>Отправьте из галереи или из дополнительных клавиатур.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve"> + <source>Send up to 100 last messages to new members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> <target>Отправитель отменил передачу файла.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> @@ -4542,6 +6876,11 @@ This is your link for group %@!</source> <target>Отправлено: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Sent directly" xml:space="preserve"> + <source>Sent directly</source> + <target>Отправлено напрямую</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> <target>Отправка файла</target> @@ -4552,11 +6891,71 @@ This is your link for group %@!</source> <target>Отправленное сообщение</target> <note>message info title</note> </trans-unit> + <trans-unit id="Sent messages" xml:space="preserve"> + <source>Sent messages</source> + <target>Отправленные сообщения</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> <target>Отправленные сообщения будут удалены через заданное время.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sent reply" xml:space="preserve"> + <source>Sent reply</source> + <target>Отправленный ответ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent total" xml:space="preserve"> + <source>Sent total</source> + <target>Всего отправлено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent via proxy" xml:space="preserve"> + <source>Sent via proxy</source> + <target>Отправлено через прокси</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server" xml:space="preserve"> + <source>Server</source> + <target>Сервер</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server added to operator %@." xml:space="preserve"> + <source>Server added to operator %@.</source> + <target>Сервер добавлен к оператору %@.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Server address" xml:space="preserve"> + <source>Server address</source> + <target>Адрес сервера</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings." xml:space="preserve"> + <source>Server address is incompatible with network settings.</source> + <target>Адрес сервера несовместим с настройками сети.</target> + <note>srv error text.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Server address is incompatible with network settings: %@.</source> + <target>Адрес сервера несовместим с сетевыми настройками: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server operator changed." xml:space="preserve"> + <source>Server operator changed.</source> + <target>Оператор серверов изменен.</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Server operators" xml:space="preserve"> + <source>Server operators</source> + <target>Операторы серверов</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server protocol changed." xml:space="preserve"> + <source>Server protocol changed.</source> + <target>Протокол сервера изменен.</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> <target>Сервер требует авторизации для создания очередей, проверьте пароль</target> @@ -4572,11 +6971,36 @@ This is your link for group %@!</source> <target>Ошибка теста сервера!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Server type" xml:space="preserve"> + <source>Server type</source> + <target>Тип сервера</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server version is incompatible with network settings." xml:space="preserve"> + <source>Server version is incompatible with network settings.</source> + <target>Версия сервера несовместима с настройками сети.</target> + <note>srv error text</note> + </trans-unit> + <trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve"> + <source>Server version is incompatible with your app: %@.</source> + <target>Версия сервера несовместима с вашим приложением: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Servers" xml:space="preserve"> <source>Servers</source> <target>Серверы</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Servers info" xml:space="preserve"> + <source>Servers info</source> + <target>Информация о серверах</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <target>Статистика серверов будет сброшена - это нельзя отменить!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Session code" xml:space="preserve"> <source>Session code</source> <target>Код сессии</target> @@ -4587,11 +7011,21 @@ This is your link for group %@!</source> <target>Установить 1 день</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set chat name…" xml:space="preserve"> + <source>Set chat name…</source> + <target>Имя чата…</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> <target>Имя контакта…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve"> + <source>Set default theme</source> + <target>Установить тему по умолчанию</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> <target>Предпочтения группы</target> @@ -4602,11 +7036,21 @@ This is your link for group %@!</source> <target>Установите код вместо системной аутентификации.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve"> + <source>Set message expiration in chats.</source> + <target>Установите срок хранения сообщений в чатах.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passcode" xml:space="preserve"> <source>Set passcode</source> <target>Установить код доступа</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set passphrase" xml:space="preserve"> + <source>Set passphrase</source> + <target>Установить пароль</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> <target>Установите пароль</target> @@ -4614,7 +7058,6 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Set the message shown to new members!" xml:space="preserve"> <source>Set the message shown to new members!</source> - <target>Установить сообщение для новых членов группы!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set timeouts for proxy/VPN" xml:space="preserve"> @@ -4627,24 +7070,55 @@ This is your link for group %@!</source> <target>Настройки</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve"> + <source>Settings were changed.</source> + <target>Настройки были изменены.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve"> + <source>Shape profile images</source> + <target>Форма картинок профилей</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share" xml:space="preserve"> <source>Share</source> <target>Поделиться</target> - <note>chat item action</note> + <note>alert action +chat item action</note> </trans-unit> <trans-unit id="Share 1-time link" xml:space="preserve"> <source>Share 1-time link</source> <target>Поделиться одноразовой ссылкой</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share 1-time link with a friend" xml:space="preserve"> + <source>Share 1-time link with a friend</source> + <target>Поделитесь одноразовой ссылкой с другом</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve"> + <source>Share SimpleX address on social media.</source> + <target>Поделитесь SimpleX адресом в социальных сетях.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address" xml:space="preserve"> <source>Share address</source> <target>Поделиться адресом</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share address publicly" xml:space="preserve"> + <source>Share address publicly</source> + <target>Поделитесь адресом</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address with contacts?" xml:space="preserve"> <source>Share address with contacts?</source> <target>Поделиться адресом с контактами?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Share from other apps." xml:space="preserve"> + <source>Share from other apps.</source> + <target>Поделитесь из других приложений.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share link" xml:space="preserve"> @@ -4652,9 +7126,19 @@ This is your link for group %@!</source> <target>Поделиться ссылкой</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve"> - <source>Share one-time invitation link</source> - <target>Поделиться ссылкой-приглашением</target> + <trans-unit id="Share profile" xml:space="preserve"> + <source>Share profile</source> + <target>Поделиться профилем</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share this 1-time invite link" xml:space="preserve"> + <source>Share this 1-time invite link</source> + <target>Поделиться одноразовой ссылкой-приглашением</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share to SimpleX" xml:space="preserve"> + <source>Share to SimpleX</source> + <target>Поделиться в SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share with contacts" xml:space="preserve"> @@ -4662,6 +7146,15 @@ This is your link for group %@!</source> <target>Поделиться с контактами</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Short link" xml:space="preserve"> + <source>Short link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show QR code" xml:space="preserve"> + <source>Show QR code</source> + <target>Показать QR код</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> <target>Показать звонки в истории телефона</target> @@ -4677,21 +7170,46 @@ This is your link for group %@!</source> <target>Показывать последние сообщения</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show message status" xml:space="preserve"> + <source>Show message status</source> + <target>Показать статус сообщения</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show percentage" xml:space="preserve"> + <source>Show percentage</source> + <target>Показать процент</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Показывать уведомления</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show → on messages sent via private routing." xml:space="preserve"> + <source>Show → on messages sent via private routing.</source> + <target>Показать → на сообщениях доставленных конфиденциально.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show:" xml:space="preserve"> <source>Show:</source> <target>Показать:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve"> + <source>SimpleX</source> + <target>SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Address" xml:space="preserve"> <source>SimpleX Address</source> <target>Адрес SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." xml:space="preserve"> + <source>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</source> + <target>SimpleX Chat и Flux заключили соглашение добавить серверы под управлением Flux в приложение.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> <target>Безопасность SimpleX Chat была проверена Trail of Bits.</target> @@ -4722,6 +7240,20 @@ This is your link for group %@!</source> <target>Адрес SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX address and 1-time links are safe to share via any messenger." xml:space="preserve"> + <source>SimpleX address and 1-time links are safe to share via any messenger.</source> + <target>Адрес SimpleX и одноразовые ссылки безопасно отправлять через любой мессенджер.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address or 1-time link?" xml:space="preserve"> + <source>SimpleX address or 1-time link?</source> + <target>Адрес SimpleX или одноразовая ссылка?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX channel link" xml:space="preserve"> + <source>SimpleX channel link</source> + <note>simplex link type</note> + </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> <target>SimpleX ссылка-контакт</target> @@ -4740,6 +7272,16 @@ This is your link for group %@!</source> <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> <target>SimpleX ссылки</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve"> + <source>SimpleX links are prohibited.</source> + <target>Ссылки SimpleX запрещены в этой группе.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve"> + <source>SimpleX links not allowed</source> + <target>Ссылки SimpleX не разрешены</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> @@ -4747,11 +7289,21 @@ This is your link for group %@!</source> <target>SimpleX одноразовая ссылка</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> + <source>SimpleX protocols reviewed by Trail of Bits.</source> + <target>Аудит SimpleX протоколов от Trail of Bits.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Simplified incognito mode" xml:space="preserve"> <source>Simplified incognito mode</source> <target>Упрощенный режим Инкогнито</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Size" xml:space="preserve"> + <source>Size</source> + <target>Размер</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Пропустить</target> @@ -4767,19 +7319,52 @@ This is your link for group %@!</source> <target>Маленькие группы (до 20)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Soft" xml:space="preserve"> + <source>Soft</source> + <target>Слабое</target> + <note>blur media</note> + </trans-unit> + <trans-unit id="Some app settings were not migrated." xml:space="preserve"> + <source>Some app settings were not migrated.</source> + <target>Некоторые настройки приложения не были перенесены.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some file(s) were not exported:" xml:space="preserve"> + <source>Some file(s) were not exported:</source> + <target>Некоторые файл(ы) не были экспортированы:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Во время импорта произошли некоторые ошибки - для получения более подробной информации вы можете обратиться к консоли.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import:" xml:space="preserve"> + <source>Some non-fatal errors occurred during import:</source> + <target>Во время импорта произошли некоторые ошибки:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some servers failed the test: %@" xml:space="preserve"> + <source>Some servers failed the test: +%@</source> + <target>Серверы не прошли тест: +%@</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> <target>Контакт</target> <note>notification title</note> </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve"> - <source>Start a new chat</source> - <target>Начать новый разговор</target> + <trans-unit id="Spam" xml:space="preserve"> + <source>Spam</source> + <target>Спам</target> + <note>blocking reason +report reason</note> + </trans-unit> + <trans-unit id="Square, circle, or anything in between." xml:space="preserve"> + <source>Square, circle, or anything in between.</source> + <target>Квадрат, круг и все, что между ними.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start chat" xml:space="preserve"> @@ -4787,11 +7372,26 @@ This is your link for group %@!</source> <target>Запустить чат</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Start chat?" xml:space="preserve"> + <source>Start chat?</source> + <target>Запустить чат?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> <target>Запустить перемещение данных</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Starting from %@." xml:space="preserve"> + <source>Starting from %@.</source> + <target>Начиная с %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Statistics" xml:space="preserve"> + <source>Statistics</source> + <target>Статистика</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> <target>Остановить</target> @@ -4802,9 +7402,9 @@ This is your link for group %@!</source> <target>Остановить SimpleX</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> - <source>Stop chat to enable database actions</source> - <target>Остановите чат, чтобы разблокировать операции с архивом чата</target> + <trans-unit id="Stop chat" xml:space="preserve"> + <source>Stop chat</source> + <target>Остановить чат</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> @@ -4835,23 +7435,63 @@ This is your link for group %@!</source> <trans-unit id="Stop sharing" xml:space="preserve"> <source>Stop sharing</source> <target>Прекратить делиться</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Stop sharing address?" xml:space="preserve"> <source>Stop sharing address?</source> <target>Прекратить делиться адресом?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Stopping chat" xml:space="preserve"> + <source>Stopping chat</source> + <target>Остановка чата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Storage" xml:space="preserve"> + <source>Storage</source> + <target>Хранилище</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Strong" xml:space="preserve"> + <source>Strong</source> + <target>Сильное</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Submit" xml:space="preserve"> <source>Submit</source> <target>Продолжить</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Subscribed" xml:space="preserve"> + <source>Subscribed</source> + <target>Подписано</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscription errors" xml:space="preserve"> + <source>Subscription errors</source> + <target>Ошибки подписки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscriptions ignored" xml:space="preserve"> + <source>Subscriptions ignored</source> + <target>Подписок игнорировано</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Support SimpleX Chat" xml:space="preserve"> <source>Support SimpleX Chat</source> <target>Поддержать SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Switch audio and video during the call." xml:space="preserve"> + <source>Switch audio and video during the call.</source> + <target>Переключайте звук и видео во время звонка.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> + <source>Switch chat profile for 1-time invitations.</source> + <target>Переключайте профиль чата для одноразовых приглашений.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="System" xml:space="preserve"> <source>System</source> <target>Системная</target> @@ -4862,11 +7502,21 @@ This is your link for group %@!</source> <target>Системная аутентификация</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP connection" xml:space="preserve"> + <source>TCP connection</source> + <target>TCP-соединение</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> <target>Таймаут TCP соединения</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP port for messaging" xml:space="preserve"> + <source>TCP port for messaging</source> + <target>TCP-порт для отправки сообщений</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <source>TCP_KEEPCNT</source> <target>TCP_KEEPCNT</target> @@ -4882,11 +7532,21 @@ This is your link for group %@!</source> <target>TCP_KEEPINTVL</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tail" xml:space="preserve"> + <source>Tail</source> + <target>Хвост</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> <target>Сделать фото</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap Create SimpleX address in the menu to create it later." xml:space="preserve"> + <source>Tap Create SimpleX address in the menu to create it later.</source> + <target>Нажмите Создать адрес SimpleX в меню, чтобы создать его позже.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> <target>Нажмите кнопку </target> @@ -4912,16 +7572,31 @@ This is your link for group %@!</source> <target>Нажмите, чтобы вступить инкогнито</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve"> - <source>Tap to start a new chat</source> - <target>Нажмите, чтобы начать чат</target> + <trans-unit id="Tap to paste link" xml:space="preserve"> + <source>Tap to paste link</source> + <target>Нажмите, чтобы вставить ссылку</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap to scan" xml:space="preserve"> + <source>Tap to scan</source> + <target>Нажмите, чтобы сканировать</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Temporary file error" xml:space="preserve"> + <source>Temporary file error</source> + <target>Временная ошибка файла</target> + <note>file error alert title</note> + </trans-unit> <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> <target>Ошибка теста на шаге %@.</target> <note>server test failure</note> </trans-unit> + <trans-unit id="Test notifications" xml:space="preserve"> + <source>Test notifications</source> + <target>Протестировать уведомления</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> <target>Тестировать сервер</target> @@ -4935,7 +7610,7 @@ This is your link for group %@!</source> <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> <target>Ошибка тестов!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> @@ -4952,11 +7627,6 @@ This is your link for group %@!</source> <target>Благодаря пользователям – добавьте переводы через Weblate!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> - <target>Первая в мире платформа без идентификаторов пользователей.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <source>The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised.</source> @@ -4969,11 +7639,31 @@ It can happen because of some bug or when the connection is compromised.</source <target>Приложение может посылать Вам уведомления о сообщениях и запросах на соединение - уведомления можно включить в Настройках.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The app protects your privacy by using different operators in each conversation." xml:space="preserve"> + <source>The app protects your privacy by using different operators in each conversation.</source> + <target>Приложение улучшает конфиденциальность используя разных операторов в каждом разговоре.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve"> + <source>The app will ask to confirm downloads from unknown file servers (except .onion).</source> + <target>Приложение будет запрашивать подтверждение загрузки с неизвестных серверов (за исключением .onion адресов).</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> <source>The attempt to change database passphrase was not completed.</source> <target>Попытка поменять пароль базы данных не была завершена.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve"> + <source>The code you scanned is not a SimpleX link QR code.</source> + <target>Этот QR код не является SimpleX-ccылкой.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The connection reached the limit of undelivered messages, your contact may be offline." xml:space="preserve"> + <source>The connection reached the limit of undelivered messages, your contact may be offline.</source> + <target>Соединение достигло предела недоставленных сообщений. Возможно, Ваш контакт не в сети.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> <target>Подтвержденное соединение будет отменено!</target> @@ -4994,6 +7684,11 @@ It can happen because of some bug or when the connection is compromised.</source <target>Шифрование работает, и новое соглашение не требуется. Это может привести к ошибкам соединения!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> + <target>Будущее коммуникаций</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The hash of the previous message is different." xml:space="preserve"> <source>The hash of the previous message is different.</source> <target>Хэш предыдущего сообщения отличается.</target> @@ -5001,17 +7696,18 @@ It can happen because of some bug or when the connection is compromised.</source </trans-unit> <trans-unit id="The message will be deleted for all members." xml:space="preserve"> <source>The message will be deleted for all members.</source> - <target>Сообщение будет удалено для всех членов группы.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The message will be marked as moderated for all members." xml:space="preserve"> <source>The message will be marked as moderated for all members.</source> - <target>Сообщение будет помечено как удаленное для всех членов группы.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> - <target>Новое поколение приватных сообщений</target> + <trans-unit id="The messages will be deleted for all members." xml:space="preserve"> + <source>The messages will be deleted for all members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve"> + <source>The messages will be marked as moderated for all members.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> @@ -5019,9 +7715,14 @@ It can happen because of some bug or when the connection is compromised.</source <target>Предыдущая версия данных чата не удалена при перемещении, её можно удалить.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> - <target>Профиль отправляется только Вашим контактам.</target> + <trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve"> + <source>The same conditions will apply to operator **%@**.</source> + <target>Те же самые условия будут приняты для оператора **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second preset operator in the app!" xml:space="preserve"> + <source>The second preset operator in the app!</source> + <target>Второй оператор серверов в приложении!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> @@ -5039,19 +7740,29 @@ It can happen because of some bug or when the connection is compromised.</source <target>Серверы для новых соединений Вашего текущего профиля чата **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Theme" xml:space="preserve"> - <source>Theme</source> - <target>Тема</target> + <trans-unit id="The servers for new files of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new files of your current chat profile **%@**.</source> + <target>Серверы для новых файлов Вашего текущего профиля **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve"> - <source>There should be at least one user profile.</source> - <target>Должен быть хотя бы один профиль пользователя.</target> + <trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve"> + <source>The text you pasted is not a SimpleX link.</source> + <target>Вставленный текст не является SimpleX-ссылкой.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> - <source>There should be at least one visible user profile.</source> - <target>Должен быть хотя бы один открытый профиль пользователя.</target> + <trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve"> + <source>The uploaded database archive will be permanently removed from the servers.</source> + <target>Загруженный архив базы данных будет навсегда удален с серверов.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Themes" xml:space="preserve"> + <source>Themes</source> + <target>Темы</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="These conditions will also apply for: **%@**." xml:space="preserve"> + <source>These conditions will also apply for: **%@**.</source> + <target>Эти условия также будут применены к: **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> @@ -5074,19 +7785,38 @@ It can happen because of some bug or when the connection is compromised.</source <target>Это действие нельзя отменить — все сообщения, отправленные или полученные раньше чем выбрано, будут удалены. Это может занять несколько минут.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</source> + <target>Это действие нельзя отменить - сообщения в этом чате, отправленные или полученные раньше чем выбрано, будут удалены.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> <target>Это действие нельзя отменить — Ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by end-to-end encryption.</source> + <target>Чат защищен end-to-end шифрованием.</target> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by quantum resistant end-to-end encryption.</source> + <target>Чат защищен квантово-устойчивым end-to-end шифрованием.</target> + <note>E2EE info chat item</note> + </trans-unit> <trans-unit id="This device name" xml:space="preserve"> <source>This device name</source> <target>Имя этого устройства</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve"> + <source>This display name is invalid. Please choose another name.</source> + <target>Ошибка имени профиля. Пожалуйста, выберите другое имя.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> <source>This group has over %lld members, delivery receipts are not sent.</source> - <target>В группе более %lld членов, отчёты о доставке выключены.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This group no longer exists." xml:space="preserve"> @@ -5104,11 +7834,30 @@ It can happen because of some bug or when the connection is compromised.</source <target>Это ваша собственная одноразовая ссылка!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve"> + <source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve"> + <source>This link was used with another mobile device, please create a new link on the desktop.</source> + <target>Эта ссылка была использована на другом мобильном, пожалуйста, создайте новую ссылку на компьютере.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This message was deleted or not received yet." xml:space="preserve"> + <source>This message was deleted or not received yet.</source> + <target>Это сообщение было удалено или еще не получено.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <source>This setting applies to messages in your current chat profile **%@**.</source> <target>Эта настройка применяется к сообщениям в Вашем текущем профиле чата **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Title" xml:space="preserve"> + <source>Title</source> + <target>Заголовок</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> <source>To ask any questions and to receive updates:</source> <target>Чтобы задать вопросы и получать уведомления о новых версиях,</target> @@ -5129,9 +7878,9 @@ It can happen because of some bug or when the connection is compromised.</source <target>Чтобы соединиться</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <target>Чтобы защитить Вашу конфиденциальность, вместо ID пользователей, которые есть в других платформах, SimpleX использует ID для очередей сообщений, разные для каждого контакта.</target> + <trans-unit id="To protect against your link being replaced, you can compare contact security codes." xml:space="preserve"> + <source>To protect against your link being replaced, you can compare contact security codes.</source> + <target>Чтобы защитить Вашу ссылку от замены, Вы можете сравнить код безопасности.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -5139,6 +7888,11 @@ It can happen because of some bug or when the connection is compromised.</source <target>Чтобы защитить Ваш часовой пояс, файлы картинок и голосовых сообщений используют UTC.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve"> + <source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source> + <target>Чтобы защитить ваш IP адрес, приложение использует Ваши SMP серверы для конфиденциальной доставки сообщений.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> @@ -5146,6 +7900,26 @@ You will be prompted to complete authentication before this feature is enabled.< Вам будет нужно пройти аутентификацию для включения блокировки.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> + <target>Чтобы защитить Вашу конфиденциальность, SimpleX использует разные идентификаторы для каждого Вашeго контакта.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To receive" xml:space="preserve"> + <source>To receive</source> + <target>Для получения</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve"> + <source>To record speech please grant permission to use Microphone.</source> + <target>Для записи речи, пожалуйста, дайте разрешение на использование микрофона.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record video please grant permission to use Camera." xml:space="preserve"> + <source>To record video please grant permission to use Camera.</source> + <target>Для записи видео, пожалуйста, дайте разрешение на использование камеры.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> <target>Для записи голосового сообщения, пожалуйста разрешите доступ к микрофону.</target> @@ -5156,26 +7930,61 @@ You will be prompted to complete authentication before this feature is enabled.< <target>Чтобы показать Ваш скрытый профиль, введите его пароль в поле поиска на странице **Ваши профили чата**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To send" xml:space="preserve"> + <source>To send</source> + <target>Для оправки</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> <source>To support instant push notifications the chat database has to be migrated.</source> <target>Для поддержки мгновенный доставки уведомлений данные чата должны быть перемещены.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To use the servers of **%@**, accept conditions of use." xml:space="preserve"> + <source>To use the servers of **%@**, accept conditions of use.</source> + <target>Чтобы использовать серверы оператора **%@**, примите условия использования.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> <target>Чтобы подтвердить end-to-end шифрование с Вашим контактом сравните (или сканируйте) код безопасности на Ваших устройствах.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle chat list:" xml:space="preserve"> + <source>Toggle chat list:</source> + <target>Переключите список чатов:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> <source>Toggle incognito when connecting.</source> <target>Установите режим Инкогнито при соединении.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Token status: %@." xml:space="preserve"> + <source>Token status: %@.</source> + <target>Статус токена: %@.</target> + <note>token status</note> + </trans-unit> + <trans-unit id="Toolbar opacity" xml:space="preserve"> + <source>Toolbar opacity</source> + <target>Прозрачность тулбара</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Total" xml:space="preserve"> + <source>Total</source> + <target>Всего</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>Отдельные сессии для</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Transport sessions" xml:space="preserve"> + <source>Transport sessions</source> + <target>Транспортные сессии</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> <target>Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта (ошибка: %@).</target> @@ -5186,16 +7995,16 @@ You will be prompted to complete authentication before this feature is enabled.< <target>Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Turkish interface" xml:space="preserve"> + <source>Turkish interface</source> + <target>Турецкий интерфейс</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> <target>Выключить</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve"> - <source>Turn off notifications?</source> - <target>Выключить уведомления?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> <target>Включить</target> @@ -5211,20 +8020,27 @@ You will be prompted to complete authentication before this feature is enabled.< <target>Разблокировать</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock for all" xml:space="preserve"> + <source>Unblock for all</source> + <target>Разблокировать для всех</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member" xml:space="preserve"> <source>Unblock member</source> - <target>Разблокировать члена группы</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unblock member for all?" xml:space="preserve"> + <source>Unblock member for all?</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unblock member?" xml:space="preserve"> <source>Unblock member?</source> - <target>Разблокировать члена группы?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve"> - <source>Unexpected error: %@</source> - <target>Неожиданная ошибка: %@</target> - <note>item status description</note> + <trans-unit id="Undelivered messages" xml:space="preserve"> + <source>Undelivered messages</source> + <target>Недоставленные сообщения</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -5234,7 +8050,7 @@ You will be prompted to complete authentication before this feature is enabled.< <trans-unit id="Unfav." xml:space="preserve"> <source>Unfav.</source> <target>Не избр.</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> @@ -5271,6 +8087,11 @@ You will be prompted to complete authentication before this feature is enabled.< <target>Неизвестная ошибка</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unknown servers!" xml:space="preserve"> + <source>Unknown servers!</source> + <target>Неизвестные серверы!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> <target>Если Вы не используете интерфейс iOS, включите режим Не отвлекать, чтобы звонок не прерывался.</target> @@ -5306,11 +8127,19 @@ To connect, please ask your contact to create another connection link and check <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> <target>Уведомлять</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> </trans-unit> <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> <target>Не прочитано</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Unsupported connection link" xml:space="preserve"> + <source>Unsupported connection link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve"> + <source>Up to 100 last messages are sent to new members.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update" xml:space="preserve"> @@ -5318,11 +8147,6 @@ To connect, please ask your contact to create another connection link and check <target>Обновить</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> - <source>Update .onion hosts setting?</source> - <target>Обновить настройки .onion хостов?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> <target>Поменять пароль</target> @@ -5333,9 +8157,14 @@ To connect, please ask your contact to create another connection link and check <target>Обновить настройки сети?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve"> - <source>Update transport isolation mode?</source> - <target>Обновить режим отдельных сессий?</target> + <trans-unit id="Update settings?" xml:space="preserve"> + <source>Update settings?</source> + <target>Обновить настройки?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updated conditions" xml:space="preserve"> + <source>Updated conditions</source> + <target>Обновленные условия</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> @@ -5343,31 +8172,70 @@ To connect, please ask your contact to create another connection link and check <target>Обновление настроек приведет к сбросу и установке нового соединения со всеми серверами.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> - <target>Обновление этих настроек приведет к сбросу и установке нового соединения со всеми серверами.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Upgrade and open chat" xml:space="preserve"> <source>Upgrade and open chat</source> <target>Обновить и открыть чат</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Upload errors" xml:space="preserve"> + <source>Upload errors</source> + <target>Ошибки загрузки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload failed" xml:space="preserve"> + <source>Upload failed</source> + <target>Ошибка загрузки</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Upload file" xml:space="preserve"> <source>Upload file</source> <target>Загрузка файла</target> <note>server test step</note> </trans-unit> + <trans-unit id="Uploaded" xml:space="preserve"> + <source>Uploaded</source> + <target>Загружено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploaded files" xml:space="preserve"> + <source>Uploaded files</source> + <target>Отправленные файлы</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploading archive" xml:space="preserve"> + <source>Uploading archive</source> + <target>Загрузка архива</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use %@" xml:space="preserve"> + <source>Use %@</source> + <target>Использовать %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> <target>Использовать .onion хосты</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use SOCKS proxy" xml:space="preserve"> + <source>Use SOCKS proxy</source> + <target>Использовать SOCKS прокси</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> <target>Использовать серверы предосталенные SimpleX Chat?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use TCP port %@ when no port is specified." xml:space="preserve"> + <source>Use TCP port %@ when no port is specified.</source> + <target>Использовать TCP-порт %@, когда порт не указан.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve"> + <source>Use TCP port 443 for preset servers only.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> <target>Использовать чат</target> @@ -5378,6 +8246,16 @@ To connect, please ask your contact to create another connection link and check <target>Использовать активный профиль</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use for files" xml:space="preserve"> + <source>Use for files</source> + <target>Использовать для файлов</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for messages" xml:space="preserve"> + <source>Use for messages</source> + <target>Использовать для сообщений</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Использовать для новых соединений</target> @@ -5398,19 +8276,58 @@ To connect, please ask your contact to create another connection link and check <target>Использовать новый Инкогнито профиль</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use only local notifications?" xml:space="preserve"> + <source>Use only local notifications?</source> + <target>Использовать только локальные нотификации?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve"> + <source>Use private routing with unknown servers when IP address is not protected.</source> + <target>Использовать конфиденциальную доставку с неизвестными серверами, когда IP адрес не защищен.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers." xml:space="preserve"> + <source>Use private routing with unknown servers.</source> + <target>Использовать конфиденциальную доставку с неизвестными серверами.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Использовать сервер</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> - <target>Профиль чата</target> + <trans-unit id="Use servers" xml:space="preserve"> + <source>Use servers</source> + <target>Использовать серверы</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> - <target>Для использования .onion хостов требуется совместимый VPN провайдер.</target> + <trans-unit id="Use short links (BETA)" xml:space="preserve"> + <source>Use short links (BETA)</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app while in the call." xml:space="preserve"> + <source>Use the app while in the call.</source> + <target>Используйте приложение во время звонка.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app with one hand." xml:space="preserve"> + <source>Use the app with one hand.</source> + <target>Используйте приложение одной рукой.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use web port" xml:space="preserve"> + <source>Use web port</source> + <target>Использовать веб-порт</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="User selection" xml:space="preserve"> + <source>User selection</source> + <target>Выбор пользователя</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Username" xml:space="preserve"> + <source>Username</source> + <target>Имя пользователя</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> @@ -5438,6 +8355,16 @@ To connect, please ask your contact to create another connection link and check <target>Проверять соединения</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Verify database passphrase" xml:space="preserve"> + <source>Verify database passphrase</source> + <target>Проверка пароля базы данных</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify passphrase" xml:space="preserve"> + <source>Verify passphrase</source> + <target>Проверить пароль</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Verify security code" xml:space="preserve"> <source>Verify security code</source> <target>Подтвердить код безопасности</target> @@ -5473,11 +8400,26 @@ To connect, please ask your contact to create another connection link and check <target>Видео и файлы до 1гб</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View conditions" xml:space="preserve"> + <source>View conditions</source> + <target>Посмотреть условия</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> <target>Показать код безопасности</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View updated conditions" xml:space="preserve"> + <source>View updated conditions</source> + <target>Посмотреть измененные условия</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Visible history" xml:space="preserve"> + <source>Visible history</source> + <target>Доступ к истории</target> + <note>chat feature</note> + </trans-unit> <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> <target>Голосовые сообщения</target> @@ -5488,11 +8430,16 @@ To connect, please ask your contact to create another connection link and check <target>Голосовые сообщения запрещены в этом чате.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <target>Голосовые сообщения запрещены в этой группе.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Voice messages not allowed" xml:space="preserve"> + <source>Voice messages not allowed</source> + <target>Голосовые сообщения не разрешены</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> <source>Voice messages prohibited!</source> <target>Голосовые сообщения запрещены!</target> @@ -5523,6 +8470,21 @@ To connect, please ask your contact to create another connection link and check <target>Ожидание видео</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wallpaper accent" xml:space="preserve"> + <source>Wallpaper accent</source> + <target>Рисунок обоев</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper background" xml:space="preserve"> + <source>Wallpaper background</source> + <target>Фон обоев</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve"> + <source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source> + <target>Внимание: запуск чата на нескольких устройствах не поддерживается и приведет к сбоям доставки сообщений</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> <source>Warning: you may lose some data!</source> <target>Предупреждение: Вы можете потерять какие то данные!</target> @@ -5543,6 +8505,11 @@ To connect, please ask your contact to create another connection link and check <target>Приветственное сообщение</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Welcome message is too long" xml:space="preserve"> + <source>Welcome message is too long</source> + <target>Приветственное сообщение слишком длинное</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="What's new" xml:space="preserve"> <source>What's new</source> <target>Новые функции</target> @@ -5553,9 +8520,14 @@ To connect, please ask your contact to create another connection link and check <target>Когда возможно</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> - <source>When people request to connect, you can accept or reject it.</source> - <target>Когда Вы получите запрос на соединение, Вы можете принять или отклонить его.</target> + <trans-unit id="When connecting audio and video calls." xml:space="preserve"> + <source>When connecting audio and video calls.</source> + <target>Во время соединения аудио и видео звонков.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When more than one operator is enabled, none of them has metadata to learn who communicates with whom." xml:space="preserve"> + <source>When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</source> + <target>Когда больше чем один оператор включен, ни один из них не видит метаданные, чтобы определить, кто соединен с кем.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> @@ -5563,29 +8535,74 @@ To connect, please ask your contact to create another connection link and check <target>Когда Вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="WiFi" xml:space="preserve"> + <source>WiFi</source> + <target>WiFi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Will be enabled in direct chats!" xml:space="preserve"> + <source>Will be enabled in direct chats!</source> + <target>Будет включено в прямых разговорах!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wired ethernet" xml:space="preserve"> + <source>Wired ethernet</source> + <target>Проводная сеть</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With encrypted files and media." xml:space="preserve"> + <source>With encrypted files and media.</source> + <target>С зашифрованными файлами и медиа.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> <target>С опциональным авто-ответом.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="With reduced battery usage." xml:space="preserve"> + <source>With reduced battery usage.</source> + <target>С уменьшенным потреблением батареи.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to file servers.</source> + <target>Без Тора или ВПН, Ваш IP адрес будет доступен серверам файлов.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source> + <target>Без Тора или ВПН, Ваш IP адрес будет доступен этим серверам файлов: %@.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> <target>Неправильный пароль базы данных</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve"> + <source>Wrong key or unknown connection - most likely this connection is deleted.</source> + <target>Неверный ключ или неизвестное соединение - скорее всего, это соединение удалено.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Wrong key or unknown file chunk address - most likely file is deleted." xml:space="preserve"> + <source>Wrong key or unknown file chunk address - most likely file is deleted.</source> + <target>Неверный ключ или неизвестный адрес блока файла - скорее всего, файл удален.</target> + <note>file error text</note> + </trans-unit> <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> <target>Неправильный пароль!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="XFTP servers" xml:space="preserve"> - <source>XFTP servers</source> - <target>XFTP серверы</target> + <trans-unit id="XFTP server" xml:space="preserve"> + <source>XFTP server</source> + <target>XFTP сервер</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> - <target>Вы</target> + <trans-unit id="You **must not** use the same database on two devices." xml:space="preserve"> + <source>You **must not** use the same database on two devices.</source> + <target>Вы **не должны** использовать одну и ту же базу данных на двух устройствах.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You accepted connection" xml:space="preserve"> @@ -5608,6 +8625,11 @@ To connect, please ask your contact to create another connection link and check <target>Вы уже соединены с контактом %@.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are already connected with %@." xml:space="preserve"> + <source>You are already connected with %@.</source> + <target>Вы уже соединены с %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You are already connecting to %@." xml:space="preserve"> <source>You are already connecting to %@.</source> <target>Вы уже соединяетесь с %@.</target> @@ -5655,14 +8677,24 @@ Repeat join request?</source> <target>Вы приглашены в группу</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are not connected to these servers. Private routing is used to deliver messages to them." xml:space="preserve"> + <source>You are not connected to these servers. Private routing is used to deliver messages to them.</source> + <target>Вы не подключены к этим серверам. Для доставки сообщений на них используется конфиденциальная доставка.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> <source>You can accept calls from lock screen, without device and app authentication.</source> <target>Вы можете принимать звонки на экране блокировки, без аутентификации.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <target>Вы также можете соединиться, открыв ссылку. Если ссылка откроется в браузере, нажмите кнопку **Open in mobile app**.</target> + <trans-unit id="You can change it in Appearance settings." xml:space="preserve"> + <source>You can change it in Appearance settings.</source> + <target>Вы можете изменить это в настройках Интерфейса.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can configure servers via settings." xml:space="preserve"> + <source>You can configure servers via settings.</source> + <target>Вы можете настроить серверы позже.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can create it later" xml:space="preserve"> @@ -5680,16 +8712,36 @@ Repeat join request?</source> <target>Вы можете включить их позже в настройках Конфиденциальности.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can give another try." xml:space="preserve"> + <source>You can give another try.</source> + <target>Вы можете попробовать еще раз.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <source>You can hide or mute a user profile - swipe it to the right.</source> <target>Вы можете скрыть профиль или выключить уведомления - потяните его вправо.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> - <target>Вы теперь можете отправлять сообщения %@</target> + <trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve"> + <source>You can make it visible to your SimpleX contacts via Settings.</source> + <target>Вы можете сделать его видимым для ваших контактов в SimpleX через Настройки.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now chat with %@" xml:space="preserve"> + <source>You can now chat with %@</source> + <target>Вы теперь можете общаться с %@</target> <note>notification body</note> </trans-unit> + <trans-unit id="You can send messages to %@ from Archived contacts." xml:space="preserve"> + <source>You can send messages to %@ from Archived contacts.</source> + <target>Вы можете отправлять сообщения %@ из Архивированных контактов.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can set connection name, to remember who the link was shared with." xml:space="preserve"> + <source>You can set connection name, to remember who the link was shared with.</source> + <target>Вы можете установить имя соединения, чтобы запомнить кому Вы отправили ссылку.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> <target>Вы можете установить просмотр уведомлений на экране блокировки в настройках.</target> @@ -5697,7 +8749,6 @@ Repeat join request?</source> </trans-unit> <trans-unit id="You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." xml:space="preserve"> <source>You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.</source> - <target>Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can share this address with your contacts to let them connect with **%@**." xml:space="preserve"> @@ -5705,16 +8756,16 @@ Repeat join request?</source> <target>Вы можете поделиться этим адресом с Вашими контактами, чтобы они могли соединиться с **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> - <source>You can share your address as a link or QR code - anybody can connect to you.</source> - <target>Вы можете использовать Ваш адрес как ссылку или как QR код - кто угодно сможет соединиться с Вами.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> <target>Вы можете запустить чат через Настройки приложения или перезапустив приложение.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve"> + <source>You can still view conversation with %@ in the list of chats.</source> + <target>Вы по-прежнему можете просмотреть разговор с %@ в списке чатов.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> <target>Вы можете включить Блокировку SimpleX через Настройки.</target> @@ -5725,21 +8776,26 @@ Repeat join request?</source> <target>Вы можете форматировать сообщения:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can view invitation link again in connection details." xml:space="preserve"> + <source>You can view invitation link again in connection details.</source> + <target>Вы можете увидеть ссылку-приглашение снова открыв соединение.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> <target>Вы не можете отправлять сообщения!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <target>Вы определяете через какие серверы Вы **получаете сообщения**, Ваши контакты - серверы, которые Вы используете для отправки.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <target>Верификация не удалась; пожалуйста, попробуйте ещё раз.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> + <target>Вы определяете, кто может соединиться.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You have already requested connection via this address!" xml:space="preserve"> <source>You have already requested connection via this address!</source> <target>Вы уже запросили соединение через этот адрес!</target> @@ -5752,11 +8808,6 @@ Repeat connection request?</source> Повторить запрос?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> - <target>У Вас нет чатов</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> <target>Пароль не сохранен на устройстве — Вы будете должны ввести его при каждом запуске чата.</target> @@ -5774,7 +8825,16 @@ Repeat connection request?</source> </trans-unit> <trans-unit id="You joined this group. Connecting to inviting group member." xml:space="preserve"> <source>You joined this group. Connecting to inviting group member.</source> - <target>Вы вступили в эту группу. Устанавливается соединение с пригласившим членом группы.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may migrate the exported database." xml:space="preserve"> + <source>You may migrate the exported database.</source> + <target>Вы можете мигрировать экспортированную базу данных.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may save the exported archive." xml:space="preserve"> + <source>You may save the exported archive.</source> + <target>Вы можете сохранить экспортированный архив.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> @@ -5782,6 +8842,11 @@ Repeat connection request?</source> <target>Вы должны всегда использовать самую новую версию данных чата, ТОЛЬКО на одном устройстве, иначе Вы можете перестать получать сообщения от каких то контактов.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You need to allow your contact to call to be able to call them." xml:space="preserve"> + <source>You need to allow your contact to call to be able to call them.</source> + <target>Чтобы включить звонки, разрешите их Вашему контакту.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> <target>Чтобы включить отправку голосовых сообщений, разрешите их Вашему контакту.</target> @@ -5797,6 +8862,11 @@ Repeat connection request?</source> <target>Вы отправили приглашение в группу</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You should receive notifications." xml:space="preserve"> + <source>You should receive notifications.</source> + <target>Вы должны получать уведомления.</target> + <note>token info</note> + </trans-unit> <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> <target>Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже!</target> @@ -5824,7 +8894,6 @@ Repeat connection request?</source> </trans-unit> <trans-unit id="You will connect to all group members." xml:space="preserve"> <source>You will connect to all group members.</source> - <target>Вы соединитесь со всеми членами группы.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve"> @@ -5832,6 +8901,11 @@ Repeat connection request?</source> <target>Вы все равно получите звонки и уведомления в профилях без звука, когда они активные.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You will stop receiving messages from this chat. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this chat. Chat history will be preserved.</source> + <target>Вы прекратите получать сообщения в этом разговоре. История будет сохранена.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> <target>Вы перестанете получать сообщения от этой группы. История чата будет сохранена.</target> @@ -5852,31 +8926,16 @@ Repeat connection request?</source> <target>Вы используете инкогнито профиль для этой группы - чтобы предотвратить раскрытие Вашего основного профиля, приглашать контакты не разрешено</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your %@ servers" xml:space="preserve"> - <source>Your %@ servers</source> - <target>Ваши %@ серверы</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your ICE servers" xml:space="preserve"> <source>Your ICE servers</source> <target>Ваши ICE серверы</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> - <target>Ваши SMP серверы</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your SimpleX address" xml:space="preserve"> <source>Your SimpleX address</source> <target>Ваш адрес SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your XFTP servers" xml:space="preserve"> - <source>Your XFTP servers</source> - <target>Ваши XFTP серверы</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your calls" xml:space="preserve"> <source>Your calls</source> <target>Ваши звонки</target> @@ -5892,16 +8951,19 @@ Repeat connection request?</source> <target>База данных НЕ зашифрована. Установите пароль, чтобы защитить Ваши данные.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your chat preferences" xml:space="preserve"> + <source>Your chat preferences</source> + <target>Ваши настройки чата</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>Ваши профили чата</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> - <target>Ваш контакт должен быть в сети чтобы установить соединение. -Вы можете отменить соединение и удалить контакт (и попробовать позже с другой ссылкой).</target> + <trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve"> + <source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source> + <target>Соединение было перемещено на %@, но при смене профиля произошла неожиданная ошибка.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> @@ -5914,18 +8976,16 @@ You can cancel this connection and remove the contact (and try later with a new <target>Ваши контакты могут разрешить окончательное удаление сообщений.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> - <source>Your contacts in SimpleX will see it. -You can change it in Settings.</source> - <target>Ваши контакты в SimpleX получат этот адрес. -Вы можете изменить это в Настройках.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your contacts will remain connected." xml:space="preserve"> <source>Your contacts will remain connected.</source> <target>Ваши контакты сохранятся.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve"> + <source>Your credentials may be sent unencrypted.</source> + <target>Ваши учетные данные могут быть отправлены в незашифрованном виде.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> <target>Текущие данные Вашего чата будет УДАЛЕНЫ и ЗАМЕНЕНЫ импортированными.</target> @@ -5956,33 +9016,36 @@ You can change it in Settings.</source> <target>Будет отправлен Ваш профиль **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> - <target>Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам. -SimpleX серверы не могут получить доступ к Вашему профилю.</target> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> + <target>Ваш профиль храниться на Вашем устройстве и отправляется только контактам.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> - <target>Ваш профиль, контакты и доставленные сообщения хранятся на Вашем устройстве.</target> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> + <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> + <target>Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам. SimpleX серверы не могут получить доступ к Вашему профилю.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve"> + <source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source> + <target>Ваш профиль был изменен. Если вы сохраните его, обновленный профиль будет отправлен всем вашим контактам.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> <target>Случайный профиль</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> - <target>Ваш сервер</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> <target>Адрес Вашего сервера</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your servers" xml:space="preserve"> + <source>Your servers</source> + <target>Ваши серверы</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your settings" xml:space="preserve"> <source>Your settings</source> <target>Настройки</target> @@ -6023,11 +9086,21 @@ SimpleX серверы не могут получить доступ к Ваше <target>принятый звонок</target> <note>call status</note> </trans-unit> + <trans-unit id="accepted invitation" xml:space="preserve"> + <source>accepted invitation</source> + <target>принятое приглашение</target> + <note>chat list item title</note> + </trans-unit> <trans-unit id="admin" xml:space="preserve"> <source>admin</source> <target>админ</target> <note>member role</note> </trans-unit> + <trans-unit id="admins" xml:space="preserve"> + <source>admins</source> + <target>админы</target> + <note>feature role</note> + </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> <target>шифрование согласовывается для %@…</target> @@ -6038,6 +9111,10 @@ SimpleX серверы не могут получить доступ к Ваше <target>шифрование согласовывается…</target> <note>chat item text</note> </trans-unit> + <trans-unit id="all members" xml:space="preserve"> + <source>all members</source> + <note>feature role</note> + </trans-unit> <trans-unit id="always" xml:space="preserve"> <source>always</source> <target>всегда</target> @@ -6048,6 +9125,16 @@ SimpleX серверы не могут получить доступ к Ваше <target>и %lld других событий</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="archived report" xml:space="preserve"> + <source>archived report</source> + <target>заархивированное сообщение о нарушении</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="attempts" xml:space="preserve"> + <source>attempts</source> + <target>попытки</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <source>audio call (not e2e encrypted)</source> <target>аудиозвонок (не e2e зашифрованный)</target> @@ -6071,13 +9158,29 @@ SimpleX серверы не могут получить доступ к Ваше <trans-unit id="blocked" xml:space="preserve"> <source>blocked</source> <target>заблокировано</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="blocked %@" xml:space="preserve"> + <source>blocked %@</source> + <target>%@ заблокирован</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="blocked by admin" xml:space="preserve"> + <source>blocked by admin</source> + <target>заблокировано администратором</target> + <note>blocked chat item +marked deleted chat item preview text</note> </trans-unit> <trans-unit id="bold" xml:space="preserve"> <source>bold</source> <target>жирный</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="call" xml:space="preserve"> + <source>call</source> + <target>звонок</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="call error" xml:space="preserve"> <source>call error</source> <target>ошибка звонка</target> @@ -6181,7 +9284,7 @@ SimpleX серверы не могут получить доступ к Ваше <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> <target>соединяется…</target> - <note>chat list item title</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> @@ -6193,6 +9296,11 @@ SimpleX серверы не могут получить доступ к Ваше <target>connection:%@</target> <note>connection information</note> </trans-unit> + <trans-unit id="contact %@ changed to %@" xml:space="preserve"> + <source>contact %1$@ changed to %2$@</source> + <target>контакт %1$@ изменён на %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> <target>у контакта есть e2e шифрование</target> @@ -6223,10 +9331,16 @@ SimpleX серверы не могут получить доступ к Ваше <target>дней</target> <note>time unit</note> </trans-unit> + <trans-unit id="decryption errors" xml:space="preserve"> + <source>decryption errors</source> + <target>ошибки расшифровки</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> <target>по умолчанию (%@)</target> - <note>pref value</note> + <note>delete after time +pref value</note> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> @@ -6273,6 +9387,11 @@ SimpleX серверы не могут получить доступ к Ваше <target>повторное сообщение</target> <note>integrity error chat item</note> </trans-unit> + <trans-unit id="duplicates" xml:space="preserve"> + <source>duplicates</source> + <target>дубликаты</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> <target>e2e зашифровано</target> @@ -6348,9 +9467,14 @@ SimpleX серверы не могут получить доступ к Ваше <target>ошибка</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="event happened" xml:space="preserve"> - <source>event happened</source> - <target>событие произошло</target> + <trans-unit id="expired" xml:space="preserve"> + <source>expired</source> + <target>истекло</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="forwarded" xml:space="preserve"> + <source>forwarded</source> + <target>переслано</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> @@ -6378,6 +9502,11 @@ SimpleX серверы не могут получить доступ к Ваше <target>Пароль базы данных будет безопасно сохранен в iOS Keychain после запуска чата или изменения пароля - это позволит получать мгновенные уведомления.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="inactive" xml:space="preserve"> + <source>inactive</source> + <target>неактивен</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> <target>инкогнито через ссылку-контакт</target> @@ -6418,6 +9547,11 @@ SimpleX серверы не могут получить доступ к Ваше <target>приглашение в группу %@</target> <note>group name</note> </trans-unit> + <trans-unit id="invite" xml:space="preserve"> + <source>invite</source> + <target>пригласить</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> <target>приглашен(а)</target> @@ -6460,14 +9594,22 @@ SimpleX серверы не могут получить доступ к Ваше </trans-unit> <trans-unit id="member" xml:space="preserve"> <source>member</source> - <target>член группы</target> <note>member role</note> </trans-unit> + <trans-unit id="member %@ changed to %@" xml:space="preserve"> + <source>member %1$@ changed to %2$@</source> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> <target>соединен(а)</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="message" xml:space="preserve"> + <source>message</source> + <target>написать</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="message received" xml:space="preserve"> <source>message received</source> <target>получено сообщение</target> @@ -6491,7 +9633,12 @@ SimpleX серверы не могут получить доступ к Ваше <trans-unit id="moderated by %@" xml:space="preserve"> <source>moderated by %@</source> <target>удалено %@</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="moderator" xml:space="preserve"> + <source>moderator</source> + <target>модератор</target> + <note>member role</note> </trans-unit> <trans-unit id="months" xml:space="preserve"> <source>months</source> @@ -6501,7 +9648,7 @@ SimpleX серверы не могут получить доступ к Ваше <trans-unit id="never" xml:space="preserve"> <source>never</source> <target>никогда</target> - <note>No comment provided by engineer.</note> + <note>delete after time</note> </trans-unit> <trans-unit id="new message" xml:space="preserve"> <source>new message</source> @@ -6532,8 +9679,8 @@ SimpleX серверы не могут получить доступ к Ваше <source>off</source> <target>нет</target> <note>enabled status - group pref value - time to disappear</note> +group pref value +time to disappear</note> </trans-unit> <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> @@ -6550,16 +9697,46 @@ SimpleX серверы не могут получить доступ к Ваше <target>да</target> <note>group pref value</note> </trans-unit> + <trans-unit id="other" xml:space="preserve"> + <source>other</source> + <target>другое</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="other errors" xml:space="preserve"> + <source>other errors</source> + <target>другие ошибки</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="owner" xml:space="preserve"> <source>owner</source> <target>владелец</target> <note>member role</note> </trans-unit> + <trans-unit id="owners" xml:space="preserve"> + <source>owners</source> + <target>владельцы</target> + <note>feature role</note> + </trans-unit> <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> <target>peer-to-peer</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="pending" xml:space="preserve"> + <source>pending</source> + <target>ожидает</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="pending approval" xml:space="preserve"> + <source>pending approval</source> + <target>ожидает утверждения</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="quantum resistant e2e encryption" xml:space="preserve"> + <source>quantum resistant e2e encryption</source> + <target>квантово-устойчивое e2e шифрование</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> <target>получен ответ…</target> @@ -6570,6 +9747,11 @@ SimpleX серверы не могут получить доступ к Ваше <target>получено подтверждение…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="rejected" xml:space="preserve"> + <source>rejected</source> + <target>отклонён</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> <target>отклонённый звонок</target> @@ -6585,11 +9767,41 @@ SimpleX серверы не могут получить доступ к Ваше <target>удалил(а) %@</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="removed contact address" xml:space="preserve"> + <source>removed contact address</source> + <target>удалён адрес контакта</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="removed profile picture" xml:space="preserve"> + <source>removed profile picture</source> + <target>удалена картинка профиля</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> <target>удалил(а) Вас из группы</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="requested to connect" xml:space="preserve"> + <source>requested to connect</source> + <target>запрошено соединение</target> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="saved" xml:space="preserve"> + <source>saved</source> + <target>сохранено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="saved from %@" xml:space="preserve"> + <source>saved from %@</source> + <target>сохранено из %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="search" xml:space="preserve"> + <source>search</source> + <target>поиск</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="sec" xml:space="preserve"> <source>sec</source> <target>сек</target> @@ -6615,6 +9827,30 @@ SimpleX серверы не могут получить доступ к Ваше <target>отправьте сообщение</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve"> + <source>server queue info: %1$@ + +last received msg: %2$@</source> + <target>информация сервера об очереди: %1$@ + +последнее полученное сообщение: %2$@</target> + <note>queue info</note> + </trans-unit> + <trans-unit id="set new contact address" xml:space="preserve"> + <source>set new contact address</source> + <target>установлен новый адрес контакта</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="set new profile picture" xml:space="preserve"> + <source>set new profile picture</source> + <target>установлена новая картинка профиля</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="standard end-to-end encryption" xml:space="preserve"> + <source>standard end-to-end encryption</source> + <target>стандартное end-to-end шифрование</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>инициализация…</target> @@ -6630,16 +9866,41 @@ SimpleX серверы не могут получить доступ к Ваше <target>этот контакт</target> <note>notification title</note> </trans-unit> + <trans-unit id="unblocked %@" xml:space="preserve"> + <source>unblocked %@</source> + <target>%@ разблокирован</target> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="unknown" xml:space="preserve"> <source>unknown</source> <target>неизвестно</target> <note>connection info</note> </trans-unit> + <trans-unit id="unknown servers" xml:space="preserve"> + <source>unknown servers</source> + <target>неизвестные серверы</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unknown status" xml:space="preserve"> + <source>unknown status</source> + <target>неизвестный статус</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unprotected" xml:space="preserve"> + <source>unprotected</source> + <target>незащищённый</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="updated group profile" xml:space="preserve"> <source>updated group profile</source> <target>обновил(а) профиль группы</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="updated profile" xml:space="preserve"> + <source>updated profile</source> + <target>профиль обновлён</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="v%@" xml:space="preserve"> <source>v%@</source> <target>v%@</target> @@ -6670,6 +9931,11 @@ SimpleX серверы не могут получить доступ к Ваше <target>через relay сервер</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="video" xml:space="preserve"> + <source>video</source> + <target>видеозвонок</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> <source>video call (not e2e encrypted)</source> <target>видеозвонок (не e2e зашифрованный)</target> @@ -6695,11 +9961,21 @@ SimpleX серверы не могут получить доступ к Ваше <target>недель</target> <note>time unit</note> </trans-unit> + <trans-unit id="when IP hidden" xml:space="preserve"> + <source>when IP hidden</source> + <target>когда IP защищен</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="yes" xml:space="preserve"> <source>yes</source> <target>да</target> <note>pref value</note> </trans-unit> + <trans-unit id="you" xml:space="preserve"> + <source>you</source> + <target>Вы</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> <target>Вы приглашены в группу</target> @@ -6710,6 +9986,11 @@ SimpleX серверы не могут получить доступ к Ваше <target>только чтение сообщений</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="you blocked %@" xml:space="preserve"> + <source>you blocked %@</source> + <target>Вы заблокировали %@</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> <target>Вы поменяли адрес</target> @@ -6750,6 +10031,11 @@ SimpleX серверы не могут получить доступ к Ваше <target>Вы создали ссылку инкогнито</target> <note>chat list item description</note> </trans-unit> + <trans-unit id="you unblocked %@" xml:space="preserve"> + <source>you unblocked %@</source> + <target>Вы разблокировали %@</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you: " xml:space="preserve"> <source>you: </source> <target>Вы: </target> @@ -6764,7 +10050,7 @@ SimpleX серверы не могут получить доступ к Ваше </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="ru" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6801,7 +10087,7 @@ SimpleX серверы не могут получить доступ к Ваше </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="ru" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> @@ -6821,4 +10107,249 @@ SimpleX серверы не могут получить доступ к Ваше </trans-unit> </body> </file> + <file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="ru" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%d new events" xml:space="preserve"> + <source>%d new events</source> + <target>%d новых сообщений</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="From %d chat(s)" xml:space="preserve"> + <source>From %d chat(s)</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From: %@" xml:space="preserve"> + <source>From: %@</source> + <target>От: %@</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>Новые события</target> + <note>notification</note> + </trans-unit> + <trans-unit id="New messages" xml:space="preserve"> + <source>New messages</source> + <target>Новые сообщения</target> + <note>notification</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="ru" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle display name</note> + </trans-unit> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> + <source>Copyright © 2024 SimpleX Chat. All rights reserved.</source> + <target>Copyright © 2024 SimpleX Chat. Все права защищены.</target> + <note>Copyright (human-readable)</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="ru" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <target>%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App is locked!" xml:space="preserve"> + <source>App is locked!</source> + <target>Приложение заблокировано!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <target>Отменить</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <target>Невозможно сохранить пароль в keychain</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Невозможно переслать сообщение</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Comment" xml:space="preserve"> + <source>Comment</source> + <target>Комментарий</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <target>В настоящее время максимальный поддерживаемый размер файла составляет %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade required" xml:space="preserve"> + <source>Database downgrade required</source> + <target>Требуется откат базы данных</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <target>База данных зашифрована!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <target>Ошибка базы данных</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <target>Пароль базы данных отличается от сохраненного в keychain.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <target>Введите пароль базы данных, чтобы открыть чат.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade required" xml:space="preserve"> + <source>Database upgrade required</source> + <target>Требуется обновление базы данных</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing file" xml:space="preserve"> + <source>Error preparing file</source> + <target>Ошибка подготовки файла</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing message" xml:space="preserve"> + <source>Error preparing message</source> + <target>Ошибка подготовки сообщения</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <target>Ошибка: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Ошибка файла</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <target>Несовместимая версия базы данных</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Ошибка подтверждения миграции</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <target>Ошибка keychain</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <target>Большой файл!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No active profile" xml:space="preserve"> + <source>No active profile</source> + <target>Нет активного профиля</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <target>Ок</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to downgrade the database." xml:space="preserve"> + <source>Open the app to downgrade the database.</source> + <target>Откройте приложение, чтобы откатить базу данных.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to upgrade the database." xml:space="preserve"> + <source>Open the app to upgrade the database.</source> + <target>Откройте приложение, чтобы обновить базу данных.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passphrase" xml:space="preserve"> + <source>Passphrase</source> + <target>Пароль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve"> + <source>Please create a profile in the SimpleX app</source> + <target>Пожалуйста, создайте профиль в приложении SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Выбранные настройки чата запрещают это сообщение.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending a message takes longer than expected." xml:space="preserve"> + <source>Sending a message takes longer than expected.</source> + <target>Отправка сообщения занимает дольше ожиданного.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending message…" xml:space="preserve"> + <source>Sending message…</source> + <target>Отправка сообщения…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <target>Поделиться</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Slow network?" xml:space="preserve"> + <source>Slow network?</source> + <target>Медленная сеть?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <target>Неизвестная ошибка базы данных: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unsupported format" xml:space="preserve"> + <source>Unsupported format</source> + <target>Неподдерживаемый формат</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wait" xml:space="preserve"> + <source>Wait</source> + <target>Подождать</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <target>Неправильный пароль базы данных</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve"> + <source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source> + <target>Вы можете разрешить функцию Поделиться в настройках Конфиденциальности / Блокировка SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + </body> + </file> </xliff> diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/contents.json b/apps/ios/SimpleX Localizations/ru.xcloc/contents.json index 2d5d76dd8f..b49b25d653 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/ru.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "ru", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "16.2" }, "version" : "1.0" } \ No newline at end of file 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 bcb39e6e03..671dd87d7d 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -2,36 +2,9 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="th" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> - <trans-unit id=" " xml:space="preserve"> - <source> -</source> - <target> -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (" xml:space="preserve"> - <source> (</source> - <target> (</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> <target> (สามารถคัดลอกได้)</target> @@ -101,6 +74,10 @@ <source>%@ connected</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve"> + <source>%@ downloaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ is connected!" xml:space="preserve"> <source>%@ is connected!</source> <target>%@ เชื่อมต่อสำเร็จ!</target> @@ -116,9 +93,16 @@ <target>%@ ได้รับการตรวจสอบแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ server" xml:space="preserve"> + <source>%@ server</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ servers" xml:space="preserve"> <source>%@ servers</source> - <target>%@ เซิร์ฟเวอร์</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve"> + <source>%@ uploaded</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ wants to connect!" xml:space="preserve"> @@ -126,6 +110,10 @@ <target>%@ อยากเชื่อมต่อ!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve"> + <source>%1$@, %2$@</source> + <note>format for date separator in chat</note> + </trans-unit> <trans-unit id="%@, %@ and %lld members" xml:space="preserve"> <source>%@, %@ and %lld members</source> <note>No comment provided by engineer.</note> @@ -144,11 +132,31 @@ <target>%d วัน</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve"> + <source>%d file(s) are still being downloaded.</source> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve"> + <source>%d file(s) failed to download.</source> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve"> + <source>%d file(s) were deleted.</source> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve"> + <source>%d file(s) were not downloaded.</source> + <note>forward confirmation reason</note> + </trans-unit> <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> <target>%d ชั่วโมง</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve"> + <source>%d messages not forwarded</source> + <note>alert title</note> + </trans-unit> <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> <target>%d นาที</target> @@ -164,6 +172,10 @@ <target>%d วินาที</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve"> + <source>%d seconds(s)</source> + <note>delete after time</note> + </trans-unit> <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> <target>%d ข้อความที่ถูกข้าม</target> @@ -207,6 +219,10 @@ <source>%lld messages blocked</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve"> + <source>%lld messages blocked by admin</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld messages marked deleted" xml:space="preserve"> <source>%lld messages marked deleted</source> <note>No comment provided by engineer.</note> @@ -224,11 +240,6 @@ <source>%lld new interface languages</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> - <source>%lld second(s)</source> - <target>%lld วินาที</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="%lld seconds" xml:space="preserve"> <source>%lld seconds</source> <target>%lld วินาที</target> @@ -279,11 +290,6 @@ <target>%u ข้อความที่ถูกข้าม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> - <source>(</source> - <target>(</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="(new)" xml:space="preserve"> <source>(new)</source> <note>No comment provided by engineer.</note> @@ -292,34 +298,26 @@ <source>(this device v%@)</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve"> - <source>)</source> - <target>)</target> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target>**เพิ่มผู้ติดต่อใหม่**: เพื่อสร้างคิวอาร์โค้ดแบบใช้ครั้งเดียวหรือลิงก์สำหรับผู้ติดต่อของคุณ</target> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve"> + <source>**Create group**: to create a new group.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> - <source>**Create link / QR code** for your contact to use.</source> - <target>**สร้างลิงค์ / คิวอาร์โค้ด** เพื่อให้ผู้ติดต่อของคุณใช้</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**เป็นส่วนตัวมากขึ้น**: ตรวจสอบข้อความใหม่ทุกๆ 20 นาที โทเค็นอุปกรณ์แชร์กับเซิร์ฟเวอร์ SimpleX Chat แต่ไม่ระบุจำนวนผู้ติดต่อหรือข้อความที่คุณมี</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**ส่วนตัวที่สุด**: ไม่ใช้เซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat ตรวจสอบข้อความเป็นระยะในพื้นหลัง (ขึ้นอยู่กับความถี่ที่คุณใช้แอป)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <target>**แปะลิงก์ที่ได้รับ** หรือเปิดในเบราว์เซอร์แล้วแตะ **เปิดในแอปมือถือ**</target> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> @@ -327,14 +325,13 @@ <target>**โปรดทราบ**: คุณจะไม่สามารถกู้คืนหรือเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**แนะนำ**: โทเค็นอุปกรณ์และการแจ้งเตือนจะถูกส่งไปยังเซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat แต่ไม่ใช่เนื้อหาข้อความ ขนาด หรือผู้ที่ส่ง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target>**สแกนคิวอาร์โค้ด**: เพื่อเชื่อมต่อกับผู้ติดต่อของคุณด้วยตนเองหรือผ่านการสนทนาทางวิดีโอ</target> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve"> + <source>**Scan / Paste link**: to connect via a link you received.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> @@ -342,6 +339,10 @@ <target>**คำเตือน**: การแจ้งเตือนแบบพุชทันทีจำเป็นต้องบันทึกรหัสผ่านไว้ใน Keychain</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve"> + <source>**Warning**: the archive will be removed.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> <source>**e2e encrypted** audio call</source> <target>การโทรเสียงแบบ **encrypted จากต้นจนจบ**</target> @@ -357,11 +358,6 @@ <target>\*ตัวหนา*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=", " xml:space="preserve"> - <source>, </source> - <target>, </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). @@ -392,11 +388,6 @@ - ประวัติการแก้ไข</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="." xml:space="preserve"> - <source>.</source> - <target>.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="0 sec" xml:space="preserve"> <source>0 sec</source> <note>time to disappear</note> @@ -409,7 +400,8 @@ <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> <target>1 วัน</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> @@ -424,16 +416,25 @@ <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> <target>1 เดือน</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> <target>1 สัปดาห์</target> - <note>time interval</note> + <note>delete after time +time interval</note> + </trans-unit> + <trans-unit id="1 year" xml:space="preserve"> + <source>1 year</source> + <note>delete after time</note> </trans-unit> <trans-unit id="1-time link" xml:space="preserve"> <source>1-time link</source> - <target>ลิงก์สำหรับใช้ 1 ครั้ง</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="5 minutes" xml:space="preserve"> @@ -451,11 +452,6 @@ <target>30 วินาที</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve"> - <source>: </source> - <target>: </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> @@ -504,31 +500,29 @@ <target>ยกเลิกการเปลี่ยนที่อยู่?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve"> - <source>About SimpleX</source> - <target>เกี่ยวกับ SimpleX</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve"> <source>About SimpleX Chat</source> <target>เกี่ยวกับ SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve"> - <source>About SimpleX address</source> - <target>เกี่ยวกับที่อยู่ SimpleX</target> + <trans-unit id="About operators" xml:space="preserve"> + <source>About operators</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accent color" xml:space="preserve"> - <source>Accent color</source> - <target>สีเน้น</target> + <trans-unit id="Accent" xml:space="preserve"> + <source>Accent</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept" xml:space="preserve"> <source>Accept</source> <target>รับ</target> <note>accept contact request via notification - accept incoming call via notification</note> +accept incoming call via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accept conditions" xml:space="preserve"> + <source>Accept conditions</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept connection request?" xml:space="preserve"> <source>Accept connection request?</source> @@ -542,16 +536,40 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>ยอมรับโหมดไม่ระบุตัวตน</target> - <note>accept contact request via notification</note> + <note>accept contact request via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve"> + <source>Accepted conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve"> + <source>Acknowledged</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve"> + <source>Acknowledgement errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Active" xml:space="preserve"> + <source>Active</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve"> + <source>Active connections</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> <target>เพิ่มที่อยู่ลงในโปรไฟล์ของคุณ เพื่อให้ผู้ติดต่อของคุณสามารถแชร์กับผู้อื่นได้ การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> - <source>Add preset servers</source> - <target>เพิ่มเซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า</target> + <trans-unit id="Add friends" xml:space="preserve"> + <source>Add friends</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve"> + <source>Add list</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add profile" xml:space="preserve"> @@ -559,14 +577,18 @@ <target>เพิ่มโปรไฟล์</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> + <target>เพิ่มเซิร์ฟเวอร์</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> <source>Add servers by scanning QR codes.</source> <target>เพิ่มเซิร์ฟเวอร์โดยการสแกนรหัสคิวอาร์โค้ด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> - <target>เพิ่มเซิร์ฟเวอร์…</target> + <trans-unit id="Add team members" xml:space="preserve"> + <source>Add team members</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -574,11 +596,39 @@ <target>เพิ่มเข้าไปในอุปกรณ์อื่น</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add to list" xml:space="preserve"> + <source>Add to list</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> <target>เพิ่มข้อความต้อนรับ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve"> + <source>Add your team members to the conversations.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve"> + <source>Added media & file servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve"> + <source>Added message servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve"> + <source>Additional accent</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve"> + <source>Additional accent 2</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve"> + <source>Additional secondary</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Address" xml:space="preserve"> <source>Address</source> <target>ที่อยู่</target> @@ -589,6 +639,18 @@ <target>การเปลี่ยนแปลงที่อยู่จะถูกยกเลิก จะใช้ที่อยู่เก่าของผู้รับ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve"> + <source>Address or 1-time link?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve"> + <source>Address settings</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve"> + <source>Admins can block a member for all.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> <source>Admins can create the links to join groups.</source> <target>ผู้ดูแลระบบสามารถสร้างลิงก์เพื่อเข้าร่วมกลุ่มต่างๆได้</target> @@ -599,6 +661,14 @@ <target>การตั้งค่าระบบเครือข่ายขั้นสูง</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve"> + <source>Advanced settings</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All" xml:space="preserve"> + <source>All</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All app data is deleted." xml:space="preserve"> <source>All app data is deleted.</source> <target>ข้อมูลแอปทั้งหมดถูกลบแล้ว.</target> @@ -609,16 +679,32 @@ <target>แชทและข้อความทั้งหมดจะถูกลบ - การดำเนินการนี้ไม่สามารถยกเลิกได้!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> <target>ข้อมูลทั้งหมดจะถูกลบเมื่อถูกป้อน</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve"> + <source>All data is kept private on your device.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> <target>สมาชิกในกลุ่มทุกคนจะยังคงเชื่อมต่ออยู่.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> <target>ข้อความทั้งหมดจะถูกลบ - ไม่สามารถยกเลิกได้! ข้อความจะถูกลบสำหรับคุณเท่านั้น.</target> @@ -628,6 +714,18 @@ <source>All new messages from %@ will be hidden!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All profiles" xml:space="preserve"> + <source>All profiles</source> + <note>profile dropdown</note> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve"> + <source>All reports will be archived for you.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All servers" xml:space="preserve"> + <source>All servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All your contacts will remain connected." xml:space="preserve"> <source>All your contacts will remain connected.</source> <target>ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่.</target> @@ -638,6 +736,10 @@ <target>ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่. ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่ การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow" xml:space="preserve"> <source>Allow</source> <target>อนุญาต</target> @@ -648,13 +750,21 @@ <target>อนุญาตการโทรเฉพาะเมื่อผู้ติดต่อของคุณอนุญาตเท่านั้น.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve"> + <source>Allow calls?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> <target>อนุญาตให้ข้อความที่หายไปเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตเท่านั้น.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> + <trans-unit id="Allow downgrade" xml:space="preserve"> + <source>Allow downgrade</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> <target>อนุญาตให้ลบข้อความแบบถาวรเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตให้คุณเท่านั้น</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -678,11 +788,23 @@ <target>อนุญาตให้ส่งข้อความที่จะหายไปหลังปิดแชท (disappearing message)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow to irreversibly delete sent messages.</source> + <trans-unit id="Allow sharing" xml:space="preserve"> + <source>Allow sharing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> <target>อนุญาตให้ลบข้อความที่ส่งไปแล้วอย่างถาวร</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve"> + <source>Allow to report messsages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve"> + <source>Allow to send SimpleX links.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow to send files and media." xml:space="preserve"> <source>Allow to send files and media.</source> <target>อนุญาตให้ส่งไฟล์และสื่อ</target> @@ -713,8 +835,8 @@ <target>อนุญาตให้ผู้ติดต่อของคุณโทรหาคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow your contacts to irreversibly delete sent messages.</source> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> <target>อนุญาตให้ผู้ติดต่อของคุณลบข้อความที่ส่งแล้วอย่างถาวร</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -741,6 +863,10 @@ <source>Already joining the group!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve"> + <source>Always use private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> <source>Always use relay</source> <target>ใช้รีเลย์เสมอ</target> @@ -751,20 +877,37 @@ <target>โปรไฟล์แชทที่ว่างเปล่าพร้อมชื่อที่ให้ไว้ได้ถูกสร้างขึ้นและแอปจะเปิดตามปกติ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Another reason" xml:space="preserve"> + <source>Another reason</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> <target>รับสาย</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> + <target>โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> <target>รุ่นแอป: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App data migration" xml:space="preserve"> + <source>App data migration</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> <source>App encrypts new local files (except videos).</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App group:" xml:space="preserve"> + <source>App group:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>ไอคอนแอป</target> @@ -780,6 +923,10 @@ <target>รหัสผ่านแอปจะถูกแทนที่ด้วยรหัสผ่านที่ทำลายตัวเอง</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App session" xml:space="preserve"> + <source>App session</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App version" xml:space="preserve"> <source>App version</source> <target>เวอร์ชันแอป</target> @@ -795,6 +942,54 @@ <target>รูปร่างลักษณะ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Apply" xml:space="preserve"> + <source>Apply</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve"> + <source>Apply to</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve"> + <source>Archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve"> + <source>Archive %lld reports?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve"> + <source>Archive all reports?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve"> + <source>Archive and upload</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve"> + <source>Archive contacts to chat later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve"> + <source>Archive report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve"> + <source>Archive report?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve"> + <source>Archive reports</source> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve"> + <source>Archived contacts</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve"> + <source>Archiving database</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Attach" xml:space="preserve"> <source>Attach</source> <target>แนบ</target> @@ -855,11 +1050,19 @@ <target>ยอมรับภาพอัตโนมัติ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve"> + <source>Auto-accept settings</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Back" xml:space="preserve"> <source>Back</source> <target>กลับ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Background" xml:space="preserve"> + <source>Background</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Bad desktop address" xml:space="preserve"> <source>Bad desktop address</source> <note>No comment provided by engineer.</note> @@ -874,19 +1077,59 @@ <target>แฮชข้อความไม่ดี</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better calls" xml:space="preserve"> + <source>Better calls</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better groups" xml:space="preserve"> <source>Better groups</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve"> + <source>Better groups performance</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve"> + <source>Better message dates.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better messages" xml:space="preserve"> <source>Better messages</source> <target>ข้อความที่ดีขึ้น</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better networking" xml:space="preserve"> + <source>Better networking</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve"> + <source>Better notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve"> + <source>Better privacy and security</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve"> + <source>Better security ✅</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve"> + <source>Better user experience</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Black" xml:space="preserve"> + <source>Black</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block" xml:space="preserve"> <source>Block</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block for all" xml:space="preserve"> + <source>Block for all</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block group members" xml:space="preserve"> <source>Block group members</source> <note>No comment provided by engineer.</note> @@ -895,17 +1138,33 @@ <source>Block member</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve"> + <source>Block member for all?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block member?" xml:space="preserve"> <source>Block member?</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve"> + <source>Blocked by admin</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve"> + <source>Blur for better privacy.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve"> + <source>Blur media</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <source>Both you and your contact can add message reactions.</source> <target>ทั้งคุณและผู้ติดต่อของคุณสามารถเพิ่มปฏิกิริยาของข้อความได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> - <source>Both you and your contact can irreversibly delete sent messages.</source> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> <target>ทั้งคุณและผู้ติดต่อของคุณสามารถลบข้อความที่ส่งแล้วอย่างถาวรได้</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -928,11 +1187,29 @@ <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Business address" xml:space="preserve"> + <source>Business address</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve"> + <source>Business chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve"> + <source>Businesses</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>ตามโปรไฟล์แชท (ค่าเริ่มต้น) หรือ [โดยการเชื่อมต่อ](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (เบต้า)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve"> + <source>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> <source>Call already ended!</source> <target>สิ้นสุดการโทรแล้ว!</target> @@ -943,9 +1220,20 @@ <target>โทร</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve"> - <source>Can't delete user profile!</source> - <target>ไม่สามารถลบโปรไฟล์ผู้ใช้ได้!</target> + <trans-unit id="Calls prohibited!" xml:space="preserve"> + <source>Calls prohibited!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve"> + <source>Camera not available</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve"> + <source>Can't call contact</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve"> + <source>Can't call member</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contact!" xml:space="preserve"> @@ -958,9 +1246,18 @@ <target>ไม่สามารถเชิญผู้ติดต่อได้!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve"> + <source>Can't message member</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> <target>ยกเลิก</target> + <note>alert action +alert button</note> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve"> + <source>Cancel migration</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> @@ -968,9 +1265,21 @@ <target>ไม่สามารถเข้าถึง keychain เพื่อบันทึกรหัสผ่านฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> <target>ไม่สามารถรับไฟล์ได้</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve"> + <source>Cellular</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change" xml:space="preserve"> @@ -978,6 +1287,14 @@ <target>เปลี่ยน</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve"> + <source>Change automatic message deletion?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve"> + <source>Change chat profiles</source> + <note>authentication reason</note> + </trans-unit> <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> <target>เปลี่ยนรหัสผ่านฐานข้อมูล?</target> @@ -1022,11 +1339,22 @@ <source>Change self-destruct passcode</source> <target>เปลี่ยนรหัสผ่านแบบทำลายตัวเอง</target> <note>authentication reason - set passcode view</note> +set passcode view</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> - <source>Chat archive</source> - <target>ที่เก็บแชทถาวร</target> + <trans-unit id="Chat" xml:space="preserve"> + <source>Chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve"> + <source>Chat already exists</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve"> + <source>Chat already exists!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve"> + <source>Chat colors</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat console" xml:space="preserve"> @@ -1044,6 +1372,10 @@ <target>ลบฐานข้อมูลแชทแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve"> + <source>Chat database exported</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> <target>นำฐานข้อมูลแชทเข้าแล้ว</target> @@ -1059,26 +1391,71 @@ <target>การแชทหยุดทํางานแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve"> + <source>Chat list</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve"> + <source>Chat migrated!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> <target>ค่ากําหนดในการแชท</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve"> + <source>Chat preferences were changed.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve"> + <source>Chat profile</source> + <target>โปรไฟล์ผู้ใช้</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve"> + <source>Chat theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> <target>แชท</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve"> + <source>Check messages every 20 min.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve"> + <source>Check messages when allowed.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> <target>ตรวจสอบที่อยู่เซิร์ฟเวอร์แล้วลองอีกครั้ง</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> <target>อินเทอร์เฟซภาษาจีนและสเปน</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> <target>เลือกไฟล์</target> @@ -1089,10 +1466,22 @@ <target>เลือกจากอัลบั้ม</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve"> + <source>Chunks deleted</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve"> + <source>Chunks downloaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve"> + <source>Chunks uploaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> <target>ลบ</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> @@ -1104,16 +1493,35 @@ <target>ลบการสนทนา?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve"> + <source>Clear group?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve"> + <source>Clear or delete group?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve"> + <source>Clear private notes?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear verification" xml:space="preserve"> <source>Clear verification</source> <target>ล้างการยืนยัน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> - <source>Colors</source> - <target>สี</target> + <trans-unit id="Color chats with the new themes." xml:space="preserve"> + <source>Color chats with the new themes.</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Color mode" xml:space="preserve"> + <source>Color mode</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve"> + <source>Community guidelines violation</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Compare file" xml:space="preserve"> <source>Compare file</source> <target>เปรียบเทียบไฟล์</target> @@ -1124,11 +1532,47 @@ <target>เปรียบเทียบรหัสความปลอดภัยกับผู้ติดต่อของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Completed" xml:space="preserve"> + <source>Completed</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve"> + <source>Conditions accepted on: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve"> + <source>Conditions of use</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve"> + <source>Conditions will be accepted on: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> <target>กำหนดค่าเซิร์ฟเวอร์ ICE</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Configure server operators" xml:space="preserve"> + <source>Configure server operators</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> <target>ยืนยัน</target> @@ -1139,11 +1583,23 @@ <target>ยืนยันรหัสผ่าน</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve"> + <source>Confirm contact deletion?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm database upgrades" xml:space="preserve"> <source>Confirm database upgrades</source> <target>ยืนยันการอัพเกรดฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve"> + <source>Confirm files from unknown servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve"> + <source>Confirm network settings</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> <target>ยืนยันรหัสผ่านใหม่…</target> @@ -1154,6 +1610,18 @@ <target>ยืนยันรหัสผ่าน</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve"> + <source>Confirm upload</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve"> + <source>Confirmed</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> <target>เชื่อมต่อ</target> @@ -1171,6 +1639,10 @@ <source>Connect to desktop</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve"> + <source>Connect to your friends faster.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connect to yourself?" xml:space="preserve"> <source>Connect to yourself?</source> <note>No comment provided by engineer.</note> @@ -1194,11 +1666,6 @@ This is your own one-time link!</source> <target>เชื่อมต่อผ่านลิงก์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve"> - <source>Connect via link / QR code</source> - <target>เชื่อมต่อผ่านลิงค์ / คิวอาร์โค้ด</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connect via one-time link" xml:space="preserve"> <source>Connect via one-time link</source> <note>No comment provided by engineer.</note> @@ -1207,14 +1674,26 @@ This is your own one-time link!</source> <source>Connect with %@</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected" xml:space="preserve"> + <source>Connected</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected desktop" xml:space="preserve"> <source>Connected desktop</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve"> + <source>Connected servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connected to desktop" xml:space="preserve"> <source>Connected to desktop</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting" xml:space="preserve"> + <source>Connecting</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> <source>Connecting to server…</source> <target>กำลังเชื่อมต่อกับเซิร์ฟเวอร์…</target> @@ -1225,6 +1704,10 @@ This is your own one-time link!</source> <target>กำลังเชื่อมต่อกับเซิร์ฟเวอร์... (ข้อผิดพลาด: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve"> + <source>Connecting to contact, please wait or check later!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting to desktop" xml:space="preserve"> <source>Connecting to desktop</source> <note>No comment provided by engineer.</note> @@ -1234,6 +1717,14 @@ This is your own one-time link!</source> <target>การเชื่อมต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve"> + <source>Connection and servers status.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve"> + <source>Connection blocked</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> <target>การเชื่อมต่อผิดพลาด</target> @@ -1244,11 +1735,32 @@ This is your own one-time link!</source> <target>การเชื่อมต่อผิดพลาด (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve"> + <source>Connection is blocked by server operator: +%@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve"> + <source>Connection not ready.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve"> + <source>Connection notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>ส่งคําขอเชื่อมต่อแล้ว!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve"> + <source>Connection requires encryption renegotiation.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve"> + <source>Connection security</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection terminated" xml:space="preserve"> <source>Connection terminated</source> <note>No comment provided by engineer.</note> @@ -1258,6 +1770,14 @@ This is your own one-time link!</source> <target>หมดเวลาการเชื่อมต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve"> + <source>Connection with desktop stopped</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve"> + <source>Connections</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact allows" xml:space="preserve"> <source>Contact allows</source> <target>ผู้ติดต่ออนุญาต</target> @@ -1268,6 +1788,10 @@ This is your own one-time link!</source> <target>ผู้ติดต่อรายนี้มีอยู่แล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact deleted!" xml:space="preserve"> + <source>Contact deleted!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> <target>ผู้ติดต่อถูกซ่อน:</target> @@ -1278,9 +1802,8 @@ This is your own one-time link!</source> <target>เชื่อมต่อกับผู้ติดต่อแล้ว</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve"> - <source>Contact is not connected yet!</source> - <target>ผู้ติดต่อยังไม่ได้เชื่อมต่อ!</target> + <trans-unit id="Contact is deleted." xml:space="preserve"> + <source>Contact is deleted.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact name" xml:space="preserve"> @@ -1293,6 +1816,10 @@ This is your own one-time link!</source> <target>การกําหนดลักษณะการติดต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve"> + <source>Contact will be deleted - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> <target>ติดต่อ</target> @@ -1303,21 +1830,37 @@ This is your own one-time link!</source> <target>ผู้ติดต่อสามารถทําเครื่องหมายข้อความเพื่อลบได้ คุณจะสามารถดูได้</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve"> + <source>Content violates conditions of use</source> + <note>blocking reason</note> + </trans-unit> <trans-unit id="Continue" xml:space="preserve"> <source>Continue</source> <target>ดำเนินการต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve"> + <source>Conversation deleted!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> <target>คัดลอก</target> - <note>chat item action</note> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve"> + <source>Copy error</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> <target>รุ่นหลัก: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Corner" xml:space="preserve"> + <source>Corner</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Correct name to %@?" xml:space="preserve"> <source>Correct name to %@?</source> <note>No comment provided by engineer.</note> @@ -1327,6 +1870,10 @@ This is your own one-time link!</source> <target>สร้าง</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve"> + <source>Create 1-time link</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> <target>สร้างที่อยู่ SimpleX</target> @@ -1336,11 +1883,6 @@ This is your own one-time link!</source> <source>Create a group using a random profile.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> - <source>Create an address to let people connect with you.</source> - <target>สร้างที่อยู่เพื่อให้ผู้อื่นเชื่อมต่อกับคุณ</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create file" xml:space="preserve"> <source>Create file</source> <target>สร้างไฟล์</target> @@ -1360,13 +1902,12 @@ This is your own one-time link!</source> <target>สร้างลิงค์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> - <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <trans-unit id="Create list" xml:space="preserve"> + <source>Create list</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve"> - <source>Create one-time invitation link</source> - <target>สร้างลิงก์เชิญแบบใช้ครั้งเดียว</target> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create profile" xml:space="preserve"> @@ -1388,9 +1929,24 @@ This is your own one-time link!</source> <target>สร้างโปรไฟล์ของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve"> - <source>Created on %@</source> - <target>สร้างเมื่อ %@</target> + <trans-unit id="Created" xml:space="preserve"> + <source>Created</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve"> + <source>Created at</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve"> + <source>Created at: %@</source> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve"> + <source>Creating archive link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve"> + <source>Creating link…</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current Passcode" xml:space="preserve"> @@ -1398,11 +1954,19 @@ This is your own one-time link!</source> <target>รหัสผ่านปัจจุบัน</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> <target>รหัสผ่านปัจจุบัน…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current profile" xml:space="preserve"> + <source>Current profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> <target>ขนาดไฟล์ที่รองรับสูงสุดในปัจจุบันคือ %@</target> @@ -1413,11 +1977,23 @@ This is your own one-time link!</source> <target>เวลาที่กําหนดเอง</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve"> + <source>Customizable message shape.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve"> + <source>Customize theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> <target>มืด</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve"> + <source>Dark mode colors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> <target>ID ฐานข้อมูล</target> @@ -1516,6 +2092,10 @@ This is your own one-time link!</source> <target>ระบบจะย้ายฐานข้อมูลเมื่อแอปรีสตาร์ท</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Debug delivery" xml:space="preserve"> + <source>Debug delivery</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> <target>กระจายอำนาจแล้ว</target> @@ -1529,17 +2109,17 @@ This is your own one-time link!</source> <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> <target>ลบ</target> - <note>chat item action</note> + <note>alert action +swipe action</note> + </trans-unit> + <trans-unit id="Delete %lld messages of members?" xml:space="preserve"> + <source>Delete %lld messages of members?</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete %lld messages?" xml:space="preserve"> <source>Delete %lld messages?</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve"> - <source>Delete Contact</source> - <target>ลบผู้ติดต่อ</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Delete address" xml:space="preserve"> <source>Delete address</source> <target>ลบที่อยู่</target> @@ -1564,14 +2144,12 @@ This is your own one-time link!</source> <source>Delete and notify contact</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve"> - <source>Delete archive</source> - <target>ลบที่เก็บถาวร</target> + <trans-unit id="Delete chat" xml:space="preserve"> + <source>Delete chat</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve"> - <source>Delete chat archive?</source> - <target>ลบที่เก็บแชทถาวร?</target> + <trans-unit id="Delete chat messages from your device." xml:space="preserve"> + <source>Delete chat messages from your device.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile" xml:space="preserve"> @@ -1584,6 +2162,10 @@ This is your own one-time link!</source> <target>ลบโปรไฟล์แชทไหม?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete chat?" xml:space="preserve"> + <source>Delete chat?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> <source>Delete connection</source> <target>ลบการเชื่อมต่อ</target> @@ -1594,9 +2176,8 @@ This is your own one-time link!</source> <target>ลบผู้ติดต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact? This cannot be undone!" xml:space="preserve"> - <source>Delete contact? -This cannot be undone!</source> + <trans-unit id="Delete contact?" xml:space="preserve"> + <source>Delete contact?</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete database" xml:space="preserve"> @@ -1604,6 +2185,10 @@ This cannot be undone!</source> <target>ลบฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete database from this device" xml:space="preserve"> + <source>Delete database from this device</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete file" xml:space="preserve"> <source>Delete file</source> <target>ลบไฟล์</target> @@ -1654,6 +2239,10 @@ This cannot be undone!</source> <target>ลบลิงค์ ไหม?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete list?" xml:space="preserve"> + <source>Delete list?</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> <target>ลบข้อความสมาชิก?</target> @@ -1667,7 +2256,7 @@ This cannot be undone!</source> <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> <target>ลบข้อความ</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> @@ -1684,9 +2273,8 @@ This cannot be undone!</source> <target>ลบฐานข้อมูลเก่า?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve"> - <source>Delete pending connection</source> - <target>ลบการเชื่อมต่อที่รอดำเนินการ</target> + <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> + <source>Delete or moderate up to 200 messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection?" xml:space="preserve"> @@ -1704,11 +2292,27 @@ This cannot be undone!</source> <target>ลบคิว</target> <note>server test step</note> </trans-unit> + <trans-unit id="Delete report" xml:space="preserve"> + <source>Delete report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete up to 20 messages at once." xml:space="preserve"> + <source>Delete up to 20 messages at once.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> <target>ลบโปรไฟล์ผู้ใช้?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete without notification" xml:space="preserve"> + <source>Delete without notification</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve"> + <source>Deleted</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Deleted at" xml:space="preserve"> <source>Deleted at</source> <target>ลบที่</target> @@ -1719,6 +2323,14 @@ This cannot be undone!</source> <target>ลบที่: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve"> + <source>Deletion errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivered even when Apple drops them." xml:space="preserve"> + <source>Delivered even when Apple drops them.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery" xml:space="preserve"> <source>Delivery</source> <note>No comment provided by engineer.</note> @@ -1750,11 +2362,35 @@ This cannot be undone!</source> <source>Desktop devices</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve"> + <source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server error: %@" xml:space="preserve"> + <source>Destination server error: %@</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve"> + <source>Destination server version of %@ is incompatible with forwarding server %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Detailed statistics" xml:space="preserve"> + <source>Detailed statistics</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Details" xml:space="preserve"> + <source>Details</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Develop" xml:space="preserve"> <source>Develop</source> <target>พัฒนา</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Developer options" xml:space="preserve"> + <source>Developer options</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> <target>เครื่องมือสำหรับนักพัฒนา</target> @@ -1785,8 +2421,12 @@ This cannot be undone!</source> <target>ข้อความโดยตรง</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve"> + <source>Direct messages between members are prohibited in this chat.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> <target>ข้อความโดยตรงระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1800,11 +2440,23 @@ This cannot be undone!</source> <target>ปิดการใช้งาน SimpleX Lock</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Disable automatic message deletion?" xml:space="preserve"> + <source>Disable automatic message deletion?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Disable delete messages" xml:space="preserve"> + <source>Disable delete messages</source> + <note>alert button</note> + </trans-unit> <trans-unit id="Disable for all" xml:space="preserve"> <source>Disable for all</source> <target>ปิดการใช้งานสำหรับทุกคน</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Disabled" xml:space="preserve"> + <source>Disabled</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> <source>Disappearing message</source> <target>ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message)</target> @@ -1820,8 +2472,8 @@ This cannot be undone!</source> <target>ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) เป็นสิ่งต้องห้ามในแชทนี้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <target>ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) เป็นสิ่งต้องห้ามในกลุ่มนี้</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1852,16 +2504,36 @@ This cannot be undone!</source> <source>Discover via local network</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve"> + <source>Do NOT send messages directly, even if your or destination server does not support private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> <source>Do NOT use SimpleX for emergency calls.</source> <target>อย่าใช้ SimpleX สําหรับการโทรฉุกเฉิน</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT use private routing." xml:space="preserve"> + <source>Do NOT use private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do it later" xml:space="preserve"> <source>Do it later</source> <target>ทำในภายหลัง</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do not send history to new members." xml:space="preserve"> + <source>Do not send history to new members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not use credentials with proxy." xml:space="preserve"> + <source>Do not use credentials with proxy.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Documents:" xml:space="preserve"> + <source>Documents:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> <target>อย่าสร้างที่อยู่</target> @@ -1872,21 +2544,62 @@ This cannot be undone!</source> <target>อย่าเปิดใช้งาน</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Don't miss important messages." xml:space="preserve"> + <source>Don't miss important messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> <target>ไม่ต้องแสดงอีก</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Done" xml:space="preserve"> + <source>Done</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Downgrade and open chat" xml:space="preserve"> <source>Downgrade and open chat</source> <target>ปรับลดรุ่นและเปิดแชท</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Download" xml:space="preserve"> + <source>Download</source> + <note>alert button +chat item action</note> + </trans-unit> + <trans-unit id="Download errors" xml:space="preserve"> + <source>Download errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download failed" xml:space="preserve"> + <source>Download failed</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Download file" xml:space="preserve"> <source>Download file</source> <target>ดาวน์โหลดไฟล์</target> <note>server test step</note> </trans-unit> + <trans-unit id="Download files" xml:space="preserve"> + <source>Download files</source> + <note>alert action</note> + </trans-unit> + <trans-unit id="Downloaded" xml:space="preserve"> + <source>Downloaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloaded files" xml:space="preserve"> + <source>Downloaded files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading archive" xml:space="preserve"> + <source>Downloading archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading link details" xml:space="preserve"> + <source>Downloading link details</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> <target>ชื่อที่แสดงซ้ำ!</target> @@ -1897,6 +2610,10 @@ This cannot be undone!</source> <target>ระยะเวลา</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="E2E encrypted notifications." xml:space="preserve"> + <source>E2E encrypted notifications.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> <target>แก้ไข</target> @@ -1917,6 +2634,10 @@ This cannot be undone!</source> <target>เปิดใช้งาน (เก็บการแทนที่)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve"> + <source>Enable Flux in Network & servers settings for better metadata privacy.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> <target>เปิดใช้งาน SimpleX Lock</target> @@ -1930,6 +2651,10 @@ This cannot be undone!</source> <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> <target>เปิดใช้งานการลบข้อความอัตโนมัติ?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Enable camera access" xml:space="preserve"> + <source>Enable camera access</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable for all" xml:space="preserve"> @@ -1937,6 +2662,10 @@ This cannot be undone!</source> <target>เปิดใช้งานสําหรับทุกคน</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve"> + <source>Enable in direct chats (BETA)!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> <target>เปิดใช้งานการแจ้งเตือนทันที?</target> @@ -1967,6 +2696,14 @@ This cannot be undone!</source> <target>เปิดใช้งานรหัสผ่านแบบทําลายตัวเอง</target> <note>set passcode view</note> </trans-unit> + <trans-unit id="Enabled" xml:space="preserve"> + <source>Enabled</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve"> + <source>Enabled for</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> <target>Encrypt</target> @@ -1995,6 +2732,10 @@ This cannot be undone!</source> <target>ข้อความที่ encrypt หรือเหตุการณ์อื่น</target> <note>notification</note> </trans-unit> + <trans-unit id="Encrypted message: app is stopped" xml:space="preserve"> + <source>Encrypted message: app is stopped</source> + <note>notification</note> + </trans-unit> <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> <target>ข้อความที่ encrypt: ความผิดพลาดในฐานข้อมูล</target> @@ -2028,6 +2769,10 @@ This cannot be undone!</source> <source>Encryption re-negotiation failed.</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encryption renegotiation in progress." xml:space="preserve"> + <source>Encryption renegotiation in progress.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter Passcode" xml:space="preserve"> <source>Enter Passcode</source> <target>ใส่รหัสผ่าน</target> @@ -2042,6 +2787,10 @@ This cannot be undone!</source> <source>Enter group name…</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve"> + <source>Enter passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter passphrase…" xml:space="preserve"> <source>Enter passphrase…</source> <target>ใส่รหัสผ่าน</target> @@ -2085,26 +2834,33 @@ This cannot be undone!</source> <target>เกิดข้อผิดพลาดในการยกเลิกการเปลี่ยนที่อยู่</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error accepting conditions" xml:space="preserve"> + <source>Error accepting conditions</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> <target>เกิดข้อผิดพลาดในการรับคำขอติดต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve"> - <source>Error accessing database file</source> - <target>เกิดข้อผิดพลาดในการเข้าถึงไฟล์ฐานข้อมูล</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> <target>เกิดข้อผิดพลาดในการเพิ่มสมาชิก</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error adding server" xml:space="preserve"> + <source>Error adding server</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> <target>เกิดข้อผิดพลาดในการเปลี่ยนที่อยู่</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing connection profile" xml:space="preserve"> + <source>Error changing connection profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error changing role" xml:space="preserve"> <source>Error changing role</source> <target>เกิดข้อผิดพลาดในการเปลี่ยนบทบาท</target> @@ -2115,6 +2871,18 @@ This cannot be undone!</source> <target>เกิดข้อผิดพลาดในการเปลี่ยนการตั้งค่า</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing to incognito!" xml:space="preserve"> + <source>Error changing to incognito!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error checking token status" xml:space="preserve"> + <source>Error checking token status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve"> + <source>Error connecting to forwarding server %@. Please try later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> <target>เกิดข้อผิดพลาดในการสร้างที่อยู่</target> @@ -2130,15 +2898,27 @@ This cannot be undone!</source> <target>เกิดข้อผิดพลาดในการสร้างลิงก์กลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating list" xml:space="preserve"> + <source>Error creating list</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error creating member contact" xml:space="preserve"> <source>Error creating member contact</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating message" xml:space="preserve"> + <source>Error creating message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>เกิดข้อผิดพลาดในการสร้างโปรไฟล์!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating report" xml:space="preserve"> + <source>Error creating report</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error decrypting file" xml:space="preserve"> <source>Error decrypting file</source> <note>No comment provided by engineer.</note> @@ -2158,11 +2938,6 @@ This cannot be undone!</source> <target>เกิดข้อผิดพลาดในการลบการเชื่อมต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve"> - <source>Error deleting contact</source> - <target>เกิดข้อผิดพลาดในการลบผู้ติดต่อ</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> <target>เกิดข้อผิดพลาดในการลบฐานข้อมูล</target> @@ -2183,6 +2958,10 @@ This cannot be undone!</source> <target>เกิดข้อผิดพลาดในการลบโปรไฟล์ผู้ใช้</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error downloading the archive" xml:space="preserve"> + <source>Error downloading the archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> <source>Error enabling delivery receipts!</source> <target>เกิดข้อผิดพลาดในการเปิดใช้ใบเสร็จการจัดส่ง!</target> @@ -2203,6 +2982,10 @@ This cannot be undone!</source> <target>เกิดข้อผิดพลาดในการส่งออกฐานข้อมูลแชท</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error exporting theme: %@" xml:space="preserve"> + <source>Error exporting theme: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> <target>เกิดข้อผิดพลาดในการนำเข้าฐานข้อมูลแชท</target> @@ -2213,24 +2996,46 @@ This cannot be undone!</source> <target>เกิดข้อผิดพลาดในการเข้าร่วมกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve"> - <source>Error loading %@ servers</source> - <target>โหลดเซิร์ฟเวอร์ %@ ผิดพลาด</target> + <trans-unit id="Error loading servers" xml:space="preserve"> + <source>Error loading servers</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error migrating settings" xml:space="preserve"> + <source>Error migrating settings</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error opening chat" xml:space="preserve"> + <source>Error opening chat</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> <target>เกิดข้อผิดพลาดในการรับไฟล์</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error reconnecting server" xml:space="preserve"> + <source>Error reconnecting server</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error reconnecting servers" xml:space="preserve"> + <source>Error reconnecting servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error registering for notifications" xml:space="preserve"> + <source>Error registering for notifications</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> <target>เกิดข้อผิดพลาดในการลบสมาชิก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve"> - <source>Error saving %@ servers</source> - <target>เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ %@</target> + <trans-unit id="Error reordering lists" xml:space="preserve"> + <source>Error reordering lists</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error resetting statistics" xml:space="preserve"> + <source>Error resetting statistics</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving ICE servers" xml:space="preserve"> @@ -2238,6 +3043,10 @@ This cannot be undone!</source> <target>เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ ICE</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving chat list" xml:space="preserve"> + <source>Error saving chat list</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> <target>เกิดข้อผิดพลาดในการบันทึกโปรไฟล์กลุ่ม</target> @@ -2253,11 +3062,23 @@ This cannot be undone!</source> <target>เกิดข้อผิดพลาดในการบันทึกรหัสผ่านไปยัง keychain</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving servers" xml:space="preserve"> + <source>Error saving servers</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error saving settings" xml:space="preserve"> + <source>Error saving settings</source> + <note>when migrating</note> + </trans-unit> <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> <target>เกิดข้อผิดพลาดในการบันทึกรหัสผ่านผู้ใช้</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error scanning code: %@" xml:space="preserve"> + <source>Error scanning code: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending email" xml:space="preserve"> <source>Error sending email</source> <target>เกิดข้อผิดพลาดในการส่งอีเมล</target> @@ -2287,16 +3108,24 @@ This cannot be undone!</source> <target>เกิดข้อผิดพลาดในการหยุดแชท</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error switching profile" xml:space="preserve"> + <source>Error switching profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> <target>เกิดข้อผิดพลาดในการเปลี่ยนโปรไฟล์!</target> - <note>No comment provided by engineer.</note> + <note>alertTitle</note> </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> <target>เกิดข้อผิดพลาดในการซิงโครไนซ์การเชื่อมต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error testing server connection" xml:space="preserve"> + <source>Error testing server connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> <target>เกิดข้อผิดพลาดในการอัปเดตลิงก์กลุ่ม</target> @@ -2307,6 +3136,10 @@ This cannot be undone!</source> <target>เกิดข้อผิดพลาดในการอัปเดตข้อความ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error updating server" xml:space="preserve"> + <source>Error updating server</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> <target>เกิดข้อผิดพลาดในการอัปเดตการตั้งค่า</target> @@ -2317,6 +3150,14 @@ This cannot be undone!</source> <target>เกิดข้อผิดพลาดในการอัปเดตข้อมูลส่วนตัวของผู้ใช้</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error uploading the archive" xml:space="preserve"> + <source>Error uploading the archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error verifying passphrase:" xml:space="preserve"> + <source>Error verifying passphrase:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error: " xml:space="preserve"> <source>Error: </source> <target>ผิดพลาด: </target> @@ -2325,7 +3166,9 @@ This cannot be undone!</source> <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> <target>ข้อผิดพลาด: % @</target> - <note>No comment provided by engineer.</note> + <note>alert message +file error text +snd error text</note> </trans-unit> <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> @@ -2337,6 +3180,14 @@ This cannot be undone!</source> <target>เกิดข้อผิดพลาด: ไม่มีแฟ้มฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Errors" xml:space="preserve"> + <source>Errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors in servers configuration." xml:space="preserve"> + <source>Errors in servers configuration.</source> + <note>servers error</note> + </trans-unit> <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> <source>Even when disabled in the conversation.</source> <target>แม้ในขณะที่ปิดใช้งานในการสนทนา</target> @@ -2351,6 +3202,10 @@ This cannot be undone!</source> <source>Expand</source> <note>chat item action</note> </trans-unit> + <trans-unit id="Expired" xml:space="preserve"> + <source>Expired</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> <target>ส่งออกฐานข้อมูล</target> @@ -2361,11 +3216,19 @@ This cannot be undone!</source> <target>ข้อผิดพลาดในการส่งออก:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Export theme" xml:space="preserve"> + <source>Export theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> <target>ที่เก็บถาวรฐานข้อมูลที่ส่งออก</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Exported file doesn't exist" xml:space="preserve"> + <source>Exported file doesn't exist</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exporting database archive…" xml:space="preserve"> <source>Exporting database archive…</source> <target>กำลังส่งออกที่เก็บถาวรฐานข้อมูล…</target> @@ -2381,15 +3244,57 @@ This cannot be undone!</source> <target>รวดเร็วและไม่ต้องรอจนกว่าผู้ส่งจะออนไลน์!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster deletion of groups." xml:space="preserve"> + <source>Faster deletion of groups.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Faster joining and more reliable messages." xml:space="preserve"> <source>Faster joining and more reliable messages.</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster sending messages." xml:space="preserve"> + <source>Faster sending messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Favorite" xml:space="preserve"> <source>Favorite</source> <target>ที่ชอบ</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Favorites" xml:space="preserve"> + <source>Favorites</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <note>file error alert title</note> + </trans-unit> + <trans-unit id="File errors: %@" xml:space="preserve"> + <source>File errors: +%@</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="File is blocked by server operator: %@." xml:space="preserve"> + <source>File is blocked by server operator: +%@.</source> + <note>file error text</note> + </trans-unit> + <trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve"> + <source>File not found - most likely file was deleted or cancelled.</source> + <note>file error text</note> + </trans-unit> + <trans-unit id="File server error: %@" xml:space="preserve"> + <source>File server error: %@</source> + <note>file error text</note> + </trans-unit> + <trans-unit id="File status" xml:space="preserve"> + <source>File status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File status: %@" xml:space="preserve"> + <source>File status: %@</source> + <note>copied message info</note> + </trans-unit> <trans-unit id="File will be deleted from servers." xml:space="preserve"> <source>File will be deleted from servers.</source> <target>ไฟล์จะถูกลบออกจากเซิร์ฟเวอร์</target> @@ -2410,6 +3315,10 @@ This cannot be undone!</source> <target>ไฟล์: % @</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files" xml:space="preserve"> + <source>Files</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> <target>ไฟล์และสื่อ</target> @@ -2420,11 +3329,15 @@ This cannot be undone!</source> <target>ไฟล์และสื่อ</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> - <source>Files and media are prohibited in this group.</source> + <trans-unit id="Files and media are prohibited." xml:space="preserve"> + <source>Files and media are prohibited.</source> <target>ไฟล์และสื่อเป็นสิ่งต้องห้ามในกลุ่มนี้</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files and media not allowed" xml:space="preserve"> + <source>Files and media not allowed</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files and media prohibited!" xml:space="preserve"> <source>Files and media prohibited!</source> <target>ไฟล์และสื่อต้องห้าม!</target> @@ -2435,6 +3348,14 @@ This cannot be undone!</source> <target>กรองแชทที่ยังไม่อ่านและแชทโปรด</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Finalize migration" xml:space="preserve"> + <source>Finalize migration</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration on another device." xml:space="preserve"> + <source>Finalize migration on another device.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> <target>ในที่สุดเราก็มีแล้ว! 🚀</target> @@ -2475,11 +3396,93 @@ This cannot be undone!</source> <target>การแก้ไขไม่สนับสนุนโดยสมาชิกกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For all moderators" xml:space="preserve"> + <source>For all moderators</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For chat profile %@:" xml:space="preserve"> + <source>For chat profile %@:</source> + <note>servers error</note> + </trans-unit> <trans-unit id="For console" xml:space="preserve"> <source>For console</source> <target>สำหรับคอนโซล</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." xml:space="preserve"> + <source>For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For me" xml:space="preserve"> + <source>For me</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For private routing" xml:space="preserve"> + <source>For private routing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For social media" xml:space="preserve"> + <source>For social media</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward" xml:space="preserve"> + <source>Forward</source> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Forward %d message(s)?" xml:space="preserve"> + <source>Forward %d message(s)?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Forward and save messages" xml:space="preserve"> + <source>Forward and save messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward messages" xml:space="preserve"> + <source>Forward messages</source> + <note>alert action</note> + </trans-unit> + <trans-unit id="Forward messages without files?" xml:space="preserve"> + <source>Forward messages without files?</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> + <source>Forward up to 20 messages at once.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded" xml:space="preserve"> + <source>Forwarded</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded from" xml:space="preserve"> + <source>Forwarded from</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding %lld messages" xml:space="preserve"> + <source>Forwarding %lld messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve"> + <source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server address is incompatible with network settings: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server version is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server version is incompatible with network settings: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Destination server error: %2$@</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Error: %2$@</source> + <note>snd error text</note> + </trans-unit> <trans-unit id="Found desktop" xml:space="preserve"> <source>Found desktop</source> <note>No comment provided by engineer.</note> @@ -2499,11 +3502,6 @@ This cannot be undone!</source> <target>ชื่อเต็ม (ไม่บังคับ)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full name:" xml:space="preserve"> - <source>Full name:</source> - <target>ชื่อเต็ม:</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Fully decentralized – visible only to members." xml:space="preserve"> <source>Fully decentralized – visible only to members.</source> <note>No comment provided by engineer.</note> @@ -2523,6 +3521,18 @@ This cannot be undone!</source> <target>GIFs และสติกเกอร์</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Get notified when mentioned." xml:space="preserve"> + <source>Get notified when mentioned.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Good afternoon!" xml:space="preserve"> + <source>Good afternoon!</source> + <note>message preview</note> + </trans-unit> + <trans-unit id="Good morning!" xml:space="preserve"> + <source>Good morning!</source> + <note>message preview</note> + </trans-unit> <trans-unit id="Group" xml:space="preserve"> <source>Group</source> <target>กลุ่ม</target> @@ -2576,36 +3586,6 @@ This cannot be undone!</source> <target>ลิงค์กลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve"> - <source>Group members can add message reactions.</source> - <target>สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> - <source>Group members can irreversibly delete sent messages.</source> - <target>สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> - <target>สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> - <target>สมาชิกกลุ่มสามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send files and media." xml:space="preserve"> - <source>Group members can send files and media.</source> - <target>สมาชิกกลุ่มสามารถส่งไฟล์และสื่อ</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> - <target>สมาชิกกลุ่มสามารถส่งข้อความเสียง</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> <target>ข้อความกลุ่ม:</target> @@ -2646,11 +3626,19 @@ This cannot be undone!</source> <target>กลุ่มจะถูกลบสำหรับคุณ - ไม่สามารถยกเลิกได้!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Groups" xml:space="preserve"> + <source>Groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Help" xml:space="preserve"> <source>Help</source> <target>ความช่วยเหลือ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Help admins moderating their groups." xml:space="preserve"> + <source>Help admins moderating their groups.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> <target>ซ่อนอยู่</target> @@ -2691,15 +3679,26 @@ This cannot be undone!</source> <target>ประวัติ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="History is not sent to new members." xml:space="preserve"> + <source>History is not sent to new members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> <target>วิธีการ SimpleX ทํางานอย่างไร</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="How it affects privacy" xml:space="preserve"> + <source>How it affects privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it helps privacy" xml:space="preserve"> + <source>How it helps privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> - <target>มันทำงานอย่างไร</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="How to" xml:space="preserve"> <source>How to</source> @@ -2716,21 +3715,24 @@ This cannot be undone!</source> <target>วิธีใช้เซิร์ฟเวอร์ของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Hungarian interface" xml:space="preserve"> + <source>Hungarian interface</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> <target>เซิร์ฟเวอร์ ICE (หนึ่งเครื่องต่อสาย)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="IP address" xml:space="preserve"> + <source>IP address</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> <source>If you can't meet in person, show QR code in a video call, or share the link.</source> <target>หากคุณไม่สามารถพบกันในชีวิตจริงได้ ให้แสดงคิวอาร์โค้ดในวิดีโอคอล หรือแชร์ลิงก์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <target>หากคุณไม่สามารถพบปะด้วยตนเอง คุณสามารถ **สแกนคิวอาร์โค้ดผ่านการสนทนาทางวิดีโอ** หรือผู้ติดต่อของคุณสามารถแชร์ลิงก์เชิญได้</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> <target>หากคุณใส่รหัสผ่านนี้เมื่อเปิดแอป ข้อมูลแอปทั้งหมดจะถูกลบอย่างถาวร!</target> @@ -2766,8 +3768,8 @@ This cannot be undone!</source> <target>โดยทันที</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <target>มีภูมิคุ้มกันต่อสแปมและการละเมิด</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2786,6 +3788,27 @@ This cannot be undone!</source> <target>นำเข้าฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Import failed" xml:space="preserve"> + <source>Import failed</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import theme" xml:space="preserve"> + <source>Import theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Importing archive" xml:space="preserve"> + <source>Importing archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved delivery, reduced traffic usage. More improvements are coming soon!" xml:space="preserve"> + <source>Improved delivery, reduced traffic usage. +More improvements are coming soon!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved message delivery" xml:space="preserve"> + <source>Improved message delivery</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> <target>ปรับปรุงความเป็นส่วนตัวและความปลอดภัยแล้ว</target> @@ -2796,11 +3819,27 @@ This cannot be undone!</source> <target>ปรับปรุงการกําหนดค่าเซิร์ฟเวอร์แล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In order to continue, chat should be stopped." xml:space="preserve"> + <source>In order to continue, chat should be stopped.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>ในการตอบกลับถึง</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In-call sounds" xml:space="preserve"> + <source>In-call sounds</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Inappropriate content" xml:space="preserve"> + <source>Inappropriate content</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Inappropriate profile" xml:space="preserve"> + <source>Inappropriate profile</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> <target>ไม่ระบุตัวตน</target> @@ -2868,6 +3907,11 @@ This cannot be undone!</source> <target>ติดตั้ง [SimpleX Chat สำหรับเทอร์มินัล](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Instant" xml:space="preserve"> + <source>Instant</source> + <target>ทันที</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> @@ -2875,29 +3919,68 @@ This cannot be undone!</source> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Instantly" xml:space="preserve"> - <source>Instantly</source> - <target>ทันที</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> <target>อินเตอร์เฟซ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Interface colors" xml:space="preserve"> + <source>Interface colors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid" xml:space="preserve"> + <source>Invalid</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (bad token)" xml:space="preserve"> + <source>Invalid (bad token)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (expired)" xml:space="preserve"> + <source>Invalid (expired)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (unregistered)" xml:space="preserve"> + <source>Invalid (unregistered)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (wrong topic)" xml:space="preserve"> + <source>Invalid (wrong topic)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid QR code" xml:space="preserve"> + <source>Invalid QR code</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> <target>ลิงค์เชื่อมต่อไม่ถูกต้อง</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid display name!" xml:space="preserve"> + <source>Invalid display name!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid link" xml:space="preserve"> + <source>Invalid link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid name!" xml:space="preserve"> <source>Invalid name!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid response" xml:space="preserve"> + <source>Invalid response</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> <target>ที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Invalid status" xml:space="preserve"> <source>Invalid status</source> @@ -2918,6 +4001,10 @@ This cannot be undone!</source> <target>เชิญสมาชิก</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invite to chat" xml:space="preserve"> + <source>Invite to chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invite to group" xml:space="preserve"> <source>Invite to group</source> <target>เชิญเข้าร่วมกลุ่ม</target> @@ -2933,8 +4020,8 @@ This cannot be undone!</source> <target>ไม่สามารถลบข้อความแบบแก้ไขไม่ได้ในแชทนี้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <target>การลบข้อความแบบแก้ไขไม่ได้เป็นสิ่งที่ห้ามในกลุ่มนี้</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2959,6 +4046,10 @@ This cannot be undone!</source> 3. การเชื่อมต่อถูกบุกรุก</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="It protects your IP address and connections." xml:space="preserve"> + <source>It protects your IP address and connections.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> <target>ดูเหมือนว่าคุณได้เชื่อมต่อผ่านลิงก์นี้แล้ว หากไม่เป็นเช่นนั้น แสดงว่ามีข้อผิดพลาด (%@).</target> @@ -2977,13 +4068,17 @@ This cannot be undone!</source> <trans-unit id="Join" xml:space="preserve"> <source>Join</source> <target>เข้าร่วม</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> <target>เข้าร่วมกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Join group conversations" xml:space="preserve"> + <source>Join group conversations</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Join group?" xml:space="preserve"> <source>Join group?</source> <note>No comment provided by engineer.</note> @@ -3007,10 +4102,22 @@ This is your link for group %@!</source> <target>กำลังจะเข้าร่วมกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep" xml:space="preserve"> + <source>Keep</source> + <note>alert action</note> + </trans-unit> + <trans-unit id="Keep conversation" xml:space="preserve"> + <source>Keep conversation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Keep the app open to use it from desktop" xml:space="preserve"> <source>Keep the app open to use it from desktop</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep unused invitation?" xml:space="preserve"> + <source>Keep unused invitation?</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> <source>Keep your connections</source> <target>รักษาการเชื่อมต่อของคุณ</target> @@ -3044,6 +4151,14 @@ This is your link for group %@!</source> <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> <target>ออกจาก</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Leave chat" xml:space="preserve"> + <source>Leave chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave chat?" xml:space="preserve"> + <source>Leave chat?</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group" xml:space="preserve"> @@ -3083,6 +4198,18 @@ This is your link for group %@!</source> <source>Linked desktops</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="List" xml:space="preserve"> + <source>List</source> + <note>swipe action</note> + </trans-unit> + <trans-unit id="List name and emoji should be different for all lists." xml:space="preserve"> + <source>List name and emoji should be different for all lists.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List name..." xml:space="preserve"> + <source>List name...</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Live message!" xml:space="preserve"> <source>Live message!</source> <target>ข้อความสด!</target> @@ -3113,11 +4240,6 @@ This is your link for group %@!</source> <target>โหมดล็อค</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> - <source>Make a private connection</source> - <target>สร้างการเชื่อมต่อแบบส่วนตัว</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make one message disappear" xml:space="preserve"> <source>Make one message disappear</source> <target>ทำให้ข้อความหายไปหนึ่งข้อความ</target> @@ -3128,21 +4250,11 @@ This is your link for group %@!</source> <target>ทำให้โปรไฟล์เป็นส่วนตัว!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> - <target>ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ %@ อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน (%@)</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> <target>ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ WebRTC ICE อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <target>หลายคนถามว่า: *หาก SimpleX ไม่มีตัวระบุผู้ใช้ จะส่งข้อความได้อย่างไร?*</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> <source>Mark deleted for everyone</source> <target>ทำเครื่องหมายว่าลบแล้วสำหรับทุกคน</target> @@ -3168,11 +4280,31 @@ This is your link for group %@!</source> <target>สูงสุด 30 วินาที รับทันที</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Media & file servers" xml:space="preserve"> + <source>Media & file servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Medium" xml:space="preserve"> + <source>Medium</source> + <note>blur media</note> + </trans-unit> <trans-unit id="Member" xml:space="preserve"> <source>Member</source> <target>สมาชิก</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member inactive" xml:space="preserve"> + <source>Member inactive</source> + <note>item status text</note> + </trans-unit> + <trans-unit id="Member reports" xml:space="preserve"> + <source>Member reports</source> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All chat members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All chat members will be notified.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> <target>บทบาทของสมาชิกจะถูกเปลี่ยนเป็น "%@" สมาชิกกลุ่มทั้งหมดจะได้รับแจ้ง</target> @@ -3183,11 +4315,61 @@ This is your link for group %@!</source> <target>บทบาทของสมาชิกจะถูกเปลี่ยนเป็น "%@" สมาชิกจะได้รับคำเชิญใหม่</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from chat - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> <target>สมาชิกจะถูกลบออกจากกลุ่ม - ไม่สามารถยกเลิกได้!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Members can add message reactions." xml:space="preserve"> + <source>Members can add message reactions.</source> + <target>สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Members can irreversibly delete sent messages. (24 hours)</source> + <target>สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can report messsages to moderators." xml:space="preserve"> + <source>Members can report messsages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send SimpleX links." xml:space="preserve"> + <source>Members can send SimpleX links.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> + <target>สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> + <target>สมาชิกกลุ่มสามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send files and media." xml:space="preserve"> + <source>Members can send files and media.</source> + <target>สมาชิกกลุ่มสามารถส่งไฟล์และสื่อ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> + <target>สมาชิกกลุ่มสามารถส่งข้อความเสียง</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mention members 👋" xml:space="preserve"> + <source>Mention members 👋</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Menus" xml:space="preserve"> + <source>Menus</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>ข้อผิดพลาดในการส่งข้อความ</target> @@ -3198,11 +4380,27 @@ This is your link for group %@!</source> <target>ใบเสร็จการส่งข้อความ!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message delivery warning" xml:space="preserve"> + <source>Message delivery warning</source> + <note>item status text</note> + </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> <target>ร่างข้อความ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message forwarded" xml:space="preserve"> + <source>Message forwarded</source> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message may be delivered later if member becomes active." xml:space="preserve"> + <source>Message may be delivered later if member becomes active.</source> + <note>item status description</note> + </trans-unit> + <trans-unit id="Message queue info" xml:space="preserve"> + <source>Message queue info</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message reactions" xml:space="preserve"> <source>Message reactions</source> <target>ปฏิกิริยาของข้อความ</target> @@ -3213,16 +4411,44 @@ This is your link for group %@!</source> <target>ห้ามแสดงปฏิกิริยาบนข้อความในแชทนี้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> - <source>Message reactions are prohibited in this group.</source> + <trans-unit id="Message reactions are prohibited." xml:space="preserve"> + <source>Message reactions are prohibited.</source> <target>ปฏิกิริยาบนข้อความเป็นสิ่งต้องห้ามในกลุ่มนี้</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message reception" xml:space="preserve"> + <source>Message reception</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message servers" xml:space="preserve"> + <source>Message servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message shape" xml:space="preserve"> + <source>Message shape</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message source remains private." xml:space="preserve"> + <source>Message source remains private.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status" xml:space="preserve"> + <source>Message status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status: %@" xml:space="preserve"> + <source>Message status: %@</source> + <note>copied message info</note> + </trans-unit> <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> <target>ข้อความ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message too large" xml:space="preserve"> + <source>Message too large</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> <target>ข้อความ</target> @@ -3237,11 +4463,63 @@ This is your link for group %@!</source> <source>Messages from %@ will be shown!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Messages in this chat will never be deleted." xml:space="preserve"> + <source>Messages in this chat will never be deleted.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages received" xml:space="preserve"> + <source>Messages received</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages sent" xml:space="preserve"> + <source>Messages sent</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages were deleted after you selected them." xml:space="preserve"> + <source>Messages were deleted after you selected them.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate device" xml:space="preserve"> + <source>Migrate device</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate from another device" xml:space="preserve"> + <source>Migrate from another device</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate here" xml:space="preserve"> + <source>Migrate here</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device" xml:space="preserve"> + <source>Migrate to another device</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device via QR code." xml:space="preserve"> + <source>Migrate to another device via QR code.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating" xml:space="preserve"> + <source>Migrating</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migrating database archive…" xml:space="preserve"> <source>Migrating database archive…</source> <target>กำลังย้ายข้อมูลที่เก็บถาวรของฐานข้อมูล…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Migration complete" xml:space="preserve"> + <source>Migration complete</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> <target>ข้อผิดพลาดในการย้ายข้อมูล:</target> @@ -3257,9 +4535,9 @@ This is your link for group %@!</source> <target>การโยกย้ายเสร็จสมบูรณ์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> - <target>การย้ายข้อมูล: %@</target> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> + <target>การย้ายข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> @@ -3277,20 +4555,27 @@ This is your link for group %@!</source> <target>กลั่นกรองที่: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="More" xml:space="preserve"> + <source>More</source> + <note>swipe action</note> + </trans-unit> <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> <target>การปรับปรุงเพิ่มเติมกำลังจะมาเร็ว ๆ นี้!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="More reliable network connection." xml:space="preserve"> + <source>More reliable network connection.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="More reliable notifications" xml:space="preserve"> + <source>More reliable notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> <source>Most likely this connection is deleted.</source> <note>item status description</note> </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> - <source>Most likely this contact has deleted the connection with you.</source> - <target>เป็นไปได้มากว่าผู้ติดต่อนี้ได้ลบการเชื่อมต่อกับคุณ</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> <target>โปรไฟล์การแชทหลายรายการ</target> @@ -3299,7 +4584,11 @@ This is your link for group %@!</source> <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> <target>ปิดเสียง</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Mute all" xml:space="preserve"> + <source>Mute all</source> + <note>notification label action</note> </trans-unit> <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> @@ -3309,13 +4598,33 @@ This is your link for group %@!</source> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> <target>ชื่อ</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> <target>เครือข่ายและเซิร์ฟเวอร์</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Network connection" xml:space="preserve"> + <source>Network connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network decentralization" xml:space="preserve"> + <source>Network decentralization</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve"> + <source>Network issues - message expired after many attempts to send it.</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Network management" xml:space="preserve"> + <source>Network management</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network operator" xml:space="preserve"> + <source>Network operator</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> <target>การตั้งค่าเครือข่าย</target> @@ -3326,11 +4635,31 @@ This is your link for group %@!</source> <target>สถานะเครือข่าย</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New" xml:space="preserve"> + <source>New</source> + <note>token status text</note> + </trans-unit> <trans-unit id="New Passcode" xml:space="preserve"> <source>New Passcode</source> <target>รหัสผ่านใหม่</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve"> + <source>New SOCKS credentials will be used every time you start the app.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve"> + <source>New SOCKS credentials will be used for each server.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat" xml:space="preserve"> + <source>New chat</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat experience 🎉" xml:space="preserve"> + <source>New chat experience 🎉</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> <target>คำขอติดต่อใหม่</target> @@ -3341,11 +4670,6 @@ This is your link for group %@!</source> <target>คำขอติดต่อใหม่:</target> <note>notification</note> </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> - <source>New database archive</source> - <target>ฐานข้อมูลใหม่สำหรับการเก็บถาวร</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="New desktop app!" xml:space="preserve"> <source>New desktop app!</source> <note>No comment provided by engineer.</note> @@ -3355,11 +4679,19 @@ This is your link for group %@!</source> <target>ชื่อที่แสดงใหม่</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <note>notification</note> + </trans-unit> <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> <target>ใหม่ใน %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New media options" xml:space="preserve"> + <source>New media options</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> <target>บทบาทของสมาชิกใหม่</target> @@ -3375,6 +4707,10 @@ This is your link for group %@!</source> <target>รหัสผ่านใหม่…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New server" xml:space="preserve"> + <source>New server</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No" xml:space="preserve"> <source>No</source> <target>เลขที่</target> @@ -3385,6 +4721,18 @@ This is your link for group %@!</source> <target>ไม่มีรหัสผ่านสำหรับแอป</target> <note>Authentication unavailable</note> </trans-unit> + <trans-unit id="No chats" xml:space="preserve"> + <source>No chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats found" xml:space="preserve"> + <source>No chats found</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats in list %@" xml:space="preserve"> + <source>No chats in list %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> <target>ไม่ได้เลือกผู้ติดต่อ</target> @@ -3404,6 +4752,10 @@ This is your link for group %@!</source> <target>ไม่มีโทเค็นอุปกรณ์!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No direct connection yet, message is forwarded by admin." xml:space="preserve"> + <source>No direct connection yet, message is forwarded by admin.</source> + <note>item status description</note> + </trans-unit> <trans-unit id="No filtered chats" xml:space="preserve"> <source>No filtered chats</source> <target>ไม่มีการกรองการแชท</target> @@ -3419,20 +4771,94 @@ This is your link for group %@!</source> <target>ไม่มีประวัติ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No info, try to reload" xml:space="preserve"> + <source>No info, try to reload</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No media & file servers." xml:space="preserve"> + <source>No media & file servers.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No message" xml:space="preserve"> + <source>No message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No message servers." xml:space="preserve"> + <source>No message servers.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No network connection" xml:space="preserve"> + <source>No network connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record speech" xml:space="preserve"> + <source>No permission to record speech</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record video" xml:space="preserve"> + <source>No permission to record video</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> <source>No permission to record voice message</source> <target>ไม่อนุญาตให้บันทึกข้อความเสียง</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No push server" xml:space="preserve"> + <source>No push server</source> + <target>ในเครื่อง</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> <target>ไม่มีไฟล์ที่ได้รับหรือส่ง</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No servers for private message routing." xml:space="preserve"> + <source>No servers for private message routing.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive files." xml:space="preserve"> + <source>No servers to receive files.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive messages." xml:space="preserve"> + <source>No servers to receive messages.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to send files." xml:space="preserve"> + <source>No servers to send files.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No token!" xml:space="preserve"> + <source>No token!</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="No unread chats" xml:space="preserve"> + <source>No unread chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> + <target>แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Not compatible!" xml:space="preserve"> <source>Not compatible!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notes" xml:space="preserve"> + <source>Notes</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing selected" xml:space="preserve"> + <source>Nothing selected</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing to forward!" xml:space="preserve"> + <source>Nothing to forward!</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> <target>การแจ้งเตือน</target> @@ -3443,6 +4869,18 @@ This is your link for group %@!</source> <target>ปิดการแจ้งเตือน!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notifications error" xml:space="preserve"> + <source>Notifications error</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Notifications privacy" xml:space="preserve"> + <source>Notifications privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications status" xml:space="preserve"> + <source>Notifications status</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. @@ -3452,43 +4890,39 @@ This is your link for group %@!</source> - ปิดการใช้งานสมาชิก (บทบาท "ผู้สังเกตการณ์")</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="OK" xml:space="preserve"> + <source>OK</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Off" xml:space="preserve"> <source>Off</source> <target>ปิด</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> - <source>Off (Local)</source> - <target>ปิด (ในเครื่อง)</target> - <note>No comment provided by engineer.</note> + <note>blur media</note> </trans-unit> <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> <target>ตกลง</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> <target>ฐานข้อมูลเก่า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> - <source>Old database archive</source> - <target>คลังฐานข้อมูลเก่า</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> <target>ลิงก์คำเชิญแบบใช้ครั้งเดียว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> + <trans-unit id="Onion hosts will be **required** for connection. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be **required** for connection. +Requires compatible VPN.</source> <target>จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> + <trans-unit id="Onion hosts will be used when available. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be used when available. +Requires compatible VPN.</source> <target>จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3497,11 +4931,19 @@ This is your link for group %@!</source> <target>โฮสต์หัวหอมจะไม่ถูกใช้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only chat owners can change preferences." xml:space="preserve"> + <source>Only chat owners can change preferences.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <target>เฉพาะอุปกรณ์ไคลเอนต์เท่านั้นที่จัดเก็บโปรไฟล์ผู้ใช้ ผู้ติดต่อ กลุ่ม และข้อความที่ส่งด้วย **การเข้ารหัส encrypt แบบ 2 ชั้น**</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only delete conversation" xml:space="preserve"> + <source>Only delete conversation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> <source>Only group owners can change group preferences.</source> <target>เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปลี่ยนค่ากําหนดลักษณะกลุ่มได้</target> @@ -3517,13 +4959,21 @@ This is your link for group %@!</source> <target>เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปิดใช้งานข้อความเสียงได้</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only sender and moderators see it" xml:space="preserve"> + <source>Only sender and moderators see it</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you and moderators see it" xml:space="preserve"> + <source>Only you and moderators see it</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only you can add message reactions." xml:space="preserve"> <source>Only you can add message reactions.</source> <target>มีเพียงคุณเท่านั้นที่สามารถแสดงปฏิกิริยาต่อข้อความได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source> <target>มีเพียงคุณเท่านั้นที่สามารถลบข้อความแบบย้อนกลับไม่ได้ (ผู้ติดต่อของคุณสามารถทำเครื่องหมายเพื่อลบได้)</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3547,8 +4997,8 @@ This is your link for group %@!</source> <target>เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถเพิ่มการโต้ตอบข้อความได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source> <target>เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถลบข้อความแบบย้อนกลับไม่ได้ (คุณสามารถทำเครื่องหมายเพื่อลบได้)</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3569,13 +5019,17 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Open" xml:space="preserve"> <source>Open</source> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>เปิดการตั้งค่า</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open changes" xml:space="preserve"> + <source>Open changes</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> <target>เปิดแชท</target> @@ -3586,25 +5040,71 @@ This is your link for group %@!</source> <target>เปิดคอนโซลการแชท</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Open conditions" xml:space="preserve"> + <source>Open conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open group" xml:space="preserve"> <source>Open group</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve"> - <source>Open user profiles</source> - <target>เปิดโปรไฟล์ผู้ใช้</target> + <trans-unit id="Open link?" xml:space="preserve"> + <source>Open link?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Open migration to another device" xml:space="preserve"> + <source>Open migration to another device</source> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> - <target>โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้</target> + <trans-unit id="Opening app…" xml:space="preserve"> + <source>Opening app…</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve"> - <source>Opening database…</source> - <target>กำลังเปิดฐานข้อมูล…</target> + <trans-unit id="Operator" xml:space="preserve"> + <source>Operator</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Operator server" xml:space="preserve"> + <source>Operator server</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Or import archive file" xml:space="preserve"> + <source>Or import archive file</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or paste archive link" xml:space="preserve"> + <source>Or paste archive link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or scan QR code" xml:space="preserve"> + <source>Or scan QR code</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or securely share this file link" xml:space="preserve"> + <source>Or securely share this file link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or show this code" xml:space="preserve"> + <source>Or show this code</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or to share privately" xml:space="preserve"> + <source>Or to share privately</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Organize chats into lists" xml:space="preserve"> + <source>Organize chats into lists</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other" xml:space="preserve"> + <source>Other</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other file errors: %@" xml:space="preserve"> + <source>Other file errors: +%@</source> + <note>alert message</note> + </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> <target>จํานวน PING</target> @@ -3640,15 +5140,18 @@ This is your link for group %@!</source> <target>ตั้งรหัสผ่านเรียบร้อยแล้ว!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Password" xml:space="preserve"> + <source>Password</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> <target>รหัสผ่านที่จะแสดง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste" xml:space="preserve"> - <source>Paste</source> - <target>แปะ</target> - <note>No comment provided by engineer.</note> + <trans-unit id="Past member %@" xml:space="preserve"> + <source>Past member %@</source> + <note>past/unknown group member</note> </trans-unit> <trans-unit id="Paste desktop address" xml:space="preserve"> <source>Paste desktop address</source> @@ -3659,22 +5162,20 @@ This is your link for group %@!</source> <target>แปะภาพ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve"> - <source>Paste received link</source> - <target>แปะลิงก์ที่ได้รับ</target> + <trans-unit id="Paste link to connect!" xml:space="preserve"> + <source>Paste link to connect!</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> - <source>Paste the link you received to connect with your contact.</source> - <note>placeholder</note> - </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> - <target>ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น</target> + <trans-unit id="Paste the link you received" xml:space="preserve"> + <source>Paste the link you received</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Periodically" xml:space="preserve"> - <source>Periodically</source> + <trans-unit id="Pending" xml:space="preserve"> + <source>Pending</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Periodic" xml:space="preserve"> + <source>Periodic</source> <target>เป็นระยะๆ</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3683,11 +5184,28 @@ This is your link for group %@!</source> <target>ข้อผิดพลาดในการถอดรหัสอย่างถาวร</target> <note>message decrypt error item</note> </trans-unit> + <trans-unit id="Picture-in-picture calls" xml:space="preserve"> + <source>Picture-in-picture calls</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Play from the chat list." xml:space="preserve"> + <source>Play from the chat list.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable calls." xml:space="preserve"> + <source>Please ask your contact to enable calls.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> <target>โปรดขอให้ผู้ติดต่อของคุณเปิดใช้งานการส่งข้อความเสียง</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers." xml:space="preserve"> + <source>Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> <target>โปรดตรวจสอบว่าคุณใช้ลิงก์ที่ถูกต้องหรือขอให้ผู้ติดต่อของคุณส่งลิงก์ใหม่ให้คุณ</target> @@ -3703,6 +5221,15 @@ This is your link for group %@!</source> <target>โปรดตรวจสอบความต้องการของคุณและการตั้งค่าผู้ติดต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve"> + <source>Please confirm that network settings are correct for this device.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact developers. Error: %@" xml:space="preserve"> + <source>Please contact developers. +Error: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> <target>โปรดติดต่อผู้ดูแลกลุ่ม</target> @@ -3743,11 +5270,27 @@ This is your link for group %@!</source> <target>โปรดจัดเก็บรหัสผ่านอย่างปลอดภัย คุณจะไม่สามารถเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please try to disable and re-enable notfications." xml:space="preserve"> + <source>Please try to disable and re-enable notfications.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token activation to complete." xml:space="preserve"> + <source>Please wait for token activation to complete.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token to be registered." xml:space="preserve"> + <source>Please wait for token to be registered.</source> + <note>token info</note> + </trans-unit> <trans-unit id="Polish interface" xml:space="preserve"> <source>Polish interface</source> <target>อินเตอร์เฟซภาษาโปแลนด์</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Port" xml:space="preserve"> + <source>Port</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> <target>อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง</target> @@ -3758,36 +5301,75 @@ This is your link for group %@!</source> <target>เก็บข้อความที่ร่างไว้ล่าสุดพร้อมไฟล์แนบ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Preset server" xml:space="preserve"> - <source>Preset server</source> - <target>เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> <target>ที่อยู่เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Preset servers" xml:space="preserve"> + <source>Preset servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Preview" xml:space="preserve"> <source>Preview</source> <target>ดูตัวอย่าง</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Previously connected servers" xml:space="preserve"> + <source>Previously connected servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> <target>ความเป็นส่วนตัวและความปลอดภัย</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Privacy for your customers." xml:space="preserve"> + <source>Privacy for your customers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy policy and conditions of use." xml:space="preserve"> + <source>Privacy policy and conditions of use.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy redefined" xml:space="preserve"> <source>Privacy redefined</source> <target>นิยามความเป็นส่วนตัวใหม่</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve"> + <source>Private chats, groups and your contacts are not accessible to server operators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> <target>ชื่อไฟล์ส่วนตัว</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private media file names." xml:space="preserve"> + <source>Private media file names.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing" xml:space="preserve"> + <source>Private message routing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing 🚀" xml:space="preserve"> + <source>Private message routing 🚀</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private notes" xml:space="preserve"> + <source>Private notes</source> + <note>name of notes to self</note> + </trans-unit> + <trans-unit id="Private routing" xml:space="preserve"> + <source>Private routing</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private routing error" xml:space="preserve"> + <source>Private routing error</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> <target>การเชื่อมต่อโปรไฟล์และเซิร์ฟเวอร์</target> @@ -3798,12 +5380,8 @@ This is your link for group %@!</source> <target>รูปโปรไฟล์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile name" xml:space="preserve"> - <source>Profile name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile name:" xml:space="preserve"> - <source>Profile name:</source> + <trans-unit id="Profile images" xml:space="preserve"> + <source>Profile images</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile password" xml:space="preserve"> @@ -3811,10 +5389,14 @@ This is your link for group %@!</source> <target>รหัสผ่านโปรไฟล์</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Profile theme" xml:space="preserve"> + <source>Profile theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> <source>Profile update will be sent to your contacts.</source> <target>การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> @@ -3836,6 +5418,14 @@ This is your link for group %@!</source> <target>ห้ามแสดงปฏิกิริยาต่อข้อความ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Prohibit reporting messages to moderators." xml:space="preserve"> + <source>Prohibit reporting messages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending SimpleX links." xml:space="preserve"> + <source>Prohibit sending SimpleX links.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> <target>ห้ามส่งข้อความโดยตรงถึงสมาชิก</target> @@ -3856,11 +5446,20 @@ This is your link for group %@!</source> <target>ห้ามส่งข้อความเสียง</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect IP address" xml:space="preserve"> + <source>Protect IP address</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> <target>ปกป้องหน้าจอแอป</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve"> + <source>Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> <target>ปกป้องโปรไฟล์การแชทของคุณด้วยรหัสผ่าน!</target> @@ -3876,16 +5475,40 @@ This is your link for group %@!</source> <target>การหมดเวลาของโปรโตคอลต่อ KB</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Proxied" xml:space="preserve"> + <source>Proxied</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied servers" xml:space="preserve"> + <source>Proxied servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxy requires password" xml:space="preserve"> + <source>Proxy requires password</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> <target>การแจ้งเตือนแบบทันที</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Push server" xml:space="preserve"> + <source>Push server</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Quantum resistant encryption" xml:space="preserve"> + <source>Quantum resistant encryption</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> <source>Rate the app</source> <target>ให้คะแนนแอป</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reachable chat toolbar" xml:space="preserve"> + <source>Reachable chat toolbar</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> <target>ตอบสนอง…</target> @@ -3894,16 +5517,20 @@ This is your link for group %@!</source> <trans-unit id="Read" xml:space="preserve"> <source>Read</source> <target>อ่าน</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Read more" xml:space="preserve"> <source>Read more</source> <target>อ่านเพิ่มเติม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> - <target>อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)</target> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</source> + <target>อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> @@ -3911,11 +5538,6 @@ This is your link for group %@!</source> <target>อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/readme.html#connect-to-friends)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> - <source>Read more in our GitHub repository.</source> - <target>อ่านเพิ่มเติมในที่เก็บ GitHub ของเรา</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> <target>อ่านเพิ่มเติมใน[พื้นที่เก็บข้อมูล GitHub](https://github.com/simplex-chat/simplex-chat#readme)</target> @@ -3925,6 +5547,10 @@ This is your link for group %@!</source> <source>Receipts are disabled</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receive errors" xml:space="preserve"> + <source>Receive errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>ได้รับเมื่อ</target> @@ -3945,6 +5571,18 @@ This is your link for group %@!</source> <target>ได้รับข้อความ</target> <note>message info title</note> </trans-unit> + <trans-unit id="Received messages" xml:space="preserve"> + <source>Received messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received reply" xml:space="preserve"> + <source>Received reply</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received total" xml:space="preserve"> + <source>Received total</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> <target>ที่อยู่ผู้รับจะถูกเปลี่ยนเป็นเซิร์ฟเวอร์อื่น การเปลี่ยนแปลงที่อยู่จะเสร็จสมบูรณ์หลังจากที่ผู้ส่งออนไลน์</target> @@ -3960,16 +5598,44 @@ This is your link for group %@!</source> <target>กำลังรับผ่าน</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Recent history and improved [directory bot](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)." xml:space="preserve"> + <source>Recent history and improved [directory bot](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).</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve"> + <source>Recipient(s) can't see who this message is from.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> <target>ผู้รับจะเห็นการอัปเดตเมื่อคุณพิมพ์</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect" xml:space="preserve"> + <source>Reconnect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> <target>เชื่อมต่อเซิร์ฟเวอร์ที่เชื่อมต่อทั้งหมดอีกครั้งเพื่อบังคับให้ส่งข้อความ มันใช้การจราจรเพิ่มเติม</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect all servers" xml:space="preserve"> + <source>Reconnect all servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers?" xml:space="preserve"> + <source>Reconnect all servers?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect server to force message delivery. It uses additional traffic.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server?" xml:space="preserve"> + <source>Reconnect server?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> <target>เชื่อมต่อเซิร์ฟเวอร์อีกครั้งหรือไม่?</target> @@ -3990,10 +5656,23 @@ This is your link for group %@!</source> <target>ลดการใช้แบตเตอรี่</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Register" xml:space="preserve"> + <source>Register</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Register notification token?" xml:space="preserve"> + <source>Register notification token?</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Registered" xml:space="preserve"> + <source>Registered</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> <target>ปฏิเสธ</target> - <note>reject incoming call via notification</note> + <note>reject incoming call via notification +swipe action</note> </trans-unit> <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> <source>Reject (sender NOT notified)</source> @@ -4019,6 +5698,14 @@ This is your link for group %@!</source> <target>ลบ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Remove archive?" xml:space="preserve"> + <source>Remove archive?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove image" xml:space="preserve"> + <source>Remove image</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> <target>ลบสมาชิกออก</target> @@ -4053,15 +5740,67 @@ This is your link for group %@!</source> <source>Repeat connection request?</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat download" xml:space="preserve"> + <source>Repeat download</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat import" xml:space="preserve"> + <source>Repeat import</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Repeat join request?" xml:space="preserve"> <source>Repeat join request?</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Repeat upload" xml:space="preserve"> + <source>Repeat upload</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reply" xml:space="preserve"> <source>Reply</source> <target>ตอบ</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Report" xml:space="preserve"> + <source>Report</source> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Report content: only group moderators will see it." xml:space="preserve"> + <source>Report content: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report member profile: only group moderators will see it." xml:space="preserve"> + <source>Report member profile: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report other: only group moderators will see it." xml:space="preserve"> + <source>Report other: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report reason?" xml:space="preserve"> + <source>Report reason?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Report spam: only group moderators will see it." xml:space="preserve"> + <source>Report spam: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report violation: only group moderators will see it." xml:space="preserve"> + <source>Report violation: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report: %@" xml:space="preserve"> + <source>Report: %@</source> + <note>report in notification</note> + </trans-unit> + <trans-unit id="Reporting messages to moderators is prohibited." xml:space="preserve"> + <source>Reporting messages to moderators is prohibited.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reports" xml:space="preserve"> + <source>Reports</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Required" xml:space="preserve"> <source>Required</source> <target>ที่จำเป็น</target> @@ -4072,16 +5811,36 @@ This is your link for group %@!</source> <target>รีเซ็ต</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset all hints" xml:space="preserve"> + <source>Reset all hints</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics" xml:space="preserve"> + <source>Reset all statistics</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics?" xml:space="preserve"> + <source>Reset all statistics?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> <target>รีเซ็ตสี</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to app theme" xml:space="preserve"> + <source>Reset to app theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> <target>รีเซ็ตเป็นค่าเริ่มต้น</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to user theme" xml:space="preserve"> + <source>Reset to user theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> <source>Restart the app to create a new chat profile</source> <target>รีสตาร์ทแอปเพื่อสร้างโปรไฟล์แชทใหม่</target> @@ -4112,14 +5871,17 @@ This is your link for group %@!</source> <target>กู้คืนข้อผิดพลาดของฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Retry" xml:space="preserve"> + <source>Retry</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> <target>เปิดเผย</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Revert" xml:space="preserve"> - <source>Revert</source> - <target>เปลี่ยนกลับ</target> + <trans-unit id="Review conditions" xml:space="preserve"> + <source>Review conditions</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke" xml:space="preserve"> @@ -4147,51 +5909,61 @@ This is your link for group %@!</source> <target>เรียกใช้แชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve"> - <source>SMP servers</source> - <target>เซิร์ฟเวอร์ SMP</target> + <trans-unit id="SMP server" xml:space="preserve"> + <source>SMP server</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SOCKS proxy" xml:space="preserve"> + <source>SOCKS proxy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safely receive files" xml:space="preserve"> + <source>Safely receive files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safer groups" xml:space="preserve"> + <source>Safer groups</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save" xml:space="preserve"> <source>Save</source> <target>บันทึก</target> - <note>chat item action</note> + <note>alert button +chat item action</note> </trans-unit> <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> <target>บันทึก (และแจ้งผู้ติดต่อ)</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> <target>บันทึกและแจ้งผู้ติดต่อ</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> <target>บันทึกและแจ้งให้สมาชิกในกลุ่มทราบ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save and reconnect" xml:space="preserve"> + <source>Save and reconnect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save and update group profile" xml:space="preserve"> <source>Save and update group profile</source> <target>บันทึกและอัปเดตโปรไฟล์กลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save archive" xml:space="preserve"> - <source>Save archive</source> - <target>บันทึกไฟล์เก็บถาวร</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve"> - <source>Save auto-accept settings</source> - <target>บันทึกการตั้งค่าการยอมรับอัตโนมัติ</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> <source>Save group profile</source> <target>บันทึกโปรไฟล์กลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save list" xml:space="preserve"> + <source>Save list</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> <target>บันทึกรหัสผ่านและเปิดแชท</target> @@ -4205,7 +5977,7 @@ This is your link for group %@!</source> <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> <target>บันทึกการตั้งค่า?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> @@ -4220,23 +5992,46 @@ This is your link for group %@!</source> <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> <target>บันทึกเซิร์ฟเวอร์?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve"> - <source>Save settings?</source> - <target>บันทึกการตั้งค่า?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> <target>บันทึกข้อความต้อนรับ?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save your profile?" xml:space="preserve"> + <source>Save your profile?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Saved" xml:space="preserve"> + <source>Saved</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> <source>Saved WebRTC ICE servers will be removed</source> <target>เซิร์ฟเวอร์ WebRTC ICE ที่บันทึกไว้จะถูกลบออก</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Saved from" xml:space="preserve"> + <source>Saved from</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved message" xml:space="preserve"> + <source>Saved message</source> + <note>message info title</note> + </trans-unit> + <trans-unit id="Saving %lld messages" xml:space="preserve"> + <source>Saving %lld messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scale" xml:space="preserve"> + <source>Scale</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan / Paste link" xml:space="preserve"> + <source>Scan / Paste link</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> <target>สแกนคิวอาร์โค้ด</target> @@ -4266,11 +6061,27 @@ This is your link for group %@!</source> <target>ค้นหา</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Search bar accepts invitation links." xml:space="preserve"> + <source>Search bar accepts invitation links.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search or paste SimpleX link" xml:space="preserve"> + <source>Search or paste SimpleX link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secondary" xml:space="preserve"> + <source>Secondary</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> <source>Secure queue</source> <target>คิวที่ปลอดภัย</target> <note>server test step</note> </trans-unit> + <trans-unit id="Secured" xml:space="preserve"> + <source>Secured</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> <target>การประเมินความปลอดภัย</target> @@ -4284,6 +6095,18 @@ This is your link for group %@!</source> <trans-unit id="Select" xml:space="preserve"> <source>Select</source> <target>เลือก</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Select chat profile" xml:space="preserve"> + <source>Select chat profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected %lld" xml:space="preserve"> + <source>Selected %lld</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct" xml:space="preserve"> @@ -4321,11 +6144,6 @@ This is your link for group %@!</source> <target>ส่งใบเสร็จรับการจัดส่งข้อความไปที่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve"> - <source>Send direct message</source> - <target>ส่งข้อความโดยตรง</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Send direct message to connect" xml:space="preserve"> <source>Send direct message to connect</source> <note>No comment provided by engineer.</note> @@ -4335,6 +6153,10 @@ This is your link for group %@!</source> <target>ส่งข้อความแบบที่หายไป</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send errors" xml:space="preserve"> + <source>Send errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> <target>ส่งตัวอย่างลิงก์</target> @@ -4345,14 +6167,25 @@ This is your link for group %@!</source> <target>ส่งข้อความสด</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send message to enable calls." xml:space="preserve"> + <source>Send message to enable calls.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when your or destination server does not support private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> <target>ส่งการแจ้งเตือน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> - <source>Send notifications:</source> - <target>ส่งการแจ้งเตือน:</target> + <trans-unit id="Send private reports" xml:space="preserve"> + <source>Send private reports</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve"> @@ -4370,10 +6203,14 @@ This is your link for group %@!</source> <target>ส่งจากแกลเลอรีหรือแป้นพิมพ์แบบกำหนดเอง</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve"> + <source>Send up to 100 last messages to new members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> <target>ผู้ส่งยกเลิกการโอนไฟล์</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> @@ -4428,6 +6265,10 @@ This is your link for group %@!</source> <target>ส่งเมื่อ: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Sent directly" xml:space="preserve"> + <source>Sent directly</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> <target>เหตุการณ์ไฟล์ที่ส่ง</target> @@ -4438,11 +6279,59 @@ This is your link for group %@!</source> <target>ข้อความที่ส่งแล้ว</target> <note>message info title</note> </trans-unit> + <trans-unit id="Sent messages" xml:space="preserve"> + <source>Sent messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> <target>ข้อความที่ส่งจะถูกลบหลังเกินเวลาที่กําหนด</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sent reply" xml:space="preserve"> + <source>Sent reply</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent total" xml:space="preserve"> + <source>Sent total</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent via proxy" xml:space="preserve"> + <source>Sent via proxy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server" xml:space="preserve"> + <source>Server</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server added to operator %@." xml:space="preserve"> + <source>Server added to operator %@.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Server address" xml:space="preserve"> + <source>Server address</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings." xml:space="preserve"> + <source>Server address is incompatible with network settings.</source> + <note>srv error text.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Server address is incompatible with network settings: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server operator changed." xml:space="preserve"> + <source>Server operator changed.</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Server operators" xml:space="preserve"> + <source>Server operators</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server protocol changed." xml:space="preserve"> + <source>Server protocol changed.</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> <target>เซิร์ฟเวอร์ต้องการการอนุญาตในการสร้างคิว โปรดตรวจสอบรหัสผ่าน</target> @@ -4458,11 +6347,31 @@ This is your link for group %@!</source> <target>การทดสอบเซิร์ฟเวอร์ล้มเหลว!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Server type" xml:space="preserve"> + <source>Server type</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server version is incompatible with network settings." xml:space="preserve"> + <source>Server version is incompatible with network settings.</source> + <note>srv error text</note> + </trans-unit> + <trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve"> + <source>Server version is incompatible with your app: %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Servers" xml:space="preserve"> <source>Servers</source> <target>เซิร์ฟเวอร์</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Servers info" xml:space="preserve"> + <source>Servers info</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Session code" xml:space="preserve"> <source>Session code</source> <note>No comment provided by engineer.</note> @@ -4472,11 +6381,19 @@ This is your link for group %@!</source> <target>ตั้ง 1 วัน</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set chat name…" xml:space="preserve"> + <source>Set chat name…</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> <target>ตั้งชื่อผู้ติดต่อ…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve"> + <source>Set default theme</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> <target>ตั้งค่าการกําหนดลักษณะกลุ่ม</target> @@ -4487,11 +6404,19 @@ This is your link for group %@!</source> <target>ตั้งแทนการรับรองความถูกต้องของระบบ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve"> + <source>Set message expiration in chats.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passcode" xml:space="preserve"> <source>Set passcode</source> <target>ตั้งรหัสผ่าน</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set passphrase" xml:space="preserve"> + <source>Set passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> <target>ตั้งรหัสผ่านเพื่อส่งออก</target> @@ -4512,24 +6437,49 @@ This is your link for group %@!</source> <target>การตั้งค่า</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve"> + <source>Settings were changed.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve"> + <source>Shape profile images</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share" xml:space="preserve"> <source>Share</source> <target>แชร์</target> - <note>chat item action</note> + <note>alert action +chat item action</note> </trans-unit> <trans-unit id="Share 1-time link" xml:space="preserve"> <source>Share 1-time link</source> <target>แชร์ลิงก์แบบใช้ครั้งเดียว</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share 1-time link with a friend" xml:space="preserve"> + <source>Share 1-time link with a friend</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve"> + <source>Share SimpleX address on social media.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address" xml:space="preserve"> <source>Share address</source> <target>แชร์ที่อยู่</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share address publicly" xml:space="preserve"> + <source>Share address publicly</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address with contacts?" xml:space="preserve"> <source>Share address with contacts?</source> <target>แชร์ที่อยู่กับผู้ติดต่อ?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Share from other apps." xml:space="preserve"> + <source>Share from other apps.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share link" xml:space="preserve"> @@ -4537,9 +6487,16 @@ This is your link for group %@!</source> <target>แชร์ลิงก์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve"> - <source>Share one-time invitation link</source> - <target>แชร์ลิงก์เชิญแบบใช้ครั้งเดียว</target> + <trans-unit id="Share profile" xml:space="preserve"> + <source>Share profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share this 1-time invite link" xml:space="preserve"> + <source>Share this 1-time invite link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share to SimpleX" xml:space="preserve"> + <source>Share to SimpleX</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share with contacts" xml:space="preserve"> @@ -4547,6 +6504,14 @@ This is your link for group %@!</source> <target>แชร์กับผู้ติดต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Short link" xml:space="preserve"> + <source>Short link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show QR code" xml:space="preserve"> + <source>Show QR code</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> <target>แสดงการโทรในประวัติการโทร</target> @@ -4561,21 +6526,41 @@ This is your link for group %@!</source> <source>Show last messages</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show message status" xml:space="preserve"> + <source>Show message status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show percentage" xml:space="preserve"> + <source>Show percentage</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>แสดงตัวอย่าง</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show → on messages sent via private routing." xml:space="preserve"> + <source>Show → on messages sent via private routing.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show:" xml:space="preserve"> <source>Show:</source> <target>แสดง:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve"> + <source>SimpleX</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Address" xml:space="preserve"> <source>SimpleX Address</source> <target>ที่อยู่ SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." xml:space="preserve"> + <source>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> <target>ความปลอดภัยของ SimpleX Chat ได้รับการตรวจสอบโดย Trail of Bits</target> @@ -4606,6 +6591,18 @@ This is your link for group %@!</source> <target>ที่อยู่ SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX address and 1-time links are safe to share via any messenger." xml:space="preserve"> + <source>SimpleX address and 1-time links are safe to share via any messenger.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address or 1-time link?" xml:space="preserve"> + <source>SimpleX address or 1-time link?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX channel link" xml:space="preserve"> + <source>SimpleX channel link</source> + <note>simplex link type</note> + </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> <target>ที่อยู่ติดต่อ SimpleX</target> @@ -4624,6 +6621,14 @@ This is your link for group %@!</source> <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> <target>ลิงก์ SimpleX</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve"> + <source>SimpleX links are prohibited.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve"> + <source>SimpleX links not allowed</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> @@ -4631,10 +6636,18 @@ This is your link for group %@!</source> <target>คำเชิญ SimpleX แบบครั้งเดียว</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> + <source>SimpleX protocols reviewed by Trail of Bits.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Simplified incognito mode" xml:space="preserve"> <source>Simplified incognito mode</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Size" xml:space="preserve"> + <source>Size</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>ข้าม</target> @@ -4649,19 +6662,44 @@ This is your link for group %@!</source> <source>Small groups (max 20)</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Soft" xml:space="preserve"> + <source>Soft</source> + <note>blur media</note> + </trans-unit> + <trans-unit id="Some app settings were not migrated." xml:space="preserve"> + <source>Some app settings were not migrated.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some file(s) were not exported:" xml:space="preserve"> + <source>Some file(s) were not exported:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>ข้อผิดพลาดที่ไม่ร้ายแรงบางอย่างเกิดขึ้นระหว่างการนำเข้า - คุณอาจดูรายละเอียดเพิ่มเติมได้ที่คอนโซล Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import:" xml:space="preserve"> + <source>Some non-fatal errors occurred during import:</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some servers failed the test: %@" xml:space="preserve"> + <source>Some servers failed the test: +%@</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> <target>ใครบางคน</target> <note>notification title</note> </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve"> - <source>Start a new chat</source> - <target>เริ่มแชทใหม่</target> + <trans-unit id="Spam" xml:space="preserve"> + <source>Spam</source> + <note>blocking reason +report reason</note> + </trans-unit> + <trans-unit id="Square, circle, or anything in between." xml:space="preserve"> + <source>Square, circle, or anything in between.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start chat" xml:space="preserve"> @@ -4669,11 +6707,23 @@ This is your link for group %@!</source> <target>เริ่มแชท</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Start chat?" xml:space="preserve"> + <source>Start chat?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> <target>เริ่มการย้ายข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Starting from %@." xml:space="preserve"> + <source>Starting from %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Statistics" xml:space="preserve"> + <source>Statistics</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> <target>หยุด</target> @@ -4684,9 +6734,8 @@ This is your link for group %@!</source> <target>หยุด SimpleX</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> - <source>Stop chat to enable database actions</source> - <target>หยุดการแชทเพื่อเปิดใช้งานการดำเนินการกับฐานข้อมูล</target> + <trans-unit id="Stop chat" xml:space="preserve"> + <source>Stop chat</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> @@ -4717,23 +6766,55 @@ This is your link for group %@!</source> <trans-unit id="Stop sharing" xml:space="preserve"> <source>Stop sharing</source> <target>หยุดแชร์</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Stop sharing address?" xml:space="preserve"> <source>Stop sharing address?</source> <target>หยุดแชร์ที่อยู่ไหม?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Stopping chat" xml:space="preserve"> + <source>Stopping chat</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Storage" xml:space="preserve"> + <source>Storage</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Strong" xml:space="preserve"> + <source>Strong</source> + <note>blur media</note> + </trans-unit> <trans-unit id="Submit" xml:space="preserve"> <source>Submit</source> <target>ส่ง</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Subscribed" xml:space="preserve"> + <source>Subscribed</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscription errors" xml:space="preserve"> + <source>Subscription errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscriptions ignored" xml:space="preserve"> + <source>Subscriptions ignored</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Support SimpleX Chat" xml:space="preserve"> <source>Support SimpleX Chat</source> <target>สนับสนุน SimpleX แชท</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Switch audio and video during the call." xml:space="preserve"> + <source>Switch audio and video during the call.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> + <source>Switch chat profile for 1-time invitations.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="System" xml:space="preserve"> <source>System</source> <target>ระบบ</target> @@ -4744,11 +6825,19 @@ This is your link for group %@!</source> <target>การรับรองความถูกต้องของระบบ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP connection" xml:space="preserve"> + <source>TCP connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> <target>หมดเวลาการเชื่อมต่อ TCP</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP port for messaging" xml:space="preserve"> + <source>TCP port for messaging</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <source>TCP_KEEPCNT</source> <target>TCP_KEEPCNT</target> @@ -4764,11 +6853,19 @@ This is your link for group %@!</source> <target>TCP_KEEPINTVL</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tail" xml:space="preserve"> + <source>Tail</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> <target>ถ่ายภาพ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap Create SimpleX address in the menu to create it later." xml:space="preserve"> + <source>Tap Create SimpleX address in the menu to create it later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> <target>แตะปุ่ม </target> @@ -4793,16 +6890,27 @@ This is your link for group %@!</source> <target>แตะเพื่อเข้าร่วมโหมดไม่ระบุตัวตน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve"> - <source>Tap to start a new chat</source> - <target>แตะเพื่อเริ่มแชทใหม่</target> + <trans-unit id="Tap to paste link" xml:space="preserve"> + <source>Tap to paste link</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap to scan" xml:space="preserve"> + <source>Tap to scan</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Temporary file error" xml:space="preserve"> + <source>Temporary file error</source> + <note>file error alert title</note> + </trans-unit> <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> <target>การทดสอบล้มเหลวในขั้นตอน %@</target> <note>server test failure</note> </trans-unit> + <trans-unit id="Test notifications" xml:space="preserve"> + <source>Test notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> <target>เซิร์ฟเวอร์ทดสอบ</target> @@ -4816,7 +6924,7 @@ This is your link for group %@!</source> <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> <target>การทดสอบล้มเหลว!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> @@ -4833,11 +6941,6 @@ This is your link for group %@!</source> <target>ขอบคุณผู้ใช้ – มีส่วนร่วมผ่าน Weblate!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> - <target>แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <source>The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised.</source> @@ -4851,11 +6954,27 @@ It can happen because of some bug or when the connection is compromised.</source <target>แอปสามารถแจ้งให้คุณทราบเมื่อคุณได้รับข้อความหรือคำขอติดต่อ - โปรดเปิดการตั้งค่าเพื่อเปิดใช้งาน</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The app protects your privacy by using different operators in each conversation." xml:space="preserve"> + <source>The app protects your privacy by using different operators in each conversation.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve"> + <source>The app will ask to confirm downloads from unknown file servers (except .onion).</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> <source>The attempt to change database passphrase was not completed.</source> <target>ความพยายามในการเปลี่ยนรหัสผ่านของฐานข้อมูลไม่เสร็จสมบูรณ์</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve"> + <source>The code you scanned is not a SimpleX link QR code.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The connection reached the limit of undelivered messages, your contact may be offline." xml:space="preserve"> + <source>The connection reached the limit of undelivered messages, your contact may be offline.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> <target>การเชื่อมต่อที่คุณยอมรับจะถูกยกเลิก!</target> @@ -4876,6 +6995,11 @@ It can happen because of some bug or when the connection is compromised.</source <target>encryption กำลังทำงานและไม่จำเป็นต้องใช้ข้อตกลง encryption ใหม่ อาจทำให้การเชื่อมต่อผิดพลาดได้!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> + <target>การส่งข้อความส่วนตัวรุ่นต่อไป</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The hash of the previous message is different." xml:space="preserve"> <source>The hash of the previous message is different.</source> <target>แฮชของข้อความก่อนหน้านี้แตกต่างกัน</target> @@ -4891,9 +7015,12 @@ It can happen because of some bug or when the connection is compromised.</source <target>ข้อความจะถูกทำเครื่องหมายว่ากลั่นกรองสำหรับสมาชิกทุกคน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> - <target>การส่งข้อความส่วนตัวรุ่นต่อไป</target> + <trans-unit id="The messages will be deleted for all members." xml:space="preserve"> + <source>The messages will be deleted for all members.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve"> + <source>The messages will be marked as moderated for all members.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> @@ -4901,9 +7028,12 @@ It can happen because of some bug or when the connection is compromised.</source <target>ฐานข้อมูลเก่าไม่ได้ถูกลบในระหว่างการย้ายข้อมูล แต่สามารถลบได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> - <target>โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น</target> + <trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve"> + <source>The same conditions will apply to operator **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second preset operator in the app!" xml:space="preserve"> + <source>The second preset operator in the app!</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> @@ -4921,19 +7051,24 @@ It can happen because of some bug or when the connection is compromised.</source <target>เซิร์ฟเวอร์สำหรับการเชื่อมต่อใหม่ของโปรไฟล์การแชทปัจจุบันของคุณ **%@**</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Theme" xml:space="preserve"> - <source>Theme</source> - <target>ธีม</target> + <trans-unit id="The servers for new files of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new files of your current chat profile **%@**.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve"> - <source>There should be at least one user profile.</source> - <target>ควรมีโปรไฟล์ผู้ใช้อย่างน้อยหนึ่งโปรไฟล์</target> + <trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve"> + <source>The text you pasted is not a SimpleX link.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> - <source>There should be at least one visible user profile.</source> - <target>ควรมีอย่างน้อยหนึ่งโปรไฟล์ผู้ใช้ที่มองเห็นได้</target> + <trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve"> + <source>The uploaded database archive will be permanently removed from the servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Themes" xml:space="preserve"> + <source>Themes</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="These conditions will also apply for: **%@**." xml:space="preserve"> + <source>These conditions will also apply for: **%@**.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> @@ -4955,15 +7090,31 @@ It can happen because of some bug or when the connection is compromised.</source <target>การดำเนินการนี้ไม่สามารถเลิกทำได้ - ข้อความที่ส่งและรับก่อนหน้าที่เลือกไว้จะถูกลบ อาจใช้เวลาหลายนาที</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> <target>การดำเนินการนี้ไม่สามารถยกเลิกได้ - โปรไฟล์ ผู้ติดต่อ ข้อความ และไฟล์ของคุณจะสูญหายไปอย่างถาวร</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by end-to-end encryption.</source> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by quantum resistant end-to-end encryption.</source> + <note>E2EE info chat item</note> + </trans-unit> <trans-unit id="This device name" xml:space="preserve"> <source>This device name</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve"> + <source>This display name is invalid. Please choose another name.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> <source>This group has over %lld members, delivery receipts are not sent.</source> <note>No comment provided by engineer.</note> @@ -4981,11 +7132,27 @@ It can happen because of some bug or when the connection is compromised.</source <source>This is your own one-time link!</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve"> + <source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve"> + <source>This link was used with another mobile device, please create a new link on the desktop.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This message was deleted or not received yet." xml:space="preserve"> + <source>This message was deleted or not received yet.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <source>This setting applies to messages in your current chat profile **%@**.</source> <target>การตั้งค่านี้ใช้กับข้อความในโปรไฟล์แชทปัจจุบันของคุณ **%@**</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Title" xml:space="preserve"> + <source>Title</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> <source>To ask any questions and to receive updates:</source> <target>หากต้องการถามคำถามและรับการอัปเดต:</target> @@ -5005,9 +7172,8 @@ It can happen because of some bug or when the connection is compromised.</source <target>เพื่อสร้างการเชื่อมต่อใหม่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <target>เพื่อปกป้องความเป็นส่วนตัว แทนที่จะใช้ ID ผู้ใช้เหมือนที่แพลตฟอร์มอื่นๆใช้ SimpleX มีตัวระบุสำหรับคิวข้อความ โดยแยกจากกันสำหรับผู้ติดต่อแต่ละราย</target> + <trans-unit id="To protect against your link being replaced, you can compare contact security codes." xml:space="preserve"> + <source>To protect against your link being replaced, you can compare contact security codes.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -5015,6 +7181,10 @@ It can happen because of some bug or when the connection is compromised.</source <target>ไฟล์ภาพ/เสียงใช้ UTC เพื่อป้องกันเขตเวลา</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve"> + <source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> @@ -5022,6 +7192,23 @@ You will be prompted to complete authentication before this feature is enabled.< คุณจะได้รับแจ้งให้ยืนยันตัวตนให้เสร็จสมบูรณ์ก่อนที่จะเปิดใช้งานคุณลักษณะนี้</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> + <target>เพื่อปกป้องความเป็นส่วนตัว แทนที่จะใช้ ID ผู้ใช้เหมือนที่แพลตฟอร์มอื่นๆใช้ SimpleX มีตัวระบุสำหรับคิวข้อความ โดยแยกจากกันสำหรับผู้ติดต่อแต่ละราย</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To receive" xml:space="preserve"> + <source>To receive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve"> + <source>To record speech please grant permission to use Microphone.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record video please grant permission to use Camera." xml:space="preserve"> + <source>To record video please grant permission to use Camera.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> <target>ในการบันทึกข้อความเสียง โปรดให้สิทธิ์ในการใช้ไมโครโฟน</target> @@ -5032,25 +7219,53 @@ You will be prompted to complete authentication before this feature is enabled.< <target>หากต้องการเปิดเผยโปรไฟล์ที่ซ่อนอยู่ของคุณ ให้ป้อนรหัสผ่านแบบเต็มในช่องค้นหาในหน้า **โปรไฟล์แชทของคุณ**</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To send" xml:space="preserve"> + <source>To send</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> <source>To support instant push notifications the chat database has to be migrated.</source> <target>เพื่อรองรับการแจ้งเตือนแบบทันที ฐานข้อมูลการแชทจะต้องได้รับการโยกย้าย</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To use the servers of **%@**, accept conditions of use." xml:space="preserve"> + <source>To use the servers of **%@**, accept conditions of use.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> <target>ในการตรวจสอบการเข้ารหัสแบบ encrypt จากต้นจนจบ กับผู้ติดต่อของคุณ ให้เปรียบเทียบ (หรือสแกน) รหัสบนอุปกรณ์ของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle chat list:" xml:space="preserve"> + <source>Toggle chat list:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> <source>Toggle incognito when connecting.</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Token status: %@." xml:space="preserve"> + <source>Token status: %@.</source> + <note>token status</note> + </trans-unit> + <trans-unit id="Toolbar opacity" xml:space="preserve"> + <source>Toolbar opacity</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Total" xml:space="preserve"> + <source>Total</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>การแยกการขนส่ง</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Transport sessions" xml:space="preserve"> + <source>Transport sessions</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> <target>กำลังพยายามเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้ (ข้อผิดพลาด: %@)</target> @@ -5061,16 +7276,15 @@ You will be prompted to complete authentication before this feature is enabled.< <target>พยายามเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Turkish interface" xml:space="preserve"> + <source>Turkish interface</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> <target>ปิด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve"> - <source>Turn off notifications?</source> - <target>ปิดการแจ้งเตือนไหม?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> <target>เปิด</target> @@ -5085,18 +7299,25 @@ You will be prompted to complete authentication before this feature is enabled.< <source>Unblock</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock for all" xml:space="preserve"> + <source>Unblock for all</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member" xml:space="preserve"> <source>Unblock member</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unblock member for all?" xml:space="preserve"> + <source>Unblock member for all?</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Unblock member?" xml:space="preserve"> <source>Unblock member?</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve"> - <source>Unexpected error: %@</source> - <target>ข้อผิดพลาดที่ไม่คาดคิด: %@</target> - <note>item status description</note> + <trans-unit id="Undelivered messages" xml:space="preserve"> + <source>Undelivered messages</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -5106,7 +7327,7 @@ You will be prompted to complete authentication before this feature is enabled.< <trans-unit id="Unfav." xml:space="preserve"> <source>Unfav.</source> <target>เลิกชอบ</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> @@ -5143,6 +7364,10 @@ You will be prompted to complete authentication before this feature is enabled.< <target>ข้อผิดพลาดที่ไม่รู้จัก</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unknown servers!" xml:space="preserve"> + <source>Unknown servers!</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> <target>ยกเว้นกรณีที่คุณใช้อินเทอร์เฟซการโทรของ iOS ให้เปิดใช้งานโหมดห้ามรบกวนเพื่อหลีกเลี่ยงการรบกวน</target> @@ -5176,11 +7401,19 @@ To connect, please ask your contact to create another connection link and check <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> <target>เปิดเสียง</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> </trans-unit> <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> <target>เปลี่ยนเป็นยังไม่ได้อ่าน</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Unsupported connection link" xml:space="preserve"> + <source>Unsupported connection link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve"> + <source>Up to 100 last messages are sent to new members.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update" xml:space="preserve"> @@ -5188,11 +7421,6 @@ To connect, please ask your contact to create another connection link and check <target>อัปเดต</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> - <source>Update .onion hosts setting?</source> - <target>อัปเดตการตั้งค่าโฮสต์ .onion ไหม?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> <target>อัปเดตรหัสผ่านของฐานข้อมูล</target> @@ -5203,9 +7431,12 @@ To connect, please ask your contact to create another connection link and check <target>อัปเดตการตั้งค่าเครือข่ายไหม?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve"> - <source>Update transport isolation mode?</source> - <target>อัปเดตโหมดการแยกการขนส่งไหม?</target> + <trans-unit id="Update settings?" xml:space="preserve"> + <source>Update settings?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updated conditions" xml:space="preserve"> + <source>Updated conditions</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> @@ -5213,31 +7444,62 @@ To connect, please ask your contact to create another connection link and check <target>การอัปเดตการตั้งค่าจะเชื่อมต่อไคลเอนต์กับเซิร์ฟเวอร์ทั้งหมดอีกครั้ง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> - <target>การอัปเดตการตั้งค่านี้จะเชื่อมต่อไคลเอนต์กับเซิร์ฟเวอร์ทั้งหมดอีกครั้ง</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Upgrade and open chat" xml:space="preserve"> <source>Upgrade and open chat</source> <target>อัปเกรดและเปิดการแชท</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Upload errors" xml:space="preserve"> + <source>Upload errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload failed" xml:space="preserve"> + <source>Upload failed</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Upload file" xml:space="preserve"> <source>Upload file</source> <target>อัปโหลดไฟล์</target> <note>server test step</note> </trans-unit> + <trans-unit id="Uploaded" xml:space="preserve"> + <source>Uploaded</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploaded files" xml:space="preserve"> + <source>Uploaded files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploading archive" xml:space="preserve"> + <source>Uploading archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use %@" xml:space="preserve"> + <source>Use %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> <target>ใช้โฮสต์ .onion</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use SOCKS proxy" xml:space="preserve"> + <source>Use SOCKS proxy</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> <target>ใช้เซิร์ฟเวอร์ SimpleX Chat ไหม?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use TCP port %@ when no port is specified." xml:space="preserve"> + <source>Use TCP port %@ when no port is specified.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve"> + <source>Use TCP port 443 for preset servers only.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> <target>ใช้แชท</target> @@ -5247,6 +7509,14 @@ To connect, please ask your contact to create another connection link and check <source>Use current profile</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use for files" xml:space="preserve"> + <source>Use for files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for messages" xml:space="preserve"> + <source>Use for messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>ใช้สำหรับการเชื่อมต่อใหม่</target> @@ -5265,19 +7535,49 @@ To connect, please ask your contact to create another connection link and check <source>Use new incognito profile</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use only local notifications?" xml:space="preserve"> + <source>Use only local notifications?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve"> + <source>Use private routing with unknown servers when IP address is not protected.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers." xml:space="preserve"> + <source>Use private routing with unknown servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>ใช้เซิร์ฟเวอร์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> - <target>โปรไฟล์ผู้ใช้</target> + <trans-unit id="Use servers" xml:space="preserve"> + <source>Use servers</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> - <target>การใช้โฮสต์ .onion ต้องการผู้ให้บริการ VPN ที่เข้ากันได้</target> + <trans-unit id="Use short links (BETA)" xml:space="preserve"> + <source>Use short links (BETA)</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app while in the call." xml:space="preserve"> + <source>Use the app while in the call.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app with one hand." xml:space="preserve"> + <source>Use the app with one hand.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use web port" xml:space="preserve"> + <source>Use web port</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="User selection" xml:space="preserve"> + <source>User selection</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Username" xml:space="preserve"> + <source>Username</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> @@ -5302,6 +7602,14 @@ To connect, please ask your contact to create another connection link and check <source>Verify connections</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Verify database passphrase" xml:space="preserve"> + <source>Verify database passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify passphrase" xml:space="preserve"> + <source>Verify passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Verify security code" xml:space="preserve"> <source>Verify security code</source> <target>ตรวจสอบรหัสความปลอดภัย</target> @@ -5336,11 +7644,23 @@ To connect, please ask your contact to create another connection link and check <target>วิดีโอและไฟล์สูงสุด 1gb</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View conditions" xml:space="preserve"> + <source>View conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> <target>ดูรหัสความปลอดภัย</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View updated conditions" xml:space="preserve"> + <source>View updated conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Visible history" xml:space="preserve"> + <source>Visible history</source> + <note>chat feature</note> + </trans-unit> <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> <target>ข้อความเสียง</target> @@ -5351,11 +7671,15 @@ To connect, please ask your contact to create another connection link and check <target>ห้ามส่งข้อความเสียงในแชทนี้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <target>ข้อความเสียงเป็นสิ่งต้องห้ามในกลุ่มนี้</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Voice messages not allowed" xml:space="preserve"> + <source>Voice messages not allowed</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> <source>Voice messages prohibited!</source> <target>ห้ามข้อความเสียง!</target> @@ -5385,6 +7709,18 @@ To connect, please ask your contact to create another connection link and check <target>กําลังรอวิดีโอ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wallpaper accent" xml:space="preserve"> + <source>Wallpaper accent</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper background" xml:space="preserve"> + <source>Wallpaper background</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve"> + <source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> <source>Warning: you may lose some data!</source> <target>คำเตือน: คุณอาจสูญเสียข้อมูลบางส่วน!</target> @@ -5405,6 +7741,10 @@ To connect, please ask your contact to create another connection link and check <target>ข้อความต้อนรับ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Welcome message is too long" xml:space="preserve"> + <source>Welcome message is too long</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="What's new" xml:space="preserve"> <source>What's new</source> <target>มีอะไรใหม่</target> @@ -5415,9 +7755,12 @@ To connect, please ask your contact to create another connection link and check <target>เมื่อพร้อมใช้งาน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> - <source>When people request to connect, you can accept or reject it.</source> - <target>เมื่อมีคนขอเชื่อมต่อ คุณสามารถยอมรับหรือปฏิเสธได้</target> + <trans-unit id="When connecting audio and video calls." xml:space="preserve"> + <source>When connecting audio and video calls.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When more than one operator is enabled, none of them has metadata to learn who communicates with whom." xml:space="preserve"> + <source>When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> @@ -5425,29 +7768,63 @@ To connect, please ask your contact to create another connection link and check <target>เมื่อคุณแชร์โปรไฟล์ที่ไม่ระบุตัวตนกับใครสักคน โปรไฟล์นี้จะใช้สำหรับกลุ่มที่พวกเขาเชิญคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="WiFi" xml:space="preserve"> + <source>WiFi</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Will be enabled in direct chats!" xml:space="preserve"> + <source>Will be enabled in direct chats!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wired ethernet" xml:space="preserve"> + <source>Wired ethernet</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With encrypted files and media." xml:space="preserve"> + <source>With encrypted files and media.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> <target>พร้อมข้อความต้อนรับที่ไม่บังคับ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="With reduced battery usage." xml:space="preserve"> + <source>With reduced battery usage.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to file servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> <target>รหัสผ่านฐานข้อมูลไม่ถูกต้อง</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve"> + <source>Wrong key or unknown connection - most likely this connection is deleted.</source> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Wrong key or unknown file chunk address - most likely file is deleted." xml:space="preserve"> + <source>Wrong key or unknown file chunk address - most likely file is deleted.</source> + <note>file error text</note> + </trans-unit> <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> <target>รหัสผ่านผิด!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="XFTP servers" xml:space="preserve"> - <source>XFTP servers</source> - <target>เซิร์ฟเวอร์ XFTP</target> + <trans-unit id="XFTP server" xml:space="preserve"> + <source>XFTP server</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> - <target>คุณ</target> + <trans-unit id="You **must not** use the same database on two devices." xml:space="preserve"> + <source>You **must not** use the same database on two devices.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You accepted connection" xml:space="preserve"> @@ -5470,6 +7847,10 @@ To connect, please ask your contact to create another connection link and check <target>คุณได้เชื่อมต่อกับ %@ แล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are already connected with %@." xml:space="preserve"> + <source>You are already connected with %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You are already connecting to %@." xml:space="preserve"> <source>You are already connecting to %@.</source> <note>No comment provided by engineer.</note> @@ -5509,14 +7890,21 @@ Repeat join request?</source> <target>คุณได้รับเชิญให้เข้าร่วมกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are not connected to these servers. Private routing is used to deliver messages to them." xml:space="preserve"> + <source>You are not connected to these servers. Private routing is used to deliver messages to them.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> <source>You can accept calls from lock screen, without device and app authentication.</source> <target>คุณสามารถรับสายจากหน้าจอล็อกโดยไม่ต้องมีการตรวจสอบสิทธิ์อุปกรณ์และแอป</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <target>คุณสามารถเชื่อมต่อได้โดยคลิกที่ลิงค์ หากเปิดในเบราว์เซอร์ ให้คลิกปุ่ม **เปิดในแอปมือถือ**</target> + <trans-unit id="You can change it in Appearance settings." xml:space="preserve"> + <source>You can change it in Appearance settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can configure servers via settings." xml:space="preserve"> + <source>You can configure servers via settings.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can create it later" xml:space="preserve"> @@ -5534,16 +7922,32 @@ Repeat join request?</source> <target>คุณสามารถเปิดใช้งานได้ในภายหลังผ่านการตั้งค่าความเป็นส่วนตัวและความปลอดภัยของแอป</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can give another try." xml:space="preserve"> + <source>You can give another try.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <source>You can hide or mute a user profile - swipe it to the right.</source> <target>คุณสามารถซ่อนหรือปิดเสียงโปรไฟล์ผู้ใช้ - ปัดไปทางขวา</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> + <trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve"> + <source>You can make it visible to your SimpleX contacts via Settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now chat with %@" xml:space="preserve"> + <source>You can now chat with %@</source> <target>ตอนนี้คุณสามารถส่งข้อความถึง %@</target> <note>notification body</note> </trans-unit> + <trans-unit id="You can send messages to %@ from Archived contacts." xml:space="preserve"> + <source>You can send messages to %@ from Archived contacts.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can set connection name, to remember who the link was shared with." xml:space="preserve"> + <source>You can set connection name, to remember who the link was shared with.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> <target>คุณสามารถตั้งค่าแสดงตัวอย่างการแจ้งเตือนบนหน้าจอล็อคผ่านการตั้งค่า</target> @@ -5559,16 +7963,15 @@ Repeat join request?</source> <target>คุณสามารถแบ่งปันที่อยู่นี้กับผู้ติดต่อของคุณเพื่อให้พวกเขาเชื่อมต่อกับ **%@**</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> - <source>You can share your address as a link or QR code - anybody can connect to you.</source> - <target>คุณสามารถแชร์ที่อยู่ของคุณเป็นลิงก์หรือรหัสคิวอาร์ - ใคร ๆ ก็สามารถเชื่อมต่อกับคุณได้</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> <target>คุณสามารถเริ่มแชทผ่านการตั้งค่าแอป / ฐานข้อมูล หรือโดยการรีสตาร์ทแอป</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve"> + <source>You can still view conversation with %@ in the list of chats.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> <target>คุณสามารถเปิด SimpleX Lock ผ่านการตั้งค่า</target> @@ -5579,21 +7982,25 @@ Repeat join request?</source> <target>คุณสามารถใช้มาร์กดาวน์เพื่อจัดรูปแบบข้อความ:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can view invitation link again in connection details." xml:space="preserve"> + <source>You can view invitation link again in connection details.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> <target>คุณไม่สามารถส่งข้อความได้!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <target>คุณควบคุมผ่านเซิร์ฟเวอร์ **เพื่อรับ** ข้อความผู้ติดต่อของคุณ - เซิร์ฟเวอร์ที่คุณใช้เพื่อส่งข้อความถึงพวกเขา</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <target>เราไม่สามารถตรวจสอบคุณได้ กรุณาลองอีกครั้ง.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> + <target>ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You have already requested connection via this address!" xml:space="preserve"> <source>You have already requested connection via this address!</source> <note>No comment provided by engineer.</note> @@ -5603,11 +8010,6 @@ Repeat join request?</source> Repeat connection request?</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> - <target>คุณไม่มีการแชท</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> <target>คุณต้องใส่รหัสผ่านทุกครั้งที่เริ่มแอป - รหัสผ่านไม่ได้จัดเก็บไว้ในอุปกรณ์</target> @@ -5627,11 +8029,23 @@ Repeat connection request?</source> <target>คุณเข้าร่วมกลุ่มนี้แล้ว กำลังเชื่อมต่อเพื่อเชิญสมาชิกกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You may migrate the exported database." xml:space="preserve"> + <source>You may migrate the exported database.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may save the exported archive." xml:space="preserve"> + <source>You may save the exported archive.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> <target>คุณต้องใช้ฐานข้อมูลแชทเวอร์ชันล่าสุดบนอุปกรณ์เครื่องเดียวเท่านั้น มิฉะนั้น คุณอาจหยุดได้รับข้อความจากผู้ติดต่อบางคน</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You need to allow your contact to call to be able to call them." xml:space="preserve"> + <source>You need to allow your contact to call to be able to call them.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> <target>คุณต้องอนุญาตให้ผู้ติดต่อของคุณส่งข้อความเสียงจึงจะสามารถส่งได้</target> @@ -5647,6 +8061,10 @@ Repeat connection request?</source> <target>คุณส่งคำเชิญเข้าร่วมกลุ่มแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You should receive notifications." xml:space="preserve"> + <source>You should receive notifications.</source> + <note>token info</note> + </trans-unit> <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> <target>คุณจะเชื่อมต่อกับกลุ่มเมื่ออุปกรณ์โฮสต์ของกลุ่มออนไลน์อยู่ โปรดรอหรือตรวจสอบภายหลัง!</target> @@ -5680,6 +8098,10 @@ Repeat connection request?</source> <target>คุณจะยังได้รับสายเรียกเข้าและการแจ้งเตือนจากโปรไฟล์ที่ปิดเสียงเมื่อโปรไฟล์ของเขามีการใช้งาน</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You will stop receiving messages from this chat. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this chat. Chat history will be preserved.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> <target>คุณจะหยุดได้รับข้อความจากกลุ่มนี้ ประวัติการแชทจะถูกรักษาไว้</target> @@ -5700,31 +8122,16 @@ Repeat connection request?</source> <target>คุณกำลังใช้โปรไฟล์ที่ไม่ระบุตัวตนสำหรับกลุ่มนี้ - ไม่อนุญาตให้เชิญผู้ติดต่อเพื่อป้องกันการแชร์โปรไฟล์หลักของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your %@ servers" xml:space="preserve"> - <source>Your %@ servers</source> - <target>เซิร์ฟเวอร์ %@ ของคุณ</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your ICE servers" xml:space="preserve"> <source>Your ICE servers</source> <target>เซิร์ฟเวอร์ ICE ของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> - <target>เซิร์ฟเวอร์ SMP ของคุณ</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your SimpleX address" xml:space="preserve"> <source>Your SimpleX address</source> <target>ที่อยู่ SimpleX ของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your XFTP servers" xml:space="preserve"> - <source>Your XFTP servers</source> - <target>เซิร์ฟเวอร์ XFTP ของคุณ</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your calls" xml:space="preserve"> <source>Your calls</source> <target>การโทรของคุณ</target> @@ -5740,16 +8147,17 @@ Repeat connection request?</source> <target>ฐานข้อมูลการแชทของคุณไม่ได้ถูก encrypt - ตั้งรหัสผ่านเพื่อ encrypt</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your chat preferences" xml:space="preserve"> + <source>Your chat preferences</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>โปรไฟล์แชทของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> - <target>ผู้ติดต่อของคุณจะต้องออนไลน์เพื่อให้การเชื่อมต่อเสร็จสมบูรณ์ -คุณสามารถยกเลิกการเชื่อมต่อนี้และลบผู้ติดต่อออก (และลองใหม่ในภายหลังด้วยลิงก์ใหม่)</target> + <trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve"> + <source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> @@ -5762,18 +8170,15 @@ You can cancel this connection and remove the contact (and try later with a new <target>ผู้ติดต่อของคุณสามารถอนุญาตให้ลบข้อความทั้งหมดได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> - <source>Your contacts in SimpleX will see it. -You can change it in Settings.</source> - <target>ผู้ติดต่อของคุณใน SimpleX จะเห็น -คุณสามารถเปลี่ยนได้ในการตั้งค่า</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your contacts will remain connected." xml:space="preserve"> <source>Your contacts will remain connected.</source> <target>ผู้ติดต่อของคุณจะยังคงเชื่อมต่ออยู่</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve"> + <source>Your credentials may be sent unencrypted.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> <target>ฐานข้อมูลแชทปัจจุบันของคุณจะถูกลบและแทนที่ด้วยฐานข้อมูลที่นำเข้า</target> @@ -5802,33 +8207,34 @@ You can change it in Settings.</source> <source>Your profile **%@** will be shared.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> - <target>โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น -เซิร์ฟเวอร์ SimpleX ไม่สามารถดูโปรไฟล์ของคุณได้</target> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> + <target>โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> - <target>โปรไฟล์ รายชื่อผู้ติดต่อ และข้อความที่ส่งของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณ</target> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> + <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> + <target>โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น เซิร์ฟเวอร์ SimpleX ไม่สามารถดูโปรไฟล์ของคุณได้</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve"> + <source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> <target>โปรไฟล์แบบสุ่มของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> - <target>เซิร์ฟเวอร์ของคุณ</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> <target>ที่อยู่เซิร์ฟเวอร์ของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your servers" xml:space="preserve"> + <source>Your servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your settings" xml:space="preserve"> <source>Your settings</source> <target>การตั้งค่าของคุณ</target> @@ -5869,11 +8275,19 @@ SimpleX servers cannot see your profile.</source> <target>รับสายแล้ว</target> <note>call status</note> </trans-unit> + <trans-unit id="accepted invitation" xml:space="preserve"> + <source>accepted invitation</source> + <note>chat list item title</note> + </trans-unit> <trans-unit id="admin" xml:space="preserve"> <source>admin</source> <target>ผู้ดูแลระบบ</target> <note>member role</note> </trans-unit> + <trans-unit id="admins" xml:space="preserve"> + <source>admins</source> + <note>feature role</note> + </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> <target>ยอมรับ encryption สำหรับ %@…</target> @@ -5884,6 +8298,10 @@ SimpleX servers cannot see your profile.</source> <target>เห็นด้วยกับการ encryption…</target> <note>chat item text</note> </trans-unit> + <trans-unit id="all members" xml:space="preserve"> + <source>all members</source> + <note>feature role</note> + </trans-unit> <trans-unit id="always" xml:space="preserve"> <source>always</source> <target>เสมอ</target> @@ -5893,6 +8311,14 @@ SimpleX servers cannot see your profile.</source> <source>and %lld other events</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="archived report" xml:space="preserve"> + <source>archived report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="attempts" xml:space="preserve"> + <source>attempts</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <source>audio call (not e2e encrypted)</source> <target>การโทรด้วยเสียง (ไม่ได้ encrypt จากต้นจนจบ)</target> @@ -5914,13 +8340,26 @@ SimpleX servers cannot see your profile.</source> </trans-unit> <trans-unit id="blocked" xml:space="preserve"> <source>blocked</source> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="blocked %@" xml:space="preserve"> + <source>blocked %@</source> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="blocked by admin" xml:space="preserve"> + <source>blocked by admin</source> + <note>blocked chat item +marked deleted chat item preview text</note> </trans-unit> <trans-unit id="bold" xml:space="preserve"> <source>bold</source> <target>ตัวหนา</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="call" xml:space="preserve"> + <source>call</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="call error" xml:space="preserve"> <source>call error</source> <target>การโทรผิดพลาด</target> @@ -6023,7 +8462,7 @@ SimpleX servers cannot see your profile.</source> <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> <target>กำลังเชื่อมต่อ…</target> - <note>chat list item title</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> @@ -6035,6 +8474,10 @@ SimpleX servers cannot see your profile.</source> <target>การเชื่อมต่อ:%@</target> <note>connection information</note> </trans-unit> + <trans-unit id="contact %@ changed to %@" xml:space="preserve"> + <source>contact %1$@ changed to %2$@</source> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> <target>ผู้ติดต่อมีการ encrypt จากต้นจนจบ</target> @@ -6065,10 +8508,15 @@ SimpleX servers cannot see your profile.</source> <target>วัน</target> <note>time unit</note> </trans-unit> + <trans-unit id="decryption errors" xml:space="preserve"> + <source>decryption errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> <target>ค่าเริ่มต้น (%@)</target> - <note>pref value</note> + <note>delete after time +pref value</note> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> @@ -6113,6 +8561,10 @@ SimpleX servers cannot see your profile.</source> <target>ข้อความที่ซ้ำกัน</target> <note>integrity error chat item</note> </trans-unit> + <trans-unit id="duplicates" xml:space="preserve"> + <source>duplicates</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> <target>encrypted จากต้นจนจบ</target> @@ -6188,8 +8640,12 @@ SimpleX servers cannot see your profile.</source> <target>ผิดพลาด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="event happened" xml:space="preserve"> - <source>event happened</source> + <trans-unit id="expired" xml:space="preserve"> + <source>expired</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="forwarded" xml:space="preserve"> + <source>forwarded</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> @@ -6217,6 +8673,10 @@ SimpleX servers cannot see your profile.</source> <target>iOS Keychain จะใช้เพื่อจัดเก็บรหัสผ่านอย่างปลอดภัยหลังจากที่คุณรีสตาร์ทแอปหรือเปลี่ยนรหัสผ่าน ซึ่งจะช่วยให้รับการแจ้งเตือนแบบทันทีได้</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="inactive" xml:space="preserve"> + <source>inactive</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> <target>ไม่ระบุตัวตนผ่านลิงค์ที่อยู่ติดต่อ</target> @@ -6257,6 +8717,10 @@ SimpleX servers cannot see your profile.</source> <target>คำเชิญเข้าร่วมกลุ่ม %@</target> <note>group name</note> </trans-unit> + <trans-unit id="invite" xml:space="preserve"> + <source>invite</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> <target>เชิญ</target> @@ -6302,11 +8766,19 @@ SimpleX servers cannot see your profile.</source> <target>สมาชิก</target> <note>member role</note> </trans-unit> + <trans-unit id="member %@ changed to %@" xml:space="preserve"> + <source>member %1$@ changed to %2$@</source> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> <target>เชื่อมต่อสำเร็จ</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="message" xml:space="preserve"> + <source>message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="message received" xml:space="preserve"> <source>message received</source> <target>ข้อความที่ได้รับ</target> @@ -6330,7 +8802,11 @@ SimpleX servers cannot see your profile.</source> <trans-unit id="moderated by %@" xml:space="preserve"> <source>moderated by %@</source> <target>กลั่นกรองโดย %@</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="moderator" xml:space="preserve"> + <source>moderator</source> + <note>member role</note> </trans-unit> <trans-unit id="months" xml:space="preserve"> <source>months</source> @@ -6340,7 +8816,7 @@ SimpleX servers cannot see your profile.</source> <trans-unit id="never" xml:space="preserve"> <source>never</source> <target>ไม่เคย</target> - <note>No comment provided by engineer.</note> + <note>delete after time</note> </trans-unit> <trans-unit id="new message" xml:space="preserve"> <source>new message</source> @@ -6371,8 +8847,8 @@ SimpleX servers cannot see your profile.</source> <source>off</source> <target>ปิด</target> <note>enabled status - group pref value - time to disappear</note> +group pref value +time to disappear</note> </trans-unit> <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> @@ -6389,16 +8865,40 @@ SimpleX servers cannot see your profile.</source> <target>เปิด</target> <note>group pref value</note> </trans-unit> + <trans-unit id="other" xml:space="preserve"> + <source>other</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="other errors" xml:space="preserve"> + <source>other errors</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="owner" xml:space="preserve"> <source>owner</source> <target>เจ้าของ</target> <note>member role</note> </trans-unit> + <trans-unit id="owners" xml:space="preserve"> + <source>owners</source> + <note>feature role</note> + </trans-unit> <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> <target>เพื่อนต่อเพื่อน</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="pending" xml:space="preserve"> + <source>pending</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="pending approval" xml:space="preserve"> + <source>pending approval</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="quantum resistant e2e encryption" xml:space="preserve"> + <source>quantum resistant e2e encryption</source> + <note>chat item text</note> + </trans-unit> <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> <target>ได้รับคำตอบ…</target> @@ -6409,6 +8909,10 @@ SimpleX servers cannot see your profile.</source> <target>ได้รับการยืนยัน…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="rejected" xml:space="preserve"> + <source>rejected</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> <target>สายถูกปฏิเสธ</target> @@ -6424,11 +8928,35 @@ SimpleX servers cannot see your profile.</source> <target>ถูกลบแล้ว %@</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="removed contact address" xml:space="preserve"> + <source>removed contact address</source> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="removed profile picture" xml:space="preserve"> + <source>removed profile picture</source> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> <target>ลบคุณออกแล้ว</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="requested to connect" xml:space="preserve"> + <source>requested to connect</source> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="saved" xml:space="preserve"> + <source>saved</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="saved from %@" xml:space="preserve"> + <source>saved from %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="search" xml:space="preserve"> + <source>search</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="sec" xml:space="preserve"> <source>sec</source> <target>วินาที</target> @@ -6453,6 +8981,24 @@ SimpleX servers cannot see your profile.</source> <source>send direct message</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve"> + <source>server queue info: %1$@ + +last received msg: %2$@</source> + <note>queue info</note> + </trans-unit> + <trans-unit id="set new contact address" xml:space="preserve"> + <source>set new contact address</source> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="set new profile picture" xml:space="preserve"> + <source>set new profile picture</source> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="standard end-to-end encryption" xml:space="preserve"> + <source>standard end-to-end encryption</source> + <note>chat item text</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>กำลังเริ่มต้น…</target> @@ -6468,16 +9014,36 @@ SimpleX servers cannot see your profile.</source> <target>ผู้ติดต่อนี้</target> <note>notification title</note> </trans-unit> + <trans-unit id="unblocked %@" xml:space="preserve"> + <source>unblocked %@</source> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="unknown" xml:space="preserve"> <source>unknown</source> <target>ไม่ทราบ</target> <note>connection info</note> </trans-unit> + <trans-unit id="unknown servers" xml:space="preserve"> + <source>unknown servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unknown status" xml:space="preserve"> + <source>unknown status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unprotected" xml:space="preserve"> + <source>unprotected</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="updated group profile" xml:space="preserve"> <source>updated group profile</source> <target>อัปเดตโปรไฟล์กลุ่มแล้ว</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="updated profile" xml:space="preserve"> + <source>updated profile</source> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="v%@" xml:space="preserve"> <source>v%@</source> <note>No comment provided by engineer.</note> @@ -6507,6 +9073,10 @@ SimpleX servers cannot see your profile.</source> <target>ผ่านรีเลย์</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="video" xml:space="preserve"> + <source>video</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> <source>video call (not e2e encrypted)</source> <target>การสนทนาทางวิดีโอ (ไม่ได้ encrypt จากต้นจนจบ)</target> @@ -6532,11 +9102,19 @@ SimpleX servers cannot see your profile.</source> <target>สัปดาห์</target> <note>time unit</note> </trans-unit> + <trans-unit id="when IP hidden" xml:space="preserve"> + <source>when IP hidden</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="yes" xml:space="preserve"> <source>yes</source> <target>ใช่</target> <note>pref value</note> </trans-unit> + <trans-unit id="you" xml:space="preserve"> + <source>you</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> <target>คุณได้รับเชิญให้เข้าร่วมกลุ่ม</target> @@ -6547,6 +9125,10 @@ SimpleX servers cannot see your profile.</source> <target>คุณเป็นผู้สังเกตการณ์</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="you blocked %@" xml:space="preserve"> + <source>you blocked %@</source> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> <target>คุณเปลี่ยนที่อยู่แล้ว</target> @@ -6587,6 +9169,10 @@ SimpleX servers cannot see your profile.</source> <target>คุณแชร์ลิงก์แบบใช้ครั้งเดียวโดยไม่ระบุตัวตนแล้ว</target> <note>chat list item description</note> </trans-unit> + <trans-unit id="you unblocked %@" xml:space="preserve"> + <source>you unblocked %@</source> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you: " xml:space="preserve"> <source>you: </source> <target>คุณ: </target> @@ -6601,7 +9187,7 @@ SimpleX servers cannot see your profile.</source> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="th" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6637,7 +9223,7 @@ SimpleX servers cannot see your profile.</source> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="th" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> @@ -6657,4 +9243,205 @@ SimpleX servers cannot see your profile.</source> </trans-unit> </body> </file> + <file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="th" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%d new events" xml:space="preserve"> + <source>%d new events</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From %d chat(s)" xml:space="preserve"> + <source>From %d chat(s)</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From: %@" xml:space="preserve"> + <source>From: %@</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <note>notification</note> + </trans-unit> + <trans-unit id="New messages" xml:space="preserve"> + <source>New messages</source> + <note>notification</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="th" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> + <source>SimpleX SE</source> + <note>Bundle display name</note> + </trans-unit> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX SE</source> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> + <source>Copyright © 2024 SimpleX Chat. All rights reserved.</source> + <note>Copyright (human-readable)</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="th" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App is locked!" xml:space="preserve"> + <source>App is locked!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Comment" xml:space="preserve"> + <source>Comment</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade required" xml:space="preserve"> + <source>Database downgrade required</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade required" xml:space="preserve"> + <source>Database upgrade required</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing file" xml:space="preserve"> + <source>Error preparing file</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing message" xml:space="preserve"> + <source>Error preparing message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No active profile" xml:space="preserve"> + <source>No active profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to downgrade the database." xml:space="preserve"> + <source>Open the app to downgrade the database.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to upgrade the database." xml:space="preserve"> + <source>Open the app to upgrade the database.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passphrase" xml:space="preserve"> + <source>Passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve"> + <source>Please create a profile in the SimpleX app</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending a message takes longer than expected." xml:space="preserve"> + <source>Sending a message takes longer than expected.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending message…" xml:space="preserve"> + <source>Sending message…</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Slow network?" xml:space="preserve"> + <source>Slow network?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unsupported format" xml:space="preserve"> + <source>Unsupported format</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wait" xml:space="preserve"> + <source>Wait</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve"> + <source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + </body> + </file> </xliff> diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/th.xcloc/contents.json b/apps/ios/SimpleX Localizations/th.xcloc/contents.json index b60f9edb3e..ee6ee63ea9 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/th.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "th", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "16.2" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..e44aedf64a --- /dev/null +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,15 @@ +{ + "colors" : [ + { + "idiom" : "universal", + "locale" : "tr" + } + ], + "properties" : { + "localizable" : true + }, + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} 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 4b2ad1548a..bbee40c2b9 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -2,2166 +2,4380 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="tr" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> - <trans-unit id=" " xml:space="preserve"> - <source> -</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (" xml:space="preserve"> - <source> (</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (can be copied)" xml:space="preserve" approved="no"> + <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> - <target state="translated"> (kopyalanabilir)</target> + <target> (kopyalanabilir)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="!1 colored!" xml:space="preserve" approved="no"> + <trans-unit id="!1 colored!" xml:space="preserve"> <source>!1 colored!</source> - <target state="translated">!1 renkli!</target> + <target>!1 renklendirilmiş!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="#secret#" xml:space="preserve" approved="no"> - <source>#secret#</source> - <target state="translated">#gizli#</target> - <note>No comment provided by engineer.</note> + <trans-unit id="# %@" xml:space="preserve"> + <source># %@</source> + <target># %@</target> + <note>copied message info title, # <title></note> </trans-unit> - <trans-unit id="%@" xml:space="preserve" approved="no"> - <source>%@</source> - <target state="translated">%@</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ %@" xml:space="preserve" approved="no"> - <source>%@ %@</source> - <target state="translated">%@ %@</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ (current)" xml:space="preserve" approved="no"> - <source>%@ (current)</source> - <target state="translated">%@ (güncel)</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ (current):" xml:space="preserve" approved="no"> - <source>%@ (current):</source> - <target state="translated">%@ (güncel):</target> + <trans-unit id="## History" xml:space="preserve"> + <source>## History</source> + <target>## Geçmiş</target> <note>copied message info</note> </trans-unit> - <trans-unit id="%@ / %@" xml:space="preserve" approved="no"> + <trans-unit id="## In reply to" xml:space="preserve"> + <source>## In reply to</source> + <target>## cevap olarak</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="#secret#" xml:space="preserve"> + <source>#secret#</source> + <target>#gizli#</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <target>%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ %@" xml:space="preserve"> + <source>%@ %@</source> + <target>%@ %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ (current)" xml:space="preserve"> + <source>%@ (current)</source> + <target>%@ (güncel)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ (current):" xml:space="preserve"> + <source>%@ (current):</source> + <target>%@ (güncel):</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="%@ / %@" xml:space="preserve"> <source>%@ / %@</source> - <target state="translated">%@ / %@</target> + <target>%@ / %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ and %@" xml:space="preserve"> + <source>%@ and %@</source> + <target>%@ ve %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve"> + <source>%@ and %@ connected</source> + <target>%@ ve %@ bağlandı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ at %@:" xml:space="preserve"> <source>%1$@ at %2$@:</source> + <target>1$@, %2$@'de:</target> <note>copied message info, <sender> at <time></note> </trans-unit> - <trans-unit id="%@ is connected!" xml:space="preserve" approved="no"> + <trans-unit id="%@ connected" xml:space="preserve"> + <source>%@ connected</source> + <target>%@ bağlandı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve"> + <source>%@ downloaded</source> + <target>%@ indirildi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ is connected!" xml:space="preserve"> <source>%@ is connected!</source> - <target state="translated">%@ bağlandı!</target> + <target>%@ bağlandı!</target> <note>notification title</note> </trans-unit> - <trans-unit id="%@ is not verified" xml:space="preserve" approved="no"> + <trans-unit id="%@ is not verified" xml:space="preserve"> <source>%@ is not verified</source> - <target state="translated">%@ onaylanmadı</target> + <target>%@ onaylanmadı</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ is verified" xml:space="preserve" approved="no"> + <trans-unit id="%@ is verified" xml:space="preserve"> <source>%@ is verified</source> - <target state="translated">%@ onaylandı</target> + <target>%@ onaylandı</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ servers" xml:space="preserve" approved="no"> + <trans-unit id="%@ server" xml:space="preserve"> + <source>%@ server</source> + <target>%@ sunucu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ servers" xml:space="preserve"> <source>%@ servers</source> - <target state="translated">%@ sunucuları</target> + <target>%@ sunucular</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ wants to connect!" xml:space="preserve" approved="no"> + <trans-unit id="%@ uploaded" xml:space="preserve"> + <source>%@ uploaded</source> + <target>%@ yüklendi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ wants to connect!" xml:space="preserve"> <source>%@ wants to connect!</source> - <target state="translated">%@ bağlanmak istiyor!</target> + <target>%@ bağlanmak istiyor!</target> <note>notification title</note> </trans-unit> - <trans-unit id="%@:" xml:space="preserve" approved="no"> + <trans-unit id="%@, %@" xml:space="preserve"> + <source>%1$@, %2$@</source> + <target>%1$@,%2$@</target> + <note>format for date separator in chat</note> + </trans-unit> + <trans-unit id="%@, %@ and %lld members" xml:space="preserve"> + <source>%@, %@ and %lld members</source> + <target>%@, %@ ve %lld üyeleri</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> + <source>%@, %@ and %lld other members connected</source> + <target>%@, %@ ve %lld diğer üyeler bağlandı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@:" xml:space="preserve"> <source>%@:</source> - <target state="translated">%@:</target> + <target>%@:</target> <note>copied message info</note> </trans-unit> - <trans-unit id="%d days" xml:space="preserve" approved="no"> + <trans-unit id="%d days" xml:space="preserve"> <source>%d days</source> - <target state="translated">%d gün</target> + <target>%d gün</target> <note>time interval</note> </trans-unit> - <trans-unit id="%d hours" xml:space="preserve" approved="no"> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve"> + <source>%d file(s) are still being downloaded.</source> + <target>%d dosyası(ları) hala indiriliyor.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve"> + <source>%d file(s) failed to download.</source> + <target>%d dosyası(ları) indirilemedi.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve"> + <source>%d file(s) were deleted.</source> + <target>%d dosyası(ları) silindi.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve"> + <source>%d file(s) were not downloaded.</source> + <target>%d dosyası(ları) indirilmedi.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> - <target state="translated">%d saat</target> + <target>%d saat</target> <note>time interval</note> </trans-unit> - <trans-unit id="%d min" xml:space="preserve" approved="no"> + <trans-unit id="%d messages not forwarded" xml:space="preserve"> + <source>%d messages not forwarded</source> + <target>%d mesajı iletilmeyedi</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> - <target state="translated">%d dakika</target> + <target>%d dakika</target> <note>time interval</note> </trans-unit> - <trans-unit id="%d months" xml:space="preserve" approved="no"> + <trans-unit id="%d months" xml:space="preserve"> <source>%d months</source> - <target state="translated">%d ay</target> + <target>%d ay</target> <note>time interval</note> </trans-unit> - <trans-unit id="%d sec" xml:space="preserve" approved="no"> + <trans-unit id="%d sec" xml:space="preserve"> <source>%d sec</source> - <target state="translated">%d saniye</target> + <target>%d saniye</target> <note>time interval</note> </trans-unit> - <trans-unit id="%d skipped message(s)" xml:space="preserve" approved="no"> + <trans-unit id="%d seconds(s)" xml:space="preserve"> + <source>%d seconds(s)</source> + <note>delete after time</note> + </trans-unit> + <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> - <target state="translated">%d okunmamış mesaj(lar)</target> + <target>%d okunmamış mesaj(lar)</target> <note>integrity error chat item</note> </trans-unit> - <trans-unit id="%d weeks" xml:space="preserve" approved="no"> + <trans-unit id="%d weeks" xml:space="preserve"> <source>%d weeks</source> - <target state="translated">%d hafta</target> + <target>%d hafta</target> <note>time interval</note> </trans-unit> - <trans-unit id="%lld" xml:space="preserve" approved="no"> + <trans-unit id="%lld" xml:space="preserve"> <source>%lld</source> - <target state="translated">%lld</target> + <target>%lld</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld %@" xml:space="preserve" approved="no"> + <trans-unit id="%lld %@" xml:space="preserve"> <source>%lld %@</source> - <target state="translated">%lld %@</target> + <target>%lld %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld contact(s) selected" xml:space="preserve" approved="no"> + <trans-unit id="%lld contact(s) selected" xml:space="preserve"> <source>%lld contact(s) selected</source> - <target state="translated">%lld kişi seçildi</target> + <target>%lld kişi seçildi</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve" approved="no"> + <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve"> <source>%lld file(s) with total size of %@</source> - <target state="translated">%lld dosya , toplam büyüklüğü %@</target> + <target>%lld dosya , toplam büyüklüğü %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld members" xml:space="preserve" approved="no"> + <trans-unit id="%lld group events" xml:space="preserve"> + <source>%lld group events</source> + <target>%lld grup etkinlikleri</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld members" xml:space="preserve"> <source>%lld members</source> - <target state="translated">%lld üyeler</target> + <target>%lld üyeler</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld minutes" xml:space="preserve" approved="no"> + <trans-unit id="%lld messages blocked" xml:space="preserve"> + <source>%lld messages blocked</source> + <target>%lld mesaj engellendi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve"> + <source>%lld messages blocked by admin</source> + <target>%lld mesaj yönetici tarafından engellendi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld messages marked deleted" xml:space="preserve"> + <source>%lld messages marked deleted</source> + <target>%lld mesaj silinmiş olarak işaretlendi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld messages moderated by %@" xml:space="preserve"> + <source>%lld messages moderated by %@</source> + <target>%lld mesaj %@ tarafından yönetildi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld minutes" xml:space="preserve"> <source>%lld minutes</source> - <target state="translated">%lld dakika</target> + <target>%lld dakika</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve" approved="no"> - <source>%lld second(s)</source> - <target state="translated">%lld saniye</target> + <trans-unit id="%lld new interface languages" xml:space="preserve"> + <source>%lld new interface languages</source> + <target>%lld yeni arayüz dilleri</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld seconds" xml:space="preserve" approved="no"> + <trans-unit id="%lld seconds" xml:space="preserve"> <source>%lld seconds</source> - <target state="translated">%lld saniye</target> + <target>%lld saniye</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldd" xml:space="preserve" approved="no"> + <trans-unit id="%lldd" xml:space="preserve"> <source>%lldd</source> - <target state="translated">%lldd</target> + <target>%lldd</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldh" xml:space="preserve" approved="no"> + <trans-unit id="%lldh" xml:space="preserve"> <source>%lldh</source> - <target state="translated">%lldh</target> + <target>%lldh</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldk" xml:space="preserve" approved="no"> + <trans-unit id="%lldk" xml:space="preserve"> <source>%lldk</source> - <target state="translated">%lldk</target> + <target>%lldk</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldm" xml:space="preserve" approved="no"> + <trans-unit id="%lldm" xml:space="preserve"> <source>%lldm</source> - <target state="translated">%lldm</target> + <target>%lldm</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldmth" xml:space="preserve" approved="no"> + <trans-unit id="%lldmth" xml:space="preserve"> <source>%lldmth</source> - <target state="translated">%lldmth</target> + <target>%lldmth</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%llds" xml:space="preserve" approved="no"> + <trans-unit id="%llds" xml:space="preserve"> <source>%llds</source> - <target state="translated">%llds</target> + <target>%llds</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldw" xml:space="preserve" approved="no"> + <trans-unit id="%lldw" xml:space="preserve"> <source>%lldw</source> - <target state="translated">%lldw</target> + <target>%lldw</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%u messages failed to decrypt." xml:space="preserve" approved="no"> + <trans-unit id="%u messages failed to decrypt." xml:space="preserve"> <source>%u messages failed to decrypt.</source> - <target state="translated">%u mesaj deşifrelenememektedir.</target> + <target>%u mesajın şifreleme çözümü başarısız oldu.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%u messages skipped." xml:space="preserve" approved="no"> + <trans-unit id="%u messages skipped." xml:space="preserve"> <source>%u messages skipped.</source> - <target state="translated">%u mesaj atlandı</target> + <target>%u mesajlar atlandı.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve" approved="no"> - <source>(</source> - <target state="translated">(</target> + <trans-unit id="(new)" xml:space="preserve"> + <source>(new)</source> + <target>(yeni)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve" approved="no"> - <source>)</source> - <target state="translated">)</target> + <trans-unit id="(this device v%@)" xml:space="preserve"> + <source>(this device v%@)</source> + <target>(bu cihaz v%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target state="translated">**Yeni kişi ekleyin**: tek seferlik QR Kodunuzu oluşturmak veya kişisel ulaşım bilgileri bağlantısı için.</target> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target>**Kişi ekle**: yeni bir davet bağlantısı oluşturmak için, ya da aldığın bağlantıyla bağlan.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no"> - <source>**Create link / QR code** for your contact to use.</source> - <target state="translated">Kişisel kullanım için **Bağlantı / QR Kodu**.</target> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve"> + <source>**Create group**: to create a new group.</source> + <target>**Grup oluştur**: yeni bir grup oluşturmak için.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> + <target>**Daha gizli**: her 20 dakikada yeni mesajlar için kontrol et. Cihaz jetonu SimpleX Chat sunucusuyla paylaşılacak, ama ne kadar kişi veya mesaja sahip olduğun paylaşılmayacak.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> + <target>**En gizli**: SimpleX Chat bildirim sunucusunu kullanma, arkaplanda mesajları periyodik olarak kontrol edin (uygulamayı ne sıklıkta kullandığınıza bağlıdır).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> + <target>**Lütfen dikkat**: Aynı veritabanını iki cihazda kullanmak, güvenlik koruması olarak bağlantılarınızdaki mesajların şifresinin çözülmesini engelleyecektir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> <source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source> + <target>**Lütfen aklınızda bulunsun**: eğer parolanızı kaybederseniz parolanızı değiştirme veya geri kurtarma ihtimaliniz YOKTUR.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> + <target>**Önerilen**: cihaz tokeni ve bildirimler SimpleX Chat bildirim sunucularına gönderilir, ama mesajın içeriği, boyutu veya kimden geldiği gönderilmez.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve"> + <source>**Scan / Paste link**: to connect via a link you received.</source> + <target>edindiğiniz bağlantı aracılığıyla bağlanmak için **Linki tarayın/yapıştırın**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> <source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source> + <target>**Dikkat**: Anında iletilen bildirimlere Anahtar Zinciri'nde kaydedilmiş parola gereklidir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve"> + <source>**Warning**: the archive will be removed.</source> + <target>**Uyarı**: arşiv silinecektir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> <source>**e2e encrypted** audio call</source> + <target>**uçtan uca şifrelenmiş** sesli arama</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**e2e encrypted** video call" xml:space="preserve"> <source>**e2e encrypted** video call</source> + <target>**uçtan uca şifrelenmiş** görüntülü arama</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="*bold*" xml:space="preserve"> <source>\*bold*</source> + <target>\*kalın*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=", " xml:space="preserve"> - <source>, </source> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> + <target>- [dizin hizmeti] 'ne bağlan(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)! +- çoklu mesaj gönderimi (20 kişiye kadar). +- daha hızlı ve daha stabil.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> + <source>- more stable message delivery. +- a bit better groups. +- and more!</source> + <target>- daha stabil mesaj gönderimi. +- birazcık daha iyi gruplar. +- ve fazlası!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="- optionally notify deleted contacts. - profile names with spaces. - and more!" xml:space="preserve"> + <source>- optionally notify deleted contacts. +- profile names with spaces. +- and more!</source> + <target>- isteğe bağlı olarak silinen kişilere bildirme. +- boşluklu profil adları +- ve fazlası!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="- voice messages up to 5 minutes. - custom time to disappear. - editing history." xml:space="preserve"> <source>- voice messages up to 5 minutes. - custom time to disappear. - editing history.</source> + <target>- 5 dakikaya kadar süren sesli mesajlar. +- mesaj kaybolması için özel zaman. +- düzenleme geçmişi.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="." xml:space="preserve"> - <source>.</source> - <note>No comment provided by engineer.</note> + <trans-unit id="0 sec" xml:space="preserve"> + <source>0 sec</source> + <target>0 saniye</target> + <note>time to disappear</note> </trans-unit> <trans-unit id="0s" xml:space="preserve"> <source>0s</source> + <target>0sn</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="1 day" xml:space="preserve" approved="no"> + <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> - <target state="translated">1 gün</target> - <note>time interval</note> + <target>1 gün</target> + <note>delete after time +time interval</note> </trans-unit> - <trans-unit id="1 hour" xml:space="preserve" approved="no"> + <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> - <target state="translated">1 saat</target> + <target>1 saat</target> <note>time interval</note> </trans-unit> - <trans-unit id="1 minute" xml:space="preserve" approved="no"> + <trans-unit id="1 minute" xml:space="preserve"> <source>1 minute</source> - <target state="translated">1 dakika</target> + <target>1 dakika</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="1 month" xml:space="preserve" approved="no"> + <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> - <target state="translated">1 ay</target> - <note>time interval</note> + <target>1 ay</target> + <note>delete after time +time interval</note> </trans-unit> - <trans-unit id="1 week" xml:space="preserve" approved="no"> + <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> - <target state="translated">1 hafta</target> - <note>time interval</note> + <target>1 hafta</target> + <note>delete after time +time interval</note> </trans-unit> - <trans-unit id="1-time link" xml:space="preserve" approved="no"> + <trans-unit id="1 year" xml:space="preserve"> + <source>1 year</source> + <note>delete after time</note> + </trans-unit> + <trans-unit id="1-time link" xml:space="preserve"> <source>1-time link</source> - <target state="translated">tek kullanımlık bağlantı</target> + <target>tek kullanımlık bağlantı</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="5 minutes" xml:space="preserve" approved="no"> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> + <target>Tek kullanımlık bağlantı *sadece bir kişi ile* kullanılabilir - kişiyle veya uygulama içinden paylaş.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="5 minutes" xml:space="preserve"> <source>5 minutes</source> - <target state="translated">5 dakika</target> + <target>5 dakika</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="6" xml:space="preserve" approved="no"> + <trans-unit id="6" xml:space="preserve"> <source>6</source> - <target state="translated">6</target> + <target>6</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="30 seconds" xml:space="preserve" approved="no"> + <trans-unit id="30 seconds" xml:space="preserve"> <source>30 seconds</source> - <target state="translated">30 saniye</target> + <target>30 saniye</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve"> - <source>: </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve" approved="no"> + <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> - <target state="translated"><p>Merhaba!</p> + <target><p>Merhaba!</p> <p><a href="%@">SimpleX Chat ile bana bağlanın</a></p></target> <note>email text</note> </trans-unit> - <trans-unit id="A new contact" xml:space="preserve" approved="no"> - <source>A new contact</source> - <target state="translated">Yeni kişi</target> - <note>notification title</note> - </trans-unit> - <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve" approved="no"> - <source>A random profile will be sent to the contact that you received this link from</source> - <target state="translated">Bu bağlantıyı aldığınız kişiye rastgele bir profil gönderilecek</target> + <trans-unit id="A few more things" xml:space="preserve"> + <source>A few more things</source> + <target>Birkaç şey daha</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="A random profile will be sent to your contact" xml:space="preserve" approved="no"> - <source>A random profile will be sent to your contact</source> - <target state="translated">Kişinize rastgele bir profil gönderilecek</target> + <trans-unit id="A new contact" xml:space="preserve"> + <source>A new contact</source> + <target>Yeni kişi</target> + <note>notification title</note> + </trans-unit> + <trans-unit id="A new random profile will be shared." xml:space="preserve"> + <source>A new random profile will be shared.</source> + <target>Yeni bir rastgele profil paylaşılacak.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> <source>A separate TCP connection will be used **for each chat profile you have in the app**.</source> + <target>**uygulamandaki olan her sohbet profili için** ayrı bir TCP bağlantısı kullanılacaktır.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." xml:space="preserve"> <source>A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</source> + <target>**uygulamandaki olan her sohbet profili için** ayrı bir TCP bağlantısı kullanılacaktır. +**Lütfen aklınızda bulunsun**: eğer çok fazla bağlantılarınız var ise pil kullanımı ve internet kullanımınız oldukça artabilir ve bazı bağlantılar hatayla karşılaşabilir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Abort" xml:space="preserve"> <source>Abort</source> + <target>İptal Et</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Abort changing address" xml:space="preserve"> <source>Abort changing address</source> + <target>Adres değişimini iptal et</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Abort changing address?" xml:space="preserve"> <source>Abort changing address?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve"> - <source>About SimpleX</source> + <target>Adres değişimi iptal edilsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve"> <source>About SimpleX Chat</source> + <target>SimpleX Chat hakkında</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve"> - <source>About SimpleX address</source> + <trans-unit id="About operators" xml:space="preserve"> + <source>About operators</source> + <target>Operatörler hakkında</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accent color" xml:space="preserve"> - <source>Accent color</source> + <trans-unit id="Accent" xml:space="preserve"> + <source>Accent</source> + <target>Ana renk</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept" xml:space="preserve"> <source>Accept</source> + <target>Kabul et</target> <note>accept contact request via notification - accept incoming call via notification</note> +accept incoming call via notification +swipe action</note> </trans-unit> - <trans-unit id="Accept contact" xml:space="preserve"> - <source>Accept contact</source> + <trans-unit id="Accept conditions" xml:space="preserve"> + <source>Accept conditions</source> + <target>Koşulları kabul et</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Accept connection request?" xml:space="preserve"> + <source>Accept connection request?</source> + <target>Bağlantı isteği kabul edilsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept contact request from %@?" xml:space="preserve"> <source>Accept contact request from %@?</source> + <target>%@ 'den gelen iletişim isteği kabul edilsin mi?</target> <note>notification body</note> </trans-unit> <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> + <target>Takma adla kabul et</target> + <note>accept contact request via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve"> + <source>Accepted conditions</source> + <target>Kabul edilmiş koşullar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve"> + <source>Acknowledged</source> + <target>Onaylandı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve"> + <source>Acknowledgement errors</source> + <target>Onay hataları</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Active" xml:space="preserve"> + <source>Active</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve"> + <source>Active connections</source> + <target>Aktif bağlantılar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> + <target>Kişilerinizin başkalarıyla paylaşabilmesi için profilinize adres ekleyin. Profil güncellemesi kişilerinize gönderilecek.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> - <source>Add preset servers</source> + <trans-unit id="Add friends" xml:space="preserve"> + <source>Add friends</source> + <target>Arkadaş ekle</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve"> + <source>Add list</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add profile" xml:space="preserve"> <source>Add profile</source> + <target>Profil ekle</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> + <target>Sunucu ekle</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> <source>Add servers by scanning QR codes.</source> + <target>Karekod taratarak sunucuları ekleyin.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> + <trans-unit id="Add team members" xml:space="preserve"> + <source>Add team members</source> + <target>Takım üyesi ekle</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> <source>Add to another device</source> + <target>Başka bir cihaza ekle</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add to list" xml:space="preserve"> + <source>Add to list</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> + <target>Karşılama mesajı ekleyin</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve"> + <source>Add your team members to the conversations.</source> + <target>Takım üyelerini konuşmalara ekle.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve"> + <source>Added media & file servers</source> + <target>medya ve dosya sunucuları eklendi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve"> + <source>Added message servers</source> + <target>Mesaj sunucuları eklendi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve"> + <source>Additional accent</source> + <target>Ek ana renk</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve"> + <source>Additional accent 2</source> + <target>Ek vurgu 2</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve"> + <source>Additional secondary</source> + <target>Ek ikincil renk</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Address" xml:space="preserve"> <source>Address</source> + <target>Adres</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve"> <source>Address change will be aborted. Old receiving address will be used.</source> + <target>Adres değişikliği iptal edilecek. Eski alıcı adresi kullanılacaktır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve"> + <source>Address or 1-time link?</source> + <target>adres mi yoksa tek kullanımlık bağlantı mı?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve"> + <source>Address settings</source> + <target>Adres seçenekleri</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve"> + <source>Admins can block a member for all.</source> + <target>Yöneticiler bir üyeyi tamamen engelleyebilirler.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> <source>Admins can create the links to join groups.</source> + <target>Yöneticiler gruplara katılmak için bağlantılar oluşturabilir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Advanced network settings" xml:space="preserve"> <source>Advanced network settings</source> + <target>Gelişmiş ağ ayarları</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve"> + <source>Advanced settings</source> + <target>Gelişmiş ayarlar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All" xml:space="preserve"> + <source>All</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All app data is deleted." xml:space="preserve"> <source>All app data is deleted.</source> + <target>Tüm uygulama verileri silinir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve"> <source>All chats and messages will be deleted - this cannot be undone!</source> + <target>Tüm konuşmalar ve mesajlar silinecektir. Bu, geri alınamaz!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> + <target>Kullanıldığında bütün veriler silinir.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All group members will remain connected." xml:space="preserve" approved="no"> + <trans-unit id="All data is kept private on your device." xml:space="preserve"> + <source>All data is kept private on your device.</source> + <target>Tüm veriler cihazınıza özeldir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> - <target state="translated">Tüm grup üyeleri bağlı kalacaktır.</target> + <target>Tüm grup üyeleri bağlı kalacaktır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <target>Bütün mesajlar ve dosyalar **uçtan-uca şifrelemeli** gönderilir, doğrudan mesajlarda kuantum güvenlik ile birlikte.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone!</source> + <target>Tüm mesajlar silinecektir - bu geri alınamaz!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> + <target>Tüm mesajlar silinecektir. Bu, geri alınamaz! Mesajlar, YALNIZCA senin için silinecektir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve"> + <source>All new messages from %@ will be hidden!</source> + <target>%@ 'den gelen bütün yeni mesajlar saklı olacak!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All profiles" xml:space="preserve"> + <source>All profiles</source> + <target>Tüm Profiller</target> + <note>profile dropdown</note> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve"> + <source>All reports will be archived for you.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All servers" xml:space="preserve"> + <source>All servers</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All your contacts will remain connected." xml:space="preserve"> <source>All your contacts will remain connected.</source> + <target>Konuştuğun kişilerin tümü bağlı kalacaktır.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve"> <source>All your contacts will remain connected. Profile update will be sent to your contacts.</source> + <target>Tüm kişileriniz bağlı kalacaktır. Profil güncellemesi kişilerinize gönderilecektir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target>Tüm kişileriniz, konuşmalarınız ve dosyalarınız güvenli bir şekilde şifrelenir ve yapılandırılmış XFTP yönlendiricilerine parçalar halinde yüklenir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow" xml:space="preserve"> <source>Allow</source> + <target>İzin ver</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow calls only if your contact allows them." xml:space="preserve"> <source>Allow calls only if your contact allows them.</source> + <target>Yalnızca irtibat kişiniz izin veriyorsa aramalara izin verin.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve"> + <source>Allow calls?</source> + <target>Aramalara izin verilsin mi ?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> + <target>Eğer kişide izin verirse kaybolan mesajlara izin ver.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> + <trans-unit id="Allow downgrade" xml:space="preserve"> + <source>Allow downgrade</source> + <target>Sürüm düşürmeye izin ver</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target>Konuştuğun kişi, kalıcı olarak silinebilen mesajlara izin veriyorsa sen de ver. (24 saat içinde)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> <source>Allow message reactions only if your contact allows them.</source> + <target>Yalnızca kişin mesaj tepkilerine izin veriyorsa sen de ver.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions." xml:space="preserve"> <source>Allow message reactions.</source> + <target>Mesaj tepkilerine izin ver.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow sending direct messages to members." xml:space="preserve" approved="no"> + <trans-unit id="Allow sending direct messages to members." xml:space="preserve"> <source>Allow sending direct messages to members.</source> - <target state="translated">Üyelere direkt mesaj göndermeye izin ver.</target> + <target>Üyelere doğrudan mesaj göndermeye izin ver.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow sending disappearing messages." xml:space="preserve"> <source>Allow sending disappearing messages.</source> + <target>Kendiliğinden yok olan mesajlar göndermeye izin ver.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow to irreversibly delete sent messages.</source> + <trans-unit id="Allow sharing" xml:space="preserve"> + <source>Allow sharing</source> + <target>Paylaşıma izin ver</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target>Gönderilen mesajların kalıcı olarak silinmesine izin ver. (24 saat içinde)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve"> + <source>Allow to report messsages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve"> + <source>Allow to send SimpleX links.</source> + <target>SimpleX bağlantıları göndilmesine izin ver.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send files and media." xml:space="preserve"> <source>Allow to send files and media.</source> + <target>Dosya ve medya göndermeye izin ver.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send voice messages." xml:space="preserve"> <source>Allow to send voice messages.</source> + <target>Sesli mesaj göndermeye izin ver.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve"> <source>Allow voice messages only if your contact allows them.</source> + <target>Yalnızca kişiniz sesli mesaj göndermeye izin veriyorsa sen de ver.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow voice messages?" xml:space="preserve"> <source>Allow voice messages?</source> + <target>Sesli mesajlara izin ver?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts adding message reactions." xml:space="preserve"> <source>Allow your contacts adding message reactions.</source> + <target>Konuştuğun kişilerin mesajlarına tepki eklemesine izin ver.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to call you." xml:space="preserve"> <source>Allow your contacts to call you.</source> + <target>Kişilerinin seni aramasına izin ver.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow your contacts to irreversibly delete sent messages.</source> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target>Kişilerinin gönderilen mesajları kalıcı olarak silmesine izin ver. (24 saat içinde)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> <source>Allow your contacts to send disappearing messages.</source> + <target>Kişilerinizin kaybolan mesajlar göndermesine izin verin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send voice messages." xml:space="preserve"> <source>Allow your contacts to send voice messages.</source> + <target>Kişilerinizin sesli mesajlar göndermesine izin verin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Already connected?" xml:space="preserve"> <source>Already connected?</source> + <target>Zaten bağlandı?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Already connecting!" xml:space="preserve"> + <source>Already connecting!</source> + <target>Zaten bağlanılıyor!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Already joining the group!" xml:space="preserve"> + <source>Already joining the group!</source> + <target>Zaten gruba bağlanılıyor!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve"> + <source>Always use private routing.</source> + <target>Her zaman gizli yönlendirme kullan.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> <source>Always use relay</source> + <target>Her zaman yönlendirici kullan</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="An empty chat profile with the provided name is created, and the app opens as usual." xml:space="preserve"> <source>An empty chat profile with the provided name is created, and the app opens as usual.</source> + <target>Verilen adla boş bir sohbet profili oluşturulur ve uygulama her zamanki gibi açılır.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Another reason" xml:space="preserve"> + <source>Another reason</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> + <target>Aramayı cevapla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> + <target>Açık kaynak protokolü ve kodu - herhangi biri sunucuları çalıştırabilir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> + <target>Uygulama sürümü: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App data migration" xml:space="preserve"> + <source>App data migration</source> + <target>Uygulama verisi taşıma</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> + <source>App encrypts new local files (except videos).</source> + <target>Uygulama yerel dosyaları şifreler (videolar dışında).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App group:" xml:space="preserve"> + <source>App group:</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> + <target>Uygulama simgesi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App passcode" xml:space="preserve"> <source>App passcode</source> + <target>Uygulama erişim kodu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App passcode is replaced with self-destruct passcode." xml:space="preserve"> <source>App passcode is replaced with self-destruct passcode.</source> + <target>Uygulama parolası kendi kendini imha eden parolayla değiştirildi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App session" xml:space="preserve"> + <source>App session</source> + <target>Uygulama oturumu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App version" xml:space="preserve"> <source>App version</source> + <target>Uygulama sürümü</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App version: v%@" xml:space="preserve"> <source>App version: v%@</source> + <target>Uygulama sürümü: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Appearance" xml:space="preserve"> <source>Appearance</source> + <target>Görünüş</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply" xml:space="preserve"> + <source>Apply</source> + <target>Uygula</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve"> + <source>Apply to</source> + <target>Şuna uygula</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve"> + <source>Archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve"> + <source>Archive %lld reports?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve"> + <source>Archive all reports?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve"> + <source>Archive and upload</source> + <target>Arşivle ve yükle</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve"> + <source>Archive contacts to chat later.</source> + <target>Daha sonra görüşmek için kişileri arşivleyin.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve"> + <source>Archive report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve"> + <source>Archive report?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve"> + <source>Archive reports</source> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve"> + <source>Archived contacts</source> + <target>Arşivli kişiler</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve"> + <source>Archiving database</source> + <target>Veritabanı arşivleniyor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Attach" xml:space="preserve"> <source>Attach</source> + <target>Ekle</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Audio & video calls" xml:space="preserve"> <source>Audio & video calls</source> + <target>Sesli & görüntülü aramalar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Audio and video calls" xml:space="preserve"> <source>Audio and video calls</source> + <target>Sesli ve görüntülü aramalar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Audio/video calls" xml:space="preserve"> <source>Audio/video calls</source> + <target>Sesli/görüntülü aramalar</target> <note>chat feature</note> </trans-unit> <trans-unit id="Audio/video calls are prohibited." xml:space="preserve"> <source>Audio/video calls are prohibited.</source> + <target>Sesli/görüntülü aramalar yasaklandı.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Authentication cancelled" xml:space="preserve"> <source>Authentication cancelled</source> + <target>Kimlik doğrulama iptal edildi</target> <note>PIN entry</note> </trans-unit> <trans-unit id="Authentication failed" xml:space="preserve"> <source>Authentication failed</source> + <target>Kimlik doğrulama başarısız oldu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Authentication is required before the call is connected, but you may miss calls." xml:space="preserve"> <source>Authentication is required before the call is connected, but you may miss calls.</source> + <target>Kimlik doğrulama aramalara bağlanmadan önce gereklidir, ama aramaları kaçırabilirsin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Authentication unavailable" xml:space="preserve"> <source>Authentication unavailable</source> + <target>Kimlik doğrulama kullanım dışı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Auto-accept" xml:space="preserve"> <source>Auto-accept</source> + <target>Otomatik-kabul et</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Auto-accept contact requests" xml:space="preserve"> <source>Auto-accept contact requests</source> + <target>İletişim isteklerini otomatik kabul et</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Auto-accept images" xml:space="preserve"> <source>Auto-accept images</source> + <target>Fotoğrafları otomatik kabul et</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve"> + <source>Auto-accept settings</source> + <target>Ayarları otomatik olarak kabul et</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Back" xml:space="preserve"> <source>Back</source> + <target>Geri</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Background" xml:space="preserve"> + <source>Background</source> + <target>Arka plan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Bad desktop address" xml:space="preserve"> + <source>Bad desktop address</source> + <target>Kötü bilgisayar adresi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Bad message ID" xml:space="preserve"> <source>Bad message ID</source> + <target>Kötü mesaj kimliği</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Bad message hash" xml:space="preserve"> <source>Bad message hash</source> + <target>Kötü mesaj karması</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better calls" xml:space="preserve"> + <source>Better calls</source> + <target>Daha iyi aramalar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better groups" xml:space="preserve"> + <source>Better groups</source> + <target>Daha iyi gruplar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve"> + <source>Better groups performance</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve"> + <source>Better message dates.</source> + <target>Daha iyi mesaj tarihleri.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Better messages" xml:space="preserve"> <source>Better messages</source> + <target>Daha iyi mesajlar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better networking" xml:space="preserve"> + <source>Better networking</source> + <target>Daha iyi ağ oluşturma</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve"> + <source>Better notifications</source> + <target>Daha iyi bildirimler</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve"> + <source>Better privacy and security</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve"> + <source>Better security ✅</source> + <target>Daha iyi güvenlik ✅</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve"> + <source>Better user experience</source> + <target>Daha iyi kullanıcı deneyimi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Black" xml:space="preserve"> + <source>Black</source> + <target>Siyah</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block" xml:space="preserve"> + <source>Block</source> + <target>Engelle</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block for all" xml:space="preserve"> + <source>Block for all</source> + <target>Herkes için engelle</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block group members" xml:space="preserve"> + <source>Block group members</source> + <target>Grup üyelerini engelle</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block member" xml:space="preserve"> + <source>Block member</source> + <target>Üyeyi engelle</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve"> + <source>Block member for all?</source> + <target>Üye herkes için engellensin mi?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block member?" xml:space="preserve"> + <source>Block member?</source> + <target>Üyeyi engelle?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve"> + <source>Blocked by admin</source> + <target>Yönetici tarafından engellendi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve"> + <source>Blur for better privacy.</source> + <target>Daha iyi gizlilik için bulanıklaştır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve"> + <source>Blur media</source> + <target>Medyayı bulanıklaştır</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <source>Both you and your contact can add message reactions.</source> + <target>Sen ve konuştuğun kişi mesaj tepkileri ekleyebilir.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> - <source>Both you and your contact can irreversibly delete sent messages.</source> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target>Konuştuğun kişi ve sen mesajları kalıcı olarak silebilirsiniz. (24 saat içinde)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> <source>Both you and your contact can make calls.</source> + <target>Sen ve konuştuğun kişi aramalar yapabilir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can send disappearing messages." xml:space="preserve"> <source>Both you and your contact can send disappearing messages.</source> + <target>Sen ve konuştuğun kişi kaybolan mesajlar gönderebilir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can send voice messages." xml:space="preserve"> <source>Both you and your contact can send voice messages.</source> + <target>Sen ve konuştuğun kişi sesli mesaj gönderebilir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target>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)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business address" xml:space="preserve"> + <source>Business address</source> + <target>İş adresi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve"> + <source>Business chats</source> + <target>İş konuşmaları</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve"> + <source>Businesses</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> + <target>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).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve"> + <source>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> <source>Call already ended!</source> + <target>Arama çoktan bitti!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Calls" xml:space="preserve"> <source>Calls</source> + <target>Aramalar</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve"> - <source>Can't delete user profile!</source> + <trans-unit id="Calls prohibited!" xml:space="preserve"> + <source>Calls prohibited!</source> + <target>Aramalara izin verilmiyor!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve"> + <source>Camera not available</source> + <target>Kamera mevcut değil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve"> + <source>Can't call contact</source> + <target>Kişi aranamıyor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve"> + <source>Can't call member</source> + <target>Üye aranamaz</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contact!" xml:space="preserve"> <source>Can't invite contact!</source> + <target>Kişi davet edilemiyor!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contacts!" xml:space="preserve"> <source>Can't invite contacts!</source> + <target>Kişiler davet edilemiyor!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve"> + <source>Can't message member</source> + <target>Üyeye mesaj gönderilemiyor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> + <target>İptal et</target> + <note>alert action +alert button</note> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve"> + <source>Cancel migration</source> + <target>Taşımayı iptal et</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> <source>Cannot access keychain to save database password</source> + <target>Veritabanı şifresini kaydetmek için Anahtar Zinciri'ne erişilemiyor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Mesaj iletilemiyor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> + <target>Dosya alınamıyor</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <target>Kapasite aşıldı - alıcı önceden gönderilen mesajları almadı.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve"> + <source>Cellular</source> + <target>Hücresel Veri</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change" xml:space="preserve"> <source>Change</source> + <target>Değiştir</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve"> + <source>Change automatic message deletion?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve"> + <source>Change chat profiles</source> + <target>Sohbet profillerini değiştir</target> + <note>authentication reason</note> + </trans-unit> <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> + <target>Veritabanı parolasını değiştir?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change lock mode" xml:space="preserve"> <source>Change lock mode</source> + <target>Kilit modunu değiştir</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Change member role?" xml:space="preserve"> <source>Change member role?</source> + <target>Üye rolünü değiştir?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change passcode" xml:space="preserve"> <source>Change passcode</source> + <target>Şifreyi değiştir</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Change receiving address" xml:space="preserve"> <source>Change receiving address</source> + <target>Alıcı adresini değiştir</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change receiving address?" xml:space="preserve"> <source>Change receiving address?</source> + <target>Alıcı adresi değiştirilsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change role" xml:space="preserve"> <source>Change role</source> + <target>Rolü değiştir</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change self-destruct mode" xml:space="preserve"> <source>Change self-destruct mode</source> + <target>Kendini yok etme modunu değiştir</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Change self-destruct passcode" xml:space="preserve"> <source>Change self-destruct passcode</source> + <target>Kendini yok eden parolayı değiştir</target> <note>authentication reason - set passcode view</note> +set passcode view</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> - <source>Chat archive</source> + <trans-unit id="Chat" xml:space="preserve"> + <source>Chat</source> + <target>Sohbet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve"> + <source>Chat already exists</source> + <target>Sohbet zaten mevcut</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve"> + <source>Chat already exists!</source> + <target>Sohbet zaten mevcut!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve"> + <source>Chat colors</source> + <target>Sohbet renkleri</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat console" xml:space="preserve"> <source>Chat console</source> + <target>Sohbet konsolu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat database" xml:space="preserve"> <source>Chat database</source> + <target>Sohbet veritabanı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat database deleted" xml:space="preserve"> <source>Chat database deleted</source> + <target>Sohbet veritabanı silindi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve"> + <source>Chat database exported</source> + <target>Veritabanı dışa aktarıldı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> + <target>Sohbet veritabanı içe aktarıldı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat is running" xml:space="preserve"> <source>Chat is running</source> + <target>Sohbet çalışıyor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat is stopped" xml:space="preserve"> <source>Chat is stopped</source> + <target>Sohbet durdu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <target>Sohbet durduruldu. Bu veritabanını zaten başka bir cihazda kullandıysanız, sohbete başlamadan önce onu geri aktarmalısınız.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve"> + <source>Chat list</source> + <target>Sohbet listesi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve"> + <source>Chat migrated!</source> + <target>Sohbet taşındı!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> + <target>Sohbet tercihleri</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve"> + <source>Chat preferences were changed.</source> + <target>Sohbet tercihleri değiştirildi.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve"> + <source>Chat profile</source> + <target>Kullanıcı profili</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve"> + <source>Chat theme</source> + <target>Sohbet teması</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <target>Sohbet bütün üyeler için silinecek - bu geri alınamaz!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <target>Sohbet senden silinecek - bu geri alınamaz!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> + <target>Sohbetler</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve"> + <source>Check messages every 20 min.</source> + <target>Her 20 dakikada mesajları kontrol et.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve"> + <source>Check messages when allowed.</source> + <target>İzin verildiğinde mesajları kontrol et.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> - <note>No comment provided by engineer.</note> + <target>Sunucu adresini kontrol edip tekrar deneyin.</target> + <note>alert title</note> </trans-unit> <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> + <target>Çince ve İspanyolca arayüz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <target>Yeni cihazda _Başka bir cihazdan taşı_ seçeneğini seçin ve QR kodunu tarayın.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> + <target>Dosya seç</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Choose from library" xml:space="preserve"> <source>Choose from library</source> + <target>Kütüphaneden seç</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve"> + <source>Chunks deleted</source> + <target>Parçalar silindi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve"> + <source>Chunks downloaded</source> + <target>Parçalar indirildi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve"> + <source>Chunks uploaded</source> + <target>Parçalar yüklendi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> - <note>No comment provided by engineer.</note> + <target>Temizle</target> + <note>swipe action</note> </trans-unit> <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> + <target>Sohbeti temizle</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Clear conversation?" xml:space="preserve"> <source>Clear conversation?</source> + <target>Sohbet temizlensin mi?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve"> + <source>Clear group?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve"> + <source>Clear or delete group?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve"> + <source>Clear private notes?</source> + <target>Gizli notlar temizlensin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Clear verification" xml:space="preserve"> <source>Clear verification</source> + <target>Doğrulamayı temizle</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> - <source>Colors</source> + <trans-unit id="Color chats with the new themes." xml:space="preserve"> + <source>Color chats with the new themes.</source> + <target>Yeni temalarla renkli sohbetler.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Color mode" xml:space="preserve"> + <source>Color mode</source> + <target>Renk modu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve"> + <source>Community guidelines violation</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Compare file" xml:space="preserve"> <source>Compare file</source> + <target>Dosya karşılaştır</target> <note>server test step</note> </trans-unit> <trans-unit id="Compare security codes with your contacts." xml:space="preserve"> <source>Compare security codes with your contacts.</source> + <target>Güvenlik kodlarını kişilerinle karşılaştır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Completed" xml:space="preserve"> + <source>Completed</source> + <target>Tamamlandı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve"> + <source>Conditions accepted on: %@.</source> + <target>Şuradaki koşullar kabul edildi: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <target>Koşullar operatör(ler) için kabul edildi: **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <target>Koşullar çoktan operatör(ler) tarafından kabul edildi: **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve"> + <source>Conditions of use</source> + <target>Kullanım koşulları</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <target>Koşullar bu operatör(ler) için kabul edilecektir: **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve"> + <source>Conditions will be accepted on: %@.</source> + <target>Koşullar şu tarihte kabul edilecektir: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <target>Koşullar etkin operatörler için şu tarihte otomatik olarak kabul edilecektir: %@.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> + <target>ICE sunucularını ayarla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Configure server operators" xml:space="preserve"> + <source>Configure server operators</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> + <target>Onayla</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Confirm Passcode" xml:space="preserve"> <source>Confirm Passcode</source> + <target>Parolayı onayla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve"> + <source>Confirm contact deletion?</source> + <target>Kişiyi silmek istediğinizden emin misiniz ?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Confirm database upgrades" xml:space="preserve"> <source>Confirm database upgrades</source> + <target>Veritabanı geliştirmelerini onayla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve"> + <source>Confirm files from unknown servers.</source> + <target>Bilinmeyen sunuculardan gelen dosyaları onayla.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve"> + <source>Confirm network settings</source> + <target>Ağ ayarlarını onaylayın</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> + <target>Yeni parolayı onayla…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Confirm password" xml:space="preserve"> <source>Confirm password</source> + <target>Şifreyi onayla</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <target>Taşımak için veritabanı parolasını hatırladığınızı doğrulayın.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve"> + <source>Confirm upload</source> + <target>Yüklemeyi onayla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve"> + <source>Confirmed</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> + <target>Bağlan</target> <note>server test step</note> </trans-unit> - <trans-unit id="Connect via contact link?" xml:space="preserve"> - <source>Connect via contact link?</source> + <trans-unit id="Connect automatically" xml:space="preserve"> + <source>Connect automatically</source> + <target>Otomatik olarak bağlan</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via group link?" xml:space="preserve"> - <source>Connect via group link?</source> + <trans-unit id="Connect incognito" xml:space="preserve"> + <source>Connect incognito</source> + <target>Gizli bağlan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect to desktop" xml:space="preserve"> + <source>Connect to desktop</source> + <target>Bilgisayara bağlan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve"> + <source>Connect to your friends faster.</source> + <target>Arkadaşlarınıza daha hızlı bağlanın.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect to yourself?" xml:space="preserve"> + <source>Connect to yourself?</source> + <target>Kendine mi bağlanacaksın?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect to yourself? This is your own SimpleX address!" xml:space="preserve"> + <source>Connect to yourself? +This is your own SimpleX address!</source> + <target>Kendine mi bağlanacaksın? +Bu senin kendi SimpleX adresin!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect to yourself? This is your own one-time link!" xml:space="preserve"> + <source>Connect to yourself? +This is your own one-time link!</source> + <target>Kendine mi bağlanacaksın? +Bu senin kendi tek kullanımlık bağlantın!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via contact address" xml:space="preserve"> + <source>Connect via contact address</source> + <target>Kişi adresi aracılığıyla bağlan</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via link" xml:space="preserve"> <source>Connect via link</source> + <target>Bağlantı aracılığıyla bağlan</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve"> - <source>Connect via link / QR code</source> + <trans-unit id="Connect via one-time link" xml:space="preserve"> + <source>Connect via one-time link</source> + <target>Tek kullanımlık bağlantı aracılığıyla bağlan</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via one-time link?" xml:space="preserve"> - <source>Connect via one-time link?</source> + <trans-unit id="Connect with %@" xml:space="preserve"> + <source>Connect with %@</source> + <target>%@ ile bağlan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connected" xml:space="preserve"> + <source>Connected</source> + <target>Bağlandı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connected desktop" xml:space="preserve"> + <source>Connected desktop</source> + <target>Bilgisayara bağlandı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve"> + <source>Connected servers</source> + <target>Bağlı sunucular</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connected to desktop" xml:space="preserve"> + <source>Connected to desktop</source> + <target>Masaüstüne bağlandı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connecting" xml:space="preserve"> + <source>Connecting</source> + <target>Bağlanıyor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> <source>Connecting to server…</source> + <target>Sunucuya bağlanıyor…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server… (error: %@)" xml:space="preserve"> <source>Connecting to server… (error: %@)</source> + <target>Sunucuya bağlanıyor…(hata:%@)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve"> + <source>Connecting to contact, please wait or check later!</source> + <target>Kişiye bağlanılıyor, lütfen bekleyin ya da daha sonra kontrol edin!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connecting to desktop" xml:space="preserve"> + <source>Connecting to desktop</source> + <target>Bilgisayara bağlanıyor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection" xml:space="preserve"> <source>Connection</source> + <target>Bağlantı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve"> + <source>Connection and servers status.</source> + <target>Bağlantı ve sunucuların durumu.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve"> + <source>Connection blocked</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> + <target>Bağlantı hatası</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection error (AUTH)" xml:space="preserve"> <source>Connection error (AUTH)</source> + <target>Bağlantı hatası (DOĞRULAMA)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection request" xml:space="preserve"> - <source>Connection request</source> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve"> + <source>Connection is blocked by server operator: +%@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve"> + <source>Connection not ready.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve"> + <source>Connection notifications</source> + <target>Bağlantı bildirimleri</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> + <target>Bağlantı daveti gönderildi!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve"> + <source>Connection requires encryption renegotiation.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve"> + <source>Connection security</source> + <target>Bağlantı güvenliği</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection terminated" xml:space="preserve"> + <source>Connection terminated</source> + <target>Bağlantı sonlandırılmış</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection timeout" xml:space="preserve"> <source>Connection timeout</source> + <target>Bağlantı süresi geçmiş</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve"> + <source>Connection with desktop stopped</source> + <target>Masaüstü ile bağlantı durduruldu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve"> + <source>Connections</source> + <target>Bağlantılar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact allows" xml:space="preserve"> <source>Contact allows</source> + <target>Kişi izin veriyor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact already exists" xml:space="preserve"> <source>Contact already exists</source> + <target>Kişi zaten mevcut</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contact and all messages will be deleted - this cannot be undone!" xml:space="preserve"> - <source>Contact and all messages will be deleted - this cannot be undone!</source> + <trans-unit id="Contact deleted!" xml:space="preserve"> + <source>Contact deleted!</source> + <target>Kişiler silindi!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> + <target>Kişi gizli:</target> <note>notification</note> </trans-unit> <trans-unit id="Contact is connected" xml:space="preserve"> <source>Contact is connected</source> + <target>Kişi bağlandı</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve"> - <source>Contact is not connected yet!</source> + <trans-unit id="Contact is deleted." xml:space="preserve"> + <source>Contact is deleted.</source> + <target>Kişi silindi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact name" xml:space="preserve"> <source>Contact name</source> + <target>Kişi adı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact preferences" xml:space="preserve"> <source>Contact preferences</source> + <target>Kişi tercihleri</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve"> + <source>Contact will be deleted - this cannot be undone!</source> + <target>Kişiler silinecek - bu geri alınamaz !</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> + <target>Kişiler</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve"> <source>Contacts can mark messages for deletion; you will be able to view them.</source> + <target>Kişiler silinmesi için mesajları işaretleyebilir; onları görüntüleyebilirsin.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve"> + <source>Content violates conditions of use</source> + <note>blocking reason</note> + </trans-unit> <trans-unit id="Continue" xml:space="preserve"> <source>Continue</source> + <target>Devam et</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve"> + <source>Conversation deleted!</source> + <target>Sohbet silindi!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> - <note>chat item action</note> + <target>Kopyala</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve"> + <source>Copy error</source> + <target>Kopyalama hatası</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> + <target>Çekirdek sürümü: v%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Corner" xml:space="preserve"> + <source>Corner</source> + <target>Köşeleri yuvarlama</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Correct name to %@?" xml:space="preserve"> + <source>Correct name to %@?</source> + <target>İsim %@ olarak düzeltilsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create" xml:space="preserve"> <source>Create</source> + <target>Oluştur</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve"> + <source>Create 1-time link</source> + <target>Tek kullanımlık bağlantı oluştur</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> + <target>SimpleX adresi oluştur</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> - <source>Create an address to let people connect with you.</source> + <trans-unit id="Create a group using a random profile." xml:space="preserve"> + <source>Create a group using a random profile.</source> + <target>Rasgele profil kullanarak grup oluştur.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create file" xml:space="preserve"> <source>Create file</source> + <target>Dosya oluştur</target> <note>server test step</note> </trans-unit> + <trans-unit id="Create group" xml:space="preserve"> + <source>Create group</source> + <target>Grup oluştur</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create group link" xml:space="preserve"> <source>Create group link</source> + <target>Grup bağlantısı oluştur</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create link" xml:space="preserve"> <source>Create link</source> + <target>Bağlantı oluştur</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve"> - <source>Create one-time invitation link</source> + <trans-unit id="Create list" xml:space="preserve"> + <source>Create list</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <target>[bilgisayar uygulaması] nda yeni bir profil oluştur(https://simplex.chat/downloads/). 💻</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create profile" xml:space="preserve"> + <source>Create profile</source> + <target>Profil oluştur</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create queue" xml:space="preserve"> <source>Create queue</source> + <target>Sıra oluştur</target> <note>server test step</note> </trans-unit> <trans-unit id="Create secret group" xml:space="preserve"> <source>Create secret group</source> + <target>Gizli grup oluştur</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create your profile" xml:space="preserve"> <source>Create your profile</source> + <target>Profilini oluştur</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve"> - <source>Created on %@</source> + <trans-unit id="Created" xml:space="preserve"> + <source>Created</source> + <target>Yaratıldı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve"> + <source>Created at</source> + <target>Şurada oluşturuldu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve"> + <source>Created at: %@</source> + <target>Şurada oluşturuldu: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve"> + <source>Creating archive link</source> + <target>Arşiv bağlantısı oluşturuluyor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve"> + <source>Creating link…</source> + <target>Link oluşturuluyor…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current Passcode" xml:space="preserve"> <source>Current Passcode</source> + <target>Şu anki şifre</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <target>Şu anki koşulların yazısı yüklenemiyor, bu bağlantıdan koşullara inceleyebilirsin:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> + <target>Şu anki parola…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Current profile" xml:space="preserve"> + <source>Current profile</source> + <target>Aktif profil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> + <target>Şu anki maksimum desteklenen dosya boyutu %@ kadardır.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Custom time" xml:space="preserve"> <source>Custom time</source> + <target>Özel saat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve"> + <source>Customizable message shape.</source> + <target>Özelleştirilebilir mesaj şekli.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve"> + <source>Customize theme</source> + <target>Renk temalarını kişiselleştir</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> + <target>Karanlık</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve"> + <source>Dark mode colors</source> + <target>Karanlık mod renkleri</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> + <target>Veritabanı kimliği</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database ID: %d" xml:space="preserve"> <source>Database ID: %d</source> + <target>Veritabanı kimliği: %d</target> <note>copied message info</note> </trans-unit> <trans-unit id="Database IDs and Transport isolation option." xml:space="preserve"> <source>Database IDs and Transport isolation option.</source> + <target>Veritabanı kimlikleri ve Taşıma izolasyonu seçeneği.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database downgrade" xml:space="preserve"> <source>Database downgrade</source> + <target>Veritabanı sürüm düşürme</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database encrypted!" xml:space="preserve"> <source>Database encrypted!</source> + <target>Veritabanı şifrelendi!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database encryption passphrase will be updated and stored in the keychain. " xml:space="preserve"> <source>Database encryption passphrase will be updated and stored in the keychain. </source> + <target>Veritabanı şifreleme parolası güncellenecek ve Anahtar Zinciri'nde saklanacaktır. +</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database encryption passphrase will be updated. " xml:space="preserve"> <source>Database encryption passphrase will be updated. </source> + <target>Veritabanı şifreleme parolası güncellenecektir. +</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database error" xml:space="preserve"> <source>Database error</source> + <target>Veritabanı hatası</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database is encrypted using a random passphrase, you can change it." xml:space="preserve"> <source>Database is encrypted using a random passphrase, you can change it.</source> + <target>Veritabanı rastgele bir parola kullanılarak şifrelenir, bunu değiştirebilirsiniz.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database is encrypted using a random passphrase. Please change it before exporting." xml:space="preserve"> <source>Database is encrypted using a random passphrase. Please change it before exporting.</source> + <target>Veritabanı rastgele bir parola kullanılarak şifrelenir. Lütfen dışa aktarmadan önce değiştirin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database passphrase" xml:space="preserve"> <source>Database passphrase</source> + <target>Veritabanı parolası</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database passphrase & export" xml:space="preserve"> <source>Database passphrase & export</source> + <target>Veritabanı parolası ve dışa aktarma</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> <source>Database passphrase is different from saved in the keychain.</source> + <target>Veritabanı parolası Anahtar Zinciri'nde kayıtlı olandan farklıdır.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> <source>Database passphrase is required to open chat.</source> + <target>Sohbeti açmak için veritabanı parolası gereklidir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database upgrade" xml:space="preserve"> <source>Database upgrade</source> + <target>Veritabanı yükseltmesi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database will be encrypted and the passphrase stored in the keychain. " xml:space="preserve"> <source>Database will be encrypted and the passphrase stored in the keychain. </source> + <target>Veritabanı şifrelenecek ve parola Anahtar Zinciri'nde saklanacaktır. +</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database will be encrypted. " xml:space="preserve"> <source>Database will be encrypted. </source> + <target>Veritabanı şifrelenecektir. +</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database will be migrated when the app restarts" xml:space="preserve"> <source>Database will be migrated when the app restarts</source> + <target>Uygulama yeniden başlatıldığında veritabanı taşınacaktır</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Debug delivery" xml:space="preserve"> + <source>Debug delivery</source> + <target>Hata ayıklama teslimatı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> + <target>Merkezi Olmayan</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Decryption error" xml:space="preserve"> <source>Decryption error</source> + <target>Şifre çözme hatası</target> <note>message decrypt error item</note> </trans-unit> <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> - <note>chat item action</note> + <target>Sil</target> + <note>alert action +swipe action</note> </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve"> - <source>Delete Contact</source> + <trans-unit id="Delete %lld messages of members?" xml:space="preserve"> + <source>Delete %lld messages of members?</source> + <target>Üyelerin %lld mesajları silinsin mi?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete %lld messages?" xml:space="preserve"> + <source>Delete %lld messages?</source> + <target>%lld mesaj silinsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete address" xml:space="preserve"> <source>Delete address</source> + <target>Adresi sil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete address?" xml:space="preserve"> <source>Delete address?</source> + <target>Adres silinsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete after" xml:space="preserve"> <source>Delete after</source> + <target>Sonra sil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete all files" xml:space="preserve"> <source>Delete all files</source> + <target>Bütün dosyaları sil</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve"> - <source>Delete archive</source> + <trans-unit id="Delete and notify contact" xml:space="preserve"> + <source>Delete and notify contact</source> + <target>Sil ve kişiye bildir</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve"> - <source>Delete chat archive?</source> + <trans-unit id="Delete chat" xml:space="preserve"> + <source>Delete chat</source> + <target>Sohbeti sil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete chat messages from your device." xml:space="preserve"> + <source>Delete chat messages from your device.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile" xml:space="preserve"> <source>Delete chat profile</source> + <target>Sohbet profilini sil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile?" xml:space="preserve"> <source>Delete chat profile?</source> + <target>Sohbet profili silinsin mi?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete chat?" xml:space="preserve"> + <source>Delete chat?</source> + <target>Sohbet silinsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> <source>Delete connection</source> + <target>Bağlantıyı sil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete contact" xml:space="preserve"> <source>Delete contact</source> + <target>Kişiyi sil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete contact?" xml:space="preserve"> <source>Delete contact?</source> + <target>Kişiyi sil?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete database" xml:space="preserve"> <source>Delete database</source> + <target>Veritabanını sil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete database from this device" xml:space="preserve"> + <source>Delete database from this device</source> + <target>Veritabanını bu cihazdan sil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete file" xml:space="preserve"> <source>Delete file</source> + <target>Dosyayı sil</target> <note>server test step</note> </trans-unit> <trans-unit id="Delete files and media?" xml:space="preserve"> <source>Delete files and media?</source> + <target>Dosyalar ve medya silinsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete files for all chat profiles" xml:space="preserve"> <source>Delete files for all chat profiles</source> + <target>Bütün sohbet profilleri için dosyaları sil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete for everyone" xml:space="preserve"> <source>Delete for everyone</source> + <target>Herkesten sil</target> <note>chat feature</note> </trans-unit> <trans-unit id="Delete for me" xml:space="preserve"> <source>Delete for me</source> + <target>Benden sil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete group" xml:space="preserve"> <source>Delete group</source> + <target>Grubu sil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete group?" xml:space="preserve"> <source>Delete group?</source> + <target>Grup silinsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete invitation" xml:space="preserve"> <source>Delete invitation</source> + <target>Daveti sil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete link" xml:space="preserve"> <source>Delete link</source> + <target>Bağlantıyı sil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete link?" xml:space="preserve"> <source>Delete link?</source> + <target>Bağlantı silinsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete list?" xml:space="preserve"> + <source>Delete list?</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> + <target>Kişinin mesajı silinsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete message?" xml:space="preserve"> <source>Delete message?</source> + <target>Mesaj silinsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> - <note>No comment provided by engineer.</note> + <target>Mesajları sil</target> + <note>alert button</note> </trans-unit> <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> + <target>Mesajları sonra sil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete old database" xml:space="preserve"> <source>Delete old database</source> + <target>Eski veritabanını sil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete old database?" xml:space="preserve"> <source>Delete old database?</source> + <target>Eski veritabanı silinsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve"> - <source>Delete pending connection</source> + <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> + <source>Delete or moderate up to 200 messages.</source> + <target>200'e kadar mesajı silin veya düzenleyin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection?" xml:space="preserve"> <source>Delete pending connection?</source> + <target>Bekleyen bağlantı silinsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete profile" xml:space="preserve"> <source>Delete profile</source> + <target>Profili sil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete queue" xml:space="preserve"> <source>Delete queue</source> + <target>Sırayı sil</target> <note>server test step</note> </trans-unit> + <trans-unit id="Delete report" xml:space="preserve"> + <source>Delete report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete up to 20 messages at once." xml:space="preserve"> + <source>Delete up to 20 messages at once.</source> + <target>Tek seferde en fazla 20 mesaj silin.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> + <target>Kullanıcı profili silinsin mi?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete without notification" xml:space="preserve"> + <source>Delete without notification</source> + <target>Bildirim göndermeden sil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve"> + <source>Deleted</source> + <target>Silindi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Deleted at" xml:space="preserve"> <source>Deleted at</source> + <target>de silindi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Deleted at: %@" xml:space="preserve"> <source>Deleted at: %@</source> + <target>%@ de silindi</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve"> + <source>Deletion errors</source> + <target>Silme hatası</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivered even when Apple drops them." xml:space="preserve"> + <source>Delivered even when Apple drops them.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivery" xml:space="preserve"> + <source>Delivery</source> + <target>Teslimat</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery receipts are disabled!" xml:space="preserve"> <source>Delivery receipts are disabled!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delivery receipts will be enabled for all contacts in all visible chat profiles." xml:space="preserve"> - <source>Delivery receipts will be enabled for all contacts in all visible chat profiles.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delivery receipts will be enabled for all contacts." xml:space="preserve"> - <source>Delivery receipts will be enabled for all contacts.</source> + <target>Mesaj gönderim bilgisi devre dışı!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delivery receipts!" xml:space="preserve"> <source>Delivery receipts!</source> + <target>Mesaj gönderildi bilgisi!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Description" xml:space="preserve"> <source>Description</source> + <target>Açıklama</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Desktop address" xml:space="preserve"> + <source>Desktop address</source> + <target>Bilgisayar adresi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve"> + <source>Desktop app version %@ is not compatible with this app.</source> + <target>Masaüstü uygulaması sürümü %@ bu uygulama ile uyumlu değildir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Desktop devices" xml:space="preserve"> + <source>Desktop devices</source> + <target>Bilgisayar cihazları</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve"> + <source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source> + <target>Hedef sunucu adresi %@, yönlendirme sunucusu %@ ayarlarıyla uyumlu değil.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server error: %@" xml:space="preserve"> + <source>Destination server error: %@</source> + <target>Hedef sunucu hatası: %@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve"> + <source>Destination server version of %@ is incompatible with forwarding server %@.</source> + <target>Hedef sunucu %@ sürümü, yönlendirme sunucusu %@ ile uyumlu değil.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Detailed statistics" xml:space="preserve"> + <source>Detailed statistics</source> + <target>Detaylı istatistikler</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Details" xml:space="preserve"> + <source>Details</source> + <target>Detaylar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Develop" xml:space="preserve"> <source>Develop</source> + <target>Geliştir</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Developer options" xml:space="preserve"> + <source>Developer options</source> + <target>Geliştirici seçenekleri</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> + <target>Geliştirici araçları</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Device" xml:space="preserve"> <source>Device</source> + <target>Cihaz</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Device authentication is disabled. Turning off SimpleX Lock." xml:space="preserve"> <source>Device authentication is disabled. Turning off SimpleX Lock.</source> + <target>Cihaz kimlik doğrulaması devre dışı. SimpleX Kilidi kapatılıyor.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." xml:space="preserve"> <source>Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication.</source> + <target>Cihaz kimlik doğrulaması etkin değil. Cihaz kimlik doğrulamasını etkinleştirdikten sonra SimpleX Kilidini Ayarlar üzerinden açabilirsiniz.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Different names, avatars and transport isolation." xml:space="preserve"> <source>Different names, avatars and transport isolation.</source> + <target>Farklı isimler, profil fotoğrafları ve taşıma izolasyonu.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Direct messages" xml:space="preserve"> <source>Direct messages</source> + <target>Doğrudan mesajlar</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve" approved="no"> - <source>Direct messages between members are prohibited in this group.</source> - <target state="translated">Bu grupta üyeler arasında direkt mesajlaşma yasaktır.</target> + <trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve"> + <source>Direct messages between members are prohibited in this chat.</source> + <target>Üyeler arası doğrudan mesajlar bu sohbette yasaktır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> + <target>Bu grupta üyeler arasında direkt mesajlaşma yasaktır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disable (keep overrides)" xml:space="preserve"> + <source>Disable (keep overrides)</source> + <target>Devre dışı bırak (geçersiz kılmaları koru)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> <source>Disable SimpleX Lock</source> + <target>SimpleX Kilidini devre dışı bırak</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Disable automatic message deletion?" xml:space="preserve"> + <source>Disable automatic message deletion?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Disable delete messages" xml:space="preserve"> + <source>Disable delete messages</source> + <note>alert button</note> + </trans-unit> + <trans-unit id="Disable for all" xml:space="preserve"> + <source>Disable for all</source> + <target>Herkes için devre dışı bırak</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disabled" xml:space="preserve"> + <source>Disabled</source> + <target>Devre dışı</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> <source>Disappearing message</source> + <target>Kaybolan mesaj</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disappearing messages" xml:space="preserve"> <source>Disappearing messages</source> + <target>Kaybolan mesajlar</target> <note>chat feature</note> </trans-unit> <trans-unit id="Disappearing messages are prohibited in this chat." xml:space="preserve"> <source>Disappearing messages are prohibited in this chat.</source> + <target>Kaybolan mesajlar bu sohbette yasaklanmış.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> + <target>Kaybolan mesajlar bu grupta yasaklanmış.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disappears at" xml:space="preserve"> <source>Disappears at</source> + <target>da kaybolur</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disappears at: %@" xml:space="preserve"> <source>Disappears at: %@</source> + <target>%@ da kaybolur</target> <note>copied message info</note> </trans-unit> <trans-unit id="Disconnect" xml:space="preserve"> <source>Disconnect</source> + <target>Bağlantıyı kes</target> <note>server test step</note> </trans-unit> - <trans-unit id="Display name" xml:space="preserve"> - <source>Display name</source> + <trans-unit id="Disconnect desktop?" xml:space="preserve"> + <source>Disconnect desktop?</source> + <target>Bilgisayarla bağlantı kesilsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Display name:" xml:space="preserve"> - <source>Display name:</source> + <trans-unit id="Discover and join groups" xml:space="preserve"> + <source>Discover and join groups</source> + <target>Keşfet ve gruplara katıl</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Discover via local network" xml:space="preserve"> + <source>Discover via local network</source> + <target>Yerel ağ aracılığıyla keşfet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve"> + <source>Do NOT send messages directly, even if your or destination server does not support private routing.</source> + <target>Sizin veya hedef sunucunun özel yönlendirmeyi desteklememesi durumunda bile mesajları doğrudan GÖNDERMEYİN.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> <source>Do NOT use SimpleX for emergency calls.</source> + <target>Acil aramalar için SimpleX'i KULLANMAYIN.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do NOT use private routing." xml:space="preserve"> + <source>Do NOT use private routing.</source> + <target>Gizli yönlendirmeyi KULLANMA.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Do it later" xml:space="preserve"> <source>Do it later</source> + <target>Sonra yap</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not send history to new members." xml:space="preserve"> + <source>Do not send history to new members.</source> + <target>Yeni üyelere geçmişi gönderme.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not use credentials with proxy." xml:space="preserve"> + <source>Do not use credentials with proxy.</source> + <target>Kimlik bilgilerini proxy ile kullanmayın.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Documents:" xml:space="preserve"> + <source>Documents:</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> + <target>Adres oluşturma</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Don't enable" xml:space="preserve"> + <source>Don't enable</source> + <target>Etkinleştirme</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Don't miss important messages." xml:space="preserve"> + <source>Don't miss important messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> + <target>Yeniden gösterme</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Done" xml:space="preserve"> + <source>Done</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Downgrade and open chat" xml:space="preserve"> <source>Downgrade and open chat</source> + <target>Sürüm düşür ve sohbeti aç</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download" xml:space="preserve"> + <source>Download</source> + <target>İndir</target> + <note>alert button +chat item action</note> + </trans-unit> + <trans-unit id="Download errors" xml:space="preserve"> + <source>Download errors</source> + <target>İndirme hataları</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download failed" xml:space="preserve"> + <source>Download failed</source> + <target>Yükleme başarısız oldu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Download file" xml:space="preserve"> <source>Download file</source> + <target>Dosya indir</target> <note>server test step</note> </trans-unit> + <trans-unit id="Download files" xml:space="preserve"> + <source>Download files</source> + <target>Dosyaları indirin</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Downloaded" xml:space="preserve"> + <source>Downloaded</source> + <target>İndirildi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloaded files" xml:space="preserve"> + <source>Downloaded files</source> + <target>Dosyalar İndirildi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading archive" xml:space="preserve"> + <source>Downloading archive</source> + <target>Arşiv indiriliyor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading link details" xml:space="preserve"> + <source>Downloading link details</source> + <target>Bağlantı detayları indiriliyor</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> + <target>Yinelenen görünen ad!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Duration" xml:space="preserve"> <source>Duration</source> + <target>Süre</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="E2E encrypted notifications." xml:space="preserve"> + <source>E2E encrypted notifications.</source> + <target>Uçtan uca şifrelenmiş bildirimler.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> + <target>Düzenle</target> <note>chat item action</note> </trans-unit> <trans-unit id="Edit group profile" xml:space="preserve"> <source>Edit group profile</source> + <target>Grup profilini düzenle</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable" xml:space="preserve"> <source>Enable</source> + <target>Etkinleştir</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable (keep overrides)" xml:space="preserve"> + <source>Enable (keep overrides)</source> + <target>Etkinleştir (geçersiz kılmaları koru)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve"> + <source>Enable Flux in Network & servers settings for better metadata privacy.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> + <target>SimpleX Kilidini etkinleştir</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Enable TCP keep-alive" xml:space="preserve"> <source>Enable TCP keep-alive</source> + <target>TCP canlı tutmayı etkinleştir</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> + <target>Otomatik mesaj silme etkinleştirilsin mi?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Enable camera access" xml:space="preserve"> + <source>Enable camera access</source> + <target>Kamera erişimini etkinleştir</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable for all" xml:space="preserve"> + <source>Enable for all</source> + <target>Herkes için etkinleştir</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve"> + <source>Enable in direct chats (BETA)!</source> + <target>Doğrudan sohbetlerde etkinleştirin (BETA)!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable later via Settings" xml:space="preserve"> - <source>Enable later via Settings</source> + <target>Anlık bildirimler etkinleştirilsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable lock" xml:space="preserve"> <source>Enable lock</source> + <target>Kilidi etkinleştir</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable notifications" xml:space="preserve"> <source>Enable notifications</source> + <target>Bildirimleri etkinleştir</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable periodic notifications?" xml:space="preserve"> <source>Enable periodic notifications?</source> + <target>Periyodik bildirimler etkinleştirilsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable self-destruct" xml:space="preserve"> <source>Enable self-destruct</source> + <target>Kendini imhayı etkinleştir</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable self-destruct passcode" xml:space="preserve"> <source>Enable self-destruct passcode</source> + <target>Kendini imha şifresini etkinleştir</target> <note>set passcode view</note> </trans-unit> + <trans-unit id="Enabled" xml:space="preserve"> + <source>Enabled</source> + <target>Etkin</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve"> + <source>Enabled for</source> + <target>Şunlar için etkinleştirildi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> + <target>Şifreleme</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Encrypt database?" xml:space="preserve"> <source>Encrypt database?</source> + <target>Veritabanı şifrelensin mi?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt local files" xml:space="preserve"> + <source>Encrypt local files</source> + <target>Yerel dosyaları şifrele</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt stored files & media" xml:space="preserve"> + <source>Encrypt stored files & media</source> + <target>Saklanan dosyaları ve medyayı şifreleyin</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Encrypted database" xml:space="preserve"> <source>Encrypted database</source> + <target>Şifrelenmiş veritabanı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Encrypted message or another event" xml:space="preserve"> <source>Encrypted message or another event</source> + <target>Şifrelenmiş mesaj veya başka bir etkinlik</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Encrypted message: app is stopped" xml:space="preserve"> + <source>Encrypted message: app is stopped</source> + <target>Şifrelenmiş mesaj: uygulama durdu</target> <note>notification</note> </trans-unit> <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> + <target>Şifrelenmiş mesaj: veritabanı hatası</target> <note>notification</note> </trans-unit> <trans-unit id="Encrypted message: database migration error" xml:space="preserve"> <source>Encrypted message: database migration error</source> + <target>Şifrelenmiş mesaj: veritabanı taşıma hatası</target> <note>notification</note> </trans-unit> <trans-unit id="Encrypted message: keychain error" xml:space="preserve"> <source>Encrypted message: keychain error</source> + <target>Şifrelenmiş mesaj: Anahtar Zinciri hatası</target> <note>notification</note> </trans-unit> <trans-unit id="Encrypted message: no passphrase" xml:space="preserve"> <source>Encrypted message: no passphrase</source> + <target>Şifrelenmiş mesaj: parola yok</target> <note>notification</note> </trans-unit> <trans-unit id="Encrypted message: unexpected error" xml:space="preserve"> <source>Encrypted message: unexpected error</source> + <target>Şifrelenmiş mesaj: beklenmeyen hata</target> <note>notification</note> </trans-unit> + <trans-unit id="Encryption re-negotiation error" xml:space="preserve"> + <source>Encryption re-negotiation error</source> + <target>Şifreleme yeniden anlaşma hatası</target> + <note>message decrypt error item</note> + </trans-unit> + <trans-unit id="Encryption re-negotiation failed." xml:space="preserve"> + <source>Encryption re-negotiation failed.</source> + <target>Şifreleme yeniden anlaşma başarısız oldu.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encryption renegotiation in progress." xml:space="preserve"> + <source>Encryption renegotiation in progress.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter Passcode" xml:space="preserve"> <source>Enter Passcode</source> + <target>Şifre gir</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter correct passphrase." xml:space="preserve"> <source>Enter correct passphrase.</source> + <target>Doğru şifreyi gir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter group name…" xml:space="preserve"> + <source>Enter group name…</source> + <target>Grup adı gir…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve"> + <source>Enter passphrase</source> + <target>Parolayı girin</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter passphrase…" xml:space="preserve"> <source>Enter passphrase…</source> + <target>Parola gir…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter password above to show!" xml:space="preserve"> <source>Enter password above to show!</source> + <target>Göstermek için yukarıdaki şifreyi gir!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter server manually" xml:space="preserve"> <source>Enter server manually</source> + <target>Sunucuya manuel olarak gir</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter this device name…" xml:space="preserve"> + <source>Enter this device name…</source> + <target>Bu cihazın adını gir…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter welcome message…" xml:space="preserve"> <source>Enter welcome message…</source> + <target>Hoşgeldin mesajı gir…</target> <note>placeholder</note> </trans-unit> <trans-unit id="Enter welcome message… (optional)" xml:space="preserve"> <source>Enter welcome message… (optional)</source> + <target>Hoşgeldin mesajı gir... (opsiyonel)</target> <note>placeholder</note> </trans-unit> + <trans-unit id="Enter your name…" xml:space="preserve"> + <source>Enter your name…</source> + <target>Adını gir…</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error" xml:space="preserve"> <source>Error</source> + <target>Hata</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error aborting address change" xml:space="preserve"> <source>Error aborting address change</source> + <target>Adres değişikliği iptal edilirken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error accepting conditions" xml:space="preserve"> + <source>Error accepting conditions</source> + <target>Koşulları kabul ederken hata oluştu</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve"> - <source>Error accessing database file</source> + <target>Bağlantı isteği kabul edilirken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> + <target>Üye(ler) eklenirken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error adding server" xml:space="preserve"> + <source>Error adding server</source> + <target>Sunucu eklenirken hata oluştu</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> + <target>Adres değiştirilirken hata oluştu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error changing connection profile" xml:space="preserve"> + <source>Error changing connection profile</source> + <target>Bağlantı profili değiştirilirken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error changing role" xml:space="preserve"> <source>Error changing role</source> + <target>Rol değiştirilirken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error changing setting" xml:space="preserve"> <source>Error changing setting</source> + <target>Ayar değiştirilirken hata oluştu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error changing to incognito!" xml:space="preserve"> + <source>Error changing to incognito!</source> + <target>Gizli moduna geçerken hata oluştu!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error checking token status" xml:space="preserve"> + <source>Error checking token status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve"> + <source>Error connecting to forwarding server %@. Please try later.</source> + <target>Yönlendirme sunucusu %@'ya bağlanırken hata oluştu. Lütfen daha sonra deneyin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> + <target>Adres oluşturulurken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error creating group" xml:space="preserve"> <source>Error creating group</source> + <target>Grup oluşturulurken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error creating group link" xml:space="preserve"> <source>Error creating group link</source> + <target>Grup bağlantısı oluşturulurken hata oluştu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating list" xml:space="preserve"> + <source>Error creating list</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error creating member contact" xml:space="preserve"> + <source>Error creating member contact</source> + <target>Kişi iletişimi oluşturulurken hata oluştu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating message" xml:space="preserve"> + <source>Error creating message</source> + <target>Mesaj oluşturulurken hata</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> + <target>Profil oluşturulurken hata oluştu!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating report" xml:space="preserve"> + <source>Error creating report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error decrypting file" xml:space="preserve"> + <source>Error decrypting file</source> + <target>Dosya şifresi çözülürken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting chat database" xml:space="preserve"> <source>Error deleting chat database</source> + <target>Sohbet veritabanı silinirken sorun oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting chat!" xml:space="preserve"> <source>Error deleting chat!</source> + <target>Sohbet silinirken hata oluştu!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting connection" xml:space="preserve"> <source>Error deleting connection</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve"> - <source>Error deleting contact</source> + <target>Bağlantı silinirken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> + <target>Veritabanı silinirken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting old database" xml:space="preserve"> <source>Error deleting old database</source> + <target>Eski veritabanı silinirken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting token" xml:space="preserve"> <source>Error deleting token</source> + <target>Token silinirken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting user profile" xml:space="preserve"> <source>Error deleting user profile</source> + <target>Kullanıcı profili silinirken hata oluştu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error downloading the archive" xml:space="preserve"> + <source>Error downloading the archive</source> + <target>Arşiv indirilirken hata oluştu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> + <source>Error enabling delivery receipts!</source> + <target>Görüldü bilgisi etkinleştirilirken hata oluştu!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error enabling notifications" xml:space="preserve"> <source>Error enabling notifications</source> + <target>Bildirimler etkinleştirilirken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error encrypting database" xml:space="preserve"> <source>Error encrypting database</source> + <target>Veritabanı şifrelemesi çözülürken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error exporting chat database" xml:space="preserve"> <source>Error exporting chat database</source> + <target>Sohbet veritabanı dışa aktarılırken hata oluştu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error exporting theme: %@" xml:space="preserve"> + <source>Error exporting theme: %@</source> + <target>Tema dışa aktarılırken hata oluştu: %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> + <target>Sohbet veritabanı içe aktarılırken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error joining group" xml:space="preserve"> <source>Error joining group</source> + <target>Gruba katılırken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve"> - <source>Error loading %@ servers</source> + <trans-unit id="Error loading servers" xml:space="preserve"> + <source>Error loading servers</source> + <target>Sunucular yüklenirken hata oluştu</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error migrating settings" xml:space="preserve"> + <source>Error migrating settings</source> + <target>Ayarlar taşınırken hata oluştu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error opening chat" xml:space="preserve"> + <source>Error opening chat</source> + <target>Sohbeti açarken sorun oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> + <target>Dosya alınırken sorun oluştu</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error reconnecting server" xml:space="preserve"> + <source>Error reconnecting server</source> + <target>Hata, sunucuya yeniden bağlanılıyor</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error reconnecting servers" xml:space="preserve"> + <source>Error reconnecting servers</source> + <target>Hata sunuculara yeniden bağlanılıyor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error registering for notifications" xml:space="preserve"> + <source>Error registering for notifications</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> + <target>Kişiyi silerken sorun oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve"> - <source>Error saving %@ servers</source> + <trans-unit id="Error reordering lists" xml:space="preserve"> + <source>Error reordering lists</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error resetting statistics" xml:space="preserve"> + <source>Error resetting statistics</source> + <target>Hata istatistikler sıfırlanıyor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving ICE servers" xml:space="preserve"> <source>Error saving ICE servers</source> + <target>ICE sunucularını kaydedirken sorun oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving chat list" xml:space="preserve"> + <source>Error saving chat list</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> + <target>Grup profili kaydedilirken sorun oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving passcode" xml:space="preserve"> <source>Error saving passcode</source> + <target>Parola kaydedilirken sorun oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving passphrase to keychain" xml:space="preserve"> <source>Error saving passphrase to keychain</source> + <target>Parolayı Anahtar Zincirine kaydederken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving servers" xml:space="preserve"> + <source>Error saving servers</source> + <target>Sunucular kaydedilirken hata oluştu</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error saving settings" xml:space="preserve"> + <source>Error saving settings</source> + <target>Ayarlar kaydedilirken hata oluştu</target> + <note>when migrating</note> + </trans-unit> <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> + <target>Kullanıcı şifresi kaydedilirken hata oluştu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error scanning code: %@" xml:space="preserve"> + <source>Error scanning code: %@</source> + <target>Kod taranırken hata oluştu: %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error sending email" xml:space="preserve"> <source>Error sending email</source> + <target>Eposta gönderilirken hata oluştu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error sending member contact invitation" xml:space="preserve"> + <source>Error sending member contact invitation</source> + <target>Kişi iletişim daveti gönderilirken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error sending message" xml:space="preserve"> <source>Error sending message</source> + <target>Mesaj gönderilirken hata oluştu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error setting delivery receipts!" xml:space="preserve"> + <source>Error setting delivery receipts!</source> + <target>Görüldü ayarlanırken hata oluştu!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error starting chat" xml:space="preserve"> <source>Error starting chat</source> + <target>Sohbet başlatılırken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error stopping chat" xml:space="preserve"> <source>Error stopping chat</source> + <target>Sohbet durdurulurken hata oluştu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error switching profile" xml:space="preserve"> + <source>Error switching profile</source> + <target>Profil değiştirme sırasında hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> - <note>No comment provided by engineer.</note> + <target>Profil değiştirilirken hata oluştu!</target> + <note>alertTitle</note> </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> + <target>Bağlantı senkronizasyonunda hata oluştu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error testing server connection" xml:space="preserve"> + <source>Error testing server connection</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> + <target>Grup bağlantısı güncellenirken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error updating message" xml:space="preserve"> <source>Error updating message</source> + <target>Mesaj güncellenirken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error updating server" xml:space="preserve"> + <source>Error updating server</source> + <target>Sunucu güncellenirken hata oluştu</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> + <target>Ayarları güncellerken hata oluştu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error updating user privacy" xml:space="preserve"> <source>Error updating user privacy</source> + <target>Kullanıcı gizliliği güncellenirken hata oluştu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error uploading the archive" xml:space="preserve"> + <source>Error uploading the archive</source> + <target>Arşiv yüklenirken hata oluştu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error verifying passphrase:" xml:space="preserve"> + <source>Error verifying passphrase:</source> + <target>Parola doğrulanırken hata oluştu:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error: " xml:space="preserve"> <source>Error: </source> + <target>Hata: </target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> - <note>No comment provided by engineer.</note> + <target>Hata: %@</target> + <note>alert message +file error text +snd error text</note> </trans-unit> <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> + <target>Hata: URL geçersiz</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error: no database file" xml:space="preserve"> <source>Error: no database file</source> + <target>Hata: veritabanı dosyası yok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors" xml:space="preserve"> + <source>Errors</source> + <target>Hatalar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors in servers configuration." xml:space="preserve"> + <source>Errors in servers configuration.</source> + <target>Sunucular yapılandırılırken hatalar oluştu.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> + <source>Even when disabled in the conversation.</source> + <target>Konuşma sırasında devre dışı bırakılsa bile.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Exit without saving" xml:space="preserve"> <source>Exit without saving</source> + <target>Kaydetmeden çık</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Expand" xml:space="preserve"> + <source>Expand</source> + <target>Genişlet</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Expired" xml:space="preserve"> + <source>Expired</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> + <target>Veritabanını dışarı aktar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Export error:" xml:space="preserve"> <source>Export error:</source> + <target>Dışarı çıkarma hatası:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Export theme" xml:space="preserve"> + <source>Export theme</source> + <target>Temayı dışa aktar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> + <target>Dışarı çıkarılmış veritabanı arşivi.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Exporting database archive..." xml:space="preserve"> - <source>Exporting database archive...</source> + <trans-unit id="Exported file doesn't exist" xml:space="preserve"> + <source>Exported file doesn't exist</source> + <target>Dışa aktarılan dosya mevcut değil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Exporting database archive…" xml:space="preserve"> <source>Exporting database archive…</source> + <target>Dışarı çıkarılmış veritabanı arşivi…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Failed to remove passphrase" xml:space="preserve"> <source>Failed to remove passphrase</source> + <target>Parola kaldırılamadı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fast and no wait until the sender is online!" xml:space="preserve"> <source>Fast and no wait until the sender is online!</source> + <target>Hızlı ve gönderici çevrimiçi olana kadar beklemek yok!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Faster deletion of groups." xml:space="preserve"> + <source>Faster deletion of groups.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Faster joining and more reliable messages." xml:space="preserve"> + <source>Faster joining and more reliable messages.</source> + <target>Daha hızlı katılma ve daha güvenilir mesajlar.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Faster sending messages." xml:space="preserve"> + <source>Faster sending messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Favorite" xml:space="preserve"> <source>Favorite</source> + <target>Favori</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Favorites" xml:space="preserve"> + <source>Favorites</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Dosya hatası</target> + <note>file error alert title</note> + </trans-unit> + <trans-unit id="File errors: %@" xml:space="preserve"> + <source>File errors: +%@</source> + <target>Dosya hataları: +%@</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="File is blocked by server operator: %@." xml:space="preserve"> + <source>File is blocked by server operator: +%@.</source> + <note>file error text</note> + </trans-unit> + <trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve"> + <source>File not found - most likely file was deleted or cancelled.</source> + <target>Dosya bulunamadı - muhtemelen dosya silindi veya göderim iptal edildi.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File server error: %@" xml:space="preserve"> + <source>File server error: %@</source> + <target>Dosya sunucusu hatası: %@</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File status" xml:space="preserve"> + <source>File status</source> + <target>Dosya durumu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File status: %@" xml:space="preserve"> + <source>File status: %@</source> + <target>Dosya durumu: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="File will be deleted from servers." xml:space="preserve"> <source>File will be deleted from servers.</source> + <target>Dosya sunuculardan silinecek.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="File will be received when your contact completes uploading it." xml:space="preserve"> <source>File will be received when your contact completes uploading it.</source> + <target>Dosya kişi yüklemeyi tamamladığında alınacak.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="File will be received when your contact is online, please wait or check later!" xml:space="preserve"> <source>File will be received when your contact is online, please wait or check later!</source> + <target>Dosya kişi çevrimiçi olduğunda alınacaktır, lütfen bekleyin veya daha sonra kontrol edin!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="File: %@" xml:space="preserve"> <source>File: %@</source> + <target>Dosya: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Files" xml:space="preserve"> + <source>Files</source> + <target>Dosyalar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> + <target>Dosyalar & medya</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Files and media" xml:space="preserve"> <source>Files and media</source> + <target>Dosyalar ve medya</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> - <source>Files and media are prohibited in this group.</source> + <trans-unit id="Files and media are prohibited." xml:space="preserve"> + <source>Files and media are prohibited.</source> + <target>Dosyalar ve medya bu grupta yasaklandı.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Files and media not allowed" xml:space="preserve"> + <source>Files and media not allowed</source> + <target>Dosyalar ve medyaya izin verilmiyor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Files and media prohibited!" xml:space="preserve"> <source>Files and media prohibited!</source> + <target>Dosyalar ve medya yasaklandı!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Filter unread and favorite chats." xml:space="preserve"> + <source>Filter unread and favorite chats.</source> + <target>Favori ve okunmamış sohbetleri filtrele.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration" xml:space="preserve"> + <source>Finalize migration</source> + <target>Taşıma işlemini sonlandır</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration on another device." xml:space="preserve"> + <source>Finalize migration on another device.</source> + <target>Taşıma işlemini başka bir cihazda sonlandırın.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> + <target>Sonunda, onlara sahibiz! 🚀</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Find chats faster" xml:space="preserve"> + <source>Find chats faster</source> + <target>Sohbetleri daha hızlı bul</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix" xml:space="preserve"> <source>Fix</source> + <target>Düzelt</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix connection" xml:space="preserve"> <source>Fix connection</source> + <target>Bağlantıyı düzelt</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix connection?" xml:space="preserve"> <source>Fix connection?</source> + <target>Bağlantı düzeltilsin mi?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix encryption after restoring backups." xml:space="preserve"> + <source>Fix encryption after restoring backups.</source> + <target>Yedekleri geri yükledikten sonra şifrelemeyi düzelt.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix not supported by contact" xml:space="preserve"> <source>Fix not supported by contact</source> + <target>Düzeltme kişi tarafından desteklenmiyor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix not supported by group member" xml:space="preserve"> <source>Fix not supported by group member</source> + <target>Düzeltme grup üyesi tarafından desteklenmiyor</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For all moderators" xml:space="preserve"> + <source>For all moderators</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For chat profile %@:" xml:space="preserve"> + <source>For chat profile %@:</source> + <target>Sohbet profili için %@:</target> + <note>servers error</note> + </trans-unit> <trans-unit id="For console" xml:space="preserve"> <source>For console</source> + <target>Konsol için</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." xml:space="preserve"> + <source>For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</source> + <target>Örneğin, eğer kişiniz SimpleX Sohbet sunucusundan mesajları alıyorsa, uygulamanız bu mesajları Flux sunucusundan iletecektir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For me" xml:space="preserve"> + <source>For me</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For private routing" xml:space="preserve"> + <source>For private routing</source> + <target>Gizli yönlendirme için</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For social media" xml:space="preserve"> + <source>For social media</source> + <target>Sosyal medya için</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward" xml:space="preserve"> + <source>Forward</source> + <target>İlet</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Forward %d message(s)?" xml:space="preserve"> + <source>Forward %d message(s)?</source> + <target>%d mesaj(lar)ı iletilsin mi?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Forward and save messages" xml:space="preserve"> + <source>Forward and save messages</source> + <target>Mesajları ilet ve kaydet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward messages" xml:space="preserve"> + <source>Forward messages</source> + <target>İletileri ilet</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Forward messages without files?" xml:space="preserve"> + <source>Forward messages without files?</source> + <target>Mesajlar dosyalar olmadan iletilsin mi ?</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> + <source>Forward up to 20 messages at once.</source> + <target>Aynı anda en fazla 20 mesaj iletin.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded" xml:space="preserve"> + <source>Forwarded</source> + <target>İletildi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded from" xml:space="preserve"> + <source>Forwarded from</source> + <target>Şuradan iletildi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding %lld messages" xml:space="preserve"> + <source>Forwarding %lld messages</source> + <target>%lld mesajlarını ilet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve"> + <source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source> + <target>Yönlendirme sunucusu %@, hedef sunucu %@'ya bağlanamadı. Lütfen daha sonra deneyin.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server address is incompatible with network settings: %@.</source> + <target>Yönlendirme sunucusu adresi ağ ayarlarıyla uyumsuz: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server version is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server version is incompatible with network settings: %@.</source> + <target>Yönlendirme sunucusu sürümü ağ ayarlarıyla uyumsuz: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Destination server error: %2$@</source> + <target>Yönlendirme sunucusu: %1$@ +Hedef sunucu hatası: %2$@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Error: %2$@</source> + <target>Yönlendirme sunucusu: %1$@ +Hata: %2$@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Found desktop" xml:space="preserve"> + <source>Found desktop</source> + <target>Bilgisayar bulundu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="French interface" xml:space="preserve"> <source>French interface</source> + <target>Fransızca arayüz</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Full link" xml:space="preserve"> <source>Full link</source> + <target>Bütün bağlantı adresi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Full name (optional)" xml:space="preserve"> <source>Full name (optional)</source> + <target>Bütün isim (opsiyonel)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full name:" xml:space="preserve"> - <source>Full name:</source> + <trans-unit id="Fully decentralized – visible only to members." xml:space="preserve"> + <source>Fully decentralized – visible only to members.</source> + <target>Tamamiyle merkezi olmayan - sadece kişilere görünür.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fully re-implemented - work in background!" xml:space="preserve"> <source>Fully re-implemented - work in background!</source> + <target>Arkaplanda çalışma - tamamiyle yeniden yapıldı!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Further reduced battery usage" xml:space="preserve"> <source>Further reduced battery usage</source> + <target>Daha da azaltılmış pil kullanımı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="GIFs and stickers" xml:space="preserve"> <source>GIFs and stickers</source> + <target>GİFler ve çıkartmalar</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Get notified when mentioned." xml:space="preserve"> + <source>Get notified when mentioned.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Good afternoon!" xml:space="preserve"> + <source>Good afternoon!</source> + <target>İyi öğlenler!</target> + <note>message preview</note> + </trans-unit> + <trans-unit id="Good morning!" xml:space="preserve"> + <source>Good morning!</source> + <target>Günaydın!</target> + <note>message preview</note> + </trans-unit> <trans-unit id="Group" xml:space="preserve"> <source>Group</source> + <target>Grup</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group already exists" xml:space="preserve"> + <source>Group already exists</source> + <target>Grup çoktan mevcut</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group already exists!" xml:space="preserve"> + <source>Group already exists!</source> + <target>Grup çoktan mevcut!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group display name" xml:space="preserve"> <source>Group display name</source> + <target>Grup görünen adı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group full name (optional)" xml:space="preserve"> <source>Group full name (optional)</source> + <target>Grubun bütün ismi (opsiyonel)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group image" xml:space="preserve"> <source>Group image</source> + <target>Grup fotoğrafı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group invitation" xml:space="preserve"> <source>Group invitation</source> + <target>Grup daveti</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group invitation expired" xml:space="preserve"> <source>Group invitation expired</source> + <target>Grup davetinin süresi doldu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group invitation is no longer valid, it was removed by sender." xml:space="preserve"> <source>Group invitation is no longer valid, it was removed by sender.</source> + <target>Grup davet artık geçerli değil, gönderici tarafından silindi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group link" xml:space="preserve"> <source>Group link</source> + <target>Grup bağlantısı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group links" xml:space="preserve"> <source>Group links</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve"> - <source>Group members can add message reactions.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> - <source>Group members can irreversibly delete sent messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send files and media." xml:space="preserve"> - <source>Group members can send files and media.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> + <target>Grup bağlantıları</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> + <target>Grup mesajı:</target> <note>notification</note> </trans-unit> <trans-unit id="Group moderation" xml:space="preserve"> <source>Group moderation</source> + <target>Grup yöneticiliği</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group preferences" xml:space="preserve"> <source>Group preferences</source> + <target>Grup tercihleri</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group profile" xml:space="preserve"> <source>Group profile</source> + <target>Grup profili</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group profile is stored on members' devices, not on the servers." xml:space="preserve"> <source>Group profile is stored on members' devices, not on the servers.</source> + <target>Grup profili üyelerin cihazlarında saklanır, sunucularda değil.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group welcome message" xml:space="preserve"> <source>Group welcome message</source> + <target>Grup hoşgeldin mesajı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group will be deleted for all members - this cannot be undone!" xml:space="preserve"> <source>Group will be deleted for all members - this cannot be undone!</source> + <target>Grup tüm üyelerden silinecektir - bu geri alınamaz!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group will be deleted for you - this cannot be undone!" xml:space="preserve"> <source>Group will be deleted for you - this cannot be undone!</source> + <target>Grup senden silinecektir - bu geri alınamaz!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Groups" xml:space="preserve"> + <source>Groups</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Help" xml:space="preserve"> <source>Help</source> + <target>Yardım</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Help admins moderating their groups." xml:space="preserve"> + <source>Help admins moderating their groups.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> + <target>Gizlenmiş</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hidden chat profiles" xml:space="preserve"> <source>Hidden chat profiles</source> + <target>Gizlenmiş sohbet profilleri</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hidden profile password" xml:space="preserve"> <source>Hidden profile password</source> + <target>Gizlenmiş profil şifresi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hide" xml:space="preserve"> <source>Hide</source> + <target>Gizle</target> <note>chat item action</note> </trans-unit> <trans-unit id="Hide app screen in the recent apps." xml:space="preserve"> <source>Hide app screen in the recent apps.</source> + <target>Son uygulamalarda uygulama ekranını gizle.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hide profile" xml:space="preserve"> <source>Hide profile</source> + <target>Profili gizle</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hide:" xml:space="preserve"> <source>Hide:</source> + <target>Gizle:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="History" xml:space="preserve"> <source>History</source> - <note>copied message info</note> + <target>Geçmiş</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="History is not sent to new members." xml:space="preserve"> + <source>History is not sent to new members.</source> + <target>Yeni üyelere geçmiş gönderilmedi.</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> + <target>SimpleX nasıl çalışır</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it affects privacy" xml:space="preserve"> + <source>How it affects privacy</source> + <target>Gizliliğinizi nasıl etkiler</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it helps privacy" xml:space="preserve"> + <source>How it helps privacy</source> + <target>Gizliliğinizi nasıl arttırır</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="How to" xml:space="preserve"> <source>How to</source> + <target>Nasıl yapılır</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How to use it" xml:space="preserve"> <source>How to use it</source> + <target>Nasıl kullanılır</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How to use your servers" xml:space="preserve"> <source>How to use your servers</source> + <target>Sunucularını nasıl kullanabilirsin</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Hungarian interface" xml:space="preserve"> + <source>Hungarian interface</source> + <target>Macarca arayüz</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> + <target>ICE sunucuları (her satıra bir tane)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="IP address" xml:space="preserve"> + <source>IP address</source> + <target>IP adresi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> <source>If you can't meet in person, show QR code in a video call, or share the link.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> + <target>Eğer onunla buluşamıyorsan görüntülü aramada QR kod göster veya bağlantığı paylaş.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> + <target>Eğer bu şifreyi uygulamayı açarken girersen, bütün uygulama verileri geri dönülemeyen bir şekilde silinecektir!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="If you enter your self-destruct passcode while opening the app:" xml:space="preserve"> <source>If you enter your self-destruct passcode while opening the app:</source> + <target>Uygulamayı açarken kendi kendini imha eden şifrenizi girerseniz:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." xml:space="preserve"> <source>If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app).</source> + <target>Sohbeti şimdi kullanmanız gerekiyorsa aşağıdaki **Daha sonra yap** seçeneğine dokunun (uygulamayı yeniden başlattığınızda veritabanını taşımanız önerilecektir).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Ignore" xml:space="preserve"> <source>Ignore</source> + <target>Yok say</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Image will be received when your contact completes uploading it." xml:space="preserve"> <source>Image will be received when your contact completes uploading it.</source> + <target>Kişi yüklemeyi bitirdiğinde fotoğraf alınacaktır.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve"> <source>Image will be received when your contact is online, please wait or check later!</source> + <target>Kişi çevrimiçi olduğunda fotoğraf alınacaktır, lütfen bekleyin veya daha sonra kontrol et!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Immediately" xml:space="preserve"> <source>Immediately</source> + <target>Hemen</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> + <target>Spam ve kötüye kullanıma karşı bağışıklı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Import" xml:space="preserve"> <source>Import</source> + <target>İçe aktar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Import chat database?" xml:space="preserve"> <source>Import chat database?</source> + <target>Sohbet veritabanı içe aktarılsın mı?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Import database" xml:space="preserve"> <source>Import database</source> + <target>Veritabanını içe aktar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import failed" xml:space="preserve"> + <source>Import failed</source> + <target>İçe aktarma başarısız oldu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import theme" xml:space="preserve"> + <source>Import theme</source> + <target>Temayı içe aktar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Importing archive" xml:space="preserve"> + <source>Importing archive</source> + <target>Arşiv içe aktarılıyor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved delivery, reduced traffic usage. More improvements are coming soon!" xml:space="preserve"> + <source>Improved delivery, reduced traffic usage. +More improvements are coming soon!</source> + <target>İyileştirilmiş teslimat, azaltılmış trafik kullanımı. +Daha fazla iyileştirme yakında geliyor!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved message delivery" xml:space="preserve"> + <source>Improved message delivery</source> + <target>İyileştirilmiş mesaj iletimi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> + <target>Geliştirilmiş gizlilik ve güvenlik</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Improved server configuration" xml:space="preserve"> <source>Improved server configuration</source> + <target>Geliştirilmiş sunucu yapılandırması</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="In order to continue, chat should be stopped." xml:space="preserve"> + <source>In order to continue, chat should be stopped.</source> + <target>Devam etmek için sohbetin durdurulması gerekiyor.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> - <note>copied message info</note> + <target>Cevap olarak</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="In-call sounds" xml:space="preserve"> + <source>In-call sounds</source> + <target>Arama içi sesler</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Inappropriate content" xml:space="preserve"> + <source>Inappropriate content</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Inappropriate profile" xml:space="preserve"> + <source>Inappropriate profile</source> + <note>report reason</note> </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> + <target>Gizli</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incognito groups" xml:space="preserve"> + <source>Incognito groups</source> + <target>Gizli gruplar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito mode" xml:space="preserve"> <source>Incognito mode</source> + <target>Gizli mod</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve"> - <source>Incognito mode is not supported here - your main profile will be sent to group members</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve"> - <source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source> + <trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve"> + <source>Incognito mode protects your privacy by using a new random profile for each contact.</source> + <target>Gizli mod her kişiye farklı olarak rasgele profiller kullanarak gizliliğinizi korur.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incoming audio call" xml:space="preserve"> <source>Incoming audio call</source> + <target>Gelen sesli arama</target> <note>notification</note> </trans-unit> <trans-unit id="Incoming call" xml:space="preserve"> <source>Incoming call</source> + <target>Gelen arama</target> <note>notification</note> </trans-unit> <trans-unit id="Incoming video call" xml:space="preserve"> <source>Incoming video call</source> + <target>Gelen görüntülü arama</target> <note>notification</note> </trans-unit> <trans-unit id="Incompatible database version" xml:space="preserve"> <source>Incompatible database version</source> + <target>Uyumsuz veritabanı sürümü</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible version" xml:space="preserve"> + <source>Incompatible version</source> + <target>Uyumsuz sürüm</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incorrect passcode" xml:space="preserve"> <source>Incorrect passcode</source> + <target>Uyumsuz parola</target> <note>PIN entry</note> </trans-unit> <trans-unit id="Incorrect security code!" xml:space="preserve"> <source>Incorrect security code!</source> + <target>Uyumsuz güvenlik kodu!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Info" xml:space="preserve"> <source>Info</source> + <target>Bilgi</target> <note>chat item action</note> </trans-unit> <trans-unit id="Initial role" xml:space="preserve"> <source>Initial role</source> + <target>Başlangıç rolü</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> <source>Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)</source> + <target>[Terminal için SimpleX Chat]i indir(https://github.com/simplex-chat/simplex-chat)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Instant" xml:space="preserve"> + <source>Instant</source> + <target>Anında</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Instantly" xml:space="preserve"> - <source>Instantly</source> + <target>Anlık bildirimler gizlenecek! +</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> + <target>Arayüz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Interface colors" xml:space="preserve"> + <source>Interface colors</source> + <target>Arayüz renkleri</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid" xml:space="preserve"> + <source>Invalid</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (bad token)" xml:space="preserve"> + <source>Invalid (bad token)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (expired)" xml:space="preserve"> + <source>Invalid (expired)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (unregistered)" xml:space="preserve"> + <source>Invalid (unregistered)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (wrong topic)" xml:space="preserve"> + <source>Invalid (wrong topic)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid QR code" xml:space="preserve"> + <source>Invalid QR code</source> + <target>Geçersiz QR kodu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> + <target>Geçersiz bağlanma bağlantısı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid display name!" xml:space="preserve"> + <source>Invalid display name!</source> + <target>Geçersiz görünen ad!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid link" xml:space="preserve"> + <source>Invalid link</source> + <target>Geçersiz bağlantı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Geçersiz taşıma onayı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid name!" xml:space="preserve"> + <source>Invalid name!</source> + <target>Geçersiz isim!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid response" xml:space="preserve"> + <source>Invalid response</source> + <target>Geçersiz yanıt</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> - <note>No comment provided by engineer.</note> + <target>Geçersiz sunucu adresi!</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Invalid status" xml:space="preserve"> + <source>Invalid status</source> + <target>Geçersiz durum</target> + <note>item status text</note> </trans-unit> <trans-unit id="Invitation expired!" xml:space="preserve"> <source>Invitation expired!</source> + <target>Davetin süresi geçti!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invite friends" xml:space="preserve"> <source>Invite friends</source> + <target>Arkadaşları davet et</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invite members" xml:space="preserve"> <source>Invite members</source> + <target>Üyeleri davet et</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invite to chat" xml:space="preserve"> + <source>Invite to chat</source> + <target>Sohbete davet et</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invite to group" xml:space="preserve"> <source>Invite to group</source> + <target>Gruba davet et</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Irreversible message deletion" xml:space="preserve"> <source>Irreversible message deletion</source> + <target>Geri dönülemeyen mesaj silimi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Irreversible message deletion is prohibited in this chat." xml:space="preserve"> <source>Irreversible message deletion is prohibited in this chat.</source> + <target>Bu sohbette geri döndürülemez mesaj silme yasaktır.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> + <target>Bu grupta geri döndürülemez mesaj silme yasaktır.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve"> <source>It allows having many anonymous connections without any shared data between them in a single chat profile.</source> + <target>Tek bir sohbet profilinde aralarında herhangi bir veri paylaşımı olmadan birçok anonim bağlantıya sahip olmaya izin verir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It can happen when you or your connection used the old database backup." xml:space="preserve"> <source>It can happen when you or your connection used the old database backup.</source> + <target>Siz veya bağlantınız eski veritabanı yedeğini kullandığında bu durum ortaya çıkabilir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It can happen when: 1. The messages expired in the sending client after 2 days or on the server after 30 days. 2. Message decryption failed, because you or your contact used old database backup. 3. The connection was compromised." xml:space="preserve"> @@ -2169,2777 +4383,5829 @@ 1. The messages expired in the sending client after 2 days or on the server after 30 days. 2. Message decryption failed, because you or your contact used old database backup. 3. The connection was compromised.</source> + <target>Şu durumlarda ortaya çıkabilir: +1. Mesajların gönderici istemcide 2 gün sonra veya sunucuda 30 gün sonra süresi dolmuştur. +2. Siz veya kişi eski veritabanı yedeği kullandığı için mesaj şifre çözme işlemi başarısız olmuştur. +3. Bağlantı tehlikeye girmiştir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="It protects your IP address and connections." xml:space="preserve"> + <source>It protects your IP address and connections.</source> + <target>IP adresinizi ve bağlantılarınızı korur.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> + <target>Bu bağlantı üzerinden zaten bağlanmışsınız gibi görünüyor. Eğer durum böyle değilse, bir hata oluştu (%@).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Italian interface" xml:space="preserve"> <source>Italian interface</source> + <target>İtalyanca arayüz</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Japanese interface" xml:space="preserve"> <source>Japanese interface</source> + <target>Japonca arayüz</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Join" xml:space="preserve"> <source>Join</source> - <note>No comment provided by engineer.</note> + <target>Katıl</target> + <note>swipe action</note> </trans-unit> <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> + <target>Gruba katıl</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Join group conversations" xml:space="preserve"> + <source>Join group conversations</source> + <target>Grup sohbetlerine katıl</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Join group?" xml:space="preserve"> + <source>Join group?</source> + <target>Gruba katılınsın mı?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Join incognito" xml:space="preserve"> <source>Join incognito</source> + <target>Gizli katıl</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Join with current profile" xml:space="preserve"> + <source>Join with current profile</source> + <target>Şu anki profille katıl</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Join your group? This is your link for group %@!" xml:space="preserve"> + <source>Join your group? +This is your link for group %@!</source> + <target>Bu gruba katılınsın mı? +Bu senin grup için bağlantın %@!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Joining group" xml:space="preserve"> <source>Joining group</source> + <target>Gruba katılınıyor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keep" xml:space="preserve"> + <source>Keep</source> + <target>Tut</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Keep conversation" xml:space="preserve"> + <source>Keep conversation</source> + <target>Sohbeti sakla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keep the app open to use it from desktop" xml:space="preserve"> + <source>Keep the app open to use it from desktop</source> + <target>Bilgisayardan kullanmak için uygulamayı açık tut</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keep unused invitation?" xml:space="preserve"> + <source>Keep unused invitation?</source> + <target>Kullanılmamış davet tutulsun mu?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Keep your connections" xml:space="preserve"> + <source>Keep your connections</source> + <target>Bağlantılarınızı koruyun</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="KeyChain error" xml:space="preserve"> <source>KeyChain error</source> + <target>Anahtar Zinciri hatası</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Keychain error" xml:space="preserve"> <source>Keychain error</source> + <target>Anahtar Zinciri hatası</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="LIVE" xml:space="preserve"> <source>LIVE</source> + <target>CANLI</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Large file!" xml:space="preserve"> <source>Large file!</source> + <target>Büyük dosya!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Learn more" xml:space="preserve"> <source>Learn more</source> + <target>Daha fazlası</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Leave" xml:space="preserve" approved="no"> + <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> - <target state="translated">Ayrıl</target> + <target>Ayrıl</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Leave chat" xml:space="preserve"> + <source>Leave chat</source> + <target>Sohbetten ayrıl</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Leave group" xml:space="preserve" approved="no"> + <trans-unit id="Leave chat?" xml:space="preserve"> + <source>Leave chat?</source> + <target>Sohbetten ayrılsın mı?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave group" xml:space="preserve"> <source>Leave group</source> - <target state="translated">Gruptan ayrıl</target> + <target>Gruptan ayrıl</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group?" xml:space="preserve"> <source>Leave group?</source> + <target>Gruptan çıkılsın mı?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Let's talk in SimpleX Chat" xml:space="preserve"> <source>Let's talk in SimpleX Chat</source> + <target>Hadi SimpleX Chat'te konuşalım</target> <note>email subject</note> </trans-unit> <trans-unit id="Light" xml:space="preserve"> <source>Light</source> + <target>Açık</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Limitations" xml:space="preserve"> <source>Limitations</source> + <target>Sınırlamalar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Link mobile and desktop apps! 🔗" xml:space="preserve"> + <source>Link mobile and desktop apps! 🔗</source> + <target>Telefon ve bilgisayar uygulamalarını bağla! 🔗</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Linked desktop options" xml:space="preserve"> + <source>Linked desktop options</source> + <target>Bağlanmış bilgisayar ayarları</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Linked desktops" xml:space="preserve"> + <source>Linked desktops</source> + <target>Bağlanmış bilgisayarlar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List" xml:space="preserve"> + <source>List</source> + <note>swipe action</note> + </trans-unit> + <trans-unit id="List name and emoji should be different for all lists." xml:space="preserve"> + <source>List name and emoji should be different for all lists.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List name..." xml:space="preserve"> + <source>List name...</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Live message!" xml:space="preserve"> <source>Live message!</source> + <target>Canlı mesaj!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Live messages" xml:space="preserve"> <source>Live messages</source> + <target>Canlı mesajlar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Local name" xml:space="preserve"> <source>Local name</source> + <target>Yerel isim</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Local profile data only" xml:space="preserve"> <source>Local profile data only</source> + <target>Sadece yerel profil verisi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Lock after" xml:space="preserve"> <source>Lock after</source> + <target>Sonra kilitle</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Lock mode" xml:space="preserve"> <source>Lock mode</source> + <target>Kilit modu</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> - <source>Make a private connection</source> + <trans-unit id="Make one message disappear" xml:space="preserve"> + <source>Make one message disappear</source> + <target>Bir mesajın kaybolmasını sağlayın</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Make profile private!" xml:space="preserve"> <source>Make profile private!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> + <target>Profili gizli yap!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> + <target>WebRTC ICE sunucu adreslerinin doğru formatta olduğundan, satırlara ayrıldığından ve yinelenmediğinden emin olun.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> <source>Mark deleted for everyone</source> + <target>Herkes için silinmiş olarak işaretle</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Mark read" xml:space="preserve"> <source>Mark read</source> + <target>Okunmuş olarak işaretle</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Mark verified" xml:space="preserve"> <source>Mark verified</source> + <target>Onaylanmış olarak işaretle</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Markdown in messages" xml:space="preserve"> <source>Markdown in messages</source> + <target>Mesajlarda işaretleme</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Max 30 seconds, received instantly." xml:space="preserve"> <source>Max 30 seconds, received instantly.</source> + <target>Maksimum 30 saniye, anında alındı.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Media & file servers" xml:space="preserve"> + <source>Media & file servers</source> + <target>Medya ve dosya sunucuları</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Medium" xml:space="preserve"> + <source>Medium</source> + <target>Orta</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Member" xml:space="preserve"> <source>Member</source> + <target>Kişi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Member inactive" xml:space="preserve"> + <source>Member inactive</source> + <target>Üye inaktif</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Member reports" xml:space="preserve"> + <source>Member reports</source> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All chat members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All chat members will be notified.</source> + <target>Üye rolü "%@" olarak değiştirilecektir. Tüm sohbet üyeleri bilgilendirilecektir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> + <target>Üye rolü "%@" olarak değiştirilecektir. Ve tüm grup üyeleri bilgilendirilecektir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Member role will be changed to "%@". The member will receive a new invitation." xml:space="preserve"> <source>Member role will be changed to "%@". The member will receive a new invitation.</source> + <target>Üye rolü "%@" olarak değiştirilecektir. Ve üye yeni bir davetiye alacaktır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from chat - this cannot be undone!</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> + <target>Üye gruptan çıkarılacaktır - bu geri alınamaz!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can add message reactions." xml:space="preserve"> + <source>Members can add message reactions.</source> + <target>Grup üyeleri mesaj tepkileri ekleyebilir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Members can irreversibly delete sent messages. (24 hours)</source> + <target>Grup üyeleri, gönderilen mesajları kalıcı olarak silebilir. (24 saat içinde)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can report messsages to moderators." xml:space="preserve"> + <source>Members can report messsages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send SimpleX links." xml:space="preserve"> + <source>Members can send SimpleX links.</source> + <target>Grup üyeleri SimpleX bağlantıları gönderebilir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> + <target>Grup üyeleri doğrudan mesajlar gönderebilir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> + <target>Grup üyeleri kaybolan mesajlar gönderebilir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send files and media." xml:space="preserve"> + <source>Members can send files and media.</source> + <target>Grup üyeleri dosyalar ve medya gönderebilir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> + <target>Grup üyeleri sesli mesajlar gönderebilir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mention members 👋" xml:space="preserve"> + <source>Mention members 👋</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Menus" xml:space="preserve"> + <source>Menus</source> + <target>Menüler</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> + <target>Mesaj gönderim hatası</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message delivery receipts!" xml:space="preserve"> + <source>Message delivery receipts!</source> + <target>Mesaj alındı bilgisi!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message delivery warning" xml:space="preserve"> + <source>Message delivery warning</source> + <target>Mesaj iletimi uyarısı</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> + <target>Mesaj taslağı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message forwarded" xml:space="preserve"> + <source>Message forwarded</source> + <target>Mesaj iletildi</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message may be delivered later if member becomes active." xml:space="preserve"> + <source>Message may be delivered later if member becomes active.</source> + <target>Kullanıcı aktif olursa mesaj iletilebilir.</target> + <note>item status description</note> + </trans-unit> + <trans-unit id="Message queue info" xml:space="preserve"> + <source>Message queue info</source> + <target>Mesaj kuyruğu bilgisi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Message reactions" xml:space="preserve"> <source>Message reactions</source> + <target>Mesaj tepkileri</target> <note>chat feature</note> </trans-unit> <trans-unit id="Message reactions are prohibited in this chat." xml:space="preserve"> <source>Message reactions are prohibited in this chat.</source> + <target>Mesaj tepkileri bu sohbette yasaklandı.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> - <source>Message reactions are prohibited in this group.</source> + <trans-unit id="Message reactions are prohibited." xml:space="preserve"> + <source>Message reactions are prohibited.</source> + <target>Mesaj tepkileri bu grupta yasaklandı.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message reception" xml:space="preserve"> + <source>Message reception</source> + <target>Mesaj alındısı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message servers" xml:space="preserve"> + <source>Message servers</source> + <target>Mesaj sunucuları</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message shape" xml:space="preserve"> + <source>Message shape</source> + <target>Mesaj şekli</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message source remains private." xml:space="preserve"> + <source>Message source remains private.</source> + <target>Mesaj kaynağı gizli kalır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status" xml:space="preserve"> + <source>Message status</source> + <target>Mesaj durumu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status: %@" xml:space="preserve"> + <source>Message status: %@</source> + <target>Mesaj durumu: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> + <target>Mesaj yazısı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message too large" xml:space="preserve"> + <source>Message too large</source> + <target>Mesaj çok büyük</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> + <target>Mesajlar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Messages & files" xml:space="preserve"> <source>Messages & files</source> + <target>Mesajlar & dosyalar</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrating database archive..." xml:space="preserve"> - <source>Migrating database archive...</source> + <trans-unit id="Messages from %@ will be shown!" xml:space="preserve"> + <source>Messages from %@ will be shown!</source> + <target>%@ den gelen mesajlar gösterilecektir!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages in this chat will never be deleted." xml:space="preserve"> + <source>Messages in this chat will never be deleted.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages received" xml:space="preserve"> + <source>Messages received</source> + <target>Mesajlar alındı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages sent" xml:space="preserve"> + <source>Messages sent</source> + <target>Mesajlar gönderildi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages were deleted after you selected them." xml:space="preserve"> + <source>Messages were deleted after you selected them.</source> + <target>Mesajlar siz seçtikten sonra silindi.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Mesajlar, dosyalar ve aramalar **uçtan uca şifreleme** ile mükemmel ileri gizlilik, inkar ve izinsiz giriş kurtarma ile korunur.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Mesajlar, dosyalar ve aramalar **kuantum dirençli e2e şifreleme** ile mükemmel ileri gizlilik, inkar ve zorla girme kurtarma ile korunur.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate device" xml:space="preserve"> + <source>Migrate device</source> + <target>Cihazı taşıma</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate from another device" xml:space="preserve"> + <source>Migrate from another device</source> + <target>Başka bir cihazdan geçiş yapın</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate here" xml:space="preserve"> + <source>Migrate here</source> + <target>Buraya göç edin</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device" xml:space="preserve"> + <source>Migrate to another device</source> + <target>Başka bir cihaza taşıma</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device via QR code." xml:space="preserve"> + <source>Migrate to another device via QR code.</source> + <target>QR kodu aracılığıyla başka bir cihaza geçiş yapın.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating" xml:space="preserve"> + <source>Migrating</source> + <target>Göçmenlik</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Migrating database archive…" xml:space="preserve"> <source>Migrating database archive…</source> + <target>Veritabanı arşivine geçiliyor…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migration complete" xml:space="preserve"> + <source>Migration complete</source> + <target>Geçiş tamamlandı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> + <target>Geçiş hatası:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." xml:space="preserve"> <source>Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat).</source> + <target>Geçiş başarısız oldu. Alttaki **Geç** tuşuna basarak şu anki veritabanını kullanabilirsiniz. Lütfen sorunu sohbet veya e-posta yoluyla uygulama geliştiricilerine bildirin[chat@simplex.chat](mailto:chat@simplex.chat).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Migration is completed" xml:space="preserve"> <source>Migration is completed</source> + <target>Geçiş tamamlandı</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> + <target>Geçişler:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> <source>Moderate</source> + <target>Yönet</target> <note>chat item action</note> </trans-unit> <trans-unit id="Moderated at" xml:space="preserve"> <source>Moderated at</source> + <target>de yönetildi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderated at: %@" xml:space="preserve"> <source>Moderated at: %@</source> + <target>%@ de yönetildi</target> <note>copied message info</note> </trans-unit> + <trans-unit id="More" xml:space="preserve"> + <source>More</source> + <note>swipe action</note> + </trans-unit> <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> + <target>Daha fazla geliştirmeler yakında geliyor!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> - <source>Most likely this contact has deleted the connection with you.</source> + <trans-unit id="More reliable network connection." xml:space="preserve"> + <source>More reliable network connection.</source> + <target>Daha güvenilir ağ bağlantısı.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="More reliable notifications" xml:space="preserve"> + <source>More reliable notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> + <source>Most likely this connection is deleted.</source> + <target>Büyük ihtimalle bu bağlantı silinmiş.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> + <target>Çoklu sohbet profili</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> - <note>No comment provided by engineer.</note> + <target>Sustur</target> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Mute all" xml:space="preserve"> + <source>Mute all</source> + <note>notification label action</note> </trans-unit> <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> + <target>Aktif değilken susturuldu!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> - <note>No comment provided by engineer.</note> + <target>İsim</target> + <note>swipe action</note> </trans-unit> <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> + <target>Ağ & sunucular</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network connection" xml:space="preserve"> + <source>Network connection</source> + <target>Ağ bağlantısı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network decentralization" xml:space="preserve"> + <source>Network decentralization</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve"> + <source>Network issues - message expired after many attempts to send it.</source> + <target>Ağ sorunları - birçok gönderme denemesinden sonra mesajın süresi doldu.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Network management" xml:space="preserve"> + <source>Network management</source> + <target>Ağ yönetimi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network operator" xml:space="preserve"> + <source>Network operator</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> + <target>Ağ ayarları</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Network status" xml:space="preserve"> <source>Network status</source> + <target>Ağ durumu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New" xml:space="preserve"> + <source>New</source> + <note>token status text</note> + </trans-unit> <trans-unit id="New Passcode" xml:space="preserve"> <source>New Passcode</source> + <target>Yeni şifre</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve"> + <source>New SOCKS credentials will be used every time you start the app.</source> + <target>Uygulamayı her başlattığınızda yeni SOCKS kimlik bilgileri kullanılacaktır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve"> + <source>New SOCKS credentials will be used for each server.</source> + <target>Her sunucu için yeni SOCKS kimlik bilgileri kullanılacaktır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat" xml:space="preserve"> + <source>New chat</source> + <target>Yeni sohbet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat experience 🎉" xml:space="preserve"> + <source>New chat experience 🎉</source> + <target>Yeni bir sohbet deneyimi 🎉</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> + <target>Yeni bağlantı isteği</target> <note>notification</note> </trans-unit> <trans-unit id="New contact:" xml:space="preserve"> <source>New contact:</source> + <target>Yeni kişi:</target> <note>notification</note> </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> - <source>New database archive</source> + <trans-unit id="New desktop app!" xml:space="preserve"> + <source>New desktop app!</source> + <target>Yeni bilgisayar uygulaması!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="New display name" xml:space="preserve"> <source>New display name</source> + <target>Yeni görünen ad</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <note>notification</note> + </trans-unit> <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> + <target>%@ da yeni</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New media options" xml:space="preserve"> + <source>New media options</source> + <target>Yeni medya seçenekleri</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> + <target>Yeni üye rolü</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="New message" xml:space="preserve"> <source>New message</source> + <target>Yeni mesaj</target> <note>notification</note> </trans-unit> <trans-unit id="New passphrase…" xml:space="preserve"> <source>New passphrase…</source> + <target>Yeni parola…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New server" xml:space="preserve"> + <source>New server</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No" xml:space="preserve"> <source>No</source> + <target>Hayır</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No app password" xml:space="preserve"> <source>No app password</source> + <target>Uygulama şifresi yok</target> <note>Authentication unavailable</note> </trans-unit> + <trans-unit id="No chats" xml:space="preserve"> + <source>No chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats found" xml:space="preserve"> + <source>No chats found</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats in list %@" xml:space="preserve"> + <source>No chats in list %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> + <target>Hiçbir kişi seçilmedi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No contacts to add" xml:space="preserve"> <source>No contacts to add</source> + <target>Eklenecek kişi yok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No delivery information" xml:space="preserve"> + <source>No delivery information</source> + <target>Gönderim bilgisi yok</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No device token!" xml:space="preserve"> <source>No device token!</source> + <target>Cihaz tokeni yok!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No direct connection yet, message is forwarded by admin." xml:space="preserve"> + <source>No direct connection yet, message is forwarded by admin.</source> + <target>Henüz direkt bağlantı yok mesaj admin tarafından yönlendirildi.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="No filtered chats" xml:space="preserve"> <source>No filtered chats</source> + <target>Filtrelenmiş sohbetler yok</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No group!" xml:space="preserve"> <source>Group not found!</source> + <target>Grup bulunamadı!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No history" xml:space="preserve"> <source>No history</source> + <target>Geçmiş yok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No info, try to reload" xml:space="preserve"> + <source>No info, try to reload</source> + <target>Bilgi yok, yenilemeyi deneyin</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No media & file servers." xml:space="preserve"> + <source>No media & file servers.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No message" xml:space="preserve"> + <source>No message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No message servers." xml:space="preserve"> + <source>No message servers.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No network connection" xml:space="preserve"> + <source>No network connection</source> + <target>Ağ bağlantısı yok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record speech" xml:space="preserve"> + <source>No permission to record speech</source> + <target>Konuşma kaydetme izni yok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record video" xml:space="preserve"> + <source>No permission to record video</source> + <target>Video kaydı için izin yok</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> <source>No permission to record voice message</source> + <target>Sesli mesaj kaydetmek için izin yok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No push server" xml:space="preserve"> + <source>No push server</source> + <target>Yerel</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> + <target>Hiç alınmış veya gönderilmiş dosya yok</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No servers for private message routing." xml:space="preserve"> + <source>No servers for private message routing.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive files." xml:space="preserve"> + <source>No servers to receive files.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive messages." xml:space="preserve"> + <source>No servers to receive messages.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to send files." xml:space="preserve"> + <source>No servers to send files.</source> + <note>servers error</note> + </trans-unit> + <trans-unit id="No token!" xml:space="preserve"> + <source>No token!</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="No unread chats" xml:space="preserve"> + <source>No unread chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> + <target>Herhangi bir kullanıcı tanımlayıcısı yok.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Not compatible!" xml:space="preserve"> + <source>Not compatible!</source> + <target>Uyumlu değil!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notes" xml:space="preserve"> + <source>Notes</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing selected" xml:space="preserve"> + <source>Nothing selected</source> + <target>Hiçbir şey seçilmedi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing to forward!" xml:space="preserve"> + <source>Nothing to forward!</source> + <target>Yönlendirilecek bir şey yok!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> + <target>Bildirimler</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Notifications are disabled!" xml:space="preserve"> <source>Notifications are disabled!</source> + <target>Bildirimler devre dışı!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notifications error" xml:space="preserve"> + <source>Notifications error</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Notifications privacy" xml:space="preserve"> + <source>Notifications privacy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications status" xml:space="preserve"> + <source>Notifications status</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. - disable members ("observer" role)</source> + <target>Şimdi yöneticiler: +- üyelerin mesajlarını silebilir +- üyeleri devre dışı bırakabilir ("gözlemci" rolü)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="OK" xml:space="preserve"> + <source>OK</source> + <target>TAMAM</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Off" xml:space="preserve"> <source>Off</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> - <source>Off (Local)</source> - <note>No comment provided by engineer.</note> + <target>Kapalı</target> + <note>blur media</note> </trans-unit> <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> - <note>No comment provided by engineer.</note> + <target>Tamam</target> + <note>alert button</note> </trans-unit> <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> - <source>Old database archive</source> + <target>Eski veritabanı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> + <target>Tek zamanlı bağlantı daveti</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> + <trans-unit id="Onion hosts will be **required** for connection. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be **required** for connection. +Requires compatible VPN.</source> + <target>Bağlantı için Onion ana bilgisayarları gerekecektir. +VPN'nin etkinleştirilmesi gerekir.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> + <trans-unit id="Onion hosts will be used when available. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be used when available. +Requires compatible VPN.</source> + <target>Onion ana bilgisayarları mevcutsa kullanılacaktır. +VPN'nin etkinleştirilmesi gerekir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will not be used." xml:space="preserve"> <source>Onion hosts will not be used.</source> + <target>Onion ana bilgisayarları kullanılmayacaktır.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only chat owners can change preferences." xml:space="preserve"> + <source>Only chat owners can change preferences.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> + <target>Yalnızca istemci cihazlar kullanıcı profillerini, kişileri, grupları ve **2 katmanlı uçtan uca şifreleme** ile gönderilen mesajları depolar.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only delete conversation" xml:space="preserve"> + <source>Only delete conversation</source> + <target>Sadece sohbeti sil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> <source>Only group owners can change group preferences.</source> + <target>Grup tercihlerini yalnızca grup sahipleri değiştirebilir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can enable files and media." xml:space="preserve"> <source>Only group owners can enable files and media.</source> + <target>Yalnızca grup sahipleri dosyaları ve medyayı etkinleştirebilir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can enable voice messages." xml:space="preserve"> <source>Only group owners can enable voice messages.</source> + <target>Yalnızca grup sahipleri sesli mesajları etkinleştirebilir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only sender and moderators see it" xml:space="preserve"> + <source>Only sender and moderators see it</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you and moderators see it" xml:space="preserve"> + <source>Only you and moderators see it</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can add message reactions." xml:space="preserve"> <source>Only you can add message reactions.</source> + <target>Sadece siz mesaj tepkileri ekleyebilirsiniz.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source> + <target>Mesajları yalnızca siz geri döndürülemez şekilde silebilirsiniz (kişiniz bunları silinmek üzere işaretleyebilir). (24 saat içinde)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can make calls." xml:space="preserve"> <source>Only you can make calls.</source> + <target>Sadece sen aramalar yapabilirsin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can send disappearing messages." xml:space="preserve"> <source>Only you can send disappearing messages.</source> + <target>Sadece sen kaybolan mesajlar gönderebilirsin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can send voice messages." xml:space="preserve"> <source>Only you can send voice messages.</source> + <target>Sadece sen sesli mesajlar gönderebilirsin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can add message reactions." xml:space="preserve"> <source>Only your contact can add message reactions.</source> + <target>Sadece karşıdaki kişi mesaj tepkileri ekleyebilir.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source> + <target>Yalnızca kişiniz mesajları geri alınamaz şekilde silebilir (silinmeleri için işaretleyebilirsiniz). (24 saat içinde)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can make calls." xml:space="preserve"> <source>Only your contact can make calls.</source> + <target>Sadece karşıdaki kişi aramalar yapabilir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can send disappearing messages." xml:space="preserve"> <source>Only your contact can send disappearing messages.</source> + <target>Sadece karşıdaki kişi kaybolan mesajlar gönderebilir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can send voice messages." xml:space="preserve"> <source>Only your contact can send voice messages.</source> + <target>Sadece karşıdaki kişi sesli mesajlar gönderebilir.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open" xml:space="preserve"> + <source>Open</source> + <target>Aç</target> + <note>alert action</note> + </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> + <target>Ayarları aç</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open changes" xml:space="preserve"> + <source>Open changes</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> + <target>Sohbeti aç</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Open chat console" xml:space="preserve"> <source>Open chat console</source> + <target>Sohbet konsolunu aç</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve"> - <source>Open user profiles</source> + <trans-unit id="Open conditions" xml:space="preserve"> + <source>Open conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open group" xml:space="preserve"> + <source>Open group</source> + <target>Grubu aç</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open link?" xml:space="preserve"> + <source>Open link?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Open migration to another device" xml:space="preserve"> + <source>Open migration to another device</source> + <target>Başka bir cihaza açık geçiş</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> + <trans-unit id="Opening app…" xml:space="preserve"> + <source>Opening app…</source> + <target>Uygulama açılıyor…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve"> - <source>Opening database…</source> + <trans-unit id="Operator" xml:space="preserve"> + <source>Operator</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve"> - <source>Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.</source> + <trans-unit id="Operator server" xml:space="preserve"> + <source>Operator server</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Or import archive file" xml:space="preserve"> + <source>Or import archive file</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Or paste archive link" xml:space="preserve"> + <source>Or paste archive link</source> + <target>Veya arşiv bağlantısını yapıştırın</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or scan QR code" xml:space="preserve"> + <source>Or scan QR code</source> + <target>Veya QR kodu okut</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or securely share this file link" xml:space="preserve"> + <source>Or securely share this file link</source> + <target>Veya bu dosya bağlantısını güvenli bir şekilde paylaşın</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or show this code" xml:space="preserve"> + <source>Or show this code</source> + <target>Veya bu kodu göster</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or to share privately" xml:space="preserve"> + <source>Or to share privately</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Organize chats into lists" xml:space="preserve"> + <source>Organize chats into lists</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other" xml:space="preserve"> + <source>Other</source> + <target>Diğer</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other file errors: %@" xml:space="preserve"> + <source>Other file errors: +%@</source> + <target>Diğer dosya hataları: +%@</target> + <note>alert message</note> + </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> + <target>PING sayısı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="PING interval" xml:space="preserve"> <source>PING interval</source> + <target>PING aralığı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Passcode" xml:space="preserve"> <source>Passcode</source> + <target>Şifre</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Passcode changed!" xml:space="preserve"> <source>Passcode changed!</source> + <target>Şifre değiştirildi!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Passcode entry" xml:space="preserve"> <source>Passcode entry</source> + <target>Şifre girişi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Passcode not changed!" xml:space="preserve"> <source>Passcode not changed!</source> + <target>Şifre değiştirilmedi!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Passcode set!" xml:space="preserve"> <source>Passcode set!</source> + <target>Şifre ayarlandı!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Password" xml:space="preserve"> + <source>Password</source> + <target>Şifre</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> + <target>Gösterilecek şifre</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste" xml:space="preserve"> - <source>Paste</source> + <trans-unit id="Past member %@" xml:space="preserve"> + <source>Past member %@</source> + <target>Geçmiş üye %@</target> + <note>past/unknown group member</note> + </trans-unit> + <trans-unit id="Paste desktop address" xml:space="preserve"> + <source>Paste desktop address</source> + <target>Bilgisayar adresini yapıştır</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Paste image" xml:space="preserve"> <source>Paste image</source> + <target>Fotoğraf yapıştır</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve"> - <source>Paste received link</source> + <trans-unit id="Paste link to connect!" xml:space="preserve"> + <source>Paste link to connect!</source> + <target>Bağlanmak için bağlantıyı yapıştır!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve"> - <source>Paste the link you received into the box below to connect with your contact.</source> + <trans-unit id="Paste the link you received" xml:space="preserve"> + <source>Paste the link you received</source> + <target>Aldığın bağlantıyı yapıştır</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> + <trans-unit id="Pending" xml:space="preserve"> + <source>Pending</source> + <target>Bekleniyor</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Periodically" xml:space="preserve"> - <source>Periodically</source> + <trans-unit id="Periodic" xml:space="preserve"> + <source>Periodic</source> + <target>Periyodik olarak</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Permanent decryption error" xml:space="preserve"> <source>Permanent decryption error</source> + <target>Kalıcı şifre çözümü hatası</target> <note>message decrypt error item</note> </trans-unit> + <trans-unit id="Picture-in-picture calls" xml:space="preserve"> + <source>Picture-in-picture calls</source> + <target>Resim içinde resim aramaları</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Play from the chat list." xml:space="preserve"> + <source>Play from the chat list.</source> + <target>Sohbet listesinden oynat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable calls." xml:space="preserve"> + <source>Please ask your contact to enable calls.</source> + <target>Lütfen kişinizden çağrılara izin vermesini isteyin.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> + <target>Lütfen konuştuğunuz kişiden sesli mesaj göndermeyi etkinleştirmesini isteyin.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers." xml:space="preserve"> + <source>Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers.</source> + <target>Lütfen telefonun ve bilgisayarın aynı lokal ağa bağlı olduğundan ve bilgisayar güvenlik duvarının bağlantıya izin verdiğinden emin olun. +Lütfen diğer herhangi bir sorunu geliştiricilerle paylaşın.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> + <target>Lütfen doğru bağlantıyı kullandığınızı kontrol edin veya kişiden size başka bir bağlantı göndermesini isteyin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please check your network connection with %@ and try again." xml:space="preserve"> <source>Please check your network connection with %@ and try again.</source> + <target>Lütfen ağ bağlantınızı %@ ile kontrol edin ve tekrar deneyin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please check yours and your contact preferences." xml:space="preserve"> <source>Please check yours and your contact preferences.</source> + <target>Lütfen sizinkini ve iletişim tercihlerinizi kontrol edin.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve"> + <source>Please confirm that network settings are correct for this device.</source> + <target>Lütfen bu cihaz için ağ ayarlarının doğru olduğunu onaylayın.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact developers. Error: %@" xml:space="preserve"> + <source>Please contact developers. +Error: %@</source> + <target>Lütfen geliştiricilerle irtibata geçin. +Hata: %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> + <target>Lütfen grup yöneticisiyle irtibata geçin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please enter correct current passphrase." xml:space="preserve"> <source>Please enter correct current passphrase.</source> + <target>Lütfen şu anki doğru olan parolayı girin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please enter the previous password after restoring database backup. This action can not be undone." xml:space="preserve"> <source>Please enter the previous password after restoring database backup. This action can not be undone.</source> + <target>Veritabanı yedeğini geri yükledikten sonra lütfen önceki şifreyi girin. Bu işlem geri alınamaz.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please remember or store it securely - there is no way to recover a lost passcode!" xml:space="preserve"> <source>Please remember or store it securely - there is no way to recover a lost passcode!</source> + <target>Lütfen iyi hatırlayın veya güvenli bir şekilde saklayın - kaybolmuş bir parolayı kurtarmanın bir yolu yoktur!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please report it to the developers." xml:space="preserve"> <source>Please report it to the developers.</source> + <target>Lütfen geliştiricilere bildirin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please restart the app and migrate the database to enable push notifications." xml:space="preserve"> <source>Please restart the app and migrate the database to enable push notifications.</source> + <target>Lütfen uygulamayı yeniden başlatın ve anlık bildirimleri etkinleştirmek için veritabanını taşıyın.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please store passphrase securely, you will NOT be able to access chat if you lose it." xml:space="preserve"> <source>Please store passphrase securely, you will NOT be able to access chat if you lose it.</source> + <target>Lütfen parolayı güvenli bir şekilde saklayın, kaybederseniz sohbete ERİŞEMEZSİNİZ.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please store passphrase securely, you will NOT be able to change it if you lose it." xml:space="preserve"> <source>Please store passphrase securely, you will NOT be able to change it if you lose it.</source> + <target>Lütfen parolayı güvenli bir şekilde saklayın, kaybederseniz parolayı DEĞİŞTİREMEZSİNİZ.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please try to disable and re-enable notfications." xml:space="preserve"> + <source>Please try to disable and re-enable notfications.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token activation to complete." xml:space="preserve"> + <source>Please wait for token activation to complete.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token to be registered." xml:space="preserve"> + <source>Please wait for token to be registered.</source> + <note>token info</note> + </trans-unit> <trans-unit id="Polish interface" xml:space="preserve"> <source>Polish interface</source> + <target>Lehçe arayüz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Port" xml:space="preserve"> + <source>Port</source> + <target>Port</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> + <target>Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil</target> <note>server test error</note> </trans-unit> <trans-unit id="Preserve the last message draft, with attachments." xml:space="preserve"> <source>Preserve the last message draft, with attachments.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Preset server" xml:space="preserve"> - <source>Preset server</source> + <target>Son mesaj taslağını ekleriyle birlikte koru.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> + <target>Ön ayarlı sunucu adresi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Preset servers" xml:space="preserve"> + <source>Preset servers</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Preview" xml:space="preserve"> <source>Preview</source> + <target>Ön izleme</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Previously connected servers" xml:space="preserve"> + <source>Previously connected servers</source> + <target>Önceden bağlanılmış sunucular</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> + <target>Gizlilik & güvenlik</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy for your customers." xml:space="preserve"> + <source>Privacy for your customers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy policy and conditions of use." xml:space="preserve"> + <source>Privacy policy and conditions of use.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Privacy redefined" xml:space="preserve"> <source>Privacy redefined</source> + <target>Gizlilik yeniden tanımlandı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve"> + <source>Private chats, groups and your contacts are not accessible to server operators.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> + <target>Gizli dosya adları</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private media file names." xml:space="preserve"> + <source>Private media file names.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing" xml:space="preserve"> + <source>Private message routing</source> + <target>Gizli mesaj yönlendirme</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing 🚀" xml:space="preserve"> + <source>Private message routing 🚀</source> + <target>Gizli mesaj yönlendirme 🚀</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private notes" xml:space="preserve"> + <source>Private notes</source> + <target>Gizli notlar</target> + <note>name of notes to self</note> + </trans-unit> + <trans-unit id="Private routing" xml:space="preserve"> + <source>Private routing</source> + <target>Gizli yönlendirme</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private routing error" xml:space="preserve"> + <source>Private routing error</source> + <target>Gizli yönlendirme hatası</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> + <target>Profil ve sunucu bağlantıları</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile image" xml:space="preserve"> <source>Profile image</source> + <target>Profil fotoğrafı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Profile images" xml:space="preserve"> + <source>Profile images</source> + <target>Profil resimleri</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile password" xml:space="preserve"> <source>Profile password</source> + <target>Profil parolası</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Profile theme" xml:space="preserve"> + <source>Profile theme</source> + <target>Profil teması</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> <source>Profile update will be sent to your contacts.</source> - <note>No comment provided by engineer.</note> + <target>Profil güncellemesi kişilerinize gönderilecektir.</target> + <note>alert message</note> </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> + <target>Sesli/görüntülü aramaları yasakla.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit irreversible message deletion." xml:space="preserve"> <source>Prohibit irreversible message deletion.</source> + <target>Geri dönüşsüz mesaj silme işlemini yasakla.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit message reactions." xml:space="preserve"> <source>Prohibit message reactions.</source> + <target>Mesaj tepkisini yasakla.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit messages reactions." xml:space="preserve"> <source>Prohibit messages reactions.</source> + <target>Mesajlarda tepkileri yasakla.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit reporting messages to moderators." xml:space="preserve"> + <source>Prohibit reporting messages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending SimpleX links." xml:space="preserve"> + <source>Prohibit sending SimpleX links.</source> + <target>SimpleX bağlantısı gönderimini yasakla.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> + <target>Üyelere doğrudan mesaj göndermeyi yasakla.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit sending disappearing messages." xml:space="preserve"> <source>Prohibit sending disappearing messages.</source> + <target>Kaybolan mesajların gönderimini yasakla.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit sending files and media." xml:space="preserve"> <source>Prohibit sending files and media.</source> + <target>Dosyalar ve medya gönderimlerini yasakla.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit sending voice messages." xml:space="preserve"> <source>Prohibit sending voice messages.</source> + <target>Sesli mesajların gönderimini yasakla.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Protect IP address" xml:space="preserve"> + <source>Protect IP address</source> + <target>IP adresini koru</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> + <target>Uygulama ekranını koru</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve"> + <source>Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings.</source> + <target>IP adresinizi kişileriniz tarafından seçilen mesajlaşma yönlendiricilerinden koruyun. +*Ağ ve sunucular* ayarlarında etkinleştirin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> + <target>Bir parolayla birlikte sohbet profillerini koru!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Protocol timeout" xml:space="preserve"> <source>Protocol timeout</source> + <target>Protokol zaman aşımı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Protocol timeout per KB" xml:space="preserve"> <source>Protocol timeout per KB</source> + <target>KB başına protokol zaman aşımı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied" xml:space="preserve"> + <source>Proxied</source> + <target>Proxyli</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied servers" xml:space="preserve"> + <source>Proxied servers</source> + <target>Proxy sunucuları</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxy requires password" xml:space="preserve"> + <source>Proxy requires password</source> + <target>Proxy şifre gerektirir</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> + <target>Anında bildirimler</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Push server" xml:space="preserve"> + <source>Push server</source> + <target>Push sunucu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Quantum resistant encryption" xml:space="preserve"> + <source>Quantum resistant encryption</source> + <target>Kuantum dirençli şifreleme</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> <source>Rate the app</source> + <target>Uygulamayı değerlendir</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reachable chat toolbar" xml:space="preserve"> + <source>Reachable chat toolbar</source> + <target>Erişilebilir sohbet araç çubuğu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> + <target>Tepki ver…</target> <note>chat item menu</note> </trans-unit> <trans-unit id="Read" xml:space="preserve"> <source>Read</source> - <note>No comment provided by engineer.</note> + <target>Oku</target> + <note>swipe action</note> </trans-unit> <trans-unit id="Read more" xml:space="preserve"> <source>Read more</source> + <target>Dahasını oku</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source> + <target>[Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</source> + <target>[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).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> <source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> - <source>Read more in our GitHub repository.</source> + <target>[Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> + <target>[GitHub deposu]nda daha fazlasını okuyun(https://github.com/simplex-chat/simplex-chat#readme).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Receipts are disabled" xml:space="preserve"> + <source>Receipts are disabled</source> + <target>Alıcılar devre dışı bırakıldı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Receive errors" xml:space="preserve"> + <source>Receive errors</source> + <target>Alım sırasında hata</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> + <target>Şuradan alındı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Received at: %@" xml:space="preserve"> <source>Received at: %@</source> + <target>Şuradan alındı: %@</target> <note>copied message info</note> </trans-unit> <trans-unit id="Received file event" xml:space="preserve"> <source>Received file event</source> + <target>Dosya etkinliği alındı</target> <note>notification</note> </trans-unit> <trans-unit id="Received message" xml:space="preserve"> <source>Received message</source> + <target>Mesaj alındı</target> <note>message info title</note> </trans-unit> + <trans-unit id="Received messages" xml:space="preserve"> + <source>Received messages</source> + <target>Alınan mesajlar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received reply" xml:space="preserve"> + <source>Received reply</source> + <target>Alınan cevap</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received total" xml:space="preserve"> + <source>Received total</source> + <target>Toplam alınan</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> + <target>Alıcı adresi farklı bir sunucuya değiştirilecektir. Gönderici çevrimiçi olduktan sonra adres değişikliği tamamlanacaktır.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Receiving file will be stopped." xml:space="preserve"> <source>Receiving file will be stopped.</source> + <target>Dosya alımı durdurulacaktır.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Receiving via" xml:space="preserve"> <source>Receiving via</source> + <target>Aracılığıyla alınıyor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recent history and improved [directory bot](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)." xml:space="preserve"> + <source>Recent history and improved [directory bot](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).</source> + <target>Yakın geçmiş ve geliştirilmiş [dizin botu](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).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve"> + <source>Recipient(s) can't see who this message is from.</source> + <target>Alıcı(lar) bu mesajın kimden geldiğini göremez.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> + <target>Alıcılar yazdığına göre güncellemeleri görecektir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect" xml:space="preserve"> + <source>Reconnect</source> + <target>Yeniden bağlan</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> + <target>Mesaj teslimini zorlamak için bağlı tüm sunucuları yeniden bağlayın. Ek trafik kullanır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers" xml:space="preserve"> + <source>Reconnect all servers</source> + <target>Tüm sunuculara yeniden bağlan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers?" xml:space="preserve"> + <source>Reconnect all servers?</source> + <target>Tüm sunuculara yeniden bağlansın mı?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect server to force message delivery. It uses additional traffic.</source> + <target>Mesajı göndermeye zorlamak için sunucuya yeniden bağlan. Bu ekstra internet kullanır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server?" xml:space="preserve"> + <source>Reconnect server?</source> + <target>Sunucuya yeniden bağlansın mı ?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> + <target>Sunuculara yeniden bağlanılsın mı?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Record updated at" xml:space="preserve"> <source>Record updated at</source> + <target>Kayıt şu zamanda güncellendi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Record updated at: %@" xml:space="preserve"> <source>Record updated at: %@</source> + <target>Kayıt şu zamanda güncellendi: %@</target> <note>copied message info</note> </trans-unit> <trans-unit id="Reduced battery usage" xml:space="preserve"> <source>Reduced battery usage</source> + <target>Azaltılmış pil kullanımı</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Register" xml:space="preserve"> + <source>Register</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Register notification token?" xml:space="preserve"> + <source>Register notification token?</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Registered" xml:space="preserve"> + <source>Registered</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> - <note>reject incoming call via notification</note> + <target>Reddet</target> + <note>reject incoming call via notification +swipe action</note> </trans-unit> - <trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve"> - <source>Reject contact (sender NOT notified)</source> + <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> + <source>Reject (sender NOT notified)</source> + <target>Reddet (göndericiye bildirim GİTMEYECEKTİR)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reject contact request" xml:space="preserve"> <source>Reject contact request</source> + <target>Bağlanma isteğini reddet</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Relay server is only used if necessary. Another party can observe your IP address." xml:space="preserve"> <source>Relay server is only used if necessary. Another party can observe your IP address.</source> + <target>Yönlendirici sunucusu yalnızca gerekli olduğunda kullanılır. Başka bir taraf IP adresinizi gözlemleyebilir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Relay server protects your IP address, but it can observe the duration of the call." xml:space="preserve"> <source>Relay server protects your IP address, but it can observe the duration of the call.</source> + <target>Yönlendirici sunucu IP adresinizi korur, ancak aramanın süresini gözlemleyebilir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Remove" xml:space="preserve"> <source>Remove</source> + <target>Sil</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove archive?" xml:space="preserve"> + <source>Remove archive?</source> + <target>Arşiv kaldırılsın mı ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove image" xml:space="preserve"> + <source>Remove image</source> + <target>Resmi kaldır</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> + <target>Kişiyi sil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Remove member?" xml:space="preserve"> <source>Remove member?</source> + <target>Kişi silinsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Remove passphrase from keychain?" xml:space="preserve"> <source>Remove passphrase from keychain?</source> + <target>Anahtar Zinciri'ndeki parola silinsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Renegotiate" xml:space="preserve"> <source>Renegotiate</source> + <target>Yeniden müzakere</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Renegotiate encryption" xml:space="preserve"> <source>Renegotiate encryption</source> + <target>Şifrelemeyi yeniden müzakere et</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Renegotiate encryption?" xml:space="preserve"> <source>Renegotiate encryption?</source> + <target>Şifreleme yeniden müzakere edilsin mi?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat connection request?" xml:space="preserve"> + <source>Repeat connection request?</source> + <target>Bağlantı isteği tekrarlansın mı?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat download" xml:space="preserve"> + <source>Repeat download</source> + <target>Tekrar indir</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat import" xml:space="preserve"> + <source>Repeat import</source> + <target>İthalatı tekrarla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat join request?" xml:space="preserve"> + <source>Repeat join request?</source> + <target>Katılma isteği tekrarlansın mı?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat upload" xml:space="preserve"> + <source>Repeat upload</source> + <target>Yüklemeyi tekrarla</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reply" xml:space="preserve"> <source>Reply</source> + <target>Yanıtla</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Report" xml:space="preserve"> + <source>Report</source> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Report content: only group moderators will see it." xml:space="preserve"> + <source>Report content: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report member profile: only group moderators will see it." xml:space="preserve"> + <source>Report member profile: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report other: only group moderators will see it." xml:space="preserve"> + <source>Report other: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report reason?" xml:space="preserve"> + <source>Report reason?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Report spam: only group moderators will see it." xml:space="preserve"> + <source>Report spam: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report violation: only group moderators will see it." xml:space="preserve"> + <source>Report violation: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report: %@" xml:space="preserve"> + <source>Report: %@</source> + <note>report in notification</note> + </trans-unit> + <trans-unit id="Reporting messages to moderators is prohibited." xml:space="preserve"> + <source>Reporting messages to moderators is prohibited.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reports" xml:space="preserve"> + <source>Reports</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Required" xml:space="preserve"> <source>Required</source> + <target>Gerekli</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reset" xml:space="preserve"> <source>Reset</source> + <target>Sıfırla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all hints" xml:space="preserve"> + <source>Reset all hints</source> + <target>Tüm ip uçlarını sıfırla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics" xml:space="preserve"> + <source>Reset all statistics</source> + <target>Tüm istatistikleri sıfırla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics?" xml:space="preserve"> + <source>Reset all statistics?</source> + <target>Tüm istatistikler sıfırlansın mı ?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> + <target>Renkleri sıfırla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset to app theme" xml:space="preserve"> + <source>Reset to app theme</source> + <target>Uygulama temasına sıfırla</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> + <target>Varsayılanlara sıfırla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset to user theme" xml:space="preserve"> + <source>Reset to user theme</source> + <target>Kullanıcı temasına sıfırla</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> <source>Restart the app to create a new chat profile</source> + <target>Yeni bir sohbet profili oluşturmak için uygulamayı yeniden başlatın</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restart the app to use imported chat database" xml:space="preserve"> <source>Restart the app to use imported chat database</source> + <target>İçe aktarılmış sohbet veritabanını kullanmak için uygulamayı yeniden başlatın</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restore" xml:space="preserve"> <source>Restore</source> + <target>Geri yükle</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restore database backup" xml:space="preserve"> <source>Restore database backup</source> + <target>Veritabanı yedeğini geri yükle</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restore database backup?" xml:space="preserve"> <source>Restore database backup?</source> + <target>Veritabanı yedeği geri yüklensin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restore database error" xml:space="preserve"> <source>Restore database error</source> + <target>Veritabanını geri yüklerken hata oluştu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Retry" xml:space="preserve"> + <source>Retry</source> + <target>Yeniden dene</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> + <target>Göster</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Revert" xml:space="preserve"> - <source>Revert</source> + <trans-unit id="Review conditions" xml:space="preserve"> + <source>Review conditions</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke" xml:space="preserve"> <source>Revoke</source> + <target>İptal et</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke file" xml:space="preserve"> <source>Revoke file</source> + <target>Dosyayı iptal et</target> <note>cancel file action</note> </trans-unit> <trans-unit id="Revoke file?" xml:space="preserve"> <source>Revoke file?</source> + <target>Dosya iptal edilsin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Role" xml:space="preserve"> <source>Role</source> + <target>Rol</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Run chat" xml:space="preserve"> <source>Run chat</source> + <target>Sohbeti çalıştır</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve"> - <source>SMP servers</source> + <trans-unit id="SMP server" xml:space="preserve"> + <source>SMP server</source> + <target>SMP sunucusu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SOCKS proxy" xml:space="preserve"> + <source>SOCKS proxy</source> + <target>SOCKS vekili</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safely receive files" xml:space="preserve"> + <source>Safely receive files</source> + <target>Dosyaları güvenle alın</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safer groups" xml:space="preserve"> + <source>Safer groups</source> + <target>Daha güvenli gruplar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save" xml:space="preserve"> <source>Save</source> - <note>chat item action</note> + <target>Kaydet</target> + <note>alert button +chat item action</note> </trans-unit> <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> - <note>No comment provided by engineer.</note> + <target>Kaydet (ve kişilere bildir)</target> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> - <note>No comment provided by engineer.</note> + <target>Kaydet ve kişilere bildir</target> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> + <target>Kaydet ve grup üyelerine bildir</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save and reconnect" xml:space="preserve"> + <source>Save and reconnect</source> + <target>Kayıt et ve yeniden bağlan</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save and update group profile" xml:space="preserve"> <source>Save and update group profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save archive" xml:space="preserve"> - <source>Save archive</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve"> - <source>Save auto-accept settings</source> + <target>Kaydet ve grup profilini güncelle</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> <source>Save group profile</source> + <target>Grup profilini kaydet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save list" xml:space="preserve"> + <source>Save list</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> + <target>Parolayı kaydet ve sohbeti aç</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save passphrase in Keychain" xml:space="preserve"> <source>Save passphrase in Keychain</source> + <target>Parolayı Anahtar Zincirinde kaydet</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> - <note>No comment provided by engineer.</note> + <target>Tercihler kaydedilsin mi?</target> + <note>alert title</note> </trans-unit> <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> + <target>Profil şifresini kaydet</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save servers" xml:space="preserve"> <source>Save servers</source> + <target>Sunucuları kaydet</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve"> - <source>Save settings?</source> - <note>No comment provided by engineer.</note> + <target>Sunucular kaydedilsin mi?</target> + <note>alert title</note> </trans-unit> <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> + <target>Hoşgeldin mesajı kaydedilsin mi?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save your profile?" xml:space="preserve"> + <source>Save your profile?</source> + <target>Profiliniz kaydedilsin mi?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Saved" xml:space="preserve"> + <source>Saved</source> + <target>Kaydedildi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> <source>Saved WebRTC ICE servers will be removed</source> + <target>Kaydedilmiş WebRTC ICE sunucuları silinecek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved from" xml:space="preserve"> + <source>Saved from</source> + <target>Tarafından kaydedildi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved message" xml:space="preserve"> + <source>Saved message</source> + <target>Kaydedilmiş mesaj</target> + <note>message info title</note> + </trans-unit> + <trans-unit id="Saving %lld messages" xml:space="preserve"> + <source>Saving %lld messages</source> + <target>%lld mesajlarını kaydet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scale" xml:space="preserve"> + <source>Scale</source> + <target>Ölçeklendir</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan / Paste link" xml:space="preserve"> + <source>Scan / Paste link</source> + <target>Tara / Bağlantı yapıştır</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> + <target>QR kodu okut</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan QR code from desktop" xml:space="preserve"> + <source>Scan QR code from desktop</source> + <target>Bilgisayardan QR kodu okut</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Scan code" xml:space="preserve"> <source>Scan code</source> + <target>Kod okut</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Scan security code from your contact's app." xml:space="preserve"> <source>Scan security code from your contact's app.</source> + <target>Kişinin uygulamasından güvenlik kodunu okut.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Scan server QR code" xml:space="preserve"> <source>Scan server QR code</source> + <target>Sunucu QR kodu okut</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Search" xml:space="preserve"> <source>Search</source> + <target>Ara</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search bar accepts invitation links." xml:space="preserve"> + <source>Search bar accepts invitation links.</source> + <target>Arama çubuğu davet bağlantılarını kabul eder.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search or paste SimpleX link" xml:space="preserve"> + <source>Search or paste SimpleX link</source> + <target>Ara veya SimpleX bağlantısını yapıştır</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secondary" xml:space="preserve"> + <source>Secondary</source> + <target>İkincil renk</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> <source>Secure queue</source> + <target>Sırayı koru</target> <note>server test step</note> </trans-unit> + <trans-unit id="Secured" xml:space="preserve"> + <source>Secured</source> + <target>Güvenli</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> + <target>Güvenlik değerlendirmesi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Security code" xml:space="preserve"> <source>Security code</source> + <target>Güvenlik kodu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Select" xml:space="preserve"> <source>Select</source> + <target>Seç</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Select chat profile" xml:space="preserve"> + <source>Select chat profile</source> + <target>Sohbet profili seç</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected %lld" xml:space="preserve"> + <source>Selected %lld</source> + <target>Seçilen %lld</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Seçilen sohbet tercihleri bu mesajı yasakladı.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct" xml:space="preserve"> <source>Self-destruct</source> + <target>Kendi kendini imha</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct passcode" xml:space="preserve"> <source>Self-destruct passcode</source> + <target>Kendini imha eden şifre</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct passcode changed!" xml:space="preserve"> <source>Self-destruct passcode changed!</source> + <target>Kendini imha eden şifre değiştirildi!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct passcode enabled!" xml:space="preserve"> <source>Self-destruct passcode enabled!</source> + <target>Kendini imha eden şifre etkinleştirildi!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send" xml:space="preserve"> <source>Send</source> + <target>Gönder</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send a live message - it will update for the recipient(s) as you type it" xml:space="preserve"> <source>Send a live message - it will update for the recipient(s) as you type it</source> + <target>Bir canlı mesaj gönder - yazışına göre kişiye(lere) kendini günceller</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send delivery receipts to" xml:space="preserve"> <source>Send delivery receipts to</source> + <target>Görüldü bilgilerini şuraya gönder</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve"> - <source>Send direct message</source> + <trans-unit id="Send direct message to connect" xml:space="preserve"> + <source>Send direct message to connect</source> + <target>Bağlanmak için doğrudan mesaj gönder</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send disappearing message" xml:space="preserve"> <source>Send disappearing message</source> + <target>Kaybolan bir mesaj gönder</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send errors" xml:space="preserve"> + <source>Send errors</source> + <target>Gönderme hataları</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> + <target>Bağlantı ön gösterimleri gönder</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send live message" xml:space="preserve"> <source>Send live message</source> + <target>Canlı mesaj gönder</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send message to enable calls." xml:space="preserve"> + <source>Send message to enable calls.</source> + <target>Çağrıları aktif etmek için mesaj gönder.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source> + <target>IP adresi korumalı olduğunda ve sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when your or destination server does not support private routing.</source> + <target>Sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> + <target>Bildirimler gönder</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> - <source>Send notifications:</source> + <trans-unit id="Send private reports" xml:space="preserve"> + <source>Send private reports</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve"> <source>Send questions and ideas</source> + <target>Fikirler ve sorular gönderin</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send receipts" xml:space="preserve"> <source>Send receipts</source> + <target>Mesajlar gönder</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve"> <source>Send them from gallery or custom keyboards.</source> + <target>Bunları galeriden veya özel klavyelerden gönder.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve"> + <source>Send up to 100 last messages to new members.</source> + <target>Yeni üyelere 100 adete kadar son mesajları gönderin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> - <note>No comment provided by engineer.</note> + <target>Gönderici dosya gönderimini iptal etti.</target> + <note>alert message</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> + <target>Gönderici bağlantı isteğini silmiş olabilir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending delivery receipts will be enabled for all contacts in all visible chat profiles." xml:space="preserve"> + <source>Sending delivery receipts will be enabled for all contacts in all visible chat profiles.</source> + <target>Görüldü bilgisi, tüm görünür sohbet profillerindeki tüm kişiler için etkinleştirilecektir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending delivery receipts will be enabled for all contacts." xml:space="preserve"> + <source>Sending delivery receipts will be enabled for all contacts.</source> + <target>Tüm kişiler için iletim bilgisi gönderme özelliği etkinleştirilecek.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sending file will be stopped." xml:space="preserve"> <source>Sending file will be stopped.</source> + <target>Dosya gönderimi durdurulacaktır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld contacts" xml:space="preserve"> + <source>Sending receipts is disabled for %lld contacts</source> + <target>Görüldü bilgisi %lld kişileri için devre dışı bırakıldı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is disabled for %lld groups</source> + <target>Görüldü bilgisi %lld grupları için devre dışı bırakıldı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve"> + <source>Sending receipts is enabled for %lld contacts</source> + <target>Görüldü bilgisi %lld kişileri için etkinleştirildi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is enabled for %lld groups</source> + <target>Görüldü bilgisi %lld grupları için etkinleştirildi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sending via" xml:space="preserve"> <source>Sending via</source> + <target>Aracılığıyla gönderiliyor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sent at" xml:space="preserve"> <source>Sent at</source> + <target>Şuradan gönderildi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sent at: %@" xml:space="preserve"> <source>Sent at: %@</source> + <target>Şuradan gönderildi: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Sent directly" xml:space="preserve"> + <source>Sent directly</source> + <target>Direkt gönderildi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> + <target>Dosya etkinliği gönderildi</target> <note>notification</note> </trans-unit> <trans-unit id="Sent message" xml:space="preserve"> <source>Sent message</source> + <target>Mesaj gönderildi</target> <note>message info title</note> </trans-unit> + <trans-unit id="Sent messages" xml:space="preserve"> + <source>Sent messages</source> + <target>Gönderilen mesajlar</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> + <target>Gönderilen mesajlar ayarlanan süreden sonra silinecektir.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sent reply" xml:space="preserve"> + <source>Sent reply</source> + <target>Gönderilen cevap</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent total" xml:space="preserve"> + <source>Sent total</source> + <target>Gönderilen tüm mesajların toplamı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent via proxy" xml:space="preserve"> + <source>Sent via proxy</source> + <target>Bir proxy aracılığıyla gönderildi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server" xml:space="preserve"> + <source>Server</source> + <target>Sunucu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server added to operator %@." xml:space="preserve"> + <source>Server added to operator %@.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Server address" xml:space="preserve"> + <source>Server address</source> + <target>Sunucu adresi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings." xml:space="preserve"> + <source>Server address is incompatible with network settings.</source> + <target>Sunucu adresi ağ ayarlarıyla uyumlu değil.</target> + <note>srv error text.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Server address is incompatible with network settings: %@.</source> + <target>Sunucu adresi ağ ayarlarıyla uyumsuz: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server operator changed." xml:space="preserve"> + <source>Server operator changed.</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Server operators" xml:space="preserve"> + <source>Server operators</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server protocol changed." xml:space="preserve"> + <source>Server protocol changed.</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> + <target>Sunucunun sıra oluşturması için yetki gereklidir, şifreyi kontrol edin</target> <note>server test error</note> </trans-unit> <trans-unit id="Server requires authorization to upload, check password" xml:space="preserve"> <source>Server requires authorization to upload, check password</source> + <target>Sunucunun yükleme yapması için yetki gereklidir, şifreyi kontrol edin</target> <note>server test error</note> </trans-unit> <trans-unit id="Server test failed!" xml:space="preserve"> <source>Server test failed!</source> + <target>Sunucu testinde hata oluştu!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server type" xml:space="preserve"> + <source>Server type</source> + <target>Sunucu tipi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server version is incompatible with network settings." xml:space="preserve"> + <source>Server version is incompatible with network settings.</source> + <target>Sunucu sürümü ağ ayarlarıyla uyumlu değil.</target> + <note>srv error text</note> + </trans-unit> + <trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve"> + <source>Server version is incompatible with your app: %@.</source> + <target>Sunucu sürümü uygulamanızla uyumlu değil: %@.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Servers" xml:space="preserve"> <source>Servers</source> + <target>Sunucular</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers info" xml:space="preserve"> + <source>Servers info</source> + <target>Sunucu bilgileri</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <target>Sunucu istatistikleri sıfırlanacaktır - bu geri alınamaz!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Session code" xml:space="preserve"> + <source>Session code</source> + <target>Oturum kodu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set 1 day" xml:space="preserve"> <source>Set 1 day</source> + <target>1 günlüğüne ayarla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set chat name…" xml:space="preserve"> + <source>Set chat name…</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> + <target>Kişi adı gir…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve"> + <source>Set default theme</source> + <target>Varsayılan temaya ayarla</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> + <target>Grup tercihlerini ayarla</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set it instead of system authentication." xml:space="preserve"> <source>Set it instead of system authentication.</source> + <target>Sistem kimlik doğrulaması yerine ayarla.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve"> + <source>Set message expiration in chats.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set passcode" xml:space="preserve"> <source>Set passcode</source> + <target>Şifre ayarla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set passphrase" xml:space="preserve"> + <source>Set passphrase</source> + <target>Parolayı ayarla</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> + <target>Dışa aktarmak için parola ayarla</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set the message shown to new members!" xml:space="preserve"> <source>Set the message shown to new members!</source> + <target>Yeni üyeler için gösterilen bir mesaj ayarla!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set timeouts for proxy/VPN" xml:space="preserve"> <source>Set timeouts for proxy/VPN</source> + <target>Vekil/VPN için zaman aşımları ayarla</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Settings" xml:space="preserve"> <source>Settings</source> + <target>Ayarlar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve"> + <source>Settings were changed.</source> + <target>Ayarlar değiştirildi.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve"> + <source>Shape profile images</source> + <target>Profil resimlerini şekillendir</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share" xml:space="preserve"> <source>Share</source> - <note>chat item action</note> + <target>Paylaş</target> + <note>alert action +chat item action</note> </trans-unit> <trans-unit id="Share 1-time link" xml:space="preserve"> <source>Share 1-time link</source> + <target>Tek kullanımlık bağlantıyı paylaş</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share 1-time link with a friend" xml:space="preserve"> + <source>Share 1-time link with a friend</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve"> + <source>Share SimpleX address on social media.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share address" xml:space="preserve"> <source>Share address</source> + <target>Adresi paylaş</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share address publicly" xml:space="preserve"> + <source>Share address publicly</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share address with contacts?" xml:space="preserve"> <source>Share address with contacts?</source> + <target>Kişilerle adres paylaşılsın mı?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Share from other apps." xml:space="preserve"> + <source>Share from other apps.</source> + <target>Diğer uygulamalardan paylaşın.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share link" xml:space="preserve"> <source>Share link</source> + <target>Bağlantıyı paylaş</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve"> - <source>Share one-time invitation link</source> + <trans-unit id="Share profile" xml:space="preserve"> + <source>Share profile</source> + <target>Profil paylaş</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share this 1-time invite link" xml:space="preserve"> + <source>Share this 1-time invite link</source> + <target>Bu tek kullanımlık bağlantı davetini paylaş</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share to SimpleX" xml:space="preserve"> + <source>Share to SimpleX</source> + <target>SimpleX ile paylaş</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share with contacts" xml:space="preserve"> <source>Share with contacts</source> + <target>Kişilerle paylaş</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Short link" xml:space="preserve"> + <source>Short link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show QR code" xml:space="preserve"> + <source>Show QR code</source> + <target>QR kodunu göster</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> + <target>Telefon geçmişinde aramaları göster</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Show developer options" xml:space="preserve"> <source>Show developer options</source> + <target>Geliştirici ayarlarını göster</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show last messages" xml:space="preserve"> + <source>Show last messages</source> + <target>Son mesajları göster</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show message status" xml:space="preserve"> + <source>Show message status</source> + <target>Mesaj durumunu göster</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show percentage" xml:space="preserve"> + <source>Show percentage</source> + <target>Yüzdeyi göster</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> + <target>Ön gösterimi göser</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show → on messages sent via private routing." xml:space="preserve"> + <source>Show → on messages sent via private routing.</source> + <target>Gizli yönlendirme yoluyla gönderilen mesajlarda → işaretini göster.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Show:" xml:space="preserve"> <source>Show:</source> + <target>Göster:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve"> + <source>SimpleX</source> + <target>SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX Address" xml:space="preserve"> <source>SimpleX Address</source> + <target>SimpleX Adresi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." xml:space="preserve"> + <source>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> + <target>SimpleX Chat güvenliği Trails of Bits tarafından denetlenmiştir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX Lock" xml:space="preserve"> <source>SimpleX Lock</source> + <target>SimpleX Kilidi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX Lock mode" xml:space="preserve"> <source>SimpleX Lock mode</source> + <target>SimpleX Kilidi modu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX Lock not enabled!" xml:space="preserve"> <source>SimpleX Lock not enabled!</source> + <target>SimpleX Kilidi etkinleştirilmedi!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX Lock turned on" xml:space="preserve"> <source>SimpleX Lock turned on</source> + <target>SimpleX Kilidi açıldı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX address" xml:space="preserve"> <source>SimpleX address</source> + <target>SimpleX adresi</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX address and 1-time links are safe to share via any messenger." xml:space="preserve"> + <source>SimpleX address and 1-time links are safe to share via any messenger.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address or 1-time link?" xml:space="preserve"> + <source>SimpleX address or 1-time link?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX channel link" xml:space="preserve"> + <source>SimpleX channel link</source> + <note>simplex link type</note> + </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> + <target>SimpleX kişi adresi</target> <note>simplex link type</note> </trans-unit> <trans-unit id="SimpleX encrypted message or connection event" xml:space="preserve"> <source>SimpleX encrypted message or connection event</source> + <target>SimpleX şifrelenmiş mesaj veya bağlantı etkinliği</target> <note>notification</note> </trans-unit> <trans-unit id="SimpleX group link" xml:space="preserve"> <source>SimpleX group link</source> + <target>SimpleX grup bağlantısı</target> <note>simplex link type</note> </trans-unit> <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> + <target>SimpleX bağlantıları</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve"> + <source>SimpleX links are prohibited.</source> + <target>SimpleX bağlantıları bu grupta yasaklandı.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve"> + <source>SimpleX links not allowed</source> + <target>SimpleX bağlantılarına izin verilmiyor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> <source>SimpleX one-time invitation</source> + <target>SimpleX tek kullanımlık davet</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> + <source>SimpleX protocols reviewed by Trail of Bits.</source> + <target>SimpleX protokolleri Trail of Bits tarafından incelenmiştir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Simplified incognito mode" xml:space="preserve"> + <source>Simplified incognito mode</source> + <target>Basitleştirilmiş gizli mod</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Size" xml:space="preserve"> + <source>Size</source> + <target>Boyut</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> + <target>Atla</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Skipped messages" xml:space="preserve"> <source>Skipped messages</source> + <target>Atlanmış mesajlar</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Small groups (max 10)" xml:space="preserve"> - <source>Small groups (max 10)</source> + <trans-unit id="Small groups (max 20)" xml:space="preserve"> + <source>Small groups (max 20)</source> + <target>Küçük gruplar (en fazla 20 kişi)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Soft" xml:space="preserve"> + <source>Soft</source> + <target>Yumuşak</target> + <note>blur media</note> + </trans-unit> + <trans-unit id="Some app settings were not migrated." xml:space="preserve"> + <source>Some app settings were not migrated.</source> + <target>Bazı uygulama ayarları taşınamadı.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some file(s) were not exported:" xml:space="preserve"> + <source>Some file(s) were not exported:</source> + <target>Bazı dosya(lar) dışa aktarılmadı:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> + <target>İçe aktarma sırasında bazı ölümcül olmayan hatalar oluştu - daha fazla ayrıntı için Sohbet konsoluna bakabilirsiniz.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import:" xml:space="preserve"> + <source>Some non-fatal errors occurred during import:</source> + <target>İçe aktarma sırasında bazı önemli olmayan hatalar oluştu:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some servers failed the test: %@" xml:space="preserve"> + <source>Some servers failed the test: +%@</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> + <target>Biri</target> <note>notification title</note> </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve"> - <source>Start a new chat</source> + <trans-unit id="Spam" xml:space="preserve"> + <source>Spam</source> + <note>blocking reason +report reason</note> + </trans-unit> + <trans-unit id="Square, circle, or anything in between." xml:space="preserve"> + <source>Square, circle, or anything in between.</source> + <target>Kare,daire, veya aralarında herhangi bir şey.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start chat" xml:space="preserve"> <source>Start chat</source> + <target>Sohbeti başlat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Start chat?" xml:space="preserve"> + <source>Start chat?</source> + <target>Sohbet başlatılsın mı?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> + <target>Geçişi başlat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Starting from %@." xml:space="preserve"> + <source>Starting from %@.</source> + <target>%@'dan başlayarak.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Statistics" xml:space="preserve"> + <source>Statistics</source> + <target>İstatistikler</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> + <target>Dur</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop SimpleX" xml:space="preserve"> <source>Stop SimpleX</source> + <target>SimpleX'i durdur</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> - <source>Stop chat to enable database actions</source> + <trans-unit id="Stop chat" xml:space="preserve"> + <source>Stop chat</source> + <target>Sohbeti kes</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> <source>Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped.</source> + <target>Sohbet veritabanını dışa aktarmak, içe aktarmak veya silmek için sohbeti durdurun. Sohbet durdurulduğunda mesaj alamaz ve gönderemezsiniz.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat?" xml:space="preserve"> <source>Stop chat?</source> + <target>Sohbet durdurulsun mu?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop file" xml:space="preserve"> <source>Stop file</source> + <target>Dosyayı durdur</target> <note>cancel file action</note> </trans-unit> <trans-unit id="Stop receiving file?" xml:space="preserve"> <source>Stop receiving file?</source> + <target>Dosya alımı durdurulsun mu?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop sending file?" xml:space="preserve"> <source>Stop sending file?</source> + <target>Dosya gönderimi durdurulsun mu?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop sharing" xml:space="preserve"> <source>Stop sharing</source> - <note>No comment provided by engineer.</note> + <target>Paylaşmayı durdur</target> + <note>alert action</note> </trans-unit> <trans-unit id="Stop sharing address?" xml:space="preserve"> <source>Stop sharing address?</source> + <target>Adresi paylaşmak durdurulsun mu?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Stopping chat" xml:space="preserve"> + <source>Stopping chat</source> + <target>Sohbeti durdurma</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Storage" xml:space="preserve"> + <source>Storage</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Strong" xml:space="preserve"> + <source>Strong</source> + <target>Güçlü</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Submit" xml:space="preserve"> <source>Submit</source> + <target>Gönder</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscribed" xml:space="preserve"> + <source>Subscribed</source> + <target>Abone olundu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscription errors" xml:space="preserve"> + <source>Subscription errors</source> + <target>Abone olurken hata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscriptions ignored" xml:space="preserve"> + <source>Subscriptions ignored</source> + <target>Abonelikler göz ardı edildi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Support SimpleX Chat" xml:space="preserve"> <source>Support SimpleX Chat</source> + <target>SimpleX Chat'e destek ol</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch audio and video during the call." xml:space="preserve"> + <source>Switch audio and video during the call.</source> + <target>Görüşme sırasında ses ve görüntüyü değiştirin.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> + <source>Switch chat profile for 1-time invitations.</source> + <target>Sohbet profilini 1 kerelik davetler için değiştirin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="System" xml:space="preserve"> <source>System</source> + <target>Sistem</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="System authentication" xml:space="preserve"> <source>System authentication</source> + <target>Sistem yetkilendirilmesi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="TCP connection" xml:space="preserve"> + <source>TCP connection</source> + <target>TCP bağlantısı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> + <target>TCP bağlantı zaman aşımı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="TCP port for messaging" xml:space="preserve"> + <source>TCP port for messaging</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <source>TCP_KEEPCNT</source> + <target>TCP_KEEPCNT</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="TCP_KEEPIDLE" xml:space="preserve"> <source>TCP_KEEPIDLE</source> + <target>TCP_BOŞTAKAL</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="TCP_KEEPINTVL" xml:space="preserve"> <source>TCP_KEEPINTVL</source> + <target>TCP_TVLDEKAL</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tail" xml:space="preserve"> + <source>Tail</source> + <target>Konuşma balonu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> + <target>Fotoğraf çek</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tap Create SimpleX address in the menu to create it later." xml:space="preserve"> + <source>Tap Create SimpleX address in the menu to create it later.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> + <target>Tuşa bas </target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tap to Connect" xml:space="preserve"> + <source>Tap to Connect</source> + <target>Bağlanmak için Tıkla</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to activate profile." xml:space="preserve"> <source>Tap to activate profile.</source> + <target>Profili etkinleştirmek için tıkla.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to join" xml:space="preserve"> <source>Tap to join</source> + <target>Katılmak için tıkla</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to join incognito" xml:space="preserve"> <source>Tap to join incognito</source> + <target>Gizli katılmak için tıkla</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve"> - <source>Tap to start a new chat</source> + <trans-unit id="Tap to paste link" xml:space="preserve"> + <source>Tap to paste link</source> + <target>Bağlantıyı yapıştırmak için tıkla</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap to scan" xml:space="preserve"> + <source>Tap to scan</source> + <target>Taramak için tıkla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Temporary file error" xml:space="preserve"> + <source>Temporary file error</source> + <target>Geçici dosya hatası</target> + <note>file error alert title</note> + </trans-unit> <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> + <target>Test %@ adımında başarısız oldu.</target> <note>server test failure</note> </trans-unit> + <trans-unit id="Test notifications" xml:space="preserve"> + <source>Test notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> + <target>Sunucuyu test et</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Test servers" xml:space="preserve"> <source>Test servers</source> + <target>Sunucuları test et</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> - <note>No comment provided by engineer.</note> + <target>Testler başarısız oldu!</target> + <note>alert title</note> </trans-unit> <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> + <target>SimpleX Chat'i indirdiğin için teşekkürler!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> <source>Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target>Kullanıcılar için teşekkürler - [Weblate aracılığıyla katkıda bulun](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Thanks to the users – contribute via Weblate!" xml:space="preserve"> <source>Thanks to the users – contribute via Weblate!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> + <target>Kullanıcılar için teşekkürler - Weblate aracılığıyla katkıda bulun!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <source>The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised.</source> + <target>Bir sonraki mesajın kimliği yanlış (bir öncekinden az veya aynı). +Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve"> <source>The app can notify you when you receive messages or contact requests - please open settings to enable.</source> + <target>Uygulama, mesaj veya iletişim isteği aldığınızda sizi bilgilendirebilir - etkinleştirmek için lütfen ayarları açın.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app protects your privacy by using different operators in each conversation." xml:space="preserve"> + <source>The app protects your privacy by using different operators in each conversation.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve"> + <source>The app will ask to confirm downloads from unknown file servers (except .onion).</source> + <target>Uygulama bilinmeyen dosya sunucularından indirmeleri onaylamanızı isteyecektir (.onion hariç).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> <source>The attempt to change database passphrase was not completed.</source> + <target>Veritabanı parolasını değiştirme girişimi tamamlanmadı.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve"> + <source>The code you scanned is not a SimpleX link QR code.</source> + <target>Taradığınız kod bir SimpleX bağlantı QR kodu değildir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The connection reached the limit of undelivered messages, your contact may be offline." xml:space="preserve"> + <source>The connection reached the limit of undelivered messages, your contact may be offline.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> + <target>Bağlantı kabulünüz iptal edilecektir!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The contact you shared this link with will NOT be able to connect!" xml:space="preserve"> <source>The contact you shared this link with will NOT be able to connect!</source> + <target>Bu bağlantıyı paylaştığınız kişi BAĞLANAMAYACAKTIR!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The created archive is available via app Settings / Database / Old database archive." xml:space="preserve"> <source>The created archive is available via app Settings / Database / Old database archive.</source> + <target>Oluşturulan arşive uygulama üzerinden Ayarlar / Veritabanı / Eski veritabanı arşivi üzerinden erişilebilir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The encryption is working and the new encryption agreement is not required. It may result in connection errors!" xml:space="preserve"> <source>The encryption is working and the new encryption agreement is not required. It may result in connection errors!</source> + <target>Şifreleme çalışıyor ve yeni şifreleme anlaşması gerekli değil. Bağlantı hatalarına neden olabilir!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The group is fully decentralized – it is visible only to the members." xml:space="preserve"> - <source>The group is fully decentralized – it is visible only to the members.</source> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> + <target>Gizli mesajlaşmanın yeni nesli</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The hash of the previous message is different." xml:space="preserve"> <source>The hash of the previous message is different.</source> + <target>Önceki mesajın hash'i farklı.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The message will be deleted for all members." xml:space="preserve"> <source>The message will be deleted for all members.</source> + <target>Mesaj tüm üyeler için silinecektir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The message will be marked as moderated for all members." xml:space="preserve"> <source>The message will be marked as moderated for all members.</source> + <target>Mesaj tüm üyeler için yönetilmiş olarak işaretlenecektir.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> + <trans-unit id="The messages will be deleted for all members." xml:space="preserve"> + <source>The messages will be deleted for all members.</source> + <target>Mesajlar tüm üyeler için silinecektir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve"> + <source>The messages will be marked as moderated for all members.</source> + <target>Mesajlar tüm üyeler için moderasyonlu olarak işaretlenecektir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> <source>The old database was not removed during the migration, it can be deleted.</source> + <target>Eski veritabanı geçiş sırasında kaldırılmadı, silinebilir.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> + <trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve"> + <source>The same conditions will apply to operator **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second preset operator in the app!" xml:space="preserve"> + <source>The second preset operator in the app!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> + <source>The second tick we missed! ✅</source> + <target>Özlediğimiz ikinci tik! ✅</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The sender will NOT be notified" xml:space="preserve"> <source>The sender will NOT be notified</source> + <target>Gönderene BİLDİRİLMEYECEKTİR</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The servers for new connections of your current chat profile **%@**." xml:space="preserve"> <source>The servers for new connections of your current chat profile **%@**.</source> + <target>Mevcut sohbet profilinizin yeni bağlantıları için sunucular **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Theme" xml:space="preserve"> - <source>Theme</source> + <trans-unit id="The servers for new files of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new files of your current chat profile **%@**.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve"> - <source>There should be at least one user profile.</source> + <trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve"> + <source>The text you pasted is not a SimpleX link.</source> + <target>Yapıştırdığın metin bir SimpleX bağlantısı değildir.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> - <source>There should be at least one visible user profile.</source> + <trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve"> + <source>The uploaded database archive will be permanently removed from the servers.</source> + <target>Yüklenen veritabanı arşivi sunuculardan kalıcı olarak kaldırılacaktır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Themes" xml:space="preserve"> + <source>Themes</source> + <target>Temalar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="These conditions will also apply for: **%@**." xml:space="preserve"> + <source>These conditions will also apply for: **%@**.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> <source>These settings are for your current profile **%@**.</source> + <target>Bu ayarlar mevcut profiliniz **%@** içindir.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="They can be overridden in contact and group settings" xml:space="preserve"> - <source>They can be overridden in contact and group settings</source> + <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve"> + <source>They can be overridden in contact and group settings.</source> + <target>Bunlar kişi ve grup ayarlarında geçersiz kılınabilir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> <source>This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</source> + <target>Bu işlem geri alınamaz - alınan ve gönderilen tüm dosyalar ve medya silinecektir. Düşük çözünürlüklü resimler kalacaktır.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." xml:space="preserve"> <source>This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</source> + <target>Bu işlem geri alınamaz - seçilenden daha önce gönderilen ve alınan mesajlar silinecektir. Bu işlem birkaç dakika sürebilir.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> + <target>Bu işlem geri alınamaz - profiliniz, kişileriniz, mesajlarınız ve dosyalarınız geri döndürülemez şekilde kaybolacaktır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by end-to-end encryption.</source> + <target>Bu sohbet uçtan uca şifreleme ile korunmaktadır.</target> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by quantum resistant end-to-end encryption.</source> + <target>Bu sohbet kuantum dirençli uçtan uca şifreleme ile korunmaktadır.</target> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This device name" xml:space="preserve"> + <source>This device name</source> + <target>Bu cihazın ismi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve"> + <source>This display name is invalid. Please choose another name.</source> + <target>Bu görünen ad geçersiz. Lütfen başka bir isim seçin.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> + <source>This group has over %lld members, delivery receipts are not sent.</source> + <target>Bu grubun %lld den fazla üyesi var,görüldü bilgisi gönderilmedi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This group no longer exists." xml:space="preserve"> <source>This group no longer exists.</source> + <target>Bu grup artık mevcut değildir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This is your own SimpleX address!" xml:space="preserve"> + <source>This is your own SimpleX address!</source> + <target>Bu senin kendi SimpleX adresin!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This is your own one-time link!" xml:space="preserve"> + <source>This is your own one-time link!</source> + <target>Bu senin kendi tek kullanımlık bağlantın!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve"> + <source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve"> + <source>This link was used with another mobile device, please create a new link on the desktop.</source> + <target>Bu bağlantı başka bir mobil cihazda kullanıldı, lütfen masaüstünde yeni bir bağlantı oluşturun.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This message was deleted or not received yet." xml:space="preserve"> + <source>This message was deleted or not received yet.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <source>This setting applies to messages in your current chat profile **%@**.</source> + <target>Bu ayar, geçerli sohbet profiliniz **%@** deki mesajlara uygulanır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Title" xml:space="preserve"> + <source>Title</source> + <target>Başlık</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> <source>To ask any questions and to receive updates:</source> + <target>Soru sormak ve güncellemeleri almak için:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To connect, your contact can scan QR code or use the link in the app." xml:space="preserve"> <source>To connect, your contact can scan QR code or use the link in the app.</source> + <target>Bağlanmak için, kişi QR kodu okutabilir veya uygulama içinden bağlantıyı kullanabilir.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve"> - <source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source> + <trans-unit id="To hide unwanted messages." xml:space="preserve"> + <source>To hide unwanted messages.</source> + <target>İstenmeyen mesajları gizlemek için.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To make a new connection" xml:space="preserve"> <source>To make a new connection</source> + <target>Yeni bir bağlantı oluşturmak için</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> + <trans-unit id="To protect against your link being replaced, you can compare contact security codes." xml:space="preserve"> + <source>To protect against your link being replaced, you can compare contact security codes.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> <source>To protect timezone, image/voice files use UTC.</source> + <target>Zaman bölgesini korumak için,fotoğraf/ses dosyaları UTC kullanır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve"> + <source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source> + <target>IP adresinizi korumak için,gizli yönlendirme mesajları iletmek için SMP sunucularınızı kullanır.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> + <target>Bilgilerinizi korumak için SimpleX Lock özelliğini açın. +Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenecektir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> + <target>Gizliliği korumak için, diğer tüm platformlar gibi kullanıcı kimliği kullanmak yerine, SimpleX mesaj kuyrukları için kişilerinizin her biri için ayrı tanımlayıcılara sahiptir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To receive" xml:space="preserve"> + <source>To receive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve"> + <source>To record speech please grant permission to use Microphone.</source> + <target>Konuşmayı kaydetmek için lütfen Mikrofon kullanma izni verin.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record video please grant permission to use Camera." xml:space="preserve"> + <source>To record video please grant permission to use Camera.</source> + <target>Video kaydetmek için lütfen Kamera kullanım izni verin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> + <target>Sesli mesaj kaydetmek için lütfen Mikrofon kullanım izni verin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." xml:space="preserve"> <source>To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page.</source> + <target>Gizli profilinizi ortaya çıkarmak için **Sohbet profilleriniz** sayfasındaki arama alanına tam bir şifre girin.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To send" xml:space="preserve"> + <source>To send</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> <source>To support instant push notifications the chat database has to be migrated.</source> + <target>Anlık anlık bildirimleri desteklemek için sohbet veritabanının taşınması gerekir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To use the servers of **%@**, accept conditions of use." xml:space="preserve"> + <source>To use the servers of **%@**, accept conditions of use.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> + <target>Kişinizle uçtan uca şifrelemeyi doğrulamak için cihazlarınızdaki kodu karşılaştırın (veya tarayın).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Toggle chat list:" xml:space="preserve"> + <source>Toggle chat list:</source> + <target>Sohbet listesini değiştir:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> + <source>Toggle incognito when connecting.</source> + <target>Bağlanırken gizli moda geçiş yap.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Token status: %@." xml:space="preserve"> + <source>Token status: %@.</source> + <note>token status</note> + </trans-unit> + <trans-unit id="Toolbar opacity" xml:space="preserve"> + <source>Toolbar opacity</source> + <target>Araç çubuğu opaklığı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Total" xml:space="preserve"> + <source>Total</source> + <target>Toplam</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> + <target>Taşıma izolasyonu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Transport sessions" xml:space="preserve"> + <source>Transport sessions</source> + <target>Taşıma oturumları</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> + <target>Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor (hata: %@).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact.</source> + <target>Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Turkish interface" xml:space="preserve"> + <source>Turkish interface</source> + <target>Türkçe arayüz</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve"> - <source>Turn off notifications?</source> + <target>Kapat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> + <target>Aç</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unable to record voice message" xml:space="preserve"> <source>Unable to record voice message</source> + <target>Sesli mesaj kaydedilemedi</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve"> - <source>Unexpected error: %@</source> + <trans-unit id="Unblock" xml:space="preserve"> + <source>Unblock</source> + <target>Engeli kaldır</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unblock for all" xml:space="preserve"> + <source>Unblock for all</source> + <target>Herkes için engeli kaldır</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unblock member" xml:space="preserve"> + <source>Unblock member</source> + <target>Üyenin engelini kaldır</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unblock member for all?" xml:space="preserve"> + <source>Unblock member for all?</source> + <target>Üyenin engeli herkes için kaldırılsın mı?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unblock member?" xml:space="preserve"> + <source>Unblock member?</source> + <target>Üyenin engeli kaldırılsın mı?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Undelivered messages" xml:space="preserve"> + <source>Undelivered messages</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> + <target>Beklenmeyen geçiş durumu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unfav." xml:space="preserve"> <source>Unfav.</source> - <note>No comment provided by engineer.</note> + <target>Favorilerden çık.</target> + <note>swipe action</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> + <target>Gizlemeyi kaldır</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unhide chat profile" xml:space="preserve"> <source>Unhide chat profile</source> + <target>Sohbet profilinin gizlemesini kaldır</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unhide profile" xml:space="preserve"> <source>Unhide profile</source> + <target>Proflin gizlenmesini kaldır</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unit" xml:space="preserve"> <source>Unit</source> + <target>Birim</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unknown caller" xml:space="preserve"> <source>Unknown caller</source> + <target>Bilinmeyen arayan</target> <note>callkit banner</note> </trans-unit> <trans-unit id="Unknown database error: %@" xml:space="preserve"> <source>Unknown database error: %@</source> + <target>Bilinmeyen veritabanı hatası: %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unknown error" xml:space="preserve"> <source>Unknown error</source> + <target>Bilinmeyen hata</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unknown servers!" xml:space="preserve"> + <source>Unknown servers!</source> + <target>Bilinmeyen sunucular!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> + <target>iOS arama arayüzünü kullanmadığınız sürece, kesintileri önlemek için Rahatsız Etmeyin modunu etkinleştirin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection." xml:space="preserve"> <source>Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection.</source> + <target>Kişiniz bağlantıyı silmediyse veya bu bağlantı kullanılmadıysa, bu bir hata olabilir - lütfen bildirin. +Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını isteyin ve sabit bir ağ bağlantınız olduğunu kontrol edin.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unlink" xml:space="preserve"> + <source>Unlink</source> + <target>Bağlantıyı Kaldır</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unlink desktop?" xml:space="preserve"> + <source>Unlink desktop?</source> + <target>Bilgisayarla bağlantı kaldırılsın mı?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unlock" xml:space="preserve"> <source>Unlock</source> + <target>Kilidi aç</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unlock app" xml:space="preserve"> <source>Unlock app</source> + <target>Uygulama kilidini aç</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> - <note>No comment provided by engineer.</note> + <target>Susturmayı kaldır</target> + <note>notification label action</note> </trans-unit> <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> + <target>Okunmamış</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Unsupported connection link" xml:space="preserve"> + <source>Unsupported connection link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve"> + <source>Up to 100 last messages are sent to new members.</source> + <target>Yeni üyelere 100e kadar en son mesajlar gönderildi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update" xml:space="preserve"> <source>Update</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> - <source>Update .onion hosts setting?</source> + <target>Güncelle</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> + <target>Veritabanı parolasını güncelle</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update network settings?" xml:space="preserve"> <source>Update network settings?</source> + <target>Bağlantı ayarları güncellensin mi?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve"> - <source>Update transport isolation mode?</source> + <trans-unit id="Update settings?" xml:space="preserve"> + <source>Update settings?</source> + <target>Ayarları güncelleyelim mi?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updated conditions" xml:space="preserve"> + <source>Updated conditions</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> <source>Updating settings will re-connect the client to all servers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> + <target>Ayarların güncellenmesi, istemciyi tüm sunuculara yeniden bağlayacaktır.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Upgrade and open chat" xml:space="preserve"> <source>Upgrade and open chat</source> + <target>Yükselt ve sohbeti aç</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload errors" xml:space="preserve"> + <source>Upload errors</source> + <target>Yükleme hataları</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload failed" xml:space="preserve"> + <source>Upload failed</source> + <target>Yükleme başarısız</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Upload file" xml:space="preserve"> <source>Upload file</source> + <target>Dosya yükle</target> <note>server test step</note> </trans-unit> + <trans-unit id="Uploaded" xml:space="preserve"> + <source>Uploaded</source> + <target>Yüklendi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploaded files" xml:space="preserve"> + <source>Uploaded files</source> + <target>Yüklenen dosyalar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploading archive" xml:space="preserve"> + <source>Uploading archive</source> + <target>Arşiv yükleme</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use %@" xml:space="preserve"> + <source>Use %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> + <target>.onion ana bilgisayarlarını kullan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use SOCKS proxy" xml:space="preserve"> + <source>Use SOCKS proxy</source> + <target>SOCKS vekili kullan</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> + <target>SimpleX Chat sunucuları kullanılsın mı?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port %@ when no port is specified." xml:space="preserve"> + <source>Use TCP port %@ when no port is specified.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve"> + <source>Use TCP port 443 for preset servers only.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> + <target>Sohbeti kullan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use current profile" xml:space="preserve"> + <source>Use current profile</source> + <target>Şu anki profili kullan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for files" xml:space="preserve"> + <source>Use for files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for messages" xml:space="preserve"> + <source>Use for messages</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> + <target>Yeni bağlantılar için kullan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use from desktop" xml:space="preserve"> + <source>Use from desktop</source> + <target>Bilgisayardan kullan</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use iOS call interface" xml:space="preserve"> <source>Use iOS call interface</source> + <target>iOS arama arayüzünden kullan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use new incognito profile" xml:space="preserve"> + <source>Use new incognito profile</source> + <target>Yeni gizli profilden kullan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use only local notifications?" xml:space="preserve"> + <source>Use only local notifications?</source> + <target>Sadece yerel bildirimler kullanılsın mı?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve"> + <source>Use private routing with unknown servers when IP address is not protected.</source> + <target>IP adresi korunmadığında bilinmeyen sunucularla gizli yönlendirme kullan.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers." xml:space="preserve"> + <source>Use private routing with unknown servers.</source> + <target>Bilinmeyen sunucularla gizli yönlendirme kullan.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> + <target>Sunucu kullan</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> + <trans-unit id="Use servers" xml:space="preserve"> + <source>Use servers</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> + <trans-unit id="Use short links (BETA)" xml:space="preserve"> + <source>Use short links (BETA)</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app while in the call." xml:space="preserve"> + <source>Use the app while in the call.</source> + <target>Görüşme sırasında uygulamayı kullanın.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app with one hand." xml:space="preserve"> + <source>Use the app with one hand.</source> + <target>Uygulamayı tek elle kullan.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use web port" xml:space="preserve"> + <source>Use web port</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="User selection" xml:space="preserve"> + <source>User selection</source> + <target>Kullanıcı seçimi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Username" xml:space="preserve"> + <source>Username</source> + <target>Kullanıcı Adı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> <source>Using SimpleX Chat servers.</source> + <target>SimpleX Chat sunucuları kullanılıyor.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify code with desktop" xml:space="preserve"> + <source>Verify code with desktop</source> + <target>Bilgisayarla kodu doğrula</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify connection" xml:space="preserve"> + <source>Verify connection</source> + <target>Bağlantıyı doğrula</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify connection security" xml:space="preserve"> <source>Verify connection security</source> + <target>Bağlantı güvenliğini doğrula</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify connections" xml:space="preserve"> + <source>Verify connections</source> + <target>Bağlantıları doğrula</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify database passphrase" xml:space="preserve"> + <source>Verify database passphrase</source> + <target>Veritabanı parolasını doğrulayın</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify passphrase" xml:space="preserve"> + <source>Verify passphrase</source> + <target>Parolayı doğrula</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify security code" xml:space="preserve"> <source>Verify security code</source> + <target>Güvenlik kodunu doğrula</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Via browser" xml:space="preserve"> <source>Via browser</source> + <target>Tarayıcı üzerinden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Via secure quantum resistant protocol." xml:space="preserve"> + <source>Via secure quantum resistant protocol.</source> + <target>Güvenli kuantum dirençli protokol ile.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Video call" xml:space="preserve"> <source>Video call</source> + <target>Görüntülü arama</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Video will be received when your contact completes uploading it." xml:space="preserve"> <source>Video will be received when your contact completes uploading it.</source> + <target>Kişiniz yüklemeyi tamamladığında video alınacaktır.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Video will be received when your contact is online, please wait or check later!" xml:space="preserve"> <source>Video will be received when your contact is online, please wait or check later!</source> + <target>Kişiniz çevrimiçi olduğunda video alınacaktır, lütfen bekleyin veya daha sonra kontrol edin!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Videos and files up to 1gb" xml:space="preserve"> <source>Videos and files up to 1gb</source> + <target>1gb'a kadar videolar ve dosyalar</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="View conditions" xml:space="preserve"> + <source>View conditions</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> + <target>Güvenlik kodunu görüntüle</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View updated conditions" xml:space="preserve"> + <source>View updated conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Visible history" xml:space="preserve"> + <source>Visible history</source> + <target>Görünür geçmiş</target> + <note>chat feature</note> + </trans-unit> <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> + <target>Sesli mesajlar</target> <note>chat feature</note> </trans-unit> <trans-unit id="Voice messages are prohibited in this chat." xml:space="preserve"> <source>Voice messages are prohibited in this chat.</source> + <target>Bu sohbette sesli mesajlar yasaktır.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> + <target>Bu grupta sesli mesajlar yasaktır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Voice messages not allowed" xml:space="preserve"> + <source>Voice messages not allowed</source> + <target>Sesli mesajlara izin verilmiyor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> <source>Voice messages prohibited!</source> + <target>Sesli mesajlar yasaktır!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice message…" xml:space="preserve"> <source>Voice message…</source> + <target>Sesli mesaj…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Waiting for desktop..." xml:space="preserve"> + <source>Waiting for desktop...</source> + <target>Bilgisayar için bekleniyor...</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Waiting for file" xml:space="preserve"> <source>Waiting for file</source> + <target>Dosya bekleniyor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Waiting for image" xml:space="preserve"> <source>Waiting for image</source> + <target>Görsel bekleniyor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Waiting for video" xml:space="preserve"> <source>Waiting for video</source> + <target>Video bekleniyor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper accent" xml:space="preserve"> + <source>Wallpaper accent</source> + <target>Duvar kağıdı vurgusu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper background" xml:space="preserve"> + <source>Wallpaper background</source> + <target>Duvar kağıdı arkaplanı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve"> + <source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source> + <target>Uyarı: birden fazla cihazda sohbet başlatmak desteklenmez ve mesaj teslim hatalarına neden olur</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> <source>Warning: you may lose some data!</source> + <target>Uyarı: Bazı verileri kaybedebilirsin!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="WebRTC ICE servers" xml:space="preserve"> <source>WebRTC ICE servers</source> + <target>WebRTC ICE sunucuları</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Welcome %@!" xml:space="preserve"> <source>Welcome %@!</source> + <target>Hoşgeldin %@!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Welcome message" xml:space="preserve"> <source>Welcome message</source> + <target>Karşılama mesajı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Welcome message is too long" xml:space="preserve"> + <source>Welcome message is too long</source> + <target>Hoş geldiniz mesajı çok uzun</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="What's new" xml:space="preserve"> <source>What's new</source> + <target>Neler yeni</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When available" xml:space="preserve"> <source>When available</source> + <target>Mevcut olduğunda</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> - <source>When people request to connect, you can accept or reject it.</source> + <trans-unit id="When connecting audio and video calls." xml:space="preserve"> + <source>When connecting audio and video calls.</source> + <target>Sesli ve görüntülü aramalara bağlanırken.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When more than one operator is enabled, none of them has metadata to learn who communicates with whom." xml:space="preserve"> + <source>When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> <source>When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</source> + <target>Biriyle gizli bir profil paylaştığınızda, bu profil sizi davet ettikleri gruplar için kullanılacaktır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="WiFi" xml:space="preserve"> + <source>WiFi</source> + <target>WiFi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Will be enabled in direct chats!" xml:space="preserve"> + <source>Will be enabled in direct chats!</source> + <target>Doğrudan sohbetlerde etkinleştirilecektir!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wired ethernet" xml:space="preserve"> + <source>Wired ethernet</source> + <target>Kablolu ethernet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With encrypted files and media." xml:space="preserve"> + <source>With encrypted files and media.</source> + <target>Şifrelenmiş dosyalar ve medya ile birlikte.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> + <target>İsteğe bağlı karşılama mesajı ile.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="With reduced battery usage." xml:space="preserve"> + <source>With reduced battery usage.</source> + <target>Azaltılmış pil kullanımı ile birlikte.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to file servers.</source> + <target>Tor veya VPN olmadan, IP adresiniz dosya sunucularına görülebilir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source> + <target>Tor veya VPN olmadan, IP adresiniz bu XFTP aktarıcıları tarafından görülebilir: %@.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> + <target>Yanlış veritabanı parolası</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve"> + <source>Wrong key or unknown connection - most likely this connection is deleted.</source> + <target>Yanlış anahtar veya bilinmeyen bağlantı - büyük olasılıkla bu bağlantı silinmiştir.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Wrong key or unknown file chunk address - most likely file is deleted." xml:space="preserve"> + <source>Wrong key or unknown file chunk address - most likely file is deleted.</source> + <target>Yanlış anahtar veya bilinmeyen dosya yığın adresi - büyük olasılıkla dosya silinmiştir.</target> + <note>file error text</note> + </trans-unit> <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> + <target>Yanlış parola!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="XFTP servers" xml:space="preserve"> - <source>XFTP servers</source> + <trans-unit id="XFTP server" xml:space="preserve"> + <source>XFTP server</source> + <target>XFTP sunucusu</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> + <trans-unit id="You **must not** use the same database on two devices." xml:space="preserve"> + <source>You **must not** use the same database on two devices.</source> + <target>Aynı veritabanını iki cihazda **kullanmamalısınız**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You accepted connection" xml:space="preserve"> <source>You accepted connection</source> + <target>Bağlantıyı onayladın</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You allow" xml:space="preserve"> <source>You allow</source> + <target>İzin veriyorsunuz</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You already have a chat profile with the same display name. Please choose another name." xml:space="preserve"> <source>You already have a chat profile with the same display name. Please choose another name.</source> + <target>Aynı görünen ada sahip bir konuşma profilin zaten var. Lütfen başka bir ad seç.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already connected to %@." xml:space="preserve"> <source>You are already connected to %@.</source> + <target>Zaten %@'a bağlısınız.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already connected with %@." xml:space="preserve"> + <source>You are already connected with %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already connecting to %@." xml:space="preserve"> + <source>You are already connecting to %@.</source> + <target>Zaten %@'a bağlanıyorsunuz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already connecting via this one-time link!" xml:space="preserve"> + <source>You are already connecting via this one-time link!</source> + <target>Bu tek seferlik bağlantı üzerinden zaten bağlanıyorsunuz!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already in group %@." xml:space="preserve"> + <source>You are already in group %@.</source> + <target>Zaten %@ grubundasın.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already joining the group %@." xml:space="preserve"> + <source>You are already joining the group %@.</source> + <target>Zaten %@ grubuna katılıyorsunuz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already joining the group via this link!" xml:space="preserve"> + <source>You are already joining the group via this link!</source> + <target>Bu bağlantı üzerinden gruba zaten katılıyorsunuz!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already joining the group via this link." xml:space="preserve"> + <source>You are already joining the group via this link.</source> + <target>Gruba zaten bu bağlantı üzerinden katılıyorsunuz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already joining the group! Repeat join request?" xml:space="preserve"> + <source>You are already joining the group! +Repeat join request?</source> + <target>Gruba zaten katılıyorsunuz! +Katılma isteği tekrarlansın mı?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve"> <source>You are connected to the server used to receive messages from this contact.</source> + <target>Bu kişiden mesaj almak için kullanılan sunucuya bağlısınız.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are invited to group" xml:space="preserve"> <source>You are invited to group</source> + <target>Gruba davet edildiniz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are not connected to these servers. Private routing is used to deliver messages to them." xml:space="preserve"> + <source>You are not connected to these servers. Private routing is used to deliver messages to them.</source> + <target>Bu sunuculara bağlı değilsiniz. Mesajları onlara iletmek için özel yönlendirme kullanılır.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> <source>You can accept calls from lock screen, without device and app authentication.</source> + <target>Cihaz ve uygulama kimlik doğrulaması olmadan kilit ekranından çağrı kabul edebilirsiniz.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> + <trans-unit id="You can change it in Appearance settings." xml:space="preserve"> + <source>You can change it in Appearance settings.</source> + <target>Görünüm ayarlarından değiştirebilirsiniz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can configure servers via settings." xml:space="preserve"> + <source>You can configure servers via settings.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can create it later" xml:space="preserve"> <source>You can create it later</source> + <target>Daha sonra oluşturabilirsiniz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can enable later via Settings" xml:space="preserve"> + <source>You can enable later via Settings</source> + <target>Daha sonra Ayarlardan etkinleştirebilirsin</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can enable them later via app Privacy & Security settings." xml:space="preserve"> <source>You can enable them later via app Privacy & Security settings.</source> + <target>Daha sonra uygulamanın Gizlilik ve Güvenlik ayarlarından etkinleştirebilirsiniz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can give another try." xml:space="preserve"> + <source>You can give another try.</source> + <target>Bir kez daha deneyebilirsiniz.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <source>You can hide or mute a user profile - swipe it to the right.</source> + <target>Bir kullanıcı profilini gizleyebilir veya sessize alabilirsiniz - sağa kaydırın.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> + <trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve"> + <source>You can make it visible to your SimpleX contacts via Settings.</source> + <target>Ayarlardan SimpleX kişilerinize görünür yapabilirsiniz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now chat with %@" xml:space="preserve"> + <source>You can now chat with %@</source> + <target>Artık %@ adresine mesaj gönderebilirsin</target> <note>notification body</note> </trans-unit> + <trans-unit id="You can send messages to %@ from Archived contacts." xml:space="preserve"> + <source>You can send messages to %@ from Archived contacts.</source> + <target>Arşivlenen kişilerden %@'ya mesaj gönderebilirsiniz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can set connection name, to remember who the link was shared with." xml:space="preserve"> + <source>You can set connection name, to remember who the link was shared with.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> + <target>Kilit ekranı bildirim önizlemesini ayarlar üzerinden ayarlayabilirsiniz.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." xml:space="preserve"> <source>You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.</source> + <target>Bir bağlantı veya QR kodu paylaşabilirsiniz - bu durumda herkes gruba katılabilir. Daha sonra silseniz bile grubun üyelerini kaybetmezsiniz.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can share this address with your contacts to let them connect with **%@**." xml:space="preserve"> <source>You can share this address with your contacts to let them connect with **%@**.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> - <source>You can share your address as a link or QR code - anybody can connect to you.</source> + <target>Bu adresi kişilerinizle paylaşarak onların **%@** ile bağlantı kurmasını sağlayabilirsiniz.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> + <target>Sohbeti uygulamada Ayarlar / Veritabanı üzerinden veya uygulamayı yeniden başlatarak başlatabilirsiniz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve"> + <source>You can still view conversation with %@ in the list of chats.</source> + <target>Sohbet listesinde %@ ile konuşmayı görüntülemeye devam edebilirsiniz.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> + <target>SimpleX Kilidini Ayarlar üzerinden açabilirsiniz.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can use markdown to format messages:" xml:space="preserve"> <source>You can use markdown to format messages:</source> + <target>Mesajları biçimlendirmek için markdown kullanabilirsiniz:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can view invitation link again in connection details." xml:space="preserve"> + <source>You can view invitation link again in connection details.</source> + <target>Bağlantı detaylarından davet bağlantısını yeniden görüntüleyebilirsin.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> + <target>Mesajlar gönderemezsiniz!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> + <target>Doğrulanamadınız; lütfen tekrar deneyin.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> + <target>Kimin bağlanabileceğine siz karar verirsiniz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You have already requested connection via this address!" xml:space="preserve"> + <source>You have already requested connection via this address!</source> + <target>Bu adres üzerinden zaten bağlantı talebinde bulundunuz!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You have already requested connection! Repeat connection request?" xml:space="preserve"> + <source>You have already requested connection! +Repeat connection request?</source> + <target>Zaten bağlantı isteğinde bulundunuz! +Bağlantı isteği tekrarlansın mı?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> + <target>Uygulama her başladığında parola girmeniz gerekir - parola cihazınızda saklanmaz.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You invited your contact" xml:space="preserve"> - <source>You invited your contact</source> + <trans-unit id="You invited a contact" xml:space="preserve"> + <source>You invited a contact</source> + <target>Bir kişiyi davet ettin</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You joined this group" xml:space="preserve"> <source>You joined this group</source> + <target>Bu gruba katıldınız</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You joined this group. Connecting to inviting group member." xml:space="preserve"> <source>You joined this group. Connecting to inviting group member.</source> + <target>Bu gruba katıldınız. Davet eden grup üyesine bağlanılıyor.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may migrate the exported database." xml:space="preserve"> + <source>You may migrate the exported database.</source> + <target>Dışa aktarılan veritabanını taşıyabilirsiniz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may save the exported archive." xml:space="preserve"> + <source>You may save the exported archive.</source> + <target>Dışa aktarılan arşivi kaydedebilirsiniz.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> + <target>Sohbet veritabanınızın en son sürümünü SADECE bir cihazda kullanmalısınız, aksi takdirde bazı kişilerden daha fazla mesaj alamayabilirsiniz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You need to allow your contact to call to be able to call them." xml:space="preserve"> + <source>You need to allow your contact to call to be able to call them.</source> + <target>Kendiniz arayabilmeniz için önce irtibat kişinizin sizi aramasına izin vermelisiniz.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> + <target>Sesli mesaj gönderebilmeniz için kişinizin de sesli mesaj göndermesine izin vermeniz gerekir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You rejected group invitation" xml:space="preserve"> <source>You rejected group invitation</source> + <target>Grup davetini reddettiniz</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You sent group invitation" xml:space="preserve"> <source>You sent group invitation</source> + <target>Grup daveti gönderdiniz</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You should receive notifications." xml:space="preserve"> + <source>You should receive notifications.</source> + <note>token info</note> + </trans-unit> <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> + <target>Grup sahibinin cihazı çevrimiçi olduğunda gruba bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You will be connected when group link host's device is online, please wait or check later!" xml:space="preserve"> + <source>You will be connected when group link host's device is online, please wait or check later!</source> + <target>Grup sahibinin cihazı çevrimiçi olduğunda gruba bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve"> <source>You will be connected when your connection request is accepted, please wait or check later!</source> + <target>Bağlantı isteğiniz kabul edildiğinde bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be connected when your contact's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected when your contact's device is online, please wait or check later!</source> + <target>Kişinizin cihazı çevrimiçi olduğunda bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be required to authenticate when you start or resume the app after 30 seconds in background." xml:space="preserve"> <source>You will be required to authenticate when you start or resume the app after 30 seconds in background.</source> + <target>Arka planda 30 saniye kaldıktan sonra uygulamayı başlattığınızda veya devam ettirdiğinizde kimlik doğrulaması yapmanız gerekecektir.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve"> - <source>You will join a group this link refers to and connect to its group members.</source> + <trans-unit id="You will connect to all group members." xml:space="preserve"> + <source>You will connect to all group members.</source> + <target>Bütün grup üyelerine bağlanacaksın.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve"> <source>You will still receive calls and notifications from muted profiles when they are active.</source> + <target>Aktif olduklarında sessize alınmış profillerden arama ve bildirim almaya devam edersiniz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You will stop receiving messages from this chat. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this chat. Chat history will be preserved.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> + <target>Bu gruptan artık mesaj almayacaksınız. Sohbet geçmişi korunacaktır.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You won't lose your contacts if you later delete your address." xml:space="preserve"> <source>You won't lose your contacts if you later delete your address.</source> + <target>Eğer sonradan bağlantınızı silseniz bile kişilerinizi kaybetmeyeceksiniz.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" xml:space="preserve"> <source>You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile</source> + <target>Gizli bir profil paylaştığınız kişiyi ana profilinizi kullandığınız gruba davet etmeye çalışıyorsunuz</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" xml:space="preserve"> <source>You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your %@ servers" xml:space="preserve"> - <source>Your %@ servers</source> + <target>Bu grup için gizli bir profil kullanıyorsunuz - ana profilinizi paylaşmayı önlemek için kişileri davet etmeye izin verilmiyor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your ICE servers" xml:space="preserve"> <source>Your ICE servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> + <target>ICE sunucularınız</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your SimpleX address" xml:space="preserve"> <source>Your SimpleX address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your XFTP servers" xml:space="preserve"> - <source>Your XFTP servers</source> + <target>SimpleX adresin</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your calls" xml:space="preserve"> <source>Your calls</source> + <target>Aramaların</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your chat database" xml:space="preserve"> <source>Your chat database</source> + <target>Sohbet veritabanınız</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your chat database is not encrypted - set passphrase to encrypt it." xml:space="preserve"> <source>Your chat database is not encrypted - set passphrase to encrypt it.</source> + <target>Sohbet veritabanınız şifrelenmemiş - şifrelemek için parola ayarlayın.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your chat profile will be sent to group members" xml:space="preserve"> - <source>Your chat profile will be sent to group members</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve"> - <source>Your chat profile will be sent to your contact</source> - <note>No comment provided by engineer.</note> + <trans-unit id="Your chat preferences" xml:space="preserve"> + <source>Your chat preferences</source> + <target>Sohbet tercihleriniz</target> + <note>alert title</note> </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> + <target>Sohbet profillerin</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> + <trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve"> + <source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source> + <target>Bağlantınız %@ adresine taşındı ancak sizi profile yönlendirirken beklenmedik bir hata oluştu.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> <source>Your contact sent a file that is larger than currently supported maximum size (%@).</source> + <target>Kişiniz şu anda desteklenen maksimum boyuttan (%@) daha büyük bir dosya gönderdi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contacts can allow full message deletion." xml:space="preserve"> <source>Your contacts can allow full message deletion.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> - <source>Your contacts in SimpleX will see it. -You can change it in Settings.</source> + <target>Kişileriniz tam mesaj silme işlemine izin verebilir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contacts will remain connected." xml:space="preserve"> <source>Your contacts will remain connected.</source> + <target>Kişileriniz bağlı kalacaktır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve"> + <source>Your credentials may be sent unencrypted.</source> + <target>Kimlik bilgileriniz şifrelenmeden gönderilebilir.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> + <target>Mevcut sohbet veritabanınız SİLİNECEK ve içe aktarılan veritabanıyla DEĞİŞTİRİLECEKTİR.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your current profile" xml:space="preserve"> <source>Your current profile</source> + <target>Mevcut profiliniz</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your preferences" xml:space="preserve"> <source>Your preferences</source> + <target>Tercihleriniz</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your privacy" xml:space="preserve"> <source>Your privacy</source> + <target>Gizliliğiniz</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> + <trans-unit id="Your profile" xml:space="preserve"> + <source>Your profile</source> + <target>Profiliniz</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>Your profile will be sent to the contact that you received this link from</source> + <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> + <source>Your profile **%@** will be shared.</source> + <target>Profiliniz **%@** paylaşılacaktır.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> + <target>Profil sadece kişilerinle paylaşılacak.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> + <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> + <target>Profiliniz cihazınızda saklanır ve sadece kişilerinizle paylaşılır. SimpleX sunucuları profilinizi göremez.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve"> + <source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source> + <target>Profiliniz değiştirildi. Kaydederseniz, güncellenmiş profil tüm kişilerinize gönderilecektir.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> + <target>Rasgele profiliniz</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> + <target>Sunucu adresiniz</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your servers" xml:space="preserve"> + <source>Your servers</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your settings" xml:space="preserve"> <source>Your settings</source> + <target>Ayarlarınız</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" xml:space="preserve"> <source>[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)</source> + <target>[Katkıda bulun](https://github.com/simplex-chat/simplex-chat#contribute)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="[Send us email](mailto:chat@simplex.chat)" xml:space="preserve"> <source>[Send us email](mailto:chat@simplex.chat)</source> + <target>[Bize e-posta gönder](mailto:chat@simplex.chat)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> <source>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</source> + <target>[Bize GitHub'da yıldız verin](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="_italic_" xml:space="preserve"> <source>\_italic_</source> + <target>\_italik_</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="`a + b`" xml:space="preserve"> <source>\`a + b`</source> + <target>\`a + b`</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="above, then choose:" xml:space="preserve"> <source>above, then choose:</source> + <target>yukarı çıkın, ardından seçin:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="accepted call" xml:space="preserve"> <source>accepted call</source> + <target>kabul edilen arama</target> <note>call status</note> </trans-unit> + <trans-unit id="accepted invitation" xml:space="preserve"> + <source>accepted invitation</source> + <note>chat list item title</note> + </trans-unit> <trans-unit id="admin" xml:space="preserve"> <source>admin</source> + <target>yönetici</target> <note>member role</note> </trans-unit> + <trans-unit id="admins" xml:space="preserve"> + <source>admins</source> + <target>yöneticiler</target> + <note>feature role</note> + </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> + <target>%@ için şifreleme kabul ediliyor…</target> <note>chat item text</note> </trans-unit> <trans-unit id="agreeing encryption…" xml:space="preserve"> <source>agreeing encryption…</source> + <target>şifreleme kabul ediliyor…</target> <note>chat item text</note> </trans-unit> + <trans-unit id="all members" xml:space="preserve"> + <source>all members</source> + <target>bütün üyeler</target> + <note>feature role</note> + </trans-unit> <trans-unit id="always" xml:space="preserve"> <source>always</source> + <target>her zaman</target> <note>pref value</note> </trans-unit> + <trans-unit id="and %lld other events" xml:space="preserve"> + <source>and %lld other events</source> + <target>ve %lld diğer etkinlikler</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="archived report" xml:space="preserve"> + <source>archived report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="attempts" xml:space="preserve"> + <source>attempts</source> + <target>denemeler</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <source>audio call (not e2e encrypted)</source> + <target>sesli arama (uçtan uca şifreli değil)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="author" xml:space="preserve"> + <source>author</source> + <target>yetkili</target> + <note>member role</note> + </trans-unit> <trans-unit id="bad message ID" xml:space="preserve"> <source>bad message ID</source> + <target>kötü mesaj kimliği</target> <note>integrity error chat item</note> </trans-unit> <trans-unit id="bad message hash" xml:space="preserve"> <source>bad message hash</source> + <target>kötü mesaj hash'i</target> <note>integrity error chat item</note> </trans-unit> + <trans-unit id="blocked" xml:space="preserve"> + <source>blocked</source> + <target>engellendi</target> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="blocked %@" xml:space="preserve"> + <source>blocked %@</source> + <target>engellendi %@</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="blocked by admin" xml:space="preserve"> + <source>blocked by admin</source> + <target>yönetici tarafından engellendi</target> + <note>blocked chat item +marked deleted chat item preview text</note> + </trans-unit> <trans-unit id="bold" xml:space="preserve"> <source>bold</source> + <target>kalın</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="call" xml:space="preserve"> + <source>call</source> + <target>Ara</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="call error" xml:space="preserve"> <source>call error</source> + <target>arama hatası</target> <note>call status</note> </trans-unit> <trans-unit id="call in progress" xml:space="preserve"> <source>call in progress</source> + <target>arama yapılıyor</target> <note>call status</note> </trans-unit> <trans-unit id="calling…" xml:space="preserve"> <source>calling…</source> + <target>aranıyor…</target> <note>call status</note> </trans-unit> <trans-unit id="cancelled %@" xml:space="preserve"> <source>cancelled %@</source> + <target>%@ iptal edildi</target> <note>feature offered item</note> </trans-unit> <trans-unit id="changed address for you" xml:space="preserve"> <source>changed address for you</source> + <target>senin için adres değiştirildi</target> <note>chat item text</note> </trans-unit> <trans-unit id="changed role of %@ to %@" xml:space="preserve"> <source>changed role of %1$@ to %2$@</source> + <target>1$@ rolünü %2$@ olarak değiştirdi</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="changed your role to %@" xml:space="preserve"> <source>changed your role to %@</source> + <target>rolünü %@ olarak değiştirdi</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="changing address for %@…" xml:space="preserve"> <source>changing address for %@…</source> + <target>%@ için adres değiştiriliyor…</target> <note>chat item text</note> </trans-unit> <trans-unit id="changing address…" xml:space="preserve"> <source>changing address…</source> + <target>adres değiştiriliyor…</target> <note>chat item text</note> </trans-unit> <trans-unit id="colored" xml:space="preserve"> <source>colored</source> + <target>renklendirilmiş</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="complete" xml:space="preserve"> <source>complete</source> + <target>tamamlandı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connect to SimpleX Chat developers." xml:space="preserve"> <source>connect to SimpleX Chat developers.</source> + <target>SimpleX Chat geliştiricilerine bağlan.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connected" xml:space="preserve"> <source>connected</source> + <target>bağlanıldı</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="connected directly" xml:space="preserve"> + <source>connected directly</source> + <target>doğrudan bağlandı</target> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="connecting" xml:space="preserve"> <source>connecting</source> + <target>bağlanılıyor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connecting (accepted)" xml:space="preserve"> <source>connecting (accepted)</source> + <target>bağlanılıyor (onaylandı)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connecting (announced)" xml:space="preserve"> <source>connecting (announced)</source> + <target>bağlanılıyor (duyuruldu)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connecting (introduced)" xml:space="preserve"> <source>connecting (introduced)</source> + <target>bağlanılıyor (tanıtıldı)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connecting (introduction invitation)" xml:space="preserve"> <source>connecting (introduction invitation)</source> + <target>bağlanılıyor (tanıtılma isteği)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connecting call" xml:space="preserve"> <source>connecting call…</source> + <target>aramaya bağlanılıyor…</target> <note>call status</note> </trans-unit> <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> - <note>chat list item title</note> + <target>bağlanılıyor…</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> + <target>bağlantı kuruldu</target> <note>chat list item title (it should not be shown</note> </trans-unit> <trans-unit id="connection:%@" xml:space="preserve"> <source>connection:%@</source> + <target>bağlantı:%@</target> <note>connection information</note> </trans-unit> + <trans-unit id="contact %@ changed to %@" xml:space="preserve"> + <source>contact %1$@ changed to %2$@</source> + <target>%1$@ kişisi %2$@ olarak değişti</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> + <target>kişi uçtan uca şifrelemeye sahiptir</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="contact has no e2e encryption" xml:space="preserve"> <source>contact has no e2e encryption</source> + <target>kişi uçtan uca şifrelemeye sahip değildir</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="creator" xml:space="preserve"> <source>creator</source> + <target>oluşturan</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="custom" xml:space="preserve"> <source>custom</source> + <target>özel</target> <note>dropdown time picker choice</note> </trans-unit> <trans-unit id="database version is newer than the app, but no down migration for: %@" xml:space="preserve"> <source>database version is newer than the app, but no down migration for: %@</source> + <target>veritabanı sürümü uygulamadan daha yeni, ancak aşağı geçiş yok: %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="days" xml:space="preserve"> <source>days</source> + <target>gün</target> <note>time unit</note> </trans-unit> + <trans-unit id="decryption errors" xml:space="preserve"> + <source>decryption errors</source> + <target>Şifre çözme hataları</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> - <note>pref value</note> + <target>varsayılan (%@)</target> + <note>delete after time +pref value</note> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> + <target>varsayılan (hayır)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="default (yes)" xml:space="preserve"> <source>default (yes)</source> + <target>varsayılan (evet)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="deleted" xml:space="preserve"> <source>deleted</source> + <target>silindi</target> <note>deleted chat item</note> </trans-unit> + <trans-unit id="deleted contact" xml:space="preserve"> + <source>deleted contact</source> + <target>silinmiş kişi</target> + <note>rcv direct event chat item</note> + </trans-unit> <trans-unit id="deleted group" xml:space="preserve"> <source>deleted group</source> + <target>silinmiş grup</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="different migration in the app/database: %@ / %@" xml:space="preserve"> <source>different migration in the app/database: %@ / %@</source> + <target>uygulamada/veritabanında farklı geçiş: %@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="direct" xml:space="preserve"> <source>direct</source> + <target>doğrudan</target> <note>connection level description</note> </trans-unit> + <trans-unit id="disabled" xml:space="preserve"> + <source>disabled</source> + <target>devre dışı</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="duplicate message" xml:space="preserve"> <source>duplicate message</source> + <target>yinelenen mesaj</target> <note>integrity error chat item</note> </trans-unit> + <trans-unit id="duplicates" xml:space="preserve"> + <source>duplicates</source> + <target>Kopyalar</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> + <target>uçtan uca şifrelenmiş</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="enabled" xml:space="preserve"> <source>enabled</source> + <target>etkin</target> <note>enabled status</note> </trans-unit> <trans-unit id="enabled for contact" xml:space="preserve"> <source>enabled for contact</source> + <target>konuşulan kişi için etkinleşti</target> <note>enabled status</note> </trans-unit> <trans-unit id="enabled for you" xml:space="preserve"> <source>enabled for you</source> + <target>senin için etkinleştirildi</target> <note>enabled status</note> </trans-unit> <trans-unit id="encryption agreed" xml:space="preserve"> <source>encryption agreed</source> + <target>şifreleme kabul edildi</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption agreed for %@" xml:space="preserve"> <source>encryption agreed for %@</source> + <target>şifreleme %@ için kabul edildi</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption ok" xml:space="preserve"> <source>encryption ok</source> + <target>şifreleme etkin</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption ok for %@" xml:space="preserve"> <source>encryption ok for %@</source> + <target>şifreleme %@ için etkin</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption re-negotiation allowed" xml:space="preserve"> <source>encryption re-negotiation allowed</source> + <target>şifrelemenin yeniden anlaşmasına izin verildi</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption re-negotiation allowed for %@" xml:space="preserve"> <source>encryption re-negotiation allowed for %@</source> + <target>şifrelemenin yeniden anlaşmasına %@ için izin verildi</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption re-negotiation required" xml:space="preserve"> <source>encryption re-negotiation required</source> + <target>şifrelemenin yeniden anlaşması gerekiyor</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption re-negotiation required for %@" xml:space="preserve"> <source>encryption re-negotiation required for %@</source> + <target>şifrelemenin yeniden anlaşması %@ için gerekiyor</target> <note>chat item text</note> </trans-unit> <trans-unit id="ended" xml:space="preserve"> <source>ended</source> + <target>bitti</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="ended call %@" xml:space="preserve"> <source>ended call %@</source> + <target>%@ araması bitti</target> <note>call status</note> </trans-unit> <trans-unit id="error" xml:space="preserve"> <source>error</source> + <target>hata</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="expired" xml:space="preserve"> + <source>expired</source> + <target>Süresi dolmuş</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="forwarded" xml:space="preserve"> + <source>forwarded</source> + <target>iletildi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> <source>group deleted</source> + <target>grup silindi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group profile updated" xml:space="preserve"> <source>group profile updated</source> + <target>grup profili güncellendi</target> <note>snd group event chat item</note> </trans-unit> <trans-unit id="hours" xml:space="preserve"> <source>hours</source> + <target>saat</target> <note>time unit</note> </trans-unit> <trans-unit id="iOS Keychain is used to securely store passphrase - it allows receiving push notifications." xml:space="preserve"> <source>iOS Keychain is used to securely store passphrase - it allows receiving push notifications.</source> + <target>iOS Anahtar Zinciri parolayı güvenli bir şekilde saklamak için kullanılır - anlık bildirimlerin alınmasını sağlar.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." xml:space="preserve"> <source>iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.</source> + <target>iOS Anahtar Zinciri, uygulamayı yeniden başlattıktan veya parolayı değiştirdikten sonra parolayı güvenli bir şekilde saklamak için kullanılacaktır - anlık bildirimlerin alınmasına izin verecektir.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="inactive" xml:space="preserve"> + <source>inactive</source> + <target>inaktif</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> + <target>kişi bağlantı linki aracılığıyla gizli</target> <note>chat list item description</note> </trans-unit> <trans-unit id="incognito via group link" xml:space="preserve"> <source>incognito via group link</source> + <target>grup bağlantısı aracılığıyla gizli</target> <note>chat list item description</note> </trans-unit> <trans-unit id="incognito via one-time link" xml:space="preserve"> <source>incognito via one-time link</source> + <target>tek seferlik bağlantısı aracılığıyla gizli</target> <note>chat list item description</note> </trans-unit> <trans-unit id="indirect (%d)" xml:space="preserve"> <source>indirect (%d)</source> + <target>dolaylı (%d)</target> <note>connection level description</note> </trans-unit> <trans-unit id="invalid chat" xml:space="preserve"> <source>invalid chat</source> + <target>geçersi̇z sohbet</target> <note>invalid chat data</note> </trans-unit> <trans-unit id="invalid chat data" xml:space="preserve"> <source>invalid chat data</source> + <target>geçersi̇z sohbet verisi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="invalid data" xml:space="preserve"> <source>invalid data</source> + <target>geçersiz veri</target> <note>invalid chat item</note> </trans-unit> <trans-unit id="invitation to group %@" xml:space="preserve"> <source>invitation to group %@</source> + <target>%@ grubuna davet</target> <note>group name</note> </trans-unit> + <trans-unit id="invite" xml:space="preserve"> + <source>invite</source> + <target>davet</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> + <target>davet edildi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="invited %@" xml:space="preserve"> <source>invited %@</source> + <target>%@ davet edildi</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="invited to connect" xml:space="preserve"> <source>invited to connect</source> + <target>bağlanmaya davet edildi</target> <note>chat list item title</note> </trans-unit> <trans-unit id="invited via your group link" xml:space="preserve"> <source>invited via your group link</source> + <target>grup bağlantınız üzerinden davet edildi</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="italic" xml:space="preserve"> <source>italic</source> + <target>italik</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="join as %@" xml:space="preserve"> <source>join as %@</source> + <target>%@ olarak katıl</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="left" xml:space="preserve"> <source>left</source> + <target>ayrıldı</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="marked deleted" xml:space="preserve"> <source>marked deleted</source> + <target>silinmiş olarak işaretlenmiş</target> <note>marked deleted chat item preview text</note> </trans-unit> <trans-unit id="member" xml:space="preserve"> <source>member</source> + <target>üye</target> <note>member role</note> </trans-unit> + <trans-unit id="member %@ changed to %@" xml:space="preserve"> + <source>member %1$@ changed to %2$@</source> + <target>kişi %1$@ , %2$@ olarak değişti</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> + <target>bağlanıldı</target> <note>rcv group event chat item</note> </trans-unit> - <trans-unit id="message received" xml:space="preserve" approved="no"> - <source>message received</source> - <target state="translated">mesaj alındı</target> - <note>notification</note> - </trans-unit> - <trans-unit id="minutes" xml:space="preserve" approved="no"> - <source>minutes</source> - <target state="translated">dakikalar</target> - <note>time unit</note> - </trans-unit> - <trans-unit id="missed call" xml:space="preserve" approved="no"> - <source>missed call</source> - <target state="translated">cevapsız arama</target> - <note>call status</note> - </trans-unit> - <trans-unit id="moderated" xml:space="preserve" approved="no"> - <source>moderated</source> - <target state="needs-translation">moderated</target> - <note>moderated chat item</note> - </trans-unit> - <trans-unit id="moderated by %@" xml:space="preserve" approved="no"> - <source>moderated by %@</source> - <target state="translated">%@ tarafından yönetilmekte</target> + <trans-unit id="message" xml:space="preserve"> + <source>message</source> + <target>mesaj</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="months" xml:space="preserve" approved="no"> + <trans-unit id="message received" xml:space="preserve"> + <source>message received</source> + <target>mesaj alındı</target> + <note>notification</note> + </trans-unit> + <trans-unit id="minutes" xml:space="preserve"> + <source>minutes</source> + <target>dakikalar</target> + <note>time unit</note> + </trans-unit> + <trans-unit id="missed call" xml:space="preserve"> + <source>missed call</source> + <target>cevapsız arama</target> + <note>call status</note> + </trans-unit> + <trans-unit id="moderated" xml:space="preserve"> + <source>moderated</source> + <target>yönetildi</target> + <note>moderated chat item</note> + </trans-unit> + <trans-unit id="moderated by %@" xml:space="preserve"> + <source>moderated by %@</source> + <target>%@ tarafından yönetilmekte</target> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="moderator" xml:space="preserve"> + <source>moderator</source> + <note>member role</note> + </trans-unit> + <trans-unit id="months" xml:space="preserve"> <source>months</source> - <target state="translated">aylar</target> + <target>aylar</target> <note>time unit</note> </trans-unit> <trans-unit id="never" xml:space="preserve"> <source>never</source> - <note>No comment provided by engineer.</note> + <target>asla</target> + <note>delete after time</note> </trans-unit> <trans-unit id="new message" xml:space="preserve"> <source>new message</source> + <target>yeni mesaj</target> <note>notification</note> </trans-unit> <trans-unit id="no" xml:space="preserve"> <source>no</source> + <target>hayır</target> <note>pref value</note> </trans-unit> <trans-unit id="no e2e encryption" xml:space="preserve"> <source>no e2e encryption</source> + <target>uçtan uca şifreleme yok</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="no text" xml:space="preserve"> <source>no text</source> + <target>metin yok</target> <note>copied message info in history</note> </trans-unit> <trans-unit id="observer" xml:space="preserve"> <source>observer</source> + <target>gözlemci</target> <note>member role</note> </trans-unit> <trans-unit id="off" xml:space="preserve"> <source>off</source> + <target>kapalı</target> <note>enabled status - group pref value</note> +group pref value +time to disappear</note> </trans-unit> <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> + <target>%@ teklif edildi</target> <note>feature offered item</note> </trans-unit> <trans-unit id="offered %@: %@" xml:space="preserve"> <source>offered %1$@: %2$@</source> + <target>%1$@: %2$@ teklif etti</target> <note>feature offered item</note> </trans-unit> <trans-unit id="on" xml:space="preserve"> <source>on</source> + <target>açık</target> <note>group pref value</note> </trans-unit> - <trans-unit id="or chat with the developers" xml:space="preserve"> - <source>or chat with the developers</source> + <trans-unit id="other" xml:space="preserve"> + <source>other</source> + <target>diğer</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="other errors" xml:space="preserve"> + <source>other errors</source> + <target>diğer hatalar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="owner" xml:space="preserve"> <source>owner</source> + <target>sahip</target> <note>member role</note> </trans-unit> + <trans-unit id="owners" xml:space="preserve"> + <source>owners</source> + <target>sahipler</target> + <note>feature role</note> + </trans-unit> <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> + <target>eşler arası</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="pending" xml:space="preserve"> + <source>pending</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="pending approval" xml:space="preserve"> + <source>pending approval</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="quantum resistant e2e encryption" xml:space="preserve"> + <source>quantum resistant e2e encryption</source> + <target>kuantuma dayanıklı e2e şifreleme</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> + <target>alınan cevap…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="received confirmation…" xml:space="preserve"> <source>received confirmation…</source> + <target>onaylama alındı…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="rejected" xml:space="preserve"> + <source>rejected</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> + <target>geri çevrilmiş çağrı</target> <note>call status</note> </trans-unit> <trans-unit id="removed" xml:space="preserve"> <source>removed</source> + <target>kaldırıldı</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="removed %@" xml:space="preserve"> <source>removed %@</source> + <target>%@ kaldırıldı</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="removed contact address" xml:space="preserve"> + <source>removed contact address</source> + <target>kişi adresi silindi</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="removed profile picture" xml:space="preserve"> + <source>removed profile picture</source> + <target>profil fotoğrafı silindi</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> + <target>sen kaldırıldın</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="requested to connect" xml:space="preserve"> + <source>requested to connect</source> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="saved" xml:space="preserve"> + <source>saved</source> + <target>kaydedildi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="saved from %@" xml:space="preserve"> + <source>saved from %@</source> + <target>%@ tarafından kaydedildi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="search" xml:space="preserve"> + <source>search</source> + <target>ara</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="sec" xml:space="preserve"> <source>sec</source> + <target>sn</target> <note>network option</note> </trans-unit> <trans-unit id="seconds" xml:space="preserve"> <source>seconds</source> + <target>saniye</target> <note>time unit</note> </trans-unit> <trans-unit id="secret" xml:space="preserve"> <source>secret</source> + <target>gizli</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="security code changed" xml:space="preserve" approved="no"> + <trans-unit id="security code changed" xml:space="preserve"> <source>security code changed</source> - <target state="translated">güvenlik kodu değiştirildi</target> + <target>güvenlik kodu değiştirildi</target> <note>chat item text</note> </trans-unit> - <trans-unit id="starting…" xml:space="preserve" approved="no"> + <trans-unit id="send direct message" xml:space="preserve"> + <source>send direct message</source> + <target>doğrudan mesaj gönder</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve"> + <source>server queue info: %1$@ + +last received msg: %2$@</source> + <target>sunucu kuyruk bilgisi: %1$@ + +son alınan msj: %2$@</target> + <note>queue info</note> + </trans-unit> + <trans-unit id="set new contact address" xml:space="preserve"> + <source>set new contact address</source> + <target>yeni kişi adresi ayarla</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="set new profile picture" xml:space="preserve"> + <source>set new profile picture</source> + <target>yeni profil fotoğrafı ayarla</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="standard end-to-end encryption" xml:space="preserve"> + <source>standard end-to-end encryption</source> + <target>standart uçtan uca şifreleme</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> - <target state="translated">başlıyor…</target> + <target>başlatılıyor…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="strike" xml:space="preserve" approved="no"> + <trans-unit id="strike" xml:space="preserve"> <source>strike</source> - <target state="needs-translation">strike</target> + <target>çizik</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="this contact" xml:space="preserve" approved="no"> + <trans-unit id="this contact" xml:space="preserve"> <source>this contact</source> - <target state="translated">Bu kişi</target> + <target>Bu kişi</target> <note>notification title</note> </trans-unit> - <trans-unit id="unknown" xml:space="preserve" approved="no"> - <source>unknown</source> - <target state="translated">bilinmeyen</target> - <note>connection info</note> - </trans-unit> - <trans-unit id="updated group profile" xml:space="preserve" approved="no"> - <source>updated group profile</source> - <target state="translated">grup profili güncellendi</target> + <trans-unit id="unblocked %@" xml:space="preserve"> + <source>unblocked %@</source> + <target>engeli kaldırıldı %@</target> <note>rcv group event chat item</note> </trans-unit> - <trans-unit id="v%@ (%@)" xml:space="preserve" approved="no"> + <trans-unit id="unknown" xml:space="preserve"> + <source>unknown</source> + <target>bilinmeyen</target> + <note>connection info</note> + </trans-unit> + <trans-unit id="unknown servers" xml:space="preserve"> + <source>unknown servers</source> + <target>bilinmeyen yönlendiriciler</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unknown status" xml:space="preserve"> + <source>unknown status</source> + <target>bilinmeyen durum</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unprotected" xml:space="preserve"> + <source>unprotected</source> + <target>korumasız</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="updated group profile" xml:space="preserve"> + <source>updated group profile</source> + <target>grup profili güncellendi</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="updated profile" xml:space="preserve"> + <source>updated profile</source> + <target>güncellenmiş profil</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="v%@" xml:space="preserve"> + <source>v%@</source> + <target>v%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="v%@ (%@)" xml:space="preserve"> <source>v%@ (%@)</source> - <target state="translated">v%@ (%@)</target> + <target>v%@ (%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="via contact address link" xml:space="preserve" approved="no"> + <trans-unit id="via contact address link" xml:space="preserve"> <source>via contact address link</source> - <target state="translated">bağlantı adres uzantısı ile</target> + <target>bağlantı adres uzantısı ile</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="via group link" xml:space="preserve" approved="no"> + <trans-unit id="via group link" xml:space="preserve"> <source>via group link</source> - <target state="translated">grup bağlantısı ile</target> + <target>grup bağlantısı ile</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="via one-time link" xml:space="preserve" approved="no"> + <trans-unit id="via one-time link" xml:space="preserve"> <source>via one-time link</source> - <target state="translated">tek kullanımlık bağlantısı ile</target> + <target>tek kullanımlık bağlantısı ile</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="via relay" xml:space="preserve" approved="no"> + <trans-unit id="via relay" xml:space="preserve"> <source>via relay</source> - <target state="needs-translation">via relay</target> + <target>yönlendirici aracılığıyla</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="video call (not e2e encrypted)" xml:space="preserve" approved="no"> + <trans-unit id="video" xml:space="preserve"> + <source>video</source> + <target>video</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> <source>video call (not e2e encrypted)</source> - <target state="translated">Görüntülü arama (şifrelenmiş değil)</target> + <target>Görüntülü arama (şifrelenmiş değil)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="waiting for answer…" xml:space="preserve" approved="no"> + <trans-unit id="waiting for answer…" xml:space="preserve"> <source>waiting for answer…</source> - <target state="translated">cevap bekleniyor…</target> + <target>cevap bekleniyor…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="waiting for confirmation…" xml:space="preserve" approved="no"> + <trans-unit id="waiting for confirmation…" xml:space="preserve"> <source>waiting for confirmation…</source> - <target state="translated">onay bekleniyor…</target> + <target>onay bekleniyor…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="wants to connect to you!" xml:space="preserve" approved="no"> + <trans-unit id="wants to connect to you!" xml:space="preserve"> <source>wants to connect to you!</source> - <target state="translated">bağlanmak istiyor!</target> + <target>bağlanmak istiyor!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="weeks" xml:space="preserve" approved="no"> + <trans-unit id="weeks" xml:space="preserve"> <source>weeks</source> - <target state="translated">haftalar</target> + <target>haftalar</target> <note>time unit</note> </trans-unit> - <trans-unit id="yes" xml:space="preserve" approved="no"> + <trans-unit id="when IP hidden" xml:space="preserve"> + <source>when IP hidden</source> + <target>IP gizliyken</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="yes" xml:space="preserve"> <source>yes</source> - <target state="translated">evet</target> + <target>evet</target> <note>pref value</note> </trans-unit> - <trans-unit id="you are invited to group" xml:space="preserve" approved="no"> + <trans-unit id="you" xml:space="preserve"> + <source>you</source> + <target>sen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> - <target state="translated">gruba davet edildiniz</target> + <target>gruba davet edildiniz</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="you are observer" xml:space="preserve" approved="no"> + <trans-unit id="you are observer" xml:space="preserve"> <source>you are observer</source> - <target state="translated">gözlemcisiniz</target> + <target>gözlemcisiniz</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="you changed address" xml:space="preserve" approved="no"> + <trans-unit id="you blocked %@" xml:space="preserve"> + <source>you blocked %@</source> + <target>engelledin %@</target> + <note>snd group event chat item</note> + </trans-unit> + <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> - <target state="translated">adresinizi değiştirdiniz</target> + <target>adresinizi değiştirdiniz</target> <note>chat item text</note> </trans-unit> - <trans-unit id="you changed address for %@" xml:space="preserve" approved="no"> + <trans-unit id="you changed address for %@" xml:space="preserve"> <source>you changed address for %@</source> - <target state="translated">adresinizi %@ ile değiştirdiniz</target> + <target>adresinizi %@ ile değiştirdiniz</target> <note>chat item text</note> </trans-unit> - <trans-unit id="you changed role for yourself to %@" xml:space="preserve" approved="no"> + <trans-unit id="you changed role for yourself to %@" xml:space="preserve"> <source>you changed role for yourself to %@</source> - <target state="translated">kişisel yetkinizi %@ olarak değiştirdiniz</target> + <target>kişisel yetkinizi %@ olarak değiştirdiniz</target> <note>snd group event chat item</note> </trans-unit> - <trans-unit id="you changed role of %@ to %@" xml:space="preserve" approved="no"> + <trans-unit id="you changed role of %@ to %@" xml:space="preserve"> <source>you changed role of %1$@ to %2$@</source> - <target state="translated">%1$@'in yetkisini %2$@ olarak değiştirdiniz</target> + <target>%1$@'in yetkisini %2$@ olarak değiştirdiniz</target> <note>snd group event chat item</note> </trans-unit> - <trans-unit id="you left" xml:space="preserve" approved="no"> + <trans-unit id="you left" xml:space="preserve"> <source>you left</source> - <target state="translated">terk ettiniz</target> + <target>terk ettiniz</target> <note>snd group event chat item</note> </trans-unit> - <trans-unit id="you removed %@" xml:space="preserve" approved="no"> + <trans-unit id="you removed %@" xml:space="preserve"> <source>you removed %@</source> - <target state="translated">%@'yi çıkarttınız</target> + <target>%@'yi çıkarttınız</target> <note>snd group event chat item</note> </trans-unit> - <trans-unit id="you shared one-time link" xml:space="preserve" approved="no"> + <trans-unit id="you shared one-time link" xml:space="preserve"> <source>you shared one-time link</source> - <target state="translated">tek kullanımlık bağlantınızı paylaştınız</target> + <target>tek kullanımlık bağlantınızı paylaştınız</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="you shared one-time link incognito" xml:space="preserve" approved="no"> + <trans-unit id="you shared one-time link incognito" xml:space="preserve"> <source>you shared one-time link incognito</source> - <target state="translated">tek kullanımlık link paylaştınız gizli</target> + <target>tek kullanımlık link paylaştınız gizli</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="you: " xml:space="preserve" approved="no"> + <trans-unit id="you unblocked %@" xml:space="preserve"> + <source>you unblocked %@</source> + <target>engelini kaldırdın %@</target> + <note>snd group event chat item</note> + </trans-unit> + <trans-unit id="you: " xml:space="preserve"> <source>you: </source> - <target state="translated">sen: </target> + <target>sen: </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="~strike~" xml:space="preserve" approved="no"> + <trans-unit id="~strike~" xml:space="preserve"> <source>\~strike~</source> - <target state="needs-translation">\~strike~</target> + <target>\~çizik~</target> <note>No comment provided by engineer.</note> </trans-unit> </body> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="tr" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> - <trans-unit id="CFBundleName" xml:space="preserve" approved="no"> + <trans-unit id="CFBundleName" xml:space="preserve"> <source>SimpleX</source> - <target state="translated">SimpleX</target> + <target>SimpleX</target> <note>Bundle name</note> </trans-unit> <trans-unit id="NSCameraUsageDescription" xml:space="preserve"> <source>SimpleX needs camera access to scan QR codes to connect to other users and for video calls.</source> + <target>SimpleX, diğer kullanıcılara bağlanmak amacıyla QR kodlarını taramak ve görüntülü aramalar için kamera erişimine ihtiyaç duyar.</target> <note>Privacy - Camera Usage Description</note> </trans-unit> <trans-unit id="NSFaceIDUsageDescription" xml:space="preserve"> <source>SimpleX uses Face ID for local authentication</source> + <target>SimpleX yerel kimlik doğrulama için Face ID kullanır</target> <note>Privacy - Face ID Usage Description</note> </trans-unit> + <trans-unit id="NSLocalNetworkUsageDescription" xml:space="preserve"> + <source>SimpleX uses local network access to allow using user chat profile via desktop app on the same network.</source> + <target>SimpleX, aynı ağdaki masaüstü uygulaması aracılığıyla kullanıcı sohbet profilinin kullanılmasına izin vermek için yerel ağ erişimini kullanır.</target> + <note>Privacy - Local Network Usage Description</note> + </trans-unit> <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> <source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source> + <target>SimpleX'in sesli ve görüntülü aramalar ve sesli mesajları kaydetmek için mikrofon erişimine ihtiyacı vardır.</target> <note>Privacy - Microphone Usage Description</note> </trans-unit> <trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve"> <source>SimpleX needs access to Photo Library for saving captured and received media</source> + <target>SimpleX'in çekilen ve alınan medyayı kaydetmek için Fotoğraf Kitaplığı'na erişmesi gerekir</target> <note>Privacy - Photo Library Additions Usage Description</note> </trans-unit> </body> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="tr" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> <source>SimpleX NSE</source> + <target>SimpleX NSE</target> <note>Bundle display name</note> </trans-unit> <trans-unit id="CFBundleName" xml:space="preserve"> <source>SimpleX NSE</source> + <target>SimpleX NSE</target> <note>Bundle name</note> </trans-unit> <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> <source>Copyright © 2022 SimpleX Chat. All rights reserved.</source> + <target>Telif Hakkı © 2024 SimpleX Chat. Tüm hakları saklıdır.</target> <note>Copyright (human-readable)</note> </trans-unit> </body> </file> + <file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="tr" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%d new events" xml:space="preserve"> + <source>%d new events</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From %d chat(s)" xml:space="preserve"> + <source>From %d chat(s)</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From: %@" xml:space="preserve"> + <source>From: %@</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <note>notification</note> + </trans-unit> + <trans-unit id="New messages" xml:space="preserve"> + <source>New messages</source> + <note>notification</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="tr" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle display name</note> + </trans-unit> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> + <source>Copyright © 2024 SimpleX Chat. All rights reserved.</source> + <target>Telif Hakkı © 2024 SimpleX Chat. Tüm hakları saklıdır.</target> + <note>Copyright (human-readable)</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="tr" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <target>%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App is locked!" xml:space="preserve"> + <source>App is locked!</source> + <target>Uygulama kilitlendi!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <target>İptal et</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <target>Veritabanı şifresini kaydetmek için Anahtar Zinciri'ne erişilemiyor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Mesaj iletilemiyor</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Comment" xml:space="preserve"> + <source>Comment</source> + <target>Yorum</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <target>Şu anki maksimum desteklenen dosya boyutu %@ kadardır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade required" xml:space="preserve"> + <source>Database downgrade required</source> + <target>Veritabanı sürüm düşürme gerekli</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <target>Veritabanı şifrelendi!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <target>Veritabanı hatası</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <target>Veritabanı parolası Anahtar Zinciri'nde kayıtlı olandan farklıdır.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <target>Konuşmayı açmak için veri tabanı parolası gerekli.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade required" xml:space="preserve"> + <source>Database upgrade required</source> + <target>Veritabanı yükseltmesi gerekli</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing file" xml:space="preserve"> + <source>Error preparing file</source> + <target>Dosya hazırlanırken hata oluştu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing message" xml:space="preserve"> + <source>Error preparing message</source> + <target>Mesaj hazırlanırken hata oluştu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <target>Hata: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Dosya hatası</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <target>Uyumsuz veritabanı sürümü</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Geçerli olmayan taşıma onayı</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <target>Anahtarlık hatası</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <target>Büyük dosya!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No active profile" xml:space="preserve"> + <source>No active profile</source> + <target>Aktif profil yok</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <target>Tamam</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to downgrade the database." xml:space="preserve"> + <source>Open the app to downgrade the database.</source> + <target>Veritabanının sürümünü düşürmek için uygulamayı açın.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to upgrade the database." xml:space="preserve"> + <source>Open the app to upgrade the database.</source> + <target>Veritabanını güncellemek için uygulamayı açın.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passphrase" xml:space="preserve"> + <source>Passphrase</source> + <target>Parola</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve"> + <source>Please create a profile in the SimpleX app</source> + <target>Lütfen SimpleX uygulamasında bir profil oluşturun</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Seçilen sohbet tercihleri bu mesajı yasakladı.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending a message takes longer than expected." xml:space="preserve"> + <source>Sending a message takes longer than expected.</source> + <target>Mesaj göndermek beklenenden daha uzun sürüyor.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending message…" xml:space="preserve"> + <source>Sending message…</source> + <target>Mesaj gönderiliyor…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <target>Paylaş</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Slow network?" xml:space="preserve"> + <source>Slow network?</source> + <target>Ağ yavaş mı?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <target>Bilinmeyen veritabanı hatası: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unsupported format" xml:space="preserve"> + <source>Unsupported format</source> + <target>Desteklenmeyen format</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wait" xml:space="preserve"> + <source>Wait</source> + <target>Bekleyin</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <target>Yanlış veritabanı parolası</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve"> + <source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source> + <target>Gizlilik ve Güvenlik / SimpleX Lock ayarlarından paylaşıma izin verebilirsiniz.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + </body> + </file> </xliff> diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..aaa7f79bc8 --- /dev/null +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,23 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.000", + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.533" + } + }, + "idiom" : "universal" + } + ], + "properties" : { + "localizable" : true + }, + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..9c675514f4 --- /dev/null +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/en.lproj/Localizable.strings new file mode 100644 index 0000000000..cb83427195 --- /dev/null +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/en.lproj/Localizable.strings @@ -0,0 +1,26 @@ +/* No comment provided by engineer. */ +"_italic_" = "\\_italic_"; + +/* No comment provided by engineer. */ +"*bold*" = "\\*bold*"; + +/* No comment provided by engineer. */ +"`a + b`" = "\\`a + b`"; + +/* No comment provided by engineer. */ +"~strike~" = "\\~strike~"; + +/* call status */ +"connecting call" = "connecting call…"; + +/* No comment provided by engineer. */ +"Connecting server…" = "Connecting to server…"; + +/* No comment provided by engineer. */ +"Connecting server… (error: %@)" = "Connecting to server… (error: %@)"; + +/* rcv group event chat item */ +"member connected" = "connected"; + +/* No comment provided by engineer. */ +"No group!" = "Group not found!"; diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings new file mode 100644 index 0000000000..d34eb67fc7 --- /dev/null +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -0,0 +1,12 @@ +/* Bundle name */ +"CFBundleName" = "SimpleX"; +/* Privacy - Camera Usage Description */ +"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; +/* Privacy - Face ID Usage Description */ +"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; +/* Privacy - Local Network Usage Description */ +"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; +/* Privacy - Photo Library Additions Usage Description */ +"NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/contents.json b/apps/ios/SimpleX Localizations/tr.xcloc/contents.json new file mode 100644 index 0000000000..2e32ea2080 --- /dev/null +++ b/apps/ios/SimpleX Localizations/tr.xcloc/contents.json @@ -0,0 +1,12 @@ +{ + "developmentRegion" : "en", + "project" : "SimpleX.xcodeproj", + "targetLocale" : "tr", + "toolInfo" : { + "toolBuildNumber" : "16C5032a", + "toolID" : "com.apple.dt.xcode", + "toolName" : "Xcode", + "toolVersion" : "16.2" + }, + "version" : "1.0" +} \ No newline at end of file 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 abd58231a9..c0375e3b02 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -2,36 +2,9 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="uk" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> - <trans-unit id=" " xml:space="preserve"> - <source> -</source> - <target> -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (" xml:space="preserve"> - <source> (</source> - <target> (</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> <target> (можна скопіювати)</target> @@ -89,6 +62,7 @@ </trans-unit> <trans-unit id="%@ and %@" xml:space="preserve"> <source>%@ and %@</source> + <target>%@ та %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ and %@ connected" xml:space="preserve"> @@ -103,6 +77,12 @@ </trans-unit> <trans-unit id="%@ connected" xml:space="preserve"> <source>%@ connected</source> + <target>%@ підключено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve"> + <source>%@ downloaded</source> + <target>%@ встановлено</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ is connected!" xml:space="preserve"> @@ -120,18 +100,34 @@ <target>%@ перевірено</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ server" xml:space="preserve"> + <source>%@ server</source> + <target>%@ сервер</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ servers" xml:space="preserve"> <source>%@ servers</source> <target>%@ сервери</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve"> + <source>%@ uploaded</source> + <target>%@ завантажено</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ wants to connect!" xml:space="preserve"> <source>%@ wants to connect!</source> <target>%@ хоче підключитися!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve"> + <source>%1$@, %2$@</source> + <target>%1$@, %2$@</target> + <note>format for date separator in chat</note> + </trans-unit> <trans-unit id="%@, %@ and %lld members" xml:space="preserve"> <source>%@, %@ and %lld members</source> + <target>%@, %@ та %lld учасників</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> @@ -149,11 +145,36 @@ <target>%d днів</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve"> + <source>%d file(s) are still being downloaded.</source> + <target>%их файл(ів) ще досі завантажуються.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve"> + <source>%d file(s) failed to download.</source> + <target>%их файлів не вийшло завантажити.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve"> + <source>%d file(s) were deleted.</source> + <target>%их файл(ів) було видалено.</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve"> + <source>%d file(s) were not downloaded.</source> + <target>%d файл(и) не було завантажено.</target> + <note>forward confirmation reason</note> + </trans-unit> <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> <target>%d годин</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve"> + <source>%d messages not forwarded</source> + <target>%d повідомлень не переслано</target> + <note>alert title</note> + </trans-unit> <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> <target>%d хв</target> @@ -169,6 +190,10 @@ <target>%d сек</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve"> + <source>%d seconds(s)</source> + <note>delete after time</note> + </trans-unit> <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> <target>%d пропущено повідомлення(ь)</target> @@ -201,6 +226,7 @@ </trans-unit> <trans-unit id="%lld group events" xml:space="preserve"> <source>%lld group events</source> + <target>%lld групові заходи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld members" xml:space="preserve"> @@ -210,14 +236,22 @@ </trans-unit> <trans-unit id="%lld messages blocked" xml:space="preserve"> <source>%lld messages blocked</source> + <target>%lld повідомлень заблоковано</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve"> + <source>%lld messages blocked by admin</source> + <target>%lld повідомлень заблоковано адміністратором</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld messages marked deleted" xml:space="preserve"> <source>%lld messages marked deleted</source> + <target>%lld повідомлень позначено як видалені</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld messages moderated by %@" xml:space="preserve"> <source>%lld messages moderated by %@</source> + <target>%lld повідомлень модерує %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld minutes" xml:space="preserve"> @@ -227,11 +261,7 @@ </trans-unit> <trans-unit id="%lld new interface languages" xml:space="preserve"> <source>%lld new interface languages</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> - <source>%lld second(s)</source> - <target>%lld секунд(и)</target> + <target>%lld нові мови інтерфейсу</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld seconds" xml:space="preserve"> @@ -284,47 +314,39 @@ <target>%u повідомлень пропущено.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> - <source>(</source> - <target>(</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="(new)" xml:space="preserve"> <source>(new)</source> + <target>(новий)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="(this device v%@)" xml:space="preserve"> <source>(this device v%@)</source> + <target>(цей пристрій v%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve"> - <source>)</source> - <target>)</target> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target>**Додати контакт**: створити нове посилання-запрошення.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target>**Додати новий контакт**: щоб створити одноразовий QR-код або посилання для свого контакту.</target> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve"> + <source>**Create group**: to create a new group.</source> + <target>**Створити групу**: створити нову групу.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> - <source>**Create link / QR code** for your contact to use.</source> - <target>**Створіть посилання / QR-код** для використання вашим контактом.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Більш приватний**: перевіряти нові повідомлення кожні 20 хвилин. Серверу SimpleX Chat передається токен пристрою, але не кількість контактів або повідомлень, які ви маєте.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Найбільш приватний**: не використовуйте сервер сповіщень SimpleX Chat, періодично перевіряйте повідомлення у фоновому режимі (залежить від того, як часто ви користуєтесь додатком).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <target>**Вставте отримане посилання** або відкрийте його в браузері і натисніть **Відкрити в мобільному додатку**.</target> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> + <target>**Зверніть увагу**: використання однієї і тієї ж бази даних на двох пристроях порушить розшифровку повідомлень з ваших з'єднань, як захист безпеки.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> @@ -332,14 +354,14 @@ <target>**Зверніть увагу: ви НЕ зможете відновити або змінити пароль, якщо втратите його.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**Рекомендується**: токен пристрою та сповіщення надсилаються на сервер сповіщень SimpleX Chat, але не вміст повідомлення, його розмір або від кого воно надійшло.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target>**Відскануйте QR-код**: щоб з'єднатися з вашим контактом особисто або за допомогою відеодзвінка.</target> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve"> + <source>**Scan / Paste link**: to connect via a link you received.</source> + <target>**Відсканувати / Вставити посилання**: підключитися за отриманим посиланням.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> @@ -347,6 +369,11 @@ <target>**Попередження**: Для отримання миттєвих пуш-сповіщень потрібна парольна фраза, збережена у брелоку.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve"> + <source>**Warning**: the archive will be removed.</source> + <target>**Попередження**: архів буде видалено.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> <source>**e2e encrypted** audio call</source> <target>**e2e encrypted** аудіодзвінок</target> @@ -362,15 +389,13 @@ <target>\*жирний*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=", " xml:space="preserve"> - <source>, </source> - <target>, </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable.</source> + <target>- підключитися до [сервера каталогів](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (БЕТА)! +- підтвердження доставлення (до 20 учасників). +- швидше і стабільніше.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> @@ -386,6 +411,9 @@ <source>- optionally notify deleted contacts. - profile names with spaces. - and more!</source> + <target>- опція сповіщати про видалені контакти. +- імена профілів з пробілами. +- та багато іншого!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="- voice messages up to 5 minutes. - custom time to disappear. - editing history." xml:space="preserve"> @@ -397,13 +425,9 @@ - історія редагування.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="." xml:space="preserve"> - <source>.</source> - <target>.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="0 sec" xml:space="preserve"> <source>0 sec</source> + <target>0 сек</target> <note>time to disappear</note> </trans-unit> <trans-unit id="0s" xml:space="preserve"> @@ -414,7 +438,8 @@ <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> <target>1 день</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> @@ -429,16 +454,27 @@ <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> <target>1 місяць</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> <target>1 тиждень</target> - <note>time interval</note> + <note>delete after time +time interval</note> + </trans-unit> + <trans-unit id="1 year" xml:space="preserve"> + <source>1 year</source> + <note>delete after time</note> </trans-unit> <trans-unit id="1-time link" xml:space="preserve"> <source>1-time link</source> - <target>1-разове посилання</target> + <target>Одноразове посилання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> + <target>Одноразове посилання можна використовувати *тільки з одним контактом* - поділіться ним особисто або через будь-який месенджер.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="5 minutes" xml:space="preserve"> @@ -456,11 +492,6 @@ <target>30 секунд</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve"> - <source>: </source> - <target>: </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> @@ -510,31 +541,32 @@ <target>Скасувати зміну адреси?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve"> - <source>About SimpleX</source> - <target>Про SimpleX</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve"> <source>About SimpleX Chat</source> <target>Про чат SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve"> - <source>About SimpleX address</source> - <target>Про адресу SimpleX</target> + <trans-unit id="About operators" xml:space="preserve"> + <source>About operators</source> + <target>Про операторів</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accent color" xml:space="preserve"> - <source>Accent color</source> - <target>Акцентний колір</target> + <trans-unit id="Accent" xml:space="preserve"> + <source>Accent</source> + <target>Акцент</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept" xml:space="preserve"> <source>Accept</source> <target>Прийняти</target> <note>accept contact request via notification - accept incoming call via notification</note> +accept incoming call via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accept conditions" xml:space="preserve"> + <source>Accept conditions</source> + <target>Прийняти умови</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept connection request?" xml:space="preserve"> <source>Accept connection request?</source> @@ -549,16 +581,45 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Прийняти інкогніто</target> - <note>accept contact request via notification</note> + <note>accept contact request via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve"> + <source>Accepted conditions</source> + <target>Прийняті умови</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve"> + <source>Acknowledged</source> + <target>Визнано</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve"> + <source>Acknowledgement errors</source> + <target>Помилки підтвердження</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Active" xml:space="preserve"> + <source>Active</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve"> + <source>Active connections</source> + <target>Активні з'єднання</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> <target>Додайте адресу до свого профілю, щоб ваші контакти могли поділитися нею з іншими людьми. Повідомлення про оновлення профілю буде надіслано вашим контактам.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> - <source>Add preset servers</source> - <target>Додавання попередньо встановлених серверів</target> + <trans-unit id="Add friends" xml:space="preserve"> + <source>Add friends</source> + <target>Додайте друзів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve"> + <source>Add list</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add profile" xml:space="preserve"> @@ -566,14 +627,19 @@ <target>Додати профіль</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> + <target>Додати сервер</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> <source>Add servers by scanning QR codes.</source> <target>Додайте сервери, відсканувавши QR-код.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> - <target>Додати сервер…</target> + <trans-unit id="Add team members" xml:space="preserve"> + <source>Add team members</source> + <target>Додайте учасників команди</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -581,11 +647,45 @@ <target>Додати до іншого пристрою</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add to list" xml:space="preserve"> + <source>Add to list</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> <target>Додати вітальне повідомлення</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve"> + <source>Add your team members to the conversations.</source> + <target>Додайте членів своєї команди до розмов.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve"> + <source>Added media & file servers</source> + <target>Додано медіа та файлові сервери</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve"> + <source>Added message servers</source> + <target>Додано сервери повідомлень</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve"> + <source>Additional accent</source> + <target>Додатковий акцент</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve"> + <source>Additional accent 2</source> + <target>Додатковий акцент 2</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve"> + <source>Additional secondary</source> + <target>Додаткова вторинна</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Address" xml:space="preserve"> <source>Address</source> <target>Адреса</target> @@ -596,6 +696,21 @@ <target>Зміна адреси буде скасована. Буде використано стару адресу отримання.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve"> + <source>Address or 1-time link?</source> + <target>Адреса чи одноразове посилання?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve"> + <source>Address settings</source> + <target>Налаштування адреси</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve"> + <source>Admins can block a member for all.</source> + <target>Адміністратори можуть заблокувати користувача для всіх.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> <source>Admins can create the links to join groups.</source> <target>Адміни можуть створювати посилання для приєднання до груп.</target> @@ -606,6 +721,15 @@ <target>Розширені налаштування мережі</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve"> + <source>Advanced settings</source> + <target>Додаткові налаштування</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All" xml:space="preserve"> + <source>All</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All app data is deleted." xml:space="preserve"> <source>All app data is deleted.</source> <target>Всі дані програми видаляються.</target> @@ -616,16 +740,35 @@ <target>Всі чати та повідомлення будуть видалені - це неможливо скасувати!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> <target>Всі дані стираються при введенні.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve"> + <source>All data is kept private on your device.</source> + <target>Всі дані є приватними для вашого пристрою.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> <target>Всі учасники групи залишаться на зв'язку.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <target>Всі повідомлення та файли надсилаються **наскрізним шифруванням**, з пост-квантовим захистом у прямих повідомленнях.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone!</source> + <target>Усі повідомлення будуть видалені - цю дію не можна скасувати!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> <target>Всі повідомлення будуть видалені - це неможливо скасувати! Повідомлення будуть видалені ТІЛЬКИ для вас.</target> @@ -633,6 +776,20 @@ </trans-unit> <trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve"> <source>All new messages from %@ will be hidden!</source> + <target>Всі нові повідомлення від %@ будуть приховані!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All profiles" xml:space="preserve"> + <source>All profiles</source> + <target>Всі профілі</target> + <note>profile dropdown</note> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve"> + <source>All reports will be archived for you.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All servers" xml:space="preserve"> + <source>All servers</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All your contacts will remain connected." xml:space="preserve"> @@ -645,6 +802,11 @@ <target>Всі ваші контакти залишаться на зв'язку. Повідомлення про оновлення профілю буде надіслано вашим контактам.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target>Всі ваші контакти, розмови та файли будуть надійно зашифровані та завантажені частинами на налаштовані XFTP-реле.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow" xml:space="preserve"> <source>Allow</source> <target>Дозволити</target> @@ -655,14 +817,24 @@ <target>Дозволяйте дзвінки, тільки якщо ваш контакт дозволяє їх.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve"> + <source>Allow calls?</source> + <target>Дозволити дзвінки?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> <target>Дозволяйте зникати повідомленням, тільки якщо контакт дозволяє вам це робити.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <target>Дозволяйте безповоротне видалення повідомлень, тільки якщо контакт дозволяє вам це зробити.</target> + <trans-unit id="Allow downgrade" xml:space="preserve"> + <source>Allow downgrade</source> + <target>Дозволити пониження версії</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target>Дозволяйте безповоротне видалення повідомлень, тільки якщо контакт дозволяє вам це зробити. (24 години)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> @@ -685,9 +857,23 @@ <target>Дозволити надсилання зникаючих повідомлень.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow to irreversibly delete sent messages.</source> - <target>Дозволяє безповоротно видаляти надіслані повідомлення.</target> + <trans-unit id="Allow sharing" xml:space="preserve"> + <source>Allow sharing</source> + <target>Дозволити спільний доступ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target>Дозволяє безповоротно видаляти надіслані повідомлення. (24 години)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve"> + <source>Allow to report messsages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve"> + <source>Allow to send SimpleX links.</source> + <target>Дозволити надсилати посилання SimpleX.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send files and media." xml:space="preserve"> @@ -720,9 +906,9 @@ <target>Дозвольте вашим контактам телефонувати вам.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow your contacts to irreversibly delete sent messages.</source> - <target>Дозвольте вашим контактам безповоротно видаляти надіслані повідомлення.</target> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target>Дозвольте вашим контактам безповоротно видаляти надіслані повідомлення. (24 години)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> @@ -742,10 +928,17 @@ </trans-unit> <trans-unit id="Already connecting!" xml:space="preserve"> <source>Already connecting!</source> + <target>Вже підключаємось!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Already joining the group!" xml:space="preserve"> <source>Already joining the group!</source> + <target>Вже приєднуємося до групи!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve"> + <source>Always use private routing.</source> + <target>Завжди використовуйте приватну маршрутизацію.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> @@ -758,18 +951,37 @@ <target>Створюється порожній профіль чату з вказаним ім'ям, і додаток відкривається у звичайному режимі.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Another reason" xml:space="preserve"> + <source>Another reason</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> <target>Відповісти на дзвінок</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> + <target>Кожен може хостити сервери.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> <target>Збірка програми: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App data migration" xml:space="preserve"> + <source>App data migration</source> + <target>Міграція даних додатків</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> <source>App encrypts new local files (except videos).</source> + <target>Додаток шифрує нові локальні файли (крім відео).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App group:" xml:space="preserve"> + <source>App group:</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App icon" xml:space="preserve"> @@ -787,6 +999,11 @@ <target>Пароль програми замінено на пароль самознищення.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App session" xml:space="preserve"> + <source>App session</source> + <target>Сесія програми</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App version" xml:space="preserve"> <source>App version</source> <target>Версія програми</target> @@ -802,6 +1019,60 @@ <target>Зовнішній вигляд</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Apply" xml:space="preserve"> + <source>Apply</source> + <target>Подати заявку</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve"> + <source>Apply to</source> + <target>Звертатися до</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve"> + <source>Archive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve"> + <source>Archive %lld reports?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve"> + <source>Archive all reports?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve"> + <source>Archive and upload</source> + <target>Архівування та завантаження</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve"> + <source>Archive contacts to chat later.</source> + <target>Архівуйте контакти, щоб поспілкуватися пізніше.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve"> + <source>Archive report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve"> + <source>Archive report?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve"> + <source>Archive reports</source> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve"> + <source>Archived contacts</source> + <target>Архівні контакти</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve"> + <source>Archiving database</source> + <target>Архівування бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Attach" xml:space="preserve"> <source>Attach</source> <target>Прикріпити</target> @@ -862,13 +1133,24 @@ <target>Автоматичне прийняття зображень</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve"> + <source>Auto-accept settings</source> + <target>Автоприйняття налаштувань</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Back" xml:space="preserve"> <source>Back</source> <target>Назад</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Background" xml:space="preserve"> + <source>Background</source> + <target>Фон</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Bad desktop address" xml:space="preserve"> <source>Bad desktop address</source> + <target>Неправильна адреса робочого столу</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Bad message ID" xml:space="preserve"> @@ -881,8 +1163,23 @@ <target>Поганий хеш повідомлення</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better calls" xml:space="preserve"> + <source>Better calls</source> + <target>Кращі дзвінки</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better groups" xml:space="preserve"> <source>Better groups</source> + <target>Кращі групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve"> + <source>Better groups performance</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve"> + <source>Better message dates.</source> + <target>Кращі дати повідомлень.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Better messages" xml:space="preserve"> @@ -890,20 +1187,78 @@ <target>Кращі повідомлення</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better networking" xml:space="preserve"> + <source>Better networking</source> + <target>Краща мережа</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve"> + <source>Better notifications</source> + <target>Кращі сповіщення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve"> + <source>Better privacy and security</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve"> + <source>Better security ✅</source> + <target>Краща безпека ✅</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve"> + <source>Better user experience</source> + <target>Покращений користувацький досвід</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Black" xml:space="preserve"> + <source>Black</source> + <target>Чорний</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block" xml:space="preserve"> <source>Block</source> + <target>Блокувати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block for all" xml:space="preserve"> + <source>Block for all</source> + <target>Заблокувати для всіх</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Block group members" xml:space="preserve"> <source>Block group members</source> + <target>Учасники групи блокування</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Block member" xml:space="preserve"> <source>Block member</source> + <target>Заблокувати користувача</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve"> + <source>Block member for all?</source> + <target>Заблокувати учасника для всіх?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Block member?" xml:space="preserve"> <source>Block member?</source> + <target>Заблокувати користувача?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve"> + <source>Blocked by admin</source> + <target>Заблокований адміністратором</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve"> + <source>Blur for better privacy.</source> + <target>Розмиття для кращої приватності.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve"> + <source>Blur media</source> + <target>Розмиття медіа</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> @@ -911,9 +1266,9 @@ <target>Реакції на повідомлення можете додавати як ви, так і ваш контакт.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> - <source>Both you and your contact can irreversibly delete sent messages.</source> - <target>І ви, і ваш контакт можете безповоротно видалити надіслані повідомлення.</target> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target>І ви, і ваш контакт можете безповоротно видалити надіслані повідомлення. (24 години)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> @@ -933,6 +1288,21 @@ </trans-unit> <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target>Болгарською, фінською, тайською та українською мовами - завдяки користувачам та [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business address" xml:space="preserve"> + <source>Business address</source> + <target>Адреса підприємства</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve"> + <source>Business chats</source> + <target>Ділові чати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve"> + <source>Businesses</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> @@ -940,6 +1310,12 @@ <target>Через профіль чату (за замовчуванням) або [за з'єднанням](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve"> + <source>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> <source>Call already ended!</source> <target>Дзвінок вже закінчився!</target> @@ -950,9 +1326,24 @@ <target>Дзвінки</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve"> - <source>Can't delete user profile!</source> - <target>Не можу видалити профіль користувача!</target> + <trans-unit id="Calls prohibited!" xml:space="preserve"> + <source>Calls prohibited!</source> + <target>Дзвінки заборонені!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve"> + <source>Camera not available</source> + <target>Камера недоступна</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve"> + <source>Can't call contact</source> + <target>Не вдається додзвонитися до контакту</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve"> + <source>Can't call member</source> + <target>Не вдається зателефонувати користувачеві</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contact!" xml:space="preserve"> @@ -965,9 +1356,20 @@ <target>Неможливо запросити контакти!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve"> + <source>Can't message member</source> + <target>Не можу надіслати повідомлення користувачеві</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> <target>Скасувати</target> + <note>alert action +alert button</note> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve"> + <source>Cancel migration</source> + <target>Скасувати міграцію</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> @@ -975,9 +1377,24 @@ <target>Не вдається отримати доступ до зв'язки ключів для збереження пароля до бази даних</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Неможливо переслати повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> <target>Не вдається отримати файл</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <target>Перевищено ліміт - одержувач не отримав раніше надіслані повідомлення.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve"> + <source>Cellular</source> + <target>Стільниковий</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change" xml:space="preserve"> @@ -985,6 +1402,15 @@ <target>Зміна</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve"> + <source>Change automatic message deletion?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve"> + <source>Change chat profiles</source> + <target>Зміна профілів користувачів</target> + <note>authentication reason</note> + </trans-unit> <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> <target>Змінити пароль до бази даних?</target> @@ -1029,11 +1455,26 @@ <source>Change self-destruct passcode</source> <target>Змінити пароль самознищення</target> <note>authentication reason - set passcode view</note> +set passcode view</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> - <source>Chat archive</source> - <target>Архів чату</target> + <trans-unit id="Chat" xml:space="preserve"> + <source>Chat</source> + <target>Чат</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve"> + <source>Chat already exists</source> + <target>Чат вже існує</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve"> + <source>Chat already exists!</source> + <target>Чат вже існує!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve"> + <source>Chat colors</source> + <target>Кольори чату</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat console" xml:space="preserve"> @@ -1051,6 +1492,11 @@ <target>Видалено базу даних чату</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve"> + <source>Chat database exported</source> + <target>Експортовано базу даних чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> <target>Імпорт бази даних чату</target> @@ -1066,26 +1512,81 @@ <target>Чат зупинено</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <target>Чат зупинено. Якщо ви вже використовували цю базу даних на іншому пристрої, перенесіть її назад перед запуском чату.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve"> + <source>Chat list</source> + <target>Список чатів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve"> + <source>Chat migrated!</source> + <target>Чат перемістився!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> <target>Налаштування чату</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve"> + <source>Chat preferences were changed.</source> + <target>Змінено налаштування чату.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve"> + <source>Chat profile</source> + <target>Профіль користувача</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve"> + <source>Chat theme</source> + <target>Тема чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <target>Чат буде видалено для всіх учасників - цю дію неможливо скасувати!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <target>Чат буде видалено для вас - цю дію неможливо скасувати!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> <target>Чати</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve"> + <source>Check messages every 20 min.</source> + <target>Перевіряйте повідомлення кожні 20 хв.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve"> + <source>Check messages when allowed.</source> + <target>Перевірте повідомлення, коли це дозволено.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> <target>Перевірте адресу сервера та спробуйте ще раз.</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> <target>Інтерфейс китайською та іспанською мовами</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <target>Виберіть _Перемістити з іншого пристрою_ на новому пристрої та відскануйте QR-код.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> <target>Виберіть файл</target> @@ -1096,10 +1597,25 @@ <target>Виберіть з бібліотеки</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve"> + <source>Chunks deleted</source> + <target>Фрагменти видалено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve"> + <source>Chunks downloaded</source> + <target>Завантажено фрагменти</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve"> + <source>Chunks uploaded</source> + <target>Завантажено фрагменти</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> <target>Чисто</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> @@ -1111,16 +1627,38 @@ <target>Відверта розмова?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve"> + <source>Clear group?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve"> + <source>Clear or delete group?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve"> + <source>Clear private notes?</source> + <target>Чисті приватні нотатки?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear verification" xml:space="preserve"> <source>Clear verification</source> <target>Очистити перевірку</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> - <source>Colors</source> - <target>Кольори</target> + <trans-unit id="Color chats with the new themes." xml:space="preserve"> + <source>Color chats with the new themes.</source> + <target>Кольорові чати з новими темами.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Color mode" xml:space="preserve"> + <source>Color mode</source> + <target>Колірний режим</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve"> + <source>Community guidelines violation</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Compare file" xml:space="preserve"> <source>Compare file</source> <target>Порівняти файл</target> @@ -1131,11 +1669,55 @@ <target>Порівняйте коди безпеки зі своїми контактами.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Completed" xml:space="preserve"> + <source>Completed</source> + <target>Завершено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve"> + <source>Conditions accepted on: %@.</source> + <target>Умови приймаються на: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <target>Для оператора(ів) приймаються умови: **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <target>Умови вже прийняті для наступних операторів: **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve"> + <source>Conditions of use</source> + <target>Умови використання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <target>Для оператора(ів) приймаються умови: **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve"> + <source>Conditions will be accepted on: %@.</source> + <target>Умови приймаються на: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <target>Умови будуть автоматично прийняті для увімкнених операторів на: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> <target>Налаштування серверів ICE</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Configure server operators" xml:space="preserve"> + <source>Configure server operators</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> <target>Підтвердити</target> @@ -1146,11 +1728,26 @@ <target>Підтвердити пароль</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve"> + <source>Confirm contact deletion?</source> + <target>Підтвердити видалення контакту?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm database upgrades" xml:space="preserve"> <source>Confirm database upgrades</source> <target>Підтвердити оновлення бази даних</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve"> + <source>Confirm files from unknown servers.</source> + <target>Підтвердити файли з невідомих серверів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve"> + <source>Confirm network settings</source> + <target>Підтвердьте налаштування мережі</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> <target>Підтвердіть нову парольну фразу…</target> @@ -1161,6 +1758,20 @@ <target>Підтвердити пароль</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <target>Переконайтеся, що ви пам'ятаєте пароль до бази даних для її перенесення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve"> + <source>Confirm upload</source> + <target>Підтвердити завантаження</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve"> + <source>Confirmed</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> <target>Підключіться</target> @@ -1168,6 +1779,7 @@ </trans-unit> <trans-unit id="Connect automatically" xml:space="preserve"> <source>Connect automatically</source> + <target>Підключення автоматично</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect incognito" xml:space="preserve"> @@ -1177,24 +1789,36 @@ </trans-unit> <trans-unit id="Connect to desktop" xml:space="preserve"> <source>Connect to desktop</source> + <target>Підключення до комп'ютера</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve"> + <source>Connect to your friends faster.</source> + <target>Швидше спілкуйтеся з друзями.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect to yourself?" xml:space="preserve"> <source>Connect to yourself?</source> + <target>З'єднатися з самим собою?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect to yourself? This is your own SimpleX address!" xml:space="preserve"> <source>Connect to yourself? This is your own SimpleX address!</source> + <target>З'єднатися з самим собою? +Це ваша власна SimpleX-адреса!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect to yourself? This is your own one-time link!" xml:space="preserve"> <source>Connect to yourself? This is your own one-time link!</source> + <target>Підключитися до себе? +Це ваше власне одноразове посилання!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via contact address" xml:space="preserve"> <source>Connect via contact address</source> + <target>Підключіться за контактною адресою</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via link" xml:space="preserve"> @@ -1202,11 +1826,6 @@ This is your own one-time link!</source> <target>Підключіться за посиланням</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve"> - <source>Connect via link / QR code</source> - <target>Підключитися за посиланням / QR-кодом</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connect via one-time link" xml:space="preserve"> <source>Connect via one-time link</source> <target>Під'єднатися за одноразовим посиланням</target> @@ -1214,14 +1833,32 @@ This is your own one-time link!</source> </trans-unit> <trans-unit id="Connect with %@" xml:space="preserve"> <source>Connect with %@</source> + <target>Підключитися до %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connected" xml:space="preserve"> + <source>Connected</source> + <target>Підключено</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connected desktop" xml:space="preserve"> <source>Connected desktop</source> + <target>Підключений робочий стіл</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve"> + <source>Connected servers</source> + <target>Підключені сервери</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connected to desktop" xml:space="preserve"> <source>Connected to desktop</source> + <target>Підключено до настільного комп'ютера</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connecting" xml:space="preserve"> + <source>Connecting</source> + <target>Підключення</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> @@ -1234,8 +1871,14 @@ This is your own one-time link!</source> <target>Підключення до сервера... (помилка: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve"> + <source>Connecting to contact, please wait or check later!</source> + <target>З'єднання з контактом, будь ласка, зачекайте або перевірте пізніше!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting to desktop" xml:space="preserve"> <source>Connecting to desktop</source> + <target>Підключення до ПК</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection" xml:space="preserve"> @@ -1243,6 +1886,15 @@ This is your own one-time link!</source> <target>Підключення</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve"> + <source>Connection and servers status.</source> + <target>Стан з'єднання та серверів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve"> + <source>Connection blocked</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> <target>Помилка підключення</target> @@ -1253,13 +1905,37 @@ This is your own one-time link!</source> <target>Помилка підключення (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve"> + <source>Connection is blocked by server operator: +%@</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve"> + <source>Connection not ready.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve"> + <source>Connection notifications</source> + <target>Сповіщення про підключення</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Запит на підключення відправлено!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve"> + <source>Connection requires encryption renegotiation.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve"> + <source>Connection security</source> + <target>Безпека з'єднання</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection terminated" xml:space="preserve"> <source>Connection terminated</source> + <target>З'єднання розірвано</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection timeout" xml:space="preserve"> @@ -1267,6 +1943,16 @@ This is your own one-time link!</source> <target>Тайм-аут з'єднання</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve"> + <source>Connection with desktop stopped</source> + <target>Припинено зв'язок з робочим столом</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve"> + <source>Connections</source> + <target>З'єднання</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact allows" xml:space="preserve"> <source>Contact allows</source> <target>Контакт дозволяє</target> @@ -1277,6 +1963,11 @@ This is your own one-time link!</source> <target>Контакт вже існує</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact deleted!" xml:space="preserve"> + <source>Contact deleted!</source> + <target>Контакт видалено!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> <target>Контакт приховано:</target> @@ -1287,9 +1978,9 @@ This is your own one-time link!</source> <target>Контакт підключений</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve"> - <source>Contact is not connected yet!</source> - <target>Контакт ще не підключено!</target> + <trans-unit id="Contact is deleted." xml:space="preserve"> + <source>Contact is deleted.</source> + <target>Контакт видалено.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact name" xml:space="preserve"> @@ -1302,6 +1993,11 @@ This is your own one-time link!</source> <target>Налаштування контактів</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve"> + <source>Contact will be deleted - this cannot be undone!</source> + <target>Контакт буде видалено - це неможливо скасувати!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> <target>Контакти</target> @@ -1312,23 +2008,43 @@ This is your own one-time link!</source> <target>Контакти можуть позначати повідомлення для видалення; ви зможете їх переглянути.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve"> + <source>Content violates conditions of use</source> + <note>blocking reason</note> + </trans-unit> <trans-unit id="Continue" xml:space="preserve"> <source>Continue</source> <target>Продовжуйте</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve"> + <source>Conversation deleted!</source> + <target>Розмова видалена!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> <target>Копіювати</target> - <note>chat item action</note> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve"> + <source>Copy error</source> + <target>Помилка копіювання</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> <target>Основна версія: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Corner" xml:space="preserve"> + <source>Corner</source> + <target>Кут</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Correct name to %@?" xml:space="preserve"> <source>Correct name to %@?</source> + <target>Виправити ім'я на %@?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create" xml:space="preserve"> @@ -1336,6 +2052,11 @@ This is your own one-time link!</source> <target>Створити</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve"> + <source>Create 1-time link</source> + <target>Створити одноразове посилання</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> <target>Створіть адресу SimpleX</target> @@ -1343,11 +2064,7 @@ This is your own one-time link!</source> </trans-unit> <trans-unit id="Create a group using a random profile." xml:space="preserve"> <source>Create a group using a random profile.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> - <source>Create an address to let people connect with you.</source> - <target>Створіть адресу, щоб люди могли з вами зв'язатися.</target> + <target>Створіть групу, використовуючи випадковий профіль.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create file" xml:space="preserve"> @@ -1357,6 +2074,7 @@ This is your own one-time link!</source> </trans-unit> <trans-unit id="Create group" xml:space="preserve"> <source>Create group</source> + <target>Створити групу</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create group link" xml:space="preserve"> @@ -1369,17 +2087,18 @@ This is your own one-time link!</source> <target>Створити посилання</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> - <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <trans-unit id="Create list" xml:space="preserve"> + <source>Create list</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve"> - <source>Create one-time invitation link</source> - <target>Створіть одноразове посилання-запрошення</target> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <target>Створіть новий профіль у [desktop app](https://simplex.chat/downloads/). 💻</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create profile" xml:space="preserve"> <source>Create profile</source> + <target>Створити профіль</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create queue" xml:space="preserve"> @@ -1397,9 +2116,29 @@ This is your own one-time link!</source> <target>Створіть свій профіль</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve"> - <source>Created on %@</source> - <target>Створено %@</target> + <trans-unit id="Created" xml:space="preserve"> + <source>Created</source> + <target>Створено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve"> + <source>Created at</source> + <target>Створено за адресою</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve"> + <source>Created at: %@</source> + <target>Створено за адресою: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve"> + <source>Creating archive link</source> + <target>Створення архівного посилання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve"> + <source>Creating link…</source> + <target>Створення посилання…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current Passcode" xml:space="preserve"> @@ -1407,11 +2146,21 @@ This is your own one-time link!</source> <target>Поточний пароль</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <target>Текст поточних умов не вдалося завантажити, ви можете переглянути умови за цим посиланням:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> <target>Поточна парольна фраза…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current profile" xml:space="preserve"> + <source>Current profile</source> + <target>Поточний профіль</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> <target>Наразі максимальний підтримуваний розмір файлу - %@.</target> @@ -1422,11 +2171,26 @@ This is your own one-time link!</source> <target>Індивідуальний час</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve"> + <source>Customizable message shape.</source> + <target>Налаштовується форма повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve"> + <source>Customize theme</source> + <target>Налаштувати тему</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> <target>Темний</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve"> + <source>Dark mode colors</source> + <target>Кольори темного режиму</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> <target>Ідентифікатор бази даних</target> @@ -1525,6 +2289,11 @@ This is your own one-time link!</source> <target>База даних буде перенесена під час перезапуску програми</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Debug delivery" xml:space="preserve"> + <source>Debug delivery</source> + <target>Доставка налагодження</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> <target>Децентралізований</target> @@ -1538,15 +2307,17 @@ This is your own one-time link!</source> <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> <target>Видалити</target> - <note>chat item action</note> + <note>alert action +swipe action</note> + </trans-unit> + <trans-unit id="Delete %lld messages of members?" xml:space="preserve"> + <source>Delete %lld messages of members?</source> + <target>Видалити %lld повідомлень користувачів?</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete %lld messages?" xml:space="preserve"> <source>Delete %lld messages?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve"> - <source>Delete Contact</source> - <target>Видалити контакт</target> + <target>Видалити %lld повідомлень?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete address" xml:space="preserve"> @@ -1571,16 +2342,16 @@ This is your own one-time link!</source> </trans-unit> <trans-unit id="Delete and notify contact" xml:space="preserve"> <source>Delete and notify contact</source> + <target>Видалити та повідомити контакт</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve"> - <source>Delete archive</source> - <target>Видалити архів</target> + <trans-unit id="Delete chat" xml:space="preserve"> + <source>Delete chat</source> + <target>Видалити чат</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve"> - <source>Delete chat archive?</source> - <target>Видалити архів чату?</target> + <trans-unit id="Delete chat messages from your device." xml:space="preserve"> + <source>Delete chat messages from your device.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile" xml:space="preserve"> @@ -1593,6 +2364,11 @@ This is your own one-time link!</source> <target>Видалити профіль чату?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete chat?" xml:space="preserve"> + <source>Delete chat?</source> + <target>Видалити чат?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> <source>Delete connection</source> <target>Видалити підключення</target> @@ -1603,9 +2379,9 @@ This is your own one-time link!</source> <target>Видалити контакт</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact? This cannot be undone!" xml:space="preserve"> - <source>Delete contact? -This cannot be undone!</source> + <trans-unit id="Delete contact?" xml:space="preserve"> + <source>Delete contact?</source> + <target>Видалити контакт?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete database" xml:space="preserve"> @@ -1613,6 +2389,11 @@ This cannot be undone!</source> <target>Видалити базу даних</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete database from this device" xml:space="preserve"> + <source>Delete database from this device</source> + <target>Видалити базу даних з цього пристрою</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete file" xml:space="preserve"> <source>Delete file</source> <target>Видалити файл</target> @@ -1663,6 +2444,10 @@ This cannot be undone!</source> <target>Видалити посилання?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete list?" xml:space="preserve"> + <source>Delete list?</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> <target>Видалити повідомлення учасника?</target> @@ -1676,7 +2461,7 @@ This cannot be undone!</source> <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> <target>Видалити повідомлення</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> @@ -1693,9 +2478,9 @@ This cannot be undone!</source> <target>Видалити стару базу даних?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve"> - <source>Delete pending connection</source> - <target>Видалити очікуване з'єднання</target> + <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> + <source>Delete or moderate up to 200 messages.</source> + <target>Видалити або модерувати до 200 повідомлень.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection?" xml:space="preserve"> @@ -1713,11 +2498,30 @@ This cannot be undone!</source> <target>Видалити чергу</target> <note>server test step</note> </trans-unit> + <trans-unit id="Delete report" xml:space="preserve"> + <source>Delete report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete up to 20 messages at once." xml:space="preserve"> + <source>Delete up to 20 messages at once.</source> + <target>Видаляйте до 20 повідомлень одночасно.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> <target>Видалити профіль користувача?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete without notification" xml:space="preserve"> + <source>Delete without notification</source> + <target>Видалення без попередження</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve"> + <source>Deleted</source> + <target>Видалено</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Deleted at" xml:space="preserve"> <source>Deleted at</source> <target>Видалено за</target> @@ -1728,6 +2532,16 @@ This cannot be undone!</source> <target>Видалено за: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve"> + <source>Deletion errors</source> + <target>Помилки видалення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivered even when Apple drops them." xml:space="preserve"> + <source>Delivered even when Apple drops them.</source> + <target>Доставляються навіть тоді, коли Apple кидає їх.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery" xml:space="preserve"> <source>Delivery</source> <target>Доставка</target> @@ -1750,14 +2564,42 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Desktop address" xml:space="preserve"> <source>Desktop address</source> + <target>Адреса робочого столу</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve"> <source>Desktop app version %@ is not compatible with this app.</source> + <target>Версія програми для настільних комп'ютерів %@ не сумісна з цією програмою.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Desktop devices" xml:space="preserve"> <source>Desktop devices</source> + <target>Настільні пристрої</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve"> + <source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source> + <target>Адреса сервера призначення %@ несумісна з налаштуваннями сервера пересилання %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server error: %@" xml:space="preserve"> + <source>Destination server error: %@</source> + <target>Помилка сервера призначення: %@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve"> + <source>Destination server version of %@ is incompatible with forwarding server %@.</source> + <target>Версія сервера призначення %@ несумісна з версією сервера переадресації %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Detailed statistics" xml:space="preserve"> + <source>Detailed statistics</source> + <target>Детальна статистика</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Details" xml:space="preserve"> + <source>Details</source> + <target>Деталі</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Develop" xml:space="preserve"> @@ -1765,6 +2607,11 @@ This cannot be undone!</source> <target>Розробник</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Developer options" xml:space="preserve"> + <source>Developer options</source> + <target>Можливості для розробників</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> <target>Інструменти для розробників</target> @@ -1795,8 +2642,13 @@ This cannot be undone!</source> <target>Прямі повідомлення</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve"> + <source>Direct messages between members are prohibited in this chat.</source> + <target>У цьому чаті заборонені прямі повідомлення між учасниками.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> <target>У цій групі заборонені прямі повідомлення між учасниками.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1810,11 +2662,24 @@ This cannot be undone!</source> <target>Вимкнути SimpleX Lock</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Disable automatic message deletion?" xml:space="preserve"> + <source>Disable automatic message deletion?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Disable delete messages" xml:space="preserve"> + <source>Disable delete messages</source> + <note>alert button</note> + </trans-unit> <trans-unit id="Disable for all" xml:space="preserve"> <source>Disable for all</source> <target>Вимкнути для всіх</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Disabled" xml:space="preserve"> + <source>Disabled</source> + <target>Вимкнено</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> <source>Disappearing message</source> <target>Зникаюче повідомлення</target> @@ -1830,8 +2695,8 @@ This cannot be undone!</source> <target>Зникаючі повідомлення в цьому чаті заборонені.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <target>У цій групі заборонено зникаючі повідомлення.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1852,14 +2717,22 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Disconnect desktop?" xml:space="preserve"> <source>Disconnect desktop?</source> + <target>Відключити робочий стіл?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Discover and join groups" xml:space="preserve"> <source>Discover and join groups</source> + <target>Знаходьте та приєднуйтесь до груп</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Discover via local network" xml:space="preserve"> <source>Discover via local network</source> + <target>Відкриття через локальну мережу</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve"> + <source>Do NOT send messages directly, even if your or destination server does not support private routing.</source> + <target>НЕ надсилайте повідомлення напряму, навіть якщо ваш сервер або сервер призначення не підтримує приватну маршрутизацію.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> @@ -1867,11 +2740,30 @@ This cannot be undone!</source> <target>НЕ використовуйте SimpleX для екстрених викликів.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT use private routing." xml:space="preserve"> + <source>Do NOT use private routing.</source> + <target>НЕ використовуйте приватну маршрутизацію.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do it later" xml:space="preserve"> <source>Do it later</source> <target>Зробіть це пізніше</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do not send history to new members." xml:space="preserve"> + <source>Do not send history to new members.</source> + <target>Не надсилайте історію новим користувачам.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not use credentials with proxy." xml:space="preserve"> + <source>Do not use credentials with proxy.</source> + <target>Не використовуйте облікові дані з проксі.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Documents:" xml:space="preserve"> + <source>Documents:</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> <target>Не створювати адресу</target> @@ -1882,21 +2774,70 @@ This cannot be undone!</source> <target>Не вмикати</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Don't miss important messages." xml:space="preserve"> + <source>Don't miss important messages.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> <target>Більше не показувати</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Done" xml:space="preserve"> + <source>Done</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Downgrade and open chat" xml:space="preserve"> <source>Downgrade and open chat</source> <target>Пониження та відкритий чат</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Download" xml:space="preserve"> + <source>Download</source> + <target>Завантажити</target> + <note>alert button +chat item action</note> + </trans-unit> + <trans-unit id="Download errors" xml:space="preserve"> + <source>Download errors</source> + <target>Помилки завантаження</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download failed" xml:space="preserve"> + <source>Download failed</source> + <target>Не вдалося завантажити</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Download file" xml:space="preserve"> <source>Download file</source> <target>Завантажити файл</target> <note>server test step</note> </trans-unit> + <trans-unit id="Download files" xml:space="preserve"> + <source>Download files</source> + <target>Завантажити файли</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Downloaded" xml:space="preserve"> + <source>Downloaded</source> + <target>Завантажено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloaded files" xml:space="preserve"> + <source>Downloaded files</source> + <target>Завантажені файли</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading archive" xml:space="preserve"> + <source>Downloading archive</source> + <target>Завантажити архів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading link details" xml:space="preserve"> + <source>Downloading link details</source> + <target>Деталі посилання для завантаження</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> <target>Дублююче ім'я користувача!</target> @@ -1907,6 +2848,11 @@ This cannot be undone!</source> <target>Тривалість</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="E2E encrypted notifications." xml:space="preserve"> + <source>E2E encrypted notifications.</source> + <target>Зашифровані сповіщення E2E.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> <target>Редагувати</target> @@ -1927,6 +2873,10 @@ This cannot be undone!</source> <target>Увімкнути (зберегти перевизначення)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve"> + <source>Enable Flux in Network & servers settings for better metadata privacy.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> <target>Увімкнути SimpleX Lock</target> @@ -1940,6 +2890,11 @@ This cannot be undone!</source> <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> <target>Увімкнути автоматичне видалення повідомлень?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Enable camera access" xml:space="preserve"> + <source>Enable camera access</source> + <target>Увімкніть доступ до камери</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable for all" xml:space="preserve"> @@ -1947,6 +2902,11 @@ This cannot be undone!</source> <target>Увімкнути для всіх</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve"> + <source>Enable in direct chats (BETA)!</source> + <target>Увімкнути в прямих чатах (BETA)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> <target>Увімкнути миттєві сповіщення?</target> @@ -1977,6 +2937,16 @@ This cannot be undone!</source> <target>Увімкнути пароль самознищення</target> <note>set passcode view</note> </trans-unit> + <trans-unit id="Enabled" xml:space="preserve"> + <source>Enabled</source> + <target>Увімкнено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve"> + <source>Enabled for</source> + <target>Увімкнено для</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> <target>Зашифрувати</target> @@ -1989,10 +2959,12 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Encrypt local files" xml:space="preserve"> <source>Encrypt local files</source> + <target>Шифрування локальних файлів</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Encrypt stored files & media" xml:space="preserve"> <source>Encrypt stored files & media</source> + <target>Шифрування збережених файлів і носіїв</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Encrypted database" xml:space="preserve"> @@ -2005,6 +2977,11 @@ This cannot be undone!</source> <target>Зашифроване повідомлення або інша подія</target> <note>notification</note> </trans-unit> + <trans-unit id="Encrypted message: app is stopped" xml:space="preserve"> + <source>Encrypted message: app is stopped</source> + <target>Зашифроване повідомлення: додаток зупинено</target> + <note>notification</note> + </trans-unit> <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> <target>Зашифроване повідомлення: помилка бази даних</target> @@ -2032,10 +3009,16 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Encryption re-negotiation error" xml:space="preserve"> <source>Encryption re-negotiation error</source> + <target>Помилка повторного узгодження шифрування</target> <note>message decrypt error item</note> </trans-unit> <trans-unit id="Encryption re-negotiation failed." xml:space="preserve"> <source>Encryption re-negotiation failed.</source> + <target>Повторне узгодження шифрування не вдалося.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encryption renegotiation in progress." xml:space="preserve"> + <source>Encryption renegotiation in progress.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter Passcode" xml:space="preserve"> @@ -2050,6 +3033,12 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Enter group name…" xml:space="preserve"> <source>Enter group name…</source> + <target>Введіть назву групи…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve"> + <source>Enter passphrase</source> + <target>Введіть парольну фразу</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter passphrase…" xml:space="preserve"> @@ -2069,6 +3058,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Enter this device name…" xml:space="preserve"> <source>Enter this device name…</source> + <target>Введіть назву пристрою…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter welcome message…" xml:space="preserve"> @@ -2083,6 +3073,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Enter your name…" xml:space="preserve"> <source>Enter your name…</source> + <target>Введіть своє ім'я…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error" xml:space="preserve"> @@ -2095,26 +3086,36 @@ This cannot be undone!</source> <target>Помилка скасування зміни адреси</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error accepting conditions" xml:space="preserve"> + <source>Error accepting conditions</source> + <target>Помилка прийняття умов</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> <target>Помилка при прийнятті запиту на контакт</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve"> - <source>Error accessing database file</source> - <target>Помилка доступу до файлу бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> <target>Помилка додавання користувача(ів)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error adding server" xml:space="preserve"> + <source>Error adding server</source> + <target>Помилка додавання сервера</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> <target>Помилка зміни адреси</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing connection profile" xml:space="preserve"> + <source>Error changing connection profile</source> + <target>Помилка при зміні профілю з'єднання</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error changing role" xml:space="preserve"> <source>Error changing role</source> <target>Помилка зміни ролі</target> @@ -2125,6 +3126,20 @@ This cannot be undone!</source> <target>Помилка зміни налаштування</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing to incognito!" xml:space="preserve"> + <source>Error changing to incognito!</source> + <target>Помилка переходу на інкогніто!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error checking token status" xml:space="preserve"> + <source>Error checking token status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve"> + <source>Error connecting to forwarding server %@. Please try later.</source> + <target>Помилка підключення до сервера переадресації %@. Спробуйте пізніше.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> <target>Помилка створення адреси</target> @@ -2140,8 +3155,18 @@ This cannot be undone!</source> <target>Помилка створення посилання на групу</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating list" xml:space="preserve"> + <source>Error creating list</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error creating member contact" xml:space="preserve"> <source>Error creating member contact</source> + <target>Помилка при створенні контакту користувача</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating message" xml:space="preserve"> + <source>Error creating message</source> + <target>Повідомлення про створення помилки</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> @@ -2149,8 +3174,13 @@ This cannot be undone!</source> <target>Помилка створення профілю!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating report" xml:space="preserve"> + <source>Error creating report</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error decrypting file" xml:space="preserve"> <source>Error decrypting file</source> + <target>Помилка розшифрування файлу</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting chat database" xml:space="preserve"> @@ -2168,11 +3198,6 @@ This cannot be undone!</source> <target>Помилка видалення з'єднання</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve"> - <source>Error deleting contact</source> - <target>Помилка видалення контакту</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> <target>Помилка видалення бази даних</target> @@ -2193,6 +3218,11 @@ This cannot be undone!</source> <target>Помилка видалення профілю користувача</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error downloading the archive" xml:space="preserve"> + <source>Error downloading the archive</source> + <target>Помилка завантаження архіву</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> <source>Error enabling delivery receipts!</source> <target>Помилка активації підтвердження доставлення!</target> @@ -2213,6 +3243,11 @@ This cannot be undone!</source> <target>Помилка експорту бази даних чату</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error exporting theme: %@" xml:space="preserve"> + <source>Error exporting theme: %@</source> + <target>Помилка експорту теми: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> <target>Помилка імпорту бази даних чату</target> @@ -2223,24 +3258,52 @@ This cannot be undone!</source> <target>Помилка приєднання до групи</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve"> - <source>Error loading %@ servers</source> - <target>Помилка завантаження %@ серверів</target> + <trans-unit id="Error loading servers" xml:space="preserve"> + <source>Error loading servers</source> + <target>Помилка завантаження серверів</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error migrating settings" xml:space="preserve"> + <source>Error migrating settings</source> + <target>Помилка міграції налаштувань</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error opening chat" xml:space="preserve"> + <source>Error opening chat</source> + <target>Помилка відкриття чату</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> <target>Помилка отримання файлу</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error reconnecting server" xml:space="preserve"> + <source>Error reconnecting server</source> + <target>Помилка перепідключення сервера</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error reconnecting servers" xml:space="preserve"> + <source>Error reconnecting servers</source> + <target>Помилка перепідключення серверів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error registering for notifications" xml:space="preserve"> + <source>Error registering for notifications</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> <target>Помилка видалення учасника</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve"> - <source>Error saving %@ servers</source> - <target>Помилка збереження %@ серверів</target> + <trans-unit id="Error reordering lists" xml:space="preserve"> + <source>Error reordering lists</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error resetting statistics" xml:space="preserve"> + <source>Error resetting statistics</source> + <target>Статистика скидання помилок</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving ICE servers" xml:space="preserve"> @@ -2248,6 +3311,10 @@ This cannot be undone!</source> <target>Помилка збереження серверів ICE</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving chat list" xml:space="preserve"> + <source>Error saving chat list</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> <target>Помилка збереження профілю групи</target> @@ -2263,11 +3330,26 @@ This cannot be undone!</source> <target>Помилка збереження пароля на keychain</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving servers" xml:space="preserve"> + <source>Error saving servers</source> + <target>Сервери збереження помилок</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error saving settings" xml:space="preserve"> + <source>Error saving settings</source> + <target>Налаштування збереження помилок</target> + <note>when migrating</note> + </trans-unit> <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> <target>Помилка збереження пароля користувача</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error scanning code: %@" xml:space="preserve"> + <source>Error scanning code: %@</source> + <target>Код помилки сканування: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending email" xml:space="preserve"> <source>Error sending email</source> <target>Помилка надсилання електронного листа</target> @@ -2275,6 +3357,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Error sending member contact invitation" xml:space="preserve"> <source>Error sending member contact invitation</source> + <target>Помилка надсилання запрошення до контактів учасника</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error sending message" xml:space="preserve"> @@ -2297,16 +3380,25 @@ This cannot be undone!</source> <target>Помилка зупинки чату</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error switching profile" xml:space="preserve"> + <source>Error switching profile</source> + <target>Помилка перемикання профілю</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> <target>Помилка перемикання профілю!</target> - <note>No comment provided by engineer.</note> + <note>alertTitle</note> </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> <target>Помилка синхронізації з'єднання</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error testing server connection" xml:space="preserve"> + <source>Error testing server connection</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> <target>Помилка оновлення посилання на групу</target> @@ -2317,6 +3409,11 @@ This cannot be undone!</source> <target>Повідомлення про помилку оновлення</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error updating server" xml:space="preserve"> + <source>Error updating server</source> + <target>Помилка оновлення сервера</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> <target>Помилка оновлення налаштувань</target> @@ -2327,6 +3424,16 @@ This cannot be undone!</source> <target>Помилка оновлення конфіденційності користувача</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error uploading the archive" xml:space="preserve"> + <source>Error uploading the archive</source> + <target>Помилка при завантаженні архіву</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error verifying passphrase:" xml:space="preserve"> + <source>Error verifying passphrase:</source> + <target>Помилка при перевірці парольної фрази:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error: " xml:space="preserve"> <source>Error: </source> <target>Помилка: </target> @@ -2335,7 +3442,9 @@ This cannot be undone!</source> <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> <target>Помилка: %@</target> - <note>No comment provided by engineer.</note> + <note>alert message +file error text +snd error text</note> </trans-unit> <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> @@ -2347,6 +3456,16 @@ This cannot be undone!</source> <target>Помилка: немає файлу бази даних</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Errors" xml:space="preserve"> + <source>Errors</source> + <target>Помилки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors in servers configuration." xml:space="preserve"> + <source>Errors in servers configuration.</source> + <target>Помилки в конфігурації серверів.</target> + <note>servers error</note> + </trans-unit> <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> <source>Even when disabled in the conversation.</source> <target>Навіть коли вимкнений у розмові.</target> @@ -2359,8 +3478,13 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Expand" xml:space="preserve"> <source>Expand</source> + <target>Розгорнути</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Expired" xml:space="preserve"> + <source>Expired</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> <target>Експорт бази даних</target> @@ -2371,11 +3495,21 @@ This cannot be undone!</source> <target>Помилка експорту:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Export theme" xml:space="preserve"> + <source>Export theme</source> + <target>Тема експорту</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> <target>Експортований архів бази даних.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Exported file doesn't exist" xml:space="preserve"> + <source>Exported file doesn't exist</source> + <target>Експортований файл не існує</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exporting database archive…" xml:space="preserve"> <source>Exporting database archive…</source> <target>Експорт архіву бази даних…</target> @@ -2391,15 +3525,65 @@ This cannot be undone!</source> <target>Швидко і без очікування, поки відправник буде онлайн!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster deletion of groups." xml:space="preserve"> + <source>Faster deletion of groups.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Faster joining and more reliable messages." xml:space="preserve"> <source>Faster joining and more reliable messages.</source> + <target>Швидше приєднання та надійніші повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Faster sending messages." xml:space="preserve"> + <source>Faster sending messages.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Favorite" xml:space="preserve"> <source>Favorite</source> <target>Улюблений</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Favorites" xml:space="preserve"> + <source>Favorites</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Помилка файлу</target> + <note>file error alert title</note> + </trans-unit> + <trans-unit id="File errors: %@" xml:space="preserve"> + <source>File errors: +%@</source> + <target>Помилки файлів: +%@</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="File is blocked by server operator: %@." xml:space="preserve"> + <source>File is blocked by server operator: +%@.</source> + <note>file error text</note> + </trans-unit> + <trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve"> + <source>File not found - most likely file was deleted or cancelled.</source> + <target>Файл не знайдено - найімовірніше, файл було видалено або скасовано.</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File server error: %@" xml:space="preserve"> + <source>File server error: %@</source> + <target>Помилка файлового сервера: %@</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File status" xml:space="preserve"> + <source>File status</source> + <target>Статус файлу</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File status: %@" xml:space="preserve"> + <source>File status: %@</source> + <target>Статус файлу: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="File will be deleted from servers." xml:space="preserve"> <source>File will be deleted from servers.</source> <target>Файл буде видалено з серверів.</target> @@ -2420,6 +3604,11 @@ This cannot be undone!</source> <target>Файл: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files" xml:space="preserve"> + <source>Files</source> + <target>Файли</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> <target>Файли та медіа</target> @@ -2430,11 +3619,16 @@ This cannot be undone!</source> <target>Файли і медіа</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> - <source>Files and media are prohibited in this group.</source> + <trans-unit id="Files and media are prohibited." xml:space="preserve"> + <source>Files and media are prohibited.</source> <target>Файли та медіа в цій групі заборонені.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files and media not allowed" xml:space="preserve"> + <source>Files and media not allowed</source> + <target>Файли та медіафайли заборонені</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files and media prohibited!" xml:space="preserve"> <source>Files and media prohibited!</source> <target>Файли та медіа заборонені!</target> @@ -2445,6 +3639,16 @@ This cannot be undone!</source> <target>Фільтруйте непрочитані та улюблені чати.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Finalize migration" xml:space="preserve"> + <source>Finalize migration</source> + <target>Завершити міграцію</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration on another device." xml:space="preserve"> + <source>Finalize migration on another device.</source> + <target>Завершіть міграцію на іншому пристрої.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> <target>Нарешті, вони у нас є! 🚀</target> @@ -2485,13 +3689,116 @@ This cannot be undone!</source> <target>Виправлення не підтримується учасником групи</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For all moderators" xml:space="preserve"> + <source>For all moderators</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For chat profile %@:" xml:space="preserve"> + <source>For chat profile %@:</source> + <target>Для профілю чату %@:</target> + <note>servers error</note> + </trans-unit> <trans-unit id="For console" xml:space="preserve"> <source>For console</source> <target>Для консолі</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." xml:space="preserve"> + <source>For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</source> + <target>Наприклад, якщо ваш контакт отримує повідомлення через сервер SimpleX Chat, ваш додаток доставлятиме їх через сервер Flux.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For me" xml:space="preserve"> + <source>For me</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For private routing" xml:space="preserve"> + <source>For private routing</source> + <target>Для приватної маршрутизації</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For social media" xml:space="preserve"> + <source>For social media</source> + <target>Для соціальних мереж</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward" xml:space="preserve"> + <source>Forward</source> + <target>Пересилання</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Forward %d message(s)?" xml:space="preserve"> + <source>Forward %d message(s)?</source> + <target>Переслати %d повідомлення(ь)?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Forward and save messages" xml:space="preserve"> + <source>Forward and save messages</source> + <target>Пересилання та збереження повідомлень</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward messages" xml:space="preserve"> + <source>Forward messages</source> + <target>Пересилання повідомлень</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Forward messages without files?" xml:space="preserve"> + <source>Forward messages without files?</source> + <target>Пересилати повідомлення без файлів?</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> + <source>Forward up to 20 messages at once.</source> + <target>Пересилайте до 20 повідомлень одночасно.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded" xml:space="preserve"> + <source>Forwarded</source> + <target>Переслано</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded from" xml:space="preserve"> + <source>Forwarded from</source> + <target>Переслано з</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding %lld messages" xml:space="preserve"> + <source>Forwarding %lld messages</source> + <target>Пересилання повідомлень %lld</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve"> + <source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source> + <target>Серверу переадресації %@ не вдалося з'єднатися з сервером призначення %@. Спробуйте пізніше.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server address is incompatible with network settings: %@.</source> + <target>Адреса сервера переадресації несумісна з налаштуваннями мережі: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server version is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server version is incompatible with network settings: %@.</source> + <target>Версія сервера переадресації несумісна з мережевими налаштуваннями: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Destination server error: %2$@</source> + <target>Сервер переадресації: %1$@ +Помилка сервера призначення: %2$@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Error: %2$@</source> + <target>Сервер переадресації: %1$@ +Помилка: %2$@</target> + <note>snd error text</note> + </trans-unit> <trans-unit id="Found desktop" xml:space="preserve"> <source>Found desktop</source> + <target>Знайдено робочий стіл</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="French interface" xml:space="preserve"> @@ -2509,13 +3816,9 @@ This cannot be undone!</source> <target>Повне ім'я (необов'язково)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full name:" xml:space="preserve"> - <source>Full name:</source> - <target>Повне ім'я:</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Fully decentralized – visible only to members." xml:space="preserve"> <source>Fully decentralized – visible only to members.</source> + <target>Повністю децентралізована - видима лише для учасників.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fully re-implemented - work in background!" xml:space="preserve"> @@ -2533,6 +3836,20 @@ This cannot be undone!</source> <target>GIF-файли та наклейки</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Get notified when mentioned." xml:space="preserve"> + <source>Get notified when mentioned.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Good afternoon!" xml:space="preserve"> + <source>Good afternoon!</source> + <target>Доброго дня!</target> + <note>message preview</note> + </trans-unit> + <trans-unit id="Good morning!" xml:space="preserve"> + <source>Good morning!</source> + <target>Доброго ранку!</target> + <note>message preview</note> + </trans-unit> <trans-unit id="Group" xml:space="preserve"> <source>Group</source> <target>Група</target> @@ -2540,10 +3857,12 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Group already exists" xml:space="preserve"> <source>Group already exists</source> + <target>Група вже існує</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group already exists!" xml:space="preserve"> <source>Group already exists!</source> + <target>Група вже існує!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group display name" xml:space="preserve"> @@ -2586,36 +3905,6 @@ This cannot be undone!</source> <target>Групові посилання</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve"> - <source>Group members can add message reactions.</source> - <target>Учасники групи можуть додавати реакції на повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> - <source>Group members can irreversibly delete sent messages.</source> - <target>Учасники групи можуть безповоротно видаляти надіслані повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> - <target>Учасники групи можуть надсилати прямі повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> - <target>Учасники групи можуть надсилати зникаючі повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send files and media." xml:space="preserve"> - <source>Group members can send files and media.</source> - <target>Учасники групи можуть надсилати файли та медіа.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> - <target>Учасники групи можуть надсилати голосові повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> <target>Групове повідомлення:</target> @@ -2656,11 +3945,19 @@ This cannot be undone!</source> <target>Група буде видалена для вас - це не може бути скасовано!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Groups" xml:space="preserve"> + <source>Groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Help" xml:space="preserve"> <source>Help</source> <target>Довідка</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Help admins moderating their groups." xml:space="preserve"> + <source>Help admins moderating their groups.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> <target>Приховано</target> @@ -2701,15 +3998,29 @@ This cannot be undone!</source> <target>Історія</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="History is not sent to new members." xml:space="preserve"> + <source>History is not sent to new members.</source> + <target>Історія не надсилається новим учасникам.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> <target>Як працює SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="How it affects privacy" xml:space="preserve"> + <source>How it affects privacy</source> + <target>Як це впливає на конфіденційність</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it helps privacy" xml:space="preserve"> + <source>How it helps privacy</source> + <target>Як це захищає приватність</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> - <target>Як це працює</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="How to" xml:space="preserve"> <source>How to</source> @@ -2726,21 +4037,26 @@ This cannot be undone!</source> <target>Як користуватися вашими серверами</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Hungarian interface" xml:space="preserve"> + <source>Hungarian interface</source> + <target>Інтерфейс угорською мовою</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> <target>Сервери ICE (по одному на лінію)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="IP address" xml:space="preserve"> + <source>IP address</source> + <target>IP-адреса</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> <source>If you can't meet in person, show QR code in a video call, or share the link.</source> <target>Якщо ви не можете зустрітися особисто, покажіть QR-код у відеодзвінку або поділіться посиланням.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <target>Якщо ви не можете зустрітися особисто, ви можете **сканувати QR-код у відеодзвінку**, або ваш контакт може поділитися посиланням на запрошення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> <target>Якщо ви введете цей пароль при відкритті програми, всі дані програми будуть безповоротно видалені!</target> @@ -2776,8 +4092,8 @@ This cannot be undone!</source> <target>Негайно</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <target>Імунітет до спаму та зловживань</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2796,6 +4112,33 @@ This cannot be undone!</source> <target>Імпорт бази даних</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Import failed" xml:space="preserve"> + <source>Import failed</source> + <target>Не вдалося імпортувати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import theme" xml:space="preserve"> + <source>Import theme</source> + <target>Імпорт теми</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Importing archive" xml:space="preserve"> + <source>Importing archive</source> + <target>Імпорт архіву</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved delivery, reduced traffic usage. More improvements are coming soon!" xml:space="preserve"> + <source>Improved delivery, reduced traffic usage. +More improvements are coming soon!</source> + <target>Покращена доставка, зменшене використання трафіку. +Незабаром з'являться нові покращення!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved message delivery" xml:space="preserve"> + <source>Improved message delivery</source> + <target>Покращена доставка повідомлень</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> <target>Покращена конфіденційність та безпека</target> @@ -2806,11 +4149,29 @@ This cannot be undone!</source> <target>Покращена конфігурація сервера</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In order to continue, chat should be stopped." xml:space="preserve"> + <source>In order to continue, chat should be stopped.</source> + <target>Для того, щоб продовжити, чат слід зупинити.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>У відповідь на</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In-call sounds" xml:space="preserve"> + <source>In-call sounds</source> + <target>Звуки вхідного дзвінка</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Inappropriate content" xml:space="preserve"> + <source>Inappropriate content</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Inappropriate profile" xml:space="preserve"> + <source>Inappropriate profile</source> + <note>report reason</note> + </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> <target>Інкогніто</target> @@ -2818,6 +4179,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Incognito groups" xml:space="preserve"> <source>Incognito groups</source> + <target>Групи інкогніто</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito mode" xml:space="preserve"> @@ -2852,6 +4214,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Incompatible version" xml:space="preserve"> <source>Incompatible version</source> + <target>Несумісна версія</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incorrect passcode" xml:space="preserve"> @@ -2879,6 +4242,11 @@ This cannot be undone!</source> <target>Встановіть [SimpleX Chat для терміналу](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Instant" xml:space="preserve"> + <source>Instant</source> + <target>Миттєво</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> @@ -2886,29 +4254,75 @@ This cannot be undone!</source> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Instantly" xml:space="preserve"> - <source>Instantly</source> - <target>Миттєво</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> <target>Інтерфейс</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Interface colors" xml:space="preserve"> + <source>Interface colors</source> + <target>Кольори інтерфейсу</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid" xml:space="preserve"> + <source>Invalid</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (bad token)" xml:space="preserve"> + <source>Invalid (bad token)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (expired)" xml:space="preserve"> + <source>Invalid (expired)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (unregistered)" xml:space="preserve"> + <source>Invalid (unregistered)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (wrong topic)" xml:space="preserve"> + <source>Invalid (wrong topic)</source> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid QR code" xml:space="preserve"> + <source>Invalid QR code</source> + <target>Неправильний QR-код</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> <target>Неправильне посилання для підключення</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid display name!" xml:space="preserve"> + <source>Invalid display name!</source> + <target>Неправильне ім'я користувача!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid link" xml:space="preserve"> + <source>Invalid link</source> + <target>Невірне посилання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Недійсне підтвердження міграції</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid name!" xml:space="preserve"> <source>Invalid name!</source> + <target>Неправильне ім'я!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid response" xml:space="preserve"> + <source>Invalid response</source> + <target>Неправильна відповідь</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> <target>Неправильна адреса сервера!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Invalid status" xml:space="preserve"> <source>Invalid status</source> @@ -2930,6 +4344,11 @@ This cannot be undone!</source> <target>Запросити учасників</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invite to chat" xml:space="preserve"> + <source>Invite to chat</source> + <target>Запросити в чат</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invite to group" xml:space="preserve"> <source>Invite to group</source> <target>Запросити до групи</target> @@ -2945,8 +4364,8 @@ This cannot be undone!</source> <target>У цьому чаті заборонено безповоротне видалення повідомлень.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <target>У цій групі заборонено безповоротне видалення повідомлень.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2971,6 +4390,11 @@ This cannot be undone!</source> 3. З'єднання було скомпрометовано.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="It protects your IP address and connections." xml:space="preserve"> + <source>It protects your IP address and connections.</source> + <target>Він захищає вашу IP-адресу та з'єднання.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> <target>Схоже, що ви вже підключені за цим посиланням. Якщо це не так, сталася помилка (%@).</target> @@ -2989,15 +4413,21 @@ This cannot be undone!</source> <trans-unit id="Join" xml:space="preserve"> <source>Join</source> <target>Приєднуйтесь</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> <target>Приєднуйтесь до групи</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Join group conversations" xml:space="preserve"> + <source>Join group conversations</source> + <target>Приєднуйтесь до групових розмов</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Join group?" xml:space="preserve"> <source>Join group?</source> + <target>Приєднатися до групи?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Join incognito" xml:space="preserve"> @@ -3007,11 +4437,14 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Join with current profile" xml:space="preserve"> <source>Join with current profile</source> + <target>Приєднатися з поточним профілем</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Join your group? This is your link for group %@!" xml:space="preserve"> <source>Join your group? This is your link for group %@!</source> + <target>Приєднатися до групи? +Це ваше посилання на групу %@!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Joining group" xml:space="preserve"> @@ -3019,10 +4452,26 @@ This is your link for group %@!</source> <target>Приєднання до групи</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep" xml:space="preserve"> + <source>Keep</source> + <target>Тримай</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Keep conversation" xml:space="preserve"> + <source>Keep conversation</source> + <target>Підтримуйте розмову</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Keep the app open to use it from desktop" xml:space="preserve"> <source>Keep the app open to use it from desktop</source> + <target>Тримайте додаток відкритим, щоб використовувати його з робочого столу</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep unused invitation?" xml:space="preserve"> + <source>Keep unused invitation?</source> + <target>Зберігати невикористані запрошення?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> <source>Keep your connections</source> <target>Зберігайте свої зв'язки</target> @@ -3056,6 +4505,16 @@ This is your link for group %@!</source> <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> <target>Залишити</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Leave chat" xml:space="preserve"> + <source>Leave chat</source> + <target>Вийти з чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave chat?" xml:space="preserve"> + <source>Leave chat?</source> + <target>Залишити чат?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group" xml:space="preserve"> @@ -3085,14 +4544,29 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Link mobile and desktop apps! 🔗" xml:space="preserve"> <source>Link mobile and desktop apps! 🔗</source> + <target>Зв'яжіть мобільні та десктопні додатки! 🔗</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Linked desktop options" xml:space="preserve"> <source>Linked desktop options</source> + <target>Параметри пов'язаного робочого столу</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Linked desktops" xml:space="preserve"> <source>Linked desktops</source> + <target>Пов'язані робочі столи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List" xml:space="preserve"> + <source>List</source> + <note>swipe action</note> + </trans-unit> + <trans-unit id="List name and emoji should be different for all lists." xml:space="preserve"> + <source>List name and emoji should be different for all lists.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List name..." xml:space="preserve"> + <source>List name...</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Live message!" xml:space="preserve"> @@ -3125,11 +4599,6 @@ This is your link for group %@!</source> <target>Режим блокування</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> - <source>Make a private connection</source> - <target>Створіть приватне з'єднання</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make one message disappear" xml:space="preserve"> <source>Make one message disappear</source> <target>Зробити так, щоб одне повідомлення зникло</target> @@ -3140,21 +4609,11 @@ This is your link for group %@!</source> <target>Зробіть профіль приватним!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> - <target>Переконайтеся, що адреси серверів %@ мають правильний формат, розділені рядками і не дублюються (%@).</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> <target>Переконайтеся, що адреси серверів WebRTC ICE мають правильний формат, розділені рядками і не дублюються.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <target>Багато людей запитували: *якщо SimpleX не має ідентифікаторів користувачів, як він може доставляти повідомлення?*</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> <source>Mark deleted for everyone</source> <target>Позначити видалено для всіх</target> @@ -3180,11 +4639,35 @@ This is your link for group %@!</source> <target>Максимум 30 секунд, отримується миттєво.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Media & file servers" xml:space="preserve"> + <source>Media & file servers</source> + <target>Медіа та файлові сервери</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Medium" xml:space="preserve"> + <source>Medium</source> + <target>Середній</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Member" xml:space="preserve"> <source>Member</source> <target>Учасник</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member inactive" xml:space="preserve"> + <source>Member inactive</source> + <target>Користувач неактивний</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Member reports" xml:space="preserve"> + <source>Member reports</source> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All chat members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All chat members will be notified.</source> + <target>Роль учасника буде змінено на "%@". Усі учасники чату отримають сповіщення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> <target>Роль учасника буде змінено на "%@". Всі учасники групи будуть повідомлені про це.</target> @@ -3195,11 +4678,64 @@ This is your link for group %@!</source> <target>Роль учасника буде змінено на "%@". Учасник отримає нове запрошення.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from chat - this cannot be undone!</source> + <target>Учасника буде видалено з чату – це неможливо скасувати!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> <target>Учасник буде видалений з групи - це неможливо скасувати!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Members can add message reactions." xml:space="preserve"> + <source>Members can add message reactions.</source> + <target>Учасники групи можуть додавати реакції на повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Members can irreversibly delete sent messages. (24 hours)</source> + <target>Учасники групи можуть безповоротно видаляти надіслані повідомлення. (24 години)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can report messsages to moderators." xml:space="preserve"> + <source>Members can report messsages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send SimpleX links." xml:space="preserve"> + <source>Members can send SimpleX links.</source> + <target>Учасники групи можуть надсилати посилання SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> + <target>Учасники групи можуть надсилати прямі повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> + <target>Учасники групи можуть надсилати зникаючі повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send files and media." xml:space="preserve"> + <source>Members can send files and media.</source> + <target>Учасники групи можуть надсилати файли та медіа.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> + <target>Учасники групи можуть надсилати голосові повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mention members 👋" xml:space="preserve"> + <source>Mention members 👋</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Menus" xml:space="preserve"> + <source>Menus</source> + <target>Меню</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>Помилка доставки повідомлення</target> @@ -3210,11 +4746,31 @@ This is your link for group %@!</source> <target>Підтвердження доставки повідомлення!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message delivery warning" xml:space="preserve"> + <source>Message delivery warning</source> + <target>Попередження про доставку повідомлення</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> <target>Чернетка повідомлення</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message forwarded" xml:space="preserve"> + <source>Message forwarded</source> + <target>Повідомлення переслано</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message may be delivered later if member becomes active." xml:space="preserve"> + <source>Message may be delivered later if member becomes active.</source> + <target>Повідомлення може бути доставлене пізніше, якщо користувач стане активним.</target> + <note>item status description</note> + </trans-unit> + <trans-unit id="Message queue info" xml:space="preserve"> + <source>Message queue info</source> + <target>Інформація про чергу повідомлень</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message reactions" xml:space="preserve"> <source>Message reactions</source> <target>Реакції на повідомлення</target> @@ -3225,16 +4781,51 @@ This is your link for group %@!</source> <target>Реакції на повідомлення в цьому чаті заборонені.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> - <source>Message reactions are prohibited in this group.</source> + <trans-unit id="Message reactions are prohibited." xml:space="preserve"> + <source>Message reactions are prohibited.</source> <target>Реакції на повідомлення в цій групі заборонені.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message reception" xml:space="preserve"> + <source>Message reception</source> + <target>Прийом повідомлень</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message servers" xml:space="preserve"> + <source>Message servers</source> + <target>Сервери повідомлень</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message shape" xml:space="preserve"> + <source>Message shape</source> + <target>Форма повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message source remains private." xml:space="preserve"> + <source>Message source remains private.</source> + <target>Джерело повідомлення залишається приватним.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status" xml:space="preserve"> + <source>Message status</source> + <target>Статус повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status: %@" xml:space="preserve"> + <source>Message status: %@</source> + <target>Статус повідомлення: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> <target>Текст повідомлення</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message too large" xml:space="preserve"> + <source>Message too large</source> + <target>Повідомлення занадто велике</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> <target>Повідомлення</target> @@ -3247,6 +4838,66 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Messages from %@ will be shown!" xml:space="preserve"> <source>Messages from %@ will be shown!</source> + <target>Повідомлення від %@ будуть показані!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages in this chat will never be deleted." xml:space="preserve"> + <source>Messages in this chat will never be deleted.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages received" xml:space="preserve"> + <source>Messages received</source> + <target>Отримані повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages sent" xml:space="preserve"> + <source>Messages sent</source> + <target>Надіслані повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages were deleted after you selected them." xml:space="preserve"> + <source>Messages were deleted after you selected them.</source> + <target>Повідомлення були видалені після того, як ви їх вибрали.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Повідомлення, файли та дзвінки захищені **наскрізним шифруванням** з ідеальною секретністю переадресації, відмовою та відновленням після злому.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>Повідомлення, файли та дзвінки захищені **квантово-стійким шифруванням e2e** з ідеальною секретністю переадресації, відмовою та відновленням після злому.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate device" xml:space="preserve"> + <source>Migrate device</source> + <target>Перенести пристрій</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate from another device" xml:space="preserve"> + <source>Migrate from another device</source> + <target>Перехід з іншого пристрою</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate here" xml:space="preserve"> + <source>Migrate here</source> + <target>Мігруйте сюди</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device" xml:space="preserve"> + <source>Migrate to another device</source> + <target>Перехід на інший пристрій</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device via QR code." xml:space="preserve"> + <source>Migrate to another device via QR code.</source> + <target>Перейдіть на інший пристрій за допомогою QR-коду.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating" xml:space="preserve"> + <source>Migrating</source> + <target>Міграція</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Migrating database archive…" xml:space="preserve"> @@ -3254,6 +4905,11 @@ This is your link for group %@!</source> <target>Перенесення архіву бази даних…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Migration complete" xml:space="preserve"> + <source>Migration complete</source> + <target>Міграція завершена</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> <target>Помилка міграції:</target> @@ -3269,9 +4925,9 @@ This is your link for group %@!</source> <target>Міграцію завершено</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> - <target>Міграції: %@</target> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> + <target>Міграції:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> @@ -3289,21 +4945,30 @@ This is your link for group %@!</source> <target>Модерується за: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="More" xml:space="preserve"> + <source>More</source> + <note>swipe action</note> + </trans-unit> <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> <target>Незабаром буде ще більше покращень!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="More reliable network connection." xml:space="preserve"> + <source>More reliable network connection.</source> + <target>Більш надійне з'єднання з мережею.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="More reliable notifications" xml:space="preserve"> + <source>More reliable notifications</source> + <target>Більш надійні сповіщення</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> <source>Most likely this connection is deleted.</source> <target>Швидше за все, це з'єднання видалено.</target> <note>item status description</note> </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> - <source>Most likely this contact has deleted the connection with you.</source> - <target>Швидше за все, цей контакт видалив зв'язок з вами.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> <target>Кілька профілів чату</target> @@ -3312,7 +4977,11 @@ This is your link for group %@!</source> <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> <target>Вимкнути звук</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Mute all" xml:space="preserve"> + <source>Mute all</source> + <note>notification label action</note> </trans-unit> <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> @@ -3322,13 +4991,38 @@ This is your link for group %@!</source> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> <target>Ім'я</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> <target>Мережа та сервери</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Network connection" xml:space="preserve"> + <source>Network connection</source> + <target>Підключення до мережі</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network decentralization" xml:space="preserve"> + <source>Network decentralization</source> + <target>Децентралізація мережі</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve"> + <source>Network issues - message expired after many attempts to send it.</source> + <target>Проблеми з мережею - термін дії повідомлення закінчився після багатьох спроб надіслати його.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Network management" xml:space="preserve"> + <source>Network management</source> + <target>Керування мережею</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network operator" xml:space="preserve"> + <source>Network operator</source> + <target>Мережевий оператор</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> <target>Налаштування мережі</target> @@ -3339,11 +5033,35 @@ This is your link for group %@!</source> <target>Стан мережі</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New" xml:space="preserve"> + <source>New</source> + <note>token status text</note> + </trans-unit> <trans-unit id="New Passcode" xml:space="preserve"> <source>New Passcode</source> <target>Новий пароль</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve"> + <source>New SOCKS credentials will be used every time you start the app.</source> + <target>Нові облікові дані SOCKS будуть використовуватися при кожному запуску програми.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve"> + <source>New SOCKS credentials will be used for each server.</source> + <target>Для кожного сервера будуть використовуватися нові облікові дані SOCKS.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat" xml:space="preserve"> + <source>New chat</source> + <target>Новий чат</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat experience 🎉" xml:space="preserve"> + <source>New chat experience 🎉</source> + <target>Новий досвід спілкування в чаті 🎉</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> <target>Новий запит на контакт</target> @@ -3354,13 +5072,9 @@ This is your link for group %@!</source> <target>Новий контакт:</target> <note>notification</note> </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> - <source>New database archive</source> - <target>Новий архів бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="New desktop app!" xml:space="preserve"> <source>New desktop app!</source> + <target>Новий десктопний додаток!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="New display name" xml:space="preserve"> @@ -3368,11 +5082,21 @@ This is your link for group %@!</source> <target>Нове ім'я відображення</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>Нові події</target> + <note>notification</note> + </trans-unit> <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> <target>Нове в %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New media options" xml:space="preserve"> + <source>New media options</source> + <target>Нові медіа-опції</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> <target>Нова роль учасника</target> @@ -3388,6 +5112,11 @@ This is your link for group %@!</source> <target>Новий пароль…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New server" xml:space="preserve"> + <source>New server</source> + <target>Новий сервер</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No" xml:space="preserve"> <source>No</source> <target>Ні</target> @@ -3398,6 +5127,18 @@ This is your link for group %@!</source> <target>Немає пароля програми</target> <note>Authentication unavailable</note> </trans-unit> + <trans-unit id="No chats" xml:space="preserve"> + <source>No chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats found" xml:space="preserve"> + <source>No chats found</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats in list %@" xml:space="preserve"> + <source>No chats in list %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> <target>Не вибрано жодного контакту</target> @@ -3418,6 +5159,11 @@ This is your link for group %@!</source> <target>Токен пристрою відсутній!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No direct connection yet, message is forwarded by admin." xml:space="preserve"> + <source>No direct connection yet, message is forwarded by admin.</source> + <target>Прямого зв'язку ще немає, повідомлення пересилається адміністратором.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="No filtered chats" xml:space="preserve"> <source>No filtered chats</source> <target>Немає фільтрованих чатів</target> @@ -3433,20 +5179,107 @@ This is your link for group %@!</source> <target>Немає історії</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No info, try to reload" xml:space="preserve"> + <source>No info, try to reload</source> + <target>Немає інформації, спробуйте перезавантажити</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No media & file servers." xml:space="preserve"> + <source>No media & file servers.</source> + <target>Ніяких медіа та файлових серверів.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No message" xml:space="preserve"> + <source>No message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No message servers." xml:space="preserve"> + <source>No message servers.</source> + <target>Ніяких серверів повідомлень.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No network connection" xml:space="preserve"> + <source>No network connection</source> + <target>Немає підключення до мережі</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record speech" xml:space="preserve"> + <source>No permission to record speech</source> + <target>Немає дозволу на запис промови</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record video" xml:space="preserve"> + <source>No permission to record video</source> + <target>Немає дозволу на запис відео</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> <source>No permission to record voice message</source> <target>Немає дозволу на запис голосового повідомлення</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No push server" xml:space="preserve"> + <source>No push server</source> + <target>Локально</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> <target>Немає отриманих або відправлених файлів</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No servers for private message routing." xml:space="preserve"> + <source>No servers for private message routing.</source> + <target>Немає серверів для маршрутизації приватних повідомлень.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive files." xml:space="preserve"> + <source>No servers to receive files.</source> + <target>Немає серверів для отримання файлів.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive messages." xml:space="preserve"> + <source>No servers to receive messages.</source> + <target>Немає серверів для отримання повідомлень.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to send files." xml:space="preserve"> + <source>No servers to send files.</source> + <target>Немає серверів для надсилання файлів.</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No token!" xml:space="preserve"> + <source>No token!</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="No unread chats" xml:space="preserve"> + <source>No unread chats</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> + <target>Ніяких ідентифікаторів користувачів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Not compatible!" xml:space="preserve"> <source>Not compatible!</source> + <target>Не сумісні!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notes" xml:space="preserve"> + <source>Notes</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing selected" xml:space="preserve"> + <source>Nothing selected</source> + <target>Нічого не вибрано</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing to forward!" xml:space="preserve"> + <source>Nothing to forward!</source> + <target>Нічого пересилати!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> <target>Сповіщення</target> @@ -3457,6 +5290,19 @@ This is your link for group %@!</source> <target>Сповіщення вимкнено!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notifications error" xml:space="preserve"> + <source>Notifications error</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Notifications privacy" xml:space="preserve"> + <source>Notifications privacy</source> + <target>Сповіщення про приватність</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications status" xml:space="preserve"> + <source>Notifications status</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. @@ -3466,44 +5312,43 @@ This is your link for group %@!</source> - відключати користувачів (роль "спостерігач")</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="OK" xml:space="preserve"> + <source>OK</source> + <target>ОК</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Off" xml:space="preserve"> <source>Off</source> <target>Вимкнено</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> - <source>Off (Local)</source> - <target>Вимкнено (локально)</target> - <note>No comment provided by engineer.</note> + <note>blur media</note> </trans-unit> <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> <target>Гаразд</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> <target>Стара база даних</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> - <source>Old database archive</source> - <target>Старий архів бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> <target>Посилання на одноразове запрошення</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> - <target>Для підключення будуть потрібні хости onion. Потрібно увімкнути VPN.</target> + <trans-unit id="Onion hosts will be **required** for connection. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be **required** for connection. +Requires compatible VPN.</source> + <target>Для підключення будуть потрібні хости onion. +Потрібно увімкнути VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> - <target>Onion хости будуть використовуватися, коли вони будуть доступні. Потрібно увімкнути VPN.</target> + <trans-unit id="Onion hosts will be used when available. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be used when available. +Requires compatible VPN.</source> + <target>Onion хости будуть використовуватися, коли вони будуть доступні. +Потрібно увімкнути VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will not be used." xml:space="preserve"> @@ -3511,11 +5356,21 @@ This is your link for group %@!</source> <target>Onion хости не будуть використовуватися.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only chat owners can change preferences." xml:space="preserve"> + <source>Only chat owners can change preferences.</source> + <target>Лише власники чату можуть змінювати налаштування.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <target>Тільки клієнтські пристрої зберігають профілі користувачів, контакти, групи та повідомлення, надіслані за допомогою **2-шарового наскрізного шифрування**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only delete conversation" xml:space="preserve"> + <source>Only delete conversation</source> + <target>Видаляйте тільки розмови</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> <source>Only group owners can change group preferences.</source> <target>Тільки власники груп можуть змінювати налаштування групи.</target> @@ -3531,14 +5386,22 @@ This is your link for group %@!</source> <target>Тільки власники груп можуть вмикати голосові повідомлення.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only sender and moderators see it" xml:space="preserve"> + <source>Only sender and moderators see it</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you and moderators see it" xml:space="preserve"> + <source>Only you and moderators see it</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only you can add message reactions." xml:space="preserve"> <source>Only you can add message reactions.</source> <target>Тільки ви можете додавати реакції на повідомлення.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <target>Тільки ви можете безповоротно видалити повідомлення (ваш контакт може позначити їх для видалення).</target> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source> + <target>Тільки ви можете безповоротно видалити повідомлення (ваш контакт може позначити їх для видалення). (24 години)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can make calls." xml:space="preserve"> @@ -3561,9 +5424,9 @@ This is your link for group %@!</source> <target>Тільки ваш контакт може додавати реакції на повідомлення.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <target>Тільки ваш контакт може безповоротно видалити повідомлення (ви можете позначити їх для видалення).</target> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source> + <target>Тільки ваш контакт може безповоротно видалити повідомлення (ви можете позначити їх для видалення). (24 години)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can make calls." xml:space="preserve"> @@ -3583,13 +5446,19 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Open" xml:space="preserve"> <source>Open</source> - <note>No comment provided by engineer.</note> + <target>Відкрито</target> + <note>alert action</note> </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Відкрийте Налаштування</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open changes" xml:space="preserve"> + <source>Open changes</source> + <target>Відкриті зміни</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> <target>Відкритий чат</target> @@ -3600,25 +5469,86 @@ This is your link for group %@!</source> <target>Відкрийте консоль чату</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Open conditions" xml:space="preserve"> + <source>Open conditions</source> + <target>Відкриті умови</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open group" xml:space="preserve"> <source>Open group</source> + <target>Відкрита група</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve"> - <source>Open user profiles</source> - <target>Відкрити профілі користувачів</target> + <trans-unit id="Open link?" xml:space="preserve"> + <source>Open link?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Open migration to another device" xml:space="preserve"> + <source>Open migration to another device</source> + <target>Відкрита міграція на інший пристрій</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> - <target>Протокол і код з відкритим вихідним кодом - будь-хто може запускати сервери.</target> + <trans-unit id="Opening app…" xml:space="preserve"> + <source>Opening app…</source> + <target>Відкриваємо програму…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve"> - <source>Opening database…</source> - <target>Відкриття бази даних…</target> + <trans-unit id="Operator" xml:space="preserve"> + <source>Operator</source> + <target>Оператор</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Operator server" xml:space="preserve"> + <source>Operator server</source> + <target>Сервер оператора</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Or import archive file" xml:space="preserve"> + <source>Or import archive file</source> + <target>Або імпортуйте архівний файл</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or paste archive link" xml:space="preserve"> + <source>Or paste archive link</source> + <target>Або вставте посилання на архів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or scan QR code" xml:space="preserve"> + <source>Or scan QR code</source> + <target>Або відскануйте QR-код</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or securely share this file link" xml:space="preserve"> + <source>Or securely share this file link</source> + <target>Або безпечно поділіться цим посиланням на файл</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or show this code" xml:space="preserve"> + <source>Or show this code</source> + <target>Або покажіть цей код</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or to share privately" xml:space="preserve"> + <source>Or to share privately</source> + <target>Або поділитися приватно</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Organize chats into lists" xml:space="preserve"> + <source>Organize chats into lists</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other" xml:space="preserve"> + <source>Other</source> + <target>Інше</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other file errors: %@" xml:space="preserve"> + <source>Other file errors: +%@</source> + <target>Інші помилки файлів: +%@</target> + <note>alert message</note> + </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> <target>Кількість PING</target> @@ -3654,18 +5584,24 @@ This is your link for group %@!</source> <target>Пароль встановлено!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Password" xml:space="preserve"> + <source>Password</source> + <target>Пароль</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> <target>Показати пароль</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste" xml:space="preserve"> - <source>Paste</source> - <target>Вставити</target> - <note>No comment provided by engineer.</note> + <trans-unit id="Past member %@" xml:space="preserve"> + <source>Past member %@</source> + <target>Колишній учасник %@</target> + <note>past/unknown group member</note> </trans-unit> <trans-unit id="Paste desktop address" xml:space="preserve"> <source>Paste desktop address</source> + <target>Вставте адресу робочого столу</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Paste image" xml:space="preserve"> @@ -3673,23 +5609,23 @@ This is your link for group %@!</source> <target>Вставити зображення</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve"> - <source>Paste received link</source> + <trans-unit id="Paste link to connect!" xml:space="preserve"> + <source>Paste link to connect!</source> + <target>Вставте посилання для підключення!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Paste the link you received" xml:space="preserve"> + <source>Paste the link you received</source> <target>Вставте отримане посилання</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> - <source>Paste the link you received to connect with your contact.</source> - <target>Вставте отримане посилання для зв'язку з вашим контактом.</target> - <note>placeholder</note> - </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> - <target>Люди можуть зв'язатися з вами лише за посиланнями, якими ви ділитеся.</target> + <trans-unit id="Pending" xml:space="preserve"> + <source>Pending</source> + <target>В очікуванні</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Periodically" xml:space="preserve"> - <source>Periodically</source> + <trans-unit id="Periodic" xml:space="preserve"> + <source>Periodic</source> <target>Періодично</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3698,11 +5634,33 @@ This is your link for group %@!</source> <target>Постійна помилка розшифрування</target> <note>message decrypt error item</note> </trans-unit> + <trans-unit id="Picture-in-picture calls" xml:space="preserve"> + <source>Picture-in-picture calls</source> + <target>Дзвінки "картинка в картинці</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Play from the chat list." xml:space="preserve"> + <source>Play from the chat list.</source> + <target>Грати зі списку чату.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable calls." xml:space="preserve"> + <source>Please ask your contact to enable calls.</source> + <target>Будь ласка, попросіть свого контакту ввімкнути дзвінки.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> <target>Будь ласка, попросіть вашого контакту увімкнути відправку голосових повідомлень.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers." xml:space="preserve"> + <source>Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers.</source> + <target>Переконайтеся, що мобільний і настільний комп'ютери підключені до однієї локальної мережі, і що брандмауер настільного комп'ютера дозволяє з'єднання. +Будь ласка, повідомте про будь-які інші проблеми розробникам.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> <target>Будь ласка, перевірте, чи ви скористалися правильним посиланням, або попросіть контактну особу надіслати вам інше.</target> @@ -3718,6 +5676,18 @@ This is your link for group %@!</source> <target>Будь ласка, перевірте свої та контактні налаштування.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve"> + <source>Please confirm that network settings are correct for this device.</source> + <target>Переконайтеся, що налаштування мережі для цього пристрою є правильними.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact developers. Error: %@" xml:space="preserve"> + <source>Please contact developers. +Error: %@</source> + <target>Зверніться до розробників. +Помилка: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> <target>Зверніться до адміністратора групи.</target> @@ -3758,11 +5728,28 @@ This is your link for group %@!</source> <target>Будь ласка, зберігайте пароль надійно, ви НЕ зможете змінити його, якщо втратите.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please try to disable and re-enable notfications." xml:space="preserve"> + <source>Please try to disable and re-enable notfications.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token activation to complete." xml:space="preserve"> + <source>Please wait for token activation to complete.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token to be registered." xml:space="preserve"> + <source>Please wait for token to be registered.</source> + <note>token info</note> + </trans-unit> <trans-unit id="Polish interface" xml:space="preserve"> <source>Polish interface</source> <target>Польський інтерфейс</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Port" xml:space="preserve"> + <source>Port</source> + <target>Порт</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> <target>Можливо, в адресі сервера неправильно вказано відбиток сертифіката</target> @@ -3773,36 +5760,83 @@ This is your link for group %@!</source> <target>Зберегти чернетку останнього повідомлення з вкладеннями.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Preset server" xml:space="preserve"> - <source>Preset server</source> - <target>Попередньо встановлений сервер</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> <target>Попередньо встановлена адреса сервера</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Preset servers" xml:space="preserve"> + <source>Preset servers</source> + <target>Попередньо встановлені сервери</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Preview" xml:space="preserve"> <source>Preview</source> <target>Попередній перегляд</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Previously connected servers" xml:space="preserve"> + <source>Previously connected servers</source> + <target>Раніше підключені сервери</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> <target>Конфіденційність і безпека</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Privacy for your customers." xml:space="preserve"> + <source>Privacy for your customers.</source> + <target>Конфіденційність для ваших клієнтів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy policy and conditions of use." xml:space="preserve"> + <source>Privacy policy and conditions of use.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy redefined" xml:space="preserve"> <source>Privacy redefined</source> <target>Конфіденційність переглянута</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve"> + <source>Private chats, groups and your contacts are not accessible to server operators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> <target>Приватні імена файлів</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private media file names." xml:space="preserve"> + <source>Private media file names.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing" xml:space="preserve"> + <source>Private message routing</source> + <target>Маршрутизація приватних повідомлень</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing 🚀" xml:space="preserve"> + <source>Private message routing 🚀</source> + <target>Маршрутизація приватних повідомлень 🚀</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private notes" xml:space="preserve"> + <source>Private notes</source> + <target>Приватні нотатки</target> + <note>name of notes to self</note> + </trans-unit> + <trans-unit id="Private routing" xml:space="preserve"> + <source>Private routing</source> + <target>Приватна маршрутизація</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private routing error" xml:space="preserve"> + <source>Private routing error</source> + <target>Помилка приватної маршрутизації</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> <target>З'єднання профілю та сервера</target> @@ -3813,12 +5847,9 @@ This is your link for group %@!</source> <target>Зображення профілю</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile name" xml:space="preserve"> - <source>Profile name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile name:" xml:space="preserve"> - <source>Profile name:</source> + <trans-unit id="Profile images" xml:space="preserve"> + <source>Profile images</source> + <target>Зображення профілю</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile password" xml:space="preserve"> @@ -3826,10 +5857,15 @@ This is your link for group %@!</source> <target>Пароль до профілю</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Profile theme" xml:space="preserve"> + <source>Profile theme</source> + <target>Тема профілю</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> <source>Profile update will be sent to your contacts.</source> <target>Оновлення профілю буде надіслано вашим контактам.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> @@ -3851,6 +5887,15 @@ This is your link for group %@!</source> <target>Заборонити реакції на повідомлення.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Prohibit reporting messages to moderators." xml:space="preserve"> + <source>Prohibit reporting messages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending SimpleX links." xml:space="preserve"> + <source>Prohibit sending SimpleX links.</source> + <target>Заборонити надсилання посилань SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> <target>Заборонити надсилати прямі повідомлення учасникам.</target> @@ -3871,11 +5916,23 @@ This is your link for group %@!</source> <target>Заборонити надсилання голосових повідомлень.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect IP address" xml:space="preserve"> + <source>Protect IP address</source> + <target>Захист IP-адреси</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> <target>Захистіть екран програми</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve"> + <source>Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings.</source> + <target>Захистіть свою IP-адресу від ретрансляторів повідомлень, обраних вашими контактами. +Увімкніть у налаштуваннях *Мережа та сервери*.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> <target>Захистіть свої профілі чату паролем!</target> @@ -3891,9 +5948,34 @@ This is your link for group %@!</source> <target>Тайм-аут протоколу на КБ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Proxied" xml:space="preserve"> + <source>Proxied</source> + <target>Проксі-сервер</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied servers" xml:space="preserve"> + <source>Proxied servers</source> + <target>Проксі-сервери</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxy requires password" xml:space="preserve"> + <source>Proxy requires password</source> + <target>Проксі вимагає пароль</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> - <target>Push-повідомлення</target> + <target>Push-сповіщення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Push server" xml:space="preserve"> + <source>Push server</source> + <target>Push-сервер</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Quantum resistant encryption" xml:space="preserve"> + <source>Quantum resistant encryption</source> + <target>Квантово-стійке шифрування</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> @@ -3901,6 +5983,11 @@ This is your link for group %@!</source> <target>Оцініть додаток</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reachable chat toolbar" xml:space="preserve"> + <source>Reachable chat toolbar</source> + <target>Доступна панель інструментів чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> <target>Реагуй…</target> @@ -3909,16 +5996,21 @@ This is your link for group %@!</source> <trans-unit id="Read" xml:space="preserve"> <source>Read</source> <target>Читати</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Read more" xml:space="preserve"> <source>Read more</source> <target>Читати далі</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> - <target>Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source> + <target>Читайте більше в [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</source> + <target>Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> @@ -3926,11 +6018,6 @@ This is your link for group %@!</source> <target>Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> - <source>Read more in our GitHub repository.</source> - <target>Читайте більше в нашому репозиторії на GitHub.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> <target>Читайте більше в нашому [GitHub репозиторії](https://github.com/simplex-chat/simplex-chat#readme).</target> @@ -3941,6 +6028,11 @@ This is your link for group %@!</source> <target>Підтвердження виключені</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receive errors" xml:space="preserve"> + <source>Receive errors</source> + <target>Отримання помилок</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Отримано за</target> @@ -3961,6 +6053,21 @@ This is your link for group %@!</source> <target>Отримано повідомлення</target> <note>message info title</note> </trans-unit> + <trans-unit id="Received messages" xml:space="preserve"> + <source>Received messages</source> + <target>Отримані повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received reply" xml:space="preserve"> + <source>Received reply</source> + <target>Отримано відповідь</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received total" xml:space="preserve"> + <source>Received total</source> + <target>Отримано всього</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> <target>Адреса отримувача буде змінена на інший сервер. Зміна адреси завершиться після того, як відправник з'явиться в мережі.</target> @@ -3976,16 +6083,51 @@ This is your link for group %@!</source> <target>Отримання через</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Recent history and improved [directory bot](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)." xml:space="preserve"> + <source>Recent history and improved [directory bot](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).</source> + <target>Нещодавня історія та покращення [directory bot](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).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve"> + <source>Recipient(s) can't see who this message is from.</source> + <target>Одержувач(и) не бачить, від кого це повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> <target>Одержувачі бачать оновлення, коли ви їх вводите.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect" xml:space="preserve"> + <source>Reconnect</source> + <target>Повторне підключення</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> <target>Перепідключіть всі підключені сервери, щоб примусово доставити повідомлення. Це використовує додатковий трафік.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect all servers" xml:space="preserve"> + <source>Reconnect all servers</source> + <target>Перепідключіть усі сервери</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers?" xml:space="preserve"> + <source>Reconnect all servers?</source> + <target>Перепідключити всі сервери?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect server to force message delivery. It uses additional traffic.</source> + <target>Перепідключити сервер для примусової доставки повідомлень. Використовує додатковий трафік.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server?" xml:space="preserve"> + <source>Reconnect server?</source> + <target>Перепідключити сервер?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> <target>Перепідключити сервери?</target> @@ -4006,10 +6148,23 @@ This is your link for group %@!</source> <target>Зменшення використання акумулятора</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Register" xml:space="preserve"> + <source>Register</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Register notification token?" xml:space="preserve"> + <source>Register notification token?</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Registered" xml:space="preserve"> + <source>Registered</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> <target>Відхилити</target> - <note>reject incoming call via notification</note> + <note>reject incoming call via notification +swipe action</note> </trans-unit> <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> <source>Reject (sender NOT notified)</source> @@ -4036,6 +6191,16 @@ This is your link for group %@!</source> <target>Видалити</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Remove archive?" xml:space="preserve"> + <source>Remove archive?</source> + <target>Видалити архів?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove image" xml:space="preserve"> + <source>Remove image</source> + <target>Видалити зображення</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> <target>Видалити учасника</target> @@ -4068,10 +6233,27 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Repeat connection request?" xml:space="preserve"> <source>Repeat connection request?</source> + <target>Повторити запит на підключення?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat download" xml:space="preserve"> + <source>Repeat download</source> + <target>Повторити завантаження</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat import" xml:space="preserve"> + <source>Repeat import</source> + <target>Повторний імпорт</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Repeat join request?" xml:space="preserve"> <source>Repeat join request?</source> + <target>Повторити запит на приєднання?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat upload" xml:space="preserve"> + <source>Repeat upload</source> + <target>Повторне завантаження</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reply" xml:space="preserve"> @@ -4079,6 +6261,46 @@ This is your link for group %@!</source> <target>Відповісти</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Report" xml:space="preserve"> + <source>Report</source> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Report content: only group moderators will see it." xml:space="preserve"> + <source>Report content: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report member profile: only group moderators will see it." xml:space="preserve"> + <source>Report member profile: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report other: only group moderators will see it." xml:space="preserve"> + <source>Report other: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report reason?" xml:space="preserve"> + <source>Report reason?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Report spam: only group moderators will see it." xml:space="preserve"> + <source>Report spam: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report violation: only group moderators will see it." xml:space="preserve"> + <source>Report violation: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report: %@" xml:space="preserve"> + <source>Report: %@</source> + <note>report in notification</note> + </trans-unit> + <trans-unit id="Reporting messages to moderators is prohibited." xml:space="preserve"> + <source>Reporting messages to moderators is prohibited.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reports" xml:space="preserve"> + <source>Reports</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Required" xml:space="preserve"> <source>Required</source> <target>Потрібно</target> @@ -4089,16 +6311,41 @@ This is your link for group %@!</source> <target>Перезавантаження</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset all hints" xml:space="preserve"> + <source>Reset all hints</source> + <target>Скинути всі підказки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics" xml:space="preserve"> + <source>Reset all statistics</source> + <target>Скинути всю статистику</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics?" xml:space="preserve"> + <source>Reset all statistics?</source> + <target>Скинути всю статистику?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> <target>Скинути кольори</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to app theme" xml:space="preserve"> + <source>Reset to app theme</source> + <target>Повернутися до теми програми</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> <target>Відновити налаштування за замовчуванням</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to user theme" xml:space="preserve"> + <source>Reset to user theme</source> + <target>Повернутися до теми користувача</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> <source>Restart the app to create a new chat profile</source> <target>Перезапустіть програму, щоб створити новий профіль чату</target> @@ -4129,14 +6376,19 @@ This is your link for group %@!</source> <target>Відновлення помилки бази даних</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Retry" xml:space="preserve"> + <source>Retry</source> + <target>Спробуйте ще раз</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> <target>Показувати</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Revert" xml:space="preserve"> - <source>Revert</source> - <target>Повернутися</target> + <trans-unit id="Review conditions" xml:space="preserve"> + <source>Review conditions</source> + <target>Умови перегляду</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke" xml:space="preserve"> @@ -4164,51 +6416,66 @@ This is your link for group %@!</source> <target>Запустити чат</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve"> - <source>SMP servers</source> - <target>Сервери SMP</target> + <trans-unit id="SMP server" xml:space="preserve"> + <source>SMP server</source> + <target>Сервер SMP</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SOCKS proxy" xml:space="preserve"> + <source>SOCKS proxy</source> + <target>Проксі SOCKS</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safely receive files" xml:space="preserve"> + <source>Safely receive files</source> + <target>Безпечне отримання файлів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safer groups" xml:space="preserve"> + <source>Safer groups</source> + <target>Безпечніші групи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save" xml:space="preserve"> <source>Save</source> <target>Зберегти</target> - <note>chat item action</note> + <note>alert button +chat item action</note> </trans-unit> <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> <target>Зберегти (і повідомити контактам)</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> <target>Зберегти та повідомити контакт</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> <target>Зберегти та повідомити учасників групи</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save and reconnect" xml:space="preserve"> + <source>Save and reconnect</source> + <target>Збережіть і підключіться знову</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save and update group profile" xml:space="preserve"> <source>Save and update group profile</source> <target>Збереження та оновлення профілю групи</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save archive" xml:space="preserve"> - <source>Save archive</source> - <target>Зберегти архів</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve"> - <source>Save auto-accept settings</source> - <target>Зберегти налаштування автоприйому</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> <source>Save group profile</source> <target>Зберегти профіль групи</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save list" xml:space="preserve"> + <source>Save list</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> <target>Збережіть пароль і відкрийте чат</target> @@ -4221,8 +6488,8 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> - <target>Зберегти налаштування?</target> - <note>No comment provided by engineer.</note> + <target>Зберегти настройки?</target> + <note>alert title</note> </trans-unit> <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> @@ -4237,23 +6504,53 @@ This is your link for group %@!</source> <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> <target>Зберегти сервери?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve"> - <source>Save settings?</source> - <target>Зберегти налаштування?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> <target>Зберегти вітальне повідомлення?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save your profile?" xml:space="preserve"> + <source>Save your profile?</source> + <target>Зберегти свій профіль?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Saved" xml:space="preserve"> + <source>Saved</source> + <target>Збережено</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> <source>Saved WebRTC ICE servers will be removed</source> <target>Збережені сервери WebRTC ICE буде видалено</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Saved from" xml:space="preserve"> + <source>Saved from</source> + <target>Збережено з</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved message" xml:space="preserve"> + <source>Saved message</source> + <target>Збережене повідомлення</target> + <note>message info title</note> + </trans-unit> + <trans-unit id="Saving %lld messages" xml:space="preserve"> + <source>Saving %lld messages</source> + <target>Збереження повідомлень %lld</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scale" xml:space="preserve"> + <source>Scale</source> + <target>Масштаб</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan / Paste link" xml:space="preserve"> + <source>Scan / Paste link</source> + <target>Відсканувати / Вставити посилання</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> <target>Відскануйте QR-код</target> @@ -4261,6 +6558,7 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Scan QR code from desktop" xml:space="preserve"> <source>Scan QR code from desktop</source> + <target>Відскануйте QR-код з робочого столу</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Scan code" xml:space="preserve"> @@ -4283,11 +6581,31 @@ This is your link for group %@!</source> <target>Пошук</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Search bar accepts invitation links." xml:space="preserve"> + <source>Search bar accepts invitation links.</source> + <target>Рядок пошуку приймає посилання-запрошення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search or paste SimpleX link" xml:space="preserve"> + <source>Search or paste SimpleX link</source> + <target>Знайдіть або вставте посилання SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secondary" xml:space="preserve"> + <source>Secondary</source> + <target>Вторинний</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> <source>Secure queue</source> <target>Безпечна черга</target> <note>server test step</note> </trans-unit> + <trans-unit id="Secured" xml:space="preserve"> + <source>Secured</source> + <target>Забезпечено</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> <target>Оцінка безпеки</target> @@ -4301,6 +6619,21 @@ This is your link for group %@!</source> <trans-unit id="Select" xml:space="preserve"> <source>Select</source> <target>Виберіть</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Select chat profile" xml:space="preserve"> + <source>Select chat profile</source> + <target>Виберіть профіль чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected %lld" xml:space="preserve"> + <source>Selected %lld</source> + <target>Вибрано %lld</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Вибрані налаштування чату забороняють це повідомлення.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct" xml:space="preserve"> @@ -4338,13 +6671,9 @@ This is your link for group %@!</source> <target>Надсилання звітів про доставку</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve"> - <source>Send direct message</source> - <target>Надішліть пряме повідомлення</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Send direct message to connect" xml:space="preserve"> <source>Send direct message to connect</source> + <target>Надішліть пряме повідомлення, щоб підключитися</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send disappearing message" xml:space="preserve"> @@ -4352,6 +6681,11 @@ This is your link for group %@!</source> <target>Надіслати зникаюче повідомлення</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send errors" xml:space="preserve"> + <source>Send errors</source> + <target>Помилки надсилання</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> <target>Надіслати попередній перегляд за посиланням</target> @@ -4362,14 +6696,28 @@ This is your link for group %@!</source> <target>Надіслати живе повідомлення</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send message to enable calls." xml:space="preserve"> + <source>Send message to enable calls.</source> + <target>Надішліть повідомлення, щоб увімкнути дзвінки.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source> + <target>Надсилайте повідомлення напряму, якщо IP-адреса захищена, а ваш сервер або сервер призначення не підтримує приватну маршрутизацію.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when your or destination server does not support private routing.</source> + <target>Надсилайте повідомлення напряму, якщо ваш сервер або сервер призначення не підтримує приватну маршрутизацію.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> <target>Надсилати сповіщення</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> - <source>Send notifications:</source> - <target>Надсилати сповіщення:</target> + <trans-unit id="Send private reports" xml:space="preserve"> + <source>Send private reports</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve"> @@ -4387,10 +6735,15 @@ This is your link for group %@!</source> <target>Надсилайте їх із галереї чи власних клавіатур.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve"> + <source>Send up to 100 last messages to new members.</source> + <target>Надішліть до 100 останніх повідомлень новим користувачам.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> <target>Відправник скасував передачу файлу.</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> @@ -4447,6 +6800,11 @@ This is your link for group %@!</source> <target>Надіслано за: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Sent directly" xml:space="preserve"> + <source>Sent directly</source> + <target>Відправлено напряму</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> <target>Подія надісланого файлу</target> @@ -4457,11 +6815,71 @@ This is your link for group %@!</source> <target>Надіслано повідомлення</target> <note>message info title</note> </trans-unit> + <trans-unit id="Sent messages" xml:space="preserve"> + <source>Sent messages</source> + <target>Надіслані повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> <target>Надіслані повідомлення будуть видалені через встановлений час.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sent reply" xml:space="preserve"> + <source>Sent reply</source> + <target>Надіслано відповідь</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent total" xml:space="preserve"> + <source>Sent total</source> + <target>Відправлено всього</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent via proxy" xml:space="preserve"> + <source>Sent via proxy</source> + <target>Відправлено через проксі</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server" xml:space="preserve"> + <source>Server</source> + <target>Сервер</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server added to operator %@." xml:space="preserve"> + <source>Server added to operator %@.</source> + <target>Сервер додано до оператора %@.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Server address" xml:space="preserve"> + <source>Server address</source> + <target>Адреса сервера</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings." xml:space="preserve"> + <source>Server address is incompatible with network settings.</source> + <target>Адреса сервера несумісна з налаштуваннями мережі.</target> + <note>srv error text.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Server address is incompatible with network settings: %@.</source> + <target>Адреса сервера несумісна з налаштуваннями мережі: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server operator changed." xml:space="preserve"> + <source>Server operator changed.</source> + <target>Оператор сервера змінився.</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Server operators" xml:space="preserve"> + <source>Server operators</source> + <target>Оператори серверів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server protocol changed." xml:space="preserve"> + <source>Server protocol changed.</source> + <target>Протокол сервера змінено.</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> <target>Сервер вимагає авторизації для створення черг, перевірте пароль</target> @@ -4477,13 +6895,39 @@ This is your link for group %@!</source> <target>Тест сервера завершився невдало!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Server type" xml:space="preserve"> + <source>Server type</source> + <target>Тип сервера</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server version is incompatible with network settings." xml:space="preserve"> + <source>Server version is incompatible with network settings.</source> + <target>Серверна версія несумісна з мережевими налаштуваннями.</target> + <note>srv error text</note> + </trans-unit> + <trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve"> + <source>Server version is incompatible with your app: %@.</source> + <target>Версія сервера несумісна з вашим додатком: %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Servers" xml:space="preserve"> <source>Servers</source> <target>Сервери</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Servers info" xml:space="preserve"> + <source>Servers info</source> + <target>Інформація про сервери</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <target>Статистика серверів буде скинута - це неможливо скасувати!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Session code" xml:space="preserve"> <source>Session code</source> + <target>Код сесії</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set 1 day" xml:space="preserve"> @@ -4491,11 +6935,20 @@ This is your link for group %@!</source> <target>Встановити 1 день</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set chat name…" xml:space="preserve"> + <source>Set chat name…</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> <target>Встановити ім'я контакту…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve"> + <source>Set default theme</source> + <target>Встановлення теми за замовчуванням</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> <target>Встановіть налаштування групи</target> @@ -4506,11 +6959,20 @@ This is your link for group %@!</source> <target>Встановіть його замість аутентифікації системи.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve"> + <source>Set message expiration in chats.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passcode" xml:space="preserve"> <source>Set passcode</source> <target>Встановити пароль</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set passphrase" xml:space="preserve"> + <source>Set passphrase</source> + <target>Встановити парольну фразу</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> <target>Встановити ключову фразу для експорту</target> @@ -4531,24 +6993,55 @@ This is your link for group %@!</source> <target>Налаштування</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve"> + <source>Settings were changed.</source> + <target>Налаштування були змінені.</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve"> + <source>Shape profile images</source> + <target>Сформуйте зображення профілю</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share" xml:space="preserve"> <source>Share</source> <target>Поділіться</target> - <note>chat item action</note> + <note>alert action +chat item action</note> </trans-unit> <trans-unit id="Share 1-time link" xml:space="preserve"> <source>Share 1-time link</source> <target>Поділитися 1-разовим посиланням</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share 1-time link with a friend" xml:space="preserve"> + <source>Share 1-time link with a friend</source> + <target>Поділіться одноразовим посиланням з другом</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve"> + <source>Share SimpleX address on social media.</source> + <target>Поділіться адресою SimpleX у соціальних мережах.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address" xml:space="preserve"> <source>Share address</source> <target>Поділитися адресою</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share address publicly" xml:space="preserve"> + <source>Share address publicly</source> + <target>Поділіться адресою публічно</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address with contacts?" xml:space="preserve"> <source>Share address with contacts?</source> <target>Поділіться адресою з контактами?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Share from other apps." xml:space="preserve"> + <source>Share from other apps.</source> + <target>Діліться з інших програм.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share link" xml:space="preserve"> @@ -4556,9 +7049,19 @@ This is your link for group %@!</source> <target>Поділіться посиланням</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve"> - <source>Share one-time invitation link</source> - <target>Поділіться посиланням на одноразове запрошення</target> + <trans-unit id="Share profile" xml:space="preserve"> + <source>Share profile</source> + <target>Поділіться профілем</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share this 1-time invite link" xml:space="preserve"> + <source>Share this 1-time invite link</source> + <target>Поділіться цим одноразовим посиланням-запрошенням</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share to SimpleX" xml:space="preserve"> + <source>Share to SimpleX</source> + <target>Поділіться з SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share with contacts" xml:space="preserve"> @@ -4566,6 +7069,15 @@ This is your link for group %@!</source> <target>Поділіться з контактами</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Short link" xml:space="preserve"> + <source>Short link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show QR code" xml:space="preserve"> + <source>Show QR code</source> + <target>Показати QR-код</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> <target>Показувати дзвінки в історії дзвінків</target> @@ -4581,21 +7093,46 @@ This is your link for group %@!</source> <target>Показати останні повідомлення</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show message status" xml:space="preserve"> + <source>Show message status</source> + <target>Показати статус повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show percentage" xml:space="preserve"> + <source>Show percentage</source> + <target>Показати відсоток</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Показати попередній перегляд</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show → on messages sent via private routing." xml:space="preserve"> + <source>Show → on messages sent via private routing.</source> + <target>Показувати → у повідомленнях, надісланих через приватну маршрутизацію.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show:" xml:space="preserve"> <source>Show:</source> <target>Показати:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve"> + <source>SimpleX</source> + <target>SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Address" xml:space="preserve"> <source>SimpleX Address</source> <target>Адреса SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." xml:space="preserve"> + <source>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</source> + <target>SimpleX Chat і Flux уклали угоду про включення серверів, керованих Flux, у додаток.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> <target>Безпека SimpleX Chat була перевірена компанією Trail of Bits.</target> @@ -4626,6 +7163,20 @@ This is your link for group %@!</source> <target>Адреса SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX address and 1-time links are safe to share via any messenger." xml:space="preserve"> + <source>SimpleX address and 1-time links are safe to share via any messenger.</source> + <target>SimpleX-адреси та одноразові посилання можна безпечно ділитися через будь-який месенджер.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address or 1-time link?" xml:space="preserve"> + <source>SimpleX address or 1-time link?</source> + <target>SimpleX адреса або одноразове посилання?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX channel link" xml:space="preserve"> + <source>SimpleX channel link</source> + <note>simplex link type</note> + </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> <target>Контактна адреса SimpleX</target> @@ -4644,6 +7195,16 @@ This is your link for group %@!</source> <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> <target>Посилання SimpleX</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve"> + <source>SimpleX links are prohibited.</source> + <target>У цій групі заборонені посилання на SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve"> + <source>SimpleX links not allowed</source> + <target>Посилання SimpleX заборонені</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> @@ -4651,8 +7212,19 @@ This is your link for group %@!</source> <target>Одноразове запрошення SimpleX</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> + <source>SimpleX protocols reviewed by Trail of Bits.</source> + <target>Протоколи SimpleX, розглянуті Trail of Bits.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Simplified incognito mode" xml:space="preserve"> <source>Simplified incognito mode</source> + <target>Спрощений режим інкогніто</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Size" xml:space="preserve"> + <source>Size</source> + <target>Розмір</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Skip" xml:space="preserve"> @@ -4670,19 +7242,51 @@ This is your link for group %@!</source> <target>Невеликі групи (максимум 20 осіб)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Soft" xml:space="preserve"> + <source>Soft</source> + <target>М'який</target> + <note>blur media</note> + </trans-unit> + <trans-unit id="Some app settings were not migrated." xml:space="preserve"> + <source>Some app settings were not migrated.</source> + <target>Деякі налаштування програми не були перенесені.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some file(s) were not exported:" xml:space="preserve"> + <source>Some file(s) were not exported:</source> + <target>Деякі файли не було експортовано:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Під час імпорту виникли деякі нефатальні помилки – ви можете переглянути консоль чату, щоб дізнатися більше.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import:" xml:space="preserve"> + <source>Some non-fatal errors occurred during import:</source> + <target>Під час імпорту виникли деякі несмертельні помилки:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some servers failed the test: %@" xml:space="preserve"> + <source>Some servers failed the test: +%@</source> + <target>Деякі сервери не пройшли тестування: +%@</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> <target>Хтось</target> <note>notification title</note> </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve"> - <source>Start a new chat</source> - <target>Почніть новий чат</target> + <trans-unit id="Spam" xml:space="preserve"> + <source>Spam</source> + <note>blocking reason +report reason</note> + </trans-unit> + <trans-unit id="Square, circle, or anything in between." xml:space="preserve"> + <source>Square, circle, or anything in between.</source> + <target>Квадрат, коло або щось середнє між ними.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start chat" xml:space="preserve"> @@ -4690,11 +7294,26 @@ This is your link for group %@!</source> <target>Почати чат</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Start chat?" xml:space="preserve"> + <source>Start chat?</source> + <target>Почати чат?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> <target>Почати міграцію</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Starting from %@." xml:space="preserve"> + <source>Starting from %@.</source> + <target>Починаючи з %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Statistics" xml:space="preserve"> + <source>Statistics</source> + <target>Статистика</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> <target>Зупинити</target> @@ -4705,9 +7324,9 @@ This is your link for group %@!</source> <target>Зупинити SimpleX</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> - <source>Stop chat to enable database actions</source> - <target>Зупиніть чат, щоб увімкнути дії з базою даних</target> + <trans-unit id="Stop chat" xml:space="preserve"> + <source>Stop chat</source> + <target>Припинити чат</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> @@ -4738,23 +7357,62 @@ This is your link for group %@!</source> <trans-unit id="Stop sharing" xml:space="preserve"> <source>Stop sharing</source> <target>Припиніть ділитися</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Stop sharing address?" xml:space="preserve"> <source>Stop sharing address?</source> <target>Припинити ділитися адресою?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Stopping chat" xml:space="preserve"> + <source>Stopping chat</source> + <target>Зупинка чату</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Storage" xml:space="preserve"> + <source>Storage</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Strong" xml:space="preserve"> + <source>Strong</source> + <target>Сильний</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Submit" xml:space="preserve"> <source>Submit</source> <target>Надіслати</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Subscribed" xml:space="preserve"> + <source>Subscribed</source> + <target>Підписано</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscription errors" xml:space="preserve"> + <source>Subscription errors</source> + <target>Помилки підписки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscriptions ignored" xml:space="preserve"> + <source>Subscriptions ignored</source> + <target>Підписки ігноруються</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Support SimpleX Chat" xml:space="preserve"> <source>Support SimpleX Chat</source> <target>Підтримка чату SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Switch audio and video during the call." xml:space="preserve"> + <source>Switch audio and video during the call.</source> + <target>Перемикайте аудіо та відео під час дзвінка.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> + <source>Switch chat profile for 1-time invitations.</source> + <target>Переключіть профіль чату для отримання одноразових запрошень.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="System" xml:space="preserve"> <source>System</source> <target>Система</target> @@ -4765,11 +7423,20 @@ This is your link for group %@!</source> <target>Автентифікація системи</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP connection" xml:space="preserve"> + <source>TCP connection</source> + <target>TCP-з'єднання</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> <target>Тайм-аут TCP-з'єднання</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP port for messaging" xml:space="preserve"> + <source>TCP port for messaging</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <source>TCP_KEEPCNT</source> <target>TCP_KEEPCNT</target> @@ -4785,11 +7452,21 @@ This is your link for group %@!</source> <target>TCP_KEEPINTVL</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tail" xml:space="preserve"> + <source>Tail</source> + <target>Хвіст</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> <target>Сфотографуйте</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap Create SimpleX address in the menu to create it later." xml:space="preserve"> + <source>Tap Create SimpleX address in the menu to create it later.</source> + <target>Натисніть «Створити адресу SimpleX» у меню, щоб створити її пізніше.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> <target>Натисніть кнопку </target> @@ -4797,6 +7474,7 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Tap to Connect" xml:space="preserve"> <source>Tap to Connect</source> + <target>Натисніть, щоб підключитися</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to activate profile." xml:space="preserve"> @@ -4814,16 +7492,30 @@ This is your link for group %@!</source> <target>Натисніть, щоб приєднатися інкогніто</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve"> - <source>Tap to start a new chat</source> - <target>Натисніть, щоб почати новий чат</target> + <trans-unit id="Tap to paste link" xml:space="preserve"> + <source>Tap to paste link</source> + <target>Натисніть, щоб вставити посилання</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap to scan" xml:space="preserve"> + <source>Tap to scan</source> + <target>Натисніть, щоб сканувати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Temporary file error" xml:space="preserve"> + <source>Temporary file error</source> + <target>Тимчасова помилка файлу</target> + <note>file error alert title</note> + </trans-unit> <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> <target>Тест завершився невдало на кроці %@.</target> <note>server test failure</note> </trans-unit> + <trans-unit id="Test notifications" xml:space="preserve"> + <source>Test notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> <target>Тестовий сервер</target> @@ -4837,7 +7529,7 @@ This is your link for group %@!</source> <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> <target>Тести не пройшли!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> @@ -4854,11 +7546,6 @@ This is your link for group %@!</source> <target>Дякуємо користувачам - зробіть свій внесок через Weblate!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> - <target>Перша платформа без жодних ідентифікаторів користувачів – приватна за дизайном.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <source>The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised.</source> @@ -4871,11 +7558,31 @@ It can happen because of some bug or when the connection is compromised.</source <target>Додаток може сповіщати вас, коли ви отримуєте повідомлення або запити на контакт - будь ласка, відкрийте налаштування, щоб увімкнути цю функцію.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The app protects your privacy by using different operators in each conversation." xml:space="preserve"> + <source>The app protects your privacy by using different operators in each conversation.</source> + <target>Додаток захищає вашу конфіденційність, використовуючи різних операторів у кожній розмові.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve"> + <source>The app will ask to confirm downloads from unknown file servers (except .onion).</source> + <target>Програма попросить підтвердити завантаження з невідомих файлових серверів (крім .onion).</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> <source>The attempt to change database passphrase was not completed.</source> <target>Спроба змінити пароль до бази даних не була завершена.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve"> + <source>The code you scanned is not a SimpleX link QR code.</source> + <target>Відсканований вами код не є QR-кодом посилання SimpleX.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The connection reached the limit of undelivered messages, your contact may be offline." xml:space="preserve"> + <source>The connection reached the limit of undelivered messages, your contact may be offline.</source> + <target>З'єднання досягло ліміту недоставлених повідомлень, ваш контакт може бути офлайн.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> <target>Прийняте вами з'єднання буде скасовано!</target> @@ -4896,6 +7603,11 @@ It can happen because of some bug or when the connection is compromised.</source <target>Шифрування працює і нова угода про шифрування не потрібна. Це може призвести до помилок з'єднання!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> + <target>Наступне покоління приватних повідомлень</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The hash of the previous message is different." xml:space="preserve"> <source>The hash of the previous message is different.</source> <target>Хеш попереднього повідомлення відрізняється.</target> @@ -4911,9 +7623,14 @@ It can happen because of some bug or when the connection is compromised.</source <target>Повідомлення буде позначено як модероване для всіх учасників.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> - <target>Наступне покоління приватних повідомлень</target> + <trans-unit id="The messages will be deleted for all members." xml:space="preserve"> + <source>The messages will be deleted for all members.</source> + <target>Повідомлення будуть видалені для всіх учасників.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve"> + <source>The messages will be marked as moderated for all members.</source> + <target>Повідомлення будуть позначені як модеровані для всіх учасників.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> @@ -4921,9 +7638,14 @@ It can happen because of some bug or when the connection is compromised.</source <target>Стара база даних не була видалена під час міграції, її можна видалити.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> - <target>Профіль доступний лише вашим контактам.</target> + <trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve"> + <source>The same conditions will apply to operator **%@**.</source> + <target>Такі ж умови діятимуть і для оператора **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second preset operator in the app!" xml:space="preserve"> + <source>The second preset operator in the app!</source> + <target>Другий попередньо встановлений оператор у застосунку!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> @@ -4941,19 +7663,29 @@ It can happen because of some bug or when the connection is compromised.</source <target>Сервери для нових підключень вашого поточного профілю чату **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Theme" xml:space="preserve"> - <source>Theme</source> - <target>Тема</target> + <trans-unit id="The servers for new files of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new files of your current chat profile **%@**.</source> + <target>Сервери для нових файлів вашого поточного профілю чату **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve"> - <source>There should be at least one user profile.</source> - <target>Повинен бути принаймні один профіль користувача.</target> + <trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve"> + <source>The text you pasted is not a SimpleX link.</source> + <target>Текст, який ви вставили, не є посиланням SimpleX.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> - <source>There should be at least one visible user profile.</source> - <target>Повинен бути принаймні один видимий профіль користувача.</target> + <trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve"> + <source>The uploaded database archive will be permanently removed from the servers.</source> + <target>Завантажений архів бази даних буде назавжди видалено з серверів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Themes" xml:space="preserve"> + <source>Themes</source> + <target>Теми</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="These conditions will also apply for: **%@**." xml:space="preserve"> + <source>These conditions will also apply for: **%@**.</source> + <target>Ці умови також поширюються на: **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> @@ -4976,13 +7708,33 @@ It can happen because of some bug or when the connection is compromised.</source <target>Цю дію неможливо скасувати - повідомлення, надіслані та отримані раніше, ніж вибрані, будуть видалені. Це може зайняти кілька хвилин.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> <target>Цю дію неможливо скасувати - ваш профіль, контакти, повідомлення та файли будуть безповоротно втрачені.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by end-to-end encryption.</source> + <target>Цей чат захищений наскрізним шифруванням.</target> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by quantum resistant end-to-end encryption.</source> + <target>Цей чат захищений квантово-стійким наскрізним шифруванням.</target> + <note>E2EE info chat item</note> + </trans-unit> <trans-unit id="This device name" xml:space="preserve"> <source>This device name</source> + <target>Це ім'я пристрою</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve"> + <source>This display name is invalid. Please choose another name.</source> + <target>Це ім'я для відображення є недійсним. Будь ласка, виберіть інше ім'я.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> @@ -4997,10 +7749,25 @@ It can happen because of some bug or when the connection is compromised.</source </trans-unit> <trans-unit id="This is your own SimpleX address!" xml:space="preserve"> <source>This is your own SimpleX address!</source> + <target>Це ваша власна SimpleX-адреса!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This is your own one-time link!" xml:space="preserve"> <source>This is your own one-time link!</source> + <target>Це ваше власне одноразове посилання!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve"> + <source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve"> + <source>This link was used with another mobile device, please create a new link on the desktop.</source> + <target>Це посилання було використано з іншого мобільного пристрою, будь ласка, створіть нове посилання на робочому столі.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This message was deleted or not received yet." xml:space="preserve"> + <source>This message was deleted or not received yet.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> @@ -5008,6 +7775,11 @@ It can happen because of some bug or when the connection is compromised.</source <target>Це налаштування застосовується до повідомлень у вашому поточному профілі чату **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Title" xml:space="preserve"> + <source>Title</source> + <target>Заголовок</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> <source>To ask any questions and to receive updates:</source> <target>Задати будь-які питання та отримувати новини:</target> @@ -5020,6 +7792,7 @@ It can happen because of some bug or when the connection is compromised.</source </trans-unit> <trans-unit id="To hide unwanted messages." xml:space="preserve"> <source>To hide unwanted messages.</source> + <target>Приховати небажані повідомлення.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To make a new connection" xml:space="preserve"> @@ -5027,9 +7800,9 @@ It can happen because of some bug or when the connection is compromised.</source <target>Щоб створити нове з'єднання</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <target>Щоб захистити конфіденційність, замість ідентифікаторів користувачів, які використовуються на всіх інших платформах, SimpleX має ідентифікатори для черг повідомлень, окремі для кожного з ваших контактів.</target> + <trans-unit id="To protect against your link being replaced, you can compare contact security codes." xml:space="preserve"> + <source>To protect against your link being replaced, you can compare contact security codes.</source> + <target>Щоб захиститися від заміни вашого посилання, ви можете порівняти коди безпеки контактів.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -5037,6 +7810,11 @@ It can happen because of some bug or when the connection is compromised.</source <target>Для захисту часового поясу у файлах зображень/голосу використовується UTC.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve"> + <source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source> + <target>Щоб захистити вашу IP-адресу, приватна маршрутизація використовує ваші SMP-сервери для доставки повідомлень.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> @@ -5044,6 +7822,26 @@ You will be prompted to complete authentication before this feature is enabled.< Перед увімкненням цієї функції вам буде запропоновано пройти автентифікацію.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> + <target>Щоб захистити конфіденційність, замість ідентифікаторів користувачів, які використовуються на всіх інших платформах, SimpleX має ідентифікатори для черг повідомлень, окремі для кожного з ваших контактів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To receive" xml:space="preserve"> + <source>To receive</source> + <target>Щоб отримати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve"> + <source>To record speech please grant permission to use Microphone.</source> + <target>Для запису промови, будь ласка, надайте дозвіл на використання мікрофону.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record video please grant permission to use Camera." xml:space="preserve"> + <source>To record video please grant permission to use Camera.</source> + <target>Для запису відео, будь ласка, надайте дозвіл на використання камери.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> <target>Щоб записати голосове повідомлення, будь ласка, надайте дозвіл на використання мікрофону.</target> @@ -5054,18 +7852,48 @@ You will be prompted to complete authentication before this feature is enabled.< <target>Щоб відкрити свій прихований профіль, введіть повний пароль у поле пошуку на сторінці **Ваші профілі чату**.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To send" xml:space="preserve"> + <source>To send</source> + <target>Щоб відправити</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> <source>To support instant push notifications the chat database has to be migrated.</source> <target>Для підтримки миттєвих push-повідомлень необхідно перенести базу даних чату.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To use the servers of **%@**, accept conditions of use." xml:space="preserve"> + <source>To use the servers of **%@**, accept conditions of use.</source> + <target>Щоб користуватися серверами **%@**, прийміть умови використання.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> <target>Щоб перевірити наскрізне шифрування з вашим контактом, порівняйте (або відскануйте) код на ваших пристроях.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle chat list:" xml:space="preserve"> + <source>Toggle chat list:</source> + <target>Перемикання списку чату:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> <source>Toggle incognito when connecting.</source> + <target>Увімкніть інкогніто при підключенні.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Token status: %@." xml:space="preserve"> + <source>Token status: %@.</source> + <note>token status</note> + </trans-unit> + <trans-unit id="Toolbar opacity" xml:space="preserve"> + <source>Toolbar opacity</source> + <target>Непрозорість панелі інструментів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Total" xml:space="preserve"> + <source>Total</source> + <target>Всього</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> @@ -5073,6 +7901,11 @@ You will be prompted to complete authentication before this feature is enabled.< <target>Транспортна ізоляція</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Transport sessions" xml:space="preserve"> + <source>Transport sessions</source> + <target>Транспортні сесії</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> <target>Спроба з'єднатися з сервером, який використовується для отримання повідомлень від цього контакту (помилка: %@).</target> @@ -5083,16 +7916,16 @@ You will be prompted to complete authentication before this feature is enabled.< <target>Спроба з'єднатися з сервером, який використовується для отримання повідомлень від цього контакту.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Turkish interface" xml:space="preserve"> + <source>Turkish interface</source> + <target>Турецький інтерфейс</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> <target>Вимкнути</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve"> - <source>Turn off notifications?</source> - <target>Вимкнути сповіщення?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> <target>Ввімкнути</target> @@ -5105,20 +7938,33 @@ You will be prompted to complete authentication before this feature is enabled.< </trans-unit> <trans-unit id="Unblock" xml:space="preserve"> <source>Unblock</source> + <target>Розблoкувати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unblock for all" xml:space="preserve"> + <source>Unblock for all</source> + <target>Розблокування для всіх</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unblock member" xml:space="preserve"> <source>Unblock member</source> + <target>Розблокувати учасника</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unblock member for all?" xml:space="preserve"> + <source>Unblock member for all?</source> + <target>Розблокувати учасника для всіх?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unblock member?" xml:space="preserve"> <source>Unblock member?</source> + <target>Розблокувати учасника?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve"> - <source>Unexpected error: %@</source> - <target>Неочікувана помилка: %@</target> - <note>item status description</note> + <trans-unit id="Undelivered messages" xml:space="preserve"> + <source>Undelivered messages</source> + <target>Недоставлені повідомлення</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -5128,7 +7974,7 @@ You will be prompted to complete authentication before this feature is enabled.< <trans-unit id="Unfav." xml:space="preserve"> <source>Unfav.</source> <target>Нелюб.</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> @@ -5165,6 +8011,11 @@ You will be prompted to complete authentication before this feature is enabled.< <target>Невідома помилка</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unknown servers!" xml:space="preserve"> + <source>Unknown servers!</source> + <target>Невідомі сервери!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> <target>Якщо ви не користуєтеся інтерфейсом виклику iOS, увімкніть режим "Не турбувати", щоб уникнути переривань.</target> @@ -5179,10 +8030,12 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Unlink" xml:space="preserve"> <source>Unlink</source> + <target>Роз'єднати зв'язок</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unlink desktop?" xml:space="preserve"> <source>Unlink desktop?</source> + <target>Від'єднати робочий стіл?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unlock" xml:space="preserve"> @@ -5198,11 +8051,20 @@ To connect, please ask your contact to create another connection link and check <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> <target>Увімкнути звук</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> </trans-unit> <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> <target>Непрочитане</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Unsupported connection link" xml:space="preserve"> + <source>Unsupported connection link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve"> + <source>Up to 100 last messages are sent to new members.</source> + <target>Новим користувачам надсилається до 100 останніх повідомлень.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update" xml:space="preserve"> @@ -5210,11 +8072,6 @@ To connect, please ask your contact to create another connection link and check <target>Оновлення</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> - <source>Update .onion hosts setting?</source> - <target>Оновити налаштування хостів .onion?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> <target>Оновити парольну фразу бази даних</target> @@ -5225,9 +8082,13 @@ To connect, please ask your contact to create another connection link and check <target>Оновити налаштування мережі?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve"> - <source>Update transport isolation mode?</source> - <target>Оновити режим транспортної ізоляції?</target> + <trans-unit id="Update settings?" xml:space="preserve"> + <source>Update settings?</source> + <target>Оновити налаштування?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updated conditions" xml:space="preserve"> + <source>Updated conditions</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> @@ -5235,31 +8096,69 @@ To connect, please ask your contact to create another connection link and check <target>Оновлення налаштувань призведе до перепідключення клієнта до всіх серверів.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> - <target>Оновлення цього параметра призведе до перепідключення клієнта до всіх серверів.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Upgrade and open chat" xml:space="preserve"> <source>Upgrade and open chat</source> <target>Оновлення та відкритий чат</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Upload errors" xml:space="preserve"> + <source>Upload errors</source> + <target>Помилки завантаження</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload failed" xml:space="preserve"> + <source>Upload failed</source> + <target>Не вдалося завантфжити</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Upload file" xml:space="preserve"> <source>Upload file</source> <target>Завантажити файл</target> <note>server test step</note> </trans-unit> + <trans-unit id="Uploaded" xml:space="preserve"> + <source>Uploaded</source> + <target>Завантажено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploaded files" xml:space="preserve"> + <source>Uploaded files</source> + <target>Завантажені файли</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploading archive" xml:space="preserve"> + <source>Uploading archive</source> + <target>Завантаження архіву</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use %@" xml:space="preserve"> + <source>Use %@</source> + <target>Використовуйте %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> <target>Використовуйте хости .onion</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use SOCKS proxy" xml:space="preserve"> + <source>Use SOCKS proxy</source> + <target>Використовуйте SOCKS проксі</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> <target>Використовувати сервери SimpleX Chat?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use TCP port %@ when no port is specified." xml:space="preserve"> + <source>Use TCP port %@ when no port is specified.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve"> + <source>Use TCP port 443 for preset servers only.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> <target>Використовуйте чат</target> @@ -5270,6 +8169,16 @@ To connect, please ask your contact to create another connection link and check <target>Використовувати поточний профіль</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use for files" xml:space="preserve"> + <source>Use for files</source> + <target>Використовуйте для файлів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for messages" xml:space="preserve"> + <source>Use for messages</source> + <target>Використовуйте для повідомлень</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Використовуйте для нових з'єднань</target> @@ -5277,6 +8186,7 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Use from desktop" xml:space="preserve"> <source>Use from desktop</source> + <target>Використання з робочого столу</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use iOS call interface" xml:space="preserve"> @@ -5289,19 +8199,57 @@ To connect, please ask your contact to create another connection link and check <target>Використовуйте новий профіль інкогніто</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use only local notifications?" xml:space="preserve"> + <source>Use only local notifications?</source> + <target>Використовувати лише локальні сповіщення?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve"> + <source>Use private routing with unknown servers when IP address is not protected.</source> + <target>Використовуйте приватну маршрутизацію з невідомими серверами, якщо IP-адреса не захищена.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers." xml:space="preserve"> + <source>Use private routing with unknown servers.</source> + <target>Використовуйте приватну маршрутизацію з невідомими серверами.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Використовувати сервер</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> - <target>Профіль користувача</target> + <trans-unit id="Use servers" xml:space="preserve"> + <source>Use servers</source> + <target>Використовуйте сервери</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> - <target>Для використання хостів .onion потрібен сумісний VPN-провайдер.</target> + <trans-unit id="Use short links (BETA)" xml:space="preserve"> + <source>Use short links (BETA)</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app while in the call." xml:space="preserve"> + <source>Use the app while in the call.</source> + <target>Використовуйте додаток під час розмови.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app with one hand." xml:space="preserve"> + <source>Use the app with one hand.</source> + <target>Використовуйте додаток однією рукою.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use web port" xml:space="preserve"> + <source>Use web port</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="User selection" xml:space="preserve"> + <source>User selection</source> + <target>Вибір користувача</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Username" xml:space="preserve"> + <source>Username</source> + <target>Ім'я користувача</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> @@ -5311,10 +8259,12 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Verify code with desktop" xml:space="preserve"> <source>Verify code with desktop</source> + <target>Перевірте код на робочому столі</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify connection" xml:space="preserve"> <source>Verify connection</source> + <target>Перевірте з'єднання</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify connection security" xml:space="preserve"> @@ -5324,6 +8274,17 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Verify connections" xml:space="preserve"> <source>Verify connections</source> + <target>Пeревірте з'єднання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify database passphrase" xml:space="preserve"> + <source>Verify database passphrase</source> + <target>Перевірте пароль до бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify passphrase" xml:space="preserve"> + <source>Verify passphrase</source> + <target>Підтвердіть парольну фразу</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify security code" xml:space="preserve"> @@ -5338,6 +8299,7 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Via secure quantum resistant protocol." xml:space="preserve"> <source>Via secure quantum resistant protocol.</source> + <target>Через безпечний квантово-стійкий протокол.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Video call" xml:space="preserve"> @@ -5360,11 +8322,26 @@ To connect, please ask your contact to create another connection link and check <target>Відео та файли до 1 Гб</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View conditions" xml:space="preserve"> + <source>View conditions</source> + <target>Умови перегляду</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> <target>Переглянути код безпеки</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View updated conditions" xml:space="preserve"> + <source>View updated conditions</source> + <target>Переглянути оновлені умови</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Visible history" xml:space="preserve"> + <source>Visible history</source> + <target>Видима історія</target> + <note>chat feature</note> + </trans-unit> <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> <target>Голосові повідомлення</target> @@ -5375,11 +8352,16 @@ To connect, please ask your contact to create another connection link and check <target>Голосові повідомлення в цьому чаті заборонені.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <target>Голосові повідомлення в цій групі заборонені.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Voice messages not allowed" xml:space="preserve"> + <source>Voice messages not allowed</source> + <target>Голосові повідомлення заборонені</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> <source>Voice messages prohibited!</source> <target>Голосові повідомлення заборонені!</target> @@ -5392,6 +8374,7 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Waiting for desktop..." xml:space="preserve"> <source>Waiting for desktop...</source> + <target>Чекаємо на десктопну версію...</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Waiting for file" xml:space="preserve"> @@ -5409,6 +8392,21 @@ To connect, please ask your contact to create another connection link and check <target>Чекаємо на відео</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wallpaper accent" xml:space="preserve"> + <source>Wallpaper accent</source> + <target>Акцент на шпалерах</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper background" xml:space="preserve"> + <source>Wallpaper background</source> + <target>Фон шпалер</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve"> + <source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source> + <target>Попередження: запуск чату на декількох пристроях не підтримується і може призвести до збоїв у доставці повідомлень</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> <source>Warning: you may lose some data!</source> <target>Попередження: ви можете втратити деякі дані!</target> @@ -5429,6 +8427,11 @@ To connect, please ask your contact to create another connection link and check <target>Вітальне повідомлення</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Welcome message is too long" xml:space="preserve"> + <source>Welcome message is too long</source> + <target>Привітальне повідомлення занадто довге</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="What's new" xml:space="preserve"> <source>What's new</source> <target>Що нового</target> @@ -5439,9 +8442,14 @@ To connect, please ask your contact to create another connection link and check <target>За наявності</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> - <source>When people request to connect, you can accept or reject it.</source> - <target>Коли люди звертаються із запитом на підключення, ви можете прийняти або відхилити його.</target> + <trans-unit id="When connecting audio and video calls." xml:space="preserve"> + <source>When connecting audio and video calls.</source> + <target>При підключенні аудіо та відеодзвінків.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When more than one operator is enabled, none of them has metadata to learn who communicates with whom." xml:space="preserve"> + <source>When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</source> + <target>Коли увімкнено більше одного оператора, жоден з них не має метаданих, щоб дізнатися, хто з ким спілкується.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> @@ -5449,29 +8457,74 @@ To connect, please ask your contact to create another connection link and check <target>Коли ви ділитеся з кимось своїм профілем інкогніто, цей профіль буде використовуватися для груп, до яких вас запрошують.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="WiFi" xml:space="preserve"> + <source>WiFi</source> + <target>WiFi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Will be enabled in direct chats!" xml:space="preserve"> + <source>Will be enabled in direct chats!</source> + <target>Буде ввімкнено в прямих чатах!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wired ethernet" xml:space="preserve"> + <source>Wired ethernet</source> + <target>Дротова мережа Ethernet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With encrypted files and media." xml:space="preserve"> + <source>With encrypted files and media.</source> + <target>З зашифрованими файлами та медіа.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> <target>З необов'язковим вітальним повідомленням.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="With reduced battery usage." xml:space="preserve"> + <source>With reduced battery usage.</source> + <target>З меншим споживанням заряду акумулятора.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to file servers.</source> + <target>Без Tor або VPN ваша IP-адреса буде видимою для файлових серверів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source> + <target>Без Tor або VPN ваша IP-адреса буде видимою для цих XFTP-ретрансляторів: %@.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> <target>Неправильний пароль до бази даних</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve"> + <source>Wrong key or unknown connection - most likely this connection is deleted.</source> + <target>Неправильний ключ або невідоме з'єднання - швидше за все, це з'єднання видалено.</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Wrong key or unknown file chunk address - most likely file is deleted." xml:space="preserve"> + <source>Wrong key or unknown file chunk address - most likely file is deleted.</source> + <target>Неправильний ключ або невідома адреса фрагмента файлу - найімовірніше, файл видалено.</target> + <note>file error text</note> + </trans-unit> <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> <target>Неправильний пароль!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="XFTP servers" xml:space="preserve"> - <source>XFTP servers</source> - <target>Сервери XFTP</target> + <trans-unit id="XFTP server" xml:space="preserve"> + <source>XFTP server</source> + <target>XFTP-сервер</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> - <target>Ти</target> + <trans-unit id="You **must not** use the same database on two devices." xml:space="preserve"> + <source>You **must not** use the same database on two devices.</source> + <target>Ви **не повинні використовувати** одну і ту ж базу даних на двох пристроях.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You accepted connection" xml:space="preserve"> @@ -5494,33 +8547,46 @@ To connect, please ask your contact to create another connection link and check <target>Ви вже підключені до %@.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are already connected with %@." xml:space="preserve"> + <source>You are already connected with %@.</source> + <target>Ви вже підключені до %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You are already connecting to %@." xml:space="preserve"> <source>You are already connecting to %@.</source> + <target>Ви вже з'єднані з %@.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already connecting via this one-time link!" xml:space="preserve"> <source>You are already connecting via this one-time link!</source> + <target>Ви вже підключаєтеся до %@.Ви вже підключаєтеся за цим одноразовим посиланням!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already in group %@." xml:space="preserve"> <source>You are already in group %@.</source> + <target>Ви вже перебуваєте в групі %@.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already joining the group %@." xml:space="preserve"> <source>You are already joining the group %@.</source> + <target>Ви вже приєдналися до групи %@.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already joining the group via this link!" xml:space="preserve"> <source>You are already joining the group via this link!</source> + <target>Ви вже приєдналися до групи за цим посиланням!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already joining the group via this link." xml:space="preserve"> <source>You are already joining the group via this link.</source> + <target>Ви вже приєдналися до групи за цим посиланням.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already joining the group! Repeat join request?" xml:space="preserve"> <source>You are already joining the group! Repeat join request?</source> + <target>Ви вже приєдналися до групи! +Повторити запит на приєднання?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve"> @@ -5533,14 +8599,24 @@ Repeat join request?</source> <target>Запрошуємо вас до групи</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are not connected to these servers. Private routing is used to deliver messages to them." xml:space="preserve"> + <source>You are not connected to these servers. Private routing is used to deliver messages to them.</source> + <target>Не підключені до цих серверів. Для доставлення повідомлень до них використовується приватна маршрутизація.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> <source>You can accept calls from lock screen, without device and app authentication.</source> <target>Ви можете приймати дзвінки з екрана блокування без автентифікації пристрою та програми.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <target>Ви також можете підключитися за посиланням. Якщо воно відкриється в браузері, натисніть кнопку **Відкрити в мобільному додатку**.</target> + <trans-unit id="You can change it in Appearance settings." xml:space="preserve"> + <source>You can change it in Appearance settings.</source> + <target>Ви можете змінити його в налаштуваннях зовнішнього вигляду.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can configure servers via settings." xml:space="preserve"> + <source>You can configure servers via settings.</source> + <target>Ви можете налаштувати сервери за допомогою налаштувань.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can create it later" xml:space="preserve"> @@ -5558,16 +8634,36 @@ Repeat join request?</source> <target>Ви можете увімкнути їх пізніше в налаштуваннях конфіденційності та безпеки програми.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can give another try." xml:space="preserve"> + <source>You can give another try.</source> + <target>Ви можете спробувати ще раз.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <source>You can hide or mute a user profile - swipe it to the right.</source> <target>Ви можете приховати або вимкнути звук профілю користувача - проведіть по ньому вправо.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> + <trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve"> + <source>You can make it visible to your SimpleX contacts via Settings.</source> + <target>Ви можете зробити його видимим для ваших контактів у SimpleX за допомогою налаштувань.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now chat with %@" xml:space="preserve"> + <source>You can now chat with %@</source> <target>Тепер ви можете надсилати повідомлення на адресу %@</target> <note>notification body</note> </trans-unit> + <trans-unit id="You can send messages to %@ from Archived contacts." xml:space="preserve"> + <source>You can send messages to %@ from Archived contacts.</source> + <target>Ви можете надсилати повідомлення на %@ з архівних контактів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can set connection name, to remember who the link was shared with." xml:space="preserve"> + <source>You can set connection name, to remember who the link was shared with.</source> + <target>Ви можете задати ім'я з'єднання, щоб запам'ятати, з ким ви поділилися посиланням.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> <target>Ви можете налаштувати попередній перегляд сповіщень на екрані блокування за допомогою налаштувань.</target> @@ -5583,16 +8679,16 @@ Repeat join request?</source> <target>Ви можете поділитися цією адресою зі своїми контактами, щоб вони могли зв'язатися з **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> - <source>You can share your address as a link or QR code - anybody can connect to you.</source> - <target>Ви можете поділитися своєю адресою у вигляді посилання або QR-коду - будь-хто зможе зв'язатися з вами.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> <target>Запустити чат можна через Налаштування програми / База даних або перезапустивши програму</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve"> + <source>You can still view conversation with %@ in the list of chats.</source> + <target>Ви все ще можете переглянути розмову з %@ у списку чатів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> <target>Увімкнути SimpleX Lock можна в Налаштуваннях.</target> @@ -5603,33 +8699,36 @@ Repeat join request?</source> <target>Ви можете використовувати розмітку для форматування повідомлень:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can view invitation link again in connection details." xml:space="preserve"> + <source>You can view invitation link again in connection details.</source> + <target>Ви можете переглянути посилання на запрошення ще раз у деталях підключення.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> <target>Ви не можете надсилати повідомлення!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <target>Ви контролюєте, через який(і) сервер(и) **отримувати** повідомлення, ваші контакти - сервери, які ви використовуєте для надсилання їм повідомлень.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <target>Вас не вдалося верифікувати, спробуйте ще раз.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> + <target>Ви вирішуєте, хто може під'єднатися.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You have already requested connection via this address!" xml:space="preserve"> <source>You have already requested connection via this address!</source> + <target>Ви вже надсилали запит на підключення за цією адресою!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You have already requested connection! Repeat connection request?" xml:space="preserve"> <source>You have already requested connection! Repeat connection request?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> - <target>У вас немає чатів</target> + <target>Ви вже надіслали запит на підключення! +Повторити запит на підключення?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> @@ -5652,11 +8751,26 @@ Repeat connection request?</source> <target>Ви приєдналися до цієї групи. Підключення до запрошеного учасника групи.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You may migrate the exported database." xml:space="preserve"> + <source>You may migrate the exported database.</source> + <target>Ви можете мігрувати експортовану базу даних.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may save the exported archive." xml:space="preserve"> + <source>You may save the exported archive.</source> + <target>Ви можете зберегти експортований архів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> <target>Ви повинні використовувати найновішу версію бази даних чату ТІЛЬКИ на одному пристрої, інакше ви можете перестати отримувати повідомлення від деяких контактів.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You need to allow your contact to call to be able to call them." xml:space="preserve"> + <source>You need to allow your contact to call to be able to call them.</source> + <target>Щоб мати змогу зателефонувати контакту, вам потрібно дозволити йому зателефонувати.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> <target>Щоб мати змогу надсилати голосові повідомлення, вам потрібно дозволити контакту надсилати їх.</target> @@ -5672,6 +8786,10 @@ Repeat connection request?</source> <target>Ви надіслали запрошення до групи</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You should receive notifications." xml:space="preserve"> + <source>You should receive notifications.</source> + <note>token info</note> + </trans-unit> <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> <target>Ви будете підключені до групи, коли пристрій господаря групи буде в мережі, будь ласка, зачекайте або перевірте пізніше!</target> @@ -5679,6 +8797,7 @@ Repeat connection request?</source> </trans-unit> <trans-unit id="You will be connected when group link host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected when group link host's device is online, please wait or check later!</source> + <target>Ви будете підключені, коли пристрій хоста групового посилання буде онлайн, будь ласка, зачекайте або перевірте пізніше!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve"> @@ -5698,6 +8817,7 @@ Repeat connection request?</source> </trans-unit> <trans-unit id="You will connect to all group members." xml:space="preserve"> <source>You will connect to all group members.</source> + <target>Ви з'єднаєтеся з усіма учасниками групи.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve"> @@ -5705,6 +8825,11 @@ Repeat connection request?</source> <target>Ви все одно отримуватимете дзвінки та сповіщення від вимкнених профілів, якщо вони активні.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You will stop receiving messages from this chat. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this chat. Chat history will be preserved.</source> + <target>Ви більше не будете отримувати повідомлення з цього чату. Історія чату буде збережена.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> <target>Ви перестанете отримувати повідомлення від цієї групи. Історія чату буде збережена.</target> @@ -5725,31 +8850,16 @@ Repeat connection request?</source> <target>Ви використовуєте профіль інкогніто для цієї групи - щоб запобігти поширенню вашого основного профілю, запрошення контактів заборонено</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your %@ servers" xml:space="preserve"> - <source>Your %@ servers</source> - <target>Ваші сервери %@</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your ICE servers" xml:space="preserve"> <source>Your ICE servers</source> <target>Ваші сервери ICE</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> - <target>Ваші SMP-сервери</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your SimpleX address" xml:space="preserve"> <source>Your SimpleX address</source> <target>Ваша адреса SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your XFTP servers" xml:space="preserve"> - <source>Your XFTP servers</source> - <target>Ваші XFTP-сервери</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your calls" xml:space="preserve"> <source>Your calls</source> <target>Твої дзвінки</target> @@ -5765,16 +8875,19 @@ Repeat connection request?</source> <target>Ваша база даних чату не зашифрована - встановіть ключову фразу, щоб зашифрувати її.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your chat preferences" xml:space="preserve"> + <source>Your chat preferences</source> + <target>Ваші налаштування чату</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>Ваші профілі чату</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> - <target>Для завершення з'єднання ваш контакт має бути онлайн. -Ви можете скасувати це з'єднання і видалити контакт (і спробувати пізніше з новим посиланням).</target> + <trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve"> + <source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source> + <target>Ваше з'єднання було переміщено на %@, але під час перенаправлення на профіль сталася несподівана помилка.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> @@ -5787,18 +8900,16 @@ You can cancel this connection and remove the contact (and try later with a new <target>Ваші контакти можуть дозволити повне видалення повідомлень.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> - <source>Your contacts in SimpleX will see it. -You can change it in Settings.</source> - <target>Ваші контакти в SimpleX побачать це. -Ви можете змінити його в Налаштуваннях.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your contacts will remain connected." xml:space="preserve"> <source>Your contacts will remain connected.</source> <target>Ваші контакти залишаться на зв'язку.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve"> + <source>Your credentials may be sent unencrypted.</source> + <target>Ваші облікові дані можуть бути надіслані незашифрованими.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> <target>Ваша поточна база даних чату буде ВИДАЛЕНА і ЗАМІНЕНА імпортованою.</target> @@ -5821,6 +8932,7 @@ You can change it in Settings.</source> </trans-unit> <trans-unit id="Your profile" xml:space="preserve"> <source>Your profile</source> + <target>Ваш профіль</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> @@ -5828,33 +8940,36 @@ You can change it in Settings.</source> <target>Ваш профіль **%@** буде опублікований.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> - <target>Ваш профіль зберігається на вашому пристрої і доступний лише вашим контактам. -Сервери SimpleX не бачать ваш профіль.</target> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> + <target>Профіль доступний лише вашим контактам.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> - <target>Ваш профіль, контакти та доставлені повідомлення зберігаються на вашому пристрої.</target> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> + <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> + <target>Ваш профіль зберігається на вашому пристрої і доступний лише вашим контактам. Сервери SimpleX не бачать ваш профіль.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve"> + <source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source> + <target>Ваш профіль було змінено. Якщо ви збережете його, оновлений профіль буде надіслано всім вашим контактам.</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> <target>Ваш випадковий профіль</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> - <target>Ваш сервер</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> <target>Адреса вашого сервера</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your servers" xml:space="preserve"> + <source>Your servers</source> + <target>Ваші сервери</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your settings" xml:space="preserve"> <source>Your settings</source> <target>Ваші налаштування</target> @@ -5895,11 +9010,21 @@ SimpleX servers cannot see your profile.</source> <target>прийнято виклик</target> <note>call status</note> </trans-unit> + <trans-unit id="accepted invitation" xml:space="preserve"> + <source>accepted invitation</source> + <target>прийняте запрошення</target> + <note>chat list item title</note> + </trans-unit> <trans-unit id="admin" xml:space="preserve"> <source>admin</source> <target>адмін</target> <note>member role</note> </trans-unit> + <trans-unit id="admins" xml:space="preserve"> + <source>admins</source> + <target>адміністратори</target> + <note>feature role</note> + </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> <target>узгодження шифрування для %@…</target> @@ -5910,6 +9035,11 @@ SimpleX servers cannot see your profile.</source> <target>узгодження шифрування…</target> <note>chat item text</note> </trans-unit> + <trans-unit id="all members" xml:space="preserve"> + <source>all members</source> + <target>всі учасники</target> + <note>feature role</note> + </trans-unit> <trans-unit id="always" xml:space="preserve"> <source>always</source> <target>завжди</target> @@ -5917,6 +9047,16 @@ SimpleX servers cannot see your profile.</source> </trans-unit> <trans-unit id="and %lld other events" xml:space="preserve"> <source>and %lld other events</source> + <target>та %lld інших подій</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="archived report" xml:space="preserve"> + <source>archived report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="attempts" xml:space="preserve"> + <source>attempts</source> + <target>спроби</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> @@ -5926,6 +9066,7 @@ SimpleX servers cannot see your profile.</source> </trans-unit> <trans-unit id="author" xml:space="preserve"> <source>author</source> + <target>автор</target> <note>member role</note> </trans-unit> <trans-unit id="bad message ID" xml:space="preserve"> @@ -5940,13 +9081,30 @@ SimpleX servers cannot see your profile.</source> </trans-unit> <trans-unit id="blocked" xml:space="preserve"> <source>blocked</source> - <note>No comment provided by engineer.</note> + <target>заблоковано</target> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="blocked %@" xml:space="preserve"> + <source>blocked %@</source> + <target>заблоковано %@</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="blocked by admin" xml:space="preserve"> + <source>blocked by admin</source> + <target>заблоковано адміністратором</target> + <note>blocked chat item +marked deleted chat item preview text</note> </trans-unit> <trans-unit id="bold" xml:space="preserve"> <source>bold</source> <target>жирний</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="call" xml:space="preserve"> + <source>call</source> + <target>дзвонити</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="call error" xml:space="preserve"> <source>call error</source> <target>помилка дзвінка</target> @@ -6014,6 +9172,7 @@ SimpleX servers cannot see your profile.</source> </trans-unit> <trans-unit id="connected directly" xml:space="preserve"> <source>connected directly</source> + <target>з'єднані безпосередньо</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="connecting" xml:space="preserve"> @@ -6049,7 +9208,7 @@ SimpleX servers cannot see your profile.</source> <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> <target>з'єднання…</target> - <note>chat list item title</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> @@ -6061,6 +9220,11 @@ SimpleX servers cannot see your profile.</source> <target>з'єднання:%@</target> <note>connection information</note> </trans-unit> + <trans-unit id="contact %@ changed to %@" xml:space="preserve"> + <source>contact %1$@ changed to %2$@</source> + <target>контакт %1$@ змінено на %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> <target>контакт має шифрування e2e</target> @@ -6091,10 +9255,16 @@ SimpleX servers cannot see your profile.</source> <target>днів</target> <note>time unit</note> </trans-unit> + <trans-unit id="decryption errors" xml:space="preserve"> + <source>decryption errors</source> + <target>помилки розшифровки</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> <target>за замовчуванням (%@)</target> - <note>pref value</note> + <note>delete after time +pref value</note> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> @@ -6113,6 +9283,7 @@ SimpleX servers cannot see your profile.</source> </trans-unit> <trans-unit id="deleted contact" xml:space="preserve"> <source>deleted contact</source> + <target>видалений контакт</target> <note>rcv direct event chat item</note> </trans-unit> <trans-unit id="deleted group" xml:space="preserve"> @@ -6140,6 +9311,11 @@ SimpleX servers cannot see your profile.</source> <target>дублююче повідомлення</target> <note>integrity error chat item</note> </trans-unit> + <trans-unit id="duplicates" xml:space="preserve"> + <source>duplicates</source> + <target>дублікати</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> <target>e2e зашифрований</target> @@ -6215,9 +9391,14 @@ SimpleX servers cannot see your profile.</source> <target>помилка</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="event happened" xml:space="preserve"> - <source>event happened</source> - <target>відбулася подія</target> + <trans-unit id="expired" xml:space="preserve"> + <source>expired</source> + <target>закінчився</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="forwarded" xml:space="preserve"> + <source>forwarded</source> + <target>переслано</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> @@ -6245,6 +9426,11 @@ SimpleX servers cannot see your profile.</source> <target>Пароль бази даних буде безпечно збережено в iOS Keychain після запуску чату або зміни пароля - це дасть змогу отримувати миттєві повідомлення.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="inactive" xml:space="preserve"> + <source>inactive</source> + <target>неактивний</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> <target>інкогніто за посиланням на контактну адресу</target> @@ -6285,6 +9471,11 @@ SimpleX servers cannot see your profile.</source> <target>запрошення до групи %@</target> <note>group name</note> </trans-unit> + <trans-unit id="invite" xml:space="preserve"> + <source>invite</source> + <target>запросити</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> <target>запрошені</target> @@ -6330,11 +9521,21 @@ SimpleX servers cannot see your profile.</source> <target>учасник</target> <note>member role</note> </trans-unit> + <trans-unit id="member %@ changed to %@" xml:space="preserve"> + <source>member %1$@ changed to %2$@</source> + <target>учасника %1$@ змінено на %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> <target>з'єднаний</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="message" xml:space="preserve"> + <source>message</source> + <target>повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="message received" xml:space="preserve"> <source>message received</source> <target>повідомлення отримано</target> @@ -6358,7 +9559,11 @@ SimpleX servers cannot see your profile.</source> <trans-unit id="moderated by %@" xml:space="preserve"> <source>moderated by %@</source> <target>модерується %@</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="moderator" xml:space="preserve"> + <source>moderator</source> + <note>member role</note> </trans-unit> <trans-unit id="months" xml:space="preserve"> <source>months</source> @@ -6368,7 +9573,7 @@ SimpleX servers cannot see your profile.</source> <trans-unit id="never" xml:space="preserve"> <source>never</source> <target>ніколи</target> - <note>No comment provided by engineer.</note> + <note>delete after time</note> </trans-unit> <trans-unit id="new message" xml:space="preserve"> <source>new message</source> @@ -6399,8 +9604,8 @@ SimpleX servers cannot see your profile.</source> <source>off</source> <target>вимкнено</target> <note>enabled status - group pref value - time to disappear</note> +group pref value +time to disappear</note> </trans-unit> <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> @@ -6414,19 +9619,47 @@ SimpleX servers cannot see your profile.</source> </trans-unit> <trans-unit id="on" xml:space="preserve"> <source>on</source> - <target>увімкнено</target> + <target>увімкненo</target> <note>group pref value</note> </trans-unit> + <trans-unit id="other" xml:space="preserve"> + <source>other</source> + <target>інший</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="other errors" xml:space="preserve"> + <source>other errors</source> + <target>інші помилки</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="owner" xml:space="preserve"> <source>owner</source> <target>власник</target> <note>member role</note> </trans-unit> + <trans-unit id="owners" xml:space="preserve"> + <source>owners</source> + <target>власники</target> + <note>feature role</note> + </trans-unit> <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> <target>одноранговий</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="pending" xml:space="preserve"> + <source>pending</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="pending approval" xml:space="preserve"> + <source>pending approval</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="quantum resistant e2e encryption" xml:space="preserve"> + <source>quantum resistant e2e encryption</source> + <target>квантово-стійке шифрування e2e</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> <target>отримали відповідь…</target> @@ -6437,6 +9670,10 @@ SimpleX servers cannot see your profile.</source> <target>отримали підтвердження…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="rejected" xml:space="preserve"> + <source>rejected</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> <target>відхилений виклик</target> @@ -6452,11 +9689,41 @@ SimpleX servers cannot see your profile.</source> <target>видалено %@</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="removed contact address" xml:space="preserve"> + <source>removed contact address</source> + <target>видалено контактну адресу</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="removed profile picture" xml:space="preserve"> + <source>removed profile picture</source> + <target>видалено зображення профілю</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> <target>прибрали вас</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="requested to connect" xml:space="preserve"> + <source>requested to connect</source> + <target>запит на підключення</target> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="saved" xml:space="preserve"> + <source>saved</source> + <target>збережено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="saved from %@" xml:space="preserve"> + <source>saved from %@</source> + <target>збережено з %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="search" xml:space="preserve"> + <source>search</source> + <target>пошук</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="sec" xml:space="preserve"> <source>sec</source> <target>сек</target> @@ -6479,8 +9746,33 @@ SimpleX servers cannot see your profile.</source> </trans-unit> <trans-unit id="send direct message" xml:space="preserve"> <source>send direct message</source> + <target>надіслати пряме повідомлення</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve"> + <source>server queue info: %1$@ + +last received msg: %2$@</source> + <target>інформація про чергу на сервері: %1$@ + +останнє отримане повідомлення: %2$@</target> + <note>queue info</note> + </trans-unit> + <trans-unit id="set new contact address" xml:space="preserve"> + <source>set new contact address</source> + <target>встановити нову контактну адресу</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="set new profile picture" xml:space="preserve"> + <source>set new profile picture</source> + <target>встановити нове зображення профілю</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="standard end-to-end encryption" xml:space="preserve"> + <source>standard end-to-end encryption</source> + <target>стандартне наскрізне шифрування</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>починаючи…</target> @@ -6496,18 +9788,44 @@ SimpleX servers cannot see your profile.</source> <target>цей контакт</target> <note>notification title</note> </trans-unit> + <trans-unit id="unblocked %@" xml:space="preserve"> + <source>unblocked %@</source> + <target>розблоковано %@</target> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="unknown" xml:space="preserve"> <source>unknown</source> <target>невідомий</target> <note>connection info</note> </trans-unit> + <trans-unit id="unknown servers" xml:space="preserve"> + <source>unknown servers</source> + <target>невідомі реле</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unknown status" xml:space="preserve"> + <source>unknown status</source> + <target>невідомий статус</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unprotected" xml:space="preserve"> + <source>unprotected</source> + <target>незахищені</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="updated group profile" xml:space="preserve"> <source>updated group profile</source> <target>оновлений профіль групи</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="updated profile" xml:space="preserve"> + <source>updated profile</source> + <target>оновлений профіль</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="v%@" xml:space="preserve"> <source>v%@</source> + <target>v%@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="v%@ (%@)" xml:space="preserve"> @@ -6535,6 +9853,11 @@ SimpleX servers cannot see your profile.</source> <target>за допомогою ретранслятора</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="video" xml:space="preserve"> + <source>video</source> + <target>відео</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> <source>video call (not e2e encrypted)</source> <target>відеодзвінок (без шифрування e2e)</target> @@ -6560,11 +9883,21 @@ SimpleX servers cannot see your profile.</source> <target>тижнів</target> <note>time unit</note> </trans-unit> + <trans-unit id="when IP hidden" xml:space="preserve"> + <source>when IP hidden</source> + <target>коли IP приховано</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="yes" xml:space="preserve"> <source>yes</source> <target>так</target> <note>pref value</note> </trans-unit> + <trans-unit id="you" xml:space="preserve"> + <source>you</source> + <target>ти</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> <target>вас запрошують до групи</target> @@ -6575,6 +9908,11 @@ SimpleX servers cannot see your profile.</source> <target>ви спостерігач</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="you blocked %@" xml:space="preserve"> + <source>you blocked %@</source> + <target>ви заблокували %@</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> <target>ви змінили адресу</target> @@ -6615,6 +9953,11 @@ SimpleX servers cannot see your profile.</source> <target>ви поділилися одноразовим посиланням інкогніто</target> <note>chat list item description</note> </trans-unit> + <trans-unit id="you unblocked %@" xml:space="preserve"> + <source>you unblocked %@</source> + <target>ви розблокували %@</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you: " xml:space="preserve"> <source>you: </source> <target>ти: </target> @@ -6629,7 +9972,7 @@ SimpleX servers cannot see your profile.</source> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="uk" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6649,6 +9992,7 @@ SimpleX servers cannot see your profile.</source> </trans-unit> <trans-unit id="NSLocalNetworkUsageDescription" xml:space="preserve"> <source>SimpleX uses local network access to allow using user chat profile via desktop app on the same network.</source> + <target>SimpleX використовує доступ до локальної мережі, щоб дозволити користувачеві користуватися профілем чату через десктопну програму в тій же мережі.</target> <note>Privacy - Local Network Usage Description</note> </trans-unit> <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> @@ -6665,7 +10009,7 @@ SimpleX servers cannot see your profile.</source> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="uk" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> @@ -6685,4 +10029,249 @@ SimpleX servers cannot see your profile.</source> </trans-unit> </body> </file> + <file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="uk" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%d new events" xml:space="preserve"> + <source>%d new events</source> + <target>%d нових подій</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="From %d chat(s)" xml:space="preserve"> + <source>From %d chat(s)</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From: %@" xml:space="preserve"> + <source>From: %@</source> + <target>Від: %@</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>Нові події</target> + <note>notification</note> + </trans-unit> + <trans-unit id="New messages" xml:space="preserve"> + <source>New messages</source> + <target>Нові повідомлення</target> + <note>notification</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="uk" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle display name</note> + </trans-unit> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> + <source>Copyright © 2024 SimpleX Chat. All rights reserved.</source> + <target>Copyright © 2024 SimpleX Chat. Всі права захищені.</target> + <note>Copyright (human-readable)</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="uk" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <target>%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App is locked!" xml:space="preserve"> + <source>App is locked!</source> + <target>Додаток заблоковано!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <target>Скасувати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <target>Не вдається отримати доступ до зв'язки ключів для збереження пароля до бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>Неможливо переслати повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Comment" xml:space="preserve"> + <source>Comment</source> + <target>Коментар</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <target>Наразі максимальний підтримуваний розмір файлу - %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade required" xml:space="preserve"> + <source>Database downgrade required</source> + <target>Потрібне оновлення бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <target>База даних зашифрована!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <target>Помилка в базі даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <target>Парольна фраза бази даних відрізняється від збереженої у в’язці ключів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <target>Для відкриття чату потрібно ввести пароль до бази даних.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade required" xml:space="preserve"> + <source>Database upgrade required</source> + <target>Потрібне оновлення бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing file" xml:space="preserve"> + <source>Error preparing file</source> + <target>Помилка підготовки файлу</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing message" xml:space="preserve"> + <source>Error preparing message</source> + <target>Повідомлення про підготовку до помилки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <target>Помилка: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>Помилка файлу</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <target>Несумісна версія бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>Недійсне підтвердження міграції</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <target>Помилка зв'язки ключів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <target>Великий файл!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No active profile" xml:space="preserve"> + <source>No active profile</source> + <target>Немає активного профілю</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <target>Гаразд</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to downgrade the database." xml:space="preserve"> + <source>Open the app to downgrade the database.</source> + <target>Відкрийте програму, щоб знизити версію бази даних.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to upgrade the database." xml:space="preserve"> + <source>Open the app to upgrade the database.</source> + <target>Відкрийте програму, щоб оновити базу даних.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passphrase" xml:space="preserve"> + <source>Passphrase</source> + <target>Парольна фраза</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve"> + <source>Please create a profile in the SimpleX app</source> + <target>Будь ласка, створіть профіль у додатку SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>Вибрані налаштування чату забороняють це повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending a message takes longer than expected." xml:space="preserve"> + <source>Sending a message takes longer than expected.</source> + <target>Надсилання повідомлення займає більше часу, ніж очікувалося.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending message…" xml:space="preserve"> + <source>Sending message…</source> + <target>Надсилаю повідомлення…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <target>Поділіться</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Slow network?" xml:space="preserve"> + <source>Slow network?</source> + <target>Повільна мережа?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <target>Невідома помилка бази даних: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unsupported format" xml:space="preserve"> + <source>Unsupported format</source> + <target>Непідтримуваний формат</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wait" xml:space="preserve"> + <source>Wait</source> + <target>Зачекай</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <target>Неправильна ключова фраза до бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve"> + <source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source> + <target>Ви можете дозволити спільний доступ у налаштуваннях Конфіденційність і безпека / SimpleX Lock.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + </body> + </file> </xliff> diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/contents.json b/apps/ios/SimpleX Localizations/uk.xcloc/contents.json index 6c122f11ab..a93c702952 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/uk.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "uk", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "16.2" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index d96537be3e..d5411f86e3 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -2,36 +2,9 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="zh-Hans" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> - <trans-unit id=" " xml:space="preserve"> - <source> -</source> - <target> -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" " xml:space="preserve"> - <source> </source> - <target> </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=" (" xml:space="preserve"> - <source> (</source> - <target> (</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> <target> (可复制)</target> @@ -89,6 +62,7 @@ </trans-unit> <trans-unit id="%@ and %@" xml:space="preserve"> <source>%@ and %@</source> + <target>%@ 和 %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ and %@ connected" xml:space="preserve"> @@ -103,6 +77,12 @@ </trans-unit> <trans-unit id="%@ connected" xml:space="preserve"> <source>%@ connected</source> + <target>%@ 已连接</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve"> + <source>%@ downloaded</source> + <target>%@ 已下载</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ is connected!" xml:space="preserve"> @@ -120,9 +100,19 @@ <target>%@ 已认证</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ server" xml:space="preserve"> + <source>%@ server</source> + <target>服务器</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ servers" xml:space="preserve"> <source>%@ servers</source> - <target>%@ 服务器</target> + <target>服务器</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve"> + <source>%@ uploaded</source> + <target>%@ 已上传</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ wants to connect!" xml:space="preserve"> @@ -130,8 +120,14 @@ <target>%@ 要连接!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve"> + <source>%1$@, %2$@</source> + <target>%1$@, %2$@</target> + <note>format for date separator in chat</note> + </trans-unit> <trans-unit id="%@, %@ and %lld members" xml:space="preserve"> <source>%@, %@ and %lld members</source> + <target>%@, %@ 和 %lld 成员</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> @@ -149,11 +145,36 @@ <target>%d 天</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve"> + <source>%d file(s) are still being downloaded.</source> + <target>仍在下载 %d 个文件。</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve"> + <source>%d file(s) failed to download.</source> + <target>%d 个文件下载失败。</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve"> + <source>%d file(s) were deleted.</source> + <target>已刪除 %d 个文件。</target> + <note>forward confirmation reason</note> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve"> + <source>%d file(s) were not downloaded.</source> + <target>未能下载 %d 个文件。</target> + <note>forward confirmation reason</note> + </trans-unit> <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> <target>%d 小时</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve"> + <source>%d messages not forwarded</source> + <target>未转发 %d 条消息</target> + <note>alert title</note> + </trans-unit> <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> <target>%d 分钟</target> @@ -169,9 +190,14 @@ <target>%d 秒</target> <note>time interval</note> </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve"> + <source>%d seconds(s)</source> + <target>%d 秒</target> + <note>delete after time</note> + </trans-unit> <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> - <target>%d 跳过消息</target> + <target>跳过的 %d 条消息</target> <note>integrity error chat item</note> </trans-unit> <trans-unit id="%d weeks" xml:space="preserve"> @@ -201,6 +227,7 @@ </trans-unit> <trans-unit id="%lld group events" xml:space="preserve"> <source>%lld group events</source> + <target>%lld 群组事件</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld members" xml:space="preserve"> @@ -210,14 +237,22 @@ </trans-unit> <trans-unit id="%lld messages blocked" xml:space="preserve"> <source>%lld messages blocked</source> + <target>%lld 条消息已屏蔽</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve"> + <source>%lld messages blocked by admin</source> + <target>%lld 被管理员阻止的消息</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld messages marked deleted" xml:space="preserve"> <source>%lld messages marked deleted</source> + <target>%lld 标记为已删除的消息</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld messages moderated by %@" xml:space="preserve"> <source>%lld messages moderated by %@</source> + <target>%lld 审核的留言 by %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld minutes" xml:space="preserve"> @@ -230,11 +265,6 @@ <target>%lld 种新的界面语言</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> - <source>%lld second(s)</source> - <target>%lld 秒</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="%lld seconds" xml:space="preserve"> <source>%lld seconds</source> <target>%lld 秒</target> @@ -285,47 +315,39 @@ <target>已跳过 %u 条消息。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> - <source>(</source> - <target>(</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="(new)" xml:space="preserve"> <source>(new)</source> + <target>(新)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="(this device v%@)" xml:space="preserve"> <source>(this device v%@)</source> + <target>(此设备 v%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve"> - <source>)</source> - <target>)</target> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target>**添加联系人**: 创建新的邀请链接,或通过您收到的链接进行连接.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target>**添加新联系人**:为您的联系人创建一次性二维码或者链接。</target> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve"> + <source>**Create group**: to create a new group.</source> + <target>**创建群组**: 创建一个新群组.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> - <source>**Create link / QR code** for your contact to use.</source> - <target>**创建链接 / 二维码** 给您的联系人使用。</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**更私密**:每20分钟检查新消息。设备令牌和 SimpleX Chat 服务器共享,但是不会共享有您有多少联系人或者消息。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**最私密**:不使用 SimpleX Chat 通知服务器,在后台定期检查消息(取决于您多经常使用应用程序)。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <target>**粘贴收到的链接**或者在浏览器里打开并且点击**在移动应用程序里打开**。</target> + <trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve"> + <source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source> + <target>**请注意**: 在两台设备上使用相同的数据库将破坏来自您的连接的消息解密,作为一种安全保护.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> @@ -333,14 +355,14 @@ <target>**请注意**:如果您丢失密码,您将无法恢复或者更改密码。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve"> + <source>**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.</source> <target>**推荐**:设备令牌和通知会发送至 SimpleX Chat 通知服务器,但是消息内容、大小或者发送人不会。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target>**扫描二维码**:见面或者通过视频通话来连接您的联系人。</target> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve"> + <source>**Scan / Paste link**: to connect via a link you received.</source> + <target>**扫描/粘贴链接**:用您收到的链接连接。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> @@ -348,6 +370,11 @@ <target>**警告**:及时推送通知需要保存在钥匙串的密码。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="**Warning**: the archive will be removed." xml:space="preserve"> + <source>**Warning**: the archive will be removed.</source> + <target>**警告**: 存档将被删除.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> <source>**e2e encrypted** audio call</source> <target>**端到端加密** 语音通话</target> @@ -363,11 +390,6 @@ <target>\*加粗*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=", " xml:space="preserve"> - <source>, </source> - <target>, </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). @@ -390,6 +412,9 @@ <source>- optionally notify deleted contacts. - profile names with spaces. - and more!</source> + <target>- 可选择通知已删除的联系人。 +- 带空格的个人资料名称。 +- 以及更多!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="- voice messages up to 5 minutes. - custom time to disappear. - editing history." xml:space="preserve"> @@ -401,13 +426,9 @@ - 编辑消息历史。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="." xml:space="preserve"> - <source>.</source> - <target>.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="0 sec" xml:space="preserve"> <source>0 sec</source> + <target>0 秒</target> <note>time to disappear</note> </trans-unit> <trans-unit id="0s" xml:space="preserve"> @@ -418,7 +439,8 @@ <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> <target>1天</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> @@ -433,18 +455,30 @@ <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> <target>1月</target> - <note>time interval</note> + <note>delete after time +time interval</note> </trans-unit> <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> <target>1周</target> - <note>time interval</note> + <note>delete after time +time interval</note> + </trans-unit> + <trans-unit id="1 year" xml:space="preserve"> + <source>1 year</source> + <target>1 年</target> + <note>delete after time</note> </trans-unit> <trans-unit id="1-time link" xml:space="preserve"> <source>1-time link</source> <target>一次性链接</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="1-time link can be used *with one contact only* - share in person or via any messenger." xml:space="preserve"> + <source>1-time link can be used *with one contact only* - share in person or via any messenger.</source> + <target>一次性链接*只能给一名联系人*使用。当面或使用聊天应用分享链接。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="5 minutes" xml:space="preserve"> <source>5 minutes</source> <target>5分钟</target> @@ -460,11 +494,6 @@ <target>30秒</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve"> - <source>: </source> - <target>: </target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> @@ -514,31 +543,32 @@ <target>中止地址更改?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve"> - <source>About SimpleX</source> - <target>关于SimpleX</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve"> <source>About SimpleX Chat</source> <target>关于SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve"> - <source>About SimpleX address</source> - <target>关于 SimpleX 地址</target> + <trans-unit id="About operators" xml:space="preserve"> + <source>About operators</source> + <target>关于运营方</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accent color" xml:space="preserve"> - <source>Accent color</source> - <target>色调</target> + <trans-unit id="Accent" xml:space="preserve"> + <source>Accent</source> + <target>强调</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept" xml:space="preserve"> <source>Accept</source> <target>接受</target> <note>accept contact request via notification - accept incoming call via notification</note> +accept incoming call via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accept conditions" xml:space="preserve"> + <source>Accept conditions</source> + <target>接受条款</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept connection request?" xml:space="preserve"> <source>Accept connection request?</source> @@ -553,16 +583,47 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>接受隐身聊天</target> - <note>accept contact request via notification</note> + <note>accept contact request via notification +swipe action</note> + </trans-unit> + <trans-unit id="Accepted conditions" xml:space="preserve"> + <source>Accepted conditions</source> + <target>已接受的条款</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledged" xml:space="preserve"> + <source>Acknowledged</source> + <target>确认</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Acknowledgement errors" xml:space="preserve"> + <source>Acknowledgement errors</source> + <target>确认错误</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Active" xml:space="preserve"> + <source>Active</source> + <target>活跃</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Active connections" xml:space="preserve"> + <source>Active connections</source> + <target>活动连接</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> <target>将地址添加到您的个人资料,以便您的联系人可以与其他人共享。个人资料更新将发送给您的联系人。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve"> - <source>Add preset servers</source> - <target>添加预设服务器</target> + <trans-unit id="Add friends" xml:space="preserve"> + <source>Add friends</source> + <target>添加好友</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add list" xml:space="preserve"> + <source>Add list</source> + <target>添加列表</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add profile" xml:space="preserve"> @@ -570,14 +631,19 @@ <target>添加个人资料</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add server" xml:space="preserve"> + <source>Add server</source> + <target>添加服务器</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> <source>Add servers by scanning QR codes.</source> <target>扫描二维码来添加服务器。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve"> - <source>Add server…</source> - <target>添加服务器…</target> + <trans-unit id="Add team members" xml:space="preserve"> + <source>Add team members</source> + <target>添加团队成员</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> @@ -585,11 +651,46 @@ <target>添加另一设备</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add to list" xml:space="preserve"> + <source>Add to list</source> + <target>添加到列表</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> <target>添加欢迎信息</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Add your team members to the conversations." xml:space="preserve"> + <source>Add your team members to the conversations.</source> + <target>将你的团队成员加入对话。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added media & file servers" xml:space="preserve"> + <source>Added media & file servers</source> + <target>已添加媒体和文件服务器</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Added message servers" xml:space="preserve"> + <source>Added message servers</source> + <target>已添加消息服务器</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent" xml:space="preserve"> + <source>Additional accent</source> + <target>附加重音</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional accent 2" xml:space="preserve"> + <source>Additional accent 2</source> + <target>附加重音 2</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Additional secondary" xml:space="preserve"> + <source>Additional secondary</source> + <target>附加二级</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Address" xml:space="preserve"> <source>Address</source> <target>地址</target> @@ -600,6 +701,21 @@ <target>将中止地址更改。将使用旧接收地址。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Address or 1-time link?" xml:space="preserve"> + <source>Address or 1-time link?</source> + <target>地址还是一次性链接?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address settings" xml:space="preserve"> + <source>Address settings</source> + <target>地址设置</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can block a member for all." xml:space="preserve"> + <source>Admins can block a member for all.</source> + <target>管理员可以为所有人封禁一名成员。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> <source>Admins can create the links to join groups.</source> <target>管理员可以创建链接以加入群组。</target> @@ -610,6 +726,16 @@ <target>高级网络设置</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve"> + <source>Advanced settings</source> + <target>高级设置</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All" xml:space="preserve"> + <source>All</source> + <target>全部</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All app data is deleted." xml:space="preserve"> <source>All app data is deleted.</source> <target>已删除所有应用程序数据。</target> @@ -620,16 +746,36 @@ <target>所有聊天记录和消息将被删除——这一行为无法撤销!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All chats will be removed from the list %@, and the list deleted." xml:space="preserve"> + <source>All chats will be removed from the list %@, and the list deleted.</source> + <target>列表 %@ 和其中全部聊天将被删除。</target> + <note>alert message</note> + </trans-unit> <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> <target>所有数据在输入后将被删除。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All data is kept private on your device." xml:space="preserve"> + <source>All data is kept private on your device.</source> + <target>所有数据都是您设备的私有数据.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> <target>所有群组成员将保持连接。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve"> + <source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source> + <target>所有消息和文件均通过**端到端加密**发送;私信以量子安全方式发送。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone!</source> + <target>所有消息都将被删除 - 这无法被撤销!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> <target>所有聊天记录和消息将被删除——这一行为无法撤销!只有您的消息会被删除。</target> @@ -637,6 +783,22 @@ </trans-unit> <trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve"> <source>All new messages from %@ will be hidden!</source> + <target>来自 %@ 的所有新消息都将被隐藏!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All profiles" xml:space="preserve"> + <source>All profiles</source> + <target>所有配置文件</target> + <note>profile dropdown</note> + </trans-unit> + <trans-unit id="All reports will be archived for you." xml:space="preserve"> + <source>All reports will be archived for you.</source> + <target>将为你存档所有举报。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All servers" xml:space="preserve"> + <source>All servers</source> + <target>全部服务器</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All your contacts will remain connected." xml:space="preserve"> @@ -649,6 +811,11 @@ <target>您的所有联系人将保持连接。个人资料更新将发送给您的联系人。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target>你的所有联系人、对话和文件将被安全加密并分块上传到配置的 XFTP 中继。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow" xml:space="preserve"> <source>Allow</source> <target>允许</target> @@ -659,14 +826,24 @@ <target>仅当您的联系人允许时才允许呼叫。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve"> + <source>Allow calls?</source> + <target>允许通话?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> <target>仅当您的联系人允许时才允许限时消息。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <target>仅有您的联系人许可后才允许不可撤回消息移除。</target> + <trans-unit id="Allow downgrade" xml:space="preserve"> + <source>Allow downgrade</source> + <target>允许降级</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target>仅有您的联系人许可后才允许不可撤回消息移除</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> @@ -689,9 +866,24 @@ <target>允许发送限时消息。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow to irreversibly delete sent messages.</source> - <target>允许不可撤回地删除已发送消息。</target> + <trans-unit id="Allow sharing" xml:space="preserve"> + <source>Allow sharing</source> + <target>允许共享</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target>允许不可撤回地删除已发送消息</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to report messsages to moderators." xml:space="preserve"> + <source>Allow to report messsages to moderators.</source> + <target>允许向 moderators 举报消息。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve"> + <source>Allow to send SimpleX links.</source> + <target>允许发送 SimpleX 链接。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send files and media." xml:space="preserve"> @@ -724,9 +916,9 @@ <target>允许您的联系人给您打电话。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> - <source>Allow your contacts to irreversibly delete sent messages.</source> - <target>允许您的联系人不可撤回地删除已发送消息。</target> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target>允许您的联系人不可撤回地删除已发送消息</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> @@ -746,10 +938,17 @@ </trans-unit> <trans-unit id="Already connecting!" xml:space="preserve"> <source>Already connecting!</source> + <target>已经在连接了!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Already joining the group!" xml:space="preserve"> <source>Already joining the group!</source> + <target>已经加入了该群组!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Always use private routing." xml:space="preserve"> + <source>Always use private routing.</source> + <target>始终使用私有路由。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> @@ -762,21 +961,41 @@ <target>已创建一个包含所提供名字的空白聊天资料,应用程序照常打开。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Another reason" xml:space="preserve"> + <source>Another reason</source> + <target>另一个理由</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> <target>接听来电</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Anybody can host servers." xml:space="preserve"> + <source>Anybody can host servers.</source> + <target>任何人都可以托管服务器。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> <target>应用程序构建:%@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App data migration" xml:space="preserve"> + <source>App data migration</source> + <target>应用数据迁移</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> <source>App encrypts new local files (except videos).</source> <target>应用程序为新的本地文件(视频除外)加密。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App group:" xml:space="preserve"> + <source>App group:</source> + <target>应用组:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>应用程序图标</target> @@ -792,6 +1011,11 @@ <target>应用程序密码被替换为自毁密码。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App session" xml:space="preserve"> + <source>App session</source> + <target>应用会话</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App version" xml:space="preserve"> <source>App version</source> <target>应用程序版本</target> @@ -807,6 +1031,66 @@ <target>外观</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Apply" xml:space="preserve"> + <source>Apply</source> + <target>应用</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve"> + <source>Apply to</source> + <target>应用于</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive" xml:space="preserve"> + <source>Archive</source> + <target>存档</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive %lld reports?" xml:space="preserve"> + <source>Archive %lld reports?</source> + <target>存档 %lld 个举报?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive all reports?" xml:space="preserve"> + <source>Archive all reports?</source> + <target>存档所有举报?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve"> + <source>Archive and upload</source> + <target>存档和上传</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive contacts to chat later." xml:space="preserve"> + <source>Archive contacts to chat later.</source> + <target>存档联系人以便稍后聊天.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report" xml:space="preserve"> + <source>Archive report</source> + <target>存档举报</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive report?" xml:space="preserve"> + <source>Archive report?</source> + <target>存档举报?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archive reports" xml:space="preserve"> + <source>Archive reports</source> + <target>存档举报</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Archived contacts" xml:space="preserve"> + <source>Archived contacts</source> + <target>已存档的联系人</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve"> + <source>Archiving database</source> + <target>正在存档数据库</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Attach" xml:space="preserve"> <source>Attach</source> <target>附件</target> @@ -867,13 +1151,24 @@ <target>自动接受图片</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Auto-accept settings" xml:space="preserve"> + <source>Auto-accept settings</source> + <target>自动接受设置</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Back" xml:space="preserve"> <source>Back</source> <target>返回</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Background" xml:space="preserve"> + <source>Background</source> + <target>背景</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Bad desktop address" xml:space="preserve"> <source>Bad desktop address</source> + <target>糟糕的桌面地址</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Bad message ID" xml:space="preserve"> @@ -886,8 +1181,24 @@ <target>错误消息散列</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better calls" xml:space="preserve"> + <source>Better calls</source> + <target>更佳的通话</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Better groups" xml:space="preserve"> <source>Better groups</source> + <target>更佳的群组</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better groups performance" xml:space="preserve"> + <source>Better groups performance</source> + <target>更好的群性能</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better message dates." xml:space="preserve"> + <source>Better message dates.</source> + <target>更好的消息日期。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Better messages" xml:space="preserve"> @@ -895,20 +1206,79 @@ <target>更好的消息</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Better networking" xml:space="preserve"> + <source>Better networking</source> + <target>更好的网络</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better notifications" xml:space="preserve"> + <source>Better notifications</source> + <target>更佳的通知</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better privacy and security" xml:space="preserve"> + <source>Better privacy and security</source> + <target>更好的隐私和安全</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better security ✅" xml:space="preserve"> + <source>Better security ✅</source> + <target>更佳的安全性✅</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better user experience" xml:space="preserve"> + <source>Better user experience</source> + <target>更佳的使用体验</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Black" xml:space="preserve"> + <source>Black</source> + <target>黑色</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Block" xml:space="preserve"> <source>Block</source> + <target>封禁</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block for all" xml:space="preserve"> + <source>Block for all</source> + <target>为所有人封禁</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Block group members" xml:space="preserve"> <source>Block group members</source> + <target>屏蔽群组成员</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Block member" xml:space="preserve"> <source>Block member</source> + <target>封禁成员</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Block member for all?" xml:space="preserve"> + <source>Block member for all?</source> + <target>为所有其他成员封禁该成员?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Block member?" xml:space="preserve"> <source>Block member?</source> + <target>封禁成员吗?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve"> + <source>Blocked by admin</source> + <target>由管理员封禁</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur for better privacy." xml:space="preserve"> + <source>Blur for better privacy.</source> + <target>模糊处理,提高私密性.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Blur media" xml:space="preserve"> + <source>Blur media</source> + <target>模糊媒体</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> @@ -916,9 +1286,9 @@ <target>您和您的联系人都可以添加消息回应。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> - <source>Both you and your contact can irreversibly delete sent messages.</source> - <target>您和您的联系人都可以不可逆转地删除已发送的消息。</target> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target>您和您的联系人都可以不可逆转地删除已发送的消息</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> @@ -941,11 +1311,35 @@ <target>保加利亚语、芬兰语、泰语和乌克兰语——感谢用户和[Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Business address" xml:space="preserve"> + <source>Business address</source> + <target>企业地址</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Business chats" xml:space="preserve"> + <source>Business chats</source> + <target>企业聊天</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Businesses" xml:space="preserve"> + <source>Businesses</source> + <target>企业</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>通过聊天资料(默认)或者[通过连接](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam." xml:space="preserve"> + <source>By using SimpleX Chat you agree to: +- send only legal content in public groups. +- respect other users – no spam.</source> + <target>使用 SimpleX Chat 代表您同意: +- 在公开群中只发送合法内容 +- 尊重其他用户 – 没有垃圾信息。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> <source>Call already ended!</source> <target>通话已结束!</target> @@ -956,9 +1350,24 @@ <target>通话</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve"> - <source>Can't delete user profile!</source> - <target>无法删除用户个人资料!</target> + <trans-unit id="Calls prohibited!" xml:space="preserve"> + <source>Calls prohibited!</source> + <target>禁止来电!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve"> + <source>Camera not available</source> + <target>相机不可用</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call contact" xml:space="preserve"> + <source>Can't call contact</source> + <target>无法呼叫联系人</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve"> + <source>Can't call member</source> + <target>无法呼叫成员</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contact!" xml:space="preserve"> @@ -971,9 +1380,20 @@ <target>无法邀请联系人!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve"> + <source>Can't message member</source> + <target>无法向成员发送消息</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> <target>取消</target> + <note>alert action +alert button</note> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve"> + <source>Cancel migration</source> + <target>取消迁移</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> @@ -981,9 +1401,24 @@ <target>无法访问钥匙串以保存数据库密码</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>无法转发消息</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> <target>无法接收文件</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve"> + <source>Capacity exceeded - recipient did not receive previously sent messages.</source> + <target>超出容量-收件人未收到以前发送的邮件。</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve"> + <source>Cellular</source> + <target>移动网络</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change" xml:space="preserve"> @@ -991,6 +1426,16 @@ <target>更改</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change automatic message deletion?" xml:space="preserve"> + <source>Change automatic message deletion?</source> + <target>更改消息自动删除设置?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Change chat profiles" xml:space="preserve"> + <source>Change chat profiles</source> + <target>更改聊天资料</target> + <note>authentication reason</note> + </trans-unit> <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> <target>更改数据库密码?</target> @@ -1035,11 +1480,26 @@ <source>Change self-destruct passcode</source> <target>更改自毁密码</target> <note>authentication reason - set passcode view</note> +set passcode view</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve"> - <source>Chat archive</source> - <target>聊天档案</target> + <trans-unit id="Chat" xml:space="preserve"> + <source>Chat</source> + <target>聊天</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists" xml:space="preserve"> + <source>Chat already exists</source> + <target>聊天已存在</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat already exists!" xml:space="preserve"> + <source>Chat already exists!</source> + <target>聊天已存在!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat colors" xml:space="preserve"> + <source>Chat colors</source> + <target>聊天颜色</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat console" xml:space="preserve"> @@ -1057,6 +1517,11 @@ <target>聊天数据库已删除</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve"> + <source>Chat database exported</source> + <target>导出的聊天数据库</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> <target>聊天数据库已导入</target> @@ -1072,26 +1537,81 @@ <target>聊天已停止</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve"> + <source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source> + <target>聊天已停止。如果你已经在另一台设备商使用过此数据库,你应该在启动聊天前将数据库传输回来。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat list" xml:space="preserve"> + <source>Chat list</source> + <target>聊天列表</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat migrated!" xml:space="preserve"> + <source>Chat migrated!</source> + <target>已迁移聊天!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> <target>聊天偏好设置</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chat preferences were changed." xml:space="preserve"> + <source>Chat preferences were changed.</source> + <target>聊天偏好设置已修改。</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Chat profile" xml:space="preserve"> + <source>Chat profile</source> + <target>用户资料</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat theme" xml:space="preserve"> + <source>Chat theme</source> + <target>聊天主题</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for all members - this cannot be undone!</source> + <target>将为所有成员删除聊天 - 此操作无法撤销!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Chat will be deleted for you - this cannot be undone!</source> + <target>将为你删除聊天 - 此操作无法撤销!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> <target>聊天</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Check messages every 20 min." xml:space="preserve"> + <source>Check messages every 20 min.</source> + <target>每 20 分钟检查消息。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check messages when allowed." xml:space="preserve"> + <source>Check messages when allowed.</source> + <target>在被允许时检查消息。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> <target>检查服务器地址并再试一次。</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> <target>中文和西班牙文界面</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve"> + <source>Choose _Migrate from another device_ on the new device and scan QR code.</source> + <target>在新设备上选择“从另一个设备迁移”并扫描二维码。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> <target>选择文件</target> @@ -1102,10 +1622,25 @@ <target>从库中选择</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Chunks deleted" xml:space="preserve"> + <source>Chunks deleted</source> + <target>已删除的块</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks downloaded" xml:space="preserve"> + <source>Chunks downloaded</source> + <target>下载的块</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chunks uploaded" xml:space="preserve"> + <source>Chunks uploaded</source> + <target>已下载的区块</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> <target>清除</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> @@ -1117,16 +1652,41 @@ <target>清除对话吗?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Clear group?" xml:space="preserve"> + <source>Clear group?</source> + <target>清除群?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear or delete group?" xml:space="preserve"> + <source>Clear or delete group?</source> + <target>清除还是删除群?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear private notes?" xml:space="preserve"> + <source>Clear private notes?</source> + <target>清除私密笔记?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Clear verification" xml:space="preserve"> <source>Clear verification</source> <target>清除验证</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Colors" xml:space="preserve"> - <source>Colors</source> - <target>颜色</target> + <trans-unit id="Color chats with the new themes." xml:space="preserve"> + <source>Color chats with the new themes.</source> + <target>使用新主题为聊天着色。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Color mode" xml:space="preserve"> + <source>Color mode</source> + <target>颜色模式</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Community guidelines violation" xml:space="preserve"> + <source>Community guidelines violation</source> + <target>违反社区指导方针</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Compare file" xml:space="preserve"> <source>Compare file</source> <target>对比文件</target> @@ -1137,11 +1697,56 @@ <target>与您的联系人比较安全码。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Completed" xml:space="preserve"> + <source>Completed</source> + <target>已完成</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions accepted on: %@." xml:space="preserve"> + <source>Conditions accepted on: %@.</source> + <target>已于 %@ 接受条款。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions are accepted for the operator(s): **%@**.</source> + <target>已接受运营方 **%@** 的条款。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions are already accepted for these operator(s): **%@**." xml:space="preserve"> + <source>Conditions are already accepted for these operator(s): **%@**.</source> + <target>已经接受下列运营方的条款:**%@**。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions of use" xml:space="preserve"> + <source>Conditions of use</source> + <target>使用条款</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted for the operator(s): **%@**." xml:space="preserve"> + <source>Conditions will be accepted for the operator(s): **%@**.</source> + <target>将接受下列运营方的条款:**%@**。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be accepted on: %@." xml:space="preserve"> + <source>Conditions will be accepted on: %@.</source> + <target>将于 %@ 接受条款。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Conditions will be automatically accepted for enabled operators on: %@." xml:space="preserve"> + <source>Conditions will be automatically accepted for enabled operators on: %@.</source> + <target>将在 %@ 自动接受启用的运营方的条款。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> <target>配置 ICE 服务器</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Configure server operators" xml:space="preserve"> + <source>Configure server operators</source> + <target>配置服务器运营方</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> <target>确认</target> @@ -1152,11 +1757,26 @@ <target>确认密码</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm contact deletion?" xml:space="preserve"> + <source>Confirm contact deletion?</source> + <target>确认删除联系人?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm database upgrades" xml:space="preserve"> <source>Confirm database upgrades</source> <target>确认数据库升级</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm files from unknown servers." xml:space="preserve"> + <source>Confirm files from unknown servers.</source> + <target>确认来自未知服务器的文件。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm network settings" xml:space="preserve"> + <source>Confirm network settings</source> + <target>确认网络设置</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> <target>确认新密码……</target> @@ -1167,6 +1787,21 @@ <target>确认密码</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve"> + <source>Confirm that you remember database passphrase to migrate it.</source> + <target>请在迁移前确认你记得数据库的密码短语。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm upload" xml:space="preserve"> + <source>Confirm upload</source> + <target>确认上传</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirmed" xml:space="preserve"> + <source>Confirmed</source> + <target>已确定</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> <target>连接</target> @@ -1174,6 +1809,7 @@ </trans-unit> <trans-unit id="Connect automatically" xml:space="preserve"> <source>Connect automatically</source> + <target>自动连接</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect incognito" xml:space="preserve"> @@ -1183,24 +1819,36 @@ </trans-unit> <trans-unit id="Connect to desktop" xml:space="preserve"> <source>Connect to desktop</source> + <target>连接到桌面</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect to your friends faster." xml:space="preserve"> + <source>Connect to your friends faster.</source> + <target>更快地与您的朋友联系。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect to yourself?" xml:space="preserve"> <source>Connect to yourself?</source> + <target>连接到你自己?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect to yourself? This is your own SimpleX address!" xml:space="preserve"> <source>Connect to yourself? This is your own SimpleX address!</source> + <target>与自己建立联系? +这是您自己的 SimpleX 地址!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect to yourself? This is your own one-time link!" xml:space="preserve"> <source>Connect to yourself? This is your own one-time link!</source> + <target>与自己建立联系? +这是您自己的一次性链接!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via contact address" xml:space="preserve"> <source>Connect via contact address</source> + <target>通过联系地址连接</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via link" xml:space="preserve"> @@ -1208,11 +1856,6 @@ This is your own one-time link!</source> <target>通过链接连接</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve"> - <source>Connect via link / QR code</source> - <target>通过群组链接/二维码连接</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connect via one-time link" xml:space="preserve"> <source>Connect via one-time link</source> <target>通过一次性链接连接</target> @@ -1220,14 +1863,32 @@ This is your own one-time link!</source> </trans-unit> <trans-unit id="Connect with %@" xml:space="preserve"> <source>Connect with %@</source> + <target>与 %@连接</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connected" xml:space="preserve"> + <source>Connected</source> + <target>已连接</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connected desktop" xml:space="preserve"> <source>Connected desktop</source> + <target>已连接的桌面</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connected servers" xml:space="preserve"> + <source>Connected servers</source> + <target>已连接的服务器</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connected to desktop" xml:space="preserve"> <source>Connected to desktop</source> + <target>已连接到桌面</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connecting" xml:space="preserve"> + <source>Connecting</source> + <target>正在连接</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> @@ -1240,8 +1901,14 @@ This is your own one-time link!</source> <target>连接服务器中……(错误:%@)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connecting to contact, please wait or check later!" xml:space="preserve"> + <source>Connecting to contact, please wait or check later!</source> + <target>正在连接到联系人,请稍候或稍后检查!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connecting to desktop" xml:space="preserve"> <source>Connecting to desktop</source> + <target>正连接到桌面</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection" xml:space="preserve"> @@ -1249,6 +1916,16 @@ This is your own one-time link!</source> <target>连接</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection and servers status." xml:space="preserve"> + <source>Connection and servers status.</source> + <target>连接和服务器状态。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection blocked" xml:space="preserve"> + <source>Connection blocked</source> + <target>连接被阻止</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> <target>连接错误</target> @@ -1259,13 +1936,40 @@ This is your own one-time link!</source> <target>连接错误(AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection is blocked by server operator: %@" xml:space="preserve"> + <source>Connection is blocked by server operator: +%@</source> + <target>连接被运营方 %@ 阻止</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection not ready." xml:space="preserve"> + <source>Connection not ready.</source> + <target>连接未就绪。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection notifications" xml:space="preserve"> + <source>Connection notifications</source> + <target>连接通知</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>已发送连接请求!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection requires encryption renegotiation." xml:space="preserve"> + <source>Connection requires encryption renegotiation.</source> + <target>连接需要加密重协商。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection security" xml:space="preserve"> + <source>Connection security</source> + <target>连接安全性</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Connection terminated" xml:space="preserve"> <source>Connection terminated</source> + <target>连接被终止</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection timeout" xml:space="preserve"> @@ -1273,6 +1977,16 @@ This is your own one-time link!</source> <target>连接超时</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Connection with desktop stopped" xml:space="preserve"> + <source>Connection with desktop stopped</source> + <target>与桌面的连接已停止</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connections" xml:space="preserve"> + <source>Connections</source> + <target>连接</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact allows" xml:space="preserve"> <source>Contact allows</source> <target>联系人允许</target> @@ -1283,6 +1997,11 @@ This is your own one-time link!</source> <target>联系人已存在</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact deleted!" xml:space="preserve"> + <source>Contact deleted!</source> + <target>联系人已删除!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> <target>联系人已隐藏:</target> @@ -1293,9 +2012,9 @@ This is your own one-time link!</source> <target>联系已连接</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve"> - <source>Contact is not connected yet!</source> - <target>联系人尚未连接!</target> + <trans-unit id="Contact is deleted." xml:space="preserve"> + <source>Contact is deleted.</source> + <target>联系人被删除。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact name" xml:space="preserve"> @@ -1308,6 +2027,11 @@ This is your own one-time link!</source> <target>联系人偏好设置</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Contact will be deleted - this cannot be undone!" xml:space="preserve"> + <source>Contact will be deleted - this cannot be undone!</source> + <target>联系人将被删除-这是无法撤消的!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> <target>联系人</target> @@ -1318,23 +2042,44 @@ This is your own one-time link!</source> <target>联系人可以将信息标记为删除;您将可以查看这些信息。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Content violates conditions of use" xml:space="preserve"> + <source>Content violates conditions of use</source> + <target>内容违反使用条款</target> + <note>blocking reason</note> + </trans-unit> <trans-unit id="Continue" xml:space="preserve"> <source>Continue</source> <target>继续</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Conversation deleted!" xml:space="preserve"> + <source>Conversation deleted!</source> + <target>对话已删除!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> <target>复制</target> - <note>chat item action</note> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy error" xml:space="preserve"> + <source>Copy error</source> + <target>复制错误</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> <target>核心版本: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Corner" xml:space="preserve"> + <source>Corner</source> + <target>拐角</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Correct name to %@?" xml:space="preserve"> <source>Correct name to %@?</source> + <target>将名称更正为 %@?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create" xml:space="preserve"> @@ -1342,6 +2087,11 @@ This is your own one-time link!</source> <target>创建</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create 1-time link" xml:space="preserve"> + <source>Create 1-time link</source> + <target>创建一次性链接</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> <target>创建 SimpleX 地址</target> @@ -1349,11 +2099,7 @@ This is your own one-time link!</source> </trans-unit> <trans-unit id="Create a group using a random profile." xml:space="preserve"> <source>Create a group using a random profile.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> - <source>Create an address to let people connect with you.</source> - <target>创建一个地址,让人们与您联系。</target> + <target>使用随机身份创建群组.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create file" xml:space="preserve"> @@ -1363,6 +2109,7 @@ This is your own one-time link!</source> </trans-unit> <trans-unit id="Create group" xml:space="preserve"> <source>Create group</source> + <target>建群</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create group link" xml:space="preserve"> @@ -1375,18 +2122,19 @@ This is your own one-time link!</source> <target>创建链接</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create list" xml:space="preserve"> + <source>Create list</source> + <target>创建列表</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> <target>在[桌面应用程序](https://simplex.chat/downloads/)中创建新的个人资料。 💻</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve"> - <source>Create one-time invitation link</source> - <target>创建一次性邀请链接</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Create profile" xml:space="preserve"> <source>Create profile</source> + <target>创建个人资料</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create queue" xml:space="preserve"> @@ -1404,9 +2152,29 @@ This is your own one-time link!</source> <target>创建您的资料</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve"> - <source>Created on %@</source> - <target>创建于 %@</target> + <trans-unit id="Created" xml:space="preserve"> + <source>Created</source> + <target>已创建</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at" xml:space="preserve"> + <source>Created at</source> + <target>创建于</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created at: %@" xml:space="preserve"> + <source>Created at: %@</source> + <target>创建于:%@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Creating archive link" xml:space="preserve"> + <source>Creating archive link</source> + <target>正在创建存档链接</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Creating link…" xml:space="preserve"> + <source>Creating link…</source> + <target>创建链接中…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current Passcode" xml:space="preserve"> @@ -1414,11 +2182,21 @@ This is your own one-time link!</source> <target>当前密码</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current conditions text couldn't be loaded, you can review conditions via this link:" xml:space="preserve"> + <source>Current conditions text couldn't be loaded, you can review conditions via this link:</source> + <target>无法加载当前条款文本,你可以通过此链接审阅条款:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> <target>现有密码……</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Current profile" xml:space="preserve"> + <source>Current profile</source> + <target>当前配置文件</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> <target>目前支持的最大文件大小为 %@。</target> @@ -1429,11 +2207,26 @@ This is your own one-time link!</source> <target>自定义时间</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Customizable message shape." xml:space="preserve"> + <source>Customizable message shape.</source> + <target>可自定义消息形状。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Customize theme" xml:space="preserve"> + <source>Customize theme</source> + <target>自定义主题</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> <target>深色</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Dark mode colors" xml:space="preserve"> + <source>Dark mode colors</source> + <target>深色模式颜色</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> <target>数据库 ID</target> @@ -1532,6 +2325,11 @@ This is your own one-time link!</source> <target>应用程序重新启动时将迁移数据库</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Debug delivery" xml:space="preserve"> + <source>Debug delivery</source> + <target>调试交付</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> <target>分散式</target> @@ -1545,15 +2343,17 @@ This is your own one-time link!</source> <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> <target>删除</target> - <note>chat item action</note> + <note>alert action +swipe action</note> + </trans-unit> + <trans-unit id="Delete %lld messages of members?" xml:space="preserve"> + <source>Delete %lld messages of members?</source> + <target>删除成员的 %lld 消息?</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete %lld messages?" xml:space="preserve"> <source>Delete %lld messages?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve"> - <source>Delete Contact</source> - <target>删除联系人</target> + <target>删除 %lld 消息?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete address" xml:space="preserve"> @@ -1578,16 +2378,17 @@ This is your own one-time link!</source> </trans-unit> <trans-unit id="Delete and notify contact" xml:space="preserve"> <source>Delete and notify contact</source> + <target>删除并通知联系人</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve"> - <source>Delete archive</source> - <target>删除档案</target> + <trans-unit id="Delete chat" xml:space="preserve"> + <source>Delete chat</source> + <target>删除聊天</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve"> - <source>Delete chat archive?</source> - <target>删除聊天档案?</target> + <trans-unit id="Delete chat messages from your device." xml:space="preserve"> + <source>Delete chat messages from your device.</source> + <target>从你的设备删除聊天消息。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile" xml:space="preserve"> @@ -1600,6 +2401,11 @@ This is your own one-time link!</source> <target>删除聊天资料?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete chat?" xml:space="preserve"> + <source>Delete chat?</source> + <target>删除聊天?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> <source>Delete connection</source> <target>删除连接</target> @@ -1610,9 +2416,9 @@ This is your own one-time link!</source> <target>删除联系人</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact? This cannot be undone!" xml:space="preserve"> - <source>Delete contact? -This cannot be undone!</source> + <trans-unit id="Delete contact?" xml:space="preserve"> + <source>Delete contact?</source> + <target>删除联系人?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete database" xml:space="preserve"> @@ -1620,6 +2426,11 @@ This cannot be undone!</source> <target>删除数据库</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete database from this device" xml:space="preserve"> + <source>Delete database from this device</source> + <target>从这部设备上删除数据库</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete file" xml:space="preserve"> <source>Delete file</source> <target>删除文件</target> @@ -1670,6 +2481,11 @@ This cannot be undone!</source> <target>删除链接?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete list?" xml:space="preserve"> + <source>Delete list?</source> + <target>删除列表?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> <target>删除成员消息?</target> @@ -1683,7 +2499,7 @@ This cannot be undone!</source> <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> <target>删除消息</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> @@ -1700,9 +2516,9 @@ This cannot be undone!</source> <target>删除旧数据库吗?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve"> - <source>Delete pending connection</source> - <target>删除挂起连接</target> + <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> + <source>Delete or moderate up to 200 messages.</source> + <target>允许自行删除或管理员移除最多200条消息。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection?" xml:space="preserve"> @@ -1720,11 +2536,31 @@ This cannot be undone!</source> <target>删除队列</target> <note>server test step</note> </trans-unit> + <trans-unit id="Delete report" xml:space="preserve"> + <source>Delete report</source> + <target>删除举报</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete up to 20 messages at once." xml:space="preserve"> + <source>Delete up to 20 messages at once.</source> + <target>一次最多删除 20 条信息。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> <target>删除用户资料?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete without notification" xml:space="preserve"> + <source>Delete without notification</source> + <target>删除而不通知</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted" xml:space="preserve"> + <source>Deleted</source> + <target>已删除</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Deleted at" xml:space="preserve"> <source>Deleted at</source> <target>已删除于</target> @@ -1735,6 +2571,16 @@ This cannot be undone!</source> <target>已删除于:%@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Deletion errors" xml:space="preserve"> + <source>Deletion errors</source> + <target>删除错误</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivered even when Apple drops them." xml:space="preserve"> + <source>Delivered even when Apple drops them.</source> + <target>已送达,即使苹果已将其删除。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery" xml:space="preserve"> <source>Delivery</source> <target>传送</target> @@ -1757,14 +2603,42 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Desktop address" xml:space="preserve"> <source>Desktop address</source> + <target>桌面地址</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve"> <source>Desktop app version %@ is not compatible with this app.</source> + <target>桌面应用程序版本 %@ 与此应用程序不兼容。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Desktop devices" xml:space="preserve"> <source>Desktop devices</source> + <target>桌面设备</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server address of %@ is incompatible with forwarding server %@ settings." xml:space="preserve"> + <source>Destination server address of %@ is incompatible with forwarding server %@ settings.</source> + <target>目标服务器地址 %@ 与转发服务器 %@ 设置不兼容。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Destination server error: %@" xml:space="preserve"> + <source>Destination server error: %@</source> + <target>目标服务器错误:%@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Destination server version of %@ is incompatible with forwarding server %@." xml:space="preserve"> + <source>Destination server version of %@ is incompatible with forwarding server %@.</source> + <target>目标服务器版本 %@ 与转发服务器 %@ 不兼容。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Detailed statistics" xml:space="preserve"> + <source>Detailed statistics</source> + <target>详细的统计数据</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Details" xml:space="preserve"> + <source>Details</source> + <target>详细信息</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Develop" xml:space="preserve"> @@ -1772,6 +2646,11 @@ This cannot be undone!</source> <target>开发</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Developer options" xml:space="preserve"> + <source>Developer options</source> + <target>开发者选项</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> <target>开发者工具</target> @@ -1784,7 +2663,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Device authentication is disabled. Turning off SimpleX Lock." xml:space="preserve"> <source>Device authentication is disabled. Turning off SimpleX Lock.</source> - <target>设备验证被禁用。关闭 SimpleX 锁定。</target> + <target>设备验证已禁用。 SimpleX 已解锁。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." xml:space="preserve"> @@ -1802,9 +2681,14 @@ This cannot be undone!</source> <target>私信</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> - <source>Direct messages between members are prohibited in this group.</source> - <target>此群中禁止成员之间私信。</target> + <trans-unit id="Direct messages between members are prohibited in this chat." xml:space="preserve"> + <source>Direct messages between members are prohibited in this chat.</source> + <target>此群禁止成员间私信。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve"> + <source>Direct messages between members are prohibited.</source> + <target>此群禁止成员间私信。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disable (keep overrides)" xml:space="preserve"> @@ -1817,11 +2701,26 @@ This cannot be undone!</source> <target>禁用 SimpleX 锁定</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Disable automatic message deletion?" xml:space="preserve"> + <source>Disable automatic message deletion?</source> + <target>禁用消息自动销毁?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Disable delete messages" xml:space="preserve"> + <source>Disable delete messages</source> + <target>停用消息删除</target> + <note>alert button</note> + </trans-unit> <trans-unit id="Disable for all" xml:space="preserve"> <source>Disable for all</source> <target>全部禁用</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Disabled" xml:space="preserve"> + <source>Disabled</source> + <target>禁用</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> <source>Disappearing message</source> <target>限时消息</target> @@ -1837,8 +2736,8 @@ This cannot be undone!</source> <target>此聊天中禁止显示限时消息。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve"> + <source>Disappearing messages are prohibited.</source> <target>该组禁止限时消息。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1859,6 +2758,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Disconnect desktop?" xml:space="preserve"> <source>Disconnect desktop?</source> + <target>断开桌面连接?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Discover and join groups" xml:space="preserve"> @@ -1868,6 +2768,12 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Discover via local network" xml:space="preserve"> <source>Discover via local network</source> + <target>通过本地网络发现</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do NOT send messages directly, even if your or destination server does not support private routing." xml:space="preserve"> + <source>Do NOT send messages directly, even if your or destination server does not support private routing.</source> + <target>请勿直接发送消息,即使您的服务器或目标服务器不支持私有路由。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> @@ -1875,11 +2781,31 @@ This cannot be undone!</source> <target>请勿使用 SimpleX 进行紧急通话。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do NOT use private routing." xml:space="preserve"> + <source>Do NOT use private routing.</source> + <target>不要使用私有路由。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Do it later" xml:space="preserve"> <source>Do it later</source> <target>稍后再做</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Do not send history to new members." xml:space="preserve"> + <source>Do not send history to new members.</source> + <target>不给新成员发送历史消息。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do not use credentials with proxy." xml:space="preserve"> + <source>Do not use credentials with proxy.</source> + <target>代理不使用身份验证凭据。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Documents:" xml:space="preserve"> + <source>Documents:</source> + <target>文档:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> <target>不创建地址</target> @@ -1890,21 +2816,72 @@ This cannot be undone!</source> <target>不要启用</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Don't miss important messages." xml:space="preserve"> + <source>Don't miss important messages.</source> + <target>不错过重要消息。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> <target>不再显示</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Done" xml:space="preserve"> + <source>Done</source> + <target>完成</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Downgrade and open chat" xml:space="preserve"> <source>Downgrade and open chat</source> <target>降级并打开聊天</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Download" xml:space="preserve"> + <source>Download</source> + <target>下载</target> + <note>alert button +chat item action</note> + </trans-unit> + <trans-unit id="Download errors" xml:space="preserve"> + <source>Download errors</source> + <target>下载错误</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download failed" xml:space="preserve"> + <source>Download failed</source> + <target>下载失败了</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Download file" xml:space="preserve"> <source>Download file</source> <target>下载文件</target> <note>server test step</note> </trans-unit> + <trans-unit id="Download files" xml:space="preserve"> + <source>Download files</source> + <target>下载文件</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Downloaded" xml:space="preserve"> + <source>Downloaded</source> + <target>已下载</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloaded files" xml:space="preserve"> + <source>Downloaded files</source> + <target>下载的文件</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading archive" xml:space="preserve"> + <source>Downloading archive</source> + <target>正在下载存档</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downloading link details" xml:space="preserve"> + <source>Downloading link details</source> + <target>正在下载链接详情</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> <target>重复的显示名!</target> @@ -1915,6 +2892,11 @@ This cannot be undone!</source> <target>时长</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="E2E encrypted notifications." xml:space="preserve"> + <source>E2E encrypted notifications.</source> + <target>端到端加密的通知。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> <target>编辑</target> @@ -1935,6 +2917,11 @@ This cannot be undone!</source> <target>启用(保持覆盖)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable Flux in Network & servers settings for better metadata privacy." xml:space="preserve"> + <source>Enable Flux in Network & servers settings for better metadata privacy.</source> + <target>在“网络&服务器”设置中启用 Flux,更好地保护元数据隐私。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> <target>启用 SimpleX 锁定</target> @@ -1948,6 +2935,11 @@ This cannot be undone!</source> <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> <target>启用自动删除消息?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Enable camera access" xml:space="preserve"> + <source>Enable camera access</source> + <target>启用相机访问</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable for all" xml:space="preserve"> @@ -1955,6 +2947,11 @@ This cannot be undone!</source> <target>全部启用</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve"> + <source>Enable in direct chats (BETA)!</source> + <target>在私聊中开启(公测)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> <target>启用即时通知?</target> @@ -1985,6 +2982,16 @@ This cannot be undone!</source> <target>启用自毁密码</target> <note>set passcode view</note> </trans-unit> + <trans-unit id="Enabled" xml:space="preserve"> + <source>Enabled</source> + <target>已启用</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enabled for" xml:space="preserve"> + <source>Enabled for</source> + <target>启用对象</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> <target>加密</target> @@ -2015,6 +3022,11 @@ This cannot be undone!</source> <target>加密消息或其他事件</target> <note>notification</note> </trans-unit> + <trans-unit id="Encrypted message: app is stopped" xml:space="preserve"> + <source>Encrypted message: app is stopped</source> + <target>加密消息:应用程序已停止</target> + <note>notification</note> + </trans-unit> <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> <target>加密消息:数据库错误</target> @@ -2042,10 +3054,17 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Encryption re-negotiation error" xml:space="preserve"> <source>Encryption re-negotiation error</source> + <target>加密重协商错误</target> <note>message decrypt error item</note> </trans-unit> <trans-unit id="Encryption re-negotiation failed." xml:space="preserve"> <source>Encryption re-negotiation failed.</source> + <target>加密重协商失败了。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encryption renegotiation in progress." xml:space="preserve"> + <source>Encryption renegotiation in progress.</source> + <target>正进行加密重协商。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter Passcode" xml:space="preserve"> @@ -2060,6 +3079,12 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Enter group name…" xml:space="preserve"> <source>Enter group name…</source> + <target>输入组名称…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter passphrase" xml:space="preserve"> + <source>Enter passphrase</source> + <target>输入密码短语</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter passphrase…" xml:space="preserve"> @@ -2079,6 +3104,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Enter this device name…" xml:space="preserve"> <source>Enter this device name…</source> + <target>输入此设备名…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter welcome message…" xml:space="preserve"> @@ -2093,6 +3119,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Enter your name…" xml:space="preserve"> <source>Enter your name…</source> + <target>请输入您的姓名…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error" xml:space="preserve"> @@ -2105,26 +3132,36 @@ This cannot be undone!</source> <target>中止地址更改错误</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error accepting conditions" xml:space="preserve"> + <source>Error accepting conditions</source> + <target>接受条款出错</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> <target>接受联系人请求错误</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve"> - <source>Error accessing database file</source> - <target>访问数据库文件错误</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> <target>添加成员错误</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error adding server" xml:space="preserve"> + <source>Error adding server</source> + <target>添加服务器出错</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> <target>更改地址错误</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing connection profile" xml:space="preserve"> + <source>Error changing connection profile</source> + <target>更改连接资料出错</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error changing role" xml:space="preserve"> <source>Error changing role</source> <target>更改角色错误</target> @@ -2135,6 +3172,20 @@ This cannot be undone!</source> <target>更改设置错误</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error changing to incognito!" xml:space="preserve"> + <source>Error changing to incognito!</source> + <target>切换至隐身聊天出错!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error checking token status" xml:space="preserve"> + <source>Error checking token status</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve"> + <source>Error connecting to forwarding server %@. Please try later.</source> + <target>连接到转发服务器 %@ 时出错。请稍后尝试。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> <target>创建地址错误</target> @@ -2150,16 +3201,31 @@ This cannot be undone!</source> <target>创建群组链接错误</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating list" xml:space="preserve"> + <source>Error creating list</source> + <target>创建列表出错</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error creating member contact" xml:space="preserve"> <source>Error creating member contact</source> <target>创建成员联系人时出错</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating message" xml:space="preserve"> + <source>Error creating message</source> + <target>创建消息出错</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>创建资料错误!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating report" xml:space="preserve"> + <source>Error creating report</source> + <target>创建举报出错</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error decrypting file" xml:space="preserve"> <source>Error decrypting file</source> <target>解密文件时出错</target> @@ -2180,11 +3246,6 @@ This cannot be undone!</source> <target>删除连接错误</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve"> - <source>Error deleting contact</source> - <target>删除联系人错误</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> <target>删除数据库错误</target> @@ -2205,6 +3266,11 @@ This cannot be undone!</source> <target>删除用户资料错误</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error downloading the archive" xml:space="preserve"> + <source>Error downloading the archive</source> + <target>下载存档出错</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> <source>Error enabling delivery receipts!</source> <target>启用送达回执出错!</target> @@ -2225,6 +3291,11 @@ This cannot be undone!</source> <target>导出聊天数据库错误</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error exporting theme: %@" xml:space="preserve"> + <source>Error exporting theme: %@</source> + <target>导出主题时出错: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> <target>导入聊天数据库错误</target> @@ -2235,24 +3306,54 @@ This cannot be undone!</source> <target>加入群组错误</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve"> - <source>Error loading %@ servers</source> - <target>加载 %@ 服务器错误</target> + <trans-unit id="Error loading servers" xml:space="preserve"> + <source>Error loading servers</source> + <target>加载服务器出错</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error migrating settings" xml:space="preserve"> + <source>Error migrating settings</source> + <target>迁移设置出错</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error opening chat" xml:space="preserve"> + <source>Error opening chat</source> + <target>打开聊天时出错</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> <target>接收文件错误</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error reconnecting server" xml:space="preserve"> + <source>Error reconnecting server</source> + <target>重新连接服务器时出错</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error reconnecting servers" xml:space="preserve"> + <source>Error reconnecting servers</source> + <target>重新连接服务器时出错</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error registering for notifications" xml:space="preserve"> + <source>Error registering for notifications</source> + <target>注册消息推送出错</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> <target>删除成员错误</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve"> - <source>Error saving %@ servers</source> - <target>保存 %@ 服务器错误</target> + <trans-unit id="Error reordering lists" xml:space="preserve"> + <source>Error reordering lists</source> + <target>重排列表出错</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error resetting statistics" xml:space="preserve"> + <source>Error resetting statistics</source> + <target>重置统计信息时出错</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving ICE servers" xml:space="preserve"> @@ -2260,6 +3361,11 @@ This cannot be undone!</source> <target>保存 ICE 服务器错误</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving chat list" xml:space="preserve"> + <source>Error saving chat list</source> + <target>保存聊天列表出错</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> <target>保存群组资料错误</target> @@ -2275,11 +3381,26 @@ This cannot be undone!</source> <target>保存密码到钥匙串错误</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error saving servers" xml:space="preserve"> + <source>Error saving servers</source> + <target>保存服务器出错</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Error saving settings" xml:space="preserve"> + <source>Error saving settings</source> + <target>保存设置出错</target> + <note>when migrating</note> + </trans-unit> <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> <target>保存用户密码时出错</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error scanning code: %@" xml:space="preserve"> + <source>Error scanning code: %@</source> + <target>扫描代码时出错:%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending email" xml:space="preserve"> <source>Error sending email</source> <target>发送电邮错误</target> @@ -2310,16 +3431,26 @@ This cannot be undone!</source> <target>停止聊天错误</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error switching profile" xml:space="preserve"> + <source>Error switching profile</source> + <target>切换配置文件出错</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> <target>切换资料错误!</target> - <note>No comment provided by engineer.</note> + <note>alertTitle</note> </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> <target>同步连接错误</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error testing server connection" xml:space="preserve"> + <source>Error testing server connection</source> + <target>检验服务器连接出错</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> <target>更新群组链接错误</target> @@ -2330,6 +3461,11 @@ This cannot be undone!</source> <target>更新消息错误</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error updating server" xml:space="preserve"> + <source>Error updating server</source> + <target>更新服务器出错</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> <target>更新设置错误</target> @@ -2340,6 +3476,16 @@ This cannot be undone!</source> <target>更新用户隐私时出错</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error uploading the archive" xml:space="preserve"> + <source>Error uploading the archive</source> + <target>上传存档出错</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error verifying passphrase:" xml:space="preserve"> + <source>Error verifying passphrase:</source> + <target>验证密码短语出错:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error: " xml:space="preserve"> <source>Error: </source> <target>错误: </target> @@ -2348,7 +3494,9 @@ This cannot be undone!</source> <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> <target>错误: %@</target> - <note>No comment provided by engineer.</note> + <note>alert message +file error text +snd error text</note> </trans-unit> <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> @@ -2360,6 +3508,16 @@ This cannot be undone!</source> <target>错误:没有数据库文件</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Errors" xml:space="preserve"> + <source>Errors</source> + <target>错误</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Errors in servers configuration." xml:space="preserve"> + <source>Errors in servers configuration.</source> + <target>服务器配置有错误。</target> + <note>servers error</note> + </trans-unit> <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> <source>Even when disabled in the conversation.</source> <target>即使在对话中被禁用。</target> @@ -2372,8 +3530,14 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Expand" xml:space="preserve"> <source>Expand</source> + <target>展开</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Expired" xml:space="preserve"> + <source>Expired</source> + <target>已过期</target> + <note>token status text</note> + </trans-unit> <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> <target>导出数据库</target> @@ -2384,11 +3548,21 @@ This cannot be undone!</source> <target>导出错误:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Export theme" xml:space="preserve"> + <source>Export theme</source> + <target>导出主题</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> <target>导出数据库归档。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Exported file doesn't exist" xml:space="preserve"> + <source>Exported file doesn't exist</source> + <target>导出的文件不存在</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Exporting database archive…" xml:space="preserve"> <source>Exporting database archive…</source> <target>导出数据库档案中…</target> @@ -2404,15 +3578,70 @@ This cannot be undone!</source> <target>快速且无需等待发件人在线!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Faster deletion of groups." xml:space="preserve"> + <source>Faster deletion of groups.</source> + <target>更快地删除群。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Faster joining and more reliable messages." xml:space="preserve"> <source>Faster joining and more reliable messages.</source> + <target>加入速度更快、信息更可靠。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Faster sending messages." xml:space="preserve"> + <source>Faster sending messages.</source> + <target>更快发送消息。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Favorite" xml:space="preserve"> <source>Favorite</source> <target>最喜欢</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Favorites" xml:space="preserve"> + <source>Favorites</source> + <target>收藏</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>文件错误</target> + <note>file error alert title</note> + </trans-unit> + <trans-unit id="File errors: %@" xml:space="preserve"> + <source>File errors: +%@</source> + <target>文件错误: +%@</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="File is blocked by server operator: %@." xml:space="preserve"> + <source>File is blocked by server operator: +%@.</source> + <target>文件被服务器运营方阻止: +%@。</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve"> + <source>File not found - most likely file was deleted or cancelled.</source> + <target>找不到文件 - 很可能文件已被删除或取消。</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File server error: %@" xml:space="preserve"> + <source>File server error: %@</source> + <target>文件服务器错误:%@</target> + <note>file error text</note> + </trans-unit> + <trans-unit id="File status" xml:space="preserve"> + <source>File status</source> + <target>文件状态</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File status: %@" xml:space="preserve"> + <source>File status: %@</source> + <target>文件状态:%@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="File will be deleted from servers." xml:space="preserve"> <source>File will be deleted from servers.</source> <target>文件将从服务器中删除。</target> @@ -2433,6 +3662,11 @@ This cannot be undone!</source> <target>文件:%@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files" xml:space="preserve"> + <source>Files</source> + <target>文件</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> <target>文件和媒体</target> @@ -2443,11 +3677,16 @@ This cannot be undone!</source> <target>文件和媒体</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> - <source>Files and media are prohibited in this group.</source> + <trans-unit id="Files and media are prohibited." xml:space="preserve"> + <source>Files and media are prohibited.</source> <target>此群组中禁止文件和媒体。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Files and media not allowed" xml:space="preserve"> + <source>Files and media not allowed</source> + <target>不允许文件和媒体</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Files and media prohibited!" xml:space="preserve"> <source>Files and media prohibited!</source> <target>禁止文件和媒体!</target> @@ -2458,6 +3697,16 @@ This cannot be undone!</source> <target>过滤未读和收藏的聊天记录。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Finalize migration" xml:space="preserve"> + <source>Finalize migration</source> + <target>完成迁移</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finalize migration on another device." xml:space="preserve"> + <source>Finalize migration on another device.</source> + <target>在另一部设备上完成迁移.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> <target>终于我们有它们了! 🚀</target> @@ -2498,13 +3747,118 @@ This cannot be undone!</source> <target>修复群组成员不支持的问题</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For all moderators" xml:space="preserve"> + <source>For all moderators</source> + <target>所有 moderators</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For chat profile %@:" xml:space="preserve"> + <source>For chat profile %@:</source> + <target>为聊天资料 %@:</target> + <note>servers error</note> + </trans-unit> <trans-unit id="For console" xml:space="preserve"> <source>For console</source> <target>用于控制台</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." xml:space="preserve"> + <source>For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</source> + <target>比如,如果你通过 SimpleX 服务器收到消息,应用会通过 Flux 服务器传送它们。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For me" xml:space="preserve"> + <source>For me</source> + <target>仅自己</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For private routing" xml:space="preserve"> + <source>For private routing</source> + <target>用于私密路由</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For social media" xml:space="preserve"> + <source>For social media</source> + <target>用于社交媒体</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward" xml:space="preserve"> + <source>Forward</source> + <target>转发</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Forward %d message(s)?" xml:space="preserve"> + <source>Forward %d message(s)?</source> + <target>转发 %d 条消息?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Forward and save messages" xml:space="preserve"> + <source>Forward and save messages</source> + <target>转发并保存消息</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forward messages" xml:space="preserve"> + <source>Forward messages</source> + <target>已转发的消息</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Forward messages without files?" xml:space="preserve"> + <source>Forward messages without files?</source> + <target>仅转发消息不转发文件?</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> + <source>Forward up to 20 messages at once.</source> + <target>一次转发最多20条消息。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded" xml:space="preserve"> + <source>Forwarded</source> + <target>已转发</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarded from" xml:space="preserve"> + <source>Forwarded from</source> + <target>转发自</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding %lld messages" xml:space="preserve"> + <source>Forwarding %lld messages</source> + <target>正在转发 %lld 条消息</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve"> + <source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source> + <target>转发服务器 %@ 无法连接到目标服务器 %@。请稍后尝试。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server address is incompatible with network settings: %@.</source> + <target>转发服务器地址与网络设置不兼容:%@。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server version is incompatible with network settings: %@." xml:space="preserve"> + <source>Forwarding server version is incompatible with network settings: %@.</source> + <target>转发服务器版本与网络设置不兼容:%@。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Destination server error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Destination server error: %2$@</source> + <target>转发服务器: %1$@ +目标服务器错误: %2$@</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Forwarding server: %@ Error: %@" xml:space="preserve"> + <source>Forwarding server: %1$@ +Error: %2$@</source> + <target>转发服务器: %1$@ +错误: %2$@</target> + <note>snd error text</note> + </trans-unit> <trans-unit id="Found desktop" xml:space="preserve"> <source>Found desktop</source> + <target>找到了桌面</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="French interface" xml:space="preserve"> @@ -2522,13 +3876,9 @@ This cannot be undone!</source> <target>全名(可选)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full name:" xml:space="preserve"> - <source>Full name:</source> - <target>全名:</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Fully decentralized – visible only to members." xml:space="preserve"> <source>Fully decentralized – visible only to members.</source> + <target>完全去中心化 - 仅对成员可见。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fully re-implemented - work in background!" xml:space="preserve"> @@ -2546,6 +3896,21 @@ This cannot be undone!</source> <target>GIF 和贴纸</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Get notified when mentioned." xml:space="preserve"> + <source>Get notified when mentioned.</source> + <target>被提及时收到通知。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Good afternoon!" xml:space="preserve"> + <source>Good afternoon!</source> + <target>下午好!</target> + <note>message preview</note> + </trans-unit> + <trans-unit id="Good morning!" xml:space="preserve"> + <source>Good morning!</source> + <target>早上好!</target> + <note>message preview</note> + </trans-unit> <trans-unit id="Group" xml:space="preserve"> <source>Group</source> <target>群组</target> @@ -2553,10 +3918,12 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Group already exists" xml:space="preserve"> <source>Group already exists</source> + <target>群组已存在</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group already exists!" xml:space="preserve"> <source>Group already exists!</source> + <target>群已存在!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group display name" xml:space="preserve"> @@ -2599,36 +3966,6 @@ This cannot be undone!</source> <target>群组链接</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve"> - <source>Group members can add message reactions.</source> - <target>群组成员可以添加信息回应。</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> - <source>Group members can irreversibly delete sent messages.</source> - <target>群组成员可以不可撤回地删除已发送的消息。</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve"> - <source>Group members can send direct messages.</source> - <target>群组成员可以私信。</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> - <source>Group members can send disappearing messages.</source> - <target>群组成员可以发送限时消息。</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send files and media." xml:space="preserve"> - <source>Group members can send files and media.</source> - <target>群组成员可以发送文件和媒体。</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve"> - <source>Group members can send voice messages.</source> - <target>群组成员可以发送语音消息。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> <target>群组消息:</target> @@ -2669,11 +4006,21 @@ This cannot be undone!</source> <target>将为您删除群组——此操作无法撤消!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Groups" xml:space="preserve"> + <source>Groups</source> + <target>群</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Help" xml:space="preserve"> <source>Help</source> <target>帮助</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Help admins moderating their groups." xml:space="preserve"> + <source>Help admins moderating their groups.</source> + <target>帮助管理员管理群组。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> <target>隐藏</target> @@ -2714,15 +4061,30 @@ This cannot be undone!</source> <target>历史记录</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="History is not sent to new members." xml:space="preserve"> + <source>History is not sent to new members.</source> + <target>未发送历史消息给新成员。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> <target>SimpleX的工作原理</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="How it affects privacy" xml:space="preserve"> + <source>How it affects privacy</source> + <target>它如何影响隐私</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it helps privacy" xml:space="preserve"> + <source>How it helps privacy</source> + <target>它如何帮助隐私</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> <target>工作原理</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="How to" xml:space="preserve"> <source>How to</source> @@ -2739,21 +4101,26 @@ This cannot be undone!</source> <target>如何使用您的服务器</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Hungarian interface" xml:space="preserve"> + <source>Hungarian interface</source> + <target>匈牙利语界面</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> <target>ICE 服务器(每行一个)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="IP address" xml:space="preserve"> + <source>IP address</source> + <target>IP 地址</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> <source>If you can't meet in person, show QR code in a video call, or share the link.</source> <target>如果您不能亲自见面,可以在视频通话中展示二维码,或分享链接。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <target>如果您不能亲自见面,您可以**扫描视频通话中的二维码**,或者您的联系人可以分享邀请链接。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> <target>如果您在打开应用时输入该密码,所有应用程序数据将被不可撤回地删除!</target> @@ -2789,8 +4156,8 @@ This cannot be undone!</source> <target>立即</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve"> + <source>Immune to spam</source> <target>不受垃圾和骚扰消息影响</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2809,6 +4176,33 @@ This cannot be undone!</source> <target>导入数据库</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Import failed" xml:space="preserve"> + <source>Import failed</source> + <target>导入失败了</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import theme" xml:space="preserve"> + <source>Import theme</source> + <target>导入主题</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Importing archive" xml:space="preserve"> + <source>Importing archive</source> + <target>正在导入存档</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved delivery, reduced traffic usage. More improvements are coming soon!" xml:space="preserve"> + <source>Improved delivery, reduced traffic usage. +More improvements are coming soon!</source> + <target>改善传送,降低流量使用。 +更多改进即将推出!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved message delivery" xml:space="preserve"> + <source>Improved message delivery</source> + <target>改进了消息传递</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> <target>改进的隐私和安全</target> @@ -2819,11 +4213,31 @@ This cannot be undone!</source> <target>改进的服务器配置</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In order to continue, chat should be stopped." xml:space="preserve"> + <source>In order to continue, chat should be stopped.</source> + <target>必须停止聊天才能继续。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>答复</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="In-call sounds" xml:space="preserve"> + <source>In-call sounds</source> + <target>通话声音</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Inappropriate content" xml:space="preserve"> + <source>Inappropriate content</source> + <target>不当内容</target> + <note>report reason</note> + </trans-unit> + <trans-unit id="Inappropriate profile" xml:space="preserve"> + <source>Inappropriate profile</source> + <target>不当个人资料</target> + <note>report reason</note> + </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> <target>隐身聊天</target> @@ -2831,6 +4245,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Incognito groups" xml:space="preserve"> <source>Incognito groups</source> + <target>匿名群组</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito mode" xml:space="preserve"> @@ -2865,6 +4280,7 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Incompatible version" xml:space="preserve"> <source>Incompatible version</source> + <target>不兼容的版本</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incorrect passcode" xml:space="preserve"> @@ -2892,6 +4308,11 @@ This cannot be undone!</source> <target>安装[用于终端的 SimpleX Chat](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Instant" xml:space="preserve"> + <source>Instant</source> + <target>即时</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> @@ -2899,29 +4320,80 @@ This cannot be undone!</source> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Instantly" xml:space="preserve"> - <source>Instantly</source> - <target>即时</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> <target>界面</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Interface colors" xml:space="preserve"> + <source>Interface colors</source> + <target>界面颜色</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid" xml:space="preserve"> + <source>Invalid</source> + <target>无效</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (bad token)" xml:space="preserve"> + <source>Invalid (bad token)</source> + <target>Token 无效</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (expired)" xml:space="preserve"> + <source>Invalid (expired)</source> + <target>无效(已过期)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (unregistered)" xml:space="preserve"> + <source>Invalid (unregistered)</source> + <target>无效(未注册)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid (wrong topic)" xml:space="preserve"> + <source>Invalid (wrong topic)</source> + <target>无效(话题有误)</target> + <note>token status text</note> + </trans-unit> + <trans-unit id="Invalid QR code" xml:space="preserve"> + <source>Invalid QR code</source> + <target>无效的二维码</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> <target>无效的连接链接</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid display name!" xml:space="preserve"> + <source>Invalid display name!</source> + <target>无效的显示名!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid link" xml:space="preserve"> + <source>Invalid link</source> + <target>无效链接</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>迁移确认无效</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invalid name!" xml:space="preserve"> <source>Invalid name!</source> + <target>无效名称!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid response" xml:space="preserve"> + <source>Invalid response</source> + <target>无效的响应</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> <target>无效的服务器地址!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Invalid status" xml:space="preserve"> <source>Invalid status</source> @@ -2943,6 +4415,11 @@ This cannot be undone!</source> <target>邀请成员</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invite to chat" xml:space="preserve"> + <source>Invite to chat</source> + <target>邀请加入聊天</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Invite to group" xml:space="preserve"> <source>Invite to group</source> <target>邀请加入群组</target> @@ -2958,8 +4435,8 @@ This cannot be undone!</source> <target>此聊天中禁止不可撤回消息移除。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve"> + <source>Irreversible message deletion is prohibited.</source> <target>此群组中禁止不可撤回消息移除。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2984,6 +4461,11 @@ This cannot be undone!</source> 3.连接被破坏。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="It protects your IP address and connections." xml:space="preserve"> + <source>It protects your IP address and connections.</source> + <target>它可以保护您的 IP 地址和连接。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> <target>您似乎已经通过此链接连接。如果不是这样,则有一个错误 (%@)。</target> @@ -3002,15 +4484,21 @@ This cannot be undone!</source> <trans-unit id="Join" xml:space="preserve"> <source>Join</source> <target>加入</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> <target>加入群组</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Join group conversations" xml:space="preserve"> + <source>Join group conversations</source> + <target>加入群对话</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Join group?" xml:space="preserve"> <source>Join group?</source> + <target>加入群组?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Join incognito" xml:space="preserve"> @@ -3020,11 +4508,14 @@ This cannot be undone!</source> </trans-unit> <trans-unit id="Join with current profile" xml:space="preserve"> <source>Join with current profile</source> + <target>使用当前档案加入</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Join your group? This is your link for group %@!" xml:space="preserve"> <source>Join your group? This is your link for group %@!</source> + <target>加入您的群组? +这是您组 %@ 的链接!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Joining group" xml:space="preserve"> @@ -3032,10 +4523,26 @@ This is your link for group %@!</source> <target>加入群组中</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep" xml:space="preserve"> + <source>Keep</source> + <target>保留</target> + <note>alert action</note> + </trans-unit> + <trans-unit id="Keep conversation" xml:space="preserve"> + <source>Keep conversation</source> + <target>保持对话</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Keep the app open to use it from desktop" xml:space="preserve"> <source>Keep the app open to use it from desktop</source> + <target>保持应用程序打开状态以从桌面使用它</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Keep unused invitation?" xml:space="preserve"> + <source>Keep unused invitation?</source> + <target>保留未使用的邀请吗?</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> <source>Keep your connections</source> <target>保持连接</target> @@ -3069,6 +4576,16 @@ This is your link for group %@!</source> <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> <target>离开</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Leave chat" xml:space="preserve"> + <source>Leave chat</source> + <target>离开聊天</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave chat?" xml:space="preserve"> + <source>Leave chat?</source> + <target>离开聊天?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group" xml:space="preserve"> @@ -3098,14 +4615,32 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Link mobile and desktop apps! 🔗" xml:space="preserve"> <source>Link mobile and desktop apps! 🔗</source> + <target>连接移动端和桌面端应用程序!🔗</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Linked desktop options" xml:space="preserve"> <source>Linked desktop options</source> + <target>已链接桌面选项</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Linked desktops" xml:space="preserve"> <source>Linked desktops</source> + <target>已链接桌面</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List" xml:space="preserve"> + <source>List</source> + <target>列表</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="List name and emoji should be different for all lists." xml:space="preserve"> + <source>List name and emoji should be different for all lists.</source> + <target>所有列表的名称和表情符号都应不同。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="List name..." xml:space="preserve"> + <source>List name...</source> + <target>列表名…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Live message!" xml:space="preserve"> @@ -3138,11 +4673,6 @@ This is your link for group %@!</source> <target>锁定模式</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> - <source>Make a private connection</source> - <target>建立私密连接</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make one message disappear" xml:space="preserve"> <source>Make one message disappear</source> <target>使一条消息消失</target> @@ -3153,21 +4683,11 @@ This is your link for group %@!</source> <target>将个人资料设为私密!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> - <target>请确保 %@服 务器地址格式正确,每行一个地址并且不重复 (%@)。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> <target>确保 WebRTC ICE 服务器地址格式正确、每行分开且不重复。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <target>许多人问: *如果SimpleX没有用户标识符,它怎么传递信息?*</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> <source>Mark deleted for everyone</source> <target>标记为所有人已删除</target> @@ -3193,11 +4713,36 @@ This is your link for group %@!</source> <target>最长30秒,立即接收。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Media & file servers" xml:space="preserve"> + <source>Media & file servers</source> + <target>Media & file servers</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Medium" xml:space="preserve"> + <source>Medium</source> + <target>中等</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Member" xml:space="preserve"> <source>Member</source> <target>成员</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member inactive" xml:space="preserve"> + <source>Member inactive</source> + <target>成员不活跃</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Member reports" xml:space="preserve"> + <source>Member reports</source> + <target>成员举报</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All chat members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All chat members will be notified.</source> + <target>将变更成员角色为“%@”。所有成员都会收到通知。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> <target>成员角色将更改为 "%@"。所有群成员将收到通知。</target> @@ -3208,11 +4753,66 @@ This is your link for group %@!</source> <target>成员角色将更改为 "%@"。该成员将收到一份新的邀请。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from chat - this cannot be undone!</source> + <target>将从聊天中删除成员 - 此操作无法撤销!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> <target>成员将被移出群组——此操作无法撤消!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Members can add message reactions." xml:space="preserve"> + <source>Members can add message reactions.</source> + <target>群组成员可以添加信息回应。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can irreversibly delete sent messages. (24 hours)" xml:space="preserve"> + <source>Members can irreversibly delete sent messages. (24 hours)</source> + <target>群组成员可以不可撤回地删除已发送的消息</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can report messsages to moderators." xml:space="preserve"> + <source>Members can report messsages to moderators.</source> + <target>成员可以向 moderators 举报消息。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send SimpleX links." xml:space="preserve"> + <source>Members can send SimpleX links.</source> + <target>群成员可发送 SimpleX 链接。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send direct messages." xml:space="preserve"> + <source>Members can send direct messages.</source> + <target>群组成员可以私信。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send disappearing messages." xml:space="preserve"> + <source>Members can send disappearing messages.</source> + <target>群组成员可以发送限时消息。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send files and media." xml:space="preserve"> + <source>Members can send files and media.</source> + <target>群组成员可以发送文件和媒体。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Members can send voice messages." xml:space="preserve"> + <source>Members can send voice messages.</source> + <target>群组成员可以发送语音消息。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mention members 👋" xml:space="preserve"> + <source>Mention members 👋</source> + <target>提及成员👋</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Menus" xml:space="preserve"> + <source>Menus</source> + <target>菜单</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>消息传递错误</target> @@ -3223,11 +4823,31 @@ This is your link for group %@!</source> <target>消息送达回执!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message delivery warning" xml:space="preserve"> + <source>Message delivery warning</source> + <target>消息传递警告</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> <target>消息草稿</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message forwarded" xml:space="preserve"> + <source>Message forwarded</source> + <target>消息已转发</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message may be delivered later if member becomes active." xml:space="preserve"> + <source>Message may be delivered later if member becomes active.</source> + <target>如果 member 变为活动状态,则稍后可能会发送消息。</target> + <note>item status description</note> + </trans-unit> + <trans-unit id="Message queue info" xml:space="preserve"> + <source>Message queue info</source> + <target>消息队列信息</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Message reactions" xml:space="preserve"> <source>Message reactions</source> <target>消息回应</target> @@ -3238,16 +4858,51 @@ This is your link for group %@!</source> <target>该聊天禁用了消息回应。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> - <source>Message reactions are prohibited in this group.</source> + <trans-unit id="Message reactions are prohibited." xml:space="preserve"> + <source>Message reactions are prohibited.</source> <target>该群组禁用了消息回应。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message reception" xml:space="preserve"> + <source>Message reception</source> + <target>消息接收</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message servers" xml:space="preserve"> + <source>Message servers</source> + <target>消息服务器</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message shape" xml:space="preserve"> + <source>Message shape</source> + <target>消息形状</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message source remains private." xml:space="preserve"> + <source>Message source remains private.</source> + <target>消息来源保持私密。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status" xml:space="preserve"> + <source>Message status</source> + <target>消息状态</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message status: %@" xml:space="preserve"> + <source>Message status: %@</source> + <target>消息状态:%@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> <target>消息正文</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Message too large" xml:space="preserve"> + <source>Message too large</source> + <target>消息太大了</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> <target>消息</target> @@ -3260,6 +4915,67 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Messages from %@ will be shown!" xml:space="preserve"> <source>Messages from %@ will be shown!</source> + <target>将显示来自 %@ 的消息!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages in this chat will never be deleted." xml:space="preserve"> + <source>Messages in this chat will never be deleted.</source> + <target>此聊天中的消息永远不会被删除。</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages received" xml:space="preserve"> + <source>Messages received</source> + <target>收到的消息</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages sent" xml:space="preserve"> + <source>Messages sent</source> + <target>已发送的消息</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages were deleted after you selected them." xml:space="preserve"> + <source>Messages were deleted after you selected them.</source> + <target>在你选中消息后这些消息已被删除。</target> + <note>alert message</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>消息、文件和通话受到 **端到端加密** 的保护,具有完全正向保密、否认和闯入恢复。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> + <source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> + <target>消息、文件和通话受到 **抗量子 e2e 加密** 的保护,具有完全正向保密、否认和闯入恢复。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate device" xml:space="preserve"> + <source>Migrate device</source> + <target>迁移设备</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate from another device" xml:space="preserve"> + <source>Migrate from another device</source> + <target>从另一台设备迁移</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate here" xml:space="preserve"> + <source>Migrate here</source> + <target>迁移到此处</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device" xml:space="preserve"> + <source>Migrate to another device</source> + <target>迁移到另一部设备</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrate to another device via QR code." xml:space="preserve"> + <source>Migrate to another device via QR code.</source> + <target>通过二维码迁移到另一部设备。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating" xml:space="preserve"> + <source>Migrating</source> + <target>迁移中</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Migrating database archive…" xml:space="preserve"> @@ -3267,6 +4983,11 @@ This is your link for group %@!</source> <target>迁移数据库档案中…</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Migration complete" xml:space="preserve"> + <source>Migration complete</source> + <target>迁移完毕</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> <target>迁移错误:</target> @@ -3282,9 +5003,9 @@ This is your link for group %@!</source> <target>迁移完成</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> - <source>Migrations: %@</source> - <target>迁移:%@</target> + <trans-unit id="Migrations:" xml:space="preserve"> + <source>Migrations:</source> + <target>迁移</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> @@ -3302,21 +5023,31 @@ This is your link for group %@!</source> <target>已被管理员移除于:%@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="More" xml:space="preserve"> + <source>More</source> + <target>更多</target> + <note>swipe action</note> + </trans-unit> <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> <target>更多改进即将推出!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="More reliable network connection." xml:space="preserve"> + <source>More reliable network connection.</source> + <target>更可靠的网络连接。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="More reliable notifications" xml:space="preserve"> + <source>More reliable notifications</source> + <target>更可靠的通知</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> <source>Most likely this connection is deleted.</source> <target>此连接很可能已被删除。</target> <note>item status description</note> </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> - <source>Most likely this contact has deleted the connection with you.</source> - <target>很可能此联系人已经删除了与您的联系。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> <target>多个聊天资料</target> @@ -3325,7 +5056,12 @@ This is your link for group %@!</source> <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> <target>静音</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> + </trans-unit> + <trans-unit id="Mute all" xml:space="preserve"> + <source>Mute all</source> + <target>全部静音</target> + <note>notification label action</note> </trans-unit> <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> @@ -3335,13 +5071,38 @@ This is your link for group %@!</source> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> <target>名称</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> <target>网络和服务器</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Network connection" xml:space="preserve"> + <source>Network connection</source> + <target>网络连接</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network decentralization" xml:space="preserve"> + <source>Network decentralization</source> + <target>网络去中心化</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network issues - message expired after many attempts to send it." xml:space="preserve"> + <source>Network issues - message expired after many attempts to send it.</source> + <target>网络问题 - 消息在多次尝试发送后过期。</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Network management" xml:space="preserve"> + <source>Network management</source> + <target>网络管理</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network operator" xml:space="preserve"> + <source>Network operator</source> + <target>网络运营方</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> <target>网络设置</target> @@ -3352,11 +5113,36 @@ This is your link for group %@!</source> <target>网络状态</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New" xml:space="preserve"> + <source>New</source> + <target>新</target> + <note>token status text</note> + </trans-unit> <trans-unit id="New Passcode" xml:space="preserve"> <source>New Passcode</source> <target>新密码</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve"> + <source>New SOCKS credentials will be used every time you start the app.</source> + <target>每次启动应用都会使用新的 SOCKS 凭据。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve"> + <source>New SOCKS credentials will be used for each server.</source> + <target>每个服务器都会使用新的 SOCKS 凭据。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat" xml:space="preserve"> + <source>New chat</source> + <target>新聊天</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New chat experience 🎉" xml:space="preserve"> + <source>New chat experience 🎉</source> + <target>新的聊天体验 🎉</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> <target>新联系人请求</target> @@ -3367,11 +5153,6 @@ This is your link for group %@!</source> <target>新联系人:</target> <note>notification</note> </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> - <source>New database archive</source> - <target>新数据库存档</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="New desktop app!" xml:space="preserve"> <source>New desktop app!</source> <target>全新桌面应用!</target> @@ -3382,11 +5163,21 @@ This is your link for group %@!</source> <target>新显示名</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <target>新事件</target> + <note>notification</note> + </trans-unit> <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> <target>%@ 的新内容</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New media options" xml:space="preserve"> + <source>New media options</source> + <target>新媒体选项</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> <target>新成员角色</target> @@ -3402,6 +5193,11 @@ This is your link for group %@!</source> <target>新密码……</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New server" xml:space="preserve"> + <source>New server</source> + <target>新服务器</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No" xml:space="preserve"> <source>No</source> <target>否</target> @@ -3412,6 +5208,21 @@ This is your link for group %@!</source> <target>没有应用程序密码</target> <note>Authentication unavailable</note> </trans-unit> + <trans-unit id="No chats" xml:space="preserve"> + <source>No chats</source> + <target>无聊天</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats found" xml:space="preserve"> + <source>No chats found</source> + <target>找不到聊天</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No chats in list %@" xml:space="preserve"> + <source>No chats in list %@</source> + <target>列表 %@ 中无聊天</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> <target>未选择联系人</target> @@ -3432,6 +5243,11 @@ This is your link for group %@!</source> <target>无设备令牌!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No direct connection yet, message is forwarded by admin." xml:space="preserve"> + <source>No direct connection yet, message is forwarded by admin.</source> + <target>还没有直接连接,消息由管理员转发。</target> + <note>item status description</note> + </trans-unit> <trans-unit id="No filtered chats" xml:space="preserve"> <source>No filtered chats</source> <target>无过滤聊天</target> @@ -3447,20 +5263,111 @@ This is your link for group %@!</source> <target>无历史记录</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No info, try to reload" xml:space="preserve"> + <source>No info, try to reload</source> + <target>无信息,尝试重新加载</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No media & file servers." xml:space="preserve"> + <source>No media & file servers.</source> + <target>无媒体和文件服务器。</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No message" xml:space="preserve"> + <source>No message</source> + <target>无消息</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No message servers." xml:space="preserve"> + <source>No message servers.</source> + <target>无消息服务器。</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No network connection" xml:space="preserve"> + <source>No network connection</source> + <target>无网络连接</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record speech" xml:space="preserve"> + <source>No permission to record speech</source> + <target>无录音权限</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record video" xml:space="preserve"> + <source>No permission to record video</source> + <target>无录像权限</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> <source>No permission to record voice message</source> <target>没有录制语音消息的权限</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No push server" xml:space="preserve"> + <source>No push server</source> + <target>本地</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> <target>未收到或发送文件</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No servers for private message routing." xml:space="preserve"> + <source>No servers for private message routing.</source> + <target>无私密消息路由服务器。</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive files." xml:space="preserve"> + <source>No servers to receive files.</source> + <target>无文件接收服务器。</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to receive messages." xml:space="preserve"> + <source>No servers to receive messages.</source> + <target>无消息接收服务器。</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No servers to send files." xml:space="preserve"> + <source>No servers to send files.</source> + <target>无文件发送服务器。</target> + <note>servers error</note> + </trans-unit> + <trans-unit id="No token!" xml:space="preserve"> + <source>No token!</source> + <target>无 token!</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="No unread chats" xml:space="preserve"> + <source>No unread chats</source> + <target>没有未读聊天</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No user identifiers." xml:space="preserve"> + <source>No user identifiers.</source> + <target>没有用户标识符。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Not compatible!" xml:space="preserve"> <source>Not compatible!</source> + <target>不兼容!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notes" xml:space="preserve"> + <source>Notes</source> + <target>附注</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing selected" xml:space="preserve"> + <source>Nothing selected</source> + <target>未选中任何内容</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Nothing to forward!" xml:space="preserve"> + <source>Nothing to forward!</source> + <target>无可转发!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> <target>通知</target> @@ -3471,6 +5378,21 @@ This is your link for group %@!</source> <target>通知被禁用!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Notifications error" xml:space="preserve"> + <source>Notifications error</source> + <target>通知错误</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Notifications privacy" xml:space="preserve"> + <source>Notifications privacy</source> + <target>通知隐私</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Notifications status" xml:space="preserve"> + <source>Notifications status</source> + <target>通知状态</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. @@ -3480,44 +5402,43 @@ This is your link for group %@!</source> - 禁用成员(“观察员”角色)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="OK" xml:space="preserve"> + <source>OK</source> + <target>好的</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Off" xml:space="preserve"> <source>Off</source> <target>关闭</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> - <source>Off (Local)</source> - <target>关闭(本地)</target> - <note>No comment provided by engineer.</note> + <note>blur media</note> </trans-unit> <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> <target>好的</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> <target>旧的数据库</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> - <source>Old database archive</source> - <target>旧数据库存档</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> <target>一次性邀请链接</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> - <target>Onion 主机将用于连接。需要启用 VPN。</target> + <trans-unit id="Onion hosts will be **required** for connection. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be **required** for connection. +Requires compatible VPN.</source> + <target>Onion 主机将是连接所必需的。 +需要兼容的 VPN。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> - <target>当可用时,将使用 Onion 主机。需要启用 VPN。</target> + <trans-unit id="Onion hosts will be used when available. Requires compatible VPN." xml:space="preserve"> + <source>Onion hosts will be used when available. +Requires compatible VPN.</source> + <target>如果可用,将使用洋葱主机。 +需要兼容的 VPN。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will not be used." xml:space="preserve"> @@ -3525,11 +5446,21 @@ This is your link for group %@!</source> <target>将不会使用 Onion 主机。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only chat owners can change preferences." xml:space="preserve"> + <source>Only chat owners can change preferences.</source> + <target>仅聊天所有人可更改首选项。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <target>只有客户端设备存储用户资料、联系人、群组和**双层端到端加密**发送的消息。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only delete conversation" xml:space="preserve"> + <source>Only delete conversation</source> + <target>仅删除对话</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> <source>Only group owners can change group preferences.</source> <target>只有群主可以改变群组偏好设置。</target> @@ -3545,14 +5476,24 @@ This is your link for group %@!</source> <target>只有群主可以启用语音信息。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Only sender and moderators see it" xml:space="preserve"> + <source>Only sender and moderators see it</source> + <target>仅发送人和moderators能看到</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you and moderators see it" xml:space="preserve"> + <source>Only you and moderators see it</source> + <target>只有你和moderators能看到</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Only you can add message reactions." xml:space="preserve"> <source>Only you can add message reactions.</source> <target>只有您可以添加消息回应。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <target>只有您可以不可撤回地删除消息(您的联系人可以将它们标记为删除)。</target> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</source> + <target>只有您可以不可撤回地删除消息(您的联系人可以将它们标记为删除)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can make calls." xml:space="preserve"> @@ -3575,9 +5516,9 @@ This is your link for group %@!</source> <target>只有您的联系人可以添加消息回应。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <target>只有您的联系人才能不可撤回地删除消息(您可以将它们标记为删除)。</target> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" xml:space="preserve"> + <source>Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</source> + <target>只有您的联系人才能不可撤回地删除消息(您可以将它们标记为删除)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can make calls." xml:space="preserve"> @@ -3598,13 +5539,18 @@ This is your link for group %@!</source> <trans-unit id="Open" xml:space="preserve"> <source>Open</source> <target>打开</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>打开设置</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open changes" xml:space="preserve"> + <source>Open changes</source> + <target>打开更改</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> <target>打开聊天</target> @@ -3615,25 +5561,85 @@ This is your link for group %@!</source> <target>打开聊天控制台</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Open conditions" xml:space="preserve"> + <source>Open conditions</source> + <target>打开条款</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open group" xml:space="preserve"> <source>Open group</source> + <target>打开群</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve"> - <source>Open user profiles</source> - <target>打开用户个人资料</target> + <trans-unit id="Open link?" xml:space="preserve"> + <source>Open link?</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Open migration to another device" xml:space="preserve"> + <source>Open migration to another device</source> + <target>打开迁移到另一台设备</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> - <source>Open-source protocol and code – anybody can run the servers.</source> - <target>开源协议和代码——任何人都可以运行服务器。</target> + <trans-unit id="Opening app…" xml:space="preserve"> + <source>Opening app…</source> + <target>正在打开应用程序…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve"> - <source>Opening database…</source> - <target>打开数据库中……</target> + <trans-unit id="Operator" xml:space="preserve"> + <source>Operator</source> + <target>运营方</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Operator server" xml:space="preserve"> + <source>Operator server</source> + <target>运营方服务器</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Or import archive file" xml:space="preserve"> + <source>Or import archive file</source> + <target>或者导入或者导入压缩文件</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or paste archive link" xml:space="preserve"> + <source>Or paste archive link</source> + <target>或粘贴存档链接</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or scan QR code" xml:space="preserve"> + <source>Or scan QR code</source> + <target>或者扫描二维码</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or securely share this file link" xml:space="preserve"> + <source>Or securely share this file link</source> + <target>或安全地分享此文件链接</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or show this code" xml:space="preserve"> + <source>Or show this code</source> + <target>或者显示此码</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Or to share privately" xml:space="preserve"> + <source>Or to share privately</source> + <target>或者私下分享</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Organize chats into lists" xml:space="preserve"> + <source>Organize chats into lists</source> + <target>将聊天组织到列表</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other" xml:space="preserve"> + <source>Other</source> + <target>其他</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Other file errors: %@" xml:space="preserve"> + <source>Other file errors: +%@</source> + <note>alert message</note> + </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> <target>PING 次数</target> @@ -3669,18 +5675,24 @@ This is your link for group %@!</source> <target>密码已设置!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Password" xml:space="preserve"> + <source>Password</source> + <target>密码</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> <target>显示密码</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste" xml:space="preserve"> - <source>Paste</source> - <target>粘贴</target> - <note>No comment provided by engineer.</note> + <trans-unit id="Past member %@" xml:space="preserve"> + <source>Past member %@</source> + <target>前任成员 %@</target> + <note>past/unknown group member</note> </trans-unit> <trans-unit id="Paste desktop address" xml:space="preserve"> <source>Paste desktop address</source> + <target>粘贴桌面地址</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Paste image" xml:space="preserve"> @@ -3688,23 +5700,23 @@ This is your link for group %@!</source> <target>粘贴图片</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve"> - <source>Paste received link</source> - <target>粘贴收到的链接</target> + <trans-unit id="Paste link to connect!" xml:space="preserve"> + <source>Paste link to connect!</source> + <target>粘贴链接以连接!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> - <source>Paste the link you received to connect with your contact.</source> - <target>将您收到的链接粘贴到下面的框中以与您的联系人联系。</target> - <note>placeholder</note> - </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> - <source>People can connect to you only via the links you share.</source> - <target>人们只能通过您共享的链接与您建立联系。</target> + <trans-unit id="Paste the link you received" xml:space="preserve"> + <source>Paste the link you received</source> + <target>粘贴您收到的链接</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Periodically" xml:space="preserve"> - <source>Periodically</source> + <trans-unit id="Pending" xml:space="preserve"> + <source>Pending</source> + <target>待定</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Periodic" xml:space="preserve"> + <source>Periodic</source> <target>定期</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3713,11 +5725,33 @@ This is your link for group %@!</source> <target>解密错误</target> <note>message decrypt error item</note> </trans-unit> + <trans-unit id="Picture-in-picture calls" xml:space="preserve"> + <source>Picture-in-picture calls</source> + <target>画中画通话</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Play from the chat list." xml:space="preserve"> + <source>Play from the chat list.</source> + <target>从聊天列表播放。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable calls." xml:space="preserve"> + <source>Please ask your contact to enable calls.</source> + <target>请要求您的联系人开通通话功能。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> <target>请让您的联系人启用发送语音消息。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers." xml:space="preserve"> + <source>Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers.</source> + <target>请检查移动设备和桌面是否连接到同一本地网络,以及桌面防火墙是否允许连接。 +请与开发人员分享任何其他问题。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> <target>请检查您使用的链接是否正确,或者让您的联系人给您发送另一个链接。</target> @@ -3733,6 +5767,18 @@ This is your link for group %@!</source> <target>请检查您和您的联系人偏好设置。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve"> + <source>Please confirm that network settings are correct for this device.</source> + <target>请确认网络设置对此这台设备正确无误。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact developers. Error: %@" xml:space="preserve"> + <source>Please contact developers. +Error: %@</source> + <target>请联系开发人员。 +错误:%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> <target>请联系群组管理员。</target> @@ -3773,11 +5819,27 @@ This is your link for group %@!</source> <target>请安全地保存密码,如果您丢失了密码,您将无法更改它。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Please try to disable and re-enable notfications." xml:space="preserve"> + <source>Please try to disable and re-enable notfications.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token activation to complete." xml:space="preserve"> + <source>Please wait for token activation to complete.</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Please wait for token to be registered." xml:space="preserve"> + <source>Please wait for token to be registered.</source> + <note>token info</note> + </trans-unit> <trans-unit id="Polish interface" xml:space="preserve"> <source>Polish interface</source> <target>波兰语界面</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Port" xml:space="preserve"> + <source>Port</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> <target>服务器地址中的证书指纹可能不正确</target> @@ -3788,36 +5850,83 @@ This is your link for group %@!</source> <target>保留最后的消息草稿及其附件。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Preset server" xml:space="preserve"> - <source>Preset server</source> - <target>预设服务器</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> <target>预设服务器地址</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Preset servers" xml:space="preserve"> + <source>Preset servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Preview" xml:space="preserve"> <source>Preview</source> <target>预览</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Previously connected servers" xml:space="preserve"> + <source>Previously connected servers</source> + <target>以前连接的服务器</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> <target>隐私和安全</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Privacy for your customers." xml:space="preserve"> + <source>Privacy for your customers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy policy and conditions of use." xml:space="preserve"> + <source>Privacy policy and conditions of use.</source> + <target>隐私政策和使用条款。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Privacy redefined" xml:space="preserve"> <source>Privacy redefined</source> <target>重新定义隐私</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private chats, groups and your contacts are not accessible to server operators." xml:space="preserve"> + <source>Private chats, groups and your contacts are not accessible to server operators.</source> + <target>服务器运营方无法访问私密聊天、群组和你的联系人。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> <target>私密文件名</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Private media file names." xml:space="preserve"> + <source>Private media file names.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing" xml:space="preserve"> + <source>Private message routing</source> + <target>私有消息路由</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private message routing 🚀" xml:space="preserve"> + <source>Private message routing 🚀</source> + <target>私有消息路由 🚀</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private notes" xml:space="preserve"> + <source>Private notes</source> + <target>私密笔记</target> + <note>name of notes to self</note> + </trans-unit> + <trans-unit id="Private routing" xml:space="preserve"> + <source>Private routing</source> + <target>专用路由</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private routing error" xml:space="preserve"> + <source>Private routing error</source> + <target>专用路由错误</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> <target>资料和服务器连接</target> @@ -3828,12 +5937,9 @@ This is your link for group %@!</source> <target>资料图片</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile name" xml:space="preserve"> - <source>Profile name</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile name:" xml:space="preserve"> - <source>Profile name:</source> + <trans-unit id="Profile images" xml:space="preserve"> + <source>Profile images</source> + <target>个人资料图</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile password" xml:space="preserve"> @@ -3841,10 +5947,15 @@ This is your link for group %@!</source> <target>个人资料密码</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Profile theme" xml:space="preserve"> + <source>Profile theme</source> + <target>个人资料主题</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> <source>Profile update will be sent to your contacts.</source> <target>个人资料更新将被发送给您的联系人。</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> @@ -3866,6 +5977,15 @@ This is your link for group %@!</source> <target>禁止消息回应。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Prohibit reporting messages to moderators." xml:space="preserve"> + <source>Prohibit reporting messages to moderators.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending SimpleX links." xml:space="preserve"> + <source>Prohibit sending SimpleX links.</source> + <target>禁止发送 SimpleX 链接。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> <target>禁止向成员发送私信。</target> @@ -3886,11 +6006,23 @@ This is your link for group %@!</source> <target>禁止发送语音消息。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect IP address" xml:space="preserve"> + <source>Protect IP address</source> + <target>保护 IP 地址</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> <target>保护应用程序屏幕</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings." xml:space="preserve"> + <source>Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings.</source> + <target>保护您的 IP 地址免受联系人选择的消息中继的攻击。 +在*网络和服务器*设置中启用。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> <target>使用密码保护您的聊天资料!</target> @@ -3906,16 +6038,45 @@ This is your link for group %@!</source> <target>每 KB 协议超时</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Proxied" xml:space="preserve"> + <source>Proxied</source> + <target>代理</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxied servers" xml:space="preserve"> + <source>Proxied servers</source> + <target>代理服务器</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Proxy requires password" xml:space="preserve"> + <source>Proxy requires password</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> <target>推送通知</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Push server" xml:space="preserve"> + <source>Push server</source> + <target>推送服务器</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Quantum resistant encryption" xml:space="preserve"> + <source>Quantum resistant encryption</source> + <target>抗量子加密</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> <source>Rate the app</source> <target>评价此应用程序</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reachable chat toolbar" xml:space="preserve"> + <source>Reachable chat toolbar</source> + <target>可访问的聊天工具栏</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> <target>回应…</target> @@ -3924,16 +6085,21 @@ This is your link for group %@!</source> <trans-unit id="Read" xml:space="preserve"> <source>Read</source> <target>已读</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Read more" xml:space="preserve"> <source>Read more</source> <target>阅读更多</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> - <target>在 [用户指南](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address) 中阅读更多内容。</target> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source> + <target>阅读更多[User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).</source> + <target>在 [用户指南](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses) 中阅读更多内容。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> @@ -3941,11 +6107,6 @@ This is your link for group %@!</source> <target>在 [用户指南](https://simplex.chat/docs/guide/readme.html#connect-to-friends) 中阅读更多内容。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> - <source>Read more in our GitHub repository.</source> - <target>在我们的 GitHub 仓库中阅读更多内容。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> <target>在我们的 [GitHub 仓库](https://github.com/simplex-chat/simplex-chat#readme) 中阅读更多信息。</target> @@ -3956,6 +6117,11 @@ This is your link for group %@!</source> <target>回执已禁用</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receive errors" xml:space="preserve"> + <source>Receive errors</source> + <target>接收错误</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>已收到于</target> @@ -3976,6 +6142,21 @@ This is your link for group %@!</source> <target>收到的信息</target> <note>message info title</note> </trans-unit> + <trans-unit id="Received messages" xml:space="preserve"> + <source>Received messages</source> + <target>收到的消息</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received reply" xml:space="preserve"> + <source>Received reply</source> + <target>已收到回复</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received total" xml:space="preserve"> + <source>Received total</source> + <target>接收总数</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> <target>接收地址将变更到不同的服务器。地址更改将在发件人上线后完成。</target> @@ -3991,16 +6172,51 @@ This is your link for group %@!</source> <target>接收通过</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Recent history and improved [directory bot](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)." xml:space="preserve"> + <source>Recent history and improved [directory bot](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).</source> + <target>最近的历史记录和改进的 [目录机器人](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve"> + <source>Recipient(s) can't see who this message is from.</source> + <target>收件人看不到这条消息来自何人。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> <target>对方会在您键入时看到更新。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect" xml:space="preserve"> + <source>Reconnect</source> + <target>重新连接</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> <target>重新连接所有已连接的服务器以强制发送信息。这会耗费更多流量。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect all servers" xml:space="preserve"> + <source>Reconnect all servers</source> + <target>重新连接所有服务器</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all servers?" xml:space="preserve"> + <source>Reconnect all servers?</source> + <target>重新连接所有服务器?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect server to force message delivery. It uses additional traffic.</source> + <target>重新连接服务器以强制发送信息。它使用额外的流量。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect server?" xml:space="preserve"> + <source>Reconnect server?</source> + <target>重新连接服务器?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> <target>是否重新连接服务器?</target> @@ -4021,10 +6237,23 @@ This is your link for group %@!</source> <target>减少电池使用量</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Register" xml:space="preserve"> + <source>Register</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Register notification token?" xml:space="preserve"> + <source>Register notification token?</source> + <note>token info</note> + </trans-unit> + <trans-unit id="Registered" xml:space="preserve"> + <source>Registered</source> + <note>token status text</note> + </trans-unit> <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> <target>拒绝</target> - <note>reject incoming call via notification</note> + <note>reject incoming call via notification +swipe action</note> </trans-unit> <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> <source>Reject (sender NOT notified)</source> @@ -4051,6 +6280,15 @@ This is your link for group %@!</source> <target>移除</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Remove archive?" xml:space="preserve"> + <source>Remove archive?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove image" xml:space="preserve"> + <source>Remove image</source> + <target>移除图片</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> <target>删除成员</target> @@ -4083,10 +6321,27 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Repeat connection request?" xml:space="preserve"> <source>Repeat connection request?</source> + <target>重复连接请求吗?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat download" xml:space="preserve"> + <source>Repeat download</source> + <target>重复下载</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat import" xml:space="preserve"> + <source>Repeat import</source> + <target>重复导入</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Repeat join request?" xml:space="preserve"> <source>Repeat join request?</source> + <target>重复加入请求吗?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Repeat upload" xml:space="preserve"> + <source>Repeat upload</source> + <target>重复上传</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reply" xml:space="preserve"> @@ -4094,6 +6349,46 @@ This is your link for group %@!</source> <target>回复</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Report" xml:space="preserve"> + <source>Report</source> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Report content: only group moderators will see it." xml:space="preserve"> + <source>Report content: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report member profile: only group moderators will see it." xml:space="preserve"> + <source>Report member profile: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report other: only group moderators will see it." xml:space="preserve"> + <source>Report other: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report reason?" xml:space="preserve"> + <source>Report reason?</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Report spam: only group moderators will see it." xml:space="preserve"> + <source>Report spam: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report violation: only group moderators will see it." xml:space="preserve"> + <source>Report violation: only group moderators will see it.</source> + <note>report reason</note> + </trans-unit> + <trans-unit id="Report: %@" xml:space="preserve"> + <source>Report: %@</source> + <note>report in notification</note> + </trans-unit> + <trans-unit id="Reporting messages to moderators is prohibited." xml:space="preserve"> + <source>Reporting messages to moderators is prohibited.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reports" xml:space="preserve"> + <source>Reports</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Required" xml:space="preserve"> <source>Required</source> <target>必须</target> @@ -4104,16 +6399,41 @@ This is your link for group %@!</source> <target>重置</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset all hints" xml:space="preserve"> + <source>Reset all hints</source> + <target>重置所有提示</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics" xml:space="preserve"> + <source>Reset all statistics</source> + <target>重置所有统计信息</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset all statistics?" xml:space="preserve"> + <source>Reset all statistics?</source> + <target>重置所有统计信息?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> <target>重置颜色</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to app theme" xml:space="preserve"> + <source>Reset to app theme</source> + <target>重置为应用程序主题</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> <target>重置为默认</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reset to user theme" xml:space="preserve"> + <source>Reset to user theme</source> + <target>重置为用户主题</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> <source>Restart the app to create a new chat profile</source> <target>重新启动应用程序以创建新的聊天资料</target> @@ -4144,29 +6464,34 @@ This is your link for group %@!</source> <target>恢复数据库错误</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Retry" xml:space="preserve"> + <source>Retry</source> + <target>重试</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> <target>揭示</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Revert" xml:space="preserve"> - <source>Revert</source> - <target>恢复</target> + <trans-unit id="Review conditions" xml:space="preserve"> + <source>Review conditions</source> + <target>审阅条款</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke" xml:space="preserve"> <source>Revoke</source> - <target>撤销</target> + <target>吊销</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke file" xml:space="preserve"> <source>Revoke file</source> - <target>撤销文件</target> + <target>吊销文件</target> <note>cancel file action</note> </trans-unit> <trans-unit id="Revoke file?" xml:space="preserve"> <source>Revoke file?</source> - <target>撤销文件?</target> + <target>吊销文件?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Role" xml:space="preserve"> @@ -4176,54 +6501,69 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Run chat" xml:space="preserve"> <source>Run chat</source> - <target>运行聊天程序</target> + <target>运行聊天</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve"> - <source>SMP servers</source> + <trans-unit id="SMP server" xml:space="preserve"> + <source>SMP server</source> <target>SMP 服务器</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SOCKS proxy" xml:space="preserve"> + <source>SOCKS proxy</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safely receive files" xml:space="preserve"> + <source>Safely receive files</source> + <target>安全接收文件</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Safer groups" xml:space="preserve"> + <source>Safer groups</source> + <target>更安全的群组</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save" xml:space="preserve"> <source>Save</source> <target>保存</target> - <note>chat item action</note> + <note>alert button +chat item action</note> </trans-unit> <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> <target>保存(并通知联系人)</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> <target>保存并通知联系人</target> - <note>No comment provided by engineer.</note> + <note>alert button</note> </trans-unit> <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> <target>保存并通知群组成员</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save and reconnect" xml:space="preserve"> + <source>Save and reconnect</source> + <target>保存并重新连接</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save and update group profile" xml:space="preserve"> <source>Save and update group profile</source> <target>保存和更新组配置文件</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save archive" xml:space="preserve"> - <source>Save archive</source> - <target>保存存档</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve"> - <source>Save auto-accept settings</source> - <target>保存自动接受设置</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> <source>Save group profile</source> <target>保存群组资料</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save list" xml:space="preserve"> + <source>Save list</source> + <target>保存列表</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> <target>保存密码并打开聊天</target> @@ -4237,7 +6577,7 @@ This is your link for group %@!</source> <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> <target>保存偏好设置?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> @@ -4252,23 +6592,53 @@ This is your link for group %@!</source> <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> <target>保存服务器?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve"> - <source>Save settings?</source> - <target>保存设置?</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> <target>保存欢迎信息?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Save your profile?" xml:space="preserve"> + <source>Save your profile?</source> + <target>保存您的个人资料?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Saved" xml:space="preserve"> + <source>Saved</source> + <target>已保存</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> <source>Saved WebRTC ICE servers will be removed</source> <target>已保存的WebRTC ICE服务器将被删除</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Saved from" xml:space="preserve"> + <source>Saved from</source> + <target>保存自</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved message" xml:space="preserve"> + <source>Saved message</source> + <target>已保存的消息</target> + <note>message info title</note> + </trans-unit> + <trans-unit id="Saving %lld messages" xml:space="preserve"> + <source>Saving %lld messages</source> + <target>正在保存 %lld 条消息</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scale" xml:space="preserve"> + <source>Scale</source> + <target>规模</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan / Paste link" xml:space="preserve"> + <source>Scan / Paste link</source> + <target>扫描 / 粘贴链接</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> <target>扫描二维码</target> @@ -4276,6 +6646,7 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Scan QR code from desktop" xml:space="preserve"> <source>Scan QR code from desktop</source> + <target>从桌面扫描二维码</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Scan code" xml:space="preserve"> @@ -4298,11 +6669,31 @@ This is your link for group %@!</source> <target>搜索</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Search bar accepts invitation links." xml:space="preserve"> + <source>Search bar accepts invitation links.</source> + <target>搜索栏接受邀请链接。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search or paste SimpleX link" xml:space="preserve"> + <source>Search or paste SimpleX link</source> + <target>搜索或粘贴 SimpleX 链接</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secondary" xml:space="preserve"> + <source>Secondary</source> + <target>二级</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> <source>Secure queue</source> <target>保护队列</target> <note>server test step</note> </trans-unit> + <trans-unit id="Secured" xml:space="preserve"> + <source>Secured</source> + <target>担保</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> <target>安全评估</target> @@ -4316,6 +6707,20 @@ This is your link for group %@!</source> <trans-unit id="Select" xml:space="preserve"> <source>Select</source> <target>选择</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Select chat profile" xml:space="preserve"> + <source>Select chat profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected %lld" xml:space="preserve"> + <source>Selected %lld</source> + <target>选定的 %lld</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>选定的聊天首选项禁止此消息。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct" xml:space="preserve"> @@ -4353,11 +6758,6 @@ This is your link for group %@!</source> <target>将送达回执发送给</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve"> - <source>Send direct message</source> - <target>发送私信</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Send direct message to connect" xml:space="preserve"> <source>Send direct message to connect</source> <target>发送私信来连接</target> @@ -4368,6 +6768,11 @@ This is your link for group %@!</source> <target>发送限时消息中</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send errors" xml:space="preserve"> + <source>Send errors</source> + <target>发送错误</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> <target>发送链接预览</target> @@ -4378,14 +6783,28 @@ This is your link for group %@!</source> <target>发送实时消息</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send message to enable calls." xml:space="preserve"> + <source>Send message to enable calls.</source> + <target>发送消息以启用呼叫。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source> + <target>当 IP 地址受到保护并且您或目标服务器不支持私有路由时,直接发送消息。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send messages directly when your or destination server does not support private routing." xml:space="preserve"> + <source>Send messages directly when your or destination server does not support private routing.</source> + <target>当您或目标服务器不支持私有路由时,直接发送消息。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> <target>发送通知</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve"> - <source>Send notifications:</source> - <target>发送通知:</target> + <trans-unit id="Send private reports" xml:space="preserve"> + <source>Send private reports</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve"> @@ -4403,10 +6822,15 @@ This is your link for group %@!</source> <target>发送它们来自图库或自定义键盘。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve"> + <source>Send up to 100 last messages to new members.</source> + <target>给新成员发送最多 100 条历史消息。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> <target>发送人已取消文件传输。</target> - <note>No comment provided by engineer.</note> + <note>alert message</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> @@ -4463,6 +6887,11 @@ This is your link for group %@!</source> <target>已发送于:%@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Sent directly" xml:space="preserve"> + <source>Sent directly</source> + <target>直接发送</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> <target>已发送文件项目</target> @@ -4473,11 +6902,66 @@ This is your link for group %@!</source> <target>已发信息</target> <note>message info title</note> </trans-unit> + <trans-unit id="Sent messages" xml:space="preserve"> + <source>Sent messages</source> + <target>已发送的消息</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> <target>已发送的消息将在设定的时间后被删除。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sent reply" xml:space="preserve"> + <source>Sent reply</source> + <target>已发送回复</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent total" xml:space="preserve"> + <source>Sent total</source> + <target>发送总数</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent via proxy" xml:space="preserve"> + <source>Sent via proxy</source> + <target>通过代理发送</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server" xml:space="preserve"> + <source>Server</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server added to operator %@." xml:space="preserve"> + <source>Server added to operator %@.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Server address" xml:space="preserve"> + <source>Server address</source> + <target>服务器地址</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings." xml:space="preserve"> + <source>Server address is incompatible with network settings.</source> + <target>服务器地址与网络设置不兼容。</target> + <note>srv error text.</note> + </trans-unit> + <trans-unit id="Server address is incompatible with network settings: %@." xml:space="preserve"> + <source>Server address is incompatible with network settings: %@.</source> + <target>服务器地址与网络设置不兼容:%@。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server operator changed." xml:space="preserve"> + <source>Server operator changed.</source> + <note>alert title</note> + </trans-unit> + <trans-unit id="Server operators" xml:space="preserve"> + <source>Server operators</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server protocol changed." xml:space="preserve"> + <source>Server protocol changed.</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> <target>服务器需要授权才能创建队列,检查密码</target> @@ -4493,13 +6977,39 @@ This is your link for group %@!</source> <target>服务器测试失败!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Server type" xml:space="preserve"> + <source>Server type</source> + <target>服务器类型</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server version is incompatible with network settings." xml:space="preserve"> + <source>Server version is incompatible with network settings.</source> + <target>服务器版本与网络设置不兼容。</target> + <note>srv error text</note> + </trans-unit> + <trans-unit id="Server version is incompatible with your app: %@." xml:space="preserve"> + <source>Server version is incompatible with your app: %@.</source> + <target>服务器版本与你的应用程序不兼容:%@。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Servers" xml:space="preserve"> <source>Servers</source> <target>服务器</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Servers info" xml:space="preserve"> + <source>Servers info</source> + <target>服务器信息</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <target>服务器统计信息将被重置 - 此操作无法撤消!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Session code" xml:space="preserve"> <source>Session code</source> + <target>会话码</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set 1 day" xml:space="preserve"> @@ -4507,11 +7017,20 @@ This is your link for group %@!</source> <target>设定1天</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set chat name…" xml:space="preserve"> + <source>Set chat name…</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> <target>设置联系人姓名……</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve"> + <source>Set default theme</source> + <target>设置默认主题</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> <target>设置群组偏好设置</target> @@ -4522,11 +7041,20 @@ This is your link for group %@!</source> <target>设置它以代替系统身份验证。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve"> + <source>Set message expiration in chats.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passcode" xml:space="preserve"> <source>Set passcode</source> <target>设置密码</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Set passphrase" xml:space="preserve"> + <source>Set passphrase</source> + <target>设置密码短语</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> <target>设置密码来导出</target> @@ -4547,24 +7075,51 @@ This is your link for group %@!</source> <target>设置</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve"> + <source>Settings were changed.</source> + <note>alert message</note> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve"> + <source>Shape profile images</source> + <target>改变个人资料图形状</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share" xml:space="preserve"> <source>Share</source> <target>分享</target> - <note>chat item action</note> + <note>alert action +chat item action</note> </trans-unit> <trans-unit id="Share 1-time link" xml:space="preserve"> <source>Share 1-time link</source> <target>分享一次性链接</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share 1-time link with a friend" xml:space="preserve"> + <source>Share 1-time link with a friend</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve"> + <source>Share SimpleX address on social media.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address" xml:space="preserve"> <source>Share address</source> <target>分享地址</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Share address publicly" xml:space="preserve"> + <source>Share address publicly</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Share address with contacts?" xml:space="preserve"> <source>Share address with contacts?</source> <target>与联系人分享地址?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Share from other apps." xml:space="preserve"> + <source>Share from other apps.</source> + <target>从其他应用程序共享。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share link" xml:space="preserve"> @@ -4572,9 +7127,18 @@ This is your link for group %@!</source> <target>分享链接</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve"> - <source>Share one-time invitation link</source> - <target>分享一次性邀请链接</target> + <trans-unit id="Share profile" xml:space="preserve"> + <source>Share profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share this 1-time invite link" xml:space="preserve"> + <source>Share this 1-time invite link</source> + <target>分享此一次性邀请链接</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share to SimpleX" xml:space="preserve"> + <source>Share to SimpleX</source> + <target>分享到 SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share with contacts" xml:space="preserve"> @@ -4582,6 +7146,15 @@ This is your link for group %@!</source> <target>与联系人分享</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Short link" xml:space="preserve"> + <source>Short link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show QR code" xml:space="preserve"> + <source>Show QR code</source> + <target>显示二维码</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> <target>在电话历史记录中显示通话</target> @@ -4597,21 +7170,46 @@ This is your link for group %@!</source> <target>显示最近的消息</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show message status" xml:space="preserve"> + <source>Show message status</source> + <target>显示消息状态</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show percentage" xml:space="preserve"> + <source>Show percentage</source> + <target>显示百分比</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>显示预览</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show → on messages sent via private routing." xml:space="preserve"> + <source>Show → on messages sent via private routing.</source> + <target>显示 → 通过专用路由发送的信息.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show:" xml:space="preserve"> <source>Show:</source> <target>显示:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve"> + <source>SimpleX</source> + <target>SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Address" xml:space="preserve"> <source>SimpleX Address</source> <target>SimpleX 地址</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." xml:space="preserve"> + <source>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</source> + <target>SimpleX Chat 与 Flux 达成了协议,将由 Flux 控制的服务器纳入 SimpleX 应用。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> <target>SimpleX Chat 的安全性 由 Trail of Bits 审核。</target> @@ -4642,6 +7240,21 @@ This is your link for group %@!</source> <target>SimpleX 地址</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="SimpleX address and 1-time links are safe to share via any messenger." xml:space="preserve"> + <source>SimpleX address and 1-time links are safe to share via any messenger.</source> + <target>可以通过任何消息应用安全分享 SimpleX 地址和一次性链接。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address or 1-time link?" xml:space="preserve"> + <source>SimpleX address or 1-time link?</source> + <target>SimpleX 地址或一次性链接?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX channel link" xml:space="preserve"> + <source>SimpleX channel link</source> + <target>SimpleX 频道链接</target> + <note>simplex link type</note> + </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> <target>SimpleX 联系地址</target> @@ -4649,17 +7262,27 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="SimpleX encrypted message or connection event" xml:space="preserve"> <source>SimpleX encrypted message or connection event</source> - <target>SimpleX 加密消息或连接项目</target> + <target>SimpleX 加密的消息或连接事件</target> <note>notification</note> </trans-unit> <trans-unit id="SimpleX group link" xml:space="preserve"> <source>SimpleX group link</source> - <target>SimpleX 群组链接</target> + <target>SimpleX 群链接</target> <note>simplex link type</note> </trans-unit> <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> <target>SimpleX 链接</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve"> + <source>SimpleX links are prohibited.</source> + <target>此群禁止 SimpleX 链接。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve"> + <source>SimpleX links not allowed</source> + <target>不允许SimpleX 链接</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> @@ -4667,11 +7290,21 @@ This is your link for group %@!</source> <target>SimpleX 一次性邀请</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> + <source>SimpleX protocols reviewed by Trail of Bits.</source> + <target>SimpleX 协议由 Trail of Bits 审阅。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Simplified incognito mode" xml:space="preserve"> <source>Simplified incognito mode</source> <target>简化的隐身模式</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Size" xml:space="preserve"> + <source>Size</source> + <target>大小</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>跳过</target> @@ -4687,19 +7320,49 @@ This is your link for group %@!</source> <target>小群组(最多 20 人)</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Soft" xml:space="preserve"> + <source>Soft</source> + <target>软</target> + <note>blur media</note> + </trans-unit> + <trans-unit id="Some app settings were not migrated." xml:space="preserve"> + <source>Some app settings were not migrated.</source> + <target>部分应用设置未被迁移。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some file(s) were not exported:" xml:space="preserve"> + <source>Some file(s) were not exported:</source> + <target>某些文件未导出:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>导入过程中发生了一些非致命错误——您可以查看聊天控制台了解更多详细信息。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import:" xml:space="preserve"> + <source>Some non-fatal errors occurred during import:</source> + <target>导入过程中出现一些非致命错误:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some servers failed the test: %@" xml:space="preserve"> + <source>Some servers failed the test: +%@</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> <target>某人</target> <note>notification title</note> </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve"> - <source>Start a new chat</source> - <target>开始新聊天</target> + <trans-unit id="Spam" xml:space="preserve"> + <source>Spam</source> + <note>blocking reason +report reason</note> + </trans-unit> + <trans-unit id="Square, circle, or anything in between." xml:space="preserve"> + <source>Square, circle, or anything in between.</source> + <target>方形、圆形、或两者之间的任意形状.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start chat" xml:space="preserve"> @@ -4707,11 +7370,26 @@ This is your link for group %@!</source> <target>开始聊天</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Start chat?" xml:space="preserve"> + <source>Start chat?</source> + <target>启动聊天吗?</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> <target>开始迁移</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Starting from %@." xml:space="preserve"> + <source>Starting from %@.</source> + <target>从 %@ 开始。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Statistics" xml:space="preserve"> + <source>Statistics</source> + <target>统计</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> <target>停止</target> @@ -4722,9 +7400,9 @@ This is your link for group %@!</source> <target>停止 SimpleX</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> - <source>Stop chat to enable database actions</source> - <target>停止聊天以启用数据库操作</target> + <trans-unit id="Stop chat" xml:space="preserve"> + <source>Stop chat</source> + <target>停止聊天程序</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> @@ -4755,23 +7433,60 @@ This is your link for group %@!</source> <trans-unit id="Stop sharing" xml:space="preserve"> <source>Stop sharing</source> <target>停止分享</target> - <note>No comment provided by engineer.</note> + <note>alert action</note> </trans-unit> <trans-unit id="Stop sharing address?" xml:space="preserve"> <source>Stop sharing address?</source> <target>停止分享地址?</target> + <note>alert title</note> + </trans-unit> + <trans-unit id="Stopping chat" xml:space="preserve"> + <source>Stopping chat</source> + <target>正在停止聊天</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Storage" xml:space="preserve"> + <source>Storage</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Strong" xml:space="preserve"> + <source>Strong</source> + <target>加粗</target> + <note>blur media</note> + </trans-unit> <trans-unit id="Submit" xml:space="preserve"> <source>Submit</source> <target>提交</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Subscribed" xml:space="preserve"> + <source>Subscribed</source> + <target>已订阅</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscription errors" xml:space="preserve"> + <source>Subscription errors</source> + <target>订阅错误</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Subscriptions ignored" xml:space="preserve"> + <source>Subscriptions ignored</source> + <target>忽略订阅</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Support SimpleX Chat" xml:space="preserve"> <source>Support SimpleX Chat</source> <target>支持 SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Switch audio and video during the call." xml:space="preserve"> + <source>Switch audio and video during the call.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> + <source>Switch chat profile for 1-time invitations.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="System" xml:space="preserve"> <source>System</source> <target>系统</target> @@ -4782,11 +7497,20 @@ This is your link for group %@!</source> <target>系统验证</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP connection" xml:space="preserve"> + <source>TCP connection</source> + <target>TCP 连接</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> <target>TCP 连接超时</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="TCP port for messaging" xml:space="preserve"> + <source>TCP port for messaging</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <source>TCP_KEEPCNT</source> <target>TCP_KEEPCNT</target> @@ -4802,11 +7526,19 @@ This is your link for group %@!</source> <target>TCP_KEEPINTVL</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tail" xml:space="preserve"> + <source>Tail</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> <target>拍照</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap Create SimpleX address in the menu to create it later." xml:space="preserve"> + <source>Tap Create SimpleX address in the menu to create it later.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> <target>点击按钮 </target> @@ -4814,6 +7546,7 @@ This is your link for group %@!</source> </trans-unit> <trans-unit id="Tap to Connect" xml:space="preserve"> <source>Tap to Connect</source> + <target>轻按连接</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to activate profile." xml:space="preserve"> @@ -4831,16 +7564,30 @@ This is your link for group %@!</source> <target>点击以加入隐身聊天</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve"> - <source>Tap to start a new chat</source> - <target>点击开始一个新聊天</target> + <trans-unit id="Tap to paste link" xml:space="preserve"> + <source>Tap to paste link</source> + <target>轻按粘贴链接</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Tap to scan" xml:space="preserve"> + <source>Tap to scan</source> + <target>轻按扫描</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Temporary file error" xml:space="preserve"> + <source>Temporary file error</source> + <target>临时文件错误</target> + <note>file error alert title</note> + </trans-unit> <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> <target>在步骤 %@ 上测试失败。</target> <note>server test failure</note> </trans-unit> + <trans-unit id="Test notifications" xml:space="preserve"> + <source>Test notifications</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> <target>测试服务器</target> @@ -4854,7 +7601,7 @@ This is your link for group %@!</source> <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> <target>测试失败!</target> - <note>No comment provided by engineer.</note> + <note>alert title</note> </trans-unit> <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> @@ -4871,11 +7618,6 @@ This is your link for group %@!</source> <target>感谢用户——通过 Weblate 做出贡献!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> - <source>The 1st platform without any user identifiers – private by design.</source> - <target>第一个没有任何用户标识符的平台 - 隐私设计.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <source>The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised.</source> @@ -4888,11 +7630,29 @@ It can happen because of some bug or when the connection is compromised.</source <target>该应用可以在您收到消息或联系人请求时通知您——请打开设置以启用通知。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The app protects your privacy by using different operators in each conversation." xml:space="preserve"> + <source>The app protects your privacy by using different operators in each conversation.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The app will ask to confirm downloads from unknown file servers (except .onion)." xml:space="preserve"> + <source>The app will ask to confirm downloads from unknown file servers (except .onion).</source> + <target>该应用程序将要求确认从未知文件服务器(.onion 除外)下载。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> <source>The attempt to change database passphrase was not completed.</source> <target>更改数据库密码的尝试未完成。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve"> + <source>The code you scanned is not a SimpleX link QR code.</source> + <target>您扫描的码不是 SimpleX 链接的二维码。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The connection reached the limit of undelivered messages, your contact may be offline." xml:space="preserve"> + <source>The connection reached the limit of undelivered messages, your contact may be offline.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> <target>您接受的连接将被取消!</target> @@ -4913,6 +7673,11 @@ It can happen because of some bug or when the connection is compromised.</source <target>加密正在运行,不需要新的加密协议。这可能会导致连接错误!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="The future of messaging" xml:space="preserve"> + <source>The future of messaging</source> + <target>下一代私密通讯软件</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="The hash of the previous message is different." xml:space="preserve"> <source>The hash of the previous message is different.</source> <target>上一条消息的散列不同。</target> @@ -4928,9 +7693,14 @@ It can happen because of some bug or when the connection is compromised.</source <target>该消息将对所有成员标记为已被管理员移除。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve"> - <source>The next generation of private messaging</source> - <target>下一代私密通讯软件</target> + <trans-unit id="The messages will be deleted for all members." xml:space="preserve"> + <source>The messages will be deleted for all members.</source> + <target>将删除所有成员的消息。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve"> + <source>The messages will be marked as moderated for all members.</source> + <target>对于所有成员,这些消息将被标记为已审核。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> @@ -4938,9 +7708,12 @@ It can happen because of some bug or when the connection is compromised.</source <target>旧数据库在迁移过程中没有被移除,可以删除。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> - <source>The profile is only shared with your contacts.</source> - <target>该资料仅与您的联系人共享。</target> + <trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve"> + <source>The same conditions will apply to operator **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second preset operator in the app!" xml:space="preserve"> + <source>The second preset operator in the app!</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> @@ -4958,19 +7731,26 @@ It can happen because of some bug or when the connection is compromised.</source <target>您当前聊天资料 **%@** 的新连接服务器。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Theme" xml:space="preserve"> - <source>Theme</source> + <trans-unit id="The servers for new files of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new files of your current chat profile **%@**.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve"> + <source>The text you pasted is not a SimpleX link.</source> + <target>您粘贴的文本不是 SimpleX 链接。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve"> + <source>The uploaded database archive will be permanently removed from the servers.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Themes" xml:space="preserve"> + <source>Themes</source> <target>主题</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve"> - <source>There should be at least one user profile.</source> - <target>应该至少有一个用户资料。</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> - <source>There should be at least one visible user profile.</source> - <target>应该至少有一个可见的用户资料。</target> + <trans-unit id="These conditions will also apply for: **%@**." xml:space="preserve"> + <source>These conditions will also apply for: **%@**.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> @@ -4993,13 +7773,33 @@ It can happen because of some bug or when the connection is compromised.</source <target>此操作无法撤消——早于所选的发送和接收的消息将被删除。 这可能需要几分钟时间。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> <target>此操作无法撤消——您的个人资料、联系人、消息和文件将不可撤回地丢失。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by end-to-end encryption.</source> + <target>此聊天受端到端加密保护。</target> + <note>E2EE info chat item</note> + </trans-unit> + <trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve"> + <source>This chat is protected by quantum resistant end-to-end encryption.</source> + <target>此聊天受抗量子的端到端加密保护。</target> + <note>E2EE info chat item</note> + </trans-unit> <trans-unit id="This device name" xml:space="preserve"> <source>This device name</source> + <target>此设备名称</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve"> + <source>This display name is invalid. Please choose another name.</source> + <target>显示名无效。请另选一个名称。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> @@ -5014,10 +7814,25 @@ It can happen because of some bug or when the connection is compromised.</source </trans-unit> <trans-unit id="This is your own SimpleX address!" xml:space="preserve"> <source>This is your own SimpleX address!</source> + <target>这是你自己的 SimpleX 地址!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This is your own one-time link!" xml:space="preserve"> <source>This is your own one-time link!</source> + <target>这是你自己的一次性链接!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." xml:space="preserve"> + <source>This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This link was used with another mobile device, please create a new link on the desktop." xml:space="preserve"> + <source>This link was used with another mobile device, please create a new link on the desktop.</source> + <target>此链接已在其他移动设备上使用,请在桌面上创建新链接。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This message was deleted or not received yet." xml:space="preserve"> + <source>This message was deleted or not received yet.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> @@ -5025,6 +7840,11 @@ It can happen because of some bug or when the connection is compromised.</source <target>此设置适用于您当前聊天资料 **%@** 中的消息。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Title" xml:space="preserve"> + <source>Title</source> + <target>标题</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> <source>To ask any questions and to receive updates:</source> <target>要提出任何问题并接收更新,请:</target> @@ -5037,6 +7857,7 @@ It can happen because of some bug or when the connection is compromised.</source </trans-unit> <trans-unit id="To hide unwanted messages." xml:space="preserve"> <source>To hide unwanted messages.</source> + <target>隐藏不需要的信息。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To make a new connection" xml:space="preserve"> @@ -5044,9 +7865,8 @@ It can happen because of some bug or when the connection is compromised.</source <target>建立新连接</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <target>为了保护隐私,SimpleX使用针对消息队列的标识符,而不是所有其他平台使用的用户ID,每个联系人都有独立的标识符。</target> + <trans-unit id="To protect against your link being replaced, you can compare contact security codes." xml:space="preserve"> + <source>To protect against your link being replaced, you can compare contact security codes.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -5054,6 +7874,11 @@ It can happen because of some bug or when the connection is compromised.</source <target>为了保护时区,图像/语音文件使用 UTC。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your IP address, private routing uses your SMP servers to deliver messages." xml:space="preserve"> + <source>To protect your IP address, private routing uses your SMP servers to deliver messages.</source> + <target>为了保护您的 IP 地址,私有路由使用您的 SMP 服务器来传递邮件。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> @@ -5061,6 +7886,23 @@ You will be prompted to complete authentication before this feature is enabled.< 在启用此功能之前,系统将提示您完成身份验证。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> + <target>为了保护隐私,SimpleX使用针对消息队列的标识符,而不是所有其他平台使用的用户ID,每个联系人都有独立的标识符。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To receive" xml:space="preserve"> + <source>To receive</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve"> + <source>To record speech please grant permission to use Microphone.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record video please grant permission to use Camera." xml:space="preserve"> + <source>To record video please grant permission to use Camera.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> <target>请授权使用麦克风以录制语音消息。</target> @@ -5071,26 +7913,58 @@ You will be prompted to complete authentication before this feature is enabled.< <target>要显示您的隐藏的个人资料,请在**您的聊天个人资料**页面的搜索字段中输入完整密码。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To send" xml:space="preserve"> + <source>To send</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> <source>To support instant push notifications the chat database has to be migrated.</source> <target>为了支持即时推送通知,聊天数据库必须被迁移。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="To use the servers of **%@**, accept conditions of use." xml:space="preserve"> + <source>To use the servers of **%@**, accept conditions of use.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> <target>要与您的联系人验证端到端加密,请比较(或扫描)您设备上的代码。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle chat list:" xml:space="preserve"> + <source>Toggle chat list:</source> + <target>切换聊天列表:</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> <source>Toggle incognito when connecting.</source> <target>在连接时切换隐身模式。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Token status: %@." xml:space="preserve"> + <source>Token status: %@.</source> + <note>token status</note> + </trans-unit> + <trans-unit id="Toolbar opacity" xml:space="preserve"> + <source>Toolbar opacity</source> + <target>工具栏不透明度</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Total" xml:space="preserve"> + <source>Total</source> + <target>共计</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>传输隔离</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Transport sessions" xml:space="preserve"> + <source>Transport sessions</source> + <target>传输会话</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> <target>正在尝试连接到用于从该联系人接收消息的服务器(错误:%@)。</target> @@ -5101,16 +7975,16 @@ You will be prompted to complete authentication before this feature is enabled.< <target>正在尝试连接到用于从该联系人接收消息的服务器。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Turkish interface" xml:space="preserve"> + <source>Turkish interface</source> + <target>土耳其语界面</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> <target>关闭</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve"> - <source>Turn off notifications?</source> - <target>关闭通知?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> <target>打开</target> @@ -5123,20 +7997,32 @@ You will be prompted to complete authentication before this feature is enabled.< </trans-unit> <trans-unit id="Unblock" xml:space="preserve"> <source>Unblock</source> + <target>解封</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unblock for all" xml:space="preserve"> + <source>Unblock for all</source> + <target>为所有人解封</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unblock member" xml:space="preserve"> <source>Unblock member</source> + <target>解封成员</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unblock member for all?" xml:space="preserve"> + <source>Unblock member for all?</source> + <target>为所有其他成员解封该成员?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unblock member?" xml:space="preserve"> <source>Unblock member?</source> + <target>解封成员吗?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve"> - <source>Unexpected error: %@</source> - <target>意外错误: %@</target> - <note>item status description</note> + <trans-unit id="Undelivered messages" xml:space="preserve"> + <source>Undelivered messages</source> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -5146,7 +8032,7 @@ You will be prompted to complete authentication before this feature is enabled.< <trans-unit id="Unfav." xml:space="preserve"> <source>Unfav.</source> <target>取消最喜欢</target> - <note>No comment provided by engineer.</note> + <note>swipe action</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> @@ -5183,6 +8069,11 @@ You will be prompted to complete authentication before this feature is enabled.< <target>未知错误</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Unknown servers!" xml:space="preserve"> + <source>Unknown servers!</source> + <target>未知服务器!</target> + <note>alert title</note> + </trans-unit> <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> <target>除非您使用 iOS 通话界面,否则请启用请勿打扰模式以避免打扰。</target> @@ -5197,10 +8088,12 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Unlink" xml:space="preserve"> <source>Unlink</source> + <target>取消链接</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unlink desktop?" xml:space="preserve"> <source>Unlink desktop?</source> + <target>取消链接桌面端?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unlock" xml:space="preserve"> @@ -5216,11 +8109,20 @@ To connect, please ask your contact to create another connection link and check <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> <target>取消静音</target> - <note>No comment provided by engineer.</note> + <note>notification label action</note> </trans-unit> <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> <target>未读</target> + <note>swipe action</note> + </trans-unit> + <trans-unit id="Unsupported connection link" xml:space="preserve"> + <source>Unsupported connection link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve"> + <source>Up to 100 last messages are sent to new members.</source> + <target>给新成员发送了最多 100 条历史消息。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update" xml:space="preserve"> @@ -5228,11 +8130,6 @@ To connect, please ask your contact to create another connection link and check <target>更新</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> - <source>Update .onion hosts setting?</source> - <target>更新 .onion 主机设置?</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> <target>更新数据库密码</target> @@ -5243,9 +8140,13 @@ To connect, please ask your contact to create another connection link and check <target>更新网络设置?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve"> - <source>Update transport isolation mode?</source> - <target>更新传输隔离模式?</target> + <trans-unit id="Update settings?" xml:space="preserve"> + <source>Update settings?</source> + <target>更新设置?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updated conditions" xml:space="preserve"> + <source>Updated conditions</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> @@ -5253,31 +8154,67 @@ To connect, please ask your contact to create another connection link and check <target>更新设置会将客户端重新连接到所有服务器。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> - <target>更新此设置将重新连接客户端到所有服务器。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Upgrade and open chat" xml:space="preserve"> <source>Upgrade and open chat</source> <target>升级并打开聊天</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Upload errors" xml:space="preserve"> + <source>Upload errors</source> + <target>上传错误</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload failed" xml:space="preserve"> + <source>Upload failed</source> + <target>上传失败了</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Upload file" xml:space="preserve"> <source>Upload file</source> <target>上传文件</target> <note>server test step</note> </trans-unit> + <trans-unit id="Uploaded" xml:space="preserve"> + <source>Uploaded</source> + <target>已上传</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploaded files" xml:space="preserve"> + <source>Uploaded files</source> + <target>已上传的文件</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Uploading archive" xml:space="preserve"> + <source>Uploading archive</source> + <target>正在上传存档</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use %@" xml:space="preserve"> + <source>Use %@</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> <target>使用 .onion 主机</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use SOCKS proxy" xml:space="preserve"> + <source>Use SOCKS proxy</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> <target>使用 SimpleX Chat 服务器?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use TCP port %@ when no port is specified." xml:space="preserve"> + <source>Use TCP port %@ when no port is specified.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use TCP port 443 for preset servers only." xml:space="preserve"> + <source>Use TCP port 443 for preset servers only.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> <target>使用聊天</target> @@ -5288,6 +8225,14 @@ To connect, please ask your contact to create another connection link and check <target>使用当前配置文件</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use for files" xml:space="preserve"> + <source>Use for files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for messages" xml:space="preserve"> + <source>Use for messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>用于新连接</target> @@ -5295,6 +8240,7 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Use from desktop" xml:space="preserve"> <source>Use from desktop</source> + <target>从桌面端使用</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use iOS call interface" xml:space="preserve"> @@ -5307,19 +8253,55 @@ To connect, please ask your contact to create another connection link and check <target>使用新的隐身配置文件</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use only local notifications?" xml:space="preserve"> + <source>Use only local notifications?</source> + <target>仅使用本地通知?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers when IP address is not protected." xml:space="preserve"> + <source>Use private routing with unknown servers when IP address is not protected.</source> + <target>当 IP 地址不受保护时,对未知服务器使用私有路由。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use private routing with unknown servers." xml:space="preserve"> + <source>Use private routing with unknown servers.</source> + <target>对未知服务器使用私有路由。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>使用服务器</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> - <target>用户资料</target> + <trans-unit id="Use servers" xml:space="preserve"> + <source>Use servers</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> - <target>使用 .onion 主机需要兼容的 VPN 提供商。</target> + <trans-unit id="Use short links (BETA)" xml:space="preserve"> + <source>Use short links (BETA)</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app while in the call." xml:space="preserve"> + <source>Use the app while in the call.</source> + <target>通话时使用本应用.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use the app with one hand." xml:space="preserve"> + <source>Use the app with one hand.</source> + <target>用一只手使用应用程序。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use web port" xml:space="preserve"> + <source>Use web port</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="User selection" xml:space="preserve"> + <source>User selection</source> + <target>用户选择</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Username" xml:space="preserve"> + <source>Username</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> @@ -5329,10 +8311,12 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Verify code with desktop" xml:space="preserve"> <source>Verify code with desktop</source> + <target>用桌面端验证代码</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify connection" xml:space="preserve"> <source>Verify connection</source> + <target>验证连接</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify connection security" xml:space="preserve"> @@ -5342,6 +8326,17 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Verify connections" xml:space="preserve"> <source>Verify connections</source> + <target>验证连接</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify database passphrase" xml:space="preserve"> + <source>Verify database passphrase</source> + <target>验证数据库密码短语</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify passphrase" xml:space="preserve"> + <source>Verify passphrase</source> + <target>验证密码短语</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify security code" xml:space="preserve"> @@ -5356,6 +8351,7 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Via secure quantum resistant protocol." xml:space="preserve"> <source>Via secure quantum resistant protocol.</source> + <target>通过安全的、抗量子计算机破解的协议。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Video call" xml:space="preserve"> @@ -5378,11 +8374,24 @@ To connect, please ask your contact to create another connection link and check <target>最大 1gb 的视频和文件</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View conditions" xml:space="preserve"> + <source>View conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> <target>查看安全码</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="View updated conditions" xml:space="preserve"> + <source>View updated conditions</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Visible history" xml:space="preserve"> + <source>Visible history</source> + <target>可见的历史</target> + <note>chat feature</note> + </trans-unit> <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> <target>语音消息</target> @@ -5393,11 +8402,16 @@ To connect, please ask your contact to create another connection link and check <target>语音信息在此聊天中被禁止。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve"> + <source>Voice messages are prohibited.</source> <target>语音信息在该群组中被禁用。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Voice messages not allowed" xml:space="preserve"> + <source>Voice messages not allowed</source> + <target>不允许语音消息</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> <source>Voice messages prohibited!</source> <target>语音消息禁止发送!</target> @@ -5410,6 +8424,7 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Waiting for desktop..." xml:space="preserve"> <source>Waiting for desktop...</source> + <target>正在等待桌面...</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Waiting for file" xml:space="preserve"> @@ -5427,6 +8442,21 @@ To connect, please ask your contact to create another connection link and check <target>等待视频中</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wallpaper accent" xml:space="preserve"> + <source>Wallpaper accent</source> + <target>壁纸装饰</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wallpaper background" xml:space="preserve"> + <source>Wallpaper background</source> + <target>壁纸背景</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve"> + <source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source> + <target>警告:不支持在多部设备上启动聊天,这么做会导致消息传送失败</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> <source>Warning: you may lose some data!</source> <target>警告:您可能会丢失部分数据!</target> @@ -5447,6 +8477,11 @@ To connect, please ask your contact to create another connection link and check <target>欢迎消息</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Welcome message is too long" xml:space="preserve"> + <source>Welcome message is too long</source> + <target>欢迎消息太大了</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="What's new" xml:space="preserve"> <source>What's new</source> <target>更新内容</target> @@ -5457,9 +8492,13 @@ To connect, please ask your contact to create another connection link and check <target>当可用时</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> - <source>When people request to connect, you can accept or reject it.</source> - <target>当人们请求连接时,您可以接受或拒绝它。</target> + <trans-unit id="When connecting audio and video calls." xml:space="preserve"> + <source>When connecting audio and video calls.</source> + <target>连接音频和视频通话时。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When more than one operator is enabled, none of them has metadata to learn who communicates with whom." xml:space="preserve"> + <source>When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> @@ -5467,29 +8506,74 @@ To connect, please ask your contact to create another connection link and check <target>当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="WiFi" xml:space="preserve"> + <source>WiFi</source> + <target>WiFi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Will be enabled in direct chats!" xml:space="preserve"> + <source>Will be enabled in direct chats!</source> + <target>将在私聊中启用!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wired ethernet" xml:space="preserve"> + <source>Wired ethernet</source> + <target>有线以太网</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With encrypted files and media." xml:space="preserve"> + <source>With encrypted files and media.</source> + <target>加密的文件和媒体。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> <target>带有可选的欢迎消息。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="With reduced battery usage." xml:space="preserve"> + <source>With reduced battery usage.</source> + <target>降低了电量使用。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to file servers." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to file servers.</source> + <target>如果没有 Tor 或 VPN,您的 IP 地址将对文件服务器可见。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve"> + <source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source> + <target>如果没有 Tor 或 VPN,您的 IP 地址将对以下 XFTP 中继可见:%@。</target> + <note>alert message</note> + </trans-unit> <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> <target>数据库密码错误</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Wrong key or unknown connection - most likely this connection is deleted." xml:space="preserve"> + <source>Wrong key or unknown connection - most likely this connection is deleted.</source> + <target>密钥错误或连接未知 - 很可能此连接已被删除。</target> + <note>snd error text</note> + </trans-unit> + <trans-unit id="Wrong key or unknown file chunk address - most likely file is deleted." xml:space="preserve"> + <source>Wrong key or unknown file chunk address - most likely file is deleted.</source> + <target>密钥错误或文件块地址未知 - 很可能文件已删除。</target> + <note>file error text</note> + </trans-unit> <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> <target>密码错误!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="XFTP servers" xml:space="preserve"> - <source>XFTP servers</source> + <trans-unit id="XFTP server" xml:space="preserve"> + <source>XFTP server</source> <target>XFTP 服务器</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> - <target>您</target> + <trans-unit id="You **must not** use the same database on two devices." xml:space="preserve"> + <source>You **must not** use the same database on two devices.</source> + <target>您 **不得** 在两台设备上使用相同的数据库。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You accepted connection" xml:space="preserve"> @@ -5512,33 +8596,45 @@ To connect, please ask your contact to create another connection link and check <target>您已经连接到 %@。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are already connected with %@." xml:space="preserve"> + <source>You are already connected with %@.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You are already connecting to %@." xml:space="preserve"> <source>You are already connecting to %@.</source> + <target>您已连接到 %@。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already connecting via this one-time link!" xml:space="preserve"> <source>You are already connecting via this one-time link!</source> + <target>你已经在通过这个一次性链接进行连接!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already in group %@." xml:space="preserve"> <source>You are already in group %@.</source> + <target>您已在组 %@ 中。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already joining the group %@." xml:space="preserve"> <source>You are already joining the group %@.</source> + <target>您已加入组 %@。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already joining the group via this link!" xml:space="preserve"> <source>You are already joining the group via this link!</source> + <target>您已经通过此链接加入群组!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already joining the group via this link." xml:space="preserve"> <source>You are already joining the group via this link.</source> + <target>你已经在通过此链接加入该群。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already joining the group! Repeat join request?" xml:space="preserve"> <source>You are already joining the group! Repeat join request?</source> + <target>您已经加入了这个群组! +重复加入请求?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve"> @@ -5551,14 +8647,23 @@ Repeat join request?</source> <target>您被邀请加入群组</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You are not connected to these servers. Private routing is used to deliver messages to them." xml:space="preserve"> + <source>You are not connected to these servers. Private routing is used to deliver messages to them.</source> + <target>您未连接到这些服务器。私有路由用于向他们发送消息。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> <source>You can accept calls from lock screen, without device and app authentication.</source> <target>您可以从锁屏上接听电话,无需设备和应用程序的认证。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <target>您也可以通过点击链接进行连接。如果在浏览器中打开,请点击“在移动应用程序中打开”按钮。</target> + <trans-unit id="You can change it in Appearance settings." xml:space="preserve"> + <source>You can change it in Appearance settings.</source> + <target>您可以在外观设置中更改它。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can configure servers via settings." xml:space="preserve"> + <source>You can configure servers via settings.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can create it later" xml:space="preserve"> @@ -5576,16 +8681,35 @@ Repeat join request?</source> <target>您可以稍后通过应用程序的 "隐私与安全 "设置启用它们。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can give another try." xml:space="preserve"> + <source>You can give another try.</source> + <target>你可以再试一次。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <source>You can hide or mute a user profile - swipe it to the right.</source> <target>您可以隐藏或静音用户个人资料——只需向右滑动。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> + <trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve"> + <source>You can make it visible to your SimpleX contacts via Settings.</source> + <target>你可以通过设置让它对你的 SimpleX 联系人可见。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now chat with %@" xml:space="preserve"> + <source>You can now chat with %@</source> <target>您现在可以给 %@ 发送消息</target> <note>notification body</note> </trans-unit> + <trans-unit id="You can send messages to %@ from Archived contacts." xml:space="preserve"> + <source>You can send messages to %@ from Archived contacts.</source> + <target>您可以从存档的联系人向%@发送消息。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can set connection name, to remember who the link was shared with." xml:space="preserve"> + <source>You can set connection name, to remember who the link was shared with.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> <target>您可以通过设置来设置锁屏通知预览。</target> @@ -5601,16 +8725,16 @@ Repeat join request?</source> <target>您可以与您的联系人分享该地址,让他们与 **%@** 联系。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> - <source>You can share your address as a link or QR code - anybody can connect to you.</source> - <target>您可以将您的地址作为链接或二维码共享——任何人都可以连接到您。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> <target>您可以通过应用程序设置/数据库或重新启动应用程序开始聊天</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve"> + <source>You can still view conversation with %@ in the list of chats.</source> + <target>您仍然可以在聊天列表中查看与 %@的对话。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> <target>您可以通过设置开启 SimpleX 锁定。</target> @@ -5621,33 +8745,36 @@ Repeat join request?</source> <target>您可以使用 markdown 来编排消息格式:</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You can view invitation link again in connection details." xml:space="preserve"> + <source>You can view invitation link again in connection details.</source> + <target>您可以在连接详情中再次查看邀请链接。</target> + <note>alert message</note> + </trans-unit> <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> <target>您无法发送消息!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <target>您可以控制接收信息使用的服务器,您的联系人则使用您发送信息时所使用的服务器。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> <target>您的身份无法验证,请再试一次。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You decide who can connect." xml:space="preserve"> + <source>You decide who can connect.</source> + <target>你决定谁可以连接。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You have already requested connection via this address!" xml:space="preserve"> <source>You have already requested connection via this address!</source> + <target>你已经请求通过此地址进行连接!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You have already requested connection! Repeat connection request?" xml:space="preserve"> <source>You have already requested connection! Repeat connection request?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> - <target>您没有聊天记录</target> + <target>您已经请求连接了! +重复连接请求?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> @@ -5670,11 +8797,26 @@ Repeat connection request?</source> <target>你加入了这个群组。连接到邀请组成员。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You may migrate the exported database." xml:space="preserve"> + <source>You may migrate the exported database.</source> + <target>您可以迁移导出的数据库。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You may save the exported archive." xml:space="preserve"> + <source>You may save the exported archive.</source> + <target>您可以保存导出的档案。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> <target>您只能在一台设备上使用最新版本的聊天数据库,否则您可能会停止接收来自某些联系人的消息。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You need to allow your contact to call to be able to call them." xml:space="preserve"> + <source>You need to allow your contact to call to be able to call them.</source> + <target>您需要允许您的联系人呼叫才能呼叫他们。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> <target>您需要允许您的联系人发送语音消息,以便您能够发送语音消息。</target> @@ -5690,6 +8832,10 @@ Repeat connection request?</source> <target>您发送了群组邀请</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You should receive notifications." xml:space="preserve"> + <source>You should receive notifications.</source> + <note>token info</note> + </trans-unit> <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> <target>您将在组主设备上线时连接到该群组,请稍等或稍后再检查!</target> @@ -5697,6 +8843,7 @@ Repeat connection request?</source> </trans-unit> <trans-unit id="You will be connected when group link host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected when group link host's device is online, please wait or check later!</source> + <target>当 Group Link Host 的设备在线时,您将被连接,请稍候或稍后检查!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve"> @@ -5716,6 +8863,7 @@ Repeat connection request?</source> </trans-unit> <trans-unit id="You will connect to all group members." xml:space="preserve"> <source>You will connect to all group members.</source> + <target>你将连接到所有群成员。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve"> @@ -5723,6 +8871,10 @@ Repeat connection request?</source> <target>当静音配置文件处于活动状态时,您仍会收到来自静音配置文件的电话和通知。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="You will stop receiving messages from this chat. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this chat. Chat history will be preserved.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> <target>您将停止接收来自该群组的消息。聊天记录将被保留。</target> @@ -5743,31 +8895,16 @@ Repeat connection request?</source> <target>您正在为该群组使用隐身个人资料——为防止共享您的主要个人资料,不允许邀请联系人</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your %@ servers" xml:space="preserve"> - <source>Your %@ servers</source> - <target>您的 %@ 服务器</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your ICE servers" xml:space="preserve"> <source>Your ICE servers</source> <target>您的 ICE 服务器</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> - <target>您的 SMP 服务器</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your SimpleX address" xml:space="preserve"> <source>Your SimpleX address</source> <target>您的 SimpleX 地址</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your XFTP servers" xml:space="preserve"> - <source>Your XFTP servers</source> - <target>您的 XFTP 服务器</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your calls" xml:space="preserve"> <source>Your calls</source> <target>您的通话</target> @@ -5783,16 +8920,17 @@ Repeat connection request?</source> <target>您的聊天数据库未加密——设置密码来加密。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your chat preferences" xml:space="preserve"> + <source>Your chat preferences</source> + <note>alert title</note> + </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>您的聊天资料</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> - <target>您的联系人需要在线才能完成连接。 -您可以取消此连接并删除联系人(然后尝试使用新链接)。</target> + <trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve"> + <source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> @@ -5805,18 +8943,15 @@ You can cancel this connection and remove the contact (and try later with a new <target>您的联系人可以允许完全删除消息。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> - <source>Your contacts in SimpleX will see it. -You can change it in Settings.</source> - <target>您的 SimpleX 的联系人会看到它。 -您可以在设置中更改它。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your contacts will remain connected." xml:space="preserve"> <source>Your contacts will remain connected.</source> <target>与您的联系人保持连接。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve"> + <source>Your credentials may be sent unencrypted.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> <target>您当前的聊天数据库将被删除并替换为导入的数据库。</target> @@ -5839,6 +8974,7 @@ You can change it in Settings.</source> </trans-unit> <trans-unit id="Your profile" xml:space="preserve"> <source>Your profile</source> + <target>您的个人资料</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> @@ -5846,33 +8982,34 @@ You can change it in Settings.</source> <target>您的个人资料 **%@** 将被共享。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> - <target>您的资料存储在您的设备上并仅与您的联系人共享。 -SimpleX 服务器无法看到您的资料。</target> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve"> + <source>Your profile is stored on your device and only shared with your contacts.</source> + <target>该资料仅与您的联系人共享。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> - <target>您的资料、联系人和发送的消息存储在您的设备上。</target> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> + <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> + <target>您的资料存储在您的设备上并仅与您的联系人共享。 SimpleX 服务器无法看到您的资料。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve"> + <source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source> + <note>alert message</note> + </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> <target>您的随机资料</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> - <target>您的服务器</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> <target>您的服务器地址</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your servers" xml:space="preserve"> + <source>Your servers</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your settings" xml:space="preserve"> <source>Your settings</source> <target>您的设置</target> @@ -5913,11 +9050,20 @@ SimpleX 服务器无法看到您的资料。</target> <target>已接受通话</target> <note>call status</note> </trans-unit> + <trans-unit id="accepted invitation" xml:space="preserve"> + <source>accepted invitation</source> + <note>chat list item title</note> + </trans-unit> <trans-unit id="admin" xml:space="preserve"> <source>admin</source> <target>管理员</target> <note>member role</note> </trans-unit> + <trans-unit id="admins" xml:space="preserve"> + <source>admins</source> + <target>管理员</target> + <note>feature role</note> + </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> <target>正在协商将加密应用于 %@…</target> @@ -5928,6 +9074,11 @@ SimpleX 服务器无法看到您的资料。</target> <target>同意加密…</target> <note>chat item text</note> </trans-unit> + <trans-unit id="all members" xml:space="preserve"> + <source>all members</source> + <target>所有成员</target> + <note>feature role</note> + </trans-unit> <trans-unit id="always" xml:space="preserve"> <source>always</source> <target>始终</target> @@ -5935,6 +9086,16 @@ SimpleX 服务器无法看到您的资料。</target> </trans-unit> <trans-unit id="and %lld other events" xml:space="preserve"> <source>and %lld other events</source> + <target>和 %lld 其他事件</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="archived report" xml:space="preserve"> + <source>archived report</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="attempts" xml:space="preserve"> + <source>attempts</source> + <target>尝试</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> @@ -5944,6 +9105,7 @@ SimpleX 服务器无法看到您的资料。</target> </trans-unit> <trans-unit id="author" xml:space="preserve"> <source>author</source> + <target>作者</target> <note>member role</note> </trans-unit> <trans-unit id="bad message ID" xml:space="preserve"> @@ -5958,13 +9120,30 @@ SimpleX 服务器无法看到您的资料。</target> </trans-unit> <trans-unit id="blocked" xml:space="preserve"> <source>blocked</source> - <note>No comment provided by engineer.</note> + <target>已封禁</target> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="blocked %@" xml:space="preserve"> + <source>blocked %@</source> + <target>已封禁 %@</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="blocked by admin" xml:space="preserve"> + <source>blocked by admin</source> + <target>由管理员封禁</target> + <note>blocked chat item +marked deleted chat item preview text</note> </trans-unit> <trans-unit id="bold" xml:space="preserve"> <source>bold</source> <target>加粗</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="call" xml:space="preserve"> + <source>call</source> + <target>呼叫</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="call error" xml:space="preserve"> <source>call error</source> <target>通话错误</target> @@ -6068,7 +9247,7 @@ SimpleX 服务器无法看到您的资料。</target> <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> <target>连接中……</target> - <note>chat list item title</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> @@ -6080,6 +9259,11 @@ SimpleX 服务器无法看到您的资料。</target> <target>连接:%@</target> <note>connection information</note> </trans-unit> + <trans-unit id="contact %@ changed to %@" xml:space="preserve"> + <source>contact %1$@ changed to %2$@</source> + <target>联系人 %1$@ 已更改为 %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> <target>联系人具有端到端加密</target> @@ -6110,10 +9294,16 @@ SimpleX 服务器无法看到您的资料。</target> <target>天</target> <note>time unit</note> </trans-unit> + <trans-unit id="decryption errors" xml:space="preserve"> + <source>decryption errors</source> + <target>解密错误</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> <target>默认 (%@)</target> - <note>pref value</note> + <note>delete after time +pref value</note> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> @@ -6132,6 +9322,7 @@ SimpleX 服务器无法看到您的资料。</target> </trans-unit> <trans-unit id="deleted contact" xml:space="preserve"> <source>deleted contact</source> + <target>已删除联系人</target> <note>rcv direct event chat item</note> </trans-unit> <trans-unit id="deleted group" xml:space="preserve"> @@ -6159,6 +9350,11 @@ SimpleX 服务器无法看到您的资料。</target> <target>重复的消息</target> <note>integrity error chat item</note> </trans-unit> + <trans-unit id="duplicates" xml:space="preserve"> + <source>duplicates</source> + <target>副本</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> <target>端到端加密</target> @@ -6234,9 +9430,14 @@ SimpleX 服务器无法看到您的资料。</target> <target>错误</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="event happened" xml:space="preserve"> - <source>event happened</source> - <target>发生的事</target> + <trans-unit id="expired" xml:space="preserve"> + <source>expired</source> + <target>过期</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="forwarded" xml:space="preserve"> + <source>forwarded</source> + <target>已转发</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> @@ -6264,6 +9465,11 @@ SimpleX 服务器无法看到您的资料。</target> <target>在您重启应用或改变密码后,iOS钥匙串将被用来安全地存储密码——它将允许接收推送通知。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="inactive" xml:space="preserve"> + <source>inactive</source> + <target>无效</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> <target>通过联系人地址链接隐身聊天</target> @@ -6304,6 +9510,11 @@ SimpleX 服务器无法看到您的资料。</target> <target>邀请您加入群组 %@</target> <note>group name</note> </trans-unit> + <trans-unit id="invite" xml:space="preserve"> + <source>invite</source> + <target>邀请</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> <target>已邀请</target> @@ -6349,11 +9560,21 @@ SimpleX 服务器无法看到您的资料。</target> <target>成员</target> <note>member role</note> </trans-unit> + <trans-unit id="member %@ changed to %@" xml:space="preserve"> + <source>member %1$@ changed to %2$@</source> + <target>成员 %1$@ 已更改为 %2$@</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> <target>已连接</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="message" xml:space="preserve"> + <source>message</source> + <target>消息</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="message received" xml:space="preserve"> <source>message received</source> <target>消息已收到</target> @@ -6377,7 +9598,11 @@ SimpleX 服务器无法看到您的资料。</target> <trans-unit id="moderated by %@" xml:space="preserve"> <source>moderated by %@</source> <target>由 %@ 审核</target> - <note>No comment provided by engineer.</note> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="moderator" xml:space="preserve"> + <source>moderator</source> + <note>member role</note> </trans-unit> <trans-unit id="months" xml:space="preserve"> <source>months</source> @@ -6387,7 +9612,7 @@ SimpleX 服务器无法看到您的资料。</target> <trans-unit id="never" xml:space="preserve"> <source>never</source> <target>从不</target> - <note>No comment provided by engineer.</note> + <note>delete after time</note> </trans-unit> <trans-unit id="new message" xml:space="preserve"> <source>new message</source> @@ -6418,8 +9643,8 @@ SimpleX 服务器无法看到您的资料。</target> <source>off</source> <target>关闭</target> <note>enabled status - group pref value - time to disappear</note> +group pref value +time to disappear</note> </trans-unit> <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> @@ -6436,16 +9661,44 @@ SimpleX 服务器无法看到您的资料。</target> <target>开启</target> <note>group pref value</note> </trans-unit> + <trans-unit id="other" xml:space="preserve"> + <source>other</source> + <target>其他</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="other errors" xml:space="preserve"> + <source>other errors</source> + <target>其他错误</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="owner" xml:space="preserve"> <source>owner</source> <target>群主</target> <note>member role</note> </trans-unit> + <trans-unit id="owners" xml:space="preserve"> + <source>owners</source> + <target>所有者</target> + <note>feature role</note> + </trans-unit> <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> <target>点对点</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="pending" xml:space="preserve"> + <source>pending</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="pending approval" xml:space="preserve"> + <source>pending approval</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="quantum resistant e2e encryption" xml:space="preserve"> + <source>quantum resistant e2e encryption</source> + <target>抗量子端到端加密</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> <target>已收到回复……</target> @@ -6456,6 +9709,10 @@ SimpleX 服务器无法看到您的资料。</target> <target>已受到确认……</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="rejected" xml:space="preserve"> + <source>rejected</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> <target>拒接来电</target> @@ -6471,11 +9728,40 @@ SimpleX 服务器无法看到您的资料。</target> <target>已删除 %@</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="removed contact address" xml:space="preserve"> + <source>removed contact address</source> + <target>删除了联系地址</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="removed profile picture" xml:space="preserve"> + <source>removed profile picture</source> + <target>删除了资料图片</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> <target>已将您移除</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="requested to connect" xml:space="preserve"> + <source>requested to connect</source> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="saved" xml:space="preserve"> + <source>saved</source> + <target>已保存</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="saved from %@" xml:space="preserve"> + <source>saved from %@</source> + <target>保存自 %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="search" xml:space="preserve"> + <source>search</source> + <target>搜索</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="sec" xml:space="preserve"> <source>sec</source> <target>秒</target> @@ -6501,6 +9787,30 @@ SimpleX 服务器无法看到您的资料。</target> <target>发送私信</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="server queue info: %@ last received msg: %@" xml:space="preserve"> + <source>server queue info: %1$@ + +last received msg: %2$@</source> + <target>服务器队列信息: %1$@ + +上次收到的消息: %2$@</target> + <note>queue info</note> + </trans-unit> + <trans-unit id="set new contact address" xml:space="preserve"> + <source>set new contact address</source> + <target>设置新的联系地址</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="set new profile picture" xml:space="preserve"> + <source>set new profile picture</source> + <target>设置新的资料图片</target> + <note>profile update event chat item</note> + </trans-unit> + <trans-unit id="standard end-to-end encryption" xml:space="preserve"> + <source>standard end-to-end encryption</source> + <target>标准端到端加密</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>启动中……</target> @@ -6516,18 +9826,44 @@ SimpleX 服务器无法看到您的资料。</target> <target>这个联系人</target> <note>notification title</note> </trans-unit> + <trans-unit id="unblocked %@" xml:space="preserve"> + <source>unblocked %@</source> + <target>未阻止 %@</target> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="unknown" xml:space="preserve"> <source>unknown</source> <target>未知</target> <note>connection info</note> </trans-unit> + <trans-unit id="unknown servers" xml:space="preserve"> + <source>unknown servers</source> + <target>未知服务器</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unknown status" xml:space="preserve"> + <source>unknown status</source> + <target>未知状态</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="unprotected" xml:space="preserve"> + <source>unprotected</source> + <target>未受保护</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="updated group profile" xml:space="preserve"> <source>updated group profile</source> <target>已更新的群组资料</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="updated profile" xml:space="preserve"> + <source>updated profile</source> + <target>更新了资料</target> + <note>profile update event chat item</note> + </trans-unit> <trans-unit id="v%@" xml:space="preserve"> <source>v%@</source> + <target>v%@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="v%@ (%@)" xml:space="preserve"> @@ -6555,6 +9891,11 @@ SimpleX 服务器无法看到您的资料。</target> <target>通过中继</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="video" xml:space="preserve"> + <source>video</source> + <target>视频</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> <source>video call (not e2e encrypted)</source> <target>视频通话(非端到端加密)</target> @@ -6580,11 +9921,21 @@ SimpleX 服务器无法看到您的资料。</target> <target>周</target> <note>time unit</note> </trans-unit> + <trans-unit id="when IP hidden" xml:space="preserve"> + <source>when IP hidden</source> + <target>当 IP 隐藏时</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="yes" xml:space="preserve"> <source>yes</source> <target>是</target> <note>pref value</note> </trans-unit> + <trans-unit id="you" xml:space="preserve"> + <source>you</source> + <target>您</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> <target>您被邀请加入群组</target> @@ -6595,6 +9946,11 @@ SimpleX 服务器无法看到您的资料。</target> <target>您是观察者</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="you blocked %@" xml:space="preserve"> + <source>you blocked %@</source> + <target>你阻止了%@</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> <target>您已更改地址</target> @@ -6635,6 +9991,11 @@ SimpleX 服务器无法看到您的资料。</target> <target>您分享了一次性链接隐身聊天</target> <note>chat list item description</note> </trans-unit> + <trans-unit id="you unblocked %@" xml:space="preserve"> + <source>you unblocked %@</source> + <target>您解封了 %@</target> + <note>snd group event chat item</note> + </trans-unit> <trans-unit id="you: " xml:space="preserve"> <source>you: </source> <target>您: </target> @@ -6649,7 +10010,7 @@ SimpleX 服务器无法看到您的资料。</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="zh-Hans" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6669,6 +10030,7 @@ SimpleX 服务器无法看到您的资料。</target> </trans-unit> <trans-unit id="NSLocalNetworkUsageDescription" xml:space="preserve"> <source>SimpleX uses local network access to allow using user chat profile via desktop app on the same network.</source> + <target>SimpleX 使用本地网络访问,允许通过同一网络上的桌面应用程序使用用户聊天配置文件。</target> <note>Privacy - Local Network Usage Description</note> </trans-unit> <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> @@ -6685,7 +10047,7 @@ SimpleX 服务器无法看到您的资料。</target> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="zh-Hans" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> @@ -6705,4 +10067,245 @@ SimpleX 服务器无法看到您的资料。</target> </trans-unit> </body> </file> + <file original="SimpleX NSE/en.lproj/Localizable.strings" source-language="en" target-language="zh-Hans" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%d new events" xml:space="preserve"> + <source>%d new events</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From %d chat(s)" xml:space="preserve"> + <source>From %d chat(s)</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="From: %@" xml:space="preserve"> + <source>From: %@</source> + <note>notification body</note> + </trans-unit> + <trans-unit id="New events" xml:space="preserve"> + <source>New events</source> + <note>notification</note> + </trans-unit> + <trans-unit id="New messages" xml:space="preserve"> + <source>New messages</source> + <note>notification</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/InfoPlist.strings" source-language="en" target-language="zh-Hans" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle display name</note> + </trans-unit> + <trans-unit id="CFBundleName" xml:space="preserve"> + <source>SimpleX SE</source> + <target>SimpleX SE</target> + <note>Bundle name</note> + </trans-unit> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> + <source>Copyright © 2024 SimpleX Chat. All rights reserved.</source> + <target>版权所有 © 2024 SimpleX Chat。保留所有权利。</target> + <note>Copyright (human-readable)</note> + </trans-unit> + </body> + </file> + <file original="SimpleX SE/en.lproj/Localizable.strings" source-language="en" target-language="zh-Hans" datatype="plaintext"> + <header> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/> + </header> + <body> + <trans-unit id="%@" xml:space="preserve"> + <source>%@</source> + <target>%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App is locked!" xml:space="preserve"> + <source>App is locked!</source> + <target>应用程序已锁定!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <target>取消</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <target>无法访问钥匙串以保存数据库密码</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot forward message" xml:space="preserve"> + <source>Cannot forward message</source> + <target>无法转发消息</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Comment" xml:space="preserve"> + <source>Comment</source> + <target>评论</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <target>当前支持的最大文件大小为 %@。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade required" xml:space="preserve"> + <source>Database downgrade required</source> + <target>需要数据库降级</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <target>数据库已加密!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <target>数据库错误</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <target>数据库密码与保存在钥匙串中的密码不同。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <target>需要数据库密码才能打开聊天。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade required" xml:space="preserve"> + <source>Database upgrade required</source> + <target>需要升级数据库</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing file" xml:space="preserve"> + <source>Error preparing file</source> + <target>准备文件时出错</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error preparing message" xml:space="preserve"> + <source>Error preparing message</source> + <target>准备消息时出错</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <target>错误:%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File error" xml:space="preserve"> + <source>File error</source> + <target>文件错误</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <target>不兼容的数据库版本</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid migration confirmation" xml:space="preserve"> + <source>Invalid migration confirmation</source> + <target>无效的迁移确认</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <target>钥匙串错误</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <target>大文件!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No active profile" xml:space="preserve"> + <source>No active profile</source> + <target>无活动配置文件</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <target>好的</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to downgrade the database." xml:space="preserve"> + <source>Open the app to downgrade the database.</source> + <target>打开应用程序以降级数据库。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open the app to upgrade the database." xml:space="preserve"> + <source>Open the app to upgrade the database.</source> + <target>打开应用程序以升级数据库。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passphrase" xml:space="preserve"> + <source>Passphrase</source> + <target>密码</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please create a profile in the SimpleX app" xml:space="preserve"> + <source>Please create a profile in the SimpleX app</source> + <target>请在 SimpleX 应用程序中创建配置文件</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Selected chat preferences prohibit this message." xml:space="preserve"> + <source>Selected chat preferences prohibit this message.</source> + <target>选定的聊天首选项禁止此消息。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending a message takes longer than expected." xml:space="preserve"> + <source>Sending a message takes longer than expected.</source> + <target>发送消息所需的时间比预期的要长。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending message…" xml:space="preserve"> + <source>Sending message…</source> + <target>正在发送消息…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <target>共享</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Slow network?" xml:space="preserve"> + <source>Slow network?</source> + <target>网络速度慢?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <target>未知数据库错误: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unsupported format" xml:space="preserve"> + <source>Unsupported format</source> + <target>不支持的格式</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wait" xml:space="preserve"> + <source>Wait</source> + <target>等待</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <target>数据库密码错误</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can allow sharing in Privacy & Security / SimpleX Lock settings." xml:space="preserve"> + <source>You can allow sharing in Privacy & Security / SimpleX Lock settings.</source> + <target>您可以在 "隐私与安全"/"SimpleX Lock "设置中允许共享。</target> + <note>No comment provided by engineer.</note> + </trans-unit> + </body> + </file> </xliff> diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/contents.json b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/contents.json index 807a15f96c..91977b0744 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "zh-Hans", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "16C5032a", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "16.2" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff index 821db2620f..3ea46ee364 100644 --- a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff @@ -109,7 +109,7 @@ </trans-unit> <trans-unit id="%d skipped message(s)" xml:space="preserve" approved="no"> <source>%d skipped message(s)</source> - <target state="translated">%d錯過了訊息</target> + <target state="translated">錯過的 %d 則訊息</target> <note>integrity error chat item</note> </trans-unit> <trans-unit id="%lld" xml:space="preserve" approved="no"> @@ -124,17 +124,17 @@ </trans-unit> <trans-unit id="%lld contact(s) selected" xml:space="preserve" approved="no"> <source>%lld contact(s) selected</source> - <target state="translated">%lld 已選擇聯絡人(s)</target> + <target state="translated">已選擇 %lld 個聯絡人</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve" approved="no"> <source>%lld file(s) with total size of %@</source> - <target state="translated">%lld 檔案(s) 的總共大小為%@</target> + <target state="translated">%lld 個檔案,總共大小 %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld members" xml:space="preserve" approved="no"> <source>%lld members</source> - <target state="translated">%lld 成員</target> + <target state="translated">%lld 個成員</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld second(s)" xml:space="preserve" approved="no"> @@ -187,23 +187,18 @@ <target state="translated">)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target state="translated">**新增新的聯絡人**:建立一次性二維碼或連結連接聯絡人。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no"> <source>**Create link / QR code** for your contact to use.</source> <target state="translated">**建立連結 / 二維碼** 讓你的聯絡人使用。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve" approved="no"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> <target state="translated">**更有私隱**:每20分鐘會檢查一次訊息。裝置權杖與 SimpleX Chat 伺服器分享中,但是不包括你的聯絡人和訊息資料。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve" approved="no"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> <target state="translated">**最有私隱**:不使用 SimpleX Chat 通知服務器,在後台定期檢查訊息(取決於你使用應用程序的頻率)。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -217,8 +212,8 @@ <target state="translated">**請注意**:如果你忘記了密碼你將不能再次復原或更改密碼。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve" approved="no"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <trans-unit id="**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." xml:space="preserve" approved="no"> + <source>**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.</source> <target state="translated">**建議**:裝置權杖和通知都會傳送去 SimpeleX Chat 的通知伺服器,但是不包括訊息內容、大小或傳送者資料。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -229,7 +224,7 @@ </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve" approved="no"> <source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source> - <target state="translated">**警告**:即時推送訊息通知需要數據庫的密碼儲存在資料庫中。</target> + <target state="translated">**警告**:即時推送訊息通知需要將數據庫的密碼儲存在資料庫中。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**e2e encrypted** audio call" xml:space="preserve" approved="no"> @@ -314,7 +309,7 @@ </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve" approved="no"> <source>About SimpleX Chat</source> - <target state="translated">關於 SimpleX 對話</target> + <target state="translated">關於 SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accent color" xml:space="preserve" approved="no"> @@ -358,9 +353,9 @@ <target state="translated">使用二維碼掃描以新增伺服器。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve" approved="no"> - <source>Add server…</source> - <target state="translated">新增伺服器…</target> + <trans-unit id="Add server" xml:space="preserve" approved="no"> + <source>Add server</source> + <target state="translated">新增伺服器</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve" approved="no"> @@ -445,7 +440,7 @@ </trans-unit> <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve" approved="no"> <source>Allow your contacts to send disappearing messages.</source> - <target state="translated">允許你的聯絡人傳送自動銷毀的訊息。</target> + <target state="translated">允許您的聯絡人傳送限時訊息。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send voice messages." xml:space="preserve" approved="no"> @@ -1178,8 +1173,8 @@ <target state="translated">私訊</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve" approved="no"> - <source>Direct messages between members are prohibited in this group.</source> + <trans-unit id="Direct messages between members are prohibited." xml:space="preserve" approved="no"> + <source>Direct messages between members are prohibited.</source> <target state="translated">私訊群組內的成員於這個群組內是禁用的。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1198,8 +1193,8 @@ <target state="translated">自動銷毀訊息已被禁止於此聊天室。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve" approved="no"> - <source>Disappearing messages are prohibited in this group.</source> + <trans-unit id="Disappearing messages are prohibited." xml:space="preserve" approved="no"> + <source>Disappearing messages are prohibited.</source> <target state="translated">自動銷毀訊息於這個群組內是禁用的。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1623,18 +1618,18 @@ <target state="translated">群組內的成員可以不可逆地刪除訊息。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve" approved="no"> - <source>Group members can send direct messages.</source> + <trans-unit id="Members can send direct messages." xml:space="preserve" approved="no"> + <source>Members can send direct messages.</source> <target state="translated">群組內的成員可以私訊群組內的成員。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve" approved="no"> - <source>Group members can send disappearing messages.</source> + <trans-unit id="Members can send disappearing messages." xml:space="preserve" approved="no"> + <source>Members can send disappearing messages.</source> <target state="translated">群組內的成員可以傳送自動銷毀的訊息。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve" approved="no"> - <source>Group members can send voice messages.</source> + <trans-unit id="Members can send voice messages." xml:space="preserve" approved="no"> + <source>Members can send voice messages.</source> <target state="translated">群組內的成員可以傳送語音訊息。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1747,8 +1742,8 @@ <target state="translated">下載圖片需要傳送者上線的時候才能下載圖片,請等待對方上線!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve" approved="no"> - <source>Immune to spam and abuse</source> + <trans-unit id="Immune to spam" xml:space="preserve" approved="no"> + <source>Immune to spam</source> <target state="translated">不受垃圾郵件和濫用行為影響</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1869,8 +1864,8 @@ <target state="translated">不可逆地刪除訊息於這個聊天室內是禁用的。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve" approved="no"> - <source>Irreversible message deletion is prohibited in this group.</source> + <trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve" approved="no"> + <source>Irreversible message deletion is prohibited.</source> <target state="translated">不可逆地刪除訊息於這個群組內是禁用的。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2217,8 +2212,8 @@ We will be adding server redundancy to prevent lost messages.</source> <target state="translated">Onion 主機不會啟用。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve" approved="no"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve" approved="no"> + <source>Only client devices store user profiles, contacts, groups, and messages.</source> <target state="translated">只有客戶端裝置才會儲存你的個人檔案、聯絡人,群組,所有訊息都會經過**兩層的端對端加密**。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2277,8 +2272,8 @@ We will be adding server redundancy to prevent lost messages.</source> <target state="translated">使用終端機開啟對話</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve" approved="no"> - <source>Open-source protocol and code – anybody can run the servers.</source> + <trans-unit id="Anybody can host servers." xml:space="preserve" approved="no"> + <source>Anybody can host servers.</source> <target state="translated">開放源碼協議和程式碼 – 任何人也可以運行伺服器。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2317,8 +2312,8 @@ We will be adding server redundancy to prevent lost messages.</source> <target state="translated">將你接收到的連結貼上至下面的框內,以開始你與你的聯絡人對話。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve" approved="no"> - <source>People can connect to you only via the links you share.</source> + <trans-unit id="You decide who can connect." xml:space="preserve" approved="no"> + <source>You decide who can connect.</source> <target state="translated">人們只能在你分享了連結後,才能和你連接。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2709,12 +2704,12 @@ We will be adding server redundancy to prevent lost messages.</source> </trans-unit> <trans-unit id="Send link previews" xml:space="preserve" approved="no"> <source>Send link previews</source> - <target state="translated">傳送可以預覽的連結</target> + <target state="translated">傳送連結預覽</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send live message" xml:space="preserve" approved="no"> <source>Send live message</source> - <target state="translated">傳送實況的訊息</target> + <target state="translated">傳送實時訊息</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send notifications" xml:space="preserve" approved="no"> @@ -2729,7 +2724,7 @@ We will be adding server redundancy to prevent lost messages.</source> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve" approved="no"> <source>Send questions and ideas</source> - <target state="translated">傳送問題和想法給開發者</target> + <target state="translated">給開發者提問題和想法</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve" approved="no"> @@ -2779,7 +2774,7 @@ We will be adding server redundancy to prevent lost messages.</source> </trans-unit> <trans-unit id="Set 1 day" xml:space="preserve" approved="no"> <source>Set 1 day</source> - <target state="translated">設定為1天</target> + <target state="translated">設定為 1 天</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve" approved="no"> @@ -3010,8 +3005,8 @@ We will be adding server redundancy to prevent lost messages.</source> <target state="translated">感謝你安裝SimpleX Chat!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve" approved="no"> - <source>The 1st platform without any user identifiers – private by design.</source> + <trans-unit id="No user identifiers." xml:space="preserve" approved="no"> + <source>No user identifiers.</source> <target state="translated">第一個沒有任何用戶識別符的通訊平台 – 以私隱為設計。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3027,7 +3022,7 @@ We will be adding server redundancy to prevent lost messages.</source> </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve" approved="no"> <source>The connection you accepted will be cancelled!</source> - <target state="translated">你所接受的連接將被取消!</target> + <target state="translated">你接受的連接將被取消!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The contact you shared this link with will NOT be able to connect!" xml:space="preserve" approved="no"> @@ -3049,8 +3044,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>The microphone does not work when the app is in the background.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve" approved="no"> - <source>The next generation of private messaging</source> + <trans-unit id="The future of messaging" xml:space="preserve" approved="no"> + <source>The future of messaging</source> <target state="translated">新一代的私密訊息平台</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3059,14 +3054,14 @@ We will be adding server redundancy to prevent lost messages.</source> <target state="translated">舊的數據庫在遷移過程中沒有被移除,可以刪除。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve" approved="no"> - <source>The profile is only shared with your contacts.</source> + <trans-unit id="Your profile is stored on your device and only shared with your contacts." xml:space="preserve" approved="no"> + <source>Your profile is stored on your device and only shared with your contacts.</source> <target state="translated">你的個人檔案只會和你的聯絡人分享。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The sender will NOT be notified" xml:space="preserve" approved="no"> <source>The sender will NOT be notified</source> - <target state="translated">發送者不會接收到通知</target> + <target state="translated">發送者不會收到通知</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Theme" xml:space="preserve" approved="no"> @@ -3076,12 +3071,12 @@ We will be adding server redundancy to prevent lost messages.</source> </trans-unit> <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve" approved="no"> <source>This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</source> - <target state="translated">這操作不能還原 - 所有已經接收和傳送的檔案和媒體檔案將刪除。低解析度圖片將保留。</target> + <target state="translated">這操作不能還原 - 將刪除所有已經接收和傳送的檔案和媒體。將保留低解析度圖片。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." xml:space="preserve" approved="no"> <source>This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</source> - <target state="translated">這操作無法撤銷 - 早於所選擇的時間發送和接收的訊息將被刪除。這可能需要幾分鐘的時間。</target> + <target state="translated">這操作無法撤銷 - 早於所選時間的收發訊息將被刪除。可能需要幾分鐘。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve" approved="no"> @@ -3118,8 +3113,8 @@ We will be adding server redundancy to prevent lost messages.</source> <source>To prevent the call interruption, enable Do Not Disturb mode.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve" approved="no"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> + <trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve" approved="no"> + <source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source> <target state="translated">為了保護隱私,而不像是其他平台般需要提取和存儲用戶的 IDs 資料,SimpleX 平台有自家佇列的標識符,這對於你的每個聯絡人也是獨一無二的。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3268,7 +3263,7 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve" approved="no"> <source>Use for new connections</source> - <target state="translated">用於新的連接</target> + <target state="translated">用於新的連線</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use server" xml:space="preserve" approved="no"> @@ -3288,7 +3283,7 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="Verify connection security" xml:space="preserve" approved="no"> <source>Verify connection security</source> - <target state="translated">驗證連接安全性</target> + <target state="translated">驗證連線安全性</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify security code" xml:space="preserve" approved="no"> @@ -3321,8 +3316,8 @@ To connect, please ask your contact to create another connection link and check <target state="translated">語音訊息於這個聊天窒是禁用的。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve" approved="no"> - <source>Voice messages are prohibited in this group.</source> + <trans-unit id="Voice messages are prohibited." xml:space="preserve" approved="no"> + <source>Voice messages are prohibited.</source> <target state="translated">語音訊息於這個群組內是禁用的。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3455,11 +3450,6 @@ To connect, please ask your contact to create another connection link and check <target state="translated">你可以使用 Markdown 語法以更清楚標明訊息:</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve" approved="no"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <target state="translated">你可以控制通過哪一個伺服器 **來接收** 你的聯絡人訊息 – 這些伺服器用來接收他們傳送給你的訊息。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve" approved="no"> <source>You could not be verified; please try again.</source> <target state="translated">你未能通過認證;請再試一次。</target> @@ -4173,7 +4163,7 @@ SimpleX 伺服器並不會看到你的個人檔案。</target> </trans-unit> <trans-unit id="via contact address link" xml:space="preserve" approved="no"> <source>via contact address link</source> - <target state="translated">透過聯絡人的邀請連結連接</target> + <target state="translated">透過聯絡人的邀請連結連線</target> <note>chat list item description</note> </trans-unit> <trans-unit id="via group link" xml:space="preserve" approved="no"> @@ -4183,7 +4173,7 @@ SimpleX 伺服器並不會看到你的個人檔案。</target> </trans-unit> <trans-unit id="via one-time link" xml:space="preserve" approved="no"> <source>via one-time link</source> - <target state="translated">透過一次性連結連接</target> + <target state="translated">透過一次性連結連線</target> <note>chat list item description</note> </trans-unit> <trans-unit id="via relay" xml:space="preserve" approved="no"> @@ -4712,7 +4702,7 @@ Available in v5.1</source> </trans-unit> <trans-unit id="%u messages failed to decrypt." xml:space="preserve" approved="no"> <source>%u messages failed to decrypt.</source> - <target state="translated">%u 訊息解密失敗。</target> + <target state="translated">%u 則訊息解密失敗。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%u messages skipped." xml:space="preserve" approved="no"> @@ -4791,8 +4781,8 @@ Available in v5.1</source> <target state="translated">訊息 & 檔案</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve" approved="no"> - <source>Migrations: %@</source> + <trans-unit id="Migrations:" xml:space="preserve" approved="no"> + <source>Migrations:</source> <target state="translated">遷移:%@</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -5162,7 +5152,7 @@ Available in v5.1</source> </trans-unit> <trans-unit id="Tap to activate profile." xml:space="preserve" approved="no"> <source>Tap to activate profile.</source> - <target state="translated">點擊以激活配置檔案。</target> + <target state="translated">點擊以激活設定檔。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="There should be at least one visible user profile." xml:space="preserve" approved="no"> @@ -5523,8 +5513,8 @@ It can happen because of some bug or when the connection is compromised.</source <target state="translated">啟用自毀密碼</target> <note>set passcode view</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve" approved="no"> - <source>Group members can add message reactions.</source> + <trans-unit id="Members can add message reactions." xml:space="preserve" approved="no"> + <source>Members can add message reactions.</source> <target state="translated">群組內的成員可以新增訊息互動。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -5699,8 +5689,8 @@ It can happen because of some bug or when the connection is compromised.</source <target state="translated">已移除在</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve" approved="no"> - <source>Message reactions are prohibited in this group.</source> + <trans-unit id="Message reactions are prohibited." xml:space="preserve" approved="no"> + <source>Message reactions are prohibited.</source> <target state="translated">訊息互動於這個群組內是禁用的。</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -5868,6 +5858,628 @@ It can happen because of some bug or when the connection is compromised.</source <target state="translated">你和你的聯絡人可以新增訊息互動。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ connected" xml:space="preserve" approved="no"> + <source>%@ connected</source> + <target state="translated">%@ 已連接</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="# %@" xml:space="preserve" approved="no"> + <source># %@</source> + <target state="translated"># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="%@ and %@" xml:space="preserve" approved="no"> + <source>%@ and %@</source> + <target state="translated">%@ 和 %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve" approved="no"> + <source>## History</source> + <target state="translated">紀錄</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve" approved="no"> + <source>## In reply to</source> + <target state="translated">回覆</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve" approved="no"> + <source>%@ and %@ connected</source> + <target state="translated">%@ 和 %@ 已連接</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ downloaded" xml:space="preserve" approved="no"> + <source>%@ downloaded</source> + <target state="translated">%@ 下載</target> + </trans-unit> + <trans-unit id="%@ uploaded" xml:space="preserve" approved="no"> + <source>%@ uploaded</source> + <target state="translated">%@ 上傳</target> + </trans-unit> + <trans-unit id="Abort" xml:space="preserve" approved="no"> + <source>Abort</source> + <target state="translated">中止</target> + </trans-unit> + <trans-unit id="**Create group**: to create a new group." xml:space="preserve" approved="no"> + <source>**Create group**: to create a new group.</source> + <target state="translated">**創建群組**: 創建一個新的群組。</target> + </trans-unit> + <trans-unit id="Abort changing address" xml:space="preserve" approved="no"> + <source>Abort changing address</source> + <target state="translated">中止更改地址</target> + </trans-unit> + <trans-unit id="Accept connection request?" xml:space="preserve" approved="no"> + <source>Accept connection request?</source> + <target state="translated">接受連線請求?</target> + </trans-unit> + <trans-unit id="Camera not available" xml:space="preserve" approved="no"> + <source>Camera not available</source> + <target state="translated">相機不可用</target> + </trans-unit> + <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve" approved="no"> + <source>All messages will be deleted - this cannot be undone!</source> + <target state="translated">所有訊息都將被刪除 - 這不能還原!</target> + </trans-unit> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve" approved="no"> + <source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source> + <target state="translated">只有你的聯絡人允許的情況下,才允許不可逆地將訊息刪除。(24小時)</target> + </trans-unit> + <trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no"> + <source>Allow to irreversibly delete sent messages. (24 hours)</source> + <target state="translated">允許將不可撤銷的訊息刪除。(24小時)</target> + </trans-unit> + <trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no"> + <source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source> + <target state="translated">允許您的聯絡人不可復原地刪除已傳送的訊息。(24小時)</target> + </trans-unit> + <trans-unit id="Bad desktop address" xml:space="preserve" approved="no"> + <source>Bad desktop address</source> + <target state="translated">無效的桌面地址</target> + </trans-unit> + <trans-unit id="Error decrypting file" xml:space="preserve" approved="no"> + <source>Error decrypting file</source> + <target state="translated">解密檔案時出錯</target> + </trans-unit> + <trans-unit id="Add contact" xml:space="preserve" approved="no"> + <source>Add contact</source> + <target state="translated">新增聯絡人</target> + </trans-unit> + <trans-unit id="Advanced settings" xml:space="preserve" approved="no"> + <source>Advanced settings</source> + <target state="translated">進階設定</target> + </trans-unit> + <trans-unit id="Allow calls?" xml:space="preserve" approved="no"> + <source>Allow calls?</source> + <target state="translated">允許通話?</target> + </trans-unit> + <trans-unit id="Allow to send files and media." xml:space="preserve" approved="no"> + <source>Allow to send files and media.</source> + <target state="translated">允許傳送檔案和媒體。</target> + </trans-unit> + <trans-unit id="Already joining the group!" xml:space="preserve" approved="no"> + <source>Already joining the group!</source> + <target state="translated">已加入群組!</target> + </trans-unit> + <trans-unit id="App data migration" xml:space="preserve" approved="no"> + <source>App data migration</source> + <target state="translated">應用資料轉移</target> + </trans-unit> + <trans-unit id="Apply" xml:space="preserve" approved="no"> + <source>Apply</source> + <target state="translated">應用</target> + </trans-unit> + <trans-unit id="Apply to" xml:space="preserve" approved="no"> + <source>Apply to</source> + <target state="translated">應用到</target> + </trans-unit> + <trans-unit id="Archive and upload" xml:space="preserve" approved="no"> + <source>Archive and upload</source> + <target state="translated">儲存並上傳</target> + </trans-unit> + <trans-unit id="Block" xml:space="preserve" approved="no"> + <source>Block</source> + <target state="translated">封鎖</target> + </trans-unit> + <trans-unit id="Block group members" xml:space="preserve" approved="no"> + <source>Block group members</source> + <target state="translated">封鎖群組成員</target> + </trans-unit> + <trans-unit id="Block member" xml:space="preserve" approved="no"> + <source>Block member</source> + <target state="translated">封鎖成員</target> + </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve" approved="no"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target state="translated">保加利亞語、芬蘭語、泰語和烏克蘭語——感謝使用者們和[Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> + </trans-unit> + <trans-unit id="Can't call member" xml:space="preserve" approved="no"> + <source>Can't call member</source> + <target state="translated">無法與成員通話</target> + </trans-unit> + <trans-unit id="Can't message member" xml:space="preserve" approved="no"> + <source>Can't message member</source> + <target state="translated">無法傳送訊息給成員</target> + </trans-unit> + <trans-unit id="Cancel migration" xml:space="preserve" approved="no"> + <source>Cancel migration</source> + <target state="translated">取消遷移</target> + </trans-unit> + <trans-unit id="Chat database exported" xml:space="preserve" approved="no"> + <source>Chat database exported</source> + <target state="translated">導出聊天數據庫</target> + </trans-unit> + <trans-unit id="0 sec" xml:space="preserve" approved="no"> + <source>0 sec</source> + <target state="translated">0 秒</target> + </trans-unit> + <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve" approved="no"> + <source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source> + <target state="translated">你的所有聯絡人、對話和文件將被安全加密並切塊上傳到設置的 XFTP 中繼。</target> + </trans-unit> + <trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve" approved="no"> + <source>Address change will be aborted. Old receiving address will be used.</source> + <target state="translated">將取消地址更改。將使用舊聯絡地址。</target> + </trans-unit> + <trans-unit id="Archiving database" xml:space="preserve" approved="no"> + <source>Archiving database</source> + <target state="translated">正在儲存資料庫</target> + </trans-unit> + <trans-unit id="Cellular" xml:space="preserve" approved="no"> + <source>Cellular</source> + <target state="translated">行動網路</target> + </trans-unit> + <trans-unit id="%@, %@ and %lld members" xml:space="preserve" approved="no"> + <source>%@, %@ and %lld members</source> + <target state="translated">%@, %@ 和 %lld 成員</target> + </trans-unit> + <trans-unit id="%lld messages marked deleted" xml:space="preserve" approved="no"> + <source>%lld messages marked deleted</source> + <target state="translated">%lld 則訊息已標記為刪除</target> + </trans-unit> + <trans-unit id="Already connecting!" xml:space="preserve" approved="no"> + <source>Already connecting!</source> + <target state="translated">已連接!</target> + </trans-unit> + <trans-unit id="Block member?" xml:space="preserve" approved="no"> + <source>Block member?</source> + <target state="translated">封鎖成員?</target> + </trans-unit> + <trans-unit id="(new)" xml:space="preserve" approved="no"> + <source>(new)</source> + <target state="translated">(新)</target> + </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve" approved="no"> + <source>%@, %@ and %lld other members connected</source> + <target state="translated">%@, %@ 和 %lld 個成員已連接</target> + </trans-unit> + <trans-unit id="A few more things" xml:space="preserve" approved="no"> + <source>A few more things</source> + <target state="translated">其他</target> + </trans-unit> + <trans-unit id="Show last messages" xml:space="preserve" approved="no"> + <source>Show last messages</source> + <target state="translated">顯示最新的訊息</target> + </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve" approved="no"> + <source>App encrypts new local files (except videos).</source> + <target state="translated">應用程式將為新的本機文件(影片除外)加密。</target> + </trans-unit> + <trans-unit id="Better groups" xml:space="preserve" approved="no"> + <source>Better groups</source> + <target state="translated">更加的群組</target> + </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve" approved="no"> + <source>%lld new interface languages</source> + <target state="translated">%lld 種新的介面語言</target> + </trans-unit> + <trans-unit id="Blocked by admin" xml:space="preserve" approved="no"> + <source>Blocked by admin</source> + <target state="translated">由管理員封鎖</target> + </trans-unit> + <trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no"> + <source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source> + <target state="translated">您與您的聯絡人都可以不可復原地删除已傳送的訊息。(24小時)</target> + </trans-unit> + <trans-unit id="Encrypt local files" xml:space="preserve" approved="no"> + <source>Encrypt local files</source> + <target state="translated">加密本機檔案</target> + </trans-unit> + <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve" approved="no"> + <source>- more stable message delivery. +- a bit better groups. +- and more!</source> + <target state="translated">- 更穩定的傳送! +- 更好的社群! +- 以及更多!</target> + </trans-unit> + <trans-unit id="- optionally notify deleted contacts. - profile names with spaces. - and more!" xml:space="preserve" approved="no"> + <source>- optionally notify deleted contacts. +- profile names with spaces. +- and more!</source> + <target state="translated">- 可選擇通知已刪除的聯絡人 +- 帶空格的共人資料名稱。 +-以及更多!</target> + </trans-unit> + <trans-unit id="Abort changing address?" xml:space="preserve" approved="no"> + <source>Abort changing address?</source> + <target state="translated">中止更改地址?</target> + </trans-unit> + <trans-unit id="Allow to send SimpleX links." xml:space="preserve" approved="no"> + <source>Allow to send SimpleX links.</source> + <target state="translated">允許傳送 SimpleX 連結。</target> + </trans-unit> + <trans-unit id="Background" xml:space="preserve" approved="no"> + <source>Background</source> + <target state="translated">後台</target> + </trans-unit> + <trans-unit id="SimpleX links not allowed" xml:space="preserve" approved="no"> + <source>SimpleX links not allowed</source> + <target state="translated">不允許 SimpleX 連結</target> + </trans-unit> + <trans-unit id="Voice messages not allowed" xml:space="preserve" approved="no"> + <source>Voice messages not allowed</source> + <target state="translated">不允許語音訊息</target> + </trans-unit> + <trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve" approved="no"> + <source>The text you pasted is not a SimpleX link.</source> + <target state="translated">您貼在這裡的連結不是 SimpleX 連結。</target> + </trans-unit> + <trans-unit id="%d file(s) were deleted." xml:space="preserve" approved="no"> + <source>%d file(s) were deleted.</source> + <target state="translated">已刪除 %d 個檔案。</target> + </trans-unit> + <trans-unit id="Reset to app theme" xml:space="preserve" approved="no"> + <source>Reset to app theme</source> + <target state="translated">重設至應用程式主題</target> + </trans-unit> + <trans-unit id="Retry" xml:space="preserve" approved="no"> + <source>Retry</source> + <target state="translated">重試</target> + </trans-unit> + <trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve" approved="no"> + <source>The uploaded database archive will be permanently removed from the servers.</source> + <target state="translated">上傳的資料庫存檔將從伺服器永久移除。</target> + </trans-unit> + <trans-unit id="Shape profile images" xml:space="preserve" approved="no"> + <source>Shape profile images</source> + <target state="translated">塑造個人資料圖片</target> + </trans-unit> + <trans-unit id="**Scan / Paste link**: to connect via a link you received." xml:space="preserve" approved="no"> + <source>**Scan / Paste link**: to connect via a link you received.</source> + <target state="translated">**掃描/貼上連結**:以透過您收到的連結連線。</target> + </trans-unit> + <trans-unit id="Reports" xml:space="preserve" approved="no"> + <source>Reports</source> + <target state="translated">舉報</target> + </trans-unit> + <trans-unit id="Use SOCKS proxy" xml:space="preserve" approved="no"> + <source>Use SOCKS proxy</source> + <target state="translated">使用 SOCKS 代理</target> + </trans-unit> + <trans-unit id="Reset all statistics" xml:space="preserve" approved="no"> + <source>Reset all statistics</source> + <target state="translated">重設所有統計數據</target> + </trans-unit> + <trans-unit id="SOCKS proxy" xml:space="preserve" approved="no"> + <source>SOCKS proxy</source> + <target state="translated">SOCKS 代理</target> + </trans-unit> + <trans-unit id="Send message to enable calls." xml:space="preserve" approved="no"> + <source>Send message to enable calls.</source> + <target state="translated">發送訊息以啟用通話功能。</target> + </trans-unit> + <trans-unit id="Send direct message to connect" xml:space="preserve" approved="no"> + <source>Send direct message to connect</source> + <target state="translated">直接發送訊息以連結</target> + </trans-unit> + <trans-unit id="Scale" xml:space="preserve" approved="no"> + <source>Scale</source> + <target state="translated">顯示比例</target> + </trans-unit> + <trans-unit id="Sent via proxy" xml:space="preserve" approved="no"> + <source>Sent via proxy</source> + <target state="translated">通過代理發送</target> + </trans-unit> + <trans-unit id="Servers info" xml:space="preserve" approved="no"> + <source>Servers info</source> + <target state="translated">伺服器訊息</target> + </trans-unit> + <trans-unit id="Set message expiration in chats." xml:space="preserve" approved="no"> + <source>Set message expiration in chats.</source> + <target state="translated">設定聊天中訊息期限。</target> + </trans-unit> + <trans-unit id="Share SimpleX address on social media." xml:space="preserve" approved="no"> + <source>Share SimpleX address on social media.</source> + <target state="translated">在社交媒體上分享 SimpleX 聯絡地址。</target> + </trans-unit> + <trans-unit id="Storage" xml:space="preserve" approved="no"> + <source>Storage</source> + <target state="translated">存儲</target> + </trans-unit> + <trans-unit id="Starting from %@." xml:space="preserve" approved="no"> + <source>Starting from %@.</source> + <target state="translated">開始於 %@。</target> + </trans-unit> + <trans-unit id="The second tick we missed! ✅" xml:space="preserve" approved="no"> + <source>The second tick we missed! ✅</source> + <target state="translated">我們錯過的第二個勾選! ✅</target> + </trans-unit> + <trans-unit id="Themes" xml:space="preserve" approved="no"> + <source>Themes</source> + <target state="translated">主題</target> + </trans-unit> + <trans-unit id="%d file(s) failed to download." xml:space="preserve" approved="no"> + <source>%d file(s) failed to download.</source> + <target state="translated">%d 個檔案下載失敗。</target> + </trans-unit> + <trans-unit id="Session code" xml:space="preserve" approved="no"> + <source>Session code</source> + <target state="translated">會話代碼</target> + </trans-unit> + <trans-unit id="Servers statistics will be reset - this cannot be undone!" xml:space="preserve" approved="no"> + <source>Servers statistics will be reset - this cannot be undone!</source> + <target state="translated">伺服器統計資料將被重設 - 此操作無法撤銷!</target> + </trans-unit> + <trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve" approved="no"> + <source>**Create 1-time link**: to create and share a new invitation link.</source> + <target state="translated">**建立一次性連結**:建立並分享新邀請連結。</target> + </trans-unit> + <trans-unit id="Set default theme" xml:space="preserve" approved="no"> + <source>Set default theme</source> + <target state="translated">設定缺省主題</target> + </trans-unit> + <trans-unit id="%lld group events" xml:space="preserve" approved="no"> + <source>%lld group events</source> + <target state="translated">%lld 個群組事件</target> + </trans-unit> + <trans-unit id="Reset all statistics?" xml:space="preserve" approved="no"> + <source>Reset all statistics?</source> + <target state="translated">重設所有統計數據?</target> + </trans-unit> + <trans-unit id="%@ server" xml:space="preserve" approved="no"> + <source>%@ server</source> + <target state="translated">%@ 伺服器</target> + </trans-unit> + <trans-unit id="%d file(s) were not downloaded." xml:space="preserve" approved="no"> + <source>%d file(s) were not downloaded.</source> + <target state="translated">%d 個檔案未下載。</target> + </trans-unit> + <trans-unit id="%d messages not forwarded" xml:space="preserve" approved="no"> + <source>%d messages not forwarded</source> + <target state="translated">%d 則訊息未轉發</target> + </trans-unit> + <trans-unit id="Test notifications" xml:space="preserve" approved="no"> + <source>Test notifications</source> + <target state="translated">测试通知</target> + </trans-unit> + <trans-unit id="(this device v%@)" xml:space="preserve" approved="no"> + <source>(this device v%@)</source> + <target state="translated">(此設備 v%@)</target> + </trans-unit> + <trans-unit id="Settings were changed." xml:space="preserve" approved="no"> + <source>Settings were changed.</source> + <target state="translated">設定已更改。</target> + </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." xml:space="preserve" approved="no"> + <source>This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</source> + <target state="translated">這操作不能撤銷 - 此聊天中早於所選訊息的收發訊息將被刪除。</target> + </trans-unit> + <trans-unit id="Subscription errors" xml:space="preserve" approved="no"> + <source>Subscription errors</source> + <target state="translated">訂閱錯誤</target> + </trans-unit> + <trans-unit id="Report" xml:space="preserve" approved="no"> + <source>Report</source> + <target state="translated">舉報</target> + </trans-unit> + <trans-unit id="Send messages directly when IP address is protected and your or destination server does not support private routing." xml:space="preserve" approved="no"> + <source>Send messages directly when IP address is protected and your or destination server does not support private routing.</source> + <target state="translated">當 IP 位址受保護且您或目的地伺服器不支援私人路由時,直接傳送訊息。</target> + </trans-unit> + <trans-unit id="Reset to user theme" xml:space="preserve" approved="no"> + <source>Reset to user theme</source> + <target state="translated">重設為使用者主題</target> + </trans-unit> + <trans-unit id="Use short links (BETA)" xml:space="preserve" approved="no"> + <source>Use short links (BETA)</source> + <target state="translated">使用短連結(Beta)</target> + </trans-unit> + <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve" approved="no"> + <source>Up to 100 last messages are sent to new members.</source> + <target state="translated">最多 100 則最後的訊息會傳送至新成員。</target> + </trans-unit> + <trans-unit id="%d seconds(s)" xml:space="preserve" approved="no"> + <source>%d seconds(s)</source> + <target state="translated">%d 秒</target> + </trans-unit> + <trans-unit id="%d file(s) are still being downloaded." xml:space="preserve" approved="no"> + <source>%d file(s) are still being downloaded.</source> + <target state="translated">仍在下載 %d 個檔案。</target> + </trans-unit> + <trans-unit id="%lld messages blocked by admin" xml:space="preserve" approved="no"> + <source>%lld messages blocked by admin</source> + <target state="translated">%lld 則訊息被管理員封鎖</target> + </trans-unit> + <trans-unit id="Report: %@" xml:space="preserve" approved="no"> + <source>Report: %@</source> + <target state="translated">舉報:%@</target> + </trans-unit> + <trans-unit id="Review conditions" xml:space="preserve" approved="no"> + <source>Review conditions</source> + <target state="translated">檢視使用條款</target> + </trans-unit> + <trans-unit id="Search or paste SimpleX link" xml:space="preserve" approved="no"> + <source>Search or paste SimpleX link</source> + <target state="translated">搜尋或貼上 SimpleX 連結</target> + </trans-unit> + <trans-unit id="Sent directly" xml:space="preserve" approved="no"> + <source>Sent directly</source> + <target state="translated">已直接發送</target> + </trans-unit> + <trans-unit id="SimpleX links are prohibited." xml:space="preserve" approved="no"> + <source>SimpleX links are prohibited.</source> + <target state="translated">這群組禁止 SimpleX 連結。</target> + </trans-unit> + <trans-unit id="Uploaded files" xml:space="preserve" approved="no"> + <source>Uploaded files</source> + <target state="translated">已上傳的檔案</target> + </trans-unit> + <trans-unit id="Use %@" xml:space="preserve" approved="no"> + <source>Use %@</source> + <target state="translated">使用 %@</target> + </trans-unit> + <trans-unit id="Upload errors" xml:space="preserve" approved="no"> + <source>Upload errors</source> + <target state="translated">上傳錯誤</target> + </trans-unit> + <trans-unit id="Use servers" xml:space="preserve" approved="no"> + <source>Use servers</source> + <target state="translated">使用伺服器</target> + </trans-unit> + <trans-unit id="security code changed" xml:space="preserve" approved="no"> + <source>security code changed</source> + <target state="translated">安全碼已變更</target> + </trans-unit> + <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve" approved="no"> + <source>These settings are for your current profile **%@**.</source> + <target state="translated">這些設定是針對您目前的設定檔 **%@**。</target> + </trans-unit> + <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve" approved="no"> + <source>They can be overridden in contact and group settings.</source> + <target state="translated">您可在連絡人和群組設定中覆寫它們。</target> + </trans-unit> + <trans-unit id="%@, %@" xml:space="preserve" approved="no"> + <source>%1$@, %2$@</source> + <target state="translated">%1$@, %2$@</target> + </trans-unit> + <trans-unit id="Verify connections" xml:space="preserve" approved="no"> + <source>Verify connections</source> + <target state="translated">驗證連線</target> + </trans-unit> + <trans-unit id="Verify connection" xml:space="preserve" approved="no"> + <source>Verify connection</source> + <target state="translated">驗證連線</target> + </trans-unit> + <trans-unit id="Verify passphrase" xml:space="preserve" approved="no"> + <source>Verify passphrase</source> + <target state="translated">驗證密碼</target> + </trans-unit> + <trans-unit id="Verify code with desktop" xml:space="preserve" approved="no"> + <source>Verify code with desktop</source> + <target state="translated">使用桌上電腦驗證代碼</target> + </trans-unit> + <trans-unit id="Save list" xml:space="preserve" approved="no"> + <source>Save list</source> + <target state="translated">儲存列表</target> + </trans-unit> + <trans-unit id="Saving %lld messages" xml:space="preserve" approved="no"> + <source>Saving %lld messages</source> + <target state="translated">正在儲存 %lld 則訊息</target> + </trans-unit> + <trans-unit id="search" xml:space="preserve" approved="no"> + <source>search</source> + <target state="translated">搜尋</target> + </trans-unit> + <trans-unit id="requested to connect" xml:space="preserve" approved="no"> + <source>requested to connect</source> + <target state="translated">已請求連結</target> + </trans-unit> + <trans-unit id="saved" xml:space="preserve" approved="no"> + <source>saved</source> + <target state="translated">已儲存</target> + </trans-unit> + <trans-unit id="video" xml:space="preserve" approved="no"> + <source>video</source> + <target state="translated">視訊</target> + </trans-unit> + <trans-unit id="Tap to Connect" xml:space="preserve" approved="no"> + <source>Tap to Connect</source> + <target state="translated">點擊以連結</target> + </trans-unit> + <trans-unit id="Unsupported connection link" xml:space="preserve" approved="no"> + <source>Unsupported connection link</source> + <target state="translated">未受支持的連線連結</target> + </trans-unit> + <trans-unit id="Saved from" xml:space="preserve" approved="no"> + <source>Saved from</source> + <target state="translated">儲存自</target> + </trans-unit> + <trans-unit id="Saved" xml:space="preserve" approved="no"> + <source>Saved</source> + <target state="translated">已儲存</target> + </trans-unit> + <trans-unit id="Scan / Paste link" xml:space="preserve" approved="no"> + <source>Scan / Paste link</source> + <target state="translated">掃描/貼上連結</target> + </trans-unit> + <trans-unit id="SimpleX" xml:space="preserve" approved="no"> + <source>SimpleX</source> + <target state="translated">SimpleX</target> + </trans-unit> + <trans-unit id="Use the app while in the call." xml:space="preserve" approved="no"> + <source>Use the app while in the call.</source> + <target state="translated">在通話時使用此應用程式。</target> + </trans-unit> + <trans-unit id="v%@" xml:space="preserve" approved="no"> + <source>v%@</source> + <target state="translated">v%@</target> + </trans-unit> + <trans-unit id="Save your profile?" xml:space="preserve" approved="no"> + <source>Save your profile?</source> + <target state="translated">儲存設定檔?</target> + </trans-unit> + <trans-unit id="Use for messages" xml:space="preserve" approved="no"> + <source>Use for messages</source> + <target state="translated">用於訊息</target> + </trans-unit> + <trans-unit id="Uploading archive" xml:space="preserve" approved="no"> + <source>Uploading archive</source> + <target state="translated">正在上傳檔案庫</target> + </trans-unit> + <trans-unit id="Unlink" xml:space="preserve" approved="no"> + <source>Unlink</source> + <target state="translated">從桌上電腦解除連結</target> + </trans-unit> + <trans-unit id="%lld messages blocked" xml:space="preserve" approved="no"> + <source>%lld messages blocked</source> + <target state="translated">已封鎖 %d 則訊息</target> + </trans-unit> + <trans-unit id="The same conditions will apply to operator **%@**." xml:space="preserve" approved="no"> + <source>The same conditions will apply to operator **%@**.</source> + <target state="translated">相同條件也適用於 **%@** 操作員。</target> + </trans-unit> + <trans-unit id="These conditions will also apply for: **%@**." xml:space="preserve" approved="no"> + <source>These conditions will also apply for: **%@**.</source> + <target state="translated">這些條件也適用於:**%@**。</target> + </trans-unit> + <trans-unit id="Upload failed" xml:space="preserve" approved="no"> + <source>Upload failed</source> + <target state="translated">上傳失敗</target> + </trans-unit> + <trans-unit id="Use the app with one hand." xml:space="preserve" approved="no"> + <source>Use the app with one hand.</source> + <target state="translated">單手使用此應用程式。</target> + </trans-unit> + <trans-unit id="Safely receive files" xml:space="preserve" approved="no"> + <source>Safely receive files</source> + <target state="translated">安全地接收檔案</target> + </trans-unit> + <trans-unit id="Saved message" xml:space="preserve" approved="no"> + <source>Saved message</source> + <target state="translated">已儲存的訊息</target> + </trans-unit> + <trans-unit id="Use from desktop" xml:space="preserve" approved="no"> + <source>Use from desktop</source> + <target state="translated">在桌上電腦上使用</target> + </trans-unit> + <trans-unit id="Via secure quantum resistant protocol." xml:space="preserve" approved="no"> + <source>Via secure quantum resistant protocol.</source> + <target state="translated">使用量子安全的協定。</target> + </trans-unit> + <trans-unit id="Uploaded" xml:space="preserve" approved="no"> + <source>Uploaded</source> + <target state="translated">已上傳</target> + </trans-unit> </body> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="zh-Hant" datatype="plaintext"> diff --git a/apps/ios/SimpleX NSE/ConcurrentQueue.swift b/apps/ios/SimpleX NSE/ConcurrentQueue.swift deleted file mode 100644 index 274a683c00..0000000000 --- a/apps/ios/SimpleX NSE/ConcurrentQueue.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// ConcurrentQueue.swift -// SimpleX NSE -// -// Created by Evgeny on 08/12/2023. -// Copyright © 2023 SimpleX Chat. All rights reserved. -// - -import Foundation - -struct DequeueElement<T> { - var elementId: UUID? - var task: Task<T?, Never> -} - -class ConcurrentQueue<T> { - private var queue: [T] = [] - private var queueLock = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.concurrent-queue.lock.\(UUID())") - private var continuations = [(elementId: UUID, continuation: CheckedContinuation<T?, Never>)]() - - func enqueue(_ el: T) { - resumeContinuation(el) { self.queue.append(el) } - } - - func frontEnqueue(_ el: T) { - resumeContinuation(el) { self.queue.insert(el, at: 0) } - } - - private func resumeContinuation(_ el: T, add: @escaping () -> Void) { - queueLock.sync { - if let (_, cont) = continuations.first { - continuations.remove(at: 0) - cont.resume(returning: el) - } else { - add() - } - } - } - - func dequeue() -> DequeueElement<T> { - queueLock.sync { - if queue.isEmpty { - let elementId = UUID() - let task = Task { - await withCheckedContinuation { cont in - continuations.append((elementId, cont)) - } - } - return DequeueElement(elementId: elementId, task: task) - } else { - let el = queue.remove(at: 0) - return DequeueElement(task: Task { el }) - } - } - } - - func cancelDequeue(_ elementId: UUID) { - queueLock.sync { - let cancelled = continuations.filter { $0.elementId == elementId } - continuations.removeAll { $0.elementId == elementId } - cancelled.forEach { $0.continuation.resume(returning: nil) } - } - } -} diff --git a/apps/ios/SimpleX NSE/NSEAPITypes.swift b/apps/ios/SimpleX NSE/NSEAPITypes.swift new file mode 100644 index 0000000000..35a838fff9 --- /dev/null +++ b/apps/ios/SimpleX NSE/NSEAPITypes.swift @@ -0,0 +1,127 @@ +// +// APITypes.swift +// SimpleX +// +// Created by EP on 01/05/2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SimpleXChat + +enum NSEChatCommand: ChatCmdProtocol { + case showActiveUser + case startChat(mainApp: Bool, enableSndFiles: Bool) + case apiActivateChat(restoreChat: Bool) + case apiSuspendChat(timeoutMicroseconds: Int) + case apiSetNetworkConfig(networkConfig: NetCfg) + case apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) + case apiSetEncryptLocalFiles(enable: Bool) + case apiGetNtfConns(nonce: String, encNtfInfo: String) + case apiGetConnNtfMessages(connMsgReqs: [ConnMsgReq]) + case receiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?, inline: Bool?) + case setFileToReceive(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?) + + var cmdString: String { + switch self { + case .showActiveUser: return "/u" + case let .startChat(mainApp, enableSndFiles): return "/_start main=\(onOff(mainApp)) snd_files=\(onOff(enableSndFiles))" + case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))" + case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)" + case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))" + case let .apiSetAppFilePaths(filesFolder, tempFolder, assetsFolder): + return "/set file paths \(encodeJSON(AppFilePaths(appFilesFolder: filesFolder, appTempFolder: tempFolder, appAssetsFolder: assetsFolder)))" + case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))" + case let .apiGetNtfConns(nonce, encNtfInfo): return "/_ntf conns \(nonce) \(encNtfInfo)" + case let .apiGetConnNtfMessages(connMsgReqs): return "/_ntf conn messages \(connMsgReqs.map { $0.cmdString }.joined(separator: ","))" + case let .receiveFile(fileId, userApprovedRelays, encrypt, inline): return "/freceive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))" + case let .setFileToReceive(fileId, userApprovedRelays, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))" + } + } + + private func onOffParam(_ param: String, _ b: Bool?) -> String { + if let b = b { + " \(param)=\(onOff(b))" + } else { + "" + } + } +} + +enum NSEChatResponse: Decodable, ChatAPIResult { + case activeUser(user: User) + case chatStarted + case chatRunning + case rcvFileAccepted(user: UserRef, chatItem: AChatItem) + case ntfConns(ntfConns: [NtfConn]) + case connNtfMessages(receivedMsgs: [RcvNtfMsgInfo]) + case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgAckInfo) + case cmdOk(user_: UserRef?) + + var responseType: String { + switch self { + case .activeUser: "activeUser" + case .chatStarted: "chatStarted" + case .chatRunning: "chatRunning" + case .rcvFileAccepted: "rcvFileAccepted" + case .ntfConns: "ntfConns" + case .connNtfMessages: "connNtfMessages" + case .ntfMessage: "ntfMessage" + case .cmdOk: "cmdOk" + } + } + + var details: String { + switch self { + case let .activeUser(user): return String(describing: user) + case .chatStarted: return noDetails + case .chatRunning: return noDetails + case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem)) + case let .ntfConns(ntfConns): return String(describing: ntfConns) + case let .connNtfMessages(receivedMsgs): return "receivedMsgs: \(String(describing: receivedMsgs))" + case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))") + case .cmdOk: return noDetails + } + } +} + +enum NSEChatEvent: Decodable, ChatAPIResult { + case chatSuspended + case contactConnected(user: UserRef, contact: Contact, userCustomProfile: Profile?) + case receivedContactRequest(user: UserRef, contactRequest: UserContactRequest) + case newChatItems(user: UserRef, chatItems: [AChatItem]) + case rcvFileSndCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer) + case sndFileComplete(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) + case sndFileRcvCancelled(user: UserRef, chatItem_: AChatItem?, sndFileTransfer: SndFileTransfer) + case callInvitation(callInvitation: RcvCallInvitation) + case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgAckInfo) + + var responseType: String { + switch self { + case .chatSuspended: "chatSuspended" + case .contactConnected: "contactConnected" + case .receivedContactRequest: "receivedContactRequest" + case .newChatItems: "newChatItems" + case .rcvFileSndCancelled: "rcvFileSndCancelled" + case .sndFileComplete: "sndFileComplete" + case .sndFileRcvCancelled: "sndFileRcvCancelled" + case .callInvitation: "callInvitation" + case .ntfMessage: "ntfMessage" + } + } + + var details: String { + switch self { + case .chatSuspended: return noDetails + case let .contactConnected(u, contact, _): return withUser(u, String(describing: contact)) + case let .receivedContactRequest(u, contactRequest): return withUser(u, String(describing: contactRequest)) + case let .newChatItems(u, chatItems): + let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n") + return withUser(u, itemsString) + case let .rcvFileSndCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .sndFileComplete(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .sndFileRcvCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .callInvitation(inv): return String(describing: inv) + case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))") + } + } +} diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index f9b4852e53..176da2481e 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -16,126 +16,59 @@ let logger = Logger() let appSuspendingDelay: UInt64 = 2_500_000_000 -let nseSuspendDelay: TimeInterval = 2 +typealias SuspendSchedule = (delay: TimeInterval, timeout: Int) -let nseSuspendTimeout: Int = 5 +let nseSuspendSchedule: SuspendSchedule = (2, 4) -typealias NtfStream = ConcurrentQueue<NSENotification> +let fastNSESuspendSchedule: SuspendSchedule = (1, 1) -// Notifications are delivered via concurrent queues, as they are all received from chat controller in a single loop that -// writes to ConcurrentQueue and when notification is processed, the instance of Notification service extension reads from the queue. -// One queue per connection (entity) is used. -// The concurrent queues allow read cancellation, to ensure that notifications are not lost in case the current thread completes -// before expected notification is read (multiple notifications can be expected, because one notification can be delivered for several messages). -actor PendingNtfs { - static let shared = PendingNtfs() - private var ntfStreams: [String: NtfStream] = [:] +public enum NSENotificationData { + case connectionEvent(_ user: User, _ connEntity: ConnectionEntity) + case contactConnected(_ user: any UserLike, _ contact: Contact) + case contactRequest(_ user: any UserLike, _ contactRequest: UserContactRequest) + case messageReceived(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) + case callInvitation(_ invitation: RcvCallInvitation) + case msgInfo(NtfMsgAckInfo) + case noNtf - func createStream(_ id: String) async { - logger.debug("NotificationService PendingNtfs.createStream: \(id, privacy: .public)") - if ntfStreams[id] == nil { - ntfStreams[id] = ConcurrentQueue() - logger.debug("NotificationService PendingNtfs.createStream: created ConcurrentQueue") - } - } - - func readStream(_ id: String, for nse: NotificationService, ntfInfo: NtfMessages) async { - logger.debug("NotificationService PendingNtfs.readStream: \(id, privacy: .public) \(ntfInfo.ntfMessages.count, privacy: .public)") - if !ntfInfo.user.showNotifications { - nse.setBestAttemptNtf(.empty) - } - if let s = ntfStreams[id] { - logger.debug("NotificationService PendingNtfs.readStream: has stream") - var expected = Set(ntfInfo.ntfMessages.map { $0.msgId }) - logger.debug("NotificationService PendingNtfs.readStream: expecting: \(expected, privacy: .public)") - var readCancelled = false - var dequeued: DequeueElement<NSENotification>? - nse.cancelRead = { - readCancelled = true - if let elementId = dequeued?.elementId { - s.cancelDequeue(elementId) - } - } - while !readCancelled { - dequeued = s.dequeue() - if let ntf = await dequeued?.task.value { - if readCancelled { - logger.debug("NotificationService PendingNtfs.readStream: read cancelled, put ntf to queue front") - s.frontEnqueue(ntf) - break - } else if case let .msgInfo(info) = ntf { - let found = expected.remove(info.msgId) - if found != nil { - logger.debug("NotificationService PendingNtfs.readStream: msgInfo, last: \(expected.isEmpty, privacy: .public)") - if expected.isEmpty { break } - } else if let msgTs = ntfInfo.msgTs, info.msgTs > msgTs { - logger.debug("NotificationService PendingNtfs.readStream: unexpected msgInfo") - s.frontEnqueue(ntf) - break - } - } else if ntfInfo.user.showNotifications { - logger.debug("NotificationService PendingNtfs.readStream: setting best attempt") - nse.setBestAttemptNtf(ntf) - if ntf.isCallInvitation { break } - } - } else { - break - } - } - nse.cancelRead = nil - logger.debug("NotificationService PendingNtfs.readStream: exiting") - } - } - - func writeStream(_ id: String, _ ntf: NSENotification) async { - logger.debug("NotificationService PendingNtfs.writeStream: \(id, privacy: .public)") - if let s = ntfStreams[id] { - logger.debug("NotificationService PendingNtfs.writeStream: writing ntf") - s.enqueue(ntf) - } - } -} - -// The current implementation assumes concurrent notification delivery and uses semaphores -// to process only one notification per connection (entity) at a time. -class NtfStreamSemaphores { - static let shared = NtfStreamSemaphores() - private static let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-semaphores.lock") - private var semaphores: [String: DispatchSemaphore] = [:] - - func waitForStream(_ id: String) { - streamSemaphore(id, value: 0)?.wait() - } - - func signalStreamReady(_ id: String) { - streamSemaphore(id, value: 1)?.signal() - } - - // this function returns nil if semaphore is just created, so passed value shoud be coordinated with the desired end value of the semaphore - private func streamSemaphore(_ id: String, value: Int) -> DispatchSemaphore? { - NtfStreamSemaphores.queue.sync { - if let s = semaphores[id] { - return s - } else { - semaphores[id] = DispatchSemaphore(value: value) - return nil - } - } - } -} - -enum NSENotification { - case nse(UNMutableNotificationContent) - case callkit(RcvCallInvitation) - case empty - case msgInfo(NtfMsgInfo) - - var isCallInvitation: Bool { + @inline(__always) + var callInvitation: RcvCallInvitation? { switch self { - case let .nse(ntf): ntf.categoryIdentifier == ntfCategoryCallInvitation - case .callkit: true - case .empty: false - case .msgInfo: false + case let .callInvitation(invitation): invitation + default: nil + } + } + + func notificationContent(_ badgeCount: Int) -> UNMutableNotificationContent { + return switch self { + case let .connectionEvent(user, connEntity): createConnectionEventNtf(user, connEntity, badgeCount) + case let .contactConnected(user, contact): createContactConnectedNtf(user, contact, badgeCount) + case let .contactRequest(user, contactRequest): createContactRequestNtf(user, contactRequest, badgeCount) + case let .messageReceived(user, cInfo, cItem): createMessageReceivedNtf(user, cInfo, cItem, badgeCount) + case let .callInvitation(invitation): createCallInvitationNtf(invitation, badgeCount) + case .msgInfo: UNMutableNotificationContent() + case .noNtf: UNMutableNotificationContent() + } + } + + @inline(__always) + var notificationEvent: NSENotificationData? { + switch self { + case .connectionEvent: self + case .contactConnected: self + case .contactRequest: self + case .messageReceived: self + case .callInvitation: self + case .msgInfo: nil + case .noNtf: nil + } + } + + @inline(__always) + var newMsgNtf: NSENotificationData? { + switch self { + case .messageReceived: self + default: nil } } } @@ -145,30 +78,143 @@ enum NSENotification { // or when background notification is received. class NSEThreads { static let shared = NSEThreads() - private static let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-threads.lock") + private let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-threads.lock") private var allThreads: Set<UUID> = [] - private var activeThreads: Set<UUID> = [] + private var activeThreads: [(threadId: UUID, nse: NotificationService)] = [] + private var droppedNotifications: [(entityId: ChatId, ntf: NSENotificationData)] = [] + @inline(__always) + private init() {} // only shared instance can be used + + @inline(__always) func newThread() -> UUID { - NSEThreads.queue.sync { + queue.sync { let (_, t) = allThreads.insert(UUID()) return t } } - func startThread(_ t: UUID) { - NSEThreads.queue.sync { + @inline(__always) + func startThread(_ t: UUID, _ service: NotificationService) { + queue.sync { if allThreads.contains(t) { - _ = activeThreads.insert(t) + activeThreads.append((t, service)) } else { logger.warning("NotificationService startThread: thread \(t) was removed before it started") } } } + // atomically: + // - checks that passed NSE instance can start processing passed notification entity, + // - adds it to the passed NSE instance, + // - marks as started, if no other NSE instance is processing it. + // Making all these steps atomic prevents a race condition between threads when both will be added and none will be started + @inline(__always) + func startEntity(_ nse: NotificationService, _ ntfEntity: NotificationEntity) -> Bool { + queue.sync { + // checking that none of activeThreads with another NSE instance processes the same entity and is not ready + let canStart = !activeThreads.contains(where: { (tId, otherNSE) in + tId != nse.threadId + && otherNSE.notificationEntities.contains(where: { (id, otherEntity) in + id == ntfEntity.entityId + && otherEntity.expectedMsg != nil + }) + }) + // atomically add entity to passed NSE instance + let id = ntfEntity.entityId + nse.notificationEntities[id] = ntfEntity + if canStart { + // and set as started, so it cannot be chosen to start by another NSE entity in nextThread + nse.notificationEntities[id]?.startedProcessingNewMsgs = true + } + return canStart + } + } + + @inline(__always) + func addDroppedNtf(_ id: ChatId, _ ntf: NSENotificationData) { + queue.sync { droppedNotifications.append((id, ntf)) } + } + + // atomically remove and return first dropped notification for the passed entity + @inline(__always) + func takeDroppedNtf(_ ntfEntity: NotificationEntity) -> (entityId: ChatId, ntf: NSENotificationData)? { + queue.sync { + if droppedNotifications.isEmpty { + nil + } else if let i = droppedNotifications.firstIndex(where: { (id, _) in id == ntfEntity.entityId }) { + droppedNotifications.remove(at: i) + } else { + nil + } + } + } + + // passes notification for processing to NSE instance chosen by rcvEntityThread + @inline(__always) + func processNotification(_ id: ChatId, _ ntf: NSENotificationData) async -> Void { + if let (nse, ntfEntity, expectedMsg) = rcvEntityThread(id, ntf) { + logger.debug("NotificationService processNotification \(id): found nse thread expecting message") + if nse.processReceivedNtf(ntfEntity, expectedMsg, ntf) { + nse.finalizeEntity(id) + } + } + } + + // atomically: + // - chooses active NSE instance that is ready to process notifications and expects message for passed entity ID + // - returns all dependencies for processing (notification entity and expected message) + // - adds notification to droppedNotifications if no ready NSE instance is found for the entity + @inline(__always) + private func rcvEntityThread(_ id: ChatId, _ ntf: NSENotificationData) -> (NotificationService, NotificationEntity, NtfMsgInfo)? { + queue.sync { + // this selects the earliest thread that: + // 1) has this connection entity in nse.notificationEntitites + // 2) has not completed processing messages for this connection entity (not ready) + let r = activeThreads.lazy.compactMap({ (_, nse) in + let ntfEntity = nse.notificationEntities[id] + return if let ntfEntity, let expectedMsg = ntfEntity.expectedMsg, ntfEntity.shouldProcessNtf { + (nse, ntfEntity, expectedMsg) + } else { + nil + } + }).first + if r == nil { droppedNotifications.append((id, ntf)) } + return r + } + } + + // Atomically mark entity in the passed NSE instance as not expecting messages, + // and signal the next NSE instance with this entity to start its processing. + @inline(__always) + func signalNextThread(_ nse: NotificationService, _ id: ChatId) { + queue.sync { + nse.notificationEntities[id]?.expectedMsg = nil + nse.notificationEntities[id]?.shouldProcessNtf = false + let next = activeThreads.first(where: { (_, nseNext) in + if let ntfEntity = nseNext.notificationEntities[id] { + ntfEntity.expectedMsg != nil && !ntfEntity.startedProcessingNewMsgs + } else { + false + } + }) + if let (tNext, nseNext) = next { + if let t = nse.threadId { logger.debug("NotificationService thread \(t): signalNextThread: signal next thread \(tNext) for entity \(id)") } + nseNext.notificationEntities[id]?.startedProcessingNewMsgs = true + nseNext.notificationEntities[id]?.semaphore.signal() + } + } + } + + @inline(__always) func endThread(_ t: UUID) -> Bool { - NSEThreads.queue.sync { - let tActive = activeThreads.remove(t) + queue.sync { + let tActive: UUID? = if let index = activeThreads.firstIndex(where: { $0.0 == t }) { + activeThreads.remove(at: index).0 + } else { + nil + } let t = allThreads.remove(t) if tActive != nil && activeThreads.isEmpty { return true @@ -180,50 +226,102 @@ class NSEThreads { } } + @inline(__always) var noThreads: Bool { allThreads.isEmpty } } +// NotificationEntity is a processing state for notifications from a single connection entity (message queue). +// Each NSE instance within NSE process can have more than one NotificationEntity. +// NotificationEntities of an NSE instance are processed concurrently, as messages arrive in any order. +// NotificationEntities for the same connection across multiple NSE instances (NSEThreads) are processed sequentially, so that the earliest NSE instance receives the earliest messages. +// The reason for this complexity is to process all required messages within allotted 30 seconds, +// accounting for the possibility that multiple notifications may be delivered concurrently. +struct NotificationEntity { + var ntfConn: NtfConn + var entityId: ChatId + + // expectedMsg == nil means that entity already has the best attempt to deliver, and no more messages are expected. + // It happens when: + // - the user is muted (set to nil in mkNotificationEntity) + // - apiGetNtfConns returns that there are no new messages (msgId in notification matches previously received), + // - messaging server fails to respond or replies that there are no messages (apiGetConnNtfMessages / getConnNtfMessage), + // - the message is received with the correct ID or timestamp (set to nil in signalNextThread). + var expectedMsg: NtfMsgInfo? + var allowedGetNextAttempts: Int = 3 + var msgBestAttemptNtf: NSENotificationData + + // startedProcessingNewMsgs determines that the entity stared processing events once it processed dropped notifications. + // It remains true when shouldProcessNtf is set to false, to prevent NSE from being chosen as the next for the entity. + // It is atomically set to true by startThead or by nextThread + var startedProcessingNewMsgs: Bool = false + + // shouldProcessNtf determines that NSE should process events for this entity, + // it is atomically set: + // - to true in processDroppedNotifications in case dropped notification is not chosen for delivery, and more messages are needed. + // - to false in nextThread + var shouldProcessNtf: Bool = false + + // this semaphone is used to wait for another NSE instance processing events for the same entity + var semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) + + var connMsgReq: ConnMsgReq? { + if let expectedMsg { + ConnMsgReq(msgConnId: ntfConn.agentConnId, msgDbQueueId: ntfConn.agentDbQueueId, msgTs: expectedMsg.msgTs) + } else { + nil + } + } +} + // Notification service extension creates a new instance of the class and calls didReceive for each notification. // Each didReceive is called in its own thread, but multiple calls can be made in one process, and, empirically, there is never // more than one process of notification service extension exists at a time. // Soon after notification service delivers the last notification it is either suspended or terminated. class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? - var bestAttemptNtf: NSENotification? + // served as notification if no message attempts (msgBestAttemptNtf) could be produced + var serviceBestAttemptNtf: UNMutableNotificationContent? var badgeCount: Int = 0 // thread is added to allThreads here - if thread did not start chat, // chat does not need to be suspended but NSE state still needs to be set to "suspended". var threadId: UUID? = NSEThreads.shared.newThread() - var receiveEntityId: String? - var cancelRead: (() -> Void)? + var notificationEntities: Dictionary<String, NotificationEntity> = [:] // key is entityId var appSubscriber: AppSubscriber? var returnedSuspension = false override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { logger.debug("DEBUGGING: NotificationService.didReceive") - let ntf = if let ntf_ = request.content.mutableCopy() as? UNMutableNotificationContent { ntf_ } else { UNMutableNotificationContent() } - setBestAttemptNtf(ntf) + let receivedNtf = if let ntf_ = request.content.mutableCopy() as? UNMutableNotificationContent { ntf_ } else { UNMutableNotificationContent() } + setServiceBestAttemptNtf(receivedNtf) self.contentHandler = contentHandler registerGroupDefaults() let appState = appStateGroupDefault.get() - logger.debug("NotificationService: app is \(appState.rawValue, privacy: .public)") + logger.debug("NotificationService: app is \(appState.rawValue)") switch appState { case .stopped: +// Use this block to debug notificaitons delivery in CLI, with "ejected" database and stopped chat +// if let nrData = ntfRequestData(request) { +// logger.debug("NotificationService get notification connections: /_ntf conns \(nrData.nonce) \(nrData.encNtfInfo)") +// contentHandler(receivedNtf) +// return; +// } setBadgeCount() - setBestAttemptNtf(createAppStoppedNtf()) - deliverBestAttemptNtf() + contentHandler(createAppStoppedNtf(badgeCount)) case .suspended: - setBadgeCount() - receiveNtfMessages(request, contentHandler) + setExpirationTimer() + receiveNtfMessages(request) case .suspending: - setBadgeCount() + // while application is suspending, the current instance will be waiting + setExpirationTimer() Task { let state: AppState = await withCheckedContinuation { cont in + // this subscriber uses message delivery via NSFileCoordinator to communicate between the app and NSE appSubscriber = appStateSubscriber { s in if s == .suspended { appSuspension(s) } } + // this is a fallback timeout, in case message from the app does not arrive DispatchQueue.global().asyncAfter(deadline: .now() + Double(appSuspendTimeout) + 1) { logger.debug("NotificationService: appSuspension timeout") appSuspension(appStateGroupDefault.get()) @@ -238,65 +336,250 @@ class NotificationService: UNNotificationServiceExtension { } } } - logger.debug("NotificationService: app state is now \(state.rawValue, privacy: .public)") - if state.inactive { - receiveNtfMessages(request, contentHandler) + logger.debug("NotificationService: app state is now \(state.rawValue)") + if state.inactive && self.contentHandler != nil { + receiveNtfMessages(request) } else { - deliverBestAttemptNtf() + contentHandler(receivedNtf) } } - default: - deliverBestAttemptNtf() + case .active: contentHandler(receivedNtf) + case .activating: contentHandler(receivedNtf) + case .bgRefresh: contentHandler(receivedNtf) } } - func receiveNtfMessages(_ request: UNNotificationRequest, _ contentHandler: @escaping (UNNotificationContent) -> Void) { + // This timer compensates for the scenarios when serviceExtensionTimeWillExpire does not fire at all. + // It is not clear why in some cases it does not fire, possibly it is a bug, + // or it depends on what the current thread is doing at the moment. + // If notification is not delivered and not cancelled, no further notifications will be processed. + @inline(__always) + private func setExpirationTimer() -> Void { + DispatchQueue.main.asyncAfter(deadline: .now() + 30) { + self.deliverBestAttemptNtf(urgent: true) + } + } + + @inline(__always) + private func ntfRequestData(_ request: UNNotificationRequest) -> (nonce: String, encNtfInfo: String)? { + if let ntfData = request.content.userInfo["notificationData"] as? [AnyHashable : Any], + let nonce = ntfData["nonce"] as? String, + let encNtfInfo = ntfData["message"] as? String { + (nonce, encNtfInfo) + } else { + nil + } + } + + // This function triggers notification message delivery for connection entities referenced in the notification. + // Notification may reference multiple connection entities (message queues) in order to compensate for Apple servers + // only delivering the latest notification, so it allows receiving messages from up to 6 contacts and groups from a + // single notification. This aggregation is handled by a notification server and is delivered via APNS servers in + // e2e encrypted envelope, and the app core prevents duplicate processing by keeping track of the last processed message. + + // The process steps: + // 0. apiGetConnNtfMessages or getConnNtfMessage get messages from the server for passed connection entities. + // We don't know in advance which chat events will be delivered from app core for a given notification, + // it may be a message, but it can also be contact request, various protocol confirmations, calls, etc., + // this function only returns metadata for the expected chat events. + // This metadata is correlated with .ntfMessage core event / .msgInfo notification marker - + // this marker allows determining when some message completed processing. + // 1. receiveMessages: singleton loop receiving events from core. + // 2. receivedMsgNtf: maps core events to notification events. + // 3. NSEThreads.shared.processNotification: chooses which notification service instance in the current process should process notification. + // While most of the time we observe that notifications are delivered sequentially, nothing in the documentation confirms it is sequential, + // and from various sources it follows that each instance executes in its own thread, so concurrency is expected. + // 4. processReceivedNtf: one of the instances of NSE processes notification event, deciding whether to request further messages + // for a given connection entity (via getConnNtfMessage) or that the correct message was received and notification can be delivered (deliverBestAttemptNtf). + // It is based on .msgInfo markers that indicate that message with a given timestamp was processed. + // 5. deliverBestAttemptNtf: is called multiple times, once each connection receives enough messages (based on .msgInfo marker). + // If further messages are expected, this function does nothing (unless it is called with urgent flag from timeout/expiration handlers). + func receiveNtfMessages(_ request: UNNotificationRequest) { logger.debug("NotificationService: receiveNtfMessages") if case .documents = dbContainerGroupDefault.get() { deliverBestAttemptNtf() return } - let userInfo = request.content.userInfo - if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any], - let nonce = ntfData["nonce"] as? String, - let encNtfInfo = ntfData["message"] as? String, - // check it here again + if let nrData = ntfRequestData(request), + // Check that the app is still inactive before starting the core. appStateGroupDefault.get().inactive { // thread is added to activeThreads tracking set here - if thread started chat it needs to be suspended - if let t = threadId { NSEThreads.shared.startThread(t) } + guard let t = threadId else { return } + NSEThreads.shared.startThread(t, self) let dbStatus = startChat() + // If database is opened successfully, get the list of connection entities (group members, contacts) + // that are referenced in the encrypted notification metadata. if case .ok = dbStatus, - let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { - logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.ntfMessages.count), privacy: .public)") - if let connEntity = ntfInfo.connEntity_ { - setBestAttemptNtf( - ntfInfo.ntfsEnabled - ? .nse(createConnectionEventNtf(ntfInfo.user, connEntity)) - : .empty - ) - if let id = connEntity.id { - receiveEntityId = id - NtfStreamSemaphores.shared.waitForStream(id) - if receiveEntityId != nil { - Task { - logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id, privacy: .public)") - await PendingNtfs.shared.createStream(id) - await PendingNtfs.shared.readStream(id, for: self, ntfInfo: ntfInfo) - deliverBestAttemptNtf() + let ntfConns = apiGetNtfConns(nonce: nrData.nonce, encNtfInfo: nrData.encNtfInfo) { + logger.debug("NotificationService: receiveNtfMessages: apiGetNtfConns ntfConns count = \(ntfConns.count)") + // uncomment localDisplayName in ConnectionEntity + // logger.debug("NotificationService: receiveNtfMessages: apiGetNtfConns ntfConns \(String(describing: ntfConns.map { $0.connEntity.localDisplayName }))") + + // Prepare expected messages - they will be delivered to the reception loop in this chain: + // They are atomically added to the instance notificationEntities inside msgReqs loop, to avoid any race conditions. + let ntfEntities = ntfConns.compactMap(mkNotificationEntity) + + // collect notification message requests for all connection entities + let msgReqs: [(chatId: String, connMsgReq: ConnMsgReq)] = ntfEntities.compactMap { ntfEntity -> (chatId: String, connMsgReq: ConnMsgReq)? in + // No need to request messages for connection entities that are "ready", + // e.g. for muted users or when the message is not expected based on notification. + let id = ntfEntity.entityId + if let expectedMsg = ntfEntity.expectedMsg { + if NSEThreads.shared.startEntity(self, ntfEntity) { // atomically checks and adds ntfEntity to NSE + // process any notifications "postponed" by the previous instance + let completed = processDroppedNotifications(ntfEntity, expectedMsg) + return if !completed, let connMsgReq = notificationEntities[id]?.connMsgReq { + (id, connMsgReq) + } else { + nil } + } else { + // wait for another instance processing the same connection entity + logger.debug("NotificationService thread \(t, privacy: .private): receiveNtfMessages: entity \(id, privacy: .private) waiting on semaphore") + // this semaphore will be released by signalNextThread function, that looks up the instance + // waiting for the connection entity via activeThreads in NSEThreads + notificationEntities[id]?.semaphore.wait() + logger.debug("NotificationService thread \(t, privacy: .private): receiveNtfMessages: entity \(id, privacy: .private) proceeding after semaphore") + Task { + // process any notifications "postponed" by the previous instance + let completed = processDroppedNotifications(ntfEntity, expectedMsg) + // Request messages from the server for this connection entity. + // It triggers event delivery to receiveMessages loop (see above). + if !completed, let connMsgReq = notificationEntities[id]?.connMsgReq, + let rcvMsg = getConnNtfMessage(connMsgReq: connMsgReq), + rcvMsg.noMsg { + // if server returns error or "no message", deliver what we have for this connection entity. + finalizeEntity(id) // also releases any waiting threads for this entity + } + } + return nil + } + } else { // no expected message + notificationEntities[id] = ntfEntity + return nil + } + } + + // Request messages for all connection entities that were not used by other instances. + // It triggers event delivery to receiveMessages loop (see above). + if !msgReqs.isEmpty, + let rcvMsgs = apiGetConnNtfMessages(connMsgReqs: msgReqs.map { $0.connMsgReq }) { + for i in 0 ..< min(msgReqs.count, rcvMsgs.count) { // a sanity check, API always returns the same size + if rcvMsgs[i].noMsg { + // mark entity as ready if there are no message on the server (or on error) + finalizeEntity(msgReqs[i].chatId) } - return } } } else if let dbStatus = dbStatus { - setBestAttemptNtf(createErrorNtf(dbStatus)) + setServiceBestAttemptNtf(createErrorNtf(dbStatus, badgeCount)) } } + // try to deliver the best attempt before exiting deliverBestAttemptNtf() } + @inline(__always) + func mkNotificationEntity(ntfConn: NtfConn) -> NotificationEntity? { + if let rcvEntityId = ntfConn.connEntity.id { + // don't receive messages for muted user profile + let expectedMsg: NtfMsgInfo? = if ntfConn.user.showNotifications { ntfConn.expectedMsg_ } else { nil } + return NotificationEntity( + ntfConn: ntfConn, + entityId: rcvEntityId, + expectedMsg: expectedMsg, + msgBestAttemptNtf: defaultBestAttemptNtf(ntfConn) + ) + } + return nil + } + + // Processes notifications received and postponed by the previous NSE instance + func processDroppedNotifications(_ ntfEntity: NotificationEntity, _ expectedMsg: NtfMsgInfo) -> Bool { + var completed = false + while !completed { + if let dropped = NSEThreads.shared.takeDroppedNtf(ntfEntity) { + completed = processReceivedNtf(ntfEntity, expectedMsg, dropped.ntf) + } else { + break + } + } + if completed { + finalizeEntity(ntfEntity.entityId) + } else { + notificationEntities[ntfEntity.entityId]?.shouldProcessNtf = true + } + return completed + } + override func serviceExtensionTimeWillExpire() { logger.debug("DEBUGGING: NotificationService.serviceExtensionTimeWillExpire") + deliverBestAttemptNtf(urgent: true) + } + + @inline(__always) + var expectingMoreMessages: Bool { + notificationEntities.contains { $0.value.expectedMsg != nil } + } + + // processReceivedNtf returns "completed" - true when no more messages for the passed entity should be processed by the current NSE instance. + // This is used to call finalizeEntity(id) and by processDroppedNotifications to decide if further processing is needed. + func processReceivedNtf(_ ntfEntity: NotificationEntity, _ expectedMsg: NtfMsgInfo, _ ntf: NSENotificationData) -> Bool { + let id = ntfEntity.entityId + if case let .msgInfo(info) = ntf { + if info.msgId == expectedMsg.msgId { + // The message for this instance is processed, no more expected, deliver. + logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): expected") + return true + } else if let msgTs = info.msgTs_, msgTs > expectedMsg.msgTs { + // Otherwise check timestamp - if it is after the currently expected timestamp, preserve .msgInfo marker for the next instance. + logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unexpected msgInfo, let other instance to process it, stopping this one") + NSEThreads.shared.addDroppedNtf(id, ntf) + return true + } else if ntfEntity.allowedGetNextAttempts > 0, let connMsgReq = ntfEntity.connMsgReq { + // Otherwise this instance expects more messages, and still has allowed attempts - + // request more messages with getConnNtfMessage. + logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unexpected msgInfo, get next message") + notificationEntities[id]?.allowedGetNextAttempts -= 1 + let receivedMsg = getConnNtfMessage(connMsgReq: connMsgReq) + if case let .info(msg) = receivedMsg, let msg { + // Server delivered message, it will be processed in the loop - see the comments in receiveNtfMessages. + logger.debug("NotificationService processNtf, on getConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private), receivedMsg msgId = \(msg.msgId, privacy: .private)") + return false + } else { + // Server reported no messages or error, deliver what we have. + logger.debug("NotificationService processNtf, on getConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private): no next message, deliver best attempt") + return true + } + } else { + // Current instance needs more messages, but ran out of attempts - deliver what we have. + logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unknown message, let other instance to process it") + return true + } + } else if ntfEntity.ntfConn.user.showNotifications { + // This is the notification event for the user with enabled notifications. + logger.debug("NotificationService processNtf: setting best attempt") + if ntf.notificationEvent != nil { + setBadgeCount() + } + // If previous "best attempt" is not a call, or if the current notification is a call, replace best attempt. + // NOTE: we are delaying it until notification marker to make sure we are not delivering stale calls that can't be connected. + // A better logic could be to check whether we have a call in the best attempt while processing .msgInfo marker above. + // If the best attempt is a call, and its marker is received, and the call is recent (e.g., the last 30 seconds), it would deliver at once, + // instead of requesting further messages. + if ntfEntity.msgBestAttemptNtf.callInvitation == nil || ntf.callInvitation != nil { + notificationEntities[id]?.msgBestAttemptNtf = ntf + } // otherwise keep call as best attempt + return false + } else { + // We should not get to this branch, as notifications are not delivered for muted users. + return true + } + } + + func finalizeEntity(_ entityId: ChatId) { + if let t = threadId { logger.debug("NotificationService thread \(t): entityReady: entity \(entityId)") } + NSEThreads.shared.signalNextThread(self, entityId) deliverBestAttemptNtf() } @@ -305,75 +588,185 @@ class NotificationService: UNNotificationServiceExtension { ntfBadgeCountGroupDefault.set(badgeCount) } - func setBestAttemptNtf(_ ntf: UNMutableNotificationContent) { - setBestAttemptNtf(.nse(ntf)) + @inline(__always) + func setServiceBestAttemptNtf(_ ntf: UNMutableNotificationContent) { + logger.debug("NotificationService.setServiceBestAttemptNtf") + serviceBestAttemptNtf = ntf } - func setBestAttemptNtf(_ ntf: NSENotification) { - logger.debug("NotificationService.setBestAttemptNtf") - if case let .nse(notification) = ntf { - notification.badge = badgeCount as NSNumber - bestAttemptNtf = .nse(notification) + private func deliverBestAttemptNtf(urgent: Bool = false) { + logger.debug("NotificationService.deliverBestAttemptNtf urgent: \(urgent) expectingMoreMessages: \(self.expectingMoreMessages)") + if let handler = contentHandler, urgent || !expectingMoreMessages { + if urgent { + contentHandler = nil + } + logger.debug("NotificationService.deliverBestAttemptNtf") + // stop processing other messages + for (key, _) in notificationEntities { + notificationEntities[key]?.shouldProcessNtf = false + } + + let suspend: Bool + if let t = threadId { + threadId = nil + suspend = NSEThreads.shared.endThread(t) && NSEThreads.shared.noThreads + } else { + suspend = false + } + deliverCallkitOrNotification(urgent: urgent, suspend: suspend, handler: handler) + } + } + + @inline(__always) + private func deliverCallkitOrNotification(urgent: Bool, suspend: Bool = false, handler: @escaping (UNNotificationContent) -> Void) { + let callInv = notificationEntities.lazy.compactMap({ $0.value.msgBestAttemptNtf.callInvitation }).first + if callInv != nil && useCallKit() { + logger.debug("NotificationService.deliverCallkitOrNotification: will suspend, callkit") + // suspending NSE even though there may be other notifications + // to allow the app to process callkit call + if urgent { + suspendChat(0) + deliverNotification(handler, callInv) + } else { + // when not "urgent", suspending NSE with delay and delivering after the suspension + // because pushkit notification must be processed without delay + // to avoid app termination. + DispatchQueue.global().asyncAfter(deadline: .now() + fastNSESuspendSchedule.delay) { + suspendChat(fastNSESuspendSchedule.timeout) + DispatchQueue.global().asyncAfter(deadline: .now() + Double(fastNSESuspendSchedule.timeout)) { + self.deliverNotification(handler, callInv) + } + } + } } else { - bestAttemptNtf = ntf + if suspend { + logger.debug("NotificationService.deliverCallkitOrNotification: will suspend") + if urgent { + suspendChat(0) + } else { + // suspension is delayed to allow chat core finalise any processing + // (e.g., send delivery receipts) + DispatchQueue.global().asyncAfter(deadline: .now() + nseSuspendSchedule.delay) { + if NSEThreads.shared.noThreads { + suspendChat(nseSuspendSchedule.timeout) + } + } + } + } + deliverNotification(handler, callInv) } } - private func deliverBestAttemptNtf() { - logger.debug("NotificationService.deliverBestAttemptNtf") - if let cancel = cancelRead { - cancelRead = nil - cancel() - } - if let id = receiveEntityId { - receiveEntityId = nil - NtfStreamSemaphores.shared.signalStreamReady(id) - } - if let t = threadId { - threadId = nil - if NSEThreads.shared.endThread(t) { - logger.debug("NotificationService.deliverBestAttemptNtf: will suspend") - // suspension is delayed to allow chat core finalise any processing - // (e.g., send delivery receipts) - DispatchQueue.global().asyncAfter(deadline: .now() + nseSuspendDelay) { - if NSEThreads.shared.noThreads { - logger.debug("NotificationService.deliverBestAttemptNtf: suspending...") - suspendChat(nseSuspendTimeout) - } - } - } - } - if let handler = contentHandler, let ntf = bestAttemptNtf { + private func deliverNotification(_ handler: @escaping (UNNotificationContent) -> Void, _ callInv: RcvCallInvitation?) { + if let serviceNtf = serviceBestAttemptNtf { + serviceBestAttemptNtf = nil contentHandler = nil - bestAttemptNtf = nil - let deliver: (UNMutableNotificationContent?) -> Void = { ntf in - let useNtf = if let ntf = ntf { - appStateGroupDefault.get().running ? UNMutableNotificationContent() : ntf - } else { - UNMutableNotificationContent() - } - handler(useNtf) - } - switch ntf { - case let .nse(content): deliver(content) - case let .callkit(invitation): - CXProvider.reportNewIncomingVoIPPushPayload([ - "displayName": invitation.contact.displayName, - "contactId": invitation.contact.id, - "media": invitation.callType.media.rawValue - ]) { error in - if error == nil { - deliver(nil) - } else { - logger.debug("NotificationService reportNewIncomingVoIPPushPayload success to CallController for \(invitation.contact.id)") - deliver(createCallInvitationNtf(invitation)) + if let callInv { + if useCallKit() { + logger.debug("NotificationService reportNewIncomingVoIPPushPayload for \(callInv.contact.id)") + CXProvider.reportNewIncomingVoIPPushPayload([ + "displayName": callInv.contact.displayName, + "contactId": callInv.contact.id, + "callUUID": callInv.callUUID ?? "", + "media": callInv.callType.media.rawValue, + "callTs": callInv.callTs.timeIntervalSince1970 + ]) { error in + logger.debug("reportNewIncomingVoIPPushPayload result: \(error)") + handler(error == nil ? UNMutableNotificationContent() : createCallInvitationNtf(callInv, self.badgeCount)) } + } else { + handler(createCallInvitationNtf(callInv, badgeCount)) } - case .empty: deliver(nil) // used to mute notifications that did not unsubscribe yet - case .msgInfo: deliver(nil) // unreachable, the best attempt is never set to msgInfo + } else if notificationEntities.isEmpty { + handler(serviceNtf) + } else { + handler(prepareNotification()) } } } + + @inline(__always) + private func prepareNotification() -> UNMutableNotificationContent { + // uncomment localDisplayName in ConnectionEntity + // let conns = self.notificationEntities.compactMap { $0.value.ntfConn.connEntity.localDisplayName } + // logger.debug("NotificationService prepareNotification for \(String(describing: conns))") + let ntfs = notificationEntities.compactMap { $0.value.msgBestAttemptNtf.notificationEvent } + let newMsgNtfs = ntfs.compactMap({ $0.newMsgNtf }) + let useNtfs = if newMsgNtfs.isEmpty { ntfs } else { newMsgNtfs } + return createNtf(useNtfs) + + func createNtf(_ ntfs: [NSENotificationData]) -> UNMutableNotificationContent { + logger.debug("NotificationService prepareNotification: \(ntfs.count) events") + return switch ntfs.count { + case 0: UNMutableNotificationContent() // used to mute notifications that did not unsubscribe yet + case 1: ntfs[0].notificationContent(badgeCount) + default: createJointNtf(ntfs) + } + } + } + + // NOTE: this can be improved when there are two or more connection entity events when no messages were delivered. + // Possibly, it is better to postpone this improvement until message priority is added to prevent notifications in muted groups, + // unless it is a mention, a reply or some other high priority message marked for notification delivery. + @inline(__always) + private func createJointNtf(_ ntfs: [NSENotificationData]) -> UNMutableNotificationContent { + let previewMode = ntfPreviewModeGroupDefault.get() + logger.debug("NotificationService.createJointNtf ntfs: \(ntfs.count)") + let (userId, chatsNames) = newMsgsChatsNames(ntfs) + if !chatsNames.isEmpty, let userId { + let body = if previewMode == .hidden { + String.localizedStringWithFormat(NSLocalizedString("From %d chat(s)", comment: "notification body"), chatsNames.count) + } else { + String.localizedStringWithFormat(NSLocalizedString("From: %@", comment: "notification body"), newMsgsChatsNamesStr(chatsNames)) + } + return createNotification( + categoryIdentifier: ntfCategoryManyEvents, + title: NSLocalizedString("New messages", comment: "notification"), + body: body, + userInfo: ["userId": userId], + badgeCount: badgeCount + ) + } else { + return createNotification( + categoryIdentifier: ntfCategoryManyEvents, + title: NSLocalizedString("New events", comment: "notification"), + body: String.localizedStringWithFormat(NSLocalizedString("%d new events", comment: "notification body"), ntfs.count), + badgeCount: badgeCount + ) + } + } + + @inline(__always) + private func newMsgsChatsNames(_ ntfs: [NSENotificationData]) -> (Int64?, [String]) { + var seenChatIds = Set<ChatId>() + var chatsNames: [String] = [] + var userId: Int64? + for ntf in ntfs { + switch ntf { + case let .messageReceived(user, chat, _): + if seenChatIds.isEmpty { userId = user.userId } + if !seenChatIds.contains(chat.id) { + seenChatIds.insert(chat.id) + chatsNames.append(chat.chatViewName) + } + default: () + } + } + return (userId, chatsNames) + } + + @inline(__always) + private func newMsgsChatsNamesStr(_ names: [String]) -> String { + return switch names.count { + case 1: names[0] + case 2: "\(names[0]) and \(names[1])" + case 3: "\(names[0] + ", " + names[1]) and \(names[2])" + default: + names.count > 3 + ? "\(names[0]), \(names[1]) and \(names.count - 2) other chats" + : "" + } + } } // nseStateGroupDefault must not be used in NSE directly, only via this singleton @@ -381,9 +774,8 @@ class NSEChatState { static let shared = NSEChatState() private var value_ = NSEState.created - var value: NSEState { - value_ - } + @inline(__always) + var value: NSEState { value_ } func set(_ state: NSEState) { nseStateGroupDefault.set(state) @@ -391,7 +783,7 @@ class NSEChatState { value_ = state } - init() { + private init() { // This is always set to .created state, as in case previous start of NSE crashed in .active state, it is stored correctly. // Otherwise the app will be activating slower set(.created) @@ -402,47 +794,65 @@ var appSubscriber: AppSubscriber = appStateSubscriber { state in logger.debug("NotificationService: appSubscriber") if state.running && NSEChatState.shared.value.canSuspend { logger.debug("NotificationService: appSubscriber app state \(state.rawValue), suspending") - suspendChat(nseSuspendTimeout) + suspendChat(fastNSESuspendSchedule.timeout) } } func appStateSubscriber(onState: @escaping (AppState) -> Void) -> AppSubscriber { appMessageSubscriber { msg in if case let .state(state) = msg { - logger.debug("NotificationService: appStateSubscriber \(state.rawValue, privacy: .public)") + logger.debug("NotificationService: appStateSubscriber \(state.rawValue)") onState(state) } } } +let seSubscriber = seMessageSubscriber { + switch $0 { + case let .state(state): + if state == .sendingMessage && NSEChatState.shared.value.canSuspend { + logger.debug("NotificationService: seSubscriber app state \(state.rawValue), suspending") + suspendChat(fastNSESuspendSchedule.timeout) + } + } +} + var receiverStarted = false let startLock = DispatchSemaphore(value: 1) let suspendLock = DispatchSemaphore(value: 1) var networkConfig: NetCfg = getNetCfg() -let xftpConfig: XFTPFileConfig? = getXFTPCfg() // startChat uses semaphore startLock to ensure that only one didReceive thread can start chat controller // Subsequent calls to didReceive will be waiting on semaphore and won't start chat again, as it will be .active func startChat() -> DBMigrationResult? { logger.debug("NotificationService: startChat") - if case .active = NSEChatState.shared.value { return .ok } + // only skip creating if there is chat controller + if case .active = NSEChatState.shared.value, hasChatCtrl() { return .ok } startLock.wait() defer { startLock.signal() } - - return switch NSEChatState.shared.value { - case .created: doStartChat() - case .starting: .ok // it should never get to this branch, as it would be waiting for start on startLock - case .active: .ok - case .suspending: activateChat() - case .suspended: activateChat() + + if hasChatCtrl() { + return switch NSEChatState.shared.value { + case .created: doStartChat() + case .starting: .ok // it should never get to this branch, as it would be waiting for start on startLock + case .active: .ok + case .suspending: activateChat() + case .suspended: activateChat() + } + } else { + // Ignore state in preference if there is no chat controller. + // State in preference may have failed to update e.g. because of a crash. + NSEChatState.shared.set(.created) + return doStartChat() } } func doStartChat() -> DBMigrationResult? { logger.debug("NotificationService: doStartChat") - hs_init(0, nil) + haskell_init_nse() let (_, dbStatus) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation(), backgroundMode: true) + logger.debug("NotificationService: doStartChat \(String(describing: dbStatus))") if dbStatus != .ok { resetChatCtrl() NSEChatState.shared.set(.created) @@ -451,12 +861,10 @@ func doStartChat() -> DBMigrationResult? { let state = NSEChatState.shared.value NSEChatState.shared.set(.starting) if let user = apiGetActiveUser() { - logger.debug("NotificationService active user \(String(describing: user))") + logger.debug("NotificationService active user \(user.displayName)") do { try setNetworkConfig(networkConfig) - try apiSetTempFolder(tempFolder: getTempFilesDirectory().path) - try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) - try setXFTPConfig(xftpConfig) + try apiSetAppFilePaths(filesFolder: getAppFilesDirectory().path, tempFolder: getTempFilesDirectory().path, assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path) try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get()) // prevent suspension while starting chat suspendLock.wait() @@ -477,7 +885,7 @@ func doStartChat() -> DBMigrationResult? { return .ok } } catch { - logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)") + logger.error("NotificationService startChat error: \(responseError(error))") } } else { logger.debug("NotificationService: no active user") @@ -504,8 +912,10 @@ func suspendChat(_ timeout: Int) { logger.debug("NotificationService: suspendChat") let state = NSEChatState.shared.value if !state.canSuspend { - logger.error("NotificationService suspendChat called, current state: \(state.rawValue, privacy: .public)") - } else { + logger.error("NotificationService suspendChat called, current state: \(state.rawValue)") + } else if hasChatCtrl() { + // only suspend if we have chat controller to avoid crashes when suspension is + // attempted when chat controller was not created suspendLock.wait() defer { suspendLock.signal() } @@ -529,7 +939,7 @@ func chatSuspended() { } // A single loop is used per Notification service extension process to receive and process all messages depending on the NSE state -// If the extension is not active yet, or suspended/suspending, or the app is running, the notifications will no be received. +// If the extension is not active yet, or suspended/suspending, or the app is running, the notifications will not be received. func receiveMessages() async { logger.debug("NotificationService receiveMessages") while true { @@ -544,13 +954,18 @@ func receiveMessages() async { } func receiveMsg() async { - if let msg = await chatRecvMsg() { + switch await chatRecvMsg() { + case let .result(msg): logger.debug("NotificationService receiveMsg: message") if let (id, ntf) = await receivedMsgNtf(msg) { logger.debug("NotificationService receiveMsg: notification") - await PendingNtfs.shared.createStream(id) - await PendingNtfs.shared.writeStream(id, ntf) + await NSEThreads.shared.processNotification(id, ntf) } + case let .error(err): + logger.error("NotificationService receivedMsgNtf error: \(String(describing: err))") + case let .invalid(type, _): + logger.error("NotificationService receivedMsgNtf invalid: \(type)") + case .none: () } } @@ -560,36 +975,46 @@ func receiveMessages() async { } } -func chatRecvMsg() async -> ChatResponse? { +func chatRecvMsg() async -> APIResult<NSEChatEvent>? { await withCheckedContinuation { cont in - let resp = recvSimpleXMsg() + let resp: APIResult<NSEChatEvent>? = recvSimpleXMsg() cont.resume(returning: resp) } } private let isInChina = SKStorefront().countryCode == "CHN" + +@inline(__always) private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() } -func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? { - logger.debug("NotificationService receivedMsgNtf: \(res.responseType, privacy: .public)") +@inline(__always) +func receivedMsgNtf(_ res: NSEChatEvent) async -> (String, NSENotificationData)? { + logger.debug("NotificationService receivedMsgNtf: \(res.responseType)") switch res { case let .contactConnected(user, contact, _): - return (contact.id, .nse(createContactConnectedNtf(user, contact))) + return (contact.id, .contactConnected(user, contact)) // case let .contactConnecting(contact): // TODO profile update case let .receivedContactRequest(user, contactRequest): - return (UserContact(contactRequest: contactRequest).id, .nse(createContactRequestNtf(user, contactRequest))) - case let .newChatItem(user, aChatItem): - let cInfo = aChatItem.chatInfo - var cItem = aChatItem.chatItem - if !cInfo.ntfsEnabled { - ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1)) + return (UserContact(contactRequest: contactRequest).id, .contactRequest(user, contactRequest)) + case let .newChatItems(user, chatItems): + // Received items are created one at a time + if let chatItem = chatItems.first { + let cInfo = chatItem.chatInfo + var cItem = chatItem.chatItem + if let file = cItem.autoReceiveFile() { + cItem = autoReceiveFile(file) ?? cItem + } + let ntf: NSENotificationData = (cInfo.ntfsEnabled(chatItem: cItem) && cItem.showNotification) ? .messageReceived(user, cInfo, cItem) : .noNtf + let chatIdOrMemberId = if case let .groupRcv(groupMember) = chatItem.chatItem.chatDir { + groupMember.id + } else { + chatItem.chatInfo.id + } + return (chatIdOrMemberId, ntf) + } else { + return nil } - if let file = cItem.autoReceiveFile() { - cItem = autoReceiveFile(file, encrypted: cItem.encryptLocalFile) ?? cItem - } - let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(createMessageReceivedNtf(user, cInfo, cItem)) : .empty - return cItem.showNotification ? (aChatItem.chatId, ntf) : nil case let .rcvFileSndCancelled(_, aChatItem, _): cleanupFile(aChatItem) return nil @@ -597,28 +1022,22 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? { cleanupDirectFile(aChatItem) return nil case let .sndFileRcvCancelled(_, aChatItem, _): - cleanupDirectFile(aChatItem) - return nil - case let .sndFileCompleteXFTP(_, aChatItem, _): - cleanupFile(aChatItem) + if let aChatItem = aChatItem { + cleanupDirectFile(aChatItem) + } return nil case let .callInvitation(invitation): // Do not post it without CallKit support, iOS will stop launching the app without showing CallKit - return ( - invitation.contact.id, - useCallKit() ? .callkit(invitation) : .nse(createCallInvitationNtf(invitation)) - ) + return (invitation.contact.id, .callInvitation(invitation)) case let .ntfMessage(_, connEntity, ntfMessage): return if let id = connEntity.id { (id, .msgInfo(ntfMessage)) } else { nil } case .chatSuspended: chatSuspended() return nil - default: - logger.debug("NotificationService receivedMsgNtf ignored event: \(res.responseType)") - return nil } } +@inline(__always) func updateNetCfg() { let newNetConfig = getNetCfg() if newNetConfig != networkConfig { @@ -627,17 +1046,22 @@ func updateNetCfg() { try setNetworkConfig(networkConfig) networkConfig = newNetConfig } catch { - logger.error("NotificationService apply changed network config error: \(responseError(error), privacy: .public)") + logger.error("NotificationService apply changed network config error: \(responseError(error))") } } } func apiGetActiveUser() -> User? { - let r = sendSimpleXCmd(.showActiveUser) - logger.debug("apiGetActiveUser sendSimpleXCmd response: \(String(describing: r))") + let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.showActiveUser) + logger.debug("apiGetActiveUser sendSimpleXCmd response: \(r.responseType)") switch r { - case let .activeUser(user): return user - case .chatCmdError(_, .error(.noActiveUser)): return nil + case let .result(.activeUser(user)): return user + case .error(.error(.noActiveUser)): + logger.debug("apiGetActiveUser sendSimpleXCmd no active user") + return nil + case let .error(err): + logger.debug("apiGetActiveUser sendSimpleXCmd error: \(String(describing: err))") + return nil default: logger.error("NotificationService apiGetActiveUser unexpected response: \(String(describing: r))") return nil @@ -645,105 +1069,135 @@ func apiGetActiveUser() -> User? { } func apiStartChat() throws -> Bool { - let r = sendSimpleXCmd(.startChat(subscribe: false, expire: false, xftp: false)) + let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.startChat(mainApp: false, enableSndFiles: false)) switch r { - case .chatStarted: return true - case .chatRunning: return false - default: throw r + case .result(.chatStarted): return true + case .result(.chatRunning): return false + default: throw r.unexpected } } func apiActivateChat() -> Bool { chatReopenStore() - let r = sendSimpleXCmd(.apiActivateChat(restoreChat: false)) - if case .cmdOk = r { return true } + let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiActivateChat(restoreChat: false)) + if case .result(.cmdOk) = r { return true } logger.error("NotificationService apiActivateChat error: \(String(describing: r))") return false } func apiSuspendChat(timeoutMicroseconds: Int) -> Bool { - let r = sendSimpleXCmd(.apiSuspendChat(timeoutMicroseconds: timeoutMicroseconds)) - if case .cmdOk = r { return true } + let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiSuspendChat(timeoutMicroseconds: timeoutMicroseconds)) + if case .result(.cmdOk) = r { return true } logger.error("NotificationService apiSuspendChat error: \(String(describing: r))") return false } -func apiSetTempFolder(tempFolder: String) throws { - let r = sendSimpleXCmd(.setTempFolder(tempFolder: tempFolder)) - if case .cmdOk = r { return } - throw r -} - -func apiSetFilesFolder(filesFolder: String) throws { - let r = sendSimpleXCmd(.setFilesFolder(filesFolder: filesFolder)) - if case .cmdOk = r { return } - throw r -} - -func setXFTPConfig(_ cfg: XFTPFileConfig?) throws { - let r = sendSimpleXCmd(.apiSetXFTPConfig(config: cfg)) - if case .cmdOk = r { return } - throw r +func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) throws { + let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder)) + if case .result(.cmdOk) = r { return } + throw r.unexpected } func apiSetEncryptLocalFiles(_ enable: Bool) throws { - let r = sendSimpleXCmd(.apiSetEncryptLocalFiles(enable: enable)) - if case .cmdOk = r { return } - throw r + let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiSetEncryptLocalFiles(enable: enable)) + if case .result(.cmdOk) = r { return } + throw r.unexpected } -func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? { +func apiGetNtfConns(nonce: String, encNtfInfo: String) -> [NtfConn]? { + guard apiGetActiveUser() != nil else { + logger.debug("NotificationService: no active user") + return nil + } + let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiGetNtfConns(nonce: nonce, encNtfInfo: encNtfInfo)) + if case let .result(.ntfConns(ntfConns)) = r { + logger.debug("NotificationService apiGetNtfConns response ntfConns: \(ntfConns.count) conections") + return ntfConns + } else if case let .error(error) = r { + logger.debug("NotificationService apiGetNtfMessage error response: \(String.init(describing: error))") + } else { + logger.debug("NotificationService apiGetNtfMessage ignored response: \(r.responseType) \(String.init(describing: r))") + } + return nil +} + +func apiGetConnNtfMessages(connMsgReqs: [ConnMsgReq]) -> [RcvNtfMsgInfo]? { guard apiGetActiveUser() != nil else { logger.debug("no active user") return nil } - let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo)) - if case let .ntfMessages(user, connEntity_, msgTs, ntfMessages) = r, let user = user { - return NtfMessages(user: user, connEntity_: connEntity_, msgTs: msgTs, ntfMessages: ntfMessages) - } else if case let .chatCmdError(_, error) = r { - logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))") - } else { - logger.debug("apiGetNtfMessage ignored response: \(r.responseType, privacy: .public) \(String.init(describing: r), privacy: .private)") +// logger.debug("NotificationService apiGetConnNtfMessages command: \(NSEChatCommand.apiGetConnNtfMessages(connMsgReqs: connMsgReqs).cmdString)") + logger.debug("NotificationService apiGetConnNtfMessages requests: \(connMsgReqs.count)") + let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiGetConnNtfMessages(connMsgReqs: connMsgReqs)) + if case let .result(.connNtfMessages(msgs)) = r { +// logger.debug("NotificationService apiGetConnNtfMessages responses: \(String(describing: msgs))") + logger.debug("NotificationService apiGetConnNtfMessages responses: total \(msgs.count), expecting messages \(msgs.count { !$0.noMsg }), errors \(msgs.count { $0.isError })") + return msgs } + logger.debug("NotificationService apiGetConnNtfMessages error: \(responseError(r.unexpected))") return nil } +func getConnNtfMessage(connMsgReq: ConnMsgReq) -> RcvNtfMsgInfo? { + let r = apiGetConnNtfMessages(connMsgReqs: [connMsgReq]) + return if let r, r.count > 0 { r[0] } else { nil } +} + func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil) -> AChatItem? { - let r = sendSimpleXCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline)) - if case let .rcvFileAccepted(_, chatItem) = r { return chatItem } - logger.error("receiveFile error: \(responseError(r))") + let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get() + let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline)) + if case let .result(.rcvFileAccepted(_, chatItem)) = r { return chatItem } + logger.error("receiveFile error: \(responseError(r.unexpected))") return nil } func apiSetFileToReceive(fileId: Int64, encrypted: Bool) { - let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId, encrypted: encrypted)) - if case .cmdOk = r { return } - logger.error("setFileToReceive error: \(responseError(r))") + let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get() + let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.setFileToReceive(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted)) + if case .result(.cmdOk) = r { return } + logger.error("setFileToReceive error: \(responseError(r.unexpected))") } -func autoReceiveFile(_ file: CIFile, encrypted: Bool) -> ChatItem? { +func autoReceiveFile(_ file: CIFile) -> ChatItem? { + let encrypted = privacyEncryptLocalFilesGroupDefault.get() switch file.fileProtocol { case .smp: return apiReceiveFile(fileId: file.fileId, encrypted: encrypted)?.chatItem case .xftp: apiSetFileToReceive(fileId: file.fileId, encrypted: encrypted) return nil + case .local: + return nil } } func setNetworkConfig(_ cfg: NetCfg) throws { - let r = sendSimpleXCmd(.apiSetNetworkConfig(networkConfig: cfg)) - if case .cmdOk = r { return } - throw r + let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiSetNetworkConfig(networkConfig: cfg)) + if case .result(.cmdOk) = r { return } + throw r.unexpected } -struct NtfMessages { - var user: User - var connEntity_: ConnectionEntity? - var msgTs: Date? - var ntfMessages: [NtfMsgInfo] - - var ntfsEnabled: Bool { - user.showNotifications && (connEntity_?.ntfsEnabled ?? false) +func defaultBestAttemptNtf(_ ntfConn: NtfConn) -> NSENotificationData { + let user = ntfConn.user + let connEntity = ntfConn.connEntity + return if !user.showNotifications { + .noNtf + } else { + switch ntfConn.connEntity { + case let .rcvDirectMsgConnection(_, contact): + contact?.chatSettings.enableNtfs == .all + ? .connectionEvent(user, connEntity) + : .noNtf + case let .rcvGroupMsgConnection(_, groupInfo, _): + groupInfo.chatSettings.enableNtfs == .all + ? .connectionEvent(user, connEntity) + : .noNtf + case .sndFileConnection: .noNtf + case .rcvFileConnection: .noNtf + case let .userContactConnection(_, userContact): + userContact.groupId == nil + ? .connectionEvent(user, connEntity) + : .noNtf + } } } diff --git a/apps/ios/SimpleX NSE/bg.lproj/Localizable.strings b/apps/ios/SimpleX NSE/bg.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/bg.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/cs.lproj/Localizable.strings b/apps/ios/SimpleX NSE/cs.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/cs.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/de.lproj/InfoPlist.strings b/apps/ios/SimpleX NSE/de.lproj/InfoPlist.strings index 9c675514f4..6cc768efe1 100644 --- a/apps/ios/SimpleX NSE/de.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX NSE/de.lproj/InfoPlist.strings @@ -5,5 +5,5 @@ "CFBundleName" = "SimpleX NSE"; /* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. All rights reserved."; diff --git a/apps/ios/SimpleX NSE/de.lproj/Localizable.strings b/apps/ios/SimpleX NSE/de.lproj/Localizable.strings new file mode 100644 index 0000000000..ec502c53c6 --- /dev/null +++ b/apps/ios/SimpleX NSE/de.lproj/Localizable.strings @@ -0,0 +1,15 @@ +/* notification body */ +"%d new events" = "%d neue Ereignisse"; + +/* notification body */ +"From %d chat(s)" = "Von %d Chat(s)"; + +/* notification body */ +"From: %@" = "Von: %@"; + +/* notification */ +"New events" = "Neue Ereignisse"; + +/* notification */ +"New messages" = "Neue Nachrichten"; + diff --git a/apps/ios/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX NSE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..9c675514f4 --- /dev/null +++ b/apps/ios/SimpleX NSE/en.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/es.lproj/Localizable.strings b/apps/ios/SimpleX NSE/es.lproj/Localizable.strings new file mode 100644 index 0000000000..685eb3d93d --- /dev/null +++ b/apps/ios/SimpleX NSE/es.lproj/Localizable.strings @@ -0,0 +1,15 @@ +/* notification body */ +"%d new events" = "%d evento(s) nuevo(s)"; + +/* notification body */ +"From %d chat(s)" = "De %d chat(s)"; + +/* notification body */ +"From: %@" = "De: %@"; + +/* notification */ +"New events" = "Eventos nuevos"; + +/* notification */ +"New messages" = "Mensajes nuevos"; + diff --git a/apps/ios/SimpleX NSE/fi.lproj/Localizable.strings b/apps/ios/SimpleX NSE/fi.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/fi.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings b/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings new file mode 100644 index 0000000000..999bb3608f --- /dev/null +++ b/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings @@ -0,0 +1,12 @@ +/* notification body */ +"%d new events" = "%d nouveaux événements"; + +/* notification body */ +"From: %@" = "De : %@"; + +/* notification */ +"New events" = "Nouveaux événements"; + +/* notification */ +"New messages" = "Nouveaux messages"; + diff --git a/apps/ios/SimpleX NSE/hu.lproj/InfoPlist.strings b/apps/ios/SimpleX NSE/hu.lproj/InfoPlist.strings new file mode 100644 index 0000000000..1a7b57b088 --- /dev/null +++ b/apps/ios/SimpleX NSE/hu.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. Minden jog fenntartva."; + diff --git a/apps/ios/SimpleX NSE/hu.lproj/Localizable.strings b/apps/ios/SimpleX NSE/hu.lproj/Localizable.strings new file mode 100644 index 0000000000..a6330b93db --- /dev/null +++ b/apps/ios/SimpleX NSE/hu.lproj/Localizable.strings @@ -0,0 +1,15 @@ +/* notification body */ +"%d new events" = "%d új esemény"; + +/* notification body */ +"From %d chat(s)" = "%d csevegésből"; + +/* notification body */ +"From: %@" = "Tőle: %@"; + +/* notification */ +"New events" = "Új események"; + +/* notification */ +"New messages" = "Új üzenetek"; + diff --git a/apps/ios/SimpleX NSE/it.lproj/Localizable.strings b/apps/ios/SimpleX NSE/it.lproj/Localizable.strings new file mode 100644 index 0000000000..a6c1ec215b --- /dev/null +++ b/apps/ios/SimpleX NSE/it.lproj/Localizable.strings @@ -0,0 +1,15 @@ +/* notification body */ +"%d new events" = "%d nuovi eventi"; + +/* notification body */ +"From %d chat(s)" = "Da %d chat"; + +/* notification body */ +"From: %@" = "Da: %@"; + +/* notification */ +"New events" = "Nuovi eventi"; + +/* notification */ +"New messages" = "Nuovi messaggi"; + diff --git a/apps/ios/SimpleX NSE/ja.lproj/Localizable.strings b/apps/ios/SimpleX NSE/ja.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/ja.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings b/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings new file mode 100644 index 0000000000..12d1e01f1d --- /dev/null +++ b/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings @@ -0,0 +1,12 @@ +/* notification body */ +"%d new events" = "‐%d nieuwe gebeurtenissen"; + +/* notification body */ +"From: %@" = "Van: %@"; + +/* notification */ +"New events" = "Nieuwe gebeurtenissen"; + +/* notification */ +"New messages" = "Nieuwe berichten"; + diff --git a/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings b/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings new file mode 100644 index 0000000000..3a577620a0 --- /dev/null +++ b/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings @@ -0,0 +1,3 @@ +/* notification body */ +"New messages in %d chats" = "Nowe wiadomości w %d czatach"; + diff --git a/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings new file mode 100644 index 0000000000..7205b37e7f --- /dev/null +++ b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings @@ -0,0 +1,12 @@ +/* notification body */ +"%d new events" = "%d новых сообщений"; + +/* notification body */ +"From: %@" = "От: %@"; + +/* notification */ +"New events" = "Новые события"; + +/* notification */ +"New messages" = "Новые сообщения"; + diff --git a/apps/ios/SimpleX NSE/th.lproj/Localizable.strings b/apps/ios/SimpleX NSE/th.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/th.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/tr.lproj/InfoPlist.strings b/apps/ios/SimpleX NSE/tr.lproj/InfoPlist.strings new file mode 100644 index 0000000000..476da9ed91 --- /dev/null +++ b/apps/ios/SimpleX NSE/tr.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Telif Hakkı © 2024 SimpleX Chat. Tüm hakları saklıdır."; + diff --git a/apps/ios/SimpleX NSE/tr.lproj/Localizable.strings b/apps/ios/SimpleX NSE/tr.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/tr.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings b/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings new file mode 100644 index 0000000000..ceace71e34 --- /dev/null +++ b/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings @@ -0,0 +1,12 @@ +/* notification body */ +"%d new events" = "%d нових подій"; + +/* notification body */ +"From: %@" = "Від: %@"; + +/* notification */ +"New events" = "Нові події"; + +/* notification */ +"New messages" = "Нові повідомлення"; + diff --git a/apps/ios/SimpleX NSE/zh-Hans.lproj/Localizable.strings b/apps/ios/SimpleX NSE/zh-Hans.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/Info.plist b/apps/ios/SimpleX SE/Info.plist new file mode 100644 index 0000000000..2ce1f45040 --- /dev/null +++ b/apps/ios/SimpleX SE/Info.plist @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>NSExtension</key> + <dict> + <key>NSExtensionAttributes</key> + <dict> + <key>NSExtensionActivationRule</key> + <dict> + <key>NSExtensionActivationSupportsText</key> + <true/> + <key>NSExtensionActivationSupportsAttachmentsWithMinCount</key> + <integer>0</integer> + <key>NSExtensionActivationSupportsAttachmentsWithMaxCount</key> + <integer>1</integer> + <key>NSExtensionActivationSupportsWebPageWithMaxCount</key> + <integer>1</integer> + <key>NSExtensionActivationSupportsWebURLWithMaxCount</key> + <integer>1</integer> + <key>NSExtensionActivationSupportsFileWithMaxCount</key> + <integer>1</integer> + <key>NSExtensionActivationSupportsImageWithMaxCount</key> + <integer>1</integer> + <key>NSExtensionActivationSupportsMovieWithMaxCount</key> + <integer>1</integer> + </dict> + </dict> + <key>NSExtensionPointIdentifier</key> + <string>com.apple.share-services</string> + <key>NSExtensionPrincipalClass</key> + <string>ShareViewController</string> + </dict> +</dict> +</plist> diff --git a/apps/ios/SimpleX SE/SEChatState.swift b/apps/ios/SimpleX SE/SEChatState.swift new file mode 100644 index 0000000000..581bff894a --- /dev/null +++ b/apps/ios/SimpleX SE/SEChatState.swift @@ -0,0 +1,39 @@ +// +// SEChatState.swift +// SimpleX SE +// +// Created by User on 18/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +// SEStateGroupDefault must not be used in the share extension directly, only via this singleton +class SEChatState { + static let shared = SEChatState() + private var value_ = seStateGroupDefault.get() + + var value: SEState { + value_ + } + + func set(_ state: SEState) { + seStateGroupDefault.set(state) + sendSEState(state) + value_ = state + } +} + +/// Waits for other processes to set their state to suspended +/// Will wait for maximum of two seconds, since they might not be running +func waitForOtherProcessesToSuspend() async { + let startTime = CFAbsoluteTimeGetCurrent() + while CFAbsoluteTimeGetCurrent() - startTime < 2 { + try? await Task.sleep(nanoseconds: 100 * NSEC_PER_MSEC) + if appStateGroupDefault.get() == .suspended && + nseStateGroupDefault.get() == .suspended { + break + } + } +} diff --git a/apps/ios/SimpleX SE/ShareAPI.swift b/apps/ios/SimpleX SE/ShareAPI.swift new file mode 100644 index 0000000000..3e901c73eb --- /dev/null +++ b/apps/ios/SimpleX SE/ShareAPI.swift @@ -0,0 +1,226 @@ +// +// ShareAPI.swift +// SimpleX SE +// +// Created by User on 15/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import OSLog +import Foundation +import SimpleXChat + +let logger = Logger() + +func apiGetActiveUser() throws -> User? { + let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.showActiveUser) + switch r { + case let .result(.activeUser(user)): return user + case .error(.error(.noActiveUser)): return nil + default: throw r.unexpected + } +} + +func apiStartChat() throws -> Bool { + let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.startChat(mainApp: false, enableSndFiles: true)) + switch r { + case .result(.chatStarted): return true + case .result(.chatRunning): return false + default: throw r.unexpected + } +} + +func apiSetNetworkConfig(_ cfg: NetCfg) throws { + let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiSetNetworkConfig(networkConfig: cfg)) + if case .result(.cmdOk) = r { return } + throw r.unexpected +} + +func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) throws { + let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder)) + if case .result(.cmdOk) = r { return } + throw r.unexpected +} + +func apiSetEncryptLocalFiles(_ enable: Bool) throws { + let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiSetEncryptLocalFiles(enable: enable)) + if case .result(.cmdOk) = r { return } + throw r.unexpected +} + +func apiGetChats(userId: User.ID) throws -> Array<ChatData> { + let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiGetChats(userId: userId)) + if case let .result(.apiChats(user: _, chats: chats)) = r { return chats } + throw r.unexpected +} + +func apiSendMessages( + chatInfo: ChatInfo, + composedMessages: [ComposedMessage] +) throws -> [AChatItem] { + let r: APIResult<SEChatResponse> = sendSimpleXCmd( + chatInfo.chatType == .local + ? SEChatCommand.apiCreateChatItems( + noteFolderId: chatInfo.apiId, + composedMessages: composedMessages + ) + : SEChatCommand.apiSendMessages( + type: chatInfo.chatType, + id: chatInfo.apiId, + live: false, + ttl: nil, + composedMessages: composedMessages + ) + ) + if case let .result(.newChatItems(_, chatItems)) = r { + return chatItems + } else { + for composedMessage in composedMessages { + if let filePath = composedMessage.fileSource?.filePath { removeFile(filePath) } + } + throw r.unexpected + } +} + +func apiActivateChat() throws { + chatReopenStore() + let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiActivateChat(restoreChat: false)) + if case .result(.cmdOk) = r { return } + throw r.unexpected +} + +func apiSuspendChat(expired: Bool) { + let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiSuspendChat(timeoutMicroseconds: expired ? 0 : 3_000000)) + // Block until `chatSuspended` received or 3 seconds has passed + var suspended = false + if case .result(.cmdOk) = r, !expired { + let startTime = CFAbsoluteTimeGetCurrent() + while CFAbsoluteTimeGetCurrent() - startTime < 3 { + let msg: APIResult<SEChatEvent>? = recvSimpleXMsg(messageTimeout: 3_500000) + switch msg { + case .result(.chatSuspended): + suspended = false + break + default: continue + } + } + } + if !suspended { + let _r1: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiSuspendChat(timeoutMicroseconds: 0)) + } + logger.debug("close store") + chatCloseStore() + SEChatState.shared.set(.inactive) +} + +enum SEChatCommand: ChatCmdProtocol { + case showActiveUser + case startChat(mainApp: Bool, enableSndFiles: Bool) + case apiActivateChat(restoreChat: Bool) + case apiSuspendChat(timeoutMicroseconds: Int) + case apiSetNetworkConfig(networkConfig: NetCfg) + case apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) + case apiSetEncryptLocalFiles(enable: Bool) + case apiGetChats(userId: Int64) + case apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage]) + case apiSendMessages(type: ChatType, id: Int64, live: Bool, ttl: Int?, composedMessages: [ComposedMessage]) + + var cmdString: String { + switch self { + case .showActiveUser: return "/u" + case let .startChat(mainApp, enableSndFiles): return "/_start main=\(onOff(mainApp)) snd_files=\(onOff(enableSndFiles))" + case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))" + case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)" + case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))" + case let .apiSetAppFilePaths(filesFolder, tempFolder, assetsFolder): + return "/set file paths \(encodeJSON(AppFilePaths(appFilesFolder: filesFolder, appTempFolder: tempFolder, appAssetsFolder: assetsFolder)))" + case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))" + case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on" + case let .apiCreateChatItems(noteFolderId, composedMessages): + let msgs = encodeJSON(composedMessages) + return "/_create *\(noteFolderId) json \(msgs)" + case let .apiSendMessages(type, id, live, ttl, composedMessages): + let msgs = encodeJSON(composedMessages) + let ttlStr = ttl != nil ? "\(ttl!)" : "default" + return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)" + } + } + + func ref(_ type: ChatType, _ id: Int64) -> String { + "\(type.rawValue)\(id)" + } +} + +enum SEChatResponse: Decodable, ChatAPIResult { + case activeUser(user: User) + case chatStarted + case chatRunning + case apiChats(user: UserRef, chats: [ChatData]) + case newChatItems(user: UserRef, chatItems: [AChatItem]) + case cmdOk(user_: UserRef?) + + var responseType: String { + switch self { + case .activeUser: "activeUser" + case .chatStarted: "chatStarted" + case .chatRunning: "chatRunning" + case .apiChats: "apiChats" + case .newChatItems: "newChatItems" + case .cmdOk: "cmdOk" + } + } + + var details: String { + switch self { + case let .activeUser(user): return String(describing: user) + case .chatStarted: return noDetails + case .chatRunning: return noDetails + case let .apiChats(u, chats): return withUser(u, String(describing: chats)) + case let .newChatItems(u, chatItems): + let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n") + return withUser(u, itemsString) + case .cmdOk: return noDetails + } + } + + static func fallbackResult(_ type: String, _ json: NSDictionary) -> SEChatResponse? { + if type == "apiChats", let r = parseApiChats(json) { + .apiChats(user: r.user, chats: r.chats) + } else { + nil + } + } +} + +enum SEChatEvent: Decodable, ChatAPIResult { + case chatSuspended + case sndFileProgressXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64) + case sndFileCompleteXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta) + case chatItemsStatusesUpdated(user: UserRef, chatItems: [AChatItem]) + case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String) + case sndFileWarning(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String) + + var responseType: String { + switch self { + case .chatSuspended: "chatSuspended" + case .sndFileProgressXFTP: "sndFileProgressXFTP" + case .sndFileCompleteXFTP: "sndFileCompleteXFTP" + case .chatItemsStatusesUpdated: "chatItemsStatusesUpdated" + case .sndFileError: "sndFileError" + case .sndFileWarning: "sndFileWarning" + } + } + + var details: String { + switch self { + case .chatSuspended: return noDetails + case let .sndFileProgressXFTP(u, chatItem, _, sentSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nsentSize: \(sentSize)\ntotalSize: \(totalSize)") + case let .sndFileCompleteXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .chatItemsStatusesUpdated(u, chatItems): + let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n") + return withUser(u, itemsString) + case let .sndFileError(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))") + case let .sndFileWarning(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))") + } + } +} diff --git a/apps/ios/SimpleX SE/ShareModel.swift b/apps/ios/SimpleX SE/ShareModel.swift new file mode 100644 index 0000000000..12a775f85c --- /dev/null +++ b/apps/ios/SimpleX SE/ShareModel.swift @@ -0,0 +1,542 @@ +// +// ShareModel.swift +// SimpleX SE +// +// Created by Levitating Pineapple on 09/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import UniformTypeIdentifiers +import AVFoundation +import SwiftUI +import SimpleXChat + +/// Maximum size of hex encoded media previews +private let MAX_DATA_SIZE: Int64 = 14000 + +/// Maximum dimension (width or height) of an image, before passed for processing +private let MAX_DOWNSAMPLE_SIZE: Int64 = 2000 + +class ShareModel: ObservableObject { + @Published var sharedContent: SharedContent? + @Published var chats: [ChatData] = [] + @Published var profileImages: [ChatInfo.ID: UIImage] = [:] + @Published var search = "" + @Published var comment = "" + @Published var selected: ChatData? + @Published var isLoaded = false + @Published var bottomBar: BottomBar = .loadingSpinner + @Published var errorAlert: ErrorAlert? + @Published var hasSimplexLink = false + @Published var alertRequiresPassword = false + var networkTimeout = CFAbsoluteTimeGetCurrent() + + enum BottomBar { + case sendButton + case loadingSpinner + case loadingBar(progress: Double) + + var isLoading: Bool { + switch self { + case .sendButton: false + case .loadingSpinner: true + case .loadingBar: true + } + } + } + + var completion: () -> Void = { + fatalError("completion has not been set") + } + + private var itemProvider: NSItemProvider? + + var isSendDisbled: Bool { sharedContent == nil || selected == nil || isProhibited(selected) } + + var isLinkPreview: Bool { + switch sharedContent { + case .url: true + default: false + } + } + + func isProhibited(_ chat: ChatData?) -> Bool { + if let chat, let sharedContent { + sharedContent.prohibited(in: chat, hasSimplexLink: hasSimplexLink) + } else { false } + } + + var filteredChats: [ChatData] { + search.isEmpty + ? filterChatsToForwardTo(chats: chats) + : filterChatsToForwardTo(chats: chats) + .filter { foundChat($0, search.localizedLowercase) } + } + + func setup(context: NSExtensionContext) { + if appLocalAuthEnabledGroupDefault.get() && !allowShareExtensionGroupDefault.get() { + errorAlert = ErrorAlert(title: "App is locked!", message: "You can allow sharing in Privacy & Security / SimpleX Lock settings.") + return + } + if let item = context.inputItems.first as? NSExtensionItem, + let itemProvider = item.attachments?.first { + self.itemProvider = itemProvider + self.completion = { + ShareModel.CompletionHandler.isEventLoopEnabled = false + context.completeRequest(returningItems: [item]) { + apiSuspendChat(expired: $0) + } + } + setup() + } + } + + func setup(with dbKey: String? = nil) { + // Init Chat + Task { + if let e = initChat(with: dbKey) { + await MainActor.run { errorAlert = e } + } else { + // Load Chats + Task { + switch fetchChats() { + case let .success(chats): + // Decode base64 images on background thread + let profileImages = chats.reduce(into: Dictionary<ChatInfo.ID, UIImage>()) { dict, chatData in + if let profileImage = chatData.chatInfo.image, + let uiImage = imageFromBase64(profileImage) { + dict[chatData.id] = uiImage + } + } + await MainActor.run { + self.chats = chats + self.profileImages = profileImages + withAnimation { isLoaded = true } + } + case let .failure(error): + await MainActor.run { errorAlert = error } + } + } + // Process Attachment + Task { + switch await getSharedContent(self.itemProvider!) { + case let .success(chatItemContent): + await MainActor.run { + self.sharedContent = chatItemContent + self.bottomBar = .sendButton + if case let .text(string) = chatItemContent { comment = string } + } + case let .failure(errorAlert): + await MainActor.run { self.errorAlert = errorAlert } + } + } + } + } + } + + func send() { + if let sharedContent, let selected { + Task { + await MainActor.run { self.bottomBar = .loadingSpinner } + do { + SEChatState.shared.set(.sendingMessage) + await waitForOtherProcessesToSuspend() + let chatItems = try apiSendMessages( + chatInfo: selected.chatInfo, + composedMessages: [ComposedMessage(fileSource: sharedContent.cryptoFile, msgContent: sharedContent.msgContent(comment: self.comment))] + ) + if selected.chatInfo.chatType == .local { + completion() + } else { + // TODO batch send: share multiple items + if let ci = chatItems.first { + await MainActor.run { self.bottomBar = .loadingBar(progress: 0) } + if let e = await handleEvents( + isGroupChat: ci.chatInfo.chatType == .group, + isWithoutFile: sharedContent.cryptoFile == nil, + chatItemId: ci.chatItem.id + ) { + await MainActor.run { errorAlert = e } + } else { + completion() + } + } + } + } catch { + if let e = error as? ErrorAlert { + await MainActor.run { errorAlert = e } + } + } + } + } + } + + private func initChat(with dbKey: String? = nil) -> ErrorAlert? { + do { + if hasChatCtrl() && dbKey == nil { + try apiActivateChat() + } else { + resetChatCtrl() // Clears retained migration result + registerGroupDefaults() + haskell_init_se() + let (_, result) = chatMigrateInit(dbKey, confirmMigrations: defaultMigrationConfirmation(), backgroundMode: false) + if let e = migrationError(result) { return e } + try apiSetAppFilePaths( + filesFolder: getAppFilesDirectory().path, + tempFolder: getTempFilesDirectory().path, + assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path + ) + let isRunning = try apiStartChat() + logger.log(level: .debug, "chat started, running: \(isRunning)") + } + try apiSetNetworkConfig(getNetCfg()) + try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get()) + } catch { return ErrorAlert(error) } + return nil + } + + private func migrationError(_ r: DBMigrationResult) -> ErrorAlert? { + let useKeychain = storeDBPassphraseGroupDefault.get() + let storedDBKey = kcDatabasePassword.get() + if case .errorNotADatabase = r { + Task { await MainActor.run { self.alertRequiresPassword = true } } + } + return switch r { + case .errorNotADatabase: + if useKeychain && storedDBKey != nil && storedDBKey != "" { + ErrorAlert( + title: "Wrong database passphrase", + message: "Database passphrase is different from saved in the keychain." + ) + } else { + ErrorAlert( + title: "Database encrypted!", + message: "Database passphrase is required to open chat." + ) + } + case let .errorMigration(_, migrationError): + switch migrationError { + case .upgrade: + ErrorAlert( + title: "Database upgrade required", + message: "Open the app to upgrade the database." + ) + case .downgrade: + ErrorAlert( + title: "Database downgrade required", + message: "Open the app to downgrade the database." + ) + case let .migrationError(mtrError): + ErrorAlert( + title: "Incompatible database version", + message: mtrErrorDescription(mtrError) + ) + } + case let .errorSQL(_, migrationSQLError): + ErrorAlert( + title: "Database error", + message: "Error: \(migrationSQLError)" + ) + case .errorKeychain: + ErrorAlert( + title: "Keychain error", + message: "Cannot access keychain to save database password" + ) + case .invalidConfirmation: + ErrorAlert("Invalid migration confirmation") + case let .unknown(json): + ErrorAlert( + title: "Database error", + message: "Unknown database error: \(json)" + ) + case .ok: nil + } + } + + private func fetchChats() -> Result<Array<ChatData>, ErrorAlert> { + do { + guard let user = try apiGetActiveUser() else { + return .failure( + ErrorAlert( + title: "No active profile", + message: "Please create a profile in the SimpleX app" + ) + ) + } + return .success(try apiGetChats(userId: user.id)) + } catch { + return .failure(ErrorAlert(error)) + } + } + + actor CompletionHandler { + static var isEventLoopEnabled = false + private var fileCompleted = false + private var messageCompleted = false + + func completeFile() { fileCompleted = true } + + func completeMessage() { messageCompleted = true } + + var isRunning: Bool { + Self.isEventLoopEnabled && !(fileCompleted && messageCompleted) + } + } + + /// Polls and processes chat events + /// Returns when message sending has completed optionally returning and error. + private func handleEvents(isGroupChat: Bool, isWithoutFile: Bool, chatItemId: ChatItem.ID) async -> ErrorAlert? { + func isMessage(for item: AChatItem?) -> Bool { + item.map { $0.chatItem.id == chatItemId } ?? false + } + + CompletionHandler.isEventLoopEnabled = true + let ch = CompletionHandler() + if isWithoutFile { await ch.completeFile() } + networkTimeout = CFAbsoluteTimeGetCurrent() + while await ch.isRunning { + if CFAbsoluteTimeGetCurrent() - networkTimeout > 30 { + await MainActor.run { + self.errorAlert = ErrorAlert(title: "Slow network?", message: "Sending a message takes longer than expected.") { + Button("Wait", role: .cancel) { self.networkTimeout = CFAbsoluteTimeGetCurrent() } + Button("Cancel", role: .destructive) { self.completion() } + } + } + } + let r: APIResult<SEChatEvent>? = recvSimpleXMsg(messageTimeout: 1_000_000) + switch r { + case let .result(.sndFileProgressXFTP(_, ci, _, sentSize, totalSize)): + guard isMessage(for: ci) else { continue } + networkTimeout = CFAbsoluteTimeGetCurrent() + await MainActor.run { + withAnimation { + let progress = Double(sentSize) / Double(totalSize) + bottomBar = .loadingBar(progress: progress) + } + } + case let .result(.sndFileCompleteXFTP(_, ci, _)): + guard isMessage(for: ci) else { continue } + if isGroupChat { + await MainActor.run { bottomBar = .loadingSpinner } + } + await ch.completeFile() + if await !ch.isRunning { break } + case let .result(.chatItemsStatusesUpdated(_, chatItems)): + guard let ci = chatItems.last else { continue } + guard isMessage(for: ci) else { continue } + if let (title, message) = ci.chatItem.meta.itemStatus.statusInfo { + // `title` and `message` already localized and interpolated + return ErrorAlert( + title: "\(title)", + message: "\(message)" + ) + } else if case let .sndSent(sndProgress) = ci.chatItem.meta.itemStatus { + switch sndProgress { + case .complete: + await ch.completeMessage() + case .partial: + if isGroupChat { + Task { + try? await Task.sleep(nanoseconds: 5 * NSEC_PER_SEC) + await ch.completeMessage() + } + } + } + } + case let .result(.sndFileError(_, ci, _, errorMessage)): + guard isMessage(for: ci) else { continue } + if let ci { cleanupFile(ci) } + return ErrorAlert(title: "File error", message: "\(fileErrorInfo(ci) ?? errorMessage)") + case let .result(.sndFileWarning(_, ci, _, errorMessage)): + guard isMessage(for: ci) else { continue } + if let ci { cleanupFile(ci) } + return ErrorAlert(title: "File error", message: "\(fileErrorInfo(ci) ?? errorMessage)") + case let .error(chatError): + return ErrorAlert(chatError) + default: continue + } + } + return nil + } + + private func fileErrorInfo(_ ci: AChatItem?) -> String? { + switch ci?.chatItem.file?.fileStatus { + case let .sndError(e): e.errorInfo + case let .sndWarning(e): e.errorInfo + default: nil + } + } +} + +/// Chat Item Content extracted from `NSItemProvider` without the comment +enum SharedContent { + case image(preview: String, cryptoFile: CryptoFile) + case movie(preview: String, duration: Int, cryptoFile: CryptoFile) + case url(preview: LinkPreview) + case text(string: String) + case data(cryptoFile: CryptoFile) + + var cryptoFile: CryptoFile? { + switch self { + case let .image(_, cryptoFile): cryptoFile + case let .movie(_, _, cryptoFile): cryptoFile + case .url: nil + case .text: nil + case let .data(cryptoFile): cryptoFile + } + } + + func msgContent(comment: String) -> MsgContent { + switch self { + case let .image(preview, _): .image(text: comment, image: preview) + case let .movie(preview, duration, _): .video(text: comment, image: preview, duration: duration) + case let .url(preview): .link(text: preview.uri.absoluteString + (comment == "" ? "" : "\n" + comment), preview: preview) + case .text: .text(comment) + case .data: .file(comment) + } + } + + func prohibited(in chatData: ChatData, hasSimplexLink: Bool) -> Bool { + chatData.prohibitedByPref( + hasSimplexLink: hasSimplexLink, + isMediaOrFileAttachment: cryptoFile != nil, + isVoice: false + ) + } +} + +fileprivate func getSharedContent(_ ip: NSItemProvider) async -> Result<SharedContent, ErrorAlert> { + if let type = firstMatching(of: [.image, .movie, .fileURL, .url, .text]) { + switch type { + // Prepare Image message + case .image: + // Animated + return if ip.hasItemConformingToTypeIdentifier(UTType.gif.identifier) { + if let url = try? await inPlaceUrl(type: type), + let data = try? Data(contentsOf: url), + let image = UIImage(data: data), + let cryptoFile = saveFile(data, generateNewFileName("IMG", "gif"), encrypted: privacyEncryptLocalFilesGroupDefault.get()), + let preview = await resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) { + .success(.image(preview: preview, cryptoFile: cryptoFile)) + } else { .failure(ErrorAlert("Error preparing message")) } + + // Static + } else { + if let image = await staticImage(), + let cryptoFile = saveImage(image), + let preview = await resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) { + .success(.image(preview: preview, cryptoFile: cryptoFile)) + } else { .failure(ErrorAlert("Error preparing message")) } + } + + // Prepare Movie message + case .movie: + if let url = try? await inPlaceUrl(type: type), + let trancodedUrl = await transcodeVideo(from: url), + let (image, duration) = AVAsset(url: trancodedUrl).generatePreview(), + let preview = await resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE), + let cryptoFile = moveTempFileFromURL(trancodedUrl) { + try? FileManager.default.removeItem(at: trancodedUrl) + return .success(.movie(preview: preview, duration: duration, cryptoFile: cryptoFile)) + } else { return .failure(ErrorAlert("Error preparing message")) } + + // Prepare Data message + case .fileURL: + if let url = try? await inPlaceUrl(type: .data) { + if isFileTooLarge(for: url) { + let sizeString = ByteCountFormatter.string( + fromByteCount: Int64(getMaxFileSize(.xftp)), + countStyle: .binary + ) + return .failure( + ErrorAlert( + title: "Large file!", + message: "Currently maximum supported file size is \(sizeString)." + ) + ) + } + if let file = saveFileFromURL(url) { + return .success(.data(cryptoFile: file)) + } + } + return .failure(ErrorAlert("Error preparing file")) + + // Prepare Link message + case .url: + if let url = try? await ip.loadItem(forTypeIdentifier: type.identifier) as? URL { + let content: SharedContent = + if privacyLinkPreviewsGroupDefault.get(), let linkPreview = await getLinkPreview(for: url) { + .url(preview: linkPreview) + } else { + .text(string: url.absoluteString) + } + return .success(content) + } else { return .failure(ErrorAlert("Error preparing message")) } + + // Prepare Text message + case .text: + return if let text = try? await ip.loadItem(forTypeIdentifier: type.identifier) as? String { + .success(.text(string: text)) + } else { .failure(ErrorAlert("Error preparing message")) } + default: return .failure(ErrorAlert("Unsupported format")) + } + } else { + return .failure(ErrorAlert("Unsupported format")) + } + + + func inPlaceUrl(type: UTType) async throws -> URL { + try await withCheckedThrowingContinuation { cont in + let _ = ip.loadInPlaceFileRepresentation(forTypeIdentifier: type.identifier) { url, bool, error in + if let url = url { + cont.resume(returning: url) + } else if let error = error { + cont.resume(throwing: error) + } else { + fatalError("Either `url` or `error` must be present") + } + } + } + } + + func firstMatching(of types: Array<UTType>) -> UTType? { + for type in types { + if ip.hasItemConformingToTypeIdentifier(type.identifier) { return type } + } + return nil + } + + func staticImage() async -> UIImage? { + if let url = try? await inPlaceUrl(type: .image), + let downsampledImage = downsampleImage(at: url, to: MAX_DOWNSAMPLE_SIZE) { + downsampledImage + } else { + /// Fallback to loading image directly from `ItemProvider` + /// in case loading from disk is not possible. Required for sharing screenshots. + try? await ip.loadItem(forTypeIdentifier: UTType.image.identifier) as? UIImage + } + } +} + + +fileprivate func transcodeVideo(from input: URL) async -> URL? { + let outputUrl = URL( + fileURLWithPath: generateNewFileName( + getTempFilesDirectory().path + "/" + "video", "mp4", + fullPath: true + ) + ) + if await makeVideoQualityLower(input, outputUrl: outputUrl) { + return outputUrl + } else { + try? FileManager.default.removeItem(at: outputUrl) + return nil + } +} + +fileprivate func isFileTooLarge(for url: URL) -> Bool { + fileSize(url) + .map { $0 > getMaxFileSize(.xftp) } + ?? false +} + diff --git a/apps/ios/SimpleX SE/ShareView.swift b/apps/ios/SimpleX SE/ShareView.swift new file mode 100644 index 0000000000..07180ffa1b --- /dev/null +++ b/apps/ios/SimpleX SE/ShareView.swift @@ -0,0 +1,230 @@ +// +// ShareView.swift +// SimpleX SE +// +// Created by Levitating Pineapple on 09/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ShareView: View { + @ObservedObject var model: ShareModel + @Environment(\.colorScheme) var colorScheme + @State private var password = String() + @AppStorage(GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS, store: groupDefaults) private var radius = defaultProfileImageCorner + + var body: some View { + NavigationView { + ZStack(alignment: .bottom) { + if model.isLoaded { + List(model.filteredChats) { chat in + let isProhibited = model.isProhibited(chat) + let isSelected = model.selected == chat + HStack { + profileImage( + chatInfoId: chat.chatInfo.id, + iconName: chatIconName(chat.chatInfo), + size: 30 + ) + Text(chat.chatInfo.displayName).foregroundStyle( + isProhibited ? .secondary : .primary + ) + Spacer() + radioButton(selected: isSelected && !isProhibited) + } + .contentShape(Rectangle()) + .onTapGesture { + if isProhibited { + model.errorAlert = ErrorAlert( + title: "Cannot forward message", + message: "Selected chat preferences prohibit this message." + ) { Button("Ok", role: .cancel) { } } + } else { + model.selected = isSelected ? nil : chat + } + } + .tag(chat) + } + } else { + ProgressView().frame(maxHeight: .infinity) + } + } + .navigationTitle("Share") + .safeAreaInset(edge: .bottom) { + switch model.bottomBar { + case .sendButton: + compose(isLoading: false) + case .loadingSpinner: + compose(isLoading: true) + case .loadingBar(let progress): + loadingBar(progress: progress) + } + } + } + .searchable( + text: $model.search, + placement: .navigationBarDrawer(displayMode: .always) + ) + .alert($model.errorAlert) { alert in + if model.alertRequiresPassword { + SecureField("Passphrase", text: $password) + Button("Ok") { + model.setup(with: password) + password = String() + } + Button("Cancel", role: .cancel) { model.completion() } + } else { + Button("Ok") { model.completion() } + } + } + .onChange(of: model.comment) { + model.hasSimplexLink = hasSimplexLink($0) + } + } + + private func compose(isLoading: Bool) -> some View { + VStack(spacing: 0) { + Divider() + if let content = model.sharedContent { + itemPreview(content) + } + HStack { + Group { + if #available(iOSApplicationExtension 16.0, *) { + TextField("Comment", text: $model.comment, axis: .vertical).lineLimit(6) + } else { + TextField("Comment", text: $model.comment) + } + } + .contentShape(Rectangle()) + .disabled(isLoading) + .padding(.horizontal, 12) + .padding(.vertical, 4) + Group { + if isLoading { + ProgressView() + } else { + Button(action: model.send) { + Image(systemName: "arrow.up.circle.fill") + .resizable() + } + .disabled(model.isSendDisbled) + } + } + .frame(width: 28, height: 28) + .padding(6) + + } + .background(Color(.systemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 20)) + .overlay( + RoundedRectangle(cornerRadius: 20) + .strokeBorder(.secondary, lineWidth: 0.5).opacity(0.7) + ) + .padding(8) + } + .background(.thinMaterial) + } + + @ViewBuilder private func itemPreview(_ content: SharedContent) -> some View { + switch content { + case let .image(preview, _): imagePreview(preview) + case let .movie(preview, _, _): imagePreview(preview) + case let .url(preview): linkPreview(preview) + case let .data(cryptoFile): + previewArea { + Image(systemName: "doc.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 30, height: 30) + .foregroundColor(Color(uiColor: .tertiaryLabel)) + .padding(.leading, 4) + Text(cryptoFile.filePath) + } + case .text: EmptyView() + } + } + + @ViewBuilder private func imagePreview(_ imgStr: String) -> some View { + if let img = imageFromBase64(imgStr) { + previewArea { + Image(uiImage: img) + .resizable() + .scaledToFit() + .frame(minHeight: 40, maxHeight: 60) + } + } else { + EmptyView() + } + } + + private func linkPreview(_ linkPreview: LinkPreview) -> some View { + previewArea { + HStack(alignment: .center, spacing: 8) { + if let uiImage = imageFromBase64(linkPreview.image) { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 80, maxHeight: 60) + } + VStack(alignment: .center, spacing: 4) { + Text(linkPreview.title) + .lineLimit(1) + Text(linkPreview.uri.absoluteString) + .font(.caption) + .lineLimit(1) + .foregroundColor(.secondary) + } + .padding(.vertical, 5) + .frame(maxWidth: .infinity, minHeight: 60) + } + } + } + + @ViewBuilder private func previewArea<V: View>(@ViewBuilder content: @escaping () -> V) -> some View { + HStack(alignment: .center, spacing: 8) { + content() + Spacer() + } + .padding(.vertical, 1) + .frame(minHeight: 54) + .background { + switch colorScheme { + case .light: LightColorPaletteApp.sentMessage + case .dark: DarkColorPaletteApp.sentMessage + @unknown default: Color(.tertiarySystemBackground) + } + } + Divider() + } + + private func loadingBar(progress: Double) -> some View { + VStack { + Text("Sending message…") + ProgressView(value: progress) + } + .padding() + .background(Material.ultraThin) + } + + @ViewBuilder private func profileImage(chatInfoId: ChatInfo.ID, iconName: String, size: Double) -> some View { + if let uiImage = model.profileImages[chatInfoId] { + clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius) + } else { + Image(systemName: iconName) + .resizable() + .foregroundColor(Color(uiColor: .tertiaryLabel)) + .frame(width: size, height: size) +// add background when adding themes to SE +// .background(Circle().fill(backgroundColor != nil ? backgroundColor! : .clear)) + } + } + + private func radioButton(selected: Bool) -> some View { + Image(systemName: selected ? "checkmark.circle.fill" : "circle") + .imageScale(.large) + .foregroundStyle(selected ? Color.accentColor : Color(.tertiaryLabel)) + } +} diff --git a/apps/ios/SimpleX SE/ShareViewController.swift b/apps/ios/SimpleX SE/ShareViewController.swift new file mode 100644 index 0000000000..bf22f44a3b --- /dev/null +++ b/apps/ios/SimpleX SE/ShareViewController.swift @@ -0,0 +1,46 @@ +// +// ShareViewController.swift +// SimpleX SE +// +// Created by Levitating Pineapple on 08/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import UIKit +import SwiftUI +import SimpleXChat + +/// Extension Entry point +/// System will create this controller each time share sheet is invoked +/// using `NSExtensionPrincipalClass` in the info.plist +@objc(ShareViewController) +class ShareViewController: UIHostingController<ShareView> { + private let model = ShareModel() + // Assuming iOS continues to only allow single share sheet to be presented at once + static var isVisible: Bool = false + + @objc init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(rootView: ShareView(model: model)) + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { fatalError() } + + override func viewDidLoad() { + ShareModel.CompletionHandler.isEventLoopEnabled = false + model.setup(context: extensionContext!) + } + + override func viewWillAppear(_ animated: Bool) { + logger.debug("ShareSheet will appear") + super.viewWillAppear(animated) + Self.isVisible = true + } + + override func viewWillDisappear(_ animated: Bool) { + logger.debug("ShareSheet will dissappear") + super.viewWillDisappear(animated) + ShareModel.CompletionHandler.isEventLoopEnabled = false + Self.isVisible = false + } +} diff --git a/apps/ios/SimpleX SE/SimpleX SE.entitlements b/apps/ios/SimpleX SE/SimpleX SE.entitlements new file mode 100644 index 0000000000..51dea2c806 --- /dev/null +++ b/apps/ios/SimpleX SE/SimpleX SE.entitlements @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>com.apple.security.application-groups</key> + <array> + <string>group.chat.simplex.app</string> + </array> + <key>keychain-access-groups</key> + <array> + <string>$(AppIdentifierPrefix)chat.simplex.app</string> + </array> +</dict> +</plist> diff --git a/apps/ios/SimpleX SE/bg.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/bg.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/bg.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/bg.lproj/Localizable.strings b/apps/ios/SimpleX SE/bg.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/bg.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/cs.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/cs.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/cs.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/cs.lproj/Localizable.strings b/apps/ios/SimpleX SE/cs.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/cs.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/de.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/de.lproj/InfoPlist.strings new file mode 100644 index 0000000000..4a387a4361 --- /dev/null +++ b/apps/ios/SimpleX SE/de.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2025 SimpleX Chat. Alle Rechte vorbehalten."; + diff --git a/apps/ios/SimpleX SE/de.lproj/Localizable.strings b/apps/ios/SimpleX SE/de.lproj/Localizable.strings new file mode 100644 index 0000000000..4c10694986 --- /dev/null +++ b/apps/ios/SimpleX SE/de.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "Die App ist gesperrt!"; + +/* No comment provided by engineer. */ +"Cancel" = "Abbrechen"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Es ist nicht möglich, auf den Schlüsselbund zuzugreifen, um das Datenbankpasswort zu speichern"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Nachricht kann nicht weitergeleitet werden"; + +/* No comment provided by engineer. */ +"Comment" = "Kommentieren"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Die maximal erlaubte Dateigröße beträgt aktuell %@."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Datenbank-Herunterstufung ist erforderlich"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "Datenbank ist verschlüsselt!"; + +/* No comment provided by engineer. */ +"Database error" = "Datenbankfehler"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Das Datenbank-Passwort unterscheidet sich vom im Schlüsselbund gespeicherten."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Um den Chat zu öffnen, ist ein Datenbank-Passwort ist erforderlich."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Datenbank-Aktualisierung erforderlich"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Fehler beim Vorbereiten der Datei"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Fehler beim Vorbereiten der Nachricht"; + +/* No comment provided by engineer. */ +"Error: %@" = "Fehler: %@"; + +/* No comment provided by engineer. */ +"File error" = "Datei-Fehler"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Datenbank-Version nicht kompatibel"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Migrations-Bestätigung ungültig"; + +/* No comment provided by engineer. */ +"Keychain error" = "Schlüsselbund-Fehler"; + +/* No comment provided by engineer. */ +"Large file!" = "Große Datei!"; + +/* No comment provided by engineer. */ +"No active profile" = "Kein aktives Profil"; + +/* No comment provided by engineer. */ +"Ok" = "OK"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Öffnen Sie die App, um die Datenbank herunterzustufen."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Öffnen Sie die App, um die Datenbank zu aktualisieren."; + +/* No comment provided by engineer. */ +"Passphrase" = "Passwort"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Bitte erstellen Sie ein Profil in der SimpleX-App"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Diese Nachricht ist wegen der gewählten Chat-Einstellungen nicht erlaubt."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "Das Senden einer Nachricht dauert länger als erwartet."; + +/* No comment provided by engineer. */ +"Sending message…" = "Nachricht wird gesendet…"; + +/* No comment provided by engineer. */ +"Share" = "Teilen"; + +/* No comment provided by engineer. */ +"Slow network?" = "Langsames Netzwerk?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Unbekannter Datenbankfehler: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Nicht unterstütztes Format"; + +/* No comment provided by engineer. */ +"Wait" = "Warten"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Falsches Datenbank-Passwort"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Sie können das Teilen in den Einstellungen zu Datenschutz & Sicherheit / SimpleX-Sperre erlauben."; + diff --git a/apps/ios/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/es.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/es.lproj/InfoPlist.strings new file mode 100644 index 0000000000..74bda58efb --- /dev/null +++ b/apps/ios/SimpleX SE/es.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. Todos los derechos reservados."; + diff --git a/apps/ios/SimpleX SE/es.lproj/Localizable.strings b/apps/ios/SimpleX SE/es.lproj/Localizable.strings new file mode 100644 index 0000000000..4cc5029537 --- /dev/null +++ b/apps/ios/SimpleX SE/es.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "¡Aplicación bloqueada!"; + +/* No comment provided by engineer. */ +"Cancel" = "Cancelar"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Keychain inaccesible para guardar la contraseña de la base de datos"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "No se puede reenviar el mensaje"; + +/* No comment provided by engineer. */ +"Comment" = "Comentario"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "El tamaño máximo de archivo admitido es %@."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Se requiere volver a versión anterior de la base de datos"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "¡Base de datos cifrada!"; + +/* No comment provided by engineer. */ +"Database error" = "Error en base de datos"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "La contraseña de la base de datos es diferente a la almacenada en keychain."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Se requiere la contraseña de la base de datos para abrir la aplicación."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Se requiere actualizar la base de datos"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Error al preparar el archivo"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Error al preparar el mensaje"; + +/* No comment provided by engineer. */ +"Error: %@" = "Error: %@"; + +/* No comment provided by engineer. */ +"File error" = "Error de archivo"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Versión de base de datos incompatible"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Confirmación de migración no válida"; + +/* No comment provided by engineer. */ +"Keychain error" = "Error en keychain"; + +/* No comment provided by engineer. */ +"Large file!" = "¡Archivo grande!"; + +/* No comment provided by engineer. */ +"No active profile" = "Ningún perfil activo"; + +/* No comment provided by engineer. */ +"Ok" = "Ok"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Abre la aplicación para volver a versión anterior de la base de datos."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Abre la aplicación para actualizar la base de datos."; + +/* No comment provided by engineer. */ +"Passphrase" = "Frase de contraseña"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Por favor, crea un perfil en SimpleX"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Las preferencias seleccionadas no permiten este mensaje."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "Enviar el mensaje lleva más tiempo del esperado."; + +/* No comment provided by engineer. */ +"Sending message…" = "Enviando mensaje…"; + +/* No comment provided by engineer. */ +"Share" = "Compartir"; + +/* No comment provided by engineer. */ +"Slow network?" = "¿Red lenta?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Error desconocido en la base de datos: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Formato sin soporte"; + +/* No comment provided by engineer. */ +"Wait" = "Espera"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Contraseña incorrecta de la base de datos"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Puedes dar permiso para compartir en Privacidad y Seguridad / Bloque SimpleX."; + diff --git a/apps/ios/SimpleX SE/fi.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/fi.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/fi.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/fi.lproj/Localizable.strings b/apps/ios/SimpleX SE/fi.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/fi.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/fr.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/fr.lproj/InfoPlist.strings new file mode 100644 index 0000000000..4f89e54128 --- /dev/null +++ b/apps/ios/SimpleX SE/fr.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. Tous droits réservés."; + diff --git a/apps/ios/SimpleX SE/fr.lproj/Localizable.strings b/apps/ios/SimpleX SE/fr.lproj/Localizable.strings new file mode 100644 index 0000000000..46a458b471 --- /dev/null +++ b/apps/ios/SimpleX SE/fr.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "L'app est verrouillée !"; + +/* No comment provided by engineer. */ +"Cancel" = "Annuler"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Impossible d'accéder à la keychain pour enregistrer le mot de passe de la base de données"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Impossible de transférer le message"; + +/* No comment provided by engineer. */ +"Comment" = "Commenter"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Actuellement, la taille maximale des fichiers supportés est de %@."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Mise à jour de la base de données nécessaire"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "Base de données chiffrée !"; + +/* No comment provided by engineer. */ +"Database error" = "Erreur de base de données"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "La phrase secrète de la base de données est différente de celle enregistrée dans la keychain."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "La phrase secrète de la base de données est nécessaire pour ouvrir le chat."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Mise à niveau de la base de données nécessaire"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Erreur lors de la préparation du fichier"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Erreur lors de la préparation du message"; + +/* No comment provided by engineer. */ +"Error: %@" = "Erreur : %@"; + +/* No comment provided by engineer. */ +"File error" = "Erreur de fichier"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Version de la base de données incompatible"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Confirmation de migration invalide"; + +/* No comment provided by engineer. */ +"Keychain error" = "Erreur de la keychain"; + +/* No comment provided by engineer. */ +"Large file!" = "Fichier trop lourd !"; + +/* No comment provided by engineer. */ +"No active profile" = "Pas de profil actif"; + +/* No comment provided by engineer. */ +"Ok" = "Ok"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Ouvrez l'app pour rétrograder la base de données."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Ouvrez l'app pour mettre à jour la base de données."; + +/* No comment provided by engineer. */ +"Passphrase" = "Phrase secrète"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Veuillez créer un profil dans l'app SimpleX"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Les paramètres de chat sélectionnés ne permettent pas l'envoi de ce message."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "L'envoi d'un message prend plus de temps que prévu."; + +/* No comment provided by engineer. */ +"Sending message…" = "Envoi du message…"; + +/* No comment provided by engineer. */ +"Share" = "Partager"; + +/* No comment provided by engineer. */ +"Slow network?" = "Réseau lent ?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Erreur inconnue de la base de données : %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Format non pris en charge"; + +/* No comment provided by engineer. */ +"Wait" = "Attendez"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Mauvaise phrase secrète pour la base de données"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Vous pouvez autoriser le partage dans les paramètres Confidentialité et sécurité / SimpleX Lock."; + diff --git a/apps/ios/SimpleX SE/hu.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/hu.lproj/InfoPlist.strings new file mode 100644 index 0000000000..e1979850d1 --- /dev/null +++ b/apps/ios/SimpleX SE/hu.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. Minden jog fenntartva."; + diff --git a/apps/ios/SimpleX SE/hu.lproj/Localizable.strings b/apps/ios/SimpleX SE/hu.lproj/Localizable.strings new file mode 100644 index 0000000000..2fedf0e6f1 --- /dev/null +++ b/apps/ios/SimpleX SE/hu.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "Az alkalmazás zárolva van!"; + +/* No comment provided by engineer. */ +"Cancel" = "Mégse"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Nem lehet hozzáférni a kulcstartóhoz az adatbázis jelszavának mentéséhez"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Nem lehet továbbítani az üzenetet"; + +/* No comment provided by engineer. */ +"Comment" = "Hozzászólás"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Jelenleg támogatott legnagyobb fájl méret: %@."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Adatbázis visszafejlesztése szükséges"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "Adatbázis titkosítva!"; + +/* No comment provided by engineer. */ +"Database error" = "Adatbázishiba"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Az adatbázis jelmondata nem egyezik a kulcstartóba mentettől."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "A csevegés megnyitásához adja meg az adatbázis jelmondatát."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Adatbázis fejlesztése szükséges"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Hiba történt a fájl előkészítésekor"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Hiba történt az üzenet előkészítésekor"; + +/* No comment provided by engineer. */ +"Error: %@" = "Hiba: %@"; + +/* No comment provided by engineer. */ +"File error" = "Fájlhiba"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Nem kompatibilis adatbázis-verzió"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Érvénytelen átköltöztetési visszaigazolás"; + +/* No comment provided by engineer. */ +"Keychain error" = "Kulcstartóhiba"; + +/* No comment provided by engineer. */ +"Large file!" = "Nagy fájl!"; + +/* No comment provided by engineer. */ +"No active profile" = "Nincs aktív profil"; + +/* No comment provided by engineer. */ +"Ok" = "Rendben"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Nyissa meg az alkalmazást az adatbázis visszafejlesztéséhez."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Nyissa meg az alkalmazást az adatbázis fejlesztéséhez."; + +/* No comment provided by engineer. */ +"Passphrase" = "Jelmondat"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Hozzon létre egy profilt a SimpleX alkalmazásban"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "A kijelölt csevegési beállítások tiltják ezt az üzenetet."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "Az üzenet elküldése a vártnál tovább tart."; + +/* No comment provided by engineer. */ +"Sending message…" = "Üzenet küldése…"; + +/* No comment provided by engineer. */ +"Share" = "Megosztás"; + +/* No comment provided by engineer. */ +"Slow network?" = "Lassú a hálózata?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Ismeretlen adatbázishiba: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Nem támogatott formátum"; + +/* No comment provided by engineer. */ +"Wait" = "Várjon"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Érvénytelen adatbázis-jelmondat"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "A megosztást az Adatvédelem és biztonság / SimpleX-zár menüben engedélyezheti."; + diff --git a/apps/ios/SimpleX SE/it.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/it.lproj/InfoPlist.strings new file mode 100644 index 0000000000..78145285c2 --- /dev/null +++ b/apps/ios/SimpleX SE/it.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. Tutti i diritti riservati."; + diff --git a/apps/ios/SimpleX SE/it.lproj/Localizable.strings b/apps/ios/SimpleX SE/it.lproj/Localizable.strings new file mode 100644 index 0000000000..e3d34650a3 --- /dev/null +++ b/apps/ios/SimpleX SE/it.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "L'app è bloccata!"; + +/* No comment provided by engineer. */ +"Cancel" = "Annulla"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Impossibile accedere al portachiavi per salvare la password del database"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Impossibile inoltrare il messaggio"; + +/* No comment provided by engineer. */ +"Comment" = "Commento"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Attualmente la dimensione massima supportata è di %@."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Downgrade del database necessario"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "Database crittografato!"; + +/* No comment provided by engineer. */ +"Database error" = "Errore del database"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "La password del database è diversa da quella salvata nel portachiavi."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "La password del database è necessaria per aprire la chat."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Aggiornamento del database necessario"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Errore nella preparazione del file"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Errore nella preparazione del messaggio"; + +/* No comment provided by engineer. */ +"Error: %@" = "Errore: %@"; + +/* No comment provided by engineer. */ +"File error" = "Errore del file"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Versione del database incompatibile"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Conferma di migrazione non valida"; + +/* No comment provided by engineer. */ +"Keychain error" = "Errore del portachiavi"; + +/* No comment provided by engineer. */ +"Large file!" = "File grande!"; + +/* No comment provided by engineer. */ +"No active profile" = "Nessun profilo attivo"; + +/* No comment provided by engineer. */ +"Ok" = "Ok"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Apri l'app per eseguire il downgrade del database."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Apri l'app per aggiornare il database."; + +/* No comment provided by engineer. */ +"Passphrase" = "Password"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Crea un profilo nell'app SimpleX"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Le preferenze della chat selezionata vietano questo messaggio."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "L'invio di un messaggio richiede più tempo del previsto."; + +/* No comment provided by engineer. */ +"Sending message…" = "Invio messaggio…"; + +/* No comment provided by engineer. */ +"Share" = "Condividi"; + +/* No comment provided by engineer. */ +"Slow network?" = "Rete lenta?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Errore del database sconosciuto: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Formato non supportato"; + +/* No comment provided by engineer. */ +"Wait" = "Attendi"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Password del database sbagliata"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Puoi consentire la condivisione in Privacy e sicurezza / impostazioni di SimpleX Lock."; + diff --git a/apps/ios/SimpleX SE/ja.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/ja.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/ja.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/ja.lproj/Localizable.strings b/apps/ios/SimpleX SE/ja.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/ja.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/nl.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/nl.lproj/InfoPlist.strings new file mode 100644 index 0000000000..c61e43a87f --- /dev/null +++ b/apps/ios/SimpleX SE/nl.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX SE/nl.lproj/Localizable.strings b/apps/ios/SimpleX SE/nl.lproj/Localizable.strings new file mode 100644 index 0000000000..e5d2487b54 --- /dev/null +++ b/apps/ios/SimpleX SE/nl.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "App is vergrendeld!"; + +/* No comment provided by engineer. */ +"Cancel" = "Annuleren"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Kan geen toegang krijgen tot de keychain om het database wachtwoord op te slaan"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Kan bericht niet doorsturen"; + +/* No comment provided by engineer. */ +"Comment" = "Opmerking"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "De momenteel maximaal ondersteunde bestandsgrootte is %@."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Database downgrade vereist"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "Database versleuteld!"; + +/* No comment provided by engineer. */ +"Database error" = "Database fout"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Het wachtwoord van de database verschilt van het wachtwoord die in de keychain is opgeslagen."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Database wachtwoord is vereist om je chats te openen."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Database upgrade vereist"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Fout bij voorbereiden bestand"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Fout bij het voorbereiden van bericht"; + +/* No comment provided by engineer. */ +"Error: %@" = "Fout: %@"; + +/* No comment provided by engineer. */ +"File error" = "Bestandsfout"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Incompatibele database versie"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Ongeldige migratie bevestiging"; + +/* No comment provided by engineer. */ +"Keychain error" = "Keychain fout"; + +/* No comment provided by engineer. */ +"Large file!" = "Groot bestand!"; + +/* No comment provided by engineer. */ +"No active profile" = "Geen actief profiel"; + +/* No comment provided by engineer. */ +"Ok" = "Ok"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Open de app om de database te downgraden."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Open de app om de database te upgraden."; + +/* No comment provided by engineer. */ +"Passphrase" = "Wachtwoord"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Maak een profiel aan in de SimpleX app"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Geselecteerde chat voorkeuren verbieden dit bericht."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "Het verzenden van een bericht duurt langer dan verwacht."; + +/* No comment provided by engineer. */ +"Sending message…" = "Bericht versturen…"; + +/* No comment provided by engineer. */ +"Share" = "Deel"; + +/* No comment provided by engineer. */ +"Slow network?" = "Traag netwerk?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Onbekende database fout: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Niet ondersteund formaat"; + +/* No comment provided by engineer. */ +"Wait" = "wachten"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Verkeerde database wachtwoord"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "U kunt delen toestaan in de instellingen voor Privacy en beveiliging / SimpleX Lock."; + diff --git a/apps/ios/SimpleX SE/pl.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/pl.lproj/InfoPlist.strings new file mode 100644 index 0000000000..81283a3f02 --- /dev/null +++ b/apps/ios/SimpleX SE/pl.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. Wszelkie prawa zastrzeżone."; + diff --git a/apps/ios/SimpleX SE/pl.lproj/Localizable.strings b/apps/ios/SimpleX SE/pl.lproj/Localizable.strings new file mode 100644 index 0000000000..c563431c28 --- /dev/null +++ b/apps/ios/SimpleX SE/pl.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "Aplikacja zablokowana!"; + +/* No comment provided by engineer. */ +"Cancel" = "Anuluj"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Nie można uzyskać dostępu do pęku kluczy aby zapisać hasło do bazy danych"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Nie można przekazać wiadomości"; + +/* No comment provided by engineer. */ +"Comment" = "Komentarz"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Obecnie maksymalny obsługiwany rozmiar pliku to %@."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Wymagane obniżenie wersji bazy danych"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "Baza danych zaszyfrowana!"; + +/* No comment provided by engineer. */ +"Database error" = "Błąd bazy danych"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Hasło bazy danych jest inne niż zapisane w pęku kluczy."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Hasło do bazy danych jest wymagane do otwarcia czatu."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Wymagana aktualizacja bazy danych"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Błąd przygotowania pliku"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Błąd przygotowania wiadomości"; + +/* No comment provided by engineer. */ +"Error: %@" = "Błąd: %@"; + +/* No comment provided by engineer. */ +"File error" = "Błąd pliku"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Niekompatybilna wersja bazy danych"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Nieprawidłowe potwierdzenie migracji"; + +/* No comment provided by engineer. */ +"Keychain error" = "Błąd pęku kluczy"; + +/* No comment provided by engineer. */ +"Large file!" = "Duży plik!"; + +/* No comment provided by engineer. */ +"No active profile" = "Brak aktywnego profilu"; + +/* No comment provided by engineer. */ +"Ok" = "Ok"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Otwórz aplikację aby obniżyć wersję bazy danych."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Otwórz aplikację aby zaktualizować bazę danych."; + +/* No comment provided by engineer. */ +"Passphrase" = "Hasło"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Proszę utworzyć profil w aplikacji SimpleX"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Wybrane preferencje czatu zabraniają tej wiadomości."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "Wysłanie wiadomości trwa dłużej niż oczekiwano."; + +/* No comment provided by engineer. */ +"Sending message…" = "Wysyłanie wiadomości…"; + +/* No comment provided by engineer. */ +"Share" = "Udostępnij"; + +/* No comment provided by engineer. */ +"Slow network?" = "Wolna sieć?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Nieznany błąd bazy danych: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Niewspierany format"; + +/* No comment provided by engineer. */ +"Wait" = "Czekaj"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Nieprawidłowe hasło bazy danych"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Możesz zezwolić na udostępnianie w ustawieniach Prywatność i bezpieczeństwo / Blokada SimpleX."; + diff --git a/apps/ios/SimpleX SE/ru.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/ru.lproj/InfoPlist.strings new file mode 100644 index 0000000000..d45b3d735d --- /dev/null +++ b/apps/ios/SimpleX SE/ru.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. Все права защищены."; + diff --git a/apps/ios/SimpleX SE/ru.lproj/Localizable.strings b/apps/ios/SimpleX SE/ru.lproj/Localizable.strings new file mode 100644 index 0000000000..0841e8e47f --- /dev/null +++ b/apps/ios/SimpleX SE/ru.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "Приложение заблокировано!"; + +/* No comment provided by engineer. */ +"Cancel" = "Отменить"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Невозможно сохранить пароль в keychain"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Невозможно переслать сообщение"; + +/* No comment provided by engineer. */ +"Comment" = "Комментарий"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "В настоящее время максимальный поддерживаемый размер файла составляет %@."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Требуется откат базы данных"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "База данных зашифрована!"; + +/* No comment provided by engineer. */ +"Database error" = "Ошибка базы данных"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Пароль базы данных отличается от сохраненного в keychain."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Введите пароль базы данных, чтобы открыть чат."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Требуется обновление базы данных"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Ошибка подготовки файла"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Ошибка подготовки сообщения"; + +/* No comment provided by engineer. */ +"Error: %@" = "Ошибка: %@"; + +/* No comment provided by engineer. */ +"File error" = "Ошибка файла"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Несовместимая версия базы данных"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Ошибка подтверждения миграции"; + +/* No comment provided by engineer. */ +"Keychain error" = "Ошибка keychain"; + +/* No comment provided by engineer. */ +"Large file!" = "Большой файл!"; + +/* No comment provided by engineer. */ +"No active profile" = "Нет активного профиля"; + +/* No comment provided by engineer. */ +"Ok" = "Ок"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Откройте приложение, чтобы откатить базу данных."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Откройте приложение, чтобы обновить базу данных."; + +/* No comment provided by engineer. */ +"Passphrase" = "Пароль"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Пожалуйста, создайте профиль в приложении SimpleX."; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Выбранные настройки чата запрещают это сообщение."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "Отправка сообщения занимает дольше ожиданного."; + +/* No comment provided by engineer. */ +"Sending message…" = "Отправка сообщения…"; + +/* No comment provided by engineer. */ +"Share" = "Поделиться"; + +/* No comment provided by engineer. */ +"Slow network?" = "Медленная сеть?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Неизвестная ошибка базы данных: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Неподдерживаемый формат"; + +/* No comment provided by engineer. */ +"Wait" = "Подождать"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Неправильный пароль базы данных"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Вы можете разрешить функцию Поделиться в настройках Конфиденциальности / Блокировка SimpleX."; + diff --git a/apps/ios/SimpleX SE/th.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/th.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/th.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/th.lproj/Localizable.strings b/apps/ios/SimpleX SE/th.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/th.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/tr.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/tr.lproj/InfoPlist.strings new file mode 100644 index 0000000000..cf1ca31f53 --- /dev/null +++ b/apps/ios/SimpleX SE/tr.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Telif Hakkı © 2024 SimpleX Chat. Tüm hakları saklıdır."; + diff --git a/apps/ios/SimpleX SE/tr.lproj/Localizable.strings b/apps/ios/SimpleX SE/tr.lproj/Localizable.strings new file mode 100644 index 0000000000..baef71c127 --- /dev/null +++ b/apps/ios/SimpleX SE/tr.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "Uygulama kilitlendi!"; + +/* No comment provided by engineer. */ +"Cancel" = "İptal et"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Veritabanı şifresini kaydetmek için Anahtar Zinciri'ne erişilemiyor"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Mesaj iletilemiyor"; + +/* No comment provided by engineer. */ +"Comment" = "Yorum"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Şu anki maksimum desteklenen dosya boyutu %@ kadardır."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Veritabanı sürüm düşürme gerekli"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "Veritabanı şifrelendi!"; + +/* No comment provided by engineer. */ +"Database error" = "Veritabanı hatası"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Veritabanı parolası Anahtar Zinciri'nde kayıtlı olandan farklıdır."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Konuşmayı açmak için veri tabanı parolası gerekli."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Veritabanı yükseltmesi gerekli"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Dosya hazırlanırken hata oluştu"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Mesaj hazırlanırken hata oluştu"; + +/* No comment provided by engineer. */ +"Error: %@" = "Hata: %@"; + +/* No comment provided by engineer. */ +"File error" = "Dosya hatası"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Uyumsuz veritabanı sürümü"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Geçerli olmayan taşıma onayı"; + +/* No comment provided by engineer. */ +"Keychain error" = "Anahtarlık hatası"; + +/* No comment provided by engineer. */ +"Large file!" = "Büyük dosya!"; + +/* No comment provided by engineer. */ +"No active profile" = "Aktif profil yok"; + +/* No comment provided by engineer. */ +"Ok" = "Tamam"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Veritabanının sürümünü düşürmek için uygulamayı açın."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Veritabanını güncellemek için uygulamayı açın."; + +/* No comment provided by engineer. */ +"Passphrase" = "Parola"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Lütfen SimpleX uygulamasında bir profil oluşturun"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Seçilen sohbet tercihleri bu mesajı yasakladı."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "Mesaj göndermek beklenenden daha uzun sürüyor."; + +/* No comment provided by engineer. */ +"Sending message…" = "Mesaj gönderiliyor…"; + +/* No comment provided by engineer. */ +"Share" = "Paylaş"; + +/* No comment provided by engineer. */ +"Slow network?" = "Ağ yavaş mı?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Bilinmeyen veritabanı hatası: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Desteklenmeyen format"; + +/* No comment provided by engineer. */ +"Wait" = "Bekleyin"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Yanlış veritabanı parolası"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Gizlilik ve Güvenlik / SimpleX Lock ayarlarından paylaşıma izin verebilirsiniz."; + diff --git a/apps/ios/SimpleX SE/uk.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/uk.lproj/InfoPlist.strings new file mode 100644 index 0000000000..18c4d5e8a5 --- /dev/null +++ b/apps/ios/SimpleX SE/uk.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. Всі права захищені."; + diff --git a/apps/ios/SimpleX SE/uk.lproj/Localizable.strings b/apps/ios/SimpleX SE/uk.lproj/Localizable.strings new file mode 100644 index 0000000000..a6da81185e --- /dev/null +++ b/apps/ios/SimpleX SE/uk.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "Додаток заблоковано!"; + +/* No comment provided by engineer. */ +"Cancel" = "Скасувати"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Не вдається отримати доступ до зв'язки ключів для збереження пароля до бази даних"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Неможливо переслати повідомлення"; + +/* No comment provided by engineer. */ +"Comment" = "Коментар"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Наразі максимальний підтримуваний розмір файлу - %@."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Потрібне оновлення бази даних"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "База даних зашифрована!"; + +/* No comment provided by engineer. */ +"Database error" = "Помилка в базі даних"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Парольна фраза бази даних відрізняється від збереженої у в’язці ключів."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Для відкриття чату потрібно ввести пароль до бази даних."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Потрібне оновлення бази даних"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Помилка підготовки файлу"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Повідомлення про підготовку до помилки"; + +/* No comment provided by engineer. */ +"Error: %@" = "Помилка: %@"; + +/* No comment provided by engineer. */ +"File error" = "Помилка файлу"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Несумісна версія бази даних"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Недійсне підтвердження міграції"; + +/* No comment provided by engineer. */ +"Keychain error" = "Помилка зв'язки ключів"; + +/* No comment provided by engineer. */ +"Large file!" = "Великий файл!"; + +/* No comment provided by engineer. */ +"No active profile" = "Немає активного профілю"; + +/* No comment provided by engineer. */ +"Ok" = "Гаразд"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Відкрийте програму, щоб знизити версію бази даних."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Відкрийте програму, щоб оновити базу даних."; + +/* No comment provided by engineer. */ +"Passphrase" = "Парольна фраза"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Будь ласка, створіть профіль у додатку SimpleX"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Вибрані налаштування чату забороняють це повідомлення."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "Надсилання повідомлення займає більше часу, ніж очікувалося."; + +/* No comment provided by engineer. */ +"Sending message…" = "Надсилаю повідомлення…"; + +/* No comment provided by engineer. */ +"Share" = "Поділіться"; + +/* No comment provided by engineer. */ +"Slow network?" = "Повільна мережа?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Невідома помилка бази даних: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Непідтримуваний формат"; + +/* No comment provided by engineer. */ +"Wait" = "Зачекай"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Неправильна ключова фраза до бази даних"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Ви можете дозволити спільний доступ у налаштуваннях Конфіденційність і безпека / SimpleX Lock."; + diff --git a/apps/ios/SimpleX SE/zh-Hans.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/zh-Hans.lproj/InfoPlist.strings new file mode 100644 index 0000000000..760be62885 --- /dev/null +++ b/apps/ios/SimpleX SE/zh-Hans.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "版权所有 © 2024 SimpleX Chat。保留所有权利。"; + diff --git a/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings b/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings new file mode 100644 index 0000000000..362e2edb74 --- /dev/null +++ b/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "应用程序已锁定!"; + +/* No comment provided by engineer. */ +"Cancel" = "取消"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "无法访问钥匙串以保存数据库密码"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "无法转发消息"; + +/* No comment provided by engineer. */ +"Comment" = "评论"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "当前支持的最大文件大小为 %@。"; + +/* No comment provided by engineer. */ +"Database downgrade required" = "需要数据库降级"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "数据库已加密!"; + +/* No comment provided by engineer. */ +"Database error" = "数据库错误"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "数据库密码与保存在钥匙串中的密码不同。"; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "需要数据库密码才能打开聊天。"; + +/* No comment provided by engineer. */ +"Database upgrade required" = "需要升级数据库"; + +/* No comment provided by engineer. */ +"Error preparing file" = "准备文件时出错"; + +/* No comment provided by engineer. */ +"Error preparing message" = "准备消息时出错"; + +/* No comment provided by engineer. */ +"Error: %@" = "错误:%@"; + +/* No comment provided by engineer. */ +"File error" = "文件错误"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "不兼容的数据库版本"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "无效的迁移确认"; + +/* No comment provided by engineer. */ +"Keychain error" = "钥匙串错误"; + +/* No comment provided by engineer. */ +"Large file!" = "大文件!"; + +/* No comment provided by engineer. */ +"No active profile" = "无活动配置文件"; + +/* No comment provided by engineer. */ +"Ok" = "好的"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "打开应用程序以降级数据库。"; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "打开应用程序以升级数据库。"; + +/* No comment provided by engineer. */ +"Passphrase" = "密码"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "请在 SimpleX 应用程序中创建配置文件"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "选定的聊天首选项禁止此消息。"; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "发送消息所需的时间比预期的要长。"; + +/* No comment provided by engineer. */ +"Sending message…" = "正在发送消息…"; + +/* No comment provided by engineer. */ +"Share" = "共享"; + +/* No comment provided by engineer. */ +"Slow network?" = "网络速度慢?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "未知数据库错误: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "不支持的格式"; + +/* No comment provided by engineer. */ +"Wait" = "等待"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "数据库密码错误"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "您可以在 \"隐私与安全\"/\"SimpleX Lock \"设置中允许共享。"; + diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 801116bf80..bb45f57992 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -16,10 +16,8 @@ 18415C6C56DBCEC2CBBD2F11 /* WebRTCClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415323A4082FC92887F906 /* WebRTCClient.swift */; }; 18415F9A2D551F9757DA4654 /* CIVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415FD2E36F13F596A45BB4 /* CIVideoView.swift */; }; 18415FEFE153C5920BFB7828 /* GroupWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1841516F0CE5992B0EDFB377 /* GroupWelcomeView.swift */; }; - 3C8C548928133C84000A3EC7 /* PasteToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */; }; 3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */; }; 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; }; - 5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00164328A26FBC0094D739 /* ContextMenu.swift */; }; 5C00168128C4FE760094D739 /* KeyChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00168028C4FE760094D739 /* KeyChain.swift */; }; 5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; }; 5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; }; @@ -43,11 +41,6 @@ 5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; }; 5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; }; 5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.swift */; }; - 5C4E80DA2B3CCD090080FAE2 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D52B3CCD090080FAE2 /* libgmp.a */; }; - 5C4E80DB2B3CCD090080FAE2 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D62B3CCD090080FAE2 /* libffi.a */; }; - 5C4E80DC2B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D72B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a */; }; - 5C4E80DD2B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D82B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a */; }; - 5C4E80DE2B3CCD090080FAE2 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80D92B3CCD090080FAE2 /* libgmpxx.a */; }; 5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; }; 5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; }; 5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; }; @@ -61,7 +54,6 @@ 5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */; }; 5C65DAF929D0CC20003CEE45 /* DeveloperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C65DAF829D0CC20003CEE45 /* DeveloperView.swift */; }; 5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C65F341297D3F3600B67AF3 /* VersionView.swift */; }; - 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; }; 5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6BA666289BD954009B8ECC /* DismissSheets.swift */; }; 5C7031162953C97F00150A12 /* CIFeaturePreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7031152953C97F00150A12 /* CIFeaturePreferenceView.swift */; }; 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; }; @@ -98,8 +90,6 @@ 5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA91282713FD00B3292C /* CreateProfile.swift */; }; 5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA992827FD8800B3292C /* HowItWorks.swift */; }; 5CB2084F28DA4B4800D024EC /* RTCServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB2084E28DA4B4800D024EC /* RTCServers.swift */; }; - 5CB2085128DB64CA00D024EC /* CreateLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB2085028DB64CA00D024EC /* CreateLinkView.swift */; }; - 5CB2085328DB7CAF00D024EC /* ConnectViaLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB2085228DB7CAF00D024EC /* ConnectViaLinkView.swift */; }; 5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */; }; 5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E62868D76D001FD2EF /* NotificationsView.swift */; }; 5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */; }; @@ -110,7 +100,6 @@ 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D627A8563F00ACCCDD /* SettingsView.swift */; }; 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* UserProfile.swift */; }; 5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */; }; - 5CBD285A295711D700EC2CF4 /* ImageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */; }; 5CBD285C29575B8E00EC2CF4 /* WhatsNewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBD285B29575B8E00EC2CF4 /* WhatsNewView.swift */; }; 5CBE6C12294487F7002D9531 /* VerifyCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBE6C11294487F7002D9531 /* VerifyCodeView.swift */; }; 5CBE6C142944CC12002D9531 /* ScanCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBE6C132944CC12002D9531 /* ScanCodeView.swift */; }; @@ -121,8 +110,6 @@ 5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; }; 5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; }; 5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; }; - 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; }; - 5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; }; 5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; }; 5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; }; @@ -148,24 +135,25 @@ 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407827ADB701007B033A /* EmojiItemView.swift */; }; 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; }; 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; }; - 5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; }; 5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */; }; - 5CF9371E2B23429500E1D781 /* ConcurrentQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371D2B23429500E1D781 /* ConcurrentQueue.swift */; }; 5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */; }; 5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF937212B25034A00E1D781 /* NSESubscriber.swift */; }; 5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; }; - 5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; }; 5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; 5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; + 640417CD2B29B8C200CCB412 /* NewChatMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */; }; + 640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CC2B29B8C200CCB412 /* NewChatView.swift */; }; + 640743612CD360E600158442 /* ChooseServerOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640743602CD360E600158442 /* ChooseServerOperators.swift */; }; 6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; }; 6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; }; 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; }; + 642BA82D2CE50495005E9412 /* NewServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642BA82C2CE50495005E9412 /* NewServerView.swift */; }; 6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; }; + 643B3B4E2CCFD6400083A2CF /* OperatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */; }; 6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; }; 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; }; 6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; }; 6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */; }; - 64466DC829FC2B3B00E3D48D /* CreateSimpleXAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */; }; 64466DCC29FFE3E800E3D48D /* MailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64466DCB29FFE3E800E3D48D /* MailView.swift */; }; 6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */; }; 644EFFDE292BCD9D00525D5B /* ComposeVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */; }; @@ -177,24 +165,85 @@ 646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; }; 647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */; }; 648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; }; + 648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */; }; 649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; }; 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; }; 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; }; 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; }; 64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; }; 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; + 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; + 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.7.0-3RUUqzi5CrDKdTTh7MfNU-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.7.0-3RUUqzi5CrDKdTTh7MfNU-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.7.0-3RUUqzi5CrDKdTTh7MfNU.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.7.0-3RUUqzi5CrDKdTTh7MfNU.a */; }; + 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; 64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */; }; 64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; }; + 64EEB0F72C353F1C00972D62 /* ServersSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */; }; 64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; }; - 8C05382E2B39887E006436DC /* VideoUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C05382D2B39887E006436DC /* VideoUtils.swift */; }; + 8C01E9C12C8EFC33008A4B0A /* objc.m in Sources */ = {isa = PBXBuildFile; fileRef = 8C01E9C02C8EFC33008A4B0A /* objc.m */; }; + 8C01E9C22C8EFF8F008A4B0A /* objc.h in Headers */ = {isa = PBXBuildFile; fileRef = 8C01E9BF2C8EFBB6008A4B0A /* objc.h */; }; + 8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */; }; + 8C74C3E52C1B900600039E77 /* ThemeTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7E3CE32C0DEAC400BFF63A /* ThemeTypes.swift */; }; + 8C74C3E72C1B901900039E77 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C852B072C1086D100BA61E8 /* Color.swift */; }; + 8C74C3E82C1B905B00039E77 /* ChatWallpaperTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C804B1D2C11F966007A63C8 /* ChatWallpaperTypes.swift */; }; + 8C74C3EA2C1B90AF00039E77 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C86EBE42C0DAE4F00E12243 /* ThemeManager.swift */; }; + 8C74C3EC2C1B92A900039E77 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C74C3EB2C1B92A900039E77 /* Theme.swift */; }; + 8C74C3EE2C1B942300039E77 /* ChatWallpaper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C74C3ED2C1B942300039E77 /* ChatWallpaper.swift */; }; + 8C7D949A2B88952700B7B9E1 /* MigrateToDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */; }; + 8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */; }; + 8C7F8F0E2C19C0C100D16888 /* ViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7F8F0D2C19C0C100D16888 /* ViewModifiers.swift */; }; + 8C8118722C220B5B00E6FC94 /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = 8C8118712C220B5B00E6FC94 /* Yams */; }; + 8C81482C2BD91CD4002CBEC3 /* AudioDevicePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C81482B2BD91CD4002CBEC3 /* AudioDevicePicker.swift */; }; + 8C9BC2652C240D5200875A27 /* ThemeModeEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */; }; + 8CAD466F2D15A8100078D18F /* ChatScrollHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CAD466E2D15A8100078D18F /* ChatScrollHelpers.swift */; }; + 8CAEF1502D11A6A000240F00 /* ChatItemsLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CAEF14F2D11A6A000240F00 /* ChatItemsLoader.swift */; }; + 8CB15EA02CFDA30600C28209 /* ChatItemsMerger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB15E9F2CFDA30600C28209 /* ChatItemsMerger.swift */; }; + 8CB3476C2CF5CFFA006787A5 /* Ink in Frameworks */ = {isa = PBXBuildFile; productRef = 8CB3476B2CF5CFFA006787A5 /* Ink */; }; + 8CB3476E2CF5F58B006787A5 /* ConditionsWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB3476D2CF5F58B006787A5 /* ConditionsWebView.swift */; }; + 8CBC14862D357CDB00BBD901 /* StorageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CBC14852D357CDB00BBD901 /* StorageView.swift */; }; + 8CC317442D4FEB9B00292A20 /* EndlessScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC317432D4FEB9B00292A20 /* EndlessScrollView.swift */; }; + 8CC317462D4FEBA800292A20 /* ScrollViewCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC317452D4FEBA800292A20 /* ScrollViewCells.swift */; }; + 8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; }; + 8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; }; + 8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */; }; + B70A39732D24090D00E80A5F /* TagListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B70A39722D24090D00E80A5F /* TagListView.swift */; }; + B70CE9E62D4BE5930080F36D /* GroupMentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B70CE9E52D4BE5930080F36D /* GroupMentions.swift */; }; + B728945B2D0C62BF00F7A19A /* ElegantEmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = B728945A2D0C62BF00F7A19A /* ElegantEmojiPicker */; }; + B73EFE532CE5FA3500C778EA /* CreateSimpleXAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73EFE522CE5FA3500C778EA /* CreateSimpleXAddress.swift */; }; + B76E6C312C5C41D900EC11AA /* ContactListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */; }; + B79ADAFF2CE4EF930083DFFD /* AddressCreationCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79ADAFE2CE4EF930083DFFD /* AddressCreationCard.swift */; }; + CE176F202C87014C00145DBC /* InvertedForegroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */; }; + CE1EB0E42C459A660099D896 /* ShareAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1EB0E32C459A660099D896 /* ShareAPI.swift */; }; + CE2AD9CE2C452A4D00E844E3 /* ChatUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */; }; + CE3097FB2C4C0C9F00180898 /* ErrorAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */; }; + CE38A29A2C3FCA54005ED185 /* ImageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */; }; + CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = CE38A29B2C3FCD72005ED185 /* SwiftyGif */; }; + CE75480A2C622630009579B7 /* SwipeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7548092C622630009579B7 /* SwipeLabel.swift */; }; + CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */; }; + CEA6E91C2CBD21B0002B5DB4 /* UserDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA6E91B2CBD21B0002B5DB4 /* UserDefault.swift */; }; + CEDB245B2C9CD71800FBC5F6 /* StickyScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDB245A2C9CD71800FBC5F6 /* StickyScrollView.swift */; }; + CEDE70222C48FD9500233B1F /* SEChatState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDE70212C48FD9500233B1F /* SEChatState.swift */; }; + CEE723AA2C3BD3D70009AE93 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */; }; + CEE723B12C3BD3D70009AE93 /* SimpleX SE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + CEE723F02C3D25C70009AE93 /* ShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723EF2C3D25C70009AE93 /* ShareView.swift */; }; + CEE723F22C3D25ED0009AE93 /* ShareModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723F12C3D25ED0009AE93 /* ShareModel.swift */; }; + CEFB2EDF2CA1BCC7004B1ECE /* SheetRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFB2EDE2CA1BCC7004B1ECE /* SheetRepresentable.swift */; }; D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; }; D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; }; D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547729AF89AF0022400A /* StoreKit.framework */; }; D741547A29AF90B00022400A /* PushKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547929AF90B00022400A /* PushKit.framework */; }; D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; }; D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; }; + E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; }; + E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; + E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; }; + E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9822C5902CE007928CC /* Localizable.strings */; }; + E5DCF9982C5906FF007928CC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9962C5906FF007928CC /* InfoPlist.strings */; }; + E5DDBE6E2DC4106800A0EFF0 /* AppAPITypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5DDBE6D2DC4106200A0EFF0 /* AppAPITypes.swift */; }; + E5DDBE702DC4217900A0EFF0 /* NSEAPITypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5DDBE6F2DC4217900A0EFF0 /* NSEAPITypes.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -226,6 +275,20 @@ remoteGlobalIDString = 5CE2BA672845308900EC33A6; remoteInfo = SimpleXChat; }; + CEE723AF2C3BD3D70009AE93 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5CA059BE279559F40002BEB4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = CEE723A62C3BD3D70009AE93; + remoteInfo = "SimpleX SE"; + }; + CEE723D12C3C21C90009AE93 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5CA059BE279559F40002BEB4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5CE2BA672845308900EC33A6; + remoteInfo = SimpleXChat; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -246,6 +309,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( + CEE723B12C3BD3D70009AE93 /* SimpleX SE.appex in Embed App Extensions */, 5CE2BA9D284555F500EC33A6 /* SimpleX NSE.appex in Embed App Extensions */, ); name = "Embed App Extensions"; @@ -263,10 +327,8 @@ 18415B08031E8FB0F7FC27F9 /* CallViewRenderers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallViewRenderers.swift; sourceTree = "<group>"; }; 18415DAAAD1ADBEDB0EDA852 /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = "<group>"; }; 18415FD2E36F13F596A45BB4 /* CIVideoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIVideoView.swift; sourceTree = "<group>"; }; - 3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteToConnectView.swift; sourceTree = "<group>"; }; 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = "<group>"; }; 3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = "<group>"; }; - 5C00164328A26FBC0094D739 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = "<group>"; }; 5C00168028C4FE760094D739 /* KeyChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyChain.swift; sourceTree = "<group>"; }; 5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = "<group>"; }; 5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = "<group>"; }; @@ -280,6 +342,9 @@ 5C13730A28156D2700F43030 /* ContactConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionView.swift; sourceTree = "<group>"; }; 5C13730C2815740A00F43030 /* DebugJSON.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DebugJSON.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = "<group>"; }; + 5C245F3C2B501E98001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; }; + 5C245F3D2B501F13001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = "tr.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; + 5C245F3E2B501F13001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = "<group>"; }; 5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = "<group>"; }; 5C2E260E27A30FDC00F70299 /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; }; @@ -287,6 +352,9 @@ 5C35CFC727B2782E00FB6C6D /* BGManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGManager.swift; sourceTree = "<group>"; }; 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NtfManager.swift; sourceTree = "<group>"; }; 5C36027227F47AD5009F19D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; + 5C371E4E2BA9AAA200100AD3 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; }; + 5C371E4F2BA9AB6400100AD3 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = "hu.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; + 5C371E502BA9AB6400100AD3 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineWidth.swift; sourceTree = "<group>"; }; 5C3A88D027DF57800060F1C2 /* FramedItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FramedItemView.swift; sourceTree = "<group>"; }; 5C3CCFCB2AE6BD3100C3F0C3 /* ConnectDesktopView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectDesktopView.swift; sourceTree = "<group>"; }; @@ -294,11 +362,6 @@ 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = "<group>"; }; 5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; }; 5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = "<group>"; }; - 5C4E80D52B3CCD090080FAE2 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; }; - 5C4E80D62B3CCD090080FAE2 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; }; - 5C4E80D72B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a"; sourceTree = "<group>"; }; - 5C4E80D82B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a"; sourceTree = "<group>"; }; - 5C4E80D92B3CCD090080FAE2 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; }; 5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = "<group>"; }; 5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = "<group>"; }; 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = "<group>"; }; @@ -324,7 +387,6 @@ 5C65DAED29CB8908003CEE45 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 5C65DAF829D0CC20003CEE45 /* DeveloperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperView.swift; sourceTree = "<group>"; }; 5C65F341297D3F3600B67AF3 /* VersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionView.swift; sourceTree = "<group>"; }; - 5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = "<group>"; }; 5C6BA666289BD954009B8ECC /* DismissSheets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissSheets.swift; sourceTree = "<group>"; }; 5C6D183229E93FBA00D430B3 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = "pl.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; 5C6D183329E93FBA00D430B3 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; }; @@ -381,8 +443,6 @@ 5CB0BA91282713FD00B3292C /* CreateProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfile.swift; sourceTree = "<group>"; }; 5CB0BA992827FD8800B3292C /* HowItWorks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowItWorks.swift; sourceTree = "<group>"; }; 5CB2084E28DA4B4800D024EC /* RTCServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTCServers.swift; sourceTree = "<group>"; }; - 5CB2085028DB64CA00D024EC /* CreateLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateLinkView.swift; sourceTree = "<group>"; }; - 5CB2085228DB7CAF00D024EC /* ConnectViaLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViaLinkView.swift; sourceTree = "<group>"; }; 5CB2085428DE647400D024EC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; }; 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = "<group>"; }; 5CB346E62868D76D001FD2EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; }; @@ -391,7 +451,7 @@ 5CB634AC29E46CF70066AD6B /* LocalAuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthView.swift; sourceTree = "<group>"; }; 5CB634AE29E4BB7D0066AD6B /* SetAppPasscodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetAppPasscodeView.swift; sourceTree = "<group>"; }; 5CB634B029E5EFEA0066AD6B /* PasscodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasscodeView.swift; sourceTree = "<group>"; }; - 5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; }; + 5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; wrapsLines = 0; }; 5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = "<group>"; }; 5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListNavLink.swift; sourceTree = "<group>"; }; 5CBD285529565CAE00EC2CF4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; }; @@ -408,8 +468,6 @@ 5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = "<group>"; }; 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = "<group>"; }; - 5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = "<group>"; }; - 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = "<group>"; }; 5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = "<group>"; }; 5CD67B8E2B0E858A00C510B1 /* hs_init.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hs_init.c; sourceTree = "<group>"; }; 5CDCAD452818589900503DA2 /* SimpleX NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX NSE.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -436,23 +494,24 @@ 5CE6C7B42AAB1527007F345C /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; }; 5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; }; 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; }; - 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; }; 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = "<group>"; }; - 5CF9371D2B23429500E1D781 /* ConcurrentQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrentQueue.swift; sourceTree = "<group>"; }; 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFileSubscriber.swift; sourceTree = "<group>"; }; 5CF937212B25034A00E1D781 /* NSESubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSESubscriber.swift; sourceTree = "<group>"; }; 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; }; - 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; }; 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; }; + 640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatMenuButton.swift; sourceTree = "<group>"; }; + 640417CC2B29B8C200CCB412 /* NewChatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatView.swift; sourceTree = "<group>"; }; + 640743602CD360E600158442 /* ChooseServerOperators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChooseServerOperators.swift; sourceTree = "<group>"; }; 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = "<group>"; }; 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = "<group>"; }; 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = "<group>"; }; + 642BA82C2CE50495005E9412 /* NewServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewServerView.swift; sourceTree = "<group>"; }; 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; }; + 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperatorView.swift; sourceTree = "<group>"; }; 6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; }; 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; }; 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = "<group>"; }; 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatInfoView.swift; sourceTree = "<group>"; }; - 64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSimpleXAddress.swift; sourceTree = "<group>"; }; 64466DCB29FFE3E800E3D48D /* MailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailView.swift; sourceTree = "<group>"; }; 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLinkView.swift; sourceTree = "<group>"; }; 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeVoiceView.swift; sourceTree = "<group>"; }; @@ -464,6 +523,7 @@ 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = "<group>"; }; 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberInfoView.swift; sourceTree = "<group>"; }; 648010AA281ADD15009009B9 /* CIFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIFileView.swift; sourceTree = "<group>"; }; + 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemForwardingView.swift; sourceTree = "<group>"; }; 6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = "<group>"; }; 649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; }; @@ -471,17 +531,122 @@ 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; }; 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; }; 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; }; + 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; }; + 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.7.0-3RUUqzi5CrDKdTTh7MfNU-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.7.0-3RUUqzi5CrDKdTTh7MfNU-ghc9.6.3.a"; sourceTree = "<group>"; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.7.0-3RUUqzi5CrDKdTTh7MfNU.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.7.0-3RUUqzi5CrDKdTTh7MfNU.a"; sourceTree = "<group>"; }; + 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; }; 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.swift; sourceTree = "<group>"; }; 64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; }; 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; }; + 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = "<group>"; }; 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; }; - 8C05382D2B39887E006436DC /* VideoUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoUtils.swift; sourceTree = "<group>"; }; + 8C01E9BF2C8EFBB6008A4B0A /* objc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = objc.h; sourceTree = "<group>"; }; + 8C01E9C02C8EFC33008A4B0A /* objc.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = objc.m; sourceTree = "<group>"; }; + 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; }; + 8C74C3EB2C1B92A900039E77 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; }; + 8C74C3ED2C1B942300039E77 /* ChatWallpaper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWallpaper.swift; sourceTree = "<group>"; }; + 8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToDevice.swift; sourceTree = "<group>"; }; + 8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateFromDevice.swift; sourceTree = "<group>"; }; + 8C7E3CE32C0DEAC400BFF63A /* ThemeTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeTypes.swift; sourceTree = "<group>"; }; + 8C7F8F0D2C19C0C100D16888 /* ViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifiers.swift; sourceTree = "<group>"; }; + 8C804B1D2C11F966007A63C8 /* ChatWallpaperTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWallpaperTypes.swift; sourceTree = "<group>"; }; + 8C81482B2BD91CD4002CBEC3 /* AudioDevicePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioDevicePicker.swift; sourceTree = "<group>"; }; + 8C852B072C1086D100BA61E8 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = "<group>"; }; + 8C86EBE42C0DAE4F00E12243 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; }; + 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeModeEditor.swift; sourceTree = "<group>"; }; + 8CAD466E2D15A8100078D18F /* ChatScrollHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatScrollHelpers.swift; sourceTree = "<group>"; }; + 8CAEF14F2D11A6A000240F00 /* ChatItemsLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemsLoader.swift; sourceTree = "<group>"; }; + 8CB15E9F2CFDA30600C28209 /* ChatItemsMerger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemsMerger.swift; sourceTree = "<group>"; }; + 8CB3476D2CF5F58B006787A5 /* ConditionsWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionsWebView.swift; sourceTree = "<group>"; }; + 8CBC14852D357CDB00BBD901 /* StorageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageView.swift; sourceTree = "<group>"; }; + 8CC317432D4FEB9B00292A20 /* EndlessScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EndlessScrollView.swift; sourceTree = "<group>"; }; + 8CC317452D4FEBA800292A20 /* ScrollViewCells.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollViewCells.swift; sourceTree = "<group>"; }; + 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = "<group>"; }; + 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = "<group>"; }; + 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableChatItemToolbars.swift; sourceTree = "<group>"; }; + B70A39722D24090D00E80A5F /* TagListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagListView.swift; sourceTree = "<group>"; }; + B70CE9E52D4BE5930080F36D /* GroupMentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMentions.swift; sourceTree = "<group>"; }; + B73EFE522CE5FA3500C778EA /* CreateSimpleXAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateSimpleXAddress.swift; sourceTree = "<group>"; }; + B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListNavLink.swift; sourceTree = "<group>"; }; + B79ADAFE2CE4EF930083DFFD /* AddressCreationCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCreationCard.swift; sourceTree = "<group>"; }; + CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvertedForegroundStyle.swift; sourceTree = "<group>"; }; + CE1EB0E32C459A660099D896 /* ShareAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAPI.swift; sourceTree = "<group>"; }; + CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatUtils.swift; sourceTree = "<group>"; }; + CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = "<group>"; }; + CE7548092C622630009579B7 /* SwipeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeLabel.swift; sourceTree = "<group>"; }; + CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemClipShape.swift; sourceTree = "<group>"; }; + CEA6E91B2CBD21B0002B5DB4 /* UserDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefault.swift; sourceTree = "<group>"; }; + CEDB245A2C9CD71800FBC5F6 /* StickyScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickyScrollView.swift; sourceTree = "<group>"; }; + CEDE70212C48FD9500233B1F /* SEChatState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SEChatState.swift; sourceTree = "<group>"; }; + CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX SE.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; }; + CEE723AE2C3BD3D70009AE93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + CEE723D42C3C21F50009AE93 /* SimpleX SE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX SE.entitlements"; sourceTree = "<group>"; }; + CEE723EF2C3D25C70009AE93 /* ShareView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareView.swift; sourceTree = "<group>"; }; + CEE723F12C3D25ED0009AE93 /* ShareModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareModel.swift; sourceTree = "<group>"; }; + CEFB2EDE2CA1BCC7004B1ECE /* SheetRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetRepresentable.swift; sourceTree = "<group>"; }; D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = "<group>"; }; D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; }; D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; }; D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = "<group>"; }; + E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9732C590275007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; }; + E5DCF9742C590276007928CC /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9752C590277007928CC /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9762C590278007928CC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9772C590279007928CC /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9782C590279007928CC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9792C59027A007928CC /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF97A2C59027A007928CC /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF97B2C59027B007928CC /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF97C2C59027B007928CC /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF97D2C59027C007928CC /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF97E2C59027C007928CC /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF97F2C59027D007928CC /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9802C59027D007928CC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9812C59027D007928CC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9832C5902CE007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9852C5902D4007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9862C5902D5007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; }; + E5DCF9872C5902D8007928CC /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9882C5902DC007928CC /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9892C5902DC007928CC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF98A2C5902DD007928CC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF98B2C5902DD007928CC /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF98C2C5902DE007928CC /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF98D2C5902DE007928CC /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF98E2C5902E0007928CC /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF98F2C5902E0007928CC /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9902C5902E1007928CC /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9912C5902E1007928CC /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9922C5902E2007928CC /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9932C5902E2007928CC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9942C5902E3007928CC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; }; + E5DCF9952C59067B007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + E5DCF9972C5906FF007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + E5DCF9992C59072A007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + E5DCF99A2C59072B007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; + E5DCF99B2C59072B007928CC /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + E5DCF99C2C59072C007928CC /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + E5DCF99D2C59072D007928CC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + E5DCF99E2C59072E007928CC /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + E5DCF99F2C59072E007928CC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + E5DCF9A02C59072F007928CC /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + E5DCF9A12C59072F007928CC /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + E5DCF9A22C590730007928CC /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + E5DCF9A32C590730007928CC /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + E5DCF9A42C590731007928CC /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + E5DCF9A52C590731007928CC /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + E5DCF9A62C590731007928CC /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + E5DCF9A72C590732007928CC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + E5DCF9A82C590732007928CC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + E5DDBE6D2DC4106200A0EFF0 /* AppAPITypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAPITypes.swift; sourceTree = "<group>"; }; + E5DDBE6F2DC4217900A0EFF0 /* NSEAPITypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEAPITypes.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -490,6 +655,9 @@ buildActionMask = 2147483647; files = ( 5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */, + B728945B2D0C62BF00F7A19A /* ElegantEmojiPicker in Frameworks */, + 8C8118722C220B5B00E6FC94 /* Yams in Frameworks */, + 8CB3476C2CF5CFFA006787A5 /* Ink in Frameworks */, D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */, D7197A1829AE89660055C05A /* WebRTC in Frameworks */, D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */, @@ -519,13 +687,22 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5C4E80DD2B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5C4E80DA2B3CCD090080FAE2 /* libgmp.a in Frameworks */, - 5C4E80DC2B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5C4E80DB2B3CCD090080FAE2 /* libffi.a in Frameworks */, - 5C4E80DE2B3CCD090080FAE2 /* libgmpxx.a in Frameworks */, + 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, + 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, + 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.7.0-3RUUqzi5CrDKdTTh7MfNU-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.7.0-3RUUqzi5CrDKdTTh7MfNU.a in Frameworks */, + CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E5DCF8DA2C56FABA007928CC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -543,6 +720,8 @@ 5C55A922283CEDE600C4E99E /* SoundPlayer.swift */, 18415323A4082FC92887F906 /* WebRTCClient.swift */, 18415B08031E8FB0F7FC27F9 /* CallViewRenderers.swift */, + 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */, + 8C81482B2BD91CD4002CBEC3 /* AudioDevicePicker.swift */, ); path = Call; sourceTree = "<group>"; @@ -550,6 +729,7 @@ 5C2E260D27A30E2400F70299 /* Views */ = { isa = PBXGroup; children = ( + B76E6C2F2C5C41C300EC11AA /* Contacts */, 5CB0BA8C282711BC00B3292C /* Onboarding */, 3C714775281C080100CB4D4B /* Call */, 5C971E1F27AEBF7000C8A3CE /* Helpers */, @@ -558,6 +738,7 @@ 5CB924DD27A8622200ACCCDD /* NewChat */, 5CFA59C22860B04D00863A68 /* Database */, 5CB634AB29E46CDB0066AD6B /* LocalAuth */, + 8C7D94982B8894D300B7B9E1 /* Migration */, 5CA8D01B2AD9B076001FD661 /* RemoteAccess */, 5CB924DF27A8678B00ACCCDD /* UserSettings */, 5C2E261127A30FEA00F70299 /* TerminalView.swift */, @@ -568,6 +749,8 @@ 5C5F4AC227A5E9AF00B51EF1 /* Chat */ = { isa = PBXGroup; children = ( + 8CC317452D4FEBA800292A20 /* ScrollViewCells.swift */, + 8CC317432D4FEB9B00292A20 /* EndlessScrollView.swift */, 6440CA01288AEC770062C672 /* Group */, 5CE4407427ADB657007B033A /* ChatItem */, 5CEACCE527DE977C000BD591 /* ComposeMessage */, @@ -580,6 +763,11 @@ 5CBE6C11294487F7002D9531 /* VerifyCodeView.swift */, 5CBE6C132944CC12002D9531 /* ScanCodeView.swift */, 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */, + 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */, + 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */, + 8CB15E9F2CFDA30600C28209 /* ChatItemsMerger.swift */, + 8CAEF14F2D11A6A000240F00 /* ChatItemsLoader.swift */, + 8CAD466E2D15A8100078D18F /* ChatScrollHelpers.swift */, ); path = Chat; sourceTree = "<group>"; @@ -587,11 +775,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5C4E80D62B3CCD090080FAE2 /* libffi.a */, - 5C4E80D52B3CCD090080FAE2 /* libgmp.a */, - 5C4E80D92B3CCD090080FAE2 /* libgmpxx.a */, - 5C4E80D82B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5-ghc9.6.3.a */, - 5C4E80D72B3CCD090080FAE2 /* libHSsimplex-chat-5.4.2.1-FP1oxJSttEYhorN1FRfI5.a */, + 64C829992D54AEEE006B9E89 /* libffi.a */, + 64C829982D54AEED006B9E89 /* libgmp.a */, + 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.7.0-3RUUqzi5CrDKdTTh7MfNU-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.7.0-3RUUqzi5CrDKdTTh7MfNU.a */, ); path = Libraries; sourceTree = "<group>"; @@ -611,6 +799,7 @@ 5C764E87279CBC8E000C6508 /* Model */ = { isa = PBXGroup; children = ( + E5DDBE6D2DC4106200A0EFF0 /* AppAPITypes.swift */, 5C764E88279CBCB3000C6508 /* ChatModel.swift */, 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */, 5C35CFC727B2782E00FB6C6D /* BGManager.swift */, @@ -619,7 +808,7 @@ 5CF937212B25034A00E1D781 /* NSESubscriber.swift */, 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */, 5C93293E2928E0FD0090FFF9 /* AudioRecPlay.swift */, - 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */, + 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */, ); path = Model; sourceTree = "<group>"; @@ -636,15 +825,21 @@ 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */, 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */, 5C6BA666289BD954009B8ECC /* DismissSheets.swift */, - 5C00164328A26FBC0094D739 /* ContextMenu.swift */, 5CA7DFC229302AF000F7FDDE /* AppSheet.swift */, 18415A7F0F189D87DEFEABCA /* PressedButtonStyle.swift */, 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */, 18415DAAAD1ADBEDB0EDA852 /* VideoPlayerView.swift */, 64466DCB29FFE3E800E3D48D /* MailView.swift */, 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */, - 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */, - 8C05382D2B39887E006436DC /* VideoUtils.swift */, + 8C7F8F0D2C19C0C100D16888 /* ViewModifiers.swift */, + 8C74C3ED2C1B942300039E77 /* ChatWallpaper.swift */, + 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */, + CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */, + CE7548092C622630009579B7 /* SwipeLabel.swift */, + CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */, + CEDB245A2C9CD71800FBC5F6 /* StickyScrollView.swift */, + CEFB2EDE2CA1BCC7004B1ECE /* SheetRepresentable.swift */, + CEA6E91B2CBD21B0002B5DB4 /* UserDefault.swift */, ); path = Helpers; sourceTree = "<group>"; @@ -659,6 +854,7 @@ 5C764E5C279C70B7000C6508 /* Libraries */, 5CA059C2279559F40002BEB4 /* Shared */, 5CDCAD462818589900503DA2 /* SimpleX NSE */, + CEE723A82C3BD3D70009AE93 /* SimpleX SE */, 5CA059DA279559F40002BEB4 /* Tests iOS */, 5CE2BA692845308900EC33A6 /* SimpleXChat */, 5CA059CB279559F40002BEB4 /* Products */, @@ -669,6 +865,7 @@ 5CA059C2279559F40002BEB4 /* Shared */ = { isa = PBXGroup; children = ( + 8C74C3E92C1B909200039E77 /* Theme */, 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */, 5C36027227F47AD5009F19D9 /* AppDelegate.swift */, 5CA059C4279559F40002BEB4 /* ContentView.swift */, @@ -688,6 +885,7 @@ 5CA059D7279559F40002BEB4 /* Tests iOS.xctest */, 5CDCAD452818589900503DA2 /* SimpleX NSE.appex */, 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */, + CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */, ); name = Products; sourceTree = "<group>"; @@ -712,13 +910,15 @@ 5CB0BA8C282711BC00B3292C /* Onboarding */ = { isa = PBXGroup; children = ( + B73EFE522CE5FA3500C778EA /* CreateSimpleXAddress.swift */, 5CB0BA8D2827126500B3292C /* OnboardingView.swift */, 5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */, 5CB0BA992827FD8800B3292C /* HowItWorks.swift */, 5CB0BA91282713FD00B3292C /* CreateProfile.swift */, - 64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */, 5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */, 5CBD285B29575B8E00EC2CF4 /* WhatsNewView.swift */, + 640743602CD360E600158442 /* ChooseServerOperators.swift */, + B79ADAFE2CE4EF930083DFFD /* AddressCreationCard.swift */, ); path = Onboarding; sourceTree = "<group>"; @@ -737,14 +937,10 @@ 5CB924DD27A8622200ACCCDD /* NewChat */ = { isa = PBXGroup; children = ( - 5C6AD81227A834E300348BD7 /* NewChatButton.swift */, - 5CCD403327A5F6DF00368C90 /* AddContactView.swift */, - 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */, - 3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */, + 640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */, + 640417CC2B29B8C200CCB412 /* NewChatView.swift */, 5CC1C99127A6C7F5000D9FF6 /* QRCode.swift */, 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */, - 5CB2085028DB64CA00D024EC /* CreateLinkView.swift */, - 5CB2085228DB7CAF00D024EC /* ConnectViaLinkView.swift */, 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */, ); path = NewChat; @@ -753,10 +949,9 @@ 5CB924DF27A8678B00ACCCDD /* UserSettings */ = { isa = PBXGroup; children = ( + 643B3B4C2CCFD34B0083A2CF /* NetworkAndServers */, 5CB924D627A8563F00ACCCDD /* SettingsView.swift */, 5CB346E62868D76D001FD2EF /* NotificationsView.swift */, - 5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */, - 5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */, 5CADE79929211BB900072E13 /* PreferencesView.swift */, 5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */, 5C05DF522840AA1D00C683F9 /* CallSettings.swift */, @@ -764,9 +959,6 @@ 5CC036DF29C488D500C0EF20 /* HiddenProfileView.swift */, 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */, 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */, - 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */, - 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */, - 5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */, 5CB2084E28DA4B4800D024EC /* RTCServers.swift */, 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */, 18415845648CA4F5A8BCA272 /* UserProfilesView.swift */, @@ -775,6 +967,8 @@ 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */, 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */, 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */, + 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */, + 8CBC14852D357CDB00BBD901 /* StorageView.swift */, ); path = UserSettings; sourceTree = "<group>"; @@ -790,6 +984,9 @@ 5C13730A28156D2700F43030 /* ContactConnectionView.swift */, 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */, 18415835CBD939A9ABDC108A /* UserPicker.swift */, + 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */, + E51CC1E52C62085600DB91FE /* OneHandUICard.swift */, + B70A39722D24090D00E80A5F /* TagListView.swift */, ); path = ChatList; sourceTree = "<group>"; @@ -798,10 +995,11 @@ isa = PBXGroup; children = ( 5CDCAD5128186DE400503DA2 /* SimpleX NSE.entitlements */, - 5CF9371D2B23429500E1D781 /* ConcurrentQueue.swift */, + E5DDBE6F2DC4217900A0EFF0 /* NSEAPITypes.swift */, 5CDCAD472818589900503DA2 /* NotificationService.swift */, 5CDCAD492818589900503DA2 /* Info.plist */, 5CB0BA862826CB3A00B3292C /* InfoPlist.strings */, + E5DCF9822C5902CE007928CC /* Localizable.strings */, ); path = "SimpleX NSE"; sourceTree = "<group>"; @@ -809,13 +1007,17 @@ 5CE2BA692845308900EC33A6 /* SimpleXChat */ = { isa = PBXGroup; children = ( + 8C86EBE32C0DAE3700E12243 /* Theme */, 5CDCAD5228186F9500503DA2 /* AppGroup.swift */, 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */, 5CDCAD7428188D2900503DA2 /* APITypes.swift */, 5C5E5D3C282447AB00B0488A /* CallTypes.swift */, - 5C9FD96A27A56D4D0075386C /* JSON.swift */, 5CDCAD7D2818941F00503DA2 /* API.swift */, + CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */, + 5C9FD96A27A56D4D0075386C /* JSON.swift */, 5CDCAD80281A7E2700503DA2 /* Notifications.swift */, + 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */, + CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */, 64DAE1502809D9F5000DA960 /* FileUtils.swift */, 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */, 5C00168028C4FE760094D739 /* KeyChain.swift */, @@ -826,6 +1028,8 @@ 5CE2BA96284537A800EC33A6 /* dummy.m */, 5CD67B8D2B0E858A00C510B1 /* hs_init.h */, 5CD67B8E2B0E858A00C510B1 /* hs_init.c */, + 8C01E9BF2C8EFBB6008A4B0A /* objc.h */, + 8C01E9C02C8EFC33008A4B0A /* objc.m */, ); path = SimpleXChat; sourceTree = "<group>"; @@ -880,7 +1084,6 @@ isa = PBXGroup; children = ( 5C4B3B09285FB130003915F2 /* DatabaseView.swift */, - 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */, 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */, 5C9CC7A828C532AB00BEF955 /* DatabaseErrorView.swift */, 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */, @@ -888,6 +1091,21 @@ path = Database; sourceTree = "<group>"; }; + 643B3B4C2CCFD34B0083A2CF /* NetworkAndServers */ = { + isa = PBXGroup; + children = ( + 5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */, + 642BA82C2CE50495005E9412 /* NewServerView.swift */, + 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */, + 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */, + 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */, + 5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */, + 5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */, + 8CB3476D2CF5F58B006787A5 /* ConditionsWebView.swift */, + ); + path = NetworkAndServers; + sourceTree = "<group>"; + }; 6440CA01288AEC770062C672 /* Group */ = { isa = PBXGroup; children = ( @@ -898,10 +1116,63 @@ 5C9C2DA42894777E00CC63B1 /* GroupProfileView.swift */, 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */, 1841516F0CE5992B0EDFB377 /* GroupWelcomeView.swift */, + B70CE9E52D4BE5930080F36D /* GroupMentions.swift */, ); path = Group; sourceTree = "<group>"; }; + 8C74C3E92C1B909200039E77 /* Theme */ = { + isa = PBXGroup; + children = ( + 8C86EBE42C0DAE4F00E12243 /* ThemeManager.swift */, + 8C74C3EB2C1B92A900039E77 /* Theme.swift */, + ); + path = Theme; + sourceTree = "<group>"; + }; + 8C7D94982B8894D300B7B9E1 /* Migration */ = { + isa = PBXGroup; + children = ( + 8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */, + 8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */, + ); + path = Migration; + sourceTree = "<group>"; + }; + 8C86EBE32C0DAE3700E12243 /* Theme */ = { + isa = PBXGroup; + children = ( + 8C7E3CE32C0DEAC400BFF63A /* ThemeTypes.swift */, + 8C852B072C1086D100BA61E8 /* Color.swift */, + 8C804B1D2C11F966007A63C8 /* ChatWallpaperTypes.swift */, + ); + path = Theme; + sourceTree = "<group>"; + }; + B76E6C2F2C5C41C300EC11AA /* Contacts */ = { + isa = PBXGroup; + children = ( + B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */, + ); + path = Contacts; + sourceTree = "<group>"; + }; + CEE723A82C3BD3D70009AE93 /* SimpleX SE */ = { + isa = PBXGroup; + children = ( + CEE723D42C3C21F50009AE93 /* SimpleX SE.entitlements */, + CEE723AE2C3BD3D70009AE93 /* Info.plist */, + CEDE70212C48FD9500233B1F /* SEChatState.swift */, + CE1EB0E32C459A660099D896 /* ShareAPI.swift */, + CEE723F12C3D25ED0009AE93 /* ShareModel.swift */, + CEE723EF2C3D25C70009AE93 /* ShareView.swift */, + CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */, + E5DCF96F2C590272007928CC /* Localizable.strings */, + E5DCF9962C5906FF007928CC /* InfoPlist.strings */, + ); + path = "SimpleX SE"; + sourceTree = "<group>"; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -911,6 +1182,7 @@ files = ( 5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */, 5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */, + 8C01E9C22C8EFF8F008A4B0A /* objc.h in Headers */, 5CE2BA952845354B00EC33A6 /* SimpleX.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -933,6 +1205,7 @@ dependencies = ( 5CE2BA6F2845308900EC33A6 /* PBXTargetDependency */, 5CE2BA9F284555F500EC33A6 /* PBXTargetDependency */, + CEE723B02C3BD3D70009AE93 /* PBXTargetDependency */, ); name = "SimpleX (iOS)"; packageProductDependencies = ( @@ -940,6 +1213,9 @@ D77B92DB2952372200A5A1CC /* SwiftyGif */, D7F0E33829964E7E0068AF69 /* LZString */, D7197A1729AE89660055C05A /* WebRTC */, + 8C8118712C220B5B00E6FC94 /* Yams */, + 8CB3476B2CF5CFFA006787A5 /* Ink */, + B728945A2D0C62BF00F7A19A /* ElegantEmojiPicker */, ); productName = "SimpleX (iOS)"; productReference = 5CA059CA279559F40002BEB4 /* SimpleX.app */; @@ -996,11 +1272,30 @@ ); name = SimpleXChat; packageProductDependencies = ( + CE38A29B2C3FCD72005ED185 /* SwiftyGif */, ); productName = SimpleXChat; productReference = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; productType = "com.apple.product-type.framework"; }; + CEE723A62C3BD3D70009AE93 /* SimpleX SE */ = { + isa = PBXNativeTarget; + buildConfigurationList = CEE723B42C3BD3D70009AE93 /* Build configuration list for PBXNativeTarget "SimpleX SE" */; + buildPhases = ( + CEE723A32C3BD3D70009AE93 /* Sources */, + CEE723A52C3BD3D70009AE93 /* Resources */, + E5DCF8DA2C56FABA007928CC /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + CEE723D22C3C21C90009AE93 /* PBXTargetDependency */, + ); + name = "SimpleX SE"; + productName = "SimpleX SE"; + productReference = CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -1008,7 +1303,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1330; + LastSwiftUpdateCheck = 1540; LastUpgradeCheck = 1340; ORGANIZATIONNAME = "SimpleX Chat"; TargetAttributes = { @@ -1028,6 +1323,9 @@ CreatedOnToolsVersion = 13.3; LastSwiftMigration = 1330; }; + CEE723A62C3BD3D70009AE93 = { + CreatedOnToolsVersion = 15.4; + }; }; }; buildConfigurationList = 5CA059C1279559F40002BEB4 /* Build configuration list for PBXProject "SimpleX" */; @@ -1051,6 +1349,8 @@ fi, uk, bg, + tr, + hu, ); mainGroup = 5CA059BD279559F40002BEB4; packageReferences = ( @@ -1058,6 +1358,9 @@ D77B92DA2952372200A5A1CC /* XCRemoteSwiftPackageReference "SwiftyGif" */, D7F0E33729964E7D0068AF69 /* XCRemoteSwiftPackageReference "lzstring-swift" */, D7197A1629AE89660055C05A /* XCRemoteSwiftPackageReference "WebRTC" */, + 8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */, + 8CB3476A2CF5CFFA006787A5 /* XCRemoteSwiftPackageReference "ink" */, + B72894592D0C62BF00F7A19A /* XCRemoteSwiftPackageReference "Elegant-Emoji-Picker" */, ); productRefGroup = 5CA059CB279559F40002BEB4 /* Products */; projectDirPath = ""; @@ -1066,6 +1369,7 @@ 5CA059C9279559F40002BEB4 /* SimpleX (iOS) */, 5CA059D6279559F40002BEB4 /* Tests iOS */, 5CDCAD442818589900503DA2 /* SimpleX NSE */, + CEE723A62C3BD3D70009AE93 /* SimpleX SE */, 5CE2BA672845308900EC33A6 /* SimpleXChat */, ); }; @@ -1095,6 +1399,7 @@ buildActionMask = 2147483647; files = ( 5CB0BA882826CB3A00B3292C /* InfoPlist.strings in Resources */, + E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1105,6 +1410,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CEE723A52C3BD3D70009AE93 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E5DCF9712C590272007928CC /* Localizable.strings in Resources */, + E5DCF9982C5906FF007928CC /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1113,13 +1427,17 @@ buildActionMask = 2147483647; files = ( 64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */, + 8CC317442D4FEB9B00292A20 /* EndlessScrollView.swift in Sources */, + 640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */, 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */, - 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */, + 640743612CD360E600158442 /* ChooseServerOperators.swift in Sources */, 5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */, 5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */, 5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */, 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */, + B79ADAFF2CE4EF930083DFFD /* AddressCreationCard.swift in Sources */, 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */, + E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */, 5C65DAF929D0CC20003CEE45 /* DeveloperView.swift in Sources */, 5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */, 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */, @@ -1127,11 +1445,13 @@ 5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */, 644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */, 6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */, + 8CC317462D4FEBA800292A20 /* ScrollViewCells.swift in Sources */, 5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */, 5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */, - 5CBD285A295711D700EC2CF4 /* ImageUtils.swift in Sources */, + 8C74C3EC2C1B92A900039E77 /* Theme.swift in Sources */, 6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */, 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */, + 8C7D949A2B88952700B7B9E1 /* MigrateToDevice.swift in Sources */, 5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */, 5C029EAA283942EA004A9677 /* CallController.swift in Sources */, 5CBE6C142944CC12002D9531 /* ScanCodeView.swift in Sources */, @@ -1143,16 +1463,19 @@ 644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */, 5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */, 5CB2084F28DA4B4800D024EC /* RTCServers.swift in Sources */, + B73EFE532CE5FA3500C778EA /* CreateSimpleXAddress.swift in Sources */, 5CB634AF29E4BB7D0066AD6B /* SetAppPasscodeView.swift in Sources */, 5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */, 5CBE6C12294487F7002D9531 /* VerifyCodeView.swift in Sources */, 3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */, - 64466DC829FC2B3B00E3D48D /* CreateSimpleXAddress.swift in Sources */, + 648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */, 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */, 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */, + B76E6C312C5C41D900EC11AA /* ContactListNavLink.swift in Sources */, 5C10D88A28F187F300E58BF0 /* FullScreenMediaView.swift in Sources */, D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */, - 5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */, + B70CE9E62D4BE5930080F36D /* GroupMentions.swift in Sources */, + CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */, 64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */, 5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */, 5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */, @@ -1160,14 +1483,15 @@ 5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */, 5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */, 5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */, + 8C7F8F0E2C19C0C100D16888 /* ViewModifiers.swift in Sources */, 5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */, - 3C8C548928133C84000A3EC7 /* PasteToConnectView.swift in Sources */, 5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */, + 8CBC14862D357CDB00BBD901 /* StorageView.swift in Sources */, 5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */, 5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */, 6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */, + 8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */, 5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */, - 5CB2085128DB64CA00D024EC /* CreateLinkView.swift in Sources */, 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */, 5CA7DFC329302AF000F7FDDE /* AppSheet.swift in Sources */, 64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */, @@ -1176,8 +1500,8 @@ 5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */, 649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */, 5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */, - 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */, 5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */, + 640417CD2B29B8C200CCB412 /* NewChatMenuButton.swift in Sources */, 5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */, 5C3A88CE27DF50170060F1C2 /* DetermineWidth.swift in Sources */, 5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */, @@ -1185,60 +1509,73 @@ 5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */, 647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */, 646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */, + 8C74C3EA2C1B90AF00039E77 /* ThemeManager.swift in Sources */, 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */, - 5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */, 5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */, 5CB634B129E5EFEA0066AD6B /* PasscodeView.swift in Sources */, + 8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */, 5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */, + CEFB2EDF2CA1BCC7004B1ECE /* SheetRepresentable.swift in Sources */, 5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */, 6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */, 5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */, 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */, 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */, + 8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */, 64466DCC29FFE3E800E3D48D /* MailView.swift in Sources */, + CE75480A2C622630009579B7 /* SwipeLabel.swift in Sources */, 5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */, 5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */, 6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */, 5C5DB70E289ABDD200730FFF /* AppearanceSettings.swift in Sources */, 5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */, + 8C74C3EE2C1B942300039E77 /* ChatWallpaper.swift in Sources */, 5C3CCFCC2AE6BD3100C3F0C3 /* ConnectDesktopView.swift in Sources */, 5C9C2DA92899DA6F00CC63B1 /* NetworkAndServers.swift in Sources */, 5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */, 5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */, 6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */, + 8CAD466F2D15A8100078D18F /* ChatScrollHelpers.swift in Sources */, 644EFFDE292BCD9D00525D5B /* ComposeVoiceView.swift in Sources */, 5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */, 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */, 6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */, + E5DDBE6E2DC4106800A0EFF0 /* AppAPITypes.swift in Sources */, + 8C9BC2652C240D5200875A27 /* ThemeModeEditor.swift in Sources */, 5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */, 5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */, - 5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */, - 5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */, 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */, - 8C05382E2B39887E006436DC /* VideoUtils.swift in Sources */, 5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */, + CEA6E91C2CBD21B0002B5DB4 /* UserDefault.swift in Sources */, 5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */, + 8CAEF1502D11A6A000240F00 /* ChatItemsLoader.swift in Sources */, 5C9C2DA52894777E00CC63B1 /* GroupProfileView.swift in Sources */, 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */, + 8C81482C2BD91CD4002CBEC3 /* AudioDevicePicker.swift in Sources */, 5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */, 5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */, 5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */, 5CBD285C29575B8E00EC2CF4 /* WhatsNewView.swift in Sources */, + 8CB3476E2CF5F58B006787A5 /* ConditionsWebView.swift in Sources */, 5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */, 5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */, + B70A39732D24090D00E80A5F /* TagListView.swift in Sources */, 5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */, 6440CA00288857A10062C672 /* CIEventView.swift in Sources */, 5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */, 5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */, 5C9329412929248A0090FFF9 /* ScanProtocolServer.swift in Sources */, + 8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */, + 64EEB0F72C353F1C00972D62 /* ServersSummaryView.swift in Sources */, 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */, 5C93293F2928E0FD0090FFF9 /* AudioRecPlay.swift in Sources */, 5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */, 5CE4407227ADB1D0007B033A /* Emoji.swift in Sources */, - 5CB2085328DB7CAF00D024EC /* ConnectViaLinkView.swift in Sources */, + CEDB245B2C9CD71800FBC5F6 /* StickyScrollView.swift in Sources */, 5C9CC7A928C532AB00BEF955 /* DatabaseErrorView.swift in Sources */, 5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */, 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */, + CE176F202C87014C00145DBC /* InvertedForegroundStyle.swift in Sources */, 5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */, 5C9C2DA7289957AE00CC63B1 /* AdvancedNetworkSettings.swift in Sources */, 5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */, @@ -1249,12 +1586,16 @@ 18415B0585EB5A9A0A7CA8CD /* PressedButtonStyle.swift in Sources */, 1841560FD1CD447955474C1D /* UserProfilesView.swift in Sources */, 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */, + 8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */, 18415C6C56DBCEC2CBBD2F11 /* WebRTCClient.swift in Sources */, + 8CB15EA02CFDA30600C28209 /* ChatItemsMerger.swift in Sources */, 184152CEF68D2336FC2EBCB0 /* CallViewRenderers.swift in Sources */, 5CB634AD29E46CF70066AD6B /* LocalAuthView.swift in Sources */, 18415FEFE153C5920BFB7828 /* GroupWelcomeView.swift in Sources */, 18415F9A2D551F9757DA4654 /* CIVideoView.swift in Sources */, + 642BA82D2CE50495005E9412 /* NewServerView.swift in Sources */, 184158C131FDB829D8A117EA /* VideoPlayerView.swift in Sources */, + 643B3B4E2CCFD6400083A2CF /* OperatorView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1272,8 +1613,8 @@ buildActionMask = 2147483647; files = ( 5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */, + E5DDBE702DC4217900A0EFF0 /* NSEAPITypes.swift in Sources */, 5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */, - 5CF9371E2B23429500E1D781 /* ConcurrentQueue.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1282,22 +1623,41 @@ buildActionMask = 2147483647; files = ( 5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */, + CE3097FB2C4C0C9F00180898 /* ErrorAlert.swift in Sources */, 5C00168128C4FE760094D739 /* KeyChain.swift in Sources */, 5CE2BA97284537A800EC33A6 /* dummy.m in Sources */, 5CE2BA922845340900EC33A6 /* FileUtils.swift in Sources */, + 8C74C3E72C1B901900039E77 /* Color.swift in Sources */, 5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */, 5CE2BA91284533A300EC33A6 /* Notifications.swift in Sources */, + 8C01E9C12C8EFC33008A4B0A /* objc.m in Sources */, 5CE2BA79284530CC00EC33A6 /* SimpleXChat.docc in Sources */, 5CE2BA90284533A300EC33A6 /* JSON.swift in Sources */, 5CE2BA8B284533A300EC33A6 /* ChatTypes.swift in Sources */, 5CE2BA8F284533A300EC33A6 /* APITypes.swift in Sources */, + CE38A29A2C3FCA54005ED185 /* ImageUtils.swift in Sources */, 5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */, 5CE2BA8C284533A300EC33A6 /* AppGroup.swift in Sources */, + 8C74C3E52C1B900600039E77 /* ThemeTypes.swift in Sources */, 5CE2BA8D284533A300EC33A6 /* CallTypes.swift in Sources */, + 8C74C3E82C1B905B00039E77 /* ChatWallpaperTypes.swift in Sources */, + CE2AD9CE2C452A4D00E844E3 /* ChatUtils.swift in Sources */, 5CE2BA8E284533A300EC33A6 /* API.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; + CEE723A32C3BD3D70009AE93 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CEDE70222C48FD9500233B1F /* SEChatState.swift in Sources */, + CEE723F02C3D25C70009AE93 /* ShareView.swift in Sources */, + CE1EB0E42C459A660099D896 /* ShareAPI.swift in Sources */, + CEE723F22C3D25ED0009AE93 /* ShareModel.swift in Sources */, + CEE723AA2C3BD3D70009AE93 /* ShareViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -1322,6 +1682,16 @@ target = 5CE2BA672845308900EC33A6 /* SimpleXChat */; targetProxy = 5CE2BAA82845617C00EC33A6 /* PBXContainerItemProxy */; }; + CEE723B02C3BD3D70009AE93 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = CEE723A62C3BD3D70009AE93 /* SimpleX SE */; + targetProxy = CEE723AF2C3BD3D70009AE93 /* PBXContainerItemProxy */; + }; + CEE723D22C3C21C90009AE93 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5CE2BA672845308900EC33A6 /* SimpleXChat */; + targetProxy = CEE723D12C3C21C90009AE93 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -1342,6 +1712,9 @@ 5C136D8F2AAB3D14006DE2FC /* fi */, 5C636F672AAB3D2400751C84 /* uk */, 5C5B67932ABAF56000DA9412 /* bg */, + 5C245F3E2B501F13001CC39F /* tr */, + 5C371E502BA9AB6400100AD3 /* hu */, + E5DCF9952C59067B007928CC /* en */, ); name = InfoPlist.strings; sourceTree = "<group>"; @@ -1364,6 +1737,8 @@ 5CE6C7B32AAB1515007F345C /* fi */, 5CE6C7B42AAB1527007F345C /* uk */, 5C5B67912ABAF4B500DA9412 /* bg */, + 5C245F3C2B501E98001CC39F /* tr */, + 5C371E4E2BA9AAA200100AD3 /* hu */, ); name = Localizable.strings; sourceTree = "<group>"; @@ -1385,10 +1760,84 @@ 5C136D8E2AAB3D14006DE2FC /* fi */, 5C636F662AAB3D2400751C84 /* uk */, 5C5B67922ABAF56000DA9412 /* bg */, + 5C245F3D2B501F13001CC39F /* tr */, + 5C371E4F2BA9AB6400100AD3 /* hu */, ); name = "SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; + E5DCF96F2C590272007928CC /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + E5DCF9702C590272007928CC /* en */, + E5DCF9722C590274007928CC /* bg */, + E5DCF9732C590275007928CC /* zh-Hans */, + E5DCF9742C590276007928CC /* nl */, + E5DCF9752C590277007928CC /* cs */, + E5DCF9762C590278007928CC /* fi */, + E5DCF9772C590279007928CC /* fr */, + E5DCF9782C590279007928CC /* de */, + E5DCF9792C59027A007928CC /* hu */, + E5DCF97A2C59027A007928CC /* it */, + E5DCF97B2C59027B007928CC /* ja */, + E5DCF97C2C59027B007928CC /* pl */, + E5DCF97D2C59027C007928CC /* ru */, + E5DCF97E2C59027C007928CC /* es */, + E5DCF97F2C59027D007928CC /* th */, + E5DCF9802C59027D007928CC /* uk */, + E5DCF9812C59027D007928CC /* tr */, + ); + name = Localizable.strings; + sourceTree = "<group>"; + }; + E5DCF9822C5902CE007928CC /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + E5DCF9832C5902CE007928CC /* en */, + E5DCF9852C5902D4007928CC /* bg */, + E5DCF9862C5902D5007928CC /* zh-Hans */, + E5DCF9872C5902D8007928CC /* cs */, + E5DCF9882C5902DC007928CC /* nl */, + E5DCF9892C5902DC007928CC /* fi */, + E5DCF98A2C5902DD007928CC /* de */, + E5DCF98B2C5902DD007928CC /* fr */, + E5DCF98C2C5902DE007928CC /* it */, + E5DCF98D2C5902DE007928CC /* hu */, + E5DCF98E2C5902E0007928CC /* ja */, + E5DCF98F2C5902E0007928CC /* pl */, + E5DCF9902C5902E1007928CC /* ru */, + E5DCF9912C5902E1007928CC /* es */, + E5DCF9922C5902E2007928CC /* th */, + E5DCF9932C5902E2007928CC /* tr */, + E5DCF9942C5902E3007928CC /* uk */, + ); + name = Localizable.strings; + sourceTree = "<group>"; + }; + E5DCF9962C5906FF007928CC /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + E5DCF9972C5906FF007928CC /* en */, + E5DCF9992C59072A007928CC /* bg */, + E5DCF99A2C59072B007928CC /* zh-Hans */, + E5DCF99B2C59072B007928CC /* cs */, + E5DCF99C2C59072C007928CC /* nl */, + E5DCF99D2C59072D007928CC /* fi */, + E5DCF99E2C59072E007928CC /* fr */, + E5DCF99F2C59072E007928CC /* de */, + E5DCF9A02C59072F007928CC /* hu */, + E5DCF9A12C59072F007928CC /* it */, + E5DCF9A22C590730007928CC /* ja */, + E5DCF9A32C590730007928CC /* pl */, + E5DCF9A42C590731007928CC /* ru */, + E5DCF9A52C590731007928CC /* es */, + E5DCF9A62C590731007928CC /* th */, + E5DCF9A72C590732007928CC /* tr */, + E5DCF9A82C590732007928CC /* uk */, + ); + name = InfoPlist.strings; + sourceTree = "<group>"; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -1427,6 +1876,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -1488,6 +1938,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -1516,12 +1967,16 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ENABLE_MODULES = YES; + CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; + CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 186; + CURRENT_PROJECT_VERSION = 285; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; + GCC_OPTIMIZATION_LEVEL = s; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "SimpleX--iOS--Info.plist"; INFOPLIST_KEY_NSCameraUsageDescription = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; @@ -1540,11 +1995,14 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.4.2; + LLVM_LTO = YES_THIN; + MARKETING_VERSION = 6.3.7; + OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000"; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; @@ -1559,12 +2017,16 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ENABLE_MODULES = YES; + CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; + CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 186; + CURRENT_PROJECT_VERSION = 285; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; - ENABLE_PREVIEWS = YES; + ENABLE_CODE_COVERAGE = NO; + ENABLE_PREVIEWS = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "SimpleX--iOS--Info.plist"; INFOPLIST_KEY_NSCameraUsageDescription = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; @@ -1583,7 +2045,9 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.4.2; + LLVM_LTO = YES; + MARKETING_VERSION = 6.3.7; + OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000"; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1599,11 +2063,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 285; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 6.3.7; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1619,11 +2083,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 285; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 6.3.7; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1639,12 +2103,15 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; + CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; + CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 186; + CURRENT_PROJECT_VERSION = 285; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; + GCC_OPTIMIZATION_LEVEL = s; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "SimpleX NSE/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "SimpleX NSE"; @@ -1655,13 +2122,15 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.4.2; + LLVM_LTO = YES; + MARKETING_VERSION = 6.3.7; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Osize"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; @@ -1671,12 +2140,15 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; + CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; + CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 186; + CURRENT_PROJECT_VERSION = 285; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; + ENABLE_CODE_COVERAGE = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "SimpleX NSE/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "SimpleX NSE"; @@ -1687,13 +2159,15 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.4.2; + LLVM_LTO = YES; + MARKETING_VERSION = 6.3.7; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Osize"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; @@ -1705,14 +2179,17 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; + CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; + CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 186; + CURRENT_PROJECT_VERSION = 285; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_BITCODE = NO; + GCC_OPTIMIZATION_LEVEL = s; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 SimpleX Chat. All rights reserved."; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -1730,7 +2207,8 @@ "$(inherited)", "$(PROJECT_DIR)/Libraries/sim", ); - MARKETING_VERSION = 5.4.2; + LLVM_LTO = YES; + MARKETING_VERSION = 6.3.7; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -1739,6 +2217,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INCLUDE_PATHS = ""; SWIFT_OBJC_BRIDGING_HEADER = ./SimpleXChat/SimpleX.h; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -1751,14 +2230,17 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; + CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; + CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 186; + CURRENT_PROJECT_VERSION = 285; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_BITCODE = NO; + ENABLE_CODE_COVERAGE = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 SimpleX Chat. All rights reserved."; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -1776,7 +2258,8 @@ "$(inherited)", "$(PROJECT_DIR)/Libraries/sim", ); - MARKETING_VERSION = 5.4.2; + LLVM_LTO = YES; + MARKETING_VERSION = 6.3.7; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -1785,6 +2268,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INCLUDE_PATHS = ""; SWIFT_OBJC_BRIDGING_HEADER = ./SimpleXChat/SimpleX.h; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; @@ -1793,6 +2277,74 @@ }; name = Release; }; + CEE723B22C3BD3D70009AE93 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 285; + DEVELOPMENT_TEAM = 5NN7GUYB6T; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "SimpleX SE/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "SimpleX SE"; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 SimpleX Chat. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 6.3.7; + PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + CEE723B32C3BD3D70009AE93 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 285; + DEVELOPMENT_TEAM = 5NN7GUYB6T; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "SimpleX SE/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "SimpleX SE"; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 SimpleX Chat. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 6.3.7; + PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1841,6 +2393,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + CEE723B42C3BD3D70009AE93 /* Build configuration list for PBXNativeTarget "SimpleX SE" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CEE723B22C3BD3D70009AE93 /* Debug */, + CEE723B32C3BD3D70009AE93 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -1848,8 +2409,32 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/twostraws/CodeScanner"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.0.0; + kind = exactVersion; + version = 2.5.0; + }; + }; + 8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/jpsim/Yams"; + requirement = { + kind = exactVersion; + version = 5.1.2; + }; + }; + 8CB3476A2CF5CFFA006787A5 /* XCRemoteSwiftPackageReference "ink" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/johnsundell/ink"; + requirement = { + kind = exactVersion; + version = 0.6.0; + }; + }; + B72894592D0C62BF00F7A19A /* XCRemoteSwiftPackageReference "Elegant-Emoji-Picker" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Finalet/Elegant-Emoji-Picker"; + requirement = { + branch = main; + kind = branch; }; }; D7197A1629AE89660055C05A /* XCRemoteSwiftPackageReference "WebRTC" */ = { @@ -1864,8 +2449,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kirualex/SwiftyGif"; requirement = { - branch = master; - kind = branch; + kind = revision; + revision = 5e8619335d394901379c9add5c4c1c2f420b3800; }; }; D7F0E33729964E7D0068AF69 /* XCRemoteSwiftPackageReference "lzstring-swift" */ = { @@ -1884,6 +2469,26 @@ package = 5C8F01CB27A6F0D8007D2C8D /* XCRemoteSwiftPackageReference "CodeScanner" */; productName = CodeScanner; }; + 8C8118712C220B5B00E6FC94 /* Yams */ = { + isa = XCSwiftPackageProductDependency; + package = 8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */; + productName = Yams; + }; + 8CB3476B2CF5CFFA006787A5 /* Ink */ = { + isa = XCSwiftPackageProductDependency; + package = 8CB3476A2CF5CFFA006787A5 /* XCRemoteSwiftPackageReference "ink" */; + productName = Ink; + }; + B728945A2D0C62BF00F7A19A /* ElegantEmojiPicker */ = { + isa = XCSwiftPackageProductDependency; + package = B72894592D0C62BF00F7A19A /* XCRemoteSwiftPackageReference "Elegant-Emoji-Picker" */; + productName = ElegantEmojiPicker; + }; + CE38A29B2C3FCD72005ED185 /* SwiftyGif */ = { + isa = XCSwiftPackageProductDependency; + package = D77B92DA2952372200A5A1CC /* XCRemoteSwiftPackageReference "SwiftyGif" */; + productName = SwiftyGif; + }; D7197A1729AE89660055C05A /* WebRTC */ = { isa = XCSwiftPackageProductDependency; package = D7197A1629AE89660055C05A /* XCRemoteSwiftPackageReference "WebRTC" */; diff --git a/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cfacb2381a..2bddf5b5b8 100644 --- a/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,12 +1,31 @@ { + "originHash" : "07434ae88cbf078ce3d27c91c1f605836aaebff0e0cef5f25317795151c77db1", "pins" : [ { "identity" : "codescanner", "kind" : "remoteSourceControl", "location" : "https://github.com/twostraws/CodeScanner", "state" : { - "revision" : "c27a66149b7483fe42e2ec6aad61d5c3fffe522d", - "version" : "2.1.1" + "revision" : "34da57fb63b47add20de8a85da58191523ccce57", + "version" : "2.5.0" + } + }, + { + "identity" : "elegant-emoji-picker", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Finalet/Elegant-Emoji-Picker", + "state" : { + "branch" : "main", + "revision" : "71d2d46092b4d550cc593614efc06438f845f6e6" + } + }, + { + "identity" : "ink", + "kind" : "remoteSourceControl", + "location" : "https://github.com/johnsundell/ink", + "state" : { + "revision" : "bcc9f219900a62c4210e6db726035d7f03ae757b", + "version" : "0.6.0" } }, { @@ -22,7 +41,6 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/kirualex/SwiftyGif", "state" : { - "branch" : "master", "revision" : "5e8619335d394901379c9add5c4c1c2f420b3800" } }, @@ -33,7 +51,16 @@ "state" : { "revision" : "34bedc50f9c58dccf4967ea59c7e6a47d620803b" } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams", + "state" : { + "revision" : "9234124cff5e22e178988c18d8b95a8ae8007f76", + "version" : "5.1.2" + } } ], - "version" : 2 + "version" : 3 } diff --git a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme index 973c30c71e..6a1d4192e6 100644 --- a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme +++ b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme @@ -41,7 +41,7 @@ </Testables> </TestAction> <LaunchAction - buildConfiguration = "Release" + buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle = "0" diff --git a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX NSE.xcscheme b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX NSE.xcscheme index e01b34199b..5467f7b762 100644 --- a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX NSE.xcscheme +++ b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX NSE.xcscheme @@ -2,7 +2,7 @@ <Scheme LastUpgradeVersion = "1400" wasCreatedForAppExtension = "YES" - version = "2.0"> + version = "1.3"> <BuildAction parallelizeBuildables = "YES" buildImplicitDependencies = "YES"> @@ -47,16 +47,14 @@ </TestAction> <LaunchAction buildConfiguration = "Debug" - selectedDebuggerIdentifier = "" - selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle = "0" - askForAppToLaunch = "Yes" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" - allowLocationSimulation = "YES" - launchAutomaticallySubstyle = "2"> + allowLocationSimulation = "YES"> <BuildableProductRunnable runnableDebuggingMode = "0"> <BuildableReference diff --git a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX SE.xcscheme b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX SE.xcscheme new file mode 100644 index 0000000000..a2639eb263 --- /dev/null +++ b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX SE.xcscheme @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1540" + wasCreatedForAppExtension = "YES" + version = "2.0"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES" + buildArchitectures = "Automatic"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "CEE723A62C3BD3D70009AE93" + BuildableName = "SimpleX SE.appex" + BlueprintName = "SimpleX SE" + ReferencedContainer = "container:SimpleX.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "5CA059C9279559F40002BEB4" + BuildableName = "SimpleX.app" + BlueprintName = "SimpleX (iOS)" + ReferencedContainer = "container:SimpleX.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "" + selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn" + launchStyle = "0" + askForAppToLaunch = "Yes" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES" + launchAutomaticallySubstyle = "2"> + <BuildableProductRunnable + runnableDebuggingMode = "0"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "5CA059C9279559F40002BEB4" + BuildableName = "SimpleX.app" + BlueprintName = "SimpleX (iOS)" + ReferencedContainer = "container:SimpleX.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES" + askForAppToLaunch = "Yes" + launchAutomaticallySubstyle = "2"> + <BuildableProductRunnable + runnableDebuggingMode = "0"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "5CA059C9279559F40002BEB4" + BuildableName = "SimpleX.app" + BlueprintName = "SimpleX (iOS)" + ReferencedContainer = "container:SimpleX.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index 8d05a066e8..0dd3483fd7 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -12,7 +12,11 @@ private var chatController: chat_ctrl? private var migrationResult: (Bool, DBMigrationResult)? -public func getChatCtrl(_ useKey: String? = nil) -> chat_ctrl { +public func hasChatCtrl() -> Bool { + chatController != nil +} + +public func getChatCtrl() -> chat_ctrl { if let controller = chatController { return controller } fatalError("chat controller not initialized") } @@ -42,7 +46,7 @@ public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: Migratio var cConfirm = confirm.rawValue.cString(using: .utf8)! // the last parameter of chat_migrate_init is used to return the pointer to chat controller let cjson = chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, backgroundMode ? 1 : 0, &chatController)! - let dbRes = dbMigrationResult(fromCString(cjson)) + let dbRes = dbMigrationResult(dataFromCString(cjson)) let encrypted = dbKey != "" let keychainErr = dbRes == .ok && useKeychain && encrypted && !kcDatabasePassword.set(dbKey) let result = (encrypted, keychainErr ? .errorKeychain : dbRes) @@ -50,7 +54,44 @@ public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: Migratio return result } +public func chatInitTemporaryDatabase(url: URL, key: String? = nil, confirmation: MigrationConfirmation = .error) -> (DBMigrationResult, chat_ctrl?) { + let dbPath = url.path + let dbKey = key ?? randomDatabasePassword() + logger.debug("chatInitTemporaryDatabase path: \(dbPath)") + var temporaryController: chat_ctrl? = nil + var cPath = dbPath.cString(using: .utf8)! + var cKey = dbKey.cString(using: .utf8)! + var cConfirm = confirmation.rawValue.cString(using: .utf8)! + let cjson = chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, 0, &temporaryController)! + return (dbMigrationResult(dataFromCString(cjson)), temporaryController) +} + +public func chatInitControllerRemovingDatabases() { + let dbPath = getAppDatabasePath().path + let fm = FileManager.default + // Remove previous databases, otherwise, can be .errorNotADatabase with nil controller + try? fm.removeItem(atPath: dbPath + CHAT_DB) + try? fm.removeItem(atPath: dbPath + AGENT_DB) + + let dbKey = randomDatabasePassword() + logger.debug("chatInitControllerRemovingDatabases path: \(dbPath)") + var cPath = dbPath.cString(using: .utf8)! + var cKey = dbKey.cString(using: .utf8)! + var cConfirm = MigrationConfirmation.error.rawValue.cString(using: .utf8)! + chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, 0, &chatController) + + // We need only controller, not databases + try? fm.removeItem(atPath: dbPath + CHAT_DB) + try? fm.removeItem(atPath: dbPath + AGENT_DB) +} + + public func chatCloseStore() { + // Prevent crash when exiting the app with already closed store (for example, after changing a database passpharase) + guard hasChatCtrl() else { + logger.error("chatCloseStore: already closed, chatCtrl is nil") + return + } let err = fromCString(chat_close_store(getChatCtrl())) if err != "" { logger.error("chatCloseStore error: \(err)") @@ -69,27 +110,42 @@ public func resetChatCtrl() { migrationResult = nil } -public func sendSimpleXCmd(_ cmd: ChatCommand) -> ChatResponse { - var c = cmd.cmdString.cString(using: .utf8)! - let cjson = chat_send_cmd(getChatCtrl(), &c)! - return chatResponse(fromCString(cjson)) +@inline(__always) +public func sendSimpleXCmd<R: ChatAPIResult>(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl? = nil) -> APIResult<R> { + if let d = sendSimpleXCmdStr(cmd.cmdString, ctrl) { + decodeAPIResult(d) + } else { + APIResult.error(.invalidJSON(json: nil)) + } +} + +@inline(__always) +public func sendSimpleXCmdStr(_ cmd: String, _ ctrl: chat_ctrl? = nil) -> Data? { + var c = cmd.cString(using: .utf8)! + return if let cjson = chat_send_cmd(ctrl ?? getChatCtrl(), &c) { + dataFromCString(cjson) + } else { + nil + } } // in microseconds -let MESSAGE_TIMEOUT: Int32 = 15_000_000 +public let MESSAGE_TIMEOUT: Int32 = 15_000_000 -public func recvSimpleXMsg() -> ChatResponse? { - if let cjson = chat_recv_msg_wait(getChatCtrl(), MESSAGE_TIMEOUT) { - let s = fromCString(cjson) - return s == "" ? nil : chatResponse(s) +@inline(__always) +public func recvSimpleXMsg<R: ChatAPIResult>(_ ctrl: chat_ctrl? = nil, messageTimeout: Int32 = MESSAGE_TIMEOUT) -> APIResult<R>? { + if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), messageTimeout), + let d = dataFromCString(cjson) { + decodeAPIResult(d) + } else { + nil } - return nil } public func parseSimpleXMarkdown(_ s: String) -> [FormattedText]? { var c = s.cString(using: .utf8)! if let cjson = chat_parse_markdown(&c) { - if let d = fromCString(cjson).data(using: .utf8) { + if let d = dataFromCString(cjson) { do { let r = try jsonDecoder.decode(ParsedMarkdown.self, from: d) return r.formattedText @@ -101,6 +157,11 @@ public func parseSimpleXMarkdown(_ s: String) -> [FormattedText]? { return nil } +public func chatJsonLength(_ s: String) -> Int { + var c = s.cString(using: .utf8)! + return Int(chat_json_length(&c)) +} + struct ParsedMarkdown: Decodable { var formattedText: [FormattedText]? } @@ -108,7 +169,7 @@ struct ParsedMarkdown: Decodable { public func parseServerAddress(_ s: String) -> ServerAddress? { var c = s.cString(using: .utf8)! if let cjson = chat_parse_server(&c) { - if let d = fromCString(cjson).data(using: .utf8) { + if let d = dataFromCString(cjson) { do { let r = try jsonDecoder.decode(ParsedServerAddress.self, from: d) return r.serverAddress @@ -125,72 +186,58 @@ struct ParsedServerAddress: Decodable { var parseError: String } +@inline(__always) public func fromCString(_ c: UnsafeMutablePointer<CChar>) -> String { let s = String.init(cString: c) free(c) return s } -public func chatResponse(_ s: String) -> ChatResponse { - let d = s.data(using: .utf8)! - // TODO is there a way to do it without copying the data? e.g: - // let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson)) - // let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free) - do { - let r = try jsonDecoder.decode(APIResponse.self, from: d) - return r.resp - } catch { - logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)") +@inline(__always) +public func dataFromCString(_ c: UnsafeMutablePointer<CChar>) -> Data? { + let len = strlen(c) + if len > 0 { + return Data(bytesNoCopy: c, count: len, deallocator: .free) + } else { + free(c) + return nil } - - var type: String? - var json: String? - if let j = try? JSONSerialization.jsonObject(with: d) as? NSDictionary { - if let jResp = j["resp"] as? NSDictionary, jResp.count == 1 || jResp.count == 2 { - type = jResp.allKeys[0] as? String - if jResp.count == 2 && type == "_owsf" { - type = jResp.allKeys[1] as? String - } - if type == "apiChats" { - if let jApiChats = jResp["apiChats"] as? NSDictionary, - let user: UserRef = try? decodeObject(jApiChats["user"] as Any), - let jChats = jApiChats["chats"] as? NSArray { - let chats = jChats.map { jChat in - if let chatData = try? parseChatData(jChat) { - return chatData - } - return ChatData.invalidJSON(prettyJSON(jChat) ?? "") - } - return .apiChats(user: user, chats: chats) - } - } else if type == "apiChat" { - if let jApiChat = jResp["apiChat"] as? NSDictionary, - let user: UserRef = try? decodeObject(jApiChat["user"] as Any), - let jChat = jApiChat["chat"] as? NSDictionary, - let chat = try? parseChatData(jChat) { - return .apiChat(user: user, chat: chat) - } - } else if type == "chatCmdError" { - if let jError = jResp["chatCmdError"] as? NSDictionary { - let user: UserRef? = try? decodeObject(jError["user_"] as Any) - return .chatCmdError(user_: user, chatError: .invalidJSON(json: prettyJSON(jError) ?? "")) - } - } else if type == "chatError" { - if let jError = jResp["chatError"] as? NSDictionary { - let user: UserRef? = try? decodeObject(jError["user_"] as Any) - return .chatError(user_: user, chatError: .invalidJSON(json: prettyJSON(jError) ?? "")) - } - } - } - json = prettyJSON(j) - } - return ChatResponse.response(type: type ?? "invalid", json: json ?? s) } -func parseChatData(_ jChat: Any) throws -> ChatData { +@inline(__always) +public func dataToString(_ d: Data?) -> String { + if let d { + String(data: d, encoding: .utf8) ?? "invalid string" + } else { + "no data" + } +} + +public func decodeUser_(_ jDict: NSDictionary) -> UserRef? { + if let user_ = jDict["user_"] { + try? decodeObject(user_ as Any) + } else { + nil + } +} + +public func errorJson(_ jDict: NSDictionary) -> Data? { + if let chatError = jDict["chatError"] { + serializeJSON(chatError) + } else { + serializeJSON(jDict) + } +} + +public func parseChatData(_ jChat: Any, _ jNavInfo: Any? = nil) throws -> (ChatData, NavigationInfo) { let jChatDict = jChat as! NSDictionary let chatInfo: ChatInfo = try decodeObject(jChatDict["chatInfo"]!) let chatStats: ChatStats = try decodeObject(jChatDict["chatStats"]!) + let navInfo: NavigationInfo = if let jNavInfo = jNavInfo as? NSDictionary, let jNav = jNavInfo["navInfo"] { + try decodeObject(jNav) + } else { + NavigationInfo() + } let jChatItems = jChatDict["chatItems"] as! NSArray let chatItems = jChatItems.map { jCI in if let ci: ChatItem = try? decodeObject(jCI) { @@ -199,16 +246,18 @@ func parseChatData(_ jChat: Any) throws -> ChatData { return ChatItem.invalidJSON( chatDir: decodeProperty(jCI, "chatDir"), meta: decodeProperty(jCI, "meta"), - json: prettyJSON(jCI) ?? "" + json: serializeJSON(jCI, options: .prettyPrinted) ) } - return ChatData(chatInfo: chatInfo, chatItems: chatItems, chatStats: chatStats) + return (ChatData(chatInfo: chatInfo, chatItems: chatItems, chatStats: chatStats), navInfo) } -func decodeObject<T: Decodable>(_ obj: Any) throws -> T { +@inline(__always) +public func decodeObject<T: Decodable>(_ obj: Any) throws -> T { try jsonDecoder.decode(T.self, from: JSONSerialization.data(withJSONObject: obj)) } +@inline(__always) func decodeProperty<T: Decodable>(_ obj: Any, _ prop: NSString) -> T? { if let jProp = (obj as? NSDictionary)?[prop] { return try? decodeObject(jProp) @@ -216,28 +265,52 @@ func decodeProperty<T: Decodable>(_ obj: Any, _ prop: NSString) -> T? { return nil } -func prettyJSON(_ obj: Any) -> String? { - if let d = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted) { - return String(decoding: d, as: UTF8.self) +@inline(__always) +func getOWSF(_ obj: NSDictionary, _ prop: NSString) -> (type: String, object: NSDictionary)? { + if let j = obj[prop] as? NSDictionary, j.count == 1 || j.count == 2 { + var type = j.allKeys[0] as? String + if j.count == 2 && type == "_owsf" { + type = j.allKeys[1] as? String + } + if let type { + return (type, j) + } } return nil } -public func responseError(_ err: Error) -> String { - if let r = err as? ChatResponse { - switch r { - case let .chatCmdError(_, chatError): return chatErrorString(chatError) - case let .chatError(_, chatError): return chatErrorString(chatError) - default: return "\(String(describing: r.responseType)), details: \(String(describing: r.details))" - } +@inline(__always) +public func serializeJSON(_ obj: Any, options: JSONSerialization.WritingOptions = []) -> Data? { + if let d = try? JSONSerialization.data(withJSONObject: obj, options: options) { + dataPrefix(d) } else { - return String(describing: err) + nil } } -func chatErrorString(_ err: ChatError) -> String { - if case let .invalidJSON(json) = err { return json } - return String(describing: err) +let MAX_JSON_VIEW_LENGTH = 2048 + +@inline(__always) +public func dataPrefix(_ d: Data) -> Data { + d.count > MAX_JSON_VIEW_LENGTH + ? Data(d.prefix(MAX_JSON_VIEW_LENGTH)) + : d +} + +public func responseError(_ err: Error) -> String { + if let e = err as? ChatError { + chatErrorString(e) + } else { + String(describing: err) + } +} + +public func chatErrorString(_ err: ChatError) -> String { + switch err { + case let .invalidJSON(json): dataToString(json) + case let .unexpectedResult(type): "unexpected result: \(type)" + default: String(describing: err) + } } public enum DBMigrationResult: Decodable, Equatable { @@ -276,15 +349,15 @@ public enum MTRError: Decodable, Equatable { case different(appMigration: String, dbMigration: String) } -func dbMigrationResult(_ s: String) -> DBMigrationResult { - let d = s.data(using: .utf8)! -// TODO is there a way to do it without copying the data? e.g: -// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson)) -// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free) - do { - return try jsonDecoder.decode(DBMigrationResult.self, from: d) - } catch let error { - logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)") - return .unknown(json: s) +func dbMigrationResult(_ d: Data?) -> DBMigrationResult { + if let d { + do { + return try jsonDecoder.decode(DBMigrationResult.self, from: d) + } catch let error { + logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)") + return .unknown(json: dataToString(d)) + } + } else { + return .unknown(json: "no data") } } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index a199966bab..b8d2361ac8 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -8,1147 +8,188 @@ import Foundation import SwiftUI +import Network public let jsonDecoder = getJSONDecoder() -let jsonEncoder = getJSONEncoder() +public let jsonEncoder = getJSONEncoder() -public enum ChatCommand { - case showActiveUser - case createActiveUser(profile: Profile?, sameServers: Bool, pastTimestamp: Bool) - case listUsers - case apiSetActiveUser(userId: Int64, viewPwd: String?) - case setAllContactReceipts(enable: Bool) - case apiSetUserContactReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) - case apiSetUserGroupReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) - case apiHideUser(userId: Int64, viewPwd: String) - case apiUnhideUser(userId: Int64, viewPwd: String) - case apiMuteUser(userId: Int64) - case apiUnmuteUser(userId: Int64) - case apiDeleteUser(userId: Int64, delSMPQueues: Bool, viewPwd: String?) - case startChat(subscribe: Bool, expire: Bool, xftp: Bool) - case apiStopChat - case apiActivateChat(restoreChat: Bool) - case apiSuspendChat(timeoutMicroseconds: Int) - case setTempFolder(tempFolder: String) - case setFilesFolder(filesFolder: String) - case apiSetXFTPConfig(config: XFTPFileConfig?) - case apiSetEncryptLocalFiles(enable: Bool) - case apiExportArchive(config: ArchiveConfig) - case apiImportArchive(config: ArchiveConfig) - case apiDeleteStorage - case apiStorageEncryption(config: DBEncryptionConfig) - case apiGetChats(userId: Int64) - case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String) - case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) - case apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool, ttl: Int?) - case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool) - case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) - case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64) - case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction) - case apiGetNtfToken - case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) - case apiVerifyToken(token: DeviceToken, nonce: String, code: String) - case apiDeleteToken(token: DeviceToken) - case apiGetNtfMessage(nonce: String, encNtfInfo: String) - case apiNewGroup(userId: Int64, incognito: Bool, groupProfile: GroupProfile) - case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) - case apiJoinGroup(groupId: Int64) - case apiMemberRole(groupId: Int64, memberId: Int64, memberRole: GroupMemberRole) - case apiRemoveMember(groupId: Int64, memberId: Int64) - case apiLeaveGroup(groupId: Int64) - case apiListMembers(groupId: Int64) - case apiUpdateGroupProfile(groupId: Int64, groupProfile: GroupProfile) - case apiCreateGroupLink(groupId: Int64, memberRole: GroupMemberRole) - case apiGroupLinkMemberRole(groupId: Int64, memberRole: GroupMemberRole) - case apiDeleteGroupLink(groupId: Int64) - case apiGetGroupLink(groupId: Int64) - case apiCreateMemberContact(groupId: Int64, groupMemberId: Int64) - case apiSendMemberContactInvitation(contactId: Int64, msg: MsgContent) - case apiGetUserProtoServers(userId: Int64, serverProtocol: ServerProtocol) - case apiSetUserProtoServers(userId: Int64, serverProtocol: ServerProtocol, servers: [ServerCfg]) - case apiTestProtoServer(userId: Int64, server: String) - case apiSetChatItemTTL(userId: Int64, seconds: Int64?) - case apiGetChatItemTTL(userId: Int64) - case apiSetNetworkConfig(networkConfig: NetCfg) - case apiGetNetworkConfig - case reconnectAllServers - case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) - case apiSetMemberSettings(groupId: Int64, groupMemberId: Int64, memberSettings: GroupMemberSettings) - case apiContactInfo(contactId: Int64) - case apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64) - case apiSwitchContact(contactId: Int64) - case apiSwitchGroupMember(groupId: Int64, groupMemberId: Int64) - case apiAbortSwitchContact(contactId: Int64) - case apiAbortSwitchGroupMember(groupId: Int64, groupMemberId: Int64) - case apiSyncContactRatchet(contactId: Int64, force: Bool) - case apiSyncGroupMemberRatchet(groupId: Int64, groupMemberId: Int64, force: Bool) - case apiGetContactCode(contactId: Int64) - case apiGetGroupMemberCode(groupId: Int64, groupMemberId: Int64) - case apiVerifyContact(contactId: Int64, connectionCode: String?) - case apiVerifyGroupMember(groupId: Int64, groupMemberId: Int64, connectionCode: String?) - case apiAddContact(userId: Int64, incognito: Bool) - case apiSetConnectionIncognito(connId: Int64, incognito: Bool) - case apiConnectPlan(userId: Int64, connReq: String) - case apiConnect(userId: Int64, incognito: Bool, connReq: String) - case apiConnectContactViaAddress(userId: Int64, incognito: Bool, contactId: Int64) - case apiDeleteChat(type: ChatType, id: Int64, notify: Bool?) - case apiClearChat(type: ChatType, id: Int64) - case apiListContacts(userId: Int64) - case apiUpdateProfile(userId: Int64, profile: Profile) - case apiSetContactPrefs(contactId: Int64, preferences: Preferences) - case apiSetContactAlias(contactId: Int64, localAlias: String) - case apiSetConnectionAlias(connId: Int64, localAlias: String) - case apiCreateMyAddress(userId: Int64) - case apiDeleteMyAddress(userId: Int64) - case apiShowMyAddress(userId: Int64) - case apiSetProfileAddress(userId: Int64, on: Bool) - case apiAddressAutoAccept(userId: Int64, autoAccept: AutoAccept?) - case apiAcceptContact(incognito: Bool, contactReqId: Int64) - case apiRejectContact(contactReqId: Int64) - // WebRTC calls - case apiSendCallInvitation(contact: Contact, callType: CallType) - case apiRejectCall(contact: Contact) - case apiSendCallOffer(contact: Contact, callOffer: WebRTCCallOffer) - case apiSendCallAnswer(contact: Contact, answer: WebRTCSession) - case apiSendCallExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo) - case apiEndCall(contact: Contact) - case apiGetCallInvitations - case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus) - case apiGetNetworkStatuses - case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) - case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) - case receiveFile(fileId: Int64, encrypted: Bool?, inline: Bool?) - case setFileToReceive(fileId: Int64, encrypted: Bool?) - case cancelFile(fileId: Int64) - // remote desktop commands - case setLocalDeviceName(displayName: String) - case connectRemoteCtrl(xrcpInvitation: String) - case findKnownRemoteCtrl - case confirmRemoteCtrl(remoteCtrlId: Int64) - case verifyRemoteCtrlSession(sessionCode: String) - case listRemoteCtrls - case stopRemoteCtrl - case deleteRemoteCtrl(remoteCtrlId: Int64) - // misc - case showVersion - case string(String) +public protocol ChatCmdProtocol { + var cmdString: String { get } +} - public var cmdString: String { - get { - switch self { - case .showActiveUser: return "/u" - case let .createActiveUser(profile, sameServers, pastTimestamp): - let user = NewUser(profile: profile, sameServers: sameServers, pastTimestamp: pastTimestamp) - return "/_create user \(encodeJSON(user))" - case .listUsers: return "/users" - case let .apiSetActiveUser(userId, viewPwd): return "/_user \(userId)\(maybePwd(viewPwd))" - case let .setAllContactReceipts(enable): return "/set receipts all \(onOff(enable))" - case let .apiSetUserContactReceipts(userId, userMsgReceiptSettings): - let umrs = userMsgReceiptSettings - return "/_set receipts contacts \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))" - case let .apiSetUserGroupReceipts(userId, userMsgReceiptSettings): - let umrs = userMsgReceiptSettings - return "/_set receipts groups \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))" - case let .apiHideUser(userId, viewPwd): return "/_hide user \(userId) \(encodeJSON(viewPwd))" - case let .apiUnhideUser(userId, viewPwd): return "/_unhide user \(userId) \(encodeJSON(viewPwd))" - case let .apiMuteUser(userId): return "/_mute user \(userId)" - case let .apiUnmuteUser(userId): return "/_unmute user \(userId)" - case let .apiDeleteUser(userId, delSMPQueues, viewPwd): return "/_delete user \(userId) del_smp=\(onOff(delSMPQueues))\(maybePwd(viewPwd))" - case let .startChat(subscribe, expire, xftp): return "/_start subscribe=\(onOff(subscribe)) expire=\(onOff(expire)) xftp=\(onOff(xftp))" - case .apiStopChat: return "/_stop" - case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))" - case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)" - case let .setTempFolder(tempFolder): return "/_temp_folder \(tempFolder)" - case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)" - case let .apiSetXFTPConfig(cfg): if let cfg = cfg { - return "/_xftp on \(encodeJSON(cfg))" - } else { - return "/_xftp off" - } - case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))" - case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))" - case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))" - case .apiDeleteStorage: return "/_db delete" - case let .apiStorageEncryption(cfg): return "/_db encryption \(encodeJSON(cfg))" - case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on" - case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" + - (search == "" ? "" : " search=\(search)") - case let .apiGetChatItemInfo(type, id, itemId): return "/_get item info \(ref(type, id)) \(itemId)" - case let .apiSendMessage(type, id, file, quotedItemId, mc, live, ttl): - let msg = encodeJSON(ComposedMessage(fileSource: file, quotedItemId: quotedItemId, msgContent: mc)) - let ttlStr = ttl != nil ? "\(ttl!)" : "default" - return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msg)" - case let .apiUpdateChatItem(type, id, itemId, mc, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(mc.cmdString)" - case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)" - case let .apiDeleteMemberChatItem(groupId, groupMemberId, itemId): return "/_delete member item #\(groupId) \(groupMemberId) \(itemId)" - case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))" - 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)" - case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)" - case let .apiGetNtfMessage(nonce, encNtfInfo): return "/_ntf message \(nonce) \(encNtfInfo)" - case let .apiNewGroup(userId, incognito, groupProfile): return "/_group \(userId) incognito=\(onOff(incognito)) \(encodeJSON(groupProfile))" - case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)" - case let .apiJoinGroup(groupId): return "/_join #\(groupId)" - case let .apiMemberRole(groupId, memberId, memberRole): return "/_member role #\(groupId) \(memberId) \(memberRole.rawValue)" - case let .apiRemoveMember(groupId, memberId): return "/_remove #\(groupId) \(memberId)" - case let .apiLeaveGroup(groupId): return "/_leave #\(groupId)" - case let .apiListMembers(groupId): return "/_members #\(groupId)" - case let .apiUpdateGroupProfile(groupId, groupProfile): return "/_group_profile #\(groupId) \(encodeJSON(groupProfile))" - case let .apiCreateGroupLink(groupId, memberRole): return "/_create link #\(groupId) \(memberRole)" - case let .apiGroupLinkMemberRole(groupId, memberRole): return "/_set link role #\(groupId) \(memberRole)" - case let .apiDeleteGroupLink(groupId): return "/_delete link #\(groupId)" - case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)" - case let .apiCreateMemberContact(groupId, groupMemberId): return "/_create member contact #\(groupId) \(groupMemberId)" - case let .apiSendMemberContactInvitation(contactId, mc): return "/_invite member contact @\(contactId) \(mc.cmdString)" - case let .apiGetUserProtoServers(userId, serverProtocol): return "/_servers \(userId) \(serverProtocol)" - case let .apiSetUserProtoServers(userId, serverProtocol, servers): return "/_servers \(userId) \(serverProtocol) \(protoServersStr(servers))" - case let .apiTestProtoServer(userId, server): return "/_server test \(userId) \(server)" - case let .apiSetChatItemTTL(userId, seconds): return "/_ttl \(userId) \(chatItemTTLStr(seconds: seconds))" - case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)" - case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))" - case .apiGetNetworkConfig: return "/network" - case .reconnectAllServers: return "/reconnect" - case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))" - case let .apiSetMemberSettings(groupId, groupMemberId, memberSettings): return "/_member settings #\(groupId) \(groupMemberId) \(encodeJSON(memberSettings))" - case let .apiContactInfo(contactId): return "/_info @\(contactId)" - case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)" - case let .apiSwitchContact(contactId): return "/_switch @\(contactId)" - case let .apiSwitchGroupMember(groupId, groupMemberId): return "/_switch #\(groupId) \(groupMemberId)" - case let .apiAbortSwitchContact(contactId): return "/_abort switch @\(contactId)" - case let .apiAbortSwitchGroupMember(groupId, groupMemberId): return "/_abort switch #\(groupId) \(groupMemberId)" - case let .apiSyncContactRatchet(contactId, force): if force { - return "/_sync @\(contactId) force=on" - } else { - return "/_sync @\(contactId)" - } - case let .apiSyncGroupMemberRatchet(groupId, groupMemberId, force): if force { - return "/_sync #\(groupId) \(groupMemberId) force=on" - } else { - return "/_sync #\(groupId) \(groupMemberId)" - } - case let .apiGetContactCode(contactId): return "/_get code @\(contactId)" - case let .apiGetGroupMemberCode(groupId, groupMemberId): return "/_get code #\(groupId) \(groupMemberId)" - case let .apiVerifyContact(contactId, .some(connectionCode)): return "/_verify code @\(contactId) \(connectionCode)" - case let .apiVerifyContact(contactId, .none): return "/_verify code @\(contactId)" - case let .apiVerifyGroupMember(groupId, groupMemberId, .some(connectionCode)): return "/_verify code #\(groupId) \(groupMemberId) \(connectionCode)" - case let .apiVerifyGroupMember(groupId, groupMemberId, .none): return "/_verify code #\(groupId) \(groupMemberId)" - case let .apiAddContact(userId, incognito): return "/_connect \(userId) incognito=\(onOff(incognito))" - case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))" - case let .apiConnectPlan(userId, connReq): return "/_connect plan \(userId) \(connReq)" - case let .apiConnect(userId, incognito, connReq): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connReq)" - case let .apiConnectContactViaAddress(userId, incognito, contactId): return "/_connect contact \(userId) incognito=\(onOff(incognito)) \(contactId)" - case let .apiDeleteChat(type, id, notify): if let notify = notify { - return "/_delete \(ref(type, id)) notify=\(onOff(notify))" - } else { - return "/_delete \(ref(type, id))" - } - case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))" - case let .apiListContacts(userId): return "/_contacts \(userId)" - case let .apiUpdateProfile(userId, profile): return "/_profile \(userId) \(encodeJSON(profile))" - case let .apiSetContactPrefs(contactId, preferences): return "/_set prefs @\(contactId) \(encodeJSON(preferences))" - case let .apiSetContactAlias(contactId, localAlias): return "/_set alias @\(contactId) \(localAlias.trimmingCharacters(in: .whitespaces))" - case let .apiSetConnectionAlias(connId, localAlias): return "/_set alias :\(connId) \(localAlias.trimmingCharacters(in: .whitespaces))" - case let .apiCreateMyAddress(userId): return "/_address \(userId)" - case let .apiDeleteMyAddress(userId): return "/_delete_address \(userId)" - case let .apiShowMyAddress(userId): return "/_show_address \(userId)" - case let .apiSetProfileAddress(userId, on): return "/_profile_address \(userId) \(onOff(on))" - case let .apiAddressAutoAccept(userId, autoAccept): return "/_auto_accept \(userId) \(AutoAccept.cmdString(autoAccept))" - case let .apiAcceptContact(incognito, contactReqId): return "/_accept incognito=\(onOff(incognito)) \(contactReqId)" - case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)" - case let .apiSendCallInvitation(contact, callType): return "/_call invite @\(contact.apiId) \(encodeJSON(callType))" - case let .apiRejectCall(contact): return "/_call reject @\(contact.apiId)" - case let .apiSendCallOffer(contact, callOffer): return "/_call offer @\(contact.apiId) \(encodeJSON(callOffer))" - case let .apiSendCallAnswer(contact, answer): return "/_call answer @\(contact.apiId) \(encodeJSON(answer))" - case let .apiSendCallExtraInfo(contact, extraInfo): return "/_call extra @\(contact.apiId) \(encodeJSON(extraInfo))" - case let .apiEndCall(contact): return "/_call end @\(contact.apiId)" - case .apiGetCallInvitations: return "/_call get" - case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)" - case .apiGetNetworkStatuses: return "/_network_statuses" - case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)" - case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))" - case let .receiveFile(fileId, encrypt, inline): return "/freceive \(fileId)\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))" - case let .setFileToReceive(fileId, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("encrypt", encrypt))" - case let .cancelFile(fileId): return "/fcancel \(fileId)" - case let .setLocalDeviceName(displayName): return "/set device name \(displayName)" - case let .connectRemoteCtrl(xrcpInv): return "/connect remote ctrl \(xrcpInv)" - case .findKnownRemoteCtrl: return "/find remote ctrl" - case let .confirmRemoteCtrl(rcId): return "/confirm remote ctrl \(rcId)" - case let .verifyRemoteCtrlSession(sessCode): return "/verify remote ctrl \(sessCode)" - case .listRemoteCtrls: return "/list remote ctrls" - case .stopRemoteCtrl: return "/stop remote ctrl" - case let .deleteRemoteCtrl(rcId): return "/delete remote ctrl \(rcId)" - case .showVersion: return "/version" - case let .string(str): return str - } - } - } +@inline(__always) +public func onOff(_ b: Bool) -> String { + b ? "on" : "off" +} - public var cmdType: String { - get { - switch self { - case .showActiveUser: return "showActiveUser" - case .createActiveUser: return "createActiveUser" - case .listUsers: return "listUsers" - case .apiSetActiveUser: return "apiSetActiveUser" - case .setAllContactReceipts: return "setAllContactReceipts" - case .apiSetUserContactReceipts: return "apiSetUserContactReceipts" - case .apiSetUserGroupReceipts: return "apiSetUserGroupReceipts" - case .apiHideUser: return "apiHideUser" - case .apiUnhideUser: return "apiUnhideUser" - case .apiMuteUser: return "apiMuteUser" - case .apiUnmuteUser: return "apiUnmuteUser" - case .apiDeleteUser: return "apiDeleteUser" - case .startChat: return "startChat" - case .apiStopChat: return "apiStopChat" - case .apiActivateChat: return "apiActivateChat" - case .apiSuspendChat: return "apiSuspendChat" - case .setTempFolder: return "setTempFolder" - case .setFilesFolder: return "setFilesFolder" - case .apiSetXFTPConfig: return "apiSetXFTPConfig" - case .apiSetEncryptLocalFiles: return "apiSetEncryptLocalFiles" - case .apiExportArchive: return "apiExportArchive" - case .apiImportArchive: return "apiImportArchive" - case .apiDeleteStorage: return "apiDeleteStorage" - case .apiStorageEncryption: return "apiStorageEncryption" - case .apiGetChats: return "apiGetChats" - case .apiGetChat: return "apiGetChat" - case .apiGetChatItemInfo: return "apiGetChatItemInfo" - case .apiSendMessage: return "apiSendMessage" - case .apiUpdateChatItem: return "apiUpdateChatItem" - case .apiDeleteChatItem: return "apiDeleteChatItem" - case .apiConnectContactViaAddress: return "apiConnectContactViaAddress" - case .apiDeleteMemberChatItem: return "apiDeleteMemberChatItem" - case .apiChatItemReaction: return "apiChatItemReaction" - case .apiGetNtfToken: return "apiGetNtfToken" - case .apiRegisterToken: return "apiRegisterToken" - case .apiVerifyToken: return "apiVerifyToken" - case .apiDeleteToken: return "apiDeleteToken" - case .apiGetNtfMessage: return "apiGetNtfMessage" - case .apiNewGroup: return "apiNewGroup" - case .apiAddMember: return "apiAddMember" - case .apiJoinGroup: return "apiJoinGroup" - case .apiMemberRole: return "apiMemberRole" - case .apiRemoveMember: return "apiRemoveMember" - case .apiLeaveGroup: return "apiLeaveGroup" - case .apiListMembers: return "apiListMembers" - case .apiUpdateGroupProfile: return "apiUpdateGroupProfile" - case .apiCreateGroupLink: return "apiCreateGroupLink" - case .apiGroupLinkMemberRole: return "apiGroupLinkMemberRole" - case .apiDeleteGroupLink: return "apiDeleteGroupLink" - case .apiGetGroupLink: return "apiGetGroupLink" - case .apiCreateMemberContact: return "apiCreateMemberContact" - case .apiSendMemberContactInvitation: return "apiSendMemberContactInvitation" - case .apiGetUserProtoServers: return "apiGetUserProtoServers" - case .apiSetUserProtoServers: return "apiSetUserProtoServers" - case .apiTestProtoServer: return "apiTestProtoServer" - case .apiSetChatItemTTL: return "apiSetChatItemTTL" - case .apiGetChatItemTTL: return "apiGetChatItemTTL" - case .apiSetNetworkConfig: return "apiSetNetworkConfig" - case .apiGetNetworkConfig: return "apiGetNetworkConfig" - case .reconnectAllServers: return "reconnectAllServers" - case .apiSetChatSettings: return "apiSetChatSettings" - case .apiSetMemberSettings: return "apiSetMemberSettings" - case .apiContactInfo: return "apiContactInfo" - case .apiGroupMemberInfo: return "apiGroupMemberInfo" - case .apiSwitchContact: return "apiSwitchContact" - case .apiSwitchGroupMember: return "apiSwitchGroupMember" - case .apiAbortSwitchContact: return "apiAbortSwitchContact" - case .apiAbortSwitchGroupMember: return "apiAbortSwitchGroupMember" - case .apiSyncContactRatchet: return "apiSyncContactRatchet" - case .apiSyncGroupMemberRatchet: return "apiSyncGroupMemberRatchet" - case .apiGetContactCode: return "apiGetContactCode" - case .apiGetGroupMemberCode: return "apiGetGroupMemberCode" - case .apiVerifyContact: return "apiVerifyContact" - case .apiVerifyGroupMember: return "apiVerifyGroupMember" - case .apiAddContact: return "apiAddContact" - case .apiSetConnectionIncognito: return "apiSetConnectionIncognito" - case .apiConnectPlan: return "apiConnectPlan" - case .apiConnect: return "apiConnect" - case .apiDeleteChat: return "apiDeleteChat" - case .apiClearChat: return "apiClearChat" - case .apiListContacts: return "apiListContacts" - case .apiUpdateProfile: return "apiUpdateProfile" - case .apiSetContactPrefs: return "apiSetContactPrefs" - case .apiSetContactAlias: return "apiSetContactAlias" - case .apiSetConnectionAlias: return "apiSetConnectionAlias" - case .apiCreateMyAddress: return "apiCreateMyAddress" - case .apiDeleteMyAddress: return "apiDeleteMyAddress" - case .apiShowMyAddress: return "apiShowMyAddress" - case .apiSetProfileAddress: return "apiSetProfileAddress" - case .apiAddressAutoAccept: return "apiAddressAutoAccept" - case .apiAcceptContact: return "apiAcceptContact" - case .apiRejectContact: return "apiRejectContact" - case .apiSendCallInvitation: return "apiSendCallInvitation" - case .apiRejectCall: return "apiRejectCall" - case .apiSendCallOffer: return "apiSendCallOffer" - case .apiSendCallAnswer: return "apiSendCallAnswer" - case .apiSendCallExtraInfo: return "apiSendCallExtraInfo" - case .apiEndCall: return "apiEndCall" - case .apiGetCallInvitations: return "apiGetCallInvitations" - case .apiCallStatus: return "apiCallStatus" - case .apiGetNetworkStatuses: return "apiGetNetworkStatuses" - case .apiChatRead: return "apiChatRead" - case .apiChatUnread: return "apiChatUnread" - case .receiveFile: return "receiveFile" - case .setFileToReceive: return "setFileToReceive" - case .cancelFile: return "cancelFile" - case .setLocalDeviceName: return "setLocalDeviceName" - case .connectRemoteCtrl: return "connectRemoteCtrl" - case .findKnownRemoteCtrl: return "findKnownRemoteCtrl" - case .confirmRemoteCtrl: return "confirmRemoteCtrl" - case .verifyRemoteCtrlSession: return "verifyRemoteCtrlSession" - case .listRemoteCtrls: return "listRemoteCtrls" - case .stopRemoteCtrl: return "stopRemoteCtrl" - case .deleteRemoteCtrl: return "deleteRemoteCtrl" - case .showVersion: return "showVersion" - case .string: return "console command" - } - } - } - - func ref(_ type: ChatType, _ id: Int64) -> String { - "\(type.rawValue)\(id)" - } - - func protoServersStr(_ servers: [ServerCfg]) -> String { - encodeJSON(ProtoServersConfig(servers: servers)) - } - - func chatItemTTLStr(seconds: Int64?) -> String { - if let seconds = seconds { - return String(seconds) - } else { - return "none" - } - } - - public var obfuscated: ChatCommand { +public enum APIResult<R>: Decodable where R: Decodable, R: ChatAPIResult { + case result(R) + case error(ChatError) + case invalid(type: String, json: Data) + + public var responseType: String { switch self { - case let .apiStorageEncryption(cfg): - return .apiStorageEncryption(config: DBEncryptionConfig(currentKey: obfuscate(cfg.currentKey), newKey: obfuscate(cfg.newKey))) - case let .apiSetActiveUser(userId, viewPwd): - return .apiSetActiveUser(userId: userId, viewPwd: obfuscate(viewPwd)) - case let .apiHideUser(userId, viewPwd): - return .apiHideUser(userId: userId, viewPwd: obfuscate(viewPwd)) - case let .apiUnhideUser(userId, viewPwd): - return .apiUnhideUser(userId: userId, viewPwd: obfuscate(viewPwd)) - case let .apiDeleteUser(userId, delSMPQueues, viewPwd): - return .apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues, viewPwd: obfuscate(viewPwd)) - default: return self + case let .result(r): r.responseType + case let .error(e): "error \(e.errorType)" + case let .invalid(type, _): "* \(type)" + } + } + + public var unexpected: ChatError { + switch self { + case let .result(r): .unexpectedResult(type: r.responseType) + case let .error(e): e + case let .invalid(type, _): .unexpectedResult(type: "* \(type)") } } - private func obfuscate(_ s: String) -> String { - s == "" ? "" : "***" - } - - private func obfuscate(_ s: String?) -> String? { - if let s = s { - return obfuscate(s) + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if container.contains(.result) { + let result = try container.decode(R.self, forKey: .result) + self = .result(result) + } else { + let error = try container.decode(ChatError.self, forKey: .error) + self = .error(error) } + } + + private enum CodingKeys: String, CodingKey { + case result, error + } +} + +public protocol ChatAPIResult: Decodable { + var responseType: String { get } + var details: String { get } + static func fallbackResult(_ type: String, _ json: NSDictionary) -> Self? +} + +extension ChatAPIResult { + public var noDetails: String { "\(self.responseType): no details" } + + @inline(__always) + public static func fallbackResult(_ type: String, _ json: NSDictionary) -> Self? { + nil + } + + @inline(__always) + public var unexpected: ChatError { + .unexpectedResult(type: self.responseType) + } +} + +public func decodeAPIResult<R: ChatAPIResult>(_ d: Data) -> APIResult<R> { +// print("decodeAPIResult \(String(describing: R.self))") + do { +// return try withStackSizeLimit { try jsonDecoder.decode(APIResult<R>.self, from: d) } + return try jsonDecoder.decode(APIResult<R>.self, from: d) + } catch {} + if let j = try? JSONSerialization.jsonObject(with: d) as? NSDictionary { + if let (_, jErr) = getOWSF(j, "error") { + return APIResult.error(.invalidJSON(json: errorJson(jErr))) as APIResult<R> + } else if let (type, jRes) = getOWSF(j, "result") { + return if let r = R.fallbackResult(type, jRes) { + APIResult.result(r) + } else { + APIResult.invalid(type: type, json: dataPrefix(d)) + } + } + } + return APIResult.invalid(type: "invalid", json: dataPrefix(d)) +} + +// Default stack size for the main thread is 1mb, for secondary threads - 512 kb. +// This function can be used to test what size is used (or to increase available stack size). +// Stack size must be a multiple of system page size (16kb). +//private let stackSizeLimit: Int = 256 * 1024 +// +//private func withStackSizeLimit<T>(_ f: @escaping () throws -> T) throws -> T { +// let semaphore = DispatchSemaphore(value: 0) +// var result: Result<T, Error>? +// let thread = Thread { +// do { +// result = .success(try f()) +// } catch { +// result = .failure(error) +// } +// semaphore.signal() +// } +// +// thread.stackSize = stackSizeLimit +// thread.qualityOfService = Thread.current.qualityOfService +// thread.start() +// +// semaphore.wait() +// +// switch result! { +// case let .success(r): return r +// case let .failure(e): throw e +// } +//} + +public func parseApiChats(_ jResp: NSDictionary) -> (user: UserRef, chats: [ChatData])? { + if let jApiChats = jResp["apiChats"] as? NSDictionary, + let user: UserRef = try? decodeObject(jApiChats["user"] as Any), + let jChats = jApiChats["chats"] as? NSArray { + let chats = jChats.map { jChat in + if let chatData = try? parseChatData(jChat) { + return chatData.0 + } + return ChatData.invalidJSON(serializeJSON(jChat, options: .prettyPrinted)) + } + return (user, chats) + } else { return nil } +} - private func onOff(_ b: Bool) -> String { - b ? "on" : "off" +public func withUser(_ u: (any UserLike)?, _ s: String) -> String { + if let id = u?.userId { + return "userId: \(id)\n\(s)" + } + return s +} + +public struct CreatedConnLink: Decodable, Hashable { + public var connFullLink: String + public var connShortLink: String? + + public init(connFullLink: String, connShortLink: String?) { + self.connFullLink = connFullLink + self.connShortLink = connShortLink } - private func onOffParam(_ param: String, _ b: Bool?) -> String { - if let b = b { - return " \(param)=\(onOff(b))" - } - return "" - } - - private func maybePwd(_ pwd: String?) -> String { - pwd == "" || pwd == nil ? "" : " " + encodeJSON(pwd) + public func simplexChatUri(short: Bool = true) -> String { + short ? (connShortLink ?? simplexChatLink(connFullLink)) : simplexChatLink(connFullLink) } } -public struct APIResponse: Decodable { - var resp: ChatResponse +public func simplexChatLink(_ uri: String) -> String { + uri.starts(with: "simplex:/") + ? uri.replacingOccurrences(of: "simplex:/", with: "https://simplex.chat/") + : uri } -public enum ChatResponse: Decodable, Error { - case response(type: String, json: String) - case activeUser(user: User) - case usersList(users: [UserInfo]) - case chatStarted - case chatRunning - case chatStopped - case chatSuspended - case apiChats(user: UserRef, chats: [ChatData]) - case apiChat(user: UserRef, chat: ChatData) - case chatItemInfo(user: UserRef, chatItem: AChatItem, chatItemInfo: ChatItemInfo) - case userProtoServers(user: UserRef, servers: UserProtoServers) - case serverTestResult(user: UserRef, testServer: String, testFailure: ProtocolTestFailure?) - case chatItemTTL(user: UserRef, chatItemTTL: Int64?) - case networkConfig(networkConfig: NetCfg) - case contactInfo(user: UserRef, contact: Contact, connectionStats_: ConnectionStats?, customUserProfile: Profile?) - case groupMemberInfo(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?) - case contactSwitchStarted(user: UserRef, contact: Contact, connectionStats: ConnectionStats) - case groupMemberSwitchStarted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) - case contactSwitchAborted(user: UserRef, contact: Contact, connectionStats: ConnectionStats) - case groupMemberSwitchAborted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) - case contactSwitch(user: UserRef, contact: Contact, switchProgress: SwitchProgress) - case groupMemberSwitch(user: UserRef, groupInfo: GroupInfo, member: GroupMember, switchProgress: SwitchProgress) - case contactRatchetSyncStarted(user: UserRef, contact: Contact, connectionStats: ConnectionStats) - case groupMemberRatchetSyncStarted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) - case contactRatchetSync(user: UserRef, contact: Contact, ratchetSyncProgress: RatchetSyncProgress) - case groupMemberRatchetSync(user: UserRef, groupInfo: GroupInfo, member: GroupMember, ratchetSyncProgress: RatchetSyncProgress) - case contactVerificationReset(user: UserRef, contact: Contact) - case groupMemberVerificationReset(user: UserRef, groupInfo: GroupInfo, member: GroupMember) - case contactCode(user: UserRef, contact: Contact, connectionCode: String) - case groupMemberCode(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionCode: String) - case connectionVerified(user: UserRef, verified: Bool, expectedCode: String) - case invitation(user: UserRef, connReqInvitation: String, connection: PendingContactConnection) - case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection) - case connectionPlan(user: UserRef, connectionPlan: ConnectionPlan) - case sentConfirmation(user: UserRef, connection: PendingContactConnection) - case sentInvitation(user: UserRef, connection: PendingContactConnection) - case sentInvitationToContact(user: UserRef, contact: Contact, customUserProfile: Profile?) - case contactAlreadyExists(user: UserRef, contact: Contact) - case contactRequestAlreadyAccepted(user: UserRef, contact: Contact) - case contactDeleted(user: UserRef, contact: Contact) - case contactDeletedByContact(user: UserRef, contact: Contact) - case chatCleared(user: UserRef, chatInfo: ChatInfo) - case userProfileNoChange(user: User) - case userProfileUpdated(user: User, fromProfile: Profile, toProfile: Profile, updateSummary: UserProfileUpdateSummary) - case userPrivacy(user: User, updatedUser: User) - case contactAliasUpdated(user: UserRef, toContact: Contact) - case connectionAliasUpdated(user: UserRef, toConnection: PendingContactConnection) - case contactPrefsUpdated(user: User, fromContact: Contact, toContact: Contact) - case userContactLink(user: User, contactLink: UserContactLink) - case userContactLinkUpdated(user: User, contactLink: UserContactLink) - case userContactLinkCreated(user: User, connReqContact: String) - case userContactLinkDeleted(user: User) - case contactConnected(user: UserRef, contact: Contact, userCustomProfile: Profile?) - case contactConnecting(user: UserRef, contact: Contact) - case receivedContactRequest(user: UserRef, contactRequest: UserContactRequest) - case acceptingContactRequest(user: UserRef, contact: Contact) - case contactRequestRejected(user: UserRef) - case contactUpdated(user: UserRef, toContact: Contact) - case groupMemberUpdated(user: UserRef, groupInfo: GroupInfo, fromMember: GroupMember, toMember: GroupMember) - // TODO remove events below - case contactsSubscribed(server: String, contactRefs: [ContactRef]) - case contactsDisconnected(server: String, contactRefs: [ContactRef]) - case contactSubSummary(user: UserRef, contactSubscriptions: [ContactSubStatus]) - // TODO remove events above - case networkStatus(networkStatus: NetworkStatus, connections: [String]) - case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus]) - case groupSubscribed(user: UserRef, groupInfo: GroupRef) - case memberSubErrors(user: UserRef, memberSubErrors: [MemberSubError]) - case groupEmpty(user: UserRef, groupInfo: GroupInfo) - case userContactLinkSubscribed - case newChatItem(user: UserRef, chatItem: AChatItem) - case chatItemStatusUpdated(user: UserRef, chatItem: AChatItem) - case chatItemUpdated(user: UserRef, chatItem: AChatItem) - case chatItemNotChanged(user: UserRef, chatItem: AChatItem) - case chatItemReaction(user: UserRef, added: Bool, reaction: ACIReaction) - case chatItemDeleted(user: UserRef, deletedChatItem: AChatItem, toChatItem: AChatItem?, byUser: Bool) - case contactsList(user: UserRef, contacts: [Contact]) - // group events - case groupCreated(user: UserRef, groupInfo: GroupInfo) - case sentGroupInvitation(user: UserRef, groupInfo: GroupInfo, contact: Contact, member: GroupMember) - case userAcceptedGroupSent(user: UserRef, groupInfo: GroupInfo, hostContact: Contact?) - case groupLinkConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember) - case userDeletedMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember) - case leftMemberUser(user: UserRef, groupInfo: GroupInfo) - case groupMembers(user: UserRef, group: Group) - case receivedGroupInvitation(user: UserRef, groupInfo: GroupInfo, contact: Contact, memberRole: GroupMemberRole) - case groupDeletedUser(user: UserRef, groupInfo: GroupInfo) - case joinedGroupMemberConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember, member: GroupMember) - case memberRole(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole) - case memberRoleUser(user: UserRef, groupInfo: GroupInfo, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole) - case deletedMemberUser(user: UserRef, groupInfo: GroupInfo, member: GroupMember) - case deletedMember(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, deletedMember: GroupMember) - case leftMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember) - case groupDeleted(user: UserRef, groupInfo: GroupInfo, member: GroupMember) - case contactsMerged(user: UserRef, intoContact: Contact, mergedContact: Contact) - case groupInvitation(user: UserRef, groupInfo: GroupInfo) // unused - case userJoinedGroup(user: UserRef, groupInfo: GroupInfo) - case joinedGroupMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember) - case connectedToGroupMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember, memberContact: Contact?) - case groupRemoved(user: UserRef, groupInfo: GroupInfo) // unused - case groupUpdated(user: UserRef, toGroup: GroupInfo) - case groupLinkCreated(user: UserRef, groupInfo: GroupInfo, connReqContact: String, memberRole: GroupMemberRole) - case groupLink(user: UserRef, groupInfo: GroupInfo, connReqContact: String, memberRole: GroupMemberRole) - case groupLinkDeleted(user: UserRef, groupInfo: GroupInfo) - case newMemberContact(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember) - case newMemberContactSentInv(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember) - case newMemberContactReceivedInv(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember) - // receiving file events - case rcvFileAccepted(user: UserRef, chatItem: AChatItem) - case rcvFileAcceptedSndCancelled(user: UserRef, rcvFileTransfer: RcvFileTransfer) - case rcvFileStart(user: UserRef, chatItem: AChatItem) - case rcvFileProgressXFTP(user: UserRef, chatItem: AChatItem, receivedSize: Int64, totalSize: Int64) - case rcvFileComplete(user: UserRef, chatItem: AChatItem) - case rcvFileCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer) - case rcvFileSndCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer) - case rcvFileError(user: UserRef, chatItem: AChatItem) - // sending file events - case sndFileStart(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) - case sndFileComplete(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) - case sndFileCancelled(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer]) - case sndFileRcvCancelled(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) - case sndFileProgressXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64) - case sndFileCompleteXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta) - case sndFileError(user: UserRef, chatItem: AChatItem) - // call events - case callInvitation(callInvitation: RcvCallInvitation) - case callOffer(user: UserRef, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool) - case callAnswer(user: UserRef, contact: Contact, answer: WebRTCSession) - case callExtraInfo(user: UserRef, contact: Contact, extraInfo: WebRTCExtraInfo) - case callEnded(user: UserRef, contact: Contact) - case callInvitations(callInvitations: [RcvCallInvitation]) - case ntfTokenStatus(status: NtfTknStatus) - case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode) - case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo]) - case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgInfo) - case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection) - // remote desktop responses/events - case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo]) - case remoteCtrlFound(remoteCtrl: RemoteCtrlInfo, ctrlAppInfo_: CtrlAppInfo?, appVersion: String, compatible: Bool) - case remoteCtrlConnecting(remoteCtrl_: RemoteCtrlInfo?, ctrlAppInfo: CtrlAppInfo, appVersion: String) - case remoteCtrlSessionCode(remoteCtrl_: RemoteCtrlInfo?, sessionCode: String) - case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo) - case remoteCtrlStopped(rcsState: RemoteCtrlSessionState, rcStopReason: RemoteCtrlStopReason) - // misc - case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration]) - case cmdOk(user: UserRef?) - case chatCmdError(user_: UserRef?, chatError: ChatError) - case chatError(user_: UserRef?, chatError: ChatError) - case archiveImported(archiveErrors: [ArchiveError]) - - public var responseType: String { - get { - switch self { - case let .response(type, _): return "* \(type)" - case .activeUser: return "activeUser" - case .usersList: return "usersList" - case .chatStarted: return "chatStarted" - case .chatRunning: return "chatRunning" - case .chatStopped: return "chatStopped" - case .chatSuspended: return "chatSuspended" - case .apiChats: return "apiChats" - case .apiChat: return "apiChat" - case .chatItemInfo: return "chatItemInfo" - case .userProtoServers: return "userProtoServers" - case .serverTestResult: return "serverTestResult" - case .chatItemTTL: return "chatItemTTL" - case .networkConfig: return "networkConfig" - case .contactInfo: return "contactInfo" - case .groupMemberInfo: return "groupMemberInfo" - case .contactSwitchStarted: return "contactSwitchStarted" - case .groupMemberSwitchStarted: return "groupMemberSwitchStarted" - case .contactSwitchAborted: return "contactSwitchAborted" - case .groupMemberSwitchAborted: return "groupMemberSwitchAborted" - case .contactSwitch: return "contactSwitch" - case .groupMemberSwitch: return "groupMemberSwitch" - case .contactRatchetSyncStarted: return "contactRatchetSyncStarted" - case .groupMemberRatchetSyncStarted: return "groupMemberRatchetSyncStarted" - case .contactRatchetSync: return "contactRatchetSync" - case .groupMemberRatchetSync: return "groupMemberRatchetSync" - case .contactVerificationReset: return "contactVerificationReset" - case .groupMemberVerificationReset: return "groupMemberVerificationReset" - case .contactCode: return "contactCode" - case .groupMemberCode: return "groupMemberCode" - case .connectionVerified: return "connectionVerified" - case .invitation: return "invitation" - case .connectionIncognitoUpdated: return "connectionIncognitoUpdated" - case .connectionPlan: return "connectionPlan" - case .sentConfirmation: return "sentConfirmation" - case .sentInvitation: return "sentInvitation" - case .sentInvitationToContact: return "sentInvitationToContact" - case .contactAlreadyExists: return "contactAlreadyExists" - case .contactRequestAlreadyAccepted: return "contactRequestAlreadyAccepted" - case .contactDeleted: return "contactDeleted" - case .contactDeletedByContact: return "contactDeletedByContact" - case .chatCleared: return "chatCleared" - case .userProfileNoChange: return "userProfileNoChange" - case .userProfileUpdated: return "userProfileUpdated" - case .userPrivacy: return "userPrivacy" - case .contactAliasUpdated: return "contactAliasUpdated" - case .connectionAliasUpdated: return "connectionAliasUpdated" - case .contactPrefsUpdated: return "contactPrefsUpdated" - case .userContactLink: return "userContactLink" - case .userContactLinkUpdated: return "userContactLinkUpdated" - case .userContactLinkCreated: return "userContactLinkCreated" - case .userContactLinkDeleted: return "userContactLinkDeleted" - case .contactConnected: return "contactConnected" - case .contactConnecting: return "contactConnecting" - case .receivedContactRequest: return "receivedContactRequest" - case .acceptingContactRequest: return "acceptingContactRequest" - case .contactRequestRejected: return "contactRequestRejected" - case .contactUpdated: return "contactUpdated" - case .groupMemberUpdated: return "groupMemberUpdated" - case .contactsSubscribed: return "contactsSubscribed" - case .contactsDisconnected: return "contactsDisconnected" - case .contactSubSummary: return "contactSubSummary" - case .networkStatus: return "networkStatus" - case .networkStatuses: return "networkStatuses" - case .groupSubscribed: return "groupSubscribed" - case .memberSubErrors: return "memberSubErrors" - case .groupEmpty: return "groupEmpty" - case .userContactLinkSubscribed: return "userContactLinkSubscribed" - case .newChatItem: return "newChatItem" - case .chatItemStatusUpdated: return "chatItemStatusUpdated" - case .chatItemUpdated: return "chatItemUpdated" - case .chatItemNotChanged: return "chatItemNotChanged" - case .chatItemReaction: return "chatItemReaction" - case .chatItemDeleted: return "chatItemDeleted" - case .contactsList: return "contactsList" - case .groupCreated: return "groupCreated" - case .sentGroupInvitation: return "sentGroupInvitation" - case .userAcceptedGroupSent: return "userAcceptedGroupSent" - case .groupLinkConnecting: return "groupLinkConnecting" - case .userDeletedMember: return "userDeletedMember" - case .leftMemberUser: return "leftMemberUser" - case .groupMembers: return "groupMembers" - case .receivedGroupInvitation: return "receivedGroupInvitation" - case .groupDeletedUser: return "groupDeletedUser" - case .joinedGroupMemberConnecting: return "joinedGroupMemberConnecting" - case .memberRole: return "memberRole" - case .memberRoleUser: return "memberRoleUser" - case .deletedMemberUser: return "deletedMemberUser" - case .deletedMember: return "deletedMember" - case .leftMember: return "leftMember" - case .groupDeleted: return "groupDeleted" - case .contactsMerged: return "contactsMerged" - case .groupInvitation: return "groupInvitation" - case .userJoinedGroup: return "userJoinedGroup" - case .joinedGroupMember: return "joinedGroupMember" - case .connectedToGroupMember: return "connectedToGroupMember" - case .groupRemoved: return "groupRemoved" - case .groupUpdated: return "groupUpdated" - case .groupLinkCreated: return "groupLinkCreated" - case .groupLink: return "groupLink" - case .groupLinkDeleted: return "groupLinkDeleted" - case .newMemberContact: return "newMemberContact" - case .newMemberContactSentInv: return "newMemberContactSentInv" - case .newMemberContactReceivedInv: return "newMemberContactReceivedInv" - case .rcvFileAccepted: return "rcvFileAccepted" - case .rcvFileAcceptedSndCancelled: return "rcvFileAcceptedSndCancelled" - case .rcvFileStart: return "rcvFileStart" - case .rcvFileProgressXFTP: return "rcvFileProgressXFTP" - case .rcvFileComplete: return "rcvFileComplete" - case .rcvFileCancelled: return "rcvFileCancelled" - case .rcvFileSndCancelled: return "rcvFileSndCancelled" - case .rcvFileError: return "rcvFileError" - case .sndFileStart: return "sndFileStart" - case .sndFileComplete: return "sndFileComplete" - case .sndFileCancelled: return "sndFileCancelled" - case .sndFileRcvCancelled: return "sndFileRcvCancelled" - case .sndFileProgressXFTP: return "sndFileProgressXFTP" - case .sndFileCompleteXFTP: return "sndFileCompleteXFTP" - case .sndFileError: return "sndFileError" - case .callInvitation: return "callInvitation" - case .callOffer: return "callOffer" - case .callAnswer: return "callAnswer" - case .callExtraInfo: return "callExtraInfo" - case .callEnded: return "callEnded" - case .callInvitations: return "callInvitations" - case .ntfTokenStatus: return "ntfTokenStatus" - case .ntfToken: return "ntfToken" - case .ntfMessages: return "ntfMessages" - case .ntfMessage: return "ntfMessage" - case .contactConnectionDeleted: return "contactConnectionDeleted" - case .remoteCtrlList: return "remoteCtrlList" - case .remoteCtrlFound: return "remoteCtrlFound" - case .remoteCtrlConnecting: return "remoteCtrlConnecting" - case .remoteCtrlSessionCode: return "remoteCtrlSessionCode" - case .remoteCtrlConnected: return "remoteCtrlConnected" - case .remoteCtrlStopped: return "remoteCtrlStopped" - case .versionInfo: return "versionInfo" - case .cmdOk: return "cmdOk" - case .chatCmdError: return "chatCmdError" - case .chatError: return "chatError" - case .archiveImported: return "archiveImported" - } - } - } - - public var details: String { - get { - switch self { - case let .response(_, json): return json - case let .activeUser(user): return String(describing: user) - case let .usersList(users): return String(describing: users) - case .chatStarted: return noDetails - case .chatRunning: return noDetails - case .chatStopped: return noDetails - case .chatSuspended: return noDetails - case let .apiChats(u, chats): return withUser(u, String(describing: chats)) - case let .apiChat(u, chat): return withUser(u, String(describing: chat)) - case let .chatItemInfo(u, chatItem, chatItemInfo): return withUser(u, "chatItem: \(String(describing: chatItem))\nchatItemInfo: \(String(describing: chatItemInfo))") - case let .userProtoServers(u, servers): return withUser(u, "servers: \(String(describing: servers))") - case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\nresult: \(String(describing: testFailure))") - case let .chatItemTTL(u, chatItemTTL): return withUser(u, String(describing: chatItemTTL)) - case let .networkConfig(networkConfig): return String(describing: networkConfig) - case let .contactInfo(u, contact, connectionStats_, customUserProfile): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats_: \(String(describing: connectionStats_))\ncustomUserProfile: \(String(describing: customUserProfile))") - case let .groupMemberInfo(u, groupInfo, member, connectionStats_): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_))") - case let .contactSwitchStarted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))") - case let .groupMemberSwitchStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))") - case let .contactSwitchAborted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))") - case let .groupMemberSwitchAborted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))") - case let .contactSwitch(u, contact, switchProgress): return withUser(u, "contact: \(String(describing: contact))\nswitchProgress: \(String(describing: switchProgress))") - case let .groupMemberSwitch(u, groupInfo, member, switchProgress): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nswitchProgress: \(String(describing: switchProgress))") - case let .contactRatchetSyncStarted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))") - case let .groupMemberRatchetSyncStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))") - case let .contactRatchetSync(u, contact, ratchetSyncProgress): return withUser(u, "contact: \(String(describing: contact))\nratchetSyncProgress: \(String(describing: ratchetSyncProgress))") - case let .groupMemberRatchetSync(u, groupInfo, member, ratchetSyncProgress): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nratchetSyncProgress: \(String(describing: ratchetSyncProgress))") - case let .contactVerificationReset(u, contact): return withUser(u, "contact: \(String(describing: contact))") - case let .groupMemberVerificationReset(u, groupInfo, member): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))") - case let .contactCode(u, contact, connectionCode): return withUser(u, "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)") - case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)") - case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)") - case let .invitation(u, connReqInvitation, connection): return withUser(u, "connReqInvitation: \(connReqInvitation)\nconnection: \(connection)") - case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection)) - case let .connectionPlan(u, connectionPlan): return withUser(u, String(describing: connectionPlan)) - case let .sentConfirmation(u, connection): return withUser(u, String(describing: connection)) - case let .sentInvitation(u, connection): return withUser(u, String(describing: connection)) - case let .sentInvitationToContact(u, contact, _): return withUser(u, String(describing: contact)) - case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact)) - case let .contactRequestAlreadyAccepted(u, contact): return withUser(u, String(describing: contact)) - case let .contactDeleted(u, contact): return withUser(u, String(describing: contact)) - case let .contactDeletedByContact(u, contact): return withUser(u, String(describing: contact)) - case let .chatCleared(u, chatInfo): return withUser(u, String(describing: chatInfo)) - case .userProfileNoChange: return noDetails - case let .userProfileUpdated(u, _, toProfile, _): return withUser(u, String(describing: toProfile)) - case let .userPrivacy(u, updatedUser): return withUser(u, String(describing: updatedUser)) - case let .contactAliasUpdated(u, toContact): return withUser(u, String(describing: toContact)) - case let .connectionAliasUpdated(u, toConnection): return withUser(u, String(describing: toConnection)) - case let .contactPrefsUpdated(u, fromContact, toContact): return withUser(u, "fromContact: \(String(describing: fromContact))\ntoContact: \(String(describing: toContact))") - case let .userContactLink(u, contactLink): return withUser(u, contactLink.responseDetails) - case let .userContactLinkUpdated(u, contactLink): return withUser(u, contactLink.responseDetails) - case let .userContactLinkCreated(u, connReq): return withUser(u, connReq) - case .userContactLinkDeleted: return noDetails - case let .contactConnected(u, contact, _): return withUser(u, String(describing: contact)) - case let .contactConnecting(u, contact): return withUser(u, String(describing: contact)) - case let .receivedContactRequest(u, contactRequest): return withUser(u, String(describing: contactRequest)) - case let .acceptingContactRequest(u, contact): return withUser(u, String(describing: contact)) - case .contactRequestRejected: return noDetails - case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact)) - case let .groupMemberUpdated(u, groupInfo, fromMember, toMember): return withUser(u, "groupInfo: \(groupInfo)\nfromMember: \(fromMember)\ntoMember: \(toMember)") - case let .contactsSubscribed(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))" - case let .contactsDisconnected(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))" - case let .contactSubSummary(u, contactSubscriptions): return withUser(u, String(describing: contactSubscriptions)) - case let .networkStatus(status, conns): return "networkStatus: \(String(describing: status))\nconnections: \(String(describing: conns))" - case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses)) - case let .groupSubscribed(u, groupInfo): return withUser(u, String(describing: groupInfo)) - case let .memberSubErrors(u, memberSubErrors): return withUser(u, String(describing: memberSubErrors)) - case let .groupEmpty(u, groupInfo): return withUser(u, String(describing: groupInfo)) - case .userContactLinkSubscribed: return noDetails - case let .newChatItem(u, chatItem): return withUser(u, String(describing: chatItem)) - case let .chatItemStatusUpdated(u, chatItem): return withUser(u, String(describing: chatItem)) - case let .chatItemUpdated(u, chatItem): return withUser(u, String(describing: chatItem)) - case let .chatItemNotChanged(u, chatItem): return withUser(u, String(describing: chatItem)) - case let .chatItemReaction(u, added, reaction): return withUser(u, "added: \(added)\n\(String(describing: reaction))") - case let .chatItemDeleted(u, deletedChatItem, toChatItem, byUser): return withUser(u, "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))\nbyUser: \(byUser)") - case let .contactsList(u, contacts): return withUser(u, String(describing: contacts)) - case let .groupCreated(u, groupInfo): return withUser(u, String(describing: groupInfo)) - 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 .groupLinkConnecting(u, groupInfo, hostMember): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(String(describing: hostMember))") - case let .userDeletedMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") - case let .leftMemberUser(u, groupInfo): return withUser(u, String(describing: groupInfo)) - case let .groupMembers(u, group): return withUser(u, String(describing: group)) - case let .receivedGroupInvitation(u, groupInfo, contact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\ncontact: \(contact)\nmemberRole: \(memberRole)") - case let .groupDeletedUser(u, groupInfo): return withUser(u, String(describing: groupInfo)) - case let .joinedGroupMemberConnecting(u, groupInfo, hostMember, member): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(hostMember)\nmember: \(member)") - case let .memberRole(u, groupInfo, byMember, member, fromRole, toRole): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\nmember: \(member)\nfromRole: \(fromRole)\ntoRole: \(toRole)") - case let .memberRoleUser(u, groupInfo, member, fromRole, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)\nfromRole: \(fromRole)\ntoRole: \(toRole)") - case let .deletedMemberUser(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") - case let .deletedMember(u, groupInfo, byMember, deletedMember): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\ndeletedMember: \(deletedMember)") - case let .leftMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") - case let .groupDeleted(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") - case let .contactsMerged(u, intoContact, mergedContact): return withUser(u, "intoContact: \(intoContact)\nmergedContact: \(mergedContact)") - case let .groupInvitation(u, groupInfo): return withUser(u, String(describing: groupInfo)) - case let .userJoinedGroup(u, groupInfo): return withUser(u, String(describing: groupInfo)) - case let .joinedGroupMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") - case let .connectedToGroupMember(u, groupInfo, member, memberContact): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)\nmemberContact: \(String(describing: memberContact))") - case let .groupRemoved(u, groupInfo): return withUser(u, String(describing: groupInfo)) - case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup)) - case let .groupLinkCreated(u, groupInfo, connReqContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)\nmemberRole: \(memberRole)") - case let .groupLink(u, groupInfo, connReqContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)\nmemberRole: \(memberRole)") - case let .groupLinkDeleted(u, groupInfo): return withUser(u, String(describing: groupInfo)) - case let .newMemberContact(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)") - case let .newMemberContactSentInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)") - case let .newMemberContactReceivedInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)") - case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem)) - case .rcvFileAcceptedSndCancelled: return noDetails - case let .rcvFileStart(u, chatItem): return withUser(u, String(describing: chatItem)) - case let .rcvFileProgressXFTP(u, chatItem, receivedSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nreceivedSize: \(receivedSize)\ntotalSize: \(totalSize)") - case let .rcvFileComplete(u, chatItem): return withUser(u, String(describing: chatItem)) - case let .rcvFileCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem)) - case let .rcvFileSndCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem)) - case let .rcvFileError(u, chatItem): return withUser(u, String(describing: chatItem)) - case let .sndFileStart(u, chatItem, _): return withUser(u, String(describing: chatItem)) - case let .sndFileComplete(u, chatItem, _): return withUser(u, String(describing: chatItem)) - case let .sndFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem)) - case let .sndFileRcvCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem)) - case let .sndFileProgressXFTP(u, chatItem, _, sentSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nsentSize: \(sentSize)\ntotalSize: \(totalSize)") - case let .sndFileCompleteXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem)) - case let .sndFileError(u, chatItem): return withUser(u, String(describing: chatItem)) - case let .callInvitation(inv): return String(describing: inv) - case let .callOffer(u, contact, callType, offer, sharedKey, askConfirmation): return withUser(u, "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))") - case let .callAnswer(u, contact, answer): return withUser(u, "contact: \(contact.id)\nanswer: \(String(describing: answer))") - case let .callExtraInfo(u, contact, extraInfo): return withUser(u, "contact: \(contact.id)\nextraInfo: \(String(describing: extraInfo))") - case let .callEnded(u, contact): return withUser(u, "contact: \(contact.id)") - case let .callInvitations(invs): return String(describing: invs) - case let .ntfTokenStatus(status): return String(describing: status) - case let .ntfToken(token, status, ntfMode): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)" - case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))") - case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))") - case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection)) - case let .remoteCtrlList(remoteCtrls): return String(describing: remoteCtrls) - case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible): return "remoteCtrl:\n\(String(describing: remoteCtrl))\nctrlAppInfo_:\n\(String(describing: ctrlAppInfo_))\nappVersion: \(appVersion)\ncompatible: \(compatible)" - case let .remoteCtrlConnecting(remoteCtrl_, ctrlAppInfo, appVersion): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nctrlAppInfo:\n\(String(describing: ctrlAppInfo))\nappVersion: \(appVersion)" - case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nsessionCode: \(sessionCode)" - case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl) - case .remoteCtrlStopped: return noDetails - case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))" - case .cmdOk: return noDetails - case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError)) - case let .chatError(u, chatError): return withUser(u, String(describing: chatError)) - case let .archiveImported(archiveErrors): return String(describing: archiveErrors) - } - } - } - - private var noDetails: String { get { "\(responseType): no details" } } - - private func withUser(_ u: (any UserLike)?, _ s: String) -> String { - if let id = u?.userId { - return "userId: \(id)\n\(s)" - } - return s - } -} - -public func chatError(_ chatResponse: ChatResponse) -> ChatErrorType? { - switch chatResponse { - case let .chatCmdError(_, .error(error)): return error - case let .chatError(_, .error(error)): return error - default: return nil - } -} - -public enum ConnectionPlan: Decodable { - case invitationLink(invitationLinkPlan: InvitationLinkPlan) - case contactAddress(contactAddressPlan: ContactAddressPlan) - case groupLink(groupLinkPlan: GroupLinkPlan) -} - -public enum InvitationLinkPlan: Decodable { - case ok - case ownLink - case connecting(contact_: Contact?) - case known(contact: Contact) -} - -public enum ContactAddressPlan: Decodable { - case ok - case ownLink - case connectingConfirmReconnect - case connectingProhibit(contact: Contact) - case known(contact: Contact) - case contactViaAddress(contact: Contact) -} - -public enum GroupLinkPlan: Decodable { - case ok - case ownLink(groupInfo: GroupInfo) - case connectingConfirmReconnect - case connectingProhibit(groupInfo_: GroupInfo?) - case known(groupInfo: GroupInfo) -} - -struct NewUser: Encodable { - var profile: Profile? - var sameServers: Bool - var pastTimestamp: Bool -} - -public enum ChatPagination { - case last(count: Int) - case after(chatItemId: Int64, count: Int) - case before(chatItemId: Int64, count: Int) - - var cmdString: String { - switch self { - case let .last(count): return "count=\(count)" - case let .after(chatItemId, count): return "after=\(chatItemId) count=\(count)" - case let .before(chatItemId, count): return "before=\(chatItemId) count=\(count)" - } - } -} - -struct ComposedMessage: Encodable { - var fileSource: CryptoFile? +public struct ComposedMessage: Encodable { + public var fileSource: CryptoFile? var quotedItemId: Int64? - var msgContent: MsgContent -} + public var msgContent: MsgContent + public var mentions: [String: Int64] -public struct XFTPFileConfig: Encodable { - var minFileSize: Int64 -} - -public struct ArchiveConfig: Encodable { - var archivePath: String - var disableCompression: Bool? - - public init(archivePath: String, disableCompression: Bool? = nil) { - self.archivePath = archivePath - self.disableCompression = disableCompression + public init(fileSource: CryptoFile? = nil, quotedItemId: Int64? = nil, msgContent: MsgContent, mentions: [String: Int64] = [:]) { + self.fileSource = fileSource + self.quotedItemId = quotedItemId + self.msgContent = msgContent + self.mentions = mentions } } -public struct DBEncryptionConfig: Codable { - public init(currentKey: String, newKey: String) { - self.currentKey = currentKey - self.newKey = newKey - } - - public var currentKey: String - public var newKey: String -} - -struct SMPServersConfig: Encodable { - var smpServers: [ServerCfg] -} - public enum ServerProtocol: String, Decodable { case smp case xftp } -public struct ProtoServersConfig: Codable { - public var servers: [ServerCfg] -} - -public struct UserProtoServers: Decodable { - public var serverProtocol: ServerProtocol - public var protoServers: [ServerCfg] - public var presetServers: [String] -} - -public struct ServerCfg: Identifiable, Equatable, Codable { - public var server: String - public var preset: Bool - public var tested: Bool? - public var enabled: Bool - var createdAt = Date() -// public var sendEnabled: Bool // can we potentially want to prevent sending on the servers we use to receive? -// Even if we don't see the use case, it's probably better to allow it in the model -// In any case, "trusted/known" servers are out of scope of this change - - public init(server: String, preset: Bool, tested: Bool?, enabled: Bool) { - self.server = server - self.preset = preset - self.tested = tested - self.enabled = enabled - } - - public static func == (l: ServerCfg, r: ServerCfg) -> Bool { - l.server == r.server && l.preset == r.preset && l.tested == r.tested && l.enabled == r.enabled - } - - public var id: String { "\(server) \(createdAt)" } - - public static var empty = ServerCfg(server: "", preset: false, tested: nil, enabled: true) - - public var isEmpty: Bool { - server.trimmingCharacters(in: .whitespaces) == "" - } - - public struct SampleData { - public var preset: ServerCfg - public var custom: ServerCfg - public var untested: ServerCfg - } - - public static var sampleData = SampleData( - preset: ServerCfg( - server: "smp://abcd@smp8.simplex.im", - preset: true, - tested: true, - enabled: true - ), - custom: ServerCfg( - server: "smp://abcd@smp9.simplex.im", - preset: false, - tested: false, - enabled: false - ), - untested: ServerCfg( - server: "smp://abcd@smp10.simplex.im", - preset: false, - tested: nil, - enabled: true - ) - ) - - enum CodingKeys: CodingKey { - case server - case preset - case tested - case enabled - } -} - -public enum ProtocolTestStep: String, Decodable, Equatable { - case connect - case disconnect - case createQueue - case secureQueue - case deleteQueue - case createFile - case uploadFile - case downloadFile - case compareFile - case deleteFile - - var text: String { - switch self { - case .connect: return NSLocalizedString("Connect", comment: "server test step") - case .disconnect: return NSLocalizedString("Disconnect", comment: "server test step") - case .createQueue: return NSLocalizedString("Create queue", comment: "server test step") - case .secureQueue: return NSLocalizedString("Secure queue", comment: "server test step") - case .deleteQueue: return NSLocalizedString("Delete queue", comment: "server test step") - case .createFile: return NSLocalizedString("Create file", comment: "server test step") - case .uploadFile: return NSLocalizedString("Upload file", comment: "server test step") - case .downloadFile: return NSLocalizedString("Download file", comment: "server test step") - case .compareFile: return NSLocalizedString("Compare file", comment: "server test step") - case .deleteFile: return NSLocalizedString("Delete file", comment: "server test step") - } - } -} - -public struct ProtocolTestFailure: Decodable, Error, Equatable { - var testStep: ProtocolTestStep - var testError: AgentErrorType - - public static func == (l: ProtocolTestFailure, r: ProtocolTestFailure) -> Bool { - l.testStep == r.testStep - } - - public var localizedDescription: String { - let err = String.localizedStringWithFormat(NSLocalizedString("Test failed at step %@.", comment: "server test failure"), testStep.text) - switch testError { - case .SMP(.AUTH): - return err + " " + NSLocalizedString("Server requires authorization to create queues, check password", comment: "server test error") - case .XFTP(.AUTH): - return err + " " + NSLocalizedString("Server requires authorization to upload, check password", comment: "server test error") - case .BROKER(_, .NETWORK): - return err + " " + NSLocalizedString("Possibly, certificate fingerprint in server address is incorrect", comment: "server test error") - default: - return err - } - } -} - public struct ServerAddress: Decodable { public var serverProtocol: ServerProtocol public var hostnames: [String] @@ -1193,41 +234,56 @@ public struct ServerAddress: Decodable { public struct NetCfg: Codable, Equatable { public var socksProxy: String? = nil + var socksMode: SocksMode = .always public var hostMode: HostMode = .publicHost public var requiredHostMode = true - public var sessionMode: TransportSessionMode + public var sessionMode = TransportSessionMode.user + public var smpProxyMode: SMPProxyMode = .always + public var smpProxyFallback: SMPProxyFallback = .allowProtected + public var smpWebPortServers: SMPWebPortServers = .preset public var tcpConnectTimeout: Int // microseconds public var tcpTimeout: Int // microseconds public var tcpTimeoutPerKb: Int // microseconds - public var tcpKeepAlive: KeepAliveOpts? + public var rcvConcurrency: Int // pool size + public var tcpKeepAlive: KeepAliveOpts? = KeepAliveOpts.defaults public var smpPingInterval: Int // microseconds - public var smpPingCount: Int // times - public var logTLSErrors: Bool + public var smpPingCount: Int = 3 // times + public var logTLSErrors: Bool = false public static let defaults: NetCfg = NetCfg( - socksProxy: nil, - sessionMode: TransportSessionMode.user, - tcpConnectTimeout: 20_000_000, + tcpConnectTimeout: 25_000_000, tcpTimeout: 15_000_000, - tcpTimeoutPerKb: 45_000, - tcpKeepAlive: KeepAliveOpts.defaults, - smpPingInterval: 1200_000_000, - smpPingCount: 3, - logTLSErrors: false + tcpTimeoutPerKb: 10_000, + rcvConcurrency: 12, + smpPingInterval: 1200_000_000 ) - public static let proxyDefaults: NetCfg = NetCfg( - socksProxy: nil, - sessionMode: TransportSessionMode.user, - tcpConnectTimeout: 30_000_000, + static let proxyDefaults: NetCfg = NetCfg( + tcpConnectTimeout: 35_000_000, tcpTimeout: 20_000_000, - tcpTimeoutPerKb: 60_000, - tcpKeepAlive: KeepAliveOpts.defaults, - smpPingInterval: 1200_000_000, - smpPingCount: 3, - logTLSErrors: false + tcpTimeoutPerKb: 15_000, + rcvConcurrency: 8, + smpPingInterval: 1200_000_000 ) + public var withProxyTimeouts: NetCfg { + var cfg = self + cfg.tcpConnectTimeout = NetCfg.proxyDefaults.tcpConnectTimeout + cfg.tcpTimeout = NetCfg.proxyDefaults.tcpTimeout + cfg.tcpTimeoutPerKb = NetCfg.proxyDefaults.tcpTimeoutPerKb + cfg.rcvConcurrency = NetCfg.proxyDefaults.rcvConcurrency + cfg.smpPingInterval = NetCfg.proxyDefaults.smpPingInterval + return cfg + } + + public var hasProxyTimeouts: Bool { + tcpConnectTimeout == NetCfg.proxyDefaults.tcpConnectTimeout && + tcpTimeout == NetCfg.proxyDefaults.tcpTimeout && + tcpTimeoutPerKb == NetCfg.proxyDefaults.tcpTimeoutPerKb && + rcvConcurrency == NetCfg.proxyDefaults.rcvConcurrency && + smpPingInterval == NetCfg.proxyDefaults.smpPingInterval + } + public var enableKeepAlive: Bool { tcpKeepAlive != nil } } @@ -1237,6 +293,63 @@ public enum HostMode: String, Codable { case publicHost = "public" } +public enum SocksMode: String, Codable { + case always = "always" + case onion = "onion" +} + +public enum SMPProxyMode: String, Codable, SelectableItem { + case always = "always" + case unknown = "unknown" + case unprotected = "unprotected" + case never = "never" + + public var label: LocalizedStringKey { + switch self { + case .always: return "always" + case .unknown: return "unknown servers" + case .unprotected: return "unprotected" + case .never: return "never" + } + } + + public var id: SMPProxyMode { self } + + public static let values: [SMPProxyMode] = [.always, .unknown, .unprotected, .never] +} + +public enum SMPProxyFallback: String, Codable, SelectableItem { + case allow = "allow" + case allowProtected = "allowProtected" + case prohibit = "prohibit" + + public var label: LocalizedStringKey { + switch self { + case .allow: return "yes" + case .allowProtected: return "when IP hidden" + case .prohibit: return "no" + } + } + + public var id: SMPProxyFallback { self } + + public static let values: [SMPProxyFallback] = [.allow, .allowProtected, .prohibit] +} + +public enum SMPWebPortServers: String, Codable, CaseIterable { + case all = "all" + case preset = "preset" + case off = "off" + + public var text: LocalizedStringKey { + switch self { + case .all: "All servers" + case .preset: "Preset servers" + case .off: "Off" + } + } +} + public enum OnionHosts: String, Identifiable { case no case prefer @@ -1273,18 +386,22 @@ public enum OnionHosts: String, Identifiable { public enum TransportSessionMode: String, Codable, Identifiable { case user + case session + case server case entity public var text: LocalizedStringKey { switch self { - case .user: return "User profile" + case .user: return "Chat profile" + case .session: return "App session" + case .server: return "Server" case .entity: return "Connection" } } public var id: TransportSessionMode { self } - public static let values: [TransportSessionMode] = [.user, .entity] + public static let values: [TransportSessionMode] = [.user, .session, .server, .entity] } public struct KeepAliveOpts: Codable, Equatable { @@ -1295,50 +412,64 @@ public struct KeepAliveOpts: Codable, Equatable { public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4) } -public enum NetworkStatus: Decodable, Equatable { - case unknown - case connected - case disconnected - case error(connectionError: String) +public struct NetworkProxy: Equatable, Codable { + public var host: String = "" + public var port: Int = 0 + public var auth: NetworkProxyAuth = .username + public var username: String = "" + public var password: String = "" - public var statusString: LocalizedStringKey { - get { - switch self { - case .connected: return "connected" - case .error: return "error" - default: return "connecting" - } - } + public static var def: NetworkProxy { + NetworkProxy() } - public var statusExplanation: LocalizedStringKey { - get { - switch self { - case .connected: return "You are connected to the server used to receive messages from this contact." - case let .error(err): return "Trying to connect to the server used to receive messages from this contact (error: \(err))." - default: return "Trying to connect to the server used to receive messages from this contact." - } + public var valid: Bool { + let hostOk = switch NWEndpoint.Host(host) { + case .ipv4: true + case .ipv6: true + default: false } + return hostOk && + port > 0 && port <= 65535 && + NetworkProxy.validCredential(username) && NetworkProxy.validCredential(password) } - public var imageName: String { - get { - switch self { - case .unknown: return "circle.dotted" - case .connected: return "circle.fill" - case .disconnected: return "ellipsis.circle.fill" - case .error: return "exclamationmark.circle.fill" + public static func validCredential(_ s: String) -> Bool { + !s.contains(":") && !s.contains("@") + } + + public func toProxyString() -> String? { + if !valid { return nil } + var res = "" + switch auth { + case .username: + let usernameTrimmed = username.trimmingCharacters(in: .whitespaces) + let passwordTrimmed = password.trimmingCharacters(in: .whitespaces) + if usernameTrimmed != "" || passwordTrimmed != "" { + res += usernameTrimmed + ":" + passwordTrimmed + "@" + } else { + res += "@" + } + case .isolate: () + } + if host != "" { + if host.contains(":") { + res += "[\(host.trimmingCharacters(in: [" ", "[", "]"]))]" + } else { + res += host.trimmingCharacters(in: .whitespaces) } } + res += ":\(port)" + return res } } -public struct ConnNetworkStatus: Decodable { - public var agentConnId: String - public var networkStatus: NetworkStatus +public enum NetworkProxyAuth: String, Codable { + case username + case isolate } -public struct ChatSettings: Codable { +public struct ChatSettings: Codable, Hashable { public var enableNtfs: MsgFilter public var sendRcpts: Bool? public var favorite: Bool @@ -1352,23 +483,58 @@ public struct ChatSettings: Codable { public static let defaults: ChatSettings = ChatSettings(enableNtfs: .all, sendRcpts: nil, favorite: false) } -public enum MsgFilter: String, Codable { - case none - case all - case mentions -} +public struct NavigationInfo: Decodable { + public var afterUnread: Int = 0 + public var afterTotal: Int = 0 -public struct UserMsgReceiptSettings: Codable { - public var enable: Bool - public var clearOverrides: Bool - - public init(enable: Bool, clearOverrides: Bool) { - self.enable = enable - self.clearOverrides = clearOverrides + public init(afterUnread: Int = 0, afterTotal: Int = 0) { + self.afterUnread = afterUnread + self.afterTotal = afterTotal } } -public struct ConnectionStats: Decodable { +public enum MsgFilter: String, Codable, Hashable { + case none + case all + case mentions + + public func nextMode(mentions: Bool) -> MsgFilter { + switch self { + case .all: mentions ? .mentions : .none + case .mentions: .none + case .none: .all + } + } + + public func text(mentions: Bool) -> String { + switch self { + case .all: NSLocalizedString("Unmute", comment: "notification label action") + case .mentions: NSLocalizedString("Mute", comment: "notification label action") + case .none: + mentions + ? NSLocalizedString("Mute all", comment: "notification label action") + : NSLocalizedString("Mute", comment: "notification label action") + } + } + + public var icon: String { + return switch self { + case .all: "speaker.wave.2" + case .mentions: "speaker.badge.exclamationmark" + case .none: "speaker.slash" + } + } + + public var iconFilled: String { + return switch self { + case .all: "speaker.wave.2.fill" + case .mentions: "speaker.badge.exclamationmark.fill" + case .none: "speaker.slash.fill" + } + } +} + +public struct ConnectionStats: Decodable, Hashable { public var connAgentVersion: Int public var rcvQueuesInfo: [RcvQueueInfo] public var sndQueuesInfo: [SndQueueInfo] @@ -1382,27 +548,31 @@ public struct ConnectionStats: Decodable { public var ratchetSyncSendProhibited: Bool { [.required, .started, .agreed].contains(ratchetSyncState) } + + public var ratchetSyncInProgress: Bool { + [.started, .agreed].contains(ratchetSyncState) + } } -public struct RcvQueueInfo: Codable { +public struct RcvQueueInfo: Codable, Hashable { public var rcvServer: String public var rcvSwitchStatus: RcvSwitchStatus? public var canAbortSwitch: Bool } -public enum RcvSwitchStatus: String, Codable { +public enum RcvSwitchStatus: String, Codable, Hashable { case switchStarted = "switch_started" case sendingQADD = "sending_qadd" case sendingQUSE = "sending_quse" case receivedMessage = "received_message" } -public struct SndQueueInfo: Codable { +public struct SndQueueInfo: Codable, Hashable { public var sndServer: String public var sndSwitchStatus: SndSwitchStatus? } -public enum SndSwitchStatus: String, Codable { +public enum SndSwitchStatus: String, Codable, Hashable { case sendingQKEY = "sending_qkey" case sendingQTEST = "sending_qtest" } @@ -1431,94 +601,12 @@ public enum RatchetSyncState: String, Decodable { case agreed } -public struct UserContactLink: Decodable { - public var connReqContact: String - public var autoAccept: AutoAccept? - - public init(connReqContact: String, autoAccept: AutoAccept? = nil) { - self.connReqContact = connReqContact - self.autoAccept = autoAccept - } - - var responseDetails: String { - "connReqContact: \(connReqContact)\nautoAccept: \(AutoAccept.cmdString(autoAccept))" - } -} - -public struct AutoAccept: Codable { - public var acceptIncognito: Bool - public var autoReply: MsgContent? - - public init(acceptIncognito: Bool, autoReply: MsgContent? = nil) { - self.acceptIncognito = acceptIncognito - self.autoReply = autoReply - } - - static func cmdString(_ autoAccept: AutoAccept?) -> String { - guard let autoAccept = autoAccept else { return "off" } - let s = "on" + (autoAccept.acceptIncognito ? " incognito=on" : "") - guard let msg = autoAccept.autoReply else { return s } - return s + " " + msg.cmdString - } -} - -public protocol SelectableItem: Hashable, Identifiable { +public protocol SelectableItem: Identifiable, Equatable { var label: LocalizedStringKey { get } static var values: [Self] { get } } -public struct DeviceToken: Decodable { - var pushProvider: PushProvider - var token: String - - public init(pushProvider: PushProvider, token: String) { - self.pushProvider = pushProvider - self.token = token - } - - public var cmdString: String { - "\(pushProvider) \(token)" - } -} - -public enum PushEnvironment: String { - case development - case production -} - -public enum PushProvider: String, Decodable { - case apns_dev - case apns_prod - - public init(env: PushEnvironment) { - switch env { - case .development: self = .apns_dev - case .production: self = .apns_prod - } - } -} - -// This notification mode is for app core, UI uses AppNotificationsMode.off to mean completely disable, -// and .local for periodic background checks -public enum NotificationsMode: String, Decodable, SelectableItem { - case off = "OFF" - case periodic = "PERIODIC" - case instant = "INSTANT" - - public var label: LocalizedStringKey { - switch self { - case .off: "Local" - case .periodic: "Periodically" - case .instant: "Instantly" - } - } - - public var id: String { self.rawValue } - - public static var values: [NotificationsMode] = [.instant, .periodic, .off] -} - -public enum NotificationPreviewMode: String, SelectableItem { +public enum NotificationPreviewMode: String, SelectableItem, Codable { case hidden case contact case message @@ -1536,47 +624,6 @@ public enum NotificationPreviewMode: String, SelectableItem { public static var values: [NotificationPreviewMode] = [.message, .contact, .hidden] } -public struct RemoteCtrlInfo: Decodable { - public var remoteCtrlId: Int64 - public var ctrlDeviceName: String - public var sessionState: RemoteCtrlSessionState? - - public var deviceViewName: String { - ctrlDeviceName == "" ? "\(remoteCtrlId)" : ctrlDeviceName - } -} - -public enum RemoteCtrlSessionState: Decodable { - case starting - case searching - case connecting - case pendingConfirmation(sessionCode: String) - case connected(sessionCode: String) -} - -public enum RemoteCtrlStopReason: Decodable { - case discoveryFailed(chatError: ChatError) - case connectionFailed(chatError: ChatError) - case setupFailed(chatError: ChatError) - case disconnected -} - -public struct CtrlAppInfo: Decodable { - public var appVersionRange: AppVersionRange - public var deviceName: String -} - -public struct AppVersionRange: Decodable { - public var minVersion: String - public var maxVersion: String -} - -public struct CoreVersionInfo: Decodable { - public var version: String - public var simplexmqVersion: String - public var simplexmqCommit: String -} - public func decodeJSON<T: Decodable>(_ json: String) -> T? { if let data = json.data(using: .utf8) { return try? jsonDecoder.decode(T.self, from: data) @@ -1593,16 +640,29 @@ private func encodeCJSON<T: Encodable>(_ value: T) -> [CChar] { encodeJSON(value).cString(using: .utf8)! } -public enum ChatError: Decodable { +public enum ChatError: Decodable, Hashable, Error { case error(errorType: ChatErrorType) case errorAgent(agentError: AgentErrorType) case errorStore(storeError: StoreError) case errorDatabase(databaseError: DatabaseError) case errorRemoteCtrl(remoteCtrlError: RemoteCtrlError) - case invalidJSON(json: String) + case invalidJSON(json: Data?) // additional case used to pass errors that failed to parse + case unexpectedResult(type: String) // additional case used to pass unexpected responses + + public var errorType: String { + switch self { + case .error: "chat" + case .errorAgent: "agent" + case .errorStore: "store" + case .errorDatabase: "database" + case .errorRemoteCtrl: "remoteCtrl" + case .invalidJSON: "invalid" + case let .unexpectedResult(type): "! \(type)" + } + } } -public enum ChatErrorType: Decodable { +public enum ChatErrorType: Decodable, Hashable { case noActiveUser case noConnectionUser(agentConnId: String) case noSndFileUser(agentSndFileId: String) @@ -1610,6 +670,7 @@ public enum ChatErrorType: Decodable { case userUnknown case activeUserExists case userExists + case invalidDisplayName case differentActiveUser(commandUserId: Int64, activeUserId: Int64) case cantDeleteActiveUser(userId: Int64) case cantDeleteLastUser(userId: Int64) @@ -1621,8 +682,8 @@ public enum ChatErrorType: Decodable { case chatNotStarted case chatNotStopped case chatStoreChanged - case connectionPlan(connectionPlan: ConnectionPlan) case invalidConnReq + case unsupportedConnReq case invalidChatMessage(connection: Connection, message: String) case contactNotReady(contact: Contact) case contactNotActive(contact: Contact) @@ -1639,7 +700,6 @@ public enum ChatErrorType: Decodable { case groupMemberNotActive case groupMemberUserRemoved case groupMemberNotFound - case groupMemberIntroNotFound(contactName: ContactName) case groupCantResendInvitation(groupInfo: GroupInfo, contactName: ContactName) case groupInternal(message: String) case fileNotFound(message: String) @@ -1656,11 +716,11 @@ public enum ChatErrorType: Decodable { case fileImageType(filePath: String) case fileImageSize(filePath: String) case fileNotReceived(fileId: Int64) - // case xFTPRcvFile - // case xFTPSndFile + case fileNotApproved(fileId: Int64, unknownServers: [String]) case fallbackToSMPProhibited(fileId: Int64) case inlineFileProhibited(fileId: Int64) case invalidQuote + case invalidForward case invalidChatItemUpdate case invalidChatItemDelete case hasCurrentCall @@ -1675,12 +735,13 @@ public enum ChatErrorType: Decodable { case agentCommandError(message: String) case invalidFileDescription(message: String) case connectionIncognitoChangeProhibited + case connectionUserChangeProhibited case peerChatVRangeIncompatible case internalError(message: String) case exception(message: String) } -public enum StoreError: Decodable { +public enum StoreError: Decodable, Hashable { case duplicateName case userNotFound(userId: Int64) case userNotFoundByName(contactName: ContactName) @@ -1717,6 +778,7 @@ public enum StoreError: Decodable { case fileIdNotFoundBySharedMsgId(sharedMsgId: String) case sndFileNotFoundXFTP(agentSndFileId: String) case rcvFileNotFoundXFTP(agentRcvFileId: String) + case extraFileDescrNotFoundXFTP(fileId: Int64) case connectionNotFound(agentConnId: String) case connectionNotFoundById(connId: Int64) case connectionNotFoundByMemberId(groupMemberId: Int64) @@ -1737,9 +799,10 @@ public enum StoreError: Decodable { case hostMemberIdNotFound(groupId: Int64) case contactNotFoundByFileId(fileId: Int64) case noGroupSndStatus(itemId: Int64, groupMemberId: Int64) + case dBException(message: String) } -public enum DatabaseError: Decodable { +public enum DatabaseError: Decodable, Hashable { case errorEncrypted case errorPlaintext case errorNoFile(dbFile: String) @@ -1747,25 +810,27 @@ public enum DatabaseError: Decodable { case errorOpen(sqliteError: SQLiteError) } -public enum SQLiteError: Decodable { +public enum SQLiteError: Decodable, Hashable { case errorNotADatabase - case error(String) + case error(dbError: String) } -public enum AgentErrorType: Decodable { - case CMD(cmdErr: CommandErrorType) +public enum AgentErrorType: Decodable, Hashable { + case CMD(cmdErr: CommandErrorType, errContext: String) case CONN(connErr: ConnectionErrorType) - case SMP(smpErr: ProtocolErrorType) + case SMP(serverAddress: String, smpErr: ProtocolErrorType) case NTF(ntfErr: ProtocolErrorType) case XFTP(xftpErr: XFTPErrorType) + case PROXY(proxyServer: String, relayServer: String, proxyErr: ProxyClientError) case RCP(rcpErr: RCErrorType) case BROKER(brokerAddress: String, brokerErr: BrokerErrorType) case AGENT(agentErr: SMPAgentError) case INTERNAL(internalErr: String) + case CRITICAL(offerRestart: Bool, criticalErr: String) case INACTIVE } -public enum CommandErrorType: Decodable { +public enum CommandErrorType: Decodable, Hashable { case PROHIBITED case SYNTAX case NO_CONN @@ -1773,7 +838,7 @@ public enum CommandErrorType: Decodable { case LARGE } -public enum ConnectionErrorType: Decodable { +public enum ConnectionErrorType: Decodable, Hashable { case NOT_FOUND case DUPLICATE case SIMPLEX @@ -1781,7 +846,7 @@ public enum ConnectionErrorType: Decodable { case NOT_AVAILABLE } -public enum BrokerErrorType: Decodable { +public enum BrokerErrorType: Decodable, Hashable { case RESPONSE(smpErr: String) case UNEXPECTED case NETWORK @@ -1790,22 +855,51 @@ public enum BrokerErrorType: Decodable { case TIMEOUT } -public enum ProtocolErrorType: Decodable { +public enum ProtocolErrorType: Decodable, Hashable { case BLOCK case SESSION case CMD(cmdErr: ProtocolCommandError) + indirect case PROXY(proxyErr: ProxyError) case AUTH + case BLOCKED(blockInfo: BlockingInfo) + case CRYPTO case QUOTA + case STORE(storeErr: String) case NO_MSG case LARGE_MSG + case EXPIRED case INTERNAL } -public enum XFTPErrorType: Decodable { +public enum ProxyError: Decodable, Hashable { + case PROTOCOL(protocolErr: ProtocolErrorType) + case BROKER(brokerErr: BrokerErrorType) + case BASIC_AUTH + case NO_SESSION +} + +public struct BlockingInfo: Decodable, Equatable, Hashable { + public var reason: BlockingReason +} + +public enum BlockingReason: String, Decodable { + case spam + case content + + public var text: String { + switch self { + case .spam: NSLocalizedString("Spam", comment: "blocking reason") + case .content: NSLocalizedString("Content violates conditions of use", comment: "blocking reason") + } + } +} + +public enum XFTPErrorType: Decodable, Hashable { case BLOCK case SESSION case CMD(cmdErr: ProtocolCommandError) case AUTH + case BLOCKED(blockInfo: BlockingInfo) case SIZE case QUOTA case DIGEST @@ -1813,13 +907,23 @@ public enum XFTPErrorType: Decodable { case NO_FILE case HAS_FILE case FILE_IO + case TIMEOUT + case REDIRECT(redirectError: String) case INTERNAL } -public enum RCErrorType: Decodable { +public enum ProxyClientError: Decodable, Hashable { + case protocolError(protocolErr: ProtocolErrorType) + case unexpectedResponse(responseStr: String) + case responseError(responseErr: ProtocolErrorType) +} + +public enum RCErrorType: Decodable, Hashable { case `internal`(internalErr: String) case identity case noLocalAddress + case newController + case notDiscovered case tlsStartFailed case exception(exception: String) case ctrlAuth @@ -1832,7 +936,7 @@ public enum RCErrorType: Decodable { case syntax(syntaxErr: String) } -public enum ProtocolCommandError: Decodable { +public enum ProtocolCommandError: Decodable, Hashable { case UNKNOWN case SYNTAX case PROHIBITED @@ -1841,20 +945,23 @@ public enum ProtocolCommandError: Decodable { case NO_ENTITY } -public enum ProtocolTransportError: Decodable { +public enum ProtocolTransportError: Decodable, Hashable { case badBlock + case version case largeMsg case badSession + case noServerAuth case handshake(handshakeErr: SMPHandshakeError) } -public enum SMPHandshakeError: Decodable { +public enum SMPHandshakeError: Decodable, Hashable { case PARSE case VERSION case IDENTITY + case BAD_AUTH } -public enum SMPAgentError: Decodable { +public enum SMPAgentError: Decodable, Hashable { case A_MESSAGE case A_PROHIBITED case A_VERSION @@ -1863,18 +970,33 @@ public enum SMPAgentError: Decodable { case A_QUEUE(queueErr: String) } -public enum ArchiveError: Decodable { - case `import`(chatError: ChatError) - case importFile(file: String, chatError: ChatError) +public enum ArchiveError: Decodable, Hashable { + case `import`(importError: String) + case fileError(file: String, fileError: String) } -public enum RemoteCtrlError: Decodable { +public enum RemoteCtrlError: Decodable, Hashable { case inactive case badState case busy case timeout + case noKnownControllers + case badController case disconnected(remoteCtrlId: Int64, reason: String) case badInvitation case badVersion(appVersion: String) -// case protocolError(protocolError: RemoteProtocolError) + case hTTP2Error(http2Error: String) + case protocolError +} + +public struct AppFilePaths: Encodable { + public let appFilesFolder: String + public let appTempFolder: String + public let appAssetsFolder: String + + public init(appFilesFolder: String, appTempFolder: String, appAssetsFolder: String) { + self.appFilesFolder = appFilesFolder + self.appTempFolder = appTempFolder + self.appAssetsFolder = appAssetsFolder + } } diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index f79c294e0c..29ccab7357 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -11,23 +11,40 @@ import SwiftUI public let appSuspendTimeout: Int = 15 // seconds +public let defaultProfileImageCorner: Double = 22.5 + let GROUP_DEFAULT_APP_STATE = "appState" let GROUP_DEFAULT_NSE_STATE = "nseState" +let GROUP_DEFAULT_SE_STATE = "seState" let GROUP_DEFAULT_DB_CONTAINER = "dbContainer" public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart" public let GROUP_DEFAULT_CHAT_LAST_BACKGROUND_RUN = "chatLastBackgroundRun" let GROUP_DEFAULT_NTF_PREVIEW_MODE = "ntfPreviewMode" public let GROUP_DEFAULT_NTF_ENABLE_LOCAL = "ntfEnableLocal" // no longer used public let GROUP_DEFAULT_NTF_ENABLE_PERIODIC = "ntfEnablePeriodic" // no longer used +// replaces DEFAULT_PERFORM_LA +let GROUP_DEFAULT_APP_LOCAL_AUTH_ENABLED = "appLocalAuthEnabled" +public let GROUP_DEFAULT_ALLOW_SHARE_EXTENSION = "allowShareExtension" +// replaces DEFAULT_PRIVACY_LINK_PREVIEWS +let GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" +// This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles" +public let GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS = "privacyAskToApproveRelays" +// replaces DEFAULT_PROFILE_IMAGE_CORNER_RADIUS +public let GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius" let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount" +public let GROUP_DEFAULT_NETWORK_SOCKS_PROXY = "networkSocksProxy" let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts" let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode" +let GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE = "networkSMPProxyMode" +let GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK = "networkSMPProxyFallback" +let GROUP_DEFAULT_NETWORK_SMP_WEB_PORT_SERVERS = "networkSMPWebPortServers" let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout" let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout" let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb" +let GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY = "networkRcvConcurrency" let GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL = "networkSMPPingInterval" let GROUP_DEFAULT_NETWORK_SMP_PING_COUNT = "networkSMPPingCount" let GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE = "networkEnableKeepAlive" @@ -36,9 +53,12 @@ let GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL = "networkTCPKeepIntvl" let GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT = "networkTCPKeepCnt" public let GROUP_DEFAULT_INCOGNITO = "incognito" let GROUP_DEFAULT_STORE_DB_PASSPHRASE = "storeDBPassphrase" -let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase" +public let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase" public let GROUP_DEFAULT_CONFIRM_DB_UPGRADES = "confirmDBUpgrades" public let GROUP_DEFAULT_CALL_KIT_ENABLED = "callKitEnabled" +public let GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED = "pqExperimentalEnabled" // no longer used +public let GROUP_DEFAULT_ONE_HAND_UI = "oneHandUI" +public let GROUP_DEFAULT_CHAT_BOTTOM_BAR = "chatBottomBar" public let APP_GROUP_NAME = "group.chat.simplex.app" @@ -49,10 +69,14 @@ public func registerGroupDefaults() { GROUP_DEFAULT_NTF_ENABLE_LOCAL: false, GROUP_DEFAULT_NTF_ENABLE_PERIODIC: false, GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS: OnionHosts.no.rawValue, - GROUP_DEFAULT_NETWORK_SESSION_MODE: TransportSessionMode.user.rawValue, + GROUP_DEFAULT_NETWORK_SESSION_MODE: TransportSessionMode.session.rawValue, + GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE: SMPProxyMode.unknown.rawValue, + GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK: SMPProxyFallback.allowProtected.rawValue, + GROUP_DEFAULT_NETWORK_SMP_WEB_PORT_SERVERS: SMPWebPortServers.preset.rawValue, GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT: NetCfg.defaults.tcpConnectTimeout, GROUP_DEFAULT_NETWORK_TCP_TIMEOUT: NetCfg.defaults.tcpTimeout, GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB: NetCfg.defaults.tcpTimeoutPerKb, + GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY: NetCfg.defaults.rcvConcurrency, GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL: NetCfg.defaults.smpPingInterval, GROUP_DEFAULT_NETWORK_SMP_PING_COUNT: NetCfg.defaults.smpPingCount, GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE: NetCfg.defaults.enableKeepAlive, @@ -62,11 +86,19 @@ public func registerGroupDefaults() { GROUP_DEFAULT_INCOGNITO: false, GROUP_DEFAULT_STORE_DB_PASSPHRASE: true, GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE: false, + GROUP_DEFAULT_APP_LOCAL_AUTH_ENABLED: true, + GROUP_DEFAULT_ALLOW_SHARE_EXTENSION: false, + GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS: true, GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true, GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false, GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true, + GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS: true, + GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner, GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false, GROUP_DEFAULT_CALL_KIT_ENABLED: true, + GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED: false, + GROUP_DEFAULT_ONE_HAND_UI: true, + GROUP_DEFAULT_CHAT_BOTTOM_BAR: true ]) } @@ -125,6 +157,11 @@ public enum NSEState: String, Codable { } } +public enum SEState: String, Codable { + case inactive + case sendingMessage +} + public enum DBContainer: String { case documents case group @@ -144,6 +181,12 @@ public let nseStateGroupDefault = EnumDefault<NSEState>( withDefault: .suspended // so that NSE that was never launched does not delay the app from resuming ) +public let seStateGroupDefault = EnumDefault<SEState>( + defaults: groupDefaults, + forKey: GROUP_DEFAULT_SE_STATE, + withDefault: .inactive +) + // inactive app states do not include "stopped" state public func allowBackgroundRefresh() -> Bool { appStateGroupDefault.get().inactive && nseStateGroupDefault.get().inactive @@ -167,10 +210,21 @@ public let ntfPreviewModeGroupDefault = EnumDefault<NotificationPreviewMode>( public let incognitoGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INCOGNITO) +public let appLocalAuthEnabledGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_APP_LOCAL_AUTH_ENABLED) + +public let allowShareExtensionGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_ALLOW_SHARE_EXTENSION) + +public let privacyLinkPreviewsGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS) + +// This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES) public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES) +public let privacyAskToApproveRelaysGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS) + +public let profileImageCornerRadiusGroupDefault = Default<Double>(defaults: groupDefaults, forKey: GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) + public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT) public let networkUseOnionHostsGroupDefault = EnumDefault<OnionHosts>( @@ -182,7 +236,25 @@ public let networkUseOnionHostsGroupDefault = EnumDefault<OnionHosts>( public let networkSessionModeGroupDefault = EnumDefault<TransportSessionMode>( defaults: groupDefaults, forKey: GROUP_DEFAULT_NETWORK_SESSION_MODE, - withDefault: .user + withDefault: .session +) + +public let networkSMPProxyModeGroupDefault = EnumDefault<SMPProxyMode>( + defaults: groupDefaults, + forKey: GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE, + withDefault: .unknown +) + +public let networkSMPProxyFallbackGroupDefault = EnumDefault<SMPProxyFallback>( + defaults: groupDefaults, + forKey: GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK, + withDefault: .allowProtected +) + +public let networkSMPWebPortServersDefault = EnumDefault<SMPWebPortServers>( + defaults: groupDefaults, + forKey: GROUP_DEFAULT_NETWORK_SMP_WEB_PORT_SERVERS, + withDefault: .preset ) public let storeDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_STORE_DB_PASSPHRASE) @@ -239,12 +311,14 @@ public class EnumDefault<T: RawRepresentable> where T.RawValue == String { } public class BoolDefault: Default<Bool> { + @inline(__always) public func get() -> Bool { self.defaults.bool(forKey: self.key) } } public class IntDefault: Default<Int> { + @inline(__always) public func get() -> Int { self.defaults.integer(forKey: self.key) } @@ -254,28 +328,31 @@ public class Default<T> { var defaults: UserDefaults var key: String + @inline(__always) public init(defaults: UserDefaults = UserDefaults.standard, forKey: String) { self.defaults = defaults self.key = forKey } + @inline(__always) public func set(_ value: T) { defaults.set(value, forKey: key) defaults.synchronize() } } -public func getXFTPCfg() -> XFTPFileConfig { - return XFTPFileConfig(minFileSize: 0) -} - public func getNetCfg() -> NetCfg { + let socksProxy = groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) let onionHosts = networkUseOnionHostsGroupDefault.get() let (hostMode, requiredHostMode) = onionHosts.hostMode let sessionMode = networkSessionModeGroupDefault.get() + let smpProxyMode = networkSMPProxyModeGroupDefault.get() + let smpProxyFallback = networkSMPProxyFallbackGroupDefault.get() + let smpWebPortServers = networkSMPWebPortServersDefault.get() let tcpConnectTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) let tcpTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) let tcpTimeoutPerKb = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) + let rcvConcurrency = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY) let smpPingInterval = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL) let smpPingCount = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_SMP_PING_COUNT) let enableKeepAlive = groupDefaults.bool(forKey: GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE) @@ -289,12 +366,17 @@ public func getNetCfg() -> NetCfg { tcpKeepAlive = nil } return NetCfg( + socksProxy: socksProxy, hostMode: hostMode, requiredHostMode: requiredHostMode, sessionMode: sessionMode, + smpProxyMode: smpProxyMode, + smpProxyFallback: smpProxyFallback, + smpWebPortServers: smpWebPortServers, tcpConnectTimeout: tcpConnectTimeout, tcpTimeout: tcpTimeout, tcpTimeoutPerKb: tcpTimeoutPerKb, + rcvConcurrency: rcvConcurrency, tcpKeepAlive: tcpKeepAlive, smpPingInterval: smpPingInterval, smpPingCount: smpPingCount, @@ -302,12 +384,18 @@ public func getNetCfg() -> NetCfg { ) } -public func setNetCfg(_ cfg: NetCfg) { +public func setNetCfg(_ cfg: NetCfg, networkProxy: NetworkProxy?) { networkUseOnionHostsGroupDefault.set(OnionHosts(netCfg: cfg)) networkSessionModeGroupDefault.set(cfg.sessionMode) + networkSMPProxyModeGroupDefault.set(cfg.smpProxyMode) + networkSMPProxyFallbackGroupDefault.set(cfg.smpProxyFallback) + let socksProxy = networkProxy?.toProxyString() + groupDefaults.set(socksProxy, forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) + networkSMPWebPortServersDefault.set(cfg.smpWebPortServers) groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) + groupDefaults.set(cfg.rcvConcurrency, forKey: GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY) groupDefaults.set(cfg.smpPingInterval, forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL) groupDefaults.set(cfg.smpPingCount, forKey: GROUP_DEFAULT_NETWORK_SMP_PING_COUNT) if let tcpKeepAlive = cfg.tcpKeepAlive { diff --git a/apps/ios/SimpleXChat/CallTypes.swift b/apps/ios/SimpleXChat/CallTypes.swift index 227a1fbda5..da1720c134 100644 --- a/apps/ios/SimpleXChat/CallTypes.swift +++ b/apps/ios/SimpleXChat/CallTypes.swift @@ -42,6 +42,7 @@ public struct RcvCallInvitation: Decodable { public var contact: Contact public var callType: CallType public var sharedKey: String? + public var callUUID: String? public var callTs: Date public var callTypeText: LocalizedStringKey { get { @@ -52,10 +53,8 @@ public struct RcvCallInvitation: Decodable { } } - public var callkitUUID: UUID? = UUID() - private enum CodingKeys: String, CodingKey { - case user, contact, callType, sharedKey, callTs + case user, contact, callType, sharedKey, callUUID, callTs } public static let sampleData = RcvCallInvitation( @@ -81,6 +80,14 @@ public enum CallMediaType: String, Codable, Equatable { case audio = "audio" } +public enum CallMediaSource: String, Codable, Equatable { + case mic = "mic" + case camera = "camera" + case screenAudio = "screenAudio" + case screenVideo = "screenVideo" + case unknown = "unknown" +} + public enum VideoCamera: String, Codable, Equatable { case user = "user" case environment = "environment" diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index a545d3508c..88246465e1 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -9,13 +9,23 @@ import Foundation import SwiftUI -public struct User: Identifiable, Decodable, UserLike, NamedChat { +// version to establishing direct connection with a group member (xGrpDirectInvVRange in core) +public let CREATE_MEMBER_CONTACT_VERSION = 2 + +// version to receive reports (MCReport) +public let REPORTS_VERSION = 12 + +public let contentModerationPostLink = URL(string: "https://simplex.chat/blog/20250114-simplex-network-large-groups-privacy-preserving-content-moderation.html#preventing-server-abuse-without-compromising-e2e-encryption")! + +public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable { public var userId: Int64 + public var agentUserId: String var userContactId: Int64 var localDisplayName: ContactName public var profile: LocalProfile public var fullPreferences: FullPreferences public var activeUser: Bool + public var activeOrder: Int64 public var displayName: String { get { profile.displayName } } public var fullName: String { get { profile.fullName } } @@ -26,6 +36,7 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat { public var sendRcptsContacts: Bool public var sendRcptsSmallGroups: Bool public var viewPwdHash: UserPwdHash? + public var uiThemes: ThemeModeOverrides? public var id: Int64 { userId } @@ -41,18 +52,20 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat { public static let sampleData = User( userId: 1, + agentUserId: "abc", userContactId: 1, localDisplayName: "alice", profile: LocalProfile.sampleData, fullPreferences: FullPreferences.sampleData, activeUser: true, + activeOrder: 0, showNtfs: true, sendRcptsContacts: true, sendRcptsSmallGroups: false ) } -public struct UserRef: Identifiable, Decodable, UserLike { +public struct UserRef: Identifiable, Decodable, UserLike, Hashable { public var userId: Int64 public var localDisplayName: ContactName @@ -63,12 +76,12 @@ public protocol UserLike: Identifiable { var userId: Int64 { get } } -public struct UserPwdHash: Decodable { +public struct UserPwdHash: Decodable, Hashable { public var hash: String public var salt: String } -public struct UserInfo: Decodable, Identifiable { +public struct UserInfo: Decodable, Identifiable, Hashable { public var user: User public var unreadCount: Int @@ -89,7 +102,7 @@ public typealias ContactName = String public typealias GroupName = String -public struct Profile: Codable, NamedChat { +public struct Profile: Codable, NamedChat, Hashable { public init( displayName: String, fullName: String, @@ -121,7 +134,7 @@ public struct Profile: Codable, NamedChat { ) } -public struct LocalProfile: Codable, NamedChat { +public struct LocalProfile: Codable, NamedChat, Hashable { public init( profileId: Int64, displayName: String, @@ -171,16 +184,16 @@ public func fromLocalProfile (_ profile: LocalProfile) -> Profile { Profile(displayName: profile.displayName, fullName: profile.fullName, image: profile.image, contactLink: profile.contactLink, preferences: profile.preferences) } -public struct UserProfileUpdateSummary: Decodable { - public var notChanged: Int +public struct UserProfileUpdateSummary: Decodable, Hashable { public var updateSuccesses: Int public var updateFailures: Int public var changedContacts: [Contact] } -public enum ChatType: String { +public enum ChatType: String, Hashable { case direct = "@" case group = "#" + case local = "*" case contactRequest = "<@" case contactConnection = ":" } @@ -202,7 +215,7 @@ extension NamedChat { public typealias ChatId = String -public struct FullPreferences: Decodable, Equatable { +public struct FullPreferences: Decodable, Equatable, Hashable { public var timedMessages: TimedMessagesPreference public var fullDelete: SimplePreference public var reactions: SimplePreference @@ -232,7 +245,7 @@ public struct FullPreferences: Decodable, Equatable { ) } -public struct Preferences: Codable { +public struct Preferences: Codable, Hashable { public var timedMessages: TimedMessagesPreference? public var fullDelete: SimplePreference? public var reactions: SimplePreference? @@ -308,11 +321,11 @@ public func contactUserPreferencesToPreferences(_ contactUserPreferences: Contac ) } -public protocol Preference: Codable, Equatable { +public protocol Preference: Codable, Equatable, Hashable { var allow: FeatureAllowed { get set } } -public struct SimplePreference: Preference { +public struct SimplePreference: Preference, Hashable { public var allow: FeatureAllowed public init(allow: FeatureAllowed) { @@ -320,7 +333,7 @@ public struct SimplePreference: Preference { } } -public struct TimedMessagesPreference: Preference { +public struct TimedMessagesPreference: Preference, Hashable { public var allow: FeatureAllowed public var ttl: Int? @@ -334,7 +347,7 @@ public struct TimedMessagesPreference: Preference { } } -public enum CustomTimeUnit { +public enum CustomTimeUnit: Hashable { case second case minute case hour @@ -433,7 +446,7 @@ public func shortTimeText(_ seconds: Int?) -> LocalizedStringKey { return CustomTimeUnit.toShortText(seconds: seconds) } -public struct ContactUserPreferences: Decodable { +public struct ContactUserPreferences: Decodable, Hashable { public var timedMessages: ContactUserPreference<TimedMessagesPreference> public var fullDelete: ContactUserPreference<SimplePreference> public var reactions: ContactUserPreference<SimplePreference> @@ -483,7 +496,7 @@ public struct ContactUserPreferences: Decodable { ) } -public struct ContactUserPreference<P: Preference>: Decodable { +public struct ContactUserPreference<P: Preference>: Decodable, Hashable { public var enabled: FeatureEnabled public var userPreference: ContactUserPref<P> public var contactPreference: P @@ -495,7 +508,7 @@ public struct ContactUserPreference<P: Preference>: Decodable { } } -public struct FeatureEnabled: Decodable { +public struct FeatureEnabled: Decodable, Hashable { public var forUser: Bool public var forContact: Bool @@ -521,12 +534,12 @@ public struct FeatureEnabled: Decodable { : NSLocalizedString("off", comment: "enabled status") } - public var iconColor: Color { - forUser ? .green : forContact ? .yellow : .secondary + public func iconColor(_ secondaryColor: Color) -> Color { + forUser ? .green : forContact ? .yellow : secondaryColor } } -public enum ContactUserPref<P: Preference>: Decodable { +public enum ContactUserPref<P: Preference>: Decodable, Hashable { case contact(preference: P) // contact override is set case user(preference: P) // global user default is used @@ -543,10 +556,11 @@ public protocol Feature { var iconFilled: String { get } var iconScale: CGFloat { get } var hasParam: Bool { get } + var hasRole: Bool { get } var text: String { get } } -public enum ChatFeature: String, Decodable, Feature { +public enum ChatFeature: String, Decodable, Feature, Hashable { case timedMessages case fullDelete case reactions @@ -569,6 +583,8 @@ public enum ChatFeature: String, Decodable, Feature { } } + public var hasRole: Bool { false } + public var text: String { switch self { case .timedMessages: return NSLocalizedString("Disappearing messages", comment: "chat feature") @@ -616,8 +632,8 @@ public enum ChatFeature: String, Decodable, Feature { } case .fullDelete: switch allowed { - case .always: return "Allow your contacts to irreversibly delete sent messages." - case .yes: return "Allow irreversible message deletion only if your contact allows it to you." + case .always: return "Allow your contacts to irreversibly delete sent messages. (24 hours)" + case .yes: return "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" case .no: return "Contacts can mark messages for deletion; you will be able to view them." } case .reactions: @@ -653,11 +669,11 @@ public enum ChatFeature: String, Decodable, Feature { : "Disappearing messages are prohibited in this chat." case .fullDelete: return enabled.forUser && enabled.forContact - ? "Both you and your contact can irreversibly delete sent messages." + ? "Both you and your contact can irreversibly delete sent messages. (24 hours)" : enabled.forUser - ? "Only you can irreversibly delete messages (your contact can mark them for deletion)." + ? "Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" : enabled.forContact - ? "Only your contact can irreversibly delete messages (you can mark them for deletion)." + ? "Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" : "Irreversible message deletion is prohibited in this chat." case .reactions: return enabled.forUser && enabled.forContact @@ -687,13 +703,16 @@ public enum ChatFeature: String, Decodable, Feature { } } -public enum GroupFeature: String, Decodable, Feature { +public enum GroupFeature: String, Decodable, Feature, Hashable { case timedMessages case directMessages case fullDelete case reactions case voice case files + case simplexLinks + case reports + case history public var id: Self { self } @@ -704,6 +723,20 @@ public enum GroupFeature: String, Decodable, Feature { } } + public var hasRole: Bool { + switch self { + case .timedMessages: false + case .directMessages: true + case .fullDelete: false + case .reactions: false + case .voice: true + case .files: true + case .simplexLinks: true + case .reports: false + case .history: false + } + } + public var text: String { switch self { case .timedMessages: return NSLocalizedString("Disappearing messages", comment: "chat feature") @@ -712,6 +745,9 @@ public enum GroupFeature: String, Decodable, Feature { case .reactions: return NSLocalizedString("Message reactions", comment: "chat feature") case .voice: return NSLocalizedString("Voice messages", comment: "chat feature") case .files: return NSLocalizedString("Files and media", comment: "chat feature") + case .simplexLinks: return NSLocalizedString("SimpleX links", comment: "chat feature") + case .reports: return NSLocalizedString("Member reports", comment: "chat feature") + case .history: return NSLocalizedString("Visible history", comment: "chat feature") } } @@ -723,6 +759,9 @@ public enum GroupFeature: String, Decodable, Feature { case .reactions: return "face.smiling" case .voice: return "mic" case .files: return "doc" + case .simplexLinks: return "link.circle" + case .reports: return "flag" + case .history: return "clock" } } @@ -734,6 +773,9 @@ public enum GroupFeature: String, Decodable, Feature { case .reactions: return "face.smiling.fill" case .voice: return "mic.fill" case .files: return "doc.fill" + case .simplexLinks: return "link.circle.fill" + case .reports: return "flag.fill" + case .history: return "clock.fill" } } @@ -759,7 +801,7 @@ public enum GroupFeature: String, Decodable, Feature { } case .fullDelete: switch enabled { - case .on: return "Allow to irreversibly delete sent messages." + case .on: return "Allow to irreversibly delete sent messages. (24 hours)" case .off: return "Prohibit irreversible message deletion." } case .reactions: @@ -777,38 +819,68 @@ public enum GroupFeature: String, Decodable, Feature { case .on: return "Allow to send files and media." case .off: return "Prohibit sending files and media." } + case .simplexLinks: + switch enabled { + case .on: return "Allow to send SimpleX links." + case .off: return "Prohibit sending SimpleX links." + } + case .reports: + switch enabled { + case .on: return "Allow to report messsages to moderators." + case .off: return "Prohibit reporting messages to moderators." + } + 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." + } } } else { switch self { case .timedMessages: switch enabled { - case .on: return "Group members can send disappearing messages." - case .off: return "Disappearing messages are prohibited in this group." + case .on: return "Members can send disappearing messages." + case .off: return "Disappearing messages are prohibited." } case .directMessages: switch enabled { - case .on: return "Group members can send direct messages." - case .off: return "Direct messages between members are prohibited in this group." + case .on: return "Members can send direct messages." + case .off: return "Direct messages between members are prohibited." } case .fullDelete: switch enabled { - case .on: return "Group members can irreversibly delete sent messages." - case .off: return "Irreversible message deletion is prohibited in this group." + case .on: return "Members can irreversibly delete sent messages. (24 hours)" + case .off: return "Irreversible message deletion is prohibited." } case .reactions: switch enabled { - case .on: return "Group members can add message reactions." - case .off: return "Message reactions are prohibited in this group." + case .on: return "Members can add message reactions." + case .off: return "Message reactions are prohibited." } case .voice: switch enabled { - case .on: return "Group members can send voice messages." - case .off: return "Voice messages are prohibited in this group." + case .on: return "Members can send voice messages." + case .off: return "Voice messages are prohibited." } case .files: switch enabled { - case .on: return "Group members can send files and media." - case .off: return "Files and media are prohibited in this group." + case .on: return "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 .off: return "SimpleX links are prohibited." + } + case .reports: + switch enabled { + case .on: return "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." } } } @@ -846,7 +918,7 @@ public enum ContactFeatureAllowed: Identifiable, Hashable { } } -public struct ContactFeaturesAllowed: Equatable { +public struct ContactFeaturesAllowed: Equatable, Hashable { public var timedMessagesAllowed: Bool public var timedMessagesTTL: Int? public var fullDelete: ContactFeatureAllowed @@ -924,7 +996,7 @@ public func contactFeatureAllowedToPref(_ contactFeatureAllowed: ContactFeatureA } } -public enum FeatureAllowed: String, Codable, Identifiable { +public enum FeatureAllowed: String, Codable, Identifiable, Hashable { case always case yes case no @@ -942,21 +1014,27 @@ public enum FeatureAllowed: String, Codable, Identifiable { } } -public struct FullGroupPreferences: Decodable, Equatable { +public struct FullGroupPreferences: Decodable, Equatable, Hashable { public var timedMessages: TimedMessagesGroupPreference - public var directMessages: GroupPreference + public var directMessages: RoleGroupPreference public var fullDelete: GroupPreference public var reactions: GroupPreference - public var voice: GroupPreference - public var files: GroupPreference + public var voice: RoleGroupPreference + public var files: RoleGroupPreference + public var simplexLinks: RoleGroupPreference + public var reports: GroupPreference + public var history: GroupPreference public init( timedMessages: TimedMessagesGroupPreference, - directMessages: GroupPreference, + directMessages: RoleGroupPreference, fullDelete: GroupPreference, reactions: GroupPreference, - voice: GroupPreference, - files: GroupPreference + voice: RoleGroupPreference, + files: RoleGroupPreference, + simplexLinks: RoleGroupPreference, + reports: GroupPreference, + history: GroupPreference ) { self.timedMessages = timedMessages self.directMessages = directMessages @@ -964,33 +1042,45 @@ public struct FullGroupPreferences: Decodable, Equatable { self.reactions = reactions self.voice = voice self.files = files + self.simplexLinks = simplexLinks + self.reports = reports + self.history = history } public static let sampleData = FullGroupPreferences( timedMessages: TimedMessagesGroupPreference(enable: .off), - directMessages: GroupPreference(enable: .off), + directMessages: RoleGroupPreference(enable: .off, role: nil), fullDelete: GroupPreference(enable: .off), reactions: GroupPreference(enable: .on), - voice: GroupPreference(enable: .on), - files: GroupPreference(enable: .on) + voice: RoleGroupPreference(enable: .on, role: nil), + files: RoleGroupPreference(enable: .on, role: nil), + simplexLinks: RoleGroupPreference(enable: .on, role: nil), + reports: GroupPreference(enable: .on), + history: GroupPreference(enable: .on) ) } -public struct GroupPreferences: Codable { +public struct GroupPreferences: Codable, Hashable { public var timedMessages: TimedMessagesGroupPreference? - public var directMessages: GroupPreference? + public var directMessages: RoleGroupPreference? public var fullDelete: GroupPreference? public var reactions: GroupPreference? - public var voice: GroupPreference? - public var files: GroupPreference? + public var voice: RoleGroupPreference? + public var files: RoleGroupPreference? + public var simplexLinks: RoleGroupPreference? + public var reports: GroupPreference? + public var history: GroupPreference? public init( - timedMessages: TimedMessagesGroupPreference?, - directMessages: GroupPreference?, - fullDelete: GroupPreference?, - reactions: GroupPreference?, - voice: GroupPreference?, - files: GroupPreference? + timedMessages: TimedMessagesGroupPreference? = nil, + directMessages: RoleGroupPreference? = nil, + fullDelete: GroupPreference? = nil, + reactions: GroupPreference? = nil, + voice: RoleGroupPreference? = nil, + files: RoleGroupPreference? = nil, + simplexLinks: RoleGroupPreference? = nil, + reports: GroupPreference? = nil, + history: GroupPreference? = nil ) { self.timedMessages = timedMessages self.directMessages = directMessages @@ -998,15 +1088,21 @@ public struct GroupPreferences: Codable { self.reactions = reactions self.voice = voice self.files = files + self.simplexLinks = simplexLinks + self.reports = reports + self.history = history } public static let sampleData = GroupPreferences( timedMessages: TimedMessagesGroupPreference(enable: .off), - directMessages: GroupPreference(enable: .off), + directMessages: RoleGroupPreference(enable: .off, role: nil), fullDelete: GroupPreference(enable: .off), reactions: GroupPreference(enable: .on), - voice: GroupPreference(enable: .on), - files: GroupPreference(enable: .on) + voice: RoleGroupPreference(enable: .on, role: nil), + files: RoleGroupPreference(enable: .on, role: nil), + simplexLinks: RoleGroupPreference(enable: .on, role: nil), + reports: GroupPreference(enable: .on), + history: GroupPreference(enable: .on) ) } @@ -1017,23 +1113,52 @@ public func toGroupPreferences(_ fullPreferences: FullGroupPreferences) -> Group fullDelete: fullPreferences.fullDelete, reactions: fullPreferences.reactions, voice: fullPreferences.voice, - files: fullPreferences.files + files: fullPreferences.files, + simplexLinks: fullPreferences.simplexLinks, + reports: fullPreferences.reports, + history: fullPreferences.history ) } -public struct GroupPreference: Codable, Equatable { +public struct GroupPreference: Codable, Equatable, Hashable { public var enable: GroupFeatureEnabled public var on: Bool { enable == .on } + public func enabled(_ role: GroupMemberRole?, for m: GroupMember?) -> GroupFeatureEnabled { + switch enable { + case .off: .off + case .on: + if let role, let m { + m.memberRole >= role ? .on : .off + } else { + .on + } + } + } + public init(enable: GroupFeatureEnabled) { self.enable = enable } } -public struct TimedMessagesGroupPreference: Codable, Equatable { +public struct RoleGroupPreference: Codable, Equatable, Hashable { + public var enable: GroupFeatureEnabled + public var role: GroupMemberRole? + + public func on(for m: GroupMember) -> Bool { + enable == .on && m.memberRole >= (role ?? .observer) + } + + public init(enable: GroupFeatureEnabled, role: GroupMemberRole?) { + self.enable = enable + self.role = role + } +} + +public struct TimedMessagesGroupPreference: Codable, Equatable, Hashable { public var enable: GroupFeatureEnabled public var ttl: Int? @@ -1047,7 +1172,7 @@ public struct TimedMessagesGroupPreference: Codable, Equatable { } } -public enum GroupFeatureEnabled: String, Codable, Identifiable { +public enum GroupFeatureEnabled: String, Codable, Identifiable, Hashable { case on case off @@ -1062,28 +1187,32 @@ public enum GroupFeatureEnabled: String, Codable, Identifiable { } } - public var iconColor: Color { + public func iconColor(_ secondaryColor: Color) -> Color { switch self { case .on: return .green - case .off: return .secondary + case .off: return secondaryColor } } } -public enum ChatInfo: Identifiable, Decodable, NamedChat { +public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { case direct(contact: Contact) case group(groupInfo: GroupInfo) + case local(noteFolder: NoteFolder) case contactRequest(contactRequest: UserContactRequest) case contactConnection(contactConnection: PendingContactConnection) - case invalidJSON(json: String) + case invalidJSON(json: Data?) private static let invalidChatName = NSLocalizedString("invalid chat", comment: "invalid chat data") + static let privateNotesChatName = NSLocalizedString("Private notes", comment: "name of notes to self") + public var localDisplayName: String { get { switch self { case let .direct(contact): return contact.localDisplayName case let .group(groupInfo): return groupInfo.localDisplayName + case .local: return "" case let .contactRequest(contactRequest): return contactRequest.localDisplayName case let .contactConnection(contactConnection): return contactConnection.localDisplayName case .invalidJSON: return ChatInfo.invalidChatName @@ -1096,6 +1225,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.displayName case let .group(groupInfo): return groupInfo.displayName + case .local: return ChatInfo.privateNotesChatName case let .contactRequest(contactRequest): return contactRequest.displayName case let .contactConnection(contactConnection): return contactConnection.displayName case .invalidJSON: return ChatInfo.invalidChatName @@ -1108,6 +1238,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.fullName case let .group(groupInfo): return groupInfo.fullName + case .local: return "" case let .contactRequest(contactRequest): return contactRequest.fullName case let .contactConnection(contactConnection): return contactConnection.fullName case .invalidJSON: return ChatInfo.invalidChatName @@ -1120,6 +1251,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.image case let .group(groupInfo): return groupInfo.image + case .local: return nil case let .contactRequest(contactRequest): return contactRequest.image case let .contactConnection(contactConnection): return contactConnection.image case .invalidJSON: return nil @@ -1132,6 +1264,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.localAlias case let .group(groupInfo): return groupInfo.localAlias + case .local: return "" case let .contactRequest(contactRequest): return contactRequest.localAlias case let .contactConnection(contactConnection): return contactConnection.localAlias case .invalidJSON: return "" @@ -1144,6 +1277,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.id case let .group(groupInfo): return groupInfo.id + case let .local(noteFolder): return noteFolder.id case let .contactRequest(contactRequest): return contactRequest.id case let .contactConnection(contactConnection): return contactConnection.id case .invalidJSON: return "" @@ -1156,6 +1290,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case .direct: return .direct case .group: return .group + case .local: return .local case .contactRequest: return .contactRequest case .contactConnection: return .contactConnection case .invalidJSON: return .direct @@ -1168,6 +1303,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.apiId case let .group(groupInfo): return groupInfo.apiId + case let .local(noteFolder): return noteFolder.apiId case let .contactRequest(contactRequest): return contactRequest.apiId case let .contactConnection(contactConnection): return contactConnection.apiId case .invalidJSON: return 0 @@ -1180,18 +1316,42 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.ready case let .group(groupInfo): return groupInfo.ready + case let .local(noteFolder): return noteFolder.ready case let .contactRequest(contactRequest): return contactRequest.ready case let .contactConnection(contactConnection): return contactConnection.ready case .invalidJSON: return false } } } + + public var chatDeleted: Bool { + get { + switch self { + case let .direct(contact): return contact.chatDeleted + default: return false + } + } + } + + public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { + get { + switch self { + case let .direct(contact): return contact.userCantSendReason + case let .group(groupInfo): return groupInfo.userCantSendReason + case let .local(noteFolder): return noteFolder.userCantSendReason + case let .contactRequest(contactRequest): return contactRequest.userCantSendReason + case let .contactConnection(contactConnection): return contactConnection.userCantSendReason + case .invalidJSON: return ("can't send messages", nil) + } + } + } public var sendMsgEnabled: Bool { get { switch self { case let .direct(contact): return contact.sendMsgEnabled case let .group(groupInfo): return groupInfo.sendMsgEnabled + case let .local(noteFolder): return noteFolder.sendMsgEnabled case let .contactRequest(contactRequest): return contactRequest.sendMsgEnabled case let .contactConnection(contactConnection): return contactConnection.sendMsgEnabled case .invalidJSON: return false @@ -1204,6 +1364,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.contactConnIncognito case let .group(groupInfo): return groupInfo.membership.memberIncognito + case .local: return false case .contactRequest: return false case let .contactConnection(contactConnection): return contactConnection.incognito case .invalidJSON: return false @@ -1218,6 +1379,13 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { } } + public var contactCard: Bool { + switch self { + case let .direct(contact): contact.activeConn == nil && contact.profile.contactLink != nil && contact.active + default: false + } + } + public var groupInfo: GroupInfo? { switch self { case let .group(groupInfo): return groupInfo @@ -1243,9 +1411,14 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { case .timedMessages: return prefs.timedMessages.on case .fullDelete: return prefs.fullDelete.on case .reactions: return prefs.reactions.on - case .voice: return prefs.voice.on + case .voice: return prefs.voice.on(for: groupInfo.membership) case .calls: return false } + case .local: + switch feature { + case .voice: return true + default: return false + } default: return false } } @@ -1263,7 +1436,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { } } - public enum ShowEnableVoiceMessagesAlert { + public enum ShowEnableVoiceMessagesAlert: Hashable { case userEnable case askContact case groupOwnerCan @@ -1281,7 +1454,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { return .other } case let .group(groupInfo): - if !groupInfo.fullGroupPreferences.voice.on { + if !groupInfo.fullGroupPreferences.voice.on(for: groupInfo.membership) { return .groupOwnerCan } else { return .other @@ -1291,8 +1464,37 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { } } - public var ntfsEnabled: Bool { - self.chatSettings?.enableNtfs == .all + public enum ShowEnableCallsAlert: Hashable { + case userEnable + case askContact + case other + } + + public var showEnableCallsAlert: ShowEnableCallsAlert { + switch self { + case let .direct(contact): + if contact.mergedPreferences.calls.userPreference.preference.allow == .no { + return .userEnable + } else if contact.mergedPreferences.calls.contactPreference.allow == .no { + return .askContact + } else { + return .other + } + default: + return .other + } + } + + public func ntfsEnabled(chatItem: ChatItem) -> Bool { + ntfsEnabled(chatItem.meta.userMention) + } + + public func ntfsEnabled(_ userMention: Bool) -> Bool { + switch self.chatSettings?.enableNtfs { + case .all: true + case .mentions: userMention + default: false + } } public var chatSettings: ChatSettings? { @@ -1302,11 +1504,28 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { default: return nil } } + + public var nextNtfMode: MsgFilter? { + self.chatSettings?.enableNtfs.nextMode(mentions: hasMentions) + } + + public var hasMentions: Bool { + if case .group = self { true } else { false } + } + + public var chatTags: [Int64]? { + switch self { + case let .direct(contact): return contact.chatTags + case let .group(groupInfo): return groupInfo.chatTags + default: return nil + } + } var createdAt: Date { switch self { case let .direct(contact): return contact.createdAt case let .group(groupInfo): return groupInfo.createdAt + case let .local(noteFolder): return noteFolder.createdAt case let .contactRequest(contactRequest): return contactRequest.createdAt case let .contactConnection(contactConnection): return contactConnection.createdAt case .invalidJSON: return .now @@ -1317,15 +1536,46 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.updatedAt case let .group(groupInfo): return groupInfo.updatedAt + case let .local(noteFolder): return noteFolder.updatedAt case let .contactRequest(contactRequest): return contactRequest.updatedAt case let .contactConnection(contactConnection): return contactConnection.updatedAt case .invalidJSON: return .now } } - public struct SampleData { + public var chatTs: Date { + switch self { + case let .direct(contact): return contact.chatTs ?? contact.updatedAt + case let .group(groupInfo): return groupInfo.chatTs ?? groupInfo.updatedAt + case let .local(noteFolder): return noteFolder.chatTs + case let .contactRequest(contactRequest): return contactRequest.updatedAt + case let .contactConnection(contactConnection): return contactConnection.updatedAt + case .invalidJSON: return .now + } + } + + public func ttl(_ globalTTL: ChatItemTTL) -> ChatTTL { + switch self { + case let .direct(contact): + return if let ciTTL = contact.chatItemTTL { + ChatTTL.chat(ChatItemTTL(ciTTL)) + } else { + ChatTTL.userDefault(globalTTL) + } + case let .group(groupInfo): + return if let ciTTL = groupInfo.chatItemTTL { + ChatTTL.chat(ChatItemTTL(ciTTL)) + } else { + ChatTTL.userDefault(globalTTL) + } + default: return ChatTTL.userDefault(globalTTL) + } + } + + public struct SampleData: Hashable { public var direct: ChatInfo public var group: ChatInfo + public var local: ChatInfo public var contactRequest: ChatInfo public var contactConnection: ChatInfo } @@ -1333,19 +1583,26 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { public static var sampleData: ChatInfo.SampleData = SampleData( direct: ChatInfo.direct(contact: Contact.sampleData), group: ChatInfo.group(groupInfo: GroupInfo.sampleData), + local: ChatInfo.local(noteFolder: NoteFolder.sampleData), contactRequest: ChatInfo.contactRequest(contactRequest: UserContactRequest.sampleData), contactConnection: ChatInfo.contactConnection(contactConnection: PendingContactConnection.getSampleData()) ) } -public struct ChatData: Decodable, Identifiable { +public struct ChatData: Decodable, Identifiable, Hashable, ChatLike { public var chatInfo: ChatInfo public var chatItems: [ChatItem] public var chatStats: ChatStats public var id: ChatId { get { chatInfo.id } } - public static func invalidJSON(_ json: String) -> ChatData { + public init(chatInfo: ChatInfo, chatItems: [ChatItem], chatStats: ChatStats = ChatStats()) { + self.chatInfo = chatInfo + self.chatItems = chatItems + self.chatStats = chatStats + } + + public static func invalidJSON(_ json: Data?) -> ChatData { ChatData( chatInfo: .invalidJSON(json: json), chatItems: [], @@ -1354,19 +1611,25 @@ public struct ChatData: Decodable, Identifiable { } } -public struct ChatStats: Decodable { - public init(unreadCount: Int = 0, minUnreadItemId: Int64 = 0, unreadChat: Bool = false) { +public struct ChatStats: Decodable, Hashable { + public init(unreadCount: Int = 0, unreadMentions: Int = 0, reportsCount: Int = 0, minUnreadItemId: Int64 = 0, unreadChat: Bool = false) { self.unreadCount = unreadCount + self.unreadMentions = unreadMentions + self.reportsCount = reportsCount self.minUnreadItemId = minUnreadItemId self.unreadChat = unreadChat } public var unreadCount: Int = 0 + public var unreadMentions: Int = 0 + // actual only via getChats() and getChat(.initial), otherwise, zero + public var reportsCount: Int = 0 public var minUnreadItemId: Int64 = 0 + // actual only via getChats(), otherwise, false public var unreadChat: Bool = false } -public struct Contact: Identifiable, Decodable, NamedChat { +public struct Contact: Identifiable, Decodable, NamedChat, Hashable { public var contactId: Int64 var localDisplayName: ContactName public var profile: LocalProfile @@ -1379,17 +1642,29 @@ public struct Contact: Identifiable, Decodable, NamedChat { public var mergedPreferences: ContactUserPreferences var createdAt: Date var updatedAt: Date + var chatTs: Date? var contactGroupMemberId: Int64? var contactGrpInvSent: Bool - + public var chatTags: [Int64] + public var chatItemTTL: Int64? + public var uiThemes: ThemeModeOverrides? + public var chatDeleted: Bool + public var id: ChatId { get { "@\(contactId)" } } public var apiId: Int64 { get { contactId } } public var ready: Bool { get { activeConn?.connStatus == .ready } } + public var sndReady: Bool { get { ready || activeConn?.connStatus == .sndReady } } public var active: Bool { get { contactStatus == .active } } - public var sendMsgEnabled: Bool { get { - (ready && active && !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false)) - || nextSendGrpInv - } } + public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { + // TODO [short links] this will have additional statuses for pending contact requests before they are accepted + if nextSendGrpInv { return nil } + if !active { return ("contact deleted", nil) } + if !sndReady { return ("contact not ready", nil) } + if activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false { return ("not synchronized", nil) } + if activeConn?.connDisabled ?? true { return ("contact disabled", nil) } + return nil + } + public var sendMsgEnabled: Bool { userCantSendReason == nil } public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } } public var displayName: String { localAlias == "" ? profile.displayName : localAlias } public var fullName: String { get { profile.fullName } } @@ -1442,16 +1717,19 @@ public struct Contact: Identifiable, Decodable, NamedChat { mergedPreferences: ContactUserPreferences.sampleData, createdAt: .now, updatedAt: .now, - contactGrpInvSent: false + contactGrpInvSent: false, + chatTags: [], + chatDeleted: false ) } -public enum ContactStatus: String, Decodable { +public enum ContactStatus: String, Decodable, Hashable { case active = "active" case deleted = "deleted" + case deletedByUser = "deletedByUser" } -public struct ContactRef: Decodable, Equatable { +public struct ContactRef: Decodable, Equatable, Hashable { var contactId: Int64 public var agentConnId: String var connId: Int64 @@ -1460,12 +1738,12 @@ public struct ContactRef: Decodable, Equatable { public var id: ChatId { get { "@\(contactId)" } } } -public struct ContactSubStatus: Decodable { +public struct ContactSubStatus: Decodable, Hashable { public var contact: Contact public var contactError: ChatError? } -public struct Connection: Decodable { +public struct Connection: Decodable, Hashable { public var connId: Int64 public var agentConnId: String public var peerChatVRange: VersionRange @@ -1474,40 +1752,58 @@ public struct Connection: Decodable { public var viaGroupLink: Bool public var customUserProfileId: Int64? public var connectionCode: SecurityCode? + public var pqSupport: Bool + public var pqEncryption: Bool + public var pqSndEnabled: Bool? + public var pqRcvEnabled: Bool? + public var authErrCounter: Int + public var quotaErrCounter: Int public var connectionStats: ConnectionStats? = nil private enum CodingKeys: String, CodingKey { - case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode + case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, quotaErrCounter } public var id: ChatId { get { ":\(connId)" } } + public var connDisabled: Bool { + authErrCounter >= 10 // authErrDisableCount in core + } + + public var connInactive: Bool { + quotaErrCounter >= 5 // quotaErrInactiveCount in core + } + + public var connPQEnabled: Bool { + pqSndEnabled == true && pqRcvEnabled == true + } + static let sampleData = Connection( connId: 1, agentConnId: "abc", - peerChatVRange: VersionRange(minVersion: 1, maxVersion: 1), + peerChatVRange: VersionRange(1, 1), connStatus: .ready, connLevel: 0, - viaGroupLink: false + viaGroupLink: false, + pqSupport: false, + pqEncryption: false, + authErrCounter: 0, + quotaErrCounter: 0 ) } -public struct VersionRange: Decodable { - public init(minVersion: Int, maxVersion: Int) { +public struct VersionRange: Decodable, Hashable { + public init(_ minVersion: Int, _ maxVersion: Int) { self.minVersion = minVersion self.maxVersion = maxVersion } public var minVersion: Int public var maxVersion: Int - - public func isCompatibleRange(_ vRange: VersionRange) -> Bool { - self.minVersion <= vRange.maxVersion && vRange.minVersion <= self.maxVersion - } } -public struct SecurityCode: Decodable, Equatable { +public struct SecurityCode: Decodable, Equatable, Hashable { public init(securityCode: String, verifiedAt: Date) { self.securityCode = securityCode self.verifiedAt = verifiedAt @@ -1517,7 +1813,7 @@ public struct SecurityCode: Decodable, Equatable { public var verifiedAt: Date } -public struct UserContact: Decodable { +public struct UserContact: Decodable, Hashable { public var userContactLinkId: Int64 // public var connReqContact: String public var groupId: Int64? @@ -1535,7 +1831,7 @@ public struct UserContact: Decodable { } } -public struct UserContactRequest: Decodable, NamedChat { +public struct UserContactRequest: Decodable, NamedChat, Hashable { var contactRequestId: Int64 public var userContactLinkId: Int64 public var cReqChatVRange: VersionRange @@ -1547,6 +1843,7 @@ public struct UserContactRequest: Decodable, NamedChat { public var id: ChatId { get { "<@\(contactRequestId)" } } public var apiId: Int64 { get { contactRequestId } } var ready: Bool { get { true } } + public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { ("can't send messages", nil) } public var sendMsgEnabled: Bool { get { false } } public var displayName: String { get { profile.displayName } } public var fullName: String { get { profile.fullName } } @@ -1556,7 +1853,7 @@ public struct UserContactRequest: Decodable, NamedChat { public static let sampleData = UserContactRequest( contactRequestId: 1, userContactLinkId: 1, - cReqChatVRange: VersionRange(minVersion: 1, maxVersion: 1), + cReqChatVRange: VersionRange(1, 1), localDisplayName: "alice", profile: Profile.sampleData, createdAt: .now, @@ -1564,14 +1861,14 @@ public struct UserContactRequest: Decodable, NamedChat { ) } -public struct PendingContactConnection: Decodable, NamedChat { +public struct PendingContactConnection: Decodable, NamedChat, Hashable { public var pccConnId: Int64 var pccAgentConnId: String var pccConnStatus: ConnStatus public var viaContactUri: Bool public var groupLinkId: String? public var customUserProfileId: Int64? - public var connReqInv: String? + public var connLinkInv: CreatedConnLink? public var localAlias: String var createdAt: Date public var updatedAt: Date @@ -1579,6 +1876,7 @@ public struct PendingContactConnection: Decodable, NamedChat { public var id: ChatId { get { ":\(pccConnId)" } } public var apiId: Int64 { get { pccConnId } } var ready: Bool { get { false } } + public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { ("can't send messages", nil) } public var sendMsgEnabled: Bool { get { false } } var localDisplayName: String { get { String.localizedStringWithFormat(NSLocalizedString("connection:%@", comment: "connection information"), pccConnId) } @@ -1586,9 +1884,11 @@ public struct PendingContactConnection: Decodable, NamedChat { public var displayName: String { get { if let initiated = pccConnStatus.initiated { - return initiated && !viaContactUri + return viaContactUri + ? NSLocalizedString("requested to connect", comment: "chat list item title") + : initiated ? NSLocalizedString("invited to connect", comment: "chat list item title") - : NSLocalizedString("connecting…", comment: "chat list item title") + : NSLocalizedString("accepted invitation", comment: "chat list item title") } else { // this should not be in the list return NSLocalizedString("connection established", comment: "chat list item title (it should not be shown") @@ -1654,8 +1954,9 @@ public struct PendingContactConnection: Decodable, NamedChat { } } -public enum ConnStatus: String, Decodable { +public enum ConnStatus: String, Decodable, Hashable { case new = "new" + case prepared = "prepared" case joined = "joined" case requested = "requested" case accepted = "accepted" @@ -1667,10 +1968,11 @@ public enum ConnStatus: String, Decodable { get { switch self { case .new: return true + case .prepared: return false case .joined: return false case .requested: return true case .accepted: return true - case .sndReady: return false + case .sndReady: return nil case .ready: return nil case .deleted: return nil } @@ -1678,7 +1980,7 @@ public enum ConnStatus: String, Decodable { } } -public struct Group: Decodable { +public struct Group: Decodable, Hashable { public var groupInfo: GroupInfo public var members: [GroupMember] @@ -1688,27 +1990,44 @@ public struct Group: Decodable { } } -public struct GroupInfo: Identifiable, Decodable, NamedChat { +public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { public var groupId: Int64 var localDisplayName: GroupName public var groupProfile: GroupProfile + public var businessChat: BusinessChatInfo? public var fullGroupPreferences: FullGroupPreferences public var membership: GroupMember - public var hostConnCustomUserProfileId: Int64? public var chatSettings: ChatSettings var createdAt: Date var updatedAt: Date + var chatTs: Date? + public var uiThemes: ThemeModeOverrides? public var id: ChatId { get { "#\(groupId)" } } public var apiId: Int64 { get { groupId } } public var ready: Bool { get { true } } - public var sendMsgEnabled: Bool { get { membership.memberActive } } - public var displayName: String { get { groupProfile.displayName } } + public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { + return if membership.memberActive { + membership.memberRole == .observer ? ("you are observer", "Please contact group admin.") : nil + } else { + switch membership.memberStatus { + case .memRejected: ("request to join rejected", nil) + case .memGroupDeleted: ("group is deleted", nil) + case .memRemoved: ("removed from group", nil) + case .memLeft: ("you left", nil) + default: ("can't send messages", nil) + } + } + } + public var sendMsgEnabled: Bool { userCantSendReason == nil } + public var displayName: String { localAlias == "" ? groupProfile.displayName : localAlias } public var fullName: String { get { groupProfile.fullName } } public var image: String? { get { groupProfile.image } } - public var localAlias: String { "" } + public var chatTags: [Int64] + public var chatItemTTL: Int64? + public var localAlias: String - public var canEdit: Bool { + public var isOwner: Bool { return membership.memberRole == .owner && membership.memberCurrent } @@ -1726,19 +2045,20 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat { groupProfile: GroupProfile.sampleData, fullGroupPreferences: FullGroupPreferences.sampleData, membership: GroupMember.sampleData, - hostConnCustomUserProfileId: nil, chatSettings: ChatSettings.defaults, createdAt: .now, - updatedAt: .now + updatedAt: .now, + chatTags: [], + localAlias: "" ) } -public struct GroupRef: Decodable { +public struct GroupRef: Decodable, Hashable { public var groupId: Int64 var localDisplayName: GroupName } -public struct GroupProfile: Codable, NamedChat { +public struct GroupProfile: Codable, NamedChat, Hashable { public init(displayName: String, fullName: String, description: String? = nil, image: String? = nil, groupPreferences: GroupPreferences? = nil) { self.displayName = displayName self.fullName = fullName @@ -1760,7 +2080,18 @@ public struct GroupProfile: Codable, NamedChat { ) } -public struct GroupMember: Identifiable, Decodable { +public struct BusinessChatInfo: Decodable, Hashable { + public var chatType: BusinessChatType + public var businessId: String + public var customerId: String +} + +public enum BusinessChatType: String, Codable, Hashable { + case business + case customer +} + +public struct GroupMember: Identifiable, Decodable, Hashable { public var groupMemberId: Int64 public var groupId: Int64 public var memberId: String @@ -1768,24 +2099,36 @@ public struct GroupMember: Identifiable, Decodable { public var memberCategory: GroupMemberCategory public var memberStatus: GroupMemberStatus public var memberSettings: GroupMemberSettings + public var blockedByAdmin: Bool public var invitedBy: InvitedBy public var localDisplayName: ContactName public var memberProfile: LocalProfile public var memberContactId: Int64? public var memberContactProfileId: Int64 public var activeConn: Connection? + public var memberChatVRange: VersionRange public var id: String { "#\(groupId) @\(groupMemberId)" } + public var ready: Bool { get { activeConn?.connStatus == .ready } } + public var sndReady: Bool { get { ready || activeConn?.connStatus == .sndReady } } + public var sendMsgEnabled: Bool { get { + sndReady + && memberCurrent + && !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false) + && !(activeConn?.connDisabled ?? true) + } } public var displayName: String { get { let p = memberProfile - return p.localAlias == "" ? p.displayName : p.localAlias + let name = p.localAlias == "" ? p.displayName : p.localAlias + return pastMember(name) } } public var fullName: String { get { memberProfile.fullName } } public var image: String? { get { memberProfile.image } } public var contactLink: String? { get { memberProfile.contactLink } } public var verified: Bool { activeConn?.connectionCode != nil } + public var blocked: Bool { blockedByAdmin || !memberSettings.showMessages } var directChatId: ChatId? { get { @@ -1800,18 +2143,40 @@ public struct GroupMember: Identifiable, Decodable { public var chatViewName: String { get { let p = memberProfile - return p.localAlias == "" - ? p.displayName + (p.fullName == "" || p.fullName == p.displayName ? "" : " / \(p.fullName)") - : p.localAlias + let name = ( + p.localAlias == "" + ? p.displayName + (p.fullName == "" || p.fullName == p.displayName ? "" : " / \(p.fullName)") + : p.localAlias + ) + return pastMember(name) + } + } + + private func pastMember(_ name: String) -> String { + memberStatus == .memUnknown + ? String.localizedStringWithFormat(NSLocalizedString("Past member %@", comment: "past/unknown group member"), name) + : name + } + + public var localAliasAndFullName: String { + get { + let p = memberProfile + let fullName = p.displayName + (p.fullName == "" || p.fullName == p.displayName ? "" : " / \(p.fullName)") + let name = p.localAlias == "" ? fullName : "\(p.localAlias) (\(fullName))" + + return pastMember(name) } } public var memberActive: Bool { switch memberStatus { + case .memRejected: return false case .memRemoved: return false case .memLeft: return false case .memGroupDeleted: return false + case .memUnknown: return false case .memInvited: return false + case .memPendingApproval: return true case .memIntroduced: return false case .memIntroInvited: return false case .memAccepted: return false @@ -1824,10 +2189,13 @@ public struct GroupMember: Identifiable, Decodable { public var memberCurrent: Bool { switch memberStatus { + case .memRejected: return false case .memRemoved: return false case .memLeft: return false case .memGroupDeleted: return false + case .memUnknown: return false case .memInvited: return false + case .memPendingApproval: return false case .memIntroduced: return true case .memIntroInvited: return true case .memAccepted: return true @@ -1841,15 +2209,33 @@ public struct GroupMember: Identifiable, Decodable { public func canBeRemoved(groupInfo: GroupInfo) -> Bool { let userRole = groupInfo.membership.memberRole return memberStatus != .memRemoved && memberStatus != .memLeft - && userRole >= .admin && userRole >= memberRole && groupInfo.membership.memberCurrent + && userRole >= .admin && userRole >= memberRole && groupInfo.membership.memberActive } public func canChangeRoleTo(groupInfo: GroupInfo) -> [GroupMemberRole]? { if !canBeRemoved(groupInfo: groupInfo) { return nil } let userRole = groupInfo.membership.memberRole - return GroupMemberRole.allCases.filter { $0 <= userRole && $0 != .author } + return GroupMemberRole.supportedRoles.filter { $0 <= userRole } } + public func canBlockForAll(groupInfo: GroupInfo) -> Bool { + let userRole = groupInfo.membership.memberRole + return memberStatus != .memRemoved && memberStatus != .memLeft && memberRole < .admin + && userRole >= .admin && userRole >= memberRole && groupInfo.membership.memberActive + } + + public var canReceiveReports: Bool { + memberRole >= .moderator && versionRange.maxVersion >= REPORTS_VERSION + } + + public var versionRange: VersionRange { + if let activeConn { + activeConn.peerChatVRange + } else { + memberChatVRange + } + } + public var memberIncognito: Bool { memberProfile.profileId != memberContactProfileId } @@ -1862,43 +2248,49 @@ public struct GroupMember: Identifiable, Decodable { memberCategory: .inviteeMember, memberStatus: .memComplete, memberSettings: GroupMemberSettings(showMessages: true), + blockedByAdmin: false, invitedBy: .user, localDisplayName: "alice", memberProfile: LocalProfile.sampleData, memberContactId: 1, memberContactProfileId: 1, - activeConn: Connection.sampleData + activeConn: Connection.sampleData, + memberChatVRange: VersionRange(2, 12) ) } -public struct GroupMemberSettings: Codable { +public struct GroupMemberSettings: Codable, Hashable { public var showMessages: Bool } -public struct GroupMemberRef: Decodable { +public struct GroupMemberRef: Decodable, Hashable { var groupMemberId: Int64 var profile: Profile } -public struct GroupMemberIds: Decodable { +public struct GroupMemberIds: Decodable, Hashable { var groupMemberId: Int64 var groupId: Int64 } -public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable { - case observer = "observer" - case author = "author" - case member = "member" - case admin = "admin" - case owner = "owner" +public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Codable, Hashable { + case observer + case author + case member + case moderator + case admin + case owner public var id: Self { self } + public static var supportedRoles: [GroupMemberRole] = [.observer, .member, .admin, .owner] + public var text: String { switch self { case .observer: return NSLocalizedString("observer", comment: "member role") case .author: return NSLocalizedString("author", comment: "member role") case .member: return NSLocalizedString("member", comment: "member role") + case .moderator: return NSLocalizedString("moderator", comment: "member role") case .admin: return NSLocalizedString("admin", comment: "member role") case .owner: return NSLocalizedString("owner", comment: "member role") } @@ -1906,11 +2298,12 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Dec private var comparisonValue: Int { switch self { - case .observer: return 0 - case .author: return 1 - case .member: return 2 - case .admin: return 3 - case .owner: return 4 + case .observer: 0 + case .author: 1 + case .member: 2 + case .moderator: 3 + case .admin: 4 + case .owner: 5 } } @@ -1919,7 +2312,7 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Dec } } -public enum GroupMemberCategory: String, Decodable { +public enum GroupMemberCategory: String, Decodable, Hashable { case userMember = "user" case inviteeMember = "invitee" case hostMember = "host" @@ -1927,11 +2320,14 @@ public enum GroupMemberCategory: String, Decodable { case postMember = "post" } -public enum GroupMemberStatus: String, Decodable { +public enum GroupMemberStatus: String, Decodable, Hashable { + case memRejected = "rejected" case memRemoved = "removed" case memLeft = "left" case memGroupDeleted = "deleted" + case memUnknown = "unknown" case memInvited = "invited" + case memPendingApproval = "pending_approval" case memIntroduced = "introduced" case memIntroInvited = "intro-inv" case memAccepted = "accepted" @@ -1942,10 +2338,13 @@ public enum GroupMemberStatus: String, Decodable { public var text: LocalizedStringKey { switch self { + case .memRejected: return "rejected" case .memRemoved: return "removed" case .memLeft: return "left" case .memGroupDeleted: return "group deleted" + case .memUnknown: return "unknown status" case .memInvited: return "invited" + case .memPendingApproval: return "pending approval" case .memIntroduced: return "connecting (introduced)" case .memIntroInvited: return "connecting (introduction invitation)" case .memAccepted: return "connecting (accepted)" @@ -1958,10 +2357,13 @@ public enum GroupMemberStatus: String, Decodable { public var shortText: LocalizedStringKey { switch self { + case .memRejected: return "rejected" case .memRemoved: return "removed" case .memLeft: return "left" case .memGroupDeleted: return "group deleted" + case .memUnknown: return "unknown" case .memInvited: return "invited" + case .memPendingApproval: return "pending" case .memIntroduced: return "connecting" case .memIntroInvited: return "connecting" case .memAccepted: return "connecting" @@ -1973,83 +2375,207 @@ public enum GroupMemberStatus: String, Decodable { } } -public enum InvitedBy: Decodable { +public struct NoteFolder: Identifiable, Decodable, NamedChat, Hashable { + public var noteFolderId: Int64 + public var favorite: Bool + public var unread: Bool + var createdAt: Date + public var updatedAt: Date + var chatTs: Date + + public var id: ChatId { get { "*\(noteFolderId)" } } + public var apiId: Int64 { get { noteFolderId } } + public var ready: Bool { get { true } } + public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { nil } + public var sendMsgEnabled: Bool { get { true } } + public var displayName: String { get { ChatInfo.privateNotesChatName } } + public var fullName: String { get { "" } } + public var image: String? { get { nil } } + public var localAlias: String { get { "" } } + + public var canEdit: Bool { true } + + public var canDelete: Bool { true } + + public var canAddMembers: Bool { false } + + public static let sampleData = NoteFolder( + noteFolderId: 1, + favorite: false, + unread: false, + createdAt: .now, + updatedAt: .now, + chatTs: .now + ) +} + +public enum InvitedBy: Decodable, Hashable { case contact(byContactId: Int64) case user case unknown } -public struct MemberSubError: Decodable { +public struct MemberSubError: Decodable, Hashable { var member: GroupMemberIds var memberError: ChatError } -public enum ConnectionEntity: Decodable { - case rcvDirectMsgConnection(contact: Contact?) - case rcvGroupMsgConnection(groupInfo: GroupInfo, groupMember: GroupMember) - case sndFileConnection(sndFileTransfer: SndFileTransfer) - case rcvFileConnection(rcvFileTransfer: RcvFileTransfer) - case userContactConnection(userContact: UserContact) +public enum ConnectionEntity: Decodable, Hashable { + case rcvDirectMsgConnection(entityConnection: Connection, contact: Contact?) + case rcvGroupMsgConnection(entityConnection: Connection, groupInfo: GroupInfo, groupMember: GroupMember) + case sndFileConnection(entityConnection: Connection, sndFileTransfer: SndFileTransfer) + case rcvFileConnection(entityConnection: Connection, rcvFileTransfer: RcvFileTransfer) + case userContactConnection(entityConnection: Connection, userContact: UserContact) public var id: String? { switch self { - case let .rcvDirectMsgConnection(contact): - return contact?.id ?? nil - case let .rcvGroupMsgConnection(_, groupMember): - return groupMember.id - case let .userContactConnection(userContact): - return userContact.id + case let .rcvDirectMsgConnection(conn, contact): + contact?.id ?? conn.id + case let .rcvGroupMsgConnection(_, _, groupMember): + groupMember.id + case let .userContactConnection(_, userContact): + userContact.id default: - return nil + nil } } + + // public var localDisplayName: String? { + // switch self { + // case let .rcvDirectMsgConnection(conn, contact): + // if let name = contact?.localDisplayName { "@\(name)" } else { conn.id } + // case let .rcvGroupMsgConnection(_, g, m): + // "#\(g.localDisplayName) @\(m.localDisplayName)" + // case let .userContactConnection(_, userContact): + // userContact.id + // default: + // nil + // } + // } - public var ntfsEnabled: Bool { + public var conn: Connection { switch self { - case let .rcvDirectMsgConnection(contact): return contact?.chatSettings.enableNtfs == .all - case let .rcvGroupMsgConnection(groupInfo, _): return groupInfo.chatSettings.enableNtfs == .all - case .sndFileConnection: return false - case .rcvFileConnection: return false - case let .userContactConnection(userContact): return userContact.groupId == nil + case let .rcvDirectMsgConnection(entityConnection, _): entityConnection + case let .rcvGroupMsgConnection(entityConnection, _, _): entityConnection + case let .sndFileConnection(entityConnection, _): entityConnection + case let .rcvFileConnection(entityConnection, _): entityConnection + case let .userContactConnection(entityConnection, _): entityConnection } } } -public struct NtfMsgInfo: Decodable { +public struct NtfConn: Decodable, Hashable { + public var user: User + public var agentConnId: String + public var agentDbQueueId: Int64 + public var connEntity: ConnectionEntity + public var expectedMsg_: NtfMsgInfo? +} + +public struct NtfMsgInfo: Decodable, Hashable { public var msgId: String public var msgTs: Date } -public struct AChatItem: Decodable { - public var chatInfo: ChatInfo - public var chatItem: ChatItem +public enum RcvNtfMsgInfo: Decodable { + case info(ntfMsgInfo: NtfMsgInfo?) + case error(ntfMsgError: AgentErrorType) + + @inline(__always) + public var noMsg: Bool { + if case let .info(msg) = self { msg == nil } else { true } + } - public var chatId: String { - if case let .groupRcv(groupMember) = chatItem.chatDir { - return groupMember.id - } - return chatInfo.id + @inline(__always) + public var isError: Bool { + if case .error = self { true } else { false } } } -public struct ACIReaction: Decodable { +let iso8601DateFormatter = { + let f = ISO8601DateFormatter() + f.formatOptions = [.withInternetDateTime] + return f +}() + +// used in apiGetConnNtfMessages +public struct ConnMsgReq { + public var msgConnId: String + public var msgDbQueueId: Int64 + public var msgTs: Date // SystemTime encodes as a number, should be taken from NtfMsgInfo + + public init(msgConnId: String, msgDbQueueId: Int64, msgTs: Date) { + self.msgConnId = msgConnId + self.msgDbQueueId = msgDbQueueId + self.msgTs = msgTs + } + + public var cmdString: String { + "\(msgConnId):\(msgDbQueueId):\(iso8601DateFormatter.string(from: msgTs))" + } +} + +public struct NtfMsgAckInfo: Decodable, Hashable { + public var msgId: String + public var msgTs_: Date? +} + +public struct ChatItemDeletion: Decodable, Hashable { + public var deletedChatItem: AChatItem + public var toChatItem: AChatItem? = nil +} + +public struct AChatItem: Decodable, Hashable { + public var chatInfo: ChatInfo + public var chatItem: ChatItem +} + +public struct CIMentionMember: Decodable, Hashable { + public var groupMemberId: Int64 + public var displayName: String + public var localAlias: String? + public var memberRole: GroupMemberRole +} + +public struct CIMention: Decodable, Hashable { + public var memberId: String + public var memberRef: CIMentionMember? + + public init(groupMember m: GroupMember) { + self.memberId = m.memberId + self.memberRef = CIMentionMember( + groupMemberId: m.groupMemberId, + displayName: m.memberProfile.displayName, + localAlias: m.memberProfile.localAlias, + memberRole: m.memberRole + ) + } +} + +public struct ACIReaction: Decodable, Hashable { public var chatInfo: ChatInfo public var chatReaction: CIReaction } -public struct CIReaction: Decodable { +public struct MemberReaction: Decodable, Hashable { + public var groupMember: GroupMember + public var reactionTs: Date +} + +public struct CIReaction: Decodable, Hashable { public var chatDir: CIDirection public var chatItem: ChatItem public var sentAt: Date public var reaction: MsgReaction } -public struct ChatItem: Identifiable, Decodable { - public init(chatDir: CIDirection, meta: CIMeta, content: CIContent, formattedText: [FormattedText]? = nil, quotedItem: CIQuote? = nil, reactions: [CIReactionCount] = [], file: CIFile? = nil) { +public struct ChatItem: Identifiable, Decodable, Hashable { + public init(chatDir: CIDirection, meta: CIMeta, content: CIContent, formattedText: [FormattedText]? = nil, mentions: [String: CIMention]? = nil, quotedItem: CIQuote? = nil, reactions: [CIReactionCount] = [], file: CIFile? = nil) { self.chatDir = chatDir self.meta = meta self.content = content self.formattedText = formattedText + self.mentions = mentions self.quotedItem = quotedItem self.reactions = reactions self.file = file @@ -2059,6 +2585,7 @@ public struct ChatItem: Identifiable, Decodable { public var meta: CIMeta public var content: CIContent public var formattedText: [FormattedText]? + public var mentions: [String: CIMention]? public var quotedItem: CIQuote? public var reactions: [CIReactionCount] public var file: CIFile? @@ -2067,7 +2594,7 @@ public struct ChatItem: Identifiable, Decodable { public var isLiveDummy: Bool = false private enum CodingKeys: String, CodingKey { - case chatDir, meta, content, formattedText, quotedItem, reactions, file + case chatDir, meta, content, formattedText, mentions, quotedItem, reactions, file } public var id: Int64 { meta.itemId } @@ -2092,6 +2619,7 @@ public struct ChatItem: Identifiable, Decodable { case .rcvDeleted: return true case .sndModerated: return true case .rcvModerated: return true + case .rcvBlocked: return true default: return false } } @@ -2136,50 +2664,57 @@ public struct ChatItem: Identifiable, Decodable { } } - private var showNtfDir: Bool { - return !chatDir.sent - } - public var showNotification: Bool { switch content { - case .sndMsgContent: return showNtfDir - case .rcvMsgContent: return showNtfDir - case .sndDeleted: return showNtfDir - case .rcvDeleted: return showNtfDir - case .sndCall: return showNtfDir + case .sndMsgContent: return false + case .rcvMsgContent: return meta.itemDeleted == nil + case .sndDeleted: return false + case .rcvDeleted: return false + case .sndCall: return false case .rcvCall: return false // notification is shown on .callInvitation instead - case .rcvIntegrityError: return showNtfDir - case .rcvDecryptionError: return showNtfDir - case .rcvGroupInvitation: return showNtfDir - case .sndGroupInvitation: return showNtfDir - case .rcvDirectEvent: return false + case .rcvIntegrityError: return false + case .rcvDecryptionError: return false + case .rcvGroupInvitation: return true + case .sndGroupInvitation: return false + case .rcvDirectEvent(rcvDirectEvent: let rcvDirectEvent): + switch rcvDirectEvent { + case .contactDeleted: return false + case .profileUpdated: return false + } case .rcvGroupEvent(rcvGroupEvent: let rcvGroupEvent): switch rcvGroupEvent { case .groupUpdated: return false case .memberConnected: return false case .memberRole: return false - case .userRole: return showNtfDir - case .userDeleted: return showNtfDir - case .groupDeleted: return showNtfDir + case .memberBlocked: return false + case .userRole: return true + case .userDeleted: return true + case .groupDeleted: return true case .memberAdded: return false case .memberLeft: return false case .memberDeleted: return false case .invitedViaGroupLink: return false case .memberCreatedContact: return false + case .memberProfileUpdated: return false } - case .sndGroupEvent: return showNtfDir + case .sndGroupEvent: return false case .rcvConnEvent: return false - case .sndConnEvent: return showNtfDir + case .sndConnEvent: return false case .rcvChatFeature: return false - case .sndChatFeature: return showNtfDir + case .sndChatFeature: return false case .rcvChatPreference: return false - case .sndChatPreference: return showNtfDir + case .sndChatPreference: return false case .rcvGroupFeature: return false - case .sndGroupFeature: return showNtfDir - case .rcvChatFeatureRejected: return showNtfDir - case .rcvGroupFeatureRejected: return showNtfDir - case .sndModerated: return true - case .rcvModerated: return true + case .sndGroupFeature: return false + case .rcvChatFeatureRejected: return true + case .rcvGroupFeatureRejected: return false + case .sndModerated: return false + case .rcvModerated: return false + case .rcvBlocked: return false + case .sndDirectE2EEInfo: return false + case .rcvDirectE2EEInfo: return false + case .sndGroupE2EEInfo: return false + case .rcvGroupE2EEInfo: return false case .invalidJSON: return false } } @@ -2205,36 +2740,75 @@ public struct ChatItem: Identifiable, Decodable { return fileSource.cryptoArgs != nil } - public var encryptLocalFile: Bool { - content.msgContent?.isVideo == false && - privacyEncryptLocalFilesGroupDefault.get() - } - public var memberDisplayName: String? { - get { - if case let .groupRcv(groupMember) = chatDir { - return groupMember.chatViewName - } else { - return nil + if case let .groupRcv(groupMember) = chatDir { + switch content { + case let .rcvGroupEvent(rcvGroupEvent: .memberProfileUpdated(fromProfile, toProfile)): + toProfile.displayName != fromProfile.displayName || toProfile.fullName != fromProfile.fullName + ? nil + : groupMember.chatViewName + default: + groupMember.chatViewName } + } else { + nil } } - public func memberToModerate(_ chatInfo: ChatInfo) -> (GroupInfo, GroupMember)? { + public var localNote: Bool { + switch chatDir { + case .localSnd, .localRcv: return true + default: return false + } + } + + public func memberToModerate(_ chatInfo: ChatInfo) -> (GroupInfo, GroupMember?)? { switch (chatInfo, chatDir) { case let (.group(groupInfo), .groupRcv(groupMember)): let m = groupInfo.membership return m.memberRole >= .admin && m.memberRole >= groupMember.memberRole && meta.itemDeleted == nil ? (groupInfo, groupMember) : nil + case let (.group(groupInfo), .groupSnd): + let m = groupInfo.membership + return m.memberRole >= .admin ? (groupInfo, nil) : nil default: return nil } } - public static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, file: CIFile? = nil, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, editable: Bool = true) -> ChatItem { + public var showLocalDelete: Bool { + switch content { + case .sndDirectE2EEInfo: return false + case .rcvDirectE2EEInfo: return false + case .sndGroupE2EEInfo: return false + case .rcvGroupE2EEInfo: return false + default: return true + } + } + + public var isReport: Bool { + switch content { + case let .sndMsgContent(msgContent), let .rcvMsgContent(msgContent): + switch msgContent { + case .report: true + default: false + } + default: false + } + } + + public var isActiveReport: Bool { + isReport && !isDeletedContent && meta.itemDeleted == nil + } + + public var canBeDeletedForSelf: Bool { + (content.msgContent != nil && !meta.isLive) || meta.itemDeleted != nil || isDeletedContent || mergeCategory != nil || showLocalDelete + } + + public static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, file: CIFile? = nil, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, deletable: Bool = true, editable: Bool = true) -> ChatItem { ChatItem( chatDir: dir, - meta: CIMeta.getSample(id, ts, text, status, itemDeleted: itemDeleted, itemEdited: itemEdited, itemLive: itemLive, editable: editable), + meta: CIMeta.getSample(id, ts, text, status, itemDeleted: itemDeleted, itemEdited: itemEdited, itemLive: itemLive, deletable: deletable, editable: editable), content: .sndMsgContent(msgContent: .text(text)), quotedItem: quotedItem, file: file @@ -2311,6 +2885,35 @@ public struct ChatItem: Identifiable, Decodable { file: nil ) } + + public static func getReportSample(text: String, reason: ReportReason, item: ChatItem, sender: GroupMember? = nil) -> ChatItem { + let chatDir = if let sender = sender { + CIDirection.groupRcv(groupMember: sender) + } else { + CIDirection.groupSnd + } + + return ChatItem( + chatDir: chatDir, + meta: CIMeta( + itemId: -2, + itemTs: .now, + itemText: "", + itemStatus: .rcvRead, + createdAt: .now, + updatedAt: .now, + itemDeleted: nil, + itemEdited: false, + itemLive: false, + userMention: false, + deletable: false, + editable: false + ), + content: .sndMsgContent(msgContent: .report(text: text, reason: reason)), + quotedItem: CIQuote.getSample(item.id, item.meta.createdAt, item.text, chatDir: item.chatDir), + file: nil + ) + } public static func deletedItemDummy() -> ChatItem { ChatItem( @@ -2325,6 +2928,8 @@ public struct ChatItem: Identifiable, Decodable { itemDeleted: nil, itemEdited: false, itemLive: false, + userMention: false, + deletable: false, editable: false ), content: .rcvDeleted(deleteMode: .cidmBroadcast), @@ -2346,6 +2951,8 @@ public struct ChatItem: Identifiable, Decodable { itemDeleted: nil, itemEdited: false, itemLive: true, + userMention: false, + deletable: false, editable: false ), content: .sndMsgContent(msgContent: .text("")), @@ -2356,7 +2963,7 @@ public struct ChatItem: Identifiable, Decodable { return item } - public static func invalidJSON(chatDir: CIDirection?, meta: CIMeta?, json: String) -> ChatItem { + public static func invalidJSON(chatDir: CIDirection?, meta: CIMeta?, json: Data?) -> ChatItem { ChatItem( chatDir: chatDir ?? .directSnd, meta: meta ?? .invalidJSON, @@ -2367,7 +2974,7 @@ public struct ChatItem: Identifiable, Decodable { } } -public enum CIMergeCategory { +public enum CIMergeCategory: Hashable { case memberConnected case rcvGroupEvent case sndGroupEvent @@ -2376,11 +2983,13 @@ public enum CIMergeCategory { case chatFeature } -public enum CIDirection: Decodable { +public enum CIDirection: Decodable, Hashable { case directSnd case directRcv case groupSnd case groupRcv(groupMember: GroupMember) + case localSnd + case localRcv public var sent: Bool { get { @@ -2389,25 +2998,38 @@ public enum CIDirection: Decodable { case .directRcv: return false case .groupSnd: return true case .groupRcv: return false + case .localSnd: return true + case .localRcv: return false } } } + + public func sameDirection(_ dir: CIDirection) -> Bool { + switch (self, dir) { + case let (.groupRcv(m1), .groupRcv(m2)): m1.groupMemberId == m2.groupMemberId + default: sent == dir.sent + } + } } -public struct CIMeta: Decodable { +public struct CIMeta: Decodable, Hashable { public var itemId: Int64 public var itemTs: Date var itemText: String public var itemStatus: CIStatus + public var sentViaProxy: Bool? public var createdAt: Date public var updatedAt: Date + public var itemForwarded: CIForwardedFrom? public var itemDeleted: CIDeleted? public var itemEdited: Bool public var itemTimed: CITimed? public var itemLive: Bool? + public var userMention: Bool + public var deletable: Bool public var editable: Bool - public var timestampText: Text { get { formatTimestampText(itemTs) } } + public var timestampText: Text { Text(formatTimestampMeta(itemTs)) } public var recent: Bool { updatedAt + 10 > .now } public var isLive: Bool { itemLive == true } public var disappearing: Bool { !isRcvNew && itemTimed?.deleteAt != nil } @@ -2417,11 +3039,7 @@ public struct CIMeta: Decodable { return false } - public func statusIcon(_ metaColor: Color = .secondary) -> (String, Color)? { - itemStatus.statusIcon(metaColor) - } - - public static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, editable: Bool = true) -> CIMeta { + public static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, deletable: Bool = true, editable: Bool = true) -> CIMeta { CIMeta( itemId: id, itemTs: ts, @@ -2432,6 +3050,8 @@ public struct CIMeta: Decodable { itemDeleted: itemDeleted, itemEdited: itemEdited, itemLive: itemLive, + userMention: false, + deletable: deletable, editable: editable ) } @@ -2447,21 +3067,34 @@ public struct CIMeta: Decodable { itemDeleted: nil, itemEdited: false, itemLive: false, + userMention: false, + deletable: false, editable: false ) } } -public struct CITimed: Decodable { +public struct CITimed: Decodable, Hashable { public var ttl: Int public var deleteAt: Date? } let msgTimeFormat = Date.FormatStyle.dateTime.hour().minute() let msgDateFormat = Date.FormatStyle.dateTime.day(.twoDigits).month(.twoDigits) +let msgDateYearFormat = Date.FormatStyle.dateTime.day(.twoDigits).month(.twoDigits).year(.twoDigits) public func formatTimestampText(_ date: Date) -> Text { - return Text(date, format: recent(date) ? msgTimeFormat : msgDateFormat) + Text(verbatim: date.formatted( + recent(date) + ? msgTimeFormat + : Calendar.current.isDate(date, equalTo: .now, toGranularity: .year) + ? msgDateFormat + : msgDateYearFormat + )) +} + +public func formatTimestampMeta(_ date: Date) -> String { + date.formatted(date: .omitted, time: .shortened) } private func recent(_ date: Date) -> Bool { @@ -2479,43 +3112,61 @@ private func recent(_ date: Date) -> Bool { return isSameDay || (now < currentDay12 && date >= previousDay18 && date < currentDay00) } -public enum CIStatus: Decodable { +public enum CIStatus: Decodable, Hashable { case sndNew case sndSent(sndProgress: SndCIStatusProgress) case sndRcvd(msgRcptStatus: MsgReceiptStatus, sndProgress: SndCIStatusProgress) case sndErrorAuth - case sndError(agentError: String) + case sndError(agentError: SndError) + case sndWarning(agentError: SndError) case rcvNew case rcvRead case invalid(text: String) - var id: String { + public var id: String { switch self { case .sndNew: return "sndNew" case .sndSent: return "sndSent" case .sndRcvd: return "sndRcvd" case .sndErrorAuth: return "sndErrorAuth" case .sndError: return "sndError" + case .sndWarning: return "sndWarning" case .rcvNew: return "rcvNew" case .rcvRead: return "rcvRead" case .invalid: return "invalid" } } - - public func statusIcon(_ metaColor: Color = .secondary) -> (String, Color)? { + + public var sent: Bool { switch self { - case .sndNew: return nil - case .sndSent: return ("checkmark", metaColor) - case let .sndRcvd(msgRcptStatus, _): + case .sndNew: true + case .sndSent: true + case .sndRcvd: true + case .sndErrorAuth: true + case .sndError: true + case .sndWarning: true + case .rcvNew: false + case .rcvRead: false + case .invalid: false + } + } + + public func statusIcon(_ metaColor: Color, _ paleMetaColor: Color, _ primaryColor: Color = .accentColor) -> (Image, Color)? { + switch self { + case .sndNew: nil + case let .sndSent(sndProgress): + (Image("checkmark.wide"), sndProgress == .partial ? paleMetaColor : metaColor) + case let .sndRcvd(msgRcptStatus, sndProgress): switch msgRcptStatus { - case .ok: return ("checkmark", metaColor) - case .badMsgHash: return ("checkmark", .red) + case .ok: (Image("checkmark.2"), sndProgress == .partial ? paleMetaColor : metaColor) + case .badMsgHash: (Image("checkmark.2"), .red) } - case .sndErrorAuth: return ("multiply", .red) - case .sndError: return ("exclamationmark.triangle.fill", .yellow) - case .rcvNew: return ("circlebadge.fill", Color.accentColor) - case .rcvRead: return nil - case .invalid: return ("questionmark", metaColor) + case .sndErrorAuth: (Image(systemName: "multiply"), .red) + case .sndError: (Image(systemName: "multiply"), .red) + case .sndWarning: (Image(systemName: "exclamationmark.triangle.fill"), .orange) + case .rcvNew: (Image(systemName: "circlebadge.fill"), primaryColor) + case .rcvRead: nil + case .invalid: (Image(systemName: "questionmark"), metaColor) } } @@ -2530,7 +3181,11 @@ public enum CIStatus: Decodable { ) case let .sndError(agentError): return ( NSLocalizedString("Message delivery error", comment: "item status text"), - String.localizedStringWithFormat(NSLocalizedString("Unexpected error: %@", comment: "item status description"), agentError) + agentError.errorInfo + ) + case let .sndWarning(agentError): return ( + NSLocalizedString("Message delivery warning", comment: "item status text"), + agentError.errorInfo ) case .rcvNew: return nil case .rcvRead: return nil @@ -2540,42 +3195,191 @@ public enum CIStatus: Decodable { ) } } + + public var isSndRcvd: Bool { + switch self { + case .sndRcvd: return true + default: return false + } + } } -public enum MsgReceiptStatus: String, Decodable { +public enum SndError: Decodable, Hashable { + case auth + case quota + case expired + case relay(srvError: SrvError) + case proxy(proxyServer: String, srvError: SrvError) + case proxyRelay(proxyServer: String, srvError: SrvError) + case other(sndError: String) + + public var errorInfo: String { + switch self { + case .auth: NSLocalizedString("Wrong key or unknown connection - most likely this connection is deleted.", comment: "snd error text") + case .quota: NSLocalizedString("Capacity exceeded - recipient did not receive previously sent messages.", comment: "snd error text") + case .expired: NSLocalizedString("Network issues - message expired after many attempts to send it.", comment: "snd error text") + case let .relay(srvError): String.localizedStringWithFormat(NSLocalizedString("Destination server error: %@", comment: "snd error text"), srvError.errorInfo) + case let .proxy(proxyServer, srvError): String.localizedStringWithFormat(NSLocalizedString("Forwarding server: %@\nError: %@", comment: "snd error text"), proxyServer, srvError.errorInfo) + case let .proxyRelay(proxyServer, srvError): String.localizedStringWithFormat(NSLocalizedString("Forwarding server: %@\nDestination server error: %@", comment: "snd error text"), proxyServer, srvError.errorInfo) + case let .other(sndError): String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "snd error text"), sndError) + } + } +} + +public enum SrvError: Decodable, Hashable { + case host + case version + case other(srvError: String) + + var id: String { + switch self { + case .host: return "host" + case .version: return "version" + case let .other(srvError): return "other \(srvError)" + } + } + + public var errorInfo: String { + switch self { + case .host: NSLocalizedString("Server address is incompatible with network settings.", comment: "srv error text.") + case .version: NSLocalizedString("Server version is incompatible with network settings.", comment: "srv error text") + case let .other(srvError): srvError + } + } +} + +public enum MsgReceiptStatus: String, Decodable, Hashable { case ok case badMsgHash } -public enum SndCIStatusProgress: String, Decodable { +public enum SndCIStatusProgress: String, Decodable, Hashable { case partial case complete } -public enum CIDeleted: Decodable { +public enum GroupSndStatus: Decodable, Hashable { + case new + case forwarded + case inactive + case sent + case rcvd(msgRcptStatus: MsgReceiptStatus) + case error(agentError: SndError) + case warning(agentError: SndError) + case invalid(text: String) + + public func statusIcon(_ metaColor: Color, _ primaryColor: Color = .accentColor) -> (Image, Color) { + switch self { + case .new: (Image(systemName: "ellipsis"), metaColor) + case .forwarded: (Image(systemName: "chevron.forward.2"), metaColor) + case .inactive: (Image(systemName: "person.badge.minus"), metaColor) + case .sent: (Image("checkmark.wide"), metaColor) + case let .rcvd(msgRcptStatus): + switch msgRcptStatus { + case .ok: (Image("checkmark.2"), metaColor) + case .badMsgHash: (Image("checkmark.2"), .red) + } + case .error: (Image(systemName: "multiply"), .red) + case .warning: (Image(systemName: "exclamationmark.triangle.fill"), .orange) + case .invalid: (Image(systemName: "questionmark"), metaColor) + } + } + + public var statusInfo: (String, String)? { + switch self { + case .new: return nil + case .forwarded: return ( + NSLocalizedString("Message forwarded", comment: "item status text"), + NSLocalizedString("No direct connection yet, message is forwarded by admin.", comment: "item status description") + ) + case .inactive: return ( + NSLocalizedString("Member inactive", comment: "item status text"), + NSLocalizedString("Message may be delivered later if member becomes active.", comment: "item status description") + ) + case .sent: return nil + case .rcvd: return nil + case let .error(agentError): return ( + NSLocalizedString("Message delivery error", comment: "item status text"), + agentError.errorInfo + ) + case let .warning(agentError): return ( + NSLocalizedString("Message delivery warning", comment: "item status text"), + agentError.errorInfo + ) + case let .invalid(text): return ( + NSLocalizedString("Invalid status", comment: "item status text"), + text + ) + } + } +} + +public enum CIDeleted: Decodable, Hashable { case deleted(deletedTs: Date?) case blocked(deletedTs: Date?) + case blockedByAdmin(deletedTs: Date?) case moderated(deletedTs: Date?, byGroupMember: GroupMember) var id: String { switch self { case .deleted: return "deleted" case .blocked: return "blocked" + case .blockedByAdmin: return "blocked by admin" case .moderated: return "moderated" } } } -public enum CIDeleteMode: String, Decodable { +public enum MsgDirection: String, Decodable, Hashable { + case rcv = "rcv" + case snd = "snd" +} + +public enum CIForwardedFrom: Decodable, Hashable { + case unknown + case contact(chatName: String, msgDir: MsgDirection, contactId: Int64?, chatItemId: Int64?) + case group(chatName: String, msgDir: MsgDirection, groupId: Int64?, chatItemId: Int64?) + + var chatName: String { + switch self { + case .unknown: "" + case let .contact(chatName, _, _, _): chatName + case let .group(chatName, _, _, _): chatName + } + } + + public var chatTypeApiIdMsgId: (ChatType, Int64, ChatItem.ID?)? { + switch self { + case .unknown: nil + case let .contact(_, _, contactId, msgId): + if let contactId { + (ChatType.direct, contactId, msgId) + } else { nil } + case let .group(_, _, groupId, msgId): + if let groupId { + (ChatType.group, groupId, msgId) + } else { nil } + } + } + + public func text(_ chatType: ChatType) -> LocalizedStringKey { + chatType == .local + ? (chatName == "" ? "saved" : "saved from \(chatName)") + : "forwarded" + } +} + +public enum CIDeleteMode: String, Decodable, Hashable { case cidmBroadcast = "broadcast" case cidmInternal = "internal" + case cidmInternalMark = "internalMark" } protocol ItemContent { var text: String { get } } -public enum CIContent: Decodable, ItemContent { +public enum CIContent: Decodable, ItemContent, Hashable { case sndMsgContent(msgContent: MsgContent) case rcvMsgContent(msgContent: MsgContent) case sndDeleted(deleteMode: CIDeleteMode) // legacy - since v4.3.0 itemDeleted field is used @@ -2595,13 +3399,18 @@ public enum CIContent: Decodable, ItemContent { case sndChatFeature(feature: ChatFeature, enabled: FeatureEnabled, param: Int?) case rcvChatPreference(feature: ChatFeature, allowed: FeatureAllowed, param: Int?) case sndChatPreference(feature: ChatFeature, allowed: FeatureAllowed, param: Int?) - case rcvGroupFeature(groupFeature: GroupFeature, preference: GroupPreference, param: Int?) - case sndGroupFeature(groupFeature: GroupFeature, preference: GroupPreference, param: Int?) + case rcvGroupFeature(groupFeature: GroupFeature, preference: GroupPreference, param: Int?, memberRole_: GroupMemberRole?) + case sndGroupFeature(groupFeature: GroupFeature, preference: GroupPreference, param: Int?, memberRole_: GroupMemberRole?) case rcvChatFeatureRejected(feature: ChatFeature) case rcvGroupFeatureRejected(groupFeature: GroupFeature) case sndModerated case rcvModerated - case invalidJSON(json: String) + case rcvBlocked + case sndDirectE2EEInfo(e2eeInfo: E2EEInfo) + case rcvDirectE2EEInfo(e2eeInfo: E2EEInfo) + case sndGroupE2EEInfo(e2eeInfo: E2EEInfo) + case rcvGroupE2EEInfo(e2eeInfo: E2EEInfo) + case invalidJSON(json: Data?) public var text: String { get { @@ -2625,21 +3434,51 @@ public enum CIContent: Decodable, ItemContent { 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): return CIContent.featureText(feature, preference.enable.text, param) - case let .sndGroupFeature(feature, preference, param): return CIContent.featureText(feature, preference.enable.text, 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 .invalidJSON: return NSLocalizedString("invalid data", comment: "invalid chat item") } } } - static func featureText(_ feature: Feature, _ enabled: String, _ param: Int?) -> String { - feature.hasParam - ? "\(feature.text): \(timeText(param))" - : "\(feature.text): \(enabled)" + private func directE2EEInfoStr(_ e2eeInfo: E2EEInfo) -> String { + e2eeInfo.pqEnabled + ? NSLocalizedString("This chat is protected by quantum resistant end-to-end encryption.", 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") + } + + static func featureText(_ feature: Feature, _ enabled: String, _ param: Int?, _ role: GroupMemberRole? = nil) -> String { + ( + feature.hasParam + ? "\(feature.text): \(timeText(param))" + : "\(feature.text): \(enabled)" + ) + + ( + feature.hasRole && role != nil + ? " (\(roleText(role)))" + : "" + ) + } + + private static func roleText(_ role: GroupMemberRole?) -> String { + switch role { + case .owner: NSLocalizedString("owners", comment: "feature role") + case .admin: NSLocalizedString("admins", comment: "feature role") + default: NSLocalizedString("all members", comment: "feature role") + } } public static func preferenceText(_ feature: Feature, _ allowed: FeatureAllowed, _ param: Int?) -> String { @@ -2669,13 +3508,21 @@ public enum CIContent: Decodable, ItemContent { case .rcvDecryptionError: return true case .rcvGroupInvitation: return true case .rcvModerated: return true + case .rcvBlocked: return true case .invalidJSON: return true default: return false } } + + public var isSndCall: Bool { + switch self { + case .sndCall: return true + default: return false + } + } } -public enum MsgDecryptError: String, Decodable { +public enum MsgDecryptError: String, Decodable, Hashable { case ratchetHeader case tooManySkipped case ratchetEarlier @@ -2693,27 +3540,27 @@ public enum MsgDecryptError: String, Decodable { } } -public struct CIQuote: Decodable, ItemContent { +public struct CIQuote: Decodable, ItemContent, Hashable { public var chatDir: CIDirection? public var itemId: Int64? var sharedMsgId: String? = nil public var sentAt: Date public var content: MsgContent public var formattedText: [FormattedText]? - public var text: String { switch (content.text, content) { case let ("", .voice(_, duration)): return durationText(duration) default: return content.text } } - public func getSender(_ membership: GroupMember?) -> String? { switch (chatDir) { case .directSnd: return "you" case .directRcv: return nil case .groupSnd: return membership?.displayName ?? "you" case let .groupRcv(member): return member.displayName + case .localSnd: return "you" + case .localRcv: return nil case nil: return nil } } @@ -2729,13 +3576,13 @@ public struct CIQuote: Decodable, ItemContent { } } -public struct CIReactionCount: Decodable { +public struct CIReactionCount: Decodable, Hashable { public var reaction: MsgReaction public var userReacted: Bool public var totalReacted: Int } -public enum MsgReaction: Hashable { +public enum MsgReaction: Hashable, Identifiable { case emoji(emoji: MREmojiChar) case unknown(type: String) @@ -2756,15 +3603,24 @@ public enum MsgReaction: Hashable { case type case emoji } + + public var id: String { + switch self { + case let .emoji(emoji): emoji.rawValue + case let .unknown(unknown): unknown + } + } } -public enum MREmojiChar: String, Codable, CaseIterable { +public enum MREmojiChar: String, Codable, CaseIterable, Hashable { case thumbsup = "👍" case thumbsdown = "👎" case smile = "😀" + case laugh = "😂" case sad = "😢" case heart = "❤" case launch = "🚀" + case check = "✅" } extension MsgReaction: Decodable { @@ -2774,8 +3630,12 @@ extension MsgReaction: Decodable { let type = try container.decode(String.self, forKey: CodingKeys.type) switch type { case "emoji": - let emoji = try container.decode(MREmojiChar.self, forKey: CodingKeys.emoji) - self = .emoji(emoji: emoji) + do { + let emoji = try container.decode(MREmojiChar.self, forKey: CodingKeys.emoji) + self = .emoji(emoji: emoji) + } catch { + self = .unknown(type: "emoji") + } default: self = .unknown(type: type) } @@ -2799,7 +3659,7 @@ extension MsgReaction: Encodable { } } -public struct CIFile: Decodable { +public struct CIFile: Decodable, Hashable { public var fileId: Int64 public var fileName: String public var fileSize: Int64 @@ -2825,12 +3685,15 @@ public struct CIFile: Decodable { case .sndComplete: return true case .sndCancelled: return true case .sndError: return true + case .sndWarning: return true case .rcvInvitation: return false case .rcvAccepted: return false case .rcvTransfer: return false + case .rcvAborted: return false case .rcvCancelled: return false case .rcvComplete: return true case .rcvError: return false + case .rcvWarning: return false case .invalid: return false } } @@ -2849,19 +3712,44 @@ public struct CIFile: Decodable { } case .sndCancelled: return nil case .sndError: return nil + case .sndWarning: return sndCancelAction case .rcvInvitation: return nil case .rcvAccepted: return rcvCancelAction case .rcvTransfer: return rcvCancelAction + case .rcvAborted: return nil case .rcvCancelled: return nil case .rcvComplete: return nil + case .rcvWarning: return rcvCancelAction case .rcvError: return nil case .invalid: return nil } } } + + public var showStatusIconInSmallView: Bool { + get { + switch fileStatus { + case .sndStored: fileProtocol != .local + case .sndTransfer: true + case .sndComplete: false + case .sndCancelled: true + case .sndError: true + case .sndWarning: true + case .rcvInvitation: false + case .rcvAccepted: true + case .rcvTransfer: true + case .rcvAborted: true + case .rcvCancelled: true + case .rcvComplete: false + case .rcvError: true + case .rcvWarning: true + case .invalid: true + } + } + } } -public struct CryptoFile: Codable { +public struct CryptoFile: Codable, Hashable { public var filePath: String // the name of the file, not a full path public var cryptoArgs: CryptoFileArgs? @@ -2873,24 +3761,63 @@ public struct CryptoFile: Codable { public static func plain(_ f: String) -> CryptoFile { CryptoFile(filePath: f, cryptoArgs: nil) } + + private func decryptToTmpFile(_ filesToDelete: inout Set<URL>) async -> URL? { + if let cfArgs = cryptoArgs { + let url = getAppFilePath(filePath) + let tempUrl = getTempFilesDirectory().appendingPathComponent(filePath) + _ = filesToDelete.insert(tempUrl) + do { + try decryptCryptoFile(fromPath: url.path, cryptoArgs: cfArgs, toPath: tempUrl.path) + return tempUrl + } catch { + logger.error("Error decrypting file: \(error.localizedDescription)") + } + } + return nil + } + + public func decryptedGet() -> URL? { + let decrypted = CryptoFile.decryptedUrls[filePath] + return if let decrypted = decrypted, FileManager.default.fileExists(atPath: decrypted.path) { decrypted } else { nil } + } + + public func decryptedGetOrCreate(_ filesToDelete: inout Set<URL>) async -> URL? { + if let decrypted = decryptedGet() { + return decrypted + } else if let decrypted = await decryptToTmpFile(&filesToDelete) { + CryptoFile.decryptedUrls[filePath] = decrypted + return decrypted + } else { + return nil + } + } + + static var decryptedUrls = Dictionary<String, URL>() } -public struct CryptoFileArgs: Codable { +public struct CryptoFileArgs: Codable, Hashable { public var fileKey: String public var fileNonce: String } -public struct CancelAction { +public struct CancelAction: Hashable { public var uiAction: String public var alert: AlertInfo } -public struct AlertInfo { +public struct AlertInfo: Hashable { public var title: LocalizedStringKey public var message: LocalizedStringKey public var confirm: LocalizedStringKey } +extension LocalizedStringKey: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine("\(self)") + } +} + private var sndCancelAction = CancelAction( uiAction: NSLocalizedString("Stop file", comment: "cancel file action"), alert: AlertInfo( @@ -2918,50 +3845,93 @@ private var rcvCancelAction = CancelAction( ) ) -public enum FileProtocol: String, Decodable { +public enum FileProtocol: String, Decodable, Hashable { case smp = "smp" case xftp = "xftp" + case local = "local" } -public enum CIFileStatus: Decodable, Equatable { +public enum CIFileStatus: Decodable, Equatable, Hashable { case sndStored case sndTransfer(sndProgress: Int64, sndTotal: Int64) case sndComplete case sndCancelled - case sndError + case sndError(sndFileError: FileError) + case sndWarning(sndFileError: FileError) case rcvInvitation case rcvAccepted case rcvTransfer(rcvProgress: Int64, rcvTotal: Int64) + case rcvAborted case rcvComplete case rcvCancelled - case rcvError + case rcvError(rcvFileError: FileError) + case rcvWarning(rcvFileError: FileError) case invalid(text: String) - var id: String { + public var id: String { switch self { case .sndStored: return "sndStored" case let .sndTransfer(sndProgress, sndTotal): return "sndTransfer \(sndProgress) \(sndTotal)" case .sndComplete: return "sndComplete" case .sndCancelled: return "sndCancelled" - case .sndError: return "sndError" + case let .sndError(sndFileError): return "sndError \(sndFileError)" + case let .sndWarning(sndFileError): return "sndWarning \(sndFileError)" case .rcvInvitation: return "rcvInvitation" case .rcvAccepted: return "rcvAccepted" case let .rcvTransfer(rcvProgress, rcvTotal): return "rcvTransfer \(rcvProgress) \(rcvTotal)" + case .rcvAborted: return "rcvAborted" case .rcvComplete: return "rcvComplete" case .rcvCancelled: return "rcvCancelled" - case .rcvError: return "rcvError" + case let .rcvError(rcvFileError): return "rcvError \(rcvFileError)" + case let .rcvWarning(rcvFileError): return "rcvWarning \(rcvFileError)" case .invalid: return "invalid" } } } -public enum MsgContent: Equatable { +public enum FileError: Decodable, Equatable, Hashable { + case auth + case blocked(server: String, blockInfo: BlockingInfo) + case noFile + case relay(srvError: SrvError) + case other(fileError: String) + + var id: String { + switch self { + case .auth: return "auth" + case let .blocked(srv, info): return "blocked \(srv) \(info)" + case .noFile: return "noFile" + case let .relay(srvError): return "relay \(srvError)" + case let .other(fileError): return "other \(fileError)" + } + } + + public var errorInfo: String { + switch self { + case .auth: NSLocalizedString("Wrong key or unknown file chunk address - most likely file is deleted.", comment: "file error text") + case let .blocked(_, info): String.localizedStringWithFormat(NSLocalizedString("File is blocked by server operator:\n%@.", comment: "file error text"), info.reason.text) + case .noFile: NSLocalizedString("File not found - most likely file was deleted or cancelled.", comment: "file error text") + case let .relay(srvError): String.localizedStringWithFormat(NSLocalizedString("File server error: %@", comment: "file error text"), srvError.errorInfo) + case let .other(fileError): String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "file error text"), fileError) + } + } + + public var moreInfoButton: (label: LocalizedStringKey, link: URL)? { + switch self { + case .blocked: ("How it works", contentModerationPostLink) + default: nil + } + } +} + +public enum MsgContent: Equatable, Hashable { case text(String) case link(text: String, preview: LinkPreview) case image(text: String, image: String) case video(text: String, image: String, duration: Int) case voice(text: String, duration: Int) case file(String) + case report(text: String, reason: ReportReason) // TODO include original JSON, possibly using https://github.com/zoul/generic-json-swift case unknown(type: String, text: String) @@ -2973,6 +3943,7 @@ public enum MsgContent: Equatable { case let .video(text, _, _): return text case let .voice(text, _): return text case let .file(text): return text + case let .report(text, _): return text case let .unknown(_, text): return text } } @@ -3005,7 +3976,24 @@ public enum MsgContent: Equatable { } } - var cmdString: String { + public var isImageOrVideo: Bool { + switch self { + case .image: true + case .video: true + default: false + } + } + + public var isMediaOrFileAttachment: Bool { + switch self { + case .image: true + case .video: true + case .file: true + default: false + } + } + + public var cmdString: String { "json \(encodeJSON(self))" } @@ -3015,6 +4003,7 @@ public enum MsgContent: Equatable { case preview case image case duration + case reason } public static func == (lhs: MsgContent, rhs: MsgContent) -> Bool { @@ -3025,6 +4014,7 @@ public enum MsgContent: Equatable { case let (.video(lt, li, ld), .video(rt, ri, rd)): return lt == rt && li == ri && ld == rd 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 (.unknown(lType, lt), .unknown(rType, rt)): return lType == rType && lt == rt default: return false } @@ -3060,6 +4050,10 @@ extension MsgContent: Decodable { case "file": let text = try container.decode(String.self, forKey: CodingKeys.text) self = .file(text) + case "report": + let text = try container.decode(String.self, forKey: CodingKeys.text) + let reason = try container.decode(ReportReason.self, forKey: CodingKeys.reason) + self = .report(text: text, reason: reason) default: let text = try? container.decode(String.self, forKey: CodingKeys.text) self = .unknown(type: type, text: text ?? "unknown message format") @@ -3097,6 +4091,10 @@ extension MsgContent: Encodable { case let .file(text): try container.encode("file", forKey: .type) try container.encode(text, forKey: .text) + case let .report(text, reason): + try container.encode("report", forKey: .type) + try container.encode(text, forKey: .text) + try container.encode(reason, forKey: .reason) // TODO use original JSON and type case let .unknown(_, text): try container.encode("text", forKey: .type) @@ -3105,12 +4103,22 @@ extension MsgContent: Encodable { } } -public struct FormattedText: Decodable { +public struct FormattedText: Decodable, Hashable { public var text: String public var format: Format? + + public static func plain(_ text: String) -> [FormattedText] { + text.isEmpty + ? [] + : [FormattedText(text: text, format: nil)] + } + + public var isSecret: Bool { + if case .secret = format { true } else { false } + } } -public enum Format: Decodable, Equatable { +public enum Format: Decodable, Equatable, Hashable { case bold case italic case strikeThrough @@ -3119,25 +4127,37 @@ public enum Format: Decodable, Equatable { case colored(color: FormatColor) case uri case simplexLink(linkType: SimplexLinkType, simplexUri: String, smpHosts: [String]) + case mention(memberName: String) case email case phone + + public var isSimplexLink: Bool { + get { + switch (self) { + case .simplexLink: return true + default: return false + } + } + } } -public enum SimplexLinkType: String, Decodable { +public enum SimplexLinkType: String, Decodable, Hashable { case contact case invitation case group + case channel public var description: String { switch self { case .contact: return NSLocalizedString("SimpleX contact address", comment: "simplex link type") case .invitation: return NSLocalizedString("SimpleX one-time invitation", comment: "simplex link type") case .group: return NSLocalizedString("SimpleX group link", comment: "simplex link type") + case .channel: return NSLocalizedString("SimpleX channel link", comment: "simplex link type") } } } -public enum FormatColor: String, Decodable { +public enum FormatColor: String, Decodable, Hashable { case red = "red" case green = "green" case blue = "blue" @@ -3147,24 +4167,81 @@ public enum FormatColor: String, Decodable { case black = "black" case white = "white" - public var uiColor: Color { - get { - switch (self) { - case .red: return .red - case .green: return .green - case .blue: return .blue - case .yellow: return .yellow - case .cyan: return .cyan - case .magenta: return .purple - case .black: return .primary - case .white: return .primary - } + public var uiColor: Color? { + switch (self) { + case .red: .red + case .green: .green + case .blue: .blue + case .yellow: .yellow + case .cyan: .cyan + case .magenta: .purple + case .black: nil + case .white: nil + } + } +} + +public enum ReportReason: Hashable { + case spam + case illegal + case community + case profile + case other + case unknown(type: String) + + public static var supportedReasons: [ReportReason] = [.spam, .illegal, .community, .profile, .other] + + public var text: String { + switch self { + case .spam: return NSLocalizedString("Spam", comment: "report reason") + case .illegal: return NSLocalizedString("Inappropriate content", comment: "report reason") + case .community: return NSLocalizedString("Community guidelines violation", comment: "report reason") + case .profile: return NSLocalizedString("Inappropriate profile", comment: "report reason") + case .other: return NSLocalizedString("Another reason", comment: "report reason") + case let .unknown(type): return type + } + } + + public var attrString: NSAttributedString { + let descr = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body) + return NSAttributedString(string: text.isEmpty ? self.text : "\(self.text): ", attributes: [ + .font: UIFont(descriptor: descr.withSymbolicTraits(.traitItalic) ?? descr, size: 0), + .foregroundColor: UIColor(Color.red) + ]) + } +} + +extension ReportReason: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .spam: try container.encode("spam") + case .illegal: try container.encode("illegal") + case .community: try container.encode("community") + case .profile: try container.encode("profile") + case .other: try container.encode("other") + case let .unknown(type): try container.encode(type) + } + } +} + +extension ReportReason: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let type = try container.decode(String.self) + switch type { + case "spam": self = .spam + case "illegal": self = .illegal + case "community": self = .community + case "profile": self = .profile + case "other": self = .other + default: self = .unknown(type: type) } } } // Struct to use with simplex API -public struct LinkPreview: Codable, Equatable { +public struct LinkPreview: Codable, Equatable, Hashable { public init(uri: URL, title: String, description: String = "", image: String) { self.uri = uri self.title = title @@ -3179,28 +4256,83 @@ public struct LinkPreview: Codable, Equatable { public var image: String } -public enum NtfTknStatus: String, Decodable { +public enum NtfTknStatus: String, Decodable, Hashable { case new = "NEW" case registered = "REGISTERED" case invalid = "INVALID" + case invalidBad = "INVALID,BAD" + case invalidTopic = "INVALID,TOPIC" + case invalidExpired = "INVALID,EXPIRED" + case invalidUnregistered = "INVALID,UNREGISTERED" case confirmed = "CONFIRMED" case active = "ACTIVE" case expired = "EXPIRED" -} -public struct SndFileTransfer: Decodable { + public var workingToken: Bool { + switch self { + case .new: true + case .registered: true + case .invalid: false + case .invalidBad: false + case .invalidTopic: false + case .invalidExpired: false + case .invalidUnregistered: false + case .confirmed: true + case .active: true + case .expired: false + } + } -} - -public struct RcvFileTransfer: Decodable { - -} - -public struct FileTransferMeta: Decodable { + public var text: String { + switch self { + case .new: NSLocalizedString("New", comment: "token status text") + case .registered: NSLocalizedString("Registered", comment: "token status text") + case .invalid: NSLocalizedString("Invalid", comment: "token status text") + case .invalidBad: NSLocalizedString("Invalid (bad token)", comment: "token status text") + case .invalidTopic: NSLocalizedString("Invalid (wrong topic)", comment: "token status text") + case .invalidExpired: NSLocalizedString("Invalid (expired)", comment: "token status text") + case .invalidUnregistered: NSLocalizedString("Invalid (unregistered)", comment: "token status text") + case .confirmed: NSLocalizedString("Confirmed", comment: "token status text") + case .active: NSLocalizedString("Active", comment: "token status text") + case .expired: NSLocalizedString("Expired", comment: "token status text") + } + } + public func info(register: Bool) -> String { + switch self { + case .new: return NSLocalizedString("Please wait for token to be registered.", comment: "token info") + case .registered: fallthrough + case .confirmed: return NSLocalizedString("Please wait for token activation to complete.", comment: "token info") + case .active: return NSLocalizedString("You should receive notifications.", comment: "token info") + case .invalid: fallthrough + case .invalidBad: fallthrough + case .invalidTopic: fallthrough + case .invalidExpired: fallthrough + case .invalidUnregistered: fallthrough + case .expired: + return register + ? NSLocalizedString("Register notification token?", comment: "token info") + : NSLocalizedString("Please try to disable and re-enable notfications.", comment: "token info") + } + } } -public enum CICallStatus: String, Decodable { +public struct SndFileTransfer: Decodable, Hashable { + +} + +public struct RcvFileTransfer: Decodable, Hashable { + public let fileId: Int64 +} + +public struct FileTransferMeta: Decodable, Hashable { + public let fileId: Int64 + public let fileName: String + public let filePath: String + public let fileSize: Int64 +} + +public enum CICallStatus: String, Decodable, Hashable { case pending case missed case rejected @@ -3232,7 +4364,7 @@ public func durationText(_ sec: Int) -> String { : String(format: "%02d:%02d:%02d", m / 60, m % 60, s) } -public enum MsgErrorType: Decodable { +public enum MsgErrorType: Decodable, Hashable { case msgSkipped(fromMsgId: Int64, toMsgId: Int64) case msgBadId(msgId: Int64) case msgBadHash @@ -3249,7 +4381,7 @@ public enum MsgErrorType: Decodable { } } -public struct CIGroupInvitation: Decodable { +public struct CIGroupInvitation: Decodable, Hashable { public var groupId: Int64 public var groupMemberId: Int64 public var localDisplayName: GroupName @@ -3265,28 +4397,52 @@ public struct CIGroupInvitation: Decodable { } } -public enum CIGroupInvitationStatus: String, Decodable { +public enum CIGroupInvitationStatus: String, Decodable, Hashable { case pending case accepted case rejected case expired } -public enum RcvDirectEvent: Decodable { +public struct E2EEInfo: Decodable, Hashable { + public var pqEnabled: Bool +} + +public enum RcvDirectEvent: Decodable, Hashable { case contactDeleted + case profileUpdated(fromProfile: Profile, toProfile: Profile) var text: String { switch self { case .contactDeleted: return NSLocalizedString("deleted contact", comment: "rcv direct event chat item") + case let .profileUpdated(fromProfile, toProfile): return profileUpdatedText(fromProfile, toProfile) + } + } + + private func profileUpdatedText(_ from: Profile, _ to: Profile) -> String { + if to.displayName != from.displayName || to.fullName != from.fullName { + String.localizedStringWithFormat(NSLocalizedString("contact %@ changed to %@", comment: "profile update event chat item"), from.profileViewName, to.profileViewName) + } else if to.image != from.image { + to.image == nil + ? NSLocalizedString("removed profile picture", comment: "profile update event chat item") + : NSLocalizedString("set new profile picture", comment: "profile update event chat item") + } else if to.contactLink != from.contactLink { + to.contactLink == nil + ? NSLocalizedString("removed contact address", comment: "profile update event chat item") + : NSLocalizedString("set new contact address", comment: "profile update event chat item") + } else { + // shouldn't happen if backend correctly creates item; UI should be synchronized with backend + NSLocalizedString("updated profile", comment: "profile update event chat item") } } } -public enum RcvGroupEvent: Decodable { +public enum RcvGroupEvent: Decodable, Hashable { case memberAdded(groupMemberId: Int64, profile: Profile) case memberConnected case memberLeft case memberRole(groupMemberId: Int64, profile: Profile, role: GroupMemberRole) + case memberBlocked(groupMemberId: Int64, profile: Profile, blocked: Bool) case userRole(role: GroupMemberRole) case memberDeleted(groupMemberId: Int64, profile: Profile) case userDeleted @@ -3294,6 +4450,7 @@ public enum RcvGroupEvent: Decodable { case groupUpdated(groupProfile: GroupProfile) case invitedViaGroupLink case memberCreatedContact + case memberProfileUpdated(fromProfile: Profile, toProfile: Profile) var text: String { switch self { @@ -3303,6 +4460,12 @@ public enum RcvGroupEvent: Decodable { case .memberLeft: return NSLocalizedString("left", comment: "rcv group event chat item") case let .memberRole(_, profile, role): return String.localizedStringWithFormat(NSLocalizedString("changed role of %@ to %@", comment: "rcv group event chat item"), profile.profileViewName, role.text) + case let .memberBlocked(_, profile, blocked): + if blocked { + return String.localizedStringWithFormat(NSLocalizedString("blocked %@", comment: "rcv group event chat item"), profile.profileViewName) + } else { + return String.localizedStringWithFormat(NSLocalizedString("unblocked %@", comment: "rcv group event chat item"), profile.profileViewName) + } case let .userRole(role): return String.localizedStringWithFormat(NSLocalizedString("changed your role to %@", comment: "rcv group event chat item"), role.text) case let .memberDeleted(_, profile): @@ -3312,13 +4475,28 @@ public enum RcvGroupEvent: Decodable { case .groupUpdated: return NSLocalizedString("updated group profile", comment: "rcv group event chat item") case .invitedViaGroupLink: return NSLocalizedString("invited via your group link", comment: "rcv group event chat item") case .memberCreatedContact: return NSLocalizedString("connected directly", comment: "rcv group event chat item") + case let .memberProfileUpdated(fromProfile, toProfile): return profileUpdatedText(fromProfile, toProfile) + } + } + + private func profileUpdatedText(_ from: Profile, _ to: Profile) -> String { + if to.displayName != from.displayName || to.fullName != from.fullName { + String.localizedStringWithFormat(NSLocalizedString("member %@ changed to %@", comment: "profile update event chat item"), from.profileViewName, to.profileViewName) + } else if to.image != from.image { + to.image == nil + ? NSLocalizedString("removed profile picture", comment: "profile update event chat item") + : NSLocalizedString("set new profile picture", comment: "profile update event chat item") + } else { + // shouldn't happen if backend correctly creates item; UI should be synchronized with backend + NSLocalizedString("updated profile", comment: "profile update event chat item") } } } -public enum SndGroupEvent: Decodable { +public enum SndGroupEvent: Decodable, Hashable { case memberRole(groupMemberId: Int64, profile: Profile, role: GroupMemberRole) case userRole(role: GroupMemberRole) + case memberBlocked(groupMemberId: Int64, profile: Profile, blocked: Bool) case memberDeleted(groupMemberId: Int64, profile: Profile) case userLeft case groupUpdated(groupProfile: GroupProfile) @@ -3329,6 +4507,12 @@ public enum SndGroupEvent: Decodable { return String.localizedStringWithFormat(NSLocalizedString("you changed role of %@ to %@", comment: "snd group event chat item"), profile.profileViewName, role.text) case let .userRole(role): return String.localizedStringWithFormat(NSLocalizedString("you changed role for yourself to %@", comment: "snd group event chat item"), role.text) + case let .memberBlocked(_, profile, blocked): + if blocked { + return String.localizedStringWithFormat(NSLocalizedString("you blocked %@", comment: "snd group event chat item"), profile.profileViewName) + } else { + return String.localizedStringWithFormat(NSLocalizedString("you unblocked %@", comment: "snd group event chat item"), profile.profileViewName) + } 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") @@ -3337,11 +4521,12 @@ public enum SndGroupEvent: Decodable { } } -public enum RcvConnEvent: Decodable { +public enum RcvConnEvent: Decodable, Hashable { case switchQueue(phase: SwitchPhase) case ratchetSync(syncStatus: RatchetSyncState) case verificationCodeReset - + case pqEnabled(enabled: Bool) + var text: String { switch self { case let .switchQueue(phase): @@ -3353,6 +4538,12 @@ public enum RcvConnEvent: Decodable { return ratchetSyncStatusToText(syncStatus) case .verificationCodeReset: return NSLocalizedString("security code changed", comment: "chat item text") + case let .pqEnabled(enabled): + if enabled { + return NSLocalizedString("quantum resistant e2e encryption", comment: "chat item text") + } else { + return NSLocalizedString("standard end-to-end encryption", comment: "chat item text") + } } } } @@ -3367,9 +4558,10 @@ func ratchetSyncStatusToText(_ ratchetSyncStatus: RatchetSyncState) -> String { } } -public enum SndConnEvent: Decodable { +public enum SndConnEvent: Decodable, Hashable { case switchQueue(phase: SwitchPhase, member: GroupMemberRef?) case ratchetSync(syncStatus: RatchetSyncState, member: GroupMemberRef?) + case pqEnabled(enabled: Bool) var text: String { switch self { @@ -3393,60 +4585,74 @@ public enum SndConnEvent: Decodable { } } return ratchetSyncStatusToText(syncStatus) + case let .pqEnabled(enabled): + if enabled { + return NSLocalizedString("quantum resistant e2e encryption", comment: "chat item text") + } else { + return NSLocalizedString("standard end-to-end encryption", comment: "chat item text") + } } } } -public enum SwitchPhase: String, Decodable { +public enum SwitchPhase: String, Decodable, Hashable { case started case confirmed case secured case completed } -public enum ChatItemTTL: Hashable, Identifiable, Comparable { +public enum ChatItemTTL: Identifiable, Comparable, Hashable { case day case week case month + case year case seconds(_ seconds: Int64) case none - public static var values: [ChatItemTTL] { [.none, .month, .week, .day] } + public static var values: [ChatItemTTL] { [.none, .year, .month, .week, .day] } public var id: Self { self } - public init(_ seconds: Int64?) { + public init(_ seconds: Int64) { switch seconds { + case 0: self = .none case 86400: self = .day case 7 * 86400: self = .week case 30 * 86400: self = .month - case let .some(n): self = .seconds(n) - case .none: self = .none + case 365 * 86400: self = .year + default: self = .seconds(seconds) } } - public var deleteAfterText: LocalizedStringKey { + public var deleteAfterText: String { switch self { - case .day: return "1 day" - case .week: return "1 week" - case .month: return "1 month" - case let .seconds(seconds): return "\(seconds) second(s)" - case .none: return "never" + case .day: return NSLocalizedString("1 day", comment: "delete after time") + case .week: return NSLocalizedString("1 week", comment: "delete after time") + case .month: return NSLocalizedString("1 month", comment: "delete after time") + case .year: return NSLocalizedString("1 year", comment: "delete after time") + case let .seconds(seconds): return String.localizedStringWithFormat(NSLocalizedString("%d seconds(s)", comment: "delete after time"), seconds) + case .none: return NSLocalizedString("never", comment: "delete after time") } } - public var seconds: Int64? { + public var seconds: Int64 { switch self { case .day: return 86400 case .week: return 7 * 86400 case .month: return 30 * 86400 + case .year: return 365 * 86400 case let .seconds(seconds): return seconds - case .none: return nil + case .none: return 0 } } private var comparisonValue: Int64 { - self.seconds ?? Int64.max + if self.seconds == 0 { + return Int64.max + } else { + return self.seconds + } } public static func < (lhs: Self, rhs: Self) -> Bool { @@ -3454,12 +4660,64 @@ public enum ChatItemTTL: Hashable, Identifiable, Comparable { } } -public struct ChatItemInfo: Decodable { - public var itemVersions: [ChatItemVersion] - public var memberDeliveryStatuses: [MemberDeliveryStatus]? +public enum ChatTTL: Identifiable, Hashable { + case userDefault(ChatItemTTL) + case chat(ChatItemTTL) + + public var id: Self { self } + + public var text: String { + switch self { + case let .chat(ttl): return ttl.deleteAfterText + case let .userDefault(ttl): return String.localizedStringWithFormat( + NSLocalizedString("default (%@)", comment: "delete after time"), + ttl.deleteAfterText) + } + } + + public var neverExpires: Bool { + switch self { + case let .chat(ttl): return ttl.seconds == 0 + case let .userDefault(ttl): return ttl.seconds == 0 + } + } + + public var value: Int64? { + switch self { + case let .chat(ttl): return ttl.seconds + case .userDefault: return nil + } + } + + public var usingDefault: Bool { + switch self { + case .userDefault: return true + case .chat: return false + } + } } -public struct ChatItemVersion: Decodable { +public struct ChatTag: Decodable, Hashable { + public var chatTagId: Int64 + public var chatTagText: String + public var chatTagEmoji: String? + + public var id: Int64 { chatTagId } + + public init(chatTagId: Int64, chatTagText: String, chatTagEmoji: String?) { + self.chatTagId = chatTagId + self.chatTagText = chatTagText + self.chatTagEmoji = chatTagEmoji + } +} + +public struct ChatItemInfo: Decodable, Hashable { + public var itemVersions: [ChatItemVersion] + public var memberDeliveryStatuses: [MemberDeliveryStatus]? + public var forwardedFromChatItem: AChatItem? +} + +public struct ChatItemVersion: Decodable, Hashable { public var chatItemVersionId: Int64 public var msgContent: MsgContent public var formattedText: [FormattedText]? @@ -3467,7 +4725,8 @@ public struct ChatItemVersion: Decodable { public var createdAt: Date } -public struct MemberDeliveryStatus: Decodable { +public struct MemberDeliveryStatus: Decodable, Hashable { public var groupMemberId: Int64 - public var memberDeliveryStatus: CIStatus + public var memberDeliveryStatus: GroupSndStatus + public var sentViaProxy: Bool? } diff --git a/apps/ios/SimpleXChat/ChatUtils.swift b/apps/ios/SimpleXChat/ChatUtils.swift new file mode 100644 index 0000000000..6cbc76ec98 --- /dev/null +++ b/apps/ios/SimpleXChat/ChatUtils.swift @@ -0,0 +1,119 @@ +// +// ChatUtils.swift +// SimpleXChat +// +// Created by Levitating Pineapple on 15/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation + +public protocol ChatLike { + var chatInfo: ChatInfo { get} + var chatItems: [ChatItem] { get } + var chatStats: ChatStats { get } +} + +extension ChatLike { + public func groupFeatureEnabled(_ feature: GroupFeature) -> Bool { + if case let .group(groupInfo) = self.chatInfo { + let p = groupInfo.fullGroupPreferences + return switch feature { + case .timedMessages: p.timedMessages.on + case .directMessages: p.directMessages.on(for: groupInfo.membership) + case .fullDelete: p.fullDelete.on + case .reactions: p.reactions.on + case .voice: p.voice.on(for: groupInfo.membership) + case .files: p.files.on(for: groupInfo.membership) + case .simplexLinks: p.simplexLinks.on(for: groupInfo.membership) + case .history: p.history.on + case .reports: p.reports.on + } + } else { + return true + } + } + + public func prohibitedByPref( + hasSimplexLink: Bool, + isMediaOrFileAttachment: Bool, + isVoice: Bool + ) -> Bool { + // preference checks should match checks in compose view + let simplexLinkProhibited = hasSimplexLink && !groupFeatureEnabled(.simplexLinks) + let fileProhibited = isMediaOrFileAttachment && !groupFeatureEnabled(.files) + let voiceProhibited = isVoice && !chatInfo.featureEnabled(.voice) + return switch chatInfo { + case .direct: voiceProhibited + case .group: simplexLinkProhibited || fileProhibited || voiceProhibited + case .local: false + case .contactRequest: false + case .contactConnection: false + case .invalidJSON: false + } + } +} + +public func filterChatsToForwardTo<C: ChatLike>(chats: [C]) -> [C] { + var filteredChats = chats.filter { c in + c.chatInfo.chatType != .local && canForwardToChat(c.chatInfo) + } + if let privateNotes = chats.first(where: { $0.chatInfo.chatType == .local }) { + filteredChats.insert(privateNotes, at: 0) + } + return filteredChats +} + +public func foundChat(_ chat: ChatLike, _ searchStr: String) -> Bool { + let cInfo = chat.chatInfo + return switch cInfo { + case let .direct(contact): + viewNameContains(cInfo, searchStr) || + contact.profile.displayName.localizedLowercase.contains(searchStr) || + contact.fullName.localizedLowercase.contains(searchStr) + default: + viewNameContains(cInfo, searchStr) + } + + func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool { + cInfo.chatViewName.localizedLowercase.contains(s) + } +} + +private func canForwardToChat(_ cInfo: ChatInfo) -> Bool { + switch cInfo { + case let .direct(contact): contact.sendMsgEnabled && !contact.nextSendGrpInv + case let .group(groupInfo): groupInfo.sendMsgEnabled + case let .local(noteFolder): noteFolder.sendMsgEnabled + case .contactRequest: false + case .contactConnection: false + case .invalidJSON: false + } +} + +public func chatIconName(_ cInfo: ChatInfo) -> String { + switch cInfo { + case .direct: "person.crop.circle.fill" + case let .group(groupInfo): + switch groupInfo.businessChat?.chatType { + case .none: "person.2.circle.fill" + case .business: "briefcase.circle.fill" + case .customer: "person.crop.circle.fill" + } + case .local: "folder.circle.fill" + case .contactRequest: "person.crop.circle.fill" + default: "circle.fill" + } +} + +public func hasSimplexLink(_ text: String?) -> Bool { + if let text, let parsedMsg = parseSimpleXMarkdown(text) { + parsedMsgHasSimplexLink(parsedMsg) + } else { + false + } +} + +public func parsedMsgHasSimplexLink(_ parsedMsg: [FormattedText]) -> Bool { + parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false }) +} diff --git a/apps/ios/SimpleXChat/CryptoFile.swift b/apps/ios/SimpleXChat/CryptoFile.swift index 0e539ba97c..dfe833f832 100644 --- a/apps/ios/SimpleXChat/CryptoFile.swift +++ b/apps/ios/SimpleXChat/CryptoFile.swift @@ -18,10 +18,10 @@ public func writeCryptoFile(path: String, data: Data) throws -> CryptoFileArgs { memcpy(ptr, (data as NSData).bytes, data.count) var cPath = path.cString(using: .utf8)! let cjson = chat_write_file(getChatCtrl(), &cPath, ptr, Int32(data.count))! - let d = fromCString(cjson).data(using: .utf8)! + let d = dataFromCString(cjson)! // TODO [unsafe] switch try jsonDecoder.decode(WriteFileResult.self, from: d) { case let .result(cfArgs): return cfArgs - case let .error(err): throw RuntimeError(err) + case let .error(err): throw RuntimeError(err) // TODO [unsafe] } } @@ -51,10 +51,10 @@ public func encryptCryptoFile(fromPath: String, toPath: String) throws -> Crypto var cFromPath = fromPath.cString(using: .utf8)! var cToPath = toPath.cString(using: .utf8)! let cjson = chat_encrypt_file(getChatCtrl(), &cFromPath, &cToPath)! - let d = fromCString(cjson).data(using: .utf8)! + let d = dataFromCString(cjson)! // TODO [unsafe] switch try jsonDecoder.decode(WriteFileResult.self, from: d) { case let .result(cfArgs): return cfArgs - case let .error(err): throw RuntimeError(err) + case let .error(err): throw RuntimeError(err) // TODO [unsafe] } } diff --git a/apps/ios/SimpleXChat/ErrorAlert.swift b/apps/ios/SimpleXChat/ErrorAlert.swift new file mode 100644 index 0000000000..a433d2313b --- /dev/null +++ b/apps/ios/SimpleXChat/ErrorAlert.swift @@ -0,0 +1,154 @@ +// +// ErrorAlert.swift +// SimpleXChat +// +// Created by Levitating Pineapple on 20/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +public struct ErrorAlert: Error { + public let title: LocalizedStringKey + public let message: LocalizedStringKey? + public let actions: Optional<() -> AnyView> + + public init( + title: LocalizedStringKey, + message: LocalizedStringKey? = nil + ) { + self.title = title + self.message = message + self.actions = nil + } + + public init<A: View>( + title: LocalizedStringKey, + message: LocalizedStringKey? = nil, + @ViewBuilder actions: @escaping () -> A + ) { + self.title = title + self.message = message + self.actions = { AnyView(actions()) } + } + + public init(_ title: LocalizedStringKey) { + self = ErrorAlert(title: title) + } + + public init(_ error: any Error) { + self = if let e = error as? ChatError { + ErrorAlert(e) + } else { + ErrorAlert("\(error.localizedDescription)") + } + } + + public init(_ chatError: ChatError) { + self = if let networkErrorAlert = getNetworkErrorAlert(chatError) { + networkErrorAlert + } else { + ErrorAlert("\(chatErrorString(chatError))") + } + } +} + +extension LocalizedStringKey: @unchecked Sendable { } + +extension View { + /// Bridges ``ErrorAlert`` to the generic alert API. + /// - Parameters: + /// - errorAlert: Binding to the Error, which is rendered in the alert + /// - actions: View Builder containing action buttons. + /// System defaults to `Ok` dismiss error action, when no actions are provided. + /// System implicitly adds `Cancel` action, if a destructive action is present + /// + /// - Returns: View, which displays ErrorAlert?, when set. + @ViewBuilder public func alert<A: View>( + _ errorAlert: Binding<ErrorAlert?>, + @ViewBuilder actions: (ErrorAlert) -> A = { _ in EmptyView() } + ) -> some View { + alert( + errorAlert.wrappedValue?.title ?? "", + isPresented: Binding<Bool>( + get: { errorAlert.wrappedValue != nil }, + set: { if !$0 { errorAlert.wrappedValue = nil } } + ), + actions: { + if let actions_ = errorAlert.wrappedValue?.actions { + actions_() + } else { + if let alert = errorAlert.wrappedValue { actions(alert) } + } + }, + message: { + if let message = errorAlert.wrappedValue?.message { + Text(message) + } + } + ) + } +} + +public func getNetworkErrorAlert(_ e: ChatError) -> ErrorAlert? { + switch e { + case let .errorAgent(.BROKER(addr, .TIMEOUT)): + ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.") + case let .errorAgent(.BROKER(addr, .NETWORK)): + ErrorAlert(title: "Connection error", message: "Please check your network connection with \(serverHostname(addr)) and try again.") + case let .errorAgent(.BROKER(addr, .HOST)): + ErrorAlert(title: "Connection error", message: "Server address is incompatible with network settings: \(serverHostname(addr)).") + case let .errorAgent(.BROKER(addr, .TRANSPORT(.version))): + ErrorAlert(title: "Connection error", message: "Server version is incompatible with your app: \(serverHostname(addr)).") + case let .errorAgent(.SMP(serverAddress, .PROXY(proxyErr))): + smpProxyErrorAlert(proxyErr, serverAddress) + case let .errorAgent(.PROXY(proxyServer, relayServer, .protocolError(.PROXY(proxyErr)))): + proxyDestinationErrorAlert(proxyErr, proxyServer, relayServer) + default: nil + } +} + +private func smpProxyErrorAlert(_ proxyErr: ProxyError, _ srvAddr: String) -> ErrorAlert? { + switch proxyErr { + case .BROKER(brokerErr: .TIMEOUT): + return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.") + case .BROKER(brokerErr: .NETWORK): + return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.") + case .BROKER(brokerErr: .HOST): + return ErrorAlert(title: "Private routing error", message: "Forwarding server address is incompatible with network settings: \(serverHostname(srvAddr)).") + case .BROKER(brokerErr: .TRANSPORT(.version)): + return ErrorAlert(title: "Private routing error", message: "Forwarding server version is incompatible with network settings: \(serverHostname(srvAddr)).") + default: + return nil + } +} + +private func proxyDestinationErrorAlert(_ proxyErr: ProxyError, _ proxyServer: String, _ relayServer: String) -> ErrorAlert? { + switch proxyErr { + case .BROKER(brokerErr: .TIMEOUT): + return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + case .BROKER(brokerErr: .NETWORK): + return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + case .NO_SESSION: + return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + case .BROKER(brokerErr: .HOST): + return ErrorAlert(title: "Private routing error", message: "Destination server address of \(serverHostname(relayServer)) is incompatible with forwarding server \(serverHostname(proxyServer)) settings.") + case .BROKER(brokerErr: .TRANSPORT(.version)): + return ErrorAlert(title: "Private routing error", message: "Destination server version of \(serverHostname(relayServer)) is incompatible with forwarding server \(serverHostname(proxyServer)).") + default: + return nil + } +} + +public func serverHostname(_ srv: String) -> String { + parseServerAddress(srv)?.hostnames.first ?? srv +} + +public func mtrErrorDescription(_ err: MTRError) -> LocalizedStringKey { + switch err { + case let .noDown(dbMigrations): + "database version is newer than the app, but no down migration for: \(dbMigrations.joined(separator: ", "))" + case let .different(appMigration, dbMigration): + "different migration in the app/database: \(appMigration) / \(dbMigration)" + } +} diff --git a/apps/ios/SimpleXChat/FileUtils.swift b/apps/ios/SimpleXChat/FileUtils.swift index 60d281f146..2341eb4a4f 100644 --- a/apps/ios/SimpleXChat/FileUtils.swift +++ b/apps/ios/SimpleXChat/FileUtils.swift @@ -8,6 +8,7 @@ import Foundation import OSLog +import UIKit let logger = Logger() @@ -22,13 +23,15 @@ public let MAX_VIDEO_SIZE_AUTO_RCV: Int64 = 1_047_552 // 1023KB public let MAX_FILE_SIZE_XFTP: Int64 = 1_073_741_824 // 1GB +public let MAX_FILE_SIZE_LOCAL: Int64 = Int64.max + public let MAX_FILE_SIZE_SMP: Int64 = 8000000 public let MAX_VOICE_MESSAGE_LENGTH = TimeInterval(300) -private let CHAT_DB: String = "_chat.db" +let CHAT_DB: String = "_chat.db" -private let AGENT_DB: String = "_agent.db" +let AGENT_DB: String = "_agent.db" private let CHAT_DB_BAK: String = "_chat.db.bak" @@ -38,7 +41,7 @@ public func getDocumentsDirectory() -> URL { FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! } -func getGroupContainerDirectory() -> URL { +public func getGroupContainerDirectory() -> URL { FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_NAME)! } @@ -69,13 +72,32 @@ func fileModificationDate(_ path: String) -> Date? { } } +public func deleteAppDatabaseAndFiles() { + let fm = FileManager.default + let dbPath = getAppDatabasePath().path + do { + try fm.removeItem(atPath: dbPath + CHAT_DB) + try fm.removeItem(atPath: dbPath + AGENT_DB) + } catch let error { + logger.error("Failed to delete all databases: \(error)") + } + try? fm.removeItem(atPath: dbPath + CHAT_DB_BAK) + try? fm.removeItem(atPath: dbPath + AGENT_DB_BAK) + try? fm.removeItem(at: getTempFilesDirectory()) + try? fm.removeItem(at: getMigrationTempFilesDirectory()) + try? fm.createDirectory(at: getTempFilesDirectory(), withIntermediateDirectories: true) + try? fm.removeItem(at: getWallpaperDirectory()) + try? fm.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) + deleteAppFiles() + _ = kcDatabasePassword.remove() + storeDBPassphraseGroupDefault.set(true) +} + public func deleteAppFiles() { let fm = FileManager.default do { - let fileNames = try fm.contentsOfDirectory(atPath: getAppFilesDirectory().path) - for fileName in fileNames { - removeFile(fileName) - } + try fm.removeItem(at: getAppFilesDirectory()) + try fm.createDirectory(at: getAppFilesDirectory(), withIntermediateDirectories: true) } catch { logger.error("FileUtils deleteAppFiles error: \(error.localizedDescription)") } @@ -165,6 +187,10 @@ public func getTempFilesDirectory() -> URL { getAppDirectory().appendingPathComponent("temp_files", isDirectory: true) } +public func getMigrationTempFilesDirectory() -> URL { + getDocumentsDirectory().appendingPathComponent("migration_temp_files", isDirectory: true) +} + public func getAppFilesDirectory() -> URL { getAppDirectory().appendingPathComponent("app_files", isDirectory: true) } @@ -173,6 +199,14 @@ public func getAppFilePath(_ fileName: String) -> URL { getAppFilesDirectory().appendingPathComponent(fileName) } +public func getWallpaperDirectory() -> URL { + getAppDirectory().appendingPathComponent("assets", isDirectory: true).appendingPathComponent("wallpapers", isDirectory: true) +} + +public func getWallpaperFilePath(_ filename: String) -> URL { + getWallpaperDirectory().appendingPathComponent(filename) +} + public func saveFile(_ data: Data, _ fileName: String, encrypted: Bool) -> CryptoFile? { let filePath = getAppFilePath(fileName) do { @@ -224,6 +258,7 @@ public func getMaxFileSize(_ fileProtocol: FileProtocol) -> Int64 { switch fileProtocol { case .xftp: return MAX_FILE_SIZE_XFTP case .smp: return MAX_FILE_SIZE_SMP + case .local: return MAX_FILE_SIZE_LOCAL } } diff --git a/apps/ios/Shared/Model/ImageUtils.swift b/apps/ios/SimpleXChat/ImageUtils.swift similarity index 52% rename from apps/ios/Shared/Model/ImageUtils.swift rename to apps/ios/SimpleXChat/ImageUtils.swift index 41d741e7e6..be43158bc1 100644 --- a/apps/ios/Shared/Model/ImageUtils.swift +++ b/apps/ios/SimpleXChat/ImageUtils.swift @@ -7,18 +7,19 @@ // import Foundation -import SimpleXChat import SwiftUI import AVKit +import SwiftyGif +import LinkPresentation -func getLoadedFileSource(_ file: CIFile?) -> CryptoFile? { +public func getLoadedFileSource(_ file: CIFile?) -> CryptoFile? { if let file = file, file.loaded { return file.fileSource } return nil } -func getLoadedImage(_ file: CIFile?) -> UIImage? { +public func getLoadedImage(_ file: CIFile?) -> UIImage? { if let fileSource = getLoadedFileSource(file) { let filePath = getAppFilePath(fileSource.filePath) do { @@ -37,7 +38,7 @@ func getLoadedImage(_ file: CIFile?) -> UIImage? { return nil } -func getFileData(_ path: URL, _ cfArgs: CryptoFileArgs?) throws -> Data { +public func getFileData(_ path: URL, _ cfArgs: CryptoFileArgs?) throws -> Data { if let cfArgs = cfArgs { return try readCryptoFile(path: path.path, cryptoArgs: cfArgs) } else { @@ -45,7 +46,7 @@ func getFileData(_ path: URL, _ cfArgs: CryptoFileArgs?) throws -> Data { } } -func getLoadedVideo(_ file: CIFile?) -> URL? { +public func getLoadedVideo(_ file: CIFile?) -> URL? { if let fileSource = getLoadedFileSource(file) { let filePath = getAppFilePath(fileSource.filePath) if FileManager.default.fileExists(atPath: filePath.path) { @@ -55,13 +56,13 @@ func getLoadedVideo(_ file: CIFile?) -> URL? { return nil } -func saveAnimImage(_ image: UIImage) -> CryptoFile? { +public func saveAnimImage(_ image: UIImage) -> CryptoFile? { let fileName = generateNewFileName("IMG", "gif") guard let imageData = image.imageData else { return nil } return saveFile(imageData, fileName, encrypted: privacyEncryptLocalFilesGroupDefault.get()) } -func saveImage(_ uiImage: UIImage) -> CryptoFile? { +public func saveImage(_ uiImage: UIImage) -> CryptoFile? { let hasAlpha = imageHasAlpha(uiImage) let ext = hasAlpha ? "png" : "jpg" if let imageDataResized = resizeImageToDataSize(uiImage, maxDataSize: MAX_IMAGE_SIZE, hasAlpha: hasAlpha) { @@ -71,7 +72,7 @@ func saveImage(_ uiImage: UIImage) -> CryptoFile? { return nil } -func cropToSquare(_ image: UIImage) -> UIImage { +public func cropToSquare(_ image: UIImage) -> UIImage { let size = image.size let side = min(size.width, size.height) let newSize = CGSize(width: side, height: side) @@ -84,7 +85,7 @@ func cropToSquare(_ image: UIImage) -> UIImage { return resizeImage(image, newBounds: CGRect(origin: .zero, size: newSize), drawIn: CGRect(origin: origin, size: size), hasAlpha: imageHasAlpha(image)) } -func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64, hasAlpha: Bool) -> Data? { +public func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64, hasAlpha: Bool) -> Data? { var img = image var data = hasAlpha ? img.pngData() : img.jpegData(compressionQuality: 0.85) var dataSize = data?.count ?? 0 @@ -99,7 +100,7 @@ func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64, hasAlpha: Bool) return data } -func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String? { +public func resizeImageToStrSizeSync(_ image: UIImage, maxDataSize: Int64) -> String? { var img = image let hasAlpha = imageHasAlpha(image) var str = compressImageStr(img, hasAlpha: hasAlpha) @@ -115,7 +116,15 @@ func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String? { return str } -func compressImageStr(_ image: UIImage, _ compressionQuality: CGFloat = 0.85, hasAlpha: Bool) -> String? { +public func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) async -> String? { + resizeImageToStrSizeSync(image, maxDataSize: maxDataSize) +} + +public func compressImageStr(_ image: UIImage, _ compressionQuality: CGFloat = 0.85, hasAlpha: Bool) -> String? { +// // Heavy workload to verify if UI gets blocked by the call +// for i in 0..<100 { +// print(image.jpegData(compressionQuality: Double(i) / 100)?.count ?? 0, terminator: ", ") +// } let ext = hasAlpha ? "png" : "jpg" if let data = hasAlpha ? image.pngData() : image.jpegData(compressionQuality: compressionQuality) { return "data:image/\(ext);base64,\(data.base64EncodedString())" @@ -129,7 +138,7 @@ private func reduceSize(_ image: UIImage, ratio: CGFloat, hasAlpha: Bool) -> UII return resizeImage(image, newBounds: bounds, drawIn: bounds, hasAlpha: hasAlpha) } -private func resizeImage(_ image: UIImage, newBounds: CGRect, drawIn: CGRect, hasAlpha: Bool) -> UIImage { +public func resizeImage(_ image: UIImage, newBounds: CGRect, drawIn: CGRect, hasAlpha: Bool) -> UIImage { let format = UIGraphicsImageRendererFormat() format.scale = 1.0 format.opaque = !hasAlpha @@ -138,7 +147,7 @@ private func resizeImage(_ image: UIImage, newBounds: CGRect, drawIn: CGRect, ha } } -func imageHasAlpha(_ img: UIImage) -> Bool { +public func imageHasAlpha(_ img: UIImage) -> Bool { if let cgImage = img.cgImage { let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue) @@ -158,7 +167,36 @@ func imageHasAlpha(_ img: UIImage) -> Bool { return false } -func saveFileFromURL(_ url: URL, encrypted: Bool) -> CryptoFile? { +/// Reduces image size, while consuming less RAM +/// +/// Used by ShareExtension to downsize large images +/// before passing them to regular image processing pipeline +/// to avoid exceeding 120MB memory +/// +/// - Parameters: +/// - url: Location of the image data +/// - size: Maximum dimension (width or height) +/// - Returns: Downsampled image or `nil`, if the image can't be located +public func downsampleImage(at url: URL, to size: Int64) -> UIImage? { + autoreleasepool { + if let source = CGImageSourceCreateWithURL(url as CFURL, nil) { + CGImageSourceCreateThumbnailAtIndex( + source, + 0, + [ + kCGImageSourceCreateThumbnailFromImageAlways: true, + kCGImageSourceShouldCacheImmediately: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceThumbnailMaxPixelSize: String(size) as CFString + ] as CFDictionary + ) + .map { UIImage(cgImage: $0) } + } else { nil } + } +} + +public func saveFileFromURL(_ url: URL) -> CryptoFile? { + let encrypted = privacyEncryptLocalFilesGroupDefault.get() let savedFile: CryptoFile? if url.startAccessingSecurityScopedResource() { do { @@ -183,19 +221,73 @@ func saveFileFromURL(_ url: URL, encrypted: Bool) -> CryptoFile? { return savedFile } -func moveTempFileFromURL(_ url: URL) -> CryptoFile? { +public func moveTempFileFromURL(_ url: URL) -> CryptoFile? { do { + let encrypted = privacyEncryptLocalFilesGroupDefault.get() let fileName = uniqueCombine(url.lastPathComponent) - try FileManager.default.moveItem(at: url, to: getAppFilePath(fileName)) - ChatModel.shared.filesToDelete.remove(url) - return CryptoFile.plain(fileName) + let savedFile: CryptoFile? + if encrypted { + let cfArgs = try encryptCryptoFile(fromPath: url.path, toPath: getAppFilePath(fileName).path) + try FileManager.default.removeItem(atPath: url.path) + savedFile = CryptoFile(filePath: fileName, cryptoArgs: cfArgs) + } else { + try FileManager.default.moveItem(at: url, to: getAppFilePath(fileName)) + savedFile = CryptoFile.plain(fileName) + } + return savedFile } catch { logger.error("ImageUtils.moveTempFileFromURL error: \(error.localizedDescription)") return nil } } -func generateNewFileName(_ prefix: String, _ ext: String, fullPath: Bool = false) -> String { +public func saveWallpaperFile(url: URL) -> String? { + let destFile = URL(fileURLWithPath: generateNewFileName(getWallpaperDirectory().path + "/" + "wallpaper", "jpg", fullPath: true)) + do { + try FileManager.default.copyItem(atPath: url.path, toPath: destFile.path) + return destFile.lastPathComponent + } catch { + logger.error("FileUtils.saveWallpaperFile error: \(error.localizedDescription)") + return nil + } +} + +public func saveWallpaperFile(image: UIImage) -> String? { + let hasAlpha = imageHasAlpha(image) + let destFile = URL(fileURLWithPath: generateNewFileName(getWallpaperDirectory().path + "/" + "wallpaper", hasAlpha ? "png" : "jpg", fullPath: true)) + let dataResized = resizeImageToDataSize(image, maxDataSize: 5_000_000, hasAlpha: hasAlpha) + do { + try dataResized!.write(to: destFile) + return destFile.lastPathComponent + } catch { + logger.error("FileUtils.saveWallpaperFile error: \(error.localizedDescription)") + return nil + } +} + +public func removeWallpaperFile(fileName: String? = nil) { + do { + try FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: getWallpaperDirectory().path), includingPropertiesForKeys: nil, options: []).forEach { url in + if url.lastPathComponent == fileName { + try FileManager.default.removeItem(at: url) + } + } + } catch { + logger.error("FileUtils.removeWallpaperFile error: \(error)") + } + if let fileName { + WallpaperType.cachedImages.removeValue(forKey: fileName) + } +} + +public func removeWallpaperFilesFromTheme(_ theme: ThemeModeOverrides?) { + if let theme { + removeWallpaperFile(fileName: theme.light?.wallpaper?.imageFile) + removeWallpaperFile(fileName: theme.dark?.wallpaper?.imageFile) + } +} + +public func generateNewFileName(_ prefix: String, _ ext: String, fullPath: Bool = false) -> String { uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)", fullPath: fullPath) } @@ -227,7 +319,7 @@ private func getTimestamp() -> String { return df.string(from: Date()) } -func dropImagePrefix(_ s: String) -> String { +public func dropImagePrefix(_ s: String) -> String { dropPrefix(dropPrefix(s, "data:image/png;base64,"), "data:image/jpg;base64,") } @@ -235,8 +327,23 @@ private func dropPrefix(_ s: String, _ prefix: String) -> String { s.hasPrefix(prefix) ? String(s.dropFirst(prefix.count)) : s } +public func makeVideoQualityLower(_ input: URL, outputUrl: URL) async -> Bool { + let asset: AVURLAsset = AVURLAsset(url: input, options: nil) + if let s = AVAssetExportSession(asset: asset, presetName: AVAssetExportPreset640x480) { + s.outputURL = outputUrl + s.outputFileType = .mp4 + s.metadataItemFilter = AVMetadataItemFilter.forSharing() + await s.export() + if let err = s.error { + logger.error("Failed to export video with error: \(err)") + } + return s.status == .completed + } + return false +} + extension AVAsset { - func generatePreview() -> (UIImage, Int)? { + public func generatePreview() -> (UIImage, Int)? { let generator = AVAssetImageGenerator(asset: self) generator.appliesPreferredTrackTransform = true var actualTime = CMTimeMake(value: 0, timescale: 0) @@ -248,7 +355,7 @@ extension AVAsset { } extension UIImage { - func replaceColor(_ from: UIColor, _ to: UIColor) -> UIImage { + public func replaceColor(_ from: UIColor, _ to: UIColor) -> UIImage { if let cgImage = cgImage { let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue) @@ -294,3 +401,92 @@ extension UIImage { return self } } + +public func imageFromBase64(_ base64Encoded: String?) -> UIImage? { + if let base64Encoded { + if let img = imageCache.object(forKey: base64Encoded as NSString) { + return img + } else if let data = Data(base64Encoded: dropImagePrefix(base64Encoded)), + let img = UIImage(data: data) { + imageCacheQueue.async { + imageCache.setObject(img, forKey: base64Encoded as NSString) + } + return img + } else { + return nil + } + } else { + return nil + } +} + +private let imageCacheQueue = DispatchQueue.global(qos: .background) + +private var imageCache: NSCache<NSString, UIImage> = { + var cache = NSCache<NSString, UIImage>() + cache.countLimit = 1000 + return cache +}() + +public func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) { + logger.debug("getLinkMetadata: fetching URL preview") + LPMetadataProvider().startFetchingMetadata(for: url){ metadata, error in + if let e = error { + logger.error("Error retrieving link metadata: \(e.localizedDescription)") + } + if let metadata = metadata, + let imageProvider = metadata.imageProvider, + imageProvider.canLoadObject(ofClass: UIImage.self) { + imageProvider.loadObject(ofClass: UIImage.self){ object, error in + var linkPreview: LinkPreview? = nil + if let error = error { + logger.error("Couldn't load image preview from link metadata with error: \(error.localizedDescription)") + } else { + if let image = object as? UIImage, + let resized = resizeImageToStrSizeSync(image, maxDataSize: 14000), + let title = metadata.title, + let uri = metadata.originalURL { + linkPreview = LinkPreview(uri: uri, title: title, image: resized) + } + } + cb(linkPreview) + } + } else { + logger.error("Could not load link preview image") + cb(nil) + } + } +} + +public func getLinkPreview(for url: URL) async -> LinkPreview? { + await withCheckedContinuation { cont in + getLinkPreview(url: url) { cont.resume(returning: $0) } + } +} + +private let squareToCircleRatio = 0.935 + +private let radiusFactor = (1 - squareToCircleRatio) / 50 + +@ViewBuilder public func clipProfileImage(_ img: Image, size: CGFloat, radius: Double, blurred: Bool = false) -> some View { + if radius >= 50 { + blurredFrame(img, size, blurred).clipShape(Circle()) + } else if radius <= 0 { + let sz = size * squareToCircleRatio + blurredFrame(img, sz, blurred).padding((size - sz) / 2) + } else { + let sz = size * (squareToCircleRatio + radius * radiusFactor) + blurredFrame(img, sz, blurred) + .clipShape(RoundedRectangle(cornerRadius: sz * radius / 100, style: .continuous)) + .padding((size - sz) / 2) + } +} + +@ViewBuilder private func blurredFrame(_ img: Image, _ size: CGFloat, _ blurred: Bool) -> some View { + let v = img.resizable().frame(width: size, height: size) + if blurred { + v.blur(radius: size / 4) + } else { + v + } +} diff --git a/apps/ios/SimpleXChat/Notifications.swift b/apps/ios/SimpleXChat/Notifications.swift index bc959cb34b..5579449caa 100644 --- a/apps/ios/SimpleXChat/Notifications.swift +++ b/apps/ios/SimpleXChat/Notifications.swift @@ -15,13 +15,14 @@ public let ntfCategoryContactConnected = "NTF_CAT_CONTACT_CONNECTED" public let ntfCategoryMessageReceived = "NTF_CAT_MESSAGE_RECEIVED" public let ntfCategoryCallInvitation = "NTF_CAT_CALL_INVITATION" public let ntfCategoryConnectionEvent = "NTF_CAT_CONNECTION_EVENT" +public let ntfCategoryManyEvents = "NTF_CAT_MANY_EVENTS" public let ntfCategoryCheckMessage = "NTF_CAT_CHECK_MESSAGE" public let appNotificationId = "chat.simplex.app.notification" let contactHidden = NSLocalizedString("Contact hidden:", comment: "notification") -public func createContactRequestNtf(_ user: any UserLike, _ contactRequest: UserContactRequest) -> UNMutableNotificationContent { +public func createContactRequestNtf(_ user: any UserLike, _ contactRequest: UserContactRequest, _ badgeCount: Int) -> UNMutableNotificationContent { let hideContent = ntfPreviewModeGroupDefault.get() == .hidden return createNotification( categoryIdentifier: ntfCategoryContactRequest, @@ -34,11 +35,12 @@ public func createContactRequestNtf(_ user: any UserLike, _ contactRequest: User hideContent ? NSLocalizedString("this contact", comment: "notification title") : contactRequest.chatViewName ), targetContentIdentifier: nil, - userInfo: ["chatId": contactRequest.id, "contactRequestId": contactRequest.apiId, "userId": user.userId] + userInfo: ["chatId": contactRequest.id, "contactRequestId": contactRequest.apiId, "userId": user.userId], + badgeCount: badgeCount ) } -public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact) -> UNMutableNotificationContent { +public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact, _ badgeCount: Int) -> UNMutableNotificationContent { let hideContent = ntfPreviewModeGroupDefault.get() == .hidden return createNotification( categoryIdentifier: ntfCategoryContactConnected, @@ -47,16 +49,17 @@ public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact) hideContent ? NSLocalizedString("A new contact", comment: "notification title") : contact.displayName ), body: String.localizedStringWithFormat( - NSLocalizedString("You can now send messages to %@", comment: "notification body"), + NSLocalizedString("You can now chat with %@", comment: "notification body"), hideContent ? NSLocalizedString("this contact", comment: "notification title") : contact.chatViewName ), targetContentIdentifier: contact.id, - userInfo: ["userId": user.userId] + userInfo: ["userId": user.userId], // userInfo: ["chatId": contact.id, "contactId": contact.apiId] + badgeCount: badgeCount ) } -public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent { +public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem, _ badgeCount: Int) -> UNMutableNotificationContent { let previewMode = ntfPreviewModeGroupDefault.get() var title: String if case let .group(groupInfo) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir { @@ -69,12 +72,13 @@ public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ title: title, body: previewMode == .message ? hideSecrets(cItem) : NSLocalizedString("new message", comment: "notification"), targetContentIdentifier: cInfo.id, - userInfo: ["userId": user.userId] + userInfo: ["userId": user.userId], // userInfo: ["chatId": cInfo.id, "chatItemId": cItem.id] + badgeCount: badgeCount ) } -public func createCallInvitationNtf(_ invitation: RcvCallInvitation) -> UNMutableNotificationContent { +public func createCallInvitationNtf(_ invitation: RcvCallInvitation, _ badgeCount: Int) -> UNMutableNotificationContent { let text = invitation.callType.media == .video ? NSLocalizedString("Incoming video call", comment: "notification") : NSLocalizedString("Incoming audio call", comment: "notification") @@ -84,17 +88,18 @@ public func createCallInvitationNtf(_ invitation: RcvCallInvitation) -> UNMutabl title: hideContent ? contactHidden : "\(invitation.contact.chatViewName):", body: text, targetContentIdentifier: nil, - userInfo: ["chatId": invitation.contact.id, "userId": invitation.user.userId] + userInfo: ["chatId": invitation.contact.id, "userId": invitation.user.userId], + badgeCount: badgeCount ) } -public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntity) -> UNMutableNotificationContent { +public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntity, _ badgeCount: Int) -> UNMutableNotificationContent { let hideContent = ntfPreviewModeGroupDefault.get() == .hidden var title: String var body: String? = nil var targetContentIdentifier: String? = nil switch connEntity { - case let .rcvDirectMsgConnection(contact): + case let .rcvDirectMsgConnection(_, contact): if let contact = contact { title = hideContent ? contactHidden : "\(contact.chatViewName):" targetContentIdentifier = contact.id @@ -102,7 +107,7 @@ public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntit title = NSLocalizedString("New contact:", comment: "notification") } body = NSLocalizedString("message received", comment: "notification") - case let .rcvGroupMsgConnection(groupInfo, groupMember): + case let .rcvGroupMsgConnection(_, groupInfo, groupMember): title = groupMsgNtfTitle(groupInfo, groupMember, hideContent: hideContent) body = NSLocalizedString("message received", comment: "notification") targetContentIdentifier = groupInfo.id @@ -118,11 +123,12 @@ public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntit title: title, body: body, targetContentIdentifier: targetContentIdentifier, - userInfo: ["userId": user.userId] + userInfo: ["userId": user.userId], + badgeCount: badgeCount ) } -public func createErrorNtf(_ dbStatus: DBMigrationResult) -> UNMutableNotificationContent { +public func createErrorNtf(_ dbStatus: DBMigrationResult, _ badgeCount: Int) -> UNMutableNotificationContent { var title: String switch dbStatus { case .errorNotADatabase: @@ -142,14 +148,16 @@ public func createErrorNtf(_ dbStatus: DBMigrationResult) -> UNMutableNotificati } return createNotification( categoryIdentifier: ntfCategoryConnectionEvent, - title: title + title: title, + badgeCount: badgeCount ) } -public func createAppStoppedNtf() -> UNMutableNotificationContent { +public func createAppStoppedNtf(_ badgeCount: Int) -> UNMutableNotificationContent { return createNotification( categoryIdentifier: ntfCategoryConnectionEvent, - title: NSLocalizedString("Encrypted message: app is stopped", comment: "notification") + title: NSLocalizedString("Encrypted message: app is stopped", comment: "notification"), + badgeCount: badgeCount ) } @@ -159,8 +167,15 @@ private func groupMsgNtfTitle(_ groupInfo: GroupInfo, _ groupMember: GroupMember : "#\(groupInfo.displayName) \(groupMember.chatViewName):" } -public func createNotification(categoryIdentifier: String, title: String, subtitle: String? = nil, body: String? = nil, - targetContentIdentifier: String? = nil, userInfo: [AnyHashable : Any] = [:]) -> UNMutableNotificationContent { +public func createNotification( + categoryIdentifier: String, + title: String, + subtitle: String? = nil, + body: String? = nil, + targetContentIdentifier: String? = nil, + userInfo: [AnyHashable : Any] = [:], + badgeCount: Int +) -> UNMutableNotificationContent { let content = UNMutableNotificationContent() content.categoryIdentifier = categoryIdentifier content.title = title @@ -170,6 +185,7 @@ public func createNotification(categoryIdentifier: String, title: String, subtit content.userInfo = userInfo // TODO move logic of adding sound here, so it applies to background notifications too content.sound = .default + content.badge = badgeCount as NSNumber // content.interruptionLevel = .active // content.relevanceScore = 0.5 // 0-1 return content @@ -187,6 +203,11 @@ func hideSecrets(_ cItem: ChatItem) -> String { } return res } else { - return cItem.text + let mc = cItem.content.msgContent + 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 + } } } diff --git a/apps/ios/SimpleXChat/SharedFileSubscriber.swift b/apps/ios/SimpleXChat/SharedFileSubscriber.swift index f496e6999e..bf5997f40b 100644 --- a/apps/ios/SimpleXChat/SharedFileSubscriber.swift +++ b/apps/ios/SimpleXChat/SharedFileSubscriber.swift @@ -12,6 +12,8 @@ public typealias AppSubscriber = SharedFileSubscriber<ProcessMessage<AppProcessM public typealias NSESubscriber = SharedFileSubscriber<ProcessMessage<NSEProcessMessage>> +public typealias SESubscriber = SharedFileSubscriber<ProcessMessage<SEProcessMessage>> + public class SharedFileSubscriber<Message: Codable>: NSObject, NSFilePresenter { var fileURL: URL public var presentedItemURL: URL? @@ -57,6 +59,8 @@ let appMessagesSharedFile = getGroupContainerDirectory().appendingPathComponent( let nseMessagesSharedFile = getGroupContainerDirectory().appendingPathComponent("chat.simplex.app.SimpleX-NSE.messages", isDirectory: false) +let seMessagesSharedFile = getGroupContainerDirectory().appendingPathComponent("chat.simplex.app.SimpleX-SE.messages", isDirectory: false) + public struct ProcessMessage<Message: Codable>: Codable { var createdAt: Date = Date.now var message: Message @@ -70,6 +74,10 @@ public enum NSEProcessMessage: Codable { case state(state: NSEState) } +public enum SEProcessMessage: Codable { + case state(state: SEState) +} + public func sendAppProcessMessage(_ message: AppProcessMessage) { SharedFileSubscriber.notify(url: appMessagesSharedFile, message: ProcessMessage(message: message)) } @@ -78,6 +86,10 @@ public func sendNSEProcessMessage(_ message: NSEProcessMessage) { SharedFileSubscriber.notify(url: nseMessagesSharedFile, message: ProcessMessage(message: message)) } +public func sendSEProcessMessage(_ message: SEProcessMessage) { + SharedFileSubscriber.notify(url: seMessagesSharedFile, message: ProcessMessage(message: message)) +} + public func appMessageSubscriber(onMessage: @escaping (AppProcessMessage) -> Void) -> AppSubscriber { SharedFileSubscriber(fileURL: appMessagesSharedFile) { (msg: ProcessMessage<AppProcessMessage>) in onMessage(msg.message) @@ -90,6 +102,12 @@ public func nseMessageSubscriber(onMessage: @escaping (NSEProcessMessage) -> Voi } } +public func seMessageSubscriber(onMessage: @escaping (SEProcessMessage) -> Void) -> SESubscriber { + SharedFileSubscriber(fileURL: seMessagesSharedFile) { (msg: ProcessMessage<SEProcessMessage>) in + onMessage(msg.message) + } +} + public func sendAppState(_ state: AppState) { sendAppProcessMessage(.state(state: state)) } @@ -97,3 +115,7 @@ public func sendAppState(_ state: AppState) { public func sendNSEState(_ state: NSEState) { sendNSEProcessMessage(.state(state: state)) } + +public func sendSEState(_ state: SEState) { + sendSEProcessMessage(.state(state: state)) +} diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h index c49d104514..92dfafca21 100644 --- a/apps/ios/SimpleXChat/SimpleX.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -10,6 +10,7 @@ #define SimpleX_h #include "hs_init.h" +#include "objc.h" extern void hs_init(int argc, char **argv[]); @@ -25,6 +26,7 @@ extern char *chat_parse_markdown(char *str); extern char *chat_parse_server(char *str); extern char *chat_password_hash(char *pwd, char *salt); extern char *chat_valid_name(char *name); +extern int chat_json_length(char *str); extern char *chat_encrypt_media(chat_ctrl ctl, char *key, char *frame, int len); extern char *chat_decrypt_media(char *key, char *frame, int len); diff --git a/apps/ios/SimpleXChat/Theme/ChatWallpaperTypes.swift b/apps/ios/SimpleXChat/Theme/ChatWallpaperTypes.swift new file mode 100644 index 0000000000..662f8b43d1 --- /dev/null +++ b/apps/ios/SimpleXChat/Theme/ChatWallpaperTypes.swift @@ -0,0 +1,402 @@ +// +// ChatWallpaper.swift +// SimpleX (iOS) +// +// Created by Avently on 06.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI + +public enum PresetWallpaper: CaseIterable { + case cats + case flowers + case hearts + case kids + case school + case travel + + var res: UIImage { + UIImage(named: "wallpaper_\(filename)")! + } + + public var filename: String { + switch self { + case .cats: "cats" + case .flowers: "flowers" + case .hearts: "hearts" + case .kids: "kids" + case .school: "school" + case .travel: "travel" + } + } + + public var scale: Float { + switch self { + case .cats: 0.63 + case .flowers: 0.53 + case .hearts: 0.59 + case .kids: 0.53 + case .school: 0.53 + case .travel: 0.68 + } + } + + public var background: [DefaultTheme: Color] { + switch self { + case .cats: wallpaperBackgrounds(light: "#ffF8F6EA") + case .flowers: wallpaperBackgrounds(light: "#ffE2FFE4") + case .hearts: wallpaperBackgrounds(light: "#ffFDECEC") + case .kids: wallpaperBackgrounds(light: "#ffdbfdfb") + case .school: wallpaperBackgrounds(light: "#ffE7F5FF") + case .travel: wallpaperBackgrounds(light: "#fff9eeff") + } + } + + public var tint: [DefaultTheme: Color] { + switch self { + case .cats: [ + DefaultTheme.LIGHT: "#ffefdca6".colorFromReadableHex(), + DefaultTheme.DARK: "#ff4b3b0e".colorFromReadableHex(), + DefaultTheme.SIMPLEX: "#ff51400f".colorFromReadableHex(), + DefaultTheme.BLACK: "#ff4b3b0e".colorFromReadableHex() + ] + case .flowers: [ + DefaultTheme.LIGHT: "#ff9CEA59".colorFromReadableHex(), + DefaultTheme.DARK: "#ff31560D".colorFromReadableHex(), + DefaultTheme.SIMPLEX: "#ff36600f".colorFromReadableHex(), + DefaultTheme.BLACK: "#ff31560D".colorFromReadableHex() + ] + case .hearts: [ + DefaultTheme.LIGHT: "#fffde0e0".colorFromReadableHex(), + DefaultTheme.DARK: "#ff3c0f0f".colorFromReadableHex(), + DefaultTheme.SIMPLEX: "#ff411010".colorFromReadableHex(), + DefaultTheme.BLACK: "#ff3C0F0F".colorFromReadableHex() + ] + case .kids: [ + DefaultTheme.LIGHT: "#ffadeffc".colorFromReadableHex(), + DefaultTheme.DARK: "#ff16404B".colorFromReadableHex(), + DefaultTheme.SIMPLEX: "#ff184753".colorFromReadableHex(), + DefaultTheme.BLACK: "#ff16404B".colorFromReadableHex() + ] + case .school: [ + DefaultTheme.LIGHT: "#ffCEEBFF".colorFromReadableHex(), + DefaultTheme.DARK: "#ff0F293B".colorFromReadableHex(), + DefaultTheme.SIMPLEX: "#ff112f43".colorFromReadableHex(), + DefaultTheme.BLACK: "#ff0F293B".colorFromReadableHex() + ] + case .travel: [ + DefaultTheme.LIGHT: "#ffeedbfe".colorFromReadableHex(), + DefaultTheme.DARK: "#ff311E48".colorFromReadableHex(), + DefaultTheme.SIMPLEX: "#ff35204e".colorFromReadableHex(), + DefaultTheme.BLACK: "#ff311E48".colorFromReadableHex() + ] + } + } + + public var colors: [DefaultTheme: ThemeColors] { + switch self { + case .cats: [ + DefaultTheme.LIGHT: ThemeColors.from( + sentMessage: "#fffffaed", + sentQuote: "#fffaf0d6", + receivedMessage: "#ffF8F7F4", + receivedQuote: "#ffefede9" + ), + DefaultTheme.DARK: ThemeColors.from( + sentMessage: "#ff2f2919", + sentQuote: "#ff473a1d", + receivedMessage: "#ff272624", + receivedQuote: "#ff373633" + ), + DefaultTheme.SIMPLEX: ThemeColors.from( + sentMessage: "#ff41371b", + sentQuote: "#ff654f1c", + receivedMessage: "#ff272624", + receivedQuote: "#ff373633" + ), + DefaultTheme.BLACK: ThemeColors.from( + sentMessage: "#ff41371b", + sentQuote: "#ff654f1c", + receivedMessage: "#ff1f1e1b", + receivedQuote: "#ff2f2d27" + ) + ] + case .flowers: [ + DefaultTheme.LIGHT: ThemeColors.from( + sentMessage: "#fff1ffe5", + sentQuote: "#ffdcf9c4", + receivedMessage: "#ffF4F8F2", + receivedQuote: "#ffe7ece7" + ), + DefaultTheme.DARK: ThemeColors.from( + sentMessage: "#ff163521", + sentQuote: "#ff1B5330", + receivedMessage: "#ff242523", + receivedQuote: "#ff353733" + ), + DefaultTheme.SIMPLEX: ThemeColors.from( + sentMessage: "#ff184739", + sentQuote: "#ff1F6F4B", + receivedMessage: "#ff242523", + receivedQuote: "#ff353733" + ), + DefaultTheme.BLACK: ThemeColors.from( + sentMessage: "#ff184739", + sentQuote: "#ff1F6F4B", + receivedMessage: "#ff1c1f1a", + receivedQuote: "#ff282b25" + ) + ] + case .hearts: [ + DefaultTheme.LIGHT: ThemeColors.from( + sentMessage: "#fffff4f4", + sentQuote: "#ffffdfdf", + receivedMessage: "#fff8f6f6", + receivedQuote: "#ffefebeb" + ), + DefaultTheme.DARK: ThemeColors.from( + sentMessage: "#ff301515", + sentQuote: "#ff4C1818", + receivedMessage: "#ff242121", + receivedQuote: "#ff3b3535" + ), + DefaultTheme.SIMPLEX: ThemeColors.from( + sentMessage: "#ff491A28", + sentQuote: "#ff761F29", + receivedMessage: "#ff242121", + receivedQuote: "#ff3b3535" + ), + DefaultTheme.BLACK: ThemeColors.from( + sentMessage: "#ff491A28", + sentQuote: "#ff761F29", + receivedMessage: "#ff1f1b1b", + receivedQuote: "#ff2e2626" + ) + ] + case .kids: [ + DefaultTheme.LIGHT: ThemeColors.from( + sentMessage: "#ffeafeff", + sentQuote: "#ffcbf4f7", + receivedMessage: "#fff3fafa", + receivedQuote: "#ffe4efef" + ), + DefaultTheme.DARK: ThemeColors.from( + sentMessage: "#ff16302F", + sentQuote: "#ff1a4a49", + receivedMessage: "#ff252626", + receivedQuote: "#ff373A39" + ), + DefaultTheme.SIMPLEX: ThemeColors.from( + sentMessage: "#ff1a4745", + sentQuote: "#ff1d6b69", + receivedMessage: "#ff252626", + receivedQuote: "#ff373a39" + ), + DefaultTheme.BLACK: ThemeColors.from( + sentMessage: "#ff1a4745", + sentQuote: "#ff1d6b69", + receivedMessage: "#ff1e1f1f", + receivedQuote: "#ff262b29" + ) + ] + case .school: [ + DefaultTheme.LIGHT: ThemeColors.from( + sentMessage: "#ffeef9ff", + sentQuote: "#ffD6EDFA", + receivedMessage: "#ffF3F5F9", + receivedQuote: "#ffe4e8ee" + ), + DefaultTheme.DARK: ThemeColors.from( + sentMessage: "#ff172833", + sentQuote: "#ff1C3E4F", + receivedMessage: "#ff26282c", + receivedQuote: "#ff393c40" + ), + DefaultTheme.SIMPLEX: ThemeColors.from( + sentMessage: "#ff1A3C5D", + sentQuote: "#ff235b80", + receivedMessage: "#ff26282c", + receivedQuote: "#ff393c40" + ), + DefaultTheme.BLACK: ThemeColors.from( + sentMessage: "#ff1A3C5D", + sentQuote: "#ff235b80", + receivedMessage: "#ff1d1e22", + receivedQuote: "#ff292b2f" + ) + ] + case .travel: [ + DefaultTheme.LIGHT: ThemeColors.from( + sentMessage: "#fffcf6ff", + sentQuote: "#fff2e0fc", + receivedMessage: "#ffF6F4F7", + receivedQuote: "#ffede9ee" + ), + DefaultTheme.DARK: ThemeColors.from( + sentMessage: "#ff33263B", + sentQuote: "#ff53385E", + receivedMessage: "#ff272528", + receivedQuote: "#ff3B373E" + ), + DefaultTheme.SIMPLEX: ThemeColors.from( + sentMessage: "#ff3C255D", + sentQuote: "#ff623485", + receivedMessage: "#ff26273B", + receivedQuote: "#ff3A394F" + ), + DefaultTheme.BLACK: ThemeColors.from( + sentMessage: "#ff3C255D", + sentQuote: "#ff623485", + receivedMessage: "#ff231f23", + receivedQuote: "#ff2c2931" + ) + ] + } + } + + public static func from(_ filename: String) -> PresetWallpaper? { + switch filename { + case PresetWallpaper.cats.filename: PresetWallpaper.cats + case PresetWallpaper.flowers.filename: PresetWallpaper.flowers + case PresetWallpaper.hearts.filename: PresetWallpaper.hearts + case PresetWallpaper.kids.filename: PresetWallpaper.kids + case PresetWallpaper.school.filename: PresetWallpaper.school + case PresetWallpaper.travel.filename: PresetWallpaper.travel + default: nil + } + } +} + +func wallpaperBackgrounds(light: String) -> [DefaultTheme : Color] { + [ + DefaultTheme.LIGHT: light.colorFromReadableHex(), + DefaultTheme.DARK: "#ff121212".colorFromReadableHex(), + DefaultTheme.SIMPLEX: "#ff111528".colorFromReadableHex(), + DefaultTheme.BLACK: "#ff070707".colorFromReadableHex() + ] +} + +public enum WallpaperScaleType: String, Codable, CaseIterable { + case fill + case fit + case `repeat` + + public var text: String { + switch self { + case .fill: "Fill" + case .fit: "Fit" + case .repeat: "Repeat" + } + } + + public func computeScaleFactor(_ srcSize: CGSize, _ dstSize: CGSize) -> (CGFloat, CGFloat) { + switch self { + case .fill: + let widthScale = dstSize.width / srcSize.width + let heightScale = dstSize.height / srcSize.height + return (max(widthScale, heightScale), max(widthScale, heightScale)) + case .fit: fallthrough + case .repeat: + let widthScale = dstSize.width / srcSize.width + let heightScale = dstSize.height / srcSize.height + return (min(widthScale, heightScale), min(widthScale, heightScale)) + } + } +} + +public enum WallpaperType: Equatable { + public var image: SwiftUI.Image? { + if let uiImage { + return SwiftUI.Image(uiImage: uiImage) + } + return nil + } + + public var uiImage: UIImage? { + let filename: String + switch self { + case let .preset(f, _): filename = f + case let .image(f, _, _): filename = f + default: return nil + } + if filename == "" { return nil } + if let image = WallpaperType.cachedImages[filename] { + return image + } else { + let res: UIImage? + if case let .preset(filename, _) = self { + res = (PresetWallpaper.from(filename) ?? PresetWallpaper.cats).res + } else { + // In case of unintentional image deletion don't crash the app + res = UIImage(contentsOfFile: getWallpaperFilePath(filename).path) + } + if let res { + WallpaperType.cachedImages[filename] = res + } + return res + } + } + + public func sameType(_ other: WallpaperType?) -> Bool { + if case let .preset(filename, _) = self, case let .preset(otherFilename, _) = other { filename == otherFilename } + else if case .image = self, case .image = other { true } + else if case .empty = self, case .empty = other { true } + else { false } + } + + public var isPreset: Bool { switch self { case .preset: true; default: false } } + + public var isImage: Bool { switch self { case .image: true; default: false } } + + public var isEmpty: Bool { switch self { case .empty: true; default: false } } + + public var scale: Float { + switch self { + case let .preset(_, scale): scale ?? 1 + case let .image(_, scale, _): scale ?? 1 + case .empty: 1 + } + } + + public func samePreset(other: PresetWallpaper?) -> Bool { if case let .preset(filename, _) = self, filename == other?.filename { true } else { false } } + + case preset(_ filename: String, _ scale: Float?) + + case image(_ filename: String, _ scale: Float?, _ scaleType: WallpaperScaleType?) + + case empty + + public func defaultBackgroundColor(_ theme: DefaultTheme, _ themeBackground: Color) -> Color { + if case let .preset(filename, _) = self { + (PresetWallpaper.from(filename) ?? PresetWallpaper.cats).background[theme]! + } else { + themeBackground + } + } + + public func defaultTintColor(_ theme: DefaultTheme) -> Color { + if case let .preset(filename, _) = self { + (PresetWallpaper.from(filename) ?? PresetWallpaper.cats).tint[theme]! + } else if case let .image(_, _, scaleType) = self, scaleType == WallpaperScaleType.repeat { + Color.clear + } else { + Color.clear + } + } + + public static var cachedImages: [String: UIImage] = [:] + + public static func from(_ wallpaper: ThemeWallpaper?) -> WallpaperType? { + if wallpaper == nil { + return nil + } else if let preset = wallpaper?.preset { + return WallpaperType.preset(preset, wallpaper?.scale) + } else if let imageFile = wallpaper?.imageFile { + return WallpaperType.image(imageFile, wallpaper?.scale, wallpaper?.scaleType) + } else { + return WallpaperType.empty + } + } +} diff --git a/apps/ios/SimpleXChat/Theme/Color.swift b/apps/ios/SimpleXChat/Theme/Color.swift new file mode 100644 index 0000000000..f307eaa5aa --- /dev/null +++ b/apps/ios/SimpleXChat/Theme/Color.swift @@ -0,0 +1,131 @@ +// +// Color.swift +// SimpleX (iOS) +// +// Created by Avently on 05.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI + +//let Purple200 = Color(0xFFBB86FC) +//let Purple500 = Color(0xFF6200EE) +//let Purple700 = Color(0xFF3700B3) +//let Teal200 = Color(0xFF03DAC5) +//let Gray = Color(0x22222222) +//let Indigo = Color(0xFF9966FF) +let SimplexBlue = Color(0, 136, 255, a: 255) +//let SimplexGreen = Color(77, 218, 103, a: 255) +//let SecretColor = Color(0x40808080) +let LightGray = Color(241, 242, 246, a: 255) +let DarkGray = Color(43, 44, 46, a: 255) +let HighOrLowlight = Color(139, 135, 134, a: 255) +//let MessagePreviewDark = Color(179, 175, 174, a: 255) +//let MessagePreviewLight = Color(49, 45, 44, a: 255) +//let ToolbarLight = Color(220, 220, 220, a: 12) +//let ToolbarDark = Color(80, 80, 80, a: 12) +//let SettingsSecondaryLight = Color(200, 196, 195, a: 90) +//let GroupDark = Color(80, 80, 80, a: 60) +//let IncomingCallLight = Color(239, 237, 236, a: 255) +//let WarningOrange = Color(255, 127, 0, a: 255) +//let WarningYellow = Color(255, 192, 0, a: 255) +//let FileLight = Color(183, 190, 199, a: 255) +//let FileDark = Color(101, 101, 106, a: 255) + +extension Color { + public init(_ argb: Int64) { + let a = Double((argb & 0xFF000000) >> 24) / 255.0 + let r = Double((argb & 0xFF0000) >> 16) / 255.0 + let g = Double((argb & 0xFF00) >> 8) / 255.0 + let b = Double((argb & 0xFF)) / 255.0 + self.init(.sRGB, red: r, green: g, blue: b, opacity: a) + } + + public init(_ r: Int, _ g: Int, _ b: Int, a: Int) { + self.init(.sRGB, red: Double(r) / 255.0, green: Double(g) / 255.0, blue: Double(b) / 255.0, opacity: Double(a) / 255.0) + } + + public func toReadableHex() -> String { + let uiColor: UIColor = .init(self) + var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0) + uiColor.getRed(&r, green: &g, blue: &b, alpha: &a) + // Can be negative values and more than 1. Extended color range, making it normal + r = min(1, max(0, r)) + g = min(1, max(0, g)) + b = min(1, max(0, b)) + a = min(1, max(0, a)) + return String(format: "#%02x%02x%02x%02x", + Int((a * 255).rounded()), + Int((r * 255).rounded()), + Int((g * 255).rounded()), + Int((b * 255).rounded()) + ) + } + + public func toHTMLHex() -> String { + let uiColor: UIColor = .init(self) + var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0) + uiColor.getRed(&r, green: &g, blue: &b, alpha: &a) + // Can be negative values and more than 1. Extended color range, making it normal + r = min(1, max(0, r)) + g = min(1, max(0, g)) + b = min(1, max(0, b)) + a = min(1, max(0, a)) + return String(format: "#%02x%02x%02x%02x", + Int((r * 255).rounded()), + Int((g * 255).rounded()), + Int((b * 255).rounded()), + Int((a * 255).rounded()) + ) + } + + public func darker(_ factor: CGFloat = 0.1) -> Color { + var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0) + UIColor(self).getRed(&r, green: &g, blue: &b, alpha: &a) + return Color(.sRGB, red: max(r * (1 - factor), 0), green: max(g * (1 - factor), 0), blue: max(b * (1 - factor), 0), opacity: a) + } + + public func lighter(_ factor: CGFloat = 0.1) -> Color { + var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0) + UIColor(self).getRed(&r, green: &g, blue: &b, alpha: &a) + return Color(.sRGB, red: min(r * (1 + factor), 1), green: min(g * (1 + factor), 1), blue: min(b * (1 + factor), 1), opacity: a) + } + + public func asGroupedBackground(_ mode: DefaultThemeMode) -> Color { + let uiColor: UIColor = .init(self) + var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0) + uiColor.getRed(&r, green: &g, blue: &b, alpha: &a) + return mode == DefaultThemeMode.light + ? Color(.sRGB, red: max(0, r - 0.052), green: max(0, g - 0.051), blue: max(0, b - 0.032), opacity: a) + : Color(.sRGB, red: min(1, r + 0.11), green: min(1, g + 0.11), blue: min(1, b + 0.115), opacity: a) + } +} + +extension String { + func colorFromReadableHex() -> Color { + // https://stackoverflow.com/a/56874327 + let hex = self.trimmingCharacters(in: ["#", " "]) + var int: UInt64 = 0 + Scanner(string: hex).scanHexInt64(&int) + let a, r, g, b: UInt64 + switch hex.count { + case 3: // RGB (12-bit) + (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: // RGB (24-bit) + (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: // ARGB (32-bit) + (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (1, 1, 1, 0) + } + + return Color( + .sRGB, + red: Double(r) / 255, + green: Double(g) / 255, + blue: Double(b) / 255, + opacity: Double(a) / 255 + ) + } +} diff --git a/apps/ios/SimpleXChat/Theme/ThemeTypes.swift b/apps/ios/SimpleXChat/Theme/ThemeTypes.swift new file mode 100644 index 0000000000..4074382543 --- /dev/null +++ b/apps/ios/SimpleXChat/Theme/ThemeTypes.swift @@ -0,0 +1,736 @@ +// +// Theme.swift +// SimpleX (iOS) +// +// Created by Avently on 03.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI + +public enum DefaultTheme: String, Codable, Equatable { + case LIGHT + case DARK + case SIMPLEX + case BLACK + + public static let SYSTEM_THEME_NAME: String = "SYSTEM" + + public var themeName: String { self.rawValue } + + public var mode: DefaultThemeMode { + self == .LIGHT + ? DefaultThemeMode.light + : DefaultThemeMode.dark + } + + public func hasChangedAnyColor(_ overrides: ThemeOverrides?) -> Bool { + if let overrides { + overrides.colors != ThemeColors() || (overrides.wallpaper != nil && (overrides.wallpaper?.background != nil || overrides.wallpaper?.tint != nil)) + } else { + false + } + } +} + +public enum DefaultThemeMode: String, Codable { + case light + case dark +} + +public class Colors: ObservableObject, NSCopying, Equatable { + @Published public var primary: Color + @Published public var primaryVariant: Color + @Published public var secondary: Color + @Published public var secondaryVariant: Color + @Published public var background: Color + @Published public var surface: Color + @Published public var error: Color + @Published public var onBackground: Color + @Published public var onSurface: Color + @Published public var isLight: Bool + + public init(primary: Color, primaryVariant: Color, secondary: Color, secondaryVariant: Color, background: Color, surface: Color, error: Color, onBackground: Color, onSurface: Color, isLight: Bool) { + self.primary = primary + self.primaryVariant = primaryVariant + self.secondary = secondary + self.secondaryVariant = secondaryVariant + self.background = background + self.surface = surface + self.error = error + self.onBackground = onBackground + self.onSurface = onSurface + self.isLight = isLight + } + + public static func == (lhs: Colors, rhs: Colors) -> Bool { + lhs.primary == rhs.primary && + lhs.primaryVariant == rhs.primaryVariant && + lhs.secondary == rhs.secondary && + lhs.secondaryVariant == rhs.secondaryVariant && + lhs.background == rhs.background && + lhs.surface == rhs.surface && + lhs.error == rhs.error && + lhs.onBackground == rhs.onBackground && + lhs.onSurface == rhs.onSurface && + lhs.isLight == rhs.isLight + } + + public func copy(with zone: NSZone? = nil) -> Any { + Colors(primary: self.primary, primaryVariant: self.primaryVariant, secondary: self.secondary, secondaryVariant: self.secondaryVariant, background: self.background, surface: self.surface, error: self.error, onBackground: self.onBackground, onSurface: self.onSurface, isLight: self.isLight) + } + + public func clone() -> Colors { copy() as! Colors } +} + +public class AppColors: ObservableObject, NSCopying, Equatable { + @Published public var title: Color + @Published public var primaryVariant2: Color + @Published public var sentMessage: Color + @Published public var sentQuote: Color + @Published public var receivedMessage: Color + @Published public var receivedQuote: Color + + public init(title: Color, primaryVariant2: Color, sentMessage: Color, sentQuote: Color, receivedMessage: Color, receivedQuote: Color) { + self.title = title + self.primaryVariant2 = primaryVariant2 + self.sentMessage = sentMessage + self.sentQuote = sentQuote + self.receivedMessage = receivedMessage + self.receivedQuote = receivedQuote + } + + public static func == (lhs: AppColors, rhs: AppColors) -> Bool { + lhs.title == rhs.title && + lhs.primaryVariant2 == rhs.primaryVariant2 && + lhs.sentMessage == rhs.sentMessage && + lhs.sentQuote == rhs.sentQuote && + lhs.receivedQuote == rhs.receivedMessage && + lhs.receivedQuote == rhs.receivedQuote + } + + public func copy(with zone: NSZone? = nil) -> Any { + AppColors(title: self.title, primaryVariant2: self.primaryVariant2, sentMessage: self.sentMessage, sentQuote: self.sentQuote, receivedMessage: self.receivedMessage, receivedQuote: self.receivedQuote) + } + + public func clone() -> AppColors { copy() as! AppColors } + + public func copy( + title: Color?, + primaryVariant2: Color?, + sentMessage: Color?, + sentQuote: Color?, + receivedMessage: Color?, + receivedQuote: Color? + ) -> AppColors { + AppColors( + title: title ?? self.title, + primaryVariant2: primaryVariant2 ?? self.primaryVariant2, + sentMessage: sentMessage ?? self.sentMessage, + sentQuote: sentQuote ?? self.sentQuote, + receivedMessage: receivedMessage ?? self.receivedMessage, + receivedQuote: receivedQuote ?? self.receivedQuote + ) + } +} + +public class AppWallpaper: ObservableObject, NSCopying, Equatable { + public static func == (lhs: AppWallpaper, rhs: AppWallpaper) -> Bool { + lhs.background == rhs.background && + lhs.tint == rhs.tint && + lhs.type == rhs.type + } + + @Published public var background: Color? = nil + @Published public var tint: Color? = nil + @Published public var type: WallpaperType = WallpaperType.empty + + public init(background: Color?, tint: Color?, type: WallpaperType) { + self.background = background + self.tint = tint + self.type = type + } + + public func copy(with zone: NSZone? = nil) -> Any { + AppWallpaper(background: self.background, tint: self.tint, type: self.type) + } + + public func clone() -> AppWallpaper { copy() as! AppWallpaper } + + public func copyWithoutDefault(_ background: Color?, _ tint: Color?, _ type: WallpaperType) -> AppWallpaper { + AppWallpaper( + background: background, + tint: tint, + type: type + ) + } +} + +public enum ThemeColor { + case primary + case primaryVariant + case secondary + case secondaryVariant + case background + case surface + case title + case sentMessage + case sentQuote + case receivedMessage + case receivedQuote + case primaryVariant2 + case wallpaperBackground + case wallpaperTint + + public func fromColors(_ colors: Colors, _ appColors: AppColors, _ appWallpaper: AppWallpaper) -> Color? { + switch (self) { + case .primary: colors.primary + case .primaryVariant: colors.primaryVariant + case .secondary: colors.secondary + case .secondaryVariant: colors.secondaryVariant + case .background: colors.background + case .surface: colors.surface + case .title: appColors.title + case .primaryVariant2: appColors.primaryVariant2 + case .sentMessage: appColors.sentMessage + case .sentQuote: appColors.sentQuote + case .receivedMessage: appColors.receivedMessage + case .receivedQuote: appColors.receivedQuote + case .wallpaperBackground: appWallpaper.background + case .wallpaperTint: appWallpaper.tint + } + } + + public var text: LocalizedStringKey { + switch (self) { + case .primary: "Accent" + case .primaryVariant: "Additional accent" + case .secondary: "Secondary" + case .secondaryVariant: "Additional secondary" + case .background: "Background" + case .surface: "Menus" + case .title: "Title" + case .primaryVariant2: "Additional accent 2" + case .sentMessage: "Sent message" + case .sentQuote: "Sent reply" + case .receivedMessage: "Received message" + case .receivedQuote: "Received reply" + case .wallpaperBackground: "Wallpaper background" + case .wallpaperTint: "Wallpaper accent" + } + } +} + +public struct ThemeColors: Codable, Equatable, Hashable { + public var primary: String? = nil + public var primaryVariant: String? = nil + public var secondary: String? = nil + public var secondaryVariant: String? = nil + public var background: String? = nil + public var surface: String? = nil + public var title: String? = nil + public var primaryVariant2: String? = nil + public var sentMessage: String? = nil + public var sentQuote: String? = nil + public var receivedMessage: String? = nil + public var receivedQuote: String? = nil + + public init(primary: String? = nil, primaryVariant: String? = nil, secondary: String? = nil, secondaryVariant: String? = nil, background: String? = nil, surface: String? = nil, title: String? = nil, primaryVariant2: String? = nil, sentMessage: String? = nil, sentQuote: String? = nil, receivedMessage: String? = nil, receivedQuote: String? = nil) { + self.primary = primary + self.primaryVariant = primaryVariant + self.secondary = secondary + self.secondaryVariant = secondaryVariant + self.background = background + self.surface = surface + self.title = title + self.primaryVariant2 = primaryVariant2 + self.sentMessage = sentMessage + self.sentQuote = sentQuote + self.receivedMessage = receivedMessage + self.receivedQuote = receivedQuote + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case primary = "accent" + case primaryVariant = "accentVariant" + case secondary + case secondaryVariant + case background + case surface = "menus" + case title + case primaryVariant2 = "accentVariant2" + case sentMessage + case sentQuote = "sentReply" + case receivedMessage + case receivedQuote = "receivedReply" + } + + public static func from(sentMessage: String, sentQuote: String, receivedMessage: String, receivedQuote: String) -> ThemeColors { + var c = ThemeColors() + c.sentMessage = sentMessage + c.sentQuote = sentQuote + c.receivedMessage = receivedMessage + c.receivedQuote = receivedQuote + return c + } + + public static func from(_ colors: Colors, _ appColors: AppColors) -> ThemeColors { + ThemeColors( + primary: colors.primary.toReadableHex(), + primaryVariant: colors.primaryVariant.toReadableHex(), + secondary: colors.secondary.toReadableHex(), + secondaryVariant: colors.secondaryVariant.toReadableHex(), + background: colors.background.toReadableHex(), + surface: colors.surface.toReadableHex(), + title: appColors.title.toReadableHex(), + primaryVariant2: appColors.primaryVariant2.toReadableHex(), + sentMessage: appColors.sentMessage.toReadableHex(), + sentQuote: appColors.sentQuote.toReadableHex(), + receivedMessage: appColors.receivedMessage.toReadableHex(), + receivedQuote: appColors.receivedQuote.toReadableHex() + ) + } +} + +public struct ThemeWallpaper: Codable, Equatable, Hashable { + public var preset: String? + public var scale: Float? + public var scaleType: WallpaperScaleType? + public var background: String? + public var tint: String? + public var image: String? + public var imageFile: String? + + public init(preset: String? = nil, scale: Float? = nil, scaleType: WallpaperScaleType? = nil, background: String? = nil, tint: String? = nil, image: String? = nil, imageFile: String? = nil) { + self.preset = preset + self.scale = scale + self.scaleType = scaleType + self.background = background + self.tint = tint + self.image = image + self.imageFile = imageFile + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case preset + case scale + case scaleType + case background + case tint + case image + case imageFile + } + + public func toAppWallpaper() -> AppWallpaper { + AppWallpaper ( + background: background?.colorFromReadableHex(), + tint: tint?.colorFromReadableHex(), + type: WallpaperType.from(self) ?? WallpaperType.empty + ) + } + + public func withFilledWallpaperPath() -> ThemeWallpaper { + let aw = toAppWallpaper() + let type = aw.type + let preset: String? = if case let WallpaperType.preset(filename, _) = type { filename } else { nil } + let scale: Float? = if scale == nil { nil } else { + if case let WallpaperType.preset(_, scale) = type { + scale + } else if case let WallpaperType.image(_, scale, _) = type { + scale + } else { + nil + } + } + let scaleType: WallpaperScaleType? = if scaleType == nil { nil } else if case let WallpaperType.image(_, _, scaleType) = type { scaleType } else { nil } + let imageFile: String? = if case let WallpaperType.image(filename, _, _) = type { filename } else { nil } + return ThemeWallpaper ( + preset: preset, + scale: scale, + scaleType: scaleType, + background: aw.background?.toReadableHex(), + tint: aw.tint?.toReadableHex(), + image: nil, + imageFile: imageFile + ) + } + + public static func from(_ type: WallpaperType, _ background: String?, _ tint: String?) -> ThemeWallpaper { + let preset: String? = if case let WallpaperType.preset(filename, _) = type { filename } else { nil } + let scale: Float? = if case let WallpaperType.preset(_, scale) = type { scale } else if case let WallpaperType.image(_, scale, _) = type { scale } else { nil } + let scaleType: WallpaperScaleType? = if case let WallpaperType.image(_, _, scaleType) = type { scaleType } else { nil } + let imageFile: String? = if case let WallpaperType.image(filename, _, _) = type { filename } else { nil } + return ThemeWallpaper( + preset: preset, + scale: scale, + scaleType: scaleType, + background: background, + tint: tint, + image: nil, + imageFile: imageFile + ) + } +} + +/// If you add new properties, make sure they serialized to YAML correctly, see: +/// encodeThemeOverrides() +public struct ThemeOverrides: Codable, Equatable, Hashable { + public var themeId: String = UUID().uuidString + public var base: DefaultTheme + public var colors: ThemeColors = ThemeColors() + public var wallpaper: ThemeWallpaper? = nil + + public init(themeId: String = UUID().uuidString, base: DefaultTheme, colors: ThemeColors = ThemeColors(), wallpaper: ThemeWallpaper? = nil) { + self.themeId = themeId + self.base = base + self.colors = colors + self.wallpaper = wallpaper + } + + public func isSame(_ type: WallpaperType?, _ themeName: String) -> Bool { + if base.themeName != themeName { + return false + } + return if let preset = wallpaper?.preset, let type, case let WallpaperType.preset(filename, _) = type, preset == filename { + true + } else if wallpaper?.imageFile != nil, let type, case WallpaperType.image = type { + true + } else if wallpaper?.preset == nil && wallpaper?.imageFile == nil && type == nil { + true + } else if wallpaper?.preset == nil && wallpaper?.imageFile == nil, let type, case WallpaperType.empty = type { + true + } else { + false + } + } + + public func withUpdatedColor(_ name: ThemeColor, _ color: String?) -> ThemeOverrides { + var c = colors + var w = wallpaper + switch name { + case ThemeColor.primary: c.primary = color + case ThemeColor.primaryVariant: c.primaryVariant = color + case ThemeColor.secondary: c.secondary = color + case ThemeColor.secondaryVariant: c.secondaryVariant = color + case ThemeColor.background: c.background = color + case ThemeColor.surface: c.surface = color + case ThemeColor.title: c.title = color + case ThemeColor.primaryVariant2: c.primaryVariant2 = color + case ThemeColor.sentMessage: c.sentMessage = color + case ThemeColor.sentQuote: c.sentQuote = color + case ThemeColor.receivedMessage: c.receivedMessage = color + case ThemeColor.receivedQuote: c.receivedQuote = color + case ThemeColor.wallpaperBackground: w?.background = color + case ThemeColor.wallpaperTint: w?.tint = color + } + return ThemeOverrides(themeId: themeId, base: base, colors: c, wallpaper: w) + } + + public func toColors(_ base: DefaultTheme, _ perChatTheme: ThemeColors?, _ perUserTheme: ThemeColors?, _ presetWallpaperTheme: ThemeColors?) -> Colors { + let baseColors = switch base { + case DefaultTheme.LIGHT: LightColorPalette + case DefaultTheme.DARK: DarkColorPalette + case DefaultTheme.SIMPLEX: SimplexColorPalette + case DefaultTheme.BLACK: BlackColorPalette + } + let c = baseColors.clone() + c.primary = perChatTheme?.primary?.colorFromReadableHex() ?? perUserTheme?.primary?.colorFromReadableHex() ?? colors.primary?.colorFromReadableHex() ?? presetWallpaperTheme?.primary?.colorFromReadableHex() ?? baseColors.primary + c.primaryVariant = perChatTheme?.primaryVariant?.colorFromReadableHex() ?? perUserTheme?.primaryVariant?.colorFromReadableHex() ?? colors.primaryVariant?.colorFromReadableHex() ?? presetWallpaperTheme?.primaryVariant?.colorFromReadableHex() ?? baseColors.primaryVariant + c.secondary = perChatTheme?.secondary?.colorFromReadableHex() ?? perUserTheme?.secondary?.colorFromReadableHex() ?? colors.secondary?.colorFromReadableHex() ?? presetWallpaperTheme?.secondary?.colorFromReadableHex() ?? baseColors.secondary + c.secondaryVariant = perChatTheme?.secondaryVariant?.colorFromReadableHex() ?? perUserTheme?.secondaryVariant?.colorFromReadableHex() ?? colors.secondaryVariant?.colorFromReadableHex() ?? presetWallpaperTheme?.secondaryVariant?.colorFromReadableHex() ?? baseColors.secondaryVariant + c.background = perChatTheme?.background?.colorFromReadableHex() ?? perUserTheme?.background?.colorFromReadableHex() ?? colors.background?.colorFromReadableHex() ?? presetWallpaperTheme?.background?.colorFromReadableHex() ?? baseColors.background + c.surface = perChatTheme?.surface?.colorFromReadableHex() ?? perUserTheme?.surface?.colorFromReadableHex() ?? colors.surface?.colorFromReadableHex() ?? presetWallpaperTheme?.surface?.colorFromReadableHex() ?? baseColors.surface + return c + } + + public func toAppColors(_ base: DefaultTheme, _ perChatTheme: ThemeColors?, _ perChatWallpaperType: WallpaperType?, _ perUserTheme: ThemeColors?, _ perUserWallpaperType: WallpaperType?, _ presetWallpaperTheme: ThemeColors?) -> AppColors { + let baseColors = switch base { + case DefaultTheme.LIGHT: LightColorPaletteApp + case DefaultTheme.DARK: DarkColorPaletteApp + case DefaultTheme.SIMPLEX: SimplexColorPaletteApp + case DefaultTheme.BLACK: BlackColorPaletteApp + } + + let sentMessageFallback = colors.sentMessage?.colorFromReadableHex() ?? presetWallpaperTheme?.sentMessage?.colorFromReadableHex() ?? baseColors.sentMessage + let sentQuoteFallback = colors.sentQuote?.colorFromReadableHex() ?? presetWallpaperTheme?.sentQuote?.colorFromReadableHex() ?? baseColors.sentQuote + let receivedMessageFallback = colors.receivedMessage?.colorFromReadableHex() ?? presetWallpaperTheme?.receivedMessage?.colorFromReadableHex() ?? baseColors.receivedMessage + let receivedQuoteFallback = colors.receivedQuote?.colorFromReadableHex() ?? presetWallpaperTheme?.receivedQuote?.colorFromReadableHex() ?? baseColors.receivedQuote + + let c = baseColors.clone() + c.title = perChatTheme?.title?.colorFromReadableHex() ?? perUserTheme?.title?.colorFromReadableHex() ?? colors.title?.colorFromReadableHex() ?? presetWallpaperTheme?.title?.colorFromReadableHex() ?? baseColors.title + c.primaryVariant2 = perChatTheme?.primaryVariant2?.colorFromReadableHex() ?? perUserTheme?.primaryVariant2?.colorFromReadableHex() ?? colors.primaryVariant2?.colorFromReadableHex() ?? presetWallpaperTheme?.primaryVariant2?.colorFromReadableHex() ?? baseColors.primaryVariant2 + c.sentMessage = if let c = perChatTheme?.sentMessage { c.colorFromReadableHex() } else if let perUserTheme, (perChatWallpaperType == nil || perUserWallpaperType == nil || perChatWallpaperType!.sameType(perUserWallpaperType)) { perUserTheme.sentMessage?.colorFromReadableHex() ?? sentMessageFallback } else { sentMessageFallback } + c.sentQuote = if let c = perChatTheme?.sentQuote { c.colorFromReadableHex() } else if let perUserTheme, (perChatWallpaperType == nil || perUserWallpaperType == nil || perChatWallpaperType!.sameType(perUserWallpaperType)) { perUserTheme.sentQuote?.colorFromReadableHex() ?? sentQuoteFallback } else { sentQuoteFallback } + c.receivedMessage = if let c = perChatTheme?.receivedMessage { c.colorFromReadableHex() } else if let perUserTheme, (perChatWallpaperType == nil || perUserWallpaperType == nil || perChatWallpaperType!.sameType(perUserWallpaperType)) { perUserTheme.receivedMessage?.colorFromReadableHex() ?? receivedMessageFallback } + else { receivedMessageFallback } + c.receivedQuote = if let c = perChatTheme?.receivedQuote { c.colorFromReadableHex() } else if let perUserTheme, (perChatWallpaperType == nil || perUserWallpaperType == nil || perChatWallpaperType!.sameType(perUserWallpaperType)) { perUserTheme.receivedQuote?.colorFromReadableHex() ?? receivedQuoteFallback } else { receivedQuoteFallback } + return c + } + + public func toAppWallpaper(_ themeOverridesForType: WallpaperType?, _ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverride?, _ themeBackgroundColor: Color) -> AppWallpaper { + let mainType: WallpaperType + if let t = themeOverridesForType { mainType = t } + // type can be nil if override is empty `"wallpaper": "{}"`, in this case no wallpaper is needed, empty. + // It's not nil to override upper level wallpaper + else if let w = perChatTheme?.wallpaper { mainType = w.toAppWallpaper().type } + else if let w = perUserTheme?.wallpaper { mainType = w.toAppWallpaper().type } + else if let w = wallpaper { mainType = w.toAppWallpaper().type } + else { return AppWallpaper(background: nil, tint: nil, type: WallpaperType.empty) } + + let first: ThemeWallpaper? = if mainType.sameType(perChatTheme?.wallpaper?.toAppWallpaper().type) { perChatTheme?.wallpaper } else { nil } + let second: ThemeWallpaper? = if mainType.sameType(perUserTheme?.wallpaper?.toAppWallpaper().type) { perUserTheme?.wallpaper } else { nil } + let third: ThemeWallpaper? = if mainType.sameType(self.wallpaper?.toAppWallpaper().type) { self.wallpaper } else { nil } + + let wallpaper: WallpaperType + switch mainType { + case let WallpaperType.preset(preset, scale): + let scale = if themeOverridesForType == nil { scale ?? first?.scale ?? second?.scale ?? third?.scale } else { second?.scale ?? third?.scale ?? scale } + wallpaper = WallpaperType.preset(preset, scale) + case let WallpaperType.image(filename, scale, scaleType): + let scale = if themeOverridesForType == nil { scale ?? first?.scale ?? second?.scale ?? third?.scale } else { second?.scale ?? third?.scale ?? scale } + let scaleType = if themeOverridesForType == nil { scaleType ?? first?.scaleType ?? second?.scaleType ?? third?.scaleType } else { second?.scaleType ?? third?.scaleType ?? scaleType } + let imageFile = if themeOverridesForType == nil { filename } else { first?.imageFile ?? second?.imageFile ?? third?.imageFile ?? filename } + wallpaper = WallpaperType.image(imageFile, scale, scaleType) + case WallpaperType.empty: + wallpaper = WallpaperType.empty + } + let background = (first?.background ?? second?.background ?? third?.background)?.colorFromReadableHex() ?? mainType.defaultBackgroundColor(base, themeBackgroundColor) + let tint = (first?.tint ?? second?.tint ?? third?.tint)?.colorFromReadableHex() ?? mainType.defaultTintColor(base) + + return AppWallpaper(background: background, tint: tint, type: wallpaper) + } + + public func withFilledColors(_ base: DefaultTheme, _ perChatTheme: ThemeColors?, _ perChatWallpaperType: WallpaperType?, _ perUserTheme: ThemeColors?, _ perUserWallpaperType: WallpaperType?, _ presetWallpaperTheme: ThemeColors?) -> ThemeColors { + let c = toColors(base, perChatTheme, perUserTheme, presetWallpaperTheme) + let ac = toAppColors(base, perChatTheme, perChatWallpaperType, perUserTheme, perUserWallpaperType, presetWallpaperTheme) + return ThemeColors( + primary: c.primary.toReadableHex(), + primaryVariant: c.primaryVariant.toReadableHex(), + secondary: c.secondary.toReadableHex(), + secondaryVariant: c.secondaryVariant.toReadableHex(), + background: c.background.toReadableHex(), + surface: c.surface.toReadableHex(), + title: ac.title.toReadableHex(), + primaryVariant2: ac.primaryVariant2.toReadableHex(), + sentMessage: ac.sentMessage.toReadableHex(), + sentQuote: ac.sentQuote.toReadableHex(), + receivedMessage: ac.receivedMessage.toReadableHex(), + receivedQuote: ac.receivedQuote.toReadableHex() + ) + } +} + +extension [ThemeOverrides] { + public func getTheme(_ themeId: String?) -> ThemeOverrides? { + self.first { $0.themeId == themeId } + } + + public func getTheme(_ themeId: String?, _ type: WallpaperType?, _ base: DefaultTheme) -> ThemeOverrides? { + self.first { $0.themeId == themeId || $0.isSame(type, base.themeName) } + } + + public func replace(_ theme: ThemeOverrides) -> [ThemeOverrides] { + let index = self.firstIndex { $0.themeId == theme.themeId || + // prevent situation when two themes has the same type but different theme id (maybe something was changed in prefs by hand) + $0.isSame(WallpaperType.from(theme.wallpaper), theme.base.themeName) + } + var a = self.map { $0 } + if let index { + a[index] = theme + } else { + a.append(theme) + } + return a + } + + public func sameTheme(_ type: WallpaperType?, _ themeName: String) -> ThemeOverrides? { first { $0.isSame(type, themeName) } } + + public func skipDuplicates() -> [ThemeOverrides] { + var res: [ThemeOverrides] = [] + self.forEach { theme in + let themeType = WallpaperType.from(theme.wallpaper) + if !res.contains(where: { $0.themeId == theme.themeId || $0.isSame(themeType, theme.base.themeName) }) { + res.append(theme) + } + } + return res + } + +} + +public struct ThemeModeOverrides: Codable, Hashable { + public var light: ThemeModeOverride? = nil + public var dark: ThemeModeOverride? = nil + + public init(light: ThemeModeOverride? = nil, dark: ThemeModeOverride? = nil) { + self.light = light + self.dark = dark + } + + public func preferredMode(_ darkTheme: Bool) -> ThemeModeOverride? { + darkTheme ? dark : light + } +} + +public struct ThemeModeOverride: Codable, Equatable, Hashable { + public var mode: DefaultThemeMode// = CurrentColors.base.mode + public var colors: ThemeColors = ThemeColors() + public var wallpaper: ThemeWallpaper? = nil + + public init(mode: DefaultThemeMode, colors: ThemeColors = ThemeColors(), wallpaper: ThemeWallpaper? = nil) { + self.mode = mode + self.colors = colors + self.wallpaper = wallpaper + } + + public var type: WallpaperType? { WallpaperType.from(wallpaper) } + + public func withUpdatedColor(_ name: ThemeColor, _ color: String?) -> ThemeModeOverride { + var c = colors + var w = wallpaper + switch (name) { + case ThemeColor.primary: c.primary = color + case ThemeColor.primaryVariant: c.primaryVariant = color + case ThemeColor.secondary: c.secondary = color + case ThemeColor.secondaryVariant: c.secondaryVariant = color + case ThemeColor.background: c.background = color + case ThemeColor.surface: c.surface = color + case ThemeColor.title: c.title = color + case ThemeColor.primaryVariant2: c.primaryVariant2 = color + case ThemeColor.sentMessage: c.sentMessage = color + case ThemeColor.sentQuote: c.sentQuote = color + case ThemeColor.receivedMessage: c.receivedMessage = color + case ThemeColor.receivedQuote: c.receivedQuote = color + case ThemeColor.wallpaperBackground: w?.background = color + case ThemeColor.wallpaperTint: w?.tint = color + } + return ThemeModeOverride(mode: mode, colors: c, wallpaper: w) + } + + public static func withFilledAppDefaults(_ mode: DefaultThemeMode, _ base: DefaultTheme) -> ThemeModeOverride { + ThemeModeOverride( + mode: mode, + colors: ThemeOverrides(base: base).withFilledColors(base, nil, nil, nil, nil, nil), + wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename) + ) + } +} + +public let DarkColorPalette = Colors( + primary: SimplexBlue, + primaryVariant: SimplexBlue, + secondary: HighOrLowlight, + secondaryVariant: DarkGray, + background: Color.black, + surface: Color(0xFF222222), + error: Color.red, + onBackground: Color.white, + onSurface: Color.white, + isLight: false +) +public let DarkColorPaletteApp = AppColors( + title: .white, + primaryVariant2: Color(0xFF18262E), + sentMessage: Color(0xFF18262E), + sentQuote: Color(0xFF1D3847), + receivedMessage: Color(0xff262627), + receivedQuote: Color(0xff373739) +) + +public let LightColorPalette = Colors ( + primary: SimplexBlue, + primaryVariant: SimplexBlue, + secondary: HighOrLowlight, + secondaryVariant: LightGray, + background: Color.white, + surface: Color.white, + error: Color.red, + onBackground: Color.black, + onSurface: Color.black, + isLight: true +) +public let LightColorPaletteApp = AppColors( + title: .black, + primaryVariant2: Color(0xFFE9F7FF), + sentMessage: Color(0xFFE9F7FF), + sentQuote: Color(0xFFD6F0FF), + receivedMessage: Color(0xfff5f5f6), + receivedQuote: Color(0xffececee) +) + +public let SimplexColorPalette = Colors( + primary: Color(0xFF70F0F9), + primaryVariant: Color(0xFF1298A5), + secondary: HighOrLowlight, + secondaryVariant: Color(0xFF2C464D), + background: Color(0xFF111528), + surface: Color(0xFF121C37), + error: Color.red, + onBackground: Color.white, + onSurface: Color.white, + isLight: false +) +public let SimplexColorPaletteApp = AppColors( + title: .white, + primaryVariant2: Color(0xFF172941), + sentMessage: Color(0xFF172941), + sentQuote: Color(0xFF1C3A57), + receivedMessage: Color(0xff25283a), + receivedQuote: Color(0xff36394a) +) + +public let BlackColorPalette = Colors( + primary: Color(0xff0077e0), + primaryVariant: Color(0xff0077e0), + secondary: HighOrLowlight, + secondaryVariant: DarkGray, + background: Color(0xff070707), + surface: Color(0xff161617), + error: Color.red, + onBackground: Color.white, + onSurface: Color.white, + isLight: false +) +public let BlackColorPaletteApp = AppColors( + title: .white, + primaryVariant2: Color(0xff243747), + sentMessage: Color(0xFF18262E), + sentQuote: Color(0xFF1D3847), + receivedMessage: Color(0xff1b1b1b), + receivedQuote: Color(0xff29292b) +) + +extension Colors { + public func updateColorsFrom(_ other: Colors) { + primary = other.primary + primaryVariant = other.primaryVariant + secondary = other.secondary + secondaryVariant = other.secondaryVariant + background = other.background + surface = other.surface + error = other.error + onBackground = other.onBackground + onSurface = other.onSurface + isLight = other.isLight + } +} + +extension AppColors { + public func updateColorsFrom(_ other: AppColors) { + title = other.title + primaryVariant2 = other.primaryVariant2 + sentMessage = other.sentMessage + sentQuote = other.sentQuote + receivedMessage = other.receivedMessage + receivedQuote = other.receivedQuote + } +} + +extension AppWallpaper { + public func updateWallpaperFrom(_ other: AppWallpaper) { + background = other.background + tint = other.tint + type = other.type + } +} diff --git a/apps/ios/SimpleXChat/dummy.m b/apps/ios/SimpleXChat/dummy.m index 64fbc32dd3..d26e108520 100644 --- a/apps/ios/SimpleXChat/dummy.m +++ b/apps/ios/SimpleXChat/dummy.m @@ -21,4 +21,13 @@ DIR *opendir$INODE64(const char *name) { return opendir(name); } +int readdir$INODE64(DIR *restrict dirp, struct dirent *restrict entry, + struct dirent **restrict result) { + return readdir_r(dirp, entry, result); +} + +DIR *fdopendir$INODE64(const char *name) { + return opendir(name); +} + #endif diff --git a/apps/ios/SimpleXChat/hs_init.c b/apps/ios/SimpleXChat/hs_init.c index 7a5ea24560..e75173d6cf 100644 --- a/apps/ios/SimpleXChat/hs_init.c +++ b/apps/ios/SimpleXChat/hs_init.c @@ -15,7 +15,7 @@ void haskell_init(void) { char *argv[] = { "simplex", "+RTS", // requires `hs_init_with_rtsopts` - "-A16m", // chunk size for new allocations + "-A64m", // chunk size for new allocations "-H64m", // initial heap size "-xn", // non-moving GC 0 @@ -23,3 +23,35 @@ void haskell_init(void) { char **pargv = argv; hs_init_with_rtsopts(&argc, &pargv); } + +void haskell_init_nse(void) { + int argc = 7; + char *argv[] = { + "simplex", + "+RTS", // requires `hs_init_with_rtsopts` + "-A256k", // chunk size for new allocations + "-H512k", // initial heap size + "-F0.5", // heap growth triggering GC + "-Fd0.3", // memory return + "-c", // compacting garbage collector + 0 + }; + char **pargv = argv; + hs_init_with_rtsopts(&argc, &pargv); +} + +void haskell_init_se(void) { + int argc = 7; + char *argv[] = { + "simplex", + "+RTS", // requires `hs_init_with_rtsopts` + "-A1m", // chunk size for new allocations + "-H1m", // initial heap size + "-F0.5", // heap growth triggering GC + "-Fd1", // memory return + "-c", // compacting garbage collector + 0 + }; + char **pargv = argv; + hs_init_with_rtsopts(&argc, &pargv); +} diff --git a/apps/ios/SimpleXChat/hs_init.h b/apps/ios/SimpleXChat/hs_init.h index 48850e819c..40be4fc263 100644 --- a/apps/ios/SimpleXChat/hs_init.h +++ b/apps/ios/SimpleXChat/hs_init.h @@ -11,4 +11,8 @@ void haskell_init(void); +void haskell_init_nse(void); + +void haskell_init_se(void); + #endif /* hs_init_h */ diff --git a/apps/ios/SimpleXChat/objc.h b/apps/ios/SimpleXChat/objc.h new file mode 100644 index 0000000000..a75a6dc5e4 --- /dev/null +++ b/apps/ios/SimpleXChat/objc.h @@ -0,0 +1,20 @@ +// +// objc.h +// SimpleX (iOS) +// +// Created by Stanislav Dmitrenko on 09.09.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +#ifndef objc_h +#define objc_h + +#import <Foundation/Foundation.h> + +@interface ObjC : NSObject + ++ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error; + +@end + +#endif /* objc_h */ diff --git a/apps/ios/SimpleXChat/objc.m b/apps/ios/SimpleXChat/objc.m new file mode 100644 index 0000000000..c6952578ab --- /dev/null +++ b/apps/ios/SimpleXChat/objc.m @@ -0,0 +1,25 @@ +// +// objc.m +// SimpleXChat +// +// Created by Stanislav Dmitrenko on 09.09.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +#import "objc.h" + +@implementation ObjC + +// https://stackoverflow.com/a/36454808 ++ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error { + @try { + tryBlock(); + return YES; + } + @catch (NSException *exception) { + *error = [[NSError alloc] initWithDomain: exception.name code: 0 userInfo: exception.userInfo]; + return NO; + } +} + +@end diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index 5a704457d1..e4bc8f2150 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -1,18 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (може да се копира)"; @@ -20,31 +5,25 @@ "_italic_" = "\\_курсив_"; /* No comment provided by engineer. */ -"- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." = "- свържете се с [директория за услуги](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjd LW3%23%2F%3Fv%3D1-2%26dh %3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (БЕТА)!\n- потвърждениe за доставка (до 20 члена).\n- по-бързо и по-стабилно."; +"- 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) (БЕТА)!\n- потвърждениe за доставка (до 20 члена).\n- по-бързо и по-стабилно."; /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- по-стабилна доставка на съобщения.\n- малко по-добри групи.\n- и още!"; +/* No comment provided by engineer. */ +"- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- по желание уведомете изтритите контакти.\n- имена на профили с интервали.\n- и още!"; + /* No comment provided by engineer. */ "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- гласови съобщения до 5 минути.\n- персонализирано време за изчезване.\n- история на редактиране."; -/* No comment provided by engineer. */ -", " = ", "; - -/* No comment provided by engineer. */ -": " = ": "; - /* No comment provided by engineer. */ "!1 colored!" = "!1 цветно!"; /* No comment provided by engineer. */ -"." = "."; +"(new)" = "(ново)"; /* No comment provided by engineer. */ -"(" = "("; - -/* 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)"; @@ -56,10 +35,10 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Звезда в GitHub](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Добави нов контакт**: за да създадете своя еднократен QR код или линк за вашия контакт."; +"**Create 1-time link**: to create and share a new invitation link." = "**Добави контакт**: за създаване на нов линк."; /* No comment provided by engineer. */ -"**Create link / QR code** for your contact to use." = "**Създай линк / QR код**, който вашият контакт да използва."; +"**Create group**: to create a new group." = "**Създай група**: за създаване на нова група."; /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "**e2e криптиран**аудио разговор"; @@ -68,26 +47,26 @@ "**e2e encrypted** video call" = "**e2e криптирано** видео разговор"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**По поверително**: проверявайте новите съобщения на всеки 20 минути. Токенът на устройството се споделя със сървъра за чат SimpleX, но не и колко контакти или съобщения имате."; +"**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." = "**По поверително**: проверявайте новите съобщения на всеки 20 минути. Токенът на устройството се споделя със сървъра за чат SimpleX, но не и колко контакти или съобщения имате."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Най-поверително**: не използвайте сървъра за известия SimpleX Chat, периодично проверявайте съобщенията във фонов режим (зависи от това колко често използвате приложението)."; +"**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." = "**Най-поверително**: не използвайте сървъра за известия SimpleX Chat, периодично проверявайте съобщенията във фонов режим (зависи от това колко често използвате приложението)."; /* No comment provided by engineer. */ -"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Поставете получения линк** или го отворете в браузъра и докоснете **Отваряне в мобилно приложение**."; +"**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." = "**Моля, обърнете внимание**: НЯМА да можете да възстановите или промените паролата, ако я загубите."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Препоръчително**: токенът на устройството и известията се изпращат до сървъра за уведомяване на SimpleX Chat, но не и съдържанието, размерът на съобщението или от кого е."; - -/* No comment provided by engineer. */ -"**Scan QR code**: to connect to your contact in person or via video call." = "**Сканирай QR код**: за да се свържете с вашия контакт лично или чрез видеообаждане."; +"**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, но не и съдържанието, размерът на съобщението или от кого е."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Внимание**: Незабавните push известия изискват парола, запазена в Keychain."; +/* No comment provided by engineer. */ +"**Warning**: the archive will be removed." = "**Внимание**: архивът ще бъде изтрит."; + /* No comment provided by engineer. */ "*bold*" = "\\*удебелен*"; @@ -118,12 +97,21 @@ /* No comment provided by engineer. */ "%@ %@" = "%@ %@"; +/* No comment provided by engineer. */ +"%@ and %@" = "%@ и %@"; + /* No comment provided by engineer. */ "%@ and %@ connected" = "%@ и %@ са свързани"; /* copied message info, <sender> at <time> */ "%@ at %@:" = "%1$@ в %2$@:"; +/* No comment provided by engineer. */ +"%@ connected" = "%@ свързан"; + +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ изтеглено"; + /* notification title */ "%@ is connected!" = "%@ е свързан!"; @@ -134,11 +122,17 @@ "%@ is verified" = "%@ е потвърдено"; /* No comment provided by engineer. */ -"%@ servers" = "%@ сървъри"; +"%@ uploaded" = "%@ качено"; /* notification title */ "%@ wants to connect!" = "%@ иска да се свърже!"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + +/* No comment provided by engineer. */ +"%@, %@ and %lld members" = "%@, %@ и %lld членове"; + /* No comment provided by engineer. */ "%@, %@ and %lld other members connected" = "%@, %@ и %lld други членове са свързани"; @@ -178,18 +172,30 @@ /* No comment provided by engineer. */ "%lld file(s) with total size of %@" = "%lld файл(а) с общ размер от %@"; +/* No comment provided by engineer. */ +"%lld group events" = "%lld групови събития"; + /* No comment provided by engineer. */ "%lld members" = "%lld членове"; +/* No comment provided by engineer. */ +"%lld messages blocked" = "%lld блокирани съобщения"; + +/* No comment provided by engineer. */ +"%lld messages blocked by admin" = "%lld съобщения, блокирани от администратора"; + +/* No comment provided by engineer. */ +"%lld messages marked deleted" = "%lld съобщения, маркирани като изтрити"; + +/* No comment provided by engineer. */ +"%lld messages moderated by %@" = "%lld съобщения, модерирани от %@"; + /* No comment provided by engineer. */ "%lld minutes" = "%lld минути"; /* No comment provided by engineer. */ "%lld new interface languages" = "%lld нови езици на интерфейса"; -/* No comment provided by engineer. */ -"%lld second(s)" = "%lld секунда(и)"; - /* No comment provided by engineer. */ "%lld seconds" = "%lld секунди"; @@ -229,10 +235,14 @@ /* No comment provided by engineer. */ "~strike~" = "\\~зачеркнат~"; +/* time to disappear */ +"0 sec" = "0 сек"; + /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 ден"; /* time interval */ @@ -241,10 +251,12 @@ /* No comment provided by engineer. */ "1 minute" = "1 минута"; -/* time interval */ +/* delete after time +time interval */ "1 month" = "1 месец"; -/* time interval */ +/* delete after time +time interval */ "1 week" = "1 седмица"; /* No comment provided by engineer. */ @@ -284,10 +296,7 @@ "Abort changing address?" = "Откажи смяна на адрес?"; /* No comment provided by engineer. */ -"About SimpleX" = "За SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "Повече за SimpleX адреса"; +"About operators" = "За операторите"; /* No comment provided by engineer. */ "About SimpleX Chat" = "За SimpleX Chat"; @@ -296,60 +305,113 @@ "above, then choose:" = "по-горе, след това избери:"; /* No comment provided by engineer. */ -"Accent color" = "Основен цвят"; +"Accent" = "Акцент"; /* accept contact request via notification - accept incoming call via notification */ +accept incoming call via notification +swipe action */ "Accept" = "Приеми"; +/* No comment provided by engineer. */ +"Accept conditions" = "Приеми условията"; + /* No comment provided by engineer. */ "Accept connection request?" = "Приемане на заявка за връзка?"; /* notification body */ "Accept contact request from %@?" = "Приемане на заявка за контакт от %@?"; -/* accept contact request via notification */ +/* accept contact request via notification +swipe action */ "Accept incognito" = "Приеми инкогнито"; /* call status */ "accepted call" = "обаждането прието"; +/* No comment provided by engineer. */ +"Accepted conditions" = "Приети условия"; + +/* No comment provided by engineer. */ +"Acknowledged" = "Потвърден"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Грешки при потвърждението"; + +/* 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 preset servers" = "Добави предварително зададени сървъри"; +"Add friends" = "Добави приятели"; /* No comment provided by engineer. */ "Add profile" = "Добави профил"; /* No comment provided by engineer. */ -"Add server…" = "Добави сървър…"; +"Add server" = "Добави сървър"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Добави сървъри чрез сканиране на QR кодове."; +/* No comment provided by engineer. */ +"Add team members" = "Добави членове на екипа"; + /* No comment provided by engineer. */ "Add to another device" = "Добави към друго устройство"; /* No comment provided by engineer. */ "Add welcome message" = "Добави съобщение при посрещане"; +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "Добавете членовете на вашия екип към разговорите."; + +/* No comment provided by engineer. */ +"Added media & file servers" = "Добавени медийни и файлови сървъри"; + +/* No comment provided by engineer. */ +"Added message servers" = "Добавени сървъри за съобщения"; + +/* No comment provided by engineer. */ +"Additional accent" = "Допълнителен акцент"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Допълнителен акцент 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Допълнителен вторичен"; + /* No comment provided by engineer. */ "Address" = "Адрес"; /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "Промяната на адреса ще бъде прекъсната. Ще се използва старият адрес за получаване."; +/* No comment provided by engineer. */ +"Address or 1-time link?" = "Адрес или еднократен линк?"; + +/* No comment provided by engineer. */ +"Address settings" = "Настройки на адреса"; + /* member role */ "admin" = "админ"; +/* feature role */ +"admins" = "администратори"; + +/* No comment provided by engineer. */ +"Admins can block a member for all." = "Администраторите могат да блокират член за всички."; + /* No comment provided by engineer. */ "Admins can create the links to join groups." = "Админите могат да създадат линкове за присъединяване към групи."; /* No comment provided by engineer. */ "Advanced network settings" = "Разширени мрежови настройки"; +/* No comment provided by engineer. */ +"Advanced settings" = "Разширени настройки"; + /* chat item text */ "agreeing encryption for %@…" = "съгласуване на криптиране за %@…"; @@ -365,29 +427,53 @@ /* No comment provided by engineer. */ "All data is erased when it is entered." = "Всички данни се изтриват при въвеждане."; +/* No comment provided by engineer. */ +"All data is kept private on your device." = "Всички данни се съхраняват поверително на вашето устройство."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Всички членове на групата ще останат свързани."; +/* feature role */ +"all members" = "всички членове"; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone!" = "Всички съобщения ще бъдат изтрити - това не може да бъде отменено!"; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Всички съобщения ще бъдат изтрити - това не може да бъде отменено! Съобщенията ще бъдат изтрити САМО за вас."; +/* No comment provided by engineer. */ +"All new messages from %@ will be hidden!" = "Всички нови съобщения от %@ ще бъдат скрити!"; + +/* profile dropdown */ +"All profiles" = "Всички профили"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Всички ваши контакти ще останат свързани."; /* No comment provided by engineer. */ "All your contacts will remain connected. Profile update will be sent to your contacts." = "Всички ваши контакти ще останат свързани. Актуализацията на профила ще бъде изпратена до вашите контакти."; +/* No comment provided by engineer. */ +"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" = "Позволи"; /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Позволи обаждания само ако вашият контакт ги разрешава."; +/* No comment provided by engineer. */ +"Allow calls?" = "Позволи обаждания?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "Позволи изчезващи съобщения само ако вашият контакт ги разрешава."; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you." = "Позволи необратимо изтриване на съобщение само ако вашият контакт го рарешава."; +"Allow downgrade" = "Позволи понижаване"; + +/* 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 message reactions only if your contact allows them." = "Позволи реакции на съобщения само ако вашият контакт ги разрешава."; @@ -402,11 +488,17 @@ "Allow sending disappearing messages." = "Разреши изпращането на изчезващи съобщения."; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages." = "Позволи необратимо изтриване на изпратените съобщения."; +"Allow sharing" = "Позволи споделяне"; + +/* No comment provided by engineer. */ +"Allow to irreversibly delete sent messages. (24 hours)" = "Позволи необратимо изтриване на изпратените съобщения. (24 часа)"; /* No comment provided by engineer. */ "Allow to send files and media." = "Позволи изпращане на файлове и медия."; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "Разрешаване на изпращане на SimpleX линкове."; + /* No comment provided by engineer. */ "Allow to send voice messages." = "Позволи изпращане на гласови съобщения."; @@ -423,7 +515,7 @@ "Allow your contacts to call you." = "Позволи на вашите контакти да ви се обаждат."; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages." = "Позволи на вашите контакти да изтриват необратимо изпратените съобщения."; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Позволи на вашите контакти да изтриват необратимо изпратените съобщения. (24 часа)"; /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Позволи на вашите контакти да изпращат изчезващи съобщения."; @@ -434,21 +526,39 @@ /* No comment provided by engineer. */ "Already connected?" = "Вече сте свързани?"; +/* No comment provided by engineer. */ +"Already connecting!" = "В процес на свързване!"; + +/* No comment provided by engineer. */ +"Already joining the group!" = "Вече се присъединихте към групата!"; + /* pref value */ "always" = "винаги"; +/* No comment provided by engineer. */ +"Always use private routing." = "Винаги използвай поверително рутиране."; + /* No comment provided by engineer. */ "Always use relay" = "Винаги използвай реле"; /* No comment provided by engineer. */ "An empty chat profile with the provided name is created, and the app opens as usual." = "Създаен беше празен профил за чат с предоставеното име и приложението се отвари както обикновено."; +/* No comment provided by engineer. */ +"and %lld other events" = "и %lld други събития"; + /* No comment provided by engineer. */ "Answer call" = "Отговор на повикване"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "Протокол и код с отворен код – всеки може да оперира собствени сървъри."; + /* No comment provided by engineer. */ "App build: %@" = "Компилация на приложението: %@"; +/* No comment provided by engineer. */ +"App data migration" = "Миграция на данните от приложението"; + /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "Приложението криптира нови локални файлове (с изключение на видеоклипове)."; @@ -461,6 +571,9 @@ /* No comment provided by engineer. */ "App passcode is replaced with self-destruct passcode." = "Кода за достъп до приложение се заменя с код за самоунищожение."; +/* No comment provided by engineer. */ +"App session" = "Сесия на приложението"; + /* No comment provided by engineer. */ "App version" = "Версия на приложението"; @@ -470,6 +583,24 @@ /* No comment provided by engineer. */ "Appearance" = "Изглед"; +/* No comment provided by engineer. */ +"Apply" = "Приложи"; + +/* No comment provided by engineer. */ +"Apply to" = "Приложи към"; + +/* No comment provided by engineer. */ +"Archive and upload" = "Архивиране и качване"; + +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Архивирайте контактите, за да разговаряте по-късно."; + +/* No comment provided by engineer. */ +"Archived contacts" = "Архивирани контакти"; + +/* No comment provided by engineer. */ +"Archiving database" = "Архивиране на база данни"; + /* No comment provided by engineer. */ "Attach" = "Прикачи"; @@ -500,6 +631,9 @@ /* No comment provided by engineer. */ "Authentication unavailable" = "Идентификацията е недостъпна"; +/* member role */ +"author" = "автор"; + /* No comment provided by engineer. */ "Auto-accept" = "Автоматично приемане"; @@ -509,9 +643,18 @@ /* No comment provided by engineer. */ "Auto-accept images" = "Автоматично приемане на изображения"; +/* alert title */ +"Auto-accept settings" = "Автоматично приемане на настройки"; + /* No comment provided by engineer. */ "Back" = "Назад"; +/* No comment provided by engineer. */ +"Background" = "Фон"; + +/* No comment provided by engineer. */ +"Bad desktop address" = "Грешен адрес на настолното устройство"; + /* integrity error chat item */ "bad message hash" = "лош хеш на съобщението"; @@ -524,9 +667,70 @@ /* No comment provided by engineer. */ "Bad message ID" = "Лошо ID на съобщението"; +/* No comment provided by engineer. */ +"Better calls" = "По-добри обаждания"; + +/* No comment provided by engineer. */ +"Better groups" = "По-добри групи"; + +/* No comment provided by engineer. */ +"Better message dates." = "По-добри дати на съобщението."; + /* No comment provided by engineer. */ "Better messages" = "По-добри съобщения"; +/* No comment provided by engineer. */ +"Better networking" = "Подобрена мрежа"; + +/* No comment provided by engineer. */ +"Better notifications" = "Подобрени известия"; + +/* No comment provided by engineer. */ +"Better security ✅" = "По-добра сигурност ✅"; + +/* No comment provided by engineer. */ +"Better user experience" = "Подобрен интерфейс"; + +/* No comment provided by engineer. */ +"Black" = "Черна"; + +/* No comment provided by engineer. */ +"Block" = "Блокирай"; + +/* No comment provided by engineer. */ +"Block for all" = "Блокирай за всички"; + +/* No comment provided by engineer. */ +"Block group members" = "Блокиране на членове на групата"; + +/* No comment provided by engineer. */ +"Block member" = "Блокирай член"; + +/* No comment provided by engineer. */ +"Block member for all?" = "Блокиране на член за всички?"; + +/* No comment provided by engineer. */ +"Block member?" = "Блокирай члена?"; + +/* marked deleted chat item preview text */ +"blocked" = "блокиран"; + +/* rcv group event chat item */ +"blocked %@" = "блокиран %@"; + +/* blocked chat item +marked deleted chat item preview text */ +"blocked by admin" = "блокиран от админ"; + +/* No comment provided by engineer. */ +"Blocked by admin" = "Блокиран от админ"; + +/* No comment provided by engineer. */ +"Blur for better privacy." = "Размазване за по-добра поверителност."; + +/* No comment provided by engineer. */ +"Blur media" = "Размазване на медия"; + /* No comment provided by engineer. */ "bold" = "удебелен"; @@ -534,7 +738,7 @@ "Both you and your contact can add message reactions." = "И вие, и вашият контакт можете да добавяте реакции към съобщението."; /* No comment provided by engineer. */ -"Both you and your contact can irreversibly delete sent messages." = "И вие, и вашият контакт можете да изтриете необратимо изпратените съобщения."; +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "И вие, и вашият контакт можете да изтриете необратимо изпратените съобщения. (24 часа)"; /* No comment provided by engineer. */ "Both you and your contact can make calls." = "И вие, и вашият контакт можете да осъществявате обаждания."; @@ -548,6 +752,12 @@ /* 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. */ +"Business address" = "Бизнес адрес"; + +/* No comment provided by engineer. */ +"Business chats" = "Бизнес чатове"; + /* 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) (БЕТА)."; @@ -567,7 +777,16 @@ "Calls" = "Обаждания"; /* No comment provided by engineer. */ -"Can't delete user profile!" = "Потребителският профил не може да се изтрие!"; +"Calls prohibited!" = "Обажданията са забранени!"; + +/* No comment provided by engineer. */ +"Camera not available" = "Камерата е неодстъпна"; + +/* No comment provided by engineer. */ +"Can't call contact" = "Обаждането на контакта не е позволено"; + +/* No comment provided by engineer. */ +"Can't call member" = "Обаждането на члена не е позволено"; /* No comment provided by engineer. */ "Can't invite contact!" = "Не може да покани контакта!"; @@ -575,21 +794,34 @@ /* No comment provided by engineer. */ "Can't invite contacts!" = "Не може да поканят контактите!"; -/* No comment provided by engineer. */ +/* alert action +alert button */ "Cancel" = "Отказ"; +/* No comment provided by engineer. */ +"Cancel migration" = "Отмени миграцията"; + /* feature offered item */ "cancelled %@" = "отменен %@"; /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Няма достъп до Keychain за запазване на паролата за базата данни"; -/* No comment provided by engineer. */ +/* alert title */ "Cannot receive file" = "Файлът не може да бъде получен"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Капацитетът е надвишен - получателят не е получил предишно изпратените съобщения."; + +/* No comment provided by engineer. */ +"Cellular" = "Мобилна мрежа"; + /* No comment provided by engineer. */ "Change" = "Промени"; +/* authentication reason */ +"Change chat profiles" = "Промени чат профилите"; + /* No comment provided by engineer. */ "Change database passphrase?" = "Промяна на паролата на базата данни?"; @@ -615,11 +847,11 @@ "Change self-destruct mode" = "Промени режима на самоунищожение"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Промени кода за достъп за самоунищожение"; /* chat item text */ -"changed address for you" = "променен е адреса за вас"; +"changed address for you" = "адреса за изпращане е променен"; /* rcv group event chat item */ "changed role of %@ to %@" = "променена роля от %1$@ на %2$@"; @@ -633,20 +865,17 @@ /* chat item text */ "changing address…" = "промяна на адреса…"; -/* No comment provided by engineer. */ -"Chat archive" = "Архив на чата"; - /* No comment provided by engineer. */ "Chat console" = "Конзола"; /* No comment provided by engineer. */ -"Chat database" = "База данни за чата"; +"Chat database" = "База данни"; /* No comment provided by engineer. */ "Chat database deleted" = "Базата данни на чата е изтрита"; /* No comment provided by engineer. */ -"Chat database imported" = "Базата данни на чат е импортирана"; +"Chat database imported" = "Базата данни на е импортирана"; /* No comment provided by engineer. */ "Chat is running" = "Чатът работи"; @@ -654,25 +883,37 @@ /* No comment provided by engineer. */ "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." = "Чатът е спрян. Ако вече сте използвали тази база данни на друго устройство, трябва да я прехвърлите обратно, преди да стартирате чата отново."; + +/* No comment provided by engineer. */ +"Chat migrated!" = "Чатът е мигриран!"; + /* No comment provided by engineer. */ "Chat preferences" = "Чат настройки"; +/* No comment provided by engineer. */ +"Chat profile" = "Потребителски профил"; + /* No comment provided by engineer. */ "Chats" = "Чатове"; -/* No comment provided by engineer. */ +/* alert title */ "Check server address and try again." = "Проверете адреса на сървъра и опитайте отново."; /* No comment provided by engineer. */ "Chinese and Spanish interface" = "Китайски и Испански интерфейс"; +/* No comment provided by engineer. */ +"Choose _Migrate from another device_ on the new device and scan QR code." = "Изберете _Мигриране от друго устройство_ на новото устройство и сканирайте QR кода."; + /* No comment provided by engineer. */ "Choose file" = "Избери файл"; /* No comment provided by engineer. */ "Choose from library" = "Избери от библиотеката"; -/* No comment provided by engineer. */ +/* swipe action */ "Clear" = "Изчисти"; /* No comment provided by engineer. */ @@ -681,15 +922,15 @@ /* No comment provided by engineer. */ "Clear conversation?" = "Изчисти разговора?"; +/* No comment provided by engineer. */ +"Clear private notes?" = "Изчистване на лични бележки?"; + /* No comment provided by engineer. */ "Clear verification" = "Изчисти проверката"; /* No comment provided by engineer. */ "colored" = "цветен"; -/* No comment provided by engineer. */ -"Colors" = "Цветове"; - /* server test step */ "Compare file" = "Сравни файл"; @@ -708,6 +949,9 @@ /* No comment provided by engineer. */ "Confirm database upgrades" = "Потвърди актуализаациите на базата данни"; +/* No comment provided by engineer. */ +"Confirm network settings" = "Потвърди мрежовите настройки"; + /* No comment provided by engineer. */ "Confirm new passphrase…" = "Потвърди новата парола…"; @@ -717,30 +961,60 @@ /* No comment provided by engineer. */ "Confirm password" = "Потвърди парола"; +/* No comment provided by engineer. */ +"Confirm that you remember database passphrase to migrate it." = "Потвърдете, че помните паролата на базата данни, преди да я мигрирате."; + +/* No comment provided by engineer. */ +"Confirm upload" = "Потвърди качването"; + /* server test step */ "Connect" = "Свързване"; +/* No comment provided by engineer. */ +"Connect automatically" = "Автоматично свъзрване"; + /* No comment provided by engineer. */ "Connect incognito" = "Свързване инкогнито"; +/* No comment provided by engineer. */ +"Connect to desktop" = "Свързване с настолно устройство"; + /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "свържете се с разработчиците на SimpleX Chat."; +/* No comment provided by engineer. */ +"Connect to yourself?" = "Свърване със себе си?"; + +/* No comment provided by engineer. */ +"Connect to yourself?\nThis is your own one-time link!" = "Свърване със себе си?\nТова е вашят еднократен линк за връзка!"; + +/* No comment provided by engineer. */ +"Connect to yourself?\nThis is your own SimpleX address!" = "Свърване със себе си?\nТова е вашият личен SimpleX адрес!"; + +/* No comment provided by engineer. */ +"Connect via contact address" = "Свързване чрез адрес за контакт"; + /* No comment provided by engineer. */ "Connect via link" = "Свърване чрез линк"; /* No comment provided by engineer. */ -"Connect via link / QR code" = "Свърване чрез линк/QR код"; +"Connect via one-time link" = "Свързване чрез еднократен линк за връзка"; /* No comment provided by engineer. */ -"Connect via one-time link" = "Свързване чрез еднократен линк за връзка"; +"Connect with %@" = "Свързване с %@"; /* No comment provided by engineer. */ "connected" = "свързан"; +/* No comment provided by engineer. */ +"Connected desktop" = "Свързано настолно устройство"; + /* rcv group event chat item */ "connected directly" = "свързан директно"; +/* No comment provided by engineer. */ +"Connected to desktop" = "Свързан с настолно устройство"; + /* No comment provided by engineer. */ "connecting" = "свързване"; @@ -765,7 +1039,10 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Свързване със сървър…(грешка: %@)"; -/* chat list item title */ +/* No comment provided by engineer. */ +"Connecting to desktop" = "Свързване с настолно устройство"; + +/* No comment provided by engineer. */ "connecting…" = "свързване…"; /* No comment provided by engineer. */ @@ -783,12 +1060,18 @@ /* No comment provided by engineer. */ "Connection request sent!" = "Заявката за връзка е изпратена!"; +/* No comment provided by engineer. */ +"Connection terminated" = "Връзката е прекратена"; + /* No comment provided by engineer. */ "Connection timeout" = "Времето на изчакване за установяване на връзката изтече"; /* connection information */ "connection:%@" = "връзка:%@"; +/* profile update event chat item */ +"contact %@ changed to %@" = "името на контакта %1$@ е променено на %2$@"; + /* No comment provided by engineer. */ "Contact allows" = "Контактът позволява"; @@ -807,9 +1090,6 @@ /* notification */ "Contact is connected" = "Контактът е свързан"; -/* No comment provided by engineer. */ -"Contact is not connected yet!" = "Контактът все още не е свързан!"; - /* No comment provided by engineer. */ "Contact name" = "Име на контакт"; @@ -825,20 +1105,29 @@ /* No comment provided by engineer. */ "Continue" = "Продължи"; -/* chat item action */ +/* No comment provided by engineer. */ "Copy" = "Копирай"; /* No comment provided by engineer. */ "Core version: v%@" = "Версия на ядрото: v%@"; /* No comment provided by engineer. */ -"Create" = "Създай"; +"Correct name to %@?" = "Поправи име на %@?"; /* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Създайте адрес, за да позволите на хората да се свързват с вас."; +"Create" = "Създаване"; + +/* No comment provided by engineer. */ +"Create 1-time link" = "Създаване на еднократна препратка"; + +/* No comment provided by engineer. */ +"Create a group using a random profile." = "Създаване група с автоматично създаден профил."; /* server test step */ -"Create file" = "Създай файл"; +"Create file" = "Създаване на файл"; + +/* No comment provided by engineer. */ +"Create group" = "Създаване на група"; /* No comment provided by engineer. */ "Create group link" = "Създай групов линк"; @@ -850,7 +1139,7 @@ "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Създайте нов профил в [настолното приложение](https://simplex.chat/downloads/). 💻"; /* No comment provided by engineer. */ -"Create one-time invitation link" = "Създай линк за еднократна покана"; +"Create profile" = "Създай профил"; /* server test step */ "Create queue" = "Създай опашка"; @@ -859,13 +1148,22 @@ "Create secret group" = "Създай тайна група"; /* No comment provided by engineer. */ -"Create SimpleX address" = "Създай SimpleX адрес"; +"Create SimpleX address" = "Създаване на адрес в SimpleX"; /* No comment provided by engineer. */ "Create your profile" = "Създай своя профил"; /* No comment provided by engineer. */ -"Created on %@" = "Създаден на %@"; +"Created at" = "Създаден на"; + +/* copied message info */ +"Created at: %@" = "Създаден на: %@"; + +/* No comment provided by engineer. */ +"Creating archive link" = "Създаване на архивен линк"; + +/* No comment provided by engineer. */ +"Creating link…" = "Линкът се създава…"; /* No comment provided by engineer. */ "creator" = "създател"; @@ -954,7 +1252,8 @@ /* message decrypt error item */ "Decryption error" = "Грешка при декриптиране"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "по подразбиране (%@)"; /* No comment provided by engineer. */ @@ -963,9 +1262,13 @@ /* No comment provided by engineer. */ "default (yes)" = "по подразбиране (да)"; -/* chat item action */ +/* alert action +swipe action */ "Delete" = "Изтрий"; +/* No comment provided by engineer. */ +"Delete %lld messages?" = "Изтриване на %lld съобщения?"; + /* No comment provided by engineer. */ "Delete address" = "Изтрий адрес"; @@ -979,10 +1282,7 @@ "Delete all files" = "Изтрий всички файлове"; /* No comment provided by engineer. */ -"Delete archive" = "Изтрий архив"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Изтриване на архива на чата?"; +"Delete and notify contact" = "Изтрий и уведоми контакт"; /* No comment provided by engineer. */ "Delete chat profile" = "Изтрий чат профила"; @@ -997,10 +1297,10 @@ "Delete contact" = "Изтрий контакт"; /* No comment provided by engineer. */ -"Delete Contact" = "Изтрий контакт"; +"Delete database" = "Изтрий базата данни"; /* No comment provided by engineer. */ -"Delete database" = "Изтрий базата данни"; +"Delete database from this device" = "Изтриване на базата данни от това устройство"; /* server test step */ "Delete file" = "Изтрий файл"; @@ -1038,7 +1338,7 @@ /* No comment provided by engineer. */ "Delete message?" = "Изтрий съобщението?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "Изтрий съобщенията"; /* No comment provided by engineer. */ @@ -1050,9 +1350,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "Изтрий старата база данни?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "Изтрий предстоящата връзка"; - /* No comment provided by engineer. */ "Delete pending connection?" = "Изтрий предстоящата връзка?"; @@ -1074,8 +1371,11 @@ /* copied message info */ "Deleted at: %@" = "Изтрито на: %@"; +/* rcv direct event chat item */ +"deleted contact" = "изтрит контакт"; + /* rcv group event chat item */ -"deleted group" = "групата изтрита"; +"deleted group" = "групата е изтрита"; /* No comment provided by engineer. */ "Delivery" = "Доставка"; @@ -1089,6 +1389,15 @@ /* No comment provided by engineer. */ "Description" = "Описание"; +/* No comment provided by engineer. */ +"Desktop address" = "Адрес на настолно устройство"; + +/* No comment provided by engineer. */ +"Desktop app version %@ is not compatible with this app." = "Версията на настолното приложение %@ не е съвместима с това приложение."; + +/* No comment provided by engineer. */ +"Desktop devices" = "Настолни устройства"; + /* No comment provided by engineer. */ "Develop" = "Разработване"; @@ -1117,7 +1426,7 @@ "Direct messages" = "Лични съобщения"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Личните съобщения между членовете са забранени в тази група."; +"Direct messages between members are prohibited." = "Личните съобщения между членовете са забранени в тази група."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Деактивиране (запазване на промените)"; @@ -1141,7 +1450,7 @@ "Disappearing messages are prohibited in this chat." = "Изчезващите съобщения са забранени в този чат."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Изчезващите съобщения са забранени в тази група."; +"Disappearing messages are prohibited." = "Изчезващите съобщения са забранени в тази група."; /* No comment provided by engineer. */ "Disappears at" = "Изчезва в"; @@ -1152,12 +1461,21 @@ /* server test step */ "Disconnect" = "Прекъсни връзката"; +/* No comment provided by engineer. */ +"Disconnect desktop?" = "Прекъсни връзката с настолното устройство?"; + /* No comment provided by engineer. */ "Discover and join groups" = "Открийте и се присъединете към групи"; +/* No comment provided by engineer. */ +"Discover via local network" = "Откриване през локалната мрежа"; + /* No comment provided by engineer. */ "Do it later" = "Отложи"; +/* No comment provided by engineer. */ +"Do not send history to new members." = "Не изпращай история на нови членове."; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "НЕ използвайте SimpleX за спешни повиквания."; @@ -1173,11 +1491,24 @@ /* No comment provided by engineer. */ "Downgrade and open chat" = "Понижи версията и отвори чата"; +/* alert button +chat item action */ +"Download" = "Изтегли"; + +/* No comment provided by engineer. */ +"Download failed" = "Неуспешно изтегляне"; + /* server test step */ "Download file" = "Свали файл"; /* No comment provided by engineer. */ -"Duplicate display name!" = "Дублирано показвано име!"; +"Downloading archive" = "Архива се изтегля"; + +/* No comment provided by engineer. */ +"Downloading link details" = "Подробности за линка се изтеглят"; + +/* No comment provided by engineer. */ +"Duplicate display name!" = "Дублирано име!"; /* integrity error chat item */ "duplicate message" = "дублирано съобщение"; @@ -1200,12 +1531,18 @@ /* No comment provided by engineer. */ "Enable (keep overrides)" = "Активиране (запазване на промените)"; -/* No comment provided by engineer. */ +/* alert title */ "Enable automatic message deletion?" = "Активиране на автоматично изтриване на съобщения?"; +/* No comment provided by engineer. */ +"Enable camera access" = "Разреши достъпа до камерата"; + /* No comment provided by engineer. */ "Enable for all" = "Активиране за всички"; +/* No comment provided by engineer. */ +"Enable in direct chats (BETA)!" = "Активиране в личните чатове (БЕТА)!"; + /* No comment provided by engineer. */ "Enable instant notifications?" = "Активирай незабавни известия?"; @@ -1233,6 +1570,9 @@ /* enabled status */ "enabled" = "активирано"; +/* No comment provided by engineer. */ +"Enabled for" = "Активирано за"; + /* enabled status */ "enabled for contact" = "активирано за контакт"; @@ -1257,6 +1597,9 @@ /* notification */ "Encrypted message or another event" = "Криптирано съобщение или друго събитие"; +/* notification */ +"Encrypted message: app is stopped" = "Криптирано съобщение: приложението е спряно"; + /* notification */ "Encrypted message: database error" = "Криптирано съобщение: грешка в базата данни"; @@ -1290,6 +1633,12 @@ /* chat item text */ "encryption re-negotiation allowed for %@" = "разрешено повторно договаряне на криптиране за %@"; +/* message decrypt error item */ +"Encryption re-negotiation error" = "Грешка при повторно договаряне на криптиране"; + +/* No comment provided by engineer. */ +"Encryption re-negotiation failed." = "Неуспешно повторно договаряне на криптирането."; + /* chat item text */ "encryption re-negotiation required" = "необходимо е повторно договаряне на криптиране"; @@ -1305,9 +1654,15 @@ /* No comment provided by engineer. */ "Enter correct passphrase." = "Въведи правилна парола."; +/* No comment provided by engineer. */ +"Enter group name…" = "Въведи име на групата…"; + /* No comment provided by engineer. */ "Enter Passcode" = "Въведете kодa за достъп"; +/* No comment provided by engineer. */ +"Enter passphrase" = "Въведи парола"; + /* No comment provided by engineer. */ "Enter passphrase…" = "Въведи парола…"; @@ -1317,12 +1672,18 @@ /* No comment provided by engineer. */ "Enter server manually" = "Въведи сървъра ръчно"; +/* No comment provided by engineer. */ +"Enter this device name…" = "Въведи името на това устройство…"; + /* placeholder */ "Enter welcome message…" = "Въведи съобщение при посрещане…"; /* placeholder */ "Enter welcome message… (optional)" = "Въведи съобщение при посрещане…(незадължително)"; +/* No comment provided by engineer. */ +"Enter your name…" = "Въведи своето име…"; + /* No comment provided by engineer. */ "error" = "грешка"; @@ -1335,9 +1696,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Грешка при приемане на заявка за контакт"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Грешка при достъпа до файла с базата данни"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Грешка при добавяне на член(ове)"; @@ -1362,6 +1720,9 @@ /* No comment provided by engineer. */ "Error creating member contact" = "Грешка при създаване на контакт с член"; +/* No comment provided by engineer. */ +"Error creating message" = "Грешка при създаване на съобщение"; + /* No comment provided by engineer. */ "Error creating profile!" = "Грешка при създаване на профил!"; @@ -1369,7 +1730,7 @@ "Error decrypting file" = "Грешка при декриптирането на файла"; /* No comment provided by engineer. */ -"Error deleting chat database" = "Грешка при изтриване на чат базата данни"; +"Error deleting chat database" = "Грешка при изтриване на базата данни"; /* No comment provided by engineer. */ "Error deleting chat!" = "Грешка при изтриването на чата!"; @@ -1377,9 +1738,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Грешка при изтриване на връзката"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Грешка при изтриване на контакт"; - /* No comment provided by engineer. */ "Error deleting database" = "Грешка при изтриване на базата данни"; @@ -1392,6 +1750,9 @@ /* No comment provided by engineer. */ "Error deleting user profile" = "Грешка при изтриване на потребителския профил"; +/* No comment provided by engineer. */ +"Error downloading the archive" = "Грешка при изтеглянето на архива"; + /* No comment provided by engineer. */ "Error enabling delivery receipts!" = "Грешка при активирането на потвърждениeто за доставка!"; @@ -1402,26 +1763,23 @@ "Error encrypting database" = "Грешка при криптиране на базата данни"; /* No comment provided by engineer. */ -"Error exporting chat database" = "Грешка при експортиране на чат базата данни"; +"Error exporting chat database" = "Грешка при експортиране на базата данни"; /* No comment provided by engineer. */ -"Error importing chat database" = "Грешка при импортиране на чат базата данни"; +"Error importing chat database" = "Грешка при импортиране на базата данни"; /* No comment provided by engineer. */ "Error joining group" = "Грешка при присъединяване към група"; /* No comment provided by engineer. */ -"Error loading %@ servers" = "Грешка при зареждане на %@ сървъри"; +"Error opening chat" = "Грешка при отваряне на чата"; -/* No comment provided by engineer. */ +/* alert title */ "Error receiving file" = "Грешка при получаване на файл"; /* No comment provided by engineer. */ "Error removing member" = "Грешка при отстраняване на член"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "Грешка при запазване на %@ сървъра"; - /* No comment provided by engineer. */ "Error saving group profile" = "Грешка при запазване на профила на групата"; @@ -1434,9 +1792,15 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "Грешка при запазване на парола в Кeychain"; +/* when migrating */ +"Error saving settings" = "Грешка при запазване на настройките"; + /* No comment provided by engineer. */ "Error saving user password" = "Грешка при запазване на потребителска парола"; +/* No comment provided by engineer. */ +"Error scanning code: %@" = "Грешка при сканиране на кода: %@"; + /* No comment provided by engineer. */ "Error sending email" = "Грешка при изпращане на имейл"; @@ -1455,7 +1819,7 @@ /* No comment provided by engineer. */ "Error stopping chat" = "Грешка при спиране на чата"; -/* No comment provided by engineer. */ +/* alertTitle */ "Error switching profile!" = "Грешка при смяна на профил!"; /* No comment provided by engineer. */ @@ -1474,9 +1838,17 @@ "Error updating user privacy" = "Грешка при актуализиране на поверителността на потребителя"; /* No comment provided by engineer. */ -"Error: " = "Грешка: "; +"Error uploading the archive" = "Грешка при качването на архива"; /* No comment provided by engineer. */ +"Error verifying passphrase:" = "Грешка при проверката на паролата:"; + +/* No comment provided by engineer. */ +"Error: " = "Грешка: "; + +/* alert message +file error text +snd error text */ "Error: %@" = "Грешка: %@"; /* No comment provided by engineer. */ @@ -1488,12 +1860,12 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Дори когато е деактивиран в разговора."; -/* No comment provided by engineer. */ -"event happened" = "събитие се случи"; - /* No comment provided by engineer. */ "Exit without saving" = "Изход без запазване"; +/* chat item action */ +"Expand" = "Разшири"; + /* No comment provided by engineer. */ "Export database" = "Експортирай база данни"; @@ -1503,6 +1875,9 @@ /* No comment provided by engineer. */ "Exported database archive." = "Експортиран архив на базата данни."; +/* No comment provided by engineer. */ +"Exported file doesn't exist" = "Експортираният файл не съществува"; + /* No comment provided by engineer. */ "Exporting database archive…" = "Експортиране на архив на базата данни…"; @@ -1513,6 +1888,9 @@ "Fast and no wait until the sender is online!" = "Бързо и без чакане, докато подателят е онлайн!"; /* No comment provided by engineer. */ +"Faster joining and more reliable messages." = "По-бързо присъединяване и по-надеждни съобщения."; + +/* swipe action */ "Favorite" = "Любим"; /* No comment provided by engineer. */ @@ -1534,7 +1912,10 @@ "Files and media" = "Файлове и медия"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Файловете и медията са забранени в тази група."; +"Files and media are prohibited." = "Файловете и медията са забранени в тази група."; + +/* No comment provided by engineer. */ +"Files and media not allowed" = "Файлове и медия не са разрешени"; /* No comment provided by engineer. */ "Files and media prohibited!" = "Файловете и медията са забранени!"; @@ -1542,6 +1923,12 @@ /* No comment provided by engineer. */ "Filter unread and favorite chats." = "Филтрирайте непрочетените и любимите чатове."; +/* No comment provided by engineer. */ +"Finalize migration" = "Завърши миграцията"; + +/* No comment provided by engineer. */ +"Finalize migration on another device." = "Завършете миграцията на другото устройство."; + /* No comment provided by engineer. */ "Finally, we have them! 🚀" = "Най-накрая ги имаме! 🚀"; @@ -1569,6 +1956,24 @@ /* No comment provided by engineer. */ "For console" = "За конзолата"; +/* chat item action */ +"Forward" = "Препрати"; + +/* No comment provided by engineer. */ +"Forward and save messages" = "Препращане и запазване на съобщения"; + +/* No comment provided by engineer. */ +"forwarded" = "препратено"; + +/* No comment provided by engineer. */ +"Forwarded" = "Препратено"; + +/* No comment provided by engineer. */ +"Forwarded from" = "Препратено от"; + +/* No comment provided by engineer. */ +"Found desktop" = "Намерено настолно устройство"; + /* No comment provided by engineer. */ "French interface" = "Френски интерфейс"; @@ -1579,7 +1984,7 @@ "Full name (optional)" = "Пълно име (незадължително)"; /* No comment provided by engineer. */ -"Full name:" = "Пълно име:"; +"Fully decentralized – visible only to members." = "Напълно децентрализирана – видима е само за членовете."; /* No comment provided by engineer. */ "Fully re-implemented - work in background!" = "Напълно преработено - работи във фонов режим!"; @@ -1593,11 +1998,17 @@ /* No comment provided by engineer. */ "Group" = "Група"; +/* No comment provided by engineer. */ +"Group already exists" = "Групата вече съществува"; + +/* No comment provided by engineer. */ +"Group already exists!" = "Групата вече съществува!"; + /* No comment provided by engineer. */ "group deleted" = "групата е изтрита"; /* No comment provided by engineer. */ -"Group display name" = "Показвано име на групата"; +"Group display name" = "Име на групата"; /* No comment provided by engineer. */ "Group full name (optional)" = "Пълно име на групата (незадължително)"; @@ -1620,24 +2031,6 @@ /* No comment provided by engineer. */ "Group links" = "Групови линкове"; -/* No comment provided by engineer. */ -"Group members can add message reactions." = "Членовете на групата могат да добавят реакции към съобщенията."; - -/* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages." = "Членовете на групата могат необратимо да изтриват изпратените съобщения."; - -/* No comment provided by engineer. */ -"Group members can send direct messages." = "Членовете на групата могат да изпращат лични съобщения."; - -/* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Членовете на групата могат да изпращат изчезващи съобщения."; - -/* No comment provided by engineer. */ -"Group members can send files and media." = "Членовете на групата могат да изпращат файлове и медия."; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "Членовете на групата могат да изпращат гласови съобщения."; - /* notification */ "Group message:" = "Групово съобщение:"; @@ -1692,12 +2085,12 @@ /* No comment provided by engineer. */ "History" = "История"; +/* No comment provided by engineer. */ +"History is not sent to new members." = "Историята не се изпраща на нови членове."; + /* time unit */ "hours" = "часове"; -/* No comment provided by engineer. */ -"How it works" = "Как работи"; - /* No comment provided by engineer. */ "How SimpleX works" = "Как работи SimpleX"; @@ -1710,15 +2103,15 @@ /* No comment provided by engineer. */ "How to use your servers" = "Как да използвате вашите сървъри"; +/* No comment provided by engineer. */ +"Hungarian interface" = "Унгарски интерфейс"; + /* No comment provided by engineer. */ "ICE servers (one per line)" = "ICE сървъри (по един на ред)"; /* No comment provided by engineer. */ "If you can't meet in person, show QR code in a video call, or share the link." = "Ако не можете да се срещнете лично, покажете QR код във видеоразговора или споделете линка."; -/* No comment provided by engineer. */ -"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Ако не можете да се срещнете на живо, можете да **сканирате QR код във видеообаждането** или вашият контакт може да сподели линк за покана."; - /* No comment provided by engineer. */ "If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Ако въведете този kод за достъп, когато отваряте приложението, всички данни от приложението ще бъдат необратимо изтрити!"; @@ -1741,29 +2134,47 @@ "Immediately" = "Веднага"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Защитен от спам и злоупотреби"; +"Immune to spam" = "Защитен от спам и злоупотреби"; /* No comment provided by engineer. */ "Import" = "Импортиране"; /* No comment provided by engineer. */ -"Import chat database?" = "Импортиране на чат база данни?"; +"Import chat database?" = "Импортиране на база данни?"; /* No comment provided by engineer. */ "Import database" = "Импортиране на база данни"; +/* No comment provided by engineer. */ +"Import failed" = "Неуспешно импортиране"; + +/* No comment provided by engineer. */ +"Importing archive" = "Импортиране на архив"; + +/* No comment provided by engineer. */ +"Improved message delivery" = "Подобрена доставка на съобщения"; + /* No comment provided by engineer. */ "Improved privacy and security" = "Подобрена поверителност и сигурност"; /* No comment provided by engineer. */ "Improved server configuration" = "Подобрена конфигурация на сървъра"; +/* No comment provided by engineer. */ +"In order to continue, chat should be stopped." = "За да продължите, чатът трябва да бъде спрян."; + /* No comment provided by engineer. */ "In reply to" = "В отговор на"; +/* No comment provided by engineer. */ +"In-call sounds" = "Звуци по време на разговор"; + /* No comment provided by engineer. */ "Incognito" = "Инкогнито"; +/* No comment provided by engineer. */ +"Incognito groups" = "Инкогнито групи"; + /* No comment provided by engineer. */ "Incognito mode" = "Режим инкогнито"; @@ -1791,6 +2202,9 @@ /* No comment provided by engineer. */ "Incompatible database version" = "Несъвместима версия на базата данни"; +/* No comment provided by engineer. */ +"Incompatible version" = "Несъвместима версия"; + /* PIN entry */ "Incorrect passcode" = "Неправилен kод за достъп"; @@ -1810,10 +2224,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Инсталирайте [SimpleX Chat за терминал](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "Незабавните push известия ще бъдат скрити!\n"; +"Instant" = "Мигновено"; /* No comment provided by engineer. */ -"Instantly" = "Мигновено"; +"Instant push notifications will be hidden!\n" = "Незабавните push известия ще бъдат скрити!\n"; /* No comment provided by engineer. */ "Interface" = "Интерфейс"; @@ -1831,6 +2245,24 @@ "invalid data" = "невалидни данни"; /* No comment provided by engineer. */ +"Invalid display name!" = "Невалидно име!"; + +/* No comment provided by engineer. */ +"Invalid link" = "Невалиден линк"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Невалидно потвърждение за мигриране"; + +/* No comment provided by engineer. */ +"Invalid name!" = "Невалидно име!"; + +/* No comment provided by engineer. */ +"Invalid QR code" = "Невалиден QR код"; + +/* No comment provided by engineer. */ +"Invalid response" = "Невалиден отговор"; + +/* alert title */ "Invalid server address!" = "Невалиден адрес на сървъра!"; /* item status text */ @@ -1876,7 +2308,7 @@ "Irreversible message deletion is prohibited in this chat." = "Необратимото изтриване на съобщения е забранено в този чат."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "Необратимото изтриване на съобщения е забранено в тази група."; +"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." = "Позволява да имате много анонимни връзки без споделени данни между тях в един чат профил ."; @@ -1899,7 +2331,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Японски интерфейс"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Присъединяване"; /* No comment provided by engineer. */ @@ -1908,12 +2340,33 @@ /* No comment provided by engineer. */ "Join group" = "Влез в групата"; +/* No comment provided by engineer. */ +"Join group conversations" = "Присъединяване към групи"; + +/* No comment provided by engineer. */ +"Join group?" = "Влез в групата?"; + /* No comment provided by engineer. */ "Join incognito" = "Влез инкогнито"; +/* No comment provided by engineer. */ +"Join with current profile" = "Присъединяване с текущия профил"; + +/* No comment provided by engineer. */ +"Join your group?\nThis is your link for group %@!" = "Влез в твоята група?\nТова е вашят линк за група %@!"; + /* No comment provided by engineer. */ "Joining group" = "Присъединяване към групата"; +/* alert action */ +"Keep" = "Запази"; + +/* No comment provided by engineer. */ +"Keep the app open to use it from desktop" = "Дръжте приложението отворено, за да го използвате от настолното устройство"; + +/* alert title */ +"Keep unused invitation?" = "Запази неизползваната покана за връзка?"; + /* No comment provided by engineer. */ "Keep your connections" = "Запазете връзките си"; @@ -1929,7 +2382,7 @@ /* No comment provided by engineer. */ "Learn more" = "Научете повече"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Напусни"; /* No comment provided by engineer. */ @@ -1950,6 +2403,15 @@ /* No comment provided by engineer. */ "Limitations" = "Ограничения"; +/* No comment provided by engineer. */ +"Link mobile and desktop apps! 🔗" = "Свържете мобилни и настолни приложения! 🔗"; + +/* No comment provided by engineer. */ +"Linked desktop options" = "Настройки на запомнени настолни устройства"; + +/* No comment provided by engineer. */ +"Linked desktops" = "Запомнени настолни устройства"; + /* No comment provided by engineer. */ "LIVE" = "НА ЖИВО"; @@ -1971,24 +2433,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Режим на заключване"; -/* No comment provided by engineer. */ -"Make a private connection" = "Добави поверителна връзка"; - /* No comment provided by engineer. */ "Make one message disappear" = "Накарайте едно съобщение да изчезне"; /* No comment provided by engineer. */ "Make profile private!" = "Направи профила поверителен!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Уверете се, че %@ сървърните адреси са в правилен формат, разделени на редове и не се дублират (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Уверете се, че адресите на WebRTC ICE сървъра са в правилен формат, разделени на редове и не са дублирани."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Много хора попитаха: *ако SimpleX няма потребителски идентификатори, как може да доставя съобщения?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Маркирай като изтрито за всички"; @@ -2013,6 +2466,9 @@ /* No comment provided by engineer. */ "Member" = "Член"; +/* profile update event chat item */ +"member %@ changed to %@" = "името на члена %1$@ е променено на %2$@"; + /* rcv group event chat item */ "member connected" = "свързан"; @@ -2025,6 +2481,27 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Членът ще бъде премахнат от групата - това не може да бъде отменено!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Членовете на групата могат да добавят реакции към съобщенията."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "Членовете на групата могат необратимо да изтриват изпратените съобщения. (24 часа)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Членовете на групата могат да изпращат лични съобщения."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Членовете на групата могат да изпращат изчезващи съобщения."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Членовете на групата могат да изпращат файлове и медия."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "Членовете на групата могат да изпращат SimpleX линкове."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Членовете на групата могат да изпращат гласови съобщения."; + /* item status text */ "Message delivery error" = "Грешка при доставката на съобщението"; @@ -2041,23 +2518,59 @@ "Message reactions are prohibited in this chat." = "Реакциите на съобщения са забранени в този чат."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Реакциите на съобщения са забранени в тази група."; +"Message reactions are prohibited." = "Реакциите на съобщения са забранени в тази група."; /* notification */ "message received" = "получено съобщение"; +/* No comment provided by engineer. */ +"Message source remains private." = "Източникът на съобщението остава скрит."; + /* No comment provided by engineer. */ "Message text" = "Текст на съобщението"; +/* No comment provided by engineer. */ +"Message too large" = "Съобщението е твърде голямо"; + /* No comment provided by engineer. */ "Messages" = "Съобщения"; /* No comment provided by engineer. */ "Messages & files" = "Съобщения и файлове"; +/* No comment provided by engineer. */ +"Messages from %@ will be shown!" = "Съобщенията от %@ ще бъдат показани!"; + +/* 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." = "Съобщенията, файловете и разговорите са защитени чрез **криптиране от край до край** с перфектна секретност при препращане, правдоподобно опровержение и възстановяване при взлом."; + +/* 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 device" = "Мигрирай устройството"; + +/* No comment provided by engineer. */ +"Migrate from another device" = "Мигриране от друго устройство"; + +/* No comment provided by engineer. */ +"Migrate here" = "Мигрирай тук"; + +/* No comment provided by engineer. */ +"Migrate to another device" = "Миграция към друго устройство"; + +/* No comment provided by engineer. */ +"Migrate to another device via QR code." = "Мигрирайте към друго устройство чрез QR код."; + +/* No comment provided by engineer. */ +"Migrating" = "Мигриране"; + /* No comment provided by engineer. */ "Migrating database archive…" = "Архивът на базата данни се мигрира…"; +/* No comment provided by engineer. */ +"Migration complete" = "Миграцията е завършена"; + /* No comment provided by engineer. */ "Migration error:" = "Грешка при мигриране:"; @@ -2068,7 +2581,7 @@ "Migration is completed" = "Миграцията е завършена"; /* No comment provided by engineer. */ -"Migrations: %@" = "Миграции: %@"; +"Migrations:" = "Миграции:"; /* time unit */ "minutes" = "минути"; @@ -2088,7 +2601,7 @@ /* copied message info */ "Moderated at: %@" = "Модерирано в: %@"; -/* No comment provided by engineer. */ +/* marked deleted chat item preview text */ "moderated by %@" = "модерирано от %@"; /* time unit */ @@ -2097,50 +2610,56 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Очаквайте скоро още подобрения!"; +/* No comment provided by engineer. */ +"More reliable network connection." = "По-надеждна мрежова връзка."; + /* item status description */ "Most likely this connection is deleted." = "Най-вероятно тази връзка е изтрита."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Най-вероятно този контакт е изтрил връзката с вас."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Множество профили за чат"; -/* No comment provided by engineer. */ +/* notification label action */ "Mute" = "Без звук"; /* No comment provided by engineer. */ "Muted when inactive!" = "Без звук при неактивност!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Име"; /* No comment provided by engineer. */ "Network & servers" = "Мрежа и сървъри"; +/* No comment provided by engineer. */ +"Network connection" = "Мрежова връзка"; + +/* No comment provided by engineer. */ +"Network management" = "Управление на мрежата"; + /* No comment provided by engineer. */ "Network settings" = "Мрежови настройки"; /* No comment provided by engineer. */ "Network status" = "Състояние на мрежата"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "никога"; +/* No comment provided by engineer. */ +"New chat" = "Нов чат"; + /* notification */ "New contact request" = "Нова заявка за контакт"; /* notification */ "New contact:" = "Нов контакт:"; -/* No comment provided by engineer. */ -"New database archive" = "Нов архив на база данни"; - /* No comment provided by engineer. */ "New desktop app!" = "Ново настолно приложение!"; /* No comment provided by engineer. */ -"New display name" = "Ново показвано име"; +"New display name" = "Ново име"; /* No comment provided by engineer. */ "New in %@" = "Ново в %@"; @@ -2193,15 +2712,27 @@ /* No comment provided by engineer. */ "No history" = "Няма история"; +/* No comment provided by engineer. */ +"No network connection" = "Няма мрежова връзка"; + /* No comment provided by engineer. */ "No permission to record voice message" = "Няма разрешение за запис на гласово съобщение"; +/* No comment provided by engineer. */ +"No push server" = "Локално"; + /* No comment provided by engineer. */ "No received or sent files" = "Няма получени или изпратени файлове"; /* copied message info in history */ "no text" = "няма текст"; +/* No comment provided by engineer. */ +"No user identifiers." = "Първата платформа без никакви потребителски идентификатори – поверителна по дизайн."; + +/* No comment provided by engineer. */ +"Not compatible!" = "Несъвместим!"; + /* No comment provided by engineer. */ "Notifications" = "Известия"; @@ -2215,30 +2746,27 @@ "observer" = "наблюдател"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "изключено"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Изключено"; -/* No comment provided by engineer. */ -"Off (Local)" = "Изключено (Локално)"; - /* feature offered item */ "offered %@" = "предлага %@"; /* feature offered item */ "offered %@: %@" = "предлага %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Ок"; /* No comment provided by engineer. */ -"Old database" = "Стара база данни"; +"OK" = "ОК"; /* No comment provided by engineer. */ -"Old database archive" = "Стар архив на база данни"; +"Old database" = "Стара база данни"; /* group pref value */ "on" = "включено"; @@ -2247,16 +2775,16 @@ "One-time invitation link" = "Линк за еднократна покана"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "За свързване ще са необходими Onion хостове. Изисква се активиране на VPN."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "За свързване ще са **необходими** Onion хостове.\nИзисква се активиране на VPN."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Ще се използват Onion хостове, когато са налични. Изисква се активиране на VPN."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Ще се използват Onion хостове, когато са налични.\nИзисква се активиране на VPN."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Няма се използват Onion хостове."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Само потребителските устройства съхраняват потребителски профили, контакти, групи и съобщения, изпратени с **двуслойно криптиране от край до край**."; +"Only client devices store user profiles, contacts, groups, and messages." = "Само потребителските устройства съхраняват потребителски профили, контакти, групи и съобщения, изпратени с **двуслойно криптиране от край до край**."; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Само собствениците на групата могат да променят груповите настройки."; @@ -2271,7 +2799,7 @@ "Only you can add message reactions." = "Само вие можете да добавяте реакции на съобщенията."; /* No comment provided by engineer. */ -"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "Само вие можете необратимо да изтриете съобщения (вашият контакт може да ги маркира за изтриване)."; +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Само вие можете необратимо да изтриете съобщения (вашият контакт може да ги маркира за изтриване). (24 часа)"; /* No comment provided by engineer. */ "Only you can make calls." = "Само вие можете да извършвате разговори."; @@ -2286,7 +2814,7 @@ "Only your contact can add message reactions." = "Само вашият контакт може да добавя реакции на съобщенията."; /* No comment provided by engineer. */ -"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Само вашият контакт може необратимо да изтрие съобщения (можете да ги маркирате за изтриване)."; +"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Само вашият контакт може необратимо да изтрие съобщения (можете да ги маркирате за изтриване). (24 часа)"; /* No comment provided by engineer. */ "Only your contact can make calls." = "Само вашият контакт може да извършва разговори."; @@ -2297,7 +2825,7 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Само вашият контакт може да изпраща гласови съобщения."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Отвори"; /* No comment provided by engineer. */ @@ -2307,20 +2835,38 @@ "Open chat console" = "Отвори конзолата"; /* No comment provided by engineer. */ -"Open Settings" = "Отвори настройки"; +"Open group" = "Отвори група"; /* authentication reason */ -"Open user profiles" = "Отвори потребителските профили"; +"Open migration to another device" = "Отвори миграцията към друго устройство"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Протокол и код с отворен код – всеки може да оперира собствени сървъри."; +"Open Settings" = "Отвори настройки"; /* No comment provided by engineer. */ -"Opening database…" = "Отваряне на база данни…"; +"Opening app…" = "Приложението се отваря…"; + +/* No comment provided by engineer. */ +"Or paste archive link" = "Или постави архивен линк"; + +/* No comment provided by engineer. */ +"Or scan QR code" = "Или сканирай QR код"; + +/* No comment provided by engineer. */ +"Or securely share this file link" = "Или сигурно споделете този линк към файла"; + +/* No comment provided by engineer. */ +"Or show this code" = "Или покажи този код"; + +/* No comment provided by engineer. */ +"Other" = "Други"; /* member role */ "owner" = "собственик"; +/* feature role */ +"owners" = "собственици"; + /* No comment provided by engineer. */ "Passcode" = "Код за достъп"; @@ -2339,30 +2885,33 @@ /* No comment provided by engineer. */ "Password to show" = "Парола за показване"; +/* past/unknown group member */ +"Past member %@" = "Бивш член %@"; + /* No comment provided by engineer. */ -"Paste" = "Постави"; +"Paste desktop address" = "Постави адрес на настолно устройство"; /* No comment provided by engineer. */ "Paste image" = "Постави изображение"; /* No comment provided by engineer. */ -"Paste received link" = "Постави получения линк"; +"Paste link to connect!" = "Поставете линк, за да се свържете!"; -/* placeholder */ -"Paste the link you received to connect with your contact." = "Поставете линка, който сте получили, за да се свържете с вашия контакт."; +/* No comment provided by engineer. */ +"Paste the link you received" = "Постави получения линк"; /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Хората могат да се свържат с вас само чрез ликовете, които споделяте."; - -/* No comment provided by engineer. */ -"Periodically" = "Периодично"; +"Periodic" = "Периодично"; /* message decrypt error item */ "Permanent decryption error" = "Постоянна грешка при декриптиране"; +/* No comment provided by engineer. */ +"Picture-in-picture calls" = "Обаждания \"картина в картина\""; + /* No comment provided by engineer. */ "PING count" = "PING бройка"; @@ -2381,6 +2930,12 @@ /* No comment provided by engineer. */ "Please check yours and your contact preferences." = "Моля, проверете вашите настройки и тези вашия за контакт."; +/* No comment provided by engineer. */ +"Please confirm that network settings are correct for this device." = "Моля, потвърдете, че мрежовите настройки са правилни за това устройство."; + +/* No comment provided by engineer. */ +"Please contact developers.\nError: %@" = "Моля, свържете се с разработчиците.\nГрешка: %@"; + /* No comment provided by engineer. */ "Please contact group admin." = "Моля, свържете се с груповия администартор."; @@ -2414,9 +2969,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Запазете последната чернова на съобщението с прикачени файлове."; -/* No comment provided by engineer. */ -"Preset server" = "Предварително зададен сървър"; - /* No comment provided by engineer. */ "Preset server address" = "Предварително зададен адрес на сървъра"; @@ -2432,6 +2984,9 @@ /* No comment provided by engineer. */ "Private filenames" = "Поверителни имена на файлове"; +/* name of notes to self */ +"Private notes" = "Лични бележки"; + /* No comment provided by engineer. */ "Profile and server connections" = "Профилни и сървърни връзки"; @@ -2439,9 +2994,12 @@ "Profile image" = "Профилно изображение"; /* No comment provided by engineer. */ -"Profile password" = "Профилна парола"; +"Profile images" = "Профилни изображения"; /* No comment provided by engineer. */ +"Profile password" = "Профилна парола"; + +/* alert message */ "Profile update will be sent to your contacts." = "Актуализацията на профила ще бъде изпратена до вашите контакти."; /* No comment provided by engineer. */ @@ -2465,6 +3023,9 @@ /* 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." = "Забрани изпращането на гласови съобщения."; @@ -2483,20 +3044,32 @@ /* No comment provided by engineer. */ "Push notifications" = "Push известия"; +/* No comment provided by engineer. */ +"Push server" = "Push сървър"; + +/* chat item text */ +"quantum resistant e2e encryption" = "квантово устойчиво e2e криптиране"; + +/* No comment provided by engineer. */ +"Quantum resistant encryption" = "Квантово устойчиво криптиране"; + /* No comment provided by engineer. */ "Rate the app" = "Оценете приложението"; /* chat item menu */ "React…" = "Реагирай…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Прочетено"; /* No comment provided by engineer. */ "Read more" = "Прочетете още"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"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)."; + +/* 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)."; @@ -2504,9 +3077,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Прочетете повече в нашето [GitHub хранилище](https://github.com/simplex-chat/simplex-chat#readme)."; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Прочетете повече в нашето хранилище в GitHub."; - /* No comment provided by engineer. */ "Receipts are disabled" = "Потвърждениeто за доставка е деактивирано"; @@ -2537,6 +3107,12 @@ /* No comment provided by engineer. */ "Receiving via" = "Получаване чрез"; +/* No comment provided by engineer. */ +"Recent history and improved [directory bot](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)." = "Скорошна история и подобрен [bot за директория за групи](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPd jdLW3%23%2F%3Fv%3D1-2% 26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)."; + +/* No comment provided by engineer. */ +"Recipient(s) can't see who this message is from." = "Получателят(ите) не могат да видят от кого е това съобщение."; + /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Получателите виждат актуализации, докато ги въвеждате."; @@ -2555,7 +3131,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Намалена консумация на батерията"; -/* reject incoming call via notification */ +/* reject incoming call via notification +swipe action */ "Reject" = "Отхвърляне"; /* No comment provided by engineer. */ @@ -2591,6 +3168,12 @@ /* rcv group event chat item */ "removed %@" = "отстранен %@"; +/* profile update event chat item */ +"removed contact address" = "премахнат адрес за контакт"; + +/* profile update event chat item */ +"removed profile picture" = "премахната профилна снимка"; + /* rcv group event chat item */ "removed you" = "ви острани"; @@ -2603,6 +3186,21 @@ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Предоговори криптирането?"; +/* No comment provided by engineer. */ +"Repeat connection request?" = "Изпрати отново заявката за свързване?"; + +/* No comment provided by engineer. */ +"Repeat download" = "Повтори изтеглянето"; + +/* No comment provided by engineer. */ +"Repeat import" = "Повтори импортирането"; + +/* No comment provided by engineer. */ +"Repeat join request?" = "Изпрати отново заявката за присъединяване?"; + +/* No comment provided by engineer. */ +"Repeat upload" = "Повтори качването"; + /* chat item action */ "Reply" = "Отговори"; @@ -2636,12 +3234,12 @@ /* No comment provided by engineer. */ "Restore database error" = "Грешка при възстановяване на базата данни"; +/* No comment provided by engineer. */ +"Retry" = "Опитай отново"; + /* chat item action */ "Reveal" = "Покажи"; -/* No comment provided by engineer. */ -"Revert" = "Отмени промените"; - /* No comment provided by engineer. */ "Revoke" = "Отзови"; @@ -2657,13 +3255,17 @@ /* No comment provided by engineer. */ "Run chat" = "Стартиране на чат"; -/* chat item action */ +/* No comment provided by engineer. */ +"Safer groups" = "По-безопасни групи"; + +/* alert button +chat item action */ "Save" = "Запази"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Запази (и уведоми контактите)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Запази и уведоми контакта"; /* No comment provided by engineer. */ @@ -2672,12 +3274,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "Запази и актуализирай профила на групата"; -/* No comment provided by engineer. */ -"Save archive" = "Запази архив"; - -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Запази настройките за автоматично приемане"; - /* No comment provided by engineer. */ "Save group profile" = "Запази профила на групата"; @@ -2687,7 +3283,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Запази паролата в Keychain"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Запази настройките?"; /* No comment provided by engineer. */ @@ -2696,14 +3292,26 @@ /* No comment provided by engineer. */ "Save servers" = "Запази сървърите"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Запази сървърите?"; /* No comment provided by engineer. */ -"Save settings?" = "Запази настройките?"; +"Save welcome message?" = "Запази съобщението при посрещане?"; /* No comment provided by engineer. */ -"Save welcome message?" = "Запази съобщението при посрещане?"; +"saved" = "запазено"; + +/* No comment provided by engineer. */ +"Saved" = "Запазено"; + +/* No comment provided by engineer. */ +"Saved from" = "Запазено от"; + +/* No comment provided by engineer. */ +"saved from %@" = "запазено от %@"; + +/* message info title */ +"Saved message" = "Запазено съобщение"; /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Запазените WebRTC ICE сървъри ще бъдат премахнати"; @@ -2714,6 +3322,9 @@ /* No comment provided by engineer. */ "Scan QR code" = "Сканирай QR код"; +/* No comment provided by engineer. */ +"Scan QR code from desktop" = "Сканирай QR код от настолното устройство"; + /* No comment provided by engineer. */ "Scan security code from your contact's app." = "Сканирайте кода за сигурност от приложението на вашия контакт."; @@ -2723,6 +3334,12 @@ /* No comment provided by engineer. */ "Search" = "Търсене"; +/* No comment provided by engineer. */ +"Search bar accepts invitation links." = "Лентата за търсене приема линк за връзка."; + +/* No comment provided by engineer. */ +"Search or paste SimpleX link" = "Търсене или поставяне на SimpleX линк"; + /* network option */ "sec" = "сек."; @@ -2744,7 +3361,7 @@ /* chat item text */ "security code changed" = "кодът за сигурност е променен"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Избери"; /* No comment provided by engineer. */ @@ -2771,9 +3388,6 @@ /* No comment provided by engineer. */ "send direct message" = "изпрати лично съобщение"; -/* No comment provided by engineer. */ -"Send direct message" = "Изпрати лично съобщение"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Изпрати лично съобщение за свързване"; @@ -2789,9 +3403,6 @@ /* No comment provided by engineer. */ "Send notifications" = "Изпращай известия"; -/* No comment provided by engineer. */ -"Send notifications:" = "Изпратени известия:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "Изпращайте въпроси и идеи"; @@ -2802,6 +3413,9 @@ "Send them from gallery or custom keyboards." = "Изпрати от галерия или персонализирани клавиатури."; /* No comment provided by engineer. */ +"Send up to 100 last messages to new members." = "Изпращане до последните 100 съобщения на нови членове."; + +/* alert message */ "Sender cancelled file transfer." = "Подателят отмени прехвърлянето на файла."; /* No comment provided by engineer. */ @@ -2858,6 +3472,9 @@ /* No comment provided by engineer. */ "Servers" = "Сървъри"; +/* No comment provided by engineer. */ +"Session code" = "Код на сесията"; + /* No comment provided by engineer. */ "Set 1 day" = "Задай 1 ден"; @@ -2870,9 +3487,18 @@ /* No comment provided by engineer. */ "Set it instead of system authentication." = "Задайте го вместо системната идентификация."; +/* profile update event chat item */ +"set new contact address" = "зададен нов адрес за контакт"; + +/* profile update event chat item */ +"set new profile picture" = "зададена нова профилна снимка"; + /* No comment provided by engineer. */ "Set passcode" = "Задай kод за достъп"; +/* No comment provided by engineer. */ +"Set passphrase" = "Задаване на парола"; + /* No comment provided by engineer. */ "Set passphrase to export" = "Задай парола за експортиране"; @@ -2885,7 +3511,11 @@ /* No comment provided by engineer. */ "Settings" = "Настройки"; -/* chat item action */ +/* No comment provided by engineer. */ +"Shape profile images" = "Променете формата на профилните изображения"; + +/* alert action +chat item action */ "Share" = "Сподели"; /* No comment provided by engineer. */ @@ -2894,14 +3524,14 @@ /* No comment provided by engineer. */ "Share address" = "Сподели адрес"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "Сподели адреса с контактите?"; /* No comment provided by engineer. */ "Share link" = "Сподели линк"; /* No comment provided by engineer. */ -"Share one-time invitation link" = "Сподели линк за еднократна покана"; +"Share this 1-time invite link" = "Сподели този еднократен линк за връзка"; /* No comment provided by engineer. */ "Share with contacts" = "Сподели с контактите"; @@ -2918,6 +3548,9 @@ /* No comment provided by engineer. */ "Show preview" = "Показване на визуализация"; +/* No comment provided by engineer. */ +"Show QR code" = "Покажи QR код"; + /* No comment provided by engineer. */ "Show:" = "Покажи:"; @@ -2939,9 +3572,15 @@ /* simplex link type */ "SimpleX group link" = "SimpleX групов линк"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "SimpleX линкове"; +/* No comment provided by engineer. */ +"SimpleX links are prohibited." = "SimpleX линкове са забранени в тази група."; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "SimpleX линковете не са разрешени"; + /* No comment provided by engineer. */ "SimpleX Lock" = "SimpleX заключване"; @@ -2969,9 +3608,6 @@ /* No comment provided by engineer. */ "Small groups (max 20)" = "Малки групи (максимум 20)"; -/* No comment provided by engineer. */ -"SMP servers" = "SMP сървъри"; - /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Някои не-фатални грешки са възникнали по време на импортиране - може да видите конзолата за повече подробности."; @@ -2979,11 +3615,17 @@ "Somebody" = "Някой"; /* No comment provided by engineer. */ -"Start a new chat" = "Започни нов чат"; +"Square, circle, or anything in between." = "Квадрат, кръг или нещо между тях."; + +/* chat item text */ +"standard end-to-end encryption" = "стандартно криптиране от край до край"; /* No comment provided by engineer. */ "Start chat" = "Започни чат"; +/* No comment provided by engineer. */ +"Start chat?" = "Стартирай чата?"; + /* No comment provided by engineer. */ "Start migration" = "Започни миграция"; @@ -2994,7 +3636,7 @@ "Stop" = "Спри"; /* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Спрете чата, за да активирате действията с базата данни"; +"Stop chat" = "Спри чата"; /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Спрете чата, за да експортирате, импортирате или изтриете чат базата данни. Няма да можете да получавате и изпращате съобщения, докато чатът е спрян."; @@ -3011,15 +3653,18 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Спри изпращането на файла?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Спри споделянето"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Спри споделянето на адреса?"; /* authentication reason */ "Stop SimpleX" = "Спри SimpleX"; +/* No comment provided by engineer. */ +"Stopping chat" = "Спиране на чата"; + /* No comment provided by engineer. */ "strike" = "зачеркнат"; @@ -3044,6 +3689,9 @@ /* No comment provided by engineer. */ "Tap to activate profile." = "Докосни за активиране на профил."; +/* No comment provided by engineer. */ +"Tap to Connect" = "Докосни за свързване"; + /* No comment provided by engineer. */ "Tap to join" = "Докосни за вход"; @@ -3051,7 +3699,10 @@ "Tap to join incognito" = "Докосни за инкогнито вход"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Докосни за започване на нов чат"; +"Tap to paste link" = "Докосни за поставяне на линк за връзка"; + +/* No comment provided by engineer. */ +"Tap to scan" = "Докосни за сканиране"; /* No comment provided by engineer. */ "TCP connection timeout" = "Времето на изчакване за установяване на TCP връзка"; @@ -3074,7 +3725,7 @@ /* No comment provided by engineer. */ "Test servers" = "Тествай сървърите"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Тестовете са неуспешни!"; /* No comment provided by engineer. */ @@ -3086,15 +3737,15 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Благодарение на потребителите – допринесете през Weblate!"; -/* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Първата платформа без никакви потребителски идентификатори – поверителна по дизайн."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Приложението може да ви уведоми, когато получите съобщения или заявки за контакт - моля, отворете настройките, за да активирате."; /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "Опитът за промяна на паролата на базата данни не беше завършен."; +/* No comment provided by engineer. */ +"The code you scanned is not a SimpleX link QR code." = "QR кодът, който сканирахте, не е SimpleX линк за връзка."; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "Връзката, която приехте, ще бъде отказана!"; @@ -3107,6 +3758,9 @@ /* 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." = "Хешът на предишното съобщение е различен."; @@ -3119,14 +3773,11 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "Съобщението ще бъде маркирано като модерирано за всички членове."; -/* No comment provided by engineer. */ -"The next generation of private messaging" = "Ново поколение поверителни съобщения"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Старата база данни не бе премахната по време на миграцията, тя може да бъде изтрита."; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "Профилът се споделя само с вашите контакти."; +"Your profile is stored on your device and only shared with your contacts." = "Профилът се споделя само с вашите контакти."; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Втората отметка, която пропуснахме! ✅"; @@ -3138,13 +3789,7 @@ "The servers for new connections of your current chat profile **%@**." = "Сървърите за нови връзки на текущия ви чат профил **%@**."; /* No comment provided by engineer. */ -"Theme" = "Тема"; - -/* No comment provided by engineer. */ -"There should be at least one user profile." = "Трябва да има поне един потребителски профил."; - -/* No comment provided by engineer. */ -"There should be at least one visible user profile." = "Трябва да има поне един видим потребителски профил."; +"The text you pasted is not a SimpleX link." = "Текстът, който поставихте, не е SimpleX линк за връзка."; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Тези настройки са за текущия ви профил **%@**."; @@ -3161,15 +3806,33 @@ /* No comment provided by engineer. */ "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." = "Този чат е защитен чрез криптиране от край до край."; + +/* E2EE info chat item */ +"This chat is protected by quantum resistant end-to-end encryption." = "Този чат е защитен от квантово устойчиво криптиране от край до край."; + /* notification title */ "this contact" = "този контакт"; +/* No comment provided by engineer. */ +"This device name" = "Името на това устройство"; + +/* No comment provided by engineer. */ +"This display name is invalid. Please choose another name." = "Това име е невалидно. Моля, изберете друго име."; + /* No comment provided by engineer. */ "This group has over %lld members, delivery receipts are not sent." = "Тази група има над %lld членове, потвърждения за доставка не се изпращат."; /* No comment provided by engineer. */ "This group no longer exists." = "Тази група вече не съществува."; +/* No comment provided by engineer. */ +"This is your own one-time link!" = "Това е вашят еднократен линк за връзка!"; + +/* No comment provided by engineer. */ +"This is your own SimpleX address!" = "Това е вашият личен SimpleX адрес!"; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Тази настройка се прилага за съобщения в текущия ви профил **%@**."; @@ -3180,10 +3843,10 @@ "To connect, your contact can scan QR code or use the link in the app." = "За да се свърже, вашият контакт може да сканира QR код или да използва линка в приложението."; /* No comment provided by engineer. */ -"To make a new connection" = "За да направите нова връзка"; +"To hide unwanted messages." = "Скриване на нежелани съобщения."; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "За да се защити поверителността, вместо потребителски идентификатори, използвани от всички други платформи, SimpleX има идентификатори за опашки от съобщения, отделни за всеки от вашите контакти."; +"To make a new connection" = "За да направите нова връзка"; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "За да не се разкрива часовата зона, файловете с изображения/глас използват UTC."; @@ -3191,6 +3854,9 @@ /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "За да защитите информацията си, включете SimpleX заключване.\nЩе бъдете подканени да извършите идентификация, преди тази функция да бъде активирана."; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "За да се защити поверителността, вместо потребителски идентификатори, използвани от всички други платформи, SimpleX има идентификатори за опашки от съобщения, отделни за всеки от вашите контакти."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "За да запишете гласово съобщение, моля, дайте разрешение за използване на микрофон."; @@ -3216,10 +3882,10 @@ "Trying to connect to the server used to receive messages from this contact." = "Опит за свързване със сървъра, използван за получаване на съобщения от този контакт."; /* No comment provided by engineer. */ -"Turn off" = "Изключи"; +"Turkish interface" = "Турски интерфейс"; /* No comment provided by engineer. */ -"Turn off notifications?" = "Изключи известията?"; +"Turn off" = "Изключи"; /* No comment provided by engineer. */ "Turn on" = "Включи"; @@ -3227,13 +3893,28 @@ /* No comment provided by engineer. */ "Unable to record voice message" = "Не може да се запише гласово съобщение"; -/* item status description */ -"Unexpected error: %@" = "Неочаквана грешка: %@"; +/* No comment provided by engineer. */ +"Unblock" = "Отблокирай"; + +/* No comment provided by engineer. */ +"Unblock for all" = "Отблокирай за всички"; + +/* No comment provided by engineer. */ +"Unblock member" = "Отблокирай член"; + +/* No comment provided by engineer. */ +"Unblock member for all?" = "Отблокиране на член за всички?"; + +/* No comment provided by engineer. */ +"Unblock member?" = "Отблокирай член?"; + +/* rcv group event chat item */ +"unblocked %@" = "отблокиран %@"; /* No comment provided by engineer. */ "Unexpected migration state" = "Неочаквано състояние на миграция"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Премахни от любимите"; /* No comment provided by engineer. */ @@ -3260,29 +3941,38 @@ /* No comment provided by engineer. */ "Unknown error" = "Непозната грешка"; +/* No comment provided by engineer. */ +"unknown status" = "неизвестен статус"; + /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Освен ако не използвате интерфейса за повикване на iOS, активирайте режима \"Не безпокой\", за да избегнете прекъсвания."; /* No comment provided by engineer. */ "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Освен ако вашият контакт не е изтрил връзката или този линк вече е бил използван, това може да е грешка - моля, докладвайте.\nЗа да се свържете, моля, помолете вашия контакт да създаде друг линк за връзка и проверете дали имате стабилна мрежова връзка."; +/* No comment provided by engineer. */ +"Unlink" = "Забрави"; + +/* No comment provided by engineer. */ +"Unlink desktop?" = "Забрави настолно устройство?"; + /* No comment provided by engineer. */ "Unlock" = "Отключи"; /* authentication reason */ "Unlock app" = "Отключи приложението"; -/* No comment provided by engineer. */ +/* notification label action */ "Unmute" = "Уведомявай"; -/* No comment provided by engineer. */ +/* swipe action */ "Unread" = "Непрочетено"; /* No comment provided by engineer. */ -"Update" = "Актуализация"; +"Up to 100 last messages are sent to new members." = "На новите членове се изпращат до последните 100 съобщения."; /* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Актуализиране на настройката за .onion хостове?"; +"Update" = "Актуализация"; /* No comment provided by engineer. */ "Update database passphrase" = "Актуализирай паролата на базата данни"; @@ -3290,24 +3980,27 @@ /* No comment provided by engineer. */ "Update network settings?" = "Актуализиране на мрежовите настройки?"; -/* No comment provided by engineer. */ -"Update transport isolation mode?" = "Актуализиране на режима на изолация на транспорта?"; - /* rcv group event chat item */ "updated group profile" = "актуализиран профил на групата"; +/* profile update event chat item */ +"updated profile" = "актуализиран профил"; + /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "Актуализирането на настройките ще свърже отново клиента към всички сървъри."; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Актуализирането на тази настройка ще свърже повторно клиента към всички сървъри."; +"Upgrade and open chat" = "Актуализирай и отвори чата"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "Актуализирай и отвори чата"; +"Upload failed" = "Неуспешно качване"; /* server test step */ "Upload file" = "Качи файл"; +/* No comment provided by engineer. */ +"Uploading archive" = "Архивът се качва"; + /* No comment provided by engineer. */ "Use .onion hosts" = "Използвай .onion хостове"; @@ -3320,12 +4013,18 @@ /* No comment provided by engineer. */ "Use for new connections" = "Използвай за нови връзки"; +/* No comment provided by engineer. */ +"Use from desktop" = "Използвай от настолно устройство"; + /* No comment provided by engineer. */ "Use iOS call interface" = "Използвай интерфейса за повикване на iOS"; /* No comment provided by engineer. */ "Use new incognito profile" = "Използвай нов инкогнито профил"; +/* No comment provided by engineer. */ +"Use only local notifications?" = "Използвай само локални известия?"; + /* No comment provided by engineer. */ "Use server" = "Използвай сървър"; @@ -3333,22 +4032,37 @@ "Use SimpleX Chat servers?" = "Използвай сървърите на SimpleX Chat?"; /* No comment provided by engineer. */ -"User profile" = "Потребителски профил"; - -/* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Използването на .onion хостове изисква съвместим VPN доставчик."; +"Use the app while in the call." = "Използвайте приложението по време на разговора."; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Използват се сървърите на SimpleX Chat."; +/* No comment provided by engineer. */ +"v%@" = "v%@"; + /* No comment provided by engineer. */ "v%@ (%@)" = "v%@ (%@)"; /* No comment provided by engineer. */ -"Verify connection security" = "Потвръди сигурността на връзката"; +"Verify code with desktop" = "Потвърди кода с настолното устройство"; /* No comment provided by engineer. */ -"Verify security code" = "Потвръди кода за сигурност"; +"Verify connection" = "Потвърди връзка"; + +/* No comment provided by engineer. */ +"Verify connection security" = "Потвърди сигурността на връзката"; + +/* No comment provided by engineer. */ +"Verify connections" = "Потвърждение за свързване"; + +/* No comment provided by engineer. */ +"Verify database passphrase" = "Проверете паролата на базата данни"; + +/* No comment provided by engineer. */ +"Verify passphrase" = "Провери паролата"; + +/* No comment provided by engineer. */ +"Verify security code" = "Потвърди кода за сигурност"; /* No comment provided by engineer. */ "Via browser" = "Чрез браузър"; @@ -3365,6 +4079,9 @@ /* No comment provided by engineer. */ "via relay" = "чрез реле"; +/* No comment provided by engineer. */ +"Via secure quantum resistant protocol." = "Чрез сигурен квантово устойчив протокол."; + /* No comment provided by engineer. */ "Video call" = "Видео разговор"; @@ -3383,6 +4100,9 @@ /* No comment provided by engineer. */ "View security code" = "Виж кода за сигурност"; +/* chat feature */ +"Visible history" = "Видима история"; + /* No comment provided by engineer. */ "Voice message…" = "Гласово съобщение…"; @@ -3393,7 +4113,10 @@ "Voice messages are prohibited in this chat." = "Гласовите съобщения са забранени в този чат."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Гласовите съобщения са забранени в тази група."; +"Voice messages are prohibited." = "Гласовите съобщения са забранени в тази група."; + +/* No comment provided by engineer. */ +"Voice messages not allowed" = "Гласовите съобщения не са разрешени"; /* No comment provided by engineer. */ "Voice messages prohibited!" = "Гласовите съобщения са забранени!"; @@ -3404,6 +4127,9 @@ /* No comment provided by engineer. */ "waiting for confirmation…" = "чака се за потвърждение…"; +/* No comment provided by engineer. */ +"Waiting for desktop..." = "Изчакване на настолно устройство…"; + /* No comment provided by engineer. */ "Waiting for file" = "Изчаква се получаването на файла"; @@ -3416,6 +4142,9 @@ /* No comment provided by engineer. */ "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" = "Внимание: стартирането на чата на множество устройства не се поддържа и ще доведе до неуспешно изпращане на съобщения"; + /* No comment provided by engineer. */ "Warning: you may lose some data!" = "Предупреждение: Може да загубите някои данни!"; @@ -3431,6 +4160,9 @@ /* No comment provided by engineer. */ "Welcome message" = "Съобщение при посрещане"; +/* No comment provided by engineer. */ +"Welcome message is too long" = "Съобщението при посрещане е твърде дълго"; + /* No comment provided by engineer. */ "What's new" = "Какво е новото"; @@ -3438,28 +4170,43 @@ "When available" = "Когато са налични"; /* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Когато хората искат да се свържат с вас, можете да ги приемете или отхвърлите."; +"When connecting audio and video calls." = "При свързване на аудио и видео разговори."; /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Когато споделяте инкогнито профил с някого, този профил ще се използва за групите, в които той ви кани."; +/* No comment provided by engineer. */ +"WiFi" = "WiFi"; + +/* No comment provided by engineer. */ +"Will be enabled in direct chats!" = "Ще бъде активирано в личните чатове!"; + +/* No comment provided by engineer. */ +"Wired ethernet" = "Кабелен Ethernet"; + +/* No comment provided by engineer. */ +"With encrypted files and media." = "С криптирани файлове и медия."; + /* No comment provided by engineer. */ "With optional welcome message." = "С незадължително съобщение при посрещане."; +/* No comment provided by engineer. */ +"With reduced battery usage." = "С намален разход на батерията."; + /* No comment provided by engineer. */ "Wrong database passphrase" = "Грешна парола за базата данни"; /* No comment provided by engineer. */ "Wrong passphrase!" = "Грешна парола!"; -/* No comment provided by engineer. */ -"XFTP servers" = "XFTP сървъри"; - /* pref value */ "yes" = "да"; /* No comment provided by engineer. */ -"You" = "Вие"; +"you" = "вие"; + +/* No comment provided by engineer. */ +"You **must not** use the same database on two devices." = "**Не трябва** да използвате една и съща база данни на две устройства."; /* No comment provided by engineer. */ "You accepted connection" = "Вие приехте връзката"; @@ -3468,11 +4215,32 @@ "You allow" = "Вие позволявате"; /* No comment provided by engineer. */ -"You already have a chat profile with the same display name. Please choose another name." = "Вече имате чат профил със същото показвано име. Моля, изберете друго име."; +"You already have a chat profile with the same display name. Please choose another name." = "Вече имате чат профил със същото име. Моля, изберете друго име."; /* No comment provided by engineer. */ "You are already connected to %@." = "Вече сте вече свързани с %@."; +/* No comment provided by engineer. */ +"You are already connecting to %@." = "Вече се свързвате с %@."; + +/* No comment provided by engineer. */ +"You are already connecting via this one-time link!" = "Вече се свързвате чрез този еднократен линк за връзка!"; + +/* No comment provided by engineer. */ +"You are already in group %@." = "Вече сте в група %@."; + +/* No comment provided by engineer. */ +"You are already joining the group %@." = "Вече се присъединявате към групата %@."; + +/* No comment provided by engineer. */ +"You are already joining the group via this link!" = "Вие вече се присъединявате към групата чрез този линк!"; + +/* No comment provided by engineer. */ +"You are already joining the group via this link." = "Вие вече се присъединявате към групата чрез този линк."; + +/* No comment provided by engineer. */ +"You are already joining the group!\nRepeat join request?" = "Вече се присъединихте към групата!\nИзпрати отново заявката за присъединяване?"; + /* No comment provided by engineer. */ "You are connected to the server used to receive messages from this contact." = "Вие сте свързани към сървъра, използван за получаване на съобщения от този контакт."; @@ -3485,11 +4253,11 @@ /* No comment provided by engineer. */ "you are observer" = "вие сте наблюдател"; -/* No comment provided by engineer. */ -"You can accept calls from lock screen, without device and app authentication." = "Можете да приемате обаждания от заключен екран, без идентификация на устройство и приложението."; +/* snd group event chat item */ +"you blocked %@" = "вие блокирахте %@"; /* No comment provided by engineer. */ -"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "Можете също да се свържете, като натиснете върху линка. Ако се отвори в браузъра, натиснете върху бутона **Отваряне в мобилно приложение**."; +"You can accept calls from lock screen, without device and app authentication." = "Можете да приемате обаждания от заключен екран, без идентификация на устройство и приложението."; /* No comment provided by engineer. */ "You can create it later" = "Можете да го създадете по-късно"; @@ -3500,11 +4268,17 @@ /* No comment provided by engineer. */ "You can enable them later via app Privacy & Security settings." = "Можете да ги активирате по-късно през настройките за \"Поверителност и сигурност\" на приложението."; +/* No comment provided by engineer. */ +"You can give another try." = "Можете да опитате още веднъж."; + /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "Можете да скриете или заглушите известията за потребителски профил - плъзнете надясно."; +/* No comment provided by engineer. */ +"You can make it visible to your SimpleX contacts via Settings." = "Можете да го направите видим за вашите контакти в SimpleX чрез Настройки."; + /* notification body */ -"You can now send messages to %@" = "Вече можете да изпращате съобщения до %@"; +"You can now chat with %@" = "Вече можете да изпращате съобщения до %@"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Можете да зададете визуализация на известията на заключен екран през настройките."; @@ -3515,9 +4289,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Можете да споделите този адрес с вашите контакти, за да им позволите да се свържат с **%@**."; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Можете да споделите адреса си като линк или QR код - всеки може да се свърже с вас."; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Можете да започнете чат през Настройки на приложението / База данни или като рестартирате приложението"; @@ -3527,14 +4298,17 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Можете да използвате markdown за форматиране на съобщенията:"; +/* alert message */ +"You can view invitation link again in connection details." = "Можете да видите отново линкът за покана в подробностите за връзката."; + /* No comment provided by engineer. */ "You can't send messages!" = "Не може да изпращате съобщения!"; /* chat item text */ -"you changed address" = "променихте адреса"; +"you changed address" = "адреса за получаване е променен"; /* chat item text */ -"you changed address for %@" = "променихте адреса за %@"; +"you changed address for %@" = "променихте адреса получаване за %@"; /* snd group event chat item */ "you changed role for yourself to %@" = "променихте ролята си на %@"; @@ -3542,14 +4316,17 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "променихте ролята на %1$@ на %2$@"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Вие контролирате през кой сървър(и) **да получавате** съобщенията, вашите контакти – сървърите, които използвате, за да им изпращате съобщения."; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "Не можахте да бъдете потвърдени; Моля, опитайте отново."; /* No comment provided by engineer. */ -"You have no chats" = "Нямате чатове"; +"You decide who can connect." = "Хората могат да се свържат с вас само чрез ликовете, които споделяте."; + +/* No comment provided by engineer. */ +"You have already requested connection via this address!" = "Вече сте заявили връзка през този адрес!"; + +/* No comment provided by engineer. */ +"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." = "Трябва да въвеждате парола при всяко стартиране на приложението - тя не се съхранява на устройството."; @@ -3587,9 +4364,15 @@ /* chat list item description */ "you shared one-time link incognito" = "споделихте еднократен инкогнито линк за връзка"; +/* snd group event chat item */ +"you unblocked %@" = "вие отблокирахте %@"; + /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "Ще бъдете свързани с групата, когато устройството на домакина на групата е онлайн, моля, изчакайте или проверете по-късно!"; +/* No comment provided by engineer. */ +"You will be connected when group link host's device is online, please wait or check later!" = "Ще бъдете свързани, когато устройството на хоста на груповата връзка е онлайн, моля, изчакайте или проверете по-късно!"; + /* No comment provided by engineer. */ "You will be connected when your connection request is accepted, please wait or check later!" = "Ще бъдете свързани, когато заявката ви за връзка бъде приета, моля, изчакайте или проверете по-късно!"; @@ -3599,6 +4382,9 @@ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Ще трябва да се идентифицирате, когато стартирате или възобновите приложението след 30 секунди във фонов режим."; +/* No comment provided by engineer. */ +"You will connect to all group members." = "Ще се свържете с всички членове на групата."; + /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Все още ще получавате обаждания и известия от заглушени профили, когато са активни."; @@ -3617,38 +4403,29 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Използвате инкогнито профил за тази група - за да се предотврати споделянето на основния ви профил, поканите на контакти не са разрешени"; -/* No comment provided by engineer. */ -"Your %@ servers" = "Вашите %@ сървъри"; - /* No comment provided by engineer. */ "Your calls" = "Вашите обаждания"; /* No comment provided by engineer. */ -"Your chat database" = "Вашата чат база данни"; +"Your chat database" = "Вашата база данни"; /* No comment provided by engineer. */ -"Your chat database is not encrypted - set passphrase to encrypt it." = "Вашата чат база данни не е криптирана - задайте парола, за да я криптирате."; +"Your chat database is not encrypted - set passphrase to encrypt it." = "Вашата база данни не е криптирана - задайте парола, за да я криптирате."; /* No comment provided by engineer. */ "Your chat profiles" = "Вашите чат профили"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Вашият контакт трябва да бъде онлайн, за да осъществите връзката.\nМожете да откажете тази връзка и да премахнете контакта (и да опитате по -късно с нов линк)."; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Вашият контакт изпрати файл, който е по-голям от поддържания в момента максимален размер (%@)."; /* No comment provided by engineer. */ "Your contacts can allow full message deletion." = "Вашите контакти могат да позволят пълното изтриване на съобщението."; -/* No comment provided by engineer. */ -"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "Вашите контакти в SimpleX ще го видят.\nМожете да го промените в Настройки."; - /* No comment provided by engineer. */ "Your contacts will remain connected." = "Вашите контакти ще останат свързани."; /* No comment provided by engineer. */ -"Your current chat database will be DELETED and REPLACED with the imported one." = "Вашата текуща чат база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната."; +"Your current chat database will be DELETED and REPLACED with the imported one." = "Вашата текуща база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната."; /* No comment provided by engineer. */ "Your current profile" = "Вашият текущ профил"; @@ -3662,11 +4439,14 @@ /* No comment provided by engineer. */ "Your privacy" = "Вашата поверителност"; +/* No comment provided by engineer. */ +"Your profile" = "Вашият профил"; + /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Вашият профил **%@** ще бъде споделен."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти.\nSimpleX сървърите не могат да видят вашия профил."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти. SimpleX сървърите не могат да видят вашия профил."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство."; @@ -3674,9 +4454,6 @@ /* No comment provided by engineer. */ "Your random profile" = "Вашият автоматично генериран профил"; -/* No comment provided by engineer. */ -"Your server" = "Вашият сървър"; - /* No comment provided by engineer. */ "Your server address" = "Вашият адрес на сървъра"; @@ -3684,11 +4461,5 @@ "Your settings" = "Вашите настройки"; /* No comment provided by engineer. */ -"Your SimpleX address" = "Вашият SimpleX адрес"; - -/* No comment provided by engineer. */ -"Your SMP servers" = "Вашите SMP сървъри"; - -/* No comment provided by engineer. */ -"Your XFTP servers" = "Вашите XFTP сървъри"; +"Your SimpleX address" = "Вашият адрес в SimpleX"; diff --git a/apps/ios/bg.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/bg.lproj/SimpleX--iOS--InfoPlist.strings index d85455d875..8b1f135a62 100644 --- a/apps/ios/bg.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/bg.lproj/SimpleX--iOS--InfoPlist.strings @@ -7,6 +7,9 @@ /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX използва Face ID за локалнa идентификация"; +/* Privacy - Local Network Usage Description */ +"NSLocalNetworkUsageDescription" = "SimpleX използва достъп до локална мрежа, за да позволи използването на потребителския чат профил чрез настолно приложение в същата мрежа."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX се нуждае от достъп до микрофона за аудио и видео разговори и за запис на гласови съобщения."; diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index 26469bea98..08a94615a3 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -1,18 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (lze kopírovat)"; @@ -20,7 +5,7 @@ "_italic_" = "\\_kurzíva_"; /* 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." = "- připojit k [adresářová služba](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.cibule) (BETA)!\n- doručenky (až 20 členů).\n- Rychlejší a stabilnější."; +"- 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." = "- připojit k [adresářová služba](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- doručenky (až 20 členů).\n- Rychlejší a stabilnější."; /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- více stabilní doručování zpráv.\n- o trochu lepší skupiny.\n- a více!"; @@ -28,23 +13,11 @@ /* No comment provided by engineer. */ "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- 5 minutové hlasové zprávy.\n- vlastní čas mizení.\n- historie úprav."; -/* No comment provided by engineer. */ -", " = ", "; - -/* No comment provided by engineer. */ -": " = ": "; - /* No comment provided by engineer. */ "!1 colored!" = "!1 barevný!"; /* No comment provided by engineer. */ -"." = "."; - -/* No comment provided by engineer. */ -"(" = "("; - -/* 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)"; @@ -55,12 +28,6 @@ /* 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. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Přidat nový kontakt**: pro vytvoření jednorázového QR kódu nebo odkazu pro váš kontakt."; - -/* No comment provided by engineer. */ -"**Create link / QR code** for your contact to use." = "**Vytvořte odkaz / QR kód** pro váš kontakt."; - /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "**e2e šifrovaný** audio hovor"; @@ -68,22 +35,16 @@ "**e2e encrypted** video call" = "**e2e šifrovaný** videohovor"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Soukromější**: kontrolovat nové zprávy každých 20 minut. Token zařízení je sdílen se serverem SimpleX Chat, ale ne kolik máte kontaktů nebo zpráv."; +"**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." = "**Soukromější**: kontrolovat nové zprávy každých 20 minut. Token zařízení je sdílen se serverem SimpleX Chat, ale ne kolik máte kontaktů nebo zpráv."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Nejsoukromější**: nepoužívejte server oznámení SimpleX Chat, pravidelně kontrolujte zprávy na pozadí (závisí na tom, jak často aplikaci používáte)."; - -/* No comment provided by engineer. */ -"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Vložte přijatý odkaz** nebo jej otevřete v prohlížeči a klepněte na **Otevřít v mobilní aplikaci**."; +"**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." = "**Nejsoukromější**: nepoužívejte server oznámení SimpleX Chat, pravidelně kontrolujte zprávy na pozadí (závisí na tom, jak často aplikaci používáte)."; /* No comment provided by engineer. */ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Upozornění**: Pokud heslo ztratíte, NEBUDETE jej moci obnovit ani změnit."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Doporučeno**: Token zařízení a oznámení se odesílají na oznamovací server SimpleX Chat, ale nikoli obsah, velikost nebo od koho jsou zprávy."; - -/* No comment provided by engineer. */ -"**Scan QR code**: to connect to your contact in person or via video call." = "** Naskenujte QR kód**: pro připojení ke kontaktu osobně nebo prostřednictvím videohovoru."; +"**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." = "**Doporučeno**: Token zařízení a oznámení se odesílají na oznamovací server SimpleX Chat, ale nikoli obsah, velikost nebo od koho jsou zprávy."; /* 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."; @@ -118,12 +79,21 @@ /* No comment provided by engineer. */ "%@ %@" = "%@ %@"; +/* No comment provided by engineer. */ +"%@ and %@" = "%@ a %@"; + /* No comment provided by engineer. */ "%@ and %@ connected" = "%@ a %@ připojen"; /* copied message info, <sender> at <time> */ "%@ at %@:" = "%1$@ na %2$@:"; +/* No comment provided by engineer. */ +"%@ connected" = "%@ připojen"; + +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ staženo"; + /* notification title */ "%@ is connected!" = "%@ je připojen!"; @@ -133,12 +103,18 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ je ověřený"; +/* No comment provided by engineer. */ +"%@ server" = "%@ server"; + /* No comment provided by engineer. */ "%@ servers" = "%@ servery"; /* notification title */ "%@ wants to connect!" = "%@ se chce připojit!"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + /* No comment provided by engineer. */ "%@, %@ and %lld other members connected" = "%@, %@ a %lld ostatní členové připojeni"; @@ -148,6 +124,18 @@ /* time interval */ "%d days" = "%d dní"; +/* forward confirmation reason */ +"%d file(s) are still being downloaded." = "%d soubor(y) stále stahován(y)."; + +/* forward confirmation reason */ +"%d file(s) failed to download." = "%d soubor(y) se nepodařilo stáhnout."; + +/* forward confirmation reason */ +"%d file(s) were deleted." = "%d soubor(y) smazán(y)."; + +/* forward confirmation reason */ +"%d file(s) were not downloaded." = "%d soubor(y) nestažen(y)."; + /* time interval */ "%d hours" = "%d hodin"; @@ -181,15 +169,21 @@ /* No comment provided by engineer. */ "%lld members" = "%lld členové"; +/* No comment provided by engineer. */ +"%lld messages blocked" = "%lld zprávy blokovaný"; + +/* No comment provided by engineer. */ +"%lld messages blocked by admin" = "%lld zprávy blokovaný adminem"; + +/* No comment provided by engineer. */ +"%lld messages marked deleted" = "%lld zprávy označeno jako smazáno"; + /* No comment provided by engineer. */ "%lld minutes" = "%lld minut"; /* No comment provided by engineer. */ "%lld new interface languages" = "%d nové jazyky rozhraní"; -/* No comment provided by engineer. */ -"%lld second(s)" = "%lld vteřin"; - /* No comment provided by engineer. */ "%lld seconds" = "%lld vteřin"; @@ -232,7 +226,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 den"; /* time interval */ @@ -241,15 +236,14 @@ /* No comment provided by engineer. */ "1 minute" = "1 minutu"; -/* time interval */ +/* delete after time +time interval */ "1 month" = "1 měsíc"; -/* time interval */ +/* delete after time +time interval */ "1 week" = "1 týden"; -/* No comment provided by engineer. */ -"1-time link" = "Jednorázový odkaz"; - /* No comment provided by engineer. */ "5 minutes" = "5 minut"; @@ -283,23 +277,15 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Přerušit změnu adresy?"; -/* No comment provided by engineer. */ -"About SimpleX" = "O SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "O SimpleX adrese"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "O SimpleX chat"; /* No comment provided by engineer. */ "above, then choose:" = "výše, pak vyberte:"; -/* No comment provided by engineer. */ -"Accent color" = "Zbarvení"; - /* accept contact request via notification - accept incoming call via notification */ +accept incoming call via notification +swipe action */ "Accept" = "Přijmout"; /* No comment provided by engineer. */ @@ -308,7 +294,8 @@ /* notification body */ "Accept contact request from %@?" = "Přijmout žádost o kontakt od %@?"; -/* accept contact request via notification */ +/* accept contact request via notification +swipe action */ "Accept incognito" = "Přijmout inkognito"; /* call status */ @@ -317,14 +304,11 @@ /* 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 preset servers" = "Přidejte přednastavené servery"; - /* No comment provided by engineer. */ "Add profile" = "Přidat profil"; /* No comment provided by engineer. */ -"Add server…" = "Přidat server…"; +"Add server" = "Přidat server"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Přidejte servery skenováním QR kódů."; @@ -387,7 +371,7 @@ "Allow disappearing messages only if your contact allows it to you." = "Povolte mizící zprávy, pouze pokud vám to váš kontakt dovolí."; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you." = "Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí."; +"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí. (24 hodin)"; /* No comment provided by engineer. */ "Allow message reactions only if your contact allows them." = "Povolit reakce na zprávy, pokud je váš kontakt povolí."; @@ -402,7 +386,7 @@ "Allow sending disappearing messages." = "Povolit odesílání mizících zpráv."; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages." = "Povolit nevratné smazání odeslaných zpráv."; +"Allow to irreversibly delete sent messages. (24 hours)" = "Povolit nevratné smazání odeslaných zpráv. (24 hodin)"; /* No comment provided by engineer. */ "Allow to send files and media." = "Povolit odesílání souborů a médii."; @@ -423,7 +407,7 @@ "Allow your contacts to call you." = "Povolte svým kontaktům vám volat."; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages." = "Umožněte svým kontaktům nevratně odstranit odeslané zprávy."; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Umožněte svým kontaktům nevratně odstranit odeslané zprávy. (24 hodin)"; /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Umožněte svým kontaktům odesílat mizící zprávy."; @@ -446,6 +430,9 @@ /* 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: %@"; @@ -534,7 +521,7 @@ "Both you and your contact can add message reactions." = "Vy i váš kontakt můžete přidávat reakce na zprávy."; /* No comment provided by engineer. */ -"Both you and your contact can irreversibly delete sent messages." = "Vy i váš kontakt můžete nevratně mazat odeslané zprávy."; +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "Vy i váš kontakt můžete nevratně mazat odeslané zprávy. (24 hodin)"; /* No comment provided by engineer. */ "Both you and your contact can make calls." = "Volat můžete vy i váš kontakt."; @@ -566,16 +553,14 @@ /* No comment provided by engineer. */ "Calls" = "Hovory"; -/* No comment provided by engineer. */ -"Can't delete user profile!" = "Nemohu smazat uživatelský profil!"; - /* No comment provided by engineer. */ "Can't invite contact!" = "Nelze pozvat kontakt!"; /* No comment provided by engineer. */ "Can't invite contacts!" = "Nelze pozvat kontakty!"; -/* No comment provided by engineer. */ +/* alert action +alert button */ "Cancel" = "Zrušit"; /* feature offered item */ @@ -584,7 +569,7 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Nelze získat přístup ke klíčence pro uložení hesla databáze"; -/* No comment provided by engineer. */ +/* alert title */ "Cannot receive file" = "Nelze přijmout soubor"; /* No comment provided by engineer. */ @@ -615,7 +600,7 @@ "Change self-destruct mode" = "Změnit režim sebedestrukce"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Změnit sebedestrukční heslo"; /* chat item text */ @@ -633,9 +618,6 @@ /* chat item text */ "changing address…" = "změna adresy…"; -/* No comment provided by engineer. */ -"Chat archive" = "Chat se archivuje"; - /* No comment provided by engineer. */ "Chat console" = "Konzola pro chat"; @@ -658,9 +640,12 @@ "Chat preferences" = "Předvolby chatu"; /* No comment provided by engineer. */ -"Chats" = "Chaty"; +"Chat profile" = "Profil uživatele"; /* No comment provided by engineer. */ +"Chats" = "Chaty"; + +/* alert title */ "Check server address and try again." = "Zkontrolujte adresu serveru a zkuste to znovu."; /* No comment provided by engineer. */ @@ -672,7 +657,7 @@ /* No comment provided by engineer. */ "Choose from library" = "Vybrat z knihovny"; -/* No comment provided by engineer. */ +/* swipe action */ "Clear" = "Vyčistit"; /* No comment provided by engineer. */ @@ -687,9 +672,6 @@ /* No comment provided by engineer. */ "colored" = "barevné"; -/* No comment provided by engineer. */ -"Colors" = "Barvy"; - /* server test step */ "Compare file" = "Porovnat soubor"; @@ -729,9 +711,6 @@ /* No comment provided by engineer. */ "Connect via link" = "Připojte se prostřednictvím odkazu"; -/* No comment provided by engineer. */ -"Connect via link / QR code" = "Připojit se prostřednictvím odkazu / QR kódu"; - /* No comment provided by engineer. */ "Connect via one-time link" = "Připojit se jednorázovým odkazem"; @@ -765,7 +744,7 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Připojování k serveru... (chyba: %@)"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "připojení…"; /* No comment provided by engineer. */ @@ -807,9 +786,6 @@ /* notification */ "Contact is connected" = "Kontakt je připojen"; -/* No comment provided by engineer. */ -"Contact is not connected yet!" = "Kontakt ještě není připojen!"; - /* No comment provided by engineer. */ "Contact name" = "Jméno kontaktu"; @@ -825,7 +801,7 @@ /* No comment provided by engineer. */ "Continue" = "Pokračovat"; -/* chat item action */ +/* No comment provided by engineer. */ "Copy" = "Kopírovat"; /* No comment provided by engineer. */ @@ -834,9 +810,6 @@ /* No comment provided by engineer. */ "Create" = "Vytvořit"; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Vytvořit adresu, aby se s vámi lidé mohli spojit."; - /* server test step */ "Create file" = "Vytvořit soubor"; @@ -850,7 +823,7 @@ "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Vytvořit nový profil v [desktop app](https://simplex.chat/downloads/). 💻"; /* No comment provided by engineer. */ -"Create one-time invitation link" = "Vytvořit jednorázovou pozvánku"; +"Create profile" = "Vytvořte si profil"; /* server test step */ "Create queue" = "Vytvořit frontu"; @@ -864,9 +837,6 @@ /* No comment provided by engineer. */ "Create your profile" = "Vytvořte si profil"; -/* No comment provided by engineer. */ -"Created on %@" = "Vytvořeno na %@"; - /* No comment provided by engineer. */ "creator" = "tvůrce"; @@ -954,7 +924,8 @@ /* message decrypt error item */ "Decryption error" = "Chyba dešifrování"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "výchozí (%@)"; /* No comment provided by engineer. */ @@ -963,7 +934,8 @@ /* No comment provided by engineer. */ "default (yes)" = "výchozí (ano)"; -/* chat item action */ +/* alert action +swipe action */ "Delete" = "Smazat"; /* No comment provided by engineer. */ @@ -978,12 +950,6 @@ /* No comment provided by engineer. */ "Delete all files" = "Odstranit všechny soubory"; -/* No comment provided by engineer. */ -"Delete archive" = "Smazat archiv"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Smazat archiv chatu?"; - /* No comment provided by engineer. */ "Delete chat profile" = "Smazat chat profil"; @@ -996,9 +962,6 @@ /* No comment provided by engineer. */ "Delete contact" = "Smazat kontakt"; -/* No comment provided by engineer. */ -"Delete Contact" = "Smazat kontakt"; - /* No comment provided by engineer. */ "Delete database" = "Odstranění databáze"; @@ -1038,7 +1001,7 @@ /* No comment provided by engineer. */ "Delete message?" = "Smazat zprávu?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "Smazat zprávy"; /* No comment provided by engineer. */ @@ -1050,9 +1013,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "Smazat starou databázi?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "Smazat čekající připojení"; - /* No comment provided by engineer. */ "Delete pending connection?" = "Smazat čekající připojení?"; @@ -1117,7 +1077,7 @@ "Direct messages" = "Přímé zprávy"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Přímé zprávy mezi členy jsou v této skupině zakázány."; +"Direct messages between members are prohibited." = "Přímé zprávy mezi členy jsou v této skupině zakázány."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Vypnout (zachovat přepsání)"; @@ -1141,7 +1101,7 @@ "Disappearing messages are prohibited in this chat." = "Mizící zprávy jsou v tomto chatu zakázány."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Mizící zprávy jsou v této skupině zakázány."; +"Disappearing messages are prohibited." = "Mizící zprávy jsou v této skupině zakázány."; /* No comment provided by engineer. */ "Disappears at" = "Zmizí v"; @@ -1200,7 +1160,7 @@ /* No comment provided by engineer. */ "Enable (keep overrides)" = "Povolit (zachovat přepsání)"; -/* No comment provided by engineer. */ +/* alert title */ "Enable automatic message deletion?" = "Povolit automatické mazání zpráv?"; /* No comment provided by engineer. */ @@ -1335,9 +1295,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Chyba při přijímání žádosti o kontakt"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Chyba přístupu k souboru databáze"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Chyba přidávání člena(ů)"; @@ -1377,9 +1334,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Chyba při mazání připojení"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Chyba mazání kontaktu"; - /* No comment provided by engineer. */ "Error deleting database" = "Chyba při mazání databáze"; @@ -1410,18 +1364,12 @@ /* No comment provided by engineer. */ "Error joining group" = "Chyba při připojování ke skupině"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Chyba načítání %@ serverů"; - -/* No comment provided by engineer. */ +/* alert title */ "Error receiving file" = "Chyba při příjmu souboru"; /* No comment provided by engineer. */ "Error removing member" = "Chyba při odebrání člena"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "Chyba při ukládání serverů %@"; - /* No comment provided by engineer. */ "Error saving group profile" = "Chyba při ukládání profilu skupiny"; @@ -1455,7 +1403,7 @@ /* No comment provided by engineer. */ "Error stopping chat" = "Chyba při zastavení chatu"; -/* No comment provided by engineer. */ +/* alertTitle */ "Error switching profile!" = "Chyba při přepínání profilu!"; /* No comment provided by engineer. */ @@ -1476,7 +1424,9 @@ /* No comment provided by engineer. */ "Error: " = "Chyba: "; -/* No comment provided by engineer. */ +/* alert message +file error text +snd error text */ "Error: %@" = "Chyba: %@"; /* No comment provided by engineer. */ @@ -1509,7 +1459,7 @@ /* No comment provided by engineer. */ "Fast and no wait until the sender is online!" = "Rychle a bez čekání, než bude odesílatel online!"; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "Oblíbené"; /* No comment provided by engineer. */ @@ -1531,7 +1481,7 @@ "Files and media" = "Soubory a média"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Soubory a média jsou zakázány v této skupině."; +"Files and media are prohibited." = "Soubory a média jsou zakázány v této skupině."; /* No comment provided by engineer. */ "Files and media prohibited!" = "Soubory a média jsou zakázány!"; @@ -1575,9 +1525,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Celé jméno (volitelně)"; -/* No comment provided by engineer. */ -"Full name:" = "Celé jméno:"; - /* No comment provided by engineer. */ "Fully re-implemented - work in background!" = "Plně přepracováno, prácuje na pozadí!"; @@ -1617,24 +1564,6 @@ /* No comment provided by engineer. */ "Group links" = "Odkazy na skupiny"; -/* No comment provided by engineer. */ -"Group members can add message reactions." = "Členové skupin mohou přidávat reakce na zprávy."; - -/* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages." = "Členové skupiny mohou nevratně mazat odeslané zprávy."; - -/* No comment provided by engineer. */ -"Group members can send direct messages." = "Členové skupiny mohou posílat přímé zprávy."; - -/* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Členové skupiny mohou posílat mizící zprávy."; - -/* No comment provided by engineer. */ -"Group members can send files and media." = "Členové skupiny mohou posílat soubory a média."; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "Členové skupiny mohou posílat hlasové zprávy."; - /* notification */ "Group message:" = "Skupinová zpráva:"; @@ -1692,9 +1621,6 @@ /* time unit */ "hours" = "hodin"; -/* No comment provided by engineer. */ -"How it works" = "Jak to funguje"; - /* No comment provided by engineer. */ "How SimpleX works" = "Jak SimpleX funguje"; @@ -1713,9 +1639,6 @@ /* No comment provided by engineer. */ "If you can't meet in person, show QR code in a video call, or share the link." = "Pokud se nemůžete setkat osobně, zobrazte QR kód ve videohovoru nebo sdílejte odkaz."; -/* No comment provided by engineer. */ -"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Pokud se nemůžete setkat osobně, můžete **naskenovat QR kód během videohovoru**, nebo váš kontakt může sdílet odkaz na pozvánku."; - /* No comment provided by engineer. */ "If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Pokud tento přístupový kód zadáte při otevření aplikace, všechna data budou nenávratně smazána!"; @@ -1738,7 +1661,7 @@ "Immediately" = "Ihned"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Odolná vůči spamu a zneužití"; +"Immune to spam" = "Odolná vůči spamu a zneužití"; /* No comment provided by engineer. */ "Import" = "Import"; @@ -1807,10 +1730,10 @@ "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)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "Okamžitá oznámení budou skryta!\n"; +"Instant" = "Okamžitě"; /* No comment provided by engineer. */ -"Instantly" = "Okamžitě"; +"Instant push notifications will be hidden!\n" = "Okamžitá oznámení budou skryta!\n"; /* No comment provided by engineer. */ "Interface" = "Rozhranní"; @@ -1827,7 +1750,7 @@ /* invalid chat item */ "invalid data" = "neplatné údaje"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Neplatná adresa serveru!"; /* item status text */ @@ -1873,7 +1796,7 @@ "Irreversible message deletion is prohibited in this chat." = "Nevratné mazání zpráv je v tomto chatu zakázáno."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "Nevratné mazání zpráv je v této skupině zakázáno."; +"Irreversible message deletion is prohibited." = "Nevratné mazání zpráv je v této skupině zakázáno."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Umožňuje mít v jednom profilu chatu mnoho anonymních spojení bez jakýchkoli sdílených údajů mezi nimi."; @@ -1896,7 +1819,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Japonské rozhraní"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Připojte se na"; /* No comment provided by engineer. */ @@ -1926,7 +1849,7 @@ /* No comment provided by engineer. */ "Learn more" = "Zjistit více"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Opustit"; /* No comment provided by engineer. */ @@ -1968,24 +1891,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Režim zámku"; -/* No comment provided by engineer. */ -"Make a private connection" = "Vytvořte si soukromé připojení"; - /* No comment provided by engineer. */ "Make one message disappear" = "Nechat jednu zprávu zmizet"; /* No comment provided by engineer. */ "Make profile private!" = "Změnit profil na soukromý!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Ujistěte se, že adresy %@ serverů jsou ve správném formátu, oddělené řádky a nejsou duplicitní (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Ujistěte se, že adresy serverů WebRTC ICE jsou ve správném formátu, oddělené na řádcích a nejsou duplicitní."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Mnoho lidí se ptalo: *Pokud SimpleX nemá žádné uživatelské identifikátory, jak může doručovat zprávy?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Označit jako smazané pro všechny"; @@ -2022,6 +1936,24 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Člen bude odstraněn ze skupiny - toto nelze vzít zpět!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Členové skupin mohou přidávat reakce na zprávy."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "Členové skupiny mohou nevratně mazat odeslané zprávy. (24 hodin)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Členové skupiny mohou posílat přímé zprávy."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Členové skupiny mohou posílat mizící zprávy."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Členové skupiny mohou posílat soubory a média."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Členové skupiny mohou posílat hlasové zprávy."; + /* item status text */ "Message delivery error" = "Chyba doručení zprávy"; @@ -2038,7 +1970,7 @@ "Message reactions are prohibited in this chat." = "Reakce na zprávy jsou v tomto chatu zakázány."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Reakce na zprávy jsou v této skupině zakázány."; +"Message reactions are prohibited." = "Reakce na zprávy jsou v této skupině zakázány."; /* notification */ "message received" = "zpráva přijata"; @@ -2065,7 +1997,7 @@ "Migration is completed" = "Přenesení dokončeno"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migrace: %@"; +"Migrations:" = "Migrace:"; /* time unit */ "minutes" = "minut"; @@ -2085,7 +2017,7 @@ /* copied message info */ "Moderated at: %@" = "Upraveno v: %@"; -/* No comment provided by engineer. */ +/* marked deleted chat item preview text */ "moderated by %@" = "moderovaný %@"; /* time unit */ @@ -2097,19 +2029,16 @@ /* item status description */ "Most likely this connection is deleted." = "Pravděpodobně je toto spojení smazáno."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Tento kontakt s největší pravděpodobností smazal spojení s vámi."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Více chatovacích profilů"; -/* No comment provided by engineer. */ +/* notification label action */ "Mute" = "Ztlumit"; /* No comment provided by engineer. */ "Muted when inactive!" = "Ztlumit při neaktivitě!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Jméno"; /* No comment provided by engineer. */ @@ -2121,7 +2050,7 @@ /* No comment provided by engineer. */ "Network status" = "Stav sítě"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "nikdy"; /* notification */ @@ -2130,9 +2059,6 @@ /* notification */ "New contact:" = "Nový kontakt:"; -/* No comment provided by engineer. */ -"New database archive" = "Archiv nové databáze"; - /* No comment provided by engineer. */ "New desktop app!" = "Nová desktopová aplikace!"; @@ -2193,12 +2119,18 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Nemáte oprávnění nahrávat hlasové zprávy"; +/* No comment provided by engineer. */ +"No push server" = "Místní"; + /* No comment provided by engineer. */ "No received or sent files" = "Žádné přijaté ani odeslané soubory"; /* copied message info in history */ "no text" = "žádný text"; +/* No comment provided by engineer. */ +"No user identifiers." = "Bez uživatelských identifikátorů"; + /* No comment provided by engineer. */ "Notifications" = "Oznámení"; @@ -2212,31 +2144,25 @@ "observer" = "pozorovatel"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "vypnuto"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Vypnout"; -/* No comment provided by engineer. */ -"Off (Local)" = "Vypnuto (místní)"; - /* feature offered item */ "offered %@" = "nabídl %@"; /* feature offered item */ "offered %@: %@" = "nabídl %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Ok"; /* No comment provided by engineer. */ "Old database" = "Stará databáze"; -/* No comment provided by engineer. */ -"Old database archive" = "Archiv staré databáze"; - /* group pref value */ "on" = "zapnuto"; @@ -2244,16 +2170,16 @@ "One-time invitation link" = "Jednorázový zvací odkaz"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Pro připojení budou vyžadováni Onion hostitelé. Vyžaduje povolení sítě VPN."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Pro připojení budou vyžadováni Onion hostitelé.\nVyžaduje povolení sítě VPN."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Onion hostitelé budou použiti, pokud jsou k dispozici. Vyžaduje povolení sítě VPN."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Onion hostitelé budou použiti, pokud jsou k dispozici.\nVyžaduje povolení sítě VPN."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion hostitelé nebudou použiti."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Pouze klientská zařízení ukládají uživatelské profily, kontakty, skupiny a zprávy odeslané s **2vrstvým šifrováním typu end-to-end**."; +"Only client devices store user profiles, contacts, groups, and messages." = "Pouze klientská zařízení ukládají uživatelské profily, kontakty, skupiny a zprávy odeslané s **2vrstvým šifrováním typu end-to-end**."; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Předvolby skupiny mohou měnit pouze vlastníci skupiny."; @@ -2268,7 +2194,7 @@ "Only you can add message reactions." = "Reakce na zprávy můžete přidávat pouze vy."; /* No comment provided by engineer. */ -"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "Nevratně mazat zprávy můžete pouze vy (váš kontakt je může označit ke smazání)."; +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Nevratně mazat zprávy můžete pouze vy (váš kontakt je může označit ke smazání). (24 hodin)"; /* No comment provided by engineer. */ "Only you can make calls." = "Volat můžete pouze vy."; @@ -2283,7 +2209,7 @@ "Only your contact can add message reactions." = "Reakce na zprávy může přidávat pouze váš kontakt."; /* No comment provided by engineer. */ -"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Nevratně mazat zprávy může pouze váš kontakt (vy je můžete označit ke smazání)."; +"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Nevratně mazat zprávy může pouze váš kontakt (vy je můžete označit ke smazání). (24 hodin)"; /* No comment provided by engineer. */ "Only your contact can make calls." = "Volat může pouze váš kontakt."; @@ -2294,7 +2220,7 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Hlasové zprávy může odesílat pouze váš kontakt."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Otevřít"; /* No comment provided by engineer. */ @@ -2306,15 +2232,6 @@ /* No comment provided by engineer. */ "Open Settings" = "Otevřít nastavení"; -/* authentication reason */ -"Open user profiles" = "Otevřít uživatelské profily"; - -/* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Protokol a kód s otevřeným zdrojovým kódem - servery může provozovat kdokoli."; - -/* No comment provided by engineer. */ -"Opening database…" = "Otvírání databáze…"; - /* member role */ "owner" = "vlastník"; @@ -2336,26 +2253,14 @@ /* No comment provided by engineer. */ "Password to show" = "Heslo k zobrazení"; -/* No comment provided by engineer. */ -"Paste" = "Vložit"; - /* No comment provided by engineer. */ "Paste image" = "Vložit obrázek"; -/* No comment provided by engineer. */ -"Paste received link" = "Vložení přijatého odkazu"; - -/* placeholder */ -"Paste the link you received to connect with your contact." = "Vložte odkaz, který jste obdrželi, do pole níže a spojte se se svým kontaktem."; - /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Lidé se s vámi mohou spojit pouze prostřednictvím odkazů, které sdílíte."; - -/* No comment provided by engineer. */ -"Periodically" = "Pravidelně"; +"Periodic" = "Pravidelně"; /* message decrypt error item */ "Permanent decryption error" = "Chyba dešifrování"; @@ -2411,9 +2316,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Zachování posledního návrhu zprávy s přílohami."; -/* No comment provided by engineer. */ -"Preset server" = "Přednastavený server"; - /* No comment provided by engineer. */ "Preset server address" = "Přednastavená adresa serveru"; @@ -2438,7 +2340,7 @@ /* No comment provided by engineer. */ "Profile password" = "Heslo profilu"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "Aktualizace profilu bude zaslána vašim kontaktům."; /* No comment provided by engineer. */ @@ -2486,14 +2388,14 @@ /* chat item menu */ "React…" = "Reagovat…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Číst"; /* No comment provided by engineer. */ "Read more" = "Přečíst více"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Další informace naleznete v [Uživatelské příručce](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"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)."; @@ -2501,9 +2403,6 @@ /* 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)."; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Další informace najdete v našem repozitáři GitHub."; - /* No comment provided by engineer. */ "Receipts are disabled" = "Informace o dodání jsou zakázány"; @@ -2552,7 +2451,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Snížení spotřeby baterie"; -/* reject incoming call via notification */ +/* reject incoming call via notification +swipe action */ "Reject" = "Odmítnout"; /* No comment provided by engineer. */ @@ -2636,9 +2536,6 @@ /* chat item action */ "Reveal" = "Odhalit"; -/* No comment provided by engineer. */ -"Revert" = "Vrátit"; - /* No comment provided by engineer. */ "Revoke" = "Odvolat"; @@ -2654,13 +2551,14 @@ /* No comment provided by engineer. */ "Run chat" = "Spustit chat"; -/* chat item action */ +/* alert button +chat item action */ "Save" = "Uložit"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Uložit (a informovat kontakty)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Uložit a upozornit kontakt"; /* No comment provided by engineer. */ @@ -2669,12 +2567,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "Uložit a aktualizovat profil skupiny"; -/* No comment provided by engineer. */ -"Save archive" = "Uložit archiv"; - -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Uložit nastavení automatického přijímání"; - /* No comment provided by engineer. */ "Save group profile" = "Uložení profilu skupiny"; @@ -2684,7 +2576,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Uložit přístupovou frázi do Klíčenky"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Uložit předvolby?"; /* No comment provided by engineer. */ @@ -2693,12 +2585,9 @@ /* No comment provided by engineer. */ "Save servers" = "Uložit servery"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Uložit servery?"; -/* No comment provided by engineer. */ -"Save settings?" = "Uložit nastavení?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Uložit uvítací zprávu?"; @@ -2741,7 +2630,7 @@ /* chat item text */ "security code changed" = "bezpečnostní kód změněn"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Vybrat"; /* No comment provided by engineer. */ @@ -2768,9 +2657,6 @@ /* No comment provided by engineer. */ "send direct message" = "odeslat přímou zprávu"; -/* No comment provided by engineer. */ -"Send direct message" = "Odeslat přímou zprávu"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Odeslat přímou zprávu pro připojení"; @@ -2786,9 +2672,6 @@ /* No comment provided by engineer. */ "Send notifications" = "Odeslat oznámení"; -/* No comment provided by engineer. */ -"Send notifications:" = "Odeslat oznámení:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "Zasílání otázek a nápadů"; @@ -2798,7 +2681,7 @@ /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Odeslat je z galerie nebo vlastní klávesnice."; -/* No comment provided by engineer. */ +/* alert message */ "Sender cancelled file transfer." = "Odesílatel zrušil přenos souboru."; /* No comment provided by engineer. */ @@ -2882,7 +2765,8 @@ /* No comment provided by engineer. */ "Settings" = "Nastavení"; -/* chat item action */ +/* alert action +chat item action */ "Share" = "Sdílet"; /* No comment provided by engineer. */ @@ -2891,15 +2775,12 @@ /* No comment provided by engineer. */ "Share address" = "Sdílet adresu"; -/* No comment provided by engineer. */ +/* 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 one-time invitation link" = "Jednorázový zvací odkaz"; - /* No comment provided by engineer. */ "Share with contacts" = "Sdílet s kontakty"; @@ -2936,7 +2817,7 @@ /* simplex link type */ "SimpleX group link" = "Skupinový odkaz SimpleX"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "Odkazy na SimpleX"; /* No comment provided by engineer. */ @@ -2966,18 +2847,12 @@ /* No comment provided by engineer. */ "Small groups (max 20)" = "Malé skupiny (max. 20)"; -/* No comment provided by engineer. */ -"SMP servers" = "SMP servery"; - /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Během importu došlo k nezávažným chybám - podrobnosti naleznete v chat konzoli."; /* notification title */ "Somebody" = "Někdo"; -/* No comment provided by engineer. */ -"Start a new chat" = "Začít nový chat"; - /* No comment provided by engineer. */ "Start chat" = "Začít chat"; @@ -2990,9 +2865,6 @@ /* No comment provided by engineer. */ "Stop" = "Zastavit"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Zastavte chat pro povolení akcí databáze"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Zastavení chatu pro export, import nebo smazání databáze chatu. Během zastavení chatu nebudete moci přijímat a odesílat zprávy."; @@ -3008,10 +2880,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Zastavit odesílání souboru?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Přestat sdílet"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Přestat sdílet adresu?"; /* authentication reason */ @@ -3047,9 +2919,6 @@ /* No comment provided by engineer. */ "Tap to join incognito" = "Klepnutím se připojíte inkognito"; -/* No comment provided by engineer. */ -"Tap to start a new chat" = "Klepnutím na zahájíte nový chat"; - /* No comment provided by engineer. */ "TCP connection timeout" = "Časový limit připojení TCP"; @@ -3071,7 +2940,7 @@ /* No comment provided by engineer. */ "Test servers" = "Testovací servery"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Testy selhaly!"; /* No comment provided by engineer. */ @@ -3083,9 +2952,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Díky uživatelům - přispívejte prostřednictvím Weblate!"; -/* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "1. Platforma bez identifikátorů uživatelů - soukromá už od záměru."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Aplikace vás může upozornit na přijaté zprávy nebo žádosti o kontakt - povolte to v nastavení."; @@ -3104,6 +2970,9 @@ /* 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ší."; @@ -3116,14 +2985,11 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "Zpráva bude pro všechny členy označena jako moderovaná."; -/* No comment provided by engineer. */ -"The next generation of private messaging" = "Nová generace soukromých zpráv"; - /* 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 profile is only shared with your contacts." = "Profil je sdílen pouze s vašimi kontakty."; +"Your profile is stored on your device and only shared with your contacts." = "Profil je sdílen pouze s vašimi kontakty."; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Druhé zaškrtnutí jsme přehlédli! ✅"; @@ -3134,15 +3000,6 @@ /* 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 **%@**."; -/* No comment provided by engineer. */ -"Theme" = "Téma"; - -/* No comment provided by engineer. */ -"There should be at least one user profile." = "Měl by tam být alespoň jeden uživatelský profil."; - -/* No comment provided by engineer. */ -"There should be at least one visible user profile." = "Měl by tam být alespoň jeden viditelný uživatelský profil."; - /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Toto nastavení je pro váš aktuální profil **%@**."; @@ -3179,15 +3036,15 @@ /* No comment provided by engineer. */ "To make a new connection" = "Vytvoření nového připojení"; -/* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Pro ochranu soukromí namísto ID uživatelů používaných všemi ostatními platformami má SimpleX identifikátory pro fronty zpráv, oddělené pro každý z vašich kontaktů."; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "K ochraně časového pásma používají obrazové/hlasové soubory UTC."; /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Chcete-li chránit své informace, zapněte zámek SimpleX Lock.\nPřed zapnutím této funkce budete vyzváni k dokončení ověření."; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Pro ochranu soukromí namísto ID uživatelů používaných všemi ostatními platformami má SimpleX identifikátory pro fronty zpráv, oddělené pro každý z vašich kontaktů."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Chcete-li nahrávat hlasové zprávy, udělte povolení k použití mikrofonu."; @@ -3215,22 +3072,16 @@ /* No comment provided by engineer. */ "Turn off" = "Vypnout"; -/* No comment provided by engineer. */ -"Turn off notifications?" = "Vypnout upozornění?"; - /* No comment provided by engineer. */ "Turn on" = "Zapnout"; /* No comment provided by engineer. */ "Unable to record voice message" = "Nelze nahrát hlasovou zprávu"; -/* item status description */ -"Unexpected error: %@" = "Neočekávaná chyba: %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "Neočekávaný stav přenášení"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Odobl."; /* No comment provided by engineer. */ @@ -3269,36 +3120,27 @@ /* authentication reason */ "Unlock app" = "Odemknout aplikaci"; -/* No comment provided by engineer. */ +/* notification label action */ "Unmute" = "Zrušit ztlumení"; -/* No comment provided by engineer. */ +/* swipe action */ "Unread" = "Nepřečtený"; /* No comment provided by engineer. */ "Update" = "Aktualizovat"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Aktualizovat nastavení hostitelů .onion?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Aktualizovat přístupovou frázi databáze"; /* No comment provided by engineer. */ "Update network settings?" = "Aktualizovat nastavení sítě?"; -/* No comment provided by engineer. */ -"Update transport isolation mode?" = "Aktualizovat režim dopravní izolace?"; - /* rcv group event chat item */ "updated group profile" = "aktualizoval profil skupiny"; /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "Aktualizací nastavení se klient znovu připojí ke všem serverům."; -/* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Aktualizace tohoto nastavení znovu připojí klienta ke všem serverům."; - /* No comment provided by engineer. */ "Upgrade and open chat" = "Zvýšit a otevřít chat"; @@ -3329,12 +3171,6 @@ /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "Používat servery SimpleX Chat?"; -/* No comment provided by engineer. */ -"User profile" = "Profil uživatele"; - -/* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Použití hostitelů .onion vyžaduje kompatibilního poskytovatele VPN."; - /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Používat servery SimpleX Chat."; @@ -3390,7 +3226,7 @@ "Voice messages are prohibited in this chat." = "Hlasové zprávy jsou v tomto chatu zakázány."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Hlasové zprávy jsou v této skupině zakázány."; +"Voice messages are prohibited." = "Hlasové zprávy jsou v této skupině zakázány."; /* No comment provided by engineer. */ "Voice messages prohibited!" = "Hlasové zprávy jsou zakázány!"; @@ -3434,9 +3270,6 @@ /* No comment provided by engineer. */ "When available" = "Když je k dispozici"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Když někdo požádá o připojení, můžete žádost přijmout nebo odmítnout."; - /* 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." = "Pokud s někým sdílíte inkognito profil, bude tento profil použit pro skupiny, do kterých vás pozve."; @@ -3449,15 +3282,9 @@ /* No comment provided by engineer. */ "Wrong passphrase!" = "Špatná přístupová fráze!"; -/* No comment provided by engineer. */ -"XFTP servers" = "XFTP servery"; - /* pref value */ "yes" = "ano"; -/* No comment provided by engineer. */ -"You" = "Vy"; - /* No comment provided by engineer. */ "You accepted connection" = "Přijali jste spojení"; @@ -3485,9 +3312,6 @@ /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Můžete přijímat hovory z obrazovky zámku, bez ověření zařízení a aplikace."; -/* No comment provided by engineer. */ -"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "Můžete se také připojit kliknutím na odkaz. Pokud se otevře v prohlížeči, klikněte na tlačítko **Otevřít v mobilní aplikaci**."; - /* No comment provided by engineer. */ "You can create it later" = "Můžete vytvořit později"; @@ -3501,7 +3325,7 @@ "You can hide or mute a user profile - swipe it to the right." = "Profil uživatele můžete skrýt nebo ztlumit - přejeďte prstem doprava."; /* notification body */ -"You can now send messages to %@" = "Nyní můžete posílat zprávy %@"; +"You can now chat with %@" = "Nyní můžete posílat zprávy %@"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Náhled oznámení na zamykací obrazovce můžete změnit v nastavení."; @@ -3512,9 +3336,6 @@ /* 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 **%@**."; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Můžete sdílet svou adresu jako odkaz nebo jako QR kód - kdokoli se k vám bude moci připojit."; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Chat můžete zahájit prostřednictvím aplikace Nastavení / Databáze nebo restartováním aplikace"; @@ -3539,14 +3360,11 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "změnili jste roli z %1$@ na %2$@"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Sami řídíte, přes který server(y) **přijímat** zprávy, své kontakty – servery, které používáte k odesílání zpráv."; - /* 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 have no chats" = "Nemáte žádné konverzace"; +"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í."; @@ -3614,9 +3432,6 @@ /* 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" = "Pro tuto skupinu používáte inkognito profil - abyste zabránili sdílení svého hlavního profilu, není pozvání kontaktů povoleno"; -/* No comment provided by engineer. */ -"Your %@ servers" = "Vaše servery %@"; - /* No comment provided by engineer. */ "Your calls" = "Vaše hovory"; @@ -3629,18 +3444,12 @@ /* No comment provided by engineer. */ "Your chat profiles" = "Vaše chat profily"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "K dokončení připojení, musí být váš kontakt online.\nToto připojení můžete zrušit a kontakt odebrat (a zkusit to později s novým odkazem)."; - /* 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 (%@)."; /* No comment provided by engineer. */ "Your contacts can allow full message deletion." = "Vaše kontakty mohou povolit úplné mazání zpráv."; -/* No comment provided by engineer. */ -"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "Vaše kontakty v SimpleX ji uvidí.\nMůžete ji změnit v Nastavení."; - /* No comment provided by engineer. */ "Your contacts will remain connected." = "Vaše kontakty zůstanou připojeny."; @@ -3663,7 +3472,7 @@ "Your profile **%@** will be shared." = "Váš profil **%@** bude sdílen."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Váš profil je uložen ve vašem zařízení a sdílen pouze s vašimi kontakty.\nServery SimpleX nevidí váš profil."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Váš profil je uložen ve vašem zařízení a sdílen pouze s vašimi kontakty. Servery SimpleX nevidí váš profil."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Váš profil, kontakty a doručené zprávy jsou uloženy ve vašem zařízení."; @@ -3671,9 +3480,6 @@ /* No comment provided by engineer. */ "Your random profile" = "Váš náhodný profil"; -/* No comment provided by engineer. */ -"Your server" = "Váš server"; - /* No comment provided by engineer. */ "Your server address" = "Adresa vašeho serveru"; @@ -3683,9 +3489,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "Vaše SimpleX adresa"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Vaše servery SMP"; - -/* No comment provided by engineer. */ -"Your XFTP servers" = "Vaše XFTP servery"; - diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index febd4c06a5..8da7835c43 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -1,18 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (kann kopiert werden)"; @@ -29,32 +14,17 @@ "- 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!"; /* No comment provided by engineer. */ -"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- Bis zu 5 Minuten lange Sprachnachrichten.\n- Zeitdauer für verschwindende Nachrichten anpassen.\n- Nachrichten-Historie bearbeiten."; - -/* No comment provided by engineer. */ -", " = ", "; - -/* No comment provided by engineer. */ -": " = ": "; +"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- Bis zu 5 Minuten lange Sprachnachrichten\n- Zeitdauer für verschwindende Nachrichten anpassen\n- Nachrichtenverlauf bearbeiten"; /* No comment provided by engineer. */ "!1 colored!" = "!1 farbig!"; -/* No comment provided by engineer. */ -"." = "."; - -/* No comment provided by engineer. */ -"(" = "("; - /* No comment provided by engineer. */ "(new)" = "(Neu)"; /* No comment provided by engineer. */ "(this device v%@)" = "(Dieses Gerät hat v%@)"; -/* No comment provided by engineer. */ -")" = ")"; - /* 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)"; @@ -65,10 +35,10 @@ "[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. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Fügen Sie einen neuen Kontakt hinzu**: Erzeugen Sie einen Einmal-QR-Code oder -Link für Ihren Kontakt."; +"**Create 1-time link**: to create and share a new invitation link." = "**Kontakt hinzufügen**: Um einen neuen Einladungslink zu erstellen."; /* No comment provided by engineer. */ -"**Create link / QR code** for your contact to use." = "**Generieren Sie einen Einladungs-Link / QR code** für Ihren Kontakt."; +"**Create group**: to create a new group." = "**Gruppe erstellen**: Um eine neue Gruppe zu erstellen."; /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "**E2E-verschlüsselter** Audioanruf"; @@ -77,26 +47,29 @@ "**e2e encrypted** video call" = "**E2E-verschlüsselter** Videoanruf"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Mehr Privatsphäre**: Es wird alle 20 Minuten auf neue Nachrichten geprüft. Nur Ihr Geräte-Token wird dem SimpleX-Chat-Server mitgeteilt, aber nicht wie viele Kontakte Sie haben oder welche Nachrichten Sie empfangen."; +"**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." = "**Mehr Privatsphäre**: Es wird alle 20 Minuten auf neue Nachrichten geprüft. Nur Ihr Geräte-Token wird dem SimpleX-Chat-Server mitgeteilt, aber nicht wie viele Kontakte Sie haben oder welche Nachrichten Sie empfangen."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Beste Privatsphäre**: Es wird kein SimpleX-Chat-Benachrichtigungs-Server genutzt, Nachrichten werden in periodischen Abständen im Hintergrund geprüft (dies hängt davon ab, wie häufig Sie die App nutzen)."; +"**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." = "**Beste Privatsphäre**: Es wird kein SimpleX-Chat-Benachrichtigungs-Server genutzt, Nachrichten werden in periodischen Abständen im Hintergrund geprüft (dies hängt davon ab, wie häufig Sie die App nutzen)."; /* No comment provided by engineer. */ -"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Fügen Sie den von Ihrem Kontakt erhaltenen Link ein** oder öffnen Sie ihn im Browser und tippen Sie auf **In mobiler App öffnen**."; +"**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Bitte beachten Sie**: Aus Sicherheitsgründen wird die Nachrichtenentschlüsselung Ihrer Verbindungen abgebrochen, wenn Sie die gleiche Datenbank auf zwei Geräten nutzen."; /* No comment provided by engineer. */ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Bitte beachten Sie**: Das Passwort kann NICHT wiederhergestellt oder geändert werden, wenn Sie es vergessen haben oder verlieren."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Empfohlen**: Nur Ihr Geräte-Token und ihre Benachrichtigungen werden an den SimpleX-Chat-Benachrichtigungs-Server gesendet, aber weder der Nachrichteninhalt noch deren Größe oder von wem sie gesendet wurde."; +"**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." = "**Empfohlen**: Nur Ihr Geräte-Token und ihre Benachrichtigungen werden an den SimpleX-Chat-Benachrichtigungs-Server gesendet, aber weder der Nachrichteninhalt noch deren Größe oder von wem sie gesendet wurde."; /* No comment provided by engineer. */ -"**Scan QR code**: to connect to your contact in person or via video call." = "**Scannen Sie den QR-Code**, um sich während einem persönlichen Treffen oder per Videoanruf mit Ihrem Kontakt zu verbinden."; +"**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. */ "**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."; +/* No comment provided by engineer. */ +"**Warning**: the archive will be removed." = "**Warnung**: Das Archiv wird gelöscht."; + /* No comment provided by engineer. */ "*bold*" = "\\*fett*"; @@ -104,7 +77,7 @@ "# %@" = "# %@"; /* copied message info */ -"## History" = "## Vergangenheit"; +"## History" = "## Nachrichtenverlauf"; /* copied message info */ "## In reply to" = "## Als Antwort auf"; @@ -139,6 +112,9 @@ /* No comment provided by engineer. */ "%@ connected" = "%@ wurde mit Ihnen verbunden"; +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ heruntergeladen"; + /* notification title */ "%@ is connected!" = "%@ ist mit Ihnen verbunden!"; @@ -149,11 +125,20 @@ "%@ is verified" = "%@ wurde erfolgreich überprüft"; /* No comment provided by engineer. */ -"%@ servers" = "%@-Server"; +"%@ server" = "%@ Server"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ Server"; + +/* No comment provided by engineer. */ +"%@ uploaded" = "%@ hochgeladen"; /* notification title */ "%@ wants to connect!" = "%@ will sich mit Ihnen verbinden!"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + /* No comment provided by engineer. */ "%@, %@ and %lld members" = "%@, %@ und %lld Mitglieder"; @@ -166,9 +151,24 @@ /* time interval */ "%d days" = "%d Tage"; +/* forward confirmation reason */ +"%d file(s) are still being downloaded." = "%d Datei(en) wird/werden immer noch heruntergeladen."; + +/* forward confirmation reason */ +"%d file(s) failed to download." = "Bei %d Datei(en) ist das Herunterladen fehlgeschlagen."; + +/* forward confirmation reason */ +"%d file(s) were deleted." = "%d Datei(en) wurde(n) gelöscht."; + +/* forward confirmation reason */ +"%d file(s) were not downloaded." = "%d Datei(en) wurde(n) nicht heruntergeladen."; + /* time interval */ "%d hours" = "%d Stunden"; +/* alert title */ +"%d messages not forwarded" = "%d Nachrichten wurden nicht weitergeleitet"; + /* time interval */ "%d min" = "%d min"; @@ -178,6 +178,9 @@ /* time interval */ "%d sec" = "%d s"; +/* delete after time */ +"%d seconds(s)" = "%d Sekunde(n)"; + /* integrity error chat item */ "%d skipped message(s)" = "%d übersprungene Nachricht(en)"; @@ -205,6 +208,9 @@ /* No comment provided by engineer. */ "%lld messages blocked" = "%lld Nachrichten blockiert"; +/* No comment provided by engineer. */ +"%lld messages blocked by admin" = "%lld Nachrichten wurden vom Administrator blockiert"; + /* No comment provided by engineer. */ "%lld messages marked deleted" = "%lld Nachrichten als gelöscht markiert"; @@ -217,9 +223,6 @@ /* No comment provided by engineer. */ "%lld new interface languages" = "%lld neue Sprachen für die Bedienoberfläche"; -/* No comment provided by engineer. */ -"%lld second(s)" = "%lld Sekunde(n)"; - /* No comment provided by engineer. */ "%lld seconds" = "%lld Sekunden"; @@ -265,8 +268,9 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ -"1 day" = "täglich"; +/* delete after time +time interval */ +"1 day" = "Älter als ein Tag"; /* time interval */ "1 hour" = "1 Stunde"; @@ -274,15 +278,23 @@ /* No comment provided by engineer. */ "1 minute" = "1 Minute"; -/* time interval */ -"1 month" = "monatlich"; +/* delete after time +time interval */ +"1 month" = "Älter als ein Monat"; -/* time interval */ -"1 week" = "wöchentlich"; +/* delete after time +time interval */ +"1 week" = "Älter als eine Woche"; + +/* delete after time */ +"1 year" = "Älter als ein Jahr"; /* No comment provided by engineer. */ "1-time link" = "Einmal-Link"; +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Ein Einmal-Link kann *nur mit einem Kontakt* genutzt werden - teilen Sie in nur persönlich oder über einen beliebigen Messenger."; + /* No comment provided by engineer. */ "5 minutes" = "5 Minuten"; @@ -308,19 +320,16 @@ "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." = "**Für jeden Kontakt und jedes Gruppenmitglied** wird eine separate TCP-Verbindung genutzt.\n**Bitte beachten Sie**: Wenn Sie viele Verbindungen haben, kann der Batterieverbrauch und die Datennutzung wesentlich höher sein und einige Verbindungen können scheitern."; /* No comment provided by engineer. */ -"Abort" = "Abbrechen"; +"Abort" = "Beenden"; /* No comment provided by engineer. */ -"Abort changing address" = "Wechsel der Adresse abbrechen"; +"Abort changing address" = "Wechsel der Empfängeradresse beenden"; /* No comment provided by engineer. */ -"Abort changing address?" = "Wechsel der Adresse abbrechen?"; +"Abort changing address?" = "Wechsel der Empfängeradresse beenden?"; /* No comment provided by engineer. */ -"About SimpleX" = "Über SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "Über die SimpleX-Adresse"; +"About operators" = "Über die Betreiber"; /* No comment provided by engineer. */ "About SimpleX Chat" = "Über SimpleX Chat"; @@ -329,83 +338,175 @@ "above, then choose:" = "Danach die gewünschte Aktion auswählen:"; /* No comment provided by engineer. */ -"Accent color" = "Akzentfarbe"; +"Accent" = "Akzent"; /* accept contact request via notification - accept incoming call via notification */ +accept incoming call via notification +swipe action */ "Accept" = "Annehmen"; +/* No comment provided by engineer. */ +"Accept conditions" = "Nutzungsbedingungen akzeptieren"; + /* No comment provided by engineer. */ "Accept connection request?" = "Kontaktanfrage annehmen?"; /* notification body */ "Accept contact request from %@?" = "Die Kontaktanfrage von %@ annehmen?"; -/* accept contact request via notification */ +/* accept contact request via notification +swipe action */ "Accept incognito" = "Inkognito akzeptieren"; /* call status */ "accepted call" = "Anruf angenommen"; /* 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 zu Ihrem Profil hinzu, damit Ihre Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet."; +"Accepted conditions" = "Akzeptierte Nutzungsbedingungen"; + +/* chat list item title */ +"accepted invitation" = "Einladung angenommen"; /* No comment provided by engineer. */ -"Add preset servers" = "Füge voreingestellte Server hinzu"; +"Acknowledged" = "Bestätigt"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Fehler bei der Bestätigung"; + +/* token status text */ +"Active" = "Aktiv"; + +/* No comment provided by engineer. */ +"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."; + +/* No comment provided by engineer. */ +"Add friends" = "Freunde aufnehmen"; + +/* No comment provided by engineer. */ +"Add list" = "Liste hinzufügen"; /* No comment provided by engineer. */ "Add profile" = "Profil hinzufügen"; /* No comment provided by engineer. */ -"Add server…" = "Füge Server hinzu…"; +"Add server" = "Server hinzufügen"; /* No comment provided by engineer. */ -"Add servers by scanning QR codes." = "Fügen Sie Server durch Scannen der QR Codes hinzu."; +"Add servers by scanning QR codes." = "Server durch Scannen von QR Codes hinzufügen."; + +/* No comment provided by engineer. */ +"Add team members" = "Team-Mitglieder aufnehmen"; /* No comment provided by engineer. */ "Add to another device" = "Einem anderen Gerät hinzufügen"; +/* No comment provided by engineer. */ +"Add to list" = "Zur Liste hinzufügen"; + /* No comment provided by engineer. */ "Add welcome message" = "Begrüßungsmeldung hinzufügen"; +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "Nehmen Sie Team-Mitglieder in Ihre Unterhaltungen auf."; + +/* No comment provided by engineer. */ +"Added media & file servers" = "Medien- und Dateiserver hinzugefügt"; + +/* No comment provided by engineer. */ +"Added message servers" = "Nachrichtenserver hinzugefügt"; + +/* No comment provided by engineer. */ +"Additional accent" = "Erste Akzentfarbe"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Zusätzlicher Akzent 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Zweite Akzentfarbe"; + /* No comment provided by engineer. */ "Address" = "Adresse"; /* No comment provided by engineer. */ -"Address change will be aborted. Old receiving address will be used." = "Der Wechsel der Adresse wird abgebrochen. Die bisherige Adresse wird weiter verwendet."; +"Address change will be aborted. Old receiving address will be used." = "Der Wechsel der Empfängeradresse wird beendet. Die bisherige Adresse wird weiter verwendet."; + +/* No comment provided by engineer. */ +"Address or 1-time link?" = "Adress- oder Einmal-Link?"; + +/* No comment provided by engineer. */ +"Address settings" = "Adress-Einstellungen"; /* member role */ "admin" = "Admin"; +/* feature role */ +"admins" = "Administratoren"; + +/* No comment provided by engineer. */ +"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."; /* No comment provided by engineer. */ "Advanced network settings" = "Erweiterte Netzwerkeinstellungen"; +/* No comment provided by engineer. */ +"Advanced settings" = "Erweiterte Einstellungen"; + /* chat item text */ "agreeing encryption for %@…" = "Verschlüsselung von %@ zustimmen…"; /* chat item text */ "agreeing encryption…" = "Verschlüsselung zustimmen…"; +/* No comment provided by engineer. */ +"All" = "Alle"; + /* No comment provided by engineer. */ "All app data is deleted." = "Werden die App-Daten komplett gelöscht."; /* No comment provided by engineer. */ -"All chats and messages will be deleted - this cannot be undone!" = "Alle Chats und Nachrichten werden gelöscht! Dies kann nicht rückgängig gemacht werden!"; +"All chats and messages will be deleted - this cannot be undone!" = "Es werden alle Chats und Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden!"; + +/* alert message */ +"All chats will be removed from the list %@, and the list deleted." = "Alle Chats werden von der Liste %@ entfernt und danach wird die Liste gelöscht."; /* No comment provided by engineer. */ "All data is erased when it is entered." = "Alle Daten werden gelöscht, sobald dieser eingegeben wird."; +/* No comment provided by engineer. */ +"All data is kept private on your device." = "Alle Daten werden nur auf Ihrem Gerät gespeichert."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Alle Gruppenmitglieder bleiben verbunden."; -/* No comment provided by engineer. */ -"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht."; +/* feature role */ +"all members" = "Alle Mitglieder"; /* No comment provided by engineer. */ -"All new messages from %@ will be hidden!" = "Alle neuen Nachrichten von %@ werden verborgen!"; +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Alle Nachrichten und Dateien werden **Ende-zu-Ende verschlüsselt** versendet - in Direkt-Nachrichten mit Post-Quantum-Security."; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone!" = "Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden!"; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht."; + +/* No comment provided by engineer. */ +"All new messages from %@ will be hidden!" = "Von %@ werden alle neuen Nachrichten ausgeblendet!"; + +/* profile dropdown */ +"All profiles" = "Alle Profile"; + +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Alle Meldungen werden für Sie archiviert."; + +/* No comment provided by engineer. */ +"All servers" = "Alle Server"; /* No comment provided by engineer. */ "All your contacts will remain connected." = "Alle Ihre Kontakte bleiben verbunden."; @@ -413,6 +514,9 @@ /* No comment provided by engineer. */ "All your contacts will remain connected. Profile update will be sent to your contacts." = "Alle Ihre Kontakte bleiben verbunden. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet."; +/* No comment provided by engineer. */ +"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Alle Ihre Kontakte, Unterhaltungen und Dateien werden sicher verschlüsselt und in Daten-Paketen auf die konfigurierten XTFP-Relais hochgeladen."; + /* No comment provided by engineer. */ "Allow" = "Erlauben"; @@ -420,10 +524,16 @@ "Allow calls only if your contact allows them." = "Erlauben Sie Anrufe nur dann, wenn es Ihr Kontakt ebenfalls erlaubt."; /* No comment provided by engineer. */ -"Allow disappearing messages only if your contact allows it to you." = "Erlauben Sie verschwindende Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt."; +"Allow calls?" = "Anrufe erlauben?"; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you." = "Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt."; +"Allow disappearing messages only if your contact allows it to you." = "Erlauben Sie verschwindende Nachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt."; + +/* No comment provided by engineer. */ +"Allow downgrade" = "Herabstufung erlauben"; + +/* 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 message reactions only if your contact allows them." = "Erlauben Sie Reaktionen auf Nachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt."; @@ -438,11 +548,20 @@ "Allow sending disappearing messages." = "Das Senden von verschwindenden Nachrichten erlauben."; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages." = "Unwiederbringliches löschen von gesendeten Nachrichten erlauben."; +"Allow sharing" = "Teilen erlauben"; + +/* No comment provided by engineer. */ +"Allow to irreversibly delete sent messages. (24 hours)" = "Unwiederbringliches löschen von gesendeten Nachrichten erlauben. (24 Stunden)"; + +/* No comment provided by engineer. */ +"Allow to report messsages to moderators." = "Melden von Nachrichten an Moderatoren erlauben."; /* No comment provided by engineer. */ "Allow to send files and media." = "Das Senden von Dateien und Medien erlauben."; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "Das Senden von SimpleX-Links erlauben."; + /* No comment provided by engineer. */ "Allow to send voice messages." = "Das Senden von Sprachnachrichten erlauben."; @@ -459,7 +578,7 @@ "Allow your contacts to call you." = "Erlaubt Ihren Kontakten Sie anzurufen."; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages." = "Erlauben Sie Ihren Kontakten gesendete Nachrichten unwiederbringlich zu löschen."; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Erlauben Sie Ihren Kontakten gesendete Nachrichten unwiederbringlich zu löschen. (24 Stunden)"; /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Erlauben Sie Ihren Kontakten das Senden von verschwindenden Nachrichten."; @@ -479,6 +598,9 @@ /* pref value */ "always" = "Immer"; +/* No comment provided by engineer. */ +"Always use private routing." = "Sie nutzen immer privates Routing."; + /* No comment provided by engineer. */ "Always use relay" = "Über ein Relais verbinden"; @@ -488,15 +610,27 @@ /* No comment provided by engineer. */ "and %lld other events" = "und %lld weitere Ereignisse"; +/* report reason */ +"Another reason" = "Anderer Grund"; + /* 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: %@"; +/* No comment provided by engineer. */ +"App data migration" = "App-Daten-Migration"; + /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "Neue lokale Dateien (außer Video-Dateien) werden von der App verschlüsselt."; +/* No comment provided by engineer. */ +"App group:" = "App-Gruppe:"; + /* No comment provided by engineer. */ "App icon" = "App-Icon"; @@ -506,6 +640,9 @@ /* No comment provided by engineer. */ "App passcode is replaced with self-destruct passcode." = "App-Zugangscode wurde durch den Selbstzerstörungs-Zugangscode ersetzt."; +/* No comment provided by engineer. */ +"App session" = "App-Sitzung"; + /* No comment provided by engineer. */ "App version" = "App Version"; @@ -515,9 +652,51 @@ /* No comment provided by engineer. */ "Appearance" = "Erscheinungsbild"; +/* No comment provided by engineer. */ +"Apply" = "Anwenden"; + +/* No comment provided by engineer. */ +"Apply to" = "Anwenden auf"; + +/* No comment provided by engineer. */ +"Archive" = "Archiv"; + +/* No comment provided by engineer. */ +"Archive %lld reports?" = "Archiviere %lld Meldungen?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "Alle Meldungen archivieren?"; + +/* No comment provided by engineer. */ +"Archive and upload" = "Archivieren und Hochladen"; + +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Kontakte für spätere Chats archivieren."; + +/* No comment provided by engineer. */ +"Archive report" = "Meldung archivieren"; + +/* No comment provided by engineer. */ +"Archive report?" = "Meldung archivieren?"; + +/* swipe action */ +"Archive reports" = "Meldungen archivieren"; + +/* No comment provided by engineer. */ +"Archived contacts" = "Archivierte Kontakte"; + +/* No comment provided by engineer. */ +"archived report" = "Archivierte Meldung"; + +/* No comment provided by engineer. */ +"Archiving database" = "Datenbank wird archiviert"; + /* No comment provided by engineer. */ "Attach" = "Anhängen"; +/* No comment provided by engineer. */ +"attempts" = "Versuche"; + /* No comment provided by engineer. */ "Audio & video calls" = "Audio- & Videoanrufe"; @@ -557,9 +736,15 @@ /* No comment provided by engineer. */ "Auto-accept images" = "Bilder automatisch akzeptieren"; +/* alert title */ +"Auto-accept settings" = "Einstellungen automatisch akzeptieren"; + /* No comment provided by engineer. */ "Back" = "Zurück"; +/* No comment provided by engineer. */ +"Background" = "Hintergrund-Farbe"; + /* No comment provided by engineer. */ "Bad desktop address" = "Falsche Desktop-Adresse"; @@ -575,15 +760,45 @@ /* No comment provided by engineer. */ "Bad message ID" = "Falsche Nachrichten-ID"; +/* No comment provided by engineer. */ +"Better calls" = "Verbesserte Anrufe"; + /* No comment provided by engineer. */ "Better groups" = "Bessere Gruppen"; +/* No comment provided by engineer. */ +"Better groups performance" = "Bessere Leistung von Gruppen"; + +/* No comment provided by engineer. */ +"Better message dates." = "Verbesserte Nachrichten-Datumsinformation"; + /* No comment provided by engineer. */ "Better messages" = "Verbesserungen bei Nachrichten"; +/* No comment provided by engineer. */ +"Better networking" = "Kontrollieren Sie Ihr Netzwerk"; + +/* No comment provided by engineer. */ +"Better notifications" = "Verbesserte Benachrichtigungen"; + +/* No comment provided by engineer. */ +"Better privacy and security" = "Bessere(r) Security und Datenschutz"; + +/* No comment provided by engineer. */ +"Better security ✅" = "Verbesserte Sicherheit ✅"; + +/* No comment provided by engineer. */ +"Better user experience" = "Verbesserte Nutzer-Erfahrung"; + +/* No comment provided by engineer. */ +"Black" = "Schwarz"; + /* No comment provided by engineer. */ "Block" = "Blockieren"; +/* No comment provided by engineer. */ +"Block for all" = "Für Alle blockieren"; + /* No comment provided by engineer. */ "Block group members" = "Gruppenmitglieder blockieren"; @@ -591,10 +806,29 @@ "Block member" = "Mitglied blockieren"; /* No comment provided by engineer. */ -"Block member?" = "Mitglied blockieren?"; +"Block member for all?" = "Mitglied für Alle blockieren?"; /* No comment provided by engineer. */ -"blocked" = "blockiert"; +"Block member?" = "Mitglied blockieren?"; + +/* marked deleted chat item preview text */ +"blocked" = "Blockiert"; + +/* rcv group event chat item */ +"blocked %@" = "%@ wurde blockiert"; + +/* blocked chat item +marked deleted chat item preview text */ +"blocked by admin" = "wurde vom Administrator blockiert"; + +/* No comment provided by engineer. */ +"Blocked by admin" = "wurde vom Administrator blockiert"; + +/* No comment provided by engineer. */ +"Blur for better privacy." = "Für bessere Privatsphäre verpixeln."; + +/* No comment provided by engineer. */ +"Blur media" = "Medium verpixeln"; /* No comment provided by engineer. */ "bold" = "fett"; @@ -603,7 +837,7 @@ "Both you and your contact can add message reactions." = "Sowohl Sie, als auch Ihr Kontakt können Reaktionen auf Nachrichten geben."; /* No comment provided by engineer. */ -"Both you and your contact can irreversibly delete sent messages." = "Sowohl Ihr Kontakt, als auch Sie können gesendete Nachrichten unwiederbringlich löschen."; +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "Sowohl Ihr Kontakt, als auch Sie können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden)"; /* No comment provided by engineer. */ "Both you and your contact can make calls." = "Sowohl Sie, als auch Ihr Kontakt können Anrufe tätigen."; @@ -617,9 +851,24 @@ /* 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)!"; +/* No comment provided by engineer. */ +"Business address" = "Geschäftliche Adresse"; + +/* No comment provided by engineer. */ +"Business chats" = "Geschäftliche Chats"; + +/* No comment provided by engineer. */ +"Businesses" = "Unternehmen"; + /* 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"; + /* No comment provided by engineer. */ "Call already ended!" = "Anruf ist bereits beendet!"; @@ -636,7 +885,16 @@ "Calls" = "Anrufe"; /* No comment provided by engineer. */ -"Can't delete user profile!" = "Das Benutzerprofil kann nicht gelöscht werden!"; +"Calls prohibited!" = "Anrufe nicht zugelassen!"; + +/* No comment provided by engineer. */ +"Camera not available" = "Kamera nicht verfügbar"; + +/* No comment provided by engineer. */ +"Can't call contact" = "Kontakt kann nicht angerufen werden"; + +/* No comment provided by engineer. */ +"Can't call member" = "Mitglied kann nicht angerufen werden"; /* No comment provided by engineer. */ "Can't invite contact!" = "Kontakt kann nicht eingeladen werden!"; @@ -645,8 +903,15 @@ "Can't invite contacts!" = "Kontakte können nicht eingeladen werden!"; /* No comment provided by engineer. */ +"Can't message member" = "Mitglied kann nicht benachrichtigt werden"; + +/* alert action +alert button */ "Cancel" = "Abbrechen"; +/* No comment provided by engineer. */ +"Cancel migration" = "Migration abbrechen"; + /* feature offered item */ "cancelled %@" = "abgebrochen %@"; @@ -654,11 +919,26 @@ "Cannot access keychain to save database password" = "Die App kann nicht auf den Schlüsselbund zugreifen, um das Datenbank-Passwort zu speichern"; /* No comment provided by engineer. */ +"Cannot forward message" = "Die Nachricht kann nicht weitergeleitet werden"; + +/* alert title */ "Cannot receive file" = "Datei kann nicht empfangen werden"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Kapazität überschritten - der Empfänger hat die zuvor gesendeten Nachrichten nicht empfangen."; + +/* No comment provided by engineer. */ +"Cellular" = "Mobilfunknetz"; + /* No comment provided by engineer. */ "Change" = "Ändern"; +/* alert title */ +"Change automatic message deletion?" = "Automatisches Löschen von Nachrichten ändern?"; + +/* authentication reason */ +"Change chat profiles" = "Chat-Profile wechseln"; + /* No comment provided by engineer. */ "Change database passphrase?" = "Datenbank-Passwort ändern?"; @@ -684,11 +964,11 @@ "Change self-destruct mode" = "Selbstzerstörungs-Modus ändern"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Selbstzerstörungs-Zugangscode ändern"; /* chat item text */ -"changed address for you" = "wechselte die Adresse für Sie"; +"changed address for you" = "Wechselte die Empfängeradresse von Ihnen"; /* rcv group event chat item */ "changed role of %@ to %@" = "änderte die Rolle von %1$@ auf %2$@"; @@ -697,13 +977,22 @@ "changed your role to %@" = "änderte Ihre Rolle auf %@"; /* chat item text */ -"changing address for %@…" = "Adresse von %@ wechseln…"; +"changing address for %@…" = "Empfängeradresse für %@ wechseln wird gestartet…"; /* chat item text */ -"changing address…" = "Wechsel der Adresse…"; +"changing address…" = "Wechsel der Empfängeradresse wurde gestartet…"; /* No comment provided by engineer. */ -"Chat archive" = "Datenbank Archiv"; +"Chat" = "Chat"; + +/* No comment provided by engineer. */ +"Chat already exists" = "Chat besteht bereits"; + +/* No comment provided by engineer. */ +"Chat already exists!" = "Chat besteht bereits!"; + +/* No comment provided by engineer. */ +"Chat colors" = "Chat-Farben"; /* No comment provided by engineer. */ "Chat console" = "Chat-Konsole"; @@ -714,6 +1003,9 @@ /* No comment provided by engineer. */ "Chat database deleted" = "Chat-Datenbank gelöscht"; +/* No comment provided by engineer. */ +"Chat database exported" = "Chat-Datenbank wurde exportiert"; + /* No comment provided by engineer. */ "Chat database imported" = "Chat-Datenbank importiert"; @@ -723,18 +1015,51 @@ /* No comment provided by engineer. */ "Chat is stopped" = "Der Chat ist beendet"; +/* 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." = "Der Chat ist angehalten. Wenn Sie diese Datenbank bereits auf einem anderen Gerät genutzt haben, sollten Sie diese vor dem Starten des Chats wieder zurückspielen."; + +/* No comment provided by engineer. */ +"Chat list" = "Chat-Liste"; + +/* No comment provided by engineer. */ +"Chat migrated!" = "Chat wurde migriert!"; + /* No comment provided by engineer. */ "Chat preferences" = "Chat-Präferenzen"; +/* alert message */ +"Chat preferences were changed." = "Die Chat-Präferenzen wurden geändert."; + +/* No comment provided by engineer. */ +"Chat profile" = "Benutzerprofil"; + +/* No comment provided by engineer. */ +"Chat theme" = "Chat-Design"; + +/* No comment provided by engineer. */ +"Chat will be deleted for all members - this cannot be undone!" = "Der Chat wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden!"; + +/* 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!"; + /* No comment provided by engineer. */ "Chats" = "Chats"; /* 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 title */ "Check server address and try again." = "Überprüfen Sie die Serveradresse und versuchen Sie es nochmal."; /* No comment provided by engineer. */ "Chinese and Spanish interface" = "Chinesische und spanische Bedienoberfläche"; +/* No comment provided by engineer. */ +"Choose _Migrate from another device_ on the new device and scan QR code." = "Wählen Sie auf dem neuen Gerät _Von einem anderen Gerät migrieren_ und scannen Sie den QR-Code."; + /* No comment provided by engineer. */ "Choose file" = "Datei auswählen"; @@ -742,22 +1067,46 @@ "Choose from library" = "Aus dem Fotoalbum auswählen"; /* No comment provided by engineer. */ -"Clear" = "Löschen"; +"Chunks deleted" = "Daten-Pakete gelöscht"; /* No comment provided by engineer. */ -"Clear conversation" = "Chatinhalte löschen"; +"Chunks downloaded" = "Daten-Pakete heruntergeladen"; /* No comment provided by engineer. */ -"Clear conversation?" = "Unterhaltung löschen?"; +"Chunks uploaded" = "Daten-Pakete hochgeladen"; + +/* swipe action */ +"Clear" = "Entfernen"; + +/* No comment provided by engineer. */ +"Clear conversation" = "Chat-Inhalte entfernen"; + +/* No comment provided by engineer. */ +"Clear conversation?" = "Chat-Inhalte entfernen?"; + +/* No comment provided by engineer. */ +"Clear group?" = "Gruppe entfernen?"; + +/* No comment provided by engineer. */ +"Clear or delete group?" = "Gruppe entfernen oder löschen?"; + +/* No comment provided by engineer. */ +"Clear private notes?" = "Private Notizen entfernen?"; /* No comment provided by engineer. */ "Clear verification" = "Überprüfung zurücknehmen"; /* No comment provided by engineer. */ -"colored" = "farbig"; +"Color chats with the new themes." = "Farbige Chats mit neuen Designs."; /* No comment provided by engineer. */ -"Colors" = "Farben"; +"Color mode" = "Farbvariante"; + +/* No comment provided by engineer. */ +"colored" = "farbig"; + +/* report reason */ +"Community guidelines violation" = "Verstoß gegen die Gemeinschaftsrichtlinien"; /* server test step */ "Compare file" = "Datei vergleichen"; @@ -768,15 +1117,51 @@ /* No comment provided by engineer. */ "complete" = "vollständig"; +/* No comment provided by engineer. */ +"Completed" = "Abgeschlossen"; + +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "Die Nutzungsbedingungen wurden akzeptiert am: %@."; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: **%@**."; + +/* No comment provided by engineer. */ +"Conditions are already accepted for these operator(s): **%@**." = "Die Nutzungsbedingungen der/des folgenden Betreiber(s) wurden schon akzeptiert: **%@**."; + +/* No comment provided by engineer. */ +"Conditions of use" = "Nutzungsbedingungen"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "Die Nutzungsbedingungen werden akzeptiert am: %@."; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "Die Nutzungsbedingungen der aktivierten Betreiber werden automatisch akzeptiert am: %@."; + /* No comment provided by engineer. */ "Configure ICE servers" = "ICE-Server konfigurieren"; +/* No comment provided by engineer. */ +"Configure server operators" = "Server-Betreiber konfigurieren"; + /* No comment provided by engineer. */ "Confirm" = "Bestätigen"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "Löschen des Kontakts bestätigen?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "Datenbank-Aktualisierungen bestätigen"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Dateien von unbekannten Servern bestätigen."; + +/* No comment provided by engineer. */ +"Confirm network settings" = "Bestätigen Sie die Netzwerkeinstellungen"; + /* No comment provided by engineer. */ "Confirm new passphrase…" = "Neues Passwort bestätigen…"; @@ -786,6 +1171,15 @@ /* No comment provided by engineer. */ "Confirm password" = "Passwort bestätigen"; +/* No comment provided by engineer. */ +"Confirm that you remember database passphrase to migrate it." = "Bitte bestätigen Sie für die Migration, dass Sie sich an Ihr Datenbank-Passwort erinnern."; + +/* No comment provided by engineer. */ +"Confirm upload" = "Hochladen bestätigen"; + +/* token status text */ +"Confirmed" = "Bestätigt"; + /* server test step */ "Connect" = "Verbinden"; @@ -801,6 +1195,9 @@ /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "Mit den SimpleX Chat-Entwicklern verbinden."; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Schneller mit Ihren Freunden verbinden."; + /* No comment provided by engineer. */ "Connect to yourself?" = "Mit Ihnen selbst verbinden?"; @@ -808,7 +1205,7 @@ "Connect to yourself?\nThis is your own one-time link!" = "Mit Ihnen selbst verbinden?\nDas ist Ihr eigener Einmal-Link!"; /* No comment provided by engineer. */ -"Connect to yourself?\nThis is your own SimpleX address!" = "Mit Ihnen selbst verbinden?\nDas ist Ihre eigene SimpleX-Adresse!"; +"Connect to yourself?\nThis is your own SimpleX address!" = "Sich mit Ihnen selbst verbinden?\nDas ist Ihre eigene SimpleX-Adresse!"; /* No comment provided by engineer. */ "Connect via contact address" = "Über die Kontakt-Adresse verbinden"; @@ -816,9 +1213,6 @@ /* No comment provided by engineer. */ "Connect via link" = "Über einen Link verbinden"; -/* No comment provided by engineer. */ -"Connect via link / QR code" = "Über einen Link / QR-Code verbinden"; - /* No comment provided by engineer. */ "Connect via one-time link" = "Über einen Einmal-Link verbinden"; @@ -828,18 +1222,27 @@ /* No comment provided by engineer. */ "connected" = "Verbunden"; +/* No comment provided by engineer. */ +"Connected" = "Verbunden"; + /* No comment provided by engineer. */ "Connected desktop" = "Verbundener Desktop"; /* rcv group event chat item */ "connected directly" = "Direkt miteinander verbunden"; +/* No comment provided by engineer. */ +"Connected servers" = "Verbundene Server"; + /* No comment provided by engineer. */ "Connected to desktop" = "Mit dem Desktop verbunden"; /* No comment provided by engineer. */ "connecting" = "verbinde"; +/* No comment provided by engineer. */ +"Connecting" = "Verbinden"; + /* No comment provided by engineer. */ "connecting (accepted)" = "Verbindung (angenommen)"; @@ -861,15 +1264,24 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Mit dem Server verbinden… (Fehler: %@)"; +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "Verbinde mit Kontakt, bitte warten oder später erneut überprüfen!"; + /* No comment provided by engineer. */ "Connecting to desktop" = "Mit dem Desktop verbinden"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "Verbinde…"; /* No comment provided by engineer. */ "Connection" = "Verbindung"; +/* No comment provided by engineer. */ +"Connection and servers status." = "Verbindungs- und Server-Status."; + +/* No comment provided by engineer. */ +"Connection blocked" = "Verbindung blockiert"; + /* No comment provided by engineer. */ "Connection error" = "Verbindungsfehler"; @@ -879,24 +1291,51 @@ /* chat list item title (it should not be shown */ "connection established" = "Verbindung hergestellt"; +/* No comment provided by engineer. */ +"Connection is blocked by server operator:\n%@" = "Die Verbindung wurde vom Server-Betreiber blockiert:\n%@"; + +/* No comment provided by engineer. */ +"Connection not ready." = "Verbindung noch nicht bereit."; + +/* No comment provided by engineer. */ +"Connection notifications" = "Verbindungsbenachrichtigungen"; + /* No comment provided by engineer. */ "Connection request sent!" = "Verbindungsanfrage wurde gesendet!"; +/* No comment provided by engineer. */ +"Connection requires encryption renegotiation." = "Die Verbindung erfordert eine Neuverhandlung der Verschlüsselung."; + +/* No comment provided by engineer. */ +"Connection security" = "Verbindungs-Sicherheit"; + /* No comment provided by engineer. */ "Connection terminated" = "Verbindung beendet"; /* No comment provided by engineer. */ "Connection timeout" = "Verbindungszeitüberschreitung"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "Die Verbindung mit dem Desktop wurde gestoppt"; + /* connection information */ "connection:%@" = "Verbindung:%@"; +/* No comment provided by engineer. */ +"Connections" = "Verbindungen"; + +/* profile update event chat item */ +"contact %@ changed to %@" = "Der Kontaktname wurde von %1$@ auf %2$@ geändert"; + /* No comment provided by engineer. */ "Contact allows" = "Der Kontakt erlaubt"; /* No comment provided by engineer. */ "Contact already exists" = "Der Kontakt ist bereits vorhanden"; +/* No comment provided by engineer. */ +"Contact deleted!" = "Kontakt gelöscht!"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "Kontakt nutzt E2E-Verschlüsselung"; @@ -910,7 +1349,7 @@ "Contact is connected" = "Mit Ihrem Kontakt verbunden"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "Ihr Kontakt ist noch nicht verbunden!"; +"Contact is deleted." = "Kontakt wurde gelöscht."; /* No comment provided by engineer. */ "Contact name" = "Kontaktname"; @@ -918,21 +1357,36 @@ /* No comment provided by engineer. */ "Contact preferences" = "Kontakt-Präferenzen"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "Kontakt wird gelöscht. Dies kann nicht rückgängig gemacht werden!"; + /* No comment provided by engineer. */ "Contacts" = "Kontakte"; /* No comment provided by engineer. */ "Contacts can mark messages for deletion; you will be able to view them." = "Ihre Kontakte können Nachrichten zum Löschen markieren. Sie können diese Nachrichten trotzdem anschauen."; +/* blocking reason */ +"Content violates conditions of use" = "Inhalt verletzt Nutzungsbedingungen"; + /* No comment provided by engineer. */ "Continue" = "Weiter"; -/* chat item action */ +/* No comment provided by engineer. */ +"Conversation deleted!" = "Chat-Inhalte entfernt!"; + +/* No comment provided by engineer. */ "Copy" = "Kopieren"; +/* No comment provided by engineer. */ +"Copy error" = "Fehlermeldung kopieren"; + /* No comment provided by engineer. */ "Core version: v%@" = "Core Version: v%@"; +/* No comment provided by engineer. */ +"Corner" = "Abrundung Ecken"; + /* No comment provided by engineer. */ "Correct name to %@?" = "Richtiger Name für %@?"; @@ -940,10 +1394,10 @@ "Create" = "Erstellen"; /* No comment provided by engineer. */ -"Create a group using a random profile." = "Erstellen Sie eine Gruppe mit einem zufälligen Profil."; +"Create 1-time link" = "Einmal-Link erstellen"; /* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Erstellen Sie eine Adresse, damit sich Personen mit Ihnen verbinden können."; +"Create a group using a random profile." = "Erstellen Sie eine Gruppe mit einem zufälligen Profil."; /* server test step */ "Create file" = "Datei erstellen"; @@ -958,10 +1412,10 @@ "Create link" = "Link erzeugen"; /* No comment provided by engineer. */ -"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Neues Profil in der [Desktop-App] erstellen (https://simplex.chat/downloads/). 💻"; +"Create list" = "Liste erstellen"; /* No comment provided by engineer. */ -"Create one-time invitation link" = "Einmal-Einladungslink erstellen"; +"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Neues Profil in der [Desktop-App] erstellen (https://simplex.chat/downloads/). 💻"; /* No comment provided by engineer. */ "Create profile" = "Profil erstellen"; @@ -979,17 +1433,35 @@ "Create your profile" = "Erstellen Sie Ihr Profil"; /* No comment provided by engineer. */ -"Created on %@" = "Erstellt am %@"; +"Created" = "Erstellt"; + +/* No comment provided by engineer. */ +"Created at" = "Erstellt um"; + +/* copied message info */ +"Created at: %@" = "Erstellt um: %@"; + +/* No comment provided by engineer. */ +"Creating archive link" = "Archiv-Link erzeugen"; + +/* No comment provided by engineer. */ +"Creating link…" = "Link wird erstellt…"; /* No comment provided by engineer. */ "creator" = "Ersteller"; +/* No comment provided by engineer. */ +"Current conditions text couldn't be loaded, you can review conditions via this link:" = "Der Text der aktuellen Nutzungsbedingungen konnte nicht geladen werden. Sie können die Nutzungsbedingungen unter diesem Link einsehen:"; + /* No comment provided by engineer. */ "Current Passcode" = "Aktueller Zugangscode"; /* No comment provided by engineer. */ "Current passphrase…" = "Aktuelles Passwort…"; +/* No comment provided by engineer. */ +"Current profile" = "Aktuelles Profil"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Die derzeit maximal unterstützte Dateigröße beträgt %@."; @@ -999,9 +1471,18 @@ /* No comment provided by engineer. */ "Custom time" = "Zeit anpassen"; +/* No comment provided by engineer. */ +"Customizable message shape." = "Anpassbares Format des Nachrichtenfelds"; + +/* No comment provided by engineer. */ +"Customize theme" = "Design anpassen"; + /* No comment provided by engineer. */ "Dark" = "Dunkel"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Farben für die dunkle Variante"; + /* No comment provided by engineer. */ "Database downgrade" = "Datenbank auf alte Version herabstufen"; @@ -1062,14 +1543,21 @@ /* time unit */ "days" = "Tage"; +/* No comment provided by engineer. */ +"Debug delivery" = "Debugging-Zustellung"; + /* No comment provided by engineer. */ "Decentralized" = "Dezentral"; /* message decrypt error item */ "Decryption error" = "Entschlüsselungsfehler"; -/* pref value */ -"default (%@)" = "Voreinstellung (%@)"; +/* No comment provided by engineer. */ +"decryption errors" = "Entschlüsselungs-Fehler"; + +/* delete after time +pref value */ +"default (%@)" = "Default (%@)"; /* No comment provided by engineer. */ "default (no)" = "Voreinstellung (Nein)"; @@ -1077,9 +1565,13 @@ /* No comment provided by engineer. */ "default (yes)" = "Voreinstellung (Ja)"; -/* chat item action */ +/* alert action +swipe action */ "Delete" = "Löschen"; +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "%lld Nachrichten der Mitglieder löschen?"; + /* No comment provided by engineer. */ "Delete %lld messages?" = "%lld Nachrichten löschen?"; @@ -1099,10 +1591,10 @@ "Delete and notify contact" = "Kontakt löschen und benachrichtigen"; /* No comment provided by engineer. */ -"Delete archive" = "Archiv löschen"; +"Delete chat" = "Chat löschen"; /* No comment provided by engineer. */ -"Delete chat archive?" = "Chat Archiv löschen?"; +"Delete chat messages from your device." = "Chat-Nachrichten von Ihrem Gerät löschen."; /* No comment provided by engineer. */ "Delete chat profile" = "Chat-Profil löschen"; @@ -1110,6 +1602,9 @@ /* No comment provided by engineer. */ "Delete chat profile?" = "Chat-Profil löschen?"; +/* No comment provided by engineer. */ +"Delete chat?" = "Chat löschen?"; + /* No comment provided by engineer. */ "Delete connection" = "Verbindung löschen"; @@ -1117,14 +1612,14 @@ "Delete contact" = "Kontakt löschen"; /* No comment provided by engineer. */ -"Delete Contact" = "Kontakt löschen"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Kontakt löschen?\nDas kann nicht rückgängig gemacht werden!"; +"Delete contact?" = "Kontakt löschen?"; /* No comment provided by engineer. */ "Delete database" = "Datenbank löschen"; +/* No comment provided by engineer. */ +"Delete database from this device" = "Datenbank auf diesem Gerät löschen"; + /* server test step */ "Delete file" = "Datei löschen"; @@ -1155,17 +1650,20 @@ /* No comment provided by engineer. */ "Delete link?" = "Link löschen?"; +/* alert title */ +"Delete list?" = "Liste löschen?"; + /* No comment provided by engineer. */ "Delete member message?" = "Nachricht des Mitglieds löschen?"; /* No comment provided by engineer. */ "Delete message?" = "Die Nachricht löschen?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "Nachrichten löschen"; /* No comment provided by engineer. */ -"Delete messages after" = "Löschen der Nachrichten"; +"Delete messages after" = "Nachrichten löschen"; /* No comment provided by engineer. */ "Delete old database" = "Alte Datenbank löschen"; @@ -1174,10 +1672,10 @@ "Delete old database?" = "Alte Datenbank löschen?"; /* No comment provided by engineer. */ -"Delete pending connection" = "Ausstehende Verbindung löschen"; +"Delete or moderate up to 200 messages." = "Bis zu 200 Nachrichten löschen oder moderieren"; /* No comment provided by engineer. */ -"Delete pending connection?" = "Die ausstehende Verbindung löschen?"; +"Delete pending connection?" = "Ausstehende Verbindung löschen?"; /* No comment provided by engineer. */ "Delete profile" = "Profil löschen"; @@ -1185,12 +1683,24 @@ /* server test step */ "Delete queue" = "Lösche Warteschlange"; +/* No comment provided by engineer. */ +"Delete report" = "Meldung löschen"; + +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Löschen Sie bis zu 20 Nachrichten auf einmal."; + /* No comment provided by engineer. */ "Delete user profile?" = "Benutzerprofil löschen?"; +/* No comment provided by engineer. */ +"Delete without notification" = "Ohne Benachrichtigung löschen"; + /* deleted chat item */ "deleted" = "Gelöscht"; +/* No comment provided by engineer. */ +"Deleted" = "Gelöscht"; + /* No comment provided by engineer. */ "Deleted at" = "Gelöscht um"; @@ -1203,6 +1713,12 @@ /* rcv group event chat item */ "deleted group" = "Gruppe gelöscht"; +/* No comment provided by engineer. */ +"Deletion errors" = "Fehler beim Löschen"; + +/* No comment provided by engineer. */ +"Delivered even when Apple drops them." = "Auslieferung, selbst wenn Apple sie löscht."; + /* No comment provided by engineer. */ "Delivery" = "Zustellung"; @@ -1224,9 +1740,27 @@ /* No comment provided by engineer. */ "Desktop devices" = "Desktop-Geräte"; +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "Adresse des Zielservers von %@ ist nicht kompatibel mit den Einstellungen des Weiterleitungsservers %@."; + +/* snd error text */ +"Destination server error: %@" = "Zielserver-Fehler: %@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "Die Version des Zielservers %@ ist nicht kompatibel mit dem Weiterleitungsserver %@."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Detaillierte Statistiken"; + +/* No comment provided by engineer. */ +"Details" = "Details"; + /* No comment provided by engineer. */ "Develop" = "Entwicklung"; +/* No comment provided by engineer. */ +"Developer options" = "Optionen für Entwickler"; + /* No comment provided by engineer. */ "Developer tools" = "Entwicklertools"; @@ -1252,11 +1786,20 @@ "Direct messages" = "Direkte Nachrichten"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt."; +"Direct messages between members are prohibited in this chat." = "In diesem Chat sind Direktnachrichten zwischen Mitgliedern nicht erlaubt."; + +/* 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. */ "Disable (keep overrides)" = "Deaktivieren (vorgenommene Einstellungen bleiben erhalten)"; +/* alert title */ +"Disable automatic message deletion?" = "Automatisches Löschen von Nachrichten deaktivieren?"; + +/* alert button */ +"Disable delete messages" = "Löschen von Nachrichten deaktivieren"; + /* No comment provided by engineer. */ "Disable for all" = "Für Alle deaktivieren"; @@ -1266,17 +1809,20 @@ /* No comment provided by engineer. */ "disabled" = "deaktiviert"; +/* No comment provided by engineer. */ +"Disabled" = "Deaktiviert"; + /* No comment provided by engineer. */ "Disappearing message" = "Verschwindende Nachricht"; /* chat feature */ -"Disappearing messages" = "verschwindende Nachrichten"; +"Disappearing messages" = "Verschwindende Nachrichten"; /* No comment provided by engineer. */ "Disappearing messages are prohibited in this chat." = "In diesem Chat sind verschwindende Nachrichten nicht erlaubt."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "In dieser Gruppe sind verschwindende Nachrichten nicht erlaubt."; +"Disappearing messages are prohibited." = "In dieser Gruppe sind verschwindende Nachrichten nicht erlaubt."; /* No comment provided by engineer. */ "Disappears at" = "Verschwindet um"; @@ -1300,7 +1846,22 @@ "Do it later" = "Später wiederholen"; /* No comment provided by engineer. */ -"Do NOT use SimpleX for emergency calls." = "Nutzen Sie SimpleX nicht für Notrufe."; +"Do not send history to new members." = "Den Nachrichtenverlauf nicht an neue Mitglieder 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."; + +/* No comment provided by engineer. */ +"Do not use credentials with proxy." = "Verwenden Sie keine Anmeldeinformationen mit einem Proxy."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "Sie nutzen KEIN privates Routing."; + +/* No comment provided by engineer. */ +"Do NOT use SimpleX for emergency calls." = "SimpleX NICHT für Notrufe nutzen."; + +/* No comment provided by engineer. */ +"Documents:" = "Dokumente:"; /* No comment provided by engineer. */ "Don't create address" = "Keine Adresse erstellt"; @@ -1308,27 +1869,64 @@ /* No comment provided by engineer. */ "Don't enable" = "Nicht aktivieren"; +/* No comment provided by engineer. */ +"Don't miss important messages." = "Verpassen Sie keine wichtigen Nachrichten."; + /* No comment provided by engineer. */ "Don't show again" = "Nicht nochmals anzeigen"; +/* No comment provided by engineer. */ +"Done" = "Fertig"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "Datenbank herabstufen und den Chat öffnen"; +/* alert button +chat item action */ +"Download" = "Herunterladen"; + +/* No comment provided by engineer. */ +"Download errors" = "Fehler beim Herunterladen"; + +/* No comment provided by engineer. */ +"Download failed" = "Herunterladen fehlgeschlagen"; + /* server test step */ "Download file" = "Datei herunterladen"; +/* alert action */ +"Download files" = "Dateien herunterladen"; + +/* No comment provided by engineer. */ +"Downloaded" = "Heruntergeladen"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Heruntergeladene Dateien"; + +/* No comment provided by engineer. */ +"Downloading archive" = "Archiv wird heruntergeladen"; + +/* No comment provided by engineer. */ +"Downloading link details" = "Link-Details werden heruntergeladen"; + /* No comment provided by engineer. */ "Duplicate display name!" = "Doppelter Anzeigename!"; /* integrity error chat item */ "duplicate message" = "Doppelte Nachricht"; +/* No comment provided by engineer. */ +"duplicates" = "Duplikate"; + /* No comment provided by engineer. */ "Duration" = "Dauer"; /* No comment provided by engineer. */ "e2e encrypted" = "E2E-verschlüsselt"; +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "E2E-verschlüsselte Benachrichtigungen."; + /* chat item action */ "Edit" = "Bearbeiten"; @@ -1341,12 +1939,21 @@ /* No comment provided by engineer. */ "Enable (keep overrides)" = "Aktivieren (vorgenommene Einstellungen bleiben erhalten)"; -/* No comment provided by engineer. */ +/* alert title */ "Enable automatic message deletion?" = "Automatisches Löschen von Nachrichten aktivieren?"; +/* No comment provided by engineer. */ +"Enable camera access" = "Kamera-Zugriff aktivieren"; + +/* No comment provided by engineer. */ +"Enable Flux in Network & servers settings for better metadata privacy." = "Für einen besseren Metadatenschutz Flux in den Netzwerk- und Servereinstellungen aktivieren."; + /* No comment provided by engineer. */ "Enable for all" = "Für Alle aktivieren"; +/* No comment provided by engineer. */ +"Enable in direct chats (BETA)!" = "Kann in direkten Chats aktiviert werden (BETA)!"; + /* No comment provided by engineer. */ "Enable instant notifications?" = "Sofortige Benachrichtigungen aktivieren?"; @@ -1374,6 +1981,12 @@ /* enabled status */ "enabled" = "Aktiviert"; +/* No comment provided by engineer. */ +"Enabled" = "Aktiviert"; + +/* No comment provided by engineer. */ +"Enabled for" = "Aktiviert für"; + /* enabled status */ "enabled for contact" = "Für Kontakt aktiviert"; @@ -1398,6 +2011,9 @@ /* notification */ "Encrypted message or another event" = "Verschlüsselte Nachricht oder ein anderes Ereignis"; +/* notification */ +"Encrypted message: app is stopped" = "Verschlüsselte Nachricht: Die App ist angehalten"; + /* notification */ "Encrypted message: database error" = "Verschlüsselte Nachricht: Datenbankfehler"; @@ -1443,6 +2059,9 @@ /* chat item text */ "encryption re-negotiation required for %@" = "Neuaushandlung der Verschlüsselung von %@ notwendig"; +/* No comment provided by engineer. */ +"Encryption renegotiation in progress." = "Die Neuverhandlung der Verschlüsselung läuft."; + /* No comment provided by engineer. */ "ended" = "beendet"; @@ -1458,6 +2077,9 @@ /* No comment provided by engineer. */ "Enter Passcode" = "Zugangscode eingeben"; +/* No comment provided by engineer. */ +"Enter passphrase" = "Passwort eingeben"; + /* No comment provided by engineer. */ "Enter passphrase…" = "Passwort eingeben…"; @@ -1486,19 +2108,25 @@ "Error" = "Fehler"; /* No comment provided by engineer. */ -"Error aborting address change" = "Fehler beim Abbrechen des Adresswechsels"; +"Error aborting address change" = "Fehler beim Beenden des Adresswechsels"; + +/* alert title */ +"Error accepting conditions" = "Fehler beim Akzeptieren der Nutzungsbedingungen"; /* No comment provided by engineer. */ "Error accepting contact request" = "Fehler beim Annehmen der Kontaktanfrage"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Fehler beim Zugriff auf die Datenbankdatei"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Fehler beim Hinzufügen von Mitgliedern"; +/* alert title */ +"Error adding server" = "Fehler beim Hinzufügen des Servers"; + /* No comment provided by engineer. */ -"Error changing address" = "Fehler beim Wechseln der Adresse"; +"Error changing address" = "Fehler beim Wechseln der Empfängeradresse"; + +/* No comment provided by engineer. */ +"Error changing connection profile" = "Fehler beim Wechseln des Verbindungs-Profils"; /* No comment provided by engineer. */ "Error changing role" = "Fehler beim Ändern der Rolle"; @@ -1506,6 +2134,15 @@ /* No comment provided by engineer. */ "Error changing setting" = "Fehler beim Ändern der Einstellung"; +/* No comment provided by engineer. */ +"Error changing to incognito!" = "Fehler beim Wechseln zum Inkognito-Profil!"; + +/* No comment provided by engineer. */ +"Error checking token status" = "Fehler beim Überprüfen des Token-Status"; + +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "Fehler beim Verbinden mit dem Weiterleitungsserver %@. Bitte versuchen Sie es später erneut."; + /* No comment provided by engineer. */ "Error creating address" = "Fehler beim Erstellen der Adresse"; @@ -1515,12 +2152,21 @@ /* No comment provided by engineer. */ "Error creating group link" = "Fehler beim Erzeugen des Gruppen-Links"; +/* alert title */ +"Error creating list" = "Fehler beim Erstellen der Liste"; + /* No comment provided by engineer. */ "Error creating member contact" = "Fehler beim Anlegen eines Mitglied-Kontaktes"; +/* No comment provided by engineer. */ +"Error creating message" = "Fehler beim Erstellen der Nachricht"; + /* No comment provided by engineer. */ "Error creating profile!" = "Fehler beim Erstellen des Profils!"; +/* No comment provided by engineer. */ +"Error creating report" = "Fehler beim Erstellen der Meldung"; + /* No comment provided by engineer. */ "Error decrypting file" = "Fehler beim Entschlüsseln der Datei"; @@ -1533,9 +2179,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Fehler beim Löschen der Verbindung"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Fehler beim Löschen des Kontakts"; - /* No comment provided by engineer. */ "Error deleting database" = "Fehler beim Löschen der Datenbank"; @@ -1548,6 +2191,9 @@ /* No comment provided by engineer. */ "Error deleting user profile" = "Fehler beim Löschen des Benutzerprofils"; +/* No comment provided by engineer. */ +"Error downloading the archive" = "Fehler beim Herunterladen des Archivs"; + /* No comment provided by engineer. */ "Error enabling delivery receipts!" = "Fehler beim Aktivieren von Empfangsbestätigungen!"; @@ -1560,23 +2206,47 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Fehler beim Exportieren der Chat-Datenbank"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Fehler beim Exportieren des Designs: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Fehler beim Importieren der Chat-Datenbank"; /* No comment provided by engineer. */ "Error joining group" = "Fehler beim Beitritt zur Gruppe"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Fehler beim Laden von %@ Servern"; +/* alert title */ +"Error loading servers" = "Fehler beim Laden der Server"; /* No comment provided by engineer. */ -"Error receiving file" = "Fehler beim Empfangen der Datei"; +"Error migrating settings" = "Fehler beim Migrieren der Einstellungen"; + +/* No comment provided by engineer. */ +"Error opening chat" = "Fehler beim Öffnen des Chats"; + +/* alert title */ +"Error receiving file" = "Fehler beim Herunterladen der Datei"; + +/* No comment provided by engineer. */ +"Error reconnecting server" = "Fehler beim Wiederherstellen der Verbindung zum Server"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Fehler beim Wiederherstellen der Verbindungen zu den Servern"; + +/* alert title */ +"Error registering for notifications" = "Fehler beim Registrieren für Benachrichtigungen"; /* No comment provided by engineer. */ "Error removing member" = "Fehler beim Entfernen des Mitglieds"; +/* alert title */ +"Error reordering lists" = "Fehler beim Umsortieren der Listen"; + /* No comment provided by engineer. */ -"Error saving %@ servers" = "Fehler beim Speichern der %@-Server"; +"Error resetting statistics" = "Fehler beim Zurücksetzen der Statistiken"; + +/* alert title */ +"Error saving chat list" = "Fehler beim Speichern der Chat-Liste"; /* No comment provided by engineer. */ "Error saving group profile" = "Fehler beim Speichern des Gruppenprofils"; @@ -1590,9 +2260,18 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "Fehler beim Speichern des Passworts in den Schlüsselbund"; +/* alert title */ +"Error saving servers" = "Fehler beim Speichern der Server"; + +/* when migrating */ +"Error saving settings" = "Fehler beim Abspeichern der Einstellungen"; + /* No comment provided by engineer. */ "Error saving user password" = "Fehler beim Speichern des Benutzer-Passworts"; +/* No comment provided by engineer. */ +"Error scanning code: %@" = "Fehler beim Scannen des Codes: %@"; + /* No comment provided by engineer. */ "Error sending email" = "Fehler beim Senden der eMail"; @@ -1612,17 +2291,26 @@ "Error stopping chat" = "Fehler beim Beenden des Chats"; /* No comment provided by engineer. */ +"Error switching profile" = "Fehler beim Wechseln des Profils"; + +/* alertTitle */ "Error switching profile!" = "Fehler beim Umschalten des Profils!"; /* No comment provided by engineer. */ "Error synchronizing connection" = "Fehler beim Synchronisieren der Verbindung"; +/* No comment provided by engineer. */ +"Error testing server connection" = "Fehler beim Testen der Server-Verbindung"; + /* No comment provided by engineer. */ "Error updating group link" = "Fehler beim Aktualisieren des Gruppen-Links"; /* No comment provided by engineer. */ "Error updating message" = "Fehler beim Aktualisieren der Nachricht"; +/* alert title */ +"Error updating server" = "Fehler beim Aktualisieren des Servers"; + /* No comment provided by engineer. */ "Error updating settings" = "Fehler beim Aktualisieren der Einstellungen"; @@ -1630,9 +2318,17 @@ "Error updating user privacy" = "Fehler beim Aktualisieren der Benutzer-Privatsphäre"; /* No comment provided by engineer. */ -"Error: " = "Fehler: "; +"Error uploading the archive" = "Fehler beim Hochladen des Archivs"; /* No comment provided by engineer. */ +"Error verifying passphrase:" = "Fehler bei der Überprüfung des Passworts:"; + +/* No comment provided by engineer. */ +"Error: " = "Fehler: "; + +/* alert message +file error text +snd error text */ "Error: %@" = "Fehler: %@"; /* No comment provided by engineer. */ @@ -1642,10 +2338,13 @@ "Error: URL is invalid" = "Fehler: URL ist ungültig"; /* No comment provided by engineer. */ -"Even when disabled in the conversation." = "Auch wenn sie im Chat deaktiviert sind."; +"Errors" = "Fehler"; + +/* servers error */ +"Errors in servers configuration." = "Fehler in der Server-Konfiguration."; /* No comment provided by engineer. */ -"event happened" = "event happened"; +"Even when disabled in the conversation." = "Auch wenn sie im Chat deaktiviert sind."; /* No comment provided by engineer. */ "Exit without saving" = "Beenden ohne Speichern"; @@ -1653,15 +2352,27 @@ /* chat item action */ "Expand" = "Erweitern"; +/* No comment provided by engineer. */ +"expired" = "Abgelaufen"; + +/* token status text */ +"Expired" = "Abgelaufen"; + /* No comment provided by engineer. */ "Export database" = "Datenbank exportieren"; /* No comment provided by engineer. */ "Export error:" = "Fehler beim Export:"; +/* No comment provided by engineer. */ +"Export theme" = "Design exportieren"; + /* No comment provided by engineer. */ "Exported database archive." = "Exportiertes Datenbankarchiv."; +/* No comment provided by engineer. */ +"Exported file doesn't exist" = "Die exportierte Datei ist nicht vorhanden"; + /* No comment provided by engineer. */ "Exporting database archive…" = "Exportieren des Datenbank-Archivs…"; @@ -1671,24 +2382,57 @@ /* No comment provided by engineer. */ "Fast and no wait until the sender is online!" = "Schnell und ohne warten auf den Absender, bis er online ist!"; +/* No comment provided by engineer. */ +"Faster deletion of groups." = "Schnelleres löschen von Gruppen."; + /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Schnellerer Gruppenbeitritt und zuverlässigere Nachrichtenzustellung."; /* No comment provided by engineer. */ +"Faster sending messages." = "Schnelleres versenden von Nachrichten."; + +/* swipe action */ "Favorite" = "Favorit"; +/* No comment provided by engineer. */ +"Favorites" = "Favoriten"; + +/* file error alert title */ +"File error" = "Datei-Fehler"; + +/* alert message */ +"File errors:\n%@" = "Datei-Fehler:\n%@"; + +/* file error text */ +"File is blocked by server operator:\n%@." = "Datei wurde vom Server-Betreiber blockiert:\n%@."; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Datei nicht gefunden - höchstwahrscheinlich wurde die Datei gelöscht oder der Transfer abgebrochen."; + +/* file error text */ +"File server error: %@" = "Datei-Server Fehler: %@"; + +/* No comment provided by engineer. */ +"File status" = "Datei-Status"; + +/* copied message info */ +"File status: %@" = "Datei-Status: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Die Datei wird von den Servern gelöscht."; /* No comment provided by engineer. */ -"File will be received when your contact completes uploading it." = "Die Datei wird empfangen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist."; +"File will be received when your contact completes uploading it." = "Die Datei wird heruntergeladen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist."; /* No comment provided by engineer. */ -"File will be received when your contact is online, please wait or check later!" = "Die Datei wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!"; +"File will be received when your contact is online, please wait or check later!" = "Die Datei wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!"; /* No comment provided by engineer. */ "File: %@" = "Datei: %@"; +/* No comment provided by engineer. */ +"Files" = "Dateien"; + /* No comment provided by engineer. */ "Files & media" = "Dateien & Medien"; @@ -1696,7 +2440,10 @@ "Files and media" = "Dateien und Medien"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "In dieser Gruppe sind Dateien und Medien nicht erlaubt."; +"Files and media are prohibited." = "In dieser Gruppe sind Dateien und Medien nicht erlaubt."; + +/* No comment provided by engineer. */ +"Files and media not allowed" = "Dateien und Medien sind nicht erlaubt"; /* No comment provided by engineer. */ "Files and media prohibited!" = "Dateien und Medien sind nicht erlaubt!"; @@ -1704,6 +2451,12 @@ /* No comment provided by engineer. */ "Filter unread and favorite chats." = "Nach ungelesenen und favorisierten Chats filtern."; +/* No comment provided by engineer. */ +"Finalize migration" = "Die Migration wird abgeschlossen"; + +/* No comment provided by engineer. */ +"Finalize migration on another device." = "Die Migration auf dem anderen Gerät wird abgeschlossen."; + /* No comment provided by engineer. */ "Finally, we have them! 🚀" = "Endlich haben wir sie! 🚀"; @@ -1728,9 +2481,72 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Reparatur wird vom Gruppenmitglied nicht unterstützt"; +/* No comment provided by engineer. */ +"For all moderators" = "Für alle Moderatoren"; + +/* servers error */ +"For chat profile %@:" = "Für das Chat-Profil %@:"; + /* No comment provided by engineer. */ "For console" = "Für Konsole"; +/* No comment provided by engineer. */ +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Wenn Ihr Kontakt beispielsweise Nachrichten über einen SimpleX-Chatserver empfängt, wird Ihre App diese über einen der Server von Flux versenden."; + +/* No comment provided by engineer. */ +"For me" = "Für mich"; + +/* No comment provided by engineer. */ +"For private routing" = "Für privates Routing"; + +/* No comment provided by engineer. */ +"For social media" = "Für soziale Medien"; + +/* chat item action */ +"Forward" = "Weiterleiten"; + +/* alert title */ +"Forward %d message(s)?" = "%d Nachricht(en) weiterleiten?"; + +/* No comment provided by engineer. */ +"Forward and save messages" = "Nachrichten weiterleiten und speichern"; + +/* alert action */ +"Forward messages" = "Nachrichten weiterleiten"; + +/* alert message */ +"Forward messages without files?" = "Nachrichten ohne Dateien weiterleiten?"; + +/* No comment provided by engineer. */ +"Forward up to 20 messages at once." = "Bis zu 20 Nachrichten auf einmal weiterleiten"; + +/* No comment provided by engineer. */ +"forwarded" = "weitergeleitet"; + +/* No comment provided by engineer. */ +"Forwarded" = "Weitergeleitet"; + +/* No comment provided by engineer. */ +"Forwarded from" = "Weitergeleitet aus"; + +/* No comment provided by engineer. */ +"Forwarding %lld messages" = "%lld Nachricht(en) wird/werden weitergeleitet"; + +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Weiterleitungsserver %@ konnte sich nicht mit dem Zielserver %@ verbinden. Bitte versuchen Sie es später erneut."; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "Adresse des Weiterleitungsservers ist nicht kompatibel mit den Netzwerkeinstellungen: %@."; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "Version des Weiterleitungsservers ist nicht kompatibel mit den Netzwerkeinstellungen: %@."; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Weiterleitungsserver: %1$@\nZielserver Fehler: %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Weiterleitungsserver: %1$@\nFehler: %2$@"; + /* No comment provided by engineer. */ "Found desktop" = "Gefundener Desktop"; @@ -1743,9 +2559,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Vollständiger Name (optional)"; -/* No comment provided by engineer. */ -"Full name:" = "Vollständiger Name:"; - /* No comment provided by engineer. */ "Fully decentralized – visible only to members." = "Vollständig dezentralisiert – nur für Mitglieder sichtbar."; @@ -1755,9 +2568,18 @@ /* No comment provided by engineer. */ "Further reduced battery usage" = "Weiter reduzierter Batterieverbrauch"; +/* No comment provided by engineer. */ +"Get notified when mentioned." = "Bei Erwähnung benachrichtigt werden."; + /* No comment provided by engineer. */ "GIFs and stickers" = "GIFs und Sticker"; +/* message preview */ +"Good afternoon!" = "Guten Nachmittag!"; + +/* message preview */ +"Good morning!" = "Guten Morgen!"; + /* No comment provided by engineer. */ "Group" = "Gruppe"; @@ -1794,24 +2616,6 @@ /* No comment provided by engineer. */ "Group links" = "Gruppen-Links"; -/* No comment provided by engineer. */ -"Group members can add message reactions." = "Gruppenmitglieder können eine Reaktion auf Nachrichten geben."; - -/* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages." = "Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen."; - -/* No comment provided by engineer. */ -"Group members can send direct messages." = "Gruppenmitglieder können Direktnachrichten versenden."; - -/* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Gruppenmitglieder können verschwindende Nachrichten senden."; - -/* No comment provided by engineer. */ -"Group members can send files and media." = "Gruppenmitglieder können Dateien und Medien senden."; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "Gruppenmitglieder können Sprachnachrichten versenden."; - /* notification */ "Group message:" = "Grppennachricht:"; @@ -1834,14 +2638,20 @@ "Group welcome message" = "Gruppen-Begrüßungsmeldung"; /* No comment provided by engineer. */ -"Group will be deleted for all members - this cannot be undone!" = "Die Gruppe wird für alle Mitglieder gelöscht - dies kann nicht rückgängig gemacht werden!"; +"Group will be deleted for all members - this cannot be undone!" = "Die Gruppe wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden!"; /* No comment provided by engineer. */ -"Group will be deleted for you - this cannot be undone!" = "Die Gruppe wird für Sie gelöscht - dies kann nicht rückgängig gemacht werden!"; +"Group will be deleted for you - this cannot be undone!" = "Die Gruppe wird nur bei Ihnen gelöscht. Dies kann nicht rückgängig gemacht werden!"; + +/* No comment provided by engineer. */ +"Groups" = "Gruppen"; /* No comment provided by engineer. */ "Help" = "Hilfe"; +/* No comment provided by engineer. */ +"Help admins moderating their groups." = "Helfen Sie Administratoren bei der Moderation ihrer Gruppen."; + /* No comment provided by engineer. */ "Hidden" = "Verborgen"; @@ -1864,12 +2674,21 @@ "Hide:" = "Verberge:"; /* No comment provided by engineer. */ -"History" = "Vergangenheit"; +"History" = "Nachrichtenverlauf"; + +/* No comment provided by engineer. */ +"History is not sent to new members." = "Der Nachrichtenverlauf wird nicht an neue Gruppenmitglieder gesendet."; /* time unit */ "hours" = "Stunden"; /* No comment provided by engineer. */ +"How it affects privacy" = "Wie es die Privatsphäre beeinflusst"; + +/* No comment provided by engineer. */ +"How it helps privacy" = "Wie es die Privatsphäre schützt"; + +/* alert button */ "How it works" = "Wie es funktioniert"; /* No comment provided by engineer. */ @@ -1884,15 +2703,15 @@ /* No comment provided by engineer. */ "How to use your servers" = "Wie Sie Ihre Server nutzen"; +/* No comment provided by engineer. */ +"Hungarian interface" = "Ungarische Bedienoberfläche"; + /* No comment provided by engineer. */ "ICE servers (one per line)" = "ICE-Server (einer pro Zeile)"; /* No comment provided by engineer. */ "If you can't meet in person, show QR code in a video call, or share the link." = "Falls Sie sich nicht persönlich treffen können, zeigen Sie den QR-Code in einem Videoanruf oder teilen Sie den Link."; -/* No comment provided by engineer. */ -"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Wenn Sie sich nicht persönlich treffen können, kann der **QR-Code während eines Videoanrufs gescannt werden**, oder Ihr Kontakt kann den Einladungslink über einen anderen Kanal mit Ihnen teilen."; - /* No comment provided by engineer. */ "If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Wenn Sie diesen Zugangscode während des Öffnens der App eingeben, werden alle App-Daten unwiederbringlich gelöscht!"; @@ -1906,16 +2725,16 @@ "Ignore" = "Ignorieren"; /* No comment provided by engineer. */ -"Image will be received when your contact completes uploading it." = "Das Bild wird empfangen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist."; +"Image will be received when your contact completes uploading it." = "Das Bild wird heruntergeladen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist."; /* No comment provided by engineer. */ -"Image will be received when your contact is online, please wait or check later!" = "Das Bild wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!"; +"Image will be received when your contact is online, please wait or check later!" = "Das Bild wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!"; /* No comment provided by engineer. */ "Immediately" = "Sofort"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Immun gegen Spam und Missbrauch"; +"Immune to spam" = "Immun gegen Spam und Missbrauch"; /* No comment provided by engineer. */ "Import" = "Importieren"; @@ -1926,15 +2745,45 @@ /* No comment provided by engineer. */ "Import database" = "Datenbank importieren"; +/* No comment provided by engineer. */ +"Import failed" = "Import ist fehlgeschlagen"; + +/* No comment provided by engineer. */ +"Import theme" = "Design importieren"; + +/* No comment provided by engineer. */ +"Importing archive" = "Archiv wird importiert"; + +/* No comment provided by engineer. */ +"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "Verbesserte Nachrichten-Auslieferung und verringerter Datenverbrauch.\nWeitere Verbesserungen sind bald verfügbar!"; + +/* No comment provided by engineer. */ +"Improved message delivery" = "Verbesserte Zustellung von Nachrichten"; + /* No comment provided by engineer. */ "Improved privacy and security" = "Verbesserte Privatsphäre und Sicherheit"; /* No comment provided by engineer. */ "Improved server configuration" = "Verbesserte Serverkonfiguration"; +/* No comment provided by engineer. */ +"In order to continue, chat should be stopped." = "Um fortzufahren, sollte der Chat beendet werden."; + /* No comment provided by engineer. */ "In reply to" = "Als Antwort auf"; +/* No comment provided by engineer. */ +"In-call sounds" = "Klingeltöne"; + +/* No comment provided by engineer. */ +"inactive" = "Inaktiv"; + +/* report reason */ +"Inappropriate content" = "Unangemessener Inhalt"; + +/* report reason */ +"Inappropriate profile" = "Unangemessenes Profil"; + /* No comment provided by engineer. */ "Incognito" = "Inkognito"; @@ -1966,10 +2815,10 @@ "Incoming video call" = "Eingehender Videoanruf"; /* No comment provided by engineer. */ -"Incompatible database version" = "Inkompatible Datenbank-Version"; +"Incompatible database version" = "Datenbank-Version nicht kompatibel"; /* No comment provided by engineer. */ -"Incompatible version" = "Inkompatible Version"; +"Incompatible version" = "Version nicht kompatibel"; /* PIN entry */ "Incorrect passcode" = "Zugangscode ist falsch"; @@ -1989,14 +2838,32 @@ /* 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)"; +/* No comment provided by engineer. */ +"Instant" = "Sofort"; + /* No comment provided by engineer. */ "Instant push notifications will be hidden!\n" = "Sofortige Push-Benachrichtigungen werden verborgen!\n"; /* No comment provided by engineer. */ -"Instantly" = "Sofort"; +"Interface" = "Schnittstelle"; /* No comment provided by engineer. */ -"Interface" = "Schnittstelle"; +"Interface colors" = "Interface-Farben"; + +/* token status text */ +"Invalid" = "Ungültig"; + +/* token status text */ +"Invalid (bad token)" = "Ungültig (falsches Token)"; + +/* token status text */ +"Invalid (expired)" = "Ungültig (abgelaufen)"; + +/* token status text */ +"Invalid (unregistered)" = "Ungültig (nicht registriert)"; + +/* token status text */ +"Invalid (wrong topic)" = "Ungültig (falsches Thema)"; /* invalid chat data */ "invalid chat" = "Ungültiger Chat"; @@ -2010,10 +2877,25 @@ /* invalid chat item */ "invalid data" = "Ungültige Daten"; +/* No comment provided by engineer. */ +"Invalid display name!" = "Ungültiger Anzeigename!"; + +/* No comment provided by engineer. */ +"Invalid link" = "Ungültiger Link"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Migrations-Bestätigung ungültig"; + /* No comment provided by engineer. */ "Invalid name!" = "Ungültiger Name!"; /* No comment provided by engineer. */ +"Invalid QR code" = "Ungültiger QR-Code"; + +/* No comment provided by engineer. */ +"Invalid response" = "Ungültige Reaktion"; + +/* alert title */ "Invalid server address!" = "Ungültige Serveradresse!"; /* item status text */ @@ -2025,12 +2907,18 @@ /* group name */ "invitation to group %@" = "Einladung zur Gruppe %@"; +/* No comment provided by engineer. */ +"invite" = "Einladen"; + /* No comment provided by engineer. */ "Invite friends" = "Freunde einladen"; /* No comment provided by engineer. */ "Invite members" = "Mitglieder einladen"; +/* No comment provided by engineer. */ +"Invite to chat" = "Zum Chat einladen"; + /* No comment provided by engineer. */ "Invite to group" = "In Gruppe einladen"; @@ -2052,6 +2940,9 @@ /* 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." = "Für die sichere Speicherung des Passworts nach dem Neustart der App und dem Wechsel des Passworts wird der iOS Schlüsselbund verwendet - dies erlaubt den Empfang von Push-Benachrichtigungen."; +/* No comment provided by engineer. */ +"IP address" = "IP-Adresse"; + /* No comment provided by engineer. */ "Irreversible message deletion" = "Unwiederbringliches löschen einer Nachricht"; @@ -2059,7 +2950,7 @@ "Irreversible message deletion is prohibited in this chat." = "In diesem Chat ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt."; +"Irreversible message deletion is prohibited." = "In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Er ermöglicht mehrere anonyme Verbindungen in einem einzigen Chat-Profil ohne Daten zwischen diesen zu teilen."; @@ -2070,6 +2961,9 @@ /* No comment provided by engineer. */ "It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Dies kann unter folgenden Umständen passieren:\n1. Die Nachrichten verfallen auf dem sendenden Client-System nach 2 Tagen oder auf dem Server nach 30 Tagen.\n2. Die Nachrichten-Entschlüsselung ist fehlgeschlagen, da von Ihnen oder Ihrem Kontakt ein altes Datenbank-Backup genutzt wurde.\n3. Die Verbindung wurde kompromittiert."; +/* No comment provided by engineer. */ +"It protects your IP address and connections." = "Ihre IP-Adresse und Verbindungen werden geschützt."; + /* 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 (%@)." = "Es sieht so aus, als ob Sie bereits über diesen Link verbunden sind. Wenn das nicht der Fall ist, gab es einen Fehler (%@)."; @@ -2082,7 +2976,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Japanische Benutzeroberfläche"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Beitreten"; /* No comment provided by engineer. */ @@ -2091,6 +2985,9 @@ /* No comment provided by engineer. */ "Join group" = "Treten Sie der Gruppe bei"; +/* No comment provided by engineer. */ +"Join group conversations" = "Gruppenunterhaltungen beitreten"; + /* No comment provided by engineer. */ "Join group?" = "Der Gruppe beitreten?"; @@ -2106,9 +3003,18 @@ /* No comment provided by engineer. */ "Joining group" = "Der Gruppe beitreten"; +/* alert action */ +"Keep" = "Behalten"; + +/* No comment provided by engineer. */ +"Keep conversation" = "Chat-Inhalte beibehalten"; + /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Die App muss geöffnet bleiben, um sie vom Desktop aus nutzen zu können"; +/* alert title */ +"Keep unused invitation?" = "Nicht genutzte Einladung behalten?"; + /* No comment provided by engineer. */ "Keep your connections" = "Ihre Verbindungen beibehalten"; @@ -2124,9 +3030,15 @@ /* No comment provided by engineer. */ "Learn more" = "Mehr erfahren"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Verlassen"; +/* No comment provided by engineer. */ +"Leave chat" = "Chat verlassen"; + +/* No comment provided by engineer. */ +"Leave chat?" = "Chat verlassen?"; + /* No comment provided by engineer. */ "Leave group" = "Gruppe verlassen"; @@ -2154,6 +3066,15 @@ /* No comment provided by engineer. */ "Linked desktops" = "Verknüpfte Desktops"; +/* swipe action */ +"List" = "Liste"; + +/* No comment provided by engineer. */ +"List name and emoji should be different for all lists." = "Der Listenname und das Emoji sollen für alle Listen unterschiedlich sein."; + +/* No comment provided by engineer. */ +"List name..." = "Listenname..."; + /* No comment provided by engineer. */ "LIVE" = "LIVE"; @@ -2175,9 +3096,6 @@ /* No comment provided by engineer. */ "Lock mode" = "Sperr-Modus"; -/* No comment provided by engineer. */ -"Make a private connection" = "Stellen Sie eine private Verbindung her"; - /* No comment provided by engineer. */ "Make one message disappear" = "Eine verschwindende Nachricht verfassen"; @@ -2185,13 +3103,7 @@ "Make profile private!" = "Privates Profil erzeugen!"; /* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Stellen Sie sicher, dass die %@-Server-Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind (%@)."; - -/* No comment provided by engineer. */ -"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht doppelt vorhanden sind."; - -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Viele Menschen haben gefragt: *Wie kann SimpleX Nachrichten zustellen, wenn es keine Benutzerkennungen gibt?*"; +"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind."; /* No comment provided by engineer. */ "Mark deleted for everyone" = "Für Alle als gelöscht markieren"; @@ -2211,15 +3123,33 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "Max. 30 Sekunden, sofort erhalten."; +/* No comment provided by engineer. */ +"Media & file servers" = "Medien- und Datei-Server"; + +/* blur media */ +"Medium" = "Medium"; + /* member role */ "member" = "Mitglied"; /* No comment provided by engineer. */ "Member" = "Mitglied"; +/* profile update event chat item */ +"member %@ changed to %@" = "Der Mitgliedsname von %1$@ wurde auf %2$@ geändert"; + /* rcv group event chat item */ "member connected" = "ist der Gruppe beigetreten"; +/* item status text */ +"Member inactive" = "Mitglied inaktiv"; + +/* chat feature */ +"Member reports" = "Mitglieder-Meldungen"; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All chat members will be notified." = "Die Rolle des Mitglieds wird auf \"%@\" geändert. Alle Chat-Mitglieder werden darüber informiert."; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Die Mitgliederrolle wird auf \"%@\" geändert. Alle Mitglieder der Gruppe werden benachrichtigt."; @@ -2227,7 +3157,43 @@ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Die Mitgliederrolle wird auf \"%@\" geändert. Das Mitglied wird eine neue Einladung erhalten."; /* No comment provided by engineer. */ -"Member will be removed from group - this cannot be undone!" = "Das Mitglied wird aus der Gruppe entfernt - dies kann nicht rückgängig gemacht werden!"; +"Member will be removed from chat - this cannot be undone!" = "Das Mitglied wird aus dem Chat entfernt. Dies kann nicht rückgängig gemacht werden!"; + +/* No comment provided by engineer. */ +"Member will be removed from group - this cannot be undone!" = "Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden!"; + +/* No comment provided by engineer. */ +"Members can add message reactions." = "Gruppenmitglieder können eine Reaktion auf Nachrichten geben."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden)"; + +/* No comment provided by engineer. */ +"Members can report messsages to moderators." = "Mitglieder können Nachrichten an Moderatoren melden."; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Gruppenmitglieder können Direktnachrichten versenden."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Gruppenmitglieder können verschwindende Nachrichten versenden."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Gruppenmitglieder können Dateien und Medien versenden."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "Gruppenmitglieder können SimpleX-Links versenden."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Gruppenmitglieder können Sprachnachrichten versenden."; + +/* No comment provided by engineer. */ +"Mention members 👋" = "Erwähnung von Mitgliedern 👋"; + +/* No comment provided by engineer. */ +"Menus" = "Menüs"; + +/* No comment provided by engineer. */ +"message" = "Nachricht"; /* item status text */ "Message delivery error" = "Fehler bei der Nachrichtenzustellung"; @@ -2235,9 +3201,21 @@ /* No comment provided by engineer. */ "Message delivery receipts!" = "Empfangsbestätigungen für Nachrichten!"; +/* item status text */ +"Message delivery warning" = "Warnung bei der Nachrichtenzustellung"; + /* No comment provided by engineer. */ "Message draft" = "Nachrichtenentwurf"; +/* item status text */ +"Message forwarded" = "Nachricht weitergeleitet"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Die Nachricht kann später zugestellt werden, wenn das Mitglied aktiv wird."; + +/* No comment provided by engineer. */ +"Message queue info" = "Nachrichten-Warteschlangen-Information"; + /* chat feature */ "Message reactions" = "Reaktionen auf Nachrichten"; @@ -2245,14 +3223,35 @@ "Message reactions are prohibited in this chat." = "In diesem Chat sind Reaktionen auf Nachrichten nicht erlaubt."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "In dieser Gruppe sind Reaktionen auf Nachrichten nicht erlaubt."; +"Message reactions are prohibited." = "In dieser Gruppe sind Reaktionen auf Nachrichten nicht erlaubt."; /* notification */ "message received" = "Nachricht empfangen"; +/* No comment provided by engineer. */ +"Message reception" = "Nachrichtenempfang"; + +/* No comment provided by engineer. */ +"Message servers" = "Nachrichten-Server"; + +/* No comment provided by engineer. */ +"Message shape" = "Nachrichten-Form"; + +/* No comment provided by engineer. */ +"Message source remains private." = "Die Nachrichtenquelle bleibt privat."; + +/* No comment provided by engineer. */ +"Message status" = "Nachrichten-Status"; + +/* copied message info */ +"Message status: %@" = "Nachrichten-Status: %@"; + /* No comment provided by engineer. */ "Message text" = "Nachrichtentext"; +/* No comment provided by engineer. */ +"Message too large" = "Die Nachricht ist zu lang"; + /* No comment provided by engineer. */ "Messages" = "Nachrichten"; @@ -2262,9 +3261,48 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Die Nachrichten von %@ werden angezeigt!"; +/* alert message */ +"Messages in this chat will never be deleted." = "Nachrichten in diesem Chat werden nie gelöscht."; + +/* No comment provided by engineer. */ +"Messages received" = "Empfangene Nachrichten"; + +/* No comment provided by engineer. */ +"Messages sent" = "Gesendete Nachrichten"; + +/* alert message */ +"Messages were deleted after you selected them." = "Die Nachrichten wurden gelöscht, nachdem Sie sie ausgewählt hatten."; + +/* 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." = "Nachrichten, Dateien und Anrufe sind durch **Ende-zu-Ende-Verschlüsselung** mit Perfect Forward Secrecy, Abstreitbarkeit und Wiederherstellung nach einer Kompromittierung geschützt."; + +/* 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." = "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"; + +/* No comment provided by engineer. */ +"Migrate from another device" = "Von einem anderen Gerät migrieren"; + +/* No comment provided by engineer. */ +"Migrate here" = "Hierher migrieren"; + +/* No comment provided by engineer. */ +"Migrate to another device" = "Auf ein anderes Gerät migrieren"; + +/* No comment provided by engineer. */ +"Migrate to another device via QR code." = "Daten können über einen QR-Code auf ein anderes Gerät migriert werden."; + +/* No comment provided by engineer. */ +"Migrating" = "Migrieren"; + /* No comment provided by engineer. */ "Migrating database archive…" = "Datenbank-Archiv wird migriert…"; +/* No comment provided by engineer. */ +"Migration complete" = "Migration abgeschlossen"; + /* No comment provided by engineer. */ "Migration error:" = "Fehler bei der Migration:"; @@ -2275,7 +3313,7 @@ "Migration is completed" = "Die Migration wurde abgeschlossen"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migrationen: %@"; +"Migrations:" = "Migrationen:"; /* time unit */ "minutes" = "Minuten"; @@ -2295,63 +3333,102 @@ /* copied message info */ "Moderated at: %@" = "Moderiert um: %@"; -/* No comment provided by engineer. */ +/* marked deleted chat item preview text */ "moderated by %@" = "Von %@ moderiert"; +/* member role */ +"moderator" = "Moderator"; + /* time unit */ "months" = "Monate"; +/* swipe action */ +"More" = "Mehr"; + /* No comment provided by engineer. */ "More improvements are coming soon!" = "Weitere Verbesserungen sind bald verfügbar!"; +/* No comment provided by engineer. */ +"More reliable network connection." = "Zuverlässigere Netzwerkverbindung."; + +/* No comment provided by engineer. */ +"More reliable notifications" = "Zuverlässigere Benachrichtigungen"; + /* item status description */ "Most likely this connection is deleted." = "Wahrscheinlich ist diese Verbindung gelöscht worden."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Dieser Kontakt hat sehr wahrscheinlich die Verbindung mit Ihnen gelöscht."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Mehrere Chat-Profile"; -/* No comment provided by engineer. */ +/* notification label action */ "Mute" = "Stummschalten"; +/* notification label action */ +"Mute all" = "Alle stummschalten"; + /* No comment provided by engineer. */ "Muted when inactive!" = "Bei Inaktivität stummgeschaltet!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Name"; /* No comment provided by engineer. */ "Network & servers" = "Netzwerk & Server"; +/* No comment provided by engineer. */ +"Network connection" = "Netzwerkverbindung"; + +/* No comment provided by engineer. */ +"Network decentralization" = "Dezentralisiertes Netzwerk"; + +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "Netzwerk-Fehler - die Nachricht ist nach vielen Sende-Versuchen abgelaufen."; + +/* No comment provided by engineer. */ +"Network management" = "Netzwerk-Verwaltung"; + +/* No comment provided by engineer. */ +"Network operator" = "Netzwerk-Betreiber"; + /* No comment provided by engineer. */ "Network settings" = "Netzwerkeinstellungen"; /* No comment provided by engineer. */ "Network status" = "Netzwerkstatus"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "nie"; +/* token status text */ +"New" = "Neu"; + +/* No comment provided by engineer. */ +"New chat" = "Neuer Chat"; + +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Neue Chat-Erfahrung 🎉"; + /* notification */ "New contact request" = "Neue Kontaktanfrage"; /* notification */ "New contact:" = "Neuer Kontakt:"; -/* No comment provided by engineer. */ -"New database archive" = "Neues Datenbankarchiv"; - /* No comment provided by engineer. */ "New desktop app!" = "Neue Desktop-App!"; /* No comment provided by engineer. */ "New display name" = "Neuer Anzeigename"; +/* notification */ +"New events" = "Neue Ereignisse"; + /* No comment provided by engineer. */ "New in %@" = "Neu in %@"; +/* No comment provided by engineer. */ +"New media options" = "Neue Medien-Optionen"; + /* No comment provided by engineer. */ "New member role" = "Neue Mitgliedsrolle"; @@ -2367,6 +3444,15 @@ /* No comment provided by engineer. */ "New passphrase…" = "Neues Passwort…"; +/* No comment provided by engineer. */ +"New server" = "Neuer Server"; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used every time you start the app." = "Jedes Mal wenn Sie die App starten, werden neue SOCKS-Anmeldeinformationen genutzt"; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used for each server." = "Für jeden Server werden neue SOCKS-Anmeldeinformationen genutzt"; + /* pref value */ "no" = "Nein"; @@ -2376,6 +3462,15 @@ /* Authentication unavailable */ "No app password" = "Kein App-Passwort"; +/* No comment provided by engineer. */ +"No chats" = "Keine Chats"; + +/* No comment provided by engineer. */ +"No chats found" = "Keine Chats gefunden"; + +/* No comment provided by engineer. */ +"No chats in list %@" = "Keine Chats in der Liste %@"; + /* No comment provided by engineer. */ "No contacts selected" = "Keine Kontakte ausgewählt"; @@ -2388,6 +3483,9 @@ /* No comment provided by engineer. */ "No device token!" = "Kein Geräte-Token!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Bisher keine direkte Verbindung. Nachricht wird von einem Admin weitergeleitet."; + /* No comment provided by engineer. */ "no e2e encryption" = "Keine E2E-Verschlüsselung"; @@ -2398,26 +3496,89 @@ "No group!" = "Die Gruppe wurde nicht gefunden!"; /* No comment provided by engineer. */ -"No history" = "Keine Vergangenheit"; +"No history" = "Kein Nachrichtenverlauf"; + +/* No comment provided by engineer. */ +"No info, try to reload" = "Keine Information - es wird versucht neu zu laden"; + +/* servers error */ +"No media & file servers." = "Keine Medien- und Dateiserver."; + +/* No comment provided by engineer. */ +"No message" = "Keine Nachricht"; + +/* servers error */ +"No message servers." = "Keine Nachrichten-Server."; + +/* No comment provided by engineer. */ +"No network connection" = "Keine Netzwerkverbindung"; + +/* No comment provided by engineer. */ +"No permission to record speech" = "Keine Genehmigung für Sprach-Aufnahmen"; + +/* No comment provided by engineer. */ +"No permission to record video" = "Keine Genehmigung für Video-Aufnahmen"; /* No comment provided by engineer. */ "No permission to record voice message" = "Keine Berechtigung für das Aufnehmen von Sprachnachrichten"; /* No comment provided by engineer. */ -"No received or sent files" = "Keine empfangenen oder gesendeten Dateien"; +"No push server" = "Lokal"; + +/* No comment provided by engineer. */ +"No received or sent files" = "Keine herunter- oder hochgeladenen Dateien"; + +/* servers error */ +"No servers for private message routing." = "Keine Server für privates Nachrichten-Routing."; + +/* servers error */ +"No servers to receive files." = "Keine Server für das Herunterladen von Dateien."; + +/* servers error */ +"No servers to receive messages." = "Keine Server für den Empfang von Nachrichten."; + +/* servers error */ +"No servers to send files." = "Keine Server für das Versenden von Dateien."; /* copied message info in history */ "no text" = "Kein Text"; +/* alert title */ +"No token!" = "Kein Token!"; + +/* No comment provided by engineer. */ +"No unread chats" = "Keine ungelesenen Chats"; + +/* No comment provided by engineer. */ +"No user identifiers." = "Keine Benutzerkennungen."; + /* No comment provided by engineer. */ "Not compatible!" = "Nicht kompatibel!"; +/* No comment provided by engineer. */ +"Notes" = "Anmerkungen"; + +/* No comment provided by engineer. */ +"Nothing selected" = "Nichts ausgewählt"; + +/* alert title */ +"Nothing to forward!" = "Es gibt nichts zum Weiterleiten!"; + /* No comment provided by engineer. */ "Notifications" = "Benachrichtigungen"; /* No comment provided by engineer. */ "Notifications are disabled!" = "Benachrichtigungen sind deaktiviert!"; +/* alert title */ +"Notifications error" = "Benachrichtigungs-Fehler"; + +/* No comment provided by engineer. */ +"Notifications privacy" = "Datenschutz für Benachrichtigungen"; + +/* alert title */ +"Notifications status" = "Benachrichtigungs-Status"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Administratoren können nun\n- Nachrichten von Gruppenmitgliedern löschen\n- Gruppenmitglieder deaktivieren (\"Beobachter\"-Rolle)"; @@ -2425,30 +3586,27 @@ "observer" = "Beobachter"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "Aus"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Aus"; -/* No comment provided by engineer. */ -"Off (Local)" = "Aus (Lokal)"; - /* feature offered item */ "offered %@" = "angeboten %@"; /* feature offered item */ "offered %@: %@" = "angeboten %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Ok"; /* No comment provided by engineer. */ -"Old database" = "Alte Datenbank"; +"OK" = "OK"; /* No comment provided by engineer. */ -"Old database archive" = "Altes Datenbankarchiv"; +"Old database" = "Alte Datenbank"; /* group pref value */ "on" = "Ein"; @@ -2457,16 +3615,22 @@ "One-time invitation link" = "Einmal-Einladungslink"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Für die Verbindung werden Onion-Hosts benötigt. Dies erfordert die Aktivierung eines VPNs."; +"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."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Onion-Hosts werden verwendet, sobald sie verfügbar sind. Dies erfordert die Aktivierung eines VPNs."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Wenn Onion-Hosts verfügbar sind, werden sie verwendet.\nDies erfordert die Aktivierung eines VPNs."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion-Hosts werden nicht verwendet."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Nur die Endgeräte speichern die Benutzerprofile, Kontakte, Gruppen und Nachrichten, welche über eine **2-Schichten Ende-zu-Ende-Verschlüsselung** gesendet werden."; +"Only chat owners can change preferences." = "Nur Chat-Eigentümer können die Präferenzen ändern."; + +/* 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."; + +/* No comment provided by engineer. */ +"Only delete conversation" = "Nur die Chat-Inhalte löschen"; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Gruppen-Präferenzen können nur von Gruppen-Eigentümern geändert werden."; @@ -2477,11 +3641,17 @@ /* No comment provided by engineer. */ "Only group owners can enable voice messages." = "Sprachnachrichten können nur von Gruppen-Eigentümern aktiviert werden."; +/* No comment provided by engineer. */ +"Only sender and moderators see it" = "Nur Absender und Moderatoren sehen es"; + +/* No comment provided by engineer. */ +"Only you and moderators see it" = "Nur Sie und Moderatoren sehen es"; + /* No comment provided by engineer. */ "Only you can add message reactions." = "Nur Sie können Reaktionen auf Nachrichten geben."; /* No comment provided by engineer. */ -"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "Nur Sie können Nachrichten unwiederbringlich löschen (Ihr Kontakt kann sie zum Löschen markieren)."; +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Nur Sie können Nachrichten unwiederbringlich löschen (Ihr Kontakt kann sie zum Löschen markieren). (24 Stunden)"; /* No comment provided by engineer. */ "Only you can make calls." = "Nur Sie können Anrufe tätigen."; @@ -2496,7 +3666,7 @@ "Only your contact can add message reactions." = "Nur Ihr Kontakt kann Reaktionen auf Nachrichten geben."; /* No comment provided by engineer. */ -"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Nur Ihr Kontakt kann Nachrichten unwiederbringlich löschen (Sie können sie zum Löschen markieren)."; +"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Nur Ihr Kontakt kann Nachrichten unwiederbringlich löschen (Sie können sie zum Löschen markieren). (24 Stunden)"; /* No comment provided by engineer. */ "Only your contact can make calls." = "Nur Ihr Kontakt kann Anrufe tätigen."; @@ -2507,33 +3677,78 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Nur Ihr Kontakt kann Sprachnachrichten versenden."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Öffnen"; +/* No comment provided by engineer. */ +"Open changes" = "Änderungen öffnen"; + /* No comment provided by engineer. */ "Open chat" = "Chat öffnen"; /* authentication reason */ "Open chat console" = "Chat-Konsole öffnen"; +/* No comment provided by engineer. */ +"Open conditions" = "Nutzungsbedingungen öffnen"; + /* No comment provided by engineer. */ "Open group" = "Gruppe öffnen"; +/* authentication reason */ +"Open migration to another device" = "Migration auf ein anderes Gerät öffnen"; + /* No comment provided by engineer. */ "Open Settings" = "Geräte-Einstellungen öffnen"; -/* authentication reason */ -"Open user profiles" = "Benutzerprofile öffnen"; +/* No comment provided by engineer. */ +"Opening app…" = "App wird geöffnet…"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Open-Source-Protokoll und -Code – Jede Person kann ihre eigenen Server aufsetzen und nutzen."; +"Operator" = "Betreiber"; + +/* alert title */ +"Operator server" = "Betreiber-Server"; /* No comment provided by engineer. */ -"Opening database…" = "Öffne Datenbank …"; +"Or import archive file" = "Oder importieren Sie eine Archiv-Datei"; + +/* No comment provided by engineer. */ +"Or paste archive link" = "Oder fügen Sie den Archiv-Link ein"; + +/* No comment provided by engineer. */ +"Or scan QR code" = "Oder den QR-Code scannen"; + +/* 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 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. */ +"Organize chats into lists" = "Chats in Listen verwalten"; + +/* No comment provided by engineer. */ +"other" = "Andere"; + +/* No comment provided by engineer. */ +"Other" = "Andere"; + +/* No comment provided by engineer. */ +"other errors" = "Andere Fehler"; + +/* alert message */ +"Other file errors:\n%@" = "Andere(r) Datei-Fehler:\n%@"; /* member role */ "owner" = "Eigentümer"; +/* feature role */ +"owners" = "Eigentümer"; + /* No comment provided by engineer. */ "Passcode" = "Zugangscode"; @@ -2550,10 +3765,13 @@ "Passcode set!" = "Zugangscode eingestellt!"; /* No comment provided by engineer. */ -"Password to show" = "Passwort anzeigen"; +"Password" = "Passwort"; /* No comment provided by engineer. */ -"Paste" = "Einfügen"; +"Password to show" = "Passwort anzeigen"; + +/* past/unknown group member */ +"Past member %@" = "Ehemaliges Mitglied %@"; /* No comment provided by engineer. */ "Paste desktop address" = "Desktop-Adresse einfügen"; @@ -2562,32 +3780,50 @@ "Paste image" = "Bild einfügen"; /* No comment provided by engineer. */ -"Paste received link" = "Fügen Sie den erhaltenen Link ein"; +"Paste link to connect!" = "Zum Verbinden den Link einfügen!"; -/* placeholder */ -"Paste the link you received to connect with your contact." = "Um sich mit Ihrem Kontakt zu verbinden, fügen Sie den erhaltenen Link in das Feld unten ein."; +/* No comment provided by engineer. */ +"Paste the link you received" = "Fügen Sie den erhaltenen Link ein"; /* No comment provided by engineer. */ "peer-to-peer" = "Peer-to-Peer"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Verbindungen mit Kontakten sind nur über Links möglich, die Sie oder Ihre Kontakte untereinander teilen."; +"pending" = "ausstehend"; /* No comment provided by engineer. */ -"Periodically" = "Periodisch"; +"Pending" = "Ausstehend"; + +/* No comment provided by engineer. */ +"pending approval" = "ausstehende Genehmigung"; + +/* No comment provided by engineer. */ +"Periodic" = "Periodisch"; /* message decrypt error item */ "Permanent decryption error" = "Entschlüsselungsfehler"; +/* No comment provided by engineer. */ +"Picture-in-picture calls" = "Bild-in-Bild-Anrufe"; + /* No comment provided by engineer. */ "PING count" = "PING-Zähler"; /* No comment provided by engineer. */ "PING interval" = "PING-Intervall"; +/* No comment provided by engineer. */ +"Play from the chat list." = "Direkt aus der Chat-Liste abspielen."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Bitten Sie Ihren Kontakt darum, Anrufe zu aktivieren."; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Bitten Sie Ihren Kontakt darum, das Senden von Sprachnachrichten zu aktivieren."; +/* No comment provided by engineer. */ +"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." = "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."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Überprüfen Sie bitte, ob Sie den richtigen Link genutzt haben oder bitten Sie Ihren Kontakt nochmal darum, Ihnen einen Link zuzusenden."; @@ -2597,6 +3833,12 @@ /* No comment provided by engineer. */ "Please check yours and your contact preferences." = "Bitte überprüfen sie sowohl Ihre, als auch die Präferenzen Ihres Kontakts."; +/* No comment provided by engineer. */ +"Please confirm that network settings are correct for this device." = "Bitte bestätigen Sie, dass die Netzwerkeinstellungen auf diesem Gerät richtig sind."; + +/* No comment provided by engineer. */ +"Please contact developers.\nError: %@" = "Bitte nehmen Sie Kontakt mit den Entwicklern auf.\nFehler: %@"; + /* No comment provided by engineer. */ "Please contact group admin." = "Bitte kontaktieren Sie den Gruppen-Administrator."; @@ -2621,9 +3863,21 @@ /* No comment provided by engineer. */ "Please store passphrase securely, you will NOT be able to change it if you lose it." = "Bitte bewahren Sie das Passwort sicher auf, Sie können es NICHT mehr ändern, wenn Sie es vergessen haben oder verlieren."; +/* token info */ +"Please try to disable and re-enable notfications." = "Bitte versuchen Sie, die Benachrichtigungen zu deaktivieren und wieder zu aktivieren."; + +/* token info */ +"Please wait for token activation to complete." = "Bitte warten Sie, bis die Token-Aktivierung abgeschlossen ist."; + +/* token info */ +"Please wait for token to be registered." = "Bitte warten Sie auf die Registrierung des Tokens."; + /* No comment provided by engineer. */ "Polish interface" = "Polnische Bedienoberfläche"; +/* No comment provided by engineer. */ +"Port" = "Port"; + /* server test error */ "Possibly, certificate fingerprint in server address is incorrect" = "Der Fingerabdruck des Zertifikats in der Serveradresse ist wahrscheinlich ungültig"; @@ -2631,23 +3885,53 @@ "Preserve the last message draft, with attachments." = "Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren."; /* No comment provided by engineer. */ -"Preset server" = "Voreingestellter Server"; +"Preset server address" = "Voreingestellte Serveradresse"; /* No comment provided by engineer. */ -"Preset server address" = "Voreingestellte Serveradresse"; +"Preset servers" = "Voreingestellte Server"; /* No comment provided by engineer. */ "Preview" = "Vorschau"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Bisher verbundene Server"; + /* No comment provided by engineer. */ "Privacy & security" = "Datenschutz & Sicherheit"; +/* No comment provided by engineer. */ +"Privacy for your customers." = "Schutz der Privatsphäre Ihrer Kunden."; + +/* No comment provided by engineer. */ +"Privacy policy and conditions of use." = "Datenschutz- und Nutzungsbedingungen."; + /* No comment provided by engineer. */ "Privacy redefined" = "Datenschutz neu definiert"; +/* 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."; + /* No comment provided by engineer. */ "Private filenames" = "Neutrale Dateinamen"; +/* No comment provided by engineer. */ +"Private media file names." = "Medien mit anonymisierten Dateinamen."; + +/* No comment provided by engineer. */ +"Private message routing" = "Privates Nachrichten-Routing"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Privates Nachrichten-Routing 🚀"; + +/* name of notes to self */ +"Private notes" = "Private Notizen"; + +/* No comment provided by engineer. */ +"Private routing" = "Privates Routing"; + +/* No comment provided by engineer. */ +"Private routing error" = "Fehler beim privaten Routing"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profil und Serververbindungen"; @@ -2655,15 +3939,15 @@ "Profile image" = "Profilbild"; /* No comment provided by engineer. */ -"Profile name" = "Profilname"; - -/* No comment provided by engineer. */ -"Profile name:" = "Profilname:"; +"Profile images" = "Profil-Bilder"; /* No comment provided by engineer. */ "Profile password" = "Passwort für Profil"; /* No comment provided by engineer. */ +"Profile theme" = "Profil-Design"; + +/* alert message */ "Profile update will be sent to your contacts." = "Profil-Aktualisierung wird an Ihre Kontakte gesendet."; /* No comment provided by engineer. */ @@ -2678,47 +3962,83 @@ /* No comment provided by engineer. */ "Prohibit messages reactions." = "Reaktionen auf Nachrichten nicht erlauben."; +/* No comment provided by engineer. */ +"Prohibit reporting messages to moderators." = "Melden von Nachrichten an Moderatoren nicht erlauben."; + /* 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 disappearing messages." = "Das Senden von verschwindenden Nachrichten verbieten."; +"Prohibit sending disappearing messages." = "Das Senden von verschwindenden Nachrichten nicht erlauben."; /* No comment provided by engineer. */ "Prohibit sending files and media." = "Das Senden von Dateien und Medien nicht erlauben."; +/* No comment provided by engineer. */ +"Prohibit sending SimpleX links." = "Das Senden von SimpleX-Links nicht erlauben."; + /* No comment provided by engineer. */ "Prohibit sending voice messages." = "Das Senden von Sprachnachrichten nicht erlauben."; /* No comment provided by engineer. */ "Protect app screen" = "App-Bildschirm schützen"; +/* No comment provided by engineer. */ +"Protect IP address" = "IP-Adresse schützen"; + /* No comment provided by engineer. */ "Protect your chat profiles with a password!" = "Ihre Chat-Profile mit einem Passwort schützen!"; +/* No comment provided by engineer. */ +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Schützen Sie Ihre IP-Adresse vor den Nachrichten-Relais, die Ihre Kontakte ausgewählt haben.\nAktivieren Sie es in den *Netzwerk & Server* Einstellungen."; + /* No comment provided by engineer. */ "Protocol timeout" = "Protokollzeitüberschreitung"; /* No comment provided by engineer. */ "Protocol timeout per KB" = "Protokollzeitüberschreitung pro kB"; +/* No comment provided by engineer. */ +"Proxied" = "Proxied"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Proxy-Server"; + +/* No comment provided by engineer. */ +"Proxy requires password" = "Der Proxy benötigt ein Passwort"; + /* No comment provided by engineer. */ "Push notifications" = "Push-Benachrichtigungen"; +/* No comment provided by engineer. */ +"Push server" = "Push-Server"; + +/* chat item text */ +"quantum resistant e2e encryption" = "Quantum-resistente E2E-Verschlüsselung"; + +/* No comment provided by engineer. */ +"Quantum resistant encryption" = "Quantum-resistente Verschlüsselung"; + /* No comment provided by engineer. */ "Rate the app" = "Bewerten Sie die App"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Chat-Symbolleiste unten"; + /* chat item menu */ "React…" = "Reagiere…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Gelesen"; /* No comment provided by engineer. */ "Read more" = "Mehr erfahren"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address) lesen."; +"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)."; + +/* 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."; @@ -2727,10 +4047,10 @@ "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."; /* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Erfahren Sie in unserem GitHub-Repository mehr dazu."; +"Receipts are disabled" = "Bestätigungen sind deaktiviert"; /* No comment provided by engineer. */ -"Receipts are disabled" = "Bestätigungen sind deaktiviert"; +"Receive errors" = "Fehler beim Empfang"; /* No comment provided by engineer. */ "received answer…" = "Antwort erhalten…"; @@ -2750,21 +4070,51 @@ /* message info title */ "Received message" = "Empfangene Nachricht"; +/* No comment provided by engineer. */ +"Received messages" = "Empfangene Nachrichten"; + +/* No comment provided by engineer. */ +"Received reply" = "Empfangene Antwort"; + +/* No comment provided by engineer. */ +"Received total" = "Summe aller empfangenen Nachrichten"; + /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Die Empfängeradresse wird auf einen anderen Server geändert. Der Adresswechsel wird abgeschlossen, wenn der Absender wieder online ist."; /* No comment provided by engineer. */ -"Receiving file will be stopped." = "Der Empfang der Datei wird beendet."; +"Receiving file will be stopped." = "Das Herunterladen der Datei wird beendet."; /* No comment provided by engineer. */ "Receiving via" = "Empfangen über"; +/* No comment provided by engineer. */ +"Recent history and improved [directory bot](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)." = "Aktueller Nachrichtenverlauf und verbesserter [Gruppenverzeichnis-Bot](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)."; + +/* No comment provided by engineer. */ +"Recipient(s) can't see who this message is from." = "Empfänger können nicht sehen, von wem die Nachricht stammt."; + /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Die Empfänger sehen Nachrichtenaktualisierungen, während Sie sie eingeben."; +/* No comment provided by engineer. */ +"Reconnect" = "Neu verbinden"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Alle verbundenen Server werden neu verbunden, um die Zustellung der Nachricht zu erzwingen. Dies verursacht zusätzlichen Datenverkehr."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Alle Server neu verbinden"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Alle Server neu verbinden?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Um die Auslieferung von Nachrichten zu erzwingen, wird der Server neu verbunden. Dafür wird weiterer Datenverkehr benötigt."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Server neu verbinden?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Die Server neu verbinden?"; @@ -2777,7 +4127,17 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Reduzierter Batterieverbrauch"; -/* reject incoming call via notification */ +/* No comment provided by engineer. */ +"Register" = "Registrieren"; + +/* token info */ +"Register notification token?" = "Benachrichtigungs-Token registrieren?"; + +/* token status text */ +"Registered" = "Registriert"; + +/* reject incoming call via notification +swipe action */ "Reject" = "Ablehnen"; /* No comment provided by engineer. */ @@ -2786,6 +4146,9 @@ /* No comment provided by engineer. */ "Reject contact request" = "Kontaktanfrage ablehnen"; +/* No comment provided by engineer. */ +"rejected" = "abgelehnt"; + /* call status */ "rejected call" = "Abgelehnter Anruf"; @@ -2798,6 +4161,12 @@ /* No comment provided by engineer. */ "Remove" = "Entfernen"; +/* No comment provided by engineer. */ +"Remove archive?" = "Archiv entfernen?"; + +/* No comment provided by engineer. */ +"Remove image" = "Bild entfernen"; + /* No comment provided by engineer. */ "Remove member" = "Mitglied entfernen"; @@ -2813,6 +4182,12 @@ /* rcv group event chat item */ "removed %@" = "hat %@ aus der Gruppe entfernt"; +/* profile update event chat item */ +"removed contact address" = "Die Kontaktadresse wurde entfernt"; + +/* profile update event chat item */ +"removed profile picture" = "Das Profil-Bild wurde entfernt"; + /* rcv group event chat item */ "removed you" = "hat Sie aus der Gruppe entfernt"; @@ -2828,24 +4203,81 @@ /* No comment provided by engineer. */ "Repeat connection request?" = "Verbindungsanfrage wiederholen?"; +/* No comment provided by engineer. */ +"Repeat download" = "Herunterladen wiederholen"; + +/* No comment provided by engineer. */ +"Repeat import" = "Import wiederholen"; + /* No comment provided by engineer. */ "Repeat join request?" = "Verbindungsanfrage wiederholen?"; +/* No comment provided by engineer. */ +"Repeat upload" = "Hochladen wiederholen"; + /* chat item action */ "Reply" = "Antwort"; +/* chat item action */ +"Report" = "Melden"; + +/* report reason */ +"Report content: only group moderators will see it." = "Inhalt melden: Nur Gruppenmoderatoren werden es sehen."; + +/* report reason */ +"Report member profile: only group moderators will see it." = "Mitgliederprofil melden: Nur Gruppenmoderatoren werden es sehen."; + +/* report reason */ +"Report other: only group moderators will see it." = "Anderes melden: Nur Gruppenmoderatoren werden es sehen."; + +/* No comment provided by engineer. */ +"Report reason?" = "Grund der Meldung?"; + +/* report reason */ +"Report spam: only group moderators will see it." = "Spam melden: Nur Gruppenmoderatoren werden es sehen."; + +/* report reason */ +"Report violation: only group moderators will see it." = "Verstoß melden: Nur Gruppenmoderatoren werden es sehen."; + +/* report in notification */ +"Report: %@" = "Meldung: %@"; + +/* No comment provided by engineer. */ +"Reporting messages to moderators is prohibited." = "Melden von Nachrichten an Moderatoren ist nicht erlaubt."; + +/* No comment provided by engineer. */ +"Reports" = "Meldungen"; + +/* chat list item title */ +"requested to connect" = "Zur Verbindung aufgefordert"; + /* No comment provided by engineer. */ "Required" = "Erforderlich"; /* No comment provided by engineer. */ "Reset" = "Zurücksetzen"; +/* No comment provided by engineer. */ +"Reset all hints" = "Alle Hinweise zurücksetzen"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Alle Statistiken zurücksetzen"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Alle Statistiken zurücksetzen?"; + /* No comment provided by engineer. */ "Reset colors" = "Farben zurücksetzen"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Auf das App-Design zurücksetzen"; + /* No comment provided by engineer. */ "Reset to defaults" = "Auf Voreinstellungen zurücksetzen"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Auf das Benutzer-spezifische Design zurücksetzen"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Um ein neues Chat-Profil zu erstellen, starten Sie die App neu"; @@ -2864,11 +4296,14 @@ /* No comment provided by engineer. */ "Restore database error" = "Fehler bei der Wiederherstellung der Datenbank"; +/* No comment provided by engineer. */ +"Retry" = "Wiederholen"; + /* chat item action */ "Reveal" = "Aufdecken"; /* No comment provided by engineer. */ -"Revert" = "Zurückkehren"; +"Review conditions" = "Nutzungsbedingungen einsehen"; /* No comment provided by engineer. */ "Revoke" = "Widerrufen"; @@ -2885,37 +4320,44 @@ /* No comment provided by engineer. */ "Run chat" = "Chat starten"; -/* chat item action */ +/* No comment provided by engineer. */ +"Safely receive files" = "Dateien sicher herunterladen"; + +/* No comment provided by engineer. */ +"Safer groups" = "Sicherere Gruppen"; + +/* alert button +chat item action */ "Save" = "Speichern"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Speichern (und Kontakte benachrichtigen)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Speichern und Kontakt benachrichtigen"; /* No comment provided by engineer. */ "Save and notify group members" = "Speichern und Gruppenmitglieder 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 archive" = "Archiv speichern"; - -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Einstellungen von \"Automatisch akzeptieren\" speichern"; - /* No comment provided by engineer. */ "Save group profile" = "Gruppenprofil speichern"; +/* No comment provided by engineer. */ +"Save list" = "Liste speichern"; + /* No comment provided by engineer. */ "Save passphrase and open chat" = "Passwort speichern und Chat öffnen"; /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Passwort im Schlüsselbund speichern"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Präferenzen speichern?"; /* No comment provided by engineer. */ @@ -2924,18 +4366,42 @@ /* No comment provided by engineer. */ "Save servers" = "Alle Server speichern"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Alle Server speichern?"; -/* No comment provided by engineer. */ -"Save settings?" = "Einstellungen speichern?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Begrüßungsmeldung speichern?"; +/* alert title */ +"Save your profile?" = "Ihr Profil speichern?"; + +/* No comment provided by engineer. */ +"saved" = "abgespeichert"; + +/* No comment provided by engineer. */ +"Saved" = "Abgespeichert"; + +/* No comment provided by engineer. */ +"Saved from" = "Abgespeichert von"; + +/* No comment provided by engineer. */ +"saved from %@" = "abgespeichert von %@"; + +/* message info title */ +"Saved message" = "Gespeicherte Nachricht"; + /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Gespeicherte WebRTC ICE-Server werden entfernt"; +/* No comment provided by engineer. */ +"Saving %lld messages" = "Es wird/werden %lld Nachricht(en) gesichert"; + +/* No comment provided by engineer. */ +"Scale" = "Skalieren"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Link scannen / einfügen"; + /* No comment provided by engineer. */ "Scan code" = "Code scannen"; @@ -2951,12 +4417,24 @@ /* No comment provided by engineer. */ "Scan server QR code" = "Scannen Sie den QR-Code des Servers"; +/* No comment provided by engineer. */ +"search" = "Suchen"; + /* No comment provided by engineer. */ "Search" = "Suche"; +/* No comment provided by engineer. */ +"Search bar accepts invitation links." = "In der Suchleiste werden nun auch Einladungslinks akzeptiert."; + +/* No comment provided by engineer. */ +"Search or paste SimpleX link" = "Suchen oder SimpleX-Link einfügen"; + /* network option */ "sec" = "sek"; +/* No comment provided by engineer. */ +"Secondary" = "Zweite Farbe"; + /* time unit */ "seconds" = "Sekunden"; @@ -2966,6 +4444,9 @@ /* server test step */ "Secure queue" = "Sichere Warteschlange"; +/* No comment provided by engineer. */ +"Secured" = "Abgesichert"; + /* No comment provided by engineer. */ "Security assessment" = "Sicherheits-Gutachten"; @@ -2975,9 +4456,18 @@ /* chat item text */ "security code changed" = "Sicherheitscode wurde geändert"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Auswählen"; +/* No comment provided by engineer. */ +"Select chat profile" = "Chat-Profil auswählen"; + +/* No comment provided by engineer. */ +"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."; + /* No comment provided by engineer. */ "Self-destruct" = "Selbstzerstörung"; @@ -3002,26 +4492,35 @@ /* No comment provided by engineer. */ "send direct message" = "Direktnachricht senden"; -/* No comment provided by engineer. */ -"Send direct message" = "Direktnachricht senden"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Eine Direktnachricht zum Verbinden senden"; /* No comment provided by engineer. */ "Send disappearing message" = "Verschwindende Nachricht senden"; +/* No comment provided by engineer. */ +"Send errors" = "Fehler beim Senden"; + /* No comment provided by engineer. */ "Send link previews" = "Link-Vorschau senden"; /* No comment provided by engineer. */ "Send live message" = "Live Nachricht senden"; +/* No comment provided by engineer. */ +"Send message to enable calls." = "Nachricht senden, um Anrufe zu aktivieren."; + +/* No comment provided by engineer. */ +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Nachrichten werden direkt versendet, wenn die IP-Adresse geschützt ist, und Ihr oder der Zielserver kein privates Routing unterstützt."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Nachrichten werden direkt versendet, wenn Ihr oder der Zielserver kein privates Routing unterstützt."; + /* No comment provided by engineer. */ "Send notifications" = "Benachrichtigungen senden"; /* No comment provided by engineer. */ -"Send notifications:" = "Benachrichtigungen senden:"; +"Send private reports" = "Private Meldungen senden"; /* No comment provided by engineer. */ "Send questions and ideas" = "Senden Sie Fragen und Ideen"; @@ -3033,6 +4532,9 @@ "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."; + +/* alert message */ "Sender cancelled file transfer." = "Der Absender hat die Dateiübertragung abgebrochen."; /* No comment provided by engineer. */ @@ -3068,15 +4570,57 @@ /* copied message info */ "Sent at: %@" = "Gesendet um: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Direkt gesendet"; + /* notification */ "Sent file event" = "Datei-Ereignis wurde gesendet"; /* message info title */ "Sent message" = "Gesendete Nachricht"; +/* No comment provided by engineer. */ +"Sent messages" = "Gesendete Nachrichten"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Gesendete Nachrichten werden nach der eingestellten Zeit gelöscht."; +/* No comment provided by engineer. */ +"Sent reply" = "Gesendete Antwort"; + +/* No comment provided by engineer. */ +"Sent total" = "Summe aller gesendeten Nachrichten"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Über einen Proxy gesendet"; + +/* No comment provided by engineer. */ +"Server" = "Server"; + +/* alert message */ +"Server added to operator %@." = "Der Server wurde dem Betreiber %@ hinzugefügt."; + +/* No comment provided by engineer. */ +"Server address" = "Server-Adresse"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel: %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel."; + +/* alert title */ +"Server operator changed." = "Der Server-Betreiber wurde geändert."; + +/* No comment provided by engineer. */ +"Server operators" = "Server-Betreiber"; + +/* alert title */ +"Server protocol changed." = "Das Server-Protokoll wurde geändert."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "Server-Warteschlangen-Information: %1$@\n\nZuletzt empfangene Nachricht: %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "Um Warteschlangen zu erzeugen benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort"; @@ -3086,27 +4630,60 @@ /* No comment provided by engineer. */ "Server test failed!" = "Server Test ist fehlgeschlagen!"; +/* No comment provided by engineer. */ +"Server type" = "Server-Typ"; + +/* srv error text */ +"Server version is incompatible with network settings." = "Die Server-Version ist nicht mit den Netzwerkeinstellungen kompatibel."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "Die Server-Version ist nicht mit Ihrer App kompatibel: %@."; + /* No comment provided by engineer. */ "Servers" = "Server"; +/* No comment provided by engineer. */ +"Servers info" = "Server-Informationen"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Die Serverstatistiken werden zurückgesetzt. Dies kann nicht rückgängig gemacht werden!"; + /* No comment provided by engineer. */ "Session code" = "Sitzungscode"; /* No comment provided by engineer. */ "Set 1 day" = "Einen Tag festlegen"; +/* No comment provided by engineer. */ +"Set chat name…" = "Chat-Name festlegen…"; + /* No comment provided by engineer. */ "Set contact name…" = "Kontaktname festlegen…"; +/* No comment provided by engineer. */ +"Set default theme" = "Default-Design einstellen"; + /* No comment provided by engineer. */ "Set group preferences" = "Gruppen-Präferenzen einstellen"; /* No comment provided by engineer. */ "Set it instead of system authentication." = "Anstelle der System-Authentifizierung festlegen."; +/* No comment provided by engineer. */ +"Set message expiration in chats." = "Verfallsdatum von Nachrichten in Chats festlegen."; + +/* profile update event chat item */ +"set new contact address" = "Es wurde eine neue Kontaktadresse festgelegt"; + +/* profile update event chat item */ +"set new profile picture" = "Es wurde ein neues Profil-Bild festgelegt"; + /* No comment provided by engineer. */ "Set passcode" = "Zugangscode einstellen"; +/* No comment provided by engineer. */ +"Set passphrase" = "Passwort festlegen"; + /* No comment provided by engineer. */ "Set passphrase to export" = "Passwort für den Export festlegen"; @@ -3119,27 +4696,58 @@ /* No comment provided by engineer. */ "Settings" = "Einstellungen"; -/* chat item action */ +/* alert message */ +"Settings were changed." = "Die Einstellungen wurden geändert."; + +/* No comment provided by engineer. */ +"Shape profile images" = "Form der Profil-Bilder"; + +/* alert action +chat item action */ "Share" = "Teilen"; /* No comment provided by engineer. */ "Share 1-time link" = "Einmal-Link teilen"; +/* No comment provided by engineer. */ +"Share 1-time link with a friend" = "Den Einmal-Einladungslink mit einem Freund teilen"; + /* No comment provided by engineer. */ "Share address" = "Adresse teilen"; /* No comment provided by engineer. */ +"Share address publicly" = "Die Adresse öffentlich teilen"; + +/* alert title */ "Share address with contacts?" = "Die Adresse mit Kontakten teilen?"; +/* No comment provided by engineer. */ +"Share from other apps." = "Aus anderen Apps heraus teilen."; + /* No comment provided by engineer. */ "Share link" = "Link teilen"; /* No comment provided by engineer. */ -"Share one-time invitation link" = "Einmal-Einladungslink teilen"; +"Share profile" = "Profil teilen"; + +/* No comment provided by engineer. */ +"Share SimpleX address on social media." = "Die SimpleX-Adresse auf sozialen Medien teilen."; + +/* No comment provided by engineer. */ +"Share this 1-time invite link" = "Teilen Sie diesen Einmal-Einladungslink"; + +/* No comment provided by engineer. */ +"Share to SimpleX" = "Mit SimpleX teilen"; /* No comment provided by engineer. */ "Share with contacts" = "Mit Kontakten teilen"; +/* No comment provided by engineer. */ +"Short link" = "Verkürzter Link"; + +/* No comment provided by engineer. */ +"Show → on messages sent via private routing." = "Bei Nachrichten, die über privates Routing versendet wurden, → anzeigen."; + /* No comment provided by engineer. */ "Show calls in phone history" = "Anrufliste anzeigen"; @@ -3149,18 +4757,42 @@ /* No comment provided by engineer. */ "Show last messages" = "Letzte Nachrichten anzeigen"; +/* No comment provided by engineer. */ +"Show message status" = "Nachrichtenstatus anzeigen"; + +/* No comment provided by engineer. */ +"Show percentage" = "Prozentualen Anteil anzeigen"; + /* No comment provided by engineer. */ "Show preview" = "Vorschau anzeigen"; +/* No comment provided by engineer. */ +"Show QR code" = "QR-Code anzeigen"; + /* No comment provided by engineer. */ "Show:" = "Anzeigen:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "SimpleX-Adresse"; /* No comment provided by engineer. */ "SimpleX Address" = "SimpleX-Adresse"; +/* No comment provided by engineer. */ +"SimpleX address and 1-time links are safe to share via any messenger." = "Die SimpleX-Adresse und Einmal-Links können sicher über beliebige Messenger geteilt werden."; + +/* No comment provided by engineer. */ +"SimpleX address or 1-time link?" = "SimpleX-Adresse oder Einmal-Link?"; + +/* simplex link type */ +"SimpleX channel link" = "SimpleX-Kanal-Link"; + +/* 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."; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "Die Sicherheit von SimpleX Chat wurde von Trail of Bits überprüft."; @@ -3173,9 +4805,15 @@ /* simplex link type */ "SimpleX group link" = "SimpleX Gruppen-Link"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "SimpleX-Links"; +/* No comment provided by engineer. */ +"SimpleX links are prohibited." = "In dieser Gruppe sind SimpleX-Links nicht erlaubt."; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "SimpleX-Links sind nicht erlaubt"; + /* No comment provided by engineer. */ "SimpleX Lock" = "SimpleX-Sperre"; @@ -3191,9 +4829,15 @@ /* simplex link type */ "SimpleX one-time invitation" = "SimpleX-Einmal-Einladung"; +/* No comment provided by engineer. */ +"SimpleX protocols reviewed by Trail of Bits." = "Die SimpleX-Protokolle wurden von Trail of Bits überprüft."; + /* No comment provided by engineer. */ "Simplified incognito mode" = "Vereinfachter Inkognito-Modus"; +/* No comment provided by engineer. */ +"Size" = "Größe"; + /* No comment provided by engineer. */ "Skip" = "Überspringen"; @@ -3204,31 +4848,65 @@ "Small groups (max 20)" = "Kleine Gruppen (max. 20)"; /* No comment provided by engineer. */ -"SMP servers" = "SMP-Server"; +"SMP server" = "SMP-Server"; + +/* No comment provided by engineer. */ +"SOCKS proxy" = "SOCKS-Proxy"; + +/* blur media */ +"Soft" = "Weich"; + +/* No comment provided by engineer. */ +"Some app settings were not migrated." = "Einige App-Einstellungen wurden nicht migriert."; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "Einzelne Datei(en) wurde(n) nicht exportiert:"; /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Während des Imports sind einige nicht schwerwiegende Fehler aufgetreten - in der Chat-Konsole finden Sie weitere Einzelheiten."; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "Während des Imports traten ein paar nicht schwerwiegende Fehler auf:"; + +/* alert message */ +"Some servers failed the test:\n%@" = "Einige Server haben den Test nicht bestanden:\n%@"; + /* notification title */ "Somebody" = "Jemand"; +/* blocking reason +report reason */ +"Spam" = "Spam"; + /* No comment provided by engineer. */ -"Start a new chat" = "Starten Sie einen neuen Chat"; +"Square, circle, or anything in between." = "Quadratisch, kreisförmig oder irgendetwas dazwischen."; + +/* chat item text */ +"standard end-to-end encryption" = "Standard-Ende-zu-Ende-Verschlüsselung"; /* No comment provided by engineer. */ "Start chat" = "Starten Sie den Chat"; +/* No comment provided by engineer. */ +"Start chat?" = "Chat starten?"; + /* No comment provided by engineer. */ "Start migration" = "Starten Sie die Migration"; +/* No comment provided by engineer. */ +"Starting from %@." = "Beginnend mit %@."; + /* No comment provided by engineer. */ "starting…" = "Verbindung wird gestartet…"; +/* No comment provided by engineer. */ +"Statistics" = "Statistiken"; + /* No comment provided by engineer. */ "Stop" = "Beenden"; /* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Chat beenden, um Datenbankaktionen zu erlauben"; +"Stop chat" = "Chat beenden"; /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Beenden Sie den Chat, um die Chat-Datenbank zu exportieren, zu importieren oder zu löschen. Solange der Chat angehalten ist, können Sie keine Nachrichten empfangen oder senden."; @@ -3237,38 +4915,65 @@ "Stop chat?" = "Chat beenden?"; /* cancel file action */ -"Stop file" = "Datei beenden"; +"Stop file" = "Herunterladen beenden"; /* No comment provided by engineer. */ -"Stop receiving file?" = "Den Empfang der Datei beenden?"; +"Stop receiving file?" = "Das Herunterladen der Datei beenden?"; /* No comment provided by engineer. */ -"Stop sending file?" = "Das Senden der Datei beenden?"; +"Stop sending file?" = "Das Hochladen der Datei beenden?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Teilen beenden"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Das Teilen der Adresse beenden?"; /* authentication reason */ "Stop SimpleX" = "Stoppen Sie SimpleX"; +/* No comment provided by engineer. */ +"Stopping chat" = "Chat wird beendet"; + +/* No comment provided by engineer. */ +"Storage" = "Ablage"; + /* No comment provided by engineer. */ "strike" = "durchstreichen"; +/* blur media */ +"Strong" = "Hart"; + /* No comment provided by engineer. */ "Submit" = "Bestätigen"; +/* No comment provided by engineer. */ +"Subscribed" = "Abonniert"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Fehler beim Abonnieren"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Nicht beachtete Abonnements"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Unterstützung von SimpleX Chat"; +/* No comment provided by engineer. */ +"Switch audio and video during the call." = "Während des Anrufs zwischen Audio und Video wechseln"; + +/* No comment provided by engineer. */ +"Switch chat profile for 1-time invitations." = "Das Chat-Profil für Einmal-Einladungen wechseln"; + /* No comment provided by engineer. */ "System" = "System"; /* No comment provided by engineer. */ "System authentication" = "System-Authentifizierung"; +/* No comment provided by engineer. */ +"Tail" = "Sprechblasen-Format"; + /* No comment provided by engineer. */ "Take picture" = "Machen Sie ein Foto"; @@ -3276,23 +4981,35 @@ "Tap button " = "Schaltfläche antippen "; /* No comment provided by engineer. */ -"Tap to activate profile." = "Tippen Sie auf das Profil um es zu aktivieren."; +"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."; /* No comment provided by engineer. */ -"Tap to Connect" = "Zum Verbinden antippen"; +"Tap to activate profile." = "Zum Aktivieren des Profils tippen."; + +/* No comment provided by engineer. */ +"Tap to Connect" = "Zum Verbinden tippen"; /* No comment provided by engineer. */ "Tap to join" = "Zum Beitreten tippen"; /* No comment provided by engineer. */ -"Tap to join incognito" = "Tippen, um Inkognito beizutreten"; +"Tap to join incognito" = "Zum Inkognito beitreten tippen"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Tippen, um einen neuen Chat zu starten"; +"Tap to paste link" = "Zum Link einfügen tippen"; + +/* No comment provided by engineer. */ +"Tap to scan" = "Zum Scannen tippen"; + +/* No comment provided by engineer. */ +"TCP connection" = "TCP-Verbindung"; /* No comment provided by engineer. */ "TCP connection timeout" = "Timeout der TCP-Verbindung"; +/* No comment provided by engineer. */ +"TCP port for messaging" = "TCP-Port für Nachrichtenübermittlung"; + /* No comment provided by engineer. */ "TCP_KEEPCNT" = "TCP_KEEPCNT"; @@ -3302,16 +5019,22 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* file error alert title */ +"Temporary file error" = "Temporärer Datei-Fehler"; + /* 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 server" = "Teste Server"; /* No comment provided by engineer. */ "Test servers" = "Teste alle Server"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Tests sind fehlgeschlagen!"; /* No comment provided by engineer. */ @@ -3323,15 +5046,24 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Dank der Nutzer - Tragen Sie per Weblate bei!"; -/* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Die erste Plattform ohne Benutzerkennungen – Privat per Design."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Wenn sie Nachrichten oder Kontaktanfragen empfangen, kann Sie die App benachrichtigen - Um dies zu aktivieren, öffnen Sie bitte die Einstellungen."; +/* 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 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)."; + /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "Die Änderung des Datenbank-Passworts konnte nicht abgeschlossen werden."; +/* 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."; + +/* 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."; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "Die von Ihnen akzeptierte Verbindung wird abgebrochen!"; @@ -3344,6 +5076,9 @@ /* No comment provided by engineer. */ "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"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "Der Hash der vorherigen Nachricht unterscheidet sich."; @@ -3357,13 +5092,22 @@ "The message will be marked as moderated for all members." = "Diese Nachricht wird für alle Mitglieder als moderiert gekennzeichnet."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "Die nächste Generation von privatem Messaging"; +"The messages will be deleted for all members." = "Die Nachrichten werden für alle Gruppenmitglieder gelöscht."; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "Die Nachrichten werden für alle Mitglieder als moderiert gekennzeichnet werden."; /* 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 profile is only shared with your contacts." = "Das Profil wird nur mit Ihren Kontakten geteilt."; +"Your profile is stored on your device and only shared with your contacts." = "Das Profil wird nur mit Ihren Kontakten geteilt."; + +/* No comment provided by engineer. */ +"The same conditions will apply to operator **%@**." = "Dieselben Nutzungsbedingungen gelten auch für den Betreiber **%@**."; + +/* No comment provided by engineer. */ +"The second preset operator in the app!" = "Der zweite voreingestellte Netzwerk-Betreiber in der App!"; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Wir haben das zweite Häkchen vermisst! ✅"; @@ -3372,16 +5116,22 @@ "The sender will NOT be notified" = "Der Absender wird NICHT benachrichtigt"; /* No comment provided by engineer. */ -"The servers for new connections of your current chat profile **%@**." = "Server der neuen Verbindungen von Ihrem aktuellen Chat-Profil **%@**."; +"The servers for new connections of your current chat profile **%@**." = "Nachrichten-Server für neue Verbindungen über Ihr aktuelles Chat-Profil **%@**."; /* No comment provided by engineer. */ -"Theme" = "Design"; +"The servers for new files of your current chat profile **%@**." = "Medien- und Datei-Server für neue Daten über Ihr aktuelles Chat-Profil **%@**."; /* No comment provided by engineer. */ -"There should be at least one user profile." = "Es muss mindestens ein Benutzer-Profil vorhanden sein."; +"The text you pasted is not a SimpleX link." = "Der von Ihnen eingefügte Text ist kein SimpleX-Link."; /* No comment provided by engineer. */ -"There should be at least one visible user profile." = "Es muss mindestens ein sichtbares Benutzer-Profil vorhanden sein."; +"The uploaded database archive will be permanently removed from the servers." = "Das hochgeladene Datenbank-Archiv wird dauerhaft von den Servern entfernt."; + +/* No comment provided by engineer. */ +"Themes" = "Design"; + +/* No comment provided by engineer. */ +"These conditions will also apply for: **%@**." = "Diese Nutzungsbedingungen gelten auch für: **%@**."; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Diese Einstellungen betreffen Ihr aktuelles Profil **%@**."; @@ -3390,13 +5140,22 @@ "They can be overridden in contact and group settings." = "Sie können in den Kontakteinstellungen überschrieben werden."; /* No comment provided by engineer. */ -"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten."; +"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Es werden alle herunter- und hochgeladenen Dateien und Medien gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. Diese Aktion kann nicht rückgängig gemacht werden!"; /* 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." = "Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dieser Vorgang kann mehrere Minuten dauern."; +"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Es werden alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, gelöscht. Dieser Vorgang kann mehrere Minuten dauern. Diese Aktion kann nicht rückgängig gemacht werden!"; + +/* alert message */ +"This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." = "Die älteren als die ausgewählten gesendeten und empfangenen Nachrichten in diesem Chat werden gelöscht. Diese Aktion kann nicht rückgängig gemacht werden!"; /* No comment provided by engineer. */ -"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Diese Aktion kann nicht rückgängig gemacht werden! Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren."; +"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. Diese Aktion kann nicht rückgängig gemacht werden!"; + +/* E2EE info chat item */ +"This chat is protected by end-to-end encryption." = "Dieser Chat ist durch Ende-zu-Ende-Verschlüsselung geschützt."; + +/* E2EE info chat item */ +"This chat is protected by quantum resistant end-to-end encryption." = "Dieser Chat ist durch Quantum-resistente Ende-zu-Ende-Verschlüsselung geschützt."; /* notification title */ "this contact" = "Dieser Kontakt"; @@ -3404,6 +5163,9 @@ /* No comment provided by engineer. */ "This device name" = "Dieser Gerätename"; +/* No comment provided by engineer. */ +"This display name is invalid. Please choose another name." = "Der Anzeigename ist ungültig. Bitte wählen Sie einen anderen Namen."; + /* No comment provided by engineer. */ "This group has over %lld members, delivery receipts are not sent." = "Es werden keine Empfangsbestätigungen gesendet, da diese Gruppe über %lld Mitglieder hat."; @@ -3416,11 +5178,23 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Das ist Ihre eigene SimpleX-Adresse!"; +/* 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."; + +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Dieser Link wurde schon mit einem anderen Mobiltelefon genutzt. Bitte erstellen sie einen neuen Link in der Desktop-App."; + +/* No comment provided by engineer. */ +"This message was deleted or not received yet." = "Diese Nachricht wurde gelöscht oder bisher noch nicht empfangen."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Diese Einstellung gilt für Nachrichten in Ihrem aktuellen Chat-Profil **%@**."; /* No comment provided by engineer. */ -"To ask any questions and to receive updates:" = "Um Fragen zu stellen und Aktualisierungen zu erhalten:"; +"Title" = "Bezeichnung"; + +/* No comment provided by engineer. */ +"To ask any questions and to receive updates:" = "Um Fragen zu stellen und aktuelle Informationen zu erhalten:"; /* No comment provided by engineer. */ "To connect, your contact can scan QR code or use the link in the app." = "Um eine Verbindung herzustellen, kann Ihr Kontakt den QR-Code scannen oder den Link in der App verwenden."; @@ -3432,7 +5206,7 @@ "To make a new connection" = "Um eine Verbindung mit einem neuen Kontakt zu erstellen"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Zum Schutz Ihrer Privatsphäre verwendet SimpleX an Stelle von Benutzerkennungen, die von allen anderen Plattformen verwendet werden, Kennungen für Nachrichtenwarteschlangen, die für jeden Ihrer Kontakte individuell sind."; +"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."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Bild- und Sprachdateinamen enthalten UTC, um Informationen zur Zeitzone zu schützen."; @@ -3440,24 +5214,60 @@ /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Um Ihre Informationen zu schützen, schalten Sie die SimpleX-Sperre ein.\nSie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funktion aktiviert wird."; +/* No comment provided by engineer. */ +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "Zum Schutz Ihrer IP-Adresse, wird für die Nachrichten-Auslieferung privates Routing über Ihre konfigurierten SMP-Server genutzt."; + +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Zum Schutz Ihrer Privatsphäre verwendet SimpleX an Stelle von Benutzerkennungen, die von allen anderen Plattformen verwendet werden, Kennungen für Nachrichtenwarteschlangen, die für jeden Ihrer Kontakte individuell sind."; + +/* No comment provided by engineer. */ +"To receive" = "Für den Empfang"; + +/* No comment provided by engineer. */ +"To record speech please grant permission to use Microphone." = "Bitte erteilen Sie für Sprach-Aufnahmen die Genehmigung das Mikrofon zu nutzen."; + +/* No comment provided by engineer. */ +"To record video please grant permission to use Camera." = "Bitte erteilen Sie für Video-Aufnahmen die Genehmigung die Kamera zu nutzen."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Bitte erlauben Sie die Nutzung des Mikrofons, um Sprachnachrichten aufnehmen zu können."; /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite **Ihre Chat-Profile** ein, um Ihr verborgenes Profil zu sehen."; +/* No comment provided by engineer. */ +"To send" = "Für das Senden"; + /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Um sofortige Push-Benachrichtigungen zu unterstützen, muss die Chat-Datenbank migriert werden."; +/* No comment provided by engineer. */ +"To use the servers of **%@**, accept conditions of use." = "Um die Server von **%@** zu nutzen, müssen Sie dessen Nutzungsbedingungen akzeptieren."; + /* 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."; +/* token status */ +"Token status: %@." = "Token-Status: %@."; + +/* No comment provided by engineer. */ +"Toolbar opacity" = "Deckkraft der Symbolleiste"; + +/* No comment provided by engineer. */ +"Total" = "Summe aller Abonnements"; + /* No comment provided by engineer. */ "Transport isolation" = "Transport-Isolation"; +/* No comment provided by engineer. */ +"Transport sessions" = "Transport-Sitzungen"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Beim Versuch die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: %@)."; @@ -3465,10 +5275,10 @@ "Trying to connect to the server used to receive messages from this contact." = "Versuche die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird."; /* No comment provided by engineer. */ -"Turn off" = "Abschalten"; +"Turkish interface" = "Türkische Bedienoberfläche"; /* No comment provided by engineer. */ -"Turn off notifications?" = "Benachrichtigungen abschalten?"; +"Turn off" = "Abschalten"; /* No comment provided by engineer. */ "Turn on" = "Einschalten"; @@ -3479,19 +5289,28 @@ /* No comment provided by engineer. */ "Unblock" = "Freigeben"; +/* No comment provided by engineer. */ +"Unblock for all" = "Für Alle freigeben"; + /* No comment provided by engineer. */ "Unblock member" = "Mitglied freigeben"; +/* No comment provided by engineer. */ +"Unblock member for all?" = "Mitglied für Alle freigeben?"; + /* No comment provided by engineer. */ "Unblock member?" = "Mitglied freigeben?"; -/* item status description */ -"Unexpected error: %@" = "Unerwarteter Fehler: %@"; +/* rcv group event chat item */ +"unblocked %@" = "%@ wurde freigegeben"; + +/* No comment provided by engineer. */ +"Undelivered messages" = "Nicht ausgelieferte Nachrichten"; /* No comment provided by engineer. */ "Unexpected migration state" = "Unerwarteter Migrationsstatus"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Fav. entf."; /* No comment provided by engineer. */ @@ -3518,6 +5337,15 @@ /* No comment provided by engineer. */ "Unknown error" = "Unbekannter Fehler"; +/* No comment provided by engineer. */ +"unknown servers" = "Unbekannte Relais"; + +/* alert title */ +"Unknown servers!" = "Unbekannte Server!"; + +/* No comment provided by engineer. */ +"unknown status" = "unbekannter Gruppenmitglieds-Status"; + /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Aktivieren Sie den Modus \"Bitte nicht stören\", um Unterbrechungen zu vermeiden, es sei denn, Sie verwenden die iOS Anrufschnittstelle."; @@ -3536,17 +5364,23 @@ /* authentication reason */ "Unlock app" = "App entsperren"; -/* No comment provided by engineer. */ +/* notification label action */ "Unmute" = "Stummschaltung aufheben"; /* No comment provided by engineer. */ +"unprotected" = "Ungeschützt"; + +/* swipe action */ "Unread" = "Ungelesen"; /* No comment provided by engineer. */ -"Update" = "Aktualisieren"; +"Unsupported connection link" = "Verbindungs-Link wird nicht unterstützt"; /* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Einstellung für .onion-Hosts aktualisieren?"; +"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. */ +"Update" = "Aktualisieren"; /* No comment provided by engineer. */ "Update database passphrase" = "Datenbank-Passwort aktualisieren"; @@ -3555,31 +5389,58 @@ "Update network settings?" = "Netzwerkeinstellungen aktualisieren?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Transport-Isolations-Modus aktualisieren?"; +"Update settings?" = "Einstellungen aktualisieren?"; + +/* No comment provided by engineer. */ +"Updated conditions" = "Aktualisierte Nutzungsbedingungen"; /* rcv group event chat item */ "updated group profile" = "Aktualisiertes Gruppenprofil"; +/* profile update event chat item */ +"updated profile" = "Das Profil wurde aktualisiert"; + /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "Die Aktualisierung der Einstellungen wird den Client wieder mit allen Servern verbinden."; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Die Aktualisierung dieser Einstellung wird den Client wieder mit allen Servern verbinden."; +"Upgrade and open chat" = "Aktualisieren und den Chat öffnen"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "Aktualisieren und den Chat öffnen"; +"Upload errors" = "Fehler beim Hochladen"; + +/* No comment provided by engineer. */ +"Upload failed" = "Hochladen fehlgeschlagen"; /* server test step */ "Upload file" = "Datei hochladen"; +/* No comment provided by engineer. */ +"Uploaded" = "Hochgeladen"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Hochgeladene Dateien"; + +/* No comment provided by engineer. */ +"Uploading archive" = "Archiv wird hochgeladen"; + /* No comment provided by engineer. */ "Use .onion hosts" = "Verwende .onion-Hosts"; +/* No comment provided by engineer. */ +"Use %@" = "Verwende %@"; + /* No comment provided by engineer. */ "Use chat" = "Verwenden Sie Chat"; /* No comment provided by engineer. */ -"Use current profile" = "Nutzen Sie das aktuelle Profil"; +"Use current profile" = "Aktuelles Profil nutzen"; + +/* No comment provided by engineer. */ +"Use for files" = "Für Dateien verwenden"; + +/* No comment provided by engineer. */ +"Use for messages" = "Für Nachrichten verwenden"; /* No comment provided by engineer. */ "Use for new connections" = "Für neue Verbindungen nutzen"; @@ -3591,19 +5452,52 @@ "Use iOS call interface" = "iOS Anrufschnittstelle nutzen"; /* No comment provided by engineer. */ -"Use new incognito profile" = "Nutzen Sie das neue Inkognito-Profil"; +"Use new incognito profile" = "Neues Inkognito-Profil nutzen"; + +/* No comment provided by engineer. */ +"Use only local notifications?" = "Nur lokale Benachrichtigungen nutzen?"; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "Sie nutzen privates Routing mit unbekannten Servern, wenn Ihre IP-Adresse nicht geschützt ist."; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "Sie nutzen privates Routing mit unbekannten Servern."; /* No comment provided by engineer. */ "Use server" = "Server nutzen"; +/* No comment provided by engineer. */ +"Use servers" = "Verwende Server"; + +/* No comment provided by engineer. */ +"Use short links (BETA)" = "Kurze Links verwenden (BETA)"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "Verwenden Sie SimpleX-Chat-Server?"; /* No comment provided by engineer. */ -"User profile" = "Benutzerprofil"; +"Use SOCKS proxy" = "SOCKS-Proxy nutzen"; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Für die Nutzung von .onion-Hosts sind kompatible VPN-Anbieter erforderlich."; +"Use TCP port %@ when no port is specified." = "Solange kein Port konfiguriert ist, wird TCP-Port %@ genutzt."; + +/* No comment provided by engineer. */ +"Use TCP port 443 for preset servers only." = "TCP-Port 443 nur für voreingestellte Server verwenden."; + +/* No comment provided by engineer. */ +"Use the app while in the call." = "Die App kann während eines Anrufs genutzt werden."; + +/* No comment provided by engineer. */ +"Use the app with one hand." = "Die App mit einer Hand bedienen."; + +/* No comment provided by engineer. */ +"Use web port" = "Web-Port nutzen"; + +/* No comment provided by engineer. */ +"User selection" = "Benutzer-Auswahl"; + +/* No comment provided by engineer. */ +"Username" = "Benutzername"; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Verwendung von SimpleX-Chat-Servern."; @@ -3626,6 +5520,12 @@ /* No comment provided by engineer. */ "Verify connections" = "Verbindungen überprüfen"; +/* No comment provided by engineer. */ +"Verify database passphrase" = "Überprüfen Sie das Datenbank-Passwort"; + +/* No comment provided by engineer. */ +"Verify passphrase" = "Überprüfen Sie das Passwort"; + /* No comment provided by engineer. */ "Verify security code" = "Sicherheitscode überprüfen"; @@ -3647,6 +5547,9 @@ /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Über ein sicheres quantenbeständiges Protokoll."; +/* No comment provided by engineer. */ +"video" = "Video"; + /* No comment provided by engineer. */ "Video call" = "Videoanruf"; @@ -3654,17 +5557,26 @@ "video call (not e2e encrypted)" = "Videoanruf (nicht E2E verschlüsselt)"; /* No comment provided by engineer. */ -"Video will be received when your contact completes uploading it." = "Das Video wird empfangen, sobald Ihr Kontakt das Hochladen beendet hat."; +"Video will be received when your contact completes uploading it." = "Das Video wird heruntergeladen, sobald Ihr Kontakt das Hochladen beendet hat."; /* No comment provided by engineer. */ -"Video will be received when your contact is online, please wait or check later!" = "Das Video wird empfangen, wenn Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später!"; +"Video will be received when your contact is online, please wait or check later!" = "Das Video wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später!"; /* No comment provided by engineer. */ "Videos and files up to 1gb" = "Videos und Dateien bis zu 1GB"; +/* No comment provided by engineer. */ +"View conditions" = "Nutzungsbedingungen anschauen"; + /* No comment provided by engineer. */ "View security code" = "Schauen Sie sich den Sicherheitscode an"; +/* No comment provided by engineer. */ +"View updated conditions" = "Aktualisierte Nutzungsbedingungen anschauen"; + +/* chat feature */ +"Visible history" = "Sichtbarer Nachrichtenverlauf"; + /* No comment provided by engineer. */ "Voice message…" = "Sprachnachrichten…"; @@ -3675,7 +5587,10 @@ "Voice messages are prohibited in this chat." = "In diesem Chat sind Sprachnachrichten nicht erlaubt."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "In dieser Gruppe sind Sprachnachrichten nicht erlaubt."; +"Voice messages are prohibited." = "In dieser Gruppe sind Sprachnachrichten nicht erlaubt."; + +/* No comment provided by engineer. */ +"Voice messages not allowed" = "Sprachnachrichten sind nicht erlaubt"; /* No comment provided by engineer. */ "Voice messages prohibited!" = "Sprachnachrichten sind nicht erlaubt!"; @@ -3698,9 +5613,18 @@ /* No comment provided by engineer. */ "Waiting for video" = "Auf das Video warten"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Wallpaper-Akzent"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Wallpaper-Hintergrund"; + /* No comment provided by engineer. */ "wants to connect to you!" = "möchte sich mit Ihnen verbinden!"; +/* No comment provided by engineer. */ +"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "Warnung: Das Starten des Chats auf mehreren Geräten wird nicht unterstützt und wird zu Fehlern bei der Nachrichtenübermittlung führen"; + /* No comment provided by engineer. */ "Warning: you may lose some data!" = "Warnung: Sie könnten einige Daten verlieren!"; @@ -3716,6 +5640,9 @@ /* No comment provided by engineer. */ "Welcome message" = "Begrüßungsmeldung"; +/* No comment provided by engineer. */ +"Welcome message is too long" = "Die Begrüßungsmeldung ist zu lang"; + /* No comment provided by engineer. */ "What's new" = "Was ist neu"; @@ -3723,28 +5650,64 @@ "When available" = "Wenn verfügbar"; /* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Wenn Personen eine Verbindung anfordern, können Sie diese annehmen oder ablehnen."; +"When connecting audio and video calls." = "Bei der Verbindung über Audio- und Video-Anrufe."; + +/* No comment provided by engineer. */ +"when IP hidden" = "Wenn die IP-Adresse versteckt ist"; + +/* No comment provided by engineer. */ +"When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "Wenn mehrere Netzwerk-Betreiber aktiviert sind, hat keiner von ihnen Metadaten, um zu erfahren, wer mit wem kommuniziert."; /* 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. */ +"WiFi" = "WiFi"; + +/* No comment provided by engineer. */ +"Will be enabled in direct chats!" = "Wird in direkten Chats automatisch aktiviert!"; + +/* No comment provided by engineer. */ +"Wired ethernet" = "Kabelgebundenes Netzwerk"; + +/* No comment provided by engineer. */ +"With encrypted files and media." = "Mit verschlüsselten Dateien und Medien."; + /* No comment provided by engineer. */ "With optional welcome message." = "Mit optionaler Begrüßungsmeldung."; +/* No comment provided by engineer. */ +"With reduced battery usage." = "Mit reduziertem Akkuverbrauch."; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für Datei-Server sichtbar sein."; + +/* alert message */ +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für diese XFTP-Relais sichtbar sein: %@."; + /* No comment provided by engineer. */ "Wrong database passphrase" = "Falsches Datenbank-Passwort"; +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "Falscher Schlüssel oder unbekannte Verbindung - höchstwahrscheinlich ist diese Verbindung gelöscht worden."; + +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Falscher Schlüssel oder unbekannte Daten-Paketadresse der Datei - höchstwahrscheinlich wurde die Datei gelöscht."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Falsches Passwort!"; /* No comment provided by engineer. */ -"XFTP servers" = "XFTP-Server"; +"XFTP server" = "XFTP-Server"; /* pref value */ "yes" = "Ja"; /* No comment provided by engineer. */ -"You" = "Ihre Daten"; +"you" = "Profil"; + +/* No comment provided by engineer. */ +"You **must not** use the same database on two devices." = "Sie dürfen die selbe Datenbank **nicht** auf zwei Geräten nutzen."; /* No comment provided by engineer. */ "You accepted connection" = "Sie haben die Verbindung akzeptiert"; @@ -3758,6 +5721,9 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "Sie sind bereits mit %@ verbunden."; +/* No comment provided by engineer. */ +"You are already connected with %@." = "Sie sind bereits mit %@ verbunden."; + /* No comment provided by engineer. */ "You are already connecting to %@." = "Sie sind bereits mit %@ verbunden."; @@ -3788,14 +5754,23 @@ /* No comment provided by engineer. */ "You are invited to group" = "Sie sind zu der Gruppe eingeladen"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Sie sind nicht mit diesen Servern verbunden. Zur Auslieferung von Nachrichten an diese Server wird privates Routing genutzt."; + /* No comment provided by engineer. */ "you are observer" = "Sie sind Beobachter"; +/* snd group event chat item */ +"you blocked %@" = "Sie haben %@ blockiert"; + /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Sie können Anrufe ohne Geräte- und App-Authentifizierung vom Sperrbildschirm aus annehmen."; /* No comment provided by engineer. */ -"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "Sie können sich auch verbinden, indem Sie auf den Link klicken. Wenn er im Browser geöffnet wird, klicken Sie auf die Schaltfläche **In mobiler App öffnen**."; +"You can change it in Appearance settings." = "Kann von Ihnen in den Erscheinungsbild-Einstellungen geändert werden."; + +/* No comment provided by engineer. */ +"You can configure servers via settings." = "Sie können die Server über die Einstellungen konfigurieren."; /* No comment provided by engineer. */ "You can create it later" = "Sie können dies später erstellen"; @@ -3806,11 +5781,23 @@ /* No comment provided by engineer. */ "You can enable them later via app Privacy & Security settings." = "Sie können diese später in den Datenschutz & Sicherheits-Einstellungen der App aktivieren."; +/* No comment provided by engineer. */ +"You can give another try." = "Sie können es nochmal probieren."; + /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "Sie können ein Benutzerprofil verbergen oder stummschalten - wischen Sie es nach rechts."; +/* No comment provided by engineer. */ +"You can make it visible to your SimpleX contacts via Settings." = "Sie können sie über Einstellungen für Ihre SimpleX-Kontakte sichtbar machen."; + /* notification body */ -"You can now send messages to %@" = "Sie können nun Nachrichten an %@ versenden"; +"You can now chat with %@" = "Sie können nun Nachrichten an %@ versenden"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "Sie können aus den archivierten Kontakten heraus Nachrichten an %@ versenden."; + +/* No comment provided by engineer. */ +"You can set connection name, to remember who the link was shared with." = "Sie können einen Verbindungsnamen festlegen, um sich zu merken, mit wem der Link geteilt wurde."; /* 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."; @@ -3822,10 +5809,10 @@ "You can share this address with your contacts to let them connect with **%@**." = "Sie können diese Adresse mit Ihren Kontakten teilen, um sie mit **%@** verbinden zu lassen."; /* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Sie können Ihre Adresse als Link oder als QR-Code teilen – Jede Person kann sich darüber mit Ihnen verbinden."; +"You can start chat via app Settings / Database or by restarting the app" = "Sie können den Chat über die App-Einstellungen / Datenbank oder durch Neustart der App starten"; /* No comment provided by engineer. */ -"You can start chat via app Settings / Database or by restarting the app" = "Sie können den Chat über die App-Einstellungen / Datenbank oder durch Neustart der App starten"; +"You can still view conversation with %@ in the list of chats." = "Sie können in der Chat-Liste weiterhin die Unterhaltung mit %@ einsehen."; /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Sie können die SimpleX-Sperre über die Einstellungen aktivieren."; @@ -3833,14 +5820,17 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Um Nachrichteninhalte zu formatieren, können Sie Markdowns verwenden:"; +/* alert message */ +"You can view invitation link again in connection details." = "Den Einladungslink können Sie in den Details der Verbindung nochmals sehen."; + /* No comment provided by engineer. */ "You can't send messages!" = "Sie können keine Nachrichten versenden!"; /* chat item text */ -"you changed address" = "Sie haben die Adresse gewechselt"; +"you changed address" = "Die Empfängeradresse wurde gewechselt"; /* chat item text */ -"you changed address for %@" = "Sie haben die Adresse für %@ gewechselt"; +"you changed address for %@" = "Die Empfängeradresse für %@ wurde gewechselt"; /* snd group event chat item */ "you changed role for yourself to %@" = "Sie haben Ihre eigene Rolle auf %@ geändert"; @@ -3849,10 +5839,10 @@ "you changed role of %@ to %@" = "Sie haben die Rolle von %1$@ auf %2$@ geändert"; /* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Sie können selbst festlegen, über welche Server Sie Ihre Nachrichten **empfangen** und an Ihre Kontakte **senden** wollen."; +"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 could not be verified; please try again." = "Sie konnten nicht überprüft werden; bitte versuchen Sie es erneut."; +"You decide who can connect." = "Sie entscheiden, wer sich mit Ihnen verbinden kann."; /* No comment provided by engineer. */ "You have already requested connection via this address!" = "Sie haben über diese Adresse bereits eine Verbindung beantragt!"; @@ -3860,9 +5850,6 @@ /* No comment provided by engineer. */ "You have already requested connection!\nRepeat connection request?" = "Sie haben bereits ein Verbindungsanfrage beantragt!\nVerbindungsanfrage wiederholen?"; -/* No comment provided by engineer. */ -"You have no chats" = "Sie haben keine Chats"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Sie müssen das Passwort jedes Mal eingeben, wenn die App startet. Es wird nicht auf dem Gerät gespeichert."; @@ -3878,9 +5865,18 @@ /* snd group event chat item */ "you left" = "Sie haben verlassen"; +/* No comment provided by engineer. */ +"You may migrate the exported database." = "Sie können die exportierte Datenbank migrieren."; + +/* No comment provided by engineer. */ +"You may save the exported archive." = "Sie können das exportierte Archiv speichern."; + /* 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." = "Sie dürfen die neueste Version Ihrer Chat-Datenbank NUR auf einem Gerät verwenden, andernfalls erhalten Sie möglicherweise keine Nachrichten mehr von einigen Ihrer Kontakte."; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "Sie müssen Ihrem Kontakt Anrufe zu Ihnen erlauben, bevor Sie ihn selbst anrufen können."; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "Sie müssen Ihrem Kontakt das Senden von Sprachnachrichten erlauben, um diese senden zu können."; @@ -3899,6 +5895,12 @@ /* chat list item description */ "you shared one-time link incognito" = "Sie haben Inkognito einen Einmal-Link geteilt"; +/* token info */ +"You should receive notifications." = "Sie sollten Benachrichtigungen erhalten."; + +/* snd group event chat item */ +"you unblocked %@" = "Sie haben %@ freigegeben"; + /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "Sie werden mit der Gruppe verbunden, sobald das Endgerät des Gruppen-Hosts online ist. Bitte warten oder schauen Sie später nochmal nach!"; @@ -3921,10 +5923,13 @@ "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 group. Chat history will be preserved." = "Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Chatverlauf wird beibehalten."; +"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."; /* No comment provided by engineer. */ -"You won't lose your contacts if you later delete your address." = "Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen."; +"You will stop receiving messages from this group. Chat history will be preserved." = "Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Nachrichtenverlauf wird beibehalten."; + +/* No comment provided by engineer. */ +"You won't lose your contacts if you later delete your address." = "Sie werden Ihre damit verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen."; /* No comment provided by engineer. */ "you: " = "Sie: "; @@ -3935,9 +5940,6 @@ /* 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" = "Sie verwenden ein Inkognito-Profil für diese Gruppe. Um zu verhindern, dass Sie Ihr Hauptprofil teilen, ist in diesem Fall das Einladen von Kontakten nicht erlaubt"; -/* No comment provided by engineer. */ -"Your %@ servers" = "Ihre %@-Server"; - /* No comment provided by engineer. */ "Your calls" = "Anrufe"; @@ -3947,11 +5949,14 @@ /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Ihre Chat-Datenbank ist nicht verschlüsselt. Bitte legen Sie ein Passwort fest, um sie zu schützen."; +/* alert title */ +"Your chat preferences" = "Ihre Chat-Präferenzen"; + /* No comment provided by engineer. */ "Your chat profiles" = "Ihre Chat-Profile"; /* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Damit die Verbindung hergestellt werden kann, muss Ihr Kontakt online sein.\nSie können diese Verbindung abbrechen und den Kontakt entfernen (und es später nochmals mit einem neuen Link versuchen)."; +"Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Ihre Verbindung wurde auf %@ verschoben. Während Sie auf das Profil weitergeleitet wurden trat aber ein unerwarteter Fehler auf."; /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ihr Kontakt hat eine Datei gesendet, die größer ist als die derzeit unterstützte maximale Größe (%@)."; @@ -3960,10 +5965,10 @@ "Your contacts can allow full message deletion." = "Ihre Kontakte können die unwiederbringliche Löschung von Nachrichten erlauben."; /* No comment provided by engineer. */ -"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "Ihre Kontakte in SimpleX werden es sehen.\nSie können es in den Einstellungen ändern."; +"Your contacts will remain connected." = "Ihre Kontakte bleiben weiterhin verbunden."; /* No comment provided by engineer. */ -"Your contacts will remain connected." = "Ihre Kontakte bleiben verbunden."; +"Your credentials may be sent unencrypted." = "Ihre Anmeldeinformationen können unverschlüsselt versendet werden."; /* No comment provided by engineer. */ "Your current chat database will be DELETED and REPLACED with the imported one." = "Ihre aktuelle Chat-Datenbank wird GELÖSCHT und durch die Importierte ERSETZT."; @@ -3978,7 +5983,7 @@ "Your preferences" = "Ihre Präferenzen"; /* No comment provided by engineer. */ -"Your privacy" = "Ihre Privatsphäre"; +"Your privacy" = "Privatsphäre"; /* No comment provided by engineer. */ "Your profile" = "Mein Profil"; @@ -3987,7 +5992,10 @@ "Your profile **%@** will be shared." = "Ihr Profil **%@** wird geteilt."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt.\nSimpleX-Server können Ihr Profil nicht einsehen."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt. SimpleX-Server können Ihr Profil nicht einsehen."; + +/* 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 profile, contacts and delivered messages are stored on your device." = "Ihr Profil, Ihre Kontakte und zugestellten Nachrichten werden auf Ihrem Gerät gespeichert."; @@ -3995,21 +6003,15 @@ /* No comment provided by engineer. */ "Your random profile" = "Ihr Zufallsprofil"; -/* No comment provided by engineer. */ -"Your server" = "Ihr Server"; - /* No comment provided by engineer. */ "Your server address" = "Ihre Serveradresse"; /* No comment provided by engineer. */ -"Your settings" = "Ihre Einstellungen"; +"Your servers" = "Ihre Server"; + +/* No comment provided by engineer. */ +"Your settings" = "Einstellungen"; /* No comment provided by engineer. */ "Your SimpleX address" = "Ihre SimpleX-Adresse"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Ihre SMP-Server"; - -/* No comment provided by engineer. */ -"Your XFTP servers" = "Ihre XFTP-Server"; - diff --git a/apps/ios/de.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/de.lproj/SimpleX--iOS--InfoPlist.strings index 5fe2ef2d09..e0554c9fb6 100644 --- a/apps/ios/de.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/de.lproj/SimpleX--iOS--InfoPlist.strings @@ -2,7 +2,7 @@ "CFBundleName" = "SimpleX"; /* Privacy - Camera Usage Description */ -"NSCameraUsageDescription" = "SimpleX benötigt Zugriff auf die Kamera, um QR Codes für die Verbindung mit anderen Nutzern zu scannen und Videoanrufe durchzuführen."; +"NSCameraUsageDescription" = "SimpleX benötigt Zugriff auf die Kamera, um QR Codes für die Verbindung mit anderen Benutzern zu scannen und Videoanrufe durchzuführen."; /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "Face ID wird von SimpleX für die lokale Authentifizierung genutzt"; @@ -14,5 +14,5 @@ "NSMicrophoneUsageDescription" = "SimpleX benötigt Zugriff auf das Mikrofon, um Audio- und Videoanrufe und die Aufnahme von Sprachnachrichten zu ermöglichen."; /* Privacy - Photo Library Additions Usage Description */ -"NSPhotoLibraryAddUsageDescription" = "SimpleX benötigt Zugriff auf das Fotoalbum, um selbst gemachte oder empfangene Bilder zu speichern"; +"NSPhotoLibraryAddUsageDescription" = "SimpleX benötigt Zugriff auf das Fotoalbum, um selbst gemachte oder heruntergeladene Bilder zu speichern"; diff --git a/apps/ios/en.lproj/Localizable.strings b/apps/ios/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/en.lproj/Localizable.strings +++ b/apps/ios/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index d9b14eddbd..28ba0f0642 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -1,18 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (puede copiarse)"; @@ -25,26 +10,20 @@ /* 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. */ +"- 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!"; + /* No comment provided by engineer. */ "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- mensajes de voz de hasta 5 minutos.\n- tiempo personalizado para mensajes temporales.\n- historial de edición."; -/* No comment provided by engineer. */ -", " = ", "; - -/* No comment provided by engineer. */ -": " = ": "; - /* No comment provided by engineer. */ "!1 colored!" = "!1 coloreado!"; /* No comment provided by engineer. */ -"." = "."; +"(new)" = "(nuevo)"; /* No comment provided by engineer. */ -"(" = "("; - -/* 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)"; @@ -56,10 +35,10 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Estrella en GitHub](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Añadir nuevo contacto**: para crear tu código QR o enlace de un uso para tu contacto."; +"**Create 1-time link**: to create and share a new invitation link." = "**Añadir contacto**: crea un enlace de invitación nuevo."; /* No comment provided by engineer. */ -"**Create link / QR code** for your contact to use." = "**Crea enlace / código QR** para que tu contacto lo use."; +"**Create group**: to create a new group." = "**Crear grupo**: crea un grupo nuevo."; /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "Llamada con **cifrado de extremo a extremo **"; @@ -68,26 +47,29 @@ "**e2e encrypted** video call" = "Videollamada con **cifrado de extremo a extremo**"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Más privado**: comprueba los mensajes nuevos cada 20 minutos. El token del dispositivo se comparte con el servidor de SimpleX Chat, pero no cuántos contactos o mensajes tienes."; +"**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." = "**Más privado**: comprueba los mensajes nuevos cada 20 minutos. El token del dispositivo se comparte con el servidor de SimpleX Chat, pero no cuántos contactos o mensajes tienes."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Más privado**: no se usa el servidor de notificaciones de SimpleX Chat, los mensajes se comprueban periódicamente en segundo plano (dependiendo de la frecuencia con la que utilices la aplicación)."; +"**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." = "**Más privado**: no se usa el servidor de notificaciones de SimpleX Chat, los mensajes se comprueban periódicamente en segundo plano (dependiendo de la frecuencia con la que utilices la aplicación)."; /* No comment provided by engineer. */ -"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Pega el enlace recibido** o ábrelo en el navegador y pulsa **Abrir en aplicación móvil**."; +"**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Recuarda**: usar la misma base de datos en dos dispositivos hará que falle el descifrado de mensajes como protección de seguridad."; /* No comment provided by engineer. */ -"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Atención**: NO podrás recuperar o cambiar la contraseña si la pierdes."; +"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Atención**: Si la pierdes NO podrás recuperar o cambiar la contraseña."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Recomendado**: el token del dispositivo y las notificaciones se envían al servidor de notificaciones de SimpleX Chat, pero no el contenido del mensaje, su tamaño o su procedencia."; +"**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." = "**Recomendado**: el token del dispositivo y las notificaciones se envían al servidor de notificaciones de SimpleX Chat, pero no el contenido del mensaje, su tamaño o su procedencia."; /* No comment provided by engineer. */ -"**Scan QR code**: to connect to your contact in person or via video call." = "**Escanear código QR**: en persona para conectarte con tu contacto, o por videollamada."; +"**Scan / Paste link**: to connect via a link you received." = "**Escanear / Pegar enlace**: para conectar mediante un enlace recibido."; /* 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."; +/* No comment provided by engineer. */ +"**Warning**: the archive will be removed." = "**Atención**: el archivo será eliminado."; + /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -118,12 +100,21 @@ /* No comment provided by engineer. */ "%@ %@" = "%@ %@"; +/* No comment provided by engineer. */ +"%@ and %@" = "%@ y %@"; + /* No comment provided by engineer. */ "%@ and %@ connected" = "%@ y %@ conectados"; /* copied message info, <sender> at <time> */ "%@ at %@:" = "%1$@ a las %2$@:"; +/* No comment provided by engineer. */ +"%@ connected" = "%@ conectado"; + +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ descargado"; + /* notification title */ "%@ is connected!" = "%@ ¡está conectado!"; @@ -134,11 +125,23 @@ "%@ is verified" = "%@ está verificado"; /* No comment provided by engineer. */ -"%@ servers" = "Servidores %@"; +"%@ server" = "%@ servidor"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ servidores"; + +/* No comment provided by engineer. */ +"%@ uploaded" = "%@ subido"; /* notification title */ "%@ wants to connect!" = "¡ %@ quiere contactar!"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + +/* No comment provided by engineer. */ +"%@, %@ and %lld members" = "%@, %@ y %lld miembro(s) más"; + /* No comment provided by engineer. */ "%@, %@ and %lld other members connected" = "%@, %@ y %lld miembros más conectados"; @@ -146,25 +149,43 @@ "%@:" = "%@:"; /* time interval */ -"%d days" = "%d días"; +"%d days" = "%d día(s)"; + +/* forward confirmation reason */ +"%d file(s) are still being downloaded." = "%d archivo(s) se está(n) descargando todavía."; + +/* forward confirmation reason */ +"%d file(s) failed to download." = "La descarga ha fallado para %d archivo(s)."; + +/* forward confirmation reason */ +"%d file(s) were deleted." = "%d archivo(s) ha(n) sido eliminado(s)."; + +/* forward confirmation reason */ +"%d file(s) were not downloaded." = "%d archivo(s) no se ha(n) descargado."; /* time interval */ -"%d hours" = "%d horas"; +"%d hours" = "%d hora(s)"; + +/* alert title */ +"%d messages not forwarded" = "%d mensaje(s) no enviado(s)"; /* time interval */ -"%d min" = "%d minutos"; +"%d min" = "%d minuto(s)"; /* time interval */ -"%d months" = "%d meses"; +"%d months" = "%d mes(es)"; /* time interval */ -"%d sec" = "%d segundos"; +"%d sec" = "%d segundo(s)"; + +/* delete after time */ +"%d seconds(s)" = "%d segundos"; /* integrity error chat item */ -"%d skipped message(s)" = "%d mensaje(s) saltado(s"; +"%d skipped message(s)" = "%d mensaje(s) omitido(s)"; /* time interval */ -"%d weeks" = "%d semanas"; +"%d weeks" = "%d semana(s)"; /* No comment provided by engineer. */ "%lld" = "%lld"; @@ -178,18 +199,30 @@ /* No comment provided by engineer. */ "%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"; + /* No comment provided by engineer. */ "%lld members" = "%lld miembros"; +/* No comment provided by engineer. */ +"%lld messages blocked" = "%lld mensaje(s) bloqueado(s)"; + +/* No comment provided by engineer. */ +"%lld messages blocked by admin" = "%lld mensajes bloqueados por el administrador"; + +/* No comment provided by engineer. */ +"%lld messages marked deleted" = "%lld mensaje(s) marcado(s) eliminado(s)"; + +/* No comment provided by engineer. */ +"%lld messages moderated by %@" = "%lld mensaje(s) moderado(s) por %@"; + /* No comment provided by engineer. */ "%lld minutes" = "%lld minutos"; /* No comment provided by engineer. */ "%lld new interface languages" = "%lld idiomas de interfaz nuevos"; -/* No comment provided by engineer. */ -"%lld second(s)" = "%lld segundo(s)"; - /* No comment provided by engineer. */ "%lld seconds" = "%lld segundos"; @@ -229,10 +262,14 @@ /* No comment provided by engineer. */ "~strike~" = "\\~strike~"; +/* time to disappear */ +"0 sec" = "0 seg"; + /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "un dia"; /* time interval */ @@ -241,14 +278,22 @@ /* No comment provided by engineer. */ "1 minute" = "1 minuto"; -/* time interval */ +/* delete after time +time interval */ "1 month" = "un mes"; -/* time interval */ +/* delete after time +time interval */ "1 week" = "una semana"; +/* delete after time */ +"1 year" = "1 año"; + /* No comment provided by engineer. */ -"1-time link" = "Enlace un uso"; +"1-time link" = "Enlace de un uso"; + +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Los enlaces de un uso pueden ser usados *solamente con un contacto* - compártelos en persona o mediante cualquier aplicación de mensajería."; /* No comment provided by engineer. */ "5 minutes" = "5 minutos"; @@ -266,28 +311,25 @@ "A new contact" = "Contacto nuevo"; /* No comment provided by engineer. */ -"A new random profile will be shared." = "Se compartirá un perfil nuevo aleatorio."; +"A new random profile will be shared." = "Compartirás un perfil nuevo aleatorio."; /* No comment provided by engineer. */ "A separate TCP connection will be used **for each chat profile you have in the app**." = "Se usará una conexión TCP independiente **por cada perfil que tengas en la aplicación**."; /* 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." = "Se usará una conexión TCP independiente **por cada contacto y miembro de grupo**.\n**Atención**: si tienes muchas conexiones, tu consumo de batería y tráfico pueden ser sustancialmente mayores y algunas conexiones pueden fallar."; +"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." = "Se usará una conexión TCP independiente **por cada contacto y miembro de grupo**.\n**Atención**: si tienes muchas conexiones, tu consumo de batería y tráfico pueden aumentar bastante y algunas conexiones pueden fallar."; /* No comment provided by engineer. */ "Abort" = "Cancelar"; /* No comment provided by engineer. */ -"Abort changing address" = "Cancelar cambio de dirección"; +"Abort changing address" = "Cancelar cambio de servidor"; /* No comment provided by engineer. */ -"Abort changing address?" = "¿Cancelar el cambio de dirección?"; +"Abort changing address?" = "¿Cancelar el cambio de servidor?"; /* No comment provided by engineer. */ -"About SimpleX" = "Acerca de SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "Acerca de la dirección SimpleX"; +"About operators" = "Acerca de los operadores"; /* No comment provided by engineer. */ "About SimpleX Chat" = "Sobre SimpleX Chat"; @@ -296,80 +338,175 @@ "above, then choose:" = "y después elige:"; /* No comment provided by engineer. */ -"Accent color" = "Color"; +"Accent" = "Color"; /* accept contact request via notification - accept incoming call via notification */ +accept incoming call via notification +swipe action */ "Accept" = "Aceptar"; +/* No comment provided by engineer. */ +"Accept conditions" = "Aceptar condiciones"; + /* No comment provided by engineer. */ "Accept connection request?" = "¿Aceptar solicitud de conexión?"; /* notification body */ "Accept contact request from %@?" = "¿Aceptar solicitud de contacto de %@?"; -/* accept contact request via notification */ +/* accept contact request via notification +swipe action */ "Accept incognito" = "Aceptar incógnito"; /* call status */ "accepted call" = "llamada aceptada"; +/* No comment provided by engineer. */ +"Accepted conditions" = "Condiciones aceptadas"; + +/* chat list item title */ +"accepted invitation" = "invitación aceptada"; + +/* No comment provided by engineer. */ +"Acknowledged" = "Confirmaciones"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Errores de confirmación"; + +/* token status text */ +"Active" = "Activo"; + +/* No comment provided by engineer. */ +"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."; /* No comment provided by engineer. */ -"Add preset servers" = "Añadir servidores predefinidos"; +"Add friends" = "Añadir amigos"; + +/* No comment provided by engineer. */ +"Add list" = "Añadir lista"; /* No comment provided by engineer. */ "Add profile" = "Añadir perfil"; /* No comment provided by engineer. */ -"Add server…" = "Añadir servidor…"; +"Add server" = "Añadir servidor"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Añadir servidores mediante el escaneo de códigos QR."; +/* No comment provided by engineer. */ +"Add team members" = "Añadir miembros del equipo"; + /* No comment provided by engineer. */ "Add to another device" = "Añadir a otro dispositivo"; +/* No comment provided by engineer. */ +"Add to list" = "Añadir a la lista"; + /* No comment provided by engineer. */ "Add welcome message" = "Añadir mensaje de bienvenida"; +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "Añade a miembros de tu equipo a las conversaciones."; + +/* No comment provided by engineer. */ +"Added media & file servers" = "Servidores de archivos y multimedia añadidos"; + +/* No comment provided by engineer. */ +"Added message servers" = "Servidores de mensajes añadidos"; + +/* No comment provided by engineer. */ +"Additional accent" = "Acento adicional"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Color adicional 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Secundario adicional"; + /* No comment provided by engineer. */ "Address" = "Dirección"; /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "El cambio de dirección se cancelará. Se usará la antigua dirección de recepción."; +/* No comment provided by engineer. */ +"Address or 1-time link?" = "¿Dirección o enlace de un uso?"; + +/* No comment provided by engineer. */ +"Address settings" = "Configurar dirección"; + /* member role */ "admin" = "administrador"; +/* feature role */ +"admins" = "administradores"; + +/* No comment provided by engineer. */ +"Admins can block a member for all." = "Los administradores pueden bloquear a un miembro para los demás."; + /* No comment provided by engineer. */ "Admins can create the links to join groups." = "Los administradores pueden crear enlaces para unirse a grupos."; /* No comment provided by engineer. */ "Advanced network settings" = "Configuración avanzada de red"; +/* No comment provided by engineer. */ +"Advanced settings" = "Configuración avanzada"; + /* chat item text */ "agreeing encryption for %@…" = "acordando cifrado para %@…"; /* chat item text */ "agreeing encryption…" = "acordando cifrado…"; +/* No comment provided by engineer. */ +"All" = "Todo"; + /* No comment provided by engineer. */ "All app data is deleted." = "Todos los datos de la aplicación se eliminarán."; /* No comment provided by engineer. */ -"All chats and messages will be deleted - this cannot be undone!" = "Se eliminarán todos los chats y mensajes. ¡No podrá deshacerse!"; +"All chats and messages will be deleted - this cannot be undone!" = "Se eliminarán todos los chats y mensajes. ¡No puede deshacerse!"; + +/* alert message */ +"All chats will be removed from the list %@, and the list deleted." = "Todos los chats se quitarán de la lista %@ y esta será eliminada."; /* No comment provided by engineer. */ "All data is erased when it is entered." = "Al introducirlo todos los datos son eliminados."; /* No comment provided by engineer. */ -"All group members will remain connected." = "Todos los miembros del grupo permanecerán conectados."; +"All data is kept private on your device." = "Todos los datos son privados y están en tu dispositivo."; /* No comment provided by engineer. */ -"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Se eliminarán todos los mensajes SOLO para tí. ¡No podrá deshacerse!"; +"All group members will remain connected." = "Todos los miembros del grupo permanecerán conectados."; + +/* feature role */ +"all members" = "todos los miembros"; + +/* No comment provided by engineer. */ +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Todos los mensajes y archivos son enviados **cifrados de extremo a extremo** y con seguridad de cifrado postcuántico en mensajes directos."; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone!" = "Todos los mensajes serán eliminados. ¡No puede deshacerse!"; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Se eliminarán todos los mensajes SOLO para tí. ¡No puede deshacerse!"; + +/* No comment provided by engineer. */ +"All new messages from %@ will be hidden!" = "¡Los mensajes nuevos de %@ estarán ocultos!"; + +/* profile dropdown */ +"All profiles" = "Todos los perfiles"; + +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Todos los informes serán archivados para ti."; + +/* No comment provided by engineer. */ +"All servers" = "Todos los servidores"; /* No comment provided by engineer. */ "All your contacts will remain connected." = "Todos tus contactos permanecerán conectados."; @@ -377,23 +514,32 @@ /* No comment provided by engineer. */ "All your contacts will remain connected. Profile update will be sent to your contacts." = "Todos tus contactos permanecerán conectados. La actualización del perfil se enviará a tus contactos."; +/* No comment provided by engineer. */ +"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Todos tus contactos, conversaciones y archivos serán cifrados, divididos y subidos de forma segura a los servidores XFTP configurados."; + /* No comment provided by engineer. */ "Allow" = "Se permite"; /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Se permiten las llamadas pero sólo si tu contacto también las permite."; +/* No comment provided by engineer. */ +"Allow calls?" = "¿Permitir llamadas?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "Se permiten los mensajes temporales pero sólo si tu contacto también los permite para tí."; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you." = "Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también la permite para tí."; +"Allow downgrade" = "Permitir versión anterior"; + +/* No comment provided by engineer. */ +"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también la permite para tí. (24 horas)"; /* 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."; /* No comment provided by engineer. */ -"Allow message reactions." = "Permitir reacciones a los mensajes."; +"Allow message reactions." = "Se permiten reacciones a los mensajes."; /* No comment provided by engineer. */ "Allow sending direct messages to members." = "Se permiten mensajes directos entre miembros."; @@ -402,11 +548,20 @@ "Allow sending disappearing messages." = "Permites el envío de mensajes temporales."; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages." = "Se permite la eliminación irreversible de mensajes."; +"Allow sharing" = "Permitir compartir"; + +/* No comment provided by engineer. */ +"Allow to irreversibly delete sent messages. (24 hours)" = "Se permite la eliminación irreversible de mensajes. (24 horas)"; + +/* No comment provided by engineer. */ +"Allow to report messsages to moderators." = "Permitir informar de mensajes a los moderadores."; /* No comment provided by engineer. */ "Allow to send files and media." = "Se permite enviar archivos y multimedia."; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "Se permite enviar enlaces SimpleX."; + /* No comment provided by engineer. */ "Allow to send voice messages." = "Permites enviar mensajes de voz."; @@ -423,7 +578,7 @@ "Allow your contacts to call you." = "Permites que tus contactos puedan llamarte."; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages." = "Permites a tus contactos eliminar irreversiblemente los mensajes enviados."; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Permites a tus contactos eliminar irreversiblemente los mensajes enviados. (24 horas)"; /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Permites a tus contactos enviar mensajes temporales."; @@ -434,26 +589,50 @@ /* No comment provided by engineer. */ "Already connected?" = "¿Ya está conectado?"; +/* No comment provided by engineer. */ +"Already connecting!" = "¡Ya en proceso de conexión!"; + +/* No comment provided by engineer. */ +"Already joining the group!" = "¡Ya en proceso de unirte al grupo!"; + /* pref value */ "always" = "siempre"; +/* No comment provided by engineer. */ +"Always use private routing." = "Usar siempre enrutamiento privado."; + /* No comment provided by engineer. */ "Always use relay" = "Usar siempre retransmisor"; /* No comment provided by engineer. */ "An empty chat profile with the provided name is created, and the app opens as usual." = "Se creará un perfil vacío con el nombre proporcionado, y la aplicación se abrirá como de costumbre."; +/* No comment provided by engineer. */ +"and %lld other events" = "y %lld evento(s) más"; + +/* report reason */ +"Another reason" = "Otro motivo"; + /* 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: %@"; +/* No comment provided by engineer. */ +"App data migration" = "Migrar datos de la aplicación"; + /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "Cifrado de los nuevos archivos locales (excepto vídeos)."; /* No comment provided by engineer. */ -"App icon" = "Icono aplicación"; +"App group:" = "Grupo app:"; + +/* No comment provided by engineer. */ +"App icon" = "Icono de la aplicación"; /* No comment provided by engineer. */ "App passcode" = "Código de acceso de la aplicación"; @@ -461,6 +640,9 @@ /* No comment provided by engineer. */ "App passcode is replaced with self-destruct passcode." = "El código de acceso será reemplazado por código de autodestrucción."; +/* No comment provided by engineer. */ +"App session" = "por sesión"; + /* No comment provided by engineer. */ "App version" = "Versión de la aplicación"; @@ -470,9 +652,51 @@ /* No comment provided by engineer. */ "Appearance" = "Apariencia"; +/* No comment provided by engineer. */ +"Apply" = "Aplicar"; + +/* No comment provided by engineer. */ +"Apply to" = "Aplicar a"; + +/* No comment provided by engineer. */ +"Archive" = "Archivar"; + +/* No comment provided by engineer. */ +"Archive %lld reports?" = "¿Archivar %lld informes?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "¿Archivar todos los informes?"; + +/* No comment provided by engineer. */ +"Archive and upload" = "Archivar y subir"; + +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Archiva contactos para charlar más tarde."; + +/* No comment provided by engineer. */ +"Archive report" = "Archivar informe"; + +/* No comment provided by engineer. */ +"Archive report?" = "¿Archivar informe?"; + +/* swipe action */ +"Archive reports" = "Archivar informes"; + +/* No comment provided by engineer. */ +"Archived contacts" = "Contactos archivados"; + +/* No comment provided by engineer. */ +"archived report" = "informes archivados"; + +/* No comment provided by engineer. */ +"Archiving database" = "Archivando base de datos"; + /* No comment provided by engineer. */ "Attach" = "Adjuntar"; +/* No comment provided by engineer. */ +"attempts" = "intentos"; + /* No comment provided by engineer. */ "Audio & video calls" = "Llamadas y videollamadas"; @@ -500,6 +724,9 @@ /* No comment provided by engineer. */ "Authentication unavailable" = "Autenticación no disponible"; +/* member role */ +"author" = "autor"; + /* No comment provided by engineer. */ "Auto-accept" = "Auto aceptar"; @@ -509,9 +736,18 @@ /* No comment provided by engineer. */ "Auto-accept images" = "Aceptar imágenes automáticamente"; +/* alert title */ +"Auto-accept settings" = "Auto aceptar configuración"; + /* No comment provided by engineer. */ "Back" = "Volver"; +/* No comment provided by engineer. */ +"Background" = "Fondo"; + +/* No comment provided by engineer. */ +"Bad desktop address" = "Dirección ordenador incorrecta"; + /* integrity error chat item */ "bad message hash" = "hash de mensaje erróneo"; @@ -524,9 +760,76 @@ /* No comment provided by engineer. */ "Bad message ID" = "ID de mensaje incorrecto"; +/* No comment provided by engineer. */ +"Better calls" = "Llamadas mejoradas"; + +/* No comment provided by engineer. */ +"Better groups" = "Grupos mejorados"; + +/* No comment provided by engineer. */ +"Better groups performance" = "Rendimiento de grupos mejorado"; + +/* No comment provided by engineer. */ +"Better message dates." = "Sistema de fechas mejorado."; + /* No comment provided by engineer. */ "Better messages" = "Mensajes mejorados"; +/* No comment provided by engineer. */ +"Better networking" = "Uso de red mejorado"; + +/* No comment provided by engineer. */ +"Better notifications" = "Notificaciones mejoradas"; + +/* No comment provided by engineer. */ +"Better privacy and security" = "Privacidad y seguridad mejoradas"; + +/* No comment provided by engineer. */ +"Better security ✅" = "Seguridad mejorada ✅"; + +/* No comment provided by engineer. */ +"Better user experience" = "Experiencia de usuario mejorada"; + +/* No comment provided by engineer. */ +"Black" = "Negro"; + +/* No comment provided by engineer. */ +"Block" = "Bloquear"; + +/* No comment provided by engineer. */ +"Block for all" = "Bloquear para todos"; + +/* No comment provided by engineer. */ +"Block group members" = "Bloquear miembros del grupo"; + +/* No comment provided by engineer. */ +"Block member" = "Bloquear miembro"; + +/* No comment provided by engineer. */ +"Block member for all?" = "¿Bloqear miembro para todos?"; + +/* No comment provided by engineer. */ +"Block member?" = "¿Bloquear miembro?"; + +/* marked deleted chat item preview text */ +"blocked" = "bloqueado"; + +/* rcv group event chat item */ +"blocked %@" = "ha bloqueado a %@"; + +/* blocked chat item +marked deleted chat item preview text */ +"blocked by admin" = "bloqueado por administrador"; + +/* No comment provided by engineer. */ +"Blocked by admin" = "Bloqueado por administrador"; + +/* No comment provided by engineer. */ +"Blur for better privacy." = "Difumina para mayor privacidad."; + +/* No comment provided by engineer. */ +"Blur media" = "Difuminar multimedia"; + /* No comment provided by engineer. */ "bold" = "negrita"; @@ -534,7 +837,7 @@ "Both you and your contact can add message reactions." = "Tanto tú como tu contacto podéis añadir reacciones a los mensajes."; /* No comment provided by engineer. */ -"Both you and your contact can irreversibly delete sent messages." = "Tanto tú como tu contacto podéis eliminar de forma irreversible los mensajes enviados."; +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "Tanto tú como tu contacto podéis eliminar de forma irreversible los mensajes enviados. (24 horas)"; /* No comment provided by engineer. */ "Both you and your contact can make calls." = "Tanto tú como tu contacto podéis realizar llamadas."; @@ -549,7 +852,22 @@ "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)!"; /* 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 (por defecto) o [por conexión](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; +"Business address" = "Dirección empresarial"; + +/* No comment provided by engineer. */ +"Business chats" = "Chats empresariales"; + +/* No comment provided by engineer. */ +"Businesses" = "Empresas"; + +/* 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"; /* No comment provided by engineer. */ "Call already ended!" = "¡La llamada ha terminado!"; @@ -567,17 +885,33 @@ "Calls" = "Llamadas"; /* No comment provided by engineer. */ -"Can't delete user profile!" = "¡No se puede eliminar el perfil!"; +"Calls prohibited!" = "¡Llamadas no permitidas!"; + +/* No comment provided by engineer. */ +"Camera not available" = "Cámara no disponible"; + +/* No comment provided by engineer. */ +"Can't call contact" = "No se puede llamar al contacto"; + +/* No comment provided by engineer. */ +"Can't call member" = "No se puede llamar al miembro"; /* No comment provided by engineer. */ "Can't invite contact!" = "¡No se puede invitar el contacto!"; /* No comment provided by engineer. */ -"Can't invite contacts!" = "¡No se puede invitar a los contactos!"; +"Can't invite contacts!" = "¡No se pueden invitar contactos!"; /* No comment provided by engineer. */ +"Can't message member" = "No se pueden enviar mensajes al miembro"; + +/* alert action +alert button */ "Cancel" = "Cancelar"; +/* No comment provided by engineer. */ +"Cancel migration" = "Cancelar migración"; + /* feature offered item */ "cancelled %@" = "cancelado %@"; @@ -585,11 +919,26 @@ "Cannot access keychain to save database password" = "Keychain inaccesible para guardar la contraseña de la base de datos"; /* No comment provided by engineer. */ +"Cannot forward message" = "No se puede reenviar el mensaje"; + +/* alert title */ "Cannot receive file" = "No se puede recibir el archivo"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Capacidad excedida - el destinatario no ha recibido los mensajes previos."; + +/* No comment provided by engineer. */ +"Cellular" = "Móvil"; + /* No comment provided by engineer. */ "Change" = "Cambiar"; +/* alert title */ +"Change automatic message deletion?" = "¿Modificar la eliminación automática de mensajes?"; + +/* authentication reason */ +"Change chat profiles" = "Cambiar perfil de usuario"; + /* No comment provided by engineer. */ "Change database passphrase?" = "¿Cambiar contraseña de la base de datos?"; @@ -597,7 +946,7 @@ "Change lock mode" = "Cambiar el modo de bloqueo"; /* No comment provided by engineer. */ -"Change member role?" = "¿Cambiar el rol del miembro?"; +"Change member role?" = "¿Cambiar rol?"; /* authentication reason */ "Change passcode" = "Cambiar código de acceso"; @@ -615,11 +964,11 @@ "Change self-destruct mode" = "Cambiar el modo de autodestrucción"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Cambiar código autodestrucción"; /* chat item text */ -"changed address for you" = "el servidor de envío ha cambiado para tí"; +"changed address for you" = "ha cambiado tu servidor de envío"; /* rcv group event chat item */ "changed role of %@ to %@" = "rol cambiado de %1$@ a %2$@"; @@ -628,44 +977,89 @@ "changed your role to %@" = "ha cambiado tu rol a %@"; /* chat item text */ -"changing address for %@…" = "cambiando dirección para %@…"; +"changing address for %@…" = "cambiando el servidor para %@…"; /* chat item text */ -"changing address…" = "cambiando dirección…"; +"changing address…" = "cambiando de servidor…"; /* No comment provided by engineer. */ -"Chat archive" = "Archivo del chat"; +"Chat" = "Chat"; + +/* No comment provided by engineer. */ +"Chat already exists" = "El chat ya existe"; + +/* No comment provided by engineer. */ +"Chat already exists!" = "¡El chat ya existe!"; + +/* No comment provided by engineer. */ +"Chat colors" = "Colores del chat"; /* No comment provided by engineer. */ "Chat console" = "Consola de Chat"; /* No comment provided by engineer. */ -"Chat database" = "Base de datos del chat"; +"Chat database" = "Base de datos de SimpleX"; /* No comment provided by engineer. */ "Chat database deleted" = "Base de datos eliminada"; +/* No comment provided by engineer. */ +"Chat database exported" = "Base de datos exportada"; + /* No comment provided by engineer. */ "Chat database imported" = "Base de datos importada"; /* No comment provided by engineer. */ -"Chat is running" = "Chat está en ejecución"; +"Chat is running" = "SimpleX está en ejecución"; /* No comment provided by engineer. */ -"Chat is stopped" = "Chat está detenido"; +"Chat is stopped" = "SimpleX está parado"; + +/* 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." = "SimpleX está parado. Si has usado esta base de datos en otro dispositivo, debes transferirla de vuelta antes de iniciar SimpleX."; + +/* No comment provided by engineer. */ +"Chat list" = "Lista de chats"; + +/* No comment provided by engineer. */ +"Chat migrated!" = "¡Chat migrado!"; /* No comment provided by engineer. */ "Chat preferences" = "Preferencias de Chat"; +/* alert message */ +"Chat preferences were changed." = "Las preferencias del chat han sido modificadas."; + +/* No comment provided by engineer. */ +"Chat profile" = "Perfil de usuario"; + +/* No comment provided by engineer. */ +"Chat theme" = "Tema de chat"; + +/* No comment provided by engineer. */ +"Chat will be deleted for all members - this cannot be undone!" = "El chat será eliminado para todos los miembros. ¡No puede deshacerse!"; + +/* No comment provided by engineer. */ +"Chat will be deleted for you - this cannot be undone!" = "El chat será eliminado para tí. ¡No puede deshacerse!"; + /* No comment provided by engineer. */ "Chats" = "Chats"; /* 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 title */ "Check server address and try again." = "Comprueba la dirección del servidor e inténtalo de nuevo."; /* No comment provided by engineer. */ "Chinese and Spanish interface" = "Interfaz en chino y español"; +/* No comment provided by engineer. */ +"Choose _Migrate from another device_ on the new device and scan QR code." = "En el nuevo dispositivo selecciona _Migrar desde otro dispositivo_ y escanéa el código QR."; + /* No comment provided by engineer. */ "Choose file" = "Elije archivo"; @@ -673,6 +1067,15 @@ "Choose from library" = "Elige de la biblioteca"; /* No comment provided by engineer. */ +"Chunks deleted" = "Bloques eliminados"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Bloques descargados"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Bloques subidos"; + +/* swipe action */ "Clear" = "Vaciar"; /* No comment provided by engineer. */ @@ -681,14 +1084,29 @@ /* No comment provided by engineer. */ "Clear conversation?" = "¿Vaciar conversación?"; +/* No comment provided by engineer. */ +"Clear group?" = "¿Vaciar grupo?"; + +/* No comment provided by engineer. */ +"Clear or delete group?" = "¿Vaciar o eliminar grupo?"; + +/* No comment provided by engineer. */ +"Clear private notes?" = "¿Eliminar notas privadas?"; + /* No comment provided by engineer. */ "Clear verification" = "Eliminar verificación"; +/* No comment provided by engineer. */ +"Color chats with the new themes." = "Colorea los chats con los nuevos temas."; + +/* No comment provided by engineer. */ +"Color mode" = "Modo de color"; + /* No comment provided by engineer. */ "colored" = "coloreado"; -/* No comment provided by engineer. */ -"Colors" = "Colores"; +/* report reason */ +"Community guidelines violation" = "Violación de las normas de la comunidad"; /* server test step */ "Compare file" = "Comparar archivo"; @@ -699,15 +1117,51 @@ /* No comment provided by engineer. */ "complete" = "completado"; +/* No comment provided by engineer. */ +"Completed" = "Completadas"; + +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "Condiciones aceptadas el: %@."; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "Las condiciones se han aceptado para el(los) operador(s): **%@**."; + +/* No comment provided by engineer. */ +"Conditions are already accepted for these operator(s): **%@**." = "Las condiciones ya se han aceptado para el/los siguiente(s) operador(s): **%@**."; + +/* No comment provided by engineer. */ +"Conditions of use" = "Condiciones de uso"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "Las condiciones serán aceptadas para el/los operador(es): **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "Las condiciones serán aceptadas el: %@."; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "Las condiciones serán aceptadas automáticamente para los operadores habilitados el: %@."; + /* No comment provided by engineer. */ "Configure ICE servers" = "Configure servidores ICE"; +/* No comment provided by engineer. */ +"Configure server operators" = "Configurar operadores de servidores"; + /* No comment provided by engineer. */ "Confirm" = "Confirmar"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "¿Confirmas la eliminación del contacto?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "Confirmar actualizaciones de la bases de datos"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Confirma archivos de servidores desconocidos."; + +/* No comment provided by engineer. */ +"Confirm network settings" = "Confirmar configuración de red"; + /* No comment provided by engineer. */ "Confirm new passphrase…" = "Confirme nueva contraseña…"; @@ -717,32 +1171,77 @@ /* No comment provided by engineer. */ "Confirm password" = "Confirmar contraseña"; +/* No comment provided by engineer. */ +"Confirm that you remember database passphrase to migrate it." = "Para migrar la base de datos confirma que recuerdas la frase de contraseña."; + +/* No comment provided by engineer. */ +"Confirm upload" = "Confirmar subida"; + +/* token status text */ +"Confirmed" = "Confirmado"; + /* server test step */ "Connect" = "Conectar"; +/* No comment provided by engineer. */ +"Connect automatically" = "Conectar automáticamente"; + /* No comment provided by engineer. */ "Connect incognito" = "Conectar incognito"; +/* No comment provided by engineer. */ +"Connect to desktop" = "Conectar con ordenador"; + /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "contacta con los desarrolladores de SimpleX Chat."; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Conecta más rápido con tus amigos."; + +/* No comment provided by engineer. */ +"Connect to yourself?" = "¿Conectarte a tí mismo?"; + +/* No comment provided by engineer. */ +"Connect to yourself?\nThis is your own one-time link!" = "¿Conectarte a tí mismo?\n¡Este es tu propio enlace de un solo uso!"; + +/* No comment provided by engineer. */ +"Connect to yourself?\nThis is your own SimpleX address!" = "¿Conectarte a tí mismo?\n¡Esta es tu propia dirección SimpleX!"; + +/* No comment provided by engineer. */ +"Connect via contact address" = "Conectar mediante dirección de contacto"; + /* No comment provided by engineer. */ "Connect via link" = "Conectar mediante enlace"; -/* No comment provided by engineer. */ -"Connect via link / QR code" = "Conecta vía enlace / Código QR"; - /* No comment provided by engineer. */ "Connect via one-time link" = "Conectar mediante enlace de un sólo uso"; +/* No comment provided by engineer. */ +"Connect with %@" = "Conectar con %@"; + /* No comment provided by engineer. */ "connected" = "conectado"; +/* No comment provided by engineer. */ +"Connected" = "Conectadas"; + +/* No comment provided by engineer. */ +"Connected desktop" = "Ordenador conectado"; + /* rcv group event chat item */ "connected directly" = "conectado directamente"; /* No comment provided by engineer. */ -"connecting" = "conectando"; +"Connected servers" = "Servidores conectados"; + +/* No comment provided by engineer. */ +"Connected to desktop" = "Conectado con ordenador"; + +/* No comment provided by engineer. */ +"connecting" = "conectando..."; + +/* No comment provided by engineer. */ +"Connecting" = "Conectando"; /* No comment provided by engineer. */ "connecting (accepted)" = "conectando (aceptado)"; @@ -765,36 +1264,78 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Conectando con el servidor... (error: %@)"; -/* chat list item title */ +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "Conectando con el contacto, por favor espera o revisa más tarde."; + +/* No comment provided by engineer. */ +"Connecting to desktop" = "Conectando con ordenador"; + +/* No comment provided by engineer. */ "connecting…" = "conectando…"; /* No comment provided by engineer. */ "Connection" = "Conexión"; +/* No comment provided by engineer. */ +"Connection and servers status." = "Estado de tu conexión y servidores."; + +/* No comment provided by engineer. */ +"Connection blocked" = "Conexión bloqueada"; + /* No comment provided by engineer. */ "Connection error" = "Error conexión"; /* No comment provided by engineer. */ -"Connection error (AUTH)" = "Error conexión (Autenticación)"; +"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 is blocked by server operator:\n%@" = "Conexión bloqueada por el operador del servidor:\n%@"; + +/* No comment provided by engineer. */ +"Connection not ready." = "Conexión no establecida."; + +/* No comment provided by engineer. */ +"Connection notifications" = "Notificaciones de conexión"; + /* No comment provided by engineer. */ "Connection request sent!" = "¡Solicitud de conexión enviada!"; /* No comment provided by engineer. */ -"Connection timeout" = "Tiempo de conexión expirado"; +"Connection requires encryption renegotiation." = "La conexión requiere renegociar el cifrado."; + +/* No comment provided by engineer. */ +"Connection security" = "Seguridad de conexión"; + +/* No comment provided by engineer. */ +"Connection terminated" = "Conexión finalizada"; + +/* No comment provided by engineer. */ +"Connection timeout" = "Tiempo de conexión agotado"; + +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "La conexión con el escritorio (desktop) se ha parado"; /* connection information */ "connection:%@" = "conexión: % @"; +/* No comment provided by engineer. */ +"Connections" = "Conexiones"; + +/* profile update event chat item */ +"contact %@ changed to %@" = "el contacto %1$@ ha cambiado a %2$@"; + /* No comment provided by engineer. */ "Contact allows" = "El contacto permite"; /* No comment provided by engineer. */ "Contact already exists" = "El contácto ya existe"; +/* No comment provided by engineer. */ +"Contact deleted!" = "¡Contacto eliminado!"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "el contacto dispone de cifrado de extremo a extremo"; @@ -808,7 +1349,7 @@ "Contact is connected" = "El contacto está en línea"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "¡El contacto aun no se ha conectado!"; +"Contact is deleted." = "El contacto está eliminado."; /* No comment provided by engineer. */ "Contact name" = "Contacto"; @@ -816,41 +1357,68 @@ /* No comment provided by engineer. */ "Contact preferences" = "Preferencias de contacto"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "El contacto será eliminado. ¡No puede deshacerse!"; + /* No comment provided by engineer. */ "Contacts" = "Contactos"; /* No comment provided by engineer. */ "Contacts can mark messages for deletion; you will be able to view them." = "Tus contactos sólo pueden marcar los mensajes para eliminar. Tu podrás verlos."; +/* blocking reason */ +"Content violates conditions of use" = "El contenido viola las condiciones de uso"; + /* No comment provided by engineer. */ "Continue" = "Continuar"; -/* chat item action */ +/* No comment provided by engineer. */ +"Conversation deleted!" = "¡Conversación eliminada!"; + +/* No comment provided by engineer. */ "Copy" = "Copiar"; +/* No comment provided by engineer. */ +"Copy error" = "Copiar error"; + /* No comment provided by engineer. */ "Core version: v%@" = "Versión Core: v%@"; +/* No comment provided by engineer. */ +"Corner" = "Esquina"; + +/* No comment provided by engineer. */ +"Correct name to %@?" = "¿Corregir el nombre a %@?"; + /* No comment provided by engineer. */ "Create" = "Crear"; /* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Crea una dirección para que otras personas puedan conectar contigo."; +"Create 1-time link" = "Crear enlace de un uso"; + +/* No comment provided by engineer. */ +"Create a group using a random profile." = "Crear grupo usando perfil aleatorio."; /* server test step */ "Create file" = "Crear archivo"; +/* No comment provided by engineer. */ +"Create group" = "Crear grupo"; + /* No comment provided by engineer. */ "Create group link" = "Crear enlace de grupo"; /* No comment provided by engineer. */ "Create link" = "Crear enlace"; +/* No comment provided by engineer. */ +"Create list" = "Crear lista"; + /* No comment provided by engineer. */ "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Crea perfil nuevo en la [aplicación para PC](https://simplex.Descargas/de chat/). 💻"; /* No comment provided by engineer. */ -"Create one-time invitation link" = "Crea enlace de invitación de un uso"; +"Create profile" = "Crear perfil"; /* server test step */ "Create queue" = "Crear cola"; @@ -859,23 +1427,41 @@ "Create secret group" = "Crea grupo secreto"; /* No comment provided by engineer. */ -"Create SimpleX address" = "Crear tu dirección SimpleX"; +"Create SimpleX address" = "Crear dirección SimpleX"; /* No comment provided by engineer. */ "Create your profile" = "Crea tu perfil"; /* No comment provided by engineer. */ -"Created on %@" = "Creado en %@"; +"Created" = "Creadas"; + +/* No comment provided by engineer. */ +"Created at" = "Creado"; + +/* copied message info */ +"Created at: %@" = "Creado: %@"; + +/* No comment provided by engineer. */ +"Creating archive link" = "Creando enlace al archivo"; + +/* No comment provided by engineer. */ +"Creating link…" = "Creando enlace…"; /* No comment provided by engineer. */ "creator" = "creador"; +/* No comment provided by engineer. */ +"Current conditions text couldn't be loaded, you can review conditions via this link:" = "El texto con las condiciones actuales no se ha podido cargar, puedes revisar las condiciones en el siguiente enlace:"; + /* No comment provided by engineer. */ "Current Passcode" = "Código de Acceso"; /* No comment provided by engineer. */ "Current passphrase…" = "Contraseña actual…"; +/* No comment provided by engineer. */ +"Current profile" = "Perfil actual"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "El tamaño máximo de archivo admitido es %@."; @@ -885,9 +1471,18 @@ /* No comment provided by engineer. */ "Custom time" = "Tiempo personalizado"; +/* No comment provided by engineer. */ +"Customizable message shape." = "Forma personalizable de los mensajes."; + +/* No comment provided by engineer. */ +"Customize theme" = "Personalizar tema"; + /* No comment provided by engineer. */ "Dark" = "Oscuro"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Colores en modo oscuro"; + /* No comment provided by engineer. */ "Database downgrade" = "Degradación de la base de datos"; @@ -925,7 +1520,7 @@ "Database passphrase & export" = "Base de datos y contraseña"; /* No comment provided by engineer. */ -"Database passphrase is different from saved in the keychain." = "La contraseña es distinta a la almacenada en Keychain."; +"Database passphrase is different from saved in the keychain." = "La contraseña es diferente a la almacenada en Keychain."; /* No comment provided by engineer. */ "Database passphrase is required to open chat." = "Para abrir la aplicación se requiere la contraseña de la base de datos."; @@ -948,24 +1543,38 @@ /* time unit */ "days" = "días"; +/* No comment provided by engineer. */ +"Debug delivery" = "Informe debug"; + /* No comment provided by engineer. */ "Decentralized" = "Descentralizada"; /* message decrypt error item */ "Decryption error" = "Error descifrado"; -/* pref value */ -"default (%@)" = "por defecto (%@)"; +/* No comment provided by engineer. */ +"decryption errors" = "errores de descifrado"; + +/* delete after time +pref value */ +"default (%@)" = "predeterminado (%@)"; /* No comment provided by engineer. */ -"default (no)" = "por defecto (no)"; +"default (no)" = "predeterminado (no)"; /* No comment provided by engineer. */ -"default (yes)" = "por defecto (sí)"; +"default (yes)" = "predeterminado (sí)"; -/* chat item action */ +/* alert action +swipe action */ "Delete" = "Eliminar"; +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "¿Eliminar %lld mensajes de miembros?"; + +/* No comment provided by engineer. */ +"Delete %lld messages?" = "¿Eliminar %lld mensajes?"; + /* No comment provided by engineer. */ "Delete address" = "Eliminar dirección"; @@ -979,16 +1588,22 @@ "Delete all files" = "Eliminar todos los archivos"; /* No comment provided by engineer. */ -"Delete archive" = "Eliminar archivo"; +"Delete and notify contact" = "Eliminar y notificar contacto"; /* No comment provided by engineer. */ -"Delete chat archive?" = "¿Eliminar archivo del chat?"; +"Delete chat" = "Eliminar chat"; + +/* No comment provided by engineer. */ +"Delete chat messages from your device." = "Elimina los mensajes del dispositivo."; /* No comment provided by engineer. */ "Delete chat profile" = "Eliminar perfil"; /* No comment provided by engineer. */ -"Delete chat profile?" = "¿Eliminar el perfil?"; +"Delete chat profile?" = "¿Eliminar perfil?"; + +/* No comment provided by engineer. */ +"Delete chat?" = "¿Eliminar chat?"; /* No comment provided by engineer. */ "Delete connection" = "Eliminar conexión"; @@ -997,16 +1612,19 @@ "Delete contact" = "Eliminar contacto"; /* No comment provided by engineer. */ -"Delete Contact" = "Eliminar contacto"; +"Delete contact?" = "¿Eliminar contacto?"; /* No comment provided by engineer. */ "Delete database" = "Eliminar base de datos"; +/* No comment provided by engineer. */ +"Delete database from this device" = "Eliminar base de datos de este dispositivo"; + /* server test step */ "Delete file" = "Eliminar archivo"; /* No comment provided by engineer. */ -"Delete files and media?" = "Eliminar archivos y multimedia?"; +"Delete files and media?" = "¿Eliminar archivos y multimedia?"; /* No comment provided by engineer. */ "Delete files for all chat profiles" = "Eliminar archivos de todos los perfiles"; @@ -1032,14 +1650,17 @@ /* No comment provided by engineer. */ "Delete link?" = "¿Eliminar enlace?"; +/* alert title */ +"Delete list?" = "¿Eliminar lista?"; + /* No comment provided by engineer. */ "Delete member message?" = "¿Eliminar el mensaje de miembro?"; /* No comment provided by engineer. */ "Delete message?" = "¿Eliminar mensaje?"; -/* No comment provided by engineer. */ -"Delete messages" = "Eliminar mensaje"; +/* alert button */ +"Delete messages" = "Activar"; /* No comment provided by engineer. */ "Delete messages after" = "Eliminar en"; @@ -1051,10 +1672,10 @@ "Delete old database?" = "¿Eliminar base de datos antigua?"; /* No comment provided by engineer. */ -"Delete pending connection" = "Eliminar conexión pendiente"; +"Delete or moderate up to 200 messages." = "Elimina o modera hasta 200 mensajes a la vez."; /* No comment provided by engineer. */ -"Delete pending connection?" = "¿Eliminar la conexion pendiente?"; +"Delete pending connection?" = "¿Eliminar conexión pendiente?"; /* No comment provided by engineer. */ "Delete profile" = "Eliminar perfil"; @@ -1062,20 +1683,41 @@ /* server test step */ "Delete queue" = "Eliminar cola"; +/* No comment provided by engineer. */ +"Delete report" = "Eliminar informe"; + +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Elimina hasta 20 mensajes a la vez."; + /* No comment provided by engineer. */ "Delete user profile?" = "¿Eliminar perfil de usuario?"; +/* No comment provided by engineer. */ +"Delete without notification" = "Elimina sin notificar"; + /* deleted chat item */ "deleted" = "eliminado"; +/* No comment provided by engineer. */ +"Deleted" = "Eliminadas"; + /* No comment provided by engineer. */ "Deleted at" = "Eliminado"; /* copied message info */ "Deleted at: %@" = "Eliminado: %@"; +/* rcv direct event chat item */ +"deleted contact" = "contacto eliminado"; + /* rcv group event chat item */ -"deleted group" = "grupo eliminado"; +"deleted group" = "ha eliminado el grupo"; + +/* No comment provided by engineer. */ +"Deletion errors" = "Errores de eliminación"; + +/* No comment provided by engineer. */ +"Delivered even when Apple drops them." = "Entregados incluso cuando Apple los descarta."; /* No comment provided by engineer. */ "Delivery" = "Entrega"; @@ -1089,9 +1731,36 @@ /* No comment provided by engineer. */ "Description" = "Descripción"; +/* No comment provided by engineer. */ +"Desktop address" = "Dirección ordenador"; + +/* No comment provided by engineer. */ +"Desktop app version %@ is not compatible with this app." = "La versión de aplicación del ordenador %\" no es compatible con esta aplicación."; + +/* No comment provided by engineer. */ +"Desktop devices" = "Ordenadores"; + +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "La dirección del servidor de destino de %@ es incompatible con la configuración del servidor de reenvío %@."; + +/* snd error text */ +"Destination server error: %@" = "Error del servidor de destino: %@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "La versión del servidor de destino de %@ es incompatible con el servidor de reenvío %@."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Estadísticas detalladas"; + +/* No comment provided by engineer. */ +"Details" = "Detalles"; + /* No comment provided by engineer. */ "Develop" = "Desarrollo"; +/* No comment provided by engineer. */ +"Developer options" = "Opciones desarrollador"; + /* No comment provided by engineer. */ "Developer tools" = "Herramientas desarrollo"; @@ -1117,11 +1786,20 @@ "Direct messages" = "Mensajes directos"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Los mensajes directos entre miembros del grupo no están permitidos."; +"Direct messages between members are prohibited in this chat." = "Mensajes directos no permitidos entre miembros de este chat."; + +/* 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. */ "Disable (keep overrides)" = "Desactivar (conservando anulaciones)"; +/* alert title */ +"Disable automatic message deletion?" = "¿Desactivar la eliminación automática de mensajes?"; + +/* alert button */ +"Disable delete messages" = "Desactivar"; + /* No comment provided by engineer. */ "Disable for all" = "Desactivar para todos"; @@ -1131,6 +1809,9 @@ /* No comment provided by engineer. */ "disabled" = "desactivado"; +/* No comment provided by engineer. */ +"Disabled" = "Desactivado"; + /* No comment provided by engineer. */ "Disappearing message" = "Mensaje temporal"; @@ -1141,7 +1822,7 @@ "Disappearing messages are prohibited in this chat." = "Los mensajes temporales no están permitidos en este chat."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Los mensajes temporales no están permitidos en este grupo."; +"Disappearing messages are prohibited." = "Los mensajes temporales no están permitidos en este grupo."; /* No comment provided by engineer. */ "Disappears at" = "Desaparecerá"; @@ -1152,15 +1833,36 @@ /* server test step */ "Disconnect" = "Desconectar"; +/* No comment provided by engineer. */ +"Disconnect desktop?" = "¿Desconectar del ordenador?"; + /* No comment provided by engineer. */ "Discover and join groups" = "Descubre y únete a grupos"; +/* No comment provided by engineer. */ +"Discover via local network" = "Descubrir en red local"; + /* No comment provided by engineer. */ "Do it later" = "Hacer más tarde"; +/* 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 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."; + +/* No comment provided by engineer. */ +"Do not use credentials with proxy." = "No uses credenciales con proxy."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "NO usar enrutamiento privado."; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "NO uses SimpleX para llamadas de emergencia."; +/* No comment provided by engineer. */ +"Documents:" = "Documentos:"; + /* No comment provided by engineer. */ "Don't create address" = "No crear dirección SimpleX"; @@ -1168,26 +1870,63 @@ "Don't enable" = "No activar"; /* No comment provided by engineer. */ -"Don't show again" = "No mostrar de nuevo"; +"Don't miss important messages." = "No pierdas los mensajes importantes."; + +/* No comment provided by engineer. */ +"Don't show again" = "No volver a mostrar"; + +/* No comment provided by engineer. */ +"Done" = "Hecho"; /* No comment provided by engineer. */ "Downgrade and open chat" = "Degradar y abrir Chat"; +/* alert button +chat item action */ +"Download" = "Descargar"; + +/* No comment provided by engineer. */ +"Download errors" = "Errores en la descarga"; + +/* No comment provided by engineer. */ +"Download failed" = "Descarga fallida"; + /* server test step */ "Download file" = "Descargar archivo"; +/* alert action */ +"Download files" = "Descargar archivos"; + +/* No comment provided by engineer. */ +"Downloaded" = "Descargado"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Archivos descargados"; + +/* No comment provided by engineer. */ +"Downloading archive" = "Descargando archivo"; + +/* No comment provided by engineer. */ +"Downloading link details" = "Descargando detalles del enlace"; + /* No comment provided by engineer. */ "Duplicate display name!" = "¡Nombre mostrado duplicado!"; /* integrity error chat item */ "duplicate message" = "mensaje duplicado"; +/* No comment provided by engineer. */ +"duplicates" = "duplicados"; + /* No comment provided by engineer. */ "Duration" = "Duración"; /* No comment provided by engineer. */ "e2e encrypted" = "cifrado de extremo a extremo"; +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "Notificaciones cifradas E2E."; + /* chat item action */ "Edit" = "Editar"; @@ -1200,12 +1939,21 @@ /* No comment provided by engineer. */ "Enable (keep overrides)" = "Activar (conservar anulaciones)"; -/* No comment provided by engineer. */ +/* 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"; + +/* No comment provided by engineer. */ +"Enable Flux in Network & servers settings for better metadata privacy." = "Habilitar Flux en la configuración de Red y servidores para mejorar la privacidad de los metadatos."; + /* No comment provided by engineer. */ "Enable for all" = "Activar para todos"; +/* No comment provided by engineer. */ +"Enable in direct chats (BETA)!" = "¡Activar en chats directos (BETA)!"; + /* No comment provided by engineer. */ "Enable instant notifications?" = "¿Activar notificación instantánea?"; @@ -1233,6 +1981,12 @@ /* enabled status */ "enabled" = "activado"; +/* No comment provided by engineer. */ +"Enabled" = "Activado"; + +/* No comment provided by engineer. */ +"Enabled for" = "Activado para"; + /* enabled status */ "enabled for contact" = "activado para el contacto"; @@ -1246,7 +2000,7 @@ "Encrypt database?" = "¿Cifrar base de datos?"; /* No comment provided by engineer. */ -"Encrypt local files" = "Cifra archivos locales"; +"Encrypt local files" = "Cifrar archivos locales"; /* No comment provided by engineer. */ "Encrypt stored files & media" = "Cifra archivos almacenados y multimedia"; @@ -1257,6 +2011,9 @@ /* notification */ "Encrypted message or another event" = "Mensaje cifrado u otro evento"; +/* notification */ +"Encrypted message: app is stopped" = "Mensaje cifrado: la aplicación está parada"; + /* notification */ "Encrypted message: database error" = "Mensaje cifrado: error base de datos"; @@ -1290,12 +2047,21 @@ /* chat item text */ "encryption re-negotiation allowed for %@" = "renegociar el cifrado permitido para %@"; +/* message decrypt error item */ +"Encryption re-negotiation error" = "Error de renegociación de cifrado"; + +/* No comment provided by engineer. */ +"Encryption re-negotiation failed." = "Renegociación de cifrado fallida."; + /* chat item text */ "encryption re-negotiation required" = "se requiere renegociar el cifrado"; /* chat item text */ "encryption re-negotiation required for %@" = "se requiere renegociar el cifrado para %@"; +/* No comment provided by engineer. */ +"Encryption renegotiation in progress." = "Renegociación de cifrado en curso."; + /* No comment provided by engineer. */ "ended" = "finalizado"; @@ -1305,9 +2071,15 @@ /* No comment provided by engineer. */ "Enter correct passphrase." = "Introduce la contraseña correcta."; +/* No comment provided by engineer. */ +"Enter group name…" = "Nombre del grupo…"; + /* No comment provided by engineer. */ "Enter Passcode" = "Introduce Código"; +/* No comment provided by engineer. */ +"Enter passphrase" = "Introduce la frase de contraseña"; + /* No comment provided by engineer. */ "Enter passphrase…" = "Introduce la contraseña…"; @@ -1315,7 +2087,10 @@ "Enter password above to show!" = "¡Introduce la contraseña arriba para mostrar!"; /* No comment provided by engineer. */ -"Enter server manually" = "Introduce el servidor manualmente"; +"Enter server manually" = "Añadir manualmente"; + +/* No comment provided by engineer. */ +"Enter this device name…" = "Nombre de este dispositivo…"; /* placeholder */ "Enter welcome message…" = "Introduce mensaje de bienvenida…"; @@ -1323,6 +2098,9 @@ /* placeholder */ "Enter welcome message… (optional)" = "Introduce mensaje de bienvenida… (opcional)"; +/* No comment provided by engineer. */ +"Enter your name…" = "Introduce tu nombre…"; + /* No comment provided by engineer. */ "error" = "error"; @@ -1332,17 +2110,23 @@ /* No comment provided by engineer. */ "Error aborting address change" = "Error al cancelar cambio de dirección"; +/* alert title */ +"Error accepting conditions" = "Error al aceptar las condiciones"; + /* No comment provided by engineer. */ "Error accepting contact request" = "Error al aceptar solicitud del contacto"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Error al acceder al archivo de la base de datos"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Error al añadir miembro(s)"; +/* alert title */ +"Error adding server" = "Error al añadir servidor"; + /* No comment provided by engineer. */ -"Error changing address" = "Error al cambiar dirección"; +"Error changing address" = "Error al cambiar servidor"; + +/* No comment provided by engineer. */ +"Error changing connection profile" = "Error al cambiar el perfil de conexión"; /* No comment provided by engineer. */ "Error changing role" = "Error al cambiar rol"; @@ -1350,6 +2134,15 @@ /* No comment provided by engineer. */ "Error changing setting" = "Error cambiando configuración"; +/* No comment provided by engineer. */ +"Error changing to incognito!" = "¡Error al cambiar a incógnito!"; + +/* No comment provided by engineer. */ +"Error checking token status" = "Error al verificar el estado del token"; + +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "Error al conectar con el servidor de reenvío %@. Por favor, inténtalo más tarde."; + /* No comment provided by engineer. */ "Error creating address" = "Error al crear dirección"; @@ -1359,12 +2152,21 @@ /* No comment provided by engineer. */ "Error creating group link" = "Error al crear enlace de grupo"; +/* alert title */ +"Error creating list" = "Error al crear lista"; + /* No comment provided by engineer. */ "Error creating member contact" = "Error al establecer contacto con el miembro"; +/* No comment provided by engineer. */ +"Error creating message" = "Error al crear mensaje"; + /* No comment provided by engineer. */ "Error creating profile!" = "¡Error al crear perfil!"; +/* No comment provided by engineer. */ +"Error creating report" = "Error al crear informe"; + /* No comment provided by engineer. */ "Error decrypting file" = "Error al descifrar el archivo"; @@ -1377,9 +2179,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Error al eliminar conexión"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Error al eliminar contacto"; - /* No comment provided by engineer. */ "Error deleting database" = "Error al eliminar base de datos"; @@ -1392,6 +2191,9 @@ /* No comment provided by engineer. */ "Error deleting user profile" = "Error al eliminar perfil"; +/* No comment provided by engineer. */ +"Error downloading the archive" = "Error al descargar el archivo"; + /* No comment provided by engineer. */ "Error enabling delivery receipts!" = "¡Error al activar confirmaciones de entrega!"; @@ -1404,23 +2206,47 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Error al exportar base de datos"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Error al exportar tema: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Error al importar base de datos"; /* No comment provided by engineer. */ -"Error joining group" = "Error al unirse al grupo"; +"Error joining group" = "Error al unirte al grupo"; + +/* alert title */ +"Error loading servers" = "Error al cargar servidores"; /* No comment provided by engineer. */ -"Error loading %@ servers" = "Error al cargar servidores %@"; +"Error migrating settings" = "Error al migrar la configuración"; /* No comment provided by engineer. */ +"Error opening chat" = "Error al abrir chat"; + +/* alert title */ "Error receiving file" = "Error al recibir archivo"; /* No comment provided by engineer. */ -"Error removing member" = "Error al eliminar miembro"; +"Error reconnecting server" = "Error al reconectar con el servidor"; /* No comment provided by engineer. */ -"Error saving %@ servers" = "Error al guardar servidores %@"; +"Error reconnecting servers" = "Error al reconectar con los servidores"; + +/* alert title */ +"Error registering for notifications" = "Error al registrarse para notificaciones"; + +/* No comment provided by engineer. */ +"Error removing member" = "Error al expulsar miembro"; + +/* alert title */ +"Error reordering lists" = "Error al reorganizar listas"; + +/* No comment provided by engineer. */ +"Error resetting statistics" = "Error al restablecer las estadísticas"; + +/* alert title */ +"Error saving chat list" = "Error al guardar listas"; /* No comment provided by engineer. */ "Error saving group profile" = "Error al guardar perfil de grupo"; @@ -1434,9 +2260,18 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "Error al guardar contraseña en Keychain"; +/* alert title */ +"Error saving servers" = "Error al guardar servidores"; + +/* when migrating */ +"Error saving settings" = "Error al guardar ajustes"; + /* No comment provided by engineer. */ "Error saving user password" = "Error al guardar contraseña de usuario"; +/* No comment provided by engineer. */ +"Error scanning code: %@" = "Error al escanear código: %@"; + /* No comment provided by engineer. */ "Error sending email" = "Error al enviar email"; @@ -1453,20 +2288,29 @@ "Error starting chat" = "Error al iniciar Chat"; /* No comment provided by engineer. */ -"Error stopping chat" = "Error al detener Chat"; +"Error stopping chat" = "Error al parar SimpleX"; /* No comment provided by engineer. */ +"Error switching profile" = "Error al cambiar perfil"; + +/* alertTitle */ "Error switching profile!" = "¡Error al cambiar perfil!"; /* No comment provided by engineer. */ "Error synchronizing connection" = "Error al sincronizar conexión"; +/* No comment provided by engineer. */ +"Error testing server connection" = "Error al testar la conexión al servidor"; + /* No comment provided by engineer. */ "Error updating group link" = "Error al actualizar enlace de grupo"; /* No comment provided by engineer. */ "Error updating message" = "Error al actualizar mensaje"; +/* alert title */ +"Error updating server" = "Error al actualizar el servidor"; + /* No comment provided by engineer. */ "Error updating settings" = "Error al actualizar configuración"; @@ -1474,9 +2318,17 @@ "Error updating user privacy" = "Error al actualizar privacidad de usuario"; /* No comment provided by engineer. */ -"Error: " = "Error: "; +"Error uploading the archive" = "Error al subir el archivo"; /* No comment provided by engineer. */ +"Error verifying passphrase:" = "Error al verificar la frase de contraseña:"; + +/* No comment provided by engineer. */ +"Error: " = "Error: "; + +/* alert message +file error text +snd error text */ "Error: %@" = "Error: %@"; /* No comment provided by engineer. */ @@ -1485,14 +2337,26 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Error: la URL no es válida"; +/* No comment provided by engineer. */ +"Errors" = "Errores"; + +/* servers error */ +"Errors in servers configuration." = "Error en la configuración del servidor."; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Incluso si está desactivado para la conversación."; /* No comment provided by engineer. */ -"event happened" = "evento ocurrido"; +"Exit without saving" = "Salir sin guardar"; + +/* chat item action */ +"Expand" = "Expandir"; /* No comment provided by engineer. */ -"Exit without saving" = "Salir sin guardar"; +"expired" = "expirados"; + +/* token status text */ +"Expired" = "Expirado"; /* No comment provided by engineer. */ "Export database" = "Exportar base de datos"; @@ -1500,9 +2364,15 @@ /* No comment provided by engineer. */ "Export error:" = "Error al exportar:"; +/* No comment provided by engineer. */ +"Export theme" = "Exportar tema"; + /* No comment provided by engineer. */ "Exported database archive." = "Archivo de base de datos exportado."; +/* No comment provided by engineer. */ +"Exported file doesn't exist" = "El archivo exportado no existe"; + /* No comment provided by engineer. */ "Exporting database archive…" = "Exportando base de datos…"; @@ -1513,20 +2383,56 @@ "Fast and no wait until the sender is online!" = "¡Rápido y sin necesidad de esperar a que el remitente esté en línea!"; /* No comment provided by engineer. */ +"Faster deletion of groups." = "Eliminación más rápida de grupos."; + +/* No comment provided by engineer. */ +"Faster joining and more reliable messages." = "Mensajería más segura y conexión más rápida."; + +/* No comment provided by engineer. */ +"Faster sending messages." = "Envío más rápido de mensajes."; + +/* swipe action */ "Favorite" = "Favoritos"; +/* No comment provided by engineer. */ +"Favorites" = "Favoritos"; + +/* file error alert title */ +"File error" = "Error de archivo"; + +/* alert message */ +"File errors:\n%@" = "Error(es) de archivo\n%@"; + +/* file error text */ +"File is blocked by server operator:\n%@." = "Archivo bloqueado por el operador del servidor\n%@."; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Archivo no encontrado, probablemente haya sido eliminado o cancelado."; + +/* file error text */ +"File server error: %@" = "Error del servidor de archivos: %@"; + +/* No comment provided by engineer. */ +"File status" = "Estado del archivo"; + +/* copied message info */ +"File status: %@" = "Estado del archivo: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "El archivo será eliminado de los servidores."; /* No comment provided by engineer. */ -"File will be received when your contact completes uploading it." = "El archivo se recibirá cuando tu contacto termine de subirlo."; +"File will be received when your contact completes uploading it." = "El archivo se recibirá cuando el contacto termine de subirlo."; /* No comment provided by engineer. */ -"File will be received when your contact is online, please wait or check later!" = "El archivo se recibirá cuando tu contacto esté en línea, ¡por favor espera o compruébalo más tarde!"; +"File will be received when your contact is online, please wait or check later!" = "El archivo se recibirá cuando el contacto esté en línea, ¡por favor espera o revisa más tarde!"; /* No comment provided by engineer. */ "File: %@" = "Archivo: %@"; +/* No comment provided by engineer. */ +"Files" = "Archivos"; + /* No comment provided by engineer. */ "Files & media" = "Archivos y multimedia"; @@ -1534,7 +2440,10 @@ "Files and media" = "Archivos y multimedia"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "No se permiten archivos y multimedia en este grupo."; +"Files and media are prohibited." = "Los archivos y multimedia no están permitidos en este grupo."; + +/* No comment provided by engineer. */ +"Files and media not allowed" = "Archivos y multimedia no permitidos"; /* No comment provided by engineer. */ "Files and media prohibited!" = "¡Archivos y multimedia no permitidos!"; @@ -1542,6 +2451,12 @@ /* No comment provided by engineer. */ "Filter unread and favorite chats." = "Filtra chats no leídos y favoritos."; +/* No comment provided by engineer. */ +"Finalize migration" = "Finalizar migración"; + +/* No comment provided by engineer. */ +"Finalize migration on another device." = "Finalizar la migración en otro dispositivo."; + /* No comment provided by engineer. */ "Finally, we have them! 🚀" = "¡Por fin los tenemos! 🚀"; @@ -1566,9 +2481,75 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Corrección no compatible con miembro del grupo"; +/* No comment provided by engineer. */ +"For all moderators" = "Para todos los moderadores"; + +/* servers error */ +"For chat profile %@:" = "Para el perfil de chat %@:"; + /* No comment provided by engineer. */ "For console" = "Para consola"; +/* No comment provided by engineer. */ +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Si por ejemplo tu contacto recibe los mensajes a través de un servidor de SimpleX Chat, tu aplicación los entregará a través de un servidor de Flux."; + +/* No comment provided by engineer. */ +"For me" = "para mí"; + +/* No comment provided by engineer. */ +"For private routing" = "Para enrutamiento privado"; + +/* No comment provided by engineer. */ +"For social media" = "Para redes sociales"; + +/* chat item action */ +"Forward" = "Reenviar"; + +/* alert title */ +"Forward %d message(s)?" = "¿Reenviar %d mensaje(s)?"; + +/* No comment provided by engineer. */ +"Forward and save messages" = "Reenviar y guardar mensajes"; + +/* alert action */ +"Forward messages" = "Reenviar mensajes"; + +/* alert message */ +"Forward messages without files?" = "¿Reenviar mensajes sin los archivos?"; + +/* No comment provided by engineer. */ +"Forward up to 20 messages at once." = "Desplazamiento de hasta 20 mensajes."; + +/* No comment provided by engineer. */ +"forwarded" = "reenviado"; + +/* No comment provided by engineer. */ +"Forwarded" = "Reenviado"; + +/* No comment provided by engineer. */ +"Forwarded from" = "Reenviado por"; + +/* No comment provided by engineer. */ +"Forwarding %lld messages" = "Reenviando %lld mensajes"; + +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "El servidor de reenvío %@ no ha podido conectarse al servidor de destino %@. Por favor, intentalo más tarde."; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "La dirección del servidor de reenvío es incompatible con la configuración de red: %@."; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "La versión del servidor de reenvío es incompatible con la configuración de red: %@."; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Servidor de reenvío: %1$@\nError del servidor de destino: %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Servidor de reenvío: %1$@\nError: %2$@"; + +/* No comment provided by engineer. */ +"Found desktop" = "Ordenador encontrado"; + /* No comment provided by engineer. */ "French interface" = "Interfaz en francés"; @@ -1579,20 +2560,35 @@ "Full name (optional)" = "Nombre completo (opcional)"; /* No comment provided by engineer. */ -"Full name:" = "Nombre completo:"; +"Fully decentralized – visible only to members." = "Totalmente descentralizado. Visible sólo para los miembros."; /* No comment provided by engineer. */ -"Fully re-implemented - work in background!" = "Completamente reimplementado: ¡funciona en segundo plano!"; +"Fully re-implemented - work in background!" = "Totalmente revisado. ¡Funciona en segundo plano!"; /* No comment provided by engineer. */ "Further reduced battery usage" = "Reducción consumo de batería"; +/* No comment provided by engineer. */ +"Get notified when mentioned." = "Las menciones ahora se notifican."; + /* No comment provided by engineer. */ "GIFs and stickers" = "GIFs y stickers"; +/* message preview */ +"Good afternoon!" = "¡Buenas tardes!"; + +/* message preview */ +"Good morning!" = "¡Buenos días!"; + /* No comment provided by engineer. */ "Group" = "Grupo"; +/* No comment provided by engineer. */ +"Group already exists" = "El grupo ya existe"; + +/* No comment provided by engineer. */ +"Group already exists!" = "¡El grupo ya existe!"; + /* No comment provided by engineer. */ "group deleted" = "grupo eliminado"; @@ -1620,24 +2616,6 @@ /* No comment provided by engineer. */ "Group links" = "Enlaces de grupo"; -/* No comment provided by engineer. */ -"Group members can add message reactions." = "Los miembros pueden añadir reacciones a los mensajes."; - -/* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages." = "Los miembros del grupo pueden eliminar mensajes de forma irreversible."; - -/* No comment provided by engineer. */ -"Group members can send direct messages." = "Los miembros del grupo pueden enviar mensajes directos."; - -/* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Los miembros del grupo pueden enviar mensajes temporales."; - -/* No comment provided by engineer. */ -"Group members can send files and media." = "Los miembros del grupo pueden enviar archivos y multimedia."; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "Los miembros del grupo pueden enviar mensajes de voz."; - /* notification */ "Group message:" = "Mensaje de grupo:"; @@ -1645,7 +2623,7 @@ "Group moderation" = "Moderación de grupos"; /* No comment provided by engineer. */ -"Group preferences" = "Preferencias de grupo"; +"Group preferences" = "Preferencias del grupo"; /* No comment provided by engineer. */ "Group profile" = "Perfil de grupo"; @@ -1660,14 +2638,20 @@ "Group welcome message" = "Mensaje de bienvenida en grupos"; /* No comment provided by engineer. */ -"Group will be deleted for all members - this cannot be undone!" = "El grupo será eliminado para todos los miembros. ¡No podrá deshacerse!"; +"Group will be deleted for all members - this cannot be undone!" = "El grupo será eliminado para todos los miembros. ¡No puede deshacerse!"; /* No comment provided by engineer. */ -"Group will be deleted for you - this cannot be undone!" = "El grupo será eliminado para tí. ¡No podrá deshacerse!"; +"Group will be deleted for you - this cannot be undone!" = "El grupo será eliminado para tí. ¡No puede deshacerse!"; + +/* No comment provided by engineer. */ +"Groups" = "Grupos"; /* No comment provided by engineer. */ "Help" = "Ayuda"; +/* No comment provided by engineer. */ +"Help admins moderating their groups." = "Ayuda a los admins a moderar sus grupos."; + /* No comment provided by engineer. */ "Hidden" = "Oculto"; @@ -1692,10 +2676,19 @@ /* No comment provided by engineer. */ "History" = "Historial"; +/* No comment provided by engineer. */ +"History is not sent to new members." = "El historial no se envía a miembros nuevos."; + /* time unit */ "hours" = "horas"; /* No comment provided by engineer. */ +"How it affects privacy" = "Cómo afecta a la privacidad"; + +/* No comment provided by engineer. */ +"How it helps privacy" = "Cómo ayuda a la privacidad"; + +/* alert button */ "How it works" = "Cómo funciona"; /* No comment provided by engineer. */ @@ -1710,14 +2703,14 @@ /* No comment provided by engineer. */ "How to use your servers" = "Cómo usar los servidores"; +/* No comment provided by engineer. */ +"Hungarian interface" = "Interfaz en húngaro"; + /* No comment provided by engineer. */ "ICE servers (one per line)" = "Servidores ICE (uno por línea)"; /* No comment provided by engineer. */ -"If you can't meet in person, show QR code in a video call, or share the link." = "Si no puedes reunirte en persona, muestra el código QR por videollamada, o comparte el enlace."; - -/* No comment provided by engineer. */ -"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Si no puedes reunirte en persona, puedes **escanear el código QR por videollamada**, o tu contacto puede compartir un enlace de invitación."; +"If you can't meet in person, show QR code in a video call, or share the link." = "Si no puedes reunirte en persona, muestra el código QR por videollamada o comparte el enlace."; /* No comment provided by engineer. */ "If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "¡Si introduces este código al abrir la aplicación, todos los datos de la misma se eliminarán de forma irreversible!"; @@ -1732,16 +2725,16 @@ "Ignore" = "Ignorar"; /* No comment provided by engineer. */ -"Image will be received when your contact completes uploading it." = "La imagen se recibirá cuando tu contacto termine de subirla."; +"Image will be received when your contact completes uploading it." = "La imagen se recibirá cuando el contacto termine de subirla."; /* No comment provided by engineer. */ -"Image will be received when your contact is online, please wait or check later!" = "La imagen se recibirá cuando tu contacto esté en línea, ¡por favor espera o compruébalo más tarde!"; +"Image will be received when your contact is online, please wait or check later!" = "La imagen se recibirá cuando el contacto esté en línea, ¡por favor espera o revisa más tarde!"; /* No comment provided by engineer. */ "Immediately" = "Inmediatamente"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Inmune a spam y abuso"; +"Immune to spam" = "Inmune a spam y abuso"; /* No comment provided by engineer. */ "Import" = "Importar"; @@ -1752,18 +2745,51 @@ /* No comment provided by engineer. */ "Import database" = "Importar base de datos"; +/* No comment provided by engineer. */ +"Import failed" = "Error de importación"; + +/* No comment provided by engineer. */ +"Import theme" = "Importar tema"; + +/* No comment provided by engineer. */ +"Importing archive" = "Importando archivo"; + +/* No comment provided by engineer. */ +"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "Reducción del tráfico y entrega mejorada.\n¡Pronto habrá nuevas mejoras!"; + +/* No comment provided by engineer. */ +"Improved message delivery" = "Entrega de mensajes mejorada"; + /* No comment provided by engineer. */ "Improved privacy and security" = "Seguridad y privacidad mejoradas"; /* No comment provided by engineer. */ "Improved server configuration" = "Configuración del servidor mejorada"; +/* No comment provided by engineer. */ +"In order to continue, chat should be stopped." = "Para continuar, SimpleX debe estar parado."; + /* No comment provided by engineer. */ "In reply to" = "En respuesta a"; +/* No comment provided by engineer. */ +"In-call sounds" = "Sonido de llamada"; + +/* No comment provided by engineer. */ +"inactive" = "inactivo"; + +/* report reason */ +"Inappropriate content" = "Contenido inapropiado"; + +/* report reason */ +"Inappropriate profile" = "Perfil inapropiado"; + /* No comment provided by engineer. */ "Incognito" = "Incógnito"; +/* No comment provided by engineer. */ +"Incognito groups" = "Grupos incógnito"; + /* No comment provided by engineer. */ "Incognito mode" = "Modo incógnito"; @@ -1791,6 +2817,9 @@ /* No comment provided by engineer. */ "Incompatible database version" = "Versión de base de datos incompatible"; +/* No comment provided by engineer. */ +"Incompatible version" = "Versión incompatible"; + /* PIN entry */ "Incorrect passcode" = "Código de acceso incorrecto"; @@ -1809,14 +2838,32 @@ /* 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)"; +/* No comment provided by engineer. */ +"Instant" = "Al instante"; + /* No comment provided by engineer. */ "Instant push notifications will be hidden!\n" = "¡Las notificaciones automáticas estarán ocultas!\n"; /* No comment provided by engineer. */ -"Instantly" = "Al instante"; +"Interface" = "Interfaz"; /* No comment provided by engineer. */ -"Interface" = "Interfaz"; +"Interface colors" = "Colores del interfaz"; + +/* token status text */ +"Invalid" = "No válido"; + +/* token status text */ +"Invalid (bad token)" = "No válido (token incorrecto)"; + +/* token status text */ +"Invalid (expired)" = "No válido (expirado)"; + +/* token status text */ +"Invalid (unregistered)" = "No válido (no registrado)"; + +/* token status text */ +"Invalid (wrong topic)" = "No válido (tópico incorrecto)"; /* invalid chat data */ "invalid chat" = "chat no válido"; @@ -1831,6 +2878,24 @@ "invalid data" = "datos no válidos"; /* No comment provided by engineer. */ +"Invalid display name!" = "¡Nombre mostrado no válido!"; + +/* No comment provided by engineer. */ +"Invalid link" = "Enlace no válido"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Confirmación de migración no válida"; + +/* No comment provided by engineer. */ +"Invalid name!" = "¡Nombre no válido!"; + +/* No comment provided by engineer. */ +"Invalid QR code" = "Código QR no válido"; + +/* No comment provided by engineer. */ +"Invalid response" = "Respuesta no válida"; + +/* alert title */ "Invalid server address!" = "¡Dirección de servidor no válida!"; /* item status text */ @@ -1842,12 +2907,18 @@ /* group name */ "invitation to group %@" = "invitación al grupo %@"; +/* No comment provided by engineer. */ +"invite" = "Invitar"; + /* No comment provided by engineer. */ "Invite friends" = "Invitar amigos"; /* No comment provided by engineer. */ "Invite members" = "Invitar miembros"; +/* No comment provided by engineer. */ +"Invite to chat" = "Invitar al chat"; + /* No comment provided by engineer. */ "Invite to group" = "Invitar al grupo"; @@ -1858,7 +2929,7 @@ "invited %@" = "ha invitado a %@"; /* chat list item title */ -"invited to connect" = "invitado a conectarse"; +"invited to connect" = "invitación a conectarse"; /* rcv group event chat item */ "invited via your group link" = "se ha unido mediante tu enlace de grupo"; @@ -1869,6 +2940,9 @@ /* 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 se usará para almacenar la contraseña de forma segura después de reiniciar la aplicación o cambiar la contraseña. Esto permitirá recibir notificaciones automáticas."; +/* No comment provided by engineer. */ +"IP address" = "Dirección IP"; + /* No comment provided by engineer. */ "Irreversible message deletion" = "Eliminación irreversible del mensaje"; @@ -1876,7 +2950,7 @@ "Irreversible message deletion is prohibited in this chat." = "La eliminación irreversible de mensajes no está permitida en este chat."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "La eliminación irreversible de mensajes no está permitida en este grupo."; +"Irreversible message deletion is prohibited." = "La eliminación irreversible de mensajes no está permitida en este grupo."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Permite tener varias conexiones anónimas sin datos compartidos entre estas dentro del mismo perfil."; @@ -1885,7 +2959,10 @@ "It can happen when you or your connection used the old database backup." = "Puede ocurrir cuando tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos."; /* No comment provided by engineer. */ -"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Esto puede suceder cuando:\n1. Los mensajes caducan en el cliente saliente tras 2 días o en el servidor tras 30 días.\n2. El descifrado ha fallado porque tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos.\n3. La conexión ha sido comprometida."; +"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." = "Esto puede suceder cuando:\n1. Los mensajes caducan tras 2 días en el cliente saliente o tras 30 días en el servidor.\n2. El descifrado ha fallado porque tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos.\n3. La conexión ha sido comprometida."; + +/* No comment provided by engineer. */ +"It protects your IP address and connections." = "Protege tu dirección IP y tus conexiones."; /* 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 (%@)." = "Parece que ya estás conectado mediante este enlace. Si no es así ha habido un error (%@)."; @@ -1899,21 +2976,45 @@ /* No comment provided by engineer. */ "Japanese interface" = "Interfáz en japonés"; -/* No comment provided by engineer. */ -"Join" = "Únete"; +/* swipe action */ +"Join" = "Unirte"; /* No comment provided by engineer. */ "join as %@" = "unirte como %@"; /* No comment provided by engineer. */ -"Join group" = "Únete al grupo"; +"Join group" = "Unirte al grupo"; /* No comment provided by engineer. */ -"Join incognito" = "Únete en modo incógnito"; +"Join group conversations" = "Unirse a la conversación del grupo"; + +/* No comment provided by engineer. */ +"Join group?" = "¿Unirte al grupo?"; + +/* No comment provided by engineer. */ +"Join incognito" = "Unirte en modo incógnito"; + +/* No comment provided by engineer. */ +"Join with current profile" = "Unirte con el perfil actual"; + +/* No comment provided by engineer. */ +"Join your group?\nThis is your link for group %@!" = "¿Unirse a tu grupo?\n¡Este es tu enlace para el grupo %@!"; /* No comment provided by engineer. */ "Joining group" = "Entrando al grupo"; +/* alert action */ +"Keep" = "Guardar"; + +/* No comment provided by engineer. */ +"Keep conversation" = "Conservar conversación"; + +/* No comment provided by engineer. */ +"Keep the app open to use it from desktop" = "Mantén la aplicación abierta para usarla desde el ordenador"; + +/* alert title */ +"Keep unused invitation?" = "¿Guardar invitación no usada?"; + /* No comment provided by engineer. */ "Keep your connections" = "Conserva tus conexiones"; @@ -1929,9 +3030,15 @@ /* No comment provided by engineer. */ "Learn more" = "Más información"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Salir"; +/* No comment provided by engineer. */ +"Leave chat" = "Salir del chat"; + +/* No comment provided by engineer. */ +"Leave chat?" = "¿Salir del chat?"; + /* No comment provided by engineer. */ "Leave group" = "Salir del grupo"; @@ -1950,6 +3057,24 @@ /* No comment provided by engineer. */ "Limitations" = "Limitaciones"; +/* No comment provided by engineer. */ +"Link mobile and desktop apps! 🔗" = "¡Enlazar aplicación móvil con ordenador! 🔗"; + +/* No comment provided by engineer. */ +"Linked desktop options" = "Opciones ordenador enlazado"; + +/* No comment provided by engineer. */ +"Linked desktops" = "Ordenadores enlazados"; + +/* swipe action */ +"List" = "Lista"; + +/* No comment provided by engineer. */ +"List name and emoji should be different for all lists." = "El nombre y el emoji deben ser diferentes en todas las listas."; + +/* No comment provided by engineer. */ +"List name..." = "Nombre de la lista..."; + /* No comment provided by engineer. */ "LIVE" = "EN VIVO"; @@ -1963,7 +3088,7 @@ "Local name" = "Nombre local"; /* No comment provided by engineer. */ -"Local profile data only" = "Sólo datos del perfil local"; +"Local profile data only" = "Eliminar sólo el perfil"; /* No comment provided by engineer. */ "Lock after" = "Bloquear en"; @@ -1971,24 +3096,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Modo bloqueo"; -/* No comment provided by engineer. */ -"Make a private connection" = "Establecer una conexión privada"; - /* No comment provided by engineer. */ "Make one message disappear" = "Escribir un mensaje temporal"; /* No comment provided by engineer. */ "Make profile private!" = "¡Hacer perfil privado!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Asegúrate de que las direcciones del servidor %@ tienen el formato correcto, están separadas por líneas y no duplicadas (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Asegúrate de que las direcciones del servidor WebRTC ICE tienen el formato correcto, están separadas por líneas y no duplicadas."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Muchos se preguntarán: *si SimpleX no tiene identificadores de usuario, ¿cómo puede entregar los mensajes?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Marcar como eliminado para todos"; @@ -2007,23 +3123,77 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "Máximo 30 segundos, recibido al instante."; +/* No comment provided by engineer. */ +"Media & file servers" = "Servidores de archivos y multimedia"; + +/* blur media */ +"Medium" = "Medio"; + /* member role */ "member" = "miembro"; /* No comment provided by engineer. */ "Member" = "Miembro"; +/* profile update event chat item */ +"member %@ changed to %@" = "el miembro %1$@ ha cambiado a %2$@"; + /* rcv group event chat item */ "member connected" = "conectado"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "El rol de miembro cambiará a \"%@\". Todos los miembros del grupo serán notificados."; +/* item status text */ +"Member inactive" = "Miembro inactivo"; + +/* chat feature */ +"Member reports" = "Informes de miembros"; /* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "El rol del miembro cambiará a \"%@\". El miembro recibirá una invitación nueva."; +"Member role will be changed to \"%@\". All chat members will be notified." = "El rol del miembro cambiará a \"%@\" y todos serán notificados."; /* No comment provided by engineer. */ -"Member will be removed from group - this cannot be undone!" = "El miembro será expulsado del grupo. ¡No podrá deshacerse!"; +"Member role will be changed to \"%@\". All group members will be notified." = "El rol del miembro cambiará a \"%@\" y se notificará al grupo."; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". The member will receive a new invitation." = "El rol del miembro cambiará a \"%@\" y recibirá una invitación nueva."; + +/* No comment provided by engineer. */ +"Member will be removed from chat - this cannot be undone!" = "El miembro será eliminado del chat. ¡No puede deshacerse!"; + +/* No comment provided by engineer. */ +"Member will be removed from group - this cannot be undone!" = "El miembro será expulsado del grupo. ¡No puede deshacerse!"; + +/* 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 irreversibly delete sent messages. (24 hours)" = "Los miembros del grupo pueden eliminar mensajes de forma irreversible. (24 horas)"; + +/* No comment provided by engineer. */ +"Members can report messsages to moderators." = "Los miembros pueden informar de mensajes a los moderadores."; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Los miembros del grupo pueden enviar mensajes directos."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Los miembros del grupo pueden enviar mensajes temporales."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Los miembros del grupo pueden enviar archivos y multimedia."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "Los miembros del grupo pueden enviar enlaces SimpleX."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Los miembros del grupo pueden enviar mensajes de voz."; + +/* No comment provided by engineer. */ +"Mention members 👋" = "Menciona a miembros 👋"; + +/* No comment provided by engineer. */ +"Menus" = "Menus"; + +/* No comment provided by engineer. */ +"message" = "mensaje"; /* item status text */ "Message delivery error" = "Error en la entrega del mensaje"; @@ -2031,33 +3201,108 @@ /* No comment provided by engineer. */ "Message delivery receipts!" = "¡Confirmación de entrega de mensajes!"; +/* item status text */ +"Message delivery warning" = "Aviso de entrega de mensaje"; + /* No comment provided by engineer. */ "Message draft" = "Borrador de mensaje"; +/* item status text */ +"Message forwarded" = "Mensaje reenviado"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "El mensaje podría ser entregado más tarde si el miembro vuelve a estar activo."; + +/* No comment provided by engineer. */ +"Message queue info" = "Información cola de mensajes"; + /* chat feature */ -"Message reactions" = "Reacciones a los mensajes"; +"Message reactions" = "Reacciones a mensajes"; /* No comment provided by engineer. */ "Message reactions are prohibited in this chat." = "Las reacciones a los mensajes no están permitidas en este chat."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Las reacciones a los mensajes no están permitidas en este grupo."; +"Message reactions are prohibited." = "Las reacciones a los mensajes no están permitidas en este grupo."; /* notification */ "message received" = "mensaje recibido"; +/* No comment provided by engineer. */ +"Message reception" = "Recepción de mensaje"; + +/* No comment provided by engineer. */ +"Message servers" = "Servidores de mensajes"; + +/* No comment provided by engineer. */ +"Message shape" = "Forma del mensaje"; + +/* No comment provided by engineer. */ +"Message source remains private." = "El autor del mensaje se mantiene privado."; + +/* No comment provided by engineer. */ +"Message status" = "Estado del mensaje"; + +/* copied message info */ +"Message status: %@" = "Estado del mensaje: %@"; + /* No comment provided by engineer. */ "Message text" = "Contacto y texto"; +/* No comment provided by engineer. */ +"Message too large" = "Mensaje demasiado largo"; + /* No comment provided by engineer. */ "Messages" = "Mensajes"; /* No comment provided by engineer. */ "Messages & files" = "Mensajes"; +/* No comment provided by engineer. */ +"Messages from %@ will be shown!" = "¡Los mensajes de %@ serán mostrados!"; + +/* alert message */ +"Messages in this chat will never be deleted." = "Los mensajes de esta conversación nunca se eliminan."; + +/* No comment provided by engineer. */ +"Messages received" = "Mensajes recibidos"; + +/* No comment provided by engineer. */ +"Messages sent" = "Mensajes enviados"; + +/* alert message */ +"Messages were deleted after you selected them." = "Los mensajes han sido eliminados después de seleccionarlos."; + +/* 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." = "Los mensajes, archivos y llamadas están protegidos mediante **cifrado de extremo a extremo** con secreto perfecto hacía adelante, repudio y recuperación tras ataque."; + +/* 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." = "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"; + +/* No comment provided by engineer. */ +"Migrate from another device" = "Migrar desde otro dispositivo"; + +/* No comment provided by engineer. */ +"Migrate here" = "Migrar aquí"; + +/* No comment provided by engineer. */ +"Migrate to another device" = "Migrar a otro dispositivo"; + +/* No comment provided by engineer. */ +"Migrate to another device via QR code." = "Migrar a otro dispositivo mediante código QR."; + +/* No comment provided by engineer. */ +"Migrating" = "Migrando"; + /* No comment provided by engineer. */ "Migrating database archive…" = "Migrando base de datos…"; +/* No comment provided by engineer. */ +"Migration complete" = "Migración completada"; + /* No comment provided by engineer. */ "Migration error:" = "Error de migración:"; @@ -2068,7 +3313,7 @@ "Migration is completed" = "Migración completada"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migraciones: %@"; +"Migrations:" = "Migraciones:"; /* time unit */ "minutes" = "minutos"; @@ -2088,63 +3333,102 @@ /* copied message info */ "Moderated at: %@" = "Moderado: %@"; -/* No comment provided by engineer. */ +/* marked deleted chat item preview text */ "moderated by %@" = "moderado por %@"; +/* member role */ +"moderator" = "moderador"; + /* time unit */ "months" = "meses"; +/* swipe action */ +"More" = "Más"; + /* No comment provided by engineer. */ "More improvements are coming soon!" = "¡Pronto habrá más mejoras!"; +/* No comment provided by engineer. */ +"More reliable network connection." = "Conexión de red más fiable."; + +/* No comment provided by engineer. */ +"More reliable notifications" = "Notificaciones más fiables"; + /* item status description */ "Most likely this connection is deleted." = "Probablemente la conexión ha sido eliminada."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Lo más probable es que este contacto haya eliminado la conexión contigo."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Múltiples perfiles"; -/* No comment provided by engineer. */ +/* notification label action */ "Mute" = "Silenciar"; +/* notification label action */ +"Mute all" = "Silenciar todo"; + /* No comment provided by engineer. */ "Muted when inactive!" = "¡Silenciado cuando está inactivo!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Nombre"; /* No comment provided by engineer. */ "Network & servers" = "Servidores y Redes"; +/* No comment provided by engineer. */ +"Network connection" = "Conexión de red"; + +/* No comment provided by engineer. */ +"Network decentralization" = "Descentralización de la 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."; + +/* No comment provided by engineer. */ +"Network management" = "Gestión de la red"; + +/* No comment provided by engineer. */ +"Network operator" = "Operador de red"; + /* No comment provided by engineer. */ "Network settings" = "Configuración de red"; /* No comment provided by engineer. */ "Network status" = "Estado de la red"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "nunca"; +/* token status text */ +"New" = "Nuevo"; + +/* No comment provided by engineer. */ +"New chat" = "Nuevo chat"; + +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Nueva experiencia de chat 🎉"; + /* notification */ "New contact request" = "Nueva solicitud de contacto"; /* notification */ "New contact:" = "Contacto nuevo:"; -/* No comment provided by engineer. */ -"New database archive" = "Nuevo archivo de bases de datos"; - /* No comment provided by engineer. */ "New desktop app!" = "Nueva aplicación para PC!"; /* No comment provided by engineer. */ "New display name" = "Nuevo nombre mostrado"; +/* notification */ +"New events" = "Eventos nuevos"; + /* No comment provided by engineer. */ "New in %@" = "Nuevo en %@"; +/* No comment provided by engineer. */ +"New media options" = "Nuevas opciones multimedia"; + /* No comment provided by engineer. */ "New member role" = "Nuevo rol de miembro"; @@ -2160,6 +3444,15 @@ /* No comment provided by engineer. */ "New passphrase…" = "Contraseña nueva…"; +/* No comment provided by engineer. */ +"New server" = "Servidor nuevo"; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used every time you start the app." = "Se usarán credenciales SOCKS nuevas cada vez que inicies la aplicación."; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used for each server." = "Se usarán credenciales SOCKS nuevas para cada servidor."; + /* pref value */ "no" = "no"; @@ -2169,6 +3462,15 @@ /* Authentication unavailable */ "No app password" = "Sin contraseña de la aplicación"; +/* No comment provided by engineer. */ +"No chats" = "Sin chats"; + +/* No comment provided by engineer. */ +"No chats found" = "Ningún chat encontrado"; + +/* No comment provided by engineer. */ +"No chats in list %@" = "Sin chats en la lista %@"; + /* No comment provided by engineer. */ "No contacts selected" = "Ningún contacto seleccionado"; @@ -2181,6 +3483,9 @@ /* No comment provided by engineer. */ "No device token!" = "¡Sin dispositivo token!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Aún no hay conexión directa con este miembro, el mensaje es reenviado por el administrador."; + /* No comment provided by engineer. */ "no e2e encryption" = "sin cifrar"; @@ -2193,21 +3498,87 @@ /* No comment provided by engineer. */ "No history" = "Sin historial"; +/* No comment provided by engineer. */ +"No info, try to reload" = "No hay información, intenta recargar"; + +/* servers error */ +"No media & file servers." = "Sin servidores para archivos y multimedia."; + +/* No comment provided by engineer. */ +"No message" = "Ningún mensaje"; + +/* servers error */ +"No message servers." = "Sin servidores para mensajes."; + +/* No comment provided by engineer. */ +"No network connection" = "Sin conexión de red"; + +/* No comment provided by engineer. */ +"No permission to record speech" = "Sin permiso para grabación de voz"; + +/* No comment provided by engineer. */ +"No permission to record video" = "Sin permiso para grabación de vídeo"; + /* No comment provided by engineer. */ "No permission to record voice message" = "Sin permiso para grabar mensajes de voz"; +/* No comment provided by engineer. */ +"No push server" = "Sin servidores push"; + /* No comment provided by engineer. */ "No received or sent files" = "Sin archivos recibidos o enviados"; +/* servers error */ +"No servers for private message routing." = "Sin servidores para enrutamiento privado."; + +/* servers error */ +"No servers to receive files." = "Sin servidores para recibir archivos."; + +/* servers error */ +"No servers to receive messages." = "Sin servidores para recibir mensajes."; + +/* servers error */ +"No servers to send files." = "Sin servidores para enviar archivos."; + /* copied message info in history */ "no text" = "sin texto"; +/* alert title */ +"No token!" = "¡Sin token!"; + +/* No comment provided by engineer. */ +"No unread chats" = "Ningún chat sin leer"; + +/* No comment provided by engineer. */ +"No user identifiers." = "Sin identificadores de usuario."; + +/* No comment provided by engineer. */ +"Not compatible!" = "¡No compatible!"; + +/* No comment provided by engineer. */ +"Notes" = "Notas"; + +/* No comment provided by engineer. */ +"Nothing selected" = "Nada seleccionado"; + +/* alert title */ +"Nothing to forward!" = "¡Nada para reenviar!"; + /* No comment provided by engineer. */ "Notifications" = "Notificaciones"; /* No comment provided by engineer. */ "Notifications are disabled!" = "¡Las notificaciones están desactivadas!"; +/* alert title */ +"Notifications error" = "Error en notificaciones"; + +/* No comment provided by engineer. */ +"Notifications privacy" = "Privacidad en las notificaciones"; + +/* alert title */ +"Notifications status" = "Estado notificaciones"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Ahora los administradores pueden:\n- eliminar mensajes de los miembros.\n- desactivar el rol miembro (a rol \"observador\")"; @@ -2215,51 +3586,54 @@ "observer" = "observador"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "desactivado"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Desactivado"; -/* No comment provided by engineer. */ -"Off (Local)" = "Desactivado (Local)"; - /* feature offered item */ "offered %@" = "ofrecido %@"; /* feature offered item */ "offered %@: %@" = "ofrecido %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Ok"; /* No comment provided by engineer. */ -"Old database" = "Base de datos antigua"; +"OK" = "OK"; /* No comment provided by engineer. */ -"Old database archive" = "Archivo de bases de datos antiguas"; +"Old database" = "Base de datos antigua"; /* group pref value */ "on" = "Activado"; /* No comment provided by engineer. */ -"One-time invitation link" = "Enlace único de invitación de un uso"; +"One-time invitation link" = "Enlace de invitación de un solo uso"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Se requieren hosts .onion para la conexión. Requiere activación de la VPN."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Se **requieren** hosts .onion para la conexión.\nRequiere activación de la VPN."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Se usarán hosts .onion si están disponibles. Requiere activación de la VPN."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Se usarán hosts .onion si están disponibles.\nRequiere activación de la VPN."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "No se usarán hosts .onion."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes enviados con **cifrado de extremo a extremo de 2 capas**."; +"Only chat owners can change preferences." = "Sólo los propietarios del chat pueden cambiar las preferencias."; /* No comment provided by engineer. */ -"Only group owners can change group preferences." = "Sólo los propietarios pueden modificar las preferencias de grupo."; +"Only client devices store user profiles, contacts, groups, and messages." = "Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes enviados con **cifrado de extremo a extremo de 2 capas**."; + +/* No comment provided by engineer. */ +"Only delete conversation" = "Eliminar sólo la conversación"; + +/* No comment provided by engineer. */ +"Only group owners can change group preferences." = "Sólo los propietarios pueden modificar las preferencias del grupo."; /* No comment provided by engineer. */ "Only group owners can enable files and media." = "Sólo los propietarios del grupo pueden activar los archivos y multimedia."; @@ -2267,11 +3641,17 @@ /* No comment provided by engineer. */ "Only group owners can enable voice messages." = "Sólo los propietarios del grupo pueden activar los mensajes de voz."; +/* No comment provided by engineer. */ +"Only sender and moderators see it" = "Solo el remitente y el moderador pueden verlo"; + +/* No comment provided by engineer. */ +"Only you and moderators see it" = "Solo tú y los moderadores podéis verlo"; + /* No comment provided by engineer. */ "Only you can add message reactions." = "Sólo tú puedes añadir reacciones a los mensajes."; /* No comment provided by engineer. */ -"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "Sólo tú puedes eliminar mensajes de forma irreversible (tu contacto puede marcarlos para eliminar)."; +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Sólo tú puedes eliminar mensajes de forma irreversible (tu contacto puede marcarlos para eliminar). (24 horas)"; /* No comment provided by engineer. */ "Only you can make calls." = "Solo tú puedes realizar llamadas."; @@ -2286,7 +3666,7 @@ "Only your contact can add message reactions." = "Sólo tu contacto puede añadir reacciones a los mensajes."; /* No comment provided by engineer. */ -"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Sólo tu contacto puede eliminar mensajes de forma irreversible (tu puedes marcarlos para eliminar)."; +"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Sólo tu contacto puede eliminar mensajes de forma irreversible (tu puedes marcarlos para eliminar). (24 horas)"; /* No comment provided by engineer. */ "Only your contact can make calls." = "Sólo tu contacto puede realizar llamadas."; @@ -2297,9 +3677,12 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Sólo tu contacto puede enviar mensajes de voz."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Abrir"; +/* No comment provided by engineer. */ +"Open changes" = "Abrir cambios"; + /* No comment provided by engineer. */ "Open chat" = "Abrir chat"; @@ -2307,20 +3690,65 @@ "Open chat console" = "Abrir consola de Chat"; /* No comment provided by engineer. */ -"Open Settings" = "Abrir Configuración"; +"Open conditions" = "Abrir condiciones"; + +/* No comment provided by engineer. */ +"Open group" = "Grupo abierto"; /* authentication reason */ -"Open user profiles" = "Abrir perfil de usuario"; +"Open migration to another device" = "Abrir menú migración a otro dispositivo"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Protocolo y código abiertos: cualquiera puede usar los servidores."; +"Open Settings" = "Abrir Configuración"; /* No comment provided by engineer. */ -"Opening database…" = "Abriendo base de datos…"; +"Opening app…" = "Iniciando aplicación…"; + +/* No comment provided by engineer. */ +"Operator" = "Operador"; + +/* alert title */ +"Operator server" = "Servidor del operador"; + +/* No comment provided by engineer. */ +"Or import archive file" = "O importa desde un archivo"; + +/* No comment provided by engineer. */ +"Or paste archive link" = "O pegar enlace del archivo"; + +/* No comment provided by engineer. */ +"Or scan QR code" = "O escanea el código QR"; + +/* 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 this code" = "O muestra el código QR"; + +/* No comment provided by engineer. */ +"Or to share privately" = "O para compartir en privado"; + +/* No comment provided by engineer. */ +"Organize chats into lists" = "Organiza tus chats en listas"; + +/* No comment provided by engineer. */ +"other" = "otros"; + +/* No comment provided by engineer. */ +"Other" = "Otro"; + +/* No comment provided by engineer. */ +"other errors" = "otros errores"; + +/* alert message */ +"Other file errors:\n%@" = "Otro(s) error(es) de archivo.\n%@"; /* member role */ "owner" = "propietario"; +/* feature role */ +"owners" = "propietarios"; + /* No comment provided by engineer. */ "Passcode" = "Código de acceso"; @@ -2337,41 +3765,65 @@ "Passcode set!" = "¡Código de acceso guardado!"; /* No comment provided by engineer. */ -"Password to show" = "Contraseña para hacerlo visible"; +"Password" = "Contraseña"; /* No comment provided by engineer. */ -"Paste" = "Pegar"; +"Password to show" = "Contraseña para hacerlo visible"; + +/* past/unknown group member */ +"Past member %@" = "Miembro pasado %@"; + +/* No comment provided by engineer. */ +"Paste desktop address" = "Pegar dirección de ordenador"; /* No comment provided by engineer. */ "Paste image" = "Pegar imagen"; /* No comment provided by engineer. */ -"Paste received link" = "Pegar enlace recibido"; +"Paste link to connect!" = "Pegar enlace para conectar!"; -/* placeholder */ -"Paste the link you received to connect with your contact." = "Pega el enlace que has recibido en el recuadro para conectar con tu contacto."; +/* No comment provided by engineer. */ +"Paste the link you received" = "Pega el enlace recibido"; /* No comment provided by engineer. */ "peer-to-peer" = "p2p"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Las personas pueden conectarse contigo solo mediante los enlaces que compartes."; +"pending" = "pendiente"; /* No comment provided by engineer. */ -"Periodically" = "Periódico"; +"Pending" = "Pendientes"; + +/* No comment provided by engineer. */ +"pending approval" = "pendiente de aprobación"; + +/* No comment provided by engineer. */ +"Periodic" = "Periódicamente"; /* message decrypt error item */ "Permanent decryption error" = "Error permanente descifrado"; +/* No comment provided by engineer. */ +"Picture-in-picture calls" = "Llamadas picture-in-picture"; + /* No comment provided by engineer. */ "PING count" = "Contador PING"; /* No comment provided by engineer. */ "PING interval" = "Intervalo PING"; +/* No comment provided by engineer. */ +"Play from the chat list." = "Reproduce desde la lista de chats."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Por favor, pide a tu contacto que active las llamadas."; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Solicita que tu contacto habilite el envío de mensajes de voz."; +/* No comment provided by engineer. */ +"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." = "Comprueba que el móvil y el ordenador están conectados a la misma red local y que el cortafuegos del ordenador permite la conexión.\nPor favor, comparte cualquier otro problema con los desarrolladores."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Comprueba que has usado el enlace correcto o pide a tu contacto que te envíe otro."; @@ -2382,7 +3834,13 @@ "Please check yours and your contact preferences." = "Comprueba tus preferencias y las de tu contacto."; /* No comment provided by engineer. */ -"Please contact group admin." = "Póngase en contacto con el administrador del grupo."; +"Please confirm that network settings are correct for this device." = "Por favor, confirma que la configuración de red es correcta para este dispositivo."; + +/* No comment provided by engineer. */ +"Please contact developers.\nError: %@" = "Por favor, contacta con los desarrolladores.\nError: %@"; + +/* No comment provided by engineer. */ +"Please contact group admin." = "Ponte en contacto con el administrador del grupo."; /* No comment provided by engineer. */ "Please enter correct current passphrase." = "Introduce la contraseña actual correcta."; @@ -2405,43 +3863,91 @@ /* No comment provided by engineer. */ "Please store passphrase securely, you will NOT be able to change it if you lose it." = "Guarda la contraseña de forma segura, NO podrás cambiarla si la pierdes."; +/* token info */ +"Please try to disable and re-enable notfications." = "Por favor, intenta desactivar y reactivar las notificaciones."; + +/* token info */ +"Please wait for token activation to complete." = "Por favor, espera a que el token de activación se complete."; + +/* token info */ +"Please wait for token to be registered." = "Por favor, espera a que el token se registre."; + /* No comment provided by engineer. */ "Polish interface" = "Interfaz en polaco"; +/* No comment provided by engineer. */ +"Port" = "Puerto"; + /* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Posiblemente la huella digital del certificado en la dirección del servidor es incorrecta"; +"Possibly, certificate fingerprint in server address is incorrect" = "Posiblemente la huella del certificado en la dirección del servidor es incorrecta"; /* 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 server" = "Servidor predefinido"; +"Preset server address" = "Dirección predefinida del servidor"; /* No comment provided by engineer. */ -"Preset server address" = "Dirección del servidor predefinida"; +"Preset servers" = "Servidores predefinidos"; /* No comment provided by engineer. */ "Preview" = "Vista previa"; /* No comment provided by engineer. */ -"Privacy & security" = "Privacidad y Seguridad"; +"Previously connected servers" = "Servidores conectados previamente"; + +/* No comment provided by engineer. */ +"Privacy & security" = "Seguridad y Privacidad"; + +/* No comment provided by engineer. */ +"Privacy for your customers." = "Privacidad para tus clientes."; + +/* No comment provided by engineer. */ +"Privacy policy and conditions of use." = "Política de privacidad y condiciones de uso."; /* No comment provided by engineer. */ "Privacy redefined" = "Privacidad redefinida"; +/* 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."; + /* No comment provided by engineer. */ "Private filenames" = "Nombres de archivos privados"; /* No comment provided by engineer. */ -"Profile and server connections" = "Perfil y conexiones de servidor"; +"Private media file names." = "Nombres privados en archivos de media."; + +/* No comment provided by engineer. */ +"Private message routing" = "Enrutamiento privado de mensajes"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Enrutamiento privado de mensajes 🚀"; + +/* name of notes to self */ +"Private notes" = "Notas privadas"; + +/* No comment provided by engineer. */ +"Private routing" = "Enrutamiento privado"; + +/* No comment provided by engineer. */ +"Private routing error" = "Error de enrutamiento privado"; + +/* No comment provided by engineer. */ +"Profile and server connections" = "Eliminar perfil y conexiones"; /* No comment provided by engineer. */ "Profile image" = "Imagen del perfil"; +/* No comment provided by engineer. */ +"Profile images" = "Forma de los perfiles"; + /* No comment provided by engineer. */ "Profile password" = "Contraseña del perfil"; /* No comment provided by engineer. */ +"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."; /* No comment provided by engineer. */ @@ -2456,6 +3962,9 @@ /* No comment provided by engineer. */ "Prohibit messages reactions." = "No se permiten reacciones a los mensajes."; +/* No comment provided by engineer. */ +"Prohibit reporting messages to moderators." = "No se permite informar de mensajes a los moderadores."; + /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "No se permiten mensajes directos entre miembros."; @@ -2465,51 +3974,84 @@ /* No comment provided by engineer. */ "Prohibit sending files and media." = "No permitir el envío de archivos y multimedia."; +/* No comment provided by engineer. */ +"Prohibit sending SimpleX links." = "No se permite enviar enlaces SimpleX."; + /* No comment provided by engineer. */ "Prohibit sending voice messages." = "No se permiten mensajes de voz."; /* No comment provided by engineer. */ -"Protect app screen" = "Proteger la pantalla de la aplicación"; +"Protect app screen" = "Proteger la pantalla"; + +/* No comment provided by engineer. */ +"Protect IP address" = "Proteger dirección IP"; /* No comment provided by engineer. */ "Protect your chat profiles with a password!" = "¡Protege tus perfiles con contraseña!"; /* No comment provided by engineer. */ -"Protocol timeout" = "Tiempo de espera del protocolo"; +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Protege tu dirección IP de los servidores de retransmisión elegidos por tus contactos.\nActívalo en ajustes de *Servidores y Redes*."; /* No comment provided by engineer. */ -"Protocol timeout per KB" = "Límite de espera del protocolo por KB"; +"Protocol timeout" = "Timeout protocolo"; /* No comment provided by engineer. */ -"Push notifications" = "Notificaciones automáticas"; +"Protocol timeout per KB" = "Timeout protocolo por KB"; + +/* No comment provided by engineer. */ +"Proxied" = "Como proxy"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Servidores con proxy"; + +/* No comment provided by engineer. */ +"Proxy requires password" = "El proxy requiere contraseña"; + +/* No comment provided by engineer. */ +"Push notifications" = "Notificaciones push"; + +/* No comment provided by engineer. */ +"Push server" = "Servidor push"; + +/* chat item text */ +"quantum resistant e2e encryption" = "cifrado e2e resistente a tecnología cuántica"; + +/* No comment provided by engineer. */ +"Quantum resistant encryption" = "Cifrado resistente a tecnología cuántica"; /* No comment provided by engineer. */ "Rate the app" = "Valora la aplicación"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Barra de menú accesible"; + /* chat item menu */ "React…" = "Reacciona…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Leer"; /* No comment provided by engineer. */ "Read more" = "Saber más"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Más información en el [Manual de usuario](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"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)."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Más información en el [Manual de usuario](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; +"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 our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Más información en nuestro [repositorio GitHub](https://github.com/simplex-chat/simplex-chat#readme)."; +"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." = "Más información en nuestro repositorio GitHub."; +"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)."; /* No comment provided by engineer. */ "Receipts are disabled" = "Las confirmaciones están desactivadas"; +/* No comment provided by engineer. */ +"Receive errors" = "Errores de recepción"; + /* No comment provided by engineer. */ "received answer…" = "respuesta recibida…"; @@ -2528,6 +4070,15 @@ /* message info title */ "Received message" = "Mensaje entrante"; +/* No comment provided by engineer. */ +"Received messages" = "Mensajes recibidos"; + +/* No comment provided by engineer. */ +"Received reply" = "Respuesta recibida"; + +/* No comment provided by engineer. */ +"Received total" = "Total recibidos"; + /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "La dirección de recepción pasará a otro servidor. El cambio se completará cuando el remitente esté en línea."; @@ -2537,12 +4088,33 @@ /* No comment provided by engineer. */ "Receiving via" = "Recibiendo vía"; +/* No comment provided by engineer. */ +"Recent history and improved [directory bot](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)." = "Historial reciente y [bot del directorio](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) mejorados."; + +/* No comment provided by engineer. */ +"Recipient(s) can't see who this message is from." = "Los destinatarios no ven de quién procede este mensaje."; + /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Los destinatarios ven actualizarse mientras escribes."; +/* No comment provided by engineer. */ +"Reconnect" = "Reconectar"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Reconectar todos los servidores conectados para forzar la entrega del mensaje. Se usa tráfico adicional."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Reconectar todos los servidores"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "¿Reconectar todos los servidores?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Reconectar con el servidor para forzar la entrega de mensajes. Se usa tráfico adicional."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "¿Reconectar servidor?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "¿Reconectar servidores?"; @@ -2555,7 +4127,17 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Reducción del uso de batería"; -/* reject incoming call via notification */ +/* No comment provided by engineer. */ +"Register" = "Registrar"; + +/* token info */ +"Register notification token?" = "¿Registrar el token de notificaciones?"; + +/* token status text */ +"Registered" = "Registrado"; + +/* reject incoming call via notification +swipe action */ "Reject" = "Rechazar"; /* No comment provided by engineer. */ @@ -2564,11 +4146,14 @@ /* No comment provided by engineer. */ "Reject contact request" = "Rechazar solicitud de contacto"; +/* No comment provided by engineer. */ +"rejected" = "rechazado"; + /* call status */ "rejected call" = "llamada rechazada"; /* No comment provided by engineer. */ -"Relay server is only used if necessary. Another party can observe your IP address." = "El retransmisor sólo se usa en caso de necesidad. Un tercero podría ver tu IP."; +"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."; @@ -2576,6 +4161,12 @@ /* No comment provided by engineer. */ "Remove" = "Eliminar"; +/* No comment provided by engineer. */ +"Remove archive?" = "¿Eliminar archivo?"; + +/* No comment provided by engineer. */ +"Remove image" = "Eliminar imagen"; + /* No comment provided by engineer. */ "Remove member" = "Expulsar miembro"; @@ -2591,6 +4182,12 @@ /* rcv group event chat item */ "removed %@" = "ha expulsado a %@"; +/* profile update event chat item */ +"removed contact address" = "dirección de contacto eliminada"; + +/* profile update event chat item */ +"removed profile picture" = "ha eliminado la imagen del perfil"; + /* rcv group event chat item */ "removed you" = "te ha expulsado"; @@ -2603,20 +4200,83 @@ /* No comment provided by engineer. */ "Renegotiate encryption?" = "¿Renegociar cifrado?"; +/* No comment provided by engineer. */ +"Repeat connection request?" = "¿Repetir solicitud de conexión?"; + +/* No comment provided by engineer. */ +"Repeat download" = "Repetir descarga"; + +/* No comment provided by engineer. */ +"Repeat import" = "Repetir importación"; + +/* No comment provided by engineer. */ +"Repeat join request?" = "¿Repetir solicitud de admisión?"; + +/* No comment provided by engineer. */ +"Repeat upload" = "Repetir subida"; + /* chat item action */ "Reply" = "Responder"; +/* chat item action */ +"Report" = "Informe"; + +/* report reason */ +"Report content: only group moderators will see it." = "Informar de contenido: sólo los moderadores del grupo lo verán."; + +/* report reason */ +"Report member profile: only group moderators will see it." = "Informar del perfil de un miembro: sólo los moderadores del grupo lo verán."; + +/* report reason */ +"Report other: only group moderators will see it." = "Informar de otros: sólo los moderadores del grupo lo verán."; + +/* No comment provided by engineer. */ +"Report reason?" = "¿Motivo del informe?"; + +/* report reason */ +"Report spam: only group moderators will see it." = "Informar de spam: sólo los moderadores del grupo lo verán."; + +/* report reason */ +"Report violation: only group moderators will see it." = "Informar de violación: sólo los moderadores del grupo lo verán."; + +/* report in notification */ +"Report: %@" = "Informe: %@"; + +/* No comment provided by engineer. */ +"Reporting messages to moderators is prohibited." = "No se permite informar de mensajes a los moderadores."; + +/* No comment provided by engineer. */ +"Reports" = "Informes"; + +/* chat list item title */ +"requested to connect" = "solicitado para conectar"; + /* No comment provided by engineer. */ "Required" = "Obligatorio"; /* No comment provided by engineer. */ "Reset" = "Restablecer"; +/* No comment provided by engineer. */ +"Reset all hints" = "Reiniciar todas las pistas"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Restablecer todas las estadísticas"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "¿Restablecer todas las estadísticas?"; + /* No comment provided by engineer. */ "Reset colors" = "Restablecer colores"; /* No comment provided by engineer. */ -"Reset to defaults" = "Restablecer valores por defecto"; +"Reset to app theme" = "Restablecer al tema de la aplicación"; + +/* No comment provided by engineer. */ +"Reset to defaults" = "Restablecer valores predetarminados"; + +/* No comment provided by engineer. */ +"Reset to user theme" = "Restablecer al tema del usuario"; /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Reinicia la aplicación para crear un perfil nuevo"; @@ -2636,11 +4296,14 @@ /* No comment provided by engineer. */ "Restore database error" = "Error al restaurar base de datos"; +/* No comment provided by engineer. */ +"Retry" = "Reintentar"; + /* chat item action */ "Reveal" = "Revelar"; /* No comment provided by engineer. */ -"Revert" = "Revertir"; +"Review conditions" = "Revisar condiciones"; /* No comment provided by engineer. */ "Revoke" = "Revocar"; @@ -2655,39 +4318,46 @@ "Role" = "Rol"; /* No comment provided by engineer. */ -"Run chat" = "Ejecutar chat"; +"Run chat" = "Ejecutar SimpleX"; -/* chat item action */ +/* No comment provided by engineer. */ +"Safely receive files" = "Recibe archivos de forma segura"; + +/* No comment provided by engineer. */ +"Safer groups" = "Grupos más seguros"; + +/* alert button +chat item action */ "Save" = "Guardar"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Guardar (y notificar contactos)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Guardar y notificar contacto"; /* No comment provided by engineer. */ -"Save and notify group members" = "Guardar y notificar a los miembros del grupo"; +"Save and notify group members" = "Guardar y notificar grupo"; /* No comment provided by engineer. */ -"Save and update group profile" = "Guardar y actualizar perfil de grupo"; +"Save and reconnect" = "Guardar y reconectar"; /* No comment provided by engineer. */ -"Save archive" = "Guardar archivo"; - -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Guardar configuración de auto aceptar"; +"Save and update group profile" = "Guardar y actualizar perfil del grupo"; /* No comment provided by engineer. */ "Save group profile" = "Guardar perfil de grupo"; +/* No comment provided by engineer. */ +"Save list" = "Guardar lista"; + /* No comment provided by engineer. */ "Save passphrase and open chat" = "Guardar contraseña y abrir el chat"; /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Guardar la contraseña en Keychain"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "¿Guardar preferencias?"; /* No comment provided by engineer. */ @@ -2696,36 +4366,75 @@ /* No comment provided by engineer. */ "Save servers" = "Guardar servidores"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "¿Guardar servidores?"; -/* No comment provided by engineer. */ -"Save settings?" = "¿Guardar configuración?"; - /* No comment provided by engineer. */ "Save welcome message?" = "¿Guardar mensaje de bienvenida?"; +/* alert title */ +"Save your profile?" = "¿Guardar tu perfil?"; + +/* No comment provided by engineer. */ +"saved" = "guardado"; + +/* No comment provided by engineer. */ +"Saved" = "Guardado"; + +/* No comment provided by engineer. */ +"Saved from" = "Guardado desde"; + +/* No comment provided by engineer. */ +"saved from %@" = "Guardado desde %@"; + +/* message info title */ +"Saved message" = "Mensaje guardado"; + /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Los servidores WebRTC ICE guardados serán eliminados"; +/* No comment provided by engineer. */ +"Saving %lld messages" = "Guardando %lld mensajes"; + +/* No comment provided by engineer. */ +"Scale" = "Escala"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Escanear / Pegar enlace"; + /* No comment provided by engineer. */ "Scan code" = "Escanear código"; /* No comment provided by engineer. */ "Scan QR code" = "Escanear código QR"; +/* No comment provided by engineer. */ +"Scan QR code from desktop" = "Escanear código QR desde ordenador"; + /* No comment provided by engineer. */ "Scan security code from your contact's app." = "Escanea el código de seguridad desde la aplicación de tu contacto."; /* No comment provided by engineer. */ -"Scan server QR code" = "Escanear código QR del servidor"; +"Scan server QR code" = "Escanear código QR"; + +/* No comment provided by engineer. */ +"search" = "buscar"; /* No comment provided by engineer. */ "Search" = "Buscar"; +/* No comment provided by engineer. */ +"Search bar accepts invitation links." = "La barra de búsqueda acepta enlaces de invitación."; + +/* No comment provided by engineer. */ +"Search or paste SimpleX link" = "Buscar o pegar enlace SimpleX"; + /* network option */ "sec" = "seg"; +/* No comment provided by engineer. */ +"Secondary" = "Secundario"; + /* time unit */ "seconds" = "segundos"; @@ -2735,6 +4444,9 @@ /* server test step */ "Secure queue" = "Cola segura"; +/* No comment provided by engineer. */ +"Secured" = "Aseguradas"; + /* No comment provided by engineer. */ "Security assessment" = "Evaluación de la seguridad"; @@ -2744,9 +4456,18 @@ /* chat item text */ "security code changed" = "código de seguridad cambiado"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Seleccionar"; +/* No comment provided by engineer. */ +"Select chat profile" = "Selecciona perfil de chat"; + +/* No comment provided by engineer. */ +"Selected %lld" = "Seleccionados %lld"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Las preferencias seleccionadas no permiten este mensaje."; + /* No comment provided by engineer. */ "Self-destruct" = "Autodestrucción"; @@ -2763,7 +4484,7 @@ "Send" = "Enviar"; /* No comment provided by engineer. */ -"Send a live message - it will update for the recipient(s) as you type it" = "Envía un mensaje en vivo: se actualizará para el(los) destinatario(s) a medida que se escribe"; +"Send a live message - it will update for the recipient(s) as you type it" = "Envía un mensaje en vivo: se actualizará para el (los) destinatario(s) a medida que se escribe"; /* No comment provided by engineer. */ "Send delivery receipts to" = "Enviar confirmaciones de entrega a"; @@ -2772,25 +4493,34 @@ "send direct message" = "Enviar mensaje directo"; /* No comment provided by engineer. */ -"Send direct message" = "Enviar mensaje directo"; - -/* No comment provided by engineer. */ -"Send direct message to connect" = "Enviar mensaje directo para conectar"; +"Send direct message to connect" = "Envía un mensaje para conectar"; /* No comment provided by engineer. */ "Send disappearing message" = "Enviar mensaje temporal"; +/* No comment provided by engineer. */ +"Send errors" = "Errores de envío"; + /* No comment provided by engineer. */ "Send link previews" = "Enviar previsualizacion de enlaces"; /* No comment provided by engineer. */ "Send live message" = "Mensaje en vivo"; +/* No comment provided by engineer. */ +"Send message to enable calls." = "Enviar mensaje para activar llamadas."; + +/* No comment provided by engineer. */ +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Enviar mensajes directamente cuando tu dirección IP está protegida y tu servidor o el de destino no admitan enrutamiento privado."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Enviar mensajes directamente cuando tu servidor o el de destino no admitan enrutamiento privado."; + /* No comment provided by engineer. */ "Send notifications" = "Enviar notificaciones"; /* No comment provided by engineer. */ -"Send notifications:" = "Enviar notificaciones:"; +"Send private reports" = "Envía informes privados"; /* No comment provided by engineer. */ "Send questions and ideas" = "Consultas y sugerencias"; @@ -2802,6 +4532,9 @@ "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."; + +/* alert message */ "Sender cancelled file transfer." = "El remitente ha cancelado la transferencia de archivos."; /* No comment provided by engineer. */ @@ -2837,15 +4570,57 @@ /* copied message info */ "Sent at: %@" = "Enviado: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Directamente"; + /* notification */ "Sent file event" = "Evento de archivo enviado"; /* message info title */ "Sent message" = "Mensaje saliente"; +/* No comment provided by engineer. */ +"Sent messages" = "Mensajes enviados"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido."; +/* No comment provided by engineer. */ +"Sent reply" = "Respuesta enviada"; + +/* No comment provided by engineer. */ +"Sent total" = "Total enviados"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Mediante proxy"; + +/* No comment provided by engineer. */ +"Server" = "Servidor"; + +/* alert message */ +"Server added to operator %@." = "Servidor añadido al operador %@."; + +/* No comment provided by engineer. */ +"Server address" = "Dirección del servidor"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "La dirección del servidor es incompatible con la configuración de la red: %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "La dirección del servidor es incompatible con la configuración de la red."; + +/* alert title */ +"Server operator changed." = "El operador del servidor ha cambiado."; + +/* No comment provided by engineer. */ +"Server operators" = "Operadores de servidores"; + +/* alert title */ +"Server protocol changed." = "El protocolo del servidor ha cambiado."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "información cola del servidor: %1$@\n\núltimo mensaje recibido: %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "El servidor requiere autorización para crear colas, comprueba la contraseña"; @@ -2853,26 +4628,62 @@ "Server requires authorization to upload, check password" = "El servidor requiere autorización para subir, comprueba la contraseña"; /* No comment provided by engineer. */ -"Server test failed!" = "¡Error en prueba del servidor!"; +"Server test failed!" = "¡Prueba no superada!"; + +/* No comment provided by engineer. */ +"Server type" = "Tipo de servidor"; + +/* srv error text */ +"Server version is incompatible with network settings." = "La versión del servidor es incompatible con la configuración de red."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "La versión del servidor es incompatible con tu aplicación: %@."; /* No comment provided by engineer. */ "Servers" = "Servidores"; +/* No comment provided by engineer. */ +"Servers info" = "Info servidores"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Las estadísticas de los servidores serán restablecidas. ¡No puede deshacerse!"; + +/* No comment provided by engineer. */ +"Session code" = "Código de sesión"; + /* No comment provided by engineer. */ "Set 1 day" = "Establecer 1 día"; +/* No comment provided by engineer. */ +"Set chat name…" = "Nombre para el chat…"; + /* No comment provided by engineer. */ "Set contact name…" = "Escribe el nombre del contacto…"; +/* No comment provided by engineer. */ +"Set default theme" = "Establecer tema predeterminado"; + /* No comment provided by engineer. */ "Set group preferences" = "Establecer preferencias de grupo"; /* No comment provided by engineer. */ "Set it instead of system authentication." = "Úsalo en lugar de la autenticación del sistema."; +/* No comment provided by engineer. */ +"Set message expiration in chats." = "Establece el vencimiento para los mensajes en los chats."; + +/* profile update event chat item */ +"set new contact address" = "nueva dirección de contacto"; + +/* profile update event chat item */ +"set new profile picture" = "tiene nueva imagen del perfil"; + /* No comment provided by engineer. */ "Set passcode" = "Código autodestrucción"; +/* No comment provided by engineer. */ +"Set passphrase" = "Definir frase de contraseña"; + /* No comment provided by engineer. */ "Set passphrase to export" = "Escribe la contraseña para exportar"; @@ -2885,27 +4696,58 @@ /* No comment provided by engineer. */ "Settings" = "Configuración"; -/* chat item action */ +/* alert message */ +"Settings were changed." = "La configuración ha sido modificada."; + +/* No comment provided by engineer. */ +"Shape profile images" = "Dar forma a las imágenes de perfil"; + +/* alert action +chat item action */ "Share" = "Compartir"; /* No comment provided by engineer. */ "Share 1-time link" = "Compartir enlace de un uso"; +/* No comment provided by engineer. */ +"Share 1-time link with a friend" = "Compartir enlace de un uso con un amigo"; + /* No comment provided by engineer. */ "Share address" = "Compartir dirección"; /* No comment provided by engineer. */ +"Share address publicly" = "Campartir dirección públicamente"; + +/* alert title */ "Share address with contacts?" = "¿Compartir la dirección con los contactos?"; +/* No comment provided by engineer. */ +"Share from other apps." = "Comparte desde otras aplicaciones."; + /* No comment provided by engineer. */ "Share link" = "Compartir enlace"; /* No comment provided by engineer. */ -"Share one-time invitation link" = "Compartir enlace de invitación de un uso"; +"Share profile" = "Perfil a compartir"; + +/* No comment provided by engineer. */ +"Share SimpleX address on social media." = "Comparte tu dirección SimpleX en redes sociales."; + +/* No comment provided by engineer. */ +"Share this 1-time invite link" = "Comparte este enlace de un solo uso"; + +/* No comment provided by engineer. */ +"Share to SimpleX" = "Compartir con Simplex"; /* No comment provided by engineer. */ "Share with contacts" = "Compartir con contactos"; +/* No comment provided by engineer. */ +"Short link" = "Enlace corto"; + +/* No comment provided by engineer. */ +"Show → on messages sent via private routing." = "Mostrar → en mensajes con enrutamiento privado."; + /* No comment provided by engineer. */ "Show calls in phone history" = "Mostrar llamadas en el historial del teléfono"; @@ -2915,18 +4757,42 @@ /* No comment provided by engineer. */ "Show last messages" = "Mostrar último mensaje"; +/* No comment provided by engineer. */ +"Show message status" = "Estado del mensaje"; + +/* No comment provided by engineer. */ +"Show percentage" = "Mostrar porcentajes"; + /* No comment provided by engineer. */ "Show preview" = "Mostrar vista previa"; +/* No comment provided by engineer. */ +"Show QR code" = "Mostrar código QR"; + /* No comment provided by engineer. */ "Show:" = "Mostrar:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "Dirección SimpleX"; /* No comment provided by engineer. */ "SimpleX Address" = "Dirección SimpleX"; +/* No comment provided by engineer. */ +"SimpleX address and 1-time links are safe to share via any messenger." = "Compartir enlaces de un solo uso y direcciones SimpleX es seguro a través de cualquier medio."; + +/* No comment provided by engineer. */ +"SimpleX address or 1-time link?" = "¿Dirección SimpleX o enlace de un uso?"; + +/* simplex link type */ +"SimpleX channel link" = "Enlace de canal SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "Simplex Chat y Flux han acordado incluir en la aplicación servidores operados por Flux."; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "La seguridad de SimpleX Chat ha sido auditada por Trail of Bits."; @@ -2939,9 +4805,15 @@ /* simplex link type */ "SimpleX group link" = "Enlace de grupo SimpleX"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "Enlaces SimpleX"; +/* No comment provided by engineer. */ +"SimpleX links are prohibited." = "Los enlaces SimpleX no se permiten en este grupo."; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "Enlaces SimpleX no permitidos"; + /* No comment provided by engineer. */ "SimpleX Lock" = "Bloqueo SimpleX"; @@ -2957,9 +4829,15 @@ /* simplex link type */ "SimpleX one-time invitation" = "Invitación SimpleX de un uso"; +/* No comment provided by engineer. */ +"SimpleX protocols reviewed by Trail of Bits." = "Protocolos de SimpleX auditados por Trail of Bits."; + /* No comment provided by engineer. */ "Simplified incognito mode" = "Modo incógnito simplificado"; +/* No comment provided by engineer. */ +"Size" = "Tamaño"; + /* No comment provided by engineer. */ "Skip" = "Omitir"; @@ -2967,40 +4845,74 @@ "Skipped messages" = "Mensajes omitidos"; /* No comment provided by engineer. */ -"Small groups (max 20)" = "Grupos pequeños (máx. 20)"; +"Small groups (max 20)" = "Grupos pequeños (max. 20)"; /* No comment provided by engineer. */ -"SMP servers" = "Servidores SMP"; +"SMP server" = "Servidor SMP"; + +/* No comment provided by engineer. */ +"SOCKS proxy" = "Proxy SOCKS"; + +/* blur media */ +"Soft" = "Suave"; + +/* No comment provided by engineer. */ +"Some app settings were not migrated." = "Algunas configuraciones de la app no han sido migradas."; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "Algunos archivos no han sido exportados:"; /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Algunos errores no críticos ocurrieron durante la importación - para más detalles puedes ver la consola de Chat."; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "Han ocurrido algunos errores no críticos durante la importación:"; + +/* alert message */ +"Some servers failed the test:\n%@" = "Algunos servidores no han superado la prueba:\n%@"; + /* notification title */ "Somebody" = "Alguien"; +/* blocking reason +report reason */ +"Spam" = "Spam"; + /* No comment provided by engineer. */ -"Start a new chat" = "Iniciar chat nuevo"; +"Square, circle, or anything in between." = "Cuadrada, circular o cualquier forma intermedia."; + +/* chat item text */ +"standard end-to-end encryption" = "cifrado estándar de extremo a extremo"; /* No comment provided by engineer. */ "Start chat" = "Iniciar chat"; +/* No comment provided by engineer. */ +"Start chat?" = "¿Iniciar chat?"; + /* No comment provided by engineer. */ "Start migration" = "Iniciar migración"; +/* No comment provided by engineer. */ +"Starting from %@." = "Iniciado el %@."; + /* No comment provided by engineer. */ "starting…" = "inicializando…"; /* No comment provided by engineer. */ -"Stop" = "Detener"; +"Statistics" = "Estadísticas"; /* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Detén SimpleX para habilitar las acciones sobre la base de datos"; +"Stop" = "Parar"; /* No comment provided by engineer. */ -"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Para poder exportar, importar o eliminar la base de datos primero debes detener Chat. Durante el tiempo que esté detenido no podrás recibir ni enviar mensajes."; +"Stop chat" = "Parar SimpleX"; /* No comment provided by engineer. */ -"Stop chat?" = "¿Detener Chat?"; +"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Para exportar, importar o eliminar la base de datos debes parar SimpleX. Mientra tanto no podrás enviar ni recibir mensajes."; + +/* No comment provided by engineer. */ +"Stop chat?" = "¿Parar Chat?"; /* cancel file action */ "Stop file" = "Detener archivo"; @@ -3011,50 +4923,92 @@ /* No comment provided by engineer. */ "Stop sending file?" = "¿Dejar de enviar el archivo?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Dejar de compartir"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "¿Dejar de compartir la dirección?"; /* authentication reason */ -"Stop SimpleX" = "Detener SimpleX"; +"Stop SimpleX" = "Parar SimpleX"; + +/* No comment provided by engineer. */ +"Stopping chat" = "Parando chat"; + +/* No comment provided by engineer. */ +"Storage" = "Almacenamiento"; /* No comment provided by engineer. */ "strike" = "tachado"; +/* blur media */ +"Strong" = "Fuerte"; + /* No comment provided by engineer. */ "Submit" = "Enviar"; +/* No comment provided by engineer. */ +"Subscribed" = "Suscritas"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Errores de suscripción"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Suscripciones ignoradas"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Soporte SimpleX Chat"; +/* No comment provided by engineer. */ +"Switch audio and video during the call." = "Intercambia audio y video durante la llamada."; + +/* No comment provided by engineer. */ +"Switch chat profile for 1-time invitations." = "Cambia el perfil de chat para invitaciones de un solo uso."; + /* No comment provided by engineer. */ "System" = "Sistema"; /* No comment provided by engineer. */ "System authentication" = "Autenticación del sistema"; +/* No comment provided by engineer. */ +"Tail" = "Cola"; + /* No comment provided by engineer. */ "Take picture" = "Tomar foto"; /* No comment provided by engineer. */ "Tap button " = "Pulsa el botón "; +/* 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."; + /* No comment provided by engineer. */ "Tap to activate profile." = "Pulsa sobre un perfil para activarlo."; /* No comment provided by engineer. */ -"Tap to join" = "Pulsa para unirse"; +"Tap to Connect" = "Pulsa para conectar"; + +/* No comment provided by engineer. */ +"Tap to join" = "Pulsa para unirte"; /* No comment provided by engineer. */ "Tap to join incognito" = "Pulsa para unirte en modo incógnito"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Pulsa para iniciar chat nuevo"; +"Tap to paste link" = "Pulsa para pegar el enlacePulsa para pegar enlace"; /* No comment provided by engineer. */ -"TCP connection timeout" = "Tiempo de espera de la conexión TCP agotado"; +"Tap to scan" = "Pulsa para escanear"; + +/* No comment provided by engineer. */ +"TCP connection" = "Conexión TCP"; + +/* No comment provided by engineer. */ +"TCP connection timeout" = "Timeout de la conexión TCP"; + +/* No comment provided by engineer. */ +"TCP port for messaging" = "Puerto TCP para mensajes"; /* No comment provided by engineer. */ "TCP_KEEPCNT" = "TCP_KEEPCNT"; @@ -3065,8 +5019,14 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* file error alert title */ +"Temporary file error" = "Error en archivo temporal"; + /* server test failure */ -"Test failed at step %@." = "La prueba ha fallado en el paso %@."; +"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 server" = "Probar servidor"; @@ -3074,27 +5034,36 @@ /* No comment provided by engineer. */ "Test servers" = "Probar servidores"; -/* No comment provided by engineer. */ -"Tests failed!" = "¡Pruebas fallidas!"; +/* alert title */ +"Tests failed!" = "¡Pruebas no superadas!"; /* No comment provided by engineer. */ "Thank you for installing SimpleX Chat!" = "¡Gracias por instalar SimpleX Chat!"; /* No comment provided by engineer. */ -"Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Gracias a los usuarios: [contribuye vía Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#traducir-el-aplicaciones)!"; +"Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "¡Nuestro agradecimiento a todos los colaboradores, [puedes contribuir a través de Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#traducir-el-aplicaciones)!"; /* No comment provided by engineer. */ -"Thanks to the users – contribute via Weblate!" = "¡Gracias a los colaboradores! Contribuye a través de Weblate."; +"Thanks to the users – contribute via Weblate!" = "¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "La primera plataforma sin identificadores de usuario: diseñada para la privacidad."; +"The app can notify you when you receive messages or contact requests - please open settings to enable." = "La aplicación puede notificarte cuando recibas mensajes o solicitudes de contacto: por favor, abre la configuración para activarlo."; /* No comment provided by engineer. */ -"The app can notify you when you receive messages or contact requests - please open settings to enable." = "La aplicación puede notificarte cuando recibas mensajes o solicitudes de contacto: abre la configuración para habilitar."; +"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 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)."; /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "El intento de cambiar la contraseña de la base de datos no se ha completado."; +/* 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."; + +/* 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."; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "¡La conexión que has aceptado se cancelará!"; @@ -3107,6 +5076,9 @@ /* No comment provided by engineer. */ "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"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "El hash del mensaje anterior es diferente."; @@ -3120,13 +5092,22 @@ "The message will be marked as moderated for all members." = "El mensaje será marcado como moderado para todos los miembros."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "La nueva generación de mensajería privada"; +"The messages will be deleted for all members." = "Los mensajes serán eliminados para todos los miembros."; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "Los mensajes serán marcados como moderados para todos los miembros."; /* 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 profile is only shared with your contacts." = "El perfil sólo se comparte con tus contactos."; +"Your profile is stored on your device and only shared with your contacts." = "El perfil sólo se comparte con tus contactos."; + +/* No comment provided by engineer. */ +"The same conditions will apply to operator **%@**." = "Las mismas condiciones se aplicarán al operador **%@**."; + +/* No comment provided by engineer. */ +"The second preset operator in the app!" = "¡Segundo operador predefinido!"; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "¡El doble check que nos faltaba! ✅"; @@ -3135,61 +5116,118 @@ "The sender will NOT be notified" = "El remitente NO será notificado"; /* No comment provided by engineer. */ -"The servers for new connections of your current chat profile **%@**." = "Lista de servidores para las conexiones nuevas de tu perfil actual **%@**."; +"The servers for new connections of your current chat profile **%@**." = "Servidores para conexiones nuevas en tu perfil **%@**."; /* No comment provided by engineer. */ -"Theme" = "Tema"; +"The servers for new files of your current chat profile **%@**." = "Servidores para enviar archivos en tu perfil **%@**."; /* No comment provided by engineer. */ -"There should be at least one user profile." = "Debe haber al menos un perfil."; +"The text you pasted is not a SimpleX link." = "El texto pegado no es un enlace de SimpleX."; /* No comment provided by engineer. */ -"There should be at least one visible user profile." = "Debe haber al menos un perfil visible."; +"The uploaded database archive will be permanently removed from the servers." = "El archivo de bases de datos subido será eliminado permanentemente de los servidores."; + +/* No comment provided by engineer. */ +"Themes" = "Temas"; + +/* No comment provided by engineer. */ +"These conditions will also apply for: **%@**." = "Estas condiciones también se aplican para: **%@**."; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Esta configuración afecta a tu perfil actual **%@**."; /* No comment provided by engineer. */ -"They can be overridden in contact and group settings." = "Se pueden anular en la configuración de contactos."; +"They can be overridden in contact and group settings." = "Se puede modificar desde la configuración particular de cada grupo y contacto."; /* No comment provided by engineer. */ -"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Esta acción no se puede deshacer. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán."; +"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Esta acción es irreversible. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán."; /* 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." = "Esta acción no se puede deshacer. Se eliminarán los mensajes enviados y recibidos anteriores a la selección. Puede tardar varios minutos."; +"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Esta acción es irreversible. Se eliminarán los mensajes enviados y recibidos anteriores a la selección. Podría tardar varios minutos."; + +/* alert message */ +"This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." = "Todos los mensajes previos al período seleccionado serán eliminados del chat. ¡No puede deshacerse!"; /* No comment provided by engineer. */ -"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Esta acción no se puede deshacer. Tu perfil, contactos, mensajes y archivos se perderán irreversiblemente."; +"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Esta acción es irreversible. Tu perfil, contactos, mensajes y archivos se perderán irreversiblemente."; + +/* E2EE info chat item */ +"This chat is protected by end-to-end encryption." = "Este chat está protegido por cifrado de extremo a extremo."; + +/* E2EE info chat item */ +"This chat is protected by quantum resistant end-to-end encryption." = "Este chat está protegido por cifrado de extremo a extremo resistente a tecnología cuántica."; /* notification title */ "this contact" = "este contacto"; +/* No comment provided by engineer. */ +"This device name" = "Nombre del dispositivo"; + +/* No comment provided by engineer. */ +"This display name is invalid. Please choose another name." = "Éste nombre mostrado no es válido. Por favor, elije otro nombre."; + /* No comment provided by engineer. */ "This group has over %lld members, delivery receipts are not sent." = "Este grupo tiene más de %lld miembros, no se enviarán confirmaciones de entrega."; /* No comment provided by engineer. */ "This group no longer exists." = "Este grupo ya no existe."; +/* No comment provided by engineer. */ +"This is your own one-time link!" = "¡Este es tu propio enlace de un solo uso!"; + +/* No comment provided by engineer. */ +"This is your own SimpleX address!" = "¡Esta es tu propia dirección SimpleX!"; + +/* 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."; + +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Este enlace ha sido usado en otro dispositivo móvil, por favor crea un enlace nuevo en el ordenador."; + +/* No comment provided by engineer. */ +"This message was deleted or not received yet." = "El mensaje ha sido eliminado o aún no se ha recibido."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Esta configuración se aplica a los mensajes del perfil actual **%@**."; +/* No comment provided by engineer. */ +"Title" = "Título"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Para consultar cualquier duda y recibir actualizaciones:"; /* No comment provided by engineer. */ "To connect, your contact can scan QR code or use the link in the app." = "Para conectarse, tu contacto puede escanear el código QR o usar el enlace en la aplicación."; +/* No comment provided by engineer. */ +"To hide unwanted messages." = "Para ocultar mensajes no deseados."; + /* No comment provided by engineer. */ "To make a new connection" = "Para hacer una conexión nueva"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Para proteger la privacidad, en lugar de los identificadores de usuario que usan el resto de plataformas, SimpleX dispone de identificadores para las colas de mensajes, independientes para cada uno de tus contactos."; +"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."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Para proteger la zona horaria, los archivos de imagen/voz usan la hora UTC."; /* No comment provided by engineer. */ -"To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Para proteger tu información, activa Bloqueo SimpleX.\nSe te pedirá que completes la autenticación antes de activar esta función."; +"To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Para proteger tu información, activa el Bloqueo SimpleX.\nSe te pedirá que completes la autenticación antes de activar esta función."; + +/* No comment provided by engineer. */ +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "Para proteger tu dirección IP, el enrutamiento privado usa tu lista de servidores SMP para enviar mensajes."; + +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Para proteger tu privacidad, SimpleX usa identificadores distintos para cada uno de tus contactos."; + +/* No comment provided by engineer. */ +"To receive" = "Para recibir"; + +/* No comment provided by engineer. */ +"To record speech please grant permission to use Microphone." = "Para grabación de voz, por favor concede el permiso para usar el micrófono."; + +/* No comment provided by engineer. */ +"To record video please grant permission to use Camera." = "Para grabación de vídeo, por favor concede el permiso para usar la cámara."; /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Para grabar el mensaje de voz concede permiso para usar el micrófono."; @@ -3197,18 +5235,39 @@ /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Para hacer visible tu perfil oculto, introduce la contraseña en el campo de búsqueda del menú **Mis perfiles**."; +/* No comment provided by engineer. */ +"To send" = "Para enviar"; + /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Para permitir las notificaciones automáticas instantáneas, la base de datos se debe migrar."; /* No comment provided by engineer. */ -"To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Para comprobar el cifrado de extremo a extremo con tu contacto compara (o escanea) el código en tus dispositivos."; +"To use the servers of **%@**, accept conditions of use." = "Para usar los servidores de **%@**, debes aceptar las condiciones de uso."; + +/* 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."; +/* token status */ +"Token status: %@." = "Estado token: %@."; + +/* No comment provided by engineer. */ +"Toolbar opacity" = "Opacidad barra"; + +/* No comment provided by engineer. */ +"Total" = "Total"; + /* No comment provided by engineer. */ "Transport isolation" = "Aislamiento de transporte"; +/* No comment provided by engineer. */ +"Transport sessions" = "Sesiones de transporte"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Intentando conectar con el servidor usado para recibir mensajes de este contacto (error: %@)."; @@ -3216,10 +5275,10 @@ "Trying to connect to the server used to receive messages from this contact." = "Intentando conectar con el servidor usado para recibir mensajes de este contacto."; /* No comment provided by engineer. */ -"Turn off" = "Desactivar"; +"Turkish interface" = "Interfaz en turco"; /* No comment provided by engineer. */ -"Turn off notifications?" = "¿Desactivar notificaciones?"; +"Turn off" = "Desactivar"; /* No comment provided by engineer. */ "Turn on" = "Activar"; @@ -3227,13 +5286,31 @@ /* No comment provided by engineer. */ "Unable to record voice message" = "No se puede grabar mensaje de voz"; -/* item status description */ -"Unexpected error: %@" = "Error inesperado: %@"; +/* No comment provided by engineer. */ +"Unblock" = "Desbloquear"; + +/* No comment provided by engineer. */ +"Unblock for all" = "Desbloquear para todos"; + +/* No comment provided by engineer. */ +"Unblock member" = "Desbloquear miembro"; + +/* No comment provided by engineer. */ +"Unblock member for all?" = "¿Desbloquear el miembro para todos?"; + +/* No comment provided by engineer. */ +"Unblock member?" = "¿Desbloquear miembro?"; + +/* rcv group event chat item */ +"unblocked %@" = "ha desbloqueado a %@"; + +/* No comment provided by engineer. */ +"Undelivered messages" = "Mensajes no entregados"; /* No comment provided by engineer. */ "Unexpected migration state" = "Estado de migración inesperado"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "No fav."; /* No comment provided by engineer. */ @@ -3260,11 +5337,26 @@ /* No comment provided by engineer. */ "Unknown error" = "Error desconocido"; +/* No comment provided by engineer. */ +"unknown servers" = "con servidores desconocidos"; + +/* alert title */ +"Unknown servers!" = "¡Servidores desconocidos!"; + +/* No comment provided by engineer. */ +"unknown status" = "estado desconocido"; + /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "A menos que utilices la interfaz de llamadas de iOS, activa el modo No molestar para evitar interrupciones."; /* 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." = "A menos que tu contacto haya eliminado la conexión o\nque este enlace ya se haya usado, podría ser un error. Por favor, notifícalo.\nPara conectarte, pide a tu contacto que cree otro enlace de conexión y comprueba que tienes buena conexión de red."; +"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." = "A menos que tu contacto haya eliminado la conexión o el enlace se haya usado, podría ser un error. Por favor, notifícalo.\nPara conectarte pide a tu contacto que cree otro enlace y comprueba la conexión de red."; + +/* No comment provided by engineer. */ +"Unlink" = "Desenlazar"; + +/* No comment provided by engineer. */ +"Unlink desktop?" = "¿Desenlazar ordenador?"; /* No comment provided by engineer. */ "Unlock" = "Desbloquear"; @@ -3272,17 +5364,23 @@ /* authentication reason */ "Unlock app" = "Desbloquear aplicación"; -/* No comment provided by engineer. */ +/* notification label action */ "Unmute" = "Activar audio"; /* No comment provided by engineer. */ +"unprotected" = "con IP desprotegida"; + +/* swipe action */ "Unread" = "No leído"; /* No comment provided by engineer. */ -"Update" = "Actualizar"; +"Unsupported connection link" = "Enlace de conexión no compatible"; /* No comment provided by engineer. */ -"Update .onion hosts setting?" = "¿Actualizar la configuración de los hosts .onion?"; +"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. */ +"Update" = "Actualizar"; /* No comment provided by engineer. */ "Update database passphrase" = "Actualizar contraseña de la base de datos"; @@ -3291,26 +5389,47 @@ "Update network settings?" = "¿Actualizar la configuración de red?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "¿Actualizar el modo de aislamiento de transporte?"; +"Update settings?" = "¿Actualizar configuración?"; + +/* No comment provided by engineer. */ +"Updated conditions" = "Condiciones actualizadas"; /* rcv group event chat item */ "updated group profile" = "ha actualizado el perfil del grupo"; -/* No comment provided by engineer. */ -"Updating settings will re-connect the client to all servers." = "Al actualizar la configuración el cliente se reconectará a todos los servidores."; +/* profile update event chat item */ +"updated profile" = "perfil actualizado"; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Al actualizar esta configuración el cliente se reconectará a todos los servidores."; +"Updating settings will re-connect the client to all servers." = "Para actualizar la configuración el cliente se reconectará a todos los servidores."; /* No comment provided by engineer. */ "Upgrade and open chat" = "Actualizar y abrir Chat"; +/* No comment provided by engineer. */ +"Upload errors" = "Errores en subida"; + +/* No comment provided by engineer. */ +"Upload failed" = "Error de subida"; + /* server test step */ "Upload file" = "Subir archivo"; +/* No comment provided by engineer. */ +"Uploaded" = "Subido"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Archivos subidos"; + +/* No comment provided by engineer. */ +"Uploading archive" = "Subiendo archivo"; + /* No comment provided by engineer. */ "Use .onion hosts" = "Usar hosts .onion"; +/* No comment provided by engineer. */ +"Use %@" = "Usar %@"; + /* No comment provided by engineer. */ "Use chat" = "Usar Chat"; @@ -3318,7 +5437,16 @@ "Use current profile" = "Usar perfil actual"; /* No comment provided by engineer. */ -"Use for new connections" = "Usar para conexiones nuevas"; +"Use for files" = "Uso para archivos"; + +/* No comment provided by engineer. */ +"Use for messages" = "Uso para mensajes"; + +/* No comment provided by engineer. */ +"Use for new connections" = "Para conexiones nuevas"; + +/* No comment provided by engineer. */ +"Use from desktop" = "Usar desde ordenador"; /* No comment provided by engineer. */ "Use iOS call interface" = "Usar interfaz de llamada de iOS"; @@ -3326,27 +5454,78 @@ /* No comment provided by engineer. */ "Use new incognito profile" = "Usar nuevo perfil incógnito"; +/* No comment provided by engineer. */ +"Use only local notifications?" = "¿Usar sólo notificaciones locales?"; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "Usar enrutamiento privado con servidores desconocidos cuando tu dirección IP no está protegida."; + +/* 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 server" = "Usar servidor"; +/* No comment provided by engineer. */ +"Use servers" = "Usar servidores"; + +/* No comment provided by engineer. */ +"Use short links (BETA)" = "Usar enlaces cortos (BETA)"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "¿Usar servidores SimpleX Chat?"; /* No comment provided by engineer. */ -"User profile" = "Perfil de usuario"; +"Use SOCKS proxy" = "Usar proxy SOCKS"; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Usar hosts .onion requiere un proveedor VPN compatible."; +"Use TCP port %@ when no port is specified." = "Se usa el puerto TCP %@ cuando no se ha especificado otro."; + +/* No comment provided by engineer. */ +"Use TCP port 443 for preset servers only." = "Usar puerto TCP 443 solo en servidores predefinidos."; + +/* No comment provided by engineer. */ +"Use the app while in the call." = "Usar la aplicación durante la llamada."; + +/* 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 web port" = "Usar puerto web"; + +/* No comment provided by engineer. */ +"User selection" = "Selección de usuarios"; + +/* No comment provided by engineer. */ +"Username" = "Nombre de usuario"; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Usar servidores SimpleX Chat."; +/* No comment provided by engineer. */ +"v%@" = "v%@"; + /* No comment provided by engineer. */ "v%@ (%@)" = "v%@ (%@)"; +/* No comment provided by engineer. */ +"Verify code with desktop" = "Verificar código con ordenador"; + +/* No comment provided by engineer. */ +"Verify connection" = "Verificar conexión"; + /* No comment provided by engineer. */ "Verify connection security" = "Comprobar la seguridad de la conexión"; +/* No comment provided by engineer. */ +"Verify connections" = "Verificar conexiones"; + +/* No comment provided by engineer. */ +"Verify database passphrase" = "Verificar la contraseña de la base de datos"; + +/* No comment provided by engineer. */ +"Verify passphrase" = "Verificar frase de contraseña"; + /* No comment provided by engineer. */ "Verify security code" = "Comprobar código de seguridad"; @@ -3365,6 +5544,12 @@ /* No comment provided by engineer. */ "via relay" = "mediante retransmisor"; +/* No comment provided by engineer. */ +"Via secure quantum resistant protocol." = "Mediante protocolo seguro de resistencia cuántica."; + +/* No comment provided by engineer. */ +"video" = "video"; + /* No comment provided by engineer. */ "Video call" = "Videollamada"; @@ -3372,17 +5557,26 @@ "video call (not e2e encrypted)" = "videollamada (sin cifrar)"; /* No comment provided by engineer. */ -"Video will be received when your contact completes uploading it." = "El video se recibirá cuando tu contacto termine de subirlo."; +"Video will be received when your contact completes uploading it." = "El video se recibirá cuando el contacto termine de subirlo."; /* No comment provided by engineer. */ -"Video will be received when your contact is online, please wait or check later!" = "El vídeo se recibirá cuando tu contacto esté en línea, por favor espera o compruébalo más tarde."; +"Video will be received when your contact is online, please wait or check later!" = "El vídeo se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde."; /* No comment provided by engineer. */ "Videos and files up to 1gb" = "Vídeos y archivos de hasta 1Gb"; +/* No comment provided by engineer. */ +"View conditions" = "Ver condiciones"; + /* No comment provided by engineer. */ "View security code" = "Mostrar código de seguridad"; +/* No comment provided by engineer. */ +"View updated conditions" = "Ver condiciones actualizadas"; + +/* chat feature */ +"Visible history" = "Historial visible"; + /* No comment provided by engineer. */ "Voice message…" = "Mensaje de voz…"; @@ -3393,7 +5587,10 @@ "Voice messages are prohibited in this chat." = "Los mensajes de voz no están permitidos en este chat."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Los mensajes de voz no están permitidos en este grupo."; +"Voice messages are prohibited." = "Los mensajes de voz no están permitidos en este grupo."; + +/* No comment provided by engineer. */ +"Voice messages not allowed" = "Mensajes de voz no permitidos"; /* No comment provided by engineer. */ "Voice messages prohibited!" = "¡Mensajes de voz no permitidos!"; @@ -3404,6 +5601,9 @@ /* No comment provided by engineer. */ "waiting for confirmation…" = "esperando confirmación…"; +/* No comment provided by engineer. */ +"Waiting for desktop..." = "Esperando ordenador..."; + /* No comment provided by engineer. */ "Waiting for file" = "Esperando archivo"; @@ -3413,9 +5613,18 @@ /* No comment provided by engineer. */ "Waiting for video" = "Esperando el vídeo"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Color imagen de fondo"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Color de fondo"; + /* No comment provided by engineer. */ "wants to connect to you!" = "¡quiere contactar contigo!"; +/* No comment provided by engineer. */ +"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "Atención: el inicio del chat en varios dispositivos es incompatible y provocará fallos en la entrega de mensajes"; + /* No comment provided by engineer. */ "Warning: you may lose some data!" = "Atención: ¡puedes perder algunos datos!"; @@ -3431,6 +5640,9 @@ /* No comment provided by engineer. */ "Welcome message" = "Mensaje de bienvenida"; +/* No comment provided by engineer. */ +"Welcome message is too long" = "Mensaje de bienvenida demasiado largo"; + /* No comment provided by engineer. */ "What's new" = "Novedades"; @@ -3438,28 +5650,64 @@ "When available" = "Si disponibles"; /* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Cuando alguien solicite conectarse podrás aceptar o rechazar la solicitud."; +"When connecting audio and video calls." = "Al iniciar llamadas de audio y vídeo."; + +/* No comment provided by engineer. */ +"when IP hidden" = "con IP oculta"; + +/* No comment provided by engineer. */ +"When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "Cuando está habilitado más de un operador, ninguno dispone de los metadatos para conocer quién se comunica con quién."; /* 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. */ +"WiFi" = "WiFi"; + +/* No comment provided by engineer. */ +"Will be enabled in direct chats!" = "¡Será habilitado en los chats directos!"; + +/* No comment provided by engineer. */ +"Wired ethernet" = "Ethernet por cable"; + +/* No comment provided by engineer. */ +"With encrypted files and media." = "Con cifrado de archivos y multimedia."; + /* No comment provided by engineer. */ "With optional welcome message." = "Con mensaje de bienvenida opcional."; +/* No comment provided by engineer. */ +"With reduced battery usage." = "Con uso reducido de batería."; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "Sin Tor o VPN, tu dirección IP será visible para los servidores de archivos."; + +/* alert message */ +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Sin Tor o VPN, tu dirección IP será visible para estos servidores XFTP: %@."; + /* No comment provided by engineer. */ "Wrong database passphrase" = "Contraseña de base de datos incorrecta"; +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "Clave incorrecta o conexión desconocida - probablemente esta conexión fue eliminada."; + +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Clave incorrecta o dirección del bloque del archivo desconocida. Es probable que el archivo se haya eliminado."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "¡Contraseña incorrecta!"; /* No comment provided by engineer. */ -"XFTP servers" = "Servidores XFTP"; +"XFTP server" = "Servidor XFTP"; /* pref value */ "yes" = "sí"; /* No comment provided by engineer. */ -"You" = "Tú"; +"you" = "tu"; + +/* No comment provided by engineer. */ +"You **must not** use the same database on two devices." = "**No debes** usar la misma base de datos en dos dispositivos."; /* No comment provided by engineer. */ "You accepted connection" = "Has aceptado la conexión"; @@ -3471,25 +5719,58 @@ "You already have a chat profile with the same display name. Please choose another name." = "Ya tienes un perfil con este nombre mostrado. Por favor, elige otro nombre."; /* No comment provided by engineer. */ -"You are already connected to %@." = "Ya estás conectado a %@."; +"You are already connected to %@." = "Ya estás conectado con %@."; + +/* No comment provided by engineer. */ +"You are already connected with %@." = "Ya estás conectado con %@."; + +/* No comment provided by engineer. */ +"You are already connecting to %@." = "Ya estás conectando con %@."; + +/* No comment provided by engineer. */ +"You are already connecting via this one-time link!" = "¡Ya estás conectando mediante este enlace de un solo uso!"; + +/* No comment provided by engineer. */ +"You are already in group %@." = "Ya estás en el grupo %@."; + +/* No comment provided by engineer. */ +"You are already joining the group %@." = "Ya estás uniéndote al grupo %@."; + +/* No comment provided by engineer. */ +"You are already joining the group via this link!" = "¡Ya estás uniéndote al grupo mediante este enlace!"; + +/* No comment provided by engineer. */ +"You are already joining the group via this link." = "Ya estás uniéndote al grupo mediante este enlace."; + +/* No comment provided by engineer. */ +"You are already joining the group!\nRepeat join request?" = "¡En proceso de unirte al grupo!\n¿Repetir solicitud de admisión?"; /* No comment provided by engineer. */ "You are connected to the server used to receive messages from this contact." = "Estás conectado al servidor usado para recibir mensajes de este contacto."; /* No comment provided by engineer. */ -"you are invited to group" = "has sido invitado al grupo"; +"you are invited to group" = "has sido invitado a un grupo"; /* No comment provided by engineer. */ -"You are invited to group" = "Has sido invitado al grupo"; +"You are invited to group" = "Has sido invitado a un grupo"; + +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "No tienes conexión directa a estos servidores. Los mensajes destinados a estos usan enrutamiento privado."; /* No comment provided by engineer. */ "you are observer" = "Tu rol es observador"; +/* snd group event chat item */ +"you blocked %@" = "has bloqueado a %@"; + /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Puede aceptar llamadas desde la pantalla de bloqueo, sin autenticación de dispositivos y aplicaciones."; /* No comment provided by engineer. */ -"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "También puedes conectarte haciendo clic en el enlace. Si se abre en el navegador, haz clic en el botón **Abrir en aplicación móvil**."; +"You can change it in Appearance settings." = "Puedes cambiar la posición de la barra desde el menú Apariencia."; + +/* No comment provided by engineer. */ +"You can configure servers via settings." = "Puedes configurar los servidores a través de su configuración."; /* No comment provided by engineer. */ "You can create it later" = "Puedes crearla más tarde"; @@ -3500,26 +5781,38 @@ /* No comment provided by engineer. */ "You can enable them later via app Privacy & Security settings." = "Puedes activarlos más tarde en la configuración de Privacidad y Seguridad."; +/* No comment provided by engineer. */ +"You can give another try." = "Puedes intentarlo de nuevo."; + /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "Puedes ocultar o silenciar un perfil deslizándolo a la derecha."; +/* No comment provided by engineer. */ +"You can make it visible to your SimpleX contacts via Settings." = "Puedes hacerlo visible para tus contactos de SimpleX en Configuración."; + /* notification body */ -"You can now send messages to %@" = "Ya puedes enviar mensajes a %@"; +"You can now chat with %@" = "Ya puedes chatear con %@"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "Puedes enviar mensajes a %@ desde Contactos archivados."; + +/* No comment provided by engineer. */ +"You can set connection name, to remember who the link was shared with." = "Puedes añadir un nombre a la conexión para recordar a quién corresponde."; /* No comment provided by engineer. */ "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 un enlace o un código QR: cualquiera podrá unirse al grupo. Si lo eliminas más tarde los miembros del grupo no se perderán."; +"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 un enlace o código QR para que cualquiera pueda unirse al grupo. Si decides eliminarlo más tarde, los miembros del grupo se mantendrán."; /* 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 **%@**."; /* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Puedes compartir tu dirección como enlace o como código QR: cualquiera podrá conectarse contigo."; +"You can start chat via app Settings / Database or by restarting the app" = "Puede iniciar Chat a través de la Configuración / Base de datos de la aplicación o reiniciando la aplicación"; /* No comment provided by engineer. */ -"You can start chat via app Settings / Database or by restarting the app" = "Puede iniciar Chat a través de la Configuración / Base de datos de la aplicación o reiniciando la aplicación"; +"You can still view conversation with %@ in the list of chats." = "Aún puedes ver la conversación con %@ en la lista de chats."; /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Puedes activar el Bloqueo SimpleX a través de Configuración."; @@ -3527,6 +5820,9 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Puedes usar la sintaxis markdown para dar formato a tus mensajes:"; +/* alert message */ +"You can view invitation link again in connection details." = "Podrás ver el enlace de invitación en detalles de conexión."; + /* No comment provided by engineer. */ "You can't send messages!" = "¡No puedes enviar mensajes!"; @@ -3542,14 +5838,17 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "has cambiado el rol de %1$@ a %2$@"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Tú controlas a través de qué servidor(es) **recibes** los mensajes. Tus contactos controlan a través de qué servidor(es) **envías** tus mensajes."; - /* 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 have no chats" = "No tienes chats"; +"You decide who can connect." = "Tu decides quién se conecta."; + +/* No comment provided by engineer. */ +"You have already requested connection via this address!" = "¡Ya has solicitado la conexión mediante esta dirección!"; + +/* No comment provided by engineer. */ +"You have already requested connection!\nRepeat connection request?" = "Ya has solicitado la conexión\n¿Repetir solicitud?"; /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "La contraseña no se almacena en el dispositivo, tienes que introducirla cada vez que inicies la aplicación."; @@ -3566,9 +5865,18 @@ /* snd group event chat item */ "you left" = "has salido"; +/* No comment provided by engineer. */ +"You may migrate the exported database." = "Puedes migrar la base de datos exportada."; + +/* No comment provided by engineer. */ +"You may save the exported archive." = "Puedes guardar el archivo exportado."; + /* 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." = "Debes usar la versión más reciente de tu base de datos ÚNICAMENTE en un dispositivo, de lo contrario podrías dejar de recibir mensajes de algunos contactos."; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "Debes permitir que tus contacto te llamen para poder llamarles."; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "Para poder enviar mensajes de voz antes debes permitir que tu contacto pueda enviarlos."; @@ -3582,26 +5890,41 @@ "You sent group invitation" = "Has enviado una invitación de grupo"; /* chat list item description */ -"you shared one-time link" = "has compartido un enlace de un uso"; +"you shared one-time link" = "enlace de un solo uso"; /* chat list item description */ "you shared one-time link incognito" = "has compartido enlace de un solo uso en modo incógnito"; -/* No comment provided by engineer. */ -"You will be connected to group when the group host's device is online, please wait or check later!" = "Te conectarás al grupo cuando el dispositivo del anfitrión esté en línea, por favor espera o compruébalo más tarde."; +/* token info */ +"You should receive notifications." = "Deberías recibir notificaciones."; + +/* snd group event chat item */ +"you unblocked %@" = "has desbloqueado a %@"; /* No comment provided by engineer. */ -"You will be connected when your connection request is accepted, please wait or check later!" = "Te conectarás cuando tu solicitud se acepte, por favor espera o compruébalo más tarde."; +"You will be connected to group when the group host's device is online, please wait or check later!" = "Te conectarás al grupo cuando el dispositivo del anfitrión esté en línea, por favor espera o revisa más tarde."; /* No comment provided by engineer. */ -"You will be connected when your contact's device is online, please wait or check later!" = "Te conectarás cuando el dispositivo de tu contacto esté en línea, por favor espera o compruébalo más tarde."; +"You will be connected when group link host's device is online, please wait or check later!" = "Te conectarás cuando el dispositivo propietario del grupo esté en línea, por favor espera o revisa más tarde."; /* No comment provided by engineer. */ -"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Se te pedirá identificarte cuándo inicies o continues usando la aplicación tras 30 segundos en segundo plano."; +"You will be connected when your connection request is accepted, please wait or check later!" = "Te conectarás cuando tu solicitud se acepte, por favor espera o revisa más tarde."; + +/* No comment provided by engineer. */ +"You will be connected when your contact's device is online, please wait or check later!" = "Te conectarás cuando el dispositivo del contacto esté en línea, por favor espera o revisa más tarde."; + +/* No comment provided by engineer. */ +"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Se te pedirá autenticarte cuando inicies la aplicación o sigas usándola tras 30 segundos en segundo plano."; + +/* No comment provided by engineer. */ +"You will connect to all group members." = "Te conectarás con todos los miembros del grupo."; /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos."; +/* No comment provided by engineer. */ +"You will stop receiving messages from this chat. Chat history will be preserved." = "Dejarás de recibir mensajes de este chat. El historial del chat se conserva."; + /* No comment provided by engineer. */ "You will stop receiving messages from this group. Chat history will be preserved." = "Dejarás de recibir mensajes de este grupo. El historial del chat se conservará."; @@ -3615,10 +5938,7 @@ "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" = "Estás intentando invitar a un contacto con el que compartes un perfil incógnito a un grupo en el que usas tu perfil principal"; /* 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" = "Estás usando un perfil incógnito para este grupo, por tanto para evitar compartir tu perfil principal no se permite invitar a contactos"; - -/* No comment provided by engineer. */ -"Your %@ servers" = "Mis servidores %@"; +"You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Estás usando un perfil incógnito en este grupo. Para evitar descubrir tu perfil principal no se permite invitar contactos"; /* No comment provided by engineer. */ "Your calls" = "Llamadas"; @@ -3629,11 +5949,14 @@ /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "La base de datos no está cifrada - establece una contraseña para cifrarla."; +/* alert title */ +"Your chat preferences" = "Tus preferencias de chat"; + /* No comment provided by engineer. */ "Your chat profiles" = "Mis perfiles"; /* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Tu contacto debe estar en línea para que se complete la conexión.\nPuedes cancelar esta conexión y eliminar el contacto (e intentarlo más tarde con un enlace nuevo)."; +"Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Tu conexión ha sido trasladada a %@ pero ha ocurrido un error inesperado al redirigirte al perfil."; /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "El contacto ha enviado un archivo mayor al máximo admitido (%@)."; @@ -3642,10 +5965,10 @@ "Your contacts can allow full message deletion." = "Tus contactos pueden permitir la eliminación completa de mensajes."; /* No comment provided by engineer. */ -"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "Tus contactos en SimpleX lo verán.\nPuedes cambiarlo en Configuración."; +"Your contacts will remain connected." = "Tus contactos permanecerán conectados."; /* No comment provided by engineer. */ -"Your contacts will remain connected." = "Tus contactos permanecerán conectados."; +"Your credentials may be sent unencrypted." = "Tus credenciales podrían ser enviadas sin cifrar."; /* No comment provided by engineer. */ "Your current chat database will be DELETED and REPLACED with the imported one." = "La base de datos actual será ELIMINADA y SUSTITUIDA por la importada."; @@ -3663,10 +5986,16 @@ "Your privacy" = "Privacidad"; /* No comment provided by engineer. */ -"Your profile **%@** will be shared." = "Tu perfil **%@** será compartido."; +"Your profile" = "Tu perfil"; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Tu perfil se almacena en tu dispositivo y sólo se comparte con tus contactos.\nLos servidores de SimpleX no pueden ver tu perfil."; +"Your profile **%@** will be shared." = "El perfil **%@** será compartido."; + +/* No comment provided by engineer. */ +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos. Los servidores SimpleX no pueden ver tu perfil."; + +/* 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 profile, contacts and delivered messages are stored on your device." = "Tu perfil, contactos y mensajes se almacenan en tu dispositivo."; @@ -3675,10 +6004,10 @@ "Your random profile" = "Tu perfil aleatorio"; /* No comment provided by engineer. */ -"Your server" = "Tu servidor"; +"Your server address" = "Dirección del servidor"; /* No comment provided by engineer. */ -"Your server address" = "Dirección de tu servidor"; +"Your servers" = "Tus servidores"; /* No comment provided by engineer. */ "Your settings" = "Configuración"; @@ -3686,9 +6015,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "Mi dirección SimpleX"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Servidores SMP"; - -/* No comment provided by engineer. */ -"Your XFTP servers" = "Servidores XFTP"; - diff --git a/apps/ios/es.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/es.lproj/SimpleX--iOS--InfoPlist.strings index b2dad8adaf..3fc7b2c453 100644 --- a/apps/ios/es.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/es.lproj/SimpleX--iOS--InfoPlist.strings @@ -7,6 +7,9 @@ /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX usa reconocimiento facial para la autenticación local"; +/* Privacy - Local Network Usage Description */ +"NSLocalNetworkUsageDescription" = "SimpleX utiliza el acceso a la red local para abrir el perfil de chat en la aplicación de ordenador en la misma red."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX necesita acceso al micrófono para las llamadas de audio, vídeo y para grabar mensajes de voz."; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index fad3a54b8b..4891c7fb26 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -1,18 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (voidaan kopioida)"; @@ -25,24 +10,9 @@ /* No comment provided by engineer. */ "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- ääniviestit enintään 5 minuuttia.\n- mukautettu katoamisaika.\n- historian muokkaaminen."; -/* No comment provided by engineer. */ -", " = ", "; - -/* No comment provided by engineer. */ -": " = ": "; - /* No comment provided by engineer. */ "!1 colored!" = "!1 värillinen!"; -/* No comment provided by engineer. */ -"." = "."; - -/* No comment provided by engineer. */ -"(" = "("; - -/* No comment provided by engineer. */ -")" = ")"; - /* No comment provided by engineer. */ "[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Osallistu](https://github.com/simplex-chat/simplex-chat#contribute)"; @@ -52,12 +22,6 @@ /* No comment provided by engineer. */ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Tähti GitHubissa](https://github.com/simplex-chat/simplex-chat)"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Lisää uusi kontakti**: luo kertakäyttöinen QR-koodi tai linkki kontaktille."; - -/* No comment provided by engineer. */ -"**Create link / QR code** for your contact to use." = "**Luo linkki / QR-koodi* kontaktille."; - /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "**e2e-salattu** äänipuhelu"; @@ -65,22 +29,16 @@ "**e2e encrypted** video call" = "**e2e-salattu** videopuhelu"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Yksityisempi**: tarkista uudet viestit 20 minuutin välein. Laitetunnus jaetaan SimpleX Chat -palvelimen kanssa, mutta ei sitä, kuinka monta yhteystietoa tai viestiä sinulla on."; +"**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." = "**Yksityisempi**: tarkista uudet viestit 20 minuutin välein. Laitetunnus jaetaan SimpleX Chat -palvelimen kanssa, mutta ei sitä, kuinka monta yhteystietoa tai viestiä sinulla on."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Yksityisin**: älä käytä SimpleX Chat -ilmoituspalvelinta, tarkista viestit ajoittain taustalla (riippuu siitä, kuinka usein käytät sovellusta)."; - -/* No comment provided by engineer. */ -"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Liitä vastaanotettu linkki** tai avaa se selaimessa ja napauta **Avaa mobiilisovelluksessa**."; +"**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." = "**Yksityisin**: älä käytä SimpleX Chat -ilmoituspalvelinta, tarkista viestit ajoittain taustalla (riippuu siitä, kuinka usein käytät sovellusta)."; /* No comment provided by engineer. */ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Huomaa**: et voi palauttaa tai muuttaa tunnuslausetta, jos kadotat sen."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Suositus**: laitetunnus ja ilmoitukset lähetetään SimpleX Chat -ilmoituspalvelimelle, mutta ei viestin sisältöä, kokoa tai sitä, keneltä se on peräisin."; - -/* No comment provided by engineer. */ -"**Scan QR code**: to connect to your contact in person or via video call." = "**Skannaa QR-koodi**: muodosta yhteys kontaktiisi henkilökohtaisesti tai videopuhelun kautta."; +"**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." = "**Suositus**: laitetunnus ja ilmoitukset lähetetään SimpleX Chat -ilmoituspalvelimelle, mutta ei viestin sisältöä, kokoa tai sitä, keneltä se on peräisin."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Varoitus**: Välittömät push-ilmoitukset vaativat tunnuslauseen, joka on tallennettu Keychainiin."; @@ -130,9 +88,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ on vahvistettu"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ palvelimet"; - /* notification title */ "%@ wants to connect!" = "%@ haluaa muodostaa yhteyden!"; @@ -184,9 +139,6 @@ /* No comment provided by engineer. */ "%lld new interface languages" = "%lld uutta käyttöliittymän kieltä"; -/* No comment provided by engineer. */ -"%lld second(s)" = "%lld sekunti(a)"; - /* No comment provided by engineer. */ "%lld seconds" = "%lld sekuntia"; @@ -229,7 +181,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 päivä"; /* time interval */ @@ -238,15 +191,14 @@ /* No comment provided by engineer. */ "1 minute" = "1 minuutti"; -/* time interval */ +/* delete after time +time interval */ "1 month" = "1 kuukausi"; -/* time interval */ +/* delete after time +time interval */ "1 week" = "1 viikko"; -/* No comment provided by engineer. */ -"1-time link" = "Kertakäyttölinkki"; - /* No comment provided by engineer. */ "5 minutes" = "5 minuuttia"; @@ -280,23 +232,15 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Keskeytä osoitteenvaihto?"; -/* No comment provided by engineer. */ -"About SimpleX" = "Tietoja SimpleX:stä"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "Tietoja SimpleX osoitteesta"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "Tietoja SimpleX Chatistä"; /* No comment provided by engineer. */ "above, then choose:" = "edellä, valitse sitten:"; -/* No comment provided by engineer. */ -"Accent color" = "Korostusväri"; - /* accept contact request via notification - accept incoming call via notification */ +accept incoming call via notification +swipe action */ "Accept" = "Hyväksy"; /* No comment provided by engineer. */ @@ -305,7 +249,8 @@ /* notification body */ "Accept contact request from %@?" = "Hyväksy kontaktipyyntö %@:ltä?"; -/* accept contact request via notification */ +/* accept contact request via notification +swipe action */ "Accept incognito" = "Hyväksy tuntematon"; /* call status */ @@ -314,14 +259,11 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Lisää osoite profiiliisi, jotta kontaktisi voivat jakaa sen muiden kanssa. Profiilipäivitys lähetetään kontakteillesi."; -/* No comment provided by engineer. */ -"Add preset servers" = "Lisää esiasetettuja palvelimia"; - /* No comment provided by engineer. */ "Add profile" = "Lisää profiili"; /* No comment provided by engineer. */ -"Add server…" = "Lisää palvelin…"; +"Add server" = "Lisää palvelin"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Lisää palvelimia skannaamalla QR-koodeja."; @@ -384,7 +326,7 @@ "Allow disappearing messages only if your contact allows it to you." = "Salli katoavat viestit vain, jos kontaktisi sallii sen sinulle."; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you." = "Salli peruuttamaton viestien poisto vain, jos kontaktisi sallii ne sinulle."; +"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Salli peruuttamaton viestien poisto vain, jos kontaktisi sallii ne sinulle. (24 tuntia)"; /* No comment provided by engineer. */ "Allow message reactions only if your contact allows them." = "Salli reaktiot viesteihin vain, jos kontaktisi sallii ne."; @@ -399,7 +341,7 @@ "Allow sending disappearing messages." = "Salli katoavien viestien lähettäminen."; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages." = "Salli lähetettyjen viestien peruuttamaton poistaminen."; +"Allow to irreversibly delete sent messages. (24 hours)" = "Salli lähetettyjen viestien peruuttamaton poistaminen. (24 tuntia)"; /* No comment provided by engineer. */ "Allow to send files and media." = "Salli tiedostojen ja median lähettäminen."; @@ -420,7 +362,7 @@ "Allow your contacts to call you." = "Salli kontaktiesi soittaa sinulle."; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages." = "Salli kontaktiesi poistaa lähetetyt viestit peruuttamattomasti."; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Salli kontaktiesi poistaa lähetetyt viestit peruuttamattomasti. (24 tuntia)"; /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Salli kontaktiesi lähettää katoavia viestejä."; @@ -443,6 +385,9 @@ /* 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: %@"; @@ -528,7 +473,7 @@ "Both you and your contact can add message reactions." = "Sekä sinä että kontaktisi voivat käyttää viestireaktioita."; /* No comment provided by engineer. */ -"Both you and your contact can irreversibly delete sent messages." = "Sekä sinä että kontaktisi voitte peruuttamattomasti poistaa lähetetyt viestit."; +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "Sekä sinä että kontaktisi voitte peruuttamattomasti poistaa lähetetyt viestit. (24 tuntia)"; /* No comment provided by engineer. */ "Both you and your contact can make calls." = "Sekä sinä että kontaktisi voitte soittaa puheluita."; @@ -557,16 +502,14 @@ /* No comment provided by engineer. */ "Calls" = "Puhelut"; -/* No comment provided by engineer. */ -"Can't delete user profile!" = "Käyttäjäprofiilia ei voi poistaa!"; - /* No comment provided by engineer. */ "Can't invite contact!" = "Kontaktia ei voi kutsua!"; /* No comment provided by engineer. */ "Can't invite contacts!" = "Kontakteja ei voi kutsua!"; -/* No comment provided by engineer. */ +/* alert action +alert button */ "Cancel" = "Peruuta"; /* feature offered item */ @@ -575,7 +518,7 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Ei pääsyä avainnippuun tietokannan salasanan tallentamiseksi"; -/* No comment provided by engineer. */ +/* alert title */ "Cannot receive file" = "Tiedostoa ei voi vastaanottaa"; /* No comment provided by engineer. */ @@ -606,7 +549,7 @@ "Change self-destruct mode" = "Vaihda itsetuhotilaa"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Vaihda itsetuhoutuva pääsykoodi"; /* chat item text */ @@ -624,9 +567,6 @@ /* chat item text */ "changing address…" = "muuttamassa osoitetta…"; -/* No comment provided by engineer. */ -"Chat archive" = "Chat-arkisto"; - /* No comment provided by engineer. */ "Chat console" = "Chat-konsoli"; @@ -649,9 +589,12 @@ "Chat preferences" = "Chat-asetukset"; /* No comment provided by engineer. */ -"Chats" = "Keskustelut"; +"Chat profile" = "Käyttäjäprofiili"; /* No comment provided by engineer. */ +"Chats" = "Keskustelut"; + +/* alert title */ "Check server address and try again." = "Tarkista palvelimen osoite ja yritä uudelleen."; /* No comment provided by engineer. */ @@ -663,7 +606,7 @@ /* No comment provided by engineer. */ "Choose from library" = "Valitse kirjastosta"; -/* No comment provided by engineer. */ +/* swipe action */ "Clear" = "Tyhjennä"; /* No comment provided by engineer. */ @@ -678,9 +621,6 @@ /* No comment provided by engineer. */ "colored" = "värillinen"; -/* No comment provided by engineer. */ -"Colors" = "Värit"; - /* server test step */ "Compare file" = "Vertaa tiedostoa"; @@ -720,9 +660,6 @@ /* No comment provided by engineer. */ "Connect via link" = "Yhdistä linkin kautta"; -/* No comment provided by engineer. */ -"Connect via link / QR code" = "Yhdistä linkillä / QR-koodilla"; - /* No comment provided by engineer. */ "Connect via one-time link" = "Yhdistä kertalinkillä"; @@ -753,7 +690,7 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Yhteyden muodostaminen palvelimeen... (virhe: %@)"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "yhdistää…"; /* No comment provided by engineer. */ @@ -795,9 +732,6 @@ /* notification */ "Contact is connected" = "Kontakti on yhdistetty"; -/* No comment provided by engineer. */ -"Contact is not connected yet!" = "Kontaktia ei ole vielä yhdistetty!"; - /* No comment provided by engineer. */ "Contact name" = "Kontaktin nimi"; @@ -813,7 +747,7 @@ /* No comment provided by engineer. */ "Continue" = "Jatka"; -/* chat item action */ +/* No comment provided by engineer. */ "Copy" = "Kopioi"; /* No comment provided by engineer. */ @@ -822,9 +756,6 @@ /* No comment provided by engineer. */ "Create" = "Luo"; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Luo osoite, jolla ihmiset voivat ottaa sinuun yhteyttä."; - /* server test step */ "Create file" = "Luo tiedosto"; @@ -838,7 +769,7 @@ "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Luo uusi profiili [työpöytäsovelluksessa](https://simplex.chat/downloads/). 💻"; /* No comment provided by engineer. */ -"Create one-time invitation link" = "Luo kertakutsulinkki"; +"Create profile" = "Luo profiilisi"; /* server test step */ "Create queue" = "Luo jono"; @@ -852,9 +783,6 @@ /* No comment provided by engineer. */ "Create your profile" = "Luo profiilisi"; -/* No comment provided by engineer. */ -"Created on %@" = "Luotu %@"; - /* No comment provided by engineer. */ "creator" = "luoja"; @@ -942,7 +870,8 @@ /* message decrypt error item */ "Decryption error" = "Salauksen purkuvirhe"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "oletusarvo (%@)"; /* No comment provided by engineer. */ @@ -951,7 +880,8 @@ /* No comment provided by engineer. */ "default (yes)" = "oletusarvo (kyllä)"; -/* chat item action */ +/* alert action +swipe action */ "Delete" = "Poista"; /* No comment provided by engineer. */ @@ -966,12 +896,6 @@ /* No comment provided by engineer. */ "Delete all files" = "Poista kaikki tiedostot"; -/* No comment provided by engineer. */ -"Delete archive" = "Poista arkisto"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Poista keskusteluarkisto?"; - /* No comment provided by engineer. */ "Delete chat profile" = "Poista keskusteluprofiili"; @@ -984,9 +908,6 @@ /* No comment provided by engineer. */ "Delete contact" = "Poista kontakti"; -/* No comment provided by engineer. */ -"Delete Contact" = "Poista kontakti"; - /* No comment provided by engineer. */ "Delete database" = "Poista tietokanta"; @@ -1026,7 +947,7 @@ /* No comment provided by engineer. */ "Delete message?" = "Poista viesti?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "Poista viestit"; /* No comment provided by engineer. */ @@ -1038,9 +959,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "Poista vanha tietokanta?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "Poista vireillä oleva yhteys"; - /* No comment provided by engineer. */ "Delete pending connection?" = "Poistetaanko odottava yhteys?"; @@ -1105,7 +1023,7 @@ "Direct messages" = "Yksityisviestit"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Yksityisviestit jäsenten välillä ovat kiellettyjä tässä ryhmässä."; +"Direct messages between members are prohibited." = "Yksityisviestit jäsenten välillä ovat kiellettyjä tässä ryhmässä."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Poista käytöstä (pidä ohitukset)"; @@ -1129,7 +1047,7 @@ "Disappearing messages are prohibited in this chat." = "Katoavat viestit ovat kiellettyjä tässä keskustelussa."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Katoavat viestit ovat kiellettyjä tässä ryhmässä."; +"Disappearing messages are prohibited." = "Katoavat viestit ovat kiellettyjä tässä ryhmässä."; /* No comment provided by engineer. */ "Disappears at" = "Katoaa klo"; @@ -1188,7 +1106,7 @@ /* No comment provided by engineer. */ "Enable (keep overrides)" = "Salli (pidä ohitukset)"; -/* No comment provided by engineer. */ +/* alert title */ "Enable automatic message deletion?" = "Ota automaattinen viestien poisto käyttöön?"; /* No comment provided by engineer. */ @@ -1320,9 +1238,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Virhe kontaktipyynnön hyväksymisessä"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Virhe tietokantatiedoston käyttämisessä"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Virhe lisättäessä jäseniä"; @@ -1359,9 +1274,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Virhe yhteyden poistamisessa"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Virhe kontaktin poistamisessa"; - /* No comment provided by engineer. */ "Error deleting database" = "Virhe tietokannan poistamisessa"; @@ -1392,18 +1304,12 @@ /* No comment provided by engineer. */ "Error joining group" = "Virhe ryhmään liittymisessä"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Virhe %@-palvelimien lataamisessa"; - -/* No comment provided by engineer. */ +/* alert title */ "Error receiving file" = "Virhe tiedoston vastaanottamisessa"; /* No comment provided by engineer. */ "Error removing member" = "Virhe poistettaessa jäsentä"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "Virhe %@ palvelimien tallentamisessa"; - /* No comment provided by engineer. */ "Error saving group profile" = "Virhe ryhmäprofiilin tallentamisessa"; @@ -1434,7 +1340,7 @@ /* No comment provided by engineer. */ "Error stopping chat" = "Virhe keskustelun lopettamisessa"; -/* No comment provided by engineer. */ +/* alertTitle */ "Error switching profile!" = "Virhe profiilin vaihdossa!"; /* No comment provided by engineer. */ @@ -1455,7 +1361,9 @@ /* No comment provided by engineer. */ "Error: " = "Virhe: "; -/* No comment provided by engineer. */ +/* alert message +file error text +snd error text */ "Error: %@" = "Virhe: %@"; /* No comment provided by engineer. */ @@ -1467,9 +1375,6 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Jopa kun ei käytössä keskustelussa."; -/* No comment provided by engineer. */ -"event happened" = "tapahtuma tapahtui"; - /* No comment provided by engineer. */ "Exit without saving" = "Poistu tallentamatta"; @@ -1491,7 +1396,7 @@ /* No comment provided by engineer. */ "Fast and no wait until the sender is online!" = "Nopea ja ei odotusta, kunnes lähettäjä on online-tilassa!"; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "Suosikki"; /* No comment provided by engineer. */ @@ -1513,7 +1418,7 @@ "Files and media" = "Tiedostot ja media"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Tiedostot ja media ovat tässä ryhmässä kiellettyjä."; +"Files and media are prohibited." = "Tiedostot ja media ovat tässä ryhmässä kiellettyjä."; /* No comment provided by engineer. */ "Files and media prohibited!" = "Tiedostot ja media kielletty!"; @@ -1557,9 +1462,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Koko nimi (valinnainen)"; -/* No comment provided by engineer. */ -"Full name:" = "Koko nimi:"; - /* No comment provided by engineer. */ "Fully re-implemented - work in background!" = "Täysin uudistettu - toimii taustalla!"; @@ -1599,24 +1501,6 @@ /* No comment provided by engineer. */ "Group links" = "Ryhmälinkit"; -/* No comment provided by engineer. */ -"Group members can add message reactions." = "Ryhmän jäsenet voivat lisätä viestireaktioita."; - -/* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages." = "Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti."; - -/* No comment provided by engineer. */ -"Group members can send direct messages." = "Ryhmän jäsenet voivat lähettää suoraviestejä."; - -/* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Ryhmän jäsenet voivat lähettää katoavia viestejä."; - -/* No comment provided by engineer. */ -"Group members can send files and media." = "Ryhmän jäsenet voivat lähettää tiedostoja ja mediaa."; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "Ryhmän jäsenet voivat lähettää ääniviestejä."; - /* notification */ "Group message:" = "Ryhmäviesti:"; @@ -1674,9 +1558,6 @@ /* time unit */ "hours" = "tuntia"; -/* No comment provided by engineer. */ -"How it works" = "Kuinka se toimii"; - /* No comment provided by engineer. */ "How SimpleX works" = "Miten SimpleX toimii"; @@ -1695,9 +1576,6 @@ /* No comment provided by engineer. */ "If you can't meet in person, show QR code in a video call, or share the link." = "Jos et voi tavata henkilökohtaisesti, näytä QR-koodi videopuhelussa tai jaa linkki."; -/* No comment provided by engineer. */ -"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Jos et voi tavata henkilökohtaisesti, voit **skannata QR-koodin videopuhelussa** tai kontaktisi voi jakaa kutsulinkin."; - /* No comment provided by engineer. */ "If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Jos syötät tämän pääsykoodin sovellusta avatessasi, kaikki sovelluksen tiedot poistetaan peruuttamattomasti!"; @@ -1720,7 +1598,7 @@ "Immediately" = "Heti"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Immuuni roskapostille ja väärinkäytöksille"; +"Immune to spam" = "Immuuni roskapostille ja väärinkäytöksille"; /* No comment provided by engineer. */ "Import" = "Tuo"; @@ -1789,10 +1667,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Asenna [SimpleX Chat terminaalille](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "Välittömät push-ilmoitukset ovat piilossa!\n"; +"Instant" = "Heti"; /* No comment provided by engineer. */ -"Instantly" = "Heti"; +"Instant push notifications will be hidden!\n" = "Välittömät push-ilmoitukset ovat piilossa!\n"; /* No comment provided by engineer. */ "Interface" = "Käyttöliittymä"; @@ -1809,7 +1687,7 @@ /* invalid chat item */ "invalid data" = "virheelliset tiedot"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Virheellinen palvelinosoite!"; /* item status text */ @@ -1855,7 +1733,7 @@ "Irreversible message deletion is prohibited in this chat." = "Viestien peruuttamaton poisto on kielletty tässä keskustelussa."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "Viestien peruuttamaton poisto on kielletty tässä ryhmässä."; +"Irreversible message deletion is prohibited." = "Viestien peruuttamaton poisto on kielletty tässä ryhmässä."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Se mahdollistaa useiden nimettömien yhteyksien muodostamisen yhdessä keskusteluprofiilissa ilman, että niiden välillä on jaettuja tietoja."; @@ -1878,7 +1756,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Japanilainen käyttöliittymä"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Liity"; /* No comment provided by engineer. */ @@ -1908,7 +1786,7 @@ /* No comment provided by engineer. */ "Learn more" = "Lue lisää"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Poistu"; /* No comment provided by engineer. */ @@ -1950,24 +1828,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Lukitustila"; -/* No comment provided by engineer. */ -"Make a private connection" = "Luo yksityinen yhteys"; - /* No comment provided by engineer. */ "Make one message disappear" = "Hävitä yksi viesti"; /* No comment provided by engineer. */ "Make profile private!" = "Tee profiilista yksityinen!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Varmista, että %@-palvelinosoitteet ovat oikeassa muodossa, että ne on erotettu toisistaan riveittäin ja että ne eivät ole päällekkäisiä (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Varmista, että WebRTC ICE -palvelinosoitteet ovat oikeassa muodossa, rivieroteltuina ja että ne eivät ole päällekkäisiä."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Monet ihmiset kysyivät: *Jos SimpleX:llä ei ole käyttäjätunnuksia, miten se voi toimittaa viestejä?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Merkitse poistetuksi kaikilta"; @@ -2004,6 +1873,24 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Jäsen poistetaan ryhmästä - tätä ei voi perua!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Ryhmän jäsenet voivat lisätä viestireaktioita."; + +/* 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)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Ryhmän jäsenet voivat lähettää suoraviestejä."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Ryhmän jäsenet voivat lähettää katoavia viestejä."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Ryhmän jäsenet voivat lähettää tiedostoja ja mediaa."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Ryhmän jäsenet voivat lähettää ääniviestejä."; + /* item status text */ "Message delivery error" = "Viestin toimitusvirhe"; @@ -2020,7 +1907,7 @@ "Message reactions are prohibited in this chat." = "Viestireaktiot ovat kiellettyjä tässä keskustelussa."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Viestireaktiot ovat kiellettyjä tässä ryhmässä."; +"Message reactions are prohibited." = "Viestireaktiot ovat kiellettyjä tässä ryhmässä."; /* notification */ "message received" = "viesti vastaanotettu"; @@ -2047,7 +1934,7 @@ "Migration is completed" = "Siirto on valmis"; /* No comment provided by engineer. */ -"Migrations: %@" = "Siirrot: %@"; +"Migrations:" = "Siirrot:"; /* time unit */ "minutes" = "minuuttia"; @@ -2067,7 +1954,7 @@ /* copied message info */ "Moderated at: %@" = "Moderoitu klo: %@"; -/* No comment provided by engineer. */ +/* marked deleted chat item preview text */ "moderated by %@" = "%@ moderoi"; /* time unit */ @@ -2079,19 +1966,16 @@ /* item status description */ "Most likely this connection is deleted." = "Todennäköisesti tämä yhteys on poistettu."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Todennäköisesti tämä kontakti on poistanut yhteyden sinuun."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Useita keskusteluprofiileja"; -/* No comment provided by engineer. */ +/* notification label action */ "Mute" = "Mykistä"; /* No comment provided by engineer. */ "Muted when inactive!" = "Mykistetty ei-aktiivisena!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Nimi"; /* No comment provided by engineer. */ @@ -2103,7 +1987,7 @@ /* No comment provided by engineer. */ "Network status" = "Verkon tila"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "ei koskaan"; /* notification */ @@ -2112,9 +1996,6 @@ /* notification */ "New contact:" = "Uusi kontakti:"; -/* No comment provided by engineer. */ -"New database archive" = "Uusi tietokanta-arkisto"; - /* No comment provided by engineer. */ "New display name" = "Uusi näyttönimi"; @@ -2172,12 +2053,18 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Ei lupaa ääniviestin tallentamiseen"; +/* No comment provided by engineer. */ +"No push server" = "Paikallinen"; + /* No comment provided by engineer. */ "No received or sent files" = "Ei vastaanotettuja tai lähetettyjä tiedostoja"; /* copied message info in history */ "no text" = "ei tekstiä"; +/* No comment provided by engineer. */ +"No user identifiers." = "Ensimmäinen alusta ilman käyttäjätunnisteita – suunniteltu yksityiseksi."; + /* No comment provided by engineer. */ "Notifications" = "Ilmoitukset"; @@ -2191,31 +2078,25 @@ "observer" = "tarkkailija"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "pois"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Pois"; -/* No comment provided by engineer. */ -"Off (Local)" = "Pois (Paikallinen)"; - /* feature offered item */ "offered %@" = "tarjottu %@"; /* feature offered item */ "offered %@: %@" = "tarjottu %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Ok"; /* No comment provided by engineer. */ "Old database" = "Vanha tietokanta"; -/* No comment provided by engineer. */ -"Old database archive" = "Vanha tietokanta-arkisto"; - /* group pref value */ "on" = "päällä"; @@ -2223,16 +2104,16 @@ "One-time invitation link" = "Kertakutsulinkki"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Yhteyden muodostamiseen tarvitaan Onion-isäntiä. Edellyttää VPN:n sallimista."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Yhteyden muodostamiseen tarvitaan Onion-isäntiä.\nEdellyttää VPN:n sallimista."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Onion-isäntiä käytetään, kun niitä on saatavilla. Edellyttää VPN:n sallimista."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Onion-isäntiä käytetään, kun niitä on saatavilla.\nEdellyttää VPN:n sallimista."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion-isäntiä ei käytetä."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Vain asiakaslaitteet tallentavat käyttäjäprofiileja, yhteystietoja, ryhmiä ja viestejä, jotka on lähetetty **kaksinkertaisella päästä päähän -salauksella**."; +"Only client devices store user profiles, contacts, groups, and messages." = "Vain asiakaslaitteet tallentavat käyttäjäprofiileja, yhteystietoja, ryhmiä ja viestejä, jotka on lähetetty **kaksinkertaisella päästä päähän -salauksella**."; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Vain ryhmän omistajat voivat muuttaa ryhmän asetuksia."; @@ -2247,7 +2128,7 @@ "Only you can add message reactions." = "Vain sinä voit lisätä viestireaktioita."; /* No comment provided by engineer. */ -"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "Vain sinä voit poistaa viestejä peruuttamattomasti (kontaktisi voi merkitä ne poistettavaksi)."; +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Vain sinä voit poistaa viestejä peruuttamattomasti (kontaktisi voi merkitä ne poistettavaksi). (24 tuntia)"; /* No comment provided by engineer. */ "Only you can make calls." = "Vain sinä voit soittaa puheluita."; @@ -2262,7 +2143,7 @@ "Only your contact can add message reactions." = "Vain kontaktisi voi lisätä viestireaktioita."; /* No comment provided by engineer. */ -"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Vain kontaktisi voi poistaa viestejä peruuttamattomasti (voit merkitä ne poistettavaksi)."; +"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Vain kontaktisi voi poistaa viestejä peruuttamattomasti (voit merkitä ne poistettavaksi). (24 tuntia)"; /* No comment provided by engineer. */ "Only your contact can make calls." = "Vain kontaktisi voi soittaa puheluita."; @@ -2282,15 +2163,6 @@ /* No comment provided by engineer. */ "Open Settings" = "Avaa Asetukset"; -/* authentication reason */ -"Open user profiles" = "Avaa käyttäjäprofiilit"; - -/* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Avoimen lähdekoodin protokolla ja koodi - kuka tahansa voi käyttää palvelimia."; - -/* No comment provided by engineer. */ -"Opening database…" = "Avataan tietokantaa…"; - /* member role */ "owner" = "omistaja"; @@ -2312,26 +2184,14 @@ /* No comment provided by engineer. */ "Password to show" = "Salasana näytettäväksi"; -/* No comment provided by engineer. */ -"Paste" = "Liitä"; - /* No comment provided by engineer. */ "Paste image" = "Liitä kuva"; -/* No comment provided by engineer. */ -"Paste received link" = "Liitä vastaanotettu linkki"; - -/* placeholder */ -"Paste the link you received to connect with your contact." = "Liitä saamasi linkki, jonka avulla voit muodostaa yhteyden kontaktiisi."; - /* No comment provided by engineer. */ "peer-to-peer" = "vertais"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Ihmiset voivat ottaa sinuun yhteyttä vain jakamiesi linkkien kautta."; - -/* No comment provided by engineer. */ -"Periodically" = "Ajoittain"; +"Periodic" = "Ajoittain"; /* message decrypt error item */ "Permanent decryption error" = "Pysyvä salauksen purkuvirhe"; @@ -2387,9 +2247,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Säilytä viimeinen viestiluonnos liitteineen."; -/* No comment provided by engineer. */ -"Preset server" = "Esiasetettu palvelin"; - /* No comment provided by engineer. */ "Preset server address" = "Esiasetettu palvelimen osoite"; @@ -2414,7 +2271,7 @@ /* No comment provided by engineer. */ "Profile password" = "Profiilin salasana"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "Profiilipäivitys lähetetään kontakteillesi."; /* No comment provided by engineer. */ @@ -2462,14 +2319,14 @@ /* chat item menu */ "React…" = "Reagoi…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Lue"; /* No comment provided by engineer. */ "Read more" = "Lue lisää"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"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)."; @@ -2477,9 +2334,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Lue lisää [GitHub-arkistosta](https://github.com/simplex-chat/simplex-chat#readme)."; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Lue lisää GitHub-tietovarastostamme."; - /* No comment provided by engineer. */ "Receipts are disabled" = "Kuittaukset pois käytöstä"; @@ -2528,7 +2382,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Pienempi akun käyttö"; -/* reject incoming call via notification */ +/* reject incoming call via notification +swipe action */ "Reject" = "Hylkää"; /* No comment provided by engineer. */ @@ -2612,9 +2467,6 @@ /* chat item action */ "Reveal" = "Paljasta"; -/* No comment provided by engineer. */ -"Revert" = "Palauta"; - /* No comment provided by engineer. */ "Revoke" = "Peruuta"; @@ -2630,13 +2482,14 @@ /* No comment provided by engineer. */ "Run chat" = "Käynnistä chat"; -/* chat item action */ +/* alert button +chat item action */ "Save" = "Tallenna"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Tallenna (ja ilmoita kontakteille)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Tallenna ja ilmoita kontaktille"; /* No comment provided by engineer. */ @@ -2645,12 +2498,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "Tallenna ja päivitä ryhmäprofiili"; -/* No comment provided by engineer. */ -"Save archive" = "Tallenna arkisto"; - -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Tallenna automaattisen hyväksynnän asetukset"; - /* No comment provided by engineer. */ "Save group profile" = "Tallenna ryhmäprofiili"; @@ -2660,7 +2507,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Tallenna tunnuslause Avainnippuun"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Tallenna asetukset?"; /* No comment provided by engineer. */ @@ -2669,12 +2516,9 @@ /* No comment provided by engineer. */ "Save servers" = "Tallenna palvelimet"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Tallenna palvelimet?"; -/* No comment provided by engineer. */ -"Save settings?" = "Tallenna asetukset?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Tallenna tervetuloviesti?"; @@ -2717,7 +2561,7 @@ /* chat item text */ "security code changed" = "turvakoodi on muuttunut"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Valitse"; /* No comment provided by engineer. */ @@ -2741,9 +2585,6 @@ /* No comment provided by engineer. */ "Send delivery receipts to" = "Lähetä toimituskuittaukset vastaanottajalle"; -/* No comment provided by engineer. */ -"Send direct message" = "Lähetä yksityisviesti"; - /* No comment provided by engineer. */ "Send disappearing message" = "Lähetä katoava viesti"; @@ -2756,9 +2597,6 @@ /* No comment provided by engineer. */ "Send notifications" = "Lähetys ilmoitukset"; -/* No comment provided by engineer. */ -"Send notifications:" = "Lähetys ilmoitukset:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "Lähetä kysymyksiä ja ideoita"; @@ -2768,7 +2606,7 @@ /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Lähetä ne galleriasta tai mukautetuista näppäimistöistä."; -/* No comment provided by engineer. */ +/* alert message */ "Sender cancelled file transfer." = "Lähettäjä peruutti tiedoston siirron."; /* No comment provided by engineer. */ @@ -2852,7 +2690,8 @@ /* No comment provided by engineer. */ "Settings" = "Asetukset"; -/* chat item action */ +/* alert action +chat item action */ "Share" = "Jaa"; /* No comment provided by engineer. */ @@ -2861,15 +2700,12 @@ /* No comment provided by engineer. */ "Share address" = "Jaa osoite"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "Jaa osoite kontakteille?"; /* No comment provided by engineer. */ "Share link" = "Jaa linkki"; -/* No comment provided by engineer. */ -"Share one-time invitation link" = "Jaa kertakutsulinkki"; - /* No comment provided by engineer. */ "Share with contacts" = "Jaa kontaktien kanssa"; @@ -2906,7 +2742,7 @@ /* simplex link type */ "SimpleX group link" = "SimpleX-ryhmän linkki"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "SimpleX-linkit"; /* No comment provided by engineer. */ @@ -2933,18 +2769,12 @@ /* No comment provided by engineer. */ "Small groups (max 20)" = "Pienryhmät (max 20)"; -/* No comment provided by engineer. */ -"SMP servers" = "SMP-palvelimet"; - /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Tuonnin aikana tapahtui joitakin ei-vakavia virheitä – saatat nähdä Chat-konsolissa lisätietoja."; /* notification title */ "Somebody" = "Joku"; -/* No comment provided by engineer. */ -"Start a new chat" = "Aloita uusi keskustelu"; - /* No comment provided by engineer. */ "Start chat" = "Aloita keskustelu"; @@ -2957,9 +2787,6 @@ /* No comment provided by engineer. */ "Stop" = "Lopeta"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Pysäytä keskustelu tietokantatoimien mahdollistamiseksi"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Pysäytä keskustelut viedäksesi, tuodaksesi tai poistaaksesi keskustelujen tietokannan. Et voi vastaanottaa ja lähettää viestejä, kun keskustelut on pysäytetty."; @@ -2975,10 +2802,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Lopeta tiedoston lähettäminen?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Lopeta jakaminen"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Lopeta osoitteen jakaminen?"; /* authentication reason */ @@ -3014,9 +2841,6 @@ /* No comment provided by engineer. */ "Tap to join incognito" = "Napauta liittyäksesi incognito-tilassa"; -/* No comment provided by engineer. */ -"Tap to start a new chat" = "Aloita uusi keskustelu napauttamalla"; - /* No comment provided by engineer. */ "TCP connection timeout" = "TCP-yhteyden aikakatkaisu"; @@ -3038,7 +2862,7 @@ /* No comment provided by engineer. */ "Test servers" = "Testipalvelimet"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Testit epäonnistuivat!"; /* No comment provided by engineer. */ @@ -3050,9 +2874,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Kiitokset käyttäjille – osallistu Weblaten kautta!"; -/* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Ensimmäinen alusta ilman käyttäjätunnisteita – suunniteltu yksityiseksi."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Sovellus voi ilmoittaa sinulle, kun saat viestejä tai yhteydenottopyyntöjä - avaa asetukset ottaaksesi ne käyttöön."; @@ -3071,6 +2892,9 @@ /* 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."; @@ -3083,14 +2907,11 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "Viesti merkitään moderoiduksi kaikille jäsenille."; -/* No comment provided by engineer. */ -"The next generation of private messaging" = "Seuraavan sukupolven yksityisviestit"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Vanhaa tietokantaa ei poistettu siirron aikana, se voidaan kuitenkin poistaa."; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "Profiili jaetaan vain kontaktiesi kanssa."; +"Your profile is stored on your device and only shared with your contacts." = "Profiili jaetaan vain kontaktiesi kanssa."; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Toinen kuittaus, joka uupui! ✅"; @@ -3101,15 +2922,6 @@ /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "Palvelimet nykyisen keskusteluprofiilisi uusille yhteyksille **%@**."; -/* No comment provided by engineer. */ -"Theme" = "Teema"; - -/* No comment provided by engineer. */ -"There should be at least one user profile." = "Käyttäjäprofiileja tulee olla vähintään yksi."; - -/* No comment provided by engineer. */ -"There should be at least one visible user profile." = "Näkyviä käyttäjäprofiileja tulee olla vähintään yksi."; - /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Nämä asetukset koskevat nykyistä profiiliasi **%@**."; @@ -3146,15 +2958,15 @@ /* No comment provided by engineer. */ "To make a new connection" = "Uuden yhteyden luominen"; -/* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Yksityisyyden suojaamiseksi kaikkien muiden alustojen käyttämien käyttäjätunnusten sijaan SimpleX käyttää viestijonojen tunnisteita, jotka ovat kaikille kontakteille erillisiä."; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Aikavyöhykkeen suojaamiseksi kuva-/äänitiedostot käyttävät UTC:tä."; /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Suojaa tietosi ottamalla SimpleX Lock käyttöön.\nSinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus otetaan käyttöön."; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Yksityisyyden suojaamiseksi kaikkien muiden alustojen käyttämien käyttäjätunnusten sijaan SimpleX käyttää viestijonojen tunnisteita, jotka ovat kaikille kontakteille erillisiä."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Jos haluat nauhoittaa ääniviestin, anna lupa käyttää mikrofonia."; @@ -3179,22 +2991,16 @@ /* No comment provided by engineer. */ "Turn off" = "Sammuta"; -/* No comment provided by engineer. */ -"Turn off notifications?" = "Kytke ilmoitukset pois päältä?"; - /* No comment provided by engineer. */ "Turn on" = "Kytke päälle"; /* No comment provided by engineer. */ "Unable to record voice message" = "Ääniviestiä ei voi tallentaa"; -/* item status description */ -"Unexpected error: %@" = "Odottamaton virhe: %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "Odottamaton siirtotila"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Epäsuotuisa."; /* No comment provided by engineer. */ @@ -3233,36 +3039,27 @@ /* authentication reason */ "Unlock app" = "Avaa sovellus"; -/* No comment provided by engineer. */ +/* notification label action */ "Unmute" = "Poista mykistys"; -/* No comment provided by engineer. */ +/* swipe action */ "Unread" = "Lukematon"; /* No comment provided by engineer. */ "Update" = "Päivitä"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Päivitä .onion-isäntien asetus?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Päivitä tietokannan tunnuslause"; /* No comment provided by engineer. */ "Update network settings?" = "Päivitä verkkoasetukset?"; -/* No comment provided by engineer. */ -"Update transport isolation mode?" = "Päivitä kuljetuksen eristystila?"; - /* rcv group event chat item */ "updated group profile" = "päivitetty ryhmäprofiili"; /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "Asetusten päivittäminen yhdistää asiakkaan uudelleen kaikkiin palvelimiin."; -/* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Tämän asetuksen päivittäminen yhdistää asiakkaan uudelleen kaikkiin palvelimiin."; - /* No comment provided by engineer. */ "Upgrade and open chat" = "Päivitä ja avaa keskustelu"; @@ -3293,12 +3090,6 @@ /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "Käytä SimpleX Chat palvelimia?"; -/* No comment provided by engineer. */ -"User profile" = "Käyttäjäprofiili"; - -/* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = ".onion-isäntien käyttäminen vaatii yhteensopivan VPN-palveluntarjoajan."; - /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Käyttää SimpleX Chat -palvelimia."; @@ -3354,7 +3145,7 @@ "Voice messages are prohibited in this chat." = "Ääniviestit ovat kiellettyjä tässä keskustelussa."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Ääniviestit ovat kiellettyjä tässä ryhmässä."; +"Voice messages are prohibited." = "Ääniviestit ovat kiellettyjä tässä ryhmässä."; /* No comment provided by engineer. */ "Voice messages prohibited!" = "Ääniviestit kielletty!"; @@ -3398,9 +3189,6 @@ /* No comment provided by engineer. */ "When available" = "Kun saatavilla"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Kun ihmiset pyytävät yhteyden muodostamista, voit hyväksyä tai hylätä sen."; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Kun jaat inkognitoprofiilin jonkun kanssa, tätä profiilia käytetään ryhmissä, joihin tämä sinut kutsuu."; @@ -3413,15 +3201,9 @@ /* No comment provided by engineer. */ "Wrong passphrase!" = "Väärä tunnuslause!"; -/* No comment provided by engineer. */ -"XFTP servers" = "XFTP-palvelimet"; - /* pref value */ "yes" = "kyllä"; -/* No comment provided by engineer. */ -"You" = "Sinä"; - /* No comment provided by engineer. */ "You accepted connection" = "Hyväksyit yhteyden"; @@ -3449,9 +3231,6 @@ /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Voit vastaanottaa puheluita lukitusnäytöltä ilman laitteen ja sovelluksen todennusta."; -/* No comment provided by engineer. */ -"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "Voit myös muodostaa yhteyden klikkaamalla linkkiä. Jos se avautuu selaimessa, napsauta **Avaa mobiilisovelluksessa**-painiketta."; - /* No comment provided by engineer. */ "You can create it later" = "Voit luoda sen myöhemmin"; @@ -3465,7 +3244,7 @@ "You can hide or mute a user profile - swipe it to the right." = "Voit piilottaa tai mykistää käyttäjäprofiilin pyyhkäisemällä sitä oikealle."; /* notification body */ -"You can now send messages to %@" = "Voit nyt lähettää viestejä %@:lle"; +"You can now chat with %@" = "Voit nyt lähettää viestejä %@:lle"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Voit määrittää lukitusnäytön ilmoituksen esikatselun asetuksista."; @@ -3476,9 +3255,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Voit jakaa tämän osoitteen kontaktiesi kanssa, jotta ne voivat muodostaa yhteyden **%@** kanssa."; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Voit jakaa osoitteesi linkkinä tai QR-koodina - kuka tahansa voi muodostaa yhteyden sinuun."; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Voit aloittaa keskustelun sovelluksen Asetukset / Tietokanta kautta tai käynnistämällä sovelluksen uudelleen"; @@ -3503,14 +3279,11 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "olet vaihtanut %1$@:n roolin %2$@:ksi"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Sinä hallitset, minkä palvelim(i)en kautta **viestit vastaanotetaan**, kontaktisi - palvelimet, joita käytät viestien lähettämiseen niille."; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "Sinua ei voitu todentaa; yritä uudelleen."; /* No comment provided by engineer. */ -"You have no chats" = "Sinulla ei ole keskusteluja"; +"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."; @@ -3578,9 +3351,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Käytät tässä ryhmässä incognito-profiilia. Kontaktien kutsuminen ei ole sallittua, jotta pääprofiilisi ei tule jaetuksi"; -/* No comment provided by engineer. */ -"Your %@ servers" = "%@-palvelimesi"; - /* No comment provided by engineer. */ "Your calls" = "Puhelusi"; @@ -3593,18 +3363,12 @@ /* No comment provided by engineer. */ "Your chat profiles" = "Keskusteluprofiilisi"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Kontaktin tulee olla online-tilassa, jotta yhteys voidaan muodostaa.\nVoit peruuttaa tämän yhteyden ja poistaa kontaktin (ja yrittää myöhemmin uudella linkillä)."; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Yhteyshenkilösi lähetti tiedoston, joka on suurempi kuin tällä hetkellä tuettu enimmäiskoko (%@)."; /* No comment provided by engineer. */ "Your contacts can allow full message deletion." = "Kontaktisi voivat sallia viestien täydellisen poistamisen."; -/* No comment provided by engineer. */ -"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "Kontaktisi SimpleX:ssä näkevät sen.\nVoit muuttaa sitä Asetuksista."; - /* No comment provided by engineer. */ "Your contacts will remain connected." = "Kontaktisi pysyvät yhdistettyinä."; @@ -3627,7 +3391,7 @@ "Your profile **%@** will be shared." = "Profiilisi **%@** jaetaan."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa.\nSimpleX-palvelimet eivät näe profiiliasi."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa. SimpleX-palvelimet eivät näe profiiliasi."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Profiilisi, kontaktisi ja toimitetut viestit tallennetaan laitteellesi."; @@ -3635,9 +3399,6 @@ /* No comment provided by engineer. */ "Your random profile" = "Satunnainen profiilisi"; -/* No comment provided by engineer. */ -"Your server" = "Palvelimesi"; - /* No comment provided by engineer. */ "Your server address" = "Palvelimesi osoite"; @@ -3647,9 +3408,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "SimpleX-osoitteesi"; -/* No comment provided by engineer. */ -"Your SMP servers" = "SMP-palvelimesi"; - -/* No comment provided by engineer. */ -"Your XFTP servers" = "XFTP-palvelimesi"; - diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 470e23b4fc..4dd75039dc 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -1,18 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (peut être copié)"; @@ -31,30 +16,15 @@ /* No comment provided by engineer. */ "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- messages vocaux pouvant durer jusqu'à 5 minutes.\n- délai personnalisé de disparition.\n- l'historique de modification."; -/* No comment provided by engineer. */ -", " = ", "; - -/* No comment provided by engineer. */ -": " = ": "; - /* No comment provided by engineer. */ "!1 colored!" = "!1 coloré!"; -/* No comment provided by engineer. */ -"." = "."; - -/* No comment provided by engineer. */ -"(" = "("; - /* No comment provided by engineer. */ "(new)" = "(nouveau)"; /* No comment provided by engineer. */ "(this device v%@)" = "(cet appareil v%@)"; -/* No comment provided by engineer. */ -")" = ")"; - /* No comment provided by engineer. */ "[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Contribuer](https://github.com/simplex-chat/simplex-chat#contribute)"; @@ -65,10 +35,10 @@ "[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. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Ajouter un nouveau contact** : pour créer un lien ou code QR unique pour votre contact."; +"**Create 1-time link**: to create and share a new invitation link." = "**Ajouter un contact** : pour créer un nouveau lien d'invitation."; /* No comment provided by engineer. */ -"**Create link / QR code** for your contact to use." = "**Créer un lien / code QR** que votre contact pourra utiliser."; +"**Create group**: to create a new group." = "**Créer un groupe** : pour créer un nouveau groupe."; /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "appel audio **chiffré de bout en bout**"; @@ -77,26 +47,29 @@ "**e2e encrypted** video call" = "appel vidéo **chiffré de bout en bout**"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Vie privée** : vérification de nouveaux messages toute les 20 minutes. Le token de l'appareil est partagé avec le serveur SimpleX, mais pas le nombre de messages ou de contacts."; +"**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." = "**Vie privée** : vérification de nouveaux messages toute les 20 minutes. Le token de l'appareil est partagé avec le serveur SimpleX, mais pas le nombre de messages ou de contacts."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Confidentiel** : ne pas utiliser le serveur de notifications SimpleX, vérification de nouveaux messages periodiquement en arrière plan (dépend de l'utilisation de l'app)."; +"**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." = "**Confidentiel** : ne pas utiliser le serveur de notifications SimpleX, vérification de nouveaux messages periodiquement en arrière plan (dépend de l'utilisation de l'app)."; /* No comment provided by engineer. */ -"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Collez le lien reçu** ou ouvrez-le dans votre navigateur et appuyez sur **Open in mobile app**."; +"**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Remarque** : l'utilisation de la même base de données sur deux appareils interrompt le déchiffrement des messages provenant de vos connexions, par mesure de sécurité."; /* No comment provided by engineer. */ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Veuillez noter** : vous NE pourrez PAS récupérer ou modifier votre phrase secrète si vous la perdez."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Recommandé** : le token de l'appareil et les notifications sont envoyés au serveur de notifications SimpleX, mais pas le contenu du message, sa taille ou son auteur."; +"**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." = "**Recommandé** : le token de l'appareil et les notifications sont envoyés au serveur de notifications SimpleX, mais pas le contenu du message, sa taille ou son auteur."; /* No comment provided by engineer. */ -"**Scan QR code**: to connect to your contact in person or via video call." = "**Scanner le code QR** : pour vous connecter à votre contact en personne ou par appel vidéo."; +"**Scan / Paste link**: to connect via a link you received." = "**Scanner / Coller** : pour vous connecter via un lien que vous avez reçu."; /* 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."; +/* No comment provided by engineer. */ +"**Warning**: the archive will be removed." = "**Avertissement** : l'archive sera supprimée."; + /* No comment provided by engineer. */ "*bold*" = "\\*gras*"; @@ -139,6 +112,9 @@ /* No comment provided by engineer. */ "%@ connected" = "%@ connecté(e)"; +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ téléchargé"; + /* notification title */ "%@ is connected!" = "%@ est connecté·e !"; @@ -148,12 +124,21 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ est vérifié·e"; +/* No comment provided by engineer. */ +"%@ server" = "Serveur %@"; + /* No comment provided by engineer. */ "%@ servers" = "Serveurs %@"; +/* No comment provided by engineer. */ +"%@ uploaded" = "%@ envoyé"; + /* notification title */ "%@ wants to connect!" = "%@ veut se connecter !"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + /* No comment provided by engineer. */ "%@, %@ and %lld members" = "%@, %@ et %lld membres"; @@ -166,9 +151,24 @@ /* time interval */ "%d days" = "%d jours"; +/* forward confirmation reason */ +"%d file(s) are still being downloaded." = "%d fichier(s) en cours de téléchargement."; + +/* forward confirmation reason */ +"%d file(s) failed to download." = "Le téléchargement de %d fichier(s) a échoué."; + +/* forward confirmation reason */ +"%d file(s) were deleted." = "Le(s) fichier(s) %d a(ont) été supprimé(s)."; + +/* forward confirmation reason */ +"%d file(s) were not downloaded." = "Le(s) fichier(s) %d n'a (n'ont) pas été téléchargé(s)."; + /* time interval */ "%d hours" = "%d heures"; +/* alert title */ +"%d messages not forwarded" = "%d messages non transférés"; + /* time interval */ "%d min" = "%d min"; @@ -178,6 +178,9 @@ /* time interval */ "%d sec" = "%d sec"; +/* delete after time */ +"%d seconds(s)" = "%d seconde(s)"; + /* integrity error chat item */ "%d skipped message(s)" = "%d message·s sauté·s"; @@ -205,6 +208,9 @@ /* No comment provided by engineer. */ "%lld messages blocked" = "%lld messages bloqués"; +/* No comment provided by engineer. */ +"%lld messages blocked by admin" = "%lld messages bloqués par l'administrateur"; + /* No comment provided by engineer. */ "%lld messages marked deleted" = "%lld messages marqués comme supprimés"; @@ -217,9 +223,6 @@ /* No comment provided by engineer. */ "%lld new interface languages" = "%lld nouvelles langues d'interface"; -/* No comment provided by engineer. */ -"%lld second(s)" = "%lld seconde·s"; - /* No comment provided by engineer. */ "%lld seconds" = "%lld secondes"; @@ -265,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 jour"; /* time interval */ @@ -274,14 +278,22 @@ /* No comment provided by engineer. */ "1 minute" = "1 minute"; -/* time interval */ +/* delete after time +time interval */ "1 month" = "1 mois"; -/* time interval */ +/* delete after time +time interval */ "1 week" = "1 semaine"; +/* delete after time */ +"1 year" = "1 an"; + /* No comment provided by engineer. */ -"1-time link" = "Lien à usage unique"; +"1-time link" = "Lien unique"; + +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Le lien unique peut être utilisé *avec un seul contact* - partagez le en personne ou via n'importe quelle messagerie."; /* No comment provided by engineer. */ "5 minutes" = "5 minutes"; @@ -308,7 +320,7 @@ "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." = "Une connexion TCP distincte sera utilisée **pour chaque contact et membre de groupe**.\n**Veuillez noter** : si vous avez de nombreuses connexions, votre consommation de batterie et de réseau peut être nettement plus élevée et certaines liaisons peuvent échouer."; /* No comment provided by engineer. */ -"Abort" = "Annuler"; +"Abort" = "Abandonner"; /* No comment provided by engineer. */ "Abort changing address" = "Annuler le changement d'adresse"; @@ -317,10 +329,7 @@ "Abort changing address?" = "Abandonner le changement d'adresse ?"; /* No comment provided by engineer. */ -"About SimpleX" = "À propos de SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "À propos de l'adresse SimpleX"; +"About operators" = "À propos des opérateurs"; /* No comment provided by engineer. */ "About SimpleX Chat" = "À propos de SimpleX Chat"; @@ -329,65 +338,133 @@ "above, then choose:" = "ci-dessus, puis choisissez :"; /* No comment provided by engineer. */ -"Accent color" = "Couleur principale"; +"Accent" = "Principale"; /* accept contact request via notification - accept incoming call via notification */ +accept incoming call via notification +swipe action */ "Accept" = "Accepter"; +/* No comment provided by engineer. */ +"Accept conditions" = "Accepter les conditions"; + /* No comment provided by engineer. */ "Accept connection request?" = "Accepter la demande de connexion ?"; /* notification body */ "Accept contact request from %@?" = "Accepter la demande de contact de %@ ?"; -/* accept contact request via notification */ +/* accept contact request via notification +swipe action */ "Accept incognito" = "Accepter en incognito"; /* call status */ "accepted call" = "appel accepté"; +/* No comment provided by engineer. */ +"Accepted conditions" = "Conditions acceptées"; + +/* chat list item title */ +"accepted invitation" = "invitation acceptée"; + +/* No comment provided by engineer. */ +"Acknowledged" = "Reçu avec accusé de réception"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Erreur d'accusé de réception"; + +/* token status text */ +"Active" = "Actif"; + +/* 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 preset servers" = "Ajouter des serveurs prédéfinis"; +"Add friends" = "Ajouter des amis"; + +/* No comment provided by engineer. */ +"Add list" = "Ajouter une liste"; /* No comment provided by engineer. */ "Add profile" = "Ajouter un profil"; /* No comment provided by engineer. */ -"Add server…" = "Ajouter un serveur…"; +"Add server" = "Ajouter un serveur"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Ajoutez des serveurs en scannant des codes QR."; +/* No comment provided by engineer. */ +"Add team members" = "Ajouter des membres à l'équipe"; + /* No comment provided by engineer. */ "Add to another device" = "Ajouter à un autre appareil"; +/* No comment provided by engineer. */ +"Add to list" = "Ajouter à la liste"; + /* No comment provided by engineer. */ "Add welcome message" = "Ajouter un message d'accueil"; +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "Ajoutez les membres de votre équipe aux conversations."; + +/* No comment provided by engineer. */ +"Added media & file servers" = "Ajout de serveurs de médias et de fichiers"; + +/* No comment provided by engineer. */ +"Added message servers" = "Ajout de serveurs de messages"; + +/* No comment provided by engineer. */ +"Additional accent" = "Accent additionnel"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Accent additionnel 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Accent secondaire"; + /* No comment provided by engineer. */ "Address" = "Adresse"; /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "Le changement d'adresse sera annulé. L'ancienne adresse de réception sera utilisée."; +/* No comment provided by engineer. */ +"Address or 1-time link?" = "Adresse ou lien unique ?"; + +/* No comment provided by engineer. */ +"Address settings" = "Paramètres de l'adresse"; + /* member role */ "admin" = "admin"; +/* feature role */ +"admins" = "admins"; + +/* No comment provided by engineer. */ +"Admins can block a member for all." = "Les admins peuvent bloquer un membre pour tous."; + /* No comment provided by engineer. */ "Admins can create the links to join groups." = "Les admins peuvent créer les liens qui permettent de rejoindre les groupes."; /* No comment provided by engineer. */ "Advanced network settings" = "Paramètres réseau avancés"; -/* chat item text */ -"agreeing encryption for %@…" = "accord sur le chiffrement pour %@…"; +/* No comment provided by engineer. */ +"Advanced settings" = "Paramètres avancés"; /* chat item text */ -"agreeing encryption…" = "accord sur le chiffrement…"; +"agreeing encryption for %@…" = "négociation du chiffrement avec %@…"; + +/* chat item text */ +"agreeing encryption…" = "négociation du chiffrement…"; + +/* No comment provided by engineer. */ +"All" = "Tout"; /* No comment provided by engineer. */ "All app data is deleted." = "Toutes les données de l'application sont supprimées."; @@ -395,35 +472,65 @@ /* No comment provided by engineer. */ "All chats and messages will be deleted - this cannot be undone!" = "Toutes les discussions et tous les messages seront supprimés - il est impossible de revenir en arrière !"; +/* alert message */ +"All chats will be removed from the list %@, and the list deleted." = "Tous les chats seront supprimés de la liste %@, et la liste sera supprimée."; + /* No comment provided by engineer. */ "All data is erased when it is entered." = "Toutes les données sont effacées lorsqu'il est saisi."; +/* No comment provided by engineer. */ +"All data is kept private on your device." = "Toutes les données restent confinées dans votre appareil."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Tous les membres du groupe resteront connectés."; +/* feature role */ +"all members" = "tous les membres"; + +/* No comment provided by engineer. */ +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Tous les messages et fichiers sont envoyés **chiffrés de bout en bout**, avec une sécurité post-quantique dans les messages directs."; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone!" = "Tous les messages seront supprimés - il n'est pas possible de revenir en arrière !"; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Tous les messages seront supprimés - impossible de revenir en arrière ! Les messages seront supprimés UNIQUEMENT pour vous."; /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Tous les nouveaux messages de %@ seront cachés !"; +/* profile dropdown */ +"All profiles" = "Tous les profiles"; + +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Tous les rapports seront archivés pour vous."; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Tous vos contacts resteront connectés."; /* No comment provided by engineer. */ "All your contacts will remain connected. Profile update will be sent to your contacts." = "Tous vos contacts resteront connectés. La mise à jour du profil sera envoyée à vos contacts."; +/* No comment provided by engineer. */ +"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Tous vos contacts, conversations et fichiers seront chiffrés en toute sécurité et transférés par morceaux vers les relais XFTP configurés."; + /* No comment provided by engineer. */ "Allow" = "Autoriser"; /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Autoriser les appels que si votre contact les autorise."; +/* No comment provided by engineer. */ +"Allow calls?" = "Autoriser les appels ?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "Autorise les messages éphémères seulement si votre contact vous l’autorise."; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you." = "Autoriser la suppression irréversible des messages uniquement si votre contact vous l'autorise."; +"Allow downgrade" = "Autoriser la rétrogradation"; + +/* No comment provided by engineer. */ +"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Autoriser la suppression irréversible des messages uniquement si votre contact vous l'autorise. (24 heures)"; /* 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."; @@ -438,11 +545,20 @@ "Allow sending disappearing messages." = "Autorise l’envoi de messages éphémères."; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages." = "Autoriser la suppression irréversible de messages envoyés."; +"Allow sharing" = "Autoriser le partage"; + +/* No comment provided by engineer. */ +"Allow to irreversibly delete sent messages. (24 hours)" = "Autoriser la suppression irréversible de messages envoyés. (24 heures)"; + +/* No comment provided by engineer. */ +"Allow to report messsages to moderators." = "Permettre de signaler des messages aux modérateurs."; /* No comment provided by engineer. */ "Allow to send files and media." = "Permet l'envoi de fichiers et de médias."; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "Autorise l'envoi de liens SimpleX."; + /* No comment provided by engineer. */ "Allow to send voice messages." = "Autoriser l'envoi de messages vocaux."; @@ -459,7 +575,7 @@ "Allow your contacts to call you." = "Autorise vos contacts à vous appeler."; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages." = "Autorise vos contacts à supprimer de manière irréversible les messages envoyés."; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Autorise vos contacts à supprimer de manière irréversible les messages envoyés. (24 heures)"; /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Autorise votre contact à envoyer des messages éphémères."; @@ -479,6 +595,9 @@ /* pref value */ "always" = "toujours"; +/* No comment provided by engineer. */ +"Always use private routing." = "Toujours utiliser le routage privé."; + /* No comment provided by engineer. */ "Always use relay" = "Se connecter via relais"; @@ -488,12 +607,21 @@ /* No comment provided by engineer. */ "and %lld other events" = "et %lld autres événements"; +/* report reason */ +"Another reason" = "Autre raison"; + /* 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 : %@"; +/* No comment provided by engineer. */ +"App data migration" = "Transfert des données de l'application"; + /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "L'application chiffre les nouveaux fichiers locaux (sauf les vidéos)."; @@ -506,6 +634,9 @@ /* No comment provided by engineer. */ "App passcode is replaced with self-destruct passcode." = "Le code d'accès de l'application est remplacé par un code d'autodestruction."; +/* No comment provided by engineer. */ +"App session" = "Session de l'app"; + /* No comment provided by engineer. */ "App version" = "Version de l'app"; @@ -515,9 +646,48 @@ /* No comment provided by engineer. */ "Appearance" = "Apparence"; +/* No comment provided by engineer. */ +"Apply" = "Appliquer"; + +/* No comment provided by engineer. */ +"Apply to" = "Appliquer à"; + +/* No comment provided by engineer. */ +"Archive" = "Archiver"; + +/* No comment provided by engineer. */ +"Archive %lld reports?" = "Archiver les rapports %lld ?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "Archiver tous les rapports ?"; + +/* No comment provided by engineer. */ +"Archive and upload" = "Archiver et téléverser"; + +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Archiver les contacts pour discuter plus tard."; + +/* No comment provided by engineer. */ +"Archive report" = "Archiver le rapport"; + +/* No comment provided by engineer. */ +"Archive report?" = "Archiver le rapport ?"; + +/* swipe action */ +"Archive reports" = "Archiver les rapports"; + +/* No comment provided by engineer. */ +"Archived contacts" = "Contacts archivés"; + +/* No comment provided by engineer. */ +"Archiving database" = "Archivage de la base de données"; + /* No comment provided by engineer. */ "Attach" = "Attacher"; +/* No comment provided by engineer. */ +"attempts" = "tentatives"; + /* No comment provided by engineer. */ "Audio & video calls" = "Appels audio et vidéo"; @@ -557,9 +727,15 @@ /* No comment provided by engineer. */ "Auto-accept images" = "Images auto-acceptées"; +/* alert title */ +"Auto-accept settings" = "Paramètres de réception automatique"; + /* No comment provided by engineer. */ "Back" = "Retour"; +/* No comment provided by engineer. */ +"Background" = "Fond"; + /* No comment provided by engineer. */ "Bad desktop address" = "Mauvaise adresse de bureau"; @@ -575,15 +751,45 @@ /* No comment provided by engineer. */ "Bad message ID" = "Mauvais ID de message"; +/* No comment provided by engineer. */ +"Better calls" = "Appels améliorés"; + /* No comment provided by engineer. */ "Better groups" = "Des groupes plus performants"; +/* No comment provided by engineer. */ +"Better groups performance" = "Meilleure performance des groupes"; + +/* No comment provided by engineer. */ +"Better message dates." = "Meilleures dates de messages."; + /* No comment provided by engineer. */ "Better messages" = "Meilleurs messages"; +/* No comment provided by engineer. */ +"Better networking" = "Meilleure gestion de réseau"; + +/* No comment provided by engineer. */ +"Better notifications" = "Notifications améliorées"; + +/* No comment provided by engineer. */ +"Better privacy and security" = "Meilleure protection de la privacité et de la sécurité"; + +/* No comment provided by engineer. */ +"Better security ✅" = "Sécurité accrue ✅"; + +/* No comment provided by engineer. */ +"Better user experience" = "Une meilleure expérience pour l'utilisateur"; + +/* No comment provided by engineer. */ +"Black" = "Noir"; + /* No comment provided by engineer. */ "Block" = "Bloquer"; +/* No comment provided by engineer. */ +"Block for all" = "Bloqué pour tous"; + /* No comment provided by engineer. */ "Block group members" = "Bloquer des membres d'un groupe"; @@ -591,11 +797,30 @@ "Block member" = "Bloquer ce membre"; /* No comment provided by engineer. */ -"Block member?" = "Bloquer ce membre ?"; +"Block member for all?" = "Bloquer le membre pour tous ?"; /* No comment provided by engineer. */ +"Block member?" = "Bloquer ce membre ?"; + +/* marked deleted chat item preview text */ "blocked" = "blocké"; +/* rcv group event chat item */ +"blocked %@" = "%@ bloqué"; + +/* blocked chat item +marked deleted chat item preview text */ +"blocked by admin" = "bloqué par l'administrateur"; + +/* No comment provided by engineer. */ +"Blocked by admin" = "Bloqué par l'administrateur"; + +/* No comment provided by engineer. */ +"Blur for better privacy." = "Rendez les images floues et protégez-les contre les regards indiscrets."; + +/* No comment provided by engineer. */ +"Blur media" = "Flouter les médias"; + /* No comment provided by engineer. */ "bold" = "gras"; @@ -603,7 +828,7 @@ "Both you and your contact can add message reactions." = "Vous et votre contact pouvez ajouter des réactions aux messages."; /* No comment provided by engineer. */ -"Both you and your contact can irreversibly delete sent messages." = "Vous et votre contact êtes tous deux en mesure de supprimer de manière irréversible les messages envoyés."; +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "Vous et votre contact êtes tous deux en mesure de supprimer de manière irréversible les messages envoyés. (24 heures)"; /* No comment provided by engineer. */ "Both you and your contact can make calls." = "Vous et votre contact pouvez tous deux passer des appels."; @@ -617,9 +842,24 @@ /* 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. */ +"Business address" = "Adresse professionnelle"; + +/* No comment provided by engineer. */ +"Business chats" = "Discussions professionnelles"; + +/* No comment provided by engineer. */ +"Businesses" = "Entreprises"; + /* 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"; + /* No comment provided by engineer. */ "Call already ended!" = "Appel déjà terminé !"; @@ -636,7 +876,16 @@ "Calls" = "Appels"; /* No comment provided by engineer. */ -"Can't delete user profile!" = "Impossible de supprimer le profil d'utilisateur !"; +"Calls prohibited!" = "Les appels ne sont pas autorisés !"; + +/* No comment provided by engineer. */ +"Camera not available" = "Caméra non disponible"; + +/* No comment provided by engineer. */ +"Can't call contact" = "Impossible d'appeler le contact"; + +/* No comment provided by engineer. */ +"Can't call member" = "Impossible d'appeler le membre"; /* No comment provided by engineer. */ "Can't invite contact!" = "Impossible d'inviter le contact !"; @@ -645,8 +894,15 @@ "Can't invite contacts!" = "Impossible d'inviter les contacts !"; /* No comment provided by engineer. */ +"Can't message member" = "Impossible d'envoyer un message à ce membre"; + +/* alert action +alert button */ "Cancel" = "Annuler"; +/* No comment provided by engineer. */ +"Cancel migration" = "Annuler le transfert"; + /* feature offered item */ "cancelled %@" = "annulé %@"; @@ -654,11 +910,26 @@ "Cannot access keychain to save database password" = "Impossible d'accéder à la keychain pour enregistrer le mot de passe de la base de données"; /* No comment provided by engineer. */ +"Cannot forward message" = "Impossible de transférer le message"; + +/* alert title */ "Cannot receive file" = "Impossible de recevoir le fichier"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Capacité dépassée - le destinataire n'a pas pu recevoir les messages envoyés précédemment."; + +/* No comment provided by engineer. */ +"Cellular" = "Cellulaire"; + /* No comment provided by engineer. */ "Change" = "Changer"; +/* alert title */ +"Change automatic message deletion?" = "Modifier la suppression automatique des messages ?"; + +/* authentication reason */ +"Change chat profiles" = "Changer de profil de discussion"; + /* No comment provided by engineer. */ "Change database passphrase?" = "Changer la phrase secrète de la base de données ?"; @@ -684,11 +955,11 @@ "Change self-destruct mode" = "Modifier le mode d'autodestruction"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Modifier le code d'autodestruction"; /* chat item text */ -"changed address for you" = "adresse modifiée pour vous"; +"changed address for you" = "changement de l'adresse du contact"; /* rcv group event chat item */ "changed role of %@ to %@" = "a modifié le rôle de %1$@ pour %2$@"; @@ -703,7 +974,16 @@ "changing address…" = "changement d'adresse…"; /* No comment provided by engineer. */ -"Chat archive" = "Archives du chat"; +"Chat" = "Discussions"; + +/* No comment provided by engineer. */ +"Chat already exists" = "La discussion existe déjà"; + +/* No comment provided by engineer. */ +"Chat already exists!" = "La discussion existe déjà !"; + +/* No comment provided by engineer. */ +"Chat colors" = "Couleurs de chat"; /* No comment provided by engineer. */ "Chat console" = "Console du chat"; @@ -714,6 +994,9 @@ /* No comment provided by engineer. */ "Chat database deleted" = "Base de données du chat supprimée"; +/* No comment provided by engineer. */ +"Chat database exported" = "Exportation de la base de données des discussions"; + /* No comment provided by engineer. */ "Chat database imported" = "Base de données du chat importée"; @@ -723,18 +1006,51 @@ /* No comment provided by engineer. */ "Chat is stopped" = "Le chat est arrêté"; +/* 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." = "Le chat est arrêté. Si vous avez déjà utilisé cette base de données sur un autre appareil, vous devez la transférer à nouveau avant de démarrer le chat."; + +/* No comment provided by engineer. */ +"Chat list" = "Liste de discussion"; + +/* No comment provided by engineer. */ +"Chat migrated!" = "Messagerie transférée !"; + /* No comment provided by engineer. */ "Chat preferences" = "Préférences de chat"; -/* No comment provided by engineer. */ -"Chats" = "Chats"; +/* alert message */ +"Chat preferences were changed." = "Les préférences de discussion ont été modifiées."; /* No comment provided by engineer. */ +"Chat profile" = "Profil d'utilisateur"; + +/* No comment provided by engineer. */ +"Chat theme" = "Thème de chat"; + +/* No comment provided by engineer. */ +"Chat will be deleted for all members - this cannot be undone!" = "La discussion sera supprimé pour tous les membres - cela ne peut pas être annulé !"; + +/* No comment provided by engineer. */ +"Chat will be deleted for you - this cannot be undone!" = "Le discussion sera supprimé pour vous - il n'est pas possible de revenir en arrière !"; + +/* No comment provided by engineer. */ +"Chats" = "Discussions"; + +/* No comment provided by engineer. */ +"Check messages every 20 min." = "Consulter les messages toutes les 20 minutes."; + +/* No comment provided by engineer. */ +"Check messages when allowed." = "Consulter les messages quand c'est possible."; + +/* alert title */ "Check server address and try again." = "Vérifiez l'adresse du serveur et réessayez."; /* No comment provided by engineer. */ "Chinese and Spanish interface" = "Interface en chinois et en espagnol"; +/* No comment provided by engineer. */ +"Choose _Migrate from another device_ on the new device and scan QR code." = "Choisissez _Transferer depuis un autre appareil_ sur le nouvel appareil et scannez le code QR."; + /* No comment provided by engineer. */ "Choose file" = "Choisir le fichier"; @@ -742,6 +1058,15 @@ "Choose from library" = "Choisir dans la photothèque"; /* No comment provided by engineer. */ +"Chunks deleted" = "Chunks supprimés"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Chunks téléchargés"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Chunks téléversés"; + +/* swipe action */ "Clear" = "Effacer"; /* No comment provided by engineer. */ @@ -750,14 +1075,29 @@ /* No comment provided by engineer. */ "Clear conversation?" = "Effacer la conversation ?"; +/* No comment provided by engineer. */ +"Clear group?" = "Vider le groupe ?"; + +/* No comment provided by engineer. */ +"Clear or delete group?" = "Vider ou supprimer le groupe ?"; + +/* No comment provided by engineer. */ +"Clear private notes?" = "Effacer les notes privées ?"; + /* No comment provided by engineer. */ "Clear verification" = "Retirer la vérification"; +/* No comment provided by engineer. */ +"Color chats with the new themes." = "Colorez vos discussions avec les nouveaux thèmes."; + +/* No comment provided by engineer. */ +"Color mode" = "Mode de couleur"; + /* No comment provided by engineer. */ "colored" = "coloré"; -/* No comment provided by engineer. */ -"Colors" = "Couleurs"; +/* report reason */ +"Community guidelines violation" = "Infraction aux règles communautaires"; /* server test step */ "Compare file" = "Comparer le fichier"; @@ -768,15 +1108,51 @@ /* No comment provided by engineer. */ "complete" = "complet"; +/* No comment provided by engineer. */ +"Completed" = "Complétées"; + +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "Conditions acceptées le : %@."; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "Les conditions sont acceptées pour le(s) opérateur(s) : **%@**."; + +/* No comment provided by engineer. */ +"Conditions are already accepted for these operator(s): **%@**." = "Les conditions sont déjà acceptées pour ces opérateurs : **%@**."; + +/* No comment provided by engineer. */ +"Conditions of use" = "Conditions d'utilisation"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "Les conditions seront acceptées pour le(s) opérateur(s) : **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "Les conditions seront acceptées le : %@."; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "Les conditions seront automatiquement acceptées pour les opérateurs activés le : %@."; + /* 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"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "Confirmer la suppression du contact ?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "Confirmer la mise à niveau de la base de données"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Confirmer les fichiers provenant de serveurs inconnus."; + +/* No comment provided by engineer. */ +"Confirm network settings" = "Confirmer les paramètres réseau"; + /* No comment provided by engineer. */ "Confirm new passphrase…" = "Confirmer la nouvelle phrase secrète…"; @@ -786,6 +1162,15 @@ /* No comment provided by engineer. */ "Confirm password" = "Confirmer le mot de passe"; +/* No comment provided by engineer. */ +"Confirm that you remember database passphrase to migrate it." = "Confirmer que vous vous souvenez de la phrase secrète de la base de données pour la transférer."; + +/* No comment provided by engineer. */ +"Confirm upload" = "Confirmer la transmission"; + +/* token status text */ +"Confirmed" = "Confirmé"; + /* server test step */ "Connect" = "Se connecter"; @@ -796,11 +1181,14 @@ "Connect incognito" = "Se connecter incognito"; /* No comment provided by engineer. */ -"Connect to desktop" = "Se connecter au bureau"; +"Connect to desktop" = "Connexion au bureau"; /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "se connecter aux developpeurs de SimpleX Chat."; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Connectez-vous à vos amis plus rapidement."; + /* No comment provided by engineer. */ "Connect to yourself?" = "Se connecter à soi-même ?"; @@ -816,9 +1204,6 @@ /* No comment provided by engineer. */ "Connect via link" = "Se connecter via un lien"; -/* No comment provided by engineer. */ -"Connect via link / QR code" = "Se connecter via un lien / code QR"; - /* No comment provided by engineer. */ "Connect via one-time link" = "Se connecter via un lien unique"; @@ -828,18 +1213,27 @@ /* No comment provided by engineer. */ "connected" = "connecté"; +/* No comment provided by engineer. */ +"Connected" = "Connecté"; + /* No comment provided by engineer. */ "Connected desktop" = "Bureau connecté"; /* rcv group event chat item */ "connected directly" = "s'est connecté.e de manière directe"; +/* No comment provided by engineer. */ +"Connected servers" = "Serveurs connectés"; + /* No comment provided by engineer. */ "Connected to desktop" = "Connecté au bureau"; /* No comment provided by engineer. */ "connecting" = "connexion"; +/* No comment provided by engineer. */ +"Connecting" = "Connexion"; + /* No comment provided by engineer. */ "connecting (accepted)" = "connexion (acceptée)"; @@ -861,15 +1255,24 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Connexion au serveur… (erreur : %@)"; +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "Connexion au contact, veuillez patienter ou vérifier plus tard !"; + /* No comment provided by engineer. */ "Connecting to desktop" = "Connexion au bureau"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "connexion…"; /* No comment provided by engineer. */ "Connection" = "Connexion"; +/* No comment provided by engineer. */ +"Connection and servers status." = "État de la connexion et des serveurs."; + +/* No comment provided by engineer. */ +"Connection blocked" = "Connexion bloquée"; + /* No comment provided by engineer. */ "Connection error" = "Erreur de connexion"; @@ -879,24 +1282,51 @@ /* chat list item title (it should not be shown */ "connection established" = "connexion établie"; +/* No comment provided by engineer. */ +"Connection is blocked by server operator:\n%@" = "La connexion est bloquée par l'opérateur du serveur :\n%@"; + +/* No comment provided by engineer. */ +"Connection not ready." = "La connexion n'est pas prête."; + +/* No comment provided by engineer. */ +"Connection notifications" = "Notifications de connexion"; + /* No comment provided by engineer. */ "Connection request sent!" = "Demande de connexion envoyée !"; +/* No comment provided by engineer. */ +"Connection requires encryption renegotiation." = "La connexion nécessite une renégociation du cryptage."; + +/* No comment provided by engineer. */ +"Connection security" = "Sécurité des connexions"; + /* No comment provided by engineer. */ "Connection terminated" = "Connexion terminée"; /* No comment provided by engineer. */ "Connection timeout" = "Délai de connexion"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "La connexion avec le bureau s'est arrêtée"; + /* connection information */ "connection:%@" = "connexion : %@"; +/* No comment provided by engineer. */ +"Connections" = "Connexions"; + +/* profile update event chat item */ +"contact %@ changed to %@" = "le contact %1$@ est devenu %2$@"; + /* No comment provided by engineer. */ "Contact allows" = "Votre contact autorise"; /* No comment provided by engineer. */ "Contact already exists" = "Contact déjà existant"; +/* No comment provided by engineer. */ +"Contact deleted!" = "Contact supprimé !"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "Ce contact a le chiffrement de bout en bout"; @@ -910,7 +1340,7 @@ "Contact is connected" = "Le contact est connecté"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "Le contact n'est pas encore connecté !"; +"Contact is deleted." = "Le contact est supprimé."; /* No comment provided by engineer. */ "Contact name" = "Nom du contact"; @@ -918,21 +1348,36 @@ /* No comment provided by engineer. */ "Contact preferences" = "Préférences de contact"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "Le contact sera supprimé - il n'est pas possible de revenir en arrière !"; + /* No comment provided by engineer. */ "Contacts" = "Contacts"; /* No comment provided by engineer. */ "Contacts can mark messages for deletion; you will be able to view them." = "Vos contacts peuvent marquer les messages pour les supprimer ; vous pourrez les consulter."; +/* blocking reason */ +"Content violates conditions of use" = "Le contenu enfreint les conditions d'utilisation"; + /* No comment provided by engineer. */ "Continue" = "Continuer"; -/* chat item action */ +/* No comment provided by engineer. */ +"Conversation deleted!" = "Conversation supprimée !"; + +/* No comment provided by engineer. */ "Copy" = "Copier"; +/* No comment provided by engineer. */ +"Copy error" = "Erreur de copie"; + /* No comment provided by engineer. */ "Core version: v%@" = "Version du cœur : v%@"; +/* No comment provided by engineer. */ +"Corner" = "Coin"; + /* No comment provided by engineer. */ "Correct name to %@?" = "Corriger le nom pour %@ ?"; @@ -940,10 +1385,10 @@ "Create" = "Créer"; /* No comment provided by engineer. */ -"Create a group using a random profile." = "Création de groupes via un profil aléatoire."; +"Create 1-time link" = "Créer un lien unique"; /* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Créez une adresse pour permettre aux gens de vous contacter."; +"Create a group using a random profile." = "Création de groupes via un profil aléatoire."; /* server test step */ "Create file" = "Créer un fichier"; @@ -958,10 +1403,10 @@ "Create link" = "Créer un lien"; /* No comment provided by engineer. */ -"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Créer un nouveau profil sur [l'application de bureau](https://simplex.chat/downloads/). 💻"; +"Create list" = "Créer une liste"; /* No comment provided by engineer. */ -"Create one-time invitation link" = "Créer un lien d'invitation unique"; +"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Créer un nouveau profil sur [l'application de bureau](https://simplex.chat/downloads/). 💻"; /* No comment provided by engineer. */ "Create profile" = "Créer le profil"; @@ -979,17 +1424,35 @@ "Create your profile" = "Créez votre profil"; /* No comment provided by engineer. */ -"Created on %@" = "Créé le %@"; +"Created" = "Créées"; + +/* No comment provided by engineer. */ +"Created at" = "Créé à"; + +/* copied message info */ +"Created at: %@" = "Créé à : %@"; + +/* No comment provided by engineer. */ +"Creating archive link" = "Création d'un lien d'archive"; + +/* No comment provided by engineer. */ +"Creating link…" = "Création d'un lien…"; /* No comment provided by engineer. */ "creator" = "créateur"; +/* No comment provided by engineer. */ +"Current conditions text couldn't be loaded, you can review conditions via this link:" = "Le texte sur les conditions actuelles n'a pas pu être chargé. Vous pouvez consulter les conditions en cliquant sur ce lien :"; + /* No comment provided by engineer. */ "Current Passcode" = "Code d'accès actuel"; /* No comment provided by engineer. */ "Current passphrase…" = "Phrase secrète actuelle…"; +/* No comment provided by engineer. */ +"Current profile" = "Profil actuel"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Actuellement, la taille maximale des fichiers supportés est de %@."; @@ -999,9 +1462,18 @@ /* No comment provided by engineer. */ "Custom time" = "Délai personnalisé"; +/* No comment provided by engineer. */ +"Customizable message shape." = "Forme des messages personnalisable."; + +/* No comment provided by engineer. */ +"Customize theme" = "Personnaliser le thème"; + /* No comment provided by engineer. */ "Dark" = "Sombre"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Couleurs en mode sombre"; + /* No comment provided by engineer. */ "Database downgrade" = "Rétrogradation de la base de données"; @@ -1062,13 +1534,20 @@ /* time unit */ "days" = "jours"; +/* 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"; -/* pref value */ +/* No comment provided by engineer. */ +"decryption errors" = "Erreurs de déchiffrement"; + +/* delete after time +pref value */ "default (%@)" = "défaut (%@)"; /* No comment provided by engineer. */ @@ -1077,9 +1556,13 @@ /* No comment provided by engineer. */ "default (yes)" = "par défaut (oui)"; -/* chat item action */ +/* alert action +swipe action */ "Delete" = "Supprimer"; +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "Supprimer %lld messages de membres ?"; + /* No comment provided by engineer. */ "Delete %lld messages?" = "Supprimer %lld messages ?"; @@ -1099,10 +1582,10 @@ "Delete and notify contact" = "Supprimer et en informer le contact"; /* No comment provided by engineer. */ -"Delete archive" = "Supprimer l'archive"; +"Delete chat" = "Supprimer la discussion"; /* No comment provided by engineer. */ -"Delete chat archive?" = "Supprimer l'archive du chat ?"; +"Delete chat messages from your device." = "Supprimer les messages de chat de votre appareil."; /* No comment provided by engineer. */ "Delete chat profile" = "Supprimer le profil de chat"; @@ -1110,6 +1593,9 @@ /* No comment provided by engineer. */ "Delete chat profile?" = "Supprimer le profil du chat ?"; +/* No comment provided by engineer. */ +"Delete chat?" = "Supprimer la discussion ?"; + /* No comment provided by engineer. */ "Delete connection" = "Supprimer la connexion"; @@ -1117,14 +1603,14 @@ "Delete contact" = "Supprimer le contact"; /* No comment provided by engineer. */ -"Delete Contact" = "Supprimer le contact"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Supprimer le contact ?\nCette opération ne peut être annulée !"; +"Delete contact?" = "Supprimer le contact ?"; /* No comment provided by engineer. */ "Delete database" = "Supprimer la base de données"; +/* No comment provided by engineer. */ +"Delete database from this device" = "Supprimer la base de données de cet appareil"; + /* server test step */ "Delete file" = "Supprimer le fichier"; @@ -1155,13 +1641,16 @@ /* No comment provided by engineer. */ "Delete link?" = "Supprimer le lien ?"; +/* alert title */ +"Delete list?" = "Supprimer la liste ?"; + /* No comment provided by engineer. */ "Delete member message?" = "Supprimer le message de ce membre ?"; /* No comment provided by engineer. */ "Delete message?" = "Supprimer le message ?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "Supprimer les messages"; /* No comment provided by engineer. */ @@ -1174,7 +1663,7 @@ "Delete old database?" = "Supprimer l'ancienne base de données ?"; /* No comment provided by engineer. */ -"Delete pending connection" = "Supprimer la connexion en attente"; +"Delete or moderate up to 200 messages." = "Supprimer ou modérer jusqu'à 200 messages."; /* No comment provided by engineer. */ "Delete pending connection?" = "Supprimer la connexion en attente ?"; @@ -1185,12 +1674,24 @@ /* server test step */ "Delete queue" = "Supprimer la file d'attente"; +/* No comment provided by engineer. */ +"Delete report" = "Supprimer le rapport"; + +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Supprimez jusqu'à 20 messages à la fois."; + /* No comment provided by engineer. */ "Delete user profile?" = "Supprimer le profil utilisateur ?"; +/* No comment provided by engineer. */ +"Delete without notification" = "Supprimer sans notification"; + /* deleted chat item */ "deleted" = "supprimé"; +/* No comment provided by engineer. */ +"Deleted" = "Supprimées"; + /* No comment provided by engineer. */ "Deleted at" = "Supprimé à"; @@ -1203,6 +1704,12 @@ /* rcv group event chat item */ "deleted group" = "groupe supprimé"; +/* No comment provided by engineer. */ +"Deletion errors" = "Erreurs de suppression"; + +/* No comment provided by engineer. */ +"Delivered even when Apple drops them." = "Distribués même quand Apple les oublie."; + /* No comment provided by engineer. */ "Delivery" = "Distribution"; @@ -1210,7 +1717,7 @@ "Delivery receipts are disabled!" = "Les accusés de réception sont désactivés !"; /* No comment provided by engineer. */ -"Delivery receipts!" = "Justificatifs de réception!"; +"Delivery receipts!" = "Justificatifs de réception !"; /* No comment provided by engineer. */ "Description" = "Description"; @@ -1224,9 +1731,27 @@ /* No comment provided by engineer. */ "Desktop devices" = "Appareils de bureau"; +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "L'adresse du serveur de destination %@ est incompatible avec les paramètres du serveur de redirection %@."; + +/* snd error text */ +"Destination server error: %@" = "Erreur du serveur de destination : %@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "La version du serveur de destination %@ est incompatible avec le serveur de redirection %@."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Statistiques détaillées"; + +/* No comment provided by engineer. */ +"Details" = "Détails"; + /* No comment provided by engineer. */ "Develop" = "Développer"; +/* No comment provided by engineer. */ +"Developer options" = "Options pour les développeurs"; + /* No comment provided by engineer. */ "Developer tools" = "Outils du développeur"; @@ -1252,11 +1777,20 @@ "Direct messages" = "Messages directs"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Les messages directs entre membres sont interdits dans ce groupe."; +"Direct messages between members are prohibited in this chat." = "Les messages directs entre membres sont interdits dans cette discussion."; + +/* No comment provided by engineer. */ +"Direct messages between members are prohibited." = "Les messages directs entre membres sont interdits dans ce groupe."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Désactiver (conserver les remplacements)"; +/* alert title */ +"Disable automatic message deletion?" = "Désactiver la suppression automatique des messages ?"; + +/* alert button */ +"Disable delete messages" = "Désactiver la suppression des messages"; + /* No comment provided by engineer. */ "Disable for all" = "Désactiver pour tous"; @@ -1266,6 +1800,9 @@ /* No comment provided by engineer. */ "disabled" = "désactivé"; +/* No comment provided by engineer. */ +"Disabled" = "Désactivé"; + /* No comment provided by engineer. */ "Disappearing message" = "Message éphémère"; @@ -1276,7 +1813,7 @@ "Disappearing messages are prohibited in this chat." = "Les messages éphémères sont interdits dans cette discussion."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Les messages éphémères sont interdits dans ce groupe."; +"Disappearing messages are prohibited." = "Les messages éphémères sont interdits dans ce groupe."; /* No comment provided by engineer. */ "Disappears at" = "Disparaîtra le"; @@ -1299,36 +1836,88 @@ /* No comment provided by engineer. */ "Do it later" = "Faites-le plus tard"; +/* No comment provided by engineer. */ +"Do not send history to new members." = "Ne pas envoyer d'historique aux nouveaux membres."; + +/* No comment provided by engineer. */ +"Do NOT send messages directly, even if your or destination server does not support private routing." = "Ne pas envoyer de messages directement, même si votre serveur ou le serveur de destination ne prend pas en charge le routage privé."; + +/* No comment provided by engineer. */ +"Do not use credentials with proxy." = "Ne pas utiliser d'identifiants avec le proxy."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "Ne pas utiliser de routage privé."; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "N'utilisez PAS SimpleX pour les appels d'urgence."; +/* No comment provided by engineer. */ +"Documents:" = "Documents:"; + /* No comment provided by engineer. */ "Don't create address" = "Ne pas créer d'adresse"; /* No comment provided by engineer. */ "Don't enable" = "Ne pas activer"; +/* No comment provided by engineer. */ +"Don't miss important messages." = "Ne manquez pas les messages importants."; + /* No comment provided by engineer. */ "Don't show again" = "Ne plus afficher"; +/* No comment provided by engineer. */ +"Done" = "Terminé"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "Rétrograder et ouvrir le chat"; +/* alert button +chat item action */ +"Download" = "Télécharger"; + +/* No comment provided by engineer. */ +"Download errors" = "Erreurs de téléchargement"; + +/* No comment provided by engineer. */ +"Download failed" = "Échec du téléchargement"; + /* server test step */ "Download file" = "Télécharger le fichier"; +/* alert action */ +"Download files" = "Télécharger les fichiers"; + +/* No comment provided by engineer. */ +"Downloaded" = "Téléchargé"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Fichiers téléchargés"; + +/* No comment provided by engineer. */ +"Downloading archive" = "Téléchargement de l'archive"; + +/* No comment provided by engineer. */ +"Downloading link details" = "Téléchargement des détails du lien"; + /* No comment provided by engineer. */ "Duplicate display name!" = "Nom d'affichage en double !"; /* integrity error chat item */ "duplicate message" = "message dupliqué"; +/* No comment provided by engineer. */ +"duplicates" = "doublons"; + /* No comment provided by engineer. */ "Duration" = "Durée"; /* No comment provided by engineer. */ "e2e encrypted" = "chiffré de bout en bout"; +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "Notifications chiffrées E2E."; + /* chat item action */ "Edit" = "Modifier"; @@ -1341,12 +1930,21 @@ /* No comment provided by engineer. */ "Enable (keep overrides)" = "Activer (conserver les remplacements)"; -/* No comment provided by engineer. */ +/* alert title */ "Enable automatic message deletion?" = "Activer la suppression automatique des messages ?"; +/* No comment provided by engineer. */ +"Enable camera access" = "Autoriser l'accès à la caméra"; + +/* No comment provided by engineer. */ +"Enable Flux in Network & servers settings for better metadata privacy." = "Activez Flux dans les paramètres du réseau et des serveurs pour une meilleure confidentialité des métadonnées."; + /* No comment provided by engineer. */ "Enable for all" = "Activer pour tous"; +/* No comment provided by engineer. */ +"Enable in direct chats (BETA)!" = "Activer dans les conversations directes (BETA) !"; + /* No comment provided by engineer. */ "Enable instant notifications?" = "Activer les notifications instantanées ?"; @@ -1374,6 +1972,12 @@ /* enabled status */ "enabled" = "activé"; +/* No comment provided by engineer. */ +"Enabled" = "Activé"; + +/* No comment provided by engineer. */ +"Enabled for" = "Activé pour"; + /* enabled status */ "enabled for contact" = "activé pour le contact"; @@ -1398,6 +2002,9 @@ /* notification */ "Encrypted message or another event" = "Message chiffrée ou autre événement"; +/* notification */ +"Encrypted message: app is stopped" = "Message chiffré : l'application est arrêtée"; + /* notification */ "Encrypted message: database error" = "Message chiffrée : erreur de base de données"; @@ -1443,6 +2050,9 @@ /* chat item text */ "encryption re-negotiation required for %@" = "renégociation de chiffrement requise pour %@"; +/* No comment provided by engineer. */ +"Encryption renegotiation in progress." = "Renégociation du chiffrement en cours."; + /* No comment provided by engineer. */ "ended" = "terminé"; @@ -1458,6 +2068,9 @@ /* No comment provided by engineer. */ "Enter Passcode" = "Entrer le code d'accès"; +/* No comment provided by engineer. */ +"Enter passphrase" = "Entrer la phrase secrète"; + /* No comment provided by engineer. */ "Enter passphrase…" = "Entrez la phrase secrète…"; @@ -1488,24 +2101,39 @@ /* No comment provided by engineer. */ "Error aborting address change" = "Erreur lors de l'annulation du changement d'adresse"; +/* alert title */ +"Error accepting conditions" = "Erreur lors de la validation des conditions"; + /* No comment provided by engineer. */ "Error accepting contact request" = "Erreur de validation de la demande de contact"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Erreur d'accès au fichier de la base de données"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Erreur lors de l'ajout de membre·s"; +/* alert title */ +"Error adding server" = "Erreur lors de l'ajout du serveur"; + /* No comment provided by engineer. */ "Error changing address" = "Erreur de changement d'adresse"; +/* No comment provided by engineer. */ +"Error changing connection profile" = "Erreur lors du changement de profil de connexion"; + /* No comment provided by engineer. */ "Error changing role" = "Erreur lors du changement de rôle"; /* No comment provided by engineer. */ "Error changing setting" = "Erreur de changement de paramètre"; +/* No comment provided by engineer. */ +"Error changing to incognito!" = "Erreur lors du passage en mode incognito !"; + +/* No comment provided by engineer. */ +"Error checking token status" = "Erreur lors de la vérification de l'état du jeton (token)"; + +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "Erreur de connexion au serveur de redirection %@. Veuillez réessayer plus tard."; + /* No comment provided by engineer. */ "Error creating address" = "Erreur lors de la création de l'adresse"; @@ -1515,12 +2143,21 @@ /* No comment provided by engineer. */ "Error creating group link" = "Erreur lors de la création du lien du groupe"; +/* alert title */ +"Error creating list" = "Erreur lors de la création de la liste"; + /* No comment provided by engineer. */ "Error creating member contact" = "Erreur lors de la création du contact du membre"; +/* No comment provided by engineer. */ +"Error creating message" = "Erreur lors de la création du message"; + /* No comment provided by engineer. */ "Error creating profile!" = "Erreur lors de la création du profil !"; +/* No comment provided by engineer. */ +"Error creating report" = "Erreur lors de la création du rapport"; + /* No comment provided by engineer. */ "Error decrypting file" = "Erreur lors du déchiffrement du fichier"; @@ -1533,9 +2170,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Erreur lors de la suppression de la connexion"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Erreur lors de la suppression du contact"; - /* No comment provided by engineer. */ "Error deleting database" = "Erreur lors de la suppression de la base de données"; @@ -1548,6 +2182,9 @@ /* No comment provided by engineer. */ "Error deleting user profile" = "Erreur lors de la suppression du profil utilisateur"; +/* No comment provided by engineer. */ +"Error downloading the archive" = "Erreur lors du téléchargement de l'archive"; + /* No comment provided by engineer. */ "Error enabling delivery receipts!" = "Erreur lors de l'activation des accusés de réception !"; @@ -1560,23 +2197,47 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Erreur lors de l'exportation de la base de données du chat"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Erreur d'exportation du thème : %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Erreur lors de l'importation de la base de données du chat"; /* No comment provided by engineer. */ "Error joining group" = "Erreur lors de la liaison avec le groupe"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Erreur lors du chargement des serveurs %@"; +/* alert title */ +"Error loading servers" = "Erreur de chargement des serveurs"; /* No comment provided by engineer. */ +"Error migrating settings" = "Erreur lors de la migration des paramètres"; + +/* No comment provided by engineer. */ +"Error opening chat" = "Erreur lors de l'ouverture du chat"; + +/* alert title */ "Error receiving file" = "Erreur lors de la réception du fichier"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Erreur de reconnexion du serveur"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Erreur de reconnexion des serveurs"; + +/* alert title */ +"Error registering for notifications" = "Erreur lors de l'inscription aux notifications"; + /* No comment provided by engineer. */ "Error removing member" = "Erreur lors de la suppression d'un membre"; +/* alert title */ +"Error reordering lists" = "Erreur lors de la réorganisation des listes"; + /* No comment provided by engineer. */ -"Error saving %@ servers" = "Erreur lors de la sauvegarde des serveurs %@"; +"Error resetting statistics" = "Erreur de réinitialisation des statistiques"; + +/* alert title */ +"Error saving chat list" = "Erreur lors de l'enregistrement de la liste des chats"; /* No comment provided by engineer. */ "Error saving group profile" = "Erreur lors de la sauvegarde du profil de groupe"; @@ -1590,9 +2251,18 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "Erreur lors de l'enregistrement de la phrase de passe dans la keychain"; +/* alert title */ +"Error saving servers" = "Erreur d'enregistrement des serveurs"; + +/* when migrating */ +"Error saving settings" = "Erreur lors de l'enregistrement des paramètres"; + /* No comment provided by engineer. */ "Error saving user password" = "Erreur d'enregistrement du mot de passe de l'utilisateur"; +/* No comment provided by engineer. */ +"Error scanning code: %@" = "Erreur lors du scan du code : %@"; + /* No comment provided by engineer. */ "Error sending email" = "Erreur lors de l'envoi de l'e-mail"; @@ -1612,17 +2282,26 @@ "Error stopping chat" = "Erreur lors de l'arrêt du chat"; /* No comment provided by engineer. */ +"Error switching profile" = "Erreur lors du changement de profil"; + +/* alertTitle */ "Error switching profile!" = "Erreur lors du changement de profil !"; /* No comment provided by engineer. */ "Error synchronizing connection" = "Erreur de synchronisation de connexion"; +/* No comment provided by engineer. */ +"Error testing server connection" = "Erreur lors du test de connexion au serveur"; + /* No comment provided by engineer. */ "Error updating group link" = "Erreur lors de la mise à jour du lien de groupe"; /* No comment provided by engineer. */ "Error updating message" = "Erreur lors de la mise à jour du message"; +/* alert title */ +"Error updating server" = "Erreur de mise à jour du serveur"; + /* No comment provided by engineer. */ "Error updating settings" = "Erreur lors de la mise à jour des paramètres"; @@ -1630,9 +2309,17 @@ "Error updating user privacy" = "Erreur de mise à jour de la confidentialité de l'utilisateur"; /* No comment provided by engineer. */ -"Error: " = "Erreur : "; +"Error uploading the archive" = "Erreur lors de l'envoi de l'archive"; /* No comment provided by engineer. */ +"Error verifying passphrase:" = "Erreur lors de la vérification de la phrase secrète :"; + +/* No comment provided by engineer. */ +"Error: " = "Erreur : "; + +/* alert message +file error text +snd error text */ "Error: %@" = "Erreur : %@"; /* No comment provided by engineer. */ @@ -1641,17 +2328,26 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Erreur : URL invalide"; +/* No comment provided by engineer. */ +"Errors" = "Erreurs"; + +/* servers error */ +"Errors in servers configuration." = "Erreurs dans la configuration des serveurs."; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Même s'il est désactivé dans la conversation."; /* No comment provided by engineer. */ -"event happened" = "event happened"; - -/* No comment provided by engineer. */ -"Exit without saving" = "Quitter sans sauvegarder"; +"Exit without saving" = "Quitter sans enregistrer"; /* chat item action */ -"Expand" = "Développer"; +"Expand" = "Étendre"; + +/* No comment provided by engineer. */ +"expired" = "expiré"; + +/* token status text */ +"Expired" = "Expiré"; /* No comment provided by engineer. */ "Export database" = "Exporter la base de données"; @@ -1659,9 +2355,15 @@ /* No comment provided by engineer. */ "Export error:" = "Erreur lors de l'exportation :"; +/* No comment provided by engineer. */ +"Export theme" = "Exporter le thème"; + /* No comment provided by engineer. */ "Exported database archive." = "Archive de la base de données exportée."; +/* No comment provided by engineer. */ +"Exported file doesn't exist" = "Le fichier exporté n'existe pas"; + /* No comment provided by engineer. */ "Exporting database archive…" = "Exportation de l'archive de la base de données…"; @@ -1671,12 +2373,42 @@ /* No comment provided by engineer. */ "Fast and no wait until the sender is online!" = "Rapide et ne nécessitant pas d'attendre que l'expéditeur soit en ligne !"; +/* No comment provided by engineer. */ +"Faster deletion of groups." = "Suppression plus rapide des groupes."; + /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Connexion plus rapide et messages plus fiables."; /* No comment provided by engineer. */ +"Faster sending messages." = "Envoi plus rapide des messages."; + +/* swipe action */ "Favorite" = "Favoris"; +/* No comment provided by engineer. */ +"Favorites" = "Favoris"; + +/* file error alert title */ +"File error" = "Erreur de fichier"; + +/* alert message */ +"File errors:\n%@" = "Erreurs de fichier :\n%@"; + +/* file error text */ +"File is blocked by server operator:\n%@." = "Le fichier est bloqué par l'opérateur du serveur :\n%@."; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Fichier introuvable - le fichier a probablement été supprimé ou annulé."; + +/* file error text */ +"File server error: %@" = "Erreur de serveur de fichiers : %@"; + +/* No comment provided by engineer. */ +"File status" = "Statut du fichier"; + +/* copied message info */ +"File status: %@" = "Statut du fichier : %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Le fichier sera supprimé des serveurs."; @@ -1689,6 +2421,9 @@ /* No comment provided by engineer. */ "File: %@" = "Fichier : %@"; +/* No comment provided by engineer. */ +"Files" = "Fichiers"; + /* No comment provided by engineer. */ "Files & media" = "Fichiers & médias"; @@ -1696,7 +2431,10 @@ "Files and media" = "Fichiers et médias"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Les fichiers et les médias sont interdits dans ce groupe."; +"Files and media are prohibited." = "Les fichiers et les médias sont interdits dans ce groupe."; + +/* No comment provided by engineer. */ +"Files and media not allowed" = "Fichiers et médias non autorisés"; /* No comment provided by engineer. */ "Files and media prohibited!" = "Fichiers et médias interdits !"; @@ -1704,11 +2442,17 @@ /* No comment provided by engineer. */ "Filter unread and favorite chats." = "Filtrer les messages non lus et favoris."; +/* No comment provided by engineer. */ +"Finalize migration" = "Finaliser le transfert"; + +/* No comment provided by engineer. */ +"Finalize migration on another device." = "Finalisez le transfert sur l'autre appareil."; + /* No comment provided by engineer. */ "Finally, we have them! 🚀" = "Enfin, les voilà ! 🚀"; /* No comment provided by engineer. */ -"Find chats faster" = "Trouver des messages plus rapidement"; +"Find chats faster" = "Recherche de message plus rapide"; /* No comment provided by engineer. */ "Fix" = "Réparer"; @@ -1717,7 +2461,7 @@ "Fix connection" = "Réparer la connexion"; /* No comment provided by engineer. */ -"Fix connection?" = "Réparer la connexion?"; +"Fix connection?" = "Réparer la connexion ?"; /* No comment provided by engineer. */ "Fix encryption after restoring backups." = "Réparer le chiffrement après la restauration des sauvegardes."; @@ -1728,9 +2472,66 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Correction non prise en charge par un membre du groupe"; +/* servers error */ +"For chat profile %@:" = "Pour le profil de discussion %@ :"; + /* No comment provided by engineer. */ "For console" = "Pour la console"; +/* No comment provided by engineer. */ +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Par exemple, si votre contact reçoit des messages via un serveur SimpleX Chat, votre application les transmettra via un serveur Flux."; + +/* No comment provided by engineer. */ +"For private routing" = "Pour le routage privé"; + +/* No comment provided by engineer. */ +"For social media" = "Pour les réseaux sociaux"; + +/* chat item action */ +"Forward" = "Transférer"; + +/* alert title */ +"Forward %d message(s)?" = "Transférer %d message(s) ?"; + +/* No comment provided by engineer. */ +"Forward and save messages" = "Transférer et sauvegarder des messages"; + +/* alert action */ +"Forward messages" = "Transférer les messages"; + +/* alert message */ +"Forward messages without files?" = "Transférer les messages sans les fichiers ?"; + +/* No comment provided by engineer. */ +"Forward up to 20 messages at once." = "Transférez jusqu'à 20 messages à la fois."; + +/* No comment provided by engineer. */ +"forwarded" = "transféré"; + +/* No comment provided by engineer. */ +"Forwarded" = "Transféré"; + +/* No comment provided by engineer. */ +"Forwarded from" = "Transféré depuis"; + +/* No comment provided by engineer. */ +"Forwarding %lld messages" = "Transfert des %lld messages"; + +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Le serveur de redirection %@ n'a pas réussi à se connecter au serveur de destination %@. Veuillez réessayer plus tard."; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "L'adresse du serveur de redirection est incompatible avec les paramètres du réseau : %@."; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "La version du serveur de redirection est incompatible avec les paramètres du réseau : %@."; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Serveur de transfert : %1$@\nErreur du serveur de destination : %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Serveur de transfert : %1$@\nErreur : %2$@"; + /* No comment provided by engineer. */ "Found desktop" = "Bureau trouvé"; @@ -1743,9 +2544,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Nom complet (optionnel)"; -/* No comment provided by engineer. */ -"Full name:" = "Nom complet :"; - /* No comment provided by engineer. */ "Fully decentralized – visible only to members." = "Entièrement décentralisé – visible que par ses membres."; @@ -1758,6 +2556,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIFs et stickers"; +/* message preview */ +"Good afternoon!" = "Bonjour !"; + +/* message preview */ +"Good morning!" = "Bonjour !"; + /* No comment provided by engineer. */ "Group" = "Groupe"; @@ -1794,24 +2598,6 @@ /* No comment provided by engineer. */ "Group links" = "Liens de groupe"; -/* No comment provided by engineer. */ -"Group members can add message reactions." = "Les membres du groupe peuvent ajouter des réactions aux messages."; - -/* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages." = "Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés."; - -/* No comment provided by engineer. */ -"Group members can send direct messages." = "Les membres du groupe peuvent envoyer des messages directs."; - -/* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Les membres du groupes peuvent envoyer des messages éphémères."; - -/* No comment provided by engineer. */ -"Group members can send files and media." = "Les membres du groupe peuvent envoyer des fichiers et des médias."; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "Les membres du groupe peuvent envoyer des messages vocaux."; - /* notification */ "Group message:" = "Message du groupe :"; @@ -1866,11 +2652,17 @@ /* No comment provided by engineer. */ "History" = "Historique"; +/* No comment provided by engineer. */ +"History is not sent to new members." = "L'historique n'est pas envoyé aux nouveaux membres."; + /* time unit */ "hours" = "heures"; /* No comment provided by engineer. */ -"How it works" = "Comment ça fonctionne"; +"How it affects privacy" = "L'impact sur la vie privée"; + +/* No comment provided by engineer. */ +"How it helps privacy" = "Comment il contribue à la protection de la vie privée"; /* No comment provided by engineer. */ "How SimpleX works" = "Comment SimpleX fonctionne"; @@ -1884,15 +2676,15 @@ /* No comment provided by engineer. */ "How to use your servers" = "Comment utiliser vos serveurs"; +/* No comment provided by engineer. */ +"Hungarian interface" = "Interface en hongrois"; + /* No comment provided by engineer. */ "ICE servers (one per line)" = "Serveurs ICE (un par ligne)"; /* No comment provided by engineer. */ "If you can't meet in person, show QR code in a video call, or share the link." = "Si vous ne pouvez pas vous rencontrer en personne, montrez le code QR lors d'un appel vidéo ou partagez le lien."; -/* No comment provided by engineer. */ -"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Si vous ne pouvez pas voir la personne, vous pouvez **scanner le code QR dans un appel vidéo**, ou votre contact peut vous partager un lien d'invitation."; - /* No comment provided by engineer. */ "If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Si vous saisissez ce code à l'ouverture de l'application, toutes les données de l'application seront irréversiblement supprimées !"; @@ -1915,7 +2707,7 @@ "Immediately" = "Immédiatement"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Protégé du spam et des abus"; +"Immune to spam" = "Protégé du spam et des abus"; /* No comment provided by engineer. */ "Import" = "Importer"; @@ -1926,15 +2718,39 @@ /* No comment provided by engineer. */ "Import database" = "Importer la base de données"; +/* No comment provided by engineer. */ +"Import failed" = "Échec de l'importation"; + +/* No comment provided by engineer. */ +"Import theme" = "Importer un thème"; + +/* No comment provided by engineer. */ +"Importing archive" = "Importation de l'archive"; + +/* No comment provided by engineer. */ +"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "Amélioration de la distribution, réduction de l'utilisation du trafic.\nD'autres améliorations sont à venir !"; + +/* No comment provided by engineer. */ +"Improved message delivery" = "Amélioration de la transmission des messages"; + /* No comment provided by engineer. */ "Improved privacy and security" = "Une meilleure sécurité et protection de la vie privée"; /* No comment provided by engineer. */ "Improved server configuration" = "Configuration de serveur améliorée"; +/* No comment provided by engineer. */ +"In order to continue, chat should be stopped." = "Pour continuer, le chat doit être interrompu."; + /* No comment provided by engineer. */ "In reply to" = "En réponse à"; +/* No comment provided by engineer. */ +"In-call sounds" = "Sons d'appel"; + +/* No comment provided by engineer. */ +"inactive" = "inactif"; + /* No comment provided by engineer. */ "Incognito" = "Incognito"; @@ -1989,14 +2805,17 @@ /* 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)"; +/* No comment provided by engineer. */ +"Instant" = "Instantané"; + /* No comment provided by engineer. */ "Instant push notifications will be hidden!\n" = "Les notifications push instantanées vont être cachées !\n"; /* No comment provided by engineer. */ -"Instantly" = "Instantané"; +"Interface" = "Interface"; /* No comment provided by engineer. */ -"Interface" = "Interface"; +"Interface colors" = "Couleurs d'interface"; /* invalid chat data */ "invalid chat" = "chat invalide"; @@ -2010,10 +2829,25 @@ /* invalid chat item */ "invalid data" = "données invalides"; +/* No comment provided by engineer. */ +"Invalid display name!" = "Nom d'affichage invalide !"; + +/* No comment provided by engineer. */ +"Invalid link" = "Lien invalide"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Confirmation de migration invalide"; + /* No comment provided by engineer. */ "Invalid name!" = "Nom invalide !"; /* No comment provided by engineer. */ +"Invalid QR code" = "Code QR invalide"; + +/* No comment provided by engineer. */ +"Invalid response" = "Réponse invalide"; + +/* alert title */ "Invalid server address!" = "Adresse de serveur invalide !"; /* item status text */ @@ -2025,12 +2859,18 @@ /* group name */ "invitation to group %@" = "invitation au groupe %@"; +/* No comment provided by engineer. */ +"invite" = "inviter"; + /* No comment provided by engineer. */ "Invite friends" = "Inviter des amis"; /* No comment provided by engineer. */ "Invite members" = "Inviter des membres"; +/* No comment provided by engineer. */ +"Invite to chat" = "Inviter à discuter"; + /* No comment provided by engineer. */ "Invite to group" = "Inviter au groupe"; @@ -2052,6 +2892,9 @@ /* 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." = "La keychain d'iOS sera utilisée pour stocker en toute sécurité la phrase secrète après le redémarrage de l'app ou la modification de la phrase secrète - il permettra de recevoir les notifications push."; +/* No comment provided by engineer. */ +"IP address" = "Adresse IP"; + /* No comment provided by engineer. */ "Irreversible message deletion" = "Suppression irréversible des messages"; @@ -2059,7 +2902,7 @@ "Irreversible message deletion is prohibited in this chat." = "La suppression irréversible de message est interdite dans ce chat."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "La suppression irréversible de messages est interdite dans ce groupe."; +"Irreversible message deletion is prohibited." = "La suppression irréversible de messages est interdite dans ce groupe."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Cela permet d'avoir plusieurs connections anonymes sans aucune données partagées entre elles sur un même profil."; @@ -2070,6 +2913,9 @@ /* No comment provided by engineer. */ "It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Cela peut arriver quand :\n1. Les messages ont expiré dans le client expéditeur après 2 jours ou sur le serveur après 30 jours.\n2. Le déchiffrement du message a échoué, car vous ou votre contact avez utilisé une ancienne sauvegarde de base de données.\n3. La connexion a été compromise."; +/* No comment provided by engineer. */ +"It protects your IP address and connections." = "Il protège votre adresse IP et vos connexions."; + /* 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 (%@)." = "Il semblerait que vous êtes déjà connecté via ce lien. Si ce n'est pas le cas, il y a eu une erreur (%@)."; @@ -2082,7 +2928,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Interface en japonais"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Rejoindre"; /* No comment provided by engineer. */ @@ -2091,6 +2937,9 @@ /* No comment provided by engineer. */ "Join group" = "Rejoindre le groupe"; +/* No comment provided by engineer. */ +"Join group conversations" = "Participez aux conversations de groupe"; + /* No comment provided by engineer. */ "Join group?" = "Rejoindre le groupe ?"; @@ -2106,9 +2955,18 @@ /* No comment provided by engineer. */ "Joining group" = "Entrain de rejoindre le groupe"; +/* alert action */ +"Keep" = "Conserver"; + +/* No comment provided by engineer. */ +"Keep conversation" = "Garder la conversation"; + /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Garder l'application ouverte pour l'utiliser depuis le bureau"; +/* alert title */ +"Keep unused invitation?" = "Conserver l'invitation inutilisée ?"; + /* No comment provided by engineer. */ "Keep your connections" = "Conserver vos connexions"; @@ -2124,9 +2982,15 @@ /* No comment provided by engineer. */ "Learn more" = "En savoir plus"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Quitter"; +/* No comment provided by engineer. */ +"Leave chat" = "Quitter la discussion"; + +/* No comment provided by engineer. */ +"Leave chat?" = "Quitter la discussion ?"; + /* No comment provided by engineer. */ "Leave group" = "Quitter le groupe"; @@ -2175,24 +3039,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Mode de verrouillage"; -/* No comment provided by engineer. */ -"Make a private connection" = "Établir une connexion privée"; - /* No comment provided by engineer. */ "Make one message disappear" = "Rendre un message éphémère"; /* No comment provided by engineer. */ "Make profile private!" = "Rendre un profil privé !"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Assurez-vous que les adresses des serveurs %@ sont au bon format et ne sont pas dupliquées, un par ligne (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Assurez-vous que les adresses des serveurs WebRTC ICE sont au bon format et ne sont pas dupliquées, un par ligne."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Beaucoup se demandent : *si SimpleX n'a pas d'identifiant d'utilisateur, comment peut-il délivrer des messages ?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Marquer comme supprimé pour tout le monde"; @@ -2211,33 +3066,90 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "Max 30 secondes, réception immédiate."; +/* No comment provided by engineer. */ +"Media & file servers" = "Serveurs de fichiers et de médias"; + +/* blur media */ +"Medium" = "Modéré"; + /* member role */ "member" = "membre"; /* No comment provided by engineer. */ "Member" = "Membre"; +/* profile update event chat item */ +"member %@ changed to %@" = "le membre %1$@ est devenu %2$@"; + /* rcv group event chat item */ "member connected" = "est connecté·e"; +/* item status text */ +"Member inactive" = "Membre inactif"; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All chat members will be notified." = "Le rôle du membre sera modifié pour « %@ ». Tous les membres du chat seront notifiés."; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Le rôle du membre sera changé pour \"%@\". Tous les membres du groupe en seront informés."; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Le rôle du membre sera changé pour \"%@\". Ce membre recevra une nouvelle invitation."; +/* No comment provided by engineer. */ +"Member will be removed from chat - this cannot be undone!" = "Le membre sera retiré de la discussion - cela ne peut pas être annulé !"; + /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Ce membre sera retiré du groupe - impossible de revenir en arrière !"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Les membres du groupe peuvent ajouter des réactions aux messages."; + +/* 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)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Les membres du groupe peuvent envoyer des messages directs."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Les membres du groupes peuvent envoyer des messages éphémères."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Les membres du groupe peuvent envoyer des fichiers et des médias."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "Les membres du groupe peuvent envoyer des liens SimpleX."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Les membres du groupe peuvent envoyer des messages vocaux."; + +/* No comment provided by engineer. */ +"Menus" = "Menus"; + +/* No comment provided by engineer. */ +"message" = "message"; + /* item status text */ "Message delivery error" = "Erreur de distribution du message"; /* No comment provided by engineer. */ "Message delivery receipts!" = "Accusés de réception des messages !"; +/* item status text */ +"Message delivery warning" = "Avertissement sur la distribution des messages"; + /* No comment provided by engineer. */ "Message draft" = "Brouillon de message"; +/* item status text */ +"Message forwarded" = "Message transféré"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Le message peut être transmis plus tard si le membre devient actif."; + +/* No comment provided by engineer. */ +"Message queue info" = "Informations sur la file d'attente des messages"; + /* chat feature */ "Message reactions" = "Réactions aux messages"; @@ -2245,14 +3157,35 @@ "Message reactions are prohibited in this chat." = "Les réactions aux messages sont interdites dans ce chat."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Les réactions aux messages sont interdites dans ce groupe."; +"Message reactions are prohibited." = "Les réactions aux messages sont interdites dans ce groupe."; /* notification */ "message received" = "message reçu"; +/* No comment provided by engineer. */ +"Message reception" = "Réception de message"; + +/* No comment provided by engineer. */ +"Message servers" = "Serveurs de messages"; + +/* No comment provided by engineer. */ +"Message shape" = "Forme du message"; + +/* No comment provided by engineer. */ +"Message source remains private." = "La source du message reste privée."; + +/* No comment provided by engineer. */ +"Message status" = "Statut du message"; + +/* copied message info */ +"Message status: %@" = "Statut du message : %@"; + /* No comment provided by engineer. */ "Message text" = "Texte du message"; +/* No comment provided by engineer. */ +"Message too large" = "Message trop volumineux"; + /* No comment provided by engineer. */ "Messages" = "Messages"; @@ -2262,9 +3195,45 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Les messages de %@ seront affichés !"; +/* No comment provided by engineer. */ +"Messages received" = "Messages reçus"; + +/* No comment provided by engineer. */ +"Messages sent" = "Messages envoyés"; + +/* alert message */ +"Messages were deleted after you selected them." = "Les messages ont été supprimés après avoir été sélectionnés."; + +/* 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." = "Les messages, fichiers et appels sont protégés par un chiffrement **de bout en bout** avec une confidentialité persistante, une répudiation et une récupération en cas d'effraction."; + +/* 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." = "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 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"; + +/* No comment provided by engineer. */ +"Migrate to another device" = "Transférer vers un autre appareil"; + +/* No comment provided by engineer. */ +"Migrate to another device via QR code." = "Transférer vers un autre appareil via un code QR."; + +/* No comment provided by engineer. */ +"Migrating" = "Transfert"; + /* No comment provided by engineer. */ "Migrating database archive…" = "Migration de l'archive de la base de données…"; +/* No comment provided by engineer. */ +"Migration complete" = "Transfert terminé"; + /* No comment provided by engineer. */ "Migration error:" = "Erreur de migration :"; @@ -2275,7 +3244,7 @@ "Migration is completed" = "La migration est terminée"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migrations : %@"; +"Migrations:" = "Migrations :"; /* time unit */ "minutes" = "minutes"; @@ -2295,7 +3264,7 @@ /* copied message info */ "Moderated at: %@" = "Modéré à : %@"; -/* No comment provided by engineer. */ +/* marked deleted chat item preview text */ "moderated by %@" = "modéré par %@"; /* time unit */ @@ -2304,54 +3273,81 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Plus d'améliorations à venir !"; +/* No comment provided by engineer. */ +"More reliable network connection." = "Connexion réseau plus fiable."; + +/* No comment provided by engineer. */ +"More reliable notifications" = "Notifications plus fiables"; + /* item status description */ "Most likely this connection is deleted." = "Connexion probablement supprimée."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Il est fort probable que ce contact ait supprimé la connexion avec vous."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Différents profils de chat"; -/* No comment provided by engineer. */ +/* notification label action */ "Mute" = "Muet"; /* No comment provided by engineer. */ "Muted when inactive!" = "Mute en cas d'inactivité !"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Nom"; /* No comment provided by engineer. */ "Network & servers" = "Réseau et serveurs"; +/* No comment provided by engineer. */ +"Network connection" = "Connexion au réseau"; + +/* No comment provided by engineer. */ +"Network decentralization" = "Décentralisation du réseau"; + +/* snd error text */ +"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."; + +/* No comment provided by engineer. */ +"Network management" = "Gestion du réseau"; + +/* No comment provided by engineer. */ +"Network operator" = "Opérateur de réseau"; + /* No comment provided by engineer. */ "Network settings" = "Paramètres réseau"; /* No comment provided by engineer. */ "Network status" = "État du réseau"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "jamais"; +/* No comment provided by engineer. */ +"New chat" = "Nouveau chat"; + +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Nouvelle expérience de discussion 🎉"; + /* notification */ "New contact request" = "Nouvelle demande de contact"; /* notification */ "New contact:" = "Nouveau contact :"; -/* No comment provided by engineer. */ -"New database archive" = "Nouvelle archive de base de données"; - /* No comment provided by engineer. */ "New desktop app!" = "Nouvelle application de bureau !"; /* No comment provided by engineer. */ "New display name" = "Nouveau nom d'affichage"; +/* notification */ +"New events" = "Nouveaux événements"; + /* No comment provided by engineer. */ "New in %@" = "Nouveautés de la %@"; +/* No comment provided by engineer. */ +"New media options" = "Nouvelles options de médias"; + /* No comment provided by engineer. */ "New member role" = "Nouveau rôle"; @@ -2367,6 +3363,15 @@ /* No comment provided by engineer. */ "New passphrase…" = "Nouvelle phrase secrète…"; +/* No comment provided by engineer. */ +"New server" = "Nouveau serveur"; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used every time you start the app." = "De nouveaux identifiants SOCKS seront utilisés chaque fois que vous démarrerez l'application."; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used for each server." = "De nouveaux identifiants SOCKS seront utilisées pour chaque serveur."; + /* pref value */ "no" = "non"; @@ -2388,11 +3393,14 @@ /* No comment provided by engineer. */ "No device token!" = "Pas de token d'appareil !"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Pas de connexion directe pour l'instant, le message est transmis par l'administrateur."; + /* No comment provided by engineer. */ "no e2e encryption" = "sans chiffrement de bout en bout"; /* No comment provided by engineer. */ -"No filtered chats" = "Pas de chats filtrés"; +"No filtered chats" = "Aucune discussion filtrés"; /* No comment provided by engineer. */ "No group!" = "Groupe introuvable !"; @@ -2400,24 +3408,69 @@ /* No comment provided by engineer. */ "No history" = "Aucun historique"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Pas d'info, essayez de recharger"; + +/* servers error */ +"No media & file servers." = "Pas de serveurs de médias et de fichiers."; + +/* servers error */ +"No message servers." = "Pas de serveurs de messages."; + +/* No comment provided by engineer. */ +"No network connection" = "Pas de connexion au réseau"; + +/* No comment provided by engineer. */ +"No permission to record speech" = "Enregistrement des conversations non autorisé"; + +/* No comment provided by engineer. */ +"No permission to record video" = "Enregistrement de la vidéo non autorisé"; + /* No comment provided by engineer. */ "No permission to record voice message" = "Pas l'autorisation d'enregistrer un message vocal"; +/* No comment provided by engineer. */ +"No push server" = "No push server"; + /* No comment provided by engineer. */ "No received or sent files" = "Aucun fichier reçu ou envoyé"; +/* servers error */ +"No servers for private message routing." = "Pas de serveurs pour le routage privé des messages."; + +/* servers error */ +"No servers to receive files." = "Pas de serveurs pour recevoir des fichiers."; + +/* servers error */ +"No servers to receive messages." = "Pas de serveurs pour recevoir des messages."; + +/* servers error */ +"No servers to send files." = "Pas de serveurs pour envoyer des fichiers."; + /* 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 !"; +/* No comment provided by engineer. */ +"Nothing selected" = "Aucune sélection"; + +/* alert title */ +"Nothing to forward!" = "Rien à transférer !"; + /* No comment provided by engineer. */ "Notifications" = "Notifications"; /* No comment provided by engineer. */ "Notifications are disabled!" = "Les notifications sont désactivées !"; +/* No comment provided by engineer. */ +"Notifications privacy" = "Notifications sécurisées"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Désormais, les administrateurs peuvent :\n- supprimer les messages des membres.\n- désactiver des membres (rôle \"observateur\")"; @@ -2425,30 +3478,27 @@ "observer" = "observateur"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "off"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Off"; -/* No comment provided by engineer. */ -"Off (Local)" = "Off (Local)"; +/* feature offered item */ +"offered %@" = "propose %@"; /* feature offered item */ -"offered %@" = "offert %@"; +"offered %@: %@" = "propose %1$@ : %2$@"; -/* feature offered item */ -"offered %@: %@" = "offert %1$@ : %2$@"; - -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Ok"; /* No comment provided by engineer. */ -"Old database" = "Ancienne base de données"; +"OK" = "OK"; /* No comment provided by engineer. */ -"Old database archive" = "Archives de l'ancienne base de données"; +"Old database" = "Ancienne base de données"; /* group pref value */ "on" = "on"; @@ -2457,16 +3507,22 @@ "One-time invitation link" = "Lien d'invitation unique"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Les hôtes .onion seront nécessaires pour la connexion. Nécessite l'activation d'un VPN."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Les hôtes .onion seront **nécessaires** pour la connexion.\nNécessite l'activation d'un VPN."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Les hôtes .onion seront utilisés dès que possible. Nécessite l'activation d'un VPN."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Les hôtes .onion seront utilisés dès que possible.\nNécessite l'activation d'un VPN."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Les hôtes .onion ne seront pas utilisés."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Seuls les appareils clients stockent les profils des utilisateurs, les contacts, les groupes et les messages envoyés avec un **chiffrement de bout en bout à deux couches**."; +"Only chat owners can change preferences." = "Seuls les propriétaires peuvent modifier les préférences."; + +/* No comment provided by engineer. */ +"Only client devices store user profiles, contacts, groups, and messages." = "Seuls les appareils clients stockent les profils des utilisateurs, les contacts, les groupes et les messages envoyés avec un **chiffrement de bout en bout à deux couches**."; + +/* No comment provided by engineer. */ +"Only delete conversation" = "Ne supprimer que la conversation"; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Seuls les propriétaires du groupe peuvent modifier les préférences du groupe."; @@ -2481,7 +3537,7 @@ "Only you can add message reactions." = "Vous seul pouvez ajouter des réactions aux messages."; /* No comment provided by engineer. */ -"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "Vous êtes le seul à pouvoir supprimer des messages de manière irréversible (votre contact peut les marquer comme supprimé)."; +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Vous êtes le seul à pouvoir supprimer des messages de manière irréversible (votre contact peut les marquer comme supprimé). (24 heures)"; /* No comment provided by engineer. */ "Only you can make calls." = "Vous seul pouvez passer des appels."; @@ -2496,7 +3552,7 @@ "Only your contact can add message reactions." = "Seul votre contact peut ajouter des réactions aux messages."; /* No comment provided by engineer. */ -"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Seul votre contact peut supprimer de manière irréversible des messages (vous pouvez les marquer comme supprimé)."; +"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Seul votre contact peut supprimer de manière irréversible des messages (vous pouvez les marquer comme supprimé). (24 heures)"; /* No comment provided by engineer. */ "Only your contact can make calls." = "Seul votre contact peut passer des appels."; @@ -2507,33 +3563,75 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Seul votre contact peut envoyer des messages vocaux."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Ouvrir"; +/* No comment provided by engineer. */ +"Open changes" = "Ouvrir les modifications"; + /* No comment provided by engineer. */ "Open chat" = "Ouvrir le chat"; /* authentication reason */ "Open chat console" = "Ouvrir la console du chat"; +/* No comment provided by engineer. */ +"Open conditions" = "Ouvrir les conditions"; + /* No comment provided by engineer. */ "Open group" = "Ouvrir le groupe"; +/* authentication reason */ +"Open migration to another device" = "Ouvrir le transfert vers un autre appareil"; + /* No comment provided by engineer. */ "Open Settings" = "Ouvrir les Paramètres"; -/* authentication reason */ -"Open user profiles" = "Ouvrir les profils d'utilisateurs"; +/* No comment provided by engineer. */ +"Opening app…" = "Ouverture de l'app…"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Protocole et code open-source – n'importe qui peut heberger un serveur."; +"Operator" = "Opérateur"; + +/* alert title */ +"Operator server" = "Serveur de l'opérateur"; /* No comment provided by engineer. */ -"Opening database…" = "Ouverture de la base de données…"; +"Or import archive file" = "Ou importer un fichier d'archive"; + +/* No comment provided by engineer. */ +"Or paste archive link" = "Ou coller le lien de l'archive"; + +/* No comment provided by engineer. */ +"Or scan QR code" = "Ou scanner le code QR"; + +/* No comment provided by engineer. */ +"Or securely share this file link" = "Ou partagez en toute sécurité le lien de ce fichier"; + +/* No comment provided by engineer. */ +"Or show this code" = "Ou montrez ce code"; + +/* No comment provided by engineer. */ +"Or to share privately" = "Ou à partager en privé"; + +/* No comment provided by engineer. */ +"other" = "autre"; + +/* No comment provided by engineer. */ +"Other" = "Autres"; + +/* No comment provided by engineer. */ +"other errors" = "autres erreurs"; + +/* alert message */ +"Other file errors:\n%@" = "Autres erreurs de fichiers :\n%@"; /* member role */ "owner" = "propriétaire"; +/* feature role */ +"owners" = "propriétaires"; + /* No comment provided by engineer. */ "Passcode" = "Code d'accès"; @@ -2550,10 +3648,13 @@ "Passcode set!" = "Code d'accès défini !"; /* No comment provided by engineer. */ -"Password to show" = "Mot de passe à entrer"; +"Password" = "Mot de passe"; /* No comment provided by engineer. */ -"Paste" = "Coller"; +"Password to show" = "Mot de passe à entrer"; + +/* past/unknown group member */ +"Past member %@" = "Ancien membre %@"; /* No comment provided by engineer. */ "Paste desktop address" = "Coller l'adresse du bureau"; @@ -2562,32 +3663,44 @@ "Paste image" = "Coller l'image"; /* No comment provided by engineer. */ -"Paste received link" = "Coller le lien reçu"; +"Paste link to connect!" = "Collez le lien pour vous connecter !"; -/* placeholder */ -"Paste the link you received to connect with your contact." = "Collez le lien que vous avez reçu dans le cadre ci-dessous pour vous connecter avec votre contact."; +/* No comment provided by engineer. */ +"Paste the link you received" = "Collez le lien que vous avez reçu"; /* No comment provided by engineer. */ "peer-to-peer" = "pair-à-pair"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "On ne peut se connecter à vous qu’avec les liens que vous partagez."; +"Pending" = "En attente"; /* No comment provided by engineer. */ -"Periodically" = "Périodique"; +"Periodic" = "Périodique"; /* message decrypt error item */ "Permanent decryption error" = "Erreur de déchiffrement"; +/* No comment provided by engineer. */ +"Picture-in-picture calls" = "Appels picture-in-picture"; + /* No comment provided by engineer. */ "PING count" = "Nombre de PING"; /* No comment provided by engineer. */ "PING interval" = "Intervalle de PING"; +/* No comment provided by engineer. */ +"Play from the chat list." = "Aperçu depuis la liste de conversation."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Veuillez demander à votre contact d'autoriser les appels."; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Veuillez demander à votre contact de permettre l'envoi de messages vocaux."; +/* No comment provided by engineer. */ +"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." = "Veuillez vérifier que le téléphone portable et l'ordinateur de bureau sont connectés au même réseau local et que le pare-feu de l'ordinateur de bureau autorise la connexion.\nVeuillez faire part de tout autre problème aux développeurs."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Veuillez vérifier que vous avez utilisé le bon lien ou demandez à votre contact de vous en envoyer un autre."; @@ -2597,6 +3710,12 @@ /* No comment provided by engineer. */ "Please check yours and your contact preferences." = "Veuillez vérifier vos préférences ainsi que celles de votre contact."; +/* No comment provided by engineer. */ +"Please confirm that network settings are correct for this device." = "Veuillez confirmer que les paramètres réseau de cet appareil sont corrects."; + +/* No comment provided by engineer. */ +"Please contact developers.\nError: %@" = "Veuillez contacter les développeurs.\nErreur : %@"; + /* No comment provided by engineer. */ "Please contact group admin." = "Veuillez contacter l'administrateur du groupe."; @@ -2624,6 +3743,9 @@ /* No comment provided by engineer. */ "Polish interface" = "Interface en polonais"; +/* No comment provided by engineer. */ +"Port" = "Port"; + /* server test error */ "Possibly, certificate fingerprint in server address is incorrect" = "Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte"; @@ -2631,23 +3753,44 @@ "Preserve the last message draft, with attachments." = "Conserver le brouillon du dernier message, avec les pièces jointes."; /* No comment provided by engineer. */ -"Preset server" = "Serveur prédéfini"; +"Preset server address" = "Adresse du serveur prédéfinie"; /* No comment provided by engineer. */ -"Preset server address" = "Adresse du serveur prédéfinie"; +"Preset servers" = "Serveurs prédéfinis"; /* No comment provided by engineer. */ "Preview" = "Aperçu"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Serveurs précédemment connectés"; + /* No comment provided by engineer. */ "Privacy & security" = "Vie privée et sécurité"; +/* 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"; +/* No comment provided by engineer. */ +"Private message routing" = "Routage privé des messages"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Routage privé des messages 🚀"; + +/* name of notes to self */ +"Private notes" = "Notes privées"; + +/* No comment provided by engineer. */ +"Private routing" = "Routage privé"; + +/* No comment provided by engineer. */ +"Private routing error" = "Erreur de routage privé"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profil et connexions au serveur"; @@ -2655,15 +3798,15 @@ "Profile image" = "Image de profil"; /* No comment provided by engineer. */ -"Profile name" = "Nom du profil"; - -/* No comment provided by engineer. */ -"Profile name:" = "Nom du profil :"; +"Profile images" = "Images de profil"; /* No comment provided by engineer. */ "Profile password" = "Mot de passe de profil"; /* 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. */ @@ -2687,38 +3830,71 @@ /* No comment provided by engineer. */ "Prohibit sending files and media." = "Interdire l'envoi de fichiers et de médias."; +/* No comment provided by engineer. */ +"Prohibit sending SimpleX links." = "Interdire l'envoi de liens SimpleX."; + /* No comment provided by engineer. */ "Prohibit sending voice messages." = "Interdire l'envoi de messages vocaux."; /* No comment provided by engineer. */ "Protect app screen" = "Protéger l'écran de l'app"; +/* No comment provided by engineer. */ +"Protect IP address" = "Protéger l'adresse IP"; + /* No comment provided by engineer. */ "Protect your chat profiles with a password!" = "Protégez vos profils de chat par un mot de passe !"; +/* No comment provided by engineer. */ +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Protégez votre adresse IP des relais de messagerie choisis par vos contacts.\nActivez-le dans les paramètres *Réseau et serveurs*."; + /* No comment provided by engineer. */ "Protocol timeout" = "Délai du protocole"; /* No comment provided by engineer. */ "Protocol timeout per KB" = "Délai d'attente du protocole par KB"; +/* No comment provided by engineer. */ +"Proxied" = "Routé via un proxy"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Serveurs routés via des proxy"; + +/* No comment provided by engineer. */ +"Proxy requires password" = "Le proxy est protégé par un mot de passe"; + /* No comment provided by engineer. */ "Push notifications" = "Notifications push"; +/* No comment provided by engineer. */ +"Push server" = "Serveur Push"; + +/* chat item text */ +"quantum resistant e2e encryption" = "chiffrement e2e résistant post-quantique"; + +/* No comment provided by engineer. */ +"Quantum resistant encryption" = "Chiffrement résistant post-quantique"; + /* No comment provided by engineer. */ "Rate the app" = "Évaluer l'app"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Barre d'outils accessible"; + /* chat item menu */ "React…" = "Réagissez…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Lire"; /* No comment provided by engineer. */ "Read more" = "En savoir plus"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Pour en savoir plus, consultez le [Guide de l'utilisateur](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"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)."; + +/* 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)."; @@ -2727,10 +3903,10 @@ "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)."; /* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Plus d'informations sur notre GitHub."; +"Receipts are disabled" = "Les accusés de réception sont désactivés"; /* No comment provided by engineer. */ -"Receipts are disabled" = "Les accusés de réception sont désactivés"; +"Receive errors" = "Erreurs reçues"; /* No comment provided by engineer. */ "received answer…" = "réponse reçu…"; @@ -2750,6 +3926,15 @@ /* message info title */ "Received message" = "Message reçu"; +/* No comment provided by engineer. */ +"Received messages" = "Messages reçus"; + +/* No comment provided by engineer. */ +"Received reply" = "Réponse reçue"; + +/* No comment provided by engineer. */ +"Received total" = "Total reçu"; + /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "L'adresse de réception sera changée pour un autre serveur. Le changement d'adresse sera terminé lorsque l'expéditeur sera en ligne."; @@ -2759,14 +3944,35 @@ /* No comment provided by engineer. */ "Receiving via" = "Réception via"; +/* No comment provided by engineer. */ +"Recent history and improved [directory bot](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)." = "Historique récent et amélioration du [bot annuaire](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)."; + +/* No comment provided by engineer. */ +"Recipient(s) can't see who this message is from." = "Le(s) destinataire(s) ne peut(vent) pas voir de qui provient ce message."; + /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Les destinataires voient les mises à jour au fur et à mesure que vous leur écrivez."; +/* No comment provided by engineer. */ +"Reconnect" = "Reconnecter"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Reconnecter tous les serveurs connectés pour forcer la livraison des messages. Cette méthode utilise du trafic supplémentaire."; /* No comment provided by engineer. */ -"Reconnect servers?" = "Reconnecter les serveurs?"; +"Reconnect all servers" = "Reconnecter tous les serveurs"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Reconnecter tous les serveurs ?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Reconnecter le serveur pour forcer la livraison des messages. Utilise du trafic supplémentaire."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Reconnecter le serveur ?"; + +/* No comment provided by engineer. */ +"Reconnect servers?" = "Reconnecter les serveurs ?"; /* No comment provided by engineer. */ "Record updated at" = "Enregistrement mis à jour le"; @@ -2777,7 +3983,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Réduction de la consommation de batterie"; -/* reject incoming call via notification */ +/* reject incoming call via notification +swipe action */ "Reject" = "Rejeter"; /* No comment provided by engineer. */ @@ -2798,6 +4005,12 @@ /* No comment provided by engineer. */ "Remove" = "Supprimer"; +/* No comment provided by engineer. */ +"Remove archive?" = "Supprimer l'archive ?"; + +/* No comment provided by engineer. */ +"Remove image" = "Enlever l'image"; + /* No comment provided by engineer. */ "Remove member" = "Retirer le membre"; @@ -2813,6 +4026,12 @@ /* rcv group event chat item */ "removed %@" = "a retiré %@"; +/* profile update event chat item */ +"removed contact address" = "suppression de l'adresse de contact"; + +/* profile update event chat item */ +"removed profile picture" = "suppression de la photo de profil"; + /* rcv group event chat item */ "removed you" = "vous a retiré"; @@ -2823,29 +4042,56 @@ "Renegotiate encryption" = "Renégocier le chiffrement"; /* No comment provided by engineer. */ -"Renegotiate encryption?" = "Renégocier le chiffrement?"; +"Renegotiate encryption?" = "Renégocier le chiffrement ?"; /* No comment provided by engineer. */ "Repeat connection request?" = "Répéter la demande de connexion ?"; +/* No comment provided by engineer. */ +"Repeat download" = "Répéter le téléchargement"; + +/* No comment provided by engineer. */ +"Repeat import" = "Répéter l'importation"; + /* No comment provided by engineer. */ "Repeat join request?" = "Répéter la requête d'adhésion ?"; +/* No comment provided by engineer. */ +"Repeat upload" = "Répéter l'envoi"; + /* chat item action */ "Reply" = "Répondre"; +/* chat list item title */ +"requested to connect" = "demande à se connecter"; + /* No comment provided by engineer. */ "Required" = "Requis"; /* No comment provided by engineer. */ "Reset" = "Réinitialisation"; +/* No comment provided by engineer. */ +"Reset all hints" = "Rétablir tous les conseils"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Réinitialiser toutes les statistiques"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Réinitialiser toutes les statistiques ?"; + /* No comment provided by engineer. */ "Reset colors" = "Réinitialisation des couleurs"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Réinitialisation au thème de l'appli"; + /* No comment provided by engineer. */ "Reset to defaults" = "Réinitialisation des valeurs par défaut"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Réinitialisation au thème de l'utilisateur"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Redémarrez l'application pour créer un nouveau profil de chat"; @@ -2864,11 +4110,14 @@ /* No comment provided by engineer. */ "Restore database error" = "Erreur de restauration de la base de données"; +/* No comment provided by engineer. */ +"Retry" = "Réessayer"; + /* chat item action */ "Reveal" = "Révéler"; /* No comment provided by engineer. */ -"Revert" = "Revenir en arrière"; +"Review conditions" = "Vérifier les conditions"; /* No comment provided by engineer. */ "Revoke" = "Révoquer"; @@ -2885,57 +4134,85 @@ /* No comment provided by engineer. */ "Run chat" = "Exécuter le chat"; -/* chat item action */ -"Save" = "Sauvegarder"; +/* No comment provided by engineer. */ +"Safely receive files" = "Réception de fichiers en toute sécurité"; /* No comment provided by engineer. */ -"Save (and notify contacts)" = "Sauvegarder (et en informer les contacts)"; +"Safer groups" = "Groupes plus sûrs"; + +/* alert button +chat item action */ +"Save" = "Enregistrer"; + +/* alert button */ +"Save (and notify contacts)" = "Enregistrer (et en informer les contacts)"; + +/* alert button */ +"Save and notify contact" = "Enregistrer et en informer le contact"; /* No comment provided by engineer. */ -"Save and notify contact" = "Sauvegarder et en informer les contacts"; +"Save and notify group members" = "Enregistrer et en informer les membres du groupe"; /* No comment provided by engineer. */ -"Save and notify group members" = "Sauvegarder et en informer les membres du groupe"; +"Save and reconnect" = "Sauvegarder et se reconnecter"; /* No comment provided by engineer. */ -"Save and update group profile" = "Sauvegarder et mettre à jour le profil du groupe"; +"Save and update group profile" = "Enregistrer et mettre à jour le profil du groupe"; /* No comment provided by engineer. */ -"Save archive" = "Sauvegarder l'archive"; +"Save group profile" = "Enregistrer le profil du groupe"; /* No comment provided by engineer. */ -"Save auto-accept settings" = "Sauvegarder les paramètres d'acceptation automatique"; +"Save passphrase and open chat" = "Enregistrer la phrase secrète et ouvrir le chat"; /* No comment provided by engineer. */ -"Save group profile" = "Sauvegarder le profil du groupe"; +"Save passphrase in Keychain" = "Enregistrer la phrase secrète dans la Keychain"; -/* No comment provided by engineer. */ -"Save passphrase and open chat" = "Sauvegarder la phrase secrète et ouvrir le chat"; - -/* No comment provided by engineer. */ -"Save passphrase in Keychain" = "Sauvegarder la phrase secrète dans la keychain"; - -/* No comment provided by engineer. */ -"Save preferences?" = "Sauvegarder les préférences ?"; +/* alert title */ +"Save preferences?" = "Enregistrer les préférences ?"; /* No comment provided by engineer. */ "Save profile password" = "Enregistrer le mot de passe du profil"; /* No comment provided by engineer. */ -"Save servers" = "Sauvegarder les serveurs"; +"Save servers" = "Enregistrer les serveurs"; + +/* alert title */ +"Save servers?" = "Enregistrer les serveurs ?"; /* No comment provided by engineer. */ -"Save servers?" = "Sauvegarder les serveurs ?"; +"Save welcome message?" = "Enregistrer le message d'accueil ?"; + +/* alert title */ +"Save your profile?" = "Sauvegarder votre profil ?"; /* No comment provided by engineer. */ -"Save settings?" = "Sauvegarder les paramètres ?"; +"saved" = "enregistré"; /* No comment provided by engineer. */ -"Save welcome message?" = "Sauvegarder le message d'accueil ?"; +"Saved" = "Enregistré"; + +/* No comment provided by engineer. */ +"Saved from" = "Enregistré depuis"; + +/* No comment provided by engineer. */ +"saved from %@" = "enregistré à partir de %@"; + +/* message info title */ +"Saved message" = "Message enregistré"; /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Les serveurs WebRTC ICE sauvegardés seront supprimés"; +/* No comment provided by engineer. */ +"Saving %lld messages" = "Sauvegarde de %lld messages"; + +/* No comment provided by engineer. */ +"Scale" = "Échelle"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Scanner / Coller un lien"; + /* No comment provided by engineer. */ "Scan code" = "Scanner le code"; @@ -2943,7 +4220,7 @@ "Scan QR code" = "Scanner un code QR"; /* No comment provided by engineer. */ -"Scan QR code from desktop" = "Scanner le code QR du bureau"; +"Scan QR code from desktop" = "Scannez le code QR du bureau"; /* No comment provided by engineer. */ "Scan security code from your contact's app." = "Scannez le code de sécurité depuis l'application de votre contact."; @@ -2952,11 +4229,23 @@ "Scan server QR code" = "Scanner un code QR de serveur"; /* No comment provided by engineer. */ -"Search" = "Recherche"; +"search" = "rechercher"; + +/* No comment provided by engineer. */ +"Search" = "Rechercher"; + +/* No comment provided by engineer. */ +"Search bar accepts invitation links." = "La barre de recherche accepte les liens d'invitation."; + +/* No comment provided by engineer. */ +"Search or paste SimpleX link" = "Rechercher ou coller un lien SimpleX"; /* network option */ "sec" = "sec"; +/* No comment provided by engineer. */ +"Secondary" = "Secondaire"; + /* time unit */ "seconds" = "secondes"; @@ -2966,6 +4255,9 @@ /* server test step */ "Secure queue" = "File d'attente sécurisée"; +/* No comment provided by engineer. */ +"Secured" = "Sécurisées"; + /* No comment provided by engineer. */ "Security assessment" = "Évaluation de sécurité"; @@ -2975,9 +4267,18 @@ /* chat item text */ "security code changed" = "code de sécurité modifié"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Choisir"; +/* No comment provided by engineer. */ +"Select chat profile" = "Sélectionner un profil de discussion"; + +/* No comment provided by engineer. */ +"Selected %lld" = "%lld sélectionné(s)"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Les préférences de chat sélectionnées interdisent ce message."; + /* No comment provided by engineer. */ "Self-destruct" = "Autodestruction"; @@ -3002,9 +4303,6 @@ /* No comment provided by engineer. */ "send direct message" = "envoyer un message direct"; -/* No comment provided by engineer. */ -"Send direct message" = "Envoyer un message direct"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Envoyer un message direct pour vous connecter"; @@ -3012,27 +4310,39 @@ "Send disappearing message" = "Envoyer un message éphémère"; /* No comment provided by engineer. */ -"Send link previews" = "Envoi d'aperçus de liens"; +"Send errors" = "Erreurs d'envoi"; + +/* No comment provided by engineer. */ +"Send link previews" = "Aperçu des liens"; /* No comment provided by engineer. */ "Send live message" = "Envoyer un message dynamique"; /* No comment provided by engineer. */ -"Send notifications" = "Envoi de notifications"; +"Send message to enable calls." = "Envoyer un message pour activer les appels."; /* No comment provided by engineer. */ -"Send notifications:" = "Envoi de notifications :"; +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Envoyer les messages de manière directe lorsque l'adresse IP est protégée et que votre serveur ou le serveur de destination ne prend pas en charge le routage privé."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Envoyez les messages de manière directe lorsque votre serveur ou le serveur de destination ne prend pas en charge le routage privé."; + +/* No comment provided by engineer. */ +"Send notifications" = "Envoi de notifications"; /* No comment provided by engineer. */ "Send questions and ideas" = "Envoyez vos questions et idées"; /* No comment provided by engineer. */ -"Send receipts" = "Envoyer les justificatifs"; +"Send receipts" = "Envoi de justificatifs"; /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Envoyez-les depuis la phototèque ou des claviers personnalisés."; /* No comment provided by engineer. */ +"Send up to 100 last messages to new members." = "Envoi des 100 derniers messages aux nouveaux membres."; + +/* alert message */ "Sender cancelled file transfer." = "L'expéditeur a annulé le transfert de fichiers."; /* No comment provided by engineer. */ @@ -3068,27 +4378,84 @@ /* copied message info */ "Sent at: %@" = "Envoyé le : %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Envoyé directement"; + /* notification */ "Sent file event" = "Événement de fichier envoyé"; /* message info title */ "Sent message" = "Message envoyé"; +/* No comment provided by engineer. */ +"Sent messages" = "Messages envoyés"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Les messages envoyés seront supprimés après une durée déterminée."; +/* No comment provided by engineer. */ +"Sent reply" = "Réponse envoyée"; + +/* No comment provided by engineer. */ +"Sent total" = "Total envoyé"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Envoyé via le proxy"; + +/* No comment provided by engineer. */ +"Server" = "Serveur"; + +/* alert message */ +"Server added to operator %@." = "Serveur ajouté à l'opérateur %@."; + +/* No comment provided by engineer. */ +"Server address" = "Adresse du serveur"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "L'adresse du serveur est incompatible avec les paramètres réseau : %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "L'adresse du serveur est incompatible avec les paramètres du réseau."; + +/* alert title */ +"Server operator changed." = "L'opérateur du serveur a changé."; + +/* No comment provided by engineer. */ +"Server operators" = "Opérateurs de serveur"; + +/* alert title */ +"Server protocol changed." = "Le protocole du serveur a été modifié."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "info sur la file d'attente du serveur : %1$@\n\ndernier message reçu : %2$@"; + /* server 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"; /* server test error */ -"Server requires authorization to upload, check password" = "Le serveur requiert une autorisation pour uploader, vérifiez le mot de passe"; +"Server requires authorization to upload, check password" = "Le serveur requiert une autorisation pour téléverser, vérifiez le mot de passe"; /* No comment provided by engineer. */ "Server test failed!" = "Échec du test du serveur !"; +/* No comment provided by engineer. */ +"Server type" = "Type de serveur"; + +/* srv error text */ +"Server version is incompatible with network settings." = "La version du serveur est incompatible avec les paramètres du réseau."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "La version du serveur est incompatible avec votre appli : %@."; + /* No comment provided by engineer. */ "Servers" = "Serveurs"; +/* No comment provided by engineer. */ +"Servers info" = "Infos serveurs"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Les statistiques des serveurs seront réinitialisées - il n'est pas possible de revenir en arrière !"; + /* No comment provided by engineer. */ "Session code" = "Code de session"; @@ -3098,15 +4465,27 @@ /* No comment provided by engineer. */ "Set contact name…" = "Définir le nom du contact…"; +/* No comment provided by engineer. */ +"Set default theme" = "Définir le thème par défaut"; + /* No comment provided by engineer. */ "Set group preferences" = "Définir les préférences du groupe"; /* No comment provided by engineer. */ "Set it instead of system authentication." = "Il permet de remplacer l'authentification du système."; +/* profile update event chat item */ +"set new contact address" = "a changé d'adresse de contact"; + +/* profile update event chat item */ +"set new profile picture" = "a changé d'image de profil"; + /* No comment provided by engineer. */ "Set passcode" = "Définir le code d'accès"; +/* No comment provided by engineer. */ +"Set passphrase" = "Définir une phrase secrète"; + /* No comment provided by engineer. */ "Set passphrase to export" = "Définir la phrase secrète pour l'export"; @@ -3119,27 +4498,55 @@ /* No comment provided by engineer. */ "Settings" = "Paramètres"; -/* chat item action */ +/* alert message */ +"Settings were changed." = "Les paramètres ont été modifiés."; + +/* No comment provided by engineer. */ +"Shape profile images" = "Images de profil modelable"; + +/* alert action +chat item action */ "Share" = "Partager"; /* No comment provided by engineer. */ "Share 1-time link" = "Partager un lien unique"; +/* No comment provided by engineer. */ +"Share 1-time link with a friend" = "Partager un lien unique avec un ami"; + /* No comment provided by engineer. */ "Share address" = "Partager l'adresse"; /* 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."; + /* No comment provided by engineer. */ "Share link" = "Partager le lien"; /* No comment provided by engineer. */ -"Share one-time invitation link" = "Partager un lien d'invitation unique"; +"Share profile" = "Partager le profil"; + +/* No comment provided by engineer. */ +"Share SimpleX address on social media." = "Partagez votre adresse SimpleX sur les réseaux sociaux."; + +/* No comment provided by engineer. */ +"Share this 1-time invite link" = "Partagez ce lien d'invitation unique"; + +/* 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é."; + /* No comment provided by engineer. */ "Show calls in phone history" = "Afficher les appels dans l'historique du téléphone"; @@ -3147,20 +4554,41 @@ "Show developer options" = "Afficher les options pour les développeurs"; /* No comment provided by engineer. */ -"Show last messages" = "Voir les derniers messages"; +"Show last messages" = "Aperçu des derniers messages"; /* No comment provided by engineer. */ -"Show preview" = "Montrer l'aperçu"; +"Show message status" = "Afficher le statut du message"; + +/* No comment provided by engineer. */ +"Show percentage" = "Afficher le pourcentage"; + +/* No comment provided by engineer. */ +"Show preview" = "Aperçu affiché"; + +/* No comment provided by engineer. */ +"Show QR code" = "Afficher le code QR"; /* No comment provided by engineer. */ "Show:" = "Afficher :"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "Adresse SimpleX"; /* No comment provided by engineer. */ "SimpleX Address" = "Adresse SimpleX"; +/* No comment provided by engineer. */ +"SimpleX address and 1-time links are safe to share via any messenger." = "Les adresses SimpleX et les liens à usage unique peuvent être partagés en toute sécurité via n'importe quelle messagerie."; + +/* No comment provided by engineer. */ +"SimpleX address or 1-time link?" = "Adresse SimpleX ou lien unique ?"; + +/* No comment provided by engineer. */ +"SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "SimpleX Chat et Flux ont conclu un accord pour inclure les serveurs exploités par Flux dans l'application."; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "La sécurité de SimpleX Chat a été auditée par Trail of Bits."; @@ -3173,9 +4601,15 @@ /* simplex link type */ "SimpleX group link" = "Lien de groupe SimpleX"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "Liens SimpleX"; +/* No comment provided by engineer. */ +"SimpleX links are prohibited." = "Les liens SimpleX sont interdits dans ce groupe."; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "Les liens SimpleX ne sont pas autorisés"; + /* No comment provided by engineer. */ "SimpleX Lock" = "SimpleX Lock"; @@ -3191,9 +4625,15 @@ /* simplex link type */ "SimpleX one-time invitation" = "Invitation unique SimpleX"; +/* No comment provided by engineer. */ +"SimpleX protocols reviewed by Trail of Bits." = "Protocoles SimpleX audité par Trail of Bits."; + /* No comment provided by engineer. */ "Simplified incognito mode" = "Mode incognito simplifié"; +/* No comment provided by engineer. */ +"Size" = "Taille"; + /* No comment provided by engineer. */ "Skip" = "Passer"; @@ -3204,31 +4644,61 @@ "Small groups (max 20)" = "Petits groupes (max 20)"; /* No comment provided by engineer. */ -"SMP servers" = "Serveurs SMP"; +"SMP server" = "Serveur SMP"; + +/* No comment provided by engineer. */ +"SOCKS proxy" = "proxy SOCKS"; + +/* blur media */ +"Soft" = "Léger"; + +/* No comment provided by engineer. */ +"Some app settings were not migrated." = "Certains paramètres de l'application n'ont pas été migrés."; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "Certains fichiers n'ont pas été exportés :"; /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Des erreurs non fatales se sont produites lors de l'importation - vous pouvez consulter la console de chat pour plus de détails."; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "L'importation a entraîné des erreurs non fatales :"; + +/* alert message */ +"Some servers failed the test:\n%@" = "Certains serveurs ont échoué le test :\n%@"; + /* notification title */ "Somebody" = "Quelqu'un"; /* No comment provided by engineer. */ -"Start a new chat" = "Commencer une nouvelle conversation"; +"Square, circle, or anything in between." = "Carré, circulaire, ou toute autre forme intermédiaire."; + +/* chat item text */ +"standard end-to-end encryption" = "chiffrement de bout en bout standard"; /* No comment provided by engineer. */ "Start chat" = "Démarrer le chat"; +/* No comment provided by engineer. */ +"Start chat?" = "Lancer le chat ?"; + /* No comment provided by engineer. */ "Start migration" = "Démarrer la migration"; +/* No comment provided by engineer. */ +"Starting from %@." = "À partir de %@."; + /* No comment provided by engineer. */ "starting…" = "lancement…"; +/* No comment provided by engineer. */ +"Statistics" = "Statistiques"; + /* No comment provided by engineer. */ "Stop" = "Arrêter"; /* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Arrêter le chat pour permettre des actions sur la base de données"; +"Stop chat" = "Arrêter le chat"; /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Arrêtez le chat pour exporter, importer ou supprimer la base de données du chat. Vous ne pourrez pas recevoir et envoyer de messages pendant que le chat est arrêté."; @@ -3245,36 +4715,63 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Arrêter l'envoi du fichier ?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Cesser le partage"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Cesser le partage d'adresse ?"; /* authentication reason */ "Stop SimpleX" = "Arrêter SimpleX"; +/* No comment provided by engineer. */ +"Stopping chat" = "Arrêt du chat"; + /* No comment provided by engineer. */ "strike" = "barré"; +/* blur media */ +"Strong" = "Fort"; + /* No comment provided by engineer. */ "Submit" = "Soumettre"; +/* No comment provided by engineer. */ +"Subscribed" = "Inscriptions"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Erreurs d'inscription"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Inscriptions ignorées"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Supporter SimpleX Chat"; +/* No comment provided by engineer. */ +"Switch audio and video during the call." = "Passer de l'audio à la vidéo pendant l'appel."; + +/* No comment provided by engineer. */ +"Switch chat profile for 1-time invitations." = "Changer de profil de chat pour les invitations à usage unique."; + /* No comment provided by engineer. */ "System" = "Système"; /* No comment provided by engineer. */ "System authentication" = "Authentification du système"; +/* No comment provided by engineer. */ +"Tail" = "Queue"; + /* No comment provided by engineer. */ "Take picture" = "Prendre une photo"; /* 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."; @@ -3288,7 +4785,13 @@ "Tap to join incognito" = "Appuyez pour rejoindre incognito"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Appuyez ici pour démarrer une nouvelle discussion"; +"Tap to paste link" = "Appuyez pour coller le lien"; + +/* No comment provided by engineer. */ +"Tap to scan" = "Appuyez pour scanner"; + +/* No comment provided by engineer. */ +"TCP connection" = "Connexion TCP"; /* No comment provided by engineer. */ "TCP connection timeout" = "Délai de connexion TCP"; @@ -3302,6 +4805,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* file error alert title */ +"Temporary file error" = "Erreur de fichier temporaire"; + /* server test failure */ "Test failed at step %@." = "Échec du test à l'étape %@."; @@ -3311,7 +4817,7 @@ /* No comment provided by engineer. */ "Test servers" = "Tester les serveurs"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Échec des tests !"; /* No comment provided by engineer. */ @@ -3323,15 +4829,24 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Merci aux utilisateurs - contribuez via Weblate !"; -/* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "La 1ère plateforme sans aucun identifiant d'utilisateur – privée par design."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "L'application peut vous avertir lorsque vous recevez des messages ou des demandes de contact - veuillez ouvrir les paramètres pour les activer."; +/* No comment provided by engineer. */ +"The app protects your privacy by using different operators in each conversation." = "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 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)."; + /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "La tentative de modification de la phrase secrète de la base de données n'a pas abouti."; +/* No comment provided by engineer. */ +"The code you scanned is not a SimpleX link QR code." = "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, your contact may be offline." = "La connexion a atteint la limite des messages non délivrés, votre contact est peut-être hors ligne."; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "La connexion que vous avez acceptée sera annulée !"; @@ -3344,6 +4859,9 @@ /* 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."; @@ -3357,13 +4875,22 @@ "The message will be marked as moderated for all members." = "Le message sera marqué comme modéré pour tous les membres."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "La nouvelle génération de messagerie privée"; +"The messages will be deleted for all members." = "Les messages seront supprimés pour tous les membres."; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "Les messages seront marqués comme modérés pour tous les membres."; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "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 profile is only shared with your contacts." = "Le profil n'est partagé qu'avec vos contacts."; +"Your profile is stored on your device and only shared with your contacts." = "Le profil n'est partagé qu'avec vos contacts."; + +/* No comment provided by engineer. */ +"The same conditions will apply to operator **%@**." = "Les mêmes conditions s'appliquent à l'opérateur **%@**."; + +/* No comment provided by engineer. */ +"The second preset operator in the app!" = "Le deuxième opérateur prédéfini de l'application !"; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Le deuxième coche que nous avons manqué ! ✅"; @@ -3375,13 +4902,19 @@ "The servers for new connections of your current chat profile **%@**." = "Les serveurs pour les nouvelles connexions de votre profil de chat actuel **%@**."; /* No comment provided by engineer. */ -"Theme" = "Thème"; +"The servers for new files of your current chat profile **%@**." = "Les serveurs pour les nouveaux fichiers de votre profil de chat actuel **%@**."; /* No comment provided by engineer. */ -"There should be at least one user profile." = "Il doit y avoir au moins un profil d'utilisateur."; +"The text you pasted is not a SimpleX link." = "Le texte collé n'est pas un lien SimpleX."; /* No comment provided by engineer. */ -"There should be at least one visible user profile." = "Il doit y avoir au moins un profil d'utilisateur visible."; +"The uploaded database archive will be permanently removed from the servers." = "L'archive de la base de données envoyée sera définitivement supprimée des serveurs."; + +/* No comment provided by engineer. */ +"Themes" = "Thèmes"; + +/* No comment provided by engineer. */ +"These conditions will also apply for: **%@**." = "Ces conditions s'appliquent également aux : **%@**."; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Ces paramètres s'appliquent à votre profil actuel **%@**."; @@ -3398,11 +4931,20 @@ /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Cette action ne peut être annulée - votre profil, vos contacts, vos messages et vos fichiers seront irréversiblement perdus."; +/* E2EE info chat item */ +"This chat is protected by end-to-end encryption." = "Cette discussion est protégée par un chiffrement de bout en bout."; + +/* E2EE info chat item */ +"This chat is protected by quantum resistant end-to-end encryption." = "Cette discussion est protégée par un chiffrement de bout en bout résistant aux technologies quantiques."; + /* notification title */ "this contact" = "ce contact"; /* No comment provided by engineer. */ -"This device name" = "Ce nom d'appareil"; +"This device name" = "Nom de cet appareil"; + +/* No comment provided by engineer. */ +"This display name is invalid. Please choose another name." = "Ce nom d'affichage est invalide. Veuillez choisir un autre nom."; /* No comment provided by engineer. */ "This group has over %lld members, delivery receipts are not sent." = "Ce groupe compte plus de %lld membres, les accusés de réception ne sont pas envoyés."; @@ -3416,14 +4958,20 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Voici votre propre adresse SimpleX !"; +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Ce lien a été utilisé avec un autre appareil mobile, veuillez créer un nouveau lien sur le bureau."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Ce paramètre s'applique aux messages de votre profil de chat actuel **%@**."; +/* No comment provided by engineer. */ +"Title" = "Titre"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Si vous avez des questions et que vous souhaitez des réponses :"; /* No comment provided by engineer. */ -"To connect, your contact can scan QR code or use the link in the app." = "Pour se connecter, votre contact peut scanner le code QR ou utiliser le lien dans l'application."; +"To connect, your contact can scan QR code or use the link in the app." = "Pour se connecter, votre contact peut scanner un code QR ou utiliser un lien dans l'app."; /* No comment provided by engineer. */ "To hide unwanted messages." = "Pour cacher les messages indésirables."; @@ -3432,7 +4980,7 @@ "To make a new connection" = "Pour établir une nouvelle connexion"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Pour protéger votre vie privée, au lieu d’IDs utilisés par toutes les autres plateformes, SimpleX a des IDs pour les queues de messages, distinctes pour chacun de vos contacts."; +"To protect against your link being replaced, you can compare contact security codes." = "Pour vous protéger contre le remplacement de votre lien, vous pouvez comparer les codes de sécurité des contacts."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Pour préserver le fuseau horaire, les fichiers image/voix utilisent le système UTC."; @@ -3440,24 +4988,57 @@ /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Pour protéger vos informations, activez la fonction SimpleX Lock.\nVous serez invité à confirmer l'authentification avant que cette fonction ne soit activée."; +/* No comment provided by engineer. */ +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "Pour protéger votre adresse IP, le routage privé utilise vos serveurs SMP pour délivrer les messages."; + +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Pour protéger votre vie privée, au lieu d’IDs utilisés par toutes les autres plateformes, SimpleX a des IDs pour les queues de messages, distinctes pour chacun de vos contacts."; + +/* No comment provided by engineer. */ +"To receive" = "Pour recevoir"; + +/* No comment provided by engineer. */ +"To record speech please grant permission to use Microphone." = "Si vous souhaitez enregistrer une conversation, veuillez autoriser l'utilisation du microphone."; + +/* No comment provided by engineer. */ +"To record video please grant permission to use Camera." = "Si vous souhaitez enregistrer une vidéo, veuillez autoriser l'utilisation de la caméra."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Pour enregistrer un message vocal, veuillez accorder la permission d'utiliser le microphone."; /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Pour révéler votre profil caché, entrez le mot de passe dans le champ de recherche de la page **Vos profils de chat**."; +/* No comment provided by engineer. */ +"To send" = "Pour envoyer"; + /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Pour prendre en charge les notifications push instantanées, la base de données du chat doit être migrée."; +/* No comment provided by engineer. */ +"To use the servers of **%@**, accept conditions of use." = "Pour utiliser les serveurs de **%@**, acceptez les conditions d'utilisation."; + /* 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."; +/* No comment provided by engineer. */ +"Toolbar opacity" = "Opacité de la barre d'outils"; + +/* No comment provided by engineer. */ +"Total" = "Total"; + /* No comment provided by engineer. */ "Transport isolation" = "Transport isolé"; +/* No comment provided by engineer. */ +"Transport sessions" = "Sessions de transport"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact (erreur : %@)."; @@ -3465,10 +5046,10 @@ "Trying to connect to the server used to receive messages from this contact." = "Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact."; /* No comment provided by engineer. */ -"Turn off" = "Désactiver"; +"Turkish interface" = "Interface en turc"; /* No comment provided by engineer. */ -"Turn off notifications?" = "Désactiver les notifications ?"; +"Turn off" = "Désactiver"; /* No comment provided by engineer. */ "Turn on" = "Activer"; @@ -3479,19 +5060,28 @@ /* No comment provided by engineer. */ "Unblock" = "Débloquer"; +/* No comment provided by engineer. */ +"Unblock for all" = "Débloquer pour tous"; + /* No comment provided by engineer. */ "Unblock member" = "Débloquer ce membre"; +/* No comment provided by engineer. */ +"Unblock member for all?" = "Débloquer le membre pour tous ?"; + /* No comment provided by engineer. */ "Unblock member?" = "Débloquer ce membre ?"; -/* item status description */ -"Unexpected error: %@" = "Erreur inattendue : %@"; +/* rcv group event chat item */ +"unblocked %@" = "%@ débloqué"; + +/* No comment provided by engineer. */ +"Undelivered messages" = "Messages non distribués"; /* No comment provided by engineer. */ "Unexpected migration state" = "État de la migration inattendu"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Unfav."; /* No comment provided by engineer. */ @@ -3518,6 +5108,15 @@ /* No comment provided by engineer. */ "Unknown error" = "Erreur inconnue"; +/* No comment provided by engineer. */ +"unknown servers" = "relais inconnus"; + +/* alert title */ +"Unknown servers!" = "Serveurs inconnus !"; + +/* No comment provided by engineer. */ +"unknown status" = "statut inconnu"; + /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "À moins que vous utilisiez l'interface d'appel d'iOS, activez le mode \"Ne pas déranger\" pour éviter les interruptions."; @@ -3536,17 +5135,20 @@ /* authentication reason */ "Unlock app" = "Déverrouiller l'app"; -/* No comment provided by engineer. */ +/* notification label action */ "Unmute" = "Démute"; /* No comment provided by engineer. */ +"unprotected" = "non protégé"; + +/* swipe action */ "Unread" = "Non lu"; /* No comment provided by engineer. */ -"Update" = "Mise à jour"; +"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. */ -"Update .onion hosts setting?" = "Mettre à jour le paramètre des hôtes .onion ?"; +"Update" = "Mise à jour"; /* No comment provided by engineer. */ "Update database passphrase" = "Mise à jour de la phrase secrète de la base de données"; @@ -3555,37 +5157,61 @@ "Update network settings?" = "Mettre à jour les paramètres réseau ?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Mettre à jour le mode d'isolement du transport ?"; +"Update settings?" = "Mettre à jour les paramètres ?"; /* rcv group event chat item */ "updated group profile" = "mise à jour du profil de groupe"; +/* profile update event chat item */ +"updated profile" = "profil mis à jour"; + /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "La mise à jour des ces paramètres reconnectera le client à tous les serveurs."; -/* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "La mise à jour de ce paramètre reconnectera le client à tous les serveurs."; - /* No comment provided by engineer. */ "Upgrade and open chat" = "Mettre à niveau et ouvrir le chat"; +/* No comment provided by engineer. */ +"Upload errors" = "Erreurs de téléversement"; + +/* No comment provided by engineer. */ +"Upload failed" = "Échec de l'envoi"; + /* server test step */ -"Upload file" = "Transférer le fichier"; +"Upload file" = "Téléverser le fichier"; + +/* No comment provided by engineer. */ +"Uploaded" = "Téléversé"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Fichiers téléversés"; + +/* No comment provided by engineer. */ +"Uploading archive" = "Envoi de l'archive"; /* No comment provided by engineer. */ "Use .onion hosts" = "Utiliser les hôtes .onions"; +/* No comment provided by engineer. */ +"Use %@" = "Utiliser %@"; + /* No comment provided by engineer. */ "Use chat" = "Utiliser le chat"; /* No comment provided by engineer. */ "Use current profile" = "Utiliser le profil actuel"; +/* No comment provided by engineer. */ +"Use for files" = "Utiliser pour les fichiers"; + +/* No comment provided by engineer. */ +"Use for messages" = "Utiliser pour les messages"; + /* No comment provided by engineer. */ "Use for new connections" = "Utiliser pour les nouvelles connexions"; /* No comment provided by engineer. */ -"Use from desktop" = "Utilisation depuis le bureau"; +"Use from desktop" = "Accès au bureau"; /* No comment provided by engineer. */ "Use iOS call interface" = "Utiliser l'interface d'appel d'iOS"; @@ -3593,20 +5219,41 @@ /* No comment provided by engineer. */ "Use new incognito profile" = "Utiliser un nouveau profil incognito"; +/* No comment provided by engineer. */ +"Use only local notifications?" = "Utilisation de notifications locales uniquement ?"; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "Utiliser le routage privé avec des serveurs inconnus lorsque l'adresse IP n'est pas protégée."; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "Utiliser le routage privé avec des serveurs inconnus."; + /* No comment provided by engineer. */ "Use server" = "Utiliser ce serveur"; +/* No comment provided by engineer. */ +"Use servers" = "Utiliser les serveurs"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "Utiliser les serveurs SimpleX Chat ?"; /* No comment provided by engineer. */ -"User profile" = "Profil d'utilisateur"; +"Use SOCKS proxy" = "Utiliser un proxy SOCKS"; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "L'utilisation des hôtes .onion nécessite un fournisseur VPN compatible."; +"Use the app while in the call." = "Utiliser l'application pendant l'appel."; /* No comment provided by engineer. */ -"Using SimpleX Chat servers." = "Utilisation des serveurs SimpleX Chat."; +"Use the app with one hand." = "Utiliser l'application d'une main."; + +/* No comment provided by engineer. */ +"User selection" = "Sélection de l'utilisateur"; + +/* No comment provided by engineer. */ +"Username" = "Nom d'utilisateur"; + +/* No comment provided by engineer. */ +"Using SimpleX Chat servers." = "Vous utilisez les serveurs SimpleX."; /* No comment provided by engineer. */ "v%@" = "v%@"; @@ -3626,6 +5273,12 @@ /* No comment provided by engineer. */ "Verify connections" = "Vérifier les connexions"; +/* No comment provided by engineer. */ +"Verify database passphrase" = "Vérifier la phrase secrète de la base de données"; + +/* No comment provided by engineer. */ +"Verify passphrase" = "Vérifier la phrase secrète"; + /* No comment provided by engineer. */ "Verify security code" = "Vérifier le code de sécurité"; @@ -3647,6 +5300,9 @@ /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Via un protocole sécurisé de cryptographie post-quantique."; +/* No comment provided by engineer. */ +"video" = "vidéo"; + /* No comment provided by engineer. */ "Video call" = "Appel vidéo"; @@ -3654,7 +5310,7 @@ "video call (not e2e encrypted)" = "appel vidéo (sans chiffrement)"; /* No comment provided by engineer. */ -"Video will be received when your contact completes uploading it." = "La vidéo ne sera reçue que lorsque votre contact aura fini de la transférer."; +"Video will be received when your contact completes uploading it." = "La vidéo ne sera reçue que lorsque votre contact aura fini la mettre en ligne."; /* No comment provided by engineer. */ "Video will be received when your contact is online, please wait or check later!" = "La vidéo ne sera reçue que lorsque votre contact sera en ligne. Veuillez patienter ou vérifier plus tard !"; @@ -3662,9 +5318,18 @@ /* No comment provided by engineer. */ "Videos and files up to 1gb" = "Vidéos et fichiers jusqu'à 1Go"; +/* No comment provided by engineer. */ +"View conditions" = "Voir les conditions"; + /* No comment provided by engineer. */ "View security code" = "Afficher le code de sécurité"; +/* No comment provided by engineer. */ +"View updated conditions" = "Voir les conditions mises à jour"; + +/* chat feature */ +"Visible history" = "Historique visible"; + /* No comment provided by engineer. */ "Voice message…" = "Message vocal…"; @@ -3675,7 +5340,10 @@ "Voice messages are prohibited in this chat." = "Les messages vocaux sont interdits dans ce chat."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Les messages vocaux sont interdits dans ce groupe."; +"Voice messages are prohibited." = "Les messages vocaux sont interdits dans ce groupe."; + +/* No comment provided by engineer. */ +"Voice messages not allowed" = "Les messages vocaux ne sont pas autorisés"; /* No comment provided by engineer. */ "Voice messages prohibited!" = "Messages vocaux interdits !"; @@ -3698,9 +5366,18 @@ /* No comment provided by engineer. */ "Waiting for video" = "En attente de la vidéo"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Accentuation du papier-peint"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Fond d'écran"; + /* No comment provided by engineer. */ "wants to connect to you!" = "veut établir une connexion !"; +/* No comment provided by engineer. */ +"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "Attention : démarrer une session de chat sur plusieurs appareils n'est pas pris en charge et entraînera des dysfonctionnements au niveau de la transmission des messages"; + /* No comment provided by engineer. */ "Warning: you may lose some data!" = "Attention : vous risquez de perdre des données !"; @@ -3716,6 +5393,9 @@ /* No comment provided by engineer. */ "Welcome message" = "Message de bienvenue"; +/* No comment provided by engineer. */ +"Welcome message is too long" = "Le message de bienvenue est trop long"; + /* No comment provided by engineer. */ "What's new" = "Quoi de neuf ?"; @@ -3723,28 +5403,64 @@ "When available" = "Quand disponible"; /* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Lorsque des personnes demandent à se connecter, vous pouvez les accepter ou les refuser."; +"When connecting audio and video calls." = "Lors des appels audio et vidéo."; + +/* No comment provided by engineer. */ +"when IP hidden" = "lorsque l'IP est masquée"; + +/* No comment provided by engineer. */ +"When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "Lorsque plusieurs opérateurs sont activés, aucun d'entre eux ne dispose de métadonnées permettant de savoir qui communique avec qui."; /* 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." = "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. */ +"WiFi" = "WiFi"; + +/* No comment provided by engineer. */ +"Will be enabled in direct chats!" = "Activé dans les discussions directes !"; + +/* No comment provided by engineer. */ +"Wired ethernet" = "Ethernet câblé"; + +/* No comment provided by engineer. */ +"With encrypted files and media." = "Avec les fichiers et les médias chiffrés."; + /* No comment provided by engineer. */ "With optional welcome message." = "Avec message de bienvenue facultatif."; +/* No comment provided by engineer. */ +"With reduced battery usage." = "Consommation réduite de la batterie."; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "Sans Tor ou un VPN, votre adresse IP sera visible par les serveurs de fichiers."; + +/* alert message */ +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Sans Tor ni VPN, votre adresse IP sera visible par ces relais XFTP : %@."; + /* No comment provided by engineer. */ "Wrong database passphrase" = "Mauvaise phrase secrète pour la base de données"; +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "Clé erronée ou connexion non identifiée - il est très probable que cette connexion soit supprimée."; + +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Mauvaise clé ou adresse inconnue du bloc de données du fichier - le fichier est probablement supprimé."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Mauvaise phrase secrète !"; /* No comment provided by engineer. */ -"XFTP servers" = "Serveurs XFTP"; +"XFTP server" = "Serveur XFTP"; /* pref value */ "yes" = "oui"; /* No comment provided by engineer. */ -"You" = "Vous"; +"you" = "vous"; + +/* No comment provided by engineer. */ +"You **must not** use the same database on two devices." = "Vous **ne devez pas** utiliser la même base de données sur deux appareils."; /* No comment provided by engineer. */ "You accepted connection" = "Vous avez accepté la connexion"; @@ -3758,6 +5474,9 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "Vous êtes déjà connecté·e à %@ via ce lien."; +/* No comment provided by engineer. */ +"You are already connected with %@." = "Vous êtes déjà connecté avec %@."; + /* No comment provided by engineer. */ "You are already connecting to %@." = "Vous êtes déjà en train de vous connecter à %@."; @@ -3788,14 +5507,23 @@ /* No comment provided by engineer. */ "You are invited to group" = "Vous êtes invité·e au groupe"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Vous n'êtes pas connecté à ces serveurs. Le routage privé est utilisé pour leur délivrer des messages."; + /* No comment provided by engineer. */ "you are observer" = "vous êtes observateur"; +/* snd group event chat item */ +"you blocked %@" = "vous avez bloqué %@"; + /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Vous pouvez accepter des appels à partir de l'écran de verrouillage, sans authentification de l'appareil ou de l'application."; /* No comment provided by engineer. */ -"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "Vous pouvez également vous connecter en cliquant sur le lien. S'il s'ouvre dans le navigateur, cliquez sur le bouton **Open in mobile app**."; +"You can change it in Appearance settings." = "Vous pouvez choisir de le modifier dans les paramètres d'apparence."; + +/* No comment provided by engineer. */ +"You can configure servers via settings." = "Vous pouvez configurer les serveurs via les paramètres."; /* No comment provided by engineer. */ "You can create it later" = "Vous pouvez la créer plus tard"; @@ -3806,11 +5534,23 @@ /* No comment provided by engineer. */ "You can enable them later via app Privacy & Security settings." = "Vous pouvez les activer ultérieurement via les paramètres de Confidentialité et Sécurité de l'application."; +/* No comment provided by engineer. */ +"You can give another try." = "Vous pouvez faire un nouvel essai."; + /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "Vous pouvez masquer ou mettre en sourdine un profil d'utilisateur - faites-le glisser vers la droite."; +/* No comment provided by engineer. */ +"You can make it visible to your SimpleX contacts via Settings." = "Vous pouvez le rendre visible à vos contacts SimpleX via Paramètres."; + /* notification body */ -"You can now send messages to %@" = "Vous pouvez maintenant envoyer des messages à %@"; +"You can now chat with %@" = "Vous pouvez maintenant envoyer des messages à %@"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "Vous pouvez envoyer des messages à %@ à partir des contacts archivés."; + +/* No comment provided by engineer. */ +"You can set connection name, to remember who the link was shared with." = "Vous pouvez définir un nom de connexion pour vous rappeler avec qui le lien a été partagé."; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Vous pouvez configurer l'aperçu des notifications sur l'écran de verrouillage via les paramètres."; @@ -3822,10 +5562,10 @@ "You can share this address with your contacts to let them connect with **%@**." = "Vous pouvez partager cette adresse avec vos contacts pour leur permettre de se connecter avec **%@**."; /* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Vous pouvez partager votre adresse sous forme de lien ou de code QR - n'importe qui pourra se connecter à vous."; +"You can start chat via app Settings / Database or by restarting the app" = "Vous pouvez lancer le chat via Paramètres / Base de données ou en redémarrant l'app"; /* No comment provided by engineer. */ -"You can start chat via app Settings / Database or by restarting the app" = "Vous pouvez lancer le chat via Paramètres / Base de données ou en redémarrant l'app"; +"You can still view conversation with %@ in the list of chats." = "Vous pouvez toujours voir la conversation avec %@ dans la liste des discussions."; /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Vous pouvez activer SimpleX Lock dans les Paramètres."; @@ -3833,6 +5573,9 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Vous pouvez utiliser le format markdown pour mettre en forme les messages :"; +/* alert message */ +"You can view invitation link again in connection details." = "Vous pouvez à nouveau consulter le lien d'invitation dans les détails de la connexion."; + /* No comment provided by engineer. */ "You can't send messages!" = "Vous ne pouvez pas envoyer de messages !"; @@ -3849,10 +5592,10 @@ "you changed role of %@ to %@" = "vous avez modifié le rôle de %1$@ pour %2$@"; /* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Vous contrôlez par quel·s serveur·s vous pouvez **transmettre** ainsi que par quel·s serveur·s vous pouvez **recevoir** les messages de vos contacts."; +"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 could not be verified; please try again." = "Vous n'avez pas pu être vérifié·e ; veuillez réessayer."; +"You decide who can connect." = "Vous choisissez qui peut se connecter."; /* No comment provided by engineer. */ "You have already requested connection via this address!" = "Vous avez déjà demandé une connexion via cette adresse !"; @@ -3860,9 +5603,6 @@ /* No comment provided by engineer. */ "You have already requested connection!\nRepeat connection request?" = "Vous avez déjà demandé une connexion !\nRépéter la demande de connexion ?"; -/* No comment provided by engineer. */ -"You have no chats" = "Vous n'avez aucune discussion"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Vous devez saisir la phrase secrète à chaque fois que l'application démarre - elle n'est pas stockée sur l'appareil."; @@ -3878,9 +5618,18 @@ /* snd group event chat item */ "you left" = "vous avez quitté"; +/* No comment provided by engineer. */ +"You may migrate the exported database." = "Vous pouvez migrer la base de données exportée."; + +/* No comment provided by engineer. */ +"You may save the exported archive." = "Vous pouvez enregistrer l'archive exportée."; + /* 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." = "Vous devez utiliser la version la plus récente de votre base de données de chat sur un seul appareil UNIQUEMENT, sinon vous risquez de ne plus recevoir les messages de certains contacts."; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "Vous devez autoriser votre contact à appeler pour pouvoir l'appeler."; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "Vous devez autoriser votre contact à envoyer des messages vocaux pour pouvoir en envoyer."; @@ -3899,6 +5648,9 @@ /* chat list item description */ "you shared one-time link incognito" = "vous avez partagé un lien unique en incognito"; +/* snd group event chat item */ +"you unblocked %@" = "vous avez débloqué %@"; + /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "Vous serez connecté·e au groupe lorsque l'appareil de l'hôte sera en ligne, veuillez attendre ou vérifier plus tard !"; @@ -3920,6 +5672,9 @@ /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "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 chat. Chat history will be preserved." = "Vous ne recevrez plus de messages de cette discussion. L'historique sera préservé."; + /* No comment provided by engineer. */ "You will stop receiving messages from this group. Chat history will be preserved." = "Vous ne recevrez plus de messages de ce groupe. L'historique du chat sera conservé."; @@ -3935,9 +5690,6 @@ /* 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" = "Vous utilisez un profil incognito pour ce groupe - pour éviter de partager votre profil principal ; inviter des contacts n'est pas possible"; -/* No comment provided by engineer. */ -"Your %@ servers" = "Vos serveurs %@"; - /* No comment provided by engineer. */ "Your calls" = "Vos appels"; @@ -3947,11 +5699,14 @@ /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Votre base de données de chat n'est pas chiffrée - définisez une phrase secrète."; +/* alert title */ +"Your chat preferences" = "Vos préférences de discussion"; + /* No comment provided by engineer. */ "Your chat profiles" = "Vos profils de chat"; /* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Votre contact a besoin d'être en ligne pour completer la connexion.\nVous pouvez annuler la connexion et supprimer le contact (et réessayer plus tard avec un autre lien)."; +"Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Votre connexion a été déplacée vers %@ mais une erreur inattendue s'est produite lors de la redirection vers le profil."; /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Votre contact a envoyé un fichier plus grand que la taille maximale supportée actuellement(%@)."; @@ -3960,10 +5715,10 @@ "Your contacts can allow full message deletion." = "Vos contacts peuvent autoriser la suppression complète des messages."; /* No comment provided by engineer. */ -"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "Vos contacts dans SimpleX la verront.\nVous pouvez modifier ce choix dans les Paramètres."; +"Your contacts will remain connected." = "Vos contacts resteront connectés."; /* No comment provided by engineer. */ -"Your contacts will remain connected." = "Vos contacts resteront connectés."; +"Your credentials may be sent unencrypted." = "Vos informations d'identification peuvent être envoyées non chiffrées."; /* No comment provided by engineer. */ "Your current chat database will be DELETED and REPLACED with the imported one." = "Votre base de données de chat actuelle va être SUPPRIMEE et REMPLACEE par celle importée."; @@ -3987,7 +5742,10 @@ "Your profile **%@** will be shared." = "Votre profil **%@** sera partagé."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Votre profil est stocké sur votre appareil et est seulement partagé avec vos contacts.\nLes serveurs SimpleX ne peuvent pas voir votre profil."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Votre profil est stocké sur votre appareil et est seulement partagé avec vos contacts. Les serveurs SimpleX ne peuvent pas voir votre profil."; + +/* alert message */ +"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Votre profil a été modifié. Si vous l'enregistrez, le profil mis à jour sera envoyé à tous vos contacts."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Votre profil, vos contacts et les messages reçus sont stockés sur votre appareil."; @@ -3996,10 +5754,10 @@ "Your random profile" = "Votre profil aléatoire"; /* No comment provided by engineer. */ -"Your server" = "Votre serveur"; +"Your server address" = "Votre adresse de serveur"; /* No comment provided by engineer. */ -"Your server address" = "Votre adresse de serveur"; +"Your servers" = "Vos serveurs"; /* No comment provided by engineer. */ "Your settings" = "Vos paramètres"; @@ -4007,9 +5765,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "Votre adresse SimpleX"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Vos serveurs SMP"; - -/* No comment provided by engineer. */ -"Your XFTP servers" = "Vos serveurs XFTP"; - diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings new file mode 100644 index 0000000000..5a9b6b4e38 --- /dev/null +++ b/apps/ios/hu.lproj/Localizable.strings @@ -0,0 +1,6017 @@ +/* No comment provided by engineer. */ +" (can be copied)" = " (másolható)"; + +/* No comment provided by engineer. */ +"_italic_" = "\\_dőlt_"; + +/* 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." = "- kapcsolódás a [könyvtár szolgáltatáshoz](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- kézbesítési jelentések (legfeljebb 20 tag).\n- gyorsabb és stabilabb."; + +/* 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. */ +"- 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!"; + +/* No comment provided by engineer. */ +"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- legfeljebb 5 perc hosszúságú hangüzenetek.\n- egyéni üzenet-eltűnési időkorlát.\n- előzmények szerkesztése."; + +/* No comment provided by engineer. */ +"!1 colored!" = "!1 színezett!"; + +/* No comment provided by engineer. */ +"(new)" = "(új)"; + +/* 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."; + +/* No comment provided by engineer. */ +"**Create group**: to create a new group." = "**Csoport létrehozása:** új csoport létrehozásához."; + +/* No comment provided by engineer. */ +"**e2e encrypted** audio call" = "**e2e titkosított** hanghívás"; + +/* No comment provided by engineer. */ +"**e2e encrypted** video call" = "**e2e titkosított** videóhívás"; + +/* No comment provided by engineer. */ +"**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)."; + +/* 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."; + +/* 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."; + +/* 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. */ +"**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Figyelmeztetés:** Az azonnali push-értesítésekhez a kulcstartóban tárolt jelmondat megadása szükséges."; + +/* No comment provided by engineer. */ +"**Warning**: the archive will be removed." = "**Figyelmeztetés:** az archívum el lesz távolítva."; + +/* No comment provided by engineer. */ +"*bold*" = "\\*félkövér*"; + +/* copied message info title, # <title> */ +"# %@" = "# %@"; + +/* copied message info */ +"## History" = "## Előzmények"; + +/* copied message info */ +"## In reply to" = "## Válaszul erre"; + +/* No comment provided by engineer. */ +"#secret#" = "#titok#"; + +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"%@ (current)" = "%@ (jelenlegi)"; + +/* copied message info */ +"%@ (current):" = "%@ (jelenlegi):"; + +/* No comment provided by engineer. */ +"%@ / %@" = "%@ / %@"; + +/* No comment provided by engineer. */ +"%@ %@" = "%@ %@"; + +/* No comment provided by engineer. */ +"%@ and %@" = "%@ és %@"; + +/* No comment provided by engineer. */ +"%@ and %@ connected" = "%@ és %@ kapcsolódott"; + +/* copied message info, <sender> at <time> */ +"%@ at %@:" = "%1$@ ekkor: %2$@:"; + +/* No comment provided by engineer. */ +"%@ connected" = "%@ kapcsolódott"; + +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ letöltve"; + +/* notification title */ +"%@ is connected!" = "%@ kapcsolódott!"; + +/* No comment provided by engineer. */ +"%@ is not verified" = "%@ nincs hitelesítve"; + +/* No comment provided by engineer. */ +"%@ is verified" = "%@ hitelesítve"; + +/* No comment provided by engineer. */ +"%@ server" = "%@ kiszolgáló"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ kiszolgáló"; + +/* No comment provided by engineer. */ +"%@ uploaded" = "%@ feltöltve"; + +/* notification title */ +"%@ wants to connect!" = "%@ kapcsolódni szeretne!"; + +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + +/* No comment provided by engineer. */ +"%@, %@ and %lld members" = "%@, %@ és további %lld tag"; + +/* No comment provided by engineer. */ +"%@, %@ and %lld other members connected" = "%@, %@ és további %lld tag kapcsolódott"; + +/* copied message info */ +"%@:" = "%@:"; + +/* time interval */ +"%d days" = "%d nap"; + +/* forward confirmation reason */ +"%d file(s) are still being downloaded." = "%d fájl letöltése még folyamatban van."; + +/* forward confirmation reason */ +"%d file(s) failed to download." = "Nem sikerült letölteni %d fájlt."; + +/* forward confirmation reason */ +"%d file(s) were deleted." = "%d fájl törölve lett."; + +/* forward confirmation reason */ +"%d file(s) were not downloaded." = "%d fájl nem lett letöltve."; + +/* time interval */ +"%d hours" = "%d óra"; + +/* alert title */ +"%d messages not forwarded" = "%d üzenet nem lett továbbítva"; + +/* time interval */ +"%d min" = "%d perc"; + +/* time interval */ +"%d months" = "%d hónap"; + +/* time interval */ +"%d sec" = "%d mp"; + +/* delete after time */ +"%d seconds(s)" = "%d másodperc"; + +/* integrity error chat item */ +"%d skipped message(s)" = "%d üzenet kihagyva"; + +/* time interval */ +"%d weeks" = "%d hét"; + +/* No comment provided by engineer. */ +"%lld" = "%lld"; + +/* No comment provided by engineer. */ +"%lld %@" = "%lld %@"; + +/* No comment provided by engineer. */ +"%lld contact(s) selected" = "%lld partner kijelölve"; + +/* No comment provided by engineer. */ +"%lld file(s) with total size of %@" = "%lld fájl, %@ összméretben"; + +/* No comment provided by engineer. */ +"%lld group events" = "%lld csoportesemény"; + +/* No comment provided by engineer. */ +"%lld members" = "%lld tag"; + +/* No comment provided by engineer. */ +"%lld messages blocked" = "%lld üzenet letiltva"; + +/* No comment provided by engineer. */ +"%lld messages blocked by admin" = "%lld üzenetet letiltott az adminisztrátor"; + +/* No comment provided by engineer. */ +"%lld messages marked deleted" = "%lld üzenet megjelölve törlésre"; + +/* No comment provided by engineer. */ +"%lld messages moderated by %@" = "%@ %lld üzenetet moderált"; + +/* No comment provided by engineer. */ +"%lld minutes" = "%lld perc"; + +/* No comment provided by engineer. */ +"%lld new interface languages" = "%lld új kezelőfelületi nyelv"; + +/* No comment provided by engineer. */ +"%lld seconds" = "%lld mp"; + +/* No comment provided by engineer. */ +"%lldd" = "%lldn"; + +/* No comment provided by engineer. */ +"%lldh" = "%lldó"; + +/* No comment provided by engineer. */ +"%lldk" = "%lldk"; + +/* No comment provided by engineer. */ +"%lldm" = "%lldp"; + +/* No comment provided by engineer. */ +"%lldmth" = "%lldh"; + +/* No comment provided by engineer. */ +"%llds" = "%lldmp"; + +/* No comment provided by engineer. */ +"%lldw" = "%lldhét"; + +/* No comment provided by engineer. */ +"%u messages failed to decrypt." = "Nem sikerült visszafejteni %u üzenetet."; + +/* No comment provided by engineer. */ +"%u messages skipped." = "%u üzenet kihagyva."; + +/* No comment provided by engineer. */ +"`a + b`" = "a + b"; + +/* email text */ +"<p>Hi!</p>\n<p><a href=\"%@\">Connect to me via SimpleX Chat</a></p>" = "<p>Üdvözlöm!</p>\n<p><a href=\"%@\">Csatlakozzon hozzám a SimpleX Chaten keresztül</a></p>"; + +/* No comment provided by engineer. */ +"~strike~" = "\\~áthúzott~"; + +/* time to disappear */ +"0 sec" = "0 mp"; + +/* No comment provided by engineer. */ +"0s" = "0s"; + +/* delete after time +time interval */ +"1 day" = "1 nap"; + +/* time interval */ +"1 hour" = "1 óra"; + +/* No comment provided by engineer. */ +"1 minute" = "1 perc"; + +/* delete after time +time interval */ +"1 month" = "1 hónap"; + +/* delete after time +time interval */ +"1 week" = "1 hét"; + +/* delete after time */ +"1 year" = "1 év"; + +/* No comment provided by engineer. */ +"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ó."; + +/* No comment provided by engineer. */ +"5 minutes" = "5 perc"; + +/* No comment provided by engineer. */ +"6" = "6"; + +/* No comment provided by engineer. */ +"30 seconds" = "30 másodperc"; + +/* No comment provided by engineer. */ +"A few more things" = "Néhány további dolog"; + +/* notification title */ +"A new contact" = "Egy új partner"; + +/* No comment provided by engineer. */ +"A new random profile will be shared." = "Egy új, véletlenszerű profil lesz megosztva."; + +/* No comment provided by engineer. */ +"A separate TCP connection will be used **for each chat profile you have in the app**." = "**Az összes csevegési profiljához az alkalmazásban** külön TCP-kapcsolat (és SOCKS-hitelesítőadat) lesz használva."; + +/* 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." = "**Az összes partneréhez és csoporttaghoz** külön TCP-kapcsolat (és SOCKS-hitelesítőadat) lesz használva.\n**Megjegyzés:** ha sok kapcsolata van, az akkumulátor-használat és az adatforgalom jelentősen megnövekedhet, és néhány kapcsolódási kísérlet sikertelen lehet."; + +/* No comment provided by engineer. */ +"Abort" = "Megszakítás"; + +/* No comment provided by engineer. */ +"Abort changing address" = "Cím módosításának megszakítása"; + +/* No comment provided by engineer. */ +"Abort changing address?" = "Megszakítja a cím módosítását?"; + +/* No comment provided by engineer. */ +"About operators" = "Az üzemeltetőkről"; + +/* No comment provided by engineer. */ +"About SimpleX Chat" = "A SimpleX Chat névjegye"; + +/* No comment provided by engineer. */ +"above, then choose:" = "gombra fent, majd válassza ki:"; + +/* No comment provided by engineer. */ +"Accent" = "Kiemelőszín"; + +/* accept contact request via notification +accept incoming call via notification +swipe action */ +"Accept" = "Elfogadás"; + +/* No comment provided by engineer. */ +"Accept conditions" = "Feltételek elfogadása"; + +/* No comment provided by engineer. */ +"Accept connection request?" = "Elfogadja a meghívási kérést?"; + +/* notification body */ +"Accept contact request from %@?" = "Elfogadja %@ meghívási kérését?"; + +/* accept contact request via notification +swipe action */ +"Accept incognito" = "Elfogadás inkognitóban"; + +/* call status */ +"accepted call" = "fogadott hívás"; + +/* No comment provided by engineer. */ +"Accepted conditions" = "Elfogadott feltételek"; + +/* chat list item title */ +"accepted invitation" = "elfogadott meghívó"; + +/* No comment provided by engineer. */ +"Acknowledged" = "Visszaigazolt"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Visszaigazolási hibák"; + +/* token status text */ +"Active" = "Aktív"; + +/* No comment provided by engineer. */ +"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."; + +/* No comment provided by engineer. */ +"Add friends" = "Barátok hozzáadása"; + +/* No comment provided by engineer. */ +"Add list" = "Lista hozzáadása"; + +/* No comment provided by engineer. */ +"Add profile" = "Profil hozzáadása"; + +/* No comment provided by engineer. */ +"Add server" = "Kiszolgáló hozzáadása"; + +/* No comment provided by engineer. */ +"Add servers by scanning QR codes." = "Kiszolgáló hozzáadása QR-kód beolvasásával."; + +/* No comment provided by engineer. */ +"Add team members" = "Munkatársak hozzáadása"; + +/* No comment provided by engineer. */ +"Add to another device" = "Hozzáadás egy másik eszközhöz"; + +/* No comment provided by engineer. */ +"Add to list" = "Hozzáadás listához"; + +/* No comment provided by engineer. */ +"Add welcome message" = "Üdvözlőüzenet hozzáadása"; + +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "Adja hozzá a munkatársait a beszélgetésekhez."; + +/* No comment provided by engineer. */ +"Added media & file servers" = "Hozzáadott média- és fájlkiszolgálók"; + +/* No comment provided by engineer. */ +"Added message servers" = "Hozzáadott üzenetkiszolgálók"; + +/* No comment provided by engineer. */ +"Additional accent" = "További kiemelőszín"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "További kiemelőszín 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "További másodlagos szín"; + +/* No comment provided by engineer. */ +"Address" = "Cím"; + +/* No comment provided by engineer. */ +"Address change will be aborted. Old receiving address will be used." = "A cím módosítása meg fog szakadni. A régi fogadási cím lesz használva."; + +/* No comment provided by engineer. */ +"Address or 1-time link?" = "Cím vagy egyszer használható meghívó?"; + +/* No comment provided by engineer. */ +"Address settings" = "Címbeállítások"; + +/* member role */ +"admin" = "adminisztrátor"; + +/* feature role */ +"admins" = "adminisztrátorok"; + +/* No comment provided by engineer. */ +"Admins can block a member for all." = "Az adminisztrátorok egy tagot a csoport összes tagja számára letilthatnak."; + +/* No comment provided by engineer. */ +"Admins can create the links to join groups." = "Az adminisztrátorok hivatkozásokat hozhatnak létre a csoportokhoz való csatlakozáshoz."; + +/* No comment provided by engineer. */ +"Advanced network settings" = "Speciális hálózati beállítások"; + +/* No comment provided by engineer. */ +"Advanced settings" = "Speciális beállítások"; + +/* chat item text */ +"agreeing encryption for %@…" = "titkosítás elfogadása %@ számára…"; + +/* chat item text */ +"agreeing encryption…" = "titkosítás elfogadása…"; + +/* No comment provided by engineer. */ +"All" = "Összes"; + +/* No comment provided by engineer. */ +"All app data is deleted." = "Az összes alkalmazásadat törölve."; + +/* No comment provided by engineer. */ +"All chats and messages will be deleted - this cannot be undone!" = "Az összes csevegés és üzenet törölve lesz – ez a művelet nem vonható vissza!"; + +/* alert message */ +"All chats will be removed from the list %@, and the list deleted." = "Az összes csevegés el lesz távolítva a(z) %@ nevű listáról, és a lista is törölve lesz."; + +/* No comment provided by engineer. */ +"All data is erased when it is entered." = "A jelkód megadása után az összes adat törölve lesz."; + +/* No comment provided by engineer. */ +"All data is kept private on your device." = "Az összes adat privát módon van tárolva az eszközén."; + +/* No comment provided by engineer. */ +"All group members will remain connected." = "Az összes csoporttag kapcsolatban marad."; + +/* feature role */ +"all members" = "összes tag"; + +/* No comment provided by engineer. */ +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Az összes üzenet és fájl **végpontok közötti titkosítással**, a közvetlen üzenetek továbbá kvantumbiztos titkosítással is rendelkeznek."; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone!" = "Az összes üzenet törölve lesz – ez a művelet nem vonható vissza!"; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Az összes üzenet törölve lesz – ez a művelet nem vonható vissza! Az üzenetek CSAK az Ön számára törlődnek."; + +/* No comment provided by engineer. */ +"All new messages from %@ will be hidden!" = "%@ összes új üzenete el lesz rejtve!"; + +/* profile dropdown */ +"All profiles" = "Összes profil"; + +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Az összes jelentés archiválva lesz az Ön számára."; + +/* No comment provided by engineer. */ +"All servers" = "Összes kiszolgáló"; + +/* No comment provided by engineer. */ +"All your contacts will remain connected." = "Az összes partnerével kapcsolatban marad."; + +/* No comment provided by engineer. */ +"All your contacts will remain connected. Profile update will be sent to your contacts." = "A partnereivel 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."; + +/* No comment provided by engineer. */ +"Allow" = "Engedélyezés"; + +/* No comment provided by engineer. */ +"Allow calls only if your contact allows them." = "A hívások kezdeményezése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi."; + +/* No comment provided by engineer. */ +"Allow calls?" = "Engedélyezi a hívásokat?"; + +/* No comment provided by engineer. */ +"Allow disappearing messages only if your contact allows it to you." = "Az eltűnő üzenetek küldése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi az Ön számára."; + +/* No comment provided by engineer. */ +"Allow downgrade" = "Visszafejlesztés engedélyezése"; + +/* 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 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."; + +/* No comment provided by engineer. */ +"Allow message reactions." = "A reakciók hozzáadása az üzenetekhez engedélyezve van."; + +/* 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 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 to irreversibly delete sent messages. (24 hours)" = "Az elküldött üzenetek végleges törlése engedélyezve van. (24 óra)"; + +/* No comment provided by engineer. */ +"Allow to report messsages to moderators." = "Az üzenetek jelentése a moderátorok felé engedélyezve van."; + +/* No comment provided by engineer. */ +"Allow to send files and media." = "A fájlok- és a médiatartalmak küldése engedélyezve van."; + +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "A SimpleX-hivatkozások küldése engedélyezve van."; + +/* No comment provided by engineer. */ +"Allow to send voice messages." = "A hangüzenetek küldése engedélyezve van."; + +/* No comment provided by engineer. */ +"Allow voice messages only if your contact allows them." = "A hangüzenetek küldése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi."; + +/* No comment provided by engineer. */ +"Allow voice messages?" = "Engedélyezi a hangüzeneteket?"; + +/* No comment provided by engineer. */ +"Allow your contacts adding message reactions." = "A reakciók hozzáadása az üzenetekhez engedélyezve van a partnerei számára."; + +/* No comment provided by engineer. */ +"Allow your contacts to call you." = "A hívások kezdeményezése engedélyezve van a partnerei számára."; + +/* No comment provided by engineer. */ +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Az elküldött üzenetek végleges törlése engedélyezve van a partnerei számára. (24 óra)"; + +/* No comment provided by engineer. */ +"Allow your contacts to send disappearing messages." = "Az eltűnő üzenetek küldésének engedélyezése a partnerei számára."; + +/* No comment provided by engineer. */ +"Allow your contacts to send voice messages." = "A hangüzenetek küldése engedélyezve van a partnerei számára."; + +/* No comment provided by engineer. */ +"Already connected?" = "Már kapcsolódott?"; + +/* No comment provided by engineer. */ +"Already connecting!" = "Kapcsolódás folyamatban!"; + +/* No comment provided by engineer. */ +"Already joining the group!" = "A csatlakozás folyamatban van a csoporthoz!"; + +/* pref value */ +"always" = "mindig"; + +/* No comment provided by engineer. */ +"Always use private routing." = "Mindig használjon privát útválasztást."; + +/* No comment provided by engineer. */ +"Always use relay" = "Mindig használjon továbbítókiszolgálót"; + +/* 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."; + +/* No comment provided by engineer. */ +"and %lld other events" = "és további %lld esemény"; + +/* report reason */ +"Another reason" = "Egyéb indoklás"; + +/* 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: %@" = "Az alkalmazás összeállítási száma: %@"; + +/* No comment provided by engineer. */ +"App data migration" = "Alkalmazásadatok átköltöztetése"; + +/* No comment provided by engineer. */ +"App encrypts new local files (except videos)." = "Az alkalmazás titkosítja a helyi fájlokat (a videók kivételével)."; + +/* No comment provided by engineer. */ +"App group:" = "Alkalmazáscsoport:"; + +/* No comment provided by engineer. */ +"App icon" = "Alkalmazásikon"; + +/* No comment provided by engineer. */ +"App passcode" = "Alkalmazás jelkód"; + +/* No comment provided by engineer. */ +"App passcode is replaced with self-destruct passcode." = "Az alkalmazás-jelkód helyettesítve lesz egy önmegsemmisítő-jelkóddal."; + +/* No comment provided by engineer. */ +"App session" = "Alkalmazás munkamenete"; + +/* No comment provided by engineer. */ +"App version" = "Az alkalmazás verziója"; + +/* No comment provided by engineer. */ +"App version: v%@" = "Az alkalmazás verziója: v%@"; + +/* No comment provided by engineer. */ +"Appearance" = "Megjelenés"; + +/* No comment provided by engineer. */ +"Apply" = "Alkalmaz"; + +/* No comment provided by engineer. */ +"Apply to" = "Alkalmazás erre"; + +/* No comment provided by engineer. */ +"Archive" = "Archívum"; + +/* No comment provided by engineer. */ +"Archive %lld reports?" = "Archivál %lld jelentést?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "Archiválja az összes jelentést?"; + +/* No comment provided by engineer. */ +"Archive and upload" = "Archiválás és feltöltés"; + +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "A partnerek archiválása a későbbi csevegéshez."; + +/* No comment provided by engineer. */ +"Archive report" = "Jelentés archiválása"; + +/* No comment provided by engineer. */ +"Archive report?" = "Archiválja a jelentést?"; + +/* swipe action */ +"Archive reports" = "Jelentések archiválása"; + +/* No comment provided by engineer. */ +"Archived contacts" = "Archivált partnerek"; + +/* No comment provided by engineer. */ +"archived report" = "archivált jelentés"; + +/* No comment provided by engineer. */ +"Archiving database" = "Adatbázis archiválása"; + +/* No comment provided by engineer. */ +"Attach" = "Mellékelés"; + +/* No comment provided by engineer. */ +"attempts" = "próbálkozások"; + +/* No comment provided by engineer. */ +"Audio & video calls" = "Hang- és videóhívások"; + +/* No comment provided by engineer. */ +"Audio and video calls" = "Hang- és videóhívások"; + +/* No comment provided by engineer. */ +"audio call (not e2e encrypted)" = "hanghívás (nem e2e titkosított)"; + +/* chat feature */ +"Audio/video calls" = "Hang- és videóhívások"; + +/* No comment provided by engineer. */ +"Audio/video calls are prohibited." = "A hívások kezdeményezése le van tiltva ebben a csevegésben."; + +/* PIN entry */ +"Authentication cancelled" = "Hitelesítés visszavonva"; + +/* No comment provided by engineer. */ +"Authentication failed" = "Sikertelen hitelesítés"; + +/* No comment provided by engineer. */ +"Authentication is required before the call is connected, but you may miss calls." = "A hívás összekapcsolása előtt hitelesítésre van szükség, de előfordulhat, hogy lemarad a hívásokról."; + +/* No comment provided by engineer. */ +"Authentication unavailable" = "Hitelesítés elérhetetlen"; + +/* member role */ +"author" = "szerző"; + +/* No comment provided by engineer. */ +"Auto-accept" = "Automatikus elfogadás"; + +/* No comment provided by engineer. */ +"Auto-accept contact requests" = "Meghívási kérések automatikus elfogadása"; + +/* No comment provided by engineer. */ +"Auto-accept images" = "Képek automatikus elfogadása"; + +/* alert title */ +"Auto-accept settings" = "Beállítások automatikus elfogadása"; + +/* No comment provided by engineer. */ +"Back" = "Vissza"; + +/* No comment provided by engineer. */ +"Background" = "Háttér"; + +/* No comment provided by engineer. */ +"Bad desktop address" = "Érvénytelen számítógépcím"; + +/* integrity error chat item */ +"bad message hash" = "érvénytelen az üzenet hasítóértéke"; + +/* No comment provided by engineer. */ +"Bad message hash" = "Érvénytelen az üzenet hasítóértéke"; + +/* integrity error chat item */ +"bad message ID" = "téves üzenet ID"; + +/* No comment provided by engineer. */ +"Bad message ID" = "Téves üzenet ID"; + +/* No comment provided by engineer. */ +"Better calls" = "Továbbfejlesztett hívásélmény"; + +/* No comment provided by engineer. */ +"Better groups" = "Továbbfejlesztett csoportok"; + +/* No comment provided by engineer. */ +"Better groups performance" = "Továbbfejlesztett, gyorsabb csoportok"; + +/* No comment provided by engineer. */ +"Better message dates." = "Továbbfejlesztett üzenetdátumok."; + +/* No comment provided by engineer. */ +"Better messages" = "Továbbfejlesztett üzenetek"; + +/* No comment provided by engineer. */ +"Better networking" = "Jobb hálózatkezelés"; + +/* No comment provided by engineer. */ +"Better notifications" = "Továbbfejlesztett értesítések"; + +/* No comment provided by engineer. */ +"Better privacy and security" = "Továbbfejlesztett adatvédelem és biztonság"; + +/* No comment provided by engineer. */ +"Better security ✅" = "Továbbfejlesztett biztonság ✅"; + +/* No comment provided by engineer. */ +"Better user experience" = "Továbbfejlesztett felhasználói élmény"; + +/* No comment provided by engineer. */ +"Black" = "Fekete"; + +/* No comment provided by engineer. */ +"Block" = "Letiltás"; + +/* No comment provided by engineer. */ +"Block for all" = "Letiltás"; + +/* No comment provided by engineer. */ +"Block group members" = "Csoporttagok letiltása"; + +/* No comment provided by engineer. */ +"Block member" = "Letiltás"; + +/* No comment provided by engineer. */ +"Block member for all?" = "Az összes tag számára letiltja a tagot?"; + +/* No comment provided by engineer. */ +"Block member?" = "Letiltja a tagot?"; + +/* marked deleted chat item preview text */ +"blocked" = "letiltva"; + +/* rcv group event chat item */ +"blocked %@" = "letiltotta őt: %@"; + +/* blocked chat item +marked deleted chat item preview text */ +"blocked by admin" = "letiltva az adminisztrátor által"; + +/* No comment provided by engineer. */ +"Blocked by admin" = "Letiltva az adminisztrátor által"; + +/* No comment provided by engineer. */ +"Blur for better privacy." = "Elhomályosítás a jobb adatvédelemért."; + +/* No comment provided by engineer. */ +"Blur media" = "Médiatartalom elhomályosítása"; + +/* No comment provided by engineer. */ +"bold" = "félkövér"; + +/* No comment provided by engineer. */ +"Both you and your contact can add message reactions." = "Mindkét fél hozzáadhat az üzenetekhez reakciókat."; + +/* No comment provided by engineer. */ +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "Mindkét fél véglegesen törölheti az elküldött üzeneteket. (24 óra)"; + +/* No comment provided by engineer. */ +"Both you and your contact can make calls." = "Mindkét fél tud hívásokat kezdeményezni."; + +/* No comment provided by engineer. */ +"Both you and your contact can send disappearing messages." = "Mindkét fél küldhet eltűnő üzeneteket."; + +/* No comment provided by engineer. */ +"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)!"; + +/* No comment provided by engineer. */ +"Business address" = "Üzleti cím"; + +/* No comment provided by engineer. */ +"Business chats" = "Üzleti csevegések"; + +/* No comment provided by engineer. */ +"Businesses" = "Üzleti"; + +/* 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"; + +/* No comment provided by engineer. */ +"Call already ended!" = "A hívás már befejeződött!"; + +/* call status */ +"call error" = "híváshiba"; + +/* call status */ +"call in progress" = "hívás folyamatban"; + +/* call status */ +"calling…" = "hívás…"; + +/* No comment provided by engineer. */ +"Calls" = "Hívások"; + +/* No comment provided by engineer. */ +"Calls prohibited!" = "A hívások le vannak tiltva!"; + +/* No comment provided by engineer. */ +"Camera not available" = "A kamera nem elérhető"; + +/* No comment provided by engineer. */ +"Can't call contact" = "Nem lehet felhívni a partnert"; + +/* No comment provided by engineer. */ +"Can't call member" = "Nem lehet felhívni a tagot"; + +/* No comment provided by engineer. */ +"Can't invite contact!" = "Nem lehet meghívni a partnert!"; + +/* No comment provided by engineer. */ +"Can't invite contacts!" = "Nem lehet meghívni a partnereket!"; + +/* No comment provided by engineer. */ +"Can't message member" = "Nem lehet üzenetet küldeni a tagnak"; + +/* alert action +alert button */ +"Cancel" = "Mégse"; + +/* No comment provided by engineer. */ +"Cancel migration" = "Átköltöztetés visszavonása"; + +/* feature offered item */ +"cancelled %@" = "%@ visszavonva"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Nem lehet hozzáférni a kulcstartóhoz az adatbázisjelszó mentéséhez"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Nem lehet továbbítani az üzenetet"; + +/* alert title */ +"Cannot receive file" = "Nem lehet fogadni a fájlt"; + +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Kapacitás túllépés – a címzett nem kapta meg a korábban elküldött üzeneteket."; + +/* No comment provided by engineer. */ +"Cellular" = "Mobilhálózat"; + +/* No comment provided by engineer. */ +"Change" = "Módosítás"; + +/* alert title */ +"Change automatic message deletion?" = "Módosítja az automatikus üzenettörlést?"; + +/* authentication reason */ +"Change chat profiles" = "Csevegési profilok módosítása"; + +/* No comment provided by engineer. */ +"Change database passphrase?" = "Módosítja az adatbázis jelmondatát?"; + +/* authentication reason */ +"Change lock mode" = "Zárolási mód módosítása"; + +/* No comment provided by engineer. */ +"Change member role?" = "Módosítja a tag szerepkörét?"; + +/* authentication reason */ +"Change passcode" = "Jelkód módosítása"; + +/* No comment provided by engineer. */ +"Change receiving address" = "Fogadási cím módosítása"; + +/* No comment provided by engineer. */ +"Change receiving address?" = "Módosítja a fogadási címet?"; + +/* No comment provided by engineer. */ +"Change role" = "Szerepkör módosítása"; + +/* authentication reason */ +"Change self-destruct mode" = "Önmegsemmisítő-mód módosítása"; + +/* authentication reason +set passcode view */ +"Change self-destruct passcode" = "Önmegsemmisítő-jelkód módosítása"; + +/* chat item text */ +"changed address for you" = "módosította a címet az Ön számára"; + +/* rcv group event chat item */ +"changed role of %@ to %@" = "a következőre módosította %1$@ szerepkörét: „%2$@”"; + +/* rcv group event chat item */ +"changed your role to %@" = "a következőre módosította az Ön szerepkörét: „%@”"; + +/* chat item text */ +"changing address for %@…" = "cím módosítása %@ számára…"; + +/* chat item text */ +"changing address…" = "cím módosítása…"; + +/* No comment provided by engineer. */ +"Chat" = "Csevegés"; + +/* No comment provided by engineer. */ +"Chat already exists" = "A csevegés már létezik"; + +/* No comment provided by engineer. */ +"Chat already exists!" = "A csevegés már létezik!"; + +/* No comment provided by engineer. */ +"Chat colors" = "Csevegés színei"; + +/* No comment provided by engineer. */ +"Chat console" = "Csevegési konzol"; + +/* No comment provided by engineer. */ +"Chat database" = "Csevegési adatbázis"; + +/* No comment provided by engineer. */ +"Chat database deleted" = "Csevegési adatbázis törölve"; + +/* No comment provided by engineer. */ +"Chat database exported" = "Csevegési adatbázis exportálva"; + +/* No comment provided by engineer. */ +"Chat database imported" = "Csevegési adatbázis importálva"; + +/* No comment provided by engineer. */ +"Chat is running" = "A csevegés fut"; + +/* No comment provided by engineer. */ +"Chat is stopped" = "A csevegés megállt"; + +/* 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." = "A csevegés megállt. Ha már használta ezt az adatbázist egy másik eszközön, úgy visszaállítás szükséges a csevegés elindítása előtt."; + +/* No comment provided by engineer. */ +"Chat list" = "Csevegési lista"; + +/* No comment provided by engineer. */ +"Chat migrated!" = "A csevegés átköltöztetve!"; + +/* No comment provided by engineer. */ +"Chat preferences" = "Csevegési beállítások"; + +/* alert message */ +"Chat preferences were changed." = "A csevegési beállítások módosultak."; + +/* No comment provided by engineer. */ +"Chat profile" = "Csevegési profil"; + +/* No comment provided by engineer. */ +"Chat theme" = "Csevegés témája"; + +/* No comment provided by engineer. */ +"Chat will be deleted for all members - this cannot be undone!" = "A csevegés minden tag számára törölve lesz – ez a művelet nem vonható vissza!"; + +/* 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!"; + +/* No comment provided by engineer. */ +"Chats" = "Csevegések"; + +/* 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 title */ +"Check server address and try again." = "Kiszolgáló címének ellenőrzése és újrapróbálkozás."; + +/* No comment provided by engineer. */ +"Chinese and Spanish interface" = "Kínai és spanyol kezelőfelület"; + +/* No comment provided by engineer. */ +"Choose _Migrate from another device_ on the new device and scan QR code." = "Válassza az _Átköltöztetés egy másik eszközről_ opciót az új eszközén és olvassa be a QR-kódot."; + +/* No comment provided by engineer. */ +"Choose file" = "Fájl kiválasztása"; + +/* No comment provided by engineer. */ +"Choose from library" = "Választás a könyvtárból"; + +/* No comment provided by engineer. */ +"Chunks deleted" = "Törölt töredékek"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Letöltött töredékek"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Feltöltött töredékek"; + +/* swipe action */ +"Clear" = "Kiürítés"; + +/* No comment provided by engineer. */ +"Clear conversation" = "Üzenetek kiürítése"; + +/* No comment provided by engineer. */ +"Clear conversation?" = "Kiüríti az üzeneteket?"; + +/* No comment provided by engineer. */ +"Clear group?" = "Kiüríti a csoportot?"; + +/* No comment provided by engineer. */ +"Clear or delete group?" = "Csoport kiürítése vagy törlése?"; + +/* No comment provided by engineer. */ +"Clear private notes?" = "Kiüríti a privát jegyzeteket?"; + +/* No comment provided by engineer. */ +"Clear verification" = "Hitelesítés törlése"; + +/* No comment provided by engineer. */ +"Color chats with the new themes." = "Csevegések színezése új témákkal."; + +/* No comment provided by engineer. */ +"Color mode" = "Színmód"; + +/* No comment provided by engineer. */ +"colored" = "színezett"; + +/* report reason */ +"Community guidelines violation" = "Közösségi irányelvek megsértése"; + +/* server test step */ +"Compare file" = "Fájl-összehasonlítás"; + +/* No comment provided by engineer. */ +"Compare security codes with your contacts." = "Biztonsági kódok összehasonlítása a partnerekével."; + +/* No comment provided by engineer. */ +"complete" = "befejezett"; + +/* No comment provided by engineer. */ +"Completed" = "Elkészült"; + +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "Feltételek elfogadásának ideje: %@."; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "A következő üzemeltető(k) számára elfogadott feltételek: **%@**."; + +/* No comment provided by engineer. */ +"Conditions are already accepted for these operator(s): **%@**." = "A feltételek már el lettek fogadva a következő üzemeltető(k) számára: **%@**."; + +/* No comment provided by engineer. */ +"Conditions of use" = "Használati feltételek"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "A feltételek el lesznek fogadva a következő üzemeltető(k) számára: **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "A feltételek el lesznek fogadva a következő időpontban: %@."; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "A feltételek automatikusan el lesznek fogadva az engedélyezett üzemeltetők számára a következő időpontban: %@."; + +/* No comment provided by engineer. */ +"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"; + +/* No comment provided by engineer. */ +"Confirm" = "Megerősítés"; + +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "Biztosan törli a partnert?"; + +/* No comment provided by engineer. */ +"Confirm database upgrades" = "Adatbázis fejlesztésének megerősítése"; + +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Ismeretlen kiszolgálókról származó fájlok megerősítése."; + +/* No comment provided by engineer. */ +"Confirm network settings" = "Hálózati beállítások megerősítése"; + +/* No comment provided by engineer. */ +"Confirm new passphrase…" = "Új jelmondat megerősítése…"; + +/* No comment provided by engineer. */ +"Confirm Passcode" = "Jelkód megerősítése"; + +/* No comment provided by engineer. */ +"Confirm password" = "Jelszó megerősítése"; + +/* No comment provided by engineer. */ +"Confirm that you remember database passphrase to migrate it." = "Az átköltöztetéshez erősítse meg, hogy emlékszik az adatbázis jelmondatára."; + +/* No comment provided by engineer. */ +"Confirm upload" = "Feltöltés megerősítése"; + +/* token status text */ +"Confirmed" = "Megerősítve"; + +/* server test step */ +"Connect" = "Kapcsolódás"; + +/* No comment provided by engineer. */ +"Connect automatically" = "Kapcsolódás automatikusan"; + +/* No comment provided by engineer. */ +"Connect incognito" = "Kapcsolódás inkognitóban"; + +/* No comment provided by engineer. */ +"Connect to desktop" = "Társítás számítógéppel"; + +/* No comment provided by engineer. */ +"connect to SimpleX Chat developers." = "kapcsolódás a SimpleX Chat fejlesztőkhöz."; + +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Kapcsolódjon gyorsabban a partnereihez."; + +/* No comment provided by engineer. */ +"Connect to yourself?" = "Kapcsolódik saját magához?"; + +/* No comment provided by engineer. */ +"Connect to yourself?\nThis is your own one-time link!" = "Kapcsolódik saját magához?\nEz a saját egyszer használható meghívója!"; + +/* No comment provided by engineer. */ +"Connect to yourself?\nThis is your own SimpleX address!" = "Kapcsolódik saját magához?\nEz a saját SimpleX-címe!"; + +/* No comment provided by engineer. */ +"Connect via contact address" = "Kapcsolódás a kapcsolattartási címen keresztül"; + +/* No comment provided by engineer. */ +"Connect via link" = "Kapcsolódás egy hivatkozáson keresztül"; + +/* No comment provided by engineer. */ +"Connect via one-time link" = "Kapcsolódás egyszer használható meghívón keresztül"; + +/* No comment provided by engineer. */ +"Connect with %@" = "Kapcsolódás a következővel: %@"; + +/* No comment provided by engineer. */ +"connected" = "kapcsolódott"; + +/* No comment provided by engineer. */ +"Connected" = "Kapcsolódott"; + +/* No comment provided by engineer. */ +"Connected desktop" = "Társított számítógép"; + +/* rcv group event chat item */ +"connected directly" = "közvetlenül kapcsolódott"; + +/* No comment provided by engineer. */ +"Connected servers" = "Kapcsolódott kiszolgálók"; + +/* No comment provided by engineer. */ +"Connected to desktop" = "Kapcsolódva a számítógéphez"; + +/* No comment provided by engineer. */ +"connecting" = "kapcsolódás"; + +/* No comment provided by engineer. */ +"Connecting" = "Kapcsolódás"; + +/* No comment provided by engineer. */ +"connecting (accepted)" = "kapcsolódás (elfogadva)"; + +/* No comment provided by engineer. */ +"connecting (announced)" = "kapcsolódás (bejelentve)"; + +/* No comment provided by engineer. */ +"connecting (introduced)" = "kapcsolódás (bemutatkozva)"; + +/* No comment provided by engineer. */ +"connecting (introduction invitation)" = "kapcsolódás (bemutatkozó meghívó)"; + +/* call status */ +"connecting call" = "kapcsolódási hívás…"; + +/* No comment provided by engineer. */ +"Connecting server…" = "Kapcsolódás a kiszolgálóhoz…"; + +/* No comment provided by engineer. */ +"Connecting server… (error: %@)" = "Kapcsolódás a kiszolgálóhoz… (hiba: %@)"; + +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "Kapcsolódás a partnerhez, várjon vagy ellenőrizze később!"; + +/* No comment provided by engineer. */ +"Connecting to desktop" = "Kapcsolódás a számítógéphez"; + +/* No comment provided by engineer. */ +"connecting…" = "kapcsolódás…"; + +/* No comment provided by engineer. */ +"Connection" = "Kapcsolat"; + +/* No comment provided by engineer. */ +"Connection and servers status." = "Kapcsolatok- és kiszolgálók állapotának megjelenítése."; + +/* No comment provided by engineer. */ +"Connection blocked" = "A kapcsolat le van tiltva"; + +/* No comment provided by engineer. */ +"Connection error" = "Kapcsolódási hiba"; + +/* No comment provided by engineer. */ +"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 is blocked by server operator:\n%@" = "A kiszolgáló üzemeltetője letiltotta a kapcsolatot:\n%@"; + +/* No comment provided by engineer. */ +"Connection not ready." = "A kapcsolat nem áll készen."; + +/* No comment provided by engineer. */ +"Connection notifications" = "Kapcsolódási értesítések"; + +/* No comment provided by engineer. */ +"Connection request sent!" = "Meghívási kérés elküldve!"; + +/* No comment provided by engineer. */ +"Connection requires encryption renegotiation." = "A kapcsolat titkosítása újraegyeztetést igényel."; + +/* No comment provided by engineer. */ +"Connection security" = "Kapcsolatbiztonság"; + +/* No comment provided by engineer. */ +"Connection terminated" = "Kapcsolat megszakítva"; + +/* No comment provided by engineer. */ +"Connection timeout" = "Időtúllépés kapcsolódáskor"; + +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "A kapcsolat a számítógéppel megszakadt"; + +/* connection information */ +"connection:%@" = "kapcsolat: %@"; + +/* No comment provided by engineer. */ +"Connections" = "Kapcsolatok"; + +/* profile update event chat item */ +"contact %@ changed to %@" = "%1$@ a következőre módosította a nevét: %2$@"; + +/* No comment provided by engineer. */ +"Contact allows" = "Partner engedélyezi"; + +/* No comment provided by engineer. */ +"Contact already exists" = "A partner már létezik"; + +/* No comment provided by engineer. */ +"Contact deleted!" = "Partner törölve!"; + +/* No comment provided by engineer. */ +"contact has e2e encryption" = "a partner e2e titkosítással rendelkezik"; + +/* No comment provided by engineer. */ +"contact has no e2e encryption" = "a partner nem rendelkezik e2e titkosítással"; + +/* notification */ +"Contact hidden:" = "Rejtett név:"; + +/* notification */ +"Contact is connected" = "Partnere kapcsolódott"; + +/* No comment provided by engineer. */ +"Contact is deleted." = "Törölt partner."; + +/* No comment provided by engineer. */ +"Contact name" = "Csak név"; + +/* No comment provided by engineer. */ +"Contact preferences" = "Partnerbeállítások"; + +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "A partner törölve lesz – ez a művelet nem vonható vissza!"; + +/* No comment provided by engineer. */ +"Contacts" = "Partnerek"; + +/* No comment provided by engineer. */ +"Contacts can mark messages for deletion; you will be able to view them." = "A partnerei törlésre jelölhetnek üzeneteket; Ön majd meg tudja nézni azokat."; + +/* blocking reason */ +"Content violates conditions of use" = "A tartalom sérti a használati feltételeket"; + +/* No comment provided by engineer. */ +"Continue" = "Folytatás"; + +/* No comment provided by engineer. */ +"Conversation deleted!" = "Beszélgetés törölve!"; + +/* No comment provided by engineer. */ +"Copy" = "Másolás"; + +/* No comment provided by engineer. */ +"Copy error" = "Másolási hiba"; + +/* No comment provided by engineer. */ +"Core version: v%@" = "Fő verzió: v%@"; + +/* No comment provided by engineer. */ +"Corner" = "Sarok"; + +/* No comment provided by engineer. */ +"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"; + +/* No comment provided by engineer. */ +"Create a group using a random profile." = "Csoport létrehozása véletlenszerű profillal."; + +/* server test step */ +"Create file" = "Fájl létrehozása"; + +/* No comment provided by engineer. */ +"Create group" = "Csoport létrehozása"; + +/* No comment provided by engineer. */ +"Create group link" = "Csoporthivatkozás létrehozása"; + +/* No comment provided by engineer. */ +"Create link" = "Hivatkozás létrehozása"; + +/* No comment provided by engineer. */ +"Create list" = "Lista létrehozása"; + +/* No comment provided by engineer. */ +"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Új profil létrehozása a [számítógép-alkalmazásban](https://simplex.chat/downloads/). 💻"; + +/* No comment provided by engineer. */ +"Create profile" = "Profil létrehozása"; + +/* server test step */ +"Create queue" = "Sorba állítás létrehozása"; + +/* No comment provided by engineer. */ +"Create secret group" = "Titkos csoport létrehozása"; + +/* No comment provided by engineer. */ +"Create SimpleX address" = "SimpleX-cím létrehozása"; + +/* No comment provided by engineer. */ +"Create your profile" = "Saját profil létrehozása"; + +/* No comment provided by engineer. */ +"Created" = "Létrehozva"; + +/* No comment provided by engineer. */ +"Created at" = "Létrehozva"; + +/* copied message info */ +"Created at: %@" = "Létrehozva: %@"; + +/* No comment provided by engineer. */ +"Creating archive link" = "Archívum hivatkozás létrehozása"; + +/* No comment provided by engineer. */ +"Creating link…" = "Hivatkozás létrehozása…"; + +/* No comment provided by engineer. */ +"creator" = "készítő"; + +/* No comment provided by engineer. */ +"Current conditions text couldn't be loaded, you can review conditions via this link:" = "A jelenlegi feltételek szövegét nem lehetett betölteni, a feltételeket a következő hivatkozáson keresztül vizsgálhatja felül:"; + +/* No comment provided by engineer. */ +"Current Passcode" = "Jelenlegi jelkód"; + +/* No comment provided by engineer. */ +"Current passphrase…" = "Jelenlegi jelmondat…"; + +/* No comment provided by engineer. */ +"Current profile" = "Jelenlegi profil"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Jelenleg támogatott legnagyobb fájl méret: %@."; + +/* dropdown time picker choice */ +"custom" = "egyéni"; + +/* No comment provided by engineer. */ +"Custom time" = "Egyéni időköz"; + +/* No comment provided by engineer. */ +"Customizable message shape." = "Személyre szabható üzenetbuborékok."; + +/* No comment provided by engineer. */ +"Customize theme" = "Téma személyre szabása"; + +/* No comment provided by engineer. */ +"Dark" = "Sötét"; + +/* No comment provided by engineer. */ +"Dark mode colors" = "Sötét mód színei"; + +/* No comment provided by engineer. */ +"Database downgrade" = "Adatbázis visszafejlesztése"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "Adatbázis titkosítva!"; + +/* No comment provided by engineer. */ +"Database encryption passphrase will be updated and stored in the keychain.\n" = "Az adatbázis titkosítási jelmondata frissülni fog és a kulcstartóban lesz tárolva.\n"; + +/* No comment provided by engineer. */ +"Database encryption passphrase will be updated.\n" = "Az adatbázis titkosítási jelmondata frissítve lesz.\n"; + +/* No comment provided by engineer. */ +"Database error" = "Adatbázishiba"; + +/* No comment provided by engineer. */ +"Database ID" = "Adatbázis-azonosító"; + +/* copied message info */ +"Database ID: %d" = "Adatbázis-azonosító: %d"; + +/* No comment provided by engineer. */ +"Database IDs and Transport isolation option." = "Adatbázis-azonosítók és átvitel-izolációs beállítások."; + +/* No comment provided by engineer. */ +"Database is encrypted using a random passphrase, you can change it." = "Az adatbázis egy véletlenszerű jelmondattal van titkosítva, amelyet szabadon módosíthat."; + +/* No comment provided by engineer. */ +"Database is encrypted using a random passphrase. Please change it before exporting." = "Az adatbázis egy véletlenszerű jelmondattal van titkosítva. Exportálás előtt módosítsa."; + +/* No comment provided by engineer. */ +"Database passphrase" = "Adatbázis-jelmondat"; + +/* No comment provided by engineer. */ +"Database passphrase & export" = "Adatbázis-jelmondat és -exportálás"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Az adatbázis jelmondata nem egyezik a kulcstartóba mentettől."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "A csevegés megnyitásához adja meg az adatbázis jelmondatát."; + +/* No comment provided by engineer. */ +"Database upgrade" = "Adatbázis fejlesztése"; + +/* No comment provided by engineer. */ +"database version is newer than the app, but no down migration for: %@" = "az adatbázis verziója újabb, mint az alkalmazásé, de a visszafelé történő átköltöztetés viszont nem lehetséges a következőhöz: %@"; + +/* No comment provided by engineer. */ +"Database will be encrypted and the passphrase stored in the keychain.\n" = "Az adatbázis titkosítva lesz, a jelmondat pedig a kulcstartóban lesz tárolva.\n"; + +/* No comment provided by engineer. */ +"Database will be encrypted.\n" = "Az adatbázis titkosítva lesz.\n"; + +/* No comment provided by engineer. */ +"Database will be migrated when the app restarts" = "Az adatbázis az alkalmazás újraindításakor lesz átköltöztetve"; + +/* time unit */ +"days" = "nap"; + +/* No comment provided by engineer. */ +"Debug delivery" = "Kézbesítési hibák felderítése"; + +/* No comment provided by engineer. */ +"Decentralized" = "Decentralizált"; + +/* message decrypt error item */ +"Decryption error" = "Titkosítás visszafejtési hiba"; + +/* No comment provided by engineer. */ +"decryption errors" = "visszafejtési hibák"; + +/* delete after time +pref value */ +"default (%@)" = "alapértelmezett (%@)"; + +/* No comment provided by engineer. */ +"default (no)" = "alapértelmezett (nem)"; + +/* No comment provided by engineer. */ +"default (yes)" = "alapértelmezett (igen)"; + +/* alert action +swipe action */ +"Delete" = "Törlés"; + +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "Törli a tagok %lld üzenetét?"; + +/* No comment provided by engineer. */ +"Delete %lld messages?" = "Töröl %lld üzenetet?"; + +/* No comment provided by engineer. */ +"Delete address" = "Cím törlése"; + +/* No comment provided by engineer. */ +"Delete address?" = "Törli a címet?"; + +/* No comment provided by engineer. */ +"Delete after" = "Törlés ennyi idő után"; + +/* No comment provided by engineer. */ +"Delete all files" = "Az összes fájl törlése"; + +/* No comment provided by engineer. */ +"Delete and notify contact" = "Törlés, és a partner értesítése"; + +/* No comment provided by engineer. */ +"Delete chat" = "Csevegés törlése"; + +/* No comment provided by engineer. */ +"Delete chat messages from your device." = "Csevegési üzenetek törlése a saját eszközéről."; + +/* No comment provided by engineer. */ +"Delete chat profile" = "Csevegési profil törlése"; + +/* No comment provided by engineer. */ +"Delete chat profile?" = "Törli a csevegési profilt?"; + +/* No comment provided by engineer. */ +"Delete chat?" = "Törli a csevegést?"; + +/* No comment provided by engineer. */ +"Delete connection" = "Kapcsolat törlése"; + +/* No comment provided by engineer. */ +"Delete contact" = "Partner törlése"; + +/* No comment provided by engineer. */ +"Delete contact?" = "Törli a partnert?"; + +/* No comment provided by engineer. */ +"Delete database" = "Adatbázis törlése"; + +/* No comment provided by engineer. */ +"Delete database from this device" = "Adatbázis törlése erről az eszközről"; + +/* server test step */ +"Delete file" = "Fájl törlése"; + +/* No comment provided by engineer. */ +"Delete files and media?" = "Törli a fájl- és a médiatartalmakat?"; + +/* No comment provided by engineer. */ +"Delete files for all chat profiles" = "Fájlok törlése az összes csevegési profilból"; + +/* chat feature */ +"Delete for everyone" = "Törlés az összes tagnál"; + +/* No comment provided by engineer. */ +"Delete for me" = "Csak nálam"; + +/* No comment provided by engineer. */ +"Delete group" = "Csoport törlése"; + +/* No comment provided by engineer. */ +"Delete group?" = "Törli a csoportot?"; + +/* No comment provided by engineer. */ +"Delete invitation" = "Meghívó törlése"; + +/* No comment provided by engineer. */ +"Delete link" = "Törlés"; + +/* No comment provided by engineer. */ +"Delete link?" = "Törli a hivatkozást?"; + +/* alert title */ +"Delete list?" = "Törli a listát?"; + +/* No comment provided by engineer. */ +"Delete member message?" = "Törli a tag üzenetét?"; + +/* No comment provided by engineer. */ +"Delete message?" = "Törli az üzenetet?"; + +/* alert button */ +"Delete messages" = "Üzenetek törlése"; + +/* No comment provided by engineer. */ +"Delete messages after" = "Üzenetek törlése ennyi idő után"; + +/* No comment provided by engineer. */ +"Delete old database" = "Régi adatbázis törlése"; + +/* No comment provided by engineer. */ +"Delete old database?" = "Törli a régi adatbázist?"; + +/* No comment provided by engineer. */ +"Delete or moderate up to 200 messages." = "Legfeljebb 200 üzenet egyszerre való törlése, vagy moderálása."; + +/* No comment provided by engineer. */ +"Delete pending connection?" = "Törli a függőben lévő meghívót?"; + +/* No comment provided by engineer. */ +"Delete profile" = "Profil törlése"; + +/* server test step */ +"Delete queue" = "Sorba állítás törlése"; + +/* No comment provided by engineer. */ +"Delete report" = "Jelentés törlése"; + +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Legfeljebb 20 üzenet egyszerre való törlése."; + +/* No comment provided by engineer. */ +"Delete user profile?" = "Törli a felhasználói profilt?"; + +/* No comment provided by engineer. */ +"Delete without notification" = "Törlés értesítés nélkül"; + +/* deleted chat item */ +"deleted" = "törölve"; + +/* No comment provided by engineer. */ +"Deleted" = "Törölve"; + +/* No comment provided by engineer. */ +"Deleted at" = "Törölve"; + +/* copied message info */ +"Deleted at: %@" = "Törölve: %@"; + +/* rcv direct event chat item */ +"deleted contact" = "törölt partner"; + +/* rcv group event chat item */ +"deleted group" = "törölt csoport"; + +/* No comment provided by engineer. */ +"Deletion errors" = "Törlési hibák"; + +/* No comment provided by engineer. */ +"Delivered even when Apple drops them." = "Kézbesítés akkor is, amikor az Apple eldobja őket."; + +/* No comment provided by engineer. */ +"Delivery" = "Kézbesítés"; + +/* No comment provided by engineer. */ +"Delivery receipts are disabled!" = "A kézbesítési jelentések le vannak tiltva!"; + +/* No comment provided by engineer. */ +"Delivery receipts!" = "Kézbesítési jelentések!"; + +/* No comment provided by engineer. */ +"Description" = "Leírás"; + +/* No comment provided by engineer. */ +"Desktop address" = "Számítógép címe"; + +/* No comment provided by engineer. */ +"Desktop app version %@ is not compatible with this app." = "A számítógép-alkalmazás verziója (%@) nem kompatibilis ezzel az alkalmazással."; + +/* No comment provided by engineer. */ +"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."; + +/* 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."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Részletes statisztikák"; + +/* No comment provided by engineer. */ +"Details" = "További részletek"; + +/* No comment provided by engineer. */ +"Develop" = "Fejlesztés"; + +/* No comment provided by engineer. */ +"Developer options" = "Fejlesztői beállítások"; + +/* No comment provided by engineer. */ +"Developer tools" = "Fejlesztői eszközök"; + +/* No comment provided by engineer. */ +"Device" = "Eszköz"; + +/* No comment provided by engineer. */ +"Device authentication is disabled. Turning off SimpleX Lock." = "Az eszközön nincs beállítva a képernyőzár. A SimpleX-zár ki van kapcsolva."; + +/* No comment provided by engineer. */ +"Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." = "Az eszközön nincs beállítva a képernyőzár. A SimpleX-zár az „Adatvédelem és biztonság” menüben kapcsolható be, miután beállította a képernyőzárat az eszközén."; + +/* No comment provided by engineer. */ +"different migration in the app/database: %@ / %@" = "különböző átköltöztetés az alkalmazásban/adatbázisban: %@ / %@"; + +/* No comment provided by engineer. */ +"Different names, avatars and transport isolation." = "Különböző nevek, profilképek és átvitel-izoláció."; + +/* connection level description */ +"direct" = "közvetlen"; + +/* chat feature */ +"Direct messages" = "Közvetlen üzenetek"; + +/* No comment provided by engineer. */ +"Direct messages between members are prohibited in this chat." = "A tagok közötti közvetlen üzenetek le vannak tiltva ebben a csevegésben."; + +/* 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. */ +"Disable (keep overrides)" = "Letiltás (felülírások megtartásával)"; + +/* alert title */ +"Disable automatic message deletion?" = "Letiltja az automatikus üzenettörlést?"; + +/* alert button */ +"Disable delete messages" = "Üzenetek törlésének letiltása"; + +/* No comment provided by engineer. */ +"Disable for all" = "Letiltás"; + +/* authentication reason */ +"Disable SimpleX Lock" = "SimpleX-zár kikapcsolása"; + +/* No comment provided by engineer. */ +"disabled" = "letiltva"; + +/* No comment provided by engineer. */ +"Disabled" = "Letiltva"; + +/* No comment provided by engineer. */ +"Disappearing message" = "Eltűnő üzenet"; + +/* chat feature */ +"Disappearing messages" = "Eltűnő üzenetek"; + +/* No comment provided by engineer. */ +"Disappearing messages are prohibited in this chat." = "Az eltűnő üzenetek küldése le van tiltva ebben a csevegésben."; + +/* No comment provided by engineer. */ +"Disappearing messages are prohibited." = "Az eltűnő üzenetek küldése le van tiltva."; + +/* No comment provided by engineer. */ +"Disappears at" = "Eltűnik"; + +/* copied message info */ +"Disappears at: %@" = "Eltűnik: %@"; + +/* server test step */ +"Disconnect" = "Kapcsolat bontása"; + +/* No comment provided by engineer. */ +"Disconnect desktop?" = "Leválasztja a számítógépet?"; + +/* No comment provided by engineer. */ +"Discover and join groups" = "Csoportok felfedezése és csatlakozás"; + +/* No comment provided by engineer. */ +"Discover via local network" = "Felfedezés helyi hálózaton keresztül"; + +/* No comment provided by engineer. */ +"Do it later" = "Befejezés később"; + +/* 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 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."; + +/* No comment provided by engineer. */ +"Do not use credentials with proxy." = "Ne használja a hitelesítőadatokat proxyval."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "NE használjon privát útválasztást."; + +/* No comment provided by engineer. */ +"Do NOT use SimpleX for emergency calls." = "NE használja a SimpleXet segélyhívásokhoz."; + +/* No comment provided by engineer. */ +"Documents:" = "Dokumentumok:"; + +/* No comment provided by engineer. */ +"Don't create address" = "Ne hozzon létre címet"; + +/* No comment provided by engineer. */ +"Don't enable" = "Ne engedélyezze"; + +/* No comment provided by engineer. */ +"Don't miss important messages." = "Ne maradjon le a fontos üzenetekről."; + +/* No comment provided by engineer. */ +"Don't show again" = "Ne mutasd újra"; + +/* No comment provided by engineer. */ +"Done" = "Kész"; + +/* No comment provided by engineer. */ +"Downgrade and open chat" = "Visszafejlesztés és a csevegés megnyitása"; + +/* alert button +chat item action */ +"Download" = "Letöltés"; + +/* No comment provided by engineer. */ +"Download errors" = "Letöltési hibák"; + +/* No comment provided by engineer. */ +"Download failed" = "Sikertelen letöltés"; + +/* server test step */ +"Download file" = "Fájl letöltése"; + +/* alert action */ +"Download files" = "Fájlok letöltése"; + +/* No comment provided by engineer. */ +"Downloaded" = "Letöltve"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Letöltött fájlok"; + +/* No comment provided by engineer. */ +"Downloading archive" = "Archívum letöltése"; + +/* No comment provided by engineer. */ +"Downloading link details" = "Letöltési hivatkozás részletei"; + +/* No comment provided by engineer. */ +"Duplicate display name!" = "Duplikált megjelenítendő név!"; + +/* integrity error chat item */ +"duplicate message" = "duplikált üzenet"; + +/* No comment provided by engineer. */ +"duplicates" = "duplikációk"; + +/* No comment provided by engineer. */ +"Duration" = "Időtartam"; + +/* No comment provided by engineer. */ +"e2e encrypted" = "e2e titkosított"; + +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "Végpontok közötti titkosított értesítések."; + +/* chat item action */ +"Edit" = "Szerkesztés"; + +/* No comment provided by engineer. */ +"Edit group profile" = "Csoportprofil szerkesztése"; + +/* No comment provided by engineer. */ +"Enable" = "Engedélyezés"; + +/* No comment provided by engineer. */ +"Enable (keep overrides)" = "Engedélyezés (felülírások megtartásával)"; + +/* 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"; + +/* No comment provided by engineer. */ +"Enable Flux in Network & servers settings for better metadata privacy." = "A Flux kiszolgálókat engedélyezheti a beállításokban, a „Hálózat és kiszolgálók” menüben, a metaadatok jobb védelme érdekében."; + +/* No comment provided by engineer. */ +"Enable for all" = "Engedélyezés az összes tag számára"; + +/* No comment provided by engineer. */ +"Enable in direct chats (BETA)!" = "Engedélyezés a közvetlen csevegésekben (BÉTA)!"; + +/* 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"; + +/* 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?"; + +/* No comment provided by engineer. */ +"Enable self-destruct" = "Önmegsemmisítés engedélyezése"; + +/* set passcode view */ +"Enable self-destruct passcode" = "Önmegsemmisítő-jelkód engedélyezése"; + +/* authentication reason */ +"Enable SimpleX Lock" = "SimpleX-zár bekapcsolása"; + +/* No comment provided by engineer. */ +"Enable TCP keep-alive" = "TCP életben tartása"; + +/* enabled status */ +"enabled" = "engedélyezve"; + +/* No comment provided by engineer. */ +"Enabled" = "Engedélyezve"; + +/* No comment provided by engineer. */ +"Enabled for" = "Számukra engedélyezve"; + +/* enabled status */ +"enabled for contact" = "engedélyezve a partner számára"; + +/* enabled status */ +"enabled for you" = "engedélyezve az Ön számára"; + +/* No comment provided by engineer. */ +"Encrypt" = "Titkosít"; + +/* No comment provided by engineer. */ +"Encrypt database?" = "Titkosítja az adatbázist?"; + +/* No comment provided by engineer. */ +"Encrypt local files" = "Helyi fájlok titkosítása"; + +/* No comment provided by engineer. */ +"Encrypt stored files & media" = "A tárolt fájlok- és a médiatartalmak titkosítása"; + +/* No comment provided by engineer. */ +"Encrypted database" = "Titkosított adatbázis"; + +/* notification */ +"Encrypted message or another event" = "Titkosított üzenet vagy más esemény"; + +/* notification */ +"Encrypted message: app is stopped" = "Titkosított üzenet: az alkalmazás megállt"; + +/* notification */ +"Encrypted message: database error" = "Titkosított üzenet: adatbázishiba"; + +/* notification */ +"Encrypted message: database migration error" = "Titkosított üzenet: adatbázis-átköltöztetési hiba"; + +/* notification */ +"Encrypted message: keychain error" = "Titkosított üzenet: kulcstartó hiba"; + +/* notification */ +"Encrypted message: no passphrase" = "Titkosított üzenet: nincs jelmondat"; + +/* notification */ +"Encrypted message: unexpected error" = "Titkosított üzenet: váratlan hiba"; + +/* chat item text */ +"encryption agreed" = "titkosítás elfogadva"; + +/* chat item text */ +"encryption agreed for %@" = "titkosítás elfogadva %@ számára"; + +/* chat item text */ +"encryption ok" = "titkosítás rendben"; + +/* chat item text */ +"encryption ok for %@" = "titkosítás rendben %@ számára"; + +/* chat item text */ +"encryption re-negotiation allowed" = "a titkosítás újraegyeztetése engedélyezve van"; + +/* chat item text */ +"encryption re-negotiation allowed for %@" = "a titkosítás újraegyeztetése engedélyezve van %@ számára"; + +/* message decrypt error item */ +"Encryption re-negotiation error" = "Hiba történt a titkosítás újraegyeztetésekor"; + +/* No comment provided by engineer. */ +"Encryption re-negotiation failed." = "Nem sikerült a titkosítást újraegyeztetni."; + +/* chat item text */ +"encryption re-negotiation required" = "a titkosítás újraegyeztetése szükséges"; + +/* chat item text */ +"encryption re-negotiation required for %@" = "a titkosítás újraegyeztetése szükséges %@ számára"; + +/* No comment provided by engineer. */ +"Encryption renegotiation in progress." = "A titkosítás újraegyeztetése folyamatban van."; + +/* No comment provided by engineer. */ +"ended" = "befejeződött"; + +/* call status */ +"ended call %@" = "%@ hívása befejeződött"; + +/* No comment provided by engineer. */ +"Enter correct passphrase." = "Adja meg a helyes jelmondatot."; + +/* No comment provided by engineer. */ +"Enter group name…" = "Adja meg a csoport nevét…"; + +/* No comment provided by engineer. */ +"Enter Passcode" = "Adja meg a jelkódot"; + +/* No comment provided by engineer. */ +"Enter passphrase" = "Adja meg a jelmondatot"; + +/* No comment provided by engineer. */ +"Enter passphrase…" = "Adja meg a jelmondatot…"; + +/* 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 server manually" = "Adja meg a kiszolgálót kézzel"; + +/* No comment provided by engineer. */ +"Enter this device name…" = "Adja meg ennek az eszköznek a nevét…"; + +/* placeholder */ +"Enter welcome message…" = "Adja meg az üdvözlőüzenetet…"; + +/* placeholder */ +"Enter welcome message… (optional)" = "Adja meg az üdvözlőüzenetet… (nem kötelező)"; + +/* No comment provided by engineer. */ +"Enter your name…" = "Adjon meg egy nevet…"; + +/* No comment provided by engineer. */ +"error" = "hiba"; + +/* No comment provided by engineer. */ +"Error" = "Hiba"; + +/* No comment provided by engineer. */ +"Error aborting address change" = "Hiba történt a cím módosításának megszakításakor"; + +/* alert title */ +"Error accepting conditions" = "Hiba történt a feltételek elfogadásakor"; + +/* No comment provided by engineer. */ +"Error accepting contact request" = "Hiba történt a meghívási kérés elfogadásakor"; + +/* No comment provided by engineer. */ +"Error adding member(s)" = "Hiba történt a tag(ok) hozzáadásakor"; + +/* alert title */ +"Error adding server" = "Hiba történt a kiszolgáló hozzáadásakor"; + +/* No comment provided by engineer. */ +"Error changing address" = "Hiba történt a cím módosításakor"; + +/* No comment provided by engineer. */ +"Error changing connection profile" = "Hiba történt a kapcsolati profilra való váltáskor"; + +/* No comment provided by engineer. */ +"Error changing role" = "Hiba történt a szerepkör módosításakor"; + +/* No comment provided by engineer. */ +"Error changing setting" = "Hiba történt a beállítás módosításakor"; + +/* No comment provided by engineer. */ +"Error changing to incognito!" = "Hiba történt az inkognitóprofilra való váltáskor!"; + +/* No comment provided by engineer. */ +"Error checking token status" = "Hiba történt a token állapotának ellenőrzésekor"; + +/* No comment provided by engineer. */ +"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."; + +/* No comment provided by engineer. */ +"Error creating address" = "Hiba történt a cím létrehozásakor"; + +/* No comment provided by engineer. */ +"Error creating group" = "Hiba történt a csoport létrehozásakor"; + +/* No comment provided by engineer. */ +"Error creating group link" = "Hiba történt a csoporthivatkozás létrehozásakor"; + +/* alert title */ +"Error creating list" = "Hiba történt a lista létrehozásakor"; + +/* No comment provided by engineer. */ +"Error creating member contact" = "Hiba történt a partnerrel történő kapcsolat létrehozásában"; + +/* No comment provided by engineer. */ +"Error creating message" = "Hiba történt az üzenet létrehozásakor"; + +/* No comment provided by engineer. */ +"Error creating profile!" = "Hiba történt a profil létrehozásakor!"; + +/* No comment provided by engineer. */ +"Error creating report" = "Hiba történt a jelentés létrehozásakor"; + +/* No comment provided by engineer. */ +"Error decrypting file" = "Hiba történt a fájl visszafejtésekor"; + +/* No comment provided by engineer. */ +"Error deleting chat database" = "Hiba történt a csevegési adatbázis törlésekor"; + +/* No comment provided by engineer. */ +"Error deleting chat!" = "Hiba történt a csevegés törlésekor!"; + +/* No comment provided by engineer. */ +"Error deleting connection" = "Hiba történt a kapcsolat törlésekor"; + +/* No comment provided by engineer. */ +"Error deleting database" = "Hiba történt az adatbázis törlésekor"; + +/* No comment provided by engineer. */ +"Error deleting old database" = "Hiba történt a régi adatbázis törlésekor"; + +/* No comment provided by engineer. */ +"Error deleting token" = "Hiba történt a token törlésekor"; + +/* No comment provided by engineer. */ +"Error deleting user profile" = "Hiba történt a felhasználó-profil törlésekor"; + +/* No comment provided by engineer. */ +"Error downloading the archive" = "Hiba történt az archívum letöltésekor"; + +/* No comment provided by engineer. */ +"Error enabling delivery receipts!" = "Hiba történt a kézbesítési jelentések engedélyezésekor!"; + +/* No comment provided by engineer. */ +"Error enabling notifications" = "Hiba történt az értesítések engedélyezésekor"; + +/* No comment provided by engineer. */ +"Error encrypting database" = "Hiba történt az adatbázis titkosításakor"; + +/* No comment provided by engineer. */ +"Error exporting chat database" = "Hiba történt a csevegési adatbázis exportálásakor"; + +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Hiba történt a téma exportálásakor: %@"; + +/* No comment provided by engineer. */ +"Error importing chat database" = "Hiba történt a csevegési adatbázis importálásakor"; + +/* No comment provided by engineer. */ +"Error joining group" = "Hiba történt a csoporthoz való csatlakozáskor"; + +/* alert title */ +"Error loading servers" = "Hiba történt a kiszolgálók betöltésekor"; + +/* No comment provided by engineer. */ +"Error migrating settings" = "Hiba történt a beállítások átköltöztetésekor"; + +/* No comment provided by engineer. */ +"Error opening chat" = "Hiba történt a csevegés megnyitásakor"; + +/* alert title */ +"Error receiving file" = "Hiba történt a fájl fogadásakor"; + +/* No comment provided by engineer. */ +"Error reconnecting server" = "Hiba történt a kiszolgálóhoz való újrakapcsolódáskor"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Hiba történt a kiszolgálókhoz való újrakapcsolódáskor"; + +/* alert title */ +"Error registering for notifications" = "Hiba történt az értesítések regisztrálásakor"; + +/* No comment provided by engineer. */ +"Error removing member" = "Hiba történt a tag eltávolításakor"; + +/* alert title */ +"Error reordering lists" = "Hiba történt a listák újrarendezésekor"; + +/* No comment provided by engineer. */ +"Error resetting statistics" = "Hiba történt a statisztikák visszaállításakor"; + +/* alert title */ +"Error saving chat list" = "Hiba történt a csevegési lista mentésekor"; + +/* No comment provided by engineer. */ +"Error saving group profile" = "Hiba történt a csoportprofil mentésekor"; + +/* No comment provided by engineer. */ +"Error saving ICE servers" = "Hiba történt az ICE-kiszolgálók mentésekor"; + +/* No comment provided by engineer. */ +"Error saving passcode" = "Hiba történt a jelkód mentésekor"; + +/* No comment provided by engineer. */ +"Error saving passphrase to keychain" = "Hiba történt a jelmondat kulcstartóba történő mentésekor"; + +/* alert title */ +"Error saving servers" = "Hiba történt a kiszolgálók mentésekor"; + +/* when migrating */ +"Error saving settings" = "Hiba történt a beállítások mentésekor"; + +/* No comment provided by engineer. */ +"Error saving user password" = "Hiba történt a felhasználó jelszavának mentésekor"; + +/* No comment provided by engineer. */ +"Error scanning code: %@" = "Hiba történt a kód beolvasásakor: %@"; + +/* No comment provided by engineer. */ +"Error sending email" = "Hiba történt az e-mail elküldésekor"; + +/* No comment provided by engineer. */ +"Error sending member contact invitation" = "Hiba történt a tag kapcsolatfelvételi meghívójának elküldésekor"; + +/* No comment provided by engineer. */ +"Error sending message" = "Hiba történt az üzenet elküldésekor"; + +/* No comment provided by engineer. */ +"Error setting delivery receipts!" = "Hiba történt a kézbesítési jelentések beállításakor!"; + +/* No comment provided by engineer. */ +"Error starting chat" = "Hiba történt a csevegés elindításakor"; + +/* No comment provided by engineer. */ +"Error stopping chat" = "Hiba történt a csevegés megállításakor"; + +/* No comment provided by engineer. */ +"Error switching profile" = "Hiba történt a profilváltáskor"; + +/* alertTitle */ +"Error switching profile!" = "Hiba történt a profilváltáskor!"; + +/* No comment provided by engineer. */ +"Error synchronizing connection" = "Hiba történt a kapcsolat szinkronizálásakor"; + +/* No comment provided by engineer. */ +"Error testing server connection" = "Hiba történt a kiszolgáló kapcsolatának tesztelésekor"; + +/* No comment provided by engineer. */ +"Error updating group link" = "Hiba történt a csoporthivatkozás frissítésekor"; + +/* No comment provided by engineer. */ +"Error updating message" = "Hiba történt az üzenet frissítésekor"; + +/* alert title */ +"Error updating server" = "Hiba történt a kiszolgáló frissítésekor"; + +/* No comment provided by engineer. */ +"Error updating settings" = "Hiba történt a beállítások frissítésekor"; + +/* No comment provided by engineer. */ +"Error updating user privacy" = "Hiba történt a felhasználói adatvédelem frissítésekor"; + +/* No comment provided by engineer. */ +"Error uploading the archive" = "Hiba történt az archívum feltöltésekor"; + +/* No comment provided by engineer. */ +"Error verifying passphrase:" = "Hiba történt a jelmondat hitelesítésekor:"; + +/* No comment provided by engineer. */ +"Error: " = "Hiba: "; + +/* alert message +file error text +snd error text */ +"Error: %@" = "Hiba: %@"; + +/* No comment provided by engineer. */ +"Error: no database file" = "Hiba: nincs adatbázisfájl"; + +/* No comment provided by engineer. */ +"Error: URL is invalid" = "Hiba: a webcím érvénytelen"; + +/* No comment provided by engineer. */ +"Errors" = "Hibák"; + +/* servers error */ +"Errors in servers configuration." = "Hibák a kiszolgálók konfigurációjában."; + +/* No comment provided by engineer. */ +"Even when disabled in the conversation." = "Akkor is, ha le van tiltva a beszélgetésben."; + +/* No comment provided by engineer. */ +"Exit without saving" = "Kilépés mentés nélkül"; + +/* chat item action */ +"Expand" = "Kibontás"; + +/* No comment provided by engineer. */ +"expired" = "lejárt"; + +/* token status text */ +"Expired" = "Lejárt"; + +/* No comment provided by engineer. */ +"Export database" = "Adatbázis exportálása"; + +/* No comment provided by engineer. */ +"Export error:" = "Exportálási hiba:"; + +/* No comment provided by engineer. */ +"Export theme" = "Téma exportálása"; + +/* No comment provided by engineer. */ +"Exported database archive." = "Exportált adatbázis-archívum."; + +/* No comment provided by engineer. */ +"Exported file doesn't exist" = "Az exportált fájl nem létezik"; + +/* No comment provided by engineer. */ +"Exporting database archive…" = "Adatbázis-archívum exportálása…"; + +/* No comment provided by engineer. */ +"Failed to remove passphrase" = "Nem sikerült eltávolítani a jelmondatot"; + +/* No comment provided by engineer. */ +"Fast and no wait until the sender is online!" = "Gyors és nem kell várni, amíg a feladó online lesz!"; + +/* No comment provided by engineer. */ +"Faster deletion of groups." = "Gyorsabb csoporttörlés."; + +/* No comment provided by engineer. */ +"Faster joining and more reliable messages." = "Gyorsabb csatlakozás és megbízhatóbb üzenetkézbesítés."; + +/* No comment provided by engineer. */ +"Faster sending messages." = "Gyorsabb üzenetküldés."; + +/* swipe action */ +"Favorite" = "Kedvenc"; + +/* No comment provided by engineer. */ +"Favorites" = "Kedvencek"; + +/* file error alert title */ +"File error" = "Fájlhiba"; + +/* alert message */ +"File errors:\n%@" = "Fájlhiba:\n%@"; + +/* file error text */ +"File is blocked by server operator:\n%@." = "A kiszolgáló üzemeltetője letiltotta a fájlt:\n%@."; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "A fájl nem található – valószínűleg a fájlt törölték vagy visszavonták."; + +/* file error text */ +"File server error: %@" = "Fájlkiszolgáló-hiba: %@"; + +/* No comment provided by engineer. */ +"File status" = "Fájl állapota"; + +/* copied message info */ +"File status: %@" = "Fájl állapota: %@"; + +/* No comment provided by engineer. */ +"File will be deleted from servers." = "A fájl törölve lesz a kiszolgálókról."; + +/* No comment provided by engineer. */ +"File will be received when your contact completes uploading it." = "A fájl akkor érkezik meg, amikor a küldője befejezte annak feltöltését."; + +/* No comment provided by engineer. */ +"File will be received when your contact is online, please wait or check later!" = "A fájl akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később!"; + +/* No comment provided by engineer. */ +"File: %@" = "Fájl: %@"; + +/* No comment provided by engineer. */ +"Files" = "Fájlok"; + +/* No comment provided by engineer. */ +"Files & media" = "Fájlok és médiatartalmak"; + +/* chat feature */ +"Files and media" = "Fájlok és médiatartalmak"; + +/* No comment provided by engineer. */ +"Files and media are prohibited." = "A fájlok- és a médiatartalmak küldése le van tiltva."; + +/* No comment provided by engineer. */ +"Files and media not allowed" = "A fájlok- és médiatartalmak nincsenek engedélyezve"; + +/* No comment provided by engineer. */ +"Files and media prohibited!" = "A fájlok- és a médiatartalmak küldése le van tiltva!"; + +/* No comment provided by engineer. */ +"Filter unread and favorite chats." = "Olvasatlan és kedvenc csevegésekre való szűrés."; + +/* No comment provided by engineer. */ +"Finalize migration" = "Átköltöztetés véglegesítése"; + +/* No comment provided by engineer. */ +"Finalize migration on another device." = "Átköltöztetés véglegesítése egy másik eszközön."; + +/* No comment provided by engineer. */ +"Finally, we have them! 🚀" = "Végre, megvannak! 🚀"; + +/* No comment provided by engineer. */ +"Find chats faster" = "Csevegési üzenetek gyorsabb megtalálása"; + +/* No comment provided by engineer. */ +"Fix" = "Javítás"; + +/* No comment provided by engineer. */ +"Fix connection" = "Kapcsolat javítása"; + +/* No comment provided by engineer. */ +"Fix connection?" = "Kapcsolat javítása?"; + +/* No comment provided by engineer. */ +"Fix encryption after restoring backups." = "Titkosítás javítása az adatmentések helyreállítása után."; + +/* No comment provided by engineer. */ +"Fix not supported by contact" = "Partner általi javítás nem támogatott"; + +/* No comment provided by engineer. */ +"Fix not supported by group member" = "Csoporttag általi javítás nem támogatott"; + +/* No comment provided by engineer. */ +"For all moderators" = "Az összes moderátor számára"; + +/* servers error */ +"For chat profile %@:" = "A(z) %@ nevű csevegési profilhoz:"; + +/* No comment provided by engineer. */ +"For console" = "Konzolhoz"; + +/* No comment provided by engineer. */ +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Például, ha a partnere egy SimpleX Chat-kiszolgálón keresztül fogadja az üzeneteket, akkor az Ön alkalmazása egy Flux-kiszolgálón keresztül fogja azokat kézbesíteni."; + +/* No comment provided by engineer. */ +"For me" = "Csak magamnak"; + +/* No comment provided by engineer. */ +"For private routing" = "A privát útválasztáshoz"; + +/* No comment provided by engineer. */ +"For social media" = "A közösségi médiához"; + +/* chat item action */ +"Forward" = "Továbbítás"; + +/* alert title */ +"Forward %d message(s)?" = "Továbbít %d üzenetet?"; + +/* No comment provided by engineer. */ +"Forward and save messages" = "Üzenetek továbbítása és mentése"; + +/* alert action */ +"Forward messages" = "Üzenetek továbbítása"; + +/* alert message */ +"Forward messages without files?" = "Továbbítja az üzeneteket fájlok nélkül?"; + +/* No comment provided by engineer. */ +"Forward up to 20 messages at once." = "Legfeljebb 20 üzenet egyszerre való továbbítása."; + +/* No comment provided by engineer. */ +"forwarded" = "továbbított"; + +/* No comment provided by engineer. */ +"Forwarded" = "Továbbított"; + +/* No comment provided by engineer. */ +"Forwarded from" = "Továbbítva innen"; + +/* No comment provided by engineer. */ +"Forwarding %lld messages" = "%lld üzenet továbbítása"; + +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "A(z) %@ továbbítókiszolgáló nem tudott kapcsolódni a(z) %@ 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: %@."; + +/* 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: %@."; + +/* snd error text */ +"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$@"; + +/* No comment provided by engineer. */ +"Found desktop" = "Megtalált számítógép"; + +/* No comment provided by engineer. */ +"French interface" = "Francia kezelőfelület"; + +/* No comment provided by engineer. */ +"Full link" = "Teljes hivatkozás"; + +/* No comment provided by engineer. */ +"Full name (optional)" = "Teljes név (nem kötelező)"; + +/* No comment provided by engineer. */ +"Fully decentralized – visible only to members." = "Teljesen decentralizált – csak a tagok számára látható."; + +/* No comment provided by engineer. */ +"Fully re-implemented - work in background!" = "Teljesen újra implementálva – háttérben történő működés!"; + +/* No comment provided by engineer. */ +"Further reduced battery usage" = "Tovább csökkentett akkumulátor-használat"; + +/* No comment provided by engineer. */ +"Get notified when mentioned." = "Kapjon értesítést, ha megemlítik."; + +/* No comment provided by engineer. */ +"GIFs and stickers" = "GIF-ek és matricák"; + +/* message preview */ +"Good afternoon!" = "Jó napot!"; + +/* message preview */ +"Good morning!" = "Jó reggelt!"; + +/* No comment provided by engineer. */ +"Group" = "Csoport"; + +/* No comment provided by engineer. */ +"Group already exists" = "A csoport már létezik"; + +/* No comment provided by engineer. */ +"Group already exists!" = "A csoport már létezik!"; + +/* No comment provided by engineer. */ +"group deleted" = "a csoport törölve"; + +/* No comment provided by engineer. */ +"Group display name" = "A csoport megjelenített neve"; + +/* No comment provided by engineer. */ +"Group full name (optional)" = "A csoport teljes neve (nem kötelező)"; + +/* No comment provided by engineer. */ +"Group image" = "Csoport profilképe"; + +/* No comment provided by engineer. */ +"Group invitation" = "Csoportmeghívó"; + +/* No comment provided by engineer. */ +"Group invitation expired" = "A csoportmeghívó lejárt"; + +/* No comment provided by engineer. */ +"Group invitation is no longer valid, it was removed by sender." = "A csoportmeghívó már nem érvényes, a küldője eltávolította."; + +/* No comment provided by engineer. */ +"Group link" = "Csoporthivatkozás"; + +/* No comment provided by engineer. */ +"Group links" = "Csoporthivatkozások"; + +/* notification */ +"Group message:" = "Csoport üzenet:"; + +/* No comment provided by engineer. */ +"Group moderation" = "Csoport moderálása"; + +/* No comment provided by engineer. */ +"Group preferences" = "Csoportbeállítások"; + +/* No comment provided by engineer. */ +"Group profile" = "Csoportprofil"; + +/* No comment provided by engineer. */ +"Group profile is stored on members' devices, not on the servers." = "A csoport profilja a tagok eszközein tárolódik, nem a kiszolgálókon."; + +/* snd group event chat item */ +"group profile updated" = "csoportprofil frissítve"; + +/* No comment provided by engineer. */ +"Group welcome message" = "A csoport üdvözlőüzenete"; + +/* No comment provided by engineer. */ +"Group will be deleted for all members - this cannot be undone!" = "A csoport törölve lesz az összes tag számára – ez a művelet nem vonható vissza!"; + +/* No comment provided by engineer. */ +"Group will be deleted for you - this cannot be undone!" = "A csoport törölve lesz az Ön számára – ez a művelet nem vonható vissza!"; + +/* No comment provided by engineer. */ +"Groups" = "Csoportok"; + +/* No comment provided by engineer. */ +"Help" = "Súgó"; + +/* No comment provided by engineer. */ +"Help admins moderating their groups." = "Segítsen az adminisztrátoroknak a csoportjaik moderálásában."; + +/* No comment provided by engineer. */ +"Hidden" = "Se név, se üzenet"; + +/* No comment provided by engineer. */ +"Hidden chat profiles" = "Rejtett csevegési profilok"; + +/* No comment provided by engineer. */ +"Hidden profile password" = "Rejtett profiljelszó"; + +/* chat item action */ +"Hide" = "Összecsukás"; + +/* No comment provided by engineer. */ +"Hide app screen in the recent apps." = "Alkalmazás képernyőjének elrejtése a gyakran használt alkalmazások között."; + +/* No comment provided by engineer. */ +"Hide profile" = "Profil elrejtése"; + +/* No comment provided by engineer. */ +"Hide:" = "Elrejtve:"; + +/* No comment provided by engineer. */ +"History" = "Előzmények"; + +/* 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."; + +/* time unit */ +"hours" = "óra"; + +/* No comment provided by engineer. */ +"How it affects privacy" = "Hogyan érinti az adatvédelmet"; + +/* No comment provided by engineer. */ +"How it helps privacy" = "Hogyan segíti az adatvédelmet"; + +/* alert button */ +"How it works" = "Hogyan működik"; + +/* No comment provided by engineer. */ +"How SimpleX works" = "Hogyan működik a SimpleX"; + +/* No comment provided by engineer. */ +"How to" = "Hogyan"; + +/* No comment provided by engineer. */ +"How to use it" = "Használati útmutató"; + +/* No comment provided by engineer. */ +"How to use your servers" = "Hogyan használja a saját kiszolgálóit"; + +/* No comment provided by engineer. */ +"Hungarian interface" = "Magyar kezelőfelület"; + +/* No comment provided by engineer. */ +"ICE servers (one per line)" = "ICE-kiszolgálók (soronként egy)"; + +/* No comment provided by engineer. */ +"If you can't meet in person, show QR code in a video call, or share the link." = "Ha nem tud személyesen találkozni, mutassa meg a QR-kódot egy videohívás közben, vagy ossza meg a hivatkozást."; + +/* No comment provided by engineer. */ +"If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Ha az alkalmazás megnyitásakor megadja ezt a jelkódot, az összes alkalmazásadat véglegesen el lesz távolítva!"; + +/* 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:"; + +/* 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 alább a **Befejezés később** lehetőségre (az alkalmazás újraindításakor fel lesz ajánlva az adatbázis átköltöztetése)."; + +/* No comment provided by engineer. */ +"Ignore" = "Mellőzés"; + +/* No comment provided by engineer. */ +"Image will be received when your contact completes uploading it." = "A kép akkor érkezik meg, amikor a küldője befejezte annak feltöltését."; + +/* No comment provided by engineer. */ +"Image will be received when your contact is online, please wait or check later!" = "A kép akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később!"; + +/* 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"; + +/* No comment provided by engineer. */ +"Import chat database?" = "Importálja a csevegési adatbázist?"; + +/* No comment provided by engineer. */ +"Import database" = "Adatbázis importálása"; + +/* No comment provided by engineer. */ +"Import failed" = "Sikertelen importálás"; + +/* No comment provided by engineer. */ +"Import theme" = "Téma importálása"; + +/* No comment provided by engineer. */ +"Importing archive" = "Archívum importálása"; + +/* No comment provided by engineer. */ +"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "Továbbfejlesztett kézbesítés, csökkentett adatforgalom-használat.\nTovábbi fejlesztések hamarosan!"; + +/* No comment provided by engineer. */ +"Improved message delivery" = "Továbbfejlesztett üzenetkézbesítés"; + +/* No comment provided by engineer. */ +"Improved privacy and security" = "Fejlesztett adatvédelem és biztonság"; + +/* No comment provided by engineer. */ +"Improved server configuration" = "Javított kiszolgáló konfiguráció"; + +/* No comment provided by engineer. */ +"In order to continue, chat should be stopped." = "A folytatáshoz a csevegést meg kell szakítani."; + +/* No comment provided by engineer. */ +"In reply to" = "Válaszul erre"; + +/* No comment provided by engineer. */ +"In-call sounds" = "Bejövő hívás csengőhangja"; + +/* No comment provided by engineer. */ +"inactive" = "inaktív"; + +/* report reason */ +"Inappropriate content" = "Kifogásolt tartalom"; + +/* report reason */ +"Inappropriate profile" = "Kifogásolt profil"; + +/* No comment provided by engineer. */ +"Incognito" = "Inkognitó"; + +/* No comment provided by engineer. */ +"Incognito groups" = "Inkognitócsoportok"; + +/* No comment provided by engineer. */ +"Incognito mode" = "Inkognitómód"; + +/* No comment provided by engineer. */ +"Incognito mode protects your privacy by using a new random profile for each contact." = "Az inkognitómód úgy védi a személyes adatait, hogy az összes partneréhez új, véletlenszerű profilt használ."; + +/* chat list item description */ +"incognito via contact address link" = "inkognitó a kapcsolattartási címhivatkozáson keresztül"; + +/* chat list item description */ +"incognito via group link" = "inkognitó a csoporthivatkozáson keresztül"; + +/* chat list item description */ +"incognito via one-time link" = "inkognitó egy egyszer használható meghívón keresztül"; + +/* notification */ +"Incoming audio call" = "Bejövő hanghívás"; + +/* notification */ +"Incoming call" = "Bejövő hívás"; + +/* notification */ +"Incoming video call" = "Bejövő videóhívás"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Nem kompatibilis adatbázis-verzió"; + +/* No comment provided by engineer. */ +"Incompatible version" = "Nem kompatibilis verzió"; + +/* PIN entry */ +"Incorrect passcode" = "Téves jelkód"; + +/* No comment provided by engineer. */ +"Incorrect security code!" = "Helytelen biztonsági kód!"; + +/* connection level description */ +"indirect (%d)" = "közvetett (%d)"; + +/* chat item action */ +"Info" = "Információ"; + +/* No comment provided by engineer. */ +"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)"; + +/* No comment provided by engineer. */ +"Instant" = "Azonnali"; + +/* No comment provided by engineer. */ +"Instant push notifications will be hidden!\n" = "Az azonnali push-értesítések el lesznek rejtve!\n"; + +/* No comment provided by engineer. */ +"Interface" = "Kezelőfelület"; + +/* No comment provided by engineer. */ +"Interface colors" = "Kezelőfelület színei"; + +/* token status text */ +"Invalid" = "Érvénytelen"; + +/* token status text */ +"Invalid (bad token)" = "Érvénytelen (hibás token)"; + +/* token status text */ +"Invalid (expired)" = "Érvénytelen (lejárt)"; + +/* token status text */ +"Invalid (unregistered)" = "Érvénytelen (nincs regisztrálva)"; + +/* token status text */ +"Invalid (wrong topic)" = "Érvénytelen (rossz topic)"; + +/* invalid chat data */ +"invalid chat" = "érvénytelen csevegés"; + +/* No comment provided by engineer. */ +"invalid chat data" = "érvénytelen csevegésadat"; + +/* No comment provided by engineer. */ +"Invalid connection link" = "Érvénytelen kapcsolattartási hivatkozás"; + +/* invalid chat item */ +"invalid data" = "érvénytelen adat"; + +/* No comment provided by engineer. */ +"Invalid display name!" = "Érvénytelen megjelenítendő név!"; + +/* No comment provided by engineer. */ +"Invalid link" = "Érvénytelen hivatkozás"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Érvénytelen átköltöztetési visszaigazolás"; + +/* No comment provided by engineer. */ +"Invalid name!" = "Érvénytelen név!"; + +/* No comment provided by engineer. */ +"Invalid QR code" = "Érvénytelen QR-kód"; + +/* No comment provided by engineer. */ +"Invalid response" = "Érvénytelen válasz"; + +/* alert title */ +"Invalid server address!" = "Érvénytelen kiszolgálócím!"; + +/* item status text */ +"Invalid status" = "Érvénytelen állapot"; + +/* No comment provided by engineer. */ +"Invitation expired!" = "A meghívó lejárt!"; + +/* group name */ +"invitation to group %@" = "meghívás a(z) %@ csoportba"; + +/* No comment provided by engineer. */ +"invite" = "meghívás"; + +/* No comment provided by engineer. */ +"Invite friends" = "Barátok meghívása"; + +/* No comment provided by engineer. */ +"Invite members" = "Tagok meghívása"; + +/* No comment provided by engineer. */ +"Invite to chat" = "Meghívás a csevegésbe"; + +/* No comment provided by engineer. */ +"Invite to group" = "Meghívás a csoportba"; + +/* No comment provided by engineer. */ +"invited" = "meghíva"; + +/* rcv group event chat item */ +"invited %@" = "meghívta őt: %@"; + +/* chat list item title */ +"invited to connect" = "Függőben lévő meghívó"; + +/* rcv group event chat item */ +"invited via your group link" = "meghíva a saját csoporthivatkozásán keresztül"; + +/* No comment provided by engineer. */ +"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "Az iOS kulcstartó a jelmondat biztonságos tárolására szolgál – lehetővé teszi a push-értesítések fogadását."; + +/* 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." = "Az iOS kulcstartó biztonságosan fogja tárolni a jelmondatot az alkalmazás újraindítása, vagy a jelmondat módosítása után – lehetővé teszi a push-értesítések fogadását."; + +/* No comment provided by engineer. */ +"IP address" = "IP-cím"; + +/* No comment provided by engineer. */ +"Irreversible message deletion" = "Végleges üzenettörlés"; + +/* No comment provided by engineer. */ +"Irreversible message deletion is prohibited in this chat." = "Az üzenetek végleges törlése le van tiltva ebben a csevegésben."; + +/* No comment provided by engineer. */ +"Irreversible message deletion is prohibited." = "Az üzenetek végleges törlése le van tiltva."; + +/* No comment provided by engineer. */ +"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Lehetővé teszi, hogy egyetlen csevegési profilon belül több névtelen kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük."; + +/* No comment provided by engineer. */ +"It can happen when you or your connection used the old database backup." = "Ez akkor fordulhat elő, ha Ön vagy a partnere régi adatbázis biztonsági mentést használt."; + +/* No comment provided by engineer. */ +"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Ez akkor fordulhat elő, ha:\n1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak.\n2. Nem sikerült az üzenetet visszafejteni, mert Ön, vagy a partnere régebbi adatbázis biztonsági mentést használt.\n3. A kapcsolat sérült."; + +/* No comment provided by engineer. */ +"It protects your IP address and connections." = "Védi az IP-címét és a kapcsolatait."; + +/* 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 (%@)." = "Úgy tűnik, már kapcsolódott ezen a hivatkozáson keresztül. Ha ez nem így van, akkor hiba történt (%@)."; + +/* No comment provided by engineer. */ +"Italian interface" = "Olasz kezelőfelület"; + +/* No comment provided by engineer. */ +"italic" = "dőlt"; + +/* No comment provided by engineer. */ +"Japanese interface" = "Japán kezelőfelület"; + +/* swipe action */ +"Join" = "Csatlakozás"; + +/* No comment provided by engineer. */ +"join as %@" = "csatlakozás mint %@"; + +/* No comment provided by engineer. */ +"Join group" = "Csatlakozás csoporthoz"; + +/* No comment provided by engineer. */ +"Join group conversations" = "Csatlakozás csoportos beszélgetésekhez"; + +/* No comment provided by engineer. */ +"Join group?" = "Csatlakozik a csoporthoz?"; + +/* No comment provided by engineer. */ +"Join incognito" = "Csatlakozás inkognitóban"; + +/* No comment provided by engineer. */ +"Join with current profile" = "Csatlakozás a jelenlegi profillal"; + +/* No comment provided by engineer. */ +"Join your group?\nThis is your link for group %@!" = "Csatlakozik a csoportjához?\nEz a saját hivatkozása a(z) %@ nevű csoporthoz!"; + +/* No comment provided by engineer. */ +"Joining group" = "Csatlakozás a csoporthoz"; + +/* alert action */ +"Keep" = "Megtartás"; + +/* No comment provided by engineer. */ +"Keep conversation" = "Beszélgetés megtartása"; + +/* No comment provided by engineer. */ +"Keep the app open to use it from desktop" = "A számítógépről való használathoz tartsd nyitva az alkalmazást"; + +/* alert title */ +"Keep unused invitation?" = "Megtartja a fel nem használt meghívót?"; + +/* No comment provided by engineer. */ +"Keep your connections" = "Kapcsolatok megtartása"; + +/* No comment provided by engineer. */ +"Keychain error" = "Kulcstartóhiba"; + +/* No comment provided by engineer. */ +"KeyChain error" = "Kulcstartóhiba"; + +/* No comment provided by engineer. */ +"Large file!" = "Nagy fájl!"; + +/* No comment provided by engineer. */ +"Learn more" = "Tudjon meg többet"; + +/* swipe action */ +"Leave" = "Elhagyás"; + +/* No comment provided by engineer. */ +"Leave chat" = "Csevegés elhagyása"; + +/* No comment provided by engineer. */ +"Leave chat?" = "Elhagyja a csevegést?"; + +/* No comment provided by engineer. */ +"Leave group" = "Csoport elhagyása"; + +/* No comment provided by engineer. */ +"Leave group?" = "Elhagyja a csoportot?"; + +/* rcv group event chat item */ +"left" = "elhagyta a csoportot"; + +/* email subject */ +"Let's talk in SimpleX Chat" = "Beszélgessünk a SimpleX Chatben"; + +/* No comment provided by engineer. */ +"Light" = "Világos"; + +/* No comment provided by engineer. */ +"Limitations" = "Korlátozások"; + +/* No comment provided by engineer. */ +"Link mobile and desktop apps! 🔗" = "Társítsa össze a hordozható eszköz- és számítógépes alkalmazásokat! 🔗"; + +/* No comment provided by engineer. */ +"Linked desktop options" = "Társított számítógép beállítások"; + +/* No comment provided by engineer. */ +"Linked desktops" = "Társított számítógépek"; + +/* swipe action */ +"List" = "Lista"; + +/* No comment provided by engineer. */ +"List name and emoji should be different for all lists." = "Az összes lista nevének és emodzsijának különbözőnek kell lennie."; + +/* No comment provided by engineer. */ +"List name..." = "Lista neve…"; + +/* No comment provided by engineer. */ +"LIVE" = "ÉLŐ"; + +/* No comment provided by engineer. */ +"Live message!" = "Élő üzenet!"; + +/* No comment provided by engineer. */ +"Live messages" = "Élő üzenetek"; + +/* No comment provided by engineer. */ +"Local name" = "Helyi név"; + +/* No comment provided by engineer. */ +"Local profile data only" = "Csak helyi profiladatok"; + +/* No comment provided by engineer. */ +"Lock after" = "Zárolás miután"; + +/* No comment provided by engineer. */ +"Lock mode" = "Zárolási mód"; + +/* No comment provided by engineer. */ +"Make one message disappear" = "Egy üzenet eltüntetése"; + +/* No comment provided by engineer. */ +"Make profile private!" = "Tegye priváttá a profilját!"; + +/* No comment provided by engineer. */ +"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Győződjön meg arról, hogy a megadott WebRTC ICE-kiszolgálók címei megfelelő formátumúak, soronként elkülönítettek, és nincsenek duplikálva."; + +/* No comment provided by engineer. */ +"Mark deleted for everyone" = "Jelölje meg az összes tag számára töröltként"; + +/* No comment provided by engineer. */ +"Mark read" = "Megjelölés olvasottként"; + +/* No comment provided by engineer. */ +"Mark verified" = "Hitelesítés"; + +/* No comment provided by engineer. */ +"Markdown in messages" = "Markdown az üzenetekben"; + +/* marked deleted chat item preview text */ +"marked deleted" = "törlésre jelölve"; + +/* No comment provided by engineer. */ +"Max 30 seconds, received instantly." = "Max. 30 másodperc, azonnal érkezett."; + +/* No comment provided by engineer. */ +"Media & file servers" = "Média- és fájlkiszolgálók"; + +/* blur media */ +"Medium" = "Közepes"; + +/* member role */ +"member" = "tag"; + +/* No comment provided by engineer. */ +"Member" = "Tag"; + +/* profile update event chat item */ +"member %@ changed to %@" = "%1$@ a következőre módosította a nevét: %2$@"; + +/* rcv group event chat item */ +"member connected" = "kapcsolódott"; + +/* item status text */ +"Member inactive" = "Inaktív tag"; + +/* chat feature */ +"Member reports" = "Tagok jelentései"; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All chat members will be notified." = "A tag szerepköre a következőre fog módosulni: „%@”. A csevegés összes tagja értesítést fog kapni."; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All group members will be notified." = "A tag szerepköre a következőre fog módosulni: „%@”. A csoport az összes tagja értesítést fog kapni."; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". The member will receive a new invitation." = "A tag szerepköre a következőre fog módosulni: „%@”. A tag új meghívást fog kapni."; + +/* No comment provided by engineer. */ +"Member will be removed from chat - this cannot be undone!" = "A tag el lesz távolítva a csevegésből – ez a művelet nem vonható vissza!"; + +/* No comment provided by engineer. */ +"Member will be removed from group - this cannot be undone!" = "A tag el lesz távolítva a csoportból – ez a művelet nem vonható vissza!"; + +/* 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 irreversibly delete sent messages. (24 hours)" = "A tagok véglegesen törölhetik az elküldött üzeneteiket. (24 óra)"; + +/* No comment provided by engineer. */ +"Members can report messsages to moderators." = "A tagok jelenthetik az üzeneteket a moderátorok felé."; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "A tagok küldhetnek egymásnak közvetlen üzeneteket."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "A tagok küldhetnek eltűnő üzeneteket."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "A tagok küldhetnek fájlokat és médiatartalmakat."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "A tagok küldhetnek SimpleX-hivatkozásokat."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "A tagok küldhetnek hangüzeneteket."; + +/* No comment provided by engineer. */ +"Mention members 👋" = "Tagok említése 👋"; + +/* No comment provided by engineer. */ +"Menus" = "Menük"; + +/* No comment provided by engineer. */ +"message" = "üzenet"; + +/* item status text */ +"Message delivery error" = "Üzenetkézbesítési hiba"; + +/* No comment provided by engineer. */ +"Message delivery receipts!" = "Üzenetkézbesítési jelentések!"; + +/* item status text */ +"Message delivery warning" = "Üzenetkézbesítési figyelmeztetés"; + +/* No comment provided by engineer. */ +"Message draft" = "Üzenetvázlat"; + +/* item status text */ +"Message forwarded" = "Továbbított üzenet"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Az üzenet később is kézbesíthető, ha a tag aktívvá válik."; + +/* No comment provided by engineer. */ +"Message queue info" = "Üzenetsorbaállítási információ"; + +/* chat feature */ +"Message reactions" = "Üzenetreakciók"; + +/* No comment provided by engineer. */ +"Message reactions are prohibited in this chat." = "A reakciók hozzáadása az üzenetekhez le van tiltva ebben a csevegésben."; + +/* No comment provided by engineer. */ +"Message reactions are prohibited." = "A reakciók hozzáadása az üzenetekhez le van tiltva."; + +/* notification */ +"message received" = "üzenet érkezett"; + +/* No comment provided by engineer. */ +"Message reception" = "Üzenetjelentés"; + +/* No comment provided by engineer. */ +"Message servers" = "Üzenetkiszolgálók"; + +/* No comment provided by engineer. */ +"Message shape" = "Üzenetbuborék alakja"; + +/* No comment provided by engineer. */ +"Message source remains private." = "Az üzenet forrása titokban marad."; + +/* No comment provided by engineer. */ +"Message status" = "Üzenet állapota"; + +/* copied message info */ +"Message status: %@" = "Üzenet állapota: %@"; + +/* No comment provided by engineer. */ +"Message text" = "Név és üzenet"; + +/* No comment provided by engineer. */ +"Message too large" = "Az üzenet túl nagy"; + +/* No comment provided by engineer. */ +"Messages" = "Üzenetek"; + +/* No comment provided by engineer. */ +"Messages & files" = "Üzenetek és fájlok"; + +/* No comment provided by engineer. */ +"Messages from %@ will be shown!" = "%@ összes üzenete meg fog jelenni!"; + +/* alert message */ +"Messages in this chat will never be deleted." = "Az ebben a csevegésben lévő üzenetek soha nem lesznek törölve."; + +/* No comment provided by engineer. */ +"Messages received" = "Fogadott üzenetek"; + +/* No comment provided by engineer. */ +"Messages sent" = "Elküldött üzenetek"; + +/* alert message */ +"Messages were deleted after you selected them." = "Az üzeneteket törölték miután kijelölte ő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**, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve."; + +/* 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." = "Az üzenetek, a fájlok és a hívások **végpontok közötti kvantumbiztos titkosítással**, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve."; + +/* 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"; + +/* No comment provided by engineer. */ +"Migrate to another device" = "Átköltöztetés egy másik eszközre"; + +/* No comment provided by engineer. */ +"Migrate to another device via QR code." = "Átköltöztetés egy másik eszközre QR-kód használatával."; + +/* No comment provided by engineer. */ +"Migrating" = "Átköltöztetés"; + +/* No comment provided by engineer. */ +"Migrating database archive…" = "Adatbázis-archívum átköltöztetése…"; + +/* No comment provided by engineer. */ +"Migration complete" = "Átköltöztetés befejezve"; + +/* No comment provided by engineer. */ +"Migration error:" = "Átköltöztetési hiba:"; + +/* No comment provided by engineer. */ +"Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." = "Sikertelen átköltöztetés. Koppintson a **Kihagyás** lehetőségre a jelenlegi adatbázis használatának folytatásához. Jelentse a problémát az alkalmazás fejlesztőinek csevegésben vagy e-mailben [chat@simplex.chat](mailto:chat@simplex.chat)."; + +/* No comment provided by engineer. */ +"Migration is completed" = "Az átköltöztetés befejeződött"; + +/* No comment provided by engineer. */ +"Migrations:" = "Átköltöztetések:"; + +/* time unit */ +"minutes" = "perc"; + +/* call status */ +"missed call" = "nem fogadott hívás"; + +/* chat item action */ +"Moderate" = "Moderálás"; + +/* moderated chat item */ +"moderated" = "moderált"; + +/* No comment provided by engineer. */ +"Moderated at" = "Moderálva"; + +/* copied message info */ +"Moderated at: %@" = "Moderálva: %@"; + +/* marked deleted chat item preview text */ +"moderated by %@" = "moderálva lett %@ által"; + +/* member role */ +"moderator" = "moderátor"; + +/* time unit */ +"months" = "hónap"; + +/* swipe action */ +"More" = "Továbbiak"; + +/* No comment provided by engineer. */ +"More improvements are coming soon!" = "Hamarosan további fejlesztések érkeznek!"; + +/* No comment provided by engineer. */ +"More reliable network connection." = "Megbízhatóbb hálózati kapcsolat."; + +/* No comment provided by engineer. */ +"More reliable notifications" = "Megbízhatóbb értesítések"; + +/* item status description */ +"Most likely this connection is deleted." = "Valószínűleg ez a kapcsolat törölve lett."; + +/* No comment provided by engineer. */ +"Multiple chat profiles" = "Több csevegési profil"; + +/* notification label action */ +"Mute" = "Némítás"; + +/* notification label action */ +"Mute all" = "Összes némítása"; + +/* No comment provided by engineer. */ +"Muted when inactive!" = "Némítás, ha inaktív!"; + +/* swipe action */ +"Name" = "Név"; + +/* No comment provided by engineer. */ +"Network & servers" = "Hálózat és kiszolgálók"; + +/* No comment provided by engineer. */ +"Network connection" = "Hálózati kapcsolat"; + +/* No comment provided by engineer. */ +"Network decentralization" = "Hálózati decentralizáció"; + +/* 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."; + +/* No comment provided by engineer. */ +"Network management" = "Hálózatkezelés"; + +/* No comment provided by engineer. */ +"Network operator" = "Hálózatüzemeltető"; + +/* No comment provided by engineer. */ +"Network settings" = "Hálózati beállítások"; + +/* No comment provided by engineer. */ +"Network status" = "Hálózat állapota"; + +/* delete after time */ +"never" = "soha"; + +/* token status text */ +"New" = "Új"; + +/* No comment provided by engineer. */ +"New chat" = "Új csevegés"; + +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Új csevegési élmény 🎉"; + +/* notification */ +"New contact request" = "Új meghívási kérés"; + +/* notification */ +"New contact:" = "Új kapcsolat:"; + +/* No comment provided by engineer. */ +"New desktop app!" = "Új számítógép-alkalmazás!"; + +/* No comment provided by engineer. */ +"New display name" = "Új megjelenítendő név"; + +/* notification */ +"New events" = "Új események"; + +/* No comment provided by engineer. */ +"New in %@" = "Újdonságok a(z) %@ verzióban"; + +/* No comment provided by engineer. */ +"New media options" = "Új médiabeállítások"; + +/* No comment provided by engineer. */ +"New member role" = "Új tag szerepköre"; + +/* notification */ +"new message" = "új üzenet"; + +/* notification */ +"New message" = "Új üzenet"; + +/* No comment provided by engineer. */ +"New Passcode" = "Új jelkód"; + +/* No comment provided by engineer. */ +"New passphrase…" = "Új jelmondat…"; + +/* No comment provided by engineer. */ +"New server" = "Új kiszolgáló"; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used every time you start the app." = "Minden alkalommal, amikor elindítja az alkalmazást, új SOCKS-hitelesítő-adatokat fog használni."; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used for each server." = "Az összes kiszolgálóhoz új, SOCKS-hitelesítő-adatok legyenek használva."; + +/* pref value */ +"no" = "nem"; + +/* No comment provided by engineer. */ +"No" = "Nem"; + +/* Authentication unavailable */ +"No app password" = "Nincs alkalmazás jelszó"; + +/* No comment provided by engineer. */ +"No chats" = "Nincsenek csevegések"; + +/* No comment provided by engineer. */ +"No chats found" = "Nem találhatók csevegések"; + +/* No comment provided by engineer. */ +"No chats in list %@" = "Nincsenek csevegések a(z) %@ nevű listában"; + +/* No comment provided by engineer. */ +"No contacts selected" = "Nincs partner kijelölve"; + +/* No comment provided by engineer. */ +"No contacts to add" = "Nincs hozzáadandó partner"; + +/* No comment provided by engineer. */ +"No delivery information" = "Nincs kézbesítési információ"; + +/* No comment provided by engineer. */ +"No device token!" = "Nincs készüléktoken!"; + +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Még nincs közvetlen kapcsolat, az üzenetet az adminisztrátor továbbítja."; + +/* No comment provided by engineer. */ +"no e2e encryption" = "nincs e2e titkosítás"; + +/* No comment provided by engineer. */ +"No filtered chats" = "Nincsenek szűrt csevegések"; + +/* No comment provided by engineer. */ +"No group!" = "Csoport nem található!"; + +/* No comment provided by engineer. */ +"No history" = "Nincsenek előzmények"; + +/* No comment provided by engineer. */ +"No info, try to reload" = "Nincs információ, próbálja meg újratölteni"; + +/* servers error */ +"No media & file servers." = "Nincsenek média- és fájlkiszolgálók."; + +/* No comment provided by engineer. */ +"No message" = "Nincs üzenet"; + +/* servers error */ +"No message servers." = "Nincsenek üzenet-kiszolgálók."; + +/* No comment provided by engineer. */ +"No network connection" = "Nincs hálózati kapcsolat"; + +/* No comment provided by engineer. */ +"No permission to record speech" = "Nincs jogosultság megadva a beszéd rögzítéséhez"; + +/* No comment provided by engineer. */ +"No permission to record video" = "Nincs jogosultság megadva a videó rögzítéséhez"; + +/* No comment provided by engineer. */ +"No permission to record voice message" = "Nincs engedély a hangüzenet rögzítésére"; + +/* No comment provided by engineer. */ +"No push server" = "Helyi"; + +/* No comment provided by engineer. */ +"No received or sent files" = "Nincsenek fogadott vagy küldött fájlok"; + +/* servers error */ +"No servers for private message routing." = "Nincsenek kiszolgálók a privát üzenet-útválasztáshoz."; + +/* servers error */ +"No servers to receive files." = "Nincsenek fájlfogadási kiszolgálók."; + +/* servers error */ +"No servers to receive messages." = "Nincsenek üzenetfogadási kiszolgálók."; + +/* servers error */ +"No servers to send files." = "Nincsenek fájlküldő-kiszolgálók."; + +/* copied message info in history */ +"no text" = "nincs szöveg"; + +/* alert title */ +"No token!" = "Nincs token!"; + +/* No comment provided by engineer. */ +"No unread chats" = "Nincsenek olvasatlan csevegések"; + +/* No comment provided by engineer. */ +"No user identifiers." = "Nincsenek felhasználó-azonosítók."; + +/* No comment provided by engineer. */ +"Not compatible!" = "Nem kompatibilis!"; + +/* No comment provided by engineer. */ +"Notes" = "Jegyzetek"; + +/* No comment provided by engineer. */ +"Nothing selected" = "Nincs semmi kijelölve"; + +/* alert title */ +"Nothing to forward!" = "Nincs mit továbbítani!"; + +/* No comment provided by engineer. */ +"Notifications" = "Értesítések"; + +/* No comment provided by engineer. */ +"Notifications are disabled!" = "Az értesítések le vannak tiltva!"; + +/* alert title */ +"Notifications error" = "Értesítési hiba"; + +/* No comment provided by engineer. */ +"Notifications privacy" = "Értesítési adatvédelem"; + +/* alert title */ +"Notifications status" = "Értesítések állapota"; + +/* No comment provided by engineer. */ +"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Most már az adminisztrátorok is:\n- törölhetik a tagok üzeneteit.\n- letilthatnak tagokat („megfigyelő” szerepkör)"; + +/* member role */ +"observer" = "megfigyelő"; + +/* enabled status +group pref value +time to disappear */ +"off" = "kikapcsolva"; + +/* blur media */ +"Off" = "Kikapcsolva"; + +/* feature offered item */ +"offered %@" = "%@ ajánlotta"; + +/* feature offered item */ +"offered %@: %@" = "ajánlotta: %1$@, ekkor: %2$@"; + +/* alert button */ +"Ok" = "Rendben"; + +/* No comment provided by engineer. */ +"OK" = "Rendben"; + +/* No comment provided by engineer. */ +"Old database" = "Régi adatbázis"; + +/* group pref value */ +"on" = "bekapcsolva"; + +/* No comment provided by engineer. */ +"One-time invitation 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."; + +/* No comment provided by engineer. */ +"Onion hosts will be used when available.\nRequires compatible VPN." = "Onion-kiszolgálók használata, ha azok rendelkezésre állnak.\nVPN engedélyezése szükséges."; + +/* 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 chat owners can change preferences." = "Csak a csevegés tulajdonosai módosíthatják a csevegési beállításokat."; + +/* No comment provided by engineer. */ +"Only client devices store user profiles, contacts, groups, and messages." = "A felhasználói profilok, partnerek, csoportok és üzenetek csak az eszközön vannak tárolva a kliensen belül."; + +/* No comment provided by engineer. */ +"Only delete conversation" = "Csak a beszélgetés törlése"; + +/* No comment provided by engineer. */ +"Only group owners can change group preferences." = "Csak a csoport tulajdonosai módosíthatják a csoportbeállításokat."; + +/* No comment provided by engineer. */ +"Only group owners can enable files and media." = "Csak a csoport tulajdonosai engedélyezhetik a fájlok- és a médiatartalmak küldését."; + +/* No comment provided by engineer. */ +"Only group owners can enable voice messages." = "Csak a csoport tulajdonosai engedélyezhetik a hangüzenetek küldését."; + +/* No comment provided by engineer. */ +"Only sender and moderators see it" = "Csak a küldő és a moderátorok látják"; + +/* No comment provided by engineer. */ +"Only you and moderators see it" = "Csak Ön és a moderátorok látják"; + +/* No comment provided by engineer. */ +"Only you can add message reactions." = "Csak Ön adhat hozzá reakciókat az üzenetekhez."; + +/* No comment provided by engineer. */ +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Véglegesen csak Ön törölhet üzeneteket (partnere csak törlésre jelölheti meg őket ). (24 óra)"; + +/* No comment provided by engineer. */ +"Only you can make calls." = "Csak Ön tud hívásokat indítani."; + +/* No comment provided by engineer. */ +"Only you can send disappearing messages." = "Csak Ön tud eltűnő üzeneteket küldeni."; + +/* No comment provided by engineer. */ +"Only you can send voice messages." = "Csak Ön tud hangüzeneteket küldeni."; + +/* No comment provided by engineer. */ +"Only your contact can add message reactions." = "Csak a partnere adhat hozzá reakciókat az üzenetekhez."; + +/* No comment provided by engineer. */ +"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Csak a partnere tudja az üzeneteket véglegesen törölni (Ön csak törlésre jelölheti meg azokat). (24 óra)"; + +/* No comment provided by engineer. */ +"Only your contact can make calls." = "Csak a partnere tud hívást indítani."; + +/* No comment provided by engineer. */ +"Only your contact can send disappearing messages." = "Csak a partnere tud eltűnő üzeneteket küldeni."; + +/* No comment provided by engineer. */ +"Only your contact can send voice messages." = "Csak a partnere tud hangüzeneteket küldeni."; + +/* alert action */ +"Open" = "Megnyitás"; + +/* No comment provided by engineer. */ +"Open changes" = "Módosítások megtekintése"; + +/* No comment provided by engineer. */ +"Open chat" = "Csevegés megnyitása"; + +/* authentication reason */ +"Open chat console" = "Csevegési konzol megnyitása"; + +/* No comment provided by engineer. */ +"Open conditions" = "Feltételek megnyitása"; + +/* No comment provided by engineer. */ +"Open group" = "Csoport megnyitása"; + +/* authentication reason */ +"Open migration to another device" = "Átköltöztetés indítása egy másik eszközre"; + +/* No comment provided by engineer. */ +"Open Settings" = "Beállítások megnyitása"; + +/* No comment provided by engineer. */ +"Opening app…" = "Az alkalmazás megnyitása…"; + +/* No comment provided by engineer. */ +"Operator" = "Üzemeltető"; + +/* alert title */ +"Operator server" = "Kiszolgáló-üzemeltető"; + +/* No comment provided by engineer. */ +"Or import archive file" = "Vagy archívumfájl importálása"; + +/* No comment provided by engineer. */ +"Or paste archive link" = "Vagy az archívum hivatkozásának beillesztése"; + +/* No comment provided by engineer. */ +"Or scan QR code" = "Vagy QR-kód beolvasása"; + +/* 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 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. */ +"Organize chats into lists" = "Csevegések listákba szervezése"; + +/* No comment provided by engineer. */ +"other" = "egyéb"; + +/* No comment provided by engineer. */ +"Other" = "További"; + +/* No comment provided by engineer. */ +"other errors" = "egyéb hibák"; + +/* alert message */ +"Other file errors:\n%@" = "Egyéb fájlhiba:\n%@"; + +/* member role */ +"owner" = "tulajdonos"; + +/* feature role */ +"owners" = "tulajdonosok"; + +/* No comment provided by engineer. */ +"Passcode" = "Jelkód"; + +/* No comment provided by engineer. */ +"Passcode changed!" = "A jelkód módosult!"; + +/* No comment provided by engineer. */ +"Passcode entry" = "Jelkód bevitele"; + +/* No comment provided by engineer. */ +"Passcode not changed!" = "A jelkód nem módosult!"; + +/* No comment provided by engineer. */ +"Passcode set!" = "A jelkód beállítva!"; + +/* No comment provided by engineer. */ +"Password" = "Jelszó"; + +/* No comment provided by engineer. */ +"Password to show" = "Jelszó a megjelenítéshez"; + +/* past/unknown group member */ +"Past member %@" = "(Már nem tag) %@"; + +/* No comment provided by engineer. */ +"Paste desktop address" = "Számítógép címének beillesztése"; + +/* No comment provided by engineer. */ +"Paste image" = "Kép beillesztése"; + +/* No comment provided by engineer. */ +"Paste link to connect!" = "Hivatkozás beillesztése a kapcsolódáshoz!"; + +/* No comment provided by engineer. */ +"Paste the link you received" = "Kapott hivatkozás beillesztése"; + +/* No comment provided by engineer. */ +"peer-to-peer" = "egyenrangú"; + +/* No comment provided by engineer. */ +"pending" = "függőben"; + +/* No comment provided by engineer. */ +"Pending" = "Függőben"; + +/* No comment provided by engineer. */ +"pending approval" = "jóváhagyásra vár"; + +/* No comment provided by engineer. */ +"Periodic" = "Időszakos"; + +/* message decrypt error item */ +"Permanent decryption error" = "Végleges visszafejtési hiba"; + +/* No comment provided by engineer. */ +"Picture-in-picture calls" = "Kép a képben hívások"; + +/* No comment provided by engineer. */ +"PING count" = "PING-ek száma"; + +/* No comment provided by engineer. */ +"PING interval" = "Időtartam a PING-ek között"; + +/* No comment provided by engineer. */ +"Play from the chat list." = "Lejátszás a csevegési listából."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Kérje meg a partnerét, hogy engedélyezze a hívásokat."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable sending voice messages." = "Kérje meg a partnerét, hogy engedélyezze a hangüzenetek küldését."; + +/* No comment provided by engineer. */ +"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." = "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."; + +/* No comment provided by engineer. */ +"Please check that you used the correct link or ask your contact to send you another one." = "Ellenőrizze, hogy a megfelelő hivatkozást használta-e, vagy kérje meg a partnerét, hogy küldjön egy másikat."; + +/* No comment provided by engineer. */ +"Please check your network connection with %@ and try again." = "Ellenőrizze a hálózati kapcsolatát a vele: %@, és próbálja újra."; + +/* No comment provided by engineer. */ +"Please check yours and your contact preferences." = "Ellenőrizze a saját- és a partnere beállításait."; + +/* No comment provided by engineer. */ +"Please confirm that network settings are correct for this device." = "Ellenőrizze, hogy a hálózati beállítások megfelelők-e ehhez az eszközhöz."; + +/* No comment provided by engineer. */ +"Please contact developers.\nError: %@" = "Lépjen kapcsolatba a fejlesztőkkel.\nHiba: %@"; + +/* No comment provided by engineer. */ +"Please contact group admin." = "Lépjen kapcsolatba a csoport adminisztrátorával."; + +/* No comment provided by engineer. */ +"Please enter correct current passphrase." = "Adja meg a helyes, jelenlegi jelmondatot."; + +/* No comment provided by engineer. */ +"Please enter the previous password after restoring database backup. This action can not be undone." = "Adja meg a korábbi jelszót az adatbázis biztonsági mentésének visszaállítása után. Ez a művelet nem vonható vissza."; + +/* No comment provided by engineer. */ +"Please remember or store it securely - there is no way to recover a lost passcode!" = "Jegyezze fel vagy tárolja el biztonságosan – az elveszett jelkódot nem lehet visszaállítani!"; + +/* No comment provided by engineer. */ +"Please report it to the developers." = "Jelentse a fejlesztőknek."; + +/* No comment provided by engineer. */ +"Please restart the app and migrate the database to enable push notifications." = "Indítsa újra az alkalmazást az adatbázis-átköltöztetéséhez szükséges push-értesítések engedélyezéséhez."; + +/* No comment provided by engineer. */ +"Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Tárolja el biztonságosan jelmondát, mert ha elveszti azt, akkor NEM férhet hozzá a csevegéshez."; + +/* No comment provided by engineer. */ +"Please store passphrase securely, you will NOT be able to change it if you lose it." = "Tárolja el biztonságosan jelmondatát, mert ha elveszíti azt, NEM tudja módosítani."; + +/* token info */ +"Please try to disable and re-enable notfications." = "Próbálja meg letiltani és újra engedélyezni az értesítéseket."; + +/* token info */ +"Please wait for token activation to complete." = "Várjon, amíg a token aktiválása befejeződik."; + +/* token info */ +"Please wait for token to be registered." = "Várjon a token regisztrálására."; + +/* No comment provided by engineer. */ +"Polish interface" = "Lengyel kezelőfelület"; + +/* No comment provided by engineer. */ +"Port" = "Port"; + +/* server test error */ +"Possibly, certificate fingerprint in server address is incorrect" = "Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen"; + +/* No comment provided by engineer. */ +"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"; + +/* No comment provided by engineer. */ +"Preset servers" = "Előre beállított kiszolgálók"; + +/* No comment provided by engineer. */ +"Preview" = "Előnézet"; + +/* No comment provided by engineer. */ +"Previously connected servers" = "Korábban kapcsolódott kiszolgálók"; + +/* No comment provided by engineer. */ +"Privacy & security" = "Adatvédelem és biztonság"; + +/* No comment provided by engineer. */ +"Privacy for your customers." = "Saját ügyfeleinek adatvédelme."; + +/* No comment provided by engineer. */ +"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"; + +/* 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 szerver üzemeltetői számára."; + +/* No comment provided by engineer. */ +"Private filenames" = "Privát fájlnevek"; + +/* No comment provided by engineer. */ +"Private media file names." = "Privát nevek a médiafájlokhoz."; + +/* No comment provided by engineer. */ +"Private message routing" = "Privát üzenet-útválasztás"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Privát üzenet-útválasztás 🚀"; + +/* name of notes to self */ +"Private notes" = "Privát jegyzetek"; + +/* No comment provided by engineer. */ +"Private routing" = "Privát útválasztás"; + +/* No comment provided by engineer. */ +"Private routing error" = "Privát útválasztási hiba"; + +/* No comment provided by engineer. */ +"Profile and server connections" = "Profil és kiszolgálókapcsolatok"; + +/* No comment provided by engineer. */ +"Profile image" = "Profilkép"; + +/* No comment provided by engineer. */ +"Profile images" = "Profilképek"; + +/* No comment provided by engineer. */ +"Profile password" = "Profiljelszó"; + +/* No comment provided by engineer. */ +"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."; + +/* 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 irreversible message deletion." = "Az elküldött üzenetek végleges törlése le van tiltva."; + +/* No comment provided by engineer. */ +"Prohibit message reactions." = "A reakciók hozzáadása az üzenethez le van tiltva."; + +/* No comment provided by engineer. */ +"Prohibit messages reactions." = "A reakciók hozzáadása az üzenetekhez le van tiltva."; + +/* No comment provided by engineer. */ +"Prohibit reporting messages to moderators." = "Az üzenetek a moderátorok felé történő jelentésének megtiltása."; + +/* 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 disappearing messages." = "Az eltűnő üzenetek küldése le van tiltva."; + +/* No comment provided by engineer. */ +"Prohibit sending files and media." = "A fájlok- és a médiatartalmak küldése le van tiltva."; + +/* No comment provided by engineer. */ +"Prohibit sending SimpleX links." = "A SimpleX-hivatkozások küldése le van tiltva."; + +/* No comment provided by engineer. */ +"Prohibit sending voice messages." = "A hangüzenetek küldése le van tiltva."; + +/* No comment provided by engineer. */ +"Protect app screen" = "Alkalmazás képernyőjének védelme"; + +/* No comment provided by engineer. */ +"Protect IP address" = "IP-cím védelme"; + +/* No comment provided by engineer. */ +"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."; + +/* No comment provided by engineer. */ +"Protocol timeout" = "Protokoll időtúllépése"; + +/* No comment provided by engineer. */ +"Protocol timeout per KB" = "Protokoll időtúllépése kB-onként"; + +/* No comment provided by engineer. */ +"Proxied" = "Proxyzott"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Proxyzott kiszolgálók"; + +/* No comment provided by engineer. */ +"Proxy requires password" = "A proxy jelszót igényel"; + +/* No comment provided by engineer. */ +"Push notifications" = "Push-értesítések"; + +/* No comment provided by engineer. */ +"Push server" = "Push-kiszolgáló"; + +/* chat item text */ +"quantum resistant e2e encryption" = "végpontok közötti kvantumbiztos titkosítás"; + +/* No comment provided by engineer. */ +"Quantum resistant encryption" = "Kvantumbiztos titkosítás"; + +/* No comment provided by engineer. */ +"Rate the app" = "Értékelje az alkalmazást"; + +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Könnyen elérhető eszköztár"; + +/* chat item menu */ +"React…" = "Reagálj…"; + +/* swipe action */ +"Read" = "Olvasott"; + +/* No comment provided by engineer. */ +"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)."; + +/* 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óban](https://github.com/simplex-chat/simplex-chat#readme)."; + +/* No comment provided by engineer. */ +"Receipts are disabled" = "A kézbesítési jelentések le vannak tiltva"; + +/* No comment provided by engineer. */ +"Receive errors" = "Üzenetfogadási hibák"; + +/* No comment provided by engineer. */ +"received answer…" = "válasz fogadása…"; + +/* No comment provided by engineer. */ +"Received at" = "Fogadva"; + +/* copied message info */ +"Received at: %@" = "Fogadva: %@"; + +/* No comment provided by engineer. */ +"received confirmation…" = "visszaigazolás fogadása…"; + +/* notification */ +"Received file event" = "Fogadott fájlesemény"; + +/* message info title */ +"Received message" = "Fogadott üzenetbuborék színe"; + +/* No comment provided by engineer. */ +"Received messages" = "Fogadott üzenetek"; + +/* No comment provided by engineer. */ +"Received reply" = "Fogadott válaszüzenet-buborék színe"; + +/* No comment provided by engineer. */ +"Received total" = "Összes fogadott üzenet"; + +/* No comment provided by engineer. */ +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "A fogadási cím egy másik kiszolgálóra fog módosulni. A cím módosítása a feladó online állapotba kerülése után fejeződik be."; + +/* No comment provided by engineer. */ +"Receiving file will be stopped." = "A fájl fogadása le fog állni."; + +/* No comment provided by engineer. */ +"Receiving via" = "Fogadás a következőn keresztül:"; + +/* No comment provided by engineer. */ +"Recent history and improved [directory bot](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)." = "Legutóbbi előzmények és továbbfejlesztett [könyvtárbot](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)."; + +/* No comment provided by engineer. */ +"Recipient(s) can't see who this message is from." = "A címzett(ek) nem látja(k), hogy kitől származik ez az üzenet."; + +/* No comment provided by engineer. */ +"Recipients see updates as you type them." = "A címzettek a beírás közben látják a szövegváltozásokat."; + +/* No comment provided by engineer. */ +"Reconnect" = "Újrakapcsolódás"; + +/* No comment provided by engineer. */ +"Reconnect all connected servers to force message delivery. It uses additional traffic." = "Az összes kiszolgálóhoz való újrakapcsolódás az üzenetkézbesítési jelentések kikényszerítéséhez. Ez további adatforgalmat használ."; + +/* No comment provided by engineer. */ +"Reconnect all servers" = "Újrakapcsolódás az összes kiszolgálóhoz"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Újrakapcsolódik az összes kiszolgálóhoz?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "A kiszolgálóhoz való újrakapcsolódás az üzenetkézbesítési jelentések kikényszerítéséhez. Ez további adatforgalmat használ."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Újrakapcsolódik a kiszolgálóhoz?"; + +/* No comment provided by engineer. */ +"Reconnect servers?" = "Újrakapcsolódik a kiszolgálókhoz?"; + +/* No comment provided by engineer. */ +"Record updated at" = "Bejegyzés frissítve"; + +/* copied message info */ +"Record updated at: %@" = "Bejegyzés frissítve: %@"; + +/* No comment provided by engineer. */ +"Reduced battery usage" = "Csökkentett akkumulátor-használat"; + +/* No comment provided by engineer. */ +"Register" = "Regisztrálás"; + +/* token info */ +"Register notification token?" = "Regisztrálja az értesítési tokent?"; + +/* token status text */ +"Registered" = "Regisztrálva"; + +/* reject incoming call via notification +swipe action */ +"Reject" = "Elutasítás"; + +/* No comment provided by engineer. */ +"Reject (sender NOT notified)" = "Elutasítás (a feladó NEM kap értesítést)"; + +/* No comment provided by engineer. */ +"Reject contact request" = "Meghívási kérés elutasítása"; + +/* No comment provided by engineer. */ +"rejected" = "elutasítva"; + +/* 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ímet."; + +/* 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 Ön IP-címét, de megfigyelheti a hívás időtartamát."; + +/* No comment provided by engineer. */ +"Remove" = "Eltávolítás"; + +/* No comment provided by engineer. */ +"Remove archive?" = "Eltávolítja az archívumot?"; + +/* No comment provided by engineer. */ +"Remove image" = "Kép eltávolítása"; + +/* No comment provided by engineer. */ +"Remove member" = "Eltávolítás"; + +/* No comment provided by engineer. */ +"Remove member?" = "Eltávolítja a tagot?"; + +/* No comment provided by engineer. */ +"Remove passphrase from keychain?" = "Eltávolítja a jelmondatot a kulcstartóból?"; + +/* No comment provided by engineer. */ +"removed" = "eltávolítva"; + +/* rcv group event chat item */ +"removed %@" = "eltávolította őt: %@"; + +/* profile update event chat item */ +"removed contact address" = "eltávolította a kapcsolattartási címet"; + +/* profile update event chat item */ +"removed profile picture" = "eltávolította a profilképét"; + +/* rcv group event chat item */ +"removed you" = "eltávolította Önt"; + +/* No comment provided by engineer. */ +"Renegotiate" = "Újraegyeztetés"; + +/* No comment provided by engineer. */ +"Renegotiate encryption" = "Titkosítás újraegyeztetése"; + +/* No comment provided by engineer. */ +"Renegotiate encryption?" = "Újraegyezteti a titkosítást?"; + +/* No comment provided by engineer. */ +"Repeat connection request?" = "Megismétli a meghívási kérést?"; + +/* No comment provided by engineer. */ +"Repeat download" = "Letöltés ismét"; + +/* No comment provided by engineer. */ +"Repeat import" = "Importálás ismét"; + +/* No comment provided by engineer. */ +"Repeat join request?" = "Megismétli a meghívási kérést?"; + +/* No comment provided by engineer. */ +"Repeat upload" = "Feltöltés ismét"; + +/* chat item action */ +"Reply" = "Válasz"; + +/* chat item action */ +"Report" = "Jelentés"; + +/* report reason */ +"Report content: only group moderators will see it." = "Tartalom jelentése: csak a csoport moderátorai látják."; + +/* report reason */ +"Report member profile: only group moderators will see it." = "Tag profiljának jelentése: csak a csoport moderátorai látják."; + +/* report reason */ +"Report other: only group moderators will see it." = "Egyéb jelentés: csak a csoport moderátorai látják."; + +/* No comment provided by engineer. */ +"Report reason?" = "Jelentés indoklása?"; + +/* report reason */ +"Report spam: only group moderators will see it." = "Kéretlen tartalom jelentése: csak a csoport moderátorai látják."; + +/* report reason */ +"Report violation: only group moderators will see it." = "Szabálysértés jelentése: csak a csoport moderátorai látják."; + +/* report in notification */ +"Report: %@" = "Jelentés: %@"; + +/* No comment provided by engineer. */ +"Reporting messages to moderators is prohibited." = "Az üzenetek jelentése a moderátorok felé le van tiltva."; + +/* No comment provided by engineer. */ +"Reports" = "Jelentések"; + +/* chat list item title */ +"requested to connect" = "Függőben lévő meghívási kérelem"; + +/* No comment provided by engineer. */ +"Required" = "Szükséges"; + +/* No comment provided by engineer. */ +"Reset" = "Visszaállítás"; + +/* No comment provided by engineer. */ +"Reset all hints" = "Tippek visszaállítása"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Az összes statisztika visszaállítása"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Visszaállítja az összes statisztikát?"; + +/* No comment provided by engineer. */ +"Reset colors" = "Színek visszaállítása"; + +/* No comment provided by engineer. */ +"Reset to app theme" = "Alkalmazás témájának visszaállítása"; + +/* No comment provided by engineer. */ +"Reset to defaults" = "Visszaállítás alapértelmezettre"; + +/* No comment provided by engineer. */ +"Reset to user theme" = "Felhasználó által létrehozott téma visszaállítása"; + +/* No comment provided by engineer. */ +"Restart the app to create a new chat profile" = "Új csevegési profil létrehozásához indítsa újra az alkalmazást"; + +/* No comment provided by engineer. */ +"Restart the app to use imported chat database" = "Az importált csevegési adatbázis használatához indítsa újra az alkalmazást"; + +/* No comment provided by engineer. */ +"Restore" = "Visszaállítás"; + +/* No comment provided by engineer. */ +"Restore database backup" = "Adatbázismentés visszaállítása"; + +/* No comment provided by engineer. */ +"Restore database backup?" = "Visszaállítja az adatbázismentést?"; + +/* No comment provided by engineer. */ +"Restore database error" = "Hiba történt az adatbázis visszaállításakor"; + +/* No comment provided by engineer. */ +"Retry" = "Újrapróbálkozás"; + +/* chat item action */ +"Reveal" = "Felfedés"; + +/* No comment provided by engineer. */ +"Review conditions" = "Feltételek felülvizsgálata"; + +/* No comment provided by engineer. */ +"Revoke" = "Visszavonás"; + +/* cancel file action */ +"Revoke file" = "Fájl visszavonása"; + +/* No comment provided by engineer. */ +"Revoke file?" = "Visszavonja a fájlt?"; + +/* No comment provided by engineer. */ +"Role" = "Szerepkör"; + +/* No comment provided by engineer. */ +"Run chat" = "Csevegési szolgáltatás indítása"; + +/* No comment provided by engineer. */ +"Safely receive files" = "Fájlok biztonságos fogadása"; + +/* No comment provided by engineer. */ +"Safer groups" = "Biztonságosabb csoportok"; + +/* alert button +chat item action */ +"Save" = "Mentés"; + +/* alert button */ +"Save (and notify contacts)" = "Mentés (és a partnerek értesítése)"; + +/* alert button */ +"Save and notify contact" = "Mentés és a partner értesítése"; + +/* 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 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 group profile" = "Csoportprofil mentése"; + +/* No comment provided by engineer. */ +"Save list" = "Lista mentése"; + +/* No comment provided by engineer. */ +"Save passphrase and open chat" = "Jelmondat mentése és a csevegés megnyitása"; + +/* No comment provided by engineer. */ +"Save passphrase in Keychain" = "Jelmondat mentése a kulcstartóba"; + +/* alert title */ +"Save preferences?" = "Menti a beállításokat?"; + +/* No comment provided by engineer. */ +"Save profile password" = "Profiljelszó mentése"; + +/* No comment provided by engineer. */ +"Save servers" = "Kiszolgálók mentése"; + +/* alert title */ +"Save servers?" = "Menti a kiszolgálókat?"; + +/* No comment provided by engineer. */ +"Save welcome message?" = "Menti az üdvözlőüzenetet?"; + +/* alert title */ +"Save your profile?" = "Menti a profilt?"; + +/* No comment provided by engineer. */ +"saved" = "mentett"; + +/* No comment provided by engineer. */ +"Saved" = "Mentett"; + +/* No comment provided by engineer. */ +"Saved from" = "Elmentve innen"; + +/* No comment provided by engineer. */ +"saved from %@" = "elmentve innen: %@"; + +/* message info title */ +"Saved message" = "Mentett üzenet"; + +/* No comment provided by engineer. */ +"Saved WebRTC ICE servers will be removed" = "A mentett WebRTC ICE-kiszolgálók el lesznek távolítva"; + +/* No comment provided by engineer. */ +"Saving %lld messages" = "%lld üzenet mentése"; + +/* No comment provided by engineer. */ +"Scale" = "Méretezés"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Hivatkozás beolvasása / beillesztése"; + +/* No comment provided by engineer. */ +"Scan code" = "Beolvasás"; + +/* No comment provided by engineer. */ +"Scan QR code" = "QR-kód beolvasása"; + +/* No comment provided by engineer. */ +"Scan QR code from desktop" = "QR-kód beolvasása a számítógépről"; + +/* No comment provided by engineer. */ +"Scan security code from your contact's app." = "Biztonsági kód beolvasása a partnere alkalmazásából."; + +/* No comment provided by engineer. */ +"Scan server QR code" = "A kiszolgáló QR-kódjának beolvasása"; + +/* No comment provided by engineer. */ +"search" = "keresés"; + +/* No comment provided by engineer. */ +"Search" = "Keresés"; + +/* No comment provided by engineer. */ +"Search bar accepts invitation links." = "A keresősáv elfogadja a meghívási hivatkozásokat."; + +/* No comment provided by engineer. */ +"Search or paste SimpleX link" = "Keresés vagy SimpleX-hivatkozás beillesztése"; + +/* network option */ +"sec" = "mp"; + +/* No comment provided by engineer. */ +"Secondary" = "Másodlagos szín"; + +/* time unit */ +"seconds" = "másodperc"; + +/* No comment provided by engineer. */ +"secret" = "titok"; + +/* server test step */ +"Secure queue" = "Biztonságos sorba állítás"; + +/* No comment provided by engineer. */ +"Secured" = "Biztosítva"; + +/* No comment provided by engineer. */ +"Security assessment" = "Biztonsági kiértékelés"; + +/* No comment provided by engineer. */ +"Security code" = "Biztonsági kód"; + +/* chat item text */ +"security code changed" = "a biztonsági kód módosult"; + +/* chat item action */ +"Select" = "Kijelölés"; + +/* No comment provided by engineer. */ +"Select chat profile" = "Csevegési profil kijelölése"; + +/* No comment provided by engineer. */ +"Selected %lld" = "%lld kijelölve"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "A kijelölt csevegési beállítások tiltják ezt az üzenetet."; + +/* No comment provided by engineer. */ +"Self-destruct" = "Önmegsemmisítés"; + +/* No comment provided by engineer. */ +"Self-destruct passcode" = "Önmegsemmisítő-jelkód"; + +/* No comment provided by engineer. */ +"Self-destruct passcode changed!" = "Az önmegsemmisítő-jelkód módosult!"; + +/* No comment provided by engineer. */ +"Self-destruct passcode enabled!" = "Az önmegsemmisítő-jelkód engedélyezve!"; + +/* No comment provided by engineer. */ +"Send" = "Küldés"; + +/* No comment provided by engineer. */ +"Send a live message - it will update for the recipient(s) as you type it" = "Élő üzenet küldése – az üzenet a címzett(ek) számára valós időben frissül, ahogy Ön beírja az üzenetet"; + +/* No comment provided by engineer. */ +"Send delivery receipts to" = "A kézbesítési jelentéseket a következő címre kell küldeni"; + +/* No comment provided by engineer. */ +"send direct message" = "közvetlen üzenet küldése"; + +/* No comment provided by engineer. */ +"Send direct message to connect" = "Közvetlen üzenet küldése a kapcsolódáshoz"; + +/* No comment provided by engineer. */ +"Send disappearing message" = "Eltűnő üzenet küldése"; + +/* No comment provided by engineer. */ +"Send errors" = "Üzenetküldési hibák"; + +/* No comment provided by engineer. */ +"Send link previews" = "Hivatkozás előnézete"; + +/* No comment provided by engineer. */ +"Send live message" = "Élő üzenet küldése"; + +/* No comment provided by engineer. */ +"Send message to enable calls." = "Üzenet küldése a hívások engedélyezéséhez."; + +/* No comment provided by engineer. */ +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Közvetlen üzenetküldés, ha az IP-cím védett és a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Közvetlen üzenetküldés, ha a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást."; + +/* No comment provided by engineer. */ +"Send notifications" = "Értesítések küldése"; + +/* No comment provided by engineer. */ +"Send private reports" = "Privát jelentések küldése"; + +/* No comment provided by engineer. */ +"Send questions and ideas" = "Ötletek és javaslatok"; + +/* No comment provided by engineer. */ +"Send receipts" = "Kézbesítési jelentések küldése"; + +/* 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."; + +/* alert message */ +"Sender cancelled file transfer." = "A fájl küldője visszavonta az átvitelt."; + +/* No comment provided by engineer. */ +"Sender may have deleted the connection request." = "A küldője törölhette a meghívási kérést."; + +/* 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."; + +/* No comment provided by engineer. */ +"Sending delivery receipts will be enabled for all contacts." = "A kézbesítési jelentések küldése az összes partnere számára engedélyezve lesz."; + +/* No comment provided by engineer. */ +"Sending file will be stopped." = "A fájl küldése le fog állni."; + +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld contacts" = "A kézbesítési jelentések le vannak tiltva %lld partnernél"; + +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld groups" = "A kézbesítési jelentések le vannak tiltva %lld csoportban"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld contacts" = "A kézbesítési jelentések engedélyezve vannak %lld partnernél"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld groups" = "A kézbesítési jelentések engedélyezve vannak %lld csoportban"; + +/* No comment provided by engineer. */ +"Sending via" = "Küldés a következőn keresztül:"; + +/* No comment provided by engineer. */ +"Sent at" = "Elküldve"; + +/* copied message info */ +"Sent at: %@" = "Elküldve: %@"; + +/* No comment provided by engineer. */ +"Sent directly" = "Közvetlenül küldött"; + +/* notification */ +"Sent file event" = "Elküldött fájlesemény"; + +/* message info title */ +"Sent message" = "Üzenetbuborék színe"; + +/* No comment provided by engineer. */ +"Sent messages" = "Elküldött üzenetek"; + +/* No comment provided by engineer. */ +"Sent messages will be deleted after set time." = "Az elküldött üzenetek törölve lesznek a beállított idő után."; + +/* No comment provided by engineer. */ +"Sent reply" = "Válaszüzenet-buborék színe"; + +/* No comment provided by engineer. */ +"Sent total" = "Összes elküldött üzenet"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Proxyn keresztül küldött"; + +/* No comment provided by engineer. */ +"Server" = "Kiszolgáló"; + +/* alert message */ +"Server added to operator %@." = "Kiszolgáló hozzáadva a következő üzemeltetőhöz: %@."; + +/* No comment provided by engineer. */ +"Server address" = "Kiszolgáló címe"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "A kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "A kiszolgáló címe nem kompatibilis a hálózati beállításokkal."; + +/* alert title */ +"Server operator changed." = "A kiszolgáló üzemeltetője módosult."; + +/* No comment provided by engineer. */ +"Server operators" = "Kiszolgálóüzemeltetők"; + +/* alert title */ +"Server protocol changed." = "A kiszolgáló-protokoll módosult."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "a kiszolgáló sorbaállítási információi: %1$@\n\nutoljára fogadott üzenet: %2$@"; + +/* server test error */ +"Server requires authorization to create queues, check password" = "A kiszolgálónak engedélyre van szüksége a sorba állítás létrehozásához, ellenőrizze a jelszavát"; + +/* server test error */ +"Server requires authorization to upload, check password" = "A kiszolgálónak hitelesítésre van szüksége a feltöltéshez, ellenőrizze jelszavát"; + +/* No comment provided by engineer. */ +"Server test failed!" = "Sikertelen kiszolgáló teszt!"; + +/* No comment provided by engineer. */ +"Server type" = "Kiszolgáló típusa"; + +/* srv error text */ +"Server version is incompatible with network settings." = "A kiszolgáló verziója nem kompatibilis a hálózati beállításokkal."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "A kiszolgáló verziója nem kompatibilis az alkalmazással: %@."; + +/* No comment provided by engineer. */ +"Servers" = "Kiszolgálók"; + +/* No comment provided by engineer. */ +"Servers info" = "Információk a kiszolgálókról"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "A kiszolgálók statisztikái visszaállnak – ez a művelet nem vonható vissza!"; + +/* No comment provided by engineer. */ +"Session code" = "Munkamenet kód"; + +/* No comment provided by engineer. */ +"Set 1 day" = "Beállítva 1 nap"; + +/* No comment provided by engineer. */ +"Set chat name…" = "Csevegés nevének beállítása…"; + +/* No comment provided by engineer. */ +"Set contact name…" = "Partner nevének beállítása…"; + +/* No comment provided by engineer. */ +"Set default theme" = "Alapértelmezett téma beállítása"; + +/* No comment provided by engineer. */ +"Set group preferences" = "Csoportbeállítások megadása"; + +/* No comment provided by engineer. */ +"Set it instead of system authentication." = "Beállítás a rendszer-hitelesítés helyett."; + +/* No comment provided by engineer. */ +"Set message expiration in chats." = "Üzenetek eltűnési idejének módosítása a csevegésekben."; + +/* profile update event chat item */ +"set new contact address" = "új kapcsolattartási címet állított be"; + +/* profile update event chat item */ +"set new profile picture" = "új profilképet állított be"; + +/* No comment provided by engineer. */ +"Set passcode" = "Jelkód beállítása"; + +/* No comment provided by engineer. */ +"Set passphrase" = "Jelmondat beállítása"; + +/* No comment provided by engineer. */ +"Set passphrase to export" = "Jelmondat beállítása az exportáláshoz"; + +/* No comment provided by engineer. */ +"Set the message shown to new members!" = "Megjelenítendő üzenet beállítása az új tagok számára!"; + +/* No comment provided by engineer. */ +"Set timeouts for proxy/VPN" = "Időtúllépések beállítása a proxy/VPN számára"; + +/* No comment provided by engineer. */ +"Settings" = "Beállítások"; + +/* alert message */ +"Settings were changed." = "A beállítások módosultak."; + +/* No comment provided by engineer. */ +"Shape profile images" = "Profilkép alakzata"; + +/* alert action +chat item action */ +"Share" = "Megosztás"; + +/* No comment provided by engineer. */ +"Share 1-time link" = "Egyszer használható meghívó megosztása"; + +/* No comment provided by engineer. */ +"Share 1-time link with a friend" = "Egyszer használható meghívó megosztása egy baráttal"; + +/* No comment provided by engineer. */ +"Share address" = "Cím megosztása"; + +/* No comment provided by engineer. */ +"Share address publicly" = "Cím nyilvános megosztása"; + +/* alert title */ +"Share address with contacts?" = "Megosztja a címet a partnereivel?"; + +/* No comment provided by engineer. */ +"Share from other apps." = "Megosztás más alkalmazásokból."; + +/* No comment provided by engineer. */ +"Share link" = "Megosztás"; + +/* No comment provided by engineer. */ +"Share profile" = "Profil 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."; + +/* No comment provided by engineer. */ +"Share this 1-time invite link" = "Ennek az egyszer használható meghívónak a megosztása"; + +/* No comment provided by engineer. */ +"Share to SimpleX" = "Megosztás a SimpleXben"; + +/* No comment provided by engineer. */ +"Share with contacts" = "Megosztás a partnerekkel"; + +/* No comment provided by engineer. */ +"Short link" = "Rövid hivatkozás"; + +/* No comment provided by engineer. */ +"Show → on messages sent via private routing." = "Egy „→” jel megjelenítése a privát útválasztáson keresztül küldött üzeneteknél."; + +/* No comment provided by engineer. */ +"Show calls in phone history" = "Hívások megjelenítése a híváslistában"; + +/* No comment provided by engineer. */ +"Show developer options" = "Fejlesztői beállítások megjelenítése"; + +/* No comment provided by engineer. */ +"Show last messages" = "Legutóbbi üzenet előnézetének megjelenítése"; + +/* No comment provided by engineer. */ +"Show message status" = "Üzenet állapotának megjelenítése"; + +/* No comment provided by engineer. */ +"Show percentage" = "Százalék megjelenítése"; + +/* No comment provided by engineer. */ +"Show preview" = "Értesítés előnézete"; + +/* No comment provided by engineer. */ +"Show QR code" = "QR-kód megjelenítése"; + +/* No comment provided by engineer. */ +"Show:" = "Megjelenítve:"; + +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX address" = "SimpleX-cím"; + +/* No comment provided by engineer. */ +"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."; + +/* No comment provided by engineer. */ +"SimpleX address or 1-time link?" = "SimpleX-cím vagy egyszer használható meghívó?"; + +/* simplex link type */ +"SimpleX channel link" = "SimpleX-csatornahivatkozás"; + +/* No comment provided by engineer. */ +"SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "A SimpleX Chat és a Flux megállapodást kötött arról, hogy a Flux által üzemeltetett kiszolgálókat beépítik az alkalmazásba."; + +/* No comment provided by engineer. */ +"SimpleX Chat security was audited by Trail of Bits." = "A SimpleX Chat biztonsága a Trail of Bits által lett auditálva."; + +/* simplex link type */ +"SimpleX contact address" = "SimpleX kapcsolattartási cím"; + +/* notification */ +"SimpleX encrypted message or connection event" = "SimpleX titkosított üzenet vagy kapcsolati esemény"; + +/* simplex link type */ +"SimpleX group link" = "SimpleX-csoporthivatkozás"; + +/* chat feature */ +"SimpleX links" = "SimpleX-hivatkozások"; + +/* No comment provided by engineer. */ +"SimpleX links are prohibited." = "A SimpleX-hivatkozások küldése le van tiltva."; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "A SimpleX-hivatkozások küldése le van tiltva"; + +/* No comment provided by engineer. */ +"SimpleX Lock" = "SimpleX-zár"; + +/* No comment provided by engineer. */ +"SimpleX Lock mode" = "Zárolási mód"; + +/* No comment provided by engineer. */ +"SimpleX Lock not enabled!" = "A SimpleX-zár nincs bekapcsolva!"; + +/* No comment provided by engineer. */ +"SimpleX Lock turned on" = "SimpleX-zár bekapcsolva"; + +/* simplex link type */ +"SimpleX one-time invitation" = "Egyszer használható SimpleX-meghívó"; + +/* No comment provided by engineer. */ +"SimpleX protocols reviewed by Trail of Bits." = "A SimpleX Chat biztonsága a Trail of Bits által lett felülvizsgálva."; + +/* No comment provided by engineer. */ +"Simplified incognito mode" = "Egyszerűsített inkognitómód"; + +/* No comment provided by engineer. */ +"Size" = "Méret"; + +/* No comment provided by engineer. */ +"Skip" = "Kihagyás"; + +/* No comment provided by engineer. */ +"Skipped messages" = "Kihagyott üzenetek"; + +/* No comment provided by engineer. */ +"Small groups (max 20)" = "Kis csoportok (max. 20 tag)"; + +/* No comment provided by engineer. */ +"SMP server" = "SMP-kiszolgáló"; + +/* No comment provided by engineer. */ +"SOCKS proxy" = "SOCKS-proxy"; + +/* blur media */ +"Soft" = "Enyhe"; + +/* No comment provided by engineer. */ +"Some app settings were not migrated." = "Egyes alkalmazásbeállítások nem lettek átköltöztetve."; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "Néhány fájl nem lett exportálva:"; + +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import - you may see Chat console for more details." = "Néhány nem végzetes hiba történt az importáláskor – további részleteket a csevegési konzolban olvashat."; + +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "Néhány nem végzetes hiba történt az importáláskor:"; + +/* alert message */ +"Some servers failed the test:\n%@" = "Néhány kiszolgáló megbukott a teszten:\n%@"; + +/* notification title */ +"Somebody" = "Valaki"; + +/* blocking reason +report reason */ +"Spam" = "Kéretlen tartalom"; + +/* No comment provided by engineer. */ +"Square, circle, or anything in between." = "Négyzet, kör vagy bármi a kettő között."; + +/* chat item text */ +"standard end-to-end encryption" = "szabványos végpontok közötti titkosítás"; + +/* No comment provided by engineer. */ +"Start chat" = "Csevegés indítása"; + +/* No comment provided by engineer. */ +"Start chat?" = "Elindítja a csevegést?"; + +/* No comment provided by engineer. */ +"Start migration" = "Átköltöztetés indítása"; + +/* No comment provided by engineer. */ +"Starting from %@." = "Statisztikagyűjtés kezdete: %@."; + +/* No comment provided by engineer. */ +"starting…" = "indítás…"; + +/* No comment provided by engineer. */ +"Statistics" = "Statisztikák"; + +/* No comment provided by engineer. */ +"Stop" = "Megállítás"; + +/* No comment provided by engineer. */ +"Stop chat" = "Csevegési szolgáltatás megállítása"; + +/* No comment provided by engineer. */ +"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "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."; + +/* No comment provided by engineer. */ +"Stop chat?" = "Megállítja a csevegést?"; + +/* cancel file action */ +"Stop file" = "Fájl megállítása"; + +/* No comment provided by engineer. */ +"Stop receiving file?" = "Megállítja a fájlfogadást?"; + +/* No comment provided by engineer. */ +"Stop sending file?" = "Megállítja a fájlküldést?"; + +/* alert action */ +"Stop sharing" = "Megosztás megállítása"; + +/* alert title */ +"Stop sharing address?" = "Megállítja a címmegosztást?"; + +/* authentication reason */ +"Stop SimpleX" = "SimpleX megállítása"; + +/* No comment provided by engineer. */ +"Stopping chat" = "Csevegés megállítása folyamatban"; + +/* No comment provided by engineer. */ +"Storage" = "Tárhely"; + +/* No comment provided by engineer. */ +"strike" = "áthúzott"; + +/* blur media */ +"Strong" = "Erős"; + +/* No comment provided by engineer. */ +"Submit" = "Elküldés"; + +/* No comment provided by engineer. */ +"Subscribed" = "Feliratkozva"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Feliratkozási hibák"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Mellőzött feliratkozások"; + +/* No comment provided by engineer. */ +"Support SimpleX Chat" = "SimpleX Chat támogatása"; + +/* No comment provided by engineer. */ +"Switch audio and video during the call." = "Hang/Videó váltása hívás közben."; + +/* No comment provided by engineer. */ +"Switch chat profile for 1-time invitations." = "Csevegési profilváltás az egyszer használható meghívókhoz."; + +/* No comment provided by engineer. */ +"System" = "Rendszer"; + +/* No comment provided by engineer. */ +"System authentication" = "Rendszer-hitelesítés"; + +/* No comment provided by engineer. */ +"Tail" = "Farok"; + +/* No comment provided by engineer. */ +"Take picture" = "Kép készítése"; + +/* No comment provided by engineer. */ +"Tap button " = "Koppintson a "; + +/* 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."; + +/* No comment provided by engineer. */ +"Tap to activate profile." = "A profil aktiválásához koppintson az ikonra."; + +/* No comment provided by engineer. */ +"Tap to Connect" = "Koppintson ide a kapcsolódáshoz"; + +/* No comment provided by engineer. */ +"Tap to join" = "Koppintson ide a csatlakozáshoz"; + +/* 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 paste link" = "Koppintson ide a hivatkozás beillesztéséhez"; + +/* No comment provided by engineer. */ +"Tap to scan" = "Koppintson ide a QR-kód beolvasásához"; + +/* No comment provided by engineer. */ +"TCP connection" = "TCP-kapcsolat"; + +/* No comment provided by engineer. */ +"TCP connection timeout" = "TCP-kapcsolat időtúllépése"; + +/* No comment provided by engineer. */ +"TCP port for messaging" = "TCP-port az üzenetváltáshoz"; + +/* No comment provided by engineer. */ +"TCP_KEEPCNT" = "TCP_KEEPCNT"; + +/* No comment provided by engineer. */ +"TCP_KEEPIDLE" = "TCP_KEEPIDLE"; + +/* No comment provided by engineer. */ +"TCP_KEEPINTVL" = "TCP_KEEPINTVL"; + +/* file error alert title */ +"Temporary file error" = "Ideiglenes fájlhiba"; + +/* 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 server" = "Kiszolgáló tesztelése"; + +/* No comment provided by engineer. */ +"Test servers" = "Kiszolgálók tesztelése"; + +/* alert title */ +"Tests failed!" = "Sikertelen tesztek!"; + +/* No comment provided by engineer. */ +"Thank you for installing SimpleX Chat!" = "Köszönjük, hogy telepítette a SimpleX Chatet!"; + +/* No comment provided by engineer. */ +"Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Köszönet a felhasználóknak [a Weblate-en való közreműködésért](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; + +/* No comment provided by engineer. */ +"Thanks to the users – contribute via Weblate!" = "Köszönet a felhasználóknak a Weblate-en való közreműködésért!"; + +/* No comment provided by engineer. */ +"The app can notify you when you receive messages or contact requests - please open settings to enable." = "Az alkalmazás értesíteni fogja, amikor üzeneteket vagy meghívási kéréseket kap – ezt a beállítások menüben engedélyezheti."; + +/* 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 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."; + +/* No comment provided by engineer. */ +"The attempt to change database passphrase was not completed." = "Az adatbázis jelmondatának módosítására tett kísérlet nem fejeződött be."; + +/* No comment provided by engineer. */ +"The code you scanned is not a SimpleX link QR code." = "A beolvasott QR-kód nem egy SimpleX-QR-kód-hivatkozás."; + +/* 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."; + +/* No comment provided by engineer. */ +"The connection you accepted will be cancelled!" = "Az Ön által elfogadott kérelem vissza lesz vonva!"; + +/* No comment provided by engineer. */ +"The contact you shared this link with will NOT be able to connect!" = "A partnere, akivel megosztotta ezt a hivatkozást, NEM fog tudni kapcsolódni!"; + +/* No comment provided by engineer. */ +"The created archive is available via app Settings / Database / Old database archive." = "A létrehozott archívum a „Beállítások / Adatbázis / Régi adatbázis-archívum” menüben érhető el."; + +/* No comment provided by engineer. */ +"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"; + +/* No comment provided by engineer. */ +"The hash of the previous message is different." = "Az előző üzenet hasítóértéke különbözik."; + +/* No comment provided by engineer. */ +"The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "A következő üzenet azonosítója érvénytelen (kisebb vagy egyenlő az előzővel).\nEz valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő."; + +/* No comment provided by engineer. */ +"The message will be deleted for all members." = "Az üzenet az összes tag számára törölve lesz."; + +/* No comment provided by engineer. */ +"The message will be marked as moderated for all members." = "Az üzenet az összes tag számára moderáltként lesz megjelölve."; + +/* No comment provided by engineer. */ +"The messages will be deleted for all members." = "Az üzenetek az összes tag számára törölve lesznek."; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "Az üzenetek az összes tag számára moderáltként lesznek megjelölve."; + +/* 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. */ +"Your profile is stored on your device and only shared with your contacts." = "A profilja csak a partnereivel van megosztva."; + +/* 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: **%@**."; + +/* No comment provided by engineer. */ +"The second preset operator in the app!" = "A második előre beállított üzemeltető az alkalmazásban!"; + +/* No comment provided by engineer. */ +"The second tick we missed! ✅" = "A második jelölés, amit kihagytunk! ✅"; + +/* No comment provided by engineer. */ +"The sender will NOT be notified" = "A feladó NEM fog értesítést kapni"; + +/* No comment provided by engineer. */ +"The servers for new connections of your current chat profile **%@**." = "A jelenlegi **%@** nevű csevegési profiljához tartozó új kapcsolatok kiszolgálói."; + +/* No comment provided by engineer. */ +"The servers for new files of your current chat profile **%@**." = "A jelenlegi **%@** nevű csevegési profiljához tartozó új fájlok kiszolgálói."; + +/* No comment provided by engineer. */ +"The text you pasted is not a SimpleX link." = "A beillesztett szöveg nem egy SimpleX-hivatkozás."; + +/* No comment provided by engineer. */ +"The uploaded database archive will be permanently removed from the servers." = "A feltöltött adatbázis-archívum véglegesen el lesz távolítva a kiszolgálókról."; + +/* No comment provided by engineer. */ +"Themes" = "Témák"; + +/* No comment provided by engineer. */ +"These conditions will also apply for: **%@**." = "Ezek a feltételek lesznek elfogadva a következő számára is: **%@**."; + +/* No comment provided by engineer. */ +"These settings are for your current profile **%@**." = "Ezek a beállítások csak a jelenlegi **%@** nevű csevegési profiljára vonatkoznak."; + +/* No comment provided by engineer. */ +"They can be overridden in contact and group settings." = "Ezek felülbírálhatók a partner- és csoportbeállításokban."; + +/* 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." = "Ez a művelet nem vonható vissza – az összes fogadott és küldött fájl a médiatartalmakkal együtt törölve lesznek. Az alacsony felbontású képek viszont megmaradnak."; + +/* 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." = "Ez a művelet nem vonható vissza – a kijelöltnél korábban küldött és fogadott üzenetek törölve lesznek. Ez több percet is igénybe vehet."; + +/* alert message */ +"This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." = "Ez a művelet nem vonható vissza – a kijelölt üzenettől korábban küldött és fogadott üzenetek törölve lesznek a csevegésből."; + +/* No comment provided by engineer. */ +"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Ez a művelet nem vonható vissza – profiljai, partnerei, üzenetei és fájljai véglegesen törölve lesznek."; + +/* E2EE info chat item */ +"This chat is protected by end-to-end encryption." = "Ez a csevegés végpontok közötti titkosítással védett."; + +/* E2EE info chat item */ +"This chat is protected by quantum resistant end-to-end encryption." = "Ez a csevegés végpontok közötti kvantumbiztos titkosítással védett."; + +/* notification title */ +"this contact" = "ez a partner"; + +/* No comment provided by engineer. */ +"This device name" = "Ennek az eszköznek a neve"; + +/* No comment provided by engineer. */ +"This display name is invalid. Please choose another name." = "Ez a megjelenítendő név érvénytelen. Válasszon egy másik nevet."; + +/* No comment provided by engineer. */ +"This group has over %lld members, delivery receipts are not sent." = "Ennek a csoportnak több mint %lld tagja van, a kézbesítési jelentések nem lesznek elküldve."; + +/* No comment provided by engineer. */ +"This group no longer exists." = "Ez a csoport már nem létezik."; + +/* No comment provided by engineer. */ +"This is your own one-time link!" = "Ez a saját egyszer használható meghívója!"; + +/* No comment provided by engineer. */ +"This is your own SimpleX address!" = "Ez a saját SimpleX-címe!"; + +/* 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."; + +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "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."; + +/* No comment provided by engineer. */ +"This message was deleted or not received yet." = "Ez az üzenet törölve lett vagy még nem érkezett meg."; + +/* No comment provided by engineer. */ +"This setting applies to messages in your current chat profile **%@**." = "Ez a beállítás csak az Ön jelenlegi **%@** nevű csevegési profiljában lévő üzenetekre vonatkozik."; + +/* No comment provided by engineer. */ +"Title" = "Cím"; + +/* No comment provided by engineer. */ +"To ask any questions and to receive updates:" = "Bármilyen kérdés feltevéséhez és a frissítésekért:"; + +/* No comment provided by engineer. */ +"To connect, your contact can scan QR code or use the link in the app." = "A kapcsolódáshoz a partnere beolvashatja a QR-kódot, vagy használhatja az alkalmazásban található hivatkozást."; + +/* No comment provided by engineer. */ +"To hide unwanted messages." = "Kéretlen üzenetek elrejtése."; + +/* No comment provided by engineer. */ +"To make a new connection" = "Új kapcsolat létrehozásához"; + +/* 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."; + +/* No comment provided by engineer. */ +"To protect timezone, image/voice files use UTC." = "Az időzóna védelmének érdekében a kép-/hangfájlok UTC-t használnak."; + +/* No comment provided by engineer. */ +"To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "A biztonsága érdekében kapcsolja be a SimpleX-zár funkciót.\nA funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beállítására az eszközén."; + +/* No comment provided by engineer. */ +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "Az IP-cím védelmének érdekében a privát útválasztás az SMP-kiszolgálókat használja az üzenetek kézbesítéséhez."; + +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Adatainak védelme érdekében a SimpleX külön üzenet-azonosítókat használ minden egyes kapcsolatához."; + +/* No comment provided by engineer. */ +"To receive" = "A fogadáshoz"; + +/* No comment provided by engineer. */ +"To record speech please grant permission to use Microphone." = "A beszéd rögzítéséhez adjon engedélyt a Mikrofon használatára."; + +/* No comment provided by engineer. */ +"To record video please grant permission to use Camera." = "A videó rögzítéséhez adjon engedélyt a Kamera használatára."; + +/* No comment provided by engineer. */ +"To record voice message please grant permission to use Microphone." = "Hangüzenet rögzítéséhez adjon engedélyt a mikrofon használathoz."; + +/* No comment provided by engineer. */ +"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Rejtett profilja felfedéséhez adja meg a teljes jelszót a keresőmezőben, a **Csevegési profilok** menüben."; + +/* No comment provided by engineer. */ +"To send" = "A küldéshez"; + +/* No comment provided by engineer. */ +"To support instant push notifications the chat database has to be migrated." = "Az azonnali push-értesítések támogatásához a csevegési adatbázis átköltöztetése szükséges."; + +/* No comment provided by engineer. */ +"To use the servers of **%@**, accept conditions of use." = "A(z) **%@** kiszolgálóinak használatához fogadja el a használati feltételeket."; + +/* 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 hitelesíté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 átváltása:"; + +/* No comment provided by engineer. */ +"Toggle incognito when connecting." = "Inkognitóra váltás kapcsolódáskor."; + +/* token status */ +"Token status: %@." = "Token állapota: %@."; + +/* No comment provided by engineer. */ +"Toolbar opacity" = "Eszköztár átlátszatlansága"; + +/* No comment provided by engineer. */ +"Total" = "Összes kapcsolat"; + +/* No comment provided by engineer. */ +"Transport isolation" = "Átvitel-izoláció"; + +/* No comment provided by engineer. */ +"Transport sessions" = "Munkamenetek átvitele"; + +/* No comment provided by engineer. */ +"Trying to connect to the server used to receive messages from this contact (error: %@)." = "Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (hiba: %@)."; + +/* No comment provided by engineer. */ +"Trying to connect to the server used to receive messages from this contact." = "Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál."; + +/* No comment provided by engineer. */ +"Turkish interface" = "Török kezelőfelület"; + +/* No comment provided by engineer. */ +"Turn off" = "Kikapcsolás"; + +/* No comment provided by engineer. */ +"Turn on" = "Bekapcsolás"; + +/* No comment provided by engineer. */ +"Unable to record voice message" = "Hangüzenet rögzítése nem lehetséges"; + +/* No comment provided by engineer. */ +"Unblock" = "Feloldás"; + +/* No comment provided by engineer. */ +"Unblock for all" = "Feloldás"; + +/* No comment provided by engineer. */ +"Unblock member" = "Tag feloldása"; + +/* No comment provided by engineer. */ +"Unblock member for all?" = "Az összes tag számára feloldja a tag letiltását?"; + +/* No comment provided by engineer. */ +"Unblock member?" = "Feloldja a tag letiltását?"; + +/* rcv group event chat item */ +"unblocked %@" = "feloldotta %@ letiltását"; + +/* No comment provided by engineer. */ +"Undelivered messages" = "Kézbesítetlen üzenetek"; + +/* No comment provided by engineer. */ +"Unexpected migration state" = "Váratlan átköltöztetési állapot"; + +/* swipe action */ +"Unfav." = "Kedvenc megszüntetése"; + +/* No comment provided by engineer. */ +"Unhide" = "Felfedés"; + +/* No comment provided by engineer. */ +"Unhide chat profile" = "Csevegési profil felfedése"; + +/* No comment provided by engineer. */ +"Unhide profile" = "Profil felfedése"; + +/* No comment provided by engineer. */ +"Unit" = "Egység"; + +/* connection info */ +"unknown" = "ismeretlen"; + +/* callkit banner */ +"Unknown caller" = "Ismeretlen hívó"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Ismeretlen adatbázishiba: %@"; + +/* No comment provided by engineer. */ +"Unknown error" = "Ismeretlen hiba"; + +/* No comment provided by engineer. */ +"unknown servers" = "ismeretlen átjátszók"; + +/* alert title */ +"Unknown servers!" = "Ismeretlen kiszolgálók!"; + +/* No comment provided by engineer. */ +"unknown status" = "ismeretlen állapot"; + +/* No comment provided by engineer. */ +"Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Hacsak nem az iOS hívási felületét használja, engedélyezze a Ne zavarjanak módot a megszakítások elkerülése érdekében."; + +/* 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." = "Hacsak a partnere nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt egyszer, lehet hogy ez egy hiba – jelentse a problémát.\nA kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcsolattartási hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e."; + +/* No comment provided by engineer. */ +"Unlink" = "Szétkapcsolás"; + +/* No comment provided by engineer. */ +"Unlink desktop?" = "Leválasztja a számítógépet?"; + +/* No comment provided by engineer. */ +"Unlock" = "Feloldás"; + +/* authentication reason */ +"Unlock app" = "Alkalmazás feloldása"; + +/* notification label action */ +"Unmute" = "Némítás megszüntetése"; + +/* No comment provided by engineer. */ +"unprotected" = "nem védett"; + +/* swipe action */ +"Unread" = "Olvasatlan"; + +/* No comment provided by engineer. */ +"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. */ +"Update" = "Frissítés"; + +/* No comment provided by engineer. */ +"Update database passphrase" = "Az adatbázis jelmondatának módosítása"; + +/* No comment provided by engineer. */ +"Update network settings?" = "Módosítja a hálózati beállításokat?"; + +/* No comment provided by engineer. */ +"Update settings?" = "Frissíti a beállításokat?"; + +/* No comment provided by engineer. */ +"Updated conditions" = "Frissített feltételek"; + +/* rcv group event chat item */ +"updated group profile" = "frissítette a csoport profilját"; + +/* profile update event chat item */ +"updated profile" = "frissített profil"; + +/* No comment provided by engineer. */ +"Updating settings will re-connect the client to all servers." = "A beállítások frissítése a kiszolgálókhoz való újra kapcsolódással jár."; + +/* No comment provided by engineer. */ +"Upgrade and open chat" = "Fejlesztés és a csevegés megnyitása"; + +/* No comment provided by engineer. */ +"Upload errors" = "Feltöltési hibák"; + +/* No comment provided by engineer. */ +"Upload failed" = "Sikertelen feltöltés"; + +/* server test step */ +"Upload file" = "Fájl feltöltése"; + +/* No comment provided by engineer. */ +"Uploaded" = "Feltöltve"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Feltöltött fájlok"; + +/* No comment provided by engineer. */ +"Uploading archive" = "Archívum feltöltése"; + +/* No comment provided by engineer. */ +"Use .onion hosts" = "Onion-kiszolgálók használata"; + +/* No comment provided by engineer. */ +"Use %@" = "%@ használata"; + +/* No comment provided by engineer. */ +"Use chat" = "SimpleX Chat használata"; + +/* No comment provided by engineer. */ +"Use current profile" = "Jelenlegi profil használata"; + +/* No comment provided by engineer. */ +"Use for files" = "Használat a fájlokhoz"; + +/* No comment provided by engineer. */ +"Use for messages" = "Használat az üzenetekhez"; + +/* No comment provided by engineer. */ +"Use for new connections" = "Alkalmazás új kapcsolatokhoz"; + +/* No comment provided by engineer. */ +"Use from desktop" = "Társítás számítógéppel"; + +/* No comment provided by engineer. */ +"Use iOS call interface" = "Az iOS hívási felületét használata"; + +/* No comment provided by engineer. */ +"Use new incognito profile" = "Új inkognitóprofil használata"; + +/* No comment provided by engineer. */ +"Use only local notifications?" = "Csak helyi értesítések használata?"; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "Használjon privát útválasztást ismeretlen kiszolgálókkal, ha az IP-cím nem védett."; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "Használjon privát útválasztást ismeretlen kiszolgálókkal."; + +/* No comment provided by engineer. */ +"Use server" = "Kiszolgáló használata"; + +/* No comment provided by engineer. */ +"Use servers" = "Kiszolgálók használata"; + +/* No comment provided by engineer. */ +"Use short links (BETA)" = "Rövid hivatkozások használata (béta)"; + +/* No comment provided by engineer. */ +"Use SimpleX Chat servers?" = "SimpleX Chat-kiszolgálók használata?"; + +/* No comment provided by engineer. */ +"Use SOCKS proxy" = "SOCKS-proxy használata"; + +/* No comment provided by engineer. */ +"Use TCP port %@ when no port is specified." = "A következő TCP-port használata, amikor nincs port megadva: %@."; + +/* No comment provided by engineer. */ +"Use TCP port 443 for preset servers only." = "A 443-as TCP-port használata kizárólag az előre beállított kiszolgálokhoz."; + +/* No comment provided by engineer. */ +"Use the app while in the call." = "Használja az alkalmazást hívás közben."; + +/* No comment provided by engineer. */ +"Use the app with one hand." = "Használja az alkalmazást egy kézzel."; + +/* No comment provided by engineer. */ +"Use web port" = "Webport használata"; + +/* No comment provided by engineer. */ +"User selection" = "Felhasználó kijelölése"; + +/* No comment provided by engineer. */ +"Username" = "Felhasználónév"; + +/* No comment provided by engineer. */ +"Using SimpleX Chat servers." = "SimpleX Chat-kiszolgálók használatban."; + +/* No comment provided by engineer. */ +"v%@" = "v%@"; + +/* No comment provided by engineer. */ +"v%@ (%@)" = "v%@ (%@)"; + +/* No comment provided by engineer. */ +"Verify code with desktop" = "Kód hitelesítése a számítógépen"; + +/* No comment provided by engineer. */ +"Verify connection" = "Kapcsolat hitelesítése"; + +/* No comment provided by engineer. */ +"Verify connection security" = "Biztonságos kapcsolat hitelesítése"; + +/* No comment provided by engineer. */ +"Verify connections" = "Kapcsolatok hitelesítése"; + +/* No comment provided by engineer. */ +"Verify database passphrase" = "Az adatbázis jelmondatának hitelesítése"; + +/* No comment provided by engineer. */ +"Verify passphrase" = "Jelmondat hitelesítése"; + +/* No comment provided by engineer. */ +"Verify security code" = "Biztonsági kód hitelesítése"; + +/* No comment provided by engineer. */ +"Via browser" = "Böngészőn keresztül"; + +/* chat list item description */ +"via contact address link" = "a kapcsolattartási címhivatkozáson keresztül"; + +/* chat list item description */ +"via group link" = "a csoporthivatkozáson keresztül"; + +/* chat list item description */ +"via one-time link" = "egy egyszer használható meghívón keresztül"; + +/* No comment provided by engineer. */ +"via relay" = "egy továbbítókiszolgálón keresztül"; + +/* No comment provided by engineer. */ +"Via secure quantum resistant protocol." = "Biztonságos kvantumbiztos protokollon keresztül."; + +/* No comment provided by engineer. */ +"video" = "videó"; + +/* No comment provided by engineer. */ +"Video call" = "Videóhívás"; + +/* No comment provided by engineer. */ +"video call (not e2e encrypted)" = "videóhívás (nem e2e titkosított)"; + +/* No comment provided by engineer. */ +"Video will be received when your contact completes uploading it." = "A videó akkor érkezik meg, amikor a küldője befejezte annak feltöltését."; + +/* No comment provided by engineer. */ +"Video will be received when your contact is online, please wait or check later!" = "A videó akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később!"; + +/* No comment provided by engineer. */ +"Videos and files up to 1gb" = "Videók és fájlok legfeljebb 1GB méretig"; + +/* No comment provided by engineer. */ +"View conditions" = "Feltételek megtekintése"; + +/* No comment provided by engineer. */ +"View security code" = "Biztonsági kód megtekintése"; + +/* No comment provided by engineer. */ +"View updated conditions" = "Frissített feltételek megtekintése"; + +/* chat feature */ +"Visible history" = "Látható előzmények"; + +/* No comment provided by engineer. */ +"Voice message…" = "Hangüzenet…"; + +/* chat feature */ +"Voice messages" = "Hangüzenetek"; + +/* No comment provided by engineer. */ +"Voice messages are prohibited in this chat." = "A hangüzenetek küldése le van tiltva ebben a csevegésben."; + +/* No comment provided by engineer. */ +"Voice messages are prohibited." = "A hangüzenetek küldése le van tiltva."; + +/* No comment provided by engineer. */ +"Voice messages not allowed" = "A hangüzenetek küldése le van tiltva"; + +/* No comment provided by engineer. */ +"Voice messages prohibited!" = "A hangüzenetek le vannak tiltva!"; + +/* No comment provided by engineer. */ +"waiting for answer…" = "várakozás a válaszra…"; + +/* No comment provided by engineer. */ +"waiting for confirmation…" = "várakozás a visszaigazolásra…"; + +/* No comment provided by engineer. */ +"Waiting for desktop..." = "Várakozás a számítógép-alkalmazásra…"; + +/* No comment provided by engineer. */ +"Waiting for file" = "Várakozás a fájlra"; + +/* No comment provided by engineer. */ +"Waiting for image" = "Várakozás a képre"; + +/* No comment provided by engineer. */ +"Waiting for video" = "Várakozás a videóra"; + +/* No comment provided by engineer. */ +"Wallpaper accent" = "Háttérkép kiemelőszíne"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Háttérkép háttérszíne"; + +/* No comment provided by engineer. */ +"wants to connect to you!" = "kapcsolatba akar lépni Önnel!"; + +/* No comment provided by engineer. */ +"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "Figyelmeztetés: a csevegés elindítása egyszerre több eszközön nem támogatott, mert üzenetkézbesítési hibákat okoz"; + +/* No comment provided by engineer. */ +"Warning: you may lose some data!" = "Figyelmeztetés: néhány adat elveszhet!"; + +/* No comment provided by engineer. */ +"WebRTC ICE servers" = "WebRTC ICE-kiszolgálók"; + +/* time unit */ +"weeks" = "hét"; + +/* No comment provided by engineer. */ +"Welcome %@!" = "Üdvözöljük %@!"; + +/* No comment provided by engineer. */ +"Welcome message" = "Üdvözlőüzenet"; + +/* No comment provided by engineer. */ +"Welcome message is too long" = "Az üdvözlőüzenet túl hosszú"; + +/* No comment provided by engineer. */ +"What's new" = "Újdonságok"; + +/* No comment provided by engineer. */ +"When available" = "Amikor elérhető"; + +/* No comment provided by engineer. */ +"When connecting audio and video calls." = "Amikor egy bejövő hang- vagy videóhívás érkezik."; + +/* No comment provided by engineer. */ +"when IP hidden" = "ha az IP-cím rejtett"; + +/* No comment provided by engineer. */ +"When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "Amikor egynél több üzemeltető van engedélyezve, akkor egyik sem rendelkezik olyan metaadatokkal, amelyekből megtudható, hogy ki kivel kommunikál."; + +/* 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." = "Inkognitóprofil megosztása esetén a rendszer azt a profilt fogja használni azokhoz a csoportokhoz, amelyekbe meghívást kapott."; + +/* No comment provided by engineer. */ +"WiFi" = "Wi-Fi"; + +/* No comment provided by engineer. */ +"Will be enabled in direct chats!" = "A közvetlen csevegésekben engedélyezve lesz!"; + +/* No comment provided by engineer. */ +"Wired ethernet" = "Vezetékes Ethernet"; + +/* No comment provided by engineer. */ +"With encrypted files and media." = "Titkosított fájlokkal és médiatartalmakkal."; + +/* No comment provided by engineer. */ +"With optional welcome message." = "Nem kötelező üdvözlőüzenettel."; + +/* No comment provided by engineer. */ +"With reduced battery usage." = "Csökkentett akkumulátor-használattal."; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "Tor vagy VPN nélkül az Ön IP-címe látható lesz 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 Ön IP-címe látható lesz a következő XFTP-továbbítókiszolgálók számára: %@."; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Érvénytelen adatbázis-jelmondat"; + +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "Érvénytelen kulcs vagy ismeretlen kapcsolat – valószínűleg ez a kapcsolat törlődött."; + +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Érvénytelen kulcs vagy ismeretlen fájltöredékcím – valószínűleg a fájl törlődött."; + +/* No comment provided by engineer. */ +"Wrong passphrase!" = "Érvénytelen jelmondat!"; + +/* No comment provided by engineer. */ +"XFTP server" = "XFTP-kiszolgáló"; + +/* pref value */ +"yes" = "igen"; + +/* No comment provided by engineer. */ +"you" = "Ön"; + +/* No comment provided by engineer. */ +"You **must not** use the same database on two devices." = "**Nem szabad** ugyanazt az adatbázist használni egyszerre két eszközön."; + +/* No comment provided by engineer. */ +"You accepted connection" = "Kapcsolat létrehozása"; + +/* No comment provided by engineer. */ +"You allow" = "Ön engedélyezi"; + +/* No comment provided by engineer. */ +"You already have a chat profile with the same display name. Please choose another name." = "Már van egy csevegési profil ugyanezzel a megjelenítendő névvel. Válasszon egy másik nevet."; + +/* No comment provided by engineer. */ +"You are already connected to %@." = "Ön már kapcsolódott a következőhöz: %@."; + +/* No comment provided by engineer. */ +"You are already connected with %@." = "Ön már kapcsolódva van vele: %@."; + +/* No comment provided by engineer. */ +"You are already connecting to %@." = "A kapcsolódás már folyamatban van a következőhöz: %@."; + +/* No comment provided by engineer. */ +"You are already connecting via this one-time link!" = "A kapcsolódás már folyamatban van ezen az egyszer használható meghívón keresztül!"; + +/* No comment provided by engineer. */ +"You are already in group %@." = "Ön már a(z) %@ nevű csoport tagja."; + +/* No comment provided by engineer. */ +"You are already joining the group %@." = "A csatlakozás már folyamatban van a(z) %@ nevű csoporthoz."; + +/* No comment provided by engineer. */ +"You are already joining the group via this link!" = "A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül!"; + +/* No comment provided by engineer. */ +"You are already joining the group via this link." = "A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül."; + +/* No comment provided by engineer. */ +"You are already joining the group!\nRepeat join request?" = "A csatlakozás már folyamatban van a csoporthoz!\nMegismétli a meghívási kérést?"; + +/* No comment provided by engineer. */ +"You are connected to the server used to receive messages from this contact." = "Ön már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál."; + +/* No comment provided by engineer. */ +"you are invited to group" = "Ön meghívást kapott a csoportba"; + +/* No comment provided by engineer. */ +"You are invited to group" = "Ön meghívást kapott a csoportba"; + +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Ön nem kapcsolódik ezekhez a kiszolgálókhoz. A privát útválasztás az üzenetek kézbesítésére szolgál."; + +/* No comment provided by engineer. */ +"you are observer" = "Ön megfigyelő"; + +/* snd group event chat item */ +"you blocked %@" = "Ön letiltotta őt: %@"; + +/* No comment provided by engineer. */ +"You can accept calls from lock screen, without device and app authentication." = "Hívásokat fogadhat a lezárási képernyőről, eszköz- és alkalmazás-hitelesítés nélkül."; + +/* No comment provided by engineer. */ +"You can change it in Appearance settings." = "Ezt a „Megjelenés” menüben módosíthatja."; + +/* No comment provided by engineer. */ +"You can configure servers via settings." = "A kiszolgálókat a „Hálózat és kiszolgálók” menüben konfigurálhatja."; + +/* No comment provided by engineer. */ +"You can create it later" = "Létrehozás később"; + +/* No comment provided by engineer. */ +"You can enable later via Settings" = "Később engedélyezheti a „Beállításokban”"; + +/* No comment provided by engineer. */ +"You can enable them later via app Privacy & Security settings." = "Később engedélyezheti őket az „Adatvédelem és biztonság” menüben."; + +/* No comment provided by engineer. */ +"You can give another try." = "Megpróbálhatja még egyszer."; + +/* No comment provided by engineer. */ +"You can hide or mute a user profile - swipe it to the right." = "Elrejtheti vagy lenémíthatja a felhasználó -profiljait – csúsztassa jobbra a profilt."; + +/* No comment provided by engineer. */ +"You can make it visible to your SimpleX contacts via Settings." = "Láthatóvá teheti a SimpleXbeli partnerei számára a „Beállításokban”."; + +/* notification body */ +"You can now chat with %@" = "Mostantól küldhet üzeneteket %@ számára"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "Az „Archivált partnerekből” továbbra is küldhet üzeneteket neki: %@."; + +/* No comment provided by engineer. */ +"You can set connection name, to remember who the link was shared with." = "Beállíthatja a partner nevét, hogy emlékezzen arra, hogy kivel osztotta meg a hivatkozást."; + +/* 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 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 csoportot Ön később törli, akkor nem fogja elveszíteni annak tagjait."; + +/* No comment provided by engineer. */ +"You can share this address with your contacts to let them connect with **%@**." = "Megoszthatja ezt a SimpleX-címet a partnereivel, hogy kapcsolatba léphessenek vele: **%@**."; + +/* No comment provided by engineer. */ +"You can start chat via app Settings / Database or by restarting the app" = "A csevegést az alkalmazás „Beállítások / Adatbázis” menüben vagy az alkalmazás újraindításával indíthatja el"; + +/* No comment provided by engineer. */ +"You can still view conversation with %@ in the list of chats." = "A(z) %@ nevű partnerével folytatott beszélgetéseit továbbra is megtekintheti a csevegések listájában."; + +/* No comment provided by engineer. */ +"You can turn on SimpleX Lock via Settings." = "A SimpleX-zár az „Adatvédelem és biztonság” menüben kapcsolható be."; + +/* No comment provided by engineer. */ +"You can use markdown to format messages:" = "Üzenetek formázása a szövegbe szúrt speciális karakterekkel:"; + +/* alert message */ +"You can view invitation link again in connection details." = "A meghívási hivatkozást újra megtekintheti a kapcsolat részleteinél."; + +/* No comment provided by engineer. */ +"You can't send messages!" = "Nem lehet üzeneteket küldeni!"; + +/* chat item text */ +"you changed address" = "Ön módosította a címet"; + +/* chat item text */ +"you changed address for %@" = "Ön módosította a címet %@ számára"; + +/* snd group event chat item */ +"you changed role for yourself to %@" = "Ön a következőre módosította a saját szerepkörét: „%@”"; + +/* snd group event chat item */ +"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 hitelesíteni; 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 via this address!" = "Már küldött egy meghívási kérést ezen a címen keresztül!"; + +/* No comment provided by engineer. */ +"You have already requested connection!\nRepeat connection request?" = "Ön már küldött egy meghívási kérést!\nMegismétli a meghívási kérést?"; + +/* No comment provided by engineer. */ +"You have to enter passphrase every time the app starts - it is not stored on the device." = "A jelmondatot minden alkalommal meg kell adnia, amikor az alkalmazás elindul – nem az eszközön van tárolva."; + +/* No comment provided by engineer. */ +"You invited a contact" = "Ön meghívta egy partnerét"; + +/* No comment provided by engineer. */ +"You joined this group" = "Ön csatlakozott ehhez a csoporthoz"; + +/* No comment provided by engineer. */ +"You joined this group. Connecting to inviting group member." = "Ön csatlakozott ehhez a csoporthoz. Kapcsolódás a meghívó csoporttaghoz."; + +/* snd group event chat item */ +"you left" = "Ön elhagyta a csoportot"; + +/* No comment provided by engineer. */ +"You may migrate the exported database." = "Az exportált adatbázist átköltöztetheti."; + +/* No comment provided by engineer. */ +"You may save the exported archive." = "Az exportált archívumot elmentheti."; + +/* 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." = "A csevegési adatbázis legfrissebb verzióját CSAK egy eszközön kell használnia, ellenkező esetben előfordulhat, hogy az üzeneteket nem fogja megkapni valamennyi partnerétől."; + +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "Engedélyeznie kell a hívásokat a partnere számára, hogy fel tudják hívni egymást."; + +/* No comment provided by engineer. */ +"You need to allow your contact to send voice messages to be able to send them." = "Engedélyeznie kell a hangüzenetek küldését a partnere számára, hogy hangüzeneteket küldhessenek egymásnak."; + +/* No comment provided by engineer. */ +"You rejected group invitation" = "Csoportmeghívó elutasítva"; + +/* snd group event chat item */ +"you removed %@" = "Ön eltávolította őt: %@"; + +/* No comment provided by engineer. */ +"You sent group invitation" = "Csoportmeghívó elküldve"; + +/* chat list item description */ +"you shared one-time link" = "Ön egy egyszer használható meghívót osztott meg"; + +/* chat list item description */ +"you shared one-time link incognito" = "Ön egy egyszer használható meghívót osztott meg inkognitóban"; + +/* token info */ +"You should receive notifications." = "Ön megkapja az értesítéseket."; + +/* snd group event chat item */ +"you unblocked %@" = "Ön feloldotta %@ letiltását"; + +/* No comment provided by engineer. */ +"You will be connected to group when the group host's device is online, please wait or check later!" = "Akkor lesz kapcsolódva a csoporthoz, amikor a csoport tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később!"; + +/* No comment provided by engineer. */ +"You will be connected when group link host's device is online, please wait or check later!" = "Akkor lesz kapcsolódva, amikor a csoporthivatkozás tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később!"; + +/* No comment provided by engineer. */ +"You will be connected when your connection request is accepted, please wait or check later!" = "Akkor lesz kapcsolódva, ha a meghívási kérése el lesz fogadva, várjon, vagy ellenőrizze később!"; + +/* No comment provided by engineer. */ +"You will be connected when your contact's device is online, please wait or check later!" = "Akkor lesz kapcsolódva, amikor a partnerének az eszköze online lesz, várjon, vagy ellenőrizze később!"; + +/* No comment provided by engineer. */ +"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Az alkalmazás elindításához vagy 30 másodpercnyi háttérben töltött idő után, az alkalmazáshoz való visszatéréshez hitelesítésre lesz szükség."; + +/* No comment provided by engineer. */ +"You will connect to all group members." = "Kapcsolódni fog a csoport összes tagjához."; + +/* 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 chat. Chat history will be preserved." = "Ön nem fog több üzenetet kapni ebből a csevegésből, de a csevegés előzményei megmaradnak."; + +/* No comment provided by engineer. */ +"You will stop receiving messages from this group. Chat history will be preserved." = "Ettől a csoporttól nem fog értesítéseket kapni. A csevegési előzmények megmaradnak."; + +/* No comment provided by engineer. */ +"You won't lose your contacts if you later delete your address." = "Nem veszíti el a partnereit, ha később törli a címét."; + +/* No comment provided by engineer. */ +"you: " = "Ön: "; + +/* 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" = "Egy olyan partnerét próbálja meghívni, akivel inkognitóprofilt osztott meg abban a csoportban, amelyben a fő profilja van használatban"; + +/* 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" = "Inkognitóprofilt használ ehhez a csoporthoz – fő profilja megosztásának elkerülése érdekében a meghívók küldése le van tiltva"; + +/* No comment provided by engineer. */ +"Your calls" = "Hívások"; + +/* No comment provided by engineer. */ +"Your chat database" = "Csevegési adatbázis"; + +/* No comment provided by engineer. */ +"Your chat database is not encrypted - set passphrase to encrypt it." = "A csevegési adatbázis nincs titkosítva – adjon meg egy jelmondatot a titkosításhoz."; + +/* alert title */ +"Your chat preferences" = "Az Ön csevegési beállításai"; + +/* No comment provided by engineer. */ +"Your chat profiles" = "Csevegési profilok"; + +/* No comment provided by engineer. */ +"Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "A kapcsolata át lett helyezve ide: %@, de egy váratlan hiba történt a profilra való átirányításkor."; + +/* No comment provided by engineer. */ +"Your contact sent a file that is larger than currently supported maximum size (%@)." = "A partnere a jelenleg megengedett maximális méretű (%@) fájlnál nagyobbat küldött."; + +/* No comment provided by engineer. */ +"Your contacts can allow full message deletion." = "A partnerei engedélyezhetik a teljes üzenet törlését."; + +/* No comment provided by engineer. */ +"Your contacts will remain connected." = "A partnerei továbbra is kapcsolódva maradnak."; + +/* No comment provided by engineer. */ +"Your credentials may be sent unencrypted." = "A hitelesítőadatai titkosítatlanul is elküldhetők."; + +/* No comment provided by engineer. */ +"Your current chat database will be DELETED and REPLACED with the imported one." = "A jelenlegi csevegési adatbázis TÖRÖLVE és CSERÉLVE lesz az importáltra."; + +/* No comment provided by engineer. */ +"Your current profile" = "Jelenlegi profil"; + +/* No comment provided by engineer. */ +"Your ICE servers" = "Saját ICE-kiszolgálók"; + +/* No comment provided by engineer. */ +"Your preferences" = "Beállítások"; + +/* No comment provided by engineer. */ +"Your privacy" = "Adatvédelem"; + +/* No comment provided by engineer. */ +"Your profile" = "Profil"; + +/* No comment provided by engineer. */ +"Your profile **%@** will be shared." = "A(z) **%@** nevű profilja meg lesz osztva."; + +/* No comment provided by engineer. */ +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "A profilja az eszközén van tárolva és csak a partnereivel van megosztva. A SimpleX-kiszolgálók nem láthatják a profilját."; + +/* 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 elmenti, a profilfrissítés el lesz küldve a partnerei számára."; + +/* No comment provided by engineer. */ +"Your profile, contacts and delivered messages are stored on your device." = "A profilja, a partnerei és az elküldött üzenetei a saját eszközén vannak tárolva."; + +/* 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"; + +/* No comment provided by engineer. */ +"Your servers" = "Saját kiszolgálók"; + +/* No comment provided by engineer. */ +"Your settings" = "Beállítások"; + +/* No comment provided by engineer. */ +"Your SimpleX address" = "Profil SimpleX-címe"; + diff --git a/apps/ios/hu.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/hu.lproj/SimpleX--iOS--InfoPlist.strings new file mode 100644 index 0000000000..f389e41458 --- /dev/null +++ b/apps/ios/hu.lproj/SimpleX--iOS--InfoPlist.strings @@ -0,0 +1,18 @@ +/* Bundle name */ +"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."; + +/* Privacy - Face ID Usage Description */ +"NSFaceIDUsageDescription" = "A SimpleX Face ID-t használ a helyi hitelesítéshez"; + +/* Privacy - Local Network Usage Description */ +"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."; + +/* Privacy - Photo Library Additions Usage Description */ +"NSPhotoLibraryAddUsageDescription" = "A SimpleXnek galéria-hozzáférésre van szüksége 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 274b909da2..b914a06079 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -1,18 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (può essere copiato)"; @@ -31,30 +16,15 @@ /* No comment provided by engineer. */ "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- messaggi vocali fino a 5 minuti.\n- tempo di scomparsa personalizzato.\n- cronologia delle modifiche."; -/* No comment provided by engineer. */ -", " = ", "; - -/* No comment provided by engineer. */ -": " = ": "; - /* No comment provided by engineer. */ "!1 colored!" = "!1 colorato!"; -/* No comment provided by engineer. */ -"." = "."; - -/* No comment provided by engineer. */ -"(" = "("; - /* No comment provided by engineer. */ "(new)" = "(nuovo)"; /* No comment provided by engineer. */ "(this device v%@)" = "(questo dispositivo v%@)"; -/* No comment provided by engineer. */ -")" = ")"; - /* No comment provided by engineer. */ "[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Contribuisci](https://github.com/simplex-chat/simplex-chat#contribute)"; @@ -62,13 +32,13 @@ "[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)" = "[Stella su GitHub](https://github.com/simplex-chat/simplex-chat)"; +"[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. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Aggiungi un contatto**: per creare il tuo codice QR o link una tantum per il tuo contatto."; +"**Create 1-time link**: to create and share a new invitation link." = "**Aggiungi contatto**: per creare un nuovo link di invito."; /* No comment provided by engineer. */ -"**Create link / QR code** for your contact to use." = "**Crea link / codice QR** da usare per il tuo contatto."; +"**Create group**: to create a new group." = "**Crea gruppo**: per creare un nuovo gruppo."; /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "Chiamata **crittografata e2e**"; @@ -77,26 +47,29 @@ "**e2e encrypted** video call" = "Videochiamata **crittografata e2e**"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Più privato**: controlla messaggi nuovi ogni 20 minuti. Viene condiviso il token del dispositivo con il server di SimpleX Chat, ma non quanti contatti o messaggi hai."; +"**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." = "**Più privato**: controlla messaggi nuovi ogni 20 minuti. Viene condiviso il token del dispositivo con il server di SimpleX Chat, ma non quanti contatti o messaggi hai."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Il più privato**: non usare il server di notifica di SimpleX Chat, controlla i messaggi periodicamente in secondo piano (dipende da quanto spesso usi l'app)."; +"**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." = "**Il più privato**: non usare il server di notifica di SimpleX Chat, controlla i messaggi periodicamente in secondo piano (dipende da quanto spesso usi l'app)."; /* No comment provided by engineer. */ -"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Incolla il link ricevuto** o aprilo nel browser e tocca **Apri in app mobile**."; +"**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Nota bene**: usare lo stesso database su due dispositivi bloccherà la decifrazione dei messaggi dalle tue connessioni, come misura di sicurezza."; /* No comment provided by engineer. */ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Nota bene**: NON potrai recuperare o cambiare la password se la perdi."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Consigliato**: vengono inviati il token del dispositivo e le notifiche al server di notifica di SimpleX Chat, ma non il contenuto del messaggio,la sua dimensione o il suo mittente."; +"**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." = "**Consigliato**: vengono inviati il token del dispositivo e le notifiche al server di notifica di SimpleX Chat, ma non il contenuto del messaggio,la sua dimensione o il suo mittente."; /* No comment provided by engineer. */ -"**Scan QR code**: to connect to your contact in person or via video call." = "**Scansiona codice QR**: per connetterti al contatto di persona o via videochiamata."; +"**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. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Attenzione**: le notifiche push istantanee richiedono una password salvata nel portachiavi."; +/* No comment provided by engineer. */ +"**Warning**: the archive will be removed." = "**Attenzione**: l'archivio verrà rimosso."; + /* No comment provided by engineer. */ "*bold*" = "\\*grassetto*"; @@ -131,13 +104,16 @@ "%@ and %@" = "%@ e %@"; /* No comment provided by engineer. */ -"%@ and %@ connected" = "%@ e %@ sono connessi/e"; +"%@ and %@ connected" = "%@ e %@ si sono connessi/e"; /* copied message info, <sender> at <time> */ "%@ at %@:" = "%1$@ alle %2$@:"; /* No comment provided by engineer. */ -"%@ connected" = "%@ si è connesso/a"; +"%@ connected" = "%@ connesso/a"; + +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ scaricati"; /* notification title */ "%@ is connected!" = "%@ è connesso/a!"; @@ -149,16 +125,25 @@ "%@ is verified" = "%@ è verificato/a"; /* No comment provided by engineer. */ -"%@ servers" = "Server %@"; +"%@ server" = "%@ server"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ server"; + +/* No comment provided by engineer. */ +"%@ uploaded" = "%@ caricati"; /* notification title */ "%@ wants to connect!" = "%@ si vuole connettere!"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + /* No comment provided by engineer. */ "%@, %@ and %lld members" = "%@, %@ e %lld membri"; /* No comment provided by engineer. */ -"%@, %@ and %lld other members connected" = "%@, %@ e altri %lld membri sono connessi"; +"%@, %@ and %lld other members connected" = "%@, %@ e altri %lld membri si sono connessi"; /* copied message info */ "%@:" = "%@:"; @@ -166,9 +151,24 @@ /* time interval */ "%d days" = "%d giorni"; +/* forward confirmation reason */ +"%d file(s) are still being downloaded." = "%d file è/sono ancora in scaricamento."; + +/* forward confirmation reason */ +"%d file(s) failed to download." = "%d file ha/hanno fallito lo scaricamento."; + +/* forward confirmation reason */ +"%d file(s) were deleted." = "%d file è/sono stato/i eliminato/i."; + +/* forward confirmation reason */ +"%d file(s) were not downloaded." = "%d file non è/sono stato/i scaricato/i."; + /* time interval */ "%d hours" = "%d ore"; +/* alert title */ +"%d messages not forwarded" = "%d messaggi non inoltrati"; + /* time interval */ "%d min" = "%d min"; @@ -178,6 +178,9 @@ /* time interval */ "%d sec" = "%d sec"; +/* delete after time */ +"%d seconds(s)" = "%d secondo/i"; + /* integrity error chat item */ "%d skipped message(s)" = "%d messaggio/i saltato/i"; @@ -205,6 +208,9 @@ /* No comment provided by engineer. */ "%lld messages blocked" = "%lld messaggi bloccati"; +/* No comment provided by engineer. */ +"%lld messages blocked by admin" = "%lld messaggi bloccati dall'amministratore"; + /* No comment provided by engineer. */ "%lld messages marked deleted" = "%lld messaggi contrassegnati eliminati"; @@ -217,9 +223,6 @@ /* No comment provided by engineer. */ "%lld new interface languages" = "%lld nuove lingue dell'interfaccia"; -/* No comment provided by engineer. */ -"%lld second(s)" = "%lld secondo/i"; - /* No comment provided by engineer. */ "%lld seconds" = "%lld secondi"; @@ -265,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 giorno"; /* time interval */ @@ -274,15 +278,23 @@ /* No comment provided by engineer. */ "1 minute" = "1 minuto"; -/* time interval */ +/* delete after time +time interval */ "1 month" = "1 mese"; -/* time interval */ +/* delete after time +time interval */ "1 week" = "1 settimana"; +/* delete after time */ +"1 year" = "1 anno"; + /* No comment provided by engineer. */ "1-time link" = "Link una tantum"; +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Il link una tantum può essere usato *con un solo contatto* - condividilo di persona o tramite qualsiasi messenger."; + /* No comment provided by engineer. */ "5 minutes" = "5 minuti"; @@ -317,10 +329,7 @@ "Abort changing address?" = "Interrompere il cambio di indirizzo?"; /* No comment provided by engineer. */ -"About SimpleX" = "Riguardo SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "Info sull'indirizzo SimpleX"; +"About operators" = "Info sugli operatori"; /* No comment provided by engineer. */ "About SimpleX Chat" = "Riguardo SimpleX Chat"; @@ -329,101 +338,202 @@ "above, then choose:" = "sopra, quindi scegli:"; /* No comment provided by engineer. */ -"Accent color" = "Colore principale"; +"Accent" = "Principale"; /* accept contact request via notification - accept incoming call via notification */ +accept incoming call via notification +swipe action */ "Accept" = "Accetta"; +/* No comment provided by engineer. */ +"Accept conditions" = "Accetta le condizioni"; + /* No comment provided by engineer. */ "Accept connection request?" = "Accettare la richiesta di connessione?"; /* notification body */ "Accept contact request from %@?" = "Accettare la richiesta di contatto da %@?"; -/* accept contact request via notification */ +/* accept contact request via notification +swipe action */ "Accept incognito" = "Accetta in incognito"; /* call status */ "accepted call" = "chiamata accettata"; +/* No comment provided by engineer. */ +"Accepted conditions" = "Condizioni accettate"; + +/* chat list item title */ +"accepted invitation" = "invito accettato"; + +/* No comment provided by engineer. */ +"Acknowledged" = "Riconosciuto"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Errori di riconoscimento"; + +/* token status text */ +"Active" = "Attivo"; + +/* No comment provided by engineer. */ +"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."; /* No comment provided by engineer. */ -"Add preset servers" = "Aggiungi server preimpostati"; +"Add friends" = "Aggiungi amici"; + +/* No comment provided by engineer. */ +"Add list" = "Aggiungi elenco"; /* No comment provided by engineer. */ "Add profile" = "Aggiungi profilo"; /* No comment provided by engineer. */ -"Add server…" = "Aggiungi server…"; +"Add server" = "Aggiungi server"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Aggiungi server scansionando codici QR."; +/* No comment provided by engineer. */ +"Add team members" = "Aggiungi membri del team"; + /* No comment provided by engineer. */ "Add to another device" = "Aggiungi ad un altro dispositivo"; +/* No comment provided by engineer. */ +"Add to list" = "Aggiungi ad un elenco"; + /* No comment provided by engineer. */ "Add welcome message" = "Aggiungi messaggio di benvenuto"; +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "Aggiungi i membri del tuo team alle conversazioni."; + +/* No comment provided by engineer. */ +"Added media & file servers" = "Server di multimediali e file aggiunti"; + +/* No comment provided by engineer. */ +"Added message servers" = "Server dei messaggi aggiunti"; + +/* No comment provided by engineer. */ +"Additional accent" = "Principale aggiuntivo"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Principale aggiuntivo 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Secondario aggiuntivo"; + /* No comment provided by engineer. */ "Address" = "Indirizzo"; /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "Il cambio di indirizzo verrà interrotto. Verrà usato il vecchio indirizzo di ricezione."; +/* No comment provided by engineer. */ +"Address or 1-time link?" = "Indirizzo o link una tantum?"; + +/* No comment provided by engineer. */ +"Address settings" = "Impostazioni dell'indirizzo"; + /* member role */ "admin" = "amministratore"; +/* feature role */ +"admins" = "amministratori"; + +/* No comment provided by engineer. */ +"Admins can block a member for all." = "Gli amministratori possono bloccare un membro per tutti."; + /* No comment provided by engineer. */ "Admins can create the links to join groups." = "Gli amministratori possono creare i link per entrare nei gruppi."; /* No comment provided by engineer. */ "Advanced network settings" = "Impostazioni di rete avanzate"; +/* No comment provided by engineer. */ +"Advanced settings" = "Impostazioni avanzate"; + /* chat item text */ "agreeing encryption for %@…" = "concordando la crittografia per %@…"; /* chat item text */ "agreeing encryption…" = "concordando la crittografia…"; +/* No comment provided by engineer. */ +"All" = "Tutte"; + /* No comment provided by engineer. */ "All app data is deleted." = "Tutti i dati dell'app vengono eliminati."; /* No comment provided by engineer. */ "All chats and messages will be deleted - this cannot be undone!" = "Tutte le chat e i messaggi verranno eliminati. Non è reversibile!"; +/* alert message */ +"All chats will be removed from the list %@, and the list deleted." = "Tutte le chat verranno rimosse dall'elenco %@ e l'elenco eliminato."; + /* No comment provided by engineer. */ "All data is erased when it is entered." = "Tutti i dati vengono cancellati quando inserito."; +/* No comment provided by engineer. */ +"All data is kept private on your device." = "Tutti i dati sono privati, nel tuo dispositivo."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Tutti i membri del gruppo resteranno connessi."; +/* feature role */ +"all members" = "tutti i membri"; + +/* No comment provided by engineer. */ +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Tutti i messaggi e i file vengono inviati **crittografati end-to-end**, con sicurezza resistenti alla quantistica nei messaggi diretti."; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone!" = "Tutti i messaggi verranno eliminati, non è reversibile!"; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Tutti i messaggi verranno eliminati, non è reversibile! I messaggi verranno eliminati SOLO per te."; /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Tutti i nuovi messaggi da %@ verrranno nascosti!"; +/* profile dropdown */ +"All profiles" = "Tutti gli profili"; + +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Tutte le segnalazioni verranno archiviate per te."; + +/* No comment provided by engineer. */ +"All servers" = "Tutti i server"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Tutti i tuoi contatti resteranno connessi."; /* No comment provided by engineer. */ "All your contacts will remain connected. Profile update will be sent to your contacts." = "Tutti i tuoi contatti resteranno connessi. L'aggiornamento del profilo verrà inviato ai tuoi contatti."; +/* No comment provided by engineer. */ +"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Tutti i tuoi contatti, le conversazioni e i file verranno criptati in modo sicuro e caricati in blocchi sui relay XFTP configurati."; + /* No comment provided by engineer. */ "Allow" = "Consenti"; /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Consenti le chiamate solo se il tuo contatto le consente."; +/* No comment provided by engineer. */ +"Allow calls?" = "Consentire le chiamate?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "Consenti i messaggi a tempo solo se il contatto li consente a te."; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you." = "Consenti l'eliminazione irreversibile dei messaggi solo se il contatto la consente a te."; +"Allow downgrade" = "Consenti downgrade"; + +/* 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 message reactions only if your contact allows them." = "Consenti reazioni ai messaggi solo se il tuo contatto le consente."; @@ -438,11 +548,20 @@ "Allow sending disappearing messages." = "Permetti l'invio di messaggi a tempo."; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages." = "Permetti di eliminare irreversibilmente i messaggi inviati."; +"Allow sharing" = "Consenti la condivisione"; + +/* No comment provided by engineer. */ +"Allow to irreversibly delete sent messages. (24 hours)" = "Permetti di eliminare irreversibilmente i messaggi inviati. (24 ore)"; + +/* No comment provided by engineer. */ +"Allow to report messsages to moderators." = "Consenti di segnalare messaggi ai moderatori."; /* No comment provided by engineer. */ "Allow to send files and media." = "Consenti l'invio di file e contenuti multimediali."; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "Consenti di inviare link di SimpleX."; + /* No comment provided by engineer. */ "Allow to send voice messages." = "Permetti l'invio di messaggi vocali."; @@ -459,7 +578,7 @@ "Allow your contacts to call you." = "Consenti ai tuoi contatti di chiamarti."; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages." = "Permetti ai tuoi contatti di eliminare irreversibilmente i messaggi inviati."; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Permetti ai tuoi contatti di eliminare irreversibilmente i messaggi inviati. (24 ore)"; /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Permetti ai tuoi contatti di inviare messaggi a tempo."; @@ -479,6 +598,9 @@ /* pref value */ "always" = "sempre"; +/* No comment provided by engineer. */ +"Always use private routing." = "Usa sempre l'instradamento privato."; + /* No comment provided by engineer. */ "Always use relay" = "Connetti via relay"; @@ -488,15 +610,27 @@ /* No comment provided by engineer. */ "and %lld other events" = "e altri %lld eventi"; +/* report reason */ +"Another reason" = "Altro motivo"; + /* 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: %@"; +/* No comment provided by engineer. */ +"App data migration" = "Migrazione dati dell'app"; + /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "L'app cripta i nuovi file locali (eccetto i video)."; +/* No comment provided by engineer. */ +"App group:" = "Gruppo app:"; + /* No comment provided by engineer. */ "App icon" = "Icona app"; @@ -506,6 +640,9 @@ /* No comment provided by engineer. */ "App passcode is replaced with self-destruct passcode." = "Il codice di accesso dell'app viene sostituito da un codice di autodistruzione."; +/* No comment provided by engineer. */ +"App session" = "Sessione dell'app"; + /* No comment provided by engineer. */ "App version" = "Versione dell'app"; @@ -515,9 +652,51 @@ /* No comment provided by engineer. */ "Appearance" = "Aspetto"; +/* No comment provided by engineer. */ +"Apply" = "Applica"; + +/* No comment provided by engineer. */ +"Apply to" = "Applica a"; + +/* No comment provided by engineer. */ +"Archive" = "Archivia"; + +/* No comment provided by engineer. */ +"Archive %lld reports?" = "Archiviare %lld segnalazioni?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "Archiviare tutte le segnalazioni?"; + +/* No comment provided by engineer. */ +"Archive and upload" = "Archivia e carica"; + +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Archivia contatti per chattare più tardi."; + +/* No comment provided by engineer. */ +"Archive report" = "Archivia la segnalazione"; + +/* No comment provided by engineer. */ +"Archive report?" = "Archiviare la segnalazione?"; + +/* swipe action */ +"Archive reports" = "Archivia segnalazioni"; + +/* No comment provided by engineer. */ +"Archived contacts" = "Contatti archiviati"; + +/* No comment provided by engineer. */ +"archived report" = "segnalazione archiviata"; + +/* No comment provided by engineer. */ +"Archiving database" = "Archiviazione del database"; + /* No comment provided by engineer. */ "Attach" = "Allega"; +/* No comment provided by engineer. */ +"attempts" = "tentativi"; + /* No comment provided by engineer. */ "Audio & video calls" = "Chiamate audio e video"; @@ -545,18 +724,27 @@ /* No comment provided by engineer. */ "Authentication unavailable" = "Autenticazione non disponibile"; +/* member role */ +"author" = "autore"; + /* No comment provided by engineer. */ "Auto-accept" = "Accetta automaticamente"; /* No comment provided by engineer. */ -"Auto-accept contact requests" = "Auto-accetta richieste di contatto"; +"Auto-accept contact requests" = "Auto-accetta le richieste di contatto"; /* No comment provided by engineer. */ -"Auto-accept images" = "Auto-accetta immagini"; +"Auto-accept images" = "Auto-accetta le immagini"; + +/* alert title */ +"Auto-accept settings" = "Accetta automaticamente le impostazioni"; /* No comment provided by engineer. */ "Back" = "Indietro"; +/* No comment provided by engineer. */ +"Background" = "Sfondo"; + /* No comment provided by engineer. */ "Bad desktop address" = "Indirizzo desktop errato"; @@ -572,15 +760,45 @@ /* No comment provided by engineer. */ "Bad message ID" = "ID del messaggio errato"; +/* No comment provided by engineer. */ +"Better calls" = "Chiamate migliorate"; + /* No comment provided by engineer. */ "Better groups" = "Gruppi migliorati"; +/* No comment provided by engineer. */ +"Better groups performance" = "Prestazioni dei gruppi migliorate"; + +/* No comment provided by engineer. */ +"Better message dates." = "Date dei messaggi migliorate."; + /* No comment provided by engineer. */ "Better messages" = "Messaggi migliorati"; +/* No comment provided by engineer. */ +"Better networking" = "Rete migliorata"; + +/* No comment provided by engineer. */ +"Better notifications" = "Notifiche migliorate"; + +/* No comment provided by engineer. */ +"Better privacy and security" = "Privacy e sicurezza migliori"; + +/* No comment provided by engineer. */ +"Better security ✅" = "Sicurezza migliorata ✅"; + +/* No comment provided by engineer. */ +"Better user experience" = "Esperienza utente migliorata"; + +/* No comment provided by engineer. */ +"Black" = "Nero"; + /* No comment provided by engineer. */ "Block" = "Blocca"; +/* No comment provided by engineer. */ +"Block for all" = "Blocca per tutti"; + /* No comment provided by engineer. */ "Block group members" = "Blocca i membri dei gruppi"; @@ -588,11 +806,30 @@ "Block member" = "Blocca membro"; /* No comment provided by engineer. */ -"Block member?" = "Bloccare il membro?"; +"Block member for all?" = "Bloccare il membro per tutti?"; /* No comment provided by engineer. */ +"Block member?" = "Bloccare il membro?"; + +/* marked deleted chat item preview text */ "blocked" = "bloccato"; +/* rcv group event chat item */ +"blocked %@" = "ha bloccato %@"; + +/* blocked chat item +marked deleted chat item preview text */ +"blocked by admin" = "bloccato dall'amministratore"; + +/* No comment provided by engineer. */ +"Blocked by admin" = "Bloccato dall'amministratore"; + +/* No comment provided by engineer. */ +"Blur for better privacy." = "Sfoca per una privacy maggiore."; + +/* No comment provided by engineer. */ +"Blur media" = "Sfocatura dei file multimediali"; + /* No comment provided by engineer. */ "bold" = "grassetto"; @@ -600,7 +837,7 @@ "Both you and your contact can add message reactions." = "Sia tu che il tuo contatto potete aggiungere reazioni ai messaggi."; /* No comment provided by engineer. */ -"Both you and your contact can irreversibly delete sent messages." = "Sia tu che il tuo contatto potete eliminare irreversibilmente i messaggi inviati."; +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "Sia tu che il tuo contatto potete eliminare irreversibilmente i messaggi inviati. (24 ore)"; /* No comment provided by engineer. */ "Both you and your contact can make calls." = "Sia tu che il tuo contatto potete effettuare chiamate."; @@ -614,9 +851,24 @@ /* No comment provided by engineer. */ "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Bulgaro, finlandese, tailandese e ucraino - grazie agli utenti e a [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; +/* No comment provided by engineer. */ +"Business address" = "Indirizzo di lavoro"; + +/* No comment provided by engineer. */ +"Business chats" = "Chat di lavoro"; + +/* No comment provided by engineer. */ +"Businesses" = "Lavorative"; + /* 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"; + /* No comment provided by engineer. */ "Call already ended!" = "Chiamata già terminata!"; @@ -633,7 +885,16 @@ "Calls" = "Chiamate"; /* No comment provided by engineer. */ -"Can't delete user profile!" = "Impossibile eliminare il profilo utente!"; +"Calls prohibited!" = "Chiamate proibite!"; + +/* No comment provided by engineer. */ +"Camera not available" = "Fotocamera non disponibile"; + +/* No comment provided by engineer. */ +"Can't call contact" = "Impossibile chiamare il contatto"; + +/* No comment provided by engineer. */ +"Can't call member" = "Impossibile chiamare il membro"; /* No comment provided by engineer. */ "Can't invite contact!" = "Impossibile invitare il contatto!"; @@ -642,8 +903,15 @@ "Can't invite contacts!" = "Impossibile invitare i contatti!"; /* No comment provided by engineer. */ +"Can't message member" = "Impossibile inviare un messaggio al membro"; + +/* alert action +alert button */ "Cancel" = "Annulla"; +/* No comment provided by engineer. */ +"Cancel migration" = "Annulla migrazione"; + /* feature offered item */ "cancelled %@" = "annullato %@"; @@ -651,11 +919,26 @@ "Cannot access keychain to save database password" = "Impossibile accedere al portachiavi per salvare la password del database"; /* No comment provided by engineer. */ +"Cannot forward message" = "Impossibile inoltrare il messaggio"; + +/* alert title */ "Cannot receive file" = "Impossibile ricevere il file"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Quota superata - il destinatario non ha ricevuto i messaggi precedentemente inviati."; + +/* No comment provided by engineer. */ +"Cellular" = "Mobile"; + /* No comment provided by engineer. */ "Change" = "Cambia"; +/* alert title */ +"Change automatic message deletion?" = "Cambiare l'eliminazione automatica dei messaggi?"; + +/* authentication reason */ +"Change chat profiles" = "Modifica profili utente"; + /* No comment provided by engineer. */ "Change database passphrase?" = "Cambiare password del database?"; @@ -681,7 +964,7 @@ "Change self-destruct mode" = "Cambia modalità di autodistruzione"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Cambia codice di autodistruzione"; /* chat item text */ @@ -691,7 +974,7 @@ "changed role of %@ to %@" = "ha cambiato il ruolo di %1$@ in %2$@"; /* rcv group event chat item */ -"changed your role to %@" = "cambiato il tuo ruolo in %@"; +"changed your role to %@" = "ha cambiato il tuo ruolo in %@"; /* chat item text */ "changing address for %@…" = "cambio indirizzo per %@…"; @@ -700,7 +983,16 @@ "changing address…" = "cambio indirizzo…"; /* No comment provided by engineer. */ -"Chat archive" = "Archivio chat"; +"Chat" = "Chat"; + +/* No comment provided by engineer. */ +"Chat already exists" = "La chat esiste già"; + +/* No comment provided by engineer. */ +"Chat already exists!" = "La chat esiste già!"; + +/* No comment provided by engineer. */ +"Chat colors" = "Colori della chat"; /* No comment provided by engineer. */ "Chat console" = "Console della chat"; @@ -711,6 +1003,9 @@ /* No comment provided by engineer. */ "Chat database deleted" = "Database della chat eliminato"; +/* No comment provided by engineer. */ +"Chat database exported" = "Database della chat esportato"; + /* No comment provided by engineer. */ "Chat database imported" = "Database della chat importato"; @@ -720,18 +1015,51 @@ /* No comment provided by engineer. */ "Chat is stopped" = "Chat fermata"; +/* 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." = "La chat è ferma. Se hai già usato questo database su un altro dispositivo, dovresti trasferirlo prima di avviare la chat."; + +/* No comment provided by engineer. */ +"Chat list" = "Elenco delle chat"; + +/* No comment provided by engineer. */ +"Chat migrated!" = "Chat migrata!"; + /* No comment provided by engineer. */ "Chat preferences" = "Preferenze della chat"; +/* alert message */ +"Chat preferences were changed." = "Le preferenze della chat sono state cambiate."; + +/* No comment provided by engineer. */ +"Chat profile" = "Profilo utente"; + +/* No comment provided by engineer. */ +"Chat theme" = "Tema della chat"; + +/* No comment provided by engineer. */ +"Chat will be deleted for all members - this cannot be undone!" = "La chat verrà eliminata per tutti i membri, non è reversibile!"; + +/* No comment provided by engineer. */ +"Chat will be deleted for you - this cannot be undone!" = "La chat verrà eliminata solo per te, non è reversibile!"; + /* No comment provided by engineer. */ "Chats" = "Chat"; /* 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 title */ "Check server address and try again." = "Controlla l'indirizzo del server e riprova."; /* No comment provided by engineer. */ "Chinese and Spanish interface" = "Interfaccia cinese e spagnola"; +/* No comment provided by engineer. */ +"Choose _Migrate from another device_ on the new device and scan QR code." = "Scegli _Migra da un altro dispositivo_ sul nuovo dispositivo e scansione il codice QR."; + /* No comment provided by engineer. */ "Choose file" = "Scegli file"; @@ -739,6 +1067,15 @@ "Choose from library" = "Scegli dalla libreria"; /* No comment provided by engineer. */ +"Chunks deleted" = "Blocchi eliminati"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Blocchi scaricati"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Blocchi inviati"; + +/* swipe action */ "Clear" = "Svuota"; /* No comment provided by engineer. */ @@ -747,14 +1084,29 @@ /* No comment provided by engineer. */ "Clear conversation?" = "Svuotare la conversazione?"; +/* No comment provided by engineer. */ +"Clear group?" = "Svuotare il gruppo?"; + +/* No comment provided by engineer. */ +"Clear or delete group?" = "Svuotare o eliminare il gruppo?"; + +/* No comment provided by engineer. */ +"Clear private notes?" = "Svuotare le note private?"; + /* No comment provided by engineer. */ "Clear verification" = "Annulla la verifica"; +/* No comment provided by engineer. */ +"Color chats with the new themes." = "Colora le chat con i nuovi temi."; + +/* No comment provided by engineer. */ +"Color mode" = "Modalità di colore"; + /* No comment provided by engineer. */ "colored" = "colorato"; -/* No comment provided by engineer. */ -"Colors" = "Colori"; +/* report reason */ +"Community guidelines violation" = "Violazione delle linee guida della comunità"; /* server test step */ "Compare file" = "Confronta file"; @@ -765,17 +1117,53 @@ /* No comment provided by engineer. */ "complete" = "completo"; +/* No comment provided by engineer. */ +"Completed" = "Completato"; + +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "Condizioni accettate il: %@."; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "Le condizioni sono state accettate per gli operatori: **%@**."; + +/* No comment provided by engineer. */ +"Conditions are already accepted for these operator(s): **%@**." = "Le condizioni sono già state accettate per i seguenti operatori: **%@**."; + +/* No comment provided by engineer. */ +"Conditions of use" = "Condizioni d'uso"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "Le condizioni verranno accettate per gli operatori: **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "Le condizioni verranno accettate il: %@."; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "Le condizioni verranno accettate automaticamente per gli operatori attivi il: %@."; + /* No comment provided by engineer. */ "Configure ICE servers" = "Configura server ICE"; +/* No comment provided by engineer. */ +"Configure server operators" = "Configura gli operatori dei server"; + /* No comment provided by engineer. */ "Confirm" = "Conferma"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "Confermare l'eliminazione del contatto?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "Conferma aggiornamenti database"; /* No comment provided by engineer. */ -"Confirm new passphrase…" = "Conferma password nuova…"; +"Confirm files from unknown servers." = "Conferma i file da server sconosciuti."; + +/* No comment provided by engineer. */ +"Confirm network settings" = "Conferma le impostazioni di rete"; + +/* No comment provided by engineer. */ +"Confirm new passphrase…" = "Conferma nuova password…"; /* No comment provided by engineer. */ "Confirm Passcode" = "Conferma il codice di accesso"; @@ -783,9 +1171,21 @@ /* No comment provided by engineer. */ "Confirm password" = "Conferma password"; +/* No comment provided by engineer. */ +"Confirm that you remember database passphrase to migrate it." = "Conferma che ricordi la password del database da migrare."; + +/* No comment provided by engineer. */ +"Confirm upload" = "Conferma caricamento"; + +/* token status text */ +"Confirmed" = "Confermato"; + /* server test step */ "Connect" = "Connetti"; +/* No comment provided by engineer. */ +"Connect automatically" = "Connetti automaticamente"; + /* No comment provided by engineer. */ "Connect incognito" = "Connetti in incognito"; @@ -795,6 +1195,9 @@ /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "connettiti agli sviluppatori di SimpleX Chat."; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Connettiti più velocemente ai tuoi amici."; + /* No comment provided by engineer. */ "Connect to yourself?" = "Connettersi a te stesso?"; @@ -810,9 +1213,6 @@ /* No comment provided by engineer. */ "Connect via link" = "Connetti via link"; -/* No comment provided by engineer. */ -"Connect via link / QR code" = "Connetti via link / codice QR"; - /* No comment provided by engineer. */ "Connect via one-time link" = "Connetti via link una tantum"; @@ -822,18 +1222,27 @@ /* No comment provided by engineer. */ "connected" = "connesso/a"; +/* No comment provided by engineer. */ +"Connected" = "Connesso"; + /* No comment provided by engineer. */ "Connected desktop" = "Desktop connesso"; /* rcv group event chat item */ "connected directly" = "si è connesso/a direttamente"; +/* No comment provided by engineer. */ +"Connected servers" = "Server connessi"; + /* No comment provided by engineer. */ "Connected to desktop" = "Connesso al desktop"; /* No comment provided by engineer. */ "connecting" = "in connessione"; +/* No comment provided by engineer. */ +"Connecting" = "In connessione"; + /* No comment provided by engineer. */ "connecting (accepted)" = "in connessione (accettato)"; @@ -855,15 +1264,24 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Connessione al server… (errore: %@)"; +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "In collegamento con il contatto, attendi o controlla più tardi!"; + /* No comment provided by engineer. */ "Connecting to desktop" = "Connessione al desktop"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "in connessione…"; /* No comment provided by engineer. */ "Connection" = "Connessione"; +/* No comment provided by engineer. */ +"Connection and servers status." = "Stato della connessione e dei server."; + +/* No comment provided by engineer. */ +"Connection blocked" = "Connessione bloccata"; + /* No comment provided by engineer. */ "Connection error" = "Errore di connessione"; @@ -873,24 +1291,51 @@ /* chat list item title (it should not be shown */ "connection established" = "connessione stabilita"; +/* No comment provided by engineer. */ +"Connection is blocked by server operator:\n%@" = "La connessione è bloccata dall'operatore del server:\n%@"; + +/* No comment provided by engineer. */ +"Connection not ready." = "Connessione non pronta."; + +/* No comment provided by engineer. */ +"Connection notifications" = "Notifiche di connessione"; + /* No comment provided by engineer. */ "Connection request sent!" = "Richiesta di connessione inviata!"; +/* No comment provided by engineer. */ +"Connection requires encryption renegotiation." = "La connessione richiede la rinegoziazione della crittografia."; + +/* No comment provided by engineer. */ +"Connection security" = "Sicurezza della connessione"; + /* No comment provided by engineer. */ "Connection terminated" = "Connessione terminata"; /* No comment provided by engineer. */ "Connection timeout" = "Connessione scaduta"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "Connessione con il desktop fermata"; + /* connection information */ "connection:%@" = "connessione:% @"; +/* No comment provided by engineer. */ +"Connections" = "Connessioni"; + +/* profile update event chat item */ +"contact %@ changed to %@" = "contatto %1$@ cambiato in %2$@"; + /* No comment provided by engineer. */ "Contact allows" = "Il contatto lo consente"; /* No comment provided by engineer. */ "Contact already exists" = "Il contatto esiste già"; +/* No comment provided by engineer. */ +"Contact deleted!" = "Contatto eliminato!"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "il contatto ha la crittografia e2e"; @@ -904,7 +1349,7 @@ "Contact is connected" = "Il contatto è connesso"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "Il contatto non è ancora connesso!"; +"Contact is deleted." = "Il contatto è stato eliminato."; /* No comment provided by engineer. */ "Contact name" = "Nome del contatto"; @@ -912,21 +1357,36 @@ /* No comment provided by engineer. */ "Contact preferences" = "Preferenze del contatto"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "Il contatto verrà eliminato - non è reversibile!"; + /* No comment provided by engineer. */ "Contacts" = "Contatti"; /* No comment provided by engineer. */ "Contacts can mark messages for deletion; you will be able to view them." = "I contatti possono contrassegnare i messaggi per l'eliminazione; potrai vederli."; +/* blocking reason */ +"Content violates conditions of use" = "Il contenuto viola le condizioni di utilizzo"; + /* No comment provided by engineer. */ "Continue" = "Continua"; -/* chat item action */ +/* No comment provided by engineer. */ +"Conversation deleted!" = "Conversazione eliminata!"; + +/* No comment provided by engineer. */ "Copy" = "Copia"; +/* No comment provided by engineer. */ +"Copy error" = "Copia errore"; + /* No comment provided by engineer. */ "Core version: v%@" = "Versione core: v%@"; +/* No comment provided by engineer. */ +"Corner" = "Angolo"; + /* No comment provided by engineer. */ "Correct name to %@?" = "Correggere il nome a %@?"; @@ -934,10 +1394,10 @@ "Create" = "Crea"; /* No comment provided by engineer. */ -"Create a group using a random profile." = "Crea un gruppo usando un profilo casuale."; +"Create 1-time link" = "Crea link una tantum"; /* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Crea un indirizzo per consentire alle persone di connettersi con te."; +"Create a group using a random profile." = "Crea un gruppo usando un profilo casuale."; /* server test step */ "Create file" = "Crea file"; @@ -952,10 +1412,10 @@ "Create link" = "Crea link"; /* No comment provided by engineer. */ -"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Crea un nuovo profilo nell'[app desktop](https://simplex.chat/downloads/). 💻"; +"Create list" = "Crea elenco"; /* No comment provided by engineer. */ -"Create one-time invitation link" = "Crea link di invito una tantum"; +"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Crea un nuovo profilo nell'[app desktop](https://simplex.chat/downloads/). 💻"; /* No comment provided by engineer. */ "Create profile" = "Crea profilo"; @@ -973,17 +1433,35 @@ "Create your profile" = "Crea il tuo profilo"; /* No comment provided by engineer. */ -"Created on %@" = "Creato il %@"; +"Created" = "Creato"; + +/* No comment provided by engineer. */ +"Created at" = "Creato il"; + +/* copied message info */ +"Created at: %@" = "Creato il: %@"; + +/* No comment provided by engineer. */ +"Creating archive link" = "Creazione link dell'archivio"; + +/* No comment provided by engineer. */ +"Creating link…" = "Creazione link…"; /* No comment provided by engineer. */ "creator" = "creatore"; +/* No comment provided by engineer. */ +"Current conditions text couldn't be loaded, you can review conditions via this link:" = "Il testo delle condizioni attuali testo non è stato caricato, puoi consultare le condizioni tramite questo link:"; + /* No comment provided by engineer. */ "Current Passcode" = "Codice di accesso attuale"; /* No comment provided by engineer. */ "Current passphrase…" = "Password attuale…"; +/* No comment provided by engineer. */ +"Current profile" = "Profilo attuale"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Attualmente la dimensione massima supportata è di %@."; @@ -993,9 +1471,18 @@ /* No comment provided by engineer. */ "Custom time" = "Tempo personalizzato"; +/* No comment provided by engineer. */ +"Customizable message shape." = "Forma dei messaggi personalizzabile."; + +/* No comment provided by engineer. */ +"Customize theme" = "Personalizza il tema"; + /* No comment provided by engineer. */ "Dark" = "Scuro"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Colori modalità scura"; + /* No comment provided by engineer. */ "Database downgrade" = "Downgrade del database"; @@ -1056,13 +1543,20 @@ /* time unit */ "days" = "giorni"; +/* No comment provided by engineer. */ +"Debug delivery" = "Debug della consegna"; + /* No comment provided by engineer. */ "Decentralized" = "Decentralizzato"; /* message decrypt error item */ "Decryption error" = "Errore di decifrazione"; -/* pref value */ +/* No comment provided by engineer. */ +"decryption errors" = "errori di decifrazione"; + +/* delete after time +pref value */ "default (%@)" = "predefinito (%@)"; /* No comment provided by engineer. */ @@ -1071,9 +1565,13 @@ /* No comment provided by engineer. */ "default (yes)" = "predefinito (sì)"; -/* chat item action */ +/* alert action +swipe action */ "Delete" = "Elimina"; +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "Eliminare %lld messaggi dei membri?"; + /* No comment provided by engineer. */ "Delete %lld messages?" = "Eliminare %lld messaggi?"; @@ -1093,10 +1591,10 @@ "Delete and notify contact" = "Elimina e avvisa il contatto"; /* No comment provided by engineer. */ -"Delete archive" = "Elimina archivio"; +"Delete chat" = "Elimina chat"; /* No comment provided by engineer. */ -"Delete chat archive?" = "Eliminare l'archivio della chat?"; +"Delete chat messages from your device." = "Elimina i messaggi di chat dal tuo dispositivo."; /* No comment provided by engineer. */ "Delete chat profile" = "Elimina il profilo di chat"; @@ -1104,6 +1602,9 @@ /* No comment provided by engineer. */ "Delete chat profile?" = "Eliminare il profilo di chat?"; +/* No comment provided by engineer. */ +"Delete chat?" = "Eliminare la chat?"; + /* No comment provided by engineer. */ "Delete connection" = "Elimina connessione"; @@ -1111,14 +1612,14 @@ "Delete contact" = "Elimina contatto"; /* No comment provided by engineer. */ -"Delete Contact" = "Elimina contatto"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Eliminare il contatto?\nNon è reversibile!"; +"Delete contact?" = "Eliminare il contatto?"; /* No comment provided by engineer. */ "Delete database" = "Elimina database"; +/* No comment provided by engineer. */ +"Delete database from this device" = "Elimina il database da questo dispositivo"; + /* server test step */ "Delete file" = "Elimina file"; @@ -1149,13 +1650,16 @@ /* No comment provided by engineer. */ "Delete link?" = "Eliminare il link?"; +/* alert title */ +"Delete list?" = "Eliminare l'elenco?"; + /* No comment provided by engineer. */ "Delete member message?" = "Eliminare il messaggio del membro?"; /* No comment provided by engineer. */ "Delete message?" = "Eliminare il messaggio?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "Elimina messaggi"; /* No comment provided by engineer. */ @@ -1168,7 +1672,7 @@ "Delete old database?" = "Eliminare il database vecchio?"; /* No comment provided by engineer. */ -"Delete pending connection" = "Elimina connessione in attesa"; +"Delete or moderate up to 200 messages." = "Elimina o modera fino a 200 messaggi."; /* No comment provided by engineer. */ "Delete pending connection?" = "Eliminare la connessione in attesa?"; @@ -1179,12 +1683,24 @@ /* server test step */ "Delete queue" = "Elimina coda"; +/* No comment provided by engineer. */ +"Delete report" = "Elimina la segnalazione"; + +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Elimina fino a 20 messaggi contemporaneamente."; + /* No comment provided by engineer. */ "Delete user profile?" = "Eliminare il profilo utente?"; +/* No comment provided by engineer. */ +"Delete without notification" = "Elimina senza avvisare"; + /* deleted chat item */ "deleted" = "eliminato"; +/* No comment provided by engineer. */ +"Deleted" = "Eliminato"; + /* No comment provided by engineer. */ "Deleted at" = "Eliminato il"; @@ -1197,6 +1713,12 @@ /* rcv group event chat item */ "deleted group" = "gruppo eliminato"; +/* No comment provided by engineer. */ +"Deletion errors" = "Errori di eliminazione"; + +/* No comment provided by engineer. */ +"Delivered even when Apple drops them." = "Consegnati anche quando Apple li scarta."; + /* No comment provided by engineer. */ "Delivery" = "Consegna"; @@ -1218,9 +1740,27 @@ /* No comment provided by engineer. */ "Desktop devices" = "Dispositivi desktop"; +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "L'indirizzo del server di destinazione di %@ è incompatibile con le impostazioni del server di inoltro %@."; + +/* snd error text */ +"Destination server error: %@" = "Errore del server di destinazione: %@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "La versione del server di destinazione di %@ è incompatibile con il server di inoltro %@."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Statistiche dettagliate"; + +/* No comment provided by engineer. */ +"Details" = "Dettagli"; + /* No comment provided by engineer. */ "Develop" = "Sviluppa"; +/* No comment provided by engineer. */ +"Developer options" = "Opzioni sviluppatore"; + /* No comment provided by engineer. */ "Developer tools" = "Strumenti di sviluppo"; @@ -1246,11 +1786,20 @@ "Direct messages" = "Messaggi diretti"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "I messaggi diretti tra i membri sono vietati in questo gruppo."; +"Direct messages between members are prohibited in this chat." = "I messaggi diretti tra i membri sono vietati in questa chat."; + +/* 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. */ "Disable (keep overrides)" = "Disattiva (mantieni sostituzioni)"; +/* alert title */ +"Disable automatic message deletion?" = "Disattivare l'eliminazione automatica dei messaggi?"; + +/* alert button */ +"Disable delete messages" = "Disattiva eliminazione messaggi"; + /* No comment provided by engineer. */ "Disable for all" = "Disattiva per tutti"; @@ -1260,6 +1809,9 @@ /* No comment provided by engineer. */ "disabled" = "disattivato"; +/* No comment provided by engineer. */ +"Disabled" = "Disattivato"; + /* No comment provided by engineer. */ "Disappearing message" = "Messaggio a tempo"; @@ -1270,7 +1822,7 @@ "Disappearing messages are prohibited in this chat." = "I messaggi a tempo sono vietati in questa chat."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "I messaggi a tempo sono vietati in questo gruppo."; +"Disappearing messages are prohibited." = "I messaggi a tempo sono vietati in questo gruppo."; /* No comment provided by engineer. */ "Disappears at" = "Scompare il"; @@ -1287,39 +1839,94 @@ /* No comment provided by engineer. */ "Discover and join groups" = "Scopri ed unisciti ai gruppi"; +/* No comment provided by engineer. */ +"Discover via local network" = "Individua via rete locale"; + /* No comment provided by engineer. */ "Do it later" = "Fallo dopo"; +/* 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 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."; + +/* No comment provided by engineer. */ +"Do not use credentials with proxy." = "Non usare credenziali con proxy."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "NON usare l'instradamento privato."; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "NON usare SimpleX per chiamate di emergenza."; +/* No comment provided by engineer. */ +"Documents:" = "Documenti:"; + /* No comment provided by engineer. */ "Don't create address" = "Non creare un indirizzo"; /* No comment provided by engineer. */ "Don't enable" = "Non attivare"; +/* No comment provided by engineer. */ +"Don't miss important messages." = "Non perdere messaggi importanti."; + /* No comment provided by engineer. */ "Don't show again" = "Non mostrare più"; +/* No comment provided by engineer. */ +"Done" = "Fatto"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "Esegui downgrade e apri chat"; +/* alert button +chat item action */ +"Download" = "Scarica"; + +/* No comment provided by engineer. */ +"Download errors" = "Errori di scaricamento"; + +/* No comment provided by engineer. */ +"Download failed" = "Scaricamento fallito"; + /* server test step */ "Download file" = "Scarica file"; +/* alert action */ +"Download files" = "Scarica i file"; + +/* No comment provided by engineer. */ +"Downloaded" = "Scaricato"; + +/* No comment provided by engineer. */ +"Downloaded files" = "File scaricati"; + +/* No comment provided by engineer. */ +"Downloading archive" = "Scaricamento archivio"; + +/* No comment provided by engineer. */ +"Downloading link details" = "Scaricamento dettagli del link"; + /* No comment provided by engineer. */ "Duplicate display name!" = "Nome da mostrare doppio!"; /* integrity error chat item */ "duplicate message" = "messaggio duplicato"; +/* No comment provided by engineer. */ +"duplicates" = "doppi"; + /* No comment provided by engineer. */ "Duration" = "Durata"; /* No comment provided by engineer. */ "e2e encrypted" = "crittografato e2e"; +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "Notifiche crittografate E2E."; + /* chat item action */ "Edit" = "Modifica"; @@ -1332,12 +1939,21 @@ /* No comment provided by engineer. */ "Enable (keep overrides)" = "Attiva (mantieni sostituzioni)"; -/* No comment provided by engineer. */ +/* 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"; + +/* No comment provided by engineer. */ +"Enable Flux in Network & servers settings for better metadata privacy." = "Attiva Flux nelle impostazioni \"Rete e server\" per una migliore privacy dei metadati."; + /* No comment provided by engineer. */ "Enable for all" = "Attiva per tutti"; +/* No comment provided by engineer. */ +"Enable in direct chats (BETA)!" = "Attivala nelle chat dirette (BETA)!"; + /* No comment provided by engineer. */ "Enable instant notifications?" = "Attivare le notifiche istantanee?"; @@ -1365,6 +1981,12 @@ /* enabled status */ "enabled" = "attivato"; +/* No comment provided by engineer. */ +"Enabled" = "Attivato"; + +/* No comment provided by engineer. */ +"Enabled for" = "Attivo per"; + /* enabled status */ "enabled for contact" = "attivato per il contatto"; @@ -1389,6 +2011,9 @@ /* notification */ "Encrypted message or another event" = "Messaggio crittografato o altro evento"; +/* notification */ +"Encrypted message: app is stopped" = "Messaggio cifrato: l'app è ferma"; + /* notification */ "Encrypted message: database error" = "Messaggio crittografato: errore del database"; @@ -1434,6 +2059,9 @@ /* chat item text */ "encryption re-negotiation required for %@" = "richiesta rinegoziazione della crittografia per %@"; +/* No comment provided by engineer. */ +"Encryption renegotiation in progress." = "Rinegoziazione della crittografia in corso."; + /* No comment provided by engineer. */ "ended" = "terminata"; @@ -1449,6 +2077,9 @@ /* No comment provided by engineer. */ "Enter Passcode" = "Inserisci il codice di accesso"; +/* No comment provided by engineer. */ +"Enter passphrase" = "Inserisci password"; + /* No comment provided by engineer. */ "Enter passphrase…" = "Inserisci la password…"; @@ -1479,24 +2110,39 @@ /* No comment provided by engineer. */ "Error aborting address change" = "Errore nell'interruzione del cambio di indirizzo"; +/* alert title */ +"Error accepting conditions" = "Errore di accettazione delle condizioni"; + /* No comment provided by engineer. */ "Error accepting contact request" = "Errore nell'accettazione della richiesta di contatto"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Errore nell'accesso al file del database"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Errore di aggiunta membro/i"; +/* alert title */ +"Error adding server" = "Errore di aggiunta del server"; + /* No comment provided by engineer. */ "Error changing address" = "Errore nella modifica dell'indirizzo"; +/* No comment provided by engineer. */ +"Error changing connection profile" = "Errore nel cambio di profilo di connessione"; + /* No comment provided by engineer. */ "Error changing role" = "Errore nel cambio di ruolo"; /* No comment provided by engineer. */ "Error changing setting" = "Errore nella modifica dell'impostazione"; +/* No comment provided by engineer. */ +"Error changing to incognito!" = "Errore nel passaggio a incognito!"; + +/* No comment provided by engineer. */ +"Error checking token status" = "Errore di controllo dello stato del token"; + +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "Errore di connessione al server di inoltro %@. Riprova più tardi."; + /* No comment provided by engineer. */ "Error creating address" = "Errore nella creazione dell'indirizzo"; @@ -1506,12 +2152,21 @@ /* No comment provided by engineer. */ "Error creating group link" = "Errore nella creazione del link del gruppo"; +/* alert title */ +"Error creating list" = "Errore nella creazione dell'elenco"; + /* No comment provided by engineer. */ "Error creating member contact" = "Errore di creazione del contatto"; +/* No comment provided by engineer. */ +"Error creating message" = "Errore di creazione del messaggio"; + /* No comment provided by engineer. */ "Error creating profile!" = "Errore nella creazione del profilo!"; +/* No comment provided by engineer. */ +"Error creating report" = "Errore nella creazione del resoconto"; + /* No comment provided by engineer. */ "Error decrypting file" = "Errore decifrando il file"; @@ -1524,9 +2179,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Errore nell'eliminazione della connessione"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Errore nell'eliminazione del contatto"; - /* No comment provided by engineer. */ "Error deleting database" = "Errore nell'eliminazione del database"; @@ -1539,6 +2191,9 @@ /* No comment provided by engineer. */ "Error deleting user profile" = "Errore nell'eliminazione del profilo utente"; +/* No comment provided by engineer. */ +"Error downloading the archive" = "Errore di scaricamento dell'archivio"; + /* No comment provided by engineer. */ "Error enabling delivery receipts!" = "Errore nell'attivazione delle ricevute di consegna!"; @@ -1551,23 +2206,47 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Errore nell'esportazione del database della chat"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Errore di esportazione del tema: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Errore nell'importazione del database della chat"; /* No comment provided by engineer. */ "Error joining group" = "Errore di ingresso nel gruppo"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Errore nel caricamento dei server %@"; +/* alert title */ +"Error loading servers" = "Errore nel caricamento dei server"; /* No comment provided by engineer. */ +"Error migrating settings" = "Errore nella migrazione delle impostazioni"; + +/* No comment provided by engineer. */ +"Error opening chat" = "Errore di apertura della chat"; + +/* alert title */ "Error receiving file" = "Errore nella ricezione del file"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Errore di riconnessione al server"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Errore di riconnessione ai server"; + +/* alert title */ +"Error registering for notifications" = "Errore di registrazione per le notifiche"; + /* No comment provided by engineer. */ "Error removing member" = "Errore nella rimozione del membro"; +/* alert title */ +"Error reordering lists" = "Errore riordinando gli elenchi"; + /* No comment provided by engineer. */ -"Error saving %@ servers" = "Errore nel salvataggio dei server %@"; +"Error resetting statistics" = "Errore di azzeramento statistiche"; + +/* alert title */ +"Error saving chat list" = "Errore nel salvataggio dell'elenco di chat"; /* No comment provided by engineer. */ "Error saving group profile" = "Errore nel salvataggio del profilo del gruppo"; @@ -1581,9 +2260,18 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "Errore nel salvataggio della password nel portachiavi"; +/* alert title */ +"Error saving servers" = "Errore di salvataggio dei server"; + +/* when migrating */ +"Error saving settings" = "Errore di salvataggio delle impostazioni"; + /* No comment provided by engineer. */ "Error saving user password" = "Errore nel salvataggio della password utente"; +/* No comment provided by engineer. */ +"Error scanning code: %@" = "Errore di scansione del codice: %@"; + /* No comment provided by engineer. */ "Error sending email" = "Errore nell'invio dell'email"; @@ -1603,17 +2291,26 @@ "Error stopping chat" = "Errore nell'interruzione della chat"; /* No comment provided by engineer. */ +"Error switching profile" = "Errore nel cambio di profilo"; + +/* alertTitle */ "Error switching profile!" = "Errore nel cambio di profilo!"; /* No comment provided by engineer. */ "Error synchronizing connection" = "Errore nella sincronizzazione della connessione"; +/* No comment provided by engineer. */ +"Error testing server connection" = "Errore provando la connessione al server"; + /* No comment provided by engineer. */ "Error updating group link" = "Errore nell'aggiornamento del link del gruppo"; /* No comment provided by engineer. */ "Error updating message" = "Errore nell'aggiornamento del messaggio"; +/* alert title */ +"Error updating server" = "Errore di aggiornamento del server"; + /* No comment provided by engineer. */ "Error updating settings" = "Errore nell'aggiornamento delle impostazioni"; @@ -1621,9 +2318,17 @@ "Error updating user privacy" = "Errore nell'aggiornamento della privacy dell'utente"; /* No comment provided by engineer. */ -"Error: " = "Errore: "; +"Error uploading the archive" = "Errore di invio dell'archivio"; /* No comment provided by engineer. */ +"Error verifying passphrase:" = "Errore di verifica della password:"; + +/* No comment provided by engineer. */ +"Error: " = "Errore: "; + +/* alert message +file error text +snd error text */ "Error: %@" = "Errore: %@"; /* No comment provided by engineer. */ @@ -1633,10 +2338,13 @@ "Error: URL is invalid" = "Errore: l'URL non è valido"; /* No comment provided by engineer. */ -"Even when disabled in the conversation." = "Anche quando disattivato nella conversazione."; +"Errors" = "Errori"; + +/* servers error */ +"Errors in servers configuration." = "Errori nella configurazione dei server."; /* No comment provided by engineer. */ -"event happened" = "evento accaduto"; +"Even when disabled in the conversation." = "Anche quando disattivato nella conversazione."; /* No comment provided by engineer. */ "Exit without saving" = "Esci senza salvare"; @@ -1644,15 +2352,27 @@ /* chat item action */ "Expand" = "Espandi"; +/* No comment provided by engineer. */ +"expired" = "scaduto"; + +/* token status text */ +"Expired" = "Scaduto"; + /* No comment provided by engineer. */ "Export database" = "Esporta database"; /* No comment provided by engineer. */ "Export error:" = "Errore di esportazione:"; +/* No comment provided by engineer. */ +"Export theme" = "Esporta tema"; + /* No comment provided by engineer. */ "Exported database archive." = "Archivio database esportato."; +/* No comment provided by engineer. */ +"Exported file doesn't exist" = "Il file esportato non esiste"; + /* No comment provided by engineer. */ "Exporting database archive…" = "Esportazione archivio database…"; @@ -1662,12 +2382,42 @@ /* No comment provided by engineer. */ "Fast and no wait until the sender is online!" = "Veloce e senza aspettare che il mittente sia in linea!"; +/* No comment provided by engineer. */ +"Faster deletion of groups." = "Eliminazione dei gruppi più veloce."; + /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Ingresso più veloce e messaggi più affidabili."; /* No comment provided by engineer. */ +"Faster sending messages." = "Invio dei messaggi più veloce."; + +/* swipe action */ "Favorite" = "Preferito"; +/* No comment provided by engineer. */ +"Favorites" = "Preferite"; + +/* file error alert title */ +"File error" = "Errore del file"; + +/* alert message */ +"File errors:\n%@" = "Errori di file:\n%@"; + +/* file error text */ +"File is blocked by server operator:\n%@." = "Il file è bloccato dall'operatore del server:\n%@."; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "File non trovato - probabilmente è stato eliminato o annullato."; + +/* file error text */ +"File server error: %@" = "Errore del server dei file: %@"; + +/* No comment provided by engineer. */ +"File status" = "Stato del file"; + +/* copied message info */ +"File status: %@" = "Stato del file: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Il file verrà eliminato dai server."; @@ -1680,6 +2430,9 @@ /* No comment provided by engineer. */ "File: %@" = "File: %@"; +/* No comment provided by engineer. */ +"Files" = "File"; + /* No comment provided by engineer. */ "Files & media" = "File e multimediali"; @@ -1687,7 +2440,10 @@ "Files and media" = "File e multimediali"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "File e contenuti multimediali sono vietati in questo gruppo."; +"Files and media are prohibited." = "File e contenuti multimediali sono vietati in questo gruppo."; + +/* No comment provided by engineer. */ +"Files and media not allowed" = "File e multimediali non consentiti"; /* No comment provided by engineer. */ "Files and media prohibited!" = "File e contenuti multimediali vietati!"; @@ -1695,6 +2451,12 @@ /* No comment provided by engineer. */ "Filter unread and favorite chats." = "Filtra le chat non lette e preferite."; +/* No comment provided by engineer. */ +"Finalize migration" = "Finalizza la migrazione"; + +/* No comment provided by engineer. */ +"Finalize migration on another device." = "Finalizza la migrazione su un altro dispositivo."; + /* No comment provided by engineer. */ "Finally, we have them! 🚀" = "Finalmente le abbiamo! 🚀"; @@ -1719,9 +2481,75 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Correzione non supportata dal membro del gruppo"; +/* No comment provided by engineer. */ +"For all moderators" = "Per tutti i moderatori"; + +/* servers error */ +"For chat profile %@:" = "Per il profilo di chat %@:"; + /* No comment provided by engineer. */ "For console" = "Per console"; +/* No comment provided by engineer. */ +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Ad esempio, se il tuo contatto riceve messaggi tramite un server di SimpleX Chat, la tua app li consegnerà tramite un server Flux."; + +/* No comment provided by engineer. */ +"For me" = "Per me"; + +/* No comment provided by engineer. */ +"For private routing" = "Per l'instradamento privato"; + +/* No comment provided by engineer. */ +"For social media" = "Per i social media"; + +/* chat item action */ +"Forward" = "Inoltra"; + +/* alert title */ +"Forward %d message(s)?" = "Inoltrare %d messaggio/i?"; + +/* No comment provided by engineer. */ +"Forward and save messages" = "Inoltra e salva i messaggi"; + +/* alert action */ +"Forward messages" = "Inoltra i messaggi"; + +/* alert message */ +"Forward messages without files?" = "Inoltrare i messaggi senza file?"; + +/* No comment provided by engineer. */ +"Forward up to 20 messages at once." = "Inoltra fino a 20 messaggi alla volta."; + +/* No comment provided by engineer. */ +"forwarded" = "inoltrato"; + +/* No comment provided by engineer. */ +"Forwarded" = "Inoltrato"; + +/* No comment provided by engineer. */ +"Forwarded from" = "Inoltrato da"; + +/* No comment provided by engineer. */ +"Forwarding %lld messages" = "Inoltro di %lld messaggi"; + +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Il server di inoltro %@ non è riuscito a connettersi al server di destinazione %@. Riprova più tardi."; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "L'indirizzo del server di inoltro è incompatibile con le impostazioni di rete: %@."; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "La versione del server di inoltro è incompatibile con le impostazioni di rete: %@."; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Server di inoltro: %1$@\nErrore del server di destinazione: %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Server di inoltro: %1$@\nErrore: %2$@"; + +/* No comment provided by engineer. */ +"Found desktop" = "Desktop trovato"; + /* No comment provided by engineer. */ "French interface" = "Interfaccia francese"; @@ -1731,9 +2559,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Nome completo (facoltativo)"; -/* No comment provided by engineer. */ -"Full name:" = "Nome completo:"; - /* No comment provided by engineer. */ "Fully decentralized – visible only to members." = "Completamente decentralizzato: visibile solo ai membri."; @@ -1743,9 +2568,18 @@ /* No comment provided by engineer. */ "Further reduced battery usage" = "Ulteriore riduzione del consumo della batteria"; +/* No comment provided by engineer. */ +"Get notified when mentioned." = "Ricevi una notifica quando menzionato."; + /* No comment provided by engineer. */ "GIFs and stickers" = "GIF e adesivi"; +/* message preview */ +"Good afternoon!" = "Buon pomeriggio!"; + +/* message preview */ +"Good morning!" = "Buongiorno!"; + /* No comment provided by engineer. */ "Group" = "Gruppo"; @@ -1782,24 +2616,6 @@ /* No comment provided by engineer. */ "Group links" = "Link del gruppo"; -/* No comment provided by engineer. */ -"Group members can add message reactions." = "I membri del gruppo possono aggiungere reazioni ai messaggi."; - -/* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages." = "I membri del gruppo possono eliminare irreversibilmente i messaggi inviati."; - -/* No comment provided by engineer. */ -"Group members can send direct messages." = "I membri del gruppo possono inviare messaggi diretti."; - -/* No comment provided by engineer. */ -"Group members can send disappearing messages." = "I membri del gruppo possono inviare messaggi a tempo."; - -/* No comment provided by engineer. */ -"Group members can send files and media." = "I membri del gruppo possono inviare file e contenuti multimediali."; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "I membri del gruppo possono inviare messaggi vocali."; - /* notification */ "Group message:" = "Messaggio del gruppo:"; @@ -1827,9 +2643,15 @@ /* No comment provided by engineer. */ "Group will be deleted for you - this cannot be undone!" = "Il gruppo verrà eliminato per te. Non è reversibile!"; +/* No comment provided by engineer. */ +"Groups" = "Gruppi"; + /* No comment provided by engineer. */ "Help" = "Aiuto"; +/* No comment provided by engineer. */ +"Help admins moderating their groups." = "Aiuta gli amministratori a moderare i loro gruppi."; + /* No comment provided by engineer. */ "Hidden" = "Nascosta"; @@ -1854,10 +2676,19 @@ /* No comment provided by engineer. */ "History" = "Cronologia"; +/* No comment provided by engineer. */ +"History is not sent to new members." = "La cronologia non viene inviata ai nuovi membri."; + /* time unit */ "hours" = "ore"; /* No comment provided by engineer. */ +"How it affects privacy" = "Come influisce sulla privacy"; + +/* No comment provided by engineer. */ +"How it helps privacy" = "Come aiuta la privacy"; + +/* alert button */ "How it works" = "Come funziona"; /* No comment provided by engineer. */ @@ -1872,15 +2703,15 @@ /* No comment provided by engineer. */ "How to use your servers" = "Come usare i tuoi server"; +/* No comment provided by engineer. */ +"Hungarian interface" = "Interfaccia in ungherese"; + /* No comment provided by engineer. */ "ICE servers (one per line)" = "Server ICE (uno per riga)"; /* No comment provided by engineer. */ "If you can't meet in person, show QR code in a video call, or share the link." = "Se non potete incontrarvi di persona, mostra il codice QR in una videochiamata o condividi il link."; -/* No comment provided by engineer. */ -"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Se non potete incontrarvi di persona, puoi **scansionare il codice QR durante la videochiamata** oppure il tuo contatto può condividere un link di invito."; - /* No comment provided by engineer. */ "If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Se inserisci questo codice all'apertura dell'app, tutti i dati di essa verranno rimossi in modo irreversibile!"; @@ -1903,7 +2734,7 @@ "Immediately" = "Immediatamente"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Immune a spam e abusi"; +"Immune to spam" = "Immune a spam e abusi"; /* No comment provided by engineer. */ "Import" = "Importa"; @@ -1914,15 +2745,45 @@ /* No comment provided by engineer. */ "Import database" = "Importa database"; +/* No comment provided by engineer. */ +"Import failed" = "Importazione fallita"; + +/* No comment provided by engineer. */ +"Import theme" = "Importa tema"; + +/* No comment provided by engineer. */ +"Importing archive" = "Importazione archivio"; + +/* No comment provided by engineer. */ +"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "Consegna migliorata, utilizzo di traffico ridotto.\nAltri miglioramenti sono in arrivo!"; + +/* No comment provided by engineer. */ +"Improved message delivery" = "Consegna dei messaggi migliorata"; + /* No comment provided by engineer. */ "Improved privacy and security" = "Privacy e sicurezza migliorate"; /* No comment provided by engineer. */ "Improved server configuration" = "Configurazione del server migliorata"; +/* No comment provided by engineer. */ +"In order to continue, chat should be stopped." = "Per continuare, la chat deve essere fermata."; + /* No comment provided by engineer. */ "In reply to" = "In risposta a"; +/* No comment provided by engineer. */ +"In-call sounds" = "Suoni nelle chiamate"; + +/* No comment provided by engineer. */ +"inactive" = "inattivo"; + +/* report reason */ +"Inappropriate content" = "Contenuto inappropriato"; + +/* report reason */ +"Inappropriate profile" = "Profilo inappropriato"; + /* No comment provided by engineer. */ "Incognito" = "Incognito"; @@ -1977,14 +2838,32 @@ /* 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)"; +/* No comment provided by engineer. */ +"Instant" = "Istantaneamente"; + /* No comment provided by engineer. */ "Instant push notifications will be hidden!\n" = "Le notifiche push istantanee saranno nascoste!\n"; /* No comment provided by engineer. */ -"Instantly" = "Istantaneamente"; +"Interface" = "Interfaccia"; /* No comment provided by engineer. */ -"Interface" = "Interfaccia"; +"Interface colors" = "Colori dell'interfaccia"; + +/* token status text */ +"Invalid" = "Non valido"; + +/* token status text */ +"Invalid (bad token)" = "Non valido (token corrotto)"; + +/* token status text */ +"Invalid (expired)" = "Non valido (scaduto)"; + +/* token status text */ +"Invalid (unregistered)" = "Non valido (non registrato)"; + +/* token status text */ +"Invalid (wrong topic)" = "Non valido (argomento sbagliato)"; /* invalid chat data */ "invalid chat" = "chat non valida"; @@ -1998,10 +2877,25 @@ /* invalid chat item */ "invalid data" = "dati non validi"; +/* No comment provided by engineer. */ +"Invalid display name!" = "Nome da mostrare non valido!"; + +/* No comment provided by engineer. */ +"Invalid link" = "Link non valido"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Conferma di migrazione non valida"; + /* No comment provided by engineer. */ "Invalid name!" = "Nome non valido!"; /* No comment provided by engineer. */ +"Invalid QR code" = "Codice QR non valido"; + +/* No comment provided by engineer. */ +"Invalid response" = "Risposta non valida"; + +/* alert title */ "Invalid server address!" = "Indirizzo del server non valido!"; /* item status text */ @@ -2013,12 +2907,18 @@ /* group name */ "invitation to group %@" = "invito al gruppo %@"; +/* No comment provided by engineer. */ +"invite" = "invita"; + /* No comment provided by engineer. */ "Invite friends" = "Invita amici"; /* No comment provided by engineer. */ "Invite members" = "Invita membri"; +/* No comment provided by engineer. */ +"Invite to chat" = "Invita in chat"; + /* No comment provided by engineer. */ "Invite to group" = "Invita al gruppo"; @@ -2040,6 +2940,9 @@ /* 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." = "Il portachiavi di iOS verrà usato per archiviare in modo sicuro la password dopo il riavvio dell'app o la modifica della password; consentirà di ricevere notifiche push."; +/* No comment provided by engineer. */ +"IP address" = "Indirizzo IP"; + /* No comment provided by engineer. */ "Irreversible message deletion" = "Eliminazione irreversibile del messaggio"; @@ -2047,7 +2950,7 @@ "Irreversible message deletion is prohibited in this chat." = "L'eliminazione irreversibile dei messaggi è vietata in questa chat."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "L'eliminazione irreversibile dei messaggi è vietata in questo gruppo."; +"Irreversible message deletion is prohibited." = "L'eliminazione irreversibile dei messaggi è vietata in questo gruppo."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Permette di avere molte connessioni anonime senza dati condivisi tra di loro in un unico profilo di chat."; @@ -2058,6 +2961,9 @@ /* No comment provided by engineer. */ "It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Può accadere quando:\n1. I messaggi sono scaduti sul client mittente dopo 2 giorni o sul server dopo 30 giorni.\n2. La decifrazione del messaggio è fallita, perché tu o il tuo contatto avete usato un backup del database vecchio.\n3. La connessione è stata compromessa."; +/* No comment provided by engineer. */ +"It protects your IP address and connections." = "Protegge il tuo indirizzo IP e le connessioni."; + /* 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 (%@)." = "Sembra che tu sia già connesso tramite questo link. In caso contrario, c'è stato un errore (%@)."; @@ -2070,7 +2976,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Interfaccia giapponese"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Entra"; /* No comment provided by engineer. */ @@ -2079,6 +2985,9 @@ /* No comment provided by engineer. */ "Join group" = "Entra nel gruppo"; +/* No comment provided by engineer. */ +"Join group conversations" = "Entra in conversazioni di gruppo"; + /* No comment provided by engineer. */ "Join group?" = "Entrare nel gruppo?"; @@ -2094,9 +3003,18 @@ /* No comment provided by engineer. */ "Joining group" = "Ingresso nel gruppo"; +/* alert action */ +"Keep" = "Tieni"; + +/* No comment provided by engineer. */ +"Keep conversation" = "Tieni la conversazione"; + /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Tieni aperta l'app per usarla dal desktop"; +/* alert title */ +"Keep unused invitation?" = "Tenere l'invito inutilizzato?"; + /* No comment provided by engineer. */ "Keep your connections" = "Mantieni le tue connessioni"; @@ -2112,9 +3030,15 @@ /* No comment provided by engineer. */ "Learn more" = "Maggiori informazioni"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Esci"; +/* No comment provided by engineer. */ +"Leave chat" = "Esci dalla chat"; + +/* No comment provided by engineer. */ +"Leave chat?" = "Uscire dalla chat?"; + /* No comment provided by engineer. */ "Leave group" = "Esci dal gruppo"; @@ -2142,6 +3066,15 @@ /* No comment provided by engineer. */ "Linked desktops" = "Desktop collegati"; +/* swipe action */ +"List" = "Elenco"; + +/* No comment provided by engineer. */ +"List name and emoji should be different for all lists." = "Il nome dell'elenco e l'emoji dovrebbero essere diversi per tutte le liste."; + +/* No comment provided by engineer. */ +"List name..." = "Nome elenco..."; + /* No comment provided by engineer. */ "LIVE" = "IN DIRETTA"; @@ -2163,24 +3096,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Modalità di blocco"; -/* No comment provided by engineer. */ -"Make a private connection" = "Crea una connessione privata"; - /* No comment provided by engineer. */ "Make one message disappear" = "Fai sparire un messaggio"; /* No comment provided by engineer. */ "Make profile private!" = "Rendi privato il profilo!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Assicurati che gli indirizzi dei server %@ siano nel formato corretto, uno per riga e non doppi (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Assicurati che gli indirizzi dei server WebRTC ICE siano nel formato corretto, uno per riga e non doppi."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Molte persone hanno chiesto: *se SimpleX non ha identificatori utente, come può recapitare i messaggi?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Contrassegna eliminato per tutti"; @@ -2199,32 +3123,98 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "Max 30 secondi, ricevuto istantaneamente."; +/* No comment provided by engineer. */ +"Media & file servers" = "Server di multimediali e file"; + +/* blur media */ +"Medium" = "Media"; + /* member role */ "member" = "membro"; /* No comment provided by engineer. */ "Member" = "Membro"; +/* profile update event chat item */ +"member %@ changed to %@" = "il membro %1$@ è diventato %2$@"; + /* rcv group event chat item */ "member connected" = "si è connesso/a"; +/* item status text */ +"Member inactive" = "Membro inattivo"; + +/* chat feature */ +"Member reports" = "Segnalazioni dei membri"; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All chat members will be notified." = "Il ruolo del membro verrà cambiato in \"%@\". Verranno notificati tutti i membri della chat."; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Il ruolo del membro verrà cambiato in \"%@\". Tutti i membri del gruppo verranno avvisati."; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Il ruolo del membro verrà cambiato in \"%@\". Il membro riceverà un invito nuovo."; +/* No comment provided by engineer. */ +"Member will be removed from chat - this cannot be undone!" = "Il membro verrà rimosso dalla chat, non è reversibile!"; + /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Il membro verrà rimosso dal gruppo, non è reversibile!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "I membri del gruppo possono aggiungere reazioni ai messaggi."; + +/* 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)"; + +/* No comment provided by engineer. */ +"Members can report messsages to moderators." = "I membri possono segnalare messaggi ai moderatori."; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "I membri del gruppo possono inviare messaggi diretti."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "I membri del gruppo possono inviare messaggi a tempo."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "I membri del gruppo possono inviare file e contenuti multimediali."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "I membri del gruppo possono inviare link di Simplex."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "I membri del gruppo possono inviare messaggi vocali."; + +/* No comment provided by engineer. */ +"Mention members 👋" = "Menziona i membri 👋"; + +/* No comment provided by engineer. */ +"Menus" = "Menu"; + +/* No comment provided by engineer. */ +"message" = "messaggio"; + /* item status text */ "Message delivery error" = "Errore di recapito del messaggio"; /* No comment provided by engineer. */ "Message delivery receipts!" = "Ricevute di consegna dei messaggi!"; +/* item status text */ +"Message delivery warning" = "Avviso di consegna del messaggio"; + /* No comment provided by engineer. */ -"Message draft" = "Bozza dei messaggi"; +"Message draft" = "Bozza del messaggio"; + +/* item status text */ +"Message forwarded" = "Messaggio inoltrato"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Il messaggio può essere consegnato più tardi se il membro diventa attivo."; + +/* No comment provided by engineer. */ +"Message queue info" = "Info coda messaggi"; /* chat feature */ "Message reactions" = "Reazioni ai messaggi"; @@ -2233,14 +3223,35 @@ "Message reactions are prohibited in this chat." = "Le reazioni ai messaggi sono vietate in questa chat."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Le reazioni ai messaggi sono vietate in questo gruppo."; +"Message reactions are prohibited." = "Le reazioni ai messaggi sono vietate in questo gruppo."; /* notification */ "message received" = "messaggio ricevuto"; +/* No comment provided by engineer. */ +"Message reception" = "Ricezione messaggi"; + +/* No comment provided by engineer. */ +"Message servers" = "Server dei messaggi"; + +/* No comment provided by engineer. */ +"Message shape" = "Forma del messaggio"; + +/* No comment provided by engineer. */ +"Message source remains private." = "La fonte del messaggio resta privata."; + +/* No comment provided by engineer. */ +"Message status" = "Stato del messaggio"; + +/* copied message info */ +"Message status: %@" = "Stato del messaggio: %@"; + /* No comment provided by engineer. */ "Message text" = "Testo del messaggio"; +/* No comment provided by engineer. */ +"Message too large" = "Messaggio troppo grande"; + /* No comment provided by engineer. */ "Messages" = "Messaggi"; @@ -2250,9 +3261,48 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "I messaggi da %@ verranno mostrati!"; +/* alert message */ +"Messages in this chat will never be deleted." = "I messaggi in questa chat non verranno mai eliminati."; + +/* No comment provided by engineer. */ +"Messages received" = "Messaggi ricevuti"; + +/* No comment provided by engineer. */ +"Messages sent" = "Messaggi inviati"; + +/* alert message */ +"Messages were deleted after you selected them." = "I messaggi sono stati eliminati dopo che li hai selezionati."; + +/* 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." = "I messaggi, i file e le chiamate sono protetti da **crittografia end-to-end** con perfect forward secrecy, ripudio e recupero da intrusione."; + +/* 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." = "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"; + +/* No comment provided by engineer. */ +"Migrate from another device" = "Migra da un altro dispositivo"; + +/* No comment provided by engineer. */ +"Migrate here" = "Migra qui"; + +/* No comment provided by engineer. */ +"Migrate to another device" = "Migra ad un altro dispositivo"; + +/* No comment provided by engineer. */ +"Migrate to another device via QR code." = "Migra ad un altro dispositivo via codice QR."; + +/* No comment provided by engineer. */ +"Migrating" = "Migrazione"; + /* No comment provided by engineer. */ "Migrating database archive…" = "Migrazione archivio del database…"; +/* No comment provided by engineer. */ +"Migration complete" = "Migrazione completata"; + /* No comment provided by engineer. */ "Migration error:" = "Errore di migrazione:"; @@ -2263,7 +3313,7 @@ "Migration is completed" = "La migrazione è completata"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migrazioni: %@"; +"Migrations:" = "Migrazioni:"; /* time unit */ "minutes" = "minuti"; @@ -2283,63 +3333,102 @@ /* copied message info */ "Moderated at: %@" = "Moderato il: %@"; -/* No comment provided by engineer. */ +/* marked deleted chat item preview text */ "moderated by %@" = "moderato da %@"; +/* member role */ +"moderator" = "moderatore"; + /* time unit */ "months" = "mesi"; +/* swipe action */ +"More" = "Altro"; + /* No comment provided by engineer. */ "More improvements are coming soon!" = "Altri miglioramenti sono in arrivo!"; +/* No comment provided by engineer. */ +"More reliable network connection." = "Connessione di rete più affidabile."; + +/* No comment provided by engineer. */ +"More reliable notifications" = "Notifiche più affidabili"; + /* item status description */ "Most likely this connection is deleted." = "Probabilmente questa connessione è stata eliminata."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Probabilmente questo contatto ha eliminato la connessione con te."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Profili di chat multipli"; -/* No comment provided by engineer. */ +/* notification label action */ "Mute" = "Silenzia"; +/* notification label action */ +"Mute all" = "Silenzia tutto"; + /* No comment provided by engineer. */ "Muted when inactive!" = "Silenzioso quando inattivo!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Nome"; /* No comment provided by engineer. */ "Network & servers" = "Rete e server"; +/* No comment provided by engineer. */ +"Network connection" = "Connessione di rete"; + +/* No comment provided by engineer. */ +"Network decentralization" = "Decentralizzazione della rete"; + +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "Problemi di rete - messaggio scaduto dopo molti tentativi di inviarlo."; + +/* No comment provided by engineer. */ +"Network management" = "Gestione della rete"; + +/* No comment provided by engineer. */ +"Network operator" = "Operatore di rete"; + /* No comment provided by engineer. */ "Network settings" = "Impostazioni di rete"; /* No comment provided by engineer. */ "Network status" = "Stato della rete"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "mai"; +/* token status text */ +"New" = "Nuovo"; + +/* No comment provided by engineer. */ +"New chat" = "Nuova chat"; + +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Una nuova esperienza di chat 🎉"; + /* notification */ "New contact request" = "Nuova richiesta di contatto"; /* notification */ "New contact:" = "Nuovo contatto:"; -/* No comment provided by engineer. */ -"New database archive" = "Nuovo archivio database"; - /* No comment provided by engineer. */ "New desktop app!" = "Nuova app desktop!"; /* No comment provided by engineer. */ "New display name" = "Nuovo nome da mostrare"; +/* notification */ +"New events" = "Nuovi eventi"; + /* No comment provided by engineer. */ "New in %@" = "Novità nella %@"; +/* No comment provided by engineer. */ +"New media options" = "Nuove opzioni multimediali"; + /* No comment provided by engineer. */ "New member role" = "Nuovo ruolo del membro"; @@ -2355,6 +3444,15 @@ /* No comment provided by engineer. */ "New passphrase…" = "Nuova password…"; +/* No comment provided by engineer. */ +"New server" = "Nuovo server"; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used every time you start the app." = "Le nuove credenziali SOCKS verranno usate ogni volta che avvii l'app."; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used for each server." = "Le nuove credenziali SOCKS verranno usate per ogni server."; + /* pref value */ "no" = "no"; @@ -2364,6 +3462,15 @@ /* Authentication unavailable */ "No app password" = "Nessuna password dell'app"; +/* No comment provided by engineer. */ +"No chats" = "Nessuna chat"; + +/* No comment provided by engineer. */ +"No chats found" = "Nessuna chat trovata"; + +/* No comment provided by engineer. */ +"No chats in list %@" = "Nessuna chat nell'elenco %@"; + /* No comment provided by engineer. */ "No contacts selected" = "Nessun contatto selezionato"; @@ -2376,6 +3483,9 @@ /* No comment provided by engineer. */ "No device token!" = "Nessun token del dispositivo!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Ancora nessuna connessione diretta, il messaggio viene inoltrato dall'amministratore."; + /* No comment provided by engineer. */ "no e2e encryption" = "nessuna crittografia e2e"; @@ -2388,21 +3498,87 @@ /* No comment provided by engineer. */ "No history" = "Nessuna cronologia"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Nessuna informazione, prova a ricaricare"; + +/* servers error */ +"No media & file servers." = "Nessun server di multimediali e file."; + +/* No comment provided by engineer. */ +"No message" = "Nessun messaggio"; + +/* servers error */ +"No message servers." = "Nessun server dei messaggi."; + +/* No comment provided by engineer. */ +"No network connection" = "Nessuna connessione di rete"; + +/* No comment provided by engineer. */ +"No permission to record speech" = "Nessuna autorizzazione per registrare l'audio"; + +/* No comment provided by engineer. */ +"No permission to record video" = "Nessuna autorizzazione per registrare il video"; + /* No comment provided by engineer. */ "No permission to record voice message" = "Nessuna autorizzazione per registrare messaggi vocali"; +/* No comment provided by engineer. */ +"No push server" = "Locale"; + /* No comment provided by engineer. */ "No received or sent files" = "Nessun file ricevuto o inviato"; +/* servers error */ +"No servers for private message routing." = "Nessun server per l'instradamento dei messaggi privati."; + +/* servers error */ +"No servers to receive files." = "Nessun server per ricevere file."; + +/* servers error */ +"No servers to receive messages." = "Nessun server per ricevere messaggi."; + +/* servers error */ +"No servers to send files." = "Nessun server per inviare file."; + /* copied message info in history */ "no text" = "nessun testo"; +/* alert title */ +"No token!" = "Nessun token!"; + +/* No comment provided by engineer. */ +"No unread chats" = "Nessuna chat non letta"; + +/* No comment provided by engineer. */ +"No user identifiers." = "Nessun identificatore utente."; + +/* No comment provided by engineer. */ +"Not compatible!" = "Non compatibile!"; + +/* No comment provided by engineer. */ +"Notes" = "Note"; + +/* No comment provided by engineer. */ +"Nothing selected" = "Nessuna selezione"; + +/* alert title */ +"Nothing to forward!" = "Niente da inoltrare!"; + /* No comment provided by engineer. */ "Notifications" = "Notifiche"; /* No comment provided by engineer. */ "Notifications are disabled!" = "Le notifiche sono disattivate!"; +/* alert title */ +"Notifications error" = "Errore delle notifiche"; + +/* No comment provided by engineer. */ +"Notifications privacy" = "Privacy delle notifiche"; + +/* alert title */ +"Notifications status" = "Stato delle notifiche"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Ora gli amministratori possono:\n- eliminare i messaggi dei membri.\n- disattivare i membri (ruolo \"osservatore\")"; @@ -2410,30 +3586,27 @@ "observer" = "osservatore"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "off"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Off"; -/* No comment provided by engineer. */ -"Off (Local)" = "Off (Locale)"; - /* feature offered item */ "offered %@" = "offerto %@"; /* feature offered item */ "offered %@: %@" = "offerto %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Ok"; /* No comment provided by engineer. */ -"Old database" = "Database vecchio"; +"OK" = "OK"; /* No comment provided by engineer. */ -"Old database archive" = "Vecchio archivio del database"; +"Old database" = "Database vecchio"; /* group pref value */ "on" = "on"; @@ -2442,16 +3615,22 @@ "One-time invitation link" = "Link di invito una tantum"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Gli host Onion saranno necessari per la connessione. Richiede l'attivazione della VPN."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Gli host Onion saranno **necessari** per la connessione.\nRichiede l'attivazione della VPN."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Gli host Onion verranno usati quando disponibili. Richiede l'attivazione della VPN."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Gli host Onion verranno usati quando disponibili.\nRichiede l'attivazione della VPN."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Gli host Onion non verranno usati."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Solo i dispositivi client archiviano profili utente, i contatti, i gruppi e i messaggi inviati con la **crittografia end-to-end a 2 livelli**."; +"Only chat owners can change preferences." = "Solo i proprietari della chat possono modificarne le preferenze."; + +/* No comment provided by engineer. */ +"Only client devices store user profiles, contacts, groups, and messages." = "Solo i dispositivi client archiviano profili utente, i contatti, i gruppi e i messaggi inviati con la **crittografia end-to-end a 2 livelli**."; + +/* No comment provided by engineer. */ +"Only delete conversation" = "Elimina solo la conversazione"; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Solo i proprietari del gruppo possono modificarne le preferenze."; @@ -2462,11 +3641,17 @@ /* No comment provided by engineer. */ "Only group owners can enable voice messages." = "Solo i proprietari del gruppo possono attivare i messaggi vocali."; +/* No comment provided by engineer. */ +"Only sender and moderators see it" = "Solo il mittente e i moderatori lo vedono"; + +/* No comment provided by engineer. */ +"Only you and moderators see it" = "Solo tu e i moderatori lo vedete"; + /* No comment provided by engineer. */ "Only you can add message reactions." = "Solo tu puoi aggiungere reazioni ai messaggi."; /* No comment provided by engineer. */ -"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "Solo tu puoi eliminare irreversibilmente i messaggi (il tuo contatto può contrassegnarli per l'eliminazione)."; +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Solo tu puoi eliminare irreversibilmente i messaggi (il tuo contatto può contrassegnarli per l'eliminazione). (24 ore)"; /* No comment provided by engineer. */ "Only you can make calls." = "Solo tu puoi effettuare chiamate."; @@ -2481,7 +3666,7 @@ "Only your contact can add message reactions." = "Solo il tuo contatto può aggiungere reazioni ai messaggi."; /* No comment provided by engineer. */ -"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Solo il tuo contatto può eliminare irreversibilmente i messaggi (tu puoi contrassegnarli per l'eliminazione)."; +"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Solo il tuo contatto può eliminare irreversibilmente i messaggi (tu puoi contrassegnarli per l'eliminazione). (24 ore)"; /* No comment provided by engineer. */ "Only your contact can make calls." = "Solo il tuo contatto può effettuare chiamate."; @@ -2492,33 +3677,78 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Solo il tuo contatto può inviare messaggi vocali."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Apri"; +/* No comment provided by engineer. */ +"Open changes" = "Apri le modifiche"; + /* No comment provided by engineer. */ "Open chat" = "Apri chat"; /* authentication reason */ "Open chat console" = "Apri la console della chat"; +/* No comment provided by engineer. */ +"Open conditions" = "Apri le condizioni"; + /* No comment provided by engineer. */ "Open group" = "Apri gruppo"; +/* authentication reason */ +"Open migration to another device" = "Apri migrazione ad un altro dispositivo"; + /* No comment provided by engineer. */ "Open Settings" = "Apri le impostazioni"; -/* authentication reason */ -"Open user profiles" = "Apri i profili utente"; +/* No comment provided by engineer. */ +"Opening app…" = "Apertura dell'app…"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Protocollo e codice open source: chiunque può gestire i server."; +"Operator" = "Operatore"; + +/* alert title */ +"Operator server" = "Server dell'operatore"; /* No comment provided by engineer. */ -"Opening database…" = "Apertura del database…"; +"Or import archive file" = "O importa file archivio"; + +/* No comment provided by engineer. */ +"Or paste archive link" = "O incolla il link dell'archivio"; + +/* No comment provided by engineer. */ +"Or scan QR code" = "O scansiona il codice QR"; + +/* 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 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. */ +"Organize chats into lists" = "Organizza le chat in elenchi"; + +/* No comment provided by engineer. */ +"other" = "altro"; + +/* No comment provided by engineer. */ +"Other" = "Altro"; + +/* No comment provided by engineer. */ +"other errors" = "altri errori"; + +/* alert message */ +"Other file errors:\n%@" = "Altri errori di file:\n%@"; /* member role */ "owner" = "proprietario"; +/* feature role */ +"owners" = "proprietari"; + /* No comment provided by engineer. */ "Passcode" = "Codice di accesso"; @@ -2535,10 +3765,13 @@ "Passcode set!" = "Codice di accesso impostato!"; /* No comment provided by engineer. */ -"Password to show" = "Password per mostrare"; +"Password" = "Password"; /* No comment provided by engineer. */ -"Paste" = "Incolla"; +"Password to show" = "Password per mostrare"; + +/* past/unknown group member */ +"Past member %@" = "Membro passato %@"; /* No comment provided by engineer. */ "Paste desktop address" = "Incolla l'indirizzo desktop"; @@ -2547,32 +3780,50 @@ "Paste image" = "Incolla immagine"; /* No comment provided by engineer. */ -"Paste received link" = "Incolla il link ricevuto"; +"Paste link to connect!" = "Incolla un link per connettere!"; -/* placeholder */ -"Paste the link you received to connect with your contact." = "Incolla il link che hai ricevuto nella casella sottostante per connetterti con il tuo contatto."; +/* No comment provided by engineer. */ +"Paste the link you received" = "Incolla il link che hai ricevuto"; /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Le persone possono connettersi a te solo tramite i link che condividi."; +"pending" = "in attesa"; /* No comment provided by engineer. */ -"Periodically" = "Periodicamente"; +"Pending" = "In attesa"; + +/* No comment provided by engineer. */ +"pending approval" = "in attesa di approvazione"; + +/* No comment provided by engineer. */ +"Periodic" = "Periodicamente"; /* message decrypt error item */ "Permanent decryption error" = "Errore di decifrazione"; +/* No comment provided by engineer. */ +"Picture-in-picture calls" = "Chiamate picture-in-picture"; + /* No comment provided by engineer. */ "PING count" = "Conteggio PING"; /* No comment provided by engineer. */ "PING interval" = "Intervallo PING"; +/* No comment provided by engineer. */ +"Play from the chat list." = "Riproduci dall'elenco delle chat."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Chiedi al contatto di attivare le chiamate."; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Chiedi al tuo contatto di attivare l'invio dei messaggi vocali."; +/* No comment provided by engineer. */ +"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." = "Controlla che mobile e desktop siano collegati alla stessa rete locale e che il firewall del desktop consenta la connessione.\nSi prega di condividere qualsiasi altro problema con gli sviluppatori."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Controlla di aver usato il link giusto o chiedi al tuo contatto di inviartene un altro."; @@ -2582,6 +3833,12 @@ /* No comment provided by engineer. */ "Please check yours and your contact preferences." = "Controlla le preferenze tue e del tuo contatto."; +/* No comment provided by engineer. */ +"Please confirm that network settings are correct for this device." = "Conferma che le impostazioni di rete sono corrette per questo dispositivo."; + +/* No comment provided by engineer. */ +"Please contact developers.\nError: %@" = "Contatta gli sviluppatori.\nErrore: %@"; + /* No comment provided by engineer. */ "Please contact group admin." = "Contatta l'amministratore del gruppo."; @@ -2606,9 +3863,21 @@ /* No comment provided by engineer. */ "Please store passphrase securely, you will NOT be able to change it if you lose it." = "Conserva la password in modo sicuro, NON potrai cambiarla se la perdi."; +/* token info */ +"Please try to disable and re-enable notfications." = "Prova a disattivare e riattivare le notifiche."; + +/* token info */ +"Please wait for token activation to complete." = "Attendi il completamento dell'attivazione del token."; + +/* token info */ +"Please wait for token to be registered." = "Attendi la registrazione del token."; + /* No comment provided by engineer. */ "Polish interface" = "Interfaccia polacca"; +/* No comment provided by engineer. */ +"Port" = "Porta"; + /* server test error */ "Possibly, certificate fingerprint in server address is incorrect" = "Probabilmente l'impronta del certificato nell'indirizzo del server è sbagliata"; @@ -2616,23 +3885,53 @@ "Preserve the last message draft, with attachments." = "Conserva la bozza dell'ultimo messaggio, con gli allegati."; /* No comment provided by engineer. */ -"Preset server" = "Server preimpostato"; +"Preset server address" = "Indirizzo server preimpostato"; /* No comment provided by engineer. */ -"Preset server address" = "Indirizzo server preimpostato"; +"Preset servers" = "Server preimpostati"; /* No comment provided by engineer. */ "Preview" = "Anteprima"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Server precedentemente connessi"; + /* No comment provided by engineer. */ "Privacy & security" = "Privacy e sicurezza"; +/* No comment provided by engineer. */ +"Privacy for your customers." = "Privacy per i tuoi clienti."; + +/* No comment provided by engineer. */ +"Privacy policy and conditions of use." = "Informativa sulla privacy e condizioni d'uso."; + /* No comment provided by engineer. */ "Privacy redefined" = "Privacy ridefinita"; +/* 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."; + /* No comment provided by engineer. */ "Private filenames" = "Nomi di file privati"; +/* No comment provided by engineer. */ +"Private media file names." = "Nomi privati dei file multimediali."; + +/* No comment provided by engineer. */ +"Private message routing" = "Instradamento privato dei messaggi"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Instradamento privato dei messaggi 🚀"; + +/* name of notes to self */ +"Private notes" = "Note private"; + +/* No comment provided by engineer. */ +"Private routing" = "Instradamento privato"; + +/* No comment provided by engineer. */ +"Private routing error" = "Errore di instradamento privato"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profilo e connessioni al server"; @@ -2640,15 +3939,15 @@ "Profile image" = "Immagine del profilo"; /* No comment provided by engineer. */ -"Profile name" = "Nome del profilo"; - -/* No comment provided by engineer. */ -"Profile name:" = "Nome del profilo:"; +"Profile images" = "Immagini del profilo"; /* No comment provided by engineer. */ "Profile password" = "Password del profilo"; /* No comment provided by engineer. */ +"Profile theme" = "Tema del profilo"; + +/* alert message */ "Profile update will be sent to your contacts." = "L'aggiornamento del profilo verrà inviato ai tuoi contatti."; /* No comment provided by engineer. */ @@ -2663,6 +3962,9 @@ /* No comment provided by engineer. */ "Prohibit messages reactions." = "Proibisci le reazioni ai messaggi."; +/* No comment provided by engineer. */ +"Prohibit reporting messages to moderators." = "Vieta di segnalare messaggi ai moderatori."; + /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "Proibisci l'invio di messaggi diretti ai membri."; @@ -2672,38 +3974,71 @@ /* No comment provided by engineer. */ "Prohibit sending files and media." = "Proibisci l'invio di file e contenuti multimediali."; +/* No comment provided by engineer. */ +"Prohibit sending SimpleX links." = "Vieta l'invio di link di SimpleX."; + /* No comment provided by engineer. */ "Prohibit sending voice messages." = "Proibisci l'invio di messaggi vocali."; /* No comment provided by engineer. */ "Protect app screen" = "Proteggi la schermata dell'app"; +/* No comment provided by engineer. */ +"Protect IP address" = "Proteggi l'indirizzo IP"; + /* No comment provided by engineer. */ "Protect your chat profiles with a password!" = "Proteggi i tuoi profili di chat con una password!"; +/* No comment provided by engineer. */ +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Proteggi il tuo indirizzo IP dai relay di messaggistica scelti dai tuoi contatti.\nAttivalo nelle impostazioni *Rete e server*."; + /* No comment provided by engineer. */ "Protocol timeout" = "Scadenza del protocollo"; /* No comment provided by engineer. */ "Protocol timeout per KB" = "Scadenza del protocollo per KB"; +/* No comment provided by engineer. */ +"Proxied" = "Via proxy"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Server via proxy"; + +/* No comment provided by engineer. */ +"Proxy requires password" = "Il proxy richiede una password"; + /* No comment provided by engineer. */ "Push notifications" = "Notifiche push"; +/* No comment provided by engineer. */ +"Push server" = "Server push"; + +/* chat item text */ +"quantum resistant e2e encryption" = "crittografia e2e resistente alla quantistica"; + +/* No comment provided by engineer. */ +"Quantum resistant encryption" = "Crittografia resistente alla quantistica"; + /* No comment provided by engineer. */ "Rate the app" = "Valuta l'app"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Barra degli strumenti di chat accessibile"; + /* chat item menu */ "React…" = "Reagisci…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Leggi"; /* No comment provided by engineer. */ "Read more" = "Leggi tutto"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Maggiori informazioni nella [Guida per l'utente](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"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)."; + +/* 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)."; @@ -2712,10 +4047,10 @@ "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)."; /* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Maggiori informazioni nel nostro repository GitHub."; +"Receipts are disabled" = "Le ricevute sono disattivate"; /* No comment provided by engineer. */ -"Receipts are disabled" = "Le ricevute sono disattivate"; +"Receive errors" = "Errori di ricezione"; /* No comment provided by engineer. */ "received answer…" = "risposta ricevuta…"; @@ -2735,6 +4070,15 @@ /* message info title */ "Received message" = "Messaggio ricevuto"; +/* No comment provided by engineer. */ +"Received messages" = "Messaggi ricevuti"; + +/* No comment provided by engineer. */ +"Received reply" = "Risposta ricevuta"; + +/* No comment provided by engineer. */ +"Received total" = "Totale ricevuto"; + /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "L'indirizzo di ricezione verrà cambiato in un server diverso. La modifica dell'indirizzo verrà completata dopo che il mittente sarà in linea."; @@ -2744,12 +4088,33 @@ /* No comment provided by engineer. */ "Receiving via" = "Ricezione via"; +/* No comment provided by engineer. */ +"Recent history and improved [directory bot](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)." = "Cronologia recente e [bot della directory](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) migliorato."; + +/* No comment provided by engineer. */ +"Recipient(s) can't see who this message is from." = "I destinatari non possono vedere da chi proviene questo messaggio."; + /* No comment provided by engineer. */ "Recipients see updates as you type them." = "I destinatari vedono gli aggiornamenti mentre li digiti."; +/* No comment provided by engineer. */ +"Reconnect" = "Riconnetti"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Riconnetti tutti i server connessi per imporre il recapito dei messaggi. Utilizza traffico aggiuntivo."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Riconnetti tutti i server"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Riconnettere tutti i server?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Riconnetti il server per forzare la consegna dei messaggi. Usa traffico aggiuntivo."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Riconnettere il server?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Riconnettere i server?"; @@ -2762,7 +4127,17 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Consumo di batteria ridotto"; -/* reject incoming call via notification */ +/* No comment provided by engineer. */ +"Register" = "Registra"; + +/* token info */ +"Register notification token?" = "Registrare il token di notifica?"; + +/* token status text */ +"Registered" = "Registrato"; + +/* reject incoming call via notification +swipe action */ "Reject" = "Rifiuta"; /* No comment provided by engineer. */ @@ -2771,6 +4146,9 @@ /* No comment provided by engineer. */ "Reject contact request" = "Rifiuta la richiesta di contatto"; +/* No comment provided by engineer. */ +"rejected" = "rifiutato"; + /* call status */ "rejected call" = "chiamata rifiutata"; @@ -2783,6 +4161,12 @@ /* No comment provided by engineer. */ "Remove" = "Rimuovi"; +/* No comment provided by engineer. */ +"Remove archive?" = "Rimuovere l'archivio?"; + +/* No comment provided by engineer. */ +"Remove image" = "Rimuovi immagine"; + /* No comment provided by engineer. */ "Remove member" = "Rimuovi membro"; @@ -2798,6 +4182,12 @@ /* rcv group event chat item */ "removed %@" = "ha rimosso %@"; +/* profile update event chat item */ +"removed contact address" = "indirizzo di contatto rimosso"; + +/* profile update event chat item */ +"removed profile picture" = "immagine del profilo rimossa"; + /* rcv group event chat item */ "removed you" = "ti ha rimosso/a"; @@ -2813,24 +4203,81 @@ /* No comment provided by engineer. */ "Repeat connection request?" = "Ripetere la richiesta di connessione?"; +/* No comment provided by engineer. */ +"Repeat download" = "Ripeti scaricamento"; + +/* No comment provided by engineer. */ +"Repeat import" = "Ripeti importazione"; + /* No comment provided by engineer. */ "Repeat join request?" = "Ripetere la richiesta di ingresso?"; +/* No comment provided by engineer. */ +"Repeat upload" = "Ripeti caricamento"; + /* chat item action */ "Reply" = "Rispondi"; +/* chat item action */ +"Report" = "Segnala"; + +/* report reason */ +"Report content: only group moderators will see it." = "Segnala contenuto: solo i moderatori del gruppo lo vedranno."; + +/* report reason */ +"Report member profile: only group moderators will see it." = "Segnala profilo: solo i moderatori del gruppo lo vedranno."; + +/* report reason */ +"Report other: only group moderators will see it." = "Segnala altro: solo i moderatori del gruppo lo vedranno."; + +/* No comment provided by engineer. */ +"Report reason?" = "Motivo della segnalazione?"; + +/* report reason */ +"Report spam: only group moderators will see it." = "Segnala spam: solo i moderatori del gruppo lo vedranno."; + +/* report reason */ +"Report violation: only group moderators will see it." = "Segnala violazione: solo i moderatori del gruppo lo vedranno."; + +/* report in notification */ +"Report: %@" = "Segnalazione: %@"; + +/* No comment provided by engineer. */ +"Reporting messages to moderators is prohibited." = "È vietato segnalare messaggi ai moderatori."; + +/* No comment provided by engineer. */ +"Reports" = "Segnalazioni"; + +/* chat list item title */ +"requested to connect" = "richiesto di connettersi"; + /* No comment provided by engineer. */ "Required" = "Obbligatorio"; /* No comment provided by engineer. */ "Reset" = "Ripristina"; +/* No comment provided by engineer. */ +"Reset all hints" = "Ripristina tutti i suggerimenti"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Azzera tutte le statistiche"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Azzerare tutte le statistiche?"; + /* No comment provided by engineer. */ "Reset colors" = "Ripristina i colori"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Ripristina al tema dell'app"; + /* No comment provided by engineer. */ "Reset to defaults" = "Ripristina i predefiniti"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Ripristina al tema dell'utente"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Riavvia l'app per creare un nuovo profilo di chat"; @@ -2849,11 +4296,14 @@ /* No comment provided by engineer. */ "Restore database error" = "Errore di ripristino del database"; +/* No comment provided by engineer. */ +"Retry" = "Riprova"; + /* chat item action */ "Reveal" = "Rivela"; /* No comment provided by engineer. */ -"Revert" = "Ripristina"; +"Review conditions" = "Leggi le condizioni"; /* No comment provided by engineer. */ "Revoke" = "Revoca"; @@ -2870,37 +4320,44 @@ /* No comment provided by engineer. */ "Run chat" = "Avvia chat"; -/* chat item action */ +/* No comment provided by engineer. */ +"Safely receive files" = "Ricevi i file in sicurezza"; + +/* No comment provided by engineer. */ +"Safer groups" = "Gruppi più sicuri"; + +/* alert button +chat item action */ "Save" = "Salva"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Salva (e avvisa i contatti)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Salva e avvisa il contatto"; /* No comment provided by engineer. */ "Save and notify group members" = "Salva e avvisa i membri del gruppo"; +/* 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 archive" = "Salva archivio"; - -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Salva le impostazioni di accettazione automatica"; - /* No comment provided by engineer. */ "Save group profile" = "Salva il profilo del gruppo"; +/* No comment provided by engineer. */ +"Save list" = "Salva elenco"; + /* No comment provided by engineer. */ "Save passphrase and open chat" = "Salva la password e apri la chat"; /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Salva password nel portachiavi"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Salvare le preferenze?"; /* No comment provided by engineer. */ @@ -2909,18 +4366,42 @@ /* No comment provided by engineer. */ "Save servers" = "Salva i server"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Salvare i server?"; -/* No comment provided by engineer. */ -"Save settings?" = "Salvare le impostazioni?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Salvare il messaggio di benvenuto?"; +/* alert title */ +"Save your profile?" = "Salvare il profilo?"; + +/* No comment provided by engineer. */ +"saved" = "salvato"; + +/* No comment provided by engineer. */ +"Saved" = "Salvato"; + +/* No comment provided by engineer. */ +"Saved from" = "Salvato da"; + +/* No comment provided by engineer. */ +"saved from %@" = "salvato da %@"; + +/* message info title */ +"Saved message" = "Messaggio salvato"; + /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "I server WebRTC ICE salvati verranno rimossi"; +/* No comment provided by engineer. */ +"Saving %lld messages" = "Salvataggio di %lld messaggi"; + +/* No comment provided by engineer. */ +"Scale" = "Scala"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Scansiona / Incolla link"; + /* No comment provided by engineer. */ "Scan code" = "Scansiona codice"; @@ -2936,12 +4417,24 @@ /* No comment provided by engineer. */ "Scan server QR code" = "Scansiona codice QR del server"; +/* No comment provided by engineer. */ +"search" = "cerca"; + /* No comment provided by engineer. */ "Search" = "Cerca"; +/* No comment provided by engineer. */ +"Search bar accepts invitation links." = "La barra di ricerca accetta i link di invito."; + +/* No comment provided by engineer. */ +"Search or paste SimpleX link" = "Cerca o incolla un link SimpleX"; + /* network option */ "sec" = "sec"; +/* No comment provided by engineer. */ +"Secondary" = "Secondario"; + /* time unit */ "seconds" = "secondi"; @@ -2951,6 +4444,9 @@ /* server test step */ "Secure queue" = "Coda sicura"; +/* No comment provided by engineer. */ +"Secured" = "Protetto"; + /* No comment provided by engineer. */ "Security assessment" = "Valutazione della sicurezza"; @@ -2960,9 +4456,18 @@ /* chat item text */ "security code changed" = "codice di sicurezza modificato"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Seleziona"; +/* No comment provided by engineer. */ +"Select chat profile" = "Seleziona il profilo di chat"; + +/* No comment provided by engineer. */ +"Selected %lld" = "%lld selezionato"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Le preferenze della chat selezionata vietano questo messaggio."; + /* No comment provided by engineer. */ "Self-destruct" = "Autodistruzione"; @@ -2987,9 +4492,6 @@ /* No comment provided by engineer. */ "send direct message" = "invia messaggio diretto"; -/* No comment provided by engineer. */ -"Send direct message" = "Invia messaggio diretto"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Invia messaggio diretto per connetterti"; @@ -2997,16 +4499,28 @@ "Send disappearing message" = "Invia messaggio a tempo"; /* No comment provided by engineer. */ -"Send link previews" = "Invia anteprime dei link"; +"Send errors" = "Errori di invio"; + +/* No comment provided by engineer. */ +"Send link previews" = "Invia le anteprime dei link"; /* No comment provided by engineer. */ "Send live message" = "Invia messaggio in diretta"; +/* No comment provided by engineer. */ +"Send message to enable calls." = "Invia un messaggio per attivare le chiamate."; + +/* No comment provided by engineer. */ +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Invia messaggi direttamente quando l'indirizzo IP è protetto e il tuo server o quello di destinazione non supporta l'instradamento privato."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Invia messaggi direttamente quando il tuo server o quello di destinazione non supporta l'instradamento privato."; + /* No comment provided by engineer. */ "Send notifications" = "Invia notifiche"; /* No comment provided by engineer. */ -"Send notifications:" = "Invia notifiche:"; +"Send private reports" = "Invia segnalazioni private"; /* No comment provided by engineer. */ "Send questions and ideas" = "Invia domande e idee"; @@ -3018,6 +4532,9 @@ "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."; + +/* alert message */ "Sender cancelled file transfer." = "Il mittente ha annullato il trasferimento del file."; /* No comment provided by engineer. */ @@ -3053,15 +4570,57 @@ /* copied message info */ "Sent at: %@" = "Inviato il: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Inviato direttamente"; + /* notification */ "Sent file event" = "Evento file inviato"; /* message info title */ "Sent message" = "Messaggio inviato"; +/* No comment provided by engineer. */ +"Sent messages" = "Messaggi inviati"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "I messaggi inviati verranno eliminati dopo il tempo impostato."; +/* No comment provided by engineer. */ +"Sent reply" = "Risposta inviata"; + +/* No comment provided by engineer. */ +"Sent total" = "Totale inviato"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Inviato via proxy"; + +/* No comment provided by engineer. */ +"Server" = "Server"; + +/* alert message */ +"Server added to operator %@." = "Server aggiunto all'operatore %@."; + +/* No comment provided by engineer. */ +"Server address" = "Indirizzo server"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "L'indirizzo del server è incompatibile con le impostazioni di rete: %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "L'indirizzo del server non è compatibile con le impostazioni di rete."; + +/* alert title */ +"Server operator changed." = "L'operatore del server è cambiato."; + +/* No comment provided by engineer. */ +"Server operators" = "Operatori server"; + +/* alert title */ +"Server protocol changed." = "Il protocollo del server è cambiato."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "info coda server: %1$@\n\nultimo msg ricevuto: %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "Il server richiede l'autorizzazione di creare code, controlla la password"; @@ -3071,27 +4630,60 @@ /* No comment provided by engineer. */ "Server test failed!" = "Test del server fallito!"; +/* No comment provided by engineer. */ +"Server type" = "Tipo server"; + +/* srv error text */ +"Server version is incompatible with network settings." = "La versione del server non è compatibile con le impostazioni di rete."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "La versione del server è incompatibile con la tua app: %@."; + /* No comment provided by engineer. */ "Servers" = "Server"; +/* No comment provided by engineer. */ +"Servers info" = "Info dei server"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Le statistiche dei server verranno azzerate - è irreversibile!"; + /* No comment provided by engineer. */ "Session code" = "Codice di sessione"; /* No comment provided by engineer. */ "Set 1 day" = "Imposta 1 giorno"; +/* No comment provided by engineer. */ +"Set chat name…" = "Imposta il nome della chat…"; + /* No comment provided by engineer. */ "Set contact name…" = "Imposta nome del contatto…"; +/* No comment provided by engineer. */ +"Set default theme" = "Imposta tema predefinito"; + /* No comment provided by engineer. */ "Set group preferences" = "Imposta le preferenze del gruppo"; /* No comment provided by engineer. */ "Set it instead of system authentication." = "Impostalo al posto dell'autenticazione di sistema."; +/* No comment provided by engineer. */ +"Set message expiration in chats." = "Imposta la scadenza dei messaggi nelle chat."; + +/* profile update event chat item */ +"set new contact address" = "impostato nuovo indirizzo di contatto"; + +/* profile update event chat item */ +"set new profile picture" = "ha impostato una nuova immagine del profilo"; + /* No comment provided by engineer. */ "Set passcode" = "Imposta codice"; +/* No comment provided by engineer. */ +"Set passphrase" = "Imposta password"; + /* No comment provided by engineer. */ "Set passphrase to export" = "Imposta la password per esportare"; @@ -3104,27 +4696,58 @@ /* No comment provided by engineer. */ "Settings" = "Impostazioni"; -/* chat item action */ +/* alert message */ +"Settings were changed." = "Le impostazioni sono state cambiate."; + +/* No comment provided by engineer. */ +"Shape profile images" = "Forma delle immagini del profilo"; + +/* alert action +chat item action */ "Share" = "Condividi"; /* No comment provided by engineer. */ "Share 1-time link" = "Condividi link una tantum"; +/* No comment provided by engineer. */ +"Share 1-time link with a friend" = "Condividi link una tantum con un amico"; + /* No comment provided by engineer. */ "Share address" = "Condividi indirizzo"; /* No comment provided by engineer. */ +"Share address publicly" = "Condividi indirizzo pubblicamente"; + +/* alert title */ "Share address with contacts?" = "Condividere l'indirizzo con i contatti?"; +/* No comment provided by engineer. */ +"Share from other apps." = "Condividi da altre app."; + /* No comment provided by engineer. */ "Share link" = "Condividi link"; /* No comment provided by engineer. */ -"Share one-time invitation link" = "Condividi link di invito una tantum"; +"Share profile" = "Condividi il profilo"; + +/* No comment provided by engineer. */ +"Share SimpleX address on social media." = "Condividi l'indirizzo SimpleX sui social media."; + +/* No comment provided by engineer. */ +"Share this 1-time invite link" = "Condividi questo link di invito una tantum"; + +/* No comment provided by engineer. */ +"Share to SimpleX" = "Condividi in SimpleX"; /* No comment provided by engineer. */ "Share with contacts" = "Condividi con i contatti"; +/* No comment provided by engineer. */ +"Short link" = "Link breve"; + +/* No comment provided by engineer. */ +"Show → on messages sent via private routing." = "Mostra → nei messaggi inviati via instradamento privato."; + /* No comment provided by engineer. */ "Show calls in phone history" = "Mostra le chiamate nella cronologia del telefono"; @@ -3134,23 +4757,47 @@ /* No comment provided by engineer. */ "Show last messages" = "Mostra ultimi messaggi"; +/* No comment provided by engineer. */ +"Show message status" = "Mostra stato del messaggio"; + +/* No comment provided by engineer. */ +"Show percentage" = "Mostra percentuale"; + /* No comment provided by engineer. */ "Show preview" = "Mostra anteprima"; +/* No comment provided by engineer. */ +"Show QR code" = "Mostra codice QR"; + /* No comment provided by engineer. */ "Show:" = "Mostra:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "Indirizzo SimpleX"; /* No comment provided by engineer. */ "SimpleX Address" = "Indirizzo SimpleX"; +/* No comment provided by engineer. */ +"SimpleX address and 1-time links are safe to share via any messenger." = "L'indirizzo SimpleX e i link una tantum sono sicuri da condividere tramite qualsiasi messenger."; + +/* No comment provided by engineer. */ +"SimpleX address or 1-time link?" = "Indirizzo SimpleX o link una tantum?"; + +/* simplex link type */ +"SimpleX channel link" = "Link del canale SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "SimpleX Chat e Flux hanno concluso un accordo per includere server gestiti da Flux nell'app."; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "La sicurezza di SimpleX Chat è stata verificata da Trail of Bits."; /* simplex link type */ -"SimpleX contact address" = "Indirizzo del contatto SimpleX"; +"SimpleX contact address" = "Indirizzo di contatto SimpleX"; /* notification */ "SimpleX encrypted message or connection event" = "Messaggio crittografato di SimpleX o evento di connessione"; @@ -3158,9 +4805,15 @@ /* simplex link type */ "SimpleX group link" = "Link gruppo SimpleX"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "Link di SimpleX"; +/* No comment provided by engineer. */ +"SimpleX links are prohibited." = "I link di SimpleX sono vietati in questo gruppo."; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "Link di SimpleX non consentiti"; + /* No comment provided by engineer. */ "SimpleX Lock" = "SimpleX Lock"; @@ -3176,9 +4829,15 @@ /* simplex link type */ "SimpleX one-time invitation" = "Invito SimpleX una tantum"; +/* No comment provided by engineer. */ +"SimpleX protocols reviewed by Trail of Bits." = "Protocolli di SimpleX esaminati da Trail of Bits."; + /* No comment provided by engineer. */ "Simplified incognito mode" = "Modalità incognito semplificata"; +/* No comment provided by engineer. */ +"Size" = "Dimensione"; + /* No comment provided by engineer. */ "Skip" = "Salta"; @@ -3189,31 +4848,65 @@ "Small groups (max 20)" = "Piccoli gruppi (max 20)"; /* No comment provided by engineer. */ -"SMP servers" = "Server SMP"; +"SMP server" = "Server SMP"; + +/* No comment provided by engineer. */ +"SOCKS proxy" = "Proxy SOCKS"; + +/* blur media */ +"Soft" = "Leggera"; + +/* No comment provided by engineer. */ +"Some app settings were not migrated." = "Alcune impostazioni dell'app non sono state migrate."; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "Alcuni file non sono stati esportati:"; /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Si sono verificati alcuni errori non gravi durante l'importazione: vedi la console della chat per i dettagli."; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "Si sono verificati alcuni errori non fatali durante l'importazione:"; + +/* alert message */ +"Some servers failed the test:\n%@" = "Alcuni server hanno fallito il test:\n%@"; + /* notification title */ "Somebody" = "Qualcuno"; +/* blocking reason +report reason */ +"Spam" = "Spam"; + /* No comment provided by engineer. */ -"Start a new chat" = "Inizia una nuova chat"; +"Square, circle, or anything in between." = "Quadrata, circolare o qualsiasi forma tra le due."; + +/* chat item text */ +"standard end-to-end encryption" = "crittografia end-to-end standard"; /* No comment provided by engineer. */ "Start chat" = "Avvia chat"; +/* No comment provided by engineer. */ +"Start chat?" = "Avviare la chat?"; + /* No comment provided by engineer. */ "Start migration" = "Avvia la migrazione"; +/* No comment provided by engineer. */ +"Starting from %@." = "Inizio da %@."; + /* No comment provided by engineer. */ "starting…" = "avvio…"; +/* No comment provided by engineer. */ +"Statistics" = "Statistiche"; + /* No comment provided by engineer. */ "Stop" = "Ferma"; /* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Ferma la chat per attivare le azioni del database"; +"Stop chat" = "Ferma la chat"; /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Ferma la chat per esportare, importare o eliminare il database della chat. Non potrai ricevere e inviare messaggi mentre la chat è ferma."; @@ -3230,36 +4923,66 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Fermare l'invio del file?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Smetti di condividere"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Smettere di condividere l'indirizzo?"; /* authentication reason */ "Stop SimpleX" = "Ferma SimpleX"; +/* No comment provided by engineer. */ +"Stopping chat" = "Arresto della chat"; + +/* No comment provided by engineer. */ +"Storage" = "Archiviazione"; + /* No comment provided by engineer. */ "strike" = "barrato"; +/* blur media */ +"Strong" = "Forte"; + /* No comment provided by engineer. */ "Submit" = "Invia"; +/* No comment provided by engineer. */ +"Subscribed" = "Iscritto"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Errori di iscrizione"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Iscrizioni ignorate"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Supporta SimpleX Chat"; +/* No comment provided by engineer. */ +"Switch audio and video during the call." = "Cambia tra audio e video durante la chiamata."; + +/* No comment provided by engineer. */ +"Switch chat profile for 1-time invitations." = "Cambia profilo di chat per inviti una tantum."; + /* No comment provided by engineer. */ "System" = "Sistema"; /* No comment provided by engineer. */ "System authentication" = "Autenticazione di sistema"; +/* No comment provided by engineer. */ +"Tail" = "Coda"; + /* No comment provided by engineer. */ "Take picture" = "Scatta foto"; /* No comment provided by engineer. */ "Tap button " = "Tocca il pulsante "; +/* 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."; + /* No comment provided by engineer. */ "Tap to activate profile." = "Tocca per attivare il profilo."; @@ -3273,11 +4996,20 @@ "Tap to join incognito" = "Toccare per entrare in incognito"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Tocca per iniziare una chat"; +"Tap to paste link" = "Tocca per incollare il link"; + +/* No comment provided by engineer. */ +"Tap to scan" = "Tocca per scansionare"; + +/* No comment provided by engineer. */ +"TCP connection" = "Connessione TCP"; /* No comment provided by engineer. */ "TCP connection timeout" = "Scadenza connessione TCP"; +/* No comment provided by engineer. */ +"TCP port for messaging" = "Porta TCP per i messaggi"; + /* No comment provided by engineer. */ "TCP_KEEPCNT" = "TCP_KEEPCNT"; @@ -3287,16 +5019,22 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* file error alert title */ +"Temporary file error" = "Errore del file temporaneo"; + /* 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 server" = "Prova server"; /* No comment provided by engineer. */ "Test servers" = "Prova i server"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Test falliti!"; /* No comment provided by engineer. */ @@ -3308,15 +5046,24 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Grazie agli utenti – contribuite via Weblate!"; -/* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "La prima piattaforma senza alcun identificatore utente – privata by design."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "L'app può avvisarti quando ricevi messaggi o richieste di contatto: apri le impostazioni per attivare."; +/* 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 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)."; + /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "Il tentativo di cambiare la password del database non è stato completato."; +/* 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."; + +/* 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."; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "La connessione che hai accettato verrà annullata!"; @@ -3329,6 +5076,9 @@ /* No comment provided by engineer. */ "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"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "L'hash del messaggio precedente è diverso."; @@ -3342,13 +5092,22 @@ "The message will be marked as moderated for all members." = "Il messaggio sarà segnato come moderato per tutti i membri."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "La nuova generazione di messaggistica privata"; +"The messages will be deleted for all members." = "I messaggi verranno eliminati per tutti i membri."; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "I messaggi verranno contrassegnati come moderati per tutti i membri."; /* 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 profile is only shared with your contacts." = "Il profilo è condiviso solo con i tuoi contatti."; +"Your profile is stored on your device and only shared with your contacts." = "Il profilo è condiviso solo con i tuoi contatti."; + +/* No comment provided by engineer. */ +"The same conditions will apply to operator **%@**." = "Le stesse condizioni si applicheranno all'operatore **%@**."; + +/* No comment provided by engineer. */ +"The second preset operator in the app!" = "Il secondo operatore preimpostato nell'app!"; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Il secondo segno di spunta che ci mancava! ✅"; @@ -3360,13 +5119,19 @@ "The servers for new connections of your current chat profile **%@**." = "I server per le nuove connessioni del profilo di chat attuale **%@**."; /* No comment provided by engineer. */ -"Theme" = "Tema"; +"The servers for new files of your current chat profile **%@**." = "I server per nuovi file del tuo profilo di chat attuale **%@**."; /* No comment provided by engineer. */ -"There should be at least one user profile." = "Deve esserci almeno un profilo utente."; +"The text you pasted is not a SimpleX link." = "Il testo che hai incollato non è un link SimpleX."; /* No comment provided by engineer. */ -"There should be at least one visible user profile." = "Deve esserci almeno un profilo utente visibile."; +"The uploaded database archive will be permanently removed from the servers." = "L'archivio del database caricato verrà rimosso definitivamente dai server."; + +/* No comment provided by engineer. */ +"Themes" = "Temi"; + +/* No comment provided by engineer. */ +"These conditions will also apply for: **%@**." = "Queste condizioni si applicheranno anche per: **%@**."; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Queste impostazioni sono per il tuo profilo attuale **%@**."; @@ -3380,15 +5145,27 @@ /* 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." = "Questa azione non può essere annullata: i messaggi inviati e ricevuti prima di quanto selezionato verranno eliminati. Potrebbe richiedere diversi minuti."; +/* alert message */ +"This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." = "Questa azione non è reversibile: i messaggi inviati e ricevuti in questa chat prima della selezione verranno eliminati."; + /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Questa azione non può essere annullata: il tuo profilo, i contatti, i messaggi e i file andranno persi in modo irreversibile."; +/* E2EE info chat item */ +"This chat is protected by end-to-end encryption." = "Questa chat è protetta da crittografia end-to-end."; + +/* E2EE info chat item */ +"This chat is protected by quantum resistant end-to-end encryption." = "Questa chat è protetta da crittografia end-to-end resistente alla quantistica."; + /* notification title */ "this contact" = "questo contatto"; /* No comment provided by engineer. */ "This device name" = "Il nome di questo dispositivo"; +/* No comment provided by engineer. */ +"This display name is invalid. Please choose another name." = "Questo nome da mostrare non è valido. Scegline un altro."; + /* No comment provided by engineer. */ "This group has over %lld members, delivery receipts are not sent." = "Questo gruppo ha più di %lld membri, le ricevute di consegna non vengono inviate."; @@ -3401,9 +5178,21 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Questo è il tuo indirizzo SimpleX!"; +/* 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."; + +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Questo link è stato usato con un altro dispositivo mobile, creane uno nuovo sul desktop."; + +/* No comment provided by engineer. */ +"This message was deleted or not received yet." = "Questo messaggio è stato eliminato o non ancora ricevuto."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Questa impostazione si applica ai messaggi del profilo di chat attuale **%@**."; +/* No comment provided by engineer. */ +"Title" = "Titoli"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Per porre domande e ricevere aggiornamenti:"; @@ -3417,7 +5206,7 @@ "To make a new connection" = "Per creare una nuova connessione"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Per proteggere la privacy, invece degli ID utente utilizzati da tutte le altre piattaforme, SimpleX ha identificatori per le code di messaggi, separati per ciascuno dei tuoi contatti."; +"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."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Per proteggere il fuso orario, i file immagine/vocali usano UTC."; @@ -3425,24 +5214,60 @@ /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Per proteggere le tue informazioni, attiva SimpleX Lock.\nTi verrà chiesto di completare l'autenticazione prima di attivare questa funzionalità."; +/* No comment provided by engineer. */ +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "Per proteggere il tuo indirizzo IP, l'instradamento privato usa i tuoi server SMP per consegnare i messaggi."; + +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Per proteggere la privacy, invece degli ID utente utilizzati da tutte le altre piattaforme, SimpleX ha identificatori per le code di messaggi, separati per ciascuno dei tuoi contatti."; + +/* No comment provided by engineer. */ +"To receive" = "Per ricevere"; + +/* No comment provided by engineer. */ +"To record speech please grant permission to use Microphone." = "Per registrare l'audio, concedi l'autorizzazione di usare il microfono."; + +/* No comment provided by engineer. */ +"To record video please grant permission to use Camera." = "Per registrare il video, concedi l'autorizzazione di usare la fotocamera."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Per registrare un messaggio vocale, concedi l'autorizzazione all'uso del microfono."; /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Per rivelare il tuo profilo nascosto, inserisci una password completa in un campo di ricerca nella pagina **I tuoi profili di chat**."; +/* No comment provided by engineer. */ +"To send" = "Per inviare"; + /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Per supportare le notifiche push istantanee, il database della chat deve essere migrato."; +/* No comment provided by engineer. */ +"To use the servers of **%@**, accept conditions of use." = "Per usare i server di **%@**, accetta le condizioni d'uso."; + /* 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."; +/* token status */ +"Token status: %@." = "Stato del token: %@."; + +/* No comment provided by engineer. */ +"Toolbar opacity" = "Opacità barra degli strumenti"; + +/* No comment provided by engineer. */ +"Total" = "Totale"; + /* No comment provided by engineer. */ "Transport isolation" = "Isolamento del trasporto"; +/* No comment provided by engineer. */ +"Transport sessions" = "Sessioni di trasporto"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Tentativo di connessione al server usato per ricevere messaggi da questo contatto (errore: %@)."; @@ -3450,10 +5275,10 @@ "Trying to connect to the server used to receive messages from this contact." = "Tentativo di connessione al server usato per ricevere messaggi da questo contatto."; /* No comment provided by engineer. */ -"Turn off" = "Spegni"; +"Turkish interface" = "Interfaccia in turco"; /* No comment provided by engineer. */ -"Turn off notifications?" = "Spegnere le notifiche?"; +"Turn off" = "Spegni"; /* No comment provided by engineer. */ "Turn on" = "Attiva"; @@ -3464,19 +5289,28 @@ /* No comment provided by engineer. */ "Unblock" = "Sblocca"; +/* No comment provided by engineer. */ +"Unblock for all" = "Sblocca per tutti"; + /* No comment provided by engineer. */ "Unblock member" = "Sblocca membro"; +/* No comment provided by engineer. */ +"Unblock member for all?" = "Sbloccare il membro per tutti?"; + /* No comment provided by engineer. */ "Unblock member?" = "Sbloccare il membro?"; -/* item status description */ -"Unexpected error: %@" = "Errore imprevisto: % @"; +/* rcv group event chat item */ +"unblocked %@" = "ha sbloccato %@"; + +/* No comment provided by engineer. */ +"Undelivered messages" = "Messaggi non consegnati"; /* No comment provided by engineer. */ "Unexpected migration state" = "Stato di migrazione imprevisto"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Non pref."; /* No comment provided by engineer. */ @@ -3503,6 +5337,15 @@ /* No comment provided by engineer. */ "Unknown error" = "Errore sconosciuto"; +/* No comment provided by engineer. */ +"unknown servers" = "relay sconosciuti"; + +/* alert title */ +"Unknown servers!" = "Server sconosciuti!"; + +/* No comment provided by engineer. */ +"unknown status" = "stato sconosciuto"; + /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "A meno che non utilizzi l'interfaccia di chiamata iOS, attiva la modalità Non disturbare per evitare interruzioni."; @@ -3521,17 +5364,23 @@ /* authentication reason */ "Unlock app" = "Sblocca l'app"; -/* No comment provided by engineer. */ +/* notification label action */ "Unmute" = "Riattiva notifiche"; /* No comment provided by engineer. */ +"unprotected" = "non protetto"; + +/* swipe action */ "Unread" = "Non letto"; /* No comment provided by engineer. */ -"Update" = "Aggiorna"; +"Unsupported connection link" = "Link di connessione non supportato"; /* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Aggiornare l'impostazione degli host .onion?"; +"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. */ +"Update" = "Aggiorna"; /* No comment provided by engineer. */ "Update database passphrase" = "Aggiorna la password del database"; @@ -3540,32 +5389,59 @@ "Update network settings?" = "Aggiornare le impostazioni di rete?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Aggiornare la modalità di isolamento del trasporto?"; +"Update settings?" = "Aggiornare le impostazioni?"; + +/* No comment provided by engineer. */ +"Updated conditions" = "Condizioni aggiornate"; /* rcv group event chat item */ -"updated group profile" = "profilo del gruppo aggiornato"; +"updated group profile" = "ha aggiornato il profilo del gruppo"; + +/* profile update event chat item */ +"updated profile" = "profilo aggiornato"; /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "L'aggiornamento delle impostazioni riconnetterà il client a tutti i server."; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "L'aggiornamento di questa impostazione riconnetterà il client a tutti i server."; +"Upgrade and open chat" = "Aggiorna e apri chat"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "Aggiorna e apri chat"; +"Upload errors" = "Errori di invio"; + +/* No comment provided by engineer. */ +"Upload failed" = "Invio fallito"; /* server test step */ "Upload file" = "Invia file"; +/* No comment provided by engineer. */ +"Uploaded" = "Inviato"; + +/* No comment provided by engineer. */ +"Uploaded files" = "File inviati"; + +/* No comment provided by engineer. */ +"Uploading archive" = "Invio dell'archivio"; + /* No comment provided by engineer. */ "Use .onion hosts" = "Usa gli host .onion"; +/* No comment provided by engineer. */ +"Use %@" = "Usa %@"; + /* No comment provided by engineer. */ "Use chat" = "Usa la chat"; /* No comment provided by engineer. */ "Use current profile" = "Usa il profilo attuale"; +/* No comment provided by engineer. */ +"Use for files" = "Usa per i file"; + +/* No comment provided by engineer. */ +"Use for messages" = "Usa per i messaggi"; + /* No comment provided by engineer. */ "Use for new connections" = "Usa per connessioni nuove"; @@ -3578,17 +5454,50 @@ /* No comment provided by engineer. */ "Use new incognito profile" = "Usa nuovo profilo in incognito"; +/* No comment provided by engineer. */ +"Use only local notifications?" = "Usare solo notifiche locali?"; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "Usa l'instradamento privato con server sconosciuti quando l'indirizzo IP non è protetto."; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "Usa l'instradamento privato con server sconosciuti."; + /* No comment provided by engineer. */ "Use server" = "Usa il server"; +/* No comment provided by engineer. */ +"Use servers" = "Usa i server"; + +/* No comment provided by engineer. */ +"Use short links (BETA)" = "Usa link brevi (BETA)"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "Usare i server di SimpleX Chat?"; /* No comment provided by engineer. */ -"User profile" = "Profilo utente"; +"Use SOCKS proxy" = "Usa proxy SOCKS"; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "L'uso di host .onion richiede un fornitore di VPN compatibile."; +"Use TCP port %@ when no port is specified." = "Usa la porta TCP %@ quando non è specificata alcuna porta."; + +/* No comment provided by engineer. */ +"Use TCP port 443 for preset servers only." = "Usa la porta TCP 443 solo per i server preimpostati."; + +/* No comment provided by engineer. */ +"Use the app while in the call." = "Usa l'app mentre sei in chiamata."; + +/* No comment provided by engineer. */ +"Use the app with one hand." = "Usa l'app con una mano sola."; + +/* No comment provided by engineer. */ +"Use web port" = "Usa porta web"; + +/* No comment provided by engineer. */ +"User selection" = "Selezione utente"; + +/* No comment provided by engineer. */ +"Username" = "Nome utente"; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Utilizzo dei server SimpleX Chat."; @@ -3611,6 +5520,12 @@ /* No comment provided by engineer. */ "Verify connections" = "Verifica le connessioni"; +/* No comment provided by engineer. */ +"Verify database passphrase" = "Verifica password del database"; + +/* No comment provided by engineer. */ +"Verify passphrase" = "Verifica password"; + /* No comment provided by engineer. */ "Verify security code" = "Verifica codice di sicurezza"; @@ -3632,6 +5547,9 @@ /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Tramite protocollo sicuro resistente alla quantistica."; +/* No comment provided by engineer. */ +"video" = "video"; + /* No comment provided by engineer. */ "Video call" = "Videochiamata"; @@ -3647,9 +5565,18 @@ /* No comment provided by engineer. */ "Videos and files up to 1gb" = "Video e file fino a 1 GB"; +/* No comment provided by engineer. */ +"View conditions" = "Vedi le condizioni"; + /* No comment provided by engineer. */ "View security code" = "Vedi codice di sicurezza"; +/* No comment provided by engineer. */ +"View updated conditions" = "Vedi le condizioni aggiornate"; + +/* chat feature */ +"Visible history" = "Cronologia visibile"; + /* No comment provided by engineer. */ "Voice message…" = "Messaggio vocale…"; @@ -3660,7 +5587,10 @@ "Voice messages are prohibited in this chat." = "I messaggi vocali sono vietati in questa chat."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "I messaggi vocali sono vietati in questo gruppo."; +"Voice messages are prohibited." = "I messaggi vocali sono vietati in questo gruppo."; + +/* No comment provided by engineer. */ +"Voice messages not allowed" = "Messaggi vocali non consentiti"; /* No comment provided by engineer. */ "Voice messages prohibited!" = "Messaggi vocali vietati!"; @@ -3671,6 +5601,9 @@ /* No comment provided by engineer. */ "waiting for confirmation…" = "in attesa di conferma…"; +/* No comment provided by engineer. */ +"Waiting for desktop..." = "In attesa del desktop..."; + /* No comment provided by engineer. */ "Waiting for file" = "In attesa del file"; @@ -3680,9 +5613,18 @@ /* No comment provided by engineer. */ "Waiting for video" = "In attesa del video"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Tinta dello sfondo"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Retro dello sfondo"; + /* No comment provided by engineer. */ "wants to connect to you!" = "vuole connettersi con te!"; +/* No comment provided by engineer. */ +"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "Attenzione: avviare la chat su più dispositivi non è supportato e provocherà problemi di recapito dei messaggi"; + /* No comment provided by engineer. */ "Warning: you may lose some data!" = "Attenzione: potresti perdere alcuni dati!"; @@ -3698,6 +5640,9 @@ /* No comment provided by engineer. */ "Welcome message" = "Messaggio di benvenuto"; +/* No comment provided by engineer. */ +"Welcome message is too long" = "Il messaggio di benvenuto è troppo lungo"; + /* No comment provided by engineer. */ "What's new" = "Novità"; @@ -3705,28 +5650,64 @@ "When available" = "Quando disponibili"; /* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Quando le persone chiedono di connettersi, puoi accettare o rifiutare."; +"When connecting audio and video calls." = "Quando si connettono le chiamate audio e video."; + +/* No comment provided by engineer. */ +"when IP hidden" = "quando l'IP è nascosto"; + +/* No comment provided by engineer. */ +"When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "Quando più di un operatore è attivato, nessuno di essi ha metadati per scoprire chi comunica con chi."; /* 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. */ +"WiFi" = "WiFi"; + +/* No comment provided by engineer. */ +"Will be enabled in direct chats!" = "Viene attivata nelle chat dirette!"; + +/* No comment provided by engineer. */ +"Wired ethernet" = "Cavo ethernet"; + +/* No comment provided by engineer. */ +"With encrypted files and media." = "Con file e multimediali criptati."; + /* No comment provided by engineer. */ "With optional welcome message." = "Con messaggio di benvenuto facoltativo."; +/* No comment provided by engineer. */ +"With reduced battery usage." = "Con consumo di batteria ridotto."; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "Senza Tor o VPN, il tuo indirizzo IP sarà visibile ai server di file."; + +/* alert message */ +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Senza Tor o VPN, il tuo indirizzo IP sarà visibile a questi relay XFTP: %@."; + /* No comment provided by engineer. */ "Wrong database passphrase" = "Password del database sbagliata"; +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "Chiave sbagliata o connessione sconosciuta - molto probabilmente questa connessione è stata eliminata."; + +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Chiave sbagliata o indirizzo sconosciuto per frammento del file - probabilmente il file è stato eliminato."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Password sbagliata!"; /* No comment provided by engineer. */ -"XFTP servers" = "Server XFTP"; +"XFTP server" = "Server XFTP"; /* pref value */ "yes" = "sì"; /* No comment provided by engineer. */ -"You" = "Tu"; +"you" = "tu"; + +/* No comment provided by engineer. */ +"You **must not** use the same database on two devices." = "**Non devi** usare lo stesso database su due dispositivi."; /* No comment provided by engineer. */ "You accepted connection" = "Hai accettato la connessione"; @@ -3740,6 +5721,9 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "Sei già connesso/a a %@."; +/* No comment provided by engineer. */ +"You are already connected with %@." = "Sei già connesso/a con %@."; + /* No comment provided by engineer. */ "You are already connecting to %@." = "Ti stai già connettendo a %@."; @@ -3770,14 +5754,23 @@ /* No comment provided by engineer. */ "You are invited to group" = "Sei stato/a invitato/a al gruppo"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Non sei connesso/a a questi server. L'instradamento privato è usato per consegnare loro i messaggi."; + /* No comment provided by engineer. */ "you are observer" = "sei un osservatore"; +/* snd group event chat item */ +"you blocked %@" = "hai bloccato %@"; + /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Puoi accettare chiamate dalla schermata di blocco, senza l'autenticazione del dispositivo e dell'app."; /* No comment provided by engineer. */ -"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "Puoi anche connetterti cliccando il link. Se si apre nel browser, clicca il pulsante **Apri nell'app mobile**."; +"You can change it in Appearance settings." = "Puoi cambiarlo nelle impostazioni dell'aspetto."; + +/* No comment provided by engineer. */ +"You can configure servers via settings." = "Puoi configurare i server nelle impostazioni."; /* No comment provided by engineer. */ "You can create it later" = "Puoi crearlo più tardi"; @@ -3788,11 +5781,23 @@ /* No comment provided by engineer. */ "You can enable them later via app Privacy & Security settings." = "Puoi attivarle più tardi nelle impostazioni di privacy e sicurezza dell'app."; +/* No comment provided by engineer. */ +"You can give another try." = "Puoi fare un altro tentativo."; + /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "Puoi nascondere o silenziare un profilo utente - scorrilo verso destra."; +/* No comment provided by engineer. */ +"You can make it visible to your SimpleX contacts via Settings." = "Puoi renderlo visibile ai tuoi contatti SimpleX nelle impostazioni."; + /* notification body */ -"You can now send messages to %@" = "Ora puoi inviare messaggi a %@"; +"You can now chat with %@" = "Ora puoi inviare messaggi a %@"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "Puoi inviare messaggi a %@ dai contatti archiviati."; + +/* No comment provided by engineer. */ +"You can set connection name, to remember who the link was shared with." = "Puoi impostare il nome della connessione per ricordare con chi è stato condiviso il link."; /* 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."; @@ -3804,10 +5809,10 @@ "You can share this address with your contacts to let them connect with **%@**." = "Puoi condividere questo indirizzo con i tuoi contatti per consentire loro di connettersi con **%@**."; /* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Puoi condividere il tuo indirizzo come link o come codice QR: chiunque potrà connettersi a te."; +"You can start chat via app Settings / Database or by restarting the app" = "Puoi avviare la chat via Impostazioni / Database o riavviando l'app"; /* No comment provided by engineer. */ -"You can start chat via app Settings / Database or by restarting the app" = "Puoi avviare la chat via Impostazioni / Database o riavviando l'app"; +"You can still view conversation with %@ in the list of chats." = "Puoi ancora vedere la conversazione con %@ nell'elenco delle chat."; /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Puoi attivare SimpleX Lock tramite le impostazioni."; @@ -3815,6 +5820,9 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Puoi usare il markdown per formattare i messaggi:"; +/* alert message */ +"You can view invitation link again in connection details." = "Puoi vedere di nuovo il link di invito nei dettagli di connessione."; + /* No comment provided by engineer. */ "You can't send messages!" = "Non puoi inviare messaggi!"; @@ -3831,10 +5839,10 @@ "you changed role of %@ to %@" = "hai cambiato il ruolo di %1$@ in %2$@"; /* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Puoi controllare attraverso quale/i server **ricevere** i messaggi, i tuoi contatti – i server che usi per inviare loro i messaggi."; +"You could not be verified; please try again." = "Non è stato possibile verificarti, riprova."; /* No comment provided by engineer. */ -"You could not be verified; please try again." = "Non è stato possibile verificarti, riprova."; +"You decide who can connect." = "Sei tu a decidere chi può connettersi."; /* No comment provided by engineer. */ "You have already requested connection via this address!" = "Hai già richiesto la connessione tramite questo indirizzo!"; @@ -3842,9 +5850,6 @@ /* No comment provided by engineer. */ "You have already requested connection!\nRepeat connection request?" = "Hai già richiesto la connessione!\nRipetere la richiesta di connessione?"; -/* No comment provided by engineer. */ -"You have no chats" = "Non hai chat"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Devi inserire la password ogni volta che si avvia l'app: non viene memorizzata sul dispositivo."; @@ -3860,9 +5865,18 @@ /* snd group event chat item */ "you left" = "sei uscito/a"; +/* No comment provided by engineer. */ +"You may migrate the exported database." = "Puoi migrare il database esportato."; + +/* No comment provided by engineer. */ +"You may save the exported archive." = "Puoi salvare l'archivio esportato."; + /* 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." = "Devi usare la versione più recente del tuo database della chat SOLO su un dispositivo, altrimenti potresti non ricevere più i messaggi da alcuni contatti."; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "Devi consentire le chiamate al tuo contatto per poterlo chiamare."; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "Devi consentire al tuo contatto di inviare messaggi vocali per poterli inviare anche tu."; @@ -3881,6 +5895,12 @@ /* chat list item description */ "you shared one-time link incognito" = "hai condiviso un link incognito una tantum"; +/* token info */ +"You should receive notifications." = "Dovresti ricevere le notifiche."; + +/* snd group event chat item */ +"you unblocked %@" = "hai sbloccato %@"; + /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "Verrai connesso/a al gruppo quando il dispositivo dell'host del gruppo sarà in linea, attendi o controlla più tardi!"; @@ -3902,6 +5922,9 @@ /* 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 chat. Chat history will be preserved." = "Non riceverai più messaggi da questa chat. La cronologia della chat verrà conservata."; + /* No comment provided by engineer. */ "You will stop receiving messages from this group. Chat history will be preserved." = "Non riceverai più messaggi da questo gruppo. La cronologia della chat verrà conservata."; @@ -3917,9 +5940,6 @@ /* 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" = "Stai usando un profilo in incognito per questo gruppo: per impedire la condivisione del tuo profilo principale non è consentito invitare contatti"; -/* No comment provided by engineer. */ -"Your %@ servers" = "I tuoi server %@"; - /* No comment provided by engineer. */ "Your calls" = "Le tue chiamate"; @@ -3929,11 +5949,14 @@ /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Il tuo database della chat non è crittografato: imposta la password per crittografarlo."; +/* alert title */ +"Your chat preferences" = "Le tue preferenze della chat"; + /* No comment provided by engineer. */ "Your chat profiles" = "I tuoi profili di chat"; /* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Il tuo contatto deve essere in linea per completare la connessione.\nPuoi annullare questa connessione e rimuovere il contatto (e riprovare più tardi con un link nuovo)."; +"Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "La tua connessione è stata spostata a %@, ma si è verificato un errore imprevisto durante il reindirizzamento al profilo."; /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Il tuo contatto ha inviato un file più grande della dimensione massima attualmente supportata (%@)."; @@ -3942,10 +5965,10 @@ "Your contacts can allow full message deletion." = "I tuoi contatti possono consentire l'eliminazione completa dei messaggi."; /* No comment provided by engineer. */ -"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "I tuoi contatti in SimpleX lo vedranno.\nPuoi modificarlo nelle impostazioni."; +"Your contacts will remain connected." = "I tuoi contatti resteranno connessi."; /* No comment provided by engineer. */ -"Your contacts will remain connected." = "I tuoi contatti resteranno connessi."; +"Your credentials may be sent unencrypted." = "Le credenziali potrebbero essere inviate in chiaro."; /* No comment provided by engineer. */ "Your current chat database will be DELETED and REPLACED with the imported one." = "Il tuo attuale database della chat verrà ELIMINATO e SOSTITUITO con quello importato."; @@ -3966,10 +5989,13 @@ "Your profile" = "Il tuo profilo"; /* No comment provided by engineer. */ -"Your profile **%@** will be shared." = "Il tuo profilo **%@** verrà condiviso."; +"Your profile **%@** will be shared." = "Verrà condiviso il tuo profilo **%@**."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti.\nI server di SimpleX non possono vedere il tuo profilo."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti. I server di SimpleX non possono vedere il tuo profilo."; + +/* 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 profile, contacts and delivered messages are stored on your device." = "Il tuo profilo, i contatti e i messaggi recapitati sono memorizzati sul tuo dispositivo."; @@ -3978,10 +6004,10 @@ "Your random profile" = "Il tuo profilo casuale"; /* No comment provided by engineer. */ -"Your server" = "Il tuo server"; +"Your server address" = "L'indirizzo del tuo server"; /* No comment provided by engineer. */ -"Your server address" = "L'indirizzo del tuo server"; +"Your servers" = "I tuoi server"; /* No comment provided by engineer. */ "Your settings" = "Le tue impostazioni"; @@ -3989,9 +6015,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "Il tuo indirizzo SimpleX"; -/* No comment provided by engineer. */ -"Your SMP servers" = "I tuoi server SMP"; - -/* No comment provided by engineer. */ -"Your XFTP servers" = "I tuoi server XFTP"; - diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index ff97d71daf..d214f88e1c 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -1,47 +1,29 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (コピー可能)"; /* No comment provided by engineer. */ "_italic_" = "\\_斜体_"; +/* No comment provided by engineer. */ +"- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." = "- [ディレクトリサービス](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) に接続 (ベータ)!\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. */ +"- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- 任意で削除された連絡先へ通知します。\n- プロフィール名に空白を含めることができます。\n- and more!"; + /* No comment provided by engineer. */ "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- 最長 5 分間の音声メッセージ。\n- 消えるまでのカスタム時間。\n- 編集履歴。"; -/* No comment provided by engineer. */ -", " = ", "; - -/* No comment provided by engineer. */ -": " = ": "; - /* No comment provided by engineer. */ "!1 colored!" = "!1 色付き!"; /* No comment provided by engineer. */ -"." = "."; +"(new)" = "(新規)"; /* No comment provided by engineer. */ -"(" = "("; - -/* 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)"; @@ -53,40 +35,43 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[GitHub でスターを付ける](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**新しい連絡先を追加**: 連絡先のワンタイム QR コードまたはリンクを作成します。"; +"**Create 1-time link**: to create and share a new invitation link." = "**コンタクトの追加**: 新しい招待リンクを作成するか、受け取ったリンクから接続します。"; /* No comment provided by engineer. */ -"**Create link / QR code** for your contact to use." = "連絡先が使用する **リンク/QR コードを作成します**。"; +"**Create group**: to create a new group." = "**グループ作成**: 新しいグループを作成する。"; /* No comment provided by engineer. */ -"**e2e encrypted** audio call" = "**e2e 暗号化**された音声通話"; +"**e2e encrypted** audio call" = "**エンドツーエンド暗号化済み**の音声通話"; /* No comment provided by engineer. */ -"**e2e encrypted** video call" = "**エンドツーエンド暗号化済み**のビデオ通話"; +"**e2e encrypted** video call" = "**エンドツーエンド暗号化済み**の テレビ電話 通話"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**よりプライベート**: 20 分ごとに新しいメッセージを確認します。 デバイス トークンは SimpleX Chat サーバーと共有されますが、連絡先やメッセージの数は共有されません。"; +"**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." = "**よりプライベート**: 20 分ごとに新しいメッセージを確認します。 デバイス トークンは SimpleX Chat サーバーと共有されますが、連絡先やメッセージの数は共有されません。"; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**最もプライベート**: SimpleX Chat 通知サーバーを使用せず、バックグラウンドで定期的にメッセージをチェックします (アプリの使用頻度によって異なります)。"; +"**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." = "**最もプライベート**: SimpleX Chat 通知サーバーを使用せず、バックグラウンドで定期的にメッセージをチェックします (アプリの使用頻度によって異なります)。"; /* No comment provided by engineer. */ -"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**受信したリンク**を貼り付けるか、ブラウザーで開いて [**モバイル アプリで開く**] をタップします。"; +"**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**注意**: 2つの端末で同じデータベースを使用すると、セキュリティ保護として、あなたが接続しているメッセージの復号化が解除されます。"; /* No comment provided by engineer. */ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**注意**: パスフレーズを紛失すると、パスフレーズを復元または変更できなくなります。"; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**推奨**: デバイス トークンと通知は SimpleX Chat 通知サーバーに送信されますが、メッセージの内容、サイズ、送信者は送信されません。"; +"**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 通知サーバーに送信されますが、メッセージの内容、サイズ、送信者は送信されません。"; /* No comment provided by engineer. */ -"**Scan QR code**: to connect to your contact in person or via video call." = "**QR コードをスキャン**: 直接またはビデオ通話で連絡先に接続します。"; +"**Scan / Paste link**: to connect via a link you received." = "**QRスキャン / リンクの貼り付け**: 受け取ったリンクで接続する。"; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**警告**: 即時の プッシュ通知には、キーチェーンに保存されたパスフレーズが必要です。"; /* No comment provided by engineer. */ -"*bold*" = "\\*太文字*"; +"**Warning**: the archive will be removed." = "**警告**: アーカイブデータは削除されます。"; + +/* No comment provided by engineer. */ +"*bold*" = "\\*太字*"; /* copied message info title, # <title> */ "# %@" = "# %@"; @@ -104,7 +89,7 @@ "%@" = "%@"; /* No comment provided by engineer. */ -"%@ (current)" = "%@ (現在)"; +"%@ (current)" = "%@ (現在)"; /* copied message info */ "%@ (current):" = "%@ (現在):"; @@ -115,12 +100,21 @@ /* No comment provided by engineer. */ "%@ %@" = "%@ %@"; +/* No comment provided by engineer. */ +"%@ and %@" = "%@ と %@"; + /* No comment provided by engineer. */ "%@ and %@ connected" = "%@ と %@ は接続中"; /* copied message info, <sender> at <time> */ "%@ at %@:" = "%1$@ at %2$@:"; +/* No comment provided by engineer. */ +"%@ connected" = "%@ 接続中"; + +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ ダウンロード済"; + /* notification title */ "%@ is connected!" = "%@ 接続中!"; @@ -130,12 +124,24 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ は検証されています"; +/* No comment provided by engineer. */ +"%@ server" = "%@ サーバー"; + /* No comment provided by engineer. */ "%@ servers" = "%@ サーバー"; +/* No comment provided by engineer. */ +"%@ uploaded" = "%@ アップロード済"; + /* notification title */ "%@ wants to connect!" = "%@ が接続を希望しています!"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + +/* No comment provided by engineer. */ +"%@, %@ and %lld members" = "%@や%@など%lld人のメンバー"; + /* No comment provided by engineer. */ "%@, %@ and %lld other members connected" = "%@, %@ および %lld 人のメンバーが接続中"; @@ -145,9 +151,24 @@ /* time interval */ "%d days" = "%d 日"; +/* forward confirmation reason */ +"%d file(s) are still being downloaded." = "%d 個のファイルをダウンロードしています。"; + +/* forward confirmation reason */ +"%d file(s) failed to download." = "%d 個のファイルがダウンロードに失敗しました。"; + +/* forward confirmation reason */ +"%d file(s) were deleted." = "%d 個のファイルが削除されました。"; + +/* forward confirmation reason */ +"%d file(s) were not downloaded." = "%d 個のファイルがダウンロードされていません。"; + /* time interval */ "%d hours" = "%d 時"; +/* alert title */ +"%d messages not forwarded" = "%d 個のメッセージが未転送"; + /* time interval */ "%d min" = "%d 分"; @@ -164,7 +185,7 @@ "%d weeks" = "%d 週"; /* No comment provided by engineer. */ -"%lld" = "%lld"; +"%lld" = ""; /* No comment provided by engineer. */ "%lld %@" = "%lld %@"; @@ -175,18 +196,30 @@ /* No comment provided by engineer. */ "%lld file(s) with total size of %@" = "%lld 個のファイル(合計サイズ: %@)"; +/* No comment provided by engineer. */ +"%lld group events" = "%lld件のグループイベント"; + /* No comment provided by engineer. */ "%lld members" = "%lld 人のメンバー"; +/* No comment provided by engineer. */ +"%lld messages blocked" = "%lld件のメッセージをブロック"; + +/* No comment provided by engineer. */ +"%lld messages blocked by admin" = "%lldのメッセージが管理者によりブロック済"; + +/* No comment provided by engineer. */ +"%lld messages marked deleted" = "%lld 件のメッセージが削除されました"; + +/* No comment provided by engineer. */ +"%lld messages moderated by %@" = "%@ により%lld 件のメッセージが検閲されました"; + /* No comment provided by engineer. */ "%lld minutes" = "%lld 分"; /* No comment provided by engineer. */ "%lld new interface languages" = "%lldつの新しいインターフェース言語"; -/* No comment provided by engineer. */ -"%lld second(s)" = "%lld 秒"; - /* No comment provided by engineer. */ "%lld seconds" = "%lld 秒"; @@ -226,10 +259,14 @@ /* No comment provided by engineer. */ "~strike~" = "\\~取り消し線~"; +/* time to disappear */ +"0 sec" = "0 秒"; + /* No comment provided by engineer. */ "0s" = "0秒"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1日"; /* time interval */ @@ -238,14 +275,19 @@ /* No comment provided by engineer. */ "1 minute" = "1分"; -/* time interval */ +/* delete after time +time interval */ "1 month" = "1ヶ月"; -/* time interval */ +/* delete after time +time interval */ "1 week" = "1週間"; /* No comment provided by engineer. */ -"1-time link" = "使い捨てのリンク"; +"1-time link" = "使い捨てリンク"; + +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "使い捨てリンクは、*ひとつの連絡先にのみ* 使用できます - 対面または任意のチャットで共有してください。"; /* No comment provided by engineer. */ "5 minutes" = "5分"; @@ -280,32 +322,25 @@ /* No comment provided by engineer. */ "Abort changing address?" = "アドレス変更を中止しますか?"; -/* No comment provided by engineer. */ -"About SimpleX" = "SimpleXについて"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "SimpleXアドレスについて"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "SimpleX Chat について"; /* No comment provided by engineer. */ "above, then choose:" = "上で選んでください:"; -/* No comment provided by engineer. */ -"Accent color" = "アクセントカラー"; - /* accept contact request via notification - accept incoming call via notification */ +accept incoming call via notification +swipe action */ "Accept" = "承諾"; /* No comment provided by engineer. */ -"Accept connection request?" = "連絡を受け入れる"; +"Accept connection request?" = "接続要求を承認?"; /* notification body */ "Accept contact request from %@?" = "%@ からの連絡要求を受け入れますか?"; -/* accept contact request via notification */ +/* accept contact request via notification +swipe action */ "Accept incognito" = "シークレットモードで承諾"; /* call status */ @@ -314,14 +349,11 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "プロフィールにアドレスを追加し、連絡先があなたのアドレスを他の人と共有できるようにします。プロフィールの更新は連絡先に送信されます。"; -/* No comment provided by engineer. */ -"Add preset servers" = "既存サーバを追加"; - /* No comment provided by engineer. */ "Add profile" = "プロフィールを追加"; /* No comment provided by engineer. */ -"Add server…" = "サーバを追加…"; +"Add server" = "サーバを追加"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "QRコードでサーバを追加する。"; @@ -332,6 +364,12 @@ /* No comment provided by engineer. */ "Add welcome message" = "ウェルカムメッセージを追加"; +/* No comment provided by engineer. */ +"Added media & file servers" = "追加されたメディア & ファイルサーバー"; + +/* No comment provided by engineer. */ +"Added message servers" = "追加されたメッセージサーバー"; + /* No comment provided by engineer. */ "Address" = "アドレス"; @@ -347,6 +385,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "ネットワーク詳細設定"; +/* No comment provided by engineer. */ +"Advanced settings" = "詳細設定"; + /* chat item text */ "agreeing encryption for %@…" = "%@の暗号化に同意しています…"; @@ -368,11 +409,17 @@ /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "全てのメッセージが削除されます(※注意:元に戻せません!※)。削除されるのは片方あなたのメッセージのみ。"; +/* profile dropdown */ +"All profiles" = "すべてのプロフィール"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "あなたの連絡先が繋がったまま継続します。"; /* No comment provided by engineer. */ -"All your contacts will remain connected. Profile update will be sent to your contacts." = "あなたの連絡先が繋がったまま継続します。"; +"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リレーに分割でアップロードされます。"; /* No comment provided by engineer. */ "Allow" = "許可"; @@ -384,7 +431,7 @@ "Allow disappearing messages only if your contact allows it to you." = "連絡先が許可している場合のみ消えるメッセージを許可する。"; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you." = "送信相手も永久メッセージ削除を許可する時のみに許可する。"; +"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "送信相手も永久メッセージ削除を許可する時のみに許可する。(24時間)"; /* No comment provided by engineer. */ "Allow message reactions only if your contact allows them." = "連絡先が許可している場合にのみ、メッセージへのリアクションを許可します。"; @@ -399,11 +446,17 @@ "Allow sending disappearing messages." = "消えるメッセージの送信を許可する。"; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages." = "送信済みメッセージの永久削除を許可する。"; +"Allow sharing" = "共有を許可"; + +/* No comment provided by engineer. */ +"Allow to irreversibly delete sent messages. (24 hours)" = "送信済みメッセージの永久削除を許可する。(24時間)"; /* No comment provided by engineer. */ "Allow to send files and media." = "ファイルやメディアの送信を許可する。"; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "SimpleXリンクの送信を許可。"; + /* No comment provided by engineer. */ "Allow to send voice messages." = "音声メッセージの送信を許可する。"; @@ -420,7 +473,7 @@ "Allow your contacts to call you." = "連絡先からの通話を許可する。"; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages." = "送信相手が永久メッセージ削除するのを許可する。"; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "送信相手が永久メッセージ削除するのを許可する。(24時間)"; /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "送信相手が消えるメッセージを送るのを許可する。"; @@ -431,9 +484,18 @@ /* No comment provided by engineer. */ "Already connected?" = "すでに接続済みですか?"; +/* No comment provided by engineer. */ +"Already connecting!" = "既に接続中です!"; + +/* No comment provided by engineer. */ +"Already joining the group!" = "すでにグループに参加しています!"; + /* pref value */ "always" = "常に"; +/* No comment provided by engineer. */ +"Always use private routing." = "プライベートルーティングを常に使用する。"; + /* No comment provided by engineer. */ "Always use relay" = "常にリレーを経由する"; @@ -443,9 +505,15 @@ /* No comment provided by engineer. */ "Answer call" = "通話に応答"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。"; + /* No comment provided by engineer. */ "App build: %@" = "アプリのビルド: %@"; +/* No comment provided by engineer. */ +"App data migration" = "アプリデータの移行"; + /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "アプリは新しいローカルファイル(ビデオを除く)を暗号化します。"; @@ -465,7 +533,16 @@ "App version: v%@" = "アプリのバージョン: v%@"; /* No comment provided by engineer. */ -"Appearance" = "見た目"; +"Appearance" = "アピアランス"; + +/* No comment provided by engineer. */ +"Apply" = "適用"; + +/* No comment provided by engineer. */ +"Apply to" = "に適用する"; + +/* No comment provided by engineer. */ +"Archive and upload" = "アーカイブとアップロード"; /* No comment provided by engineer. */ "Attach" = "添付する"; @@ -531,7 +608,7 @@ "Both you and your contact can add message reactions." = "自分も相手もメッセージへのリアクションを追加できます。"; /* No comment provided by engineer. */ -"Both you and your contact can irreversibly delete sent messages." = "あなたと連絡相手が送信済みメッセージを永久削除できます。"; +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "あなたと連絡相手が送信済みメッセージを永久削除できます。(24時間)"; /* No comment provided by engineer. */ "Both you and your contact can make calls." = "あなたからも連絡先からも通話ができます。"; @@ -563,16 +640,14 @@ /* No comment provided by engineer. */ "Calls" = "通話"; -/* No comment provided by engineer. */ -"Can't delete user profile!" = "ユーザープロフィールが削除できません!"; - /* No comment provided by engineer. */ "Can't invite contact!" = "連絡先を招待できません!"; /* No comment provided by engineer. */ "Can't invite contacts!" = "連絡先を招待できません!"; -/* No comment provided by engineer. */ +/* alert action +alert button */ "Cancel" = "中止"; /* feature offered item */ @@ -581,7 +656,7 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "データベースのパスワードを保存するためのキーチェーンにアクセスできません"; -/* No comment provided by engineer. */ +/* alert title */ "Cannot receive file" = "ファイル受信ができません"; /* No comment provided by engineer. */ @@ -612,7 +687,7 @@ "Change self-destruct mode" = "自己破壊モードの変更"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "自己破壊パスコードを変更する"; /* chat item text */ @@ -630,9 +705,6 @@ /* chat item text */ "changing address…" = "アドレスを変更しています…"; -/* No comment provided by engineer. */ -"Chat archive" = "チャットのアーカイブ"; - /* No comment provided by engineer. */ "Chat console" = "チャットのコンソール"; @@ -655,9 +727,15 @@ "Chat preferences" = "チャット設定"; /* No comment provided by engineer. */ -"Chats" = "チャット"; +"Chat profile" = "ユーザープロフィール"; /* No comment provided by engineer. */ +"Chat theme" = "チャットテーマ"; + +/* No comment provided by engineer. */ +"Chats" = "チャット"; + +/* alert title */ "Check server address and try again." = "サーバのアドレスを確認してから再度試してください。"; /* No comment provided by engineer. */ @@ -670,6 +748,12 @@ "Choose from library" = "ライブラリから選択"; /* No comment provided by engineer. */ +"Chunks deleted" = "チャンクが削除されました"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "チャンクがダウンロードされました"; + +/* swipe action */ "Clear" = "消す"; /* No comment provided by engineer. */ @@ -678,14 +762,17 @@ /* No comment provided by engineer. */ "Clear conversation?" = "ダイアログのクリアしますか?"; +/* No comment provided by engineer. */ +"Clear private notes?" = "プライベートノートを消しますか?"; + /* No comment provided by engineer. */ "Clear verification" = "検証を消す"; /* No comment provided by engineer. */ -"colored" = "色付き"; +"Color mode" = "色設定"; /* No comment provided by engineer. */ -"Colors" = "色"; +"colored" = "色付き"; /* server test step */ "Compare file" = "ファイルを比較"; @@ -696,6 +783,9 @@ /* No comment provided by engineer. */ "complete" = "完了"; +/* No comment provided by engineer. */ +"Completed" = "完了"; + /* No comment provided by engineer. */ "Configure ICE servers" = "ICEサーバを設定"; @@ -720,24 +810,42 @@ /* No comment provided by engineer. */ "Connect incognito" = "シークレットモードで接続"; +/* No comment provided by engineer. */ +"Connect to desktop" = "デスクトップに接続"; + /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "SimpleX Chat 開発者に接続します。"; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "友達ともっと速くつながりましょう。"; + /* No comment provided by engineer. */ "Connect via link" = "リンク経由で接続"; /* No comment provided by engineer. */ -"Connect via link / QR code" = "リンク・QRコード経由で接続"; - -/* No comment provided by engineer. */ -"Connect via one-time link" = "使い捨てリンク経由で接続しますか?"; +"Connect via one-time link" = "ワンタイムリンクで接続"; /* No comment provided by engineer. */ "connected" = "接続中"; +/* No comment provided by engineer. */ +"Connected" = "接続中"; + +/* No comment provided by engineer. */ +"Connected desktop" = "デスクトップに接続済"; + +/* No comment provided by engineer. */ +"Connected servers" = "接続中のサーバ"; + +/* No comment provided by engineer. */ +"Connected to desktop" = "デスクトップに接続済"; + /* No comment provided by engineer. */ "connecting" = "接続待ち"; +/* No comment provided by engineer. */ +"Connecting" = "接続待ち"; + /* No comment provided by engineer. */ "connecting (accepted)" = "接続待ち (承諾済み)"; @@ -759,12 +867,21 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "サーバーに接続中… (エラー: %@)"; -/* chat list item title */ +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "連絡先に接続中です。しばらくお待ちいただくか、後で確認してください!"; + +/* No comment provided by engineer. */ +"Connecting to desktop" = "デスクトップに接続中"; + +/* No comment provided by engineer. */ "connecting…" = "接続待ち…"; /* No comment provided by engineer. */ "Connection" = "接続"; +/* No comment provided by engineer. */ +"Connection and servers status." = "接続とサーバーのステータス。"; + /* No comment provided by engineer. */ "Connection error" = "接続エラー"; @@ -777,6 +894,9 @@ /* No comment provided by engineer. */ "Connection request sent!" = "接続リクエストを送信しました!"; +/* No comment provided by engineer. */ +"Connection terminated" = "接続停止"; + /* No comment provided by engineer. */ "Connection timeout" = "接続タイムアウト"; @@ -801,9 +921,6 @@ /* notification */ "Contact is connected" = "連絡先は接続中"; -/* No comment provided by engineer. */ -"Contact is not connected yet!" = "連絡先がまだ繋がってません!"; - /* No comment provided by engineer. */ "Contact name" = "連絡先の名前"; @@ -819,7 +936,7 @@ /* No comment provided by engineer. */ "Continue" = "続ける"; -/* chat item action */ +/* No comment provided by engineer. */ "Copy" = "コピー"; /* No comment provided by engineer. */ @@ -828,9 +945,6 @@ /* No comment provided by engineer. */ "Create" = "作成"; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "人とつながるためのアドレスを作成する。"; - /* server test step */ "Create file" = "ファイルを作成"; @@ -844,7 +958,7 @@ "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "[デスクトップアプリ](https://simplex.chat/downloads/)で新しいプロファイルを作成します。 💻"; /* No comment provided by engineer. */ -"Create one-time invitation link" = "使い捨ての招待リンクを生成する"; +"Create profile" = "プロフィールを作成する"; /* server test step */ "Create queue" = "キューの作成"; @@ -858,9 +972,6 @@ /* No comment provided by engineer. */ "Create your profile" = "プロフィールを作成する"; -/* No comment provided by engineer. */ -"Created on %@" = "%@ によって作成されました"; - /* No comment provided by engineer. */ "creator" = "作成者"; @@ -879,9 +990,15 @@ /* No comment provided by engineer. */ "Custom time" = "カスタム時間"; +/* No comment provided by engineer. */ +"Customize theme" = "カスタムテーマ"; + /* No comment provided by engineer. */ "Dark" = "ダークモード"; +/* No comment provided by engineer. */ +"Dark mode colors" = "ダークモードカラー"; + /* No comment provided by engineer. */ "Database downgrade" = "データーベースのダウングレード"; @@ -942,13 +1059,17 @@ /* time unit */ "days" = "日"; +/* No comment provided by engineer. */ +"Debug delivery" = "配信のデバッグ"; + /* No comment provided by engineer. */ "Decentralized" = "分散型"; /* message decrypt error item */ "Decryption error" = "復号化エラー"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "デフォルト (%@)"; /* No comment provided by engineer. */ @@ -957,7 +1078,8 @@ /* No comment provided by engineer. */ "default (yes)" = "デフォルト(はい)"; -/* chat item action */ +/* alert action +swipe action */ "Delete" = "削除"; /* No comment provided by engineer. */ @@ -972,12 +1094,6 @@ /* No comment provided by engineer. */ "Delete all files" = "ファイルを全て削除"; -/* No comment provided by engineer. */ -"Delete archive" = "アーカイブを削除"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "チャットのアーカイブを削除しますか?"; - /* No comment provided by engineer. */ "Delete chat profile" = "チャットのプロフィールを削除する"; @@ -990,9 +1106,6 @@ /* No comment provided by engineer. */ "Delete contact" = "連絡先を削除"; -/* No comment provided by engineer. */ -"Delete Contact" = "連絡先を削除"; - /* No comment provided by engineer. */ "Delete database" = "データベースを削除"; @@ -1032,7 +1145,7 @@ /* No comment provided by engineer. */ "Delete message?" = "メッセージを削除しますか?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "メッセージを削除"; /* No comment provided by engineer. */ @@ -1044,9 +1157,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "古いデータベースを削除しますか?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "確認待ちの接続を削除"; - /* No comment provided by engineer. */ "Delete pending connection?" = "接続待ちの接続を削除しますか?"; @@ -1072,10 +1182,10 @@ "deleted group" = "削除されたグループ"; /* No comment provided by engineer. */ -"Delivery" = "Delivery"; +"Delivery" = "配信"; /* No comment provided by engineer. */ -"Delivery receipts are disabled!" = "Delivery receipts are disabled!"; +"Delivery receipts are disabled!" = "配信通知の停止!"; /* No comment provided by engineer. */ "Delivery receipts!" = "配信通知!"; @@ -1083,9 +1193,15 @@ /* No comment provided by engineer. */ "Description" = "説明"; +/* No comment provided by engineer. */ +"Desktop devices" = "デスクトップ機器"; + /* No comment provided by engineer. */ "Develop" = "開発"; +/* No comment provided by engineer. */ +"Developer options" = "開発者向けの設定"; + /* No comment provided by engineer. */ "Developer tools" = "開発ツール"; @@ -1111,7 +1227,7 @@ "Direct messages" = "ダイレクトメッセージ"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "このグループではメンバー間のダイレクトメッセージが使用禁止です。"; +"Direct messages between members are prohibited." = "このグループではメンバー間のダイレクトメッセージが使用禁止です。"; /* No comment provided by engineer. */ "Disable (keep overrides)" = "無効にする(設定の優先を維持)"; @@ -1135,7 +1251,7 @@ "Disappearing messages are prohibited in this chat." = "このチャットでは消えるメッセージが使用禁止です。"; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "このグループでは消えるメッセージが使用禁止です。"; +"Disappearing messages are prohibited." = "このグループでは消えるメッセージが使用禁止です。"; /* No comment provided by engineer. */ "Disappears at" = "に消えます"; @@ -1156,7 +1272,7 @@ "Do NOT use SimpleX for emergency calls." = "緊急通報にSimpleXを使用しないでください。"; /* No comment provided by engineer. */ -"Don't create address" = "アドレスを作成しないでください"; +"Don't create address" = "アドレスを作成しない"; /* No comment provided by engineer. */ "Don't enable" = "有効にしない"; @@ -1194,7 +1310,7 @@ /* No comment provided by engineer. */ "Enable (keep overrides)" = "有効にする(設定の優先を維持)"; -/* No comment provided by engineer. */ +/* alert title */ "Enable automatic message deletion?" = "自動メッセージ削除を有効にしますか?"; /* No comment provided by engineer. */ @@ -1329,9 +1445,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "連絡先リクエストの承諾にエラー発生"; -/* No comment provided by engineer. */ -"Error accessing database file" = "データベースファイルへのアクセスエラー"; - /* No comment provided by engineer. */ "Error adding member(s)" = "メンバー追加にエラー発生"; @@ -1371,9 +1484,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "接続の削除エラー"; -/* No comment provided by engineer. */ -"Error deleting contact" = "連絡先の削除にエラー発生"; - /* No comment provided by engineer. */ "Error deleting database" = "データベースの削除にエラー発生"; @@ -1401,18 +1511,12 @@ /* No comment provided by engineer. */ "Error joining group" = "グループ参加にエラー発生"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "%@ サーバーのロード中にエラーが発生"; - -/* No comment provided by engineer. */ +/* alert title */ "Error receiving file" = "ファイル受信にエラー発生"; /* No comment provided by engineer. */ "Error removing member" = "メンバー除名にエラー発生"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "%@ サーバの保存エラー"; - /* No comment provided by engineer. */ "Error saving group profile" = "グループのプロフィール保存にエラー発生"; @@ -1443,7 +1547,7 @@ /* No comment provided by engineer. */ "Error stopping chat" = "チャット停止にエラー発生"; -/* No comment provided by engineer. */ +/* alertTitle */ "Error switching profile!" = "プロフィール切り替えにエラー発生!"; /* No comment provided by engineer. */ @@ -1464,7 +1568,9 @@ /* No comment provided by engineer. */ "Error: " = "エラー : "; -/* No comment provided by engineer. */ +/* alert message +file error text +snd error text */ "Error: %@" = "エラー : %@"; /* No comment provided by engineer. */ @@ -1476,9 +1582,6 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "会話中に無効になっている場合でも。"; -/* No comment provided by engineer. */ -"event happened" = "イベント発生"; - /* No comment provided by engineer. */ "Exit without saving" = "保存せずに閉じる"; @@ -1500,7 +1603,7 @@ /* No comment provided by engineer. */ "Fast and no wait until the sender is online!" = "送信者がオンラインになるまでの待ち時間がなく、速い!"; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "お気に入り"; /* No comment provided by engineer. */ @@ -1522,7 +1625,7 @@ "Files and media" = "ファイルとメディア"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "このグループでは、ファイルとメディアは禁止されています。"; +"Files and media are prohibited." = "このグループでは、ファイルとメディアは禁止されています。"; /* No comment provided by engineer. */ "Files and media prohibited!" = "ファイルとメディアは禁止されています!"; @@ -1566,9 +1669,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "フルネーム (任意):"; -/* No comment provided by engineer. */ -"Full name:" = "フルネーム:"; - /* No comment provided by engineer. */ "Fully re-implemented - work in background!" = "完全に再実装されました - バックグラウンドで動作します!"; @@ -1608,24 +1708,6 @@ /* No comment provided by engineer. */ "Group links" = "グループのリンク"; -/* No comment provided by engineer. */ -"Group members can add message reactions." = "グループメンバーはメッセージへのリアクションを追加できます。"; - -/* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages." = "グループのメンバーがメッセージを完全削除することができます。"; - -/* No comment provided by engineer. */ -"Group members can send direct messages." = "グループのメンバーがダイレクトメッセージを送信できます。"; - -/* No comment provided by engineer. */ -"Group members can send disappearing messages." = "グループのメンバーが消えるメッセージを送信できます。"; - -/* No comment provided by engineer. */ -"Group members can send files and media." = "グループメンバーはファイルやメディアを送信できます。"; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "グループのメンバーが音声メッセージを送信できます。"; - /* notification */ "Group message:" = "グループメッセージ:"; @@ -1683,9 +1765,6 @@ /* time unit */ "hours" = "時間"; -/* No comment provided by engineer. */ -"How it works" = "技術の説明"; - /* No comment provided by engineer. */ "How SimpleX works" = "SimpleX の仕組み"; @@ -1704,9 +1783,6 @@ /* No comment provided by engineer. */ "If you can't meet in person, show QR code in a video call, or share the link." = "直接会えない場合は、ビデオ通話で QR コードを表示するか、リンクを共有してください。"; -/* No comment provided by engineer. */ -"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "直接会えない場合は、**ビデオ通話で QR コードを表示する**か、リンクを共有してください。"; - /* No comment provided by engineer. */ "If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "アプリを開くときにこのパスコードを入力すると、アプリのすべてのデータが元に戻せないように削除されます!"; @@ -1729,7 +1805,7 @@ "Immediately" = "即座に"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "スパムや悪質送信を防止"; +"Immune to spam" = "スパムや悪質送信を防止"; /* No comment provided by engineer. */ "Import" = "読み込む"; @@ -1798,10 +1874,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "インストール [ターミナル用SimpleX Chat](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "インスタントプッシュ通知は非表示になります!\n"; +"Instant" = "即時"; /* No comment provided by engineer. */ -"Instantly" = "すぐに"; +"Instant push notifications will be hidden!\n" = "インスタントプッシュ通知は非表示になります!\n"; /* No comment provided by engineer. */ "Interface" = "インターフェース"; @@ -1818,7 +1894,7 @@ /* invalid chat item */ "invalid data" = "無効なデータ"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "無効なサーバアドレス!"; /* item status text */ @@ -1864,7 +1940,7 @@ "Irreversible message deletion is prohibited in this chat." = "このチャットではメッセージの完全削除が使用禁止です。"; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "このグループではメッセージの完全削除が使用禁止です。"; +"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." = "これにより単一のチャット プロファイル内で、データを共有せずに多数の匿名の接続をすることができます。"; @@ -1887,7 +1963,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "日本語UI"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "参加"; /* No comment provided by engineer. */ @@ -1917,7 +1993,7 @@ /* No comment provided by engineer. */ "Learn more" = "さらに詳しく"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "脱退"; /* No comment provided by engineer. */ @@ -1959,24 +2035,15 @@ /* No comment provided by engineer. */ "Lock mode" = "ロックモード"; -/* No comment provided by engineer. */ -"Make a private connection" = "プライベートな接続をする"; - /* No comment provided by engineer. */ "Make one message disappear" = "メッセージを1つ消す"; /* No comment provided by engineer. */ "Make profile private!" = "プロフィールを非表示にできます!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "%@ サーバー アドレスが正しい形式で、行が区切られており、重複していないことを確認してください (%@)。"; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "WebRTC ICEサーバのアドレスを正しく1行ずつに分けて、重複しないように、形式もご確認ください。"; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "多くの人が次のような質問をしました: *SimpleX にユーザー識別子がない場合、どうやってメッセージを配信できるのですか?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "全員に対して削除済みマークを付ける"; @@ -2013,6 +2080,24 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "メンバーをグループから除名する (※元に戻せません※)!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "グループメンバーはメッセージへのリアクションを追加できます。"; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "グループのメンバーがメッセージを完全削除することができます。(24時間)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "グループのメンバーがダイレクトメッセージを送信できます。"; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "グループのメンバーが消えるメッセージを送信できます。"; + +/* No comment provided by engineer. */ +"Members can send files and media." = "グループメンバーはファイルやメディアを送信できます。"; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "グループのメンバーが音声メッセージを送信できます。"; + /* item status text */ "Message delivery error" = "メッセージ送信エラー"; @@ -2026,7 +2111,7 @@ "Message reactions are prohibited in this chat." = "このチャットではメッセージへのリアクションは禁止されています。"; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "このグループではメッセージへのリアクションは禁止されています。"; +"Message reactions are prohibited." = "このグループではメッセージへのリアクションは禁止されています。"; /* notification */ "message received" = "メッセージを受信"; @@ -2040,6 +2125,15 @@ /* No comment provided by engineer. */ "Messages & files" = "メッセージ & ファイル"; +/* 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." = "メッセージ、ファイル、通話は、前方秘匿性、否認可能性および侵入復元性を備えた**エンドツーエンドの暗号化**によって保護されます。"; + +/* 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…" = "データベースのアーカイブを移行しています…"; @@ -2053,7 +2147,7 @@ "Migration is completed" = "移行が完了しました"; /* No comment provided by engineer. */ -"Migrations: %@" = "移行: %@"; +"Migrations:" = "移行:"; /* time unit */ "minutes" = "分"; @@ -2073,7 +2167,7 @@ /* copied message info */ "Moderated at: %@" = "モデレーターによって介入済み: %@"; -/* No comment provided by engineer. */ +/* marked deleted chat item preview text */ "moderated by %@" = "%@ によってモデレートされた"; /* time unit */ @@ -2085,19 +2179,16 @@ /* item status description */ "Most likely this connection is deleted." = "おそらく、この接続は削除されています。"; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "恐らくこの連絡先があなたとの接続を削除しました。"; - /* No comment provided by engineer. */ "Multiple chat profiles" = "複数チャットのプロフィール"; -/* No comment provided by engineer. */ +/* notification label action */ "Mute" = "ミュート"; /* No comment provided by engineer. */ "Muted when inactive!" = "非アクティブ時はミュート!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "名前"; /* No comment provided by engineer. */ @@ -2109,7 +2200,7 @@ /* No comment provided by engineer. */ "Network status" = "ネットワーク状況"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "一度も"; /* notification */ @@ -2118,9 +2209,6 @@ /* notification */ "New contact:" = "新しい連絡先:"; -/* No comment provided by engineer. */ -"New database archive" = "新しいデータベースのアーカイブ"; - /* No comment provided by engineer. */ "New desktop app!" = "新しいデスクトップアプリ!"; @@ -2181,12 +2269,18 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "音声メッセージを録音する権限がありません"; +/* No comment provided by engineer. */ +"No push server" = "自分のみ"; + /* No comment provided by engineer. */ "No received or sent files" = "送受信済みのファイルがありません"; /* copied message info in history */ "no text" = "テキストなし"; +/* No comment provided by engineer. */ +"No user identifiers." = "世界初のユーザーIDのないプラットフォーム|設計も元からプライベート。"; + /* No comment provided by engineer. */ "Notifications" = "通知"; @@ -2200,31 +2294,25 @@ "observer" = "オブザーバー"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "オフ"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "オフ"; -/* No comment provided by engineer. */ -"Off (Local)" = "オフ(自分のみ)"; - /* feature offered item */ "offered %@" = "提供された %@"; /* feature offered item */ "offered %@: %@" = "提供された %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "OK"; /* No comment provided by engineer. */ "Old database" = "古いデータベース"; -/* No comment provided by engineer. */ -"Old database archive" = "過去のデータベースアーカイブ"; - /* group pref value */ "on" = "オン"; @@ -2232,16 +2320,16 @@ "One-time invitation link" = "使い捨ての招待リンク"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "接続にオニオンのホストが必要となります。VPN を有効にする必要があります。"; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "接続にオニオンのホストが必要となります。\nVPN を有効にする必要があります。"; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "オニオンのホストが利用可能時に使われます。VPN を有効にする必要があります。"; +"Onion hosts will be used when available.\nRequires compatible VPN." = "オニオンのホストが利用可能時に使われます。\nVPN を有効にする必要があります。"; /* No comment provided by engineer. */ "Onion hosts will not be used." = "オニオンのホストが使われません。"; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "**2 レイヤーのエンドツーエンド暗号化**を使用して送信されたユーザー プロファイル、連絡先、グループ、メッセージを保存できるのはクライアント デバイスのみです。"; +"Only client devices store user profiles, contacts, groups, and messages." = "**2 レイヤーのエンドツーエンド暗号化**を使用して送信されたユーザー プロファイル、連絡先、グループ、メッセージを保存できるのはクライアント デバイスのみです。"; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "グループ設定を変えられるのはグループのオーナーだけです。"; @@ -2256,7 +2344,7 @@ "Only you can add message reactions." = "メッセージへのリアクションを追加できるのは、あなただけです。"; /* No comment provided by engineer. */ -"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "メッセージの完全削除はあなたにしかできません (あなたの連絡先は削除対象とすることができます)。"; +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "メッセージの完全削除はあなたにしかできません (あなたの連絡先は削除対象とすることができます)。(24時間)"; /* No comment provided by engineer. */ "Only you can make calls." = "自分からのみ通話ができます。"; @@ -2271,7 +2359,7 @@ "Only your contact can add message reactions." = "メッセージへのリアクションを追加できるのは連絡先だけです。"; /* No comment provided by engineer. */ -"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "メッセージを完全削除できるのはあなたの連絡相手だけです (あなたは削除対象とすることができます)。"; +"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "メッセージを完全削除できるのはあなたの連絡相手だけです (あなたは削除対象とすることができます)。(24時間)"; /* No comment provided by engineer. */ "Only your contact can make calls." = "連絡先からのみ通話ができます。"; @@ -2282,7 +2370,7 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "音声メッセージを送れるのはあなたの連絡相手だけです。"; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "開く"; /* No comment provided by engineer. */ @@ -2294,15 +2382,6 @@ /* No comment provided by engineer. */ "Open Settings" = "設定を開く"; -/* authentication reason */ -"Open user profiles" = "ユーザープロフィールを開く"; - -/* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。"; - -/* No comment provided by engineer. */ -"Opening database…" = "データベースを開いています…"; - /* member role */ "owner" = "オーナー"; @@ -2324,26 +2403,14 @@ /* No comment provided by engineer. */ "Password to show" = "パスワードを表示する"; -/* No comment provided by engineer. */ -"Paste" = "貼り付け"; - /* No comment provided by engineer. */ "Paste image" = "画像の貼り付け"; -/* No comment provided by engineer. */ -"Paste received link" = "頂いたリンクを貼り付ける"; - -/* placeholder */ -"Paste the link you received to connect with your contact." = "連絡相手から頂いたリンクを以下の入力欄に貼り付けて繋がります。"; - /* No comment provided by engineer. */ "peer-to-peer" = "P2P"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。"; - -/* No comment provided by engineer. */ -"Periodically" = "定期的に"; +"Periodic" = "定期的に"; /* message decrypt error item */ "Permanent decryption error" = "永続的な復号化エラー"; @@ -2399,9 +2466,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "添付を含めて、下書きを保存する。"; -/* No comment provided by engineer. */ -"Preset server" = "プレセットサーバ"; - /* No comment provided by engineer. */ "Preset server address" = "プレセットサーバのアドレス"; @@ -2417,6 +2481,9 @@ /* No comment provided by engineer. */ "Private filenames" = "プライベートなファイル名"; +/* name of notes to self */ +"Private notes" = "プライベートノート"; + /* No comment provided by engineer. */ "Profile and server connections" = "プロフィールとサーバ接続"; @@ -2426,7 +2493,7 @@ /* No comment provided by engineer. */ "Profile password" = "プロフィールのパスワード"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "連絡先にプロフィール更新のお知らせが届きます。"; /* No comment provided by engineer. */ @@ -2474,23 +2541,20 @@ /* chat item menu */ "React…" = "反応する…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "読む"; /* No comment provided by engineer. */ "Read more" = "続きを読む"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "詳しくは[ユーザーガイド](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)をご覧ください。"; +"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://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)をご覧ください。"; - -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "GitHubリポジトリで詳細をご確認ください。"; +"Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "詳しくは[GitHubリポジトリ](https://github.com/simplex-chat/simplex-chat#readme)をご覧ください。"; /* No comment provided by engineer. */ "received answer…" = "回答を受け取りました…"; @@ -2537,7 +2601,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "電池使用量低減"; -/* reject incoming call via notification */ +/* reject incoming call via notification +swipe action */ "Reject" = "拒否"; /* No comment provided by engineer. */ @@ -2621,9 +2686,6 @@ /* chat item action */ "Reveal" = "開示する"; -/* No comment provided by engineer. */ -"Revert" = "元に戻す"; - /* No comment provided by engineer. */ "Revoke" = "取り消す"; @@ -2639,13 +2701,14 @@ /* No comment provided by engineer. */ "Run chat" = "チャット起動"; -/* chat item action */ +/* alert button +chat item action */ "Save" = "保存"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "保存(連絡先に通知)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "保存して、連絡先にに知らせる"; /* No comment provided by engineer. */ @@ -2654,12 +2717,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "グループプロファイルの保存と更新"; -/* No comment provided by engineer. */ -"Save archive" = "アーカイブを保存"; - -/* No comment provided by engineer. */ -"Save auto-accept settings" = "自動受け入れ設定を保存する"; - /* No comment provided by engineer. */ "Save group profile" = "グループプロフィールの保存"; @@ -2669,7 +2726,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "パスフレーズをキーチェーンに保存"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "この設定でよろしいですか?"; /* No comment provided by engineer. */ @@ -2678,12 +2735,9 @@ /* No comment provided by engineer. */ "Save servers" = "サーバを保存"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "サーバを保存しますか?"; -/* No comment provided by engineer. */ -"Save settings?" = "設定を保存しますか?"; - /* No comment provided by engineer. */ "Save welcome message?" = "ウェルカムメッセージを保存しますか?"; @@ -2726,7 +2780,7 @@ /* chat item text */ "security code changed" = "セキュリティコードが変更されました"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "選択"; /* No comment provided by engineer. */ @@ -2747,9 +2801,6 @@ /* No comment provided by engineer. */ "Send a live message - it will update for the recipient(s) as you type it" = "ライブメッセージを送信 (入力しながら宛先の画面で更新される)"; -/* No comment provided by engineer. */ -"Send direct message" = "ダイレクトメッセージを送信"; - /* No comment provided by engineer. */ "Send direct message to connect" = "ダイレクトメッセージを送信して接続する"; @@ -2765,16 +2816,13 @@ /* No comment provided by engineer. */ "Send notifications" = "通知を送信する"; -/* No comment provided by engineer. */ -"Send notifications:" = "通知を送信する:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "質問やアイデアを送る"; /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "ギャラリーまたはカスタム キーボードから送信します。"; -/* No comment provided by engineer. */ +/* alert message */ "Sender cancelled file transfer." = "送信者がファイル転送をキャンセルしました。"; /* No comment provided by engineer. */ @@ -2840,7 +2888,8 @@ /* No comment provided by engineer. */ "Settings" = "設定"; -/* chat item action */ +/* alert action +chat item action */ "Share" = "共有する"; /* No comment provided by engineer. */ @@ -2849,15 +2898,12 @@ /* No comment provided by engineer. */ "Share address" = "アドレスを共有する"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "アドレスを連絡先と共有しますか?"; /* No comment provided by engineer. */ "Share link" = "リンクを送る"; -/* No comment provided by engineer. */ -"Share one-time invitation link" = "使い捨ての招待リンクを共有"; - /* No comment provided by engineer. */ "Share with contacts" = "連絡先と共有する"; @@ -2894,7 +2940,7 @@ /* simplex link type */ "SimpleX group link" = "SimpleXグループリンク"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "SimpleXリンク"; /* No comment provided by engineer. */ @@ -2924,18 +2970,12 @@ /* No comment provided by engineer. */ "Small groups (max 20)" = "小グループ(最大20名)"; -/* No comment provided by engineer. */ -"SMP servers" = "SMPサーバ"; - /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "インポート中に致命的でないエラーが発生しました - 詳細はチャットコンソールを参照してください。"; /* notification title */ "Somebody" = "誰か"; -/* No comment provided by engineer. */ -"Start a new chat" = "新しいチャットを開始する"; - /* No comment provided by engineer. */ "Start chat" = "チャットを開始する"; @@ -2948,9 +2988,6 @@ /* No comment provided by engineer. */ "Stop" = "停止"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "チャットを停止してデータベースアクションを有効にします"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "データベースのエクスポート、読み込み、削除するにはチャットを閉じてからです。チャットを閉じると送受信ができなくなります。"; @@ -2966,10 +3003,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "ファイルの送信を停止しますか?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "共有を停止"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "アドレスの共有を停止しますか?"; /* authentication reason */ @@ -3005,9 +3042,6 @@ /* No comment provided by engineer. */ "Tap to join incognito" = "タップしてシークレットモードで参加"; -/* No comment provided by engineer. */ -"Tap to start a new chat" = "タップして新しいチャットを始める"; - /* No comment provided by engineer. */ "TCP connection timeout" = "TCP接続タイムアウト"; @@ -3029,7 +3063,7 @@ /* No comment provided by engineer. */ "Test servers" = "テストサーバ"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "テストは失敗しました!"; /* No comment provided by engineer. */ @@ -3041,9 +3075,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "ユーザーに感謝します – Weblate 経由で貢献してください!"; -/* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "世界初のユーザーIDのないプラットフォーム|設計も元からプライベート。"; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "アプリは、メッセージや連絡先のリクエストを受信したときに通知することができます - 設定を開いて有効にしてください。"; @@ -3062,6 +3093,9 @@ /* 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." = "以前のメッセージとハッシュ値が異なります。"; @@ -3074,14 +3108,11 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "メッセージは、すべてのメンバーに対してモデレートされたものとして表示されます。"; -/* No comment provided by engineer. */ -"The next generation of private messaging" = "次世代のプライバシー・メッセンジャー"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "古いデータベースは移行時に削除されなかったので、削除することができます。"; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "プロフィールは連絡先にしか共有されません。"; +"Your profile is stored on your device and only shared with your contacts." = "プロフィールは連絡先にしか共有されません。"; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "長らくお待たせしました! ✅"; @@ -3092,15 +3123,6 @@ /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "現在のチャットプロフィールの新しい接続のサーバ **%@**。"; -/* No comment provided by engineer. */ -"Theme" = "テーマ"; - -/* No comment provided by engineer. */ -"There should be at least one user profile." = "少なくとも1つのユーザープロファイルが必要です。"; - -/* No comment provided by engineer. */ -"There should be at least one visible user profile." = "少なくとも1つのユーザープロフィールが表示されている必要があります。"; - /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "これらの設定は現在のプロファイル **%@** 用です。"; @@ -3134,15 +3156,15 @@ /* No comment provided by engineer. */ "To make a new connection" = "新規に接続する場合"; -/* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "プライバシーを保護するために、SimpleX には、他のすべてのプラットフォームで使用されるユーザー ID の代わりに、連絡先ごとに個別のメッセージ キューの識別子があります。"; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "時間帯を漏らさないために、画像と音声ファイルはUTCを使います。"; /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "あなたのデータを守るために、SimpleXロックをオンにしてください。\nオンにするには、認証ステップが行われます。"; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "プライバシーを保護するために、SimpleX には、他のすべてのプラットフォームで使用されるユーザー ID の代わりに、連絡先ごとに個別のメッセージ キューの識別子があります。"; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "音声メッセージを録音する場合は、マイクの使用を許可してください。"; @@ -3167,22 +3189,16 @@ /* No comment provided by engineer. */ "Turn off" = "オフにする"; -/* No comment provided by engineer. */ -"Turn off notifications?" = "通知をオフにしますか?"; - /* No comment provided by engineer. */ "Turn on" = "オンにする"; /* No comment provided by engineer. */ "Unable to record voice message" = "音声メッセージを録音できません"; -/* item status description */ -"Unexpected error: %@" = "予期しないエラー: %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "予期しない移行状態"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "お気に入りを取り消す。"; /* No comment provided by engineer. */ @@ -3221,36 +3237,27 @@ /* authentication reason */ "Unlock app" = "アプリのロック解除"; -/* No comment provided by engineer. */ +/* notification label action */ "Unmute" = "ミュート解除"; -/* No comment provided by engineer. */ +/* swipe action */ "Unread" = "未読"; /* No comment provided by engineer. */ "Update" = "更新"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = ".onionのホスト設定を更新しますか?"; - /* No comment provided by engineer. */ "Update database passphrase" = "データベースのパスフレーズを更新"; /* No comment provided by engineer. */ "Update network settings?" = "ネットワーク設定を更新しますか?"; -/* No comment provided by engineer. */ -"Update transport isolation mode?" = "トランスポート隔離モードを更新しますか?"; - /* rcv group event chat item */ "updated group profile" = "グループプロフィールを更新しました"; /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "設定を更新すると、全サーバにクライントの再接続が行われます。"; -/* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "設定を更新すると、全サーバにクライントの再接続が行われます。"; - /* No comment provided by engineer. */ "Upgrade and open chat" = "アップグレードしてチャットを開く"; @@ -3281,12 +3288,6 @@ /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "SimpleX チャット サーバーを使用しますか?"; -/* No comment provided by engineer. */ -"User profile" = "ユーザープロフィール"; - -/* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = ".onionホストを使用するには、互換性のあるVPNプロバイダーが必要です。"; - /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "SimpleX チャット サーバーを使用する。"; @@ -3342,7 +3343,7 @@ "Voice messages are prohibited in this chat." = "このチャットでは音声メッセージが使用禁止です。"; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "このグループでは音声メッセージが使用禁止です。"; +"Voice messages are prohibited." = "このグループでは音声メッセージが使用禁止です。"; /* No comment provided by engineer. */ "Voice messages prohibited!" = "音声メッセージは使用禁止です!"; @@ -3386,9 +3387,6 @@ /* No comment provided by engineer. */ "When available" = "利用可能時に"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "接続が要求されたら、それを受け入れるか拒否するかを選択できます。"; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "連絡相手にシークレットモードのプロフィールを共有すると、その連絡相手に招待されたグループでも同じプロフィールが使われます。"; @@ -3401,15 +3399,9 @@ /* No comment provided by engineer. */ "Wrong passphrase!" = "パスフレーズが違います!"; -/* No comment provided by engineer. */ -"XFTP servers" = "XFTPサーバ"; - /* pref value */ "yes" = "はい"; -/* No comment provided by engineer. */ -"You" = "あなた"; - /* No comment provided by engineer. */ "You accepted connection" = "接続を承認しました"; @@ -3437,9 +3429,6 @@ /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "デバイスやアプリの認証を行わずに、ロック画面から通話を受けることができます。"; -/* No comment provided by engineer. */ -"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "リンクをクリックすることでも接続できます。ブラウザで開いた場合は、**モバイルアプリで開く**ボタンをクリックしてください。"; - /* No comment provided by engineer. */ "You can create it later" = "後からでも作成できます"; @@ -3452,8 +3441,11 @@ /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "ユーザープロファイルを右にスワイプすると、非表示またはミュートにすることができます。"; +/* No comment provided by engineer. */ +"You can make it visible to your SimpleX contacts via Settings." = "設定でSimpleXの連絡先に表示させることができます。"; + /* notification body */ -"You can now send messages to %@" = "%@ にメッセージを送信できるようになりました"; +"You can now chat with %@" = "%@ にメッセージを送信できるようになりました"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "設定からロック画面の通知プレビューを設定できます。"; @@ -3464,9 +3456,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "このアドレスを連絡先と共有して、**%@** に接続できるようにすることができます。"; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "アドレスをリンクやQRコードとして共有することで、誰でも接続することができます。"; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "アプリの設定/データベースから、またはアプリを再起動することでチャットを開始できます"; @@ -3491,14 +3480,11 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "%1$@ の役割を %2$@ に変更しました"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "あなたはメッセージの受信に使用するサーバーを制御し、連絡先はあなたがメッセージの送信に使用するサーバーを使用することができます。"; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "確認できませんでした。 もう一度お試しください。"; /* No comment provided by engineer. */ -"You have no chats" = "あなたはチャットがありません"; +"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." = "アプリ起動時にパスフレーズを入力しなければなりません。端末に保存されてません。"; @@ -3566,9 +3552,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "シークレットモードのプロフィールでこのグループに参加しています。メインのプロフィールを守るために、招待することができません"; -/* No comment provided by engineer. */ -"Your %@ servers" = "あなたの %@ サーバー"; - /* No comment provided by engineer. */ "Your calls" = "あなたの通話"; @@ -3581,18 +3564,12 @@ /* No comment provided by engineer. */ "Your chat profiles" = "あなたのチャットプロフィール"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "接続を完了するには、連絡相手がオンラインになる必要があります。\nこの接続をキャンセルして、連絡先を削除をすることもできます (後でやり直すこともできます)。"; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "連絡先が現在サポートされている最大サイズ (%@) より大きいファイルを送信しました。"; /* No comment provided by engineer. */ "Your contacts can allow full message deletion." = "連絡先がメッセージの完全削除を許可できます。"; -/* No comment provided by engineer. */ -"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "SimpleX の連絡先に表示されます。\n設定で変更できます。"; - /* No comment provided by engineer. */ "Your contacts will remain connected." = "連絡先は接続されたままになります。"; @@ -3615,7 +3592,7 @@ "Your profile **%@** will be shared." = "あなたのプロファイル **%@** が共有されます。"; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "プロフィールはデバイスに保存され、連絡先とのみ共有されます。\nSimpleX サーバーはあなたのプロファイルを参照できません。"; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "プロフィールはデバイスに保存され、連絡先とのみ共有されます。 SimpleX サーバーはあなたのプロファイルを参照できません。"; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "あなたのプロフィール、連絡先、送信したメッセージがご自分の端末に保存されます。"; @@ -3623,9 +3600,6 @@ /* No comment provided by engineer. */ "Your random profile" = "あなたのランダム・プロフィール"; -/* No comment provided by engineer. */ -"Your server" = "あなたのサーバ"; - /* No comment provided by engineer. */ "Your server address" = "あなたのサーバアドレス"; @@ -3635,9 +3609,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "あなたのSimpleXアドレス"; -/* No comment provided by engineer. */ -"Your SMP servers" = "あなたのSMPサーバ"; - -/* No comment provided by engineer. */ -"Your XFTP servers" = "あなたのXFTPサーバ"; - diff --git a/apps/ios/ja.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/ja.lproj/SimpleX--iOS--InfoPlist.strings index 64d806794b..e5eda32632 100644 --- a/apps/ios/ja.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/ja.lproj/SimpleX--iOS--InfoPlist.strings @@ -2,14 +2,14 @@ "CFBundleName" = "SimpleX"; /* Privacy - Camera Usage Description */ -"NSCameraUsageDescription" = "SimpleX は、他のユーザーに接続したりビデオ通話を行うために QR コードをスキャンするためにカメラにアクセスする必要があります。"; +"NSCameraUsageDescription" = "SimpleXは他のユーザーに接続したりビデオ通話する際にQRコード読み取りのためにカメラにアクセスする必要があります。"; /* Privacy - Face ID Usage Description */ -"NSFaceIDUsageDescription" = "SimpleX はローカル認証に Face ID を使用します"; +"NSFaceIDUsageDescription" = "SimpleXはローカル認証にFace IDを使用します"; /* Privacy - Microphone Usage Description */ -"NSMicrophoneUsageDescription" = "SimpleX では、音声通話やビデオ通話、および音声メッセージの録音のためにマイクへのアクセスが必要です。"; +"NSMicrophoneUsageDescription" = "SimpleXは音声通話やビデオ通話および音声メッセージの録音のためにマイクにアクセスする必要があります。"; /* Privacy - Photo Library Additions Usage Description */ -"NSPhotoLibraryAddUsageDescription" = "SimpleX は、キャプチャおよび受信したメディアを保存するためにフォト ライブラリにアクセスする必要があります"; +"NSPhotoLibraryAddUsageDescription" = "SimpleXはキャプチャおよび受信したメディアを保存するためにフォトライブラリにアクセスする必要があります"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 784acf1a9b..232de56641 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -1,18 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (kan gekopieerd worden)"; @@ -31,30 +16,15 @@ /* No comment provided by engineer. */ "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- spraakberichten tot 5 minuten.\n- aangepaste tijd om te verdwijnen.\n- bewerkingsgeschiedenis."; -/* No comment provided by engineer. */ -", " = ", "; - -/* No comment provided by engineer. */ -": " = ": "; - /* No comment provided by engineer. */ "!1 colored!" = "!1 gekleurd!"; -/* No comment provided by engineer. */ -"." = "."; - -/* No comment provided by engineer. */ -"(" = "("; - /* No comment provided by engineer. */ "(new)" = "(nieuw)"; /* No comment provided by engineer. */ "(this device v%@)" = "(dit apparaat v%@)"; -/* No comment provided by engineer. */ -")" = ")"; - /* No comment provided by engineer. */ "[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Bijdragen](https://github.com/simplex-chat/simplex-chat#contribute)"; @@ -65,10 +35,10 @@ "[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. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Nieuw contact toevoegen**: om uw eenmalige QR-code of link voor uw contact te maken."; +"**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."; /* No comment provided by engineer. */ -"**Create link / QR code** for your contact to use." = "**Maak een link / QR-code aan** die uw contact kan gebruiken."; +"**Create group**: to create a new group." = "**Groep aanmaken**: om een nieuwe groep aan te maken."; /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "**e2e versleuteld** audio gesprek"; @@ -77,26 +47,29 @@ "**e2e encrypted** video call" = "**e2e versleuteld** video gesprek"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Meer privé**: bekijk elke 20 minuten nieuwe berichten. Apparaattoken wordt gedeeld met de SimpleX Chat-server, maar niet hoeveel contacten of berichten u heeft."; +"**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." = "**Meer privé**: bekijk elke 20 minuten nieuwe berichten. Apparaattoken wordt gedeeld met de SimpleX Chat-server, maar niet hoeveel contacten of berichten u heeft."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Meest privé**: gebruik geen SimpleX Chat-notificatie server, controleer berichten regelmatig op de achtergrond (afhankelijk van hoe vaak u de app gebruikt)."; +"**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." = "**Meest privé**: gebruik geen SimpleX Chat-notificatie server, controleer berichten regelmatig op de achtergrond (afhankelijk van hoe vaak u de app gebruikt)."; /* No comment provided by engineer. */ -"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Plak de ontvangen link** of open deze in de browser en tik op **Openen in mobiele app**."; +"**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Let op**: als u dezelfde database op twee apparaten gebruikt, wordt de decodering van berichten van uw verbindingen verbroken, als veiligheidsmaatregel."; /* No comment provided by engineer. */ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Let op**: u kunt het wachtwoord NIET herstellen of wijzigen als u het kwijtraakt."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Aanbevolen**: apparaattoken en meldingen worden naar de SimpleX Chat-meldingsserver gestuurd, maar niet de berichtinhoud, -grootte of van wie het afkomstig is."; +"**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." = "**Aanbevolen**: apparaattoken en meldingen worden naar de SimpleX Chat-meldingsserver gestuurd, maar niet de berichtinhoud, -grootte of van wie het afkomstig is."; /* No comment provided by engineer. */ -"**Scan QR code**: to connect to your contact in person or via video call." = "**Scan QR-code**: om persoonlijk of via een video gesprek verbinding te maken met uw contact."; +"**Scan / Paste link**: to connect via a link you received." = "**Link scannen/plakken**: om verbinding te maken via een link die u hebt ontvangen."; /* 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."; +/* No comment provided by engineer. */ +"**Warning**: the archive will be removed." = "**Waarschuwing**: het archief wordt verwijderd."; + /* No comment provided by engineer. */ "*bold*" = "\\*vetgedrukt*"; @@ -139,6 +112,9 @@ /* No comment provided by engineer. */ "%@ connected" = "%@ verbonden"; +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ gedownload"; + /* notification title */ "%@ is connected!" = "%@ is verbonden!"; @@ -148,12 +124,21 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ is geverifieerd"; +/* No comment provided by engineer. */ +"%@ server" = "%@ server"; + /* No comment provided by engineer. */ "%@ servers" = "%@ servers"; +/* No comment provided by engineer. */ +"%@ uploaded" = "%@ geüpload"; + /* notification title */ "%@ wants to connect!" = "%@ wil verbinding maken!"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + /* No comment provided by engineer. */ "%@, %@ and %lld members" = "%@, %@ en %lld leden"; @@ -166,9 +151,24 @@ /* time interval */ "%d days" = "%d dagen"; +/* forward confirmation reason */ +"%d file(s) are still being downloaded." = "%d bestand(en) worden nog gedownload."; + +/* forward confirmation reason */ +"%d file(s) failed to download." = "%d bestand(en) konden niet worden gedownload."; + +/* forward confirmation reason */ +"%d file(s) were deleted." = "%d bestand(en) zijn verwijderd."; + +/* forward confirmation reason */ +"%d file(s) were not downloaded." = "%d bestand(en) zijn niet gedownload."; + /* time interval */ "%d hours" = "%d uren"; +/* alert title */ +"%d messages not forwarded" = "%d berichten niet doorgestuurd"; + /* time interval */ "%d min" = "%d min"; @@ -178,6 +178,9 @@ /* time interval */ "%d sec" = "%d sec"; +/* delete after time */ +"%d seconds(s)" = "%d seconden"; + /* integrity error chat item */ "%d skipped message(s)" = "%d overgeslagen bericht(en)"; @@ -205,6 +208,9 @@ /* No comment provided by engineer. */ "%lld messages blocked" = "%lld berichten geblokkeerd"; +/* No comment provided by engineer. */ +"%lld messages blocked by admin" = "%lld berichten geblokkeerd door beheerder"; + /* No comment provided by engineer. */ "%lld messages marked deleted" = "%lld berichten gemarkeerd als verwijderd"; @@ -217,9 +223,6 @@ /* No comment provided by engineer. */ "%lld new interface languages" = "%lld nieuwe interface-talen"; -/* No comment provided by engineer. */ -"%lld second(s)" = "%lld seconde(n)"; - /* No comment provided by engineer. */ "%lld seconds" = "%lld seconden"; @@ -265,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 dag"; /* time interval */ @@ -274,15 +278,23 @@ /* No comment provided by engineer. */ "1 minute" = "1 minuut"; -/* time interval */ +/* delete after time +time interval */ "1 month" = "1 maand"; -/* time interval */ +/* delete after time +time interval */ "1 week" = "1 week"; +/* delete after time */ +"1 year" = "1 jaar"; + /* No comment provided by engineer. */ "1-time link" = "Eenmalige link"; +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Eenmalige link die *slechts met één contactpersoon* kan worden gebruikt - deel persoonlijk of via een messenger."; + /* No comment provided by engineer. */ "5 minutes" = "5 minuten"; @@ -302,7 +314,7 @@ "A new random profile will be shared." = "Een nieuw willekeurig profiel wordt gedeeld."; /* No comment provided by engineer. */ -"A separate TCP connection will be used **for each chat profile you have in the app**." = "Er wordt een aparte TCP-verbinding gebruikt **voor elk chat profiel dat je in de app hebt**."; +"A separate TCP connection will be used **for each chat profile you have in the app**." = "Er wordt een aparte TCP-verbinding gebruikt **voor elk chatprofiel dat je in de app hebt**."; /* 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." = "Er wordt een aparte TCP-verbinding gebruikt **voor elk contact en groepslid**.\n**Let op**: als u veel verbindingen heeft, kan uw batterij- en verkeersverbruik aanzienlijk hoger zijn en kunnen sommige verbindingen uitvallen."; @@ -317,10 +329,7 @@ "Abort changing address?" = "Adres wijziging afbreken?"; /* No comment provided by engineer. */ -"About SimpleX" = "Over SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "Over SimpleX adres"; +"About operators" = "Over operatoren"; /* No comment provided by engineer. */ "About SimpleX Chat" = "Over SimpleX Chat"; @@ -329,44 +338,94 @@ "above, then choose:" = "hier boven, kies dan:"; /* No comment provided by engineer. */ -"Accent color" = "Accent kleur"; +"Accent" = "Accent"; /* accept contact request via notification - accept incoming call via notification */ +accept incoming call via notification +swipe action */ "Accept" = "Accepteer"; +/* No comment provided by engineer. */ +"Accept conditions" = "Accepteer voorwaarden"; + /* No comment provided by engineer. */ "Accept connection request?" = "Accepteer contact"; /* notification body */ "Accept contact request from %@?" = "Accepteer contactverzoek van %@?"; -/* accept contact request via notification */ +/* accept contact request via notification +swipe action */ "Accept incognito" = "Accepteer incognito"; /* call status */ "accepted call" = "geaccepteerde oproep"; +/* No comment provided by engineer. */ +"Accepted conditions" = "Geaccepteerde voorwaarden"; + +/* chat list item title */ +"accepted invitation" = "geaccepteerde uitnodiging"; + +/* No comment provided by engineer. */ +"Acknowledged" = "Erkend"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Bevestigingsfouten"; + +/* token status text */ +"Active" = "actief"; + +/* 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 preset servers" = "Vooraf ingestelde servers toevoegen"; +"Add friends" = "Vrienden toevoegen"; + +/* No comment provided by engineer. */ +"Add list" = "Lijst toevoegen"; /* No comment provided by engineer. */ "Add profile" = "Profiel toevoegen"; /* No comment provided by engineer. */ -"Add server…" = "Server toevoegen…"; +"Add server" = "Server toevoegen"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Servers toevoegen door QR-codes te scannen."; +/* No comment provided by engineer. */ +"Add team members" = "Teamleden toevoegen"; + /* No comment provided by engineer. */ "Add to another device" = "Toevoegen aan een ander apparaat"; /* No comment provided by engineer. */ -"Add welcome message" = "Welkomst bericht toevoegen"; +"Add to list" = "Toevoegen aan lijst"; + +/* No comment provided by engineer. */ +"Add welcome message" = "Welkom bericht toevoegen"; + +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "Voeg uw teamleden toe aan de gesprekken."; + +/* No comment provided by engineer. */ +"Added media & file servers" = "Media- en bestandsservers toegevoegd"; + +/* No comment provided by engineer. */ +"Added message servers" = "Berichtservers toegevoegd"; + +/* No comment provided by engineer. */ +"Additional accent" = "Extra accent"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Extra accent 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Extra secundair"; /* No comment provided by engineer. */ "Address" = "Adres"; @@ -374,62 +433,110 @@ /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "Adres wijziging wordt afgebroken. Het oude ontvangstadres wordt gebruikt."; +/* No comment provided by engineer. */ +"Address or 1-time link?" = "Adres of eenmalige link?"; + +/* No comment provided by engineer. */ +"Address settings" = "Adres instellingen"; + /* member role */ "admin" = "Beheerder"; +/* feature role */ +"admins" = "beheerders"; + +/* No comment provided by engineer. */ +"Admins can block a member for all." = "Beheerders kunnen een lid voor iedereen blokkeren."; + /* No comment provided by engineer. */ "Admins can create the links to join groups." = "Beheerders kunnen de uitnodiging links naar groepen aanmaken."; /* No comment provided by engineer. */ "Advanced network settings" = "Geavanceerde netwerk instellingen"; +/* No comment provided by engineer. */ +"Advanced settings" = "Geavanceerde instellingen"; + /* chat item text */ "agreeing encryption for %@…" = "versleuteling overeenkomen voor %@…"; /* chat item text */ "agreeing encryption…" = "versleuteling overeenkomen…"; +/* No comment provided by engineer. */ +"All" = "alle"; + /* No comment provided by engineer. */ "All app data is deleted." = "Alle app-gegevens worden verwijderd."; /* No comment provided by engineer. */ "All chats and messages will be deleted - this cannot be undone!" = "Alle chats en berichten worden verwijderd, dit kan niet ongedaan worden gemaakt!"; +/* alert message */ +"All chats will be removed from the list %@, and the list deleted." = "Alle chats worden uit de lijst %@ verwijderd en de lijst wordt verwijderd."; + /* No comment provided by engineer. */ "All data is erased when it is entered." = "Alle gegevens worden bij het invoeren gewist."; +/* No comment provided by engineer. */ +"All data is kept private on your device." = "Alle gegevens zijn privé op uw apparaat."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Alle groepsleden blijven verbonden."; +/* feature role */ +"all members" = "alle leden"; + +/* No comment provided by engineer. */ +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Alle berichten en bestanden worden **end-to-end versleuteld** verzonden, met post-quantumbeveiliging in directe berichten."; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone!" = "Alle berichten worden verwijderd. Dit kan niet ongedaan worden gemaakt!"; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Alle berichten worden verwijderd, dit kan niet ongedaan worden gemaakt! De berichten worden ALLEEN voor jou verwijderd."; /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Alle nieuwe berichten van %@ worden verborgen!"; +/* profile dropdown */ +"All profiles" = "Alle profielen"; + +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Alle rapporten worden voor u gearchiveerd."; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Al uw contacten blijven verbonden."; /* No comment provided by engineer. */ "All your contacts will remain connected. Profile update will be sent to your contacts." = "Al uw contacten blijven verbonden. Profiel update wordt naar uw contacten verzonden."; +/* No comment provided by engineer. */ +"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Al uw contacten, gesprekken en bestanden worden veilig gecodeerd en in delen geüpload naar geconfigureerde XFTP-relays."; + /* No comment provided by engineer. */ "Allow" = "Toestaan"; /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Sta oproepen alleen toe als uw contact dit toestaat."; +/* No comment provided by engineer. */ +"Allow calls?" = "Oproepen toestaan?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "Sta verdwijnende berichten alleen toe als uw contact dit toestaat."; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you." = "Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contact dit toestaat."; +"Allow downgrade" = "Downgraden toestaan"; /* No comment provided by engineer. */ -"Allow message reactions only if your contact allows them." = "Sta berichtreacties alleen toe als uw contact dit toestaat."; +"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Sta het definitief verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur)"; /* No comment provided by engineer. */ -"Allow message reactions." = "Sta berichtreacties toe."; +"Allow message reactions only if your contact allows them." = "Sta bericht reacties alleen toe als uw contact dit toestaat."; + +/* No comment provided by engineer. */ +"Allow message reactions." = "Sta bericht reacties toe."; /* No comment provided by engineer. */ "Allow sending direct messages to members." = "Sta het verzenden van directe berichten naar leden toe."; @@ -438,11 +545,20 @@ "Allow sending disappearing messages." = "Toestaan dat verdwijnende berichten worden verzonden."; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages." = "Sta toe om verzonden berichten onomkeerbaar te verwijderen."; +"Allow sharing" = "Delen toestaan"; + +/* No comment provided by engineer. */ +"Allow to irreversibly delete sent messages. (24 hours)" = "Sta toe om verzonden berichten definitief te verwijderen. (24 uur)"; + +/* No comment provided by engineer. */ +"Allow to report messsages to moderators." = "Hiermee kunt u berichten rapporteren aan moderators."; /* No comment provided by engineer. */ "Allow to send files and media." = "Sta toe om bestanden en media te verzenden."; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "Sta toe dat SimpleX-links worden verzonden."; + /* No comment provided by engineer. */ "Allow to send voice messages." = "Sta toe om spraak berichten te verzenden."; @@ -453,13 +569,13 @@ "Allow voice messages?" = "Spraak berichten toestaan?"; /* No comment provided by engineer. */ -"Allow your contacts adding message reactions." = "Sta uw contactpersonen toe om berichtreacties toe te voegen."; +"Allow your contacts adding message reactions." = "Sta uw contactpersonen toe om bericht reacties toe te voegen."; /* No comment provided by engineer. */ "Allow your contacts to call you." = "Sta toe dat uw contacten u bellen."; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages." = "Laat uw contacten verzonden berichten onomkeerbaar verwijderen."; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Laat uw contacten verzonden berichten definitief verwijderen. (24 uur)"; /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Sta toe dat uw contacten verdwijnende berichten verzenden."; @@ -480,7 +596,10 @@ "always" = "altijd"; /* No comment provided by engineer. */ -"Always use relay" = "Verbinden via relais"; +"Always use private routing." = "Gebruik altijd privéroutering."; + +/* No comment provided by engineer. */ +"Always use relay" = "Altijd relay gebruiken"; /* No comment provided by engineer. */ "An empty chat profile with the provided name is created, and the app opens as usual." = "Er wordt een leeg chatprofiel met de opgegeven naam gemaakt en de app wordt zoals gewoonlijk geopend."; @@ -488,15 +607,27 @@ /* No comment provided by engineer. */ "and %lld other events" = "en %lld andere gebeurtenissen"; +/* report reason */ +"Another reason" = "Een andere reden"; + /* 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: %@"; +/* No comment provided by engineer. */ +"App data migration" = "Migratie van app-gegevens"; + /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "App versleutelt nieuwe lokale bestanden (behalve video's)."; +/* No comment provided by engineer. */ +"App group:" = "App-groep:"; + /* No comment provided by engineer. */ "App icon" = "App icon"; @@ -506,6 +637,9 @@ /* No comment provided by engineer. */ "App passcode is replaced with self-destruct passcode." = "De app-toegangscode wordt vervangen door een zelfvernietigings wachtwoord."; +/* No comment provided by engineer. */ +"App session" = "Appsessie"; + /* No comment provided by engineer. */ "App version" = "App versie"; @@ -515,9 +649,51 @@ /* No comment provided by engineer. */ "Appearance" = "Uiterlijk"; +/* No comment provided by engineer. */ +"Apply" = "Toepassen"; + +/* No comment provided by engineer. */ +"Apply to" = "Toepassen op"; + +/* No comment provided by engineer. */ +"Archive" = "Archief"; + +/* No comment provided by engineer. */ +"Archive %lld reports?" = "%lld rapporten archiveren?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "Alle rapporten archiveren?"; + +/* No comment provided by engineer. */ +"Archive and upload" = "Archiveren en uploaden"; + +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Archiveer contacten om later te chatten."; + +/* No comment provided by engineer. */ +"Archive report" = "Rapport archiveren"; + +/* No comment provided by engineer. */ +"Archive report?" = "Rapport archiveren?"; + +/* swipe action */ +"Archive reports" = "Rapporten archiveren"; + +/* No comment provided by engineer. */ +"Archived contacts" = "Gearchiveerde contacten"; + +/* No comment provided by engineer. */ +"archived report" = "gearchiveerd rapport"; + +/* No comment provided by engineer. */ +"Archiving database" = "Database archiveren"; + /* No comment provided by engineer. */ "Attach" = "Bijvoegen"; +/* No comment provided by engineer. */ +"attempts" = "pogingen"; + /* No comment provided by engineer. */ "Audio & video calls" = "Audio en video gesprekken"; @@ -531,7 +707,7 @@ "Audio/video calls" = "Audio/video oproepen"; /* No comment provided by engineer. */ -"Audio/video calls are prohibited." = "Audio/video gesprekken zijn verboden."; +"Audio/video calls are prohibited." = "Audio/video gesprekken zijn niet toegestaan."; /* PIN entry */ "Authentication cancelled" = "Verificatie geannuleerd"; @@ -557,9 +733,15 @@ /* No comment provided by engineer. */ "Auto-accept images" = "Afbeeldingen automatisch accepteren"; +/* alert title */ +"Auto-accept settings" = "Instellingen automatisch accepteren"; + /* No comment provided by engineer. */ "Back" = "Terug"; +/* No comment provided by engineer. */ +"Background" = "Achtergrond"; + /* No comment provided by engineer. */ "Bad desktop address" = "Onjuist desktopadres"; @@ -575,15 +757,45 @@ /* No comment provided by engineer. */ "Bad message ID" = "Onjuiste bericht-ID"; +/* No comment provided by engineer. */ +"Better calls" = "Betere gesprekken"; + /* No comment provided by engineer. */ "Better groups" = "Betere groepen"; +/* No comment provided by engineer. */ +"Better groups performance" = "Betere prestaties van groepen"; + +/* No comment provided by engineer. */ +"Better message dates." = "Betere datums voor berichten."; + /* No comment provided by engineer. */ "Better messages" = "Betere berichten"; +/* No comment provided by engineer. */ +"Better networking" = "Beter netwerk"; + +/* No comment provided by engineer. */ +"Better notifications" = "Betere meldingen"; + +/* No comment provided by engineer. */ +"Better privacy and security" = "Betere privacy en veiligheid"; + +/* No comment provided by engineer. */ +"Better security ✅" = "Betere beveiliging ✅"; + +/* No comment provided by engineer. */ +"Better user experience" = "Betere gebruikerservaring"; + +/* No comment provided by engineer. */ +"Black" = "Zwart"; + /* No comment provided by engineer. */ "Block" = "Blokkeren"; +/* No comment provided by engineer. */ +"Block for all" = "Blokkeren voor iedereen"; + /* No comment provided by engineer. */ "Block group members" = "Groepsleden blokkeren"; @@ -591,19 +803,38 @@ "Block member" = "Lid blokkeren"; /* No comment provided by engineer. */ -"Block member?" = "Lid blokkeren?"; +"Block member for all?" = "Lid voor iedereen blokkeren?"; /* No comment provided by engineer. */ +"Block member?" = "Lid blokkeren?"; + +/* marked deleted chat item preview text */ "blocked" = "geblokkeerd"; +/* rcv group event chat item */ +"blocked %@" = "blokkeerde %@"; + +/* blocked chat item +marked deleted chat item preview text */ +"blocked by admin" = "geblokkeerd door beheerder"; + +/* No comment provided by engineer. */ +"Blocked by admin" = "Geblokkeerd door beheerder"; + +/* No comment provided by engineer. */ +"Blur for better privacy." = "Vervagen voor betere privacy."; + +/* No comment provided by engineer. */ +"Blur media" = "Vervaag media"; + /* No comment provided by engineer. */ "bold" = "vetgedrukt"; /* No comment provided by engineer. */ -"Both you and your contact can add message reactions." = "Zowel u als uw contact kunnen berichtreacties toevoegen."; +"Both you and your contact can add message reactions." = "Zowel u als uw contact kunnen bericht reacties toevoegen."; /* No comment provided by engineer. */ -"Both you and your contact can irreversibly delete sent messages." = "Zowel jij als je contact kunnen verzonden berichten onherroepelijk verwijderen."; +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "Zowel jij als je contact kunnen verzonden berichten onherroepelijk verwijderen. (24 uur)"; /* No comment provided by engineer. */ "Both you and your contact can make calls." = "Zowel u als uw contact kunnen bellen."; @@ -618,7 +849,22 @@ "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Bulgaars, Fins, Thais en Oekraïens - dankzij de gebruikers en [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; /* No comment provided by engineer. */ -"By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Via chat profiel (standaard) of [via verbinding](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; +"Business address" = "Zakelijk adres"; + +/* No comment provided by engineer. */ +"Business chats" = "Zakelijke chats"; + +/* No comment provided by engineer. */ +"Businesses" = "bedrijven"; + +/* 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"; /* No comment provided by engineer. */ "Call already ended!" = "Oproep al beëindigd!"; @@ -636,7 +882,16 @@ "Calls" = "Oproepen"; /* No comment provided by engineer. */ -"Can't delete user profile!" = "Kan gebruikers profiel niet verwijderen!"; +"Calls prohibited!" = "Bellen niet toegestaan!"; + +/* No comment provided by engineer. */ +"Camera not available" = "Camera niet beschikbaar"; + +/* No comment provided by engineer. */ +"Can't call contact" = "Kan contact niet bellen"; + +/* No comment provided by engineer. */ +"Can't call member" = "Kan lid niet bellen"; /* No comment provided by engineer. */ "Can't invite contact!" = "Kan contact niet uitnodigen!"; @@ -645,8 +900,15 @@ "Can't invite contacts!" = "Kan geen contacten uitnodigen!"; /* No comment provided by engineer. */ +"Can't message member" = "Kan geen bericht sturen naar lid"; + +/* alert action +alert button */ "Cancel" = "Annuleren"; +/* No comment provided by engineer. */ +"Cancel migration" = "Migratie annuleren"; + /* feature offered item */ "cancelled %@" = "geannuleerd %@"; @@ -654,11 +916,26 @@ "Cannot access keychain to save database password" = "Geen toegang tot de keychain om database wachtwoord op te slaan"; /* No comment provided by engineer. */ +"Cannot forward message" = "Kan bericht niet doorsturen"; + +/* alert title */ "Cannot receive file" = "Kan bestand niet ontvangen"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Capaciteit overschreden - ontvanger heeft eerder verzonden berichten niet ontvangen."; + +/* No comment provided by engineer. */ +"Cellular" = "Mobiel"; + /* No comment provided by engineer. */ "Change" = "Veranderen"; +/* alert title */ +"Change automatic message deletion?" = "Automatisch verwijderen van berichten wijzigen?"; + +/* authentication reason */ +"Change chat profiles" = "Gebruikersprofielen wijzigen"; + /* No comment provided by engineer. */ "Change database passphrase?" = "Wachtwoord database wijzigen?"; @@ -684,7 +961,7 @@ "Change self-destruct mode" = "Zelfvernietigings modus wijzigen"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Zelfvernietigings code wijzigen"; /* chat item text */ @@ -703,7 +980,16 @@ "changing address…" = "adres wijzigen…"; /* No comment provided by engineer. */ -"Chat archive" = "Gesprek archief"; +"Chat" = "Chat"; + +/* No comment provided by engineer. */ +"Chat already exists" = "Chat bestaat al"; + +/* No comment provided by engineer. */ +"Chat already exists!" = "Chat bestaat al!"; + +/* No comment provided by engineer. */ +"Chat colors" = "Chat kleuren"; /* No comment provided by engineer. */ "Chat console" = "Chat console"; @@ -714,6 +1000,9 @@ /* No comment provided by engineer. */ "Chat database deleted" = "Chat database verwijderd"; +/* No comment provided by engineer. */ +"Chat database exported" = "Chat database geëxporteerd"; + /* No comment provided by engineer. */ "Chat database imported" = "Chat database geïmporteerd"; @@ -723,18 +1012,51 @@ /* No comment provided by engineer. */ "Chat is stopped" = "Chat is gestopt"; +/* 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 gestopt. Als je deze database al op een ander apparaat hebt gebruikt, moet je deze terugzetten voordat je met chatten begint."; + +/* No comment provided by engineer. */ +"Chat list" = "Chatlijst"; + +/* No comment provided by engineer. */ +"Chat migrated!" = "Chat gemigreerd!"; + /* No comment provided by engineer. */ "Chat preferences" = "Gesprek voorkeuren"; -/* No comment provided by engineer. */ -"Chats" = "Gesprekken"; +/* alert message */ +"Chat preferences were changed." = "Chatvoorkeuren zijn gewijzigd."; /* No comment provided by engineer. */ +"Chat profile" = "Gebruikers profiel"; + +/* No comment provided by engineer. */ +"Chat theme" = "Chat thema"; + +/* No comment provided by engineer. */ +"Chat will be deleted for all members - this cannot be undone!" = "De chat wordt voor alle leden verwijderd - dit kan niet ongedaan worden gemaakt!"; + +/* 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!"; + +/* No comment provided by engineer. */ +"Chats" = "Chats"; + +/* No comment provided by engineer. */ +"Check messages every 20 min." = "Controleer uw berichten elke 20 minuten."; + +/* No comment provided by engineer. */ +"Check messages when allowed." = "Controleer berichten indien toegestaan."; + +/* alert title */ "Check server address and try again." = "Controleer het server adres en probeer het opnieuw."; /* No comment provided by engineer. */ "Chinese and Spanish interface" = "Chinese en Spaanse interface"; +/* No comment provided by engineer. */ +"Choose _Migrate from another device_ on the new device and scan QR code." = "Kies _Migreren vanaf een ander apparaat_ op het nieuwe apparaat en scan de QR-code."; + /* No comment provided by engineer. */ "Choose file" = "Kies bestand"; @@ -742,6 +1064,15 @@ "Choose from library" = "Kies uit bibliotheek"; /* No comment provided by engineer. */ +"Chunks deleted" = "Stukken verwijderd"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Stukken gedownload"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Stukken geüpload"; + +/* swipe action */ "Clear" = "Wissen"; /* No comment provided by engineer. */ @@ -750,14 +1081,29 @@ /* No comment provided by engineer. */ "Clear conversation?" = "Gesprek wissen?"; +/* No comment provided by engineer. */ +"Clear group?" = "Groep wissen?"; + +/* No comment provided by engineer. */ +"Clear or delete group?" = "Groep wissen of verwijderen?"; + +/* No comment provided by engineer. */ +"Clear private notes?" = "Privénotities verwijderen?"; + /* No comment provided by engineer. */ "Clear verification" = "Verwijderd verificatie"; +/* No comment provided by engineer. */ +"Color chats with the new themes." = "Kleurchats met de nieuwe thema's."; + +/* No comment provided by engineer. */ +"Color mode" = "Kleur mode"; + /* No comment provided by engineer. */ "colored" = "gekleurd"; -/* No comment provided by engineer. */ -"Colors" = "Kleuren"; +/* report reason */ +"Community guidelines violation" = "Schending van de communityrichtlijnen"; /* server test step */ "Compare file" = "Bestand vergelijken"; @@ -768,15 +1114,51 @@ /* No comment provided by engineer. */ "complete" = "compleet"; +/* No comment provided by engineer. */ +"Completed" = "Voltooid"; + +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "Voorwaarden geaccepteerd op: %@."; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "Voorwaarden worden geaccepteerd voor de operator(s): **%@**."; + +/* No comment provided by engineer. */ +"Conditions are already accepted for these operator(s): **%@**." = "Voorwaarden zijn reeds geaccepteerd voor de volgende operator(s): **%@**."; + +/* No comment provided by engineer. */ +"Conditions of use" = "Gebruiksvoorwaarden"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "Voorwaarden worden geaccepteerd voor de operator(s): **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "Voorwaarden worden geaccepteerd op: %@."; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "Voorwaarden worden automatisch geaccepteerd voor ingeschakelde operators op: %@."; + /* 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"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "Contact verwijderen bevestigen?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "Bevestig database upgrades"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Bevestig bestanden van onbekende servers."; + +/* No comment provided by engineer. */ +"Confirm network settings" = "Bevestig netwerk instellingen"; + /* No comment provided by engineer. */ "Confirm new passphrase…" = "Bevestig nieuw wachtwoord…"; @@ -786,6 +1168,15 @@ /* No comment provided by engineer. */ "Confirm password" = "Bevestig wachtwoord"; +/* No comment provided by engineer. */ +"Confirm that you remember database passphrase to migrate it." = "Bevestig dat u het wachtwoord voor de database onthoudt om deze te migreren."; + +/* No comment provided by engineer. */ +"Confirm upload" = "Bevestig het uploaden"; + +/* token status text */ +"Confirmed" = "Bevestigd"; + /* server test step */ "Connect" = "Verbind"; @@ -801,6 +1192,9 @@ /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "maak verbinding met SimpleX Chat-ontwikkelaars."; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Maak sneller verbinding met je vrienden."; + /* No comment provided by engineer. */ "Connect to yourself?" = "Verbinding maken met jezelf?"; @@ -816,9 +1210,6 @@ /* No comment provided by engineer. */ "Connect via link" = "Maak verbinding via link"; -/* No comment provided by engineer. */ -"Connect via link / QR code" = "Maak verbinding via link / QR-code"; - /* No comment provided by engineer. */ "Connect via one-time link" = "Verbinden via een eenmalige link?"; @@ -828,18 +1219,27 @@ /* No comment provided by engineer. */ "connected" = "verbonden"; +/* No comment provided by engineer. */ +"Connected" = "Verbonden"; + /* No comment provided by engineer. */ "Connected desktop" = "Verbonden desktop"; /* rcv group event chat item */ "connected directly" = "direct verbonden"; +/* No comment provided by engineer. */ +"Connected servers" = "Verbonden servers"; + /* No comment provided by engineer. */ "Connected to desktop" = "Verbonden met desktop"; /* No comment provided by engineer. */ "connecting" = "Verbinden"; +/* No comment provided by engineer. */ +"Connecting" = "Verbinden"; + /* No comment provided by engineer. */ "connecting (accepted)" = "verbinden (geaccepteerd)"; @@ -861,15 +1261,24 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Verbinden met server... (fout: %@)"; +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "Er wordt verbinding gemaakt met het contact. Even geduld of controleer het later!"; + /* No comment provided by engineer. */ "Connecting to desktop" = "Verbinding maken met desktop"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "Verbinden…"; /* No comment provided by engineer. */ "Connection" = "Verbinding"; +/* No comment provided by engineer. */ +"Connection and servers status." = "Verbindings- en serverstatus."; + +/* No comment provided by engineer. */ +"Connection blocked" = "Verbinding geblokkeerd"; + /* No comment provided by engineer. */ "Connection error" = "Verbindingsfout"; @@ -879,24 +1288,51 @@ /* chat list item title (it should not be shown */ "connection established" = "verbinding gemaakt"; +/* No comment provided by engineer. */ +"Connection is blocked by server operator:\n%@" = "Verbinding is geblokkeerd door serveroperator:\n%@"; + +/* No comment provided by engineer. */ +"Connection not ready." = "Verbinding nog niet klaar."; + +/* No comment provided by engineer. */ +"Connection notifications" = "Verbindingsmeldingen"; + /* No comment provided by engineer. */ "Connection request sent!" = "Verbindingsverzoek verzonden!"; +/* No comment provided by engineer. */ +"Connection requires encryption renegotiation." = "Verbinding vereist heronderhandeling over encryptie."; + +/* No comment provided by engineer. */ +"Connection security" = "Beveiliging van de verbinding"; + /* No comment provided by engineer. */ "Connection terminated" = "Verbinding beëindigd"; /* No comment provided by engineer. */ "Connection timeout" = "Timeout verbinding"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "Verbinding met desktop is gestopt"; + /* connection information */ "connection:%@" = "verbinding:%@"; +/* No comment provided by engineer. */ +"Connections" = "Verbindingen"; + +/* profile update event chat item */ +"contact %@ changed to %@" = "contactpersoon %1$@ gewijzigd in %2$@"; + /* No comment provided by engineer. */ "Contact allows" = "Contact maakt het mogelijk"; /* No comment provided by engineer. */ "Contact already exists" = "Contact bestaat al"; +/* No comment provided by engineer. */ +"Contact deleted!" = "Contact verwijderd!"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "contact heeft e2e-codering"; @@ -910,7 +1346,7 @@ "Contact is connected" = "Contact is verbonden"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "Contact is nog niet verbonden!"; +"Contact is deleted." = "Contact is verwijderd."; /* No comment provided by engineer. */ "Contact name" = "Contact naam"; @@ -918,21 +1354,36 @@ /* No comment provided by engineer. */ "Contact preferences" = "Contact voorkeuren"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "Het contact wordt verwijderd. Dit kan niet ongedaan worden gemaakt!"; + /* No comment provided by engineer. */ "Contacts" = "Contacten"; /* No comment provided by engineer. */ "Contacts can mark messages for deletion; you will be able to view them." = "Contact personen kunnen berichten markeren voor verwijdering; u kunt ze wel bekijken."; +/* blocking reason */ +"Content violates conditions of use" = "Inhoud schendt de gebruiksvoorwaarden"; + /* No comment provided by engineer. */ "Continue" = "Doorgaan"; -/* chat item action */ +/* No comment provided by engineer. */ +"Conversation deleted!" = "Gesprek verwijderd!"; + +/* No comment provided by engineer. */ "Copy" = "Kopiëren"; +/* No comment provided by engineer. */ +"Copy error" = "Kopieerfout"; + /* No comment provided by engineer. */ "Core version: v%@" = "Core versie: v% @"; +/* No comment provided by engineer. */ +"Corner" = "Hoek"; + /* No comment provided by engineer. */ "Correct name to %@?" = "Juiste naam voor %@?"; @@ -940,10 +1391,10 @@ "Create" = "Maak"; /* No comment provided by engineer. */ -"Create a group using a random profile." = "Maak een groep met een willekeurig profiel."; +"Create 1-time link" = "Eenmalige link maken"; /* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Maak een adres aan zodat mensen contact met je kunnen opnemen."; +"Create a group using a random profile." = "Maak een groep met een willekeurig profiel."; /* server test step */ "Create file" = "Bestand maken"; @@ -958,10 +1409,10 @@ "Create link" = "Maak link"; /* No comment provided by engineer. */ -"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Maak een nieuw profiel aan in [desktop-app](https://simplex.chat/downloads/). 💻"; +"Create list" = "Maak een lijst"; /* No comment provided by engineer. */ -"Create one-time invitation link" = "Maak een eenmalige uitnodiging link"; +"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Maak een nieuw profiel aan in [desktop-app](https://simplex.chat/downloads/). 💻"; /* No comment provided by engineer. */ "Create profile" = "Maak een profiel aan"; @@ -979,17 +1430,35 @@ "Create your profile" = "Maak je profiel aan"; /* No comment provided by engineer. */ -"Created on %@" = "Gemaakt op %@"; +"Created" = "Gemaakt"; + +/* No comment provided by engineer. */ +"Created at" = "Gemaakt op"; + +/* copied message info */ +"Created at: %@" = "Aangemaakt op: %@"; + +/* No comment provided by engineer. */ +"Creating archive link" = "Archief link maken"; + +/* No comment provided by engineer. */ +"Creating link…" = "Link maken…"; /* No comment provided by engineer. */ "creator" = "creator"; +/* No comment provided by engineer. */ +"Current conditions text couldn't be loaded, you can review conditions via this link:" = "De tekst van de huidige voorwaarden kon niet worden geladen. U kunt de voorwaarden bekijken via deze link:"; + /* No comment provided by engineer. */ "Current Passcode" = "Huidige toegangscode"; /* No comment provided by engineer. */ "Current passphrase…" = "Huidige wachtwoord…"; +/* No comment provided by engineer. */ +"Current profile" = "Huidig profiel"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "De momenteel maximaal ondersteunde bestandsgrootte is %@."; @@ -999,9 +1468,18 @@ /* No comment provided by engineer. */ "Custom time" = "Aangepaste tijd"; +/* No comment provided by engineer. */ +"Customizable message shape." = "Aanpasbare berichtvorm."; + +/* No comment provided by engineer. */ +"Customize theme" = "Thema aanpassen"; + /* No comment provided by engineer. */ "Dark" = "Donker"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Kleuren in donkere modus"; + /* No comment provided by engineer. */ "Database downgrade" = "Database downgraden"; @@ -1042,7 +1520,7 @@ "Database passphrase is different from saved in the keychain." = "Het wachtwoord van de database verschilt van het wachtwoord dat is opgeslagen in de keychain."; /* No comment provided by engineer. */ -"Database passphrase is required to open chat." = "Database wachtwoord is vereist om je gesprekken te openen."; +"Database passphrase is required to open chat." = "Database wachtwoord is vereist om je chats te openen."; /* No comment provided by engineer. */ "Database upgrade" = "Database upgrade"; @@ -1062,13 +1540,20 @@ /* time unit */ "days" = "dagen"; +/* No comment provided by engineer. */ +"Debug delivery" = "Foutopsporing bezorging"; + /* No comment provided by engineer. */ "Decentralized" = "Gedecentraliseerd"; /* message decrypt error item */ "Decryption error" = "Decodering fout"; -/* pref value */ +/* No comment provided by engineer. */ +"decryption errors" = "decoderingsfouten"; + +/* delete after time +pref value */ "default (%@)" = "standaard (%@)"; /* No comment provided by engineer. */ @@ -1077,9 +1562,13 @@ /* No comment provided by engineer. */ "default (yes)" = "standaard (ja)"; -/* chat item action */ +/* alert action +swipe action */ "Delete" = "Verwijderen"; +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "%lld berichten van leden verwijderen?"; + /* No comment provided by engineer. */ "Delete %lld messages?" = "%lld berichten verwijderen?"; @@ -1096,19 +1585,22 @@ "Delete all files" = "Verwijder alle bestanden"; /* No comment provided by engineer. */ -"Delete and notify contact" = "Contact verwijderen en op de hoogte stellen"; +"Delete and notify contact" = "Verwijderen en contact op de hoogte stellen"; /* No comment provided by engineer. */ -"Delete archive" = "Archief verwijderen"; +"Delete chat" = "Chat verwijderen"; /* No comment provided by engineer. */ -"Delete chat archive?" = "Chat archief verwijderen?"; +"Delete chat messages from your device." = "Verwijder chatberichten van uw apparaat."; /* No comment provided by engineer. */ -"Delete chat profile" = "Chat profiel verwijderen"; +"Delete chat profile" = "Chatprofiel verwijderen"; /* No comment provided by engineer. */ -"Delete chat profile?" = "Chat profiel verwijderen?"; +"Delete chat profile?" = "Chatprofiel verwijderen?"; + +/* No comment provided by engineer. */ +"Delete chat?" = "Chat verwijderen?"; /* No comment provided by engineer. */ "Delete connection" = "Verbinding verwijderen"; @@ -1117,14 +1609,14 @@ "Delete contact" = "Verwijder contact"; /* No comment provided by engineer. */ -"Delete Contact" = "Verwijder contact"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Verwijder contact?\nDit kan niet ongedaan gemaakt worden!"; +"Delete contact?" = "Verwijder contact?"; /* No comment provided by engineer. */ "Delete database" = "Database verwijderen"; +/* No comment provided by engineer. */ +"Delete database from this device" = "Verwijder de database van dit apparaat"; + /* server test step */ "Delete file" = "Verwijder bestand"; @@ -1132,7 +1624,7 @@ "Delete files and media?" = "Bestanden en media verwijderen?"; /* No comment provided by engineer. */ -"Delete files for all chat profiles" = "Verwijder bestanden voor alle chat profielen"; +"Delete files for all chat profiles" = "Verwijder bestanden voor alle chatprofielen"; /* chat feature */ "Delete for everyone" = "Verwijderen voor iedereen"; @@ -1155,13 +1647,16 @@ /* No comment provided by engineer. */ "Delete link?" = "Link verwijderen?"; +/* alert title */ +"Delete list?" = "Lijst verwijderen?"; + /* No comment provided by engineer. */ "Delete member message?" = "Bericht van lid verwijderen?"; /* No comment provided by engineer. */ "Delete message?" = "Verwijder bericht?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "Verwijder berichten"; /* No comment provided by engineer. */ @@ -1174,7 +1669,7 @@ "Delete old database?" = "Oude database verwijderen?"; /* No comment provided by engineer. */ -"Delete pending connection" = "Wachtende verbinding verwijderen"; +"Delete or moderate up to 200 messages." = "Maximaal 200 berichten verwijderen of modereren."; /* No comment provided by engineer. */ "Delete pending connection?" = "Wachtende verbinding verwijderen?"; @@ -1185,12 +1680,24 @@ /* server test step */ "Delete queue" = "Wachtrij verwijderen"; +/* No comment provided by engineer. */ +"Delete report" = "Rapport verwijderen"; + +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Verwijder maximaal 20 berichten tegelijk."; + /* No comment provided by engineer. */ "Delete user profile?" = "Gebruikers profiel verwijderen?"; +/* No comment provided by engineer. */ +"Delete without notification" = "Verwijderen zonder melding"; + /* deleted chat item */ "deleted" = "verwijderd"; +/* No comment provided by engineer. */ +"Deleted" = "Verwijderd"; + /* No comment provided by engineer. */ "Deleted at" = "Verwijderd om"; @@ -1203,6 +1710,12 @@ /* rcv group event chat item */ "deleted group" = "verwijderde groep"; +/* No comment provided by engineer. */ +"Deletion errors" = "Verwijderingsfouten"; + +/* No comment provided by engineer. */ +"Delivered even when Apple drops them." = "Geleverd ook als Apple ze verliest"; + /* No comment provided by engineer. */ "Delivery" = "Bezorging"; @@ -1224,9 +1737,27 @@ /* No comment provided by engineer. */ "Desktop devices" = "Desktop apparaten"; +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "Het bestemmingsserveradres van %@ is niet compatibel met de doorstuurserverinstellingen %@."; + +/* snd error text */ +"Destination server error: %@" = "Bestemmingsserverfout: %@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "De versie van de bestemmingsserver %@ is niet compatibel met de doorstuurserver %@."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Gedetailleerde statistieken"; + +/* No comment provided by engineer. */ +"Details" = "Details"; + /* No comment provided by engineer. */ "Develop" = "Ontwikkelen"; +/* No comment provided by engineer. */ +"Developer options" = "Ontwikkelaars opties"; + /* No comment provided by engineer. */ "Developer tools" = "Ontwikkel gereedschap"; @@ -1252,11 +1783,20 @@ "Direct messages" = "Directe berichten"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Directe berichten tussen leden zijn verboden in deze groep."; +"Direct messages between members are prohibited in this chat." = "Directe berichten tussen leden zijn in deze chat niet toegestaan."; + +/* No comment provided by engineer. */ +"Direct messages between members are prohibited." = "Directe berichten tussen leden zijn niet toegestaan."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Uitschakelen (overschrijvingen behouden)"; +/* alert title */ +"Disable automatic message deletion?" = "Automatisch verwijderen van berichten uitschakelen?"; + +/* alert button */ +"Disable delete messages" = "Berichten verwijderen uitschakelen"; + /* No comment provided by engineer. */ "Disable for all" = "Uitschakelen voor iedereen"; @@ -1266,6 +1806,9 @@ /* No comment provided by engineer. */ "disabled" = "uitgeschakeld"; +/* No comment provided by engineer. */ +"Disabled" = "Uitgeschakeld"; + /* No comment provided by engineer. */ "Disappearing message" = "Verdwijnend bericht"; @@ -1273,10 +1816,10 @@ "Disappearing messages" = "Verdwijnende berichten"; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this chat." = "Verdwijnende berichten zijn verboden in dit gesprek."; +"Disappearing messages are prohibited in this chat." = "Verdwijnende berichten zijn niet toegestaan in dit gesprek."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Verdwijnende berichten zijn verboden in deze groep."; +"Disappearing messages are prohibited." = "Verdwijnende berichten zijn niet toegestaan."; /* No comment provided by engineer. */ "Disappears at" = "Verdwijnt op"; @@ -1299,36 +1842,88 @@ /* No comment provided by engineer. */ "Do it later" = "Doe het later"; +/* No comment provided by engineer. */ +"Do not send history to new members." = "Stuur geen geschiedenis naar nieuwe leden."; + +/* No comment provided by engineer. */ +"Do NOT send messages directly, even if your or destination server does not support private routing." = "Stuur GEEN berichten rechtstreeks, zelfs als uw of de bestemmingsserver geen privéroutering ondersteunt."; + +/* No comment provided by engineer. */ +"Do not use credentials with proxy." = "Gebruik geen inloggegevens met proxy."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "Gebruik GEEN privéroutering."; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "Gebruik SimpleX NIET voor noodoproepen."; +/* No comment provided by engineer. */ +"Documents:" = "Documenten:"; + /* No comment provided by engineer. */ "Don't create address" = "Maak geen adres aan"; /* No comment provided by engineer. */ "Don't enable" = "Niet inschakelen"; +/* No comment provided by engineer. */ +"Don't miss important messages." = "Mis geen belangrijke berichten."; + /* No comment provided by engineer. */ "Don't show again" = "Niet meer weergeven"; +/* No comment provided by engineer. */ +"Done" = "Klaar"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "Downgraden en chat openen"; +/* alert button +chat item action */ +"Download" = "Downloaden"; + +/* No comment provided by engineer. */ +"Download errors" = "Downloadfouten"; + +/* No comment provided by engineer. */ +"Download failed" = "Download mislukt"; + /* server test step */ "Download file" = "Download bestand"; +/* alert action */ +"Download files" = "‐Bestanden downloaden"; + +/* No comment provided by engineer. */ +"Downloaded" = "Gedownload"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Gedownloade bestanden"; + +/* No comment provided by engineer. */ +"Downloading archive" = "Archief downloaden"; + +/* No comment provided by engineer. */ +"Downloading link details" = "Link gegevens downloaden"; + /* No comment provided by engineer. */ "Duplicate display name!" = "Dubbele weergavenaam!"; /* integrity error chat item */ "duplicate message" = "dubbel bericht"; +/* No comment provided by engineer. */ +"duplicates" = "duplicaten"; + /* No comment provided by engineer. */ "Duration" = "Duur"; /* No comment provided by engineer. */ "e2e encrypted" = "e2e versleuteld"; +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "E2E versleutelde meldingen."; + /* chat item action */ "Edit" = "Bewerk"; @@ -1341,12 +1936,21 @@ /* No comment provided by engineer. */ "Enable (keep overrides)" = "Inschakelen (overschrijvingen behouden)"; -/* No comment provided by engineer. */ +/* alert title */ "Enable automatic message deletion?" = "Automatisch verwijderen van berichten aanzetten?"; +/* No comment provided by engineer. */ +"Enable camera access" = "Schakel cameratoegang in"; + +/* No comment provided by engineer. */ +"Enable Flux in Network & servers settings for better metadata privacy." = "Schakel Flux in bij Netwerk- en serverinstellingen voor betere privacy van metagegevens."; + /* No comment provided by engineer. */ "Enable for all" = "Inschakelen voor iedereen"; +/* No comment provided by engineer. */ +"Enable in direct chats (BETA)!" = "Activeer in directe chats (BETA)!"; + /* No comment provided by engineer. */ "Enable instant notifications?" = "Onmiddellijke meldingen inschakelen?"; @@ -1374,6 +1978,12 @@ /* enabled status */ "enabled" = "ingeschakeld"; +/* No comment provided by engineer. */ +"Enabled" = "Ingeschakeld"; + +/* No comment provided by engineer. */ +"Enabled for" = "Ingeschakeld voor"; + /* enabled status */ "enabled for contact" = "ingeschakeld voor contact"; @@ -1398,6 +2008,9 @@ /* notification */ "Encrypted message or another event" = "Versleuteld bericht of een andere gebeurtenis"; +/* notification */ +"Encrypted message: app is stopped" = "Versleuteld bericht: app is gestopt"; + /* notification */ "Encrypted message: database error" = "Versleuteld bericht: database fout"; @@ -1443,6 +2056,9 @@ /* chat item text */ "encryption re-negotiation required for %@" = "heronderhandeling van versleuteling vereist voor % @"; +/* No comment provided by engineer. */ +"Encryption renegotiation in progress." = "Er wordt opnieuw onderhandeld over de encryptie."; + /* No comment provided by engineer. */ "ended" = "geëindigd"; @@ -1458,6 +2074,9 @@ /* No comment provided by engineer. */ "Enter Passcode" = "Voer toegangscode in"; +/* No comment provided by engineer. */ +"Enter passphrase" = "Voer het wachtwoord in"; + /* No comment provided by engineer. */ "Enter passphrase…" = "Voer wachtwoord in…"; @@ -1471,10 +2090,10 @@ "Enter this device name…" = "Voer deze apparaatnaam in…"; /* placeholder */ -"Enter welcome message…" = "Welkomst bericht invoeren…"; +"Enter welcome message…" = "Welkom bericht invoeren…"; /* placeholder */ -"Enter welcome message… (optional)" = "Voer welkomst bericht in... (optioneel)"; +"Enter welcome message… (optional)" = "Voer welkom bericht in... (optioneel)"; /* No comment provided by engineer. */ "Enter your name…" = "Vul uw naam in…"; @@ -1488,24 +2107,39 @@ /* No comment provided by engineer. */ "Error aborting address change" = "Fout bij het afbreken van adres wijziging"; +/* alert title */ +"Error accepting conditions" = "Fout bij het accepteren van voorwaarden"; + /* No comment provided by engineer. */ "Error accepting contact request" = "Fout bij het accepteren van een contactverzoek"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Fout bij toegang tot database bestand"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Fout bij het toevoegen van leden"; +/* alert title */ +"Error adding server" = "Fout bij toevoegen server"; + /* No comment provided by engineer. */ "Error changing address" = "Fout bij wijzigen van adres"; +/* No comment provided by engineer. */ +"Error changing connection profile" = "Fout bij wijzigen van verbindingsprofiel"; + /* No comment provided by engineer. */ "Error changing role" = "Fout bij wisselen van rol"; /* No comment provided by engineer. */ "Error changing setting" = "Fout bij wijzigen van instelling"; +/* No comment provided by engineer. */ +"Error changing to incognito!" = "Fout bij het overschakelen naar incognito!"; + +/* No comment provided by engineer. */ +"Error checking token status" = "Fout bij het controleren van de tokenstatus"; + +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "Fout bij het verbinden met doorstuurserver %@. Probeer het later opnieuw."; + /* No comment provided by engineer. */ "Error creating address" = "Fout bij aanmaken van adres"; @@ -1515,12 +2149,21 @@ /* No comment provided by engineer. */ "Error creating group link" = "Fout bij maken van groep link"; +/* alert title */ +"Error creating list" = "Fout bij het aanmaken van de lijst"; + /* No comment provided by engineer. */ "Error creating member contact" = "Fout bij aanmaken contact"; +/* No comment provided by engineer. */ +"Error creating message" = "Fout bij het maken van een bericht"; + /* No comment provided by engineer. */ "Error creating profile!" = "Fout bij aanmaken van profiel!"; +/* No comment provided by engineer. */ +"Error creating report" = "Fout bij het rapporteren"; + /* No comment provided by engineer. */ "Error decrypting file" = "Fout bij het ontsleutelen van bestand"; @@ -1533,9 +2176,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Fout bij verwijderen van verbinding"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Fout bij het verwijderen van contact"; - /* No comment provided by engineer. */ "Error deleting database" = "Fout bij het verwijderen van de database"; @@ -1548,6 +2188,9 @@ /* No comment provided by engineer. */ "Error deleting user profile" = "Fout bij het verwijderen van gebruikers profiel"; +/* No comment provided by engineer. */ +"Error downloading the archive" = "Fout bij het downloaden van het archief"; + /* No comment provided by engineer. */ "Error enabling delivery receipts!" = "Fout bij het inschakelen van ontvangst bevestiging!"; @@ -1560,23 +2203,47 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Fout bij het exporteren van de chat database"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Fout bij exporteren van thema: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Fout bij het importeren van de chat database"; /* No comment provided by engineer. */ "Error joining group" = "Fout bij lid worden van groep"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Fout bij het laden van %@ servers"; +/* alert title */ +"Error loading servers" = "Fout bij het laden van servers"; /* No comment provided by engineer. */ +"Error migrating settings" = "Fout bij migreren van instellingen"; + +/* No comment provided by engineer. */ +"Error opening chat" = "Fout bij het openen van de chat"; + +/* alert title */ "Error receiving file" = "Fout bij ontvangen van bestand"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Fout bij opnieuw verbinding maken met de server"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Fout bij opnieuw verbinden van servers"; + +/* alert title */ +"Error registering for notifications" = "Fout bij registreren voor meldingen"; + /* No comment provided by engineer. */ "Error removing member" = "Fout bij verwijderen van lid"; +/* alert title */ +"Error reordering lists" = "Fout bij het opnieuw ordenen van lijsten"; + /* No comment provided by engineer. */ -"Error saving %@ servers" = "Fout bij opslaan van %@ servers"; +"Error resetting statistics" = "Fout bij het resetten van statistieken"; + +/* alert title */ +"Error saving chat list" = "Fout bij het opslaan van chatlijst"; /* No comment provided by engineer. */ "Error saving group profile" = "Fout bij opslaan van groep profiel"; @@ -1590,9 +2257,18 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "Fout bij opslaan van wachtwoord in de keychain"; +/* alert title */ +"Error saving servers" = "Fout bij het opslaan van servers"; + +/* when migrating */ +"Error saving settings" = "Fout bij opslaan van instellingen"; + /* No comment provided by engineer. */ "Error saving user password" = "Fout bij opslaan gebruikers wachtwoord"; +/* No comment provided by engineer. */ +"Error scanning code: %@" = "Fout bij het scannen van code: %@"; + /* No comment provided by engineer. */ "Error sending email" = "Fout bij het verzenden van e-mail"; @@ -1612,17 +2288,26 @@ "Error stopping chat" = "Fout bij het stoppen van de chat"; /* No comment provided by engineer. */ +"Error switching profile" = "Fout bij wisselen van profiel"; + +/* alertTitle */ "Error switching profile!" = "Fout bij wisselen van profiel!"; /* No comment provided by engineer. */ "Error synchronizing connection" = "Fout bij het synchroniseren van de verbinding"; +/* No comment provided by engineer. */ +"Error testing server connection" = "Fout bij het testen van de serververbinding"; + /* No comment provided by engineer. */ "Error updating group link" = "Fout bij bijwerken van groep link"; /* No comment provided by engineer. */ "Error updating message" = "Fout bij updaten van bericht"; +/* alert title */ +"Error updating server" = "Fout bij het updaten van de server"; + /* No comment provided by engineer. */ "Error updating settings" = "Fout bij bijwerken van instellingen"; @@ -1630,9 +2315,17 @@ "Error updating user privacy" = "Fout bij updaten van gebruikers privacy"; /* No comment provided by engineer. */ -"Error: " = "Fout: "; +"Error uploading the archive" = "Fout bij het uploaden van het archief"; /* No comment provided by engineer. */ +"Error verifying passphrase:" = "Fout bij het verifiëren van het wachtwoord:"; + +/* No comment provided by engineer. */ +"Error: " = "Fout: "; + +/* alert message +file error text +snd error text */ "Error: %@" = "Fout: %@"; /* No comment provided by engineer. */ @@ -1642,16 +2335,25 @@ "Error: URL is invalid" = "Fout: URL is ongeldig"; /* No comment provided by engineer. */ -"Even when disabled in the conversation." = "Zelfs wanneer uitgeschakeld in het gesprek."; +"Errors" = "Fouten"; + +/* servers error */ +"Errors in servers configuration." = "Fouten in de serverconfiguratie."; /* No comment provided by engineer. */ -"event happened" = "gebeurtenis gebeurd"; +"Even when disabled in the conversation." = "Zelfs wanneer uitgeschakeld in het gesprek."; /* No comment provided by engineer. */ "Exit without saving" = "Afsluiten zonder opslaan"; /* chat item action */ -"Expand" = "Uitbreiden"; +"Expand" = "Uitklappen"; + +/* No comment provided by engineer. */ +"expired" = "verlopen"; + +/* token status text */ +"Expired" = "Verlopen"; /* No comment provided by engineer. */ "Export database" = "Database exporteren"; @@ -1659,9 +2361,15 @@ /* No comment provided by engineer. */ "Export error:" = "Exportfout:"; +/* No comment provided by engineer. */ +"Export theme" = "Exporteer thema"; + /* No comment provided by engineer. */ "Exported database archive." = "Geëxporteerd database archief."; +/* No comment provided by engineer. */ +"Exported file doesn't exist" = "Geëxporteerd bestand bestaat niet"; + /* No comment provided by engineer. */ "Exporting database archive…" = "Database archief exporteren…"; @@ -1671,12 +2379,42 @@ /* No comment provided by engineer. */ "Fast and no wait until the sender is online!" = "Snel en niet wachten tot de afzender online is!"; +/* No comment provided by engineer. */ +"Faster deletion of groups." = "Sneller verwijderen van groepen."; + /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Snellere deelname en betrouwbaardere berichten."; /* No comment provided by engineer. */ +"Faster sending messages." = "Sneller verzenden van berichten."; + +/* swipe action */ "Favorite" = "Favoriet"; +/* No comment provided by engineer. */ +"Favorites" = "Favorieten"; + +/* file error alert title */ +"File error" = "Bestandsfout"; + +/* alert message */ +"File errors:\n%@" = "Bestandsfouten:\n%@"; + +/* file error text */ +"File is blocked by server operator:\n%@." = "Bestand is geblokkeerd door serveroperator:\n%@."; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Bestand niet gevonden - hoogstwaarschijnlijk is het bestand verwijderd of geannuleerd."; + +/* file error text */ +"File server error: %@" = "Bestandsserverfout: %@"; + +/* No comment provided by engineer. */ +"File status" = "Bestandsstatus"; + +/* copied message info */ +"File status: %@" = "Bestandsstatus: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Het bestand wordt van de servers verwijderd."; @@ -1689,6 +2427,9 @@ /* No comment provided by engineer. */ "File: %@" = "Bestand: %@"; +/* No comment provided by engineer. */ +"Files" = "Bestanden"; + /* No comment provided by engineer. */ "Files & media" = "Bestanden en media"; @@ -1696,19 +2437,28 @@ "Files and media" = "Bestanden en media"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Bestanden en media zijn verboden in deze groep."; +"Files and media are prohibited." = "Bestanden en media zijn niet toegestaan."; /* No comment provided by engineer. */ -"Files and media prohibited!" = "Bestanden en media verboden!"; +"Files and media not allowed" = "Bestanden en media niet toegestaan"; + +/* No comment provided by engineer. */ +"Files and media prohibited!" = "Bestanden en media niet toegestaan!"; /* No comment provided by engineer. */ "Filter unread and favorite chats." = "Filter ongelezen en favoriete chats."; +/* No comment provided by engineer. */ +"Finalize migration" = "Voltooi de migratie"; + +/* No comment provided by engineer. */ +"Finalize migration on another device." = "Voltooi de migratie op een ander apparaat."; + /* No comment provided by engineer. */ "Finally, we have them! 🚀" = "Eindelijk, we hebben ze! 🚀"; /* No comment provided by engineer. */ -"Find chats faster" = "Vind gesprekken sneller"; +"Find chats faster" = "Vind chats sneller"; /* No comment provided by engineer. */ "Fix" = "Herstel"; @@ -1728,9 +2478,72 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Herstel wordt niet ondersteund door groepslid"; +/* No comment provided by engineer. */ +"For all moderators" = "Voor alle moderators"; + +/* servers error */ +"For chat profile %@:" = "Voor chatprofiel %@:"; + /* No comment provided by engineer. */ "For console" = "Voor console"; +/* No comment provided by engineer. */ +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Als uw contactpersoon bijvoorbeeld berichten ontvangt via een SimpleX Chat-server, worden deze door uw app via een Flux-server verzonden."; + +/* No comment provided by engineer. */ +"For me" = "Voor mij"; + +/* No comment provided by engineer. */ +"For private routing" = "Voor privé-routering"; + +/* No comment provided by engineer. */ +"For social media" = "Voor social media"; + +/* chat item action */ +"Forward" = "Doorsturen"; + +/* alert title */ +"Forward %d message(s)?" = "%d bericht(en) doorsturen?"; + +/* No comment provided by engineer. */ +"Forward and save messages" = "Berichten doorsturen en opslaan"; + +/* alert action */ +"Forward messages" = "Berichten doorsturen"; + +/* alert message */ +"Forward messages without files?" = "Berichten doorsturen zonder bestanden?"; + +/* No comment provided by engineer. */ +"Forward up to 20 messages at once." = "Stuur maximaal 20 berichten tegelijk door."; + +/* No comment provided by engineer. */ +"forwarded" = "doorgestuurd"; + +/* No comment provided by engineer. */ +"Forwarded" = "Doorgestuurd"; + +/* No comment provided by engineer. */ +"Forwarded from" = "Doorgestuurd vanuit"; + +/* No comment provided by engineer. */ +"Forwarding %lld messages" = "%lld berichten doorsturen"; + +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "De doorstuurserver %@ kon geen verbinding maken met de bestemmingsserver %@. Probeer het later opnieuw."; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "Het adres van de doorstuurserver is niet compatibel met de netwerkinstellingen: %@."; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "De doorstuurserverversie is niet compatibel met de netwerkinstellingen: %@."; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Doorstuurserver: %1$@\nBestemmingsserverfout: %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Doorstuurserver: %1$@\nFout: %2$@"; + /* No comment provided by engineer. */ "Found desktop" = "Desktop gevonden"; @@ -1743,9 +2556,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Volledige naam (optioneel)"; -/* No comment provided by engineer. */ -"Full name:" = "Volledige naam:"; - /* No comment provided by engineer. */ "Fully decentralized – visible only to members." = "Volledig gedecentraliseerd – alleen zichtbaar voor leden."; @@ -1755,9 +2565,18 @@ /* No comment provided by engineer. */ "Further reduced battery usage" = "Verder verminderd batterij verbruik"; +/* No comment provided by engineer. */ +"Get notified when mentioned." = "Ontvang een melding als u vermeld wordt."; + /* No comment provided by engineer. */ "GIFs and stickers" = "GIF's en stickers"; +/* message preview */ +"Good afternoon!" = "Goedemiddag!"; + +/* message preview */ +"Good morning!" = "Goedemorgen!"; + /* No comment provided by engineer. */ "Group" = "Groep"; @@ -1794,24 +2613,6 @@ /* No comment provided by engineer. */ "Group links" = "Groep links"; -/* No comment provided by engineer. */ -"Group members can add message reactions." = "Groepsleden kunnen berichtreacties toevoegen."; - -/* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages." = "Groepsleden kunnen verzonden berichten onherroepelijk verwijderen."; - -/* No comment provided by engineer. */ -"Group members can send direct messages." = "Groepsleden kunnen directe berichten sturen."; - -/* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Groepsleden kunnen verdwijnende berichten sturen."; - -/* No comment provided by engineer. */ -"Group members can send files and media." = "Groepsleden kunnen bestanden en media verzenden."; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "Groepsleden kunnen spraak berichten verzenden."; - /* notification */ "Group message:" = "Groep bericht:"; @@ -1831,7 +2632,7 @@ "group profile updated" = "groep profiel bijgewerkt"; /* No comment provided by engineer. */ -"Group welcome message" = "Groep welkomst bericht"; +"Group welcome message" = "Groep welkom bericht"; /* No comment provided by engineer. */ "Group will be deleted for all members - this cannot be undone!" = "Groep wordt verwijderd voor alle leden, dit kan niet ongedaan worden gemaakt!"; @@ -1839,14 +2640,20 @@ /* No comment provided by engineer. */ "Group will be deleted for you - this cannot be undone!" = "De groep wordt voor u verwijderd, dit kan niet ongedaan worden gemaakt!"; +/* No comment provided by engineer. */ +"Groups" = "Groepen"; + /* No comment provided by engineer. */ "Help" = "Help"; +/* No comment provided by engineer. */ +"Help admins moderating their groups." = "Help beheerders bij het modereren van hun groepen."; + /* No comment provided by engineer. */ "Hidden" = "Verborgen"; /* No comment provided by engineer. */ -"Hidden chat profiles" = "Verborgen chat profielen"; +"Hidden chat profiles" = "Verborgen chatprofielen"; /* No comment provided by engineer. */ "Hidden profile password" = "Verborgen profiel wachtwoord"; @@ -1866,10 +2673,19 @@ /* No comment provided by engineer. */ "History" = "Geschiedenis"; +/* No comment provided by engineer. */ +"History is not sent to new members." = "Geschiedenis wordt niet naar nieuwe leden gestuurd."; + /* time unit */ "hours" = "uren"; /* No comment provided by engineer. */ +"How it affects privacy" = "Hoe het de privacy beïnvloedt"; + +/* No comment provided by engineer. */ +"How it helps privacy" = "Hoe het de privacy helpt"; + +/* alert button */ "How it works" = "Hoe het werkt"; /* No comment provided by engineer. */ @@ -1884,6 +2700,9 @@ /* No comment provided by engineer. */ "How to use your servers" = "Hoe u uw servers gebruikt"; +/* No comment provided by engineer. */ +"Hungarian interface" = "Hongaarse interface"; + /* No comment provided by engineer. */ "ICE servers (one per line)" = "ICE servers (één per lijn)"; @@ -1891,10 +2710,7 @@ "If you can't meet in person, show QR code in a video call, or share the link." = "Als je elkaar niet persoonlijk kunt ontmoeten, laat dan de QR-code zien in een videogesprek of deel de link."; /* No comment provided by engineer. */ -"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Als u elkaar niet persoonlijk kunt ontmoeten, kunt u **de QR-code scannen in het video gesprek**, of uw contact kan een uitnodiging link delen."; - -/* No comment provided by engineer. */ -"If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens onomkeerbaar verwijderd!"; +"If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens definitief verwijderd!"; /* No comment provided by engineer. */ "If you enter your self-destruct passcode while opening the app:" = "Als u uw zelfvernietigings wachtwoord invoert tijdens het openen van de app:"; @@ -1915,7 +2731,7 @@ "Immediately" = "Onmiddellijk"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Immuun voor spam en misbruik"; +"Immune to spam" = "Immuun voor spam en misbruik"; /* No comment provided by engineer. */ "Import" = "Importeren"; @@ -1926,15 +2742,45 @@ /* No comment provided by engineer. */ "Import database" = "Database importeren"; +/* No comment provided by engineer. */ +"Import failed" = "Importeren is mislukt"; + +/* No comment provided by engineer. */ +"Import theme" = "Thema importeren"; + +/* No comment provided by engineer. */ +"Importing archive" = "Archief importeren"; + +/* No comment provided by engineer. */ +"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "Verbeterde levering, minder data gebruik.\nBinnenkort meer verbeteringen!"; + +/* No comment provided by engineer. */ +"Improved message delivery" = "Verbeterde berichtbezorging"; + /* No comment provided by engineer. */ "Improved privacy and security" = "Verbeterde privacy en veiligheid"; /* No comment provided by engineer. */ "Improved server configuration" = "Verbeterde serverconfiguratie"; +/* No comment provided by engineer. */ +"In order to continue, chat should be stopped." = "Om verder te kunnen gaan, moet de chat worden gestopt."; + /* No comment provided by engineer. */ "In reply to" = "In antwoord op"; +/* No comment provided by engineer. */ +"In-call sounds" = "Geluiden tijdens het bellen"; + +/* No comment provided by engineer. */ +"inactive" = "inactief"; + +/* report reason */ +"Inappropriate content" = "Ongepaste inhoud"; + +/* report reason */ +"Inappropriate profile" = "Ongepast profiel"; + /* No comment provided by engineer. */ "Incognito" = "Incognito"; @@ -1948,7 +2794,7 @@ "Incognito mode protects your privacy by using a new random profile for each contact." = "Incognito -modus beschermt uw privacy met behulp van een nieuw willekeurig profiel voor elk contact."; /* chat list item description */ -"incognito via contact address link" = "incognito via contactadres link"; +"incognito via contact address link" = "incognito via contact adres link"; /* chat list item description */ "incognito via group link" = "incognito via groep link"; @@ -1989,14 +2835,32 @@ /* 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)"; +/* No comment provided by engineer. */ +"Instant" = "Direct"; + /* No comment provided by engineer. */ "Instant push notifications will be hidden!\n" = "Directe push meldingen worden verborgen!\n"; /* No comment provided by engineer. */ -"Instantly" = "Meteen"; +"Interface" = "Interface"; /* No comment provided by engineer. */ -"Interface" = "Interface"; +"Interface colors" = "Interface kleuren"; + +/* token status text */ +"Invalid" = "Ongeldig"; + +/* token status text */ +"Invalid (bad token)" = "Ongeldig (ongeldig token)"; + +/* token status text */ +"Invalid (expired)" = "Ongeldig (verlopen)"; + +/* token status text */ +"Invalid (unregistered)" = "Ongeldig (niet geregistreerd)"; + +/* token status text */ +"Invalid (wrong topic)" = "Ongeldig (verkeerd onderwerp)"; /* invalid chat data */ "invalid chat" = "ongeldige gesprek"; @@ -2010,10 +2874,25 @@ /* invalid chat item */ "invalid data" = "ongeldige gegevens"; +/* No comment provided by engineer. */ +"Invalid display name!" = "Ongeldige weergavenaam!"; + +/* No comment provided by engineer. */ +"Invalid link" = "Ongeldige link"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Ongeldige migratie bevestiging"; + /* No comment provided by engineer. */ "Invalid name!" = "Ongeldige naam!"; /* No comment provided by engineer. */ +"Invalid QR code" = "Ongeldige QR-code"; + +/* No comment provided by engineer. */ +"Invalid response" = "Ongeldig antwoord"; + +/* alert title */ "Invalid server address!" = "Ongeldig server adres!"; /* item status text */ @@ -2025,12 +2904,18 @@ /* group name */ "invitation to group %@" = "uitnodiging voor groep %@"; +/* No comment provided by engineer. */ +"invite" = "uitnodiging"; + /* No comment provided by engineer. */ "Invite friends" = "Nodig vrienden uit"; /* No comment provided by engineer. */ "Invite members" = "Nodig leden uit"; +/* No comment provided by engineer. */ +"Invite to chat" = "Uitnodigen voor een chat"; + /* No comment provided by engineer. */ "Invite to group" = "Uitnodigen voor groep"; @@ -2052,17 +2937,20 @@ /* 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 wordt gebruikt om het wachtwoord veilig op te slaan nadat u de app opnieuw hebt opgestart of het wachtwoord hebt gewijzigd, hiermee kunt u push meldingen ontvangen."; +/* No comment provided by engineer. */ +"IP address" = "IP-adres"; + /* No comment provided by engineer. */ "Irreversible message deletion" = "Onomkeerbare berichtverwijdering"; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this chat." = "Het onomkeerbaar verwijderen van berichten is verboden in dit gesprek."; +"Irreversible message deletion is prohibited in this chat." = "Het definitief verwijderen van berichten is niet toegestaan in dit gesprek."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "Het onomkeerbaar verwijderen van berichten is verboden in deze groep."; +"Irreversible message deletion is prohibited." = "Het definitief verwijderen van berichten is verbHet definitief verwijderen van berichten is niet toegestaan.."; /* No comment provided by engineer. */ -"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Het maakt het mogelijk om veel anonieme verbindingen te hebben zonder enige gedeelde gegevens tussen hen in een enkel chat profiel."; +"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Het maakt het mogelijk om veel anonieme verbindingen te hebben zonder enige gedeelde gegevens tussen hen in een enkel chatprofiel."; /* No comment provided by engineer. */ "It can happen when you or your connection used the old database backup." = "Het kan gebeuren wanneer u of de ander een oude database back-up gebruikt."; @@ -2070,6 +2958,9 @@ /* No comment provided by engineer. */ "It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Het kan gebeuren wanneer:\n1. De berichten zijn na 2 dagen verlopen bij de verzendende client of na 30 dagen op de server.\n2. Decodering van het bericht is mislukt, omdat u of uw contact een oude database back-up heeft gebruikt.\n3. De verbinding is verbroken."; +/* No comment provided by engineer. */ +"It protects your IP address and connections." = "Het beschermt uw IP-adres en verbindingen."; + /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Het lijkt erop dat u al bent verbonden via deze link. Als dit niet het geval is, is er een fout opgetreden (%@)."; @@ -2082,8 +2973,8 @@ /* No comment provided by engineer. */ "Japanese interface" = "Japanse interface"; -/* No comment provided by engineer. */ -"Join" = "Word lid van"; +/* swipe action */ +"Join" = "Word lid"; /* No comment provided by engineer. */ "join as %@" = "deelnemen als %@"; @@ -2091,6 +2982,9 @@ /* No comment provided by engineer. */ "Join group" = "Word lid van groep"; +/* No comment provided by engineer. */ +"Join group conversations" = "Neem deel aan groepsgesprekken"; + /* No comment provided by engineer. */ "Join group?" = "Deelnemen aan groep?"; @@ -2106,9 +3000,18 @@ /* No comment provided by engineer. */ "Joining group" = "Deel nemen aan groep"; +/* alert action */ +"Keep" = "Bewaar"; + +/* No comment provided by engineer. */ +"Keep conversation" = "Behoud het gesprek"; + /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Houd de app geopend om deze vanaf de desktop te gebruiken"; +/* alert title */ +"Keep unused invitation?" = "Ongebruikte uitnodiging bewaren?"; + /* No comment provided by engineer. */ "Keep your connections" = "Behoud uw verbindingen"; @@ -2124,9 +3027,15 @@ /* No comment provided by engineer. */ "Learn more" = "Kom meer te weten"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Verlaten"; +/* No comment provided by engineer. */ +"Leave chat" = "Chat verlaten"; + +/* No comment provided by engineer. */ +"Leave chat?" = "Chat verlaten?"; + /* No comment provided by engineer. */ "Leave group" = "Groep verlaten"; @@ -2134,7 +3043,7 @@ "Leave group?" = "Groep verlaten?"; /* rcv group event chat item */ -"left" = "verlaten"; +"left" = "is vertrokken"; /* email subject */ "Let's talk in SimpleX Chat" = "Laten we praten in SimpleX Chat"; @@ -2154,6 +3063,15 @@ /* No comment provided by engineer. */ "Linked desktops" = "Gelinkte desktops"; +/* swipe action */ +"List" = "Lijst"; + +/* No comment provided by engineer. */ +"List name and emoji should be different for all lists." = "De naam en emoji van de lijst moeten voor alle lijsten verschillend zijn."; + +/* No comment provided by engineer. */ +"List name..." = "Naam van lijst..."; + /* No comment provided by engineer. */ "LIVE" = "LIVE"; @@ -2175,24 +3093,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Vergrendeling modus"; -/* No comment provided by engineer. */ -"Make a private connection" = "Maak een privéverbinding"; - /* No comment provided by engineer. */ "Make one message disappear" = "Eén bericht laten verdwijnen"; /* No comment provided by engineer. */ "Make profile private!" = "Profiel privé maken!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Zorg ervoor dat %@ server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Zorg ervoor dat WebRTC ICE server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Veel mensen vroegen: *als SimpleX geen gebruikers-ID's heeft, hoe kan het dan berichten bezorgen?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Markeer verwijderd voor iedereen"; @@ -2211,48 +3120,135 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "Max 30 seconden, direct ontvangen."; +/* No comment provided by engineer. */ +"Media & file servers" = "Media- en bestandsservers"; + +/* blur media */ +"Medium" = "Medium"; + /* member role */ "member" = "lid"; /* No comment provided by engineer. */ "Member" = "Lid"; +/* profile update event chat item */ +"member %@ changed to %@" = "lid %1$@ gewijzigd in %2$@"; + /* rcv group event chat item */ "member connected" = "is toegetreden"; +/* item status text */ +"Member inactive" = "Lid inactief"; + +/* chat feature */ +"Member reports" = "Ledenrapporten"; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All chat members will be notified." = "De rol van het lid wordt gewijzigd naar \"%@\". Alle chatleden worden op de hoogte gebracht."; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "De rol van lid wordt gewijzigd in \"%@\". Alle groepsleden worden op de hoogte gebracht."; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "De rol van lid wordt gewijzigd in \"%@\". Het lid ontvangt een nieuwe uitnodiging."; +/* No comment provided by engineer. */ +"Member will be removed from chat - this cannot be undone!" = "Lid wordt verwijderd uit de chat - dit kan niet ongedaan worden gemaakt!"; + /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Lid wordt uit de groep verwijderd, dit kan niet ongedaan worden gemaakt!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Groepsleden kunnen bericht reacties toevoegen."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "Groepsleden kunnen verzonden berichten onherroepelijk verwijderen. (24 uur)"; + +/* No comment provided by engineer. */ +"Members can report messsages to moderators." = "Leden kunnen berichten melden bij moderators."; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Groepsleden kunnen directe berichten sturen."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Groepsleden kunnen verdwijnende berichten sturen."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Groepsleden kunnen bestanden en media verzenden."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "Groepsleden kunnen SimpleX-links verzenden."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Groepsleden kunnen spraak berichten verzenden."; + +/* No comment provided by engineer. */ +"Mention members 👋" = "Vermeld leden 👋"; + +/* No comment provided by engineer. */ +"Menus" = "Menu's"; + +/* No comment provided by engineer. */ +"message" = "bericht"; + /* item status text */ "Message delivery error" = "Fout bij bezorging van bericht"; /* No comment provided by engineer. */ "Message delivery receipts!" = "Ontvangst bevestiging voor berichten!"; +/* item status text */ +"Message delivery warning" = "Waarschuwing voor berichtbezorging"; + /* No comment provided by engineer. */ "Message draft" = "Concept bericht"; +/* item status text */ +"Message forwarded" = "Bericht doorgestuurd"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Het bericht kan later worden bezorgd als het lid actief wordt."; + +/* No comment provided by engineer. */ +"Message queue info" = "Informatie over berichtenwachtrij"; + /* chat feature */ "Message reactions" = "Reacties op berichten"; /* No comment provided by engineer. */ -"Message reactions are prohibited in this chat." = "Reacties op berichten zijn verboden in deze chat."; +"Message reactions are prohibited in this chat." = "Reacties op berichten zijn niet toegestaan in deze chat."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Reacties op berichten zijn verboden in deze groep."; +"Message reactions are prohibited." = "Reacties op berichten zijn niet toegestaan."; /* notification */ "message received" = "bericht ontvangen"; +/* No comment provided by engineer. */ +"Message reception" = "Bericht ontvangst"; + +/* No comment provided by engineer. */ +"Message servers" = "Berichtservers"; + +/* No comment provided by engineer. */ +"Message shape" = "Berichtvorm"; + +/* No comment provided by engineer. */ +"Message source remains private." = "Berichtbron blijft privé."; + +/* No comment provided by engineer. */ +"Message status" = "Berichtstatus"; + +/* copied message info */ +"Message status: %@" = "Berichtstatus: %@"; + /* No comment provided by engineer. */ "Message text" = "Bericht tekst"; +/* No comment provided by engineer. */ +"Message too large" = "Bericht te groot"; + /* No comment provided by engineer. */ "Messages" = "Berichten"; @@ -2262,20 +3258,59 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Berichten van %@ worden getoond!"; +/* alert message */ +"Messages in this chat will never be deleted." = "Berichten in deze chat zullen nooit worden verwijderd."; + +/* No comment provided by engineer. */ +"Messages received" = "Berichten ontvangen"; + +/* No comment provided by engineer. */ +"Messages sent" = "Berichten verzonden"; + +/* alert message */ +"Messages were deleted after you selected them." = "Berichten zijn verwijderd nadat u ze had geselecteerd."; + +/* 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." = "Berichten, bestanden en oproepen worden beschermd door **end-to-end codering** met perfecte voorwaartse geheimhouding, afwijzing en inbraakherstel."; + +/* 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." = "Berichten, bestanden en oproepen worden beschermd door **kwantumbestendige e2e encryptie** met perfecte voorwaartse geheimhouding, afwijzing en inbraakherstel."; + +/* 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"; + +/* No comment provided by engineer. */ +"Migrate to another device" = "Migreer naar een ander apparaat"; + +/* No comment provided by engineer. */ +"Migrate to another device via QR code." = "Migreer naar een ander apparaat via QR-code."; + +/* No comment provided by engineer. */ +"Migrating" = "Migreren"; + /* No comment provided by engineer. */ "Migrating database archive…" = "Database archief migreren…"; +/* No comment provided by engineer. */ +"Migration complete" = "Migratie voltooid"; + /* No comment provided by engineer. */ "Migration error:" = "Migratiefout:"; /* No comment provided by engineer. */ -"Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." = "Migratie mislukt. Tik hieronder op **Overslaan** om de huidige database te blijven gebruiken. Meld het probleem aan de app-ontwikkelaars via chat of e-mail [chat@simplex.chat](mailto:chat@simplex.chat)."; +"Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." = "Migratie mislukt. Tik hier onder op **Overslaan** om de huidige database te blijven gebruiken. Meld het probleem aan de app-ontwikkelaars via chat of e-mail [chat@simplex.chat](mailto:chat@simplex.chat)."; /* No comment provided by engineer. */ "Migration is completed" = "Migratie is voltooid"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migraties: %@"; +"Migrations:" = "Migraties:"; /* time unit */ "minutes" = "minuten"; @@ -2295,63 +3330,102 @@ /* copied message info */ "Moderated at: %@" = "Gemodereerd op: %@"; -/* No comment provided by engineer. */ +/* marked deleted chat item preview text */ "moderated by %@" = "gemodereerd door %@"; +/* member role */ +"moderator" = "moderator"; + /* time unit */ "months" = "maanden"; +/* swipe action */ +"More" = "Meer"; + /* No comment provided by engineer. */ "More improvements are coming soon!" = "Meer verbeteringen volgen snel!"; +/* No comment provided by engineer. */ +"More reliable network connection." = "Betrouwbaardere netwerkverbinding."; + +/* No comment provided by engineer. */ +"More reliable notifications" = "Betrouwbaardere meldingen"; + /* item status description */ "Most likely this connection is deleted." = "Hoogstwaarschijnlijk is deze verbinding verwijderd."; /* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Hoogstwaarschijnlijk heeft dit contact de verbinding met jou verwijderd."; +"Multiple chat profiles" = "Meerdere chatprofielen"; -/* No comment provided by engineer. */ -"Multiple chat profiles" = "Meerdere chat profielen"; - -/* No comment provided by engineer. */ +/* notification label action */ "Mute" = "Dempen"; +/* notification label action */ +"Mute all" = "Alles dempen"; + /* No comment provided by engineer. */ "Muted when inactive!" = "Gedempt wanneer inactief!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Naam"; /* No comment provided by engineer. */ "Network & servers" = "Netwerk & servers"; +/* No comment provided by engineer. */ +"Network connection" = "Netwerkverbinding"; + +/* No comment provided by engineer. */ +"Network decentralization" = "Netwerk decentralisatie"; + +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "Netwerkproblemen - bericht is verlopen na vele pogingen om het te verzenden."; + +/* No comment provided by engineer. */ +"Network management" = "Netwerkbeheer"; + +/* No comment provided by engineer. */ +"Network operator" = "Netwerkbeheerder"; + /* No comment provided by engineer. */ "Network settings" = "Netwerk instellingen"; /* No comment provided by engineer. */ "Network status" = "Netwerk status"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "nooit"; +/* token status text */ +"New" = "Nieuw"; + +/* No comment provided by engineer. */ +"New chat" = "Nieuw gesprek"; + +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Nieuwe chatervaring 🎉"; + /* notification */ "New contact request" = "Nieuw contactverzoek"; /* notification */ "New contact:" = "Nieuw contact:"; -/* No comment provided by engineer. */ -"New database archive" = "Nieuw database archief"; - /* No comment provided by engineer. */ "New desktop app!" = "Nieuwe desktop app!"; /* No comment provided by engineer. */ "New display name" = "Nieuwe weergavenaam"; +/* notification */ +"New events" = "Nieuwe gebeurtenissen"; + /* No comment provided by engineer. */ "New in %@" = "Nieuw in %@"; +/* No comment provided by engineer. */ +"New media options" = "Nieuwe media-opties"; + /* No comment provided by engineer. */ "New member role" = "Nieuwe leden rol"; @@ -2367,6 +3441,15 @@ /* No comment provided by engineer. */ "New passphrase…" = "Nieuw wachtwoord…"; +/* No comment provided by engineer. */ +"New server" = "Nieuwe server"; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used every time you start the app." = "Elke keer dat u de app start, worden er nieuwe SOCKS-inloggegevens gebruikt."; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used for each server." = "Voor elke server worden nieuwe SOCKS-inloggegevens gebruikt."; + /* pref value */ "no" = "Nee"; @@ -2376,6 +3459,15 @@ /* Authentication unavailable */ "No app password" = "Geen app wachtwoord"; +/* No comment provided by engineer. */ +"No chats" = "Geen chats"; + +/* No comment provided by engineer. */ +"No chats found" = "Geen chats gevonden"; + +/* No comment provided by engineer. */ +"No chats in list %@" = "Geen chats in lijst %@"; + /* No comment provided by engineer. */ "No contacts selected" = "Geen contacten geselecteerd"; @@ -2388,11 +3480,14 @@ /* No comment provided by engineer. */ "No device token!" = "Geen apparaattoken!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Nog geen directe verbinding, bericht wordt doorgestuurd door beheerder."; + /* No comment provided by engineer. */ "no e2e encryption" = "geen e2e versleuteling"; /* No comment provided by engineer. */ -"No filtered chats" = "Geen gefilterde gesprekken"; +"No filtered chats" = "Geen gefilterde chats"; /* No comment provided by engineer. */ "No group!" = "Groep niet gevonden!"; @@ -2400,24 +3495,87 @@ /* No comment provided by engineer. */ "No history" = "Geen geschiedenis"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Geen info, probeer opnieuw te laden"; + +/* servers error */ +"No media & file servers." = "Geen media- en bestandsservers."; + +/* No comment provided by engineer. */ +"No message" = "Geen bericht"; + +/* servers error */ +"No message servers." = "Geen berichtenservers."; + +/* No comment provided by engineer. */ +"No network connection" = "Geen netwerkverbinding"; + +/* No comment provided by engineer. */ +"No permission to record speech" = "Geen toestemming om spraak op te nemen"; + +/* No comment provided by engineer. */ +"No permission to record video" = "Geen toestemming om video op te nemen"; + /* No comment provided by engineer. */ "No permission to record voice message" = "Geen toestemming om spraakbericht op te nemen"; +/* No comment provided by engineer. */ +"No push server" = "Lokaal"; + /* No comment provided by engineer. */ "No received or sent files" = "Geen ontvangen of verzonden bestanden"; +/* servers error */ +"No servers for private message routing." = "Geen servers voor het routeren van privéberichten."; + +/* servers error */ +"No servers to receive files." = "Geen servers om bestanden te ontvangen."; + +/* servers error */ +"No servers to receive messages." = "Geen servers om berichten te ontvangen."; + +/* servers error */ +"No servers to send files." = "Geen servers om bestanden te verzenden."; + /* copied message info in history */ "no text" = "geen tekst"; +/* alert title */ +"No token!" = "Geen token!"; + +/* 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!"; +/* No comment provided by engineer. */ +"Notes" = "Notities"; + +/* No comment provided by engineer. */ +"Nothing selected" = "Niets geselecteerd"; + +/* alert title */ +"Nothing to forward!" = "Niets om door te sturen!"; + /* No comment provided by engineer. */ "Notifications" = "Meldingen"; /* No comment provided by engineer. */ "Notifications are disabled!" = "Meldingen zijn uitgeschakeld!"; +/* alert title */ +"Notifications error" = "Meldingsfout"; + +/* No comment provided by engineer. */ +"Notifications privacy" = "Privacy van meldingen"; + +/* alert title */ +"Notifications status" = "Meldingsstatus"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Nu kunnen beheerders: \n- berichten van leden verwijderen.\n- schakel leden uit (\"waarnemer\" rol)"; @@ -2425,30 +3583,27 @@ "observer" = "Waarnemer"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "uit"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Uit"; -/* No comment provided by engineer. */ -"Off (Local)" = "Uit (lokaal)"; - /* feature offered item */ "offered %@" = "voorgesteld %@"; /* feature offered item */ "offered %@: %@" = "voorgesteld %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "OK"; /* No comment provided by engineer. */ -"Old database" = "Oude database"; +"OK" = "OK"; /* No comment provided by engineer. */ -"Old database archive" = "Oud database archief"; +"Old database" = "Oude database"; /* group pref value */ "on" = "aan"; @@ -2457,16 +3612,22 @@ "One-time invitation link" = "Eenmalige uitnodiging link"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Onion hosts zullen nodig zijn voor verbinding. Vereist het inschakelen van VPN."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Onion hosts zullen nodig zijn voor verbinding.\nVereist het inschakelen van VPN."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Onion hosts worden gebruikt indien beschikbaar. Vereist het inschakelen van VPN."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Onion hosts worden gebruikt indien beschikbaar.\nVereist het inschakelen van VPN."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion hosts worden niet gebruikt."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Alleen client apparaten slaan gebruikers profielen, contacten, groepen en berichten op die zijn verzonden met **2-laags end-to-end-codering**."; +"Only chat owners can change preferences." = "Alleen chateigenaren kunnen voorkeuren wijzigen."; + +/* No comment provided by engineer. */ +"Only client devices store user profiles, contacts, groups, and messages." = "Alleen client apparaten slaan gebruikers profielen, contacten, groepen en berichten op die zijn verzonden met **2-laags end-to-end-codering**."; + +/* No comment provided by engineer. */ +"Only delete conversation" = "Alleen conversatie verwijderen"; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Alleen groep eigenaren kunnen groep voorkeuren wijzigen."; @@ -2478,10 +3639,16 @@ "Only group owners can enable voice messages." = "Alleen groep eigenaren kunnen spraak berichten inschakelen."; /* No comment provided by engineer. */ -"Only you can add message reactions." = "Alleen jij kunt berichtreacties toevoegen."; +"Only sender and moderators see it" = "Alleen de verzender en moderators zien het"; /* No comment provided by engineer. */ -"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "Alleen jij kunt berichten onomkeerbaar verwijderen (je contact kan ze markeren voor verwijdering)."; +"Only you and moderators see it" = "Alleen jij en moderators zien het"; + +/* No comment provided by engineer. */ +"Only you can add message reactions." = "Alleen jij kunt bericht reacties toevoegen."; + +/* No comment provided by engineer. */ +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Alleen jij kunt berichten definitief verwijderen (je contact kan ze markeren voor verwijdering). (24 uur)"; /* No comment provided by engineer. */ "Only you can make calls." = "Alleen jij kunt bellen."; @@ -2493,10 +3660,10 @@ "Only you can send voice messages." = "Alleen jij kunt spraak berichten verzenden."; /* No comment provided by engineer. */ -"Only your contact can add message reactions." = "Alleen uw contact kan berichtreacties toevoegen."; +"Only your contact can add message reactions." = "Alleen uw contact kan bericht reacties toevoegen."; /* No comment provided by engineer. */ -"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Alleen uw contact kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering)."; +"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Alleen uw contact kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering). (24 uur)"; /* No comment provided by engineer. */ "Only your contact can make calls." = "Alleen je contact kan bellen."; @@ -2507,33 +3674,78 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Alleen uw contact kan spraak berichten verzenden."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Open"; /* No comment provided by engineer. */ -"Open chat" = "Gesprekken openen"; +"Open changes" = "Wijzigingen openen"; + +/* No comment provided by engineer. */ +"Open chat" = "Chat openen"; /* authentication reason */ "Open chat console" = "Chat console openen"; +/* No comment provided by engineer. */ +"Open conditions" = "Open voorwaarden"; + /* No comment provided by engineer. */ "Open group" = "Open groep"; +/* authentication reason */ +"Open migration to another device" = "Open de migratie naar een ander apparaat"; + /* No comment provided by engineer. */ "Open Settings" = "Open instellingen"; -/* authentication reason */ -"Open user profiles" = "Gebruikers profielen openen"; +/* No comment provided by engineer. */ +"Opening app…" = "App openen…"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Open-source protocol en code. Iedereen kan de servers draaien."; +"Operator" = "Operator"; + +/* alert title */ +"Operator server" = "Operatorserver"; /* No comment provided by engineer. */ -"Opening database…" = "Database openen…"; +"Or import archive file" = "Of importeer archiefbestand"; + +/* No comment provided by engineer. */ +"Or paste archive link" = "Of plak de archief link"; + +/* No comment provided by engineer. */ +"Or scan QR code" = "Of scan de QR-code"; + +/* No comment provided by engineer. */ +"Or securely share this file link" = "Of deel deze bestands link veilig"; + +/* No comment provided by engineer. */ +"Or show this code" = "Of laat deze code zien"; + +/* No comment provided by engineer. */ +"Or to share privately" = "Of om privé te delen"; + +/* No comment provided by engineer. */ +"Organize chats into lists" = "Organiseer chats in lijsten"; + +/* No comment provided by engineer. */ +"other" = "overig"; + +/* No comment provided by engineer. */ +"Other" = "Ander"; + +/* No comment provided by engineer. */ +"other errors" = "overige fouten"; + +/* alert message */ +"Other file errors:\n%@" = "Andere bestandsfouten:\n%@"; /* member role */ "owner" = "Eigenaar"; +/* feature role */ +"owners" = "eigenaren"; + /* No comment provided by engineer. */ "Passcode" = "Toegangscode"; @@ -2550,10 +3762,13 @@ "Passcode set!" = "Toegangscode ingesteld!"; /* No comment provided by engineer. */ -"Password to show" = "Wachtwoord om weer te geven"; +"Password" = "Wachtwoord"; /* No comment provided by engineer. */ -"Paste" = "Plakken"; +"Password to show" = "Wachtwoord om weer te geven"; + +/* past/unknown group member */ +"Past member %@" = "Voormalig lid %@"; /* No comment provided by engineer. */ "Paste desktop address" = "Desktopadres plakken"; @@ -2562,32 +3777,50 @@ "Paste image" = "Afbeelding plakken"; /* No comment provided by engineer. */ -"Paste received link" = "Plak de ontvangen link"; +"Paste link to connect!" = "Plak een link om te verbinden!"; -/* placeholder */ -"Paste the link you received to connect with your contact." = "Plak de link die je hebt ontvangen in het vak hieronder om verbinding te maken met je contact."; +/* No comment provided by engineer. */ +"Paste the link you received" = "Plak de link die je hebt ontvangen"; /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Mensen kunnen alleen verbinding met u maken via de links die u deelt."; +"pending" = "In behandeling"; /* No comment provided by engineer. */ -"Periodically" = "Periodiek"; +"Pending" = "in behandeling"; + +/* No comment provided by engineer. */ +"pending approval" = "in afwachting van goedkeuring"; + +/* No comment provided by engineer. */ +"Periodic" = "Periodiek"; /* message decrypt error item */ "Permanent decryption error" = "Decodering fout"; +/* No comment provided by engineer. */ +"Picture-in-picture calls" = "Beeld-in-beeld oproepen"; + /* No comment provided by engineer. */ "PING count" = "PING count"; /* No comment provided by engineer. */ "PING interval" = "PING interval"; +/* No comment provided by engineer. */ +"Play from the chat list." = "Afspelen via de chat lijst."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Vraag uw contactpersoon om oproepen in te schakelen."; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Vraag uw contact om het verzenden van spraak berichten in te schakelen."; +/* No comment provided by engineer. */ +"Please check that 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." = "Controleer of mobiel en desktop met hetzelfde lokale netwerk zijn verbonden en of de desktopfirewall de verbinding toestaat.\nDeel eventuele andere problemen met de ontwikkelaars."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Controleer of u de juiste link heeft gebruikt of vraag uw contact om u een andere te sturen."; @@ -2597,6 +3830,12 @@ /* No comment provided by engineer. */ "Please check yours and your contact preferences." = "Controleer de uwe en uw contact voorkeuren."; +/* No comment provided by engineer. */ +"Please confirm that network settings are correct for this device." = "Controleer of de netwerk instellingen correct zijn voor dit apparaat."; + +/* No comment provided by engineer. */ +"Please contact developers.\nError: %@" = "Neem contact op met ontwikkelaars.\nFout: %@"; + /* No comment provided by engineer. */ "Please contact group admin." = "Neem contact op met de groep beheerder."; @@ -2616,14 +3855,26 @@ "Please restart the app and migrate the database to enable push notifications." = "Start de app opnieuw en migreer de database om push meldingen in te schakelen."; /* No comment provided by engineer. */ -"Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Sla het wachtwoord veilig op. Als u deze kwijtraakt, heeft u GEEN toegang tot de gesprekken."; +"Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Sla het wachtwoord veilig op. Als u deze kwijtraakt, heeft u GEEN toegang tot de chats."; /* No comment provided by engineer. */ "Please store passphrase securely, you will NOT be able to change it if you lose it." = "Bewaar het wachtwoord veilig, u kunt deze NIET wijzigen als u het kwijtraakt."; +/* token info */ +"Please try to disable and re-enable notfications." = "Probeer meldingen uit en weer in te schakelen."; + +/* token info */ +"Please wait for token activation to complete." = "Wacht tot de tokenactivering voltooid is."; + +/* token info */ +"Please wait for token to be registered." = "Wacht tot het token is geregistreerd."; + /* No comment provided by engineer. */ "Polish interface" = "Poolse interface"; +/* No comment provided by engineer. */ +"Port" = "Poort"; + /* server test error */ "Possibly, certificate fingerprint in server address is incorrect" = "Mogelijk is de certificaat vingerafdruk in het server adres onjuist"; @@ -2631,23 +3882,53 @@ "Preserve the last message draft, with attachments." = "Bewaar het laatste berichtconcept, met bijlagen."; /* No comment provided by engineer. */ -"Preset server" = "Vooraf ingestelde server"; +"Preset server address" = "Vooraf ingesteld server adres"; /* No comment provided by engineer. */ -"Preset server address" = "Vooraf ingesteld server adres"; +"Preset servers" = "Vooraf ingestelde servers"; /* No comment provided by engineer. */ "Preview" = "Voorbeeld"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Eerder verbonden servers"; + /* No comment provided by engineer. */ "Privacy & security" = "Privacy en beveiliging"; +/* No comment provided by engineer. */ +"Privacy for your customers." = "Privacy voor uw klanten."; + +/* 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"; +/* No comment provided by engineer. */ +"Private media file names." = "Namen van persoonlijke mediabestanden."; + +/* No comment provided by engineer. */ +"Private message routing" = "Routering van privéberichten"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Routing van privéberichten🚀"; + +/* name of notes to self */ +"Private notes" = "Privé notities"; + +/* No comment provided by engineer. */ +"Private routing" = "Privéroutering"; + +/* No comment provided by engineer. */ +"Private routing error" = "Fout in privéroutering"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profiel- en serververbindingen"; @@ -2655,29 +3936,32 @@ "Profile image" = "profielfoto"; /* No comment provided by engineer. */ -"Profile name" = "Profielnaam"; - -/* No comment provided by engineer. */ -"Profile name:" = "Profielnaam:"; +"Profile images" = "Profiel afbeeldingen"; /* No comment provided by engineer. */ "Profile password" = "Profiel wachtwoord"; /* 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."; /* No comment provided by engineer. */ -"Prohibit irreversible message deletion." = "Verbied het onomkeerbaar verwijderen van berichten."; +"Prohibit irreversible message deletion." = "Verbied het definitief verwijderen van berichten."; /* No comment provided by engineer. */ -"Prohibit message reactions." = "Berichtreacties verbieden."; +"Prohibit message reactions." = "Bericht reacties verbieden."; /* No comment provided by engineer. */ "Prohibit messages reactions." = "Berichten reacties verbieden."; +/* No comment provided by engineer. */ +"Prohibit reporting messages to moderators." = "Het melden van berichten aan moderators is niet toegestaan."; + /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "Verbied het sturen van directe berichten naar leden."; @@ -2687,6 +3971,9 @@ /* No comment provided by engineer. */ "Prohibit sending files and media." = "Verbied het verzenden van bestanden en media."; +/* No comment provided by engineer. */ +"Prohibit sending SimpleX links." = "Verbied het verzenden van SimpleX-links"; + /* No comment provided by engineer. */ "Prohibit sending voice messages." = "Verbieden het verzenden van spraak berichten."; @@ -2694,7 +3981,13 @@ "Protect app screen" = "App scherm verbergen"; /* No comment provided by engineer. */ -"Protect your chat profiles with a password!" = "Bescherm je chat profielen met een wachtwoord!"; +"Protect IP address" = "Bescherm het IP-adres"; + +/* No comment provided by engineer. */ +"Protect your chat profiles with a password!" = "Bescherm je chatprofielen met een wachtwoord!"; + +/* No comment provided by engineer. */ +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Bescherm uw IP-adres tegen de berichtenrelais die door uw contacten zijn gekozen.\nSchakel dit in in *Netwerk en servers*-instellingen."; /* No comment provided by engineer. */ "Protocol timeout" = "Protocol timeout"; @@ -2702,23 +3995,47 @@ /* No comment provided by engineer. */ "Protocol timeout per KB" = "Protocol timeout per KB"; +/* No comment provided by engineer. */ +"Proxied" = "Proxied"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Proxied servers"; + +/* No comment provided by engineer. */ +"Proxy requires password" = "Proxy vereist wachtwoord"; + /* No comment provided by engineer. */ "Push notifications" = "Push meldingen"; +/* No comment provided by engineer. */ +"Push server" = "Push server"; + +/* chat item text */ +"quantum resistant e2e encryption" = "quantum bestendige e2e-codering"; + +/* No comment provided by engineer. */ +"Quantum resistant encryption" = "quantum bestendige encryptie"; + /* No comment provided by engineer. */ "Rate the app" = "Beoordeel de app"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Toegankelijke chatwerkbalk"; + /* chat item menu */ "React…" = "Reageer…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Lees"; /* No comment provided by engineer. */ "Read more" = "Lees meer"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/app-settings.html#uw-simplex-contactadres)."; +"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)."; + +/* 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)."; @@ -2727,10 +4044,10 @@ "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)."; /* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Lees meer in onze GitHub repository."; +"Receipts are disabled" = "Bevestigingen zijn uitgeschakeld"; /* No comment provided by engineer. */ -"Receipts are disabled" = "Bevestigingen zijn uitgeschakeld"; +"Receive errors" = "Fouten ontvangen"; /* No comment provided by engineer. */ "received answer…" = "antwoord gekregen…"; @@ -2750,6 +4067,15 @@ /* message info title */ "Received message" = "Ontvangen bericht"; +/* No comment provided by engineer. */ +"Received messages" = "Ontvangen berichten"; + +/* No comment provided by engineer. */ +"Received reply" = "Antwoord ontvangen"; + +/* No comment provided by engineer. */ +"Received total" = "Totaal ontvangen"; + /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Het ontvangstadres wordt gewijzigd naar een andere server. Adres wijziging wordt voltooid nadat de afzender online is."; @@ -2759,12 +4085,33 @@ /* No comment provided by engineer. */ "Receiving via" = "Ontvang via"; +/* No comment provided by engineer. */ +"Recent history and improved [directory bot](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)." = "Recente geschiedenis en verbeterde [directory bot](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)."; + +/* No comment provided by engineer. */ +"Recipient(s) can't see who this message is from." = "Ontvanger(s) kunnen niet zien van wie dit bericht afkomstig is."; + /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Ontvangers zien updates terwijl u ze typt."; +/* No comment provided by engineer. */ +"Reconnect" = "opnieuw verbinden"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Verbind alle verbonden servers opnieuw om de bezorging van berichten af te dwingen. Het maakt gebruik van extra data."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Maak opnieuw verbinding met alle servers"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Alle servers opnieuw verbinden?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Maak opnieuw verbinding met de server om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra verkeer.Maak opnieuw verbinding met de server om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra data."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Server opnieuw verbinden?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Servers opnieuw verbinden?"; @@ -2777,7 +4124,17 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Verminderd batterijgebruik"; -/* reject incoming call via notification */ +/* No comment provided by engineer. */ +"Register" = "Register"; + +/* token info */ +"Register notification token?" = "Meldingstoken registreren?"; + +/* token status text */ +"Registered" = "Geregistreerd"; + +/* reject incoming call via notification +swipe action */ "Reject" = "Afwijzen"; /* No comment provided by engineer. */ @@ -2786,6 +4143,9 @@ /* No comment provided by engineer. */ "Reject contact request" = "Contactverzoek afwijzen"; +/* No comment provided by engineer. */ +"rejected" = "afgewezen"; + /* call status */ "rejected call" = "geweigerde oproep"; @@ -2798,6 +4158,12 @@ /* No comment provided by engineer. */ "Remove" = "Verwijderen"; +/* No comment provided by engineer. */ +"Remove archive?" = "Archief verwijderen?"; + +/* No comment provided by engineer. */ +"Remove image" = "Verwijder afbeelding"; + /* No comment provided by engineer. */ "Remove member" = "Lid verwijderen"; @@ -2813,6 +4179,12 @@ /* rcv group event chat item */ "removed %@" = "verwijderd %@"; +/* profile update event chat item */ +"removed contact address" = "contactadres verwijderd"; + +/* profile update event chat item */ +"removed profile picture" = "profielfoto verwijderd"; + /* rcv group event chat item */ "removed you" = "heeft je verwijderd"; @@ -2828,26 +4200,83 @@ /* No comment provided by engineer. */ "Repeat connection request?" = "Verbindingsverzoek herhalen?"; +/* No comment provided by engineer. */ +"Repeat download" = "Herhaal het downloaden"; + +/* No comment provided by engineer. */ +"Repeat import" = "Herhaal import"; + /* No comment provided by engineer. */ "Repeat join request?" = "Deelnameverzoek herhalen?"; +/* No comment provided by engineer. */ +"Repeat upload" = "Herhaal het uploaden"; + /* chat item action */ "Reply" = "Antwoord"; +/* chat item action */ +"Report" = "rapporteren"; + +/* report reason */ +"Report content: only group moderators will see it." = "Inhoud melden: alleen groepsmoderators kunnen dit zien."; + +/* report reason */ +"Report member profile: only group moderators will see it." = "Rapporteer ledenprofiel: alleen groepsmoderators kunnen dit zien."; + +/* report reason */ +"Report other: only group moderators will see it." = "Anders melden: alleen groepsmoderators kunnen het zien."; + +/* No comment provided by engineer. */ +"Report reason?" = "Reden melding?"; + +/* report reason */ +"Report spam: only group moderators will see it." = "Spam melden: alleen groepsmoderators kunnen het zien."; + +/* report reason */ +"Report violation: only group moderators will see it." = "Rapporteer overtreding: alleen groepsmoderators kunnen dit zien."; + +/* report in notification */ +"Report: %@" = "rapporteer: %@"; + +/* No comment provided by engineer. */ +"Reporting messages to moderators is prohibited." = "Het is niet toegestaan om berichten aan moderators te melden."; + +/* No comment provided by engineer. */ +"Reports" = "Rapporten"; + +/* chat list item title */ +"requested to connect" = "verzocht om verbinding te maken"; + /* No comment provided by engineer. */ "Required" = "Vereist"; /* No comment provided by engineer. */ "Reset" = "Resetten"; +/* No comment provided by engineer. */ +"Reset all hints" = "Alle hints resetten"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Reset alle statistieken"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Alle statistieken resetten?"; + /* No comment provided by engineer. */ "Reset colors" = "Kleuren resetten"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Terugzetten naar app thema"; + /* No comment provided by engineer. */ "Reset to defaults" = "Resetten naar standaardwaarden"; /* No comment provided by engineer. */ -"Restart the app to create a new chat profile" = "Start de app opnieuw om een nieuw chat profiel aan te maken"; +"Reset to user theme" = "Terugzetten naar gebruikersthema"; + +/* No comment provided by engineer. */ +"Restart the app to create a new chat profile" = "Start de app opnieuw om een nieuw chatprofiel aan te maken"; /* No comment provided by engineer. */ "Restart the app to use imported chat database" = "Start de app opnieuw om de geïmporteerde chat database te gebruiken"; @@ -2864,11 +4293,14 @@ /* No comment provided by engineer. */ "Restore database error" = "Database fout herstellen"; +/* No comment provided by engineer. */ +"Retry" = "Opnieuw proberen"; + /* chat item action */ "Reveal" = "Onthullen"; /* No comment provided by engineer. */ -"Revert" = "Terugdraaien"; +"Review conditions" = "Voorwaarden bekijken"; /* No comment provided by engineer. */ "Revoke" = "Intrekken"; @@ -2885,57 +4317,88 @@ /* No comment provided by engineer. */ "Run chat" = "Chat uitvoeren"; -/* chat item action */ +/* No comment provided by engineer. */ +"Safely receive files" = "Veilig bestanden ontvangen"; + +/* No comment provided by engineer. */ +"Safer groups" = "Veiligere groepen"; + +/* alert button +chat item action */ "Save" = "Opslaan"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Bewaar (en informeer contacten)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Opslaan en Contact melden"; /* No comment provided by engineer. */ "Save and notify group members" = "Opslaan en groep leden melden"; +/* No comment provided by engineer. */ +"Save and reconnect" = "Opslaan en opnieuw verbinden"; + /* No comment provided by engineer. */ "Save and update group profile" = "Groep profiel opslaan en bijwerken"; -/* No comment provided by engineer. */ -"Save archive" = "Bewaar archief"; - -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Sla instellingen voor automatisch accepteren op"; - /* No comment provided by engineer. */ "Save group profile" = "Groep profiel opslaan"; /* No comment provided by engineer. */ -"Save passphrase and open chat" = "Bewaar het wachtwoord en open je gesprekken"; +"Save list" = "Lijst opslaan"; + +/* No comment provided by engineer. */ +"Save passphrase and open chat" = "Wachtwoord opslaan en open je chats"; /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Sla het wachtwoord op in de Keychain"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Voorkeuren opslaan?"; /* No comment provided by engineer. */ "Save profile password" = "Bewaar profiel wachtwoord"; /* No comment provided by engineer. */ -"Save servers" = "Bewaar servers"; +"Save servers" = "Servers opslaan"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Servers opslaan?"; /* No comment provided by engineer. */ -"Save settings?" = "Instellingen opslaan?"; +"Save welcome message?" = "Welkom bericht opslaan?"; + +/* alert title */ +"Save your profile?" = "Uw profiel opslaan?"; /* No comment provided by engineer. */ -"Save welcome message?" = "Welkomst bericht opslaan?"; +"saved" = "opgeslagen"; + +/* No comment provided by engineer. */ +"Saved" = "Opgeslagen"; + +/* No comment provided by engineer. */ +"Saved from" = "Opgeslagen van"; + +/* No comment provided by engineer. */ +"saved from %@" = "opgeslagen van %@"; + +/* message info title */ +"Saved message" = "Opgeslagen bericht"; /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Opgeslagen WebRTC ICE servers worden verwijderd"; +/* No comment provided by engineer. */ +"Saving %lld messages" = "%lld berichten opslaan"; + +/* No comment provided by engineer. */ +"Scale" = "Schaal"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Link scannen/plakken"; + /* No comment provided by engineer. */ "Scan code" = "Code scannen"; @@ -2951,12 +4414,24 @@ /* No comment provided by engineer. */ "Scan server QR code" = "Scan server QR-code"; +/* No comment provided by engineer. */ +"search" = "zoekopdracht"; + /* No comment provided by engineer. */ "Search" = "Zoeken"; +/* No comment provided by engineer. */ +"Search bar accepts invitation links." = "Zoekbalk accepteert uitnodigingslinks."; + +/* No comment provided by engineer. */ +"Search or paste SimpleX link" = "Zoeken of plak een SimpleX link"; + /* network option */ "sec" = "sec"; +/* No comment provided by engineer. */ +"Secondary" = "Secundair"; + /* time unit */ "seconds" = "seconden"; @@ -2966,6 +4441,9 @@ /* server test step */ "Secure queue" = "Veilige wachtrij"; +/* No comment provided by engineer. */ +"Secured" = "Beveiligd"; + /* No comment provided by engineer. */ "Security assessment" = "Beveiligingsbeoordeling"; @@ -2975,9 +4453,18 @@ /* chat item text */ "security code changed" = "beveiligingscode gewijzigd"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Selecteer"; +/* No comment provided by engineer. */ +"Select chat profile" = "Selecteer chatprofiel"; + +/* No comment provided by engineer. */ +"Selected %lld" = "%lld geselecteerd"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Geselecteerde chat voorkeuren verbieden dit bericht."; + /* No comment provided by engineer. */ "Self-destruct" = "Zelfvernietiging"; @@ -3002,26 +4489,35 @@ /* No comment provided by engineer. */ "send direct message" = "stuur een direct bericht"; -/* No comment provided by engineer. */ -"Send direct message" = "Direct bericht sturen"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Stuur een direct bericht om verbinding te maken"; /* No comment provided by engineer. */ "Send disappearing message" = "Stuur een verdwijnend bericht"; +/* No comment provided by engineer. */ +"Send errors" = "Verzend fouten"; + /* No comment provided by engineer. */ "Send link previews" = "Link voorbeelden verzenden"; /* No comment provided by engineer. */ "Send live message" = "Stuur een livebericht"; +/* No comment provided by engineer. */ +"Send message to enable calls." = "Stuur een bericht om oproepen mogelijk te maken."; + +/* No comment provided by engineer. */ +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Stuur berichten rechtstreeks als het IP-adres beschermd is en uw of bestemmingsserver geen privéroutering ondersteunt."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Stuur berichten rechtstreeks wanneer uw of de doelserver geen privéroutering ondersteunt."; + /* No comment provided by engineer. */ "Send notifications" = "Meldingen verzenden"; /* No comment provided by engineer. */ -"Send notifications:" = "Meldingen verzenden:"; +"Send private reports" = "Rapporteer privé"; /* No comment provided by engineer. */ "Send questions and ideas" = "Stuur vragen en ideeën"; @@ -3033,6 +4529,9 @@ "Send them from gallery or custom keyboards." = "Stuur ze vanuit de galerij of aangepaste toetsenborden."; /* No comment provided by engineer. */ +"Send up to 100 last messages to new members." = "Stuur tot 100 laatste berichten naar nieuwe leden."; + +/* alert message */ "Sender cancelled file transfer." = "Afzender heeft bestandsoverdracht geannuleerd."; /* No comment provided by engineer. */ @@ -3068,15 +4567,57 @@ /* copied message info */ "Sent at: %@" = "Verzonden op: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Direct verzonden"; + /* notification */ "Sent file event" = "Verzonden bestandsgebeurtenis"; /* message info title */ "Sent message" = "Verzonden bericht"; +/* No comment provided by engineer. */ +"Sent messages" = "Verzonden berichten"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Verzonden berichten worden na ingestelde tijd verwijderd."; +/* No comment provided by engineer. */ +"Sent reply" = "Antwoord verzonden"; + +/* No comment provided by engineer. */ +"Sent total" = "Totaal verzonden"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Verzonden via proxy"; + +/* No comment provided by engineer. */ +"Server" = "Server"; + +/* alert message */ +"Server added to operator %@." = "Server toegevoegd aan operator %@."; + +/* No comment provided by engineer. */ +"Server address" = "Server adres"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "Serveradres is incompatibel met netwerkinstellingen: %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "Serveradres is niet compatibel met netwerkinstellingen."; + +/* alert title */ +"Server operator changed." = "Serveroperator gewijzigd."; + +/* No comment provided by engineer. */ +"Server operators" = "Serverbeheerders"; + +/* alert title */ +"Server protocol changed." = "Serverprotocol gewijzigd."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "informatie over serverwachtrij: %1$@\n\nlaatst ontvangen bericht: %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "Server vereist autorisatie om wachtrijen te maken, controleer wachtwoord"; @@ -3086,27 +4627,60 @@ /* No comment provided by engineer. */ "Server test failed!" = "Servertest mislukt!"; +/* No comment provided by engineer. */ +"Server type" = "Server type"; + +/* srv error text */ +"Server version is incompatible with network settings." = "Serverversie is incompatibel met netwerkinstellingen."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "Serverversie is incompatibel met uw app: %@."; + /* No comment provided by engineer. */ "Servers" = "Servers"; +/* No comment provided by engineer. */ +"Servers info" = "Server informatie"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Serverstatistieken worden gereset - dit kan niet ongedaan worden gemaakt!"; + /* No comment provided by engineer. */ "Session code" = "Sessie code"; /* No comment provided by engineer. */ "Set 1 day" = "Stel 1 dag in"; +/* No comment provided by engineer. */ +"Set chat name…" = "Stel chatnaam in…"; + /* No comment provided by engineer. */ "Set contact name…" = "Contactnaam instellen…"; +/* No comment provided by engineer. */ +"Set default theme" = "Stel het standaard thema in"; + /* No comment provided by engineer. */ "Set group preferences" = "Groep voorkeuren instellen"; /* No comment provided by engineer. */ "Set it instead of system authentication." = "Stel het in in plaats van systeemverificatie."; +/* No comment provided by engineer. */ +"Set message expiration in chats." = "Stel de berichtvervaldatum in chats in."; + +/* profile update event chat item */ +"set new contact address" = "nieuw contactadres instellen"; + +/* profile update event chat item */ +"set new profile picture" = "nieuwe profielfoto"; + /* No comment provided by engineer. */ "Set passcode" = "Toegangscode instellen"; +/* No comment provided by engineer. */ +"Set passphrase" = "Wachtwoord instellen"; + /* No comment provided by engineer. */ "Set passphrase to export" = "Wachtwoord instellen om te exporteren"; @@ -3119,27 +4693,55 @@ /* No comment provided by engineer. */ "Settings" = "Instellingen"; -/* chat item action */ +/* alert message */ +"Settings were changed." = "Instellingen zijn gewijzigd."; + +/* No comment provided by engineer. */ +"Shape profile images" = "Vorm profiel afbeeldingen"; + +/* alert action +chat item action */ "Share" = "Deel"; /* No comment provided by engineer. */ "Share 1-time link" = "Eenmalige link delen"; +/* No comment provided by engineer. */ +"Share 1-time link with a friend" = "Deel eenmalig een link met een vriend"; + /* No comment provided by engineer. */ "Share address" = "Adres delen"; /* 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."; + /* No comment provided by engineer. */ "Share link" = "Deel link"; /* No comment provided by engineer. */ -"Share one-time invitation link" = "Eenmalige uitnodiging link delen"; +"Share profile" = "Profiel delen"; + +/* No comment provided by engineer. */ +"Share SimpleX address on social media." = "Deel het SimpleX-adres op sociale media."; + +/* No comment provided by engineer. */ +"Share this 1-time invite link" = "Deel deze eenmalige uitnodigingslink"; + +/* 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. */ +"Show → on messages sent via private routing." = "Toon → bij berichten verzonden via privéroutering."; + /* No comment provided by engineer. */ "Show calls in phone history" = "Toon oproepen in de telefoongeschiedenis"; @@ -3149,18 +4751,39 @@ /* No comment provided by engineer. */ "Show last messages" = "Laat laatste berichten zien"; +/* No comment provided by engineer. */ +"Show message status" = "Toon berichtstatus"; + +/* No comment provided by engineer. */ +"Show percentage" = "Percentage weergeven"; + /* No comment provided by engineer. */ "Show preview" = "Toon voorbeeld"; +/* No comment provided by engineer. */ +"Show QR code" = "Toon QR-code"; + /* No comment provided by engineer. */ "Show:" = "Toon:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "SimpleX adres"; /* No comment provided by engineer. */ "SimpleX Address" = "SimpleX adres"; +/* No comment provided by engineer. */ +"SimpleX address and 1-time links are safe to share via any messenger." = "SimpleX-adressen en eenmalige links kunnen veilig worden gedeeld via elke messenger."; + +/* No comment provided by engineer. */ +"SimpleX address or 1-time link?" = "SimpleX adres of eenmalige link?"; + +/* No comment provided by engineer. */ +"SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "Simplex-chat en flux hebben een overeenkomst gemaakt om door flux geëxploiteerde servers in de app op te nemen."; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "De beveiliging van SimpleX Chat is gecontroleerd door Trail of Bits."; @@ -3173,9 +4796,15 @@ /* simplex link type */ "SimpleX group link" = "SimpleX groep link"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "SimpleX links"; +/* No comment provided by engineer. */ +"SimpleX links are prohibited." = "SimpleX-links zijn niet toegestaan."; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "SimpleX-links zijn niet toegestaan"; + /* No comment provided by engineer. */ "SimpleX Lock" = "SimpleX Vergrendelen"; @@ -3191,9 +4820,15 @@ /* simplex link type */ "SimpleX one-time invitation" = "Eenmalige SimpleX uitnodiging"; +/* No comment provided by engineer. */ +"SimpleX protocols reviewed by Trail of Bits." = "SimpleX-protocollen beoordeeld door Trail of Bits."; + /* No comment provided by engineer. */ "Simplified incognito mode" = "Vereenvoudigde incognitomodus"; +/* No comment provided by engineer. */ +"Size" = "Maat"; + /* No comment provided by engineer. */ "Skip" = "Overslaan"; @@ -3204,31 +4839,65 @@ "Small groups (max 20)" = "Kleine groepen (max 20)"; /* No comment provided by engineer. */ -"SMP servers" = "SMP servers"; +"SMP server" = "SMP server"; + +/* No comment provided by engineer. */ +"SOCKS proxy" = "SOCKS proxy"; + +/* blur media */ +"Soft" = "Soft"; + +/* No comment provided by engineer. */ +"Some app settings were not migrated." = "Sommige app-instellingen zijn niet gemigreerd."; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "Sommige bestanden zijn niet geëxporteerd:"; /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren - u kunt de Chat console raadplegen voor meer details."; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren:"; + +/* alert message */ +"Some servers failed the test:\n%@" = "Sommige servers zijn niet geslaagd voor de test:\n%@"; + /* notification title */ "Somebody" = "Iemand"; +/* blocking reason +report reason */ +"Spam" = "Spam"; + /* No comment provided by engineer. */ -"Start a new chat" = "Begin een nieuw gesprek"; +"Square, circle, or anything in between." = "Vierkant, cirkel of iets daartussenin."; + +/* chat item text */ +"standard end-to-end encryption" = "standaard end-to-end encryptie"; /* No comment provided by engineer. */ "Start chat" = "Begin gesprek"; +/* No comment provided by engineer. */ +"Start chat?" = "Begin chat?"; + /* No comment provided by engineer. */ "Start migration" = "Start migratie"; +/* No comment provided by engineer. */ +"Starting from %@." = "Beginnend vanaf %@."; + /* No comment provided by engineer. */ "starting…" = "beginnen…"; +/* No comment provided by engineer. */ +"Statistics" = "Statistieken"; + /* No comment provided by engineer. */ "Stop" = "Stop"; /* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Stop de chat om database acties mogelijk te maken"; +"Stop chat" = "Stop chat"; /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Stop de chat om de chat database te exporteren, importeren of verwijderen. U kunt geen berichten ontvangen en verzenden terwijl de chat is gestopt."; @@ -3245,30 +4914,57 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Bestand verzenden stoppen?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Stop met delen"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Stop met het delen van adres?"; /* authentication reason */ "Stop SimpleX" = "Stop SimpleX"; /* No comment provided by engineer. */ -"strike" = "staking"; +"Stopping chat" = "Chat stoppen"; /* No comment provided by engineer. */ -"Submit" = "Indienen"; +"Storage" = "Opslag"; + +/* No comment provided by engineer. */ +"strike" = "staking"; + +/* blur media */ +"Strong" = "Krachtig"; + +/* No comment provided by engineer. */ +"Submit" = "Bevestigen"; + +/* No comment provided by engineer. */ +"Subscribed" = "Subscribed"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Subscription fouten"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Subscriptions genegeerd"; /* No comment provided by engineer. */ "Support SimpleX Chat" = "Ondersteuning van SimpleX Chat"; +/* No comment provided by engineer. */ +"Switch audio and video during the call." = "Wisselen tussen audio en video tijdens het gesprek."; + +/* No comment provided by engineer. */ +"Switch chat profile for 1-time invitations." = "Wijzig chatprofiel voor eenmalige uitnodigingen."; + /* No comment provided by engineer. */ "System" = "Systeem"; /* No comment provided by engineer. */ "System authentication" = "Systeem authenticatie"; +/* No comment provided by engineer. */ +"Tail" = "Staart"; + /* No comment provided by engineer. */ "Take picture" = "Foto nemen"; @@ -3276,23 +4972,35 @@ "Tap button " = "Tik op de knop "; /* No comment provided by engineer. */ -"Tap to activate profile." = "Tik om profiel te activeren."; +"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 Connect" = "Tik om verbinding te maken"; +"Tap to activate profile." = "Tik hier om profiel te activeren."; /* No comment provided by engineer. */ -"Tap to join" = "Tik om lid te worden"; +"Tap to Connect" = "Tik hier om verbinding te maken"; /* No comment provided by engineer. */ -"Tap to join incognito" = "Tik om incognito lid te worden"; +"Tap to join" = "Tik hier om lid te worden"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Tik om een nieuw gesprek te starten"; +"Tap to join incognito" = "Tik hier om incognito lid te worden"; + +/* No comment provided by engineer. */ +"Tap to paste link" = "Tik hier om de link te plakken"; + +/* No comment provided by engineer. */ +"Tap to scan" = "Tik hier om te scannen"; + +/* No comment provided by engineer. */ +"TCP connection" = "TCP verbinding"; /* No comment provided by engineer. */ "TCP connection timeout" = "Timeout van TCP-verbinding"; +/* No comment provided by engineer. */ +"TCP port for messaging" = "TCP-poort voor berichtenuitwisseling"; + /* No comment provided by engineer. */ "TCP_KEEPCNT" = "TCP_KEEPCNT"; @@ -3302,16 +5010,22 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* file error alert title */ +"Temporary file error" = "Tijdelijke bestandsfout"; + /* server test failure */ "Test failed at step %@." = "Test mislukt bij stap %@."; +/* No comment provided by engineer. */ +"Test notifications" = "Testmeldingen"; + /* No comment provided by engineer. */ "Test server" = "Server test"; /* No comment provided by engineer. */ "Test servers" = "Servers testen"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Testen mislukt!"; /* No comment provided by engineer. */ @@ -3323,15 +5037,24 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Dank aan de gebruikers – draag bij via Weblate!"; -/* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Het eerste platform zonder gebruikers-ID's, privé door ontwerp."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "De app kan u op de hoogte stellen wanneer u berichten of contact verzoeken ontvangt - open de instellingen om dit in te schakelen."; +/* No comment provided by engineer. */ +"The app protects your privacy by using different operators in each conversation." = "De app beschermt uw privacy door in elk gesprek andere operatoren te gebruiken."; + +/* 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."; + /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "De poging om het wachtwoord van de database te wijzigen is niet voltooid."; +/* No comment provided by engineer. */ +"The code you scanned is not a SimpleX link QR code." = "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, your contact may be offline." = "De verbinding heeft de limiet van niet-afgeleverde berichten bereikt. Uw contactpersoon is mogelijk offline."; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "De door u geaccepteerde verbinding wordt geannuleerd!"; @@ -3344,6 +5067,9 @@ /* 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."; @@ -3357,13 +5083,22 @@ "The message will be marked as moderated for all members." = "Het bericht wordt gemarkeerd als gemodereerd voor alle leden."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "De volgende generatie privéberichten"; +"The messages will be deleted for all members." = "De berichten worden voor alle leden verwijderd."; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "De berichten worden voor alle leden als gemodereerd gemarkeerd."; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "De oude database is niet verwijderd tijdens de migratie, deze kan worden verwijderd."; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "Het profiel wordt alleen gedeeld met uw contacten."; +"Your profile is stored on your device and only shared with your contacts." = "Het profiel wordt alleen gedeeld met uw contacten."; + +/* No comment provided by engineer. */ +"The same conditions will apply to operator **%@**." = "Dezelfde voorwaarden gelden voor operator **%@**."; + +/* No comment provided by engineer. */ +"The second preset operator in the app!" = "De tweede vooraf ingestelde operator in de app!"; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "De tweede vink die we gemist hebben! ✅"; @@ -3372,16 +5107,22 @@ "The sender will NOT be notified" = "De afzender wordt NIET op de hoogte gebracht"; /* No comment provided by engineer. */ -"The servers for new connections of your current chat profile **%@**." = "De servers voor nieuwe verbindingen van uw huidige chat profiel **%@**."; +"The servers for new connections of your current chat profile **%@**." = "De servers voor nieuwe verbindingen van uw huidige chatprofiel **%@**."; /* No comment provided by engineer. */ -"Theme" = "Thema"; +"The servers for new files of your current chat profile **%@**." = "De servers voor nieuwe bestanden van uw huidige chatprofiel **%@**."; /* No comment provided by engineer. */ -"There should be at least one user profile." = "Er moet ten minste één gebruikers profiel zijn."; +"The text you pasted is not a SimpleX link." = "De tekst die u hebt geplakt is geen SimpleX link."; /* No comment provided by engineer. */ -"There should be at least one visible user profile." = "Er moet ten minste één zichtbaar gebruikers profiel zijn."; +"The uploaded database archive will be permanently removed from the servers." = "Het geüploade databasearchief wordt permanent van de servers verwijderd."; + +/* No comment provided by engineer. */ +"Themes" = "Thema's"; + +/* No comment provided by engineer. */ +"These conditions will also apply for: **%@**." = "Deze voorwaarden zijn ook van toepassing op: **%@**."; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Deze instellingen zijn voor uw huidige profiel **%@**."; @@ -3395,14 +5136,26 @@ /* 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." = "Deze actie kan niet ongedaan worden gemaakt, de berichten die eerder zijn verzonden en ontvangen dan geselecteerd, worden verwijderd. Het kan enkele minuten duren."; +/* alert message */ +"This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." = "Deze actie kan niet ongedaan worden gemaakt. De berichten die eerder in deze chat zijn verzonden en ontvangen dan geselecteerd, worden verwijderd."; + /* No comment provided by engineer. */ -"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren."; +"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan definitief verloren."; + +/* E2EE info chat item */ +"This chat is protected by end-to-end encryption." = "Deze chat is beveiligd met end-to-end codering."; + +/* E2EE info chat item */ +"This chat is protected by quantum resistant end-to-end encryption." = "Deze chat wordt beschermd door quantum bestendige end-to-end codering."; /* notification title */ "this contact" = "dit contact"; /* No comment provided by engineer. */ -"This device name" = "Deze apparaatnaam"; +"This device name" = "Naam van dit apparaat"; + +/* No comment provided by engineer. */ +"This display name is invalid. Please choose another name." = "Deze weergavenaam is ongeldig. Kies een andere naam."; /* No comment provided by engineer. */ "This group has over %lld members, delivery receipts are not sent." = "Deze groep heeft meer dan %lld -leden, ontvangstbevestigingen worden niet verzonden."; @@ -3417,7 +5170,16 @@ "This is your own SimpleX address!" = "Dit is uw eigen SimpleX adres!"; /* No comment provided by engineer. */ -"This setting applies to messages in your current chat profile **%@**." = "Deze instelling is van toepassing op berichten in je huidige chat profiel **%@**."; +"This link was used with another mobile device, please create a new link on the desktop." = "Deze link is gebruikt met een ander mobiel apparaat. Maak een nieuwe link op de desktop."; + +/* No comment provided by engineer. */ +"This message was deleted or not received yet." = "Dit bericht is verwijderd of nog niet ontvangen."; + +/* No comment provided by engineer. */ +"This setting applies to messages in your current chat profile **%@**." = "Deze instelling is van toepassing op berichten in je huidige chatprofiel **%@**."; + +/* No comment provided by engineer. */ +"Title" = "Titel"; /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Om vragen te stellen en updates te ontvangen:"; @@ -3432,7 +5194,7 @@ "To make a new connection" = "Om een nieuwe verbinding te maken"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers-ID's die door alle andere platforms worden gebruikt, ID's voor berichten wachtrijen, afzonderlijk voor elk van uw contacten."; +"To protect against your link being replaced, you can compare contact security codes." = "Om te voorkomen dat uw link wordt vervangen, kunt u contactbeveiligingscodes vergelijken."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Om de tijdzone te beschermen, gebruiken afbeeldings-/spraakbestanden UTC."; @@ -3440,24 +5202,60 @@ /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Schakel SimpleX Vergrendelen om uw informatie te beschermen.\nU wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingeschakeld."; +/* No comment provided by engineer. */ +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "Om uw IP-adres te beschermen, gebruikt privéroutering uw SMP-servers om berichten te bezorgen."; + +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers-ID's die door alle andere platforms worden gebruikt, ID's voor berichten wachtrijen, afzonderlijk voor elk van uw contacten."; + +/* No comment provided by engineer. */ +"To receive" = "Om te ontvangen"; + +/* No comment provided by engineer. */ +"To record speech please grant permission to use Microphone." = "Geef toestemming om de microfoon te gebruiken om spraak op te nemen."; + +/* No comment provided by engineer. */ +"To record video please grant permission to use Camera." = "Om video op te nemen, dient u toestemming te geven om de camera te gebruiken."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Geef toestemming om de microfoon te gebruiken om een spraakbericht op te nemen."; /* No comment provided by engineer. */ -"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoek veld in op de pagina **Uw chat profielen**."; +"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoek veld in op de pagina **Uw chatprofielen**."; + +/* No comment provided by engineer. */ +"To send" = "Om te verzenden"; /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Om directe push meldingen te ondersteunen, moet de chat database worden gemigreerd."; +/* No comment provided by engineer. */ +"To use the servers of **%@**, accept conditions of use." = "Om de servers van **%@** te gebruiken, moet u de gebruiksvoorwaarden accepteren."; + /* 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."; +/* token status */ +"Token status: %@." = "Tokenstatus: %@."; + +/* No comment provided by engineer. */ +"Toolbar opacity" = "De transparantie van de werkbalk"; + +/* No comment provided by engineer. */ +"Total" = "Totaal"; + /* No comment provided by engineer. */ "Transport isolation" = "Transport isolation"; +/* No comment provided by engineer. */ +"Transport sessions" = "Transportsessies"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Proberen verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen (fout: %@)."; @@ -3465,10 +5263,10 @@ "Trying to connect to the server used to receive messages from this contact." = "Proberen verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen."; /* No comment provided by engineer. */ -"Turn off" = "Uitschakelen"; +"Turkish interface" = "Turkse interface"; /* No comment provided by engineer. */ -"Turn off notifications?" = "Schakel meldingen uit?"; +"Turn off" = "Uitschakelen"; /* No comment provided by engineer. */ "Turn on" = "Zet aan"; @@ -3479,26 +5277,35 @@ /* No comment provided by engineer. */ "Unblock" = "Deblokkeren"; +/* No comment provided by engineer. */ +"Unblock for all" = "Deblokkeer voor iedereen"; + /* No comment provided by engineer. */ "Unblock member" = "Lid deblokkeren"; +/* No comment provided by engineer. */ +"Unblock member for all?" = "Lid voor iedereen deblokkeren?"; + /* No comment provided by engineer. */ "Unblock member?" = "Lid deblokkeren?"; -/* item status description */ -"Unexpected error: %@" = "Onverwachte fout: %@"; +/* rcv group event chat item */ +"unblocked %@" = "gedeblokkeerd %@"; + +/* No comment provided by engineer. */ +"Undelivered messages" = "Niet afgeleverde berichten"; /* No comment provided by engineer. */ "Unexpected migration state" = "Onverwachte migratiestatus"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Niet fav."; /* No comment provided by engineer. */ "Unhide" = "zichtbaar maken"; /* No comment provided by engineer. */ -"Unhide chat profile" = "Chat profiel zichtbaar maken"; +"Unhide chat profile" = "Chatprofiel zichtbaar maken"; /* No comment provided by engineer. */ "Unhide profile" = "Profiel zichtbaar maken"; @@ -3518,6 +5325,15 @@ /* No comment provided by engineer. */ "Unknown error" = "Onbekende fout"; +/* No comment provided by engineer. */ +"unknown servers" = "onbekende relays"; + +/* alert title */ +"Unknown servers!" = "Onbekende servers!"; + +/* No comment provided by engineer. */ +"unknown status" = "onbekende status"; + /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Schakel de modus Niet storen in om onderbrekingen te voorkomen, tenzij u de iOS-oproepinterface gebruikt."; @@ -3536,17 +5352,20 @@ /* authentication reason */ "Unlock app" = "Ontgrendel app"; -/* No comment provided by engineer. */ +/* notification label action */ "Unmute" = "Dempen opheffen"; /* No comment provided by engineer. */ +"unprotected" = "onbeschermd"; + +/* swipe action */ "Unread" = "Ongelezen"; /* No comment provided by engineer. */ -"Update" = "Update"; +"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. */ -"Update .onion hosts setting?" = ".onion hosts-instelling updaten?"; +"Update" = "Update"; /* No comment provided by engineer. */ "Update database passphrase" = "Database wachtwoord bijwerken"; @@ -3555,32 +5374,59 @@ "Update network settings?" = "Netwerk instellingen bijwerken?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Transportisolatiemodus updaten?"; +"Update settings?" = "Instellingen actualiseren?"; + +/* No comment provided by engineer. */ +"Updated conditions" = "Bijgewerkte voorwaarden"; /* rcv group event chat item */ "updated group profile" = "bijgewerkt groep profiel"; +/* profile update event chat item */ +"updated profile" = "bijgewerkt profiel"; + /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "Door de instellingen bij te werken, wordt de client opnieuw verbonden met alle servers."; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Als u deze instelling bijwerkt, wordt de client opnieuw verbonden met alle servers."; +"Upgrade and open chat" = "Upgrade en open chat"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "Upgrade en open chat"; +"Upload errors" = "Upload fouten"; + +/* No comment provided by engineer. */ +"Upload failed" = "Upload mislukt"; /* server test step */ "Upload file" = "Upload bestand"; +/* No comment provided by engineer. */ +"Uploaded" = "Geüpload"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Geüploade bestanden"; + +/* No comment provided by engineer. */ +"Uploading archive" = "Archief uploaden"; + /* No comment provided by engineer. */ "Use .onion hosts" = "Gebruik .onion-hosts"; +/* No comment provided by engineer. */ +"Use %@" = "Gebruik %@"; + /* No comment provided by engineer. */ "Use chat" = "Gebruik chat"; /* No comment provided by engineer. */ "Use current profile" = "Gebruik het huidige profiel"; +/* No comment provided by engineer. */ +"Use for files" = "Gebruik voor bestanden"; + +/* No comment provided by engineer. */ +"Use for messages" = "Gebruik voor berichten"; + /* No comment provided by engineer. */ "Use for new connections" = "Gebruik voor nieuwe verbindingen"; @@ -3593,20 +5439,47 @@ /* No comment provided by engineer. */ "Use new incognito profile" = "Gebruik een nieuw incognitoprofiel"; +/* No comment provided by engineer. */ +"Use only local notifications?" = "Alleen lokale meldingen gebruiken?"; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "Gebruik privéroutering met onbekende servers wanneer het IP-adres niet beveiligd is."; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "Gebruik privéroutering met onbekende servers."; + /* No comment provided by engineer. */ "Use server" = "Gebruik server"; +/* No comment provided by engineer. */ +"Use servers" = "Gebruik servers"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "SimpleX Chat servers gebruiken?"; /* No comment provided by engineer. */ -"User profile" = "Gebruikers profiel"; +"Use SOCKS proxy" = "Gebruik SOCKS proxy"; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Het gebruik van .onion-hosts vereist een compatibele VPN-provider."; +"Use TCP port %@ when no port is specified." = "Gebruik TCP-poort %@ als er geen poort is opgegeven."; /* No comment provided by engineer. */ -"Using SimpleX Chat servers." = "SimpleX Chat servers gebruiken."; +"Use the app while in the call." = "Gebruik de app tijdens het gesprek."; + +/* No comment provided by engineer. */ +"Use the app with one hand." = "Gebruik de app met één hand."; + +/* No comment provided by engineer. */ +"Use web port" = "Gebruik een webpoort"; + +/* No comment provided by engineer. */ +"User selection" = "Gebruikersselectie"; + +/* No comment provided by engineer. */ +"Username" = "Gebruikersnaam"; + +/* No comment provided by engineer. */ +"Using SimpleX Chat servers." = "Gebruik SimpleX Chat servers."; /* No comment provided by engineer. */ "v%@" = "v%@"; @@ -3626,6 +5499,12 @@ /* No comment provided by engineer. */ "Verify connections" = "Controleer verbindingen"; +/* No comment provided by engineer. */ +"Verify database passphrase" = "Controleer het wachtwoord van de database"; + +/* No comment provided by engineer. */ +"Verify passphrase" = "Controleer het wachtwoord"; + /* No comment provided by engineer. */ "Verify security code" = "Controleer de beveiligingscode"; @@ -3633,7 +5512,7 @@ "Via browser" = "Via browser"; /* chat list item description */ -"via contact address link" = "via contactadres link"; +"via contact address link" = "via contact adres link"; /* chat list item description */ "via group link" = "via groep link"; @@ -3642,11 +5521,14 @@ "via one-time link" = "via een eenmalige link"; /* No comment provided by engineer. */ -"via relay" = "via relais"; +"via relay" = "via relay"; /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Via een beveiligd kwantumbestendig protocol."; +/* No comment provided by engineer. */ +"video" = "video"; + /* No comment provided by engineer. */ "Video call" = "video oproep"; @@ -3662,9 +5544,18 @@ /* No comment provided by engineer. */ "Videos and files up to 1gb" = "Video's en bestanden tot 1 GB"; +/* No comment provided by engineer. */ +"View conditions" = "Bekijk voorwaarden"; + /* No comment provided by engineer. */ "View security code" = "Beveiligingscode bekijken"; +/* No comment provided by engineer. */ +"View updated conditions" = "Bekijk de bijgewerkte voorwaarden"; + +/* chat feature */ +"Visible history" = "Zichtbare geschiedenis"; + /* No comment provided by engineer. */ "Voice message…" = "Spraakbericht…"; @@ -3672,13 +5563,16 @@ "Voice messages" = "Spraak berichten"; /* No comment provided by engineer. */ -"Voice messages are prohibited in this chat." = "Spraak berichten zijn verboden in deze chat."; +"Voice messages are prohibited in this chat." = "Spraak berichten zijn niet toegestaan in dit gesprek."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Spraak berichten zijn verboden in deze groep."; +"Voice messages are prohibited." = "Spraak berichten zijn niet toegestaan."; /* No comment provided by engineer. */ -"Voice messages prohibited!" = "Spraak berichten verboden!"; +"Voice messages not allowed" = "Spraakberichten niet toegestaan"; + +/* No comment provided by engineer. */ +"Voice messages prohibited!" = "Spraak berichten niet toegestaan!"; /* No comment provided by engineer. */ "waiting for answer…" = "wachten op antwoord…"; @@ -3698,9 +5592,18 @@ /* No comment provided by engineer. */ "Waiting for video" = "Wachten op video"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Achtergrond accent"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Wallpaper achtergrond"; + /* No comment provided by engineer. */ "wants to connect to you!" = "wil met je in contact komen!"; +/* No comment provided by engineer. */ +"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "Waarschuwing: het starten van de chat op meerdere apparaten wordt niet ondersteund en zal leiden tot mislukte bezorging van berichten"; + /* No comment provided by engineer. */ "Warning: you may lose some data!" = "Waarschuwing: u kunt sommige gegevens verliezen!"; @@ -3714,7 +5617,10 @@ "Welcome %@!" = "Welkom %@!"; /* No comment provided by engineer. */ -"Welcome message" = "Welkomst bericht"; +"Welcome message" = "Welkom bericht"; + +/* No comment provided by engineer. */ +"Welcome message is too long" = "Welkom bericht is te lang"; /* No comment provided by engineer. */ "What's new" = "Wat is er nieuw"; @@ -3723,28 +5629,64 @@ "When available" = "Wanneer beschikbaar"; /* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Wanneer mensen vragen om verbinding te maken, kunt u dit accepteren of weigeren."; +"When connecting audio and video calls." = "Bij het verbinden van audio- en video-oproepen."; + +/* No comment provided by engineer. */ +"when IP hidden" = "wanneer IP verborgen is"; + +/* No comment provided by engineer. */ +"When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "Wanneer er meer dan één operator is ingeschakeld, beschikt geen enkele operator over metagegevens om te achterhalen wie met wie communiceert."; /* 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." = "Wanneer je een incognito profiel met iemand deelt, wordt dit profiel gebruikt voor de groepen waarvoor ze je uitnodigen."; /* No comment provided by engineer. */ -"With optional welcome message." = "Met optioneel welkomst bericht."; +"WiFi" = "Wifi"; + +/* No comment provided by engineer. */ +"Will be enabled in direct chats!" = "Wordt ingeschakeld in directe chats!"; + +/* No comment provided by engineer. */ +"Wired ethernet" = "Bekabeld Ethernet"; + +/* No comment provided by engineer. */ +"With encrypted files and media." = "‐Met versleutelde bestanden en media."; + +/* No comment provided by engineer. */ +"With optional welcome message." = "Met optioneel welkom bericht."; + +/* No comment provided by engineer. */ +"With reduced battery usage." = "Met verminderd batterijgebruik."; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "Zonder Tor of VPN is uw IP-adres zichtbaar voor bestandsservers."; + +/* alert message */ +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Zonder Tor of VPN zal uw IP-adres zichtbaar zijn voor deze XFTP-relays: %@."; /* No comment provided by engineer. */ "Wrong database passphrase" = "Verkeerd wachtwoord voor de database"; +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "Verkeerde sleutel of onbekende verbinding - hoogstwaarschijnlijk is deze verbinding verwijderd."; + +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Verkeerde sleutel of onbekend bestanddeeladres - hoogstwaarschijnlijk is het bestand verwijderd."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Verkeerd wachtwoord!"; /* No comment provided by engineer. */ -"XFTP servers" = "XFTP servers"; +"XFTP server" = "XFTP server"; /* pref value */ "yes" = "Ja"; /* No comment provided by engineer. */ -"You" = "Jij"; +"you" = "jij"; + +/* No comment provided by engineer. */ +"You **must not** use the same database on two devices." = "U **mag** niet dezelfde database op twee apparaten gebruiken."; /* No comment provided by engineer. */ "You accepted connection" = "Je hebt de verbinding geaccepteerd"; @@ -3753,11 +5695,14 @@ "You allow" = "Jij staat toe"; /* No comment provided by engineer. */ -"You already have a chat profile with the same display name. Please choose another name." = "Je hebt al een chat profiel met dezelfde weergave naam. Kies een andere naam."; +"You already have a chat profile with the same display name. Please choose another name." = "Je hebt al een chatprofiel met dezelfde weergave naam. Kies een andere naam."; /* No comment provided by engineer. */ "You are already connected to %@." = "U bent al verbonden met %@."; +/* No comment provided by engineer. */ +"You are already connected with %@." = "U bent al verbonden met %@."; + /* No comment provided by engineer. */ "You are already connecting to %@." = "U maakt al verbinding met %@."; @@ -3789,13 +5734,22 @@ "You are invited to group" = "Je bent uitgenodigd voor de groep"; /* No comment provided by engineer. */ -"you are observer" = "jij bent waarnemer"; +"You are not connected to these servers. Private routing is used to deliver messages to them." = "U bent niet verbonden met deze servers. Privéroutering wordt gebruikt om berichten bij hen af te leveren."; + +/* No comment provided by engineer. */ +"you are observer" = "je bent waarnemer"; + +/* snd group event chat item */ +"you blocked %@" = "je hebt %@ geblokkeerd"; /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "U kunt oproepen van het vergrendelingsscherm accepteren, zonder apparaat- en app-verificatie."; /* No comment provided by engineer. */ -"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "U kunt ook verbinding maken door op de link te klikken. Als het in de browser wordt geopend, klikt u op de knop **Openen in mobiele app**."; +"You can change it in Appearance settings." = "U kunt dit wijzigen in de instellingen onder uiterlijk."; + +/* No comment provided by engineer. */ +"You can configure servers via settings." = "U kunt servers configureren via instellingen."; /* No comment provided by engineer. */ "You can create it later" = "U kan het later maken"; @@ -3806,11 +5760,23 @@ /* No comment provided by engineer. */ "You can enable them later via app Privacy & Security settings." = "U kunt ze later inschakelen via de privacy- en beveiligingsinstellingen van de app."; +/* No comment provided by engineer. */ +"You can give another try." = "Je kunt het nog een keer proberen."; + /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "U kunt een gebruikers profiel verbergen of dempen - veeg het naar rechts."; +/* No comment provided by engineer. */ +"You can make it visible to your SimpleX contacts via Settings." = "Je kunt het via Instellingen zichtbaar maken voor je SimpleX contacten."; + /* notification body */ -"You can now send messages to %@" = "Je kunt nu berichten sturen naar %@"; +"You can now chat with %@" = "Je kunt nu berichten sturen naar %@"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "U kunt berichten naar %@ sturen vanuit gearchiveerde contacten."; + +/* No comment provided by engineer. */ +"You can set connection name, to remember who the link was shared with." = "U kunt een verbindingsnaam instellen, zodat u kunt onthouden met wie de link is gedeeld."; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "U kunt een voorbeeld van een melding op het vergrendeld scherm instellen via instellingen."; @@ -3822,10 +5788,10 @@ "You can share this address with your contacts to let them connect with **%@**." = "U kunt dit adres delen met uw contacten om hen verbinding te laten maken met **%@**."; /* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "U kunt uw adres delen als een link of als een QR-code. Iedereen kan verbinding met u maken."; +"You can start chat via app Settings / Database or by restarting the app" = "U kunt de chat starten via app Instellingen / Database of door de app opnieuw op te starten"; /* No comment provided by engineer. */ -"You can start chat via app Settings / Database or by restarting the app" = "U kunt de chat starten via app Instellingen / Database of door de app opnieuw op te starten"; +"You can still view conversation with %@ in the list of chats." = "Je kunt het gesprek met %@ nog steeds bekijken in de lijst met chats."; /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Je kunt SimpleX Vergrendeling aanzetten via Instellingen."; @@ -3833,6 +5799,9 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "U kunt markdown gebruiken voor opmaak in berichten:"; +/* alert message */ +"You can view invitation link again in connection details." = "U kunt de uitnodigingslink opnieuw bekijken in de verbindingsdetails."; + /* No comment provided by engineer. */ "You can't send messages!" = "Je kunt geen berichten versturen!"; @@ -3849,10 +5818,10 @@ "you changed role of %@ to %@" = "je veranderde de rol van %1$@ in %2$@"; /* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "U bepaalt via welke server(s) de berichten **ontvangen**, uw contacten de servers die u gebruikt om ze berichten te sturen."; +"You could not be verified; please try again." = "U kon niet worden geverifieerd; probeer het opnieuw."; /* No comment provided by engineer. */ -"You could not be verified; please try again." = "U kon niet worden geverifieerd; probeer het opnieuw."; +"You decide who can connect." = "Jij bepaalt wie er verbinding mag maken."; /* No comment provided by engineer. */ "You have already requested connection via this address!" = "U heeft al een verbinding aangevraagd via dit adres!"; @@ -3860,9 +5829,6 @@ /* No comment provided by engineer. */ "You have already requested connection!\nRepeat connection request?" = "Je hebt al verbinding aangevraagd!\nVerbindingsverzoek herhalen?"; -/* No comment provided by engineer. */ -"You have no chats" = "Je hebt geen gesprekken"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "U moet elke keer dat de app start het wachtwoord invoeren, deze wordt niet op het apparaat opgeslagen."; @@ -3876,11 +5842,20 @@ "You joined this group. Connecting to inviting group member." = "Je bent lid geworden van deze groep. Verbinding maken met uitnodigend groepslid."; /* snd group event chat item */ -"you left" = "jij bent vertrokken"; +"you left" = "je bent vertrokken"; + +/* No comment provided by engineer. */ +"You may migrate the exported database." = "U kunt de geëxporteerde database migreren."; + +/* No comment provided by engineer. */ +"You may save the exported archive." = "U kunt het geëxporteerde archief opslaan."; /* 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." = "U mag ALLEEN de meest recente versie van uw chat database op één apparaat gebruiken, anders ontvangt u mogelijk geen berichten meer van sommige contacten."; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "U moet uw contactpersoon toestemming geven om te bellen, zodat hij/zij je kan bellen."; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "U moet uw contact toestemming geven om spraak berichten te verzenden om ze te kunnen verzenden."; @@ -3899,6 +5874,12 @@ /* chat list item description */ "you shared one-time link incognito" = "je hebt een eenmalige link incognito gedeeld"; +/* token info */ +"You should receive notifications." = "U zou meldingen moeten ontvangen."; + +/* snd group event chat item */ +"you unblocked %@" = "je hebt %@ gedeblokkeerd"; + /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "Je wordt verbonden met de groep wanneer het apparaat van de groep host online is, even geduld a.u.b. of controleer het later!"; @@ -3920,6 +5901,9 @@ /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "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 chat. Chat history will be preserved." = "U ontvangt geen berichten meer van deze chat. De chatgeschiedenis blijft bewaard."; + /* No comment provided by engineer. */ "You will stop receiving messages from this group. Chat history will be preserved." = "Je ontvangt geen berichten meer van deze groep. Je gesprek geschiedenis blijft behouden."; @@ -3935,9 +5919,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Je gebruikt een incognito profiel voor deze groep. Om te voorkomen dat je je hoofdprofiel deelt, is het niet toegestaan om contacten uit te nodigen"; -/* No comment provided by engineer. */ -"Your %@ servers" = "Uw %@ servers"; - /* No comment provided by engineer. */ "Your calls" = "Uw oproepen"; @@ -3947,11 +5928,14 @@ /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Uw chat database is niet versleuteld, stel een wachtwoord in om deze te versleutelen."; +/* alert title */ +"Your chat preferences" = "Uw chat voorkeuren"; + /* No comment provided by engineer. */ "Your chat profiles" = "Uw chat profielen"; /* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Uw contact moet online zijn om de verbinding te voltooien.\nU kunt deze verbinding verbreken en het contact verwijderen en later proberen met een nieuwe link."; +"Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Uw verbinding is verplaatst naar %@, maar er is een onverwachte fout opgetreden tijdens het omleiden naar het profiel."; /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Uw contact heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%@)."; @@ -3960,10 +5944,10 @@ "Your contacts can allow full message deletion." = "Uw contacten kunnen volledige verwijdering van berichten toestaan."; /* No comment provided by engineer. */ -"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "Uw contacten in SimpleX kunnen het zien. \nU kunt dit wijzigen in Instellingen."; +"Your contacts will remain connected." = "Uw contacten blijven verbonden."; /* No comment provided by engineer. */ -"Your contacts will remain connected." = "Uw contacten blijven verbonden."; +"Your credentials may be sent unencrypted." = "Uw inloggegevens worden mogelijk niet-versleuteld verzonden."; /* No comment provided by engineer. */ "Your current chat database will be DELETED and REPLACED with the imported one." = "Uw huidige chat database wordt VERWIJDERD en VERVANGEN door de geïmporteerde."; @@ -3987,7 +5971,10 @@ "Your profile **%@** will be shared." = "Uw profiel **%@** wordt gedeeld."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten.\nSimpleX servers kunnen uw profiel niet zien."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten. SimpleX servers kunnen uw profiel niet zien."; + +/* alert message */ +"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Je profiel is gewijzigd. Als je het opslaat, wordt het bijgewerkte profiel naar al je contacten verzonden."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Uw profiel, contacten en afgeleverde berichten worden op uw apparaat opgeslagen."; @@ -3996,10 +5983,10 @@ "Your random profile" = "Je willekeurige profiel"; /* No comment provided by engineer. */ -"Your server" = "Uw server"; +"Your server address" = "Uw server adres"; /* No comment provided by engineer. */ -"Your server address" = "Uw server adres"; +"Your servers" = "Uw servers"; /* No comment provided by engineer. */ "Your settings" = "Uw instellingen"; @@ -4007,9 +5994,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "Uw SimpleX adres"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Uw SMP servers"; - -/* No comment provided by engineer. */ -"Your XFTP servers" = "Uw XFTP servers"; - diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index e0f4c4dee0..31a9b87662 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -1,18 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (można skopiować)"; @@ -32,19 +17,7 @@ "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- wiadomości głosowe do 5 minut.\n- niestandardowy czas zniknięcia.\n- historia edycji."; /* No comment provided by engineer. */ -", " = ", "; - -/* No comment provided by engineer. */ -": " = ": "; - -/* No comment provided by engineer. */ -"!1 colored!" = "!1 kolorowy!"; - -/* No comment provided by engineer. */ -"." = "."; - -/* No comment provided by engineer. */ -"(" = "("; +"!1 colored!" = "!1 pokolorowany!"; /* No comment provided by engineer. */ "(new)" = "(nowy)"; @@ -52,9 +25,6 @@ /* No comment provided by engineer. */ "(this device v%@)" = "(to urządzenie v%@)"; -/* No comment provided by engineer. */ -")" = ")"; - /* No comment provided by engineer. */ "[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Przyczyń się](https://github.com/simplex-chat/simplex-chat#contribute)"; @@ -65,10 +35,10 @@ "[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. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Dodaj nowy kontakt**: aby stworzyć swój jednorazowy kod QR lub link dla kontaktu."; +"**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."; /* No comment provided by engineer. */ -"**Create link / QR code** for your contact to use." = "**Utwórz link / kod QR**, aby Twój kontakt mógł z niego skorzystać."; +"**Create group**: to create a new group." = "**Utwórz grupę**: aby utworzyć nową grupę."; /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "**szyfrowane e2e** połączenie audio"; @@ -77,25 +47,28 @@ "**e2e encrypted** video call" = "**szyfrowane e2e** połączenie wideo"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Bardziej prywatny**: sprawdzanie nowych wiadomości co 20 minut. Token urządzenia jest współdzielony z serwerem SimpleX Chat, ale nie informacje o liczbie kontaktów lub wiadomości."; +"**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." = "**Bardziej prywatny**: sprawdzanie nowych wiadomości odbywa się co 20 minut. Współdzielony z serwerem SimpleX Chat jest token urządzenia, lecz nie informacje o liczbie kontaktów lub wiadomości."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Najbardziej prywatny**: nie korzystaj z serwera powiadomień SimpleX Chat, sprawdzaj wiadomości okresowo w tle (zależy jak często korzystasz z aplikacji)."; +"**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." = "**Najbardziej prywatny**: nie korzystaj z serwera powiadomień SimpleX Chat, wiadomości sprawdzane są co jakiś czas w tle (zależne od tego jak często korzystasz z aplikacji)."; /* No comment provided by engineer. */ -"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Wklej otrzymany link** lub otwórz go w przeglądarce i dotknij **Otwórz w aplikacji mobilnej**."; +"**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "*Uwaga*: w celach bezpieczeństwa użycie tej samej bazy danych na dwóch różnych urządzeniach spowoduje brak możliwości odszyfrowywania wiadomości z Twoich połączeń."; /* No comment provided by engineer. */ -"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Uwaga**: NIE będziesz w stanie odzyskać lub zmienić hasła, jeśli je stracisz."; +"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Uwaga**: NIE będziesz w stanie odzyskać lub zmienić kodu dostępu, jeśli go stracisz."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Zalecane**: token urządzenia i powiadomienia są wysyłane do serwera powiadomień SimpleX Chat, ale nie treść wiadomości, rozmiar lub od kogo jest."; +"**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." = "**Zalecane**: do serwera powiadomień SimpleX Chat wysyłany jest token urządzenia i powiadomienia, lecz nie treść wiadomości, jej rozmiar lub od kogo ona jest."; /* No comment provided by engineer. */ -"**Scan QR code**: to connect to your contact in person or via video call." = "**Skanuj kod QR**: aby połączyć się z kontaktem osobiście lub za pomocą połączenia wideo."; +"**Scan / Paste link**: to connect via a link you received." = "**Zeskanuj / Wklej link**: aby połączyć się za pomocą otrzymanego linku."; /* No comment provided by engineer. */ -"**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Uwaga**: Natychmiastowe powiadomienia push wymagają hasła zapisanego w Keychain."; +"**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Uwaga**: Natychmiastowe powiadomienia push wymagają zapisania kodu dostępu w Keychain."; + +/* No comment provided by engineer. */ +"**Warning**: the archive will be removed." = "**Ostrzeżenie**: archiwum zostanie usunięte."; /* No comment provided by engineer. */ "*bold*" = "\\*pogrubiony*"; @@ -139,6 +112,9 @@ /* No comment provided by engineer. */ "%@ connected" = "%@ połączony"; +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ pobrane"; + /* notification title */ "%@ is connected!" = "%@ jest połączony!"; @@ -149,11 +125,20 @@ "%@ is verified" = "%@ jest zweryfikowany"; /* No comment provided by engineer. */ -"%@ servers" = "%@ serwery"; +"%@ server" = "%@ serwer"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ serwery/ów"; + +/* No comment provided by engineer. */ +"%@ uploaded" = "%@ wgrane"; /* notification title */ "%@ wants to connect!" = "%@ chce się połączyć!"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + /* No comment provided by engineer. */ "%@, %@ and %lld members" = "%@, %@ i %lld członków"; @@ -166,9 +151,24 @@ /* time interval */ "%d days" = "%d dni"; +/* forward confirmation reason */ +"%d file(s) are still being downloaded." = "%d plik(ów) jest dalej pobieranych."; + +/* forward confirmation reason */ +"%d file(s) failed to download." = "%d plik(ów) nie udało się pobrać."; + +/* forward confirmation reason */ +"%d file(s) were deleted." = "%d plik(ów) zostało usuniętych."; + +/* forward confirmation reason */ +"%d file(s) were not downloaded." = "%d plik(ów) nie zostało pobranych."; + /* time interval */ "%d hours" = "%d godzin"; +/* alert title */ +"%d messages not forwarded" = "%d wiadomości nie przekazanych"; + /* time interval */ "%d min" = "%d min"; @@ -178,6 +178,9 @@ /* time interval */ "%d sec" = "%d sek"; +/* delete after time */ +"%d seconds(s)" = "%d sekundach"; + /* integrity error chat item */ "%d skipped message(s)" = "%d pominięte wiadomość(i)"; @@ -205,6 +208,9 @@ /* No comment provided by engineer. */ "%lld messages blocked" = "%lld wiadomości zablokowanych"; +/* No comment provided by engineer. */ +"%lld messages blocked by admin" = "%lld wiadomości zablokowanych przez admina"; + /* No comment provided by engineer. */ "%lld messages marked deleted" = "%lld wiadomości oznaczonych do usunięcia"; @@ -217,9 +223,6 @@ /* No comment provided by engineer. */ "%lld new interface languages" = "%lld nowe języki interfejsu"; -/* No comment provided by engineer. */ -"%lld second(s)" = "%lld sekund(y)"; - /* No comment provided by engineer. */ "%lld seconds" = "%lld sekund"; @@ -265,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 dzień"; /* time interval */ @@ -274,14 +278,22 @@ /* No comment provided by engineer. */ "1 minute" = "1 minuta"; -/* time interval */ +/* delete after time +time interval */ "1 month" = "1 miesiąc"; -/* time interval */ +/* delete after time +time interval */ "1 week" = "1 tydzień"; +/* delete after time */ +"1 year" = "1 roku"; + /* No comment provided by engineer. */ -"1-time link" = "1-razowy link"; +"1-time link" = "link jednorazowy"; + +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Link jednorazowy może być użyty *tylko z jednym kontaktem* - udostępnij go osobiście lub przez dowolny komunikator."; /* No comment provided by engineer. */ "5 minutes" = "5 minut"; @@ -317,10 +329,7 @@ "Abort changing address?" = "Przerwać zmianę adresu?"; /* No comment provided by engineer. */ -"About SimpleX" = "O SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "O adresie SimpleX"; +"About operators" = "O operatorach"; /* No comment provided by engineer. */ "About SimpleX Chat" = "O SimpleX Chat"; @@ -329,101 +338,196 @@ "above, then choose:" = "powyżej, a następnie wybierz:"; /* No comment provided by engineer. */ -"Accent color" = "Kolor akcentu"; +"Accent" = "Akcent"; /* accept contact request via notification - accept incoming call via notification */ +accept incoming call via notification +swipe action */ "Accept" = "Akceptuj"; +/* No comment provided by engineer. */ +"Accept conditions" = "Zaakceptuj warunki"; + /* No comment provided by engineer. */ "Accept connection request?" = "Zaakceptować prośbę o połączenie?"; /* notification body */ "Accept contact request from %@?" = "Zaakceptuj prośbę o kontakt od %@?"; -/* accept contact request via notification */ +/* accept contact request via notification +swipe action */ "Accept incognito" = "Akceptuj incognito"; /* call status */ "accepted call" = "zaakceptowane połączenie"; +/* No comment provided by engineer. */ +"Accepted conditions" = "Zaakceptowano warunki"; + +/* No comment provided by engineer. */ +"Acknowledged" = "Potwierdzono"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Błędy potwierdzenia"; + +/* token status text */ +"Active" = "Aktywne"; + +/* 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 preset servers" = "Dodaj gotowe serwery"; +"Add friends" = "Dodaj znajomych"; + +/* No comment provided by engineer. */ +"Add list" = "Dodaj listę"; /* No comment provided by engineer. */ "Add profile" = "Dodaj profil"; /* No comment provided by engineer. */ -"Add server…" = "Dodaj serwer…"; +"Add server" = "Dodaj serwer"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Dodaj serwery, skanując kody QR."; +/* No comment provided by engineer. */ +"Add team members" = "Dodaj członków zespołu"; + /* No comment provided by engineer. */ "Add to another device" = "Dodaj do innego urządzenia"; +/* No comment provided by engineer. */ +"Add to list" = "Dodaj do listy"; + /* No comment provided by engineer. */ "Add welcome message" = "Dodaj wiadomość powitalną"; +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "Dodaj członków zespołu do konwersacji."; + +/* No comment provided by engineer. */ +"Added media & file servers" = "Dodano serwery multimediów i plików"; + +/* No comment provided by engineer. */ +"Added message servers" = "Dodano serwery wiadomości"; + +/* No comment provided by engineer. */ +"Additional accent" = "Dodatkowy akcent"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Dodatkowy akcent 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Dodatkowy drugorzędny"; + /* No comment provided by engineer. */ "Address" = "Adres"; /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "Zmiana adresu zostanie przerwana. Użyty zostanie stary adres odbiorczy."; +/* No comment provided by engineer. */ +"Address or 1-time link?" = "Adres czy jednorazowy link?"; + +/* No comment provided by engineer. */ +"Address settings" = "Ustawienia adresu"; + /* member role */ "admin" = "administrator"; +/* feature role */ +"admins" = "administratorzy"; + +/* No comment provided by engineer. */ +"Admins can block a member for all." = "Administratorzy mogą blokować członka dla wszystkich."; + /* No comment provided by engineer. */ "Admins can create the links to join groups." = "Administratorzy mogą tworzyć linki do dołączania do grup."; /* No comment provided by engineer. */ "Advanced network settings" = "Zaawansowane ustawienia sieci"; +/* No comment provided by engineer. */ +"Advanced settings" = "Zaawansowane ustawienia"; + /* chat item text */ "agreeing encryption for %@…" = "uzgadnianie szyfrowania dla %@…"; /* chat item text */ "agreeing encryption…" = "uzgadnianie szyfrowania…"; +/* No comment provided by engineer. */ +"All" = "Wszystko"; + /* No comment provided by engineer. */ "All app data is deleted." = "Wszystkie dane aplikacji są usunięte."; /* No comment provided by engineer. */ "All chats and messages will be deleted - this cannot be undone!" = "Wszystkie czaty i wiadomości zostaną usunięte - nie można tego cofnąć!"; +/* alert message */ +"All chats will be removed from the list %@, and the list deleted." = "Wszystkie rozmowy zostaną usunięte z listy %@, a lista usunięta."; + /* No comment provided by engineer. */ "All data is erased when it is entered." = "Wszystkie dane są usuwane po jego wprowadzeniu."; +/* No comment provided by engineer. */ +"All data is kept private on your device." = "Wszystkie dane są prywatne na Twoim urządzeniu."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Wszyscy członkowie grupy pozostaną połączeni."; +/* feature role */ +"all members" = "wszyscy członkowie"; + +/* No comment provided by engineer. */ +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Wszystkie wiadomości i pliki są wysyłane **z szyfrowaniem end-to-end**, z bezpieczeństwem postkwantowym w wiadomościach bezpośrednich."; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone!" = "Wszystkie wiadomości zostaną usunięte – nie można tego cofnąć!"; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Wszystkie wiadomości zostaną usunięte - nie można tego cofnąć! Wiadomości zostaną usunięte TYLKO dla Ciebie."; /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Wszystkie nowe wiadomości z %@ zostaną ukryte!"; +/* profile dropdown */ +"All profiles" = "Wszystkie profile"; + +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Wszystkie raporty zostaną dla Ciebie zarchiwizowane."; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Wszystkie Twoje kontakty pozostaną połączone."; /* No comment provided by engineer. */ "All your contacts will remain connected. Profile update will be sent to your contacts." = "Wszystkie Twoje kontakty pozostaną połączone. Aktualizacja profilu zostanie wysłana do Twoich kontaktów."; +/* No comment provided by engineer. */ +"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Wszystkie twoje kontakty, konwersacje i pliki będą bezpiecznie szyfrowane i wgrywane w kawałkach do skonfigurowanych przekaźników XFTP."; + /* No comment provided by engineer. */ "Allow" = "Pozwól"; /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Zezwalaj na połączenia tylko wtedy, gdy Twój kontakt na to pozwala."; +/* No comment provided by engineer. */ +"Allow calls?" = "Zezwolić na połączenia?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "Zezwól na znikające wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli."; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you." = "Zezwalaj na nieodwracalne usuwanie wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli."; +"Allow downgrade" = "Zezwól na obniżenie wersji"; + +/* No comment provided by engineer. */ +"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Zezwalaj na nieodwracalne usuwanie wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli. (24 godziny)"; /* 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."; @@ -438,11 +542,20 @@ "Allow sending disappearing messages." = "Zezwól na wysyłanie znikających wiadomości."; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages." = "Zezwól na nieodwracalne usunięcie wysłanych wiadomości."; +"Allow sharing" = "Zezwól na udostępnianie"; + +/* 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)"; + +/* No comment provided by engineer. */ +"Allow to report messsages to moderators." = "Zezwól na zgłaszanie wiadomości moderatorom."; /* No comment provided by engineer. */ "Allow to send files and media." = "Pozwól na wysyłanie plików i mediów."; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "Zezwól na wysyłanie linków SimpleX."; + /* No comment provided by engineer. */ "Allow to send voice messages." = "Zezwól na wysyłanie wiadomości głosowych."; @@ -459,7 +572,7 @@ "Allow your contacts to call you." = "Zezwól swoim kontaktom na połączenia do Ciebie."; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages." = "Zezwól swoim kontaktom na nieodwracalne usuwanie wysłanych wiadomości."; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Zezwól swoim kontaktom na nieodwracalne usuwanie wysłanych wiadomości. (24 godziny)"; /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Zezwól swoim kontaktom na wysyłanie znikających wiadomości."; @@ -479,6 +592,9 @@ /* pref value */ "always" = "zawsze"; +/* No comment provided by engineer. */ +"Always use private routing." = "Zawsze używaj prywatnego trasowania."; + /* No comment provided by engineer. */ "Always use relay" = "Zawsze używaj przekaźnika"; @@ -488,15 +604,27 @@ /* No comment provided by engineer. */ "and %lld other events" = "i %lld innych wydarzeń"; +/* report reason */ +"Another reason" = "Inny powód"; + /* 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: %@"; +/* No comment provided by engineer. */ +"App data migration" = "Migracja danych aplikacji"; + /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "Aplikacja szyfruje nowe lokalne pliki (bez filmów)."; +/* No comment provided by engineer. */ +"App group:" = "Grupa aplikacji:"; + /* No comment provided by engineer. */ "App icon" = "Ikona aplikacji"; @@ -506,6 +634,9 @@ /* No comment provided by engineer. */ "App passcode is replaced with self-destruct passcode." = "Pin aplikacji został zastąpiony pinem samozniszczenia."; +/* No comment provided by engineer. */ +"App session" = "Sesja aplikacji"; + /* No comment provided by engineer. */ "App version" = "Wersja aplikacji"; @@ -515,9 +646,48 @@ /* No comment provided by engineer. */ "Appearance" = "Wygląd"; +/* No comment provided by engineer. */ +"Apply" = "Zastosuj"; + +/* No comment provided by engineer. */ +"Apply to" = "Zastosuj dla"; + +/* No comment provided by engineer. */ +"Archive" = "Archiwizuj"; + +/* No comment provided by engineer. */ +"Archive %lld reports?" = "Archiwizować %lld reports?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "Archiwizować wszystkie zgłoszenia?"; + +/* No comment provided by engineer. */ +"Archive and upload" = "Archiwizuj i prześlij"; + +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Archiwizuj kontakty aby porozmawiać później."; + +/* No comment provided by engineer. */ +"Archive report" = "Archiwizuj zgłoszenie"; + +/* No comment provided by engineer. */ +"Archive report?" = "Archiwizować zgłoszenie?"; + +/* swipe action */ +"Archive reports" = "Archiwizuj zgłoszenia"; + +/* No comment provided by engineer. */ +"Archived contacts" = "Zarchiwizowane kontakty"; + +/* No comment provided by engineer. */ +"Archiving database" = "Archiwizowanie bazy danych"; + /* No comment provided by engineer. */ "Attach" = "Dołącz"; +/* No comment provided by engineer. */ +"attempts" = "próby"; + /* No comment provided by engineer. */ "Audio & video calls" = "Połączenia audio i wideo"; @@ -557,9 +727,15 @@ /* No comment provided by engineer. */ "Auto-accept images" = "Automatyczne akceptowanie obrazów"; +/* alert title */ +"Auto-accept settings" = "Ustawienia automatycznej akceptacji"; + /* No comment provided by engineer. */ "Back" = "Wstecz"; +/* No comment provided by engineer. */ +"Background" = "Tło"; + /* No comment provided by engineer. */ "Bad desktop address" = "Zły adres komputera"; @@ -575,15 +751,39 @@ /* No comment provided by engineer. */ "Bad message ID" = "Zły identyfikator wiadomości"; +/* No comment provided by engineer. */ +"Better calls" = "Lepsze połączenia"; + /* No comment provided by engineer. */ "Better groups" = "Lepsze grupy"; +/* No comment provided by engineer. */ +"Better message dates." = "Lepsze daty wiadomości."; + /* No comment provided by engineer. */ "Better messages" = "Lepsze wiadomości"; +/* No comment provided by engineer. */ +"Better networking" = "Lepsze sieciowanie"; + +/* No comment provided by engineer. */ +"Better notifications" = "Lepsze powiadomienia"; + +/* No comment provided by engineer. */ +"Better security ✅" = "Lepsze zabezpieczenia ✅"; + +/* No comment provided by engineer. */ +"Better user experience" = "Lepszy interfejs użytkownika"; + +/* No comment provided by engineer. */ +"Black" = "Czarny"; + /* No comment provided by engineer. */ "Block" = "Zablokuj"; +/* No comment provided by engineer. */ +"Block for all" = "Zablokuj dla wszystkich"; + /* No comment provided by engineer. */ "Block group members" = "Blokuj członków grupy"; @@ -591,11 +791,30 @@ "Block member" = "Zablokuj członka"; /* No comment provided by engineer. */ -"Block member?" = "Zablokować członka?"; +"Block member for all?" = "Zablokować członka dla wszystkich?"; /* No comment provided by engineer. */ +"Block member?" = "Zablokować członka?"; + +/* marked deleted chat item preview text */ "blocked" = "zablokowany"; +/* rcv group event chat item */ +"blocked %@" = "zablokowany %@"; + +/* blocked chat item +marked deleted chat item preview text */ +"blocked by admin" = "zablokowany przez admina"; + +/* No comment provided by engineer. */ +"Blocked by admin" = "Zablokowany przez admina"; + +/* No comment provided by engineer. */ +"Blur for better privacy." = "Rozmycie dla lepszej prywatności."; + +/* No comment provided by engineer. */ +"Blur media" = "Rozmycie mediów"; + /* No comment provided by engineer. */ "bold" = "pogrubiona"; @@ -603,7 +822,7 @@ "Both you and your contact can add message reactions." = "Zarówno Ty, jak i Twój kontakt możecie dodawać reakcje wiadomości."; /* No comment provided by engineer. */ -"Both you and your contact can irreversibly delete sent messages." = "Zarówno Ty, jak i Twój kontakt możecie nieodwracalnie usunąć wysłane wiadomości."; +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "Zarówno Ty, jak i Twój kontakt możecie nieodwracalnie usunąć wysłane wiadomości. (24 godziny)"; /* No comment provided by engineer. */ "Both you and your contact can make calls." = "Zarówno Ty, jak i Twój kontakt możecie nawiązywać połączenia."; @@ -617,9 +836,21 @@ /* 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. */ +"Business address" = "Adres firmowy"; + +/* No comment provided by engineer. */ +"Business chats" = "Czaty biznesowe"; + +/* No comment provided by engineer. */ +"Businesses" = "Firmy"; + /* 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. */ +"call" = "zadzwoń"; + /* No comment provided by engineer. */ "Call already ended!" = "Połączenie już zakończone!"; @@ -636,7 +867,16 @@ "Calls" = "Połączenia"; /* No comment provided by engineer. */ -"Can't delete user profile!" = "Nie można usunąć profilu użytkownika!"; +"Calls prohibited!" = "Połączenia zakazane!"; + +/* No comment provided by engineer. */ +"Camera not available" = "Kamera nie dostępna"; + +/* No comment provided by engineer. */ +"Can't call contact" = "Nie można zadzwonić do kontaktu"; + +/* No comment provided by engineer. */ +"Can't call member" = "Nie można zadzwonić do członka"; /* No comment provided by engineer. */ "Can't invite contact!" = "Nie można zaprosić kontaktu!"; @@ -645,8 +885,15 @@ "Can't invite contacts!" = "Nie można zaprosić kontaktów!"; /* No comment provided by engineer. */ +"Can't message member" = "Nie można wysłać wiadomości do członka"; + +/* alert action +alert button */ "Cancel" = "Anuluj"; +/* No comment provided by engineer. */ +"Cancel migration" = "Anuluj migrację"; + /* feature offered item */ "cancelled %@" = "anulowany %@"; @@ -654,11 +901,23 @@ "Cannot access keychain to save database password" = "Nie można uzyskać dostępu do pęku kluczy, aby zapisać hasło do bazy danych"; /* No comment provided by engineer. */ +"Cannot forward message" = "Nie można przekazać wiadomości"; + +/* alert title */ "Cannot receive file" = "Nie można odebrać pliku"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Przekroczono pojemność - odbiorca nie otrzymał wcześniej wysłanych wiadomości."; + +/* No comment provided by engineer. */ +"Cellular" = "Sieć komórkowa"; + /* No comment provided by engineer. */ "Change" = "Zmień"; +/* authentication reason */ +"Change chat profiles" = "Zmień profil czatu"; + /* No comment provided by engineer. */ "Change database passphrase?" = "Zmienić hasło bazy danych?"; @@ -684,7 +943,7 @@ "Change self-destruct mode" = "Zmień tryb samozniszczenia"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Zmień pin samozniszczenia"; /* chat item text */ @@ -703,7 +962,16 @@ "changing address…" = "zmiana adresu…"; /* No comment provided by engineer. */ -"Chat archive" = "Archiwum czatu"; +"Chat" = "Czat"; + +/* No comment provided by engineer. */ +"Chat already exists" = "Czat już istnieje"; + +/* No comment provided by engineer. */ +"Chat already exists!" = "Czat już istnieje!"; + +/* No comment provided by engineer. */ +"Chat colors" = "Kolory czatu"; /* No comment provided by engineer. */ "Chat console" = "Konsola czatu"; @@ -715,7 +983,10 @@ "Chat database deleted" = "Baza danych czatu usunięta"; /* No comment provided by engineer. */ -"Chat database imported" = "Zaimportowano bazę danych czatu"; +"Chat database exported" = "Wyeksportowano bazę danych czatów"; + +/* No comment provided by engineer. */ +"Chat database imported" = "Zaimportowano bazę danych czatów"; /* No comment provided by engineer. */ "Chat is running" = "Czat jest uruchomiony"; @@ -723,18 +994,51 @@ /* No comment provided by engineer. */ "Chat is stopped" = "Czat jest zatrzymany"; +/* 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." = "Czat został zatrzymany. Jeśli korzystałeś już z tej bazy danych na innym urządzeniu, powinieneś przenieść ją z powrotem przed rozpoczęciem czatu."; + +/* No comment provided by engineer. */ +"Chat list" = "Lista czatów"; + +/* No comment provided by engineer. */ +"Chat migrated!" = "Czat zmigrowany!"; + /* No comment provided by engineer. */ "Chat preferences" = "Preferencje czatu"; +/* alert message */ +"Chat preferences were changed." = "Preferencje czatu zostały zmienione."; + +/* No comment provided by engineer. */ +"Chat profile" = "Profil użytkownika"; + +/* No comment provided by engineer. */ +"Chat theme" = "Motyw czatu"; + +/* No comment provided by engineer. */ +"Chat will be deleted for all members - this cannot be undone!" = "Czat zostanie usunięty dla wszystkich członków – tej operacji nie można cofnąć!"; + +/* 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ąć!"; + /* No comment provided by engineer. */ "Chats" = "Czaty"; /* No comment provided by engineer. */ +"Check messages every 20 min." = "Sprawdzaj wiadomości co 20 min."; + +/* No comment provided by engineer. */ +"Check messages when allowed." = "Sprawdź wiadomości, gdy będzie to dopuszczone."; + +/* alert title */ "Check server address and try again." = "Sprawdź adres serwera i spróbuj ponownie."; /* No comment provided by engineer. */ "Chinese and Spanish interface" = "Chiński i hiszpański interfejs"; +/* No comment provided by engineer. */ +"Choose _Migrate from another device_ on the new device and scan QR code." = "Wybierz _Zmigruj z innego urządzenia_ na nowym urządzeniu i zeskanuj kod QR."; + /* No comment provided by engineer. */ "Choose file" = "Wybierz plik"; @@ -742,6 +1046,15 @@ "Choose from library" = "Wybierz z biblioteki"; /* No comment provided by engineer. */ +"Chunks deleted" = "Fragmenty usunięte"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Fragmenty pobrane"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Fragmenty przesłane"; + +/* swipe action */ "Clear" = "Wyczyść"; /* No comment provided by engineer. */ @@ -750,14 +1063,20 @@ /* No comment provided by engineer. */ "Clear conversation?" = "Wyczyścić rozmowę?"; +/* No comment provided by engineer. */ +"Clear private notes?" = "Wyczyścić prywatne notatki?"; + /* No comment provided by engineer. */ "Clear verification" = "Wyczyść weryfikację"; /* No comment provided by engineer. */ -"colored" = "kolorowy"; +"Color chats with the new themes." = "Koloruj czaty z nowymi motywami."; /* No comment provided by engineer. */ -"Colors" = "Kolory"; +"Color mode" = "Tryb koloru"; + +/* No comment provided by engineer. */ +"colored" = "kolorowy"; /* server test step */ "Compare file" = "Porównaj plik"; @@ -768,15 +1087,39 @@ /* No comment provided by engineer. */ "complete" = "kompletny"; +/* No comment provided by engineer. */ +"Completed" = "Zakończono"; + +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "Warunki zaakceptowane dnia: %@."; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "Warunki zostały zaakceptowane przez operatora(-ów): **%@**."; + +/* No comment provided by engineer. */ +"Conditions are already accepted for these operator(s): **%@**." = "Warunki zostały już zaakceptowane przez tego(-ych) operatora(-ów): **%@**."; + +/* No comment provided by engineer. */ +"Conditions of use" = "Warunki użytkowania"; + /* No comment provided by engineer. */ "Configure ICE servers" = "Skonfiguruj serwery ICE"; /* No comment provided by engineer. */ "Confirm" = "Potwierdź"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "Potwierdzić usunięcie kontaktu?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "Potwierdź aktualizacje bazy danych"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Potwierdzaj pliki z nieznanych serwerów."; + +/* No comment provided by engineer. */ +"Confirm network settings" = "Potwierdź ustawienia sieciowe"; + /* No comment provided by engineer. */ "Confirm new passphrase…" = "Potwierdź nowe hasło…"; @@ -786,6 +1129,12 @@ /* No comment provided by engineer. */ "Confirm password" = "Potwierdź hasło"; +/* No comment provided by engineer. */ +"Confirm that you remember database passphrase to migrate it." = "Potwierdź, że pamiętasz hasło do bazy danych, aby ją zmigrować."; + +/* No comment provided by engineer. */ +"Confirm upload" = "Potwierdź wgranie"; + /* server test step */ "Connect" = "Połącz"; @@ -801,6 +1150,9 @@ /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "połącz się z deweloperami SimpleX Chat."; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Szybciej łącz się ze znajomymi."; + /* No comment provided by engineer. */ "Connect to yourself?" = "Połączyć się ze sobą?"; @@ -816,9 +1168,6 @@ /* No comment provided by engineer. */ "Connect via link" = "Połącz się przez link"; -/* No comment provided by engineer. */ -"Connect via link / QR code" = "Połącz się przez link / kod QR"; - /* No comment provided by engineer. */ "Connect via one-time link" = "Połącz przez jednorazowy link"; @@ -828,18 +1177,27 @@ /* No comment provided by engineer. */ "connected" = "połączony"; +/* No comment provided by engineer. */ +"Connected" = "Połączony"; + /* No comment provided by engineer. */ "Connected desktop" = "Połączony komputer"; /* rcv group event chat item */ "connected directly" = "połącz bezpośrednio"; +/* No comment provided by engineer. */ +"Connected servers" = "Połączone serwery"; + /* No comment provided by engineer. */ "Connected to desktop" = "Połączony do komputera"; /* No comment provided by engineer. */ "connecting" = "łączenie"; +/* No comment provided by engineer. */ +"Connecting" = "Łączenie"; + /* No comment provided by engineer. */ "connecting (accepted)" = "łączenie (zaakceptowane)"; @@ -861,15 +1219,21 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Łączenie z serwerem... (błąd: %@)"; +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "Łączenie z kontaktem, poczekaj lub sprawdź później!"; + /* No comment provided by engineer. */ "Connecting to desktop" = "Łączenie z komputerem"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "łączenie…"; /* No comment provided by engineer. */ "Connection" = "Połączenie"; +/* No comment provided by engineer. */ +"Connection and servers status." = "Stan połączenia i serwerów."; + /* No comment provided by engineer. */ "Connection error" = "Błąd połączenia"; @@ -879,6 +1243,9 @@ /* chat list item title (it should not be shown */ "connection established" = "połączenie ustanowione"; +/* No comment provided by engineer. */ +"Connection notifications" = "Powiadomienia o połączeniu"; + /* No comment provided by engineer. */ "Connection request sent!" = "Prośba o połączenie wysłana!"; @@ -888,15 +1255,27 @@ /* No comment provided by engineer. */ "Connection timeout" = "Czas połączenia minął"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "Połączenie z komputerem zakończone"; + /* connection information */ "connection:%@" = "połączenie: %@"; +/* No comment provided by engineer. */ +"Connections" = "Połączenia"; + +/* profile update event chat item */ +"contact %@ changed to %@" = "kontakt %1$@ zmieniony na %2$@"; + /* No comment provided by engineer. */ "Contact allows" = "Kontakt pozwala"; /* No comment provided by engineer. */ "Contact already exists" = "Kontakt już istnieje"; +/* No comment provided by engineer. */ +"Contact deleted!" = "Kontakt usunięty!"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "kontakt posiada szyfrowanie e2e"; @@ -910,7 +1289,7 @@ "Contact is connected" = "Kontakt jest połączony"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "Kontakt nie jest jeszcze połączony!"; +"Contact is deleted." = "Kontakt jest usunięty."; /* No comment provided by engineer. */ "Contact name" = "Nazwa kontaktu"; @@ -918,6 +1297,9 @@ /* No comment provided by engineer. */ "Contact preferences" = "Preferencje kontaktu"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "Kontakt zostanie usunięty – nie można tego cofnąć!"; + /* No comment provided by engineer. */ "Contacts" = "Kontakty"; @@ -927,12 +1309,21 @@ /* No comment provided by engineer. */ "Continue" = "Kontynuuj"; -/* chat item action */ +/* No comment provided by engineer. */ +"Conversation deleted!" = "Rozmowa usunięta!"; + +/* No comment provided by engineer. */ "Copy" = "Kopiuj"; +/* No comment provided by engineer. */ +"Copy error" = "Kopiuj błąd"; + /* No comment provided by engineer. */ "Core version: v%@" = "Wersja rdzenia: v%@"; +/* No comment provided by engineer. */ +"Corner" = "Róg"; + /* No comment provided by engineer. */ "Correct name to %@?" = "Poprawić imię na %@?"; @@ -942,9 +1333,6 @@ /* No comment provided by engineer. */ "Create a group using a random profile." = "Utwórz grupę używając losowego profilu."; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Utwórz adres, aby ludzie mogli się z Tobą połączyć."; - /* server test step */ "Create file" = "Utwórz plik"; @@ -960,9 +1348,6 @@ /* No comment provided by engineer. */ "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Utwórz nowy profil w [aplikacji desktopowej](https://simplex.chat/downloads/). 💻"; -/* No comment provided by engineer. */ -"Create one-time invitation link" = "Utwórz jednorazowy link do zaproszenia"; - /* No comment provided by engineer. */ "Create profile" = "Utwórz profil"; @@ -979,7 +1364,19 @@ "Create your profile" = "Utwórz swój profil"; /* No comment provided by engineer. */ -"Created on %@" = "Utworzony w dniu %@"; +"Created" = "Utworzono"; + +/* No comment provided by engineer. */ +"Created at" = "Utworzony o"; + +/* copied message info */ +"Created at: %@" = "Utworzony o: %@"; + +/* No comment provided by engineer. */ +"Creating archive link" = "Tworzenie linku archiwum"; + +/* No comment provided by engineer. */ +"Creating link…" = "Tworzenie linku…"; /* No comment provided by engineer. */ "creator" = "twórca"; @@ -990,6 +1387,9 @@ /* No comment provided by engineer. */ "Current passphrase…" = "Obecne hasło…"; +/* No comment provided by engineer. */ +"Current profile" = "Bieżący profil"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Obecnie maksymalna obsługiwana wielkość pliku wynosi %@."; @@ -999,9 +1399,15 @@ /* No comment provided by engineer. */ "Custom time" = "Niestandardowy czas"; +/* No comment provided by engineer. */ +"Customize theme" = "Dostosuj motyw"; + /* No comment provided by engineer. */ "Dark" = "Ciemny"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Kolory ciemnego trybu"; + /* No comment provided by engineer. */ "Database downgrade" = "Obniż wersję bazy danych"; @@ -1062,13 +1468,20 @@ /* time unit */ "days" = "dni"; +/* 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"; -/* pref value */ +/* No comment provided by engineer. */ +"decryption errors" = "błąd odszyfrowywania"; + +/* delete after time +pref value */ "default (%@)" = "domyślne (%@)"; /* No comment provided by engineer. */ @@ -1077,9 +1490,13 @@ /* No comment provided by engineer. */ "default (yes)" = "domyślnie (tak)"; -/* chat item action */ +/* alert action +swipe action */ "Delete" = "Usuń"; +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "Usunąć %lld wiadomości członków?"; + /* No comment provided by engineer. */ "Delete %lld messages?" = "Usunąć %lld wiadomości?"; @@ -1098,12 +1515,6 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Usuń i powiadom kontakt"; -/* No comment provided by engineer. */ -"Delete archive" = "Usuń archiwum"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Usunąć archiwum czatu?"; - /* No comment provided by engineer. */ "Delete chat profile" = "Usuń profil czatu"; @@ -1117,14 +1528,14 @@ "Delete contact" = "Usuń kontakt"; /* No comment provided by engineer. */ -"Delete Contact" = "Usuń Kontakt"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Usunąć kontakt?\nTo nie może być cofnięte!"; +"Delete contact?" = "Usunąć kontakt?"; /* No comment provided by engineer. */ "Delete database" = "Usuń bazę danych"; +/* No comment provided by engineer. */ +"Delete database from this device" = "Usuń bazę danych z tego urządzenia"; + /* server test step */ "Delete file" = "Usuń plik"; @@ -1161,7 +1572,7 @@ /* No comment provided by engineer. */ "Delete message?" = "Usunąć wiadomość?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "Usuń wiadomości"; /* No comment provided by engineer. */ @@ -1173,9 +1584,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "Usunąć starą bazę danych?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "Usuń oczekujące połączenie"; - /* No comment provided by engineer. */ "Delete pending connection?" = "Usunąć oczekujące połączenie?"; @@ -1185,12 +1593,21 @@ /* server test step */ "Delete queue" = "Usuń kolejkę"; +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Usuń do 20 wiadomości na raz."; + /* No comment provided by engineer. */ "Delete user profile?" = "Usunąć profil użytkownika?"; +/* No comment provided by engineer. */ +"Delete without notification" = "Usuń bez powiadomienia"; + /* deleted chat item */ "deleted" = "usunięty"; +/* No comment provided by engineer. */ +"Deleted" = "Usunięto"; + /* No comment provided by engineer. */ "Deleted at" = "Usunięto o"; @@ -1203,6 +1620,9 @@ /* rcv group event chat item */ "deleted group" = "usunięta grupa"; +/* No comment provided by engineer. */ +"Deletion errors" = "Błędy usuwania"; + /* No comment provided by engineer. */ "Delivery" = "Dostarczenie"; @@ -1224,9 +1644,27 @@ /* No comment provided by engineer. */ "Desktop devices" = "Urządzenia komputerowe"; +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "Adres serwera docelowego %@ jest niekompatybilny z ustawieniami serwera przekazującego %@."; + +/* snd error text */ +"Destination server error: %@" = "Błąd docelowego serwera: %@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "Wersja serwera docelowego %@ jest niekompatybilna z serwerem przekierowującym %@."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Szczegółowe statystyki"; + +/* No comment provided by engineer. */ +"Details" = "Szczegóły"; + /* No comment provided by engineer. */ "Develop" = "Deweloperskie"; +/* No comment provided by engineer. */ +"Developer options" = "Opcje deweloperskie"; + /* No comment provided by engineer. */ "Developer tools" = "Narzędzia deweloperskie"; @@ -1252,7 +1690,7 @@ "Direct messages" = "Bezpośrednie wiadomości"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Bezpośrednie wiadomości między członkami są zabronione w tej grupie."; +"Direct messages between members are prohibited." = "Bezpośrednie wiadomości między członkami są zabronione w tej grupie."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Wyłącz (zachowaj nadpisania)"; @@ -1266,6 +1704,9 @@ /* No comment provided by engineer. */ "disabled" = "wyłączony"; +/* No comment provided by engineer. */ +"Disabled" = "Wyłączony"; + /* No comment provided by engineer. */ "Disappearing message" = "Znikająca wiadomość"; @@ -1276,7 +1717,7 @@ "Disappearing messages are prohibited in this chat." = "Znikające wiadomości są zabronione na tym czacie."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Znikające wiadomości są zabronione w tej grupie."; +"Disappearing messages are prohibited." = "Znikające wiadomości są zabronione w tej grupie."; /* No comment provided by engineer. */ "Disappears at" = "Znika o"; @@ -1299,6 +1740,18 @@ /* No comment provided by engineer. */ "Do it later" = "Zrób to później"; +/* No comment provided by engineer. */ +"Do not send history to new members." = "Nie wysyłaj historii do nowych członków."; + +/* No comment provided by engineer. */ +"Do NOT send messages directly, even if your or destination server does not support private routing." = "NIE wysyłaj wiadomości bezpośrednio, nawet jeśli serwer docelowy nie obsługuje prywatnego trasowania."; + +/* No comment provided by engineer. */ +"Do not use credentials with proxy." = "Nie używaj danych logowania do proxy."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "NIE używaj prywatnego trasowania."; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "NIE używaj SimpleX do połączeń alarmowych."; @@ -1314,15 +1767,43 @@ /* No comment provided by engineer. */ "Downgrade and open chat" = "Obniż wersję i otwórz czat"; +/* alert button +chat item action */ +"Download" = "Pobierz"; + +/* No comment provided by engineer. */ +"Download errors" = "Błędy pobierania"; + +/* No comment provided by engineer. */ +"Download failed" = "Pobieranie nie udane"; + /* server test step */ "Download file" = "Pobierz plik"; +/* alert action */ +"Download files" = "Pobierz pliki"; + +/* No comment provided by engineer. */ +"Downloaded" = "Pobrane"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Pobrane pliki"; + +/* No comment provided by engineer. */ +"Downloading archive" = "Pobieranie archiwum"; + +/* No comment provided by engineer. */ +"Downloading link details" = "Pobieranie szczegółów linku"; + /* No comment provided by engineer. */ "Duplicate display name!" = "Zduplikowana wyświetlana nazwa!"; /* integrity error chat item */ "duplicate message" = "zduplikowana wiadomość"; +/* No comment provided by engineer. */ +"duplicates" = "duplikaty"; + /* No comment provided by engineer. */ "Duration" = "Czas trwania"; @@ -1341,12 +1822,18 @@ /* No comment provided by engineer. */ "Enable (keep overrides)" = "Włącz (zachowaj nadpisania)"; -/* No comment provided by engineer. */ +/* alert title */ "Enable automatic message deletion?" = "Czy włączyć automatyczne usuwanie wiadomości?"; +/* No comment provided by engineer. */ +"Enable camera access" = "Włącz dostęp do kamery"; + /* No comment provided by engineer. */ "Enable for all" = "Włącz dla wszystkich"; +/* No comment provided by engineer. */ +"Enable in direct chats (BETA)!" = "Włącz w czatach bezpośrednich (BETA)!"; + /* No comment provided by engineer. */ "Enable instant notifications?" = "Włączyć natychmiastowe powiadomienia?"; @@ -1374,6 +1861,12 @@ /* enabled status */ "enabled" = "włączone"; +/* No comment provided by engineer. */ +"Enabled" = "Włączony"; + +/* No comment provided by engineer. */ +"Enabled for" = "Włączony dla"; + /* enabled status */ "enabled for contact" = "włączone dla kontaktu"; @@ -1398,6 +1891,9 @@ /* notification */ "Encrypted message or another event" = "Zaszyfrowana wiadomość lub inne zdarzenie"; +/* notification */ +"Encrypted message: app is stopped" = "Zaszyfrowana wiadomość: aplikacja jest zatrzymana"; + /* notification */ "Encrypted message: database error" = "Zaszyfrowana wiadomość: błąd bazy danych"; @@ -1458,6 +1954,9 @@ /* No comment provided by engineer. */ "Enter Passcode" = "Wprowadź Pin"; +/* No comment provided by engineer. */ +"Enter passphrase" = "Wprowadź hasło"; + /* No comment provided by engineer. */ "Enter passphrase…" = "Wprowadź hasło…"; @@ -1491,21 +1990,27 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Błąd przyjmowania prośby o kontakt"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Błąd dostępu do pliku bazy danych"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Błąd dodawania członka(ów)"; /* No comment provided by engineer. */ "Error changing address" = "Błąd zmiany adresu"; +/* No comment provided by engineer. */ +"Error changing connection profile" = "Błąd zmiany połączenia profilu"; + /* No comment provided by engineer. */ "Error changing role" = "Błąd zmiany roli"; /* No comment provided by engineer. */ "Error changing setting" = "Błąd zmiany ustawienia"; +/* No comment provided by engineer. */ +"Error changing to incognito!" = "Błąd zmiany na incognito!"; + +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "Błąd połączenia z serwerem przekierowania %@. Spróbuj ponownie później."; + /* No comment provided by engineer. */ "Error creating address" = "Błąd tworzenia adresu"; @@ -1518,6 +2023,9 @@ /* No comment provided by engineer. */ "Error creating member contact" = "Błąd tworzenia kontaktu członka"; +/* No comment provided by engineer. */ +"Error creating message" = "Błąd tworzenia wiadomości"; + /* No comment provided by engineer. */ "Error creating profile!" = "Błąd tworzenia profilu!"; @@ -1533,9 +2041,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Błąd usuwania połączenia"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Błąd usuwania kontaktu"; - /* No comment provided by engineer. */ "Error deleting database" = "Błąd usuwania bazy danych"; @@ -1548,6 +2053,9 @@ /* No comment provided by engineer. */ "Error deleting user profile" = "Błąd usuwania profilu użytkownika"; +/* No comment provided by engineer. */ +"Error downloading the archive" = "Błąd pobierania archiwum"; + /* No comment provided by engineer. */ "Error enabling delivery receipts!" = "Błąd włączania potwierdzeń dostawy!"; @@ -1560,6 +2068,9 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Błąd eksportu bazy danych czatu"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Błąd eksportowania motywu: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Błąd importu bazy danych czatu"; @@ -1567,16 +2078,25 @@ "Error joining group" = "Błąd dołączenia do grupy"; /* No comment provided by engineer. */ -"Error loading %@ servers" = "Błąd ładowania %@ serwerów"; +"Error migrating settings" = "Błąd migracji ustawień"; /* No comment provided by engineer. */ +"Error opening chat" = "Błąd otwierania czatu"; + +/* alert title */ "Error receiving file" = "Błąd odbioru pliku"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Błąd ponownego łączenia z serwerem"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Błąd ponownego łączenia serwerów"; + /* No comment provided by engineer. */ "Error removing member" = "Błąd usuwania członka"; /* No comment provided by engineer. */ -"Error saving %@ servers" = "Błąd zapisu %@ serwerów"; +"Error resetting statistics" = "Błąd resetowania statystyk"; /* No comment provided by engineer. */ "Error saving group profile" = "Błąd zapisu profilu grupy"; @@ -1590,9 +2110,15 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "Błąd zapisu hasła do pęku kluczy"; +/* when migrating */ +"Error saving settings" = "Błąd zapisywania ustawień"; + /* No comment provided by engineer. */ "Error saving user password" = "Błąd zapisu hasła użytkownika"; +/* No comment provided by engineer. */ +"Error scanning code: %@" = "Błąd skanowanie kodu: %@"; + /* No comment provided by engineer. */ "Error sending email" = "Błąd wysyłania e-mail"; @@ -1612,6 +2138,9 @@ "Error stopping chat" = "Błąd zatrzymania czatu"; /* No comment provided by engineer. */ +"Error switching profile" = "Błąd zmiany profilu"; + +/* alertTitle */ "Error switching profile!" = "Błąd przełączania profilu!"; /* No comment provided by engineer. */ @@ -1630,9 +2159,17 @@ "Error updating user privacy" = "Błąd aktualizacji prywatności użytkownika"; /* No comment provided by engineer. */ -"Error: " = "Błąd: "; +"Error uploading the archive" = "Błąd wgrywania archiwum"; /* No comment provided by engineer. */ +"Error verifying passphrase:" = "Błąd weryfikowania hasła:"; + +/* No comment provided by engineer. */ +"Error: " = "Błąd: "; + +/* alert message +file error text +snd error text */ "Error: %@" = "Błąd: %@"; /* No comment provided by engineer. */ @@ -1642,10 +2179,10 @@ "Error: URL is invalid" = "Błąd: URL jest nieprawidłowy"; /* No comment provided by engineer. */ -"Even when disabled in the conversation." = "Nawet po wyłączeniu w rozmowie."; +"Errors" = "Błędy"; /* No comment provided by engineer. */ -"event happened" = "nowe wydarzenie"; +"Even when disabled in the conversation." = "Nawet po wyłączeniu w rozmowie."; /* No comment provided by engineer. */ "Exit without saving" = "Wyjdź bez zapisywania"; @@ -1653,15 +2190,24 @@ /* chat item action */ "Expand" = "Rozszerz"; +/* No comment provided by engineer. */ +"expired" = "wygasły"; + /* No comment provided by engineer. */ "Export database" = "Eksportuj bazę danych"; /* No comment provided by engineer. */ "Export error:" = "Błąd eksportu:"; +/* No comment provided by engineer. */ +"Export theme" = "Eksportuj motyw"; + /* No comment provided by engineer. */ "Exported database archive." = "Wyeksportowane archiwum bazy danych."; +/* No comment provided by engineer. */ +"Exported file doesn't exist" = "Wyeksportowany plik nie istnieje"; + /* No comment provided by engineer. */ "Exporting database archive…" = "Eksportowanie archiwum bazy danych…"; @@ -1674,9 +2220,27 @@ /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Szybsze dołączenie i bardziej niezawodne wiadomości."; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "Ulubione"; +/* file error alert title */ +"File error" = "Błąd pliku"; + +/* alert message */ +"File errors:\n%@" = "Błędy pliku:\n%@"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Nie odnaleziono pliku - najprawdopodobniej plik został usunięty lub anulowany."; + +/* file error text */ +"File server error: %@" = "Błąd serwera plików: %@"; + +/* No comment provided by engineer. */ +"File status" = "Status pliku"; + +/* copied message info */ +"File status: %@" = "Status pliku: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Plik zostanie usunięty z serwerów."; @@ -1689,6 +2253,9 @@ /* No comment provided by engineer. */ "File: %@" = "Plik: %@"; +/* No comment provided by engineer. */ +"Files" = "Pliki"; + /* No comment provided by engineer. */ "Files & media" = "Pliki i media"; @@ -1696,7 +2263,10 @@ "Files and media" = "Pliki i media"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Pliki i media są zabronione w tej grupie."; +"Files and media are prohibited." = "Pliki i media są zabronione w tej grupie."; + +/* No comment provided by engineer. */ +"Files and media not allowed" = "Pliki i multimedia nie są dozwolone"; /* No comment provided by engineer. */ "Files and media prohibited!" = "Pliki i media zabronione!"; @@ -1704,6 +2274,12 @@ /* No comment provided by engineer. */ "Filter unread and favorite chats." = "Filtruj nieprzeczytane i ulubione czaty."; +/* No comment provided by engineer. */ +"Finalize migration" = "Dokończ migrację"; + +/* No comment provided by engineer. */ +"Finalize migration on another device." = "Dokończ migrację na innym urządzeniu."; + /* No comment provided by engineer. */ "Finally, we have them! 🚀" = "W końcu je mamy! 🚀"; @@ -1731,6 +2307,48 @@ /* No comment provided by engineer. */ "For console" = "Dla konsoli"; +/* chat item action */ +"Forward" = "Przekaż dalej"; + +/* alert title */ +"Forward %d message(s)?" = "Przekazać %d wiadomość(i)?"; + +/* No comment provided by engineer. */ +"Forward and save messages" = "Przesyłaj dalej i zapisuj wiadomości"; + +/* alert action */ +"Forward messages" = "Przekaż wiadomości"; + +/* alert message */ +"Forward messages without files?" = "Przekazać wiadomości bez plików?"; + +/* No comment provided by engineer. */ +"forwarded" = "przekazane dalej"; + +/* No comment provided by engineer. */ +"Forwarded" = "Przekazane dalej"; + +/* No comment provided by engineer. */ +"Forwarded from" = "Przekazane dalej od"; + +/* No comment provided by engineer. */ +"Forwarding %lld messages" = "Przekazywanie %lld wiadomości"; + +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Serwer przekazujący %@ nie mógł połączyć się z serwerem docelowym %@. Spróbuj ponownie później."; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "Adres serwera przekierowującego jest niekompatybilny z ustawieniami sieciowymi: %@."; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "Wersja serwera przekierowującego jest niekompatybilna z ustawieniami sieciowymi: %@."; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Serwer przekazujący: %1$@\nBłąd serwera docelowego: %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Serwer przekazujący: %1$@\nBłąd: %2$@"; + /* No comment provided by engineer. */ "Found desktop" = "Znaleziono komputer"; @@ -1743,9 +2361,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Pełna nazwa (opcjonalna)"; -/* No comment provided by engineer. */ -"Full name:" = "Pełna nazwa:"; - /* No comment provided by engineer. */ "Fully decentralized – visible only to members." = "W pełni zdecentralizowana – widoczna tylko dla członków."; @@ -1758,6 +2373,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIF-y i naklejki"; +/* message preview */ +"Good afternoon!" = "Dzień dobry!"; + +/* message preview */ +"Good morning!" = "Dzień dobry!"; + /* No comment provided by engineer. */ "Group" = "Grupa"; @@ -1794,24 +2415,6 @@ /* No comment provided by engineer. */ "Group links" = "Linki grupowe"; -/* No comment provided by engineer. */ -"Group members can add message reactions." = "Członkowie grupy mogą dodawać reakcje wiadomości."; - -/* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages." = "Członkowie grupy mogą nieodwracalnie usuwać wysłane wiadomości."; - -/* No comment provided by engineer. */ -"Group members can send direct messages." = "Członkowie grupy mogą wysyłać bezpośrednie wiadomości."; - -/* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Członkowie grupy mogą wysyłać znikające wiadomości."; - -/* No comment provided by engineer. */ -"Group members can send files and media." = "Członkowie grupy mogą wysyłać pliki i media."; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "Członkowie grupy mogą wysyłać wiadomości głosowe."; - /* notification */ "Group message:" = "Wiadomość grupowa:"; @@ -1866,12 +2469,12 @@ /* No comment provided by engineer. */ "History" = "Historia"; +/* No comment provided by engineer. */ +"History is not sent to new members." = "Historia nie jest wysyłana do nowych członków."; + /* time unit */ "hours" = "godziny"; -/* No comment provided by engineer. */ -"How it works" = "Jak to działa"; - /* No comment provided by engineer. */ "How SimpleX works" = "Jak działa SimpleX"; @@ -1884,15 +2487,15 @@ /* No comment provided by engineer. */ "How to use your servers" = "Jak korzystać z Twoich serwerów"; +/* No comment provided by engineer. */ +"Hungarian interface" = "Węgierski interfejs"; + /* No comment provided by engineer. */ "ICE servers (one per line)" = "Serwery ICE (po jednym na linię)"; /* No comment provided by engineer. */ "If you can't meet in person, show QR code in a video call, or share the link." = "Jeśli nie możesz spotkać się osobiście, pokaż kod QR w rozmowie wideo lub udostępnij link."; -/* No comment provided by engineer. */ -"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Jeśli nie możesz spotkać się osobiście, możesz **zeskanować kod QR w rozmowie wideo** lub Twój kontakt może udostępnić link z zaproszeniem."; - /* No comment provided by engineer. */ "If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Jeśli wprowadzisz ten pin podczas otwierania aplikacji, wszystkie dane aplikacji zostaną nieodwracalnie usunięte!"; @@ -1915,7 +2518,7 @@ "Immediately" = "Natychmiast"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Odporność na spam i nadużycia"; +"Immune to spam" = "Odporność na spam i nadużycia"; /* No comment provided by engineer. */ "Import" = "Importuj"; @@ -1926,15 +2529,36 @@ /* No comment provided by engineer. */ "Import database" = "Importuj bazę danych"; +/* No comment provided by engineer. */ +"Import failed" = "Import nie udał się"; + +/* No comment provided by engineer. */ +"Import theme" = "Importuj motyw"; + +/* No comment provided by engineer. */ +"Importing archive" = "Importowanie archiwum"; + +/* No comment provided by engineer. */ +"Improved message delivery" = "Ulepszona dostawa wiadomości"; + /* No comment provided by engineer. */ "Improved privacy and security" = "Zwiększona prywatność i bezpieczeństwo"; /* No comment provided by engineer. */ "Improved server configuration" = "Ulepszona konfiguracja serwera"; +/* No comment provided by engineer. */ +"In order to continue, chat should be stopped." = "Aby konturować, czat musi zostać zatrzymany."; + /* No comment provided by engineer. */ "In reply to" = "W odpowiedzi na"; +/* No comment provided by engineer. */ +"In-call sounds" = "Dźwięki w rozmowie"; + +/* No comment provided by engineer. */ +"inactive" = "nieaktywny"; + /* No comment provided by engineer. */ "Incognito" = "Incognito"; @@ -1989,14 +2613,17 @@ /* 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)"; +/* No comment provided by engineer. */ +"Instant" = "Natychmiastowo"; + /* No comment provided by engineer. */ "Instant push notifications will be hidden!\n" = "Natychmiastowe powiadomienia push będą ukryte!\n"; /* No comment provided by engineer. */ -"Instantly" = "Natychmiastowo"; +"Interface" = "Interfejs"; /* No comment provided by engineer. */ -"Interface" = "Interfejs"; +"Interface colors" = "Kolory interfejsu"; /* invalid chat data */ "invalid chat" = "nieprawidłowy czat"; @@ -2010,10 +2637,25 @@ /* invalid chat item */ "invalid data" = "nieprawidłowe dane"; +/* No comment provided by engineer. */ +"Invalid display name!" = "Nieprawidłowa nazwa wyświetlana!"; + +/* No comment provided by engineer. */ +"Invalid link" = "Nieprawidłowy link"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Nieprawidłowe potwierdzenie migracji"; + /* No comment provided by engineer. */ "Invalid name!" = "Nieprawidłowa nazwa!"; /* No comment provided by engineer. */ +"Invalid QR code" = "Nieprawidłowy kod QR"; + +/* No comment provided by engineer. */ +"Invalid response" = "Nieprawidłowa odpowiedź"; + +/* alert title */ "Invalid server address!" = "Nieprawidłowy adres serwera!"; /* item status text */ @@ -2025,6 +2667,9 @@ /* group name */ "invitation to group %@" = "zaproszenie do grupy %@"; +/* No comment provided by engineer. */ +"invite" = "zaproś"; + /* No comment provided by engineer. */ "Invite friends" = "Zaproś znajomych"; @@ -2052,6 +2697,9 @@ /* 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 będzie używany do bezpiecznego przechowywania hasła po ponownym uruchomieniu aplikacji lub zmianie hasła - pozwoli to na otrzymywanie powiadomień push."; +/* No comment provided by engineer. */ +"IP address" = "Adres IP"; + /* No comment provided by engineer. */ "Irreversible message deletion" = "Nieodwracalne usuwanie wiadomości"; @@ -2059,7 +2707,7 @@ "Irreversible message deletion is prohibited in this chat." = "Nieodwracalne usuwanie wiadomości jest na tym czacie zabronione."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "Nieodwracalne usuwanie wiadomości jest w tej grupie zabronione."; +"Irreversible message deletion is prohibited." = "Nieodwracalne usuwanie wiadomości jest w tej grupie zabronione."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "To pozwala na posiadanie wielu anonimowych połączeń bez żadnych wspólnych danych między nimi w pojedynczym profilu czatu."; @@ -2070,6 +2718,9 @@ /* No comment provided by engineer. */ "It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Może to nastąpić, gdy:\n1. Wiadomości wygasły w wysyłającym kliencie po 2 dniach lub na serwerze po 30 dniach.\n2. Odszyfrowanie wiadomości nie powiodło się, ponieważ Ty lub Twój kontakt użyliście starej kopii zapasowej bazy danych.\n3. Połączenie zostało skompromitowane."; +/* No comment provided by engineer. */ +"It protects your IP address and connections." = "Chroni Twój adres IP i połączenia."; + /* 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 (%@)." = "Wygląda na to, że jesteś już połączony przez ten link. Jeśli tak nie jest, wystąpił błąd (%@)."; @@ -2082,7 +2733,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Japoński interfejs"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Dołącz"; /* No comment provided by engineer. */ @@ -2091,6 +2742,9 @@ /* No comment provided by engineer. */ "Join group" = "Dołącz do grupy"; +/* No comment provided by engineer. */ +"Join group conversations" = "Dołącz do grupowej rozmowy"; + /* No comment provided by engineer. */ "Join group?" = "Dołączyć do grupy?"; @@ -2106,9 +2760,18 @@ /* No comment provided by engineer. */ "Joining group" = "Dołączanie do grupy"; +/* alert action */ +"Keep" = "Zachowaj"; + +/* No comment provided by engineer. */ +"Keep conversation" = "Zachowaj rozmowę"; + /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Zostaw aplikację otwartą i używaj ją z komputera"; +/* alert title */ +"Keep unused invitation?" = "Zachować nieużyte zaproszenie?"; + /* No comment provided by engineer. */ "Keep your connections" = "Zachowaj swoje połączenia"; @@ -2124,7 +2787,7 @@ /* No comment provided by engineer. */ "Learn more" = "Dowiedz się więcej"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Opuść"; /* No comment provided by engineer. */ @@ -2175,24 +2838,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Tryb blokady"; -/* No comment provided by engineer. */ -"Make a private connection" = "Nawiąż prywatne połączenie"; - /* No comment provided by engineer. */ "Make one message disappear" = "Spraw, aby jedna wiadomość zniknęła"; /* No comment provided by engineer. */ "Make profile private!" = "Ustaw profil jako prywatny!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Upewnij się, że adresy serwerów %@ są w poprawnym formacie, rozdzielone liniami i nie są zduplikowane (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Upewnij się, że adresy serwerów WebRTC ICE są w poprawnym formacie, rozdzielone liniami i nie są zduplikowane."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Wiele osób pytało: *jeśli SimpleX nie ma identyfikatora użytkownika, jak może dostarczać wiadomości?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Oznacz jako usunięty dla wszystkich"; @@ -2211,15 +2865,27 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "Maksymalnie 30 sekund, odbierane natychmiast."; +/* No comment provided by engineer. */ +"Media & file servers" = "Serwery mediów i plików"; + +/* blur media */ +"Medium" = "Średni"; + /* member role */ "member" = "członek"; /* No comment provided by engineer. */ "Member" = "Członek"; +/* profile update event chat item */ +"member %@ changed to %@" = "członek %1$@ zmieniony na %2$@"; + /* rcv group event chat item */ "member connected" = "połączony"; +/* item status text */ +"Member inactive" = "Członek nieaktywny"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Rola członka grupy zostanie zmieniona na \"%@\". Wszyscy członkowie grupy zostaną powiadomieni."; @@ -2229,15 +2895,54 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Członek zostanie usunięty z grupy - nie można tego cofnąć!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Członkowie grupy mogą dodawać reakcje wiadomości."; + +/* 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)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Członkowie grupy mogą wysyłać bezpośrednie wiadomości."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Członkowie grupy mogą wysyłać znikające wiadomości."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Członkowie grupy mogą wysyłać pliki i media."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "Członkowie grupy mogą wysyłać linki SimpleX."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Członkowie grupy mogą wysyłać wiadomości głosowe."; + +/* No comment provided by engineer. */ +"Menus" = "Menu"; + +/* No comment provided by engineer. */ +"message" = "wiadomość"; + /* item status text */ "Message delivery error" = "Błąd dostarczenia wiadomości"; /* No comment provided by engineer. */ "Message delivery receipts!" = "Potwierdzenia dostarczenia wiadomości!"; +/* item status text */ +"Message delivery warning" = "Ostrzeżenie dostarczenia wiadomości"; + /* No comment provided by engineer. */ "Message draft" = "Wersja robocza wiadomości"; +/* item status text */ +"Message forwarded" = "Wiadomość przekazana"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Wiadomość może zostać dostarczona później jeśli członek stanie się aktywny."; + +/* No comment provided by engineer. */ +"Message queue info" = "Informacje kolejki wiadomości"; + /* chat feature */ "Message reactions" = "Reakcje wiadomości"; @@ -2245,14 +2950,35 @@ "Message reactions are prohibited in this chat." = "Reakcje wiadomości są zabronione na tym czacie."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Reakcje wiadomości są zabronione w tej grupie."; +"Message reactions are prohibited." = "Reakcje wiadomości są zabronione w tej grupie."; /* notification */ "message received" = "wiadomość otrzymana"; +/* No comment provided by engineer. */ +"Message reception" = "Odebranie wiadomości"; + +/* No comment provided by engineer. */ +"Message servers" = "Serwery wiadomości"; + +/* No comment provided by engineer. */ +"Message shape" = "Kształt wiadomości"; + +/* No comment provided by engineer. */ +"Message source remains private." = "Źródło wiadomości pozostaje prywatne."; + +/* No comment provided by engineer. */ +"Message status" = "Status wiadomości"; + +/* copied message info */ +"Message status: %@" = "Status wiadomości: %@"; + /* No comment provided by engineer. */ "Message text" = "Tekst wiadomości"; +/* No comment provided by engineer. */ +"Message too large" = "Wiadomość jest zbyt duża"; + /* No comment provided by engineer. */ "Messages" = "Wiadomości"; @@ -2262,9 +2988,45 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Wiadomości od %@ zostaną pokazane!"; +/* No comment provided by engineer. */ +"Messages received" = "Otrzymane wiadomości"; + +/* No comment provided by engineer. */ +"Messages sent" = "Wysłane wiadomości"; + +/* alert message */ +"Messages were deleted after you selected them." = "Wiadomości zostały usunięte po wybraniu ich."; + +/* 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." = "Wiadomości, pliki i połączenia są chronione przez **szyfrowanie end-to-end** z doskonałym utajnianiem z wyprzedzeniem i odzyskiem po złamaniu."; + +/* 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." = "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 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"; + +/* No comment provided by engineer. */ +"Migrate to another device" = "Zmigruj do innego urządzenia"; + +/* No comment provided by engineer. */ +"Migrate to another device via QR code." = "Zmigruj do innego urządzenia przez kod QR."; + +/* No comment provided by engineer. */ +"Migrating" = "Migrowanie"; + /* No comment provided by engineer. */ "Migrating database archive…" = "Migrowanie archiwum bazy danych…"; +/* No comment provided by engineer. */ +"Migration complete" = "Migracja zakończona"; + /* No comment provided by engineer. */ "Migration error:" = "Błąd migracji:"; @@ -2275,7 +3037,7 @@ "Migration is completed" = "Migracja została zakończona"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migracje: %@"; +"Migrations:" = "Migracje:"; /* time unit */ "minutes" = "minuty"; @@ -2295,7 +3057,7 @@ /* copied message info */ "Moderated at: %@" = "Moderowany o: %@"; -/* No comment provided by engineer. */ +/* marked deleted chat item preview text */ "moderated by %@" = "moderowany przez %@"; /* time unit */ @@ -2304,45 +3066,57 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Więcej ulepszeń już wkrótce!"; +/* No comment provided by engineer. */ +"More reliable network connection." = "Bardziej niezawodne połączenia sieciowe."; + /* item status description */ "Most likely this connection is deleted." = "Najprawdopodobniej to połączenie jest usunięte."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Najprawdopodobniej ten kontakt usunął połączenie z Tobą."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Wiele profili czatu"; -/* No comment provided by engineer. */ +/* notification label action */ "Mute" = "Wycisz"; /* No comment provided by engineer. */ "Muted when inactive!" = "Wyciszony, gdy jest nieaktywny!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Nazwa"; /* No comment provided by engineer. */ "Network & servers" = "Sieć i serwery"; +/* No comment provided by engineer. */ +"Network connection" = "Połączenie z siecią"; + +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "Błąd sieciowy - wiadomość wygasła po wielu próbach wysłania jej."; + +/* No comment provided by engineer. */ +"Network management" = "Zarządzenie sieciowe"; + /* No comment provided by engineer. */ "Network settings" = "Ustawienia sieci"; /* No comment provided by engineer. */ "Network status" = "Status sieci"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "nigdy"; +/* No comment provided by engineer. */ +"New chat" = "Nowy czat"; + +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Nowe możliwości czatu 🎉"; + /* notification */ "New contact request" = "Nowa prośba o kontakt"; /* notification */ "New contact:" = "Nowy kontakt:"; -/* No comment provided by engineer. */ -"New database archive" = "Nowe archiwum bazy danych"; - /* No comment provided by engineer. */ "New desktop app!" = "Nowa aplikacja desktopowa!"; @@ -2352,6 +3126,9 @@ /* No comment provided by engineer. */ "New in %@" = "Nowość w %@"; +/* No comment provided by engineer. */ +"New media options" = "Nowe opcje mediów"; + /* No comment provided by engineer. */ "New member role" = "Nowa rola członka"; @@ -2367,6 +3144,12 @@ /* No comment provided by engineer. */ "New passphrase…" = "Nowe hasło…"; +/* No comment provided by engineer. */ +"New SOCKS credentials will be used every time you start the app." = "Nowe poświadczenia SOCKS będą używane przy każdym uruchomieniu aplikacji."; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used for each server." = "Dla każdego serwera zostaną użyte nowe poświadczenia SOCKS."; + /* pref value */ "no" = "nie"; @@ -2388,6 +3171,9 @@ /* No comment provided by engineer. */ "No device token!" = "Brak tokenu urządzenia!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Brak bezpośredniego połączenia, wiadomość została przekazana przez administratora."; + /* No comment provided by engineer. */ "no e2e encryption" = "brak szyfrowania e2e"; @@ -2400,18 +3186,42 @@ /* No comment provided by engineer. */ "No history" = "Brak historii"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Brak informacji, spróbuj przeładować"; + +/* No comment provided by engineer. */ +"No network connection" = "Brak połączenia z siecią"; + +/* No comment provided by engineer. */ +"No permission to record speech" = "Brak zezwoleń do nagrania rozmowy"; + +/* No comment provided by engineer. */ +"No permission to record video" = "Brak zezwoleń do nagrania wideo"; + /* No comment provided by engineer. */ "No permission to record voice message" = "Brak uprawnień do nagrywania wiadomości głosowej"; +/* No comment provided by engineer. */ +"No push server" = "Lokalnie"; + /* No comment provided by engineer. */ "No received or sent files" = "Brak odebranych lub wysłanych plików"; /* copied message info in history */ "no text" = "brak tekstu"; +/* No comment provided by engineer. */ +"No user identifiers." = "Brak identyfikatorów użytkownika."; + /* No comment provided by engineer. */ "Not compatible!" = "Nie kompatybilny!"; +/* No comment provided by engineer. */ +"Nothing selected" = "Nic nie jest zaznaczone"; + +/* alert title */ +"Nothing to forward!" = "Nic do przekazania!"; + /* No comment provided by engineer. */ "Notifications" = "Powiadomienia"; @@ -2425,30 +3235,27 @@ "observer" = "obserwator"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "wyłączony"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Wyłączony"; -/* No comment provided by engineer. */ -"Off (Local)" = "Wyłączony (Lokalnie)"; - /* feature offered item */ "offered %@" = "zaoferował %@"; /* feature offered item */ "offered %@: %@" = "zaoferował %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Ok"; /* No comment provided by engineer. */ -"Old database" = "Stara baza danych"; +"OK" = "OK"; /* No comment provided by engineer. */ -"Old database archive" = "Stare archiwum bazy danych"; +"Old database" = "Stara baza danych"; /* group pref value */ "on" = "włączone"; @@ -2457,16 +3264,19 @@ "One-time invitation link" = "Jednorazowy link zaproszenia"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Hosty onion będą wymagane do połączenia. Wymaga włączenia VPN."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Hosty onion będą wymagane do połączenia.\nWymaga włączenia VPN."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Hosty onion będą używane, gdy będą dostępne. Wymaga włączenia VPN."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Hosty onion będą używane, gdy będą dostępne.\nWymaga włączenia VPN."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Hosty onion nie będą używane."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Tylko urządzenia klienckie przechowują profile użytkowników, kontakty, grupy i wiadomości wysyłane za pomocą **2-warstwowego szyfrowania end-to-end**."; +"Only client devices store user profiles, contacts, groups, and messages." = "Tylko urządzenia klienckie przechowują profile użytkowników, kontakty, grupy i wiadomości wysyłane za pomocą **2-warstwowego szyfrowania end-to-end**."; + +/* No comment provided by engineer. */ +"Only delete conversation" = "Usuń tylko rozmowę"; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Tylko właściciele grup mogą zmieniać preferencje grupy."; @@ -2481,7 +3291,7 @@ "Only you can add message reactions." = "Tylko Ty możesz dodawać reakcje wiadomości."; /* No comment provided by engineer. */ -"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "Tylko Ty możesz nieodwracalnie usunąć wiadomości (Twój kontakt może oznaczyć je do usunięcia)."; +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Tylko Ty możesz nieodwracalnie usunąć wiadomości (Twój kontakt może oznaczyć je do usunięcia). (24 godziny)"; /* No comment provided by engineer. */ "Only you can make calls." = "Tylko Ty możesz wykonywać połączenia."; @@ -2496,7 +3306,7 @@ "Only your contact can add message reactions." = "Tylko Twój kontakt może dodawać reakcje wiadomości."; /* No comment provided by engineer. */ -"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Tylko Twój kontakt może nieodwracalnie usunąć wiadomości (możesz oznaczyć je do usunięcia)."; +"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Tylko Twój kontakt może nieodwracalnie usunąć wiadomości (możesz oznaczyć je do usunięcia). (24 godziny)"; /* No comment provided by engineer. */ "Only your contact can make calls." = "Tylko Twój kontakt może wykonywać połączenia."; @@ -2507,7 +3317,7 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Tylko Twój kontakt może wysyłać wiadomości głosowe."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Otwórz"; /* No comment provided by engineer. */ @@ -2519,21 +3329,45 @@ /* No comment provided by engineer. */ "Open group" = "Grupa otwarta"; +/* authentication reason */ +"Open migration to another device" = "Otwórz migrację na innym urządzeniu"; + /* No comment provided by engineer. */ "Open Settings" = "Otwórz Ustawienia"; -/* authentication reason */ -"Open user profiles" = "Otwórz profile użytkownika"; +/* No comment provided by engineer. */ +"Opening app…" = "Otwieranie aplikacji…"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Otwarto źródłowy protokół i kod - każdy może uruchomić serwery."; +"Or paste archive link" = "Lub wklej link archiwum"; /* No comment provided by engineer. */ -"Opening database…" = "Otwieranie bazy danych…"; +"Or scan QR code" = "Lub zeskanuj kod QR"; + +/* No comment provided by engineer. */ +"Or securely share this file link" = "Lub bezpiecznie udostępnij ten link pliku"; + +/* No comment provided by engineer. */ +"Or show this code" = "Lub pokaż ten kod"; + +/* No comment provided by engineer. */ +"other" = "inne"; + +/* No comment provided by engineer. */ +"Other" = "Inne"; + +/* No comment provided by engineer. */ +"other errors" = "inne błędy"; + +/* alert message */ +"Other file errors:\n%@" = "Inne błędy pliku:\n%@"; /* member role */ "owner" = "właściciel"; +/* feature role */ +"owners" = "właściciele"; + /* No comment provided by engineer. */ "Passcode" = "Pin"; @@ -2550,10 +3384,13 @@ "Passcode set!" = "Pin ustawiony!"; /* No comment provided by engineer. */ -"Password to show" = "Hasło do wyświetlenia"; +"Password" = "Hasło"; /* No comment provided by engineer. */ -"Paste" = "Wklej"; +"Password to show" = "Hasło do wyświetlenia"; + +/* past/unknown group member */ +"Past member %@" = "Były członek %@"; /* No comment provided by engineer. */ "Paste desktop address" = "Wklej adres komputera"; @@ -2562,32 +3399,44 @@ "Paste image" = "Wklej obraz"; /* No comment provided by engineer. */ -"Paste received link" = "Wklej otrzymany link"; +"Paste link to connect!" = "Wklej link, aby połączyć!"; -/* placeholder */ -"Paste the link you received to connect with your contact." = "Wklej otrzymany link w pole poniżej, aby połączyć się z kontaktem."; +/* No comment provided by engineer. */ +"Paste the link you received" = "Wklej link, który otrzymałeś"; /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Ludzie mogą się z Tobą połączyć tylko poprzez linki, które udostępniasz."; +"Pending" = "Oczekujące"; /* No comment provided by engineer. */ -"Periodically" = "Okresowo"; +"Periodic" = "Okresowo"; /* message decrypt error item */ "Permanent decryption error" = "Stały błąd odszyfrowania"; +/* No comment provided by engineer. */ +"Picture-in-picture calls" = "Połączenia obraz-w-obrazie"; + /* No comment provided by engineer. */ "PING count" = "Liczba PINGÓW"; /* No comment provided by engineer. */ "PING interval" = "Interwał PINGU"; +/* No comment provided by engineer. */ +"Play from the chat list." = "Odtwórz z listy czatów."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Poproś kontakt o włącznie połączeń."; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Poproś Twój kontakt o włączenie wysyłania wiadomości głosowych."; +/* No comment provided by engineer. */ +"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." = "Sprawdź, czy telefon i komputer są podłączone do tej samej sieci lokalnej i czy zapora sieciowa komputera umożliwia połączenie.\nProszę podzielić się innymi problemami z deweloperami."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Sprawdź, czy użyłeś prawidłowego linku lub poproś Twój kontakt o przesłanie innego."; @@ -2597,14 +3446,20 @@ /* No comment provided by engineer. */ "Please check yours and your contact preferences." = "Proszę sprawdzić preferencje Twoje i Twojego kontaktu."; +/* No comment provided by engineer. */ +"Please confirm that network settings are correct for this device." = "Proszę potwierdzić, że ustawienia sieciowe są prawidłowe dla tego urządzenia."; + +/* No comment provided by engineer. */ +"Please contact developers.\nError: %@" = "Proszę skontaktować się z deweloperami.\nBłąd: %@"; + /* No comment provided by engineer. */ "Please contact group admin." = "Skontaktuj się z administratorem grupy."; /* No comment provided by engineer. */ -"Please enter correct current passphrase." = "Wprowadź poprawne aktualne hasło."; +"Please enter correct current passphrase." = "Wprowadź poprawny obecny kod dostępu."; /* No comment provided by engineer. */ -"Please enter the previous password after restoring database backup. This action can not be undone." = "Proszę podać poprzednie hasło po przywróceniu kopii zapasowej bazy danych. Tej czynności nie można cofnąć."; +"Please enter the previous password after restoring database backup. This action can not be undone." = "Proszę podać poprzedni kod dostępu po przywróceniu kopii zapasowej bazy danych. Tej czynności nie można cofnąć."; /* No comment provided by engineer. */ "Please remember or store it securely - there is no way to recover a lost passcode!" = "Prosimy o jego zapamiętanie lub bezpieczne przechowywanie - nie ma możliwości odzyskania utraconego pinu!"; @@ -2616,29 +3471,32 @@ "Please restart the app and migrate the database to enable push notifications." = "Uruchom ponownie aplikację i przeprowadź migrację bazy danych, aby włączyć powiadomienia push."; /* No comment provided by engineer. */ -"Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Prosimy o bezpieczne przechowywanie hasła, w przypadku jego utraty NIE będzie można uzyskać dostępu do czatu."; +"Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Przechowuj kod dostępu w bezpieczny sposób, w przypadku jego utraty NIE będzie można uzyskać dostępu do czatu."; /* No comment provided by engineer. */ -"Please store passphrase securely, you will NOT be able to change it if you lose it." = "Prosimy o bezpieczne przechowywanie hasła, w przypadku jego utraty NIE będzie można go zmienić."; +"Please store passphrase securely, you will NOT be able to change it if you lose it." = "Przechowuj kod dostępu w bezpieczny sposób, w przypadku jego utraty NIE będzie można go zmienić."; /* No comment provided by engineer. */ "Polish interface" = "Polski interfejs"; +/* No comment provided by engineer. */ +"Port" = "Port"; + /* server test error */ "Possibly, certificate fingerprint in server address is incorrect" = "Możliwe, że odcisk palca certyfikatu w adresie serwera jest nieprawidłowy"; /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Zachowaj ostatnią wersję roboczą wiadomości wraz z załącznikami."; -/* No comment provided by engineer. */ -"Preset server" = "Wstępnie ustawiony serwer"; - /* No comment provided by engineer. */ "Preset server address" = "Wstępnie ustawiony adres serwera"; /* No comment provided by engineer. */ "Preview" = "Podgląd"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Wcześniej połączone serwery"; + /* No comment provided by engineer. */ "Privacy & security" = "Prywatność i bezpieczeństwo"; @@ -2648,6 +3506,21 @@ /* No comment provided by engineer. */ "Private filenames" = "Prywatne nazwy plików"; +/* No comment provided by engineer. */ +"Private message routing" = "Trasowanie prywatnych wiadomości"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Trasowanie prywatnych wiadomości🚀"; + +/* name of notes to self */ +"Private notes" = "Prywatne notatki"; + +/* No comment provided by engineer. */ +"Private routing" = "Prywatne trasowanie"; + +/* No comment provided by engineer. */ +"Private routing error" = "Błąd prywatnego trasowania"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profil i połączenia z serwerem"; @@ -2655,15 +3528,15 @@ "Profile image" = "Zdjęcie profilowe"; /* No comment provided by engineer. */ -"Profile name" = "Nazwa profilu"; - -/* No comment provided by engineer. */ -"Profile name:" = "Nazwa profilu:"; +"Profile images" = "Zdjęcia profilowe"; /* No comment provided by engineer. */ "Profile password" = "Hasło profilu"; /* 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. */ @@ -2687,38 +3560,71 @@ /* No comment provided by engineer. */ "Prohibit sending files and media." = "Zakaz wysyłania plików i mediów."; +/* No comment provided by engineer. */ +"Prohibit sending SimpleX links." = "Zabroń wysyłania linków SimpleX."; + /* No comment provided by engineer. */ "Prohibit sending voice messages." = "Zabroń wysyłania wiadomości głosowych."; /* No comment provided by engineer. */ "Protect app screen" = "Chroń ekran aplikacji"; +/* No comment provided by engineer. */ +"Protect IP address" = "Chroń adres IP"; + /* No comment provided by engineer. */ "Protect your chat profiles with a password!" = "Chroń swoje profile czatu hasłem!"; +/* No comment provided by engineer. */ +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Chroni Twój adres IP przed przekaźnikami wiadomości wybranych przez Twoje kontakty.\nWłącz w ustawianiach *Sieć i serwery* ."; + /* No comment provided by engineer. */ "Protocol timeout" = "Limit czasu protokołu"; /* No comment provided by engineer. */ "Protocol timeout per KB" = "Limit czasu protokołu na KB"; +/* No comment provided by engineer. */ +"Proxied" = "Trasowane przez proxy"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Serwery trasowane przez proxy"; + +/* No comment provided by engineer. */ +"Proxy requires password" = "Proxy wymaga hasła"; + /* No comment provided by engineer. */ "Push notifications" = "Powiadomienia push"; +/* No comment provided by engineer. */ +"Push server" = "Serwer Push"; + +/* chat item text */ +"quantum resistant e2e encryption" = "kwantowo odporne szyfrowanie e2e"; + +/* No comment provided by engineer. */ +"Quantum resistant encryption" = "Kwantowo odporne szyfrowanie"; + /* No comment provided by engineer. */ "Rate the app" = "Oceń aplikację"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Osiągalny pasek narzędzi czatu"; + /* chat item menu */ "React…" = "Reaguj…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Czytaj"; /* No comment provided by engineer. */ "Read more" = "Przeczytaj więcej"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Przeczytaj więcej w [Podręczniku Użytkownika](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"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)."; + +/* 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)."; @@ -2727,10 +3633,10 @@ "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)."; /* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Przeczytaj więcej na naszym repozytorium GitHub."; +"Receipts are disabled" = "Potwierdzenia są wyłączone"; /* No comment provided by engineer. */ -"Receipts are disabled" = "Potwierdzenia są wyłączone"; +"Receive errors" = "Błędy otrzymania"; /* No comment provided by engineer. */ "received answer…" = "otrzymano odpowiedź…"; @@ -2750,6 +3656,15 @@ /* message info title */ "Received message" = "Otrzymano wiadomość"; +/* No comment provided by engineer. */ +"Received messages" = "Otrzymane wiadomości"; + +/* No comment provided by engineer. */ +"Received reply" = "Otrzymano odpowiedź"; + +/* No comment provided by engineer. */ +"Received total" = "Otrzymano łącznie"; + /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Adres odbiorczy zostanie zmieniony na inny serwer. Zmiana adresu zostanie zakończona gdy nadawca będzie online."; @@ -2759,12 +3674,33 @@ /* No comment provided by engineer. */ "Receiving via" = "Odbieranie przez"; +/* No comment provided by engineer. */ +"Recent history and improved [directory bot](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)." = "Ostania historia i ulepszony [bot adresowy](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)."; + +/* No comment provided by engineer. */ +"Recipient(s) can't see who this message is from." = "Odbiorca/y nie mogą zobaczyć od kogo jest ta wiadomość."; + /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Odbiorcy widzą aktualizacje podczas ich wpisywania."; +/* No comment provided by engineer. */ +"Reconnect" = "Połącz ponownie"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Połącz ponownie wszystkie połączone serwery, aby wymusić dostarczanie wiadomości. Wykorzystuje dodatkowy ruch."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Połącz ponownie wszystkie serwery"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Połączyć ponownie wszystkie serwery?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Ponownie połącz z serwerem w celu wymuszenia dostarczenia wiadomości. Wykorzystuje to dodatkowy ruch."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Połączyć ponownie serwer?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Ponownie połączyć serwery?"; @@ -2777,7 +3713,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Zmniejszone zużycie baterii"; -/* reject incoming call via notification */ +/* reject incoming call via notification +swipe action */ "Reject" = "Odrzuć"; /* No comment provided by engineer. */ @@ -2798,6 +3735,12 @@ /* No comment provided by engineer. */ "Remove" = "Usuń"; +/* No comment provided by engineer. */ +"Remove archive?" = "Usunąć archiwum?"; + +/* No comment provided by engineer. */ +"Remove image" = "Usuń obraz"; + /* No comment provided by engineer. */ "Remove member" = "Usuń członka"; @@ -2813,6 +3756,12 @@ /* rcv group event chat item */ "removed %@" = "usunięto %@"; +/* profile update event chat item */ +"removed contact address" = "usunięto adres kontaktu"; + +/* profile update event chat item */ +"removed profile picture" = "usunięto zdjęcie profilu"; + /* rcv group event chat item */ "removed you" = "usunął cię"; @@ -2828,9 +3777,18 @@ /* No comment provided by engineer. */ "Repeat connection request?" = "Powtórzyć prośbę połączenia?"; +/* No comment provided by engineer. */ +"Repeat download" = "Powtórz pobieranie"; + +/* No comment provided by engineer. */ +"Repeat import" = "Powtórz importowanie"; + /* No comment provided by engineer. */ "Repeat join request?" = "Powtórzyć prośbę dołączenia?"; +/* No comment provided by engineer. */ +"Repeat upload" = "Powtórz wgrywanie"; + /* chat item action */ "Reply" = "Odpowiedz"; @@ -2840,12 +3798,27 @@ /* No comment provided by engineer. */ "Reset" = "Resetuj"; +/* No comment provided by engineer. */ +"Reset all hints" = "Zresetuj wszystkie wskazówki"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Resetuj wszystkie statystyki"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Zresetować wszystkie statystyki?"; + /* No comment provided by engineer. */ "Reset colors" = "Resetuj kolory"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Zresetuj do motywu aplikacji"; + /* No comment provided by engineer. */ "Reset to defaults" = "Przywróć wartości domyślne"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Zresetuj do motywu użytkownika"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Uruchom ponownie aplikację, aby utworzyć nowy profil czatu"; @@ -2864,12 +3837,12 @@ /* No comment provided by engineer. */ "Restore database error" = "Błąd przywracania bazy danych"; +/* No comment provided by engineer. */ +"Retry" = "Ponów"; + /* chat item action */ "Reveal" = "Ujawnij"; -/* No comment provided by engineer. */ -"Revert" = "Przywrócić"; - /* No comment provided by engineer. */ "Revoke" = "Odwołaj"; @@ -2885,27 +3858,31 @@ /* No comment provided by engineer. */ "Run chat" = "Uruchom czat"; -/* chat item action */ +/* No comment provided by engineer. */ +"Safely receive files" = "Bezpiecznie otrzymuj pliki"; + +/* No comment provided by engineer. */ +"Safer groups" = "Bezpieczniejsze grupy"; + +/* alert button +chat item action */ "Save" = "Zapisz"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Zapisz (i powiadom kontakty)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Zapisz i powiadom kontakt"; /* No comment provided by engineer. */ "Save and notify group members" = "Zapisz i powiadom członków grupy"; +/* No comment provided by engineer. */ +"Save and reconnect" = "Zapisz i połącz ponownie"; + /* No comment provided by engineer. */ "Save and update group profile" = "Zapisz i zaktualizuj profil grupowy"; -/* No comment provided by engineer. */ -"Save archive" = "Zapisz archiwum"; - -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Zapisz ustawienia automatycznej akceptacji"; - /* No comment provided by engineer. */ "Save group profile" = "Zapisz profil grupy"; @@ -2915,7 +3892,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Zapisz hasło w pęku kluczy"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Zapisać preferencje?"; /* No comment provided by engineer. */ @@ -2924,18 +3901,42 @@ /* No comment provided by engineer. */ "Save servers" = "Zapisz serwery"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Zapisać serwery?"; -/* No comment provided by engineer. */ -"Save settings?" = "Zapisać ustawienia?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Zapisać wiadomość powitalną?"; +/* alert title */ +"Save your profile?" = "Zapisać Twój profil?"; + +/* No comment provided by engineer. */ +"saved" = "zapisane"; + +/* No comment provided by engineer. */ +"Saved" = "Zapisane"; + +/* No comment provided by engineer. */ +"Saved from" = "Zapisane od"; + +/* No comment provided by engineer. */ +"saved from %@" = "zapisane od %@"; + +/* message info title */ +"Saved message" = "Zachowano wiadomość"; + /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Zapisane serwery WebRTC ICE zostaną usunięte"; +/* No comment provided by engineer. */ +"Saving %lld messages" = "Zapisywanie %lld wiadomości"; + +/* No comment provided by engineer. */ +"Scale" = "Skaluj"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Skanuj / Wklej link"; + /* No comment provided by engineer. */ "Scan code" = "Zeskanuj kod"; @@ -2951,12 +3952,24 @@ /* No comment provided by engineer. */ "Scan server QR code" = "Zeskanuj kod QR serwera"; +/* No comment provided by engineer. */ +"search" = "szukaj"; + /* No comment provided by engineer. */ "Search" = "Szukaj"; +/* No comment provided by engineer. */ +"Search bar accepts invitation links." = "Pasek wyszukiwania akceptuje linki zaproszenia."; + +/* No comment provided by engineer. */ +"Search or paste SimpleX link" = "Wyszukaj lub wklej link SimpleX"; + /* network option */ "sec" = "sek"; +/* No comment provided by engineer. */ +"Secondary" = "Drugorzędny"; + /* time unit */ "seconds" = "sekundy"; @@ -2966,6 +3979,9 @@ /* server test step */ "Secure queue" = "Bezpieczna kolejka"; +/* No comment provided by engineer. */ +"Secured" = "Zabezpieczone"; + /* No comment provided by engineer. */ "Security assessment" = "Ocena bezpieczeństwa"; @@ -2975,9 +3991,18 @@ /* chat item text */ "security code changed" = "kod bezpieczeństwa zmieniony"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Wybierz"; +/* No comment provided by engineer. */ +"Select chat profile" = "Wybierz profil czatu"; + +/* No comment provided by engineer. */ +"Selected %lld" = "Zaznaczono %lld"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Wybrane preferencje czatu zabraniają tej wiadomości."; + /* No comment provided by engineer. */ "Self-destruct" = "Samozniszczenie"; @@ -3002,15 +4027,15 @@ /* No comment provided by engineer. */ "send direct message" = "wyślij wiadomość bezpośrednią"; -/* No comment provided by engineer. */ -"Send direct message" = "Wyślij wiadomość bezpośrednią"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Wyślij wiadomość bezpośrednią aby połączyć"; /* No comment provided by engineer. */ "Send disappearing message" = "Wyślij znikającą wiadomość"; +/* No comment provided by engineer. */ +"Send errors" = "Wyślij błędy"; + /* No comment provided by engineer. */ "Send link previews" = "Wyślij podgląd linku"; @@ -3018,10 +4043,16 @@ "Send live message" = "Wyślij wiadomość na żywo"; /* No comment provided by engineer. */ -"Send notifications" = "Wyślij powiadomienia"; +"Send message to enable calls." = "Wyślij wiadomość aby włączyć połączenia."; /* No comment provided by engineer. */ -"Send notifications:" = "Wyślij powiadomienia:"; +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Wysyłaj wiadomości bezpośrednio, gdy adres IP jest chroniony i Twój lub docelowy serwer nie obsługuje prywatnego trasowania."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Wysyłaj wiadomości bezpośrednio, gdy Twój lub docelowy serwer nie obsługuje prywatnego trasowania."; + +/* No comment provided by engineer. */ +"Send notifications" = "Wyślij powiadomienia"; /* No comment provided by engineer. */ "Send questions and ideas" = "Wyślij pytania i pomysły"; @@ -3033,6 +4064,9 @@ "Send them from gallery or custom keyboards." = "Wyślij je z galerii lub niestandardowych klawiatur."; /* No comment provided by engineer. */ +"Send up to 100 last messages to new members." = "Wysyłaj do 100 ostatnich wiadomości do nowych członków."; + +/* alert message */ "Sender cancelled file transfer." = "Nadawca anulował transfer pliku."; /* No comment provided by engineer. */ @@ -3068,15 +4102,45 @@ /* copied message info */ "Sent at: %@" = "Wysłano o: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Wysłano bezpośrednio"; + /* notification */ "Sent file event" = "Wyślij zdarzenie pliku"; /* message info title */ "Sent message" = "Wyślij wiadomość"; +/* No comment provided by engineer. */ +"Sent messages" = "Wysłane wiadomości"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Wysłane wiadomości zostaną usunięte po ustawionym czasie."; +/* No comment provided by engineer. */ +"Sent reply" = "Wyślij odpowiedź"; + +/* No comment provided by engineer. */ +"Sent total" = "Wysłano łącznie"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Wysłano przez proxy"; + +/* No comment provided by engineer. */ +"Server" = "Serwer"; + +/* No comment provided by engineer. */ +"Server address" = "Adres serwera"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "Adres serwera jest niekompatybilny z ustawieniami sieci: %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "Adres serwera jest niekompatybilny z ustawieniami sieciowymi."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "Informacje kolejki serwera: %1$@\n\nostatnia otrzymana wiadomość: %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "Serwer wymaga autoryzacji do tworzenia kolejek, sprawdź hasło"; @@ -3086,9 +4150,24 @@ /* No comment provided by engineer. */ "Server test failed!" = "Test serwera nie powiódł się!"; +/* No comment provided by engineer. */ +"Server type" = "Typ serwera"; + +/* srv error text */ +"Server version is incompatible with network settings." = "Wersja serwera jest niekompatybilna z ustawieniami sieciowymi."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "Wersja serwera jest niekompatybilna z aplikacją: %@."; + /* No comment provided by engineer. */ "Servers" = "Serwery"; +/* No comment provided by engineer. */ +"Servers info" = "Informacje o serwerach"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Statystyki serwerów zostaną zresetowane - nie można tego cofnąć!"; + /* No comment provided by engineer. */ "Session code" = "Kod sesji"; @@ -3098,15 +4177,27 @@ /* No comment provided by engineer. */ "Set contact name…" = "Ustaw nazwę kontaktu…"; +/* No comment provided by engineer. */ +"Set default theme" = "Ustaw domyślny motyw"; + /* No comment provided by engineer. */ "Set group preferences" = "Ustaw preferencje grupy"; /* No comment provided by engineer. */ "Set it instead of system authentication." = "Ustaw go zamiast uwierzytelniania systemowego."; +/* profile update event chat item */ +"set new contact address" = "ustaw nowy adres kontaktu"; + +/* profile update event chat item */ +"set new profile picture" = "ustaw nowe zdjęcie profilu"; + /* No comment provided by engineer. */ "Set passcode" = "Ustaw pin"; +/* No comment provided by engineer. */ +"Set passphrase" = "Ustaw hasło"; + /* No comment provided by engineer. */ "Set passphrase to export" = "Ustaw hasło do eksportu"; @@ -3119,7 +4210,14 @@ /* No comment provided by engineer. */ "Settings" = "Ustawienia"; -/* chat item action */ +/* alert message */ +"Settings were changed." = "Ustawienia zostały zmienione."; + +/* No comment provided by engineer. */ +"Shape profile images" = "Kształtuj obrazy profilowe"; + +/* alert action +chat item action */ "Share" = "Udostępnij"; /* No comment provided by engineer. */ @@ -3128,18 +4226,30 @@ /* No comment provided by engineer. */ "Share address" = "Udostępnij adres"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "Udostępnić adres kontaktom?"; +/* No comment provided by engineer. */ +"Share from other apps." = "Udostępnij z innych aplikacji."; + /* No comment provided by engineer. */ "Share link" = "Udostępnij link"; /* No comment provided by engineer. */ -"Share one-time invitation link" = "Jednorazowy link zaproszenia"; +"Share profile" = "Udostępnij profil"; + +/* No comment provided by engineer. */ +"Share this 1-time invite link" = "Udostępnij ten jednorazowy link"; + +/* 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. */ +"Show → on messages sent via private routing." = "Pokaż → na wiadomościach wysłanych przez prywatne trasowanie."; + /* No comment provided by engineer. */ "Show calls in phone history" = "Pokaż połączenia w historii telefonu"; @@ -3149,12 +4259,24 @@ /* No comment provided by engineer. */ "Show last messages" = "Pokaż ostatnie wiadomości"; +/* No comment provided by engineer. */ +"Show message status" = "Pokaż status wiadomości"; + +/* No comment provided by engineer. */ +"Show percentage" = "Pokaż procent"; + /* No comment provided by engineer. */ "Show preview" = "Pokaż podgląd"; +/* No comment provided by engineer. */ +"Show QR code" = "Pokaż kod QR"; + /* No comment provided by engineer. */ "Show:" = "Pokaż:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "Adres SimpleX"; @@ -3173,9 +4295,15 @@ /* simplex link type */ "SimpleX group link" = "Link grupy SimpleX"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "Linki SimpleX"; +/* No comment provided by engineer. */ +"SimpleX links are prohibited." = "Linki SimpleX są zablokowane na tej grupie."; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "Linki SimpleX są niedozwolone"; + /* No comment provided by engineer. */ "SimpleX Lock" = "Blokada SimpleX"; @@ -3194,6 +4322,9 @@ /* No comment provided by engineer. */ "Simplified incognito mode" = "Uproszczony tryb incognito"; +/* No comment provided by engineer. */ +"Size" = "Rozmiar"; + /* No comment provided by engineer. */ "Skip" = "Pomiń"; @@ -3204,31 +4335,58 @@ "Small groups (max 20)" = "Małe grupy (maks. 20)"; /* No comment provided by engineer. */ -"SMP servers" = "Serwery SMP"; +"SMP server" = "Serwer SMP"; + +/* No comment provided by engineer. */ +"SOCKS proxy" = "Proxy SOCKS"; + +/* blur media */ +"Soft" = "Łagodny"; + +/* No comment provided by engineer. */ +"Some app settings were not migrated." = "Niektóre ustawienia aplikacji nie zostały zmigrowane."; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "Niektóre plik(i) nie zostały wyeksportowane:"; /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Podczas importu wystąpiły niekrytyczne błędy - więcej szczegółów można znaleźć w konsoli czatu."; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "Podczas importu wystąpiły niekrytyczne błędy:"; + /* notification title */ "Somebody" = "Ktoś"; /* No comment provided by engineer. */ -"Start a new chat" = "Rozpocznij nowy czat"; +"Square, circle, or anything in between." = "Kwadrat, okrąg lub cokolwiek pomiędzy."; + +/* chat item text */ +"standard end-to-end encryption" = "standardowe szyfrowanie end-to-end"; /* No comment provided by engineer. */ "Start chat" = "Rozpocznij czat"; +/* No comment provided by engineer. */ +"Start chat?" = "Rozpocząć czat?"; + /* No comment provided by engineer. */ "Start migration" = "Rozpocznij migrację"; +/* No comment provided by engineer. */ +"Starting from %@." = "Zaczynanie od %@."; + /* No comment provided by engineer. */ "starting…" = "uruchamianie…"; +/* No comment provided by engineer. */ +"Statistics" = "Statystyki"; + /* No comment provided by engineer. */ "Stop" = "Zatrzymaj"; /* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Zatrzymaj czat, aby umożliwić działania na bazie danych"; +"Stop chat" = "Zatrzymaj czat"; /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Zatrzymaj czat, aby wyeksportować, zaimportować lub usunąć bazę danych czatu. Podczas zatrzymania chatu nie będzie można odbierać ani wysyłać wiadomości."; @@ -3245,21 +4403,36 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Przestać wysyłać plik?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Przestań udostępniać"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Przestać udostępniać adres?"; /* authentication reason */ "Stop SimpleX" = "Zatrzymaj SimpleX"; +/* No comment provided by engineer. */ +"Stopping chat" = "Zatrzymywanie czatu"; + /* No comment provided by engineer. */ "strike" = "strajk"; +/* blur media */ +"Strong" = "Silne"; + /* No comment provided by engineer. */ "Submit" = "Zatwierdź"; +/* No comment provided by engineer. */ +"Subscribed" = "Zasubskrybowano"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Błędy subskrypcji"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Subskrypcje zignorowane"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Wspieraj SimpleX Chat"; @@ -3269,6 +4442,9 @@ /* No comment provided by engineer. */ "System authentication" = "Uwierzytelnianie systemu"; +/* No comment provided by engineer. */ +"Tail" = "Ogon"; + /* No comment provided by engineer. */ "Take picture" = "Zrób zdjęcie"; @@ -3288,7 +4464,13 @@ "Tap to join incognito" = "Dotnij, aby dołączyć w trybie incognito"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Dotknij, aby rozpocząć nowy czat"; +"Tap to paste link" = "Dotknij, aby wkleić link"; + +/* No comment provided by engineer. */ +"Tap to scan" = "Dotknij, aby zeskanować"; + +/* No comment provided by engineer. */ +"TCP connection" = "Połączenie TCP"; /* No comment provided by engineer. */ "TCP connection timeout" = "Limit czasu połączenia TCP"; @@ -3302,6 +4484,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* file error alert title */ +"Temporary file error" = "Tymczasowy błąd pliku"; + /* server test failure */ "Test failed at step %@." = "Test nie powiódł się na etapie %@."; @@ -3311,7 +4496,7 @@ /* No comment provided by engineer. */ "Test servers" = "Przetestuj serwery"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Testy nie powiodły się!"; /* No comment provided by engineer. */ @@ -3323,15 +4508,18 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Podziękowania dla użytkowników - wkład za pośrednictwem Weblate!"; -/* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Pierwsza platforma bez żadnych identyfikatorów użytkowników – z założenia prywatna."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Aplikacja może powiadamiać Cię, gdy otrzymujesz wiadomości lub prośby o kontakt — otwórz ustawienia, aby włączyć."; +/* 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)."; + /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "Próba zmiany hasła bazy danych nie została zakończona."; +/* No comment provided by engineer. */ +"The code you scanned is not a SimpleX link QR code." = "Kod, który zeskanowałeś nie jest kodem QR linku SimpleX."; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "Zaakceptowane przez Ciebie połączenie zostanie anulowane!"; @@ -3344,6 +4532,9 @@ /* 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."; @@ -3357,13 +4548,16 @@ "The message will be marked as moderated for all members." = "Wiadomość zostanie oznaczona jako moderowana dla wszystkich członków."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "Następna generacja prywatnych wiadomości"; +"The messages will be deleted for all members." = "Wiadomości zostaną usunięte dla wszystkich członków."; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "Wiadomości zostaną oznaczone jako moderowane dla wszystkich członków."; /* 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 profile is only shared with your contacts." = "Profil jest udostępniany tylko Twoim kontaktom."; +"Your profile is stored on your device and only shared with your contacts." = "Profil jest udostępniany tylko Twoim kontaktom."; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Drugi tik, który przegapiliśmy! ✅"; @@ -3375,13 +4569,13 @@ "The servers for new connections of your current chat profile **%@**." = "Serwery dla nowych połączeń bieżącego profilu czatu **%@**."; /* No comment provided by engineer. */ -"Theme" = "Motyw"; +"The text you pasted is not a SimpleX link." = "Tekst, który wkleiłeś nie jest linkiem SimpleX."; /* No comment provided by engineer. */ -"There should be at least one user profile." = "Powinien istnieć co najmniej jeden profil użytkownika."; +"The uploaded database archive will be permanently removed from the servers." = "Przesłane archiwum bazy danych zostanie trwale usunięte z serwerów."; /* No comment provided by engineer. */ -"There should be at least one visible user profile." = "Powinien istnieć co najmniej jeden widoczny profil użytkownika."; +"Themes" = "Motywy"; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Te ustawienia dotyczą Twojego bieżącego profilu **%@**."; @@ -3398,12 +4592,21 @@ /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Tego działania nie można cofnąć - Twój profil, kontakty, wiadomości i pliki zostaną nieodwracalnie utracone."; +/* E2EE info chat item */ +"This chat is protected by end-to-end encryption." = "Ten czat jest chroniony przez szyfrowanie end-to-end."; + +/* E2EE info chat item */ +"This chat is protected by quantum resistant end-to-end encryption." = "Ten czat jest chroniony przez kwantowo odporne szyfrowanie end-to-end."; + /* notification title */ "this contact" = "ten kontakt"; /* No comment provided by engineer. */ "This device name" = "Nazwa tego urządzenia"; +/* No comment provided by engineer. */ +"This display name is invalid. Please choose another name." = "Nazwa wyświetlana jest nieprawidłowa. Proszę wybrać inną nazwę."; + /* No comment provided by engineer. */ "This group has over %lld members, delivery receipts are not sent." = "Ta grupa ma ponad %lld członków, potwierdzenia dostawy nie są wysyłane."; @@ -3416,9 +4619,15 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "To jest twój własny adres SimpleX!"; +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Ten link dostał użyty z innym urządzeniem mobilnym, proszę stworzyć nowy link na komputerze."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "To ustawienie dotyczy wiadomości Twojego bieżącego profilu czatu **%@**."; +/* No comment provided by engineer. */ +"Title" = "Tytuł"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Aby zadać wszelkie pytania i otrzymywać aktualizacje:"; @@ -3431,15 +4640,24 @@ /* No comment provided by engineer. */ "To make a new connection" = "Aby nawiązać nowe połączenie"; -/* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Aby chronić prywatność, zamiast identyfikatorów użytkowników używanych przez wszystkie inne platformy, SimpleX ma identyfikatory dla kolejek wiadomości, oddzielne dla każdego z Twoich kontaktów."; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Aby chronić strefę czasową, pliki obrazów/głosów używają UTC."; /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Aby chronić swoje informacje, włącz funkcję blokady SimpleX.\nPrzed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania."; +/* No comment provided by engineer. */ +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "Aby chronić Twój adres IP, prywatne trasowanie używa Twoich serwerów SMP, aby dostarczyć wiadomości."; + +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Aby chronić prywatność, zamiast identyfikatorów użytkowników używanych przez wszystkie inne platformy, SimpleX ma identyfikatory dla kolejek wiadomości, oddzielne dla każdego z Twoich kontaktów."; + +/* No comment provided by engineer. */ +"To record speech please grant permission to use Microphone." = "Aby nagrać rozmowę, proszę zezwolić na użycie Mikrofonu."; + +/* No comment provided by engineer. */ +"To record video please grant permission to use Camera." = "Aby nagrać wideo, proszę zezwolić na użycie Aparatu."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Aby nagrać wiadomość głosową należy udzielić zgody na użycie Mikrofonu."; @@ -3452,12 +4670,24 @@ /* 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."; +/* No comment provided by engineer. */ +"Toolbar opacity" = "Nieprzezroczystość paska narzędzi"; + +/* No comment provided by engineer. */ +"Total" = "Łącznie"; + /* No comment provided by engineer. */ "Transport isolation" = "Izolacja transportu"; +/* No comment provided by engineer. */ +"Transport sessions" = "Sesje transportowe"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu (błąd: %@)."; @@ -3465,10 +4695,10 @@ "Trying to connect to the server used to receive messages from this contact." = "Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu."; /* No comment provided by engineer. */ -"Turn off" = "Wyłącz"; +"Turkish interface" = "Turecki interfejs"; /* No comment provided by engineer. */ -"Turn off notifications?" = "Wyłączyć powiadomienia?"; +"Turn off" = "Wyłącz"; /* No comment provided by engineer. */ "Turn on" = "Włącz"; @@ -3479,19 +4709,25 @@ /* No comment provided by engineer. */ "Unblock" = "Odblokuj"; +/* No comment provided by engineer. */ +"Unblock for all" = "Odblokuj dla wszystkich"; + /* No comment provided by engineer. */ "Unblock member" = "Odblokuj członka"; +/* No comment provided by engineer. */ +"Unblock member for all?" = "Odblokować członka dla wszystkich?"; + /* No comment provided by engineer. */ "Unblock member?" = "Odblokować członka?"; -/* item status description */ -"Unexpected error: %@" = "Nieoczekiwany błąd: %@"; +/* rcv group event chat item */ +"unblocked %@" = "odblokowano %@"; /* No comment provided by engineer. */ "Unexpected migration state" = "Nieoczekiwany stan migracji"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Nie ulub."; /* No comment provided by engineer. */ @@ -3518,6 +4754,15 @@ /* No comment provided by engineer. */ "Unknown error" = "Nieznany błąd"; +/* No comment provided by engineer. */ +"unknown servers" = "nieznane przekaźniki"; + +/* alert title */ +"Unknown servers!" = "Nieznane serwery!"; + +/* No comment provided by engineer. */ +"unknown status" = "nieznany status"; + /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "O ile nie korzystasz z interfejsu połączeń systemu iOS, włącz tryb Nie przeszkadzać, aby uniknąć przerywania."; @@ -3536,17 +4781,20 @@ /* authentication reason */ "Unlock app" = "Odblokuj aplikację"; -/* No comment provided by engineer. */ +/* notification label action */ "Unmute" = "Wyłącz wyciszenie"; /* No comment provided by engineer. */ +"unprotected" = "niezabezpieczony"; + +/* swipe action */ "Unread" = "Nieprzeczytane"; /* No comment provided by engineer. */ -"Update" = "Aktualizuj"; +"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. */ -"Update .onion hosts setting?" = "Zaktualizować ustawienie hostów .onion?"; +"Update" = "Aktualizuj"; /* No comment provided by engineer. */ "Update database passphrase" = "Aktualizuj hasło do bazy danych"; @@ -3555,23 +4803,38 @@ "Update network settings?" = "Zaktualizować ustawienia sieci?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Zaktualizować tryb izolacji transportu?"; +"Update settings?" = "Zaktualizować ustawienia?"; /* rcv group event chat item */ "updated group profile" = "zaktualizowano profil grupy"; +/* profile update event chat item */ +"updated profile" = "zaktualizowano profil"; + /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "Aktualizacja ustawień spowoduje ponowne połączenie klienta ze wszystkimi serwerami."; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Aktualizacja tych ustawień spowoduje ponowne połączenie klienta ze wszystkimi serwerami."; +"Upgrade and open chat" = "Zaktualizuj i otwórz czat"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "Zaktualizuj i otwórz czat"; +"Upload errors" = "Błędy przesłania"; + +/* No comment provided by engineer. */ +"Upload failed" = "Wgrywanie nie udane"; /* server test step */ "Upload file" = "Prześlij plik"; +/* No comment provided by engineer. */ +"Uploaded" = "Przesłane"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Przesłane pliki"; + +/* No comment provided by engineer. */ +"Uploading archive" = "Wgrywanie archiwum"; + /* No comment provided by engineer. */ "Use .onion hosts" = "Użyj hostów .onion"; @@ -3593,6 +4856,15 @@ /* No comment provided by engineer. */ "Use new incognito profile" = "Użyj nowego profilu incognito"; +/* No comment provided by engineer. */ +"Use only local notifications?" = "Używać tylko lokalnych powiadomień?"; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "Używaj prywatnego trasowania z nieznanymi serwerami, gdy adres IP nie jest chroniony."; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "Używaj prywatnego trasowania z nieznanymi serwerami."; + /* No comment provided by engineer. */ "Use server" = "Użyj serwera"; @@ -3600,10 +4872,19 @@ "Use SimpleX Chat servers?" = "Użyć serwerów SimpleX Chat?"; /* No comment provided by engineer. */ -"User profile" = "Profil użytkownika"; +"Use SOCKS proxy" = "Użyj proxy SOCKS"; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Używanie hostów .onion wymaga kompatybilnego dostawcy VPN."; +"Use the app while in the call." = "Używaj aplikacji podczas połączenia."; + +/* No comment provided by engineer. */ +"Use the app with one hand." = "Korzystaj z aplikacji jedną ręką."; + +/* No comment provided by engineer. */ +"User selection" = "Wybór użytkownika"; + +/* No comment provided by engineer. */ +"Username" = "Nazwa użytkownika"; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Używanie serwerów SimpleX Chat."; @@ -3626,6 +4907,12 @@ /* No comment provided by engineer. */ "Verify connections" = "Zweryfikuj połączenia"; +/* No comment provided by engineer. */ +"Verify database passphrase" = "Zweryfikuj hasło bazy danych"; + +/* No comment provided by engineer. */ +"Verify passphrase" = "Zweryfikuj hasło"; + /* No comment provided by engineer. */ "Verify security code" = "Weryfikuj kod bezpieczeństwa"; @@ -3647,6 +4934,9 @@ /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Dzięki bezpiecznemu protokołowi odpornego kwantowo."; +/* No comment provided by engineer. */ +"video" = "wideo"; + /* No comment provided by engineer. */ "Video call" = "Połączenie wideo"; @@ -3665,6 +4955,9 @@ /* No comment provided by engineer. */ "View security code" = "Pokaż kod bezpieczeństwa"; +/* chat feature */ +"Visible history" = "Widoczna historia"; + /* No comment provided by engineer. */ "Voice message…" = "Wiadomość głosowa…"; @@ -3675,7 +4968,10 @@ "Voice messages are prohibited in this chat." = "Wiadomości głosowe są zabronione na tym czacie."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Wiadomości głosowe są zabronione w tej grupie."; +"Voice messages are prohibited." = "Wiadomości głosowe są zabronione w tej grupie."; + +/* No comment provided by engineer. */ +"Voice messages not allowed" = "Wiadomości głosowe są niedozwolone"; /* No comment provided by engineer. */ "Voice messages prohibited!" = "Wiadomości głosowe zabronione!"; @@ -3698,9 +4994,18 @@ /* No comment provided by engineer. */ "Waiting for video" = "Oczekiwanie na film"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Akcent tapety"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Tło tapety"; + /* No comment provided by engineer. */ "wants to connect to you!" = "chce się z Tobą połączyć!"; +/* No comment provided by engineer. */ +"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "Ostrzeżenie: rozpoczęcie czatu na wielu urządzeniach nie jest wspierane i spowoduje niepowodzenia dostarczania wiadomości"; + /* No comment provided by engineer. */ "Warning: you may lose some data!" = "Uwaga: możesz stracić niektóre dane!"; @@ -3716,6 +5021,9 @@ /* No comment provided by engineer. */ "Welcome message" = "Wiadomość powitalna"; +/* No comment provided by engineer. */ +"Welcome message is too long" = "Wiadomość powitalna jest zbyt długa"; + /* No comment provided by engineer. */ "What's new" = "Co nowego"; @@ -3723,28 +5031,61 @@ "When available" = "Gdy dostępny"; /* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Kiedy ludzie proszą o połączenie, możesz je zaakceptować lub odrzucić."; +"When connecting audio and video calls." = "Podczas łączenia połączeń audio i wideo."; + +/* No comment provided by engineer. */ +"when IP hidden" = "gdy IP ukryty"; /* 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." = "Gdy udostępnisz komuś profil incognito, będzie on używany w grupach, do których Cię zaprosi."; +/* No comment provided by engineer. */ +"WiFi" = "WiFi"; + +/* No comment provided by engineer. */ +"Will be enabled in direct chats!" = "Zostanie włączone w czatach bezpośrednich!"; + +/* No comment provided by engineer. */ +"Wired ethernet" = "Połączenie ethernet (po kablu)"; + +/* No comment provided by engineer. */ +"With encrypted files and media." = "Z zaszyfrowanymi plikami i multimediami."; + /* No comment provided by engineer. */ "With optional welcome message." = "Z opcjonalną wiadomością powitalną."; +/* No comment provided by engineer. */ +"With reduced battery usage." = "Ze zmniejszonym zużyciem baterii."; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "Bez Tor lub VPN, Twój adres IP będzie widoczny do serwerów plików."; + +/* alert message */ +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Bez Tor lub VPN, Twój adres IP będzie widoczny dla tych przekaźników XFTP: %@."; + /* No comment provided by engineer. */ "Wrong database passphrase" = "Nieprawidłowe hasło bazy danych"; +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "Zły klucz lub nieznane połączenie - najprawdopodobniej to połączenie jest usunięte."; + +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Zły klucz lub nieznany adres fragmentu pliku - najprawdopodobniej plik został usunięty."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Nieprawidłowe hasło!"; /* No comment provided by engineer. */ -"XFTP servers" = "Serwery XFTP"; +"XFTP server" = "Serwer XFTP"; /* pref value */ "yes" = "tak"; /* No comment provided by engineer. */ -"You" = "Ty"; +"you" = "Ty"; + +/* No comment provided by engineer. */ +"You **must not** use the same database on two devices." = "**Nie możesz** używać tej samej bazy na dwóch urządzeniach."; /* No comment provided by engineer. */ "You accepted connection" = "Zaakceptowałeś połączenie"; @@ -3788,14 +5129,20 @@ /* No comment provided by engineer. */ "You are invited to group" = "Jesteś zaproszony do grupy"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Nie jesteś połączony z tymi serwerami. Prywatne trasowanie jest używane do dostarczania do nich wiadomości."; + /* No comment provided by engineer. */ "you are observer" = "jesteś obserwatorem"; +/* snd group event chat item */ +"you blocked %@" = "zablokowałeś %@"; + /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Możesz przyjmować połączenia z ekranu blokady, bez uwierzytelniania urządzenia i aplikacji."; /* No comment provided by engineer. */ -"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "Możesz też połączyć się klikając w link. Jeśli otworzy się on w przeglądarce, kliknij przycisk **Otwórz w aplikacji mobilnej**."; +"You can change it in Appearance settings." = "Możesz to zmienić w ustawieniach wyglądu."; /* No comment provided by engineer. */ "You can create it later" = "Możesz go utworzyć później"; @@ -3806,11 +5153,20 @@ /* No comment provided by engineer. */ "You can enable them later via app Privacy & Security settings." = "Możesz je włączyć później w ustawieniach Prywatności i Bezpieczeństwa aplikacji."; +/* No comment provided by engineer. */ +"You can give another try." = "Możesz spróbować ponownie."; + /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "Możesz ukryć lub wyciszyć profil użytkownika - przesuń palcem w prawo."; +/* No comment provided by engineer. */ +"You can make it visible to your SimpleX contacts via Settings." = "Możesz ustawić go jako widoczny dla swoich kontaktów SimpleX w Ustawieniach."; + /* notification body */ -"You can now send messages to %@" = "Możesz teraz wysyłać wiadomości do %@"; +"You can now chat with %@" = "Możesz teraz wysyłać wiadomości do %@"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "Możesz wysyłać wiadomości do %@ ze zarchiwizowanych kontaktów."; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Podgląd powiadomień na ekranie blokady można ustawić w ustawieniach."; @@ -3822,10 +5178,10 @@ "You can share this address with your contacts to let them connect with **%@**." = "Możesz udostępnić ten adres Twoim kontaktom, aby umożliwić im połączenie z **%@**."; /* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Możesz udostępnić swój adres jako link lub jako kod QR - każdy będzie mógł się z Tobą połączyć."; +"You can start chat via app Settings / Database or by restarting the app" = "Możesz rozpocząć czat poprzez Ustawienia aplikacji / Baza danych lub poprzez ponowne uruchomienie aplikacji"; /* No comment provided by engineer. */ -"You can start chat via app Settings / Database or by restarting the app" = "Możesz rozpocząć czat poprzez Ustawienia aplikacji / Baza danych lub poprzez ponowne uruchomienie aplikacji"; +"You can still view conversation with %@ in the list of chats." = "Nadal możesz przeglądać rozmowę z %@ na liście czatów."; /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Możesz włączyć blokadę SimpleX poprzez Ustawienia."; @@ -3833,6 +5189,9 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Możesz używać markdown do formatowania wiadomości:"; +/* alert message */ +"You can view invitation link again in connection details." = "Możesz zobaczyć link zaproszenia ponownie w szczegółach połączenia."; + /* No comment provided by engineer. */ "You can't send messages!" = "Nie możesz wysyłać wiadomości!"; @@ -3849,10 +5208,10 @@ "you changed role of %@ to %@" = "zmieniłeś rolę %1$@ na %2$@"; /* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Kontrolujesz przez który serwer(y) **odbierać** wiadomości, Twoje kontakty - serwery, których używasz do wysyłania im wiadomości."; +"You could not be verified; please try again." = "Nie można zweryfikować użytkownika; proszę spróbować ponownie."; /* No comment provided by engineer. */ -"You could not be verified; please try again." = "Nie można zweryfikować użytkownika; proszę spróbować ponownie."; +"You decide who can connect." = "Ty decydujesz, kto może się połączyć."; /* No comment provided by engineer. */ "You have already requested connection via this address!" = "Już prosiłeś o połączenie na ten adres!"; @@ -3860,9 +5219,6 @@ /* No comment provided by engineer. */ "You have already requested connection!\nRepeat connection request?" = "Już prosiłeś o połączenie!\nPowtórzyć prośbę połączenia?"; -/* No comment provided by engineer. */ -"You have no chats" = "Nie masz czatów"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Musisz wprowadzić hasło przy każdym uruchomieniu aplikacji - nie jest one przechowywane na urządzeniu."; @@ -3878,9 +5234,18 @@ /* snd group event chat item */ "you left" = "wyszedłeś"; +/* No comment provided by engineer. */ +"You may migrate the exported database." = "Możesz zmigrować wyeksportowaną bazy danych."; + +/* No comment provided by engineer. */ +"You may save the exported archive." = "Możesz zapisać wyeksportowane archiwum."; + /* 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." = "Musisz używać najnowszej wersji bazy danych czatu TYLKO na jednym urządzeniu, w przeciwnym razie możesz przestać otrzymywać wiadomości od niektórych kontaktów."; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "Aby móc dzwonić, musisz zezwolić kontaktowi na połączenia."; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "Musisz zezwolić Twojemu kontaktowi na wysyłanie wiadomości głosowych, aby móc je wysyłać."; @@ -3899,6 +5264,9 @@ /* chat list item description */ "you shared one-time link incognito" = "udostępniłeś jednorazowy link incognito"; +/* snd group event chat item */ +"you unblocked %@" = "odblokowałeś %@"; + /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "Zostaniesz połączony do grupy, gdy urządzenie gospodarza grupy będzie online, proszę czekać lub sprawdzić później!"; @@ -3935,9 +5303,6 @@ /* 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" = "Używasz profilu incognito dla tej grupy - aby zapobiec udostępnianiu głównego profilu zapraszanie kontaktów jest zabronione"; -/* No comment provided by engineer. */ -"Your %@ servers" = "Twoje serwery %@"; - /* No comment provided by engineer. */ "Your calls" = "Twoje połączenia"; @@ -3947,11 +5312,14 @@ /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Baza danych czatu nie jest szyfrowana - ustaw hasło, aby ją zaszyfrować."; +/* alert title */ +"Your chat preferences" = "Twoje preferencje czatu"; + /* No comment provided by engineer. */ "Your chat profiles" = "Twoje profile czatu"; /* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Twój kontakt musi być online, aby połączenie zostało zakończone.\nMożesz anulować to połączenie i usunąć kontakt (i spróbować później z nowym linkiem)."; +"Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Twoje połączenie zostało przeniesione do %@, ale podczas przekierowywania do profilu wystąpił nieoczekiwany błąd."; /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Twój kontakt wysłał plik, który jest większy niż obecnie obsługiwany maksymalny rozmiar (%@)."; @@ -3960,10 +5328,10 @@ "Your contacts can allow full message deletion." = "Twoje kontakty mogą zezwolić na pełne usunięcie wiadomości."; /* No comment provided by engineer. */ -"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "Twoje kontakty w SimpleX będą to widzieć.\nMożesz to zmienić w Ustawieniach."; +"Your contacts will remain connected." = "Twoje kontakty pozostaną połączone."; /* No comment provided by engineer. */ -"Your contacts will remain connected." = "Twoje kontakty pozostaną połączone."; +"Your credentials may be sent unencrypted." = "Twoje poświadczenia mogą zostać wysłane niezaszyfrowane."; /* No comment provided by engineer. */ "Your current chat database will be DELETED and REPLACED with the imported one." = "Twoja obecna baza danych czatu zostanie usunięta i zastąpiona zaimportowaną."; @@ -3987,7 +5355,10 @@ "Your profile **%@** will be shared." = "Twój profil **%@** zostanie udostępniony."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Twój profil jest przechowywany na urządzeniu i udostępniany tylko Twoim kontaktom.\nSerwery SimpleX nie mogą zobaczyć Twojego profilu."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Twój profil jest przechowywany na urządzeniu i udostępniany tylko Twoim kontaktom. Serwery SimpleX nie mogą zobaczyć Twojego profilu."; + +/* alert message */ +"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Twój profil został zmieniony. Jeśli go zapiszesz, zaktualizowany profil zostanie wysłany do wszystkich kontaktów."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Twój profil, kontakty i dostarczone wiadomości są przechowywane na Twoim urządzeniu."; @@ -3996,10 +5367,10 @@ "Your random profile" = "Twój losowy profil"; /* No comment provided by engineer. */ -"Your server" = "Twój serwer"; +"Your server address" = "Twój adres serwera"; /* No comment provided by engineer. */ -"Your server address" = "Twój adres serwera"; +"Your servers" = "Twoje serwery"; /* No comment provided by engineer. */ "Your settings" = "Twoje ustawienia"; @@ -4007,9 +5378,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "Twój adres SimpleX"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Twoje serwery SMP"; - -/* No comment provided by engineer. */ -"Your XFTP servers" = "Twoje serwery XFTP"; - diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 03e4c4c508..cb837836ff 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -1,18 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (можно скопировать)"; @@ -31,30 +16,15 @@ /* No comment provided by engineer. */ "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- голосовые сообщения до 5 минут.\n- настройка времени исчезающих сообщений.\n- история редактирования."; -/* No comment provided by engineer. */ -", " = ", "; - -/* No comment provided by engineer. */ -": " = ": "; - /* No comment provided by engineer. */ "!1 colored!" = "!1 цвет!"; -/* No comment provided by engineer. */ -"." = "."; - -/* No comment provided by engineer. */ -"(" = "("; - /* No comment provided by engineer. */ "(new)" = "(новое)"; /* No comment provided by engineer. */ "(this device v%@)" = "(это устройство v%@)"; -/* No comment provided by engineer. */ -")" = ")"; - /* No comment provided by engineer. */ "[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Внести свой вклад](https://github.com/simplex-chat/simplex-chat#contribute)"; @@ -65,10 +35,10 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Поставить звездочку в GitHub](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Добавить новый контакт**: чтобы создать одноразовый QR код или ссылку для Вашего контакта."; +"**Create 1-time link**: to create and share a new invitation link." = "**Добавить контакт**: создать и поделиться новой ссылкой-приглашением."; /* No comment provided by engineer. */ -"**Create link / QR code** for your contact to use." = "**Создать ссылку / QR код** для Вашего контакта."; +"**Create group**: to create a new group." = "**Создать группу**: создать новую группу."; /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "**e2e зашифрованный** аудиозвонок"; @@ -77,26 +47,29 @@ "**e2e encrypted** video call" = "**e2e зашифрованный** видеозвонок"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Более конфиденциально**: проверять новые сообщения каждые 20 минут. Токен устройства будет отправлен на сервер уведомлений SimpleX Chat, но у сервера не будет информации о количестве контактов и сообщений."; +"**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." = "**Более конфиденциально**: проверять новые сообщения каждые 20 минут. Только токен устройства будет отправлен на сервер уведомлений SimpleX Chat, но у сервера не будет информации о количестве контактов и какой либо информации о сообщениях."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Самый конфиденциальный**: не использовать сервер уведомлений SimpleX Chat, проверять сообщения периодически в фоновом режиме (зависит от того насколько часто Вы используете приложение)."; +"**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." = "**Самый конфиденциальный**: не использовать сервер уведомлений SimpleX Chat. Сообщения проверяются в фоновом режиме, когда система позволяет, в зависимости от того, как часто Вы используете приложение."; /* No comment provided by engineer. */ -"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Вставить полученную ссылку**, или откройте её в браузере и нажмите **Open in mobile app**."; +"**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." = "**Внимание**: Вы не сможете восстановить или поменять пароль, если Вы его потеряете."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Рекомендовано**: токен устройства и уведомления отправляются на сервер SimpleX Chat, но сервер не получает сами сообщения, их размер или от кого они."; +"**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, но сервер не получает сами сообщения, их размер или от кого они."; /* No comment provided by engineer. */ -"**Scan QR code**: to connect to your contact in person or via video call." = "**Сканировать QR код**: соединиться с Вашим контактом при встрече или во время видеозвонка."; +"**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."; +/* No comment provided by engineer. */ +"**Warning**: the archive will be removed." = "**Внимание**: архив будет удален."; + /* No comment provided by engineer. */ "*bold*" = "\\*жирный*"; @@ -139,6 +112,9 @@ /* No comment provided by engineer. */ "%@ connected" = "%@ соединен(а)"; +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ загружено"; + /* notification title */ "%@ is connected!" = "Установлено соединение с %@!"; @@ -148,17 +124,26 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ подтверждён"; +/* No comment provided by engineer. */ +"%@ server" = "%@ сервер"; + /* No comment provided by engineer. */ "%@ servers" = "%@ серверы"; +/* No comment provided by engineer. */ +"%@ uploaded" = "%@ загружено"; + /* notification title */ "%@ wants to connect!" = "%@ хочет соединиться!"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + /* No comment provided by engineer. */ "%@, %@ and %lld members" = "%@, %@ и %lld членов группы"; /* No comment provided by engineer. */ -"%@, %@ and %lld other members connected" = "%@, %@ и %lld других членов соединены"; +"%@, %@ and %lld other members connected" = "установлено соединение с %@, %@ и %lld другими членами группы"; /* copied message info */ "%@:" = "%@:"; @@ -166,9 +151,24 @@ /* time interval */ "%d days" = "%d дней"; +/* forward confirmation reason */ +"%d file(s) are still being downloaded." = "%d файл(ов) загружаются."; + +/* forward confirmation reason */ +"%d file(s) failed to download." = "%d файл(ов) не удалось загрузить."; + +/* forward confirmation reason */ +"%d file(s) were deleted." = "%d файлов было удалено."; + +/* forward confirmation reason */ +"%d file(s) were not downloaded." = "%d файлов не было загружено."; + /* time interval */ "%d hours" = "%d ч."; +/* alert title */ +"%d messages not forwarded" = "%d сообщений не переслано"; + /* time interval */ "%d min" = "%d мин"; @@ -178,6 +178,9 @@ /* time interval */ "%d sec" = "%d сек"; +/* delete after time */ +"%d seconds(s)" = "%d секунд"; + /* integrity error chat item */ "%d skipped message(s)" = "%d пропущенных сообщение(й)"; @@ -200,11 +203,14 @@ "%lld group events" = "%lld событий"; /* No comment provided by engineer. */ -"%lld members" = "Членов группы: %lld"; +"%lld members" = "%lld членов"; /* No comment provided by engineer. */ "%lld messages blocked" = "%lld сообщений заблокировано"; +/* No comment provided by engineer. */ +"%lld messages blocked by admin" = "%lld сообщений заблокировано администратором"; + /* No comment provided by engineer. */ "%lld messages marked deleted" = "%lld сообщений помечено удалёнными"; @@ -217,9 +223,6 @@ /* No comment provided by engineer. */ "%lld new interface languages" = "%lld новых языков интерфейса"; -/* No comment provided by engineer. */ -"%lld second(s)" = "%lld секунд"; - /* No comment provided by engineer. */ "%lld seconds" = "%lld секунд"; @@ -265,7 +268,8 @@ /* No comment provided by engineer. */ "0s" = "0с"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 день"; /* time interval */ @@ -274,15 +278,23 @@ /* No comment provided by engineer. */ "1 minute" = "1 минута"; -/* time interval */ +/* delete after time +time interval */ "1 month" = "1 месяц"; -/* time interval */ +/* delete after time +time interval */ "1 week" = "1 неделю"; +/* delete after time */ +"1 year" = "1 год"; + /* No comment provided by engineer. */ "1-time link" = "Одноразовая ссылка"; +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Одноразовая ссылка может быть использована *только с одним контактом* - поделитесь при встрече или через любой мессенджер."; + /* No comment provided by engineer. */ "5 minutes" = "5 минут"; @@ -305,7 +317,7 @@ "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-соединение (и авторизация SOCKS) будет использоваться **для каждого контакта и члена группы**.\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" = "Прекратить"; @@ -317,10 +329,7 @@ "Abort changing address?" = "Прекратить изменение адреса?"; /* No comment provided by engineer. */ -"About SimpleX" = "О SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "Об адресе SimpleX"; +"About operators" = "Об операторах"; /* No comment provided by engineer. */ "About SimpleX Chat" = "Информация о SimpleX Chat"; @@ -329,77 +338,157 @@ "above, then choose:" = "наверху, затем выберите:"; /* No comment provided by engineer. */ -"Accent color" = "Основной цвет"; +"Accent" = "Акцент"; /* accept contact request via notification - accept incoming call via notification */ +accept incoming call via notification +swipe action */ "Accept" = "Принять"; +/* No comment provided by engineer. */ +"Accept conditions" = "Принять условия"; + /* No comment provided by engineer. */ "Accept connection request?" = "Принять запрос?"; /* notification body */ "Accept contact request from %@?" = "Принять запрос на соединение от %@?"; -/* accept contact request via notification */ +/* accept contact request via notification +swipe action */ "Accept incognito" = "Принять инкогнито"; /* call status */ "accepted call" = "принятый звонок"; +/* No comment provided by engineer. */ +"Accepted conditions" = "Принятые условия"; + +/* chat list item title */ +"accepted invitation" = "принятое приглашение"; + +/* No comment provided by engineer. */ +"Acknowledged" = "Подтверждено"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Ошибки подтверждения"; + +/* token status text */ +"Active" = "Активный"; + +/* 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 preset servers" = "Добавить серверы по умолчанию"; +"Add friends" = "Добавить друзей"; + +/* No comment provided by engineer. */ +"Add list" = "Добавить список"; /* No comment provided by engineer. */ "Add profile" = "Добавить профиль"; /* No comment provided by engineer. */ -"Add server…" = "Добавить сервер…"; +"Add server" = "Добавить сервер"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Добавить серверы через QR код."; +/* No comment provided by engineer. */ +"Add team members" = "Добавить сотрудников"; + /* No comment provided by engineer. */ "Add to another device" = "Добавить на другое устройство"; +/* No comment provided by engineer. */ +"Add to list" = "Добавить в список"; + /* No comment provided by engineer. */ "Add welcome message" = "Добавить приветственное сообщение"; +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "Добавьте сотрудников в разговор."; + +/* No comment provided by engineer. */ +"Added media & file servers" = "Дополнительные серверы файлов и медиа"; + +/* No comment provided by engineer. */ +"Added message servers" = "Дополнительные серверы сообщений"; + +/* No comment provided by engineer. */ +"Additional accent" = "Дополнительный акцент"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Дополнительный акцент 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Вторичный 2"; + /* No comment provided by engineer. */ "Address" = "Адрес"; /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "Изменение адреса будет прекращено. Будет использоваться старый адрес."; +/* No comment provided by engineer. */ +"Address or 1-time link?" = "Адрес или одноразовая ссылка?"; + +/* No comment provided by engineer. */ +"Address settings" = "Настройки адреса"; + /* member role */ "admin" = "админ"; +/* feature role */ +"admins" = "админы"; + +/* No comment provided by engineer. */ +"Admins can block a member for all." = "Админы могут заблокировать члена группы."; + /* No comment provided by engineer. */ "Admins can create the links to join groups." = "Админы могут создать ссылки для вступления в группу."; /* No comment provided by engineer. */ "Advanced network settings" = "Настройки сети"; +/* No comment provided by engineer. */ +"Advanced settings" = "Настройки сети"; + /* chat item text */ "agreeing encryption for %@…" = "шифрование согласовывается для %@…"; /* chat item text */ "agreeing encryption…" = "шифрование согласовывается…"; +/* No comment provided by engineer. */ +"All" = "Все"; + /* No comment provided by engineer. */ "All app data is deleted." = "Все данные приложения будут удалены."; /* No comment provided by engineer. */ "All chats and messages will be deleted - this cannot be undone!" = "Все чаты и сообщения будут удалены - это нельзя отменить!"; +/* alert message */ +"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." = "Все данные удаляются при его вводе."; /* No comment provided by engineer. */ -"All group members will remain connected." = "Все члены группы, которые соединились через эту ссылку, останутся в группе."; +"All data is kept private on your device." = "Все данные хранятся только на вашем устройстве."; + +/* No comment provided by engineer. */ +"All group members will remain connected." = "Все члены группы останутся соединены."; + +/* 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. */ +"All messages will be deleted - this cannot be undone!" = "Все сообщения будут удалены - это нельзя отменить!"; /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Все сообщения будут удалены - это действие нельзя отменить! Сообщения будут удалены только для Вас."; @@ -407,23 +496,38 @@ /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Все новые сообщения от %@ будут скрыты!"; +/* profile dropdown */ +"All profiles" = "Все профили"; + +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Все сообщения о нарушениях будут заархивированы для вас."; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Все контакты, которые соединились через этот адрес, сохранятся."; /* No comment provided by engineer. */ "All your contacts will remain connected. Profile update will be sent to your contacts." = "Все Ваши контакты сохранятся. Обновленный профиль будет отправлен Вашим контактам."; +/* No comment provided by engineer. */ +"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" = "Разрешить"; /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Разрешить звонки, только если их разрешает Ваш контакт."; +/* No comment provided by engineer. */ +"Allow calls?" = "Разрешить звонки?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "Разрешить исчезающие сообщения, только если Ваш контакт разрешает их Вам."; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you." = "Разрешить необратимое удаление сообщений, только если Ваш контакт разрешает это Вам."; +"Allow downgrade" = "Разрешить прямую доставку"; + +/* 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 message reactions only if your contact allows them." = "Разрешить реакции на сообщения, только если ваш контакт разрешает их."; @@ -432,17 +536,26 @@ "Allow message reactions." = "Разрешить реакции на сообщения."; /* No comment provided by engineer. */ -"Allow sending direct messages to members." = "Разрешить посылать прямые сообщения членам группы."; +"Allow sending direct messages to members." = "Разрешить личные сообщения членам группы."; /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Разрешить посылать исчезающие сообщения."; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages." = "Разрешить необратимо удалять отправленные сообщения."; +"Allow sharing" = "Разрешить поделиться"; + +/* No comment provided by engineer. */ +"Allow to irreversibly delete sent messages. (24 hours)" = "Разрешить необратимо удалять отправленные сообщения. (24 часа)"; + +/* No comment provided by engineer. */ +"Allow to report messsages to moderators." = "Разрешить отправлять сообщения о нарушениях модераторам."; /* No comment provided by engineer. */ "Allow to send files and media." = "Разрешить посылать файлы и медиа."; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "Разрешить отправлять ссылки SimpleX."; + /* No comment provided by engineer. */ "Allow to send voice messages." = "Разрешить отправлять голосовые сообщения."; @@ -459,7 +572,7 @@ "Allow your contacts to call you." = "Разрешить Вашим контактам звонить Вам."; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages." = "Разрешить Вашим контактам необратимо удалять отправленные сообщения."; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Разрешить Вашим контактам необратимо удалять отправленные сообщения. (24 часа)"; /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Разрешить Вашим контактам отправлять исчезающие сообщения."; @@ -479,6 +592,9 @@ /* pref value */ "always" = "всегда"; +/* No comment provided by engineer. */ +"Always use private routing." = "Всегда использовать конфиденциальную доставку."; + /* No comment provided by engineer. */ "Always use relay" = "Всегда соединяться через relay"; @@ -488,15 +604,27 @@ /* No comment provided by engineer. */ "and %lld other events" = "и %lld других событий"; +/* report reason */ +"Another reason" = "Другая причина"; + /* No comment provided by engineer. */ "Answer call" = "Принять звонок"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "Кто угодно может запустить сервер."; + /* No comment provided by engineer. */ "App build: %@" = "Сборка приложения: %@"; +/* No comment provided by engineer. */ +"App data migration" = "Миграция данных"; + /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "Приложение шифрует новые локальные файлы (кроме видео)."; +/* No comment provided by engineer. */ +"App group:" = "Группа приложения:"; + /* No comment provided by engineer. */ "App icon" = "Иконка"; @@ -506,6 +634,9 @@ /* No comment provided by engineer. */ "App passcode is replaced with self-destruct passcode." = "Код доступа в приложение будет заменен кодом самоуничтожения."; +/* No comment provided by engineer. */ +"App session" = "Сессия приложения"; + /* No comment provided by engineer. */ "App version" = "Версия приложения"; @@ -515,9 +646,51 @@ /* No comment provided by engineer. */ "Appearance" = "Интерфейс"; +/* No comment provided by engineer. */ +"Apply" = "Применить"; + +/* No comment provided by engineer. */ +"Apply to" = "Применить к"; + +/* No comment provided by engineer. */ +"Archive" = "Архивировать"; + +/* No comment provided by engineer. */ +"Archive %lld reports?" = "Архивировать %lld сообщений о нарушениях?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "Архивировать все сообщения о нарушениях?"; + +/* No comment provided by engineer. */ +"Archive and upload" = "Архивировать и загрузить"; + +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Архивируйте контакты чтобы продолжить переписку."; + +/* No comment provided by engineer. */ +"Archive report" = "Архивировать сообщение о нарушении"; + +/* No comment provided by engineer. */ +"Archive report?" = "Архивировать сообщение о нарушении?"; + +/* swipe action */ +"Archive reports" = "Архивировать сообщения о нарушениях"; + +/* No comment provided by engineer. */ +"Archived contacts" = "Архивированные контакты"; + +/* No comment provided by engineer. */ +"archived report" = "заархивированное сообщение о нарушении"; + +/* No comment provided by engineer. */ +"Archiving database" = "Подготовка архива"; + /* No comment provided by engineer. */ "Attach" = "Прикрепить"; +/* No comment provided by engineer. */ +"attempts" = "попытки"; + /* No comment provided by engineer. */ "Audio & video calls" = "Аудио- и видеозвонки"; @@ -557,9 +730,15 @@ /* No comment provided by engineer. */ "Auto-accept images" = "Автоприем изображений"; +/* alert title */ +"Auto-accept settings" = "Настройки автоприема"; + /* No comment provided by engineer. */ "Back" = "Назад"; +/* No comment provided by engineer. */ +"Background" = "Фон"; + /* No comment provided by engineer. */ "Bad desktop address" = "Неверный адрес компьютера"; @@ -575,27 +754,76 @@ /* No comment provided by engineer. */ "Bad message ID" = "Ошибка ID сообщения"; +/* No comment provided by engineer. */ +"Better calls" = "Улучшенные звонки"; + /* No comment provided by engineer. */ "Better groups" = "Улучшенные группы"; +/* No comment provided by engineer. */ +"Better groups performance" = "Улучшенная производительность групп"; + +/* No comment provided by engineer. */ +"Better message dates." = "Улучшенные даты сообщений."; + /* No comment provided by engineer. */ "Better messages" = "Улучшенные сообщения"; +/* No comment provided by engineer. */ +"Better networking" = "Улучшенные сетевые функции"; + +/* No comment provided by engineer. */ +"Better notifications" = "Улучшенные уведомления"; + +/* No comment provided by engineer. */ +"Better privacy and security" = "Улучшенная конфиденциальность и безопасность"; + +/* No comment provided by engineer. */ +"Better security ✅" = "Улучшенная безопасность ✅"; + +/* No comment provided by engineer. */ +"Better user experience" = "Улучшенный интерфейс"; + +/* No comment provided by engineer. */ +"Black" = "Черная"; + /* No comment provided by engineer. */ "Block" = "Заблокировать"; /* No comment provided by engineer. */ -"Block group members" = "Блокируйте членов группы"; +"Block for all" = "Заблокировать для всех"; + +/* No comment provided by engineer. */ +"Block group members" = "Заблокировать членов группы"; /* No comment provided by engineer. */ "Block member" = "Заблокировать члена группы"; /* No comment provided by engineer. */ -"Block member?" = "Заблокировать члена группы?"; +"Block member for all?" = "Заблокировать для всех?"; /* No comment provided by engineer. */ +"Block member?" = "Заблокировать члена группы?"; + +/* marked deleted chat item preview text */ "blocked" = "заблокировано"; +/* rcv group event chat item */ +"blocked %@" = "%@ заблокирован"; + +/* blocked chat item +marked deleted chat item preview text */ +"blocked by admin" = "заблокировано администратором"; + +/* No comment provided by engineer. */ +"Blocked by admin" = "Заблокирован администратором"; + +/* No comment provided by engineer. */ +"Blur for better privacy." = "Размыть для конфиденциальности."; + +/* No comment provided by engineer. */ +"Blur media" = "Размытие изображений"; + /* No comment provided by engineer. */ "bold" = "жирный"; @@ -603,7 +831,7 @@ "Both you and your contact can add message reactions." = "И Вы, и Ваш контакт можете добавлять реакции на сообщения."; /* No comment provided by engineer. */ -"Both you and your contact can irreversibly delete sent messages." = "Вы и Ваш контакт можете необратимо удалять отправленные сообщения."; +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "Вы и Ваш контакт можете необратимо удалять отправленные сообщения. (24 часа)"; /* No comment provided by engineer. */ "Both you and your contact can make calls." = "Вы и Ваш контакт можете совершать звонки."; @@ -617,9 +845,24 @@ /* 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. */ +"Business address" = "Бизнес адрес"; + +/* No comment provided by engineer. */ +"Business chats" = "Бизнес разговоры"; + +/* No comment provided by engineer. */ +"Businesses" = "Бизнесы"; + /* 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!" = "Звонок уже завершен!"; @@ -636,7 +879,16 @@ "Calls" = "Звонки"; /* No comment provided by engineer. */ -"Can't delete user profile!" = "Нельзя удалить профиль пользователя!"; +"Calls prohibited!" = "Звонки запрещены!"; + +/* No comment provided by engineer. */ +"Camera not available" = "Камера недоступна"; + +/* No comment provided by engineer. */ +"Can't call contact" = "Не удается позвонить контакту"; + +/* No comment provided by engineer. */ +"Can't call member" = "Не удаётся позвонить члену группы"; /* No comment provided by engineer. */ "Can't invite contact!" = "Нельзя пригласить контакт!"; @@ -645,8 +897,15 @@ "Can't invite contacts!" = "Нельзя пригласить контакты!"; /* No comment provided by engineer. */ +"Can't message member" = "Не удаётся отправить сообщение члену группы"; + +/* alert action +alert button */ "Cancel" = "Отменить"; +/* No comment provided by engineer. */ +"Cancel migration" = "Отменить миграцию"; + /* feature offered item */ "cancelled %@" = "отменил(a) %@"; @@ -654,11 +913,26 @@ "Cannot access keychain to save database password" = "Ошибка доступа к Keychain при сохранении пароля"; /* No comment provided by engineer. */ +"Cannot forward message" = "Невозможно переслать сообщение"; + +/* alert title */ "Cannot receive file" = "Невозможно получить файл"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Превышено количество сообщений - предыдущие сообщения не доставлены."; + +/* No comment provided by engineer. */ +"Cellular" = "Мобильная сеть"; + /* No comment provided by engineer. */ "Change" = "Поменять"; +/* alert title */ +"Change automatic message deletion?" = "Измененить автоматическое удаление сообщений?"; + +/* authentication reason */ +"Change chat profiles" = "Поменять профили"; + /* No comment provided by engineer. */ "Change database passphrase?" = "Поменять пароль базы данных?"; @@ -684,7 +958,7 @@ "Change self-destruct mode" = "Изменить режим самоуничтожения"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Изменить код самоуничтожения"; /* chat item text */ @@ -703,7 +977,16 @@ "changing address…" = "смена адреса…"; /* No comment provided by engineer. */ -"Chat archive" = "Архив чата"; +"Chat" = "Разговор"; + +/* No comment provided by engineer. */ +"Chat already exists" = "Разговор уже существует"; + +/* No comment provided by engineer. */ +"Chat already exists!" = "Разговор уже существует!"; + +/* No comment provided by engineer. */ +"Chat colors" = "Цвета чата"; /* No comment provided by engineer. */ "Chat console" = "Консоль"; @@ -714,6 +997,9 @@ /* No comment provided by engineer. */ "Chat database deleted" = "Данные чата удалены"; +/* No comment provided by engineer. */ +"Chat database exported" = "Данные чата экспортированы"; + /* No comment provided by engineer. */ "Chat database imported" = "Архив чата импортирован"; @@ -723,18 +1009,51 @@ /* No comment provided by engineer. */ "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." = "Чат остановлен. Если вы уже использовали эту базу данных на другом устройстве, перенесите ее обратно до запуска чата."; + +/* No comment provided by engineer. */ +"Chat list" = "Список чатов"; + +/* No comment provided by engineer. */ +"Chat migrated!" = "Чат мигрирован!"; + /* No comment provided by engineer. */ "Chat preferences" = "Предпочтения"; +/* alert message */ +"Chat preferences were changed." = "Настройки чата были изменены."; + +/* No comment provided by engineer. */ +"Chat profile" = "Профиль чата"; + +/* No comment provided by engineer. */ +"Chat theme" = "Тема чата"; + +/* No comment provided by engineer. */ +"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. */ "Chats" = "Чаты"; /* No comment provided by engineer. */ +"Check messages every 20 min." = "Проверять сообщения каждые 20 минут."; + +/* No comment provided by engineer. */ +"Check messages when allowed." = "Проверять сообщения по возможности."; + +/* alert title */ "Check server address and try again." = "Проверьте адрес сервера и попробуйте снова."; /* No comment provided by engineer. */ "Chinese and Spanish interface" = "Китайский и Испанский интерфейс"; +/* No comment provided by engineer. */ +"Choose _Migrate from another device_ on the new device and scan QR code." = "Выберите _Мигрировать с другого устройства_ на новом устройстве и сосканируйте QR код."; + /* No comment provided by engineer. */ "Choose file" = "Выбрать файл"; @@ -742,6 +1061,15 @@ "Choose from library" = "Выбрать из библиотеки"; /* No comment provided by engineer. */ +"Chunks deleted" = "Блоков удалено"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Блоков принято"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Блоков загружено"; + +/* swipe action */ "Clear" = "Очистить"; /* No comment provided by engineer. */ @@ -750,14 +1078,29 @@ /* No comment provided by engineer. */ "Clear conversation?" = "Очистить разговор?"; +/* No comment provided by engineer. */ +"Clear group?" = "Очистить группу?"; + +/* No comment provided by engineer. */ +"Clear or delete group?" = "Очистить или удалить группу?"; + +/* No comment provided by engineer. */ +"Clear private notes?" = "Очистить личные заметки?"; + /* No comment provided by engineer. */ "Clear verification" = "Сбросить подтверждение"; +/* No comment provided by engineer. */ +"Color chats with the new themes." = "Добавьте цвета к чатам в настройках."; + +/* No comment provided by engineer. */ +"Color mode" = "Режим цветов"; + /* No comment provided by engineer. */ "colored" = "цвет"; -/* No comment provided by engineer. */ -"Colors" = "Цвета"; +/* report reason */ +"Community guidelines violation" = "Нарушение правил группы"; /* server test step */ "Compare file" = "Сравнение файла"; @@ -768,15 +1111,51 @@ /* No comment provided by engineer. */ "complete" = "соединение завершено"; +/* No comment provided by engineer. */ +"Completed" = "Готово"; + +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "Условия приняты: %@."; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "Условия приняты для оператора(ов): **%@**."; + +/* No comment provided by engineer. */ +"Conditions are already accepted for these operator(s): **%@**." = "Условия уже приняты для следующих оператора(ов): **%@**."; + +/* No comment provided by engineer. */ +"Conditions of use" = "Условия использования"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "Условия будут приняты для оператора(ов): **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "Условия будут приняты: %@."; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "Условия будут автоматически приняты для включенных операторов: %@."; + /* No comment provided by engineer. */ "Configure ICE servers" = "Настройка ICE серверов"; +/* No comment provided by engineer. */ +"Configure server operators" = "Настроить операторов серверов"; + /* No comment provided by engineer. */ "Confirm" = "Подтвердить"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "Потвердить удаление контакта?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "Подтвердить обновление базы данных"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Подтверждать файлы с неизвестных серверов."; + +/* No comment provided by engineer. */ +"Confirm network settings" = "Подтвердите настройки сети"; + /* No comment provided by engineer. */ "Confirm new passphrase…" = "Подтвердите новый пароль…"; @@ -786,6 +1165,15 @@ /* No comment provided by engineer. */ "Confirm password" = "Подтвердить пароль"; +/* No comment provided by engineer. */ +"Confirm that you remember database passphrase to migrate it." = "Подтвердите, что Вы помните пароль базы данных для ее миграции."; + +/* No comment provided by engineer. */ +"Confirm upload" = "Подтвердить загрузку"; + +/* token status text */ +"Confirmed" = "Подтвержденный"; + /* server test step */ "Connect" = "Соединиться"; @@ -801,6 +1189,9 @@ /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "соединитесь с разработчиками."; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Соединяйтесь с друзьями быстрее."; + /* No comment provided by engineer. */ "Connect to yourself?" = "Соединиться с самим собой?"; @@ -816,9 +1207,6 @@ /* No comment provided by engineer. */ "Connect via link" = "Соединиться через ссылку"; -/* No comment provided by engineer. */ -"Connect via link / QR code" = "Соединиться через ссылку / QR код"; - /* No comment provided by engineer. */ "Connect via one-time link" = "Соединиться через одноразовую ссылку"; @@ -828,18 +1216,27 @@ /* No comment provided by engineer. */ "connected" = "соединение установлено"; +/* No comment provided by engineer. */ +"Connected" = "Соединено"; + /* No comment provided by engineer. */ "Connected desktop" = "Подключенный компьютер"; /* rcv group event chat item */ "connected directly" = "соединен(а) напрямую"; +/* No comment provided by engineer. */ +"Connected servers" = "Подключенные серверы"; + /* No comment provided by engineer. */ "Connected to desktop" = "Компьютер подключен"; /* No comment provided by engineer. */ "connecting" = "соединяется"; +/* No comment provided by engineer. */ +"Connecting" = "Соединяется"; + /* No comment provided by engineer. */ "connecting (accepted)" = "соединяется (приглашение принято)"; @@ -861,15 +1258,24 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Устанавливается соединение с сервером… (ошибка: %@)"; +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "Контакт соединяется, подождите или проверьте позже!"; + /* No comment provided by engineer. */ "Connecting to desktop" = "Подключение к компьютеру"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "соединяется…"; /* No comment provided by engineer. */ "Connection" = "Соединение"; +/* No comment provided by engineer. */ +"Connection and servers status." = "Состояние соединения и серверов."; + +/* No comment provided by engineer. */ +"Connection blocked" = "Соединение заблокировано"; + /* No comment provided by engineer. */ "Connection error" = "Ошибка соединения"; @@ -879,24 +1285,51 @@ /* chat list item title (it should not be shown */ "connection established" = "соединение установлено"; +/* No comment provided by engineer. */ +"Connection is blocked by server operator:\n%@" = "Соединение заблокировано сервером оператора:\n%@"; + +/* No comment provided by engineer. */ +"Connection not ready." = "Соединение не готово."; + +/* No comment provided by engineer. */ +"Connection notifications" = "Уведомления по соединениям"; + /* No comment provided by engineer. */ "Connection request sent!" = "Запрос на соединение отправлен!"; +/* No comment provided by engineer. */ +"Connection requires encryption renegotiation." = "Соединение требует повторного согласования шифрования."; + +/* No comment provided by engineer. */ +"Connection security" = "Безопасность соединения"; + /* No comment provided by engineer. */ "Connection terminated" = "Подключение прервано"; /* No comment provided by engineer. */ "Connection timeout" = "Превышено время соединения"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "Соединение с компьютером остановлено"; + /* connection information */ "connection:%@" = "connection:%@"; +/* No comment provided by engineer. */ +"Connections" = "Соединения"; + +/* profile update event chat item */ +"contact %@ changed to %@" = "контакт %1$@ изменён на %2$@"; + /* No comment provided by engineer. */ "Contact allows" = "Контакт разрешает"; /* No comment provided by engineer. */ "Contact already exists" = "Существующий контакт"; +/* No comment provided by engineer. */ +"Contact deleted!" = "Контакт удален!"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "у контакта есть e2e шифрование"; @@ -910,7 +1343,7 @@ "Contact is connected" = "Соединение с контактом установлено"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "Соединение еще не установлено!"; +"Contact is deleted." = "Контакт удален."; /* No comment provided by engineer. */ "Contact name" = "Имена контактов"; @@ -918,21 +1351,36 @@ /* No comment provided by engineer. */ "Contact preferences" = "Предпочтения контакта"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "Контакт будет удален — это нельзя отменить!"; + /* No comment provided by engineer. */ "Contacts" = "Контакты"; /* No comment provided by engineer. */ "Contacts can mark messages for deletion; you will be able to view them." = "Контакты могут помечать сообщения для удаления; Вы сможете просмотреть их."; +/* blocking reason */ +"Content violates conditions of use" = "Содержание нарушает условия использования"; + /* No comment provided by engineer. */ "Continue" = "Продолжить"; -/* chat item action */ +/* 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. */ "Core version: v%@" = "Версия ядра: v%@"; +/* No comment provided by engineer. */ +"Corner" = "Угол"; + /* No comment provided by engineer. */ "Correct name to %@?" = "Исправить имя на %@?"; @@ -940,10 +1388,10 @@ "Create" = "Создать"; /* No comment provided by engineer. */ -"Create a group using a random profile." = "Создайте группу, используя случайный профиль."; +"Create 1-time link" = "Создать одноразовую ссылку"; /* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Создайте адрес, чтобы можно было соединиться с вами."; +"Create a group using a random profile." = "Создайте группу, используя случайный профиль."; /* server test step */ "Create file" = "Создание файла"; @@ -958,10 +1406,10 @@ "Create link" = "Создать ссылку"; /* No comment provided by engineer. */ -"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Создайте новый профиль в [приложении для компьютера](https://simplex.chat/downloads/). 💻"; +"Create list" = "Создать список"; /* No comment provided by engineer. */ -"Create one-time invitation link" = "Создать ссылку-приглашение"; +"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Создайте новый профиль в [приложении для компьютера](https://simplex.chat/downloads/). 💻"; /* No comment provided by engineer. */ "Create profile" = "Создать профиль"; @@ -979,17 +1427,35 @@ "Create your profile" = "Создать профиль"; /* No comment provided by engineer. */ -"Created on %@" = "Дата создания %@"; +"Created" = "Создано"; + +/* No comment provided by engineer. */ +"Created at" = "Создано"; + +/* copied message info */ +"Created at: %@" = "Создано: %@"; + +/* No comment provided by engineer. */ +"Creating archive link" = "Создание ссылки на архив"; + +/* No comment provided by engineer. */ +"Creating link…" = "Создаётся ссылка…"; /* No comment provided by engineer. */ "creator" = "создатель"; +/* No comment provided by engineer. */ +"Current conditions text couldn't be loaded, you can review conditions via this link:" = "Текст условий использования не может быть показан, вы можете посмотреть их через ссылку:"; + /* No comment provided by engineer. */ "Current Passcode" = "Текущий Код"; /* No comment provided by engineer. */ "Current passphrase…" = "Текущий пароль…"; +/* No comment provided by engineer. */ +"Current profile" = "Текущий профиль"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Максимальный размер файла - %@."; @@ -999,9 +1465,18 @@ /* No comment provided by engineer. */ "Custom time" = "Пользовательское время"; +/* No comment provided by engineer. */ +"Customizable message shape." = "Настраиваемая форма сообщений."; + +/* No comment provided by engineer. */ +"Customize theme" = "Настроить тему"; + /* No comment provided by engineer. */ "Dark" = "Тёмная"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Цвета тёмного режима"; + /* No comment provided by engineer. */ "Database downgrade" = "Откат базы данных"; @@ -1062,13 +1537,20 @@ /* time unit */ "days" = "дней"; +/* No comment provided by engineer. */ +"Debug delivery" = "Отладка доставки"; + /* No comment provided by engineer. */ "Decentralized" = "Децентрализованный"; /* message decrypt error item */ "Decryption error" = "Ошибка расшифровки"; -/* pref value */ +/* No comment provided by engineer. */ +"decryption errors" = "ошибки расшифровки"; + +/* delete after time +pref value */ "default (%@)" = "по умолчанию (%@)"; /* No comment provided by engineer. */ @@ -1077,9 +1559,13 @@ /* No comment provided by engineer. */ "default (yes)" = "по умолчанию (да)"; -/* chat item action */ +/* alert action +swipe action */ "Delete" = "Удалить"; +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "Удалить %lld сообщений членов группы?"; + /* No comment provided by engineer. */ "Delete %lld messages?" = "Удалить %lld сообщений?"; @@ -1099,10 +1585,10 @@ "Delete and notify contact" = "Удалить и уведомить контакт"; /* No comment provided by engineer. */ -"Delete archive" = "Удалить архив"; +"Delete chat" = "Удалить разговор"; /* No comment provided by engineer. */ -"Delete chat archive?" = "Удалить архив чата?"; +"Delete chat messages from your device." = "Удалить сообщения с вашего устройства."; /* No comment provided by engineer. */ "Delete chat profile" = "Удалить профиль чата"; @@ -1110,6 +1596,9 @@ /* No comment provided by engineer. */ "Delete chat profile?" = "Удалить профиль?"; +/* No comment provided by engineer. */ +"Delete chat?" = "Удалить разговор?"; + /* No comment provided by engineer. */ "Delete connection" = "Удалить соединение"; @@ -1117,14 +1606,14 @@ "Delete contact" = "Удалить контакт"; /* No comment provided by engineer. */ -"Delete Contact" = "Удалить контакт"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Удалить контакт?\nЭто не может быть отменено!"; +"Delete contact?" = "Удалить контакт?"; /* No comment provided by engineer. */ "Delete database" = "Удалить данные чата"; +/* No comment provided by engineer. */ +"Delete database from this device" = "Удалить базу данных с этого устройства"; + /* server test step */ "Delete file" = "Удалить файл"; @@ -1155,13 +1644,16 @@ /* No comment provided by engineer. */ "Delete link?" = "Удалить ссылку?"; +/* alert title */ +"Delete list?" = "Удалить список?"; + /* No comment provided by engineer. */ "Delete member message?" = "Удалить сообщение участника?"; /* No comment provided by engineer. */ "Delete message?" = "Удалить сообщение?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "Удалить сообщения"; /* No comment provided by engineer. */ @@ -1174,7 +1666,7 @@ "Delete old database?" = "Удалить предыдущую версию данных?"; /* No comment provided by engineer. */ -"Delete pending connection" = "Удалить соединение"; +"Delete or moderate up to 200 messages." = "Удаляйте или модерируйте до 200 сообщений."; /* No comment provided by engineer. */ "Delete pending connection?" = "Удалить ожидаемое соединение?"; @@ -1185,12 +1677,24 @@ /* server test step */ "Delete queue" = "Удаление очереди"; +/* No comment provided by engineer. */ +"Delete report" = "Удалить сообщение о нарушении"; + +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Удаляйте до 20 сообщений за раз."; + /* No comment provided by engineer. */ "Delete user profile?" = "Удалить профиль пользователя?"; +/* No comment provided by engineer. */ +"Delete without notification" = "Удалить без уведомления"; + /* deleted chat item */ "deleted" = "удалено"; +/* No comment provided by engineer. */ +"Deleted" = "Удалено"; + /* No comment provided by engineer. */ "Deleted at" = "Удалено"; @@ -1203,6 +1707,12 @@ /* rcv group event chat item */ "deleted group" = "удалил(а) группу"; +/* No comment provided by engineer. */ +"Deletion errors" = "Ошибки удаления"; + +/* No comment provided by engineer. */ +"Delivered even when Apple drops them." = "Доставляются даже тогда, когда Apple их теряет."; + /* No comment provided by engineer. */ "Delivery" = "Доставка"; @@ -1224,9 +1734,27 @@ /* No comment provided by engineer. */ "Desktop devices" = "Компьютеры"; +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "Адрес сервера назначения %@ несовместим с настройками пересылающего сервера %@."; + +/* snd error text */ +"Destination server error: %@" = "Ошибка сервера получателя: %@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "Версия сервера назначения %@ несовместима с пересылающим сервером %@."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Подробная статистика"; + +/* No comment provided by engineer. */ +"Details" = "Подробности"; + /* No comment provided by engineer. */ "Develop" = "Для разработчиков"; +/* No comment provided by engineer. */ +"Developer options" = "Опции разработчика"; + /* No comment provided by engineer. */ "Developer tools" = "Инструменты разработчика"; @@ -1252,11 +1780,17 @@ "Direct messages" = "Прямые сообщения"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Прямые сообщения между членами группы запрещены."; +"Direct messages between members are prohibited in this chat." = "Личные сообщения запрещены в этой группе."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Выключить (кроме исключений)"; +/* alert title */ +"Disable automatic message deletion?" = "Отключить автоматическое удаление сообщений?"; + +/* alert button */ +"Disable delete messages" = "Отключить удаление сообщений"; + /* No comment provided by engineer. */ "Disable for all" = "Выключить для всех"; @@ -1266,6 +1800,9 @@ /* No comment provided by engineer. */ "disabled" = "выключено"; +/* No comment provided by engineer. */ +"Disabled" = "Выключено"; + /* No comment provided by engineer. */ "Disappearing message" = "Исчезающее сообщение"; @@ -1276,7 +1813,7 @@ "Disappearing messages are prohibited in this chat." = "Исчезающие сообщения запрещены в этом чате."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Исчезающие сообщения запрещены в этой группе."; +"Disappearing messages are prohibited." = "Исчезающие сообщения запрещены в этой группе."; /* No comment provided by engineer. */ "Disappears at" = "Исчезает"; @@ -1299,36 +1836,85 @@ /* No comment provided by engineer. */ "Do it later" = "Отложить"; +/* 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." = "Не использовать учетные данные с прокси."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "Не использовать конфиденциальную доставку."; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "Не используйте SimpleX для экстренных звонков."; +/* No comment provided by engineer. */ +"Documents:" = "Документы:"; + /* No comment provided by engineer. */ "Don't create address" = "Не создавать адрес"; /* No comment provided by engineer. */ "Don't enable" = "Не включать"; +/* No comment provided by engineer. */ +"Don't miss important messages." = "Не пропустите важные сообщения."; + /* No comment provided by engineer. */ "Don't show again" = "Не показывать"; +/* No comment provided by engineer. */ +"Done" = "Готово"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "Откатить версию и открыть чат"; +/* alert button +chat item action */ +"Download" = "Загрузить"; + +/* No comment provided by engineer. */ +"Download errors" = "Ошибки приема"; + +/* No comment provided by engineer. */ +"Download failed" = "Ошибка загрузки"; + /* server test step */ "Download file" = "Загрузка файла"; +/* alert action */ +"Download files" = "Загрузить файлы"; + +/* No comment provided by engineer. */ +"Downloaded" = "Принято"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Принятые файлы"; + +/* No comment provided by engineer. */ +"Downloading archive" = "Загрузка архива"; + +/* No comment provided by engineer. */ +"Downloading link details" = "Загрузка ссылки архива"; + /* No comment provided by engineer. */ "Duplicate display name!" = "Имя профиля уже используется!"; /* integrity error chat item */ "duplicate message" = "повторное сообщение"; +/* No comment provided by engineer. */ +"duplicates" = "дубликаты"; + /* No comment provided by engineer. */ "Duration" = "Длительность"; /* No comment provided by engineer. */ "e2e encrypted" = "e2e зашифровано"; +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "E2E зашифрованные нотификации."; + /* chat item action */ "Edit" = "Редактировать"; @@ -1341,12 +1927,18 @@ /* No comment provided by engineer. */ "Enable (keep overrides)" = "Включить (кроме исключений)"; -/* No comment provided by engineer. */ +/* alert title */ "Enable automatic message deletion?" = "Включить автоматическое удаление сообщений?"; +/* No comment provided by engineer. */ +"Enable camera access" = "Включить доступ к камере"; + /* No comment provided by engineer. */ "Enable for all" = "Включить для всех"; +/* No comment provided by engineer. */ +"Enable in direct chats (BETA)!" = "Включите для контактов (BETA)!"; + /* No comment provided by engineer. */ "Enable instant notifications?" = "Включить мгновенные уведомления?"; @@ -1374,6 +1966,12 @@ /* enabled status */ "enabled" = "включено"; +/* No comment provided by engineer. */ +"Enabled" = "Включено"; + +/* No comment provided by engineer. */ +"Enabled for" = "Включено для"; + /* enabled status */ "enabled for contact" = "включено для контакта"; @@ -1398,6 +1996,9 @@ /* notification */ "Encrypted message or another event" = "Зашифрованное сообщение или событие чата"; +/* notification */ +"Encrypted message: app is stopped" = "Зашифрованное сообщение: приложение остановлено"; + /* notification */ "Encrypted message: database error" = "Зашифрованное сообщение: ошибка базы данных"; @@ -1443,6 +2044,9 @@ /* chat item text */ "encryption re-negotiation required for %@" = "требуется новое соглашение о шифровании для %@"; +/* No comment provided by engineer. */ +"Encryption renegotiation in progress." = "Выполняется повторное согласование шифрования."; + /* No comment provided by engineer. */ "ended" = "завершён"; @@ -1458,6 +2062,9 @@ /* No comment provided by engineer. */ "Enter Passcode" = "Введите Код"; +/* No comment provided by engineer. */ +"Enter passphrase" = "Введите пароль"; + /* No comment provided by engineer. */ "Enter passphrase…" = "Введите пароль…"; @@ -1488,24 +2095,36 @@ /* No comment provided by engineer. */ "Error aborting address change" = "Ошибка при прекращении изменения адреса"; +/* alert title */ +"Error accepting conditions" = "Ошибка приема условий"; + /* No comment provided by engineer. */ "Error accepting contact request" = "Ошибка при принятии запроса на соединение"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Ошибка при доступе к данным чата"; - -/* No comment provided by engineer. */ -"Error adding member(s)" = "Ошибка при добавлении членов группы"; +/* alert title */ +"Error adding server" = "Ошибка добавления сервера"; /* No comment provided by engineer. */ "Error changing address" = "Ошибка при изменении адреса"; +/* No comment provided by engineer. */ +"Error changing connection profile" = "Ошибка при изменении профиля соединения"; + /* No comment provided by engineer. */ "Error changing role" = "Ошибка при изменении роли"; /* No comment provided by engineer. */ "Error changing setting" = "Ошибка при изменении настройки"; +/* No comment provided by engineer. */ +"Error changing to incognito!" = "Ошибка при смене на Инкогнито!"; + +/* No comment provided by engineer. */ +"Error checking token status" = "Ошибка проверки статуса токена"; + +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "Ошибка подключения к пересылающему серверу %@. Попробуйте позже."; + /* No comment provided by engineer. */ "Error creating address" = "Ошибка при создании адреса"; @@ -1515,12 +2134,18 @@ /* No comment provided by engineer. */ "Error creating group link" = "Ошибка при создании ссылки группы"; +/* alert title */ +"Error creating list" = "Ошибка создания списка"; + /* No comment provided by engineer. */ -"Error creating member contact" = "Ошибка создания контакта с членом группы"; +"Error creating message" = "Ошибка создания сообщения"; /* No comment provided by engineer. */ "Error creating profile!" = "Ошибка создания профиля!"; +/* No comment provided by engineer. */ +"Error creating report" = "Ошибка создания сообщения о нарушении"; + /* No comment provided by engineer. */ "Error decrypting file" = "Ошибка расшифровки файла"; @@ -1533,9 +2158,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Ошибка при удалении соединения"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Ошибка при удалении контакта"; - /* No comment provided by engineer. */ "Error deleting database" = "Ошибка при удалении данных чата"; @@ -1548,6 +2170,9 @@ /* No comment provided by engineer. */ "Error deleting user profile" = "Ошибка удаления профиля пользователя"; +/* No comment provided by engineer. */ +"Error downloading the archive" = "Ошибка загрузки архива"; + /* No comment provided by engineer. */ "Error enabling delivery receipts!" = "Ошибка при включении отчётов о доставке!"; @@ -1560,23 +2185,44 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Ошибка при экспорте архива чата"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Ошибка экспорта темы: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Ошибка при импорте архива чата"; /* No comment provided by engineer. */ "Error joining group" = "Ошибка при вступлении в группу"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Ошибка загрузки %@ серверов"; +/* alert title */ +"Error loading servers" = "Ошибка загрузки серверов"; /* No comment provided by engineer. */ +"Error migrating settings" = "Ошибка миграции настроек"; + +/* No comment provided by engineer. */ +"Error opening chat" = "Ошибка доступа к чату"; + +/* alert title */ "Error receiving file" = "Ошибка при получении файла"; /* No comment provided by engineer. */ -"Error removing member" = "Ошибка при удалении члена группы"; +"Error reconnecting server" = "Ошибка переподключения к серверу"; /* No comment provided by engineer. */ -"Error saving %@ servers" = "Ошибка при сохранении %@ серверов"; +"Error reconnecting servers" = "Ошибка переподключения к серверам"; + +/* alert title */ +"Error registering for notifications" = "Ошибка регистрации для уведомлений"; + +/* alert title */ +"Error reordering lists" = "Ошибка сортировки списков"; + +/* No comment provided by engineer. */ +"Error resetting statistics" = "Ошибка сброса статистики"; + +/* alert title */ +"Error saving chat list" = "Ошибка сохранения списка чатов"; /* No comment provided by engineer. */ "Error saving group profile" = "Ошибка при сохранении профиля группы"; @@ -1590,14 +2236,20 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "Ошибка сохранения пароля в Keychain"; +/* alert title */ +"Error saving servers" = "Ошибка сохранения серверов"; + +/* when migrating */ +"Error saving settings" = "Ошибка сохранения настроек"; + /* No comment provided by engineer. */ "Error saving user password" = "Ошибка при сохранении пароля пользователя"; /* No comment provided by engineer. */ -"Error sending email" = "Ошибка отправки email"; +"Error scanning code: %@" = "Ошибка сканирования кода: %@"; /* No comment provided by engineer. */ -"Error sending member contact invitation" = "Ошибка отправки приглашения члену группы"; +"Error sending email" = "Ошибка отправки email"; /* No comment provided by engineer. */ "Error sending message" = "Ошибка при отправке сообщения"; @@ -1612,17 +2264,26 @@ "Error stopping chat" = "Ошибка при остановке чата"; /* No comment provided by engineer. */ +"Error switching profile" = "Ошибка переключения профиля"; + +/* alertTitle */ "Error switching profile!" = "Ошибка выбора профиля!"; /* No comment provided by engineer. */ "Error synchronizing connection" = "Ошибка синхронизации соединения"; +/* No comment provided by engineer. */ +"Error testing server connection" = "Ошибка проверки соединения с сервером"; + /* No comment provided by engineer. */ "Error updating group link" = "Ошибка обновления ссылки группы"; /* No comment provided by engineer. */ "Error updating message" = "Ошибка при обновлении сообщения"; +/* alert title */ +"Error updating server" = "Ошибка сохранения сервера"; + /* No comment provided by engineer. */ "Error updating settings" = "Ошибка при сохранении настроек сети"; @@ -1630,9 +2291,17 @@ "Error updating user privacy" = "Ошибка при обновлении конфиденциальности"; /* No comment provided by engineer. */ -"Error: " = "Ошибка: "; +"Error uploading the archive" = "Ошибка загрузки архива"; /* No comment provided by engineer. */ +"Error verifying passphrase:" = "Ошибка подтверждения пароля:"; + +/* No comment provided by engineer. */ +"Error: " = "Ошибка: "; + +/* alert message +file error text +snd error text */ "Error: %@" = "Ошибка: %@"; /* No comment provided by engineer. */ @@ -1642,10 +2311,13 @@ "Error: URL is invalid" = "Ошибка: неверная ссылка"; /* No comment provided by engineer. */ -"Even when disabled in the conversation." = "Даже когда они выключены в разговоре."; +"Errors" = "Ошибки"; + +/* servers error */ +"Errors in servers configuration." = "Ошибки в настройках серверов."; /* No comment provided by engineer. */ -"event happened" = "событие произошло"; +"Even when disabled in the conversation." = "Даже когда они выключены в разговоре."; /* No comment provided by engineer. */ "Exit without saving" = "Выйти без сохранения"; @@ -1653,15 +2325,27 @@ /* chat item action */ "Expand" = "Раскрыть"; +/* No comment provided by engineer. */ +"expired" = "истекло"; + +/* token status text */ +"Expired" = "Истекший"; + /* No comment provided by engineer. */ "Export database" = "Экспорт архива чата"; /* No comment provided by engineer. */ "Export error:" = "Ошибка при экспорте:"; +/* No comment provided by engineer. */ +"Export theme" = "Экспорт темы"; + /* No comment provided by engineer. */ "Exported database archive." = "Архив чата экспортирован."; +/* No comment provided by engineer. */ +"Exported file doesn't exist" = "Экспортированный файл не существует"; + /* No comment provided by engineer. */ "Exporting database archive…" = "Архив чата экспортируется…"; @@ -1671,12 +2355,42 @@ /* No comment provided by engineer. */ "Fast and no wait until the sender is online!" = "Быстрые и не нужно ждать, когда отправитель онлайн!"; +/* No comment provided by engineer. */ +"Faster deletion of groups." = "Ускорено удаление групп."; + /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Быстрое вступление и надежная доставка сообщений."; /* No comment provided by engineer. */ +"Faster sending messages." = "Ускорена отправка сообщений."; + +/* swipe action */ "Favorite" = "Избранный"; +/* No comment provided by engineer. */ +"Favorites" = "Избранное"; + +/* file error alert title */ +"File error" = "Ошибка файла"; + +/* alert message */ +"File errors:\n%@" = "Ошибки файлов:\n%@"; + +/* file error text */ +"File is blocked by server operator:\n%@." = "Файл заблокирован оператором сервера:\n%@."; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Файл не найден - скорее всего, файл был удален или отменен."; + +/* file error text */ +"File server error: %@" = "Ошибка сервера файлов: %@"; + +/* No comment provided by engineer. */ +"File status" = "Статус файла"; + +/* copied message info */ +"File status: %@" = "Статус файла: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Файл будет удалён с серверов."; @@ -1689,6 +2403,9 @@ /* No comment provided by engineer. */ "File: %@" = "Файл: %@"; +/* No comment provided by engineer. */ +"Files" = "Файлы"; + /* No comment provided by engineer. */ "Files & media" = "Файлы и медиа"; @@ -1696,7 +2413,10 @@ "Files and media" = "Файлы и медиа"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Файлы и медиа запрещены в этой группе."; +"Files and media are prohibited." = "Файлы и медиа запрещены в этой группе."; + +/* No comment provided by engineer. */ +"Files and media not allowed" = "Файлы и медиа не разрешены"; /* No comment provided by engineer. */ "Files and media prohibited!" = "Файлы и медиа запрещены!"; @@ -1704,6 +2424,12 @@ /* No comment provided by engineer. */ "Filter unread and favorite chats." = "Фильтровать непрочитанные и избранные чаты."; +/* No comment provided by engineer. */ +"Finalize migration" = "Завершить миграцию"; + +/* No comment provided by engineer. */ +"Finalize migration on another device." = "Завершите миграцию на другом устройстве."; + /* No comment provided by engineer. */ "Finally, we have them! 🚀" = "Наконец-то, мы их добавили! 🚀"; @@ -1726,11 +2452,71 @@ "Fix not supported by contact" = "Починка не поддерживается контактом"; /* No comment provided by engineer. */ -"Fix not supported by group member" = "Починка не поддерживается членом группы"; +"For all moderators" = "Для всех модераторов"; + +/* servers error */ +"For chat profile %@:" = "Для профиля чата %@:"; /* No comment provided by engineer. */ "For console" = "Для консоли"; +/* No comment provided by engineer. */ +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Например, если Ваш контакт получает сообщения через сервер SimpleX Chat, Ваше приложение доставит их через сервер Flux."; + +/* No comment provided by engineer. */ +"For me" = "Для меня"; + +/* No comment provided by engineer. */ +"For private routing" = "Для доставки сообщений"; + +/* No comment provided by engineer. */ +"For social media" = "Для социальных сетей"; + +/* chat item action */ +"Forward" = "Переслать"; + +/* alert title */ +"Forward %d message(s)?" = "Переслать %d сообщение(й)?"; + +/* No comment provided by engineer. */ +"Forward and save messages" = "Переслать и сохранить сообщение"; + +/* alert action */ +"Forward messages" = "Переслать сообщения"; + +/* alert message */ +"Forward messages without files?" = "Переслать сообщения без файлов?"; + +/* No comment provided by engineer. */ +"Forward up to 20 messages at once." = "Пересылайте до 20 сообщений за раз."; + +/* No comment provided by engineer. */ +"forwarded" = "переслано"; + +/* No comment provided by engineer. */ +"Forwarded" = "Переслано"; + +/* No comment provided by engineer. */ +"Forwarded from" = "Переслано из"; + +/* No comment provided by engineer. */ +"Forwarding %lld messages" = "Пересылка %lld сообщений"; + +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Пересылающий сервер %@ не смог подключиться к серверу назначения %@. Попробуйте позже."; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "Адрес пересылающего сервера несовместим с настройками сети: %@."; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "Версия пересылающего сервера несовместима с настройками сети: %@."; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Пересылающий сервер: %1$@\nОшибка сервера получателя: %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Пересылающий сервер: %1$@\nОшибка: %2$@"; + /* No comment provided by engineer. */ "Found desktop" = "Компьютер найден"; @@ -1743,21 +2529,24 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Полное имя (не обязательно)"; -/* No comment provided by engineer. */ -"Full name:" = "Полное имя:"; - -/* No comment provided by engineer. */ -"Fully decentralized – visible only to members." = "Группа полностью децентрализована – она видна только членам."; - /* No comment provided by engineer. */ "Fully re-implemented - work in background!" = "Полностью обновлены - работают в фоне!"; /* No comment provided by engineer. */ "Further reduced battery usage" = "Уменьшенное потребление батареи"; +/* No comment provided by engineer. */ +"Get notified when mentioned." = "Уведомления, когда Вас упомянули."; + /* No comment provided by engineer. */ "GIFs and stickers" = "ГИФ файлы и стикеры"; +/* message preview */ +"Good afternoon!" = "Добрый день!"; + +/* message preview */ +"Good morning!" = "Доброе утро!"; + /* No comment provided by engineer. */ "Group" = "Группа"; @@ -1777,7 +2566,7 @@ "Group full name (optional)" = "Полное имя (необязательно)"; /* No comment provided by engineer. */ -"Group image" = "Аватар группы"; +"Group image" = "Картинка группы"; /* No comment provided by engineer. */ "Group invitation" = "Приглашение в группу"; @@ -1794,24 +2583,6 @@ /* No comment provided by engineer. */ "Group links" = "Ссылки групп"; -/* No comment provided by engineer. */ -"Group members can add message reactions." = "Члены группы могут добавлять реакции на сообщения."; - -/* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages." = "Члены группы могут необратимо удалять отправленные сообщения."; - -/* No comment provided by engineer. */ -"Group members can send direct messages." = "Члены группы могут посылать прямые сообщения."; - -/* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Члены группы могут посылать исчезающие сообщения."; - -/* No comment provided by engineer. */ -"Group members can send files and media." = "Члены группы могут слать файлы и медиа."; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "Члены группы могут отправлять голосовые сообщения."; - /* notification */ "Group message:" = "Групповое сообщение:"; @@ -1824,24 +2595,24 @@ /* No comment provided by engineer. */ "Group profile" = "Профиль группы"; -/* No comment provided by engineer. */ -"Group profile is stored on members' devices, not on the servers." = "Профиль группы хранится на устройствах членов, а не на серверах."; - /* snd group event chat item */ "group profile updated" = "профиль группы обновлен"; /* No comment provided by engineer. */ "Group welcome message" = "Приветственное сообщение группы"; -/* No comment provided by engineer. */ -"Group will be deleted for all members - this cannot be undone!" = "Группа будет удалена для всех членов - это действие нельзя отменить!"; - /* No comment provided by engineer. */ "Group will be deleted for you - this cannot be undone!" = "Группа будет удалена для Вас - это действие нельзя отменить!"; +/* No comment provided by engineer. */ +"Groups" = "Группы"; + /* No comment provided by engineer. */ "Help" = "Помощь"; +/* No comment provided by engineer. */ +"Help admins moderating their groups." = "Помогайте администраторам модерировать их группы."; + /* No comment provided by engineer. */ "Hidden" = "Скрытое"; @@ -1870,6 +2641,12 @@ "hours" = "часов"; /* No comment provided by engineer. */ +"How it affects privacy" = "Как это влияет на конфиденциальность"; + +/* No comment provided by engineer. */ +"How it helps privacy" = "Как это улучшает конфиденциальность"; + +/* alert button */ "How it works" = "Как это работает"; /* No comment provided by engineer. */ @@ -1879,20 +2656,20 @@ "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" = "Как использовать серверы"; +/* No comment provided by engineer. */ +"Hungarian interface" = "Венгерский интерфейс"; + /* No comment provided by engineer. */ "ICE servers (one per line)" = "ICE серверы (один на строке)"; /* No comment provided by engineer. */ "If you can't meet in person, show QR code in a video call, or share the link." = "Если Вы не можете встретиться лично, покажите QR-код во время видеозвонка или поделитесь ссылкой."; -/* No comment provided by engineer. */ -"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Если Вы не можете встретиться лично, Вы можете **сосканировать QR код во время видеозвонка**, или Ваш контакт может отправить Вам ссылку."; - /* No comment provided by engineer. */ "If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Если Вы введете этот код при открытии приложения, все данные приложения будут безвозвратно удалены!"; @@ -1915,7 +2692,7 @@ "Immediately" = "Сразу"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Защищен от спама"; +"Immune to spam" = "Защищен от спама"; /* No comment provided by engineer. */ "Import" = "Импортировать"; @@ -1926,15 +2703,45 @@ /* No comment provided by engineer. */ "Import database" = "Импорт архива чата"; +/* No comment provided by engineer. */ +"Import failed" = "Ошибка импорта"; + +/* No comment provided by engineer. */ +"Import theme" = "Импорт темы"; + +/* No comment provided by engineer. */ +"Importing archive" = "Импорт архива"; + +/* No comment provided by engineer. */ +"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "Улучшенная доставка, меньше трафик."; + +/* No comment provided by engineer. */ +"Improved message delivery" = "Улучшенная доставка сообщений"; + /* No comment provided by engineer. */ "Improved privacy and security" = "Улучшенная безопасность"; /* No comment provided by engineer. */ "Improved server configuration" = "Улучшенная конфигурация серверов"; +/* No comment provided by engineer. */ +"In order to continue, chat should be stopped." = "Чтобы продолжить, чат должен быть остановлен."; + /* No comment provided by engineer. */ "In reply to" = "В ответ на"; +/* No comment provided by engineer. */ +"In-call sounds" = "Звуки во время звонков"; + +/* No comment provided by engineer. */ +"inactive" = "неактивен"; + +/* report reason */ +"Inappropriate content" = "Неприемлемый контент"; + +/* report reason */ +"Inappropriate profile" = "Неприемлемый профиль"; + /* No comment provided by engineer. */ "Incognito" = "Инкогнито"; @@ -1989,14 +2796,32 @@ /* No comment provided by engineer. */ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "[SimpleX Chat для терминала](https://github.com/simplex-chat/simplex-chat)"; +/* No comment provided by engineer. */ +"Instant" = "Мгновенно"; + /* No comment provided by engineer. */ "Instant push notifications will be hidden!\n" = "Мгновенные уведомления будут скрыты!\n"; /* No comment provided by engineer. */ -"Instantly" = "Мгновенно"; +"Interface" = "Интерфейс"; /* No comment provided by engineer. */ -"Interface" = "Интерфейс"; +"Interface colors" = "Цвета интерфейса"; + +/* token status text */ +"Invalid" = "Недействительный"; + +/* token status text */ +"Invalid (bad token)" = "Недействительный (плохой токен)"; + +/* token status text */ +"Invalid (expired)" = "Недействительный (истекший)"; + +/* token status text */ +"Invalid (unregistered)" = "Недействительный (незарегистрированный)"; + +/* token status text */ +"Invalid (wrong topic)" = "Недействительный (плохой заголовок)"; /* invalid chat data */ "invalid chat" = "ошибка чата"; @@ -2010,10 +2835,25 @@ /* invalid chat item */ "invalid data" = "ошибка данных"; +/* No comment provided by engineer. */ +"Invalid display name!" = "Ошибка имени!"; + +/* No comment provided by engineer. */ +"Invalid link" = "Ошибка ссылки"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Ошибка подтверждения миграции"; + /* No comment provided by engineer. */ "Invalid name!" = "Неверное имя!"; /* No comment provided by engineer. */ +"Invalid QR code" = "Неверный QR код"; + +/* No comment provided by engineer. */ +"Invalid response" = "Ошибка ответа"; + +/* alert title */ "Invalid server address!" = "Ошибка в адресе сервера!"; /* item status text */ @@ -2025,11 +2865,14 @@ /* group name */ "invitation to group %@" = "приглашение в группу %@"; +/* No comment provided by engineer. */ +"invite" = "пригласить"; + /* No comment provided by engineer. */ "Invite friends" = "Пригласить друзей"; /* No comment provided by engineer. */ -"Invite members" = "Пригласить членов группы"; +"Invite to chat" = "Пригласить в разговор"; /* No comment provided by engineer. */ "Invite to group" = "Пригласить в группу"; @@ -2052,6 +2895,9 @@ /* No comment provided by engineer. */ "iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "Пароль базы данных будет безопасно сохранен в iOS Keychain после запуска чата или изменения пароля - это позволит получать мгновенные уведомления."; +/* No comment provided by engineer. */ +"IP address" = "IP адрес"; + /* No comment provided by engineer. */ "Irreversible message deletion" = "Окончательное удаление сообщений"; @@ -2059,7 +2905,7 @@ "Irreversible message deletion is prohibited in this chat." = "Необратимое удаление сообщений запрещено в этом чате."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "Необратимое удаление сообщений запрещено в этой группе."; +"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." = "Это позволяет иметь много анонимных соединений без общих данных между ними в одном профиле пользователя."; @@ -2070,6 +2916,9 @@ /* No comment provided by engineer. */ "It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Это может произойти, когда:\n1. Клиент отправителя удалил неотправленные сообщения через 2 дня, или сервер – через 30 дней.\n2. Расшифровка сообщения была невозможна, когда Вы или Ваш контакт использовали старую копию базы данных.\n3. Соединение компроментировано."; +/* No comment provided by engineer. */ +"It 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 (%@)." = "Возможно, Вы уже соединились через эту ссылку. Если это не так, то это ошибка (%@)."; @@ -2082,7 +2931,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Японский интерфейс"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Вступить"; /* No comment provided by engineer. */ @@ -2091,6 +2940,9 @@ /* No comment provided by engineer. */ "Join group" = "Вступить в группу"; +/* No comment provided by engineer. */ +"Join group conversations" = "Присоединяйтесь к разговорам в группах"; + /* No comment provided by engineer. */ "Join group?" = "Вступить в группу?"; @@ -2106,9 +2958,18 @@ /* No comment provided by engineer. */ "Joining group" = "Вступление в группу"; +/* alert action */ +"Keep" = "Оставить"; + +/* No comment provided by engineer. */ +"Keep conversation" = "Оставить разговор"; + /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Оставьте приложение открытым, чтобы использовать его с компьютера"; +/* alert title */ +"Keep unused invitation?" = "Оставить неиспользованное приглашение?"; + /* No comment provided by engineer. */ "Keep your connections" = "Сохраните Ваши соединения"; @@ -2124,9 +2985,15 @@ /* No comment provided by engineer. */ "Learn more" = "Узнать больше"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Выйти"; +/* No comment provided by engineer. */ +"Leave chat" = "Покинуть разговор"; + +/* No comment provided by engineer. */ +"Leave chat?" = "Покинуть разговор?"; + /* No comment provided by engineer. */ "Leave group" = "Выйти из группы"; @@ -2154,6 +3021,15 @@ /* No comment provided by engineer. */ "Linked desktops" = "Связанные компьютеры"; +/* swipe action */ +"List" = "Список"; + +/* No comment provided by engineer. */ +"List name and emoji should be different for all lists." = "Название списка и эмодзи должны быть разными для всех списков."; + +/* No comment provided by engineer. */ +"List name..." = "Имя списка..."; + /* No comment provided by engineer. */ "LIVE" = "LIVE"; @@ -2175,24 +3051,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Режим блокировки"; -/* No comment provided by engineer. */ -"Make a private connection" = "Добавьте контакт"; - /* No comment provided by engineer. */ "Make one message disappear" = "Одно исчезающее сообщение"; /* No comment provided by engineer. */ "Make profile private!" = "Сделайте профиль скрытым!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Пожалуйста, проверьте, что адреса %@ серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Пожалуйста, проверьте, что адреса WebRTC ICE серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Много пользователей спросили: *как SimpleX доставляет сообщения без идентификаторов пользователей?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Пометить как удаленное для всех"; @@ -2211,23 +3078,26 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "Макс. 30 секунд, доставляются мгновенно."; -/* member role */ -"member" = "член группы"; - /* No comment provided by engineer. */ -"Member" = "Член группы"; +"Media & file servers" = "Серверы файлов и медиа"; + +/* blur media */ +"Medium" = "Среднее"; /* rcv group event chat item */ "member connected" = "соединен(а)"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "Роль члена группы будет изменена на \"%@\". Все члены группы получат сообщение."; +/* chat feature */ +"Member reports" = "Сообщения о нарушениях"; /* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "Роль члена группы будет изменена на \"%@\". Будет отправлено новое приглашение."; +"Member role will be changed to \"%@\". All chat members will be notified." = "Роль участника будет изменена на \"%@\". Все участники разговора получат уведомление."; /* No comment provided by engineer. */ -"Member will be removed from group - this cannot be undone!" = "Член группы будет удален - это действие нельзя отменить!"; +"Menus" = "Меню"; + +/* No comment provided by engineer. */ +"message" = "написать"; /* item status text */ "Message delivery error" = "Ошибка доставки сообщения"; @@ -2235,9 +3105,18 @@ /* No comment provided by engineer. */ "Message delivery receipts!" = "Отчеты о доставке сообщений!"; +/* item status text */ +"Message delivery warning" = "Предупреждение доставки сообщения"; + /* No comment provided by engineer. */ "Message draft" = "Черновик сообщения"; +/* item status text */ +"Message forwarded" = "Сообщение переслано"; + +/* No comment provided by engineer. */ +"Message queue info" = "Информация об очереди сообщений"; + /* chat feature */ "Message reactions" = "Реакции на сообщения"; @@ -2245,14 +3124,35 @@ "Message reactions are prohibited in this chat." = "Реакции на сообщения в этом чате запрещены."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Реакции на сообщения запрещены в этой группе."; +"Message reactions are prohibited." = "Реакции на сообщения запрещены в этой группе."; /* notification */ "message received" = "получено сообщение"; +/* No comment provided by engineer. */ +"Message reception" = "Прием сообщений"; + +/* No comment provided by engineer. */ +"Message servers" = "Серверы сообщений"; + +/* No comment provided by engineer. */ +"Message shape" = "Форма сообщений"; + +/* No comment provided by engineer. */ +"Message source remains private." = "Источник сообщения остаётся конфиденциальным."; + +/* No comment provided by engineer. */ +"Message status" = "Статус сообщения"; + +/* copied message info */ +"Message status: %@" = "Статус сообщения: %@"; + /* No comment provided by engineer. */ "Message text" = "Текст сообщения"; +/* No comment provided by engineer. */ +"Message too large" = "Сообщение слишком большое"; + /* No comment provided by engineer. */ "Messages" = "Сообщения"; @@ -2262,9 +3162,48 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Сообщения от %@ будут показаны!"; +/* alert message */ +"Messages in this chat will never be deleted." = "Сообщения в этом чате никогда не будут удалены."; + +/* No comment provided by engineer. */ +"Messages received" = "Получено сообщений"; + +/* No comment provided by engineer. */ +"Messages sent" = "Сообщений отправлено"; + +/* alert message */ +"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), правдоподобным отрицанием и восстановлением от взлома."; + +/* 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), правдоподобным отрицанием и восстановлением от взлома."; + +/* No comment provided by engineer. */ +"Migrate device" = "Мигрировать устройство"; + +/* No comment provided by engineer. */ +"Migrate from another device" = "Миграция с другого устройства"; + +/* No comment provided by engineer. */ +"Migrate here" = "Мигрировать сюда"; + +/* No comment provided by engineer. */ +"Migrate to another device" = "Мигрировать на другое устройство"; + +/* No comment provided by engineer. */ +"Migrate to another device via QR code." = "Мигрируйте на другое устройство через QR код."; + +/* No comment provided by engineer. */ +"Migrating" = "Миграция"; + /* No comment provided by engineer. */ "Migrating database archive…" = "Данные чата перемещаются…"; +/* No comment provided by engineer. */ +"Migration complete" = "Миграция завершена"; + /* No comment provided by engineer. */ "Migration error:" = "Ошибка при перемещении данных:"; @@ -2275,7 +3214,7 @@ "Migration is completed" = "Перемещение данных завершено"; /* No comment provided by engineer. */ -"Migrations: %@" = "Миграции: %@"; +"Migrations:" = "Миграции:"; /* time unit */ "minutes" = "минут"; @@ -2295,65 +3234,101 @@ /* copied message info */ "Moderated at: %@" = "Модерировано: %@"; -/* No comment provided by engineer. */ +/* marked deleted chat item preview text */ "moderated by %@" = "удалено %@"; +/* member role */ +"moderator" = "модератор"; + /* time unit */ "months" = "месяцев"; +/* swipe action */ +"More" = "Больше"; + /* No comment provided by engineer. */ "More improvements are coming soon!" = "Дополнительные улучшения скоро!"; +/* No comment provided by engineer. */ +"More reliable network connection." = "Более надежное соединение с сетью."; + +/* No comment provided by engineer. */ +"More reliable notifications" = "Более надежные уведомления"; + /* item status description */ "Most likely this connection is deleted." = "Скорее всего, соединение удалено."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Скорее всего, этот контакт удалил соединение с Вами."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Много профилей чата"; -/* No comment provided by engineer. */ +/* notification label action */ "Mute" = "Без звука"; +/* notification label action */ +"Mute all" = "Все без звука"; + /* No comment provided by engineer. */ "Muted when inactive!" = "Без звука, когда не активный!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Имя"; /* No comment provided by engineer. */ "Network & servers" = "Сеть & серверы"; +/* No comment provided by engineer. */ +"Network connection" = "Интернет-соединение"; + +/* No comment provided by engineer. */ +"Network decentralization" = "Децентрализация сети"; + +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "Ошибка сети - сообщение не было отправлено после многократных попыток."; + +/* No comment provided by engineer. */ +"Network management" = "Статус сети"; + +/* No comment provided by engineer. */ +"Network operator" = "Оператор сети"; + /* No comment provided by engineer. */ "Network settings" = "Настройки сети"; /* No comment provided by engineer. */ "Network status" = "Состояние сети"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "никогда"; +/* token status text */ +"New" = "Новый"; + +/* No comment provided by engineer. */ +"New chat" = "Новый чат"; + +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Новый интерфейс 🎉"; + /* notification */ "New contact request" = "Новый запрос на соединение"; /* notification */ "New contact:" = "Новый контакт:"; -/* No comment provided by engineer. */ -"New database archive" = "Новый архив чата"; - /* No comment provided by engineer. */ "New desktop app!" = "Приложение для компьютера!"; /* No comment provided by engineer. */ "New display name" = "Новое имя"; +/* notification */ +"New events" = "Новые события"; + /* No comment provided by engineer. */ "New in %@" = "Новое в %@"; /* No comment provided by engineer. */ -"New member role" = "Роль члена группы"; +"New media options" = "Новые медиа-опции"; /* notification */ "new message" = "новое сообщение"; @@ -2367,6 +3342,15 @@ /* No comment provided by engineer. */ "New passphrase…" = "Новый пароль…"; +/* No comment provided by engineer. */ +"New server" = "Новый сервер"; + +/* No comment provided by engineer. */ +"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 будут использоваться для каждого сервера."; + /* pref value */ "no" = "нет"; @@ -2376,6 +3360,15 @@ /* Authentication unavailable */ "No app password" = "Нет кода доступа"; +/* No comment provided by engineer. */ +"No chats" = "Нет чатов"; + +/* No comment provided by engineer. */ +"No chats found" = "Чаты не найдены"; + +/* No comment provided by engineer. */ +"No chats in list %@" = "Нет чатов в списке %@"; + /* No comment provided by engineer. */ "No contacts selected" = "Контакты не выбраны"; @@ -2388,6 +3381,9 @@ /* No comment provided by engineer. */ "No device token!" = "Отсутствует токен устройства!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Прямого соединения пока нет, сообщение переслано или будет переслано админом."; + /* No comment provided by engineer. */ "no e2e encryption" = "нет e2e шифрования"; @@ -2400,55 +3396,112 @@ /* No comment provided by engineer. */ "No history" = "Нет истории"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Нет информации, попробуйте перезагрузить"; + +/* servers error */ +"No media & file servers." = "Нет серверов файлов и медиа."; + +/* No comment provided by engineer. */ +"No message" = "Нет сообщения"; + +/* servers error */ +"No message servers." = "Нет серверов сообщений."; + +/* No comment provided by engineer. */ +"No network connection" = "Нет интернет-соединения"; + +/* No comment provided by engineer. */ +"No permission to record speech" = "Нет разрешения на запись речи"; + +/* No comment provided by engineer. */ +"No permission to record video" = "Нет разрешения на запись видео"; + /* No comment provided by engineer. */ "No permission to record voice message" = "Нет разрешения для записи голосового сообщения"; +/* No comment provided by engineer. */ +"No push server" = "Без сервера нотификаций"; + /* No comment provided by engineer. */ "No received or sent files" = "Нет полученных или отправленных файлов"; +/* servers error */ +"No servers for private message routing." = "Нет серверов для доставки сообщений."; + +/* servers error */ +"No servers to receive files." = "Нет серверов для приема файлов."; + +/* servers error */ +"No servers to receive messages." = "Нет серверов для приема сообщений."; + +/* servers error */ +"No servers to send files." = "Нет серверов для отправки файлов."; + /* copied message info in history */ "no text" = "нет текста"; +/* alert title */ +"No token!" = "Нет токена!"; + +/* No comment provided by engineer. */ +"No unread chats" = "Нет непрочитанных чатов"; + +/* No comment provided by engineer. */ +"No user identifiers." = "Без идентификаторов пользователей."; + /* No comment provided by engineer. */ "Not compatible!" = "Несовместимая версия!"; +/* No comment provided by engineer. */ +"Notes" = "Заметки"; + +/* No comment provided by engineer. */ +"Nothing selected" = "Ничего не выбрано"; + +/* alert title */ +"Nothing to forward!" = "Нет сообщений, которые можно переслать!"; + /* No comment provided by engineer. */ "Notifications" = "Уведомления"; /* No comment provided by engineer. */ "Notifications are disabled!" = "Уведомления выключены"; +/* alert title */ +"Notifications error" = "Ошибка уведомлений"; + /* No comment provided by engineer. */ -"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Теперь админы могут:\n- удалять сообщения членов.\n- приостанавливать членов (роль \"наблюдатель\")"; +"Notifications privacy" = "Конфиденциальность уведомлений"; + +/* alert title */ +"Notifications status" = "Статус уведомлений"; /* member role */ "observer" = "читатель"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "нет"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Выключено"; -/* No comment provided by engineer. */ -"Off (Local)" = "Выключить (Локальные)"; - /* feature offered item */ "offered %@" = "предложил(a) %@"; /* feature offered item */ "offered %@: %@" = "предложил(a) %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Ок"; /* No comment provided by engineer. */ -"Old database" = "Предыдущая версия данных чата"; +"OK" = "OK"; /* No comment provided by engineer. */ -"Old database archive" = "Старый архив чата"; +"Old database" = "Предыдущая версия данных чата"; /* group pref value */ "on" = "да"; @@ -2457,16 +3510,22 @@ "One-time invitation link" = "Одноразовая ссылка"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Подключаться только к onion хостам. Требуется включенный VPN."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Подключаться только к **onion** хостам.\nТребуется совместимый VPN."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Onion хосты используются, если возможно. Требуется включенный VPN."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Onion хосты используются, если возможно.\nТребуется совместимый VPN."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion хосты не используются."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Только пользовательские устройства хранят контакты, группы и сообщения, которые отправляются **с двухуровневым end-to-end шифрованием**."; +"Only chat owners can change preferences." = "Только владельцы разговора могут поменять предпочтения."; + +/* No comment provided by engineer. */ +"Only client devices store user profiles, contacts, groups, and messages." = "Только пользовательские устройства хранят контакты, группы и сообщения."; + +/* No comment provided by engineer. */ +"Only delete conversation" = "Удалить только разговор"; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Только владельцы группы могут изменять предпочтения группы."; @@ -2477,11 +3536,17 @@ /* No comment provided by engineer. */ "Only group owners can enable voice messages." = "Только владельцы группы могут разрешить голосовые сообщения."; +/* No comment provided by engineer. */ +"Only sender and moderators see it" = "Только отправитель и модераторы видят это"; + +/* No comment provided by engineer. */ +"Only you and moderators see it" = "Только вы и модераторы видят это"; + /* No comment provided by engineer. */ "Only you can add message reactions." = "Только Вы можете добавлять реакции на сообщения."; /* No comment provided by engineer. */ -"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "Только Вы можете необратимо удалять сообщения (Ваш контакт может помечать их на удаление)."; +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Только Вы можете необратимо удалять сообщения (Ваш контакт может помечать их на удаление). (24 часа)"; /* No comment provided by engineer. */ "Only you can make calls." = "Только Вы можете совершать звонки."; @@ -2496,7 +3561,7 @@ "Only your contact can add message reactions." = "Только Ваш контакт может добавлять реакции на сообщения."; /* No comment provided by engineer. */ -"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Только Ваш контакт может необратимо удалять сообщения (Вы можете помечать их на удаление)."; +"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Только Ваш контакт может необратимо удалять сообщения (Вы можете помечать их на удаление). (24 часа)"; /* No comment provided by engineer. */ "Only your contact can make calls." = "Только Ваш контакт может совершать звонки."; @@ -2507,33 +3572,78 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Только Ваш контакт может отправлять голосовые сообщения."; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "Открыть"; +/* No comment provided by engineer. */ +"Open changes" = "Открыть изменения"; + /* No comment provided by engineer. */ "Open chat" = "Открыть чат"; /* authentication reason */ "Open chat console" = "Открыть консоль"; +/* No comment provided by engineer. */ +"Open conditions" = "Открыть условия"; + /* No comment provided by engineer. */ "Open group" = "Открыть группу"; +/* authentication reason */ +"Open migration to another device" = "Открытие миграции на другое устройство"; + /* No comment provided by engineer. */ "Open Settings" = "Открыть Настройки"; -/* authentication reason */ -"Open user profiles" = "Открыть профили пользователя"; +/* No comment provided by engineer. */ +"Opening app…" = "Приложение отрывается…"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Открытый протокол и код - кто угодно может запустить сервер."; +"Operator" = "Оператор"; + +/* alert title */ +"Operator server" = "Сервер оператора"; /* No comment provided by engineer. */ -"Opening database…" = "Открытие базы данных…"; +"Or import archive file" = "Или импортировать файл архива"; + +/* No comment provided by engineer. */ +"Or paste archive link" = "Или вставьте ссылку архива"; + +/* No comment provided by engineer. */ +"Or scan QR code" = "Или отсканируйте QR код"; + +/* No comment provided by engineer. */ +"Or securely share this file link" = "Или передайте эту ссылку"; + +/* No comment provided by engineer. */ +"Or show this code" = "Или покажите этот код"; + +/* No comment provided by engineer. */ +"Or to share privately" = "Или поделиться конфиденциально"; + +/* No comment provided by engineer. */ +"Organize chats into lists" = "Организуйте чаты в списки"; + +/* No comment provided by engineer. */ +"other" = "другое"; + +/* No comment provided by engineer. */ +"Other" = "Другaя сеть"; + +/* No comment provided by engineer. */ +"other errors" = "другие ошибки"; + +/* alert message */ +"Other file errors:\n%@" = "Другие ошибки файлов:\n%@"; /* member role */ "owner" = "владелец"; +/* feature role */ +"owners" = "владельцы"; + /* No comment provided by engineer. */ "Passcode" = "Код доступа"; @@ -2550,10 +3660,10 @@ "Passcode set!" = "Код доступа установлен!"; /* No comment provided by engineer. */ -"Password to show" = "Пароль чтобы раскрыть"; +"Password" = "Пароль"; /* No comment provided by engineer. */ -"Paste" = "Вставить"; +"Password to show" = "Пароль чтобы раскрыть"; /* No comment provided by engineer. */ "Paste desktop address" = "Вставить адрес компьютера"; @@ -2562,32 +3672,50 @@ "Paste image" = "Вставить изображение"; /* No comment provided by engineer. */ -"Paste received link" = "Вставить полученную ссылку"; +"Paste link to connect!" = "Вставьте ссылку, чтобы соединиться!"; -/* placeholder */ -"Paste the link you received to connect with your contact." = "Чтобы соединиться, вставьте ссылку, полученную от Вашего контакта."; +/* No comment provided by engineer. */ +"Paste the link you received" = "Вставьте полученную ссылку"; /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "С Вами можно соединиться только через созданные Вами ссылки."; +"pending" = "ожидает"; /* No comment provided by engineer. */ -"Periodically" = "Периодически"; +"Pending" = "Ожидает"; + +/* No comment provided by engineer. */ +"pending approval" = "ожидает утверждения"; + +/* No comment provided by engineer. */ +"Periodic" = "Периодически"; /* message decrypt error item */ "Permanent decryption error" = "Ошибка расшифровки"; +/* No comment provided by engineer. */ +"Picture-in-picture calls" = "Звонки с картинкой-в-картинке"; + /* No comment provided by engineer. */ "PING count" = "Количество PING"; /* No comment provided by engineer. */ "PING interval" = "Интервал PING"; +/* No comment provided by engineer. */ +"Play from the chat list." = "Открыть из списка чатов."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Попросите Вашего контакта разрешить звонки."; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Попросите у Вашего контакта разрешить отправку голосовых сообщений."; +/* No comment provided by engineer. */ +"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." = "Пожалуйста, проверьте, что Вы использовали правильную ссылку или попросите, чтобы Ваш контакт отправил Вам другую ссылку."; @@ -2597,6 +3725,12 @@ /* No comment provided by engineer. */ "Please check yours and your contact preferences." = "Проверьте предпочтения Вашего контакта."; +/* No comment provided by engineer. */ +"Please confirm that network settings are correct for this device." = "Пожалуйста, подтвердите, что настройки сети верны для этого устройства."; + +/* No comment provided by engineer. */ +"Please contact developers.\nError: %@" = "Пожалуйста, сообщите разработчикам.\nОшибка: %@"; + /* No comment provided by engineer. */ "Please contact group admin." = "Пожалуйста, свяжитесь с админом группы."; @@ -2621,9 +3755,21 @@ /* No comment provided by engineer. */ "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." = "Попробуйте выключить и снова включить уведомления."; + +/* token info */ +"Please wait for token activation to complete." = "Пожалуйста, дождитесь завершения активации токена."; + +/* token info */ +"Please wait for token to be registered." = "Пожалуйста, дождитесь регистрации токена."; + /* No comment provided by engineer. */ "Polish interface" = "Польский интерфейс"; +/* No comment provided by engineer. */ +"Port" = "Порт"; + /* server test error */ "Possibly, certificate fingerprint in server address is incorrect" = "Возможно, хэш сертификата в адресе сервера неверный"; @@ -2631,39 +3777,69 @@ "Preserve the last message draft, with attachments." = "Сохранить последний черновик, вместе с вложениями."; /* No comment provided by engineer. */ -"Preset server" = "Сервер по умолчанию"; +"Preset server address" = "Адрес сервера по умолчанию"; /* No comment provided by engineer. */ -"Preset server address" = "Адрес сервера по умолчанию"; +"Preset servers" = "Серверы по умолчанию"; /* No comment provided by engineer. */ "Preview" = "Просмотр"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Ранее подключенные серверы"; + /* No comment provided by engineer. */ "Privacy & security" = "Конфиденциальность"; +/* No comment provided by engineer. */ +"Privacy for your customers." = "Конфиденциальность для ваших покупателей."; + +/* 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" = "Защищенные имена файлов"; +/* No comment provided by engineer. */ +"Private media file names." = "Конфиденциальные названия медиафайлов."; + +/* No comment provided by engineer. */ +"Private message routing" = "Конфиденциальная доставка сообщений"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Конфиденциальная доставка 🚀"; + +/* name of notes to self */ +"Private notes" = "Личные заметки"; + +/* No comment provided by engineer. */ +"Private routing" = "Конфиденциальная доставка"; + +/* No comment provided by engineer. */ +"Private routing error" = "Ошибка конфиденциальной доставки"; + /* No comment provided by engineer. */ "Profile and server connections" = "Профиль и соединения на сервере"; /* No comment provided by engineer. */ -"Profile image" = "Аватар"; +"Profile image" = "Картинка профиля"; /* No comment provided by engineer. */ -"Profile name" = "Имя профиля"; - -/* No comment provided by engineer. */ -"Profile name:" = "Имя профиля:"; +"Profile images" = "Картинки профилей"; /* No comment provided by engineer. */ "Profile password" = "Пароль профиля"; /* No comment provided by engineer. */ +"Profile theme" = "Тема профиля"; + +/* alert message */ "Profile update will be sent to your contacts." = "Обновлённый профиль будет отправлен Вашим контактам."; /* No comment provided by engineer. */ @@ -2679,7 +3855,7 @@ "Prohibit messages reactions." = "Запретить реакции на сообщения."; /* No comment provided by engineer. */ -"Prohibit sending direct messages to members." = "Запретить посылать прямые сообщения членам группы."; +"Prohibit reporting messages to moderators." = "Запретить жаловаться модераторам группы."; /* No comment provided by engineer. */ "Prohibit sending disappearing messages." = "Запретить посылать исчезающие сообщения."; @@ -2687,38 +3863,71 @@ /* 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." = "Запретить отправлять голосовые сообщений."; /* No comment provided by engineer. */ "Protect app screen" = "Защитить экран приложения"; +/* No comment provided by engineer. */ +"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Включите в настройках *Сети и серверов*."; + /* No comment provided by engineer. */ "Protocol timeout" = "Таймаут протокола"; /* No comment provided by engineer. */ "Protocol timeout per KB" = "Таймаут протокола на KB"; +/* No comment provided by engineer. */ +"Proxied" = "Проксировано"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Проксированные серверы"; + +/* No comment provided by engineer. */ +"Proxy requires password" = "Прокси требует пароль"; + /* No comment provided by engineer. */ "Push notifications" = "Доставка уведомлений"; +/* No comment provided by engineer. */ +"Push server" = "Сервер уведомлений"; + +/* chat item text */ +"quantum resistant e2e encryption" = "квантово-устойчивое e2e шифрование"; + +/* No comment provided by engineer. */ +"Quantum resistant encryption" = "Квантово-устойчивое шифрование"; + /* No comment provided by engineer. */ "Rate the app" = "Оценить приложение"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Доступная панель чата"; + /* chat item menu */ "React…" = "Реакция…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Прочитано"; /* No comment provided by engineer. */ "Read more" = "Узнать больше"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Узнать больше в [Руководстве пользователя](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"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)."; + +/* 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)."; @@ -2727,10 +3936,10 @@ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Узнайте больше из нашего [GitHub репозитория](https://github.com/simplex-chat/simplex-chat#readme)."; /* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Узнайте больше из нашего GitHub репозитория."; +"Receipts are disabled" = "Отчёты о доставке выключены"; /* No comment provided by engineer. */ -"Receipts are disabled" = "Отчёты о доставке выключены"; +"Receive errors" = "Ошибки приема"; /* No comment provided by engineer. */ "received answer…" = "получен ответ…"; @@ -2750,6 +3959,15 @@ /* message info title */ "Received message" = "Полученное сообщение"; +/* No comment provided by engineer. */ +"Received messages" = "Полученные сообщения"; + +/* No comment provided by engineer. */ +"Received reply" = "Полученный ответ"; + +/* No comment provided by engineer. */ +"Received total" = "Всего получено"; + /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Адрес получения сообщений будет перемещён на другой сервер. Изменение адреса завершится после того как отправитель будет онлайн."; @@ -2759,12 +3977,33 @@ /* No comment provided by engineer. */ "Receiving via" = "Получение через"; +/* No comment provided by engineer. */ +"Recent history and improved [directory bot](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)." = "История сообщений и улучшенный [каталог групп](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)."; + +/* No comment provided by engineer. */ +"Recipient(s) can't see who this message is from." = "Получатели не видят от кого это сообщение."; + /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Получатели видят их в то время как Вы их набираете."; +/* No comment provided by engineer. */ +"Reconnect" = "Переподключить"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Повторно подключите все серверы, чтобы принудительно доставить сообщения. Используется дополнительный трафик."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Переподключить все серверы"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Переподключить все серверы?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Переподключить сервер для устранения неполадок доставки сообщений. Это использует дополнительный трафик."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Переподключить сервер?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Переподключить серверы?"; @@ -2777,7 +4016,17 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Уменьшенное потребление батареи"; -/* reject incoming call via notification */ +/* No comment provided by engineer. */ +"Register" = "Зарегистрировать"; + +/* token info */ +"Register notification token?" = "Зарегистрировать токен уведомлений?"; + +/* token status text */ +"Registered" = "Зарегистрирован"; + +/* reject incoming call via notification +swipe action */ "Reject" = "Отклонить"; /* No comment provided by engineer. */ @@ -2786,6 +4035,9 @@ /* No comment provided by engineer. */ "Reject contact request" = "Отклонить запрос"; +/* No comment provided by engineer. */ +"rejected" = "отклонён"; + /* call status */ "rejected call" = "отклонённый звонок"; @@ -2799,10 +4051,10 @@ "Remove" = "Удалить"; /* No comment provided by engineer. */ -"Remove member" = "Удалить члена группы"; +"Remove archive?" = "Удалить архив?"; /* No comment provided by engineer. */ -"Remove member?" = "Удалить члена группы?"; +"Remove image" = "Удалить изображение"; /* No comment provided by engineer. */ "Remove passphrase from keychain?" = "Удалить пароль из Keychain?"; @@ -2813,6 +4065,12 @@ /* rcv group event chat item */ "removed %@" = "удалил(а) %@"; +/* profile update event chat item */ +"removed contact address" = "удалён адрес контакта"; + +/* profile update event chat item */ +"removed profile picture" = "удалена картинка профиля"; + /* rcv group event chat item */ "removed you" = "удалил(а) Вас из группы"; @@ -2828,24 +4086,81 @@ /* No comment provided by engineer. */ "Repeat connection request?" = "Повторить запрос на соединение?"; +/* No comment provided by engineer. */ +"Repeat download" = "Повторить загрузку"; + +/* No comment provided by engineer. */ +"Repeat import" = "Повторить импорт"; + /* No comment provided by engineer. */ "Repeat join request?" = "Повторить запрос на вступление?"; +/* No comment provided by engineer. */ +"Repeat upload" = "Повторить загрузку"; + /* chat item action */ "Reply" = "Ответить"; +/* chat item action */ +"Report" = "Пожаловаться"; + +/* report reason */ +"Report content: only group moderators will see it." = "Пожаловаться на сообщение: увидят только модераторы группы."; + +/* report reason */ +"Report member profile: only group moderators will see it." = "Пожаловаться на профиль: увидят только модераторы группы."; + +/* report reason */ +"Report other: only group moderators will see it." = "Пожаловаться: увидят только модераторы группы."; + +/* No comment provided by engineer. */ +"Report reason?" = "Причина сообщения?"; + +/* report reason */ +"Report spam: only group moderators will see it." = "Пожаловаться на спам: увидят только модераторы группы."; + +/* report reason */ +"Report violation: only group moderators will see it." = "Пожаловаться на нарушение: увидят только модераторы группы."; + +/* report in notification */ +"Report: %@" = "Сообщение о нарушении: %@"; + +/* No comment provided by engineer. */ +"Reporting messages to moderators is prohibited." = "Сообщения о нарушениях запрещены в этой группе."; + +/* No comment provided by engineer. */ +"Reports" = "Сообщения о нарушениях"; + +/* chat list item title */ +"requested to connect" = "запрошено соединение"; + /* No comment provided by engineer. */ "Required" = "Обязательно"; /* No comment provided by engineer. */ "Reset" = "Сбросить"; +/* No comment provided by engineer. */ +"Reset all hints" = "Сбросить все подсказки"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Сбросить всю статистику"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Сбросить всю статистику?"; + /* No comment provided by engineer. */ "Reset colors" = "Сбросить цвета"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Сбросить на тему приложения"; + /* No comment provided by engineer. */ "Reset to defaults" = "Сбросить настройки"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Сбросить на тему пользователя"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Перезапустите приложение, чтобы создать новый профиль."; @@ -2864,11 +4179,14 @@ /* No comment provided by engineer. */ "Restore database error" = "Ошибка при восстановлении базы данных"; +/* No comment provided by engineer. */ +"Retry" = "Повторить"; + /* chat item action */ "Reveal" = "Показать"; /* No comment provided by engineer. */ -"Revert" = "Отменить изменения"; +"Review conditions" = "Посмотреть условия"; /* No comment provided by engineer. */ "Revoke" = "Отозвать"; @@ -2885,37 +4203,41 @@ /* No comment provided by engineer. */ "Run chat" = "Запустить chat"; -/* chat item action */ +/* No comment provided by engineer. */ +"Safely receive files" = "Получайте файлы безопасно"; + +/* No comment provided by engineer. */ +"Safer groups" = "Более безопасные группы"; + +/* alert button +chat item action */ "Save" = "Сохранить"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Сохранить (и уведомить контакты)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Сохранить и уведомить контакт"; /* No comment provided by engineer. */ -"Save and notify group members" = "Сохранить и уведомить членов группы"; +"Save and reconnect" = "Сохранить и переподключиться"; /* No comment provided by engineer. */ "Save and update group profile" = "Сохранить сообщение и обновить группу"; -/* No comment provided by engineer. */ -"Save archive" = "Сохранить архив"; - -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Сохранить настройки автоприема"; - /* No comment provided by engineer. */ "Save group profile" = "Сохранить профиль группы"; +/* No comment provided by engineer. */ +"Save list" = "Сохранить список"; + /* No comment provided by engineer. */ "Save passphrase and open chat" = "Сохранить пароль и открыть чат"; /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Сохранить пароль в Keychain"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Сохранить предпочтения?"; /* No comment provided by engineer. */ @@ -2924,18 +4246,42 @@ /* No comment provided by engineer. */ "Save servers" = "Сохранить серверы"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Сохранить серверы?"; -/* No comment provided by engineer. */ -"Save settings?" = "Сохранить настройки?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Сохранить приветственное сообщение?"; +/* alert title */ +"Save your profile?" = "Сохранить ваш профиль?"; + +/* No comment provided by engineer. */ +"saved" = "сохранено"; + +/* No comment provided by engineer. */ +"Saved" = "Сохранено"; + +/* No comment provided by engineer. */ +"Saved from" = "Сохранено из"; + +/* No comment provided by engineer. */ +"saved from %@" = "сохранено из %@"; + +/* message info title */ +"Saved message" = "Сохраненное сообщение"; + /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Сохраненные WebRTC ICE серверы будут удалены"; +/* No comment provided by engineer. */ +"Saving %lld messages" = "Сохранение %lld сообщений"; + +/* No comment provided by engineer. */ +"Scale" = "Масштаб"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Сканировать / Вставить ссылку"; + /* No comment provided by engineer. */ "Scan code" = "Сканировать код"; @@ -2951,12 +4297,24 @@ /* No comment provided by engineer. */ "Scan server QR code" = "Сканировать QR код сервера"; +/* No comment provided by engineer. */ +"search" = "поиск"; + /* No comment provided by engineer. */ "Search" = "Поиск"; +/* No comment provided by engineer. */ +"Search bar accepts invitation links." = "Поле поиска поддерживает ссылки-приглашения."; + +/* No comment provided by engineer. */ +"Search or paste SimpleX link" = "Искать или вставьте ссылку SimpleX"; + /* network option */ "sec" = "сек"; +/* No comment provided by engineer. */ +"Secondary" = "Вторичный"; + /* time unit */ "seconds" = "секунд"; @@ -2966,6 +4324,9 @@ /* server test step */ "Secure queue" = "Защита очереди"; +/* No comment provided by engineer. */ +"Secured" = "Защищено"; + /* No comment provided by engineer. */ "Security assessment" = "Аудит безопасности"; @@ -2975,9 +4336,18 @@ /* chat item text */ "security code changed" = "код безопасности изменился"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Выбрать"; +/* No comment provided by engineer. */ +"Select chat profile" = "Выберите профиль чата"; + +/* No comment provided by engineer. */ +"Selected %lld" = "Выбрано %lld"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Выбранные настройки чата запрещают это сообщение."; + /* No comment provided by engineer. */ "Self-destruct" = "Самоуничтожение"; @@ -3002,26 +4372,35 @@ /* No comment provided by engineer. */ "send direct message" = "отправьте сообщение"; -/* No comment provided by engineer. */ -"Send direct message" = "Отправить сообщение"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Отправьте сообщение чтобы соединиться"; /* No comment provided by engineer. */ "Send disappearing message" = "Отправить исчезающее сообщение"; +/* No comment provided by engineer. */ +"Send errors" = "Ошибки отправки"; + /* No comment provided by engineer. */ "Send link previews" = "Отправлять картинки ссылок"; /* No comment provided by engineer. */ "Send live message" = "Отправить живое сообщение"; +/* No comment provided by engineer. */ +"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 адрес защищен, и Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Отправлять сообщения напрямую, когда Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку."; + /* No comment provided by engineer. */ "Send notifications" = "Отправлять уведомления"; /* No comment provided by engineer. */ -"Send notifications:" = "Отправлять уведомления:"; +"Send private reports" = "Вы можете сообщить о нарушениях"; /* No comment provided by engineer. */ "Send questions and ideas" = "Отправьте вопросы и идеи"; @@ -3032,7 +4411,7 @@ /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Отправьте из галереи или из дополнительных клавиатур."; -/* No comment provided by engineer. */ +/* alert message */ "Sender cancelled file transfer." = "Отправитель отменил передачу файла."; /* No comment provided by engineer. */ @@ -3068,15 +4447,57 @@ /* copied message info */ "Sent at: %@" = "Отправлено: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Отправлено напрямую"; + /* notification */ "Sent file event" = "Отправка файла"; /* message info title */ "Sent message" = "Отправленное сообщение"; +/* No comment provided by engineer. */ +"Sent messages" = "Отправленные сообщения"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Отправленные сообщения будут удалены через заданное время."; +/* No comment provided by engineer. */ +"Sent reply" = "Отправленный ответ"; + +/* No comment provided by engineer. */ +"Sent total" = "Всего отправлено"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Отправлено через прокси"; + +/* No comment provided by engineer. */ +"Server" = "Сервер"; + +/* alert message */ +"Server added to operator %@." = "Сервер добавлен к оператору %@."; + +/* No comment provided by engineer. */ +"Server address" = "Адрес сервера"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "Адрес сервера несовместим с сетевыми настройками: %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "Адрес сервера несовместим с настройками сети."; + +/* alert title */ +"Server operator changed." = "Оператор серверов изменен."; + +/* No comment provided by engineer. */ +"Server operators" = "Операторы серверов"; + +/* alert title */ +"Server protocol changed." = "Протокол сервера изменен."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "информация сервера об очереди: %1$@\n\nпоследнее полученное сообщение: %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "Сервер требует авторизации для создания очередей, проверьте пароль"; @@ -3086,32 +4507,62 @@ /* No comment provided by engineer. */ "Server test failed!" = "Ошибка теста сервера!"; +/* No comment provided by engineer. */ +"Server type" = "Тип сервера"; + +/* srv error text */ +"Server version is incompatible with network settings." = "Версия сервера несовместима с настройками сети."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "Версия сервера несовместима с вашим приложением: %@."; + /* No comment provided by engineer. */ "Servers" = "Серверы"; +/* No comment provided by engineer. */ +"Servers info" = "Информация о серверах"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Статистика серверов будет сброшена - это нельзя отменить!"; + /* No comment provided by engineer. */ "Session code" = "Код сессии"; /* No comment provided by engineer. */ "Set 1 day" = "Установить 1 день"; +/* No comment provided by engineer. */ +"Set chat name…" = "Имя чата…"; + /* No comment provided by engineer. */ "Set contact name…" = "Имя контакта…"; +/* No comment provided by engineer. */ +"Set default theme" = "Установить тему по умолчанию"; + /* No comment provided by engineer. */ "Set group preferences" = "Предпочтения группы"; /* No comment provided by engineer. */ "Set it instead of system authentication." = "Установите код вместо системной аутентификации."; +/* No comment provided by engineer. */ +"Set message expiration in chats." = "Установите срок хранения сообщений в чатах."; + +/* profile update event chat item */ +"set new contact address" = "установлен новый адрес контакта"; + +/* profile update event chat item */ +"set new profile picture" = "установлена новая картинка профиля"; + /* No comment provided by engineer. */ "Set passcode" = "Установить код доступа"; /* No comment provided by engineer. */ -"Set passphrase to export" = "Установите пароль"; +"Set passphrase" = "Установить пароль"; /* No comment provided by engineer. */ -"Set the message shown to new members!" = "Установить сообщение для новых членов группы!"; +"Set passphrase to export" = "Установите пароль"; /* No comment provided by engineer. */ "Set timeouts for proxy/VPN" = "Установить таймауты для прокси/VPN"; @@ -3119,27 +4570,55 @@ /* No comment provided by engineer. */ "Settings" = "Настройки"; -/* chat item action */ +/* alert message */ +"Settings were changed." = "Настройки были изменены."; + +/* No comment provided by engineer. */ +"Shape profile images" = "Форма картинок профилей"; + +/* alert action +chat item action */ "Share" = "Поделиться"; /* No comment provided by engineer. */ "Share 1-time link" = "Поделиться одноразовой ссылкой"; +/* No comment provided by engineer. */ +"Share 1-time link with a friend" = "Поделитесь одноразовой ссылкой с другом"; + /* No comment provided by engineer. */ "Share address" = "Поделиться адресом"; /* No comment provided by engineer. */ +"Share address publicly" = "Поделитесь адресом"; + +/* alert title */ "Share address with contacts?" = "Поделиться адресом с контактами?"; +/* No comment provided by engineer. */ +"Share from other apps." = "Поделитесь из других приложений."; + /* No comment provided by engineer. */ "Share link" = "Поделиться ссылкой"; /* No comment provided by engineer. */ -"Share one-time invitation link" = "Поделиться ссылкой-приглашением"; +"Share profile" = "Поделиться профилем"; + +/* No comment provided by engineer. */ +"Share SimpleX address on social media." = "Поделитесь SimpleX адресом в социальных сетях."; + +/* No comment provided by engineer. */ +"Share this 1-time invite link" = "Поделиться одноразовой ссылкой-приглашением"; + +/* No comment provided by engineer. */ +"Share to SimpleX" = "Поделиться в SimpleX"; /* No comment provided by engineer. */ "Share with contacts" = "Поделиться с контактами"; +/* No comment provided by engineer. */ +"Show → on messages sent via private routing." = "Показать → на сообщениях доставленных конфиденциально."; + /* No comment provided by engineer. */ "Show calls in phone history" = "Показать звонки в истории телефона"; @@ -3149,18 +4628,39 @@ /* No comment provided by engineer. */ "Show last messages" = "Показывать последние сообщения"; +/* No comment provided by engineer. */ +"Show message status" = "Показать статус сообщения"; + +/* No comment provided by engineer. */ +"Show percentage" = "Показать процент"; + /* No comment provided by engineer. */ "Show preview" = "Показывать уведомления"; +/* No comment provided by engineer. */ +"Show QR code" = "Показать QR код"; + /* No comment provided by engineer. */ "Show:" = "Показать:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "Адрес SimpleX"; /* No comment provided by engineer. */ "SimpleX Address" = "Адрес SimpleX"; +/* No comment provided by engineer. */ +"SimpleX address and 1-time links are safe to share via any messenger." = "Адрес SimpleX и одноразовые ссылки безопасно отправлять через любой мессенджер."; + +/* No comment provided by engineer. */ +"SimpleX address or 1-time link?" = "Адрес SimpleX или одноразовая ссылка?"; + +/* No comment provided by engineer. */ +"SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "SimpleX Chat и Flux заключили соглашение добавить серверы под управлением Flux в приложение."; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "Безопасность SimpleX Chat была проверена Trail of Bits."; @@ -3173,9 +4673,15 @@ /* simplex link type */ "SimpleX group link" = "SimpleX ссылка группы"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "SimpleX ссылки"; +/* No comment provided by engineer. */ +"SimpleX links are prohibited." = "Ссылки SimpleX запрещены в этой группе."; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "Ссылки SimpleX не разрешены"; + /* No comment provided by engineer. */ "SimpleX Lock" = "Блокировка SimpleX"; @@ -3191,9 +4697,15 @@ /* simplex link type */ "SimpleX one-time invitation" = "SimpleX одноразовая ссылка"; +/* No comment provided by engineer. */ +"SimpleX protocols reviewed by Trail of Bits." = "Аудит SimpleX протоколов от Trail of Bits."; + /* No comment provided by engineer. */ "Simplified incognito mode" = "Упрощенный режим Инкогнито"; +/* No comment provided by engineer. */ +"Size" = "Размер"; + /* No comment provided by engineer. */ "Skip" = "Пропустить"; @@ -3204,31 +4716,65 @@ "Small groups (max 20)" = "Маленькие группы (до 20)"; /* No comment provided by engineer. */ -"SMP servers" = "SMP серверы"; +"SMP server" = "SMP сервер"; + +/* No comment provided by engineer. */ +"SOCKS proxy" = "SOCKS прокси"; + +/* blur media */ +"Soft" = "Слабое"; + +/* No comment provided by engineer. */ +"Some app settings were not migrated." = "Некоторые настройки приложения не были перенесены."; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "Некоторые файл(ы) не были экспортированы:"; /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Во время импорта произошли некоторые ошибки - для получения более подробной информации вы можете обратиться к консоли."; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "Во время импорта произошли некоторые ошибки:"; + +/* alert message */ +"Some servers failed the test:\n%@" = "Серверы не прошли тест:\n%@"; + /* notification title */ "Somebody" = "Контакт"; +/* blocking reason +report reason */ +"Spam" = "Спам"; + /* No comment provided by engineer. */ -"Start a new chat" = "Начать новый разговор"; +"Square, circle, or anything in between." = "Квадрат, круг и все, что между ними."; + +/* chat item text */ +"standard end-to-end encryption" = "стандартное end-to-end шифрование"; /* No comment provided by engineer. */ "Start chat" = "Запустить чат"; +/* No comment provided by engineer. */ +"Start chat?" = "Запустить чат?"; + /* No comment provided by engineer. */ "Start migration" = "Запустить перемещение данных"; +/* No comment provided by engineer. */ +"Starting from %@." = "Начиная с %@."; + /* No comment provided by engineer. */ "starting…" = "инициализация…"; +/* No comment provided by engineer. */ +"Statistics" = "Статистика"; + /* No comment provided by engineer. */ "Stop" = "Остановить"; /* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Остановите чат, чтобы разблокировать операции с архивом чата"; +"Stop chat" = "Остановить чат"; /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Остановите чат, чтобы экспортировать или импортировать архив чата или удалить данные чата. Вы не сможете получать и отправлять сообщения, пока чат остановлен."; @@ -3245,36 +4791,66 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Остановить отправку файла?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Прекратить делиться"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Прекратить делиться адресом?"; /* authentication reason */ "Stop SimpleX" = "Остановить SimpleX"; +/* No comment provided by engineer. */ +"Stopping chat" = "Остановка чата"; + +/* No comment provided by engineer. */ +"Storage" = "Хранилище"; + /* No comment provided by engineer. */ "strike" = "зачеркнуть"; +/* blur media */ +"Strong" = "Сильное"; + /* No comment provided by engineer. */ "Submit" = "Продолжить"; +/* No comment provided by engineer. */ +"Subscribed" = "Подписано"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Ошибки подписки"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Подписок игнорировано"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Поддержать SimpleX Chat"; +/* No comment provided by engineer. */ +"Switch audio and video during the call." = "Переключайте звук и видео во время звонка."; + +/* No comment provided by engineer. */ +"Switch chat profile for 1-time invitations." = "Переключайте профиль чата для одноразовых приглашений."; + /* No comment provided by engineer. */ "System" = "Системная"; /* No comment provided by engineer. */ "System authentication" = "Системная аутентификация"; +/* No comment provided by engineer. */ +"Tail" = "Хвост"; + /* No comment provided by engineer. */ "Take picture" = "Сделать фото"; /* No comment provided by engineer. */ "Tap button " = "Нажмите кнопку "; +/* No comment provided by engineer. */ +"Tap Create SimpleX address in the menu to create it later." = "Нажмите Создать адрес SimpleX в меню, чтобы создать его позже."; + /* No comment provided by engineer. */ "Tap to activate profile." = "Нажмите, чтобы сделать профиль активным."; @@ -3288,11 +4864,20 @@ "Tap to join incognito" = "Нажмите, чтобы вступить инкогнито"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Нажмите, чтобы начать чат"; +"Tap to paste link" = "Нажмите, чтобы вставить ссылку"; + +/* No comment provided by engineer. */ +"Tap to scan" = "Нажмите, чтобы сканировать"; + +/* No comment provided by engineer. */ +"TCP connection" = "TCP-соединение"; /* No comment provided by engineer. */ "TCP connection timeout" = "Таймаут TCP соединения"; +/* No comment provided by engineer. */ +"TCP port for messaging" = "TCP-порт для отправки сообщений"; + /* No comment provided by engineer. */ "TCP_KEEPCNT" = "TCP_KEEPCNT"; @@ -3302,16 +4887,22 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* file error alert title */ +"Temporary file error" = "Временная ошибка файла"; + /* server test failure */ "Test failed at step %@." = "Ошибка теста на шаге %@."; +/* No comment provided by engineer. */ +"Test notifications" = "Протестировать уведомления"; + /* No comment provided by engineer. */ "Test server" = "Тестировать сервер"; /* No comment provided by engineer. */ "Test servers" = "Тестировать серверы"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Ошибка тестов!"; /* No comment provided by engineer. */ @@ -3323,15 +4914,24 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Благодаря пользователям – добавьте переводы через Weblate!"; -/* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Первая в мире платформа без идентификаторов пользователей."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Приложение может посылать Вам уведомления о сообщениях и запросах на соединение - уведомления можно включить в Настройках."; +/* No comment provided by engineer. */ +"The app protects your privacy by using different operators in each conversation." = "Приложение улучшает конфиденциальность используя разных операторов в каждом разговоре."; + +/* No comment provided by engineer. */ +"The app will ask to confirm downloads from unknown file servers (except .onion)." = "Приложение будет запрашивать подтверждение загрузки с неизвестных серверов (за исключением .onion адресов)."; + /* No comment provided by engineer. */ "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ылкой."; + +/* No comment provided by engineer. */ +"The connection reached the limit of undelivered messages, your contact may be offline." = "Соединение достигло предела недоставленных сообщений. Возможно, Ваш контакт не в сети."; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "Подтвержденное соединение будет отменено!"; @@ -3344,26 +4944,26 @@ /* 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." = "Хэш предыдущего сообщения отличается."; /* No comment provided by engineer. */ "The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "Неправильный ID предыдущего сообщения (меньше или равен предыдущему).\nЭто может произойти из-за ошибки программы, или когда соединение компроментировано."; -/* No comment provided by engineer. */ -"The message will be deleted for all members." = "Сообщение будет удалено для всех членов группы."; - -/* No comment provided by engineer. */ -"The message will be marked as moderated for all members." = "Сообщение будет помечено как удаленное для всех членов группы."; - -/* No comment provided by engineer. */ -"The next generation of private messaging" = "Новое поколение приватных сообщений"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Предыдущая версия данных чата не удалена при перемещении, её можно удалить."; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "Профиль отправляется только Вашим контактам."; +"Your profile is stored on your device and only shared with your contacts." = "Ваш профиль храниться на Вашем устройстве и отправляется только контактам."; + +/* No comment provided by engineer. */ +"The same conditions will apply to operator **%@**." = "Те же самые условия будут приняты для оператора **%@**."; + +/* No comment provided by engineer. */ +"The second preset operator in the app!" = "Второй оператор серверов в приложении!"; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Вторая галочка - знать, что доставлено! ✅"; @@ -3375,13 +4975,19 @@ "The servers for new connections of your current chat profile **%@**." = "Серверы для новых соединений Вашего текущего профиля чата **%@**."; /* No comment provided by engineer. */ -"Theme" = "Тема"; +"The servers for new files of your current chat profile **%@**." = "Серверы для новых файлов Вашего текущего профиля **%@**."; /* No comment provided by engineer. */ -"There should be at least one user profile." = "Должен быть хотя бы один профиль пользователя."; +"The text you pasted is not a SimpleX link." = "Вставленный текст не является SimpleX-ссылкой."; /* No comment provided by engineer. */ -"There should be at least one visible user profile." = "Должен быть хотя бы один открытый профиль пользователя."; +"The uploaded database archive will be permanently removed from the servers." = "Загруженный архив базы данных будет навсегда удален с серверов."; + +/* No comment provided by engineer. */ +"Themes" = "Темы"; + +/* No comment provided by engineer. */ +"These conditions will also apply for: **%@**." = "Эти условия также будут применены к: **%@**."; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Установки для Вашего активного профиля **%@**."; @@ -3395,9 +5001,18 @@ /* 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." = "Это действие нельзя отменить — все сообщения, отправленные или полученные раньше чем выбрано, будут удалены. Это может занять несколько минут."; +/* 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." = "Это действие нельзя отменить — Ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны."; +/* E2EE info chat item */ +"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 шифрованием."; + /* notification title */ "this contact" = "этот контакт"; @@ -3405,7 +5020,7 @@ "This device name" = "Имя этого устройства"; /* No comment provided by engineer. */ -"This group has over %lld members, delivery receipts are not sent." = "В группе более %lld членов, отчёты о доставке выключены."; +"This display name is invalid. Please choose another name." = "Ошибка имени профиля. Пожалуйста, выберите другое имя."; /* No comment provided by engineer. */ "This group no longer exists." = "Эта группа больше не существует."; @@ -3416,9 +5031,18 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Это ваш собственный адрес SimpleX!"; +/* No comment provided by engineer. */ +"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." = "Это сообщение было удалено или еще не получено."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Эта настройка применяется к сообщениям в Вашем текущем профиле чата **%@**."; +/* No comment provided by engineer. */ +"Title" = "Заголовок"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Чтобы задать вопросы и получать уведомления о новых версиях,"; @@ -3432,7 +5056,7 @@ "To make a new connection" = "Чтобы соединиться"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Чтобы защитить Вашу конфиденциальность, вместо ID пользователей, которые есть в других платформах, SimpleX использует ID для очередей сообщений, разные для каждого контакта."; +"To protect against your link being replaced, you can compare contact security codes." = "Чтобы защитить Вашу ссылку от замены, Вы можете сравнить код безопасности."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Чтобы защитить Ваш часовой пояс, файлы картинок и голосовых сообщений используют UTC."; @@ -3440,24 +5064,60 @@ /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Чтобы защитить Вашу информацию, включите блокировку SimpleX Chat.\nВам будет нужно пройти аутентификацию для включения блокировки."; +/* No comment provided by engineer. */ +"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го контакта."; + +/* No comment provided by engineer. */ +"To receive" = "Для получения"; + +/* No comment provided by engineer. */ +"To record speech please grant permission to use Microphone." = "Для записи речи, пожалуйста, дайте разрешение на использование микрофона."; + +/* No comment provided by engineer. */ +"To record video please grant permission to use Camera." = "Для записи видео, пожалуйста, дайте разрешение на использование камеры."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Для записи голосового сообщения, пожалуйста разрешите доступ к микрофону."; /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Чтобы показать Ваш скрытый профиль, введите его пароль в поле поиска на странице **Ваши профили чата**."; +/* No comment provided by engineer. */ +"To send" = "Для оправки"; + /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Для поддержки мгновенный доставки уведомлений данные чата должны быть перемещены."; +/* No comment provided by engineer. */ +"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:" = "Переключите список чатов:"; + /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Установите режим Инкогнито при соединении."; +/* token status */ +"Token status: %@." = "Статус токена: %@."; + +/* No comment provided by engineer. */ +"Toolbar opacity" = "Прозрачность тулбара"; + +/* No comment provided by engineer. */ +"Total" = "Всего"; + /* No comment provided by engineer. */ "Transport isolation" = "Отдельные сессии для"; +/* No comment provided by engineer. */ +"Transport sessions" = "Транспортные сессии"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта (ошибка: %@)."; @@ -3465,10 +5125,10 @@ "Trying to connect to the server used to receive messages from this contact." = "Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта."; /* No comment provided by engineer. */ -"Turn off" = "Выключить"; +"Turkish interface" = "Турецкий интерфейс"; /* No comment provided by engineer. */ -"Turn off notifications?" = "Выключить уведомления?"; +"Turn off" = "Выключить"; /* No comment provided by engineer. */ "Turn on" = "Включить"; @@ -3480,18 +5140,18 @@ "Unblock" = "Разблокировать"; /* No comment provided by engineer. */ -"Unblock member" = "Разблокировать члена группы"; +"Unblock for all" = "Разблокировать для всех"; + +/* rcv group event chat item */ +"unblocked %@" = "%@ разблокирован"; /* No comment provided by engineer. */ -"Unblock member?" = "Разблокировать члена группы?"; - -/* item status description */ -"Unexpected error: %@" = "Неожиданная ошибка: %@"; +"Undelivered messages" = "Недоставленные сообщения"; /* No comment provided by engineer. */ "Unexpected migration state" = "Неожиданная ошибка при перемещении данных чата"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Не избр."; /* No comment provided by engineer. */ @@ -3518,6 +5178,15 @@ /* No comment provided by engineer. */ "Unknown error" = "Неизвестная ошибка"; +/* No comment provided by engineer. */ +"unknown servers" = "неизвестные серверы"; + +/* alert title */ +"Unknown servers!" = "Неизвестные серверы!"; + +/* No comment provided by engineer. */ +"unknown status" = "неизвестный статус"; + /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Если Вы не используете интерфейс iOS, включите режим Не отвлекать, чтобы звонок не прерывался."; @@ -3536,18 +5205,18 @@ /* authentication reason */ "Unlock app" = "Разблокировать"; -/* No comment provided by engineer. */ +/* notification label action */ "Unmute" = "Уведомлять"; /* No comment provided by engineer. */ +"unprotected" = "незащищённый"; + +/* swipe action */ "Unread" = "Не прочитано"; /* No comment provided by engineer. */ "Update" = "Обновить"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Обновить настройки .onion хостов?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Поменять пароль"; @@ -3555,32 +5224,59 @@ "Update network settings?" = "Обновить настройки сети?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Обновить режим отдельных сессий?"; +"Update settings?" = "Обновить настройки?"; + +/* No comment provided by engineer. */ +"Updated conditions" = "Обновленные условия"; /* rcv group event chat item */ "updated group profile" = "обновил(а) профиль группы"; +/* profile update event chat item */ +"updated profile" = "профиль обновлён"; + /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "Обновление настроек приведет к сбросу и установке нового соединения со всеми серверами."; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Обновление этих настроек приведет к сбросу и установке нового соединения со всеми серверами."; +"Upgrade and open chat" = "Обновить и открыть чат"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "Обновить и открыть чат"; +"Upload errors" = "Ошибки загрузки"; + +/* No comment provided by engineer. */ +"Upload failed" = "Ошибка загрузки"; /* server test step */ "Upload file" = "Загрузка файла"; +/* No comment provided by engineer. */ +"Uploaded" = "Загружено"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Отправленные файлы"; + +/* No comment provided by engineer. */ +"Uploading archive" = "Загрузка архива"; + /* No comment provided by engineer. */ "Use .onion hosts" = "Использовать .onion хосты"; +/* No comment provided by engineer. */ +"Use %@" = "Использовать %@"; + /* No comment provided by engineer. */ "Use chat" = "Использовать чат"; /* No comment provided by engineer. */ "Use current profile" = "Использовать активный профиль"; +/* No comment provided by engineer. */ +"Use for files" = "Использовать для файлов"; + +/* No comment provided by engineer. */ +"Use for messages" = "Использовать для сообщений"; + /* No comment provided by engineer. */ "Use for new connections" = "Использовать для новых соединений"; @@ -3593,17 +5289,44 @@ /* No comment provided by engineer. */ "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 адрес не защищен."; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "Использовать конфиденциальную доставку с неизвестными серверами."; + /* No comment provided by engineer. */ "Use server" = "Использовать сервер"; +/* No comment provided by engineer. */ +"Use servers" = "Использовать серверы"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "Использовать серверы предосталенные SimpleX Chat?"; /* No comment provided by engineer. */ -"User profile" = "Профиль чата"; +"Use SOCKS proxy" = "Использовать SOCKS прокси"; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Для использования .onion хостов требуется совместимый VPN провайдер."; +"Use TCP port %@ when no port is specified." = "Использовать TCP-порт %@, когда порт не указан."; + +/* No comment provided by engineer. */ +"Use the app while in the call." = "Используйте приложение во время звонка."; + +/* No comment provided by engineer. */ +"Use the app with one hand." = "Используйте приложение одной рукой."; + +/* No comment provided by engineer. */ +"Use web port" = "Использовать веб-порт"; + +/* No comment provided by engineer. */ +"User selection" = "Выбор пользователя"; + +/* No comment provided by engineer. */ +"Username" = "Имя пользователя"; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Используются серверы, предоставленные SimpleX Chat."; @@ -3626,6 +5349,12 @@ /* No comment provided by engineer. */ "Verify connections" = "Проверять соединения"; +/* No comment provided by engineer. */ +"Verify database passphrase" = "Проверка пароля базы данных"; + +/* No comment provided by engineer. */ +"Verify passphrase" = "Проверить пароль"; + /* No comment provided by engineer. */ "Verify security code" = "Подтвердить код безопасности"; @@ -3647,6 +5376,9 @@ /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Через безопасный квантово-устойчивый протокол."; +/* No comment provided by engineer. */ +"video" = "видеозвонок"; + /* No comment provided by engineer. */ "Video call" = "Видеозвонок"; @@ -3662,9 +5394,18 @@ /* No comment provided by engineer. */ "Videos and files up to 1gb" = "Видео и файлы до 1гб"; +/* No comment provided by engineer. */ +"View conditions" = "Посмотреть условия"; + /* No comment provided by engineer. */ "View security code" = "Показать код безопасности"; +/* No comment provided by engineer. */ +"View updated conditions" = "Посмотреть измененные условия"; + +/* chat feature */ +"Visible history" = "Доступ к истории"; + /* No comment provided by engineer. */ "Voice message…" = "Голосовое сообщение…"; @@ -3675,7 +5416,10 @@ "Voice messages are prohibited in this chat." = "Голосовые сообщения запрещены в этом чате."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Голосовые сообщения запрещены в этой группе."; +"Voice messages are prohibited." = "Голосовые сообщения запрещены в этой группе."; + +/* No comment provided by engineer. */ +"Voice messages not allowed" = "Голосовые сообщения не разрешены"; /* No comment provided by engineer. */ "Voice messages prohibited!" = "Голосовые сообщения запрещены!"; @@ -3698,9 +5442,18 @@ /* No comment provided by engineer. */ "Waiting for video" = "Ожидание видео"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Рисунок обоев"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Фон обоев"; + /* No comment provided by engineer. */ "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" = "Внимание: запуск чата на нескольких устройствах не поддерживается и приведет к сбоям доставки сообщений"; + /* No comment provided by engineer. */ "Warning: you may lose some data!" = "Предупреждение: Вы можете потерять какие то данные!"; @@ -3716,6 +5469,9 @@ /* No comment provided by engineer. */ "Welcome message" = "Приветственное сообщение"; +/* No comment provided by engineer. */ +"Welcome message is too long" = "Приветственное сообщение слишком длинное"; + /* No comment provided by engineer. */ "What's new" = "Новые функции"; @@ -3723,28 +5479,64 @@ "When available" = "Когда возможно"; /* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Когда Вы получите запрос на соединение, Вы можете принять или отклонить его."; +"When connecting audio and video calls." = "Во время соединения аудио и видео звонков."; + +/* No comment provided by engineer. */ +"when IP hidden" = "когда IP защищен"; + +/* No comment provided by engineer. */ +"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." = "Когда Вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом."; +/* No comment provided by engineer. */ +"WiFi" = "WiFi"; + +/* No comment provided by engineer. */ +"Will be enabled in direct chats!" = "Будет включено в прямых разговорах!"; + +/* No comment provided by engineer. */ +"Wired ethernet" = "Проводная сеть"; + +/* No comment provided by engineer. */ +"With encrypted files and media." = "С зашифрованными файлами и медиа."; + /* No comment provided by engineer. */ "With optional welcome message." = "С опциональным авто-ответом."; +/* No comment provided by engineer. */ +"With reduced battery usage." = "С уменьшенным потреблением батареи."; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "Без Тора или ВПН, Ваш IP адрес будет доступен серверам файлов."; + +/* alert message */ +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Без Тора или ВПН, Ваш IP адрес будет доступен этим серверам файлов: %@."; + /* No comment provided by engineer. */ "Wrong database passphrase" = "Неправильный пароль базы данных"; +/* snd error text */ +"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." = "Неверный ключ или неизвестный адрес блока файла - скорее всего, файл удален."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Неправильный пароль!"; /* No comment provided by engineer. */ -"XFTP servers" = "XFTP серверы"; +"XFTP server" = "XFTP сервер"; /* pref value */ "yes" = "да"; /* No comment provided by engineer. */ -"You" = "Вы"; +"you" = "Вы"; + +/* No comment provided by engineer. */ +"You **must not** use the same database on two devices." = "Вы **не должны** использовать одну и ту же базу данных на двух устройствах."; /* No comment provided by engineer. */ "You accepted connection" = "Вы приняли приглашение соединиться"; @@ -3758,6 +5550,9 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "Вы уже соединены с контактом %@."; +/* No comment provided by engineer. */ +"You are already connected with %@." = "Вы уже соединены с %@."; + /* No comment provided by engineer. */ "You are already connecting to %@." = "Вы уже соединяетесь с %@."; @@ -3788,14 +5583,23 @@ /* No comment provided by engineer. */ "You are invited to group" = "Вы приглашены в группу"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Вы не подключены к этим серверам. Для доставки сообщений на них используется конфиденциальная доставка."; + /* No comment provided by engineer. */ "you are observer" = "только чтение сообщений"; +/* snd group event chat item */ +"you blocked %@" = "Вы заблокировали %@"; + /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Вы можете принимать звонки на экране блокировки, без аутентификации."; /* No comment provided by engineer. */ -"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "Вы также можете соединиться, открыв ссылку. Если ссылка откроется в браузере, нажмите кнопку **Open in mobile app**."; +"You can change it in Appearance settings." = "Вы можете изменить это в настройках Интерфейса."; + +/* No comment provided by engineer. */ +"You can configure servers via settings." = "Вы можете настроить серверы позже."; /* No comment provided by engineer. */ "You can create it later" = "Вы можете создать его позже"; @@ -3806,26 +5610,35 @@ /* No comment provided by engineer. */ "You can enable them later via app Privacy & Security settings." = "Вы можете включить их позже в настройках Конфиденциальности."; +/* No comment provided by engineer. */ +"You can give another try." = "Вы можете попробовать еще раз."; + /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "Вы можете скрыть профиль или выключить уведомления - потяните его вправо."; +/* No comment provided by engineer. */ +"You can make it visible to your SimpleX contacts via Settings." = "Вы можете сделать его видимым для ваших контактов в SimpleX через Настройки."; + /* notification body */ -"You can now send messages to %@" = "Вы теперь можете отправлять сообщения %@"; +"You can now chat with %@" = "Вы теперь можете общаться с %@"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "Вы можете отправлять сообщения %@ из Архивированных контактов."; + +/* No comment provided by engineer. */ +"You can set connection name, to remember who the link was shared with." = "Вы можете установить имя соединения, чтобы запомнить кому Вы отправили ссылку."; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Вы можете установить просмотр уведомлений на экране блокировки в настройках."; -/* No comment provided by engineer. */ -"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились."; - /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Вы можете поделиться этим адресом с Вашими контактами, чтобы они могли соединиться с **%@**."; /* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Вы можете использовать Ваш адрес как ссылку или как QR код - кто угодно сможет соединиться с Вами."; +"You can start chat via app Settings / Database or by restarting the app" = "Вы можете запустить чат через Настройки приложения или перезапустив приложение."; /* No comment provided by engineer. */ -"You can start chat via app Settings / Database or by restarting the app" = "Вы можете запустить чат через Настройки приложения или перезапустив приложение."; +"You can still view conversation with %@ in the list of chats." = "Вы по-прежнему можете просмотреть разговор с %@ в списке чатов."; /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Вы можете включить Блокировку SimpleX через Настройки."; @@ -3833,6 +5646,9 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Вы можете форматировать сообщения:"; +/* alert message */ +"You can view invitation link again in connection details." = "Вы можете увидеть ссылку-приглашение снова открыв соединение."; + /* No comment provided by engineer. */ "You can't send messages!" = "Вы не можете отправлять сообщения!"; @@ -3849,10 +5665,10 @@ "you changed role of %@ to %@" = "Вы поменяли роль члена %1$@ на: %2$@"; /* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Вы определяете через какие серверы Вы **получаете сообщения**, Ваши контакты - серверы, которые Вы используете для отправки."; +"You could not be verified; please try again." = "Верификация не удалась; пожалуйста, попробуйте ещё раз."; /* No comment provided by engineer. */ -"You could not be verified; please try again." = "Верификация не удалась; пожалуйста, попробуйте ещё раз."; +"You decide who can connect." = "Вы определяете, кто может соединиться."; /* No comment provided by engineer. */ "You have already requested connection via this address!" = "Вы уже запросили соединение через этот адрес!"; @@ -3860,9 +5676,6 @@ /* No comment provided by engineer. */ "You have already requested connection!\nRepeat connection request?" = "Вы уже запросили соединение!\nПовторить запрос?"; -/* No comment provided by engineer. */ -"You have no chats" = "У Вас нет чатов"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Пароль не сохранен на устройстве — Вы будете должны ввести его при каждом запуске чата."; @@ -3872,15 +5685,21 @@ /* No comment provided by engineer. */ "You joined this group" = "Вы вступили в эту группу"; -/* No comment provided by engineer. */ -"You joined this group. Connecting to inviting group member." = "Вы вступили в эту группу. Устанавливается соединение с пригласившим членом группы."; - /* snd group event chat item */ "you left" = "Вы покинули группу"; +/* No comment provided by engineer. */ +"You may migrate the exported database." = "Вы можете мигрировать экспортированную базу данных."; + +/* No comment provided by engineer. */ +"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." = "Вы должны всегда использовать самую новую версию данных чата, ТОЛЬКО на одном устройстве, иначе Вы можете перестать получать сообщения от каких то контактов."; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "Чтобы включить звонки, разрешите их Вашему контакту."; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "Чтобы включить отправку голосовых сообщений, разрешите их Вашему контакту."; @@ -3899,6 +5718,12 @@ /* chat list item description */ "you shared one-time link incognito" = "Вы создали ссылку инкогнито"; +/* token info */ +"You should receive notifications." = "Вы должны получать уведомления."; + +/* snd group event chat item */ +"you unblocked %@" = "Вы разблокировали %@"; + /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже!"; @@ -3915,10 +5740,10 @@ "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 connect to all group members." = "Вы соединитесь со всеми членами группы."; +"You will still receive calls and notifications from muted profiles when they are active." = "Вы все равно получите звонки и уведомления в профилях без звука, когда они активные."; /* No comment provided by engineer. */ -"You will still receive calls and notifications from muted profiles when they are active." = "Вы все равно получите звонки и уведомления в профилях без звука, когда они активные."; +"You will stop receiving messages from this chat. Chat history will be preserved." = "Вы прекратите получать сообщения в этом разговоре. История будет сохранена."; /* No comment provided by engineer. */ "You will stop receiving messages from this group. Chat history will be preserved." = "Вы перестанете получать сообщения от этой группы. История чата будет сохранена."; @@ -3935,9 +5760,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Вы используете инкогнито профиль для этой группы - чтобы предотвратить раскрытие Вашего основного профиля, приглашать контакты не разрешено"; -/* No comment provided by engineer. */ -"Your %@ servers" = "Ваши %@ серверы"; - /* No comment provided by engineer. */ "Your calls" = "Ваши звонки"; @@ -3947,11 +5769,14 @@ /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "База данных НЕ зашифрована. Установите пароль, чтобы защитить Ваши данные."; +/* alert title */ +"Your chat preferences" = "Ваши настройки чата"; + /* No comment provided by engineer. */ "Your chat profiles" = "Ваши профили чата"; /* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Ваш контакт должен быть в сети чтобы установить соединение.\nВы можете отменить соединение и удалить контакт (и попробовать позже с другой ссылкой)."; +"Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Соединение было перемещено на %@, но при смене профиля произошла неожиданная ошибка."; /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ваш контакт отправил файл, размер которого превышает максимальный размер (%@)."; @@ -3960,10 +5785,10 @@ "Your contacts can allow full message deletion." = "Ваши контакты могут разрешить окончательное удаление сообщений."; /* No comment provided by engineer. */ -"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "Ваши контакты в SimpleX получат этот адрес.\nВы можете изменить это в Настройках."; +"Your contacts will remain connected." = "Ваши контакты сохранятся."; /* No comment provided by engineer. */ -"Your contacts will remain connected." = "Ваши контакты сохранятся."; +"Your credentials may be sent unencrypted." = "Ваши учетные данные могут быть отправлены в незашифрованном виде."; /* No comment provided by engineer. */ "Your current chat database will be DELETED and REPLACED with the imported one." = "Текущие данные Вашего чата будет УДАЛЕНЫ и ЗАМЕНЕНЫ импортированными."; @@ -3987,7 +5812,10 @@ "Your profile **%@** will be shared." = "Будет отправлен Ваш профиль **%@**."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам.\nSimpleX серверы не могут получить доступ к Вашему профилю."; +"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." = "Ваш профиль был изменен. Если вы сохраните его, обновленный профиль будет отправлен всем вашим контактам."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Ваш профиль, контакты и доставленные сообщения хранятся на Вашем устройстве."; @@ -3996,10 +5824,10 @@ "Your random profile" = "Случайный профиль"; /* No comment provided by engineer. */ -"Your server" = "Ваш сервер"; +"Your server address" = "Адрес Вашего сервера"; /* No comment provided by engineer. */ -"Your server address" = "Адрес Вашего сервера"; +"Your servers" = "Ваши серверы"; /* No comment provided by engineer. */ "Your settings" = "Настройки"; @@ -4007,9 +5835,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "Ваш адрес SimpleX"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Ваши SMP серверы"; - -/* No comment provided by engineer. */ -"Your XFTP servers" = "Ваши XFTP серверы"; - diff --git a/apps/ios/sounds/connecting_call.mp3 b/apps/ios/sounds/connecting_call.mp3 new file mode 100644 index 0000000000..fc425bab97 Binary files /dev/null and b/apps/ios/sounds/connecting_call.mp3 differ diff --git a/apps/ios/sounds/in_call.mp3 b/apps/ios/sounds/in_call.mp3 new file mode 100644 index 0000000000..1049be4462 Binary files /dev/null and b/apps/ios/sounds/in_call.mp3 differ diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index eb47218422..57c0466eb9 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -1,18 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (สามารถคัดลอกได้)"; @@ -25,24 +10,9 @@ /* No comment provided by engineer. */ "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- ข้อความเสียงนานสุด 5 นาที\n- เวลาที่กำหนดเองที่จะหายไป\n- ประวัติการแก้ไข"; -/* No comment provided by engineer. */ -", " = ", "; - -/* No comment provided by engineer. */ -": " = ": "; - /* No comment provided by engineer. */ "!1 colored!" = "!1 มีสี!"; -/* No comment provided by engineer. */ -"." = "."; - -/* No comment provided by engineer. */ -"(" = "("; - -/* No comment provided by engineer. */ -")" = ")"; - /* No comment provided by engineer. */ "[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[มีส่วนร่วม](https://github.com/simplex-chat/simplex-chat#contribute)"; @@ -52,12 +22,6 @@ /* No comment provided by engineer. */ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[ติดดาวบน GitHub](https://github.com/simplex-chat/simplex-chat)"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**เพิ่มผู้ติดต่อใหม่**: เพื่อสร้างคิวอาร์โค้ดแบบใช้ครั้งเดียวหรือลิงก์สำหรับผู้ติดต่อของคุณ"; - -/* No comment provided by engineer. */ -"**Create link / QR code** for your contact to use." = "**สร้างลิงค์ / คิวอาร์โค้ด** เพื่อให้ผู้ติดต่อของคุณใช้"; - /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "การโทรเสียงแบบ **encrypted จากต้นจนจบ**"; @@ -65,22 +29,16 @@ "**e2e encrypted** video call" = "**encrypted จากต้นจนจบ** การสนทนาทางวิดีโอ"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**เป็นส่วนตัวมากขึ้น**: ตรวจสอบข้อความใหม่ทุกๆ 20 นาที โทเค็นอุปกรณ์แชร์กับเซิร์ฟเวอร์ SimpleX Chat แต่ไม่ระบุจำนวนผู้ติดต่อหรือข้อความที่คุณมี"; +"**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." = "**เป็นส่วนตัวมากขึ้น**: ตรวจสอบข้อความใหม่ทุกๆ 20 นาที โทเค็นอุปกรณ์แชร์กับเซิร์ฟเวอร์ SimpleX Chat แต่ไม่ระบุจำนวนผู้ติดต่อหรือข้อความที่คุณมี"; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**ส่วนตัวที่สุด**: ไม่ใช้เซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat ตรวจสอบข้อความเป็นระยะในพื้นหลัง (ขึ้นอยู่กับความถี่ที่คุณใช้แอป)"; - -/* No comment provided by engineer. */ -"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**แปะลิงก์ที่ได้รับ** หรือเปิดในเบราว์เซอร์แล้วแตะ **เปิดในแอปมือถือ**"; +"**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." = "**ส่วนตัวที่สุด**: ไม่ใช้เซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat ตรวจสอบข้อความเป็นระยะในพื้นหลัง (ขึ้นอยู่กับความถี่ที่คุณใช้แอป)"; /* No comment provided by engineer. */ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**โปรดทราบ**: คุณจะไม่สามารถกู้คืนหรือเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย"; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**แนะนำ**: โทเค็นอุปกรณ์และการแจ้งเตือนจะถูกส่งไปยังเซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat แต่ไม่ใช่เนื้อหาข้อความ ขนาด หรือผู้ที่ส่ง"; - -/* No comment provided by engineer. */ -"**Scan QR code**: to connect to your contact in person or via video call." = "**สแกนคิวอาร์โค้ด**: เพื่อเชื่อมต่อกับผู้ติดต่อของคุณด้วยตนเองหรือผ่านการสนทนาทางวิดีโอ"; +"**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 แต่ไม่ใช่เนื้อหาข้อความ ขนาด หรือผู้ที่ส่ง"; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**คำเตือน**: การแจ้งเตือนแบบพุชทันทีจำเป็นต้องบันทึกรหัสผ่านไว้ใน Keychain"; @@ -118,9 +76,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ ได้รับการตรวจสอบแล้ว"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ เซิร์ฟเวอร์"; - /* notification title */ "%@ wants to connect!" = "%@ อยากเชื่อมต่อ!"; @@ -166,9 +121,6 @@ /* No comment provided by engineer. */ "%lld minutes" = "%lld นาที"; -/* No comment provided by engineer. */ -"%lld second(s)" = "%lld วินาที"; - /* No comment provided by engineer. */ "%lld seconds" = "%lld วินาที"; @@ -211,7 +163,8 @@ /* No comment provided by engineer. */ "0s" = "0s"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 วัน"; /* time interval */ @@ -220,15 +173,14 @@ /* No comment provided by engineer. */ "1 minute" = "1 นาที"; -/* time interval */ +/* delete after time +time interval */ "1 month" = "1 เดือน"; -/* time interval */ +/* delete after time +time interval */ "1 week" = "1 สัปดาห์"; -/* No comment provided by engineer. */ -"1-time link" = "ลิงก์สำหรับใช้ 1 ครั้ง"; - /* No comment provided by engineer. */ "5 minutes" = "5 นาที"; @@ -259,29 +211,22 @@ /* No comment provided by engineer. */ "Abort changing address?" = "ยกเลิกการเปลี่ยนที่อยู่?"; -/* No comment provided by engineer. */ -"About SimpleX" = "เกี่ยวกับ SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "เกี่ยวกับที่อยู่ SimpleX"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "เกี่ยวกับ SimpleX Chat"; /* No comment provided by engineer. */ "above, then choose:" = "ด้านบน จากนั้นเลือก:"; -/* No comment provided by engineer. */ -"Accent color" = "สีเน้น"; - /* accept contact request via notification - accept incoming call via notification */ +accept incoming call via notification +swipe action */ "Accept" = "รับ"; /* notification body */ "Accept contact request from %@?" = "รับการขอติดต่อจาก %@?"; -/* accept contact request via notification */ +/* accept contact request via notification +swipe action */ "Accept incognito" = "ยอมรับโหมดไม่ระบุตัวตน"; /* call status */ @@ -290,14 +235,11 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "เพิ่มที่อยู่ลงในโปรไฟล์ของคุณ เพื่อให้ผู้ติดต่อของคุณสามารถแชร์กับผู้อื่นได้ การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ"; -/* No comment provided by engineer. */ -"Add preset servers" = "เพิ่มเซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า"; - /* No comment provided by engineer. */ "Add profile" = "เพิ่มโปรไฟล์"; /* No comment provided by engineer. */ -"Add server…" = "เพิ่มเซิร์ฟเวอร์…"; +"Add server" = "เพิ่มเซิร์ฟเวอร์"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "เพิ่มเซิร์ฟเวอร์โดยการสแกนรหัสคิวอาร์โค้ด"; @@ -360,7 +302,7 @@ "Allow disappearing messages only if your contact allows it to you." = "อนุญาตให้ข้อความที่หายไปเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตเท่านั้น."; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you." = "อนุญาตให้ลบข้อความแบบถาวรเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตให้คุณเท่านั้น"; +"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "อนุญาตให้ลบข้อความแบบถาวรเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตให้คุณเท่านั้น"; /* No comment provided by engineer. */ "Allow message reactions only if your contact allows them." = "อนุญาตการแสดงปฏิกิริยาต่อข้อความเฉพาะเมื่อผู้ติดต่อของคุณอนุญาตเท่านั้น"; @@ -375,7 +317,7 @@ "Allow sending disappearing messages." = "อนุญาตให้ส่งข้อความที่จะหายไปหลังปิดแชท (disappearing message)"; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages." = "อนุญาตให้ลบข้อความที่ส่งไปแล้วอย่างถาวร"; +"Allow to irreversibly delete sent messages. (24 hours)" = "อนุญาตให้ลบข้อความที่ส่งไปแล้วอย่างถาวร"; /* No comment provided by engineer. */ "Allow to send files and media." = "อนุญาตให้ส่งไฟล์และสื่อ"; @@ -396,7 +338,7 @@ "Allow your contacts to call you." = "อนุญาตให้ผู้ติดต่อของคุณโทรหาคุณ"; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages." = "อนุญาตให้ผู้ติดต่อของคุณลบข้อความที่ส่งแล้วอย่างถาวร"; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "อนุญาตให้ผู้ติดต่อของคุณลบข้อความที่ส่งแล้วอย่างถาวร"; /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "อนุญาตให้ผู้ติดต่อของคุณส่งข้อความที่จะหายไปหลังปิดแชท (disappearing messages)"; @@ -419,6 +361,9 @@ /* No comment provided by engineer. */ "Answer call" = "รับสาย"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้"; + /* No comment provided by engineer. */ "App build: %@" = "รุ่นแอป: %@"; @@ -504,7 +449,7 @@ "Both you and your contact can add message reactions." = "ทั้งคุณและผู้ติดต่อของคุณสามารถเพิ่มปฏิกิริยาของข้อความได้"; /* No comment provided by engineer. */ -"Both you and your contact can irreversibly delete sent messages." = "ทั้งคุณและผู้ติดต่อของคุณสามารถลบข้อความที่ส่งแล้วอย่างถาวรได้"; +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "ทั้งคุณและผู้ติดต่อของคุณสามารถลบข้อความที่ส่งแล้วอย่างถาวรได้"; /* No comment provided by engineer. */ "Both you and your contact can make calls." = "ทั้งคุณและผู้ติดต่อของคุณสามารถโทรออกได้"; @@ -533,16 +478,14 @@ /* No comment provided by engineer. */ "Calls" = "โทร"; -/* No comment provided by engineer. */ -"Can't delete user profile!" = "ไม่สามารถลบโปรไฟล์ผู้ใช้ได้!"; - /* No comment provided by engineer. */ "Can't invite contact!" = "ไม่สามารถเชิญผู้ติดต่อได้!"; /* No comment provided by engineer. */ "Can't invite contacts!" = "ไม่สามารถเชิญผู้ติดต่อได้!"; -/* No comment provided by engineer. */ +/* alert action +alert button */ "Cancel" = "ยกเลิก"; /* feature offered item */ @@ -551,7 +494,7 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "ไม่สามารถเข้าถึง keychain เพื่อบันทึกรหัสผ่านฐานข้อมูล"; -/* No comment provided by engineer. */ +/* alert title */ "Cannot receive file" = "ไม่สามารถรับไฟล์ได้"; /* No comment provided by engineer. */ @@ -582,7 +525,7 @@ "Change self-destruct mode" = "เปลี่ยนโหมดทําลายตัวเอง"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "เปลี่ยนรหัสผ่านแบบทำลายตัวเอง"; /* chat item text */ @@ -600,9 +543,6 @@ /* chat item text */ "changing address…" = "กำลังเปลี่ยนที่อยู่…"; -/* No comment provided by engineer. */ -"Chat archive" = "ที่เก็บแชทถาวร"; - /* No comment provided by engineer. */ "Chat console" = "คอนโซลแชท"; @@ -625,9 +565,12 @@ "Chat preferences" = "ค่ากําหนดในการแชท"; /* No comment provided by engineer. */ -"Chats" = "แชท"; +"Chat profile" = "โปรไฟล์ผู้ใช้"; /* No comment provided by engineer. */ +"Chats" = "แชท"; + +/* alert title */ "Check server address and try again." = "ตรวจสอบที่อยู่เซิร์ฟเวอร์แล้วลองอีกครั้ง"; /* No comment provided by engineer. */ @@ -639,7 +582,7 @@ /* No comment provided by engineer. */ "Choose from library" = "เลือกจากอัลบั้ม"; -/* No comment provided by engineer. */ +/* swipe action */ "Clear" = "ลบ"; /* No comment provided by engineer. */ @@ -654,9 +597,6 @@ /* No comment provided by engineer. */ "colored" = "มีสี"; -/* No comment provided by engineer. */ -"Colors" = "สี"; - /* server test step */ "Compare file" = "เปรียบเทียบไฟล์"; @@ -693,9 +633,6 @@ /* No comment provided by engineer. */ "Connect via link" = "เชื่อมต่อผ่านลิงก์"; -/* No comment provided by engineer. */ -"Connect via link / QR code" = "เชื่อมต่อผ่านลิงค์ / คิวอาร์โค้ด"; - /* No comment provided by engineer. */ "connected" = "เชื่อมต่อสำเร็จ"; @@ -723,7 +660,7 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "กำลังเชื่อมต่อกับเซิร์ฟเวอร์... (ข้อผิดพลาด: %@)"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "กำลังเชื่อมต่อ…"; /* No comment provided by engineer. */ @@ -765,9 +702,6 @@ /* notification */ "Contact is connected" = "เชื่อมต่อกับผู้ติดต่อแล้ว"; -/* No comment provided by engineer. */ -"Contact is not connected yet!" = "ผู้ติดต่อยังไม่ได้เชื่อมต่อ!"; - /* No comment provided by engineer. */ "Contact name" = "ชื่อผู้ติดต่อ"; @@ -783,7 +717,7 @@ /* No comment provided by engineer. */ "Continue" = "ดำเนินการต่อ"; -/* chat item action */ +/* No comment provided by engineer. */ "Copy" = "คัดลอก"; /* No comment provided by engineer. */ @@ -792,9 +726,6 @@ /* No comment provided by engineer. */ "Create" = "สร้าง"; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "สร้างที่อยู่เพื่อให้ผู้อื่นเชื่อมต่อกับคุณ"; - /* server test step */ "Create file" = "สร้างไฟล์"; @@ -804,9 +735,6 @@ /* No comment provided by engineer. */ "Create link" = "สร้างลิงค์"; -/* No comment provided by engineer. */ -"Create one-time invitation link" = "สร้างลิงก์เชิญแบบใช้ครั้งเดียว"; - /* server test step */ "Create queue" = "สร้างคิว"; @@ -819,9 +747,6 @@ /* No comment provided by engineer. */ "Create your profile" = "สร้างโปรไฟล์ของคุณ"; -/* No comment provided by engineer. */ -"Created on %@" = "สร้างเมื่อ %@"; - /* No comment provided by engineer. */ "creator" = "ผู้สร้าง"; @@ -909,7 +834,8 @@ /* message decrypt error item */ "Decryption error" = "ข้อผิดพลาดในการ decrypt"; -/* pref value */ +/* delete after time +pref value */ "default (%@)" = "ค่าเริ่มต้น (%@)"; /* No comment provided by engineer. */ @@ -918,7 +844,8 @@ /* No comment provided by engineer. */ "default (yes)" = "ค่าเริ่มต้น (ใช่)"; -/* chat item action */ +/* alert action +swipe action */ "Delete" = "ลบ"; /* No comment provided by engineer. */ @@ -933,12 +860,6 @@ /* No comment provided by engineer. */ "Delete all files" = "ลบไฟล์ทั้งหมด"; -/* No comment provided by engineer. */ -"Delete archive" = "ลบที่เก็บถาวร"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "ลบที่เก็บแชทถาวร?"; - /* No comment provided by engineer. */ "Delete chat profile" = "ลบโปรไฟล์แชท"; @@ -951,9 +872,6 @@ /* No comment provided by engineer. */ "Delete contact" = "ลบผู้ติดต่อ"; -/* No comment provided by engineer. */ -"Delete Contact" = "ลบผู้ติดต่อ"; - /* No comment provided by engineer. */ "Delete database" = "ลบฐานข้อมูล"; @@ -993,7 +911,7 @@ /* No comment provided by engineer. */ "Delete message?" = "ลบข้อความ?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "ลบข้อความ"; /* No comment provided by engineer. */ @@ -1005,9 +923,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "ลบฐานข้อมูลเก่า?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "ลบการเชื่อมต่อที่รอดำเนินการ"; - /* No comment provided by engineer. */ "Delete pending connection?" = "ลบการเชื่อมต่อที่รอดำเนินการหรือไม่?"; @@ -1069,7 +984,7 @@ "Direct messages" = "ข้อความโดยตรง"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "ข้อความโดยตรงระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้"; +"Direct messages between members are prohibited." = "ข้อความโดยตรงระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้"; /* No comment provided by engineer. */ "Disable (keep overrides)" = "ปิดใช้งาน (เก็บการแทนที่)"; @@ -1090,7 +1005,7 @@ "Disappearing messages are prohibited in this chat." = "ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) เป็นสิ่งต้องห้ามในแชทนี้"; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) เป็นสิ่งต้องห้ามในกลุ่มนี้"; +"Disappearing messages are prohibited." = "ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) เป็นสิ่งต้องห้ามในกลุ่มนี้"; /* No comment provided by engineer. */ "Disappears at" = "หายไปที่"; @@ -1146,7 +1061,7 @@ /* No comment provided by engineer. */ "Enable (keep overrides)" = "เปิดใช้งาน (เก็บการแทนที่)"; -/* No comment provided by engineer. */ +/* alert title */ "Enable automatic message deletion?" = "เปิดใช้งานการลบข้อความอัตโนมัติ?"; /* No comment provided by engineer. */ @@ -1275,9 +1190,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "เกิดข้อผิดพลาดในการรับคำขอติดต่อ"; -/* No comment provided by engineer. */ -"Error accessing database file" = "เกิดข้อผิดพลาดในการเข้าถึงไฟล์ฐานข้อมูล"; - /* No comment provided by engineer. */ "Error adding member(s)" = "เกิดข้อผิดพลาดในการเพิ่มสมาชิก"; @@ -1311,9 +1223,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "เกิดข้อผิดพลาดในการลบการเชื่อมต่อ"; -/* No comment provided by engineer. */ -"Error deleting contact" = "เกิดข้อผิดพลาดในการลบผู้ติดต่อ"; - /* No comment provided by engineer. */ "Error deleting database" = "เกิดข้อผิดพลาดในการลบฐานข้อมูล"; @@ -1344,18 +1253,12 @@ /* No comment provided by engineer. */ "Error joining group" = "เกิดข้อผิดพลาดในการเข้าร่วมกลุ่ม"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "โหลดเซิร์ฟเวอร์ %@ ผิดพลาด"; - -/* No comment provided by engineer. */ +/* alert title */ "Error receiving file" = "เกิดข้อผิดพลาดในการรับไฟล์"; /* No comment provided by engineer. */ "Error removing member" = "เกิดข้อผิดพลาดในการลบสมาชิก"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ %@"; - /* No comment provided by engineer. */ "Error saving group profile" = "เกิดข้อผิดพลาดในการบันทึกโปรไฟล์กลุ่ม"; @@ -1386,7 +1289,7 @@ /* No comment provided by engineer. */ "Error stopping chat" = "เกิดข้อผิดพลาดในการหยุดแชท"; -/* No comment provided by engineer. */ +/* alertTitle */ "Error switching profile!" = "เกิดข้อผิดพลาดในการเปลี่ยนโปรไฟล์!"; /* No comment provided by engineer. */ @@ -1407,7 +1310,9 @@ /* No comment provided by engineer. */ "Error: " = "ผิดพลาด: "; -/* No comment provided by engineer. */ +/* alert message +file error text +snd error text */ "Error: %@" = "ข้อผิดพลาด: % @"; /* No comment provided by engineer. */ @@ -1440,7 +1345,7 @@ /* No comment provided by engineer. */ "Fast and no wait until the sender is online!" = "รวดเร็วและไม่ต้องรอจนกว่าผู้ส่งจะออนไลน์!"; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "ที่ชอบ"; /* No comment provided by engineer. */ @@ -1462,7 +1367,7 @@ "Files and media" = "ไฟล์และสื่อ"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "ไฟล์และสื่อเป็นสิ่งต้องห้ามในกลุ่มนี้"; +"Files and media are prohibited." = "ไฟล์และสื่อเป็นสิ่งต้องห้ามในกลุ่มนี้"; /* No comment provided by engineer. */ "Files and media prohibited!" = "ไฟล์และสื่อต้องห้าม!"; @@ -1506,9 +1411,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "ชื่อเต็ม (ไม่บังคับ)"; -/* No comment provided by engineer. */ -"Full name:" = "ชื่อเต็ม:"; - /* No comment provided by engineer. */ "Fully re-implemented - work in background!" = "ดำเนินการใหม่อย่างสมบูรณ์ - ทำงานในพื้นหลัง!"; @@ -1548,24 +1450,6 @@ /* No comment provided by engineer. */ "Group links" = "ลิงค์กลุ่ม"; -/* No comment provided by engineer. */ -"Group members can add message reactions." = "สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้"; - -/* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages." = "สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร"; - -/* No comment provided by engineer. */ -"Group members can send direct messages." = "สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้"; - -/* No comment provided by engineer. */ -"Group members can send disappearing messages." = "สมาชิกกลุ่มสามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้"; - -/* No comment provided by engineer. */ -"Group members can send files and media." = "สมาชิกกลุ่มสามารถส่งไฟล์และสื่อ"; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "สมาชิกกลุ่มสามารถส่งข้อความเสียง"; - /* notification */ "Group message:" = "ข้อความกลุ่ม:"; @@ -1623,9 +1507,6 @@ /* time unit */ "hours" = "ชั่วโมง"; -/* No comment provided by engineer. */ -"How it works" = "มันทำงานอย่างไร"; - /* No comment provided by engineer. */ "How SimpleX works" = "วิธีการ SimpleX ทํางานอย่างไร"; @@ -1644,9 +1525,6 @@ /* No comment provided by engineer. */ "If you can't meet in person, show QR code in a video call, or share the link." = "หากคุณไม่สามารถพบกันในชีวิตจริงได้ ให้แสดงคิวอาร์โค้ดในวิดีโอคอล หรือแชร์ลิงก์"; -/* No comment provided by engineer. */ -"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "หากคุณไม่สามารถพบปะด้วยตนเอง คุณสามารถ **สแกนคิวอาร์โค้ดผ่านการสนทนาทางวิดีโอ** หรือผู้ติดต่อของคุณสามารถแชร์ลิงก์เชิญได้"; - /* No comment provided by engineer. */ "If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "หากคุณใส่รหัสผ่านนี้เมื่อเปิดแอป ข้อมูลแอปทั้งหมดจะถูกลบอย่างถาวร!"; @@ -1669,7 +1547,7 @@ "Immediately" = "โดยทันที"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "มีภูมิคุ้มกันต่อสแปมและการละเมิด"; +"Immune to spam" = "มีภูมิคุ้มกันต่อสแปมและการละเมิด"; /* No comment provided by engineer. */ "Import" = "นำเข้า"; @@ -1735,10 +1613,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "ติดตั้ง [SimpleX Chat สำหรับเทอร์มินัล](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "การแจ้งเตือนโดยทันทีจะถูกซ่อน!\n"; +"Instant" = "ทันที"; /* No comment provided by engineer. */ -"Instantly" = "ทันที"; +"Instant push notifications will be hidden!\n" = "การแจ้งเตือนโดยทันทีจะถูกซ่อน!\n"; /* No comment provided by engineer. */ "Interface" = "อินเตอร์เฟซ"; @@ -1755,7 +1633,7 @@ /* invalid chat item */ "invalid data" = "ข้อมูลไม่ถูกต้อง"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "ที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง!"; /* No comment provided by engineer. */ @@ -1798,7 +1676,7 @@ "Irreversible message deletion is prohibited in this chat." = "ไม่สามารถลบข้อความแบบแก้ไขไม่ได้ในแชทนี้"; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "การลบข้อความแบบแก้ไขไม่ได้เป็นสิ่งที่ห้ามในกลุ่มนี้"; +"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." = "อนุญาตให้มีการเชื่อมต่อที่ไม่ระบุตัวตนจำนวนมากโดยไม่มีข้อมูลที่ใช้ร่วมกันระหว่างกันในโปรไฟล์การแชทเดียว"; @@ -1821,7 +1699,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "อินเทอร์เฟซภาษาญี่ปุ่น"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "เข้าร่วม"; /* No comment provided by engineer. */ @@ -1851,7 +1729,7 @@ /* No comment provided by engineer. */ "Learn more" = "ศึกษาเพิ่มเติม"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "ออกจาก"; /* No comment provided by engineer. */ @@ -1893,24 +1771,15 @@ /* No comment provided by engineer. */ "Lock mode" = "โหมดล็อค"; -/* No comment provided by engineer. */ -"Make a private connection" = "สร้างการเชื่อมต่อแบบส่วนตัว"; - /* No comment provided by engineer. */ "Make one message disappear" = "ทำให้ข้อความหายไปหนึ่งข้อความ"; /* No comment provided by engineer. */ "Make profile private!" = "ทำให้โปรไฟล์เป็นส่วนตัว!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ %@ อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน (%@)"; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ WebRTC ICE อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน"; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "หลายคนถามว่า: *หาก SimpleX ไม่มีตัวระบุผู้ใช้ จะส่งข้อความได้อย่างไร?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "ทำเครื่องหมายว่าลบแล้วสำหรับทุกคน"; @@ -1947,6 +1816,24 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "สมาชิกจะถูกลบออกจากกลุ่ม - ไม่สามารถยกเลิกได้!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้"; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้"; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "สมาชิกกลุ่มสามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้"; + +/* No comment provided by engineer. */ +"Members can send files and media." = "สมาชิกกลุ่มสามารถส่งไฟล์และสื่อ"; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "สมาชิกกลุ่มสามารถส่งข้อความเสียง"; + /* item status text */ "Message delivery error" = "ข้อผิดพลาดในการส่งข้อความ"; @@ -1963,7 +1850,7 @@ "Message reactions are prohibited in this chat." = "ห้ามแสดงปฏิกิริยาบนข้อความในแชทนี้"; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "ปฏิกิริยาบนข้อความเป็นสิ่งต้องห้ามในกลุ่มนี้"; +"Message reactions are prohibited." = "ปฏิกิริยาบนข้อความเป็นสิ่งต้องห้ามในกลุ่มนี้"; /* notification */ "message received" = "ข้อความที่ได้รับ"; @@ -1990,7 +1877,7 @@ "Migration is completed" = "การโยกย้ายเสร็จสมบูรณ์"; /* No comment provided by engineer. */ -"Migrations: %@" = "การย้ายข้อมูล: %@"; +"Migrations:" = "การย้ายข้อมูล"; /* time unit */ "minutes" = "นาที"; @@ -2010,7 +1897,7 @@ /* copied message info */ "Moderated at: %@" = "กลั่นกรองที่: %@"; -/* No comment provided by engineer. */ +/* marked deleted chat item preview text */ "moderated by %@" = "กลั่นกรองโดย %@"; /* time unit */ @@ -2019,19 +1906,16 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "การปรับปรุงเพิ่มเติมกำลังจะมาเร็ว ๆ นี้!"; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "เป็นไปได้มากว่าผู้ติดต่อนี้ได้ลบการเชื่อมต่อกับคุณ"; - /* No comment provided by engineer. */ "Multiple chat profiles" = "โปรไฟล์การแชทหลายรายการ"; -/* No comment provided by engineer. */ +/* notification label action */ "Mute" = "ปิดเสียง"; /* No comment provided by engineer. */ "Muted when inactive!" = "ปิดเสียงเมื่อไม่ได้ใช้งาน!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "ชื่อ"; /* No comment provided by engineer. */ @@ -2043,7 +1927,7 @@ /* No comment provided by engineer. */ "Network status" = "สถานะเครือข่าย"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "ไม่เคย"; /* notification */ @@ -2052,9 +1936,6 @@ /* notification */ "New contact:" = "คำขอติดต่อใหม่:"; -/* No comment provided by engineer. */ -"New database archive" = "ฐานข้อมูลใหม่สำหรับการเก็บถาวร"; - /* No comment provided by engineer. */ "New display name" = "ชื่อที่แสดงใหม่"; @@ -2109,12 +1990,18 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "ไม่อนุญาตให้บันทึกข้อความเสียง"; +/* No comment provided by engineer. */ +"No push server" = "ในเครื่อง"; + /* No comment provided by engineer. */ "No received or sent files" = "ไม่มีไฟล์ที่ได้รับหรือส่ง"; /* copied message info in history */ "no text" = "ไม่มีข้อความ"; +/* No comment provided by engineer. */ +"No user identifiers." = "แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว"; + /* No comment provided by engineer. */ "Notifications" = "การแจ้งเตือน"; @@ -2128,31 +2015,25 @@ "observer" = "ผู้สังเกตการณ์"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "ปิด"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "ปิด"; -/* No comment provided by engineer. */ -"Off (Local)" = "ปิด (ในเครื่อง)"; - /* feature offered item */ "offered %@" = "เสนอแล้ว %@"; /* feature offered item */ "offered %@: %@" = "เสนอแล้ว %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "ตกลง"; /* No comment provided by engineer. */ "Old database" = "ฐานข้อมูลเก่า"; -/* No comment provided by engineer. */ -"Old database archive" = "คลังฐานข้อมูลเก่า"; - /* group pref value */ "on" = "เปิด"; @@ -2160,16 +2041,16 @@ "One-time invitation link" = "ลิงก์คำเชิญแบบใช้ครั้งเดียว"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ"; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ"; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ"; +"Onion hosts will be used when available.\nRequires compatible VPN." = "จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ"; /* No comment provided by engineer. */ "Onion hosts will not be used." = "โฮสต์หัวหอมจะไม่ถูกใช้"; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "เฉพาะอุปกรณ์ไคลเอนต์เท่านั้นที่จัดเก็บโปรไฟล์ผู้ใช้ ผู้ติดต่อ กลุ่ม และข้อความที่ส่งด้วย **การเข้ารหัส encrypt แบบ 2 ชั้น**"; +"Only client devices store user profiles, contacts, groups, and messages." = "เฉพาะอุปกรณ์ไคลเอนต์เท่านั้นที่จัดเก็บโปรไฟล์ผู้ใช้ ผู้ติดต่อ กลุ่ม และข้อความที่ส่งด้วย **การเข้ารหัส encrypt แบบ 2 ชั้น**"; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปลี่ยนค่ากําหนดลักษณะกลุ่มได้"; @@ -2184,7 +2065,7 @@ "Only you can add message reactions." = "มีเพียงคุณเท่านั้นที่สามารถแสดงปฏิกิริยาต่อข้อความได้"; /* No comment provided by engineer. */ -"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "มีเพียงคุณเท่านั้นที่สามารถลบข้อความแบบย้อนกลับไม่ได้ (ผู้ติดต่อของคุณสามารถทำเครื่องหมายเพื่อลบได้)"; +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "มีเพียงคุณเท่านั้นที่สามารถลบข้อความแบบย้อนกลับไม่ได้ (ผู้ติดต่อของคุณสามารถทำเครื่องหมายเพื่อลบได้)"; /* No comment provided by engineer. */ "Only you can make calls." = "มีเพียงคุณเท่านั้นที่โทรออกได้"; @@ -2199,7 +2080,7 @@ "Only your contact can add message reactions." = "เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถเพิ่มการโต้ตอบข้อความได้"; /* No comment provided by engineer. */ -"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถลบข้อความแบบย้อนกลับไม่ได้ (คุณสามารถทำเครื่องหมายเพื่อลบได้)"; +"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถลบข้อความแบบย้อนกลับไม่ได้ (คุณสามารถทำเครื่องหมายเพื่อลบได้)"; /* No comment provided by engineer. */ "Only your contact can make calls." = "ผู้ติดต่อของคุณเท่านั้นที่สามารถโทรออกได้"; @@ -2219,15 +2100,6 @@ /* No comment provided by engineer. */ "Open Settings" = "เปิดการตั้งค่า"; -/* authentication reason */ -"Open user profiles" = "เปิดโปรไฟล์ผู้ใช้"; - -/* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้"; - -/* No comment provided by engineer. */ -"Opening database…" = "กำลังเปิดฐานข้อมูล…"; - /* member role */ "owner" = "เจ้าของ"; @@ -2249,23 +2121,14 @@ /* No comment provided by engineer. */ "Password to show" = "รหัสผ่านที่จะแสดง"; -/* No comment provided by engineer. */ -"Paste" = "แปะ"; - /* No comment provided by engineer. */ "Paste image" = "แปะภาพ"; -/* No comment provided by engineer. */ -"Paste received link" = "แปะลิงก์ที่ได้รับ"; - /* No comment provided by engineer. */ "peer-to-peer" = "เพื่อนต่อเพื่อน"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น"; - -/* No comment provided by engineer. */ -"Periodically" = "เป็นระยะๆ"; +"Periodic" = "เป็นระยะๆ"; /* message decrypt error item */ "Permanent decryption error" = "ข้อผิดพลาดในการถอดรหัสอย่างถาวร"; @@ -2321,9 +2184,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "เก็บข้อความที่ร่างไว้ล่าสุดพร้อมไฟล์แนบ"; -/* No comment provided by engineer. */ -"Preset server" = "เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า"; - /* No comment provided by engineer. */ "Preset server address" = "ที่อยู่เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า"; @@ -2348,7 +2208,7 @@ /* No comment provided by engineer. */ "Profile password" = "รหัสผ่านโปรไฟล์"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ"; /* No comment provided by engineer. */ @@ -2396,14 +2256,14 @@ /* chat item menu */ "React…" = "ตอบสนอง…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "อ่าน"; /* No comment provided by engineer. */ "Read more" = "อ่านเพิ่มเติม"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)"; +"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)"; @@ -2411,9 +2271,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "อ่านเพิ่มเติมใน[พื้นที่เก็บข้อมูล GitHub](https://github.com/simplex-chat/simplex-chat#readme)"; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "อ่านเพิ่มเติมในที่เก็บ GitHub ของเรา"; - /* No comment provided by engineer. */ "received answer…" = "ได้รับคำตอบ…"; @@ -2459,7 +2316,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "ลดการใช้แบตเตอรี่"; -/* reject incoming call via notification */ +/* reject incoming call via notification +swipe action */ "Reject" = "ปฏิเสธ"; /* No comment provided by engineer. */ @@ -2540,9 +2398,6 @@ /* chat item action */ "Reveal" = "เปิดเผย"; -/* No comment provided by engineer. */ -"Revert" = "เปลี่ยนกลับ"; - /* No comment provided by engineer. */ "Revoke" = "ถอน"; @@ -2558,13 +2413,14 @@ /* No comment provided by engineer. */ "Run chat" = "เรียกใช้แชท"; -/* chat item action */ +/* alert button +chat item action */ "Save" = "บันทึก"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "บันทึก (และแจ้งผู้ติดต่อ)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "บันทึกและแจ้งผู้ติดต่อ"; /* No comment provided by engineer. */ @@ -2573,12 +2429,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "บันทึกและอัปเดตโปรไฟล์กลุ่ม"; -/* No comment provided by engineer. */ -"Save archive" = "บันทึกไฟล์เก็บถาวร"; - -/* No comment provided by engineer. */ -"Save auto-accept settings" = "บันทึกการตั้งค่าการยอมรับอัตโนมัติ"; - /* No comment provided by engineer. */ "Save group profile" = "บันทึกโปรไฟล์กลุ่ม"; @@ -2588,7 +2438,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "บันทึกข้อความรหัสผ่านใน Keychain"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "บันทึกการตั้งค่า?"; /* No comment provided by engineer. */ @@ -2597,12 +2447,9 @@ /* No comment provided by engineer. */ "Save servers" = "บันทึกเซิร์ฟเวอร์"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "บันทึกเซิร์ฟเวอร์?"; -/* No comment provided by engineer. */ -"Save settings?" = "บันทึกการตั้งค่า?"; - /* No comment provided by engineer. */ "Save welcome message?" = "บันทึกข้อความต้อนรับ?"; @@ -2645,7 +2492,7 @@ /* chat item text */ "security code changed" = "เปลี่ยนรหัสความปลอดภัยแล้ว"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "เลือก"; /* No comment provided by engineer. */ @@ -2669,9 +2516,6 @@ /* No comment provided by engineer. */ "Send delivery receipts to" = "ส่งใบเสร็จรับการจัดส่งข้อความไปที่"; -/* No comment provided by engineer. */ -"Send direct message" = "ส่งข้อความโดยตรง"; - /* No comment provided by engineer. */ "Send disappearing message" = "ส่งข้อความแบบที่หายไป"; @@ -2684,9 +2528,6 @@ /* No comment provided by engineer. */ "Send notifications" = "ส่งการแจ้งเตือน"; -/* No comment provided by engineer. */ -"Send notifications:" = "ส่งการแจ้งเตือน:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "ส่งคําถามและความคิด"; @@ -2696,7 +2537,7 @@ /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "ส่งจากแกลเลอรีหรือแป้นพิมพ์แบบกำหนดเอง"; -/* No comment provided by engineer. */ +/* alert message */ "Sender cancelled file transfer." = "ผู้ส่งยกเลิกการโอนไฟล์"; /* No comment provided by engineer. */ @@ -2774,7 +2615,8 @@ /* No comment provided by engineer. */ "Settings" = "การตั้งค่า"; -/* chat item action */ +/* alert action +chat item action */ "Share" = "แชร์"; /* No comment provided by engineer. */ @@ -2783,15 +2625,12 @@ /* No comment provided by engineer. */ "Share address" = "แชร์ที่อยู่"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "แชร์ที่อยู่กับผู้ติดต่อ?"; /* No comment provided by engineer. */ "Share link" = "แชร์ลิงก์"; -/* No comment provided by engineer. */ -"Share one-time invitation link" = "แชร์ลิงก์เชิญแบบใช้ครั้งเดียว"; - /* No comment provided by engineer. */ "Share with contacts" = "แชร์กับผู้ติดต่อ"; @@ -2825,7 +2664,7 @@ /* simplex link type */ "SimpleX group link" = "ลิงค์กลุ่ม SimpleX"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "ลิงก์ SimpleX"; /* No comment provided by engineer. */ @@ -2849,18 +2688,12 @@ /* No comment provided by engineer. */ "Skipped messages" = "ข้อความที่ข้ามไป"; -/* No comment provided by engineer. */ -"SMP servers" = "เซิร์ฟเวอร์ SMP"; - /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "ข้อผิดพลาดที่ไม่ร้ายแรงบางอย่างเกิดขึ้นระหว่างการนำเข้า - คุณอาจดูรายละเอียดเพิ่มเติมได้ที่คอนโซล Chat"; /* notification title */ "Somebody" = "ใครบางคน"; -/* No comment provided by engineer. */ -"Start a new chat" = "เริ่มแชทใหม่"; - /* No comment provided by engineer. */ "Start chat" = "เริ่มแชท"; @@ -2873,9 +2706,6 @@ /* No comment provided by engineer. */ "Stop" = "หยุด"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "หยุดการแชทเพื่อเปิดใช้งานการดำเนินการกับฐานข้อมูล"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "หยุดแชทเพื่อส่งออก นำเข้า หรือลบฐานข้อมูลแชท คุณจะไม่สามารถรับและส่งข้อความได้ในขณะที่การแชทหยุดลง"; @@ -2891,10 +2721,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "หยุดส่งไฟล์ไหม?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "หยุดแชร์"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "หยุดแชร์ที่อยู่ไหม?"; /* authentication reason */ @@ -2930,9 +2760,6 @@ /* No comment provided by engineer. */ "Tap to join incognito" = "แตะเพื่อเข้าร่วมโหมดไม่ระบุตัวตน"; -/* No comment provided by engineer. */ -"Tap to start a new chat" = "แตะเพื่อเริ่มแชทใหม่"; - /* No comment provided by engineer. */ "TCP connection timeout" = "หมดเวลาการเชื่อมต่อ TCP"; @@ -2954,7 +2781,7 @@ /* No comment provided by engineer. */ "Test servers" = "เซิร์ฟเวอร์ทดสอบ"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "การทดสอบล้มเหลว!"; /* No comment provided by engineer. */ @@ -2966,9 +2793,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "ขอบคุณผู้ใช้ – มีส่วนร่วมผ่าน Weblate!"; -/* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว"; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "แอปสามารถแจ้งให้คุณทราบเมื่อคุณได้รับข้อความหรือคำขอติดต่อ - โปรดเปิดการตั้งค่าเพื่อเปิดใช้งาน"; @@ -2987,6 +2811,9 @@ /* 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." = "แฮชของข้อความก่อนหน้านี้แตกต่างกัน"; @@ -2999,14 +2826,11 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "ข้อความจะถูกทำเครื่องหมายว่ากลั่นกรองสำหรับสมาชิกทุกคน"; -/* No comment provided by engineer. */ -"The next generation of private messaging" = "การส่งข้อความส่วนตัวรุ่นต่อไป"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "ฐานข้อมูลเก่าไม่ได้ถูกลบในระหว่างการย้ายข้อมูล แต่สามารถลบได้"; /* No comment provided by engineer. */ -"The profile is only shared with your contacts." = "โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น"; +"Your profile is stored on your device and only shared with your contacts." = "โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น"; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "ขีดที่สองที่เราพลาด! ✅"; @@ -3017,15 +2841,6 @@ /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "เซิร์ฟเวอร์สำหรับการเชื่อมต่อใหม่ของโปรไฟล์การแชทปัจจุบันของคุณ **%@**"; -/* No comment provided by engineer. */ -"Theme" = "ธีม"; - -/* No comment provided by engineer. */ -"There should be at least one user profile." = "ควรมีโปรไฟล์ผู้ใช้อย่างน้อยหนึ่งโปรไฟล์"; - -/* No comment provided by engineer. */ -"There should be at least one visible user profile." = "ควรมีอย่างน้อยหนึ่งโปรไฟล์ผู้ใช้ที่มองเห็นได้"; - /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "การตั้งค่าเหล่านี้ใช้สำหรับโปรไฟล์ปัจจุบันของคุณ **%@**"; @@ -3056,15 +2871,15 @@ /* No comment provided by engineer. */ "To make a new connection" = "เพื่อสร้างการเชื่อมต่อใหม่"; -/* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "เพื่อปกป้องความเป็นส่วนตัว แทนที่จะใช้ ID ผู้ใช้เหมือนที่แพลตฟอร์มอื่นๆใช้ SimpleX มีตัวระบุสำหรับคิวข้อความ โดยแยกจากกันสำหรับผู้ติดต่อแต่ละราย"; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "ไฟล์ภาพ/เสียงใช้ UTC เพื่อป้องกันเขตเวลา"; /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "เพื่อปกป้องข้อมูลของคุณ ให้เปิด SimpleX Lock\nคุณจะได้รับแจ้งให้ยืนยันตัวตนให้เสร็จสมบูรณ์ก่อนที่จะเปิดใช้งานคุณลักษณะนี้"; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "เพื่อปกป้องความเป็นส่วนตัว แทนที่จะใช้ ID ผู้ใช้เหมือนที่แพลตฟอร์มอื่นๆใช้ SimpleX มีตัวระบุสำหรับคิวข้อความ โดยแยกจากกันสำหรับผู้ติดต่อแต่ละราย"; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "ในการบันทึกข้อความเสียง โปรดให้สิทธิ์ในการใช้ไมโครโฟน"; @@ -3089,22 +2904,16 @@ /* No comment provided by engineer. */ "Turn off" = "ปิด"; -/* No comment provided by engineer. */ -"Turn off notifications?" = "ปิดการแจ้งเตือนไหม?"; - /* No comment provided by engineer. */ "Turn on" = "เปิด"; /* No comment provided by engineer. */ "Unable to record voice message" = "ไม่สามารถบันทึกข้อความเสียง"; -/* item status description */ -"Unexpected error: %@" = "ข้อผิดพลาดที่ไม่คาดคิด: %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "สถานะการย้ายข้อมูลที่ไม่คาดคิด"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "เลิกชอบ"; /* No comment provided by engineer. */ @@ -3143,36 +2952,27 @@ /* authentication reason */ "Unlock app" = "ปลดล็อคแอป"; -/* No comment provided by engineer. */ +/* notification label action */ "Unmute" = "เปิดเสียง"; -/* No comment provided by engineer. */ +/* swipe action */ "Unread" = "เปลี่ยนเป็นยังไม่ได้อ่าน"; /* No comment provided by engineer. */ "Update" = "อัปเดต"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "อัปเดตการตั้งค่าโฮสต์ .onion ไหม?"; - /* No comment provided by engineer. */ "Update database passphrase" = "อัปเดตรหัสผ่านของฐานข้อมูล"; /* No comment provided by engineer. */ "Update network settings?" = "อัปเดตการตั้งค่าเครือข่ายไหม?"; -/* No comment provided by engineer. */ -"Update transport isolation mode?" = "อัปเดตโหมดการแยกการขนส่งไหม?"; - /* rcv group event chat item */ "updated group profile" = "อัปเดตโปรไฟล์กลุ่มแล้ว"; /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "การอัปเดตการตั้งค่าจะเชื่อมต่อไคลเอนต์กับเซิร์ฟเวอร์ทั้งหมดอีกครั้ง"; -/* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "การอัปเดตการตั้งค่านี้จะเชื่อมต่อไคลเอนต์กับเซิร์ฟเวอร์ทั้งหมดอีกครั้ง"; - /* No comment provided by engineer. */ "Upgrade and open chat" = "อัปเกรดและเปิดการแชท"; @@ -3197,12 +2997,6 @@ /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "ใช้เซิร์ฟเวอร์ SimpleX Chat ไหม?"; -/* No comment provided by engineer. */ -"User profile" = "โปรไฟล์ผู้ใช้"; - -/* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "การใช้โฮสต์ .onion ต้องการผู้ให้บริการ VPN ที่เข้ากันได้"; - /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "กำลังใช้เซิร์ฟเวอร์ SimpleX Chat อยู่"; @@ -3258,7 +3052,7 @@ "Voice messages are prohibited in this chat." = "ห้ามส่งข้อความเสียงในแชทนี้"; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "ข้อความเสียงเป็นสิ่งต้องห้ามในกลุ่มนี้"; +"Voice messages are prohibited." = "ข้อความเสียงเป็นสิ่งต้องห้ามในกลุ่มนี้"; /* No comment provided by engineer. */ "Voice messages prohibited!" = "ห้ามข้อความเสียง!"; @@ -3302,9 +3096,6 @@ /* No comment provided by engineer. */ "When available" = "เมื่อพร้อมใช้งาน"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "เมื่อมีคนขอเชื่อมต่อ คุณสามารถยอมรับหรือปฏิเสธได้"; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "เมื่อคุณแชร์โปรไฟล์ที่ไม่ระบุตัวตนกับใครสักคน โปรไฟล์นี้จะใช้สำหรับกลุ่มที่พวกเขาเชิญคุณ"; @@ -3317,15 +3108,9 @@ /* No comment provided by engineer. */ "Wrong passphrase!" = "รหัสผ่านผิด!"; -/* No comment provided by engineer. */ -"XFTP servers" = "เซิร์ฟเวอร์ XFTP"; - /* pref value */ "yes" = "ใช่"; -/* No comment provided by engineer. */ -"You" = "คุณ"; - /* No comment provided by engineer. */ "You accepted connection" = "คุณยอมรับการเชื่อมต่อ"; @@ -3353,9 +3138,6 @@ /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "คุณสามารถรับสายจากหน้าจอล็อกโดยไม่ต้องมีการตรวจสอบสิทธิ์อุปกรณ์และแอป"; -/* No comment provided by engineer. */ -"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "คุณสามารถเชื่อมต่อได้โดยคลิกที่ลิงค์ หากเปิดในเบราว์เซอร์ ให้คลิกปุ่ม **เปิดในแอปมือถือ**"; - /* No comment provided by engineer. */ "You can create it later" = "คุณสามารถสร้างได้ในภายหลัง"; @@ -3369,7 +3151,7 @@ "You can hide or mute a user profile - swipe it to the right." = "คุณสามารถซ่อนหรือปิดเสียงโปรไฟล์ผู้ใช้ - ปัดไปทางขวา"; /* notification body */ -"You can now send messages to %@" = "ตอนนี้คุณสามารถส่งข้อความถึง %@"; +"You can now chat with %@" = "ตอนนี้คุณสามารถส่งข้อความถึง %@"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "คุณสามารถตั้งค่าแสดงตัวอย่างการแจ้งเตือนบนหน้าจอล็อคผ่านการตั้งค่า"; @@ -3380,9 +3162,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "คุณสามารถแบ่งปันที่อยู่นี้กับผู้ติดต่อของคุณเพื่อให้พวกเขาเชื่อมต่อกับ **%@**"; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "คุณสามารถแชร์ที่อยู่ของคุณเป็นลิงก์หรือรหัสคิวอาร์ - ใคร ๆ ก็สามารถเชื่อมต่อกับคุณได้"; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "คุณสามารถเริ่มแชทผ่านการตั้งค่าแอป / ฐานข้อมูล หรือโดยการรีสตาร์ทแอป"; @@ -3407,14 +3186,11 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "คุณเปลี่ยนบทบาทของ %1$@ เป็น %2$@"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "คุณควบคุมผ่านเซิร์ฟเวอร์ **เพื่อรับ** ข้อความผู้ติดต่อของคุณ - เซิร์ฟเวอร์ที่คุณใช้เพื่อส่งข้อความถึงพวกเขา"; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "เราไม่สามารถตรวจสอบคุณได้ กรุณาลองอีกครั้ง."; /* No comment provided by engineer. */ -"You have no chats" = "คุณไม่มีการแชท"; +"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." = "คุณต้องใส่รหัสผ่านทุกครั้งที่เริ่มแอป - รหัสผ่านไม่ได้จัดเก็บไว้ในอุปกรณ์"; @@ -3479,9 +3255,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "คุณกำลังใช้โปรไฟล์ที่ไม่ระบุตัวตนสำหรับกลุ่มนี้ - ไม่อนุญาตให้เชิญผู้ติดต่อเพื่อป้องกันการแชร์โปรไฟล์หลักของคุณ"; -/* No comment provided by engineer. */ -"Your %@ servers" = "เซิร์ฟเวอร์ %@ ของคุณ"; - /* No comment provided by engineer. */ "Your calls" = "การโทรของคุณ"; @@ -3494,18 +3267,12 @@ /* No comment provided by engineer. */ "Your chat profiles" = "โปรไฟล์แชทของคุณ"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "ผู้ติดต่อของคุณจะต้องออนไลน์เพื่อให้การเชื่อมต่อเสร็จสมบูรณ์\nคุณสามารถยกเลิกการเชื่อมต่อนี้และลบผู้ติดต่อออก (และลองใหม่ในภายหลังด้วยลิงก์ใหม่)"; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "ผู้ติดต่อของคุณส่งไฟล์ที่ใหญ่กว่าขนาดสูงสุดที่รองรับในปัจจุบัน (%@)"; /* No comment provided by engineer. */ "Your contacts can allow full message deletion." = "ผู้ติดต่อของคุณสามารถอนุญาตให้ลบข้อความทั้งหมดได้"; -/* No comment provided by engineer. */ -"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "ผู้ติดต่อของคุณใน SimpleX จะเห็น\nคุณสามารถเปลี่ยนได้ในการตั้งค่า"; - /* No comment provided by engineer. */ "Your contacts will remain connected." = "ผู้ติดต่อของคุณจะยังคงเชื่อมต่ออยู่"; @@ -3525,7 +3292,7 @@ "Your privacy" = "ความเป็นส่วนตัวของคุณ"; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น\nเซิร์ฟเวอร์ SimpleX ไม่สามารถดูโปรไฟล์ของคุณได้"; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น เซิร์ฟเวอร์ SimpleX ไม่สามารถดูโปรไฟล์ของคุณได้"; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "โปรไฟล์ รายชื่อผู้ติดต่อ และข้อความที่ส่งของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณ"; @@ -3533,9 +3300,6 @@ /* No comment provided by engineer. */ "Your random profile" = "โปรไฟล์แบบสุ่มของคุณ"; -/* No comment provided by engineer. */ -"Your server" = "เซิร์ฟเวอร์ของคุณ"; - /* No comment provided by engineer. */ "Your server address" = "ที่อยู่เซิร์ฟเวอร์ของคุณ"; @@ -3545,9 +3309,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "ที่อยู่ SimpleX ของคุณ"; -/* No comment provided by engineer. */ -"Your SMP servers" = "เซิร์ฟเวอร์ SMP ของคุณ"; - -/* No comment provided by engineer. */ -"Your XFTP servers" = "เซิร์ฟเวอร์ XFTP ของคุณ"; - diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings new file mode 100644 index 0000000000..e3bb11d1cc --- /dev/null +++ b/apps/ios/tr.lproj/Localizable.strings @@ -0,0 +1,5422 @@ +/* No comment provided by engineer. */ +" (can be copied)" = " (kopyalanabilir)"; + +/* No comment provided by engineer. */ +"_italic_" = "\\_italik_"; + +/* 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." = "- [dizin hizmeti] 'ne bağlan(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- çoklu mesaj gönderimi (20 kişiye kadar).\n- daha hızlı ve daha stabil."; + +/* No comment provided by engineer. */ +"- more stable message delivery.\n- a bit better groups.\n- and more!" = "- daha stabil mesaj gönderimi.\n- birazcık daha iyi gruplar.\n- ve fazlası!"; + +/* No comment provided by engineer. */ +"- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- isteğe bağlı olarak silinen kişilere bildirme.\n- boşluklu profil adları\n- ve fazlası!"; + +/* No comment provided by engineer. */ +"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- 5 dakikaya kadar süren sesli mesajlar.\n- mesaj kaybolması için özel zaman.\n- düzenleme geçmişi."; + +/* No comment provided by engineer. */ +"!1 colored!" = "!1 renklendirilmiş!"; + +/* No comment provided by engineer. */ +"(new)" = "(yeni)"; + +/* 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."; + +/* No comment provided by engineer. */ +"**Create group**: to create a new group." = "**Grup oluştur**: yeni bir grup oluşturmak için."; + +/* No comment provided by engineer. */ +"**e2e encrypted** audio call" = "**uçtan uca şifrelenmiş** sesli arama"; + +/* No comment provided by engineer. */ +"**e2e encrypted** video call" = "**uçtan uca şifrelenmiş** görüntülü arama"; + +/* No comment provided by engineer. */ +"**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." = "**Daha gizli**: her 20 dakikada yeni mesajlar için kontrol et. Cihaz jetonu SimpleX Chat sunucusuyla paylaşılacak, ama ne kadar kişi veya mesaja sahip olduğun paylaşılmayacak."; + +/* 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." = "**En gizli**: SimpleX Chat bildirim sunucusunu kullanma, arkaplanda mesajları periyodik olarak kontrol edin (uygulamayı ne sıklıkta kullandığınıza bağlıdır)."; + +/* 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." = "**Lütfen dikkat**: Aynı veritabanını iki cihazda kullanmak, güvenlik koruması olarak bağlantılarınızdaki mesajların şifresinin çözülmesini engelleyecektir."; + +/* No comment provided by engineer. */ +"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Lütfen aklınızda bulunsun**: eğer parolanızı kaybederseniz parolanızı değiştirme veya geri kurtarma ihtimaliniz YOKTUR."; + +/* 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." = "**Önerilen**: cihaz tokeni ve bildirimler SimpleX Chat bildirim sunucularına gönderilir, ama mesajın içeriği, boyutu veya kimden geldiği gönderilmez."; + +/* No comment provided by engineer. */ +"**Scan / Paste link**: to connect via a link you received." = "edindiğiniz bağlantı aracılığıyla bağlanmak için **Linki tarayın/yapıştırın**."; + +/* 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."; + +/* No comment provided by engineer. */ +"**Warning**: the archive will be removed." = "**Uyarı**: arşiv silinecektir."; + +/* No comment provided by engineer. */ +"*bold*" = "\\*kalın*"; + +/* copied message info title, # <title> */ +"# %@" = "# %@"; + +/* copied message info */ +"## History" = "## Geçmiş"; + +/* copied message info */ +"## In reply to" = "## cevap olarak"; + +/* No comment provided by engineer. */ +"#secret#" = "#gizli#"; + +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"%@ (current)" = "%@ (güncel)"; + +/* copied message info */ +"%@ (current):" = "%@ (güncel):"; + +/* No comment provided by engineer. */ +"%@ / %@" = "%@ / %@"; + +/* No comment provided by engineer. */ +"%@ %@" = "%@ %@"; + +/* No comment provided by engineer. */ +"%@ and %@" = "%@ ve %@"; + +/* No comment provided by engineer. */ +"%@ and %@ connected" = "%@ ve %@ bağlandı"; + +/* copied message info, <sender> at <time> */ +"%@ at %@:" = "1$@, %2$@'de:"; + +/* No comment provided by engineer. */ +"%@ connected" = "%@ bağlandı"; + +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ indirildi"; + +/* notification title */ +"%@ is connected!" = "%@ bağlandı!"; + +/* No comment provided by engineer. */ +"%@ is not verified" = "%@ onaylanmadı"; + +/* No comment provided by engineer. */ +"%@ is verified" = "%@ onaylandı"; + +/* No comment provided by engineer. */ +"%@ server" = "%@ sunucu"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ sunucular"; + +/* No comment provided by engineer. */ +"%@ uploaded" = "%@ yüklendi"; + +/* notification title */ +"%@ wants to connect!" = "%@ bağlanmak istiyor!"; + +/* format for date separator in chat */ +"%@, %@" = "%1$@,%2$@"; + +/* No comment provided by engineer. */ +"%@, %@ and %lld members" = "%@, %@ ve %lld üyeleri"; + +/* No comment provided by engineer. */ +"%@, %@ and %lld other members connected" = "%@, %@ ve %lld diğer üyeler bağlandı"; + +/* copied message info */ +"%@:" = "%@:"; + +/* time interval */ +"%d days" = "%d gün"; + +/* forward confirmation reason */ +"%d file(s) are still being downloaded." = "%d dosyası(ları) hala indiriliyor."; + +/* forward confirmation reason */ +"%d file(s) failed to download." = "%d dosyası(ları) indirilemedi."; + +/* forward confirmation reason */ +"%d file(s) were deleted." = "%d dosyası(ları) silindi."; + +/* forward confirmation reason */ +"%d file(s) were not downloaded." = "%d dosyası(ları) indirilmedi."; + +/* time interval */ +"%d hours" = "%d saat"; + +/* alert title */ +"%d messages not forwarded" = "%d mesajı iletilmeyedi"; + +/* time interval */ +"%d min" = "%d dakika"; + +/* time interval */ +"%d months" = "%d ay"; + +/* time interval */ +"%d sec" = "%d saniye"; + +/* integrity error chat item */ +"%d skipped message(s)" = "%d okunmamış mesaj(lar)"; + +/* time interval */ +"%d weeks" = "%d hafta"; + +/* No comment provided by engineer. */ +"%lld" = "%lld"; + +/* No comment provided by engineer. */ +"%lld %@" = "%lld %@"; + +/* No comment provided by engineer. */ +"%lld contact(s) selected" = "%lld kişi seçildi"; + +/* No comment provided by engineer. */ +"%lld file(s) with total size of %@" = "%lld dosya , toplam büyüklüğü %@"; + +/* No comment provided by engineer. */ +"%lld group events" = "%lld grup etkinlikleri"; + +/* No comment provided by engineer. */ +"%lld members" = "%lld üyeler"; + +/* No comment provided by engineer. */ +"%lld messages blocked" = "%lld mesaj engellendi"; + +/* No comment provided by engineer. */ +"%lld messages blocked by admin" = "%lld mesaj yönetici tarafından engellendi"; + +/* No comment provided by engineer. */ +"%lld messages marked deleted" = "%lld mesaj silinmiş olarak işaretlendi"; + +/* No comment provided by engineer. */ +"%lld messages moderated by %@" = "%lld mesaj %@ tarafından yönetildi"; + +/* No comment provided by engineer. */ +"%lld minutes" = "%lld dakika"; + +/* No comment provided by engineer. */ +"%lld new interface languages" = "%lld yeni arayüz dilleri"; + +/* No comment provided by engineer. */ +"%lld seconds" = "%lld saniye"; + +/* No comment provided by engineer. */ +"%lldd" = "%lldd"; + +/* No comment provided by engineer. */ +"%lldh" = "%lldh"; + +/* No comment provided by engineer. */ +"%lldk" = "%lldk"; + +/* No comment provided by engineer. */ +"%lldm" = "%lldm"; + +/* No comment provided by engineer. */ +"%lldmth" = "%lldmth"; + +/* No comment provided by engineer. */ +"%llds" = "%llds"; + +/* No comment provided by engineer. */ +"%lldw" = "%lldw"; + +/* No comment provided by engineer. */ +"%u messages failed to decrypt." = "%u mesajın şifreleme çözümü başarısız oldu."; + +/* No comment provided by engineer. */ +"%u messages skipped." = "%u mesajlar atlandı."; + +/* No comment provided by engineer. */ +"`a + b`" = "\\`a + b`"; + +/* email text */ +"<p>Hi!</p>\n<p><a href=\"%@\">Connect to me via SimpleX Chat</a></p>" = "<p>Merhaba!</p>\n<p><a href=\"%@\">SimpleX Chat ile bana bağlanın</a></p>"; + +/* No comment provided by engineer. */ +"~strike~" = "\\~çizik~"; + +/* time to disappear */ +"0 sec" = "0 saniye"; + +/* No comment provided by engineer. */ +"0s" = "0sn"; + +/* delete after time +time interval */ +"1 day" = "1 gün"; + +/* time interval */ +"1 hour" = "1 saat"; + +/* No comment provided by engineer. */ +"1 minute" = "1 dakika"; + +/* delete after time +time interval */ +"1 month" = "1 ay"; + +/* delete after time +time interval */ +"1 week" = "1 hafta"; + +/* No comment provided by engineer. */ +"1-time link" = "tek kullanımlık bağlantı"; + +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Tek kullanımlık bağlantı *sadece bir kişi ile* kullanılabilir - kişiyle veya uygulama içinden paylaş."; + +/* No comment provided by engineer. */ +"5 minutes" = "5 dakika"; + +/* No comment provided by engineer. */ +"6" = "6"; + +/* No comment provided by engineer. */ +"30 seconds" = "30 saniye"; + +/* No comment provided by engineer. */ +"A few more things" = "Birkaç şey daha"; + +/* notification title */ +"A new contact" = "Yeni kişi"; + +/* No comment provided by engineer. */ +"A new random profile will be shared." = "Yeni bir rastgele profil paylaşılacak."; + +/* No comment provided by engineer. */ +"A separate TCP connection will be used **for each chat profile you have in the app**." = "**uygulamandaki olan her sohbet profili için** ayrı bir TCP bağlantısı kullanılacaktır."; + +/* 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." = "**uygulamandaki olan her sohbet profili için** ayrı bir TCP bağlantısı kullanılacaktır.\n**Lütfen aklınızda bulunsun**: eğer çok fazla bağlantılarınız var ise pil kullanımı ve internet kullanımınız oldukça artabilir ve bazı bağlantılar hatayla karşılaşabilir."; + +/* No comment provided by engineer. */ +"Abort" = "İptal Et"; + +/* No comment provided by engineer. */ +"Abort changing address" = "Adres değişimini iptal et"; + +/* No comment provided by engineer. */ +"Abort changing address?" = "Adres değişimi iptal edilsin mi?"; + +/* No comment provided by engineer. */ +"About operators" = "Operatörler hakkında"; + +/* No comment provided by engineer. */ +"About SimpleX Chat" = "SimpleX Chat hakkında"; + +/* No comment provided by engineer. */ +"above, then choose:" = "yukarı çıkın, ardından seçin:"; + +/* No comment provided by engineer. */ +"Accent" = "Ana renk"; + +/* accept contact request via notification +accept incoming call via notification +swipe action */ +"Accept" = "Kabul et"; + +/* No comment provided by engineer. */ +"Accept conditions" = "Koşulları kabul et"; + +/* No comment provided by engineer. */ +"Accept connection request?" = "Bağlantı isteği kabul edilsin mi?"; + +/* notification body */ +"Accept contact request from %@?" = "%@ 'den gelen iletişim isteği kabul edilsin mi?"; + +/* accept contact request via notification +swipe action */ +"Accept incognito" = "Takma adla kabul et"; + +/* call status */ +"accepted call" = "kabul edilen arama"; + +/* No comment provided by engineer. */ +"Accepted conditions" = "Kabul edilmiş koşullar"; + +/* No comment provided by engineer. */ +"Acknowledged" = "Onaylandı"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Onay hataları"; + +/* 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"; + +/* No comment provided by engineer. */ +"Add profile" = "Profil ekle"; + +/* No comment provided by engineer. */ +"Add server" = "Sunucu ekle"; + +/* No comment provided by engineer. */ +"Add servers by scanning QR codes." = "Karekod taratarak sunucuları ekleyin."; + +/* No comment provided by engineer. */ +"Add team members" = "Takım üyesi ekle"; + +/* No comment provided by engineer. */ +"Add to another device" = "Başka bir cihaza ekle"; + +/* No comment provided by engineer. */ +"Add welcome message" = "Karşılama mesajı ekleyin"; + +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "Takım üyelerini konuşmalara ekle."; + +/* No comment provided by engineer. */ +"Added media & file servers" = "medya ve dosya sunucuları eklendi"; + +/* No comment provided by engineer. */ +"Added message servers" = "Mesaj sunucuları eklendi"; + +/* No comment provided by engineer. */ +"Additional accent" = "Ek ana renk"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Ek vurgu 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Ek ikincil renk"; + +/* No comment provided by engineer. */ +"Address" = "Adres"; + +/* No comment provided by engineer. */ +"Address change will be aborted. Old receiving address will be used." = "Adres değişikliği iptal edilecek. Eski alıcı adresi kullanılacaktır."; + +/* No comment provided by engineer. */ +"Address or 1-time link?" = "adres mi yoksa tek kullanımlık bağlantı mı?"; + +/* No comment provided by engineer. */ +"Address settings" = "Adres seçenekleri"; + +/* member role */ +"admin" = "yönetici"; + +/* feature role */ +"admins" = "yöneticiler"; + +/* No comment provided by engineer. */ +"Admins can block a member for all." = "Yöneticiler bir üyeyi tamamen engelleyebilirler."; + +/* No comment provided by engineer. */ +"Admins can create the links to join groups." = "Yöneticiler gruplara katılmak için bağlantılar oluşturabilir."; + +/* No comment provided by engineer. */ +"Advanced network settings" = "Gelişmiş ağ ayarları"; + +/* No comment provided by engineer. */ +"Advanced settings" = "Gelişmiş ayarlar"; + +/* chat item text */ +"agreeing encryption for %@…" = "%@ için şifreleme kabul ediliyor…"; + +/* chat item text */ +"agreeing encryption…" = "şifreleme kabul ediliyor…"; + +/* No comment provided by engineer. */ +"All app data is deleted." = "Tüm uygulama verileri silinir."; + +/* No comment provided by engineer. */ +"All chats and messages will be deleted - this cannot be undone!" = "Tüm konuşmalar ve mesajlar silinecektir. Bu, geri alınamaz!"; + +/* No comment provided by engineer. */ +"All data is erased when it is entered." = "Kullanıldığında bütün veriler silinir."; + +/* No comment provided by engineer. */ +"All data is kept private on your device." = "Tüm veriler cihazınıza özeldir."; + +/* No comment provided by engineer. */ +"All group members will remain connected." = "Tüm grup üyeleri bağlı kalacaktır."; + +/* feature role */ +"all members" = "bütün üyeler"; + +/* No comment provided by engineer. */ +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Bütün mesajlar ve dosyalar **uçtan-uca şifrelemeli** gönderilir, doğrudan mesajlarda kuantum güvenlik ile birlikte."; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone!" = "Tüm mesajlar silinecektir - bu geri alınamaz!"; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Tüm mesajlar silinecektir. Bu, geri alınamaz! Mesajlar, YALNIZCA senin için silinecektir."; + +/* No comment provided by engineer. */ +"All new messages from %@ will be hidden!" = "%@ 'den gelen bütün yeni mesajlar saklı olacak!"; + +/* profile dropdown */ +"All profiles" = "Tüm Profiller"; + +/* No comment provided by engineer. */ +"All your contacts will remain connected." = "Konuştuğun kişilerin tümü bağlı kalacaktır."; + +/* No comment provided by engineer. */ +"All your contacts will remain connected. Profile update will be sent to your contacts." = "Tüm kişileriniz bağlı kalacaktır. Profil güncellemesi kişilerinize gönderilecektir."; + +/* No comment provided by engineer. */ +"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Tüm kişileriniz, konuşmalarınız ve dosyalarınız güvenli bir şekilde şifrelenir ve yapılandırılmış XFTP yönlendiricilerine parçalar halinde yüklenir."; + +/* No comment provided by engineer. */ +"Allow" = "İzin ver"; + +/* No comment provided by engineer. */ +"Allow calls only if your contact allows them." = "Yalnızca irtibat kişiniz izin veriyorsa aramalara izin verin."; + +/* No comment provided by engineer. */ +"Allow calls?" = "Aramalara izin verilsin mi ?"; + +/* No comment provided by engineer. */ +"Allow disappearing messages only if your contact allows it to you." = "Eğer kişide izin verirse kaybolan mesajlara izin ver."; + +/* No comment provided by engineer. */ +"Allow downgrade" = "Sürüm düşürmeye izin ver"; + +/* No comment provided by engineer. */ +"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Konuştuğun kişi, kalıcı olarak silinebilen mesajlara izin veriyorsa sen de ver. (24 saat içinde)"; + +/* 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."; + +/* No comment provided by engineer. */ +"Allow message reactions." = "Mesaj tepkilerine izin ver."; + +/* No comment provided by engineer. */ +"Allow sending direct messages to members." = "Üyelere doğrudan mesaj göndermeye izin ver."; + +/* No comment provided by engineer. */ +"Allow sending disappearing messages." = "Kendiliğinden yok olan mesajlar göndermeye izin ver."; + +/* No comment provided by engineer. */ +"Allow sharing" = "Paylaşıma izin ver"; + +/* 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)"; + +/* No comment provided by engineer. */ +"Allow to send files and media." = "Dosya ve medya göndermeye izin ver."; + +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "SimpleX bağlantıları göndilmesine izin ver."; + +/* No comment provided by engineer. */ +"Allow to send voice messages." = "Sesli mesaj göndermeye izin ver."; + +/* No comment provided by engineer. */ +"Allow voice messages only if your contact allows them." = "Yalnızca kişiniz sesli mesaj göndermeye izin veriyorsa sen de ver."; + +/* No comment provided by engineer. */ +"Allow voice messages?" = "Sesli mesajlara izin ver?"; + +/* No comment provided by engineer. */ +"Allow your contacts adding message reactions." = "Konuştuğun kişilerin mesajlarına tepki eklemesine izin ver."; + +/* No comment provided by engineer. */ +"Allow your contacts to call you." = "Kişilerinin seni aramasına izin ver."; + +/* No comment provided by engineer. */ +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Kişilerinin gönderilen mesajları kalıcı olarak silmesine izin ver. (24 saat içinde)"; + +/* No comment provided by engineer. */ +"Allow your contacts to send disappearing messages." = "Kişilerinizin kaybolan mesajlar göndermesine izin verin."; + +/* No comment provided by engineer. */ +"Allow your contacts to send voice messages." = "Kişilerinizin sesli mesajlar göndermesine izin verin."; + +/* No comment provided by engineer. */ +"Already connected?" = "Zaten bağlandı?"; + +/* No comment provided by engineer. */ +"Already connecting!" = "Zaten bağlanılıyor!"; + +/* No comment provided by engineer. */ +"Already joining the group!" = "Zaten gruba bağlanılıyor!"; + +/* pref value */ +"always" = "her zaman"; + +/* No comment provided by engineer. */ +"Always use private routing." = "Her zaman gizli yönlendirme kullan."; + +/* No comment provided by engineer. */ +"Always use relay" = "Her zaman yönlendirici kullan"; + +/* No comment provided by engineer. */ +"An empty chat profile with the provided name is created, and the app opens as usual." = "Verilen adla boş bir sohbet profili oluşturulur ve uygulama her zamanki gibi açılır."; + +/* No comment provided by engineer. */ +"and %lld other events" = "ve %lld diğer etkinlikler"; + +/* 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ü: %@"; + +/* No comment provided by engineer. */ +"App data migration" = "Uygulama verisi taşıma"; + +/* No comment provided by engineer. */ +"App encrypts new local files (except videos)." = "Uygulama yerel dosyaları şifreler (videolar dışında)."; + +/* No comment provided by engineer. */ +"App icon" = "Uygulama simgesi"; + +/* No comment provided by engineer. */ +"App passcode" = "Uygulama erişim kodu"; + +/* No comment provided by engineer. */ +"App passcode is replaced with self-destruct passcode." = "Uygulama parolası kendi kendini imha eden parolayla değiştirildi."; + +/* No comment provided by engineer. */ +"App session" = "Uygulama oturumu"; + +/* No comment provided by engineer. */ +"App version" = "Uygulama sürümü"; + +/* No comment provided by engineer. */ +"App version: v%@" = "Uygulama sürümü: v%@"; + +/* No comment provided by engineer. */ +"Appearance" = "Görünüş"; + +/* No comment provided by engineer. */ +"Apply" = "Uygula"; + +/* No comment provided by engineer. */ +"Apply to" = "Şuna uygula"; + +/* No comment provided by engineer. */ +"Archive and upload" = "Arşivle ve yükle"; + +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Daha sonra görüşmek için kişileri arşivleyin."; + +/* No comment provided by engineer. */ +"Archived contacts" = "Arşivli kişiler"; + +/* No comment provided by engineer. */ +"Archiving database" = "Veritabanı arşivleniyor"; + +/* No comment provided by engineer. */ +"Attach" = "Ekle"; + +/* No comment provided by engineer. */ +"attempts" = "denemeler"; + +/* No comment provided by engineer. */ +"Audio & video calls" = "Sesli & görüntülü aramalar"; + +/* No comment provided by engineer. */ +"Audio and video calls" = "Sesli ve görüntülü aramalar"; + +/* No comment provided by engineer. */ +"audio call (not e2e encrypted)" = "sesli arama (uçtan uca şifreli değil)"; + +/* chat feature */ +"Audio/video calls" = "Sesli/görüntülü aramalar"; + +/* No comment provided by engineer. */ +"Audio/video calls are prohibited." = "Sesli/görüntülü aramalar yasaklandı."; + +/* PIN entry */ +"Authentication cancelled" = "Kimlik doğrulama iptal edildi"; + +/* No comment provided by engineer. */ +"Authentication failed" = "Kimlik doğrulama başarısız oldu"; + +/* No comment provided by engineer. */ +"Authentication is required before the call is connected, but you may miss calls." = "Kimlik doğrulama aramalara bağlanmadan önce gereklidir, ama aramaları kaçırabilirsin."; + +/* No comment provided by engineer. */ +"Authentication unavailable" = "Kimlik doğrulama kullanım dışı"; + +/* member role */ +"author" = "yetkili"; + +/* No comment provided by engineer. */ +"Auto-accept" = "Otomatik-kabul et"; + +/* No comment provided by engineer. */ +"Auto-accept contact requests" = "İletişim isteklerini otomatik kabul et"; + +/* No comment provided by engineer. */ +"Auto-accept images" = "Fotoğrafları otomatik kabul et"; + +/* alert title */ +"Auto-accept settings" = "Ayarları otomatik olarak kabul et"; + +/* No comment provided by engineer. */ +"Back" = "Geri"; + +/* No comment provided by engineer. */ +"Background" = "Arka plan"; + +/* No comment provided by engineer. */ +"Bad desktop address" = "Kötü bilgisayar adresi"; + +/* integrity error chat item */ +"bad message hash" = "kötü mesaj hash'i"; + +/* No comment provided by engineer. */ +"Bad message hash" = "Kötü mesaj karması"; + +/* integrity error chat item */ +"bad message ID" = "kötü mesaj kimliği"; + +/* No comment provided by engineer. */ +"Bad message ID" = "Kötü mesaj kimliği"; + +/* No comment provided by engineer. */ +"Better calls" = "Daha iyi aramalar"; + +/* No comment provided by engineer. */ +"Better groups" = "Daha iyi gruplar"; + +/* No comment provided by engineer. */ +"Better message dates." = "Daha iyi mesaj tarihleri."; + +/* No comment provided by engineer. */ +"Better messages" = "Daha iyi mesajlar"; + +/* No comment provided by engineer. */ +"Better networking" = "Daha iyi ağ oluşturma"; + +/* No comment provided by engineer. */ +"Better notifications" = "Daha iyi bildirimler"; + +/* No comment provided by engineer. */ +"Better security ✅" = "Daha iyi güvenlik ✅"; + +/* No comment provided by engineer. */ +"Better user experience" = "Daha iyi kullanıcı deneyimi"; + +/* No comment provided by engineer. */ +"Black" = "Siyah"; + +/* No comment provided by engineer. */ +"Block" = "Engelle"; + +/* No comment provided by engineer. */ +"Block for all" = "Herkes için engelle"; + +/* No comment provided by engineer. */ +"Block group members" = "Grup üyelerini engelle"; + +/* No comment provided by engineer. */ +"Block member" = "Üyeyi engelle"; + +/* No comment provided by engineer. */ +"Block member for all?" = "Üye herkes için engellensin mi?"; + +/* No comment provided by engineer. */ +"Block member?" = "Üyeyi engelle?"; + +/* marked deleted chat item preview text */ +"blocked" = "engellendi"; + +/* rcv group event chat item */ +"blocked %@" = "engellendi %@"; + +/* blocked chat item +marked deleted chat item preview text */ +"blocked by admin" = "yönetici tarafından engellendi"; + +/* No comment provided by engineer. */ +"Blocked by admin" = "Yönetici tarafından engellendi"; + +/* No comment provided by engineer. */ +"Blur for better privacy." = "Daha iyi gizlilik için bulanıklaştır."; + +/* No comment provided by engineer. */ +"Blur media" = "Medyayı bulanıklaştır"; + +/* No comment provided by engineer. */ +"bold" = "kalın"; + +/* No comment provided by engineer. */ +"Both you and your contact can add message reactions." = "Sen ve konuştuğun kişi mesaj tepkileri ekleyebilir."; + +/* No comment provided by engineer. */ +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "Konuştuğun kişi ve sen mesajları kalıcı olarak silebilirsiniz. (24 saat içinde)"; + +/* No comment provided by engineer. */ +"Both you and your contact can make calls." = "Sen ve konuştuğun kişi aramalar yapabilir."; + +/* No comment provided by engineer. */ +"Both you and your contact can send disappearing messages." = "Sen ve konuştuğun kişi kaybolan mesajlar gönderebilir."; + +/* No comment provided by engineer. */ +"Both you and your contact can send voice messages." = "Sen ve konuştuğun kişi sesli mesaj gönderebilir."; + +/* 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. */ +"Business address" = "İş adresi"; + +/* No comment provided by engineer. */ +"Business chats" = "İş konuşmaları"; + +/* 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. */ +"call" = "Ara"; + +/* No comment provided by engineer. */ +"Call already ended!" = "Arama çoktan bitti!"; + +/* call status */ +"call error" = "arama hatası"; + +/* call status */ +"call in progress" = "arama yapılıyor"; + +/* call status */ +"calling…" = "aranıyor…"; + +/* No comment provided by engineer. */ +"Calls" = "Aramalar"; + +/* No comment provided by engineer. */ +"Calls prohibited!" = "Aramalara izin verilmiyor!"; + +/* No comment provided by engineer. */ +"Camera not available" = "Kamera mevcut değil"; + +/* No comment provided by engineer. */ +"Can't call contact" = "Kişi aranamıyor"; + +/* No comment provided by engineer. */ +"Can't call member" = "Üye aranamaz"; + +/* No comment provided by engineer. */ +"Can't invite contact!" = "Kişi davet edilemiyor!"; + +/* No comment provided by engineer. */ +"Can't invite contacts!" = "Kişiler davet edilemiyor!"; + +/* No comment provided by engineer. */ +"Can't message member" = "Üyeye mesaj gönderilemiyor"; + +/* alert action +alert button */ +"Cancel" = "İptal et"; + +/* No comment provided by engineer. */ +"Cancel migration" = "Taşımayı iptal et"; + +/* feature offered item */ +"cancelled %@" = "%@ iptal edildi"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Veritabanı şifresini kaydetmek için Anahtar Zinciri'ne erişilemiyor"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Mesaj iletilemiyor"; + +/* alert title */ +"Cannot receive file" = "Dosya alınamıyor"; + +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Kapasite aşıldı - alıcı önceden gönderilen mesajları almadı."; + +/* No comment provided by engineer. */ +"Cellular" = "Hücresel Veri"; + +/* No comment provided by engineer. */ +"Change" = "Değiştir"; + +/* authentication reason */ +"Change chat profiles" = "Sohbet profillerini değiştir"; + +/* No comment provided by engineer. */ +"Change database passphrase?" = "Veritabanı parolasını değiştir?"; + +/* authentication reason */ +"Change lock mode" = "Kilit modunu değiştir"; + +/* No comment provided by engineer. */ +"Change member role?" = "Üye rolünü değiştir?"; + +/* authentication reason */ +"Change passcode" = "Şifreyi değiştir"; + +/* No comment provided by engineer. */ +"Change receiving address" = "Alıcı adresini değiştir"; + +/* No comment provided by engineer. */ +"Change receiving address?" = "Alıcı adresi değiştirilsin mi?"; + +/* No comment provided by engineer. */ +"Change role" = "Rolü değiştir"; + +/* authentication reason */ +"Change self-destruct mode" = "Kendini yok etme modunu değiştir"; + +/* authentication reason +set passcode view */ +"Change self-destruct passcode" = "Kendini yok eden parolayı değiştir"; + +/* chat item text */ +"changed address for you" = "senin için adres değiştirildi"; + +/* rcv group event chat item */ +"changed role of %@ to %@" = "1$@ rolünü %2$@ olarak değiştirdi"; + +/* rcv group event chat item */ +"changed your role to %@" = "rolünü %@ olarak değiştirdi"; + +/* chat item text */ +"changing address for %@…" = "%@ için adres değiştiriliyor…"; + +/* chat item text */ +"changing address…" = "adres değiştiriliyor…"; + +/* No comment provided by engineer. */ +"Chat" = "Sohbet"; + +/* No comment provided by engineer. */ +"Chat already exists" = "Sohbet zaten mevcut"; + +/* No comment provided by engineer. */ +"Chat already exists!" = "Sohbet zaten mevcut!"; + +/* No comment provided by engineer. */ +"Chat colors" = "Sohbet renkleri"; + +/* No comment provided by engineer. */ +"Chat console" = "Sohbet konsolu"; + +/* No comment provided by engineer. */ +"Chat database" = "Sohbet veritabanı"; + +/* No comment provided by engineer. */ +"Chat database deleted" = "Sohbet veritabanı silindi"; + +/* No comment provided by engineer. */ +"Chat database exported" = "Veritabanı dışa aktarıldı"; + +/* No comment provided by engineer. */ +"Chat database imported" = "Sohbet veritabanı içe aktarıldı"; + +/* No comment provided by engineer. */ +"Chat is running" = "Sohbet çalışıyor"; + +/* No comment provided by engineer. */ +"Chat is stopped" = "Sohbet durdu"; + +/* 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." = "Sohbet durduruldu. Bu veritabanını zaten başka bir cihazda kullandıysanız, sohbete başlamadan önce onu geri aktarmalısınız."; + +/* No comment provided by engineer. */ +"Chat list" = "Sohbet listesi"; + +/* No comment provided by engineer. */ +"Chat migrated!" = "Sohbet taşındı!"; + +/* No comment provided by engineer. */ +"Chat preferences" = "Sohbet tercihleri"; + +/* alert message */ +"Chat preferences were changed." = "Sohbet tercihleri değiştirildi."; + +/* No comment provided by engineer. */ +"Chat profile" = "Kullanıcı profili"; + +/* No comment provided by engineer. */ +"Chat theme" = "Sohbet teması"; + +/* No comment provided by engineer. */ +"Chat will be deleted for all members - this cannot be undone!" = "Sohbet bütün üyeler için silinecek - bu geri alınamaz!"; + +/* No comment provided by engineer. */ +"Chat will be deleted for you - this cannot be undone!" = "Sohbet senden silinecek - bu geri alınamaz!"; + +/* No comment provided by engineer. */ +"Chats" = "Sohbetler"; + +/* No comment provided by engineer. */ +"Check messages every 20 min." = "Her 20 dakikada mesajları kontrol et."; + +/* No comment provided by engineer. */ +"Check messages when allowed." = "İzin verildiğinde mesajları kontrol et."; + +/* alert title */ +"Check server address and try again." = "Sunucu adresini kontrol edip tekrar deneyin."; + +/* No comment provided by engineer. */ +"Chinese and Spanish interface" = "Çince ve İspanyolca arayüz"; + +/* No comment provided by engineer. */ +"Choose _Migrate from another device_ on the new device and scan QR code." = "Yeni cihazda _Başka bir cihazdan taşı_ seçeneğini seçin ve QR kodunu tarayın."; + +/* No comment provided by engineer. */ +"Choose file" = "Dosya seç"; + +/* No comment provided by engineer. */ +"Choose from library" = "Kütüphaneden seç"; + +/* No comment provided by engineer. */ +"Chunks deleted" = "Parçalar silindi"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Parçalar indirildi"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Parçalar yüklendi"; + +/* swipe action */ +"Clear" = "Temizle"; + +/* No comment provided by engineer. */ +"Clear conversation" = "Sohbeti temizle"; + +/* No comment provided by engineer. */ +"Clear conversation?" = "Sohbet temizlensin mi?"; + +/* No comment provided by engineer. */ +"Clear private notes?" = "Gizli notlar temizlensin mi?"; + +/* No comment provided by engineer. */ +"Clear verification" = "Doğrulamayı temizle"; + +/* No comment provided by engineer. */ +"Color chats with the new themes." = "Yeni temalarla renkli sohbetler."; + +/* No comment provided by engineer. */ +"Color mode" = "Renk modu"; + +/* No comment provided by engineer. */ +"colored" = "renklendirilmiş"; + +/* server test step */ +"Compare file" = "Dosya karşılaştır"; + +/* No comment provided by engineer. */ +"Compare security codes with your contacts." = "Güvenlik kodlarını kişilerinle karşılaştır."; + +/* No comment provided by engineer. */ +"complete" = "tamamlandı"; + +/* No comment provided by engineer. */ +"Completed" = "Tamamlandı"; + +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "Şuradaki koşullar kabul edildi: %@."; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "Koşullar operatör(ler) için kabul edildi: **%@**."; + +/* No comment provided by engineer. */ +"Conditions are already accepted for these operator(s): **%@**." = "Koşullar çoktan operatör(ler) tarafından kabul edildi: **%@**."; + +/* No comment provided by engineer. */ +"Conditions of use" = "Kullanım koşulları"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "Koşullar bu operatör(ler) için kabul edilecektir: **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "Koşullar şu tarihte kabul edilecektir: %@."; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "Koşullar etkin operatörler için şu tarihte otomatik olarak kabul edilecektir: %@."; + +/* No comment provided by engineer. */ +"Configure ICE servers" = "ICE sunucularını ayarla"; + +/* No comment provided by engineer. */ +"Confirm" = "Onayla"; + +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "Kişiyi silmek istediğinizden emin misiniz ?"; + +/* No comment provided by engineer. */ +"Confirm database upgrades" = "Veritabanı geliştirmelerini onayla"; + +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Bilinmeyen sunuculardan gelen dosyaları onayla."; + +/* No comment provided by engineer. */ +"Confirm network settings" = "Ağ ayarlarını onaylayın"; + +/* No comment provided by engineer. */ +"Confirm new passphrase…" = "Yeni parolayı onayla…"; + +/* No comment provided by engineer. */ +"Confirm Passcode" = "Parolayı onayla"; + +/* No comment provided by engineer. */ +"Confirm password" = "Şifreyi onayla"; + +/* No comment provided by engineer. */ +"Confirm that you remember database passphrase to migrate it." = "Taşımak için veritabanı parolasını hatırladığınızı doğrulayın."; + +/* No comment provided by engineer. */ +"Confirm upload" = "Yüklemeyi onayla"; + +/* server test step */ +"Connect" = "Bağlan"; + +/* No comment provided by engineer. */ +"Connect automatically" = "Otomatik olarak bağlan"; + +/* No comment provided by engineer. */ +"Connect incognito" = "Gizli bağlan"; + +/* No comment provided by engineer. */ +"Connect to desktop" = "Bilgisayara bağlan"; + +/* No comment provided by engineer. */ +"connect to SimpleX Chat developers." = "SimpleX Chat geliştiricilerine bağlan."; + +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Arkadaşlarınıza daha hızlı bağlanın."; + +/* No comment provided by engineer. */ +"Connect to yourself?" = "Kendine mi bağlanacaksın?"; + +/* No comment provided by engineer. */ +"Connect to yourself?\nThis is your own one-time link!" = "Kendine mi bağlanacaksın?\nBu senin kendi tek kullanımlık bağlantın!"; + +/* No comment provided by engineer. */ +"Connect to yourself?\nThis is your own SimpleX address!" = "Kendine mi bağlanacaksın?\nBu senin kendi SimpleX adresin!"; + +/* No comment provided by engineer. */ +"Connect via contact address" = "Kişi adresi aracılığıyla bağlan"; + +/* No comment provided by engineer. */ +"Connect via link" = "Bağlantı aracılığıyla bağlan"; + +/* No comment provided by engineer. */ +"Connect via one-time link" = "Tek kullanımlık bağlantı aracılığıyla bağlan"; + +/* No comment provided by engineer. */ +"Connect with %@" = "%@ ile bağlan"; + +/* No comment provided by engineer. */ +"connected" = "bağlanıldı"; + +/* No comment provided by engineer. */ +"Connected" = "Bağlandı"; + +/* No comment provided by engineer. */ +"Connected desktop" = "Bilgisayara bağlandı"; + +/* rcv group event chat item */ +"connected directly" = "doğrudan bağlandı"; + +/* No comment provided by engineer. */ +"Connected servers" = "Bağlı sunucular"; + +/* No comment provided by engineer. */ +"Connected to desktop" = "Masaüstüne bağlandı"; + +/* No comment provided by engineer. */ +"connecting" = "bağlanılıyor"; + +/* No comment provided by engineer. */ +"Connecting" = "Bağlanıyor"; + +/* No comment provided by engineer. */ +"connecting (accepted)" = "bağlanılıyor (onaylandı)"; + +/* No comment provided by engineer. */ +"connecting (announced)" = "bağlanılıyor (duyuruldu)"; + +/* No comment provided by engineer. */ +"connecting (introduced)" = "bağlanılıyor (tanıtıldı)"; + +/* No comment provided by engineer. */ +"connecting (introduction invitation)" = "bağlanılıyor (tanıtılma isteği)"; + +/* call status */ +"connecting call" = "aramaya bağlanılıyor…"; + +/* No comment provided by engineer. */ +"Connecting server…" = "Sunucuya bağlanıyor…"; + +/* No comment provided by engineer. */ +"Connecting server… (error: %@)" = "Sunucuya bağlanıyor…(hata:%@)"; + +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "Kişiye bağlanılıyor, lütfen bekleyin ya da daha sonra kontrol edin!"; + +/* No comment provided by engineer. */ +"Connecting to desktop" = "Bilgisayara bağlanıyor"; + +/* No comment provided by engineer. */ +"connecting…" = "bağlanılıyor…"; + +/* No comment provided by engineer. */ +"Connection" = "Bağlantı"; + +/* No comment provided by engineer. */ +"Connection and servers status." = "Bağlantı ve sunucuların durumu."; + +/* No comment provided by engineer. */ +"Connection error" = "Bağlantı hatası"; + +/* No comment provided by engineer. */ +"Connection error (AUTH)" = "Bağlantı hatası (DOĞRULAMA)"; + +/* chat list item title (it should not be shown */ +"connection established" = "bağlantı kuruldu"; + +/* No comment provided by engineer. */ +"Connection notifications" = "Bağlantı bildirimleri"; + +/* No comment provided by engineer. */ +"Connection request sent!" = "Bağlantı daveti gönderildi!"; + +/* No comment provided by engineer. */ +"Connection security" = "Bağlantı güvenliği"; + +/* No comment provided by engineer. */ +"Connection terminated" = "Bağlantı sonlandırılmış"; + +/* No comment provided by engineer. */ +"Connection timeout" = "Bağlantı süresi geçmiş"; + +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "Masaüstü ile bağlantı durduruldu"; + +/* connection information */ +"connection:%@" = "bağlantı:%@"; + +/* No comment provided by engineer. */ +"Connections" = "Bağlantılar"; + +/* profile update event chat item */ +"contact %@ changed to %@" = "%1$@ kişisi %2$@ olarak değişti"; + +/* No comment provided by engineer. */ +"Contact allows" = "Kişi izin veriyor"; + +/* No comment provided by engineer. */ +"Contact already exists" = "Kişi zaten mevcut"; + +/* No comment provided by engineer. */ +"Contact deleted!" = "Kişiler silindi!"; + +/* No comment provided by engineer. */ +"contact has e2e encryption" = "kişi uçtan uca şifrelemeye sahiptir"; + +/* No comment provided by engineer. */ +"contact has no e2e encryption" = "kişi uçtan uca şifrelemeye sahip değildir"; + +/* notification */ +"Contact hidden:" = "Kişi gizli:"; + +/* notification */ +"Contact is connected" = "Kişi bağlandı"; + +/* No comment provided by engineer. */ +"Contact is deleted." = "Kişi silindi."; + +/* No comment provided by engineer. */ +"Contact name" = "Kişi adı"; + +/* No comment provided by engineer. */ +"Contact preferences" = "Kişi tercihleri"; + +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "Kişiler silinecek - bu geri alınamaz !"; + +/* No comment provided by engineer. */ +"Contacts" = "Kişiler"; + +/* No comment provided by engineer. */ +"Contacts can mark messages for deletion; you will be able to view them." = "Kişiler silinmesi için mesajları işaretleyebilir; onları görüntüleyebilirsin."; + +/* No comment provided by engineer. */ +"Continue" = "Devam et"; + +/* No comment provided by engineer. */ +"Conversation deleted!" = "Sohbet silindi!"; + +/* No comment provided by engineer. */ +"Copy" = "Kopyala"; + +/* No comment provided by engineer. */ +"Copy error" = "Kopyalama hatası"; + +/* No comment provided by engineer. */ +"Core version: v%@" = "Çekirdek sürümü: v%@"; + +/* No comment provided by engineer. */ +"Corner" = "Köşeleri yuvarlama"; + +/* No comment provided by engineer. */ +"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"; + +/* No comment provided by engineer. */ +"Create a group using a random profile." = "Rasgele profil kullanarak grup oluştur."; + +/* server test step */ +"Create file" = "Dosya oluştur"; + +/* No comment provided by engineer. */ +"Create group" = "Grup oluştur"; + +/* No comment provided by engineer. */ +"Create group link" = "Grup bağlantısı oluştur"; + +/* No comment provided by engineer. */ +"Create link" = "Bağlantı oluştur"; + +/* No comment provided by engineer. */ +"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "[bilgisayar uygulaması] nda yeni bir profil oluştur(https://simplex.chat/downloads/). 💻"; + +/* No comment provided by engineer. */ +"Create profile" = "Profil oluştur"; + +/* server test step */ +"Create queue" = "Sıra oluştur"; + +/* No comment provided by engineer. */ +"Create secret group" = "Gizli grup oluştur"; + +/* No comment provided by engineer. */ +"Create SimpleX address" = "SimpleX adresi oluştur"; + +/* No comment provided by engineer. */ +"Create your profile" = "Profilini oluştur"; + +/* No comment provided by engineer. */ +"Created" = "Yaratıldı"; + +/* No comment provided by engineer. */ +"Created at" = "Şurada oluşturuldu"; + +/* copied message info */ +"Created at: %@" = "Şurada oluşturuldu: %@"; + +/* No comment provided by engineer. */ +"Creating archive link" = "Arşiv bağlantısı oluşturuluyor"; + +/* No comment provided by engineer. */ +"Creating link…" = "Link oluşturuluyor…"; + +/* No comment provided by engineer. */ +"creator" = "oluşturan"; + +/* No comment provided by engineer. */ +"Current conditions text couldn't be loaded, you can review conditions via this link:" = "Şu anki koşulların yazısı yüklenemiyor, bu bağlantıdan koşullara inceleyebilirsin:"; + +/* No comment provided by engineer. */ +"Current Passcode" = "Şu anki şifre"; + +/* No comment provided by engineer. */ +"Current passphrase…" = "Şu anki parola…"; + +/* No comment provided by engineer. */ +"Current profile" = "Aktif profil"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Şu anki maksimum desteklenen dosya boyutu %@ kadardır."; + +/* dropdown time picker choice */ +"custom" = "özel"; + +/* No comment provided by engineer. */ +"Custom time" = "Özel saat"; + +/* No comment provided by engineer. */ +"Customizable message shape." = "Özelleştirilebilir mesaj şekli."; + +/* No comment provided by engineer. */ +"Customize theme" = "Renk temalarını kişiselleştir"; + +/* No comment provided by engineer. */ +"Dark" = "Karanlık"; + +/* No comment provided by engineer. */ +"Dark mode colors" = "Karanlık mod renkleri"; + +/* No comment provided by engineer. */ +"Database downgrade" = "Veritabanı sürüm düşürme"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "Veritabanı şifrelendi!"; + +/* No comment provided by engineer. */ +"Database encryption passphrase will be updated and stored in the keychain.\n" = "Veritabanı şifreleme parolası güncellenecek ve Anahtar Zinciri'nde saklanacaktır.\n"; + +/* No comment provided by engineer. */ +"Database encryption passphrase will be updated.\n" = "Veritabanı şifreleme parolası güncellenecektir.\n"; + +/* No comment provided by engineer. */ +"Database error" = "Veritabanı hatası"; + +/* No comment provided by engineer. */ +"Database ID" = "Veritabanı kimliği"; + +/* copied message info */ +"Database ID: %d" = "Veritabanı kimliği: %d"; + +/* No comment provided by engineer. */ +"Database IDs and Transport isolation option." = "Veritabanı kimlikleri ve Taşıma izolasyonu seçeneği."; + +/* No comment provided by engineer. */ +"Database is encrypted using a random passphrase, you can change it." = "Veritabanı rastgele bir parola kullanılarak şifrelenir, bunu değiştirebilirsiniz."; + +/* No comment provided by engineer. */ +"Database is encrypted using a random passphrase. Please change it before exporting." = "Veritabanı rastgele bir parola kullanılarak şifrelenir. Lütfen dışa aktarmadan önce değiştirin."; + +/* No comment provided by engineer. */ +"Database passphrase" = "Veritabanı parolası"; + +/* No comment provided by engineer. */ +"Database passphrase & export" = "Veritabanı parolası ve dışa aktarma"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Veritabanı parolası Anahtar Zinciri'nde kayıtlı olandan farklıdır."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Sohbeti açmak için veritabanı parolası gereklidir."; + +/* No comment provided by engineer. */ +"Database upgrade" = "Veritabanı yükseltmesi"; + +/* No comment provided by engineer. */ +"database version is newer than the app, but no down migration for: %@" = "veritabanı sürümü uygulamadan daha yeni, ancak aşağı geçiş yok: %@"; + +/* No comment provided by engineer. */ +"Database will be encrypted and the passphrase stored in the keychain.\n" = "Veritabanı şifrelenecek ve parola Anahtar Zinciri'nde saklanacaktır.\n"; + +/* No comment provided by engineer. */ +"Database will be encrypted.\n" = "Veritabanı şifrelenecektir.\n"; + +/* No comment provided by engineer. */ +"Database will be migrated when the app restarts" = "Uygulama yeniden başlatıldığında veritabanı taşınacaktır"; + +/* time unit */ +"days" = "gün"; + +/* 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ı"; + +/* No comment provided by engineer. */ +"decryption errors" = "Şifre çözme hataları"; + +/* delete after time +pref value */ +"default (%@)" = "varsayılan (%@)"; + +/* No comment provided by engineer. */ +"default (no)" = "varsayılan (hayır)"; + +/* No comment provided by engineer. */ +"default (yes)" = "varsayılan (evet)"; + +/* alert action +swipe action */ +"Delete" = "Sil"; + +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "Üyelerin %lld mesajları silinsin mi?"; + +/* No comment provided by engineer. */ +"Delete %lld messages?" = "%lld mesaj silinsin mi?"; + +/* No comment provided by engineer. */ +"Delete address" = "Adresi sil"; + +/* No comment provided by engineer. */ +"Delete address?" = "Adres silinsin mi?"; + +/* No comment provided by engineer. */ +"Delete after" = "Sonra sil"; + +/* No comment provided by engineer. */ +"Delete all files" = "Bütün dosyaları sil"; + +/* No comment provided by engineer. */ +"Delete and notify contact" = "Sil ve kişiye bildir"; + +/* No comment provided by engineer. */ +"Delete chat" = "Sohbeti sil"; + +/* No comment provided by engineer. */ +"Delete chat profile" = "Sohbet profilini sil"; + +/* No comment provided by engineer. */ +"Delete chat profile?" = "Sohbet profili silinsin mi?"; + +/* No comment provided by engineer. */ +"Delete chat?" = "Sohbet silinsin mi?"; + +/* No comment provided by engineer. */ +"Delete connection" = "Bağlantıyı sil"; + +/* No comment provided by engineer. */ +"Delete contact" = "Kişiyi sil"; + +/* No comment provided by engineer. */ +"Delete contact?" = "Kişiyi sil?"; + +/* No comment provided by engineer. */ +"Delete database" = "Veritabanını sil"; + +/* No comment provided by engineer. */ +"Delete database from this device" = "Veritabanını bu cihazdan sil"; + +/* server test step */ +"Delete file" = "Dosyayı sil"; + +/* No comment provided by engineer. */ +"Delete files and media?" = "Dosyalar ve medya silinsin mi?"; + +/* No comment provided by engineer. */ +"Delete files for all chat profiles" = "Bütün sohbet profilleri için dosyaları sil"; + +/* chat feature */ +"Delete for everyone" = "Herkesten sil"; + +/* No comment provided by engineer. */ +"Delete for me" = "Benden sil"; + +/* No comment provided by engineer. */ +"Delete group" = "Grubu sil"; + +/* No comment provided by engineer. */ +"Delete group?" = "Grup silinsin mi?"; + +/* No comment provided by engineer. */ +"Delete invitation" = "Daveti sil"; + +/* No comment provided by engineer. */ +"Delete link" = "Bağlantıyı sil"; + +/* No comment provided by engineer. */ +"Delete link?" = "Bağlantı silinsin mi?"; + +/* No comment provided by engineer. */ +"Delete member message?" = "Kişinin mesajı silinsin mi?"; + +/* No comment provided by engineer. */ +"Delete message?" = "Mesaj silinsin mi?"; + +/* alert button */ +"Delete messages" = "Mesajları sil"; + +/* No comment provided by engineer. */ +"Delete messages after" = "Mesajları sonra sil"; + +/* No comment provided by engineer. */ +"Delete old database" = "Eski veritabanını sil"; + +/* No comment provided by engineer. */ +"Delete old database?" = "Eski veritabanı silinsin mi?"; + +/* No comment provided by engineer. */ +"Delete or moderate up to 200 messages." = "200'e kadar mesajı silin veya düzenleyin."; + +/* No comment provided by engineer. */ +"Delete pending connection?" = "Bekleyen bağlantı silinsin mi?"; + +/* No comment provided by engineer. */ +"Delete profile" = "Profili sil"; + +/* server test step */ +"Delete queue" = "Sırayı sil"; + +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Tek seferde en fazla 20 mesaj silin."; + +/* No comment provided by engineer. */ +"Delete user profile?" = "Kullanıcı profili silinsin mi?"; + +/* No comment provided by engineer. */ +"Delete without notification" = "Bildirim göndermeden sil"; + +/* deleted chat item */ +"deleted" = "silindi"; + +/* No comment provided by engineer. */ +"Deleted" = "Silindi"; + +/* No comment provided by engineer. */ +"Deleted at" = "de silindi"; + +/* copied message info */ +"Deleted at: %@" = "%@ de silindi"; + +/* rcv direct event chat item */ +"deleted contact" = "silinmiş kişi"; + +/* rcv group event chat item */ +"deleted group" = "silinmiş grup"; + +/* No comment provided by engineer. */ +"Deletion errors" = "Silme hatası"; + +/* No comment provided by engineer. */ +"Delivery" = "Teslimat"; + +/* No comment provided by engineer. */ +"Delivery receipts are disabled!" = "Mesaj gönderim bilgisi devre dışı!"; + +/* No comment provided by engineer. */ +"Delivery receipts!" = "Mesaj gönderildi bilgisi!"; + +/* No comment provided by engineer. */ +"Description" = "Açıklama"; + +/* No comment provided by engineer. */ +"Desktop address" = "Bilgisayar adresi"; + +/* No comment provided by engineer. */ +"Desktop app version %@ is not compatible with this app." = "Masaüstü uygulaması sürümü %@ bu uygulama ile uyumlu değildir."; + +/* No comment provided by engineer. */ +"Desktop devices" = "Bilgisayar cihazları"; + +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "Hedef sunucu adresi %@, yönlendirme sunucusu %@ ayarlarıyla uyumlu değil."; + +/* snd error text */ +"Destination server error: %@" = "Hedef sunucu hatası: %@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "Hedef sunucu %@ sürümü, yönlendirme sunucusu %@ ile uyumlu değil."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Detaylı istatistikler"; + +/* No comment provided by engineer. */ +"Details" = "Detaylar"; + +/* No comment provided by engineer. */ +"Develop" = "Geliştir"; + +/* No comment provided by engineer. */ +"Developer options" = "Geliştirici seçenekleri"; + +/* No comment provided by engineer. */ +"Developer tools" = "Geliştirici araçları"; + +/* No comment provided by engineer. */ +"Device" = "Cihaz"; + +/* No comment provided by engineer. */ +"Device authentication is disabled. Turning off SimpleX Lock." = "Cihaz kimlik doğrulaması devre dışı. SimpleX Kilidi kapatılıyor."; + +/* No comment provided by engineer. */ +"Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." = "Cihaz kimlik doğrulaması etkin değil. Cihaz kimlik doğrulamasını etkinleştirdikten sonra SimpleX Kilidini Ayarlar üzerinden açabilirsiniz."; + +/* No comment provided by engineer. */ +"different migration in the app/database: %@ / %@" = "uygulamada/veritabanında farklı geçiş: %@ / %@"; + +/* No comment provided by engineer. */ +"Different names, avatars and transport isolation." = "Farklı isimler, profil fotoğrafları ve taşıma izolasyonu."; + +/* connection level description */ +"direct" = "doğrudan"; + +/* chat feature */ +"Direct messages" = "Doğrudan mesajlar"; + +/* No comment provided by engineer. */ +"Direct messages between members are prohibited in this chat." = "Üyeler arası doğrudan mesajlar bu sohbette yasaktır."; + +/* No comment provided by engineer. */ +"Direct messages between members are prohibited." = "Bu grupta üyeler arasında direkt mesajlaşma yasaktır."; + +/* No comment provided by engineer. */ +"Disable (keep overrides)" = "Devre dışı bırak (geçersiz kılmaları koru)"; + +/* No comment provided by engineer. */ +"Disable for all" = "Herkes için devre dışı bırak"; + +/* authentication reason */ +"Disable SimpleX Lock" = "SimpleX Kilidini devre dışı bırak"; + +/* No comment provided by engineer. */ +"disabled" = "devre dışı"; + +/* No comment provided by engineer. */ +"Disabled" = "Devre dışı"; + +/* No comment provided by engineer. */ +"Disappearing message" = "Kaybolan mesaj"; + +/* chat feature */ +"Disappearing messages" = "Kaybolan mesajlar"; + +/* No comment provided by engineer. */ +"Disappearing messages are prohibited in this chat." = "Kaybolan mesajlar bu sohbette yasaklanmış."; + +/* No comment provided by engineer. */ +"Disappearing messages are prohibited." = "Kaybolan mesajlar bu grupta yasaklanmış."; + +/* No comment provided by engineer. */ +"Disappears at" = "da kaybolur"; + +/* copied message info */ +"Disappears at: %@" = "%@ da kaybolur"; + +/* server test step */ +"Disconnect" = "Bağlantıyı kes"; + +/* No comment provided by engineer. */ +"Disconnect desktop?" = "Bilgisayarla bağlantı kesilsin mi?"; + +/* No comment provided by engineer. */ +"Discover and join groups" = "Keşfet ve gruplara katıl"; + +/* No comment provided by engineer. */ +"Discover via local network" = "Yerel ağ aracılığıyla keşfet"; + +/* No comment provided by engineer. */ +"Do it later" = "Sonra yap"; + +/* No comment provided by engineer. */ +"Do not send history to new members." = "Yeni üyelere geçmişi gönderme."; + +/* No comment provided by engineer. */ +"Do NOT send messages directly, even if your or destination server does not support private routing." = "Sizin veya hedef sunucunun özel yönlendirmeyi desteklememesi durumunda bile mesajları doğrudan GÖNDERMEYİN."; + +/* No comment provided by engineer. */ +"Do not use credentials with proxy." = "Kimlik bilgilerini proxy ile kullanmayın."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "Gizli yönlendirmeyi KULLANMA."; + +/* No comment provided by engineer. */ +"Do NOT use SimpleX for emergency calls." = "Acil aramalar için SimpleX'i KULLANMAYIN."; + +/* No comment provided by engineer. */ +"Don't create address" = "Adres oluşturma"; + +/* No comment provided by engineer. */ +"Don't enable" = "Etkinleştirme"; + +/* No comment provided by engineer. */ +"Don't show again" = "Yeniden gösterme"; + +/* No comment provided by engineer. */ +"Downgrade and open chat" = "Sürüm düşür ve sohbeti aç"; + +/* alert button +chat item action */ +"Download" = "İndir"; + +/* No comment provided by engineer. */ +"Download errors" = "İndirme hataları"; + +/* No comment provided by engineer. */ +"Download failed" = "Yükleme başarısız oldu"; + +/* server test step */ +"Download file" = "Dosya indir"; + +/* alert action */ +"Download files" = "Dosyaları indirin"; + +/* No comment provided by engineer. */ +"Downloaded" = "İndirildi"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Dosyalar İndirildi"; + +/* No comment provided by engineer. */ +"Downloading archive" = "Arşiv indiriliyor"; + +/* No comment provided by engineer. */ +"Downloading link details" = "Bağlantı detayları indiriliyor"; + +/* No comment provided by engineer. */ +"Duplicate display name!" = "Yinelenen görünen ad!"; + +/* integrity error chat item */ +"duplicate message" = "yinelenen mesaj"; + +/* No comment provided by engineer. */ +"duplicates" = "Kopyalar"; + +/* No comment provided by engineer. */ +"Duration" = "Süre"; + +/* No comment provided by engineer. */ +"e2e encrypted" = "uçtan uca şifrelenmiş"; + +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "Uçtan uca şifrelenmiş bildirimler."; + +/* chat item action */ +"Edit" = "Düzenle"; + +/* No comment provided by engineer. */ +"Edit group profile" = "Grup profilini düzenle"; + +/* No comment provided by engineer. */ +"Enable" = "Etkinleştir"; + +/* No comment provided by engineer. */ +"Enable (keep overrides)" = "Etkinleştir (geçersiz kılmaları koru)"; + +/* alert title */ +"Enable automatic message deletion?" = "Otomatik mesaj silme etkinleştirilsin mi?"; + +/* No comment provided by engineer. */ +"Enable camera access" = "Kamera erişimini etkinleştir"; + +/* No comment provided by engineer. */ +"Enable for all" = "Herkes için etkinleştir"; + +/* No comment provided by engineer. */ +"Enable in direct chats (BETA)!" = "Doğrudan sohbetlerde etkinleştirin (BETA)!"; + +/* No comment provided by engineer. */ +"Enable instant notifications?" = "Anlık bildirimler etkinleştirilsin mi?"; + +/* 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?"; + +/* No comment provided by engineer. */ +"Enable self-destruct" = "Kendini imhayı etkinleştir"; + +/* set passcode view */ +"Enable self-destruct passcode" = "Kendini imha şifresini etkinleştir"; + +/* authentication reason */ +"Enable SimpleX Lock" = "SimpleX Kilidini etkinleştir"; + +/* No comment provided by engineer. */ +"Enable TCP keep-alive" = "TCP canlı tutmayı etkinleştir"; + +/* enabled status */ +"enabled" = "etkin"; + +/* No comment provided by engineer. */ +"Enabled" = "Etkin"; + +/* No comment provided by engineer. */ +"Enabled for" = "Şunlar için etkinleştirildi"; + +/* enabled status */ +"enabled for contact" = "konuşulan kişi için etkinleşti"; + +/* enabled status */ +"enabled for you" = "senin için etkinleştirildi"; + +/* No comment provided by engineer. */ +"Encrypt" = "Şifreleme"; + +/* No comment provided by engineer. */ +"Encrypt database?" = "Veritabanı şifrelensin mi?"; + +/* No comment provided by engineer. */ +"Encrypt local files" = "Yerel dosyaları şifrele"; + +/* No comment provided by engineer. */ +"Encrypt stored files & media" = "Saklanan dosyaları ve medyayı şifreleyin"; + +/* No comment provided by engineer. */ +"Encrypted database" = "Şifrelenmiş veritabanı"; + +/* notification */ +"Encrypted message or another event" = "Şifrelenmiş mesaj veya başka bir etkinlik"; + +/* notification */ +"Encrypted message: app is stopped" = "Şifrelenmiş mesaj: uygulama durdu"; + +/* notification */ +"Encrypted message: database error" = "Şifrelenmiş mesaj: veritabanı hatası"; + +/* notification */ +"Encrypted message: database migration error" = "Şifrelenmiş mesaj: veritabanı taşıma hatası"; + +/* notification */ +"Encrypted message: keychain error" = "Şifrelenmiş mesaj: Anahtar Zinciri hatası"; + +/* notification */ +"Encrypted message: no passphrase" = "Şifrelenmiş mesaj: parola yok"; + +/* notification */ +"Encrypted message: unexpected error" = "Şifrelenmiş mesaj: beklenmeyen hata"; + +/* chat item text */ +"encryption agreed" = "şifreleme kabul edildi"; + +/* chat item text */ +"encryption agreed for %@" = "şifreleme %@ için kabul edildi"; + +/* chat item text */ +"encryption ok" = "şifreleme etkin"; + +/* chat item text */ +"encryption ok for %@" = "şifreleme %@ için etkin"; + +/* chat item text */ +"encryption re-negotiation allowed" = "şifrelemenin yeniden anlaşmasına izin verildi"; + +/* chat item text */ +"encryption re-negotiation allowed for %@" = "şifrelemenin yeniden anlaşmasına %@ için izin verildi"; + +/* message decrypt error item */ +"Encryption re-negotiation error" = "Şifreleme yeniden anlaşma hatası"; + +/* No comment provided by engineer. */ +"Encryption re-negotiation failed." = "Şifreleme yeniden anlaşma başarısız oldu."; + +/* chat item text */ +"encryption re-negotiation required" = "şifrelemenin yeniden anlaşması gerekiyor"; + +/* chat item text */ +"encryption re-negotiation required for %@" = "şifrelemenin yeniden anlaşması %@ için gerekiyor"; + +/* No comment provided by engineer. */ +"ended" = "bitti"; + +/* call status */ +"ended call %@" = "%@ araması bitti"; + +/* No comment provided by engineer. */ +"Enter correct passphrase." = "Doğru şifreyi gir."; + +/* No comment provided by engineer. */ +"Enter group name…" = "Grup adı gir…"; + +/* No comment provided by engineer. */ +"Enter Passcode" = "Şifre gir"; + +/* No comment provided by engineer. */ +"Enter passphrase" = "Parolayı girin"; + +/* No comment provided by engineer. */ +"Enter passphrase…" = "Parola gir…"; + +/* No comment provided by engineer. */ +"Enter password above to show!" = "Göstermek için yukarıdaki şifreyi gir!"; + +/* No comment provided by engineer. */ +"Enter server manually" = "Sunucuya manuel olarak gir"; + +/* No comment provided by engineer. */ +"Enter this device name…" = "Bu cihazın adını gir…"; + +/* placeholder */ +"Enter welcome message…" = "Hoşgeldin mesajı gir…"; + +/* placeholder */ +"Enter welcome message… (optional)" = "Hoşgeldin mesajı gir... (opsiyonel)"; + +/* No comment provided by engineer. */ +"Enter your name…" = "Adını gir…"; + +/* No comment provided by engineer. */ +"error" = "hata"; + +/* No comment provided by engineer. */ +"Error" = "Hata"; + +/* No comment provided by engineer. */ +"Error aborting address change" = "Adres değişikliği iptal edilirken hata oluştu"; + +/* alert title */ +"Error accepting conditions" = "Koşulları kabul ederken hata oluştu"; + +/* No comment provided by engineer. */ +"Error accepting contact request" = "Bağlantı isteği kabul edilirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error adding member(s)" = "Üye(ler) eklenirken hata oluştu"; + +/* alert title */ +"Error adding server" = "Sunucu eklenirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error changing address" = "Adres değiştirilirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error changing connection profile" = "Bağlantı profili değiştirilirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error changing role" = "Rol değiştirilirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error changing setting" = "Ayar değiştirilirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error changing to incognito!" = "Gizli moduna geçerken hata oluştu!"; + +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "Yönlendirme sunucusu %@'ya bağlanırken hata oluştu. Lütfen daha sonra deneyin."; + +/* No comment provided by engineer. */ +"Error creating address" = "Adres oluşturulurken hata oluştu"; + +/* No comment provided by engineer. */ +"Error creating group" = "Grup oluşturulurken hata oluştu"; + +/* No comment provided by engineer. */ +"Error creating group link" = "Grup bağlantısı oluşturulurken hata oluştu"; + +/* No comment provided by engineer. */ +"Error creating member contact" = "Kişi iletişimi oluşturulurken hata oluştu"; + +/* No comment provided by engineer. */ +"Error creating message" = "Mesaj oluşturulurken hata"; + +/* No comment provided by engineer. */ +"Error creating profile!" = "Profil oluşturulurken hata oluştu!"; + +/* No comment provided by engineer. */ +"Error decrypting file" = "Dosya şifresi çözülürken hata oluştu"; + +/* No comment provided by engineer. */ +"Error deleting chat database" = "Sohbet veritabanı silinirken sorun oluştu"; + +/* No comment provided by engineer. */ +"Error deleting chat!" = "Sohbet silinirken hata oluştu!"; + +/* No comment provided by engineer. */ +"Error deleting connection" = "Bağlantı silinirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error deleting database" = "Veritabanı silinirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error deleting old database" = "Eski veritabanı silinirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error deleting token" = "Token silinirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error deleting user profile" = "Kullanıcı profili silinirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error downloading the archive" = "Arşiv indirilirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error enabling delivery receipts!" = "Görüldü bilgisi etkinleştirilirken hata oluştu!"; + +/* No comment provided by engineer. */ +"Error enabling notifications" = "Bildirimler etkinleştirilirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error encrypting database" = "Veritabanı şifrelemesi çözülürken hata oluştu"; + +/* No comment provided by engineer. */ +"Error exporting chat database" = "Sohbet veritabanı dışa aktarılırken hata oluştu"; + +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Tema dışa aktarılırken hata oluştu: %@"; + +/* No comment provided by engineer. */ +"Error importing chat database" = "Sohbet veritabanı içe aktarılırken hata oluştu"; + +/* No comment provided by engineer. */ +"Error joining group" = "Gruba katılırken hata oluştu"; + +/* alert title */ +"Error loading servers" = "Sunucular yüklenirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error migrating settings" = "Ayarlar taşınırken hata oluştu"; + +/* No comment provided by engineer. */ +"Error opening chat" = "Sohbeti açarken sorun oluştu"; + +/* alert title */ +"Error receiving file" = "Dosya alınırken sorun oluştu"; + +/* No comment provided by engineer. */ +"Error reconnecting server" = "Hata, sunucuya yeniden bağlanılıyor"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Hata sunuculara yeniden bağlanılıyor"; + +/* No comment provided by engineer. */ +"Error removing member" = "Kişiyi silerken sorun oluştu"; + +/* No comment provided by engineer. */ +"Error resetting statistics" = "Hata istatistikler sıfırlanıyor"; + +/* No comment provided by engineer. */ +"Error saving group profile" = "Grup profili kaydedilirken sorun oluştu"; + +/* No comment provided by engineer. */ +"Error saving ICE servers" = "ICE sunucularını kaydedirken sorun oluştu"; + +/* No comment provided by engineer. */ +"Error saving passcode" = "Parola kaydedilirken sorun oluştu"; + +/* No comment provided by engineer. */ +"Error saving passphrase to keychain" = "Parolayı Anahtar Zincirine kaydederken hata oluştu"; + +/* alert title */ +"Error saving servers" = "Sunucular kaydedilirken hata oluştu"; + +/* when migrating */ +"Error saving settings" = "Ayarlar kaydedilirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error saving user password" = "Kullanıcı şifresi kaydedilirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error scanning code: %@" = "Kod taranırken hata oluştu: %@"; + +/* No comment provided by engineer. */ +"Error sending email" = "Eposta gönderilirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error sending member contact invitation" = "Kişi iletişim daveti gönderilirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error sending message" = "Mesaj gönderilirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error setting delivery receipts!" = "Görüldü ayarlanırken hata oluştu!"; + +/* No comment provided by engineer. */ +"Error starting chat" = "Sohbet başlatılırken hata oluştu"; + +/* No comment provided by engineer. */ +"Error stopping chat" = "Sohbet durdurulurken hata oluştu"; + +/* No comment provided by engineer. */ +"Error switching profile" = "Profil değiştirme sırasında hata oluştu"; + +/* alertTitle */ +"Error switching profile!" = "Profil değiştirilirken hata oluştu!"; + +/* No comment provided by engineer. */ +"Error synchronizing connection" = "Bağlantı senkronizasyonunda hata oluştu"; + +/* No comment provided by engineer. */ +"Error updating group link" = "Grup bağlantısı güncellenirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error updating message" = "Mesaj güncellenirken hata oluştu"; + +/* alert title */ +"Error updating server" = "Sunucu güncellenirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error updating settings" = "Ayarları güncellerken hata oluştu"; + +/* No comment provided by engineer. */ +"Error updating user privacy" = "Kullanıcı gizliliği güncellenirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error uploading the archive" = "Arşiv yüklenirken hata oluştu"; + +/* No comment provided by engineer. */ +"Error verifying passphrase:" = "Parola doğrulanırken hata oluştu:"; + +/* No comment provided by engineer. */ +"Error: " = "Hata: "; + +/* alert message +file error text +snd error text */ +"Error: %@" = "Hata: %@"; + +/* No comment provided by engineer. */ +"Error: no database file" = "Hata: veritabanı dosyası yok"; + +/* No comment provided by engineer. */ +"Error: URL is invalid" = "Hata: URL geçersiz"; + +/* No comment provided by engineer. */ +"Errors" = "Hatalar"; + +/* servers error */ +"Errors in servers configuration." = "Sunucular yapılandırılırken hatalar oluştu."; + +/* No comment provided by engineer. */ +"Even when disabled in the conversation." = "Konuşma sırasında devre dışı bırakılsa bile."; + +/* No comment provided by engineer. */ +"Exit without saving" = "Kaydetmeden çık"; + +/* chat item action */ +"Expand" = "Genişlet"; + +/* No comment provided by engineer. */ +"expired" = "Süresi dolmuş"; + +/* No comment provided by engineer. */ +"Export database" = "Veritabanını dışarı aktar"; + +/* No comment provided by engineer. */ +"Export error:" = "Dışarı çıkarma hatası:"; + +/* No comment provided by engineer. */ +"Export theme" = "Temayı dışa aktar"; + +/* No comment provided by engineer. */ +"Exported database archive." = "Dışarı çıkarılmış veritabanı arşivi."; + +/* No comment provided by engineer. */ +"Exported file doesn't exist" = "Dışa aktarılan dosya mevcut değil"; + +/* No comment provided by engineer. */ +"Exporting database archive…" = "Dışarı çıkarılmış veritabanı arşivi…"; + +/* No comment provided by engineer. */ +"Failed to remove passphrase" = "Parola kaldırılamadı"; + +/* No comment provided by engineer. */ +"Fast and no wait until the sender is online!" = "Hızlı ve gönderici çevrimiçi olana kadar beklemek yok!"; + +/* No comment provided by engineer. */ +"Faster joining and more reliable messages." = "Daha hızlı katılma ve daha güvenilir mesajlar."; + +/* swipe action */ +"Favorite" = "Favori"; + +/* file error alert title */ +"File error" = "Dosya hatası"; + +/* alert message */ +"File errors:\n%@" = "Dosya hataları:\n%@"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Dosya bulunamadı - muhtemelen dosya silindi veya göderim iptal edildi."; + +/* file error text */ +"File server error: %@" = "Dosya sunucusu hatası: %@"; + +/* No comment provided by engineer. */ +"File status" = "Dosya durumu"; + +/* copied message info */ +"File status: %@" = "Dosya durumu: %@"; + +/* No comment provided by engineer. */ +"File will be deleted from servers." = "Dosya sunuculardan silinecek."; + +/* No comment provided by engineer. */ +"File will be received when your contact completes uploading it." = "Dosya kişi yüklemeyi tamamladığında alınacak."; + +/* No comment provided by engineer. */ +"File will be received when your contact is online, please wait or check later!" = "Dosya kişi çevrimiçi olduğunda alınacaktır, lütfen bekleyin veya daha sonra kontrol edin!"; + +/* No comment provided by engineer. */ +"File: %@" = "Dosya: %@"; + +/* No comment provided by engineer. */ +"Files" = "Dosyalar"; + +/* No comment provided by engineer. */ +"Files & media" = "Dosyalar & medya"; + +/* chat feature */ +"Files and media" = "Dosyalar ve medya"; + +/* No comment provided by engineer. */ +"Files and media are prohibited." = "Dosyalar ve medya bu grupta yasaklandı."; + +/* No comment provided by engineer. */ +"Files and media not allowed" = "Dosyalar ve medyaya izin verilmiyor"; + +/* No comment provided by engineer. */ +"Files and media prohibited!" = "Dosyalar ve medya yasaklandı!"; + +/* No comment provided by engineer. */ +"Filter unread and favorite chats." = "Favori ve okunmamış sohbetleri filtrele."; + +/* No comment provided by engineer. */ +"Finalize migration" = "Taşıma işlemini sonlandır"; + +/* No comment provided by engineer. */ +"Finalize migration on another device." = "Taşıma işlemini başka bir cihazda sonlandırın."; + +/* No comment provided by engineer. */ +"Finally, we have them! 🚀" = "Sonunda, onlara sahibiz! 🚀"; + +/* No comment provided by engineer. */ +"Find chats faster" = "Sohbetleri daha hızlı bul"; + +/* No comment provided by engineer. */ +"Fix" = "Düzelt"; + +/* No comment provided by engineer. */ +"Fix connection" = "Bağlantıyı düzelt"; + +/* No comment provided by engineer. */ +"Fix connection?" = "Bağlantı düzeltilsin mi?"; + +/* No comment provided by engineer. */ +"Fix encryption after restoring backups." = "Yedekleri geri yükledikten sonra şifrelemeyi düzelt."; + +/* No comment provided by engineer. */ +"Fix not supported by contact" = "Düzeltme kişi tarafından desteklenmiyor"; + +/* No comment provided by engineer. */ +"Fix not supported by group member" = "Düzeltme grup üyesi tarafından desteklenmiyor"; + +/* servers error */ +"For chat profile %@:" = "Sohbet profili için %@:"; + +/* No comment provided by engineer. */ +"For console" = "Konsol için"; + +/* No comment provided by engineer. */ +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Örneğin, eğer kişiniz SimpleX Sohbet sunucusundan mesajları alıyorsa, uygulamanız bu mesajları Flux sunucusundan iletecektir."; + +/* No comment provided by engineer. */ +"For private routing" = "Gizli yönlendirme için"; + +/* No comment provided by engineer. */ +"For social media" = "Sosyal medya için"; + +/* chat item action */ +"Forward" = "İlet"; + +/* alert title */ +"Forward %d message(s)?" = "%d mesaj(lar)ı iletilsin mi?"; + +/* No comment provided by engineer. */ +"Forward and save messages" = "Mesajları ilet ve kaydet"; + +/* alert action */ +"Forward messages" = "İletileri ilet"; + +/* alert message */ +"Forward messages without files?" = "Mesajlar dosyalar olmadan iletilsin mi ?"; + +/* No comment provided by engineer. */ +"Forward up to 20 messages at once." = "Aynı anda en fazla 20 mesaj iletin."; + +/* No comment provided by engineer. */ +"forwarded" = "iletildi"; + +/* No comment provided by engineer. */ +"Forwarded" = "İletildi"; + +/* No comment provided by engineer. */ +"Forwarded from" = "Şuradan iletildi"; + +/* No comment provided by engineer. */ +"Forwarding %lld messages" = "%lld mesajlarını ilet"; + +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Yönlendirme sunucusu %@, hedef sunucu %@'ya bağlanamadı. Lütfen daha sonra deneyin."; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "Yönlendirme sunucusu adresi ağ ayarlarıyla uyumsuz: %@."; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "Yönlendirme sunucusu sürümü ağ ayarlarıyla uyumsuz: %@."; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Yönlendirme sunucusu: %1$@\nHedef sunucu hatası: %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Yönlendirme sunucusu: %1$@\nHata: %2$@"; + +/* No comment provided by engineer. */ +"Found desktop" = "Bilgisayar bulundu"; + +/* No comment provided by engineer. */ +"French interface" = "Fransızca arayüz"; + +/* No comment provided by engineer. */ +"Full link" = "Bütün bağlantı adresi"; + +/* No comment provided by engineer. */ +"Full name (optional)" = "Bütün isim (opsiyonel)"; + +/* No comment provided by engineer. */ +"Fully decentralized – visible only to members." = "Tamamiyle merkezi olmayan - sadece kişilere görünür."; + +/* No comment provided by engineer. */ +"Fully re-implemented - work in background!" = "Arkaplanda çalışma - tamamiyle yeniden yapıldı!"; + +/* No comment provided by engineer. */ +"Further reduced battery usage" = "Daha da azaltılmış pil kullanımı"; + +/* No comment provided by engineer. */ +"GIFs and stickers" = "GİFler ve çıkartmalar"; + +/* message preview */ +"Good afternoon!" = "İyi öğlenler!"; + +/* message preview */ +"Good morning!" = "Günaydın!"; + +/* No comment provided by engineer. */ +"Group" = "Grup"; + +/* No comment provided by engineer. */ +"Group already exists" = "Grup çoktan mevcut"; + +/* No comment provided by engineer. */ +"Group already exists!" = "Grup çoktan mevcut!"; + +/* No comment provided by engineer. */ +"group deleted" = "grup silindi"; + +/* No comment provided by engineer. */ +"Group display name" = "Grup görünen adı"; + +/* No comment provided by engineer. */ +"Group full name (optional)" = "Grubun bütün ismi (opsiyonel)"; + +/* No comment provided by engineer. */ +"Group image" = "Grup fotoğrafı"; + +/* No comment provided by engineer. */ +"Group invitation" = "Grup daveti"; + +/* No comment provided by engineer. */ +"Group invitation expired" = "Grup davetinin süresi doldu"; + +/* No comment provided by engineer. */ +"Group invitation is no longer valid, it was removed by sender." = "Grup davet artık geçerli değil, gönderici tarafından silindi."; + +/* No comment provided by engineer. */ +"Group link" = "Grup bağlantısı"; + +/* No comment provided by engineer. */ +"Group links" = "Grup bağlantıları"; + +/* notification */ +"Group message:" = "Grup mesajı:"; + +/* No comment provided by engineer. */ +"Group moderation" = "Grup yöneticiliği"; + +/* No comment provided by engineer. */ +"Group preferences" = "Grup tercihleri"; + +/* No comment provided by engineer. */ +"Group profile" = "Grup profili"; + +/* No comment provided by engineer. */ +"Group profile is stored on members' devices, not on the servers." = "Grup profili üyelerin cihazlarında saklanır, sunucularda değil."; + +/* snd group event chat item */ +"group profile updated" = "grup profili güncellendi"; + +/* No comment provided by engineer. */ +"Group welcome message" = "Grup hoşgeldin mesajı"; + +/* No comment provided by engineer. */ +"Group will be deleted for all members - this cannot be undone!" = "Grup tüm üyelerden silinecektir - bu geri alınamaz!"; + +/* No comment provided by engineer. */ +"Group will be deleted for you - this cannot be undone!" = "Grup senden silinecektir - bu geri alınamaz!"; + +/* No comment provided by engineer. */ +"Help" = "Yardım"; + +/* No comment provided by engineer. */ +"Hidden" = "Gizlenmiş"; + +/* No comment provided by engineer. */ +"Hidden chat profiles" = "Gizlenmiş sohbet profilleri"; + +/* No comment provided by engineer. */ +"Hidden profile password" = "Gizlenmiş profil şifresi"; + +/* chat item action */ +"Hide" = "Gizle"; + +/* No comment provided by engineer. */ +"Hide app screen in the recent apps." = "Son uygulamalarda uygulama ekranını gizle."; + +/* No comment provided by engineer. */ +"Hide profile" = "Profili gizle"; + +/* No comment provided by engineer. */ +"Hide:" = "Gizle:"; + +/* No comment provided by engineer. */ +"History" = "Geçmiş"; + +/* No comment provided by engineer. */ +"History is not sent to new members." = "Yeni üyelere geçmiş gönderilmedi."; + +/* time unit */ +"hours" = "saat"; + +/* No comment provided by engineer. */ +"How it affects privacy" = "Gizliliğinizi nasıl etkiler"; + +/* No comment provided by engineer. */ +"How it helps privacy" = "Gizliliğinizi nasıl arttırır"; + +/* No comment provided by engineer. */ +"How SimpleX works" = "SimpleX nasıl çalışır"; + +/* No comment provided by engineer. */ +"How to" = "Nasıl yapılır"; + +/* No comment provided by engineer. */ +"How to use it" = "Nasıl kullanılır"; + +/* No comment provided by engineer. */ +"How to use your servers" = "Sunucularını nasıl kullanabilirsin"; + +/* No comment provided by engineer. */ +"Hungarian interface" = "Macarca arayüz"; + +/* No comment provided by engineer. */ +"ICE servers (one per line)" = "ICE sunucuları (her satıra bir tane)"; + +/* No comment provided by engineer. */ +"If you can't meet in person, show QR code in a video call, or share the link." = "Eğer onunla buluşamıyorsan görüntülü aramada QR kod göster veya bağlantığı paylaş."; + +/* No comment provided by engineer. */ +"If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Eğer bu şifreyi uygulamayı açarken girersen, bütün uygulama verileri geri dönülemeyen bir şekilde silinecektir!"; + +/* No comment provided by engineer. */ +"If you enter your self-destruct passcode while opening the app:" = "Uygulamayı açarken kendi kendini imha eden şifrenizi girerseniz:"; + +/* 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)." = "Sohbeti şimdi kullanmanız gerekiyorsa aşağıdaki **Daha sonra yap** seçeneğine dokunun (uygulamayı yeniden başlattığınızda veritabanını taşımanız önerilecektir)."; + +/* No comment provided by engineer. */ +"Ignore" = "Yok say"; + +/* No comment provided by engineer. */ +"Image will be received when your contact completes uploading it." = "Kişi yüklemeyi bitirdiğinde fotoğraf alınacaktır."; + +/* No comment provided by engineer. */ +"Image will be received when your contact is online, please wait or check later!" = "Kişi çevrimiçi olduğunda fotoğraf alınacaktır, lütfen bekleyin veya daha sonra kontrol et!"; + +/* 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"; + +/* No comment provided by engineer. */ +"Import chat database?" = "Sohbet veritabanı içe aktarılsın mı?"; + +/* No comment provided by engineer. */ +"Import database" = "Veritabanını içe aktar"; + +/* No comment provided by engineer. */ +"Import failed" = "İçe aktarma başarısız oldu"; + +/* No comment provided by engineer. */ +"Import theme" = "Temayı içe aktar"; + +/* No comment provided by engineer. */ +"Importing archive" = "Arşiv içe aktarılıyor"; + +/* No comment provided by engineer. */ +"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "İyileştirilmiş teslimat, azaltılmış trafik kullanımı.\nDaha fazla iyileştirme yakında geliyor!"; + +/* No comment provided by engineer. */ +"Improved message delivery" = "İyileştirilmiş mesaj iletimi"; + +/* No comment provided by engineer. */ +"Improved privacy and security" = "Geliştirilmiş gizlilik ve güvenlik"; + +/* No comment provided by engineer. */ +"Improved server configuration" = "Geliştirilmiş sunucu yapılandırması"; + +/* No comment provided by engineer. */ +"In order to continue, chat should be stopped." = "Devam etmek için sohbetin durdurulması gerekiyor."; + +/* No comment provided by engineer. */ +"In reply to" = "Cevap olarak"; + +/* No comment provided by engineer. */ +"In-call sounds" = "Arama içi sesler"; + +/* No comment provided by engineer. */ +"inactive" = "inaktif"; + +/* No comment provided by engineer. */ +"Incognito" = "Gizli"; + +/* No comment provided by engineer. */ +"Incognito groups" = "Gizli gruplar"; + +/* No comment provided by engineer. */ +"Incognito mode" = "Gizli mod"; + +/* No comment provided by engineer. */ +"Incognito mode protects your privacy by using a new random profile for each contact." = "Gizli mod her kişiye farklı olarak rasgele profiller kullanarak gizliliğinizi korur."; + +/* chat list item description */ +"incognito via contact address link" = "kişi bağlantı linki aracılığıyla gizli"; + +/* chat list item description */ +"incognito via group link" = "grup bağlantısı aracılığıyla gizli"; + +/* chat list item description */ +"incognito via one-time link" = "tek seferlik bağlantısı aracılığıyla gizli"; + +/* notification */ +"Incoming audio call" = "Gelen sesli arama"; + +/* notification */ +"Incoming call" = "Gelen arama"; + +/* notification */ +"Incoming video call" = "Gelen görüntülü arama"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Uyumsuz veritabanı sürümü"; + +/* No comment provided by engineer. */ +"Incompatible version" = "Uyumsuz sürüm"; + +/* PIN entry */ +"Incorrect passcode" = "Uyumsuz parola"; + +/* No comment provided by engineer. */ +"Incorrect security code!" = "Uyumsuz güvenlik kodu!"; + +/* connection level description */ +"indirect (%d)" = "dolaylı (%d)"; + +/* chat item action */ +"Info" = "Bilgi"; + +/* No comment provided by engineer. */ +"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)"; + +/* No comment provided by engineer. */ +"Instant" = "Anında"; + +/* No comment provided by engineer. */ +"Instant push notifications will be hidden!\n" = "Anlık bildirimler gizlenecek!\n"; + +/* No comment provided by engineer. */ +"Interface" = "Arayüz"; + +/* No comment provided by engineer. */ +"Interface colors" = "Arayüz renkleri"; + +/* invalid chat data */ +"invalid chat" = "geçersi̇z sohbet"; + +/* No comment provided by engineer. */ +"invalid chat data" = "geçersi̇z sohbet verisi"; + +/* No comment provided by engineer. */ +"Invalid connection link" = "Geçersiz bağlanma bağlantısı"; + +/* invalid chat item */ +"invalid data" = "geçersiz veri"; + +/* No comment provided by engineer. */ +"Invalid display name!" = "Geçersiz görünen ad!"; + +/* No comment provided by engineer. */ +"Invalid link" = "Geçersiz bağlantı"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Geçersiz taşıma onayı"; + +/* No comment provided by engineer. */ +"Invalid name!" = "Geçersiz isim!"; + +/* No comment provided by engineer. */ +"Invalid QR code" = "Geçersiz QR kodu"; + +/* No comment provided by engineer. */ +"Invalid response" = "Geçersiz yanıt"; + +/* alert title */ +"Invalid server address!" = "Geçersiz sunucu adresi!"; + +/* item status text */ +"Invalid status" = "Geçersiz durum"; + +/* No comment provided by engineer. */ +"Invitation expired!" = "Davetin süresi geçti!"; + +/* group name */ +"invitation to group %@" = "%@ grubuna davet"; + +/* No comment provided by engineer. */ +"invite" = "davet"; + +/* No comment provided by engineer. */ +"Invite friends" = "Arkadaşları davet et"; + +/* No comment provided by engineer. */ +"Invite members" = "Üyeleri davet et"; + +/* No comment provided by engineer. */ +"Invite to chat" = "Sohbete davet et"; + +/* No comment provided by engineer. */ +"Invite to group" = "Gruba davet et"; + +/* No comment provided by engineer. */ +"invited" = "davet edildi"; + +/* rcv group event chat item */ +"invited %@" = "%@ davet edildi"; + +/* chat list item title */ +"invited to connect" = "bağlanmaya davet edildi"; + +/* rcv group event chat item */ +"invited via your group link" = "grup bağlantınız üzerinden davet edildi"; + +/* No comment provided by engineer. */ +"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "iOS Anahtar Zinciri parolayı güvenli bir şekilde saklamak için kullanılır - anlık bildirimlerin alınmasını sağlar."; + +/* 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 Anahtar Zinciri, uygulamayı yeniden başlattıktan veya parolayı değiştirdikten sonra parolayı güvenli bir şekilde saklamak için kullanılacaktır - anlık bildirimlerin alınmasına izin verecektir."; + +/* No comment provided by engineer. */ +"IP address" = "IP adresi"; + +/* No comment provided by engineer. */ +"Irreversible message deletion" = "Geri dönülemeyen mesaj silimi"; + +/* No comment provided by engineer. */ +"Irreversible message deletion is prohibited in this chat." = "Bu sohbette geri döndürülemez mesaj silme yasaktır."; + +/* No comment provided by engineer. */ +"Irreversible message deletion is prohibited." = "Bu grupta geri döndürülemez mesaj silme yasaktır."; + +/* No comment provided by engineer. */ +"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Tek bir sohbet profilinde aralarında herhangi bir veri paylaşımı olmadan birçok anonim bağlantıya sahip olmaya izin verir."; + +/* No comment provided by engineer. */ +"It can happen when you or your connection used the old database backup." = "Siz veya bağlantınız eski veritabanı yedeğini kullandığında bu durum ortaya çıkabilir."; + +/* No comment provided by engineer. */ +"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Şu durumlarda ortaya çıkabilir:\n1. Mesajların gönderici istemcide 2 gün sonra veya sunucuda 30 gün sonra süresi dolmuştur.\n2. Siz veya kişi eski veritabanı yedeği kullandığı için mesaj şifre çözme işlemi başarısız olmuştur.\n3. Bağlantı tehlikeye girmiştir."; + +/* No comment provided by engineer. */ +"It protects your IP address and connections." = "IP adresinizi ve bağlantılarınızı korur."; + +/* 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 (%@)." = "Bu bağlantı üzerinden zaten bağlanmışsınız gibi görünüyor. Eğer durum böyle değilse, bir hata oluştu (%@)."; + +/* No comment provided by engineer. */ +"Italian interface" = "İtalyanca arayüz"; + +/* No comment provided by engineer. */ +"italic" = "italik"; + +/* No comment provided by engineer. */ +"Japanese interface" = "Japonca arayüz"; + +/* swipe action */ +"Join" = "Katıl"; + +/* No comment provided by engineer. */ +"join as %@" = "%@ olarak katıl"; + +/* No comment provided by engineer. */ +"Join group" = "Gruba katıl"; + +/* No comment provided by engineer. */ +"Join group conversations" = "Grup sohbetlerine katıl"; + +/* No comment provided by engineer. */ +"Join group?" = "Gruba katılınsın mı?"; + +/* No comment provided by engineer. */ +"Join incognito" = "Gizli katıl"; + +/* No comment provided by engineer. */ +"Join with current profile" = "Şu anki profille katıl"; + +/* No comment provided by engineer. */ +"Join your group?\nThis is your link for group %@!" = "Bu gruba katılınsın mı?\nBu senin grup için bağlantın %@!"; + +/* No comment provided by engineer. */ +"Joining group" = "Gruba katılınıyor"; + +/* alert action */ +"Keep" = "Tut"; + +/* No comment provided by engineer. */ +"Keep conversation" = "Sohbeti sakla"; + +/* No comment provided by engineer. */ +"Keep the app open to use it from desktop" = "Bilgisayardan kullanmak için uygulamayı açık tut"; + +/* alert title */ +"Keep unused invitation?" = "Kullanılmamış davet tutulsun mu?"; + +/* No comment provided by engineer. */ +"Keep your connections" = "Bağlantılarınızı koruyun"; + +/* No comment provided by engineer. */ +"Keychain error" = "Anahtar Zinciri hatası"; + +/* No comment provided by engineer. */ +"KeyChain error" = "Anahtar Zinciri hatası"; + +/* No comment provided by engineer. */ +"Large file!" = "Büyük dosya!"; + +/* No comment provided by engineer. */ +"Learn more" = "Daha fazlası"; + +/* swipe action */ +"Leave" = "Ayrıl"; + +/* No comment provided by engineer. */ +"Leave chat" = "Sohbetten ayrıl"; + +/* No comment provided by engineer. */ +"Leave chat?" = "Sohbetten ayrılsın mı?"; + +/* No comment provided by engineer. */ +"Leave group" = "Gruptan ayrıl"; + +/* No comment provided by engineer. */ +"Leave group?" = "Gruptan çıkılsın mı?"; + +/* rcv group event chat item */ +"left" = "ayrıldı"; + +/* email subject */ +"Let's talk in SimpleX Chat" = "Hadi SimpleX Chat'te konuşalım"; + +/* No comment provided by engineer. */ +"Light" = "Açık"; + +/* No comment provided by engineer. */ +"Limitations" = "Sınırlamalar"; + +/* No comment provided by engineer. */ +"Link mobile and desktop apps! 🔗" = "Telefon ve bilgisayar uygulamalarını bağla! 🔗"; + +/* No comment provided by engineer. */ +"Linked desktop options" = "Bağlanmış bilgisayar ayarları"; + +/* No comment provided by engineer. */ +"Linked desktops" = "Bağlanmış bilgisayarlar"; + +/* No comment provided by engineer. */ +"LIVE" = "CANLI"; + +/* No comment provided by engineer. */ +"Live message!" = "Canlı mesaj!"; + +/* No comment provided by engineer. */ +"Live messages" = "Canlı mesajlar"; + +/* No comment provided by engineer. */ +"Local name" = "Yerel isim"; + +/* No comment provided by engineer. */ +"Local profile data only" = "Sadece yerel profil verisi"; + +/* No comment provided by engineer. */ +"Lock after" = "Sonra kilitle"; + +/* No comment provided by engineer. */ +"Lock mode" = "Kilit modu"; + +/* No comment provided by engineer. */ +"Make one message disappear" = "Bir mesajın kaybolmasını sağlayın"; + +/* No comment provided by engineer. */ +"Make profile private!" = "Profili gizli yap!"; + +/* No comment provided by engineer. */ +"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "WebRTC ICE sunucu adreslerinin doğru formatta olduğundan, satırlara ayrıldığından ve yinelenmediğinden emin olun."; + +/* No comment provided by engineer. */ +"Mark deleted for everyone" = "Herkes için silinmiş olarak işaretle"; + +/* No comment provided by engineer. */ +"Mark read" = "Okunmuş olarak işaretle"; + +/* No comment provided by engineer. */ +"Mark verified" = "Onaylanmış olarak işaretle"; + +/* No comment provided by engineer. */ +"Markdown in messages" = "Mesajlarda işaretleme"; + +/* marked deleted chat item preview text */ +"marked deleted" = "silinmiş olarak işaretlenmiş"; + +/* No comment provided by engineer. */ +"Max 30 seconds, received instantly." = "Maksimum 30 saniye, anında alındı."; + +/* No comment provided by engineer. */ +"Media & file servers" = "Medya ve dosya sunucuları"; + +/* blur media */ +"Medium" = "Orta"; + +/* member role */ +"member" = "üye"; + +/* No comment provided by engineer. */ +"Member" = "Kişi"; + +/* profile update event chat item */ +"member %@ changed to %@" = "kişi %1$@ , %2$@ olarak değişti"; + +/* rcv group event chat item */ +"member connected" = "bağlanıldı"; + +/* item status text */ +"Member inactive" = "Üye inaktif"; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All chat members will be notified." = "Üye rolü \"%@\" olarak değiştirilecektir. Tüm sohbet üyeleri bilgilendirilecektir."; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All group members will be notified." = "Üye rolü \"%@\" olarak değiştirilecektir. Ve tüm grup üyeleri bilgilendirilecektir."; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". The member will receive a new invitation." = "Üye rolü \"%@\" olarak değiştirilecektir. Ve üye yeni bir davetiye alacaktır."; + +/* No comment provided by engineer. */ +"Member will be removed from group - this cannot be undone!" = "Üye gruptan çıkarılacaktır - bu geri alınamaz!"; + +/* No comment provided by engineer. */ +"Members can add message reactions." = "Grup üyeleri mesaj tepkileri ekleyebilir."; + +/* 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)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Grup üyeleri doğrudan mesajlar gönderebilir."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Grup üyeleri kaybolan mesajlar gönderebilir."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Grup üyeleri dosyalar ve medya gönderebilir."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "Grup üyeleri SimpleX bağlantıları gönderebilir."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Grup üyeleri sesli mesajlar gönderebilir."; + +/* No comment provided by engineer. */ +"Menus" = "Menüler"; + +/* No comment provided by engineer. */ +"message" = "mesaj"; + +/* item status text */ +"Message delivery error" = "Mesaj gönderim hatası"; + +/* No comment provided by engineer. */ +"Message delivery receipts!" = "Mesaj alındı bilgisi!"; + +/* item status text */ +"Message delivery warning" = "Mesaj iletimi uyarısı"; + +/* No comment provided by engineer. */ +"Message draft" = "Mesaj taslağı"; + +/* item status text */ +"Message forwarded" = "Mesaj iletildi"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Kullanıcı aktif olursa mesaj iletilebilir."; + +/* No comment provided by engineer. */ +"Message queue info" = "Mesaj kuyruğu bilgisi"; + +/* chat feature */ +"Message reactions" = "Mesaj tepkileri"; + +/* No comment provided by engineer. */ +"Message reactions are prohibited in this chat." = "Mesaj tepkileri bu sohbette yasaklandı."; + +/* No comment provided by engineer. */ +"Message reactions are prohibited." = "Mesaj tepkileri bu grupta yasaklandı."; + +/* notification */ +"message received" = "mesaj alındı"; + +/* No comment provided by engineer. */ +"Message reception" = "Mesaj alındısı"; + +/* No comment provided by engineer. */ +"Message servers" = "Mesaj sunucuları"; + +/* No comment provided by engineer. */ +"Message shape" = "Mesaj şekli"; + +/* No comment provided by engineer. */ +"Message source remains private." = "Mesaj kaynağı gizli kalır."; + +/* No comment provided by engineer. */ +"Message status" = "Mesaj durumu"; + +/* copied message info */ +"Message status: %@" = "Mesaj durumu: %@"; + +/* No comment provided by engineer. */ +"Message text" = "Mesaj yazısı"; + +/* No comment provided by engineer. */ +"Message too large" = "Mesaj çok büyük"; + +/* No comment provided by engineer. */ +"Messages" = "Mesajlar"; + +/* No comment provided by engineer. */ +"Messages & files" = "Mesajlar & dosyalar"; + +/* No comment provided by engineer. */ +"Messages from %@ will be shown!" = "%@ den gelen mesajlar gösterilecektir!"; + +/* No comment provided by engineer. */ +"Messages received" = "Mesajlar alındı"; + +/* No comment provided by engineer. */ +"Messages sent" = "Mesajlar gönderildi"; + +/* alert message */ +"Messages were deleted after you selected them." = "Mesajlar siz seçtikten sonra silindi."; + +/* 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." = "Mesajlar, dosyalar ve aramalar **uçtan uca şifreleme** ile mükemmel ileri gizlilik, inkar ve izinsiz giriş kurtarma ile korunur."; + +/* 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." = "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 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"; + +/* No comment provided by engineer. */ +"Migrate to another device" = "Başka bir cihaza taşıma"; + +/* No comment provided by engineer. */ +"Migrate to another device via QR code." = "QR kodu aracılığıyla başka bir cihaza geçiş yapın."; + +/* No comment provided by engineer. */ +"Migrating" = "Göçmenlik"; + +/* No comment provided by engineer. */ +"Migrating database archive…" = "Veritabanı arşivine geçiliyor…"; + +/* No comment provided by engineer. */ +"Migration complete" = "Geçiş tamamlandı"; + +/* No comment provided by engineer. */ +"Migration error:" = "Geçiş hatası:"; + +/* No comment provided by engineer. */ +"Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." = "Geçiş başarısız oldu. Alttaki **Geç** tuşuna basarak şu anki veritabanını kullanabilirsiniz. Lütfen sorunu sohbet veya e-posta yoluyla uygulama geliştiricilerine bildirin[chat@simplex.chat](mailto:chat@simplex.chat)."; + +/* No comment provided by engineer. */ +"Migration is completed" = "Geçiş tamamlandı"; + +/* No comment provided by engineer. */ +"Migrations:" = "Geçişler:"; + +/* time unit */ +"minutes" = "dakikalar"; + +/* call status */ +"missed call" = "cevapsız arama"; + +/* chat item action */ +"Moderate" = "Yönet"; + +/* moderated chat item */ +"moderated" = "yönetildi"; + +/* No comment provided by engineer. */ +"Moderated at" = "de yönetildi"; + +/* copied message info */ +"Moderated at: %@" = "%@ de yönetildi"; + +/* marked deleted chat item preview text */ +"moderated by %@" = "%@ tarafından yönetilmekte"; + +/* time unit */ +"months" = "aylar"; + +/* No comment provided by engineer. */ +"More improvements are coming soon!" = "Daha fazla geliştirmeler yakında geliyor!"; + +/* No comment provided by engineer. */ +"More reliable network connection." = "Daha güvenilir ağ bağlantısı."; + +/* item status description */ +"Most likely this connection is deleted." = "Büyük ihtimalle bu bağlantı silinmiş."; + +/* No comment provided by engineer. */ +"Multiple chat profiles" = "Çoklu sohbet profili"; + +/* notification label action */ +"Mute" = "Sustur"; + +/* No comment provided by engineer. */ +"Muted when inactive!" = "Aktif değilken susturuldu!"; + +/* swipe action */ +"Name" = "İsim"; + +/* No comment provided by engineer. */ +"Network & servers" = "Ağ & sunucular"; + +/* No comment provided by engineer. */ +"Network connection" = "Ağ bağlantısı"; + +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "Ağ sorunları - birçok gönderme denemesinden sonra mesajın süresi doldu."; + +/* No comment provided by engineer. */ +"Network management" = "Ağ yönetimi"; + +/* No comment provided by engineer. */ +"Network settings" = "Ağ ayarları"; + +/* No comment provided by engineer. */ +"Network status" = "Ağ durumu"; + +/* delete after time */ +"never" = "asla"; + +/* No comment provided by engineer. */ +"New chat" = "Yeni sohbet"; + +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Yeni bir sohbet deneyimi 🎉"; + +/* notification */ +"New contact request" = "Yeni bağlantı isteği"; + +/* notification */ +"New contact:" = "Yeni kişi:"; + +/* No comment provided by engineer. */ +"New desktop app!" = "Yeni bilgisayar uygulaması!"; + +/* No comment provided by engineer. */ +"New display name" = "Yeni görünen ad"; + +/* No comment provided by engineer. */ +"New in %@" = "%@ da yeni"; + +/* No comment provided by engineer. */ +"New media options" = "Yeni medya seçenekleri"; + +/* No comment provided by engineer. */ +"New member role" = "Yeni üye rolü"; + +/* notification */ +"new message" = "yeni mesaj"; + +/* notification */ +"New message" = "Yeni mesaj"; + +/* No comment provided by engineer. */ +"New Passcode" = "Yeni şifre"; + +/* No comment provided by engineer. */ +"New passphrase…" = "Yeni parola…"; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used every time you start the app." = "Uygulamayı her başlattığınızda yeni SOCKS kimlik bilgileri kullanılacaktır."; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used for each server." = "Her sunucu için yeni SOCKS kimlik bilgileri kullanılacaktır."; + +/* pref value */ +"no" = "hayır"; + +/* No comment provided by engineer. */ +"No" = "Hayır"; + +/* Authentication unavailable */ +"No app password" = "Uygulama şifresi yok"; + +/* No comment provided by engineer. */ +"No contacts selected" = "Hiçbir kişi seçilmedi"; + +/* No comment provided by engineer. */ +"No contacts to add" = "Eklenecek kişi yok"; + +/* No comment provided by engineer. */ +"No delivery information" = "Gönderim bilgisi yok"; + +/* No comment provided by engineer. */ +"No device token!" = "Cihaz tokeni yok!"; + +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Henüz direkt bağlantı yok mesaj admin tarafından yönlendirildi."; + +/* No comment provided by engineer. */ +"no e2e encryption" = "uçtan uca şifreleme yok"; + +/* No comment provided by engineer. */ +"No filtered chats" = "Filtrelenmiş sohbetler yok"; + +/* No comment provided by engineer. */ +"No group!" = "Grup bulunamadı!"; + +/* No comment provided by engineer. */ +"No history" = "Geçmiş yok"; + +/* No comment provided by engineer. */ +"No info, try to reload" = "Bilgi yok, yenilemeyi deneyin"; + +/* No comment provided by engineer. */ +"No network connection" = "Ağ bağlantısı yok"; + +/* No comment provided by engineer. */ +"No permission to record speech" = "Konuşma kaydetme izni yok"; + +/* No comment provided by engineer. */ +"No permission to record video" = "Video kaydı için izin yok"; + +/* No comment provided by engineer. */ +"No permission to record voice message" = "Sesli mesaj kaydetmek için izin yok"; + +/* No comment provided by engineer. */ +"No push server" = "Yerel"; + +/* No comment provided by engineer. */ +"No received or sent files" = "Hiç alınmış veya gönderilmiş dosya yok"; + +/* copied message info in history */ +"no text" = "metin 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!"; + +/* No comment provided by engineer. */ +"Nothing selected" = "Hiçbir şey seçilmedi"; + +/* alert title */ +"Nothing to forward!" = "Yönlendirilecek bir şey yok!"; + +/* No comment provided by engineer. */ +"Notifications" = "Bildirimler"; + +/* No comment provided by engineer. */ +"Notifications are disabled!" = "Bildirimler devre dışı!"; + +/* No comment provided by engineer. */ +"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Şimdi yöneticiler:\n- üyelerin mesajlarını silebilir\n- üyeleri devre dışı bırakabilir (\"gözlemci\" rolü)"; + +/* member role */ +"observer" = "gözlemci"; + +/* enabled status +group pref value +time to disappear */ +"off" = "kapalı"; + +/* blur media */ +"Off" = "Kapalı"; + +/* feature offered item */ +"offered %@" = "%@ teklif edildi"; + +/* feature offered item */ +"offered %@: %@" = "%1$@: %2$@ teklif etti"; + +/* alert button */ +"Ok" = "Tamam"; + +/* No comment provided by engineer. */ +"OK" = "TAMAM"; + +/* No comment provided by engineer. */ +"Old database" = "Eski veritabanı"; + +/* group pref value */ +"on" = "açık"; + +/* No comment provided by engineer. */ +"One-time invitation link" = "Tek zamanlı bağlantı daveti"; + +/* No comment provided by engineer. */ +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Bağlantı için Onion ana bilgisayarları gerekecektir.\nVPN'nin etkinleştirilmesi gerekir."; + +/* No comment provided by engineer. */ +"Onion hosts will be used when available.\nRequires compatible VPN." = "Onion ana bilgisayarları mevcutsa kullanılacaktır.\nVPN'nin etkinleştirilmesi gerekir."; + +/* No comment provided by engineer. */ +"Onion hosts will not be used." = "Onion ana bilgisayarları kullanılmayacaktır."; + +/* No comment provided by engineer. */ +"Only client devices store user profiles, contacts, groups, and messages." = "Yalnızca istemci cihazlar kullanıcı profillerini, kişileri, grupları ve **2 katmanlı uçtan uca şifreleme** ile gönderilen mesajları depolar."; + +/* No comment provided by engineer. */ +"Only delete conversation" = "Sadece sohbeti sil"; + +/* No comment provided by engineer. */ +"Only group owners can change group preferences." = "Grup tercihlerini yalnızca grup sahipleri değiştirebilir."; + +/* No comment provided by engineer. */ +"Only group owners can enable files and media." = "Yalnızca grup sahipleri dosyaları ve medyayı etkinleştirebilir."; + +/* No comment provided by engineer. */ +"Only group owners can enable voice messages." = "Yalnızca grup sahipleri sesli mesajları etkinleştirebilir."; + +/* No comment provided by engineer. */ +"Only you can add message reactions." = "Sadece siz mesaj tepkileri ekleyebilirsiniz."; + +/* No comment provided by engineer. */ +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Mesajları yalnızca siz geri döndürülemez şekilde silebilirsiniz (kişiniz bunları silinmek üzere işaretleyebilir). (24 saat içinde)"; + +/* No comment provided by engineer. */ +"Only you can make calls." = "Sadece sen aramalar yapabilirsin."; + +/* No comment provided by engineer. */ +"Only you can send disappearing messages." = "Sadece sen kaybolan mesajlar gönderebilirsin."; + +/* No comment provided by engineer. */ +"Only you can send voice messages." = "Sadece sen sesli mesajlar gönderebilirsin."; + +/* No comment provided by engineer. */ +"Only your contact can add message reactions." = "Sadece karşıdaki kişi mesaj tepkileri ekleyebilir."; + +/* No comment provided by engineer. */ +"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Yalnızca kişiniz mesajları geri alınamaz şekilde silebilir (silinmeleri için işaretleyebilirsiniz). (24 saat içinde)"; + +/* No comment provided by engineer. */ +"Only your contact can make calls." = "Sadece karşıdaki kişi aramalar yapabilir."; + +/* No comment provided by engineer. */ +"Only your contact can send disappearing messages." = "Sadece karşıdaki kişi kaybolan mesajlar gönderebilir."; + +/* No comment provided by engineer. */ +"Only your contact can send voice messages." = "Sadece karşıdaki kişi sesli mesajlar gönderebilir."; + +/* alert action */ +"Open" = "Aç"; + +/* No comment provided by engineer. */ +"Open chat" = "Sohbeti aç"; + +/* authentication reason */ +"Open chat console" = "Sohbet konsolunu aç"; + +/* No comment provided by engineer. */ +"Open group" = "Grubu aç"; + +/* authentication reason */ +"Open migration to another device" = "Başka bir cihaza açık geçiş"; + +/* No comment provided by engineer. */ +"Open Settings" = "Ayarları aç"; + +/* No comment provided by engineer. */ +"Opening app…" = "Uygulama açılıyor…"; + +/* No comment provided by engineer. */ +"Or paste archive link" = "Veya arşiv bağlantısını yapıştırın"; + +/* No comment provided by engineer. */ +"Or scan QR code" = "Veya QR kodu okut"; + +/* No comment provided by engineer. */ +"Or securely share this file link" = "Veya bu dosya bağlantısını güvenli bir şekilde paylaşın"; + +/* No comment provided by engineer. */ +"Or show this code" = "Veya bu kodu göster"; + +/* No comment provided by engineer. */ +"other" = "diğer"; + +/* No comment provided by engineer. */ +"Other" = "Diğer"; + +/* No comment provided by engineer. */ +"other errors" = "diğer hatalar"; + +/* alert message */ +"Other file errors:\n%@" = "Diğer dosya hataları:\n%@"; + +/* member role */ +"owner" = "sahip"; + +/* feature role */ +"owners" = "sahipler"; + +/* No comment provided by engineer. */ +"Passcode" = "Şifre"; + +/* No comment provided by engineer. */ +"Passcode changed!" = "Şifre değiştirildi!"; + +/* No comment provided by engineer. */ +"Passcode entry" = "Şifre girişi"; + +/* No comment provided by engineer. */ +"Passcode not changed!" = "Şifre değiştirilmedi!"; + +/* No comment provided by engineer. */ +"Passcode set!" = "Şifre ayarlandı!"; + +/* No comment provided by engineer. */ +"Password" = "Şifre"; + +/* No comment provided by engineer. */ +"Password to show" = "Gösterilecek şifre"; + +/* past/unknown group member */ +"Past member %@" = "Geçmiş üye %@"; + +/* No comment provided by engineer. */ +"Paste desktop address" = "Bilgisayar adresini yapıştır"; + +/* No comment provided by engineer. */ +"Paste image" = "Fotoğraf yapıştır"; + +/* No comment provided by engineer. */ +"Paste link to connect!" = "Bağlanmak için bağlantıyı yapıştır!"; + +/* No comment provided by engineer. */ +"Paste the link you received" = "Aldığın bağlantıyı yapıştır"; + +/* No comment provided by engineer. */ +"peer-to-peer" = "eşler arası"; + +/* No comment provided by engineer. */ +"Pending" = "Bekleniyor"; + +/* No comment provided by engineer. */ +"Periodic" = "Periyodik olarak"; + +/* message decrypt error item */ +"Permanent decryption error" = "Kalıcı şifre çözümü hatası"; + +/* No comment provided by engineer. */ +"Picture-in-picture calls" = "Resim içinde resim aramaları"; + +/* No comment provided by engineer. */ +"PING count" = "PING sayısı"; + +/* No comment provided by engineer. */ +"PING interval" = "PING aralığı"; + +/* No comment provided by engineer. */ +"Play from the chat list." = "Sohbet listesinden oynat."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Lütfen kişinizden çağrılara izin vermesini isteyin."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable sending voice messages." = "Lütfen konuştuğunuz kişiden sesli mesaj göndermeyi etkinleştirmesini isteyin."; + +/* No comment provided by engineer. */ +"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." = "Lütfen telefonun ve bilgisayarın aynı lokal ağa bağlı olduğundan ve bilgisayar güvenlik duvarının bağlantıya izin verdiğinden emin olun.\nLütfen diğer herhangi bir sorunu geliştiricilerle paylaşın."; + +/* No comment provided by engineer. */ +"Please check that you used the correct link or ask your contact to send you another one." = "Lütfen doğru bağlantıyı kullandığınızı kontrol edin veya kişiden size başka bir bağlantı göndermesini isteyin."; + +/* No comment provided by engineer. */ +"Please check your network connection with %@ and try again." = "Lütfen ağ bağlantınızı %@ ile kontrol edin ve tekrar deneyin."; + +/* No comment provided by engineer. */ +"Please check yours and your contact preferences." = "Lütfen sizinkini ve iletişim tercihlerinizi kontrol edin."; + +/* No comment provided by engineer. */ +"Please confirm that network settings are correct for this device." = "Lütfen bu cihaz için ağ ayarlarının doğru olduğunu onaylayın."; + +/* No comment provided by engineer. */ +"Please contact developers.\nError: %@" = "Lütfen geliştiricilerle irtibata geçin.\nHata: %@"; + +/* No comment provided by engineer. */ +"Please contact group admin." = "Lütfen grup yöneticisiyle irtibata geçin."; + +/* No comment provided by engineer. */ +"Please enter correct current passphrase." = "Lütfen şu anki doğru olan parolayı girin."; + +/* No comment provided by engineer. */ +"Please enter the previous password after restoring database backup. This action can not be undone." = "Veritabanı yedeğini geri yükledikten sonra lütfen önceki şifreyi girin. Bu işlem geri alınamaz."; + +/* No comment provided by engineer. */ +"Please remember or store it securely - there is no way to recover a lost passcode!" = "Lütfen iyi hatırlayın veya güvenli bir şekilde saklayın - kaybolmuş bir parolayı kurtarmanın bir yolu yoktur!"; + +/* No comment provided by engineer. */ +"Please report it to the developers." = "Lütfen geliştiricilere bildirin."; + +/* No comment provided by engineer. */ +"Please restart the app and migrate the database to enable push notifications." = "Lütfen uygulamayı yeniden başlatın ve anlık bildirimleri etkinleştirmek için veritabanını taşıyın."; + +/* No comment provided by engineer. */ +"Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Lütfen parolayı güvenli bir şekilde saklayın, kaybederseniz sohbete ERİŞEMEZSİNİZ."; + +/* No comment provided by engineer. */ +"Please store passphrase securely, you will NOT be able to change it if you lose it." = "Lütfen parolayı güvenli bir şekilde saklayın, kaybederseniz parolayı DEĞİŞTİREMEZSİNİZ."; + +/* No comment provided by engineer. */ +"Polish interface" = "Lehçe arayüz"; + +/* No comment provided by engineer. */ +"Port" = "Port"; + +/* server test error */ +"Possibly, certificate fingerprint in server address is incorrect" = "Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil"; + +/* No comment provided by engineer. */ +"Preserve the last message draft, with attachments." = "Son mesaj taslağını ekleriyle birlikte koru."; + +/* No comment provided by engineer. */ +"Preset server address" = "Ön ayarlı sunucu adresi"; + +/* No comment provided by engineer. */ +"Preview" = "Ön izleme"; + +/* No comment provided by engineer. */ +"Previously connected servers" = "Önceden bağlanılmış sunucular"; + +/* No comment provided by engineer. */ +"Privacy & security" = "Gizlilik & güvenlik"; + +/* No comment provided by engineer. */ +"Privacy redefined" = "Gizlilik yeniden tanımlandı"; + +/* No comment provided by engineer. */ +"Private filenames" = "Gizli dosya adları"; + +/* No comment provided by engineer. */ +"Private message routing" = "Gizli mesaj yönlendirme"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Gizli mesaj yönlendirme 🚀"; + +/* name of notes to self */ +"Private notes" = "Gizli notlar"; + +/* No comment provided by engineer. */ +"Private routing" = "Gizli yönlendirme"; + +/* No comment provided by engineer. */ +"Private routing error" = "Gizli yönlendirme hatası"; + +/* No comment provided by engineer. */ +"Profile and server connections" = "Profil ve sunucu bağlantıları"; + +/* No comment provided by engineer. */ +"Profile image" = "Profil fotoğrafı"; + +/* No comment provided by engineer. */ +"Profile images" = "Profil resimleri"; + +/* No comment provided by engineer. */ +"Profile password" = "Profil parolası"; + +/* 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."; + +/* No comment provided by engineer. */ +"Prohibit irreversible message deletion." = "Geri dönüşsüz mesaj silme işlemini yasakla."; + +/* No comment provided by engineer. */ +"Prohibit message reactions." = "Mesaj tepkisini yasakla."; + +/* No comment provided by engineer. */ +"Prohibit messages reactions." = "Mesajlarda tepkileri yasakla."; + +/* No comment provided by engineer. */ +"Prohibit sending direct messages to members." = "Üyelere doğrudan mesaj göndermeyi yasakla."; + +/* No comment provided by engineer. */ +"Prohibit sending disappearing messages." = "Kaybolan mesajların gönderimini yasakla."; + +/* No comment provided by engineer. */ +"Prohibit sending files and media." = "Dosyalar ve medya gönderimlerini yasakla."; + +/* No comment provided by engineer. */ +"Prohibit sending SimpleX links." = "SimpleX bağlantısı gönderimini yasakla."; + +/* No comment provided by engineer. */ +"Prohibit sending voice messages." = "Sesli mesajların gönderimini yasakla."; + +/* No comment provided by engineer. */ +"Protect app screen" = "Uygulama ekranını koru"; + +/* No comment provided by engineer. */ +"Protect IP address" = "IP adresini koru"; + +/* No comment provided by engineer. */ +"Protect your chat profiles with a password!" = "Bir parolayla birlikte sohbet profillerini koru!"; + +/* No comment provided by engineer. */ +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "IP adresinizi kişileriniz tarafından seçilen mesajlaşma yönlendiricilerinden koruyun.\n*Ağ ve sunucular* ayarlarında etkinleştirin."; + +/* No comment provided by engineer. */ +"Protocol timeout" = "Protokol zaman aşımı"; + +/* No comment provided by engineer. */ +"Protocol timeout per KB" = "KB başına protokol zaman aşımı"; + +/* No comment provided by engineer. */ +"Proxied" = "Proxyli"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Proxy sunucuları"; + +/* No comment provided by engineer. */ +"Proxy requires password" = "Proxy şifre gerektirir"; + +/* No comment provided by engineer. */ +"Push notifications" = "Anında bildirimler"; + +/* No comment provided by engineer. */ +"Push server" = "Push sunucu"; + +/* chat item text */ +"quantum resistant e2e encryption" = "kuantuma dayanıklı e2e şifreleme"; + +/* No comment provided by engineer. */ +"Quantum resistant encryption" = "Kuantum dirençli şifreleme"; + +/* No comment provided by engineer. */ +"Rate the app" = "Uygulamayı değerlendir"; + +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Erişilebilir sohbet araç çubuğu"; + +/* chat item menu */ +"React…" = "Tepki ver…"; + +/* swipe action */ +"Read" = "Oku"; + +/* No comment provided by engineer. */ +"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)."; + +/* 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)."; + +/* No comment provided by engineer. */ +"Receipts are disabled" = "Alıcılar devre dışı bırakıldı"; + +/* No comment provided by engineer. */ +"Receive errors" = "Alım sırasında hata"; + +/* No comment provided by engineer. */ +"received answer…" = "alınan cevap…"; + +/* No comment provided by engineer. */ +"Received at" = "Şuradan alındı"; + +/* copied message info */ +"Received at: %@" = "Şuradan alındı: %@"; + +/* No comment provided by engineer. */ +"received confirmation…" = "onaylama alındı…"; + +/* notification */ +"Received file event" = "Dosya etkinliği alındı"; + +/* message info title */ +"Received message" = "Mesaj alındı"; + +/* No comment provided by engineer. */ +"Received messages" = "Alınan mesajlar"; + +/* No comment provided by engineer. */ +"Received reply" = "Alınan cevap"; + +/* No comment provided by engineer. */ +"Received total" = "Toplam alınan"; + +/* No comment provided by engineer. */ +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Alıcı adresi farklı bir sunucuya değiştirilecektir. Gönderici çevrimiçi olduktan sonra adres değişikliği tamamlanacaktır."; + +/* No comment provided by engineer. */ +"Receiving file will be stopped." = "Dosya alımı durdurulacaktır."; + +/* No comment provided by engineer. */ +"Receiving via" = "Aracılığıyla alınıyor"; + +/* No comment provided by engineer. */ +"Recent history and improved [directory bot](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)." = "Yakın geçmiş ve geliştirilmiş [dizin botu](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)."; + +/* No comment provided by engineer. */ +"Recipient(s) can't see who this message is from." = "Alıcı(lar) bu mesajın kimden geldiğini göremez."; + +/* No comment provided by engineer. */ +"Recipients see updates as you type them." = "Alıcılar yazdığına göre güncellemeleri görecektir."; + +/* No comment provided by engineer. */ +"Reconnect" = "Yeniden bağlan"; + +/* No comment provided by engineer. */ +"Reconnect all connected servers to force message delivery. It uses additional traffic." = "Mesaj teslimini zorlamak için bağlı tüm sunucuları yeniden bağlayın. Ek trafik kullanır."; + +/* No comment provided by engineer. */ +"Reconnect all servers" = "Tüm sunuculara yeniden bağlan"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Tüm sunuculara yeniden bağlansın mı?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Mesajı göndermeye zorlamak için sunucuya yeniden bağlan. Bu ekstra internet kullanır."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Sunucuya yeniden bağlansın mı ?"; + +/* No comment provided by engineer. */ +"Reconnect servers?" = "Sunuculara yeniden bağlanılsın mı?"; + +/* No comment provided by engineer. */ +"Record updated at" = "Kayıt şu zamanda güncellendi"; + +/* copied message info */ +"Record updated at: %@" = "Kayıt şu zamanda güncellendi: %@"; + +/* No comment provided by engineer. */ +"Reduced battery usage" = "Azaltılmış pil kullanımı"; + +/* reject incoming call via notification +swipe action */ +"Reject" = "Reddet"; + +/* No comment provided by engineer. */ +"Reject (sender NOT notified)" = "Reddet (göndericiye bildirim GİTMEYECEKTİR)"; + +/* No comment provided by engineer. */ +"Reject contact request" = "Bağlanma isteğini reddet"; + +/* call status */ +"rejected call" = "geri çevrilmiş çağrı"; + +/* No comment provided by engineer. */ +"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."; + +/* No comment provided by engineer. */ +"Relay server protects your IP address, but it can observe the duration of the call." = "Yönlendirici sunucu IP adresinizi korur, ancak aramanın süresini gözlemleyebilir."; + +/* No comment provided by engineer. */ +"Remove" = "Sil"; + +/* No comment provided by engineer. */ +"Remove archive?" = "Arşiv kaldırılsın mı ?"; + +/* No comment provided by engineer. */ +"Remove image" = "Resmi kaldır"; + +/* No comment provided by engineer. */ +"Remove member" = "Kişiyi sil"; + +/* No comment provided by engineer. */ +"Remove member?" = "Kişi silinsin mi?"; + +/* No comment provided by engineer. */ +"Remove passphrase from keychain?" = "Anahtar Zinciri'ndeki parola silinsin mi?"; + +/* No comment provided by engineer. */ +"removed" = "kaldırıldı"; + +/* rcv group event chat item */ +"removed %@" = "%@ kaldırıldı"; + +/* profile update event chat item */ +"removed contact address" = "kişi adresi silindi"; + +/* profile update event chat item */ +"removed profile picture" = "profil fotoğrafı silindi"; + +/* rcv group event chat item */ +"removed you" = "sen kaldırıldın"; + +/* No comment provided by engineer. */ +"Renegotiate" = "Yeniden müzakere"; + +/* No comment provided by engineer. */ +"Renegotiate encryption" = "Şifrelemeyi yeniden müzakere et"; + +/* No comment provided by engineer. */ +"Renegotiate encryption?" = "Şifreleme yeniden müzakere edilsin mi?"; + +/* No comment provided by engineer. */ +"Repeat connection request?" = "Bağlantı isteği tekrarlansın mı?"; + +/* No comment provided by engineer. */ +"Repeat download" = "Tekrar indir"; + +/* No comment provided by engineer. */ +"Repeat import" = "İthalatı tekrarla"; + +/* No comment provided by engineer. */ +"Repeat join request?" = "Katılma isteği tekrarlansın mı?"; + +/* No comment provided by engineer. */ +"Repeat upload" = "Yüklemeyi tekrarla"; + +/* chat item action */ +"Reply" = "Yanıtla"; + +/* No comment provided by engineer. */ +"Required" = "Gerekli"; + +/* No comment provided by engineer. */ +"Reset" = "Sıfırla"; + +/* No comment provided by engineer. */ +"Reset all hints" = "Tüm ip uçlarını sıfırla"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Tüm istatistikleri sıfırla"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Tüm istatistikler sıfırlansın mı ?"; + +/* No comment provided by engineer. */ +"Reset colors" = "Renkleri sıfırla"; + +/* No comment provided by engineer. */ +"Reset to app theme" = "Uygulama temasına sıfırla"; + +/* No comment provided by engineer. */ +"Reset to defaults" = "Varsayılanlara sıfırla"; + +/* No comment provided by engineer. */ +"Reset to user theme" = "Kullanıcı temasına sıfırla"; + +/* No comment provided by engineer. */ +"Restart the app to create a new chat profile" = "Yeni bir sohbet profili oluşturmak için uygulamayı yeniden başlatın"; + +/* No comment provided by engineer. */ +"Restart the app to use imported chat database" = "İçe aktarılmış sohbet veritabanını kullanmak için uygulamayı yeniden başlatın"; + +/* No comment provided by engineer. */ +"Restore" = "Geri yükle"; + +/* No comment provided by engineer. */ +"Restore database backup" = "Veritabanı yedeğini geri yükle"; + +/* No comment provided by engineer. */ +"Restore database backup?" = "Veritabanı yedeği geri yüklensin mi?"; + +/* No comment provided by engineer. */ +"Restore database error" = "Veritabanını geri yüklerken hata oluştu"; + +/* No comment provided by engineer. */ +"Retry" = "Yeniden dene"; + +/* chat item action */ +"Reveal" = "Göster"; + +/* No comment provided by engineer. */ +"Revoke" = "İptal et"; + +/* cancel file action */ +"Revoke file" = "Dosyayı iptal et"; + +/* No comment provided by engineer. */ +"Revoke file?" = "Dosya iptal edilsin mi?"; + +/* No comment provided by engineer. */ +"Role" = "Rol"; + +/* No comment provided by engineer. */ +"Run chat" = "Sohbeti çalıştır"; + +/* No comment provided by engineer. */ +"Safely receive files" = "Dosyaları güvenle alın"; + +/* No comment provided by engineer. */ +"Safer groups" = "Daha güvenli gruplar"; + +/* alert button +chat item action */ +"Save" = "Kaydet"; + +/* alert button */ +"Save (and notify contacts)" = "Kaydet (ve kişilere bildir)"; + +/* alert button */ +"Save and notify contact" = "Kaydet ve kişilere bildir"; + +/* No comment provided by engineer. */ +"Save and notify group members" = "Kaydet ve grup üyelerine bildir"; + +/* No comment provided by engineer. */ +"Save and reconnect" = "Kayıt et ve yeniden bağlan"; + +/* No comment provided by engineer. */ +"Save and update group profile" = "Kaydet ve grup profilini güncelle"; + +/* No comment provided by engineer. */ +"Save group profile" = "Grup profilini kaydet"; + +/* No comment provided by engineer. */ +"Save passphrase and open chat" = "Parolayı kaydet ve sohbeti aç"; + +/* No comment provided by engineer. */ +"Save passphrase in Keychain" = "Parolayı Anahtar Zincirinde kaydet"; + +/* alert title */ +"Save preferences?" = "Tercihler kaydedilsin mi?"; + +/* No comment provided by engineer. */ +"Save profile password" = "Profil şifresini kaydet"; + +/* No comment provided by engineer. */ +"Save servers" = "Sunucuları kaydet"; + +/* alert title */ +"Save servers?" = "Sunucular kaydedilsin mi?"; + +/* No comment provided by engineer. */ +"Save welcome message?" = "Hoşgeldin mesajı kaydedilsin mi?"; + +/* alert title */ +"Save your profile?" = "Profiliniz kaydedilsin mi?"; + +/* No comment provided by engineer. */ +"saved" = "kaydedildi"; + +/* No comment provided by engineer. */ +"Saved" = "Kaydedildi"; + +/* No comment provided by engineer. */ +"Saved from" = "Tarafından kaydedildi"; + +/* No comment provided by engineer. */ +"saved from %@" = "%@ tarafından kaydedildi"; + +/* message info title */ +"Saved message" = "Kaydedilmiş mesaj"; + +/* No comment provided by engineer. */ +"Saved WebRTC ICE servers will be removed" = "Kaydedilmiş WebRTC ICE sunucuları silinecek"; + +/* No comment provided by engineer. */ +"Saving %lld messages" = "%lld mesajlarını kaydet"; + +/* No comment provided by engineer. */ +"Scale" = "Ölçeklendir"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Tara / Bağlantı yapıştır"; + +/* No comment provided by engineer. */ +"Scan code" = "Kod okut"; + +/* No comment provided by engineer. */ +"Scan QR code" = "QR kodu okut"; + +/* No comment provided by engineer. */ +"Scan QR code from desktop" = "Bilgisayardan QR kodu okut"; + +/* No comment provided by engineer. */ +"Scan security code from your contact's app." = "Kişinin uygulamasından güvenlik kodunu okut."; + +/* No comment provided by engineer. */ +"Scan server QR code" = "Sunucu QR kodu okut"; + +/* No comment provided by engineer. */ +"search" = "ara"; + +/* No comment provided by engineer. */ +"Search" = "Ara"; + +/* No comment provided by engineer. */ +"Search bar accepts invitation links." = "Arama çubuğu davet bağlantılarını kabul eder."; + +/* No comment provided by engineer. */ +"Search or paste SimpleX link" = "Ara veya SimpleX bağlantısını yapıştır"; + +/* network option */ +"sec" = "sn"; + +/* No comment provided by engineer. */ +"Secondary" = "İkincil renk"; + +/* time unit */ +"seconds" = "saniye"; + +/* No comment provided by engineer. */ +"secret" = "gizli"; + +/* server test step */ +"Secure queue" = "Sırayı koru"; + +/* No comment provided by engineer. */ +"Secured" = "Güvenli"; + +/* No comment provided by engineer. */ +"Security assessment" = "Güvenlik değerlendirmesi"; + +/* No comment provided by engineer. */ +"Security code" = "Güvenlik kodu"; + +/* chat item text */ +"security code changed" = "güvenlik kodu değiştirildi"; + +/* chat item action */ +"Select" = "Seç"; + +/* No comment provided by engineer. */ +"Select chat profile" = "Sohbet profili seç"; + +/* No comment provided by engineer. */ +"Selected %lld" = "Seçilen %lld"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Seçilen sohbet tercihleri bu mesajı yasakladı."; + +/* No comment provided by engineer. */ +"Self-destruct" = "Kendi kendini imha"; + +/* No comment provided by engineer. */ +"Self-destruct passcode" = "Kendini imha eden şifre"; + +/* No comment provided by engineer. */ +"Self-destruct passcode changed!" = "Kendini imha eden şifre değiştirildi!"; + +/* No comment provided by engineer. */ +"Self-destruct passcode enabled!" = "Kendini imha eden şifre etkinleştirildi!"; + +/* No comment provided by engineer. */ +"Send" = "Gönder"; + +/* No comment provided by engineer. */ +"Send a live message - it will update for the recipient(s) as you type it" = "Bir canlı mesaj gönder - yazışına göre kişiye(lere) kendini günceller"; + +/* No comment provided by engineer. */ +"Send delivery receipts to" = "Görüldü bilgilerini şuraya gönder"; + +/* No comment provided by engineer. */ +"send direct message" = "doğrudan mesaj gönder"; + +/* No comment provided by engineer. */ +"Send direct message to connect" = "Bağlanmak için doğrudan mesaj gönder"; + +/* No comment provided by engineer. */ +"Send disappearing message" = "Kaybolan bir mesaj gönder"; + +/* No comment provided by engineer. */ +"Send errors" = "Gönderme hataları"; + +/* No comment provided by engineer. */ +"Send link previews" = "Bağlantı ön gösterimleri gönder"; + +/* No comment provided by engineer. */ +"Send live message" = "Canlı mesaj gönder"; + +/* No comment provided by engineer. */ +"Send message to enable calls." = "Çağrıları aktif etmek için mesaj gönder."; + +/* No comment provided by engineer. */ +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "IP adresi korumalı olduğunda ve sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin."; + +/* No comment provided by engineer. */ +"Send notifications" = "Bildirimler gönder"; + +/* No comment provided by engineer. */ +"Send questions and ideas" = "Fikirler ve sorular gönderin"; + +/* No comment provided by engineer. */ +"Send receipts" = "Mesajlar gönder"; + +/* No comment provided by engineer. */ +"Send them from gallery or custom keyboards." = "Bunları galeriden veya özel klavyelerden gönder."; + +/* No comment provided by engineer. */ +"Send up to 100 last messages to new members." = "Yeni üyelere 100 adete kadar son mesajları gönderin."; + +/* alert message */ +"Sender cancelled file transfer." = "Gönderici dosya gönderimini iptal etti."; + +/* No comment provided by engineer. */ +"Sender may have deleted the connection request." = "Gönderici bağlantı isteğini silmiş olabilir."; + +/* No comment provided by engineer. */ +"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."; + +/* No comment provided by engineer. */ +"Sending delivery receipts will be enabled for all contacts." = "Tüm kişiler için iletim bilgisi gönderme özelliği etkinleştirilecek."; + +/* No comment provided by engineer. */ +"Sending file will be stopped." = "Dosya gönderimi durdurulacaktır."; + +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld contacts" = "Görüldü bilgisi %lld kişileri için devre dışı bırakıldı"; + +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld groups" = "Görüldü bilgisi %lld grupları için devre dışı bırakıldı"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld contacts" = "Görüldü bilgisi %lld kişileri için etkinleştirildi"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld groups" = "Görüldü bilgisi %lld grupları için etkinleştirildi"; + +/* No comment provided by engineer. */ +"Sending via" = "Aracılığıyla gönderiliyor"; + +/* No comment provided by engineer. */ +"Sent at" = "Şuradan gönderildi"; + +/* copied message info */ +"Sent at: %@" = "Şuradan gönderildi: %@"; + +/* No comment provided by engineer. */ +"Sent directly" = "Direkt gönderildi"; + +/* notification */ +"Sent file event" = "Dosya etkinliği gönderildi"; + +/* message info title */ +"Sent message" = "Mesaj gönderildi"; + +/* No comment provided by engineer. */ +"Sent messages" = "Gönderilen mesajlar"; + +/* No comment provided by engineer. */ +"Sent messages will be deleted after set time." = "Gönderilen mesajlar ayarlanan süreden sonra silinecektir."; + +/* No comment provided by engineer. */ +"Sent reply" = "Gönderilen cevap"; + +/* No comment provided by engineer. */ +"Sent total" = "Gönderilen tüm mesajların toplamı"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Bir proxy aracılığıyla gönderildi"; + +/* No comment provided by engineer. */ +"Server" = "Sunucu"; + +/* No comment provided by engineer. */ +"Server address" = "Sunucu adresi"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "Sunucu adresi ağ ayarlarıyla uyumsuz: %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "Sunucu adresi ağ ayarlarıyla uyumlu değil."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "sunucu kuyruk bilgisi: %1$@\n\nson alınan msj: %2$@"; + +/* server test error */ +"Server requires authorization to create queues, check password" = "Sunucunun sıra oluşturması için yetki gereklidir, şifreyi kontrol edin"; + +/* server test error */ +"Server requires authorization to upload, check password" = "Sunucunun yükleme yapması için yetki gereklidir, şifreyi kontrol edin"; + +/* No comment provided by engineer. */ +"Server test failed!" = "Sunucu testinde hata oluştu!"; + +/* No comment provided by engineer. */ +"Server type" = "Sunucu tipi"; + +/* srv error text */ +"Server version is incompatible with network settings." = "Sunucu sürümü ağ ayarlarıyla uyumlu değil."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "Sunucu sürümü uygulamanızla uyumlu değil: %@."; + +/* No comment provided by engineer. */ +"Servers" = "Sunucular"; + +/* No comment provided by engineer. */ +"Servers info" = "Sunucu bilgileri"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Sunucu istatistikleri sıfırlanacaktır - bu geri alınamaz!"; + +/* No comment provided by engineer. */ +"Session code" = "Oturum kodu"; + +/* No comment provided by engineer. */ +"Set 1 day" = "1 günlüğüne ayarla"; + +/* No comment provided by engineer. */ +"Set contact name…" = "Kişi adı gir…"; + +/* No comment provided by engineer. */ +"Set default theme" = "Varsayılan temaya ayarla"; + +/* No comment provided by engineer. */ +"Set group preferences" = "Grup tercihlerini ayarla"; + +/* No comment provided by engineer. */ +"Set it instead of system authentication." = "Sistem kimlik doğrulaması yerine ayarla."; + +/* profile update event chat item */ +"set new contact address" = "yeni kişi adresi ayarla"; + +/* profile update event chat item */ +"set new profile picture" = "yeni profil fotoğrafı ayarla"; + +/* No comment provided by engineer. */ +"Set passcode" = "Şifre ayarla"; + +/* No comment provided by engineer. */ +"Set passphrase" = "Parolayı ayarla"; + +/* No comment provided by engineer. */ +"Set passphrase to export" = "Dışa aktarmak için parola ayarla"; + +/* No comment provided by engineer. */ +"Set the message shown to new members!" = "Yeni üyeler için gösterilen bir mesaj ayarla!"; + +/* No comment provided by engineer. */ +"Set timeouts for proxy/VPN" = "Vekil/VPN için zaman aşımları ayarla"; + +/* No comment provided by engineer. */ +"Settings" = "Ayarlar"; + +/* alert message */ +"Settings were changed." = "Ayarlar değiştirildi."; + +/* No comment provided by engineer. */ +"Shape profile images" = "Profil resimlerini şekillendir"; + +/* alert action +chat item action */ +"Share" = "Paylaş"; + +/* No comment provided by engineer. */ +"Share 1-time link" = "Tek kullanımlık bağlantıyı paylaş"; + +/* No comment provided by engineer. */ +"Share address" = "Adresi paylaş"; + +/* 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."; + +/* No comment provided by engineer. */ +"Share link" = "Bağlantıyı paylaş"; + +/* No comment provided by engineer. */ +"Share profile" = "Profil paylaş"; + +/* No comment provided by engineer. */ +"Share this 1-time invite link" = "Bu tek kullanımlık bağlantı davetini paylaş"; + +/* 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. */ +"Show → on messages sent via private routing." = "Gizli yönlendirme yoluyla gönderilen mesajlarda → işaretini göster."; + +/* No comment provided by engineer. */ +"Show calls in phone history" = "Telefon geçmişinde aramaları göster"; + +/* No comment provided by engineer. */ +"Show developer options" = "Geliştirici ayarlarını göster"; + +/* No comment provided by engineer. */ +"Show last messages" = "Son mesajları göster"; + +/* No comment provided by engineer. */ +"Show message status" = "Mesaj durumunu göster"; + +/* No comment provided by engineer. */ +"Show percentage" = "Yüzdeyi göster"; + +/* No comment provided by engineer. */ +"Show preview" = "Ön gösterimi göser"; + +/* No comment provided by engineer. */ +"Show QR code" = "QR kodunu göster"; + +/* No comment provided by engineer. */ +"Show:" = "Göster:"; + +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX address" = "SimpleX adresi"; + +/* No comment provided by engineer. */ +"SimpleX Address" = "SimpleX Adresi"; + +/* No comment provided by engineer. */ +"SimpleX Chat security was audited by Trail of Bits." = "SimpleX Chat güvenliği Trails of Bits tarafından denetlenmiştir."; + +/* simplex link type */ +"SimpleX contact address" = "SimpleX kişi adresi"; + +/* notification */ +"SimpleX encrypted message or connection event" = "SimpleX şifrelenmiş mesaj veya bağlantı etkinliği"; + +/* simplex link type */ +"SimpleX group link" = "SimpleX grup bağlantısı"; + +/* chat feature */ +"SimpleX links" = "SimpleX bağlantıları"; + +/* No comment provided by engineer. */ +"SimpleX links are prohibited." = "SimpleX bağlantıları bu grupta yasaklandı."; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "SimpleX bağlantılarına izin verilmiyor"; + +/* No comment provided by engineer. */ +"SimpleX Lock" = "SimpleX Kilidi"; + +/* No comment provided by engineer. */ +"SimpleX Lock mode" = "SimpleX Kilidi modu"; + +/* No comment provided by engineer. */ +"SimpleX Lock not enabled!" = "SimpleX Kilidi etkinleştirilmedi!"; + +/* No comment provided by engineer. */ +"SimpleX Lock turned on" = "SimpleX Kilidi açıldı"; + +/* simplex link type */ +"SimpleX one-time invitation" = "SimpleX tek kullanımlık davet"; + +/* No comment provided by engineer. */ +"SimpleX protocols reviewed by Trail of Bits." = "SimpleX protokolleri Trail of Bits tarafından incelenmiştir."; + +/* No comment provided by engineer. */ +"Simplified incognito mode" = "Basitleştirilmiş gizli mod"; + +/* No comment provided by engineer. */ +"Size" = "Boyut"; + +/* No comment provided by engineer. */ +"Skip" = "Atla"; + +/* No comment provided by engineer. */ +"Skipped messages" = "Atlanmış mesajlar"; + +/* No comment provided by engineer. */ +"Small groups (max 20)" = "Küçük gruplar (en fazla 20 kişi)"; + +/* No comment provided by engineer. */ +"SMP server" = "SMP sunucusu"; + +/* No comment provided by engineer. */ +"SOCKS proxy" = "SOCKS vekili"; + +/* blur media */ +"Soft" = "Yumuşak"; + +/* No comment provided by engineer. */ +"Some app settings were not migrated." = "Bazı uygulama ayarları taşınamadı."; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "Bazı dosya(lar) dışa aktarılmadı:"; + +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import - you may see Chat console for more details." = "İçe aktarma sırasında bazı ölümcül olmayan hatalar oluştu - daha fazla ayrıntı için Sohbet konsoluna bakabilirsiniz."; + +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "İçe aktarma sırasında bazı önemli olmayan hatalar oluştu:"; + +/* notification title */ +"Somebody" = "Biri"; + +/* No comment provided by engineer. */ +"Square, circle, or anything in between." = "Kare,daire, veya aralarında herhangi bir şey."; + +/* chat item text */ +"standard end-to-end encryption" = "standart uçtan uca şifreleme"; + +/* No comment provided by engineer. */ +"Start chat" = "Sohbeti başlat"; + +/* No comment provided by engineer. */ +"Start chat?" = "Sohbet başlatılsın mı?"; + +/* No comment provided by engineer. */ +"Start migration" = "Geçişi başlat"; + +/* No comment provided by engineer. */ +"Starting from %@." = "%@'dan başlayarak."; + +/* No comment provided by engineer. */ +"starting…" = "başlatılıyor…"; + +/* No comment provided by engineer. */ +"Statistics" = "İstatistikler"; + +/* No comment provided by engineer. */ +"Stop" = "Dur"; + +/* No comment provided by engineer. */ +"Stop chat" = "Sohbeti kes"; + +/* No comment provided by engineer. */ +"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Sohbet veritabanını dışa aktarmak, içe aktarmak veya silmek için sohbeti durdurun. Sohbet durdurulduğunda mesaj alamaz ve gönderemezsiniz."; + +/* No comment provided by engineer. */ +"Stop chat?" = "Sohbet durdurulsun mu?"; + +/* cancel file action */ +"Stop file" = "Dosyayı durdur"; + +/* No comment provided by engineer. */ +"Stop receiving file?" = "Dosya alımı durdurulsun mu?"; + +/* No comment provided by engineer. */ +"Stop sending file?" = "Dosya gönderimi durdurulsun mu?"; + +/* alert action */ +"Stop sharing" = "Paylaşmayı durdur"; + +/* alert title */ +"Stop sharing address?" = "Adresi paylaşmak durdurulsun mu?"; + +/* authentication reason */ +"Stop SimpleX" = "SimpleX'i durdur"; + +/* No comment provided by engineer. */ +"Stopping chat" = "Sohbeti durdurma"; + +/* No comment provided by engineer. */ +"strike" = "çizik"; + +/* blur media */ +"Strong" = "Güçlü"; + +/* No comment provided by engineer. */ +"Submit" = "Gönder"; + +/* No comment provided by engineer. */ +"Subscribed" = "Abone olundu"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Abone olurken hata"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Abonelikler göz ardı edildi"; + +/* No comment provided by engineer. */ +"Support SimpleX Chat" = "SimpleX Chat'e destek ol"; + +/* No comment provided by engineer. */ +"Switch audio and video during the call." = "Görüşme sırasında ses ve görüntüyü değiştirin."; + +/* No comment provided by engineer. */ +"Switch chat profile for 1-time invitations." = "Sohbet profilini 1 kerelik davetler için değiştirin."; + +/* No comment provided by engineer. */ +"System" = "Sistem"; + +/* No comment provided by engineer. */ +"System authentication" = "Sistem yetkilendirilmesi"; + +/* No comment provided by engineer. */ +"Tail" = "Konuşma balonu"; + +/* No comment provided by engineer. */ +"Take picture" = "Fotoğraf çek"; + +/* No comment provided by engineer. */ +"Tap button " = "Tuşa bas "; + +/* No comment provided by engineer. */ +"Tap to activate profile." = "Profili etkinleştirmek için tıkla."; + +/* No comment provided by engineer. */ +"Tap to Connect" = "Bağlanmak için Tıkla"; + +/* No comment provided by engineer. */ +"Tap to join" = "Katılmak için tıkla"; + +/* No comment provided by engineer. */ +"Tap to join incognito" = "Gizli katılmak için tıkla"; + +/* No comment provided by engineer. */ +"Tap to paste link" = "Bağlantıyı yapıştırmak için tıkla"; + +/* No comment provided by engineer. */ +"Tap to scan" = "Taramak için tıkla"; + +/* No comment provided by engineer. */ +"TCP connection" = "TCP bağlantısı"; + +/* No comment provided by engineer. */ +"TCP connection timeout" = "TCP bağlantı zaman aşımı"; + +/* No comment provided by engineer. */ +"TCP_KEEPCNT" = "TCP_KEEPCNT"; + +/* No comment provided by engineer. */ +"TCP_KEEPIDLE" = "TCP_BOŞTAKAL"; + +/* No comment provided by engineer. */ +"TCP_KEEPINTVL" = "TCP_TVLDEKAL"; + +/* file error alert title */ +"Temporary file error" = "Geçici dosya hatası"; + +/* server test failure */ +"Test failed at step %@." = "Test %@ adımında başarısız oldu."; + +/* No comment provided by engineer. */ +"Test server" = "Sunucuyu test et"; + +/* No comment provided by engineer. */ +"Test servers" = "Sunucuları test et"; + +/* alert title */ +"Tests failed!" = "Testler başarısız oldu!"; + +/* No comment provided by engineer. */ +"Thank you for installing SimpleX Chat!" = "SimpleX Chat'i indirdiğin için teşekkürler!"; + +/* No comment provided by engineer. */ +"Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Kullanıcılar için teşekkürler - [Weblate aracılığıyla katkıda bulun](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; + +/* No comment provided by engineer. */ +"Thanks to the users – contribute via Weblate!" = "Kullanıcılar için teşekkürler - Weblate aracılığıyla katkıda bulun!"; + +/* No comment provided by engineer. */ +"The app can notify you when you receive messages or contact requests - please open settings to enable." = "Uygulama, mesaj veya iletişim isteği aldığınızda sizi bilgilendirebilir - etkinleştirmek için lütfen ayarları açın."; + +/* 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ç)."; + +/* No comment provided by engineer. */ +"The attempt to change database passphrase was not completed." = "Veritabanı parolasını değiştirme girişimi tamamlanmadı."; + +/* No comment provided by engineer. */ +"The code you scanned is not a SimpleX link QR code." = "Taradığınız kod bir SimpleX bağlantı QR kodu değildir."; + +/* No comment provided by engineer. */ +"The connection you accepted will be cancelled!" = "Bağlantı kabulünüz iptal edilecektir!"; + +/* No comment provided by engineer. */ +"The contact you shared this link with will NOT be able to connect!" = "Bu bağlantıyı paylaştığınız kişi BAĞLANAMAYACAKTIR!"; + +/* No comment provided by engineer. */ +"The created archive is available via app Settings / Database / Old database archive." = "Oluşturulan arşive uygulama üzerinden Ayarlar / Veritabanı / Eski veritabanı arşivi üzerinden erişilebilir."; + +/* 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ı."; + +/* No comment provided by engineer. */ +"The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "Bir sonraki mesajın kimliği yanlış (bir öncekinden az veya aynı).\nBazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir."; + +/* No comment provided by engineer. */ +"The message will be deleted for all members." = "Mesaj tüm üyeler için silinecektir."; + +/* No comment provided by engineer. */ +"The message will be marked as moderated for all members." = "Mesaj tüm üyeler için yönetilmiş olarak işaretlenecektir."; + +/* No comment provided by engineer. */ +"The messages will be deleted for all members." = "Mesajlar tüm üyeler için silinecektir."; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "Mesajlar tüm üyeler için moderasyonlu olarak işaretlenecektir."; + +/* No comment provided by engineer. */ +"The old database was not removed during the migration, it can be deleted." = "Eski veritabanı geçiş sırasında kaldırılmadı, silinebilir."; + +/* No comment provided by engineer. */ +"Your profile is stored on your device and only shared with your contacts." = "Profil sadece kişilerinle paylaşılacak."; + +/* No comment provided by engineer. */ +"The second tick we missed! ✅" = "Özlediğimiz ikinci tik! ✅"; + +/* No comment provided by engineer. */ +"The sender will NOT be notified" = "Gönderene BİLDİRİLMEYECEKTİR"; + +/* No comment provided by engineer. */ +"The servers for new connections of your current chat profile **%@**." = "Mevcut sohbet profilinizin yeni bağlantıları için sunucular **%@**."; + +/* No comment provided by engineer. */ +"The text you pasted is not a SimpleX link." = "Yapıştırdığın metin bir SimpleX bağlantısı değildir."; + +/* No comment provided by engineer. */ +"The uploaded database archive will be permanently removed from the servers." = "Yüklenen veritabanı arşivi sunuculardan kalıcı olarak kaldırılacaktır."; + +/* No comment provided by engineer. */ +"Themes" = "Temalar"; + +/* No comment provided by engineer. */ +"These settings are for your current profile **%@**." = "Bu ayarlar mevcut profiliniz **%@** içindir."; + +/* No comment provided by engineer. */ +"They can be overridden in contact and group settings." = "Bunlar kişi ve grup ayarlarında geçersiz kılınabilir."; + +/* 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." = "Bu işlem geri alınamaz - alınan ve gönderilen tüm dosyalar ve medya silinecektir. Düşük çözünürlüklü resimler kalacaktır."; + +/* 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." = "Bu işlem geri alınamaz - seçilenden daha önce gönderilen ve alınan mesajlar silinecektir. Bu işlem birkaç dakika sürebilir."; + +/* No comment provided by engineer. */ +"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Bu işlem geri alınamaz - profiliniz, kişileriniz, mesajlarınız ve dosyalarınız geri döndürülemez şekilde kaybolacaktır."; + +/* E2EE info chat item */ +"This chat is protected by end-to-end encryption." = "Bu sohbet uçtan uca şifreleme ile korunmaktadır."; + +/* E2EE info chat item */ +"This chat is protected by quantum resistant end-to-end encryption." = "Bu sohbet kuantum dirençli uçtan uca şifreleme ile korunmaktadır."; + +/* notification title */ +"this contact" = "Bu kişi"; + +/* No comment provided by engineer. */ +"This device name" = "Bu cihazın ismi"; + +/* No comment provided by engineer. */ +"This display name is invalid. Please choose another name." = "Bu görünen ad geçersiz. Lütfen başka bir isim seçin."; + +/* No comment provided by engineer. */ +"This group has over %lld members, delivery receipts are not sent." = "Bu grubun %lld den fazla üyesi var,görüldü bilgisi gönderilmedi."; + +/* No comment provided by engineer. */ +"This group no longer exists." = "Bu grup artık mevcut değildir."; + +/* No comment provided by engineer. */ +"This is your own one-time link!" = "Bu senin kendi tek kullanımlık bağlantın!"; + +/* No comment provided by engineer. */ +"This is your own SimpleX address!" = "Bu senin kendi SimpleX adresin!"; + +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Bu bağlantı başka bir mobil cihazda kullanıldı, lütfen masaüstünde yeni bir bağlantı oluşturun."; + +/* No comment provided by engineer. */ +"This setting applies to messages in your current chat profile **%@**." = "Bu ayar, geçerli sohbet profiliniz **%@** deki mesajlara uygulanır."; + +/* No comment provided by engineer. */ +"Title" = "Başlık"; + +/* No comment provided by engineer. */ +"To ask any questions and to receive updates:" = "Soru sormak ve güncellemeleri almak için:"; + +/* No comment provided by engineer. */ +"To connect, your contact can scan QR code or use the link in the app." = "Bağlanmak için, kişi QR kodu okutabilir veya uygulama içinden bağlantıyı kullanabilir."; + +/* No comment provided by engineer. */ +"To hide unwanted messages." = "İstenmeyen mesajları gizlemek için."; + +/* No comment provided by engineer. */ +"To make a new connection" = "Yeni bir bağlantı oluşturmak için"; + +/* No comment provided by engineer. */ +"To protect timezone, image/voice files use UTC." = "Zaman bölgesini korumak için,fotoğraf/ses dosyaları UTC kullanır."; + +/* No comment provided by engineer. */ +"To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Bilgilerinizi korumak için SimpleX Lock özelliğini açın.\nBu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenecektir."; + +/* No comment provided by engineer. */ +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "IP adresinizi korumak için,gizli yönlendirme mesajları iletmek için SMP sunucularınızı kullanır."; + +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Gizliliği korumak için, diğer tüm platformlar gibi kullanıcı kimliği kullanmak yerine, SimpleX mesaj kuyrukları için kişilerinizin her biri için ayrı tanımlayıcılara sahiptir."; + +/* No comment provided by engineer. */ +"To record speech please grant permission to use Microphone." = "Konuşmayı kaydetmek için lütfen Mikrofon kullanma izni verin."; + +/* No comment provided by engineer. */ +"To record video please grant permission to use Camera." = "Video kaydetmek için lütfen Kamera kullanım izni verin."; + +/* No comment provided by engineer. */ +"To record voice message please grant permission to use Microphone." = "Sesli mesaj kaydetmek için lütfen Mikrofon kullanım izni verin."; + +/* No comment provided by engineer. */ +"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Gizli profilinizi ortaya çıkarmak için **Sohbet profilleriniz** sayfasındaki arama alanına tam bir şifre girin."; + +/* No comment provided by engineer. */ +"To support instant push notifications the chat database has to be migrated." = "Anlık anlık bildirimleri desteklemek için sohbet veritabanının taşınması gerekir."; + +/* 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."; + +/* No comment provided by engineer. */ +"Toolbar opacity" = "Araç çubuğu opaklığı"; + +/* No comment provided by engineer. */ +"Total" = "Toplam"; + +/* No comment provided by engineer. */ +"Transport isolation" = "Taşıma izolasyonu"; + +/* No comment provided by engineer. */ +"Transport sessions" = "Taşıma oturumları"; + +/* No comment provided by engineer. */ +"Trying to connect to the server used to receive messages from this contact (error: %@)." = "Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor (hata: %@)."; + +/* No comment provided by engineer. */ +"Trying to connect to the server used to receive messages from this contact." = "Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor."; + +/* No comment provided by engineer. */ +"Turkish interface" = "Türkçe arayüz"; + +/* No comment provided by engineer. */ +"Turn off" = "Kapat"; + +/* No comment provided by engineer. */ +"Turn on" = "Aç"; + +/* No comment provided by engineer. */ +"Unable to record voice message" = "Sesli mesaj kaydedilemedi"; + +/* No comment provided by engineer. */ +"Unblock" = "Engeli kaldır"; + +/* No comment provided by engineer. */ +"Unblock for all" = "Herkes için engeli kaldır"; + +/* No comment provided by engineer. */ +"Unblock member" = "Üyenin engelini kaldır"; + +/* No comment provided by engineer. */ +"Unblock member for all?" = "Üyenin engeli herkes için kaldırılsın mı?"; + +/* No comment provided by engineer. */ +"Unblock member?" = "Üyenin engeli kaldırılsın mı?"; + +/* rcv group event chat item */ +"unblocked %@" = "engeli kaldırıldı %@"; + +/* No comment provided by engineer. */ +"Unexpected migration state" = "Beklenmeyen geçiş durumu"; + +/* swipe action */ +"Unfav." = "Favorilerden çık."; + +/* No comment provided by engineer. */ +"Unhide" = "Gizlemeyi kaldır"; + +/* No comment provided by engineer. */ +"Unhide chat profile" = "Sohbet profilinin gizlemesini kaldır"; + +/* No comment provided by engineer. */ +"Unhide profile" = "Proflin gizlenmesini kaldır"; + +/* No comment provided by engineer. */ +"Unit" = "Birim"; + +/* connection info */ +"unknown" = "bilinmeyen"; + +/* callkit banner */ +"Unknown caller" = "Bilinmeyen arayan"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Bilinmeyen veritabanı hatası: %@"; + +/* No comment provided by engineer. */ +"Unknown error" = "Bilinmeyen hata"; + +/* No comment provided by engineer. */ +"unknown servers" = "bilinmeyen yönlendiriciler"; + +/* alert title */ +"Unknown servers!" = "Bilinmeyen sunucular!"; + +/* No comment provided by engineer. */ +"unknown status" = "bilinmeyen durum"; + +/* No comment provided by engineer. */ +"Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "iOS arama arayüzünü kullanmadığınız sürece, kesintileri önlemek için Rahatsız Etmeyin modunu etkinleştirin."; + +/* 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." = "Kişiniz bağlantıyı silmediyse veya bu bağlantı kullanılmadıysa, bu bir hata olabilir - lütfen bildirin.\nBağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını isteyin ve sabit bir ağ bağlantınız olduğunu kontrol edin."; + +/* No comment provided by engineer. */ +"Unlink" = "Bağlantıyı Kaldır"; + +/* No comment provided by engineer. */ +"Unlink desktop?" = "Bilgisayarla bağlantı kaldırılsın mı?"; + +/* No comment provided by engineer. */ +"Unlock" = "Kilidi aç"; + +/* authentication reason */ +"Unlock app" = "Uygulama kilidini aç"; + +/* notification label action */ +"Unmute" = "Susturmayı kaldır"; + +/* No comment provided by engineer. */ +"unprotected" = "korumasız"; + +/* swipe action */ +"Unread" = "Okunmamış"; + +/* No comment provided by engineer. */ +"Up to 100 last messages are sent to new members." = "Yeni üyelere 100e kadar en son mesajlar gönderildi."; + +/* No comment provided by engineer. */ +"Update" = "Güncelle"; + +/* No comment provided by engineer. */ +"Update database passphrase" = "Veritabanı parolasını güncelle"; + +/* No comment provided by engineer. */ +"Update network settings?" = "Bağlantı ayarları güncellensin mi?"; + +/* No comment provided by engineer. */ +"Update settings?" = "Ayarları güncelleyelim mi?"; + +/* rcv group event chat item */ +"updated group profile" = "grup profili güncellendi"; + +/* profile update event chat item */ +"updated profile" = "güncellenmiş profil"; + +/* No comment provided by engineer. */ +"Updating settings will re-connect the client to all servers." = "Ayarların güncellenmesi, istemciyi tüm sunuculara yeniden bağlayacaktır."; + +/* No comment provided by engineer. */ +"Upgrade and open chat" = "Yükselt ve sohbeti aç"; + +/* No comment provided by engineer. */ +"Upload errors" = "Yükleme hataları"; + +/* No comment provided by engineer. */ +"Upload failed" = "Yükleme başarısız"; + +/* server test step */ +"Upload file" = "Dosya yükle"; + +/* No comment provided by engineer. */ +"Uploaded" = "Yüklendi"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Yüklenen dosyalar"; + +/* No comment provided by engineer. */ +"Uploading archive" = "Arşiv yükleme"; + +/* No comment provided by engineer. */ +"Use .onion hosts" = ".onion ana bilgisayarlarını kullan"; + +/* No comment provided by engineer. */ +"Use chat" = "Sohbeti kullan"; + +/* No comment provided by engineer. */ +"Use current profile" = "Şu anki profili kullan"; + +/* No comment provided by engineer. */ +"Use for new connections" = "Yeni bağlantılar için kullan"; + +/* No comment provided by engineer. */ +"Use from desktop" = "Bilgisayardan kullan"; + +/* No comment provided by engineer. */ +"Use iOS call interface" = "iOS arama arayüzünden kullan"; + +/* No comment provided by engineer. */ +"Use new incognito profile" = "Yeni gizli profilden kullan"; + +/* No comment provided by engineer. */ +"Use only local notifications?" = "Sadece yerel bildirimler kullanılsın mı?"; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "IP adresi korunmadığında bilinmeyen sunucularla gizli yönlendirme kullan."; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "Bilinmeyen sunucularla gizli yönlendirme kullan."; + +/* No comment provided by engineer. */ +"Use server" = "Sunucu kullan"; + +/* No comment provided by engineer. */ +"Use SimpleX Chat servers?" = "SimpleX Chat sunucuları kullanılsın mı?"; + +/* No comment provided by engineer. */ +"Use SOCKS proxy" = "SOCKS vekili kullan"; + +/* No comment provided by engineer. */ +"Use the app while in the call." = "Görüşme sırasında uygulamayı kullanın."; + +/* No comment provided by engineer. */ +"Use the app with one hand." = "Uygulamayı tek elle kullan."; + +/* No comment provided by engineer. */ +"User selection" = "Kullanıcı seçimi"; + +/* No comment provided by engineer. */ +"Username" = "Kullanıcı Adı"; + +/* No comment provided by engineer. */ +"Using SimpleX Chat servers." = "SimpleX Chat sunucuları kullanılıyor."; + +/* No comment provided by engineer. */ +"v%@" = "v%@"; + +/* No comment provided by engineer. */ +"v%@ (%@)" = "v%@ (%@)"; + +/* No comment provided by engineer. */ +"Verify code with desktop" = "Bilgisayarla kodu doğrula"; + +/* No comment provided by engineer. */ +"Verify connection" = "Bağlantıyı doğrula"; + +/* No comment provided by engineer. */ +"Verify connection security" = "Bağlantı güvenliğini doğrula"; + +/* No comment provided by engineer. */ +"Verify connections" = "Bağlantıları doğrula"; + +/* No comment provided by engineer. */ +"Verify database passphrase" = "Veritabanı parolasını doğrulayın"; + +/* No comment provided by engineer. */ +"Verify passphrase" = "Parolayı doğrula"; + +/* No comment provided by engineer. */ +"Verify security code" = "Güvenlik kodunu doğrula"; + +/* No comment provided by engineer. */ +"Via browser" = "Tarayıcı üzerinden"; + +/* chat list item description */ +"via contact address link" = "bağlantı adres uzantısı ile"; + +/* chat list item description */ +"via group link" = "grup bağlantısı ile"; + +/* chat list item description */ +"via one-time link" = "tek kullanımlık bağlantısı ile"; + +/* No comment provided by engineer. */ +"via relay" = "yönlendirici aracılığıyla"; + +/* No comment provided by engineer. */ +"Via secure quantum resistant protocol." = "Güvenli kuantum dirençli protokol ile."; + +/* No comment provided by engineer. */ +"video" = "video"; + +/* No comment provided by engineer. */ +"Video call" = "Görüntülü arama"; + +/* No comment provided by engineer. */ +"video call (not e2e encrypted)" = "Görüntülü arama (şifrelenmiş değil)"; + +/* No comment provided by engineer. */ +"Video will be received when your contact completes uploading it." = "Kişiniz yüklemeyi tamamladığında video alınacaktır."; + +/* No comment provided by engineer. */ +"Video will be received when your contact is online, please wait or check later!" = "Kişiniz çevrimiçi olduğunda video alınacaktır, lütfen bekleyin veya daha sonra kontrol edin!"; + +/* No comment provided by engineer. */ +"Videos and files up to 1gb" = "1gb'a kadar videolar ve dosyalar"; + +/* No comment provided by engineer. */ +"View security code" = "Güvenlik kodunu görüntüle"; + +/* chat feature */ +"Visible history" = "Görünür geçmiş"; + +/* No comment provided by engineer. */ +"Voice message…" = "Sesli mesaj…"; + +/* chat feature */ +"Voice messages" = "Sesli mesajlar"; + +/* No comment provided by engineer. */ +"Voice messages are prohibited in this chat." = "Bu sohbette sesli mesajlar yasaktır."; + +/* No comment provided by engineer. */ +"Voice messages are prohibited." = "Bu grupta sesli mesajlar yasaktır."; + +/* No comment provided by engineer. */ +"Voice messages not allowed" = "Sesli mesajlara izin verilmiyor"; + +/* No comment provided by engineer. */ +"Voice messages prohibited!" = "Sesli mesajlar yasaktır!"; + +/* No comment provided by engineer. */ +"waiting for answer…" = "cevap bekleniyor…"; + +/* No comment provided by engineer. */ +"waiting for confirmation…" = "onay bekleniyor…"; + +/* No comment provided by engineer. */ +"Waiting for desktop..." = "Bilgisayar için bekleniyor..."; + +/* No comment provided by engineer. */ +"Waiting for file" = "Dosya bekleniyor"; + +/* No comment provided by engineer. */ +"Waiting for image" = "Görsel bekleniyor"; + +/* No comment provided by engineer. */ +"Waiting for video" = "Video bekleniyor"; + +/* No comment provided by engineer. */ +"Wallpaper accent" = "Duvar kağıdı vurgusu"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Duvar kağıdı arkaplanı"; + +/* No comment provided by engineer. */ +"wants to connect to you!" = "bağlanmak istiyor!"; + +/* No comment provided by engineer. */ +"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "Uyarı: birden fazla cihazda sohbet başlatmak desteklenmez ve mesaj teslim hatalarına neden olur"; + +/* No comment provided by engineer. */ +"Warning: you may lose some data!" = "Uyarı: Bazı verileri kaybedebilirsin!"; + +/* No comment provided by engineer. */ +"WebRTC ICE servers" = "WebRTC ICE sunucuları"; + +/* time unit */ +"weeks" = "haftalar"; + +/* No comment provided by engineer. */ +"Welcome %@!" = "Hoşgeldin %@!"; + +/* No comment provided by engineer. */ +"Welcome message" = "Karşılama mesajı"; + +/* No comment provided by engineer. */ +"Welcome message is too long" = "Hoş geldiniz mesajı çok uzun"; + +/* No comment provided by engineer. */ +"What's new" = "Neler yeni"; + +/* No comment provided by engineer. */ +"When available" = "Mevcut olduğunda"; + +/* No comment provided by engineer. */ +"When connecting audio and video calls." = "Sesli ve görüntülü aramalara bağlanırken."; + +/* No comment provided by engineer. */ +"when IP hidden" = "IP gizliyken"; + +/* 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." = "Biriyle gizli bir profil paylaştığınızda, bu profil sizi davet ettikleri gruplar için kullanılacaktır."; + +/* No comment provided by engineer. */ +"WiFi" = "WiFi"; + +/* No comment provided by engineer. */ +"Will be enabled in direct chats!" = "Doğrudan sohbetlerde etkinleştirilecektir!"; + +/* No comment provided by engineer. */ +"Wired ethernet" = "Kablolu ethernet"; + +/* No comment provided by engineer. */ +"With encrypted files and media." = "Şifrelenmiş dosyalar ve medya ile birlikte."; + +/* No comment provided by engineer. */ +"With optional welcome message." = "İsteğe bağlı karşılama mesajı ile."; + +/* No comment provided by engineer. */ +"With reduced battery usage." = "Azaltılmış pil kullanımı ile birlikte."; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "Tor veya VPN olmadan, IP adresiniz dosya sunucularına görülebilir."; + +/* alert message */ +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Tor veya VPN olmadan, IP adresiniz bu XFTP aktarıcıları tarafından görülebilir: %@."; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Yanlış veritabanı parolası"; + +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "Yanlış anahtar veya bilinmeyen bağlantı - büyük olasılıkla bu bağlantı silinmiştir."; + +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Yanlış anahtar veya bilinmeyen dosya yığın adresi - büyük olasılıkla dosya silinmiştir."; + +/* No comment provided by engineer. */ +"Wrong passphrase!" = "Yanlış parola!"; + +/* No comment provided by engineer. */ +"XFTP server" = "XFTP sunucusu"; + +/* pref value */ +"yes" = "evet"; + +/* No comment provided by engineer. */ +"you" = "sen"; + +/* No comment provided by engineer. */ +"You **must not** use the same database on two devices." = "Aynı veritabanını iki cihazda **kullanmamalısınız**."; + +/* No comment provided by engineer. */ +"You accepted connection" = "Bağlantıyı onayladın"; + +/* No comment provided by engineer. */ +"You allow" = "İzin veriyorsunuz"; + +/* No comment provided by engineer. */ +"You already have a chat profile with the same display name. Please choose another name." = "Aynı görünen ada sahip bir konuşma profilin zaten var. Lütfen başka bir ad seç."; + +/* No comment provided by engineer. */ +"You are already connected to %@." = "Zaten %@'a bağlısınız."; + +/* No comment provided by engineer. */ +"You are already connecting to %@." = "Zaten %@'a bağlanıyorsunuz."; + +/* No comment provided by engineer. */ +"You are already connecting via this one-time link!" = "Bu tek seferlik bağlantı üzerinden zaten bağlanıyorsunuz!"; + +/* No comment provided by engineer. */ +"You are already in group %@." = "Zaten %@ grubundasın."; + +/* No comment provided by engineer. */ +"You are already joining the group %@." = "Zaten %@ grubuna katılıyorsunuz."; + +/* No comment provided by engineer. */ +"You are already joining the group via this link!" = "Bu bağlantı üzerinden gruba zaten katılıyorsunuz!"; + +/* No comment provided by engineer. */ +"You are already joining the group via this link." = "Gruba zaten bu bağlantı üzerinden katılıyorsunuz."; + +/* No comment provided by engineer. */ +"You are already joining the group!\nRepeat join request?" = "Gruba zaten katılıyorsunuz!\nKatılma isteği tekrarlansın mı?"; + +/* No comment provided by engineer. */ +"You are connected to the server used to receive messages from this contact." = "Bu kişiden mesaj almak için kullanılan sunucuya bağlısınız."; + +/* No comment provided by engineer. */ +"you are invited to group" = "gruba davet edildiniz"; + +/* No comment provided by engineer. */ +"You are invited to group" = "Gruba davet edildiniz"; + +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Bu sunuculara bağlı değilsiniz. Mesajları onlara iletmek için özel yönlendirme kullanılır."; + +/* No comment provided by engineer. */ +"you are observer" = "gözlemcisiniz"; + +/* snd group event chat item */ +"you blocked %@" = "engelledin %@"; + +/* No comment provided by engineer. */ +"You can accept calls from lock screen, without device and app authentication." = "Cihaz ve uygulama kimlik doğrulaması olmadan kilit ekranından çağrı kabul edebilirsiniz."; + +/* No comment provided by engineer. */ +"You can change it in Appearance settings." = "Görünüm ayarlarından değiştirebilirsiniz."; + +/* No comment provided by engineer. */ +"You can create it later" = "Daha sonra oluşturabilirsiniz"; + +/* No comment provided by engineer. */ +"You can enable later via Settings" = "Daha sonra Ayarlardan etkinleştirebilirsin"; + +/* No comment provided by engineer. */ +"You can enable them later via app Privacy & Security settings." = "Daha sonra uygulamanın Gizlilik ve Güvenlik ayarlarından etkinleştirebilirsiniz."; + +/* No comment provided by engineer. */ +"You can give another try." = "Bir kez daha deneyebilirsiniz."; + +/* No comment provided by engineer. */ +"You can hide or mute a user profile - swipe it to the right." = "Bir kullanıcı profilini gizleyebilir veya sessize alabilirsiniz - sağa kaydırın."; + +/* No comment provided by engineer. */ +"You can make it visible to your SimpleX contacts via Settings." = "Ayarlardan SimpleX kişilerinize görünür yapabilirsiniz."; + +/* notification body */ +"You can now chat with %@" = "Artık %@ adresine mesaj gönderebilirsin"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "Arşivlenen kişilerden %@'ya mesaj gönderebilirsiniz."; + +/* No comment provided by engineer. */ +"You can set lock screen notification preview via settings." = "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 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."; + +/* No comment provided by engineer. */ +"You can share this address with your contacts to let them connect with **%@**." = "Bu adresi kişilerinizle paylaşarak onların **%@** ile bağlantı kurmasını sağlayabilirsiniz."; + +/* No comment provided by engineer. */ +"You can start chat via app Settings / Database or by restarting the app" = "Sohbeti uygulamada Ayarlar / Veritabanı üzerinden veya uygulamayı yeniden başlatarak başlatabilirsiniz"; + +/* No comment provided by engineer. */ +"You can still view conversation with %@ in the list of chats." = "Sohbet listesinde %@ ile konuşmayı görüntülemeye devam edebilirsiniz."; + +/* No comment provided by engineer. */ +"You can turn on SimpleX Lock via Settings." = "SimpleX Kilidini Ayarlar üzerinden açabilirsiniz."; + +/* No comment provided by engineer. */ +"You can use markdown to format messages:" = "Mesajları biçimlendirmek için markdown kullanabilirsiniz:"; + +/* alert message */ +"You can view invitation link again in connection details." = "Bağlantı detaylarından davet bağlantısını yeniden görüntüleyebilirsin."; + +/* No comment provided by engineer. */ +"You can't send messages!" = "Mesajlar gönderemezsiniz!"; + +/* chat item text */ +"you changed address" = "adresinizi değiştirdiniz"; + +/* chat item text */ +"you changed address for %@" = "adresinizi %@ ile değiştirdiniz"; + +/* snd group event chat item */ +"you changed role for yourself to %@" = "kişisel yetkinizi %@ olarak değiştirdiniz"; + +/* snd group event chat item */ +"you changed role of %@ to %@" = "%1$@'in yetkisini %2$@ olarak değiştirdiniz"; + +/* 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 via this address!" = "Bu adres üzerinden zaten bağlantı talebinde bulundunuz!"; + +/* No comment provided by engineer. */ +"You have already requested connection!\nRepeat connection request?" = "Zaten bağlantı isteğinde bulundunuz!\nBağlantı isteği tekrarlansın mı?"; + +/* No comment provided by engineer. */ +"You have to enter passphrase every time the app starts - it is not stored on the device." = "Uygulama her başladığında parola girmeniz gerekir - parola cihazınızda saklanmaz."; + +/* No comment provided by engineer. */ +"You invited a contact" = "Bir kişiyi davet ettin"; + +/* No comment provided by engineer. */ +"You joined this group" = "Bu gruba katıldınız"; + +/* No comment provided by engineer. */ +"You joined this group. Connecting to inviting group member." = "Bu gruba katıldınız. Davet eden grup üyesine bağlanılıyor."; + +/* snd group event chat item */ +"you left" = "terk ettiniz"; + +/* No comment provided by engineer. */ +"You may migrate the exported database." = "Dışa aktarılan veritabanını taşıyabilirsiniz."; + +/* No comment provided by engineer. */ +"You may save the exported archive." = "Dışa aktarılan arşivi kaydedebilirsiniz."; + +/* 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." = "Sohbet veritabanınızın en son sürümünü SADECE bir cihazda kullanmalısınız, aksi takdirde bazı kişilerden daha fazla mesaj alamayabilirsiniz."; + +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "Kendiniz arayabilmeniz için önce irtibat kişinizin sizi aramasına izin vermelisiniz."; + +/* No comment provided by engineer. */ +"You need to allow your contact to send voice messages to be able to send them." = "Sesli mesaj gönderebilmeniz için kişinizin de sesli mesaj göndermesine izin vermeniz gerekir."; + +/* No comment provided by engineer. */ +"You rejected group invitation" = "Grup davetini reddettiniz"; + +/* snd group event chat item */ +"you removed %@" = "%@'yi çıkarttınız"; + +/* No comment provided by engineer. */ +"You sent group invitation" = "Grup daveti gönderdiniz"; + +/* chat list item description */ +"you shared one-time link" = "tek kullanımlık bağlantınızı paylaştınız"; + +/* chat list item description */ +"you shared one-time link incognito" = "tek kullanımlık link paylaştınız gizli"; + +/* snd group event chat item */ +"you unblocked %@" = "engelini kaldırdın %@"; + +/* No comment provided by engineer. */ +"You will be connected to group when the group host's device is online, please wait or check later!" = "Grup sahibinin cihazı çevrimiçi olduğunda gruba bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!"; + +/* No comment provided by engineer. */ +"You will be connected when group link host's device is online, please wait or check later!" = "Grup sahibinin cihazı çevrimiçi olduğunda gruba bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!"; + +/* No comment provided by engineer. */ +"You will be connected when your connection request is accepted, please wait or check later!" = "Bağlantı isteğiniz kabul edildiğinde bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!"; + +/* No comment provided by engineer. */ +"You will be connected when your contact's device is online, please wait or check later!" = "Kişinizin cihazı çevrimiçi olduğunda bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!"; + +/* No comment provided by engineer. */ +"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Arka planda 30 saniye kaldıktan sonra uygulamayı başlattığınızda veya devam ettirdiğinizde kimlik doğrulaması yapmanız gerekecektir."; + +/* No comment provided by engineer. */ +"You will connect to all group members." = "Bütün grup üyelerine bağlanacaksın."; + +/* No comment provided by engineer. */ +"You will still receive calls and notifications from muted profiles when they are active." = "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 group. Chat history will be preserved." = "Bu gruptan artık mesaj almayacaksınız. Sohbet geçmişi korunacaktır."; + +/* No comment provided by engineer. */ +"You won't lose your contacts if you later delete your address." = "Eğer sonradan bağlantınızı silseniz bile kişilerinizi kaybetmeyeceksiniz."; + +/* No comment provided by engineer. */ +"you: " = "sen: "; + +/* 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" = "Gizli bir profil paylaştığınız kişiyi ana profilinizi kullandığınız gruba davet etmeye çalışıyorsunuz"; + +/* 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" = "Bu grup için gizli bir profil kullanıyorsunuz - ana profilinizi paylaşmayı önlemek için kişileri davet etmeye izin verilmiyor"; + +/* No comment provided by engineer. */ +"Your calls" = "Aramaların"; + +/* No comment provided by engineer. */ +"Your chat database" = "Sohbet veritabanınız"; + +/* No comment provided by engineer. */ +"Your chat database is not encrypted - set passphrase to encrypt it." = "Sohbet veritabanınız şifrelenmemiş - şifrelemek için parola ayarlayın."; + +/* alert title */ +"Your chat preferences" = "Sohbet tercihleriniz"; + +/* No comment provided by engineer. */ +"Your chat profiles" = "Sohbet profillerin"; + +/* No comment provided by engineer. */ +"Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Bağlantınız %@ adresine taşındı ancak sizi profile yönlendirirken beklenmedik bir hata oluştu."; + +/* No comment provided by engineer. */ +"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Kişiniz şu anda desteklenen maksimum boyuttan (%@) daha büyük bir dosya gönderdi."; + +/* No comment provided by engineer. */ +"Your contacts can allow full message deletion." = "Kişileriniz tam mesaj silme işlemine izin verebilir."; + +/* No comment provided by engineer. */ +"Your contacts will remain connected." = "Kişileriniz bağlı kalacaktır."; + +/* No comment provided by engineer. */ +"Your credentials may be sent unencrypted." = "Kimlik bilgileriniz şifrelenmeden gönderilebilir."; + +/* No comment provided by engineer. */ +"Your current chat database will be DELETED and REPLACED with the imported one." = "Mevcut sohbet veritabanınız SİLİNECEK ve içe aktarılan veritabanıyla DEĞİŞTİRİLECEKTİR."; + +/* No comment provided by engineer. */ +"Your current profile" = "Mevcut profiliniz"; + +/* No comment provided by engineer. */ +"Your ICE servers" = "ICE sunucularınız"; + +/* No comment provided by engineer. */ +"Your preferences" = "Tercihleriniz"; + +/* No comment provided by engineer. */ +"Your privacy" = "Gizliliğiniz"; + +/* No comment provided by engineer. */ +"Your profile" = "Profiliniz"; + +/* No comment provided by engineer. */ +"Your profile **%@** will be shared." = "Profiliniz **%@** paylaşılacaktır."; + +/* No comment provided by engineer. */ +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Profiliniz cihazınızda saklanır ve sadece kişilerinizle paylaşılır. SimpleX sunucuları profilinizi göremez."; + +/* alert message */ +"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Profiliniz değiştirildi. Kaydederseniz, güncellenmiş profil tüm kişilerinize gönderilecektir."; + +/* No comment provided by engineer. */ +"Your profile, contacts and delivered messages are stored on your device." = "Profiliniz, kişileriniz ve gönderilmiş mesajlar cihazınızda saklanır."; + +/* No comment provided by engineer. */ +"Your random profile" = "Rasgele profiliniz"; + +/* No comment provided by engineer. */ +"Your server address" = "Sunucu adresiniz"; + +/* No comment provided by engineer. */ +"Your settings" = "Ayarlarınız"; + +/* No comment provided by engineer. */ +"Your SimpleX address" = "SimpleX adresin"; + diff --git a/apps/ios/tr.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/tr.lproj/SimpleX--iOS--InfoPlist.strings new file mode 100644 index 0000000000..fe0af4cc90 --- /dev/null +++ b/apps/ios/tr.lproj/SimpleX--iOS--InfoPlist.strings @@ -0,0 +1,18 @@ +/* Bundle name */ +"CFBundleName" = "SimpleX"; + +/* Privacy - Camera Usage Description */ +"NSCameraUsageDescription" = "SimpleX, diğer kullanıcılara bağlanmak amacıyla QR kodlarını taramak ve görüntülü aramalar için kamera erişimine ihtiyaç duyar."; + +/* Privacy - Face ID Usage Description */ +"NSFaceIDUsageDescription" = "SimpleX yerel kimlik doğrulama için Face ID kullanır"; + +/* Privacy - Local Network Usage Description */ +"NSLocalNetworkUsageDescription" = "SimpleX, aynı ağdaki masaüstü uygulaması aracılığıyla kullanıcı sohbet profilinin kullanılmasına izin vermek için yerel ağ erişimini kullanır."; + +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "SimpleX'in sesli ve görüntülü aramalar ve sesli mesajları kaydetmek için mikrofon erişimine ihtiyacı vardır."; + +/* Privacy - Photo Library Additions Usage Description */ +"NSPhotoLibraryAddUsageDescription" = "SimpleX'in çekilen ve alınan medyayı kaydetmek için Fotoğraf Kitaplığı'na erişmesi gerekir"; + diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index aabd1a7101..734b8dda82 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -1,47 +1,29 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (можна скопіювати)"; /* No comment provided by engineer. */ "_italic_" = "\\_курсив_"; +/* No comment provided by engineer. */ +"- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." = "- підключитися до [сервера каталогів](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (БЕТА)!\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. */ +"- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- опція сповіщати про видалені контакти.\n- імена профілів з пробілами.\n- та багато іншого!"; + /* No comment provided by engineer. */ "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- голосові повідомлення до 5 хвилин.\n- користувальницький час зникнення.\n- історія редагування."; -/* No comment provided by engineer. */ -", " = ", "; - -/* No comment provided by engineer. */ -": " = ": "; - /* No comment provided by engineer. */ "!1 colored!" = "!1 кольоровий!"; /* No comment provided by engineer. */ -"." = "."; +"(new)" = "(новий)"; /* No comment provided by engineer. */ -"(" = "("; - -/* 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)"; @@ -53,10 +35,10 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Зірка на GitHub](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Додати новий контакт**: щоб створити одноразовий QR-код або посилання для свого контакту."; +"**Create 1-time link**: to create and share a new invitation link." = "**Додати контакт**: створити нове посилання-запрошення."; /* No comment provided by engineer. */ -"**Create link / QR code** for your contact to use." = "**Створіть посилання / QR-код** для використання вашим контактом."; +"**Create group**: to create a new group." = "**Створити групу**: створити нову групу."; /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "**e2e encrypted** аудіодзвінок"; @@ -65,26 +47,29 @@ "**e2e encrypted** video call" = "**e2e encrypted** відеодзвінок"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Більш приватний**: перевіряти нові повідомлення кожні 20 хвилин. Серверу SimpleX Chat передається токен пристрою, але не кількість контактів або повідомлень, які ви маєте."; +"**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." = "**Більш приватний**: перевіряти нові повідомлення кожні 20 хвилин. Серверу SimpleX Chat передається токен пристрою, але не кількість контактів або повідомлень, які ви маєте."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Найбільш приватний**: не використовуйте сервер сповіщень SimpleX Chat, періодично перевіряйте повідомлення у фоновому режимі (залежить від того, як часто ви користуєтесь додатком)."; +"**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." = "**Найбільш приватний**: не використовуйте сервер сповіщень SimpleX Chat, періодично перевіряйте повідомлення у фоновому режимі (залежить від того, як часто ви користуєтесь додатком)."; /* No comment provided by engineer. */ -"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Вставте отримане посилання** або відкрийте його в браузері і натисніть **Відкрити в мобільному додатку**."; +"**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." = "**Зверніть увагу: ви НЕ зможете відновити або змінити пароль, якщо втратите його."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Рекомендується**: токен пристрою та сповіщення надсилаються на сервер сповіщень SimpleX Chat, але не вміст повідомлення, його розмір або від кого воно надійшло."; +"**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, але не вміст повідомлення, його розмір або від кого воно надійшло."; /* No comment provided by engineer. */ -"**Scan QR code**: to connect to your contact in person or via video call." = "**Відскануйте QR-код**: щоб з'єднатися з вашим контактом особисто або за допомогою відеодзвінка."; +"**Scan / Paste link**: to connect via a link you received." = "**Відсканувати / Вставити посилання**: підключитися за отриманим посиланням."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Попередження**: Для отримання миттєвих пуш-сповіщень потрібна парольна фраза, збережена у брелоку."; +/* No comment provided by engineer. */ +"**Warning**: the archive will be removed." = "**Попередження**: архів буде видалено."; + /* No comment provided by engineer. */ "*bold*" = "\\*жирний*"; @@ -115,12 +100,21 @@ /* No comment provided by engineer. */ "%@ %@" = "%@ %@"; +/* No comment provided by engineer. */ +"%@ and %@" = "%@ та %@"; + /* No comment provided by engineer. */ "%@ and %@ connected" = "%@ і %@ підключено"; /* copied message info, <sender> at <time> */ "%@ at %@:" = "%1$@ за %2$@:"; +/* No comment provided by engineer. */ +"%@ connected" = "%@ підключено"; + +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ встановлено"; + /* notification title */ "%@ is connected!" = "%@ підключено!"; @@ -130,12 +124,24 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ перевірено"; +/* No comment provided by engineer. */ +"%@ server" = "%@ сервер"; + /* No comment provided by engineer. */ "%@ servers" = "%@ сервери"; +/* No comment provided by engineer. */ +"%@ uploaded" = "%@ завантажено"; + /* notification title */ "%@ wants to connect!" = "%@ хоче підключитися!"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + +/* No comment provided by engineer. */ +"%@, %@ and %lld members" = "%@, %@ та %lld учасників"; + /* No comment provided by engineer. */ "%@, %@ and %lld other members connected" = "%@, %@ та %lld інші підключені учасники"; @@ -145,9 +151,24 @@ /* time interval */ "%d days" = "%d днів"; +/* forward confirmation reason */ +"%d file(s) are still being downloaded." = "%их файл(ів) ще досі завантажуються."; + +/* forward confirmation reason */ +"%d file(s) failed to download." = "%их файлів не вийшло завантажити."; + +/* forward confirmation reason */ +"%d file(s) were deleted." = "%их файл(ів) було видалено."; + +/* forward confirmation reason */ +"%d file(s) were not downloaded." = "%d файл(и) не було завантажено."; + /* time interval */ "%d hours" = "%d годин"; +/* alert title */ +"%d messages not forwarded" = "%d повідомлень не переслано"; + /* time interval */ "%d min" = "%d хв"; @@ -175,14 +196,29 @@ /* No comment provided by engineer. */ "%lld file(s) with total size of %@" = "%lld файл(и) загальним розміром %@"; +/* No comment provided by engineer. */ +"%lld group events" = "%lld групові заходи"; + /* No comment provided by engineer. */ "%lld members" = "%lld учасників"; +/* No comment provided by engineer. */ +"%lld messages blocked" = "%lld повідомлень заблоковано"; + +/* No comment provided by engineer. */ +"%lld messages blocked by admin" = "%lld повідомлень заблоковано адміністратором"; + +/* No comment provided by engineer. */ +"%lld messages marked deleted" = "%lld повідомлень позначено як видалені"; + +/* No comment provided by engineer. */ +"%lld messages moderated by %@" = "%lld повідомлень модерує %@"; + /* No comment provided by engineer. */ "%lld minutes" = "%lld хвилин"; /* No comment provided by engineer. */ -"%lld second(s)" = "%lld секунд(и)"; +"%lld new interface languages" = "%lld нові мови інтерфейсу"; /* No comment provided by engineer. */ "%lld seconds" = "%lld секунд"; @@ -223,10 +259,14 @@ /* No comment provided by engineer. */ "~strike~" = "\\~закреслити~"; +/* time to disappear */ +"0 sec" = "0 сек"; + /* No comment provided by engineer. */ "0s" = "0с"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1 день"; /* time interval */ @@ -235,14 +275,19 @@ /* No comment provided by engineer. */ "1 minute" = "1 хвилина"; -/* time interval */ +/* delete after time +time interval */ "1 month" = "1 місяць"; -/* time interval */ +/* delete after time +time interval */ "1 week" = "1 тиждень"; /* No comment provided by engineer. */ -"1-time link" = "1-разове посилання"; +"1-time link" = "Одноразове посилання"; + +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Одноразове посилання можна використовувати *тільки з одним контактом* - поділіться ним особисто або через будь-який месенджер."; /* No comment provided by engineer. */ "5 minutes" = "5 хвилин"; @@ -278,10 +323,7 @@ "Abort changing address?" = "Скасувати зміну адреси?"; /* No comment provided by engineer. */ -"About SimpleX" = "Про SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "Про адресу SimpleX"; +"About operators" = "Про операторів"; /* No comment provided by engineer. */ "About SimpleX Chat" = "Про чат SimpleX"; @@ -290,60 +332,116 @@ "above, then choose:" = "вище, а потім обирайте:"; /* No comment provided by engineer. */ -"Accent color" = "Акцентний колір"; +"Accent" = "Акцент"; /* accept contact request via notification - accept incoming call via notification */ +accept incoming call via notification +swipe action */ "Accept" = "Прийняти"; +/* No comment provided by engineer. */ +"Accept conditions" = "Прийняти умови"; + /* No comment provided by engineer. */ "Accept connection request?" = "Прийняти запит на підключення?"; /* notification body */ "Accept contact request from %@?" = "Прийняти запит на контакт від %@?"; -/* accept contact request via notification */ +/* accept contact request via notification +swipe action */ "Accept incognito" = "Прийняти інкогніто"; /* call status */ "accepted call" = "прийнято виклик"; +/* No comment provided by engineer. */ +"Accepted conditions" = "Прийняті умови"; + +/* chat list item title */ +"accepted invitation" = "прийняте запрошення"; + +/* No comment provided by engineer. */ +"Acknowledged" = "Визнано"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Помилки підтвердження"; + +/* 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 preset servers" = "Додавання попередньо встановлених серверів"; +"Add friends" = "Додайте друзів"; /* No comment provided by engineer. */ "Add profile" = "Додати профіль"; /* No comment provided by engineer. */ -"Add server…" = "Додати сервер…"; +"Add server" = "Додати сервер"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Додайте сервери, відсканувавши QR-код."; +/* No comment provided by engineer. */ +"Add team members" = "Додайте учасників команди"; + /* No comment provided by engineer. */ "Add to another device" = "Додати до іншого пристрою"; /* No comment provided by engineer. */ "Add welcome message" = "Додати вітальне повідомлення"; +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "Додайте членів своєї команди до розмов."; + +/* No comment provided by engineer. */ +"Added media & file servers" = "Додано медіа та файлові сервери"; + +/* No comment provided by engineer. */ +"Added message servers" = "Додано сервери повідомлень"; + +/* No comment provided by engineer. */ +"Additional accent" = "Додатковий акцент"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Додатковий акцент 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Додаткова вторинна"; + /* No comment provided by engineer. */ "Address" = "Адреса"; /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "Зміна адреси буде скасована. Буде використано стару адресу отримання."; +/* No comment provided by engineer. */ +"Address or 1-time link?" = "Адреса чи одноразове посилання?"; + +/* No comment provided by engineer. */ +"Address settings" = "Налаштування адреси"; + /* member role */ "admin" = "адмін"; +/* feature role */ +"admins" = "адміністратори"; + +/* No comment provided by engineer. */ +"Admins can block a member for all." = "Адміністратори можуть заблокувати користувача для всіх."; + /* No comment provided by engineer. */ "Admins can create the links to join groups." = "Адміни можуть створювати посилання для приєднання до груп."; /* No comment provided by engineer. */ "Advanced network settings" = "Розширені налаштування мережі"; +/* No comment provided by engineer. */ +"Advanced settings" = "Додаткові налаштування"; + /* chat item text */ "agreeing encryption for %@…" = "узгодження шифрування для %@…"; @@ -359,29 +457,56 @@ /* No comment provided by engineer. */ "All data is erased when it is entered." = "Всі дані стираються при введенні."; +/* No comment provided by engineer. */ +"All data is kept private on your device." = "Всі дані є приватними для вашого пристрою."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Всі учасники групи залишаться на зв'язку."; +/* feature role */ +"all members" = "всі учасники"; + +/* 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!" = "Усі повідомлення будуть видалені - цю дію не можна скасувати!"; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Всі повідомлення будуть видалені - це неможливо скасувати! Повідомлення будуть видалені ТІЛЬКИ для вас."; +/* No comment provided by engineer. */ +"All new messages from %@ will be hidden!" = "Всі нові повідомлення від %@ будуть приховані!"; + +/* profile dropdown */ +"All profiles" = "Всі профілі"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Всі ваші контакти залишаться на зв'язку."; /* No comment provided by engineer. */ "All your contacts will remain connected. Profile update will be sent to your contacts." = "Всі ваші контакти залишаться на зв'язку. Повідомлення про оновлення профілю буде надіслано вашим контактам."; +/* No comment provided by engineer. */ +"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" = "Дозволити"; /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Дозволяйте дзвінки, тільки якщо ваш контакт дозволяє їх."; +/* No comment provided by engineer. */ +"Allow calls?" = "Дозволити дзвінки?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "Дозволяйте зникати повідомленням, тільки якщо контакт дозволяє вам це робити."; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you." = "Дозволяйте безповоротне видалення повідомлень, тільки якщо контакт дозволяє вам це зробити."; +"Allow downgrade" = "Дозволити пониження версії"; + +/* 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 message reactions only if your contact allows them." = "Дозволяйте реакції на повідомлення, тільки якщо ваш контакт дозволяє їх."; @@ -396,11 +521,17 @@ "Allow sending disappearing messages." = "Дозволити надсилання зникаючих повідомлень."; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages." = "Дозволяє безповоротно видаляти надіслані повідомлення."; +"Allow sharing" = "Дозволити спільний доступ"; + +/* No comment provided by engineer. */ +"Allow to irreversibly delete sent messages. (24 hours)" = "Дозволяє безповоротно видаляти надіслані повідомлення. (24 години)"; /* No comment provided by engineer. */ "Allow to send files and media." = "Дозволяє надсилати файли та медіа."; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "Дозволити надсилати посилання SimpleX."; + /* No comment provided by engineer. */ "Allow to send voice messages." = "Дозволити надсилати голосові повідомлення."; @@ -417,7 +548,7 @@ "Allow your contacts to call you." = "Дозвольте вашим контактам телефонувати вам."; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages." = "Дозвольте вашим контактам безповоротно видаляти надіслані повідомлення."; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Дозвольте вашим контактам безповоротно видаляти надіслані повідомлення. (24 години)"; /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Дозвольте своїм контактам надсилати зникаючі повідомлення."; @@ -428,21 +559,42 @@ /* No comment provided by engineer. */ "Already connected?" = "Вже підключено?"; +/* No comment provided by engineer. */ +"Already connecting!" = "Вже підключаємось!"; + +/* No comment provided by engineer. */ +"Already joining the group!" = "Вже приєднуємося до групи!"; + /* pref value */ "always" = "завжди"; +/* No comment provided by engineer. */ +"Always use private routing." = "Завжди використовуйте приватну маршрутизацію."; + /* No comment provided by engineer. */ "Always use relay" = "Завжди використовуйте реле"; /* No comment provided by engineer. */ "An empty chat profile with the provided name is created, and the app opens as usual." = "Створюється порожній профіль чату з вказаним ім'ям, і додаток відкривається у звичайному режимі."; +/* No comment provided by engineer. */ +"and %lld other events" = "та %lld інших подій"; + /* No comment provided by engineer. */ "Answer call" = "Відповісти на дзвінок"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "Кожен може хостити сервери."; + /* No comment provided by engineer. */ "App build: %@" = "Збірка програми: %@"; +/* No comment provided by engineer. */ +"App data migration" = "Міграція даних додатків"; + +/* No comment provided by engineer. */ +"App encrypts new local files (except videos)." = "Додаток шифрує нові локальні файли (крім відео)."; + /* No comment provided by engineer. */ "App icon" = "Іконка програми"; @@ -452,6 +604,9 @@ /* No comment provided by engineer. */ "App passcode is replaced with self-destruct passcode." = "Пароль програми замінено на пароль самознищення."; +/* No comment provided by engineer. */ +"App session" = "Сесія програми"; + /* No comment provided by engineer. */ "App version" = "Версія програми"; @@ -461,9 +616,30 @@ /* No comment provided by engineer. */ "Appearance" = "Зовнішній вигляд"; +/* No comment provided by engineer. */ +"Apply" = "Подати заявку"; + +/* No comment provided by engineer. */ +"Apply to" = "Звертатися до"; + +/* No comment provided by engineer. */ +"Archive and upload" = "Архівування та завантаження"; + +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Архівуйте контакти, щоб поспілкуватися пізніше."; + +/* No comment provided by engineer. */ +"Archived contacts" = "Архівні контакти"; + +/* No comment provided by engineer. */ +"Archiving database" = "Архівування бази даних"; + /* No comment provided by engineer. */ "Attach" = "Прикріпити"; +/* No comment provided by engineer. */ +"attempts" = "спроби"; + /* No comment provided by engineer. */ "Audio & video calls" = "Аудіо та відео дзвінки"; @@ -491,6 +667,9 @@ /* No comment provided by engineer. */ "Authentication unavailable" = "Автентифікація недоступна"; +/* member role */ +"author" = "автор"; + /* No comment provided by engineer. */ "Auto-accept" = "Автоприйняття"; @@ -500,9 +679,18 @@ /* No comment provided by engineer. */ "Auto-accept images" = "Автоматичне прийняття зображень"; +/* alert title */ +"Auto-accept settings" = "Автоприйняття налаштувань"; + /* No comment provided by engineer. */ "Back" = "Назад"; +/* No comment provided by engineer. */ +"Background" = "Фон"; + +/* No comment provided by engineer. */ +"Bad desktop address" = "Неправильна адреса робочого столу"; + /* integrity error chat item */ "bad message hash" = "невірний хеш повідомлення"; @@ -515,9 +703,70 @@ /* No comment provided by engineer. */ "Bad message ID" = "Неправильний ідентифікатор повідомлення"; +/* No comment provided by engineer. */ +"Better calls" = "Кращі дзвінки"; + +/* No comment provided by engineer. */ +"Better groups" = "Кращі групи"; + +/* No comment provided by engineer. */ +"Better message dates." = "Кращі дати повідомлень."; + /* No comment provided by engineer. */ "Better messages" = "Кращі повідомлення"; +/* No comment provided by engineer. */ +"Better networking" = "Краща мережа"; + +/* No comment provided by engineer. */ +"Better notifications" = "Кращі сповіщення"; + +/* No comment provided by engineer. */ +"Better security ✅" = "Краща безпека ✅"; + +/* No comment provided by engineer. */ +"Better user experience" = "Покращений користувацький досвід"; + +/* No comment provided by engineer. */ +"Black" = "Чорний"; + +/* No comment provided by engineer. */ +"Block" = "Блокувати"; + +/* No comment provided by engineer. */ +"Block for all" = "Заблокувати для всіх"; + +/* No comment provided by engineer. */ +"Block group members" = "Учасники групи блокування"; + +/* No comment provided by engineer. */ +"Block member" = "Заблокувати користувача"; + +/* No comment provided by engineer. */ +"Block member for all?" = "Заблокувати учасника для всіх?"; + +/* No comment provided by engineer. */ +"Block member?" = "Заблокувати користувача?"; + +/* marked deleted chat item preview text */ +"blocked" = "заблоковано"; + +/* rcv group event chat item */ +"blocked %@" = "заблоковано %@"; + +/* blocked chat item +marked deleted chat item preview text */ +"blocked by admin" = "заблоковано адміністратором"; + +/* No comment provided by engineer. */ +"Blocked by admin" = "Заблокований адміністратором"; + +/* No comment provided by engineer. */ +"Blur for better privacy." = "Розмиття для кращої приватності."; + +/* No comment provided by engineer. */ +"Blur media" = "Розмиття медіа"; + /* No comment provided by engineer. */ "bold" = "жирний"; @@ -525,7 +774,7 @@ "Both you and your contact can add message reactions." = "Реакції на повідомлення можете додавати як ви, так і ваш контакт."; /* No comment provided by engineer. */ -"Both you and your contact can irreversibly delete sent messages." = "І ви, і ваш контакт можете безповоротно видалити надіслані повідомлення."; +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "І ви, і ваш контакт можете безповоротно видалити надіслані повідомлення. (24 години)"; /* No comment provided by engineer. */ "Both you and your contact can make calls." = "Дзвонити можете як ви, так і ваш контакт."; @@ -536,9 +785,21 @@ /* No comment provided by engineer. */ "Both you and your contact can send voice messages." = "Надсилати голосові повідомлення можете як ви, так і ваш контакт."; +/* No comment provided by engineer. */ +"Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Болгарською, фінською, тайською та українською мовами - завдяки користувачам та [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; + +/* No comment provided by engineer. */ +"Business address" = "Адреса підприємства"; + +/* No comment provided by engineer. */ +"Business chats" = "Ділові чати"; + /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Через профіль чату (за замовчуванням) або [за з'єднанням](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; +/* No comment provided by engineer. */ +"call" = "дзвонити"; + /* No comment provided by engineer. */ "Call already ended!" = "Дзвінок вже закінчився!"; @@ -555,7 +816,16 @@ "Calls" = "Дзвінки"; /* No comment provided by engineer. */ -"Can't delete user profile!" = "Не можу видалити профіль користувача!"; +"Calls prohibited!" = "Дзвінки заборонені!"; + +/* No comment provided by engineer. */ +"Camera not available" = "Камера недоступна"; + +/* No comment provided by engineer. */ +"Can't call contact" = "Не вдається додзвонитися до контакту"; + +/* No comment provided by engineer. */ +"Can't call member" = "Не вдається зателефонувати користувачеві"; /* No comment provided by engineer. */ "Can't invite contact!" = "Не вдається запросити контакт!"; @@ -564,8 +834,15 @@ "Can't invite contacts!" = "Неможливо запросити контакти!"; /* No comment provided by engineer. */ +"Can't message member" = "Не можу надіслати повідомлення користувачеві"; + +/* alert action +alert button */ "Cancel" = "Скасувати"; +/* No comment provided by engineer. */ +"Cancel migration" = "Скасувати міграцію"; + /* feature offered item */ "cancelled %@" = "скасовано %@"; @@ -573,11 +850,23 @@ "Cannot access keychain to save database password" = "Не вдається отримати доступ до зв'язки ключів для збереження пароля до бази даних"; /* No comment provided by engineer. */ +"Cannot forward message" = "Неможливо переслати повідомлення"; + +/* alert title */ "Cannot receive file" = "Не вдається отримати файл"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Перевищено ліміт - одержувач не отримав раніше надіслані повідомлення."; + +/* No comment provided by engineer. */ +"Cellular" = "Стільниковий"; + /* No comment provided by engineer. */ "Change" = "Зміна"; +/* authentication reason */ +"Change chat profiles" = "Зміна профілів користувачів"; + /* No comment provided by engineer. */ "Change database passphrase?" = "Змінити пароль до бази даних?"; @@ -603,7 +892,7 @@ "Change self-destruct mode" = "Змінити режим самознищення"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "Змінити пароль самознищення"; /* chat item text */ @@ -622,7 +911,16 @@ "changing address…" = "змінює адресу…"; /* No comment provided by engineer. */ -"Chat archive" = "Архів чату"; +"Chat" = "Чат"; + +/* No comment provided by engineer. */ +"Chat already exists" = "Чат вже існує"; + +/* No comment provided by engineer. */ +"Chat already exists!" = "Чат вже існує!"; + +/* No comment provided by engineer. */ +"Chat colors" = "Кольори чату"; /* No comment provided by engineer. */ "Chat console" = "Консоль чату"; @@ -633,6 +931,9 @@ /* No comment provided by engineer. */ "Chat database deleted" = "Видалено базу даних чату"; +/* No comment provided by engineer. */ +"Chat database exported" = "Експортовано базу даних чату"; + /* No comment provided by engineer. */ "Chat database imported" = "Імпорт бази даних чату"; @@ -642,18 +943,51 @@ /* No comment provided by engineer. */ "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." = "Чат зупинено. Якщо ви вже використовували цю базу даних на іншому пристрої, перенесіть її назад перед запуском чату."; + +/* No comment provided by engineer. */ +"Chat list" = "Список чатів"; + +/* No comment provided by engineer. */ +"Chat migrated!" = "Чат перемістився!"; + /* No comment provided by engineer. */ "Chat preferences" = "Налаштування чату"; +/* alert message */ +"Chat preferences were changed." = "Змінено налаштування чату."; + +/* No comment provided by engineer. */ +"Chat profile" = "Профіль користувача"; + +/* No comment provided by engineer. */ +"Chat theme" = "Тема чату"; + +/* No comment provided by engineer. */ +"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. */ "Chats" = "Чати"; /* No comment provided by engineer. */ +"Check messages every 20 min." = "Перевіряйте повідомлення кожні 20 хв."; + +/* No comment provided by engineer. */ +"Check messages when allowed." = "Перевірте повідомлення, коли це дозволено."; + +/* alert title */ "Check server address and try again." = "Перевірте адресу сервера та спробуйте ще раз."; /* No comment provided by engineer. */ "Chinese and Spanish interface" = "Інтерфейс китайською та іспанською мовами"; +/* No comment provided by engineer. */ +"Choose _Migrate from another device_ on the new device and scan QR code." = "Виберіть _Перемістити з іншого пристрою_ на новому пристрої та відскануйте QR-код."; + /* No comment provided by engineer. */ "Choose file" = "Виберіть файл"; @@ -661,6 +995,15 @@ "Choose from library" = "Виберіть з бібліотеки"; /* No comment provided by engineer. */ +"Chunks deleted" = "Фрагменти видалено"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Завантажено фрагменти"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Завантажено фрагменти"; + +/* swipe action */ "Clear" = "Чисто"; /* No comment provided by engineer. */ @@ -669,14 +1012,20 @@ /* No comment provided by engineer. */ "Clear conversation?" = "Відверта розмова?"; +/* No comment provided by engineer. */ +"Clear private notes?" = "Чисті приватні нотатки?"; + /* No comment provided by engineer. */ "Clear verification" = "Очистити перевірку"; /* No comment provided by engineer. */ -"colored" = "кольоровий"; +"Color chats with the new themes." = "Кольорові чати з новими темами."; /* No comment provided by engineer. */ -"Colors" = "Кольори"; +"Color mode" = "Колірний режим"; + +/* No comment provided by engineer. */ +"colored" = "кольоровий"; /* server test step */ "Compare file" = "Порівняти файл"; @@ -687,15 +1036,48 @@ /* No comment provided by engineer. */ "complete" = "завершено"; +/* No comment provided by engineer. */ +"Completed" = "Завершено"; + +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "Умови приймаються на: %@."; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "Для оператора(ів) приймаються умови: **%@**."; + +/* No comment provided by engineer. */ +"Conditions are already accepted for these operator(s): **%@**." = "Умови вже прийняті для наступних операторів: **%@**."; + +/* No comment provided by engineer. */ +"Conditions of use" = "Умови використання"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "Для оператора(ів) приймаються умови: **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "Умови приймаються на: %@."; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "Умови будуть автоматично прийняті для увімкнених операторів на: %@."; + /* No comment provided by engineer. */ "Configure ICE servers" = "Налаштування серверів ICE"; /* No comment provided by engineer. */ "Confirm" = "Підтвердити"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "Підтвердити видалення контакту?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "Підтвердити оновлення бази даних"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Підтвердити файли з невідомих серверів."; + +/* No comment provided by engineer. */ +"Confirm network settings" = "Підтвердьте налаштування мережі"; + /* No comment provided by engineer. */ "Confirm new passphrase…" = "Підтвердіть нову парольну фразу…"; @@ -705,30 +1087,75 @@ /* No comment provided by engineer. */ "Confirm password" = "Підтвердити пароль"; +/* No comment provided by engineer. */ +"Confirm that you remember database passphrase to migrate it." = "Переконайтеся, що ви пам'ятаєте пароль до бази даних для її перенесення."; + +/* No comment provided by engineer. */ +"Confirm upload" = "Підтвердити завантаження"; + /* server test step */ "Connect" = "Підключіться"; +/* No comment provided by engineer. */ +"Connect automatically" = "Підключення автоматично"; + /* No comment provided by engineer. */ "Connect incognito" = "Підключайтеся інкогніто"; +/* No comment provided by engineer. */ +"Connect to desktop" = "Підключення до комп'ютера"; + /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "зв'язатися з розробниками SimpleX Chat."; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Швидше спілкуйтеся з друзями."; + +/* No comment provided by engineer. */ +"Connect to yourself?" = "З'єднатися з самим собою?"; + +/* No comment provided by engineer. */ +"Connect to yourself?\nThis is your own one-time link!" = "Підключитися до себе? \nЦе ваше власне одноразове посилання!"; + +/* No comment provided by engineer. */ +"Connect to yourself?\nThis is your own SimpleX address!" = "З'єднатися з самим собою? \nЦе ваша власна SimpleX-адреса!"; + +/* No comment provided by engineer. */ +"Connect via contact address" = "Підключіться за контактною адресою"; + /* No comment provided by engineer. */ "Connect via link" = "Підключіться за посиланням"; /* No comment provided by engineer. */ -"Connect via link / QR code" = "Підключитися за посиланням / QR-кодом"; +"Connect via one-time link" = "Під'єднатися за одноразовим посиланням"; /* No comment provided by engineer. */ -"Connect via one-time link" = "Під'єднатися за одноразовим посиланням"; +"Connect with %@" = "Підключитися до %@"; /* No comment provided by engineer. */ "connected" = "з'єднаний"; +/* No comment provided by engineer. */ +"Connected" = "Підключено"; + +/* No comment provided by engineer. */ +"Connected desktop" = "Підключений робочий стіл"; + +/* rcv group event chat item */ +"connected directly" = "з'єднані безпосередньо"; + +/* No comment provided by engineer. */ +"Connected servers" = "Підключені сервери"; + +/* No comment provided by engineer. */ +"Connected to desktop" = "Підключено до настільного комп'ютера"; + /* No comment provided by engineer. */ "connecting" = "з'єднання"; +/* No comment provided by engineer. */ +"Connecting" = "Підключення"; + /* No comment provided by engineer. */ "connecting (accepted)" = "з'єднання (прийнято)"; @@ -750,12 +1177,21 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Підключення до сервера... (помилка: %@)"; -/* chat list item title */ +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "З'єднання з контактом, будь ласка, зачекайте або перевірте пізніше!"; + +/* No comment provided by engineer. */ +"Connecting to desktop" = "Підключення до ПК"; + +/* No comment provided by engineer. */ "connecting…" = "з'єднання…"; /* No comment provided by engineer. */ "Connection" = "Підключення"; +/* No comment provided by engineer. */ +"Connection and servers status." = "Стан з'єднання та серверів."; + /* No comment provided by engineer. */ "Connection error" = "Помилка підключення"; @@ -765,21 +1201,42 @@ /* chat list item title (it should not be shown */ "connection established" = "з'єднання встановлене"; +/* No comment provided by engineer. */ +"Connection notifications" = "Сповіщення про підключення"; + /* No comment provided by engineer. */ "Connection request sent!" = "Запит на підключення відправлено!"; +/* No comment provided by engineer. */ +"Connection security" = "Безпека з'єднання"; + +/* No comment provided by engineer. */ +"Connection terminated" = "З'єднання розірвано"; + /* No comment provided by engineer. */ "Connection timeout" = "Тайм-аут з'єднання"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "Припинено зв'язок з робочим столом"; + /* connection information */ "connection:%@" = "з'єднання:%@"; +/* No comment provided by engineer. */ +"Connections" = "З'єднання"; + +/* profile update event chat item */ +"contact %@ changed to %@" = "контакт %1$@ змінено на %2$@"; + /* No comment provided by engineer. */ "Contact allows" = "Контакт дозволяє"; /* No comment provided by engineer. */ "Contact already exists" = "Контакт вже існує"; +/* No comment provided by engineer. */ +"Contact deleted!" = "Контакт видалено!"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "контакт має шифрування e2e"; @@ -793,7 +1250,7 @@ "Contact is connected" = "Контакт підключений"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "Контакт ще не підключено!"; +"Contact is deleted." = "Контакт видалено."; /* No comment provided by engineer. */ "Contact name" = "Ім'я контактної особи"; @@ -801,6 +1258,9 @@ /* No comment provided by engineer. */ "Contact preferences" = "Налаштування контактів"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "Контакт буде видалено - це неможливо скасувати!"; + /* No comment provided by engineer. */ "Contacts" = "Контакти"; @@ -810,21 +1270,39 @@ /* No comment provided by engineer. */ "Continue" = "Продовжуйте"; -/* chat item action */ +/* 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. */ "Core version: v%@" = "Основна версія: v%@"; +/* No comment provided by engineer. */ +"Corner" = "Кут"; + +/* No comment provided by engineer. */ +"Correct name to %@?" = "Виправити ім'я на %@?"; + /* No comment provided by engineer. */ "Create" = "Створити"; /* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Створіть адресу, щоб люди могли з вами зв'язатися."; +"Create 1-time link" = "Створити одноразове посилання"; + +/* No comment provided by engineer. */ +"Create a group using a random profile." = "Створіть групу, використовуючи випадковий профіль."; /* server test step */ "Create file" = "Створити файл"; +/* No comment provided by engineer. */ +"Create group" = "Створити групу"; + /* No comment provided by engineer. */ "Create group link" = "Створити групове посилання"; @@ -832,7 +1310,10 @@ "Create link" = "Створити посилання"; /* No comment provided by engineer. */ -"Create one-time invitation link" = "Створіть одноразове посилання-запрошення"; +"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Створіть новий профіль у [desktop app](https://simplex.chat/downloads/). 💻"; + +/* No comment provided by engineer. */ +"Create profile" = "Створити профіль"; /* server test step */ "Create queue" = "Створити чергу"; @@ -847,17 +1328,35 @@ "Create your profile" = "Створіть свій профіль"; /* No comment provided by engineer. */ -"Created on %@" = "Створено %@"; +"Created" = "Створено"; + +/* No comment provided by engineer. */ +"Created at" = "Створено за адресою"; + +/* copied message info */ +"Created at: %@" = "Створено за адресою: %@"; + +/* No comment provided by engineer. */ +"Creating archive link" = "Створення архівного посилання"; + +/* No comment provided by engineer. */ +"Creating link…" = "Створення посилання…"; /* No comment provided by engineer. */ "creator" = "творець"; +/* No comment provided by engineer. */ +"Current conditions text couldn't be loaded, you can review conditions via this link:" = "Текст поточних умов не вдалося завантажити, ви можете переглянути умови за цим посиланням:"; + /* No comment provided by engineer. */ "Current Passcode" = "Поточний пароль"; /* No comment provided by engineer. */ "Current passphrase…" = "Поточна парольна фраза…"; +/* No comment provided by engineer. */ +"Current profile" = "Поточний профіль"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Наразі максимальний підтримуваний розмір файлу - %@."; @@ -867,9 +1366,18 @@ /* No comment provided by engineer. */ "Custom time" = "Індивідуальний час"; +/* No comment provided by engineer. */ +"Customizable message shape." = "Налаштовується форма повідомлення."; + +/* No comment provided by engineer. */ +"Customize theme" = "Налаштувати тему"; + /* No comment provided by engineer. */ "Dark" = "Темний"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Кольори темного режиму"; + /* No comment provided by engineer. */ "Database downgrade" = "Пониження версії бази даних"; @@ -930,13 +1438,20 @@ /* time unit */ "days" = "днів"; +/* No comment provided by engineer. */ +"Debug delivery" = "Доставка налагодження"; + /* No comment provided by engineer. */ "Decentralized" = "Децентралізований"; /* message decrypt error item */ "Decryption error" = "Помилка розшифровки"; -/* pref value */ +/* No comment provided by engineer. */ +"decryption errors" = "помилки розшифровки"; + +/* delete after time +pref value */ "default (%@)" = "за замовчуванням (%@)"; /* No comment provided by engineer. */ @@ -945,9 +1460,16 @@ /* No comment provided by engineer. */ "default (yes)" = "за замовчуванням (так)"; -/* chat item action */ +/* alert action +swipe action */ "Delete" = "Видалити"; +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "Видалити %lld повідомлень користувачів?"; + +/* No comment provided by engineer. */ +"Delete %lld messages?" = "Видалити %lld повідомлень?"; + /* No comment provided by engineer. */ "Delete address" = "Видалити адресу"; @@ -961,10 +1483,10 @@ "Delete all files" = "Видалити всі файли"; /* No comment provided by engineer. */ -"Delete archive" = "Видалити архів"; +"Delete and notify contact" = "Видалити та повідомити контакт"; /* No comment provided by engineer. */ -"Delete chat archive?" = "Видалити архів чату?"; +"Delete chat" = "Видалити чат"; /* No comment provided by engineer. */ "Delete chat profile" = "Видалити профіль чату"; @@ -972,6 +1494,9 @@ /* No comment provided by engineer. */ "Delete chat profile?" = "Видалити профіль чату?"; +/* No comment provided by engineer. */ +"Delete chat?" = "Видалити чат?"; + /* No comment provided by engineer. */ "Delete connection" = "Видалити підключення"; @@ -979,11 +1504,14 @@ "Delete contact" = "Видалити контакт"; /* No comment provided by engineer. */ -"Delete Contact" = "Видалити контакт"; +"Delete contact?" = "Видалити контакт?"; /* No comment provided by engineer. */ "Delete database" = "Видалити базу даних"; +/* No comment provided by engineer. */ +"Delete database from this device" = "Видалити базу даних з цього пристрою"; + /* server test step */ "Delete file" = "Видалити файл"; @@ -1020,7 +1548,7 @@ /* No comment provided by engineer. */ "Delete message?" = "Видалити повідомлення?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "Видалити повідомлення"; /* No comment provided by engineer. */ @@ -1033,7 +1561,7 @@ "Delete old database?" = "Видалити стару базу даних?"; /* No comment provided by engineer. */ -"Delete pending connection" = "Видалити очікуване з'єднання"; +"Delete or moderate up to 200 messages." = "Видалити або модерувати до 200 повідомлень."; /* No comment provided by engineer. */ "Delete pending connection?" = "Видалити очікуване з'єднання?"; @@ -1044,21 +1572,39 @@ /* server test step */ "Delete queue" = "Видалити чергу"; +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Видаляйте до 20 повідомлень одночасно."; + /* No comment provided by engineer. */ "Delete user profile?" = "Видалити профіль користувача?"; +/* No comment provided by engineer. */ +"Delete without notification" = "Видалення без попередження"; + /* deleted chat item */ "deleted" = "видалено"; +/* No comment provided by engineer. */ +"Deleted" = "Видалено"; + /* No comment provided by engineer. */ "Deleted at" = "Видалено за"; /* copied message info */ "Deleted at: %@" = "Видалено за: %@"; +/* rcv direct event chat item */ +"deleted contact" = "видалений контакт"; + /* rcv group event chat item */ "deleted group" = "видалено групу"; +/* No comment provided by engineer. */ +"Deletion errors" = "Помилки видалення"; + +/* No comment provided by engineer. */ +"Delivered even when Apple drops them." = "Доставляються навіть тоді, коли Apple кидає їх."; + /* No comment provided by engineer. */ "Delivery" = "Доставка"; @@ -1071,9 +1617,36 @@ /* No comment provided by engineer. */ "Description" = "Опис"; +/* No comment provided by engineer. */ +"Desktop address" = "Адреса робочого столу"; + +/* No comment provided by engineer. */ +"Desktop app version %@ is not compatible with this app." = "Версія програми для настільних комп'ютерів %@ не сумісна з цією програмою."; + +/* No comment provided by engineer. */ +"Desktop devices" = "Настільні пристрої"; + +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "Адреса сервера призначення %@ несумісна з налаштуваннями сервера пересилання %@."; + +/* snd error text */ +"Destination server error: %@" = "Помилка сервера призначення: %@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "Версія сервера призначення %@ несумісна з версією сервера переадресації %@."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Детальна статистика"; + +/* No comment provided by engineer. */ +"Details" = "Деталі"; + /* No comment provided by engineer. */ "Develop" = "Розробник"; +/* No comment provided by engineer. */ +"Developer options" = "Можливості для розробників"; + /* No comment provided by engineer. */ "Developer tools" = "Інструменти для розробників"; @@ -1099,7 +1672,10 @@ "Direct messages" = "Прямі повідомлення"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "У цій групі заборонені прямі повідомлення між учасниками."; +"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. */ "Disable (keep overrides)" = "Вимкнути (зберегти перевизначення)"; @@ -1113,6 +1689,9 @@ /* No comment provided by engineer. */ "disabled" = "вимкнено"; +/* No comment provided by engineer. */ +"Disabled" = "Вимкнено"; + /* No comment provided by engineer. */ "Disappearing message" = "Зникаюче повідомлення"; @@ -1123,7 +1702,7 @@ "Disappearing messages are prohibited in this chat." = "Зникаючі повідомлення в цьому чаті заборонені."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "У цій групі заборонено зникаючі повідомлення."; +"Disappearing messages are prohibited." = "У цій групі заборонено зникаючі повідомлення."; /* No comment provided by engineer. */ "Disappears at" = "Зникає за"; @@ -1134,9 +1713,30 @@ /* server test step */ "Disconnect" = "Від'єднати"; +/* No comment provided by engineer. */ +"Disconnect desktop?" = "Відключити робочий стіл?"; + +/* No comment provided by engineer. */ +"Discover and join groups" = "Знаходьте та приєднуйтесь до груп"; + +/* No comment provided by engineer. */ +"Discover via local network" = "Відкриття через локальну мережу"; + /* No comment provided by engineer. */ "Do it later" = "Зробіть це пізніше"; +/* No comment provided by engineer. */ +"Do not send history to new members." = "Не надсилайте історію новим користувачам."; + +/* 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." = "Не використовуйте облікові дані з проксі."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "НЕ використовуйте приватну маршрутизацію."; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "НЕ використовуйте SimpleX для екстрених викликів."; @@ -1152,21 +1752,52 @@ /* No comment provided by engineer. */ "Downgrade and open chat" = "Пониження та відкритий чат"; +/* alert button +chat item action */ +"Download" = "Завантажити"; + +/* No comment provided by engineer. */ +"Download errors" = "Помилки завантаження"; + +/* No comment provided by engineer. */ +"Download failed" = "Не вдалося завантажити"; + /* server test step */ "Download file" = "Завантажити файл"; +/* alert action */ +"Download files" = "Завантажити файли"; + +/* No comment provided by engineer. */ +"Downloaded" = "Завантажено"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Завантажені файли"; + +/* No comment provided by engineer. */ +"Downloading archive" = "Завантажити архів"; + +/* No comment provided by engineer. */ +"Downloading link details" = "Деталі посилання для завантаження"; + /* No comment provided by engineer. */ "Duplicate display name!" = "Дублююче ім'я користувача!"; /* integrity error chat item */ "duplicate message" = "дублююче повідомлення"; +/* No comment provided by engineer. */ +"duplicates" = "дублікати"; + /* No comment provided by engineer. */ "Duration" = "Тривалість"; /* No comment provided by engineer. */ "e2e encrypted" = "e2e зашифрований"; +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "Зашифровані сповіщення E2E."; + /* chat item action */ "Edit" = "Редагувати"; @@ -1179,12 +1810,18 @@ /* No comment provided by engineer. */ "Enable (keep overrides)" = "Увімкнути (зберегти перевизначення)"; -/* No comment provided by engineer. */ +/* alert title */ "Enable automatic message deletion?" = "Увімкнути автоматичне видалення повідомлень?"; +/* No comment provided by engineer. */ +"Enable camera access" = "Увімкніть доступ до камери"; + /* No comment provided by engineer. */ "Enable for all" = "Увімкнути для всіх"; +/* No comment provided by engineer. */ +"Enable in direct chats (BETA)!" = "Увімкнути в прямих чатах (BETA)!"; + /* No comment provided by engineer. */ "Enable instant notifications?" = "Увімкнути миттєві сповіщення?"; @@ -1212,6 +1849,12 @@ /* enabled status */ "enabled" = "увімкнено"; +/* No comment provided by engineer. */ +"Enabled" = "Увімкнено"; + +/* No comment provided by engineer. */ +"Enabled for" = "Увімкнено для"; + /* enabled status */ "enabled for contact" = "увімкнено для контакту"; @@ -1224,12 +1867,21 @@ /* No comment provided by engineer. */ "Encrypt database?" = "Зашифрувати базу даних?"; +/* No comment provided by engineer. */ +"Encrypt local files" = "Шифрування локальних файлів"; + +/* No comment provided by engineer. */ +"Encrypt stored files & media" = "Шифрування збережених файлів і носіїв"; + /* No comment provided by engineer. */ "Encrypted database" = "Зашифрована база даних"; /* notification */ "Encrypted message or another event" = "Зашифроване повідомлення або інша подія"; +/* notification */ +"Encrypted message: app is stopped" = "Зашифроване повідомлення: додаток зупинено"; + /* notification */ "Encrypted message: database error" = "Зашифроване повідомлення: помилка бази даних"; @@ -1263,6 +1915,12 @@ /* chat item text */ "encryption re-negotiation allowed for %@" = "переузгодження шифрування дозволено для %@"; +/* message decrypt error item */ +"Encryption re-negotiation error" = "Помилка повторного узгодження шифрування"; + +/* No comment provided by engineer. */ +"Encryption re-negotiation failed." = "Повторне узгодження шифрування не вдалося."; + /* chat item text */ "encryption re-negotiation required" = "потрібне повторне узгодження шифрування"; @@ -1278,9 +1936,15 @@ /* No comment provided by engineer. */ "Enter correct passphrase." = "Введіть правильну парольну фразу."; +/* No comment provided by engineer. */ +"Enter group name…" = "Введіть назву групи…"; + /* No comment provided by engineer. */ "Enter Passcode" = "Введіть пароль"; +/* No comment provided by engineer. */ +"Enter passphrase" = "Введіть парольну фразу"; + /* No comment provided by engineer. */ "Enter passphrase…" = "Введіть пароль…"; @@ -1290,12 +1954,18 @@ /* No comment provided by engineer. */ "Enter server manually" = "Увійдіть на сервер вручну"; +/* No comment provided by engineer. */ +"Enter this device name…" = "Введіть назву пристрою…"; + /* placeholder */ "Enter welcome message…" = "Введіть вітальне повідомлення…"; /* placeholder */ "Enter welcome message… (optional)" = "Введіть вітальне повідомлення... (необов'язково)"; +/* No comment provided by engineer. */ +"Enter your name…" = "Введіть своє ім'я…"; + /* No comment provided by engineer. */ "error" = "помилка"; @@ -1305,24 +1975,36 @@ /* No comment provided by engineer. */ "Error aborting address change" = "Помилка скасування зміни адреси"; +/* alert title */ +"Error accepting conditions" = "Помилка прийняття умов"; + /* No comment provided by engineer. */ "Error accepting contact request" = "Помилка при прийнятті запиту на контакт"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Помилка доступу до файлу бази даних"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Помилка додавання користувача(ів)"; +/* alert title */ +"Error adding server" = "Помилка додавання сервера"; + /* No comment provided by engineer. */ "Error changing address" = "Помилка зміни адреси"; +/* No comment provided by engineer. */ +"Error changing connection profile" = "Помилка при зміні профілю з'єднання"; + /* No comment provided by engineer. */ "Error changing role" = "Помилка зміни ролі"; /* No comment provided by engineer. */ "Error changing setting" = "Помилка зміни налаштування"; +/* No comment provided by engineer. */ +"Error changing to incognito!" = "Помилка переходу на інкогніто!"; + +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "Помилка підключення до сервера переадресації %@. Спробуйте пізніше."; + /* No comment provided by engineer. */ "Error creating address" = "Помилка створення адреси"; @@ -1332,9 +2014,18 @@ /* No comment provided by engineer. */ "Error creating group link" = "Помилка створення посилання на групу"; +/* No comment provided by engineer. */ +"Error creating member contact" = "Помилка при створенні контакту користувача"; + +/* No comment provided by engineer. */ +"Error creating message" = "Повідомлення про створення помилки"; + /* No comment provided by engineer. */ "Error creating profile!" = "Помилка створення профілю!"; +/* No comment provided by engineer. */ +"Error decrypting file" = "Помилка розшифрування файлу"; + /* No comment provided by engineer. */ "Error deleting chat database" = "Помилка видалення бази даних чату"; @@ -1344,9 +2035,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Помилка видалення з'єднання"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Помилка видалення контакту"; - /* No comment provided by engineer. */ "Error deleting database" = "Помилка видалення бази даних"; @@ -1359,6 +2047,9 @@ /* No comment provided by engineer. */ "Error deleting user profile" = "Помилка видалення профілю користувача"; +/* No comment provided by engineer. */ +"Error downloading the archive" = "Помилка завантаження архіву"; + /* No comment provided by engineer. */ "Error enabling delivery receipts!" = "Помилка активації підтвердження доставлення!"; @@ -1371,23 +2062,38 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Помилка експорту бази даних чату"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Помилка експорту теми: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Помилка імпорту бази даних чату"; /* No comment provided by engineer. */ "Error joining group" = "Помилка приєднання до групи"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Помилка завантаження %@ серверів"; +/* alert title */ +"Error loading servers" = "Помилка завантаження серверів"; /* No comment provided by engineer. */ +"Error migrating settings" = "Помилка міграції налаштувань"; + +/* No comment provided by engineer. */ +"Error opening chat" = "Помилка відкриття чату"; + +/* alert title */ "Error receiving file" = "Помилка отримання файлу"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Помилка перепідключення сервера"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Помилка перепідключення серверів"; + /* No comment provided by engineer. */ "Error removing member" = "Помилка видалення учасника"; /* No comment provided by engineer. */ -"Error saving %@ servers" = "Помилка збереження %@ серверів"; +"Error resetting statistics" = "Статистика скидання помилок"; /* No comment provided by engineer. */ "Error saving group profile" = "Помилка збереження профілю групи"; @@ -1401,12 +2107,24 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "Помилка збереження пароля на keychain"; +/* alert title */ +"Error saving servers" = "Сервери збереження помилок"; + +/* when migrating */ +"Error saving settings" = "Налаштування збереження помилок"; + /* No comment provided by engineer. */ "Error saving user password" = "Помилка збереження пароля користувача"; +/* No comment provided by engineer. */ +"Error scanning code: %@" = "Код помилки сканування: %@"; + /* No comment provided by engineer. */ "Error sending email" = "Помилка надсилання електронного листа"; +/* No comment provided by engineer. */ +"Error sending member contact invitation" = "Помилка надсилання запрошення до контактів учасника"; + /* No comment provided by engineer. */ "Error sending message" = "Помилка надсилання повідомлення"; @@ -1420,6 +2138,9 @@ "Error stopping chat" = "Помилка зупинки чату"; /* No comment provided by engineer. */ +"Error switching profile" = "Помилка перемикання профілю"; + +/* alertTitle */ "Error switching profile!" = "Помилка перемикання профілю!"; /* No comment provided by engineer. */ @@ -1431,6 +2152,9 @@ /* No comment provided by engineer. */ "Error updating message" = "Повідомлення про помилку оновлення"; +/* alert title */ +"Error updating server" = "Помилка оновлення сервера"; + /* No comment provided by engineer. */ "Error updating settings" = "Помилка оновлення налаштувань"; @@ -1438,9 +2162,17 @@ "Error updating user privacy" = "Помилка оновлення конфіденційності користувача"; /* No comment provided by engineer. */ -"Error: " = "Помилка: "; +"Error uploading the archive" = "Помилка при завантаженні архіву"; /* No comment provided by engineer. */ +"Error verifying passphrase:" = "Помилка при перевірці парольної фрази:"; + +/* No comment provided by engineer. */ +"Error: " = "Помилка: "; + +/* alert message +file error text +snd error text */ "Error: %@" = "Помилка: %@"; /* No comment provided by engineer. */ @@ -1449,14 +2181,23 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Помилка: URL-адреса невірна"; +/* No comment provided by engineer. */ +"Errors" = "Помилки"; + +/* servers error */ +"Errors in servers configuration." = "Помилки в конфігурації серверів."; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Навіть коли вимкнений у розмові."; /* No comment provided by engineer. */ -"event happened" = "відбулася подія"; +"Exit without saving" = "Вихід без збереження"; + +/* chat item action */ +"Expand" = "Розгорнути"; /* No comment provided by engineer. */ -"Exit without saving" = "Вихід без збереження"; +"expired" = "закінчився"; /* No comment provided by engineer. */ "Export database" = "Експорт бази даних"; @@ -1464,9 +2205,15 @@ /* No comment provided by engineer. */ "Export error:" = "Помилка експорту:"; +/* No comment provided by engineer. */ +"Export theme" = "Тема експорту"; + /* No comment provided by engineer. */ "Exported database archive." = "Експортований архів бази даних."; +/* No comment provided by engineer. */ +"Exported file doesn't exist" = "Експортований файл не існує"; + /* No comment provided by engineer. */ "Exporting database archive…" = "Експорт архіву бази даних…"; @@ -1477,8 +2224,29 @@ "Fast and no wait until the sender is online!" = "Швидко і без очікування, поки відправник буде онлайн!"; /* No comment provided by engineer. */ +"Faster joining and more reliable messages." = "Швидше приєднання та надійніші повідомлення."; + +/* swipe action */ "Favorite" = "Улюблений"; +/* file error alert title */ +"File error" = "Помилка файлу"; + +/* alert message */ +"File errors:\n%@" = "Помилки файлів:\n%@"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Файл не знайдено - найімовірніше, файл було видалено або скасовано."; + +/* file error text */ +"File server error: %@" = "Помилка файлового сервера: %@"; + +/* No comment provided by engineer. */ +"File status" = "Статус файлу"; + +/* copied message info */ +"File status: %@" = "Статус файлу: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Файл буде видалено з серверів."; @@ -1491,6 +2259,9 @@ /* No comment provided by engineer. */ "File: %@" = "Файл: %@"; +/* No comment provided by engineer. */ +"Files" = "Файли"; + /* No comment provided by engineer. */ "Files & media" = "Файли та медіа"; @@ -1498,7 +2269,10 @@ "Files and media" = "Файли і медіа"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Файли та медіа в цій групі заборонені."; +"Files and media are prohibited." = "Файли та медіа в цій групі заборонені."; + +/* No comment provided by engineer. */ +"Files and media not allowed" = "Файли та медіафайли заборонені"; /* No comment provided by engineer. */ "Files and media prohibited!" = "Файли та медіа заборонені!"; @@ -1506,6 +2280,12 @@ /* No comment provided by engineer. */ "Filter unread and favorite chats." = "Фільтруйте непрочитані та улюблені чати."; +/* No comment provided by engineer. */ +"Finalize migration" = "Завершити міграцію"; + +/* No comment provided by engineer. */ +"Finalize migration on another device." = "Завершіть міграцію на іншому пристрої."; + /* No comment provided by engineer. */ "Finally, we have them! 🚀" = "Нарешті, вони у нас є! 🚀"; @@ -1530,9 +2310,69 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Виправлення не підтримується учасником групи"; +/* servers error */ +"For chat profile %@:" = "Для профілю чату %@:"; + /* No comment provided by engineer. */ "For console" = "Для консолі"; +/* No comment provided by engineer. */ +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Наприклад, якщо ваш контакт отримує повідомлення через сервер SimpleX Chat, ваш додаток доставлятиме їх через сервер Flux."; + +/* No comment provided by engineer. */ +"For private routing" = "Для приватної маршрутизації"; + +/* No comment provided by engineer. */ +"For social media" = "Для соціальних мереж"; + +/* chat item action */ +"Forward" = "Пересилання"; + +/* alert title */ +"Forward %d message(s)?" = "Переслати %d повідомлення(ь)?"; + +/* No comment provided by engineer. */ +"Forward and save messages" = "Пересилання та збереження повідомлень"; + +/* alert action */ +"Forward messages" = "Пересилання повідомлень"; + +/* alert message */ +"Forward messages without files?" = "Пересилати повідомлення без файлів?"; + +/* No comment provided by engineer. */ +"Forward up to 20 messages at once." = "Пересилайте до 20 повідомлень одночасно."; + +/* No comment provided by engineer. */ +"forwarded" = "переслано"; + +/* No comment provided by engineer. */ +"Forwarded" = "Переслано"; + +/* No comment provided by engineer. */ +"Forwarded from" = "Переслано з"; + +/* No comment provided by engineer. */ +"Forwarding %lld messages" = "Пересилання повідомлень %lld"; + +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Серверу переадресації %@ не вдалося з'єднатися з сервером призначення %@. Спробуйте пізніше."; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "Адреса сервера переадресації несумісна з налаштуваннями мережі: %@."; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "Версія сервера переадресації несумісна з мережевими налаштуваннями: %@."; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Сервер переадресації: %1$@\nПомилка сервера призначення: %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Сервер переадресації: %1$@\nПомилка: %2$@"; + +/* No comment provided by engineer. */ +"Found desktop" = "Знайдено робочий стіл"; + /* No comment provided by engineer. */ "French interface" = "Французький інтерфейс"; @@ -1543,7 +2383,7 @@ "Full name (optional)" = "Повне ім'я (необов'язково)"; /* No comment provided by engineer. */ -"Full name:" = "Повне ім'я:"; +"Fully decentralized – visible only to members." = "Повністю децентралізована - видима лише для учасників."; /* No comment provided by engineer. */ "Fully re-implemented - work in background!" = "Повністю перероблено - робота у фоновому режимі!"; @@ -1554,9 +2394,21 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIF-файли та наклейки"; +/* message preview */ +"Good afternoon!" = "Доброго дня!"; + +/* message preview */ +"Good morning!" = "Доброго ранку!"; + /* No comment provided by engineer. */ "Group" = "Група"; +/* No comment provided by engineer. */ +"Group already exists" = "Група вже існує"; + +/* No comment provided by engineer. */ +"Group already exists!" = "Група вже існує!"; + /* No comment provided by engineer. */ "group deleted" = "групу видалено"; @@ -1584,24 +2436,6 @@ /* No comment provided by engineer. */ "Group links" = "Групові посилання"; -/* No comment provided by engineer. */ -"Group members can add message reactions." = "Учасники групи можуть додавати реакції на повідомлення."; - -/* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages." = "Учасники групи можуть безповоротно видаляти надіслані повідомлення."; - -/* No comment provided by engineer. */ -"Group members can send direct messages." = "Учасники групи можуть надсилати прямі повідомлення."; - -/* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Учасники групи можуть надсилати зникаючі повідомлення."; - -/* No comment provided by engineer. */ -"Group members can send files and media." = "Учасники групи можуть надсилати файли та медіа."; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "Учасники групи можуть надсилати голосові повідомлення."; - /* notification */ "Group message:" = "Групове повідомлення:"; @@ -1656,11 +2490,17 @@ /* No comment provided by engineer. */ "History" = "Історія"; +/* No comment provided by engineer. */ +"History is not sent to new members." = "Історія не надсилається новим учасникам."; + /* time unit */ "hours" = "години"; /* No comment provided by engineer. */ -"How it works" = "Як це працює"; +"How it affects privacy" = "Як це впливає на конфіденційність"; + +/* No comment provided by engineer. */ +"How it helps privacy" = "Як це захищає приватність"; /* No comment provided by engineer. */ "How SimpleX works" = "Як працює SimpleX"; @@ -1674,15 +2514,15 @@ /* No comment provided by engineer. */ "How to use your servers" = "Як користуватися вашими серверами"; +/* No comment provided by engineer. */ +"Hungarian interface" = "Інтерфейс угорською мовою"; + /* No comment provided by engineer. */ "ICE servers (one per line)" = "Сервери ICE (по одному на лінію)"; /* No comment provided by engineer. */ "If you can't meet in person, show QR code in a video call, or share the link." = "Якщо ви не можете зустрітися особисто, покажіть QR-код у відеодзвінку або поділіться посиланням."; -/* No comment provided by engineer. */ -"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Якщо ви не можете зустрітися особисто, ви можете **сканувати QR-код у відеодзвінку**, або ваш контакт може поділитися посиланням на запрошення."; - /* No comment provided by engineer. */ "If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Якщо ви введете цей пароль при відкритті програми, всі дані програми будуть безповоротно видалені!"; @@ -1705,7 +2545,7 @@ "Immediately" = "Негайно"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Імунітет до спаму та зловживань"; +"Immune to spam" = "Імунітет до спаму та зловживань"; /* No comment provided by engineer. */ "Import" = "Імпорт"; @@ -1716,18 +2556,45 @@ /* No comment provided by engineer. */ "Import database" = "Імпорт бази даних"; +/* No comment provided by engineer. */ +"Import failed" = "Не вдалося імпортувати"; + +/* No comment provided by engineer. */ +"Import theme" = "Імпорт теми"; + +/* No comment provided by engineer. */ +"Importing archive" = "Імпорт архіву"; + +/* No comment provided by engineer. */ +"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "Покращена доставка, зменшене використання трафіку.\nНезабаром з'являться нові покращення!"; + +/* No comment provided by engineer. */ +"Improved message delivery" = "Покращена доставка повідомлень"; + /* No comment provided by engineer. */ "Improved privacy and security" = "Покращена конфіденційність та безпека"; /* No comment provided by engineer. */ "Improved server configuration" = "Покращена конфігурація сервера"; +/* No comment provided by engineer. */ +"In order to continue, chat should be stopped." = "Для того, щоб продовжити, чат слід зупинити."; + /* No comment provided by engineer. */ "In reply to" = "У відповідь на"; +/* No comment provided by engineer. */ +"In-call sounds" = "Звуки вхідного дзвінка"; + +/* No comment provided by engineer. */ +"inactive" = "неактивний"; + /* No comment provided by engineer. */ "Incognito" = "Інкогніто"; +/* No comment provided by engineer. */ +"Incognito groups" = "Групи інкогніто"; + /* No comment provided by engineer. */ "Incognito mode" = "Режим інкогніто"; @@ -1755,6 +2622,9 @@ /* No comment provided by engineer. */ "Incompatible database version" = "Несумісна версія бази даних"; +/* No comment provided by engineer. */ +"Incompatible version" = "Несумісна версія"; + /* PIN entry */ "Incorrect passcode" = "Неправильний пароль"; @@ -1773,14 +2643,17 @@ /* No comment provided by engineer. */ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Встановіть [SimpleX Chat для терміналу](https://github.com/simplex-chat/simplex-chat)"; +/* No comment provided by engineer. */ +"Instant" = "Миттєво"; + /* No comment provided by engineer. */ "Instant push notifications will be hidden!\n" = "Миттєві пуш-сповіщення будуть приховані!\n"; /* No comment provided by engineer. */ -"Instantly" = "Миттєво"; +"Interface" = "Інтерфейс"; /* No comment provided by engineer. */ -"Interface" = "Інтерфейс"; +"Interface colors" = "Кольори інтерфейсу"; /* invalid chat data */ "invalid chat" = "недійсний чат"; @@ -1795,6 +2668,24 @@ "invalid data" = "невірні дані"; /* No comment provided by engineer. */ +"Invalid display name!" = "Неправильне ім'я користувача!"; + +/* No comment provided by engineer. */ +"Invalid link" = "Невірне посилання"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Недійсне підтвердження міграції"; + +/* No comment provided by engineer. */ +"Invalid name!" = "Неправильне ім'я!"; + +/* No comment provided by engineer. */ +"Invalid QR code" = "Неправильний QR-код"; + +/* No comment provided by engineer. */ +"Invalid response" = "Неправильна відповідь"; + +/* alert title */ "Invalid server address!" = "Неправильна адреса сервера!"; /* item status text */ @@ -1806,12 +2697,18 @@ /* group name */ "invitation to group %@" = "запрошення до групи %@"; +/* No comment provided by engineer. */ +"invite" = "запросити"; + /* No comment provided by engineer. */ "Invite friends" = "Запросити друзів"; /* No comment provided by engineer. */ "Invite members" = "Запросити учасників"; +/* No comment provided by engineer. */ +"Invite to chat" = "Запросити в чат"; + /* No comment provided by engineer. */ "Invite to group" = "Запросити до групи"; @@ -1833,6 +2730,9 @@ /* No comment provided by engineer. */ "iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "Пароль бази даних буде безпечно збережено в iOS Keychain після запуску чату або зміни пароля - це дасть змогу отримувати миттєві повідомлення."; +/* No comment provided by engineer. */ +"IP address" = "IP-адреса"; + /* No comment provided by engineer. */ "Irreversible message deletion" = "Безповоротне видалення повідомлення"; @@ -1840,7 +2740,7 @@ "Irreversible message deletion is prohibited in this chat." = "У цьому чаті заборонено безповоротне видалення повідомлень."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "У цій групі заборонено безповоротне видалення повідомлень."; +"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." = "Це дозволяє мати багато анонімних з'єднань без будь-яких спільних даних між ними в одному профілі чату."; @@ -1851,6 +2751,9 @@ /* No comment provided by engineer. */ "It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Це може статися, коли:\n1. Термін дії повідомлень закінчився в клієнті-відправнику через 2 дні або на сервері через 30 днів.\n2. Не вдалося розшифрувати повідомлення, тому що ви або ваш контакт використовували стару резервну копію бази даних.\n3. З'єднання було скомпрометовано."; +/* No comment provided by engineer. */ +"It 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 (%@)." = "Схоже, що ви вже підключені за цим посиланням. Якщо це не так, сталася помилка (%@)."; @@ -1863,7 +2766,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Японський інтерфейс"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Приєднуйтесь"; /* No comment provided by engineer. */ @@ -1872,12 +2775,36 @@ /* No comment provided by engineer. */ "Join group" = "Приєднуйтесь до групи"; +/* No comment provided by engineer. */ +"Join group conversations" = "Приєднуйтесь до групових розмов"; + +/* No comment provided by engineer. */ +"Join group?" = "Приєднатися до групи?"; + /* No comment provided by engineer. */ "Join incognito" = "Приєднуйтесь інкогніто"; +/* No comment provided by engineer. */ +"Join with current profile" = "Приєднатися з поточним профілем"; + +/* No comment provided by engineer. */ +"Join your group?\nThis is your link for group %@!" = "Приєднатися до групи?\nЦе ваше посилання на групу %@!"; + /* No comment provided by engineer. */ "Joining group" = "Приєднання до групи"; +/* alert action */ +"Keep" = "Тримай"; + +/* No comment provided by engineer. */ +"Keep conversation" = "Підтримуйте розмову"; + +/* No comment provided by engineer. */ +"Keep the app open to use it from desktop" = "Тримайте додаток відкритим, щоб використовувати його з робочого столу"; + +/* alert title */ +"Keep unused invitation?" = "Зберігати невикористані запрошення?"; + /* No comment provided by engineer. */ "Keep your connections" = "Зберігайте свої зв'язки"; @@ -1893,9 +2820,15 @@ /* No comment provided by engineer. */ "Learn more" = "Дізнайтеся більше"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Залишити"; +/* No comment provided by engineer. */ +"Leave chat" = "Вийти з чату"; + +/* No comment provided by engineer. */ +"Leave chat?" = "Залишити чат?"; + /* No comment provided by engineer. */ "Leave group" = "Покинути групу"; @@ -1914,6 +2847,15 @@ /* No comment provided by engineer. */ "Limitations" = "Обмеження"; +/* No comment provided by engineer. */ +"Link mobile and desktop apps! 🔗" = "Зв'яжіть мобільні та десктопні додатки! 🔗"; + +/* No comment provided by engineer. */ +"Linked desktop options" = "Параметри пов'язаного робочого столу"; + +/* No comment provided by engineer. */ +"Linked desktops" = "Пов'язані робочі столи"; + /* No comment provided by engineer. */ "LIVE" = "НАЖИВО"; @@ -1935,24 +2877,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Режим блокування"; -/* No comment provided by engineer. */ -"Make a private connection" = "Створіть приватне з'єднання"; - /* No comment provided by engineer. */ "Make one message disappear" = "Зробити так, щоб одне повідомлення зникло"; /* No comment provided by engineer. */ "Make profile private!" = "Зробіть профіль приватним!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Переконайтеся, що адреси серверів %@ мають правильний формат, розділені рядками і не дублюються (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Переконайтеся, що адреси серверів WebRTC ICE мають правильний формат, розділені рядками і не дублюються."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Багато людей запитували: *якщо SimpleX не має ідентифікаторів користувачів, як він може доставляти повідомлення?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Позначити видалено для всіх"; @@ -1971,33 +2904,90 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "Максимум 30 секунд, отримується миттєво."; +/* No comment provided by engineer. */ +"Media & file servers" = "Медіа та файлові сервери"; + +/* blur media */ +"Medium" = "Середній"; + /* member role */ "member" = "учасник"; /* No comment provided by engineer. */ "Member" = "Учасник"; +/* profile update event chat item */ +"member %@ changed to %@" = "учасника %1$@ змінено на %2$@"; + /* rcv group event chat item */ "member connected" = "з'єднаний"; +/* item status text */ +"Member inactive" = "Користувач неактивний"; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All chat members will be notified." = "Роль учасника буде змінено на \"%@\". Усі учасники чату отримають сповіщення."; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Роль учасника буде змінено на \"%@\". Всі учасники групи будуть повідомлені про це."; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Роль учасника буде змінено на \"%@\". Учасник отримає нове запрошення."; +/* No comment provided by engineer. */ +"Member will be removed from chat - this cannot be undone!" = "Учасника буде видалено з чату – це неможливо скасувати!"; + /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Учасник буде видалений з групи - це неможливо скасувати!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Учасники групи можуть додавати реакції на повідомлення."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "Учасники групи можуть безповоротно видаляти надіслані повідомлення. (24 години)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Учасники групи можуть надсилати прямі повідомлення."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Учасники групи можуть надсилати зникаючі повідомлення."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Учасники групи можуть надсилати файли та медіа."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "Учасники групи можуть надсилати посилання SimpleX."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Учасники групи можуть надсилати голосові повідомлення."; + +/* No comment provided by engineer. */ +"Menus" = "Меню"; + +/* No comment provided by engineer. */ +"message" = "повідомлення"; + /* item status text */ "Message delivery error" = "Помилка доставки повідомлення"; /* No comment provided by engineer. */ "Message delivery receipts!" = "Підтвердження доставки повідомлення!"; +/* item status text */ +"Message delivery warning" = "Попередження про доставку повідомлення"; + /* No comment provided by engineer. */ "Message draft" = "Чернетка повідомлення"; +/* item status text */ +"Message forwarded" = "Повідомлення переслано"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Повідомлення може бути доставлене пізніше, якщо користувач стане активним."; + +/* No comment provided by engineer. */ +"Message queue info" = "Інформація про чергу повідомлень"; + /* chat feature */ "Message reactions" = "Реакції на повідомлення"; @@ -2005,23 +2995,83 @@ "Message reactions are prohibited in this chat." = "Реакції на повідомлення в цьому чаті заборонені."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Реакції на повідомлення в цій групі заборонені."; +"Message reactions are prohibited." = "Реакції на повідомлення в цій групі заборонені."; /* notification */ "message received" = "повідомлення отримано"; +/* No comment provided by engineer. */ +"Message reception" = "Прийом повідомлень"; + +/* No comment provided by engineer. */ +"Message servers" = "Сервери повідомлень"; + +/* No comment provided by engineer. */ +"Message shape" = "Форма повідомлення"; + +/* No comment provided by engineer. */ +"Message source remains private." = "Джерело повідомлення залишається приватним."; + +/* No comment provided by engineer. */ +"Message status" = "Статус повідомлення"; + +/* copied message info */ +"Message status: %@" = "Статус повідомлення: %@"; + /* No comment provided by engineer. */ "Message text" = "Текст повідомлення"; +/* No comment provided by engineer. */ +"Message too large" = "Повідомлення занадто велике"; + /* No comment provided by engineer. */ "Messages" = "Повідомлення"; /* No comment provided by engineer. */ "Messages & files" = "Повідомлення та файли"; +/* No comment provided by engineer. */ +"Messages from %@ will be shown!" = "Повідомлення від %@ будуть показані!"; + +/* No comment provided by engineer. */ +"Messages received" = "Отримані повідомлення"; + +/* No comment provided by engineer. */ +"Messages sent" = "Надіслані повідомлення"; + +/* alert message */ +"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." = "Повідомлення, файли та дзвінки захищені **наскрізним шифруванням** з ідеальною секретністю переадресації, відмовою та відновленням після злому."; + +/* 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 device" = "Перенести пристрій"; + +/* No comment provided by engineer. */ +"Migrate from another device" = "Перехід з іншого пристрою"; + +/* No comment provided by engineer. */ +"Migrate here" = "Мігруйте сюди"; + +/* No comment provided by engineer. */ +"Migrate to another device" = "Перехід на інший пристрій"; + +/* No comment provided by engineer. */ +"Migrate to another device via QR code." = "Перейдіть на інший пристрій за допомогою QR-коду."; + +/* No comment provided by engineer. */ +"Migrating" = "Міграція"; + /* No comment provided by engineer. */ "Migrating database archive…" = "Перенесення архіву бази даних…"; +/* No comment provided by engineer. */ +"Migration complete" = "Міграція завершена"; + /* No comment provided by engineer. */ "Migration error:" = "Помилка міграції:"; @@ -2032,7 +3082,7 @@ "Migration is completed" = "Міграцію завершено"; /* No comment provided by engineer. */ -"Migrations: %@" = "Міграції: %@"; +"Migrations:" = "Міграції:"; /* time unit */ "minutes" = "хвилини"; @@ -2052,7 +3102,7 @@ /* copied message info */ "Moderated at: %@" = "Модерується за: %@"; -/* No comment provided by engineer. */ +/* marked deleted chat item preview text */ "moderated by %@" = "модерується %@"; /* time unit */ @@ -2061,36 +3111,60 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Незабаром буде ще більше покращень!"; +/* No comment provided by engineer. */ +"More reliable network connection." = "Більш надійне з'єднання з мережею."; + +/* No comment provided by engineer. */ +"More reliable notifications" = "Більш надійні сповіщення"; + /* item status description */ "Most likely this connection is deleted." = "Швидше за все, це з'єднання видалено."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Швидше за все, цей контакт видалив зв'язок з вами."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Кілька профілів чату"; -/* No comment provided by engineer. */ +/* notification label action */ "Mute" = "Вимкнути звук"; /* No comment provided by engineer. */ "Muted when inactive!" = "Вимкнено, коли неактивний!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Ім'я"; /* No comment provided by engineer. */ "Network & servers" = "Мережа та сервери"; +/* No comment provided by engineer. */ +"Network connection" = "Підключення до мережі"; + +/* No comment provided by engineer. */ +"Network decentralization" = "Децентралізація мережі"; + +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "Проблеми з мережею - термін дії повідомлення закінчився після багатьох спроб надіслати його."; + +/* No comment provided by engineer. */ +"Network management" = "Керування мережею"; + +/* No comment provided by engineer. */ +"Network operator" = "Мережевий оператор"; + /* No comment provided by engineer. */ "Network settings" = "Налаштування мережі"; /* No comment provided by engineer. */ "Network status" = "Стан мережі"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "ніколи"; +/* No comment provided by engineer. */ +"New chat" = "Новий чат"; + +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Новий досвід спілкування в чаті 🎉"; + /* notification */ "New contact request" = "Новий запит на контакт"; @@ -2098,14 +3172,20 @@ "New contact:" = "Новий контакт:"; /* No comment provided by engineer. */ -"New database archive" = "Новий архів бази даних"; +"New desktop app!" = "Новий десктопний додаток!"; /* No comment provided by engineer. */ "New display name" = "Нове ім'я відображення"; +/* notification */ +"New events" = "Нові події"; + /* No comment provided by engineer. */ "New in %@" = "Нове в %@"; +/* No comment provided by engineer. */ +"New media options" = "Нові медіа-опції"; + /* No comment provided by engineer. */ "New member role" = "Нова роль учасника"; @@ -2121,6 +3201,15 @@ /* No comment provided by engineer. */ "New passphrase…" = "Новий пароль…"; +/* No comment provided by engineer. */ +"New server" = "Новий сервер"; + +/* No comment provided by engineer. */ +"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."; + /* pref value */ "no" = "ні"; @@ -2142,6 +3231,9 @@ /* No comment provided by engineer. */ "No device token!" = "Токен пристрою відсутній!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Прямого зв'язку ще немає, повідомлення пересилається адміністратором."; + /* No comment provided by engineer. */ "no e2e encryption" = "без шифрування e2e"; @@ -2154,21 +3246,69 @@ /* No comment provided by engineer. */ "No history" = "Немає історії"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Немає інформації, спробуйте перезавантажити"; + +/* servers error */ +"No media & file servers." = "Ніяких медіа та файлових серверів."; + +/* servers error */ +"No message servers." = "Ніяких серверів повідомлень."; + +/* No comment provided by engineer. */ +"No network connection" = "Немає підключення до мережі"; + +/* No comment provided by engineer. */ +"No permission to record speech" = "Немає дозволу на запис промови"; + +/* No comment provided by engineer. */ +"No permission to record video" = "Немає дозволу на запис відео"; + /* No comment provided by engineer. */ "No permission to record voice message" = "Немає дозволу на запис голосового повідомлення"; +/* No comment provided by engineer. */ +"No push server" = "Локально"; + /* No comment provided by engineer. */ "No received or sent files" = "Немає отриманих або відправлених файлів"; +/* servers error */ +"No servers for private message routing." = "Немає серверів для маршрутизації приватних повідомлень."; + +/* servers error */ +"No servers to receive files." = "Немає серверів для отримання файлів."; + +/* servers error */ +"No servers to receive messages." = "Немає серверів для отримання повідомлень."; + +/* servers error */ +"No servers to send files." = "Немає серверів для надсилання файлів."; + /* copied message info in history */ "no text" = "без тексту"; +/* No comment provided by engineer. */ +"No user identifiers." = "Ніяких ідентифікаторів користувачів."; + +/* No comment provided by engineer. */ +"Not compatible!" = "Не сумісні!"; + +/* No comment provided by engineer. */ +"Nothing selected" = "Нічого не вибрано"; + +/* alert title */ +"Nothing to forward!" = "Нічого пересилати!"; + /* No comment provided by engineer. */ "Notifications" = "Сповіщення"; /* No comment provided by engineer. */ "Notifications are disabled!" = "Сповіщення вимкнено!"; +/* No comment provided by engineer. */ +"Notifications privacy" = "Сповіщення про приватність"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Тепер адміністратори можуть\n- видаляти повідомлення користувачів.\n- відключати користувачів (роль \"спостерігач\")"; @@ -2176,48 +3316,51 @@ "observer" = "спостерігач"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "вимкнено"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Вимкнено"; -/* No comment provided by engineer. */ -"Off (Local)" = "Вимкнено (локально)"; - /* feature offered item */ "offered %@" = "запропоновано %@"; /* feature offered item */ "offered %@: %@" = "запропонував %1$@: %2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "Гаразд"; +/* No comment provided by engineer. */ +"OK" = "ОК"; + /* No comment provided by engineer. */ "Old database" = "Стара база даних"; -/* No comment provided by engineer. */ -"Old database archive" = "Старий архів бази даних"; - /* group pref value */ -"on" = "увімкнено"; +"on" = "увімкненo"; /* No comment provided by engineer. */ "One-time invitation link" = "Посилання на одноразове запрошення"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Для підключення будуть потрібні хости onion. Потрібно увімкнути VPN."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Для підключення будуть потрібні хости onion.\nПотрібно увімкнути VPN."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Onion хости будуть використовуватися, коли вони будуть доступні. Потрібно увімкнути VPN."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Onion хости будуть використовуватися, коли вони будуть доступні.\nПотрібно увімкнути VPN."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion хости не будуть використовуватися."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Тільки клієнтські пристрої зберігають профілі користувачів, контакти, групи та повідомлення, надіслані за допомогою **2-шарового наскрізного шифрування**."; +"Only chat owners can change preferences." = "Лише власники чату можуть змінювати налаштування."; + +/* No comment provided by engineer. */ +"Only client devices store user profiles, contacts, groups, and messages." = "Тільки клієнтські пристрої зберігають профілі користувачів, контакти, групи та повідомлення, надіслані за допомогою **2-шарового наскрізного шифрування**."; + +/* No comment provided by engineer. */ +"Only delete conversation" = "Видаляйте тільки розмови"; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Тільки власники груп можуть змінювати налаштування групи."; @@ -2232,7 +3375,7 @@ "Only you can add message reactions." = "Тільки ви можете додавати реакції на повідомлення."; /* No comment provided by engineer. */ -"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "Тільки ви можете безповоротно видалити повідомлення (ваш контакт може позначити їх для видалення)."; +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Тільки ви можете безповоротно видалити повідомлення (ваш контакт може позначити їх для видалення). (24 години)"; /* No comment provided by engineer. */ "Only you can make calls." = "Дзвонити можете тільки ви."; @@ -2247,7 +3390,7 @@ "Only your contact can add message reactions." = "Тільки ваш контакт може додавати реакції на повідомлення."; /* No comment provided by engineer. */ -"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Тільки ваш контакт може безповоротно видалити повідомлення (ви можете позначити їх для видалення)."; +"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Тільки ваш контакт може безповоротно видалити повідомлення (ви можете позначити їх для видалення). (24 години)"; /* No comment provided by engineer. */ "Only your contact can make calls." = "Тільки ваш контакт може здійснювати дзвінки."; @@ -2258,6 +3401,12 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Тільки ваш контакт може надсилати голосові повідомлення."; +/* alert action */ +"Open" = "Відкрито"; + +/* No comment provided by engineer. */ +"Open changes" = "Відкриті зміни"; + /* No comment provided by engineer. */ "Open chat" = "Відкритий чат"; @@ -2265,20 +3414,62 @@ "Open chat console" = "Відкрийте консоль чату"; /* No comment provided by engineer. */ -"Open Settings" = "Відкрийте Налаштування"; +"Open conditions" = "Відкриті умови"; + +/* No comment provided by engineer. */ +"Open group" = "Відкрита група"; /* authentication reason */ -"Open user profiles" = "Відкрити профілі користувачів"; +"Open migration to another device" = "Відкрита міграція на інший пристрій"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Протокол і код з відкритим вихідним кодом - будь-хто може запускати сервери."; +"Open Settings" = "Відкрийте Налаштування"; /* No comment provided by engineer. */ -"Opening database…" = "Відкриття бази даних…"; +"Opening app…" = "Відкриваємо програму…"; + +/* No comment provided by engineer. */ +"Operator" = "Оператор"; + +/* alert title */ +"Operator server" = "Сервер оператора"; + +/* No comment provided by engineer. */ +"Or import archive file" = "Або імпортуйте архівний файл"; + +/* No comment provided by engineer. */ +"Or paste archive link" = "Або вставте посилання на архів"; + +/* No comment provided by engineer. */ +"Or scan QR code" = "Або відскануйте QR-код"; + +/* No comment provided by engineer. */ +"Or securely share this file link" = "Або безпечно поділіться цим посиланням на файл"; + +/* No comment provided by engineer. */ +"Or show this code" = "Або покажіть цей код"; + +/* No comment provided by engineer. */ +"Or to share privately" = "Або поділитися приватно"; + +/* No comment provided by engineer. */ +"other" = "інший"; + +/* No comment provided by engineer. */ +"Other" = "Інше"; + +/* No comment provided by engineer. */ +"other errors" = "інші помилки"; + +/* alert message */ +"Other file errors:\n%@" = "Інші помилки файлів:\n%@"; /* member role */ "owner" = "власник"; +/* feature role */ +"owners" = "власники"; + /* No comment provided by engineer. */ "Passcode" = "Пароль"; @@ -2295,41 +3486,59 @@ "Passcode set!" = "Пароль встановлено!"; /* No comment provided by engineer. */ -"Password to show" = "Показати пароль"; +"Password" = "Пароль"; /* No comment provided by engineer. */ -"Paste" = "Вставити"; +"Password to show" = "Показати пароль"; + +/* past/unknown group member */ +"Past member %@" = "Колишній учасник %@"; + +/* No comment provided by engineer. */ +"Paste desktop address" = "Вставте адресу робочого столу"; /* No comment provided by engineer. */ "Paste image" = "Вставити зображення"; /* No comment provided by engineer. */ -"Paste received link" = "Вставте отримане посилання"; +"Paste link to connect!" = "Вставте посилання для підключення!"; -/* placeholder */ -"Paste the link you received to connect with your contact." = "Вставте отримане посилання для зв'язку з вашим контактом."; +/* No comment provided by engineer. */ +"Paste the link you received" = "Вставте отримане посилання"; /* No comment provided by engineer. */ "peer-to-peer" = "одноранговий"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Люди можуть зв'язатися з вами лише за посиланнями, якими ви ділитеся."; +"Pending" = "В очікуванні"; /* No comment provided by engineer. */ -"Periodically" = "Періодично"; +"Periodic" = "Періодично"; /* message decrypt error item */ "Permanent decryption error" = "Постійна помилка розшифрування"; +/* No comment provided by engineer. */ +"Picture-in-picture calls" = "Дзвінки \"картинка в картинці"; + /* No comment provided by engineer. */ "PING count" = "Кількість PING"; /* No comment provided by engineer. */ "PING interval" = "Інтервал PING"; +/* No comment provided by engineer. */ +"Play from the chat list." = "Грати зі списку чату."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Будь ласка, попросіть свого контакту ввімкнути дзвінки."; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Будь ласка, попросіть вашого контакту увімкнути відправку голосових повідомлень."; +/* No comment provided by engineer. */ +"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." = "Будь ласка, перевірте, чи ви скористалися правильним посиланням, або попросіть контактну особу надіслати вам інше."; @@ -2339,6 +3548,12 @@ /* No comment provided by engineer. */ "Please check yours and your contact preferences." = "Будь ласка, перевірте свої та контактні налаштування."; +/* No comment provided by engineer. */ +"Please confirm that network settings are correct for this device." = "Переконайтеся, що налаштування мережі для цього пристрою є правильними."; + +/* No comment provided by engineer. */ +"Please contact developers.\nError: %@" = "Зверніться до розробників.\nПомилка: %@"; + /* No comment provided by engineer. */ "Please contact group admin." = "Зверніться до адміністратора групи."; @@ -2366,6 +3581,9 @@ /* No comment provided by engineer. */ "Polish interface" = "Польський інтерфейс"; +/* No comment provided by engineer. */ +"Port" = "Порт"; + /* server test error */ "Possibly, certificate fingerprint in server address is incorrect" = "Можливо, в адресі сервера неправильно вказано відбиток сертифіката"; @@ -2373,33 +3591,60 @@ "Preserve the last message draft, with attachments." = "Зберегти чернетку останнього повідомлення з вкладеннями."; /* No comment provided by engineer. */ -"Preset server" = "Попередньо встановлений сервер"; +"Preset server address" = "Попередньо встановлена адреса сервера"; /* No comment provided by engineer. */ -"Preset server address" = "Попередньо встановлена адреса сервера"; +"Preset servers" = "Попередньо встановлені сервери"; /* No comment provided by engineer. */ "Preview" = "Попередній перегляд"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Раніше підключені сервери"; + /* No comment provided by engineer. */ "Privacy & security" = "Конфіденційність і безпека"; +/* No comment provided by engineer. */ +"Privacy for your customers." = "Конфіденційність для ваших клієнтів."; + /* No comment provided by engineer. */ "Privacy redefined" = "Конфіденційність переглянута"; /* No comment provided by engineer. */ "Private filenames" = "Приватні імена файлів"; +/* No comment provided by engineer. */ +"Private message routing" = "Маршрутизація приватних повідомлень"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Маршрутизація приватних повідомлень 🚀"; + +/* name of notes to self */ +"Private notes" = "Приватні нотатки"; + +/* No comment provided by engineer. */ +"Private routing" = "Приватна маршрутизація"; + +/* No comment provided by engineer. */ +"Private routing error" = "Помилка приватної маршрутизації"; + /* No comment provided by engineer. */ "Profile and server connections" = "З'єднання профілю та сервера"; /* No comment provided by engineer. */ "Profile image" = "Зображення профілю"; +/* No comment provided by engineer. */ +"Profile images" = "Зображення профілю"; + /* No comment provided by engineer. */ "Profile password" = "Пароль до профілю"; /* No comment provided by engineer. */ +"Profile theme" = "Тема профілю"; + +/* alert message */ "Profile update will be sent to your contacts." = "Оновлення профілю буде надіслано вашим контактам."; /* No comment provided by engineer. */ @@ -2423,15 +3668,24 @@ /* 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." = "Заборонити надсилання голосових повідомлень."; /* No comment provided by engineer. */ "Protect app screen" = "Захистіть екран програми"; +/* No comment provided by engineer. */ +"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Увімкніть у налаштуваннях *Мережа та сервери*."; + /* No comment provided by engineer. */ "Protocol timeout" = "Тайм-аут протоколу"; @@ -2439,22 +3693,46 @@ "Protocol timeout per KB" = "Тайм-аут протоколу на КБ"; /* No comment provided by engineer. */ -"Push notifications" = "Push-повідомлення"; +"Proxied" = "Проксі-сервер"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Проксі-сервери"; + +/* No comment provided by engineer. */ +"Proxy requires password" = "Проксі вимагає пароль"; + +/* No comment provided by engineer. */ +"Push notifications" = "Push-сповіщення"; + +/* No comment provided by engineer. */ +"Push server" = "Push-сервер"; + +/* chat item text */ +"quantum resistant e2e encryption" = "квантово-стійке шифрування e2e"; + +/* No comment provided by engineer. */ +"Quantum resistant encryption" = "Квантово-стійке шифрування"; /* No comment provided by engineer. */ "Rate the app" = "Оцініть додаток"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Доступна панель інструментів чату"; + /* chat item menu */ "React…" = "Реагуй…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Читати"; /* No comment provided by engineer. */ "Read more" = "Читати далі"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"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)."; + +/* 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)."; @@ -2463,10 +3741,10 @@ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Читайте більше в нашому [GitHub репозиторії](https://github.com/simplex-chat/simplex-chat#readme)."; /* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Читайте більше в нашому репозиторії на GitHub."; +"Receipts are disabled" = "Підтвердження виключені"; /* No comment provided by engineer. */ -"Receipts are disabled" = "Підтвердження виключені"; +"Receive errors" = "Отримання помилок"; /* No comment provided by engineer. */ "received answer…" = "отримали відповідь…"; @@ -2486,6 +3764,15 @@ /* message info title */ "Received message" = "Отримано повідомлення"; +/* No comment provided by engineer. */ +"Received messages" = "Отримані повідомлення"; + +/* No comment provided by engineer. */ +"Received reply" = "Отримано відповідь"; + +/* No comment provided by engineer. */ +"Received total" = "Отримано всього"; + /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Адреса отримувача буде змінена на інший сервер. Зміна адреси завершиться після того, як відправник з'явиться в мережі."; @@ -2495,12 +3782,33 @@ /* No comment provided by engineer. */ "Receiving via" = "Отримання через"; +/* No comment provided by engineer. */ +"Recent history and improved [directory bot](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)." = "Нещодавня історія та покращення [directory bot](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)."; + +/* No comment provided by engineer. */ +"Recipient(s) can't see who this message is from." = "Одержувач(и) не бачить, від кого це повідомлення."; + /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Одержувачі бачать оновлення, коли ви їх вводите."; +/* No comment provided by engineer. */ +"Reconnect" = "Повторне підключення"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Перепідключіть всі підключені сервери, щоб примусово доставити повідомлення. Це використовує додатковий трафік."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Перепідключіть усі сервери"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Перепідключити всі сервери?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Перепідключити сервер для примусової доставки повідомлень. Використовує додатковий трафік."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Перепідключити сервер?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Перепідключити сервери?"; @@ -2513,7 +3821,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Зменшення використання акумулятора"; -/* reject incoming call via notification */ +/* reject incoming call via notification +swipe action */ "Reject" = "Відхилити"; /* No comment provided by engineer. */ @@ -2534,6 +3843,12 @@ /* No comment provided by engineer. */ "Remove" = "Видалити"; +/* No comment provided by engineer. */ +"Remove archive?" = "Видалити архів?"; + +/* No comment provided by engineer. */ +"Remove image" = "Видалити зображення"; + /* No comment provided by engineer. */ "Remove member" = "Видалити учасника"; @@ -2549,6 +3864,12 @@ /* rcv group event chat item */ "removed %@" = "видалено %@"; +/* profile update event chat item */ +"removed contact address" = "видалено контактну адресу"; + +/* profile update event chat item */ +"removed profile picture" = "видалено зображення профілю"; + /* rcv group event chat item */ "removed you" = "прибрали вас"; @@ -2561,21 +3882,54 @@ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Переузгодьте шифрування?"; +/* No comment provided by engineer. */ +"Repeat connection request?" = "Повторити запит на підключення?"; + +/* No comment provided by engineer. */ +"Repeat download" = "Повторити завантаження"; + +/* No comment provided by engineer. */ +"Repeat import" = "Повторний імпорт"; + +/* No comment provided by engineer. */ +"Repeat join request?" = "Повторити запит на приєднання?"; + +/* No comment provided by engineer. */ +"Repeat upload" = "Повторне завантаження"; + /* chat item action */ "Reply" = "Відповісти"; +/* chat list item title */ +"requested to connect" = "запит на підключення"; + /* No comment provided by engineer. */ "Required" = "Потрібно"; /* No comment provided by engineer. */ "Reset" = "Перезавантаження"; +/* No comment provided by engineer. */ +"Reset all hints" = "Скинути всі підказки"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Скинути всю статистику"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Скинути всю статистику?"; + /* No comment provided by engineer. */ "Reset colors" = "Скинути кольори"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Повернутися до теми програми"; + /* No comment provided by engineer. */ "Reset to defaults" = "Відновити налаштування за замовчуванням"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Повернутися до теми користувача"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Перезапустіть програму, щоб створити новий профіль чату"; @@ -2594,11 +3948,14 @@ /* No comment provided by engineer. */ "Restore database error" = "Відновлення помилки бази даних"; +/* No comment provided by engineer. */ +"Retry" = "Спробуйте ще раз"; + /* chat item action */ "Reveal" = "Показувати"; /* No comment provided by engineer. */ -"Revert" = "Повернутися"; +"Review conditions" = "Умови перегляду"; /* No comment provided by engineer. */ "Revoke" = "Відкликати"; @@ -2615,27 +3972,31 @@ /* No comment provided by engineer. */ "Run chat" = "Запустити чат"; -/* chat item action */ +/* No comment provided by engineer. */ +"Safely receive files" = "Безпечне отримання файлів"; + +/* No comment provided by engineer. */ +"Safer groups" = "Безпечніші групи"; + +/* alert button +chat item action */ "Save" = "Зберегти"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Зберегти (і повідомити контактам)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Зберегти та повідомити контакт"; /* No comment provided by engineer. */ "Save and notify group members" = "Зберегти та повідомити учасників групи"; +/* No comment provided by engineer. */ +"Save and reconnect" = "Збережіть і підключіться знову"; + /* No comment provided by engineer. */ "Save and update group profile" = "Збереження та оновлення профілю групи"; -/* No comment provided by engineer. */ -"Save archive" = "Зберегти архів"; - -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Зберегти налаштування автоприйому"; - /* No comment provided by engineer. */ "Save group profile" = "Зберегти профіль групи"; @@ -2645,8 +4006,8 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Збережіть парольну фразу в Keychain"; -/* No comment provided by engineer. */ -"Save preferences?" = "Зберегти налаштування?"; +/* alert title */ +"Save preferences?" = "Зберегти настройки?"; /* No comment provided by engineer. */ "Save profile password" = "Зберегти пароль профілю"; @@ -2654,36 +4015,75 @@ /* No comment provided by engineer. */ "Save servers" = "Зберегти сервери"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Зберегти сервери?"; -/* No comment provided by engineer. */ -"Save settings?" = "Зберегти налаштування?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Зберегти вітальне повідомлення?"; +/* alert title */ +"Save your profile?" = "Зберегти свій профіль?"; + +/* No comment provided by engineer. */ +"saved" = "збережено"; + +/* No comment provided by engineer. */ +"Saved" = "Збережено"; + +/* No comment provided by engineer. */ +"Saved from" = "Збережено з"; + +/* No comment provided by engineer. */ +"saved from %@" = "збережено з %@"; + +/* message info title */ +"Saved message" = "Збережене повідомлення"; + /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Збережені сервери WebRTC ICE буде видалено"; +/* No comment provided by engineer. */ +"Saving %lld messages" = "Збереження повідомлень %lld"; + +/* No comment provided by engineer. */ +"Scale" = "Масштаб"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Відсканувати / Вставити посилання"; + /* No comment provided by engineer. */ "Scan code" = "Сканувати код"; /* No comment provided by engineer. */ "Scan QR code" = "Відскануйте QR-код"; +/* No comment provided by engineer. */ +"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-код сервера"; +/* No comment provided by engineer. */ +"search" = "пошук"; + /* No comment provided by engineer. */ "Search" = "Пошук"; +/* No comment provided by engineer. */ +"Search bar accepts invitation links." = "Рядок пошуку приймає посилання-запрошення."; + +/* No comment provided by engineer. */ +"Search or paste SimpleX link" = "Знайдіть або вставте посилання SimpleX"; + /* network option */ "sec" = "сек"; +/* No comment provided by engineer. */ +"Secondary" = "Вторинний"; + /* time unit */ "seconds" = "секунди"; @@ -2693,6 +4093,9 @@ /* server test step */ "Secure queue" = "Безпечна черга"; +/* No comment provided by engineer. */ +"Secured" = "Забезпечено"; + /* No comment provided by engineer. */ "Security assessment" = "Оцінка безпеки"; @@ -2702,9 +4105,18 @@ /* chat item text */ "security code changed" = "змінено код безпеки"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Виберіть"; +/* No comment provided by engineer. */ +"Select chat profile" = "Виберіть профіль чату"; + +/* No comment provided by engineer. */ +"Selected %lld" = "Вибрано %lld"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Вибрані налаштування чату забороняють це повідомлення."; + /* No comment provided by engineer. */ "Self-destruct" = "Самознищення"; @@ -2727,11 +4139,17 @@ "Send delivery receipts to" = "Надсилання звітів про доставку"; /* No comment provided by engineer. */ -"Send direct message" = "Надішліть пряме повідомлення"; +"send direct message" = "надіслати пряме повідомлення"; + +/* No comment provided by engineer. */ +"Send direct message to connect" = "Надішліть пряме повідомлення, щоб підключитися"; /* No comment provided by engineer. */ "Send disappearing message" = "Надіслати зникаюче повідомлення"; +/* No comment provided by engineer. */ +"Send errors" = "Помилки надсилання"; + /* No comment provided by engineer. */ "Send link previews" = "Надіслати попередній перегляд за посиланням"; @@ -2739,10 +4157,16 @@ "Send live message" = "Надіслати живе повідомлення"; /* No comment provided by engineer. */ -"Send notifications" = "Надсилати сповіщення"; +"Send message to enable calls." = "Надішліть повідомлення, щоб увімкнути дзвінки."; /* No comment provided by engineer. */ -"Send notifications:" = "Надсилати сповіщення:"; +"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." = "Надсилайте повідомлення напряму, якщо ваш сервер або сервер призначення не підтримує приватну маршрутизацію."; + +/* No comment provided by engineer. */ +"Send notifications" = "Надсилати сповіщення"; /* No comment provided by engineer. */ "Send questions and ideas" = "Надсилайте запитання та ідеї"; @@ -2754,6 +4178,9 @@ "Send them from gallery or custom keyboards." = "Надсилайте їх із галереї чи власних клавіатур."; /* No comment provided by engineer. */ +"Send up to 100 last messages to new members." = "Надішліть до 100 останніх повідомлень новим користувачам."; + +/* alert message */ "Sender cancelled file transfer." = "Відправник скасував передачу файлу."; /* No comment provided by engineer. */ @@ -2789,15 +4216,57 @@ /* copied message info */ "Sent at: %@" = "Надіслано за: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Відправлено напряму"; + /* notification */ "Sent file event" = "Подія надісланого файлу"; /* message info title */ "Sent message" = "Надіслано повідомлення"; +/* No comment provided by engineer. */ +"Sent messages" = "Надіслані повідомлення"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Надіслані повідомлення будуть видалені через встановлений час."; +/* No comment provided by engineer. */ +"Sent reply" = "Надіслано відповідь"; + +/* No comment provided by engineer. */ +"Sent total" = "Відправлено всього"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Відправлено через проксі"; + +/* No comment provided by engineer. */ +"Server" = "Сервер"; + +/* alert message */ +"Server added to operator %@." = "Сервер додано до оператора %@."; + +/* No comment provided by engineer. */ +"Server address" = "Адреса сервера"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "Адреса сервера несумісна з налаштуваннями мережі: %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "Адреса сервера несумісна з налаштуваннями мережі."; + +/* alert title */ +"Server operator changed." = "Оператор сервера змінився."; + +/* No comment provided by engineer. */ +"Server operators" = "Оператори серверів"; + +/* alert title */ +"Server protocol changed." = "Протокол сервера змінено."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "інформація про чергу на сервері: %1$@\n\nостаннє отримане повідомлення: %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "Сервер вимагає авторизації для створення черг, перевірте пароль"; @@ -2807,24 +4276,54 @@ /* No comment provided by engineer. */ "Server test failed!" = "Тест сервера завершився невдало!"; +/* No comment provided by engineer. */ +"Server type" = "Тип сервера"; + +/* srv error text */ +"Server version is incompatible with network settings." = "Серверна версія несумісна з мережевими налаштуваннями."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "Версія сервера несумісна з вашим додатком: %@."; + /* No comment provided by engineer. */ "Servers" = "Сервери"; +/* No comment provided by engineer. */ +"Servers info" = "Інформація про сервери"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Статистика серверів буде скинута - це неможливо скасувати!"; + +/* No comment provided by engineer. */ +"Session code" = "Код сесії"; + /* No comment provided by engineer. */ "Set 1 day" = "Встановити 1 день"; /* No comment provided by engineer. */ "Set contact name…" = "Встановити ім'я контакту…"; +/* No comment provided by engineer. */ +"Set default theme" = "Встановлення теми за замовчуванням"; + /* No comment provided by engineer. */ "Set group preferences" = "Встановіть налаштування групи"; /* No comment provided by engineer. */ "Set it instead of system authentication." = "Встановіть його замість аутентифікації системи."; +/* profile update event chat item */ +"set new contact address" = "встановити нову контактну адресу"; + +/* profile update event chat item */ +"set new profile picture" = "встановити нове зображення профілю"; + /* No comment provided by engineer. */ "Set passcode" = "Встановити пароль"; +/* No comment provided by engineer. */ +"Set passphrase" = "Встановити парольну фразу"; + /* No comment provided by engineer. */ "Set passphrase to export" = "Встановити ключову фразу для експорту"; @@ -2837,27 +4336,55 @@ /* No comment provided by engineer. */ "Settings" = "Налаштування"; -/* chat item action */ +/* alert message */ +"Settings were changed." = "Налаштування були змінені."; + +/* No comment provided by engineer. */ +"Shape profile images" = "Сформуйте зображення профілю"; + +/* alert action +chat item action */ "Share" = "Поділіться"; /* No comment provided by engineer. */ "Share 1-time link" = "Поділитися 1-разовим посиланням"; +/* No comment provided by engineer. */ +"Share 1-time link with a friend" = "Поділіться одноразовим посиланням з другом"; + /* No comment provided by engineer. */ "Share address" = "Поділитися адресою"; /* No comment provided by engineer. */ +"Share address publicly" = "Поділіться адресою публічно"; + +/* alert title */ "Share address with contacts?" = "Поділіться адресою з контактами?"; +/* No comment provided by engineer. */ +"Share from other apps." = "Діліться з інших програм."; + /* No comment provided by engineer. */ "Share link" = "Поділіться посиланням"; /* No comment provided by engineer. */ -"Share one-time invitation link" = "Поділіться посиланням на одноразове запрошення"; +"Share profile" = "Поділіться профілем"; + +/* No comment provided by engineer. */ +"Share SimpleX address on social media." = "Поділіться адресою SimpleX у соціальних мережах."; + +/* No comment provided by engineer. */ +"Share this 1-time invite link" = "Поділіться цим одноразовим посиланням-запрошенням"; + +/* No comment provided by engineer. */ +"Share to SimpleX" = "Поділіться з SimpleX"; /* No comment provided by engineer. */ "Share with contacts" = "Поділіться з контактами"; +/* No comment provided by engineer. */ +"Show → on messages sent via private routing." = "Показувати → у повідомленнях, надісланих через приватну маршрутизацію."; + /* No comment provided by engineer. */ "Show calls in phone history" = "Показувати дзвінки в історії дзвінків"; @@ -2867,18 +4394,39 @@ /* No comment provided by engineer. */ "Show last messages" = "Показати останні повідомлення"; +/* No comment provided by engineer. */ +"Show message status" = "Показати статус повідомлення"; + +/* No comment provided by engineer. */ +"Show percentage" = "Показати відсоток"; + /* No comment provided by engineer. */ "Show preview" = "Показати попередній перегляд"; +/* No comment provided by engineer. */ +"Show QR code" = "Показати QR-код"; + /* No comment provided by engineer. */ "Show:" = "Показати:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "Адреса SimpleX"; /* No comment provided by engineer. */ "SimpleX Address" = "Адреса SimpleX"; +/* No comment provided by engineer. */ +"SimpleX address and 1-time links are safe to share via any messenger." = "SimpleX-адреси та одноразові посилання можна безпечно ділитися через будь-який месенджер."; + +/* No comment provided by engineer. */ +"SimpleX address or 1-time link?" = "SimpleX адреса або одноразове посилання?"; + +/* No comment provided by engineer. */ +"SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "SimpleX Chat і Flux уклали угоду про включення серверів, керованих Flux, у додаток."; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "Безпека SimpleX Chat була перевірена компанією Trail of Bits."; @@ -2891,9 +4439,15 @@ /* simplex link type */ "SimpleX group link" = "Посилання на групу SimpleX"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "Посилання SimpleX"; +/* No comment provided by engineer. */ +"SimpleX links are prohibited." = "У цій групі заборонені посилання на SimpleX."; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "Посилання SimpleX заборонені"; + /* No comment provided by engineer. */ "SimpleX Lock" = "SimpleX Lock"; @@ -2909,6 +4463,15 @@ /* simplex link type */ "SimpleX one-time invitation" = "Одноразове запрошення SimpleX"; +/* No comment provided by engineer. */ +"SimpleX protocols reviewed by Trail of Bits." = "Протоколи SimpleX, розглянуті Trail of Bits."; + +/* No comment provided by engineer. */ +"Simplified incognito mode" = "Спрощений режим інкогніто"; + +/* No comment provided by engineer. */ +"Size" = "Розмір"; + /* No comment provided by engineer. */ "Skip" = "Пропустити"; @@ -2919,31 +4482,61 @@ "Small groups (max 20)" = "Невеликі групи (максимум 20 осіб)"; /* No comment provided by engineer. */ -"SMP servers" = "Сервери SMP"; +"SMP server" = "Сервер SMP"; + +/* No comment provided by engineer. */ +"SOCKS proxy" = "Проксі SOCKS"; + +/* blur media */ +"Soft" = "М'який"; + +/* No comment provided by engineer. */ +"Some app settings were not migrated." = "Деякі налаштування програми не були перенесені."; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "Деякі файли не було експортовано:"; /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Під час імпорту виникли деякі нефатальні помилки – ви можете переглянути консоль чату, щоб дізнатися більше."; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "Під час імпорту виникли деякі несмертельні помилки:"; + +/* alert message */ +"Some servers failed the test:\n%@" = "Деякі сервери не пройшли тестування:\n%@"; + /* notification title */ "Somebody" = "Хтось"; /* No comment provided by engineer. */ -"Start a new chat" = "Почніть новий чат"; +"Square, circle, or anything in between." = "Квадрат, коло або щось середнє між ними."; + +/* chat item text */ +"standard end-to-end encryption" = "стандартне наскрізне шифрування"; /* No comment provided by engineer. */ "Start chat" = "Почати чат"; +/* No comment provided by engineer. */ +"Start chat?" = "Почати чат?"; + /* No comment provided by engineer. */ "Start migration" = "Почати міграцію"; +/* No comment provided by engineer. */ +"Starting from %@." = "Починаючи з %@."; + /* No comment provided by engineer. */ "starting…" = "починаючи…"; +/* No comment provided by engineer. */ +"Statistics" = "Статистика"; + /* No comment provided by engineer. */ "Stop" = "Зупинити"; /* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Зупиніть чат, щоб увімкнути дії з базою даних"; +"Stop chat" = "Припинити чат"; /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Зупиніть чат, щоб експортувати, імпортувати або видалити базу даних чату. Ви не зможете отримувати та надсилати повідомлення, поки чат зупинено."; @@ -2960,39 +4553,69 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Припинити надсилання файлу?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Припиніть ділитися"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Припинити ділитися адресою?"; /* authentication reason */ "Stop SimpleX" = "Зупинити SimpleX"; +/* No comment provided by engineer. */ +"Stopping chat" = "Зупинка чату"; + /* No comment provided by engineer. */ "strike" = "закреслено"; +/* blur media */ +"Strong" = "Сильний"; + /* No comment provided by engineer. */ "Submit" = "Надіслати"; +/* No comment provided by engineer. */ +"Subscribed" = "Підписано"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Помилки підписки"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Підписки ігноруються"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Підтримка чату SimpleX"; +/* No comment provided by engineer. */ +"Switch audio and video during the call." = "Перемикайте аудіо та відео під час дзвінка."; + +/* No comment provided by engineer. */ +"Switch chat profile for 1-time invitations." = "Переключіть профіль чату для отримання одноразових запрошень."; + /* No comment provided by engineer. */ "System" = "Система"; /* No comment provided by engineer. */ "System authentication" = "Автентифікація системи"; +/* No comment provided by engineer. */ +"Tail" = "Хвіст"; + /* No comment provided by engineer. */ "Take picture" = "Сфотографуйте"; /* No comment provided by engineer. */ "Tap button " = "Натисніть кнопку "; +/* No comment provided by engineer. */ +"Tap Create SimpleX address in the menu to create it later." = "Натисніть «Створити адресу SimpleX» у меню, щоб створити її пізніше."; + /* No comment provided by engineer. */ "Tap to activate profile." = "Натисніть, щоб активувати профіль."; +/* No comment provided by engineer. */ +"Tap to Connect" = "Натисніть, щоб підключитися"; + /* No comment provided by engineer. */ "Tap to join" = "Натисніть, щоб приєднатися"; @@ -3000,7 +4623,13 @@ "Tap to join incognito" = "Натисніть, щоб приєднатися інкогніто"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Натисніть, щоб почати новий чат"; +"Tap to paste link" = "Натисніть, щоб вставити посилання"; + +/* No comment provided by engineer. */ +"Tap to scan" = "Натисніть, щоб сканувати"; + +/* No comment provided by engineer. */ +"TCP connection" = "TCP-з'єднання"; /* No comment provided by engineer. */ "TCP connection timeout" = "Тайм-аут TCP-з'єднання"; @@ -3014,6 +4643,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* file error alert title */ +"Temporary file error" = "Тимчасова помилка файлу"; + /* server test failure */ "Test failed at step %@." = "Тест завершився невдало на кроці %@."; @@ -3023,7 +4655,7 @@ /* No comment provided by engineer. */ "Test servers" = "Тестові сервери"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Тести не пройшли!"; /* No comment provided by engineer. */ @@ -3035,15 +4667,24 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Дякуємо користувачам - зробіть свій внесок через Weblate!"; -/* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Перша платформа без жодних ідентифікаторів користувачів – приватна за дизайном."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Додаток може сповіщати вас, коли ви отримуєте повідомлення або запити на контакт - будь ласка, відкрийте налаштування, щоб увімкнути цю функцію."; +/* No comment provided by engineer. */ +"The app protects your privacy by using different operators in each conversation." = "Додаток захищає вашу конфіденційність, використовуючи різних операторів у кожній розмові."; + +/* No comment provided by engineer. */ +"The app will ask to confirm downloads from unknown file servers (except .onion)." = "Програма попросить підтвердити завантаження з невідомих файлових серверів (крім .onion)."; + /* No comment provided by engineer. */ "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."; + +/* No comment provided by engineer. */ +"The connection reached the limit of undelivered messages, your contact may be offline." = "З'єднання досягло ліміту недоставлених повідомлень, ваш контакт може бути офлайн."; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "Прийняте вами з'єднання буде скасовано!"; @@ -3056,6 +4697,9 @@ /* 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." = "Хеш попереднього повідомлення відрізняється."; @@ -3069,13 +4713,22 @@ "The message will be marked as moderated for all members." = "Повідомлення буде позначено як модероване для всіх учасників."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "Наступне покоління приватних повідомлень"; +"The messages will be deleted for all members." = "Повідомлення будуть видалені для всіх учасників."; + +/* No comment provided by engineer. */ +"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 profile is only shared with your contacts." = "Профіль доступний лише вашим контактам."; +"Your profile is stored on your device and only shared with your contacts." = "Профіль доступний лише вашим контактам."; + +/* No comment provided by engineer. */ +"The same conditions will apply to operator **%@**." = "Такі ж умови діятимуть і для оператора **%@**."; + +/* No comment provided by engineer. */ +"The second preset operator in the app!" = "Другий попередньо встановлений оператор у застосунку!"; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Другу галочку ми пропустили! ✅"; @@ -3087,13 +4740,19 @@ "The servers for new connections of your current chat profile **%@**." = "Сервери для нових підключень вашого поточного профілю чату **%@**."; /* No comment provided by engineer. */ -"Theme" = "Тема"; +"The servers for new files of your current chat profile **%@**." = "Сервери для нових файлів вашого поточного профілю чату **%@**."; /* No comment provided by engineer. */ -"There should be at least one user profile." = "Повинен бути принаймні один профіль користувача."; +"The text you pasted is not a SimpleX link." = "Текст, який ви вставили, не є посиланням SimpleX."; /* No comment provided by engineer. */ -"There should be at least one visible user profile." = "Повинен бути принаймні один видимий профіль користувача."; +"The uploaded database archive will be permanently removed from the servers." = "Завантажений архів бази даних буде назавжди видалено з серверів."; + +/* No comment provided by engineer. */ +"Themes" = "Теми"; + +/* No comment provided by engineer. */ +"These conditions will also apply for: **%@**." = "Ці умови також поширюються на: **%@**."; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Ці налаштування стосуються вашого поточного профілю **%@**."; @@ -3110,29 +4769,56 @@ /* No comment provided by engineer. */ "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." = "Цей чат захищений наскрізним шифруванням."; + +/* E2EE info chat item */ +"This chat is protected by quantum resistant end-to-end encryption." = "Цей чат захищений квантово-стійким наскрізним шифруванням."; + /* notification title */ "this contact" = "цей контакт"; +/* No comment provided by engineer. */ +"This device name" = "Це ім'я пристрою"; + +/* No comment provided by engineer. */ +"This display name is invalid. Please choose another name." = "Це ім'я для відображення є недійсним. Будь ласка, виберіть інше ім'я."; + /* No comment provided by engineer. */ "This group has over %lld members, delivery receipts are not sent." = "У цій групі більше %lld учасників, підтвердження доставки не надсилаються."; /* No comment provided by engineer. */ "This group no longer exists." = "Цієї групи більше не існує."; +/* No comment provided by engineer. */ +"This is your own one-time link!" = "Це ваше власне одноразове посилання!"; + +/* No comment provided by engineer. */ +"This is your own SimpleX address!" = "Це ваша власна SimpleX-адреса!"; + +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Це посилання було використано з іншого мобільного пристрою, будь ласка, створіть нове посилання на робочому столі."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Це налаштування застосовується до повідомлень у вашому поточному профілі чату **%@**."; +/* No comment provided by engineer. */ +"Title" = "Заголовок"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Задати будь-які питання та отримувати новини:"; /* No comment provided by engineer. */ "To connect, your contact can scan QR code or use the link in the app." = "Щоб підключитися, ваш контакт може відсканувати QR-код або скористатися посиланням у додатку."; +/* No comment provided by engineer. */ +"To hide unwanted messages." = "Приховати небажані повідомлення."; + /* No comment provided by engineer. */ "To make a new connection" = "Щоб створити нове з'єднання"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Щоб захистити конфіденційність, замість ідентифікаторів користувачів, які використовуються на всіх інших платформах, SimpleX має ідентифікатори для черг повідомлень, окремі для кожного з ваших контактів."; +"To protect against your link being replaced, you can compare contact security codes." = "Щоб захиститися від заміни вашого посилання, ви можете порівняти коди безпеки контактів."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Для захисту часового поясу у файлах зображень/голосу використовується UTC."; @@ -3140,21 +4826,57 @@ /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Щоб захистити вашу інформацію, увімкніть SimpleX Lock.\nПеред увімкненням цієї функції вам буде запропоновано пройти автентифікацію."; +/* No comment provided by engineer. */ +"To 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 має ідентифікатори для черг повідомлень, окремі для кожного з ваших контактів."; + +/* No comment provided by engineer. */ +"To receive" = "Щоб отримати"; + +/* No comment provided by engineer. */ +"To record speech please grant permission to use Microphone." = "Для запису промови, будь ласка, надайте дозвіл на використання мікрофону."; + +/* No comment provided by engineer. */ +"To record video please grant permission to use Camera." = "Для запису відео, будь ласка, надайте дозвіл на використання камери."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Щоб записати голосове повідомлення, будь ласка, надайте дозвіл на використання мікрофону."; /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Щоб відкрити свій прихований профіль, введіть повний пароль у поле пошуку на сторінці **Ваші профілі чату**."; +/* No comment provided by engineer. */ +"To send" = "Щоб відправити"; + /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Для підтримки миттєвих push-повідомлень необхідно перенести базу даних чату."; +/* No comment provided by engineer. */ +"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." = "Щоб перевірити наскрізне шифрування з вашим контактом, порівняйте (або відскануйте) код на ваших пристроях."; +/* No comment provided by engineer. */ +"Toggle chat list:" = "Перемикання списку чату:"; + +/* No comment provided by engineer. */ +"Toggle incognito when connecting." = "Увімкніть інкогніто при підключенні."; + +/* No comment provided by engineer. */ +"Toolbar opacity" = "Непрозорість панелі інструментів"; + +/* No comment provided by engineer. */ +"Total" = "Всього"; + /* No comment provided by engineer. */ "Transport isolation" = "Транспортна ізоляція"; +/* No comment provided by engineer. */ +"Transport sessions" = "Транспортні сесії"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Спроба з'єднатися з сервером, який використовується для отримання повідомлень від цього контакту (помилка: %@)."; @@ -3162,10 +4884,10 @@ "Trying to connect to the server used to receive messages from this contact." = "Спроба з'єднатися з сервером, який використовується для отримання повідомлень від цього контакту."; /* No comment provided by engineer. */ -"Turn off" = "Вимкнути"; +"Turkish interface" = "Турецький інтерфейс"; /* No comment provided by engineer. */ -"Turn off notifications?" = "Вимкнути сповіщення?"; +"Turn off" = "Вимкнути"; /* No comment provided by engineer. */ "Turn on" = "Ввімкнути"; @@ -3173,13 +4895,31 @@ /* No comment provided by engineer. */ "Unable to record voice message" = "Не вдається записати голосове повідомлення"; -/* item status description */ -"Unexpected error: %@" = "Неочікувана помилка: %@"; +/* No comment provided by engineer. */ +"Unblock" = "Розблoкувати"; + +/* No comment provided by engineer. */ +"Unblock for all" = "Розблокування для всіх"; + +/* No comment provided by engineer. */ +"Unblock member" = "Розблокувати учасника"; + +/* No comment provided by engineer. */ +"Unblock member for all?" = "Розблокувати учасника для всіх?"; + +/* No comment provided by engineer. */ +"Unblock member?" = "Розблокувати учасника?"; + +/* rcv group event chat item */ +"unblocked %@" = "розблоковано %@"; + +/* No comment provided by engineer. */ +"Undelivered messages" = "Недоставлені повідомлення"; /* No comment provided by engineer. */ "Unexpected migration state" = "Неочікуваний стан міграції"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Нелюб."; /* No comment provided by engineer. */ @@ -3206,29 +4946,47 @@ /* No comment provided by engineer. */ "Unknown error" = "Невідома помилка"; +/* No comment provided by engineer. */ +"unknown servers" = "невідомі реле"; + +/* alert title */ +"Unknown servers!" = "Невідомі сервери!"; + +/* No comment provided by engineer. */ +"unknown status" = "невідомий статус"; + /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Якщо ви не користуєтеся інтерфейсом виклику iOS, увімкніть режим \"Не турбувати\", щоб уникнути переривань."; /* No comment provided by engineer. */ "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Якщо ваш контакт не видалив з'єднання або якщо це посилання вже використовувалося, це може бути помилкою - будь ласка, повідомте про це.\nЩоб підключитися, попросіть вашого контакта створити інше посилання і перевірте, чи маєте ви стабільне з'єднання з мережею."; +/* No comment provided by engineer. */ +"Unlink" = "Роз'єднати зв'язок"; + +/* No comment provided by engineer. */ +"Unlink desktop?" = "Від'єднати робочий стіл?"; + /* No comment provided by engineer. */ "Unlock" = "Розблокувати"; /* authentication reason */ "Unlock app" = "Розблокувати додаток"; -/* No comment provided by engineer. */ +/* notification label action */ "Unmute" = "Увімкнути звук"; /* No comment provided by engineer. */ +"unprotected" = "незахищені"; + +/* swipe action */ "Unread" = "Непрочитане"; /* No comment provided by engineer. */ -"Update" = "Оновлення"; +"Up to 100 last messages are sent to new members." = "Новим користувачам надсилається до 100 останніх повідомлень."; /* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Оновити налаштування хостів .onion?"; +"Update" = "Оновлення"; /* No comment provided by engineer. */ "Update database passphrase" = "Оновити парольну фразу бази даних"; @@ -3237,62 +4995,128 @@ "Update network settings?" = "Оновити налаштування мережі?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Оновити режим транспортної ізоляції?"; +"Update settings?" = "Оновити налаштування?"; /* rcv group event chat item */ "updated group profile" = "оновлений профіль групи"; +/* profile update event chat item */ +"updated profile" = "оновлений профіль"; + /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "Оновлення налаштувань призведе до перепідключення клієнта до всіх серверів."; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Оновлення цього параметра призведе до перепідключення клієнта до всіх серверів."; +"Upgrade and open chat" = "Оновлення та відкритий чат"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "Оновлення та відкритий чат"; +"Upload errors" = "Помилки завантаження"; + +/* No comment provided by engineer. */ +"Upload failed" = "Не вдалося завантфжити"; /* server test step */ "Upload file" = "Завантажити файл"; +/* No comment provided by engineer. */ +"Uploaded" = "Завантажено"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Завантажені файли"; + +/* No comment provided by engineer. */ +"Uploading archive" = "Завантаження архіву"; + /* No comment provided by engineer. */ "Use .onion hosts" = "Використовуйте хости .onion"; +/* No comment provided by engineer. */ +"Use %@" = "Використовуйте %@"; + /* No comment provided by engineer. */ "Use chat" = "Використовуйте чат"; /* No comment provided by engineer. */ "Use current profile" = "Використовувати поточний профіль"; +/* No comment provided by engineer. */ +"Use for files" = "Використовуйте для файлів"; + +/* No comment provided by engineer. */ +"Use for messages" = "Використовуйте для повідомлень"; + /* No comment provided by engineer. */ "Use for new connections" = "Використовуйте для нових з'єднань"; +/* No comment provided by engineer. */ +"Use from desktop" = "Використання з робочого столу"; + /* No comment provided by engineer. */ "Use iOS call interface" = "Використовуйте інтерфейс виклику iOS"; /* No comment provided by engineer. */ "Use new incognito profile" = "Використовуйте новий профіль інкогніто"; +/* No comment provided by engineer. */ +"Use only local notifications?" = "Використовувати лише локальні сповіщення?"; + +/* No comment provided by engineer. */ +"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 server" = "Використовувати сервер"; +/* No comment provided by engineer. */ +"Use servers" = "Використовуйте сервери"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "Використовувати сервери SimpleX Chat?"; /* No comment provided by engineer. */ -"User profile" = "Профіль користувача"; +"Use SOCKS proxy" = "Використовуйте SOCKS проксі"; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Для використання хостів .onion потрібен сумісний VPN-провайдер."; +"Use the app while in the call." = "Використовуйте додаток під час розмови."; + +/* No comment provided by engineer. */ +"Use the app with one hand." = "Використовуйте додаток однією рукою."; + +/* No comment provided by engineer. */ +"User selection" = "Вибір користувача"; + +/* No comment provided by engineer. */ +"Username" = "Ім'я користувача"; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Використання серверів SimpleX Chat."; +/* No comment provided by engineer. */ +"v%@" = "v%@"; + /* No comment provided by engineer. */ "v%@ (%@)" = "v%@ (%@)"; +/* No comment provided by engineer. */ +"Verify code with desktop" = "Перевірте код на робочому столі"; + +/* No comment provided by engineer. */ +"Verify connection" = "Перевірте з'єднання"; + /* No comment provided by engineer. */ "Verify connection security" = "Перевірте безпеку з'єднання"; +/* No comment provided by engineer. */ +"Verify connections" = "Пeревірте з'єднання"; + +/* No comment provided by engineer. */ +"Verify database passphrase" = "Перевірте пароль до бази даних"; + +/* No comment provided by engineer. */ +"Verify passphrase" = "Підтвердіть парольну фразу"; + /* No comment provided by engineer. */ "Verify security code" = "Підтвердіть код безпеки"; @@ -3311,6 +5135,12 @@ /* No comment provided by engineer. */ "via relay" = "за допомогою ретранслятора"; +/* No comment provided by engineer. */ +"Via secure quantum resistant protocol." = "Через безпечний квантово-стійкий протокол."; + +/* No comment provided by engineer. */ +"video" = "відео"; + /* No comment provided by engineer. */ "Video call" = "Відеодзвінок"; @@ -3326,9 +5156,18 @@ /* No comment provided by engineer. */ "Videos and files up to 1gb" = "Відео та файли до 1 Гб"; +/* No comment provided by engineer. */ +"View conditions" = "Умови перегляду"; + /* No comment provided by engineer. */ "View security code" = "Переглянути код безпеки"; +/* No comment provided by engineer. */ +"View updated conditions" = "Переглянути оновлені умови"; + +/* chat feature */ +"Visible history" = "Видима історія"; + /* No comment provided by engineer. */ "Voice message…" = "Голосове повідомлення…"; @@ -3339,7 +5178,10 @@ "Voice messages are prohibited in this chat." = "Голосові повідомлення в цьому чаті заборонені."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Голосові повідомлення в цій групі заборонені."; +"Voice messages are prohibited." = "Голосові повідомлення в цій групі заборонені."; + +/* No comment provided by engineer. */ +"Voice messages not allowed" = "Голосові повідомлення заборонені"; /* No comment provided by engineer. */ "Voice messages prohibited!" = "Голосові повідомлення заборонені!"; @@ -3350,6 +5192,9 @@ /* No comment provided by engineer. */ "waiting for confirmation…" = "чекаємо на підтвердження…"; +/* No comment provided by engineer. */ +"Waiting for desktop..." = "Чекаємо на десктопну версію..."; + /* No comment provided by engineer. */ "Waiting for file" = "Очікування файлу"; @@ -3359,9 +5204,18 @@ /* No comment provided by engineer. */ "Waiting for video" = "Чекаємо на відео"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Акцент на шпалерах"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Фон шпалер"; + /* No comment provided by engineer. */ "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" = "Попередження: запуск чату на декількох пристроях не підтримується і може призвести до збоїв у доставці повідомлень"; + /* No comment provided by engineer. */ "Warning: you may lose some data!" = "Попередження: ви можете втратити деякі дані!"; @@ -3377,6 +5231,9 @@ /* No comment provided by engineer. */ "Welcome message" = "Вітальне повідомлення"; +/* No comment provided by engineer. */ +"Welcome message is too long" = "Привітальне повідомлення занадто довге"; + /* No comment provided by engineer. */ "What's new" = "Що нового"; @@ -3384,28 +5241,64 @@ "When available" = "За наявності"; /* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Коли люди звертаються із запитом на підключення, ви можете прийняти або відхилити його."; +"When connecting audio and video calls." = "При підключенні аудіо та відеодзвінків."; + +/* No comment provided by engineer. */ +"when IP hidden" = "коли IP приховано"; + +/* No comment provided by engineer. */ +"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." = "Коли ви ділитеся з кимось своїм профілем інкогніто, цей профіль буде використовуватися для груп, до яких вас запрошують."; +/* No comment provided by engineer. */ +"WiFi" = "WiFi"; + +/* No comment provided by engineer. */ +"Will be enabled in direct chats!" = "Буде ввімкнено в прямих чатах!"; + +/* No comment provided by engineer. */ +"Wired ethernet" = "Дротова мережа Ethernet"; + +/* No comment provided by engineer. */ +"With encrypted files and media." = "З зашифрованими файлами та медіа."; + /* No comment provided by engineer. */ "With optional welcome message." = "З необов'язковим вітальним повідомленням."; +/* No comment provided by engineer. */ +"With reduced battery usage." = "З меншим споживанням заряду акумулятора."; + +/* No comment provided by engineer. */ +"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: %@." = "Без Tor або VPN ваша IP-адреса буде видимою для цих XFTP-ретрансляторів: %@."; + /* No comment provided by engineer. */ "Wrong database passphrase" = "Неправильний пароль до бази даних"; +/* snd error text */ +"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." = "Неправильний ключ або невідома адреса фрагмента файлу - найімовірніше, файл видалено."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Неправильний пароль!"; /* No comment provided by engineer. */ -"XFTP servers" = "Сервери XFTP"; +"XFTP server" = "XFTP-сервер"; /* pref value */ "yes" = "так"; /* No comment provided by engineer. */ -"You" = "Ти"; +"you" = "ти"; + +/* No comment provided by engineer. */ +"You **must not** use the same database on two devices." = "Ви **не повинні використовувати** одну і ту ж базу даних на двох пристроях."; /* No comment provided by engineer. */ "You accepted connection" = "Ви прийняли підключення"; @@ -3419,6 +5312,30 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "Ви вже підключені до %@."; +/* No comment provided by engineer. */ +"You are already connected with %@." = "Ви вже підключені до %@."; + +/* No comment provided by engineer. */ +"You are already connecting to %@." = "Ви вже з'єднані з %@."; + +/* No comment provided by engineer. */ +"You are already connecting via this one-time link!" = "Ви вже підключаєтеся до %@.Ви вже підключаєтеся за цим одноразовим посиланням!"; + +/* No comment provided by engineer. */ +"You are already in group %@." = "Ви вже перебуваєте в групі %@."; + +/* No comment provided by engineer. */ +"You are already joining the group %@." = "Ви вже приєдналися до групи %@."; + +/* No comment provided by engineer. */ +"You are already joining the group via this link!" = "Ви вже приєдналися до групи за цим посиланням!"; + +/* No comment provided by engineer. */ +"You are already joining the group via this link." = "Ви вже приєдналися до групи за цим посиланням."; + +/* No comment provided by engineer. */ +"You are already joining the group!\nRepeat join request?" = "Ви вже приєдналися до групи!\nПовторити запит на приєднання?"; + /* No comment provided by engineer. */ "You are connected to the server used to receive messages from this contact." = "Ви підключені до сервера, який використовується для отримання повідомлень від цього контакту."; @@ -3428,14 +5345,23 @@ /* No comment provided by engineer. */ "You are invited to group" = "Запрошуємо вас до групи"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Не підключені до цих серверів. Для доставлення повідомлень до них використовується приватна маршрутизація."; + /* No comment provided by engineer. */ "you are observer" = "ви спостерігач"; +/* snd group event chat item */ +"you blocked %@" = "ви заблокували %@"; + /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Ви можете приймати дзвінки з екрана блокування без автентифікації пристрою та програми."; /* No comment provided by engineer. */ -"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "Ви також можете підключитися за посиланням. Якщо воно відкриється в браузері, натисніть кнопку **Відкрити в мобільному додатку**."; +"You can change it in Appearance settings." = "Ви можете змінити його в налаштуваннях зовнішнього вигляду."; + +/* No comment provided by engineer. */ +"You can configure servers via settings." = "Ви можете налаштувати сервери за допомогою налаштувань."; /* No comment provided by engineer. */ "You can create it later" = "Ви можете створити його пізніше"; @@ -3446,11 +5372,23 @@ /* No comment provided by engineer. */ "You can enable them later via app Privacy & Security settings." = "Ви можете увімкнути їх пізніше в налаштуваннях конфіденційності та безпеки програми."; +/* No comment provided by engineer. */ +"You can give another try." = "Ви можете спробувати ще раз."; + /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "Ви можете приховати або вимкнути звук профілю користувача - проведіть по ньому вправо."; +/* No comment provided by engineer. */ +"You can make it visible to your SimpleX contacts via Settings." = "Ви можете зробити його видимим для ваших контактів у SimpleX за допомогою налаштувань."; + /* notification body */ -"You can now send messages to %@" = "Тепер ви можете надсилати повідомлення на адресу %@"; +"You can now chat with %@" = "Тепер ви можете надсилати повідомлення на адресу %@"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "Ви можете надсилати повідомлення на %@ з архівних контактів."; + +/* No comment provided by engineer. */ +"You can set connection name, to remember who the link was shared with." = "Ви можете задати ім'я з'єднання, щоб запам'ятати, з ким ви поділилися посиланням."; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Ви можете налаштувати попередній перегляд сповіщень на екрані блокування за допомогою налаштувань."; @@ -3462,10 +5400,10 @@ "You can share this address with your contacts to let them connect with **%@**." = "Ви можете поділитися цією адресою зі своїми контактами, щоб вони могли зв'язатися з **%@**."; /* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Ви можете поділитися своєю адресою у вигляді посилання або QR-коду - будь-хто зможе зв'язатися з вами."; +"You can start chat via app Settings / Database or by restarting the app" = "Запустити чат можна через Налаштування програми / База даних або перезапустивши програму"; /* No comment provided by engineer. */ -"You can start chat via app Settings / Database or by restarting the app" = "Запустити чат можна через Налаштування програми / База даних або перезапустивши програму"; +"You can still view conversation with %@ in the list of chats." = "Ви все ще можете переглянути розмову з %@ у списку чатів."; /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Увімкнути SimpleX Lock можна в Налаштуваннях."; @@ -3473,6 +5411,9 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Ви можете використовувати розмітку для форматування повідомлень:"; +/* alert message */ +"You can view invitation link again in connection details." = "Ви можете переглянути посилання на запрошення ще раз у деталях підключення."; + /* No comment provided by engineer. */ "You can't send messages!" = "Ви не можете надсилати повідомлення!"; @@ -3488,14 +5429,17 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "ви змінили роль %1$@ на %2$@"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Ви контролюєте, через який(і) сервер(и) **отримувати** повідомлення, ваші контакти - сервери, які ви використовуєте для надсилання їм повідомлень."; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "Вас не вдалося верифікувати, спробуйте ще раз."; /* No comment provided by engineer. */ -"You have no chats" = "У вас немає чатів"; +"You decide who can connect." = "Ви вирішуєте, хто може під'єднатися."; + +/* No comment provided by engineer. */ +"You have already requested connection via this address!" = "Ви вже надсилали запит на підключення за цією адресою!"; + +/* No comment provided by engineer. */ +"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." = "Вам доведеться вводити парольну фразу щоразу під час запуску програми - вона не зберігається на пристрої."; @@ -3512,9 +5456,18 @@ /* snd group event chat item */ "you left" = "ти пішов"; +/* No comment provided by engineer. */ +"You may migrate the exported database." = "Ви можете мігрувати експортовану базу даних."; + +/* No comment provided by engineer. */ +"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." = "Ви повинні використовувати найновішу версію бази даних чату ТІЛЬКИ на одному пристрої, інакше ви можете перестати отримувати повідомлення від деяких контактів."; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "Щоб мати змогу зателефонувати контакту, вам потрібно дозволити йому зателефонувати."; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "Щоб мати змогу надсилати голосові повідомлення, вам потрібно дозволити контакту надсилати їх."; @@ -3533,9 +5486,15 @@ /* chat list item description */ "you shared one-time link incognito" = "ви поділилися одноразовим посиланням інкогніто"; +/* snd group event chat item */ +"you unblocked %@" = "ви розблокували %@"; + /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "Ви будете підключені до групи, коли пристрій господаря групи буде в мережі, будь ласка, зачекайте або перевірте пізніше!"; +/* No comment provided by engineer. */ +"You will be connected when group link host's device is online, please wait or check later!" = "Ви будете підключені, коли пристрій хоста групового посилання буде онлайн, будь ласка, зачекайте або перевірте пізніше!"; + /* No comment provided by engineer. */ "You will be connected when your connection request is accepted, please wait or check later!" = "Ви будете підключені, коли ваш запит на підключення буде прийнято, будь ласка, зачекайте або перевірте пізніше!"; @@ -3545,9 +5504,15 @@ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Вам потрібно буде пройти автентифікацію при запуску або відновленні програми після 30 секунд роботи у фоновому режимі."; +/* No comment provided by engineer. */ +"You will connect to all group members." = "Ви з'єднаєтеся з усіма учасниками групи."; + /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Ви все одно отримуватимете дзвінки та сповіщення від вимкнених профілів, якщо вони активні."; +/* No comment provided by engineer. */ +"You will stop receiving messages from this chat. Chat history will be preserved." = "Ви більше не будете отримувати повідомлення з цього чату. Історія чату буде збережена."; + /* No comment provided by engineer. */ "You will stop receiving messages from this group. Chat history will be preserved." = "Ви перестанете отримувати повідомлення від цієї групи. Історія чату буде збережена."; @@ -3563,9 +5528,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Ви використовуєте профіль інкогніто для цієї групи - щоб запобігти поширенню вашого основного профілю, запрошення контактів заборонено"; -/* No comment provided by engineer. */ -"Your %@ servers" = "Ваші сервери %@"; - /* No comment provided by engineer. */ "Your calls" = "Твої дзвінки"; @@ -3575,11 +5537,14 @@ /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Ваша база даних чату не зашифрована - встановіть ключову фразу, щоб зашифрувати її."; +/* alert title */ +"Your chat preferences" = "Ваші налаштування чату"; + /* No comment provided by engineer. */ "Your chat profiles" = "Ваші профілі чату"; /* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Для завершення з'єднання ваш контакт має бути онлайн.\nВи можете скасувати це з'єднання і видалити контакт (і спробувати пізніше з новим посиланням)."; +"Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Ваше з'єднання було переміщено на %@, але під час перенаправлення на профіль сталася несподівана помилка."; /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ваш контакт надіслав файл, розмір якого перевищує підтримуваний на цей момент максимальний розмір (%@)."; @@ -3588,10 +5553,10 @@ "Your contacts can allow full message deletion." = "Ваші контакти можуть дозволити повне видалення повідомлень."; /* No comment provided by engineer. */ -"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "Ваші контакти в SimpleX побачать це.\nВи можете змінити його в Налаштуваннях."; +"Your contacts will remain connected." = "Ваші контакти залишаться на зв'язку."; /* No comment provided by engineer. */ -"Your contacts will remain connected." = "Ваші контакти залишаться на зв'язку."; +"Your credentials may be sent unencrypted." = "Ваші облікові дані можуть бути надіслані незашифрованими."; /* No comment provided by engineer. */ "Your current chat database will be DELETED and REPLACED with the imported one." = "Ваша поточна база даних чату буде ВИДАЛЕНА і ЗАМІНЕНА імпортованою."; @@ -3608,11 +5573,17 @@ /* No comment provided by engineer. */ "Your privacy" = "Ваша конфіденційність"; +/* No comment provided by engineer. */ +"Your profile" = "Ваш профіль"; + /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Ваш профіль **%@** буде опублікований."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Ваш профіль зберігається на вашому пристрої і доступний лише вашим контактам.\nСервери SimpleX не бачать ваш профіль."; +"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." = "Ваш профіль було змінено. Якщо ви збережете його, оновлений профіль буде надіслано всім вашим контактам."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Ваш профіль, контакти та доставлені повідомлення зберігаються на вашому пристрої."; @@ -3621,10 +5592,10 @@ "Your random profile" = "Ваш випадковий профіль"; /* No comment provided by engineer. */ -"Your server" = "Ваш сервер"; +"Your server address" = "Адреса вашого сервера"; /* No comment provided by engineer. */ -"Your server address" = "Адреса вашого сервера"; +"Your servers" = "Ваші сервери"; /* No comment provided by engineer. */ "Your settings" = "Ваші налаштування"; @@ -3632,9 +5603,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "Ваша адреса SimpleX"; -/* No comment provided by engineer. */ -"Your SMP servers" = "Ваші SMP-сервери"; - -/* No comment provided by engineer. */ -"Your XFTP servers" = "Ваші XFTP-сервери"; - diff --git a/apps/ios/uk.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/uk.lproj/SimpleX--iOS--InfoPlist.strings index 2e3c6b8930..081ea9b074 100644 --- a/apps/ios/uk.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/uk.lproj/SimpleX--iOS--InfoPlist.strings @@ -7,6 +7,9 @@ /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX використовує Face ID для локальної автентифікації"; +/* Privacy - Local Network Usage Description */ +"NSLocalNetworkUsageDescription" = "SimpleX використовує доступ до локальної мережі, щоб дозволити користувачеві користуватися профілем чату через десктопну програму в тій же мережі."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX потребує доступу до мікрофона для аудіо та відео дзвінків, а також для запису голосових повідомлень."; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index c106e27f23..e3f9669d9f 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -1,18 +1,3 @@ -/* No comment provided by engineer. */ -"\n" = "\n"; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" (" = " ("; - /* No comment provided by engineer. */ " (can be copied)" = " (可复制)"; @@ -25,26 +10,20 @@ /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- 更稳定的传输!\n- 更好的社群!\n- 以及更多!"; +/* No comment provided by engineer. */ +"- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- 可选择通知已删除的联系人。\n- 带空格的个人资料名称。\n- 以及更多!"; + /* No comment provided by engineer. */ "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- 语音消息最长5分钟。\n- 自定义限时消息。\n- 编辑消息历史。"; -/* No comment provided by engineer. */ -", " = ", "; - -/* No comment provided by engineer. */ -": " = ": "; - /* No comment provided by engineer. */ "!1 colored!" = "!1 种彩色!"; /* No comment provided by engineer. */ -"." = "."; +"(new)" = "(新)"; /* No comment provided by engineer. */ -"(" = "("; - -/* 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)"; @@ -56,10 +35,10 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[在 GitHub 上加星](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**添加新联系人**:为您的联系人创建一次性二维码或者链接。"; +"**Create 1-time link**: to create and share a new invitation link." = "**添加联系人**: 创建新的邀请链接,或通过您收到的链接进行连接."; /* No comment provided by engineer. */ -"**Create link / QR code** for your contact to use." = "**创建链接 / 二维码** 给您的联系人使用。"; +"**Create group**: to create a new group." = "**创建群组**: 创建一个新群组."; /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "**端到端加密** 语音通话"; @@ -68,26 +47,29 @@ "**e2e encrypted** video call" = "**端到端加密** 视频通话"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**更私密**:每20分钟检查新消息。设备令牌和 SimpleX Chat 服务器共享,但是不会共享有您有多少联系人或者消息。"; +"**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." = "**更私密**:每20分钟检查新消息。设备令牌和 SimpleX Chat 服务器共享,但是不会共享有您有多少联系人或者消息。"; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**最私密**:不使用 SimpleX Chat 通知服务器,在后台定期检查消息(取决于您多经常使用应用程序)。"; +"**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." = "**最私密**:不使用 SimpleX Chat 通知服务器,在后台定期检查消息(取决于您多经常使用应用程序)。"; /* No comment provided by engineer. */ -"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**粘贴收到的链接**或者在浏览器里打开并且点击**在移动应用程序里打开**。"; +"**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." = "**请注意**:如果您丢失密码,您将无法恢复或者更改密码。"; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**推荐**:设备令牌和通知会发送至 SimpleX Chat 通知服务器,但是消息内容、大小或者发送人不会。"; +"**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 通知服务器,但是消息内容、大小或者发送人不会。"; /* No comment provided by engineer. */ -"**Scan QR code**: to connect to your contact in person or via video call." = "**扫描二维码**:见面或者通过视频通话来连接您的联系人。"; +"**Scan / Paste link**: to connect via a link you received." = "**扫描/粘贴链接**:用您收到的链接连接。"; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**警告**:及时推送通知需要保存在钥匙串的密码。"; +/* No comment provided by engineer. */ +"**Warning**: the archive will be removed." = "**警告**: 存档将被删除."; + /* No comment provided by engineer. */ "*bold*" = "\\*加粗*"; @@ -118,12 +100,21 @@ /* No comment provided by engineer. */ "%@ %@" = "%@ %@"; +/* No comment provided by engineer. */ +"%@ and %@" = "%@ 和 %@"; + /* No comment provided by engineer. */ "%@ and %@ connected" = "%@ 和%@ 以建立连接"; /* copied message info, <sender> at <time> */ "%@ at %@:" = "@ %2$@:"; +/* No comment provided by engineer. */ +"%@ connected" = "%@ 已连接"; + +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ 已下载"; + /* notification title */ "%@ is connected!" = "%@ 已连接!"; @@ -134,11 +125,23 @@ "%@ is verified" = "%@ 已认证"; /* No comment provided by engineer. */ -"%@ servers" = "%@ 服务器"; +"%@ server" = "服务器"; + +/* No comment provided by engineer. */ +"%@ servers" = "服务器"; + +/* No comment provided by engineer. */ +"%@ uploaded" = "%@ 已上传"; /* notification title */ "%@ wants to connect!" = "%@ 要连接!"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + +/* No comment provided by engineer. */ +"%@, %@ and %lld members" = "%@, %@ 和 %lld 成员"; + /* No comment provided by engineer. */ "%@, %@ and %lld other members connected" = "%@, %@ 和 %lld 个成员"; @@ -148,9 +151,24 @@ /* time interval */ "%d days" = "%d 天"; +/* forward confirmation reason */ +"%d file(s) are still being downloaded." = "仍在下载 %d 个文件。"; + +/* forward confirmation reason */ +"%d file(s) failed to download." = "%d 个文件下载失败。"; + +/* forward confirmation reason */ +"%d file(s) were deleted." = "已刪除 %d 个文件。"; + +/* forward confirmation reason */ +"%d file(s) were not downloaded." = "未能下载 %d 个文件。"; + /* time interval */ "%d hours" = "%d 小时"; +/* alert title */ +"%d messages not forwarded" = "未转发 %d 条消息"; + /* time interval */ "%d min" = "%d 分钟"; @@ -160,8 +178,11 @@ /* time interval */ "%d sec" = "%d 秒"; +/* delete after time */ +"%d seconds(s)" = "%d 秒"; + /* integrity error chat item */ -"%d skipped message(s)" = "%d 跳过消息"; +"%d skipped message(s)" = "跳过的 %d 条消息"; /* time interval */ "%d weeks" = "%d 星期"; @@ -178,18 +199,30 @@ /* No comment provided by engineer. */ "%lld file(s) with total size of %@" = "%lld 总文件大小 %@"; +/* No comment provided by engineer. */ +"%lld group events" = "%lld 群组事件"; + /* No comment provided by engineer. */ "%lld members" = "%lld 成员"; +/* No comment provided by engineer. */ +"%lld messages blocked" = "%lld 条消息已屏蔽"; + +/* No comment provided by engineer. */ +"%lld messages blocked by admin" = "%lld 被管理员阻止的消息"; + +/* No comment provided by engineer. */ +"%lld messages marked deleted" = "%lld 标记为已删除的消息"; + +/* No comment provided by engineer. */ +"%lld messages moderated by %@" = "%lld 审核的留言 by %@"; + /* No comment provided by engineer. */ "%lld minutes" = "%lld 分钟"; /* No comment provided by engineer. */ "%lld new interface languages" = "%lld 种新的界面语言"; -/* No comment provided by engineer. */ -"%lld second(s)" = "%lld 秒"; - /* No comment provided by engineer. */ "%lld seconds" = "%lld 秒"; @@ -229,10 +262,14 @@ /* No comment provided by engineer. */ "~strike~" = "\\~删去~"; +/* time to disappear */ +"0 sec" = "0 秒"; + /* No comment provided by engineer. */ "0s" = "0秒"; -/* time interval */ +/* delete after time +time interval */ "1 day" = "1天"; /* time interval */ @@ -241,15 +278,23 @@ /* No comment provided by engineer. */ "1 minute" = "1分钟"; -/* time interval */ +/* delete after time +time interval */ "1 month" = "1月"; -/* time interval */ +/* delete after time +time interval */ "1 week" = "1周"; +/* delete after time */ +"1 year" = "1 年"; + /* No comment provided by engineer. */ "1-time link" = "一次性链接"; +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "一次性链接*只能给一名联系人*使用。当面或使用聊天应用分享链接。"; + /* No comment provided by engineer. */ "5 minutes" = "5分钟"; @@ -284,10 +329,7 @@ "Abort changing address?" = "中止地址更改?"; /* No comment provided by engineer. */ -"About SimpleX" = "关于SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "关于 SimpleX 地址"; +"About operators" = "关于运营方"; /* No comment provided by engineer. */ "About SimpleX Chat" = "关于SimpleX Chat"; @@ -296,98 +338,199 @@ "above, then choose:" = "上面,然后选择:"; /* No comment provided by engineer. */ -"Accent color" = "色调"; +"Accent" = "强调"; /* accept contact request via notification - accept incoming call via notification */ +accept incoming call via notification +swipe action */ "Accept" = "接受"; +/* No comment provided by engineer. */ +"Accept conditions" = "接受条款"; + /* No comment provided by engineer. */ "Accept connection request?" = "接受联系人?"; /* notification body */ "Accept contact request from %@?" = "接受来自 %@ 的联系人请求?"; -/* accept contact request via notification */ +/* accept contact request via notification +swipe action */ "Accept incognito" = "接受隐身聊天"; /* call status */ "accepted call" = "已接受通话"; +/* No comment provided by engineer. */ +"Accepted conditions" = "已接受的条款"; + +/* No comment provided by engineer. */ +"Acknowledged" = "确认"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "确认错误"; + +/* token status text */ +"Active" = "活跃"; + +/* 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 preset servers" = "添加预设服务器"; +"Add friends" = "添加好友"; + +/* No comment provided by engineer. */ +"Add list" = "添加列表"; /* No comment provided by engineer. */ "Add profile" = "添加个人资料"; /* No comment provided by engineer. */ -"Add server…" = "添加服务器…"; +"Add server" = "添加服务器"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "扫描二维码来添加服务器。"; +/* No comment provided by engineer. */ +"Add team members" = "添加团队成员"; + /* No comment provided by engineer. */ "Add to another device" = "添加另一设备"; +/* No comment provided by engineer. */ +"Add to list" = "添加到列表"; + /* No comment provided by engineer. */ "Add welcome message" = "添加欢迎信息"; +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "将你的团队成员加入对话。"; + +/* No comment provided by engineer. */ +"Added media & file servers" = "已添加媒体和文件服务器"; + +/* No comment provided by engineer. */ +"Added message servers" = "已添加消息服务器"; + +/* No comment provided by engineer. */ +"Additional accent" = "附加重音"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "附加重音 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "附加二级"; + /* No comment provided by engineer. */ "Address" = "地址"; /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "将中止地址更改。将使用旧接收地址。"; +/* No comment provided by engineer. */ +"Address or 1-time link?" = "地址还是一次性链接?"; + +/* No comment provided by engineer. */ +"Address settings" = "地址设置"; + /* member role */ "admin" = "管理员"; +/* feature role */ +"admins" = "管理员"; + +/* No comment provided by engineer. */ +"Admins can block a member for all." = "管理员可以为所有人封禁一名成员。"; + /* No comment provided by engineer. */ "Admins can create the links to join groups." = "管理员可以创建链接以加入群组。"; /* No comment provided by engineer. */ "Advanced network settings" = "高级网络设置"; +/* No comment provided by engineer. */ +"Advanced settings" = "高级设置"; + /* chat item text */ "agreeing encryption for %@…" = "正在协商将加密应用于 %@…"; /* chat item text */ "agreeing encryption…" = "同意加密…"; +/* No comment provided by engineer. */ +"All" = "全部"; + /* No comment provided by engineer. */ "All app data is deleted." = "已删除所有应用程序数据。"; /* No comment provided by engineer. */ "All chats and messages will be deleted - this cannot be undone!" = "所有聊天记录和消息将被删除——这一行为无法撤销!"; +/* alert message */ +"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." = "所有数据在输入后将被删除。"; +/* No comment provided by engineer. */ +"All data is kept private on your device." = "所有数据都是您设备的私有数据."; + /* No comment provided by engineer. */ "All group members will remain connected." = "所有群组成员将保持连接。"; +/* feature role */ +"all members" = "所有成员"; + +/* 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!" = "所有消息都将被删除 - 这无法被撤销!"; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "所有聊天记录和消息将被删除——这一行为无法撤销!只有您的消息会被删除。"; +/* No comment provided by engineer. */ +"All new messages from %@ will be hidden!" = "来自 %@ 的所有新消息都将被隐藏!"; + +/* profile dropdown */ +"All profiles" = "所有配置文件"; + +/* No comment provided by engineer. */ +"All reports will be archived for you." = "将为你存档所有举报。"; + +/* No comment provided by engineer. */ +"All servers" = "全部服务器"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "所有联系人会保持连接。"; /* No comment provided by engineer. */ "All your contacts will remain connected. Profile update will be sent to your contacts." = "您的所有联系人将保持连接。个人资料更新将发送给您的联系人。"; +/* No comment provided by engineer. */ +"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" = "允许"; /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "仅当您的联系人允许时才允许呼叫。"; +/* No comment provided by engineer. */ +"Allow calls?" = "允许通话?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "仅当您的联系人允许时才允许限时消息。"; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you." = "仅有您的联系人许可后才允许不可撤回消息移除。"; +"Allow downgrade" = "允许降级"; + +/* No comment provided by engineer. */ +"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "仅有您的联系人许可后才允许不可撤回消息移除"; /* No comment provided by engineer. */ "Allow message reactions only if your contact allows them." = "只有您的联系人允许时才允许消息回应。"; @@ -402,11 +545,20 @@ "Allow sending disappearing messages." = "允许发送限时消息。"; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages." = "允许不可撤回地删除已发送消息。"; +"Allow sharing" = "允许共享"; + +/* No comment provided by engineer. */ +"Allow to irreversibly delete sent messages. (24 hours)" = "允许不可撤回地删除已发送消息"; + +/* No comment provided by engineer. */ +"Allow to report messsages to moderators." = "允许向 moderators 举报消息。"; /* No comment provided by engineer. */ "Allow to send files and media." = "允许发送文件和媒体。"; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "允许发送 SimpleX 链接。"; + /* No comment provided by engineer. */ "Allow to send voice messages." = "允许发送语音消息。"; @@ -423,7 +575,7 @@ "Allow your contacts to call you." = "允许您的联系人给您打电话。"; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages." = "允许您的联系人不可撤回地删除已发送消息。"; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "允许您的联系人不可撤回地删除已发送消息"; /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "允许您的联系人发送限时消息。"; @@ -434,24 +586,48 @@ /* No comment provided by engineer. */ "Already connected?" = "已连接?"; +/* No comment provided by engineer. */ +"Already connecting!" = "已经在连接了!"; + +/* No comment provided by engineer. */ +"Already joining the group!" = "已经加入了该群组!"; + /* pref value */ "always" = "始终"; +/* No comment provided by engineer. */ +"Always use private routing." = "始终使用私有路由。"; + /* No comment provided by engineer. */ "Always use relay" = "一直使用中继"; /* No comment provided by engineer. */ "An empty chat profile with the provided name is created, and the app opens as usual." = "已创建一个包含所提供名字的空白聊天资料,应用程序照常打开。"; +/* No comment provided by engineer. */ +"and %lld other events" = "和 %lld 其他事件"; + +/* report reason */ +"Another reason" = "另一个理由"; + /* No comment provided by engineer. */ "Answer call" = "接听来电"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "任何人都可以托管服务器。"; + /* No comment provided by engineer. */ "App build: %@" = "应用程序构建:%@"; +/* No comment provided by engineer. */ +"App data migration" = "应用数据迁移"; + /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "应用程序为新的本地文件(视频除外)加密。"; +/* No comment provided by engineer. */ +"App group:" = "应用组:"; + /* No comment provided by engineer. */ "App icon" = "应用程序图标"; @@ -461,6 +637,9 @@ /* No comment provided by engineer. */ "App passcode is replaced with self-destruct passcode." = "应用程序密码被替换为自毁密码。"; +/* No comment provided by engineer. */ +"App session" = "应用会话"; + /* No comment provided by engineer. */ "App version" = "应用程序版本"; @@ -470,9 +649,48 @@ /* No comment provided by engineer. */ "Appearance" = "外观"; +/* No comment provided by engineer. */ +"Apply" = "应用"; + +/* No comment provided by engineer. */ +"Apply to" = "应用于"; + +/* No comment provided by engineer. */ +"Archive" = "存档"; + +/* No comment provided by engineer. */ +"Archive %lld reports?" = "存档 %lld 个举报?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "存档所有举报?"; + +/* No comment provided by engineer. */ +"Archive and upload" = "存档和上传"; + +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "存档联系人以便稍后聊天."; + +/* No comment provided by engineer. */ +"Archive report" = "存档举报"; + +/* No comment provided by engineer. */ +"Archive report?" = "存档举报?"; + +/* swipe action */ +"Archive reports" = "存档举报"; + +/* No comment provided by engineer. */ +"Archived contacts" = "已存档的联系人"; + +/* No comment provided by engineer. */ +"Archiving database" = "正在存档数据库"; + /* No comment provided by engineer. */ "Attach" = "附件"; +/* No comment provided by engineer. */ +"attempts" = "尝试"; + /* No comment provided by engineer. */ "Audio & video calls" = "语音和视频通话"; @@ -500,6 +718,9 @@ /* No comment provided by engineer. */ "Authentication unavailable" = "身份验证不可用"; +/* member role */ +"author" = "作者"; + /* No comment provided by engineer. */ "Auto-accept" = "自动接受"; @@ -509,9 +730,18 @@ /* No comment provided by engineer. */ "Auto-accept images" = "自动接受图片"; +/* alert title */ +"Auto-accept settings" = "自动接受设置"; + /* No comment provided by engineer. */ "Back" = "返回"; +/* No comment provided by engineer. */ +"Background" = "背景"; + +/* No comment provided by engineer. */ +"Bad desktop address" = "糟糕的桌面地址"; + /* integrity error chat item */ "bad message hash" = "错误消息散列"; @@ -524,9 +754,76 @@ /* No comment provided by engineer. */ "Bad message ID" = "错误消息 ID"; +/* No comment provided by engineer. */ +"Better calls" = "更佳的通话"; + +/* No comment provided by engineer. */ +"Better groups" = "更佳的群组"; + +/* No comment provided by engineer. */ +"Better groups performance" = "更好的群性能"; + +/* No comment provided by engineer. */ +"Better message dates." = "更好的消息日期。"; + /* No comment provided by engineer. */ "Better messages" = "更好的消息"; +/* No comment provided by engineer. */ +"Better networking" = "更好的网络"; + +/* No comment provided by engineer. */ +"Better notifications" = "更佳的通知"; + +/* No comment provided by engineer. */ +"Better privacy and security" = "更好的隐私和安全"; + +/* No comment provided by engineer. */ +"Better security ✅" = "更佳的安全性✅"; + +/* No comment provided by engineer. */ +"Better user experience" = "更佳的使用体验"; + +/* No comment provided by engineer. */ +"Black" = "黑色"; + +/* No comment provided by engineer. */ +"Block" = "封禁"; + +/* No comment provided by engineer. */ +"Block for all" = "为所有人封禁"; + +/* No comment provided by engineer. */ +"Block group members" = "屏蔽群组成员"; + +/* No comment provided by engineer. */ +"Block member" = "封禁成员"; + +/* No comment provided by engineer. */ +"Block member for all?" = "为所有其他成员封禁该成员?"; + +/* No comment provided by engineer. */ +"Block member?" = "封禁成员吗?"; + +/* marked deleted chat item preview text */ +"blocked" = "已封禁"; + +/* rcv group event chat item */ +"blocked %@" = "已封禁 %@"; + +/* blocked chat item +marked deleted chat item preview text */ +"blocked by admin" = "由管理员封禁"; + +/* No comment provided by engineer. */ +"Blocked by admin" = "由管理员封禁"; + +/* No comment provided by engineer. */ +"Blur for better privacy." = "模糊处理,提高私密性."; + +/* No comment provided by engineer. */ +"Blur media" = "模糊媒体"; + /* No comment provided by engineer. */ "bold" = "加粗"; @@ -534,7 +831,7 @@ "Both you and your contact can add message reactions." = "您和您的联系人都可以添加消息回应。"; /* No comment provided by engineer. */ -"Both you and your contact can irreversibly delete sent messages." = "您和您的联系人都可以不可逆转地删除已发送的消息。"; +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "您和您的联系人都可以不可逆转地删除已发送的消息"; /* No comment provided by engineer. */ "Both you and your contact can make calls." = "您和您的联系人都可以拨打电话。"; @@ -548,9 +845,24 @@ /* 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. */ +"Business address" = "企业地址"; + +/* No comment provided by engineer. */ +"Business chats" = "企业聊天"; + +/* No comment provided by engineer. */ +"Businesses" = "企业"; + /* 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" = "呼叫"; + /* No comment provided by engineer. */ "Call already ended!" = "通话已结束!"; @@ -567,7 +879,16 @@ "Calls" = "通话"; /* No comment provided by engineer. */ -"Can't delete user profile!" = "无法删除用户个人资料!"; +"Calls prohibited!" = "禁止来电!"; + +/* No comment provided by engineer. */ +"Camera not available" = "相机不可用"; + +/* No comment provided by engineer. */ +"Can't call contact" = "无法呼叫联系人"; + +/* No comment provided by engineer. */ +"Can't call member" = "无法呼叫成员"; /* No comment provided by engineer. */ "Can't invite contact!" = "无法邀请联系人!"; @@ -576,8 +897,15 @@ "Can't invite contacts!" = "无法邀请联系人!"; /* No comment provided by engineer. */ +"Can't message member" = "无法向成员发送消息"; + +/* alert action +alert button */ "Cancel" = "取消"; +/* No comment provided by engineer. */ +"Cancel migration" = "取消迁移"; + /* feature offered item */ "cancelled %@" = "已取消 %@"; @@ -585,11 +913,26 @@ "Cannot access keychain to save database password" = "无法访问钥匙串以保存数据库密码"; /* No comment provided by engineer. */ +"Cannot forward message" = "无法转发消息"; + +/* alert title */ "Cannot receive file" = "无法接收文件"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "超出容量-收件人未收到以前发送的邮件。"; + +/* No comment provided by engineer. */ +"Cellular" = "移动网络"; + /* No comment provided by engineer. */ "Change" = "更改"; +/* alert title */ +"Change automatic message deletion?" = "更改消息自动删除设置?"; + +/* authentication reason */ +"Change chat profiles" = "更改聊天资料"; + /* No comment provided by engineer. */ "Change database passphrase?" = "更改数据库密码?"; @@ -615,7 +958,7 @@ "Change self-destruct mode" = "更改自毁模式"; /* authentication reason - set passcode view */ +set passcode view */ "Change self-destruct passcode" = "更改自毁密码"; /* chat item text */ @@ -634,7 +977,16 @@ "changing address…" = "更改地址…"; /* No comment provided by engineer. */ -"Chat archive" = "聊天档案"; +"Chat" = "聊天"; + +/* No comment provided by engineer. */ +"Chat already exists" = "聊天已存在"; + +/* No comment provided by engineer. */ +"Chat already exists!" = "聊天已存在!"; + +/* No comment provided by engineer. */ +"Chat colors" = "聊天颜色"; /* No comment provided by engineer. */ "Chat console" = "聊天控制台"; @@ -645,6 +997,9 @@ /* No comment provided by engineer. */ "Chat database deleted" = "聊天数据库已删除"; +/* No comment provided by engineer. */ +"Chat database exported" = "导出的聊天数据库"; + /* No comment provided by engineer. */ "Chat database imported" = "聊天数据库已导入"; @@ -654,18 +1009,51 @@ /* No comment provided by engineer. */ "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." = "聊天已停止。如果你已经在另一台设备商使用过此数据库,你应该在启动聊天前将数据库传输回来。"; + +/* No comment provided by engineer. */ +"Chat list" = "聊天列表"; + +/* No comment provided by engineer. */ +"Chat migrated!" = "已迁移聊天!"; + /* No comment provided by engineer. */ "Chat preferences" = "聊天偏好设置"; +/* alert message */ +"Chat preferences were changed." = "聊天偏好设置已修改。"; + +/* No comment provided by engineer. */ +"Chat profile" = "用户资料"; + +/* No comment provided by engineer. */ +"Chat theme" = "聊天主题"; + +/* No comment provided by engineer. */ +"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. */ "Chats" = "聊天"; /* No comment provided by engineer. */ +"Check messages every 20 min." = "每 20 分钟检查消息。"; + +/* No comment provided by engineer. */ +"Check messages when allowed." = "在被允许时检查消息。"; + +/* alert title */ "Check server address and try again." = "检查服务器地址并再试一次。"; /* No comment provided by engineer. */ "Chinese and Spanish interface" = "中文和西班牙文界面"; +/* No comment provided by engineer. */ +"Choose _Migrate from another device_ on the new device and scan QR code." = "在新设备上选择“从另一个设备迁移”并扫描二维码。"; + /* No comment provided by engineer. */ "Choose file" = "选择文件"; @@ -673,6 +1061,15 @@ "Choose from library" = "从库中选择"; /* No comment provided by engineer. */ +"Chunks deleted" = "已删除的块"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "下载的块"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "已下载的区块"; + +/* swipe action */ "Clear" = "清除"; /* No comment provided by engineer. */ @@ -681,14 +1078,29 @@ /* No comment provided by engineer. */ "Clear conversation?" = "清除对话吗?"; +/* No comment provided by engineer. */ +"Clear group?" = "清除群?"; + +/* No comment provided by engineer. */ +"Clear or delete group?" = "清除还是删除群?"; + +/* No comment provided by engineer. */ +"Clear private notes?" = "清除私密笔记?"; + /* No comment provided by engineer. */ "Clear verification" = "清除验证"; +/* No comment provided by engineer. */ +"Color chats with the new themes." = "使用新主题为聊天着色。"; + +/* No comment provided by engineer. */ +"Color mode" = "颜色模式"; + /* No comment provided by engineer. */ "colored" = "彩色"; -/* No comment provided by engineer. */ -"Colors" = "颜色"; +/* report reason */ +"Community guidelines violation" = "违反社区指导方针"; /* server test step */ "Compare file" = "对比文件"; @@ -699,15 +1111,51 @@ /* No comment provided by engineer. */ "complete" = "完整的"; +/* No comment provided by engineer. */ +"Completed" = "已完成"; + +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "已于 %@ 接受条款。"; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "已接受运营方 **%@** 的条款。"; + +/* No comment provided by engineer. */ +"Conditions are already accepted for these operator(s): **%@**." = "已经接受下列运营方的条款:**%@**。"; + +/* No comment provided by engineer. */ +"Conditions of use" = "使用条款"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "将接受下列运营方的条款:**%@**。"; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "将于 %@ 接受条款。"; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "将在 %@ 自动接受启用的运营方的条款。"; + /* No comment provided by engineer. */ "Configure ICE servers" = "配置 ICE 服务器"; +/* No comment provided by engineer. */ +"Configure server operators" = "配置服务器运营方"; + /* No comment provided by engineer. */ "Confirm" = "确认"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "确认删除联系人?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "确认数据库升级"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "确认来自未知服务器的文件。"; + +/* No comment provided by engineer. */ +"Confirm network settings" = "确认网络设置"; + /* No comment provided by engineer. */ "Confirm new passphrase…" = "确认新密码……"; @@ -717,33 +1165,78 @@ /* No comment provided by engineer. */ "Confirm password" = "确认密码"; +/* No comment provided by engineer. */ +"Confirm that you remember database passphrase to migrate it." = "请在迁移前确认你记得数据库的密码短语。"; + +/* No comment provided by engineer. */ +"Confirm upload" = "确认上传"; + +/* token status text */ +"Confirmed" = "已确定"; + /* server test step */ "Connect" = "连接"; +/* No comment provided by engineer. */ +"Connect automatically" = "自动连接"; + /* No comment provided by engineer. */ "Connect incognito" = "在隐身状态下连接"; +/* No comment provided by engineer. */ +"Connect to desktop" = "连接到桌面"; + /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "连接到 SimpleX Chat 开发者。"; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "更快地与您的朋友联系。"; + +/* No comment provided by engineer. */ +"Connect to yourself?" = "连接到你自己?"; + +/* No comment provided by engineer. */ +"Connect to yourself?\nThis is your own one-time link!" = "与自己建立联系?\n这是您自己的一次性链接!"; + +/* No comment provided by engineer. */ +"Connect to yourself?\nThis is your own SimpleX address!" = "与自己建立联系?\n这是您自己的 SimpleX 地址!"; + +/* No comment provided by engineer. */ +"Connect via contact address" = "通过联系地址连接"; + /* No comment provided by engineer. */ "Connect via link" = "通过链接连接"; -/* No comment provided by engineer. */ -"Connect via link / QR code" = "通过群组链接/二维码连接"; - /* No comment provided by engineer. */ "Connect via one-time link" = "通过一次性链接连接"; +/* No comment provided by engineer. */ +"Connect with %@" = "与 %@连接"; + /* No comment provided by engineer. */ "connected" = "已连接"; +/* No comment provided by engineer. */ +"Connected" = "已连接"; + +/* No comment provided by engineer. */ +"Connected desktop" = "已连接的桌面"; + /* rcv group event chat item */ "connected directly" = "已直连"; +/* No comment provided by engineer. */ +"Connected servers" = "已连接的服务器"; + +/* No comment provided by engineer. */ +"Connected to desktop" = "已连接到桌面"; + /* No comment provided by engineer. */ "connecting" = "连接中"; +/* No comment provided by engineer. */ +"Connecting" = "正在连接"; + /* No comment provided by engineer. */ "connecting (accepted)" = "连接中(已接受)"; @@ -765,12 +1258,24 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "连接服务器中……(错误:%@)"; -/* chat list item title */ +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "正在连接到联系人,请稍候或稍后检查!"; + +/* No comment provided by engineer. */ +"Connecting to desktop" = "正连接到桌面"; + +/* No comment provided by engineer. */ "connecting…" = "连接中……"; /* No comment provided by engineer. */ "Connection" = "连接"; +/* No comment provided by engineer. */ +"Connection and servers status." = "连接和服务器状态。"; + +/* No comment provided by engineer. */ +"Connection blocked" = "连接被阻止"; + /* No comment provided by engineer. */ "Connection error" = "连接错误"; @@ -780,21 +1285,51 @@ /* chat list item title (it should not be shown */ "connection established" = "连接已建立"; +/* No comment provided by engineer. */ +"Connection is blocked by server operator:\n%@" = "连接被运营方 %@ 阻止"; + +/* No comment provided by engineer. */ +"Connection not ready." = "连接未就绪。"; + +/* No comment provided by engineer. */ +"Connection notifications" = "连接通知"; + /* No comment provided by engineer. */ "Connection request sent!" = "已发送连接请求!"; +/* No comment provided by engineer. */ +"Connection requires encryption renegotiation." = "连接需要加密重协商。"; + +/* No comment provided by engineer. */ +"Connection security" = "连接安全性"; + +/* No comment provided by engineer. */ +"Connection terminated" = "连接被终止"; + /* No comment provided by engineer. */ "Connection timeout" = "连接超时"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "与桌面的连接已停止"; + /* connection information */ "connection:%@" = "连接:%@"; +/* No comment provided by engineer. */ +"Connections" = "连接"; + +/* profile update event chat item */ +"contact %@ changed to %@" = "联系人 %1$@ 已更改为 %2$@"; + /* No comment provided by engineer. */ "Contact allows" = "联系人允许"; /* No comment provided by engineer. */ "Contact already exists" = "联系人已存在"; +/* No comment provided by engineer. */ +"Contact deleted!" = "联系人已删除!"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "联系人具有端到端加密"; @@ -808,7 +1343,7 @@ "Contact is connected" = "联系已连接"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "联系人尚未连接!"; +"Contact is deleted." = "联系人被删除。"; /* No comment provided by engineer. */ "Contact name" = "联系人姓名"; @@ -816,41 +1351,68 @@ /* No comment provided by engineer. */ "Contact preferences" = "联系人偏好设置"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "联系人将被删除-这是无法撤消的!"; + /* No comment provided by engineer. */ "Contacts" = "联系人"; /* No comment provided by engineer. */ "Contacts can mark messages for deletion; you will be able to view them." = "联系人可以将信息标记为删除;您将可以查看这些信息。"; +/* blocking reason */ +"Content violates conditions of use" = "内容违反使用条款"; + /* No comment provided by engineer. */ "Continue" = "继续"; -/* chat item action */ +/* 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. */ "Core version: v%@" = "核心版本: v%@"; +/* No comment provided by engineer. */ +"Corner" = "拐角"; + +/* No comment provided by engineer. */ +"Correct name to %@?" = "将名称更正为 %@?"; + /* No comment provided by engineer. */ "Create" = "创建"; /* No comment provided by engineer. */ -"Create an address to let people connect with you." = "创建一个地址,让人们与您联系。"; +"Create 1-time link" = "创建一次性链接"; + +/* No comment provided by engineer. */ +"Create a group using a random profile." = "使用随机身份创建群组."; /* server test step */ "Create file" = "创建文件"; +/* No comment provided by engineer. */ +"Create group" = "建群"; + /* No comment provided by engineer. */ "Create group link" = "创建群组链接"; /* No comment provided by engineer. */ "Create link" = "创建链接"; +/* No comment provided by engineer. */ +"Create list" = "创建列表"; + /* No comment provided by engineer. */ "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "在[桌面应用程序](https://simplex.chat/downloads/)中创建新的个人资料。 💻"; /* No comment provided by engineer. */ -"Create one-time invitation link" = "创建一次性邀请链接"; +"Create profile" = "创建个人资料"; /* server test step */ "Create queue" = "创建队列"; @@ -865,17 +1427,35 @@ "Create your profile" = "创建您的资料"; /* No comment provided by engineer. */ -"Created on %@" = "创建于 %@"; +"Created" = "已创建"; + +/* No comment provided by engineer. */ +"Created at" = "创建于"; + +/* copied message info */ +"Created at: %@" = "创建于:%@"; + +/* No comment provided by engineer. */ +"Creating archive link" = "正在创建存档链接"; + +/* No comment provided by engineer. */ +"Creating link…" = "创建链接中…"; /* No comment provided by engineer. */ "creator" = "创建者"; +/* No comment provided by engineer. */ +"Current conditions text couldn't be loaded, you can review conditions via this link:" = "无法加载当前条款文本,你可以通过此链接审阅条款:"; + /* No comment provided by engineer. */ "Current Passcode" = "当前密码"; /* No comment provided by engineer. */ "Current passphrase…" = "现有密码……"; +/* No comment provided by engineer. */ +"Current profile" = "当前配置文件"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "目前支持的最大文件大小为 %@。"; @@ -885,9 +1465,18 @@ /* No comment provided by engineer. */ "Custom time" = "自定义时间"; +/* No comment provided by engineer. */ +"Customizable message shape." = "可自定义消息形状。"; + +/* No comment provided by engineer. */ +"Customize theme" = "自定义主题"; + /* No comment provided by engineer. */ "Dark" = "深色"; +/* No comment provided by engineer. */ +"Dark mode colors" = "深色模式颜色"; + /* No comment provided by engineer. */ "Database downgrade" = "数据库降级"; @@ -948,13 +1537,20 @@ /* time unit */ "days" = "天"; +/* No comment provided by engineer. */ +"Debug delivery" = "调试交付"; + /* No comment provided by engineer. */ "Decentralized" = "分散式"; /* message decrypt error item */ "Decryption error" = "解密错误"; -/* pref value */ +/* No comment provided by engineer. */ +"decryption errors" = "解密错误"; + +/* delete after time +pref value */ "default (%@)" = "默认 (%@)"; /* No comment provided by engineer. */ @@ -963,9 +1559,16 @@ /* No comment provided by engineer. */ "default (yes)" = "默认 (是)"; -/* chat item action */ +/* alert action +swipe action */ "Delete" = "删除"; +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "删除成员的 %lld 消息?"; + +/* No comment provided by engineer. */ +"Delete %lld messages?" = "删除 %lld 消息?"; + /* No comment provided by engineer. */ "Delete address" = "删除地址"; @@ -979,10 +1582,13 @@ "Delete all files" = "删除所有文件"; /* No comment provided by engineer. */ -"Delete archive" = "删除档案"; +"Delete and notify contact" = "删除并通知联系人"; /* No comment provided by engineer. */ -"Delete chat archive?" = "删除聊天档案?"; +"Delete chat" = "删除聊天"; + +/* No comment provided by engineer. */ +"Delete chat messages from your device." = "从你的设备删除聊天消息。"; /* No comment provided by engineer. */ "Delete chat profile" = "删除聊天资料"; @@ -990,6 +1596,9 @@ /* No comment provided by engineer. */ "Delete chat profile?" = "删除聊天资料?"; +/* No comment provided by engineer. */ +"Delete chat?" = "删除聊天?"; + /* No comment provided by engineer. */ "Delete connection" = "删除连接"; @@ -997,11 +1606,14 @@ "Delete contact" = "删除联系人"; /* No comment provided by engineer. */ -"Delete Contact" = "删除联系人"; +"Delete contact?" = "删除联系人?"; /* No comment provided by engineer. */ "Delete database" = "删除数据库"; +/* No comment provided by engineer. */ +"Delete database from this device" = "从这部设备上删除数据库"; + /* server test step */ "Delete file" = "删除文件"; @@ -1032,13 +1644,16 @@ /* No comment provided by engineer. */ "Delete link?" = "删除链接?"; +/* alert title */ +"Delete list?" = "删除列表?"; + /* No comment provided by engineer. */ "Delete member message?" = "删除成员消息?"; /* No comment provided by engineer. */ "Delete message?" = "删除消息吗?"; -/* No comment provided by engineer. */ +/* alert button */ "Delete messages" = "删除消息"; /* No comment provided by engineer. */ @@ -1051,7 +1666,7 @@ "Delete old database?" = "删除旧数据库吗?"; /* No comment provided by engineer. */ -"Delete pending connection" = "删除挂起连接"; +"Delete or moderate up to 200 messages." = "允许自行删除或管理员移除最多200条消息。"; /* No comment provided by engineer. */ "Delete pending connection?" = "删除待定连接?"; @@ -1062,21 +1677,42 @@ /* server test step */ "Delete queue" = "删除队列"; +/* No comment provided by engineer. */ +"Delete report" = "删除举报"; + +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "一次最多删除 20 条信息。"; + /* No comment provided by engineer. */ "Delete user profile?" = "删除用户资料?"; +/* No comment provided by engineer. */ +"Delete without notification" = "删除而不通知"; + /* deleted chat item */ "deleted" = "已删除"; +/* No comment provided by engineer. */ +"Deleted" = "已删除"; + /* No comment provided by engineer. */ "Deleted at" = "已删除于"; /* copied message info */ "Deleted at: %@" = "已删除于:%@"; +/* rcv direct event chat item */ +"deleted contact" = "已删除联系人"; + /* rcv group event chat item */ "deleted group" = "已删除群组"; +/* No comment provided by engineer. */ +"Deletion errors" = "删除错误"; + +/* No comment provided by engineer. */ +"Delivered even when Apple drops them." = "已送达,即使苹果已将其删除。"; + /* No comment provided by engineer. */ "Delivery" = "传送"; @@ -1089,9 +1725,36 @@ /* No comment provided by engineer. */ "Description" = "描述"; +/* No comment provided by engineer. */ +"Desktop address" = "桌面地址"; + +/* No comment provided by engineer. */ +"Desktop app version %@ is not compatible with this app." = "桌面应用程序版本 %@ 与此应用程序不兼容。"; + +/* No comment provided by engineer. */ +"Desktop devices" = "桌面设备"; + +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "目标服务器地址 %@ 与转发服务器 %@ 设置不兼容。"; + +/* snd error text */ +"Destination server error: %@" = "目标服务器错误:%@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "目标服务器版本 %@ 与转发服务器 %@ 不兼容。"; + +/* No comment provided by engineer. */ +"Detailed statistics" = "详细的统计数据"; + +/* No comment provided by engineer. */ +"Details" = "详细信息"; + /* No comment provided by engineer. */ "Develop" = "开发"; +/* No comment provided by engineer. */ +"Developer options" = "开发者选项"; + /* No comment provided by engineer. */ "Developer tools" = "开发者工具"; @@ -1099,7 +1762,7 @@ "Device" = "设备"; /* No comment provided by engineer. */ -"Device authentication is disabled. Turning off SimpleX Lock." = "设备验证被禁用。关闭 SimpleX 锁定。"; +"Device authentication is disabled. Turning off SimpleX Lock." = "设备验证已禁用。 SimpleX 已解锁。"; /* No comment provided by engineer. */ "Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." = "没有启用设备验证。一旦启用设备验证,您可以通过设置打开 SimpleX 锁定。"; @@ -1117,11 +1780,20 @@ "Direct messages" = "私信"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "此群中禁止成员之间私信。"; +"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. */ "Disable (keep overrides)" = "禁用(保留覆盖)"; +/* alert title */ +"Disable automatic message deletion?" = "禁用消息自动销毁?"; + +/* alert button */ +"Disable delete messages" = "停用消息删除"; + /* No comment provided by engineer. */ "Disable for all" = "全部禁用"; @@ -1131,6 +1803,9 @@ /* No comment provided by engineer. */ "disabled" = "关闭"; +/* No comment provided by engineer. */ +"Disabled" = "禁用"; + /* No comment provided by engineer. */ "Disappearing message" = "限时消息"; @@ -1141,7 +1816,7 @@ "Disappearing messages are prohibited in this chat." = "此聊天中禁止显示限时消息。"; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "该组禁止限时消息。"; +"Disappearing messages are prohibited." = "该组禁止限时消息。"; /* No comment provided by engineer. */ "Disappears at" = "消失于"; @@ -1152,42 +1827,100 @@ /* server test step */ "Disconnect" = "断开连接"; +/* No comment provided by engineer. */ +"Disconnect desktop?" = "断开桌面连接?"; + /* No comment provided by engineer. */ "Discover and join groups" = "发现和加入群组"; +/* No comment provided by engineer. */ +"Discover via local network" = "通过本地网络发现"; + /* No comment provided by engineer. */ "Do it later" = "稍后再做"; +/* No comment provided by engineer. */ +"Do not send history to new members." = "不给新成员发送历史消息。"; + +/* 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." = "代理不使用身份验证凭据。"; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "不要使用私有路由。"; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "请勿使用 SimpleX 进行紧急通话。"; +/* No comment provided by engineer. */ +"Documents:" = "文档:"; + /* No comment provided by engineer. */ "Don't create address" = "不创建地址"; /* No comment provided by engineer. */ "Don't enable" = "不要启用"; +/* No comment provided by engineer. */ +"Don't miss important messages." = "不错过重要消息。"; + /* No comment provided by engineer. */ "Don't show again" = "不再显示"; +/* No comment provided by engineer. */ +"Done" = "完成"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "降级并打开聊天"; +/* alert button +chat item action */ +"Download" = "下载"; + +/* No comment provided by engineer. */ +"Download errors" = "下载错误"; + +/* No comment provided by engineer. */ +"Download failed" = "下载失败了"; + /* server test step */ "Download file" = "下载文件"; +/* alert action */ +"Download files" = "下载文件"; + +/* No comment provided by engineer. */ +"Downloaded" = "已下载"; + +/* No comment provided by engineer. */ +"Downloaded files" = "下载的文件"; + +/* No comment provided by engineer. */ +"Downloading archive" = "正在下载存档"; + +/* No comment provided by engineer. */ +"Downloading link details" = "正在下载链接详情"; + /* No comment provided by engineer. */ "Duplicate display name!" = "重复的显示名!"; /* integrity error chat item */ "duplicate message" = "重复的消息"; +/* No comment provided by engineer. */ +"duplicates" = "副本"; + /* No comment provided by engineer. */ "Duration" = "时长"; /* No comment provided by engineer. */ "e2e encrypted" = "端到端加密"; +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "端到端加密的通知。"; + /* chat item action */ "Edit" = "编辑"; @@ -1200,12 +1933,21 @@ /* No comment provided by engineer. */ "Enable (keep overrides)" = "启用(保持覆盖)"; -/* No comment provided by engineer. */ +/* alert title */ "Enable automatic message deletion?" = "启用自动删除消息?"; +/* No comment provided by engineer. */ +"Enable camera access" = "启用相机访问"; + +/* No comment provided by engineer. */ +"Enable Flux in Network & servers settings for better metadata privacy." = "在“网络&服务器”设置中启用 Flux,更好地保护元数据隐私。"; + /* No comment provided by engineer. */ "Enable for all" = "全部启用"; +/* No comment provided by engineer. */ +"Enable in direct chats (BETA)!" = "在私聊中开启(公测)!"; + /* No comment provided by engineer. */ "Enable instant notifications?" = "启用即时通知?"; @@ -1233,6 +1975,12 @@ /* enabled status */ "enabled" = "已启用"; +/* No comment provided by engineer. */ +"Enabled" = "已启用"; + +/* No comment provided by engineer. */ +"Enabled for" = "启用对象"; + /* enabled status */ "enabled for contact" = "已为联系人启用"; @@ -1257,6 +2005,9 @@ /* notification */ "Encrypted message or another event" = "加密消息或其他事件"; +/* notification */ +"Encrypted message: app is stopped" = "加密消息:应用程序已停止"; + /* notification */ "Encrypted message: database error" = "加密消息:数据库错误"; @@ -1290,12 +2041,21 @@ /* chat item text */ "encryption re-negotiation allowed for %@" = "允许对 %@ 进行加密重新协商"; +/* message decrypt error item */ +"Encryption re-negotiation error" = "加密重协商错误"; + +/* No comment provided by engineer. */ +"Encryption re-negotiation failed." = "加密重协商失败了。"; + /* chat item text */ "encryption re-negotiation required" = "需要重新进行加密协商"; /* chat item text */ "encryption re-negotiation required for %@" = "需要为 %@ 重新进行加密协商"; +/* No comment provided by engineer. */ +"Encryption renegotiation in progress." = "正进行加密重协商。"; + /* No comment provided by engineer. */ "ended" = "已结束"; @@ -1305,9 +2065,15 @@ /* No comment provided by engineer. */ "Enter correct passphrase." = "输入正确密码。"; +/* No comment provided by engineer. */ +"Enter group name…" = "输入组名称…"; + /* No comment provided by engineer. */ "Enter Passcode" = "输入密码"; +/* No comment provided by engineer. */ +"Enter passphrase" = "输入密码短语"; + /* No comment provided by engineer. */ "Enter passphrase…" = "输入密码……"; @@ -1317,12 +2083,18 @@ /* No comment provided by engineer. */ "Enter server manually" = "手动输入服务器"; +/* No comment provided by engineer. */ +"Enter this device name…" = "输入此设备名…"; + /* placeholder */ "Enter welcome message…" = "输入欢迎消息……"; /* placeholder */ "Enter welcome message… (optional)" = "输入欢迎消息……(可选)"; +/* No comment provided by engineer. */ +"Enter your name…" = "请输入您的姓名…"; + /* No comment provided by engineer. */ "error" = "错误"; @@ -1332,24 +2104,36 @@ /* No comment provided by engineer. */ "Error aborting address change" = "中止地址更改错误"; +/* alert title */ +"Error accepting conditions" = "接受条款出错"; + /* No comment provided by engineer. */ "Error accepting contact request" = "接受联系人请求错误"; -/* No comment provided by engineer. */ -"Error accessing database file" = "访问数据库文件错误"; - /* No comment provided by engineer. */ "Error adding member(s)" = "添加成员错误"; +/* alert title */ +"Error adding server" = "添加服务器出错"; + /* No comment provided by engineer. */ "Error changing address" = "更改地址错误"; +/* No comment provided by engineer. */ +"Error changing connection profile" = "更改连接资料出错"; + /* No comment provided by engineer. */ "Error changing role" = "更改角色错误"; /* No comment provided by engineer. */ "Error changing setting" = "更改设置错误"; +/* No comment provided by engineer. */ +"Error changing to incognito!" = "切换至隐身聊天出错!"; + +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "连接到转发服务器 %@ 时出错。请稍后尝试。"; + /* No comment provided by engineer. */ "Error creating address" = "创建地址错误"; @@ -1359,12 +2143,21 @@ /* No comment provided by engineer. */ "Error creating group link" = "创建群组链接错误"; +/* alert title */ +"Error creating list" = "创建列表出错"; + /* No comment provided by engineer. */ "Error creating member contact" = "创建成员联系人时出错"; +/* No comment provided by engineer. */ +"Error creating message" = "创建消息出错"; + /* No comment provided by engineer. */ "Error creating profile!" = "创建资料错误!"; +/* No comment provided by engineer. */ +"Error creating report" = "创建举报出错"; + /* No comment provided by engineer. */ "Error decrypting file" = "解密文件时出错"; @@ -1377,9 +2170,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "删除连接错误"; -/* No comment provided by engineer. */ -"Error deleting contact" = "删除联系人错误"; - /* No comment provided by engineer. */ "Error deleting database" = "删除数据库错误"; @@ -1392,6 +2182,9 @@ /* No comment provided by engineer. */ "Error deleting user profile" = "删除用户资料错误"; +/* No comment provided by engineer. */ +"Error downloading the archive" = "下载存档出错"; + /* No comment provided by engineer. */ "Error enabling delivery receipts!" = "启用送达回执出错!"; @@ -1404,23 +2197,47 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "导出聊天数据库错误"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "导出主题时出错: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "导入聊天数据库错误"; /* No comment provided by engineer. */ "Error joining group" = "加入群组错误"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "加载 %@ 服务器错误"; +/* alert title */ +"Error loading servers" = "加载服务器出错"; /* No comment provided by engineer. */ +"Error migrating settings" = "迁移设置出错"; + +/* No comment provided by engineer. */ +"Error opening chat" = "打开聊天时出错"; + +/* alert title */ "Error receiving file" = "接收文件错误"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "重新连接服务器时出错"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "重新连接服务器时出错"; + +/* alert title */ +"Error registering for notifications" = "注册消息推送出错"; + /* No comment provided by engineer. */ "Error removing member" = "删除成员错误"; +/* alert title */ +"Error reordering lists" = "重排列表出错"; + /* No comment provided by engineer. */ -"Error saving %@ servers" = "保存 %@ 服务器错误"; +"Error resetting statistics" = "重置统计信息时出错"; + +/* alert title */ +"Error saving chat list" = "保存聊天列表出错"; /* No comment provided by engineer. */ "Error saving group profile" = "保存群组资料错误"; @@ -1434,9 +2251,18 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "保存密码到钥匙串错误"; +/* alert title */ +"Error saving servers" = "保存服务器出错"; + +/* when migrating */ +"Error saving settings" = "保存设置出错"; + /* No comment provided by engineer. */ "Error saving user password" = "保存用户密码时出错"; +/* No comment provided by engineer. */ +"Error scanning code: %@" = "扫描代码时出错:%@"; + /* No comment provided by engineer. */ "Error sending email" = "发送电邮错误"; @@ -1456,17 +2282,26 @@ "Error stopping chat" = "停止聊天错误"; /* No comment provided by engineer. */ +"Error switching profile" = "切换配置文件出错"; + +/* alertTitle */ "Error switching profile!" = "切换资料错误!"; /* No comment provided by engineer. */ "Error synchronizing connection" = "同步连接错误"; +/* No comment provided by engineer. */ +"Error testing server connection" = "检验服务器连接出错"; + /* No comment provided by engineer. */ "Error updating group link" = "更新群组链接错误"; /* No comment provided by engineer. */ "Error updating message" = "更新消息错误"; +/* alert title */ +"Error updating server" = "更新服务器出错"; + /* No comment provided by engineer. */ "Error updating settings" = "更新设置错误"; @@ -1474,9 +2309,17 @@ "Error updating user privacy" = "更新用户隐私时出错"; /* No comment provided by engineer. */ -"Error: " = "错误: "; +"Error uploading the archive" = "上传存档出错"; /* No comment provided by engineer. */ +"Error verifying passphrase:" = "验证密码短语出错:"; + +/* No comment provided by engineer. */ +"Error: " = "错误: "; + +/* alert message +file error text +snd error text */ "Error: %@" = "错误: %@"; /* No comment provided by engineer. */ @@ -1485,14 +2328,26 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "错误:URL 无效"; +/* No comment provided by engineer. */ +"Errors" = "错误"; + +/* servers error */ +"Errors in servers configuration." = "服务器配置有错误。"; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "即使在对话中被禁用。"; /* No comment provided by engineer. */ -"event happened" = "发生的事"; +"Exit without saving" = "退出而不保存"; + +/* chat item action */ +"Expand" = "展开"; /* No comment provided by engineer. */ -"Exit without saving" = "退出而不保存"; +"expired" = "过期"; + +/* token status text */ +"Expired" = "已过期"; /* No comment provided by engineer. */ "Export database" = "导出数据库"; @@ -1500,9 +2355,15 @@ /* No comment provided by engineer. */ "Export error:" = "导出错误:"; +/* No comment provided by engineer. */ +"Export theme" = "导出主题"; + /* No comment provided by engineer. */ "Exported database archive." = "导出数据库归档。"; +/* No comment provided by engineer. */ +"Exported file doesn't exist" = "导出的文件不存在"; + /* No comment provided by engineer. */ "Exporting database archive…" = "导出数据库档案中…"; @@ -1513,8 +2374,41 @@ "Fast and no wait until the sender is online!" = "快速且无需等待发件人在线!"; /* No comment provided by engineer. */ +"Faster deletion of groups." = "更快地删除群。"; + +/* No comment provided by engineer. */ +"Faster joining and more reliable messages." = "加入速度更快、信息更可靠。"; + +/* No comment provided by engineer. */ +"Faster sending messages." = "更快发送消息。"; + +/* swipe action */ "Favorite" = "最喜欢"; +/* No comment provided by engineer. */ +"Favorites" = "收藏"; + +/* file error alert title */ +"File error" = "文件错误"; + +/* alert message */ +"File errors:\n%@" = "文件错误:\n%@"; + +/* file error text */ +"File is blocked by server operator:\n%@." = "文件被服务器运营方阻止:\n%@。"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "找不到文件 - 很可能文件已被删除或取消。"; + +/* file error text */ +"File server error: %@" = "文件服务器错误:%@"; + +/* No comment provided by engineer. */ +"File status" = "文件状态"; + +/* copied message info */ +"File status: %@" = "文件状态:%@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "文件将从服务器中删除。"; @@ -1527,6 +2421,9 @@ /* No comment provided by engineer. */ "File: %@" = "文件:%@"; +/* No comment provided by engineer. */ +"Files" = "文件"; + /* No comment provided by engineer. */ "Files & media" = "文件和媒体"; @@ -1534,7 +2431,10 @@ "Files and media" = "文件和媒体"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "此群组中禁止文件和媒体。"; +"Files and media are prohibited." = "此群组中禁止文件和媒体。"; + +/* No comment provided by engineer. */ +"Files and media not allowed" = "不允许文件和媒体"; /* No comment provided by engineer. */ "Files and media prohibited!" = "禁止文件和媒体!"; @@ -1542,6 +2442,12 @@ /* No comment provided by engineer. */ "Filter unread and favorite chats." = "过滤未读和收藏的聊天记录。"; +/* No comment provided by engineer. */ +"Finalize migration" = "完成迁移"; + +/* No comment provided by engineer. */ +"Finalize migration on another device." = "在另一部设备上完成迁移."; + /* No comment provided by engineer. */ "Finally, we have them! 🚀" = "终于我们有它们了! 🚀"; @@ -1566,9 +2472,75 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "修复群组成员不支持的问题"; +/* No comment provided by engineer. */ +"For all moderators" = "所有 moderators"; + +/* servers error */ +"For chat profile %@:" = "为聊天资料 %@:"; + /* No comment provided by engineer. */ "For console" = "用于控制台"; +/* No comment provided by engineer. */ +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "比如,如果你通过 SimpleX 服务器收到消息,应用会通过 Flux 服务器传送它们。"; + +/* No comment provided by engineer. */ +"For me" = "仅自己"; + +/* No comment provided by engineer. */ +"For private routing" = "用于私密路由"; + +/* No comment provided by engineer. */ +"For social media" = "用于社交媒体"; + +/* chat item action */ +"Forward" = "转发"; + +/* alert title */ +"Forward %d message(s)?" = "转发 %d 条消息?"; + +/* No comment provided by engineer. */ +"Forward and save messages" = "转发并保存消息"; + +/* alert action */ +"Forward messages" = "已转发的消息"; + +/* alert message */ +"Forward messages without files?" = "仅转发消息不转发文件?"; + +/* No comment provided by engineer. */ +"Forward up to 20 messages at once." = "一次转发最多20条消息。"; + +/* No comment provided by engineer. */ +"forwarded" = "已转发"; + +/* No comment provided by engineer. */ +"Forwarded" = "已转发"; + +/* No comment provided by engineer. */ +"Forwarded from" = "转发自"; + +/* No comment provided by engineer. */ +"Forwarding %lld messages" = "正在转发 %lld 条消息"; + +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "转发服务器 %@ 无法连接到目标服务器 %@。请稍后尝试。"; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "转发服务器地址与网络设置不兼容:%@。"; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "转发服务器版本与网络设置不兼容:%@。"; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "转发服务器: %1$@\n目标服务器错误: %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "转发服务器: %1$@\n错误: %2$@"; + +/* No comment provided by engineer. */ +"Found desktop" = "找到了桌面"; + /* No comment provided by engineer. */ "French interface" = "法语界面"; @@ -1579,7 +2551,7 @@ "Full name (optional)" = "全名(可选)"; /* No comment provided by engineer. */ -"Full name:" = "全名:"; +"Fully decentralized – visible only to members." = "完全去中心化 - 仅对成员可见。"; /* No comment provided by engineer. */ "Fully re-implemented - work in background!" = "完全重新实现 - 在后台工作!"; @@ -1587,12 +2559,27 @@ /* No comment provided by engineer. */ "Further reduced battery usage" = "进一步减少电池使用"; +/* No comment provided by engineer. */ +"Get notified when mentioned." = "被提及时收到通知。"; + /* No comment provided by engineer. */ "GIFs and stickers" = "GIF 和贴纸"; +/* message preview */ +"Good afternoon!" = "下午好!"; + +/* message preview */ +"Good morning!" = "早上好!"; + /* No comment provided by engineer. */ "Group" = "群组"; +/* No comment provided by engineer. */ +"Group already exists" = "群组已存在"; + +/* No comment provided by engineer. */ +"Group already exists!" = "群已存在!"; + /* No comment provided by engineer. */ "group deleted" = "群组已删除"; @@ -1620,24 +2607,6 @@ /* No comment provided by engineer. */ "Group links" = "群组链接"; -/* No comment provided by engineer. */ -"Group members can add message reactions." = "群组成员可以添加信息回应。"; - -/* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages." = "群组成员可以不可撤回地删除已发送的消息。"; - -/* No comment provided by engineer. */ -"Group members can send direct messages." = "群组成员可以私信。"; - -/* No comment provided by engineer. */ -"Group members can send disappearing messages." = "群组成员可以发送限时消息。"; - -/* No comment provided by engineer. */ -"Group members can send files and media." = "群组成员可以发送文件和媒体。"; - -/* No comment provided by engineer. */ -"Group members can send voice messages." = "群组成员可以发送语音消息。"; - /* notification */ "Group message:" = "群组消息:"; @@ -1665,9 +2634,15 @@ /* No comment provided by engineer. */ "Group will be deleted for you - this cannot be undone!" = "将为您删除群组——此操作无法撤消!"; +/* No comment provided by engineer. */ +"Groups" = "群"; + /* No comment provided by engineer. */ "Help" = "帮助"; +/* No comment provided by engineer. */ +"Help admins moderating their groups." = "帮助管理员管理群组。"; + /* No comment provided by engineer. */ "Hidden" = "隐藏"; @@ -1692,10 +2667,19 @@ /* No comment provided by engineer. */ "History" = "历史记录"; +/* No comment provided by engineer. */ +"History is not sent to new members." = "未发送历史消息给新成员。"; + /* time unit */ "hours" = "小时"; /* No comment provided by engineer. */ +"How it affects privacy" = "它如何影响隐私"; + +/* No comment provided by engineer. */ +"How it helps privacy" = "它如何帮助隐私"; + +/* alert button */ "How it works" = "工作原理"; /* No comment provided by engineer. */ @@ -1710,15 +2694,15 @@ /* No comment provided by engineer. */ "How to use your servers" = "如何使用您的服务器"; +/* No comment provided by engineer. */ +"Hungarian interface" = "匈牙利语界面"; + /* No comment provided by engineer. */ "ICE servers (one per line)" = "ICE 服务器(每行一个)"; /* No comment provided by engineer. */ "If you can't meet in person, show QR code in a video call, or share the link." = "如果您不能亲自见面,可以在视频通话中展示二维码,或分享链接。"; -/* No comment provided by engineer. */ -"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "如果您不能亲自见面,您可以**扫描视频通话中的二维码**,或者您的联系人可以分享邀请链接。"; - /* No comment provided by engineer. */ "If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "如果您在打开应用时输入该密码,所有应用程序数据将被不可撤回地删除!"; @@ -1741,7 +2725,7 @@ "Immediately" = "立即"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "不受垃圾和骚扰消息影响"; +"Immune to spam" = "不受垃圾和骚扰消息影响"; /* No comment provided by engineer. */ "Import" = "导入"; @@ -1752,18 +2736,51 @@ /* No comment provided by engineer. */ "Import database" = "导入数据库"; +/* No comment provided by engineer. */ +"Import failed" = "导入失败了"; + +/* No comment provided by engineer. */ +"Import theme" = "导入主题"; + +/* No comment provided by engineer. */ +"Importing archive" = "正在导入存档"; + +/* No comment provided by engineer. */ +"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "改善传送,降低流量使用。\n更多改进即将推出!"; + +/* No comment provided by engineer. */ +"Improved message delivery" = "改进了消息传递"; + /* No comment provided by engineer. */ "Improved privacy and security" = "改进的隐私和安全"; /* No comment provided by engineer. */ "Improved server configuration" = "改进的服务器配置"; +/* No comment provided by engineer. */ +"In order to continue, chat should be stopped." = "必须停止聊天才能继续。"; + /* No comment provided by engineer. */ "In reply to" = "答复"; +/* No comment provided by engineer. */ +"In-call sounds" = "通话声音"; + +/* No comment provided by engineer. */ +"inactive" = "无效"; + +/* report reason */ +"Inappropriate content" = "不当内容"; + +/* report reason */ +"Inappropriate profile" = "不当个人资料"; + /* No comment provided by engineer. */ "Incognito" = "隐身聊天"; +/* No comment provided by engineer. */ +"Incognito groups" = "匿名群组"; + /* No comment provided by engineer. */ "Incognito mode" = "隐身模式"; @@ -1791,6 +2808,9 @@ /* No comment provided by engineer. */ "Incompatible database version" = "数据库版本不兼容"; +/* No comment provided by engineer. */ +"Incompatible version" = "不兼容的版本"; + /* PIN entry */ "Incorrect passcode" = "密码错误"; @@ -1809,14 +2829,32 @@ /* No comment provided by engineer. */ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "安装[用于终端的 SimpleX Chat](https://github.com/simplex-chat/simplex-chat)"; +/* No comment provided by engineer. */ +"Instant" = "即时"; + /* No comment provided by engineer. */ "Instant push notifications will be hidden!\n" = "即时推送通知将被隐藏!\n"; /* No comment provided by engineer. */ -"Instantly" = "即时"; +"Interface" = "界面"; /* No comment provided by engineer. */ -"Interface" = "界面"; +"Interface colors" = "界面颜色"; + +/* token status text */ +"Invalid" = "无效"; + +/* token status text */ +"Invalid (bad token)" = "Token 无效"; + +/* token status text */ +"Invalid (expired)" = "无效(已过期)"; + +/* token status text */ +"Invalid (unregistered)" = "无效(未注册)"; + +/* token status text */ +"Invalid (wrong topic)" = "无效(话题有误)"; /* invalid chat data */ "invalid chat" = "无效聊天"; @@ -1831,6 +2869,24 @@ "invalid data" = "无效数据"; /* No comment provided by engineer. */ +"Invalid display name!" = "无效的显示名!"; + +/* No comment provided by engineer. */ +"Invalid link" = "无效链接"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "迁移确认无效"; + +/* No comment provided by engineer. */ +"Invalid name!" = "无效名称!"; + +/* No comment provided by engineer. */ +"Invalid QR code" = "无效的二维码"; + +/* No comment provided by engineer. */ +"Invalid response" = "无效的响应"; + +/* alert title */ "Invalid server address!" = "无效的服务器地址!"; /* item status text */ @@ -1842,12 +2898,18 @@ /* group name */ "invitation to group %@" = "邀请您加入群组 %@"; +/* No comment provided by engineer. */ +"invite" = "邀请"; + /* No comment provided by engineer. */ "Invite friends" = "邀请朋友"; /* No comment provided by engineer. */ "Invite members" = "邀请成员"; +/* No comment provided by engineer. */ +"Invite to chat" = "邀请加入聊天"; + /* No comment provided by engineer. */ "Invite to group" = "邀请加入群组"; @@ -1869,6 +2931,9 @@ /* 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钥匙串将被用来安全地存储密码——它将允许接收推送通知。"; +/* No comment provided by engineer. */ +"IP address" = "IP 地址"; + /* No comment provided by engineer. */ "Irreversible message deletion" = "不可撤回消息移除"; @@ -1876,7 +2941,7 @@ "Irreversible message deletion is prohibited in this chat." = "此聊天中禁止不可撤回消息移除。"; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "此群组中禁止不可撤回消息移除。"; +"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." = "它允许在一个聊天资料中有多个匿名连接,而它们之间没有任何共享数据。"; @@ -1887,6 +2952,9 @@ /* No comment provided by engineer. */ "It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "它可能在以下情况发生:\n1. 消息在发送客户端 2 天后或在服务器上 30 天后过期。\n2. 消息解密失败,因为您或您的联系人使用了旧的数据库备份。\n3.连接被破坏。"; +/* No comment provided by engineer. */ +"It 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 (%@)." = "您似乎已经通过此链接连接。如果不是这样,则有一个错误 (%@)。"; @@ -1899,7 +2967,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "日语界面"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "加入"; /* No comment provided by engineer. */ @@ -1908,12 +2976,36 @@ /* No comment provided by engineer. */ "Join group" = "加入群组"; +/* No comment provided by engineer. */ +"Join group conversations" = "加入群对话"; + +/* No comment provided by engineer. */ +"Join group?" = "加入群组?"; + /* No comment provided by engineer. */ "Join incognito" = "加入隐身聊天"; +/* No comment provided by engineer. */ +"Join with current profile" = "使用当前档案加入"; + +/* No comment provided by engineer. */ +"Join your group?\nThis is your link for group %@!" = "加入您的群组?\n这是您组 %@ 的链接!"; + /* No comment provided by engineer. */ "Joining group" = "加入群组中"; +/* alert action */ +"Keep" = "保留"; + +/* No comment provided by engineer. */ +"Keep conversation" = "保持对话"; + +/* No comment provided by engineer. */ +"Keep the app open to use it from desktop" = "保持应用程序打开状态以从桌面使用它"; + +/* alert title */ +"Keep unused invitation?" = "保留未使用的邀请吗?"; + /* No comment provided by engineer. */ "Keep your connections" = "保持连接"; @@ -1929,9 +3021,15 @@ /* No comment provided by engineer. */ "Learn more" = "了解更多"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "离开"; +/* No comment provided by engineer. */ +"Leave chat" = "离开聊天"; + +/* No comment provided by engineer. */ +"Leave chat?" = "离开聊天?"; + /* No comment provided by engineer. */ "Leave group" = "离开群组"; @@ -1950,6 +3048,24 @@ /* No comment provided by engineer. */ "Limitations" = "限制"; +/* No comment provided by engineer. */ +"Link mobile and desktop apps! 🔗" = "连接移动端和桌面端应用程序!🔗"; + +/* No comment provided by engineer. */ +"Linked desktop options" = "已链接桌面选项"; + +/* No comment provided by engineer. */ +"Linked desktops" = "已链接桌面"; + +/* swipe action */ +"List" = "列表"; + +/* No comment provided by engineer. */ +"List name and emoji should be different for all lists." = "所有列表的名称和表情符号都应不同。"; + +/* No comment provided by engineer. */ +"List name..." = "列表名…"; + /* No comment provided by engineer. */ "LIVE" = "实时"; @@ -1971,24 +3087,15 @@ /* No comment provided by engineer. */ "Lock mode" = "锁定模式"; -/* No comment provided by engineer. */ -"Make a private connection" = "建立私密连接"; - /* No comment provided by engineer. */ "Make one message disappear" = "使一条消息消失"; /* No comment provided by engineer. */ "Make profile private!" = "将个人资料设为私密!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "请确保 %@服 务器地址格式正确,每行一个地址并且不重复 (%@)。"; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "确保 WebRTC ICE 服务器地址格式正确、每行分开且不重复。"; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "许多人问: *如果SimpleX没有用户标识符,它怎么传递信息?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "标记为所有人已删除"; @@ -2007,33 +3114,99 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "最长30秒,立即接收。"; +/* No comment provided by engineer. */ +"Media & file servers" = "Media & file servers"; + +/* blur media */ +"Medium" = "中等"; + /* member role */ "member" = "成员"; /* No comment provided by engineer. */ "Member" = "成员"; +/* profile update event chat item */ +"member %@ changed to %@" = "成员 %1$@ 已更改为 %2$@"; + /* rcv group event chat item */ "member connected" = "已连接"; +/* item status text */ +"Member inactive" = "成员不活跃"; + +/* chat feature */ +"Member reports" = "成员举报"; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All chat members will be notified." = "将变更成员角色为“%@”。所有成员都会收到通知。"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "成员角色将更改为 \"%@\"。所有群成员将收到通知。"; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "成员角色将更改为 \"%@\"。该成员将收到一份新的邀请。"; +/* No comment provided by engineer. */ +"Member will be removed from chat - this cannot be undone!" = "将从聊天中删除成员 - 此操作无法撤销!"; + /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "成员将被移出群组——此操作无法撤消!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "群组成员可以添加信息回应。"; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "群组成员可以不可撤回地删除已发送的消息"; + +/* No comment provided by engineer. */ +"Members can report messsages to moderators." = "成员可以向 moderators 举报消息。"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "群组成员可以私信。"; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "群组成员可以发送限时消息。"; + +/* No comment provided by engineer. */ +"Members can send files and media." = "群组成员可以发送文件和媒体。"; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "群成员可发送 SimpleX 链接。"; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "群组成员可以发送语音消息。"; + +/* No comment provided by engineer. */ +"Mention members 👋" = "提及成员👋"; + +/* No comment provided by engineer. */ +"Menus" = "菜单"; + +/* No comment provided by engineer. */ +"message" = "消息"; + /* item status text */ "Message delivery error" = "消息传递错误"; /* No comment provided by engineer. */ "Message delivery receipts!" = "消息送达回执!"; +/* item status text */ +"Message delivery warning" = "消息传递警告"; + /* No comment provided by engineer. */ "Message draft" = "消息草稿"; +/* item status text */ +"Message forwarded" = "消息已转发"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "如果 member 变为活动状态,则稍后可能会发送消息。"; + +/* No comment provided by engineer. */ +"Message queue info" = "消息队列信息"; + /* chat feature */ "Message reactions" = "消息回应"; @@ -2041,23 +3214,86 @@ "Message reactions are prohibited in this chat." = "该聊天禁用了消息回应。"; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "该群组禁用了消息回应。"; +"Message reactions are prohibited." = "该群组禁用了消息回应。"; /* notification */ "message received" = "消息已收到"; +/* No comment provided by engineer. */ +"Message reception" = "消息接收"; + +/* No comment provided by engineer. */ +"Message servers" = "消息服务器"; + +/* No comment provided by engineer. */ +"Message shape" = "消息形状"; + +/* No comment provided by engineer. */ +"Message source remains private." = "消息来源保持私密。"; + +/* No comment provided by engineer. */ +"Message status" = "消息状态"; + +/* copied message info */ +"Message status: %@" = "消息状态:%@"; + /* No comment provided by engineer. */ "Message text" = "消息正文"; +/* No comment provided by engineer. */ +"Message too large" = "消息太大了"; + /* No comment provided by engineer. */ "Messages" = "消息"; /* No comment provided by engineer. */ "Messages & files" = "消息"; +/* No comment provided by engineer. */ +"Messages from %@ will be shown!" = "将显示来自 %@ 的消息!"; + +/* alert message */ +"Messages in this chat will never be deleted." = "此聊天中的消息永远不会被删除。"; + +/* No comment provided by engineer. */ +"Messages received" = "收到的消息"; + +/* No comment provided by engineer. */ +"Messages sent" = "已发送的消息"; + +/* alert message */ +"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." = "消息、文件和通话受到 **端到端加密** 的保护,具有完全正向保密、否认和闯入恢复。"; + +/* 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 device" = "迁移设备"; + +/* No comment provided by engineer. */ +"Migrate from another device" = "从另一台设备迁移"; + +/* No comment provided by engineer. */ +"Migrate here" = "迁移到此处"; + +/* No comment provided by engineer. */ +"Migrate to another device" = "迁移到另一部设备"; + +/* No comment provided by engineer. */ +"Migrate to another device via QR code." = "通过二维码迁移到另一部设备。"; + +/* No comment provided by engineer. */ +"Migrating" = "迁移中"; + /* No comment provided by engineer. */ "Migrating database archive…" = "迁移数据库档案中…"; +/* No comment provided by engineer. */ +"Migration complete" = "迁移完毕"; + /* No comment provided by engineer. */ "Migration error:" = "迁移错误:"; @@ -2068,7 +3304,7 @@ "Migration is completed" = "迁移完成"; /* No comment provided by engineer. */ -"Migrations: %@" = "迁移:%@"; +"Migrations:" = "迁移"; /* time unit */ "minutes" = "分钟"; @@ -2088,63 +3324,99 @@ /* copied message info */ "Moderated at: %@" = "已被管理员移除于:%@"; -/* No comment provided by engineer. */ +/* marked deleted chat item preview text */ "moderated by %@" = "由 %@ 审核"; /* time unit */ "months" = "月"; +/* swipe action */ +"More" = "更多"; + /* No comment provided by engineer. */ "More improvements are coming soon!" = "更多改进即将推出!"; +/* No comment provided by engineer. */ +"More reliable network connection." = "更可靠的网络连接。"; + +/* No comment provided by engineer. */ +"More reliable notifications" = "更可靠的通知"; + /* item status description */ "Most likely this connection is deleted." = "此连接很可能已被删除。"; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "很可能此联系人已经删除了与您的联系。"; - /* No comment provided by engineer. */ "Multiple chat profiles" = "多个聊天资料"; -/* No comment provided by engineer. */ +/* notification label action */ "Mute" = "静音"; +/* notification label action */ +"Mute all" = "全部静音"; + /* No comment provided by engineer. */ "Muted when inactive!" = "不活动时静音!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "名称"; /* No comment provided by engineer. */ "Network & servers" = "网络和服务器"; +/* No comment provided by engineer. */ +"Network connection" = "网络连接"; + +/* No comment provided by engineer. */ +"Network decentralization" = "网络去中心化"; + +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "网络问题 - 消息在多次尝试发送后过期。"; + +/* No comment provided by engineer. */ +"Network management" = "网络管理"; + +/* No comment provided by engineer. */ +"Network operator" = "网络运营方"; + /* No comment provided by engineer. */ "Network settings" = "网络设置"; /* No comment provided by engineer. */ "Network status" = "网络状态"; -/* No comment provided by engineer. */ +/* delete after time */ "never" = "从不"; +/* token status text */ +"New" = "新"; + +/* No comment provided by engineer. */ +"New chat" = "新聊天"; + +/* No comment provided by engineer. */ +"New chat experience 🎉" = "新的聊天体验 🎉"; + /* notification */ "New contact request" = "新联系人请求"; /* notification */ "New contact:" = "新联系人:"; -/* No comment provided by engineer. */ -"New database archive" = "新数据库存档"; - /* No comment provided by engineer. */ "New desktop app!" = "全新桌面应用!"; /* No comment provided by engineer. */ "New display name" = "新显示名"; +/* notification */ +"New events" = "新事件"; + /* No comment provided by engineer. */ "New in %@" = "%@ 的新内容"; +/* No comment provided by engineer. */ +"New media options" = "新媒体选项"; + /* No comment provided by engineer. */ "New member role" = "新成员角色"; @@ -2160,6 +3432,15 @@ /* No comment provided by engineer. */ "New passphrase…" = "新密码……"; +/* No comment provided by engineer. */ +"New server" = "新服务器"; + +/* No comment provided by engineer. */ +"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 凭据。"; + /* pref value */ "no" = "否"; @@ -2169,6 +3450,15 @@ /* Authentication unavailable */ "No app password" = "没有应用程序密码"; +/* No comment provided by engineer. */ +"No chats" = "无聊天"; + +/* No comment provided by engineer. */ +"No chats found" = "找不到聊天"; + +/* No comment provided by engineer. */ +"No chats in list %@" = "列表 %@ 中无聊天"; + /* No comment provided by engineer. */ "No contacts selected" = "未选择联系人"; @@ -2181,6 +3471,9 @@ /* No comment provided by engineer. */ "No device token!" = "无设备令牌!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "还没有直接连接,消息由管理员转发。"; + /* No comment provided by engineer. */ "no e2e encryption" = "无端到端加密"; @@ -2193,21 +3486,87 @@ /* No comment provided by engineer. */ "No history" = "无历史记录"; +/* No comment provided by engineer. */ +"No info, try to reload" = "无信息,尝试重新加载"; + +/* servers error */ +"No media & file servers." = "无媒体和文件服务器。"; + +/* No comment provided by engineer. */ +"No message" = "无消息"; + +/* servers error */ +"No message servers." = "无消息服务器。"; + +/* No comment provided by engineer. */ +"No network connection" = "无网络连接"; + +/* No comment provided by engineer. */ +"No permission to record speech" = "无录音权限"; + +/* No comment provided by engineer. */ +"No permission to record video" = "无录像权限"; + /* No comment provided by engineer. */ "No permission to record voice message" = "没有录制语音消息的权限"; +/* No comment provided by engineer. */ +"No push server" = "本地"; + /* No comment provided by engineer. */ "No received or sent files" = "未收到或发送文件"; +/* servers error */ +"No servers for private message routing." = "无私密消息路由服务器。"; + +/* servers error */ +"No servers to receive files." = "无文件接收服务器。"; + +/* servers error */ +"No servers to receive messages." = "无消息接收服务器。"; + +/* servers error */ +"No servers to send files." = "无文件发送服务器。"; + /* copied message info in history */ "no text" = "无文本"; +/* alert title */ +"No token!" = "无 token!"; + +/* No comment provided by engineer. */ +"No unread chats" = "没有未读聊天"; + +/* No comment provided by engineer. */ +"No user identifiers." = "没有用户标识符。"; + +/* No comment provided by engineer. */ +"Not compatible!" = "不兼容!"; + +/* No comment provided by engineer. */ +"Notes" = "附注"; + +/* No comment provided by engineer. */ +"Nothing selected" = "未选中任何内容"; + +/* alert title */ +"Nothing to forward!" = "无可转发!"; + /* No comment provided by engineer. */ "Notifications" = "通知"; /* No comment provided by engineer. */ "Notifications are disabled!" = "通知被禁用!"; +/* alert title */ +"Notifications error" = "通知错误"; + +/* No comment provided by engineer. */ +"Notifications privacy" = "通知隐私"; + +/* alert title */ +"Notifications status" = "通知状态"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "现在管理员可以:\n- 删除成员的消息。\n- 禁用成员(“观察员”角色)"; @@ -2215,30 +3574,27 @@ "observer" = "观察者"; /* enabled status - group pref value - time to disappear */ +group pref value +time to disappear */ "off" = "关闭"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "关闭"; -/* No comment provided by engineer. */ -"Off (Local)" = "关闭(本地)"; - /* feature offered item */ "offered %@" = "已提供 %@"; /* feature offered item */ "offered %@: %@" = "已提供 %1$@:%2$@"; -/* No comment provided by engineer. */ +/* alert button */ "Ok" = "好的"; /* No comment provided by engineer. */ -"Old database" = "旧的数据库"; +"OK" = "好的"; /* No comment provided by engineer. */ -"Old database archive" = "旧数据库存档"; +"Old database" = "旧的数据库"; /* group pref value */ "on" = "开启"; @@ -2247,16 +3603,22 @@ "One-time invitation link" = "一次性邀请链接"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Onion 主机将用于连接。需要启用 VPN。"; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Onion 主机将是连接所必需的。\n需要兼容的 VPN。"; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "当可用时,将使用 Onion 主机。需要启用 VPN。"; +"Onion hosts will be used when available.\nRequires compatible VPN." = "如果可用,将使用洋葱主机。\n需要兼容的 VPN。"; /* No comment provided by engineer. */ "Onion hosts will not be used." = "将不会使用 Onion 主机。"; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "只有客户端设备存储用户资料、联系人、群组和**双层端到端加密**发送的消息。"; +"Only chat owners can change preferences." = "仅聊天所有人可更改首选项。"; + +/* No comment provided by engineer. */ +"Only client devices store user profiles, contacts, groups, and messages." = "只有客户端设备存储用户资料、联系人、群组和**双层端到端加密**发送的消息。"; + +/* No comment provided by engineer. */ +"Only delete conversation" = "仅删除对话"; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "只有群主可以改变群组偏好设置。"; @@ -2267,11 +3629,17 @@ /* No comment provided by engineer. */ "Only group owners can enable voice messages." = "只有群主可以启用语音信息。"; +/* No comment provided by engineer. */ +"Only sender and moderators see it" = "仅发送人和moderators能看到"; + +/* No comment provided by engineer. */ +"Only you and moderators see it" = "只有你和moderators能看到"; + /* No comment provided by engineer. */ "Only you can add message reactions." = "只有您可以添加消息回应。"; /* No comment provided by engineer. */ -"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "只有您可以不可撤回地删除消息(您的联系人可以将它们标记为删除)。"; +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "只有您可以不可撤回地删除消息(您的联系人可以将它们标记为删除)"; /* No comment provided by engineer. */ "Only you can make calls." = "只有您可以拨打电话。"; @@ -2286,7 +3654,7 @@ "Only your contact can add message reactions." = "只有您的联系人可以添加消息回应。"; /* No comment provided by engineer. */ -"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "只有您的联系人才能不可撤回地删除消息(您可以将它们标记为删除)。"; +"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "只有您的联系人才能不可撤回地删除消息(您可以将它们标记为删除)"; /* No comment provided by engineer. */ "Only your contact can make calls." = "只有您的联系人可以拨打电话。"; @@ -2297,9 +3665,12 @@ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "只有您的联系人可以发送语音消息。"; -/* No comment provided by engineer. */ +/* alert action */ "Open" = "打开"; +/* No comment provided by engineer. */ +"Open changes" = "打开更改"; + /* No comment provided by engineer. */ "Open chat" = "打开聊天"; @@ -2307,20 +3678,62 @@ "Open chat console" = "打开聊天控制台"; /* No comment provided by engineer. */ -"Open Settings" = "打开设置"; +"Open conditions" = "打开条款"; + +/* No comment provided by engineer. */ +"Open group" = "打开群"; /* authentication reason */ -"Open user profiles" = "打开用户个人资料"; +"Open migration to another device" = "打开迁移到另一台设备"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "开源协议和代码——任何人都可以运行服务器。"; +"Open Settings" = "打开设置"; /* No comment provided by engineer. */ -"Opening database…" = "打开数据库中……"; +"Opening app…" = "正在打开应用程序…"; + +/* No comment provided by engineer. */ +"Operator" = "运营方"; + +/* alert title */ +"Operator server" = "运营方服务器"; + +/* No comment provided by engineer. */ +"Or import archive file" = "或者导入或者导入压缩文件"; + +/* No comment provided by engineer. */ +"Or paste archive link" = "或粘贴存档链接"; + +/* No comment provided by engineer. */ +"Or scan QR code" = "或者扫描二维码"; + +/* No comment provided by engineer. */ +"Or securely share this file link" = "或安全地分享此文件链接"; + +/* No comment provided by engineer. */ +"Or show this code" = "或者显示此码"; + +/* No comment provided by engineer. */ +"Or to share privately" = "或者私下分享"; + +/* No comment provided by engineer. */ +"Organize chats into lists" = "将聊天组织到列表"; + +/* No comment provided by engineer. */ +"other" = "其他"; + +/* No comment provided by engineer. */ +"Other" = "其他"; + +/* No comment provided by engineer. */ +"other errors" = "其他错误"; /* member role */ "owner" = "群主"; +/* feature role */ +"owners" = "所有者"; + /* No comment provided by engineer. */ "Passcode" = "密码"; @@ -2337,41 +3750,59 @@ "Passcode set!" = "密码已设置!"; /* No comment provided by engineer. */ -"Password to show" = "显示密码"; +"Password" = "密码"; /* No comment provided by engineer. */ -"Paste" = "粘贴"; +"Password to show" = "显示密码"; + +/* past/unknown group member */ +"Past member %@" = "前任成员 %@"; + +/* No comment provided by engineer. */ +"Paste desktop address" = "粘贴桌面地址"; /* No comment provided by engineer. */ "Paste image" = "粘贴图片"; /* No comment provided by engineer. */ -"Paste received link" = "粘贴收到的链接"; +"Paste link to connect!" = "粘贴链接以连接!"; -/* placeholder */ -"Paste the link you received to connect with your contact." = "将您收到的链接粘贴到下面的框中以与您的联系人联系。"; +/* No comment provided by engineer. */ +"Paste the link you received" = "粘贴您收到的链接"; /* No comment provided by engineer. */ "peer-to-peer" = "点对点"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "人们只能通过您共享的链接与您建立联系。"; +"Pending" = "待定"; /* No comment provided by engineer. */ -"Periodically" = "定期"; +"Periodic" = "定期"; /* message decrypt error item */ "Permanent decryption error" = "解密错误"; +/* No comment provided by engineer. */ +"Picture-in-picture calls" = "画中画通话"; + /* No comment provided by engineer. */ "PING count" = "PING 次数"; /* No comment provided by engineer. */ "PING interval" = "PING 间隔"; +/* No comment provided by engineer. */ +"Play from the chat list." = "从聊天列表播放。"; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "请要求您的联系人开通通话功能。"; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "请让您的联系人启用发送语音消息。"; +/* No comment provided by engineer. */ +"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." = "请检查您使用的链接是否正确,或者让您的联系人给您发送另一个链接。"; @@ -2381,6 +3812,12 @@ /* No comment provided by engineer. */ "Please check yours and your contact preferences." = "请检查您和您的联系人偏好设置。"; +/* No comment provided by engineer. */ +"Please confirm that network settings are correct for this device." = "请确认网络设置对此这台设备正确无误。"; + +/* No comment provided by engineer. */ +"Please contact developers.\nError: %@" = "请联系开发人员。\n错误:%@"; + /* No comment provided by engineer. */ "Please contact group admin." = "请联系群组管理员。"; @@ -2414,34 +3851,61 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "保留最后的消息草稿及其附件。"; -/* No comment provided by engineer. */ -"Preset server" = "预设服务器"; - /* No comment provided by engineer. */ "Preset server address" = "预设服务器地址"; /* No comment provided by engineer. */ "Preview" = "预览"; +/* No comment provided by engineer. */ +"Previously connected servers" = "以前连接的服务器"; + /* No comment provided by engineer. */ "Privacy & security" = "隐私和安全"; +/* 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" = "私密文件名"; +/* No comment provided by engineer. */ +"Private message routing" = "私有消息路由"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "私有消息路由 🚀"; + +/* name of notes to self */ +"Private notes" = "私密笔记"; + +/* No comment provided by engineer. */ +"Private routing" = "专用路由"; + +/* No comment provided by engineer. */ +"Private routing error" = "专用路由错误"; + /* No comment provided by engineer. */ "Profile and server connections" = "资料和服务器连接"; /* No comment provided by engineer. */ "Profile image" = "资料图片"; +/* No comment provided by engineer. */ +"Profile images" = "个人资料图"; + /* No comment provided by engineer. */ "Profile password" = "个人资料密码"; /* No comment provided by engineer. */ +"Profile theme" = "个人资料主题"; + +/* alert message */ "Profile update will be sent to your contacts." = "个人资料更新将被发送给您的联系人。"; /* No comment provided by engineer. */ @@ -2465,38 +3929,68 @@ /* 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." = "禁止发送语音消息。"; /* No comment provided by engineer. */ "Protect app screen" = "保护应用程序屏幕"; +/* No comment provided by engineer. */ +"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在*网络和服务器*设置中启用。"; + /* No comment provided by engineer. */ "Protocol timeout" = "协议超时"; /* No comment provided by engineer. */ "Protocol timeout per KB" = "每 KB 协议超时"; +/* No comment provided by engineer. */ +"Proxied" = "代理"; + +/* No comment provided by engineer. */ +"Proxied servers" = "代理服务器"; + /* No comment provided by engineer. */ "Push notifications" = "推送通知"; +/* No comment provided by engineer. */ +"Push server" = "推送服务器"; + +/* chat item text */ +"quantum resistant e2e encryption" = "抗量子端到端加密"; + +/* No comment provided by engineer. */ +"Quantum resistant encryption" = "抗量子加密"; + /* No comment provided by engineer. */ "Rate the app" = "评价此应用程序"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "可访问的聊天工具栏"; + /* chat item menu */ "React…" = "回应…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "已读"; /* No comment provided by engineer. */ "Read more" = "阅读更多"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "在 [用户指南](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address) 中阅读更多内容。"; +"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)。"; + +/* 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) 中阅读更多内容。"; @@ -2505,10 +3999,10 @@ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "在我们的 [GitHub 仓库](https://github.com/simplex-chat/simplex-chat#readme) 中阅读更多信息。"; /* No comment provided by engineer. */ -"Read more in our GitHub repository." = "在我们的 GitHub 仓库中阅读更多内容。"; +"Receipts are disabled" = "回执已禁用"; /* No comment provided by engineer. */ -"Receipts are disabled" = "回执已禁用"; +"Receive errors" = "接收错误"; /* No comment provided by engineer. */ "received answer…" = "已收到回复……"; @@ -2528,6 +4022,15 @@ /* message info title */ "Received message" = "收到的信息"; +/* No comment provided by engineer. */ +"Received messages" = "收到的消息"; + +/* No comment provided by engineer. */ +"Received reply" = "已收到回复"; + +/* No comment provided by engineer. */ +"Received total" = "接收总数"; + /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "接收地址将变更到不同的服务器。地址更改将在发件人上线后完成。"; @@ -2537,12 +4040,33 @@ /* No comment provided by engineer. */ "Receiving via" = "接收通过"; +/* No comment provided by engineer. */ +"Recent history and improved [directory bot](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)." = "最近的历史记录和改进的 [目录机器人](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)."; + +/* No comment provided by engineer. */ +"Recipient(s) can't see who this message is from." = "收件人看不到这条消息来自何人。"; + /* No comment provided by engineer. */ "Recipients see updates as you type them." = "对方会在您键入时看到更新。"; +/* No comment provided by engineer. */ +"Reconnect" = "重新连接"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "重新连接所有已连接的服务器以强制发送信息。这会耗费更多流量。"; +/* No comment provided by engineer. */ +"Reconnect all servers" = "重新连接所有服务器"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "重新连接所有服务器?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "重新连接服务器以强制发送信息。它使用额外的流量。"; + +/* No comment provided by engineer. */ +"Reconnect server?" = "重新连接服务器?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "是否重新连接服务器?"; @@ -2555,7 +4079,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "减少电池使用量"; -/* reject incoming call via notification */ +/* reject incoming call via notification +swipe action */ "Reject" = "拒绝"; /* No comment provided by engineer. */ @@ -2576,6 +4101,9 @@ /* No comment provided by engineer. */ "Remove" = "移除"; +/* No comment provided by engineer. */ +"Remove image" = "移除图片"; + /* No comment provided by engineer. */ "Remove member" = "删除成员"; @@ -2591,6 +4119,12 @@ /* rcv group event chat item */ "removed %@" = "已删除 %@"; +/* profile update event chat item */ +"removed contact address" = "删除了联系地址"; + +/* profile update event chat item */ +"removed profile picture" = "删除了资料图片"; + /* rcv group event chat item */ "removed you" = "已将您移除"; @@ -2603,6 +4137,21 @@ /* No comment provided by engineer. */ "Renegotiate encryption?" = "重新协商加密?"; +/* No comment provided by engineer. */ +"Repeat connection request?" = "重复连接请求吗?"; + +/* No comment provided by engineer. */ +"Repeat download" = "重复下载"; + +/* No comment provided by engineer. */ +"Repeat import" = "重复导入"; + +/* No comment provided by engineer. */ +"Repeat join request?" = "重复加入请求吗?"; + +/* No comment provided by engineer. */ +"Repeat upload" = "重复上传"; + /* chat item action */ "Reply" = "回复"; @@ -2612,12 +4161,27 @@ /* No comment provided by engineer. */ "Reset" = "重置"; +/* No comment provided by engineer. */ +"Reset all hints" = "重置所有提示"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "重置所有统计信息"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "重置所有统计信息?"; + /* No comment provided by engineer. */ "Reset colors" = "重置颜色"; +/* No comment provided by engineer. */ +"Reset to app theme" = "重置为应用程序主题"; + /* No comment provided by engineer. */ "Reset to defaults" = "重置为默认"; +/* No comment provided by engineer. */ +"Reset to user theme" = "重置为用户主题"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "重新启动应用程序以创建新的聊天资料"; @@ -2636,58 +4200,68 @@ /* No comment provided by engineer. */ "Restore database error" = "恢复数据库错误"; +/* No comment provided by engineer. */ +"Retry" = "重试"; + /* chat item action */ "Reveal" = "揭示"; /* No comment provided by engineer. */ -"Revert" = "恢复"; +"Review conditions" = "审阅条款"; /* No comment provided by engineer. */ -"Revoke" = "撤销"; +"Revoke" = "吊销"; /* cancel file action */ -"Revoke file" = "撤销文件"; +"Revoke file" = "吊销文件"; /* No comment provided by engineer. */ -"Revoke file?" = "撤销文件?"; +"Revoke file?" = "吊销文件?"; /* No comment provided by engineer. */ "Role" = "角色"; /* No comment provided by engineer. */ -"Run chat" = "运行聊天程序"; +"Run chat" = "运行聊天"; -/* chat item action */ +/* No comment provided by engineer. */ +"Safely receive files" = "安全接收文件"; + +/* No comment provided by engineer. */ +"Safer groups" = "更安全的群组"; + +/* alert button +chat item action */ "Save" = "保存"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "保存(并通知联系人)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "保存并通知联系人"; /* No comment provided by engineer. */ "Save and notify group members" = "保存并通知群组成员"; +/* No comment provided by engineer. */ +"Save and reconnect" = "保存并重新连接"; + /* No comment provided by engineer. */ "Save and update group profile" = "保存和更新组配置文件"; -/* No comment provided by engineer. */ -"Save archive" = "保存存档"; - -/* No comment provided by engineer. */ -"Save auto-accept settings" = "保存自动接受设置"; - /* No comment provided by engineer. */ "Save group profile" = "保存群组资料"; +/* No comment provided by engineer. */ +"Save list" = "保存列表"; + /* No comment provided by engineer. */ "Save passphrase and open chat" = "保存密码并打开聊天"; /* No comment provided by engineer. */ "Save passphrase in Keychain" = "在钥匙串中保存密码"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "保存偏好设置?"; /* No comment provided by engineer. */ @@ -2696,36 +4270,75 @@ /* No comment provided by engineer. */ "Save servers" = "保存服务器"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "保存服务器?"; -/* No comment provided by engineer. */ -"Save settings?" = "保存设置?"; - /* No comment provided by engineer. */ "Save welcome message?" = "保存欢迎信息?"; +/* alert title */ +"Save your profile?" = "保存您的个人资料?"; + +/* No comment provided by engineer. */ +"saved" = "已保存"; + +/* No comment provided by engineer. */ +"Saved" = "已保存"; + +/* No comment provided by engineer. */ +"Saved from" = "保存自"; + +/* No comment provided by engineer. */ +"saved from %@" = "保存自 %@"; + +/* message info title */ +"Saved message" = "已保存的消息"; + /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "已保存的WebRTC ICE服务器将被删除"; +/* No comment provided by engineer. */ +"Saving %lld messages" = "正在保存 %lld 条消息"; + +/* No comment provided by engineer. */ +"Scale" = "规模"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "扫描 / 粘贴链接"; + /* No comment provided by engineer. */ "Scan code" = "扫码"; /* No comment provided by engineer. */ "Scan QR code" = "扫描二维码"; +/* No comment provided by engineer. */ +"Scan QR code from desktop" = "从桌面扫描二维码"; + /* No comment provided by engineer. */ "Scan security code from your contact's app." = "从您联系人的应用程序中扫描安全码。"; /* No comment provided by engineer. */ "Scan server QR code" = "扫描服务器二维码"; +/* No comment provided by engineer. */ +"search" = "搜索"; + /* No comment provided by engineer. */ "Search" = "搜索"; +/* No comment provided by engineer. */ +"Search bar accepts invitation links." = "搜索栏接受邀请链接。"; + +/* No comment provided by engineer. */ +"Search or paste SimpleX link" = "搜索或粘贴 SimpleX 链接"; + /* network option */ "sec" = "秒"; +/* No comment provided by engineer. */ +"Secondary" = "二级"; + /* time unit */ "seconds" = "秒"; @@ -2735,6 +4348,9 @@ /* server test step */ "Secure queue" = "保护队列"; +/* No comment provided by engineer. */ +"Secured" = "担保"; + /* No comment provided by engineer. */ "Security assessment" = "安全评估"; @@ -2744,9 +4360,15 @@ /* chat item text */ "security code changed" = "安全密码已更改"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "选择"; +/* No comment provided by engineer. */ +"Selected %lld" = "选定的 %lld"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "选定的聊天首选项禁止此消息。"; + /* No comment provided by engineer. */ "Self-destruct" = "自毁"; @@ -2771,15 +4393,15 @@ /* No comment provided by engineer. */ "send direct message" = "发送私信"; -/* No comment provided by engineer. */ -"Send direct message" = "发送私信"; - /* No comment provided by engineer. */ "Send direct message to connect" = "发送私信来连接"; /* No comment provided by engineer. */ "Send disappearing message" = "发送限时消息中"; +/* No comment provided by engineer. */ +"Send errors" = "发送错误"; + /* No comment provided by engineer. */ "Send link previews" = "发送链接预览"; @@ -2787,10 +4409,16 @@ "Send live message" = "发送实时消息"; /* No comment provided by engineer. */ -"Send notifications" = "发送通知"; +"Send message to enable calls." = "发送消息以启用呼叫。"; /* No comment provided by engineer. */ -"Send notifications:" = "发送通知:"; +"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." = "当您或目标服务器不支持私有路由时,直接发送消息。"; + +/* No comment provided by engineer. */ +"Send notifications" = "发送通知"; /* No comment provided by engineer. */ "Send questions and ideas" = "发送问题和想法"; @@ -2802,6 +4430,9 @@ "Send them from gallery or custom keyboards." = "发送它们来自图库或自定义键盘。"; /* No comment provided by engineer. */ +"Send up to 100 last messages to new members." = "给新成员发送最多 100 条历史消息。"; + +/* alert message */ "Sender cancelled file transfer." = "发送人已取消文件传输。"; /* No comment provided by engineer. */ @@ -2837,15 +4468,42 @@ /* copied message info */ "Sent at: %@" = "已发送于:%@"; +/* No comment provided by engineer. */ +"Sent directly" = "直接发送"; + /* notification */ "Sent file event" = "已发送文件项目"; /* message info title */ "Sent message" = "已发信息"; +/* No comment provided by engineer. */ +"Sent messages" = "已发送的消息"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "已发送的消息将在设定的时间后被删除。"; +/* No comment provided by engineer. */ +"Sent reply" = "已发送回复"; + +/* No comment provided by engineer. */ +"Sent total" = "发送总数"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "通过代理发送"; + +/* No comment provided by engineer. */ +"Server address" = "服务器地址"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "服务器地址与网络设置不兼容:%@。"; + +/* srv error text. */ +"Server address is incompatible with network settings." = "服务器地址与网络设置不兼容。"; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "服务器队列信息: %1$@\n\n上次收到的消息: %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "服务器需要授权才能创建队列,检查密码"; @@ -2855,24 +4513,54 @@ /* No comment provided by engineer. */ "Server test failed!" = "服务器测试失败!"; +/* No comment provided by engineer. */ +"Server type" = "服务器类型"; + +/* srv error text */ +"Server version is incompatible with network settings." = "服务器版本与网络设置不兼容。"; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "服务器版本与你的应用程序不兼容:%@。"; + /* No comment provided by engineer. */ "Servers" = "服务器"; +/* No comment provided by engineer. */ +"Servers info" = "服务器信息"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "服务器统计信息将被重置 - 此操作无法撤消!"; + +/* No comment provided by engineer. */ +"Session code" = "会话码"; + /* No comment provided by engineer. */ "Set 1 day" = "设定1天"; /* No comment provided by engineer. */ "Set contact name…" = "设置联系人姓名……"; +/* No comment provided by engineer. */ +"Set default theme" = "设置默认主题"; + /* No comment provided by engineer. */ "Set group preferences" = "设置群组偏好设置"; /* No comment provided by engineer. */ "Set it instead of system authentication." = "设置它以代替系统身份验证。"; +/* profile update event chat item */ +"set new contact address" = "设置新的联系地址"; + +/* profile update event chat item */ +"set new profile picture" = "设置新的资料图片"; + /* No comment provided by engineer. */ "Set passcode" = "设置密码"; +/* No comment provided by engineer. */ +"Set passphrase" = "设置密码短语"; + /* No comment provided by engineer. */ "Set passphrase to export" = "设置密码来导出"; @@ -2885,7 +4573,11 @@ /* No comment provided by engineer. */ "Settings" = "设置"; -/* chat item action */ +/* No comment provided by engineer. */ +"Shape profile images" = "改变个人资料图形状"; + +/* alert action +chat item action */ "Share" = "分享"; /* No comment provided by engineer. */ @@ -2894,18 +4586,27 @@ /* No comment provided by engineer. */ "Share address" = "分享地址"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "与联系人分享地址?"; +/* No comment provided by engineer. */ +"Share from other apps." = "从其他应用程序共享。"; + /* No comment provided by engineer. */ "Share link" = "分享链接"; /* No comment provided by engineer. */ -"Share one-time invitation link" = "分享一次性邀请链接"; +"Share this 1-time invite link" = "分享此一次性邀请链接"; + +/* No comment provided by engineer. */ +"Share to SimpleX" = "分享到 SimpleX"; /* No comment provided by engineer. */ "Share with contacts" = "与联系人分享"; +/* No comment provided by engineer. */ +"Show → on messages sent via private routing." = "显示 → 通过专用路由发送的信息."; + /* No comment provided by engineer. */ "Show calls in phone history" = "在电话历史记录中显示通话"; @@ -2915,18 +4616,42 @@ /* No comment provided by engineer. */ "Show last messages" = "显示最近的消息"; +/* No comment provided by engineer. */ +"Show message status" = "显示消息状态"; + +/* No comment provided by engineer. */ +"Show percentage" = "显示百分比"; + /* No comment provided by engineer. */ "Show preview" = "显示预览"; +/* No comment provided by engineer. */ +"Show QR code" = "显示二维码"; + /* No comment provided by engineer. */ "Show:" = "显示:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "SimpleX 地址"; /* No comment provided by engineer. */ "SimpleX Address" = "SimpleX 地址"; +/* No comment provided by engineer. */ +"SimpleX address and 1-time links are safe to share via any messenger." = "可以通过任何消息应用安全分享 SimpleX 地址和一次性链接。"; + +/* No comment provided by engineer. */ +"SimpleX address or 1-time link?" = "SimpleX 地址或一次性链接?"; + +/* simplex link type */ +"SimpleX channel link" = "SimpleX 频道链接"; + +/* No comment provided by engineer. */ +"SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "SimpleX Chat 与 Flux 达成了协议,将由 Flux 控制的服务器纳入 SimpleX 应用。"; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "SimpleX Chat 的安全性 由 Trail of Bits 审核。"; @@ -2934,13 +4659,19 @@ "SimpleX contact address" = "SimpleX 联系地址"; /* notification */ -"SimpleX encrypted message or connection event" = "SimpleX 加密消息或连接项目"; +"SimpleX encrypted message or connection event" = "SimpleX 加密的消息或连接事件"; /* simplex link type */ -"SimpleX group link" = "SimpleX 群组链接"; +"SimpleX group link" = "SimpleX 群链接"; + +/* chat feature */ +"SimpleX links" = "SimpleX 链接"; /* No comment provided by engineer. */ -"SimpleX links" = "SimpleX 链接"; +"SimpleX links are prohibited." = "此群禁止 SimpleX 链接。"; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "不允许SimpleX 链接"; /* No comment provided by engineer. */ "SimpleX Lock" = "SimpleX 锁定"; @@ -2957,9 +4688,15 @@ /* simplex link type */ "SimpleX one-time invitation" = "SimpleX 一次性邀请"; +/* No comment provided by engineer. */ +"SimpleX protocols reviewed by Trail of Bits." = "SimpleX 协议由 Trail of Bits 审阅。"; + /* No comment provided by engineer. */ "Simplified incognito mode" = "简化的隐身模式"; +/* No comment provided by engineer. */ +"Size" = "大小"; + /* No comment provided by engineer. */ "Skip" = "跳过"; @@ -2970,31 +4707,55 @@ "Small groups (max 20)" = "小群组(最多 20 人)"; /* No comment provided by engineer. */ -"SMP servers" = "SMP 服务器"; +"SMP server" = "SMP 服务器"; + +/* blur media */ +"Soft" = "软"; + +/* No comment provided by engineer. */ +"Some app settings were not migrated." = "部分应用设置未被迁移。"; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "某些文件未导出:"; /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "导入过程中发生了一些非致命错误——您可以查看聊天控制台了解更多详细信息。"; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "导入过程中出现一些非致命错误:"; + /* notification title */ "Somebody" = "某人"; /* No comment provided by engineer. */ -"Start a new chat" = "开始新聊天"; +"Square, circle, or anything in between." = "方形、圆形、或两者之间的任意形状."; + +/* chat item text */ +"standard end-to-end encryption" = "标准端到端加密"; /* No comment provided by engineer. */ "Start chat" = "开始聊天"; +/* No comment provided by engineer. */ +"Start chat?" = "启动聊天吗?"; + /* No comment provided by engineer. */ "Start migration" = "开始迁移"; +/* No comment provided by engineer. */ +"Starting from %@." = "从 %@ 开始。"; + /* No comment provided by engineer. */ "starting…" = "启动中……"; +/* No comment provided by engineer. */ +"Statistics" = "统计"; + /* No comment provided by engineer. */ "Stop" = "停止"; /* No comment provided by engineer. */ -"Stop chat to enable database actions" = "停止聊天以启用数据库操作"; +"Stop chat" = "停止聊天程序"; /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "停止聊天以便导出、导入或删除聊天数据库。在聊天停止期间,您将无法收发消息。"; @@ -3011,21 +4772,36 @@ /* No comment provided by engineer. */ "Stop sending file?" = "停止发送文件?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "停止分享"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "停止分享地址?"; /* authentication reason */ "Stop SimpleX" = "停止 SimpleX"; +/* No comment provided by engineer. */ +"Stopping chat" = "正在停止聊天"; + /* No comment provided by engineer. */ "strike" = "删去"; +/* blur media */ +"Strong" = "加粗"; + /* No comment provided by engineer. */ "Submit" = "提交"; +/* No comment provided by engineer. */ +"Subscribed" = "已订阅"; + +/* No comment provided by engineer. */ +"Subscription errors" = "订阅错误"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "忽略订阅"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "支持 SimpleX Chat"; @@ -3044,6 +4820,9 @@ /* No comment provided by engineer. */ "Tap to activate profile." = "点击以激活个人资料。"; +/* No comment provided by engineer. */ +"Tap to Connect" = "轻按连接"; + /* No comment provided by engineer. */ "Tap to join" = "点击加入"; @@ -3051,7 +4830,13 @@ "Tap to join incognito" = "点击以加入隐身聊天"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "点击开始一个新聊天"; +"Tap to paste link" = "轻按粘贴链接"; + +/* No comment provided by engineer. */ +"Tap to scan" = "轻按扫描"; + +/* No comment provided by engineer. */ +"TCP connection" = "TCP 连接"; /* No comment provided by engineer. */ "TCP connection timeout" = "TCP 连接超时"; @@ -3065,6 +4850,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* file error alert title */ +"Temporary file error" = "临时文件错误"; + /* server test failure */ "Test failed at step %@." = "在步骤 %@ 上测试失败。"; @@ -3074,7 +4862,7 @@ /* No comment provided by engineer. */ "Test servers" = "测试服务器"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "测试失败!"; /* No comment provided by engineer. */ @@ -3086,15 +4874,18 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "感谢用户——通过 Weblate 做出贡献!"; -/* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "第一个没有任何用户标识符的平台 - 隐私设计."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "该应用可以在您收到消息或联系人请求时通知您——请打开设置以启用通知。"; +/* No comment provided by engineer. */ +"The app will ask to confirm downloads from unknown file servers (except .onion)." = "该应用程序将要求确认从未知文件服务器(.onion 除外)下载。"; + /* No comment provided by engineer. */ "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." = "您扫描的码不是 SimpleX 链接的二维码。"; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "您接受的连接将被取消!"; @@ -3107,6 +4898,9 @@ /* 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." = "上一条消息的散列不同。"; @@ -3120,13 +4914,16 @@ "The message will be marked as moderated for all members." = "该消息将对所有成员标记为已被管理员移除。"; /* No comment provided by engineer. */ -"The next generation of private messaging" = "下一代私密通讯软件"; +"The messages will be deleted for all members." = "将删除所有成员的消息。"; + +/* No comment provided by engineer. */ +"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 profile is only shared with your contacts." = "该资料仅与您的联系人共享。"; +"Your profile is stored on your device and only shared with your contacts." = "该资料仅与您的联系人共享。"; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "我们错过的第二个\"√\"!✅"; @@ -3138,13 +4935,10 @@ "The servers for new connections of your current chat profile **%@**." = "您当前聊天资料 **%@** 的新连接服务器。"; /* No comment provided by engineer. */ -"Theme" = "主题"; +"The text you pasted is not a SimpleX link." = "您粘贴的文本不是 SimpleX 链接。"; /* No comment provided by engineer. */ -"There should be at least one user profile." = "应该至少有一个用户资料。"; - -/* No comment provided by engineer. */ -"There should be at least one visible user profile." = "应该至少有一个可见的用户资料。"; +"Themes" = "主题"; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "这些设置适用于您当前的配置文件 **%@**。"; @@ -3161,18 +4955,42 @@ /* No comment provided by engineer. */ "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." = "此聊天受端到端加密保护。"; + +/* E2EE info chat item */ +"This chat is protected by quantum resistant end-to-end encryption." = "此聊天受抗量子的端到端加密保护。"; + /* notification title */ "this contact" = "这个联系人"; +/* No comment provided by engineer. */ +"This device name" = "此设备名称"; + +/* No comment provided by engineer. */ +"This display name is invalid. Please choose another name." = "显示名无效。请另选一个名称。"; + /* No comment provided by engineer. */ "This group has over %lld members, delivery receipts are not sent." = "该组有超过 %lld 个成员,不发送送货单。"; /* No comment provided by engineer. */ "This group no longer exists." = "该群组已不存在。"; +/* No comment provided by engineer. */ +"This is your own one-time link!" = "这是你自己的一次性链接!"; + +/* No comment provided by engineer. */ +"This is your own SimpleX address!" = "这是你自己的 SimpleX 地址!"; + +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "此链接已在其他移动设备上使用,请在桌面上创建新链接。"; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "此设置适用于您当前聊天资料 **%@** 中的消息。"; +/* No comment provided by engineer. */ +"Title" = "标题"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "要提出任何问题并接收更新,请:"; @@ -3180,10 +4998,10 @@ "To connect, your contact can scan QR code or use the link in the app." = "您的联系人可以扫描二维码或使用应用程序中的链接来建立连接。"; /* No comment provided by engineer. */ -"To make a new connection" = "建立新连接"; +"To hide unwanted messages." = "隐藏不需要的信息。"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "为了保护隐私,SimpleX使用针对消息队列的标识符,而不是所有其他平台使用的用户ID,每个联系人都有独立的标识符。"; +"To make a new connection" = "建立新连接"; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "为了保护时区,图像/语音文件使用 UTC。"; @@ -3191,6 +5009,12 @@ /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "为保护您的信息,请打开 SimpleX 锁定。\n在启用此功能之前,系统将提示您完成身份验证。"; +/* No comment provided by engineer. */ +"To 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使用针对消息队列的标识符,而不是所有其他平台使用的用户ID,每个联系人都有独立的标识符。"; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "请授权使用麦克风以录制语音消息。"; @@ -3203,12 +5027,24 @@ /* 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." = "在连接时切换隐身模式。"; +/* No comment provided by engineer. */ +"Toolbar opacity" = "工具栏不透明度"; + +/* No comment provided by engineer. */ +"Total" = "共计"; + /* No comment provided by engineer. */ "Transport isolation" = "传输隔离"; +/* No comment provided by engineer. */ +"Transport sessions" = "传输会话"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "正在尝试连接到用于从该联系人接收消息的服务器(错误:%@)。"; @@ -3216,10 +5052,10 @@ "Trying to connect to the server used to receive messages from this contact." = "正在尝试连接到用于从该联系人接收消息的服务器。"; /* No comment provided by engineer. */ -"Turn off" = "关闭"; +"Turkish interface" = "土耳其语界面"; /* No comment provided by engineer. */ -"Turn off notifications?" = "关闭通知?"; +"Turn off" = "关闭"; /* No comment provided by engineer. */ "Turn on" = "打开"; @@ -3227,13 +5063,28 @@ /* No comment provided by engineer. */ "Unable to record voice message" = "无法录制语音消息"; -/* item status description */ -"Unexpected error: %@" = "意外错误: %@"; +/* No comment provided by engineer. */ +"Unblock" = "解封"; + +/* No comment provided by engineer. */ +"Unblock for all" = "为所有人解封"; + +/* No comment provided by engineer. */ +"Unblock member" = "解封成员"; + +/* No comment provided by engineer. */ +"Unblock member for all?" = "为所有其他成员解封该成员?"; + +/* No comment provided by engineer. */ +"Unblock member?" = "解封成员吗?"; + +/* rcv group event chat item */ +"unblocked %@" = "未阻止 %@"; /* No comment provided by engineer. */ "Unexpected migration state" = "未预料的迁移状态"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "取消最喜欢"; /* No comment provided by engineer. */ @@ -3260,29 +5111,47 @@ /* No comment provided by engineer. */ "Unknown error" = "未知错误"; +/* No comment provided by engineer. */ +"unknown servers" = "未知服务器"; + +/* alert title */ +"Unknown servers!" = "未知服务器!"; + +/* No comment provided by engineer. */ +"unknown status" = "未知状态"; + /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "除非您使用 iOS 通话界面,否则请启用请勿打扰模式以避免打扰。"; /* No comment provided by engineer. */ "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "除非您的联系人已删除此连接或此链接已被使用,否则它可能是一个错误——请报告。\n如果要连接,请让您的联系人创建另一个连接链接,并检查您的网络连接是否稳定。"; +/* No comment provided by engineer. */ +"Unlink" = "取消链接"; + +/* No comment provided by engineer. */ +"Unlink desktop?" = "取消链接桌面端?"; + /* No comment provided by engineer. */ "Unlock" = "解锁"; /* authentication reason */ "Unlock app" = "解锁应用程序"; -/* No comment provided by engineer. */ +/* notification label action */ "Unmute" = "取消静音"; /* No comment provided by engineer. */ +"unprotected" = "未受保护"; + +/* swipe action */ "Unread" = "未读"; /* No comment provided by engineer. */ -"Update" = "更新"; +"Up to 100 last messages are sent to new members." = "给新成员发送了最多 100 条历史消息。"; /* No comment provided by engineer. */ -"Update .onion hosts setting?" = "更新 .onion 主机设置?"; +"Update" = "更新"; /* No comment provided by engineer. */ "Update database passphrase" = "更新数据库密码"; @@ -3291,23 +5160,38 @@ "Update network settings?" = "更新网络设置?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "更新传输隔离模式?"; +"Update settings?" = "更新设置?"; /* rcv group event chat item */ "updated group profile" = "已更新的群组资料"; +/* profile update event chat item */ +"updated profile" = "更新了资料"; + /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "更新设置会将客户端重新连接到所有服务器。"; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "更新此设置将重新连接客户端到所有服务器。"; +"Upgrade and open chat" = "升级并打开聊天"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "升级并打开聊天"; +"Upload errors" = "上传错误"; + +/* No comment provided by engineer. */ +"Upload failed" = "上传失败了"; /* server test step */ "Upload file" = "上传文件"; +/* No comment provided by engineer. */ +"Uploaded" = "已上传"; + +/* No comment provided by engineer. */ +"Uploaded files" = "已上传的文件"; + +/* No comment provided by engineer. */ +"Uploading archive" = "正在上传存档"; + /* No comment provided by engineer. */ "Use .onion hosts" = "使用 .onion 主机"; @@ -3320,12 +5204,24 @@ /* No comment provided by engineer. */ "Use for new connections" = "用于新连接"; +/* No comment provided by engineer. */ +"Use from desktop" = "从桌面端使用"; + /* No comment provided by engineer. */ "Use iOS call interface" = "使用 iOS 通话界面"; /* No comment provided by engineer. */ "Use new incognito profile" = "使用新的隐身配置文件"; +/* No comment provided by engineer. */ +"Use only local notifications?" = "仅使用本地通知?"; + +/* No comment provided by engineer. */ +"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 server" = "使用服务器"; @@ -3333,20 +5229,41 @@ "Use SimpleX Chat servers?" = "使用 SimpleX Chat 服务器?"; /* No comment provided by engineer. */ -"User profile" = "用户资料"; +"Use the app while in the call." = "通话时使用本应用."; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "使用 .onion 主机需要兼容的 VPN 提供商。"; +"Use the app with one hand." = "用一只手使用应用程序。"; + +/* No comment provided by engineer. */ +"User selection" = "用户选择"; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "使用 SimpleX Chat 服务器。"; +/* No comment provided by engineer. */ +"v%@" = "v%@"; + /* No comment provided by engineer. */ "v%@ (%@)" = "v%@ (%@)"; +/* No comment provided by engineer. */ +"Verify code with desktop" = "用桌面端验证代码"; + +/* No comment provided by engineer. */ +"Verify connection" = "验证连接"; + /* No comment provided by engineer. */ "Verify connection security" = "验证连接安全"; +/* No comment provided by engineer. */ +"Verify connections" = "验证连接"; + +/* No comment provided by engineer. */ +"Verify database passphrase" = "验证数据库密码短语"; + +/* No comment provided by engineer. */ +"Verify passphrase" = "验证密码短语"; + /* No comment provided by engineer. */ "Verify security code" = "验证安全码"; @@ -3365,6 +5282,12 @@ /* No comment provided by engineer. */ "via relay" = "通过中继"; +/* No comment provided by engineer. */ +"Via secure quantum resistant protocol." = "通过安全的、抗量子计算机破解的协议。"; + +/* No comment provided by engineer. */ +"video" = "视频"; + /* No comment provided by engineer. */ "Video call" = "视频通话"; @@ -3383,6 +5306,9 @@ /* No comment provided by engineer. */ "View security code" = "查看安全码"; +/* chat feature */ +"Visible history" = "可见的历史"; + /* No comment provided by engineer. */ "Voice message…" = "语音消息……"; @@ -3393,7 +5319,10 @@ "Voice messages are prohibited in this chat." = "语音信息在此聊天中被禁止。"; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "语音信息在该群组中被禁用。"; +"Voice messages are prohibited." = "语音信息在该群组中被禁用。"; + +/* No comment provided by engineer. */ +"Voice messages not allowed" = "不允许语音消息"; /* No comment provided by engineer. */ "Voice messages prohibited!" = "语音消息禁止发送!"; @@ -3404,6 +5333,9 @@ /* No comment provided by engineer. */ "waiting for confirmation…" = "等待确认中……"; +/* No comment provided by engineer. */ +"Waiting for desktop..." = "正在等待桌面..."; + /* No comment provided by engineer. */ "Waiting for file" = "等待文件中"; @@ -3413,9 +5345,18 @@ /* No comment provided by engineer. */ "Waiting for video" = "等待视频中"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "壁纸装饰"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "壁纸背景"; + /* No comment provided by engineer. */ "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" = "警告:不支持在多部设备上启动聊天,这么做会导致消息传送失败"; + /* No comment provided by engineer. */ "Warning: you may lose some data!" = "警告:您可能会丢失部分数据!"; @@ -3431,6 +5372,9 @@ /* No comment provided by engineer. */ "Welcome message" = "欢迎消息"; +/* No comment provided by engineer. */ +"Welcome message is too long" = "欢迎消息太大了"; + /* No comment provided by engineer. */ "What's new" = "更新内容"; @@ -3438,28 +5382,61 @@ "When available" = "当可用时"; /* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "当人们请求连接时,您可以接受或拒绝它。"; +"When connecting audio and video calls." = "连接音频和视频通话时。"; + +/* No comment provided by engineer. */ +"when IP hidden" = "当 IP 隐藏时"; /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。"; +/* No comment provided by engineer. */ +"WiFi" = "WiFi"; + +/* No comment provided by engineer. */ +"Will be enabled in direct chats!" = "将在私聊中启用!"; + +/* No comment provided by engineer. */ +"Wired ethernet" = "有线以太网"; + +/* No comment provided by engineer. */ +"With encrypted files and media." = "加密的文件和媒体。"; + /* No comment provided by engineer. */ "With optional welcome message." = "带有可选的欢迎消息。"; +/* No comment provided by engineer. */ +"With reduced battery usage." = "降低了电量使用。"; + +/* No comment provided by engineer. */ +"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: %@." = "如果没有 Tor 或 VPN,您的 IP 地址将对以下 XFTP 中继可见:%@。"; + /* No comment provided by engineer. */ "Wrong database passphrase" = "数据库密码错误"; +/* snd error text */ +"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." = "密钥错误或文件块地址未知 - 很可能文件已删除。"; + /* No comment provided by engineer. */ "Wrong passphrase!" = "密码错误!"; /* No comment provided by engineer. */ -"XFTP servers" = "XFTP 服务器"; +"XFTP server" = "XFTP 服务器"; /* pref value */ "yes" = "是"; /* No comment provided by engineer. */ -"You" = "您"; +"you" = "您"; + +/* No comment provided by engineer. */ +"You **must not** use the same database on two devices." = "您 **不得** 在两台设备上使用相同的数据库。"; /* No comment provided by engineer. */ "You accepted connection" = "您已接受连接"; @@ -3473,6 +5450,27 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "您已经连接到 %@。"; +/* No comment provided by engineer. */ +"You are already connecting to %@." = "您已连接到 %@。"; + +/* No comment provided by engineer. */ +"You are already connecting via this one-time link!" = "你已经在通过这个一次性链接进行连接!"; + +/* No comment provided by engineer. */ +"You are already in group %@." = "您已在组 %@ 中。"; + +/* No comment provided by engineer. */ +"You are already joining the group %@." = "您已加入组 %@。"; + +/* No comment provided by engineer. */ +"You are already joining the group via this link!" = "您已经通过此链接加入群组!"; + +/* No comment provided by engineer. */ +"You are already joining the group via this link." = "你已经在通过此链接加入该群。"; + +/* No comment provided by engineer. */ +"You are already joining the group!\nRepeat join request?" = "您已经加入了这个群组!\n重复加入请求?"; + /* No comment provided by engineer. */ "You are connected to the server used to receive messages from this contact." = "您已连接到用于接收该联系人消息的服务器。"; @@ -3482,14 +5480,20 @@ /* No comment provided by engineer. */ "You are invited to group" = "您被邀请加入群组"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "您未连接到这些服务器。私有路由用于向他们发送消息。"; + /* No comment provided by engineer. */ "you are observer" = "您是观察者"; +/* snd group event chat item */ +"you blocked %@" = "你阻止了%@"; + /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "您可以从锁屏上接听电话,无需设备和应用程序的认证。"; /* No comment provided by engineer. */ -"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "您也可以通过点击链接进行连接。如果在浏览器中打开,请点击“在移动应用程序中打开”按钮。"; +"You can change it in Appearance settings." = "您可以在外观设置中更改它。"; /* No comment provided by engineer. */ "You can create it later" = "您可以以后创建它"; @@ -3500,11 +5504,20 @@ /* No comment provided by engineer. */ "You can enable them later via app Privacy & Security settings." = "您可以稍后通过应用程序的 \"隐私与安全 \"设置启用它们。"; +/* No comment provided by engineer. */ +"You can give another try." = "你可以再试一次。"; + /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "您可以隐藏或静音用户个人资料——只需向右滑动。"; +/* No comment provided by engineer. */ +"You can make it visible to your SimpleX contacts via Settings." = "你可以通过设置让它对你的 SimpleX 联系人可见。"; + /* notification body */ -"You can now send messages to %@" = "您现在可以给 %@ 发送消息"; +"You can now chat with %@" = "您现在可以给 %@ 发送消息"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "您可以从存档的联系人向%@发送消息。"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "您可以通过设置来设置锁屏通知预览。"; @@ -3516,10 +5529,10 @@ "You can share this address with your contacts to let them connect with **%@**." = "您可以与您的联系人分享该地址,让他们与 **%@** 联系。"; /* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "您可以将您的地址作为链接或二维码共享——任何人都可以连接到您。"; +"You can start chat via app Settings / Database or by restarting the app" = "您可以通过应用程序设置/数据库或重新启动应用程序开始聊天"; /* No comment provided by engineer. */ -"You can start chat via app Settings / Database or by restarting the app" = "您可以通过应用程序设置/数据库或重新启动应用程序开始聊天"; +"You can still view conversation with %@ in the list of chats." = "您仍然可以在聊天列表中查看与 %@的对话。"; /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "您可以通过设置开启 SimpleX 锁定。"; @@ -3527,6 +5540,9 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "您可以使用 markdown 来编排消息格式:"; +/* alert message */ +"You can view invitation link again in connection details." = "您可以在连接详情中再次查看邀请链接。"; + /* No comment provided by engineer. */ "You can't send messages!" = "您无法发送消息!"; @@ -3542,14 +5558,17 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "您已将 %1$@ 的角色更改为 %2$@"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "您可以控制接收信息使用的服务器,您的联系人则使用您发送信息时所使用的服务器。"; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "您的身份无法验证,请再试一次。"; /* No comment provided by engineer. */ -"You have no chats" = "您没有聊天记录"; +"You decide who can connect." = "你决定谁可以连接。"; + +/* No comment provided by engineer. */ +"You have already requested connection via this address!" = "你已经请求通过此地址进行连接!"; + +/* No comment provided by engineer. */ +"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." = "您必须在每次应用程序启动时输入密码——它不存储在设备上。"; @@ -3566,9 +5585,18 @@ /* snd group event chat item */ "you left" = "您已离开"; +/* No comment provided by engineer. */ +"You may migrate the exported database." = "您可以迁移导出的数据库。"; + +/* No comment provided by engineer. */ +"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." = "您只能在一台设备上使用最新版本的聊天数据库,否则您可能会停止接收来自某些联系人的消息。"; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "您需要允许您的联系人呼叫才能呼叫他们。"; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "您需要允许您的联系人发送语音消息,以便您能够发送语音消息。"; @@ -3587,9 +5615,15 @@ /* chat list item description */ "you shared one-time link incognito" = "您分享了一次性链接隐身聊天"; +/* snd group event chat item */ +"you unblocked %@" = "您解封了 %@"; + /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "您将在组主设备上线时连接到该群组,请稍等或稍后再检查!"; +/* No comment provided by engineer. */ +"You will be connected when group link host's device is online, please wait or check later!" = "当 Group Link Host 的设备在线时,您将被连接,请稍候或稍后检查!"; + /* No comment provided by engineer. */ "You will be connected when your connection request is accepted, please wait or check later!" = "当您的连接请求被接受后,您将可以连接,请稍等或稍后检查!"; @@ -3599,6 +5633,9 @@ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "当您启动应用或在应用程序驻留后台超过30 秒后,您将需要进行身份验证。"; +/* No comment provided by engineer. */ +"You will connect to all group members." = "你将连接到所有群成员。"; + /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "当静音配置文件处于活动状态时,您仍会收到来自静音配置文件的电话和通知。"; @@ -3617,9 +5654,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "您正在为该群组使用隐身个人资料——为防止共享您的主要个人资料,不允许邀请联系人"; -/* No comment provided by engineer. */ -"Your %@ servers" = "您的 %@ 服务器"; - /* No comment provided by engineer. */ "Your calls" = "您的通话"; @@ -3632,18 +5666,12 @@ /* No comment provided by engineer. */ "Your chat profiles" = "您的聊天资料"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "您的联系人需要在线才能完成连接。\n您可以取消此连接并删除联系人(然后尝试使用新链接)。"; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "您的联系人发送的文件大于当前支持的最大大小 (%@)。"; /* No comment provided by engineer. */ "Your contacts can allow full message deletion." = "您的联系人可以允许完全删除消息。"; -/* No comment provided by engineer. */ -"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "您的 SimpleX 的联系人会看到它。\n您可以在设置中更改它。"; - /* No comment provided by engineer. */ "Your contacts will remain connected." = "与您的联系人保持连接。"; @@ -3662,11 +5690,14 @@ /* No comment provided by engineer. */ "Your privacy" = "您的隐私设置"; +/* No comment provided by engineer. */ +"Your profile" = "您的个人资料"; + /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "您的个人资料 **%@** 将被共享。"; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "您的资料存储在您的设备上并仅与您的联系人共享。\nSimpleX 服务器无法看到您的资料。"; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "您的资料存储在您的设备上并仅与您的联系人共享。 SimpleX 服务器无法看到您的资料。"; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "您的资料、联系人和发送的消息存储在您的设备上。"; @@ -3674,9 +5705,6 @@ /* No comment provided by engineer. */ "Your random profile" = "您的随机资料"; -/* No comment provided by engineer. */ -"Your server" = "您的服务器"; - /* No comment provided by engineer. */ "Your server address" = "您的服务器地址"; @@ -3686,9 +5714,3 @@ /* No comment provided by engineer. */ "Your SimpleX address" = "您的 SimpleX 地址"; -/* No comment provided by engineer. */ -"Your SMP servers" = "您的 SMP 服务器"; - -/* No comment provided by engineer. */ -"Your XFTP servers" = "您的 XFTP 服务器"; - diff --git a/apps/ios/zh-Hans.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/zh-Hans.lproj/SimpleX--iOS--InfoPlist.strings index b3192851c8..199d5faf7c 100644 --- a/apps/ios/zh-Hans.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/zh-Hans.lproj/SimpleX--iOS--InfoPlist.strings @@ -7,6 +7,9 @@ /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX 使用Face ID进行本地身份验证"; +/* Privacy - Local Network Usage Description */ +"NSLocalNetworkUsageDescription" = "SimpleX 使用本地网络访问,允许通过同一网络上的桌面应用程序使用用户聊天配置文件。"; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX 需要麦克风访问权限才能进行音频和视频通话,以及录制语音消息。"; diff --git a/apps/multiplatform/android/build.gradle.kts b/apps/multiplatform/android/build.gradle.kts index a9909d2f6f..8fc9d104d2 100644 --- a/apps/multiplatform/android/build.gradle.kts +++ b/apps/multiplatform/android/build.gradle.kts @@ -8,12 +8,14 @@ plugins { } android { - compileSdkVersion(34) + compileSdk = 34 defaultConfig { applicationId = "chat.simplex.app" - minSdkVersion(28) - targetSdkVersion(33) + namespace = "chat.simplex.app" + minSdk = 26 + //noinspection OldTargetApi + targetSdk = 34 // !!! // skip version code after release to F-Droid, as it uses two version codes versionCode = (extra["android.version_code"] as String).toInt() @@ -46,12 +48,7 @@ android { proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } kotlinOptions { - jvmTarget = "1.8" freeCompilerArgs += "-opt-in=kotlinx.coroutines.DelicateCoroutinesApi" freeCompilerArgs += "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi" freeCompilerArgs += "-opt-in=androidx.compose.ui.text.ExperimentalTextApi" @@ -71,38 +68,45 @@ android { isMinifyEnabled = false } } - packagingOptions { + buildFeatures { + buildConfig = true + } + packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } jniLibs.useLegacyPackaging = rootProject.extra["compression.level"] as Int != 0 } android.sourceSets["main"].assets.setSrcDirs(listOf("../common/src/commonMain/resources/assets")) - val isRelease = gradle.startParameter.taskNames.find { it.toLowerCase().contains("release") } != null - val isBundle = gradle.startParameter.taskNames.find { it.toLowerCase().contains("bundle") } != null - // if (isRelease) { + val isRelease = gradle.startParameter.taskNames.find { it.lowercase().contains("release") } != null + val isBundle = gradle.startParameter.taskNames.find { it.lowercase().contains("bundle") } != null // Comma separated list of languages that will be included in the apk - android.defaultConfig.resConfigs( + android.defaultConfig.resourceConfigurations += listOf( "en", "ar", "bg", + "ca", "cs", "de", "es", + "fa", "fi", "fr", + "hu", "it", "iw", "ja", + "lt", "nl", "pl", "pt-rBR", "ru", "th", + "tr", "uk", "zh-rCN" ) - // } + ndkVersion = "23.1.7779620" if (isBundle) { defaultConfig.ndk.abiFilters("arm64-v8a", "armeabi-v7a") } else { @@ -123,41 +127,44 @@ android { dependencies { implementation(project(":common")) - implementation("androidx.core:core-ktx:1.7.0") + implementation("androidx.core:core-ktx:1.13.1") //implementation("androidx.compose.ui:ui:${rootProject.extra["compose.version"] as String}") //implementation("androidx.compose.material:material:$compose_version") //implementation("androidx.compose.ui:ui-tooling-preview:$compose_version") - implementation("androidx.appcompat:appcompat:1.5.1") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.1") - implementation("androidx.lifecycle:lifecycle-process:2.4.1") - implementation("androidx.activity:activity-compose:1.5.0") - val work_version = "2.7.1" - implementation("androidx.work:work-runtime-ktx:$work_version") - implementation("androidx.work:work-multiprocess:$work_version") + implementation("androidx.appcompat:appcompat:1.7.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4") + implementation("androidx.lifecycle:lifecycle-process:2.8.4") + implementation("androidx.activity:activity-compose:1.9.1") + val workVersion = "2.9.1" + implementation("androidx.work:work-runtime-ktx:$workVersion") + implementation("androidx.work:work-multiprocess:$workVersion") - implementation("com.jakewharton:process-phoenix:2.1.2") + implementation("com.jakewharton:process-phoenix:3.0.0") + + //Camera Permission + implementation("com.google.accompanist:accompanist-permissions:0.34.0") //implementation("androidx.compose.material:material-icons-extended:$compose_version") //implementation("androidx.compose.ui:ui-util:$compose_version") testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.1.3") - androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") + androidTestImplementation("androidx.test.ext:junit:1.2.1") + androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") //androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version") - debugImplementation("androidx.compose.ui:ui-tooling:1.4.3") + debugImplementation("androidx.compose.ui:ui-tooling:1.6.4") } tasks { val compressApk by creating { doLast { - val isRelease = gradle.startParameter.taskNames.find { it.toLowerCase().contains("release") } != null + val isRelease = gradle.startParameter.taskNames.find { it.lowercase().contains("release") } != null val buildType: String = if (isRelease) "release" else "debug" val javaHome = System.getProperties()["java.home"] ?: org.gradle.internal.jvm.Jvm.current().javaHome val sdkDir = android.sdkDirectory.absolutePath - var keyAlias = "" - var keyPassword = "" - var storeFile = "" - var storePassword = "" + val keyAlias: String + val keyPassword: String + val storeFile: String + val storePassword: String if (project.properties["android.injected.signing.key.alias"] != null) { keyAlias = project.properties["android.injected.signing.key.alias"] as String keyPassword = project.properties["android.injected.signing.key.password"] as String @@ -182,16 +189,16 @@ tasks { } exec { workingDir("../../../scripts/android") - setEnvironment(mapOf("JAVA_HOME" to "$javaHome")) + environment = mapOf("JAVA_HOME" to "$javaHome") commandLine = listOf( "./compress-and-sign-apk.sh", "${rootProject.extra["compression.level"]}", "$outputDir", - "$sdkDir", - "$storeFile", - "$storePassword", - "$keyAlias", - "$keyPassword" + sdkDir, + storeFile, + storePassword, + keyAlias, + keyPassword ) } diff --git a/apps/multiplatform/android/src/main/AndroidManifest.xml b/apps/multiplatform/android/src/main/AndroidManifest.xml index d8350ee222..0470977bcd 100644 --- a/apps/multiplatform/android/src/main/AndroidManifest.xml +++ b/apps/multiplatform/android/src/main/AndroidManifest.xml @@ -21,6 +21,20 @@ <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" /> + <!-- Requirements that allows to specify foreground service types --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> + + <!-- Allows to query app name and icon that can open specific file type --> + <queries> + <intent> + <action android:name="android.intent.action.VIEW" /> + <data android:mimeType="*/*" /> + </intent> + </queries> + <application android:name="SimplexApp" android:allowBackup="false" @@ -63,8 +77,33 @@ <data android:scheme="http" /> <data android:scheme="https" /> <data android:host="simplex.chat" /> + <data android:host="smp4.simplex.im" /> + <data android:host="smp5.simplex.im" /> + <data android:host="smp6.simplex.im" /> + <data android:host="smp7.simplex.im" /> <!-- TODO remove --> + <data android:host="smp8.simplex.im" /> + <data android:host="smp9.simplex.im" /> + <data android:host="smp10.simplex.im" /> + <data android:host="smp11.simplex.im" /> + <data android:host="smp12.simplex.im" /> + <data android:host="smp14.simplex.im" /> + <data android:host="smp15.simplex.im" /> + <data android:host="smp16.simplex.im" /> + <data android:host="smp17.simplex.im" /> + <data android:host="smp18.simplex.im" /> + <data android:host="smp19.simplex.im" /> + <data android:host="smp1.simplexonflux.com" /> + <data android:host="smp2.simplexonflux.com" /> + <data android:host="smp3.simplexonflux.com" /> + <data android:host="smp4.simplexonflux.com" /> + <data android:host="smp5.simplexonflux.com" /> + <data android:host="smp6.simplexonflux.com" /> <data android:pathPrefix="/invitation" /> <data android:pathPrefix="/contact" /> + <data android:pathPrefix="/a" /> + <data android:pathPrefix="/c" /> + <data android:pathPrefix="/g" /> + <data android:pathPrefix="/i" /> </intent-filter> <!-- Receive files from other apps --> <intent-filter> @@ -103,11 +142,13 @@ </intent-filter> </activity-alias> - - <activity android:name=".views.call.IncomingCallActivity" + <activity android:name=".views.call.CallActivity" android:showOnLockScreen="true" android:exported="false" - android:launchMode="singleTask"/> + android:launchMode="singleInstance" + android:supportsPictureInPicture="true" + android:autoRemoveFromRecents="true" + android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"/> <provider android:name="androidx.core.content.FileProvider" @@ -130,9 +171,25 @@ android:name=".SimplexService" android:enabled="true" android:exported="false" - android:stopWithTask="false"></service> + android:stopWithTask="false" + android:foregroundServiceType="remoteMessaging" + /> <!-- SimplexService restart on reboot --> + + <service + android:name=".CallService" + android:enabled="true" + android:exported="false" + android:stopWithTask="false" + android:foregroundServiceType="mediaPlayback|microphone|camera|remoteMessaging" + /> + + <receiver + android:name=".CallService$CallActionReceiver" + android:enabled="true" + android:exported="false" /> + <receiver android:name=".SimplexService$StartReceiver" android:enabled="true" @@ -147,5 +204,16 @@ android:name=".SimplexService$AutoRestartReceiver" android:enabled="true" android:exported="false" /> + + <!-- SimplexService restart on app update --> + <receiver + android:name=".SimplexService$AppUpdateReceiver" + android:enabled="true" + android:exported="false"> + <intent-filter> + <action android:name="android.intent.action.MY_PACKAGE_REPLACED" /> + </intent-filter> + </receiver> + </application> </manifest> diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/CallService.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/CallService.kt new file mode 100644 index 0000000000..6c3d96bebc --- /dev/null +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/CallService.kt @@ -0,0 +1,207 @@ +package chat.simplex.app + +import android.app.* +import android.content.* +import android.content.pm.PackageManager +import android.content.pm.ServiceInfo +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.os.* +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.core.app.NotificationCompat +import androidx.core.app.ServiceCompat +import androidx.core.content.ContextCompat +import chat.simplex.app.model.NtfManager.EndCallAction +import chat.simplex.app.views.call.CallActivity +import chat.simplex.common.model.NotificationPreviewMode +import chat.simplex.common.platform.* +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import kotlinx.datetime.Instant + +class CallService: Service() { + private var wakeLock: PowerManager.WakeLock? = null + private var notificationManager: NotificationManager? = null + private var serviceNotification: Notification? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Log.d(TAG, "onStartCommand startId: $startId") + if (intent != null) { + val action = intent.action + Log.d(TAG, "intent action $action") + when (action) { + Action.START.name -> startService() + else -> Log.e(TAG, "No action in the intent") + } + } else { + Log.d(TAG, "null intent. Probably restarted by the system.") + } + ServiceCompat.startForeground(this, CALL_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType()) + return START_STICKY + } + + override fun onCreate() { + super.onCreate() + Log.d(TAG, "Call service created") + notificationManager = createNotificationChannel() + ServiceCompat.startForeground(this, CALL_SERVICE_ID, updateNotification(), foregroundServiceType()) + } + + override fun onDestroy() { + Log.d(TAG, "Call service destroyed") + try { + wakeLock?.let { + while (it.isHeld) it.release() // release all, in case acquired more than once + } + wakeLock = null + } catch (e: Exception) { + Log.d(TAG, "Exception while releasing wakelock: ${e.message}") + } + super.onDestroy() + } + + private fun startService() { + Log.d(TAG, "CallService startService") + if (wakeLock != null) return + wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run { + newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG).apply { + acquire() + } + } + } + + private fun createNotificationIfNeeded(): Notification { + val ntf = serviceNotification + if (ntf != null) return ntf + + return updateNotification() + } + + fun updateNotification(): Notification { + val call = chatModel.activeCall.value + val previewMode = appPreferences.notificationPreviewMode.get() + val title = if (previewMode == NotificationPreviewMode.HIDDEN.name) + generalGetString(MR.strings.notification_preview_somebody) + else + call?.contact?.profile?.displayName ?: "" + val text = generalGetString(if (call?.hasVideo == true) MR.strings.call_service_notification_video_call else MR.strings.call_service_notification_audio_call) + val image = call?.contact?.image + val largeIcon = if (image == null || previewMode == NotificationPreviewMode.HIDDEN.name) + BitmapFactory.decodeResource(resources, R.drawable.icon) + else + base64ToBitmap(image).asAndroidBitmap() + + val ntf = createNotification(title, text, largeIcon, call?.connectedAt) + serviceNotification = ntf + ServiceCompat.startForeground(this, CALL_SERVICE_ID, ntf, foregroundServiceType()) + return ntf + } + + private fun foregroundServiceType(): Int { + val call = chatModel.activeCall.value + return if (call == null) { + if (Build.VERSION.SDK_INT >= 34) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING + } else { + 0 + } + } else if (Build.VERSION.SDK_INT >= 30) { + if (call.hasVideo && ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA + } else { + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE + } + } else if (Build.VERSION.SDK_INT >= 29) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK + } else { + 0 + } + } + + private fun createNotificationChannel(): NotificationManager? { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val channel = NotificationChannel(CALL_NOTIFICATION_CHANNEL_ID, CALL_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT) + notificationManager.createNotificationChannel(channel) + return notificationManager + } + return null + } + + private fun createNotification(title: String, text: String, icon: Bitmap, connectedAt: Instant? = null): Notification { + val pendingIntent: PendingIntent = Intent(this, CallActivity::class.java).let { notificationIntent -> + PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE) + } + + val endCallPendingIntent: PendingIntent = Intent(this, CallActionReceiver::class.java).let { notificationIntent -> + notificationIntent.setAction(EndCallAction) + PendingIntent.getBroadcast(this, 1, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE) + } + + val builder = NotificationCompat.Builder(this, CALL_NOTIFICATION_CHANNEL_ID) + .setSmallIcon(R.drawable.ntf_icon) + .setLargeIcon(icon.clipToCircle()) + .setColor(0x88FFFF) + .setContentTitle(title) + .setContentText(text) + .setContentIntent(pendingIntent) + .setSilent(true) + .addAction(R.drawable.ntf_icon, generalGetString(MR.strings.call_service_notification_end_call), endCallPendingIntent) + if (connectedAt != null) { + builder.setUsesChronometer(true) + builder.setWhen(connectedAt.epochSeconds * 1000) + } + + return builder.build() + } + + override fun onBind(intent: Intent): IBinder { + return CallServiceBinder() + } + + inner class CallServiceBinder : Binder() { + fun getService() = this@CallService + } + + enum class Action { + START, + } + + class CallActionReceiver: BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when (intent?.action) { + EndCallAction -> { + val call = chatModel.activeCall.value + if (call != null) { + withBGApi { + chatModel.callManager.endCall(call) + } + } + } + else -> { + Log.e(TAG, "Unknown action. Make sure you provided an action") + } + } + } + } + +companion object { + const val TAG = "CALL_SERVICE" + const val CALL_NOTIFICATION_CHANNEL_ID = "chat.simplex.app.CALL_SERVICE_NOTIFICATION" + const val CALL_NOTIFICATION_CHANNEL_NAME = "SimpleX Chat call service" + const val CALL_SERVICE_ID = 6788 + const val WAKE_LOCK_TAG = "CallService::lock" + + fun startService(): Intent { + Log.d(TAG, "CallService start") + return Intent(androidAppContext, CallService::class.java).also { + it.action = Action.START.name + ContextCompat.startForegroundService(androidAppContext, it) + } + } + + fun stopService() { + androidAppContext.stopService(Intent(androidAppContext, CallService::class.java)) + } + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt index 8d64ae3c80..bacdfe70af 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt @@ -1,11 +1,14 @@ package chat.simplex.app +import android.content.Context import android.content.Intent import android.net.Uri import android.os.* +import android.view.View import android.view.WindowManager import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatDelegate +import androidx.activity.enableEdgeToEdge +import androidx.compose.ui.platform.ClipboardManager import androidx.fragment.app.FragmentActivity import chat.simplex.app.model.NtfManager import chat.simplex.app.model.NtfManager.getUserIdFromIntent @@ -17,17 +20,28 @@ import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.onboarding.* import chat.simplex.common.platform.* +import chat.simplex.res.MR import kotlinx.coroutines.* import java.lang.ref.WeakReference class MainActivity: FragmentActivity() { + companion object { + const val OLD_ANDROID_UI_FLAGS = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + } override fun onCreate(savedInstanceState: Bundle?) { + mainActivity = WeakReference(this) platform.androidSetNightModeIfSupported() + val c = CurrentColors.value.colors + platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight) applyAppLocale(ChatModel.controller.appPrefs.appLanguage) + // This flag makes status bar and navigation bar fully transparent. But on API level < 30 it breaks insets entirely + // https://issuetracker.google.com/issues/236862874 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) + } super.onCreate(savedInstanceState) // testJson() - mainActivity = WeakReference(this) // When call ended and orientation changes, it re-process old intent, it's unneeded. // Only needed to be processed on first creation of activity if (savedInstanceState == null) { @@ -42,6 +56,7 @@ class MainActivity: FragmentActivity() { WindowManager.LayoutParams.FLAG_SECURE ) } + enableEdgeToEdge() setContent { AppScreen() } @@ -49,7 +64,7 @@ class MainActivity: FragmentActivity() { SimplexApp.context.schedulePeriodicWakeUp() } - override fun onNewIntent(intent: Intent?) { + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) processIntent(intent) processExternalIntent(intent) @@ -58,6 +73,17 @@ class MainActivity: FragmentActivity() { override fun onResume() { super.onResume() AppLock.recheckAuthState() + withApi { + delay(1000) + if (!isAppOnForeground) return@withApi + /** + * When the app calls [ClipboardManager.shareText] and a user copies text in clipboard, Android denies + * access to clipboard because the app considered in background. + * This will ensure that the app will get the event on resume + * */ + val service = getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager + chatModel.clipboardHasText.value = service.hasPrimaryClip() + } } override fun onPause() { @@ -77,11 +103,12 @@ class MainActivity: FragmentActivity() { } override fun onBackPressed() { - if ( - onBackPressedDispatcher.hasEnabledCallbacks() // Has something to do in a backstack - || Build.VERSION.SDK_INT >= Build.VERSION_CODES.R // Android 11 or above - || isTaskRoot // there are still other tasks after we reach the main (home) activity - ) { + val canFinishActivity = ( + onBackPressedDispatcher.hasEnabledCallbacks() // Has something to do in a backstack + || Build.VERSION.SDK_INT >= Build.VERSION_CODES.R // Android 11 or above + || isTaskRoot // there are still other tasks after we reach the main (home) activity + ) && SimplexApp.context.chatModel.sharedContent.value !is SharedContent.Forward + if (canFinishActivity) { // https://medium.com/mobile-app-development-publication/the-risk-of-android-strandhogg-security-issue-and-how-it-can-be-mitigated-80d2ddb4af06 super.onBackPressed() } @@ -92,9 +119,15 @@ class MainActivity: FragmentActivity() { AppLock.laFailed.value = true } if (!onBackPressedDispatcher.hasEnabledCallbacks()) { + val sharedContent = chatModel.sharedContent.value // Drop shared content - SimplexApp.context.chatModel.sharedContent.value = null - finish() + chatModel.sharedContent.value = null + if (sharedContent is SharedContent.Forward) { + chatModel.chatId.value = sharedContent.fromChatInfo.id + } + if (canFinishActivity) { + finish() + } } } } @@ -127,7 +160,9 @@ fun processIntent(intent: Intent?) { "android.intent.action.VIEW" -> { val uri = intent.data if (uri != null) { - chatModel.appOpenUrl.value = null to uri.toURI() + chatModel.appOpenUrl.value = null to uri.toString() + } else { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_parsing_uri_title), generalGetString(MR.strings.error_parsing_uri_desc)) } } } @@ -194,5 +229,5 @@ fun isMediaIntent(intent: Intent): Boolean = // val str: String = """ // """.trimIndent() // -// println(json.decodeFromString<APIResponse>(str)) +// println(json.decodeFromString<APIResult>(str)) //} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt index 3b8902b58c..9264ca6abf 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt @@ -3,11 +3,11 @@ package chat.simplex.app import android.content.Context import android.util.Log import androidx.work.* -import chat.simplex.app.* import chat.simplex.app.SimplexService.Companion.showPassphraseNotification import chat.simplex.common.model.ChatController +import chat.simplex.common.platform.* import chat.simplex.common.views.helpers.DBMigrationResult -import chat.simplex.app.BuildConfig +import chat.simplex.common.views.helpers.DatabaseUtils import kotlinx.coroutines.* import java.util.Date import java.util.concurrent.TimeUnit @@ -29,12 +29,14 @@ object MessagesFetcherWorker { .setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) .build() - WorkManager.getInstance(SimplexApp.context).enqueueUniqueWork(UNIQUE_WORK_TAG, ExistingWorkPolicy.REPLACE, periodicWorkRequest) + SimplexApp.context.getWorkManagerInstance().enqueueUniqueWork(UNIQUE_WORK_TAG, ExistingWorkPolicy.REPLACE, periodicWorkRequest) } - fun cancelAll() { - Log.d(TAG, "Worker: canceled all tasks") - WorkManager.getInstance(SimplexApp.context).cancelUniqueWork(UNIQUE_WORK_TAG) + fun cancelAll(withLog: Boolean = true) { + if (withLog) { + Log.d(TAG, "Worker: canceled all tasks") + } + SimplexApp.context.getWorkManagerInstance().cancelUniqueWork(UNIQUE_WORK_TAG) } } @@ -57,6 +59,10 @@ class MessagesFetcherWork( val durationSeconds = inputData.getInt(INPUT_DATA_DURATION, 60) var shouldReschedule = true try { + // In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here + if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) { + initChatControllerOnStart() + } withTimeout(durationSeconds * 1000L) { val chatController = ChatController SimplexService.waitDbMigrationEnds(chatController) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index ee43da5d44..5545595dc6 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -1,25 +1,37 @@ package chat.simplex.app -import android.app.Application -import android.app.UiModeManager +import android.annotation.SuppressLint +import android.app.* +import android.content.Context +import chat.simplex.common.platform.Log +import android.content.Intent +import android.content.pm.ActivityInfo import android.os.* +import android.view.View +import androidx.compose.animation.core.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.core.view.ViewCompat import androidx.lifecycle.* import androidx.work.* +import chat.simplex.app.MainActivity.Companion.OLD_ANDROID_UI_FLAGS import chat.simplex.app.model.NtfManager -import chat.simplex.common.helpers.APPLICATION_ID -import chat.simplex.common.helpers.requiresIgnoringBattery +import chat.simplex.app.model.NtfManager.AcceptCallAction +import chat.simplex.app.views.call.CallActivity +import chat.simplex.common.helpers.* import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs -import chat.simplex.common.model.ChatModel.updatingChatsMutex import chat.simplex.common.platform.* -import chat.simplex.common.ui.theme.CurrentColors -import chat.simplex.common.ui.theme.DefaultTheme -import chat.simplex.common.views.call.RcvCallInvitation +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.call.* +import chat.simplex.common.views.database.deleteOldChatArchive import chat.simplex.common.views.helpers.* import chat.simplex.common.views.onboarding.OnboardingStage import com.jakewharton.processphoenix.ProcessPhoenix import kotlinx.coroutines.* -import kotlinx.coroutines.sync.withLock import java.io.* import java.util.* import java.util.concurrent.TimeUnit @@ -43,8 +55,8 @@ class SimplexApp: Application(), LifecycleEventObserver { try { Looper.loop() } catch (e: Throwable) { - if (e.message != null && e.message!!.startsWith("Unable to start activity")) { - android.os.Process.killProcess(android.os.Process.myPid()) + if (e is UnsatisfiedLinkError || e.message?.startsWith("Unable to start activity") == true) { + Process.killProcess(Process.myPid()) break } else { // Send it to our exception handled because it will not get the exception otherwise @@ -55,26 +67,32 @@ class SimplexApp: Application(), LifecycleEventObserver { } } context = this - initHaskell() + initHaskell(packageName) initMultiplatform() + reconfigureBroadcastReceivers() + runMigrations() tmpDir.deleteRecursively() tmpDir.mkdir() + deleteOldChatArchive() - withBGApi { - initChatController() - runMigrations() + // Present screen for continue migration if it wasn't finished yet + if (chatModel.migrationState.value != null) { + // It's important, otherwise, user may be locked in undefined state + appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + } else if (DatabaseUtils.ksAppPassword.get() == null || DatabaseUtils.ksSelfDestructPassword.get() == null) { + initChatControllerOnStart() } ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp) } override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { Log.d(TAG, "onStateChanged: $event") - withApi { + withLongRunningApi { when (event) { Lifecycle.Event.ON_START -> { isAppOnForeground = true if (chatModel.chatRunning.value == true) { - updatingChatsMutex.withLock { + withContext(Dispatchers.Main) { kotlin.runCatching { val currentUserId = chatModel.currentUser.value?.userId val chats = ArrayList(chatController.apiGetChats(chatModel.remoteHostId())) @@ -87,7 +105,7 @@ class SimplexApp: Application(), LifecycleEventObserver { /** Pass old chatStats because unreadCounter can be changed already while [ChatController.apiGetChats] is executing */ if (indexOfCurrentChat >= 0) chats[indexOfCurrentChat] = chats[indexOfCurrentChat].copy(chatStats = oldStats) } - chatModel.updateChats(chats) + chatModel.chatsContext.updateChats(chats) } }.onFailure { Log.e(TAG, it.stackTraceToString()) } } @@ -105,7 +123,10 @@ class SimplexApp: Application(), LifecycleEventObserver { * */ if (chatModel.chatRunning.value != false && chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete && - appPrefs.notificationsMode.get() == NotificationsMode.SERVICE + appPrefs.notificationsMode.get() == NotificationsMode.SERVICE && + // New installation passes all checks above and tries to start the service which is not needed at all + // because preferred notification type is not yet chosen. So, check that the user has initialized db already + appPrefs.newDatabaseInitialized.get() ) { SimplexService.start() } @@ -130,6 +151,7 @@ class SimplexApp: Application(), LifecycleEventObserver { * */ fun schedulePeriodicServiceRestartWorker() = CoroutineScope(Dispatchers.Default).launch { if (!allowToStartServiceAfterAppExit()) { + getWorkManagerInstance().cancelUniqueWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC) return@launch } val workerVersion = chatController.appPrefs.autoRestartWorkerVersion.get() @@ -146,11 +168,12 @@ class SimplexApp: Application(), LifecycleEventObserver { .addTag(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC) .build() Log.d(TAG, "ServiceStartWorker: Scheduling period work every ${SimplexService.SERVICE_START_WORKER_INTERVAL_MINUTES} minutes") - WorkManager.getInstance(context)?.enqueueUniquePeriodicWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC, workPolicy, work) + getWorkManagerInstance().enqueueUniquePeriodicWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC, workPolicy, work) } fun schedulePeriodicWakeUp() = CoroutineScope(Dispatchers.Default).launch { if (!allowToStartPeriodically()) { + MessagesFetcherWorker.cancelAll(withLog = false) return@launch } MessagesFetcherWorker.scheduleWork() @@ -164,13 +187,15 @@ class SimplexApp: Application(), LifecycleEventObserver { androidAppContext = this APPLICATION_ID = BuildConfig.APPLICATION_ID ntfManager = object : chat.simplex.common.platform.NtfManager() { - override fun notifyCallInvitation(invitation: RcvCallInvitation) = NtfManager.notifyCallInvitation(invitation) + override fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean = NtfManager.notifyCallInvitation(invitation) override fun hasNotificationsForChat(chatId: String): Boolean = NtfManager.hasNotificationsForChat(chatId) override fun cancelNotificationsForChat(chatId: String) = NtfManager.cancelNotificationsForChat(chatId) + override fun cancelNotificationsForUser(userId: Long) = NtfManager.cancelNotificationsForUser(userId) override fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String?, actions: List<Pair<NotificationAction, () -> Unit>>) = NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions.map { it.first }) override fun androidCreateNtfChannelsMaybeShowAlert() = NtfManager.createNtfChannelsMaybeShowAlert() override fun cancelCallNotification() = NtfManager.cancelCallNotification() override fun cancelAllNotifications() = NtfManager.cancelAllNotifications() + override fun showMessage(title: String, text: String) = NtfManager.showMessage(title, text) } platform = object : PlatformInterface { override suspend fun androidServiceStart() { @@ -181,18 +206,33 @@ class SimplexApp: Application(), LifecycleEventObserver { SimplexService.safeStopService() } + override fun androidCallServiceSafeStop() { + CallService.stopService() + } + override fun androidNotificationsModeChanged(mode: NotificationsMode) { if (mode.requiresIgnoringBattery && !SimplexService.isBackgroundAllowed()) { appPrefs.backgroundServiceNoticeShown.set(false) } SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE) + SimplexService.AppUpdateReceiver.toggleReceiver(mode == NotificationsMode.SERVICE) CoroutineScope(Dispatchers.Default).launch { - if (mode == NotificationsMode.SERVICE) + if (mode == NotificationsMode.SERVICE) { SimplexService.start() - else + // Sometimes, when we change modes fast from one to another, system destroys the service after start. + // We can wait a little and restart the service, and it will work in 100% of cases + delay(2000) + if (!SimplexService.isServiceStarted && appPrefs.notificationsMode.get() == NotificationsMode.SERVICE) { + Log.i(TAG, "Service tried to start but destroyed by system, repeating once more") + SimplexService.start() + } + } else { SimplexService.safeStopService() + } + } + if (mode != NotificationsMode.SERVICE) { + getWorkManagerInstance().cancelUniqueWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC) } - if (mode != NotificationsMode.PERIODIC) { MessagesFetcherWorker.cancelAll() } @@ -209,6 +249,7 @@ class SimplexApp: Application(), LifecycleEventObserver { } override fun androidChatStopped() { + getWorkManagerInstance().cancelUniqueWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC) SimplexService.safeStopService() MessagesFetcherWorker.cancelAll() } @@ -228,8 +269,7 @@ class SimplexApp: Application(), LifecycleEventObserver { override fun androidSetNightModeIfSupported() { if (Build.VERSION.SDK_INT < 31) return - - val light = if (CurrentColors.value.name == DefaultTheme.SYSTEM.name) { + val light = if (CurrentColors.value.name == DefaultTheme.SYSTEM_THEME_NAME) { null } else { CurrentColors.value.colors.isLight @@ -243,6 +283,78 @@ class SimplexApp: Application(), LifecycleEventObserver { uiModeManager.setApplicationNightMode(mode) } + override fun androidSetStatusAndNavigationBarAppearance(isLightStatusBar: Boolean, isLightNavBar: Boolean, blackNavBar: Boolean, themeBackgroundColor: Color) { + val window = mainActivity.get()?.window ?: return + @Suppress("DEPRECATION") + val statusLight = isLightStatusBar && chatModel.activeCall.value == null + val navBarLight = isLightNavBar || windowOrientation() == WindowOrientation.LANDSCAPE + val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView) + if (windowInsetController?.isAppearanceLightStatusBars != statusLight) { + windowInsetController?.isAppearanceLightStatusBars = statusLight + } + window.navigationBarColor = Color.Transparent.toArgb() + if (windowInsetController?.isAppearanceLightNavigationBars != navBarLight) { + windowInsetController?.isAppearanceLightNavigationBars = navBarLight + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + window.decorView.systemUiVisibility = if (statusLight && navBarLight) { + View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or OLD_ANDROID_UI_FLAGS + } else if (statusLight) { + View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or OLD_ANDROID_UI_FLAGS + } else if (navBarLight) { + View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or OLD_ANDROID_UI_FLAGS + } else { + OLD_ANDROID_UI_FLAGS + } + window.navigationBarColor = if (blackNavBar) Color.Black.toArgb() else themeBackgroundColor.toArgb() + } else { + window.navigationBarColor = Color.Transparent.toArgb() + } + } + + override fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long?, chatId: ChatId?) { + val context = mainActivity.get() ?: return + val intent = Intent(context, CallActivity::class.java) + .addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + if (acceptCall) { + intent.setAction(AcceptCallAction) + .putExtra("remoteHostId", remoteHostId) + .putExtra("chatId", chatId) + } + intent.flags += Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT + context.startActivity(intent) + } + + override fun androidPictureInPictureAllowed(): Boolean { + val appOps = androidAppContext.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager + return appOps.checkOpNoThrow(AppOpsManager.OPSTR_PICTURE_IN_PICTURE, Process.myUid(), packageName) == AppOpsManager.MODE_ALLOWED + } + + override fun androidCallEnded() { + activeCallDestroyWebView() + } + + override fun androidRestartNetworkObserver() { + NetworkObserver.shared.restartNetworkObserver() + } + + override fun androidIsXiaomiDevice(): Boolean = setOf("xiaomi", "redmi", "poco").contains(Build.BRAND.lowercase()) + + @SuppressLint("SourceLockedOrientationActivity") + @Composable + override fun androidLockPortraitOrientation() { + val context = LocalContext.current + DisposableEffect(Unit) { + val activity = context as? Activity ?: return@DisposableEffect onDispose {} + // Lock orientation to portrait in order to have good experience with calls + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + onDispose { + // Unlock orientation + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + } + } + } + override suspend fun androidAskToAllowBackgroundCalls(): Boolean { if (SimplexService.isBackgroundRestricted()) { val userChoice: CompletableDeferred<Boolean> = CompletableDeferred() @@ -253,6 +365,16 @@ class SimplexApp: Application(), LifecycleEventObserver { } return true } + + override fun androidCreateActiveCallState(): Closeable = ActiveCallState() + + override val androidApiLevel: Int get() = Build.VERSION.SDK_INT } } + + // Make sure that receivers enabled state is in actual state (same as in prefs) + private fun reconfigureBroadcastReceivers() { + val mode = appPrefs.notificationsMode.get() + SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE) + SimplexService.AppUpdateReceiver.toggleReceiver(mode == NotificationsMode.SERVICE)} } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt index 2cb6c12da0..ad86759ba9 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt @@ -4,29 +4,35 @@ import android.annotation.SuppressLint import android.app.* import android.content.* import android.content.pm.PackageManager +import android.content.pm.ServiceInfo import android.net.Uri import android.os.* +import android.os.SystemClock import android.provider.Settings import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import chat.simplex.common.platform.Log import androidx.core.app.NotificationCompat +import androidx.core.app.ServiceCompat import androidx.core.content.ContextCompat import androidx.work.* +import chat.simplex.app.model.NtfManager import chat.simplex.common.AppLock import chat.simplex.common.helpers.requiresIgnoringBattery import chat.simplex.common.model.ChatController import chat.simplex.common.model.NotificationsMode -import chat.simplex.common.platform.androidAppContext +import chat.simplex.common.platform.* import chat.simplex.common.views.helpers.* import kotlinx.coroutines.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import kotlin.system.exitProcess // based on: // https://robertohuertas.com/2019/06/29/android_foreground_services/ @@ -34,12 +40,13 @@ import dev.icerock.moko.resources.compose.stringResource class SimplexService: Service() { private var wakeLock: PowerManager.WakeLock? = null - private var isStartingService = false + private var isCheckingNewMessages = false private var notificationManager: NotificationManager? = null private var serviceNotification: Notification? = null override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.d(TAG, "onStartCommand startId: $startId") + isServiceStarting = false if (intent != null) { val action = intent.action Log.d(TAG, "intent action $action") @@ -50,18 +57,15 @@ class SimplexService: Service() { } else { Log.d(TAG, "null intent. Probably restarted by the system.") } - startForeground(SIMPLEX_SERVICE_ID, serviceNotification) + ServiceCompat.startForeground(this, SIMPLEX_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType()) return START_STICKY // to restart if killed } override fun onCreate() { super.onCreate() Log.d(TAG, "Simplex service created") - val title = generalGetString(MR.strings.simplex_service_notification_title) - val text = generalGetString(MR.strings.simplex_service_notification_text) - notificationManager = createNotificationChannel() - serviceNotification = createNotification(title, text) - startForeground(SIMPLEX_SERVICE_ID, serviceNotification) + createNotificationIfNeeded() + ServiceCompat.startForeground(this, SIMPLEX_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType()) /** * The reason [stopAfterStart] exists is because when the service is not called [startForeground] yet, and * we call [stopSelf] on the same service, [ForegroundServiceDidNotStartInTimeException] will be thrown. @@ -71,7 +75,12 @@ class SimplexService: Service() { stopForeground(true) stopSelf() } else { + isServiceStarting = false isServiceStarted = true + // In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here + if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) { + initChatControllerOnStart() + } } } @@ -85,6 +94,7 @@ class SimplexService: Service() { } catch (e: Exception) { Log.d(TAG, "Exception while releasing wakelock: ${e.message}") } + isServiceStarting = false isServiceStarted = false stopAfterStart = false saveServiceState(this, ServiceState.STOPPED) @@ -95,12 +105,32 @@ class SimplexService: Service() { super.onDestroy() } + private fun createNotificationIfNeeded(): Notification { + val ntf = serviceNotification + if (ntf != null) return ntf + + val title = generalGetString(MR.strings.simplex_service_notification_title) + val text = generalGetString(MR.strings.simplex_service_notification_text) + notificationManager = createNotificationChannel() + val newNtf = createNotification(title, text) + serviceNotification = newNtf + return newNtf + } + + private fun foregroundServiceType(): Int { + return if (Build.VERSION.SDK_INT >= 34) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING + } else { + 0 + } + } + private fun startService() { Log.d(TAG, "SimplexService startService") - if (wakeLock != null || isStartingService) return + if (wakeLock != null || isCheckingNewMessages) return val self = this - isStartingService = true - withApi { + isCheckingNewMessages = true + withLongRunningApi { val chatController = ChatController waitDbMigrationEnds(chatController) try { @@ -109,8 +139,9 @@ class SimplexService: Service() { if (chatDbStatus != DBMigrationResult.OK) { Log.w(chat.simplex.app.TAG, "SimplexService: problem with the database: $chatDbStatus") showPassphraseNotification(chatDbStatus) + androidAppContext.getWorkManagerInstance().cancelUniqueWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC) safeStopService() - return@withApi + return@withLongRunningApi } saveServiceState(self, ServiceState.STARTED) wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run { @@ -119,7 +150,7 @@ class SimplexService: Service() { } } } finally { - isStartingService = false + isCheckingNewMessages = false } } } @@ -173,6 +204,11 @@ class SimplexService: Service() { // Just to make sure that after restart of the app the user will need to re-authenticate AppLock.clearAuthState() + if (appPreferences.chatStopped.get()) { + stopSelf() + exitProcess(0) + } + // If notification service isn't enabled or battery optimization isn't disabled, we shouldn't restart the service if (!SimplexApp.context.allowToStartServiceAfterAppExit()) { return @@ -213,6 +249,29 @@ class SimplexService: Service() { } } + // restart on app update + class AppUpdateReceiver: BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + // If notification service is enabled and battery optimization is disabled, restart the service on app update + if (SimplexApp.context.allowToStartServiceAfterAppExit()) { + Log.d(TAG, "AppUpdateReceiver: onReceive called") + scheduleStart(context) + } + } + + companion object { + fun toggleReceiver(enable: Boolean) { + Log.d(TAG, "AppUpdateReceiver: toggleReceiver enabled: $enable") + val component = ComponentName(BuildConfig.APPLICATION_ID, AppUpdateReceiver::class.java.name) + SimplexApp.context.packageManager.setComponentEnabledSetting( + component, + if (enable) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP + ) + } + } + } + class ServiceStartWorker(private val context: Context, params: WorkerParameters): CoroutineWorker(context, params) { override suspend fun doWork(): Result { val id = this.id @@ -253,12 +312,13 @@ class SimplexService: Service() { private const val SHARED_PREFS_SERVICE_STATE = "SIMPLEX_SERVICE_STATE" private const val WORK_NAME_ONCE = "ServiceStartWorkerOnce" - private var isServiceStarted = false + var isServiceStarting = false + var isServiceStarted = false private var stopAfterStart = false fun scheduleStart(context: Context) { Log.d(TAG, "Enqueuing work to start subscriber service") - val workManager = WorkManager.getInstance(context) + val workManager = context.getWorkManagerInstance() val startServiceRequest = OneTimeWorkRequest.Builder(ServiceStartWorker::class.java).build() workManager.enqueueUniqueWork(WORK_NAME_ONCE, ExistingWorkPolicy.KEEP, startServiceRequest) // Unique avoids races! } @@ -272,16 +332,21 @@ class SimplexService: Service() { fun safeStopService() { if (isServiceStarted) { androidAppContext.stopService(Intent(androidAppContext, SimplexService::class.java)) - } else { + } else if (isServiceStarting) { stopAfterStart = true } } private suspend fun serviceAction(action: Action) { + if (!NtfManager.areNotificationsEnabledInSystem()) { + Log.d(TAG, "SimplexService serviceAction: ${action.name}. Notifications are not enabled in OS yet, not starting service") + return + } Log.d(TAG, "SimplexService serviceAction: ${action.name}") withContext(Dispatchers.IO) { Intent(androidAppContext, SimplexService::class.java).also { it.action = action.name + isServiceStarting = true ContextCompat.startForegroundService(androidAppContext, it) } } @@ -423,50 +488,70 @@ class SimplexService: Service() { }, confirmButton = { TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(MR.strings.ok)) } - } + }, + shape = RoundedCornerShape(corner = CornerSize(25.dp)) ) } - private fun showBGServiceNoticeIgnoreOptimization(mode: NotificationsMode, showOffAlert: Boolean) = AlertManager.shared.showAlert { - val ignoreOptimization = { - AlertManager.shared.hideAlert() - askAboutIgnoringBatteryOptimization() + private var showingIgnoreNotification = false + private fun showBGServiceNoticeIgnoreOptimization(mode: NotificationsMode, showOffAlert: Boolean) { + // that's workaround for situation when the app receives onPause/onResume events multiple times + // (for example, after showing system alert for enabling notifications) which triggers showing that alert multiple times + if (showingIgnoreNotification) { + return } - val disableNotifications = { - AlertManager.shared.hideAlert() - disableNotifications(mode, showOffAlert) - } - AlertDialog( - onDismissRequest = disableNotifications, - title = { - Row { - Icon( - painterResource(MR.images.ic_bolt), - contentDescription = - if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.icon_descr_instant_notifications) else stringResource(MR.strings.periodic_notifications), - ) - Text( - if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.service_notifications) else stringResource(MR.strings.periodic_notifications), - fontWeight = FontWeight.Bold - ) - } - }, - text = { - Column { - Text( - if (mode == NotificationsMode.SERVICE) annotatedStringResource(MR.strings.to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery) else annotatedStringResource(MR.strings.periodic_notifications_desc), - Modifier.padding(bottom = 8.dp) - ) - Text(annotatedStringResource(MR.strings.turn_off_battery_optimization)) - } - }, - dismissButton = { - TextButton(onClick = disableNotifications) { Text(stringResource(MR.strings.disable_notifications_button), color = MaterialTheme.colors.error) } - }, - confirmButton = { - TextButton(onClick = ignoreOptimization) { Text(stringResource(MR.strings.turn_off_battery_optimization_button)) } + showingIgnoreNotification = true + AlertManager.shared.showAlert { + val ignoreOptimization = { + AlertManager.shared.hideAlert() + showingIgnoreNotification = false + askAboutIgnoringBatteryOptimization() } - ) + val disableNotifications = { + AlertManager.shared.hideAlert() + showingIgnoreNotification = false + disableNotifications(mode, showOffAlert) + } + AlertDialog( + onDismissRequest = disableNotifications, + title = { + Row { + Icon( + painterResource(MR.images.ic_bolt), + contentDescription = + if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.icon_descr_instant_notifications) else stringResource(MR.strings.periodic_notifications), + ) + Text( + if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.service_notifications) else stringResource(MR.strings.periodic_notifications), + fontWeight = FontWeight.Bold + ) + } + }, + text = { + Column { + Text( + if (mode == NotificationsMode.SERVICE) annotatedStringResource(MR.strings.to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery) else annotatedStringResource(MR.strings.periodic_notifications_desc), + Modifier.padding(bottom = 8.dp) + ) + Text(annotatedStringResource(MR.strings.turn_off_battery_optimization)) + + if (platform.androidIsXiaomiDevice() && (mode == NotificationsMode.PERIODIC || mode == NotificationsMode.SERVICE)) { + Text( + annotatedStringResource(MR.strings.xiaomi_ignore_battery_optimization), + Modifier.padding(top = 8.dp) + ) + } + } + }, + dismissButton = { + TextButton(onClick = disableNotifications) { Text(stringResource(MR.strings.disable_notifications_button), color = MaterialTheme.colors.error) } + }, + confirmButton = { + TextButton(onClick = ignoreOptimization) { Text(stringResource(MR.strings.turn_off_battery_optimization_button)) } + }, + shape = RoundedCornerShape(corner = CornerSize(25.dp)) + ) + } } private fun showBGServiceNoticeSystemRestricted(mode: NotificationsMode, showOffAlert: Boolean) = AlertManager.shared.showAlert { @@ -507,7 +592,8 @@ class SimplexService: Service() { }, confirmButton = { TextButton(onClick = unrestrict) { Text(stringResource(MR.strings.turn_off_system_restriction_button)) } - } + }, + shape = RoundedCornerShape(corner = CornerSize(25.dp)) ) } @@ -534,7 +620,8 @@ class SimplexService: Service() { }, confirmButton = { TextButton(onClick = unrestrict) { Text(stringResource(MR.strings.turn_off_system_restriction_button)) } - } + }, + shape = RoundedCornerShape(corner = CornerSize(25.dp)) ) val scope = rememberCoroutineScope() DisposableEffect(Unit) { @@ -578,13 +665,14 @@ class SimplexService: Service() { }, confirmButton = { TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(MR.strings.ok)) } - } + }, + shape = RoundedCornerShape(corner = CornerSize(25.dp)) ) } fun isBackgroundAllowed(): Boolean = isIgnoringBatteryOptimizations() && !isBackgroundRestricted() - fun isIgnoringBatteryOptimizations(): Boolean { + private fun isIgnoringBatteryOptimizations(): Boolean { val powerManager = androidAppContext.getSystemService(Application.POWER_SERVICE) as PowerManager return powerManager.isIgnoringBatteryOptimizations(androidAppContext.packageName) } @@ -629,6 +717,8 @@ class SimplexService: Service() { } ChatController.appPrefs.notificationsMode.set(NotificationsMode.OFF) StartReceiver.toggleReceiver(false) + AppUpdateReceiver.toggleReceiver(false) + androidAppContext.getWorkManagerInstance().cancelUniqueWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC) MessagesFetcherWorker.cancelAll() safeStopService() } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt index 916f40df13..cf19589d4a 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt @@ -4,6 +4,7 @@ import android.app.* import android.app.TaskStackBuilder import android.content.* import android.content.pm.PackageManager +import android.graphics.Bitmap import android.graphics.BitmapFactory import android.hardware.display.DisplayManager import android.media.AudioAttributes @@ -13,7 +14,7 @@ import androidx.compose.ui.graphics.asAndroidBitmap import androidx.core.app.* import chat.simplex.app.* import chat.simplex.app.TAG -import chat.simplex.app.views.call.IncomingCallActivity +import chat.simplex.app.views.call.CallActivity import chat.simplex.app.views.call.getKeyguardManager import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* @@ -30,9 +31,10 @@ object NtfManager { const val ShowChatsAction: String = "chat.simplex.app.SHOW_CHATS" // DO NOT change notification channel settings / names - const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_1" + const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_2" const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL" const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL" + const val EndCallAction: String = "chat.simplex.app.END_CALL" const val CallNotificationId: Int = -1 private const val UserIdKey: String = "userId" private const val ChatIdKey: String = "chatId" @@ -46,11 +48,12 @@ object NtfManager { } private val manager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - private var prevNtfTime = mutableMapOf<String, Long>() + // (UserId, ChatId) -> Time + private var prevNtfTime = mutableMapOf<Pair<Long, ChatId>, Long>() private val msgNtfTimeoutMs = 30000L init { - if (manager.areNotificationsEnabled()) createNtfChannelsMaybeShowAlert() + if (areNotificationsEnabledInSystem()) createNtfChannelsMaybeShowAlert() } private fun callNotificationChannel(channelId: String, channelName: String): NotificationChannel { @@ -59,7 +62,7 @@ object NtfManager { .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) .build() - val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once) + val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/raw/ring_once") Log.d(TAG, "callNotificationChannel sound: $soundUri") callChannel.setSound(soundUri, attrs) callChannel.enableVibration(true) @@ -70,12 +73,27 @@ object NtfManager { } fun cancelNotificationsForChat(chatId: String) { - prevNtfTime.remove(chatId) + val key = prevNtfTime.keys.firstOrNull { it.second == chatId } + prevNtfTime.remove(key) manager.cancel(chatId.hashCode()) val msgNtfs = manager.activeNotifications.filter { ntf -> ntf.notification.channelId == MessageChannel } - if (msgNtfs.count() == 1) { + if (msgNtfs.size <= 1) { + // Have a group notification with no children so cancel it + manager.cancel(0) + } + } + + fun cancelNotificationsForUser(userId: Long) { + prevNtfTime.keys.filter { it.first == userId }.forEach { + prevNtfTime.remove(it) + manager.cancel(it.second.hashCode()) + } + val msgNtfs = manager.activeNotifications.filter { ntf -> + ntf.notification.channelId == MessageChannel + } + if (msgNtfs.size <= 1) { // Have a group notification with no children so cancel it manager.cancel(0) } @@ -85,8 +103,8 @@ object NtfManager { if (!user.showNotifications) return Log.d(TAG, "notifyMessageReceived $chatId") val now = Clock.System.now().toEpochMilliseconds() - val recentNotification = (now - prevNtfTime.getOrDefault(chatId, 0) < msgNtfTimeoutMs) - prevNtfTime[chatId] = now + val recentNotification = (now - prevNtfTime.getOrDefault(user.userId to chatId, 0) < msgNtfTimeoutMs) + prevNtfTime[user.userId to chatId] = now val previewMode = appPreferences.notificationPreviewMode.get() val title = if (previewMode == NotificationPreviewMode.HIDDEN.name) generalGetString(MR.strings.notification_preview_somebody) else displayName val content = if (previewMode != NotificationPreviewMode.MESSAGE.name) generalGetString(MR.strings.notification_preview_new_message) else msgText @@ -140,7 +158,7 @@ object NtfManager { } } - fun notifyCallInvitation(invitation: RcvCallInvitation) { + fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean { val keyguardManager = getKeyguardManager(context) Log.d( TAG, @@ -149,7 +167,7 @@ object NtfManager { "callOnLockScreen ${appPreferences.callOnLockScreen.get()}, " + "onForeground ${isAppOnForeground}" ) - if (isAppOnForeground) return + if (isAppOnForeground) return false val contactId = invitation.contact.id Log.d(TAG, "notifyCallInvitation $contactId") val image = invitation.contact.image @@ -157,13 +175,13 @@ object NtfManager { val screenOff = displayManager.displays.all { it.state != Display.STATE_ON } var ntfBuilder = if ((keyguardManager.isKeyguardLocked || screenOff) && appPreferences.callOnLockScreen.get() != CallOnLockScreen.DISABLE) { - val fullScreenIntent = Intent(context, IncomingCallActivity::class.java) + val fullScreenIntent = Intent(context, CallActivity::class.java) val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) NotificationCompat.Builder(context, CallChannel) .setFullScreenIntent(fullScreenPendingIntent, true) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) } else { - val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once) + val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/raw/ring_once") val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) NotificationCompat.Builder(context, CallChannel) .setContentIntent(chatPendingIntent(OpenChatAction, invitation.user.userId, invitation.contact.id)) @@ -206,6 +224,39 @@ object NtfManager { notify(CallNotificationId, notification) } } + return true + } + + fun showMessage(title: String, text: String) { + val builder = NotificationCompat.Builder(context, MessageChannel) + .setContentTitle(title) + .setContentText(text) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setGroup(MessageGroup) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) + .setSmallIcon(R.drawable.ntf_icon) + .setLargeIcon(null as Bitmap?) + .setColor(0x88FFFF) + .setAutoCancel(true) + .setVibrate(null) + .setContentIntent(chatPendingIntent(ShowChatsAction, null, null)) + .setSilent(false) + + val summary = NotificationCompat.Builder(context, MessageChannel) + .setSmallIcon(R.drawable.ntf_icon) + .setColor(0x88FFFF) + .setGroup(MessageGroup) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) + .setGroupSummary(true) + .setContentIntent(chatPendingIntent(ShowChatsAction, null)) + .build() + + with(NotificationManagerCompat.from(context)) { + if (ActivityCompat.checkSelfPermission(SimplexApp.context, android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { + notify("MESSAGE".hashCode(), builder.build()) + notify(0, summary) + } + } } fun cancelCallNotification() { @@ -236,6 +287,8 @@ object NtfManager { } } + fun areNotificationsEnabledInSystem() = manager.areNotificationsEnabled() + /** * This function creates notifications channels. On Android 13+ calling it for the first time will trigger system alert, * The alert asks a user to allow or disallow to show notifications for the app. That's why it should be called only when the user @@ -248,6 +301,7 @@ object NtfManager { manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(MR.strings.ntf_channel_calls))) // Remove old channels since they can't be edited manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION") + manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION_1") manager.deleteNotificationChannel("chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION") } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/CallActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/CallActivity.kt new file mode 100644 index 0000000000..995b584fce --- /dev/null +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/CallActivity.kt @@ -0,0 +1,464 @@ +package chat.simplex.app.views.call + +import android.Manifest +import android.app.* +import android.content.* +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.graphics.Rect +import android.os.* +import android.util.Rational +import android.view.* +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.trackPipAnimationHintView +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.* +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import chat.simplex.app.* +import chat.simplex.app.R +import chat.simplex.app.TAG +import chat.simplex.app.model.NtfManager +import chat.simplex.app.model.NtfManager.AcceptCallAction +import chat.simplex.common.helpers.applyAppLocale +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.platform.* +import chat.simplex.common.platform.chatModel +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.call.* +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import com.google.accompanist.permissions.rememberMultiplePermissionsState +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import java.lang.ref.WeakReference +import java.util.* +import chat.simplex.common.platform.chatModel as m + +class CallActivity: ComponentActivity(), ServiceConnection { + + var boundService: CallService? = null + + override fun onCreate(savedInstanceState: Bundle?) { + applyAppLocale(appPrefs.appLanguage) + super.onCreate(savedInstanceState) + callActivity = WeakReference(this) + when (intent?.action) { + AcceptCallAction -> { + val remoteHostId = intent.getLongExtra("remoteHostId", -1).takeIf { it != -1L } + val chatId = intent.getStringExtra("chatId") + val invitation = (m.callInvitations.values + m.activeCallInvitation.value).lastOrNull { + it?.remoteHostId == remoteHostId && it?.contact?.id == chatId + } + if (invitation != null) { + m.callManager.acceptIncomingCall(invitation = invitation) + } + } + } + + setContent { CallActivityView() } + + if (isOnLockScreenNow()) { + unlockForIncomingCall() + } + } + + override fun onDestroy() { + super.onDestroy() + (mainActivity.get() ?: this).applyAppLocale(appPrefs.appLanguage) + if (isOnLockScreenNow()) { + lockAfterIncomingCall() + } + try { + unbindService(this) + } catch (e: Exception) { + Log.i(TAG, "Unable to unbind service: " + e.stackTraceToString()) + } + } + + private fun isOnLockScreenNow() = getKeyguardManager(this).isKeyguardLocked + + fun setPipParams(video: Boolean, sourceRectHint: Rect? = null, viewRatio: Rational? = null) { + // By manually specifying source rect we exclude empty background while toggling PiP + val builder = PictureInPictureParams.Builder() + // that's limitation of Android. Otherwise, may crash on devices like Z Fold 3 + .setAspectRatio(viewRatio?.coerceIn(Rational(100, 239)..Rational(239, 100))) + .setSourceRectHint(sourceRectHint) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + builder.setAutoEnterEnabled(video) + } + setPictureInPictureParams(builder.build()) + } + + override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) + m.activeCallViewIsCollapsed.value = isInPictureInPictureMode + val layoutType = if (!isInPictureInPictureMode) { + LayoutType.Default + } else { + LayoutType.RemoteVideo + } + m.callCommand.add(WCallCommand.Layout(layoutType)) + } + + private fun hasGrantedPermissions(): Boolean { + val grantedAudio = ContextCompat.checkSelfPermission(this, android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED + val grantedCamera = !callHasVideo() || ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED + return grantedAudio && grantedCamera + } + + @Deprecated("Was deprecated in OS") + override fun onBackPressed() { + if (isOnLockScreenNow()) { + super.onBackPressed() + } else if (!hasGrantedPermissions() && !callHasVideo()) { + val call = m.activeCall.value + if (call != null) { + withBGApi { chatModel.callManager.endCall(call) } + } + } else { + m.activeCallViewIsCollapsed.value = true + } + } + + override fun onPictureInPictureRequested(): Boolean { + Log.d(TAG, "Requested picture-in-picture from the system") + return super.onPictureInPictureRequested() + } + + override fun onUserLeaveHint() { + super.onUserLeaveHint() + // On Android 12+ PiP is enabled automatically when a user hides the app + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callHasVideo() && platform.androidPictureInPictureAllowed()) { + enterPictureInPictureMode() + } + } + + override fun onResume() { + super.onResume() + m.activeCallViewIsCollapsed.value = false + } + + private fun unlockForIncomingCall() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + setShowWhenLocked(true) + setTurnScreenOn(true) + } else { + window.addFlags(activityFlags) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + getKeyguardManager(this).requestDismissKeyguard(this, null) + } + } + + private fun lockAfterIncomingCall() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + setShowWhenLocked(false) + setTurnScreenOn(false) + } else { + window.clearFlags(activityFlags) + } + } + + fun startServiceAndBind() { + /** + * On Android 12 there is a bug that prevents starting activity after pressing back button + * (the error says that it denies to start activity in background). + * Workaround is to bind to a service + * */ + bindService(CallService.startService(), this, 0) + } + + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + boundService = (service as CallService.CallServiceBinder).getService() + } + + override fun onServiceDisconnected(name: ComponentName?) { + boundService = null + } + + companion object { + const val activityFlags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + } +} + +fun getKeyguardManager(context: Context): KeyguardManager = + context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + +private fun callHasVideo() = m.activeCall.value?.hasVideo == true || m.activeCallInvitation.value?.callType?.media == CallMediaType.Video + +@Composable +fun CallActivityView() { + val switchingCall = m.switchingCall.value + val invitation = m.activeCallInvitation.value + val call = remember { m.activeCall }.value + val showCallView = m.showCallView.value + val activity = LocalContext.current as CallActivity + LaunchedEffect(Unit) { + snapshotFlow { m.activeCallViewIsCollapsed.value } + .collect { collapsed -> + when { + collapsed -> { + if (!platform.androidPictureInPictureAllowed() || !callHasVideo()) { + activity.moveTaskToBack(true) + activity.startActivity(Intent(activity, MainActivity::class.java)) + } else if (!activity.isInPictureInPictureMode && activity.lifecycle.currentState == Lifecycle.State.RESUMED) { + // User pressed back button, show MainActivity + activity.startActivity(Intent(activity, MainActivity::class.java)) + activity.enterPictureInPictureMode() + } + } + callHasVideo() && !platform.androidPictureInPictureAllowed() -> { + // PiP disabled by user + platform.androidStartCallActivity(false) + } + activity.isInPictureInPictureMode -> { + platform.androidStartCallActivity(false) + } + } + } + } + SimpleXTheme { + var prevCall by remember { mutableStateOf(call) } + KeyChangeEffect(m.activeCall.value, remember { appPrefs.appLanguage.state }.value) { + if (m.activeCall.value != null) { + prevCall = m.activeCall.value + activity.boundService?.updateNotification() + } + } + Box(Modifier.background(Color.Black)) { + if (call != null) { + val permissionsState = rememberMultiplePermissionsState( + permissions = if (callHasVideo()) { + listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) + } else { + listOf(Manifest.permission.RECORD_AUDIO) + } + ) + // callState == connected is needed in a situation when a peer enabled camera in audio call while a user didn't grant camera permission yet, + // so no need to hide active call view in this case + if (permissionsState.allPermissionsGranted || call.callState == CallState.Connected) { + ActiveCallView() + LaunchedEffect(Unit) { + activity.startServiceAndBind() + } + } + if ((!permissionsState.allPermissionsGranted && call.callState != CallState.Connected) || call.wantsToEnableCamera) { + CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callHasVideo() || call.wantsToEnableCamera) { + withBGApi { chatModel.callManager.endCall(call) } + } + val cameraAndMicPermissions = rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)) + DisposableEffect(cameraAndMicPermissions.allPermissionsGranted) { + onDispose { + if (call.wantsToEnableCamera && cameraAndMicPermissions.allPermissionsGranted) { + val activeCall = chatModel.activeCall.value + if (activeCall != null && activeCall.contact.apiId == call.contact.apiId) { + chatModel.activeCall.value = activeCall.copy(wantsToEnableCamera = false) + chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Camera, enable = true)) + } + } + } + } + } + val view = LocalView.current + if (callHasVideo()) { + val scope = rememberCoroutineScope() + LaunchedEffect(Unit) { + scope.launch { + activity.setPipParams(callHasVideo(), viewRatio = Rational(view.width, view.height)) + activity.trackPipAnimationHintView(view) + } + } + } + } else if (prevCall != null) { + prevCall?.let { ActiveCallOverlayDisabled(it) } + } + if (invitation != null) { + if (call == null) { + Surface( + Modifier + .fillMaxSize(), + color = MaterialTheme.colors.background, + contentColor = LocalContentColor.current + ) { + IncomingCallLockScreenAlert(invitation, m) + } + } else { + IncomingCallAlertView(invitation, m) + } + } + } + if (!m.activeCallViewIsCollapsed.value) { + AlertManager.shared.showInView() + } + } + LaunchedEffect(invitation, call, switchingCall, showCallView) { + if (!switchingCall && invitation == null && (!showCallView || call == null)) { + Log.d(TAG, "CallActivityView: finishing activity") + activity.finish() + } + } +} + +/** +* Related to lockscreen +* */ + +@Composable +fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatModel) { + val cm = chatModel.callManager + val callOnLockScreen by remember { mutableStateOf(chatModel.controller.appPrefs.callOnLockScreen.get()) } + val context = LocalContext.current + DisposableEffect(Unit) { + onDispose { + // Cancel notification whatever happens next since otherwise sound from notification and from inside the app can co-exist + ntfManager.cancelCallNotification() + } + } + IncomingCallLockScreenAlertLayout( + invitation, + callOnLockScreen, + chatModel, + rejectCall = { cm.endCall(invitation = invitation) }, + ignoreCall = { + chatModel.activeCallInvitation.value = null + ntfManager.cancelCallNotification() + }, + acceptCall = { cm.acceptIncomingCall(invitation = invitation) }, + openApp = { + val intent = Intent(context, MainActivity::class.java) + .setAction(NtfManager.OpenChatAction) + .putExtra("userId", invitation.user.userId) + .putExtra("chatId", invitation.contact.id) + context.startActivity(intent) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + getKeyguardManager(context).requestDismissKeyguard((context as Activity), null) + } + (context as Activity).finish() + } + ) +} + +@Composable +fun IncomingCallLockScreenAlertLayout( + invitation: RcvCallInvitation, + callOnLockScreen: CallOnLockScreen?, + chatModel: ChatModel, + rejectCall: () -> Unit, + ignoreCall: () -> Unit, + acceptCall: () -> Unit, + openApp: () -> Unit +) { + Column( + Modifier + .padding(30.dp) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + IncomingCallInfo(invitation, chatModel) + Spacer(Modifier.fillMaxHeight().weight(1f)) + if (callOnLockScreen == CallOnLockScreen.ACCEPT) { + ProfileImage(size = 192.dp, image = invitation.contact.profile.image) + Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2) + Spacer(Modifier.fillMaxHeight().weight(1f)) + Row { + LockScreenCallButton(stringResource(MR.strings.reject), painterResource(R.drawable.ic_call_end_filled), Color.Red, rejectCall) + Spacer(Modifier.size(48.dp)) + LockScreenCallButton(stringResource(MR.strings.ignore), painterResource(R.drawable.ic_close), MaterialTheme.colors.primary, ignoreCall) + Spacer(Modifier.size(48.dp)) + LockScreenCallButton(stringResource(MR.strings.accept), painterResource(R.drawable.ic_check_filled), SimplexGreen, acceptCall) + } + } else if (callOnLockScreen == CallOnLockScreen.SHOW) { + SimpleXLogo() + Text(stringResource(MR.strings.open_simplex_chat_to_accept_call), textAlign = TextAlign.Center, lineHeight = 22.sp) + Text(stringResource(MR.strings.allow_accepting_calls_from_lock_screen), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2, lineHeight = 22.sp) + Spacer(Modifier.fillMaxHeight().weight(1f)) + SimpleButton(text = stringResource(MR.strings.open_verb), icon = painterResource(R.drawable.ic_check_filled), click = openApp) + } + } +} + +@Composable +private fun SimpleXLogo() { + Image( + painter = painterResource(if (isInDarkTheme()) R.drawable.logo_light else R.drawable.logo), + contentDescription = stringResource(MR.strings.image_descr_simplex_logo), + modifier = Modifier + .padding(vertical = DEFAULT_PADDING) + .fillMaxWidth(0.80f) + ) +} + +@Composable +private fun LockScreenCallButton(text: String, icon: Painter, color: Color, action: () -> Unit) { + Surface( + shape = RoundedCornerShape(10.dp), + color = Color.Transparent, + contentColor = LocalContentColor.current + ) { + Column( + Modifier + .defaultMinSize(minWidth = 50.dp) + .padding(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + IconButton(action) { + Icon(icon, text, tint = color, modifier = Modifier.scale(1.75f)) + } + Spacer(Modifier.height(DEFAULT_PADDING)) + Text(text, style = MaterialTheme.typography.body2, color = MaterialTheme.colors.secondary) + } + } +} + +@Preview/*( + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true +)*/ +@Composable +fun PreviewIncomingCallLockScreenAlert() { + SimpleXTheme(true) { + Surface( + Modifier + .fillMaxSize(), + color = MaterialTheme.colors.background, + contentColor = LocalContentColor.current + ) { + IncomingCallLockScreenAlertLayout( + invitation = RcvCallInvitation( + callUUID = "", + remoteHostId = null, + user = User.sampleData, + contact = Contact.sampleData, + callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)), + sharedKey = null, + callTs = Clock.System.now() + ), + callOnLockScreen = null, + chatModel = SimplexApp.context.chatModel, + rejectCall = {}, + ignoreCall = {}, + acceptCall = {}, + openApp = {}, + ) + } + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt deleted file mode 100644 index f5c46a0eb5..0000000000 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt +++ /dev/null @@ -1,250 +0,0 @@ -package chat.simplex.app.views.call - -import android.app.Activity -import android.app.KeyguardManager -import android.content.Context -import android.content.Intent -import android.os.Build -import android.os.Bundle -import chat.simplex.common.platform.Log -import android.view.WindowManager -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -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.scale -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import chat.simplex.app.* -import chat.simplex.app.R -import chat.simplex.common.model.* -import chat.simplex.app.model.NtfManager.OpenChatAction -import chat.simplex.common.platform.ntfManager -import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.call.* -import chat.simplex.common.views.helpers.* -import chat.simplex.res.MR -import dev.icerock.moko.resources.compose.stringResource -import kotlinx.datetime.Clock - -class IncomingCallActivity: ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { IncomingCallActivityView(ChatModel) } - unlockForIncomingCall() - } - - override fun onDestroy() { - super.onDestroy() - lockAfterIncomingCall() - } - - private fun unlockForIncomingCall() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - setShowWhenLocked(true) - setTurnScreenOn(true) - } else { - window.addFlags(activityFlags) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - getKeyguardManager(this).requestDismissKeyguard(this, null) - } - } - - private fun lockAfterIncomingCall() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - setShowWhenLocked(false) - setTurnScreenOn(false) - } else { - window.clearFlags(activityFlags) - } - } - - companion object { - const val activityFlags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON - } -} - -fun getKeyguardManager(context: Context): KeyguardManager = - context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager - -@Composable -fun IncomingCallActivityView(m: ChatModel) { - val switchingCall = m.switchingCall.value - val invitation = m.activeCallInvitation.value - val call = m.activeCall.value - val showCallView = m.showCallView.value - val activity = LocalContext.current as Activity - LaunchedEffect(invitation, call, switchingCall, showCallView) { - if (!switchingCall && invitation == null && (!showCallView || call == null)) { - Log.d(TAG, "IncomingCallActivityView: finishing activity") - activity.finish() - } - } - SimpleXTheme { - Surface( - Modifier - .fillMaxSize(), - color = MaterialTheme.colors.background - ) { - if (showCallView) { - Box { - ActiveCallView() - if (invitation != null) IncomingCallAlertView(invitation, m) - } - } else if (invitation != null) { - IncomingCallLockScreenAlert(invitation, m) - } - } - } -} - -@Composable -fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatModel) { - val cm = chatModel.callManager - val callOnLockScreen by remember { mutableStateOf(chatModel.controller.appPrefs.callOnLockScreen.get()) } - val context = LocalContext.current - DisposableEffect(Unit) { - onDispose { - // Cancel notification whatever happens next since otherwise sound from notification and from inside the app can co-exist - ntfManager.cancelCallNotification() - } - } - IncomingCallLockScreenAlertLayout( - invitation, - callOnLockScreen, - chatModel, - rejectCall = { cm.endCall(invitation = invitation) }, - ignoreCall = { - chatModel.activeCallInvitation.value = null - ntfManager.cancelCallNotification() - }, - acceptCall = { cm.acceptIncomingCall(invitation = invitation) }, - openApp = { - val intent = Intent(context, MainActivity::class.java) - .setAction(OpenChatAction) - .putExtra("userId", invitation.user.userId) - .putExtra("chatId", invitation.contact.id) - context.startActivity(intent) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - getKeyguardManager(context).requestDismissKeyguard((context as Activity), null) - } - (context as Activity).finish() - } - ) -} - -@Composable -fun IncomingCallLockScreenAlertLayout( - invitation: RcvCallInvitation, - callOnLockScreen: CallOnLockScreen?, - chatModel: ChatModel, - rejectCall: () -> Unit, - ignoreCall: () -> Unit, - acceptCall: () -> Unit, - openApp: () -> Unit -) { - Column( - Modifier - .padding(30.dp) - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - IncomingCallInfo(invitation, chatModel) - Spacer(Modifier.fillMaxHeight().weight(1f)) - if (callOnLockScreen == CallOnLockScreen.ACCEPT) { - ProfileImage(size = 192.dp, image = invitation.contact.profile.image) - Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2) - Spacer(Modifier.fillMaxHeight().weight(1f)) - Row { - LockScreenCallButton(stringResource(MR.strings.reject), painterResource(R.drawable.ic_call_end_filled), Color.Red, rejectCall) - Spacer(Modifier.size(48.dp)) - LockScreenCallButton(stringResource(MR.strings.ignore), painterResource(R.drawable.ic_close), MaterialTheme.colors.primary, ignoreCall) - Spacer(Modifier.size(48.dp)) - LockScreenCallButton(stringResource(MR.strings.accept), painterResource(R.drawable.ic_check_filled), SimplexGreen, acceptCall) - } - } else if (callOnLockScreen == CallOnLockScreen.SHOW) { - SimpleXLogo() - Text(stringResource(MR.strings.open_simplex_chat_to_accept_call), textAlign = TextAlign.Center, lineHeight = 22.sp) - Text(stringResource(MR.strings.allow_accepting_calls_from_lock_screen), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2, lineHeight = 22.sp) - Spacer(Modifier.fillMaxHeight().weight(1f)) - SimpleButton(text = stringResource(MR.strings.open_verb), icon = painterResource(R.drawable.ic_check_filled), click = openApp) - } - } -} - -@Composable -private fun SimpleXLogo() { - Image( - painter = painterResource(if (isInDarkTheme()) R.drawable.logo_light else R.drawable.logo), - contentDescription = stringResource(MR.strings.image_descr_simplex_logo), - modifier = Modifier - .padding(vertical = DEFAULT_PADDING) - .fillMaxWidth(0.80f) - ) -} - -@Composable -private fun LockScreenCallButton(text: String, icon: Painter, color: Color, action: () -> Unit) { - Surface( - shape = RoundedCornerShape(10.dp), - color = Color.Transparent - ) { - Column( - Modifier - .defaultMinSize(minWidth = 50.dp) - .padding(4.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - IconButton(action) { - Icon(icon, text, tint = color, modifier = Modifier.scale(1.75f)) - } - Spacer(Modifier.height(DEFAULT_PADDING)) - Text(text, style = MaterialTheme.typography.body2, color = MaterialTheme.colors.secondary) - } - } -} - -@Preview/*( - uiMode = Configuration.UI_MODE_NIGHT_YES, - showBackground = true -)*/ -@Composable -fun PreviewIncomingCallLockScreenAlert() { - SimpleXTheme(true) { - Surface( - Modifier - .fillMaxSize(), - color = MaterialTheme.colors.background - ) { - IncomingCallLockScreenAlertLayout( - invitation = RcvCallInvitation( - remoteHostId = null, - user = User.sampleData, - contact = Contact.sampleData, - callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)), - sharedKey = null, - callTs = Clock.System.now() - ), - callOnLockScreen = null, - chatModel = SimplexApp.context.chatModel, - rejectCall = {}, - ignoreCall = {}, - acceptCall = {}, - openApp = {}, - ) - } - } -} diff --git a/apps/multiplatform/android/src/main/res/values/colors.xml b/apps/multiplatform/android/src/main/res/values/colors.xml index e1a994e57f..1833a6d9a3 100644 --- a/apps/multiplatform/android/src/main/res/values/colors.xml +++ b/apps/multiplatform/android/src/main/res/values/colors.xml @@ -2,6 +2,5 @@ <resources> <color name="black">#FF000000</color> <color name="white">#FFFFFFFF</color> - <color name="highOrLowLight">#8b8786</color> <color name="window_background_dark">#121212</color> </resources> \ No newline at end of file diff --git a/apps/multiplatform/build.gradle.kts b/apps/multiplatform/build.gradle.kts index f404207529..a0cd35b3ca 100644 --- a/apps/multiplatform/build.gradle.kts +++ b/apps/multiplatform/build.gradle.kts @@ -47,6 +47,9 @@ buildscript { classpath(kotlin("gradle-plugin", version = rootProject.extra["kotlin.version"] as String)) classpath("dev.icerock.moko:resources-generator:0.23.0") + // Workaround gradle sync issue: https://github.com/gmazzo/gradle-buildconfig-plugin/issues/137#issuecomment-1935739759 + classpath("com.squareup:kotlinpoet:1.16.0") + // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } @@ -80,6 +83,30 @@ plugins { id("org.jetbrains.kotlin.plugin.serialization") apply false } +// https://raymondctc.medium.com/configuring-your-sourcecompatibility-targetcompatibility-and-kotlinoptions-jvmtarget-all-at-once-66bf2198145f +val jvmVersion: Provider<String> = providers.gradleProperty("kotlin.jvm.target") + +configure(subprojects) { + // Apply compileOptions to subprojects + plugins.withType<com.android.build.gradle.BasePlugin>().configureEach { + extensions.findByType<com.android.build.gradle.BaseExtension>()?.apply { + jvmVersion.map { JavaVersion.toVersion(it) }.orNull?.let { + compileOptions { + sourceCompatibility = it + targetCompatibility = it + } + } + } + } + + // Apply kotlinOptions.jvmTarget to subprojects + tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { + kotlinOptions { + if (jvmVersion.isPresent) jvmTarget = jvmVersion.get() + } + } +} + tasks.register("clean", Delete::class) { delete(rootProject.buildDir) } diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index 4d2eeca2ee..e2927e4aaf 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -4,17 +4,16 @@ plugins { id("com.android.library") id("org.jetbrains.kotlin.plugin.serialization") id("dev.icerock.mobile.multiplatform-resources") - id("com.github.gmazzo.buildconfig") version "4.0.4" + id("com.github.gmazzo.buildconfig") version "5.3.5" } group = "chat.simplex" version = extra["android.version_name"] as String kotlin { - android() - jvm("desktop") { - jvmToolchain(11) - } + androidTarget() + jvm("desktop") + applyDefaultHierarchyTemplate() sourceSets { all { languageSettings { @@ -35,71 +34,89 @@ kotlin { api(compose.runtime) api(compose.foundation) api(compose.material) - api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2") - api("org.jetbrains.kotlinx:kotlinx-datetime:0.3.2") - api("com.russhwolf:multiplatform-settings:1.0.0") - api("com.charleskorn.kaml:kaml:0.43.0") - api("dev.icerock.moko:resources-compose:0.23.0") + api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + api("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0") + api("com.russhwolf:multiplatform-settings:1.1.1") + api("com.charleskorn.kaml:kaml:0.59.0") api("org.jetbrains.compose.ui:ui-text:${rootProject.extra["compose.version"] as String}") implementation("org.jetbrains.compose.components:components-animatedimage:${rootProject.extra["compose.version"] as String}") //Barcode - api("org.boofcv:boofcv-core:0.40.1") + api("org.boofcv:boofcv-core:1.1.3") implementation("com.godaddy.android.colorpicker:compose-color-picker-jvm:0.7.0") // Link Previews - implementation("org.jsoup:jsoup:1.13.1") + implementation("org.jsoup:jsoup:1.17.2") // Resources - implementation("dev.icerock.moko:resources:0.23.0") + api("dev.icerock.moko:resources:0.23.0") + api("dev.icerock.moko:resources-compose:0.23.0") + + // Markdown + implementation("com.mikepenz:multiplatform-markdown-renderer:0.27.0") + implementation("com.mikepenz:multiplatform-markdown-renderer-m2:0.27.0") } } val commonTest by getting { dependencies { implementation(kotlin("test")) + implementation(kotlin("test-junit")) + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) } } val androidMain by getting { + kotlin.srcDir("build/generated/moko/androidMain/src") dependencies { - implementation("androidx.activity:activity-compose:1.5.0") - val work_version = "2.7.1" - implementation("androidx.work:work-runtime-ktx:$work_version") - implementation("com.google.accompanist:accompanist-insets:0.23.0") - implementation("dev.icerock.moko:resources:0.23.0") + implementation("androidx.activity:activity-compose:1.9.1") + val workVersion = "2.9.1" + implementation("androidx.work:work-runtime-ktx:$workVersion") // Video support - implementation("com.google.android.exoplayer:exoplayer:2.17.1") + implementation("com.google.android.exoplayer:exoplayer:2.19.1") // Biometric authentication - implementation("androidx.biometric:biometric:1.2.0-alpha04") + implementation("androidx.biometric:biometric:1.2.0-alpha05") //Barcode - implementation("org.boofcv:boofcv-android:0.40.1") + implementation("org.boofcv:boofcv-android:1.1.3") //Camera Permission - implementation("com.google.accompanist:accompanist-permissions:0.23.0") + implementation("com.google.accompanist:accompanist-permissions:0.34.0") - implementation("androidx.webkit:webkit:1.4.0") + implementation("androidx.webkit:webkit:1.11.0") // GIFs support - implementation("io.coil-kt:coil-compose:2.1.0") - implementation("io.coil-kt:coil-gif:2.1.0") + implementation("io.coil-kt:coil-compose:2.6.0") + implementation("io.coil-kt:coil-gif:2.6.0") - implementation("com.jakewharton:process-phoenix:2.1.2") + // Emojis + implementation("androidx.emoji2:emoji2-emojipicker:1.4.0") - val camerax_version = "1.1.0-beta01" - implementation("androidx.camera:camera-core:${camerax_version}") - implementation("androidx.camera:camera-camera2:${camerax_version}") - implementation("androidx.camera:camera-lifecycle:${camerax_version}") - implementation("androidx.camera:camera-view:${camerax_version}") + implementation("com.jakewharton:process-phoenix:3.0.0") + + val cameraXVersion = "1.3.4" + implementation("androidx.camera:camera-core:${cameraXVersion}") + implementation("androidx.camera:camera-camera2:${cameraXVersion}") + implementation("androidx.camera:camera-lifecycle:${cameraXVersion}") + implementation("androidx.camera:camera-view:${cameraXVersion}") + + // Calls lifecycle listener + implementation("androidx.lifecycle:lifecycle-process:2.8.4") } } val desktopMain by getting { dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.7.1") - implementation("com.github.Dansoftowner:jSystemThemeDetector:3.6") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.8.0") + implementation("com.github.Dansoftowner:jSystemThemeDetector:3.8") { + exclude("net.java.dev.jna") + } + // For jSystemThemeDetector only + implementation("net.java.dev.jna:jna-platform:5.14.0") implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT") - implementation("org.slf4j:slf4j-simple:2.0.7") - implementation("uk.co.caprica:vlcj:4.7.3") + 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.squareup.okhttp3:okhttp:4.12.0") } } val desktopTest by getting @@ -107,23 +124,21 @@ kotlin { } android { - compileSdkVersion(34) + namespace = "chat.simplex.common" + compileSdk = 34 sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") defaultConfig { - minSdkVersion(28) - targetSdkVersion(33) - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + minSdk = 26 } + testOptions.targetSdk = 34 + lint.targetSdk = 34 val isAndroid = gradle.startParameter.taskNames.find { - val lower = it.toLowerCase() + val lower = it.lowercase() lower.contains("release") || lower.startsWith("assemble") || lower.startsWith("install") } != null if (isAndroid) { // This is not needed on Android but can't be moved to desktopMain because MR lib don't support this. - // No other ways to exclude a file work but it's large and should be excluded + // No other ways to exclude a file work, but it's large and should be excluded kotlin.sourceSets["commonMain"].resources.exclude("/MR/fonts/NotoColorEmoji-Regular.ttf") } } @@ -139,6 +154,8 @@ buildConfig { buildConfigField("int", "ANDROID_VERSION_CODE", "${extra["android.version_code"]}") buildConfigField("String", "DESKTOP_VERSION_NAME", "\"${extra["desktop.version_name"]}\"") buildConfigField("int", "DESKTOP_VERSION_CODE", "${extra["desktop.version_code"]}") + buildConfigField("String", "DATABASE_BACKEND", "\"${extra["database.backend"]}\"") + buildConfigField("Boolean", "ANDROID_BUNDLE", "${extra["android.bundle"]}") } } @@ -154,8 +171,10 @@ afterEvaluate { val endStringRegex = Regex("</string>[ ]*") val endTagRegex = Regex("</") val anyHtmlRegex = Regex("[^>]*>.*(<|>).*</string>|[^>]*>.*(<|>).*</string>") + val fontLtGtRegex = Regex("[^>]*>.*<font[^>]*>.*</font>.*</string>") + val unbracketedColorRegex = Regex("color=#[abcdefABCDEF0-9]{3,6}") val correctHtmlRegex = Regex("[^>]*>.*<b>.*</b>.*</string>|[^>]*>.*<i>.*</i>.*</string>|[^>]*>.*<u>.*</u>.*</string>|[^>]*>.*<font[^>]*>.*</font>.*</string>") - val possibleFormat = listOf("s", "d", "1\$s", "1\$d", "2s", "f") + val possibleFormat = listOf("s", "d", "1\$s", "2\$s", "3\$s", "4\$s", "1\$d", "2\$d", "3\$d", "4\$d", "2s", "f") fun String.id(): String = replace("<string name=\"", "").trim().substringBefore("\"") @@ -181,7 +200,11 @@ afterEvaluate { substring = substring.substringAfter("%") if (was.length == substring.length) break } - return formats + return if (formats.any { it.startsWith("1$") || it.startsWith("2$") || it.startsWith("3$") || it.startsWith("4$") }) { + formats.sortedBy { it.trim('s', 'd', 'f', '$').toIntOrNull() ?: throw Exception("Formatting don't have positional arguments: $this \nin $filepath") } + } else { + formats + } } fun String.removeCDATA(): String = @@ -194,9 +217,12 @@ afterEvaluate { fun String.addCDATA(filepath: String): String { //return this if (anyHtmlRegex.matches(this)) { - val countOfStartTag = count { it == '<' } - val countOfEndTag = count { it == '>' } - if (countOfStartTag != countOfEndTag || countOfStartTag != endTagRegex.findAll(this).count() * 2 || !correctHtmlRegex.matches(this)) { + val prepared = if (fontLtGtRegex.matches(this) || unbracketedColorRegex.containsMatchIn(this)) { + replace("<", "<").replace(">", ">").replace(unbracketedColorRegex) { it.value.replace("color=#", "color=\"#") + "\"" } + } else this + val countOfStartTag = prepared.count { it == '<' } + val countOfEndTag = prepared.count { it == '>' } + if (countOfStartTag != countOfEndTag || countOfStartTag != endTagRegex.findAll(prepared).count() * 2 || !correctHtmlRegex.matches(prepared)) { if (debug) { println("Wrong string:") println(this) @@ -206,7 +232,7 @@ afterEvaluate { throw Exception("Wrong string: $this \nin $filepath") } } - val res = replace(startStringRegex) { it.value + "<![CDATA[" }.replace(endStringRegex) { "]]>" + it.value } + val res = prepared.replace(startStringRegex) { it.value + "<![CDATA[" }.replace(endStringRegex) { "]]>" + it.value } if (debug) { println("Changed string:") println(this) @@ -245,7 +271,9 @@ afterEvaluate { if (isBase) { baseFormatting[lineId] = fixedLine.formatting(file.absolutePath) } else if (baseFormatting[lineId] != fixedLine.formatting(file.absolutePath)) { - errors.add("Incorrect formatting in string: $fixedLine \nin ${file.absolutePath}") + errors.add("Incorrect formatting in string: $fixedLine \nin ${file.absolutePath}.\n" + + "If you want to remove non-base translation, search this Regex and replace with empty value in IDE:\n" + + "[ ]*<.*\"${line.substringAfter("\"").substringBefore("\"")}\"[^/]*\\n*.*string>\\n") } finalLines.add(fixedLine) } else if (multiline.isEmpty() && startStringRegex.containsMatchIn(line)) { diff --git a/apps/multiplatform/common/src/androidMain/AndroidManifest.xml b/apps/multiplatform/common/src/androidMain/AndroidManifest.xml index 74520465ae..eeb822503d 100644 --- a/apps/multiplatform/common/src/androidMain/AndroidManifest.xml +++ b/apps/multiplatform/common/src/androidMain/AndroidManifest.xml @@ -1,4 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="chat.simplex.common"> <uses-permission android:name="android.permission.VIBRATE" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> </manifest> diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt index e237272eb0..540533e5ad 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt @@ -17,6 +17,7 @@ val NotificationsMode.requiresIgnoringBattery lateinit var APPLICATION_ID: String -fun Uri.toURI(): URI = URI(toString()) +fun Uri.toURI(): URI = URI(toString().replace("\n", "")) +fun Uri.toURIOrNull(): URI? = try { toURI() } catch (e: Exception) { null } fun URI.toUri(): Uri = Uri.parse(toString()) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Locale.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Locale.kt index b9d7d27ba9..c289715886 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Locale.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Locale.kt @@ -31,8 +31,7 @@ private fun Activity.applyLocale(locale: Locale) { Locale.setDefault(locale) val appConf = Configuration(androidAppContext.resources.configuration).apply { setLocale(locale) } val activityConf = Configuration(resources.configuration).apply { setLocale(locale) } - @Suppress("DEPRECATION") - androidAppContext.resources.updateConfiguration(appConf, resources.displayMetrics) + androidAppContext = androidAppContext.createConfigurationContext(appConf) @Suppress("DEPRECATION") resources.updateConfiguration(activityConf, resources.displayMetrics) } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/NetworkObserver.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/NetworkObserver.kt new file mode 100644 index 0000000000..825bc8b846 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/NetworkObserver.kt @@ -0,0 +1,104 @@ +package chat.simplex.common.helpers + +import android.net.* +import android.util.Log +import androidx.core.content.getSystemService +import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.UserNetworkInfo +import chat.simplex.common.model.UserNetworkType +import chat.simplex.common.platform.* +import chat.simplex.common.views.helpers.withBGApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay + +class NetworkObserver { + private var prevInfo: UserNetworkInfo? = null + + // When having both mobile and Wi-Fi networks enabled with Wi-Fi being active, then disabling Wi-Fi, network reports its offline (which is true) + // but since it will be online after switching to mobile, there is no need to inform backend about such temporary change. + // But if it will not be online after some seconds, report it and apply required measures + private var noNetworkJob = Job() as Job + private val networkCallback = object: ConnectivityManager.NetworkCallback() { + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) = networkCapabilitiesChanged(networkCapabilities) + override fun onLost(network: Network) = networkLost() + } + private val connectivityManager: ConnectivityManager? = androidAppContext.getSystemService() + + fun restartNetworkObserver() { + if (connectivityManager == null) { + Log.e(TAG, "Connectivity manager is unavailable, network observer is disabled") + val info = UserNetworkInfo( + networkType = UserNetworkType.OTHER, + online = true, + ) + prevInfo = info + setNetworkInfo(info) + return + } + try { + connectivityManager.unregisterNetworkCallback(networkCallback) + } catch (e: Exception) { + // do nothing + } + val initialCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + if (initialCapabilities != null) { + networkCapabilitiesChanged(initialCapabilities) + } else { + networkLost() + } + try { + connectivityManager.registerDefaultNetworkCallback(networkCallback) + } catch (e: Exception) { + Log.e(TAG, "Error registering network callback: ${e.stackTraceToString()}") + } + } + + private fun networkCapabilitiesChanged(capabilities: NetworkCapabilities) { + connectivityManager ?: return + val info = UserNetworkInfo( + networkType = networkTypeFromCapabilities(capabilities), + online = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED), + ) + if (prevInfo != info) { + prevInfo = info + setNetworkInfo(info) + } + } + + private fun networkLost() { + Log.d(TAG, "Network changed: lost") + val none = UserNetworkInfo(networkType = UserNetworkType.NONE, false) + prevInfo = none + setNetworkInfo(none) + } + + private fun setNetworkInfo(info: UserNetworkInfo) { + Log.d(TAG, "Network changed: $info") + noNetworkJob.cancel() + if (info.online) { + withBGApi { + if (controller.hasChatCtrl() && controller.apiSetNetworkInfo(info)) { + chatModel.networkInfo.value = info + } + } + } else { + noNetworkJob = withBGApi { + delay(3000) + if (controller.hasChatCtrl() && controller.apiSetNetworkInfo(info)) { + chatModel.networkInfo.value = info + } + } + } + } + + private fun networkTypeFromCapabilities(capabilities: NetworkCapabilities): UserNetworkType = when { + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> UserNetworkType.ETHERNET + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> UserNetworkType.WIFI + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> UserNetworkType.CELLULAR + else -> UserNetworkType.OTHER + } + + companion object { + val shared = NetworkObserver() + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Permissions.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Permissions.kt new file mode 100644 index 0000000000..bfb45d7413 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Permissions.kt @@ -0,0 +1,30 @@ +package chat.simplex.common.helpers + +import android.content.* +import android.net.Uri +import android.provider.Settings +import chat.simplex.common.platform.* +import chat.simplex.common.views.helpers.AlertManager +import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.res.MR + +fun Context.openAppSettingsInSystem() { + Intent().apply { + action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + data = Uri.parse("package:${androidAppContext.packageName}") + try { + startActivity(this) + } catch (e: ActivityNotFoundException) { + Log.e(TAG, e.stackTraceToString()) + } + } +} + +fun Context.showAllowPermissionInSettingsAlert(action: () -> Unit = ::openAppSettingsInSystem) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.permissions_grant_in_settings), + text = generalGetString(MR.strings.permissions_find_in_settings_and_grant), + confirmText = generalGetString(MR.strings.permissions_open_settings), + onConfirm = action, + ) +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/SoundPlayer.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/SoundPlayer.kt index 25369ffdfd..d567b3f7f9 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/SoundPlayer.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/SoundPlayer.kt @@ -2,19 +2,16 @@ package chat.simplex.common.helpers import android.media.* import android.net.Uri -import android.os.VibrationEffect -import android.os.Vibrator +import android.os.* import androidx.core.content.ContextCompat import chat.simplex.common.R -import chat.simplex.common.platform.SoundPlayerInterface -import chat.simplex.common.platform.androidAppContext -import chat.simplex.common.views.helpers.withScope -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay +import chat.simplex.common.platform.* +import chat.simplex.common.views.helpers.withApi +import kotlinx.coroutines.* object SoundPlayer: SoundPlayerInterface { private var player: MediaPlayer? = null - var playing = false + private var playing = false override fun start(scope: CoroutineScope, sound: Boolean) { player?.reset() @@ -31,10 +28,10 @@ object SoundPlayer: SoundPlayerInterface { val vibrator = ContextCompat.getSystemService(androidAppContext, Vibrator::class.java) val effect = VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE) playing = true - withScope(scope) { + scope.launch { while (playing) { if (sound) player?.start() - vibrator?.vibrate(effect) + vibrator?.vibrateApiVersionAware(effect) delay(3500) } } @@ -45,3 +42,82 @@ object SoundPlayer: SoundPlayerInterface { player?.stop() } } + +object CallSoundsPlayer: CallSoundsPlayerInterface { + private var player: MediaPlayer? = null + private var playingJob: Job? = null + + private fun start(soundPath: String, delay: Long, scope: CoroutineScope) { + playingJob?.cancel() + player?.reset() + player = MediaPlayer().apply { + setAudioAttributes( + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING) + .build() + ) + setDataSource(androidAppContext, Uri.parse(soundPath)) + prepare() + } + if (delay < 1000) { + player?.isLooping = true + player?.start() + return + } + playingJob = scope.launch { + while (isActive) { + player?.start() + delay(delay) + } + } + } + + override fun startConnectingCallSound(scope: CoroutineScope) { + // Taken from https://github.com/TelegramOrg/Telegram-Android + // https://github.com/TelegramOrg/Telegram-Android/blob/master/LICENSE + start("android.resource://" + androidAppContext.packageName + "/" + R.raw.connecting_call, 0, scope) + } + + override fun startInCallSound(scope: CoroutineScope) { + start("android.resource://" + androidAppContext.packageName + "/" + R.raw.in_call, 2000, scope) + } + + override fun vibrate(times: Int) { + val vibrator = ContextCompat.getSystemService(androidAppContext, Vibrator::class.java) + val effect = VibrationEffect.createOneShot(20, VibrationEffect.DEFAULT_AMPLITUDE) + vibrator?.vibrateApiVersionAware(effect) + repeat(times - 1) { + withApi { + delay(50) + vibrator?.vibrateApiVersionAware(effect) + } + } + } + + override fun stop() { + playingJob?.cancel() + player?.stop() + } +} + +private fun Vibrator.vibrateApiVersionAware(effect: VibrationEffect) { + if (Build.VERSION.SDK_INT >= 33) { + vibrate( + effect, + VibrationAttributes.Builder() + .setUsage(VibrationAttributes.USAGE_ALARM) + .build() + ) + } else if (Build.VERSION.SDK_INT >= 29) { + vibrate( + effect, + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_ALARM) + .build() + ) + } else { + vibrate(effect) + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt index 90b18bde93..cd1672f3e9 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt @@ -4,9 +4,10 @@ import android.annotation.SuppressLint import android.content.Context import android.net.LocalServerSocket import android.util.Log +import androidx.activity.ComponentActivity import androidx.fragment.app.FragmentActivity -import chat.simplex.common.* -import chat.simplex.common.platform.* +import androidx.work.Configuration +import androidx.work.WorkManager import java.io.* import java.lang.ref.WeakReference import java.util.* @@ -23,26 +24,26 @@ var isAppOnForeground: Boolean = false @Suppress("ConstantLocale") val defaultLocale: Locale = Locale.getDefault() +actual fun isAppVisibleAndFocused(): Boolean = isAppOnForeground + @SuppressLint("StaticFieldLeak") lateinit var androidAppContext: Context -lateinit var mainActivity: WeakReference<FragmentActivity> +var mainActivity: WeakReference<FragmentActivity> = WeakReference(null) +var callActivity: WeakReference<ComponentActivity> = WeakReference(null) -fun initHaskell() { - val socketName = "chat.simplex.app.local.socket.address.listen.native.cmd2" + Random.nextLong(100000) +fun initHaskell(packageName: String) { val s = Semaphore(0) thread(name="stdout/stderr pipe") { Log.d(TAG, "starting server") - var server: LocalServerSocket? = null - for (i in 0..100) { - try { - server = LocalServerSocket(socketName + i) - break - } catch (e: IOException) { - Log.e(TAG, e.stackTraceToString()) - } - } - if (server == null) { - throw Error("Unable to setup local server socket. Contact developers") + val server: LocalServerSocket + try { + server = LocalServerSocket(packageName) + } catch (e: IOException) { + Log.e(TAG, e.stackTraceToString()) + Log.e(TAG, "Unable to setup local server socket. Contact developers") + s.release() + // Will not have logs from backend + return@thread } Log.d(TAG, "started server") s.release() @@ -56,7 +57,7 @@ fun initHaskell() { Log.d(TAG, "starting receiver loop") while (true) { val line = input.readLine() ?: break - Log.w("$TAG (stdout/stderr)", line) + Log.w(TAG, "(stdout/stderr) $line") logbuffer.add(line) } Log.w(TAG, "exited receiver loop") @@ -66,7 +67,20 @@ fun initHaskell() { System.loadLibrary("app-lib") s.acquire() - pipeStdOutToSocket(socketName) + pipeStdOutToSocket(packageName) initHS() } + +fun Context.getWorkManagerInstance(): WorkManager { + // https://github.com/OneSignal/OneSignal-Android-SDK/pull/2052/files + // https://github.com/OneSignal/OneSignal-Android-SDK/issues/1672 + if (!WorkManager.isInitialized()) { + try { + WorkManager.initialize(this, Configuration.Builder().build()) + } catch (e: IllegalStateException) { + Log.e(TAG, "Error initializing WorkManager: ${e.stackTraceToString()}") + } + } + return WorkManager.getInstance(this) +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt index dc6c53ecbc..d9a5fb59e3 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt @@ -30,10 +30,31 @@ internal class Cryptor: CryptorInterface { } return null } - val cipher: Cipher = Cipher.getInstance(TRANSFORMATION) - val spec = GCMParameterSpec(128, iv) - cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) - return runCatching { String(cipher.doFinal(data))}.onFailure { Log.e(TAG, "doFinal: ${it.stackTraceToString()}") }.getOrNull() + + try { + val cipher: Cipher = Cipher.getInstance(TRANSFORMATION) + val spec = GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) + return String(cipher.doFinal(data)) + } catch (e: Throwable) { + Log.e(TAG, "cipher.init: ${e.stackTraceToString()}") + val randomPassphrase = appPreferences.initialRandomDBPassphrase.get() + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.error_reading_passphrase), + text = generalGetString(if (randomPassphrase) { + MR.strings.restore_passphrase_can_not_be_read_desc + } else { + MR.strings.restore_passphrase_can_not_be_read_enter_manually_desc + } + ) + .plus("\n\n").plus(e.stackTraceToString()) + ) + if (randomPassphrase) { + // do not allow to override initial random passphrase in case of such error + throw e + } + return null + } } override fun encryptText(text: String, alias: String): Pair<ByteArray, ByteArray> { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt index dfc8c1d4e7..03012e318c 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt @@ -15,8 +15,12 @@ actual val dataDir: File = androidAppContext.dataDir actual val tmpDir: File = androidAppContext.getDir("temp", Application.MODE_PRIVATE) actual val filesDir: File = File(dataDir.absolutePath + File.separator + "files") actual val appFilesDir: File = File(filesDir.absolutePath + File.separator + "app_files") +actual val wallpapersDir: File = File(filesDir.absolutePath + File.separator + "assets" + File.separator + "wallpapers").also { it.mkdirs() } actual val coreTmpDir: File = File(filesDir.absolutePath + File.separator + "temp_files") actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "files" +actual val preferencesDir = File(dataDir.absolutePath + File.separator + "shared_prefs") +actual val preferencesTmpDir = File(tmpDir, "prefs_tmp") + .also { it.deleteRecursively() } actual val chatDatabaseFileName: String = "files_chat.db" actual val agentDatabaseFileName: String = "files_agent.db" @@ -27,6 +31,8 @@ actual val remoteHostsDir: File = File(tmpDir.absolutePath + File.separator + "r actual fun desktopOpenDatabaseDir() {} +actual fun desktopOpenDir(dir: File) {} + @Composable actual fun rememberFileChooserLauncher(getContent: Boolean, rememberedValue: Any?, onResult: (URI?) -> Unit): FileChooserLauncher { val launcher = rememberLauncherForActivityResult( diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Images.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Images.android.kt index 832f0d9cbb..fc323f6ffd 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Images.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Images.android.kt @@ -61,6 +61,16 @@ actual fun cropToSquare(image: ImageBitmap): ImageBitmap { return Bitmap.createBitmap(image.asAndroidBitmap(), xOffset, yOffset, side, side).asImageBitmap() } +fun Bitmap.clipToCircle(): Bitmap { + val circle = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val path = android.graphics.Path() + path.addCircle(width / 2f, height / 2f, min(width, height) / 2f, android.graphics.Path.Direction.CCW) + val canvas = android.graphics.Canvas(circle) + canvas.clipPath(path) + canvas.drawBitmap(this, 0f, 0f, null) + return circle +} + actual fun compressImageStr(bitmap: ImageBitmap): String { val usePng = bitmap.hasAlpha() val ext = if (usePng) "png" else "jpg" diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Log.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Log.android.kt index aca8efcb6f..9255584deb 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Log.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Log.android.kt @@ -1,10 +1,11 @@ package chat.simplex.common.platform import android.util.Log +import chat.simplex.common.model.ChatController.appPrefs actual object Log { - actual fun d(tag: String, text: String) = Log.d(tag, text).run{} - actual fun e(tag: String, text: String) = Log.e(tag, text).run{} - actual fun i(tag: String, text: String) = Log.i(tag, text).run{} - actual fun w(tag: String, text: String) = Log.w(tag, text).run{} + actual fun d(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.DEBUG && appPrefs.developerTools.get()) Log.d(tag, text) } + actual fun e(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.ERROR || !appPrefs.developerTools.get()) Log.e(tag, text) } + actual fun i(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.INFO && appPrefs.developerTools.get()) Log.i(tag, text) } + actual fun w(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.WARNING || !appPrefs.developerTools.get()) Log.w(tag, text) } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt index 26ada2b7e6..5d07aae088 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt @@ -3,26 +3,18 @@ package chat.simplex.common.platform import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter -import com.google.accompanist.insets.navigationBarsWithImePadding import java.io.File -actual fun Modifier.navigationBarsWithImePadding(): Modifier = navigationBarsWithImePadding() - -@Composable -actual fun ProvideWindowInsets( - consumeWindowInsets: Boolean, - windowInsetsAnimationsEnabled: Boolean, - content: @Composable () -> Unit -) { - com.google.accompanist.insets.ProvideWindowInsets(content = content) -} - @Composable actual fun Modifier.desktopOnExternalDrag( enabled: Boolean, onFiles: (List<File>) -> Unit, - onImage: (Painter) -> Unit, + onImage: (File) -> Unit, onText: (String) -> Unit ): Modifier = this actual fun Modifier.onRightClick(action: () -> Unit): Modifier = this + +actual fun Modifier.desktopPointerHoverIconHand(): Modifier = this + +actual fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier = Modifier diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt index 9e28c4f2bc..4f48ccca52 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt @@ -2,41 +2,47 @@ package chat.simplex.common.platform import android.annotation.SuppressLint import android.content.Context +import android.graphics.drawable.ColorDrawable import android.os.Build import android.text.InputType import android.util.Log -import android.view.OnReceiveContentListener -import android.view.ViewGroup +import android.view.* import android.view.inputmethod.* import android.widget.EditText import android.widget.TextView -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.children import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.InputConnectionCompat import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doOnTextChanged import chat.simplex.common.R import chat.simplex.common.helpers.toURI +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.views.chat.* -import chat.simplex.common.views.helpers.SharedContent -import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.filter import java.lang.reflect.Field import java.net.URI @@ -44,22 +50,25 @@ import java.net.URI actual fun PlatformTextField( composeState: MutableState<ComposeState>, sendMsgEnabled: Boolean, + disabledText: String?, sendMsgButtonDisabled: Boolean, textStyle: MutableState<TextStyle>, showDeleteTextButton: MutableState<Boolean>, - userIsObserver: Boolean, - onMessageChange: (String) -> Unit, + placeholder: String, + showVoiceButton: Boolean, + onMessageChange: (ComposeMessage) -> Unit, onUpArrow: () -> Unit, onFilesPasted: (List<URI>) -> Unit, + focusRequester: FocusRequester?, onDone: () -> Unit, ) { val cs = composeState.value val textColor = MaterialTheme.colors.onBackground - val tintColor = MaterialTheme.colors.secondaryVariant - val padding = PaddingValues(12.dp, 7.dp, 45.dp, 0.dp) - val paddingStart = with(LocalDensity.current) { 12.dp.roundToPx() } + val hintColor = MaterialTheme.colors.secondary + val padding = PaddingValues(0.dp, 7.dp, 50.dp, 0.dp) + val paddingStart = 0 val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() } - val paddingEnd = with(LocalDensity.current) { 45.dp.roundToPx() } + val paddingEnd = with(LocalDensity.current) { 50.dp.roundToPx() } val paddingBottom = with(LocalDensity.current) { 7.dp.roundToPx() } var showKeyboard by remember { mutableStateOf(false) } var freeFocus by remember { mutableStateOf(false) } @@ -78,9 +87,17 @@ actual fun PlatformTextField( freeFocus = true } } + LaunchedEffect(Unit) { + snapshotFlow { ModalManager.start.modalCount.value } + .filter { it > 0 } + .collect { + freeFocus = true + } + } - AndroidView(modifier = Modifier, factory = { - val editText = @SuppressLint("AppCompatCustomView") object: EditText(it) { + val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl + AndroidView(modifier = Modifier, factory = { context -> + val editText = @SuppressLint("AppCompatCustomView") object: EditText(context) { override fun setOnReceiveContentListener( mimeTypes: Array<out String>?, listener: OnReceiveContentListener? @@ -102,17 +119,26 @@ actual fun PlatformTextField( } return InputConnectionCompat.createWrapper(connection, editorInfo, onCommit) } + + override fun onSelectionChanged(selStart: Int, selEnd: Int) { + val start = minOf(text.length, minOf(selStart, selEnd)) + val end = minOf(text.length, maxOf(selStart, selEnd)) + onMessageChange(ComposeMessage(text.toString(), TextRange(start, end))) + super.onSelectionChanged(start, end) + } } editText.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) editText.maxLines = 16 editText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or editText.inputType editText.setTextColor(textColor.toArgb()) - editText.textSize = textStyle.value.fontSize.value - val drawable = androidAppContext.getDrawable(R.drawable.send_msg_view_background)!! - DrawableCompat.setTint(drawable, tintColor.toArgb()) - editText.background = drawable - editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom) - editText.setText(cs.message) + editText.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get() + editText.background = ColorDrawable(Color.Transparent.toArgb()) + editText.textDirection = if (isRtl) EditText.TEXT_DIRECTION_LOCALE else EditText.TEXT_DIRECTION_ANY_RTL + editText.setPaddingRelative(paddingStart, paddingTop, paddingEnd, paddingBottom) + editText.setText(cs.message.text) + editText.setSelection(cs.message.selection.start, cs.message.selection.end) + editText.hint = placeholder + editText.setHintTextColor(hintColor.toArgb()) if (Build.VERSION.SDK_INT >= 29) { editText.textCursorDrawable?.let { DrawableCompat.setTint(it, CurrentColors.value.colors.secondary.toArgb()) } } else { @@ -124,25 +150,37 @@ actual fun PlatformTextField( Log.e(TAG, e.stackTraceToString()) } } + editText.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> + // shows keyboard when user had search field on ChatView focused before clicking on this text field + // it still produce weird animation of closing/opening keyboard but the solution is to replace this Android EditText with Compose BasicTextField + if (hasFocus) { + showKeyboard = true + } + } editText.doOnTextChanged { text, _, _, _ -> if (!composeState.value.inProgress) { - onMessageChange(text.toString()) - } else if (text.toString() != composeState.value.message) { - editText.setText(composeState.value.message) + onMessageChange(ComposeMessage(text.toString(), TextRange(minOf(editText.selectionStart, editText.selectionEnd), maxOf(editText.selectionStart, editText.selectionEnd)))) + } else if (text.toString() != composeState.value.message.text) { + editText.setText(composeState.value.message.text) + editText.setSelection(composeState.value.message.selection.start, composeState.value.message.selection.end) } } editText.doAfterTextChanged { text -> if (composeState.value.preview is ComposePreview.VoicePreview && text.toString() != "") editText.setText("") } - editText + val workaround = WorkaroundFocusSearchLayout(context) + workaround.addView(editText) + workaround.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + workaround }) { + val it = it.children.first() as EditText it.setTextColor(textColor.toArgb()) - it.textSize = textStyle.value.fontSize.value - DrawableCompat.setTint(it.background, tintColor.toArgb()) + it.setHintTextColor(hintColor.toArgb()) + it.hint = placeholder + it.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get() it.isFocusable = composeState.value.preview !is ComposePreview.VoicePreview it.isFocusableInTouchMode = it.isFocusable - if (cs.message != it.text.toString()) { - it.setText(cs.message) - // Set cursor to the end of the text - it.setSelection(it.text.length) + if (cs.message.text != it.text.toString() || cs.message.selection.start != it.selectionStart || cs.message.selection.end != it.selectionEnd) { + it.setText(cs.message.text) + it.setSelection(cs.message.selection.start, cs.message.selection.end) } if (showKeyboard) { it.requestFocus() @@ -158,16 +196,16 @@ actual fun PlatformTextField( showDeleteTextButton.value = it.lineCount >= 4 && !cs.inProgress } if (composeState.value.preview is ComposePreview.VoicePreview) { - ComposeOverlay(MR.strings.voice_message_send_text, textStyle, padding) - } else if (userIsObserver) { - ComposeOverlay(MR.strings.you_are_observer, textStyle, padding) + ComposeOverlay(generalGetString(MR.strings.voice_message_send_text), textStyle, padding) + } else if (disabledText != null) { + ComposeOverlay(disabledText, textStyle, padding) } } @Composable -private fun ComposeOverlay(textId: StringResource, textStyle: MutableState<TextStyle>, padding: PaddingValues) { +private fun ComposeOverlay(text: String, textStyle: MutableState<TextStyle>, padding: PaddingValues) { Text( - generalGetString(textId), + text, Modifier.padding(padding), color = MaterialTheme.colors.secondary, style = textStyle.value.copy(fontStyle = FontStyle.Italic) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt index 5b0d3c778f..e5dda23f0f 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt @@ -126,16 +126,11 @@ actual object AudioPlayer: AudioPlayerInterface { .build() ) } - // Filepath: String, onProgressUpdate - private val currentlyPlaying: MutableState<Pair<String, (position: Int?, state: TrackState) -> Unit>?> = mutableStateOf(null) + override val currentlyPlaying: MutableState<CurrentlyPlayingState?> = mutableStateOf(null) private var progressJob: Job? = null - enum class TrackState { - PLAYING, PAUSED, REPLACED - } - // Returns real duration of the track - private fun start(fileSource: CryptoFile, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? { + private fun start(fileSource: CryptoFile, smallView: Boolean, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? { val absoluteFilePath = if (fileSource.isAbsolutePath) fileSource.filePath else getAppFilePath(fileSource.filePath) if (!File(absoluteFilePath).exists()) { Log.e(TAG, "No such file: ${fileSource.filePath}") @@ -145,7 +140,7 @@ actual object AudioPlayer: AudioPlayerInterface { VideoPlayerHolder.stopAll() RecorderInterface.stopRecording?.invoke() val current = currentlyPlaying.value - if (current == null || current.first != fileSource.filePath) { + if (current == null || current.fileSource.filePath != fileSource.filePath || smallView != current.smallView) { stopListener() player.reset() runCatching { @@ -156,19 +151,19 @@ actual object AudioPlayer: AudioPlayerInterface { } }.onFailure { Log.e(TAG, it.stackTraceToString()) - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.message) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.stackTraceToString()) return null } runCatching { player.prepare() }.onFailure { // Can happen when audio file is broken Log.e(TAG, it.stackTraceToString()) - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.message) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.stackTraceToString()) return null } } if (seek != null) player.seekTo(seek) player.start() - currentlyPlaying.value = fileSource.filePath to onProgressUpdate + currentlyPlaying.value = CurrentlyPlayingState(fileSource, onProgressUpdate, smallView) progressJob = CoroutineScope(Dispatchers.Default).launch { onProgressUpdate(player.currentPosition, TrackState.PLAYING) while(isActive && player.isPlaying) { @@ -192,6 +187,10 @@ actual object AudioPlayer: AudioPlayerInterface { } keepScreenOn(false) onProgressUpdate(null, TrackState.PAUSED) + + if (smallView && isActive) { + stopListener() + } } return player.duration } @@ -215,7 +214,7 @@ actual object AudioPlayer: AudioPlayerInterface { // FileName or filePath are ok override fun stop(fileName: String?) { - if (fileName != null && currentlyPlaying.value?.first?.endsWith(fileName) == true) { + if (fileName != null && currentlyPlaying.value?.fileSource?.filePath?.endsWith(fileName) == true) { stop() } } @@ -223,7 +222,7 @@ actual object AudioPlayer: AudioPlayerInterface { private fun stopListener() { val afterCoroutineCancel: CompletionHandler = { // Notify prev audio listener about stop - currentlyPlaying.value?.second?.invoke(null, TrackState.REPLACED) + currentlyPlaying.value?.onProgressUpdate?.invoke(null, TrackState.REPLACED) currentlyPlaying.value = null } /** Preventing race by calling a code AFTER coroutine ends, so [TrackState] will be: @@ -244,11 +243,12 @@ actual object AudioPlayer: AudioPlayerInterface { progress: MutableState<Int>, duration: MutableState<Int>, resetOnEnd: Boolean, + smallView: Boolean, ) { if (progress.value == duration.value) { progress.value = 0 } - val realDuration = start(fileSource, progress.value) { pro, state -> + val realDuration = start(fileSource, smallView, progress.value) { pro, state -> if (pro != null) { progress.value = pro } @@ -274,7 +274,7 @@ actual object AudioPlayer: AudioPlayerInterface { override fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) { pro.value = ms - if (currentlyPlaying.value?.first == filePath) { + if (currentlyPlaying.value?.fileSource?.filePath == filePath) { player.seekTo(ms) } } @@ -296,6 +296,7 @@ actual object AudioPlayer: AudioPlayerInterface { } actual typealias SoundPlayer = chat.simplex.common.helpers.SoundPlayer +actual typealias CallSoundsPlayer = chat.simplex.common.helpers.CallSoundsPlayer class CryptoMediaSource(val data: ByteArray) : MediaDataSource() { override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt index e15d1f9268..d4b77274ba 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt @@ -6,17 +6,21 @@ import android.content.Context import android.content.SharedPreferences import android.content.res.Configuration import android.text.BidiFormatter +import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.* import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap import chat.simplex.common.model.AppPreferences import com.russhwolf.settings.Settings import com.russhwolf.settings.SharedPreferencesSettings +import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.desc.desc @@ -46,8 +50,18 @@ actual fun windowOrientation(): WindowOrientation = when (mainActivity.get()?.re } @Composable -actual fun windowWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp +actual fun windowWidth(): Dp { + val direction = LocalLayoutDirection.current + val cutout = WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal).asPaddingValues() + return LocalConfiguration.current.screenWidthDp.dp - cutout.calculateStartPadding(direction) - cutout.calculateEndPadding(direction) +} + +@Composable +actual fun windowHeight(): Dp = LocalConfiguration.current.screenHeightDp.dp actual fun desktopExpandWindowToWidth(width: Dp) {} actual fun isRtl(text: CharSequence): Boolean = BidiFormatter.getInstance().isRtl(text) + +actual fun ImageResource.toComposeImageBitmap(): ImageBitmap? = + getDrawable(androidAppContext)?.toBitmap()?.asImageBitmap() diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt new file mode 100644 index 0000000000..0d07de28f8 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt @@ -0,0 +1,191 @@ +package chat.simplex.common.platform + +import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.views.chatlist.NavigationBarBackground +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.OnboardingStage +import kotlinx.coroutines.flow.filter +import kotlin.math.absoluteValue + +@Composable +actual fun LazyColumnWithScrollBar( + modifier: Modifier, + state: LazyListState?, + contentPadding: PaddingValues, + reverseLayout: Boolean, + verticalArrangement: Arrangement.Vertical, + horizontalAlignment: Alignment.Horizontal, + flingBehavior: FlingBehavior, + userScrollEnabled: Boolean, + additionalBarOffset: State<Dp>?, + additionalTopBar: State<Boolean>, + chatBottomBar: State<Boolean>, + fillMaxSize: Boolean, + content: LazyListScope.() -> Unit +) { + val handler = LocalAppBarHandler.current + require(handler != null) { "Using LazyColumnWithScrollBar and without AppBarHandler is an error. Use LazyColumnWithScrollBarNoAppBar instead" } + + val state = state ?: handler.listState + val connection = handler.connection + LaunchedEffect(Unit) { + if (reverseLayout) { + snapshotFlow { state.layoutInfo.visibleItemsInfo.lastOrNull()?.offset ?: 0 } + .collect { scrollPosition -> + connection.appBarOffset = if (state.layoutInfo.visibleItemsInfo.lastOrNull()?.index == state.layoutInfo.totalItemsCount - 1) { + state.layoutInfo.viewportEndOffset - scrollPosition.toFloat() - state.layoutInfo.afterContentPadding + } else { + // show always when last item is not visible + -1000f + } + //Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } + } else { + snapshotFlow { state.firstVisibleItemScrollOffset } + .filter { state.firstVisibleItemIndex == 0 } + .collect { scrollPosition -> + val offset = connection.appBarOffset + if ((offset + scrollPosition + state.layoutInfo.afterContentPadding).absoluteValue > 1) { + connection.appBarOffset = -scrollPosition.toFloat() + //Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } + } + } + } + LazyColumn( + if (fillMaxSize) { + Modifier.fillMaxSize().copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection) + } else { + Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection) + }, + state, + contentPadding, + reverseLayout, + verticalArrangement, + horizontalAlignment, + flingBehavior, + userScrollEnabled + ) { + content() + } +} + + +@Composable +actual fun LazyColumnWithScrollBarNoAppBar( + modifier: Modifier, + state: LazyListState?, + contentPadding: PaddingValues, + reverseLayout: Boolean, + verticalArrangement: Arrangement.Vertical, + horizontalAlignment: Alignment.Horizontal, + flingBehavior: FlingBehavior, + userScrollEnabled: Boolean, + additionalBarOffset: State<Dp>?, + additionalTopBar: State<Boolean>, + chatBottomBar: State<Boolean>, + maxHeight: State<Dp>?, + containerAlignment: Alignment, + content: LazyListScope.() -> Unit + ) { + val state = state ?: rememberLazyListState() + LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled) { + content() + } +} + +@Composable +actual fun ColumnWithScrollBar( + modifier: Modifier, + verticalArrangement: Arrangement.Vertical, + horizontalAlignment: Alignment.Horizontal, + state: ScrollState?, + maxIntrinsicSize: Boolean, + fillMaxSize: Boolean, + content: @Composable() (ColumnScope.() -> Unit) +) { + val handler = LocalAppBarHandler.current + require(handler != null) { "Using ColumnWithScrollBar and without AppBarHandler is an error. Use ColumnWithScrollBarNoAppBar instead" } + + val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier).imePadding() else modifier.imePadding() + val state = state ?: handler.scrollState + val connection = handler.connection + LaunchedEffect(Unit) { + snapshotFlow { state.value } + .collect { scrollPosition -> + val offset = connection.appBarOffset + if ((offset + scrollPosition).absoluteValue > 1) { + connection.appBarOffset = -scrollPosition.toFloat() +// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } + } + } + val oneHandUI = remember { derivedStateOf { if (appPrefs.onboardingStage.state.value == OnboardingStage.OnboardingComplete) appPrefs.oneHandUI.state.value else false } } + Box(Modifier.fillMaxHeight()) { + Column( + if (maxIntrinsicSize) { + Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection).verticalScroll(state).height(IntrinsicSize.Max) + } else { + Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection).verticalScroll(state) + }, verticalArrangement, horizontalAlignment + ) { + if (oneHandUI.value) { + Spacer(Modifier.padding(top = DEFAULT_PADDING + 5.dp).windowInsetsTopHeight(WindowInsets.statusBars)) + content() + Spacer(Modifier.navigationBarsPadding().padding(bottom = AppBarHeight * fontSizeSqrtMultiplier)) + } else { + Spacer(Modifier.statusBarsPadding().padding(top = AppBarHeight * fontSizeSqrtMultiplier)) + content() + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) + } + } + if (!oneHandUI.value) { + NavigationBarBackground(false, false) + } + } +} + +@Composable +actual fun ColumnWithScrollBarNoAppBar( + modifier: Modifier, + verticalArrangement: Arrangement.Vertical, + horizontalAlignment: Alignment.Horizontal, + state: ScrollState?, + maxIntrinsicSize: Boolean, + content: @Composable() (ColumnScope.() -> Unit) +) { + val modifier = modifier.imePadding() + val state = state ?: rememberScrollState() + val oneHandUI = remember { appPrefs.oneHandUI.state } + Box(Modifier.fillMaxHeight()) { + Column( + if (maxIntrinsicSize) { + modifier.verticalScroll(state).height(IntrinsicSize.Max) + } else { + modifier.verticalScroll(state) + }, verticalArrangement, horizontalAlignment + ) { + if (oneHandUI.value) { + Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars)) + content() + } else { + content() + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) + } + } + if (!oneHandUI.value) { + NavigationBarBackground(false, false) + } + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt index f0c5ea6941..79f0a8636b 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt @@ -3,31 +3,52 @@ package chat.simplex.common.platform import android.Manifest import android.content.* import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.content.pm.PackageManager import android.net.Uri import android.provider.MediaStore import android.webkit.MimeTypeMap +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.UriHandler +import androidx.core.graphics.drawable.toBitmap import chat.simplex.common.helpers.* import chat.simplex.common.model.* import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR import java.io.BufferedOutputStream import java.io.File -import chat.simplex.res.MR +import java.net.URI +import kotlin.math.min + +data class OpenDefaultApp( + val name: String, + val icon: ImageBitmap, + val isSystemChooser: Boolean +) actual fun ClipboardManager.shareText(text: String) { - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, text) - type = "text/plain" - flags = FLAG_ACTIVITY_NEW_TASK + var text = text + for (i in 10 downTo 1) { + try { + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, text) + type = "text/plain" + flags = FLAG_ACTIVITY_NEW_TASK + } + val shareIntent = Intent.createChooser(sendIntent, null) + shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) + androidAppContext.startActivity(shareIntent) + break + } catch (e: Exception) { + Log.e(TAG, "Failed to share text: ${e.stackTraceToString()}") + text = text.substring(0, min(i * 1000, text.length)) + } } - val shareIntent = Intent.createChooser(sendIntent, null) - shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) - androidAppContext.startActivity(shareIntent) } -actual fun shareFile(text: String, fileSource: CryptoFile) { +fun openOrShareFile(text: String, fileSource: CryptoFile, justOpen: Boolean, useChooser: Boolean = true) { val uri = if (fileSource.cryptoArgs != null) { val tmpFile = File(tmpDir, fileSource.filePath) tmpFile.deleteOnExit() @@ -36,6 +57,7 @@ actual fun shareFile(text: String, fileSource: CryptoFile) { decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs, tmpFile.absolutePath) } catch (e: Exception) { Log.e(TAG, "Unable to decrypt crypto file: " + e.stackTraceToString()) + AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString()) return } getAppFileUri(tmpFile.absolutePath) @@ -44,18 +66,55 @@ actual fun shareFile(text: String, fileSource: CryptoFile) { } val ext = fileSource.filePath.substringAfterLast(".") val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND + val sendIntent: Intent = Intent(if (justOpen) Intent.ACTION_VIEW else Intent.ACTION_SEND).apply { /*if (text.isNotEmpty()) { putExtra(Intent.EXTRA_TEXT, text) }*/ - putExtra(Intent.EXTRA_STREAM, uri.toUri()) - type = mimeType - flags = Intent.FLAG_ACTIVITY_NEW_TASK + if (justOpen) { + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + setDataAndType(uri.toUri(), mimeType) + } else { + putExtra(Intent.EXTRA_STREAM, uri.toUri()) + type = mimeType + } } - val shareIntent = Intent.createChooser(sendIntent, null) - shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) - androidAppContext.startActivity(shareIntent) + if (useChooser) { + val shareIntent = Intent.createChooser(sendIntent, null) + shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) + androidAppContext.startActivity(shareIntent) + } else { + sendIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) + androidAppContext.startActivity(sendIntent) + } +} + +fun queryDefaultAppForExtension(ext: String, encryptedFileUri: URI): OpenDefaultApp? { + val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return null + val openIntent = Intent(Intent.ACTION_VIEW) + openIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + openIntent.setDataAndType(encryptedFileUri.toUri(), mimeType) + val pm = androidAppContext.packageManager +//// This method returns the list of apps but no priority, nor default flag +// val resInfoList: List<ResolveInfo> = if (Build.VERSION.SDK_INT >= 33) { +// pm.queryIntentActivities(openIntent, PackageManager.ResolveInfoFlags.of((PackageManager.MATCH_DEFAULT_ONLY).toLong())) +// } else { +// pm.queryIntentActivities(openIntent, PackageManager.MATCH_DEFAULT_ONLY) +// }.sortedBy { it.priority } +// val first = resInfoList.firstOrNull { it.isDefault } ?: resInfoList.firstOrNull() ?: return null + val act = pm.resolveActivity(openIntent, PackageManager.MATCH_DEFAULT_ONLY) ?: return null +// Log.d(TAG, "Default launch action ${act} ${act.loadLabel(pm)} ${act.activityInfo?.name}") + val label = act.loadLabel(pm).toString() + val icon = act.loadIcon(pm).toBitmap().asImageBitmap() + val chooser = act.activityInfo?.name?.endsWith("ResolverActivity") == true + return OpenDefaultApp(label, icon, chooser) +} + +actual fun shareFile(text: String, fileSource: CryptoFile) { + openOrShareFile(text, fileSource, justOpen = false) +} + +actual fun openFile(fileSource: CryptoFile) { + openOrShareFile("", fileSource, justOpen = true) } actual fun UriHandler.sendEmail(subject: String, body: CharSequence) { @@ -102,6 +161,7 @@ fun saveImage(ciFile: CIFile?) { decryptCryptoFile(filePath, ciFile.fileSource.cryptoArgs, tmpFile.absolutePath) } catch (e: Exception) { Log.e(TAG, "Unable to decrypt crypto file: " + e.stackTraceToString()) + AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString()) return@createTmpFileAndDelete } tmpFile.inputStream().use { it.copyTo(outputStream) } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt index 371c140133..f6066d1624 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt @@ -3,18 +3,22 @@ package chat.simplex.common.platform import android.app.Activity import android.content.Context import android.content.pm.ActivityInfo -import android.graphics.Rect import android.os.* import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.ime import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalView import chat.simplex.common.AppScreen +import chat.simplex.common.model.clearAndNotify import chat.simplex.common.views.helpers.* import androidx.compose.ui.platform.LocalContext as LocalContext1 import chat.simplex.res.MR +import kotlinx.coroutines.* actual fun showToast(text: String, timeout: Long) = Toast.makeText(androidAppContext, text, Toast.LENGTH_SHORT).show() @@ -41,34 +45,22 @@ actual fun LocalMultiplatformView(): Any? = LocalView.current @Composable actual fun getKeyboardState(): State<KeyboardState> { - val keyboardState = remember { mutableStateOf(KeyboardState.Closed) } - val view = LocalView.current - DisposableEffect(view) { - val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener { - val rect = Rect() - view.getWindowVisibleDisplayFrame(rect) - val screenHeight = view.rootView.height - val keypadHeight = screenHeight - rect.bottom - keyboardState.value = if (keypadHeight > screenHeight * 0.15) { - KeyboardState.Opened - } else { - KeyboardState.Closed - } - } - view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener) - - onDispose { - view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener) + val density = LocalDensity.current + val ime = WindowInsets.ime + return remember { + derivedStateOf { + if (ime.getBottom(density) == 0) KeyboardState.Closed else KeyboardState.Opened } } - - return keyboardState } -actual fun hideKeyboard(view: Any?) { +actual fun hideKeyboard(view: Any?, clearFocus: Boolean) { // LALAL // LocalSoftwareKeyboardController.current?.hide() if (view is View) { + if (clearFocus) { + view.clearFocus() + } (androidAppContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(view.windowToken, 0) } } @@ -82,9 +74,13 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler { if (ModalManager.start.hasModalsOpen()) { ModalManager.start.closeModal() } else if (chatModel.chatId.value != null) { - // Since no modals are open, the problem is probably in ChatView - chatModel.chatId.value = null - chatModel.chatItems.clear() + withApi { + withContext(Dispatchers.Main) { + // Since no modals are open, the problem is probably in ChatView + chatModel.chatId.value = null + chatModel.chatsContext.chatItems.clearAndNotify() + } + } } else { // ChatList, nothing to do. Maybe to show other view except ChatList } @@ -97,12 +93,14 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler { mainActivity.get()?.recreate() } else { mainActivity.get()?.apply { - window - ?.decorView - ?.findViewById<ViewGroup>(android.R.id.content) - ?.removeViewAt(0) - setContent { - AppScreen() + runOnUiThread { + window + ?.decorView + ?.findViewById<ViewGroup>(android.R.id.content) + ?.removeViewAt(0) + setContent { + AppScreen() + } } } } @@ -110,7 +108,8 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler { Handler(Looper.getMainLooper()).post { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.app_was_crashed), - text = e.stackTraceToString() + text = e.stackTraceToString(), + shareText = true ) } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt index d3b4609bc4..61b404f7d6 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt @@ -33,9 +33,9 @@ actual class VideoPlayer actual constructor( override val duration: MutableState<Long> = mutableStateOf(defaultDuration) override val preview: MutableState<ImageBitmap> = mutableStateOf(defaultPreview) - init { - setPreviewAndDuration() - } + + // Currently unused because we use low-quality preview + // init { setPreviewAndDuration() } val player = ExoPlayer.Builder(androidAppContext, DefaultRenderersFactory(androidAppContext)) @@ -69,7 +69,7 @@ actual class VideoPlayer actual constructor( } private fun start(seek: Long? = null, onProgressUpdate: (position: Long?, state: TrackState) -> Unit): Boolean { - val filepath = getAppFilePath(uri) + val filepath = if (uri.scheme == "file") uri.toFile().absolutePath else getAppFilePath(uri) if (filepath == null || !File(filepath).exists()) { Log.e(TAG, "No such file: $filepath") brokenVideo.value = true @@ -88,7 +88,7 @@ actual class VideoPlayer actual constructor( player.setMediaSource(source, seek ?: 0L) }.onFailure { Log.e(TAG, it.stackTraceToString()) - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.message) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.stackTraceToString()) brokenVideo.value = true return false } @@ -97,7 +97,7 @@ actual class VideoPlayer actual constructor( runCatching { player.prepare() }.onFailure { // Can happen when video file is broken Log.e(TAG, it.stackTraceToString()) - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.message) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.stackTraceToString()) brokenVideo.value = true return false } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallAudioDeviceManager.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallAudioDeviceManager.kt new file mode 100644 index 0000000000..0c23e95285 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallAudioDeviceManager.kt @@ -0,0 +1,242 @@ +package chat.simplex.common.views.call + +import android.content.Context +import android.media.* +import android.media.AudioManager.OnCommunicationDeviceChangedListener +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.runtime.* +import chat.simplex.common.platform.* +import dev.icerock.moko.resources.ImageResource +import chat.simplex.res.MR +import dev.icerock.moko.resources.StringResource +import java.util.concurrent.Executors + +interface CallAudioDeviceManagerInterface { + val devices: State<List<AudioDeviceInfo>> + val currentDevice: MutableState<AudioDeviceInfo?> + fun start() + fun stop() + // AudioDeviceInfo.AudioDeviceType + fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyExternal: Boolean) + // AudioDeviceInfo.AudioDeviceType + fun selectDevice(id: Int) + + companion object { + fun new(): CallAudioDeviceManagerInterface = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PostSCallAudioDeviceManager() + } else { + PreSCallAudioDeviceManager() + } + } +} + +@RequiresApi(Build.VERSION_CODES.S) +class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface { + private val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager + override val devices: MutableState<List<AudioDeviceInfo>> = mutableStateOf(emptyList()) + override val currentDevice: MutableState<AudioDeviceInfo?> = mutableStateOf(null) + + private val audioCallback = object: AudioDeviceCallback() { + override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) { + Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}") + super.onAudioDevicesAdded(addedDevices) + val oldDevices = devices.value + devices.value = am.availableCommunicationDevices + Log.d(TAG, "Added audio devices2: ${devices.value.map { it.type }}") + + if (devices.value.size - oldDevices.size > 0) { + selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, false) + } + } + + override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) { + Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}") + super.onAudioDevicesRemoved(removedDevices) + devices.value = am.availableCommunicationDevices + } + } + + private val listener: OnCommunicationDeviceChangedListener = OnCommunicationDeviceChangedListener { device -> + devices.value = am.availableCommunicationDevices + currentDevice.value = device + } + + override fun start() { + am.mode = AudioManager.MODE_IN_COMMUNICATION + currentDevice.value = am.communicationDevice + am.registerAudioDeviceCallback(audioCallback, null) + am.addOnCommunicationDeviceChangedListener(Executors.newSingleThreadExecutor(), listener) + } + + override fun stop() { + try { + am.unregisterAudioDeviceCallback(audioCallback) + am.removeOnCommunicationDeviceChangedListener(listener) + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + } + } + + override fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyExternal: Boolean) { + Log.d(TAG, "selectLastExternalDeviceOrDefault: set audio mode, speaker enabled: $speaker") + val commDevice = am.communicationDevice + if (keepAnyExternal && commDevice != null && commDevice.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE && commDevice.type != AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) { + // some external device selected already, no need to change it + return + } + + val preferredInternalDevice = if (speaker) AudioDeviceInfo.TYPE_BUILTIN_SPEAKER else AudioDeviceInfo.TYPE_BUILTIN_EARPIECE + val externalDevice = devices.value.lastOrNull { it.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE && it.type != AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } + // External device already selected + if (externalDevice != null && externalDevice.type == am.communicationDevice?.type) { + return + } + if (externalDevice != null) { + am.setCommunicationDevice(externalDevice) + } else if (am.communicationDevice?.type != preferredInternalDevice) { + am.availableCommunicationDevices.firstOrNull { it.type == preferredInternalDevice }?.let { + am.setCommunicationDevice(it) + } + } + } + + override fun selectDevice(id: Int) { + val device = devices.value.lastOrNull { it.id == id } + if (device != null && am.communicationDevice?.id != id ) { + am.setCommunicationDevice(device) + } + } +} + +class PreSCallAudioDeviceManager: CallAudioDeviceManagerInterface { + private val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager + override val devices: MutableState<List<AudioDeviceInfo>> = mutableStateOf(emptyList()) + override val currentDevice: MutableState<AudioDeviceInfo?> = mutableStateOf(null) + + private val audioCallback = object: AudioDeviceCallback() { + override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) { + Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}") + super.onAudioDevicesAdded(addedDevices) + devices.value = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).filter { it.hasSupportedType() }.excludeSameType().excludeEarpieceIfWired() + selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, false) + } + + override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) { + Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}") + super.onAudioDevicesRemoved(removedDevices) + devices.value = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).filter { it.hasSupportedType() }.excludeSameType().excludeEarpieceIfWired() + selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, true) + } + } + + override fun start() { + am.mode = AudioManager.MODE_IN_COMMUNICATION + am.registerAudioDeviceCallback(audioCallback, null) + } + + override fun stop() { + am.unregisterAudioDeviceCallback(audioCallback) + am.stopBluetoothSco() + } + + override fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyExternal: Boolean) { + Log.d(TAG, "selectLastExternalDeviceOrDefault: set audio mode, speaker enabled: $speaker") + val preferredInternalDevice = if (speaker) AudioDeviceInfo.TYPE_BUILTIN_SPEAKER else AudioDeviceInfo.TYPE_BUILTIN_EARPIECE + val externalDevice = devices.value.lastOrNull { it.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE && it.type != AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } + if (externalDevice != null) { + selectDevice(externalDevice.id) + } else { + am.stopBluetoothSco() + am.isWiredHeadsetOn = false + am.isSpeakerphoneOn = preferredInternalDevice == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER + am.isBluetoothScoOn = false + val newCurrentDevice = devices.value.firstOrNull { it.type == preferredInternalDevice } + adaptToCurrentlyActiveDevice(newCurrentDevice) + } + } + + override fun selectDevice(id: Int) { + val device = devices.value.lastOrNull { it.id == id } + val isExternalDevice = device != null && device.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE && device.type != AudioDeviceInfo.TYPE_BUILTIN_SPEAKER + if (isExternalDevice) { + am.isSpeakerphoneOn = false + if (device?.type == AudioDeviceInfo.TYPE_WIRED_HEADSET || device?.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) { + am.isWiredHeadsetOn = true + am.stopBluetoothSco() + am.isBluetoothScoOn = false + } else { + am.isWiredHeadsetOn = false + am.startBluetoothSco() + am.isBluetoothScoOn = true + } + adaptToCurrentlyActiveDevice(device) + } else { + am.stopBluetoothSco() + am.isWiredHeadsetOn = false + am.isSpeakerphoneOn = device?.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER + am.isBluetoothScoOn = false + adaptToCurrentlyActiveDevice(device) + } + } + + private fun adaptToCurrentlyActiveDevice(newCurrentDevice: AudioDeviceInfo?) { + currentDevice.value = newCurrentDevice + } + + private fun AudioDeviceInfo.hasSupportedType(): Boolean = when (type) { + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> true + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> true + + AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> true + AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> true + else -> false + } + + private fun List<AudioDeviceInfo>.excludeSameType(): List<AudioDeviceInfo> = + groupBy { it.type }.flatMap { devices -> listOf(devices.value.minByOrNull { it.id }!!) } + + // Earpiece will not work if there is a wired connection + private fun List<AudioDeviceInfo>.excludeEarpieceIfWired(): List<AudioDeviceInfo> = + if (any { it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET || it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES }) + filter { it.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE } + else this +} + +val AudioDeviceInfo.icon: ImageResource + get() = when (this.type) { + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> MR.images.ic_volume_down + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> MR.images.ic_volume_up + + AudioDeviceInfo.TYPE_BLUETOOTH_SCO, + AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, + AudioDeviceInfo.TYPE_BLE_HEADSET, + AudioDeviceInfo.TYPE_BLE_SPEAKER -> MR.images.ic_bluetooth + + AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> MR.images.ic_headphones + + AudioDeviceInfo.TYPE_USB_HEADSET, AudioDeviceInfo.TYPE_USB_DEVICE -> MR.images.ic_usb + else -> MR.images.ic_brand_awareness_filled + } + +val AudioDeviceInfo.name: StringResource? + get() = when (this.type) { + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> MR.strings.audio_device_earpiece + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> MR.strings.audio_device_speaker + + AudioDeviceInfo.TYPE_BLUETOOTH_SCO, + AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, + AudioDeviceInfo.TYPE_BLE_HEADSET, + AudioDeviceInfo.TYPE_BLE_SPEAKER -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // Use product name instead + null + } else { + MR.strings.audio_device_bluetooth + } + + AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> MR.strings.audio_device_wired_headphones + + AudioDeviceInfo.TYPE_USB_HEADSET, AudioDeviceInfo.TYPE_USB_DEVICE -> null // Use product name instead + else -> null // Use product name instead + } 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 5f30d21bb9..166f4ec355 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 @@ -1,134 +1,170 @@ package chat.simplex.common.views.call +import SectionSpacer +import SectionView import android.Manifest import android.annotation.SuppressLint import android.app.Activity import android.content.* -import android.content.pm.ActivityInfo +import android.content.pm.PackageManager import android.media.* import android.os.Build import android.os.PowerManager import android.os.PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK +import android.os.PowerManager.WakeLock +import android.view.View import android.view.ViewGroup import android.webkit.* import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource 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.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.dp +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.* import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver +import androidx.core.content.ContextCompat +import androidx.lifecycle.* import androidx.webkit.WebViewAssetLoader import androidx.webkit.WebViewClientCompat +import chat.simplex.common.helpers.applyAppLocale +import chat.simplex.common.helpers.showAllowPermissionInSettingsAlert import chat.simplex.common.model.* -import chat.simplex.common.ui.theme.* -import chat.simplex.common.model.ChatModel -import chat.simplex.common.model.Contact +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR -import com.google.accompanist.permissions.rememberMultiplePermissionsState +import com.google.accompanist.permissions.* import dev.icerock.moko.resources.StringResource +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.datetime.Clock -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString +import java.io.Closeable -@SuppressLint("SourceLockedOrientationActivity") -@Composable -actual fun ActiveCallView() { - val chatModel = ChatModel - BackHandler(onBack = { - val call = chatModel.activeCall.value - if (call != null) withBGApi { chatModel.callManager.endCall(call) } - }) - val audioViaBluetooth = rememberSaveable { mutableStateOf(false) } - val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE } - LaunchedEffect(Unit) { - // Start service when call happening since it's not already started. - // It's needed to prevent Android from shutting down a microphone after a minute or so when screen is off - if (!ntfModeService) platform.androidServiceStart() - } - DisposableEffect(Unit) { - val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager - var btDeviceCount = 0 - val audioCallback = object: AudioDeviceCallback() { - override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) { - Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}") - super.onAudioDevicesAdded(addedDevices) - val addedCount = addedDevices.count { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO } - btDeviceCount += addedCount - audioViaBluetooth.value = btDeviceCount > 0 - if (addedCount > 0 && chatModel.activeCall.value?.callState == CallState.Connected) { - // Setting params in Connected state makes sure that Bluetooth will NOT be broken on Android < 12 - setCallSound(chatModel.activeCall.value?.soundSpeaker ?: return, audioViaBluetooth) - } - } - override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) { - Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}") - super.onAudioDevicesRemoved(removedDevices) - val removedCount = removedDevices.count { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO } - btDeviceCount -= removedCount - audioViaBluetooth.value = btDeviceCount > 0 - if (btDeviceCount == 0 && chatModel.activeCall.value?.callState == CallState.Connected) { - // Setting params in Connected state makes sure that Bluetooth will NOT be broken on Android < 12 - setCallSound(chatModel.activeCall.value?.soundSpeaker ?: return, audioViaBluetooth) - } - } +// Should be destroy()'ed and set as null when call is ended. Otherwise, it will be a leak +@SuppressLint("StaticFieldLeak") +private var staticWebView: WebView? = null + +// WebView methods must be called on Main thread +fun activeCallDestroyWebView() = withApi { + // Stop it when call ended + platform.androidCallServiceSafeStop() + staticWebView?.destroy() + staticWebView = null + Log.d(TAG, "CallView: webview was destroyed") +} + +class ActiveCallState: Closeable { + val proximityLock: WakeLock? = screenOffWakeLock() + var wasConnected = false + val callAudioDeviceManager = CallAudioDeviceManagerInterface.new() + private var closed = false + + init { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + callAudioDeviceManager.start() } - am.registerAudioDeviceCallback(audioCallback, null) + } + + override fun close() { + if (closed) return + closed = true + CallSoundsPlayer.stop() + if (wasConnected) { + CallSoundsPlayer.vibrate() + } + callAudioDeviceManager.stop() + dropAudioManagerOverrides() + if (proximityLock?.isHeld == true) { + proximityLock.release() + } + } + + private fun screenOffWakeLock(): WakeLock? { val pm = (androidAppContext.getSystemService(Context.POWER_SERVICE) as PowerManager) - val proximityLock = if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { + return if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, androidAppContext.packageName + ":proximityLock") } else { null } - proximityLock?.acquire() - onDispose { - // Stop it when call ended - if (!ntfModeService) platform.androidServiceSafeStop() - dropAudioManagerOverrides() - am.unregisterAudioDeviceCallback(audioCallback) - proximityLock?.release() + } +} + + +@SuppressLint("SourceLockedOrientationActivity") +@Composable +actual fun ActiveCallView() { + val call = remember { chatModel.activeCall }.value + val callState = call?.androidCallState as ActiveCallState? + val scope = rememberCoroutineScope() + LaunchedEffect(call) { + if (call?.callState == CallState.Connected && callState != null && !callState.wasConnected) { + CallSoundsPlayer.vibrate(2) + callState.wasConnected = true + } + } + LaunchedEffect(callState, chatModel.activeCallViewIsCollapsed.value) { + callState ?: return@LaunchedEffect + if (chatModel.activeCallViewIsCollapsed.value) { + if (callState.proximityLock?.isHeld == true) callState.proximityLock.release() + } else { + delay(1000) + if (callState.proximityLock?.isHeld == false) callState.proximityLock.acquire() } } - val scope = rememberCoroutineScope() Box(Modifier.fillMaxSize()) { WebRTCView(chatModel.callCommand) { apiMsg -> Log.d(TAG, "received from WebRTCView: $apiMsg") val call = chatModel.activeCall.value if (call != null) { + val callState = call.androidCallState as ActiveCallState Log.d(TAG, "has active call $call") val callRh = call.remoteHostId when (val r = apiMsg.resp) { is WCallResponse.Capabilities -> withBGApi { - val callType = CallType(call.localMedia, r.capabilities) + val callType = CallType(call.initialCallType, r.capabilities) chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType) - chatModel.activeCall.value = call.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) + updateActiveCall(call) { it.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + // Starting is delayed to make Android <= 11 working good with Bluetooth + callState.callAudioDeviceManager.start() + } else { + callState.callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true) + } + CallSoundsPlayer.startConnectingCallSound(scope) + activeCallWaitDeliveryReceipt(scope) } is WCallResponse.Offer -> withBGApi { - chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities) - chatModel.activeCall.value = call.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities) + chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.initialCallType, r.capabilities) + updateActiveCall(call) { it.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities) } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + // Starting is delayed to make Android <= 11 working good with Bluetooth + callState.callAudioDeviceManager.start() + } else { + callState.callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true) + } } is WCallResponse.Answer -> withBGApi { chatModel.controller.apiSendCallAnswer(callRh, call.contact, r.answer, r.iceCandidates) - chatModel.activeCall.value = call.copy(callState = CallState.Negotiated) + updateActiveCall(call) { it.copy(callState = CallState.Negotiated) } + CallSoundsPlayer.stop() } is WCallResponse.Ice -> withBGApi { chatModel.controller.apiSendCallExtraInfo(callRh, call.contact, r.iceCandidates) @@ -137,44 +173,60 @@ actual fun ActiveCallView() { try { val callStatus = json.decodeFromString<WebRTCCallStatus>("\"${r.state.connectionState}\"") if (callStatus == WebRTCCallStatus.Connected) { - chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectedAt = Clock.System.now()) - setCallSound(call.soundSpeaker, audioViaBluetooth) + updateActiveCall(call) { it.copy(callState = CallState.Connected, connectedAt = Clock.System.now()) } } withBGApi { chatModel.controller.apiCallStatus(callRh, call.contact, callStatus) } - } catch (e: Error) { + } catch (e: Throwable) { Log.d(TAG,"call status ${r.state.connectionState} not used") } is WCallResponse.Connected -> { - chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo) - scope.launch { - setCallSound(call.soundSpeaker, audioViaBluetooth) + updateActiveCall(call) { it.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo) } + } + is WCallResponse.PeerMedia -> { + updateActiveCall(call) { + val sources = it.peerMediaSources + when (r.source) { + CallMediaSource.Mic -> it.copy(peerMediaSources = sources.copy(mic = r.enabled)) + CallMediaSource.Camera -> it.copy(peerMediaSources = sources.copy(camera = r.enabled)) + CallMediaSource.ScreenAudio -> it.copy(peerMediaSources = sources.copy(screenAudio = r.enabled)) + CallMediaSource.ScreenVideo -> it.copy(peerMediaSources = sources.copy(screenVideo = r.enabled)) + } } } is WCallResponse.End -> { withBGApi { chatModel.callManager.endCall(call) } } is WCallResponse.Ended -> { - chatModel.activeCall.value = call.copy(callState = CallState.Ended) + updateActiveCall(call) { it.copy(callState = CallState.Ended) } withBGApi { chatModel.callManager.endCall(call) } - chatModel.showCallView.value = false } is WCallResponse.Ok -> when (val cmd = apiMsg.command) { is WCallCommand.Answer -> - chatModel.activeCall.value = call.copy(callState = CallState.Negotiated) + updateActiveCall(call) { it.copy(callState = CallState.Negotiated) } is WCallCommand.Media -> { - when (cmd.media) { - CallMediaType.Video -> chatModel.activeCall.value = call.copy(videoEnabled = cmd.enable) - CallMediaType.Audio -> chatModel.activeCall.value = call.copy(audioEnabled = cmd.enable) + updateActiveCall(call) { + val sources = it.localMediaSources + when (cmd.source) { + CallMediaSource.Mic -> { + val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager + am.isMicrophoneMute = !cmd.enable + it.copy(localMediaSources = sources.copy(mic = cmd.enable)) + } + CallMediaSource.Camera -> it.copy(localMediaSources = sources.copy(camera = cmd.enable)) + CallMediaSource.ScreenAudio -> it.copy(localMediaSources = sources.copy(screenAudio = cmd.enable)) + CallMediaSource.ScreenVideo -> it.copy(localMediaSources = sources.copy(screenVideo = cmd.enable)) + } } } is WCallCommand.Camera -> { - chatModel.activeCall.value = call.copy(localCamera = cmd.camera) - if (!call.audioEnabled) { - chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false)) + updateActiveCall(call) { it.copy(localCamera = cmd.camera) } + if (!call.localMediaSources.mic) { + chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = false)) } } - is WCallCommand.End -> - chatModel.showCallView.value = false + is WCallCommand.End -> { + withBGApi { chatModel.callManager.endCall(call) } + } else -> {} } is WCallResponse.Error -> { @@ -183,25 +235,33 @@ actual fun ActiveCallView() { } } } - val call = chatModel.activeCall.value - if (call != null) ActiveCallOverlay(call, chatModel, audioViaBluetooth) + val showOverlay = when { + call == null -> false + !platform.androidPictureInPictureAllowed() -> true + !chatModel.activeCallViewIsCollapsed.value -> true + else -> false + } + if (call != null && showOverlay && callState != null) { + ActiveCallOverlay(call, chatModel, callState.callAudioDeviceManager) + } + } + KeyChangeEffect(callState, call?.localMediaSources?.hasVideo) { + if (call != null && call.hasVideo && callState != null && callState.callAudioDeviceManager.currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) { + // enabling speaker on user action (peer action ignored) and not disabling it again + callState.callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true) + } } - val context = LocalContext.current DisposableEffect(Unit) { val activity = context as? Activity ?: return@DisposableEffect onDispose {} val prevVolumeControlStream = activity.volumeControlStream activity.volumeControlStream = AudioManager.STREAM_VOICE_CALL - // Lock orientation to portrait in order to have good experience with calls - activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT chatModel.activeCallViewIsVisible.value = true // After the first call, End command gets added to the list which prevents making another calls chatModel.callCommand.removeAll { it is WCallCommand.End } keepScreenOn(true) onDispose { activity.volumeControlStream = prevVolumeControlStream - // Unlock orientation - activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED chatModel.activeCallViewIsVisible.value = false chatModel.callCommand.clear() keepScreenOn(false) @@ -210,49 +270,46 @@ actual fun ActiveCallView() { } @Composable -private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, audioViaBluetooth: MutableState<Boolean>) { +private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, callAudioDeviceManager: CallAudioDeviceManagerInterface) { ActiveCallOverlayLayout( call = call, - speakerCanBeEnabled = !audioViaBluetooth.value, + devices = remember(callAudioDeviceManager) { callAudioDeviceManager.devices }.value, + currentDevice = remember(callAudioDeviceManager) { callAudioDeviceManager.currentDevice }, dismiss = { withBGApi { chatModel.callManager.endCall(call) } }, - toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = !call.audioEnabled)) }, - toggleVideo = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled)) }, + toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = !call.localMediaSources.mic)) }, + selectDevice = { callAudioDeviceManager.selectDevice(it.id) }, + toggleVideo = { + if (ContextCompat.checkSelfPermission(androidAppContext, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { + chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Camera, enable = !call.localMediaSources.camera)) + } else { + updateActiveCall(call) { it.copy(wantsToEnableCamera = true) } + } + }, toggleSound = { - var call = chatModel.activeCall.value - if (call != null) { - call = call.copy(soundSpeaker = !call.soundSpeaker) - chatModel.activeCall.value = call - setCallSound(call.soundSpeaker, audioViaBluetooth) + val enableSpeaker = callAudioDeviceManager.currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE + val preferredInternalDevice = callAudioDeviceManager.devices.value.firstOrNull { it.type == if (enableSpeaker) AudioDeviceInfo.TYPE_BUILTIN_SPEAKER else AudioDeviceInfo.TYPE_BUILTIN_EARPIECE } + if (preferredInternalDevice != null) { + callAudioDeviceManager.selectDevice(preferredInternalDevice.id) } }, flipCamera = { chatModel.callCommand.add(WCallCommand.Camera(call.localCamera.flipped)) } ) } -private fun setCallSound(speaker: Boolean, audioViaBluetooth: MutableState<Boolean>) { - val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager - Log.d(TAG, "setCallSound: set audio mode, speaker enabled: $speaker") - am.mode = AudioManager.MODE_IN_COMMUNICATION - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - val btDevice = am.availableCommunicationDevices.lastOrNull { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO } - val preferredSecondaryDevice = if (speaker) AudioDeviceInfo.TYPE_BUILTIN_SPEAKER else AudioDeviceInfo.TYPE_BUILTIN_EARPIECE - if (btDevice != null) { - am.setCommunicationDevice(btDevice) - } else if (am.communicationDevice?.type != preferredSecondaryDevice) { - am.availableCommunicationDevices.firstOrNull { it.type == preferredSecondaryDevice }?.let { - am.setCommunicationDevice(it) - } - } - } else { - if (audioViaBluetooth.value) { - am.isSpeakerphoneOn = false - am.startBluetoothSco() - } else { - am.stopBluetoothSco() - am.isSpeakerphoneOn = speaker - } - am.isBluetoothScoOn = am.isBluetoothScoAvailableOffCall && audioViaBluetooth.value - } +@Composable +fun ActiveCallOverlayDisabled(call: Call) { + ActiveCallOverlayLayout( + call = call, + devices = emptyList(), + currentDevice = remember { mutableStateOf(null) }, + enabled = false, + dismiss = {}, + toggleAudio = {}, + selectDevice = {}, + toggleVideo = {}, + toggleSound = {}, + flipCamera = {} + ) } private fun dropAudioManagerOverrides() { @@ -270,61 +327,86 @@ private fun dropAudioManagerOverrides() { @Composable private fun ActiveCallOverlayLayout( call: Call, - speakerCanBeEnabled: Boolean, + devices: List<AudioDeviceInfo>, + currentDevice: State<AudioDeviceInfo?>, + enabled: Boolean = true, dismiss: () -> Unit, toggleAudio: () -> Unit, + selectDevice: (AudioDeviceInfo) -> Unit, toggleVideo: () -> Unit, toggleSound: () -> Unit, flipCamera: () -> Unit ) { - Column(Modifier.padding(DEFAULT_PADDING)) { - when (call.peerMedia ?: call.localMedia) { - CallMediaType.Video -> { - CallInfoView(call, alignment = Alignment.Start) - Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) { - DisabledBackgroundCallsButton() + Column { + CallAppBar( + title = { + if (call.hasVideo) { + Text(call.contact.chatViewName, Modifier.offset(x = (-4).dp).padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1) } - Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { - ToggleAudioButton(call, toggleAudio) - Spacer(Modifier.size(40.dp)) - IconButton(onClick = dismiss) { - Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp)) - } - if (call.videoEnabled) { - ControlButton(call, painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, flipCamera) - ControlButton(call, painterResource(MR.images.ic_videocam_filled), MR.strings.icon_descr_video_off, toggleVideo) - } else { - Spacer(Modifier.size(48.dp)) - ControlButton(call, painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, toggleVideo) - } - } - } - CallMediaType.Audio -> { - Spacer(Modifier.fillMaxHeight().weight(1f)) - Column( - Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + }, + onBack = { chatModel.activeCallViewIsCollapsed.value = true } + ) + Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { + @Composable + fun SelectSoundDevice(size: Dp) { + if (devices.size == 2 && + devices.all { it.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE || it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } || + currentDevice.value == null || + devices.none { it.id == currentDevice.value?.id } ) { - ProfileImage(size = 192.dp, image = call.contact.profile.image) - CallInfoView(call, alignment = Alignment.CenterHorizontally) - } - Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) { - DisabledBackgroundCallsButton() - } - Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.CenterStart) { - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { - IconButton(onClick = dismiss) { - Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp)) + val isSpeaker = currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER + ToggleSoundButton(enabled, isSpeaker, !call.peerMediaSources.mic, toggleSound, size = size) + } else { + ExposedDropDownSettingWithIcon( + devices.map { Triple(it, if (call.peerMediaSources.mic) it.icon else MR.images.ic_volume_off, if (it.name != null) generalGetString(it.name!!) else it.productName.toString()) }, + currentDevice, + fontSize = 18.sp, + boxSize = size, + listIconSize = 30.dp, + iconColor = Color(0xFFFFFFD8), + background = controlButtonsBackground(), + minWidth = 300.dp, + onSelected = { + if (it != null) { + selectDevice(it) + } } + ) + } + } + + when (call.hasVideo) { + true -> VideoCallInfoView(call) + false -> { + Spacer(Modifier.fillMaxHeight().weight(1f)) + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + ProfileImage(size = 192.dp, image = call.contact.profile.image) + AudioCallInfoView(call) } - Box(Modifier.padding(start = 32.dp)) { - ToggleAudioButton(call, toggleAudio) - } - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { - Box(Modifier.padding(end = 32.dp)) { - ToggleSoundButton(call, speakerCanBeEnabled, toggleSound) - } + } + } + Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) { + DisabledBackgroundCallsButton() + } + + BoxWithConstraints(Modifier.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) + Row(horizontalArrangement = Arrangement.spacedBy(padding), verticalAlignment = Alignment.CenterVertically) { + ToggleMicButton(call, enabled, toggleAudio, size = size) + SelectSoundDevice(size = size) + ControlButton(painterResource(MR.images.ic_call_end_filled), MR.strings.icon_descr_hang_up, enabled = enabled, dismiss, background = Color.Red, size = size, iconPaddingPercent = 0.166f) + if (call.localMediaSources.camera) { + ControlButton(painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, enabled, flipCamera, size = size) + ControlButton(painterResource(MR.images.ic_videocam_filled), MR.strings.icon_descr_video_off, enabled, toggleVideo, size = size) + } else { + Spacer(Modifier.size(size)) + ControlButton(painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, enabled, toggleVideo, size = size) } } } @@ -333,39 +415,56 @@ private fun ActiveCallOverlayLayout( } @Composable -private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, action: () -> Unit, enabled: Boolean = true) { - if (call.hasMedia) { - IconButton(onClick = action, enabled = enabled) { - Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else MaterialTheme.colors.secondary, modifier = Modifier.size(40.dp)) - } - } else { - Spacer(Modifier.size(40.dp)) +private fun ControlButton(icon: Painter, iconText: StringResource, enabled: Boolean = true, action: () -> Unit, background: Color = controlButtonsBackground(), size: Dp, iconPaddingPercent: Float = 0.2f) { + ControlButtonWrap(enabled, action, background, size) { + Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else MaterialTheme.colors.secondary, modifier = Modifier.padding(size * iconPaddingPercent).fillMaxSize()) } } @Composable -private fun ToggleAudioButton(call: Call, toggleAudio: () -> Unit) { - if (call.audioEnabled) { - ControlButton(call, painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, toggleAudio) - } else { - ControlButton(call, painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, toggleAudio) +private fun ControlButtonWrap(enabled: Boolean = true, action: () -> Unit, background: Color = controlButtonsBackground(), size: Dp, content: @Composable () -> Unit) { + val ripple = remember { ripple(bounded = false, radius = size / 2, color = background.lighter(0.1f)) } + Box( + Modifier + .background(background, CircleShape) + .size(size) + .clickable( + onClick = action, + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = ripple, + enabled = enabled + ), + contentAlignment = Alignment.Center + ) { + content() } } @Composable -private fun ToggleSoundButton(call: Call, enabled: Boolean, toggleSound: () -> Unit) { - if (call.soundSpeaker) { - ControlButton(call, painterResource(MR.images.ic_volume_up), MR.strings.icon_descr_speaker_off, toggleSound, enabled) +private fun ToggleMicButton(call: Call, enabled: Boolean = true, toggleAudio: () -> Unit, size: Dp) { + if (call.localMediaSources.mic) { + ControlButton(painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, enabled, toggleAudio, size = size) } else { - ControlButton(call, painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, toggleSound, enabled) + ControlButton(painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, enabled, toggleAudio, size = size) } } @Composable -fun CallInfoView(call: Call, alignment: Alignment.Horizontal) { - @Composable fun InfoText(text: String, style: TextStyle = MaterialTheme.typography.body2) = - Text(text, color = Color(0xFFFFFFD8), style = style) - Column(horizontalAlignment = alignment) { +private fun ToggleSoundButton(enabled: Boolean, speaker: Boolean, muted: Boolean, toggleSound: () -> Unit, size: Dp) { + when { + muted -> ControlButton(painterResource(MR.images.ic_volume_off), MR.strings.icon_descr_sound_muted, enabled, toggleSound, size = size) + speaker -> ControlButton(painterResource(MR.images.ic_volume_up), MR.strings.icon_descr_speaker_off, enabled, toggleSound, size = size) + else -> ControlButton(painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, enabled, toggleSound, size = size) + } +} + +@Composable +fun controlButtonsBackground(): Color = if (chatModel.activeCall.value?.peerMediaSources?.hasVideo == true) Color.Black.copy(0.2f) else Color.White.copy(0.2f) + +@Composable +fun AudioCallInfoView(call: Call) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { InfoText(call.contact.chatViewName, style = MaterialTheme.typography.h2) InfoText(call.callState.text) @@ -375,6 +474,21 @@ fun CallInfoView(call: Call, alignment: Alignment.Horizontal) { } } +@Composable +fun VideoCallInfoView(call: Call) { + Column(horizontalAlignment = Alignment.Start) { + InfoText(call.callState.text) + + val connInfo = call.connectionInfo + val connInfoText = if (connInfo == null) "" else " (${connInfo.text})" + InfoText(call.encryptionStatus + connInfoText) + } +} + +@Composable +fun InfoText(text: String, modifier: Modifier = Modifier, style: TextStyle = MaterialTheme.typography.body2) = + Text(text, modifier, color = Color(0xFFFFFFD8), style = style) + @Composable private fun DisabledBackgroundCallsButton() { var show by remember { mutableStateOf(!platform.androidIsBackgroundCallAllowed()) } @@ -383,7 +497,7 @@ private fun DisabledBackgroundCallsButton() { Modifier .padding(bottom = 24.dp) .clickable { - withBGApi { + withLongRunningApi { show = !platform.androidAskToAllowBackgroundCalls() } } @@ -393,7 +507,7 @@ private fun DisabledBackgroundCallsButton() { ) { Text(stringResource(MR.strings.system_restricted_background_in_call_title), color = WarningOrange) Spacer(Modifier.width(8.dp)) - IconButton(onClick = { show = false }, Modifier.size(24.dp)) { + IconButton(onClick = { show = false }, Modifier.size(22.dp)) { Icon(painterResource(MR.images.ic_close), null, tint = WarningOrange) } } @@ -451,34 +565,139 @@ private fun DisabledBackgroundCallsButton() { //} @Composable -fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIMessage) -> Unit) { - val scope = rememberCoroutineScope() - val webView = remember { mutableStateOf<WebView?>(null) } +fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Unit) { + val audioPermission = rememberPermissionState(Manifest.permission.RECORD_AUDIO) + val cameraPermission = rememberPermissionState(Manifest.permission.CAMERA) val permissionsState = rememberMultiplePermissionsState( - permissions = listOf( - Manifest.permission.CAMERA, - Manifest.permission.RECORD_AUDIO, - Manifest.permission.MODIFY_AUDIO_SETTINGS, - Manifest.permission.INTERNET - ) + permissions = if (hasVideo) { + listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) + } else { + listOf(Manifest.permission.RECORD_AUDIO) + } ) + val context = LocalContext.current + val buttonEnabled = remember { mutableStateOf(true) } + LaunchedEffect(Unit) { + if (!pipActive) { + permissionsState.launchMultiplePermissionRequestWithFallback(buttonEnabled, context::showAllowPermissionInSettingsAlert) + } + } + + if (pipActive) { + Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceEvenly) { + if (audioPermission.status is PermissionStatus.Denied) { + Icon( + painterResource(MR.images.ic_call_500), + stringResource(MR.strings.permissions_record_audio), + Modifier.size(22.dp), + tint = Color(0xFFFFFFD8) + ) + } + if (hasVideo && cameraPermission.status is PermissionStatus.Denied) { + Icon( + painterResource(MR.images.ic_videocam), + stringResource(MR.strings.permissions_camera), + Modifier.size(22.dp), + tint = Color(0xFFFFFFD8) + ) + } + } + } else { + ModalView(background = Color.Black, showAppBar = false, close = {}) { + Column { + Spacer(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier)) + AppBarTitle(stringResource(MR.strings.permissions_required)) + Spacer(Modifier.weight(1f)) + val onClick = { + if (permissionsState.shouldShowRationale) { + context.showAllowPermissionInSettingsAlert() + } else { + permissionsState.launchMultiplePermissionRequestWithFallback(buttonEnabled, context::showAllowPermissionInSettingsAlert) + } + } + Text(stringResource(MR.strings.permissions_grant), Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), textAlign = TextAlign.Center, color = Color(0xFFFFFFD8)) + SectionSpacer() + SectionView { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + val text = if (hasVideo && audioPermission.status is PermissionStatus.Denied && cameraPermission.status is PermissionStatus.Denied) { + stringResource(MR.strings.permissions_camera_and_record_audio) + } else if (audioPermission.status is PermissionStatus.Denied) { + stringResource(MR.strings.permissions_record_audio) + } else if (hasVideo && cameraPermission.status is PermissionStatus.Denied) { + stringResource(MR.strings.permissions_camera) + } else null + if (text != null) { + GrantPermissionButton(text, buttonEnabled.value, onClick) + } + } + } + + Spacer(Modifier.weight(1f)) + Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING), contentAlignment = Alignment.Center) { + SimpleButtonFrame(cancel, Modifier.height(60.dp)) { + Text(stringResource(MR.strings.call_service_notification_end_call), fontSize = 20.sp, color = Color(0xFFFFFFD8)) + } + } + } + } + } +} + +@Composable +private fun GrantPermissionButton(text: String, enabled: Boolean, onClick: () -> Unit) { + Row( + Modifier + .clickable(enabled = enabled, onClick = onClick) + .heightIn(min = 30.dp) + .background(WarningOrange.copy(0.3f), RoundedCornerShape(50)), + verticalAlignment = Alignment.CenterVertically + ) { + Text(text, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), fontSize = 20.sp, color = WarningOrange) + } +} + +/** + * The idea of this function is to ask system to show permission dialog and to see if it's really doing it. + * Otherwise, show alert with a button that opens settings for manual permission granting + * */ +private fun MultiplePermissionsState.launchMultiplePermissionRequestWithFallback(buttonEnabled: MutableState<Boolean>, fallback: () -> Unit) { + buttonEnabled.value = false + val lifecycleOwner = ProcessLifecycleOwner.get().lifecycle + var useFallback = true + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_PAUSE) { + useFallback = false + buttonEnabled.value = true + } + } + lifecycleOwner.addObserver(observer) + withBGApi { + delay(2000) + if (useFallback && chatModel.activeCall.value != null) { + fallback() + } + buttonEnabled.value = true + }.invokeOnCompletion { + // Main thread only + withApi { + lifecycleOwner.removeObserver(observer) + } + } + launchMultiplePermissionRequest() +} + +@Composable +fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIMessage) -> Unit) { + val webView = remember { mutableStateOf<WebView?>(null) } fun processCommand(wv: WebView, cmd: WCallCommand) { val apiCall = WVAPICall(command = cmd) wv.evaluateJavascript("processCommand(${json.encodeToString(apiCall)})", null) } - val lifecycleOwner = LocalLifecycleOwner.current - DisposableEffect(lifecycleOwner) { - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_RESUME || event == Lifecycle.Event.ON_START) { - permissionsState.launchMultiplePermissionRequest() - } - } - lifecycleOwner.lifecycle.addObserver(observer) + DisposableEffect(Unit) { onDispose { - val wv = webView.value - if (wv != null) processCommand(wv, WCallCommand.End) - lifecycleOwner.lifecycle.removeObserver(observer) - webView.value?.destroy() + // val wv = webView.value + // if (wv != null) processCommand(wv, WCallCommand.End) + // webView.value?.destroy() webView.value = null } } @@ -501,11 +720,12 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM .addPathHandler("/assets/www/", WebViewAssetLoader.AssetsPathHandler(LocalContext.current)) .build() - if (permissionsState.allPermissionsGranted) { - Box(Modifier.fillMaxSize()) { - AndroidView( - factory = { AndroidViewContext -> - WebView(AndroidViewContext).apply { + Box(Modifier.fillMaxSize()) { + AndroidView( + factory = { + try { + (staticWebView ?: WebView(androidAppContext)).apply { + reapplyLocale() layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, @@ -524,17 +744,30 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM this.clearHistory() this.clearCache(true) this.addJavascriptInterface(WebRTCInterface(onResponse), "WebRTCInterface") + this.setBackgroundColor(android.graphics.Color.BLACK) val webViewSettings = this.settings webViewSettings.allowFileAccess = true webViewSettings.allowContentAccess = true webViewSettings.javaScriptEnabled = true webViewSettings.mediaPlaybackRequiresUserGesture = false webViewSettings.cacheMode = WebSettings.LOAD_NO_CACHE - this.loadUrl("file:android_asset/www/android/call.html") + if (staticWebView == null) { + this.loadUrl("file:android_asset/www/android/call.html") + } else { + webView.value = this + } } + } catch (e: Exception) { + Log.e(TAG, "Error initializing WebView: ${e.stackTraceToString()}") + if (e.stackTraceToString().contains("/lib64")) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), generalGetString(MR.strings.error_initializing_web_view_wrong_arch).format(e.stackTraceToString())) + } else { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), generalGetString(MR.strings.error_initializing_web_view).format(e.stackTraceToString())) + } + return@AndroidView View(androidAppContext) } - ) { /* WebView */ } - } + } + ) { /* WebView */ } } } @@ -554,6 +787,25 @@ class WebRTCInterface(private val onResponse: (WVAPIMessage) -> Unit) { } } +private fun updateActiveCall(initial: Call, transform: (Call) -> Call) { + val activeCall = chatModel.activeCall.value + if (activeCall != null && activeCall.contact.apiId == initial.contact.apiId) { + chatModel.activeCall.value = transform(activeCall) + } else { + Log.d(TAG, "withActiveCall: ignoring, not in call with the contact ${activeCall?.contact?.id}") + } +} + +/* +* Creating WebView automatically drops user's custom app locale to default system locale. +* Preventing it by re-applying custom locale +* https://issuetracker.google.com/issues/109833940 +* */ +private fun reapplyLocale() { + mainActivity.get()?.applyAppLocale(appPrefs.appLanguage) + callActivity.get()?.applyAppLocale(appPrefs.appLanguage) +} + private class LocalContentWebViewClient(val webView: MutableState<WebView?>, private val assetLoader: WebViewAssetLoader) : WebViewClientCompat() { override fun shouldInterceptRequest( view: WebView, @@ -566,6 +818,7 @@ private class LocalContentWebViewClient(val webView: MutableState<WebView?>, pri super.onPageFinished(view, url) view.evaluateJavascript("sendMessageToNative = (msg) => WebRTCInterface.postMessage(JSON.stringify(msg))", null) webView.value = view + staticWebView = view Log.d(TAG, "WebRTCView: webview ready") // for debugging // view.evaluateJavascript("sendMessageToNative = ({resp}) => WebRTCInterface.postMessage(JSON.stringify({command: resp}))", null) @@ -579,21 +832,26 @@ fun PreviewActiveCallOverlayVideo() { ActiveCallOverlayLayout( call = Call( remoteHostId = null, + userProfile = Profile.sampleData, contact = Contact.sampleData, callState = CallState.Negotiated, - localMedia = CallMediaType.Video, - peerMedia = CallMediaType.Video, + initialCallType = CallMediaType.Video, + peerMediaSources = CallMediaSources(), + callUUID = "", connectionInfo = ConnectionInfo( RTCIceCandidate(RTCIceCandidateType.Host, "tcp"), RTCIceCandidate(RTCIceCandidateType.Host, "tcp") - ) + ), + androidCallState = {} ), - speakerCanBeEnabled = true, + devices = emptyList(), + currentDevice = remember { mutableStateOf(null) }, dismiss = {}, toggleAudio = {}, + selectDevice = {}, toggleVideo = {}, toggleSound = {}, - flipCamera = {} + flipCamera = {}, ) } } @@ -605,18 +863,23 @@ fun PreviewActiveCallOverlayAudio() { ActiveCallOverlayLayout( call = Call( remoteHostId = null, + userProfile = Profile.sampleData, contact = Contact.sampleData, callState = CallState.Negotiated, - localMedia = CallMediaType.Audio, - peerMedia = CallMediaType.Audio, + initialCallType = CallMediaType.Audio, + peerMediaSources = CallMediaSources(), + callUUID = "", connectionInfo = ConnectionInfo( RTCIceCandidate(RTCIceCandidateType.Host, "udp"), RTCIceCandidate(RTCIceCandidateType.Host, "udp") - ) + ), + androidCallState = {} ), - speakerCanBeEnabled = true, + devices = emptyList(), + currentDevice = remember { mutableStateOf(null) }, dismiss = {}, toggleAudio = {}, + selectDevice = {}, toggleVideo = {}, toggleSound = {}, flipCamera = {} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.android.kt new file mode 100644 index 0000000000..b24150ed24 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.android.kt @@ -0,0 +1,57 @@ +package chat.simplex.common.views.chat.item + +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.* +import chat.simplex.common.model.CryptoFile +import chat.simplex.common.platform.* +import chat.simplex.common.views.helpers.DefaultDropdownMenu +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import java.net.URI + +@Composable +actual fun SaveOrOpenFileMenu( + showMenu: MutableState<Boolean>, + encrypted: Boolean, + ext: String?, + encryptedUri: URI, + fileSource: CryptoFile, + saveFile: () -> Unit +) { + val defaultApp = remember(encryptedUri.toString()) { if (ext != null) queryDefaultAppForExtension(ext, encryptedUri) else null } + DefaultDropdownMenu(showMenu) { + if (defaultApp != null) { + if (!defaultApp.isSystemChooser) { + ItemAction( + stringResource(MR.strings.open_with_app).format(defaultApp.name), + defaultApp.icon, + textColor = MaterialTheme.colors.primary, + onClick = { + openOrShareFile("", fileSource, justOpen = true, useChooser = false) + showMenu.value = false + } + ) + } else { + ItemAction( + stringResource(MR.strings.open_with_app).format("…"), + painterResource(MR.images.ic_open_in_new), + color = MaterialTheme.colors.primary, + onClick = { + openOrShareFile("", fileSource, justOpen = true, useChooser = false) + showMenu.value = false + } + ) + } + } + ItemAction( + stringResource(MR.strings.save_verb), + painterResource(if (encrypted) MR.images.ic_lock_open_right else MR.images.ic_download), + color = MaterialTheme.colors.primary, + onClick = { + saveFile() + showMenu.value = false + } + ) + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt index 28c00ec018..ae5b8043ed 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt @@ -2,20 +2,20 @@ package chat.simplex.common.views.chat.item import android.os.Build.VERSION.SDK_INT import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext -import chat.simplex.common.helpers.toUri import chat.simplex.common.model.CIFile import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.views.helpers.ModalManager import coil.ImageLoader import coil.compose.rememberAsyncImagePainter import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder import coil.request.ImageRequest -import java.net.URI @Composable actual fun SimpleAndAnimatedImageView( @@ -23,6 +23,7 @@ actual fun SimpleAndAnimatedImageView( imageBitmap: ImageBitmap, file: CIFile?, imageProvider: () -> ImageGalleryProvider, + smallView: Boolean, ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit ) { val context = LocalContext.current @@ -43,6 +44,7 @@ actual fun SimpleAndAnimatedImageView( } private val imageLoader = ImageLoader.Builder(androidAppContext) + .networkObserverEnabled(false) .components { if (SDK_INT >= 28) { add(ImageDecoderDecoder.Factory()) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.android.kt index f2f3e27766..a8c084bbad 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.android.kt @@ -44,3 +44,10 @@ actual fun LocalWindowWidth(): Dp { (rect.width() / density).dp } } + +@Composable +actual fun LocalWindowHeight(): Dp { + val view = LocalView.current + val density = LocalDensity.current + return with(density) { view.height.toDp() } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt index 3aa4a92619..9e8eb8ee8f 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt @@ -13,9 +13,9 @@ import chat.simplex.common.model.ChatItem import chat.simplex.common.model.MsgContent import chat.simplex.common.platform.FileChooserLauncher import chat.simplex.common.platform.saveImage -import chat.simplex.common.views.helpers.SharedContent -import chat.simplex.common.views.helpers.withApi +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import com.google.accompanist.permissions.PermissionStatus import com.google.accompanist.permissions.rememberPermissionState import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -31,13 +31,13 @@ actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserL ItemAction(stringResource(MR.strings.save_verb), painterResource(if (cItem.file?.fileSource?.cryptoArgs == null) MR.images.ic_download else MR.images.ic_lock_open_right), onClick = { when (cItem.content.msgContent) { is MsgContent.MCImage -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.hasPermission) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.status == PermissionStatus.Granted) { saveImage(cItem.file) } else { writePermissionState.launchPermissionRequest() } } - is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") } + is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withLongRunningApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") } else -> {} } showMenu.value = false diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.android.kt index d4efdc3e59..dad8872012 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.android.kt @@ -3,7 +3,7 @@ package chat.simplex.common.views.chat.item import android.os.Build import android.view.View import androidx.compose.foundation.Image -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.BitmapPainter @@ -11,8 +11,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.isVisible -import chat.simplex.common.helpers.toUri import chat.simplex.common.platform.VideoPlayer +import chat.simplex.common.platform.androidAppContext import chat.simplex.res.MR import coil.ImageLoader import coil.compose.rememberAsyncImagePainter @@ -23,21 +23,11 @@ import coil.size.Size import com.google.android.exoplayer2.ui.AspectRatioFrameLayout import com.google.android.exoplayer2.ui.StyledPlayerView import dev.icerock.moko.resources.compose.stringResource -import java.net.URI @Composable actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) { - // I'm making a new instance of imageLoader here because if I use one instance in multiple places + // I'm using a new private instance of imageLoader here because if I use one instance in multiple places // after end of composition here a GIF from the first instance will be paused automatically which isn't what I want - val imageLoader = ImageLoader.Builder(LocalContext.current) - .components { - if (Build.VERSION.SDK_INT >= 28) { - add(ImageDecoderDecoder.Factory()) - } else { - add(GifDecoder.Factory()) - } - } - .build() Image( rememberAsyncImagePainter( ImageRequest.Builder(LocalContext.current).data(data = data).size(Size.ORIGINAL).build(), @@ -73,3 +63,14 @@ actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: ( modifier ) } + +private val imageLoader = ImageLoader.Builder(androidAppContext) + .networkObserverEnabled(false) + .components { + if (Build.VERSION.SDK_INT >= 28) { + add(ImageDecoderDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + } + .build() diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt index 30f5b81387..9a3d9e5e4f 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt @@ -16,11 +16,13 @@ actual fun ChatListNavLinkLayout( click: () -> Unit, dropdownMenuItems: (@Composable () -> Unit)?, showMenu: MutableState<Boolean>, - stopped: Boolean, - selectedChat: State<Boolean> + disabled: Boolean, + selectedChat: State<Boolean>, + nextChatSelected: State<Boolean>, ) { var modifier = Modifier.fillMaxWidth() - if (!stopped) modifier = modifier + + if (!disabled) modifier = modifier .combinedClickable(onClick = click, onLongClick = { showMenu.value = true }) .onRightClick { showMenu.value = true } Box(modifier) { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt index cb74664a48..8c3b161a5c 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt @@ -1,8 +1,118 @@ package chat.simplex.common.views.chatlist +import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.* import androidx.compose.runtime.* -import chat.simplex.common.views.helpers.* -import kotlinx.coroutines.flow.MutableStateFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.* +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.* +import chat.simplex.common.ANDROID_CALL_TOP_PADDING +import chat.simplex.common.model.durationText +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.call.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import kotlinx.coroutines.delay +import kotlinx.datetime.Clock + +private val CALL_INTERACTIVE_AREA_HEIGHT = 74.dp +private val CALL_TOP_OFFSET = (-10).dp +private val CALL_TOP_GREEN_LINE_HEIGHT = ANDROID_CALL_TOP_PADDING - CALL_TOP_OFFSET +private val CALL_BOTTOM_ICON_OFFSET = (-15).dp +private val CALL_BOTTOM_ICON_HEIGHT = CALL_INTERACTIVE_AREA_HEIGHT + CALL_BOTTOM_ICON_OFFSET @Composable -actual fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>) {} +actual fun TagsRow(content: @Composable() (() -> Unit)) { + Row( + modifier = Modifier + .padding(horizontal = 14.dp) + .horizontalScroll(rememberScrollState()), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp) + ) { + content() + } +} + +@Composable +actual fun ActiveCallInteractiveArea(call: Call) { + val onClick = { platform.androidStartCallActivity(false) } + val statusBar = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT + statusBar)) { + val source = remember { MutableInteractionSource() } + val ripple = remember { ripple(bounded = true, 3000.dp) } + Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT + statusBar).clickable(onClick = onClick, indication = ripple, interactionSource = source)) { + GreenLine(statusBar, call) + } + Box( + Modifier + .offset(y = CALL_BOTTOM_ICON_OFFSET) + .size(CALL_BOTTOM_ICON_HEIGHT) + .background(SimplexGreen, CircleShape) + .clip(CircleShape) + .clickable(onClick = onClick, indication = ripple, interactionSource = source) + .align(Alignment.BottomCenter), + contentAlignment = Alignment.Center + ) { + if (call.hasVideo) { + Icon(painterResource(MR.images.ic_videocam_filled), null, Modifier.size(27.dp).offset(x = 2.5.dp, y = 2.dp), tint = Color.White) + } else { + Icon(painterResource(MR.images.ic_call_filled), null, Modifier.size(27.dp).offset(x = -0.5.dp, y = 2.dp), tint = Color.White) + } + } + } +} + +@Composable +private fun GreenLine(statusBarHeight: Dp, call: Call) { + Row( + Modifier + .fillMaxSize() + .background(SimplexGreen) + .padding(top = -CALL_TOP_OFFSET + statusBarHeight) + .padding(horizontal = DEFAULT_PADDING), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + ContactName(call.contact.displayName) + Spacer(Modifier.weight(1f)) + CallDuration(call) + } + DisposableEffect(Unit) { + platform.androidSetStatusAndNavigationBarAppearance(false, CurrentColors.value.colors.isLight) + onDispose { + platform.androidSetStatusAndNavigationBarAppearance(CurrentColors.value.colors.isLight, CurrentColors.value.colors.isLight) + } + } +} + +@Composable +private fun ContactName(name: String) { + Text(name, Modifier.width(windowWidth() * 0.35f), color = Color.White, maxLines = 1, overflow = TextOverflow.Ellipsis) +} + +@Composable +private fun CallDuration(call: Call) { + val connectedAt = call.connectedAt + if (connectedAt != null) { + val time = remember { mutableStateOf(durationText(0)) } + LaunchedEffect(Unit) { + while (true) { + time.value = durationText((Clock.System.now() - connectedAt).inWholeSeconds.toInt()) + delay(250) + } + } + val text = time.value + val sp40Or50 = with(LocalDensity.current) { if (text.length >= 6) 60.sp.toDp() else 42.sp.toDp() } + val offset = with(LocalDensity.current) { 7.sp.toDp() } + Text(text, Modifier.offset(x = offset).widthIn(min = sp40Or50), color = Color.White) + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/TagListView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/TagListView.android.kt new file mode 100644 index 0000000000..ab6d375d75 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/TagListView.android.kt @@ -0,0 +1,81 @@ +package chat.simplex.common.views.chatlist + +import SectionItemView +import android.view.ViewGroup +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.emoji2.emojipicker.EmojiPickerView +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF +import chat.simplex.common.views.chat.topPaddingToContent +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource + +@Composable +actual fun ChatTagInput(name: MutableState<String>, showError: State<Boolean>, emoji: MutableState<String?>) { + SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) { + Box(Modifier + .clip(shape = CircleShape) + .clickable { + ModalManager.start.showModalCloseable { close -> + EmojiPicker(close = { + close() + emoji.value = it + }) + } + } + .padding(4.dp) + ) { + val emojiValue = emoji.value + if (emojiValue != null) { + Text(emojiValue) + } else { + Icon( + painter = painterResource(MR.images.ic_add_reaction), + contentDescription = null, + tint = MaterialTheme.colors.secondary + ) + } + } + Spacer(Modifier.width(8.dp)) + TagListNameTextField(name, showError = showError) + } +} + +@Composable +private fun EmojiPicker(close: (String?) -> Unit) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val topPaddingToContent = topPaddingToContent(false) + + Column ( + modifier = Modifier.fillMaxSize().navigationBarsPadding().padding( + start = DEFAULT_PADDING_HALF, + end = DEFAULT_PADDING_HALF, + top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else topPaddingToContent, + bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp + ), + ) { + AndroidView( + factory = { context -> + EmojiPickerView(context).apply { + emojiGridColumns = 10 + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + setOnEmojiPickedListener { pickedEmoji -> + close(pickedEmoji.emoji) + } + } + } + ) + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt new file mode 100644 index 0000000000..54e3061d25 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt @@ -0,0 +1,228 @@ +package chat.simplex.common.views.chatlist + +import SectionItemView +import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.DrawerDefaults.ScrimOpacity +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.graphics.* +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.* +import chat.simplex.common.model.User +import chat.simplex.common.model.UserInfo +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow + +private val USER_PICKER_IMAGE_SIZE = 44.dp +private val USER_PICKER_ROW_PADDING = 16.dp + +@Composable +actual fun UserPickerUsersSection( + users: List<UserInfo>, + iconColor: Color, + stopped: Boolean, + onUserClicked: (user: User) -> Unit, +) { + val scrollState = rememberScrollState() + val screenWidthDp = windowWidth() + + if (users.isNotEmpty()) { + SectionItemView( + padding = PaddingValues(), + disabled = stopped + ) { + Box { + Row( + modifier = Modifier.horizontalScroll(scrollState), + ) { + Spacer(Modifier.width(DEFAULT_PADDING)) + Row(horizontalArrangement = Arrangement.spacedBy(USER_PICKER_ROW_PADDING)) { + users.forEach { u -> + UserPickerUserBox(u, stopped, modifier = Modifier.userBoxWidth(u.user, users.size, screenWidthDp)) { + onUserClicked(it) + withBGApi { + delay(500) + scrollState.scrollTo(0) + } + } + } + } + Spacer(Modifier.width(DEFAULT_PADDING)) + } + } + } + } +} +@Composable +fun UserPickerUserBox( + userInfo: UserInfo, + stopped: Boolean, + modifier: Modifier = Modifier, + onClick: (user: User) -> Unit, +) { + Row( + modifier = modifier + .userPickerBoxModifier() + .clickable ( + onClick = { onClick(userInfo.user) }, + enabled = !stopped + ) + .background(MaterialTheme.colors.background) + .padding(USER_PICKER_ROW_PADDING), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(USER_PICKER_ROW_PADDING) + ) { + Box { + ProfileImageForActiveCall(size = USER_PICKER_IMAGE_SIZE, image = userInfo.user.profile.image, color = MaterialTheme.colors.secondaryVariant) + + if (userInfo.unreadCount > 0 && !userInfo.user.activeUser) { + unreadBadge(userInfo.unreadCount, userInfo.user.showNtfs, false) + } + } + val user = userInfo.user + Text( + user.displayName, + fontWeight = if (user.activeUser) FontWeight.Bold else FontWeight.Normal, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +private fun Modifier.userPickerBoxModifier(): Modifier { + val percent = remember { appPreferences.profileImageCornerRadius.state } + val r = kotlin.math.max(0f, percent.value) + + val cornerSize = when { + r >= 50 -> 50 + r <= 0 -> 0 + else -> r.toInt() + } + + val shape = RoundedCornerShape(CornerSize(cornerSize)) + return this.clip(shape).border(1.dp, MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 1 - userPickerAlpha() - 0.02f), shape) +} + + +private fun calculateFraction(pos: Float) = + (pos / 1f).coerceIn(0f, 1f) + +@Composable +actual fun PlatformUserPicker(modifier: Modifier, pickerState: MutableStateFlow<AnimatedViewState>, content: @Composable () -> Unit) { + val pickerIsVisible = pickerState.collectAsState().value.isVisible() + val dismissState = rememberDismissState(initialValue = if (pickerIsVisible) DismissValue.Default else DismissValue.DismissedToEnd) { + if (it == DismissValue.DismissedToEnd && pickerState.value.isVisible()) { + pickerState.value = AnimatedViewState.HIDING + } + true + } + val height = remember { mutableIntStateOf(0) } + val heightValue = height.intValue + val clickableModifier = if (pickerIsVisible) { + Modifier.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { pickerState.value = AnimatedViewState.HIDING }) + } else { + Modifier + } + Box { + Box( + Modifier + .fillMaxSize() + .then(clickableModifier) + .drawBehind { + val pos = calculatePosition(dismissState) + val colors = CurrentColors.value.colors + val resultingColor = if (colors.isLight) colors.onSurface.copy(alpha = ScrimOpacity) else Color.Black.copy(0.64f) + drawRect( + if (pos != 0f) resultingColor else Color.Transparent, + alpha = calculateFraction(pos = pos) + ) + } + .graphicsLayer { + if (heightValue == 0) { + alpha = 0f + } + translationY = dismissState.offset.value + }, + contentAlignment = Alignment.BottomCenter + ) { + Box( + Modifier.onSizeChanged { height.intValue = it.height } + ) { + KeyChangeEffect(pickerIsVisible) { + if (pickerState.value.isVisible()) { + try { + dismissState.animateTo(DismissValue.Default, userPickerAnimSpec()) + } catch (e: CancellationException) { + Log.e(TAG, "Cancelled animateTo: ${e.stackTraceToString()}") + pickerState.value = AnimatedViewState.GONE + } + } else { + try { + dismissState.animateTo(DismissValue.DismissedToEnd, userPickerAnimSpec()) + } catch (e: CancellationException) { + Log.e(TAG, "Cancelled animateTo2: ${e.stackTraceToString()}") + pickerState.value = AnimatedViewState.VISIBLE + } + } + } + val draggableModifier = if (height.intValue != 0) + Modifier.draggableBottomDrawerModifier( + state = dismissState, + swipeDistance = height.intValue.toFloat(), + ) + else Modifier + Box(draggableModifier.then(modifier).navigationBarsPadding()) { + content() + } + } + } + NavigationBarBackground( + modifier = Modifier.graphicsLayer { alpha = if (calculatePosition(dismissState) > 0.1f) 1f else 0f }, + color = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha()) + ) + } +} + +private fun calculatePosition(dismissState: DismissState): Float = when { + dismissState.progress.from == DismissValue.Default && dismissState.progress.to == DismissValue.Default -> 1f + dismissState.progress.from == DismissValue.DismissedToEnd && dismissState.progress.to == DismissValue.DismissedToEnd -> 0f + dismissState.progress.to == DismissValue.Default -> dismissState.progress.fraction + else -> 1 - dismissState.progress.fraction +} + +private fun Modifier.draggableBottomDrawerModifier( + state: DismissState, + swipeDistance: Float, +): Modifier = this.swipeable( + state = state, + anchors = mapOf(0f to DismissValue.Default, swipeDistance to DismissValue.DismissedToEnd), + thresholds = { _, _ -> FractionalThreshold(0.3f) }, + orientation = Orientation.Vertical, + resistance = null +) + +private fun Modifier.userBoxWidth(user: User, totalUsers: Int, windowWidth: Dp): Modifier { + return if (totalUsers == 1) { + this.width(windowWidth - DEFAULT_PADDING * 2) + } else if (user.activeUser) { + this.width(windowWidth - DEFAULT_PADDING - (USER_PICKER_ROW_PADDING * 3) - USER_PICKER_IMAGE_SIZE) + } else { + this.widthIn(max = (windowWidth - (DEFAULT_PADDING * 2)) * 0.618f) + } +} \ No newline at end of file diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.android.kt index df2499926f..83677f3318 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.android.kt @@ -2,6 +2,7 @@ package chat.simplex.common.views.database import SectionItemView import SectionTextFooter +import TextIconSpaced import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable @@ -22,8 +23,9 @@ actual fun SavePassphraseSetting( useKeychain: Boolean, initialRandomDBPassphrase: Boolean, storedKey: Boolean, - progressIndicator: Boolean, minHeight: Dp, + enabled: Boolean, + smallPadding: Boolean, onCheckedChange: (Boolean) -> Unit, ) { SectionItemView(minHeight = minHeight) { @@ -33,7 +35,11 @@ actual fun SavePassphraseSetting( stringResource(MR.strings.save_passphrase_in_keychain), tint = if (storedKey) SimplexGreen else MaterialTheme.colors.secondary ) - Spacer(Modifier.padding(horizontal = 4.dp)) + if (smallPadding) { + Spacer(Modifier.padding(horizontal = 4.dp)) + } else { + TextIconSpaced(false) + } Text( stringResource(MR.strings.save_passphrase_in_keychain), Modifier.padding(end = 24.dp), @@ -43,7 +49,7 @@ actual fun SavePassphraseSetting( DefaultSwitch( checked = useKeychain, onCheckedChange = onCheckedChange, - enabled = !initialRandomDBPassphrase && !progressIndicator + enabled = enabled ) } } @@ -55,13 +61,14 @@ actual fun DatabaseEncryptionFooter( chatDbEncrypted: Boolean?, storedKey: MutableState<Boolean>, initialRandomDBPassphrase: MutableState<Boolean>, + migration: Boolean, ) { if (chatDbEncrypted == false) { SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted)) } else if (useKeychain.value) { if (storedKey.value) { SectionTextFooter(generalGetString(MR.strings.keychain_is_storing_securely)) - if (initialRandomDBPassphrase.value) { + if (initialRandomDBPassphrase.value && !migration) { SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase)) } else { SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase)) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseView.android.kt new file mode 100644 index 0000000000..e392c0999f --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseView.android.kt @@ -0,0 +1,7 @@ +package chat.simplex.common.views.database + +import chat.simplex.common.views.usersettings.restartApp + +actual fun restartChatOrApp() { + restartApp() +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.android.kt new file mode 100644 index 0000000000..18f3455e36 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.android.kt @@ -0,0 +1,106 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.common.model.CustomTimeUnit +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import com.sd.lib.compose.wheel_picker.* + +@Composable +actual fun CustomTimePicker( + selection: MutableState<Int>, + timeUnitsLimits: List<TimeUnitLimits> +) { + fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List<Int> { + val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit) + val regularUnitValues = (unitLimits.minValue..unitLimits.maxValue).toList() + return regularUnitValues + if (regularUnitValues.contains(selectedValue)) emptyList() else listOf(selectedValue) + } + + val (unit, duration) = CustomTimeUnit.toTimeUnit(selection.value) + val selectedUnit: MutableState<CustomTimeUnit> = remember { mutableStateOf(unit) } + val selectedDuration = remember { mutableStateOf(duration) } + val selectedUnitValues = remember { mutableStateOf(getUnitValues(selectedUnit.value, selectedDuration.value)) } + val isTriggered = remember { mutableStateOf(false) } + + LaunchedEffect(selectedUnit.value) { + // on initial composition, if passed selection doesn't fit into picker bounds, so that selectedDuration is bigger than selectedUnit maxValue + // (e.g., for selection = 121 seconds: selectedUnit would be Second, selectedDuration would be 121 > selectedUnit maxValue of 120), + // selectedDuration would've been replaced by maxValue - isTriggered check prevents this by skipping LaunchedEffect on initial composition + if (isTriggered.value) { + val maxValue = timeUnitsLimits.firstOrNull { it.timeUnit == selectedUnit.value }?.maxValue + if (maxValue != null && selectedDuration.value > maxValue) { + selectedDuration.value = maxValue + selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value) + } else { + selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value) + selection.value = selectedUnit.value.toSeconds * selectedDuration.value + } + } else { + isTriggered.value = true + } + } + + LaunchedEffect(selectedDuration.value) { + selection.value = selectedUnit.value.toSeconds * selectedDuration.value + } + + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING), + horizontalArrangement = Arrangement.spacedBy(0.dp) + ) { + Column(Modifier.weight(1f)) { + val durationPickerState = rememberFWheelPickerState(selectedUnitValues.value.indexOf(selectedDuration.value)) + FVerticalWheelPicker( + count = selectedUnitValues.value.count(), + state = durationPickerState, + unfocusedCount = 2, + focus = { + FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary) + } + ) { index -> + Text( + selectedUnitValues.value[index].toString(), + fontSize = 18.sp, + color = MaterialTheme.colors.primary + ) + } + LaunchedEffect(durationPickerState) { + snapshotFlow { durationPickerState.currentIndex } + .collect { + selectedDuration.value = selectedUnitValues.value[it] + } + } + } + Column(Modifier.weight(1f)) { + val unitPickerState = rememberFWheelPickerState(timeUnitsLimits.indexOfFirst { it.timeUnit == selectedUnit.value }) + FVerticalWheelPicker( + count = timeUnitsLimits.count(), + state = unitPickerState, + unfocusedCount = 2, + focus = { + FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary) + } + ) { index -> + Text( + timeUnitsLimits[index].timeUnit.text, + fontSize = 18.sp, + color = MaterialTheme.colors.primary + ) + } + LaunchedEffect(unitPickerState) { + snapshotFlow { unitPickerState.currentIndex } + .collect { + selectedUnit.value = timeUnitsLimits[it].timeUnit + } + } + } + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/GetImageView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/GetImageView.android.kt index 98d1f8fb19..1c7ba1dcf0 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/GetImageView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/GetImageView.android.kt @@ -171,6 +171,8 @@ actual fun GetImageBottomSheet( modifier = Modifier .fillMaxWidth() .wrapContentHeight() + .imePadding() + .navigationBarsPadding() .onFocusChanged { focusState -> if (!focusState.hasFocus) hideBottomSheet() } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.android.kt index b238bdf7ca..07426c7fbf 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.android.kt @@ -14,6 +14,7 @@ actual fun authenticate( promptSubtitle: String, selfDestruct: Boolean, usingLAMode: LAMode, + oneTime: Boolean, completed: (LAResult) -> Unit ) { val activity = mainActivity.get() ?: return completed(LAResult.Error("")) @@ -27,7 +28,7 @@ actual fun authenticate( else -> completed(LAResult.Unavailable()) } LAMode.PASSCODE -> { - authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, completed) + authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, oneTime, completed) } } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt index d244294763..9d1e0c4e97 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt @@ -1,5 +1,7 @@ package chat.simplex.common.views.helpers +import android.content.ClipboardManager +import android.content.Context import android.content.res.Resources import android.graphics.* import android.graphics.Typeface @@ -12,6 +14,7 @@ import android.text.SpannedString import android.text.style.* import android.util.Base64 import android.view.WindowManager +import androidx.compose.runtime.* import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.* @@ -55,6 +58,19 @@ fun keepScreenOn(on: Boolean) { } } +@Composable +actual fun SetupClipboardListener() { + DisposableEffect(Unit) { + val service = androidAppContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val listener = { chatModel.clipboardHasText.value = service.hasPrimaryClip() } + chatModel.clipboardHasText.value = service.hasPrimaryClip() + service.addPrimaryClipChangedListener(listener) + onDispose { + service.removePrimaryClipChangedListener(listener) + } + } +} + actual fun escapedHtmlToAnnotatedString(text: String, density: Density): AnnotatedString { return spannableStringToAnnotatedString(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY), density) } @@ -172,7 +188,12 @@ actual suspend fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>? return if (filePath != null && file != null) { try { val data = if (file.fileSource?.cryptoArgs != null) { - readCryptoFile(getAppFilePath(file.fileSource.filePath), file.fileSource.cryptoArgs) + try { + readCryptoFile(getAppFilePath(file.fileSource.filePath), file.fileSource.cryptoArgs) + } catch (e: Exception) { + Log.e(TAG, "Unable to read crypto file: " + e.stackTraceToString()) + return null + } } else { File(getAppFilePath(file.fileName)).readBytes() } @@ -220,10 +241,15 @@ private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, } actual fun getFileName(uri: URI): String? { - return androidAppContext.contentResolver.query(uri.toUri(), null, null, null, null)?.use { cursor -> - val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - cursor.moveToFirst() - cursor.getString(nameIndex) + return try { + androidAppContext.contentResolver.query(uri.toUri(), null, null, null, null)?.use { cursor -> + val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + cursor.moveToFirst() + // Can make an exception + cursor.getString(nameIndex) + } + } catch (e: Exception) { + null } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/WorkaroundFocusSearchLayout.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/WorkaroundFocusSearchLayout.kt new file mode 100644 index 0000000000..d111b99385 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/WorkaroundFocusSearchLayout.kt @@ -0,0 +1,41 @@ +package chat.simplex.common.views.helpers + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout + +/** + * A workaround for the ANR issue on Compose 1.7.x. + * https://issuetracker.google.com/issues/369354336 + * Code from: + * https://issuetracker.google.com/issues/369354336#comment8 +*/ +class WorkaroundFocusSearchLayout : FrameLayout { + + constructor( + context: Context, + ) : super(context) + + constructor( + context: Context, + attrs: AttributeSet?, + ) : super(context, attrs) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int, + ) : super(context, attrs, defStyleAttr, defStyleRes) + + override fun focusSearch(focused: View?, direction: Int): View? { + return null + } +} \ No newline at end of file diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/ConnectViaLinkView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/ConnectViaLinkView.android.kt deleted file mode 100644 index e5a7ae40a5..0000000000 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/ConnectViaLinkView.android.kt +++ /dev/null @@ -1,68 +0,0 @@ -package chat.simplex.common.views.newchat - -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.unit.sp -import chat.simplex.common.model.ChatModel -import chat.simplex.common.model.RemoteHostInfo -import chat.simplex.res.MR - -@Composable -actual fun ConnectViaLinkView(m: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) { - // TODO this should close if remote host changes in model - val selection = remember { - mutableStateOf( - runCatching { ConnectViaLinkTab.valueOf(m.controller.appPrefs.connectViaLinkTab.get()!!) }.getOrDefault(ConnectViaLinkTab.SCAN) - ) - } - val tabTitles = ConnectViaLinkTab.values().map { - when (it) { - ConnectViaLinkTab.SCAN -> stringResource(MR.strings.scan_QR_code) - ConnectViaLinkTab.PASTE -> stringResource(MR.strings.paste_the_link_you_received) - } - } - Column( - Modifier.fillMaxHeight(), - verticalArrangement = Arrangement.SpaceBetween - ) { - Column(Modifier.weight(1f)) { - when (selection.value) { - ConnectViaLinkTab.SCAN -> { - ScanToConnectView(m, rh, close) - } - ConnectViaLinkTab.PASTE -> { - PasteToConnectView(m, rh, close) - } - } - } - TabRow( - selectedTabIndex = selection.value.ordinal, - backgroundColor = Color.Transparent, - contentColor = MaterialTheme.colors.primary, - ) { - tabTitles.forEachIndexed { index, it -> - Tab( - selected = selection.value.ordinal == index, - onClick = { - selection.value = ConnectViaLinkTab.values()[index] - m.controller.appPrefs.connectViaLinkTab.set(selection.value .name) - }, - text = { Text(it, fontSize = 13.sp) }, - icon = { - Icon( - if (ConnectViaLinkTab.SCAN.ordinal == index) painterResource(MR.images.ic_qr_code) else painterResource(MR.images.ic_article), - it - ) - }, - selectedContentColor = MaterialTheme.colors.primary, - unselectedContentColor = MaterialTheme.colors.secondary, - ) - } - } - } -} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt index e7453ce20a..6cf432a15f 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt @@ -2,16 +2,21 @@ package chat.simplex.common.views.newchat import android.Manifest import android.annotation.SuppressLint +import android.content.pm.PackageManager import android.util.Log import android.view.ViewGroup import androidx.camera.core.* import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.PreviewView +import androidx.compose.foundation.layout.* +import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.platform.* +import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat import boofcv.abst.fiducial.QrCodeDetector @@ -20,23 +25,30 @@ import boofcv.android.ConvertCameraImage import boofcv.factory.fiducial.FactoryFiducial import boofcv.struct.image.GrayU8 import chat.simplex.common.platform.TAG +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import com.google.accompanist.permissions.PermissionStatus import com.google.accompanist.permissions.rememberPermissionState import com.google.common.util.concurrent.ListenableFuture +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.delay import java.util.concurrent.* // Adapted from learntodroid - https://gist.github.com/learntodroid/8f839be0b29d0378f843af70607bd7f5 @Composable -actual fun QRCodeScanner(onBarcode: (String) -> Unit) { - val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) - LaunchedEffect(Unit) { - cameraPermissionState.launchPermissionRequest() - } +actual fun QRCodeScanner( + showQRCodeScanner: MutableState<Boolean>, + padding: PaddingValues, + onBarcode: suspend (String) -> Boolean +) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current - var preview by remember { mutableStateOf<Preview?>(null) } - var lastAnalyzedTimeStamp = 0L - var contactLink = "" + val preview = remember { mutableStateOf<Preview?>(null) } + val contactLink = remember { mutableStateOf("") } + val checkingLink = remember { mutableStateOf(false) } val cameraProviderFuture by produceState<ListenableFuture<ProcessCameraProvider>?>(initialValue = null) { value = ProcessCameraProvider.getInstance(context) @@ -48,57 +60,109 @@ actual fun QRCodeScanner(onBarcode: (String) -> Unit) { } } - AndroidView( - factory = { AndroidViewContext -> - PreviewView(AndroidViewContext).apply { - this.scaleType = PreviewView.ScaleType.FILL_CENTER - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - ) - implementationMode = PreviewView.ImplementationMode.COMPATIBLE - } - }, - modifier = Modifier.clipToBounds() - ) { previewView -> - val cameraSelector: CameraSelector = CameraSelector.Builder() - .requireLensFacing(CameraSelector.LENS_FACING_BACK) - .build() - val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor() - cameraProviderFuture?.addListener({ - preview = Preview.Builder().build().also { - it.setSurfaceProvider(previewView.surfaceProvider) - } - val detector: QrCodeDetector<GrayU8> = FactoryFiducial.qrcode(null, GrayU8::class.java) - fun getQR(imageProxy: ImageProxy) { - val currentTimeStamp = System.currentTimeMillis() - if (currentTimeStamp - lastAnalyzedTimeStamp >= TimeUnit.SECONDS.toMillis(1)) { - detector.process(imageProxyToGrayU8(imageProxy)) - val found = detector.detections - val qr = found.firstOrNull() - if (qr != null) { - if (qr.message != contactLink) { - // Make sure link is new and not a repeat - contactLink = qr.message - onBarcode(contactLink) + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) + val modifier = Modifier + .padding(padding) + .clipToBounds() + .widthIn(max = 400.dp) + .aspectRatio(1f) + val showScanner = remember { showQRCodeScanner } + if (showScanner.value && cameraPermissionState.status == PermissionStatus.Granted) { + AndroidView( + factory = { AndroidViewContext -> + PreviewView(AndroidViewContext).apply { + this.scaleType = PreviewView.ScaleType.FILL_CENTER + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + implementationMode = PreviewView.ImplementationMode.COMPATIBLE + } + }, + modifier = modifier + ) { previewView -> + val cameraSelector: CameraSelector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_BACK) + .build() + val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor() + cameraProviderFuture?.addListener({ + preview.value = Preview.Builder().build().also { + it.setSurfaceProvider(previewView.surfaceProvider) + } + val detector: QrCodeDetector<GrayU8> = FactoryFiducial.qrcode(null, GrayU8::class.java) + suspend fun getQR(imageProxy: ImageProxy) { + if (checkingLink.value) return + checkingLink.value = true + + detector.process(imageProxyToGrayU8(imageProxy)) + val found = detector.detections + val qr = found.firstOrNull() + if (qr != null) { + if (qr.message != contactLink.value) { + // Make sure link is new and not a repeat if that link was handled successfully + if (onBarcode(qr.message)) { + contactLink.value = qr.message + } + // just some delay to not spam endlessly with alert in case the user scan something wrong, and it fails fast + // (for example, scan user's address while verifying contact code - it prevents alert spam) + delay(1000) + } } + checkingLink.value = false + imageProxy.close() + } + + val imageAnalyzer = ImageAnalysis.Analyzer { proxy -> withApi { getQR(proxy) } } + val imageAnalysis: ImageAnalysis = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .setImageQueueDepth(1) + .build() + .also { it.setAnalyzer(cameraExecutor, imageAnalyzer) } + try { + cameraProviderFuture?.get()?.unbindAll() + cameraProviderFuture?.get()?.bindToLifecycle(lifecycleOwner, cameraSelector, preview.value, imageAnalysis) + } catch (e: Exception) { + Log.d(TAG, "CameraPreview: ${e.localizedMessage}") + } + }, ContextCompat.getMainExecutor(context)) + } + } else { + val buttonColors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.9f), + contentColor = MaterialTheme.colors.primary, + disabledBackgroundColor = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.9f), + disabledContentColor = MaterialTheme.colors.primary, + ) + var permissionRequested by rememberSaveable { mutableStateOf(false) } + when { + cameraPermissionState.status is PermissionStatus.Denied && !permissionRequested && showScanner.value -> { + LaunchedEffect(Unit) { + permissionRequested = true + cameraPermissionState.launchPermissionRequest() + } + } + cameraPermissionState.status is PermissionStatus.Denied -> { + Button({ cameraPermissionState.launchPermissionRequest() }, modifier = modifier, colors = buttonColors) { + Icon(painterResource(MR.images.ic_camera_enhance), null) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Text(stringResource(MR.strings.enable_camera_access)) + } + } + cameraPermissionState.status == PermissionStatus.Granted -> { + Button({ showQRCodeScanner.value = true }, modifier = modifier, colors = buttonColors) { + Icon(painterResource(MR.images.ic_qr_code), null) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Text(stringResource(MR.strings.tap_to_scan)) + } + } + !LocalContext.current.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) -> { + Button({ }, enabled = false, modifier = modifier, colors = buttonColors) { + Text(stringResource(MR.strings.camera_not_available)) } } - imageProxy.close() } - val imageAnalyzer = ImageAnalysis.Analyzer { proxy -> getQR(proxy) } - val imageAnalysis: ImageAnalysis = ImageAnalysis.Builder() - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) - .setImageQueueDepth(1) - .build() - .also { it.setAnalyzer(cameraExecutor, imageAnalyzer) } - try { - cameraProviderFuture?.get()?.unbindAll() - cameraProviderFuture?.get()?.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis) - } catch (e: Exception) { - Log.d(TAG, "CameraPreview: ${e.localizedMessage}") - } - }, ContextCompat.getMainExecutor(context)) + } } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.android.kt deleted file mode 100644 index f046f44bee..0000000000 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.android.kt +++ /dev/null @@ -1,22 +0,0 @@ -package chat.simplex.common.views.newchat - -import android.Manifest -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import chat.simplex.common.model.ChatModel -import chat.simplex.common.model.RemoteHostInfo -import com.google.accompanist.permissions.rememberPermissionState - -@Composable -actual fun ScanToConnectView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) { - val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) - LaunchedEffect(Unit) { - cameraPermissionState.launchPermissionRequest() - } - ConnectContactLayout( - chatModel = chatModel, - rh = rh, - incognitoPref = chatModel.controller.appPrefs.incognito, - close = close - ) -} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.android.kt index 605c40445a..0378fcbd7a 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.android.kt @@ -4,18 +4,31 @@ import android.Manifest import android.os.Build import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import chat.simplex.common.platform.ntfManager -import com.google.accompanist.permissions.rememberPermissionState +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.platform.* +import com.google.accompanist.permissions.* @Composable actual fun SetNotificationsModeAdditions() { if (Build.VERSION.SDK_INT >= 33) { val notificationsPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) - LaunchedEffect(notificationsPermissionState.hasPermission) { - if (notificationsPermissionState.hasPermission) { - ntfManager.androidCreateNtfChannelsMaybeShowAlert() + LaunchedEffect(notificationsPermissionState.status == PermissionStatus.Granted) { + val canAsk = appPrefs.canAskToEnableNotifications.get() + if (notificationsPermissionState.status is PermissionStatus.Denied) { + if (notificationsPermissionState.status.shouldShowRationale || !canAsk) { + if (canAsk) { + appPrefs.canAskToEnableNotifications.set(false) + } + Log.w(TAG, "Notifications are disabled and nobody will ask to enable them") + } else { + notificationsPermissionState.launchPermissionRequest() + } } else { - notificationsPermissionState.launchPermissionRequest() + if (!canAsk) { + // the user allowed notifications in system alert or manually in settings, allow to ask him next time if needed + appPrefs.canAskToEnableNotifications.set(true) + } + ntfManager.androidCreateNtfChannelsMaybeShowAlert() } } } else { 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 bfe87b17d7..d9d3af7bb7 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 @@ -1,6 +1,8 @@ package chat.simplex.common.views.onboarding +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import chat.simplex.common.model.SharedPreference import chat.simplex.common.model.User import chat.simplex.res.MR @@ -8,8 +10,8 @@ import chat.simplex.res.MR @Composable actual fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference<OnboardingStage>, onclick: (() -> Unit)?) { if (user == null) { - OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, onclick = onclick) + OnboardingActionButton(Modifier.fillMaxWidth(), labelId = MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onclick = onclick) } else { - OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, true, onclick = onclick) + OnboardingActionButton(Modifier.fillMaxWidth(), labelId = MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onclick = onclick) } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt index e02c011fac..320a8e876a 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt @@ -2,6 +2,7 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced +import SectionSpacer import SectionView import android.app.Activity import android.content.ComponentName @@ -12,19 +13,18 @@ import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.MaterialTheme.colors import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.* +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.toBitmap -import chat.simplex.common.R import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -32,19 +32,20 @@ import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* import chat.simplex.common.helpers.APPLICATION_ID import chat.simplex.common.helpers.saveAppLocale -import chat.simplex.common.views.usersettings.AppearanceScope.ColorEditor +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.res.MR +import dev.icerock.moko.resources.ImageResource +import dev.icerock.moko.resources.compose.painterResource import kotlinx.coroutines.delay -enum class AppIcon(val resId: Int) { - DEFAULT(R.drawable.icon_round_common), - DARK_BLUE(R.drawable.icon_dark_blue_round_common), +enum class AppIcon(val image: ImageResource) { + DEFAULT(MR.images.ic_simplex_light), + DARK_BLUE(MR.images.ic_simplex_dark), } @Composable -actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) { +actual fun AppearanceView(m: ChatModel) { val appIcon = remember { mutableStateOf(findEnabledIcon()) } - fun setAppIcon(newIcon: AppIcon) { if (appIcon.value == newIcon) return val newComponent = ComponentName(APPLICATION_ID, "chat.simplex.app.MainActivity_${newIcon.name.lowercase()}") @@ -61,18 +62,11 @@ actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatMod appIcon.value = newIcon } - AppearanceScope.AppearanceLayout( appIcon, m.controller.appPrefs.appLanguage, m.controller.appPrefs.systemDarkTheme, changeIcon = ::setAppIcon, - showSettingsModal = showSettingsModal, - editColor = { name, initialColor -> - ModalManager.start.showModalCloseable { close -> - ColorEditor(name, initialColor, close) - } - }, ) } @@ -82,14 +76,10 @@ fun AppearanceScope.AppearanceLayout( languagePref: SharedPreference<String?>, systemDarkTheme: SharedPreference<String?>, changeIcon: (AppIcon) -> Unit, - showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - editColor: (ThemeColor, Color) -> Unit, ) { - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.appearance_settings)) - SectionView(stringResource(MR.strings.settings_section_title_language), padding = PaddingValues()) { + SectionView(stringResource(MR.strings.settings_section_title_interface), contentPadding = PaddingValues()) { val context = LocalContext.current // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // SectionItemWithValue( @@ -115,23 +105,44 @@ fun AppearanceScope.AppearanceLayout( } } // } - } - SectionDividerSpaced() - SectionView(stringResource(MR.strings.settings_section_title_icon), padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) { + SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) { enabled -> + if (enabled) appPrefs.chatBottomBar.set(true) + } + if (remember { appPrefs.oneHandUI.state }.value) { + SettingsPreferenceItem(icon = null, stringResource(MR.strings.chat_bottom_bar), ChatModel.controller.appPrefs.chatBottomBar) + } + } + + SectionDividerSpaced() + ThemesSection(systemDarkTheme) + + SectionDividerSpaced() + AppToolbarsSection() + + SectionDividerSpaced() + MessageShapeSection() + + SectionDividerSpaced() + ProfileImageSection() + + SectionDividerSpaced(maxTopPadding = true) + + SectionView(stringResource(MR.strings.settings_section_title_icon), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) { LazyRow { items(AppIcon.values().size, { index -> AppIcon.values()[index] }) { index -> val item = AppIcon.values()[index] - val mipmap = ContextCompat.getDrawable(LocalContext.current, item.resId)!! Image( - bitmap = mipmap.toBitmap().asImageBitmap(), + painterResource(item.image), contentDescription = "", contentScale = ContentScale.Fit, modifier = Modifier - .shadow(if (item == icon.value) 1.dp else 0.dp, ambientColor = colors.secondaryVariant) + .border(1.dp, color = if (item == icon.value) colors.secondaryVariant else Color.Transparent, RoundedCornerShape(percent = 22)) + .clip(RoundedCornerShape(percent = 22)) .size(70.dp) .clickable { changeIcon(item) } .padding(10.dp) + .clip(CircleShape) ) if (index + 1 != AppIcon.values().size) { @@ -142,7 +153,8 @@ fun AppearanceScope.AppearanceLayout( } SectionDividerSpaced(maxTopPadding = true) - ThemesSection(systemDarkTheme, showSettingsModal, editColor) + FontScaleSection() + SectionBottomSpacer() } } @@ -162,8 +174,6 @@ fun PreviewAppearanceSettings() { languagePref = SharedPreference({ null }, {}), systemDarkTheme = SharedPreference({ null }, {}), changeIcon = {}, - showSettingsModal = { {} }, - editColor = { _, _ -> }, ) } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt index 49a29cf141..04b59732dd 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt @@ -2,11 +2,9 @@ package chat.simplex.common.views.usersettings import SectionView import androidx.compose.runtime.Composable -import androidx.work.WorkManager import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* -import chat.simplex.common.views.helpers.AlertManager -import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import com.jakewharton.processphoenix.ProcessPhoenix import dev.icerock.moko.resources.compose.painterResource @@ -15,26 +13,25 @@ import dev.icerock.moko.resources.compose.stringResource @Composable actual fun SettingsSectionApp( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showVersion: () -> Unit, withAuth: (title: String, desc: String, block: () -> Unit) -> Unit ) { SectionView(stringResource(MR.strings.settings_section_title_app)) { - SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) }, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp) + SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) }) + SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(withAuth) }) AppVersionItem(showVersion) } } -private fun restartApp() { +fun restartApp() { ProcessPhoenix.triggerRebirth(androidAppContext) shutdownApp() } private fun shutdownApp() { - WorkManager.getInstance(androidAppContext).cancelAllWork() + androidAppContext.getWorkManagerInstance().cancelAllWork() platform.androidServiceSafeStop() Runtime.getRuntime().exit(0) } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.android.kt new file mode 100644 index 0000000000..e52515b345 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.android.kt @@ -0,0 +1,19 @@ +package chat.simplex.common.views.usersettings.networkAndServers + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +actual fun ConditionsBox(modifier: Modifier, scrollState: ScrollState, content: @Composable() (BoxScope.() -> Unit)){ + Box( + modifier = modifier + .verticalScroll(scrollState) + .padding(8.dp) + ) { + content() + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.android.kt similarity index 69% rename from apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.android.kt rename to apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.android.kt index af5a27be11..8b5def7451 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.android.kt @@ -1,13 +1,13 @@ -package chat.simplex.common.views.usersettings +package chat.simplex.common.views.usersettings.networkAndServers import android.Manifest import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import chat.simplex.common.model.ServerCfg +import chat.simplex.common.model.UserServer import com.google.accompanist.permissions.rememberPermissionState @Composable -actual fun ScanProtocolServer(rhId: Long?, onNext: (ServerCfg) -> Unit) { +actual fun ScanProtocolServer(rhId: Long?, onNext: (UserServer) -> Unit) { val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) LaunchedEffect(Unit) { cameraPermissionState.launchPermissionRequest() diff --git a/apps/multiplatform/common/src/androidMain/res/drawable/edit_text_cursor.xml b/apps/multiplatform/common/src/androidMain/res/drawable/edit_text_cursor.xml index 683c3a4dd4..948ae4d4bf 100644 --- a/apps/multiplatform/common/src/androidMain/res/drawable/edit_text_cursor.xml +++ b/apps/multiplatform/common/src/androidMain/res/drawable/edit_text_cursor.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" > - <solid android:color="@color/highOrLowLight" /> + <solid android:color="#8b8786" /> <size android:width="1dp" /> </shape> diff --git a/apps/multiplatform/common/src/androidMain/res/drawable/icon_dark_blue_round_common.png b/apps/multiplatform/common/src/androidMain/res/drawable/icon_dark_blue_round_common.png deleted file mode 100644 index e413ad857d..0000000000 Binary files a/apps/multiplatform/common/src/androidMain/res/drawable/icon_dark_blue_round_common.png and /dev/null differ diff --git a/apps/multiplatform/common/src/androidMain/res/drawable/icon_round_common.png b/apps/multiplatform/common/src/androidMain/res/drawable/icon_round_common.png deleted file mode 100644 index 755904ee8e..0000000000 Binary files a/apps/multiplatform/common/src/androidMain/res/drawable/icon_round_common.png and /dev/null differ diff --git a/apps/multiplatform/common/src/androidMain/res/raw/connecting_call.mp3 b/apps/multiplatform/common/src/androidMain/res/raw/connecting_call.mp3 new file mode 100644 index 0000000000..fc425bab97 Binary files /dev/null and b/apps/multiplatform/common/src/androidMain/res/raw/connecting_call.mp3 differ diff --git a/apps/multiplatform/common/src/androidMain/res/raw/in_call.mp3 b/apps/multiplatform/common/src/androidMain/res/raw/in_call.mp3 new file mode 100644 index 0000000000..1049be4462 Binary files /dev/null and b/apps/multiplatform/common/src/androidMain/res/raw/in_call.mp3 differ diff --git a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c index 676c58fb49..b9b5277aeb 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c @@ -36,7 +36,7 @@ Java_chat_simplex_common_platform_CoreKt_initHS(__unused JNIEnv *env, __unused j char *argv[] = { "simplex", "+RTS", // requires `hs_init_with_rtsopts` - "-A16m", // chunk size for new allocations + "-A64m", // chunk size for new allocations "-H64m", // initial heap size "-xn", // non-moving GC NULL @@ -57,6 +57,7 @@ typedef long* chat_ctrl; */ extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl); +extern char *chat_close_store(chat_ctrl ctrl); extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); extern char *chat_send_remote_cmd(chat_ctrl ctrl, const int rhId, const char *cmd); extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated @@ -65,6 +66,7 @@ extern char *chat_parse_markdown(const char *str); extern char *chat_parse_server(const char *str); extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_valid_name(const char *name); +extern int chat_json_length(const char *str); extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length); extern char *chat_read_file(const char *path, const char *key, const char *nonce); extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path); @@ -93,6 +95,12 @@ Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, __unused j return ret; } +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatCloseStore(JNIEnv *env, __unused jclass clazz, jlong controller) { + jstring res = (*env)->NewStringUTF(env, chat_close_store((void*)controller)); + return res; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) { const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE); @@ -156,6 +164,14 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz return res; } +JNIEXPORT int JNICALL +Java_chat_simplex_common_platform_CoreKt_chatJsonLength(JNIEnv *env, jclass clazz, jstring str) { + const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE); + int res = chat_json_length(_str); + (*env)->ReleaseStringUTFChars(env, str, _str); + return res; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) { const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE); diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c index 292715bdc5..5c921c400d 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c @@ -10,10 +10,10 @@ JNIEXPORT void JNICALL Java_chat_simplex_common_platform_CoreKt_initHS(JNIEnv *env, jclass clazz) { #ifdef _WIN32 int argc = 4; - char *argv[] = {"simplex", "+RTS", "-A16m", "-H64m", NULL}; // non-moving GC is broken on windows with GHC 9.4-9.6.3 + char *argv[] = {"simplex", "+RTS", "-A64m", "-H64m", NULL}; // non-moving GC is broken on windows with GHC 9.4-9.6.3 #else int argc = 5; - char *argv[] = {"simplex", "+RTS", "-A16m", "-H64m", "-xn", NULL}; // see android/simplex-api.c for details + char *argv[] = {"simplex", "+RTS", "-A64m", "-H64m", "-xn", NULL}; // see android/simplex-api.c for details #endif char **pargv = argv; hs_init_with_rtsopts(&argc, &pargv); @@ -30,6 +30,7 @@ typedef long* chat_ctrl; */ extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl); +extern char *chat_close_store(chat_ctrl ctrl); extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); extern char *chat_send_remote_cmd(chat_ctrl ctrl, const int rhId, const char *cmd); extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated @@ -38,6 +39,7 @@ extern char *chat_parse_markdown(const char *str); extern char *chat_parse_server(const char *str); extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_valid_name(const char *name); +extern int chat_json_length(const char *str); extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length); extern char *chat_read_file(const char *path, const char *key, const char *nonce); extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path); @@ -106,6 +108,12 @@ Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, jclass cla return ret; } +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatCloseStore(JNIEnv *env, jclass clazz, jlong controller) { + jstring res = decode_to_utf8_string(env, chat_close_store((void*)controller)); + return res; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, jclass clazz, jlong controller, jstring msg) { const char *_msg = encode_to_utf8_chars(env, msg); @@ -166,6 +174,14 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz return res; } +JNIEXPORT int JNICALL +Java_chat_simplex_common_platform_CoreKt_chatJsonLength(JNIEnv *env, jclass clazz, jstring str) { + const char *_str = encode_to_utf8_chars(env, str); + int res = chat_json_length(_str); + (*env)->ReleaseStringUTFChars(env, str, _str); + return res; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) { const char *_path = encode_to_utf8_chars(env, path); 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 d457eb57a1..d88a450fd1 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 @@ -1,8 +1,9 @@ package chat.simplex.common +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.Animatable -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -10,22 +11,28 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.draw.* +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.* +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView 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.CreateFirstProfile import chat.simplex.common.views.helpers.SimpleButton import chat.simplex.common.views.SplashView -import chat.simplex.common.views.call.ActiveCallView -import chat.simplex.common.views.call.IncomingCallAlertView +import chat.simplex.common.views.call.* import chat.simplex.common.views.chat.ChatView import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.database.DatabaseErrorView import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.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.onboarding.* import chat.simplex.common.views.usersettings.* @@ -35,17 +42,37 @@ import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -data class SettingsViewState( - val userPickerState: MutableStateFlow<AnimatedViewState>, - val scaffoldState: ScaffoldState -) - @Composable fun AppScreen() { + AppBarHandler.appBarMaxHeightPx = with(LocalDensity.current) { AppBarHeight.roundToPx() } SimpleXTheme { - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - Surface(color = MaterialTheme.colors.background) { - MainScreen() + Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) { + // This padding applies to landscape view only taking care of navigation bar and holes in screen in status bar area + // (because nav bar and holes located on vertical sides of screen in landscape view) + val direction = LocalLayoutDirection.current + val safePadding = WindowInsets.safeDrawing.asPaddingValues() + val cutout = WindowInsets.displayCutout.asPaddingValues() + val cutoutStart = cutout.calculateStartPadding(direction) + val cutoutEnd = cutout.calculateEndPadding(direction) + val cutoutMax = maxOf(cutoutStart, cutoutEnd) + val paddingStartUntouched = safePadding.calculateStartPadding(direction) + val paddingStart = paddingStartUntouched - cutoutStart + val paddingEndUntouched = safePadding.calculateEndPadding(direction) + val paddingEnd = paddingEndUntouched - cutoutEnd + // Such a strange layout is needed because the main content should be covered by solid color in order to hide overflow + // of some elements that may have negative offset (so, can't use Row {}). + // To check: go to developer settings of Android, choose Display cutout -> Punch hole, and rotate the phone to landscape, open any chat + Box { + val fullscreenGallery = remember { chatModel.fullscreenGalleryVisible } + Box(Modifier.padding(start = paddingStart + cutoutMax, end = paddingEnd + cutoutMax).consumeWindowInsets(PaddingValues(start = paddingStartUntouched, end = paddingEndUntouched))) { + Box(Modifier.drawBehind { + if (fullscreenGallery.value) { + drawRect(Color.Black, topLeft = Offset(-(paddingStart + cutoutMax).toPx(), 0f), Size(size.width + (paddingStart + cutoutMax).toPx() + (paddingEnd + cutoutMax).toPx(), size.height)) + } + }) { + MainScreen() + } + } } } } @@ -64,9 +91,10 @@ fun MainScreen() { LaunchedEffect(showAdvertiseLAAlert) { if ( !chatModel.controller.appPrefs.laNoticeShown.get() + && !appPrefs.performLA.get() && showAdvertiseLAAlert && chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete - && chatModel.chats.count() > 1 + && chatModel.chats.size > 3 && chatModel.activeCallInvitation.value == null ) { AppLock.showLANotice(ChatModel.controller.appPrefs.laNoticeShown) } @@ -76,6 +104,7 @@ fun MainScreen() { laUnavailableInstructionAlert() } } + platform.desktopShowAppUpdateNotice() LaunchedEffect(chatModel.clearOverlays.value) { if (chatModel.clearOverlays.value) { ModalManager.closeAllModalsEverywhere() @@ -85,7 +114,7 @@ fun MainScreen() { @Composable fun AuthView() { - Surface(color = MaterialTheme.colors.background) { + Surface(color = MaterialTheme.colors.background.copy(1f), contentColor = LocalContentColor.current) { Box( Modifier.fillMaxSize(), contentAlignment = Alignment.Center @@ -103,13 +132,23 @@ fun MainScreen() { } Box { + val unauthorized = remember { derivedStateOf { AppLock.userAuthorized.value != true } } val onboarding by remember { chatModel.controller.appPrefs.onboardingStage.state } val localUserCreated = chatModel.localUserCreated.value var showInitializationView by remember { mutableStateOf(false) } when { - chatModel.chatDbStatus.value == null && showInitializationView -> InitializationView() + 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() + } + } + chatModel.dbMigrationInProgress.value -> DefaultProgressView(stringResource(MR.strings.database_migration_in_progress)) + chatModel.chatDbStatus.value == null && showInitializationView -> DefaultProgressView(stringResource(MR.strings.opening_database)) showChatDatabaseError -> { - chatModel.chatDbStatus.value?.let { + // Prevent showing keyboard on Android when: passcode enabled and database password not saved + if (!unauthorized.value && chatModel.chatDbStatus.value != null) { DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs) } } @@ -123,33 +162,56 @@ fun MainScreen() { userPickerState.value = AnimatedViewState.VISIBLE } } - val scaffoldState = rememberScaffoldState() - val settingsState = remember { SettingsViewState(userPickerState, scaffoldState) } + SetupClipboardListener() if (appPlatform.isAndroid) { - AndroidScreen(settingsState) + AndroidWrapInCallLayout { + AndroidScreen(userPickerState) + } } else { - DesktopScreen(settingsState) + DesktopScreen(userPickerState) } } } - onboarding == OnboardingStage.Step1_SimpleXInfo -> { - SimpleXInfo(chatModel, onboarding = true) - if (appPlatform.isDesktop) { - ModalManager.fullscreen.showInView() + 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() + } + } + 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) } } - onboarding == OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {} - onboarding == OnboardingStage.LinkAMobile -> LinkAMobile() - onboarding == OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel) - onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel, null) - onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel) } if (appPlatform.isAndroid) { - ModalManager.fullscreen.showInView() + AndroidWrapInCallLayout { + ModalManager.fullscreen.showInView() + } SwitchingUsersView() } - val unauthorized = remember { derivedStateOf { AppLock.userAuthorized.value != true } } if (unauthorized.value && !(chatModel.activeCallViewIsVisible.value && chatModel.showCallView.value)) { LaunchedEffect(Unit) { // With these constrains when user presses back button while on ChatList, activity destroys and shows auth request @@ -161,16 +223,24 @@ fun MainScreen() { if (chatModel.controller.appPrefs.performLA.get() && AppLock.laFailed.value) { AuthView() } else { - SplashView() + SplashView(true) ModalManager.fullscreen.showPasscodeInView() } } else { if (chatModel.showCallView.value) { - ActiveCallView() - } else { - // It's needed for privacy settings toggle, so it can be shown even if the app is passcode unlocked - ModalManager.fullscreen.showPasscodeInView() + if (appPlatform.isAndroid) { + LaunchedEffect(Unit) { + // This if prevents running the activity in the following condition: + // - the activity already started before and was destroyed by collapsing active call (start audio call, press back button, go to a launcher) + if (!chatModel.activeCallViewIsCollapsed.value) { + platform.androidStartCallActivity(false) + } + } + } else { + ActiveCallView() + } } + ModalManager.fullscreen.showOneTimePasscodeInView() AlertManager.privacySensitive.showInView() if (onboarding == OnboardingStage.OnboardingComplete) { LaunchedEffect(chatModel.currentUser.value, chatModel.appOpenUrl.value) { @@ -203,18 +273,43 @@ fun MainScreen() { } } +val ANDROID_CALL_TOP_PADDING = 40.dp + @Composable -fun AndroidScreen(settingsState: SettingsViewState) { +fun AndroidWrapInCallLayout(content: @Composable () -> Unit) { + val call = remember { chatModel.activeCall}.value + val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted + Box { + Box(Modifier.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)) { + content() + } + if (call != null && showCallArea) { + ActiveCallInteractiveArea(call) + } + } +} + +@Composable +fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) { BoxWithConstraints { - var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) } + val currentChatId = remember { mutableStateOf(chatModel.chatId.value) } val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) } + val cutout = WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal).asPaddingValues() + val direction = LocalLayoutDirection.current + val hasCutout = cutout.calculateStartPadding(direction) + cutout.calculateEndPadding(direction) > 0.dp Box( Modifier + // clipping only for devices with cutout currently visible on sides. It prevents showing chat list with open chat view + // In order cases it's not needed to use clip + .then(if (hasCutout) Modifier.clip(RectangleShape) else Modifier) .graphicsLayer { - translationX = -offset.value.dp.toPx() + // minOf thing is needed for devices with holes in screen while the user on ChatView rotates his phone from portrait to landscape + // because in this case (at least in emulator) maxWidth changes in two steps: big first, smaller on next frame. + // But offset is remembered already, so this is a better way than dropping a value of offset + translationX = -minOf(offset.value.dp, maxWidth).toPx() } ) { - StartPartOfScreen(settingsState) + StartPartOfScreen(userPickerState) } val scope = rememberCoroutineScope() val onComposed: suspend (chatId: String?) -> Unit = { chatId -> @@ -235,36 +330,47 @@ fun AndroidScreen(settingsState: SettingsViewState) { .distinctUntilChanged() .collect { if (it == null) onComposed(null) - currentChatId = it + currentChatId.value = it } } } - Box(Modifier.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }) Box2@{ - currentChatId?.let { - ChatView(it, chatModel, onComposed) + Box(Modifier + .then(if (hasCutout) Modifier.clip(RectangleShape) else Modifier) + .graphicsLayer { translationX = maxWidth.toPx() - minOf(offset.value.dp, maxWidth).toPx() } + ) Box2@{ + currentChatId.value?.let { + ChatView(chatsCtx = chatModel.chatsContext, currentChatId, onComposed = onComposed) } } } } @Composable -fun StartPartOfScreen(settingsState: SettingsViewState) { +fun StartPartOfScreen(userPickerState: MutableStateFlow<AnimatedViewState>) { if (chatModel.setDeliveryReceipts.value) { - SetDeliveryReceiptsView(chatModel) + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + SetDeliveryReceiptsView(chatModel) + } } else { val stopped = chatModel.chatRunning.value == false - if (chatModel.sharedContent.value == null) - ChatListView(chatModel, settingsState, AppLock::setPerformLA, stopped) - else - ShareListView(chatModel, settingsState, stopped) + if (chatModel.sharedContent.value == null) { + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ChatListView(chatModel, userPickerState, AppLock::setPerformLA, stopped) + } + } else { + // LALAL initial load of view doesn't show blur. Focusing text field shows it + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler(keyboardCoversBar = false)) { + ShareListView(chatModel, stopped) + } + } } } @Composable fun CenterPartOfScreen() { - val currentChatId by remember { ChatModel.chatId } + val currentChatId = remember { ChatModel.chatId } LaunchedEffect(Unit) { - snapshotFlow { currentChatId } + snapshotFlow { currentChatId.value } .distinctUntilChanged() .collect { if (it != null) { @@ -272,7 +378,7 @@ fun CenterPartOfScreen() { } } } - when (val id = currentChatId) { + when (currentChatId.value) { null -> { if (!rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) { Box( @@ -287,7 +393,7 @@ fun CenterPartOfScreen() { ModalManager.center.showInView() } } - else -> ChatView(id, chatModel) {} + else -> ChatView(chatsCtx = chatModel.chatsContext, currentChatId) {} } } @@ -297,65 +403,43 @@ fun EndPartOfScreen() { } @Composable -fun DesktopScreen(settingsState: SettingsViewState) { - Box { - // 56.dp is a size of unused space of settings drawer - Box(Modifier.width(DEFAULT_START_MODAL_WIDTH + 56.dp)) { - StartPartOfScreen(settingsState) - } - Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH)) { - ModalManager.start.showInView() - SwitchingUsersView() - } - Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH).clipToBounds()) { - Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) { - CenterPartOfScreen() - } - if (ModalManager.end.hasModalsOpen()) { - VerticalDivider() - } - Box(Modifier.widthIn(max = DEFAULT_END_MODAL_WIDTH).clipToBounds()) { - EndPartOfScreen() - } - } - val (userPickerState, scaffoldState ) = settingsState - val scope = rememberCoroutineScope() - if (scaffoldState.drawerState.isOpen) { - Box( - Modifier - .fillMaxSize() - .padding(start = DEFAULT_START_MODAL_WIDTH) - .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { - ModalManager.start.closeModals() - scope.launch { settingsState.scaffoldState.drawerState.close() } - }) - ) - } - VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH)) +fun DesktopScreen(userPickerState: MutableStateFlow<AnimatedViewState>) { + Box(Modifier.width(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) { + StartPartOfScreen(userPickerState) tryOrShowError("UserPicker", error = {}) { - UserPicker(chatModel, userPickerState) { - scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } - userPickerState.value = AnimatedViewState.GONE - } - } - ModalManager.fullscreen.showInView() - } -} - -@Composable -fun InitializationView() { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - CircularProgressIndicator( - Modifier - .padding(bottom = DEFAULT_PADDING) - .size(30.dp), - color = MaterialTheme.colors.secondary, - strokeWidth = 2.5.dp - ) - Text(stringResource(MR.strings.opening_database)) + UserPicker(chatModel, userPickerState, setPerformLA = AppLock::setPerformLA) } } + Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) { + ModalManager.start.showInView() + SwitchingUsersView() + } + Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) { + Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) { + CenterPartOfScreen() + } + if (ModalManager.end.hasModalsOpen()) { + VerticalDivider() + } + Box(Modifier.widthIn(max = DEFAULT_END_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) { + EndPartOfScreen() + } + } + if (userPickerState.collectAsState().value.isVisible() || (ModalManager.start.hasModalsOpen && !ModalManager.center.hasModalsOpen)) { + Box( + Modifier + .fillMaxSize() + .padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) + .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { + if (chatModel.centerPanelBackgroundClickHandler == null || chatModel.centerPanelBackgroundClickHandler?.invoke() == false) { + ModalManager.start.closeModals() + userPickerState.value = AnimatedViewState.HIDING + } + }) + ) + } + VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) + ModalManager.fullscreen.showInView() } @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt index a1d4c0c62a..d6f9640cb9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt @@ -1,11 +1,11 @@ package chat.simplex.common import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface +import androidx.compose.material.* import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.localauth.SetAppPasscodeView @@ -32,7 +32,7 @@ object AppLock { fun showLANotice(laNoticeShown: SharedPreference<Boolean>) { Log.d(TAG, "showLANotice") - if (!laNoticeShown.get()) { + if (!laNoticeShown.get() && !appPrefs.performLA.get()) { laNoticeShown.set(true) AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.la_notice_title_simplex_lock), @@ -58,6 +58,8 @@ object AppLock { private fun showChooseLAMode() { Log.d(TAG, "showLANotice") + if (appPrefs.performLA.get()) return + AlertManager.shared.showAlertDialogStacked( title = generalGetString(MR.strings.la_lock_mode), text = null, @@ -81,21 +83,23 @@ object AppLock { authenticate( generalGetString(MR.strings.auth_enable_simplex_lock), generalGetString(MR.strings.auth_confirm_credential), + oneTime = true, completed = { laResult -> when (laResult) { LAResult.Success -> { - m.performLA.value = true + m.showAuthScreen.value = true appPrefs.performLA.set(true) laTurnedOnAlert() } is LAResult.Failed -> { /* Can be called multiple times on every failure */ } is LAResult.Error -> { - m.performLA.value = false - appPrefs.performLA.set(false) + m.showAuthScreen.value = false + // Don't drop auth pref in case of state inconsistency (eg, you have set passcode but somehow bypassed toggle and turned it off and then on) + // appPrefs.performLA.set(false) laFailedAlert() } is LAResult.Unavailable -> { - m.performLA.value = false + m.showAuthScreen.value = false appPrefs.performLA.set(false) m.showAdvertiseLAUnavailableAlert.value = true } @@ -105,19 +109,22 @@ object AppLock { } private fun setPasscode() { + if (appPrefs.performLA.get()) return + val appPrefs = ChatController.appPrefs ModalManager.fullscreen.showCustomModal { close -> - Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background.copy(1f), contentColor = LocalContentColor.current) { SetAppPasscodeView( submit = { - ChatModel.performLA.value = true + ChatModel.showAuthScreen.value = true appPrefs.performLA.set(true) appPrefs.laMode.set(LAMode.PASSCODE) laTurnedOnAlert() }, cancel = { - ChatModel.performLA.value = false - appPrefs.performLA.set(false) + ChatModel.showAuthScreen.value = false + // Don't drop auth pref in case of state inconsistency (eg, you have set passcode but somehow bypassed toggle and turned it off and then on) + // appPrefs.performLA.set(false) laPasscodeNotSetAlert() }, close = close @@ -148,6 +155,7 @@ object AppLock { else generalGetString(MR.strings.auth_unlock), selfDestruct = true, + oneTime = false, completed = { laResult -> when (laResult) { LAResult.Success -> @@ -161,7 +169,7 @@ object AppLock { } is LAResult.Unavailable -> { userAuthorized.value = true - m.performLA.value = false + m.showAuthScreen.value = false m.controller.appPrefs.performLA.set(false) laUnavailableTurningOffAlert() } @@ -193,22 +201,23 @@ object AppLock { generalGetString(MR.strings.auth_confirm_credential) else "", + oneTime = true, completed = { laResult -> val prefPerformLA = m.controller.appPrefs.performLA when (laResult) { LAResult.Success -> { - m.performLA.value = true + m.showAuthScreen.value = true prefPerformLA.set(true) laTurnedOnAlert() } is LAResult.Failed -> { /* Can be called multiple times on every failure */ } is LAResult.Error -> { - m.performLA.value = false + m.showAuthScreen.value = false prefPerformLA.set(false) laFailedAlert() } is LAResult.Unavailable -> { - m.performLA.value = false + m.showAuthScreen.value = false prefPerformLA.set(false) laUnavailableInstructionAlert() } @@ -228,12 +237,13 @@ object AppLock { generalGetString(MR.strings.auth_confirm_credential) else generalGetString(MR.strings.auth_disable_simplex_lock), + oneTime = true, completed = { laResult -> val prefPerformLA = m.controller.appPrefs.performLA val selfDestructPref = m.controller.appPrefs.selfDestruct when (laResult) { LAResult.Success -> { - m.performLA.value = false + m.showAuthScreen.value = false prefPerformLA.set(false) DatabaseUtils.ksAppPassword.remove() selfDestructPref.set(false) @@ -241,12 +251,12 @@ object AppLock { } is LAResult.Failed -> { /* Can be called multiple times on every failure */ } is LAResult.Error -> { - m.performLA.value = true + m.showAuthScreen.value = true prefPerformLA.set(true) laFailedAlert() } is LAResult.Unavailable -> { - m.performLA.value = false + m.showAuthScreen.value = false prefPerformLA.set(false) laUnavailableTurningOffAlert() } 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 708bbb9073..61c20587bf 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 @@ -2,22 +2,29 @@ package chat.simplex.common.model import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateMap -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.* import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.* import androidx.compose.ui.text.style.TextDecoration +import chat.simplex.common.model.MsgFilter.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* -import chat.simplex.common.views.chat.ComposeState +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.chat.item.contentModerationPostLink +import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.migration.MigrationToDeviceState +import chat.simplex.common.views.migration.MigrationToState import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock +import kotlin.collections.removeAll as remAll import kotlinx.datetime.* import kotlinx.datetime.TimeZone import kotlinx.serialization.* @@ -25,11 +32,14 @@ import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.* +import java.io.Closeable import java.io.File import java.net.URI import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.util.* +import java.util.concurrent.atomic.AtomicLong +import kotlin.collections.ArrayList import kotlin.random.Random import kotlin.time.* @@ -47,21 +57,37 @@ object ChatModel { val chatDbChanged = mutableStateOf<Boolean>(false) val chatDbEncrypted = mutableStateOf<Boolean?>(false) val chatDbStatus = mutableStateOf<DBMigrationResult?>(null) - val chats = mutableStateListOf<Chat>() + val ctrlInitInProgress = mutableStateOf(false) + val dbMigrationInProgress = mutableStateOf(false) + val incompleteInitializedDbRemoved = mutableStateOf(false) // map of connections network statuses, key is agent connection id val networkStatuses = mutableStateMapOf<String, NetworkStatus>() val switchingUsersAndHosts = mutableStateOf(false) // current chat val chatId = mutableStateOf<String?>(null) - val chatItems = mutableStateListOf<ChatItem>() - val chatItemStatuses = mutableMapOf<Long, CIStatus>() - val groupMembers = mutableStateListOf<GroupMember>() + val openAroundItemId: MutableState<Long?> = mutableStateOf(null) + val chatsContext = ChatsContext(null) + val secondaryChatsContext = mutableStateOf<ChatsContext?>(null) + // declaration of chatsContext should be before any other variable that is taken from ChatsContext class and used in the model, otherwise, strange crash with NullPointerException for "this" parameter in random functions + val chats: State<List<Chat>> = chatsContext.chats + // rhId, chatId + val deletedChats = mutableStateOf<List<Pair<Long?, String>>>(emptyList()) + val groupMembers = mutableStateOf<List<GroupMember>>(emptyList()) + val groupMembersIndexes = mutableStateOf<Map<Long, Int>>(emptyMap()) + val membersLoaded = mutableStateOf(false) + // Chat Tags + val userTags = mutableStateOf(emptyList<ChatTag>()) + val activeChatTagFilter = mutableStateOf<ActiveFilter?>(null) + val presetTags = mutableStateMapOf<PresetTagKind, Int>() + val unreadTags = mutableStateMapOf<Long, Int>() + + // false: default placement, true: floating window. + // Used for deciding to add terminal items on main thread or not. Floating means appPrefs.terminalAlwaysVisible + var terminalsVisible = setOf<Boolean>() val terminalItems = mutableStateOf<List<TerminalItem>>(listOf()) val userAddress = mutableStateOf<UserContactLinkRec?>(null) - // Allows to temporary save servers that are being edited on multiple screens - val userSMPServersUnsaved = mutableStateOf<(List<ServerCfg>)?>(null) val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None) // set when app opened from external intent @@ -71,7 +97,13 @@ object ChatModel { val desktopOnboardingRandomPassword = mutableStateOf(false) // set when app is opened via contact or invitation URI (rhId, uri) - val appOpenUrl = mutableStateOf<Pair<Long?, URI>?>(null) + val appOpenUrl = mutableStateOf<Pair<Long?, String>?>(null) + + // Needed to check for bottom nav bar and to apply or not navigation bar color on Android + val newChatSheetVisible = mutableStateOf(false) + + // Needed to apply black color to left/right cutout area on Android + val fullscreenGalleryVisible = mutableStateOf(false) // preferences val notificationPreviewMode by lazy { @@ -83,7 +115,7 @@ object ChatModel { } ) } - val performLA by lazy { mutableStateOf(ChatController.appPrefs.performLA.get()) } + val showAuthScreen by lazy { mutableStateOf(ChatController.appPrefs.performLA.get()) } val showAdvertiseLAUnavailableAlert = mutableStateOf(false) val showChatPreviews by lazy { mutableStateOf(ChatController.appPrefs.privacyShowChatPreviews.get()) } @@ -93,23 +125,34 @@ object ChatModel { val activeCallInvitation = mutableStateOf<RcvCallInvitation?>(null) val activeCall = mutableStateOf<Call?>(null) val activeCallViewIsVisible = mutableStateOf<Boolean>(false) + val activeCallViewIsCollapsed = mutableStateOf<Boolean>(false) val callCommand = mutableStateListOf<WCallCommand>() val showCallView = mutableStateOf(false) val switchingCall = mutableStateOf(false) - // currently showing QR code - val connReqInv = mutableStateOf(null as String?) + // currently showing invitation + val showingInvitation = mutableStateOf(null as ShowingInvitation?) + + val migrationState: MutableState<MigrationToState?> by lazy { mutableStateOf(MigrationToDeviceState.makeMigrationState()) } var draft = mutableStateOf(null as ComposeState?) var draftChatId = mutableStateOf(null as String?) - // working with external intents + // working with external intents or internal forwarding of chat items val sharedContent = mutableStateOf(null as SharedContent?) val filesToDelete = mutableSetOf<File>() val simplexLinkMode by lazy { mutableStateOf(ChatController.appPrefs.simplexLinkMode.get()) } - var updatingChatsMutex: Mutex = Mutex() + val clipboardHasText = mutableStateOf(false) + val networkInfo = mutableStateOf(UserNetworkInfo(networkType = UserNetworkType.OTHER, online = true)) + + val conditions = mutableStateOf(ServerOperatorConditionsDetail.empty) + + val updatingProgress = mutableStateOf(null as Float?) + var updatingRequest: Closeable? = null + + val changingActiveUserMutex: Mutex = Mutex() val desktopNoUserNoRemote: Boolean @Composable get() = appPlatform.isDesktop && currentUser.value == null && currentRemoteHost.value == null fun desktopNoUserNoRemote(): Boolean = appPlatform.isDesktop && currentUser.value == null && currentRemoteHost.value == null @@ -122,6 +165,12 @@ object ChatModel { val remoteHostPairing = mutableStateOf<Pair<RemoteHostInfo?, RemoteHostSessionState>?>(null) val remoteCtrlSession = mutableStateOf<RemoteCtrlSession?>(null) + val processedCriticalError: ProcessedErrors<AgentErrorType.CRITICAL> = ProcessedErrors(60_000) + val processedInternalError: ProcessedErrors<AgentErrorType.INTERNAL> = ProcessedErrors(20_000) + + // return true if you handled the click + var centerPanelBackgroundClickHandler: (() -> Boolean)? = null + fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) { currentUser.value } else { @@ -143,236 +192,721 @@ object ChatModel { fun removeUser(user: User) { val i = getUserIndex(user) - if (i != -1 && users[i].user.userId != currentUser.value?.userId) { + if (i != -1) { users.removeAt(i) } } - // toList() here is to prevent ConcurrentModificationException that is rarely happens but happens - fun hasChat(rhId: Long?, id: String): Boolean = chats.toList().firstOrNull { it.id == id && it.remoteHostId == rhId } != null - // TODO pass rhId? - fun getChat(id: String): Chat? = chats.toList().firstOrNull { it.id == id } - fun getContactChat(contactId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId } - fun getGroupChat(groupId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId } - fun getGroupMember(groupMemberId: Long): GroupMember? = groupMembers.firstOrNull { it.groupMemberId == groupMemberId } - private fun getChatIndex(rhId: Long?, id: String): Int = chats.toList().indexOfFirst { it.id == id && it.remoteHostId == rhId } - fun addChat(chat: Chat) = chats.add(index = 0, chat) + fun updateChatTags(rhId: Long?) { + val newPresetTags = mutableMapOf<PresetTagKind, Int>() + val newUnreadTags = mutableMapOf<Long, Int>() - fun updateChatInfo(rhId: Long?, cInfo: ChatInfo) { - val i = getChatIndex(rhId, cInfo.id) - if (i >= 0) { - val currentCInfo = chats[i].chatInfo - var newCInfo = cInfo - if (currentCInfo is ChatInfo.Direct && newCInfo is ChatInfo.Direct) { - val currentStats = currentCInfo.contact.activeConn?.connectionStats - val newConn = newCInfo.contact.activeConn - val newStats = newConn?.connectionStats - if (currentStats != null && newConn != null && newStats == null) { - newCInfo = newCInfo.copy( - contact = newCInfo.contact.copy( - activeConn = newConn.copy( - connectionStats = currentStats + for (chat in chats.value.filter { it.remoteHostId == rhId }) { + for (tag in PresetTagKind.entries) { + if (presetTagMatchesChat(tag, chat.chatInfo, chat.chatStats)) { + newPresetTags[tag] = (newPresetTags[tag] ?: 0) + 1 + } + } + if (chat.unreadTag) { + val chatTags: List<Long> = when (val cInfo = chat.chatInfo) { + is ChatInfo.Direct -> cInfo.contact.chatTags + is ChatInfo.Group -> cInfo.groupInfo.chatTags + else -> emptyList() + } + chatTags.forEach { tag -> + newUnreadTags[tag] = (newUnreadTags[tag] ?: 0) + 1 + } + } + } + + presetTags.clear() + presetTags.putAll(newPresetTags) + unreadTags.clear() + unreadTags.putAll(newUnreadTags) + + clearActiveChatFilterIfNeeded() + } + + fun updateChatFavorite(favorite: Boolean, wasFavorite: Boolean) { + val count = presetTags[PresetTagKind.FAVORITES] + + if (favorite && !wasFavorite) { + presetTags[PresetTagKind.FAVORITES] = (count ?: 0) + 1 + } else if (!favorite && wasFavorite && count != null) { + presetTags[PresetTagKind.FAVORITES] = maxOf(0, count - 1) + clearActiveChatFilterIfNeeded() + } + } + + private fun addPresetChatTags(chatInfo: ChatInfo, chatStats: Chat.ChatStats) { + for (tag in PresetTagKind.entries) { + if (presetTagMatchesChat(tag, chatInfo, chatStats)) { + presetTags[tag] = (presetTags[tag] ?: 0) + 1 + } + } + } + + fun removePresetChatTags(chatInfo: ChatInfo, chatStats: Chat.ChatStats) { + for (tag in PresetTagKind.entries) { + if (presetTagMatchesChat(tag, chatInfo, chatStats)) { + val count = presetTags[tag] + if (count != null) { + presetTags[tag] = maxOf(0, count - 1) + } + } + } + clearActiveChatFilterIfNeeded() + } + + fun moveChatTagUnread(chat: Chat, oldTags: List<Long>?, newTags: List<Long>) { + if (chat.unreadTag) { + oldTags?.forEach { t -> + val oldCount = unreadTags[t] + if (oldCount != null) { + unreadTags[t] = maxOf(0, oldCount - 1) + } + } + + newTags.forEach { t -> + unreadTags[t] = (unreadTags[t] ?: 0) + 1 + } + } + } + // TODO pass rhId? + fun getChat(id: String): Chat? = chats.value.firstOrNull { it.id == id } + fun getContactChat(contactId: Long): Chat? = chats.value.firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId } + fun getGroupChat(groupId: Long): Chat? = chats.value.firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId } + + fun populateGroupMembersIndexes() { + groupMembersIndexes.value = emptyMap() + val gmIndexes = groupMembersIndexes.value.toMutableMap() + groupMembers.value.forEachIndexed { i, member -> + gmIndexes[member.groupMemberId] = i + } + groupMembersIndexes.value = gmIndexes + } + + fun getGroupMember(groupMemberId: Long): GroupMember? { + val memberIndex = groupMembersIndexes.value[groupMemberId] + return if (memberIndex != null) { + groupMembers.value[memberIndex] + } else { + null + } + } + + class ChatsContext(val contentTag: MsgContentTag?) { + val chats = mutableStateOf(SnapshotStateList<Chat>()) + /** if you modify the items by adding/removing them, use helpers methods like [addToChatItems], [removeLastChatItems], [removeAllAndNotify], [clearAndNotify] and so on. + * If some helper is missing, create it. Notify is needed to track state of items that we added manually (not via api call). See [apiLoadMessages]. + * If you use api call to get the items, use just [add] instead of [addToChatItems]. + * Never modify underlying list directly because it produces unexpected results in ChatView's LazyColumn (setting by index is ok) */ + val chatItems = mutableStateOf(SnapshotStateList<ChatItem>()) + val chatItemStatuses = mutableMapOf<Long, CIStatus>() + // set listener here that will be notified on every add/delete of a chat item + val chatState = ActiveChatState() + + fun hasChat(rhId: Long?, id: String): Boolean = chats.value.firstOrNull { it.id == id && it.remoteHostId == rhId } != null + fun getChat(id: String): Chat? = chats.value.firstOrNull { it.id == id } + private fun getChatIndex(rhId: Long?, id: String): Int = chats.value.indexOfFirst { it.id == id && it.remoteHostId == rhId } + + suspend fun addChat(chat: Chat) { + chats.add(index = 0, chat) + popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = 0) + } + + private suspend fun reorderChat(chat: Chat, toIndex: Int) { + val newChats = SnapshotStateList<Chat>() + newChats.addAll(chats.value) + newChats.remove(chat) + newChats.add(index = toIndex, chat) + chats.replaceAll(newChats) + popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = toIndex) + } + + fun updateChatInfo(rhId: Long?, cInfo: ChatInfo) { + val i = getChatIndex(rhId, cInfo.id) + if (i >= 0) { + val currentCInfo = chats[i].chatInfo + var newCInfo = cInfo + if (currentCInfo is ChatInfo.Direct && newCInfo is ChatInfo.Direct) { + val currentStats = currentCInfo.contact.activeConn?.connectionStats + val newConn = newCInfo.contact.activeConn + val newStats = newConn?.connectionStats + if (currentStats != null && newConn != null && newStats == null) { + newCInfo = newCInfo.copy( + contact = newCInfo.contact.copy( + activeConn = newConn.copy( + connectionStats = currentStats + ) ) ) - ) - } - } - chats[i] = chats[i].copy(chatInfo = newCInfo) - } - } - - fun updateContactConnection(rhId: Long?, contactConnection: PendingContactConnection) = updateChat(rhId, ChatInfo.ContactConnection(contactConnection)) - - fun updateContact(rhId: Long?, contact: Contact) = updateChat(rhId, ChatInfo.Direct(contact), addMissing = contact.directOrUsed) - - fun updateContactConnectionStats(rhId: Long?, contact: Contact, connectionStats: ConnectionStats) { - val updatedConn = contact.activeConn?.copy(connectionStats = connectionStats) - val updatedContact = contact.copy(activeConn = updatedConn) - updateContact(rhId, updatedContact) - } - - fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo)) - - private fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) { - if (hasChat(rhId, cInfo.id)) { - updateChatInfo(rhId, cInfo) - } else if (addMissing) { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf())) - } - } - - fun updateChats(newChats: List<Chat>) { - chats.clear() - chats.addAll(newChats) - - val cId = chatId.value - // If chat is null, it was deleted in background after apiGetChats call - if (cId != null && getChat(cId) == null) { - chatId.value = null - } - } - - fun replaceChat(rhId: Long?, id: String, chat: Chat) { - val i = getChatIndex(rhId, id) - if (i >= 0) { - chats[i] = chat - } else { - // invalid state, correcting - chats.add(index = 0, chat) - } - } - - suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) = updatingChatsMutex.withLock { - // update previews - val i = getChatIndex(rhId, cInfo.id) - val chat: Chat - if (i >= 0) { - chat = chats[i] - val newPreviewItem = when (cInfo) { - is ChatInfo.Group -> { - val currentPreviewItem = chat.chatItems.firstOrNull() - if (currentPreviewItem != null) { - if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) { - cItem - } else { - currentPreviewItem - } - } else { - cItem } } - else -> cItem + chats[i] = chats[i].copy(chatInfo = newCInfo) } - chats[i] = chat.copy( - chatItems = arrayListOf(newPreviewItem), - chatStats = + } + + fun updateChatStats(rhId: Long?, chatId: ChatId, chatStats: Chat.ChatStats) { + val i = getChatIndex(rhId, chatId) + if (i >= 0) { + chats[i] = chats[i].copy(chatStats = chatStats) + } + } + + suspend fun updateContactConnection(rhId: Long?, contactConnection: PendingContactConnection) = updateChat(rhId, ChatInfo.ContactConnection(contactConnection)) + + suspend fun updateContact(rhId: Long?, contact: Contact) = updateChat(rhId, ChatInfo.Direct(contact), addMissing = contact.directOrUsed) + + suspend fun updateContactConnectionStats(rhId: Long?, contact: Contact, connectionStats: ConnectionStats) { + val updatedConn = contact.activeConn?.copy(connectionStats = connectionStats) + val updatedContact = contact.copy(activeConn = updatedConn) + updateContact(rhId, updatedContact) + } + + suspend fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo)) + + private suspend fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) { + if (hasChat(rhId, cInfo.id)) { + updateChatInfo(rhId, cInfo) + } else if (addMissing) { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf())) + addPresetChatTags(cInfo, Chat.ChatStats()) + } + } + + fun updateChats(newChats: List<Chat>) { + chats.replaceAll(newChats) + popChatCollector.clear() + + val cId = chatId.value + // If chat is null, it was deleted in background after apiGetChats call + if (cId != null && getChat(cId) == null) { + chatId.value = null + } + } + + suspend fun replaceChat(rhId: Long?, id: String, chat: Chat) { + val i = getChatIndex(rhId, id) + if (i >= 0) { + chats[i] = chat + } else { + // invalid state, correcting + addChat(chat) + } + } + + fun addToChatItems(index: Int, elem: ChatItem) { + chatItems.value = SnapshotStateList<ChatItem>().apply { addAll(chatItems.value); add(index, elem); chatState.itemAdded(elem.id to elem.isRcvNew) } + } + + fun addToChatItems(elem: ChatItem) { + chatItems.value = SnapshotStateList<ChatItem>().apply { addAll(chatItems.value); add(elem); chatState.itemAdded(elem.id to elem.isRcvNew) } + } + + fun removeLastChatItems() { + val removed: Triple<Long, Int, Boolean> + chatItems.value = SnapshotStateList<ChatItem>().apply { + addAll(chatItems.value) + val remIndex = lastIndex + val rem = removeLast() + removed = Triple(rem.id, remIndex, rem.isRcvNew) + } + chatState.itemsRemoved(listOf(removed), chatItems.value) + } + + suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { + // mark chat non deleted + if (cInfo is ChatInfo.Direct && cInfo.chatDeleted) { + val updatedContact = cInfo.contact.copy(chatDeleted = false) + updateContact(rhId, updatedContact) + } + // update previews + val i = getChatIndex(rhId, cInfo.id) + val chat: Chat + if (i >= 0) { + chat = chats[i] + val newPreviewItem = when (cInfo) { + is ChatInfo.Group -> { + val currentPreviewItem = chat.chatItems.firstOrNull() + if (currentPreviewItem != null) { + if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) { + cItem + } else { + currentPreviewItem + } + } else { + cItem + } + } + else -> cItem + } + val wasUnread = chat.unreadTag + chats[i] = chat.copy( + chatItems = arrayListOf(newPreviewItem), + chatStats = if (cItem.meta.itemStatus is CIStatus.RcvNew) { - val minUnreadId = if(chat.chatStats.minUnreadItemId == 0L) cItem.id else chat.chatStats.minUnreadItemId increaseUnreadCounter(rhId, currentUser.value!!) - chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, minUnreadItemId = minUnreadId) + chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, unreadMentions = if (cItem.meta.userMention) chat.chatStats.unreadMentions + 1 else chat.chatStats.unreadMentions) } else chat.chatStats - ) - if (i > 0) { - popChat_(i) + ) + updateChatTagReadNoContentTag(chats[i], wasUnread) + + if (appPlatform.isDesktop && cItem.chatDir.sent) { + reorderChat(chats[i], 0) + } else { + popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = i) + } + } else { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) } - } else { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) - } - Log.d(TAG, "TODOCHAT: addChatItem: adding to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}") - withContext(Dispatchers.Main) { - // add to current chat - if (chatId.value == cInfo.id) { - Log.d(TAG, "TODOCHAT: addChatItem: chatIds are equal, size ${chatItems.size}") - // Prevent situation when chat item already in the list received from backend - if (chatItems.none { it.id == cItem.id }) { - if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { - chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem) - } else { - chatItems.add(cItem) - Log.d(TAG, "TODOCHAT: addChatItem: added to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}") + withContext(Dispatchers.Main) { + // add to current chat + if (chatId.value == cInfo.id) { + // Prevent situation when chat item already in the list received from backend + if (chatItems.value.none { it.id == cItem.id }) { + if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { + addToChatItems(kotlin.math.max(0, chatItems.value.lastIndex), cItem) + } else { + addToChatItems(cItem) + } } } } } - } - suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean = updatingChatsMutex.withLock { - // update previews - val i = getChatIndex(rhId, cInfo.id) - val chat: Chat - val res: Boolean - if (i >= 0) { - chat = chats[i] - val pItem = chat.chatItems.lastOrNull() - if (pItem?.id == cItem.id) { - chats[i] = chat.copy(chatItems = arrayListOf(cItem)) - if (pItem.isRcvNew && !cItem.isRcvNew) { - // status changed from New to Read, update counter - decreaseCounterInChat(rhId, cInfo.id) + suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean { + // update previews + val i = getChatIndex(rhId, cInfo.id) + val chat: Chat + val res: Boolean + if (i >= 0) { + chat = chats[i] + val pItem = chat.chatItems.lastOrNull() + if (pItem?.id == cItem.id) { + chats[i] = chat.copy(chatItems = arrayListOf(cItem)) + if (pItem.isRcvNew && !cItem.isRcvNew) { + // status changed from New to Read, update counter + decreaseCounterInChatNoContentTag(rhId, cInfo.id) + } + } + res = false + } else { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) + res = true + } + return withContext(Dispatchers.Main) { + // update current chat + if (chatId.value == cInfo.id) { + if (cItem.isDeletedContent || cItem.meta.itemDeleted != null) { + AudioPlayer.stop(cItem) + } + val items = chatItems.value + val itemIndex = items.indexOfFirst { it.id == cItem.id } + if (itemIndex >= 0) { + items[itemIndex] = cItem + false + } else { + val status = chatItemStatuses.remove(cItem.id) + val ci = if (status != null && cItem.meta.itemStatus is CIStatus.SndNew) { + cItem.copy(meta = cItem.meta.copy(itemStatus = status)) + } else { + cItem + } + addToChatItems(ci) + true + } + } else { + res } } - res = false - } else { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) - res = true } - Log.d(TAG, "TODOCHAT: upsertChatItem: upserting to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}") - return withContext(Dispatchers.Main) { - // update current chat + + suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null, atIndex: Int? = null) { + withContext(Dispatchers.Main) { + if (chatId.value == cInfo.id) { + val items = chatItems.value + val itemIndex = atIndex ?: items.indexOfFirst { it.id == cItem.id } + if (itemIndex >= 0) { + items[itemIndex] = cItem + } + } else if (status != null) { + chatItemStatuses[cItem.id] = status + } + } + } + + fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { + if (cItem.isRcvNew) { + decreaseCounterInChatNoContentTag(rhId, cInfo.id) + } + // update previews + val i = getChatIndex(rhId, cInfo.id) + val chat: Chat + if (i >= 0) { + chat = chats[i] + val pItem = chat.chatItems.lastOrNull() + if (pItem?.id == cItem.id) { + chats[i] = chat.copy(chatItems = arrayListOf(ChatItem.deletedItemDummy)) + } + } + // remove from current chat if (chatId.value == cInfo.id) { - val itemIndex = chatItems.indexOfFirst { it.id == cItem.id } - if (itemIndex >= 0) { - chatItems[itemIndex] = cItem - Log.d(TAG, "TODOCHAT: upsertChatItem: updated in chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}") + chatItems.removeAllAndNotify { + // We delete taking into account meta.createdAt to make sure we will not be in situation when two items with the same id will be deleted + // (it can happen if already deleted chat item in backend still in the list and new one came with the same (re-used) chat item id) + val remove = it.id == cItem.id && it.meta.createdAt == cItem.meta.createdAt + if (remove) { AudioPlayer.stop(it) } + remove + } + } + } + + suspend fun removeMemberItems(rhId: Long?, removedMember: GroupMember, byMember: GroupMember, groupInfo: GroupInfo) { + fun removedUpdatedItem(item: ChatItem): ChatItem? { + val newContent = when { + item.chatDir is CIDirection.GroupSnd && removedMember.groupMemberId == groupInfo.membership.groupMemberId -> CIContent.SndModerated + item.chatDir is CIDirection.GroupRcv && item.chatDir.groupMember.groupMemberId == removedMember.groupMemberId -> CIContent.RcvModerated + else -> return null + } + val updatedItem = item.copy( + meta = item.meta.copy(itemDeleted = CIDeleted.Moderated(Clock.System.now(), byGroupMember = byMember)), + content = if (groupInfo.fullGroupPreferences.fullDelete.on) newContent else item.content + ) + if (item.isActiveReport) { + decreaseGroupReportsCounter(rhId, groupInfo.id) + } + return updatedItem + } + + // this should not happen, only another member can "remove" user, user can only "leave" (another event). + if (byMember.groupMemberId == groupInfo.membership.groupMemberId) { + Log.d(TAG, "exiting removeMemberItems") + return + } + val cInfo = ChatInfo.Group(groupInfo) + if (chatId.value == groupInfo.id) { + for (i in 0 until chatItems.value.size) { + val updatedItem = removedUpdatedItem(chatItems.value[i]) + if (updatedItem != null) { + updateChatItem(cInfo, updatedItem, atIndex = i) + } + } + } else { + val i = getChatIndex(rhId, groupInfo.id) + val chat = chats[i] + if (chat.chatItems.isNotEmpty()) { + val updatedItem = removedUpdatedItem(chat.chatItems[0]) + if (updatedItem != null) { + chats.value[i] = chat.copy(chatItems = listOf(updatedItem)) + } + } + } + } + + fun clearChat(rhId: Long?, cInfo: ChatInfo) { + // clear preview + val i = getChatIndex(rhId, cInfo.id) + if (i >= 0) { + decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount) + val chatBefore = chats[i] + chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo) + markChatTagRead(chatBefore) + } + // clear current chat + if (chatId.value == cInfo.id) { + chatItemStatuses.clear() + chatItems.clearAndNotify() + } + } + + val popChatCollector = PopChatCollector(this) + + // TODO [contexts] no reason for this to be nested? + class PopChatCollector(chatsCtx: ChatsContext) { + private val subject = MutableSharedFlow<Unit>() + private var remoteHostId: Long? = null + private val chatsToPop = mutableMapOf<ChatId, Instant>() + + init { + withLongRunningApi { + subject + .throttleLatest(2000) + .collect { + withContext(Dispatchers.Main) { + chatsCtx.chats.replaceAll(popCollectedChats()) + } + } + } + } + + suspend fun throttlePopChat(rhId: Long?, chatId: ChatId, currentPosition: Int) { + if (rhId != remoteHostId) { + chatsToPop.clear() + remoteHostId = rhId + } + if (currentPosition > 0 || chatsToPop.isNotEmpty()) { + chatsToPop[chatId] = Clock.System.now() + subject.emit(Unit) + } + } + + fun clear() = chatsToPop.clear() + + private fun popCollectedChats(): List<Chat> { + val chs = mutableListOf<Chat>() + // collect chats that received updates + for ((chatId, popTs) in chatsToPop.entries) { + val ch = getChat(chatId) + if (ch != null) { + ch.popTs = popTs + chs.add(ch) + } + } + // sort chats by pop timestamp in descending order + val newChats = ArrayList(chs.sortedByDescending { it.popTs }) + newChats.addAll(chats.value.filter { !chatsToPop.containsKey(it.chatInfo.id) } ) + chatsToPop.clear() + return newChats + } + } + + fun markChatItemsRead(remoteHostId: Long?, id: ChatId, itemIds: List<Long>? = null) { + val (markedRead, mentionsMarkedRead) = markItemsReadInCurrentChat(id, itemIds) + // update preview + val chatIdx = getChatIndex(remoteHostId, id) + if (chatIdx >= 0) { + val chat = chats[chatIdx] + val lastId = chat.chatItems.lastOrNull()?.id + if (lastId != null) { + val wasUnread = chat.unreadTag + val unreadCount = if (itemIds != null) chat.chatStats.unreadCount - markedRead else 0 + val unreadMentions = if (itemIds != null) chat.chatStats.unreadMentions - mentionsMarkedRead else 0 + decreaseUnreadCounter(remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount) + chats[chatIdx] = chat.copy( + chatStats = chat.chatStats.copy(unreadCount = unreadCount, unreadMentions = unreadMentions) + ) + updateChatTagReadNoContentTag(chats[chatIdx], wasUnread) + } + } + } + + private fun markItemsReadInCurrentChat(id: ChatId, itemIds: List<Long>? = null): Pair<Int, Int> { + var markedRead = 0 + var mentionsMarkedRead = 0 + if (chatId.value == id) { + val items = chatItems.value + var i = items.lastIndex + val itemIdsFromRange = itemIds?.toMutableSet() ?: mutableSetOf() + val markedReadIds = mutableSetOf<Long>() + while (i >= 0) { + val item = items[i] + if (item.meta.itemStatus is CIStatus.RcvNew && (itemIds == null || itemIdsFromRange.contains(item.id))) { + val newItem = item.withStatus(CIStatus.RcvRead()) + items[i] = newItem + if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) { + items[i] = newItem.copy(meta = newItem.meta.copy(itemTimed = newItem.meta.itemTimed.copy( + deleteAt = Clock.System.now() + newItem.meta.itemTimed.ttl.toDuration(DurationUnit.SECONDS))) + ) + } + markedReadIds.add(item.id) + markedRead++ + if (item.meta.userMention) { + mentionsMarkedRead++ + } + if (itemIds != null) { + itemIdsFromRange.remove(item.id) + // already set all needed items as read, can finish the loop + if (itemIdsFromRange.isEmpty()) break + } + } + i-- + } + chatState.itemsRead(if (itemIds != null) markedReadIds else null, items) + } + return markedRead to mentionsMarkedRead + } + + private fun decreaseCounterInChatNoContentTag(rhId: Long?, chatId: ChatId) { + // updates anything only in main ChatView, not GroupReportsView or anything else from the future + if (contentTag != null) return + + val chatIndex = getChatIndex(rhId, chatId) + if (chatIndex == -1) return + + val chat = chats[chatIndex] + val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0) + val wasUnread = chat.unreadTag + decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount) + chats[chatIndex] = chat.copy( + chatStats = chat.chatStats.copy( + unreadCount = unreadCount, + ) + ) + updateChatTagReadNoContentTag(chats[chatIndex], wasUnread) + } + + fun removeChat(rhId: Long?, id: String) { + val i = getChatIndex(rhId, id) + if (i != -1) { + val chat = chats.removeAt(i) + removePresetChatTags(chat.chatInfo, chat.chatStats) + removeWallpaperFilesFromChat(chat) + } + } + + suspend fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean { + // user member was updated + if (groupInfo.membership.groupMemberId == member.groupMemberId) { + updateGroup(rhId, groupInfo) + return false + } + // update current chat + return if (chatId.value == groupInfo.id) { + if (groupMembers.value.isNotEmpty() && groupMembers.value.firstOrNull()?.groupId != groupInfo.groupId) { + // stale data, should be cleared at that point, otherwise, duplicated items will be here which will produce crashes in LazyColumn + groupMembers.value = emptyList() + groupMembersIndexes.value = emptyMap() + } + val memberIndex = groupMembersIndexes.value[member.groupMemberId] + val updated = chatItems.value.map { + // Take into account only specific changes, not all. Other member updates are not important and can be skipped + if (it.chatDir is CIDirection.GroupRcv && it.chatDir.groupMember.groupMemberId == member.groupMemberId && + (it.chatDir.groupMember.image != member.image || + it.chatDir.groupMember.chatViewName != member.chatViewName || + it.chatDir.groupMember.blocked != member.blocked || + it.chatDir.groupMember.memberRole != member.memberRole) + ) + it.copy(chatDir = CIDirection.GroupRcv(member)) + else + it + } + if (updated != chatItems.value) { + chatItems.replaceAll(updated) + } + val gMembers = groupMembers.value.toMutableList() + if (memberIndex != null) { + gMembers[memberIndex] = member + groupMembers.value = gMembers false } else { - val status = chatItemStatuses.remove(cItem.id) - val ci = if (status != null && cItem.meta.itemStatus is CIStatus.SndNew) { - cItem.copy(meta = cItem.meta.copy(itemStatus = status)) - } else { - cItem - } - chatItems.add(ci) - Log.d(TAG, "TODOCHAT: upsertChatItem: added to chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}") + gMembers.add(member) + groupMembers.value = gMembers + val gmIndexes = groupMembersIndexes.value.toMutableMap() + gmIndexes[member.groupMemberId] = groupMembers.size - 1 + groupMembersIndexes.value = gmIndexes true } } else { - res + false } } - } - suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) { - withContext(Dispatchers.Main) { - if (chatId.value == cInfo.id) { - val itemIndex = chatItems.indexOfFirst { it.id == cItem.id } - if (itemIndex >= 0) { - chatItems[itemIndex] = cItem + suspend fun updateGroupMemberConnectionStats(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) { + val memberConn = member.activeConn + if (memberConn != null) { + val updatedConn = memberConn.copy(connectionStats = connectionStats) + val updatedMember = member.copy(activeConn = updatedConn) + upsertGroupMember(rhId, groupInfo, updatedMember) + } + } + + fun increaseUnreadCounter(rhId: Long?, user: UserLike) { + changeUnreadCounterNoContentTag(rhId, user, 1) + } + + fun decreaseUnreadCounter(rhId: Long?, user: UserLike, by: Int = 1) { + changeUnreadCounterNoContentTag(rhId, user, -by) + } + + private fun changeUnreadCounterNoContentTag(rhId: Long?, user: UserLike, by: Int) { + // updates anything only in main ChatView, not GroupReportsView or anything else from the future + if (contentTag != null) return + + val i = users.indexOfFirst { it.user.userId == user.userId && it.user.remoteHostId == rhId } + if (i != -1) { + users[i] = users[i].copy(unreadCount = users[i].unreadCount + by) + } + } + + fun updateChatTagReadNoContentTag(chat: Chat, wasUnread: Boolean) { + // updates anything only in main ChatView, not GroupReportsView or anything else from the future + if (contentTag != null) return + + val tags = chat.chatInfo.chatTags ?: return + val nowUnread = chat.unreadTag + + if (nowUnread && !wasUnread) { + tags.forEach { tag -> + unreadTags[tag] = (unreadTags[tag] ?: 0) + 1 } - } else if (status != null) { - chatItemStatuses[cItem.id] = status + } else if (!nowUnread && wasUnread) { + markChatTagReadNoContentTag_(chat, tags) } } + + fun markChatTagRead(chat: Chat) { + if (chat.unreadTag) { + chat.chatInfo.chatTags?.let { tags -> + markChatTagReadNoContentTag_(chat, tags) + } + } + } + + private fun markChatTagReadNoContentTag_(chat: Chat, tags: List<Long>) { + // updates anything only in main ChatView, not GroupReportsView or anything else from the future + if (contentTag != null) return + + for (tag in tags) { + val count = unreadTags[tag] + if (count != null) { + unreadTags[tag] = maxOf(0, count - 1) + } + } + } + + fun increaseGroupReportsCounter(rhId: Long?, chatId: ChatId) { + changeGroupReportsCounter(rhId, chatId, 1) + } + + fun decreaseGroupReportsCounter(rhId: Long?, chatId: ChatId, by: Int = 1) { + changeGroupReportsCounter(rhId, chatId, -by) + } + + private fun changeGroupReportsCounter(rhId: Long?, chatId: ChatId, by: Int = 0) { + if (by == 0) return + + val i = getChatIndex(rhId, chatId) + if (i >= 0) { + val chat = chats.value[i] + chats[i] = chat.copy( + chatStats = chat.chatStats.copy( + reportsCount = (chat.chatStats.reportsCount + by).coerceAtLeast(0), + ) + ) + val wasReportsCount = chat.chatStats.reportsCount + val nowReportsCount = chats[i].chatStats.reportsCount + val by = if (wasReportsCount == 0 && nowReportsCount > 0) 1 else if (wasReportsCount > 0 && nowReportsCount == 0) -1 else 0 + changeGroupReportsTagNoContentTag(by) + } + } + + private fun changeGroupReportsTagNoContentTag(by: Int = 0) { + if (by == 0 || contentTag != null) return + presetTags[PresetTagKind.GROUP_REPORTS] = kotlin.math.max(0, (presetTags[PresetTagKind.GROUP_REPORTS] ?: 0) + by) + clearActiveChatFilterIfNeeded() + } } - fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { - if (cItem.isRcvNew) { - decreaseCounterInChat(rhId, cInfo.id) - } - // update previews - val i = getChatIndex(rhId, cInfo.id) - val chat: Chat - if (i >= 0) { - chat = chats[i] - val pItem = chat.chatItems.lastOrNull() - if (pItem?.id == cItem.id) { - chats[i] = chat.copy(chatItems = arrayListOf(ChatItem.deletedItemDummy)) - } - } - // remove from current chat - if (chatId.value == cInfo.id) { - val itemIndex = chatItems.indexOfFirst { it.id == cItem.id } - if (itemIndex >= 0) { - AudioPlayer.stop(chatItems[itemIndex]) - chatItems.removeAt(itemIndex) - } - } - } - - fun clearChat(rhId: Long?, cInfo: ChatInfo) { - // clear preview - val i = getChatIndex(rhId, cInfo.id) - if (i >= 0) { - decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount) - chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo) - } - // clear current chat - if (chatId.value == cInfo.id) { - chatItemStatuses.clear() - chatItems.clear() + fun clearActiveChatFilterIfNeeded() { + val clear = when(val f = activeChatTagFilter.value) { + is ActiveFilter.PresetTag -> (presetTags[f.tag] ?: 0) == 0 + is ActiveFilter.UserTag -> userTags.value.none { it.chatTagId == f.tag.chatTagId } + is ActiveFilter.Unread, null -> false } + if (clear) activeChatTagFilter.value = null } fun updateCurrentUser(rhId: Long?, newProfile: Profile, preferences: FullChatPreferences? = null) { @@ -388,109 +922,47 @@ object ChatModel { currentUser.value = updated } + fun updateCurrentUserUiThemes(rhId: Long?, uiThemes: ThemeModeOverrides?) { + val current = currentUser.value ?: return + val updated = current.copy( + uiThemes = uiThemes + ) + val i = users.indexOfFirst { it.user.userId == current.userId && it.user.remoteHostId == rhId } + if (i != -1) { + users[i] = users[i].copy(user = updated) + } + currentUser.value = updated + } + suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem { val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct) withContext(Dispatchers.Main) { - chatItems.add(cItem) + chatsContext.addToChatItems(cItem) } return cItem } fun removeLiveDummy() { - if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { - chatItems.removeLast() - } - } - - fun markChatItemsRead(chat: Chat, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) { - val cInfo = chat.chatInfo - val markedRead = markItemsReadInCurrentChat(chat, range) - // update preview - val chatIdx = getChatIndex(chat.remoteHostId, cInfo.id) - if (chatIdx >= 0) { - val chat = chats[chatIdx] - val lastId = chat.chatItems.lastOrNull()?.id - if (lastId != null) { - val unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0 - decreaseUnreadCounter(chat.remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount) - chats[chatIdx] = chat.copy( - chatStats = chat.chatStats.copy( - unreadCount = unreadCount, - // Can't use minUnreadItemId currently since chat items can have unread items between read items - //minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1 - ) - ) - } - } - } - - private fun markItemsReadInCurrentChat(chat: Chat, range: CC.ItemRange? = null): Int { - val cInfo = chat.chatInfo - var markedRead = 0 - if (chatId.value == cInfo.id) { - var i = 0 - Log.d(TAG, "TODOCHAT: markItemsReadInCurrentChat: marking read ${cInfo.id}, current chatId ${chatId.value}, size was ${chatItems.size}") - while (i < chatItems.count()) { - val item = chatItems[i] - if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || (range.from <= item.id && item.id <= range.to))) { - val newItem = item.withStatus(CIStatus.RcvRead()) - chatItems[i] = newItem - if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) { - chatItems[i] = newItem.copy(meta = newItem.meta.copy(itemTimed = newItem.meta.itemTimed.copy( - deleteAt = Clock.System.now() + newItem.meta.itemTimed.ttl.toDuration(DurationUnit.SECONDS))) - ) - } - markedRead++ + if (chatsContext.chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { + withApi { + withContext(Dispatchers.Main) { + chatsContext.removeLastChatItems() } - i += 1 } - Log.d(TAG, "TODOCHAT: markItemsReadInCurrentChat: marked read ${cInfo.id}, current chatId ${chatId.value}, size now ${chatItems.size}") - } - return markedRead - } - - private fun decreaseCounterInChat(rhId: Long?, chatId: ChatId) { - val chatIndex = getChatIndex(rhId, chatId) - if (chatIndex == -1) return - - val chat = chats[chatIndex] - val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0) - decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount) - chats[chatIndex] = chat.copy( - chatStats = chat.chatStats.copy( - unreadCount = unreadCount, - ) - ) - } - - fun increaseUnreadCounter(rhId: Long?, user: UserLike) { - changeUnreadCounter(rhId, user, 1) - } - - fun decreaseUnreadCounter(rhId: Long?, user: UserLike, by: Int = 1) { - changeUnreadCounter(rhId, user, -by) - } - - private fun changeUnreadCounter(rhId: Long?, user: UserLike, by: Int) { - val i = users.indexOfFirst { it.user.userId == user.userId && it.user.remoteHostId == rhId } - if (i != -1) { - users[i] = users[i].copy(unreadCount = users[i].unreadCount + by) } } - fun getChatItemIndexOrNull(cItem: ChatItem): Int? { - val reversedChatItems = chatItems.asReversed() + fun getChatItemIndexOrNull(cItem: ChatItem, reversedChatItems: List<ChatItem>): Int? { val index = reversedChatItems.indexOfFirst { it.id == cItem.id } return if (index != -1) index else null } // this function analyses "connected" events and assumes that each member will be there only once - fun getConnectedMemberNames(cItem: ChatItem): Pair<Int, List<String>> { + fun getConnectedMemberNames(cItem: ChatItem, reversedChatItems: List<ChatItem>): Pair<Int, List<String>> { var count = 0 val ns = mutableListOf<String>() - var idx = getChatItemIndexOrNull(cItem) + var idx = getChatItemIndexOrNull(cItem, reversedChatItems) if (cItem.mergeCategory != null && idx != null) { - val reversedChatItems = chatItems.asReversed() while (idx < reversedChatItems.size) { val ci = reversedChatItems[idx] if (ci.mergeCategory != cItem.mergeCategory) break @@ -505,22 +977,10 @@ object ChatModel { return count to ns } - // returns the index of the passed item and the next item (it has smaller index) - fun getNextChatItem(ci: ChatItem): Pair<Int?, ChatItem?> { - val i = getChatItemIndexOrNull(ci) - return if (i != null) { - val reversedChatItems = chatItems.asReversed() - i to if (i > 0) reversedChatItems[i - 1] else null - } else { - null to null - } - } - // returns the index of the first item in the same merged group (the first hidden item) // and the previous visible item with another merge category - fun getPrevShownChatItem(ciIndex: Int?, ciCategory: CIMergeCategory?): Pair<Int?, ChatItem?> { + fun getPrevShownChatItem(ciIndex: Int?, ciCategory: CIMergeCategory?, reversedChatItems: List<ChatItem>): Pair<Int?, ChatItem?> { var i = ciIndex ?: return null to null - val reversedChatItems = chatItems.asReversed() val fst = reversedChatItems.lastIndex while (i < fst) { i++ @@ -533,8 +993,7 @@ object ChatModel { } // returns the previous member in the same merge group and the count of members in this group - fun getPrevHiddenMember(member: GroupMember, range: IntRange): Pair<GroupMember?, Int> { - val reversedChatItems = chatItems.asReversed() + fun getPrevHiddenMember(member: GroupMember, range: IntRange, reversedChatItems: List<ChatItem>): Pair<GroupMember?, Int> { var prevMember: GroupMember? = null val names: MutableSet<Long> = mutableSetOf() for (i in range) { @@ -556,54 +1015,38 @@ object ChatModel { // } // } - private fun popChat_(i: Int) { - val chat = chats.removeAt(i) - chats.add(index = 0, chat) - } - - fun dismissConnReqView(id: String) { - if (connReqInv.value == null) return - val info = getChat(id)?.chatInfo as? ChatInfo.ContactConnection ?: return - if (info.contactConnection.connReqInv == connReqInv.value) { - connReqInv.value = null - ModalManager.center.closeModals() - } - } - - fun removeChat(rhId: Long?, id: String) { - chats.removeAll { it.id == id && it.remoteHostId == rhId } - } - - fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean { - // user member was updated - if (groupInfo.membership.groupMemberId == member.groupMemberId) { - updateGroup(rhId, groupInfo) - return false - } - // update current chat - return if (chatId.value == groupInfo.id) { - val memberIndex = groupMembers.indexOfFirst { it.groupMemberId == member.groupMemberId } - if (memberIndex >= 0) { - groupMembers[memberIndex] = member - false - } else { - groupMembers.add(member) - true + fun replaceConnReqView(id: String, withId: String) { + if (id == showingInvitation.value?.connId) { + withApi { + withContext(Dispatchers.Main) { + showingInvitation.value = null + chatsContext.chatItems.clearAndNotify() + chatModel.chatId.value = withId + } } - } else { - false + ModalManager.start.closeModals() + ModalManager.end.closeModals() } } - fun updateGroupMemberConnectionStats(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) { - val memberConn = member.activeConn - if (memberConn != null) { - val updatedConn = memberConn.copy(connectionStats = connectionStats) - val updatedMember = member.copy(activeConn = updatedConn) - upsertGroupMember(rhId, groupInfo, updatedMember) + fun dismissConnReqView(id: String) = withApi { + if (id == showingInvitation.value?.connId) { + withContext(Dispatchers.Main) { + showingInvitation.value = null + chatsContext.chatItems.clearAndNotify() + chatModel.chatId.value = null + } + // Close NewChatView + ModalManager.start.closeModals() + ModalManager.center.closeModals() + ModalManager.end.closeModals() } } + fun markShowingInvitationUsed() { + showingInvitation.value = showingInvitation.value?.copy(connChatUsed = true) + } + fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) { val conn = contact.activeConn if (conn != null) { @@ -620,7 +1063,18 @@ object ChatModel { } fun addTerminalItem(item: TerminalItem) { - if (terminalItems.value.size >= 500) { + val maxItems = if (appPreferences.developerTools.get()) 500 else 200 + if (terminalsVisible.isNotEmpty()) { + withApi { + addTerminalItem(item, maxItems) + } + } else { + addTerminalItem(item, maxItems) + } + } + + private fun addTerminalItem(item: TerminalItem, maxItems: Int) { + if (terminalItems.value.size >= maxItems) { terminalItems.value = terminalItems.value.subList(1, terminalItems.value.size) } terminalItems.value += item @@ -630,9 +1084,17 @@ object ChatModel { fun connectedToRemote(): Boolean = currentRemoteHost.value != null || remoteCtrlSession.value?.active == true } +data class ShowingInvitation( + val connId: String, + val connLink: CreatedConnLink, + val connChatUsed: Boolean, + val conn: PendingContactConnection +) + enum class ChatType(val type: String) { Direct("@"), Group("#"), + Local("*"), ContactRequest("<@"), ContactConnection(":"); } @@ -646,10 +1108,12 @@ data class User( val profile: LocalProfile, val fullPreferences: FullChatPreferences, override val activeUser: Boolean, + val activeOrder: Long, override val showNtfs: Boolean, val sendRcptsContacts: Boolean, val sendRcptsSmallGroups: Boolean, - val viewPwdHash: UserPwdHash? + val viewPwdHash: UserPwdHash?, + val uiThemes: ThemeModeOverrides? = null, ): NamedChat, UserLike { override val displayName: String get() = profile.displayName override val fullName: String get() = profile.fullName @@ -672,10 +1136,12 @@ data class User( profile = LocalProfile.sampleData, fullPreferences = FullChatPreferences.sampleData, activeUser = true, + activeOrder = 0, showNtfs = true, sendRcptsContacts = true, sendRcptsSmallGroups = false, viewPwdHash = null, + uiThemes = null, ) } } @@ -724,6 +1190,11 @@ interface NamedChat { val localAlias: String val chatViewName: String get() = localAlias.ifEmpty { displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") } + + fun anyNameContains(searchAnyCase: String): Boolean { + val s = searchAnyCase.trim().lowercase() + return chatViewName.lowercase().contains(s) || displayName.lowercase().contains(s) || fullName.lowercase().contains(s) + } } interface SomeChat { @@ -732,8 +1203,9 @@ interface SomeChat { val id: ChatId val apiId: Long val ready: Boolean + val chatDeleted: Boolean + val userCantSendReason: Pair<String, String?>? val sendMsgEnabled: Boolean - val ntfsEnabled: Boolean val incognito: Boolean fun featureEnabled(feature: ChatFeature): Boolean val timedMessagesTTL: Int? @@ -748,12 +1220,8 @@ data class Chat( val chatItems: List<ChatItem>, val chatStats: ChatStats = ChatStats() ) { - val userCanSend: Boolean - get() = when (chatInfo) { - is ChatInfo.Direct -> true - is ChatInfo.Group -> chatInfo.groupInfo.membership.memberRole >= GroupMemberRole.Member - else -> false - } + @Transient + var popTs: Instant? = null val nextSendGrpInv: Boolean get() = when (chatInfo) { @@ -761,18 +1229,31 @@ data class Chat( else -> false } - val userIsObserver: Boolean get() = when(chatInfo) { - is ChatInfo.Group -> { - val m = chatInfo.groupInfo.membership - m.memberActive && m.memberRole == GroupMemberRole.Observer - } - else -> false + val unreadTag: Boolean get() = when (chatInfo.chatSettings?.enableNtfs) { + All -> chatStats.unreadChat || chatStats.unreadCount > 0 + Mentions -> chatStats.unreadChat || chatStats.unreadMentions > 0 + else -> chatStats.unreadChat } val id: String get() = chatInfo.id + fun groupFeatureEnabled(feature: GroupFeature): Boolean = + if (chatInfo is ChatInfo.Group) { + chatInfo.groupInfo.groupFeatureEnabled(feature) + } else { + true + } + @Serializable - data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0, val unreadChat: Boolean = false) + data class ChatStats( + val unreadCount: Int = 0, + val unreadMentions: Int = 0, + // actual only via getChats() and getChat(.initial), otherwise, zero + val reportsCount: Int = 0, + val minUnreadItemId: Long = 0, + // actual only via getChats(), otherwise, false + val unreadChat: Boolean = false + ) companion object { val sampleData = Chat( @@ -793,8 +1274,9 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = contact.id override val apiId get() = contact.apiId override val ready get() = contact.ready + override val chatDeleted get() = contact.chatDeleted + override val userCantSendReason get() = contact.userCantSendReason override val sendMsgEnabled get() = contact.sendMsgEnabled - override val ntfsEnabled get() = contact.ntfsEnabled override val incognito get() = contact.incognito override fun featureEnabled(feature: ChatFeature) = contact.featureEnabled(feature) override val timedMessagesTTL: Int? get() = contact.timedMessagesTTL @@ -804,6 +1286,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val fullName get() = contact.fullName override val image get() = contact.image override val localAlias: String get() = contact.localAlias + override fun anyNameContains(searchAnyCase: String): Boolean = contact.anyNameContains(searchAnyCase) companion object { val sampleData = Direct(Contact.sampleData) @@ -817,8 +1300,9 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = groupInfo.id override val apiId get() = groupInfo.apiId override val ready get() = groupInfo.ready + override val chatDeleted get() = groupInfo.chatDeleted + override val userCantSendReason get() = groupInfo.userCantSendReason override val sendMsgEnabled get() = groupInfo.sendMsgEnabled - override val ntfsEnabled get() = groupInfo.ntfsEnabled override val incognito get() = groupInfo.incognito override fun featureEnabled(feature: ChatFeature) = groupInfo.featureEnabled(feature) override val timedMessagesTTL: Int? get() = groupInfo.timedMessagesTTL @@ -834,6 +1318,31 @@ sealed class ChatInfo: SomeChat, NamedChat { } } + @Serializable @SerialName("local") + data class Local(val noteFolder: NoteFolder): ChatInfo() { + override val chatType get() = ChatType.Local + override val localDisplayName get() = noteFolder.localDisplayName + override val id get() = noteFolder.id + override val apiId get() = noteFolder.apiId + override val ready get() = noteFolder.ready + override val chatDeleted get() = noteFolder.chatDeleted + override val userCantSendReason get() = noteFolder.userCantSendReason + override val sendMsgEnabled get() = noteFolder.sendMsgEnabled + override val incognito get() = noteFolder.incognito + override fun featureEnabled(feature: ChatFeature) = noteFolder.featureEnabled(feature) + override val timedMessagesTTL: Int? get() = noteFolder.timedMessagesTTL + override val createdAt get() = noteFolder.createdAt + override val updatedAt get() = noteFolder.updatedAt + override val displayName get() = noteFolder.displayName + override val fullName get() = noteFolder.fullName + override val image get() = noteFolder.image + override val localAlias get() = noteFolder.localAlias + + companion object { + val sampleData = Local(NoteFolder.sampleData) + } + } + @Serializable @SerialName("contactRequest") class ContactRequest(val contactRequest: UserContactRequest): ChatInfo() { override val chatType get() = ChatType.ContactRequest @@ -841,8 +1350,9 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = contactRequest.id override val apiId get() = contactRequest.apiId override val ready get() = contactRequest.ready + override val chatDeleted get() = contactRequest.chatDeleted + override val userCantSendReason get() = contactRequest.userCantSendReason override val sendMsgEnabled get() = contactRequest.sendMsgEnabled - override val ntfsEnabled get() = contactRequest.ntfsEnabled override val incognito get() = contactRequest.incognito override fun featureEnabled(feature: ChatFeature) = contactRequest.featureEnabled(feature) override val timedMessagesTTL: Int? get() = contactRequest.timedMessagesTTL @@ -865,8 +1375,9 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = contactConnection.id override val apiId get() = contactConnection.apiId override val ready get() = contactConnection.ready + override val chatDeleted get() = contactConnection.chatDeleted + override val userCantSendReason get() = contactConnection.userCantSendReason override val sendMsgEnabled get() = contactConnection.sendMsgEnabled - override val ntfsEnabled get() = false override val incognito get() = contactConnection.incognito override fun featureEnabled(feature: ChatFeature) = contactConnection.featureEnabled(feature) override val timedMessagesTTL: Int? get() = contactConnection.timedMessagesTTL @@ -884,19 +1395,22 @@ sealed class ChatInfo: SomeChat, NamedChat { } @Serializable @SerialName("invalidJSON") - class InvalidJSON(val json: String): ChatInfo() { + class InvalidJSON( + val json: String, + override val apiId: Long = -idGenerator.getAndIncrement(), + override val createdAt: Instant = Clock.System.now(), + override val updatedAt: Instant = Clock.System.now() + ): ChatInfo() { override val chatType get() = ChatType.Direct override val localDisplayName get() = invalidChatName - override val id get() = "" - override val apiId get() = 0L + override val id get() = "?$apiId" override val ready get() = false + override val chatDeleted get() = false + override val userCantSendReason get() = generalGetString(MR.strings.cant_send_message_generic) to null override val sendMsgEnabled get() = false - override val ntfsEnabled get() = false override val incognito get() = false override fun featureEnabled(feature: ChatFeature) = false override val timedMessagesTTL: Int? get() = null - override val createdAt get() = Clock.System.now() - override val updatedAt get() = Clock.System.now() override val displayName get() = invalidChatName override val fullName get() = invalidChatName override val image get() = null @@ -904,16 +1418,54 @@ sealed class ChatInfo: SomeChat, NamedChat { companion object { private val invalidChatName = generalGetString(MR.strings.invalid_chat) + private val idGenerator = AtomicLong(0) } } + fun ntfsEnabled(ci: ChatItem): Boolean = + ntfsEnabled(ci.meta.userMention) + + fun ntfsEnabled(userMention: Boolean): Boolean = + when (chatSettings?.enableNtfs) { + All -> true + Mentions -> userMention + else -> false + } + val chatSettings get() = when(this) { is Direct -> contact.chatSettings is Group -> groupInfo.chatSettings else -> null } -} + + val chatTs: Instant + get() = when(this) { + is Direct -> contact.chatTs ?: contact.updatedAt + is Group -> groupInfo.chatTs ?: groupInfo.updatedAt + is Local -> noteFolder.chatTs + is ContactRequest -> contactRequest.updatedAt + is ContactConnection -> contactConnection.updatedAt + is InvalidJSON -> updatedAt + } + + val chatTags: List<Long>? + get() = when (this) { + is Direct -> contact.chatTags + is Group -> groupInfo.chatTags + else -> null + } + + val nextNtfMode: MsgFilter? get() = this.chatSettings?.enableNtfs?.nextMode(mentions = this.hasMentions) + + val hasMentions: Boolean get() = this is Group + + val contactCard: Boolean + get() = when (this) { + is Direct -> contact.activeConn == null && contact.profile.contactLink != null && contact.active + else -> false + } + } @Serializable sealed class NetworkStatus { @@ -953,19 +1505,32 @@ data class Contact( val mergedPreferences: ContactUserPreferences, override val createdAt: Instant, override val updatedAt: Instant, + val chatTs: Instant?, val contactGroupMemberId: Long? = null, - val contactGrpInvSent: Boolean + val contactGrpInvSent: Boolean, + val chatTags: List<Long>, + val chatItemTTL: Long?, + override val chatDeleted: Boolean, + val uiThemes: ThemeModeOverrides? = null, ): SomeChat, NamedChat { override val chatType get() = ChatType.Direct override val id get() = "@$contactId" override val apiId get() = contactId override val ready get() = activeConn?.connStatus == ConnStatus.Ready + val sndReady get() = ready || activeConn?.connStatus == ConnStatus.SndReady val active get() = contactStatus == ContactStatus.Active - override val sendMsgEnabled get() = - (ready && active && !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?: false)) - || nextSendGrpInv + override val userCantSendReason: Pair<String, String?>? + get() { + // TODO [short links] this will have additional statuses for pending contact requests before they are accepted + if (nextSendGrpInv) return null + if (!active) return generalGetString(MR.strings.cant_send_message_contact_deleted) to null + if (!sndReady) return generalGetString(MR.strings.cant_send_message_contact_not_ready) to null + if (activeConn?.connectionStats?.ratchetSyncSendProhibited == true) return generalGetString(MR.strings.cant_send_message_contact_not_synchronized) to null + if (activeConn?.connDisabled == true) return generalGetString(MR.strings.cant_send_message_contact_disabled) to null + return null + } + override val sendMsgEnabled get() = userCantSendReason == null val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent - override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All override val incognito get() = contactConnIncognito override fun featureEnabled(feature: ChatFeature) = when (feature) { ChatFeature.TimedMessages -> mergedPreferences.timedMessages.enabled.forUser @@ -982,6 +1547,12 @@ data class Contact( override val localAlias get() = profile.localAlias val verified get() = activeConn?.connectionCode != null + override fun anyNameContains(searchAnyCase: String): Boolean { + val s = searchAnyCase.trim().lowercase() + return profile.chatViewName.lowercase().contains(s) || profile.displayName.lowercase().contains(s) || profile.fullName.lowercase().contains(s) + } + + val directOrUsed: Boolean get() = if (activeConn != null) { (activeConn.connLevel == 0 && !activeConn.viaGroupLink) || contactUsed @@ -1021,15 +1592,27 @@ data class Contact( mergedPreferences = ContactUserPreferences.sampleData, createdAt = Clock.System.now(), updatedAt = Clock.System.now(), - contactGrpInvSent = false + chatTs = Clock.System.now(), + contactGrpInvSent = false, + chatDeleted = false, + uiThemes = null, + chatTags = emptyList(), + chatItemTTL = null, ) } } +@Serializable +data class NavigationInfo( + val afterUnread: Int = 0, + val afterTotal: Int = 0 +) + @Serializable enum class ContactStatus { @SerialName("active") Active, - @SerialName("deleted") Deleted; + @SerialName("deleted") Deleted, + @SerialName("deletedByUser") DeletedByUser; } @Serializable @@ -1058,20 +1641,32 @@ data class Connection( val viaGroupLink: Boolean, val customUserProfileId: Long? = null, val connectionCode: SecurityCode? = null, - val connectionStats: ConnectionStats? = null + val pqSupport: Boolean, + val pqEncryption: Boolean, + val pqSndEnabled: Boolean? = null, + val pqRcvEnabled: Boolean? = null, + val connectionStats: ConnectionStats? = null, + val authErrCounter: Int, + val quotaErrCounter: Int ) { val id: ChatId get() = ":$connId" + + val connDisabled: Boolean + get() = authErrCounter >= 10 // authErrDisableCount in core + + val connInactive: Boolean + get() = quotaErrCounter >= 5 // quotaErrInactiveCount in core + + val connPQEnabled: Boolean + get() = pqSndEnabled == true && pqRcvEnabled == true + companion object { - val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, peerChatVRange = VersionRange(1, 1), customUserProfileId = null) + val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, peerChatVRange = VersionRange(1, 1), customUserProfileId = null, pqSupport = false, pqEncryption = false, authErrCounter = 0, quotaErrCounter = 0) } } @Serializable -data class VersionRange(val minVersion: Int, val maxVersion: Int) { - - fun isCompatibleRange(vRange: VersionRange): Boolean = - this.minVersion <= vRange.maxVersion && vRange.minVersion <= this.maxVersion -} +data class VersionRange(val minVersion: Int, val maxVersion: Int) @Serializable data class SecurityCode(val securityCode: String, val verifiedAt: Instant) @@ -1127,7 +1722,6 @@ data class LocalProfile( @Serializable data class UserProfileUpdateSummary( - val notChanged: Int, val updateSuccesses: Int, val updateFailures: Int, val changedContacts: List<Contact> @@ -1139,39 +1733,67 @@ class Group ( var members: List<GroupMember> ) +@Serializable +sealed class ForwardConfirmation { + @Serializable @SerialName("filesNotAccepted") data class FilesNotAccepted(val fileIds: List<Long>) : ForwardConfirmation() + @Serializable @SerialName("filesInProgress") data class FilesInProgress(val filesCount: Int) : ForwardConfirmation() + @Serializable @SerialName("filesMissing") data class FilesMissing(val filesCount: Int) : ForwardConfirmation() + @Serializable @SerialName("filesFailed") data class FilesFailed(val filesCount: Int) : ForwardConfirmation() +} + @Serializable data class GroupInfo ( val groupId: Long, override val localDisplayName: String, val groupProfile: GroupProfile, + val businessChat: BusinessChatInfo? = null, val fullGroupPreferences: FullGroupPreferences, val membership: GroupMember, - val hostConnCustomUserProfileId: Long? = null, val chatSettings: ChatSettings, override val createdAt: Instant, - override val updatedAt: Instant + override val updatedAt: Instant, + val chatTs: Instant?, + val uiThemes: ThemeModeOverrides? = null, + val chatTags: List<Long>, + val chatItemTTL: Long?, + override val localAlias: String, ): SomeChat, NamedChat { override val chatType get() = ChatType.Group override val id get() = "#$groupId" override val apiId get() = groupId override val ready get() = membership.memberActive - override val sendMsgEnabled get() = membership.memberActive - override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All + override val chatDeleted get() = false + override val userCantSendReason: Pair<String, String?>? get() = + if (membership.memberActive) { + if (membership.memberRole == GroupMemberRole.Observer) { + generalGetString(MR.strings.observer_cant_send_message_title) to generalGetString(MR.strings.observer_cant_send_message_desc) + } else { + null + } + } else { + when (membership.memberStatus) { + GroupMemberStatus.MemRejected -> generalGetString(MR.strings.cant_send_message_rejected) to null + GroupMemberStatus.MemGroupDeleted -> generalGetString(MR.strings.cant_send_message_group_deleted) to null + GroupMemberStatus.MemRemoved -> generalGetString(MR.strings.cant_send_message_mem_removed) to null + GroupMemberStatus.MemLeft -> generalGetString(MR.strings.cant_send_message_you_left) to null + else -> generalGetString(MR.strings.cant_send_message_generic) to null + } + } + override val sendMsgEnabled get() = userCantSendReason == null override val incognito get() = membership.memberIncognito override fun featureEnabled(feature: ChatFeature) = when (feature) { ChatFeature.TimedMessages -> fullGroupPreferences.timedMessages.on ChatFeature.FullDelete -> fullGroupPreferences.fullDelete.on ChatFeature.Reactions -> fullGroupPreferences.reactions.on - ChatFeature.Voice -> fullGroupPreferences.voice.on + ChatFeature.Voice -> fullGroupPreferences.voice.on(membership) ChatFeature.Calls -> false } override val timedMessagesTTL: Int? get() = with(fullGroupPreferences.timedMessages) { if (on) ttl else null } - override val displayName get() = groupProfile.displayName + override val displayName get() = localAlias.ifEmpty { groupProfile.displayName } override val fullName get() = groupProfile.fullName override val image get() = groupProfile.image - override val localAlias get() = "" - val canEdit: Boolean + val isOwner: Boolean get() = membership.memberRole == GroupMemberRole.Owner && membership.memberCurrent val canDelete: Boolean @@ -1180,6 +1802,24 @@ data class GroupInfo ( val canAddMembers: Boolean get() = membership.memberRole >= GroupMemberRole.Admin && membership.memberActive + val canModerate: Boolean + get() = membership.memberRole >= GroupMemberRole.Moderator && membership.memberActive + + fun groupFeatureEnabled(feature: GroupFeature): Boolean { + val p = fullGroupPreferences + return when (feature) { + GroupFeature.TimedMessages -> p.timedMessages.on + GroupFeature.DirectMessages -> p.directMessages.on(membership) + GroupFeature.FullDelete -> p.fullDelete.on + GroupFeature.Reactions -> p.reactions.on + GroupFeature.Voice -> p.voice.on(membership) + GroupFeature.Files -> p.files.on(membership) + GroupFeature.SimplexLinks -> p.simplexLinks.on(membership) + GroupFeature.Reports -> p.reports.on + GroupFeature.History -> p.history.on + } + } + companion object { val sampleData = GroupInfo( groupId = 1, @@ -1187,10 +1827,14 @@ data class GroupInfo ( groupProfile = GroupProfile.sampleData, fullGroupPreferences = FullGroupPreferences.sampleData, membership = GroupMember.sampleData, - hostConnCustomUserProfileId = null, chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false), createdAt = Clock.System.now(), - updatedAt = Clock.System.now() + updatedAt = Clock.System.now(), + chatTs = Clock.System.now(), + uiThemes = null, + chatTags = emptyList(), + localAlias = "", + chatItemTTL = null ) } } @@ -1215,6 +1859,19 @@ data class GroupProfile ( } } +@Serializable +data class BusinessChatInfo ( + val chatType: BusinessChatType, + val businessId: String, + val customerId: String, +) + +@Serializable +enum class BusinessChatType { + @SerialName("business") Business, + @SerialName("customer") Customer, +} + @Serializable data class GroupMember ( val groupMemberId: Long, @@ -1224,28 +1881,71 @@ data class GroupMember ( val memberCategory: GroupMemberCategory, val memberStatus: GroupMemberStatus, val memberSettings: GroupMemberSettings, + val blockedByAdmin: Boolean, val invitedBy: InvitedBy, val localDisplayName: String, val memberProfile: LocalProfile, val memberContactId: Long? = null, val memberContactProfileId: Long, var activeConn: Connection? = null -) { +): NamedChat { val id: String get() = "#$groupId @$groupMemberId" - val displayName: String get() = memberProfile.localAlias.ifEmpty { memberProfile.displayName } - val fullName: String get() = memberProfile.fullName - val image: String? get() = memberProfile.image + val ready get() = activeConn?.connStatus == ConnStatus.Ready + val sndReady get() = ready || activeConn?.connStatus == ConnStatus.SndReady + val sendMsgEnabled get() = + sndReady + && memberCurrent + && !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?: false) + && !(activeConn?.connDisabled ?: true) + override val displayName: String + get() { + val p = memberProfile + val name = p.localAlias.ifEmpty { p.displayName } + return pastMember(name) + } + override val fullName: String get() = memberProfile.fullName + override val image: String? get() = memberProfile.image val contactLink: String? = memberProfile.contactLink val verified get() = activeConn?.connectionCode != null + val blocked get() = blockedByAdmin || !memberSettings.showMessages - val chatViewName: String - get() = memberProfile.localAlias.ifEmpty { displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") } + override val localAlias: String = memberProfile.localAlias + + override val chatViewName: String + get() { + val p = memberProfile + val name = p.localAlias.ifEmpty { p.displayName + (if (p.fullName == "" || p.fullName == p.displayName) "" else " / ${p.fullName}") } + return pastMember(name) + } + + private fun pastMember(name: String): String { + return if (memberStatus == GroupMemberStatus.MemUnknown) + String.format(generalGetString(MR.strings.past_member_vName), name) + else + name + } + + val localAliasAndFullName: String + get() { + val p = memberProfile + val fullName = p.displayName + (if (p.fullName == "" || p.fullName == p.displayName) "" else " / ${p.fullName}") + + val name = if (p.localAlias.isNotEmpty()) { + "${p.localAlias} ($fullName)" + } else { + fullName + } + return pastMember(name) + } val memberActive: Boolean get() = when (this.memberStatus) { + GroupMemberStatus.MemRejected -> false GroupMemberStatus.MemRemoved -> false GroupMemberStatus.MemLeft -> false GroupMemberStatus.MemGroupDeleted -> false + GroupMemberStatus.MemUnknown -> false GroupMemberStatus.MemInvited -> false + GroupMemberStatus.MemPendingApproval -> true GroupMemberStatus.MemIntroduced -> false GroupMemberStatus.MemIntroInvited -> false GroupMemberStatus.MemAccepted -> false @@ -1256,10 +1956,13 @@ data class GroupMember ( } val memberCurrent: Boolean get() = when (this.memberStatus) { + GroupMemberStatus.MemRejected -> false GroupMemberStatus.MemRemoved -> false GroupMemberStatus.MemLeft -> false GroupMemberStatus.MemGroupDeleted -> false + GroupMemberStatus.MemUnknown -> false GroupMemberStatus.MemInvited -> false + GroupMemberStatus.MemPendingApproval -> false GroupMemberStatus.MemIntroduced -> true GroupMemberStatus.MemIntroInvited -> true GroupMemberStatus.MemAccepted -> true @@ -1272,15 +1975,21 @@ data class GroupMember ( fun canBeRemoved(groupInfo: GroupInfo): Boolean { val userRole = groupInfo.membership.memberRole return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft - && userRole >= GroupMemberRole.Admin && userRole >= memberRole && groupInfo.membership.memberCurrent + && userRole >= GroupMemberRole.Admin && userRole >= memberRole && groupInfo.membership.memberActive } fun canChangeRoleTo(groupInfo: GroupInfo): List<GroupMemberRole>? = if (!canBeRemoved(groupInfo)) null else groupInfo.membership.memberRole.let { userRole -> - GroupMemberRole.values().filter { it <= userRole && it != GroupMemberRole.Author } + GroupMemberRole.selectableRoles.filter { it <= userRole } } + fun canBlockForAll(groupInfo: GroupInfo): Boolean { + val userRole = groupInfo.membership.memberRole + return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft && memberRole < GroupMemberRole.Moderator + && userRole >= GroupMemberRole.Moderator && userRole >= memberRole && groupInfo.membership.memberActive + } + val memberIncognito = memberProfile.profileId != memberContactProfileId companion object { @@ -1292,6 +2001,7 @@ data class GroupMember ( memberCategory = GroupMemberCategory.InviteeMember, memberStatus = GroupMemberStatus.MemComplete, memberSettings = GroupMemberSettings(showMessages = true), + blockedByAdmin = false, invitedBy = InvitedBy.IBUser(), localDisplayName = "alice", memberProfile = LocalProfile.sampleData, @@ -1322,13 +2032,19 @@ enum class GroupMemberRole(val memberRole: String) { @SerialName("observer") Observer("observer"), // order matters in comparisons @SerialName("author") Author("author"), @SerialName("member") Member("member"), + @SerialName("moderator") Moderator("moderator"), @SerialName("admin") Admin("admin"), @SerialName("owner") Owner("owner"); + companion object { + val selectableRoles: List<GroupMemberRole> = listOf(Observer, Member, Admin, Owner) + } + val text: String get() = when (this) { Observer -> generalGetString(MR.strings.group_member_role_observer) Author -> generalGetString(MR.strings.group_member_role_author) Member -> generalGetString(MR.strings.group_member_role_member) + Moderator -> generalGetString(MR.strings.group_member_role_moderator) Admin -> generalGetString(MR.strings.group_member_role_admin) Owner -> generalGetString(MR.strings.group_member_role_owner) } @@ -1345,10 +2061,13 @@ enum class GroupMemberCategory { @Serializable enum class GroupMemberStatus { + @SerialName("rejected") MemRejected, @SerialName("removed") MemRemoved, @SerialName("left") MemLeft, @SerialName("deleted") MemGroupDeleted, + @SerialName("unknown") MemUnknown, @SerialName("invited") MemInvited, + @SerialName("pending_approval") MemPendingApproval, @SerialName("introduced") MemIntroduced, @SerialName("intro-inv") MemIntroInvited, @SerialName("accepted") MemAccepted, @@ -1358,10 +2077,13 @@ enum class GroupMemberStatus { @SerialName("creator") MemCreator; val text: String get() = when (this) { + MemRejected -> generalGetString(MR.strings.group_member_status_rejected) MemRemoved -> generalGetString(MR.strings.group_member_status_removed) MemLeft -> generalGetString(MR.strings.group_member_status_left) MemGroupDeleted -> generalGetString(MR.strings.group_member_status_group_deleted) + MemUnknown -> generalGetString(MR.strings.group_member_status_unknown) MemInvited -> generalGetString(MR.strings.group_member_status_invited) + MemPendingApproval -> generalGetString(MR.strings.group_member_status_pending_approval) MemIntroduced -> generalGetString(MR.strings.group_member_status_introduced) MemIntroInvited -> generalGetString(MR.strings.group_member_status_intro_invitation) MemAccepted -> generalGetString(MR.strings.group_member_status_accepted) @@ -1372,10 +2094,13 @@ enum class GroupMemberStatus { } val shortText: String get() = when (this) { + MemRejected -> generalGetString(MR.strings.group_member_status_rejected) MemRemoved -> generalGetString(MR.strings.group_member_status_removed) MemLeft -> generalGetString(MR.strings.group_member_status_left) MemGroupDeleted -> generalGetString(MR.strings.group_member_status_group_deleted) + MemUnknown -> generalGetString(MR.strings.group_member_status_unknown_short) MemInvited -> generalGetString(MR.strings.group_member_status_invited) + MemPendingApproval -> generalGetString(MR.strings.group_member_status_pending_approval_short) MemIntroduced -> generalGetString(MR.strings.group_member_status_connecting) MemIntroInvited -> generalGetString(MR.strings.group_member_status_connecting) MemAccepted -> generalGetString(MR.strings.group_member_status_connecting) @@ -1416,6 +2141,43 @@ class MemberSubError ( val memberError: ChatError ) +@Serializable +class NoteFolder( + val noteFolderId: Long, + val favorite: Boolean, + val unread: Boolean, + override val createdAt: Instant, + override val updatedAt: Instant, + val chatTs: Instant +): SomeChat, NamedChat { + override val chatType get() = ChatType.Local + override val id get() = "*$noteFolderId" + override val apiId get() = noteFolderId + override val chatDeleted get() = false + override val ready get() = true + override val userCantSendReason: Pair<String, String?>? = null + override val sendMsgEnabled get() = true + override val incognito get() = false + override fun featureEnabled(feature: ChatFeature) = feature == ChatFeature.Voice + override val timedMessagesTTL: Int? get() = null + override val displayName get() = generalGetString(MR.strings.note_folder_local_display_name) + override val fullName get() = "" + override val image get() = null + override val localAlias get() = "" + override val localDisplayName: String get() = "" + + companion object { + val sampleData = NoteFolder( + noteFolderId = 1, + favorite = false, + unread = false, + createdAt = Clock.System.now(), + updatedAt = Clock.System.now(), + chatTs = Clock.System.now() + ) + } +} + @Serializable class UserContactRequest ( val contactRequestId: Long, @@ -1428,9 +2190,10 @@ class UserContactRequest ( override val chatType get() = ChatType.ContactRequest override val id get() = "<@$contactRequestId" override val apiId get() = contactRequestId + override val chatDeleted get() = false override val ready get() = true + override val userCantSendReason = generalGetString(MR.strings.cant_send_message_generic) to null override val sendMsgEnabled get() = false - override val ntfsEnabled get() = false override val incognito get() = false override fun featureEnabled(feature: ChatFeature) = false override val timedMessagesTTL: Int? get() = null @@ -1459,7 +2222,7 @@ class PendingContactConnection( val viaContactUri: Boolean, val groupLinkId: String? = null, val customUserProfileId: Long? = null, - val connReqInv: String? = null, + val connLinkInv: CreatedConnLink? = null, override val localAlias: String, override val createdAt: Instant, override val updatedAt: Instant @@ -1467,9 +2230,10 @@ class PendingContactConnection( override val chatType get() = ChatType.ContactConnection override val id get () = ":$pccConnId" override val apiId get() = pccConnId + override val chatDeleted get() = false override val ready get() = false + override val userCantSendReason = generalGetString(MR.strings.cant_send_message_generic) to null override val sendMsgEnabled get() = false - override val ntfsEnabled get() = false override val incognito get() = customUserProfileId != null override fun featureEnabled(feature: ChatFeature) = false override val timedMessagesTTL: Int? get() = null @@ -1482,8 +2246,9 @@ class PendingContactConnection( generalGetString(MR.strings.display_name_connection_established) } else { generalGetString( - if (initiated && !viaContactUri) MR.strings.display_name_invited_to_connect - else MR.strings.display_name_connecting + if (viaContactUri) MR.strings.display_name_requested_to_connect + else if (initiated) MR.strings.display_name_invited_to_connect + else MR.strings.display_name_accepted_invitation ) } } @@ -1525,6 +2290,7 @@ class PendingContactConnection( @Serializable enum class ConnStatus { @SerialName("new") New, + @SerialName("prepared") Prepared, @SerialName("joined") Joined, @SerialName("requested") Requested, @SerialName("accepted") Accepted, @@ -1534,15 +2300,22 @@ enum class ConnStatus { val initiated: Boolean? get() = when (this) { New -> true + Prepared -> false Joined -> false Requested -> true Accepted -> true - SndReady -> false + SndReady -> null Ready -> null Deleted -> null } } +@Serializable +data class ChatItemDeletion ( + val deletedChatItem: AChatItem, + val toChatItem: AChatItem? = null +) + @Serializable class AChatItem ( val chatInfo: ChatInfo, @@ -1555,6 +2328,36 @@ class ACIReaction( val chatReaction: CIReaction ) +@Serializable +data class MemberReaction( + val groupMember: GroupMember, + val reactionTs: Instant +) + +@Serializable +data class CIMentionMember( + val groupMemberId: Long, + val displayName: String, + val localAlias: String?, + val memberRole: GroupMemberRole +) + +@Serializable +data class CIMention( + val memberId: String, + val memberRef: CIMentionMember? +) { + constructor(groupMember: GroupMember): this( + groupMember.memberId, + CIMentionMember( + groupMember.groupMemberId, + groupMember.memberProfile.displayName, + groupMember.memberProfile.localAlias, + groupMember.memberRole + ) + ) +} + @Serializable class CIReaction( val chatDir: CIDirection, @@ -1569,6 +2372,7 @@ data class ChatItem ( val meta: CIMeta, val content: CIContent, val formattedText: List<FormattedText>? = null, + val mentions: Map<String, CIMention>? = null, val quotedItem: CIQuote? = null, val reactions: List<CIReactionCount>, val file: CIFile? = null @@ -1590,17 +2394,33 @@ data class ChatItem ( val allowAddReaction: Boolean get() = meta.itemDeleted == null && !isLiveDummy && (reactions.count { it.userReacted } < 3) - private val isLiveDummy: Boolean get() = meta.itemId == TEMP_LIVE_CHAT_ITEM_ID + val isLiveDummy: Boolean get() = meta.itemId == TEMP_LIVE_CHAT_ITEM_ID val encryptedFile: Boolean? = if (file?.fileSource == null) null else file.fileSource.cryptoArgs != null - val encryptLocalFile: Boolean - get() = content.msgContent !is MsgContent.MCVideo && - chatController.appPrefs.privacyEncryptLocalFiles.get() - val memberDisplayName: String? get() = - if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.chatViewName - else null + when (chatDir) { + is CIDirection.GroupRcv -> when (content) { + is CIContent.RcvGroupEventContent -> when (val event = content.rcvGroupEvent) { + is RcvGroupEvent.MemberProfileUpdated -> { + val to = event.toProfile + val from = event.fromProfile + when { + to.displayName != from.displayName || to.fullName != from.fullName -> null + else -> chatDir.groupMember.chatViewName + } + } + + else -> chatDir.groupMember.chatViewName + } + + else -> chatDir.groupMember.chatViewName + } + + else -> null + } + + val localNote: Boolean = chatDir is CIDirection.LocalSnd || chatDir is CIDirection.LocalRcv val isDeletedContent: Boolean get() = when (content) { @@ -1608,6 +2428,7 @@ data class ChatItem ( is CIContent.RcvDeleted -> true is CIContent.SndModerated -> true is CIContent.RcvModerated -> true + is CIContent.RcvBlocked -> true else -> false } @@ -1646,60 +2467,95 @@ data class ChatItem ( } } - fun memberToModerate(chatInfo: ChatInfo): Pair<GroupInfo, GroupMember>? { + fun memberToModerate(chatInfo: ChatInfo): Pair<GroupInfo, GroupMember?>? { return if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupRcv) { val m = chatInfo.groupInfo.membership - if (m.memberRole >= GroupMemberRole.Admin && m.memberRole >= chatDir.groupMember.memberRole && meta.itemDeleted == null) { + if (m.memberRole >= GroupMemberRole.Moderator && m.memberRole >= chatDir.groupMember.memberRole && meta.itemDeleted == null) { chatInfo.groupInfo to chatDir.groupMember } else { null } + } else if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupSnd) { + val m = chatInfo.groupInfo.membership + if (m.memberRole >= GroupMemberRole.Moderator) { + chatInfo.groupInfo to null + } else { + null + } } else { null } } - private val showNtfDir: Boolean get() = !chatDir.sent + val showLocalDelete: Boolean + get() = when (content) { + is CIContent.SndDirectE2EEInfo -> false + is CIContent.RcvDirectE2EEInfo -> false + is CIContent.SndGroupE2EEInfo -> false + is CIContent.RcvGroupE2EEInfo -> false + else -> true + } + + val isReport: Boolean get() = when (content) { + is CIContent.SndMsgContent, is CIContent.RcvMsgContent -> + content.msgContent is MsgContent.MCReport + else -> false + } + + val isActiveReport: Boolean get() = isReport && !isDeletedContent && meta.itemDeleted == null + + val canBeDeletedForSelf: Boolean + get() = (content.msgContent != null && !meta.isLive) || meta.itemDeleted != null || isDeletedContent || mergeCategory != null || showLocalDelete val showNotification: Boolean get() = when (content) { - is CIContent.SndMsgContent -> showNtfDir - is CIContent.RcvMsgContent -> showNtfDir - is CIContent.SndDeleted -> showNtfDir - is CIContent.RcvDeleted -> showNtfDir - is CIContent.SndCall -> showNtfDir + is CIContent.SndMsgContent -> false + is CIContent.RcvMsgContent -> meta.itemDeleted == null + is CIContent.SndDeleted -> false + is CIContent.RcvDeleted -> false + is CIContent.SndCall -> false is CIContent.RcvCall -> false // notification is shown on CallInvitation instead - is CIContent.RcvIntegrityError -> showNtfDir - is CIContent.RcvDecryptionError -> showNtfDir - is CIContent.RcvGroupInvitation -> showNtfDir - is CIContent.SndGroupInvitation -> showNtfDir - is CIContent.RcvDirectEventContent -> false + is CIContent.RcvIntegrityError -> false + is CIContent.RcvDecryptionError -> false + is CIContent.RcvGroupInvitation -> true + is CIContent.SndGroupInvitation -> false + is CIContent.RcvDirectEventContent -> when (content.rcvDirectEvent) { + is RcvDirectEvent.ContactDeleted -> false + is RcvDirectEvent.ProfileUpdated -> false + } is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) { is RcvGroupEvent.MemberAdded -> false is RcvGroupEvent.MemberConnected -> false is RcvGroupEvent.MemberLeft -> false is RcvGroupEvent.MemberRole -> false - is RcvGroupEvent.UserRole -> showNtfDir + is RcvGroupEvent.MemberBlocked -> false + is RcvGroupEvent.UserRole -> true is RcvGroupEvent.MemberDeleted -> false - is RcvGroupEvent.UserDeleted -> showNtfDir - is RcvGroupEvent.GroupDeleted -> showNtfDir + is RcvGroupEvent.UserDeleted -> true + is RcvGroupEvent.GroupDeleted -> true is RcvGroupEvent.GroupUpdated -> false is RcvGroupEvent.InvitedViaGroupLink -> false is RcvGroupEvent.MemberCreatedContact -> false + is RcvGroupEvent.MemberProfileUpdated -> false } - is CIContent.SndGroupEventContent -> showNtfDir + is CIContent.SndGroupEventContent -> false is CIContent.RcvConnEventContent -> false - is CIContent.SndConnEventContent -> showNtfDir + is CIContent.SndConnEventContent -> false is CIContent.RcvChatFeature -> false - is CIContent.SndChatFeature -> showNtfDir + is CIContent.SndChatFeature -> false is CIContent.RcvChatPreference -> false - is CIContent.SndChatPreference -> showNtfDir + is CIContent.SndChatPreference -> false is CIContent.RcvGroupFeature -> false - is CIContent.SndGroupFeature -> showNtfDir - is CIContent.RcvChatFeatureRejected -> showNtfDir - is CIContent.RcvGroupFeatureRejected -> showNtfDir - is CIContent.SndModerated -> true - is CIContent.RcvModerated -> true + is CIContent.SndGroupFeature -> false + is CIContent.RcvChatFeatureRejected -> true + is CIContent.RcvGroupFeatureRejected -> false + is CIContent.SndModerated -> false + is CIContent.RcvModerated -> false + is CIContent.RcvBlocked -> false + is CIContent.SndDirectE2EEInfo -> false + is CIContent.RcvDirectE2EEInfo -> false + is CIContent.SndGroupE2EEInfo -> false + is CIContent.RcvGroupE2EEInfo -> false is CIContent.InvalidJSON -> false } @@ -1712,16 +2568,20 @@ data class ChatItem ( ts: Instant = Clock.System.now(), text: String = "hello\nthere", status: CIStatus = CIStatus.SndNew(), + sentViaProxy: Boolean? = null, quotedItem: CIQuote? = null, file: CIFile? = null, + itemForwarded: CIForwardedFrom? = null, itemDeleted: CIDeleted? = null, itemEdited: Boolean = false, itemTimed: CITimed? = null, + itemLive: Boolean = false, + deletable: Boolean = true, editable: Boolean = true ) = ChatItem( chatDir = dir, - meta = CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, itemTimed, editable), + meta = CIMeta.getSample(id, ts, text, status, sentViaProxy, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, deletable, editable), content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)), quotedItem = quotedItem, reactions = listOf(), @@ -1803,13 +2663,17 @@ data class ChatItem ( itemTs = Clock.System.now(), itemText = generalGetString(MR.strings.deleted_description), itemStatus = CIStatus.RcvRead(), + sentViaProxy = null, createdAt = Clock.System.now(), updatedAt = Clock.System.now(), + itemForwarded = null, itemDeleted = null, itemEdited = false, itemTimed = null, itemLive = false, - editable = false + deletable = false, + editable = false, + userMention = false, ), content = CIContent.RcvDeleted(deleteMode = CIDeleteMode.cidmBroadcast), quotedItem = null, @@ -1824,13 +2688,17 @@ data class ChatItem ( itemTs = Clock.System.now(), itemText = "", itemStatus = CIStatus.RcvRead(), + sentViaProxy = null, createdAt = Clock.System.now(), updatedAt = Clock.System.now(), + itemForwarded = null, itemDeleted = null, itemEdited = false, itemTimed = null, itemLive = true, - editable = false + deletable = false, + editable = false, + userMention = false, ), content = CIContent.SndMsgContent(MsgContent.MCText("")), quotedItem = null, @@ -1850,6 +2718,83 @@ data class ChatItem ( } } +fun MutableState<SnapshotStateList<Chat>>.add(index: Int, elem: Chat) { + value = SnapshotStateList<Chat>().apply { addAll(value); add(index, elem) } +} + +fun MutableState<SnapshotStateList<Chat>>.add(elem: Chat) { + value = SnapshotStateList<Chat>().apply { addAll(value); add(elem) } +} + +// For some reason, Kotlin version crashes if the list is empty +fun <T> MutableList<T>.removeAll(predicate: (T) -> Boolean): Boolean = if (isEmpty()) false else remAll(predicate) + +fun <T> MutableState<SnapshotStateList<T>>.addAll(index: Int, elems: List<T>) { + value = SnapshotStateList<T>().apply { addAll(value); addAll(index, elems) } +} + +fun <T> MutableState<SnapshotStateList<T>>.addAll(elems: List<T>) { + value = SnapshotStateList<T>().apply { addAll(value); addAll(elems) } +} + +fun MutableState<SnapshotStateList<Chat>>.removeAll(block: (Chat) -> Boolean) { + value = SnapshotStateList<Chat>().apply { addAll(value); removeAll(block) } +} + +// TODO [contexts] operates with both contexts? +// Removes item(s) from chatItems and notifies a listener about removed item(s) +fun MutableState<SnapshotStateList<ChatItem>>.removeAllAndNotify(block: (ChatItem) -> Boolean) { + val toRemove = ArrayList<Triple<Long, Int, Boolean>>() + value = SnapshotStateList<ChatItem>().apply { + addAll(value) + var i = 0 + removeAll { + val remove = block(it) + if (remove) toRemove.add(Triple(it.id, i, it.isRcvNew)) + i++ + remove + } + } + if (toRemove.isNotEmpty()) { + chatModel.chatsContext.chatState.itemsRemoved(toRemove, value) + chatModel.secondaryChatsContext.value?.chatState?.itemsRemoved(toRemove, value) + } +} + +fun MutableState<SnapshotStateList<Chat>>.removeAt(index: Int): Chat { + val new = SnapshotStateList<Chat>() + new.addAll(value) + val res = new.removeAt(index) + value = new + return res +} + +fun <T> MutableState<SnapshotStateList<T>>.replaceAll(elems: List<T>) { + value = SnapshotStateList<T>().apply { addAll(elems) } +} + +fun MutableState<SnapshotStateList<Chat>>.clear() { + value = SnapshotStateList() +} + +// TODO [contexts] operates with both contexts? +// Removes all chatItems and notifies a listener about it +fun MutableState<SnapshotStateList<ChatItem>>.clearAndNotify() { + value = SnapshotStateList() + chatModel.chatsContext.chatState.clear() + chatModel.secondaryChatsContext.value?.chatState?.clear() +} + +fun <T> State<SnapshotStateList<T>>.asReversed(): MutableList<T> = value.asReversed() + +fun <T> State<SnapshotStateList<T>>.toList(): List<T> = value.toList() + +operator fun <T> State<SnapshotStateList<T>>.get(i: Int): T = value[i] + +operator fun <T> State<SnapshotStateList<T>>.set(index: Int, elem: T) { value[index] = elem } + +val State<List<Any>>.size: Int get() = value.size + enum class CIMergeCategory { MemberConnected, RcvGroupEvent, @@ -1865,12 +2810,16 @@ sealed class CIDirection { @Serializable @SerialName("directRcv") class DirectRcv: CIDirection() @Serializable @SerialName("groupSnd") class GroupSnd: CIDirection() @Serializable @SerialName("groupRcv") class GroupRcv(val groupMember: GroupMember): CIDirection() + @Serializable @SerialName("localSnd") class LocalSnd: CIDirection() + @Serializable @SerialName("localRcv") class LocalRcv: CIDirection() val sent: Boolean get() = when(this) { is DirectSnd -> true is DirectRcv -> false is GroupSnd -> true is GroupRcv -> false + is LocalSnd -> true + is LocalRcv -> false } } @@ -1880,15 +2829,20 @@ data class CIMeta ( val itemTs: Instant, val itemText: String, val itemStatus: CIStatus, + val sentViaProxy: Boolean?, val createdAt: Instant, val updatedAt: Instant, + val itemForwarded: CIForwardedFrom?, val itemDeleted: CIDeleted?, val itemEdited: Boolean, val itemTimed: CITimed?, val itemLive: Boolean?, + val userMention: Boolean, + val deletable: Boolean, val editable: Boolean ) { - val timestampText: String get() = getTimestampText(itemTs) + val timestampText: String get() = getTimestampText(itemTs, true) + val recent: Boolean get() = updatedAt + 10.toDuration(DurationUnit.SECONDS) > Clock.System.now() val isLive: Boolean get() = itemLive == true val disappearing: Boolean get() = !isRcvNew && itemTimed?.deleteAt != null @@ -1904,21 +2858,26 @@ data class CIMeta ( companion object { fun getSample( - id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(), - itemDeleted: CIDeleted? = null, itemEdited: Boolean = false, itemTimed: CITimed? = null, itemLive: Boolean = false, editable: Boolean = true + id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(), sentViaProxy: Boolean? = null, + itemForwarded: CIForwardedFrom? = null, itemDeleted: CIDeleted? = null, itemEdited: Boolean = false, + itemTimed: CITimed? = null, itemLive: Boolean = false, deletable: Boolean = true, editable: Boolean = true ): CIMeta = CIMeta( itemId = id, itemTs = ts, itemText = text, itemStatus = status, + sentViaProxy = sentViaProxy, createdAt = ts, updatedAt = ts, + itemForwarded = itemForwarded, itemDeleted = itemDeleted, itemEdited = itemEdited, itemTimed = itemTimed, itemLive = itemLive, - editable = editable + deletable = deletable, + editable = editable, + userMention = false, ) fun invalidJSON(): CIMeta = @@ -1928,13 +2887,17 @@ data class CIMeta ( itemTs = Clock.System.now(), itemText = "invalid JSON", itemStatus = CIStatus.SndNew(), + sentViaProxy = null, createdAt = Clock.System.now(), updatedAt = Clock.System.now(), + itemForwarded = null, itemDeleted = null, itemEdited = false, itemTimed = null, itemLive = false, - editable = false + deletable = false, + editable = false, + userMention = false ) } } @@ -1945,7 +2908,18 @@ data class CITimed( val deleteAt: Instant? ) -fun getTimestampText(t: Instant): String { +fun getTimestampDateText(t: Instant): String { + val tz = TimeZone.currentSystemDefault() + val time = t.toLocalDateTime(tz).toJavaLocalDateTime() + val weekday = time.format(DateTimeFormatter.ofPattern("EEE")) + val dayMonthYear = time.format(DateTimeFormatter.ofPattern( + if (Clock.System.now().toLocalDateTime(tz).year == time.year) "d MMM" else "d MMM yyyy") + ) + + return "$weekday, $dayMonthYear" +} + +fun getTimestampText(t: Instant, shortFormat: Boolean = false): String { val tz = TimeZone.currentSystemDefault() val now: LocalDateTime = Clock.System.now().toLocalDateTime(tz) val time: LocalDateTime = t.toLocalDateTime(tz) @@ -1953,16 +2927,23 @@ fun getTimestampText(t: Instant): String { val recent = now.date == time.date || (period.years == 0 && period.months == 0 && period.days == 1 && now.hour < 12 && time.hour >= 18 ) val dateFormatter = - if (recent) { + if (recent || shortFormat) { DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) } else { + val dayMonthFormat = when (Locale.getDefault().country) { + "US" -> "M/dd" + "DE" -> "dd.MM" + "RU" -> "dd.MM" + else -> "dd/MM" + } + val dayMonthYearFormat = when (Locale.getDefault().country) { + "US" -> "M/dd/yy" + "DE" -> "dd.MM.yy" + "RU" -> "dd.MM.yy" + else -> "dd/MM/yy" + } DateTimeFormatter.ofPattern( - when (Locale.getDefault().country) { - "US" -> "M/dd" - "DE" -> "dd.MM" - "RU" -> "dd.MM" - else -> "dd/MM" - } + if (now.year == time.year) dayMonthFormat else dayMonthYearFormat ) // DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) } @@ -1976,13 +2957,21 @@ fun localTimestamp(t: Instant): String { return ts.toJavaLocalDateTime().format(dateFormatter) } +fun localDate(t: Instant): String { + val tz = TimeZone.currentSystemDefault() + val ts: LocalDateTime = t.toLocalDateTime(tz) + val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) + return ts.toJavaLocalDateTime().format(dateFormatter) +} + @Serializable sealed class CIStatus { @Serializable @SerialName("sndNew") class SndNew: CIStatus() @Serializable @SerialName("sndSent") class SndSent(val sndProgress: SndCIStatusProgress): CIStatus() @Serializable @SerialName("sndRcvd") class SndRcvd(val msgRcptStatus: MsgReceiptStatus, val sndProgress: SndCIStatusProgress): CIStatus() @Serializable @SerialName("sndErrorAuth") class SndErrorAuth: CIStatus() - @Serializable @SerialName("sndError") class SndError(val agentError: String): CIStatus() + @Serializable @SerialName("sndError") class CISSndError(val agentError: SndError): CIStatus() + @Serializable @SerialName("sndWarning") class SndWarning(val agentError: SndError): CIStatus() @Serializable @SerialName("rcvNew") class RcvNew: CIStatus() @Serializable @SerialName("rcvRead") class RcvRead: CIStatus() @Serializable @SerialName("invalid") class Invalid(val text: String): CIStatus() @@ -2006,7 +2995,8 @@ sealed class CIStatus { MsgReceiptStatus.BadMsgHash -> MR.images.ic_double_check to Color.Red } is SndErrorAuth -> MR.images.ic_close to Color.Red - is SndError -> MR.images.ic_warning_filled to WarningYellow + is CISSndError -> MR.images.ic_close to Color.Red + is SndWarning -> MR.images.ic_warning_filled to WarningOrange is RcvNew -> MR.images.ic_circle_filled to primaryColor is RcvRead -> null is CIStatus.Invalid -> MR.images.ic_question_mark to metaColor @@ -2017,13 +3007,48 @@ sealed class CIStatus { is SndSent -> null is SndRcvd -> null is SndErrorAuth -> generalGetString(MR.strings.message_delivery_error_title) to generalGetString(MR.strings.message_delivery_error_desc) - is SndError -> generalGetString(MR.strings.message_delivery_error_title) to (generalGetString(MR.strings.unknown_error) + ": $agentError") + is CISSndError -> generalGetString(MR.strings.message_delivery_error_title) to agentError.errorInfo + is SndWarning -> generalGetString(MR.strings.message_delivery_warning_title) to agentError.errorInfo is RcvNew -> null is RcvRead -> null is Invalid -> "Invalid status" to this.text } } +@Serializable +sealed class SndError { + @Serializable @SerialName("auth") class Auth: SndError() + @Serializable @SerialName("quota") class Quota: SndError() + @Serializable @SerialName("expired") class Expired: SndError() + @Serializable @SerialName("relay") class Relay(val srvError: SrvError): SndError() + @Serializable @SerialName("proxy") class Proxy(val proxyServer: String, val srvError: SrvError): SndError() + @Serializable @SerialName("proxyRelay") class ProxyRelay(val proxyServer: String, val srvError: SrvError): SndError() + @Serializable @SerialName("other") class Other(val sndError: String): SndError() + + val errorInfo: String get() = when (this) { + is SndError.Auth -> generalGetString(MR.strings.snd_error_auth) + is SndError.Quota -> generalGetString(MR.strings.snd_error_quota) + is SndError.Expired -> generalGetString(MR.strings.snd_error_expired) + is SndError.Relay -> generalGetString(MR.strings.snd_error_relay).format(srvError.errorInfo) + is SndError.Proxy -> generalGetString(MR.strings.snd_error_proxy).format(proxyServer, srvError.errorInfo) + is SndError.ProxyRelay -> generalGetString(MR.strings.snd_error_proxy_relay).format(proxyServer, srvError.errorInfo) + is SndError.Other -> generalGetString(MR.strings.ci_status_other_error).format(sndError) + } +} + +@Serializable +sealed class SrvError { + @Serializable @SerialName("host") class Host: SrvError() + @Serializable @SerialName("version") class Version: SrvError() + @Serializable @SerialName("other") class Other(val srvError: String): SrvError() + + val errorInfo: String get() = when (this) { + is SrvError.Host -> generalGetString(MR.strings.srv_error_host) + is SrvError.Version -> generalGetString(MR.strings.srv_error_version) + is SrvError.Other -> srvError + } +} + @Serializable enum class MsgReceiptStatus { @SerialName("ok") Ok, @@ -2036,16 +3061,98 @@ enum class SndCIStatusProgress { @SerialName("complete") Complete; } +@Serializable +sealed class GroupSndStatus { + @Serializable @SerialName("new") class New: GroupSndStatus() + @Serializable @SerialName("forwarded") class Forwarded: GroupSndStatus() + @Serializable @SerialName("inactive") class Inactive: GroupSndStatus() + @Serializable @SerialName("sent") class Sent: GroupSndStatus() + @Serializable @SerialName("rcvd") class Rcvd(val msgRcptStatus: MsgReceiptStatus): GroupSndStatus() + @Serializable @SerialName("error") class Error(val agentError: SndError): GroupSndStatus() + @Serializable @SerialName("warning") class Warning(val agentError: SndError): GroupSndStatus() + @Serializable @SerialName("invalid") class Invalid(val text: String): GroupSndStatus() + + fun statusIcon( + primaryColor: Color, + metaColor: Color = CurrentColors.value.colors.secondary, + paleMetaColor: Color = CurrentColors.value.colors.secondary + ): Pair<ImageResource, Color> = + when (this) { + is New -> MR.images.ic_more_horiz to metaColor + is Forwarded -> MR.images.ic_chevron_right_2 to metaColor + is Inactive -> MR.images.ic_person_off to metaColor + is Sent -> MR.images.ic_check_filled to metaColor + is Rcvd -> when(this.msgRcptStatus) { + MsgReceiptStatus.Ok -> MR.images.ic_double_check to metaColor + MsgReceiptStatus.BadMsgHash -> MR.images.ic_double_check to Color.Red + } + is Error -> MR.images.ic_close to Color.Red + is Warning -> MR.images.ic_warning_filled to WarningOrange + is Invalid -> MR.images.ic_question_mark to metaColor + } + + val statusInto: Pair<String, String>? get() = when (this) { + is New -> null + is Forwarded -> generalGetString(MR.strings.message_forwarded_title) to generalGetString(MR.strings.message_forwarded_desc) + is Inactive -> generalGetString(MR.strings.member_inactive_title) to generalGetString(MR.strings.member_inactive_desc) + is Sent -> null + is Rcvd -> null + is Error -> generalGetString(MR.strings.message_delivery_error_title) to agentError.errorInfo + is Warning -> generalGetString(MR.strings.message_delivery_warning_title) to agentError.errorInfo + is Invalid -> "Invalid status" to this.text + } +} + @Serializable sealed class CIDeleted { @Serializable @SerialName("deleted") class Deleted(val deletedTs: Instant?): CIDeleted() @Serializable @SerialName("blocked") class Blocked(val deletedTs: Instant?): CIDeleted() + @Serializable @SerialName("blockedByAdmin") class BlockedByAdmin(val deletedTs: Instant?): CIDeleted() @Serializable @SerialName("moderated") class Moderated(val deletedTs: Instant?, val byGroupMember: GroupMember): CIDeleted() } +@Serializable +enum class MsgDirection { + @SerialName("rcv") Rcv, + @SerialName("snd") Snd; +} + +@Serializable +sealed class CIForwardedFrom { + @Serializable @SerialName("unknown") object Unknown: CIForwardedFrom() + @Serializable @SerialName("contact") class Contact(override val chatName: String, val msgDir: MsgDirection, val contactId: Long? = null, val chatItemId: Long? = null): CIForwardedFrom() + @Serializable @SerialName("group") class Group(override val chatName: String, val msgDir: MsgDirection, val groupId: Long? = null, val chatItemId: Long? = null): CIForwardedFrom() + + open val chatName: String + get() = when (this) { + Unknown -> "" + is Contact -> chatName + is Group -> chatName + } + + val chatTypeApiIdMsgId: Triple<ChatType, Long, Long?>? + get() = when (this) { + Unknown -> null + is Contact -> if (contactId != null) Triple(ChatType.Direct, contactId, chatItemId) else null + is Group -> if (groupId != null) Triple(ChatType.Group, groupId, chatItemId) else null + } + + fun text(chatType: ChatType): String = + if (chatType == ChatType.Local) { + if (chatName.isEmpty()) { + generalGetString(MR.strings.saved_description) + } else { + generalGetString(MR.strings.saved_from_description).format(chatName) + } + } else { + generalGetString(MR.strings.forwarded_description) + } +} + @Serializable enum class CIDeleteMode(val deleteMode: String) { @SerialName("internal") cidmInternal("internal"), + @SerialName("internalMark") cidmInternalMark("internalMark"), @SerialName("broadcast") cidmBroadcast("broadcast"); } @@ -2078,12 +3185,17 @@ sealed class CIContent: ItemContent { @Serializable @SerialName("sndChatFeature") class SndChatFeature(val feature: ChatFeature, val enabled: FeatureEnabled, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvChatPreference") class RcvChatPreference(val feature: ChatFeature, val allowed: FeatureAllowed, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("sndChatPreference") class SndChatPreference(val feature: ChatFeature, val allowed: FeatureAllowed, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null } - @Serializable @SerialName("rcvGroupFeature") class RcvGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null } - @Serializable @SerialName("sndGroupFeature") class SndGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("rcvGroupFeature") class RcvGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null, val memberRole_: GroupMemberRole?): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("sndGroupFeature") class SndGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null, val memberRole_: GroupMemberRole?): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvChatFeatureRejected") class RcvChatFeatureRejected(val feature: ChatFeature): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvGroupFeatureRejected") class RcvGroupFeatureRejected(val groupFeature: GroupFeature): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("sndModerated") object SndModerated: CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvModerated") object RcvModerated: CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("rcvBlocked") object RcvBlocked: CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("sndDirectE2EEInfo") class SndDirectE2EEInfo(val e2eeInfo: E2EEInfo): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("rcvDirectE2EEInfo") class RcvDirectE2EEInfo(val e2eeInfo: E2EEInfo): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("sndGroupE2EEInfo") class SndGroupE2EEInfo(val e2eeInfo: E2EEInfo): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("rcvGroupE2EEInfo") class RcvGroupE2EEInfo(val e2eeInfo: E2EEInfo): 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) { @@ -2106,12 +3218,17 @@ sealed class CIContent: ItemContent { is SndChatFeature -> featureText(feature, enabled.text, param) is RcvChatPreference -> preferenceText(feature, allowed, param) is SndChatPreference -> preferenceText(feature, allowed, param) - is RcvGroupFeature -> featureText(groupFeature, preference.enable.text, param) - is SndGroupFeature -> featureText(groupFeature, preference.enable.text, param) + is RcvGroupFeature -> featureText(groupFeature, preference.enable.text, param, memberRole_) + is SndGroupFeature -> featureText(groupFeature, preference.enable.text, param, memberRole_) is RcvChatFeatureRejected -> "${feature.text}: ${generalGetString(MR.strings.feature_received_prohibited)}" is RcvGroupFeatureRejected -> "${groupFeature.text}: ${generalGetString(MR.strings.feature_received_prohibited)}" is SndModerated -> generalGetString(MR.strings.moderated_description) is RcvModerated -> generalGetString(MR.strings.moderated_description) + 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 InvalidJSON -> "invalid data" } @@ -2124,16 +3241,39 @@ sealed class CIContent: ItemContent { is RcvDecryptionError -> true is RcvGroupInvitation -> true is RcvModerated -> true + is RcvBlocked -> true is InvalidJSON -> true else -> false } companion object { - fun featureText(feature: Feature, enabled: String, param: Int?): String = - if (feature.hasParam) { + fun directE2EEInfoStr(e2EEInfo: E2EEInfo): String = + if (e2EEInfo.pqEnabled) { + generalGetString(MR.strings.e2ee_info_pq_short) + } else { + e2eeInfoNoPQStr + } + + private val e2eeInfoNoPQStr: String = generalGetString(MR.strings.e2ee_info_no_pq_short) + + fun featureText(feature: Feature, enabled: String, param: Int?, role: GroupMemberRole? = null): String = + (if (feature.hasParam) { "${feature.text}: ${timeText(param)}" } else { "${feature.text}: $enabled" + }) + ( + if (feature.hasRole && role != null) + " (${roleText(role)})" + else + "" + ) + + private fun roleText(role: GroupMemberRole?): String = + when (role) { + GroupMemberRole.Owner -> generalGetString(MR.strings.feature_roles_owners) + GroupMemberRole.Admin -> generalGetString(MR.strings.feature_roles_admins) + GroupMemberRole.Moderator -> generalGetString(MR.strings.feature_roles_moderators) + else -> generalGetString(MR.strings.feature_roles_all_members) } fun preferenceText(feature: Feature, allowed: FeatureAllowed, param: Int?): String = when { @@ -2186,6 +3326,8 @@ class CIQuote ( is CIDirection.DirectRcv -> null is CIDirection.GroupSnd -> membership?.displayName ?: generalGetString(MR.strings.sender_you_pronoun) is CIDirection.GroupRcv -> chatDir.groupMember.displayName + is CIDirection.LocalSnd -> generalGetString(MR.strings.sender_you_pronoun) + is CIDirection.LocalRcv -> null null -> null } @@ -2208,11 +3350,21 @@ sealed class MsgReaction { MREmojiChar.Heart -> "❤️" else -> emoji.value } - is Unknown -> "" + is Unknown -> "?" } companion object { - val values: List<MsgReaction> get() = MREmojiChar.values().map(::Emoji) + val values: List<MsgReaction> get() = MREmojiChar.entries.map(::Emoji) + val supported: List<MsgReaction> get() = listOf( + MREmojiChar.ThumbsUp, + MREmojiChar.ThumbsDown, + MREmojiChar.Smile, + MREmojiChar.Laugh, + MREmojiChar.Sad, + MREmojiChar.Heart, + MREmojiChar.Launch, + MREmojiChar.Check + ).map(::Emoji) } } @@ -2230,8 +3382,13 @@ object MsgReactionSerializer : KSerializer<MsgReaction> { return if (json is JsonObject && "type" in json) { when(val t = json["type"]?.jsonPrimitive?.content ?: "") { "emoji" -> { - val emoji = Json.decodeFromString<MREmojiChar>(json["emoji"].toString()) - if (emoji == null) MsgReaction.Unknown(t, json) else MsgReaction.Emoji(emoji) + val msgReaction = try { + val emoji = Json.decodeFromString<MREmojiChar>(json["emoji"].toString()) + MsgReaction.Emoji(emoji) + } catch (e: Throwable) { + MsgReaction.Unknown(t, json) + } + msgReaction } else -> MsgReaction.Unknown(t, json) } @@ -2259,9 +3416,11 @@ enum class MREmojiChar(val value: String) { @SerialName("👍") ThumbsUp("👍"), @SerialName("👎") ThumbsDown("👎"), @SerialName("😀") Smile("😀"), + @SerialName("😂") Laugh("😂"), @SerialName("😢") Sad("😢"), @SerialName("❤") Heart("❤"), - @SerialName("🚀") Launch("🚀"); + @SerialName("🚀") Launch("🚀"), + @SerialName("✅") Check("✅"); } @Serializable @@ -2279,12 +3438,15 @@ data class CIFile( is CIFileStatus.SndComplete -> true is CIFileStatus.SndCancelled -> true is CIFileStatus.SndError -> true + is CIFileStatus.SndWarning -> true is CIFileStatus.RcvInvitation -> false is CIFileStatus.RcvAccepted -> false is CIFileStatus.RcvTransfer -> false + is CIFileStatus.RcvAborted -> false is CIFileStatus.RcvCancelled -> false is CIFileStatus.RcvComplete -> true is CIFileStatus.RcvError -> false + is CIFileStatus.RcvWarning -> false is CIFileStatus.Invalid -> false } @@ -2300,15 +3462,36 @@ data class CIFile( } is CIFileStatus.SndCancelled -> null is CIFileStatus.SndError -> null + is CIFileStatus.SndWarning -> sndCancelAction is CIFileStatus.RcvInvitation -> null is CIFileStatus.RcvAccepted -> rcvCancelAction is CIFileStatus.RcvTransfer -> rcvCancelAction + is CIFileStatus.RcvAborted -> null is CIFileStatus.RcvCancelled -> null is CIFileStatus.RcvComplete -> null is CIFileStatus.RcvError -> null + is CIFileStatus.RcvWarning -> rcvCancelAction is CIFileStatus.Invalid -> null } + val showStatusIconInSmallView: Boolean = when (fileStatus) { + is CIFileStatus.SndStored -> fileProtocol != FileProtocol.LOCAL + is CIFileStatus.SndTransfer -> true + is CIFileStatus.SndComplete -> false + is CIFileStatus.SndCancelled -> true + is CIFileStatus.SndError -> true + is CIFileStatus.SndWarning -> true + is CIFileStatus.RcvInvitation -> false + is CIFileStatus.RcvAccepted -> true + is CIFileStatus.RcvTransfer -> true + is CIFileStatus.RcvAborted -> true + is CIFileStatus.RcvCancelled -> true + is CIFileStatus.RcvComplete -> false + is CIFileStatus.RcvError -> true + is CIFileStatus.RcvWarning -> true + is CIFileStatus.Invalid -> true + } + /** * DO NOT CALL this function in compose scope, [LaunchedEffect], [DisposableEffect] and so on. Only with [withBGApi] or [runBlocking]. * Otherwise, it will be canceled when moving to another screen/item/view, etc @@ -2342,6 +3525,12 @@ data class CIFile( return res } + fun forwardingAllowed(): Boolean = when { + chatModel.connectedToRemote() && cachedRemoteFileRequests[fileSource] != false && loaded -> true + getLoadedFilePath(this) != null -> true + else -> false + } + companion object { fun getSample( fileId: Long = 1, @@ -2382,10 +3571,42 @@ data class CryptoFile( tmpFile?.delete() } + private fun decryptToTmpFile(): URI? { + val absoluteFilePath = if (isAbsolutePath) filePath else getAppFilePath(filePath) + val tmpFile = createTmpFileIfNeeded() + try { + decryptCryptoFile(absoluteFilePath, cryptoArgs ?: return null, tmpFile.absolutePath) + } catch (e: Exception) { + Log.e(TAG, "Unable to decrypt crypto file: " + e.stackTraceToString()) + AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString()) + return null + } + return tmpFile.toURI() + } + + fun decryptedGet(): URI? { + val decrypted = decryptedUris[filePath] + return if (decrypted != null && decrypted.toFile().exists()) { + decrypted + } else { + null + } + } + + fun decryptedGetOrCreate(): URI? { + val decrypted = decryptedGet() ?: decryptToTmpFile() + if (decrypted != null) { + decryptedUris[filePath] = decrypted + } + return decrypted + } + companion object { fun plain(f: String): CryptoFile = CryptoFile(f, null) fun desktopPlain(f: URI): CryptoFile = CryptoFile(f.toFile().absolutePath, null) + + private val decryptedUris = mutableMapOf<String, URI>() } } @@ -2431,7 +3652,8 @@ private val rcvCancelAction: CancelAction = CancelAction( @Serializable enum class FileProtocol { @SerialName("smp") SMP, - @SerialName("xftp") XFTP; + @SerialName("xftp") XFTP, + @SerialName("local") LOCAL; } @Serializable @@ -2440,13 +3662,16 @@ sealed class CIFileStatus { @Serializable @SerialName("sndTransfer") class SndTransfer(val sndProgress: Long, val sndTotal: Long): CIFileStatus() @Serializable @SerialName("sndComplete") object SndComplete: CIFileStatus() @Serializable @SerialName("sndCancelled") object SndCancelled: CIFileStatus() - @Serializable @SerialName("sndError") object SndError: CIFileStatus() + @Serializable @SerialName("sndError") class SndError(val sndFileError: FileError): CIFileStatus() + @Serializable @SerialName("sndWarning") class SndWarning(val sndFileError: FileError): CIFileStatus() @Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus() @Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus() @Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus() + @Serializable @SerialName("rcvAborted") object RcvAborted: CIFileStatus() @Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus() @Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus() - @Serializable @SerialName("rcvError") object RcvError: CIFileStatus() + @Serializable @SerialName("rcvError") class RcvError(val rcvFileError: FileError): CIFileStatus() + @Serializable @SerialName("rcvWarning") class RcvWarning(val rcvFileError: FileError): CIFileStatus() @Serializable @SerialName("invalid") class Invalid(val text: String): CIFileStatus() val sent: Boolean get() = when (this) { @@ -2455,16 +3680,41 @@ sealed class CIFileStatus { is SndComplete -> true is SndCancelled -> true is SndError -> true + is SndWarning -> true is RcvInvitation -> false is RcvAccepted -> false is RcvTransfer -> false + is RcvAborted -> false is RcvComplete -> false is RcvCancelled -> false is RcvError -> false + is RcvWarning -> false is Invalid -> false } } +@Serializable +sealed class FileError { + @Serializable @SerialName("auth") class Auth: FileError() + @Serializable @SerialName("blocked") class Blocked(val server: String, val blockInfo: BlockingInfo): FileError() + @Serializable @SerialName("noFile") class NoFile: FileError() + @Serializable @SerialName("relay") class Relay(val srvError: SrvError): FileError() + @Serializable @SerialName("other") class Other(val fileError: String): FileError() + + val errorInfo: String get() = when (this) { + is Auth -> generalGetString(MR.strings.file_error_auth) + is Blocked -> generalGetString(MR.strings.file_error_blocked).format(blockInfo.reason.text) + is NoFile -> generalGetString(MR.strings.file_error_no_file) + is Relay -> generalGetString(MR.strings.file_error_relay).format(srvError.errorInfo) + is Other -> generalGetString(MR.strings.ci_status_other_error).format(fileError) + } + + val moreInfoButton: Pair<String, String>? get() = when(this) { + is Blocked -> generalGetString(MR.strings.how_it_works) to contentModerationPostLink + else -> null + } +} + @Suppress("SERIALIZER_TYPE_INCOMPATIBLE") @Serializable(with = MsgContentSerializer::class) sealed class MsgContent { @@ -2476,8 +3726,23 @@ sealed class MsgContent { @Serializable(with = MsgContentSerializer::class) class MCVideo(override val text: String, val image: String, val duration: Int): 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 MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent() + val isVoice: Boolean get() = + when (this) { + is MCVoice -> true + else -> false + } + + val isMediaOrFileAttachment: Boolean get() = + when (this) { + is MCImage -> true + is MCVideo -> true + is MCFile -> true + else -> false + } + val cmdString: String get() = if (this is MCUnknown) "json $json" else "json ${json.encodeToString(this)}" } @@ -2514,6 +3779,9 @@ enum class CIGroupInvitationStatus { @SerialName("expired") Expired; } +@Serializable +class E2EEInfo (val pqEnabled: Boolean) {} + object MsgContentSerializer : KSerializer<MsgContent> { override val descriptor: SerialDescriptor = buildSerialDescriptor("MsgContent", PolymorphicKind.SEALED) { element("MCText", buildClassSerialDescriptor("MCText") { @@ -2535,6 +3803,10 @@ object MsgContentSerializer : KSerializer<MsgContent> { element("MCFile", buildClassSerialDescriptor("MCFile") { element<String>("text") }) + element("MCReport", buildClassSerialDescriptor("MCReport") { + element<String>("text") + element<ReportReason>("reason") + }) element("MCUnknown", buildClassSerialDescriptor("MCUnknown")) } @@ -2565,6 +3837,10 @@ object MsgContentSerializer : KSerializer<MsgContent> { MsgContent.MCVoice(text, duration) } "file" -> MsgContent.MCFile(text) + "report" -> { + val reason = Json.decodeFromString<ReportReason>(json["reason"].toString()) + MsgContent.MCReport(text, reason) + } else -> MsgContent.MCUnknown(t, text, json) } } else { @@ -2613,17 +3889,34 @@ object MsgContentSerializer : KSerializer<MsgContent> { put("type", "file") put("text", value.text) } + is MsgContent.MCReport -> + buildJsonObject { + put("type", "report") + put("text", value.text) + put("reason", json.encodeToJsonElement(value.reason)) + } is MsgContent.MCUnknown -> value.json } encoder.encodeJsonElement(json) } } +@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, +} + @Serializable class FormattedText(val text: String, val format: Format? = null) { // TODO make it dependent on simplexLinkMode preference fun link(mode: SimplexLinkMode): String? = when (format) { - is Format.Uri -> text + is Format.Uri -> if (text.startsWith("http://", ignoreCase = true) || text.startsWith("https://", ignoreCase = true)) text else "https://$text" is Format.SimplexLink -> if (mode == SimplexLinkMode.BROWSER) text else format.simplexUri is Format.Email -> "mailto:$text" is Format.Phone -> "tel:$text" @@ -2636,6 +3929,10 @@ class FormattedText(val text: String, val format: Format? = null) { fun simplexLinkText(linkType: SimplexLinkType, smpHosts: List<String>): String = "${linkType.description} (${String.format(generalGetString(MR.strings.simplex_link_connection), smpHosts.firstOrNull() ?: "?")})" + + companion object { + fun plain(text: String): List<FormattedText> = if (text.isEmpty()) emptyList() else listOf(FormattedText(text)) + } } @Serializable @@ -2648,6 +3945,7 @@ sealed class Format { @Serializable @SerialName("colored") class Colored(val color: FormatColor): Format() @Serializable @SerialName("uri") class Uri: Format() @Serializable @SerialName("simplexLink") class SimplexLink(val linkType: SimplexLinkType, val simplexUri: String, val smpHosts: List<String>): Format() + @Serializable @SerialName("mention") class Mention(val memberName: String): Format() @Serializable @SerialName("email") class Email: Format() @Serializable @SerialName("phone") class Phone: Format() @@ -2660,10 +3958,13 @@ sealed class Format { is Colored -> SpanStyle(color = this.color.uiColor) is Uri -> linkStyle is SimplexLink -> linkStyle + is Mention -> SpanStyle(fontWeight = FontWeight.Medium) is Email -> linkStyle is Phone -> linkStyle } + val isSimplexLink = this is SimplexLink + companion object { val linkStyle @Composable get() = SpanStyle(color = MaterialTheme.colors.primary, textDecoration = TextDecoration.Underline) } @@ -2673,12 +3974,14 @@ sealed class Format { enum class SimplexLinkType(val linkType: String) { contact("contact"), invitation("invitation"), - group("group"); + group("group"), + channel("channel"); val description: String get() = generalGetString(when (this) { contact -> MR.strings.simplex_link_contact invitation -> MR.strings.simplex_link_invitation group -> MR.strings.simplex_link_group + channel -> MR.strings.simplex_link_channel }) } @@ -2705,14 +4008,73 @@ enum class FormatColor(val color: String) { } } + +@Serializable(with = ReportReasonSerializer::class) +sealed class ReportReason { + @Serializable @SerialName("spam") object Spam: ReportReason() + @Serializable @SerialName("illegal") object Illegal: ReportReason() + @Serializable @SerialName("community") object Community: ReportReason() + @Serializable @SerialName("profile") object Profile: ReportReason() + @Serializable @SerialName("other") object Other: ReportReason() + @Serializable @SerialName("unknown") data class Unknown(val type: String): ReportReason() + + companion object { + val supportedReasons: List<ReportReason> = listOf(Spam, Illegal, Community, Profile, Other) + } + + val text: String get() = when (this) { + Spam -> generalGetString(MR.strings.report_reason_spam) + Illegal -> generalGetString(MR.strings.report_reason_illegal) + Community -> generalGetString(MR.strings.report_reason_community) + Profile -> generalGetString(MR.strings.report_reason_profile) + Other -> generalGetString(MR.strings.report_reason_other) + is Unknown -> type + } +} + +object ReportReasonSerializer : KSerializer<ReportReason> { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("ReportReason", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): ReportReason { + return when (val value = decoder.decodeString()) { + "spam" -> ReportReason.Spam + "illegal" -> ReportReason.Illegal + "community" -> ReportReason.Community + "profile" -> ReportReason.Profile + "other" -> ReportReason.Other + else -> ReportReason.Unknown(value) + } + } + + override fun serialize(encoder: Encoder, value: ReportReason) { + val stringValue = when (value) { + is ReportReason.Spam -> "spam" + is ReportReason.Illegal -> "illegal" + is ReportReason.Community -> "community" + is ReportReason.Profile -> "profile" + is ReportReason.Other -> "other" + is ReportReason.Unknown -> value.type + } + encoder.encodeString(stringValue) + } +} + @Serializable class SndFileTransfer() {} @Serializable -class RcvFileTransfer() {} +data class RcvFileTransfer( + val fileId: Long, +) @Serializable -class FileTransferMeta() {} +data class FileTransferMeta( + val fileId: Long, + val fileName: String, + val filePath: String, + val fileSize: Long, +) @Serializable enum class CICallStatus { @@ -2761,10 +4123,30 @@ sealed class MsgErrorType() { @Serializable sealed class RcvDirectEvent() { @Serializable @SerialName("contactDeleted") class ContactDeleted(): RcvDirectEvent() + @Serializable @SerialName("profileUpdated") class ProfileUpdated(val fromProfile: Profile, val toProfile: Profile): RcvDirectEvent() val text: String get() = when (this) { is ContactDeleted -> generalGetString(MR.strings.rcv_direct_event_contact_deleted) + is ProfileUpdated -> profileUpdatedText(fromProfile, toProfile) } + + private fun profileUpdatedText(from: Profile, to: Profile): String = + when { + to.displayName != from.displayName || to.fullName != from.fullName -> + generalGetString(MR.strings.profile_update_event_contact_name_changed).format(from.profileViewName, to.profileViewName) + + to.image != from.image -> when (to.image) { + null -> generalGetString(MR.strings.profile_update_event_removed_picture) + else -> generalGetString(MR.strings.profile_update_event_set_new_picture) + } + + to.contactLink != from.contactLink -> when (to.contactLink) { + null -> generalGetString(MR.strings.profile_update_event_removed_address) + else -> generalGetString(MR.strings.profile_update_event_set_new_address) + } + // shouldn't happen if backend correctly creates item; UI should be synchronized with backend + else -> generalGetString(MR.strings.profile_update_event_updated_profile) + } } @Serializable @@ -2773,6 +4155,7 @@ sealed class RcvGroupEvent() { @Serializable @SerialName("memberConnected") class MemberConnected(): RcvGroupEvent() @Serializable @SerialName("memberLeft") class MemberLeft(): RcvGroupEvent() @Serializable @SerialName("memberRole") class MemberRole(val groupMemberId: Long, val profile: Profile, val role: GroupMemberRole): RcvGroupEvent() + @Serializable @SerialName("memberBlocked") class MemberBlocked(val groupMemberId: Long, val profile: Profile, val blocked: Boolean): RcvGroupEvent() @Serializable @SerialName("userRole") class UserRole(val role: GroupMemberRole): RcvGroupEvent() @Serializable @SerialName("memberDeleted") class MemberDeleted(val groupMemberId: Long, val profile: Profile): RcvGroupEvent() @Serializable @SerialName("userDeleted") class UserDeleted(): RcvGroupEvent() @@ -2780,12 +4163,18 @@ sealed class RcvGroupEvent() { @Serializable @SerialName("groupUpdated") class GroupUpdated(val groupProfile: GroupProfile): RcvGroupEvent() @Serializable @SerialName("invitedViaGroupLink") class InvitedViaGroupLink(): RcvGroupEvent() @Serializable @SerialName("memberCreatedContact") class MemberCreatedContact(): RcvGroupEvent() + @Serializable @SerialName("memberProfileUpdated") class MemberProfileUpdated(val fromProfile: Profile, val toProfile: Profile): RcvGroupEvent() val text: String get() = 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 MemberLeft -> generalGetString(MR.strings.rcv_group_event_member_left) is MemberRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_member_role), profile.profileViewName, role.text) + is MemberBlocked -> if (blocked) { + String.format(generalGetString(MR.strings.rcv_group_event_member_blocked), profile.profileViewName) + } else { + String.format(generalGetString(MR.strings.rcv_group_event_member_unblocked), profile.profileViewName) + } 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) @@ -2793,13 +4182,28 @@ sealed class RcvGroupEvent() { is GroupUpdated -> generalGetString(MR.strings.rcv_group_event_updated_group_profile) is InvitedViaGroupLink -> generalGetString(MR.strings.rcv_group_event_invited_via_your_group_link) is MemberCreatedContact -> generalGetString(MR.strings.rcv_group_event_member_created_contact) + is MemberProfileUpdated -> profileUpdatedText(fromProfile, toProfile) } + + private fun profileUpdatedText(from: Profile, to: Profile): String = + when { + to.displayName != from.displayName || to.fullName != from.fullName -> + generalGetString(MR.strings.profile_update_event_member_name_changed).format(from.profileViewName, to.profileViewName) + + to.image != from.image -> when (to.image) { + null -> generalGetString(MR.strings.profile_update_event_removed_picture) + else -> generalGetString(MR.strings.profile_update_event_set_new_picture) + } + // shouldn't happen if backend correctly creates item; UI should be synchronized with backend + else -> generalGetString(MR.strings.profile_update_event_updated_profile) + } } @Serializable sealed class SndGroupEvent() { @Serializable @SerialName("memberRole") class MemberRole(val groupMemberId: Long, val profile: Profile, val role: GroupMemberRole): SndGroupEvent() @Serializable @SerialName("userRole") class UserRole(val role: GroupMemberRole): SndGroupEvent() + @Serializable @SerialName("memberBlocked") class MemberBlocked(val groupMemberId: Long, val profile: Profile, val blocked: Boolean): SndGroupEvent() @Serializable @SerialName("memberDeleted") class MemberDeleted(val groupMemberId: Long, val profile: Profile): SndGroupEvent() @Serializable @SerialName("userLeft") class UserLeft(): SndGroupEvent() @Serializable @SerialName("groupUpdated") class GroupUpdated(val groupProfile: GroupProfile): SndGroupEvent() @@ -2807,6 +4211,11 @@ sealed class SndGroupEvent() { val text: String get() = 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) { + String.format(generalGetString(MR.strings.snd_group_event_member_blocked), profile.profileViewName) + } else { + String.format(generalGetString(MR.strings.snd_group_event_member_unblocked), profile.profileViewName) + } 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) @@ -2818,6 +4227,7 @@ sealed class RcvConnEvent { @Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase): RcvConnEvent() @Serializable @SerialName("ratchetSync") class RatchetSync(val syncStatus: RatchetSyncState): RcvConnEvent() @Serializable @SerialName("verificationCodeReset") object VerificationCodeReset: RcvConnEvent() + @Serializable @SerialName("pqEnabled") class PQEnabled(val enabled: Boolean): RcvConnEvent() val text: String get() = when (this) { is SwitchQueue -> when (phase) { @@ -2826,6 +4236,11 @@ sealed class RcvConnEvent { } is RatchetSync -> ratchetSyncStatusToText(syncStatus) is VerificationCodeReset -> generalGetString(MR.strings.rcv_conn_event_verification_code_reset) + is PQEnabled -> if (enabled) { + generalGetString(MR.strings.conn_event_enabled_pq) + } else { + generalGetString(MR.strings.conn_event_disabled_pq) + } } } @@ -2843,6 +4258,7 @@ fun ratchetSyncStatusToText(ratchetSyncStatus: RatchetSyncState): String { sealed class SndConnEvent { @Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase, val member: GroupMemberRef? = null): SndConnEvent() @Serializable @SerialName("ratchetSync") class RatchetSync(val syncStatus: RatchetSyncState, val member: GroupMemberRef? = null): SndConnEvent() + @Serializable @SerialName("pqEnabled") class PQEnabled(val enabled: Boolean): SndConnEvent() val text: String get() = when (this) { @@ -2871,6 +4287,12 @@ sealed class SndConnEvent { } ratchetSyncStatusToText(syncStatus) } + + is PQEnabled -> if (enabled) { + generalGetString(MR.strings.conn_event_enabled_pq) + } else { + generalGetString(MR.strings.conn_event_disabled_pq) + } } } @@ -2882,41 +4304,66 @@ enum class SwitchPhase { @SerialName("completed") Completed } -sealed class ChatItemTTL: Comparable<ChatItemTTL?> { +sealed class ChatItemTTL: Comparable<ChatItemTTL> { object Day: ChatItemTTL() object Week: ChatItemTTL() object Month: ChatItemTTL() + object Year: ChatItemTTL() data class Seconds(val secs: Long): ChatItemTTL() object None: ChatItemTTL() - override fun compareTo(other: ChatItemTTL?): Int = (seconds ?: Long.MAX_VALUE).compareTo(other?.seconds ?: Long.MAX_VALUE) + override fun compareTo(other: ChatItemTTL): Int = + (seconds.takeIf { it != 0L } ?: Long.MAX_VALUE) + .compareTo(other.seconds.takeIf { it != 0L } ?: Long.MAX_VALUE) - val seconds: Long? + val seconds: Long get() = when (this) { - is None -> null + is None -> 0 is Day -> 86400L is Week -> 7 * 86400L is Month -> 30 * 86400L + is Year -> 365 * 86400L is Seconds -> secs } + val text: String + get() = when(this) { + is None -> generalGetString(MR.strings.chat_item_ttl_none) + is Day -> generalGetString(MR.strings.chat_item_ttl_day) + is Week -> generalGetString(MR.strings.chat_item_ttl_week) + is Month -> generalGetString(MR.strings.chat_item_ttl_month) + is Year -> generalGetString(MR.strings.chat_item_ttl_year) + is Seconds -> String.format(generalGetString(MR.strings.chat_item_ttl_seconds), secs) + } + + val neverExpires: Boolean get() = this is None + companion object { - fun fromSeconds(seconds: Long?): ChatItemTTL = + fun fromSeconds(seconds: Long): ChatItemTTL = when (seconds) { - null -> None + 0L -> None 86400L -> Day 7 * 86400L -> Week 30 * 86400L -> Month + 365 * 86400L -> Year else -> Seconds(seconds) } } } +@Serializable +data class ChatTag( + val chatTagId: Long, + val chatTagText: String, + val chatTagEmoji: String? +) + @Serializable class ChatItemInfo( val itemVersions: List<ChatItemVersion>, - val memberDeliveryStatuses: List<MemberDeliveryStatus>? + val memberDeliveryStatuses: List<MemberDeliveryStatus>?, + val forwardedFromChatItem: AChatItem? ) @Serializable @@ -2931,7 +4378,8 @@ data class ChatItemVersion( @Serializable data class MemberDeliveryStatus( val groupMemberId: Long, - val memberDeliveryStatus: CIStatus + val memberDeliveryStatus: GroupSndStatus, + val sentViaProxy: Boolean? ) enum class NotificationPreviewMode { 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 4af3e3f2ed..979d79c839 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 @@ -1,38 +1,62 @@ package chat.simplex.common.model +import SectionItemView +import androidx.compose.foundation.layout.* +import androidx.compose.material.* import chat.simplex.common.views.helpers.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter -import chat.simplex.common.model.ChatModel.updatingChatsMutex +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.getNetCfg +import chat.simplex.common.model.ChatController.setNetCfg +import chat.simplex.common.model.ChatModel.changingActiveUserMutex +import chat.simplex.common.model.MsgContent.MCUnknown +import chat.simplex.common.model.SMPProxyFallback.AllowProtected +import chat.simplex.common.model.SMPProxyMode.Always import dev.icerock.moko.resources.compose.painterResource import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* -import chat.simplex.common.views.newchat.ConnectViaLinkTab +import chat.simplex.common.views.chat.item.showContentBlockedAlert +import chat.simplex.common.views.chat.item.showQuotedItemDoesNotExistAlert +import chat.simplex.common.views.chatlist.openGroupChat +import chat.simplex.common.views.migration.MigrationFileLinkData import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.usersettings.* +import chat.simplex.common.views.usersettings.networkAndServers.serverHostname import com.charleskorn.kaml.Yaml import com.charleskorn.kaml.YamlConfiguration import chat.simplex.res.MR import com.russhwolf.settings.Settings +import dev.icerock.moko.resources.ImageResource +import dev.icerock.moko.resources.StringResource +import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.sync.withLock import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.serialization.* -import kotlinx.serialization.builtins.MapSerializer -import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.builtins.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.* import java.util.Date typealias ChatCtrl = Long -// currentChatVersion in core -const val CURRENT_CHAT_VERSION: Int = 2 - // version range that supports establishing direct connection with a group member (xGrpDirectInvVRange in core) -val CREATE_MEMBER_CONTACT_VRANGE = VersionRange(minVersion = 2, maxVersion = CURRENT_CHAT_VERSION) +val CREATE_MEMBER_CONTACT_VERSION = 2 enum class CallOnLockScreen { DISABLE, @@ -62,22 +86,12 @@ class AppPreferences { if (!runServiceInBackground.get()) NotificationsMode.OFF else NotificationsMode.default ) { NotificationsMode.values().firstOrNull { it.name == this } } val notificationPreviewMode = mkStrPreference(SHARED_PREFS_NOTIFICATION_PREVIEW_MODE, NotificationPreviewMode.default.name) + val canAskToEnableNotifications = mkBoolPreference(SHARED_PREFS_CAN_ASK_TO_ENABLE_NOTIFICATIONS, true) val backgroundServiceNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_NOTICE_SHOWN, false) val backgroundServiceBatteryNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, false) val autoRestartWorkerVersion = mkIntPreference(SHARED_PREFS_AUTO_RESTART_WORKER_VERSION, 0) val webrtcPolicyRelay = mkBoolPreference(SHARED_PREFS_WEBRTC_POLICY_RELAY, true) - private val _callOnLockScreen = mkStrPreference(SHARED_PREFS_WEBRTC_CALLS_ON_LOCK_SCREEN, CallOnLockScreen.default.name) - val callOnLockScreen: SharedPreference<CallOnLockScreen> = SharedPreference( - get = fun(): CallOnLockScreen { - val value = _callOnLockScreen.get() ?: return CallOnLockScreen.default - return try { - CallOnLockScreen.valueOf(value) - } catch (e: Error) { - CallOnLockScreen.default - } - }, - set = fun(action: CallOnLockScreen) { _callOnLockScreen.set(action.name) } - ) + val callOnLockScreen: SharedPreference<CallOnLockScreen> = mkSafeEnumPreference(SHARED_PREFS_WEBRTC_CALLS_ON_LOCK_SCREEN, CallOnLockScreen.default) val performLA = mkBoolPreference(SHARED_PREFS_PERFORM_LA, false) val laMode = mkEnumPreference(SHARED_PREFS_LA_MODE, LAMode.default) { LAMode.values().firstOrNull { it.name == this } } val laLockDelay = mkIntPreference(SHARED_PREFS_LA_LOCK_DELAY, 30) @@ -86,48 +100,57 @@ class AppPreferences { val privacyProtectScreen = mkBoolPreference(SHARED_PREFS_PRIVACY_PROTECT_SCREEN, true) val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true) val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true) - private val _simplexLinkMode = mkStrPreference(SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE, SimplexLinkMode.default.name) - val simplexLinkMode: SharedPreference<SimplexLinkMode> = SharedPreference( - get = fun(): SimplexLinkMode { - val value = _simplexLinkMode.get() ?: return SimplexLinkMode.default - return try { - SimplexLinkMode.valueOf(value) - } catch (e: Error) { - SimplexLinkMode.default - } - }, - set = fun(mode: SimplexLinkMode) { _simplexLinkMode.set(mode.name) } - ) + val privacyChatListOpenLinks = mkEnumPreference(SHARED_PREFS_PRIVACY_CHAT_LIST_OPEN_LINKS, PrivacyChatListOpenLinksMode.ASK) { PrivacyChatListOpenLinksMode.values().firstOrNull { it.name == this } } + val simplexLinkMode: SharedPreference<SimplexLinkMode> = mkSafeEnumPreference(SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE, SimplexLinkMode.default) val privacyShowChatPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS, true) val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true) + val privacyShortLinks = mkBoolPreference(SHARED_PREFS_PRIVACY_SHORT_LINKS, false) val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false) val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true) + val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true) + val privacyMediaBlurRadius = mkIntPreference(SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS, 0) + // Blur broken on Android 12, see https://github.com/chrisbanes/haze/issues/77. And not available before 12 + val deviceSupportsBlur = appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 32 + val appearanceBarsBlurRadius = mkIntPreference(SHARED_PREFS_APPEARANCE_BARS_BLUR_RADIUS, if (deviceSupportsBlur) 50 else 0) val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false) val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false) val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null) val chatArchiveTime = mkDatePreference(SHARED_PREFS_CHAT_ARCHIVE_TIME, null) val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null) + val chatStopped = mkBoolPreference(SHARED_PREFS_CHAT_STOPPED, false) val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false) + val logLevel = mkEnumPreference(SHARED_PREFS_LOG_LEVEL, LogLevel.WARNING) { LogLevel.entries.firstOrNull { it.name == this } } + val showInternalErrors = mkBoolPreference(SHARED_PREFS_SHOW_INTERNAL_ERRORS, false) + val showSlowApiCalls = mkBoolPreference(SHARED_PREFS_SHOW_SLOW_API_CALLS, false) val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false) val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false) - val networkProxyHostPort = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, "localhost:9050") - private val _networkSessionMode = mkStrPreference(SHARED_PREFS_NETWORK_SESSION_MODE, TransportSessionMode.default.name) - val networkSessionMode: SharedPreference<TransportSessionMode> = SharedPreference( - get = fun(): TransportSessionMode { - val value = _networkSessionMode.get() ?: return TransportSessionMode.default + val networkShowSubscriptionPercentage = mkBoolPreference(SHARED_PREFS_NETWORK_SHOW_SUBSCRIPTION_PERCENTAGE, false) + private val _networkProxy = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, json.encodeToString(NetworkProxy())) + val networkProxy: SharedPreference<NetworkProxy> = SharedPreference( + get = fun(): NetworkProxy { + val value = _networkProxy.get() ?: return NetworkProxy() return try { - TransportSessionMode.valueOf(value) - } catch (e: Error) { - TransportSessionMode.default + if (value.startsWith("{")) { + json.decodeFromString(value) + } else { + NetworkProxy(host = value.substringBefore(":").ifBlank { "localhost" }, port = value.substringAfter(":").toIntOrNull() ?: 9050) + } + } catch (e: Throwable) { + NetworkProxy() } }, - set = fun(mode: TransportSessionMode) { _networkSessionMode.set(mode.name) } + set = fun(proxy: NetworkProxy) { _networkProxy.set(json.encodeToString(proxy)) } ) - val networkHostMode = mkStrPreference(SHARED_PREFS_NETWORK_HOST_MODE, HostMode.OnionViaSocks.name) + val networkSessionMode: SharedPreference<TransportSessionMode> = mkSafeEnumPreference(SHARED_PREFS_NETWORK_SESSION_MODE, TransportSessionMode.default) + val networkSMPProxyMode: SharedPreference<SMPProxyMode> = mkSafeEnumPreference(SHARED_PREFS_NETWORK_SMP_PROXY_MODE, SMPProxyMode.default) + val networkSMPProxyFallback: SharedPreference<SMPProxyFallback> = mkSafeEnumPreference(SHARED_PREFS_NETWORK_SMP_PROXY_FALLBACK, SMPProxyFallback.default) + val networkHostMode: SharedPreference<HostMode> = mkSafeEnumPreference(SHARED_PREFS_NETWORK_HOST_MODE, HostMode.default) val networkRequiredHostMode = mkBoolPreference(SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE, false) + val networkSMPWebPortServers: SharedPreference<SMPWebPortServers> = mkSafeEnumPreference(SHARED_PREFS_NETWORK_SMP_WEB_PORT_SERVERS, SMPWebPortServers.default) val networkTCPConnectTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT, NetCfg.defaults.tcpConnectTimeout, NetCfg.proxyDefaults.tcpConnectTimeout) val networkTCPTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT, NetCfg.defaults.tcpTimeout, NetCfg.proxyDefaults.tcpTimeout) val networkTCPTimeoutPerKb = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT_PER_KB, NetCfg.defaults.tcpTimeoutPerKb, NetCfg.proxyDefaults.tcpTimeoutPerKb) + val networkRcvConcurrency = mkIntPreference(SHARED_PREFS_NETWORK_RCV_CONCURRENCY, NetCfg.defaults.rcvConcurrency) val networkSMPPingInterval = mkLongPreference(SHARED_PREFS_NETWORK_SMP_PING_INTERVAL, NetCfg.defaults.smpPingInterval) val networkSMPPingCount = mkIntPreference(SHARED_PREFS_NETWORK_SMP_PING_COUNT, NetCfg.defaults.smpPingCount) val networkEnableKeepAlive = mkBoolPreference(SHARED_PREFS_NETWORK_ENABLE_KEEP_ALIVE, NetCfg.defaults.enableKeepAlive) @@ -135,13 +158,19 @@ class AppPreferences { val networkTCPKeepIntvl = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_INTVL, KeepAliveOpts.defaults.keepIntvl) val networkTCPKeepCnt = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_CNT, KeepAliveOpts.defaults.keepCnt) val incognito = mkBoolPreference(SHARED_PREFS_INCOGNITO, false) - val connectViaLinkTab = mkStrPreference(SHARED_PREFS_CONNECT_VIA_LINK_TAB, ConnectViaLinkTab.SCAN.name) val liveMessageAlertShown = mkBoolPreference(SHARED_PREFS_LIVE_MESSAGE_ALERT_SHOWN, false) val showHiddenProfilesNotice = mkBoolPreference(SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE, true) + val oneHandUICardShown = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN, false) + val addressCreationCardShown = mkBoolPreference(SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN, false) val showMuteProfileAlert = mkBoolPreference(SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT, true) val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null) + val appUpdateChannel = mkEnumPreference(SHARED_PREFS_APP_UPDATE_CHANNEL, AppUpdatesChannel.DISABLED) { AppUpdatesChannel.entries.firstOrNull { it.name == this } } + val appSkippedUpdate = mkStrPreference(SHARED_PREFS_APP_SKIPPED_UPDATE, "") + val appUpdateNoticeShown = mkBoolPreference(SHARED_PREFS_APP_UPDATE_NOTICE_SHOWN, false) val onboardingStage = mkEnumPreference(SHARED_PREFS_ONBOARDING_STAGE, OnboardingStage.OnboardingComplete) { OnboardingStage.values().firstOrNull { it.name == this } } + val migrationToStage = mkStrPreference(SHARED_PREFS_MIGRATION_TO_STAGE, null) + val migrationFromStage = mkStrPreference(SHARED_PREFS_MIGRATION_FROM_STAGE, null) val storeDBPassphrase = mkBoolPreference(SHARED_PREFS_STORE_DB_PASSPHRASE, true) val initialRandomDBPassphrase = mkBoolPreference(SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE, false) val encryptedDBPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE, null) @@ -155,13 +184,36 @@ class AppPreferences { val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false) val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null) - val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name) - val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.name) - val themeOverrides = mkMapPreference(SHARED_PREFS_THEMES, mapOf(), encode = { + // This flag is set when database is first initialized and resets only when the database is removed. + // This is needed for recover from incomplete initialization when only one database file is created. + // If false - the app will clear database folder on missing file and re-initialize. + // Note that this situation can only happen if passphrase for the first database is incorrect because, otherwise, backend will re-create second database automatically + val newDatabaseInitialized = mkBoolPreference(SHARED_PREFS_NEW_DATABASE_INITIALIZED, false) + + /** after importing new database, this flag will be set and unset only after importing app settings in [initChatController] */ + 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 currentThemeIds = mkMapPreference(SHARED_PREFS_CURRENT_THEME_IDs, mapOf(), encode = { + json.encodeToString(MapSerializer(String.serializer(), String.serializer()), it) + }, decode = { + json.decodeFromString(MapSerializer(String.serializer(), String.serializer()), it) + }) + // Deprecated. Remove key from preferences in 2025 + val themeOverridesOld = mkMapPreference(SHARED_PREFS_THEMES_OLD, mapOf(), encode = { json.encodeToString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it) }, decode = { - json.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it) + jsonCoerceInputValues.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it) }, settingsThemes) + val themeOverrides = mkThemeOverridesPreference() + val profileImageCornerRadius = mkFloatPreference(SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS, 22.5f) + val chatItemRoundness = mkFloatPreference(SHARED_PREFS_CHAT_ITEM_ROUNDNESS, 0.75f) + val chatItemTail = mkBoolPreference(SHARED_PREFS_CHAT_ITEM_TAIL, true) + val fontScale = mkFloatPreference(SHARED_PREFS_FONT_SCALE, 1f) + val densityScale = mkFloatPreference(SHARED_PREFS_DENSITY_SCALE, 1f) + val inAppBarsDefaultAlpha = if (deviceSupportsBlur) 0.875f else 0.975f + val inAppBarsAlpha = mkFloatPreference(SHARED_PREFS_IN_APP_BARS_ALPHA, inAppBarsDefaultAlpha) val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null) val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0) @@ -174,7 +226,29 @@ class AppPreferences { val offerRemoteMulticast = mkBoolPreference(SHARED_PREFS_OFFER_REMOTE_MULTICAST, true) val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null) - + + val showDeleteConversationNotice = mkBoolPreference(SHARED_PREFS_SHOW_DELETE_CONVERSATION_NOTICE, true) + val showDeleteContactNotice = mkBoolPreference(SHARED_PREFS_SHOW_DELETE_CONTACT_NOTICE, true) + val showSentViaProxy = mkBoolPreference(SHARED_PREFS_SHOW_SENT_VIA_RPOXY, false) + + + val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true) + val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false) + + val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, true) + val chatBottomBar = mkBoolPreference(SHARED_PREFS_CHAT_BOTTOM_BAR, true) + + val hintPreferences: List<Pair<SharedPreference<Boolean>, Boolean>> = listOf( + laNoticeShown to false, + oneHandUICardShown to false, + addressCreationCardShown to false, + liveMessageAlertShown to false, + showHiddenProfilesNotice to true, + showMuteProfileAlert to true, + showDeleteConversationNotice to true, + showDeleteContactNotice to true, + ) + private fun mkIntPreference(prefName: String, default: Int) = SharedPreference( get = fun() = settings.getInt(prefName, default), @@ -187,6 +261,12 @@ class AppPreferences { set = fun(value) = settings.putLong(prefName, value) ) + private fun mkFloatPreference(prefName: String, default: Float) = + SharedPreference( + get = fun() = settings.getFloat(prefName, default), + set = fun(value) = settings.putFloat(prefName, value) + ) + private fun mkTimeoutPreference(prefName: String, default: Long, proxyDefault: Long): SharedPreference<Long> { val d = if (networkUseSocksProxy.get()) proxyDefault else default return SharedPreference( @@ -221,7 +301,19 @@ class AppPreferences { set = fun(value) = settings.putString(prefName, value.toString()) ) - // LALAL + private inline fun <reified T : Enum<T>> mkSafeEnumPreference(key: String, default: T): SharedPreference<T> = SharedPreference( + get = { + val value = settings.getString(key, "") + if (value == "") return@SharedPreference default + try { + enumValueOf<T>(value) + } catch (e: IllegalArgumentException) { + default + } + }, + set = { value -> settings.putString(key, value.name) } + ) + private fun mkDatePreference(prefName: String, default: Instant?): SharedPreference<Instant?> = SharedPreference( get = { @@ -242,6 +334,12 @@ class AppPreferences { set = fun(value) = prefs.putString(prefName, encode(value)) ) + private fun mkThemeOverridesPreference(): SharedPreference<List<ThemeOverrides>> = + SharedPreference( + get = fun() = themeOverridesStore ?: (readThemeOverrides()).also { themeOverridesStore = it }, + set = fun(value) { if (writeThemeOverrides(value)) { themeOverridesStore = value } } + ) + companion object { const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS" internal const val SHARED_PREFS_THEMES_ID = "chat.simplex.app.THEMES" @@ -249,6 +347,7 @@ class AppPreferences { private const val SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND = "RunServiceInBackground" private const val SHARED_PREFS_NOTIFICATIONS_MODE = "NotificationsMode" private const val SHARED_PREFS_NOTIFICATION_PREVIEW_MODE = "NotificationPreviewMode" + private const val SHARED_PREFS_CAN_ASK_TO_ENABLE_NOTIFICATIONS = "CanAskToEnableNotifications" private const val SHARED_PREFS_SERVICE_NOTICE_SHOWN = "BackgroundServiceNoticeShown" private const val SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN = "BackgroundServiceBatteryNoticeShown" private const val SHARED_PREFS_WEBRTC_POLICY_RELAY = "WebrtcPolicyRelay" @@ -262,29 +361,48 @@ class AppPreferences { private const val SHARED_PREFS_PRIVACY_ACCEPT_IMAGES = "PrivacyAcceptImages" private const val SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE = "PrivacyTransferImagesInline" private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews" + private const val SHARED_PREFS_PRIVACY_CHAT_LIST_OPEN_LINKS = "ChatListOpenLinks" private const val SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE = "PrivacySimplexLinkMode" private const val SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS = "PrivacyShowChatPreviews" private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft" + private const val SHARED_PREFS_PRIVACY_SHORT_LINKS = "PrivacyShortLinks" private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet" private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles" + private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays" + private const val SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS = "PrivacyMediaBlurRadius" + private const val SHARED_PREFS_APPEARANCE_BARS_BLUR_RADIUS = "AppearanceBarsBlurRadius" const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup" private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls" private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites" private const val SHARED_PREFS_CHAT_ARCHIVE_NAME = "ChatArchiveName" private const val SHARED_PREFS_CHAT_ARCHIVE_TIME = "ChatArchiveTime" private const val SHARED_PREFS_APP_LANGUAGE = "AppLanguage" + private const val SHARED_PREFS_APP_UPDATE_CHANNEL = "AppUpdateChannel" + private const val SHARED_PREFS_APP_SKIPPED_UPDATE = "AppSkippedUpdate" + private const val SHARED_PREFS_APP_UPDATE_NOTICE_SHOWN = "AppUpdateNoticeShown" private const val SHARED_PREFS_ONBOARDING_STAGE = "OnboardingStage" + const val SHARED_PREFS_MIGRATION_TO_STAGE = "MigrationToStage" + const val SHARED_PREFS_MIGRATION_FROM_STAGE = "MigrationFromStage" private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart" + private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped" private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools" + private const val SHARED_PREFS_LOG_LEVEL = "LogLevel" + private const val SHARED_PREFS_SHOW_INTERNAL_ERRORS = "ShowInternalErrors" + private const val SHARED_PREFS_SHOW_SLOW_API_CALLS = "ShowSlowApiCalls" private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible" private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy" + private const val SHARED_PREFS_NETWORK_SHOW_SUBSCRIPTION_PERCENTAGE = "ShowSubscriptionPercentage" private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort" private const val SHARED_PREFS_NETWORK_SESSION_MODE = "NetworkSessionMode" + private const val SHARED_PREFS_NETWORK_SMP_PROXY_MODE = "NetworkSMPProxyMode" + private const val SHARED_PREFS_NETWORK_SMP_PROXY_FALLBACK = "NetworkSMPProxyFallback" private const val SHARED_PREFS_NETWORK_HOST_MODE = "NetworkHostMode" private const val SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE = "NetworkRequiredHostMode" + private const val SHARED_PREFS_NETWORK_SMP_WEB_PORT_SERVERS = "NetworkSMPWebPortServers" private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT = "NetworkTCPConnectTimeout" private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT = "NetworkTCPTimeout" private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb" + private const val SHARED_PREFS_NETWORK_RCV_CONCURRENCY = "networkRcvConcurrency" private const val SHARED_PREFS_NETWORK_SMP_PING_INTERVAL = "NetworkSMPPingInterval" private const val SHARED_PREFS_NETWORK_SMP_PING_COUNT = "NetworkSMPPingCount" private const val SHARED_PREFS_NETWORK_ENABLE_KEEP_ALIVE = "NetworkEnableKeepAlive" @@ -292,9 +410,10 @@ class AppPreferences { private const val SHARED_PREFS_NETWORK_TCP_KEEP_INTVL = "NetworkTCPKeepIntvl" private const val SHARED_PREFS_NETWORK_TCP_KEEP_CNT = "NetworkTCPKeepCnt" private const val SHARED_PREFS_INCOGNITO = "Incognito" - private const val SHARED_PREFS_CONNECT_VIA_LINK_TAB = "ConnectViaLinkTab" private const val SHARED_PREFS_LIVE_MESSAGE_ALERT_SHOWN = "LiveMessageAlertShown" private const val SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE = "ShowHiddenProfilesNotice" + private const val SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN = "OneHandUICardShown" + private const val SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN = "AddressCreationCardShown" private const val SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT = "ShowMuteProfileAlert" private const val SHARED_PREFS_STORE_DB_PASSPHRASE = "StoreDBPassphrase" private const val SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE = "InitialRandomDBPassphrase" @@ -305,12 +424,24 @@ class AppPreferences { private const val SHARED_PREFS_ENCRYPTED_SELF_DESTRUCT_PASSPHRASE = "EncryptedSelfDestructPassphrase" private const val SHARED_PREFS_INITIALIZATION_VECTOR_SELF_DESTRUCT_PASSPHRASE = "InitializationVectorSelfDestructPassphrase" private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt" + private const val SHARED_PREFS_NEW_DATABASE_INITIALIZED = "NewDatabaseInitialized" + private const val SHARED_PREFS_SHOULD_IMPORT_APP_SETTINGS = "ShouldImportAppSettings" private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades" + private const val SHARED_PREFS_ONE_HAND_UI = "OneHandUI" + private const val SHARED_PREFS_CHAT_BOTTOM_BAR = "ChatBottomBar" private const val SHARED_PREFS_SELF_DESTRUCT = "LocalAuthenticationSelfDestruct" private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName" + private const val SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED = "PQExperimentalEnabled" // no longer used private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme" + private const val SHARED_PREFS_CURRENT_THEME_IDs = "CurrentThemeIds" private const val SHARED_PREFS_SYSTEM_DARK_THEME = "SystemDarkTheme" - private const val SHARED_PREFS_THEMES = "Themes" + private const val SHARED_PREFS_THEMES_OLD = "Themes" + private const val SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS = "ProfileImageCornerRadius" + private const val SHARED_PREFS_CHAT_ITEM_ROUNDNESS = "ChatItemRoundness" + private const val SHARED_PREFS_CHAT_ITEM_TAIL = "ChatItemTail" + private const val SHARED_PREFS_FONT_SCALE = "FontScale" + private const val SHARED_PREFS_DENSITY_SCALE = "DensityScale" + private const val SHARED_PREFS_IN_APP_BARS_ALPHA = "InAppBarsAlpha" private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion" private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode" private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime" @@ -320,6 +451,14 @@ class AppPreferences { private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto" private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast" private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState" + private const val SHARED_PREFS_SHOW_DELETE_CONVERSATION_NOTICE = "showDeleteConversationNotice" + private const val SHARED_PREFS_SHOW_DELETE_CONTACT_NOTICE = "showDeleteContactNotice" + private const val SHARED_PREFS_SHOW_SENT_VIA_RPOXY = "showSentViaProxy" + + private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled" + private const val SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS = "iOSCallKitCallsInRecents" + + private var themeOverridesStore: List<ThemeOverrides>? = null } } @@ -329,38 +468,54 @@ object ChatController { var ctrl: ChatCtrl? = -1 val appPrefs: AppPreferences by lazy { AppPreferences() } + val messagesChannel: Channel<API> = Channel() + val chatModel = ChatModel private var receiverStarted = false var lastMsgReceivedTimestamp: Long = System.currentTimeMillis() private set - private fun currentUserId(funcName: String): Long { + fun hasChatCtrl() = ctrl != -1L && ctrl != null + + suspend fun getAgentSubsTotal(rh: Long?): Pair<SMPServerSubs, Boolean>? { + val userId = currentUserId("getAgentSubsTotal") + + val r = sendCmd(rh, CC.GetAgentSubsTotal(userId), log = false) + if (r is API.Result && r.res is CR.AgentSubsTotal) return r.res.subsTotal to r.res.hasSession + Log.e(TAG, "getAgentSubsTotal bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun getAgentServersSummary(rh: Long?): PresentedServersSummary? { + val userId = currentUserId("getAgentServersSummary") + + val r = sendCmd(rh, CC.GetAgentServersSummary(userId), log = false) + if (r is API.Result && r.res is CR.AgentServersSummary) return r.res.serversSummary + Log.e(TAG, "getAgentServersSummary bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun resetAgentServersStats(rh: Long?): Boolean = sendCommandOkResp(rh, CC.ResetAgentServersStats()) + + private suspend fun currentUserId(funcName: String): Long = changingActiveUserMutex.withLock { val userId = chatModel.currentUser.value?.userId if (userId == null) { val error = "$funcName: no current user" Log.e(TAG, error) throw Exception(error) } - return userId + userId } suspend fun startChat(user: User) { Log.d(TAG, "user: $user") try { - if (chatModel.chatRunning.value == true) return apiSetNetworkConfig(getNetCfg()) - apiSetTempFolder(coreTmpDir.absolutePath) - apiSetFilesFolder(appFilesDir.absolutePath) - if (appPlatform.isDesktop) { - apiSetRemoteHostsFolder(remoteHostsDir.absolutePath) - } - apiSetXFTPConfig(getXFTPCfg()) - apiSetEncryptLocalFiles(appPrefs.privacyEncryptLocalFiles.get()) - val justStarted = apiStartChat() + val chatRunning = apiCheckChatRunning() val users = listUsers(null) chatModel.users.clear() chatModel.users.addAll(users) - if (justStarted) { + if (!chatRunning) { chatModel.currentUser.value = user chatModel.localUserCreated.value = true getUserChatData(null) @@ -368,15 +523,20 @@ object ChatController { chatModel.chatRunning.value = true startReceiver() setLocalDeviceName(appPrefs.deviceNameForRemoteAccess.get()!!) + if (appPreferences.onboardingStage.get() == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) { + chatModel.setDeliveryReceipts.value = true + } Log.d(TAG, "startChat: started") } else { - updatingChatsMutex.withLock { + withContext(Dispatchers.Main) { val chats = apiGetChats(null) - chatModel.updateChats(chats) + chatModel.chatsContext.updateChats(chats) } Log.d(TAG, "startChat: running") } - } catch (e: Error) { + apiStartChat() + appPrefs.chatStopped.set(false) + } catch (e: Throwable) { Log.e(TAG, "failed starting chat $e") throw e } @@ -386,13 +546,6 @@ object ChatController { Log.d(TAG, "user: null") try { if (chatModel.chatRunning.value == true) return - apiSetTempFolder(coreTmpDir.absolutePath) - apiSetFilesFolder(appFilesDir.absolutePath) - if (appPlatform.isDesktop) { - apiSetRemoteHostsFolder(remoteHostsDir.absolutePath) - } - apiSetXFTPConfig(getXFTPCfg()) - apiSetEncryptLocalFiles(appPrefs.privacyEncryptLocalFiles.get()) chatModel.users.clear() chatModel.currentUser.value = null chatModel.localUserCreated.value = false @@ -401,12 +554,30 @@ object ChatController { startReceiver() setLocalDeviceName(appPrefs.deviceNameForRemoteAccess.get()!!) Log.d(TAG, "startChat: started without user") - } catch (e: Error) { + } catch (e: Throwable) { Log.e(TAG, "failed starting chat without user $e") throw e } } + suspend fun startChatWithTemporaryDatabase(ctrl: ChatCtrl, netCfg: NetCfg): User? { + Log.d(TAG, "startChatWithTemporaryDatabase") + val migrationActiveUser = apiGetActiveUser(null, ctrl) ?: apiCreateActiveUser(null, Profile(displayName = "Temp", fullName = ""), ctrl = ctrl) + if (!apiSetNetworkConfig(netCfg, ctrl = ctrl)) { + Log.e(TAG, "Error setting network config, stopping migration") + return null + } + apiSetAppFilePaths( + getMigrationTempFilesDirectory().absolutePath, + getMigrationTempFilesDirectory().absolutePath, + wallpapersDir.parentFile.absolutePath, + remoteHostsDir.absolutePath, + ctrl + ) + apiStartChat(ctrl) + return migrationActiveUser + } + suspend fun changeActiveUser(rhId: Long?, toUserId: Long, viewPwd: String?) { try { changeActiveUser_(rhId, toUserId, viewPwd) @@ -416,15 +587,22 @@ object ChatController { } } - suspend fun changeActiveUser_(rhId: Long?, toUserId: Long, viewPwd: String?) { - val currentUser = apiSetActiveUser(rhId, toUserId, viewPwd) - chatModel.currentUser.value = currentUser + suspend fun changeActiveUser_(rhId: Long?, toUserId: Long?, viewPwd: String?) { + val prevActiveUser = chatModel.currentUser.value + val currentUser = changingActiveUserMutex.withLock { + (if (toUserId != null) apiSetActiveUser(rhId, toUserId, viewPwd) else apiGetActiveUser(rhId)).also { + chatModel.currentUser.value = it + } + } + if (prevActiveUser?.hidden == true) { + ntfManager.cancelNotificationsForUser(prevActiveUser.userId) + } val users = listUsers(rhId) chatModel.users.clear() chatModel.users.addAll(users) getUserChatData(rhId) val invitation = chatModel.callInvitations.values.firstOrNull { inv -> inv.user.userId == toUserId } - if (invitation != null) { + if (invitation != null && currentUser != null) { chatModel.callManager.reportNewIncomingCall(invitation.copy(user = currentUser)) } } @@ -433,10 +611,13 @@ object ChatController { val hasUser = chatModel.currentUser.value != null chatModel.userAddress.value = if (hasUser) apiGetUserAddress(rhId) else null chatModel.chatItemTTL.value = if (hasUser) getChatItemTTL(rhId) else ChatItemTTL.None - updatingChatsMutex.withLock { + withContext(Dispatchers.Main) { val chats = apiGetChats(rhId) - chatModel.updateChats(chats) + chatModel.chatsContext.updateChats(chats) } + chatModel.userTags.value = apiGetChatTags(rhId).takeIf { hasUser } ?: emptyList() + chatModel.activeChatTagFilter.value = null + chatModel.updateChatTags(rhId) } private fun startReceiver() { @@ -455,7 +636,22 @@ object ChatController { } try { val msg = recvMsg(ctrl) - if (msg != null) processReceivedMsg(msg) + if (msg != null) { + val finishedWithoutTimeout = withTimeoutOrNull(60_000L) { + processReceivedMsg(msg) + messagesChannel.trySend(msg) + } + if (finishedWithoutTimeout == null) { + Log.e(TAG, "Timeout reached while processing received message: " + msg.responseType) + if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.possible_slow_function_title), + text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.responseType + "\n" + Exception().stackTraceToString()), + shareText = true + ) + } + } + } } catch (e: Exception) { Log.e(TAG, "ChatController recvMsg/processReceivedMsg exception: " + e.stackTraceToString()); } catch (e: Throwable) { @@ -466,40 +662,45 @@ object ChatController { } } - suspend fun sendCmd(rhId: Long?, cmd: CC): CR { - val ctrl = ctrl ?: throw Exception("Controller is not initialized") + suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null, log: Boolean = true): API { + val ctrl = otherCtrl ?: ctrl ?: throw Exception("Controller is not initialized") return withContext(Dispatchers.IO) { val c = cmd.cmdString - chatModel.addTerminalItem(TerminalItem.cmd(rhId, cmd.obfuscated)) - Log.d(TAG, "sendCmd: ${cmd.cmdType}") - val json = if (rhId == null) chatSendCmd(ctrl, c) else chatSendRemoteCmd(ctrl, rhId.toInt(), c) - val r = APIResponse.decodeStr(json) - Log.d(TAG, "sendCmd response type ${r.resp.responseType}") - if (r.resp is CR.Response || r.resp is CR.Invalid) { - Log.d(TAG, "sendCmd response json $json") + if (log) { + chatModel.addTerminalItem(TerminalItem.cmd(rhId, cmd.obfuscated)) + Log.d(TAG, "sendCmd: ${cmd.cmdType}") } - chatModel.addTerminalItem(TerminalItem.resp(rhId, r.resp)) - r.resp + val rStr = if (rhId == null) chatSendCmd(ctrl, c) else chatSendRemoteCmd(ctrl, rhId.toInt(), c) + // coroutine was cancelled already, no need to process response (helps with apiListMembers - very heavy query in large groups) + interruptIfCancelled() + val r = json.decodeFromString<API>(rStr) + if (log) { + Log.d(TAG, "sendCmd response type ${r.responseType}") + if (r is API.Result && (r.res is CR.Response || r.res is CR.Invalid)) { + Log.d(TAG, "sendCmd response json $rStr") + } + chatModel.addTerminalItem(TerminalItem.resp(rhId, r)) + } + r } } - private fun recvMsg(ctrl: ChatCtrl): APIResponse? { - val json = chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT) - return if (json == "") { + fun recvMsg(ctrl: ChatCtrl): API? { + val rStr = chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT) + return if (rStr == "") { null } else { - val apiResp = APIResponse.decodeStr(json) - val r = apiResp.resp + val r = json.decodeFromString<API>(rStr) Log.d(TAG, "chatRecvMsg: ${r.responseType}") - if (r is CR.Response || r is CR.Invalid) Log.d(TAG, "chatRecvMsg json: $json") - apiResp + if (r is API.Result && (r.res is CR.Response || r.res is CR.Invalid)) Log.d(TAG, "chatRecvMsg json: $rStr") + r } } - suspend fun apiGetActiveUser(rh: Long?): User? { - val r = sendCmd(rh, CC.ShowActiveUser()) - if (r is CR.ActiveUser) return r.user.updateRemoteHostId(rh) + suspend fun apiGetActiveUser(rh: Long?, ctrl: ChatCtrl? = null): User? { + val r = sendCmd(rh, CC.ShowActiveUser(), ctrl) + if (r is API.Result && r.res is CR.ActiveUser) return r.res.user.updateRemoteHostId(rh) Log.d(TAG, "apiGetActiveUser: ${r.responseType} ${r.details}") if (rh == null) { chatModel.localUserCreated.value = false @@ -507,14 +708,19 @@ object ChatController { return null } - suspend fun apiCreateActiveUser(rh: Long?, p: Profile?, sameServers: Boolean = false, pastTimestamp: Boolean = false): User? { - val r = sendCmd(rh, CC.CreateActiveUser(p, sameServers = sameServers, pastTimestamp = pastTimestamp)) - if (r is CR.ActiveUser) return r.user.updateRemoteHostId(rh) - else if ( - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.DuplicateName || - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat && r.chatError.errorType is ChatErrorType.UserExists + suspend fun apiCreateActiveUser(rh: Long?, p: Profile?, pastTimestamp: Boolean = false, ctrl: ChatCtrl? = null): User? { + val r = sendCmd(rh, CC.CreateActiveUser(p, pastTimestamp = pastTimestamp), ctrl) + if (r is API.Result && r.res is CR.ActiveUser) return r.res.user.updateRemoteHostId(rh) + val e = (r as? API.Error)?.err + if ( + e is ChatError.ChatErrorStore && e.storeError is StoreError.DuplicateName || + e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.UserExists ) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_duplicate_title), generalGetString(MR.strings.failed_to_create_user_duplicate_desc)) + } else if ( + e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.InvalidDisplayName + ) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_invalid_title), generalGetString(MR.strings.failed_to_create_user_invalid_desc)) } else { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_title), r.details) } @@ -524,8 +730,8 @@ object ChatController { suspend fun listUsers(rh: Long?): List<UserInfo> { val r = sendCmd(rh, CC.ListUsers()) - if (r is CR.UsersList) { - val users = if (rh == null) r.users else r.users.map { it.copy(user = it.user.copy(remoteHostId = rh)) } + if (r is API.Result && r.res is CR.UsersList) { + val users = if (rh == null) r.res.users else r.res.users.map { it.copy(user = it.user.copy(remoteHostId = rh)) } return users.sortedBy { it.user.chatViewName } } Log.d(TAG, "listUsers: ${r.responseType} ${r.details}") @@ -534,26 +740,26 @@ object ChatController { suspend fun apiSetActiveUser(rh: Long?, userId: Long, viewPwd: String?): User { val r = sendCmd(rh, CC.ApiSetActiveUser(userId, viewPwd)) - if (r is CR.ActiveUser) return r.user.updateRemoteHostId(rh) + if (r is API.Result && r.res is CR.ActiveUser) return r.res.user.updateRemoteHostId(rh) Log.d(TAG, "apiSetActiveUser: ${r.responseType} ${r.details}") throw Exception("failed to set the user as active ${r.responseType} ${r.details}") } suspend fun apiSetAllContactReceipts(rh: Long?, enable: Boolean) { val r = sendCmd(rh, CC.SetAllContactReceipts(enable)) - if (r is CR.CmdOk) return + if (r.result is CR.CmdOk) return throw Exception("failed to set receipts for all users ${r.responseType} ${r.details}") } suspend fun apiSetUserContactReceipts(u: User, userMsgReceiptSettings: UserMsgReceiptSettings) { val r = sendCmd(u.remoteHostId, CC.ApiSetUserContactReceipts(u.userId, userMsgReceiptSettings)) - if (r is CR.CmdOk) return + if (r.result is CR.CmdOk) return throw Exception("failed to set receipts for user contacts ${r.responseType} ${r.details}") } suspend fun apiSetUserGroupReceipts(u: User, userMsgReceiptSettings: UserMsgReceiptSettings) { val r = sendCmd(u.remoteHostId, CC.ApiSetUserGroupReceipts(u.userId, userMsgReceiptSettings)) - if (r is CR.CmdOk) return + if (r.result is CR.CmdOk) return throw Exception("failed to set receipts for user groups ${r.responseType} ${r.details}") } @@ -571,438 +777,694 @@ object ChatController { private suspend fun setUserPrivacy(rh: Long?, cmd: CC): User { val r = sendCmd(rh, cmd) - if (r is CR.UserPrivacy) return r.updatedUser.updateRemoteHostId(rh) + if (r is API.Result && r.res is CR.UserPrivacy) return r.res.updatedUser.updateRemoteHostId(rh) else throw Exception("Failed to change user privacy: ${r.responseType} ${r.details}") } suspend fun apiDeleteUser(u: User, delSMPQueues: Boolean, viewPwd: String?) { val r = sendCmd(u.remoteHostId, CC.ApiDeleteUser(u.userId, delSMPQueues, viewPwd)) - if (r is CR.CmdOk) return + if (r.result is CR.CmdOk) return Log.d(TAG, "apiDeleteUser: ${r.responseType} ${r.details}") throw Exception("failed to delete the user ${r.responseType} ${r.details}") } - suspend fun apiStartChat(): Boolean { - val r = sendCmd(null, CC.StartChat(expire = true)) - when (r) { + suspend fun apiStartChat(ctrl: ChatCtrl? = null): Boolean { + val r = sendCmd(null, CC.StartChat(mainApp = true), ctrl) + when (r.result) { is CR.ChatStarted -> return true is CR.ChatRunning -> return false - else -> throw Error("failed starting chat: ${r.responseType} ${r.details}") + else -> throw Exception("failed starting chat: ${r.responseType} ${r.details}") + } + } + + private suspend fun apiCheckChatRunning(): Boolean { + val r = sendCmd(null, CC.CheckChatRunning()) + when (r.result) { + is CR.ChatRunning -> return true + is CR.ChatStopped -> return false + else -> throw Exception("failed check chat running: ${r.responseType} ${r.details}") } } suspend fun apiStopChat(): Boolean { val r = sendCmd(null, CC.ApiStopChat()) - when (r) { - is CR.ChatStopped -> return true - else -> throw Error("failed stopping chat: ${r.responseType} ${r.details}") - } + if (r.result is CR.ChatStopped) return true + throw Exception("failed stopping chat: ${r.responseType} ${r.details}") } - private suspend fun apiSetTempFolder(tempFolder: String) { - val r = sendCmd(null, CC.SetTempFolder(tempFolder)) - if (r is CR.CmdOk) return - throw Error("failed to set temp folder: ${r.responseType} ${r.details}") - } - - private suspend fun apiSetFilesFolder(filesFolder: String) { - val r = sendCmd(null, CC.SetFilesFolder(filesFolder)) - if (r is CR.CmdOk) return - throw Error("failed to set files folder: ${r.responseType} ${r.details}") - } - - private suspend fun apiSetRemoteHostsFolder(remoteHostsFolder: String) { - val r = sendCmd(null, CC.SetRemoteHostsFolder(remoteHostsFolder)) - if (r is CR.CmdOk) return - throw Error("failed to set remote hosts folder: ${r.responseType} ${r.details}") - } - - suspend fun apiSetXFTPConfig(cfg: XFTPFileConfig?) { - val r = sendCmd(null, CC.ApiSetXFTPConfig(cfg)) - if (r is CR.CmdOk) return - throw Error("apiSetXFTPConfig bad response: ${r.responseType} ${r.details}") + suspend fun apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String, remoteHostsFolder: String, ctrl: ChatCtrl? = null) { + val r = sendCmd(null, CC.ApiSetAppFilePaths(filesFolder, tempFolder, assetsFolder, remoteHostsFolder), ctrl) + if (r.result is CR.CmdOk) return + throw Exception("failed to set app file paths: ${r.responseType} ${r.details}") } suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetEncryptLocalFiles(enable)) - suspend fun apiExportArchive(config: ArchiveConfig) { + suspend fun apiSaveAppSettings(settings: AppSettings) { + val r = sendCmd(null, CC.ApiSaveSettings(settings)) + if (r.result is CR.CmdOk) return + throw Exception("failed to set app settings: ${r.responseType} ${r.details}") + } + + suspend fun apiGetAppSettings(settings: AppSettings): AppSettings { + val r = sendCmd(null, CC.ApiGetSettings(settings)) + if (r is API.Result && r.res is CR.AppSettingsR) return r.res.appSettings + throw Exception("failed to get app settings: ${r.responseType} ${r.details}") + } + + suspend fun apiExportArchive(config: ArchiveConfig): List<ArchiveError> { val r = sendCmd(null, CC.ApiExportArchive(config)) - if (r is CR.CmdOk) return - throw Error("failed to export archive: ${r.responseType} ${r.details}") + if (r is API.Result && r.res is CR.ArchiveExported) return r.res.archiveErrors + throw Exception("failed to export archive: ${r.responseType} ${r.details}") } suspend fun apiImportArchive(config: ArchiveConfig): List<ArchiveError> { val r = sendCmd(null, CC.ApiImportArchive(config)) - if (r is CR.ArchiveImported) return r.archiveErrors - throw Error("failed to import archive: ${r.responseType} ${r.details}") + if (r is API.Result && r.res is CR.ArchiveImported) return r.res.archiveErrors + throw Exception("failed to import archive: ${r.responseType} ${r.details}") } suspend fun apiDeleteStorage() { val r = sendCmd(null, CC.ApiDeleteStorage()) - if (r is CR.CmdOk) return - throw Error("failed to delete storage: ${r.responseType} ${r.details}") + if (r.result is CR.CmdOk) return + throw Exception("failed to delete storage: ${r.responseType} ${r.details}") } - suspend fun apiStorageEncryption(currentKey: String = "", newKey: String = ""): CR.ChatCmdError? { + suspend fun apiStorageEncryption(currentKey: String = "", newKey: String = ""): ChatError? { val r = sendCmd(null, CC.ApiStorageEncryption(DBEncryptionConfig(currentKey, newKey))) - if (r is CR.CmdOk) return null - else if (r is CR.ChatCmdError) return r + if (r.result is CR.CmdOk) return null + else if (r is API.Error) return r.err throw Exception("failed to set storage encryption: ${r.responseType} ${r.details}") } + suspend fun testStorageEncryption(key: String, ctrl: ChatCtrl? = null): ChatError? { + val r = sendCmd(null, CC.TestStorageEncryption(key), ctrl) + if (r.result is CR.CmdOk) return null + else if (r is API.Error) return r.err + throw Exception("failed to test storage encryption: ${r.responseType} ${r.details}") + } + suspend fun apiGetChats(rh: Long?): List<Chat> { val userId = kotlin.runCatching { currentUserId("apiGetChats") }.getOrElse { return emptyList() } val r = sendCmd(rh, CC.ApiGetChats(userId)) - if (r is CR.ApiChats) return if (rh == null) r.chats else r.chats.map { it.copy(remoteHostId = rh) } + if (r is API.Result && r.res is CR.ApiChats) return if (rh == null) r.res.chats else r.res.chats.map { it.copy(remoteHostId = rh) } Log.e(TAG, "failed getting the list of chats: ${r.responseType} ${r.details}") AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_parse_chats_title), generalGetString(MR.strings.contact_developers)) return emptyList() } - suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, pagination: ChatPagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), search: String = ""): Chat? { - val r = sendCmd(rh, CC.ApiGetChat(type, id, pagination, search)) - if (r is CR.ApiChat) return if (rh == null) r.chat else r.chat.copy(remoteHostId = rh) - Log.e(TAG, "apiGetChat bad response: ${r.responseType} ${r.details}") - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_parse_chat_title), generalGetString(MR.strings.contact_developers)) + private suspend fun apiGetChatTags(rh: Long?): List<ChatTag>?{ + val userId = currentUserId("apiGetChatTags") + val r = sendCmd(rh, CC.ApiGetChatTags(userId)) + if (r is API.Result && r.res is CR.ChatTags) return r.res.userTags + Log.e(TAG, "apiGetChatTags bad response: ${r.responseType} ${r.details}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_loading_chat_tags), "${r.responseType}: ${r.details}") return null } - suspend fun apiSendMessage(rh: Long?, type: ChatType, id: Long, file: CryptoFile? = null, quotedItemId: Long? = null, mc: MsgContent, live: Boolean = false, ttl: Int? = null): AChatItem? { - val cmd = CC.ApiSendMessage(type, id, file, quotedItemId, mc, live, ttl) + suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, contentTag: MsgContentTag? = null, pagination: ChatPagination, search: String = ""): Pair<Chat, NavigationInfo>? { + val r = sendCmd(rh, CC.ApiGetChat(type, id, contentTag, pagination, search)) + if (r is API.Result && r.res is CR.ApiChat) return if (rh == null) r.res.chat to r.res.navInfo else r.res.chat.copy(remoteHostId = rh) to r.res.navInfo + Log.e(TAG, "apiGetChat bad response: ${r.responseType} ${r.details}") + val e = (r as? API.Error)?.err + if (pagination is ChatPagination.Around && e is ChatError.ChatErrorStore && e.storeError is StoreError.ChatItemNotFound) { + showQuotedItemDoesNotExistAlert() + } else { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_parse_chat_title), generalGetString(MR.strings.contact_developers)) + } + return null + } + + suspend fun apiCreateChatTag(rh: Long?, tag: ChatTagData): List<ChatTag>? { + val r = sendCmd(rh, CC.ApiCreateChatTag(tag)) + if (r is API.Result && r.res is CR.ChatTags) return r.res.userTags + Log.e(TAG, "apiCreateChatTag bad response: ${r.responseType} ${r.details}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_creating_chat_tags), "${r.responseType}: ${r.details}") + return null + } + + suspend fun apiSetChatTags(rh: Long?, type: ChatType, id: Long, tagIds: List<Long>): Pair<List<ChatTag>, List<Long>>? { + val r = sendCmd(rh, CC.ApiSetChatTags(type, id, tagIds)) + if (r is API.Result && r.res is CR.TagsUpdated) return r.res.userTags to r.res.chatTags + Log.e(TAG, "apiSetChatTags bad response: ${r.responseType} ${r.details}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_updating_chat_tags), "${r.responseType}: ${r.details}") + return null + } + + suspend fun apiDeleteChatTag(rh: Long?, tagId: Long) = sendCommandOkResp(rh, CC.ApiDeleteChatTag(tagId)) + + suspend fun apiUpdateChatTag(rh: Long?, tagId: Long, tag: ChatTagData) = sendCommandOkResp(rh, CC.ApiUpdateChatTag(tagId, tag)) + + suspend fun apiReorderChatTags(rh: Long?, tagIds: List<Long>) = sendCommandOkResp(rh, CC.ApiReorderChatTags(tagIds)) + + suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, live: Boolean = false, ttl: Int? = null, composedMessages: List<ComposedMessage>): List<AChatItem>? { + val cmd = CC.ApiSendMessages(type, id, live, ttl, composedMessages) + return processSendMessageCmd(rh, cmd) + } + + private suspend fun processSendMessageCmd(rh: Long?, cmd: CC): List<AChatItem>? { val r = sendCmd(rh, cmd) - return when (r) { - is CR.NewChatItem -> r.chatItem + return when { + r is API.Result && r.res is CR.NewChatItems -> r.res.chatItems + r is API.Error && r.err is ChatError.ChatErrorStore && r.err.storeError is StoreError.LargeMsg && cmd is CC.ApiSendMessages -> { + val mc = cmd.composedMessages.last().msgContent + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.maximum_message_size_title), + if (mc is MsgContent.MCImage || mc is MsgContent.MCVideo || mc is MsgContent.MCLink) { + generalGetString(MR.strings.maximum_message_size_reached_non_text) + } else { + generalGetString(MR.strings.maximum_message_size_reached_text) + } + ) + null + } + r is API.Error && r.err is ChatError.ChatErrorStore && r.err.storeError is StoreError.LargeMsg && cmd is CC.ApiForwardChatItems -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.maximum_message_size_title), + generalGetString(MR.strings.maximum_message_size_reached_forwarding) + ) + null + } else -> { if (!(networkErrorAlert(r))) { - apiErrorAlert("apiSendMessage", generalGetString(MR.strings.error_sending_message), r) + apiErrorAlert("processSendMessageCmd", generalGetString(MR.strings.error_sending_message), r) } null } } } - suspend fun apiGetChatItemInfo(rh: Long?, type: ChatType, id: Long, itemId: Long): ChatItemInfo? { - return when (val r = sendCmd(rh, CC.ApiGetChatItemInfo(type, id, itemId))) { - is CR.ApiChatItemInfo -> r.chatItemInfo - else -> { - apiErrorAlert("apiGetChatItemInfo", generalGetString(MR.strings.error_loading_details), r) - null - } - } + suspend fun apiCreateChatItems(rh: Long?, noteFolderId: Long, composedMessages: List<ComposedMessage>): List<AChatItem>? { + val cmd = CC.ApiCreateChatItems(noteFolderId, composedMessages) + val r = sendCmd(rh, cmd) + if (r is API.Result && r.res is CR.NewChatItems) return r.res.chatItems + apiErrorAlert("apiCreateChatItems", generalGetString(MR.strings.error_creating_message), r) + return null } - suspend fun apiUpdateChatItem(rh: Long?, type: ChatType, id: Long, itemId: Long, mc: MsgContent, live: Boolean = false): AChatItem? { - val r = sendCmd(rh, CC.ApiUpdateChatItem(type, id, itemId, mc, live)) - if (r is CR.ChatItemUpdated) return r.chatItem + suspend fun apiReportMessage(rh: Long?, groupId: Long, chatItemId: Long, reportReason: ReportReason, reportText: String): List<AChatItem>? { + val r = sendCmd(rh, CC.ApiReportMessage(groupId, chatItemId, reportReason, reportText)) + if (r is API.Result && r.res is CR.NewChatItems) return r.res.chatItems + apiErrorAlert("apiReportMessage", generalGetString(MR.strings.error_creating_report), r) + return null + } + + suspend fun apiGetChatItemInfo(rh: Long?, type: ChatType, id: Long, itemId: Long): ChatItemInfo? { + val r = sendCmd(rh, CC.ApiGetChatItemInfo(type, id, itemId)) + if (r is API.Result && r.res is CR.ApiChatItemInfo) return r.res.chatItemInfo + apiErrorAlert("apiGetChatItemInfo", generalGetString(MR.strings.error_loading_details), r) + return null + } + + suspend fun apiForwardChatItems(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemIds: List<Long>, ttl: Int?): List<ChatItem>? { + val cmd = CC.ApiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl) + return processSendMessageCmd(rh, cmd)?.map { it.chatItem } + } + + suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, chatItemIds: List<Long>): CR.ForwardPlan? { + val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, chatItemIds)) + if (r is API.Result && r.res is CR.ForwardPlan) return r.res + apiErrorAlert("apiPlanForwardChatItems", generalGetString(MR.strings.error_forwarding_messages), r) + return null + } + + suspend fun apiUpdateChatItem(rh: Long?, type: ChatType, id: Long, itemId: Long, updatedMessage: UpdatedMessage, live: Boolean = false): AChatItem? { + val r = sendCmd(rh, CC.ApiUpdateChatItem(type, id, itemId, updatedMessage, live)) + when { + r is API.Result && r.res is CR.ChatItemUpdated -> return r.res.chatItem + r is API.Result && r.res is CR.ChatItemNotChanged -> return r.res.chatItem + r is API.Error && r.err is ChatError.ChatErrorStore && r.err.storeError is StoreError.LargeMsg -> { + val mc = updatedMessage.msgContent + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.maximum_message_size_title), + if (mc is MsgContent.MCImage || mc is MsgContent.MCVideo || mc is MsgContent.MCLink) { + generalGetString(MR.strings.maximum_message_size_reached_non_text) + } else { + generalGetString(MR.strings.maximum_message_size_reached_text) + } + ) + return null + } + } + Log.e(TAG, "apiUpdateChatItem bad response: ${r.responseType} ${r.details}") return null } suspend fun apiChatItemReaction(rh: Long?, type: ChatType, id: Long, itemId: Long, add: Boolean, reaction: MsgReaction): ChatItem? { val r = sendCmd(rh, CC.ApiChatItemReaction(type, id, itemId, add, reaction)) - if (r is CR.ChatItemReaction) return r.reaction.chatReaction.chatItem + if (r is API.Result && r.res is CR.ChatItemReaction) return r.res.reaction.chatReaction.chatItem Log.e(TAG, "apiUpdateChatItem bad response: ${r.responseType} ${r.details}") return null } - suspend fun apiDeleteChatItem(rh: Long?, type: ChatType, id: Long, itemId: Long, mode: CIDeleteMode): CR.ChatItemDeleted? { - val r = sendCmd(rh, CC.ApiDeleteChatItem(type, id, itemId, mode)) - if (r is CR.ChatItemDeleted) return r + suspend fun apiGetReactionMembers(rh: Long?, groupId: Long, itemId: Long, reaction: MsgReaction): List<MemberReaction>? { + val userId = currentUserId("apiGetReactionMembers") + val r = sendCmd(rh, CC.ApiGetReactionMembers(userId, groupId, itemId, reaction)) + if (r is API.Result && r.res is CR.ReactionMembers) return r.res.memberReactions + Log.e(TAG, "apiGetReactionMembers bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun apiDeleteChatItems(rh: Long?, type: ChatType, id: Long, itemIds: List<Long>, mode: CIDeleteMode): List<ChatItemDeletion>? { + val r = sendCmd(rh, CC.ApiDeleteChatItem(type, id, itemIds, mode)) + if (r is API.Result && r.res is CR.ChatItemsDeleted) return r.res.chatItemDeletions Log.e(TAG, "apiDeleteChatItem bad response: ${r.responseType} ${r.details}") return null } - suspend fun apiDeleteMemberChatItem(rh: Long?, groupId: Long, groupMemberId: Long, itemId: Long): Pair<ChatItem, ChatItem?>? { - val r = sendCmd(rh, CC.ApiDeleteMemberChatItem(groupId, groupMemberId, itemId)) - if (r is CR.ChatItemDeleted) return r.deletedChatItem.chatItem to r.toChatItem?.chatItem + suspend fun apiDeleteMemberChatItems(rh: Long?, groupId: Long, itemIds: List<Long>): List<ChatItemDeletion>? { + val r = sendCmd(rh, CC.ApiDeleteMemberChatItem(groupId, itemIds)) + if (r is API.Result && r.res is CR.ChatItemsDeleted) return r.res.chatItemDeletions Log.e(TAG, "apiDeleteMemberChatItem bad response: ${r.responseType} ${r.details}") return null } - suspend fun getUserProtoServers(rh: Long?, serverProtocol: ServerProtocol): UserProtocolServers? { - val userId = kotlin.runCatching { currentUserId("getUserProtoServers") }.getOrElse { return null } - val r = sendCmd(rh, CC.APIGetUserProtoServers(userId, serverProtocol)) - return if (r is CR.UserProtoServers) { if (rh == null) r.servers else r.servers.copy(protoServers = r.servers.protoServers.map { it.copy(remoteHostId = rh) }) } - else { - Log.e(TAG, "getUserProtoServers bad response: ${r.responseType} ${r.details}") - AlertManager.shared.showAlertMsg( - generalGetString(if (serverProtocol == ServerProtocol.SMP) MR.strings.error_loading_smp_servers else MR.strings.error_loading_xftp_servers), - "${r.responseType}: ${r.details}" - ) - null - } + suspend fun apiArchiveReceivedReports(rh: Long?, groupId: Long): CR.GroupChatItemsDeleted? { + val r = sendCmd(rh, CC.ApiArchiveReceivedReports(groupId)) + if (r is API.Result && r.res is CR.GroupChatItemsDeleted) return r.res + Log.e(TAG, "apiArchiveReceivedReports bad response: ${r.responseType} ${r.details}") + return null } - suspend fun setUserProtoServers(rh: Long?, serverProtocol: ServerProtocol, servers: List<ServerCfg>): Boolean { - val userId = kotlin.runCatching { currentUserId("setUserProtoServers") }.getOrElse { return false } - val r = sendCmd(rh, CC.APISetUserProtoServers(userId, serverProtocol, servers)) - return when (r) { - is CR.CmdOk -> true - else -> { - Log.e(TAG, "setUserProtoServers bad response: ${r.responseType} ${r.details}") - AlertManager.shared.showAlertMsg( - generalGetString(if (serverProtocol == ServerProtocol.SMP) MR.strings.error_saving_smp_servers else MR.strings.error_saving_xftp_servers), - generalGetString(if (serverProtocol == ServerProtocol.SMP) MR.strings.ensure_smp_server_address_are_correct_format_and_unique else MR.strings.ensure_xftp_server_address_are_correct_format_and_unique) - ) - false - } - } + suspend fun apiDeleteReceivedReports(rh: Long?, groupId: Long, itemIds: List<Long>, mode: CIDeleteMode): List<ChatItemDeletion>? { + val r = sendCmd(rh, CC.ApiDeleteReceivedReports(groupId, itemIds, mode)) + if (r is API.Result && r.res is CR.ChatItemsDeleted) return r.res.chatItemDeletions + Log.e(TAG, "apiDeleteReceivedReports bad response: ${r.responseType} ${r.details}") + return null } suspend fun testProtoServer(rh: Long?, server: String): ProtocolTestFailure? { val userId = currentUserId("testProtoServer") val r = sendCmd(rh, CC.APITestProtoServer(userId, server)) - return when (r) { - is CR.ServerTestResult -> r.testFailure - else -> { - Log.e(TAG, "testProtoServer bad response: ${r.responseType} ${r.details}") - throw Exception("testProtoServer bad response: ${r.responseType} ${r.details}") - } - } + if (r is API.Result && r.res is CR.ServerTestResult) return r.res.testFailure + Log.e(TAG, "testProtoServer bad response: ${r.responseType} ${r.details}") + throw Exception("testProtoServer bad response: ${r.responseType} ${r.details}") + } + + suspend fun getServerOperators(rh: Long?): ServerOperatorConditionsDetail? { + val r = sendCmd(rh, CC.ApiGetServerOperators()) + if (r is API.Result && r.res is CR.ServerOperatorConditions) return r.res.conditions + Log.e(TAG, "getServerOperators bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun setServerOperators(rh: Long?, operators: List<ServerOperator>): ServerOperatorConditionsDetail? { + val r = sendCmd(rh, CC.ApiSetServerOperators(operators)) + if (r is API.Result && r.res is CR.ServerOperatorConditions) return r.res.conditions + Log.e(TAG, "setServerOperators bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun getUserServers(rh: Long?): List<UserOperatorServers>? { + val userId = currentUserId("getUserServers") + val r = sendCmd(rh, CC.ApiGetUserServers(userId)) + if (r is API.Result && r.res is CR.UserServers) return r.res.userServers + Log.e(TAG, "getUserServers bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun setUserServers(rh: Long?, userServers: List<UserOperatorServers>): Boolean { + val userId = currentUserId("setUserServers") + val r = sendCmd(rh, CC.ApiSetUserServers(userId, userServers)) + if (r.result is CR.CmdOk) return true + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.failed_to_save_servers), + "${r.responseType}: ${r.details}" + ) + Log.e(TAG, "setUserServers bad response: ${r.responseType} ${r.details}") + return false + } + + suspend fun validateServers(rh: Long?, userServers: List<UserOperatorServers>): List<UserServersError>? { + val userId = currentUserId("validateServers") + val r = sendCmd(rh, CC.ApiValidateServers(userId, userServers)) + if (r is API.Result && r.res is CR.UserServersValidation) return r.res.serverErrors + Log.e(TAG, "validateServers bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun getUsageConditions(rh: Long?): Triple<UsageConditionsDetail, String?, UsageConditionsDetail?>? { + val r = sendCmd(rh, CC.ApiGetUsageConditions()) + if (r is API.Result && r.res is CR.UsageConditions) return Triple(r.res.usageConditions, r.res.conditionsText, r.res.acceptedConditions) + Log.e(TAG, "getUsageConditions bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun setConditionsNotified(rh: Long?, conditionsId: Long): Boolean { + val r = sendCmd(rh, CC.ApiSetConditionsNotified(conditionsId)) + if (r.result is CR.CmdOk) return true + Log.e(TAG, "setConditionsNotified bad response: ${r.responseType} ${r.details}") + return false + } + + suspend fun acceptConditions(rh: Long?, conditionsId: Long, operatorIds: List<Long>): ServerOperatorConditionsDetail? { + val r = sendCmd(rh, CC.ApiAcceptConditions(conditionsId, operatorIds)) + if (r is API.Result && r.res is CR.ServerOperatorConditions) return r.res.conditions + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.error_accepting_operator_conditions), + "${r.responseType}: ${r.details}" + ) + Log.e(TAG, "acceptConditions bad response: ${r.responseType} ${r.details}") + return null } suspend fun getChatItemTTL(rh: Long?): ChatItemTTL { val userId = currentUserId("getChatItemTTL") val r = sendCmd(rh, CC.APIGetChatItemTTL(userId)) - if (r is CR.ChatItemTTL) return ChatItemTTL.fromSeconds(r.chatItemTTL) + if (r is API.Result && r.res is CR.ChatItemTTL) { + return if (r.res.chatItemTTL != null) { + ChatItemTTL.fromSeconds(r.res.chatItemTTL) + } else { + ChatItemTTL.None + } + } throw Exception("failed to get chat item TTL: ${r.responseType} ${r.details}") } suspend fun setChatItemTTL(rh: Long?, chatItemTTL: ChatItemTTL) { val userId = currentUserId("setChatItemTTL") val r = sendCmd(rh, CC.APISetChatItemTTL(userId, chatItemTTL.seconds)) - if (r is CR.CmdOk) return + if (r.result is CR.CmdOk) return throw Exception("failed to set chat item TTL: ${r.responseType} ${r.details}") } - suspend fun apiSetNetworkConfig(cfg: NetCfg): Boolean { - val r = sendCmd(null, CC.APISetNetworkConfig(cfg)) - return when (r) { - is CR.CmdOk -> true - else -> { - Log.e(TAG, "apiSetNetworkConfig bad response: ${r.responseType} ${r.details}") - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.error_setting_network_config), - "${r.responseType}: ${r.details}" - ) - false - } - } + suspend fun setChatTTL(rh: Long?, chatType: ChatType, id: Long, chatItemTTL: ChatItemTTL?) { + val userId = currentUserId("setChatTTL") + val r = sendCmd(rh, CC.APISetChatTTL(userId, chatType, id, chatItemTTL?.seconds)) + if (r.result is CR.CmdOk) return + throw Exception("failed to set chat TTL: ${r.responseType} ${r.details}") } + suspend fun apiSetNetworkConfig(cfg: NetCfg, showAlertOnError: Boolean = true, ctrl: ChatCtrl? = null): Boolean { + val r = sendCmd(null, CC.APISetNetworkConfig(cfg), ctrl) + if (r.result is CR.CmdOk) return true + Log.e(TAG, "apiSetNetworkConfig bad response: ${r.responseType} ${r.details}") + if (showAlertOnError) { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.error_setting_network_config), + "${r.responseType}: ${r.details}" + ) + } + return false + } + + suspend fun reconnectServer(rh: Long?, server: String): Boolean { + val userId = currentUserId("reconnectServer") + return sendCommandOkResp(rh, CC.ReconnectServer(userId, server)) + } + + suspend fun reconnectAllServers(rh: Long?): Boolean = sendCommandOkResp(rh, CC.ReconnectAllServers()) + suspend fun apiSetSettings(rh: Long?, type: ChatType, id: Long, settings: ChatSettings): Boolean { val r = sendCmd(rh, CC.APISetChatSettings(type, id, settings)) - return when (r) { - is CR.CmdOk -> true - else -> { - Log.e(TAG, "apiSetSettings bad response: ${r.responseType} ${r.details}") - false - } - } + if (r.result is CR.CmdOk) return true + Log.e(TAG, "apiSetSettings bad response: ${r.responseType} ${r.details}") + return false } + suspend fun apiSetNetworkInfo(networkInfo: UserNetworkInfo): Boolean = + sendCommandOkResp(null, CC.APISetNetworkInfo(networkInfo)) + suspend fun apiSetMemberSettings(rh: Long?, groupId: Long, groupMemberId: Long, memberSettings: GroupMemberSettings): Boolean = sendCommandOkResp(rh, CC.ApiSetMemberSettings(groupId, groupMemberId, memberSettings)) suspend fun apiContactInfo(rh: Long?, contactId: Long): Pair<ConnectionStats?, Profile?>? { val r = sendCmd(rh, CC.APIContactInfo(contactId)) - if (r is CR.ContactInfo) return r.connectionStats_ to r.customUserProfile + if (r is API.Result && r.res is CR.ContactInfo) return r.res.connectionStats_ to r.res.customUserProfile Log.e(TAG, "apiContactInfo bad response: ${r.responseType} ${r.details}") return null } suspend fun apiGroupMemberInfo(rh: Long?, groupId: Long, groupMemberId: Long): Pair<GroupMember, ConnectionStats?>? { val r = sendCmd(rh, CC.APIGroupMemberInfo(groupId, groupMemberId)) - if (r is CR.GroupMemberInfo) return Pair(r.member, r.connectionStats_) + if (r is API.Result && r.res is CR.GroupMemberInfo) return r.res.member to r.res.connectionStats_ Log.e(TAG, "apiGroupMemberInfo bad response: ${r.responseType} ${r.details}") return null } + suspend fun apiContactQueueInfo(rh: Long?, contactId: Long): Pair<RcvMsgInfo?, ServerQueueInfo>? { + val r = sendCmd(rh, CC.APIContactQueueInfo(contactId)) + if (r is API.Result && r.res is CR.QueueInfoR) return r.res.rcvMsgInfo to r.res.queueInfo + apiErrorAlert("apiContactQueueInfo", generalGetString(MR.strings.error), r) + return null + } + + suspend fun apiGroupMemberQueueInfo(rh: Long?, groupId: Long, groupMemberId: Long): Pair<RcvMsgInfo?, ServerQueueInfo>? { + val r = sendCmd(rh, CC.APIGroupMemberQueueInfo(groupId, groupMemberId)) + if (r is API.Result && r.res is CR.QueueInfoR) return r.res.rcvMsgInfo to r.res.queueInfo + apiErrorAlert("apiGroupMemberQueueInfo", generalGetString(MR.strings.error), r) + return null + } + suspend fun apiSwitchContact(rh: Long?, contactId: Long): ConnectionStats? { val r = sendCmd(rh, CC.APISwitchContact(contactId)) - if (r is CR.ContactSwitchStarted) return r.connectionStats + if (r is API.Result && r.res is CR.ContactSwitchStarted) return r.res.connectionStats apiErrorAlert("apiSwitchContact", generalGetString(MR.strings.error_changing_address), r) return null } suspend fun apiSwitchGroupMember(rh: Long?, groupId: Long, groupMemberId: Long): Pair<GroupMember, ConnectionStats>? { val r = sendCmd(rh, CC.APISwitchGroupMember(groupId, groupMemberId)) - if (r is CR.GroupMemberSwitchStarted) return Pair(r.member, r.connectionStats) + if (r is API.Result && r.res is CR.GroupMemberSwitchStarted) return r.res.member to r.res.connectionStats apiErrorAlert("apiSwitchGroupMember", generalGetString(MR.strings.error_changing_address), r) return null } suspend fun apiAbortSwitchContact(rh: Long?, contactId: Long): ConnectionStats? { val r = sendCmd(rh, CC.APIAbortSwitchContact(contactId)) - if (r is CR.ContactSwitchAborted) return r.connectionStats + if (r is API.Result && r.res is CR.ContactSwitchAborted) return r.res.connectionStats apiErrorAlert("apiAbortSwitchContact", generalGetString(MR.strings.error_aborting_address_change), r) return null } suspend fun apiAbortSwitchGroupMember(rh: Long?, groupId: Long, groupMemberId: Long): Pair<GroupMember, ConnectionStats>? { val r = sendCmd(rh, CC.APIAbortSwitchGroupMember(groupId, groupMemberId)) - if (r is CR.GroupMemberSwitchAborted) return Pair(r.member, r.connectionStats) + if (r is API.Result && r.res is CR.GroupMemberSwitchAborted) return r.res.member to r.res.connectionStats apiErrorAlert("apiAbortSwitchGroupMember", generalGetString(MR.strings.error_aborting_address_change), r) return null } suspend fun apiSyncContactRatchet(rh: Long?, contactId: Long, force: Boolean): ConnectionStats? { val r = sendCmd(rh, CC.APISyncContactRatchet(contactId, force)) - if (r is CR.ContactRatchetSyncStarted) return r.connectionStats + if (r is API.Result && r.res is CR.ContactRatchetSyncStarted) return r.res.connectionStats apiErrorAlert("apiSyncContactRatchet", generalGetString(MR.strings.error_synchronizing_connection), r) return null } suspend fun apiSyncGroupMemberRatchet(rh: Long?, groupId: Long, groupMemberId: Long, force: Boolean): Pair<GroupMember, ConnectionStats>? { val r = sendCmd(rh, CC.APISyncGroupMemberRatchet(groupId, groupMemberId, force)) - if (r is CR.GroupMemberRatchetSyncStarted) return Pair(r.member, r.connectionStats) + if (r is API.Result && r.res is CR.GroupMemberRatchetSyncStarted) return r.res.member to r.res.connectionStats apiErrorAlert("apiSyncGroupMemberRatchet", generalGetString(MR.strings.error_synchronizing_connection), r) return null } suspend fun apiGetContactCode(rh: Long?, contactId: Long): Pair<Contact, String>? { val r = sendCmd(rh, CC.APIGetContactCode(contactId)) - if (r is CR.ContactCode) return r.contact to r.connectionCode + if (r is API.Result && r.res is CR.ContactCode) return r.res.contact to r.res.connectionCode Log.e(TAG,"failed to get contact code: ${r.responseType} ${r.details}") return null } suspend fun apiGetGroupMemberCode(rh: Long?, groupId: Long, groupMemberId: Long): Pair<GroupMember, String>? { val r = sendCmd(rh, CC.APIGetGroupMemberCode(groupId, groupMemberId)) - if (r is CR.GroupMemberCode) return r.member to r.connectionCode + if (r is API.Result && r.res is CR.GroupMemberCode) return r.res.member to r.res.connectionCode Log.e(TAG,"failed to get group member code: ${r.responseType} ${r.details}") return null } suspend fun apiVerifyContact(rh: Long?, contactId: Long, connectionCode: String?): Pair<Boolean, String>? { - return when (val r = sendCmd(rh, CC.APIVerifyContact(contactId, connectionCode))) { - is CR.ConnectionVerified -> r.verified to r.expectedCode - else -> null - } + val r = sendCmd(rh, CC.APIVerifyContact(contactId, connectionCode)) + if (r is API.Result && r.res is CR.ConnectionVerified) return r.res.verified to r.res.expectedCode + Log.e(TAG, "apiVerifyContact bad response: ${r.responseType} ${r.details}") + return null } suspend fun apiVerifyGroupMember(rh: Long?, groupId: Long, groupMemberId: Long, connectionCode: String?): Pair<Boolean, String>? { - return when (val r = sendCmd(rh, CC.APIVerifyGroupMember(groupId, groupMemberId, connectionCode))) { - is CR.ConnectionVerified -> r.verified to r.expectedCode - else -> null - } + val r = sendCmd(rh, CC.APIVerifyGroupMember(groupId, groupMemberId, connectionCode)) + if (r is API.Result && r.res is CR.ConnectionVerified) return r.res.verified to r.res.expectedCode + Log.e(TAG, "apiVerifyGroupMember bad response: ${r.responseType} ${r.details}") + return null } - - - suspend fun apiAddContact(rh: Long?, incognito: Boolean): Pair<String, PendingContactConnection>? { - val userId = chatModel.currentUser.value?.userId ?: run { - Log.e(TAG, "apiAddContact: no current user") - return null - } - val r = sendCmd(rh, CC.APIAddContact(userId, incognito)) - return when (r) { - is CR.Invitation -> r.connReqInvitation to r.connection - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiAddContact", generalGetString(MR.strings.connection_error), r) - } - null - } + suspend fun apiAddContact(rh: Long?, incognito: Boolean): Pair<Pair<CreatedConnLink, PendingContactConnection>?, (() -> Unit)?> { + val userId = try { currentUserId("apiAddContact") } catch (e: Exception) { return null to null } + val short = appPrefs.privacyShortLinks.get() + val r = sendCmd(rh, CC.APIAddContact(userId, short = short, incognito = incognito)) + return when { + r is API.Result && r.res is CR.Invitation -> (r.res.connLinkInvitation to r.res.connection) to null + !(networkErrorAlert(r)) -> null to { apiErrorAlert("apiAddContact", generalGetString(MR.strings.connection_error), r) } + else -> null to null } } suspend fun apiSetConnectionIncognito(rh: Long?, connId: Long, incognito: Boolean): PendingContactConnection? { val r = sendCmd(rh, CC.ApiSetConnectionIncognito(connId, incognito)) - if (r is CR.ConnectionIncognitoUpdated) return r.toConnection - Log.e(TAG, "apiSetConnectionIncognito bad response: ${r.responseType} ${r.details}") - return null - } - - suspend fun apiConnectPlan(rh: Long?, connReq: String): ConnectionPlan? { - val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null } - val r = sendCmd(rh, CC.APIConnectPlan(userId, connReq)) - if (r is CR.CRConnectionPlan) return r.connectionPlan - Log.e(TAG, "apiConnectPlan bad response: ${r.responseType} ${r.details}") - return null - } - - suspend fun apiConnect(rh: Long?, incognito: Boolean, connReq: String): PendingContactConnection? { - val userId = chatModel.currentUser.value?.userId ?: run { - Log.e(TAG, "apiConnect: no current user") - return null + if (r is API.Result && r.res is CR.ConnectionIncognitoUpdated) return r.res.toConnection + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiSetConnectionIncognito", generalGetString(MR.strings.error_sending_message), r) } - val r = sendCmd(rh, CC.APIConnect(userId, incognito, connReq)) + return null + } + + suspend fun apiChangeConnectionUser(rh: Long?, connId: Long, userId: Long): PendingContactConnection? { + val r = sendCmd(rh, CC.ApiChangeConnectionUser(connId, userId)) + if (r is API.Result && r.res is CR.ConnectionUserChanged) return r.res.toConnection + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiChangeConnectionUser", generalGetString(MR.strings.error_sending_message), r) + } + return null + } + + suspend fun apiConnectPlan(rh: Long?, connLink: String): Pair<CreatedConnLink, ConnectionPlan>? { + val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null } + val r = sendCmd(rh, CC.APIConnectPlan(userId, connLink)) + if (r is API.Result && r.res is CR.CRConnectionPlan) return r.res.connLink to r.res.connectionPlan + apiConnectResponseAlert(r) + return null + } + + suspend fun apiConnect(rh: Long?, incognito: Boolean, connLink: CreatedConnLink): PendingContactConnection? { + val userId = try { currentUserId("apiConnect") } catch (e: Exception) { return null } + val r = sendCmd(rh, CC.APIConnect(userId, incognito, connLink)) when { - r is CR.SentConfirmation -> return r.connection - r is CR.SentInvitation -> return r.connection - r is CR.ContactAlreadyExists -> { + r is API.Result && r.res is CR.SentConfirmation -> return r.res.connection + r is API.Result && r.res is CR.SentInvitation -> return r.res.connection + r is API.Result && r.res is CR.ContactAlreadyExists -> AlertManager.shared.showAlertMsg( generalGetString(MR.strings.contact_already_exists), - String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), r.contact.displayName) + String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), r.res.contact.displayName) ) - return null - } - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat - && r.chatError.errorType is ChatErrorType.InvalidConnReq -> { + else -> apiConnectResponseAlert(r) + } + return null + } + + private fun apiConnectResponseAlert(r: API) { + when { + r is API.Error && r.err is ChatError.ChatErrorChat + && r.err.errorType is ChatErrorType.InvalidConnReq -> { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.invalid_connection_link), generalGetString(MR.strings.please_check_correct_link_and_maybe_ask_for_a_new_one) ) - return null } - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent - && r.chatError.agentError is AgentErrorType.SMP - && r.chatError.agentError.smpErr is SMPErrorType.AUTH -> { + r is API.Error && r.err is ChatError.ChatErrorChat + && r.err.errorType is ChatErrorType.UnsupportedConnReq -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.unsupported_connection_link), + generalGetString(MR.strings.link_requires_newer_app_version_please_upgrade) + ) + } + r is API.Error && r.err is ChatError.ChatErrorAgent + && r.err.agentError is AgentErrorType.SMP + && r.err.agentError.smpErr is SMPErrorType.AUTH -> { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.connection_error_auth), generalGetString(MR.strings.connection_error_auth_desc) ) - return null + } + r is API.Error && r.err is ChatError.ChatErrorAgent + && r.err.agentError is AgentErrorType.SMP + && r.err.agentError.smpErr is SMPErrorType.BLOCKED -> { + showContentBlockedAlert( + generalGetString(MR.strings.connection_error_blocked), + generalGetString(MR.strings.connection_error_blocked_desc).format(r.err.agentError.smpErr.blockInfo.reason.text), + ) + } + r is API.Error && r.err is ChatError.ChatErrorAgent + && r.err.agentError is AgentErrorType.SMP + && r.err.agentError.smpErr is SMPErrorType.QUOTA -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.connection_error_quota), + generalGetString(MR.strings.connection_error_quota_desc) + ) } else -> { if (!(networkErrorAlert(r))) { apiErrorAlert("apiConnect", generalGetString(MR.strings.connection_error), r) } - return null } } } suspend fun apiConnectContactViaAddress(rh: Long?, incognito: Boolean, contactId: Long): Contact? { - val userId = chatModel.currentUser.value?.userId ?: run { - Log.e(TAG, "apiConnectContactViaAddress: no current user") - return null - } + val userId = try { currentUserId("apiConnectContactViaAddress") } catch (e: Exception) { return null } val r = sendCmd(rh, CC.ApiConnectContactViaAddress(userId, incognito, contactId)) - when { - r is CR.SentInvitationToContact -> return r.contact - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiConnectContactViaAddress", generalGetString(MR.strings.connection_error), r) - } - return null + if (r is API.Result && r.res is CR.SentInvitationToContact) return r.res.contact + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiConnectContactViaAddress", generalGetString(MR.strings.connection_error), r) + } + return null + } + + suspend fun deleteChat(chat: Chat, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) { + val cInfo = chat.chatInfo + if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, chatDeleteMode = chatDeleteMode)) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(chat.remoteHostId, cInfo.id) } } } - suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, notify: Boolean? = null): Boolean { - val r = sendCmd(rh, CC.ApiDeleteChat(type, id, notify)) - when { - r is CR.ContactDeleted && type == ChatType.Direct -> return true - r is CR.ContactConnectionDeleted && type == ChatType.ContactConnection -> return true - r is CR.GroupDeletedUser && type == ChatType.Group -> return true + suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Boolean { + chatModel.deletedChats.value += rh to type.type + id + val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode)) + val res = r.result + val success = when { + res is CR.ContactDeleted && type == ChatType.Direct -> true + res is CR.ContactConnectionDeleted && type == ChatType.ContactConnection -> true + res is CR.GroupDeletedUser && type == ChatType.Group -> true else -> { val titleId = when (type) { ChatType.Direct -> MR.strings.error_deleting_contact ChatType.Group -> MR.strings.error_deleting_group + ChatType.Local -> MR.strings.error_deleting_note_folder ChatType.ContactRequest -> MR.strings.error_deleting_contact_request ChatType.ContactConnection -> MR.strings.error_deleting_pending_contact_connection } apiErrorAlert("apiDeleteChat", generalGetString(titleId), r) + false + } + } + chatModel.deletedChats.value -= rh to type.type + id + return success + } + + suspend fun apiDeleteContact(rh: Long?, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Contact? { + val type = ChatType.Direct + chatModel.deletedChats.value += rh to type.type + id + val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode)) + val contact = if (r is API.Result && r.res is CR.ContactDeleted) { + r.res.contact + } else { + val titleId = MR.strings.error_deleting_contact + apiErrorAlert("apiDeleteChat", generalGetString(titleId), r) + null + } + chatModel.deletedChats.value -= rh to type.type + id + return contact + } + + fun clearChat(chat: Chat, close: (() -> Unit)? = null) { + withBGApi { + val updatedChatInfo = apiClearChat(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId) + if (updatedChatInfo != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.clearChat(chat.remoteHostId, updatedChatInfo) + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.clearChat(chat.remoteHostId, updatedChatInfo) + } + ntfManager.cancelNotificationsForChat(chat.chatInfo.id) + close?.invoke() } } - return false } suspend fun apiClearChat(rh: Long?, type: ChatType, id: Long): ChatInfo? { val r = sendCmd(rh, CC.ApiClearChat(type, id)) - if (r is CR.ChatCleared) return r.chatInfo + if (r is API.Result && r.res is CR.ChatCleared) return r.res.chatInfo Log.e(TAG, "apiClearChat bad response: ${r.responseType} ${r.details}") return null } @@ -1010,9 +1472,9 @@ object ChatController { suspend fun apiUpdateProfile(rh: Long?, profile: Profile): Pair<Profile, List<Contact>>? { val userId = kotlin.runCatching { currentUserId("apiUpdateProfile") }.getOrElse { return null } val r = sendCmd(rh, CC.ApiUpdateProfile(userId, profile)) - if (r is CR.UserProfileNoChange) return profile to emptyList() - if (r is CR.UserProfileUpdated) return r.toProfile to r.updateSummary.changedContacts - if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.DuplicateName) { + if (r is API.Result && r.res is CR.UserProfileNoChange) return profile to emptyList() + if (r is API.Result && r.res is CR.UserProfileUpdated) return r.res.toProfile to r.res.updateSummary.changedContacts + if (r is API.Error && r.err is ChatError.ChatErrorStore && r.err.storeError is StoreError.DuplicateName) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_duplicate_title), generalGetString(MR.strings.failed_to_create_user_duplicate_desc)) } Log.e(TAG, "apiUpdateProfile bad response: ${r.responseType} ${r.details}") @@ -1021,52 +1483,70 @@ object ChatController { suspend fun apiSetProfileAddress(rh: Long?, on: Boolean): User? { val userId = try { currentUserId("apiSetProfileAddress") } catch (e: Exception) { return null } - return when (val r = sendCmd(rh, CC.ApiSetProfileAddress(userId, on))) { - is CR.UserProfileNoChange -> null - is CR.UserProfileUpdated -> r.user.updateRemoteHostId(rh) + val r = sendCmd(rh, CC.ApiSetProfileAddress(userId, on)) + return when { + r is API.Result && r.res is CR.UserProfileNoChange -> null + r is API.Result && r.res is CR.UserProfileUpdated -> r.res.user.updateRemoteHostId(rh) else -> throw Exception("failed to set profile address: ${r.responseType} ${r.details}") } } suspend fun apiSetContactPrefs(rh: Long?, contactId: Long, prefs: ChatPreferences): Contact? { val r = sendCmd(rh, CC.ApiSetContactPrefs(contactId, prefs)) - if (r is CR.ContactPrefsUpdated) return r.toContact + if (r is API.Result && r.res is CR.ContactPrefsUpdated) return r.res.toContact Log.e(TAG, "apiSetContactPrefs bad response: ${r.responseType} ${r.details}") return null } suspend fun apiSetContactAlias(rh: Long?, contactId: Long, localAlias: String): Contact? { val r = sendCmd(rh, CC.ApiSetContactAlias(contactId, localAlias)) - if (r is CR.ContactAliasUpdated) return r.toContact + if (r is API.Result && r.res is CR.ContactAliasUpdated) return r.res.toContact Log.e(TAG, "apiSetContactAlias bad response: ${r.responseType} ${r.details}") return null } + suspend fun apiSetGroupAlias(rh: Long?, groupId: Long, localAlias: String): GroupInfo? { + val r = sendCmd(rh, CC.ApiSetGroupAlias(groupId, localAlias)) + if (r is API.Result && r.res is CR.GroupAliasUpdated) return r.res.toGroup + Log.e(TAG, "apiSetGroupAlias bad response: ${r.responseType} ${r.details}") + return null + } + suspend fun apiSetConnectionAlias(rh: Long?, connId: Long, localAlias: String): PendingContactConnection? { val r = sendCmd(rh, CC.ApiSetConnectionAlias(connId, localAlias)) - if (r is CR.ConnectionAliasUpdated) return r.toConnection + if (r is API.Result && r.res is CR.ConnectionAliasUpdated) return r.res.toConnection Log.e(TAG, "apiSetConnectionAlias bad response: ${r.responseType} ${r.details}") return null } - suspend fun apiCreateUserAddress(rh: Long?): String? { + suspend fun apiSetUserUIThemes(rh: Long?, userId: Long, themes: ThemeModeOverrides?): Boolean { + val r = sendCmd(rh, CC.ApiSetUserUIThemes(userId, themes)) + if (r.result is CR.CmdOk) return true + Log.e(TAG, "apiSetUserUIThemes bad response: ${r.responseType} ${r.details}") + return false + } + + suspend fun apiSetChatUIThemes(rh: Long?, chatId: ChatId, themes: ThemeModeOverrides?): Boolean { + val r = sendCmd(rh, CC.ApiSetChatUIThemes(chatId, themes)) + if (r.result is CR.CmdOk) return true + Log.e(TAG, "apiSetChatUIThemes bad response: ${r.responseType} ${r.details}") + return false + } + + suspend fun apiCreateUserAddress(rh: Long?, short: Boolean): CreatedConnLink? { val userId = kotlin.runCatching { currentUserId("apiCreateUserAddress") }.getOrElse { return null } - val r = sendCmd(rh, CC.ApiCreateMyAddress(userId)) - return when (r) { - is CR.UserContactLinkCreated -> r.connReqContact - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiCreateUserAddress", generalGetString(MR.strings.error_creating_address), r) - } - null - } + val r = sendCmd(rh, CC.ApiCreateMyAddress(userId, short)) + if (r is API.Result && r.res is CR.UserContactLinkCreated) return r.res.connLinkContact + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiCreateUserAddress", generalGetString(MR.strings.error_creating_address), r) } + return null } suspend fun apiDeleteUserAddress(rh: Long?): User? { val userId = try { currentUserId("apiDeleteUserAddress") } catch (e: Exception) { return null } val r = sendCmd(rh, CC.ApiDeleteMyAddress(userId)) - if (r is CR.UserContactLinkDeleted) return r.user.updateRemoteHostId(rh) + if (r is API.Result && r.res is CR.UserContactLinkDeleted) return r.res.user.updateRemoteHostId(rh) Log.e(TAG, "apiDeleteUserAddress bad response: ${r.responseType} ${r.details}") return null } @@ -1074,9 +1554,9 @@ object ChatController { private suspend fun apiGetUserAddress(rh: Long?): UserContactLinkRec? { val userId = kotlin.runCatching { currentUserId("apiGetUserAddress") }.getOrElse { return null } val r = sendCmd(rh, CC.ApiShowMyAddress(userId)) - if (r is CR.UserContactLink) return r.contactLink - if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore - && r.chatError.storeError is StoreError.UserContactLinkNotFound + if (r is API.Result && r.res is CR.UserContactLink) return r.res.contactLink + if (r is API.Error && r.err is ChatError.ChatErrorStore + && r.err.storeError is StoreError.UserContactLinkNotFound ) { return null } @@ -1087,9 +1567,9 @@ object ChatController { suspend fun userAddressAutoAccept(rh: Long?, autoAccept: AutoAccept?): UserContactLinkRec? { val userId = kotlin.runCatching { currentUserId("userAddressAutoAccept") }.getOrElse { return null } val r = sendCmd(rh, CC.ApiAddressAutoAccept(userId, autoAccept)) - if (r is CR.UserContactLinkUpdated) return r.contactLink - if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore - && r.chatError.storeError is StoreError.UserContactLinkNotFound + if (r is API.Result && r.res is CR.UserContactLinkUpdated) return r.res.contactLink + if (r is API.Error && r.err is ChatError.ChatErrorStore + && r.err.storeError is StoreError.UserContactLinkNotFound ) { return null } @@ -1100,10 +1580,10 @@ object ChatController { suspend fun apiAcceptContactRequest(rh: Long?, incognito: Boolean, contactReqId: Long): Contact? { val r = sendCmd(rh, CC.ApiAcceptContact(incognito, contactReqId)) return when { - r is CR.AcceptingContactRequest -> r.contact - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent - && r.chatError.agentError is AgentErrorType.SMP - && r.chatError.agentError.smpErr is SMPErrorType.AUTH -> { + r is API.Result && r.res is CR.AcceptingContactRequest -> r.res.contact + r is API.Error && r.err is ChatError.ChatErrorAgent + && r.err.agentError is AgentErrorType.SMP + && r.err.agentError.smpErr is SMPErrorType.AUTH -> { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.connection_error_auth), generalGetString(MR.strings.sender_may_have_deleted_the_connection_request) @@ -1121,101 +1601,241 @@ object ChatController { suspend fun apiRejectContactRequest(rh: Long?, contactReqId: Long): Boolean { val r = sendCmd(rh, CC.ApiRejectContact(contactReqId)) - if (r is CR.ContactRequestRejected) return true + if (r is API.Result && r.res is CR.ContactRequestRejected) return true Log.e(TAG, "apiRejectContactRequest bad response: ${r.responseType} ${r.details}") return false } + suspend fun apiGetCallInvitations(rh: Long?): List<RcvCallInvitation> { + val r = sendCmd(rh, CC.ApiGetCallInvitations()) + if (r is API.Result && r.res is CR.CallInvitations) return r.res.callInvitations + Log.e(TAG, "apiGetCallInvitations bad response: ${r.responseType} ${r.details}") + return emptyList() + } + suspend fun apiSendCallInvitation(rh: Long?, contact: Contact, callType: CallType): Boolean { val r = sendCmd(rh, CC.ApiSendCallInvitation(contact, callType)) - return r is CR.CmdOk + return r.result is CR.CmdOk } suspend fun apiRejectCall(rh: Long?, contact: Contact): Boolean { val r = sendCmd(rh, CC.ApiRejectCall(contact)) - return r is CR.CmdOk + return r.result is CR.CmdOk } suspend fun apiSendCallOffer(rh: Long?, contact: Contact, rtcSession: String, rtcIceCandidates: String, media: CallMediaType, capabilities: CallCapabilities): Boolean { val webRtcSession = WebRTCSession(rtcSession, rtcIceCandidates) val callOffer = WebRTCCallOffer(CallType(media, capabilities), webRtcSession) val r = sendCmd(rh, CC.ApiSendCallOffer(contact, callOffer)) - return r is CR.CmdOk + return r.result is CR.CmdOk } suspend fun apiSendCallAnswer(rh: Long?, contact: Contact, rtcSession: String, rtcIceCandidates: String): Boolean { val answer = WebRTCSession(rtcSession, rtcIceCandidates) val r = sendCmd(rh, CC.ApiSendCallAnswer(contact, answer)) - return r is CR.CmdOk + return r.result is CR.CmdOk } suspend fun apiSendCallExtraInfo(rh: Long?, contact: Contact, rtcIceCandidates: String): Boolean { val extraInfo = WebRTCExtraInfo(rtcIceCandidates) val r = sendCmd(rh, CC.ApiSendCallExtraInfo(contact, extraInfo)) - return r is CR.CmdOk + return r.result is CR.CmdOk } suspend fun apiEndCall(rh: Long?, contact: Contact): Boolean { val r = sendCmd(rh, CC.ApiEndCall(contact)) - return r is CR.CmdOk + return r.result is CR.CmdOk } suspend fun apiCallStatus(rh: Long?, contact: Contact, status: WebRTCCallStatus): Boolean { val r = sendCmd(rh, CC.ApiCallStatus(contact, status)) - return r is CR.CmdOk + return r.result is CR.CmdOk } suspend fun apiGetNetworkStatuses(rh: Long?): List<ConnNetworkStatus>? { val r = sendCmd(rh, CC.ApiGetNetworkStatuses()) - if (r is CR.NetworkStatuses) return r.networkStatuses + if (r is API.Result && r.res is CR.NetworkStatuses) return r.res.networkStatuses Log.e(TAG, "apiGetNetworkStatuses bad response: ${r.responseType} ${r.details}") return null } - suspend fun apiChatRead(rh: Long?, type: ChatType, id: Long, range: CC.ItemRange): Boolean { - val r = sendCmd(rh, CC.ApiChatRead(type, id, range)) - if (r is CR.CmdOk) return true + suspend fun apiChatRead(rh: Long?, type: ChatType, id: Long): Boolean { + val r = sendCmd(rh, CC.ApiChatRead(type, id)) + if (r.result is CR.CmdOk) return true Log.e(TAG, "apiChatRead bad response: ${r.responseType} ${r.details}") return false } + suspend fun apiChatItemsRead(rh: Long?, type: ChatType, id: Long, itemIds: List<Long>): Boolean { + val r = sendCmd(rh, CC.ApiChatItemsRead(type, id, itemIds)) + if (r.result is CR.CmdOk) return true + Log.e(TAG, "apiChatItemsRead bad response: ${r.responseType} ${r.details}") + return false + } + suspend fun apiChatUnread(rh: Long?, type: ChatType, id: Long, unreadChat: Boolean): Boolean { val r = sendCmd(rh, CC.ApiChatUnread(type, id, unreadChat)) - if (r is CR.CmdOk) return true + if (r.result is CR.CmdOk) return true Log.e(TAG, "apiChatUnread bad response: ${r.responseType} ${r.details}") return false } - suspend fun apiReceiveFile(rh: Long?, fileId: Long, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? { - // -1 here is to override default behavior of providing current remote host id because file can be asked by local device while remote is connected - val r = sendCmd(rh, CC.ReceiveFile(fileId, encrypted, inline)) - return when (r) { - is CR.RcvFileAccepted -> r.chatItem - is CR.RcvFileAcceptedSndCancelled -> { - Log.d(TAG, "apiReceiveFile error: sender cancelled file transfer") - if (!auto) { + suspend fun uploadStandaloneFile(user: UserLike, file: CryptoFile, ctrl: ChatCtrl? = null): Pair<FileTransferMeta?, String?> { + val r = sendCmd(null, CC.ApiUploadStandaloneFile(user.userId, file), ctrl) + return if (r is API.Result && r.res is CR.SndStandaloneFileCreated) { + r.res.fileTransferMeta to null + } else { + Log.e(TAG, "uploadStandaloneFile error: $r") + null to r.toString() + } + } + + suspend fun downloadStandaloneFile(user: UserLike, url: String, file: CryptoFile, ctrl: ChatCtrl? = null): Pair<RcvFileTransfer?, String?> { + val r = sendCmd(null, CC.ApiDownloadStandaloneFile(user.userId, url, file), ctrl) + return if (r is API.Result && r.res is CR.RcvStandaloneFileCreated) { + r.res.rcvFileTransfer to null + } else { + Log.e(TAG, "downloadStandaloneFile error: $r") + null to r.toString() + } + } + + suspend fun standaloneFileInfo(url: String, ctrl: ChatCtrl? = null): MigrationFileLinkData? { + val r = sendCmd(null, CC.ApiStandaloneFileInfo(url), ctrl) + return if (r is API.Result && r.res is CR.StandaloneFileInfo) { + r.res.fileMeta + } else { + Log.e(TAG, "standaloneFileInfo error: $r") + null + } + } + + suspend fun receiveFiles(rhId: Long?, user: UserLike, fileIds: List<Long>, userApprovedRelays: Boolean = false, auto: Boolean = false) { + val fileIdsToApprove = mutableListOf<Long>() + val srvsToApprove = mutableSetOf<String>() + val otherFileErrs = mutableListOf<API>() + + for (fileId in fileIds) { + val r = sendCmd( + rhId, CC.ReceiveFile( + fileId, + userApprovedRelays = userApprovedRelays || !appPrefs.privacyAskToApproveRelays.get(), + encrypt = appPrefs.privacyEncryptLocalFiles.get(), + inline = null + ) + ) + if (r is API.Result && r.res is CR.RcvFileAccepted) { + chatItemSimpleUpdate(rhId, user, r.res.chatItem) + } else { + val maybeChatError = apiChatErrorType(r) + if (maybeChatError is ChatErrorType.FileNotApproved) { + fileIdsToApprove.add(maybeChatError.fileId) + srvsToApprove.addAll(maybeChatError.unknownServers.map { serverHostname(it) }) + } else { + otherFileErrs.add(r) + } + } + } + + if (!auto) { + // If there are not approved files, alert is shown the same way both in case of singular and plural files reception + if (fileIdsToApprove.isNotEmpty()) { + showFilesToApproveAlert( + srvsToApprove = srvsToApprove, + otherFileErrs = otherFileErrs, + approveFiles = { + withBGApi { + receiveFiles( + rhId = rhId, + user = user, + fileIds = fileIdsToApprove, + userApprovedRelays = true + ) + } + } + ) + } else if (otherFileErrs.size == 1) { // If there is a single other error, we differentiate on it + val errCR = otherFileErrs.first() + if (errCR is API.Result && errCR.res is CR.RcvFileAcceptedSndCancelled) { + Log.d(TAG, "receiveFiles error: sender cancelled file transfer") AlertManager.shared.showAlertMsg( generalGetString(MR.strings.cannot_receive_file), generalGetString(MR.strings.sender_cancelled_file_transfer) ) - } - null - } - - else -> { - if (!(networkErrorAlert(r))) { - val maybeChatError = chatError(r) + } else { + val maybeChatError = apiChatErrorType(errCR) if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) { - Log.d(TAG, "apiReceiveFile ignoring FileCancelled or FileAlreadyReceiving error") + Log.d(TAG, "receiveFiles ignoring FileCancelled or FileAlreadyReceiving error") } else { - apiErrorAlert("apiReceiveFile", generalGetString(MR.strings.error_receiving_file), r) + apiErrorAlert("receiveFiles", generalGetString(MR.strings.error_receiving_file), errCR) } } - null + } else if (otherFileErrs.size > 1) { // If there are multiple other errors, we show general alert + val errsStr = otherFileErrs.map { json.encodeToString(it) }.joinToString(separator = "\n") + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.error_receiving_file), + text = String.format(generalGetString(MR.strings.n_file_errors), otherFileErrs.size, errsStr), + shareText = true + ) } } } + private fun showFilesToApproveAlert( + srvsToApprove: Set<String>, + otherFileErrs: List<API>, + approveFiles: (() -> Unit) + ) { + val srvsToApproveStr = srvsToApprove.sorted().joinToString(separator = ", ") + val alertText = + generalGetString(MR.strings.file_not_approved_descr).format(srvsToApproveStr) + + (if (otherFileErrs.isNotEmpty()) "\n" + generalGetString(MR.strings.n_other_file_errors).format(otherFileErrs.size) else "") + + AlertManager.shared.showAlertDialogButtonsColumn(generalGetString(MR.strings.file_not_approved_title), alertText, belowTextContent = { + if (otherFileErrs.isNotEmpty()) { + val clipboard = LocalClipboardManager.current + SimpleButtonFrame(click = { + clipboard.setText(AnnotatedString(otherFileErrs.map { json.encodeToString(it) }.joinToString(separator = "\n"))) + }) { + Icon( + painterResource(MR.images.ic_content_copy), + contentDescription = null, + tint = MaterialTheme.colors.primary, + modifier = Modifier.padding(end = 8.dp) + ) + Text(generalGetString(MR.strings.copy_error), color = MaterialTheme.colors.primary) + } + } + }) { + Row( + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), + horizontalArrangement = Arrangement.SpaceBetween + ) { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + // Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard + delay(200) + focusRequester.requestFocus() + } + TextButton(onClick = AlertManager.shared::hideAlert) { Text(generalGetString(MR.strings.cancel_verb)) } + TextButton(onClick = { + approveFiles.invoke() + AlertManager.shared.hideAlert() + }, Modifier.focusRequester(focusRequester)) { Text(generalGetString(MR.strings.download_file)) } + } + } + } + + suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, userApprovedRelays: Boolean = false, auto: Boolean = false) { + receiveFiles( + rhId = rhId, + user = user, + fileIds = listOf(fileId), + userApprovedRelays = userApprovedRelays, + auto = auto + ) + } + suspend fun cancelFile(rh: Long?, user: User, fileId: Long) { val chatItem = apiCancelFile(rh, fileId) if (chatItem != null) { @@ -1224,11 +1844,11 @@ object ChatController { } } - suspend fun apiCancelFile(rh: Long?, fileId: Long): AChatItem? { - val r = sendCmd(rh, CC.CancelFile(fileId)) - return when (r) { - is CR.SndFileCancelled -> r.chatItem - is CR.RcvFileCancelled -> r.chatItem + suspend fun apiCancelFile(rh: Long?, fileId: Long, ctrl: ChatCtrl? = null): AChatItem? { + val r = sendCmd(rh, CC.CancelFile(fileId), ctrl) + return when { + r is API.Result && r.res is CR.SndFileCancelled -> r.res.chatItem_ + r is API.Result && r.res is CR.RcvFileCancelled -> r.res.chatItem_ else -> { Log.d(TAG, "apiCancelFile bad response: ${r.responseType} ${r.details}") null @@ -1239,32 +1859,32 @@ object ChatController { suspend fun apiNewGroup(rh: Long?, incognito: Boolean, groupProfile: GroupProfile): GroupInfo? { val userId = kotlin.runCatching { currentUserId("apiNewGroup") }.getOrElse { return null } val r = sendCmd(rh, CC.ApiNewGroup(userId, incognito, groupProfile)) - if (r is CR.GroupCreated) return r.groupInfo + if (r is API.Result && r.res is CR.GroupCreated) return r.res.groupInfo Log.e(TAG, "apiNewGroup bad response: ${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)) - return when (r) { - is CR.SentGroupInvitation -> r.member - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiAddMember", generalGetString(MR.strings.error_adding_members), r) - } - null - } + if (r is API.Result && r.res is CR.SentGroupInvitation) return r.res.member + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiAddMember", generalGetString(MR.strings.error_adding_members), r) } + return null } suspend fun apiJoinGroup(rh: Long?, groupId: Long) { val r = sendCmd(rh, CC.ApiJoinGroup(groupId)) - when (r) { - is CR.UserAcceptedGroupSent -> - chatModel.updateGroup(rh, r.groupInfo) - is CR.ChatCmdError -> { - val e = r.chatError - suspend fun deleteGroup() { if (apiDeleteChat(rh, ChatType.Group, groupId)) { chatModel.removeChat(rh, "#$groupId") } } + when { + r is API.Result && r.res is CR.UserAcceptedGroupSent -> + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rh, r.res.groupInfo) + } + r is API.Error -> { + val e = r.err + suspend fun deleteGroup() { if (apiDeleteChat(rh, ChatType.Group, groupId)) { + withContext(Dispatchers.Main) { chatModel.chatsContext.removeChat(rh, "#$groupId") } } + } if (e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.AUTH) { deleteGroup() AlertManager.shared.showAlertMsg(generalGetString(MR.strings.alert_title_group_invitation_expired), generalGetString(MR.strings.alert_message_group_invitation_expired)) @@ -1279,47 +1899,53 @@ object ChatController { } } - suspend fun apiRemoveMember(rh: Long?, groupId: Long, memberId: Long): GroupMember? = - when (val r = sendCmd(rh, CC.ApiRemoveMember(groupId, memberId))) { - is CR.UserDeletedMember -> r.member - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiRemoveMember", generalGetString(MR.strings.error_removing_member), r) - } - null - } + suspend fun apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List<Long>, withMessages: Boolean = false): List<GroupMember>? { + val r = sendCmd(rh, CC.ApiRemoveMembers(groupId, memberIds, withMessages)) + if (r is API.Result && r.res is CR.UserDeletedMembers) return r.res.members + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiRemoveMembers", generalGetString(MR.strings.error_removing_member), r) } + return null + } - suspend fun apiMemberRole(rh: Long?, groupId: Long, memberId: Long, memberRole: GroupMemberRole): GroupMember = - when (val r = sendCmd(rh, CC.ApiMemberRole(groupId, memberId, memberRole))) { - is CR.MemberRoleUser -> r.member - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiMemberRole", generalGetString(MR.strings.error_changing_role), r) - } - throw Exception("failed to change member role: ${r.responseType} ${r.details}") - } + suspend fun apiMembersRole(rh: Long?, groupId: Long, memberIds: List<Long>, memberRole: GroupMemberRole): List<GroupMember> { + val r = sendCmd(rh, CC.ApiMembersRole(groupId, memberIds, memberRole)) + if (r is API.Result && r.res is CR.MembersRoleUser) return r.res.members + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiMembersRole", generalGetString(MR.strings.error_changing_role), r) } + throw Exception("failed to change member role: ${r.responseType} ${r.details}") + } + + suspend fun apiBlockMembersForAll(rh: Long?, groupId: Long, memberIds: List<Long>, blocked: Boolean): List<GroupMember> { + val r = sendCmd(rh, CC.ApiBlockMembersForAll(groupId, memberIds, blocked)) + if (r is API.Result && r.res is CR.MembersBlockedForAllUser) return r.res.members + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiBlockMembersForAll", generalGetString(MR.strings.error_blocking_member_for_all), r) + } + throw Exception("failed to block member for all: ${r.responseType} ${r.details}") + } suspend fun apiLeaveGroup(rh: Long?, groupId: Long): GroupInfo? { val r = sendCmd(rh, CC.ApiLeaveGroup(groupId)) - if (r is CR.LeftMemberUser) return r.groupInfo + if (r is API.Result && r.res is CR.LeftMemberUser) return r.res.groupInfo Log.e(TAG, "apiLeaveGroup bad response: ${r.responseType} ${r.details}") return null } suspend fun apiListMembers(rh: Long?, groupId: Long): List<GroupMember> { val r = sendCmd(rh, CC.ApiListMembers(groupId)) - if (r is CR.GroupMembers) return r.group.members + if (r is API.Result && r.res is CR.GroupMembers) return r.res.group.members Log.e(TAG, "apiListMembers bad response: ${r.responseType} ${r.details}") return emptyList() } suspend fun apiUpdateGroup(rh: Long?, groupId: Long, groupProfile: GroupProfile): GroupInfo? { - return when (val r = sendCmd(rh, CC.ApiUpdateGroupProfile(groupId, groupProfile))) { - is CR.GroupUpdated -> r.toGroup - is CR.ChatCmdError -> { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_group_profile), "$r.chatError") + val r = sendCmd(rh, CC.ApiUpdateGroupProfile(groupId, groupProfile)) + 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") null } else -> { @@ -1333,81 +1959,66 @@ object ChatController { } } - suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair<String, GroupMemberRole>? { - return when (val r = sendCmd(rh, CC.APICreateGroupLink(groupId, memberRole))) { - is CR.GroupLinkCreated -> r.connReqContact to r.memberRole - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiCreateGroupLink", generalGetString(MR.strings.error_creating_link_for_group), r) - } - null - } + suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair<CreatedConnLink, GroupMemberRole>? { + val short = appPrefs.privacyShortLinks.get() + val r = sendCmd(rh, CC.APICreateGroupLink(groupId, memberRole, short)) + if (r is API.Result && r.res is CR.GroupLinkCreated) return r.res.connLinkContact to r.res.memberRole + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiCreateGroupLink", generalGetString(MR.strings.error_creating_link_for_group), r) } + return null } - suspend fun apiGroupLinkMemberRole(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair<String, GroupMemberRole>? { - return when (val r = sendCmd(rh, CC.APIGroupLinkMemberRole(groupId, memberRole))) { - is CR.GroupLink -> r.connReqContact to r.memberRole - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiGroupLinkMemberRole", generalGetString(MR.strings.error_updating_link_for_group), r) - } - null - } + suspend fun apiGroupLinkMemberRole(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair<CreatedConnLink, GroupMemberRole>? { + val r = sendCmd(rh, CC.APIGroupLinkMemberRole(groupId, memberRole)) + if (r is API.Result && r.res is CR.GroupLink) return r.res.connLinkContact to r.res.memberRole + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiGroupLinkMemberRole", generalGetString(MR.strings.error_updating_link_for_group), r) } + return null } suspend fun apiDeleteGroupLink(rh: Long?, groupId: Long): Boolean { - return when (val r = sendCmd(rh, CC.APIDeleteGroupLink(groupId))) { - is CR.GroupLinkDeleted -> true - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiDeleteGroupLink", generalGetString(MR.strings.error_deleting_link_for_group), r) - } - false - } + val r = sendCmd(rh, CC.APIDeleteGroupLink(groupId)) + if (r is API.Result && r.res is CR.GroupLinkDeleted) return true + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiDeleteGroupLink", generalGetString(MR.strings.error_deleting_link_for_group), r) } + return false } - suspend fun apiGetGroupLink(rh: Long?, groupId: Long): Pair<String, GroupMemberRole>? { - return when (val r = sendCmd(rh, CC.APIGetGroupLink(groupId))) { - is CR.GroupLink -> r.connReqContact to r.memberRole - else -> { - Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}") - null - } - } + suspend fun apiGetGroupLink(rh: Long?, groupId: Long): Pair<CreatedConnLink, GroupMemberRole>? { + val r = sendCmd(rh, CC.APIGetGroupLink(groupId)) + if (r is API.Result && r.res is CR.GroupLink) return r.res.connLinkContact to r.res.memberRole + Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}") + return null } suspend fun apiCreateMemberContact(rh: Long?, groupId: Long, groupMemberId: Long): Contact? { - return when (val r = sendCmd(rh, CC.APICreateMemberContact(groupId, groupMemberId))) { - is CR.NewMemberContact -> r.contact - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiCreateMemberContact", generalGetString(MR.strings.error_creating_member_contact), r) - } - null - } + val r = sendCmd(rh, CC.APICreateMemberContact(groupId, groupMemberId)) + if (r is API.Result && r.res is CR.NewMemberContact) return r.res.contact + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiCreateMemberContact", generalGetString(MR.strings.error_creating_member_contact), r) } + return null } suspend fun apiSendMemberContactInvitation(rh: Long?, contactId: Long, mc: MsgContent): Contact? { - return when (val r = sendCmd(rh, CC.APISendMemberContactInvitation(contactId, mc))) { - is CR.NewMemberContactSentInv -> r.contact - else -> { - if (!(networkErrorAlert(r))) { - apiErrorAlert("apiSendMemberContactInvitation", generalGetString(MR.strings.error_sending_message_contact_invitation), r) - } - null - } + val r = sendCmd(rh, CC.APISendMemberContactInvitation(contactId, mc)) + if (r is API.Result && r.res is CR.NewMemberContactSentInv) return r.res.contact + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiSendMemberContactInvitation", generalGetString(MR.strings.error_sending_message_contact_invitation), r) } + return null } suspend fun allowFeatureToContact(rh: Long?, contact: Contact, feature: ChatFeature, param: Int? = null) { val prefs = contact.mergedPreferences.toPreferences().setAllowed(feature, param = param) val toContact = apiSetContactPrefs(rh, contact.contactId, prefs) if (toContact != null) { - chatModel.updateContact(rh, toContact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rh, toContact) + } } } @@ -1415,7 +2026,7 @@ object ChatController { suspend fun listRemoteHosts(): List<RemoteHostInfo>? { val r = sendCmd(null, CC.ListRemoteHosts()) - if (r is CR.RemoteHostList) return r.remoteHosts + if (r is API.Result && r.res is CR.RemoteHostList) return r.res.remoteHosts apiErrorAlert("listRemoteHosts", generalGetString(MR.strings.error_alert_title), r) return null } @@ -1428,14 +2039,14 @@ object ChatController { suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = true, address: RemoteCtrlAddress?, port: Int?): CR.RemoteHostStarted? { val r = sendCmd(null, CC.StartRemoteHost(rhId, multicast, address, port)) - if (r is CR.RemoteHostStarted) return r + if (r is API.Result && r.res is CR.RemoteHostStarted) return r.res apiErrorAlert("startRemoteHost", generalGetString(MR.strings.error_alert_title), r) return null } suspend fun switchRemoteHost (rhId: Long?): RemoteHostInfo? { val r = sendCmd(null, CC.SwitchRemoteHost(rhId)) - if (r is CR.CurrentRemoteHost) return r.remoteHost_ + if (r is API.Result && r.res is CR.CurrentRemoteHost) return r.res.remoteHost_ apiErrorAlert("switchRemoteHost", generalGetString(MR.strings.error_alert_title), r) return null } @@ -1457,45 +2068,49 @@ object ChatController { suspend fun storeRemoteFile(rhId: Long, storeEncrypted: Boolean?, localPath: String): CryptoFile? { val r = sendCmd(null, CC.StoreRemoteFile(rhId, storeEncrypted, localPath)) - if (r is CR.RemoteFileStored) return r.remoteFileSource + if (r is API.Result && r.res is CR.RemoteFileStored) return r.res.remoteFileSource apiErrorAlert("storeRemoteFile", generalGetString(MR.strings.error_alert_title), r) return null } - suspend fun getRemoteFile(rhId: Long, file: RemoteFile): Boolean = sendCmd(null, CC.GetRemoteFile(rhId, file)) is CR.CmdOk + suspend fun getRemoteFile(rhId: Long, file: RemoteFile): Boolean = sendCmd(null, CC.GetRemoteFile(rhId, file)).result is CR.CmdOk - suspend fun connectRemoteCtrl(desktopAddress: String): Pair<SomeRemoteCtrl?, CR.ChatCmdError?> { + suspend fun connectRemoteCtrl(desktopAddress: String): Pair<SomeRemoteCtrl?, ChatError?> { val r = sendCmd(null, CC.ConnectRemoteCtrl(desktopAddress)) - return if (r is CR.RemoteCtrlConnecting) SomeRemoteCtrl(r.remoteCtrl_, r.ctrlAppInfo, r.appVersion) to null - else if (r is CR.ChatCmdError) null to r - else { - apiErrorAlert("connectRemoteCtrl", generalGetString(MR.strings.error_alert_title), r) - null to null + return when { + r is API.Result && r.res is CR.RemoteCtrlConnecting -> SomeRemoteCtrl(r.res.remoteCtrl_, r.res.ctrlAppInfo, r.res.appVersion) to null + r is API.Error -> null to r.err + else -> { + apiErrorAlert("connectRemoteCtrl", generalGetString(MR.strings.error_alert_title), r) + null to null + } } } suspend fun findKnownRemoteCtrl(): Boolean = sendCommandOkResp(null, CC.FindKnownRemoteCtrl()) - suspend fun confirmRemoteCtrl(rcId: Long): Pair<SomeRemoteCtrl?, CR.ChatCmdError?> { + suspend fun confirmRemoteCtrl(rcId: Long): Pair<SomeRemoteCtrl?, ChatError?> { val r = sendCmd(null, CC.ConfirmRemoteCtrl(remoteCtrlId = rcId)) - return if (r is CR.RemoteCtrlConnecting) SomeRemoteCtrl(r.remoteCtrl_, r.ctrlAppInfo, r.appVersion) to null - else if (r is CR.ChatCmdError) null to r - else { - apiErrorAlert("confirmRemoteCtrl", generalGetString(MR.strings.error_alert_title), r) - null to null + return when { + r is API.Result && r.res is CR.RemoteCtrlConnecting -> SomeRemoteCtrl(r.res.remoteCtrl_, r.res.ctrlAppInfo, r.res.appVersion) to null + r is API.Error -> null to r.err + else -> { + apiErrorAlert("confirmRemoteCtrl", generalGetString(MR.strings.error_alert_title), r) + null to null + } } } suspend fun verifyRemoteCtrlSession(sessionCode: String): RemoteCtrlInfo? { val r = sendCmd(null, CC.VerifyRemoteCtrlSession(sessionCode)) - if (r is CR.RemoteCtrlConnected) return r.remoteCtrl + if (r is API.Result && r.res is CR.RemoteCtrlConnected) return r.res.remoteCtrl apiErrorAlert("verifyRemoteCtrlSession", generalGetString(MR.strings.error_alert_title), r) return null } suspend fun listRemoteCtrls(): List<RemoteCtrlInfo>? { val r = sendCmd(null, CC.ListRemoteCtrls()) - if (r is CR.RemoteCtrlList) return r.remoteCtrls + if (r is API.Result && r.res is CR.RemoteCtrlList) return r.res.remoteCtrls apiErrorAlert("listRemoteCtrls", generalGetString(MR.strings.error_alert_title), r) return null } @@ -1504,40 +2119,110 @@ object ChatController { suspend fun deleteRemoteCtrl(rcId: Long): Boolean = sendCommandOkResp(null, CC.DeleteRemoteCtrl(rcId)) - private suspend fun sendCommandOkResp(rh: Long?, cmd: CC): Boolean { - val r = sendCmd(rh, cmd) - val ok = r is CR.CmdOk + private suspend fun sendCommandOkResp(rh: Long?, cmd: CC, ctrl: ChatCtrl? = null): Boolean { + val r = sendCmd(rh, cmd, ctrl) + val ok = r is API.Result && r.res is CR.CmdOk if (!ok) apiErrorAlert(cmd.cmdType, generalGetString(MR.strings.error_alert_title), r) return ok } suspend fun apiGetVersion(): CoreVersionInfo? { val r = sendCmd(null, CC.ShowVersion()) - return if (r is CR.VersionInfo) { - r.versionInfo - } else { - Log.e(TAG, "apiGetVersion bad response: ${r.responseType} ${r.details}") - null - } + if (r is API.Result && r.res is CR.VersionInfo) return r.res.versionInfo + Log.e(TAG, "apiGetVersion bad response: ${r.responseType} ${r.details}") + return null } - private fun networkErrorAlert(r: CR): Boolean { + private fun networkErrorAlert(r: API): Boolean { + if (r !is API.Error) return false + val e = r.err return when { - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent - && r.chatError.agentError is AgentErrorType.BROKER - && r.chatError.agentError.brokerErr is BrokerErrorType.TIMEOUT -> { + e is ChatError.ChatErrorAgent + && e.agentError is AgentErrorType.BROKER + && e.agentError.brokerErr is BrokerErrorType.TIMEOUT -> { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.connection_timeout), - String.format(generalGetString(MR.strings.network_error_desc), serverHostname(r.chatError.agentError.brokerAddress)) + String.format(generalGetString(MR.strings.network_error_desc), serverHostname(e.agentError.brokerAddress)) ) true } - r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent - && r.chatError.agentError is AgentErrorType.BROKER - && r.chatError.agentError.brokerErr is BrokerErrorType.NETWORK -> { + e is ChatError.ChatErrorAgent + && e.agentError is AgentErrorType.BROKER + && e.agentError.brokerErr is BrokerErrorType.NETWORK -> { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.connection_error), - String.format(generalGetString(MR.strings.network_error_desc), serverHostname(r.chatError.agentError.brokerAddress)) + String.format(generalGetString(MR.strings.network_error_desc), serverHostname(e.agentError.brokerAddress)) + ) + true + } + e is ChatError.ChatErrorAgent + && e.agentError is AgentErrorType.BROKER + && e.agentError.brokerErr is BrokerErrorType.HOST -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.connection_error), + String.format(generalGetString(MR.strings.network_error_broker_host_desc), serverHostname(e.agentError.brokerAddress)) + ) + true + } + e is ChatError.ChatErrorAgent + && e.agentError is AgentErrorType.BROKER + && e.agentError.brokerErr is BrokerErrorType.TRANSPORT + && e.agentError.brokerErr.transportErr is SMPTransportError.Version -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.connection_error), + String.format(generalGetString(MR.strings.network_error_broker_version_desc), serverHostname(e.agentError.brokerAddress)) + ) + true + } + e is ChatError.ChatErrorAgent + && e.agentError is AgentErrorType.SMP + && e.agentError.smpErr is SMPErrorType.PROXY -> + smpProxyErrorAlert(e.agentError.smpErr.proxyErr, e.agentError.serverAddress) + e is ChatError.ChatErrorAgent + && e.agentError is AgentErrorType.PROXY + && e.agentError.proxyErr is ProxyClientError.ProxyProtocolError + && e.agentError.proxyErr.protocolErr is SMPErrorType.PROXY -> + proxyDestinationErrorAlert( + e.agentError.proxyErr.protocolErr.proxyErr, + e.agentError.proxyServer, + e.agentError.relayServer + ) + else -> false + } + } + + private fun smpProxyErrorAlert(pe: ProxyError, srvAddr: String): Boolean { + return when { + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.TIMEOUT -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.smp_proxy_error_connecting), serverHostname(srvAddr)) + ) + true + } + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.NETWORK -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.smp_proxy_error_connecting), serverHostname(srvAddr)) + ) + true + } + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.HOST -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.smp_proxy_error_broker_host), serverHostname(srvAddr)) + ) + true + } + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.TRANSPORT + && pe.brokerErr.transportErr is SMPTransportError.Version -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.smp_proxy_error_broker_version), serverHostname(srvAddr)) ) true } @@ -1545,31 +2230,81 @@ object ChatController { } } - private fun apiErrorAlert(method: String, title: String, r: CR) { + private fun proxyDestinationErrorAlert(pe: ProxyError, proxyServer: String, relayServer: String): Boolean { + return when { + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.TIMEOUT -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.proxy_destination_error_failed_to_connect), serverHostname(proxyServer), serverHostname(relayServer)) + ) + true + } + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.NETWORK -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.proxy_destination_error_failed_to_connect), serverHostname(proxyServer), serverHostname(relayServer)) + ) + true + } + pe is ProxyError.NO_SESSION -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.proxy_destination_error_failed_to_connect), serverHostname(proxyServer), serverHostname(relayServer)) + ) + true + } + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.HOST -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.proxy_destination_error_broker_host), serverHostname(relayServer), serverHostname(proxyServer)) + ) + true + } + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.TRANSPORT + && pe.brokerErr.transportErr is SMPTransportError.Version -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.proxy_destination_error_broker_version), serverHostname(relayServer), serverHostname(proxyServer)) + ) + true + } + else -> false + } + } + + private fun apiErrorAlert(method: String, title: String, r: API) { val errMsg = "${r.responseType}: ${r.details}" Log.e(TAG, "$method bad response: $errMsg") AlertManager.shared.showAlertMsg(title, errMsg) } - private suspend fun processReceivedMsg(apiResp: APIResponse) { + private suspend fun processReceivedMsg(msg: API) { lastMsgReceivedTimestamp = System.currentTimeMillis() - val r = apiResp.resp - val rhId = apiResp.remoteHostId + val rhId = msg.rhId fun active(user: UserLike): Boolean = activeUser(rhId, user) - chatModel.addTerminalItem(TerminalItem.resp(rhId, r)) + chatModel.addTerminalItem(TerminalItem.resp(rhId, msg)) + val r = msg.result when (r) { is CR.ContactDeletedByContact -> { if (active(r.user) && r.contact.directOrUsed) { - chatModel.updateContact(rhId, r.contact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, r.contact) + } } } is CR.ContactConnected -> { if (active(r.user) && r.contact.directOrUsed) { - chatModel.updateContact(rhId, r.contact) - val conn = r.contact.activeConn - if (conn != null) { - chatModel.dismissConnReqView(conn.id) - chatModel.removeChat(rhId, conn.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, r.contact) + val conn = r.contact.activeConn + if (conn != null) { + chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") + chatModel.chatsContext.removeChat(rhId, conn.id) + } } } if (r.contact.directOrUsed) { @@ -1579,51 +2314,81 @@ object ChatController { } is CR.ContactConnecting -> { if (active(r.user) && r.contact.directOrUsed) { - chatModel.updateContact(rhId, r.contact) - val conn = r.contact.activeConn - if (conn != null) { - chatModel.dismissConnReqView(conn.id) - chatModel.removeChat(rhId, conn.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, r.contact) + val conn = r.contact.activeConn + if (conn != null) { + chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") + chatModel.chatsContext.removeChat(rhId, conn.id) + } } } } + is CR.ContactSndReady -> { + if (active(r.user) && r.contact.directOrUsed) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, r.contact) + val conn = r.contact.activeConn + if (conn != null) { + chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") + chatModel.chatsContext.removeChat(rhId, conn.id) + } + } + } + chatModel.setContactNetworkStatus(r.contact, NetworkStatus.Connected()) + } is CR.ReceivedContactRequest -> { val contactRequest = r.contactRequest val cInfo = ChatInfo.ContactRequest(contactRequest) if (active(r.user)) { - if (chatModel.hasChat(rhId, contactRequest.id)) { - chatModel.updateChatInfo(rhId, cInfo) - } else { - chatModel.addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf())) + withContext(Dispatchers.Main) { + if (chatModel.chatsContext.hasChat(rhId, contactRequest.id)) { + chatModel.chatsContext.updateChatInfo(rhId, cInfo) + } else { + chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf())) + } } } ntfManager.notifyContactRequestReceived(r.user, cInfo) } is CR.ContactUpdated -> { - if (active(r.user) && chatModel.hasChat(rhId, r.toContact.id)) { + if (active(r.user) && chatModel.chatsContext.hasChat(rhId, r.toContact.id)) { val cInfo = ChatInfo.Direct(r.toContact) - chatModel.updateChatInfo(rhId, cInfo) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatInfo(rhId, cInfo) + } } } is CR.GroupMemberUpdated -> { if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.toMember) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.toMember) + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, r.groupInfo, r.toMember) + } } } is CR.ContactsMerged -> { - if (active(r.user) && chatModel.hasChat(rhId, r.mergedContact.id)) { + if (active(r.user) && chatModel.chatsContext.hasChat(rhId, r.mergedContact.id)) { if (chatModel.chatId.value == r.mergedContact.id) { chatModel.chatId.value = r.intoContact.id } - chatModel.removeChat(rhId, r.mergedContact.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(rhId, r.mergedContact.id) + } } } + // ContactsSubscribed, ContactsDisconnected and ContactSubSummary are only used in CLI, + // They have to be used here for remote desktop to process these status updates. is CR.ContactsSubscribed -> updateContactsStatus(r.contactRefs, NetworkStatus.Connected()) is CR.ContactsDisconnected -> updateContactsStatus(r.contactRefs, NetworkStatus.Disconnected()) is CR.ContactSubSummary -> { for (sub in r.contactSubscriptions) { if (active(r.user)) { - chatModel.updateContact(rhId, sub.contact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, sub.contact) + } } val err = sub.contactError if (err == null) { @@ -1643,133 +2408,265 @@ object ChatController { chatModel.networkStatuses[s.agentConnId] = s.networkStatus } } - is CR.NewChatItem -> { - val cInfo = r.chatItem.chatInfo - val cItem = r.chatItem.chatItem - if (active(r.user)) { - chatModel.addChatItem(rhId, cInfo, cItem) - } else if (cItem.isRcvNew && cInfo.ntfsEnabled) { - chatModel.increaseUnreadCounter(rhId, r.user) - } - val file = cItem.file - val mc = cItem.content.msgContent - if (file != null && + is CR.NewChatItems -> withBGApi { + r.chatItems.forEach { chatItem -> + val cInfo = chatItem.chatInfo + val cItem = chatItem.chatItem + if (active(r.user)) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.addChatItem(rhId, cInfo, cItem) + if (cItem.isActiveReport) { + chatModel.chatsContext.increaseGroupReportsCounter(rhId, cInfo.id) + } + } + withContext(Dispatchers.Main) { + if (cItem.isReport) { + chatModel.secondaryChatsContext.value?.addChatItem(rhId, cInfo, cItem) + } + } + } else if (cItem.isRcvNew && cInfo.ntfsEnabled(cItem)) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.increaseUnreadCounter(rhId, r.user) + } + } + val file = cItem.file + val mc = cItem.content.msgContent + if (file != null && appPrefs.privacyAcceptImages.get() && ((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV) || (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV) - || (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) { - withApi { receiveFile(rhId, r.user, file.fileId, encrypted = cItem.encryptLocalFile && chatController.appPrefs.privacyEncryptLocalFiles.get(), auto = true) } - } - if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId)) { - ntfManager.notifyMessageReceived(r.user, cInfo, cItem) + || (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted)) + ) { + receiveFile(rhId, r.user, file.fileId, auto = true) + } + ntfManager.notifyMessageReceived(rhId, r.user, cInfo, cItem) } } - is CR.ChatItemStatusUpdated -> { - val cInfo = r.chatItem.chatInfo - val cItem = r.chatItem.chatItem - if (!cItem.isDeletedContent && active(r.user)) { - chatModel.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) + is CR.ChatItemsStatusesUpdated -> + r.chatItems.forEach { chatItem -> + val cInfo = chatItem.chatInfo + val cItem = chatItem.chatItem + if (!cItem.isDeletedContent && active(r.user)) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) + } + withContext(Dispatchers.Main) { + if (cItem.isReport) { + chatModel.secondaryChatsContext.value?.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) + } + } + } } - } is CR.ChatItemUpdated -> - chatItemSimpleUpdate(rhId, r.user, r.chatItem) + chatItemUpdateNotify(rhId, r.user, r.chatItem) is CR.ChatItemReaction -> { if (active(r.user)) { - chatModel.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) + } + withContext(Dispatchers.Main) { + if (r.reaction.chatReaction.chatItem.isReport) { + chatModel.secondaryChatsContext.value?.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) + } + } } } - is CR.ChatItemDeleted -> { + is CR.ChatItemsDeleted -> { if (!active(r.user)) { - if (r.toChatItem == null && r.deletedChatItem.chatItem.isRcvNew && r.deletedChatItem.chatInfo.ntfsEnabled) { - chatModel.decreaseUnreadCounter(rhId, r.user) + r.chatItemDeletions.forEach { (deletedChatItem, toChatItem) -> + if (toChatItem == null && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled(deletedChatItem.chatItem)) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.decreaseUnreadCounter(rhId, r.user) + } + } } return } - - val cInfo = r.deletedChatItem.chatInfo - val cItem = r.deletedChatItem.chatItem - AudioPlayer.stop(cItem) - val isLastChatItem = chatModel.getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id - if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) { - ntfManager.cancelNotificationsForChat(cInfo.id) - ntfManager.displayNotification( - r.user, - cInfo.id, - cInfo.displayName, - generalGetString(if (r.toChatItem != null) MR.strings.marked_deleted_description else MR.strings.deleted_description) - ) - } - if (r.toChatItem == null) { - chatModel.removeChatItem(rhId, cInfo, cItem) - } else { - chatModel.upsertChatItem(rhId, cInfo, r.toChatItem.chatItem) + r.chatItemDeletions.forEach { (deletedChatItem, toChatItem) -> + val cInfo = deletedChatItem.chatInfo + val cItem = deletedChatItem.chatItem + if (chatModel.chatId.value != null) { + // Stop voice playback only inside a chat, allow to play in a chat list + AudioPlayer.stop(cItem) + } + val isLastChatItem = chatModel.getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id + if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) { + ntfManager.cancelNotificationsForChat(cInfo.id) + ntfManager.displayNotification( + r.user, + cInfo.id, + cInfo.displayName, + generalGetString(if (toChatItem != null) MR.strings.marked_deleted_description else MR.strings.deleted_description) + ) + } + withContext(Dispatchers.Main) { + if (toChatItem == null) { + chatModel.chatsContext.removeChatItem(rhId, cInfo, cItem) + } else { + chatModel.chatsContext.upsertChatItem(rhId, cInfo, toChatItem.chatItem) + } + if (cItem.isActiveReport) { + chatModel.chatsContext.decreaseGroupReportsCounter(rhId, cInfo.id) + } + } + withContext(Dispatchers.Main) { + if (cItem.isReport) { + if (toChatItem == null) { + chatModel.secondaryChatsContext.value?.removeChatItem(rhId, cInfo, cItem) + } else { + chatModel.secondaryChatsContext.value?.upsertChatItem(rhId, cInfo, toChatItem.chatItem) + } + } + } } } + is CR.GroupChatItemsDeleted -> { + groupChatItemsDeleted(rhId, r) + } is CR.ReceivedGroupInvitation -> { if (active(r.user)) { - chatModel.updateGroup(rhId, r.groupInfo) // update so that repeat group invitations are not duplicated + withContext(Dispatchers.Main) { + // update so that repeat group invitations are not duplicated + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) + } // TODO NtfManager.shared.notifyGroupInvitation } } is CR.UserAcceptedGroupSent -> { if (!active(r.user)) return - chatModel.updateGroup(rhId, r.groupInfo) - val conn = r.hostContact?.activeConn - if (conn != null) { - chatModel.dismissConnReqView(conn.id) - chatModel.removeChat(rhId, conn.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) + val conn = r.hostContact?.activeConn + if (conn != null) { + chatModel.replaceConnReqView(conn.id, "#${r.groupInfo.groupId}") + chatModel.chatsContext.removeChat(rhId, conn.id) + } } } is CR.GroupLinkConnecting -> { if (!active(r.user)) return - chatModel.updateGroup(rhId, r.groupInfo) - val hostConn = r.hostMember.activeConn - if (hostConn != null) { - chatModel.dismissConnReqView(hostConn.id) - chatModel.removeChat(rhId, hostConn.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) + val hostConn = r.hostMember.activeConn + if (hostConn != null) { + chatModel.replaceConnReqView(hostConn.id, "#${r.groupInfo.groupId}") + chatModel.chatsContext.removeChat(rhId, hostConn.id) + } + } + } + is CR.BusinessLinkConnecting -> { + if (!active(r.user)) return + + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) + } + if (chatModel.chatId.value == r.fromContact.id) { + openGroupChat(rhId, r.groupInfo.groupId) + } + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(rhId, r.fromContact.id) } } is CR.JoinedGroupMemberConnecting -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.DeletedMemberUser -> // TODO update user member if (active(r.user)) { - chatModel.updateGroup(rhId, r.groupInfo) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) + if (r.withMessages) { + chatModel.chatsContext.removeMemberItems(rhId, r.groupInfo.membership, byMember = r.member, r.groupInfo) + } + } + withContext(Dispatchers.Main) { + if (r.withMessages) { + chatModel.secondaryChatsContext.value?.removeMemberItems(rhId, r.groupInfo.membership, byMember = r.member, r.groupInfo) + } + } } is CR.DeletedMember -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.deletedMember) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.deletedMember) + if (r.withMessages) { + chatModel.chatsContext.removeMemberItems(rhId, r.deletedMember, byMember = r.byMember, r.groupInfo) + } + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, r.groupInfo, r.deletedMember) + if (r.withMessages) { + chatModel.secondaryChatsContext.value?.removeMemberItems(rhId, r.deletedMember, byMember = r.byMember, r.groupInfo) + } + } } is CR.LeftMember -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member) + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.MemberRole -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member) + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, r.groupInfo, r.member) + } } - is CR.MemberRoleUser -> + is CR.MembersRoleUser -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withContext(Dispatchers.Main) { + r.members.forEach { member -> + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, member) + } + } + withContext(Dispatchers.Main) { + r.members.forEach { member -> + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, r.groupInfo, member) + } + } + } + is CR.MemberBlockedForAll -> + if (active(r.user)) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member) + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.GroupDeleted -> // TODO update user member if (active(r.user)) { - chatModel.updateGroup(rhId, r.groupInfo) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) + } } is CR.UserJoinedGroup -> if (active(r.user)) { - chatModel.updateGroup(rhId, r.groupInfo) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) + } } is CR.JoinedGroupMember -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.ConnectedToGroupMember -> { if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member) + } } if (r.memberContact != null) { chatModel.setContactNetworkStatus(r.memberContact, NetworkStatus.Connected()) @@ -1777,11 +2674,15 @@ object ChatController { } is CR.GroupUpdated -> if (active(r.user)) { - chatModel.updateGroup(rhId, r.toGroup) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.toGroup) + } } is CR.NewMemberContactReceivedInv -> if (active(r.user)) { - chatModel.updateContact(rhId, r.contact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, r.contact) + } } is CR.RcvFileStart -> chatItemSimpleUpdate(rhId, r.user, r.chatItem) @@ -1791,11 +2692,21 @@ object ChatController { chatItemSimpleUpdate(rhId, r.user, r.chatItem) cleanupFile(r.chatItem) } - is CR.RcvFileProgressXFTP -> - chatItemSimpleUpdate(rhId, r.user, r.chatItem) + is CR.RcvFileProgressXFTP -> { + if (r.chatItem_ != null) { + chatItemSimpleUpdate(rhId, r.user, r.chatItem_) + } + } is CR.RcvFileError -> { - chatItemSimpleUpdate(rhId, r.user, r.chatItem) - cleanupFile(r.chatItem) + if (r.chatItem_ != null) { + chatItemSimpleUpdate(rhId, r.user, r.chatItem_) + cleanupFile(r.chatItem_) + } + } + is CR.RcvFileWarning -> { + if (r.chatItem_ != null) { + chatItemSimpleUpdate(rhId, r.user, r.chatItem_) + } } is CR.SndFileStart -> chatItemSimpleUpdate(rhId, r.user, r.chatItem) @@ -1804,18 +2715,29 @@ object ChatController { cleanupDirectFile(r.chatItem) } is CR.SndFileRcvCancelled -> { - chatItemSimpleUpdate(rhId, r.user, r.chatItem) - cleanupDirectFile(r.chatItem) + if (r.chatItem_ != null) { + chatItemSimpleUpdate(rhId, r.user, r.chatItem_) + cleanupDirectFile(r.chatItem_) + } + } + is CR.SndFileProgressXFTP -> { + if (r.chatItem_ != null) { + chatItemSimpleUpdate(rhId, r.user, r.chatItem_) + } } - is CR.SndFileProgressXFTP -> - chatItemSimpleUpdate(rhId, r.user, r.chatItem) is CR.SndFileCompleteXFTP -> { chatItemSimpleUpdate(rhId, r.user, r.chatItem) - cleanupFile(r.chatItem) } is CR.SndFileError -> { - chatItemSimpleUpdate(rhId, r.user, r.chatItem) - cleanupFile(r.chatItem) + if (r.chatItem_ != null) { + chatItemSimpleUpdate(rhId, r.user, r.chatItem_) + cleanupFile(r.chatItem_) + } + } + is CR.SndFileWarning -> { + if (r.chatItem_ != null) { + chatItemSimpleUpdate(rhId, r.user, r.chatItem_) + } } is CR.CallInvitation -> { chatModel.callManager.reportNewIncomingCall(r.callInvitation.copy(remoteHostId = rhId)) @@ -1824,7 +2746,7 @@ object ChatController { // TODO askConfirmation? // TODO check encryption is compatible withCall(r, r.contact) { call -> - chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, peerMedia = r.callType.media, sharedKey = r.sharedKey) + chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, sharedKey = r.sharedKey) val useRelay = appPrefs.webrtcPolicyRelay.get() val iceServers = getIceServers() Log.d(TAG, ".callOffer iceServers $iceServers") @@ -1854,37 +2776,82 @@ object ChatController { if (invitation != null) { chatModel.callManager.reportCallRemoteEnded(invitation = invitation) } - withCall(r, r.contact) { _ -> - chatModel.callCommand.add(WCallCommand.End) - withApi { - chatModel.activeCall.value = null - chatModel.showCallView.value = false - } + withCall(r, r.contact) { call -> + withBGApi { chatModel.callManager.endCall(call) } } } is CR.ContactSwitch -> - chatModel.updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats) + if (active(r.user)) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats) + } + } is CR.GroupMemberSwitch -> - chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats) + if (active(r.user)) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats) + } + } is CR.ContactRatchetSync -> - chatModel.updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats) + if (active(r.user)) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats) + } + } is CR.GroupMemberRatchetSync -> - chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats) + if (active(r.user)) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats) + } + } is CR.RemoteHostSessionCode -> { chatModel.remoteHostPairing.value = r.remoteHost_ to RemoteHostSessionState.PendingConfirmation(r.sessionCode) } is CR.RemoteHostConnected -> { // TODO needs to update it instead in sessions chatModel.currentRemoteHost.value = r.remoteHost + ModalManager.start.closeModals() switchUIRemoteHost(r.remoteHost.remoteHostId) } + is CR.ContactDisabled -> { + if (active(r.user)) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, r.contact) + } + } + } is CR.RemoteHostStopped -> { val disconnectedHost = chatModel.remoteHosts.firstOrNull { it.remoteHostId == r.remoteHostId_ } chatModel.remoteHostPairing.value = null if (disconnectedHost != null) { - showToast( - generalGetString(MR.strings.remote_host_was_disconnected_toast).format(disconnectedHost.hostDeviceName.ifEmpty { disconnectedHost.remoteHostId.toString() }) - ) + val deviceName = disconnectedHost.hostDeviceName.ifEmpty { disconnectedHost.remoteHostId.toString() } + when (r.rhStopReason) { + is RemoteHostStopReason.ConnectionFailed -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.remote_host_was_disconnected_title), + if (r.rhStopReason.chatError is ChatError.ChatErrorRemoteHost) { + r.rhStopReason.chatError.remoteHostError.localizedString(deviceName) + } else { + generalGetString(MR.strings.remote_host_disconnected_from).format(deviceName, r.rhStopReason.chatError.string) + } + ) + } + is RemoteHostStopReason.Crashed -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.remote_host_was_disconnected_title), + if (r.rhStopReason.chatError is ChatError.ChatErrorRemoteHost) { + r.rhStopReason.chatError.remoteHostError.localizedString(deviceName) + } else { + generalGetString(MR.strings.remote_host_disconnected_from).format(deviceName, r.rhStopReason.chatError.string) + } + ) + } + is RemoteHostStopReason.Disconnected -> { + if (r.rhsState is RemoteHostSessionState.Connected || r.rhsState is RemoteHostSessionState.Confirmed) { + showToast(generalGetString(MR.strings.remote_host_was_disconnected_toast).format(deviceName)) + } + } + } } if (chatModel.remoteHostId() == r.remoteHostId_) { chatModel.currentRemoteHost.value = null @@ -1915,13 +2882,79 @@ object ChatController { val sess = chatModel.remoteCtrlSession.value if (sess != null) { chatModel.remoteCtrlSession.value = null + ModalManager.fullscreen.closeModals() + fun showAlert(chatError: ChatError) { + when { + r.rcStopReason is RemoteCtrlStopReason.Disconnected -> + {} + r.rcStopReason is RemoteCtrlStopReason.ConnectionFailed + && r.rcStopReason.chatError is ChatError.ChatErrorAgent + && r.rcStopReason.chatError.agentError is AgentErrorType.RCP + && r.rcStopReason.chatError.agentError.rcpErr is RCErrorType.IDENTITY -> + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.remote_ctrl_was_disconnected_title), + text = generalGetString(MR.strings.remote_ctrl_connection_stopped_identity_desc) + ) + else -> + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.remote_ctrl_was_disconnected_title), + text = if (chatError is ChatError.ChatErrorRemoteCtrl) { + chatError.remoteCtrlError.localizedString + } else { + generalGetString(MR.strings.remote_ctrl_connection_stopped_desc) + }, + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.ok), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + val clipboard = LocalClipboardManager.current + SectionItemView({ + clipboard.setText(AnnotatedString(json.encodeToString(r.rcStopReason))) + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.copy_error), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) + } + } + when (r.rcStopReason) { + is RemoteCtrlStopReason.DiscoveryFailed -> showAlert(r.rcStopReason.chatError) + is RemoteCtrlStopReason.ConnectionFailed -> showAlert(r.rcStopReason.chatError) + is RemoteCtrlStopReason.SetupFailed -> showAlert(r.rcStopReason.chatError) + is RemoteCtrlStopReason.Disconnected -> { + /*AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.remote_ctrl_was_disconnected_title), + )*/ + } + } + if (sess.sessionState is UIRemoteCtrlSessionState.Connected) { switchToLocalSession() } } } + is CR.ContactPQEnabled -> + if (active(r.user)) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, r.contact) + } + } else -> - Log.d(TAG , "unsupported event: ${r.responseType}") + Log.d(TAG , "unsupported event: ${msg.responseType}") + } + val e = (msg as? API.Error)?.err + when { + e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.CRITICAL -> + chatModel.processedCriticalError.newError(e.agentError, e.agentError.offerRestart) + e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.INTERNAL && appPrefs.developerTools.get() && appPrefs.showInternalErrors.get() -> + chatModel.processedInternalError.newError(e.agentError, false) + else -> + Log.d(TAG , "unsupported event: ${msg.responseType}") } } @@ -1943,20 +2976,18 @@ object ChatController { } } - fun switchToLocalSession() { + suspend fun switchToLocalSession() { val m = chatModel m.remoteCtrlSession.value = null - withBGApi { - val users = listUsers(null) - m.users.clear() - m.users.addAll(users) - getUserChatData(null) - val statuses = apiGetNetworkStatuses(null) - if (statuses != null) { - chatModel.networkStatuses.clear() - val ss = statuses.associate { it.agentConnId to it.networkStatus }.toMap() - chatModel.networkStatuses.putAll(ss) - } + val users = listUsers(null) + m.users.clear() + m.users.addAll(users) + getUserChatData(null) + val statuses = apiGetNetworkStatuses(null) + if (statuses != null) { + chatModel.networkStatuses.clear() + val ss = statuses.associate { it.agentConnId to it.networkStatus }.toMap() + chatModel.networkStatuses.putAll(ss) } } @@ -1972,28 +3003,101 @@ object ChatController { } } - suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, encrypted: Boolean, auto: Boolean = false) { - val chatItem = apiReceiveFile(rhId, fileId, encrypted = encrypted, auto = auto) - if (chatItem != null) { - chatItemSimpleUpdate(rhId, user, chatItem) - } - } - suspend fun leaveGroup(rh: Long?, groupId: Long) { val groupInfo = apiLeaveGroup(rh, groupId) if (groupInfo != null) { - chatModel.updateGroup(rh, groupInfo) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rh, groupInfo) + } } } private suspend fun chatItemSimpleUpdate(rh: Long?, user: UserLike, aChatItem: AChatItem) { + if (activeUser(rh, user)) { + val cInfo = aChatItem.chatInfo + val cItem = aChatItem.chatItem + withContext(Dispatchers.Main) { chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem) } + withContext(Dispatchers.Main) { + if (cItem.isReport) { + chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem) + } + } + } + } + + suspend fun groupChatItemsDeleted(rhId: Long?, r: CR.GroupChatItemsDeleted) { + if (!activeUser(rhId, r.user)) { + val users = chatController.listUsers(rhId) + chatModel.users.clear() + chatModel.users.addAll(users) + return + } + val cInfo = ChatInfo.Group(r.groupInfo) + withContext(Dispatchers.Main) { + val chatsCtx = chatModel.chatsContext + r.chatItemIDs.forEach { itemId -> + chatsCtx.decreaseGroupReportsCounter(rhId, cInfo.id) + val cItem = chatsCtx.chatItems.value.lastOrNull { it.id == itemId } ?: return@forEach + if (chatModel.chatId.value != null) { + // Stop voice playback only inside a chat, allow to play in a chat list + AudioPlayer.stop(cItem) + } + val isLastChatItem = chatsCtx.getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id + if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) { + ntfManager.cancelNotificationsForChat(cInfo.id) + ntfManager.displayNotification( + r.user, + cInfo.id, + cInfo.displayName, + generalGetString(MR.strings.marked_deleted_description) + ) + } + val deleted = if (r.member_ != null && (cItem.chatDir as CIDirection.GroupRcv?)?.groupMember?.groupMemberId != r.member_.groupMemberId) { + CIDeleted.Moderated(Clock.System.now(), r.member_) + } else { + CIDeleted.Deleted(Clock.System.now()) + } + chatsCtx.upsertChatItem(rhId, cInfo, cItem.copy(meta = cItem.meta.copy(itemDeleted = deleted))) + } + } + withContext(Dispatchers.Main) { + val chatsCtx = chatModel.secondaryChatsContext.value + if (chatsCtx != null) { + r.chatItemIDs.forEach { itemId -> + val cItem = chatsCtx.chatItems.value.lastOrNull { it.id == itemId } ?: return@forEach + if (chatModel.chatId.value != null) { + // Stop voice playback only inside a chat, allow to play in a chat list + AudioPlayer.stop(cItem) + } + val deleted = if (r.member_ != null && (cItem.chatDir as CIDirection.GroupRcv?)?.groupMember?.groupMemberId != r.member_.groupMemberId) { + CIDeleted.Moderated(Clock.System.now(), r.member_) + } else { + CIDeleted.Deleted(Clock.System.now()) + } + chatsCtx.upsertChatItem(rhId, cInfo, cItem.copy(meta = cItem.meta.copy(itemDeleted = deleted))) + } + } + } + } + + private suspend fun chatItemUpdateNotify(rh: Long?, user: UserLike, aChatItem: AChatItem) { val cInfo = aChatItem.chatInfo val cItem = aChatItem.chatItem - val notify = { ntfManager.notifyMessageReceived(user, cInfo, cItem) } + val notify = { ntfManager.notifyMessageReceived(rh, user, cInfo, cItem) } if (!activeUser(rh, user)) { notify() - } else if (chatModel.upsertChatItem(rh, cInfo, cItem)) { - notify() + } else { + val createdChat = withContext(Dispatchers.Main) { chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem) } + withContext(Dispatchers.Main) { + if (cItem.content.msgContent is MsgContent.MCReport) { + chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem) + } + } + if (createdChat) { + notify() + } else if (cItem.content is CIContent.RcvCall && cItem.content.status == CICallStatus.Missed) { + notify() + } } } @@ -2033,8 +3137,16 @@ object ChatController { chatModel.users.addAll(users) chatModel.currentUser.value = user if (user == null) { - chatModel.chatItems.clear() - chatModel.chats.clear() + withContext(Dispatchers.Main) { + chatModel.chatsContext.chatItems.clearAndNotify() + chatModel.chatsContext.chats.clear() + chatModel.chatsContext.popChatCollector.clear() + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.chatItems?.clearAndNotify() + chatModel.secondaryChatsContext.value?.chats?.clear() + chatModel.secondaryChatsContext.value?.popChatCollector?.clear() + } } val statuses = apiGetNetworkStatuses(rhId) if (statuses != null) { @@ -2062,28 +3174,24 @@ object ChatController { } } - fun getXFTPCfg(): XFTPFileConfig { - return XFTPFileConfig(minFileSize = 0) - } - fun getNetCfg(): NetCfg { val useSocksProxy = appPrefs.networkUseSocksProxy.get() - val proxyHostPort = appPrefs.networkProxyHostPort.get() + val networkProxy = appPrefs.networkProxy.get() val socksProxy = if (useSocksProxy) { - if (proxyHostPort?.startsWith("localhost:") == true) { - proxyHostPort.removePrefix("localhost") - } else { - proxyHostPort ?: ":9050" - } + networkProxy.toProxyString() } else { null } - val hostMode = HostMode.valueOf(appPrefs.networkHostMode.get()!!) + val hostMode = appPrefs.networkHostMode.get() val requiredHostMode = appPrefs.networkRequiredHostMode.get() val sessionMode = appPrefs.networkSessionMode.get() + val smpProxyMode = appPrefs.networkSMPProxyMode.get() + val smpProxyFallback = appPrefs.networkSMPProxyFallback.get() + val smpWebPortServers = appPrefs.networkSMPWebPortServers.get() val tcpConnectTimeout = appPrefs.networkTCPConnectTimeout.get() val tcpTimeout = appPrefs.networkTCPTimeout.get() val tcpTimeoutPerKb = appPrefs.networkTCPTimeoutPerKb.get() + val rcvConcurrency = appPrefs.networkRcvConcurrency.get() val smpPingInterval = appPrefs.networkSMPPingInterval.get() val smpPingCount = appPrefs.networkSMPPingCount.get() val enableKeepAlive = appPrefs.networkEnableKeepAlive.get() @@ -2100,9 +3208,13 @@ object ChatController { hostMode = hostMode, requiredHostMode = requiredHostMode, sessionMode = sessionMode, + smpProxyMode = smpProxyMode, + smpProxyFallback = smpProxyFallback, + smpWebPortServers = smpWebPortServers, tcpConnectTimeout = tcpConnectTimeout, tcpTimeout = tcpTimeout, tcpTimeoutPerKb = tcpTimeoutPerKb, + rcvConcurrency = rcvConcurrency, tcpKeepAlive = tcpKeepAlive, smpPingInterval = smpPingInterval, smpPingCount = smpPingCount @@ -2110,16 +3222,20 @@ object ChatController { } /** - * [AppPreferences.networkProxyHostPort] is not changed here, use appPrefs to set it + * [AppPreferences.networkProxy] is not changed here, use appPrefs to set it * */ fun setNetCfg(cfg: NetCfg) { appPrefs.networkUseSocksProxy.set(cfg.useSocksProxy) - appPrefs.networkHostMode.set(cfg.hostMode.name) + appPrefs.networkHostMode.set(cfg.hostMode) appPrefs.networkRequiredHostMode.set(cfg.requiredHostMode) appPrefs.networkSessionMode.set(cfg.sessionMode) + appPrefs.networkSMPProxyMode.set(cfg.smpProxyMode) + appPrefs.networkSMPProxyFallback.set(cfg.smpProxyFallback) + appPrefs.networkSMPWebPortServers.set(cfg.smpWebPortServers) appPrefs.networkTCPConnectTimeout.set(cfg.tcpConnectTimeout) appPrefs.networkTCPTimeout.set(cfg.tcpTimeout) appPrefs.networkTCPTimeoutPerKb.set(cfg.tcpTimeoutPerKb) + appPrefs.networkRcvConcurrency.set(cfg.rcvConcurrency) appPrefs.networkSMPPingInterval.set(cfg.smpPingInterval) appPrefs.networkSMPPingCount.set(cfg.smpPingCount) if (cfg.tcpKeepAlive != null) { @@ -2135,13 +3251,17 @@ object ChatController { class SharedPreference<T>(val get: () -> T, set: (T) -> Unit) { val set: (T) -> Unit - private val _state: MutableState<T> by lazy { mutableStateOf(get()) } - val state: State<T> by lazy { _state } + private val _state: MutableState<T> = mutableStateOf(get()) + val state: State<T> = _state init { this.set = { value -> - set(value) - _state.value = value + try { + set(value) + _state.value = value + } catch (e: Exception) { + Log.e(TAG, "Error saving settings: ${e.stackTraceToString()}") + } } } } @@ -2150,7 +3270,7 @@ class SharedPreference<T>(val get: () -> T, set: (T) -> Unit) { sealed class CC { class Console(val cmd: String): CC() class ShowActiveUser: CC() - class CreateActiveUser(val profile: Profile?, val sameServers: Boolean, val pastTimestamp: Boolean): CC() + class CreateActiveUser(val profile: Profile?, val pastTimestamp: Boolean): CC() class ListUsers: CC() class ApiSetActiveUser(val userId: Long, val viewPwd: String?): CC() class SetAllContactReceipts(val enable: Boolean): CC() @@ -2161,50 +3281,78 @@ sealed class CC { class ApiMuteUser(val userId: Long): CC() class ApiUnmuteUser(val userId: Long): CC() class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC() - class StartChat(val expire: Boolean): CC() + class StartChat(val mainApp: Boolean): CC() + class CheckChatRunning: CC() class ApiStopChat: CC() - class SetTempFolder(val tempFolder: String): CC() - class SetFilesFolder(val filesFolder: String): CC() - class SetRemoteHostsFolder(val remoteHostsFolder: String): CC() - class ApiSetXFTPConfig(val config: XFTPFileConfig?): CC() + @Serializable + class ApiSetAppFilePaths(val appFilesFolder: String, val appTempFolder: String, val appAssetsFolder: String, val appRemoteHostsFolder: String): CC() class ApiSetEncryptLocalFiles(val enable: Boolean): CC() class ApiExportArchive(val config: ArchiveConfig): CC() class ApiImportArchive(val config: ArchiveConfig): CC() class ApiDeleteStorage: CC() class ApiStorageEncryption(val config: DBEncryptionConfig): CC() + class TestStorageEncryption(val key: String): CC() + class ApiSaveSettings(val settings: AppSettings): CC() + class ApiGetSettings(val settings: AppSettings): CC() + class ApiGetChatTags(val userId: Long): CC() class ApiGetChats(val userId: Long): CC() - class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC() + class ApiGetChat(val type: ChatType, val id: Long, val contentTag: MsgContentTag?, val pagination: ChatPagination, val search: String = ""): CC() class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC() - class ApiSendMessage(val type: ChatType, val id: Long, val file: CryptoFile?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean, val ttl: Int?): CC() - class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): CC() - class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC() - class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): CC() + class ApiSendMessages(val type: ChatType, val id: Long, val live: Boolean, val ttl: Int?, val composedMessages: List<ComposedMessage>): CC() + class ApiCreateChatTag(val tag: ChatTagData): CC() + class ApiSetChatTags(val type: ChatType, val id: Long, val tagIds: List<Long>): CC() + class ApiDeleteChatTag(val tagId: Long): CC() + class ApiUpdateChatTag(val tagId: Long, val tagData: ChatTagData): CC() + class ApiReorderChatTags(val tagIds: List<Long>): CC() + class ApiCreateChatItems(val noteFolderId: Long, val composedMessages: List<ComposedMessage>): CC() + class ApiReportMessage(val groupId: Long, val chatItemId: Long, val reportReason: ReportReason, val reportText: String): CC() + class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val updatedMessage: UpdatedMessage, val live: Boolean): CC() + class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemIds: List<Long>, val mode: CIDeleteMode): CC() + class ApiDeleteMemberChatItem(val groupId: Long, val itemIds: List<Long>): CC() + class ApiArchiveReceivedReports(val groupId: Long): CC() + class ApiDeleteReceivedReports(val groupId: Long, val itemIds: List<Long>, val mode: CIDeleteMode): CC() class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): 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 chatItemIds: List<Long>): CC() + class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemIds: List<Long>, val ttl: Int?): CC() class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC() class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC() class ApiJoinGroup(val groupId: Long): CC() - class ApiMemberRole(val groupId: Long, val memberId: Long, val memberRole: GroupMemberRole): CC() - class ApiRemoveMember(val groupId: Long, val memberId: Long): CC() + class ApiMembersRole(val groupId: Long, val memberIds: List<Long>, val memberRole: GroupMemberRole): CC() + class ApiBlockMembersForAll(val groupId: Long, val memberIds: List<Long>, val blocked: Boolean): CC() + class ApiRemoveMembers(val groupId: Long, val memberIds: List<Long>, val withMessages: Boolean): CC() class ApiLeaveGroup(val groupId: Long): CC() class ApiListMembers(val groupId: Long): CC() class ApiUpdateGroupProfile(val groupId: Long, val groupProfile: GroupProfile): CC() - class APICreateGroupLink(val groupId: Long, val memberRole: GroupMemberRole): CC() + class APICreateGroupLink(val groupId: Long, val memberRole: GroupMemberRole, val short: Boolean): CC() class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC() class APIDeleteGroupLink(val groupId: Long): CC() class APIGetGroupLink(val groupId: Long): CC() class APICreateMemberContact(val groupId: Long, val groupMemberId: Long): CC() class APISendMemberContactInvitation(val contactId: Long, val mc: MsgContent): CC() - class APIGetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol): CC() - class APISetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol, val servers: List<ServerCfg>): CC() class APITestProtoServer(val userId: Long, val server: String): CC() - class APISetChatItemTTL(val userId: Long, val seconds: Long?): CC() + class ApiGetServerOperators(): CC() + class ApiSetServerOperators(val operators: List<ServerOperator>): CC() + class ApiGetUserServers(val userId: Long): CC() + class ApiSetUserServers(val userId: Long, val userServers: List<UserOperatorServers>): CC() + class ApiValidateServers(val userId: Long, val userServers: List<UserOperatorServers>): CC() + class ApiGetUsageConditions(): CC() + class ApiSetConditionsNotified(val conditionsId: Long): CC() + class ApiAcceptConditions(val conditionsId: Long, val operatorIds: List<Long>): CC() + class APISetChatItemTTL(val userId: Long, val seconds: Long): CC() class APIGetChatItemTTL(val userId: Long): CC() + class APISetChatTTL(val userId: Long, val chatType: ChatType, val id: Long, val seconds: Long?): CC() class APISetNetworkConfig(val networkConfig: NetCfg): CC() class APIGetNetworkConfig: CC() + class APISetNetworkInfo(val networkInfo: UserNetworkInfo): CC() + class ReconnectServer(val userId: Long, val server: String): CC() + class ReconnectAllServers: CC() class APISetChatSettings(val type: ChatType, val id: Long, val chatSettings: ChatSettings): CC() class ApiSetMemberSettings(val groupId: Long, val groupMemberId: Long, val memberSettings: GroupMemberSettings): CC() class APIContactInfo(val contactId: Long): CC() class APIGroupMemberInfo(val groupId: Long, val groupMemberId: Long): CC() + class APIContactQueueInfo(val contactId: Long): CC() + class APIGroupMemberQueueInfo(val groupId: Long, val groupMemberId: Long): CC() class APISwitchContact(val contactId: Long): CC() class APISwitchGroupMember(val groupId: Long, val groupMemberId: Long): CC() class APIAbortSwitchContact(val contactId: Long): CC() @@ -2215,23 +3363,28 @@ sealed class CC { class APIGetGroupMemberCode(val groupId: Long, val groupMemberId: Long): CC() class APIVerifyContact(val contactId: Long, val connectionCode: String?): CC() class APIVerifyGroupMember(val groupId: Long, val groupMemberId: Long, val connectionCode: String?): CC() - class APIAddContact(val userId: Long, val incognito: Boolean): CC() + class APIAddContact(val userId: Long, val short: Boolean, val incognito: Boolean): CC() class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC() - class APIConnectPlan(val userId: Long, val connReq: String): CC() - class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC() + class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC() + class APIConnectPlan(val userId: Long, val connLink: String): CC() + class APIConnect(val userId: Long, val incognito: Boolean, val connLink: CreatedConnLink): CC() class ApiConnectContactViaAddress(val userId: Long, val incognito: Boolean, val contactId: Long): CC() - class ApiDeleteChat(val type: ChatType, val id: Long, val notify: Boolean?): CC() + class ApiDeleteChat(val type: ChatType, val id: Long, val chatDeleteMode: ChatDeleteMode): CC() class ApiClearChat(val type: ChatType, val id: Long): CC() class ApiListContacts(val userId: Long): CC() class ApiUpdateProfile(val userId: Long, val profile: Profile): CC() class ApiSetContactPrefs(val contactId: Long, val prefs: ChatPreferences): CC() class ApiSetContactAlias(val contactId: Long, val localAlias: String): CC() + class ApiSetGroupAlias(val groupId: Long, val localAlias: String): CC() class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC() - class ApiCreateMyAddress(val userId: Long): CC() + class ApiSetUserUIThemes(val userId: Long, val themes: ThemeModeOverrides?): CC() + class ApiSetChatUIThemes(val chatId: String, val themes: ThemeModeOverrides?): CC() + class ApiCreateMyAddress(val userId: Long, val short: Boolean): CC() class ApiDeleteMyAddress(val userId: Long): CC() class ApiShowMyAddress(val userId: Long): CC() class ApiSetProfileAddress(val userId: Long, val on: Boolean): CC() class ApiAddressAutoAccept(val userId: Long, val autoAccept: AutoAccept?): CC() + class ApiGetCallInvitations: CC() class ApiSendCallInvitation(val contact: Contact, val callType: CallType): CC() class ApiRejectCall(val contact: Contact): CC() class ApiSendCallOffer(val contact: Contact, val callOffer: WebRTCCallOffer): CC() @@ -2242,9 +3395,10 @@ sealed class CC { class ApiGetNetworkStatuses(): CC() class ApiAcceptContact(val incognito: Boolean, val contactReqId: Long): CC() class ApiRejectContact(val contactReqId: Long): CC() - class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC() + class ApiChatRead(val type: ChatType, val id: Long): CC() + class ApiChatItemsRead(val type: ChatType, val id: Long, val itemIds: List<Long>): CC() class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC() - class ReceiveFile(val fileId: Long, val encrypt: Boolean, val inline: Boolean?): CC() + class ReceiveFile(val fileId: Long, val userApprovedRelays: Boolean, val encrypt: Boolean, val inline: Boolean?): CC() class CancelFile(val fileId: Long): CC() // Remote control class SetLocalDeviceName(val displayName: String): CC() @@ -2262,14 +3416,20 @@ sealed class CC { class ListRemoteCtrls(): CC() class StopRemoteCtrl(): CC() class DeleteRemoteCtrl(val remoteCtrlId: Long): CC() + class ApiUploadStandaloneFile(val userId: Long, val file: CryptoFile): CC() + class ApiDownloadStandaloneFile(val userId: Long, val url: String, val file: CryptoFile): CC() + class ApiStandaloneFileInfo(val url: String): CC() // misc class ShowVersion(): CC() + class ResetAgentServersStats(): CC() + class GetAgentSubsTotal(val userId: Long): CC() + class GetAgentServersSummary(val userId: Long): CC() val cmdString: String get() = when (this) { is Console -> cmd is ShowActiveUser -> "/u" is CreateActiveUser -> { - val user = NewUser(profile, sameServers = sameServers, pastTimestamp = pastTimestamp) + val user = NewUser(profile, pastTimestamp = pastTimestamp) "/_create user ${json.encodeToString(user)}" } is ListUsers -> "/users" @@ -2288,53 +3448,96 @@ sealed class CC { is ApiMuteUser -> "/_mute user $userId" is ApiUnmuteUser -> "/_unmute user $userId" is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}" - is StartChat -> "/_start subscribe=on expire=${onOff(expire)} xftp=on" + is StartChat -> "/_start main=${onOff(mainApp)}" + is CheckChatRunning -> "/_check running" is ApiStopChat -> "/_stop" - is SetTempFolder -> "/_temp_folder $tempFolder" - is SetFilesFolder -> "/_files_folder $filesFolder" - is SetRemoteHostsFolder -> "/remote_hosts_folder $remoteHostsFolder" - is ApiSetXFTPConfig -> if (config != null) "/_xftp on ${json.encodeToString(config)}" else "/_xftp off" + is ApiSetAppFilePaths -> "/set file paths ${json.encodeToString(this)}" is ApiSetEncryptLocalFiles -> "/_files_encrypt ${onOff(enable)}" is ApiExportArchive -> "/_db export ${json.encodeToString(config)}" is ApiImportArchive -> "/_db import ${json.encodeToString(config)}" is ApiDeleteStorage -> "/_db delete" is ApiStorageEncryption -> "/_db encryption ${json.encodeToString(config)}" + is TestStorageEncryption -> "/db test key $key" + is ApiSaveSettings -> "/_save app settings ${json.encodeToString(settings)}" + is ApiGetSettings -> "/_get app settings ${json.encodeToString(settings)}" + is ApiGetChatTags -> "/_get tags $userId" is ApiGetChats -> "/_get chats $userId pcc=on" - is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search") - is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId" - is ApiSendMessage -> { - val ttlStr = if (ttl != null) "$ttl" else "default" - "/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}" + is ApiGetChat -> { + val tag = if (contentTag == null) { + "" + } else { + " content=${contentTag.name.lowercase()}" + } + "/_get chat ${chatRef(type, id)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search") } - is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${mc.cmdString}" - is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}" - is ApiDeleteMemberChatItem -> "/_delete member item #$groupId $groupMemberId $itemId" + is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId" + is ApiSendMessages -> { + val msgs = json.encodeToString(composedMessages) + val ttlStr = if (ttl != null) "$ttl" else "default" + "/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json $msgs" + } + is ApiCreateChatTag -> "/_create tag ${json.encodeToString(tag)}" + is ApiSetChatTags -> "/_tags ${chatRef(type, id)} ${tagIds.joinToString(",")}" + is ApiDeleteChatTag -> "/_delete tag $tagId" + is ApiUpdateChatTag -> "/_update tag $tagId ${json.encodeToString(tagData)}" + is ApiReorderChatTags -> "/_reorder tags ${tagIds.joinToString(",")}" + is ApiCreateChatItems -> { + val msgs = json.encodeToString(composedMessages) + "/_create *$noteFolderId json $msgs" + } + is ApiReportMessage -> "/_report #$groupId $chatItemId reason=${json.encodeToString(reportReason).trim('"')} $reportText" + is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${updatedMessage.cmdString}" + is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} ${itemIds.joinToString(",")} ${mode.deleteMode}" + is ApiDeleteMemberChatItem -> "/_delete member item #$groupId ${itemIds.joinToString(",")}" + is ApiArchiveReceivedReports -> "/_archive reports #$groupId" + is ApiDeleteReceivedReports -> "/_delete reports #$groupId ${itemIds.joinToString(",")} ${mode.deleteMode}" is ApiChatItemReaction -> "/_reaction ${chatRef(type, id)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}" + is ApiGetReactionMembers -> "/_reaction members $userId #$groupId $itemId ${json.encodeToString(reaction)}" + is ApiForwardChatItems -> { + val ttlStr = if (ttl != null) "$ttl" else "default" + "/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} ${itemIds.joinToString(",")} ttl=${ttlStr}" + } + is ApiPlanForwardChatItems -> { + "/_forward plan ${chatRef(fromChatType, fromChatId)} ${chatItemIds.joinToString(",")}" + } is ApiNewGroup -> "/_group $userId incognito=${onOff(incognito)} ${json.encodeToString(groupProfile)}" is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}" is ApiJoinGroup -> "/_join #$groupId" - is ApiMemberRole -> "/_member role #$groupId $memberId ${memberRole.memberRole}" - is ApiRemoveMember -> "/_remove #$groupId $memberId" + is ApiMembersRole -> "/_member role #$groupId ${memberIds.joinToString(",")} ${memberRole.memberRole}" + is ApiBlockMembersForAll -> "/_block #$groupId ${memberIds.joinToString(",")} blocked=${onOff(blocked)}" + is ApiRemoveMembers -> "/_remove #$groupId ${memberIds.joinToString(",")} messages=${onOff(withMessages)}" is ApiLeaveGroup -> "/_leave #$groupId" is ApiListMembers -> "/_members #$groupId" is ApiUpdateGroupProfile -> "/_group_profile #$groupId ${json.encodeToString(groupProfile)}" - is APICreateGroupLink -> "/_create link #$groupId ${memberRole.name.lowercase()}" + is APICreateGroupLink -> "/_create link #$groupId ${memberRole.name.lowercase()} short=${onOff(short)}" is APIGroupLinkMemberRole -> "/_set link role #$groupId ${memberRole.name.lowercase()}" is APIDeleteGroupLink -> "/_delete link #$groupId" is APIGetGroupLink -> "/_get link #$groupId" is APICreateMemberContact -> "/_create member contact #$groupId $groupMemberId" is APISendMemberContactInvitation -> "/_invite member contact @$contactId ${mc.cmdString}" - is APIGetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()}" - is APISetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()} ${protoServersStr(servers)}" is APITestProtoServer -> "/_server test $userId $server" + is ApiGetServerOperators -> "/_operators" + is ApiSetServerOperators -> "/_operators ${json.encodeToString(operators)}" + is ApiGetUserServers -> "/_servers $userId" + is ApiSetUserServers -> "/_servers $userId ${json.encodeToString(userServers)}" + is ApiValidateServers -> "/_validate_servers $userId ${json.encodeToString(userServers)}" + is ApiGetUsageConditions -> "/_conditions" + is ApiSetConditionsNotified -> "/_conditions_notified ${conditionsId}" + is ApiAcceptConditions -> "/_accept_conditions ${conditionsId} ${operatorIds.joinToString(",")}" is APISetChatItemTTL -> "/_ttl $userId ${chatItemTTLStr(seconds)}" is APIGetChatItemTTL -> "/_ttl $userId" + is APISetChatTTL -> "/_ttl $userId ${chatRef(chatType, id)} ${chatItemTTLStr(seconds)}" is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}" is APIGetNetworkConfig -> "/network" + is APISetNetworkInfo -> "/_network info ${json.encodeToString(networkInfo)}" + is ReconnectServer -> "/reconnect $userId $server" + is ReconnectAllServers -> "/reconnect" is APISetChatSettings -> "/_settings ${chatRef(type, id)} ${json.encodeToString(chatSettings)}" is ApiSetMemberSettings -> "/_member settings #$groupId $groupMemberId ${json.encodeToString(memberSettings)}" is APIContactInfo -> "/_info @$contactId" is APIGroupMemberInfo -> "/_info #$groupId $groupMemberId" + is APIContactQueueInfo -> "/_queue info @$contactId" + is APIGroupMemberQueueInfo -> "/_queue info #$groupId $groupMemberId" is APISwitchContact -> "/_switch @$contactId" is APISwitchGroupMember -> "/_switch #$groupId $groupMemberId" is APIAbortSwitchContact -> "/_abort switch @$contactId" @@ -2345,29 +3548,30 @@ sealed class CC { is APIGetGroupMemberCode -> "/_get code #$groupId $groupMemberId" is APIVerifyContact -> "/_verify code @$contactId" + if (connectionCode != null) " $connectionCode" else "" is APIVerifyGroupMember -> "/_verify code #$groupId $groupMemberId" + if (connectionCode != null) " $connectionCode" else "" - is APIAddContact -> "/_connect $userId incognito=${onOff(incognito)}" + is APIAddContact -> "/_connect $userId short=${onOff(short)} incognito=${onOff(incognito)}" is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}" - is APIConnectPlan -> "/_connect plan $userId $connReq" - is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq" + is ApiChangeConnectionUser -> "/_set conn user :$connId $userId" + is APIConnectPlan -> "/_connect plan $userId $connLink" + is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} ${connLink.connFullLink} ${connLink.connShortLink ?: ""}" is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId" - is ApiDeleteChat -> if (notify != null) { - "/_delete ${chatRef(type, id)} notify=${onOff(notify)}" - } else { - "/_delete ${chatRef(type, id)}" - } + is ApiDeleteChat -> "/_delete ${chatRef(type, id)} ${chatDeleteMode.cmdString}" is ApiClearChat -> "/_clear chat ${chatRef(type, id)}" is ApiListContacts -> "/_contacts $userId" is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}" is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}" is ApiSetContactAlias -> "/_set alias @$contactId ${localAlias.trim()}" + is ApiSetGroupAlias -> "/_set alias #$groupId ${localAlias.trim()}" is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}" - is ApiCreateMyAddress -> "/_address $userId" + is ApiSetUserUIThemes -> "/_set theme user $userId ${if (themes != null) json.encodeToString(themes) else ""}" + is ApiSetChatUIThemes -> "/_set theme $chatId ${if (themes != null) json.encodeToString(themes) else ""}" + is ApiCreateMyAddress -> "/_address $userId short=${onOff(short)}" is ApiDeleteMyAddress -> "/_delete_address $userId" is ApiShowMyAddress -> "/_show_address $userId" is ApiSetProfileAddress -> "/_profile_address $userId ${onOff(on)}" is ApiAddressAutoAccept -> "/_auto_accept $userId ${AutoAccept.cmdString(autoAccept)}" is ApiAcceptContact -> "/_accept incognito=${onOff(incognito)} $contactReqId" is ApiRejectContact -> "/_reject $contactReqId" + is ApiGetCallInvitations -> "/_call get" is ApiSendCallInvitation -> "/_call invite @${contact.apiId} ${json.encodeToString(callType)}" is ApiRejectCall -> "/_call reject @${contact.apiId}" is ApiSendCallOffer -> "/_call offer @${contact.apiId} ${json.encodeToString(callOffer)}" @@ -2376,16 +3580,18 @@ sealed class CC { is ApiEndCall -> "/_call end @${contact.apiId}" is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}" is ApiGetNetworkStatuses -> "/_network_statuses" - is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}" + is ApiChatRead -> "/_read chat ${chatRef(type, id)}" + is ApiChatItemsRead -> "/_read chat items ${chatRef(type, id)} ${itemIds.joinToString(",")}" is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}" is ReceiveFile -> "/freceive $fileId" + - (if (encrypt == null) "" else " encrypt=${onOff(encrypt)}") + + " approved_relays=${onOff(userApprovedRelays)}" + + " encrypt=${onOff(encrypt)}" + (if (inline == null) "" else " inline=${onOff(inline)}") is CancelFile -> "/fcancel $fileId" is SetLocalDeviceName -> "/set device name $displayName" is ListRemoteHosts -> "/list remote hosts" - is StartRemoteHost -> "/start remote host " + (if (remoteHostId == null) "new" else "$remoteHostId multicast=${onOff(multicast)}") + (if (address != null) " addr=${address.address} iface=${address.`interface`}" else "") + (if (port != null) " port=$port" else "") + is StartRemoteHost -> "/start remote host " + (if (remoteHostId == null) "new" else "$remoteHostId multicast=${onOff(multicast)}") + (if (address != null) " addr=${address.address} iface=${json.encodeToString(address.`interface`)}" else "") + (if (port != null) " port=$port" else "") is SwitchRemoteHost -> "/switch remote host " + if (remoteHostId == null) "local" else "$remoteHostId" is StopRemoteHost -> "/stop remote host " + if (remoteHostKey == null) "new" else "$remoteHostKey" is DeleteRemoteHost -> "/delete remote host $remoteHostId" @@ -2401,7 +3607,13 @@ sealed class CC { is ListRemoteCtrls -> "/list remote ctrls" is StopRemoteCtrl -> "/stop remote ctrl" is DeleteRemoteCtrl -> "/delete remote ctrl $remoteCtrlId" + is ApiUploadStandaloneFile -> "/_upload $userId ${file.filePath}" + is ApiDownloadStandaloneFile -> "/_download $userId $url ${file.filePath}" + is ApiStandaloneFileInfo -> "/_download info $url" is ShowVersion -> "/version" + is ResetAgentServersStats -> "/reset servers stats" + is GetAgentSubsTotal -> "/get subs total $userId" + is GetAgentServersSummary -> "/get servers summary $userId" } val cmdType: String get() = when (this) { @@ -2419,29 +3631,44 @@ sealed class CC { is ApiUnmuteUser -> "apiUnmuteUser" is ApiDeleteUser -> "apiDeleteUser" is StartChat -> "startChat" + is CheckChatRunning -> "checkChatRunning" is ApiStopChat -> "apiStopChat" - is SetTempFolder -> "setTempFolder" - is SetFilesFolder -> "setFilesFolder" - is SetRemoteHostsFolder -> "setRemoteHostsFolder" - is ApiSetXFTPConfig -> "apiSetXFTPConfig" + is ApiSetAppFilePaths -> "apiSetAppFilePaths" is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles" is ApiExportArchive -> "apiExportArchive" is ApiImportArchive -> "apiImportArchive" is ApiDeleteStorage -> "apiDeleteStorage" is ApiStorageEncryption -> "apiStorageEncryption" + is TestStorageEncryption -> "testStorageEncryption" + is ApiSaveSettings -> "apiSaveSettings" + is ApiGetSettings -> "apiGetSettings" + is ApiGetChatTags -> "apiGetChatTags" is ApiGetChats -> "apiGetChats" is ApiGetChat -> "apiGetChat" is ApiGetChatItemInfo -> "apiGetChatItemInfo" - is ApiSendMessage -> "apiSendMessage" + is ApiSendMessages -> "apiSendMessages" + is ApiCreateChatTag -> "apiCreateChatTag" + is ApiSetChatTags -> "apiSetChatTags" + is ApiDeleteChatTag -> "apiDeleteChatTag" + is ApiUpdateChatTag -> "apiUpdateChatTag" + is ApiReorderChatTags -> "apiReorderChatTags" + is ApiCreateChatItems -> "apiCreateChatItems" + is ApiReportMessage -> "apiReportMessage" is ApiUpdateChatItem -> "apiUpdateChatItem" is ApiDeleteChatItem -> "apiDeleteChatItem" is ApiDeleteMemberChatItem -> "apiDeleteMemberChatItem" + is ApiArchiveReceivedReports -> "apiArchiveReceivedReports" + is ApiDeleteReceivedReports -> "apiDeleteReceivedReports" is ApiChatItemReaction -> "apiChatItemReaction" + is ApiGetReactionMembers -> "apiGetReactionMembers" + is ApiForwardChatItems -> "apiForwardChatItems" + is ApiPlanForwardChatItems -> "apiPlanForwardChatItems" is ApiNewGroup -> "apiNewGroup" is ApiAddMember -> "apiAddMember" is ApiJoinGroup -> "apiJoinGroup" - is ApiMemberRole -> "apiMemberRole" - is ApiRemoveMember -> "apiRemoveMember" + is ApiMembersRole -> "apiMembersRole" + is ApiBlockMembersForAll -> "apiBlockMembersForAll" + is ApiRemoveMembers -> "apiRemoveMembers" is ApiLeaveGroup -> "apiLeaveGroup" is ApiListMembers -> "apiListMembers" is ApiUpdateGroupProfile -> "apiUpdateGroupProfile" @@ -2451,17 +3678,29 @@ sealed class CC { is APIGetGroupLink -> "apiGetGroupLink" is APICreateMemberContact -> "apiCreateMemberContact" is APISendMemberContactInvitation -> "apiSendMemberContactInvitation" - is APIGetUserProtoServers -> "apiGetUserProtoServers" - is APISetUserProtoServers -> "apiSetUserProtoServers" is APITestProtoServer -> "testProtoServer" + is ApiGetServerOperators -> "apiGetServerOperators" + is ApiSetServerOperators -> "apiSetServerOperators" + is ApiGetUserServers -> "apiGetUserServers" + is ApiSetUserServers -> "apiSetUserServers" + is ApiValidateServers -> "apiValidateServers" + is ApiGetUsageConditions -> "apiGetUsageConditions" + is ApiSetConditionsNotified -> "apiSetConditionsNotified" + is ApiAcceptConditions -> "apiAcceptConditions" is APISetChatItemTTL -> "apiSetChatItemTTL" is APIGetChatItemTTL -> "apiGetChatItemTTL" + is APISetChatTTL -> "apiSetChatTTL" is APISetNetworkConfig -> "apiSetNetworkConfig" is APIGetNetworkConfig -> "apiGetNetworkConfig" + is APISetNetworkInfo -> "apiSetNetworkInfo" + is ReconnectServer -> "reconnectServer" + is ReconnectAllServers -> "reconnectAllServers" is APISetChatSettings -> "apiSetChatSettings" is ApiSetMemberSettings -> "apiSetMemberSettings" is APIContactInfo -> "apiContactInfo" is APIGroupMemberInfo -> "apiGroupMemberInfo" + is APIContactQueueInfo -> "apiContactQueueInfo" + is APIGroupMemberQueueInfo -> "apiGroupMemberQueueInfo" is APISwitchContact -> "apiSwitchContact" is APISwitchGroupMember -> "apiSwitchGroupMember" is APIAbortSwitchContact -> "apiAbortSwitchContact" @@ -2474,6 +3713,7 @@ sealed class CC { is APIVerifyGroupMember -> "apiVerifyGroupMember" is APIAddContact -> "apiAddContact" is ApiSetConnectionIncognito -> "apiSetConnectionIncognito" + is ApiChangeConnectionUser -> "apiChangeConnectionUser" is APIConnectPlan -> "apiConnectPlan" is APIConnect -> "apiConnect" is ApiConnectContactViaAddress -> "apiConnectContactViaAddress" @@ -2483,7 +3723,10 @@ sealed class CC { is ApiUpdateProfile -> "apiUpdateProfile" is ApiSetContactPrefs -> "apiSetContactPrefs" is ApiSetContactAlias -> "apiSetContactAlias" + is ApiSetGroupAlias -> "apiSetGroupAlias" is ApiSetConnectionAlias -> "apiSetConnectionAlias" + is ApiSetUserUIThemes -> "apiSetUserUIThemes" + is ApiSetChatUIThemes -> "apiSetChatUIThemes" is ApiCreateMyAddress -> "apiCreateMyAddress" is ApiDeleteMyAddress -> "apiDeleteMyAddress" is ApiShowMyAddress -> "apiShowMyAddress" @@ -2491,6 +3734,7 @@ sealed class CC { is ApiAddressAutoAccept -> "apiAddressAutoAccept" is ApiAcceptContact -> "apiAcceptContact" is ApiRejectContact -> "apiRejectContact" + is ApiGetCallInvitations -> "apiGetCallInvitations" is ApiSendCallInvitation -> "apiSendCallInvitation" is ApiRejectCall -> "apiRejectCall" is ApiSendCallOffer -> "apiSendCallOffer" @@ -2500,6 +3744,7 @@ sealed class CC { is ApiCallStatus -> "apiCallStatus" is ApiGetNetworkStatuses -> "apiGetNetworkStatuses" is ApiChatRead -> "apiChatRead" + is ApiChatItemsRead -> "apiChatItemsRead" is ApiChatUnread -> "apiChatUnread" is ReceiveFile -> "receiveFile" is CancelFile -> "cancelFile" @@ -2518,13 +3763,19 @@ sealed class CC { is ListRemoteCtrls -> "listRemoteCtrls" is StopRemoteCtrl -> "stopRemoteCtrl" is DeleteRemoteCtrl -> "deleteRemoteCtrl" + is ApiUploadStandaloneFile -> "apiUploadStandaloneFile" + is ApiDownloadStandaloneFile -> "apiDownloadStandaloneFile" + is ApiStandaloneFileInfo -> "apiStandaloneFileInfo" is ShowVersion -> "showVersion" + is ResetAgentServersStats -> "resetAgentServersStats" + is GetAgentSubsTotal -> "getAgentSubsTotal" + is GetAgentServersSummary -> "getAgentServersSummary" } - class ItemRange(val from: Long, val to: Long) + data class ItemRange(val from: Long, val to: Long) fun chatItemTTLStr(seconds: Long?): String { - if (seconds == null) return "none" + if (seconds == null) return "default" return seconds.toString() } @@ -2535,6 +3786,7 @@ sealed class CC { is ApiHideUser -> ApiHideUser(userId, obfuscate(viewPwd)) is ApiUnhideUser -> ApiUnhideUser(userId, obfuscate(viewPwd)) is ApiDeleteUser -> ApiDeleteUser(userId, delSMPQueues, obfuscateOrNull(viewPwd)) + is TestStorageEncryption -> TestStorageEncryption(obfuscate(key)) else -> this } @@ -2547,21 +3799,18 @@ sealed class CC { null } - private fun onOff(b: Boolean): String = if (b) "on" else "off" - private fun maybePwd(pwd: String?): String = if (pwd == "" || pwd == null) "" else " " + json.encodeToString(pwd) companion object { fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}" - - fun protoServersStr(servers: List<ServerCfg>) = json.encodeToString(ProtoServersConfig(servers)) } } +fun onOff(b: Boolean): String = if (b) "on" else "off" + @Serializable data class NewUser( val profile: Profile?, - val sameServers: Boolean, val pastTimestamp: Boolean ) @@ -2569,25 +3818,35 @@ sealed class ChatPagination { class Last(val count: Int): ChatPagination() class After(val chatItemId: Long, val count: Int): ChatPagination() class Before(val chatItemId: Long, val count: Int): ChatPagination() + class Around(val chatItemId: Long, val count: Int): ChatPagination() + class Initial(val count: Int): ChatPagination() val cmdString: String get() = when (this) { is Last -> "count=${this.count}" is After -> "after=${this.chatItemId} count=${this.count}" is Before -> "before=${this.chatItemId} count=${this.count}" + is Around -> "around=${this.chatItemId} count=${this.count}" + is Initial -> "initial=${this.count}" } companion object { - const val INITIAL_COUNT = 100 + val INITIAL_COUNT = if (appPlatform.isDesktop) 100 else 75 const val PRELOAD_COUNT = 100 const val UNTIL_PRELOAD_COUNT = 50 } } @Serializable -class ComposedMessage(val fileSource: CryptoFile?, val quotedItemId: Long?, val msgContent: MsgContent) +class ComposedMessage(val fileSource: CryptoFile?, val quotedItemId: Long?, val msgContent: MsgContent, val mentions: Map<String, Long>) @Serializable -class XFTPFileConfig(val minFileSize: Long) +class UpdatedMessage(val msgContent: MsgContent, val mentions: Map<String, Long>) { + val cmdString: String get() = + if (msgContent is MCUnknown) "json $json" else "json ${json.encodeToString(this)}" +} + +@Serializable +class ChatTagData(val emoji: String?, val text: String) @Serializable class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? = null, val parentTempDirectory: String? = null) @@ -2602,24 +3861,311 @@ enum class ServerProtocol { } @Serializable -data class ProtoServersConfig( - val servers: List<ServerCfg> +enum class OperatorTag { + @SerialName("simplex") SimpleX, + @SerialName("flux") Flux +} + +data class ServerOperatorInfo( + val description: List<String>, + val website: String, + val selfhost: Pair<String, String>? = null, + val logo: ImageResource, + val largeLogo: ImageResource, + val logoDarkMode: ImageResource, + val largeLogoDarkMode: ImageResource +) +val operatorsInfo: Map<OperatorTag, ServerOperatorInfo> = mapOf( + OperatorTag.SimpleX to ServerOperatorInfo( + description = listOf( + "SimpleX Chat is the first communication network that has no user profile IDs of any kind, not even random numbers or keys that identify the users.", + "SimpleX Chat Ltd develops the communication software for SimpleX network." + ), + website = "https://simplex.chat", + logo = MR.images.decentralized, + largeLogo = MR.images.logo, + logoDarkMode = MR.images.decentralized_light, + largeLogoDarkMode = MR.images.logo_light + ), + OperatorTag.Flux to ServerOperatorInfo( + description = listOf( + "Flux is the largest decentralized cloud, based on a global network of user-operated nodes.", + "Flux offers a powerful, scalable, and affordable cutting edge technology platform for all.", + "Flux operates servers in SimpleX network to improve its privacy and decentralization." + ), + website = "https://runonflux.com", + selfhost = "Self-host SimpleX servers on Flux" to "https://home.runonflux.io/apps/marketplace?q=simplex", + logo = MR.images.flux_logo_symbol, + largeLogo = MR.images.flux_logo, + logoDarkMode = MR.images.flux_logo_symbol, + largeLogoDarkMode = MR.images.flux_logo_light + ), ) @Serializable -data class UserProtocolServers( - val serverProtocol: ServerProtocol, - val protoServers: List<ServerCfg>, - val presetServers: List<String>, +data class UsageConditionsDetail( + val conditionsId: Long, + val conditionsCommit: String, + val notifiedAt: Instant?, + val createdAt: Instant +) { + companion object { + val sampleData = UsageConditionsDetail( + conditionsId = 1, + conditionsCommit = "11a44dc1fd461a93079f897048b46998db55da5c", + notifiedAt = null, + createdAt = Clock.System.now() + ) + } +} + +@Serializable +sealed class UsageConditionsAction { + @Serializable @SerialName("review") data class Review(val operators: List<ServerOperator>, val deadline: Instant?, val showNotice: Boolean) : UsageConditionsAction() + @Serializable @SerialName("accepted") data class Accepted(val operators: List<ServerOperator>) : UsageConditionsAction() + + val shouldShowNotice: Boolean + get() = when (this) { + is Review -> showNotice + else -> false + } +} + +@Serializable +data class ServerOperatorConditionsDetail( + val serverOperators: List<ServerOperator>, + val currentConditions: UsageConditionsDetail, + val conditionsAction: UsageConditionsAction? +) { + companion object { + val empty = ServerOperatorConditionsDetail( + serverOperators = emptyList(), + currentConditions = UsageConditionsDetail(conditionsId = 0, conditionsCommit = "empty", notifiedAt = null, createdAt = Clock.System.now()), + conditionsAction = null + ) + } +} + +@Serializable() +sealed class ConditionsAcceptance { + @Serializable @SerialName("accepted") data class Accepted(val acceptedAt: Instant?, val autoAccepted: Boolean) : ConditionsAcceptance() + @Serializable @SerialName("required") data class Required(val deadline: Instant?) : ConditionsAcceptance() + + val conditionsAccepted: Boolean + get() = when (this) { + is Accepted -> true + is Required -> false + } + + val usageAllowed: Boolean + get() = when (this) { + is Accepted -> true + is Required -> this.deadline != null + } +} + +@Serializable +data class ServerOperator( + val operatorId: Long, + val operatorTag: OperatorTag?, + val tradeName: String, + val legalName: String?, + val serverDomains: List<String>, + val conditionsAcceptance: ConditionsAcceptance, + val enabled: Boolean, + val smpRoles: ServerRoles, + val xftpRoles: ServerRoles, +) { + companion object { + val dummyOperatorInfo = ServerOperatorInfo( + description = listOf("Default"), + website = "https://simplex.chat", + logo = MR.images.decentralized, + largeLogo = MR.images.logo, + logoDarkMode = MR.images.decentralized_light, + largeLogoDarkMode = MR.images.logo_light + ) + + val sampleData1 = ServerOperator( + operatorId = 1, + operatorTag = OperatorTag.SimpleX, + tradeName = "SimpleX Chat", + legalName = "SimpleX Chat Ltd", + serverDomains = listOf("simplex.im"), + conditionsAcceptance = ConditionsAcceptance.Accepted(acceptedAt = null, autoAccepted = false), + enabled = true, + smpRoles = ServerRoles(storage = true, proxy = true), + xftpRoles = ServerRoles(storage = true, proxy = true) + ) + } + + val id: Long + get() = operatorId + + override fun equals(other: Any?): Boolean { + if (other !is ServerOperator) return false + return other.operatorId == this.operatorId && + other.operatorTag == this.operatorTag && + other.tradeName == this.tradeName && + other.legalName == this.legalName && + other.serverDomains == this.serverDomains && + other.conditionsAcceptance == this.conditionsAcceptance && + other.enabled == this.enabled && + other.smpRoles == this.smpRoles && + other.xftpRoles == this.xftpRoles + } + + override fun hashCode(): Int { + var result = operatorId.hashCode() + result = 31 * result + (operatorTag?.hashCode() ?: 0) + result = 31 * result + tradeName.hashCode() + result = 31 * result + (legalName?.hashCode() ?: 0) + result = 31 * result + serverDomains.hashCode() + result = 31 * result + conditionsAcceptance.hashCode() + result = 31 * result + enabled.hashCode() + result = 31 * result + smpRoles.hashCode() + result = 31 * result + xftpRoles.hashCode() + return result + } + + val legalName_: String + get() = legalName ?: tradeName + + val info: ServerOperatorInfo get() { + return if (this.operatorTag != null) { + operatorsInfo[this.operatorTag] ?: dummyOperatorInfo + } else { + dummyOperatorInfo + } + } + + val logo: ImageResource + @Composable + get() { + return if (isInDarkTheme()) info.logoDarkMode else info.logo + } + + val largeLogo: ImageResource + @Composable + get() { + return if (isInDarkTheme()) info.largeLogoDarkMode else info.largeLogo + } +} + +@Serializable +data class ServerRoles( + val storage: Boolean, + val proxy: Boolean ) @Serializable -data class ServerCfg( +data class UserOperatorServers( + val operator: ServerOperator?, + val smpServers: List<UserServer>, + val xftpServers: List<UserServer> +) { + val id: String + get() = operator?.operatorId?.toString() ?: "nil operator" + + val operator_: ServerOperator + get() = operator ?: ServerOperator( + operatorId = 0, + operatorTag = null, + tradeName = "", + legalName = null, + serverDomains = emptyList(), + conditionsAcceptance = ConditionsAcceptance.Accepted(null, autoAccepted = false), + enabled = false, + smpRoles = ServerRoles(storage = true, proxy = true), + xftpRoles = ServerRoles(storage = true, proxy = true) + ) + + companion object { + val sampleData1 = UserOperatorServers( + operator = ServerOperator.sampleData1, + smpServers = listOf(UserServer.sampleData.preset), + xftpServers = listOf(UserServer.sampleData.xftpPreset) + ) + + val sampleDataNilOperator = UserOperatorServers( + operator = null, + smpServers = listOf(UserServer.sampleData.preset), + xftpServers = listOf(UserServer.sampleData.xftpPreset) + ) + } +} + +@Serializable +sealed class UserServersError { + @Serializable @SerialName("noServers") data class NoServers(val protocol: ServerProtocol, val user: UserRef?): UserServersError() + @Serializable @SerialName("storageMissing") data class StorageMissing(val protocol: ServerProtocol, val user: UserRef?): UserServersError() + @Serializable @SerialName("proxyMissing") data class ProxyMissing(val protocol: ServerProtocol, val user: UserRef?): UserServersError() + @Serializable @SerialName("duplicateServer") data class DuplicateServer(val protocol: ServerProtocol, val duplicateServer: String, val duplicateHost: String): UserServersError() + + val globalError: String? + get() = when (this.protocol_) { + ServerProtocol.SMP -> globalSMPError + ServerProtocol.XFTP -> globalXFTPError + } + + private val protocol_: ServerProtocol + get() = when (this) { + is NoServers -> this.protocol + is StorageMissing -> this.protocol + is ProxyMissing -> this.protocol + is DuplicateServer -> this.protocol + } + + val globalSMPError: String? + get() = if (this.protocol_ == ServerProtocol.SMP) { + when (this) { + is NoServers -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_message_servers_configured)}" } + ?: generalGetString(MR.strings.no_message_servers_configured) + + is StorageMissing -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_message_servers_configured_for_receiving)}" } + ?: generalGetString(MR.strings.no_message_servers_configured_for_receiving) + + is ProxyMissing -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_message_servers_configured_for_private_routing)}" } + ?: generalGetString(MR.strings.no_message_servers_configured_for_private_routing) + + else -> null + } + } else { + null + } + + val globalXFTPError: String? + get() = if (this.protocol_ == ServerProtocol.XFTP) { + when (this) { + is NoServers -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_media_servers_configured)}" } + ?: generalGetString(MR.strings.no_media_servers_configured) + + is StorageMissing -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_media_servers_configured_for_sending)}" } + ?: generalGetString(MR.strings.no_media_servers_configured_for_sending) + + is ProxyMissing -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_media_servers_configured_for_private_routing)}" } + ?: generalGetString(MR.strings.no_media_servers_configured_for_private_routing) + + else -> null + } + } else { + null + } + + private fun userStr(user: UserRef): String { + return String.format(generalGetString(MR.strings.for_chat_profile), user.localDisplayName) + } +} + +@Serializable +data class UserServer( val remoteHostId: Long?, + val serverId: Long?, val server: String, val preset: Boolean, val tested: Boolean? = null, - val enabled: Boolean + val enabled: Boolean, + val deleted: Boolean ) { @Transient private val createdAt: Date = Date() @@ -2633,35 +4179,51 @@ data class ServerCfg( get() = server.isBlank() companion object { - val empty = ServerCfg(remoteHostId = null, server = "", preset = false, tested = null, enabled = true) + val empty = UserServer(remoteHostId = null, serverId = null, server = "", preset = false, tested = null, enabled = false, deleted = false) class SampleData( - val preset: ServerCfg, - val custom: ServerCfg, - val untested: ServerCfg + val preset: UserServer, + val custom: UserServer, + val untested: UserServer, + val xftpPreset: UserServer ) val sampleData = SampleData( - preset = ServerCfg( + preset = UserServer( remoteHostId = null, + serverId = 1, server = "smp://abcd@smp8.simplex.im", preset = true, tested = true, - enabled = true + enabled = true, + deleted = false ), - custom = ServerCfg( + custom = UserServer( remoteHostId = null, + serverId = 2, server = "smp://abcd@smp9.simplex.im", preset = false, tested = false, - enabled = false + enabled = false, + deleted = false ), - untested = ServerCfg( + untested = UserServer( remoteHostId = null, + serverId = 3, server = "smp://abcd@smp10.simplex.im", preset = false, tested = null, - enabled = true + enabled = true, + deleted = false + ), + xftpPreset = UserServer( + remoteHostId = null, + serverId = 4, + server = "xftp://abcd@xftp8.simplex.im", + preset = true, + tested = true, + enabled = true, + deleted = false ) ) } @@ -2771,56 +4333,48 @@ data class ParsedServerAddress ( @Serializable data class NetCfg( val socksProxy: String?, - val hostMode: HostMode, - val requiredHostMode: Boolean, - val sessionMode: TransportSessionMode, + val socksMode: SocksMode = SocksMode.default, + val hostMode: HostMode = HostMode.default, + val requiredHostMode: Boolean = false, + val sessionMode: TransportSessionMode = TransportSessionMode.default, + val smpProxyMode: SMPProxyMode = SMPProxyMode.default, + val smpProxyFallback: SMPProxyFallback = SMPProxyFallback.default, + val smpWebPortServers: SMPWebPortServers = SMPWebPortServers.default, val tcpConnectTimeout: Long, // microseconds val tcpTimeout: Long, // microseconds val tcpTimeoutPerKb: Long, // microseconds - val tcpKeepAlive: KeepAliveOpts?, + val rcvConcurrency: Int, // pool size + val tcpKeepAlive: KeepAliveOpts? = KeepAliveOpts.defaults, val smpPingInterval: Long, // microseconds - val smpPingCount: Int, - val logTLSErrors: Boolean = false + val smpPingCount: Int = 3, + val logTLSErrors: Boolean = false, ) { val useSocksProxy: Boolean get() = socksProxy != null val enableKeepAlive: Boolean get() = tcpKeepAlive != null - fun withHostPort(hostPort: String?, default: String? = ":9050"): NetCfg { - val socksProxy = if (hostPort?.startsWith("localhost:") == true) { - hostPort.removePrefix("localhost") - } else { - hostPort ?: default - } - return copy(socksProxy = socksProxy) + fun withProxy(proxy: NetworkProxy?, default: String? = ":9050"): NetCfg { + return copy(socksProxy = proxy?.toProxyString() ?: default) } companion object { val defaults: NetCfg = NetCfg( socksProxy = null, - hostMode = HostMode.OnionViaSocks, - requiredHostMode = false, - sessionMode = TransportSessionMode.User, - tcpConnectTimeout = 20_000_000, + tcpConnectTimeout = 25_000_000, tcpTimeout = 15_000_000, - tcpTimeoutPerKb = 45_000, - tcpKeepAlive = KeepAliveOpts.defaults, - smpPingInterval = 1200_000_000, - smpPingCount = 3 + tcpTimeoutPerKb = 10_000, + rcvConcurrency = 12, + smpPingInterval = 1200_000_000 ) val proxyDefaults: NetCfg = NetCfg( socksProxy = ":9050", - hostMode = HostMode.OnionViaSocks, - requiredHostMode = false, - sessionMode = TransportSessionMode.User, - tcpConnectTimeout = 30_000_000, + tcpConnectTimeout = 35_000_000, tcpTimeout = 20_000_000, - tcpTimeoutPerKb = 60_000, - tcpKeepAlive = KeepAliveOpts.defaults, - smpPingInterval = 1200_000_000, - smpPingCount = 3 + tcpTimeoutPerKb = 15_000, + rcvConcurrency = 8, + smpPingInterval = 1200_000_000 ) } @@ -2841,6 +4395,39 @@ data class NetCfg( } } +@Serializable +data class NetworkProxy( + val username: String = "", + val password: String = "", + val auth: NetworkProxyAuth = NetworkProxyAuth.ISOLATE, + val host: String = "localhost", + val port: Int = 9050 +) { + fun toProxyString(): String { + var res = "" + if (auth == NetworkProxyAuth.USERNAME && (username.isNotBlank() || password.isNotBlank())) { + res += username.trim() + ":" + password.trim() + "@" + } else if (auth == NetworkProxyAuth.USERNAME) { + res += "@" + } + if (host != "localhost") { + res += if (host.contains(':')) "[${host.trim(' ', '[', ']')}]" else host.trim() + } + if (port != 9050 || res.isEmpty()) { + res += ":$port" + } + return res + } +} + +@Serializable +enum class NetworkProxyAuth { + @SerialName("isolate") + ISOLATE, + @SerialName("username") + USERNAME, +} + enum class OnionHosts { NEVER, PREFER, REQUIRED } @@ -2850,15 +4437,72 @@ enum class HostMode { @SerialName("onionViaSocks") OnionViaSocks, @SerialName("onion") Onion, @SerialName("public") Public; + + companion object { + val default = OnionViaSocks + } +} + +@Serializable +enum class SocksMode { + @SerialName("always") Always, + @SerialName("onion") Onion; + + companion object { + val default = Always + } +} + +@Serializable +enum class SMPProxyMode { + @SerialName("always") Always, + @SerialName("unknown") Unknown, + @SerialName("unprotected") Unprotected, + @SerialName("never") Never; + + companion object { + val default = Always + } +} + +@Serializable +enum class SMPProxyFallback { + @SerialName("allow") Allow, + @SerialName("allowProtected") AllowProtected, + @SerialName("prohibit") Prohibit; + + companion object { + val default = AllowProtected + } +} + +@Serializable +enum class SMPWebPortServers { + @SerialName("all") All, + @SerialName("preset") Preset, + @SerialName("off") Off; + + val text get(): StringResource = when (this) { + All -> MR.strings.network_smp_web_port_all + Preset -> MR.strings.network_smp_web_port_preset + Off -> MR.strings.network_smp_web_port_off + } + + companion object { + val default = Preset + } } @Serializable enum class TransportSessionMode { @SerialName("user") User, + @SerialName("session") Session, + @SerialName("server") Server, @SerialName("entity") Entity; companion object { - val default = User + val default = Session + val safeValues = arrayOf(User, Session, Server) } } @@ -2889,7 +4533,37 @@ data class ChatSettings( enum class MsgFilter { @SerialName("all") All, @SerialName("none") None, - @SerialName("mentions") Mentions, + @SerialName("mentions") Mentions; + + fun nextMode(mentions: Boolean): MsgFilter { + return when (this) { + All -> if (mentions) Mentions else None + Mentions -> None + None -> All + } + } + + fun text(mentions: Boolean): StringResource { + return when (this) { + All -> MR.strings.unmute_chat + Mentions -> MR.strings.mute_chat + None -> if (mentions) MR.strings.mute_all_chat else MR.strings.mute_chat + } + } + + val icon: ImageResource + get() = when (this) { + All -> MR.images.ic_notifications + Mentions -> MR.images.ic_notification_important + None -> MR.images.ic_notifications_off + } + + val iconFilled: ImageResource + get() = when (this) { + All -> MR.images.ic_notifications + Mentions -> MR.images.ic_notification_important_filled + None -> MR.images.ic_notifications_off_filled + } } @Serializable @@ -2970,6 +4644,157 @@ data class TimedMessagesPreference( } } +@Serializable +data class PresentedServersSummary( + val statsStartedAt: Instant, + val allUsersSMP: SMPServersSummary, + val allUsersXFTP: XFTPServersSummary, + val currentUserSMP: SMPServersSummary, + val currentUserXFTP: XFTPServersSummary +) + +@Serializable +data class SMPServersSummary( + val smpTotals: SMPTotals, + val currentlyUsedSMPServers: List<SMPServerSummary>, + val previouslyUsedSMPServers: List<SMPServerSummary>, + val onlyProxiedSMPServers: List<SMPServerSummary> +) + +@Serializable +data class SMPTotals( + val sessions: ServerSessions, + val subs: SMPServerSubs, + val stats: AgentSMPServerStatsData +) + +@Serializable +data class SMPServerSummary( + val smpServer: String, + val known: Boolean? = null, + val sessions: ServerSessions? = null, + val subs: SMPServerSubs? = null, + val stats: AgentSMPServerStatsData? = null +) { + val hasSubs: Boolean + get() = subs != null + + val sessionsOrNew: ServerSessions + get() = sessions ?: ServerSessions.newServerSessions + + val subsOrNew: SMPServerSubs + get() = subs ?: SMPServerSubs.newSMPServerSubs +} + +@Serializable +data class ServerSessions( + val ssConnected: Int, + val ssErrors: Int, + val ssConnecting: Int +) { + companion object { + val newServerSessions = ServerSessions( + ssConnected = 0, + ssErrors = 0, + ssConnecting = 0 + ) + } + + val hasSess: Boolean + get() = ssConnected > 0 +} + +@Serializable +data class SMPServerSubs( + val ssActive: Int, + val ssPending: Int +) { + companion object { + val newSMPServerSubs = SMPServerSubs( + ssActive = 0, + ssPending = 0 + ) + } + + val total: Int + get() = ssActive + ssPending + + val shareOfActive: Float + get() = if (total != 0) ssActive.toFloat() / total else 0f +} + +@Serializable +data class AgentSMPServerStatsData( + val _sentDirect: Int, + val _sentViaProxy: Int, + val _sentProxied: Int, + val _sentDirectAttempts: Int, + val _sentViaProxyAttempts: Int, + val _sentProxiedAttempts: Int, + val _sentAuthErrs: Int, + val _sentQuotaErrs: Int, + val _sentExpiredErrs: Int, + val _sentOtherErrs: Int, + val _recvMsgs: Int, + val _recvDuplicates: Int, + val _recvCryptoErrs: Int, + val _recvErrs: Int, + val _ackMsgs: Int, + val _ackAttempts: Int, + val _ackNoMsgErrs: Int, + val _ackOtherErrs: Int, + val _connCreated: Int, + val _connSecured: Int, + val _connCompleted: Int, + val _connDeleted: Int, + val _connDelAttempts: Int, + val _connDelErrs: Int, + val _connSubscribed: Int, + val _connSubAttempts: Int, + val _connSubIgnored: Int, + val _connSubErrs: Int +) + +@Serializable +data class XFTPServersSummary( + val xftpTotals: XFTPTotals, + val currentlyUsedXFTPServers: List<XFTPServerSummary>, + val previouslyUsedXFTPServers: List<XFTPServerSummary> +) + +@Serializable +data class XFTPTotals( + val sessions: ServerSessions, + val stats: AgentXFTPServerStatsData +) + +@Serializable +data class XFTPServerSummary( + val xftpServer: String, + val known: Boolean? = null, + val sessions: ServerSessions? = null, + val stats: AgentXFTPServerStatsData? = null, + val rcvInProgress: Boolean, + val sndInProgress: Boolean, + val delInProgress: Boolean +) {} + +@Serializable +data class AgentXFTPServerStatsData( + val _uploads: Int, + val _uploadsSize: Long, + val _uploadAttempts: Int, + val _uploadErrs: Int, + val _downloads: Int, + val _downloadsSize: Long, + val _downloadAttempts: Int, + val _downloadAuthErrs: Int, + val _downloadErrs: Int, + val _deletions: Int, + val _deleteAttempts: Int, + val _deleteErrs: Int +) + sealed class CustomTimeUnit { object Second: CustomTimeUnit() object Minute: CustomTimeUnit() @@ -3186,6 +5011,7 @@ interface Feature { @Composable fun iconFilled(): Painter val hasParam: Boolean + val hasRole: Boolean } @Serializable @@ -3205,6 +5031,7 @@ enum class ChatFeature: Feature { TimedMessages -> true else -> false } + override val hasRole: Boolean = false override val text: String get() = when(this) { @@ -3304,13 +5131,29 @@ enum class GroupFeature: Feature { @SerialName("fullDelete") FullDelete, @SerialName("reactions") Reactions, @SerialName("voice") Voice, - @SerialName("files") Files; + @SerialName("files") Files, + @SerialName("simplexLinks") SimplexLinks, + @SerialName("reports") Reports, + @SerialName("history") History; override val hasParam: Boolean get() = when(this) { TimedMessages -> true else -> false } + override val hasRole: Boolean + get() = when (this) { + TimedMessages -> false + DirectMessages -> true + FullDelete -> false + Reactions -> false + Voice -> true + Files -> true + SimplexLinks -> true + Reports -> false + History -> false + } + override val text: String get() = when(this) { TimedMessages -> generalGetString(MR.strings.timed_messages) @@ -3319,6 +5162,9 @@ enum class GroupFeature: Feature { Reactions -> generalGetString(MR.strings.message_reactions) 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) + History -> generalGetString(MR.strings.recent_history) } val icon: Painter @@ -3329,6 +5175,9 @@ enum class GroupFeature: Feature { Reactions -> painterResource(MR.images.ic_add_reaction) Voice -> painterResource(MR.images.ic_keyboard_voice) Files -> painterResource(MR.images.ic_draft) + SimplexLinks -> painterResource(MR.images.ic_link) + Reports -> painterResource(MR.images.ic_flag) + History -> painterResource(MR.images.ic_schedule) } @Composable @@ -3339,6 +5188,9 @@ enum class GroupFeature: Feature { Reactions -> painterResource(MR.images.ic_add_reaction_filled) Voice -> painterResource(MR.images.ic_keyboard_voice_filled) Files -> painterResource(MR.images.ic_draft_filled) + SimplexLinks -> painterResource(MR.images.ic_link) + Reports -> painterResource(MR.images.ic_flag_filled) + History -> painterResource(MR.images.ic_schedule_filled) } fun enableDescription(enabled: GroupFeatureEnabled, canEdit: Boolean): String = @@ -3368,6 +5220,18 @@ enum class GroupFeature: Feature { GroupFeatureEnabled.ON -> generalGetString(MR.strings.allow_to_send_files) GroupFeatureEnabled.OFF -> generalGetString(MR.strings.prohibit_sending_files) } + SimplexLinks -> when(enabled) { + GroupFeatureEnabled.ON -> generalGetString(MR.strings.allow_to_send_simplex_links) + GroupFeatureEnabled.OFF -> generalGetString(MR.strings.prohibit_sending_simplex_links) + } + Reports -> when(enabled) { + GroupFeatureEnabled.ON -> generalGetString(MR.strings.enable_sending_member_reports) + 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) + } } } else { when(this) { @@ -3377,7 +5241,7 @@ enum class GroupFeature: Feature { } DirectMessages -> when(enabled) { GroupFeatureEnabled.ON -> generalGetString(MR.strings.group_members_can_send_dms) - GroupFeatureEnabled.OFF -> generalGetString(MR.strings.direct_messages_are_prohibited_in_chat) + GroupFeatureEnabled.OFF -> generalGetString(MR.strings.direct_messages_are_prohibited) } FullDelete -> when(enabled) { GroupFeatureEnabled.ON -> generalGetString(MR.strings.group_members_can_delete) @@ -3395,6 +5259,18 @@ enum class GroupFeature: Feature { GroupFeatureEnabled.ON -> generalGetString(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.OFF -> generalGetString(MR.strings.simplex_links_are_prohibited_in_group) + } + Reports -> when(enabled) { + GroupFeatureEnabled.ON -> generalGetString(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) + } } } } @@ -3504,11 +5380,14 @@ enum class FeatureAllowed { @Serializable data class FullGroupPreferences( val timedMessages: TimedMessagesGroupPreference, - val directMessages: GroupPreference, + val directMessages: RoleGroupPreference, val fullDelete: GroupPreference, val reactions: GroupPreference, - val voice: GroupPreference, - val files: GroupPreference, + val voice: RoleGroupPreference, + val files: RoleGroupPreference, + val simplexLinks: RoleGroupPreference, + val reports: GroupPreference, + val history: GroupPreference, ) { fun toGroupPreferences(): GroupPreferences = GroupPreferences( @@ -3518,37 +5397,49 @@ data class FullGroupPreferences( reactions = reactions, voice = voice, files = files, + simplexLinks = simplexLinks, + reports = reports, + history = history, ) companion object { val sampleData = FullGroupPreferences( timedMessages = TimedMessagesGroupPreference(GroupFeatureEnabled.OFF), - directMessages = GroupPreference(GroupFeatureEnabled.OFF), + directMessages = RoleGroupPreference(GroupFeatureEnabled.OFF, role = null), fullDelete = GroupPreference(GroupFeatureEnabled.OFF), reactions = GroupPreference(GroupFeatureEnabled.ON), - voice = GroupPreference(GroupFeatureEnabled.ON), - files = GroupPreference(GroupFeatureEnabled.ON), + voice = RoleGroupPreference(GroupFeatureEnabled.ON, role = null), + files = RoleGroupPreference(GroupFeatureEnabled.ON, role = null), + simplexLinks = RoleGroupPreference(GroupFeatureEnabled.ON, role = null), + reports = GroupPreference(GroupFeatureEnabled.ON), + history = GroupPreference(GroupFeatureEnabled.ON), ) } } @Serializable data class GroupPreferences( - val timedMessages: TimedMessagesGroupPreference?, - val directMessages: GroupPreference?, - val fullDelete: GroupPreference?, - val reactions: GroupPreference?, - val voice: GroupPreference?, - val files: GroupPreference?, + val timedMessages: TimedMessagesGroupPreference? = null, + val directMessages: RoleGroupPreference? = null, + val fullDelete: GroupPreference? = null, + val reactions: GroupPreference? = null, + val voice: RoleGroupPreference? = null, + val files: RoleGroupPreference? = null, + val simplexLinks: RoleGroupPreference? = null, + val reports: GroupPreference? = null, + val history: GroupPreference? = null, ) { companion object { val sampleData = GroupPreferences( timedMessages = TimedMessagesGroupPreference(GroupFeatureEnabled.OFF), - directMessages = GroupPreference(GroupFeatureEnabled.OFF), + directMessages = RoleGroupPreference(GroupFeatureEnabled.OFF, role = null), fullDelete = GroupPreference(GroupFeatureEnabled.OFF), reactions = GroupPreference(GroupFeatureEnabled.ON), - voice = GroupPreference(GroupFeatureEnabled.ON), - files = GroupPreference(GroupFeatureEnabled.ON), + voice = RoleGroupPreference(GroupFeatureEnabled.ON, role = null), + files = RoleGroupPreference(GroupFeatureEnabled.ON, role = null), + simplexLinks = RoleGroupPreference(GroupFeatureEnabled.ON, role = null), + reports = GroupPreference(GroupFeatureEnabled.ON), + history = GroupPreference(GroupFeatureEnabled.ON), ) } } @@ -3558,6 +5449,26 @@ data class GroupPreference( val enable: GroupFeatureEnabled ) { val on: Boolean get() = enable == GroupFeatureEnabled.ON + + fun enabled(role: GroupMemberRole?, m: GroupMember?): GroupFeatureEnabled = + when (enable) { + GroupFeatureEnabled.OFF -> GroupFeatureEnabled.OFF + GroupFeatureEnabled.ON -> + if (role != null && m != null) { + if (m.memberRole >= role) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF + } else { + GroupFeatureEnabled.ON + } + } +} + +@Serializable +data class RoleGroupPreference( + val enable: GroupFeatureEnabled, + val role: GroupMemberRole? = null, +) { + fun on(m: GroupMember): Boolean = + enable == GroupFeatureEnabled.ON && m.memberRole >= (role ?: GroupMemberRole.Observer) } @Serializable @@ -3646,61 +5557,145 @@ val json = Json { explicitNulls = false } +// Can decode unknown enum to default value specified for this field +val jsonCoerceInputValues = Json { + prettyPrint = true + ignoreUnknownKeys = true + encodeDefaults = true + explicitNulls = false + coerceInputValues = true +} + +val jsonShort = Json { + prettyPrint = false + ignoreUnknownKeys = true + encodeDefaults = true + explicitNulls = false +} + val yaml = Yaml(configuration = YamlConfiguration( strictMode = false, encodeDefaults = false, + /** ~5.5 MB limitation since wallpaper is limited by 5 MB, see [saveWallpaperFile] */ + codePointLimit = 5500000, )) -@Serializable -class APIResponse(val resp: CR, val remoteHostId: Long?, val corr: String? = null) { - companion object { - fun decodeStr(str: String): APIResponse { - return try { - json.decodeFromString(str) - } catch(e: Throwable) { - try { - Log.d(TAG, e.localizedMessage ?: "") - val data = json.parseToJsonElement(str).jsonObject - val resp = data["resp"]!!.jsonObject - val type = resp["type"]?.jsonPrimitive?.contentOrNull ?: "invalid" - val corr = data["corr"]?.toString() - val remoteHostId = data["remoteHostId"]?.jsonPrimitive?.longOrNull - try { - if (type == "apiChats") { - val user: UserRef = json.decodeFromJsonElement(resp["user"]!!.jsonObject) - val chats: List<Chat> = resp["chats"]!!.jsonArray.map { - parseChatData(it) - } - return APIResponse(CR.ApiChats(user, chats), remoteHostId, corr) - } else if (type == "apiChat") { - val user: UserRef = json.decodeFromJsonElement(resp["user"]!!.jsonObject) - val chat = parseChatData(resp["chat"]!!) - return APIResponse(CR.ApiChat(user, chat), remoteHostId, corr) - } else if (type == "chatCmdError") { - val userObject = resp["user_"]?.jsonObject - val user = runCatching<UserRef?> { json.decodeFromJsonElement(userObject!!) }.getOrNull() - return APIResponse(CR.ChatCmdError(user, ChatError.ChatErrorInvalidJSON(json.encodeToString(resp["chatError"]))), remoteHostId, corr) - } else if (type == "chatError") { - val userObject = resp["user_"]?.jsonObject - val user = runCatching<UserRef?> { json.decodeFromJsonElement(userObject!!) }.getOrNull() - return APIResponse(CR.ChatRespError(user, ChatError.ChatErrorInvalidJSON(json.encodeToString(resp["chatError"]))), remoteHostId, corr) - } - } catch (e: Exception) { - Log.e(TAG, "Exception while parsing chat(s): " + e.stackTraceToString()) - } catch (e: Throwable) { - Log.e(TAG, "Throwable while parsing chat(s): " + e.stackTraceToString()) - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), e.stackTraceToString()) +@Suppress("SERIALIZER_TYPE_INCOMPATIBLE") +@Serializable(with = APISerializer::class) +sealed class API { + @Serializable(with = APISerializer::class) class Result(val remoteHostId: Long?, val res: CR) : API() + @Serializable(with = APISerializer::class) class Error(val remoteHostId: Long?, val err: ChatError) : API() + + val ok: Boolean get() = this is API.Result && this.res is CR.CmdOk + val result: CR? get() = (this as? API.Result)?.res + val rhId: Long? get() = when (this) { + is Result -> remoteHostId + is Error -> remoteHostId + } + + val pair: Pair<CR?, ChatError?> get() = when (this) { + is Result -> res to null + is Error -> null to err + } + + val responseType: String get() = when (this) { + is Result -> res.responseType + is Error -> "error ${err.resultType}" + } + + val details: String get() = when (this) { + is Result -> res.details + is Error -> "error ${err.string}" + } +} + +object APISerializer : KSerializer<API> { + override val descriptor: SerialDescriptor = buildSerialDescriptor("API", PolymorphicKind.SEALED) { + element("Result", buildClassSerialDescriptor("Result") { + element<Long?>("remoteHostId") + element<CR>("result") + }) + element("Error", buildClassSerialDescriptor("Error") { + element<Long?>("remoteHostId") + element<ChatError>("error") + }) + } + + override fun deserialize(decoder: Decoder): API { + require(decoder is JsonDecoder) + val j = try { decoder.decodeJsonElement() } catch(e: Exception) { null } catch(e: Throwable) { null } + if (j == null) return API.Error(remoteHostId = null, ChatError.ChatErrorInvalidJSON("")) + if (j !is JsonObject) return API.Error(remoteHostId = null, ChatError.ChatErrorInvalidJSON(json.encodeToString(j))) + val remoteHostId = j["remoteHostId"]?.jsonPrimitive?.longOrNull + val jRes = j["result"] + if (jRes != null) { + val result = try { + decoder.json.decodeFromJsonElement<CR>(jRes) + } catch (e: Exception) { + fallbackResult(jRes) + } catch (e: Throwable) { + fallbackResult(jRes) + } + return API.Result(remoteHostId, result) + } + val jErr = j["error"] + if (jErr != null) { + val error = try { + decoder.json.decodeFromJsonElement<ChatError>(jErr) + } catch (e: Exception) { + fallbackChatError(jErr) + } catch (e: Throwable) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), e.stackTraceToString()) + fallbackChatError(jErr) + } + return API.Error(remoteHostId, error) + } + return API.Error(remoteHostId, fallbackChatError(j)) + } + + private fun fallbackResult(jRes: JsonElement): CR { + if (jRes is JsonObject) { + val type = jRes["type"]?.jsonPrimitive?.contentOrNull ?: "invalid" + try { + if (type == "apiChats") { + val user: UserRef = json.decodeFromJsonElement(jRes["user"]!!.jsonObject) + val chats: List<Chat> = jRes["chats"]!!.jsonArray.map { + parseChatData(it) } - APIResponse(CR.Response(type, json.encodeToString(data)), remoteHostId, corr) - } catch(e: Exception) { - APIResponse(CR.Invalid(str), remoteHostId = null) - } catch(e: Throwable) { - Log.e(TAG, "Throwable2 while parsing chat(s): " + e.stackTraceToString()) - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), e.stackTraceToString()) - APIResponse(CR.Invalid(str), remoteHostId = null) + return CR.ApiChats(user, chats) + } else if (type == "apiChat") { + val user: UserRef = json.decodeFromJsonElement(jRes["user"]!!.jsonObject) + val chat = parseChatData(jRes["chat"]!!) + return CR.ApiChat(user, chat) } + } catch (e: Exception) { + Log.e(TAG, "Exception while parsing chat(s): " + e.stackTraceToString()) + } catch (e: Throwable) { + Log.e(TAG, "Throwable while parsing chat(s): " + e.stackTraceToString()) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), e.stackTraceToString()) + } + return CR.Response(type, json.encodeToString(jRes)) + } + return CR.Response(type = "invalid", json.encodeToString(jRes)) + } + + private fun fallbackChatError(jErr: JsonElement): ChatError { + return ChatError.ChatErrorInvalidJSON(json.encodeToString(jErr)) + } + + override fun serialize(encoder: Encoder, value: API) { + require(encoder is JsonEncoder) + val json = when (value) { + is API.Result -> buildJsonObject { + value.remoteHostId?.let { put("remoteHostId", it) } + put("result", encoder.json.encodeToJsonElement(value.res)) + } + is API.Error -> buildJsonObject { + value.remoteHostId?.let { put("remoteHostId", it) } + put("error", encoder.json.encodeToJsonElement(value.err)) } } + encoder.encodeJsonElement(json) } } @@ -3732,14 +5727,19 @@ sealed class CR { @Serializable @SerialName("chatRunning") class ChatRunning: CR() @Serializable @SerialName("chatStopped") class ChatStopped: CR() @Serializable @SerialName("apiChats") class ApiChats(val user: UserRef, val chats: List<Chat>): CR() - @Serializable @SerialName("apiChat") class ApiChat(val user: UserRef, val chat: Chat): CR() + @Serializable @SerialName("apiChat") class ApiChat(val user: UserRef, val chat: Chat, val navInfo: NavigationInfo = NavigationInfo()): CR() + @Serializable @SerialName("chatTags") class ChatTags(val user: UserRef, val userTags: List<ChatTag>): CR() @Serializable @SerialName("chatItemInfo") class ApiChatItemInfo(val user: UserRef, val chatItem: AChatItem, val chatItemInfo: ChatItemInfo): CR() - @Serializable @SerialName("userProtoServers") class UserProtoServers(val user: UserRef, val servers: UserProtocolServers): CR() @Serializable @SerialName("serverTestResult") class ServerTestResult(val user: UserRef, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR() + @Serializable @SerialName("serverOperatorConditions") class ServerOperatorConditions(val conditions: ServerOperatorConditionsDetail): CR() + @Serializable @SerialName("userServers") class UserServers(val user: UserRef, val userServers: List<UserOperatorServers>): CR() + @Serializable @SerialName("userServersValidation") class UserServersValidation(val user: UserRef, val serverErrors: List<UserServersError>): CR() + @Serializable @SerialName("usageConditions") class UsageConditions(val usageConditions: UsageConditionsDetail, val conditionsText: String?, val acceptedConditions: UsageConditionsDetail?): CR() @Serializable @SerialName("chatItemTTL") class ChatItemTTL(val user: UserRef, val chatItemTTL: Long? = null): CR() @Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR() @Serializable @SerialName("contactInfo") class ContactInfo(val user: UserRef, val contact: Contact, val connectionStats_: ConnectionStats? = null, val customUserProfile: Profile? = null): CR() @Serializable @SerialName("groupMemberInfo") class GroupMemberInfo(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats_: ConnectionStats? = null): CR() + @Serializable @SerialName("queueInfo") class QueueInfoR(val user: UserRef, val rcvMsgInfo: RcvMsgInfo?, val queueInfo: ServerQueueInfo): CR() @Serializable @SerialName("contactSwitchStarted") class ContactSwitchStarted(val user: UserRef, val contact: Contact, val connectionStats: ConnectionStats): CR() @Serializable @SerialName("groupMemberSwitchStarted") class GroupMemberSwitchStarted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR() @Serializable @SerialName("contactSwitchAborted") class ContactSwitchAborted(val user: UserRef, val contact: Contact, val connectionStats: ConnectionStats): CR() @@ -3750,19 +5750,18 @@ sealed class CR { @Serializable @SerialName("groupMemberRatchetSyncStarted") class GroupMemberRatchetSyncStarted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR() @Serializable @SerialName("contactRatchetSync") class ContactRatchetSync(val user: UserRef, val contact: Contact, val ratchetSyncProgress: RatchetSyncProgress): CR() @Serializable @SerialName("groupMemberRatchetSync") class GroupMemberRatchetSync(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val ratchetSyncProgress: RatchetSyncProgress): CR() - @Serializable @SerialName("contactVerificationReset") class ContactVerificationReset(val user: UserRef, val contact: Contact): CR() - @Serializable @SerialName("groupMemberVerificationReset") class GroupMemberVerificationReset(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("contactCode") class ContactCode(val user: UserRef, val contact: Contact, val connectionCode: String): CR() @Serializable @SerialName("groupMemberCode") class GroupMemberCode(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionCode: String): CR() @Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: UserRef, val verified: Boolean, val expectedCode: String): CR() - @Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): CR() + @Serializable @SerialName("tagsUpdated") class TagsUpdated(val user: UserRef, val userTags: List<ChatTag>, val chatTags: List<Long>): CR() + @Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connLinkInvitation: CreatedConnLink, val connection: PendingContactConnection): CR() @Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR() - @Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connectionPlan: ConnectionPlan): CR() + @Serializable @SerialName("connectionUserChanged") class ConnectionUserChanged(val user: UserRef, val fromConnection: PendingContactConnection, val toConnection: PendingContactConnection, val newUser: UserRef): CR() + @Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connLink: CreatedConnLink, val connectionPlan: ConnectionPlan): CR() @Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef, val connection: PendingContactConnection): CR() @Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef, val connection: PendingContactConnection): CR() @Serializable @SerialName("sentInvitationToContact") class SentInvitationToContact(val user: UserRef, val contact: Contact, val customUserProfile: Profile?): CR() @Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR() - @Serializable @SerialName("contactRequestAlreadyAccepted") class ContactRequestAlreadyAccepted(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("contactDeleted") class ContactDeleted(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("contactDeletedByContact") class ContactDeletedByContact(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("chatCleared") class ChatCleared(val user: UserRef, val chatInfo: ChatInfo): CR() @@ -3770,14 +5769,16 @@ sealed class CR { @Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val user: User, val fromProfile: Profile, val toProfile: Profile, val updateSummary: UserProfileUpdateSummary): CR() @Serializable @SerialName("userPrivacy") class UserPrivacy(val user: User, val updatedUser: User): CR() @Serializable @SerialName("contactAliasUpdated") class ContactAliasUpdated(val user: UserRef, val toContact: Contact): CR() + @Serializable @SerialName("groupAliasUpdated") class GroupAliasUpdated(val user: UserRef, val toGroup: GroupInfo): CR() @Serializable @SerialName("connectionAliasUpdated") class ConnectionAliasUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR() @Serializable @SerialName("contactPrefsUpdated") class ContactPrefsUpdated(val user: UserRef, val fromContact: Contact, val toContact: Contact): CR() @Serializable @SerialName("userContactLink") class UserContactLink(val user: User, val contactLink: UserContactLinkRec): CR() @Serializable @SerialName("userContactLinkUpdated") class UserContactLinkUpdated(val user: User, val contactLink: UserContactLinkRec): CR() - @Serializable @SerialName("userContactLinkCreated") class UserContactLinkCreated(val user: User, val connReqContact: String): CR() + @Serializable @SerialName("userContactLinkCreated") class UserContactLinkCreated(val user: User, val connLinkContact: CreatedConnLink): CR() @Serializable @SerialName("userContactLinkDeleted") class UserContactLinkDeleted(val user: User): CR() @Serializable @SerialName("contactConnected") class ContactConnected(val user: UserRef, val contact: Contact, val userCustomProfile: Profile? = null): CR() @Serializable @SerialName("contactConnecting") class ContactConnecting(val user: UserRef, val contact: Contact): CR() + @Serializable @SerialName("contactSndReady") class ContactSndReady(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("receivedContactRequest") class ReceivedContactRequest(val user: UserRef, val contactRequest: UserContactRequest): CR() @Serializable @SerialName("acceptingContactRequest") class AcceptingContactRequest(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("contactRequestRejected") class ContactRequestRejected(val user: UserRef): CR() @@ -3790,42 +5791,42 @@ sealed class CR { // TODO remove above @Serializable @SerialName("networkStatus") class NetworkStatusResp(val networkStatus: NetworkStatus, val connections: List<String>): CR() @Serializable @SerialName("networkStatuses") class NetworkStatuses(val user_: UserRef?, val networkStatuses: List<ConnNetworkStatus>): CR() - @Serializable @SerialName("groupSubscribed") class GroupSubscribed(val user: UserRef, val group: GroupRef): CR() - @Serializable @SerialName("memberSubErrors") class MemberSubErrors(val user: UserRef, val memberSubErrors: List<MemberSubError>): CR() - @Serializable @SerialName("groupEmpty") class GroupEmpty(val user: UserRef, val group: GroupInfo): CR() - @Serializable @SerialName("userContactLinkSubscribed") class UserContactLinkSubscribed: CR() - @Serializable @SerialName("newChatItem") class NewChatItem(val user: UserRef, val chatItem: AChatItem): CR() - @Serializable @SerialName("chatItemStatusUpdated") class ChatItemStatusUpdated(val user: UserRef, val chatItem: AChatItem): CR() + @Serializable @SerialName("newChatItems") class NewChatItems(val user: UserRef, val chatItems: List<AChatItem>): CR() + @Serializable @SerialName("chatItemsStatusesUpdated") class ChatItemsStatusesUpdated(val user: UserRef, val chatItems: List<AChatItem>): CR() @Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val user: UserRef, val chatItem: AChatItem): CR() @Serializable @SerialName("chatItemNotChanged") class ChatItemNotChanged(val user: UserRef, val chatItem: AChatItem): CR() @Serializable @SerialName("chatItemReaction") class ChatItemReaction(val user: UserRef, val added: Boolean, val reaction: ACIReaction): CR() - @Serializable @SerialName("chatItemDeleted") class ChatItemDeleted(val user: UserRef, val deletedChatItem: AChatItem, val toChatItem: AChatItem? = null, val byUser: Boolean): CR() + @Serializable @SerialName("reactionMembers") class ReactionMembers(val user: UserRef, val memberReactions: List<MemberReaction>): CR() + @Serializable @SerialName("chatItemsDeleted") class ChatItemsDeleted(val user: UserRef, val chatItemDeletions: List<ChatItemDeletion>, val byUser: Boolean): CR() + @Serializable @SerialName("groupChatItemsDeleted") class GroupChatItemsDeleted(val user: UserRef, val groupInfo: GroupInfo, val chatItemIDs: List<Long>, val byUser: Boolean, val member_: GroupMember?): CR() + @Serializable @SerialName("forwardPlan") class ForwardPlan(val user: UserRef, val itemsCount: Int, val chatItemIds: List<Long>, val forwardConfirmation: ForwardConfirmation? = null): CR() // group events @Serializable @SerialName("groupCreated") class GroupCreated(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR() @Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val user: UserRef, val groupInfo: GroupInfo, val hostContact: Contact? = null): CR() @Serializable @SerialName("groupLinkConnecting") class GroupLinkConnecting (val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember): CR() - @Serializable @SerialName("userDeletedMember") class UserDeletedMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("businessLinkConnecting") class BusinessLinkConnecting (val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember, val fromContact: Contact): CR() + @Serializable @SerialName("userDeletedMembers") class UserDeletedMembers(val user: UserRef, val groupInfo: GroupInfo, val members: List<GroupMember>, val withMessages: Boolean): CR() @Serializable @SerialName("leftMemberUser") class LeftMemberUser(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("groupMembers") class GroupMembers(val user: UserRef, val group: Group): CR() @Serializable @SerialName("receivedGroupInvitation") class ReceivedGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val memberRole: GroupMemberRole): CR() @Serializable @SerialName("groupDeletedUser") class GroupDeletedUser(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("joinedGroupMemberConnecting") class JoinedGroupMemberConnecting(val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember, val member: GroupMember): CR() @Serializable @SerialName("memberRole") class MemberRole(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val member: GroupMember, val fromRole: GroupMemberRole, val toRole: GroupMemberRole): CR() - @Serializable @SerialName("memberRoleUser") class MemberRoleUser(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val fromRole: GroupMemberRole, val toRole: GroupMemberRole): CR() - @Serializable @SerialName("deletedMemberUser") class DeletedMemberUser(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() - @Serializable @SerialName("deletedMember") class DeletedMember(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val deletedMember: GroupMember): CR() + @Serializable @SerialName("membersRoleUser") class MembersRoleUser(val user: UserRef, val groupInfo: GroupInfo, val members: List<GroupMember>, val toRole: GroupMemberRole): CR() + @Serializable @SerialName("memberBlockedForAll") class MemberBlockedForAll(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val member: GroupMember, val blocked: Boolean): CR() + @Serializable @SerialName("membersBlockedForAllUser") class MembersBlockedForAllUser(val user: UserRef, val groupInfo: GroupInfo, val members: List<GroupMember>, val blocked: Boolean): CR() + @Serializable @SerialName("deletedMemberUser") class DeletedMemberUser(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val withMessages: Boolean): CR() + @Serializable @SerialName("deletedMember") class DeletedMember(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val deletedMember: GroupMember, val withMessages: Boolean): CR() @Serializable @SerialName("leftMember") class LeftMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("groupDeleted") class GroupDeleted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("contactsMerged") class ContactsMerged(val user: UserRef, val intoContact: Contact, val mergedContact: Contact): CR() - @Serializable @SerialName("groupInvitation") class GroupInvitation(val user: UserRef, val groupInfo: GroupInfo): CR() // unused @Serializable @SerialName("userJoinedGroup") class UserJoinedGroup(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("joinedGroupMember") class JoinedGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val memberContact: Contact? = null): CR() - @Serializable @SerialName("groupRemoved") class GroupRemoved(val user: UserRef, val groupInfo: GroupInfo): CR() // unused @Serializable @SerialName("groupUpdated") class GroupUpdated(val user: UserRef, val toGroup: GroupInfo): CR() - @Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: UserRef, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR() - @Serializable @SerialName("groupLink") class GroupLink(val user: UserRef, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR() + @Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: UserRef, val groupInfo: GroupInfo, val connLinkContact: CreatedConnLink, val memberRole: GroupMemberRole): CR() + @Serializable @SerialName("groupLink") class GroupLink(val user: UserRef, val groupInfo: GroupInfo, val connLinkContact: CreatedConnLink, val memberRole: GroupMemberRole): CR() @Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("newMemberContact") class NewMemberContact(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("newMemberContactSentInv") class NewMemberContactSentInv(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR() @@ -3833,27 +5834,39 @@ sealed class CR { // receiving file events @Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val user: UserRef, val chatItem: AChatItem): CR() @Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val user: UserRef, val rcvFileTransfer: RcvFileTransfer): CR() - @Serializable @SerialName("rcvFileStart") class RcvFileStart(val user: UserRef, val chatItem: AChatItem): CR() + @Serializable @SerialName("standaloneFileInfo") class StandaloneFileInfo(val fileMeta: MigrationFileLinkData?): CR() + @Serializable @SerialName("rcvStandaloneFileCreated") class RcvStandaloneFileCreated(val user: UserRef, val rcvFileTransfer: RcvFileTransfer): CR() + @Serializable @SerialName("rcvFileStart") class RcvFileStart(val user: UserRef, val chatItem: AChatItem): CR() // send by chats + @Serializable @SerialName("rcvFileProgressXFTP") class RcvFileProgressXFTP(val user: UserRef, val chatItem_: AChatItem?, val receivedSize: Long, val totalSize: Long, val rcvFileTransfer: RcvFileTransfer): CR() @Serializable @SerialName("rcvFileComplete") class RcvFileComplete(val user: UserRef, val chatItem: AChatItem): CR() - @Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: UserRef, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR() + @Serializable @SerialName("rcvStandaloneFileComplete") class RcvStandaloneFileComplete(val user: UserRef, val targetPath: String, val rcvFileTransfer: RcvFileTransfer): CR() + @Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: UserRef, val chatItem_: AChatItem?, val rcvFileTransfer: RcvFileTransfer): CR() @Serializable @SerialName("rcvFileSndCancelled") class RcvFileSndCancelled(val user: UserRef, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR() - @Serializable @SerialName("rcvFileProgressXFTP") class RcvFileProgressXFTP(val user: UserRef, val chatItem: AChatItem, val receivedSize: Long, val totalSize: Long): CR() - @Serializable @SerialName("rcvFileError") class RcvFileError(val user: UserRef, val chatItem: AChatItem): CR() + @Serializable @SerialName("rcvFileError") class RcvFileError(val user: UserRef, val chatItem_: AChatItem?, val agentError: AgentErrorType, val rcvFileTransfer: RcvFileTransfer): CR() + @Serializable @SerialName("rcvFileWarning") class RcvFileWarning(val user: UserRef, val chatItem_: AChatItem?, val agentError: AgentErrorType, val rcvFileTransfer: RcvFileTransfer): CR() // sending file events @Serializable @SerialName("sndFileStart") class SndFileStart(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() - @Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List<SndFileTransfer>): CR() - @Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() - @Serializable @SerialName("sndFileProgressXFTP") class SndFileProgressXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sentSize: Long, val totalSize: Long): CR() + @Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val user: UserRef, val chatItem_: AChatItem?, val sndFileTransfer: SndFileTransfer): CR() + @Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List<SndFileTransfer>): CR() + @Serializable @SerialName("sndStandaloneFileCreated") class SndStandaloneFileCreated(val user: UserRef, val fileTransferMeta: FileTransferMeta): CR() // returned by _upload + @Serializable @SerialName("sndFileStartXFTP") class SndFileStartXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR() // not used + @Serializable @SerialName("sndFileProgressXFTP") class SndFileProgressXFTP(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val sentSize: Long, val totalSize: Long): CR() + @Serializable @SerialName("sndFileRedirectStartXFTP") class SndFileRedirectStartXFTP(val user: UserRef, val fileTransferMeta: FileTransferMeta, val redirectMeta: FileTransferMeta): CR() @Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR() - @Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem: AChatItem): CR() + @Serializable @SerialName("sndStandaloneFileComplete") class SndStandaloneFileComplete(val user: UserRef, val fileTransferMeta: FileTransferMeta, val rcvURIs: List<String>): CR() + @Serializable @SerialName("sndFileCancelledXFTP") class SndFileCancelledXFTP(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta): CR() + @Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val errorMessage: String): CR() + @Serializable @SerialName("sndFileWarning") class SndFileWarning(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val errorMessage: String): CR() // call events @Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR() + @Serializable @SerialName("callInvitations") class CallInvitations(val callInvitations: List<RcvCallInvitation>): CR() @Serializable @SerialName("callOffer") class CallOffer(val user: UserRef, val contact: Contact, val callType: CallType, val offer: WebRTCSession, val sharedKey: String? = null, val askConfirmation: Boolean): CR() @Serializable @SerialName("callAnswer") class CallAnswer(val user: UserRef, val contact: Contact, val answer: WebRTCSession): CR() @Serializable @SerialName("callExtraInfo") class CallExtraInfo(val user: UserRef, val contact: Contact, val extraInfo: WebRTCExtraInfo): CR() @Serializable @SerialName("callEnded") class CallEnded(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("contactConnectionDeleted") class ContactConnectionDeleted(val user: UserRef, val connection: PendingContactConnection): CR() + @Serializable @SerialName("contactDisabled") class ContactDisabled(val user: UserRef, val contact: Contact): CR() // remote events (desktop) @Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List<RemoteHostInfo>): CR() @Serializable @SerialName("currentRemoteHost") class CurrentRemoteHost(val remoteHost_: RemoteHostInfo?): CR() @@ -3870,11 +5883,17 @@ sealed class CR { @Serializable @SerialName("remoteCtrlSessionCode") class RemoteCtrlSessionCode(val remoteCtrl_: RemoteCtrlInfo?, val sessionCode: String): CR() @Serializable @SerialName("remoteCtrlConnected") class RemoteCtrlConnected(val remoteCtrl: RemoteCtrlInfo): CR() @Serializable @SerialName("remoteCtrlStopped") class RemoteCtrlStopped(val rcsState: RemoteCtrlSessionState, val rcStopReason: RemoteCtrlStopReason): CR() + // pq + @Serializable @SerialName("contactPQAllowed") class ContactPQAllowed(val user: UserRef, val contact: Contact, val pqEncryption: Boolean): CR() + @Serializable @SerialName("contactPQEnabled") class ContactPQEnabled(val user: UserRef, val contact: Contact, val pqEnabled: Boolean): CR() + // misc @Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo, val chatMigrations: List<UpMigration>, val agentMigrations: List<UpMigration>): CR() @Serializable @SerialName("cmdOk") class CmdOk(val user: UserRef?): CR() - @Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: UserRef?, val chatError: ChatError): CR() - @Serializable @SerialName("chatError") class ChatRespError(val user_: UserRef?, val chatError: ChatError): CR() + @Serializable @SerialName("archiveExported") class ArchiveExported(val archiveErrors: List<ArchiveError>): CR() @Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List<ArchiveError>): CR() + @Serializable @SerialName("appSettings") class AppSettingsR(val appSettings: AppSettings): CR() + @Serializable @SerialName("agentSubsTotal") class AgentSubsTotal(val user: UserRef, val subsTotal: SMPServerSubs, val hasSession: Boolean): CR() + @Serializable @SerialName("agentServersSummary") class AgentServersSummary(val user: UserRef, val serversSummary: PresentedServersSummary): CR() // general @Serializable class Response(val type: String, val json: String): CR() @Serializable class Invalid(val str: String): CR() @@ -3887,13 +5906,18 @@ sealed class CR { is ChatStopped -> "chatStopped" is ApiChats -> "apiChats" is ApiChat -> "apiChat" + is ChatTags -> "chatTags" is ApiChatItemInfo -> "chatItemInfo" - is UserProtoServers -> "userProtoServers" is ServerTestResult -> "serverTestResult" + is ServerOperatorConditions -> "serverOperatorConditions" + is UserServers -> "userServers" + is UserServersValidation -> "userServersValidation" + is UsageConditions -> "usageConditions" is ChatItemTTL -> "chatItemTTL" is NetworkConfig -> "networkConfig" is ContactInfo -> "contactInfo" is GroupMemberInfo -> "groupMemberInfo" + is QueueInfoR -> "queueInfo" is ContactSwitchStarted -> "contactSwitchStarted" is GroupMemberSwitchStarted -> "groupMemberSwitchStarted" is ContactSwitchAborted -> "contactSwitchAborted" @@ -3904,19 +5928,18 @@ sealed class CR { is GroupMemberRatchetSyncStarted -> "groupMemberRatchetSyncStarted" is ContactRatchetSync -> "contactRatchetSync" is GroupMemberRatchetSync -> "groupMemberRatchetSync" - is ContactVerificationReset -> "contactVerificationReset" - is GroupMemberVerificationReset -> "groupMemberVerificationReset" is ContactCode -> "contactCode" is GroupMemberCode -> "groupMemberCode" is ConnectionVerified -> "connectionVerified" + is TagsUpdated -> "tagsUpdated" is Invitation -> "invitation" is ConnectionIncognitoUpdated -> "connectionIncognitoUpdated" + is ConnectionUserChanged -> "ConnectionUserChanged" is CRConnectionPlan -> "connectionPlan" is SentConfirmation -> "sentConfirmation" is SentInvitation -> "sentInvitation" is SentInvitationToContact -> "sentInvitationToContact" is ContactAlreadyExists -> "contactAlreadyExists" - is ContactRequestAlreadyAccepted -> "contactRequestAlreadyAccepted" is ContactDeleted -> "contactDeleted" is ContactDeletedByContact -> "contactDeletedByContact" is ChatCleared -> "chatCleared" @@ -3924,6 +5947,7 @@ sealed class CR { is UserProfileUpdated -> "userProfileUpdated" is UserPrivacy -> "userPrivacy" is ContactAliasUpdated -> "contactAliasUpdated" + is GroupAliasUpdated -> "groupAliasUpdated" is ConnectionAliasUpdated -> "connectionAliasUpdated" is ContactPrefsUpdated -> "contactPrefsUpdated" is UserContactLink -> "userContactLink" @@ -3932,6 +5956,7 @@ sealed class CR { is UserContactLinkDeleted -> "userContactLinkDeleted" is ContactConnected -> "contactConnected" is ContactConnecting -> "contactConnecting" + is ContactSndReady -> "contactSndReady" is ReceivedContactRequest -> "receivedContactRequest" is AcceptingContactRequest -> "acceptingContactRequest" is ContactRequestRejected -> "contactRequestRejected" @@ -3942,38 +5967,38 @@ sealed class CR { is ContactSubSummary -> "contactSubSummary" is NetworkStatusResp -> "networkStatus" is NetworkStatuses -> "networkStatuses" - is GroupSubscribed -> "groupSubscribed" - is MemberSubErrors -> "memberSubErrors" - is GroupEmpty -> "groupEmpty" - is UserContactLinkSubscribed -> "userContactLinkSubscribed" - is NewChatItem -> "newChatItem" - is ChatItemStatusUpdated -> "chatItemStatusUpdated" + is NewChatItems -> "newChatItems" + is ChatItemsStatusesUpdated -> "chatItemsStatusesUpdated" is ChatItemUpdated -> "chatItemUpdated" is ChatItemNotChanged -> "chatItemNotChanged" is ChatItemReaction -> "chatItemReaction" - is ChatItemDeleted -> "chatItemDeleted" + is ReactionMembers -> "reactionMembers" + is ChatItemsDeleted -> "chatItemsDeleted" + is GroupChatItemsDeleted -> "groupChatItemsDeleted" + is ForwardPlan -> "forwardPlan" is GroupCreated -> "groupCreated" is SentGroupInvitation -> "sentGroupInvitation" is UserAcceptedGroupSent -> "userAcceptedGroupSent" is GroupLinkConnecting -> "groupLinkConnecting" - is UserDeletedMember -> "userDeletedMember" + is BusinessLinkConnecting -> "businessLinkConnecting" + is UserDeletedMembers -> "userDeletedMembers" is LeftMemberUser -> "leftMemberUser" is GroupMembers -> "groupMembers" is ReceivedGroupInvitation -> "receivedGroupInvitation" is GroupDeletedUser -> "groupDeletedUser" is JoinedGroupMemberConnecting -> "joinedGroupMemberConnecting" is MemberRole -> "memberRole" - is MemberRoleUser -> "memberRoleUser" + is MembersRoleUser -> "membersRoleUser" + is MemberBlockedForAll -> "memberBlockedForAll" + is MembersBlockedForAllUser -> "membersBlockedForAllUser" is DeletedMemberUser -> "deletedMemberUser" is DeletedMember -> "deletedMember" is LeftMember -> "leftMember" is GroupDeleted -> "groupDeleted" is ContactsMerged -> "contactsMerged" - is GroupInvitation -> "groupInvitation" is UserJoinedGroup -> "userJoinedGroup" is JoinedGroupMember -> "joinedGroupMember" is ConnectedToGroupMember -> "connectedToGroupMember" - is GroupRemoved -> "groupRemoved" is GroupUpdated -> "groupUpdated" is GroupLinkCreated -> "groupLinkCreated" is GroupLink -> "groupLink" @@ -3982,26 +6007,38 @@ sealed class CR { is NewMemberContactSentInv -> "newMemberContactSentInv" is NewMemberContactReceivedInv -> "newMemberContactReceivedInv" is RcvFileAcceptedSndCancelled -> "rcvFileAcceptedSndCancelled" + is StandaloneFileInfo -> "standaloneFileInfo" + is RcvStandaloneFileCreated -> "rcvStandaloneFileCreated" is RcvFileAccepted -> "rcvFileAccepted" is RcvFileStart -> "rcvFileStart" is RcvFileComplete -> "rcvFileComplete" + is RcvStandaloneFileComplete -> "rcvStandaloneFileComplete" is RcvFileCancelled -> "rcvFileCancelled" + is SndStandaloneFileCreated -> "sndStandaloneFileCreated" + is SndFileStartXFTP -> "sndFileStartXFTP" is RcvFileSndCancelled -> "rcvFileSndCancelled" is RcvFileProgressXFTP -> "rcvFileProgressXFTP" + is SndFileRedirectStartXFTP -> "sndFileRedirectStartXFTP" is RcvFileError -> "rcvFileError" - is SndFileCancelled -> "sndFileCancelled" + is RcvFileWarning -> "rcvFileWarning" + is SndFileStart -> "sndFileStart" is SndFileComplete -> "sndFileComplete" is SndFileRcvCancelled -> "sndFileRcvCancelled" - is SndFileStart -> "sndFileStart" + is SndFileCancelled -> "sndFileCancelled" is SndFileProgressXFTP -> "sndFileProgressXFTP" is SndFileCompleteXFTP -> "sndFileCompleteXFTP" + is SndStandaloneFileComplete -> "sndStandaloneFileComplete" + is SndFileCancelledXFTP -> "sndFileCancelledXFTP" is SndFileError -> "sndFileError" + is SndFileWarning -> "sndFileWarning" + is CallInvitations -> "callInvitations" is CallInvitation -> "callInvitation" is CallOffer -> "callOffer" is CallAnswer -> "callAnswer" is CallExtraInfo -> "callExtraInfo" is CallEnded -> "callEnded" is ContactConnectionDeleted -> "contactConnectionDeleted" + is ContactDisabled -> "contactDisabled" is RemoteHostList -> "remoteHostList" is CurrentRemoteHost -> "currentRemoteHost" is RemoteHostStarted -> "remoteHostStarted" @@ -4016,11 +6053,15 @@ sealed class CR { is RemoteCtrlSessionCode -> "remoteCtrlSessionCode" is RemoteCtrlConnected -> "remoteCtrlConnected" is RemoteCtrlStopped -> "remoteCtrlStopped" + is ContactPQAllowed -> "contactPQAllowed" + is ContactPQEnabled -> "contactPQEnabled" is VersionInfo -> "versionInfo" + is AgentSubsTotal -> "agentSubsTotal" + is AgentServersSummary -> "agentServersSummary" is CmdOk -> "cmdOk" - is ChatCmdError -> "chatCmdError" - is ChatRespError -> "chatError" + is ArchiveExported -> "archiveExported" is ArchiveImported -> "archiveImported" + is AppSettingsR -> "appSettings" is Response -> "* $type" is Invalid -> "* invalid json" } @@ -4032,14 +6073,19 @@ sealed class CR { is ChatRunning -> noDetails() is ChatStopped -> noDetails() is ApiChats -> withUser(user, json.encodeToString(chats)) - is ApiChat -> withUser(user, json.encodeToString(chat)) - is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(AChatItem)}\n${json.encodeToString(chatItemInfo)}") - is UserProtoServers -> withUser(user, "servers: ${json.encodeToString(servers)}") + is ApiChat -> withUser(user, "remoteHostId: ${chat.remoteHostId}\nchatInfo: ${chat.chatInfo}\nchatStats: ${chat.chatStats}\nnavInfo: ${navInfo}\nchatItems: ${chat.chatItems}") + is ChatTags -> withUser(user, "userTags: ${json.encodeToString(userTags)}") + is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\n${json.encodeToString(chatItemInfo)}") is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}") + is ServerOperatorConditions -> "conditions: ${json.encodeToString(conditions)}" + is UserServers -> withUser(user, "userServers: ${json.encodeToString(userServers)}") + is UserServersValidation -> withUser(user, "serverErrors: ${json.encodeToString(serverErrors)}") + is UsageConditions -> "usageConditions: ${json.encodeToString(usageConditions)}\nnacceptedConditions: ${json.encodeToString(acceptedConditions)}" is ChatItemTTL -> withUser(user, json.encodeToString(chatItemTTL)) is NetworkConfig -> json.encodeToString(networkConfig) is ContactInfo -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats_)}") is GroupMemberInfo -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionStats: ${json.encodeToString(connectionStats_)}") + is QueueInfoR -> withUser(user, "rcvMsgInfo: ${json.encodeToString(rcvMsgInfo)}\nqueueInfo: ${json.encodeToString(queueInfo)}\n") is ContactSwitchStarted -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats)}") is GroupMemberSwitchStarted -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionStats: ${json.encodeToString(connectionStats)}") is ContactSwitchAborted -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats)}") @@ -4050,19 +6096,18 @@ sealed class CR { is GroupMemberRatchetSyncStarted -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionStats: ${json.encodeToString(connectionStats)}") is ContactRatchetSync -> withUser(user, "contact: ${json.encodeToString(contact)}\nratchetSyncProgress: ${json.encodeToString(ratchetSyncProgress)}") is GroupMemberRatchetSync -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nratchetSyncProgress: ${json.encodeToString(ratchetSyncProgress)}") - is ContactVerificationReset -> withUser(user, "contact: ${json.encodeToString(contact)}") - is GroupMemberVerificationReset -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}") is ContactCode -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionCode: $connectionCode") is GroupMemberCode -> withUser(user, "groupInfo: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionCode: $connectionCode") is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode") - is Invitation -> withUser(user, "connReqInvitation: $connReqInvitation\nconnection: $connection") + is TagsUpdated -> withUser(user, "userTags: ${json.encodeToString(userTags)}\nchatTags: ${json.encodeToString(chatTags)}") + is Invitation -> withUser(user, "connLinkInvitation: ${json.encodeToString(connLinkInvitation)}\nconnection: $connection") is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection)) - is CRConnectionPlan -> withUser(user, json.encodeToString(connectionPlan)) + is ConnectionUserChanged -> withUser(user, "fromConnection: ${json.encodeToString(fromConnection)}\ntoConnection: ${json.encodeToString(toConnection)}\nnewUser: ${json.encodeToString(newUser)}" ) + is CRConnectionPlan -> withUser(user, "connLink: ${json.encodeToString(connLink)}\nconnectionPlan: ${json.encodeToString(connectionPlan)}") is SentConfirmation -> withUser(user, json.encodeToString(connection)) is SentInvitation -> withUser(user, json.encodeToString(connection)) is SentInvitationToContact -> withUser(user, json.encodeToString(contact)) is ContactAlreadyExists -> withUser(user, json.encodeToString(contact)) - is ContactRequestAlreadyAccepted -> withUser(user, json.encodeToString(contact)) is ContactDeleted -> withUser(user, json.encodeToString(contact)) is ContactDeletedByContact -> withUser(user, json.encodeToString(contact)) is ChatCleared -> withUser(user, json.encodeToString(chatInfo)) @@ -4070,14 +6115,16 @@ sealed class CR { is UserProfileUpdated -> withUser(user, json.encodeToString(toProfile)) is UserPrivacy -> withUser(user, json.encodeToString(updatedUser)) is ContactAliasUpdated -> withUser(user, json.encodeToString(toContact)) + is GroupAliasUpdated -> withUser(user, json.encodeToString(toGroup)) is ConnectionAliasUpdated -> withUser(user, json.encodeToString(toConnection)) is ContactPrefsUpdated -> withUser(user, "fromContact: $fromContact\ntoContact: \n${json.encodeToString(toContact)}") is UserContactLink -> withUser(user, contactLink.responseDetails) is UserContactLinkUpdated -> withUser(user, contactLink.responseDetails) - is UserContactLinkCreated -> withUser(user, connReqContact) + is UserContactLinkCreated -> withUser(user, json.encodeToString(connLinkContact)) is UserContactLinkDeleted -> withUser(user, noDetails()) is ContactConnected -> withUser(user, json.encodeToString(contact)) is ContactConnecting -> withUser(user, json.encodeToString(contact)) + is ContactSndReady -> withUser(user, json.encodeToString(contact)) is ReceivedContactRequest -> withUser(user, json.encodeToString(contactRequest)) is AcceptingContactRequest -> withUser(user, json.encodeToString(contact)) is ContactRequestRejected -> withUser(user, noDetails()) @@ -4088,66 +6135,78 @@ sealed class CR { is ContactSubSummary -> withUser(user, json.encodeToString(contactSubscriptions)) is NetworkStatusResp -> "networkStatus $networkStatus\nconnections: $connections" is NetworkStatuses -> withUser(user_, json.encodeToString(networkStatuses)) - is GroupSubscribed -> withUser(user, json.encodeToString(group)) - is MemberSubErrors -> withUser(user, json.encodeToString(memberSubErrors)) - is GroupEmpty -> withUser(user, json.encodeToString(group)) - is UserContactLinkSubscribed -> noDetails() - is NewChatItem -> withUser(user, json.encodeToString(chatItem)) - is ChatItemStatusUpdated -> withUser(user, json.encodeToString(chatItem)) + is NewChatItems -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) }) + is ChatItemsStatusesUpdated -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) }) is ChatItemUpdated -> withUser(user, json.encodeToString(chatItem)) is ChatItemNotChanged -> withUser(user, json.encodeToString(chatItem)) is ChatItemReaction -> withUser(user, "added: $added\n${json.encodeToString(reaction)}") - is ChatItemDeleted -> withUser(user, "deletedChatItem:\n${json.encodeToString(deletedChatItem)}\ntoChatItem:\n${json.encodeToString(toChatItem)}\nbyUser: $byUser") + is ReactionMembers -> withUser(user, "memberReactions: ${json.encodeToString(memberReactions)}") + is ChatItemsDeleted -> withUser(user, "${chatItemDeletions.map { (deletedChatItem, toChatItem) -> "deletedChatItem: ${json.encodeToString(deletedChatItem)}\ntoChatItem: ${json.encodeToString(toChatItem)}" }} \nbyUser: $byUser") + is GroupChatItemsDeleted -> withUser(user, "chatItemIDs: $chatItemIDs\nbyUser: $byUser\nmember_: $member_") + is ForwardPlan -> withUser(user, "itemsCount: $itemsCount\nchatItemIds: ${json.encodeToString(chatItemIds)}\nforwardConfirmation: ${json.encodeToString(forwardConfirmation)}") is GroupCreated -> withUser(user, json.encodeToString(groupInfo)) is SentGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmember: $member") is UserAcceptedGroupSent -> json.encodeToString(groupInfo) is GroupLinkConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember") - is UserDeletedMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member") + is BusinessLinkConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember\nfromContact: $fromContact") + is UserDeletedMembers -> withUser(user, "groupInfo: $groupInfo\nmembers: $members\nwithMessages: $withMessages") is LeftMemberUser -> withUser(user, json.encodeToString(groupInfo)) is GroupMembers -> withUser(user, json.encodeToString(group)) is ReceivedGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmemberRole: $memberRole") is GroupDeletedUser -> withUser(user, json.encodeToString(groupInfo)) is JoinedGroupMemberConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember\nmember: $member") is MemberRole -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\nmember: $member\nfromRole: $fromRole\ntoRole: $toRole") - is MemberRoleUser -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nfromRole: $fromRole\ntoRole: $toRole") - is DeletedMemberUser -> withUser(user, "groupInfo: $groupInfo\nmember: $member") - is DeletedMember -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\ndeletedMember: $deletedMember") + is MembersRoleUser -> withUser(user, "groupInfo: $groupInfo\nmembers: $members\ntoRole: $toRole") + is MemberBlockedForAll -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\nmember: $member\nblocked: $blocked") + is MembersBlockedForAllUser -> withUser(user, "groupInfo: $groupInfo\nmembers: $members\nblocked: $blocked") + is DeletedMemberUser -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nwithMessages: ${withMessages}") + is DeletedMember -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\ndeletedMember: $deletedMember\nwithMessages: ${withMessages}") is LeftMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member") is GroupDeleted -> withUser(user, "groupInfo: $groupInfo\nmember: $member") is ContactsMerged -> withUser(user, "intoContact: $intoContact\nmergedContact: $mergedContact") - is GroupInvitation -> withUser(user, json.encodeToString(groupInfo)) is UserJoinedGroup -> withUser(user, json.encodeToString(groupInfo)) is JoinedGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member") is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nmemberContact: $memberContact") - is GroupRemoved -> withUser(user, json.encodeToString(groupInfo)) is GroupUpdated -> withUser(user, json.encodeToString(toGroup)) - is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole") - is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole") + is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnLinkContact: $connLinkContact\nmemberRole: $memberRole") + is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnLinkContact: $connLinkContact\nmemberRole: $memberRole") is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo)) is NewMemberContact -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member") is NewMemberContactSentInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member") is NewMemberContactReceivedInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member") is RcvFileAcceptedSndCancelled -> withUser(user, noDetails()) + is StandaloneFileInfo -> json.encodeToString(fileMeta) + is RcvStandaloneFileCreated -> noDetails() is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem)) is RcvFileStart -> withUser(user, json.encodeToString(chatItem)) is RcvFileComplete -> withUser(user, json.encodeToString(chatItem)) - is RcvFileCancelled -> withUser(user, json.encodeToString(chatItem)) + is RcvFileCancelled -> withUser(user, json.encodeToString(chatItem_)) is RcvFileSndCancelled -> withUser(user, json.encodeToString(chatItem)) - is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize") - is RcvFileError -> withUser(user, json.encodeToString(chatItem)) - is SndFileCancelled -> json.encodeToString(chatItem) + is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem_)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize") + is RcvStandaloneFileComplete -> withUser(user, targetPath) + is RcvFileError -> withUser(user, "chatItem_: ${json.encodeToString(chatItem_)}\nagentError: ${agentError.string}\nrcvFileTransfer: $rcvFileTransfer") + is RcvFileWarning -> withUser(user, "chatItem_: ${json.encodeToString(chatItem_)}\nagentError: ${agentError.string}\nrcvFileTransfer: $rcvFileTransfer") + is SndFileCancelled -> json.encodeToString(chatItem_) + is SndStandaloneFileCreated -> noDetails() + is SndFileStartXFTP -> withUser(user, json.encodeToString(chatItem)) is SndFileComplete -> withUser(user, json.encodeToString(chatItem)) - is SndFileRcvCancelled -> withUser(user, json.encodeToString(chatItem)) + is SndFileRcvCancelled -> withUser(user, json.encodeToString(chatItem_)) is SndFileStart -> withUser(user, json.encodeToString(chatItem)) - is SndFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nsentSize: $sentSize\ntotalSize: $totalSize") + is SndFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem_)}\nsentSize: $sentSize\ntotalSize: $totalSize") + is SndFileRedirectStartXFTP -> withUser(user, json.encodeToString(redirectMeta)) is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem)) - is SndFileError -> withUser(user, json.encodeToString(chatItem)) + is SndStandaloneFileComplete -> withUser(user, rcvURIs.size.toString()) + is SndFileCancelledXFTP -> withUser(user, json.encodeToString(chatItem_)) + is SndFileError -> withUser(user, "errorMessage: ${json.encodeToString(errorMessage)}\nchatItem: ${json.encodeToString(chatItem_)}") + is SndFileWarning -> withUser(user, "errorMessage: ${json.encodeToString(errorMessage)}\nchatItem: ${json.encodeToString(chatItem_)}") + is CallInvitations -> "callInvitations: ${json.encodeToString(callInvitations)}" is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}" is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}") is CallAnswer -> withUser(user, "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}") is CallExtraInfo -> withUser(user, "contact: ${contact.id}\nextraInfo: ${json.encodeToString(extraInfo)}") is CallEnded -> withUser(user, "contact: ${contact.id}") is ContactConnectionDeleted -> withUser(user, json.encodeToString(connection)) + is ContactDisabled -> withUser(user, json.encodeToString(contact)) // remote events (mobile) is RemoteHostList -> json.encodeToString(remoteHosts) is CurrentRemoteHost -> if (remoteHost_ == null) "local" else json.encodeToString(remoteHost_) @@ -4176,14 +6235,18 @@ sealed class CR { (if (remoteCtrl_ == null) "null" else json.encodeToString(remoteCtrl_)) + "\nsessionCode: $sessionCode" is RemoteCtrlConnected -> json.encodeToString(remoteCtrl) - is RemoteCtrlStopped -> noDetails() + is RemoteCtrlStopped -> "rcsState: $rcsState\nrcsStopReason: $rcStopReason" + is ContactPQAllowed -> withUser(user, "contact: ${contact.id}\npqEncryption: $pqEncryption") + is ContactPQEnabled -> withUser(user, "contact: ${contact.id}\npqEnabled: $pqEnabled") + is AgentSubsTotal -> withUser(user, "subsTotal: ${subsTotal}\nhasSession: $hasSession") + is AgentServersSummary -> withUser(user, json.encodeToString(serversSummary)) is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" + "chat migrations: ${json.encodeToString(chatMigrations.map { it.upName })}\n\n" + "agent migrations: ${json.encodeToString(agentMigrations.map { it.upName })}" is CmdOk -> withUser(user, noDetails()) - is ChatCmdError -> withUser(user_, chatError.string) - is ChatRespError -> withUser(user_, chatError.string) + is ArchiveExported -> "${archiveErrors.map { it.string } }" is ArchiveImported -> "${archiveErrors.map { it.string } }" + is AppSettingsR -> json.encodeToString(appSettings) is Response -> json is Invalid -> str } @@ -4193,19 +6256,51 @@ sealed class CR { private fun withUser(u: UserLike?, s: String): String = if (u != null) "userId: ${u.userId}\n$s" else s } -fun chatError(r: CR): ChatErrorType? { - return ( - if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat) r.chatError.errorType - else if (r is CR.ChatRespError && r.chatError is ChatError.ChatErrorChat) r.chatError.errorType - else null - ) +fun apiChatErrorType(r: API): ChatErrorType? = + if (r is API.Error && r.err is ChatError.ChatErrorChat) r.err.errorType + else null + +@Serializable +sealed class ChatDeleteMode { + @Serializable @SerialName("full") class Full(val notify: Boolean): ChatDeleteMode() + @Serializable @SerialName("entity") class Entity(val notify: Boolean): ChatDeleteMode() + @Serializable @SerialName("messages") class Messages: ChatDeleteMode() + + val cmdString: String get() = when (this) { + is ChatDeleteMode.Full -> "full notify=${onOff(notify)}" + is ChatDeleteMode.Entity -> "entity notify=${onOff(notify)}" + is ChatDeleteMode.Messages -> "messages" + } } +@Serializable +data class CreatedConnLink(val connFullLink: String, val connShortLink: String?) { + fun simplexChatUri(short: Boolean): String = + if (short) connShortLink ?: simplexChatLink(connFullLink) + else simplexChatLink(connFullLink) + + companion object { + val nullableStateSaver: Saver<CreatedConnLink?, Pair<String?, String?>> = Saver( + save = { link -> link?.connFullLink to link?.connShortLink }, + restore = { saved -> + val connFullLink = saved.first + if (connFullLink == null) null + else CreatedConnLink(connFullLink = connFullLink, connShortLink = saved.second) + } + ) + } +} + +fun simplexChatLink(uri: String): String = + if (uri.startsWith("simplex:/")) uri.replace("simplex:/", "https://simplex.chat/") + else uri + @Serializable sealed class ConnectionPlan { @Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan() @Serializable @SerialName("contactAddress") class ContactAddress(val contactAddressPlan: ContactAddressPlan): ConnectionPlan() @Serializable @SerialName("groupLink") class GroupLink(val groupLinkPlan: GroupLinkPlan): ConnectionPlan() + @Serializable @SerialName("error") class Error(val chatError: ChatError): ConnectionPlan() } @Serializable @@ -4241,13 +6336,14 @@ abstract class TerminalItem { val date: Instant = Clock.System.now() abstract val label: String abstract val details: String + val createdAtNanos: Long = System.nanoTime() class Cmd(override val id: Long, override val remoteHostId: Long?, val cmd: CC): TerminalItem() { override val label get() = "> ${cmd.cmdString}" override val details get() = cmd.cmdString } - class Resp(override val id: Long, override val remoteHostId: Long?, val resp: CR): TerminalItem() { + class Resp(override val id: Long, override val remoteHostId: Long?, val resp: API): TerminalItem() { override val label get() = "< ${resp.responseType}" override val details get() = resp.details } @@ -4255,11 +6351,11 @@ abstract class TerminalItem { companion object { val sampleData = listOf( Cmd(0, null, CC.ShowActiveUser()), - Resp(1, null, CR.ActiveUser(User.sampleData)) + Resp(1, null, API.Result(null, CR.ActiveUser(User.sampleData))) ) fun cmd(rhId: Long?, c: CC) = Cmd(System.currentTimeMillis(), rhId, c) - fun resp(rhId: Long?, r: CR) = Resp(System.currentTimeMillis(), rhId, r) + fun resp(rhId: Long?, r: API) = Resp(System.currentTimeMillis(), rhId, r) } } @@ -4276,6 +6372,9 @@ class ConnectionStats( val ratchetSyncSendProhibited: Boolean get() = listOf(RatchetSyncState.Required, RatchetSyncState.Started, RatchetSyncState.Agreed).contains(ratchetSyncState) + + val ratchetSyncInProgress: Boolean get() = + listOf(RatchetSyncState.Started, RatchetSyncState.Agreed).contains(ratchetSyncState) } @Serializable @@ -4334,16 +6433,21 @@ enum class RatchetSyncState { } @Serializable -class UserContactLinkRec(val connReqContact: String, val autoAccept: AutoAccept? = null) { - val responseDetails: String get() = "connReqContact: ${connReqContact}\nautoAccept: ${AutoAccept.cmdString(autoAccept)}" +class UserContactLinkRec(val connLinkContact: CreatedConnLink, val autoAccept: AutoAccept? = null) { + val responseDetails: String get() = "connLinkContact: ${connLinkContact}\nautoAccept: ${AutoAccept.cmdString(autoAccept)}" } @Serializable -class AutoAccept(val acceptIncognito: Boolean, val autoReply: MsgContent?) { +class AutoAccept(val businessAddress: Boolean, val acceptIncognito: Boolean, val autoReply: MsgContent?) { companion object { fun cmdString(autoAccept: AutoAccept?): String { if (autoAccept == null) return "off" - val s = "on" + if (autoAccept.acceptIncognito) " incognito=on" else "" + var s = "on" + if (autoAccept.acceptIncognito) { + s += " incognito=on" + } else if (autoAccept.businessAddress) { + s += " business" + } val msg = autoAccept.autoReply ?: return s return s + " " + msg.cmdString } @@ -4395,6 +6499,16 @@ sealed class ChatError { @Serializable @SerialName("errorRemoteHost") class ChatErrorRemoteHost(val remoteHostError: RemoteHostError): ChatError() @Serializable @SerialName("errorRemoteCtrl") class ChatErrorRemoteCtrl(val remoteCtrlError: RemoteCtrlError): ChatError() @Serializable @SerialName("invalidJSON") class ChatErrorInvalidJSON(val json: String): ChatError() + + val resultType: String get() = when (this) { + is ChatErrorChat -> "chat" + is ChatErrorAgent -> "agent" + is ChatErrorStore -> "store" + is ChatErrorDatabase -> "database" + is ChatErrorRemoteHost -> "remoteHost" + is ChatErrorRemoteCtrl -> "remoteCtrl" + is ChatErrorInvalidJSON -> "invalid json" + } } @Serializable @@ -4416,11 +6530,13 @@ sealed class ChatErrorType { is EmptyUserPassword -> "emptyUserPassword" is UserAlreadyHidden -> "userAlreadyHidden" is UserNotHidden -> "userNotHidden" + is InvalidDisplayName -> "invalidDisplayName" is ChatNotStarted -> "chatNotStarted" is ChatNotStopped -> "chatNotStopped" is ChatStoreChanged -> "chatStoreChanged" is ConnectionPlanChatError -> "connectionPlan" is InvalidConnReq -> "invalidConnReq" + is UnsupportedConnReq -> "unsupportedConnReq" is InvalidChatMessage -> "invalidChatMessage" is ContactNotReady -> "contactNotReady" is ContactNotActive -> "contactNotActive" @@ -4437,7 +6553,6 @@ sealed class ChatErrorType { is GroupMemberNotActive -> "groupMemberNotActive" is GroupMemberUserRemoved -> "groupMemberUserRemoved" is GroupMemberNotFound -> "groupMemberNotFound" - is GroupMemberIntroNotFound -> "groupMemberIntroNotFound" is GroupCantResendInvitation -> "groupCantResendInvitation" is GroupInternal -> "groupInternal" is FileNotFound -> "fileNotFound" @@ -4447,18 +6562,18 @@ sealed class ChatErrorType { is FileCancel -> "fileCancel" is FileAlreadyExists -> "fileAlreadyExists" is FileRead -> "fileRead" - is FileWrite -> "fileWrite" + is FileWrite -> "fileWrite $message" is FileSend -> "fileSend" is FileRcvChunk -> "fileRcvChunk" is FileInternal -> "fileInternal" is FileImageType -> "fileImageType" is FileImageSize -> "fileImageSize" is FileNotReceived -> "fileNotReceived" - // is XFTPRcvFile -> "xftpRcvFile" - // is XFTPSndFile -> "xftpSndFile" + is FileNotApproved -> "fileNotApproved" is FallbackToSMPProhibited -> "fallbackToSMPProhibited" is InlineFileProhibited -> "inlineFileProhibited" is InvalidQuote -> "invalidQuote" + is InvalidForward -> "invalidForward" is InvalidChatItemUpdate -> "invalidChatItemUpdate" is InvalidChatItemDelete -> "invalidChatItemDelete" is HasCurrentCall -> "hasCurrentCall" @@ -4473,6 +6588,7 @@ sealed class ChatErrorType { is AgentCommandError -> "agentCommandError" is InvalidFileDescription -> "invalidFileDescription" is ConnectionIncognitoChangeProhibited -> "connectionIncognitoChangeProhibited" + is ConnectionUserChangeProhibited -> "connectionUserChangeProhibited" is PeerChatVRangeIncompatible -> "peerChatVRangeIncompatible" is InternalError -> "internalError" is CEException -> "exception $message" @@ -4493,11 +6609,13 @@ sealed class ChatErrorType { @Serializable @SerialName("emptyUserPassword") class EmptyUserPassword(val userId: Long): ChatErrorType() @Serializable @SerialName("userAlreadyHidden") class UserAlreadyHidden(val userId: Long): ChatErrorType() @Serializable @SerialName("userNotHidden") class UserNotHidden(val userId: Long): ChatErrorType() + @Serializable @SerialName("invalidDisplayName") object InvalidDisplayName: ChatErrorType() @Serializable @SerialName("chatNotStarted") object ChatNotStarted: ChatErrorType() @Serializable @SerialName("chatNotStopped") object ChatNotStopped: ChatErrorType() @Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType() @Serializable @SerialName("connectionPlan") class ConnectionPlanChatError(val connectionPlan: ConnectionPlan): ChatErrorType() @Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType() + @Serializable @SerialName("unsupportedConnReq") object UnsupportedConnReq: ChatErrorType() @Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType() @Serializable @SerialName("contactNotReady") class ContactNotReady(val contact: Contact): ChatErrorType() @Serializable @SerialName("contactNotActive") class ContactNotActive(val contact: Contact): ChatErrorType() @@ -4514,7 +6632,6 @@ sealed class ChatErrorType { @Serializable @SerialName("groupMemberNotActive") object GroupMemberNotActive: ChatErrorType() @Serializable @SerialName("groupMemberUserRemoved") object GroupMemberUserRemoved: ChatErrorType() @Serializable @SerialName("groupMemberNotFound") object GroupMemberNotFound: ChatErrorType() - @Serializable @SerialName("groupMemberIntroNotFound") class GroupMemberIntroNotFound(val contactName: String): ChatErrorType() @Serializable @SerialName("groupCantResendInvitation") class GroupCantResendInvitation(val groupInfo: GroupInfo, val contactName: String): ChatErrorType() @Serializable @SerialName("groupInternal") class GroupInternal(val message: String): ChatErrorType() @Serializable @SerialName("fileNotFound") class FileNotFound(val message: String): ChatErrorType() @@ -4531,11 +6648,11 @@ sealed class ChatErrorType { @Serializable @SerialName("fileImageType") class FileImageType(val filePath: String): ChatErrorType() @Serializable @SerialName("fileImageSize") class FileImageSize(val filePath: String): ChatErrorType() @Serializable @SerialName("fileNotReceived") class FileNotReceived(val fileId: Long): ChatErrorType() - // @Serializable @SerialName("xFTPRcvFile") object XFTPRcvFile: ChatErrorType() - // @Serializable @SerialName("xFTPSndFile") object XFTPSndFile: ChatErrorType() + @Serializable @SerialName("fileNotApproved") class FileNotApproved(val fileId: Long, val unknownServers: List<String>): ChatErrorType() @Serializable @SerialName("fallbackToSMPProhibited") class FallbackToSMPProhibited(val fileId: Long): ChatErrorType() @Serializable @SerialName("inlineFileProhibited") class InlineFileProhibited(val fileId: Long): ChatErrorType() @Serializable @SerialName("invalidQuote") object InvalidQuote: ChatErrorType() + @Serializable @SerialName("invalidForward") object InvalidForward: ChatErrorType() @Serializable @SerialName("invalidChatItemUpdate") object InvalidChatItemUpdate: ChatErrorType() @Serializable @SerialName("invalidChatItemDelete") object InvalidChatItemDelete: ChatErrorType() @Serializable @SerialName("hasCurrentCall") object HasCurrentCall: ChatErrorType() @@ -4550,6 +6667,7 @@ sealed class ChatErrorType { @Serializable @SerialName("agentCommandError") class AgentCommandError(val message: String): ChatErrorType() @Serializable @SerialName("invalidFileDescription") class InvalidFileDescription(val message: String): ChatErrorType() @Serializable @SerialName("connectionIncognitoChangeProhibited") object ConnectionIncognitoChangeProhibited: ChatErrorType() + @Serializable @SerialName("connectionUserChangeProhibited") object ConnectionUserChangeProhibited: ChatErrorType() @Serializable @SerialName("peerChatVRangeIncompatible") object PeerChatVRangeIncompatible: ChatErrorType() @Serializable @SerialName("internalError") class InternalError(val message: String): ChatErrorType() @Serializable @SerialName("exception") class CEException(val message: String): ChatErrorType() @@ -4560,61 +6678,79 @@ sealed class StoreError { val string: String get() = when (this) { is DuplicateName -> "duplicateName" - is UserNotFound -> "userNotFound" - is UserNotFoundByName -> "userNotFoundByName" - is UserNotFoundByContactId -> "userNotFoundByContactId" - is UserNotFoundByGroupId -> "userNotFoundByGroupId" - is UserNotFoundByFileId -> "userNotFoundByFileId" - is UserNotFoundByContactRequestId -> "userNotFoundByContactRequestId" - is ContactNotFound -> "contactNotFound" - is ContactNotFoundByName -> "contactNotFoundByName" - is ContactNotFoundByMemberId -> "contactNotFoundByMemberId" - is ContactNotReady -> "contactNotReady" + is UserNotFound -> "userNotFound $userId" + is UserNotFoundByName -> "userNotFoundByName $contactName" + is UserNotFoundByContactId -> "userNotFoundByContactId $contactId" + is UserNotFoundByGroupId -> "userNotFoundByGroupId $groupId" + is UserNotFoundByFileId -> "userNotFoundByFileId $fileId" + is UserNotFoundByContactRequestId -> "userNotFoundByContactRequestId $contactRequestId" + is ContactNotFound -> "contactNotFound $contactId" + is ContactNotFoundByName -> "contactNotFoundByName $contactName" + is ContactNotFoundByMemberId -> "contactNotFoundByMemberId $groupMemberId" + is ContactNotReady -> "contactNotReady $contactName" is DuplicateContactLink -> "duplicateContactLink" is UserContactLinkNotFound -> "userContactLinkNotFound" - is ContactRequestNotFound -> "contactRequestNotFound" - is ContactRequestNotFoundByName -> "contactRequestNotFoundByName" - is GroupNotFound -> "groupNotFound" - is GroupNotFoundByName -> "groupNotFoundByName" - is GroupMemberNameNotFound -> "groupMemberNameNotFound" - is GroupMemberNotFound -> "groupMemberNotFound" - is GroupMemberNotFoundByMemberId -> "groupMemberNotFoundByMemberId" - is MemberContactGroupMemberNotFound -> "memberContactGroupMemberNotFound" + is ContactRequestNotFound -> "contactRequestNotFound $contactRequestId" + is ContactRequestNotFoundByName -> "contactRequestNotFoundByName $contactName" + is GroupNotFound -> "groupNotFound $groupId" + is GroupNotFoundByName -> "groupNotFoundByName $groupName" + is GroupMemberNameNotFound -> "groupMemberNameNotFound $groupId $groupMemberName" + is GroupMemberNotFound -> "groupMemberNotFound $groupMemberId" + is GroupMemberNotFoundByMemberId -> "groupMemberNotFoundByMemberId $memberId" + is MemberContactGroupMemberNotFound -> "memberContactGroupMemberNotFound $contactId" is GroupWithoutUser -> "groupWithoutUser" is DuplicateGroupMember -> "duplicateGroupMember" is GroupAlreadyJoined -> "groupAlreadyJoined" is GroupInvitationNotFound -> "groupInvitationNotFound" - is SndFileNotFound -> "sndFileNotFound" - is SndFileInvalid -> "sndFileInvalid" - is RcvFileNotFound -> "rcvFileNotFound" - is RcvFileDescrNotFound -> "rcvFileDescrNotFound" - is FileNotFound -> "fileNotFound" - is RcvFileInvalid -> "rcvFileInvalid" + is NoteFolderAlreadyExists -> "noteFolderAlreadyExists $noteFolderId" + is NoteFolderNotFound -> "noteFolderNotFound $noteFolderId" + is UserNoteFolderNotFound -> "userNoteFolderNotFound" + is SndFileNotFound -> "sndFileNotFound $fileId" + is SndFileInvalid -> "sndFileInvalid $fileId" + is RcvFileNotFound -> "rcvFileNotFound $fileId" + is RcvFileDescrNotFound -> "rcvFileDescrNotFound $fileId" + is FileNotFound -> "fileNotFound $fileId" + is RcvFileInvalid -> "rcvFileInvalid $fileId" is RcvFileInvalidDescrPart -> "rcvFileInvalidDescrPart" - is SharedMsgIdNotFoundByFileId -> "sharedMsgIdNotFoundByFileId" - is FileIdNotFoundBySharedMsgId -> "fileIdNotFoundBySharedMsgId" - is SndFileNotFoundXFTP -> "sndFileNotFoundXFTP" - is RcvFileNotFoundXFTP -> "rcvFileNotFoundXFTP" - is ConnectionNotFound -> "connectionNotFound" - is ConnectionNotFoundById -> "connectionNotFoundById" - is ConnectionNotFoundByMemberId -> "connectionNotFoundByMemberId" - is PendingConnectionNotFound -> "pendingConnectionNotFound" + is LocalFileNoTransfer -> "localFileNoTransfer $fileId" + is SharedMsgIdNotFoundByFileId -> "sharedMsgIdNotFoundByFileId $fileId" + is FileIdNotFoundBySharedMsgId -> "fileIdNotFoundBySharedMsgId $sharedMsgId" + is SndFileNotFoundXFTP -> "sndFileNotFoundXFTP $agentSndFileId" + is RcvFileNotFoundXFTP -> "rcvFileNotFoundXFTP $agentRcvFileId" + is ConnectionNotFound -> "connectionNotFound $agentConnId" + is ConnectionNotFoundById -> "connectionNotFoundById $connId" + is ConnectionNotFoundByMemberId -> "connectionNotFoundByMemberId $groupMemberId" + is PendingConnectionNotFound -> "pendingConnectionNotFound $connId" is IntroNotFound -> "introNotFound" is UniqueID -> "uniqueID" - is InternalError -> "internalError" - is NoMsgDelivery -> "noMsgDelivery" - is BadChatItem -> "badChatItem" - is ChatItemNotFound -> "chatItemNotFound" - is ChatItemNotFoundByText -> "chatItemNotFoundByText" - is ChatItemSharedMsgIdNotFound -> "chatItemSharedMsgIdNotFound" - is ChatItemNotFoundByFileId -> "chatItemNotFoundByFileId" - is ChatItemNotFoundByGroupId -> "chatItemNotFoundByGroupId" - is ProfileNotFound -> "profileNotFound" - is DuplicateGroupLink -> "duplicateGroupLink" - is GroupLinkNotFound -> "groupLinkNotFound" - is HostMemberIdNotFound -> "hostMemberIdNotFound" - is ContactNotFoundByFileId -> "contactNotFoundByFileId" - is NoGroupSndStatus -> "noGroupSndStatus" + is LargeMsg -> "largeMsg" + is InternalError -> "internalError $message" + is DBException -> "dBException $message" + is DBBusyError -> "dBBusyError $message" + is BadChatItem -> "badChatItem $itemId" + is ChatItemNotFound -> "chatItemNotFound $itemId" + is ChatItemNotFoundByText -> "chatItemNotFoundByText $text" + is ChatItemSharedMsgIdNotFound -> "chatItemSharedMsgIdNotFound $sharedMsgId" + is ChatItemNotFoundByFileId -> "chatItemNotFoundByFileId $fileId" + is ChatItemNotFoundByContactId -> "chatItemNotFoundByContactId $contactId" + is ChatItemNotFoundByGroupId -> "chatItemNotFoundByGroupId $groupId" + is ProfileNotFound -> "profileNotFound $profileId" + is DuplicateGroupLink -> "duplicateGroupLink ${groupInfo.groupId}" + is GroupLinkNotFound -> "groupLinkNotFound ${groupInfo.groupId}" + is HostMemberIdNotFound -> "hostMemberIdNotFound $groupId" + is ContactNotFoundByFileId -> "contactNotFoundByFileId $fileId" + is NoGroupSndStatus -> "noGroupSndStatus $itemId $groupMemberId" + is DuplicateGroupMessage -> "duplicateGroupMessage $groupId $sharedMsgId $authorGroupMemberId $authorGroupMemberId" + is RemoteHostNotFound -> "remoteHostNotFound $remoteHostId" + is RemoteHostUnknown -> "remoteHostUnknown" + is RemoteHostDuplicateCA -> "remoteHostDuplicateCA" + is RemoteCtrlNotFound -> "remoteCtrlNotFound $remoteCtrlId" + is RemoteCtrlDuplicateCA -> "remoteCtrlDuplicateCA" + is ProhibitedDeleteUser -> "prohibitedDeleteUser $userId $contactId" + is OperatorNotFound -> "operatorNotFound $serverOperatorId" + is UsageConditionsNotFound -> "usageConditionsNotFound" + is InvalidQuote -> "invalidQuote" + is InvalidMention -> "invalidMention" } @Serializable @SerialName("duplicateName") object DuplicateName: StoreError() @@ -4642,6 +6778,9 @@ sealed class StoreError { @Serializable @SerialName("duplicateGroupMember") object DuplicateGroupMember: StoreError() @Serializable @SerialName("groupAlreadyJoined") object GroupAlreadyJoined: StoreError() @Serializable @SerialName("groupInvitationNotFound") object GroupInvitationNotFound: StoreError() + @Serializable @SerialName("noteFolderAlreadyExists") class NoteFolderAlreadyExists(val noteFolderId: Long): StoreError() + @Serializable @SerialName("noteFolderNotFound") class NoteFolderNotFound(val noteFolderId: Long): StoreError() + @Serializable @SerialName("userNoteFolderNotFound") object UserNoteFolderNotFound: StoreError() @Serializable @SerialName("sndFileNotFound") class SndFileNotFound(val fileId: Long): StoreError() @Serializable @SerialName("sndFileInvalid") class SndFileInvalid(val fileId: Long): StoreError() @Serializable @SerialName("rcvFileNotFound") class RcvFileNotFound(val fileId: Long): StoreError() @@ -4649,6 +6788,7 @@ sealed class StoreError { @Serializable @SerialName("fileNotFound") class FileNotFound(val fileId: Long): StoreError() @Serializable @SerialName("rcvFileInvalid") class RcvFileInvalid(val fileId: Long): StoreError() @Serializable @SerialName("rcvFileInvalidDescrPart") object RcvFileInvalidDescrPart: StoreError() + @Serializable @SerialName("localFileNoTransfer") class LocalFileNoTransfer(val fileId: Long): StoreError() @Serializable @SerialName("sharedMsgIdNotFoundByFileId") class SharedMsgIdNotFoundByFileId(val fileId: Long): StoreError() @Serializable @SerialName("fileIdNotFoundBySharedMsgId") class FileIdNotFoundBySharedMsgId(val sharedMsgId: String): StoreError() @Serializable @SerialName("sndFileNotFoundXFTP") class SndFileNotFoundXFTP(val agentSndFileId: String): StoreError() @@ -4659,13 +6799,16 @@ sealed class StoreError { @Serializable @SerialName("pendingConnectionNotFound") class PendingConnectionNotFound(val connId: Long): StoreError() @Serializable @SerialName("introNotFound") object IntroNotFound: StoreError() @Serializable @SerialName("uniqueID") object UniqueID: StoreError() + @Serializable @SerialName("largeMsg") object LargeMsg: StoreError() @Serializable @SerialName("internalError") class InternalError(val message: String): StoreError() - @Serializable @SerialName("noMsgDelivery") class NoMsgDelivery(val connId: Long, val agentMsgId: String): StoreError() + @Serializable @SerialName("dBException") class DBException(val message: String): StoreError() + @Serializable @SerialName("dBBusyError") class DBBusyError(val message: String): StoreError() @Serializable @SerialName("badChatItem") class BadChatItem(val itemId: Long): StoreError() @Serializable @SerialName("chatItemNotFound") class ChatItemNotFound(val itemId: Long): StoreError() @Serializable @SerialName("chatItemNotFoundByText") class ChatItemNotFoundByText(val text: String): StoreError() @Serializable @SerialName("chatItemSharedMsgIdNotFound") class ChatItemSharedMsgIdNotFound(val sharedMsgId: String): StoreError() @Serializable @SerialName("chatItemNotFoundByFileId") class ChatItemNotFoundByFileId(val fileId: Long): StoreError() + @Serializable @SerialName("chatItemNotFoundByContactId") class ChatItemNotFoundByContactId(val contactId: Long): StoreError() @Serializable @SerialName("chatItemNotFoundByGroupId") class ChatItemNotFoundByGroupId(val groupId: Long): StoreError() @Serializable @SerialName("profileNotFound") class ProfileNotFound(val profileId: Long): StoreError() @Serializable @SerialName("duplicateGroupLink") class DuplicateGroupLink(val groupInfo: GroupInfo): StoreError() @@ -4673,6 +6816,17 @@ sealed class StoreError { @Serializable @SerialName("hostMemberIdNotFound") class HostMemberIdNotFound(val groupId: Long): StoreError() @Serializable @SerialName("contactNotFoundByFileId") class ContactNotFoundByFileId(val fileId: Long): StoreError() @Serializable @SerialName("noGroupSndStatus") class NoGroupSndStatus(val itemId: Long, val groupMemberId: Long): StoreError() + @Serializable @SerialName("duplicateGroupMessage") class DuplicateGroupMessage(val groupId: Long, val sharedMsgId: String, val authorGroupMemberId: Long?, val forwardedByGroupMemberId: Long?): StoreError() + @Serializable @SerialName("remoteHostNotFound") class RemoteHostNotFound(val remoteHostId: Long): StoreError() + @Serializable @SerialName("remoteHostUnknown") object RemoteHostUnknown: StoreError() + @Serializable @SerialName("remoteHostDuplicateCA") object RemoteHostDuplicateCA: StoreError() + @Serializable @SerialName("remoteCtrlNotFound") class RemoteCtrlNotFound(val remoteCtrlId: Long): StoreError() + @Serializable @SerialName("remoteCtrlDuplicateCA") class RemoteCtrlDuplicateCA: StoreError() + @Serializable @SerialName("prohibitedDeleteUser") class ProhibitedDeleteUser(val userId: Long, val contactId: Long): StoreError() + @Serializable @SerialName("operatorNotFound") class OperatorNotFound(val serverOperatorId: Long): StoreError() + @Serializable @SerialName("usageConditionsNotFound") object UsageConditionsNotFound: StoreError() + @Serializable @SerialName("invalidQuote") object InvalidQuote: StoreError() + @Serializable @SerialName("invalidMention") object InvalidMention: StoreError() } @Serializable @@ -4694,32 +6848,36 @@ sealed class DatabaseError { @Serializable sealed class SQLiteError { @Serializable @SerialName("errorNotADatabase") object ErrorNotADatabase: SQLiteError() - @Serializable @SerialName("error") class Error(val error: String): SQLiteError() + @Serializable @SerialName("error") class Error(val dbError: String): SQLiteError() } @Serializable sealed class AgentErrorType { val string: String get() = when (this) { - is CMD -> "CMD ${cmdErr.string}" + is CMD -> "CMD ${cmdErr.string} $errContext" is CONN -> "CONN ${connErr.string}" is SMP -> "SMP ${smpErr.string}" // is NTF -> "NTF ${ntfErr.string}" is XFTP -> "XFTP ${xftpErr.string}" + is PROXY -> "PROXY $proxyServer $relayServer ${proxyErr.string}" is RCP -> "RCP ${rcpErr.string}" is BROKER -> "BROKER ${brokerErr.string}" is AGENT -> "AGENT ${agentErr.string}" is INTERNAL -> "INTERNAL $internalErr" + is CRITICAL -> "CRITICAL $offerRestart $criticalErr" is INACTIVE -> "INACTIVE" } - @Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType): AgentErrorType() + @Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType, val errContext: String): AgentErrorType() @Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType() - @Serializable @SerialName("SMP") class SMP(val smpErr: SMPErrorType): AgentErrorType() + @Serializable @SerialName("SMP") class SMP(val serverAddress: String, val smpErr: SMPErrorType): AgentErrorType() // @Serializable @SerialName("NTF") class NTF(val ntfErr: SMPErrorType): AgentErrorType() @Serializable @SerialName("XFTP") class XFTP(val xftpErr: XFTPErrorType): AgentErrorType() + @Serializable @SerialName("PROXY") class PROXY(val proxyServer: String, val relayServer: String, val proxyErr: ProxyClientError): AgentErrorType() @Serializable @SerialName("RCP") class RCP(val rcpErr: RCErrorType): AgentErrorType() @Serializable @SerialName("BROKER") class BROKER(val brokerAddress: String, val brokerErr: BrokerErrorType): AgentErrorType() @Serializable @SerialName("AGENT") class AGENT(val agentErr: SMPAgentError): AgentErrorType() @Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType() + @Serializable @SerialName("CRITICAL") data class CRITICAL(val offerRestart: Boolean, val criticalErr: String): AgentErrorType() @Serializable @SerialName("INACTIVE") object INACTIVE: AgentErrorType() } @@ -4773,28 +6931,69 @@ sealed class BrokerErrorType { @Serializable @SerialName("TIMEOUT") object TIMEOUT: BrokerErrorType() } +// ProtocolErrorType @Serializable sealed class SMPErrorType { val string: String get() = when (this) { is BLOCK -> "BLOCK" is SESSION -> "SESSION" is CMD -> "CMD ${cmdErr.string}" + is PROXY -> "PROXY ${proxyErr.string}" is AUTH -> "AUTH" + is BLOCKED -> "BLOCKED ${json.encodeToString(blockInfo)}" + is CRYPTO -> "CRYPTO" is QUOTA -> "QUOTA" + is STORE -> "STORE $storeErr" is NO_MSG -> "NO_MSG" is LARGE_MSG -> "LARGE_MSG" + is EXPIRED -> "EXPIRED" is INTERNAL -> "INTERNAL" } @Serializable @SerialName("BLOCK") class BLOCK: SMPErrorType() @Serializable @SerialName("SESSION") class SESSION: SMPErrorType() @Serializable @SerialName("CMD") class CMD(val cmdErr: ProtocolCommandError): SMPErrorType() + @Serializable @SerialName("PROXY") class PROXY(val proxyErr: ProxyError): SMPErrorType() @Serializable @SerialName("AUTH") class AUTH: SMPErrorType() + @Serializable @SerialName("BLOCKED") class BLOCKED(val blockInfo: BlockingInfo): SMPErrorType() + @Serializable @SerialName("CRYPTO") class CRYPTO: SMPErrorType() @Serializable @SerialName("QUOTA") class QUOTA: SMPErrorType() + @Serializable @SerialName("STORE") class STORE(val storeErr: String): SMPErrorType() @Serializable @SerialName("NO_MSG") class NO_MSG: SMPErrorType() @Serializable @SerialName("LARGE_MSG") class LARGE_MSG: SMPErrorType() + @Serializable @SerialName("EXPIRED") class EXPIRED: SMPErrorType() @Serializable @SerialName("INTERNAL") class INTERNAL: SMPErrorType() } +@Serializable +sealed class ProxyError { + val string: String get() = when (this) { + is PROTOCOL -> "PROTOCOL ${protocolErr.string}" + is BROKER -> "BROKER ${brokerErr.string}" + is BASIC_AUTH -> "BASIC_AUTH" + is NO_SESSION -> "NO_SESSION" + } + @Serializable @SerialName("PROTOCOL") class PROTOCOL(val protocolErr: SMPErrorType): ProxyError() + @Serializable @SerialName("BROKER") class BROKER(val brokerErr: BrokerErrorType): ProxyError() + @Serializable @SerialName("BASIC_AUTH") class BASIC_AUTH: ProxyError() + @Serializable @SerialName("NO_SESSION") class NO_SESSION: ProxyError() +} + +@Serializable +data class BlockingInfo( + val reason: BlockingReason +) + +@Serializable +enum class BlockingReason { + @SerialName("spam") Spam, + @SerialName("content") Content; + + val text: String get() = when (this) { + Spam -> generalGetString(MR.strings.blocking_reason_spam) + Content -> generalGetString(MR.strings.blocking_reason_content) + } +} + @Serializable sealed class ProtocolCommandError { val string: String get() = when (this) { @@ -4817,13 +7016,17 @@ sealed class ProtocolCommandError { sealed class SMPTransportError { val string: String get() = when (this) { is BadBlock -> "badBlock" + is Version -> "version" is LargeMsg -> "largeMsg" is BadSession -> "badSession" + is NoServerAuth -> "noServerAuth" is Handshake -> "handshake ${handshakeErr.string}" } @Serializable @SerialName("badBlock") class BadBlock: SMPTransportError() + @Serializable @SerialName("version") class Version: SMPTransportError() @Serializable @SerialName("largeMsg") class LargeMsg: SMPTransportError() @Serializable @SerialName("badSession") class BadSession: SMPTransportError() + @Serializable @SerialName("noServerAuth") class NoServerAuth: SMPTransportError() @Serializable @SerialName("handshake") class Handshake(val handshakeErr: SMPHandshakeError): SMPTransportError() } @@ -4833,10 +7036,12 @@ sealed class SMPHandshakeError { is PARSE -> "PARSE" is VERSION -> "VERSION" is IDENTITY -> "IDENTITY" + is BAD_AUTH -> "BAD_AUTH" } @Serializable @SerialName("PARSE") class PARSE: SMPHandshakeError() @Serializable @SerialName("VERSION") class VERSION: SMPHandshakeError() @Serializable @SerialName("IDENTITY") class IDENTITY: SMPHandshakeError() + @Serializable @SerialName("BAD_AUTH") class BAD_AUTH: SMPHandshakeError() } @Serializable @@ -4864,6 +7069,7 @@ sealed class XFTPErrorType { is SESSION -> "SESSION" is CMD -> "CMD ${cmdErr.string}" is AUTH -> "AUTH" + is BLOCKED -> "BLOCKED ${json.encodeToString(blockInfo)}" is SIZE -> "SIZE" is QUOTA -> "QUOTA" is DIGEST -> "DIGEST" @@ -4871,12 +7077,15 @@ sealed class XFTPErrorType { is NO_FILE -> "NO_FILE" is HAS_FILE -> "HAS_FILE" is FILE_IO -> "FILE_IO" + is TIMEOUT -> "TIMEOUT" + is REDIRECT -> "REDIRECT" is INTERNAL -> "INTERNAL" } @Serializable @SerialName("BLOCK") object BLOCK: XFTPErrorType() @Serializable @SerialName("SESSION") object SESSION: XFTPErrorType() @Serializable @SerialName("CMD") class CMD(val cmdErr: ProtocolCommandError): XFTPErrorType() @Serializable @SerialName("AUTH") object AUTH: XFTPErrorType() + @Serializable @SerialName("BLOCKED") class BLOCKED(val blockInfo: BlockingInfo): XFTPErrorType() @Serializable @SerialName("SIZE") object SIZE: XFTPErrorType() @Serializable @SerialName("QUOTA") object QUOTA: XFTPErrorType() @Serializable @SerialName("DIGEST") object DIGEST: XFTPErrorType() @@ -4884,15 +7093,31 @@ sealed class XFTPErrorType { @Serializable @SerialName("NO_FILE") object NO_FILE: XFTPErrorType() @Serializable @SerialName("HAS_FILE") object HAS_FILE: XFTPErrorType() @Serializable @SerialName("FILE_IO") object FILE_IO: XFTPErrorType() + @Serializable @SerialName("TIMEOUT") object TIMEOUT: XFTPErrorType() + @Serializable @SerialName("REDIRECT") class REDIRECT(val redirectError: String): XFTPErrorType() @Serializable @SerialName("INTERNAL") object INTERNAL: XFTPErrorType() } +@Serializable +sealed class ProxyClientError { + val string: String get() = when (this) { + is ProxyProtocolError -> "ProxyProtocolError $protocolErr" + is ProxyUnexpectedResponse -> "ProxyUnexpectedResponse $responseStr" + is ProxyResponseError -> "ProxyResponseError $responseErr" + } + @Serializable @SerialName("protocolError") class ProxyProtocolError(val protocolErr: SMPErrorType): ProxyClientError() + @Serializable @SerialName("unexpectedResponse") class ProxyUnexpectedResponse(val responseStr: String): ProxyClientError() + @Serializable @SerialName("responseError") class ProxyResponseError(val responseErr: SMPErrorType): ProxyClientError() +} + @Serializable sealed class RCErrorType { val string: String get() = when (this) { is INTERNAL -> "INTERNAL $internalErr" is IDENTITY -> "IDENTITY" is NO_LOCAL_ADDRESS -> "NO_LOCAL_ADDRESS" + is NEW_CONTROLLER -> "NEW_CONTROLLER" + is NOT_DISCOVERED -> "NOT_DISCOVERED" is TLS_START_FAILED -> "TLS_START_FAILED" is EXCEPTION -> "EXCEPTION $EXCEPTION" is CTRL_AUTH -> "CTRL_AUTH" @@ -4907,6 +7132,8 @@ sealed class RCErrorType { @Serializable @SerialName("internal") data class INTERNAL(val internalErr: String): RCErrorType() @Serializable @SerialName("identity") object IDENTITY: RCErrorType() @Serializable @SerialName("noLocalAddress") object NO_LOCAL_ADDRESS: RCErrorType() + @Serializable @SerialName("newController") object NEW_CONTROLLER: RCErrorType() + @Serializable @SerialName("notDiscovered") object NOT_DISCOVERED: RCErrorType() @Serializable @SerialName("tlsStartFailed") object TLS_START_FAILED: RCErrorType() @Serializable @SerialName("exception") data class EXCEPTION(val exception: String): RCErrorType() @Serializable @SerialName("ctrlAuth") object CTRL_AUTH: RCErrorType() @@ -4922,11 +7149,11 @@ sealed class RCErrorType { @Serializable sealed class ArchiveError { val string: String get() = when (this) { - is ArchiveErrorImport -> "import ${chatError.string}" - is ArchiveErrorImportFile -> "importFile $file ${chatError.string}" + is ArchiveErrorImport -> "import ${importError}" + is ArchiveErrorFile -> "importFile $file ${fileError}" } - @Serializable @SerialName("import") class ArchiveErrorImport(val chatError: ChatError): ArchiveError() - @Serializable @SerialName("importFile") class ArchiveErrorImportFile(val file: String, val chatError: ChatError): ArchiveError() + @Serializable @SerialName("import") class ArchiveErrorImport(val importError: String): ArchiveError() + @Serializable @SerialName("fileError") class ArchiveErrorFile(val file: String, val fileError: String): ArchiveError() } @Serializable @@ -4940,6 +7167,15 @@ sealed class RemoteHostError { is BadVersion -> "badVersion" is Disconnected -> "disconnected" } + fun localizedString(name: String): String = when (this) { + is Missing -> generalGetString(MR.strings.remote_host_error_missing) + is Inactive -> generalGetString(MR.strings.remote_host_error_inactive) + is Busy -> generalGetString(MR.strings.remote_host_error_busy) + is Timeout -> generalGetString(MR.strings.remote_host_error_timeout) + is BadState -> generalGetString(MR.strings.remote_host_error_bad_state) + is BadVersion -> generalGetString(MR.strings.remote_host_error_bad_version) + is Disconnected -> generalGetString(MR.strings.remote_host_error_disconnected) + }.format(name) @Serializable @SerialName("missing") object Missing: RemoteHostError() @Serializable @SerialName("inactive") object Inactive: RemoteHostError() @Serializable @SerialName("busy") object Busy: RemoteHostError() @@ -4956,18 +7192,39 @@ sealed class RemoteCtrlError { is BadState -> "badState" is Busy -> "busy" is Timeout -> "timeout" + is NoKnownControllers -> "noKnownControllers" + is BadController -> "badController" is Disconnected -> "disconnected" is BadInvitation -> "badInvitation" is BadVersion -> "badVersion" + is HTTP2Error -> "http2Error" + is ProtocolError -> "protocolError" } + val localizedString: String get() = when (this) { + is Inactive -> generalGetString(MR.strings.remote_ctrl_error_inactive) + is BadState -> generalGetString(MR.strings.remote_ctrl_error_bad_state) + is Busy -> generalGetString(MR.strings.remote_ctrl_error_busy) + is Timeout -> generalGetString(MR.strings.remote_ctrl_error_timeout) + is NoKnownControllers -> "no known controllers" + is BadController -> "bad controller" + is Disconnected -> generalGetString(MR.strings.remote_ctrl_error_disconnected) + is BadInvitation -> generalGetString(MR.strings.remote_ctrl_error_bad_invitation) + is BadVersion -> generalGetString(MR.strings.remote_ctrl_error_bad_version) + is HTTP2Error -> "HTTP2 error" + is ProtocolError -> "protocol error" + } + @Serializable @SerialName("inactive") object Inactive: RemoteCtrlError() @Serializable @SerialName("badState") object BadState: RemoteCtrlError() @Serializable @SerialName("busy") object Busy: RemoteCtrlError() @Serializable @SerialName("timeout") object Timeout: RemoteCtrlError() + @Serializable @SerialName("noKnownControllers") object NoKnownControllers: RemoteCtrlError() + @Serializable @SerialName("badController") object BadController: RemoteCtrlError() @Serializable @SerialName("disconnected") class Disconnected(val remoteCtrlId: Long, val reason: String): RemoteCtrlError() @Serializable @SerialName("badInvitation") object BadInvitation: RemoteCtrlError() @Serializable @SerialName("badVersion") data class BadVersion(val appVersion: String): RemoteCtrlError() - //@Serializable @SerialName("protocolError") data class ProtocolError(val protocolError: RemoteProtocolError): RemoteCtrlError() + @Serializable @SerialName("hTTP2Error") data class HTTP2Error(val http2Error: String): RemoteCtrlError() + @Serializable @SerialName("protocolError") object ProtocolError: RemoteCtrlError() } enum class NotificationsMode() { @@ -4977,3 +7234,362 @@ enum class NotificationsMode() { val default: NotificationsMode = SERVICE } } + +@Serializable +enum class PrivacyChatListOpenLinksMode { + @SerialName("yes") YES, + @SerialName("no") NO, + @SerialName("ask") ASK +} + +@Serializable +data class AppSettings( + var networkConfig: NetCfg? = null, + var networkProxy: NetworkProxy? = null, + var privacyEncryptLocalFiles: Boolean? = null, + var privacyAskToApproveRelays: Boolean? = null, + var privacyAcceptImages: Boolean? = null, + var privacyLinkPreviews: Boolean? = null, + var privacyChatListOpenLinks: PrivacyChatListOpenLinksMode? = null, + var privacyShowChatPreviews: Boolean? = null, + var privacySaveLastDraft: Boolean? = null, + var privacyProtectScreen: Boolean? = null, + var privacyMediaBlurRadius: Int? = null, + var notificationMode: AppSettingsNotificationMode? = null, + var notificationPreviewMode: AppSettingsNotificationPreviewMode? = null, + var webrtcPolicyRelay: Boolean? = null, + var webrtcICEServers: List<String>? = null, + var confirmRemoteSessions: Boolean? = null, + var connectRemoteViaMulticast: Boolean? = null, + var connectRemoteViaMulticastAuto: Boolean? = null, + var developerTools: Boolean? = null, + var confirmDBUpgrades: Boolean? = null, + var androidCallOnLockScreen: AppSettingsLockScreenCalls? = null, + var iosCallKitEnabled: Boolean? = null, + var iosCallKitCallsInRecents: Boolean? = null, + var uiProfileImageCornerRadius: Float? = null, + var uiChatItemRoundness: Float? = null, + var uiChatItemTail: Boolean? = null, + var uiColorScheme: String? = null, + var uiDarkColorScheme: String? = null, + var uiCurrentThemeIds: Map<String, String>? = null, + var uiThemes: List<ThemeOverrides>? = null, + var oneHandUI: Boolean? = null, + var chatBottomBar: Boolean? = null +) { + fun prepareForExport(): AppSettings { + val empty = AppSettings() + val def = defaults + if (networkConfig != def.networkConfig) { empty.networkConfig = networkConfig } + if (networkProxy != def.networkProxy) { empty.networkProxy = networkProxy } + if (privacyEncryptLocalFiles != def.privacyEncryptLocalFiles) { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles } + if (privacyAskToApproveRelays != def.privacyAskToApproveRelays) { empty.privacyAskToApproveRelays = privacyAskToApproveRelays } + if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages } + if (privacyLinkPreviews != def.privacyLinkPreviews) { empty.privacyLinkPreviews = privacyLinkPreviews } + if (privacyChatListOpenLinks != def.privacyChatListOpenLinks) { empty.privacyChatListOpenLinks = privacyChatListOpenLinks } + if (privacyShowChatPreviews != def.privacyShowChatPreviews) { empty.privacyShowChatPreviews = privacyShowChatPreviews } + if (privacySaveLastDraft != def.privacySaveLastDraft) { empty.privacySaveLastDraft = privacySaveLastDraft } + if (privacyProtectScreen != def.privacyProtectScreen) { empty.privacyProtectScreen = privacyProtectScreen } + if (privacyMediaBlurRadius != def.privacyMediaBlurRadius) { empty.privacyMediaBlurRadius = privacyMediaBlurRadius } + if (notificationMode != def.notificationMode) { empty.notificationMode = notificationMode } + if (notificationPreviewMode != def.notificationPreviewMode) { empty.notificationPreviewMode = notificationPreviewMode } + if (webrtcPolicyRelay != def.webrtcPolicyRelay) { empty.webrtcPolicyRelay = webrtcPolicyRelay } + if (webrtcICEServers != def.webrtcICEServers) { empty.webrtcICEServers = webrtcICEServers } + if (confirmRemoteSessions != def.confirmRemoteSessions) { empty.confirmRemoteSessions = confirmRemoteSessions } + if (connectRemoteViaMulticast != def.connectRemoteViaMulticast) { empty.connectRemoteViaMulticast = connectRemoteViaMulticast } + if (connectRemoteViaMulticastAuto != def.connectRemoteViaMulticastAuto) { empty.connectRemoteViaMulticastAuto = connectRemoteViaMulticastAuto } + if (developerTools != def.developerTools) { empty.developerTools = developerTools } + if (confirmDBUpgrades != def.confirmDBUpgrades) { empty.confirmDBUpgrades = confirmDBUpgrades } + if (androidCallOnLockScreen != def.androidCallOnLockScreen) { empty.androidCallOnLockScreen = androidCallOnLockScreen } + if (iosCallKitEnabled != def.iosCallKitEnabled) { empty.iosCallKitEnabled = iosCallKitEnabled } + if (iosCallKitCallsInRecents != def.iosCallKitCallsInRecents) { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents } + if (uiProfileImageCornerRadius != def.uiProfileImageCornerRadius) { empty.uiProfileImageCornerRadius = uiProfileImageCornerRadius } + if (uiChatItemRoundness != def.uiChatItemRoundness) { empty.uiChatItemRoundness = uiChatItemRoundness } + if (uiChatItemTail != def.uiChatItemTail) { empty.uiChatItemTail = uiChatItemTail } + if (uiColorScheme != def.uiColorScheme) { empty.uiColorScheme = uiColorScheme } + if (uiDarkColorScheme != def.uiDarkColorScheme) { empty.uiDarkColorScheme = uiDarkColorScheme } + if (uiCurrentThemeIds != def.uiCurrentThemeIds) { empty.uiCurrentThemeIds = uiCurrentThemeIds } + if (uiThemes != def.uiThemes) { empty.uiThemes = uiThemes } + if (oneHandUI != def.oneHandUI) { empty.oneHandUI = oneHandUI } + if (chatBottomBar != def.chatBottomBar) { empty.chatBottomBar = chatBottomBar } + return empty + } + + fun importIntoApp() { + val def = appPreferences + var net = networkConfig?.copy() + if (net != null) { + // migrating from iOS BUT shouldn't be here ever because it should be changed on migration stage + if (net.hostMode == HostMode.Onion) { + net = net.copy(hostMode = HostMode.Public, requiredHostMode = true) + } + if (net.socksProxy != null) { + net = net.copy(socksProxy = networkProxy?.toProxyString()) + } + setNetCfg(net) + } + networkProxy?.let { def.networkProxy.set(it) } + privacyEncryptLocalFiles?.let { def.privacyEncryptLocalFiles.set(it) } + privacyAskToApproveRelays?.let { def.privacyAskToApproveRelays.set(it) } + privacyAcceptImages?.let { def.privacyAcceptImages.set(it) } + privacyLinkPreviews?.let { def.privacyLinkPreviews.set(it) } + privacyChatListOpenLinks?.let { def.privacyChatListOpenLinks.set(it) } + privacyShowChatPreviews?.let { def.privacyShowChatPreviews.set(it) } + privacySaveLastDraft?.let { def.privacySaveLastDraft.set(it) } + privacyProtectScreen?.let { def.privacyProtectScreen.set(it) } + privacyMediaBlurRadius?.let { def.privacyMediaBlurRadius.set(it) } + notificationMode?.let { def.notificationsMode.set(it.toNotificationsMode()) } + notificationPreviewMode?.let { def.notificationPreviewMode.set(it.toNotificationPreviewMode().name) } + webrtcPolicyRelay?.let { def.webrtcPolicyRelay.set(it) } + webrtcICEServers?.let { def.webrtcIceServers.set(it.joinToString(separator = "\n")) } + confirmRemoteSessions?.let { def.confirmRemoteSessions.set(it) } + connectRemoteViaMulticast?.let { def.connectRemoteViaMulticast.set(it) } + connectRemoteViaMulticastAuto?.let { def.connectRemoteViaMulticastAuto.set(it) } + developerTools?.let { def.developerTools.set(it) } + confirmDBUpgrades?.let { def.confirmDBUpgrades.set(it) } + androidCallOnLockScreen?.let { def.callOnLockScreen.set(it.toCallOnLockScreen()) } + iosCallKitEnabled?.let { def.iosCallKitEnabled.set(it) } + iosCallKitCallsInRecents?.let { def.iosCallKitCallsInRecents.set(it) } + uiProfileImageCornerRadius?.let { def.profileImageCornerRadius.set(it) } + uiChatItemRoundness?.let { def.chatItemRoundness.set(it) } + uiChatItemTail?.let { def.chatItemTail.set(it) } + uiColorScheme?.let { def.currentTheme.set(it) } + uiDarkColorScheme?.let { def.systemDarkTheme.set(it) } + uiCurrentThemeIds?.let { def.currentThemeIds.set(it) } + uiThemes?.let { def.themeOverrides.set(it.skipDuplicates()) } + oneHandUI?.let { def.oneHandUI.set(it) } + chatBottomBar?.let { if (appPlatform.isAndroid) def.chatBottomBar.set(it) else def.chatBottomBar.set(true) } + } + + companion object { + val defaults: AppSettings + get() = AppSettings( + networkConfig = NetCfg.defaults, + networkProxy = null, + privacyEncryptLocalFiles = true, + privacyAskToApproveRelays = true, + privacyAcceptImages = true, + privacyLinkPreviews = true, + privacyChatListOpenLinks = PrivacyChatListOpenLinksMode.ASK, + privacyShowChatPreviews = true, + privacySaveLastDraft = true, + privacyProtectScreen = false, + privacyMediaBlurRadius = 0, + notificationMode = AppSettingsNotificationMode.INSTANT, + notificationPreviewMode = AppSettingsNotificationPreviewMode.MESSAGE, + webrtcPolicyRelay = true, + webrtcICEServers = emptyList(), + confirmRemoteSessions = false, + connectRemoteViaMulticast = true, + connectRemoteViaMulticastAuto = true, + developerTools = false, + confirmDBUpgrades = false, + androidCallOnLockScreen = AppSettingsLockScreenCalls.SHOW, + iosCallKitEnabled = true, + iosCallKitCallsInRecents = false, + uiProfileImageCornerRadius = 22.5f, + uiChatItemRoundness = 0.75f, + uiChatItemTail = true, + uiColorScheme = DefaultTheme.SYSTEM_THEME_NAME, + uiDarkColorScheme = DefaultTheme.SIMPLEX.themeName, + uiCurrentThemeIds = null, + uiThemes = null, + oneHandUI = true, + chatBottomBar = true, + ) + + val current: AppSettings + get() { + val def = appPreferences + return defaults.copy( + networkConfig = getNetCfg(), + networkProxy = def.networkProxy.get(), + privacyEncryptLocalFiles = def.privacyEncryptLocalFiles.get(), + privacyAskToApproveRelays = def.privacyAskToApproveRelays.get(), + privacyAcceptImages = def.privacyAcceptImages.get(), + privacyLinkPreviews = def.privacyLinkPreviews.get(), + privacyChatListOpenLinks = def.privacyChatListOpenLinks.get(), + privacyShowChatPreviews = def.privacyShowChatPreviews.get(), + privacySaveLastDraft = def.privacySaveLastDraft.get(), + privacyProtectScreen = def.privacyProtectScreen.get(), + privacyMediaBlurRadius = def.privacyMediaBlurRadius.get(), + notificationMode = AppSettingsNotificationMode.from(def.notificationsMode.get()), + notificationPreviewMode = AppSettingsNotificationPreviewMode.from(NotificationPreviewMode.valueOf(def.notificationPreviewMode.get()!!)), + webrtcPolicyRelay = def.webrtcPolicyRelay.get(), + webrtcICEServers = def.webrtcIceServers.get()?.lines(), + confirmRemoteSessions = def.confirmRemoteSessions.get(), + connectRemoteViaMulticast = def.connectRemoteViaMulticast.get(), + connectRemoteViaMulticastAuto = def.connectRemoteViaMulticastAuto.get(), + developerTools = def.developerTools.get(), + confirmDBUpgrades = def.confirmDBUpgrades.get(), + androidCallOnLockScreen = AppSettingsLockScreenCalls.from(def.callOnLockScreen.get()), + iosCallKitEnabled = def.iosCallKitEnabled.get(), + iosCallKitCallsInRecents = def.iosCallKitCallsInRecents.get(), + uiProfileImageCornerRadius = def.profileImageCornerRadius.get(), + uiChatItemRoundness = def.chatItemRoundness.get(), + uiChatItemTail = def.chatItemTail.get(), + uiColorScheme = def.currentTheme.get() ?: DefaultTheme.SYSTEM_THEME_NAME, + uiDarkColorScheme = def.systemDarkTheme.get() ?: DefaultTheme.SIMPLEX.themeName, + uiCurrentThemeIds = def.currentThemeIds.get(), + uiThemes = def.themeOverrides.get(), + oneHandUI = def.oneHandUI.get(), + chatBottomBar = def.chatBottomBar.get() + ) + } + } +} + +@Serializable +enum class AppSettingsNotificationMode { + @SerialName("off") OFF, + @SerialName("periodic") PERIODIC, + @SerialName("instant") INSTANT; + + fun toNotificationsMode(): NotificationsMode = + when (this) { + INSTANT -> NotificationsMode.SERVICE + PERIODIC -> NotificationsMode.PERIODIC + OFF -> NotificationsMode.OFF + } + + companion object { + fun from(mode: NotificationsMode): AppSettingsNotificationMode = + when (mode) { + NotificationsMode.SERVICE -> INSTANT + NotificationsMode.PERIODIC -> PERIODIC + NotificationsMode.OFF -> OFF + } + } +} + +@Serializable +enum class AppSettingsNotificationPreviewMode { + @SerialName("message") MESSAGE, + @SerialName("contact") CONTACT, + @SerialName("hidden") HIDDEN; + + fun toNotificationPreviewMode(): NotificationPreviewMode = + when (this) { + MESSAGE -> NotificationPreviewMode.MESSAGE + CONTACT -> NotificationPreviewMode.CONTACT + HIDDEN -> NotificationPreviewMode.HIDDEN + } + + companion object { + val default: AppSettingsNotificationPreviewMode = MESSAGE + + fun from(mode: NotificationPreviewMode): AppSettingsNotificationPreviewMode = + when (mode) { + NotificationPreviewMode.MESSAGE -> MESSAGE + NotificationPreviewMode.CONTACT -> CONTACT + NotificationPreviewMode.HIDDEN -> HIDDEN + } + } +} + +@Serializable +enum class AppSettingsLockScreenCalls { + @SerialName("disable") DISABLE, + @SerialName("show") SHOW, + @SerialName("accept") ACCEPT; + + fun toCallOnLockScreen(): CallOnLockScreen = + when (this) { + DISABLE -> CallOnLockScreen.DISABLE + SHOW -> CallOnLockScreen.SHOW + ACCEPT -> CallOnLockScreen.ACCEPT + } + + companion object { + val default = SHOW + + fun from(mode: CallOnLockScreen): AppSettingsLockScreenCalls = + when (mode) { + CallOnLockScreen.DISABLE -> DISABLE + CallOnLockScreen.SHOW -> SHOW + CallOnLockScreen.ACCEPT -> ACCEPT + } + } +} + +@Serializable +data class UserNetworkInfo( + val networkType: UserNetworkType, + val online: Boolean, +) + +enum class UserNetworkType { + @SerialName("none") NONE, + @SerialName("cellular") CELLULAR, + @SerialName("wifi") WIFI, + @SerialName("ethernet") ETHERNET, + @SerialName("other") OTHER; + + val text: String + get() = when (this) { + NONE -> generalGetString(MR.strings.network_type_no_network_connection) + CELLULAR -> generalGetString(MR.strings.network_type_cellular) + WIFI -> generalGetString(MR.strings.network_type_network_wifi) + ETHERNET -> generalGetString(MR.strings.network_type_ethernet) + OTHER -> generalGetString(MR.strings.network_type_other) + } +} + +@Serializable +data class RcvMsgInfo ( + val msgId: Long, + val msgDeliveryId: Long, + val msgDeliveryStatus: String, + val agentMsgId: Long, + val agentMsgMeta: String +) + +@Serializable +data class ServerQueueInfo ( + val server: String, + val rcvId: String, + val sndId: String, + val ntfId: String? = null, + val status: String, + val info: QueueInfo +) + +@Serializable +data class QueueInfo ( + val qiSnd: Boolean, + val qiNtf: Boolean, + val qiSub: QSub? = null, + val qiSize: Int, + val qiMsg: MsgInfo? = null +) + +@Serializable +data class QSub ( + val qSubThread: QSubThread, + val qDelivered: String? = null +) + +enum class QSubThread { + @SerialName("noSub") + NO_SUB, + @SerialName("subPending") + SUB_PENDING, + @SerialName("subThread") + SUB_THREAD, + @SerialName("prohibitSub") + PROHIBIT_SUB +} + +@Serializable +data class MsgInfo ( + val msgId: String, + val msgTs: Instant, + val msgType: MsgType, +) + +enum class MsgType { + @SerialName("message") + MESSAGE, + @SerialName("quota") + QUOTA +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/other/wheel-picker/FWheelPickerDefault.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/other/wheel-picker/FWheelPickerDefault.kt index 9760e9c9f2..052e388f97 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/other/wheel-picker/FWheelPickerDefault.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/other/wheel-picker/FWheelPickerDefault.kt @@ -2,7 +2,6 @@ package com.sd.lib.compose.wheel_picker import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background -import chat.simplex.common.ui.theme.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -12,6 +11,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import chat.simplex.common.ui.theme.isInDarkTheme /** * The default implementation of focus view in vertical. @@ -76,7 +76,7 @@ fun FWheelPickerFocusHorizontal( */ private val DefaultDividerColor: Color @Composable - get() = (if (isSystemInDarkTheme()) { + get() = (if (isInDarkTheme()) { Color.White } else { Color.Black diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt index 7d5b1b0196..780f8c25b4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt @@ -3,7 +3,8 @@ package chat.simplex.common.platform import chat.simplex.common.BuildConfigCommon import chat.simplex.common.model.* import chat.simplex.common.ui.theme.DefaultTheme -import java.io.File +import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.res.MR import java.util.* enum class AppPlatform { @@ -20,11 +21,15 @@ expect val appPlatform: AppPlatform expect val deviceName: String +expect fun isAppVisibleAndFocused(): Boolean + val appVersionInfo: Pair<String, Int?> = if (appPlatform == AppPlatform.ANDROID) BuildConfigCommon.ANDROID_VERSION_NAME to BuildConfigCommon.ANDROID_VERSION_CODE else BuildConfigCommon.DESKTOP_VERSION_NAME to BuildConfigCommon.DESKTOP_VERSION_CODE +val databaseBackend: String = if (appPlatform == AppPlatform.ANDROID) "sqlite" else BuildConfigCommon.DATABASE_BACKEND + class FifoQueue<E>(private var capacity: Int) : LinkedList<E>() { override fun add(element: E): Boolean { if(size > capacity) removeFirst() @@ -42,6 +47,12 @@ fun runMigrations() { ChatController.appPrefs.currentTheme.set(DefaultTheme.SIMPLEX.name) } lastMigration.set(117) + } else if (lastMigration.get() < 203) { + // Moving to a different key for storing themes as a List + val oldOverrides = ChatController.appPrefs.themeOverridesOld.get().values.toList() + ChatController.appPrefs.themeOverrides.set(oldOverrides) + ChatController.appPrefs.currentThemeIds.set(oldOverrides.associate { it.base.themeName to it.themeId }) + lastMigration.set(203) } else { lastMigration.set(BuildConfigCommon.ANDROID_VERSION_CODE) break @@ -49,3 +60,16 @@ fun runMigrations() { } } } + +enum class AppUpdatesChannel { + DISABLED, + STABLE, + BETA; + + val text: String + get() = when (this) { + DISABLED -> generalGetString(MR.strings.app_check_for_updates_disabled) + STABLE -> generalGetString(MR.strings.app_check_for_updates_stable) + BETA -> generalGetString(MR.strings.app_check_for_updates_beta) + } +} 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 7d097efb7a..a9f2dcaffc 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 @@ -1,9 +1,16 @@ package chat.simplex.common.platform import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.ChatModel.currentUser import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.helpers.DatabaseUtils.ksDatabasePassword +import chat.simplex.common.views.helpers.DatabaseUtils.randomDatabasePassword import chat.simplex.common.views.onboarding.OnboardingStage -import kotlinx.serialization.decodeFromString +import chat.simplex.res.MR +import kotlinx.coroutines.* +import java.io.File import java.nio.ByteBuffer // ghc's rts @@ -14,6 +21,7 @@ external fun pipeStdOutToSocket(socketName: String) : Int // SimpleX API typealias ChatCtrl = Long external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array<Any> +external fun chatCloseStore(ctrl: ChatCtrl): String external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String external fun chatSendRemoteCmd(ctrl: ChatCtrl, rhId: Int, msg: String): String external fun chatRecvMsg(ctrl: ChatCtrl): String @@ -22,6 +30,7 @@ external fun chatParseMarkdown(str: String): String external fun chatParseServer(str: String): String external fun chatPasswordHash(pwd: String, salt: String): String external fun chatValidName(name: String): String +external fun chatJsonLength(str: String): Int external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String external fun chatReadFile(path: String, key: String, nonce: String): Array<Any> external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String @@ -35,25 +44,98 @@ val appPreferences: AppPreferences val chatController: ChatController = ChatController -suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) { - val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() - val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp - val migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value) - val res: DBMigrationResult = kotlin.runCatching { - json.decodeFromString<DBMigrationResult>(migrated[0] as String) - }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } - val ctrl = if (res is DBMigrationResult.OK) { - migrated[1] as Long - } else null - chatController.ctrl = ctrl - chatModel.chatDbEncrypted.value = dbKey != "" - chatModel.chatDbStatus.value = res - if (res != DBMigrationResult.OK) { - Log.d(TAG, "Unable to migrate successfully: $res") - } else if (startChat) { +fun initChatControllerOnStart() { + withLongRunningApi { + if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) { + initChatController(startChat = ::showStartChatAfterRestartAlert) + } else { + initChatController() + } + } +} + +suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: () -> CompletableDeferred<Boolean> = { CompletableDeferred(true) }) { + try { + if (chatModel.ctrlInitInProgress.value) return + chatModel.ctrlInitInProgress.value = true + if (!appPrefs.storeDBPassphrase.get() && !appPrefs.initialRandomDBPassphrase.get()) { + ksDatabasePassword.remove() + } + val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() + val confirm = confirmMigrations ?: if (appPreferences.developerTools.get() && appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp + var migrated: Array<Any> = if (databaseBackend == "postgres") { + chatMigrateInit("simplex_v1", "postgresql://simplex@/simplex_v1", MigrationConfirmation.Error.value) + } else { + chatMigrateInit(dbAbsolutePrefixPath, dbKey, MigrationConfirmation.Error.value) + } + var res: DBMigrationResult = runCatching { + json.decodeFromString<DBMigrationResult>(migrated[0] as String) + }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } + val rerunMigration = res is DBMigrationResult.ErrorMigration && when (res.migrationError) { + // we don't allow to run down migrations without confirmation in UI, so currently it won't be YesUpDown + is MigrationError.Upgrade -> confirm == MigrationConfirmation.YesUp || confirm == MigrationConfirmation.YesUpDown + is MigrationError.Downgrade -> confirm == MigrationConfirmation.YesUpDown + is MigrationError.Error -> false + } + if (rerunMigration) { + chatModel.dbMigrationInProgress.value = true + migrated = if (databaseBackend == "postgres") { + chatMigrateInit("simplex_v1", "postgresql://simplex@/simplex_v1", confirm.value) + } else { + chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value) + } + res = runCatching { + json.decodeFromString<DBMigrationResult>(migrated[0] as String) + }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } + } + val ctrl = if (res is DBMigrationResult.OK) { + migrated[1] as Long + } else null + chatController.ctrl = ctrl + chatModel.chatDbEncrypted.value = dbKey != "" + chatModel.chatDbStatus.value = res + if (res != DBMigrationResult.OK) { + Log.d(TAG, "Unable to migrate successfully: $res") + if (!appPrefs.newDatabaseInitialized.get() && DatabaseUtils.hasOnlyOneDatabase(dataDir.absolutePath)) { + if (chatModel.incompleteInitializedDbRemoved.value) { + Log.d(TAG, "Incomplete initialized databases were removed but after repeated migration only one database exists again, not trying to remove again") + } else { + val dbPath = dbAbsolutePrefixPath + File(dbPath + "_chat.db").delete() + File(dbPath + "_agent.db").delete() + chatModel.incompleteInitializedDbRemoved.value = true + Log.d(TAG, "Incomplete initialized databases were removed for the first time, repeating migration") + chatModel.ctrlInitInProgress.value = false + initChatController(useKey, confirmMigrations, startChat) + } + } + return + } + appPrefs.newDatabaseInitialized.set(true) + chatModel.incompleteInitializedDbRemoved.value = false + platform.androidRestartNetworkObserver() + controller.apiSetAppFilePaths( + appFilesDir.absolutePath, + coreTmpDir.absolutePath, + wallpapersDir.parentFile.absolutePath, + remoteHostsDir.absolutePath, + ctrl + ) + controller.apiSetEncryptLocalFiles(controller.appPrefs.privacyEncryptLocalFiles.get()) // If we migrated successfully means previous re-encryption process on database level finished successfully too if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null) val user = chatController.apiGetActiveUser(null) + chatModel.currentUser.value = user + chatModel.conditions.value = chatController.getServerOperators(null) ?: ServerOperatorConditionsDetail.empty + if (appPrefs.shouldImportAppSettings.get()) { + try { + val appSettings = controller.apiGetAppSettings(AppSettings.current.prepareForExport()) + appSettings.importIntoApp() + appPrefs.shouldImportAppSettings.set(false) + } catch (e: Exception) { + Log.e(TAG, "Error while importing app settings: " + e.stackTraceToString()) + } + } if (user == null) { chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.currentUser.value = null @@ -71,18 +153,72 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat } else { chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } - } else { + } else if (startChat().await()) { val savedOnboardingStage = appPreferences.onboardingStage.get() - appPreferences.onboardingStage.set(if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) { - OnboardingStage.Step3_CreateSimpleXAddress + 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 + } } else { savedOnboardingStage - }) - if (appPreferences.onboardingStage.get() == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) { - chatModel.setDeliveryReceipts.value = true + } + if (appPreferences.onboardingStage.get() != newStage) { + appPreferences.onboardingStage.set(newStage) } chatController.startChat(user) platform.androidChatInitializedAndStarted() + } else { + chatController.getUserChatData(null) + chatModel.localUserCreated.value = currentUser.value != null + chatModel.chatRunning.value = false } + } finally { + chatModel.ctrlInitInProgress.value = false + chatModel.dbMigrationInProgress.value = false } } + +fun chatInitTemporaryDatabase(dbPath: String, key: String? = null, confirmation: MigrationConfirmation = MigrationConfirmation.Error): Pair<DBMigrationResult, ChatCtrl?> { + val dbKey = key ?: randomDatabasePassword() + Log.d(TAG, "chatInitTemporaryDatabase path: $dbPath") + val migrated = chatMigrateInit(dbPath, dbKey, confirmation.value) + val res = runCatching { + json.decodeFromString<DBMigrationResult>(migrated[0] as String) + }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } + + return res to migrated[1] as ChatCtrl +} + +fun chatInitControllerRemovingDatabases() { + val dbPath = dbAbsolutePrefixPath + // Remove previous databases, otherwise, can be .errorNotADatabase with null controller + File(dbPath + "_chat.db").delete() + File(dbPath + "_agent.db").delete() + + val dbKey = randomDatabasePassword() + Log.d(TAG, "chatInitControllerRemovingDatabases path: $dbPath") + val migrated = chatMigrateInit(dbPath, dbKey, MigrationConfirmation.Error.value) + val res = runCatching { + json.decodeFromString<DBMigrationResult>(migrated[0] as String) + }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } + + val ctrl = migrated[1] as Long + chatController.ctrl = ctrl + // We need only controller, not databases + File(dbPath + "_chat.db").delete() + File(dbPath + "_agent.db").delete() +} + +fun showStartChatAfterRestartAlert(): CompletableDeferred<Boolean> { + val deferred = CompletableDeferred<Boolean>() + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.start_chat_question), + text = generalGetString(MR.strings.chat_is_stopped_you_should_transfer_database), + onConfirm = { deferred.complete(true) }, + onDismiss = { deferred.complete(false) }, + onDismissRequest = { deferred.complete(false) } + ) + return deferred +} 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 a6c93cc2f3..0a4f670fe0 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 @@ -1,28 +1,35 @@ package chat.simplex.common.platform import androidx.compose.runtime.Composable -import chat.simplex.common.model.CIFile -import chat.simplex.common.model.CryptoFile -import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import com.charleskorn.kaml.* +import kotlinx.serialization.encodeToString import java.io.* import java.net.URI import java.net.URLDecoder import java.net.URLEncoder +import java.nio.file.Files +import java.nio.file.StandardCopyOption expect val dataDir: File expect val tmpDir: File expect val filesDir: File expect val appFilesDir: File +expect val wallpapersDir: File expect val coreTmpDir: File expect val dbAbsolutePrefixPath: String +expect val preferencesDir: File +expect val preferencesTmpDir: File expect val chatDatabaseFileName: String expect val agentDatabaseFileName: String /** * This is used only for temporary storing db archive for export. -* Providing [tmpDir] instead crashes the app. Check db export before moving from this path to something else +* Providing [tmpDir] instead crashes the app on Android (only). Check db export before moving from this path to something else * */ expect val databaseExportDir: File @@ -30,6 +37,8 @@ expect val remoteHostsDir: File expect fun desktopOpenDatabaseDir() +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:")) @@ -42,7 +51,7 @@ fun copyFileToFile(from: File, to: URI, finally: () -> Unit) { } } showToast(generalGetString(MR.strings.file_saved)) - } catch (e: Error) { + } catch (e: Throwable) { showToast(generalGetString(MR.strings.error_saving_file)) Log.e(TAG, "copyFileToFile error saving file $e") } finally { @@ -58,7 +67,7 @@ fun copyBytesToFile(bytes: ByteArrayInputStream, to: URI, finally: () -> Unit) { } } showToast(generalGetString(MR.strings.file_saved)) - } catch (e: Error) { + } catch (e: Throwable) { showToast(generalGetString(MR.strings.error_saving_file)) Log.e(TAG, "copyBytesToFile error saving file $e") } finally { @@ -66,6 +75,8 @@ fun copyBytesToFile(bytes: ByteArrayInputStream, to: URI, finally: () -> Unit) { } } +fun getMigrationTempFilesDirectory(): File = File(dataDir, "migration_temp_files") + fun getAppFilePath(fileName: String): String { val rh = chatModel.currentRemoteHost.value val s = File.separator @@ -76,6 +87,20 @@ fun getAppFilePath(fileName: String): String { } } +fun getWallpaperFilePath(fileName: String): String { + val rh = chatModel.currentRemoteHost.value + val s = File.separator + val path = if (rh == null) { + wallpapersDir.absolutePath + s + fileName + } else { + remoteHostsDir.absolutePath + s + rh.storePath + s + "simplex_v1_assets" + s + "wallpapers" + s + fileName + } + File(path).parentFile.mkdirs() + return path +} + +fun getPreferenceFilePath(fileName: String = "themes.yaml"): String = preferencesDir.absolutePath + File.separator + fileName + fun getLoadedFilePath(file: CIFile?): String? { val f = file?.fileSource?.filePath return if (f != null && file.loaded) { @@ -96,6 +121,49 @@ fun getLoadedFileSource(file: CIFile?): CryptoFile? { } } +fun readThemeOverrides(): List<ThemeOverrides> { + return try { + val file = File(getPreferenceFilePath("themes.yaml")) + if (!file.exists()) return emptyList() + + file.inputStream().use { + val map = yaml.parseToYamlNode(it).yamlMap + val list = map.get<YamlList>("themes") + val res = ArrayList<ThemeOverrides>() + list?.items?.forEach { + try { + res.add(yaml.decodeFromYamlNode(ThemeOverrides.serializer(), it)) + } catch (e: Throwable) { + Log.e(TAG, "Error while reading specific theme: ${e.stackTraceToString()}") + } + } + res.skipDuplicates() + } + } catch (e: Throwable) { + Log.e(TAG, "Error while reading themes file: ${e.stackTraceToString()}") + emptyList() + } +} + +private const val lock = "themesWriter" + +fun writeThemeOverrides(overrides: List<ThemeOverrides>): Boolean = + synchronized(lock) { + try { + val themesFile = File(getPreferenceFilePath("themes.yaml")) + createTmpFileAndDelete(preferencesTmpDir) { tmpFile -> + val string = yaml.encodeToString(ThemesFile(themes = overrides)) + tmpFile.bufferedWriter().use { it.write(string) } + themesFile.parentFile.mkdirs() + Files.move(tmpFile.toPath(), themesFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + true + } catch (e: Exception) { + Log.e(TAG, "Error writing themes file: ${e.stackTraceToString()}") + false + } + } + private fun fileReady(file: CIFile, filePath: String) = File(filePath).exists() && CIFile.cachedRemoteFileRequests[file.fileSource] != false @@ -107,6 +175,7 @@ private fun fileReady(file: CIFile, filePath: String) = @Composable expect fun rememberFileChooserLauncher(getContent: Boolean, rememberedValue: Any? = null, onResult: (URI?) -> Unit): FileChooserLauncher +@Composable expect fun rememberFileChooserMultipleLauncher(onResult: (List<URI>) -> Unit): FileChooserMultipleLauncher expect class FileChooserLauncher() { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Log.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Log.kt index a1b39527d1..1c393d19ed 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Log.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Log.kt @@ -2,6 +2,10 @@ package chat.simplex.common.platform const val TAG = "SIMPLEX" +enum class LogLevel { + DEBUG, INFO, WARNING, ERROR +} + expect object Log { fun d(tag: String, text: String) fun e(tag: String, text: String) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt index 1141ab21ab..be7022ca80 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt @@ -1,25 +1,92 @@ package chat.simplex.common.platform -import androidx.compose.runtime.Composable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.BlurredEdgeTreatment +import androidx.compose.ui.draw.blur import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.views.helpers.KeyChangeEffect +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.filter import java.io.File -expect fun Modifier.navigationBarsWithImePadding(): Modifier - -@Composable -expect fun ProvideWindowInsets( - consumeWindowInsets: Boolean = true, - windowInsetsAnimationsEnabled: Boolean = true, - content: @Composable () -> Unit -) - @Composable expect fun Modifier.desktopOnExternalDrag( enabled: Boolean = true, onFiles: (List<File>) -> Unit = {}, - onImage: (Painter) -> Unit = {}, + onImage: (File) -> Unit = {}, onText: (String) -> Unit = {} ): Modifier expect fun Modifier.onRightClick(action: () -> Unit): Modifier + +expect fun Modifier.desktopPointerHoverIconHand(): Modifier + +expect fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier + +@Composable +fun Modifier.desktopModifyBlurredState(enabled: Boolean, blurred: MutableState<Boolean>, showMenu: State<Boolean>,): Modifier { + val blurRadius = remember { appPrefs.privacyMediaBlurRadius.state } + if (appPlatform.isDesktop) { + KeyChangeEffect(blurRadius.value) { + blurred.value = enabled && blurRadius.value > 0 + } + } + return if (appPlatform.isDesktop && enabled && blurRadius.value > 0 && !showMenu.value) { + var job: Job = remember { Job() } + LaunchedEffect(Unit) { + // The approach here is to allow menu to show up and to not blur the view. When menu is shown and mouse is hovering, + // unhovered action is still received, but we don't need to handle it until menu closes. When it closes, it takes one frame to catch a + // hover action again and if: + // 1. mouse is still on the view, the hover action will cancel this coroutine and the view will stay unblurred + // 2. mouse is not on the view, the view will become blurred after 100 ms + job = launch { + delay(100) + blurred.value = true + } + } + this then Modifier.desktopOnHovered { hovered -> + job.cancel() + blurred.value = !hovered && !showMenu.value + } + } else { + this + } +} + +@Composable +fun Modifier.privacyBlur( + enabled: Boolean, + blurred: MutableState<Boolean> = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) }, + scrollState: State<Boolean>, + onLongClick: () -> Unit = {} +): Modifier { + val blurRadius = remember { appPrefs.privacyMediaBlurRadius.state } + return if (enabled && blurred.value) { + this then Modifier.blur( + radiusX = remember { appPrefs.privacyMediaBlurRadius.state }.value.dp, + radiusY = remember { appPrefs.privacyMediaBlurRadius.state }.value.dp, + edgeTreatment = BlurredEdgeTreatment(RoundedCornerShape(0.dp)) + ) + .combinedClickable( + onLongClick = onLongClick, + onClick = { + blurred.value = false + } + ) + } else if (enabled && blurRadius.value > 0 && appPlatform.isAndroid) { + LaunchedEffect(Unit) { + snapshotFlow { scrollState.value } + .filter { it } + .filter { !blurred.value } + .collect { blurred.value = true } + } + this + } else { + this + } +} 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 6ca065086f..5b9e63963c 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 @@ -36,9 +36,17 @@ abstract class NtfManager { ) ) - fun notifyMessageReceived(user: UserLike, cInfo: ChatInfo, cItem: ChatItem) { - if (!cInfo.ntfsEnabled) return - displayNotification(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem)) + fun notifyMessageReceived(rhId: Long?, user: UserLike, cInfo: ChatInfo, cItem: ChatItem) { + if ( + cItem.showNotification && + cInfo.ntfsEnabled(cItem) && + ( + allowedToShowNotification() || + chatModel.chatId.value != cInfo.id || + chatModel.remoteHostId() != rhId) + ) { + displayNotification(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem)) + } } fun acceptContactRequestAction(userId: Long?, incognito: Boolean, chatId: ChatId) { @@ -55,7 +63,7 @@ abstract class NtfManager { } fun openChatAction(userId: Long?, chatId: ChatId) { - withBGApi { + withLongRunningApi { awaitChatStartedIfNeeded(chatModel) if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { // TODO include remote host ID in desktop notifications? @@ -65,12 +73,12 @@ abstract class NtfManager { } val cInfo = chatModel.getChat(chatId)?.chatInfo chatModel.clearOverlays.value = true - if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(null, cInfo, chatModel) + if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(secondaryChatsCtx = null, rhId = null, cInfo) } } fun showChatsAction(userId: Long?) { - withBGApi { + withLongRunningApi { awaitChatStartedIfNeeded(chatModel) if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { // TODO include remote host ID in desktop notifications? @@ -93,12 +101,14 @@ abstract class NtfManager { } } - abstract fun notifyCallInvitation(invitation: RcvCallInvitation) + abstract fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean abstract fun hasNotificationsForChat(chatId: String): Boolean abstract fun cancelNotificationsForChat(chatId: String) + abstract fun cancelNotificationsForUser(userId: Long) abstract fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<Pair<NotificationAction, () -> Unit>> = emptyList()) abstract fun cancelCallNotification() abstract fun cancelAllNotifications() + abstract fun showMessage(title: String, text: String) // Android only abstract fun androidCreateNtfChannelsMaybeShowAlert() @@ -124,7 +134,12 @@ abstract class NtfManager { } res } else { - cItem.text + val mc = cItem.content.msgContent + if (mc is MsgContent.MCReport) { + generalGetString(MR.strings.notification_group_report).format(cItem.text.ifEmpty { mc.reason.text }) + } else { + cItem.text + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt index e55c2c939a..448100bc17 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt @@ -1,17 +1,38 @@ package chat.simplex.common.platform +import androidx.compose.animation.core.* +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import chat.simplex.common.model.ChatId import chat.simplex.common.model.NotificationsMode +import chat.simplex.common.ui.theme.CurrentColors +import kotlinx.coroutines.Job +import java.io.Closeable interface PlatformInterface { suspend fun androidServiceStart() {} fun androidServiceSafeStop() {} + fun androidCallServiceSafeStop() {} fun androidNotificationsModeChanged(mode: NotificationsMode) {} fun androidChatStartedAfterBeingOff() {} fun androidChatStopped() {} fun androidChatInitializedAndStarted() {} fun androidIsBackgroundCallAllowed(): Boolean = true fun androidSetNightModeIfSupported() {} + fun androidSetStatusAndNavigationBarAppearance(isLightStatusBar: Boolean, isLightNavBar: Boolean, blackNavBar: Boolean = false, themeBackgroundColor: Color = CurrentColors.value.colors.background) {} + fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {} + fun androidPictureInPictureAllowed(): Boolean = true + fun androidCallEnded() {} + fun androidRestartNetworkObserver() {} + fun androidCreateActiveCallState(): Closeable = Closeable { } + fun androidIsXiaomiDevice(): Boolean = false + val androidApiLevel: Int? get() = null + @Composable fun androidLockPortraitOrientation() {} suspend fun androidAskToAllowBackgroundCalls(): Boolean = true + @Composable fun desktopShowAppUpdateNotice() {} } /** * Multiplatform project has separate directories per platform + common directory that contains directories per platform + common for all of them. diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt index af47f9c3e0..6b301b9df4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt @@ -1,8 +1,10 @@ package chat.simplex.common.platform -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState +import androidx.compose.runtime.* +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle +import chat.simplex.common.views.chat.ComposeMessage import chat.simplex.common.views.chat.ComposeState import java.net.URI @@ -10,12 +12,15 @@ import java.net.URI expect fun PlatformTextField( composeState: MutableState<ComposeState>, sendMsgEnabled: Boolean, + disabledText: String?, sendMsgButtonDisabled: Boolean, textStyle: MutableState<TextStyle>, showDeleteTextButton: MutableState<Boolean>, - userIsObserver: Boolean, - onMessageChange: (String) -> Unit, + placeholder: String, + showVoiceButton: Boolean, + onMessageChange: (ComposeMessage) -> Unit, onUpArrow: () -> Unit, onFilesPasted: (List<URI>) -> Unit, + focusRequester: FocusRequester? = null, onDone: () -> Unit, ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt index 0e0f769487..fd1824d5b6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt @@ -16,13 +16,25 @@ interface RecorderInterface { expect class RecorderNative(): RecorderInterface +enum class TrackState { + PLAYING, PAUSED, REPLACED +} + +data class CurrentlyPlayingState( + val fileSource: CryptoFile, + val onProgressUpdate: (position: Int?, state: TrackState) -> Unit, + val smallView: Boolean, +) + interface AudioPlayerInterface { + val currentlyPlaying: MutableState<CurrentlyPlayingState?> fun play( fileSource: CryptoFile, audioPlaying: MutableState<Boolean>, progress: MutableState<Int>, duration: MutableState<Int>, resetOnEnd: Boolean, + smallView: Boolean, ) fun stop() fun stop(item: ChatItem) @@ -39,4 +51,13 @@ interface SoundPlayerInterface { fun stop() } +interface CallSoundsPlayerInterface { + fun startConnectingCallSound(scope: CoroutineScope) + fun startInCallSound(scope: CoroutineScope) + fun stop() + fun vibrate(times: Int = 1) +} + expect object SoundPlayer: SoundPlayerInterface + +expect object CallSoundsPlayer: CallSoundsPlayerInterface diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt index 2ee668fb23..fd712624bd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt @@ -1,11 +1,13 @@ package chat.simplex.common.platform import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import com.russhwolf.settings.Settings +import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource @Composable @@ -28,6 +30,11 @@ expect fun windowOrientation(): WindowOrientation @Composable expect fun windowWidth(): Dp +@Composable +expect fun windowHeight(): Dp + expect fun desktopExpandWindowToWidth(width: Dp) expect fun isRtl(text: CharSequence): Boolean + +expect fun ImageResource.toComposeImageBitmap(): ImageBitmap? diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt new file mode 100644 index 0000000000..b1965ec845 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt @@ -0,0 +1,76 @@ +package chat.simplex.common.platform + +import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.ScrollableDefaults +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.* +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.dp + +@Composable +expect fun LazyColumnWithScrollBar( + modifier: Modifier = Modifier, + state: LazyListState? = null, + contentPadding: PaddingValues = PaddingValues(0.dp), + reverseLayout: Boolean = false, + verticalArrangement: Arrangement.Vertical = + if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), + userScrollEnabled: Boolean = true, + additionalBarOffset: State<Dp>? = null, + additionalTopBar: State<Boolean> = remember { mutableStateOf(false) }, + chatBottomBar: State<Boolean> = remember { mutableStateOf(true) }, + // by default, this function will include .fillMaxSize() without you doing anything. If you don't need it, pass `false` here + // maxSize (at least maxHeight) is needed for blur on appBars to work correctly + fillMaxSize: Boolean = true, + content: LazyListScope.() -> Unit +) + +@Composable +expect fun LazyColumnWithScrollBarNoAppBar( + modifier: Modifier = Modifier, + state: LazyListState? = null, + contentPadding: PaddingValues = PaddingValues(0.dp), + reverseLayout: Boolean = false, + verticalArrangement: Arrangement.Vertical = + if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), + userScrollEnabled: Boolean = true, + additionalBarOffset: State<Dp>? = null, + additionalTopBar: State<Boolean> = remember { mutableStateOf(false) }, + chatBottomBar: State<Boolean> = remember { mutableStateOf(true) }, + maxHeight: State<Dp>? = null, + containerAlignment: Alignment = Alignment.TopStart, + content: LazyListScope.() -> Unit +) + +@Composable +expect fun ColumnWithScrollBar( + modifier: Modifier = Modifier, + verticalArrangement: Arrangement.Vertical = Arrangement.Top, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + state: ScrollState? = null, + // set true when you want to show something in the center with respected .fillMaxSize() + maxIntrinsicSize: Boolean = false, + // by default, this function will include .fillMaxSize() without you doing anything. If you don't need it, pass `false` here + // maxSize (at least maxHeight) is needed for blur on appBars to work correctly + fillMaxSize: Boolean = true, + content: @Composable ColumnScope.() -> Unit +) + +@Composable +expect fun ColumnWithScrollBarNoAppBar( + modifier: Modifier = Modifier, + verticalArrangement: Arrangement.Vertical = Arrangement.Top, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + state: ScrollState? = null, + // set true when you want to show something in the center with respected .fillMaxSize() + maxIntrinsicSize: Boolean = false, + content: @Composable ColumnScope.() -> Unit +) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt index 72bb3caaac..bd469298b7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt @@ -8,3 +8,4 @@ expect fun UriHandler.sendEmail(subject: String, body: CharSequence) expect fun ClipboardManager.shareText(text: String) expect fun shareFile(text: String, fileSource: CryptoFile) +expect fun openFile(fileSource: CryptoFile) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt index 28ac357c2d..af202fcabd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt @@ -13,7 +13,7 @@ expect fun LocalMultiplatformView(): Any? @Composable expect fun getKeyboardState(): State<KeyboardState> -expect fun hideKeyboard(view: Any?) +expect fun hideKeyboard(view: Any?, clearFocus: Boolean = false) expect fun androidIsFinishingMainActivity(): Boolean 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 a01fc9de51..c50ea5c349 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 @@ -1,6 +1,12 @@ package chat.simplex.common.ui.theme -import androidx.compose.ui.graphics.Color +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 val Purple200 = Color(0xFFBB86FC) val Purple500 = Color(0xFF6200EE) @@ -25,4 +31,6 @@ 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 MenuTextColorDark = Color.White.copy(alpha = 0.8f) + +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 6af3156ca2..01e19ea478 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 @@ -1,57 +1,142 @@ package chat.simplex.common.ui.theme -import androidx.compose.foundation.background import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.* -import androidx.compose.ui.unit.dp +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.* import chat.simplex.common.model.ChatController -import chat.simplex.common.platform.isInNightMode +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex +import chat.simplex.common.ui.theme.ThemeManager.toReadableHex import chat.simplex.common.views.helpers.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import chat.simplex.res.MR +import kotlinx.serialization.Transient +import java.util.UUID enum class DefaultTheme { - SYSTEM, LIGHT, DARK, SIMPLEX; + LIGHT, DARK, SIMPLEX, BLACK; - // Call it only with base theme, not SYSTEM - fun hasChangedAnyColor(colors: Colors, appColors: AppColors): Boolean { - val palette = when (this) { - SYSTEM -> return false - LIGHT -> LightColorPalette - DARK -> DarkColorPalette - SIMPLEX -> SimplexColorPalette - } - val appPalette = when (this) { - SYSTEM -> return false - LIGHT -> LightColorPaletteApp - DARK -> DarkColorPaletteApp - SIMPLEX -> SimplexColorPaletteApp - } - return colors.primary != palette.primary || - colors.primaryVariant != palette.primaryVariant || - colors.secondary != palette.secondary || - colors.secondaryVariant != palette.secondaryVariant || - colors.background != palette.background || - colors.surface != palette.surface || - appColors != appPalette + companion object { + const val SYSTEM_THEME_NAME: String = "SYSTEM" + } + + val themeName: String + get() = name + + val mode: DefaultThemeMode get() = if (this == LIGHT) DefaultThemeMode.LIGHT else DefaultThemeMode.DARK + + fun hasChangedAnyColor(overrides: ThemeOverrides?): Boolean { + if (overrides == null) return false + return overrides.colors != ThemeColors() || + overrides.wallpaper != null && (overrides.wallpaper.background != null || overrides.wallpaper.tint != null) } } -data class AppColors( - val title: Color, - val sentMessage: Color, - val receivedMessage: Color -) +@Serializable +enum class DefaultThemeMode { + @SerialName("light") LIGHT, + @SerialName("dark") DARK +} + +@Stable +class AppColors( + title: Color, + primaryVariant2: Color, + sentMessage: Color, + sentQuote: Color, + receivedMessage: Color, + receivedQuote: Color, +) { + var title by mutableStateOf(title, structuralEqualityPolicy()) + internal set + var primaryVariant2 by mutableStateOf(primaryVariant2, structuralEqualityPolicy()) + internal set + var sentMessage by mutableStateOf(sentMessage, structuralEqualityPolicy()) + internal set + var sentQuote by mutableStateOf(sentQuote, structuralEqualityPolicy()) + internal set + var receivedMessage by mutableStateOf(receivedMessage, structuralEqualityPolicy()) + internal set + var receivedQuote by mutableStateOf(receivedQuote, structuralEqualityPolicy()) + internal set + + fun copy( + title: Color = this.title, + primaryVariant2: Color = this.primaryVariant2, + sentMessage: Color = this.sentMessage, + sentQuote: Color = this.sentQuote, + receivedMessage: Color = this.receivedMessage, + receivedQuote: Color = this.receivedQuote, + ): AppColors = AppColors( + title, + primaryVariant2, + sentMessage, + sentQuote, + receivedMessage, + receivedQuote, + ) + + override fun toString(): String { + return buildString { + append("AppColors(") + append("title=$title, ") + append("primaryVariant2=$primaryVariant2, ") + append("sentMessage=$sentMessage, ") + append("sentQuote=$sentQuote, ") + append("receivedMessage=$receivedMessage, ") + append("receivedQuote=$receivedQuote") + append(")") + } + } +} + +@Stable +class AppWallpaper( + background: Color? = null, + tint: Color? = null, + type: WallpaperType = WallpaperType.Empty, +) { + var background by mutableStateOf(background, structuralEqualityPolicy()) + internal set + var tint by mutableStateOf(tint, structuralEqualityPolicy()) + internal set + var type by mutableStateOf(type, structuralEqualityPolicy()) + internal set + + fun copy( + background: Color? = this.background, + tint: Color? = this.tint, + type: WallpaperType = this.type, + ): AppWallpaper = AppWallpaper( + background, + tint, + type, + ) + + override fun toString(): String { + return buildString { + append("AppWallpaper(") + append("background=$background, ") + append("tint=$tint, ") + append("type=$type") + append(")") + } + } +} enum class ThemeColor { - PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE, TITLE, SENT_MESSAGE, RECEIVED_MESSAGE; + PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE, TITLE, SENT_MESSAGE, SENT_QUOTE, RECEIVED_MESSAGE, RECEIVED_QUOTE, PRIMARY_VARIANT2, WALLPAPER_BACKGROUND, WALLPAPER_TINT; - fun fromColors(colors: Colors, appColors: AppColors): Color { + fun fromColors(colors: Colors, appColors: AppColors, appWallpaper: AppWallpaper): Color? { return when (this) { PRIMARY -> colors.primary PRIMARY_VARIANT -> colors.primaryVariant @@ -60,8 +145,13 @@ enum class ThemeColor { BACKGROUND -> colors.background SURFACE -> colors.surface TITLE -> appColors.title + PRIMARY_VARIANT2 -> appColors.primaryVariant2 SENT_MESSAGE -> appColors.sentMessage + SENT_QUOTE -> appColors.sentQuote RECEIVED_MESSAGE -> appColors.receivedMessage + RECEIVED_QUOTE -> appColors.receivedQuote + WALLPAPER_BACKGROUND -> appWallpaper.background + WALLPAPER_TINT -> appWallpaper.tint } } @@ -74,8 +164,13 @@ enum class ThemeColor { BACKGROUND -> generalGetString(MR.strings.color_background) SURFACE -> generalGetString(MR.strings.color_surface) TITLE -> generalGetString(MR.strings.color_title) + PRIMARY_VARIANT2 -> generalGetString(MR.strings.color_primary_variant2) SENT_MESSAGE -> generalGetString(MR.strings.color_sent_message) + SENT_QUOTE -> generalGetString(MR.strings.color_sent_quote) RECEIVED_MESSAGE -> generalGetString(MR.strings.color_received_message) + RECEIVED_QUOTE -> generalGetString(MR.strings.color_received_quote) + WALLPAPER_BACKGROUND -> generalGetString(MR.strings.color_wallpaper_background) + WALLPAPER_TINT -> generalGetString(MR.strings.color_wallpaper_tint) } } @@ -91,45 +186,232 @@ data class ThemeColors( @SerialName("menus") val surface: String? = null, val title: String? = null, + @SerialName("accentVariant2") + val primaryVariant2: String? = null, val sentMessage: String? = null, + @SerialName("sentReply") + val sentQuote: String? = null, val receivedMessage: String? = null, + @SerialName("receivedReply") + val receivedQuote: String? = null, ) { - fun toColors(base: DefaultTheme): Colors { + companion object { + fun from(colors: Colors, appColors: AppColors): ThemeColors = + ThemeColors( + primary = colors.primary.toReadableHex(), + primaryVariant = colors.primaryVariant.toReadableHex(), + secondary = colors.secondary.toReadableHex(), + secondaryVariant = colors.secondaryVariant.toReadableHex(), + background = colors.background.toReadableHex(), + surface = colors.surface.toReadableHex(), + title = appColors.title.toReadableHex(), + primaryVariant2 = appColors.primaryVariant2.toReadableHex(), + sentMessage = appColors.sentMessage.toReadableHex(), + sentQuote = appColors.sentQuote.toReadableHex(), + receivedMessage = appColors.receivedMessage.toReadableHex(), + receivedQuote = appColors.receivedQuote.toReadableHex(), + ) + } +} + +@Serializable +data class ThemeWallpaper ( + val preset: String? = null, + val scale: Float? = null, + val scaleType: WallpaperScaleType? = null, + val background: String? = null, + val tint: String? = null, + val image: String? = null, + val imageFile: String? = null, +) { + fun toAppWallpaper(): AppWallpaper { + return AppWallpaper( + background = background?.colorFromReadableHex(), + tint = tint?.colorFromReadableHex(), + type = WallpaperType.from(this) ?: WallpaperType.Empty + ) + } + + fun withFilledWallpaperBase64(): ThemeWallpaper { + val aw = toAppWallpaper() + val type = aw.type + return ThemeWallpaper( + image = if (type is WallpaperType.Image && type.image != null) resizeImageToStrSize(type.image!!, 5_000_000) else null, + imageFile = null, + preset = if (type is WallpaperType.Preset) type.filename else null, + scale = if (type is WallpaperType.Preset) type.scale else if (type is WallpaperType.Image) type.scale else 1f, + scaleType = if (type is WallpaperType.Image) type.scaleType else null, + background = aw.background?.toReadableHex(), + tint = aw.tint?.toReadableHex(), + ) + } + + fun withFilledWallpaperPath(): ThemeWallpaper { + val aw = toAppWallpaper() + val type = aw.type + return ThemeWallpaper( + image = null, + imageFile = if (type is WallpaperType.Image) type.filename else null, + preset = if (type is WallpaperType.Preset) type.filename else null, + scale = if (scale == null) null else if (type is WallpaperType.Preset) type.scale else if (type is WallpaperType.Image) scale else null, + scaleType = if (scaleType == null) null else if (type is WallpaperType.Image) type.scaleType else null, + background = aw.background?.toReadableHex(), + tint = aw.tint?.toReadableHex(), + ) + } + + fun importFromString(): ThemeWallpaper = + if (preset == null && image != null) { + // Need to save image from string and to save its path + try { + val parsed = base64ToBitmap(image) + val filename = saveWallpaperFile(parsed) + copy(image = null, imageFile = filename) + } catch (e: Exception) { + Log.e(TAG, "Error while parsing/copying the image: ${e.stackTraceToString()}") + ThemeWallpaper() + } + } else this + + companion object { + fun from(type: WallpaperType, background: String?, tint: String?): ThemeWallpaper { + return ThemeWallpaper( + image = null, + imageFile = if (type is WallpaperType.Image) type.filename else null, + preset = if (type is WallpaperType.Preset) type.filename else null, + scale = if (type is WallpaperType.Preset) type.scale else if (type is WallpaperType.Image) type.scale else null, + scaleType = if (type is WallpaperType.Image) type.scaleType else null, + background = background, + tint = tint, + ) + } + } +} + +@Serializable +data class ThemesFile( + val themes: List<ThemeOverrides> = emptyList() +) + +@Serializable +data class ThemeOverrides ( + val themeId: String = UUID.randomUUID().toString(), + val base: DefaultTheme, + val colors: ThemeColors = ThemeColors(), + val wallpaper: ThemeWallpaper? = null, +) { + + fun isSame(type: WallpaperType?, themeName: String): Boolean = + ( + (wallpaper?.preset != null && type is WallpaperType.Preset && wallpaper.preset == type.filename) || + (wallpaper?.imageFile != null && type is WallpaperType.Image) || + (wallpaper?.preset == null && wallpaper?.imageFile == null && (type == WallpaperType.Empty || type == null)) + ) && base.themeName == themeName + + fun withUpdatedColor(name: ThemeColor, color: String?): ThemeOverrides { + return copy( + colors = when (name) { + ThemeColor.PRIMARY -> colors.copy(primary = color) + ThemeColor.PRIMARY_VARIANT -> colors.copy(primaryVariant = color) + ThemeColor.SECONDARY -> colors.copy(secondary = color) + ThemeColor.SECONDARY_VARIANT -> colors.copy(secondaryVariant = color) + ThemeColor.BACKGROUND -> colors.copy(background = color) + ThemeColor.SURFACE -> colors.copy(surface = color) + ThemeColor.TITLE -> colors.copy(title = color) + ThemeColor.PRIMARY_VARIANT2 -> colors.copy(primaryVariant2 = color) + ThemeColor.SENT_MESSAGE -> colors.copy(sentMessage = color) + ThemeColor.SENT_QUOTE -> colors.copy(sentQuote = color) + ThemeColor.RECEIVED_MESSAGE -> colors.copy(receivedMessage = color) + ThemeColor.RECEIVED_QUOTE -> colors.copy(receivedQuote = color) + ThemeColor.WALLPAPER_BACKGROUND -> colors.copy() + ThemeColor.WALLPAPER_TINT -> colors.copy() + }, wallpaper = when (name) { + ThemeColor.WALLPAPER_BACKGROUND -> wallpaper?.copy(background = color) + ThemeColor.WALLPAPER_TINT -> wallpaper?.copy(tint = color) + else -> wallpaper?.copy() + } + ) + } + + fun toColors(base: DefaultTheme, perChatTheme: ThemeColors?, perUserTheme: ThemeColors?, presetWallpaperTheme: ThemeColors?): Colors { val baseColors = when (base) { DefaultTheme.LIGHT -> LightColorPalette DefaultTheme.DARK -> DarkColorPalette DefaultTheme.SIMPLEX -> SimplexColorPalette - // shouldn't be here - DefaultTheme.SYSTEM -> LightColorPalette + DefaultTheme.BLACK -> BlackColorPalette } return baseColors.copy( - primary = primary?.colorFromReadableHex() ?: baseColors.primary, - primaryVariant = primaryVariant?.colorFromReadableHex() ?: baseColors.primaryVariant, - secondary = secondary?.colorFromReadableHex() ?: baseColors.secondary, - secondaryVariant = secondaryVariant?.colorFromReadableHex() ?: baseColors.secondaryVariant, - background = background?.colorFromReadableHex() ?: baseColors.background, - surface = surface?.colorFromReadableHex() ?: baseColors.surface, + primary = perChatTheme?.primary?.colorFromReadableHex() ?: perUserTheme?.primary?.colorFromReadableHex() ?: colors.primary?.colorFromReadableHex() ?: presetWallpaperTheme?.primary?.colorFromReadableHex() ?: baseColors.primary, + primaryVariant = perChatTheme?.primaryVariant?.colorFromReadableHex() ?: perUserTheme?.primaryVariant?.colorFromReadableHex() ?: colors.primaryVariant?.colorFromReadableHex() ?: presetWallpaperTheme?.primaryVariant?.colorFromReadableHex() ?: baseColors.primaryVariant, + secondary = perChatTheme?.secondary?.colorFromReadableHex() ?: perUserTheme?.secondary?.colorFromReadableHex() ?: colors.secondary?.colorFromReadableHex() ?: presetWallpaperTheme?.secondary?.colorFromReadableHex() ?: baseColors.secondary, + secondaryVariant = perChatTheme?.secondaryVariant?.colorFromReadableHex() ?: perUserTheme?.secondaryVariant?.colorFromReadableHex() ?: colors.secondaryVariant?.colorFromReadableHex() ?: presetWallpaperTheme?.secondaryVariant?.colorFromReadableHex() ?: baseColors.secondaryVariant, + background = perChatTheme?.background?.colorFromReadableHex() ?: perUserTheme?.background?.colorFromReadableHex() ?: colors.background?.colorFromReadableHex() ?: presetWallpaperTheme?.background?.colorFromReadableHex() ?: baseColors.background, + surface = perChatTheme?.surface?.colorFromReadableHex() ?: perUserTheme?.surface?.colorFromReadableHex() ?: colors.surface?.colorFromReadableHex() ?: presetWallpaperTheme?.surface?.colorFromReadableHex() ?: baseColors.surface, ) } - fun toAppColors(base: DefaultTheme): AppColors { + fun toAppColors(base: DefaultTheme, perChatTheme: ThemeColors?, perChatWallpaperType: WallpaperType?, perUserTheme: ThemeColors?, perUserWallpaperType: WallpaperType?, presetWallpaperTheme: ThemeColors?): AppColors { val baseColors = when (base) { DefaultTheme.LIGHT -> LightColorPaletteApp DefaultTheme.DARK -> DarkColorPaletteApp DefaultTheme.SIMPLEX -> SimplexColorPaletteApp - // shouldn't be here - DefaultTheme.SYSTEM -> LightColorPaletteApp + DefaultTheme.BLACK -> BlackColorPaletteApp } + + val sentMessageFallback = colors.sentMessage?.colorFromReadableHex() ?: presetWallpaperTheme?.sentMessage?.colorFromReadableHex() ?: baseColors.sentMessage + val sentQuoteFallback = colors.sentQuote?.colorFromReadableHex() ?: presetWallpaperTheme?.sentQuote?.colorFromReadableHex() ?: baseColors.sentQuote + val receivedMessageFallback = colors.receivedMessage?.colorFromReadableHex() ?: presetWallpaperTheme?.receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage + val receivedQuoteFallback = colors.receivedQuote?.colorFromReadableHex() ?: presetWallpaperTheme?.receivedQuote?.colorFromReadableHex() ?: baseColors.receivedQuote return baseColors.copy( - title = title?.colorFromReadableHex() ?: baseColors.title, - sentMessage = sentMessage?.colorFromReadableHex() ?: baseColors.sentMessage, - receivedMessage = receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage, + title = perChatTheme?.title?.colorFromReadableHex() ?: perUserTheme?.title?.colorFromReadableHex() ?: colors.title?.colorFromReadableHex() ?: presetWallpaperTheme?.title?.colorFromReadableHex() ?: baseColors.title, + primaryVariant2 = perChatTheme?.primaryVariant2?.colorFromReadableHex() ?: perUserTheme?.primaryVariant2?.colorFromReadableHex() ?: colors.primaryVariant2?.colorFromReadableHex() ?: presetWallpaperTheme?.primaryVariant2?.colorFromReadableHex() ?: baseColors.primaryVariant2, + sentMessage = if (perChatTheme?.sentMessage != null) perChatTheme.sentMessage.colorFromReadableHex() + else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.sentMessage?.colorFromReadableHex() ?: sentMessageFallback + else sentMessageFallback, + sentQuote = if (perChatTheme?.sentQuote != null) perChatTheme.sentQuote.colorFromReadableHex() + else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.sentQuote?.colorFromReadableHex() ?: sentQuoteFallback + else sentQuoteFallback, + receivedMessage = if (perChatTheme?.receivedMessage != null) perChatTheme.receivedMessage.colorFromReadableHex() + else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.receivedMessage?.colorFromReadableHex() ?: receivedMessageFallback + else receivedMessageFallback, + receivedQuote = if (perChatTheme?.receivedQuote != null) perChatTheme.receivedQuote.colorFromReadableHex() + else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.receivedQuote?.colorFromReadableHex() ?: receivedQuoteFallback + else receivedQuoteFallback, ) } - fun withFilledColors(base: DefaultTheme): ThemeColors { - val c = toColors(base) - val ac = toAppColors(base) + fun toAppWallpaper(themeOverridesForType: WallpaperType?, perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverride?, materialBackgroundColor: Color): AppWallpaper { + val mainType = when { + themeOverridesForType != null -> themeOverridesForType + // type can be null if override is empty `"wallpaper": "{}"`, in this case no wallpaper is needed, empty. + // It's not null to override upper level wallpaper + perChatTheme?.wallpaper != null -> perChatTheme.wallpaper.toAppWallpaper().type + perUserTheme?.wallpaper != null -> perUserTheme.wallpaper.toAppWallpaper().type + else -> wallpaper?.toAppWallpaper()?.type ?: return AppWallpaper() + } + val first: ThemeWallpaper? = if (mainType.sameType(perChatTheme?.wallpaper?.toAppWallpaper()?.type)) perChatTheme?.wallpaper else null + val second: ThemeWallpaper? = if (mainType.sameType(perUserTheme?.wallpaper?.toAppWallpaper()?.type)) perUserTheme?.wallpaper else null + val third: ThemeWallpaper? = if (mainType.sameType(this.wallpaper?.toAppWallpaper()?.type)) this.wallpaper else null + + return AppWallpaper(type = when (mainType) { + is WallpaperType.Preset -> mainType.copy( + scale = mainType.scale ?: first?.scale ?: second?.scale ?: third?.scale + ) + is WallpaperType.Image -> mainType.copy( + scale = if (themeOverridesForType == null) mainType.scale ?: first?.scale ?: second?.scale ?: third?.scale else second?.scale ?: third?.scale ?: mainType.scale, + scaleType = if (themeOverridesForType == null) mainType.scaleType ?: first?.scaleType ?: second?.scaleType ?: third?.scaleType else second?.scaleType ?: third?.scaleType ?: mainType.scaleType, + filename = if (themeOverridesForType == null) mainType.filename else first?.imageFile ?: second?.imageFile ?: third?.imageFile ?: mainType.filename, + ) + is WallpaperType.Empty -> mainType + }, + background = (first?.background ?: second?.background ?: third?.background)?.colorFromReadableHex() ?: mainType.defaultBackgroundColor(base, materialBackgroundColor), + tint = (first?.tint ?: second?.tint ?: third?.tint)?.colorFromReadableHex() ?: mainType.defaultTintColor(base) + ) + } + + fun withFilledColors(base: DefaultTheme, perChatTheme: ThemeColors?, perChatWallpaperType: WallpaperType?, perUserTheme: ThemeColors?, perUserWallpaperType: WallpaperType?, presetWallpaperTheme: ThemeColors?): ThemeColors { + val c = toColors(base, perChatTheme, perUserTheme, presetWallpaperTheme) + val ac = toAppColors(base, perChatTheme, perChatWallpaperType, perUserTheme, perUserWallpaperType, presetWallpaperTheme) return ThemeColors( primary = c.primary.toReadableHex(), primaryVariant = c.primaryVariant.toReadableHex(), @@ -138,23 +420,71 @@ data class ThemeColors( background = c.background.toReadableHex(), surface = c.surface.toReadableHex(), title = ac.title.toReadableHex(), + primaryVariant2 = ac.primaryVariant2.toReadableHex(), sentMessage = ac.sentMessage.toReadableHex(), - receivedMessage = ac.receivedMessage.toReadableHex() + sentQuote = ac.sentQuote.toReadableHex(), + receivedMessage = ac.receivedMessage.toReadableHex(), + receivedQuote = ac.receivedQuote.toReadableHex(), ) } } -private fun String.colorFromReadableHex(): Color = - Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong()) +fun List<ThemeOverrides>.getTheme(themeId: String?): ThemeOverrides? = + firstOrNull { it.themeId == themeId } -private fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb()) +fun List<ThemeOverrides>.getTheme(themeId: String?, type: WallpaperType?, base: DefaultTheme): ThemeOverrides? = + firstOrNull { it.themeId == themeId || it.isSame(type, base.themeName)} + +fun List<ThemeOverrides>.replace(theme: ThemeOverrides): List<ThemeOverrides> { + val index = indexOfFirst { it.themeId == theme.themeId || + // prevent situation when two themes has the same type but different theme id (maybe something was changed in prefs by hand) + it.isSame(WallpaperType.from(theme.wallpaper), theme.base.themeName) + } + return if (index != -1) { + val a = ArrayList(this) + a[index] = theme + a + } else { + this + theme + } +} + +fun List<ThemeOverrides>.sameTheme(type: WallpaperType?, themeName: String): ThemeOverrides? = firstOrNull { it.isSame(type, themeName) } + +/** See [ThemesTest.testSkipDuplicates] */ +fun List<ThemeOverrides>.skipDuplicates(): List<ThemeOverrides> { + val res = ArrayList<ThemeOverrides>() + forEach { theme -> + val themeType = WallpaperType.from(theme.wallpaper) + if (res.none { it.themeId == theme.themeId || it.isSame(themeType, theme.base.themeName) }) { + res.add(theme) + } + } + return res +} @Serializable -data class ThemeOverrides ( - val base: DefaultTheme, - val colors: ThemeColors +data class ThemeModeOverrides ( + val light: ThemeModeOverride? = null, + val dark: ThemeModeOverride? = null ) { - fun withUpdatedColor(name: ThemeColor, color: String): ThemeOverrides { + fun preferredMode(darkTheme: Boolean): ThemeModeOverride? = when (darkTheme) { + false -> light + else -> dark + } +} + +@Serializable +data class ThemeModeOverride ( + val mode: DefaultThemeMode = CurrentColors.value.base.mode, + val colors: ThemeColors = ThemeColors(), + val wallpaper: ThemeWallpaper? = null, +) { + + @Transient + val type = WallpaperType.from(wallpaper) + + fun withUpdatedColor(name: ThemeColor, color: String?): ThemeModeOverride { return copy(colors = when (name) { ThemeColor.PRIMARY -> colors.copy(primary = color) ThemeColor.PRIMARY_VARIANT -> colors.copy(primaryVariant = color) @@ -163,32 +493,129 @@ data class ThemeOverrides ( ThemeColor.BACKGROUND -> colors.copy(background = color) ThemeColor.SURFACE -> colors.copy(surface = color) ThemeColor.TITLE -> colors.copy(title = color) + ThemeColor.PRIMARY_VARIANT2 -> colors.copy(primaryVariant2 = color) ThemeColor.SENT_MESSAGE -> colors.copy(sentMessage = color) + ThemeColor.SENT_QUOTE -> colors.copy(sentQuote = color) ThemeColor.RECEIVED_MESSAGE -> colors.copy(receivedMessage = color) - }) + ThemeColor.RECEIVED_QUOTE -> colors.copy(receivedQuote = color) + ThemeColor.WALLPAPER_BACKGROUND -> colors.copy() + ThemeColor.WALLPAPER_TINT -> colors.copy() + }, wallpaper = when (name) { + ThemeColor.WALLPAPER_BACKGROUND -> wallpaper?.copy(background = color) + ThemeColor.WALLPAPER_TINT -> wallpaper?.copy(tint = color) + else -> wallpaper?.copy() + } + ) + } + + fun removeSameColors(base: DefaultTheme): ThemeModeOverride { + val c = when (base) { + DefaultTheme.LIGHT -> LightColorPalette + DefaultTheme.DARK -> DarkColorPalette + DefaultTheme.SIMPLEX -> SimplexColorPalette + DefaultTheme.BLACK -> BlackColorPalette + } + val ac = when (base) { + DefaultTheme.LIGHT -> LightColorPaletteApp + DefaultTheme.DARK -> DarkColorPaletteApp + DefaultTheme.SIMPLEX -> SimplexColorPaletteApp + DefaultTheme.BLACK -> BlackColorPaletteApp + } + val w = when (val wallpaperType = WallpaperType.from(wallpaper)) { + is WallpaperType.Preset -> { + val p = PresetWallpaper.from(wallpaperType.filename) + ThemeWallpaper( + preset = wallpaperType.filename, + scale = p?.scale ?: wallpaper?.scale, + scaleType = null, + background = p?.background?.get(base)?.toReadableHex(), + tint = p?.tint?.get(base)?.toReadableHex(), + image = null, + imageFile = null, + ) + } + is WallpaperType.Image -> { + ThemeWallpaper( + preset = null, + scale = null, + scaleType = WallpaperScaleType.FILL, + background = Color.Transparent.toReadableHex(), + tint = Color.Transparent.toReadableHex(), + image = null, + imageFile = null, + ) + } + else -> { + ThemeWallpaper() + } + } + + return copy( + colors = ThemeColors( + primary = if (colors.primary?.colorFromReadableHex() != c.primary) colors.primary else null, + primaryVariant = if (colors.primaryVariant?.colorFromReadableHex() != c.primaryVariant) colors.primaryVariant else null, + secondary = if (colors.secondary?.colorFromReadableHex() != c.secondary) colors.secondary else null, + secondaryVariant = if (colors.secondaryVariant?.colorFromReadableHex() != c.secondaryVariant) colors.secondaryVariant else null, + background = if (colors.background?.colorFromReadableHex() != c.background) colors.background else null, + surface = if (colors.surface?.colorFromReadableHex() != c.surface) colors.surface else null, + title = if (colors.title?.colorFromReadableHex() != ac.title) colors.title else null, + primaryVariant2 = if (colors.primaryVariant2?.colorFromReadableHex() != ac.primaryVariant2) colors.primary else null, + sentMessage = if (colors.sentMessage?.colorFromReadableHex() != ac.sentMessage) colors.sentMessage else null, + sentQuote = if (colors.sentQuote?.colorFromReadableHex() != ac.sentQuote) colors.sentQuote else null, + receivedMessage = if (colors.receivedMessage?.colorFromReadableHex() != ac.receivedMessage) colors.receivedMessage else null, + receivedQuote = if (colors.receivedQuote?.colorFromReadableHex() != ac.receivedQuote) colors.receivedQuote else null, + ), + wallpaper = wallpaper?.copy( + preset = wallpaper.preset, + scale = if (wallpaper.scale != w.scale) wallpaper.scale else null, + scaleType = if (wallpaper.scaleType != w.scaleType) wallpaper.scaleType else null, + background = if (wallpaper.background != w.background) wallpaper.background else null, + tint = if (wallpaper.tint != w.tint) wallpaper.tint else null, + image = wallpaper.image, + imageFile = wallpaper.imageFile, + ) + ) + } + + companion object { + fun withFilledAppDefaults(mode: DefaultThemeMode, base: DefaultTheme): ThemeModeOverride = + ThemeModeOverride( + mode = mode, + colors = ThemeOverrides(base = base).withFilledColors(base, null, null, null, null, null), + wallpaper = ThemeWallpaper(preset = PresetWallpaper.SCHOOL.filename) + ) } } -fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, shape: Shape = RectangleShape): Modifier { - return if (baseTheme == DefaultTheme.SIMPLEX) { - this.background(brush = Brush.linearGradient( - listOf( - CurrentColors.value.colors.background.darker(0.4f), - CurrentColors.value.colors.background.lighter(0.4f) - ), - Offset(0f, Float.POSITIVE_INFINITY), - Offset(Float.POSITIVE_INFINITY, 0f) - ), shape = shape) - } else { - this.background(color = CurrentColors.value.colors.background, shape = shape) +fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, bgLayerSize: MutableState<IntSize>?, bgLayer: GraphicsLayer?/*, shape: Shape = RectangleShape*/): Modifier { + return drawBehind { + copyBackgroundToAppBar(bgLayerSize, bgLayer) { + if (baseTheme == DefaultTheme.SIMPLEX) { + drawRect(brush = themedBackgroundBrush()) + } else { + drawRect(CurrentColors.value.colors.background) + } + } } } +fun themedBackgroundBrush(): Brush = Brush.linearGradient( + listOf( + CurrentColors.value.colors.background.darker(0.4f), + CurrentColors.value.colors.background.lighter(0.4f) + ), + Offset(0f, Float.POSITIVE_INFINITY), + Offset(Float.POSITIVE_INFINITY, 0f) +) + val DEFAULT_PADDING = 20.dp +val DEFAULT_ONBOARDING_HORIZONTAL_PADDING = 25.dp val DEFAULT_SPACE_AFTER_ICON = 4.dp val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2 val DEFAULT_BOTTOM_PADDING = 48.dp 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_START_MODAL_WIDTH = 388.dp val DEFAULT_MIN_CENTER_MODAL_WIDTH = 590.dp @@ -203,7 +630,6 @@ val DarkColorPalette = darkColors( // background = Color.Black, surface = Color(0xFF222222), // background = Color(0xFF121212), -// surface = Color(0xFF121212), error = Color.Red, onBackground = Color(0xFFFFFBFA), onSurface = Color(0xFFFFFBFA), @@ -211,8 +637,11 @@ val DarkColorPalette = darkColors( ) val DarkColorPaletteApp = AppColors( title = SimplexBlue, - sentMessage = Color(0x1E45B8FF), - receivedMessage = Color(0x20B1B0B5) + primaryVariant2 = Color(0xFF18262E), + sentMessage = Color(0xFF18262E), + sentQuote = Color(0xFF1D3847), + receivedMessage = Color(0xff262627), + receivedQuote = Color(0xff373739), ) val LightColorPalette = lightColors( @@ -230,8 +659,11 @@ val LightColorPalette = lightColors( ) val LightColorPaletteApp = AppColors( title = SimplexBlue, - sentMessage = Color(0x1E45B8FF), - receivedMessage = Color(0x20B1B0B5) + primaryVariant2 = Color(0xFFE9F7FF), + sentMessage = Color(0xFFE9F7FF), + sentQuote = Color(0xFFD6F0FF), + receivedMessage = Color(0xfff5f5f6), + receivedQuote = Color(0xffececee), ) val SimplexColorPalette = darkColors( @@ -250,40 +682,155 @@ val SimplexColorPalette = darkColors( ) val SimplexColorPaletteApp = AppColors( title = Color(0xFF267BE5), - sentMessage = Color(0x1E45B8FF), - receivedMessage = Color(0x20B1B0B5) + primaryVariant2 = Color(0xFF172941), + sentMessage = Color(0xFF172941), + sentQuote = Color(0xFF1C3A57), + receivedMessage = Color(0xff25283a), + receivedQuote = Color(0xff36394a), ) -val CurrentColors: MutableStateFlow<ThemeManager.ActiveTheme> = MutableStateFlow(ThemeManager.currentColors(isInNightMode())) +val BlackColorPalette = darkColors( + primary = Color(0xff0077e0), // If this value changes also need to update #0088ff in string resource files + primaryVariant = Color(0xff0077e0), + secondary = HighOrLowlight, + secondaryVariant = DarkGray, + background = Color(0xff070707), + surface = Color(0xff161617), + // background = Color(0xFF121212), + // surface = Color(0xFF121212), + error = Color.Red, + onBackground = Color(0xFFFFFBFA), + onSurface = Color(0xFFFFFBFA), + // onError: Color = Color.Black, +) +val BlackColorPaletteApp = AppColors( + title = Color(0xff0077e0), + primaryVariant2 = Color(0xff243747), + sentMessage = Color(0xFF18262E), + sentQuote = Color(0xFF1D3847), + receivedMessage = Color(0xff1b1b1b), + receivedQuote = Color(0xff29292b), +) + +var systemInDarkThemeCurrently: Boolean = isInNightMode() + +val CurrentColors: MutableStateFlow<ThemeManager.ActiveTheme> = MutableStateFlow(ThemeManager.currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())) @Composable fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLight +@Composable expect fun isSystemInDarkTheme(): Boolean +internal val LocalAppColors = staticCompositionLocalOf { LightColorPaletteApp } +internal val LocalAppWallpaper = staticCompositionLocalOf { AppWallpaper() } + +val MaterialTheme.appColors: AppColors + @Composable + @ReadOnlyComposable + get() = LocalAppColors.current + +fun AppColors.updateColorsFrom(other: AppColors) { + title = other.title + primaryVariant2 = other.primaryVariant2 + sentMessage = other.sentMessage + sentQuote = other.sentQuote + receivedMessage = other.receivedMessage + receivedQuote = other.receivedQuote +} + +fun AppWallpaper.updateWallpaperFrom(other: AppWallpaper) { + background = other.background + tint = other.tint + type = other.type +} + +val MaterialTheme.wallpaper: AppWallpaper + @Composable + @ReadOnlyComposable + get() = LocalAppWallpaper.current + fun reactOnDarkThemeChanges(isDark: Boolean) { - if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM.name && CurrentColors.value.colors.isLight == isDark) { + systemInDarkThemeCurrently = isDark + if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM_THEME_NAME && CurrentColors.value.colors.isLight == isDark) { // Change active colors from light to dark and back based on system theme - ThemeManager.applyTheme(DefaultTheme.SYSTEM.name, isDark) + ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME) } } @Composable fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) { - LaunchedEffect(darkTheme) { - // For preview - if (darkTheme != null) - CurrentColors.value = ThemeManager.currentColors(darkTheme) - } - val systemDark = isSystemInDarkTheme() - LaunchedEffect(systemDark) { - reactOnDarkThemeChanges(systemDark) +// TODO: Fix preview working with dark/light theme + +// LaunchedEffect(darkTheme) { +// // For preview +// if (darkTheme != null) +// CurrentColors.value = ThemeManager.currentColors(darkTheme, null, null, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get()) +// } + val systemDark = rememberUpdatedState(isSystemInDarkTheme()) + LaunchedEffect(Unit) { + // snapshotFlow vs LaunchedEffect reduce number of recomposes + snapshotFlow { systemDark.value } + .collect { + reactOnDarkThemeChanges(systemDark.value) + } } val theme by CurrentColors.collectAsState() + LaunchedEffect(Unit) { + // snapshotFlow vs LaunchedEffect reduce number of recomposes when user is changed or it's themes + snapshotFlow { chatModel.currentUser.value?.uiThemes } + .collect { + ThemeManager.applyTheme(appPrefs.currentTheme.get()!!) + } + } MaterialTheme( colors = theme.colors, typography = Typography, shapes = Shapes, - content = content + content = { + val density = Density(LocalDensity.current.density * desktopDensityScaleMultiplier, LocalDensity.current.fontScale * fontSizeMultiplier) + val rememberedAppColors = remember { + // Explicitly creating a new object here so we don't mutate the initial [appColors] + // provided, and overwrite the values set in it. + theme.appColors.copy() + }.apply { updateColorsFrom(theme.appColors) } + val rememberedWallpaper = remember { + // Explicitly creating a new object here so we don't mutate the initial [wallpaper] + // provided, and overwrite the values set in it. + theme.wallpaper.copy() + }.apply { updateWallpaperFrom(theme.wallpaper) } + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colors.onBackground, + LocalAppColors provides rememberedAppColors, + LocalAppWallpaper provides rememberedWallpaper, + LocalDensity provides density, + content = content) + } + ) +} + +@Composable +fun SimpleXThemeOverride(theme: ThemeManager.ActiveTheme, content: @Composable () -> Unit) { + MaterialTheme( + colors = theme.colors, + typography = Typography, + shapes = Shapes, + content = { + val rememberedAppColors = remember { + // Explicitly creating a new object here so we don't mutate the initial [appColors] + // provided, and overwrite the values set in it. + theme.appColors.copy() + }.apply { updateColorsFrom(theme.appColors) } + val rememberedWallpaper = remember { + // Explicitly creating a new object here so we don't mutate the initial [wallpaper] + // provided, and overwrite the values set in it. + theme.wallpaper.copy() + }.apply { updateWallpaperFrom(theme.wallpaper) } + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colors.onBackground, + LocalAppColors provides rememberedAppColors, + LocalAppWallpaper provides rememberedWallpaper, + content = content) + } ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt index 49d3203455..07f2b678cf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt @@ -1,14 +1,14 @@ package chat.simplex.common.ui.theme import androidx.compose.material.Colors +import androidx.compose.runtime.MutableState import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.text.font.FontFamily -import chat.simplex.res.MR -import chat.simplex.common.model.AppPreferences -import chat.simplex.common.model.ChatController -import chat.simplex.common.platform.platform -import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.views.helpers.* +import java.io.File // https://github.com/rsms/inter // I place it here because IDEA shows an error (but still works anyway) when this declaration inside Type.kt @@ -18,140 +18,223 @@ expect val EmojiFont: FontFamily object ThemeManager { private val appPrefs: AppPreferences = ChatController.appPrefs - data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors) + data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors, val wallpaper: AppWallpaper = AppWallpaper()) private fun systemDarkThemeColors(): Pair<Colors, DefaultTheme> = when (appPrefs.systemDarkTheme.get()) { - DefaultTheme.DARK.name -> DarkColorPalette to DefaultTheme.DARK - DefaultTheme.SIMPLEX.name -> SimplexColorPalette to DefaultTheme.SIMPLEX + DefaultTheme.DARK.themeName -> DarkColorPalette to DefaultTheme.DARK + DefaultTheme.SIMPLEX.themeName -> SimplexColorPalette to DefaultTheme.SIMPLEX + DefaultTheme.BLACK.themeName -> BlackColorPalette to DefaultTheme.BLACK else -> SimplexColorPalette to DefaultTheme.SIMPLEX } - fun currentColors(darkForSystemTheme: Boolean): ActiveTheme { + private fun nonSystemThemeName(): String { val themeName = appPrefs.currentTheme.get()!! - val themeOverrides = appPrefs.themeOverrides.get() - - val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { + return if (themeName != DefaultTheme.SYSTEM_THEME_NAME) { themeName } else { - if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name + if (systemInDarkThemeCurrently) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.themeName } - val theme = themeOverrides[nonSystemThemeName] + } + + fun defaultActiveTheme(appSettingsTheme: List<ThemeOverrides>): ThemeOverrides? { + val nonSystemThemeName = nonSystemThemeName() + val defaultThemeId = appPrefs.currentThemeIds.get()[nonSystemThemeName] + return appSettingsTheme.getTheme(defaultThemeId) + } + + fun defaultActiveTheme(perUserTheme: ThemeModeOverrides?, appSettingsTheme: List<ThemeOverrides>): ThemeModeOverride { + val perUserTheme = if (!CurrentColors.value.colors.isLight) perUserTheme?.dark else perUserTheme?.light + if (perUserTheme != null) { + return perUserTheme + } + val defaultTheme = defaultActiveTheme(appSettingsTheme) + return ThemeModeOverride(colors = defaultTheme?.colors ?: ThemeColors(), wallpaper = defaultTheme?.wallpaper + // Situation when user didn't change global theme at all (it is not saved yet). Using defaults + ?: ThemeWallpaper.from(PresetWallpaper.SCHOOL.toType(CurrentColors.value.base), null, null)) + } + + fun currentColors(themeOverridesForType: WallpaperType?, perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverrides?, appSettingsTheme: List<ThemeOverrides>): ActiveTheme { + val themeName = appPrefs.currentTheme.get()!! + val nonSystemThemeName = nonSystemThemeName() + val defaultTheme = defaultActiveTheme(appSettingsTheme) + val baseTheme = when (nonSystemThemeName) { - DefaultTheme.LIGHT.name -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp) - DefaultTheme.DARK.name -> Triple(DefaultTheme.DARK, DarkColorPalette, DarkColorPaletteApp) - DefaultTheme.SIMPLEX.name -> Triple(DefaultTheme.SIMPLEX, SimplexColorPalette, SimplexColorPaletteApp) - else -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp) + DefaultTheme.LIGHT.themeName -> ActiveTheme(DefaultTheme.LIGHT.themeName, DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.LIGHT))) + DefaultTheme.DARK.themeName -> ActiveTheme(DefaultTheme.DARK.themeName, DefaultTheme.DARK, DarkColorPalette, DarkColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.DARK))) + DefaultTheme.SIMPLEX.themeName -> ActiveTheme(DefaultTheme.SIMPLEX.themeName, DefaultTheme.SIMPLEX, SimplexColorPalette, SimplexColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.SIMPLEX))) + DefaultTheme.BLACK.themeName -> ActiveTheme(DefaultTheme.BLACK.themeName, DefaultTheme.BLACK, BlackColorPalette, BlackColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.BLACK))) + else -> ActiveTheme(DefaultTheme.LIGHT.themeName, DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.LIGHT))) } - if (theme == null) { - return ActiveTheme(themeName, baseTheme.first, baseTheme.second, baseTheme.third) + + val perUserTheme = if (baseTheme.colors.isLight) perUserTheme?.light else perUserTheme?.dark + val theme = (appSettingsTheme.sameTheme(themeOverridesForType ?: perChatTheme?.type ?: perUserTheme?.type ?: defaultTheme?.wallpaper?.toAppWallpaper()?.type, nonSystemThemeName) ?: defaultTheme) + + if (theme == null && perUserTheme == null && perChatTheme == null && themeOverridesForType == null) { + return ActiveTheme(themeName, baseTheme.base, baseTheme.colors, baseTheme.appColors, baseTheme.wallpaper) } - return ActiveTheme(themeName, baseTheme.first, theme.colors.toColors(theme.base), theme.colors.toAppColors(theme.base)) + val presetWallpaperTheme = when { + perChatTheme?.wallpaper != null -> if (perChatTheme.wallpaper.preset != null) PresetWallpaper.from(perChatTheme.wallpaper.preset)?.colors?.get(baseTheme.base) else null + perUserTheme?.wallpaper != null -> if (perUserTheme.wallpaper.preset != null) PresetWallpaper.from(perUserTheme.wallpaper.preset)?.colors?.get(baseTheme.base) else null + else -> if (theme?.wallpaper?.preset != null) PresetWallpaper.from(theme.wallpaper.preset)?.colors?.get(baseTheme.base) else null + } + val themeOrEmpty = theme ?: ThemeOverrides(base = baseTheme.base) + val colors = themeOrEmpty.toColors(themeOrEmpty.base, perChatTheme?.colors, perUserTheme?.colors, presetWallpaperTheme) + return ActiveTheme( + themeName, + baseTheme.base, + colors, + themeOrEmpty.toAppColors(themeOrEmpty.base, perChatTheme?.colors, perChatTheme?.type, perUserTheme?.colors, perUserTheme?.type, presetWallpaperTheme), + themeOrEmpty.toAppWallpaper(themeOverridesForType, perChatTheme, perUserTheme, colors.background) + ) } - fun currentThemeOverridesForExport(darkForSystemTheme: Boolean): ThemeOverrides { - val themeName = appPrefs.currentTheme.get()!! - val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { - themeName - } else { - if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name + fun currentThemeOverridesForExport(perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverrides?): ThemeOverrides { + val current = currentColors(null, perChatTheme, perUserTheme, appPrefs.themeOverrides.get()) + val wType = current.wallpaper.type + val wBackground = current.wallpaper.background + val wTint = current.wallpaper.tint + return ThemeOverrides( + themeId = "", + base = current.base, + colors = ThemeColors.from(current.colors, current.appColors), + wallpaper = if (wType !is WallpaperType.Empty) ThemeWallpaper.from(wType, wBackground?.toReadableHex(), wTint?.toReadableHex()).withFilledWallpaperBase64() else null + ) + } + + fun applyTheme(theme: String) { + if (appPrefs.currentTheme.get() != theme) { + appPrefs.currentTheme.set(theme) } - val overrides = appPrefs.themeOverrides.get().toMutableMap() - val nonFilledTheme = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) - return nonFilledTheme.copy(colors = nonFilledTheme.colors.withFilledColors(CurrentColors.value.base)) - } - - // colors, default theme enum, localized name of theme - fun allThemes(darkForSystemTheme: Boolean): List<Triple<Colors, DefaultTheme, String>> { - val allThemes = ArrayList<Triple<Colors, DefaultTheme, String>>() - allThemes.add( - Triple( - if (darkForSystemTheme) systemDarkThemeColors().first else LightColorPalette, - DefaultTheme.SYSTEM, - generalGetString(MR.strings.theme_system) - ) - ) - allThemes.add( - Triple( - LightColorPalette, - DefaultTheme.LIGHT, - generalGetString(MR.strings.theme_light) - ) - ) - allThemes.add( - Triple( - DarkColorPalette, - DefaultTheme.DARK, - generalGetString(MR.strings.theme_dark) - ) - ) - allThemes.add( - Triple( - SimplexColorPalette, - DefaultTheme.SIMPLEX, - generalGetString(MR.strings.theme_simplex) - ) - ) - return allThemes - } - - fun applyTheme(theme: String, darkForSystemTheme: Boolean) { - appPrefs.currentTheme.set(theme) - CurrentColors.value = currentColors(darkForSystemTheme) + CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) platform.androidSetNightModeIfSupported() + val c = CurrentColors.value.colors + platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight) } - fun changeDarkTheme(theme: String, darkForSystemTheme: Boolean) { + fun changeDarkTheme(theme: String) { appPrefs.systemDarkTheme.set(theme) - CurrentColors.value = currentColors(darkForSystemTheme) + CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) } - fun saveAndApplyThemeColor(name: ThemeColor, color: Color? = null, darkForSystemTheme: Boolean) { - val themeName = appPrefs.currentTheme.get()!! - val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { - themeName - } else { - if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name + fun saveAndApplyThemeColor(baseTheme: DefaultTheme, name: ThemeColor, color: Color? = null, pref: SharedPreference<List<ThemeOverrides>> = appPrefs.themeOverrides) { + val nonSystemThemeName = baseTheme.themeName + val overrides = pref.get() + val themeId = appPrefs.currentThemeIds.get()[nonSystemThemeName] + val prevValue = overrides.getTheme(themeId) ?: ThemeOverrides(base = baseTheme) + pref.set(overrides.replace(prevValue.withUpdatedColor(name, color?.toReadableHex()))) + val themeIds = appPrefs.currentThemeIds.get().toMutableMap() + themeIds[nonSystemThemeName] = prevValue.themeId + appPrefs.currentThemeIds.set(themeIds) + CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + } + + fun applyThemeColor(name: ThemeColor, color: Color? = null, pref: MutableState<ThemeModeOverride>) { + pref.value = pref.value.withUpdatedColor(name, color?.toReadableHex()) + } + + fun saveAndApplyWallpaper(baseTheme: DefaultTheme, type: WallpaperType?, pref: SharedPreference<List<ThemeOverrides>> = appPrefs.themeOverrides) { + val nonSystemThemeName = baseTheme.themeName + val overrides = pref.get() + val theme = overrides.sameTheme(type, baseTheme.themeName) + val prevValue = theme ?: ThemeOverrides(base = baseTheme) + pref.set(overrides.replace(prevValue.copy(wallpaper = if (type != null && type !is WallpaperType.Empty) ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint) else null))) + val themeIds = appPrefs.currentThemeIds.get().toMutableMap() + themeIds[nonSystemThemeName] = prevValue.themeId + appPrefs.currentThemeIds.set(themeIds) + CurrentColors.value = currentColors( null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + } + + fun copyFromSameThemeOverrides(type: WallpaperType?, lowerLevelOverride: ThemeModeOverride?, pref: MutableState<ThemeModeOverride>): Boolean { + val overrides = appPrefs.themeOverrides.get() + val sameWallpaper = if (lowerLevelOverride?.type?.sameType(type) == true) lowerLevelOverride.wallpaper else overrides.sameTheme(type, CurrentColors.value.base.themeName)?.wallpaper + if (sameWallpaper == null) { + if (type != null) { + pref.value = ThemeModeOverride(wallpaper = ThemeWallpaper.from(type, null, null).copy(scale = null, scaleType = null)) + } else { + // Make an empty wallpaper to override any top level ones + pref.value = ThemeModeOverride(wallpaper = ThemeWallpaper()) + } + return true } - var colorToSet = color - if (colorToSet == null) { - // Setting default color from a base theme - colorToSet = when(nonSystemThemeName) { - DefaultTheme.LIGHT.name -> name.fromColors(LightColorPalette, LightColorPaletteApp) - DefaultTheme.DARK.name -> name.fromColors(DarkColorPalette, DarkColorPaletteApp) - DefaultTheme.SIMPLEX.name -> name.fromColors(SimplexColorPalette, SimplexColorPaletteApp) - // Will not be here - else -> return + var type = sameWallpaper.toAppWallpaper().type + if (type is WallpaperType.Image && sameWallpaper.imageFile == type.filename) { + // same image file. Needs to be copied first in order to be able to remove the file once it's not needed anymore without affecting main theme override + val filename = saveWallpaperFile(File(getWallpaperFilePath(type.filename)).toURI()) + if (filename != null) { + type = WallpaperType.Image(filename, type.scale, type.scaleType) + } else { + Log.e(TAG, "Error while copying wallpaper from global overrides to chat overrides") + return false } } - val overrides = appPrefs.themeOverrides.get().toMutableMap() - val prevValue = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) - overrides[nonSystemThemeName] = prevValue.withUpdatedColor(name, colorToSet.toReadableHex()) - appPrefs.themeOverrides.set(overrides) - CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) + val prevValue = pref.value + pref.value = prevValue.copy( + colors = ThemeColors(), + wallpaper = ThemeWallpaper.from(type, null, null).copy(scale = null, scaleType = null) + ) + return true } - fun saveAndApplyThemeOverrides(theme: ThemeOverrides, darkForSystemTheme: Boolean) { - val overrides = appPrefs.themeOverrides.get().toMutableMap() - val prevValue = overrides[theme.base.name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) - overrides[theme.base.name] = prevValue.copy(colors = theme.colors) - appPrefs.themeOverrides.set(overrides) - appPrefs.currentTheme.set(theme.base.name) - CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) + fun applyWallpaper(type: WallpaperType?, pref: MutableState<ThemeModeOverride>) { + val prevValue = pref.value + pref.value = prevValue.copy( + wallpaper = if (type != null) + ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint) + else null + ) } - fun resetAllThemeColors(darkForSystemTheme: Boolean) { - val themeName = appPrefs.currentTheme.get()!! - val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { - themeName - } else { - if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name + fun saveAndApplyThemeOverrides(theme: ThemeOverrides, pref: SharedPreference<List<ThemeOverrides>> = appPrefs.themeOverrides) { + val wallpaper = theme.wallpaper?.importFromString() + val nonSystemThemeName = theme.base.themeName + val overrides = pref.get() + val prevValue = overrides.getTheme(null, wallpaper?.toAppWallpaper()?.type, theme.base) ?: ThemeOverrides(base = theme.base) + if (prevValue.wallpaper?.imageFile != null) { + File(getWallpaperFilePath(prevValue.wallpaper.imageFile)).delete() + } + pref.set(overrides.replace(prevValue.copy(base = theme.base, colors = theme.colors, wallpaper = wallpaper))) + appPrefs.currentTheme.set(nonSystemThemeName) + val currentThemeIds = appPrefs.currentThemeIds.get().toMutableMap() + currentThemeIds[nonSystemThemeName] = prevValue.themeId + appPrefs.currentThemeIds.set(currentThemeIds) + CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + } + + fun resetAllThemeColors(pref: SharedPreference<List<ThemeOverrides>> = appPrefs.themeOverrides) { + val nonSystemThemeName = nonSystemThemeName() + val themeId = appPrefs.currentThemeIds.get()[nonSystemThemeName] ?: return + val overrides = pref.get() + val prevValue = overrides.getTheme(themeId) ?: return + pref.set(overrides.replace(prevValue.copy(colors = ThemeColors(), wallpaper = prevValue.wallpaper?.copy(background = null, tint = null)))) + CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + } + + fun resetAllThemeColors(pref: MutableState<ThemeModeOverride>) { + val prevValue = pref.value + pref.value = prevValue.copy(colors = ThemeColors(), wallpaper = prevValue.wallpaper?.copy(background = null, tint = null)) + } + + fun removeTheme(themeId: String?) { + val themes = ArrayList(appPrefs.themeOverrides.get()) + themes.removeAll { it.themeId == themeId } + appPrefs.themeOverrides.set(themes) + } + + fun String.colorFromReadableHex(): Color = + Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong()) + + fun Color.toReadableHex(): String { + val s = Integer.toHexString(toArgb()) + return when { + this == Color.Transparent -> "#00ffffff" + s.length == 1 -> "#ff$s$s$s$s$s$s" + s.length == 2 -> "#ff$s$s$s" + s.length == 3 -> "#ff$s$s" + s.length == 4 && this.alpha == 0f -> "#0000$s" // 000088ff treated as 88ff + s.length == 4 -> "#ff00$s" + s.length == 6 && this.alpha == 0f -> "#00$s" + s.length == 6 -> "#ff$s" + else -> "#$s" } - val overrides = appPrefs.themeOverrides.get().toMutableMap() - val prevValue = overrides[nonSystemThemeName] ?: return - overrides[nonSystemThemeName] = prevValue.copy(colors = ThemeColors()) - appPrefs.themeOverrides.set(overrides) - CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) } } - -private fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb()) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/SplashView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/SplashView.kt index cf9a8dfb62..a049230f27 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/SplashView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/SplashView.kt @@ -1,17 +1,17 @@ package chat.simplex.common.views import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface +import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @Composable -fun SplashView() { +fun SplashView(nonTransparent: Boolean = false) { Surface( Modifier .fillMaxSize(), - color = MaterialTheme.colors.background + color = if (nonTransparent) MaterialTheme.colors.background.copy(1f) else MaterialTheme.colors.background, + contentColor = LocalContentColor.current ) { // Image( // painter = painterResource(MR.images.logo), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt index 4f01d4a39f..37aa7fc1d1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt @@ -7,36 +7,39 @@ import androidx.compose.foundation.lazy.* import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.item.CONSOLE_COMPOSE_LAYOUT_ID +import chat.simplex.common.views.chat.item.AdaptingBottomPaddingLayout +import chat.simplex.common.views.chatlist.NavigationBarBackground +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch @Composable -fun TerminalView(chatModel: ChatModel, close: () -> Unit) { +fun TerminalView(floating: Boolean = false) { val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) } - val close = { - close() - if (appPlatform.isDesktop) { - ModalManager.center.closeModals() - } - } - BackHandler(onBack = { - close() - }) TerminalLayout( composeState, + floating, sendCommand = { sendCommand(chatModel, composeState) }, - close ) } @@ -44,16 +47,16 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose val developerTools = chatModel.controller.appPrefs.developerTools.get() val prefPerformLA = chatModel.controller.appPrefs.performLA.get() val s = composeState.value.message - if (s.startsWith("/sql") && (!prefPerformLA || !developerTools)) { - val resp = CR.ChatCmdError(null, ChatError.ChatErrorChat(ChatErrorType.CommandError("Failed reading: empty"))) - chatModel.addTerminalItem(TerminalItem.cmd(null, CC.Console(s))) + if (s.text.startsWith("/sql") && (!prefPerformLA || !developerTools)) { + val resp = API.Error(null, ChatError.ChatErrorChat(ChatErrorType.CommandError("Failed reading: empty"))) + chatModel.addTerminalItem(TerminalItem.cmd(null, CC.Console(s.text))) chatModel.addTerminalItem(TerminalItem.resp(null, resp)) composeState.value = ComposeState(useLinkPreviews = false) } else { - withApi { + withBGApi { // show "in progress" // TODO show active remote host in chat console? - chatModel.controller.sendCmd(chatModel.remoteHostId(), CC.Console(s)) + chatModel.controller.sendCmd(chatModel.remoteHostId(), CC.Console(s.text)) composeState.value = ComposeState(useLinkPreviews = false) // hide "in progress" } @@ -63,21 +66,32 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose @Composable fun TerminalLayout( composeState: MutableState<ComposeState>, + floating: Boolean, sendCommand: () -> Unit, - close: () -> Unit ) { val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) val textStyle = remember { mutableStateOf(smallFont) } - fun onMessageChange(s: String) { + fun onMessageChange(s: ComposeMessage) { composeState.value = composeState.value.copy(message = s) } - - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - Scaffold( - topBar = { CloseSheetBar(close) }, - bottomBar = { - Box(Modifier.padding(horizontal = 8.dp)) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + Box(Modifier.fillMaxSize()) { + val composeViewHeight = remember { mutableStateOf(0.dp) } + AdaptingBottomPaddingLayout(Modifier, CONSOLE_COMPOSE_LAYOUT_ID, composeViewHeight) { + TerminalLog(floating, composeViewHeight) + Column( + Modifier + .layoutId(CONSOLE_COMPOSE_LAYOUT_ID) + .align(Alignment.BottomCenter) + .navigationBarsPadding() + .consumeWindowInsets(PaddingValues(bottom = if (oneHandUI.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp)) + .imePadding() + .padding(bottom = if (oneHandUI.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp) + .background(MaterialTheme.colors.background) + ) { + Divider() + Surface(Modifier.padding(horizontal = 8.dp), color = MaterialTheme.colors.background, contentColor = MaterialTheme.colors.onBackground) { SendMsgView( composeState = composeState, showVoiceRecordIcon = false, @@ -85,50 +99,74 @@ fun TerminalLayout( isDirectChat = false, liveMessageAlertShown = SharedPreference(get = { false }, set = {}), sendMsgEnabled = true, + userCantSendReason = null, + sendButtonEnabled = true, nextSendGrpInv = false, needToAllowVoiceToContact = false, allowedVoiceByPrefs = false, - userIsObserver = false, - userCanSend = true, allowVoiceToContact = {}, + placeholder = "", sendMessage = { sendCommand() }, sendLiveMessage = null, updateLiveMessage = null, editPrevMessage = {}, onMessageChange = ::onMessageChange, onFilesPasted = {}, - textStyle = textStyle + textStyle = textStyle, + focusRequester = remember { FocusRequester() } ) } - }, - modifier = Modifier.navigationBarsWithImePadding() - ) { contentPadding -> - Surface( - modifier = Modifier - .padding(contentPadding) - .fillMaxWidth(), - color = MaterialTheme.colors.background - ) { - TerminalLog() } } + if (!oneHandUI.value) { + NavigationBarBackground(true, oneHandUI.value) + } } } -private var lazyListState = 0 to 0 - @Composable -fun TerminalLog() { - val listState = rememberLazyListState(lazyListState.first, lazyListState.second) - DisposableEffect(Unit) { - onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } - } +fun TerminalLog(floating: Boolean, composeViewHeight: State<Dp>) { val reversedTerminalItems by remember { derivedStateOf { chatModel.terminalItems.value.asReversed() } } - val clipboard = LocalClipboardManager.current - LazyColumn(state = listState, reverseLayout = true) { - items(reversedTerminalItems) { item -> + val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState() + var autoScrollToBottom = rememberSaveable { mutableStateOf(true) } + LaunchedEffect(Unit) { + launch { + snapshotFlow { listState.layoutInfo.totalItemsCount } + .filter { autoScrollToBottom.value } + .collect { + try { + listState.scrollToItem(0) + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + } + } + } + var oldNumberOfElements = listState.layoutInfo.totalItemsCount + launch { + snapshotFlow { listState.firstVisibleItemIndex } + .drop(1) + .collect { + if (oldNumberOfElements != listState.layoutInfo.totalItemsCount) { + oldNumberOfElements = listState.layoutInfo.totalItemsCount + return@collect + } + autoScrollToBottom.value = it == 0 + } + } + } + LazyColumnWithScrollBar ( + state = listState, + contentPadding = PaddingValues( + top = topPaddingToContent(false), + bottom = composeViewHeight.value + ), + reverseLayout = true, + additionalBarOffset = composeViewHeight + ) { + items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item -> + val clipboard = LocalClipboardManager.current val rhId = item.remoteHostId val rhIdStr = if (rhId == null) "" else "$rhId " Text( @@ -139,20 +177,37 @@ fun TerminalLog() { modifier = Modifier .fillMaxWidth() .clickable { - ModalManager.start.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) { - SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) { - val details = item.details - .let { - if (it.length < 100_000) it - else it.substring(0, 100_000) - } - Text(details, modifier = Modifier.heightIn(max = 50_000.dp).padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING)) + val modalPlace = if (floating) { + ModalManager.floatingTerminal + } else { + ModalManager.start + } + modalPlace.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) { + ColumnWithScrollBar { + SelectionContainer { + val details = item.details + .let { + if (it.length < 100_000) it + else it.substring(0, 100_000) + } + Text(details, modifier = Modifier.heightIn(max = 50_000.dp).padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING)) + } } } }.padding(horizontal = 8.dp, vertical = 4.dp) ) } } + DisposableEffect(Unit) { + val terminals = chatModel.terminalsVisible.toMutableSet() + terminals += floating + chatModel.terminalsVisible = terminals + onDispose { + val terminals = chatModel.terminalsVisible.toMutableSet() + terminals -= floating + chatModel.terminalsVisible = terminals + } + } } @Preview/*( @@ -166,7 +221,7 @@ fun PreviewTerminalLayout() { TerminalLayout( composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, sendCommand = {}, - close = {} + floating = false ) } } 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 29d7033290..8317c6cf6c 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 @@ -2,8 +2,8 @@ package chat.simplex.common.views 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 @@ -17,11 +17,12 @@ import androidx.compose.ui.graphics.SolidColor 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.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.* @@ -39,8 +40,6 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { val scrollState = rememberScrollState() val keyboardState by getKeyboardState() var savedKeyboardState by remember { mutableStateOf(keyboardState) } - - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { Box( modifier = Modifier .fillMaxSize() @@ -49,11 +48,9 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { val displayName = rememberSaveable { mutableStateOf("") } val focusRequester = remember { FocusRequester() } - Column( - modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()) - ) { + ColumnWithScrollBar { Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { - AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = 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), @@ -101,7 +98,6 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { } } } - } } @Composable @@ -110,49 +106,39 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) { val scrollState = rememberScrollState() val keyboardState by getKeyboardState() var savedKeyboardState by remember { mutableStateOf(keyboardState) } - - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(top = 20.dp) - ) { - val displayName = rememberSaveable { mutableStateOf("") } - val focusRequester = remember { FocusRequester() } - - Column( - modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()) - ) { - /*CloseSheetBar(close = { - if (chatModel.users.isEmpty()) { - chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo - } else { - close() + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ModalView({ + if (chatModel.users.none { !it.user.hidden }) { + appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + } else { + close() + } + }) { + 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) } - })*/ - Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { - AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING) 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)) - Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Text( - stringResource(MR.strings.display_name), - fontSize = 16.sp - ) - val name = displayName.value.trim() - val validName = mkValidName(name) - Spacer(Modifier.height(20.dp)) - if (name != validName) { - IconButton({ showInvalidNameAlert(mkValidName(displayName.value), displayName) }, Modifier.size(20.dp)) { - Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) - } - } - } - ProfileNameField(displayName, "", { it.trim() == mkValidName(it) }, focusRequester) + ProfileNameField(displayName, stringResource(MR.strings.display_name), { it.trim() == mkValidName(it) }, focusRequester) } Spacer(Modifier.fillMaxHeight().weight(1f)) - OnboardingButtons(displayName, close) + Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).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, + onboarding = null, + enabled = canCreateProfile(displayName.value), + onclick = { createProfileOnboarding(chat.simplex.common.platform.chatModel, displayName.value, close) } + ) + // Reserve space + TextButtonBelowOnboardingButton("", null) + } LaunchedEffect(Unit) { delay(300) @@ -175,10 +161,12 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) { } fun createProfileInNoProfileSetup(displayName: String, close: () -> Unit) { - withApi { - val user = controller.apiCreateActiveUser(null, Profile(displayName.trim(), "", null)) ?: return@withApi - controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress) - chatModel.chatRunning.value = false + withBGApi { + val user = controller.apiCreateActiveUser(null, Profile(displayName.trim(), "", null)) ?: return@withBGApi + if (!chatModel.connectedToRemote()) { + chatModel.localUserCreated.value = true + } + controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_ChooseServerOperators) controller.startChat(user) controller.switchUIRemoteHost(null) close() @@ -186,15 +174,15 @@ fun createProfileInNoProfileSetup(displayName: String, close: () -> Unit) { } fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) { - withApi { + withBGApi { val rhId = chatModel.remoteHostId() val user = chatModel.controller.apiCreateActiveUser( rhId, Profile(displayName.trim(), "", null) - ) ?: return@withApi + ) ?: return@withBGApi chatModel.currentUser.value = user if (chatModel.users.isEmpty()) { chatModel.controller.startChat(user) - chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress) + chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode) } else { val users = chatModel.controller.listUsers(rhId) chatModel.users.clear() @@ -206,16 +194,18 @@ fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () } fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () -> Unit) { - withApi { + withBGApi { chatModel.currentUser.value = chatModel.controller.apiCreateActiveUser( null, Profile(displayName.trim(), "", null) - ) ?: return@withApi + ) ?: return@withBGApi + chatModel.localUserCreated.value = true val onboardingStage = chatModel.controller.appPrefs.onboardingStage - if (chatModel.users.isEmpty()) { + // No users or no visible users + if (chatModel.users.none { u -> !u.user.hidden }) { onboardingStage.set(if (appPlatform.isDesktop && chatModel.controller.appPrefs.initialRandomDBPassphrase.get() && !chatModel.desktopOnboardingRandomPassword.value) { OnboardingStage.Step2_5_SetupDatabasePassphrase } else { - OnboardingStage.Step3_CreateSimpleXAddress + OnboardingStage.Step3_ChooseServerOperators }) } else { // the next two lines are only needed for failure case when because of the database error the app gets stuck on on-boarding screen, @@ -226,28 +216,6 @@ fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () } } -@Composable -fun OnboardingButtons(displayName: MutableState<String>, close: () -> Unit) { - Row { - SimpleButtonDecorated( - text = stringResource(MR.strings.about_simplex), - icon = painterResource(MR.images.ic_arrow_back_ios_new), - textDecoration = TextDecoration.None, - fontWeight = FontWeight.Medium - ) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } - Spacer(Modifier.fillMaxWidth().weight(1f)) - val enabled = canCreateProfile(displayName.value) - val createModifier: Modifier = Modifier.clickable(enabled) { createProfileOnboarding(chatModel, displayName.value, close) }.padding(8.dp) - val createColor: Color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary - Surface(shape = RoundedCornerShape(20.dp), color = Color.Transparent) { - Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) { - Text(stringResource(MR.strings.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium) - Icon(painterResource(MR.images.ic_arrow_forward_ios), stringResource(MR.strings.create_profile_button), tint = createColor) - } - } - } -} - @Composable fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isValid: (String) -> Boolean = { true }, focusRequester: FocusRequester? = null) { var valid by rememberSaveable { mutableStateOf(true) } @@ -265,15 +233,12 @@ fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isVal } val modifier = Modifier .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING) - .navigationBarsWithImePadding() + .heightIn(min = 50.dp) .onFocusChanged { focused = it.isFocused } - Box( + Column( Modifier - .fillMaxWidth() - .height(52.dp) - .border(border = BorderStroke(1.dp, strokeColor), shape = RoundedCornerShape(50)), - contentAlignment = Alignment.Center + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally ) { BasicTextField( value = name.value, @@ -281,8 +246,32 @@ fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isVal modifier = if (focusRequester == null) modifier else modifier.focusRequester(focusRequester), textStyle = TextStyle(fontSize = 18.sp, color = colors.onBackground), singleLine = true, - cursorBrush = SolidColor(MaterialTheme.colors.secondary) + cursorBrush = SolidColor(MaterialTheme.colors.secondary), + decorationBox = @Composable { innerTextField -> + TextFieldDefaults.TextFieldDecorationBox( + value = name.value, + innerTextField = innerTextField, + placeholder = if (placeholder != "") {{ Text(placeholder, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary, lineHeight = 22.sp)) }} else null, + contentPadding = PaddingValues(), + label = null, + visualTransformation = VisualTransformation.None, + leadingIcon = null, + trailingIcon = if (!valid && placeholder != "") { + { + IconButton({ showInvalidNameAlert(mkValidName(name.value), name) }, Modifier.size(20.dp)) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) + } + } + } else null, + singleLine = true, + enabled = true, + isError = false, + interactionSource = remember { MutableInteractionSource() }, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified) + ) + } ) + Divider(color = strokeColor) } LaunchedEffect(Unit) { snapshotFlow { name.value } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt index d0c9a6e4c6..d6ab57a70d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt @@ -1,8 +1,8 @@ package chat.simplex.common.views.call -import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.* import chat.simplex.common.platform.* -import chat.simplex.common.views.helpers.withApi +import chat.simplex.common.views.helpers.withBGApi import kotlinx.datetime.Clock import kotlin.time.Duration.Companion.minutes @@ -13,8 +13,8 @@ class CallManager(val chatModel: ChatModel) { callInvitations[invitation.contact.id] = invitation if (invitation.user.showNotifications) { if (Clock.System.now() - invitation.callTs <= 3.minutes) { + invitation.sentNotification = ntfManager.notifyCallInvitation(invitation) activeCallInvitation.value = invitation - ntfManager.notifyCallInvitation(invitation) } else { val contact = invitation.contact ntfManager.displayNotification(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText) @@ -23,31 +23,36 @@ class CallManager(val chatModel: ChatModel) { } } - fun acceptIncomingCall(invitation: RcvCallInvitation) { + fun acceptIncomingCall(invitation: RcvCallInvitation) = withBGApi { val call = chatModel.activeCall.value - if (call == null) { - justAcceptIncomingCall(invitation = invitation) + val contactInfo = chatModel.controller.apiContactInfo(invitation.remoteHostId, invitation.contact.contactId) + val profile = contactInfo?.second ?: invitation.user.profile.toProfile() + // In case the same contact calling while previous call didn't end yet (abnormal ending of call from the other side) + if (call == null || (call.remoteHostId == invitation.remoteHostId && call.contact.id == invitation.contact.id)) { + justAcceptIncomingCall(invitation = invitation, profile) } else { - withApi { - chatModel.switchingCall.value = true - try { - endCall(call = call) - justAcceptIncomingCall(invitation = invitation) - } finally { - withApi { chatModel.switchingCall.value = false } - } + chatModel.switchingCall.value = true + try { + endCall(call = call) + justAcceptIncomingCall(invitation = invitation, profile) + } finally { + chatModel.switchingCall.value = false } } } - private fun justAcceptIncomingCall(invitation: RcvCallInvitation) { + private fun justAcceptIncomingCall(invitation: RcvCallInvitation, userProfile: Profile) { with (chatModel) { + activeCall.value?.androidCallState?.close() activeCall.value = Call( remoteHostId = invitation.remoteHostId, + userProfile = userProfile, contact = invitation.contact, + callUUID = invitation.callUUID, callState = CallState.InvitationAccepted, - localMedia = invitation.callType.media, + initialCallType = invitation.callType.media, sharedKey = invitation.sharedKey, + androidCallState = platform.androidCreateActiveCallState() ) showCallView.value = true val useRelay = controller.appPrefs.webrtcPolicyRelay.get() @@ -68,17 +73,24 @@ class CallManager(val chatModel: ChatModel) { } suspend fun endCall(call: Call) { - with (chatModel) { + with(chatModel) { + // If there is active call currently and it's with other contact, don't interrupt it + if (activeCall.value != null && !(activeCall.value?.remoteHostId == call.remoteHostId && activeCall.value?.contact?.id == call.contact.id)) return + + // Don't destroy WebView if you plan to accept next call right after this one + if (!switchingCall.value) { + showCallView.value = false + activeCall.value?.androidCallState?.close() + activeCall.value = null + activeCallViewIsCollapsed.value = false + platform.androidCallEnded() + } if (call.callState == CallState.Ended) { Log.d(TAG, "CallManager.endCall: call ended") - activeCall.value = null - showCallView.value = false } else { Log.d(TAG, "CallManager.endCall: ending call...") - callCommand.add(WCallCommand.End) - showCallView.value = false + //callCommand.add(WCallCommand.End) controller.apiEndCall(call.remoteHostId, call.contact) - activeCall.value = null } } } @@ -90,7 +102,7 @@ class CallManager(val chatModel: ChatModel) { activeCallInvitation.value = null ntfManager.cancelCallNotification() } - withApi { + withBGApi { if (!controller.apiRejectCall(invitation.remoteHostId, invitation.contact)) { Log.e(TAG, "apiRejectCall error") } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt index 2f4ffbb836..8f5aba138d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt @@ -1,6 +1,27 @@ package chat.simplex.common.views.call import androidx.compose.runtime.Composable +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.platform.* +import kotlinx.coroutines.* @Composable expect fun ActiveCallView() + +fun activeCallWaitDeliveryReceipt(scope: CoroutineScope) = scope.launch(Dispatchers.Default) { + for (msg in controller.messagesChannel) { + val call = chatModel.activeCall.value + if (call == null || call.callState > CallState.InvitationSent) break + if (msg.rhId == call.remoteHostId && + msg is API.Result && + msg.res is CR.ChatItemsStatusesUpdated && + msg.res.chatItems.any { + it.chatInfo.id == call.contact.id && it.chatItem.content is CIContent.SndCall && it.chatItem.meta.itemStatus is CIStatus.SndRcvd + } + ) { + CallSoundsPlayer.startInCallSound(scope) + break + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt index 47dd0a27d1..4d8c1fae46 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt @@ -15,11 +15,10 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import chat.simplex.common.model.* +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.ProfileImage import chat.simplex.common.views.usersettings.ProfilePreview -import chat.simplex.common.platform.ntfManager -import chat.simplex.common.platform.SoundPlayer import chat.simplex.res.MR import kotlinx.datetime.Clock @@ -27,7 +26,11 @@ import kotlinx.datetime.Clock fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) { val cm = chatModel.callManager val scope = rememberCoroutineScope() - LaunchedEffect(true) { SoundPlayer.start(scope, sound = !chatModel.showCallView.value) } + LaunchedEffect(Unit) { + if (chatModel.activeCallInvitation.value?.sentNotification == false || appPlatform.isDesktop) { + SoundPlayer.start(scope, sound = !chatModel.showCallView.value) + } + } DisposableEffect(true) { onDispose { SoundPlayer.stop() } } IncomingCallAlertLayout( invitation, @@ -50,7 +53,7 @@ fun IncomingCallAlertLayout( acceptCall: () -> Unit ) { val color = if (isInDarkTheme()) MaterialTheme.colors.surface else IncomingCallLight - Column(Modifier.fillMaxWidth().background(color).padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) { + Column(Modifier.fillMaxWidth().background(color).statusBarsPadding().padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) { IncomingCallInfo(invitation, chatModel) Spacer(Modifier.height(8.dp)) Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { @@ -85,7 +88,8 @@ fun IncomingCallInfo(invitation: RcvCallInvitation, chatModel: ChatModel) { private fun CallButton(text: String, icon: Painter, color: Color, action: () -> Unit) { Surface( shape = RoundedCornerShape(10.dp), - color = Color.Transparent + color = Color.Transparent, + contentColor = LocalContentColor.current ) { Column( Modifier @@ -111,6 +115,7 @@ fun PreviewIncomingCallAlertLayout() { contact = Contact.sampleData, callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)), sharedKey = null, + callUUID = "", callTs = Clock.System.now() ), chatModel = ChatModel, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt index 3e79dfb4fc..705fc6a28f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt @@ -2,31 +2,38 @@ package chat.simplex.common.views.call import chat.simplex.common.views.helpers.generalGetString import chat.simplex.common.model.* +import chat.simplex.common.platform.appPlatform import chat.simplex.res.MR import kotlinx.datetime.Instant import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import java.io.Closeable import java.net.URI -import java.util.* import kotlin.collections.ArrayList data class Call( val remoteHostId: Long?, + val userProfile: Profile, val contact: Contact, + val callUUID: String?, val callState: CallState, - val localMedia: CallMediaType, + val initialCallType: CallMediaType, + val localMediaSources: CallMediaSources = CallMediaSources(mic = true, camera = initialCallType == CallMediaType.Video), val localCapabilities: CallCapabilities? = null, - val peerMedia: CallMediaType? = null, + val peerMediaSources: CallMediaSources = CallMediaSources(), val sharedKey: String? = null, - val audioEnabled: Boolean = true, - val videoEnabled: Boolean = localMedia == CallMediaType.Video, - val soundSpeaker: Boolean = localMedia == CallMediaType.Video, var localCamera: VideoCamera = VideoCamera.User, val connectionInfo: ConnectionInfo? = null, - var connectedAt: Instant? = null + var connectedAt: Instant? = null, + + // When a user has audio call, and then he wants to enable camera but didn't grant permissions for using camera yet, + // we show permissions view without enabling camera before permissions are granted. After they are granted, enabling camera + val wantsToEnableCamera: Boolean = false, + + val androidCallState: Closeable ) { val encrypted: Boolean get() = localEncrypted && sharedKey != null - val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false + private val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false val encryptionStatus: String get() = when(callState) { CallState.WaitCapabilities -> "" @@ -35,7 +42,8 @@ data class Call( else -> generalGetString(if (!localEncrypted) MR.strings.status_no_e2e_encryption else if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_e2e_encrypted) } - val hasMedia: Boolean get() = callState == CallState.OfferSent || callState == CallState.Negotiated || callState == CallState.Connected + val hasVideo: Boolean + get() = localMediaSources.hasVideo || peerMediaSources.hasVideo } enum class CallState { @@ -65,16 +73,28 @@ enum class CallState { @Serializable data class WVAPICall(val corrId: Int? = null, val command: WCallCommand) @Serializable data class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null) +@Serializable data class CallMediaSources( + val mic: Boolean = false, + val camera: Boolean = false, + val screenAudio: Boolean = false, + val screenVideo: Boolean = false +) { + val hasVideo: Boolean + get() = camera || screenVideo +} + @Serializable sealed class WCallCommand { @Serializable @SerialName("capabilities") data class Capabilities(val media: CallMediaType): WCallCommand() + @Serializable @SerialName("permission") data class Permission(val title: String, val chrome: String, val safari: String): WCallCommand() @Serializable @SerialName("start") data class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand() @Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand() @Serializable @SerialName("answer") data class Answer (val answer: String, val iceCandidates: String): WCallCommand() @Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallCommand() - @Serializable @SerialName("media") data class Media(val media: CallMediaType, val enable: Boolean): WCallCommand() + @Serializable @SerialName("media") data class Media(val source: CallMediaSource, val enable: Boolean): WCallCommand() @Serializable @SerialName("camera") data class Camera(val camera: VideoCamera): WCallCommand() @Serializable @SerialName("description") data class Description(val state: String, val description: String): WCallCommand() + @Serializable @SerialName("layout") data class Layout(val layout: LayoutType): WCallCommand() @Serializable @SerialName("end") object End: WCallCommand() } @@ -86,6 +106,7 @@ sealed class WCallResponse { @Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallResponse() @Serializable @SerialName("connection") data class Connection(val state: ConnectionState): WCallResponse() @Serializable @SerialName("connected") data class Connected(val connectionInfo: ConnectionInfo): WCallResponse() + @Serializable @SerialName("peerMedia") data class PeerMedia(val source: CallMediaSource, val enabled: Boolean): WCallResponse() @Serializable @SerialName("end") object End: WCallResponse() @Serializable @SerialName("ended") object Ended: WCallResponse() @Serializable @SerialName("ok") object Ok: WCallResponse() @@ -102,6 +123,7 @@ sealed class WCallResponse { val contact: Contact, val callType: CallType, val sharedKey: String? = null, + val callUUID: String, val callTs: Instant ) { val callTypeText: String get() = generalGetString(when(callType.media) { @@ -112,6 +134,9 @@ sealed class WCallResponse { CallMediaType.Video -> MR.strings.incoming_video_call CallMediaType.Audio -> MR.strings.incoming_audio_call }) + + // Shows whether notification was shown or not to prevent playing sound twice in both notification and in-app + var sentNotification: Boolean = false } @Serializable data class CallCapabilities(val encryption: Boolean) @Serializable data class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) { @@ -157,6 +182,14 @@ enum class CallMediaType { @SerialName("audio") Audio } +@Serializable +enum class CallMediaSource { + @SerialName("mic") Mic, + @SerialName("camera") Camera, + @SerialName("screenAudio") ScreenAudio, + @SerialName("screenVideo") ScreenVideo +} + @Serializable enum class VideoCamera { @SerialName("user") User, @@ -164,6 +197,13 @@ enum class VideoCamera { val flipped: VideoCamera get() = if (this == User) Environment else User } +@Serializable +enum class LayoutType { + @SerialName("default") Default, + @SerialName("localVideo") LocalVideo, + @SerialName("remoteVideo") RemoteVideo +} + @Serializable data class ConnectionState( val connectionState: String, @@ -173,10 +213,11 @@ data class ConnectionState( ) // the servers are expected in this format: -// stun:stun.simplex.im:443?transport=tcp -// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443?transport=tcp +// stuns:stun.simplex.im:443?transport=tcp +// turns:private2:Hxuq2QxUjnhj96Zq2r4HjqHRj@turn.simplex.im:443?transport=tcp fun parseRTCIceServer(str: String): RTCIceServer? { var s = replaceScheme(str, "stun:") + s = replaceScheme(s, "stuns:") s = replaceScheme(s, "turn:") s = replaceScheme(s, "turns:") val u = runCatching { URI(s) }.getOrNull() @@ -184,7 +225,7 @@ fun parseRTCIceServer(str: String): RTCIceServer? { val scheme = u.scheme val host = u.host val port = u.port - if (u.path == "" && (scheme == "stun" || scheme == "turn" || scheme == "turns")) { + if (u.path == "" && (scheme == "stun" || scheme == "stuns" || scheme == "turn" || scheme == "turns")) { val userInfo = u.userInfo?.split(":") val query = if (u.query == null || u.query == "") "" else "?${u.query}" return RTCIceServer( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 694ec2ba18..2a77d0a6dc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -12,37 +12,50 @@ import SectionView import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.* import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.* +import chat.simplex.common.views.call.CallMediaType +import chat.simplex.common.views.chatlist.* import androidx.compose.ui.text.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.group.ChatTTLSection import chat.simplex.common.views.chatlist.updateChatSettings +import chat.simplex.common.views.database.* import chat.simplex.common.views.newchat.* import chat.simplex.res.MR +import kotlinx.coroutines.* import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.datetime.Clock +import kotlinx.serialization.encodeToString +import java.io.File @Composable fun ChatInfoView( + chatsCtx: ChatModel.ChatsContext, chatModel: ChatModel, contact: Contact, connectionStats: ConnectionStats?, @@ -50,10 +63,11 @@ fun ChatInfoView( localAlias: String, connectionCode: String?, close: () -> Unit, + onSearchClicked: () -> Unit ) { BackHandler(onBack = close) val contact = rememberUpdatedState(contact).value - val chat = remember(contact.id) { chatModel.chats.firstOrNull { it.id == contact.id } } + val chat = remember(contact.id) { chatModel.chats.value.firstOrNull { it.id == contact.id } } val currentUser = remember { chatModel.currentUser }.value val connStats = remember(contact.id, connectionStats) { mutableStateOf(connectionStats) } val developerTools = chatModel.controller.appPrefs.developerTools.get() @@ -63,17 +77,28 @@ fun ChatInfoView( } val chatRh = chat.remoteHostId val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) } + val chatItemTTL = remember(contact.id) { mutableStateOf(if (contact.chatItemTTL != null) ChatItemTTL.fromSeconds(contact.chatItemTTL) else null) } + val deletingItems = rememberSaveable(contact.id) { mutableStateOf(false) } + ChatInfoLayout( chat, contact, currentUser, sendReceipts = sendReceipts, setSendReceipts = { sendRcpts -> - withApi { - val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool) - updateChatSettings(chat, chatSettings, chatModel) - sendReceipts.value = sendRcpts + val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool) + updateChatSettings(chat.remoteHostId, chat.chatInfo, chatSettings, chatModel) + sendReceipts.value = sendRcpts + }, + chatItemTTL = chatItemTTL, + setChatItemTTL = { + if (it == chatItemTTL.value) { + return@ChatInfoLayout } + val previousChatTTL = chatItemTTL.value + chatItemTTL.value = it + + setChatTTLAlert(chatsCtx, chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems) }, connStats = connStats, contactNetworkStatus.value, @@ -93,14 +118,16 @@ fun ChatInfoView( } }, deleteContact = { deleteContactDialog(chat, chatModel, close) }, - clearChat = { clearChatDialog(chat, chatModel, close) }, + clearChat = { clearChatDialog(chat, close) }, switchContactAddress = { showSwitchAddressAlert(switchAddress = { - withApi { + withBGApi { val cStats = chatModel.controller.apiSwitchContact(chatRh, contact.contactId) connStats.value = cStats if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnectionStats(chatRh, contact, cStats) + } } close.invoke() } @@ -108,33 +135,27 @@ fun ChatInfoView( }, abortSwitchContactAddress = { showAbortSwitchAddressAlert(abortSwitchAddress = { - withApi { + withBGApi { val cStats = chatModel.controller.apiAbortSwitchContact(chatRh, contact.contactId) connStats.value = cStats if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnectionStats(chatRh, contact, cStats) + } } } }) }, syncContactConnection = { - withApi { - val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) - connStats.value = cStats - if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) - } + withBGApi { + syncContactConnection(chatRh, contact, connStats, force = false) close.invoke() } }, syncContactConnectionForce = { showSyncConnectionForceAlert(syncConnectionForce = { - withApi { - val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = true) - connStats.value = cStats - if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) - } + withBGApi { + syncContactConnection(chatRh, contact, connStats, force = true) close.invoke() } }) @@ -149,14 +170,16 @@ fun ChatInfoView( verify = { code -> chatModel.controller.apiVerifyContact(chatRh, ct.contactId, code)?.let { r -> val (verified, existingCode) = r - chatModel.updateContact( - chatRh, - ct.copy( - activeConn = ct.activeConn?.copy( - connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact( + chatRh, + ct.copy( + activeConn = ct.activeConn?.copy( + connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null + ) ) ) - ) + } r } }, @@ -164,11 +187,24 @@ fun ChatInfoView( ) } } - } + }, + close = close, + onSearchClicked = onSearchClicked, + deletingItems = deletingItems ) } } +suspend fun syncContactConnection(rhId: Long?, contact: Contact, connectionStats: MutableState<ConnectionStats?>, force: Boolean) { + val cStats = chatModel.controller.apiSyncContactRatchet(rhId, contact.contactId, force = force) + connectionStats.value = cStats + if (cStats != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnectionStats(rhId, contact, cStats) + } + } +} + sealed class SendReceipts { object Yes: SendReceipts() object No: SendReceipts() @@ -199,40 +235,42 @@ sealed class SendReceipts { fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) { val chatInfo = chat.chatInfo + if (chatInfo is ChatInfo.Direct) { + val contact = chatInfo.contact + when { + contact.sndReady && contact.active && !chatInfo.chatDeleted -> + deleteContactOrConversationDialog(chat, contact, chatModel, close) + + contact.sndReady && contact.active && chatInfo.chatDeleted -> + deleteContactWithoutConversation(chat, chatModel, close) + + else -> // !(contact.sndReady && contact.active) + deleteNotReadyContact(chat, chatModel, close) + } + } +} + +private fun deleteContactOrConversationDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)?) { AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.delete_contact_question), - text = AnnotatedString(generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning)), buttons = { Column { - if (chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) { - // Delete and notify contact - SectionItemView({ - AlertManager.shared.hideAlert() - withApi { - deleteContact(chat, chatModel, close, notify = true) - } - }) { - Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) - } - // Delete - SectionItemView({ - AlertManager.shared.hideAlert() - withApi { - deleteContact(chat, chatModel, close, notify = false) - } - }) { - Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) - } - } else { - // Delete - SectionItemView({ - AlertManager.shared.hideAlert() - withApi { - deleteContact(chat, chatModel, close) - } - }) { - Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + // Only delete conversation + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact(chat, chatModel, close, chatDeleteMode = ChatDeleteMode.Messages()) + if (chatModel.controller.appPrefs.showDeleteConversationNotice.get()) { + showDeleteConversationNotice(contact) } + }) { + Text(generalGetString(MR.strings.only_delete_conversation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + // Delete contact + SectionItemView({ + AlertManager.shared.hideAlert() + deleteActiveContactDialog(chat, contact, chatModel, close) + }) { + Text(generalGetString(MR.strings.button_delete_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) } // Cancel SectionItemView({ @@ -245,13 +283,207 @@ fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = ) } -fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, notify: Boolean? = null) { +private fun showDeleteConversationNotice(contact: Contact) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.conversation_deleted), + text = String.format(generalGetString(MR.strings.you_can_still_send_messages_to_contact), contact.displayName), + confirmText = generalGetString(MR.strings.ok), + dismissText = generalGetString(MR.strings.dont_show_again), + onDismiss = { + chatModel.controller.appPrefs.showDeleteConversationNotice.set(false) + }, + ) +} + +sealed class ContactDeleteMode { + class Full: ContactDeleteMode() + class Entity: ContactDeleteMode() + + fun toChatDeleteMode(notify: Boolean): ChatDeleteMode = + when (this) { + is Full -> ChatDeleteMode.Full(notify) + is Entity -> ChatDeleteMode.Entity(notify) + } +} + +private fun deleteActiveContactDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)? = null) { + val contactDeleteMode = mutableStateOf<ContactDeleteMode>(ContactDeleteMode.Full()) + + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.delete_contact_question), + text = generalGetString(MR.strings.delete_contact_cannot_undo_warning), + buttons = { + Column { + // Keep conversation toggle + SectionItemView { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text(stringResource(MR.strings.keep_conversation)) + Spacer(Modifier.width(DEFAULT_PADDING)) + DefaultSwitch( + checked = contactDeleteMode.value is ContactDeleteMode.Entity, + onCheckedChange = { + contactDeleteMode.value = + if (it) ContactDeleteMode.Entity() else ContactDeleteMode.Full() + }, + ) + } + } + // Delete without notification + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.value.toChatDeleteMode(notify = false)) + if (contactDeleteMode.value is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) { + showDeleteContactNotice(contact) + } + }) { + Text(generalGetString(MR.strings.delete_without_notification), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + // Delete contact and notify + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.value.toChatDeleteMode(notify = true)) + if (contactDeleteMode.value is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) { + showDeleteContactNotice(contact) + } + }) { + Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) +} + +private fun deleteContactWithoutConversation(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.confirm_delete_contact_question), + text = generalGetString(MR.strings.delete_contact_cannot_undo_warning), + buttons = { + Column { + // Delete and notify contact + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact( + chat, + chatModel, + close, + chatDeleteMode = ContactDeleteMode.Full().toChatDeleteMode(notify = true) + ) + }) { + Text( + generalGetString(MR.strings.delete_and_notify_contact), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.error + ) + } + // Delete without notification + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact( + chat, + chatModel, + close, + chatDeleteMode = ContactDeleteMode.Full().toChatDeleteMode(notify = false) + ) + }) { + Text( + generalGetString(MR.strings.delete_without_notification), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.error + ) + } + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text( + stringResource(MR.strings.cancel_verb), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.primary + ) + } + } + ) +} + +private fun deleteNotReadyContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.confirm_delete_contact_question), + text = generalGetString(MR.strings.delete_contact_cannot_undo_warning), + buttons = { + // Confirm + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact( + chat, + chatModel, + close, + chatDeleteMode = ContactDeleteMode.Full().toChatDeleteMode(notify = false) + ) + }) { + Text( + generalGetString(MR.strings.confirm_verb), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.error + ) + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text( + stringResource(MR.strings.cancel_verb), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.primary + ) + } + } + ) +} + +private fun showDeleteContactNotice(contact: Contact) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.contact_deleted), + text = String.format(generalGetString(MR.strings.you_can_still_view_conversation_with_contact), contact.displayName), + confirmText = generalGetString(MR.strings.ok), + dismissText = generalGetString(MR.strings.dont_show_again), + onDismiss = { + chatModel.controller.appPrefs.showDeleteContactNotice.set(false) + }, + ) +} + +fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) { val chatInfo = chat.chatInfo - withApi { + withBGApi { val chatRh = chat.remoteHostId - val r = chatModel.controller.apiDeleteChat(chatRh, chatInfo.chatType, chatInfo.apiId, notify) - if (r) { - chatModel.removeChat(chatRh, chatInfo.id) + val ct = chatModel.controller.apiDeleteContact(chatRh, chatInfo.apiId, chatDeleteMode) + if (ct != null) { + withContext(Dispatchers.Main) { + when (chatDeleteMode) { + is ChatDeleteMode.Full -> + chatModel.chatsContext.removeChat(chatRh, chatInfo.id) + is ChatDeleteMode.Entity -> + chatModel.chatsContext.updateContact(chatRh, ct) + is ChatDeleteMode.Messages -> + chatModel.chatsContext.clearChat(chatRh, ChatInfo.Direct(ct)) + } + } if (chatModel.chatId.value == chatInfo.id) { chatModel.chatId.value = null ModalManager.end.closeModals() @@ -262,23 +494,22 @@ fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, notify } } -fun clearChatDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) { - val chatInfo = chat.chatInfo +fun clearChatDialog(chat: Chat, close: (() -> Unit)? = null) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.clear_chat_question), text = generalGetString(MR.strings.clear_chat_warning), confirmText = generalGetString(MR.strings.clear_verb), - onConfirm = { - withApi { - val chatRh = chat.remoteHostId - val updatedChatInfo = chatModel.controller.apiClearChat(chatRh, chatInfo.chatType, chatInfo.apiId) - if (updatedChatInfo != null) { - chatModel.clearChat(chatRh, updatedChatInfo) - ntfManager.cancelNotificationsForChat(chatInfo.id) - close?.invoke() - } - } - }, + onConfirm = { controller.clearChat(chat, close) }, + destructive = true, + ) +} + +fun clearNoteFolderDialog(chat: Chat, close: (() -> Unit)? = null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.clear_note_folder_question), + text = generalGetString(MR.strings.clear_note_folder_warning), + confirmText = generalGetString(MR.strings.clear_verb), + onConfirm = { controller.clearChat(chat, close) }, destructive = true, ) } @@ -290,7 +521,9 @@ fun ChatInfoLayout( currentUser: User, sendReceipts: State<SendReceipts>, setSendReceipts: (SendReceipts) -> Unit, - connStats: State<ConnectionStats?>, + chatItemTTL: MutableState<ChatItemTTL?>, + setChatItemTTL: (ChatItemTTL?) -> Unit, + connStats: MutableState<ConnectionStats?>, contactNetworkStatus: NetworkStatus, customUserProfile: Profile?, localAlias: String, @@ -305,6 +538,9 @@ fun ChatInfoLayout( syncContactConnection: () -> Unit, syncContactConnectionForce: () -> Unit, verifyClicked: () -> Unit, + close: () -> Unit, + onSearchClicked: () -> Unit, + deletingItems: State<Boolean> ) { val cStats = connStats.value val scrollState = rememberScrollState() @@ -312,11 +548,7 @@ fun ChatInfoLayout( KeyChangeEffect(chat.id) { scope.launch { scrollState.scrollTo(0) } } - Column( - Modifier - .fillMaxWidth() - .verticalScroll(scrollState) - ) { + ColumnWithScrollBar { Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center @@ -325,7 +557,29 @@ fun ChatInfoLayout( } LocalAliasEditor(chat.id, localAlias, updateValue = onLocalAliasChanged) + SectionSpacer() + + Box( + Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Row( + Modifier + .widthIn(max = 460.dp) + .padding(horizontal = DEFAULT_PADDING), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + SearchButton(modifier = Modifier.fillMaxWidth(0.25f), chat, contact, close, onSearchClicked) + AudioCallButton(modifier = Modifier.fillMaxWidth(0.33f), chat, contact, connStats) + VideoButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact, connStats) + MuteButton(modifier = Modifier.fillMaxWidth(1f), chat, contact) + } + } + + SectionSpacer() + if (customUserProfile != null) { SectionView(generalGetString(MR.strings.incognito).uppercase()) { SectionItemViewSpaceBetween { @@ -336,8 +590,8 @@ fun ChatInfoLayout( SectionDividerSpaced() } - if (contact.ready && contact.active) { - SectionView { + SectionView { + if (contact.ready && contact.active) { if (connectionCode != null) { VerifyCodeButton(contact.verified, verifyClicked) } @@ -346,21 +600,42 @@ fun ChatInfoLayout( if (cStats != null && cStats.ratchetSyncAllowed) { SynchronizeConnectionButton(syncContactConnection) } - // } else if (developerTools) { - // SynchronizeConnectionButtonForce(syncContactConnectionForce) - // } + // } else if (developerTools) { + // SynchronizeConnectionButtonForce(syncContactConnectionForce) + // } + } + + WallpaperButton { + ModalManager.end.showModal { + val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } } + val c = chat.value + if (c != null) { + ChatWallpaperEditorModal(c) + } + } + } + } + SectionDividerSpaced(maxBottomPadding = false) + + ChatTTLSection(chatItemTTL, setChatItemTTL, deletingItems) + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + + val conn = contact.activeConn + if (conn != null) { + SectionView { + InfoRow("E2E encryption", if (conn.connPQEnabled) "Quantum resistant" else "Standard") + SectionDividerSpaced() } - SectionDividerSpaced() } if (contact.contactLink != null) { SectionView(stringResource(MR.strings.address_section_title).uppercase()) { - SimpleXLinkQRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) + SimpleXLinkQRCode(contact.contactLink) val clipboard = LocalClipboardManager.current ShareAddressButton { clipboard.shareText(simplexChatLink(contact.contactLink)) } SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(contact.displayName)) } - SectionDividerSpaced() + SectionDividerSpaced(maxTopPadding = true) } if (contact.ready && contact.active) { @@ -394,7 +669,7 @@ fun ChatInfoLayout( } } } - SectionDividerSpaced() + SectionDividerSpaced(maxBottomPadding = false) } SectionView { @@ -407,6 +682,19 @@ fun ChatInfoLayout( SectionView(title = stringResource(MR.strings.section_title_for_console)) { InfoRow(stringResource(MR.strings.info_row_local_name), chat.chatInfo.localDisplayName) InfoRow(stringResource(MR.strings.info_row_database_id), chat.chatInfo.apiId.toString()) + SectionItemView({ + withBGApi { + val info = controller.apiContactQueueInfo(chat.remoteHostId, chat.chatInfo.apiId) + if (info != null) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.message_queue_info), + text = queueInfoText(info) + ) + } + } + }) { + Text(stringResource(MR.strings.info_row_debug_delivery)) + } } } SectionBottomSpacer() @@ -433,13 +721,19 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) { Icon(painterResource(MR.images.ic_verified_user), null, tint = MaterialTheme.colors.secondary) } ) + val clipboard = LocalClipboardManager.current + val copyNameToClipboard = { + clipboard.setText(AnnotatedString(contact.profile.displayName)) + showToast(generalGetString(MR.strings.copied)) + } Text( text, inlineContent = inlineContent, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), textAlign = TextAlign.Center, maxLines = 3, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard) ) if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != contact.profile.displayName) { Text( @@ -447,7 +741,8 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) { color = MaterialTheme.colors.onBackground, textAlign = TextAlign.Center, maxLines = 4, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard) ) } } @@ -460,6 +755,7 @@ fun LocalAliasEditor( center: Boolean = true, leadingIcon: Boolean = false, focus: Boolean = false, + isContact: Boolean = true, updateValue: (String) -> Unit ) { val state = remember(chatId) { @@ -476,7 +772,7 @@ fun LocalAliasEditor( state, { Text( - generalGetString(MR.strings.text_field_set_contact_placeholder), + generalGetString(if (isContact) MR.strings.text_field_set_contact_placeholder else MR.strings.text_field_set_chat_placeholder), textAlign = if (center) TextAlign.Center else TextAlign.Start, color = MaterialTheme.colors.secondary ) @@ -510,6 +806,236 @@ fun LocalAliasEditor( } } +@Composable +fun SearchButton( + modifier: Modifier, + chat: Chat, + contact: Contact, + close: () -> Unit, + onSearchClicked: () -> Unit +) { + val disabled = !contact.ready || chat.chatItems.isEmpty() + InfoViewActionButton( + modifier = modifier, + icon = painterResource(MR.images.ic_search), + title = generalGetString(MR.strings.info_view_search_button), + disabled = disabled, + disabledLook = disabled, + onClick = { + if (appPlatform.isAndroid) { + close.invoke() + } + onSearchClicked() + } + ) +} + +@Composable +fun MuteButton( + modifier: Modifier, + chat: Chat, + contact: Contact +) { + val enableNtfs = remember { mutableStateOf(contact.chatSettings.enableNtfs ) } + val nextNtfMode by remember { derivedStateOf { enableNtfs.value.nextMode(false) } } + val disabled = !contact.ready || !contact.active + + InfoViewActionButton( + modifier = modifier, + icon = painterResource(nextNtfMode.icon), + title = stringResource(nextNtfMode.text(false)), + disabled = disabled, + disabledLook = disabled, + onClick = { + toggleNotifications(chat.remoteHostId, chat.chatInfo, nextNtfMode, chatModel, enableNtfs) + } + ) +} + +@Composable +fun AudioCallButton( + modifier: Modifier, + chat: Chat, + contact: Contact, + connectionStats: MutableState<ConnectionStats?> +) { + CallButton( + modifier = modifier, + chat, + contact, + connectionStats, + icon = painterResource(MR.images.ic_call), + title = generalGetString(MR.strings.info_view_call_button), + mediaType = CallMediaType.Audio + ) +} + +@Composable +fun VideoButton( + modifier: Modifier, + chat: Chat, + contact: Contact, + connectionStats: MutableState<ConnectionStats?> +) { + CallButton( + modifier = modifier, + chat, + contact, + connectionStats, + icon = painterResource(MR.images.ic_videocam), + title = generalGetString(MR.strings.info_view_video_button), + mediaType = CallMediaType.Video + ) +} + +@Composable +fun CallButton( + modifier: Modifier, + chat: Chat, + contact: Contact, + connectionStats: MutableState<ConnectionStats?>, + icon: Painter, + title: String, + mediaType: CallMediaType +) { + val canCall = contact.ready && contact.active && contact.mergedPreferences.calls.enabled.forUser && chatModel.activeCall.value == null + val needToAllowCallsToContact = remember(chat.chatInfo) { + chat.chatInfo is ChatInfo.Direct && with(chat.chatInfo.contact.mergedPreferences.calls) { + ((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) && + contactPreference.allow == FeatureAllowed.YES + } + } + val allowedCallsByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Calls) } + + InfoViewActionButton( + modifier = modifier, + icon = icon, + title = title, + disabled = chatModel.activeCall.value != null, + disabledLook = !canCall, + onClick = + when { + canCall -> { { + val connStats = connectionStats.value + if (connStats != null) { + if (connStats.ratchetSyncState == RatchetSyncState.Ok) { + startChatCall(chat.remoteHostId, chat.chatInfo, mediaType) + } else if (connStats.ratchetSyncAllowed) { + showFixConnectionAlert(syncConnection = { + withBGApi { syncContactConnection(chat.remoteHostId, contact, connectionStats, force = false) } + }) + } else { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.cant_call_contact_alert_title), + generalGetString(MR.strings.encryption_renegotiation_in_progress) + ) + } + } + } } + contact.nextSendGrpInv -> { { showCantCallContactSendMessageAlert() } } + !contact.active -> { { showCantCallContactDeletedAlert() } } + !contact.ready -> { { showCantCallContactConnectingAlert() } } + needToAllowCallsToContact -> { { showNeedToAllowCallsAlert(onConfirm = { allowCallsToContact(chat) }) } } + !allowedCallsByPrefs -> { { showCallsProhibitedAlert() }} + else -> { { AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.cant_call_contact_alert_title)) } } + } + ) +} + +private fun showCantCallContactSendMessageAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.cant_call_contact_alert_title), + text = generalGetString(MR.strings.cant_call_member_send_message_alert_text) + ) +} + +private fun showCantCallContactConnectingAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.cant_call_contact_alert_title), + text = generalGetString(MR.strings.cant_call_contact_connecting_wait_alert_text) + ) +} + +private fun showCantCallContactDeletedAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.cant_call_contact_alert_title), + text = generalGetString(MR.strings.cant_call_contact_deleted_alert_text) + ) +} + +private fun showNeedToAllowCallsAlert(onConfirm: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.allow_calls_question), + text = generalGetString(MR.strings.you_need_to_allow_calls), + confirmText = generalGetString(MR.strings.allow_verb), + dismissText = generalGetString(MR.strings.cancel_verb), + onConfirm = onConfirm, + ) +} + +private fun allowCallsToContact(chat: Chat) { + val contact = (chat.chatInfo as ChatInfo.Direct?)?.contact ?: return + withBGApi { + chatModel.controller.allowFeatureToContact(chat.remoteHostId, contact, ChatFeature.Calls) + } +} + +private fun showCallsProhibitedAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.calls_prohibited_alert_title), + text = generalGetString(MR.strings.calls_prohibited_ask_to_enable_calls_alert_text) + ) +} + +@Composable +fun InfoViewActionButton( + modifier: Modifier, + icon: Painter, + title: String, + disabled: Boolean, + disabledLook: Boolean, + onClick: () -> Unit +) { + Box(modifier) { + Column( + Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + IconButton( + onClick = onClick, + enabled = !disabled + ) { + Box( + Modifier + .size(56.dp) + .background( + if (disabledLook) MaterialTheme.colors.secondaryVariant else MaterialTheme.colors.primary, + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + icon, + contentDescription = null, + Modifier.size(22.dp * fontSizeSqrtMultiplier), + tint = if (disabledLook) MaterialTheme.colors.secondary else MaterialTheme.colors.onPrimary + ) + } + } + Text( + title.capitalize(Locale.current), + Modifier.padding(top = DEFAULT_SPACE_AFTER_ICON), + style = MaterialTheme.typography.subtitle2.copy(fontWeight = FontWeight.Normal, fontSize = 12.sp), + color = MaterialTheme.colors.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + @Composable private fun NetworkStatusRow(networkStatus: NetworkStatus) { Row( @@ -643,6 +1169,15 @@ private fun SendReceiptsOption(currentUser: User, state: State<SendReceipts>, on ) } +@Composable +fun WallpaperButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_image), + stringResource(MR.strings.settings_section_title_chat_theme), + click = onClick + ) +} + @Composable fun ClearChatButton(onClick: () -> Unit) { SettingsActionItem( @@ -676,10 +1211,80 @@ fun ShareAddressButton(onClick: () -> Unit) { ) } -private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withApi { +@Composable +fun ModalData.ChatWallpaperEditorModal(chat: Chat) { + val themes = remember(CurrentColors.collectAsState().value.base) { + (chat.chatInfo as? ChatInfo.Direct)?.contact?.uiThemes + ?: (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.uiThemes + ?: ThemeModeOverrides() + } + val globalThemeUsed = remember { stateGetOrPut("globalThemeUsed") { false } } + val initialTheme = remember(CurrentColors.collectAsState().value.base) { + val preferred = themes.preferredMode(!CurrentColors.value.colors.isLight) + globalThemeUsed.value = preferred == null + preferred ?: ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + } + ChatWallpaperEditor( + initialTheme, + applyToMode = if (themes.light == themes.dark) null else initialTheme.mode, + globalThemeUsed = globalThemeUsed, + save = { applyToMode, newTheme -> + save(applyToMode, newTheme, chatModel.getChat(chat.id) ?: chat) + }) +} + +suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, chat: Chat) { + val unchangedThemes: ThemeModeOverrides = ((chat.chatInfo as? ChatInfo.Direct)?.contact?.uiThemes ?: (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.uiThemes) ?: ThemeModeOverrides() + val wallpaperFiles = setOf(unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile) + var changedThemes: ThemeModeOverrides? = unchangedThemes + val changed = newTheme?.copy(wallpaper = newTheme.wallpaper?.withFilledWallpaperPath()) + changedThemes = when (applyToMode) { + null -> changedThemes?.copy(light = changed?.copy(mode = DefaultThemeMode.LIGHT), dark = changed?.copy(mode = DefaultThemeMode.DARK)) + DefaultThemeMode.LIGHT -> changedThemes?.copy(light = changed?.copy(mode = applyToMode)) + DefaultThemeMode.DARK -> changedThemes?.copy(dark = changed?.copy(mode = applyToMode)) + } + changedThemes = if (changedThemes?.light != null || changedThemes?.dark != null) { + val light = changedThemes.light + val dark = changedThemes.dark + val currentMode = CurrentColors.value.base.mode + // same image file for both modes, copy image to make them as different files + if (light?.wallpaper?.imageFile != null && dark?.wallpaper?.imageFile != null && light.wallpaper.imageFile == dark.wallpaper.imageFile) { + val imageFile = if (currentMode == DefaultThemeMode.LIGHT) { + dark.wallpaper.imageFile + } else { + light.wallpaper.imageFile + } + val filePath = saveWallpaperFile(File(getWallpaperFilePath(imageFile)).toURI()) + changedThemes = if (currentMode == DefaultThemeMode.LIGHT) { + changedThemes.copy(dark = dark.copy(wallpaper = dark.wallpaper.copy(imageFile = filePath))) + } else { + changedThemes.copy(light = light.copy(wallpaper = light.wallpaper.copy(imageFile = filePath))) + } + } + changedThemes + } else { + null + } + val wallpaperFilesToDelete = wallpaperFiles - changedThemes?.light?.wallpaper?.imageFile - changedThemes?.dark?.wallpaper?.imageFile + wallpaperFilesToDelete.forEach(::removeWallpaperFile) + + if (controller.apiSetChatUIThemes(chat.remoteHostId, chat.id, changedThemes)) { + withContext(Dispatchers.Main) { + if (chat.chatInfo is ChatInfo.Direct) { + chatModel.chatsContext.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes))) + } else if (chat.chatInfo is ChatInfo.Group) { + chatModel.chatsContext.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes))) + } + } + } +} + +private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi { val chatRh = chat.remoteHostId chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let { - chatModel.updateContact(chatRh, it) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(chatRh, it) + } } } @@ -712,6 +1317,98 @@ fun showSyncConnectionForceAlert(syncConnectionForce: () -> Unit) { ) } +fun showFixConnectionAlert(syncConnection: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.sync_connection_question), + text = generalGetString(MR.strings.sync_connection_desc), + confirmText = generalGetString(MR.strings.sync_connection_confirm), + onConfirm = syncConnection, + ) +} + +fun queueInfoText(info: Pair<RcvMsgInfo?, ServerQueueInfo>): String { + val (rcvMsgInfo, qInfo) = info + val msgInfo: String = if (rcvMsgInfo != null) json.encodeToString(rcvMsgInfo) else generalGetString(MR.strings.message_queue_info_none) + return generalGetString(MR.strings.message_queue_info_server_info).format(json.encodeToString(qInfo), msgInfo) +} + +fun setChatTTLAlert( + chatsCtx: ChatModel.ChatsContext, + rhId: Long?, + chatInfo: ChatInfo, + selectedChatTTL: MutableState<ChatItemTTL?>, + previousChatTTL: ChatItemTTL?, + progressIndicator: MutableState<Boolean> +) { + val defaultTTL = chatModel.chatItemTTL.value + val previouslyUsedTTL = previousChatTTL ?: defaultTTL + val newTTLToUse = selectedChatTTL.value ?: defaultTTL + + AlertManager.shared.showAlertDialog( + title = generalGetString( + if (newTTLToUse.neverExpires) { + MR.strings.disable_automatic_deletion_question + } else if (!previouslyUsedTTL.neverExpires || selectedChatTTL.value == null) { + MR.strings.change_automatic_deletion_question + } else MR.strings.enable_automatic_deletion_question), + text = generalGetString(if (newTTLToUse.neverExpires) MR.strings.disable_automatic_deletion_message else MR.strings.change_automatic_chat_deletion_message), + confirmText = generalGetString(if (newTTLToUse.neverExpires) MR.strings.disable_automatic_deletion else MR.strings.delete_messages), + onConfirm = { setChatTTL(chatsCtx, rhId, chatInfo, selectedChatTTL, progressIndicator, previousChatTTL) }, + onDismiss = { selectedChatTTL.value = previousChatTTL }, + onDismissRequest = { selectedChatTTL.value = previousChatTTL }, + destructive = true, + ) +} + +private fun setChatTTL( + chatsCtx: ChatModel.ChatsContext, + rhId: Long?, + chatInfo: ChatInfo, + chatTTL: MutableState<ChatItemTTL?>, + progressIndicator: MutableState<Boolean>, + previousChatTTL: ChatItemTTL? + ) { + progressIndicator.value = true + withBGApi { + try { + chatModel.controller.setChatTTL(rhId, chatInfo.chatType, chatInfo.apiId, chatTTL.value) + afterSetChatTTL(chatsCtx, rhId, chatInfo, progressIndicator) + } catch (e: Exception) { + chatTTL.value = previousChatTTL + afterSetChatTTL(chatsCtx, rhId, chatInfo, progressIndicator) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_changing_message_deletion), e.stackTraceToString()) + } + } +} + +private suspend fun afterSetChatTTL(chatsCtx: ChatModel.ChatsContext, rhId: Long?, chatInfo: ChatInfo, progressIndicator: MutableState<Boolean>) { + try { + val pagination = ChatPagination.Initial(ChatPagination.INITIAL_COUNT) + val (chat, navInfo) = controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId, null, pagination) ?: return + if (chat.chatItems.isEmpty()) { + // replacing old chat with the same old chat but without items. Less intrusive way of clearing a preview + withContext(Dispatchers.Main) { + val oldChat = chatModel.chatsContext.getChat(chat.id) + if (oldChat != null) { + chatModel.chatsContext.replaceChat(oldChat.remoteHostId, oldChat.id, oldChat.copy(chatItems = emptyList())) + } + } + } + if (chat.remoteHostId != chatModel.remoteHostId() || chat.id != chatModel.chatId.value) return + processLoadedChat( + chatsCtx, + chat, + navInfo, + pagination = pagination, + openAroundItemId = null + ) + } catch (e: Exception) { + Log.e(TAG, "apiGetChat error: ${e.stackTraceToString()}") + } finally { + progressIndicator.value = false + } +} + @Preview @Composable fun PreviewChatInfoLayout() { @@ -726,6 +1423,8 @@ fun PreviewChatInfoLayout() { User.sampleData, sendReceipts = remember { mutableStateOf(SendReceipts.Yes) }, setSendReceipts = {}, + chatItemTTL = remember { mutableStateOf(ChatItemTTL.fromSeconds(0)) }, + setChatItemTTL = {}, localAlias = "", connectionCode = "123", developerTools = false, @@ -741,6 +1440,9 @@ fun PreviewChatInfoLayout() { syncContactConnection = {}, syncContactConnectionForce = {}, verifyClicked = {}, + close = {}, + onSearchClicked = {}, + deletingItems = remember { mutableStateOf(false) } ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt index 63cd25092e..9c36f4896b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt @@ -13,9 +13,8 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.* import androidx.compose.ui.text.AnnotatedString import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -24,35 +23,41 @@ 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.platform.onRightClick -import chat.simplex.common.views.chat.item.ItemAction -import chat.simplex.common.views.chat.item.MarkdownText +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.platform.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.platform.shareText import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.group.MemberProfileImage +import chat.simplex.common.views.chat.item.* +import chat.simplex.common.views.chatlist.* import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource +import kotlinx.serialization.encodeToString sealed class CIInfoTab { class Delivery(val memberDeliveryStatuses: List<MemberDeliveryStatus>): CIInfoTab() object History: CIInfoTab() class Quote(val quotedItem: CIQuote): CIInfoTab() + class Forwarded(val forwardedFromChatItem: AChatItem): CIInfoTab() } @Composable -fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) { +fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean, chatInfo: ChatInfo?) { val sent = ci.chatDir.sent - val appColors = CurrentColors.collectAsState().value.appColors + val appColors = MaterialTheme.appColors val uriHandler = LocalUriHandler.current val selection = remember { mutableStateOf<CIInfoTab>(CIInfoTab.History) } @Composable - fun TextBubble(text: String, formattedText: List<FormattedText>?, sender: String?, showMenu: MutableState<Boolean>) { + fun TextBubble(text: String, formattedText: List<FormattedText>?, sender: String?, showMenu: MutableState<Boolean>, mentions: Map<String, CIMention>? = null, userMemberId: String? = null, ) { if (text != "") { MarkdownText( text, if (text.isEmpty()) emptyList() else formattedText, sender = sender, + mentions = mentions, + userMemberId = userMemberId, senderBold = true, + toggleSecrets = true, linkMode = SimplexLinkMode.DESCRIPTION, uriHandler = uriHandler, onLinkLongClick = { showMenu.value = true } ) @@ -72,12 +77,17 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d Column { Box( - Modifier.clip(RoundedCornerShape(18.dp)).background(itemColor).padding(bottom = 3.dp) + Modifier.clipChatItem().background(itemColor).padding(bottom = 3.dp) .combinedClickable(onLongClick = { showMenu.value = true }, onClick = {}) .onRightClick { showMenu.value = true } ) { Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) { - TextBubble(text, ciVersion.formattedText, sender = null, showMenu) + TextBubble(text, ciVersion.formattedText, sender = null, showMenu = showMenu, mentions = ci.mentions, + userMemberId = when { + chatInfo is ChatInfo.Group -> chatInfo.groupInfo.membership.memberId + else -> null + } + ) } } Row(Modifier.padding(start = 12.dp, top = 3.dp, bottom = 16.dp)) { @@ -119,7 +129,7 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d Column { Box( - Modifier.clip(RoundedCornerShape(18.dp)).background(quoteColor).padding(bottom = 3.dp) + Modifier.clipChatItem().background(quoteColor).padding(bottom = 3.dp) .combinedClickable(onLongClick = { showMenu.value = true }, onClick = {}) .onRightClick { showMenu.value = true } ) { @@ -151,11 +161,96 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d } } + val local = when (ci.chatDir) { + is CIDirection.LocalSnd -> true + is CIDirection.LocalRcv -> true + else -> false + } + + @Composable + fun ForwardedFromSender(forwardedFromItem: AChatItem) { + @Composable + fun ItemText(text: String, fontStyle: FontStyle = FontStyle.Normal, color: Color = MaterialTheme.colors.onBackground) { + Text( + text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.body1, + fontStyle = fontStyle, + color = color, + ) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + ChatInfoImage(forwardedFromItem.chatInfo, size = 57.dp) + Column( + modifier = Modifier + .padding(start = 15.dp) + .weight(1F) + ) { + if (forwardedFromItem.chatItem.chatDir.sent) { + ItemText(text = stringResource(MR.strings.sender_you_pronoun), fontStyle = FontStyle.Italic) + Spacer(Modifier.height(7.dp)) + ItemText(forwardedFromItem.chatInfo.chatViewName, color = MaterialTheme.colors.secondary) + } else if (forwardedFromItem.chatItem.chatDir is CIDirection.GroupRcv) { + ItemText(text = forwardedFromItem.chatItem.chatDir.groupMember.chatViewName) + Spacer(Modifier.height(7.dp)) + ItemText(forwardedFromItem.chatInfo.chatViewName, color = MaterialTheme.colors.secondary) + } else { + ItemText(forwardedFromItem.chatInfo.chatViewName, color = MaterialTheme.colors.onBackground) + } + } + } + } + + @Composable + fun ForwardedFromView(forwardedFromItem: AChatItem) { + Column { + SectionItemView( + click = { + withBGApi { + openChat(secondaryChatsCtx = null, chatRh, forwardedFromItem.chatInfo) + ModalManager.end.closeModals() + } + }, + padding = PaddingValues(start = 17.dp, end = DEFAULT_PADDING) + ) { + ForwardedFromSender(forwardedFromItem) + } + + if (!local) { + Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 41.dp, end = DEFAULT_PADDING_HALF, bottom = DEFAULT_PADDING_HALF)) + Text(stringResource(MR.strings.recipients_can_not_see_who_message_from), Modifier.padding(horizontal = DEFAULT_PADDING), fontSize = 12.sp, color = MaterialTheme.colors.secondary) + } + } + } + + @Composable + fun ExpandableInfoRow(title: String, value: String) { + val expanded = remember { mutableStateOf(false) } + Row( + Modifier + .fillMaxWidth() + .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT) + .padding(PaddingValues(horizontal = DEFAULT_PADDING)) + .clickable { expanded.value = !expanded.value }, + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(title, color = MaterialTheme.colors.onBackground) + if (expanded.value) { + Text(value, color = MaterialTheme.colors.secondary) + } else { + Text(value, color = MaterialTheme.colors.secondary, maxLines = 1) + } + } + } + @Composable fun Details() { - AppBarTitle(stringResource(if (sent) MR.strings.sent_message else MR.strings.received_message)) + AppBarTitle(stringResource(if (ci.localNote) MR.strings.saved_message_title else if (sent) MR.strings.sent_message else MR.strings.received_message)) SectionView { - InfoRow(stringResource(MR.strings.info_row_sent_at), localTimestamp(ci.meta.itemTs)) + InfoRow(stringResource(if (!ci.localNote) MR.strings.info_row_sent_at else MR.strings.info_row_created_at), localTimestamp(ci.meta.itemTs)) if (!sent) { InfoRow(stringResource(MR.strings.info_row_received_at), localTimestamp(ci.meta.createdAt)) } @@ -179,53 +274,75 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d if (devTools) { InfoRow(stringResource(MR.strings.info_row_database_id), ci.meta.itemId.toString()) InfoRow(stringResource(MR.strings.info_row_updated_at), localTimestamp(ci.meta.updatedAt)) + ExpandableInfoRow(stringResource(MR.strings.info_row_message_status), jsonShort.encodeToString(ci.meta.itemStatus)) + if (ci.file != null) { + ExpandableInfoRow(stringResource(MR.strings.info_row_file_status), jsonShort.encodeToString(ci.file.fileStatus)) + } } } } @Composable fun HistoryTab() { - Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) { + ColumnWithScrollBar { Details() - SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) + SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) val versions = ciInfo.itemVersions if (versions.isNotEmpty()) { - SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { Text(stringResource(MR.strings.edit_history), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING)) versions.forEachIndexed { i, ciVersion -> ItemVersionView(ciVersion, current = i == 0) } } } else { - SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(stringResource(MR.strings.no_history), color = MaterialTheme.colors.secondary) } } } SectionBottomSpacer() - } - } - - @Composable - fun QuoteTab(qi: CIQuote) { - Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) { - Details() - SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) - SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { - Text(stringResource(MR.strings.in_reply_to), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING)) - QuotedMsgView(qi) - } SectionBottomSpacer() } } @Composable - fun MemberDeliveryStatusView(member: GroupMember, status: CIStatus) { + fun QuoteTab(qi: CIQuote) { + ColumnWithScrollBar { + Details() + SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) + SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Text(stringResource(MR.strings.in_reply_to), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING)) + QuotedMsgView(qi) + } + SectionBottomSpacer() + SectionBottomSpacer() + } + } + + @Composable + fun ForwardedFromTab(forwardedFromItem: AChatItem) { + ColumnWithScrollBar { + Details() + SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) + SectionView { + Text(stringResource(if (local) MR.strings.saved_from_chat_item_info_title else MR.strings.forwarded_from_chat_item_info_title), + style = MaterialTheme.typography.h2, + modifier = Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING)) + ForwardedFromView(forwardedFromItem) + } + SectionBottomSpacer() + SectionBottomSpacer() + } + } + + @Composable + fun MemberDeliveryStatusView(member: GroupMember, status: GroupSndStatus, sentViaProxy: Boolean?) { SectionItemView( padding = PaddingValues(horizontal = 0.dp) ) { - ProfileImage(size = 36.dp, member.image) + MemberProfileImage(size = 36.dp, member) Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON)) Text( member.chatViewName, @@ -234,7 +351,19 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d overflow = TextOverflow.Ellipsis ) Spacer(Modifier.fillMaxWidth().weight(1f)) - val statusIcon = status.statusIcon(MaterialTheme.colors.primary, CurrentColors.value.colors.secondary) + if (sentViaProxy == true) { + Box( + Modifier.size(36.dp), + contentAlignment = Alignment.Center + ) { + Icon( + painterResource(MR.images.ic_arrow_forward), + contentDescription = null, + tint = CurrentColors.value.colors.secondary + ) + } + } + val (icon, statusColor) = status.statusIcon(MaterialTheme.colors.primary, CurrentColors.value.colors.secondary) var modifier = Modifier.size(36.dp).clip(RoundedCornerShape(20.dp)) val info = status.statusInto if (info != null) { @@ -246,45 +375,37 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d } } Box(modifier, contentAlignment = Alignment.Center) { - if (statusIcon != null) { - val (icon, statusColor) = statusIcon - Icon( - painterResource(icon), - contentDescription = null, - tint = statusColor - ) - } else { - Icon( - painterResource(MR.images.ic_more_horiz), - contentDescription = null, - tint = CurrentColors.value.colors.secondary - ) - } + Icon( + painterResource(icon), + contentDescription = null, + tint = statusColor + ) } } } @Composable fun DeliveryTab(memberDeliveryStatuses: List<MemberDeliveryStatus>) { - Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) { + ColumnWithScrollBar { Details() - SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) + SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) val mss = membersStatuses(chatModel, memberDeliveryStatuses) if (mss.isNotEmpty()) { - SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { Text(stringResource(MR.strings.delivery), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING)) - mss.forEach { (member, status) -> - MemberDeliveryStatusView(member, status) + mss.forEach { (member, status, sentViaProxy) -> + MemberDeliveryStatusView(member, status, sentViaProxy) } } } else { - SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(stringResource(MR.strings.no_info_on_delivery), color = MaterialTheme.colors.secondary) } } } SectionBottomSpacer() + SectionBottomSpacer() } } @@ -294,6 +415,7 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d is CIInfoTab.Delivery -> stringResource(MR.strings.delivery) is CIInfoTab.History -> stringResource(MR.strings.edit_history) is CIInfoTab.Quote -> stringResource(MR.strings.in_reply_to) + is CIInfoTab.Forwarded -> stringResource(if (local) MR.strings.saved_chat_item_info_tab else MR.strings.forwarded_chat_item_info_tab) } } @@ -302,6 +424,7 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d is CIInfoTab.Delivery -> MR.images.ic_double_check is CIInfoTab.History -> MR.images.ic_history is CIInfoTab.Quote -> MR.images.ic_reply + is CIInfoTab.Forwarded -> MR.images.ic_forward } } @@ -313,22 +436,19 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d if (ci.quotedItem != null) { numTabs += 1 } + if (ciInfo.forwardedFromChatItem != null) { + numTabs += 1 + } return numTabs } Column { if (numTabs() > 1) { - Column( + Box( Modifier - .fillMaxHeight(), - verticalArrangement = Arrangement.SpaceBetween + .fillMaxHeight() ) { - LaunchedEffect(Unit) { - if (ciInfo.memberDeliveryStatuses != null) { - selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses) - } - } - Column(Modifier.weight(1f)) { + Column { when (val sel = selection.value) { is CIInfoTab.Delivery -> { DeliveryTab(sel.memberDeliveryStatuses) @@ -341,6 +461,10 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d is CIInfoTab.Quote -> { QuoteTab(sel.quotedItem) } + + is CIInfoTab.Forwarded -> { + ForwardedFromTab(sel.forwardedFromChatItem) + } } } val availableTabs = mutableListOf<CIInfoTab>() @@ -351,29 +475,47 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d if (ci.quotedItem != null) { availableTabs.add(CIInfoTab.Quote(ci.quotedItem)) } - TabRow( - selectedTabIndex = availableTabs.indexOfFirst { it::class == selection.value::class }, - backgroundColor = Color.Transparent, - contentColor = MaterialTheme.colors.primary, - ) { - availableTabs.forEach { ciInfoTab -> - Tab( - selected = selection.value::class == ciInfoTab::class, - onClick = { - selection.value = ciInfoTab - }, - text = { Text(tabTitle(ciInfoTab), fontSize = 13.sp) }, - icon = { - Icon( - painterResource(tabIcon(ciInfoTab)), - tabTitle(ciInfoTab) - ) - }, - selectedContentColor = MaterialTheme.colors.primary, - unselectedContentColor = MaterialTheme.colors.secondary, - ) + if (ciInfo.forwardedFromChatItem != null) { + availableTabs.add(CIInfoTab.Forwarded(ciInfo.forwardedFromChatItem)) + } + if (availableTabs.none { it.javaClass == selection.value.javaClass }) { + selection.value = availableTabs.first() + } + LaunchedEffect(ciInfo) { + if (ciInfo.forwardedFromChatItem != null && selection.value is CIInfoTab.Forwarded) { + selection.value = CIInfoTab.Forwarded(ciInfo.forwardedFromChatItem) + } else if (ciInfo.memberDeliveryStatuses != null) { + selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses) } } + val oneHandUI = remember { appPrefs.oneHandUI.state } + Box(Modifier.align(Alignment.BottomCenter).navigationBarsPadding().offset(x = 0.dp, y = if (oneHandUI.value) -AppBarHeight * fontSizeSqrtMultiplier else 0.dp)) { + TabRow( + selectedTabIndex = availableTabs.indexOfFirst { it::class == selection.value::class }, + Modifier.height(AppBarHeight * fontSizeSqrtMultiplier), + backgroundColor = MaterialTheme.colors.background, + contentColor = MaterialTheme.colors.primary, + ) { + availableTabs.forEach { ciInfoTab -> + LeadingIconTab( + selected = selection.value::class == ciInfoTab::class, + onClick = { + selection.value = ciInfoTab + }, + text = { Text(tabTitle(ciInfoTab), fontSize = 13.sp) }, + icon = { + Icon( + painterResource(tabIcon(ciInfoTab)), + tabTitle(ciInfoTab) + ) + }, + selectedContentColor = MaterialTheme.colors.primary, + unselectedContentColor = MaterialTheme.colors.secondary, + ) + } + } + Divider() + } } } else { HistoryTab() @@ -381,10 +523,10 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d } } -private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List<MemberDeliveryStatus>): List<Pair<GroupMember, CIStatus>> { +private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List<MemberDeliveryStatus>): List<Triple<GroupMember, GroupSndStatus, Boolean?>> { return memberDeliveryStatuses.mapNotNull { mds -> chatModel.getGroupMember(mds.groupMemberId)?.let { mem -> - mem to mds.memberDeliveryStatus + Triple(mem, mds.memberDeliveryStatus, mds.sentViaProxy) } } } @@ -392,9 +534,9 @@ private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List<M fun itemInfoShareText(chatModel: ChatModel, ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolean): String { val meta = ci.meta val sent = ci.chatDir.sent - val shareText = mutableListOf<String>("# " + generalGetString(if (sent) MR.strings.sent_message else MR.strings.received_message), "") + val shareText = mutableListOf<String>("# " + generalGetString(if (ci.localNote) MR.strings.saved_message_title else if (sent) MR.strings.sent_message else MR.strings.received_message), "") - shareText.add(String.format(generalGetString(MR.strings.share_text_sent_at), localTimestamp(meta.itemTs))) + shareText.add(String.format(generalGetString(if (ci.localNote) MR.strings.share_text_created_at else MR.strings.share_text_sent_at), localTimestamp(meta.itemTs))) if (!ci.chatDir.sent) { shareText.add(String.format(generalGetString(MR.strings.share_text_received_at), localTimestamp(meta.createdAt))) } @@ -418,6 +560,10 @@ fun itemInfoShareText(chatModel: ChatModel, ci: ChatItem, chatItemInfo: ChatItem if (devTools) { shareText.add(String.format(generalGetString(MR.strings.share_text_database_id), meta.itemId)) shareText.add(String.format(generalGetString(MR.strings.share_text_updated_at), meta.updatedAt)) + shareText.add(String.format(generalGetString(MR.strings.share_text_message_status), jsonShort.encodeToString(ci.meta.itemStatus))) + if (ci.file != null) { + shareText.add(String.format(generalGetString(MR.strings.share_text_file_status), jsonShort.encodeToString(ci.file.fileStatus))) + } } val qi = ci.quotedItem if (qi != null) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt new file mode 100644 index 0000000000..eabe9cb60a --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt @@ -0,0 +1,520 @@ +package chat.simplex.common.views.chat + +import androidx.compose.runtime.snapshots.SnapshotStateList +import chat.simplex.common.model.* +import chat.simplex.common.platform.chatModel +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.StateFlow +import kotlinx.datetime.Instant +import kotlin.math.min + +const val TRIM_KEEP_COUNT = 200 + +suspend fun apiLoadSingleMessage( + chatsCtx: ChatModel.ChatsContext, + rhId: Long?, + chatType: ChatType, + apiId: Long, + itemId: Long +): ChatItem? = coroutineScope { + val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.contentTag, ChatPagination.Around(itemId, 0), "") ?: return@coroutineScope null + chat.chatItems.firstOrNull() +} + +suspend fun apiLoadMessages( + chatsCtx: ChatModel.ChatsContext, + rhId: Long?, + chatType: ChatType, + apiId: Long, + pagination: ChatPagination, + search: String = "", + openAroundItemId: Long? = null, + visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 } +) = coroutineScope { + val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.contentTag, pagination, search) ?: return@coroutineScope + // For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes + /** When [openAroundItemId] is provided, chatId can be different too */ + if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last && openAroundItemId == null) + || !isActive) return@coroutineScope + processLoadedChat(chatsCtx, chat, navInfo, pagination, openAroundItemId, visibleItemIndexesNonReversed) +} + +suspend fun processLoadedChat( + chatsCtx: ChatModel.ChatsContext, + chat: Chat, + navInfo: NavigationInfo, + pagination: ChatPagination, + openAroundItemId: Long?, + visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 } +) { + val chatState = chatsCtx.chatState + val (splits, unreadAfterItemId, totalAfter, unreadTotal, unreadAfter, unreadAfterNewestLoaded) = chatState + val oldItems = chatsCtx.chatItems.value + val newItems = SnapshotStateList<ChatItem>() + when (pagination) { + is ChatPagination.Initial -> { + val newSplits = if (chat.chatItems.isNotEmpty() && navInfo.afterTotal > 0) listOf(chat.chatItems.last().id) else emptyList() + if (chatsCtx.contentTag == null) { + // update main chats, not content tagged + withContext(Dispatchers.Main) { + val oldChat = chatModel.chatsContext.getChat(chat.id) + if (oldChat == null) { + chatModel.chatsContext.addChat(chat) + } else { + chatModel.chatsContext.updateChatInfo(chat.remoteHostId, chat.chatInfo) + // unreadChat is currently not actual in getChat query (always false) + chatModel.chatsContext.updateChatStats(chat.remoteHostId, chat.id, chat.chatStats.copy(unreadChat = oldChat.chatStats.unreadChat)) + } + } + } + withContext(Dispatchers.Main) { + chatsCtx.chatItemStatuses.clear() + chatsCtx.chatItems.replaceAll(chat.chatItems) + chatModel.chatId.value = chat.id + splits.value = newSplits + if (chat.chatItems.isNotEmpty()) { + unreadAfterItemId.value = chat.chatItems.last().id + } + totalAfter.value = navInfo.afterTotal + unreadTotal.value = chat.chatStats.unreadCount + unreadAfter.value = navInfo.afterUnread + unreadAfterNewestLoaded.value = navInfo.afterUnread + } + } + is ChatPagination.Before -> { + newItems.addAll(oldItems) + val indexInCurrentItems: Int = oldItems.indexOfFirst { it.id == pagination.chatItemId } + if (indexInCurrentItems == -1) return + val (newIds, _) = mapItemsToIds(chat.chatItems) + val wasSize = newItems.size + val (oldUnreadSplitIndex, newUnreadSplitIndex, trimmedIds, newSplits) = removeDuplicatesAndModifySplitsOnBeforePagination( + unreadAfterItemId, newItems, newIds, splits, visibleItemIndexesNonReversed + ) + val insertAt = (indexInCurrentItems - (wasSize - newItems.size) + trimmedIds.size).coerceAtLeast(0) + newItems.addAll(insertAt, chat.chatItems) + withContext(Dispatchers.Main) { + chatsCtx.chatItems.replaceAll(newItems) + splits.value = newSplits + chatState.moveUnreadAfterItem(oldUnreadSplitIndex, newUnreadSplitIndex, oldItems) + } + } + is ChatPagination.After -> { + newItems.addAll(oldItems) + val indexInCurrentItems: Int = oldItems.indexOfFirst { it.id == pagination.chatItemId } + if (indexInCurrentItems == -1) return + + val mappedItems = mapItemsToIds(chat.chatItems) + val newIds = mappedItems.first + val (newSplits, unreadInLoaded) = removeDuplicatesAndModifySplitsOnAfterPagination( + mappedItems.second, pagination.chatItemId, newItems, newIds, chat, splits + ) + val indexToAdd = min(indexInCurrentItems + 1, newItems.size) + val indexToAddIsLast = indexToAdd == newItems.size + newItems.addAll(indexToAdd, chat.chatItems) + withContext(Dispatchers.Main) { + chatsCtx.chatItems.replaceAll(newItems) + splits.value = newSplits + chatState.moveUnreadAfterItem(splits.value.firstOrNull() ?: newItems.last().id, newItems) + // loading clear bottom area, updating number of unread items after the newest loaded item + if (indexToAddIsLast) { + unreadAfterNewestLoaded.value -= unreadInLoaded + } + } + } + is ChatPagination.Around -> { + val newSplits: ArrayList<Long> = if (openAroundItemId == null) { + newItems.addAll(oldItems) + ArrayList(removeDuplicatesAndUpperSplits(newItems, chat, splits, visibleItemIndexesNonReversed)) + } else { + arrayListOf() + } + val (itemIndex, splitIndex) = indexToInsertAround(chat.chatInfo.chatType, chat.chatItems.lastOrNull(), to = newItems, newSplits.toSet()) + //indexToInsertAroundTest() + newItems.addAll(itemIndex, chat.chatItems) + newSplits.add(splitIndex, chat.chatItems.last().id) + + withContext(Dispatchers.Main) { + chatsCtx.chatItems.replaceAll(newItems) + splits.value = newSplits + unreadAfterItemId.value = chat.chatItems.last().id + totalAfter.value = navInfo.afterTotal + unreadTotal.value = chat.chatStats.unreadCount + unreadAfter.value = navInfo.afterUnread + + if (openAroundItemId != null) { + unreadAfterNewestLoaded.value = navInfo.afterUnread + chatModel.openAroundItemId.value = openAroundItemId + chatModel.chatId.value = chat.id + } else { + // no need to set it, count will be wrong + // unreadAfterNewestLoaded.value = navInfo.afterUnread + } + } + } + is ChatPagination.Last -> { + newItems.addAll(oldItems) + val newSplits = removeDuplicatesAndUnusedSplits(newItems, chat, chatState.splits.value) + removeDuplicates(newItems, chat) + newItems.addAll(chat.chatItems) + withContext(Dispatchers.Main) { + chatsCtx.chatItems.replaceAll(newItems) + chatState.splits.value = newSplits + unreadAfterNewestLoaded.value = 0 + } + } + } +} + +private data class ModifiedSplits ( + val oldUnreadSplitIndex: Int, + val newUnreadSplitIndex: Int, + val trimmedIds: Set<Long>, + val newSplits: List<Long>, +) + +private fun removeDuplicatesAndModifySplitsOnBeforePagination( + unreadAfterItemId: StateFlow<Long>, + newItems: SnapshotStateList<ChatItem>, + newIds: Set<Long>, + splits: StateFlow<List<Long>>, + visibleItemIndexesNonReversed: () -> IntRange +): ModifiedSplits { + var oldUnreadSplitIndex: Int = -1 + var newUnreadSplitIndex: Int = -1 + val visibleItemIndexes = visibleItemIndexesNonReversed() + var lastSplitIndexTrimmed = -1 + var allowedTrimming = true + var index = 0 + /** keep the newest [TRIM_KEEP_COUNT] items (bottom area) and oldest [TRIM_KEEP_COUNT] items, trim others */ + val trimRange = visibleItemIndexes.last + TRIM_KEEP_COUNT .. newItems.size - TRIM_KEEP_COUNT + val trimmedIds = mutableSetOf<Long>() + val prevItemTrimRange = visibleItemIndexes.last + TRIM_KEEP_COUNT + 1 .. newItems.size - TRIM_KEEP_COUNT + var newSplits = splits.value + + newItems.removeAll { + val invisibleItemToTrim = trimRange.contains(index) && allowedTrimming + val prevItemWasTrimmed = prevItemTrimRange.contains(index) && allowedTrimming + // may disable it after clearing the whole split range + if (splits.value.isNotEmpty() && it.id == splits.value.firstOrNull()) { + // trim only in one split range + allowedTrimming = false + } + val indexInSplits = splits.value.indexOf(it.id) + if (indexInSplits != -1) { + lastSplitIndexTrimmed = indexInSplits + } + if (invisibleItemToTrim) { + if (prevItemWasTrimmed) { + trimmedIds.add(it.id) + } else { + newUnreadSplitIndex = index + // prev item is not supposed to be trimmed, so exclude current one from trimming and set a split here instead. + // this allows to define splitRange of the oldest items and to start loading trimmed items when user scrolls in the opposite direction + if (lastSplitIndexTrimmed == -1) { + newSplits = listOf(it.id) + newSplits + } else { + val new = ArrayList(newSplits) + new[lastSplitIndexTrimmed] = it.id + newSplits = new + } + } + } + if (unreadAfterItemId.value == it.id) { + oldUnreadSplitIndex = index + } + index++ + (invisibleItemToTrim && prevItemWasTrimmed) || newIds.contains(it.id) + } + // will remove any splits that now becomes obsolete because items were merged + newSplits = newSplits.filterNot { split -> newIds.contains(split) || trimmedIds.contains(split) } + return ModifiedSplits(oldUnreadSplitIndex, newUnreadSplitIndex, trimmedIds, newSplits) +} + +private fun removeDuplicatesAndModifySplitsOnAfterPagination( + unreadInLoaded: Int, + paginationChatItemId: Long, + newItems: SnapshotStateList<ChatItem>, + newIds: Set<Long>, + chat: Chat, + splits: StateFlow<List<Long>> +): Pair<List<Long>, Int> { + var unreadInLoaded = unreadInLoaded + var firstItemIdBelowAllSplits: Long? = null + val splitsToRemove = ArrayList<Long>() + val indexInSplitRanges = splits.value.indexOf(paginationChatItemId) + // Currently, it should always load from split range + val loadingFromSplitRange = indexInSplitRanges != -1 + val topSplits: List<Long> + val splitsToMerge: ArrayList<Long> + if (loadingFromSplitRange && indexInSplitRanges + 1 <= splits.value.size) { + splitsToMerge = ArrayList(splits.value.subList(indexInSplitRanges + 1, splits.value.size)) + topSplits = splits.value.take(indexInSplitRanges + 1) + } else { + splitsToMerge = ArrayList() + topSplits = emptyList() + } + newItems.removeAll { + val duplicate = newIds.contains(it.id) + if (loadingFromSplitRange && duplicate) { + if (splitsToMerge.contains(it.id)) { + splitsToMerge.remove(it.id) + splitsToRemove.add(it.id) + } else if (firstItemIdBelowAllSplits == null && splitsToMerge.isEmpty()) { + // we passed all splits and found duplicated item below all of them, which means no splits anymore below the loaded items + firstItemIdBelowAllSplits = it.id + } + } + if (duplicate && it.isRcvNew) { + unreadInLoaded-- + } + duplicate + } + var newSplits: List<Long> = emptyList() + if (firstItemIdBelowAllSplits != null) { + // no splits below anymore, all were merged with bottom items + newSplits = topSplits + } else { + if (splitsToRemove.isNotEmpty()) { + val new = ArrayList(splits.value) + new.removeAll(splitsToRemove.toSet()) + newSplits = new + } + val enlargedSplit = splits.value.indexOf(paginationChatItemId) + if (enlargedSplit != -1) { + // move the split to the end of loaded items + val new = ArrayList(splits.value) + new[enlargedSplit] = chat.chatItems.last().id + newSplits = new + // Log.d(TAG, "Enlarged split range $newSplits") + } + } + return newSplits to unreadInLoaded +} + +private fun removeDuplicatesAndUpperSplits( + newItems: SnapshotStateList<ChatItem>, + chat: Chat, + splits: StateFlow<List<Long>>, + visibleItemIndexesNonReversed: () -> IntRange +): List<Long> { + if (splits.value.isEmpty()) { + removeDuplicates(newItems, chat) + return splits.value + } + + val newSplits = splits.value.toMutableList() + val visibleItemIndexes = visibleItemIndexesNonReversed() + val (newIds, _) = mapItemsToIds(chat.chatItems) + val idsToTrim = ArrayList<MutableSet<Long>>() + idsToTrim.add(mutableSetOf()) + var index = 0 + newItems.removeAll { + val duplicate = newIds.contains(it.id) + if (!duplicate && visibleItemIndexes.first > index) { + idsToTrim.last().add(it.id) + } + if (visibleItemIndexes.first > index && splits.value.contains(it.id)) { + newSplits -= it.id + // closing previous range. All items in idsToTrim that ends with empty set should be deleted. + // Otherwise, the last set should be excluded from trimming because it is in currently visible split range + idsToTrim.add(mutableSetOf()) + } + + index++ + duplicate + } + if (idsToTrim.last().isNotEmpty()) { + // it has some elements to trim from currently visible range which means the items shouldn't be trimmed + // Otherwise, the last set would be empty + idsToTrim.removeLast() + } + val allItemsToDelete = idsToTrim.flatten() + if (allItemsToDelete.isNotEmpty()) { + newItems.removeAll { allItemsToDelete.contains(it.id) } + } + return newSplits +} + +private fun removeDuplicatesAndUnusedSplits( + newItems: SnapshotStateList<ChatItem>, + chat: Chat, + splits: List<Long> +): List<Long> { + if (splits.isEmpty()) { + removeDuplicates(newItems, chat) + return splits + } + + val newSplits = splits.toMutableList() + val (newIds, _) = mapItemsToIds(chat.chatItems) + newItems.removeAll { + val duplicate = newIds.contains(it.id) + if (duplicate) { + val firstIndex = newSplits.indexOf(it.id) + if (firstIndex != -1) { + newSplits.removeAt(firstIndex) + } + } + duplicate + } + return newSplits +} + +// ids, number of unread items +private fun mapItemsToIds(items: List<ChatItem>): Pair<Set<Long>, Int> { + var unreadInLoaded = 0 + val ids = mutableSetOf<Long>() + var i = 0 + while (i < items.size) { + val item = items[i] + ids.add(item.id) + if (item.isRcvNew) { + unreadInLoaded++ + } + i++ + } + return ids to unreadInLoaded +} + +private fun removeDuplicates(newItems: SnapshotStateList<ChatItem>, chat: Chat) { + val (newIds, _) = mapItemsToIds(chat.chatItems) + newItems.removeAll { newIds.contains(it.id) } +} + +private data class SameTimeItem(val index: Int, val item: ChatItem) + +// return (item index, split index) +private fun indexToInsertAround(chatType: ChatType, lastNew: ChatItem?, to: List<ChatItem>, splits: Set<Long>): Pair<Int, Int> { + if (to.size <= 0 || lastNew == null) { + return 0 to 0 + } + // group sorting: item_ts, item_id + // everything else: created_at, item_id + val compareByTimeTs = chatType == ChatType.Group + // in case several items have the same time as another item in the `to` array + var sameTime: ArrayList<SameTimeItem> = arrayListOf() + + // trying to find new split index for item looks difficult but allows to not use one more loop. + // The idea is to memorize how many splits were till any index (map number of splits until index) + // and use resulting itemIndex to decide new split index position. + // Because of the possibility to have many items with the same timestamp, it's possible to see `itemIndex < || == || > i`. + val splitsTillIndex: ArrayList<Int> = arrayListOf() + var splitsPerPrevIndex = 0 + + for (i in to.indices) { + val item = to[i] + + splitsPerPrevIndex = if (splits.contains(item.id)) splitsPerPrevIndex + 1 else splitsPerPrevIndex + splitsTillIndex.add(splitsPerPrevIndex) + val itemIsNewer = (if (compareByTimeTs) item.meta.itemTs > lastNew.meta.itemTs else item.meta.createdAt > lastNew.meta.createdAt) + if (itemIsNewer || i + 1 == to.size) { + val same = if (compareByTimeTs) lastNew.meta.itemTs == item.meta.itemTs else lastNew.meta.createdAt == item.meta.createdAt + if (same) { + sameTime.add(SameTimeItem(i, item)) + } + // time to stop the loop. Item is newer, or it's the last item in `to` array, taking previous items and checking position inside them + val itemIndex: Int + val first = if (sameTime.size > 1) sameTime.sortedWith { prev, next -> prev.item.meta.itemId.compareTo(next.item.id) }.firstOrNull { same -> same.item.id > lastNew.id } else null + if (sameTime.size > 1 && first != null) { + itemIndex = first.index + } else if (sameTime.size == 1) { + itemIndex = if (sameTime[0].item.id > lastNew.id) sameTime[0].index else sameTime[0].index + 1 + } else { + itemIndex = if (itemIsNewer) i else i + 1 + } + val splitIndex = splitsTillIndex[min(itemIndex, splitsTillIndex.size - 1)] + val prevItemSplitIndex = if (itemIndex == 0) 0 else splitsTillIndex[min(itemIndex - 1, splitsTillIndex.size - 1)] + return Pair(itemIndex, if (splitIndex == prevItemSplitIndex) splitIndex else prevItemSplitIndex) + } + val same = if (compareByTimeTs) lastNew.meta.itemTs == item.meta.itemTs else lastNew.meta.createdAt == item.meta.createdAt + if (same) { + sameTime.add(SameTimeItem(index = i, item = item)) + } else { + sameTime = arrayListOf() + } + } + // shouldn't be here + return Pair(to.size, splits.size) +} + +private fun indexToInsertAroundTest() { + fun assert(one: Pair<Int, Int>, two: Pair<Int, Int>) { + if (one != two) { + throw Exception("$one != $two") + } + } + + val itemsToInsert = listOf(ChatItem.getSampleData(3, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds( 3), "")) + val items1 = listOf( + ChatItem.getSampleData(0, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds( 0), ""), + ChatItem.getSampleData(1, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds( 1), ""), + ChatItem.getSampleData(2, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds( 2), "") + ) + assert(indexToInsertAround(ChatType.Group, itemsToInsert.lastOrNull(), to = items1, setOf(1)), Pair(3, 1)) + + val items2 = listOf( + ChatItem.getSampleData(0, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(0), ""), + ChatItem.getSampleData(1, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(1), ""), + ChatItem.getSampleData(2, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(3), "") + ) + assert(indexToInsertAround(ChatType.Group, itemsToInsert.lastOrNull(), to = items2, setOf(2)), Pair(3, 1)) + + val items3 = listOf( + ChatItem.getSampleData(0, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(0), ""), + ChatItem.getSampleData(1, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(3), ""), + ChatItem.getSampleData(2, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(3), "") + ) + assert(indexToInsertAround(ChatType.Group, itemsToInsert.lastOrNull(), to = items3, setOf(1)), Pair(3, 1)) + + val items4 = listOf( + ChatItem.getSampleData(0, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(0), ""), + ChatItem.getSampleData(4, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(3), ""), + ChatItem.getSampleData(5, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(3), "") + ) + assert(indexToInsertAround(ChatType.Group, itemsToInsert.lastOrNull(), to = items4, setOf(4)), Pair(1, 0)) + + val items5 = listOf( + ChatItem.getSampleData(0, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(0), ""), + ChatItem.getSampleData(2, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(3), ""), + ChatItem.getSampleData(4, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(3), "") + ) + assert(indexToInsertAround(ChatType.Group, itemsToInsert.lastOrNull(), to = items5, setOf(2)), Pair(2, 1)) + + val items6 = listOf( + ChatItem.getSampleData(4, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(4), ""), + ChatItem.getSampleData(5, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(4), ""), + ChatItem.getSampleData(6, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(4), "") + ) + assert(indexToInsertAround(ChatType.Group, itemsToInsert.lastOrNull(), to = items6, setOf(5)), Pair(0, 0)) + + val items7 = listOf( + ChatItem.getSampleData(4, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(4), ""), + ChatItem.getSampleData(5, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(4), ""), + ChatItem.getSampleData(6, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(4), "") + ) + assert(indexToInsertAround(ChatType.Group, null, to = items7, setOf(6)), Pair(0, 0)) + + val items8 = listOf( + ChatItem.getSampleData(2, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(4), ""), + ChatItem.getSampleData(4, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(3), ""), + ChatItem.getSampleData(5, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(4), "") + ) + assert(indexToInsertAround(ChatType.Group, itemsToInsert.lastOrNull(), to = items8, setOf(2)), Pair(0, 0)) + + val items9 = listOf( + ChatItem.getSampleData(2, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(3), ""), + ChatItem.getSampleData(4, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(3), ""), + ChatItem.getSampleData(5, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(4), "") + ) + assert(indexToInsertAround(ChatType.Group, itemsToInsert.lastOrNull(), to = items9, setOf(5)), Pair(1, 0)) + + val items10 = listOf( + ChatItem.getSampleData(4, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(3), ""), + ChatItem.getSampleData(5, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(3), ""), + ChatItem.getSampleData(6, CIDirection.GroupSnd(), Instant.fromEpochMilliseconds(4), "") + ) + assert(indexToInsertAround(ChatType.Group, itemsToInsert.lastOrNull(), to = items10, setOf(4)), Pair(0, 0)) + + val items11: List<ChatItem> = listOf() + assert(indexToInsertAround(ChatType.Group, itemsToInsert.lastOrNull(), to = items11, emptySet()), Pair(0, 0)) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsMerger.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsMerger.kt new file mode 100644 index 0000000000..d98c041478 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsMerger.kt @@ -0,0 +1,374 @@ +package chat.simplex.common.views.chat + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.* +import chat.simplex.common.model.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow + +data class MergedItems ( + val items: List<MergedItem>, + val splits: List<SplitRange>, + // chat item id, index in list + val indexInParentItems: Map<Long, Int>, +) { + companion object { + fun create(items: List<ChatItem>, unreadCount: State<Int>, revealedItems: Set<Long>, chatState: ActiveChatState): MergedItems { + if (items.isEmpty()) return MergedItems(emptyList(), emptyList(), emptyMap()) + + val unreadAfterItemId = chatState.unreadAfterItemId + val itemSplits = chatState.splits.value + val mergedItems = ArrayList<MergedItem>() + // Indexes of splits here will be related to reversedChatItems, not chatModel.chatItems + val splitRanges = ArrayList<SplitRange>() + val indexInParentItems = mutableMapOf<Long, Int>() + var index = 0 + var unclosedSplitIndex: Int? = null + var unclosedSplitIndexInParent: Int? = null + var visibleItemIndexInParent = -1 + var unreadBefore = unreadCount.value - chatState.unreadAfterNewestLoaded.value + var lastRevealedIdsInMergedItems: MutableList<Long>? = null + var lastRangeInReversedForMergedItems: MutableStateFlow<IntRange>? = null + var recent: MergedItem? = null + while (index < items.size) { + val item = items[index] + val prev = items.getOrNull(index - 1) + val next = items.getOrNull(index + 1) + val category = item.mergeCategory + val itemIsSplit = itemSplits.contains(item.id) + + if (item.id == unreadAfterItemId.value) { + unreadBefore = unreadCount.value - chatState.unreadAfter.value + } + if (item.isRcvNew) unreadBefore-- + + val revealed = item.mergeCategory == null || revealedItems.contains(item.id) + if (recent is MergedItem.Grouped && recent.mergeCategory == category && !revealedItems.contains(recent.items.first().item.id) && !itemIsSplit) { + val listItem = ListItem(item, prev, next, unreadBefore) + recent.items.add(listItem) + + if (item.isRcvNew) { + recent.unreadIds.add(item.id) + } + if (lastRevealedIdsInMergedItems != null && lastRangeInReversedForMergedItems != null) { + if (revealed) { + lastRevealedIdsInMergedItems += item.id + } + lastRangeInReversedForMergedItems.value = lastRangeInReversedForMergedItems.value.first..index + } + } else { + visibleItemIndexInParent++ + val listItem = ListItem(item, prev, next, unreadBefore) + recent = if (item.mergeCategory != null) { + if (item.mergeCategory != prev?.mergeCategory || lastRevealedIdsInMergedItems == null) { + lastRevealedIdsInMergedItems = if (revealedItems.contains(item.id)) mutableListOf(item.id) else mutableListOf() + } else if (revealed) { + lastRevealedIdsInMergedItems += item.id + } + lastRangeInReversedForMergedItems = MutableStateFlow(index .. index) + MergedItem.Grouped( + items = arrayListOf(listItem), + revealed = revealed, + revealedIdsWithinGroup = lastRevealedIdsInMergedItems, + rangeInReversed = lastRangeInReversedForMergedItems, + mergeCategory = item.mergeCategory, + startIndexInReversedItems = index, + unreadIds = if (item.isRcvNew) mutableSetOf(item.id) else mutableSetOf() + ) + } else { + lastRangeInReversedForMergedItems = null + MergedItem.Single( + item = listItem, + startIndexInReversedItems = index + ) + } + mergedItems.add(recent) + } + if (itemIsSplit) { + // found item that is considered as a split + if (unclosedSplitIndex != null && unclosedSplitIndexInParent != null) { + // it was at least second split in the list + splitRanges.add(SplitRange(unclosedSplitIndex until index, unclosedSplitIndexInParent until visibleItemIndexInParent)) + } + unclosedSplitIndex = index + unclosedSplitIndexInParent = visibleItemIndexInParent + } else if (index + 1 == items.size && unclosedSplitIndex != null && unclosedSplitIndexInParent != null) { + // just one split for the whole list, there will be no more, it's the end + splitRanges.add(SplitRange(unclosedSplitIndex .. index, unclosedSplitIndexInParent .. visibleItemIndexInParent)) + } + indexInParentItems[item.id] = visibleItemIndexInParent + index++ + } + return MergedItems( + mergedItems, + splitRanges, + indexInParentItems + ) + } + } +} + +sealed class MergedItem { + abstract val startIndexInReversedItems: Int + + // the item that is always single, cannot be grouped and always revealed + data class Single( + val item: ListItem, + override val startIndexInReversedItems: Int, + ): MergedItem() + + /** The item that can contain multiple items or just one depending on revealed state. When the whole group of merged items is revealed, + * there will be multiple [Grouped] items with revealed flag set to true. When the whole group is collapsed, it will be just one instance + * of [Grouped] item with all grouped items inside [items]. In other words, number of [MergedItem] will always be equal to number of + * visible rows in ChatView LazyColumn */ + @Stable + data class Grouped ( + val items: ArrayList<ListItem>, + val revealed: Boolean, + // it stores ids for all consecutive revealed items from the same group in order to hide them all on user's action + // it's the same list instance for all Grouped items within revealed group + /** @see reveal */ + val revealedIdsWithinGroup: MutableList<Long>, + val rangeInReversed: MutableStateFlow<IntRange>, + val mergeCategory: CIMergeCategory?, + val unreadIds: MutableSet<Long>, + override val startIndexInReversedItems: Int, + ): MergedItem() { + fun reveal(reveal: Boolean, revealedItems: MutableState<Set<Long>>) { + val newRevealed = revealedItems.value.toMutableSet() + var i = 0 + if (reveal) { + while (i < items.size) { + newRevealed.add(items[i].item.id) + i++ + } + } else { + while (i < revealedIdsWithinGroup.size) { + newRevealed.remove(revealedIdsWithinGroup[i]) + i++ + } + revealedIdsWithinGroup.clear() + } + revealedItems.value = newRevealed + } + } + + fun hasUnread(): Boolean = when (this) { + is Single -> item.item.isRcvNew + is Grouped -> unreadIds.isNotEmpty() + } + + fun newest(): ListItem = when (this) { + is Single -> item + is Grouped -> items.first() + } + + fun oldest(): ListItem = when (this) { + is Single -> item + is Grouped -> items.last() + } + + fun lastIndexInReversed(): Int = when (this) { + is Single -> startIndexInReversedItems + is Grouped -> startIndexInReversedItems + items.lastIndex + } +} + +data class SplitRange( + /** range of indexes inside reversedChatItems where the first element is the split (it's index is [indexRangeInReversed.first]) + * so [0, 1, 2, -100-, 101] if the 3 is a split, SplitRange(indexRange = 3 .. 4) will be this SplitRange instance + * (3, 4 indexes of the splitRange with the split itself at index 3) + * */ + val indexRangeInReversed: IntRange, + /** range of indexes inside LazyColumn where the first element is the split (it's index is [indexRangeInParentItems.first]) */ + val indexRangeInParentItems: IntRange +) + +data class ListItem( + val item: ChatItem, + val prevItem: ChatItem?, + val nextItem: ChatItem?, + // how many unread items before (older than) this one (excluding this one) + val unreadBefore: Int +) + +data class ActiveChatState ( + val splits: MutableStateFlow<List<Long>> = MutableStateFlow(emptyList()), + val unreadAfterItemId: MutableStateFlow<Long> = MutableStateFlow(-1L), + // total items after unread after item (exclusive) + val totalAfter: MutableStateFlow<Int> = MutableStateFlow(0), + val unreadTotal: MutableStateFlow<Int> = MutableStateFlow(0), + // exclusive + val unreadAfter: MutableStateFlow<Int> = MutableStateFlow(0), + // exclusive + val unreadAfterNewestLoaded: MutableStateFlow<Int> = MutableStateFlow(0) +) { + fun moveUnreadAfterItem(toItemId: Long?, nonReversedItems: List<ChatItem>) { + toItemId ?: return + val currentIndex = nonReversedItems.indexOfFirst { it.id == unreadAfterItemId.value } + val newIndex = nonReversedItems.indexOfFirst { it.id == toItemId } + if (currentIndex == -1 || newIndex == -1) return + unreadAfterItemId.value = toItemId + val unreadDiff = if (newIndex > currentIndex) { + -nonReversedItems.subList(currentIndex + 1, newIndex + 1).count { it.isRcvNew } + } else { + nonReversedItems.subList(newIndex + 1, currentIndex + 1).count { it.isRcvNew } + } + unreadAfter.value += unreadDiff + } + + fun moveUnreadAfterItem(fromIndex: Int, toIndex: Int, nonReversedItems: List<ChatItem>) { + if (fromIndex == -1 || toIndex == -1) return + unreadAfterItemId.value = nonReversedItems[toIndex].id + val unreadDiff = if (toIndex > fromIndex) { + -nonReversedItems.subList(fromIndex + 1, toIndex + 1).count { it.isRcvNew } + } else { + nonReversedItems.subList(toIndex + 1, fromIndex + 1).count { it.isRcvNew } + } + unreadAfter.value += unreadDiff + } + + fun clear() { + splits.value = emptyList() + unreadAfterItemId.value = -1L + totalAfter.value = 0 + unreadTotal.value = 0 + unreadAfter.value = 0 + unreadAfterNewestLoaded.value = 0 + } + + fun itemsRead(itemIds: Set<Long>?, newItems: List<ChatItem>) { + if (itemIds == null) { + // special case when the whole chat became read + unreadTotal.value = 0 + unreadAfter.value = 0 + return + } + var unreadAfterItemIndex: Int = -1 + // since it's more often that the newest items become read, it's logical to loop from the end of the list to finish it faster + var i = newItems.lastIndex + val ids = itemIds.toMutableSet() + // intermediate variables to prevent re-setting state value a lot of times without reason + var newUnreadTotal = unreadTotal.value + var newUnreadAfter = unreadAfter.value + while (i >= 0) { + val item = newItems[i] + if (item.id == unreadAfterItemId.value) { + unreadAfterItemIndex = i + } + if (ids.contains(item.id)) { + // was unread, now this item is read + if (unreadAfterItemIndex == -1) { + newUnreadAfter-- + } + newUnreadTotal-- + ids.remove(item.id) + if (ids.isEmpty()) break + } + i-- + } + unreadTotal.value = newUnreadTotal + unreadAfter.value = newUnreadAfter + } + + fun itemAdded(item: Pair<Long, Boolean>) { + if (item.second) { + unreadAfter.value++ + unreadTotal.value++ + } + } + + fun itemsRemoved(itemIds: List<Triple<Long, Int, Boolean>>, newItems: List<ChatItem>) { + val newSplits = ArrayList<Long>() + for (split in splits.value) { + val index = itemIds.indexOfFirst { it.first == split } + // deleted the item that was right before the split between items, find newer item so it will act like the split + if (index != -1) { + val newSplit = newItems.getOrNull(itemIds[index].second - itemIds.count { it.second <= index })?.id + // it the whole section is gone and splits overlap, don't add it at all + if (newSplit != null && !newSplits.contains(newSplit)) { + newSplits.add(newSplit) + } + } else { + newSplits.add(split) + } + } + splits.value = newSplits + + val index = itemIds.indexOfFirst { it.first == unreadAfterItemId.value } + // unread after item was removed + if (index != -1) { + var newUnreadAfterItemId = newItems.getOrNull(itemIds[index].second - itemIds.count { it.second <= index })?.id + val newUnreadAfterItemWasNull = newUnreadAfterItemId == null + if (newUnreadAfterItemId == null) { + // everything on top (including unread after item) were deleted, take top item as unread after id + newUnreadAfterItemId = newItems.firstOrNull()?.id + } + if (newUnreadAfterItemId != null) { + unreadAfterItemId.value = newUnreadAfterItemId + totalAfter.value -= itemIds.count { it.second > index } + unreadTotal.value -= itemIds.count { it.second <= index && it.third } + unreadAfter.value -= itemIds.count { it.second > index && it.third } + if (newUnreadAfterItemWasNull) { + // since the unread after item was moved one item after initial position, adjust counters accordingly + if (newItems.firstOrNull()?.isRcvNew == true) { + unreadTotal.value++ + unreadAfter.value-- + } + } + } else { + // all items were deleted, 0 items in chatItems + unreadAfterItemId.value = -1L + totalAfter.value = 0 + unreadTotal.value = 0 + unreadAfter.value = 0 + } + } else { + totalAfter.value -= itemIds.size + } + } +} + +fun visibleItemIndexesNonReversed(mergedItems: State<MergedItems>, reversedItemsSize: Int, listState: LazyListState): IntRange { + val zero = 0 .. 0 + if (listState.layoutInfo.totalItemsCount == 0) return zero + val newest = mergedItems.value.items.getOrNull(listState.firstVisibleItemIndex)?.startIndexInReversedItems + val oldest = mergedItems.value.items.getOrNull(listState.layoutInfo.visibleItemsInfo.last().index)?.lastIndexInReversed() + if (newest == null || oldest == null) return zero + val range = reversedItemsSize - oldest .. reversedItemsSize - newest + if (range.first < 0 || range.last < 0) return zero + + // visible items mapped to their underlying data structure which is chatModel.chatItems + return range +} + +/** Helps in debugging */ +//@Composable +//fun BoxScope.ShowChatState() { +// Box(Modifier.align(Alignment.Center).size(200.dp).background(Color.Black)) { +// val s = chatModel.chatState +// Text( +// "itemId ${s.unreadAfterItemId.value} / ${chatModel.chatItems.value.firstOrNull { it.id == s.unreadAfterItemId.value }?.text}, \nunreadAfter ${s.unreadAfter.value}, afterNewest ${s.unreadAfterNewestLoaded.value}", +// color = Color.White +// ) +// } +//} +//// Returns items mapping for easy checking the structure +//fun MergedItems.mappingToString(): String = items.mapIndexed { index, g -> +// when (g) { +// is MergedItem.Single -> +// "\nstartIndexInParentItems $index, startIndexInReversedItems ${g.startIndexInReversedItems}, " + +// "revealed true, " + +// "mergeCategory null " + +// "\nunreadBefore ${g.item.unreadBefore}" +// +// is MergedItem.Grouped -> +// "\nstartIndexInParentItems $index, startIndexInReversedItems ${g.startIndexInReversedItems}, " + +// "revealed ${g.revealed}, " + +// "mergeCategory ${g.items[0].item.mergeCategory} " + +// g.items.mapIndexed { i, it -> +// "\nunreadBefore ${it.unreadBefore} ${Triple(index, g.startIndexInReversedItems + i, it.item.id)}" +// } +// } +//}.toString() 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 0e2a7c1680..6d7cdcdebe 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 @@ -1,29 +1,37 @@ package chat.simplex.common.views.chat +import androidx.compose.animation.* +import androidx.compose.animation.core.* import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.gestures.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* -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.mapSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.* -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.draw.* +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.text.* +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.CIDirection.GroupRcv +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ChatModel.activeCall +import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* import chat.simplex.common.views.chat.group.* @@ -33,446 +41,654 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.model.GroupInfo import chat.simplex.common.platform.* import chat.simplex.common.platform.AudioPlayer +import chat.simplex.common.views.newchat.ContactConnectionInfoView import chat.simplex.res.MR import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.datetime.Clock +import kotlinx.datetime.* import java.io.File import java.net.URI -import kotlin.math.sign +import kotlin.math.* + +@Stable +data class ItemSeparation(val timestamp: Boolean, val largeGap: Boolean, val date: Instant?) @Composable -fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: String) -> Unit) { - val activeChat = remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatId }) } - val searchText = rememberSaveable { mutableStateOf("") } +// staleChatId means the id that was before chatModel.chatId becomes null. It's needed for Android only to make transition from chat +// to chat list smooth. Otherwise, chat view will become blank right before the transition starts +fun ChatView( + chatsCtx: ChatModel.ChatsContext, + staleChatId: State<String?>, + scrollToItemId: MutableState<Long?> = remember { mutableStateOf(null) }, + onComposed: suspend (chatId: String) -> Unit +) { + val showSearch = rememberSaveable { mutableStateOf(false) } + // They have their own iterator inside for a reason to prevent crash "Reading a state that was created after the snapshot..." + val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } } + val activeChatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo } } val user = chatModel.currentUser.value - val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() - val composeState = rememberSaveable(saver = ComposeState.saver()) { - mutableStateOf( - if (chatModel.draftChatId.value == chatId && chatModel.draft.value != null) { - chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews) - } else { - ComposeState(useLinkPreviews = useLinkPreviews) - } - ) - } - val attachmentOption = rememberSaveable { mutableStateOf<AttachmentOption?>(null) } - val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) - val scope = rememberCoroutineScope() - LaunchedEffect(Unit) { - // snapshotFlow here is because it reacts much faster on changes in chatModel.chatId.value. - // With LaunchedEffect(chatModel.chatId.value) there is a noticeable delay before reconstruction of the view - launch { - snapshotFlow { chatModel.chatId.value } - .distinctUntilChanged() - .onEach { Log.d(TAG, "TODOCHAT: chatId: activeChatId ${activeChat.value?.id} == new chatId $it ${activeChat.value?.id == it} ") } - .filter { it != null && activeChat.value?.id != it } - .collect { chatId -> - // Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly - // Also for situation when chatId changes after clicking in notification, etc - activeChat.value = chatModel.getChat(chatId!!) - Log.d(TAG, "TODOCHAT: chatId: activeChatId became ${activeChat.value?.id}") - markUnreadChatAsRead(activeChat, chatModel) - } + val chatInfo = activeChatInfo.value + if (chatInfo == null || user == null) { + LaunchedEffect(Unit) { + chatModel.chatId.value = null + ModalManager.end.closeModals() } - launch { - snapshotFlow { - /** - * It's possible that in some cases concurrent modification can happen on [ChatModel.chats] list. - * In this case only error log will be printed here (no crash). - * TODO: Re-write [ChatModel.chats] logic to a new list assignment instead of changing content of mutableList to prevent that - * */ - try { - chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value } - } catch (e: ConcurrentModificationException) { - Log.e(TAG, e.stackTraceToString()) - null - } - } - .distinctUntilChanged() - .onEach { Log.d(TAG, "TODOCHAT: chats: activeChatId ${activeChat.value?.id} == new chatId ${it?.id} ${activeChat.value?.id == it?.id} ") } - // Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions - .filter { it != null && it?.chatInfo != activeChat.value?.chatInfo } - .collect { - activeChat.value = it - Log.d(TAG, "TODOCHAT: chats: activeChatId became ${activeChat.value?.id}") - } - } - } - val view = LocalMultiplatformView() - val chat = activeChat.value - if (chat == null || user == null) { - chatModel.chatId.value = null - ModalManager.end.closeModals() } else { - val chatRh = chat.remoteHostId + val searchText = rememberSaveable { mutableStateOf("") } + val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() + val composeState = rememberSaveable(saver = ComposeState.saver()) { + val draft = chatModel.draft.value + val sharedContent = chatModel.sharedContent.value + mutableStateOf( + if (chatModel.draftChatId.value == staleChatId.value && draft != null && (sharedContent !is SharedContent.Forward || sharedContent.fromChatInfo.id == staleChatId.value)) { + draft + } else { + ComposeState(useLinkPreviews = useLinkPreviews) + } + ) + } + val attachmentOption = rememberSaveable { mutableStateOf<AttachmentOption?>(null) } + val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) + val scope = rememberCoroutineScope() + val selectedChatItems = rememberSaveable { mutableStateOf(null as Set<Long>?) } + LaunchedEffect(Unit) { + // snapshotFlow here is because it reacts much faster on changes in chatModel.chatId.value. + // With LaunchedEffect(chatModel.chatId.value) there is a noticeable delay before reconstruction of the view + launch { + snapshotFlow { chatModel.chatId.value } + .distinctUntilChanged() + .filterNotNull() + .collect { chatId -> + if (chatsCtx.contentTag == null) { + markUnreadChatAsRead(chatId) + } + showSearch.value = false + searchText.value = "" + selectedChatItems.value = null + } + } + } + val view = LocalMultiplatformView() + val chatRh = remoteHostId.value // We need to have real unreadCount value for displaying it inside top right button // Having activeChat reloaded on every change in it is inefficient (UI lags) val unreadCount = remember { derivedStateOf { - chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }?.chatStats?.unreadCount ?: 0 + chatsCtx.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatStats?.unreadCount ?: 0 } } val clipboard = LocalClipboardManager.current + CompositionLocalProvider( + LocalAppBarHandler provides rememberAppBarHandler(chatInfo.id, keyboardCoversBar = false), + ) { + when (chatInfo) { + is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> { + var groupMembersJob: Job = remember { Job() } + val perChatTheme = remember(chatInfo, CurrentColors.value.base) { if (chatInfo is ChatInfo.Direct) chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null } + val overrides = if (perChatTheme != null) ThemeManager.currentColors(null, perChatTheme, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) else null + val fullDeleteAllowed = remember(chatInfo) { chatInfo.featureEnabled(ChatFeature.FullDelete) } - ChatLayout( - chat, - unreadCount, - composeState, - composeView = { - Column( - Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - if ( - chat.chatInfo is ChatInfo.Direct - && !chat.chatInfo.contact.ready - && chat.chatInfo.contact.active - && !chat.chatInfo.contact.nextSendGrpInv - ) { - Text( - generalGetString(MR.strings.contact_connection_pending), - Modifier.padding(top = 4.dp), - fontSize = 14.sp, - color = MaterialTheme.colors.secondary - ) + SimpleXThemeOverride(overrides ?: CurrentColors.collectAsState().value) { + val onSearchValueChanged: (String) -> Unit = onSearchValueChanged@{ value -> + val sameText = searchText.value == value + // showSearch can be false with empty text when it was closed manually after clicking on message from search to load .around it + // (required on Android to have this check to prevent call to search with old text) + val emptyAndClosedSearch = searchText.value.isEmpty() && !showSearch.value && chatsCtx.contentTag == null + val c = chatModel.getChat(chatInfo.id) + if (sameText || emptyAndClosedSearch || c == null || chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged + withBGApi { + apiFindMessages(chatsCtx, c, value) + searchText.value = value + } } - ComposeView( - chatModel, chat, composeState, attachmentOption, - showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } } + ChatLayout( + chatsCtx = chatsCtx, + remoteHostId = remoteHostId, + chatInfo = activeChatInfo, + unreadCount, + composeState, + composeView = { focusRequester -> + if (selectedChatItems.value == null) { + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if ( + chatInfo is ChatInfo.Direct + && !chatInfo.contact.sndReady + && chatInfo.contact.active + && !chatInfo.contact.nextSendGrpInv + ) { + Text( + generalGetString(MR.strings.contact_connection_pending), + Modifier.padding(top = 4.dp), + fontSize = 14.sp, + color = MaterialTheme.colors.secondary + ) + } + ComposeView( + chatModel, Chat(remoteHostId = chatRh, chatInfo = chatInfo, chatItems = emptyList()), composeState, attachmentOption, + showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }, + focusRequester = focusRequester + ) + } + } else { + SelectedItemsButtonsToolbar( + chatsCtx = chatsCtx, + selectedChatItems = selectedChatItems, + chatInfo = chatInfo, + deleteItems = { canDeleteForAll -> + val itemIds = selectedChatItems.value + val questionText = + if (!canDeleteForAll || fullDeleteAllowed || chatInfo is ChatInfo.Local) + generalGetString(MR.strings.delete_messages_cannot_be_undone_warning) + else + generalGetString(MR.strings.delete_messages_mark_deleted_warning) + if (itemIds != null) { + deleteMessagesAlertDialog( + itemIds.sorted(), + questionText = questionText, + forAll = canDeleteForAll, + deleteMessages = { ids, forAll -> + deleteMessages(chatRh, chatInfo, ids, forAll, moderate = false) { + selectedChatItems.value = null + } + } + ) + } + }, + archiveItems = { archiveItems(chatRh, chatInfo, selectedChatItems) }, + moderateItems = { + if (chatInfo is ChatInfo.Group) { + val itemIds = selectedChatItems.value + if (itemIds != null) { + moderateMessagesAlertDialog(itemIds.sorted(), moderateMessageQuestionText(chatInfo.featureEnabled(ChatFeature.FullDelete), itemIds.size), deleteMessages = { ids -> + deleteMessages(chatRh, chatInfo, ids, true, moderate = true) { + selectedChatItems.value = null + } + }) + } + } + }, + forwardItems = { + val itemIds = selectedChatItems.value + + if (itemIds != null) { + withBGApi { + val chatItemIds = itemIds.toList() + val forwardPlan = controller.apiPlanForwardChatItems( + rh = chatRh, + fromChatType = chatInfo.chatType, + fromChatId = chatInfo.apiId, + chatItemIds = chatItemIds + ) + + if (forwardPlan != null) { + if (forwardPlan.chatItemIds.count() < chatItemIds.count() || forwardPlan.forwardConfirmation != null) { + handleForwardConfirmation(chatRh, forwardPlan, chatInfo) + } else { + forwardContent(forwardPlan.chatItemIds, chatInfo) + } + } + } + } + }, + ) + } + }, + scrollToItemId, + attachmentOption, + attachmentBottomSheetState, + searchText, + useLinkPreviews = useLinkPreviews, + linkMode = chatModel.simplexLinkMode.value, + selectedChatItems = selectedChatItems, + back = { + hideKeyboard(view) + AudioPlayer.stop() + chatModel.chatId.value = null + chatModel.groupMembers.value = emptyList() + chatModel.groupMembersIndexes.value = emptyMap() + chatModel.membersLoaded.value = false + }, + info = { + if (ModalManager.end.hasModalsOpen()) { + ModalManager.end.closeModals() + return@ChatLayout + } + hideKeyboard(view) + groupMembersJob.cancel() + groupMembersJob = scope.launch(Dispatchers.Default) { + // The idea is to preload information before showing a modal because large groups can take time to load all members + var preloadedContactInfo: Pair<ConnectionStats?, Profile?>? = null + var preloadedCode: String? = null + var preloadedLink: Pair<CreatedConnLink, GroupMemberRole>? = null + if (chatInfo is ChatInfo.Direct) { + preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) + preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second + } else if (chatInfo is ChatInfo.Group) { + setGroupMembers(chatRh, chatInfo.groupInfo, chatModel) + preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId) + } + if (!isActive) return@launch + + val selectedItems: MutableState<Set<Long>?> = mutableStateOf(null) + ModalManager.end.showCustomModal { close -> + val appBar = remember { mutableStateOf(null as @Composable (BoxScope.() -> Unit)?) } + ModalView(close, appBar = appBar.value) { + val chatInfo = remember { activeChatInfo }.value + if (chatInfo is ChatInfo.Direct) { + var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) } + var code: String? by remember { mutableStateOf(preloadedCode) } + KeyChangeEffect(chatInfo.id, ChatModel.networkStatuses.toMap()) { + contactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) + preloadedContactInfo = contactInfo + code = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second + preloadedCode = code + } + ChatInfoView(chatsCtx, chatModel, chatInfo.contact, contactInfo?.first, contactInfo?.second, chatInfo.localAlias, code, close) { + showSearch.value = true + } + } else if (chatInfo is ChatInfo.Group) { + var link: Pair<CreatedConnLink, GroupMemberRole>? by remember(chatInfo.id) { mutableStateOf(preloadedLink) } + KeyChangeEffect(chatInfo.id) { + setGroupMembers(chatRh, chatInfo.groupInfo, chatModel) + link = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId) + preloadedLink = link + } + GroupChatInfoView(chatsCtx, chatRh, chatInfo.id, link?.first, link?.second, selectedItems, appBar, scrollToItemId, { + link = it + preloadedLink = it + }, close, { showSearch.value = true }) + } else { + LaunchedEffect(Unit) { + close() + } + } + LaunchedEffect(Unit) { + snapshotFlow { activeChatInfo.value?.id } + .drop(1) + .collect { + appBar.value = null + selectedItems.value = null + } + } + } + } + } + }, + showGroupReports = { + val info = activeChatInfo.value ?: return@ChatLayout + if (ModalManager.end.hasModalsOpen()) { + ModalManager.end.closeModals() + return@ChatLayout + } + hideKeyboard(view) + scope.launch { + showGroupReportsView(staleChatId, scrollToItemId, info) + } + }, + showMemberInfo = { groupInfo: GroupInfo, member: GroupMember -> + hideKeyboard(view) + groupMembersJob.cancel() + groupMembersJob = scope.launch(Dispatchers.Default) { + val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId) + val stats = r?.second + val (_, code) = if (member.memberActive) { + val memCode = chatModel.controller.apiGetGroupMemberCode(chatRh, groupInfo.apiId, member.groupMemberId) + member to memCode?.second + } else { + member to null + } + setGroupMembers(chatRh, groupInfo, chatModel) + if (!isActive) return@launch + + if (chatsCtx.contentTag == null) { + ModalManager.end.closeModals() + } + ModalManager.end.showModalCloseable(true) { close -> + remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem -> + GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close) + } + } + } + }, + loadMessages = { chatId, pagination, visibleItemIndexes -> + val c = chatModel.getChat(chatId) + if (chatModel.chatId.value != chatId) return@ChatLayout + if (c != null) { + apiLoadMessages(chatsCtx, c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, pagination, searchText.value, null, visibleItemIndexes) + } + }, + deleteMessage = { itemId, mode -> + withBGApi { + val toDeleteItem = reversedChatItemsStatic(chatsCtx).lastOrNull { it.id == itemId } + val toModerate = toDeleteItem?.memberToModerate(chatInfo) + val groupInfo = toModerate?.first + val groupMember = toModerate?.second + val deletedChatItem: ChatItem? + val toChatItem: ChatItem? + val r = if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) { + chatModel.controller.apiDeleteMemberChatItems( + chatRh, + groupId = groupInfo.groupId, + itemIds = listOf(itemId) + ) + } else { + chatModel.controller.apiDeleteChatItems( + chatRh, + type = chatInfo.chatType, + id = chatInfo.apiId, + itemIds = listOf(itemId), + mode = mode + ) + } + val deleted = r?.firstOrNull() + if (deleted != null) { + deletedChatItem = deleted.deletedChatItem.chatItem + toChatItem = deleted.toChatItem?.chatItem + withContext(Dispatchers.Main) { + if (toChatItem != null) { + chatModel.chatsContext.upsertChatItem(chatRh, chatInfo, toChatItem) + } else { + chatModel.chatsContext.removeChatItem(chatRh, chatInfo, deletedChatItem) + } + val deletedItem = deleted.deletedChatItem.chatItem + if (deletedItem.isActiveReport) { + chatModel.chatsContext.decreaseGroupReportsCounter(chatRh, chatInfo.id) + } + } + withContext(Dispatchers.Main) { + if (deletedChatItem.isReport) { + if (toChatItem != null) { + chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem) + } else { + chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, deletedChatItem) + } + } + } + } + } + }, + deleteMessages = { itemIds -> deleteMessages(chatRh, chatInfo, itemIds, false, moderate = false) }, + archiveReports = { itemIds, forAll -> archiveReports(chatRh, chatInfo, itemIds, forAll) }, + receiveFile = { fileId -> + withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId) } + }, + cancelFile = { fileId -> + withBGApi { chatModel.controller.cancelFile(chatRh, user, fileId) } + }, + joinGroup = { groupId, onComplete -> + withBGApi { + chatModel.controller.apiJoinGroup(chatRh, groupId) + onComplete.invoke() + } + }, + startCall = out@{ media -> startChatCall(chatRh, chatInfo, media) }, + endCall = { + val call = chatModel.activeCall.value + if (call != null) withBGApi { chatModel.callManager.endCall(call) } + }, + acceptCall = { contact -> + hideKeyboard(view) + withBGApi { + val invitation = chatModel.callInvitations.remove(contact.id) + ?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id } + if (invitation == null) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended)) + } else { + chatModel.callManager.acceptIncomingCall(invitation = invitation) + } + } + }, + acceptFeature = { contact, feature, param -> + withBGApi { + chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param) + } + }, + openDirectChat = { contactId -> + scope.launch { + openDirectChat(chatRh, contactId) + } + }, + forwardItem = { cInfo, cItem -> + chatModel.chatId.value = null + chatModel.sharedContent.value = SharedContent.Forward(listOf(cItem), cInfo) + }, + updateContactStats = { contact -> + withBGApi { + val r = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) + if (r != null) { + val contactStats = r.first + if (contactStats != null) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnectionStats(chatRh, contact, contactStats) + } + } + } + }, + updateMemberStats = { groupInfo, member -> + withBGApi { + val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId) + if (r != null) { + val memStats = r.second + if (memStats != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats) + } + } + } + } + }, + syncContactConnection = { contact -> + withBGApi { + val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) + if (cStats != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnectionStats(chatRh, contact, cStats) + } + } + } + }, + syncMemberConnection = { groupInfo, member -> + withBGApi { + val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false) + if (r != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second) + } + } + } + }, + findModelChat = { chatId -> + chatModel.getChat(chatId) + }, + findModelMember = { memberId -> + chatModel.groupMembers.value.find { it.id == memberId } + }, + setReaction = { cInfo, cItem, add, reaction -> + withBGApi { + val updatedCI = chatModel.controller.apiChatItemReaction( + rh = chatRh, + type = cInfo.chatType, + id = cInfo.apiId, + itemId = cItem.id, + add = add, + reaction = reaction + ) + if (updatedCI != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatItem(cInfo, updatedCI) + } + withContext(Dispatchers.Main) { + if (cItem.isReport) { + chatModel.secondaryChatsContext.value?.updateChatItem(cInfo, updatedCI) + } + } + } + } + }, + showItemDetails = { cInfo, cItem -> + suspend fun loadChatItemInfo(): ChatItemInfo? = coroutineScope { + val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id) + if (ciInfo != null) { + if (chatInfo is ChatInfo.Group) { + setGroupMembers(chatRh, chatInfo.groupInfo, chatModel) + if (!isActive) return@coroutineScope null + } + } + ciInfo + } + groupMembersJob.cancel() + groupMembersJob = scope.launch(Dispatchers.Default) { + var initialCiInfo = loadChatItemInfo() ?: return@launch + if (!ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) { + ModalManager.end.closeModals() + } + ModalManager.end.showModalCloseable(endButtons = { + ShareButton { + clipboard.shareText(itemInfoShareText(chatModel, cItem, initialCiInfo, chatModel.controller.appPrefs.developerTools.get())) + } + }) { close -> + var ciInfo by remember(cItem.id) { mutableStateOf(initialCiInfo) } + ChatItemInfoView(chatRh, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get(), chatInfo) + LaunchedEffect(cItem.id) { + withContext(Dispatchers.Default) { + for (msg in controller.messagesChannel) { + if (msg.rhId == chatRh && + msg is API.Result && + msg.res is CR.ChatItemsStatusesUpdated && + msg.res.chatItems.any { it.chatItem.id == cItem.id } + ) { + ciInfo = loadChatItemInfo() ?: return@withContext + initialCiInfo = ciInfo + } + } + } + } + KeyChangeEffect(chatModel.chatId.value) { + close() + } + } + } + }, + addMembers = { groupInfo -> addGroupMembers(view = view, groupInfo = groupInfo, rhId = chatRh, close = { ModalManager.end.closeModals() }) }, + openGroupLink = { groupInfo -> openGroupLink(view = view, groupInfo = groupInfo, rhId = chatRh, close = { ModalManager.end.closeModals() }) }, + markItemsRead = { itemsIds -> + withBGApi { + withContext(Dispatchers.Main) { + chatModel.chatsContext.markChatItemsRead(chatRh, chatInfo.id, itemsIds) + ntfManager.cancelNotificationsForChat(chatInfo.id) + chatModel.controller.apiChatItemsRead( + chatRh, + chatInfo.chatType, + chatInfo.apiId, + itemsIds + ) + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.markChatItemsRead(chatRh, chatInfo.id, itemsIds) + } + } + }, + markChatRead = { + withBGApi { + withContext(Dispatchers.Main) { + chatModel.chatsContext.markChatItemsRead(chatRh, chatInfo.id) + ntfManager.cancelNotificationsForChat(chatInfo.id) + chatModel.controller.apiChatRead( + chatRh, + chatInfo.chatType, + chatInfo.apiId + ) + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.markChatItemsRead(chatRh, chatInfo.id) + } + } + }, + changeNtfsState = { enabled, currentValue -> toggleNotifications(chatRh, chatInfo, enabled, chatModel, currentValue) }, + onSearchValueChanged = onSearchValueChanged, + closeSearch = { + showSearch.value = false + searchText.value = "" + }, + onComposed, + developerTools = chatModel.controller.appPrefs.developerTools.get(), + showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(), + showSearch = showSearch ) } - }, - attachmentOption, - attachmentBottomSheetState, - chatModel.chatItems, - searchText, - useLinkPreviews = useLinkPreviews, - linkMode = chatModel.simplexLinkMode.value, - back = { - hideKeyboard(view) - AudioPlayer.stop() - chatModel.chatId.value = null - chatModel.groupMembers.clear() - }, - info = { - if (ModalManager.end.hasModalsOpen()) { - ModalManager.end.closeModals() - return@ChatLayout - } - hideKeyboard(view) - withApi { - // The idea is to preload information before showing a modal because large groups can take time to load all members - var preloadedContactInfo: Pair<ConnectionStats?, Profile?>? = null - var preloadedCode: String? = null - var preloadedLink: Pair<String, GroupMemberRole>? = null - if (chat.chatInfo is ChatInfo.Direct) { - preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId) - preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second - } else if (chat.chatInfo is ChatInfo.Group) { - setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel) - preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId) - } - ModalManager.end.showModalCloseable(true) { close -> - val chat = remember { activeChat }.value - if (chat?.chatInfo is ChatInfo.Direct) { - var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) } - var code: String? by remember { mutableStateOf(preloadedCode) } - KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) { - contactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId) - preloadedContactInfo = contactInfo - code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second - preloadedCode = code - } - ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close) - } else if (chat?.chatInfo is ChatInfo.Group) { - var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) } - KeyChangeEffect(chat.id) { - setGroupMembers(chatRh, (chat.chatInfo as ChatInfo.Group).groupInfo, chatModel) - link = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId) - preloadedLink = link - } - GroupChatInfoView(chatModel, chatRh, chat.id, link?.first, link?.second, { - link = it - preloadedLink = it - }, close) - } - } - } - }, - showMemberInfo = { groupInfo: GroupInfo, member: GroupMember -> - hideKeyboard(view) - withApi { - val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId) - val stats = r?.second - val (_, code) = if (member.memberActive) { - val memCode = chatModel.controller.apiGetGroupMemberCode(chatRh, groupInfo.apiId, member.groupMemberId) - member to memCode?.second - } else { - member to null - } - setGroupMembers(chatRh, groupInfo, chatModel) - ModalManager.end.closeModals() - ModalManager.end.showModalCloseable(true) { close -> - remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem -> - GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close) - } - } - } - }, - loadPrevMessages = { - if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout - val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) - val firstId = chatModel.chatItems.firstOrNull()?.id - if (c != null && firstId != null) { - withApi { - Log.d(TAG, "TODOCHAT: loadPrevMessages: loading for ${c.id}, current chatId ${ChatModel.chatId.value}, size was ${ChatModel.chatItems.size}") - apiLoadPrevMessages(c, chatModel, firstId, searchText.value) - Log.d(TAG, "TODOCHAT: loadPrevMessages: loaded for ${c.id}, current chatId ${ChatModel.chatId.value}, size now ${ChatModel.chatItems.size}") - } - } - }, - deleteMessage = { itemId, mode -> - withApi { - val cInfo = chat.chatInfo - val toDeleteItem = chatModel.chatItems.firstOrNull { it.id == itemId } - val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo) - val groupInfo = toModerate?.first - val groupMember = toModerate?.second - val deletedChatItem: ChatItem? - val toChatItem: ChatItem? - if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) { - val r = chatModel.controller.apiDeleteMemberChatItem( - chatRh, - groupId = groupInfo.groupId, - groupMemberId = groupMember.groupMemberId, - itemId = itemId - ) - deletedChatItem = r?.first - toChatItem = r?.second - } else { - val r = chatModel.controller.apiDeleteChatItem( - chatRh, - type = cInfo.chatType, - id = cInfo.apiId, - itemId = itemId, - mode = mode - ) - deletedChatItem = r?.deletedChatItem?.chatItem - toChatItem = r?.toChatItem?.chatItem - } - if (toChatItem == null && deletedChatItem != null) { - chatModel.removeChatItem(chatRh, cInfo, deletedChatItem) - } else if (toChatItem != null) { - chatModel.upsertChatItem(chatRh, cInfo, toChatItem) - } - } - }, - deleteMessages = { itemIds -> - if (itemIds.isNotEmpty()) { - val chatInfo = chat.chatInfo - withBGApi { - val deletedItems: ArrayList<ChatItem> = arrayListOf() - for (itemId in itemIds) { - val di = chatModel.controller.apiDeleteChatItem( - chatRh, chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal - )?.deletedChatItem?.chatItem - if (di != null) { - deletedItems.add(di) - } - } - for (di in deletedItems) { - chatModel.removeChatItem(chatRh, chatInfo, di) - } - } - } - }, - receiveFile = { fileId, encrypted -> - withApi { chatModel.controller.receiveFile(chatRh, user, fileId, encrypted) } - }, - cancelFile = { fileId -> - withApi { chatModel.controller.cancelFile(chatRh, user, fileId) } - }, - joinGroup = { groupId, onComplete -> - withApi { - chatModel.controller.apiJoinGroup(chatRh, groupId) - onComplete.invoke() - } - }, - startCall = out@ { media -> - withBGApi { - val cInfo = chat.chatInfo - if (cInfo is ChatInfo.Direct) { - chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media) - chatModel.showCallView.value = true - chatModel.callCommand.add(WCallCommand.Capabilities(media)) - } - } - }, - endCall = { - val call = chatModel.activeCall.value - if (call != null) withApi { chatModel.callManager.endCall(call) } - }, - acceptCall = { contact -> - hideKeyboard(view) - val invitation = chatModel.callInvitations.remove(contact.id) - if (invitation == null) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended)) - } else { - chatModel.callManager.acceptIncomingCall(invitation = invitation) - } - }, - acceptFeature = { contact, feature, param -> - withApi { - chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param) - } - }, - openDirectChat = { contactId -> - withApi { - openDirectChat(chatRh, contactId, chatModel) - } - }, - updateContactStats = { contact -> - withApi { - val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId) - if (r != null) { - val contactStats = r.first - if (contactStats != null) - chatModel.updateContactConnectionStats(chatRh, contact, contactStats) - } - } - }, - updateMemberStats = { groupInfo, member -> - withApi { - val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId) - if (r != null) { - val memStats = r.second - if (memStats != null) { - chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats) - } - } - } - }, - syncContactConnection = { contact -> - withApi { - val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) - if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) - } - } - }, - syncMemberConnection = { groupInfo, member -> - withApi { - val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false) - if (r != null) { - chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second) - } - } - }, - findModelChat = { chatId -> - chatModel.getChat(chatId) - }, - findModelMember = { memberId -> - chatModel.groupMembers.find { it.id == memberId } - }, - setReaction = { cInfo, cItem, add, reaction -> - withApi { - val updatedCI = chatModel.controller.apiChatItemReaction( - rh = chatRh, - type = cInfo.chatType, - id = cInfo.apiId, - itemId = cItem.id, - add = add, - reaction = reaction - ) - if (updatedCI != null) { - chatModel.updateChatItem(cInfo, updatedCI) - } - } - }, - showItemDetails = { cInfo, cItem -> - withApi { - val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id) - if (ciInfo != null) { - if (chat.chatInfo is ChatInfo.Group) { - setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel) - } + } + is ChatInfo.ContactConnection -> { + val close = { chatModel.chatId.value = null } + ModalView(close, showClose = appPlatform.isAndroid, content = { + ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connLinkInv, chatInfo.contactConnection, false, close) + }) + LaunchedEffect(chatInfo.id) { + onComposed(chatInfo.id) ModalManager.end.closeModals() - ModalManager.end.showModal(endButtons = { ShareButton { - clipboard.shareText(itemInfoShareText(chatModel, cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get())) - } }) { - ChatItemInfoView(chatModel, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get()) + withContext(Dispatchers.Main) { + chatModel.chatsContext.chatItems.clearAndNotify() } } - } - }, - addMembers = { groupInfo -> - hideKeyboard(view) - withApi { - setGroupMembers(chatRh, groupInfo, chatModel) - ModalManager.end.closeModals() - ModalManager.end.showModalCloseable(true) { close -> - AddGroupMembersView(chatRh, groupInfo, false, chatModel, close) + } + is ChatInfo.InvalidJSON -> { + val close = { chatModel.chatId.value = null } + ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chatInfo.json) } }, content = { + InvalidJSONView(chatInfo.json) + }) + LaunchedEffect(chatInfo.id) { + onComposed(chatInfo.id) + ModalManager.end.closeModals() + withContext(Dispatchers.Main) { + chatModel.chatsContext.chatItems.clearAndNotify() + } } - } - }, - openGroupLink = { groupInfo -> - hideKeyboard(view) - withApi { - val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId) - ModalManager.end.closeModals() - ModalManager.end.showModalCloseable(true) { - GroupLinkView(chatModel, chatRh, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null) - } - } - }, - markRead = { range, unreadCountAfter -> - chatModel.markChatItemsRead(chat, range, unreadCountAfter) - ntfManager.cancelNotificationsForChat(chat.id) - withBGApi { - chatModel.controller.apiChatRead( - chatRh, - chat.chatInfo.chatType, - chat.chatInfo.apiId, - range - ) - } - }, - changeNtfsState = { enabled, currentValue -> toggleNotifications(chat, enabled, chatModel, currentValue) }, - onSearchValueChanged = { value -> - if (searchText.value == value) return@ChatLayout - if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout - val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) ?: return@ChatLayout - withApi { - apiFindMessages(c, chatModel, value) - searchText.value = value - } - }, - onComposed, - developerTools = chatModel.controller.appPrefs.developerTools.get(), - ) + } + else -> {} + } + } + } +} + +fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType) { + withBGApi { + if (chatInfo is ChatInfo.Direct) { + val contactInfo = chatModel.controller.apiContactInfo(remoteHostId, chatInfo.contact.contactId) + val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi + activeCall.value?.androidCallState?.close() + chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, initialCallType = media, userProfile = profile, androidCallState = platform.androidCreateActiveCallState()) + chatModel.showCallView.value = true + chatModel.callCommand.add(WCallCommand.Capabilities(media)) + } } } @Composable fun ChatLayout( - chat: Chat, + chatsCtx: ChatModel.ChatsContext, + remoteHostId: State<Long?>, + chatInfo: State<ChatInfo?>, unreadCount: State<Int>, composeState: MutableState<ComposeState>, - composeView: (@Composable () -> Unit), + composeView: (@Composable (FocusRequester?) -> Unit), + scrollToItemId: MutableState<Long?>, attachmentOption: MutableState<AttachmentOption?>, attachmentBottomSheetState: ModalBottomSheetState, - chatItems: List<ChatItem>, searchValue: State<String>, useLinkPreviews: Boolean, linkMode: SimplexLinkMode, + selectedChatItems: MutableState<Set<Long>?>, back: () -> Unit, info: () -> Unit, + showGroupReports: () -> Unit, showMemberInfo: (GroupInfo, GroupMember) -> Unit, - loadPrevMessages: () -> Unit, + loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List<Long>) -> Unit, - receiveFile: (Long, Boolean) -> Unit, + archiveReports: (List<Long>, Boolean) -> Unit, + receiveFile: (Long) -> Unit, cancelFile: (Long) -> Unit, joinGroup: (Long, () -> Unit) -> Unit, startCall: (CallMediaType) -> Unit, @@ -480,6 +696,7 @@ fun ChatLayout( acceptCall: (Contact) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, openDirectChat: (Long) -> Unit, + forwardItem: (ChatInfo, ChatItem) -> Unit, updateContactStats: (Contact) -> Unit, updateMemberStats: (GroupInfo, GroupMember) -> Unit, syncContactConnection: (Contact) -> Unit, @@ -490,11 +707,15 @@ fun ChatLayout( showItemDetails: (ChatInfo, ChatItem) -> Unit, addMembers: (GroupInfo) -> Unit, openGroupLink: (GroupInfo) -> Unit, - markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, - changeNtfsState: (Boolean, currentValue: MutableState<Boolean>) -> Unit, + markItemsRead: (List<Long>) -> Unit, + markChatRead: () -> Unit, + changeNtfsState: (MsgFilter, currentValue: MutableState<MsgFilter>) -> Unit, onSearchValueChanged: (String) -> Unit, + closeSearch: () -> Unit, onComposed: suspend (chatId: String) -> Unit, developerTools: Boolean, + showViaProxy: Boolean, + showSearch: MutableState<Boolean> ) { val scope = rememberCoroutineScope() val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } } @@ -502,57 +723,151 @@ fun ChatLayout( Modifier .fillMaxWidth() .desktopOnExternalDrag( - enabled = !attachmentDisabled.value && rememberUpdatedState(chat.userCanSend).value, + enabled = remember(attachmentDisabled.value, chatInfo.value?.sendMsgEnabled) { mutableStateOf(!attachmentDisabled.value && chatInfo.value?.sendMsgEnabled == true) }.value, onFiles = { paths -> composeState.onFilesAttached(paths.map { it.toURI() }) }, - onImage = { - // TODO: file is not saved anywhere?! - val tmpFile = File.createTempFile("image", ".bmp", tmpDir) - tmpFile.deleteOnExit() - chatModel.filesToDelete.add(tmpFile) - val uri = tmpFile.toURI() - CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(uri), null) } - }, + onImage = { file -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(file.toURI()), null) } }, onText = { // Need to parse HTML in order to correctly display the content //composeState.value = composeState.value.copy(message = composeState.value.message + it) }, ) ) { - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - ModalBottomSheetLayout( - scrimColor = Color.Black.copy(alpha = 0.12F), - modifier = Modifier.navigationBarsWithImePadding(), - sheetContent = { - ChooseAttachmentView( - attachmentOption, - hide = { scope.launch { attachmentBottomSheetState.hide() } } - ) - }, - sheetState = attachmentBottomSheetState, - sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) - ) { - val floatingButton: MutableState<@Composable () -> Unit> = remember { mutableStateOf({}) } - val setFloatingButton = { button: @Composable () -> Unit -> - floatingButton.value = button + ModalBottomSheetLayout( + scrimColor = Color.Black.copy(alpha = 0.12F), + sheetElevation = 0.dp, + sheetContent = { + ChooseAttachmentView( + attachmentOption, + hide = { scope.launch { attachmentBottomSheetState.hide() } } + ) + }, + sheetState = attachmentBottomSheetState, + sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) + ) { + val composeViewHeight = remember { mutableStateOf(0.dp) } + Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer, drawWallpaper = chatsCtx.contentTag == null)) { + val remoteHostId = remember { remoteHostId }.value + val chatInfo = remember { chatInfo }.value + val oneHandUI = remember { appPrefs.oneHandUI.state } + val chatBottomBar = remember { appPrefs.chatBottomBar.state } + val composeViewFocusRequester = remember { if (appPlatform.isDesktop) FocusRequester() else null } + AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) { + if (chatInfo != null) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) { + // disables scrolling to top of chat item on click inside the bubble + CompositionLocalProvider(LocalBringIntoViewSpec provides object : BringIntoViewSpec { + override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float = 0f + }) { + ChatItemsList( + chatsCtx, remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue, + useLinkPreviews, linkMode, scrollToItemId, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages, archiveReports, + receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, + updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, + setReaction, showItemDetails, markItemsRead, markChatRead, closeSearch, remember { { onComposed(it) } }, developerTools, showViaProxy, + ) + } + if (chatInfo is ChatInfo.Group && composeState.value.message.text.isNotEmpty()) { + Column( + Modifier + .align(Alignment.BottomStart) + .padding(bottom = composeViewHeight.value) + ) { + GroupMentions( + rhId = remoteHostId, + composeState = composeState, + composeViewFocusRequester = composeViewFocusRequester, + chatInfo = chatInfo, + ) + } + } + } + } + if (chatsCtx.contentTag == MsgContentTag.Report) { + Column( + Modifier + .layoutId(CHAT_COMPOSE_LAYOUT_ID) + .align(Alignment.BottomCenter) + .navigationBarsPadding() + .imePadding() + ) { + AnimatedVisibility(selectedChatItems.value != null) { + if (chatInfo != null) { + SelectedItemsButtonsToolbar( + chatsCtx = chatsCtx, + selectedChatItems = selectedChatItems, + chatInfo = chatInfo, + deleteItems = { _ -> + val itemIds = selectedChatItems.value + val questionText = generalGetString(MR.strings.delete_messages_cannot_be_undone_warning) + if (itemIds != null) { + deleteMessagesAlertDialog(itemIds.sorted(), questionText = questionText, forAll = false, deleteMessages = { ids, _ -> + deleteMessages(remoteHostId, chatInfo, ids, false, moderate = false) { + selectedChatItems.value = null + } + }) + } + }, + archiveItems = { archiveItems(remoteHostId, chatInfo, selectedChatItems) }, + moderateItems = {}, + forwardItems = {} + ) + } + } + if (oneHandUI.value) { + // That's placeholder to take some space for bottom app bar in oneHandUI + Box(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier)) + } + } + } else { + Box( + Modifier + .layoutId(CHAT_COMPOSE_LAYOUT_ID) + .align(Alignment.BottomCenter) + .imePadding() + .navigationBarsPadding() + .then(if (oneHandUI.value && chatBottomBar.value) Modifier.padding(bottom = AppBarHeight * fontSizeSqrtMultiplier) else Modifier) + ) { + composeView(composeViewFocusRequester) + } + } } - - Scaffold( - topBar = { ChatInfoToolbar(chat, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged) }, - bottomBar = composeView, - modifier = Modifier.navigationBarsWithImePadding(), - floatingActionButton = { floatingButton.value() }, - ) { contentPadding -> - BoxWithConstraints(Modifier - .fillMaxHeight() - .padding(contentPadding) - ) { - ChatItemsList( - chat, unreadCount, composeState, chatItems, searchValue, - useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages, - receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, - updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, - setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, - ) + val reportsCount = reportsCount(chatInfo?.id) + if (oneHandUI.value && chatBottomBar.value) { + if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canModerate && chatsCtx.contentTag == null && reportsCount > 0) { + ReportedCountToolbar(reportsCount, withStatusBar = true, showGroupReports) + } else { + StatusBarBackground() + } + } else { + NavigationBarBackground(true, oneHandUI.value, noAlpha = true) + } + if (chatsCtx.contentTag == MsgContentTag.Report) { + if (oneHandUI.value) { + StatusBarBackground() + } + Column(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) { + Box { + if (selectedChatItems.value == null) { + GroupReportsAppBar(chatsCtx, { ModalManager.end.closeModal() }, onSearchValueChanged) + } else { + SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value) + } + } + } + } else { + Column(if (oneHandUI.value && chatBottomBar.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) { + Box { + if (selectedChatItems.value == null) { + if (chatInfo != null) { + ChatInfoToolbar(chatsCtx, chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) + } + } else { + SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value) + } + } + if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canModerate && chatsCtx.contentTag == null && reportsCount > 0 && (!oneHandUI.value || !chatBottomBar.value)) { + ReportedCountToolbar(reportsCount, withStatusBar = false, showGroupReports) + } } } } @@ -561,71 +876,76 @@ fun ChatLayout( } @Composable -fun ChatInfoToolbar( - chat: Chat, +fun BoxScope.ChatInfoToolbar( + chatsCtx: ChatModel.ChatsContext, + chatInfo: ChatInfo, back: () -> Unit, info: () -> Unit, startCall: (CallMediaType) -> Unit, endCall: () -> Unit, addMembers: (GroupInfo) -> Unit, openGroupLink: (GroupInfo) -> Unit, - changeNtfsState: (Boolean, currentValue: MutableState<Boolean>) -> Unit, + changeNtfsState: (MsgFilter, currentValue: MutableState<MsgFilter>) -> Unit, onSearchValueChanged: (String) -> Unit, + showSearch: MutableState<Boolean> ) { val scope = rememberCoroutineScope() val showMenu = rememberSaveable { mutableStateOf(false) } - var showSearch by rememberSaveable { mutableStateOf(false) } + val onBackClicked = { - if (!showSearch) { + if (!showSearch.value) { back() } else { onSearchValueChanged("") - showSearch = false + showSearch.value = false } } - if (appPlatform.isAndroid) { + if (appPlatform.isAndroid && chatsCtx.contentTag == null) { BackHandler(onBack = onBackClicked) } val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val menuItems = arrayListOf<@Composable () -> Unit>() val activeCall by remember { chatModel.activeCall } - menuItems.add { - ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = { - showMenu.value = false - showSearch = true - }) + if (chatInfo is ChatInfo.Local) { + barButtons.add { + IconButton( + { + showMenu.value = false + showSearch.value = true + }, enabled = chatInfo.noteFolder.ready + ) { + Icon( + painterResource(MR.images.ic_search), + stringResource(MR.strings.search_verb).capitalize(Locale.current), + tint = if (chatInfo.noteFolder.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + ) + } + } + } else { + menuItems.add { + ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = { + showMenu.value = false + showSearch.value = true + }) + } } - if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.allowsFeature(ChatFeature.Calls)) { + if (chatInfo is ChatInfo.Direct && chatInfo.contact.mergedPreferences.calls.enabled.forUser) { if (activeCall == null) { barButtons.add { - if (appPlatform.isAndroid) { - IconButton({ - showMenu.value = false - startCall(CallMediaType.Audio) - }, enabled = chat.chatInfo.contact.ready && chat.chatInfo.contact.active - ) { - Icon( - painterResource(MR.images.ic_call_500), - stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current), - tint = if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary - ) - } - } else { - IconButton({ - showMenu.value = false - startCall(CallMediaType.Video) - }, enabled = chat.chatInfo.contact.ready && chat.chatInfo.contact.active - ) { - Icon( - painterResource(MR.images.ic_videocam), - stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), - tint = if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary - ) - } + IconButton({ + showMenu.value = false + startCall(CallMediaType.Audio) + }, enabled = chatInfo.contact.ready && chatInfo.contact.active + ) { + Icon( + painterResource(MR.images.ic_call_500), + stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current), + tint = if (chatInfo.contact.ready && chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + ) } } - } else if (activeCall?.contact?.id == chat.id) { + } else if (activeCall?.contact?.id == chatInfo.id && appPlatform.isDesktop) { barButtons.add { val call = remember { chatModel.activeCall }.value val connectedAt = call?.connectedAt @@ -654,27 +974,20 @@ fun ChatInfoToolbar( } } } - if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active && activeCall == null) { + if (chatInfo.contact.ready && chatInfo.contact.active && activeCall == null) { menuItems.add { - if (appPlatform.isAndroid) { - ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = { - showMenu.value = false - startCall(CallMediaType.Video) - }) - } else { - ItemAction(stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current), painterResource(MR.images.ic_call_500), onClick = { - showMenu.value = false - startCall(CallMediaType.Audio) - }) - } + ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = { + showMenu.value = false + startCall(CallMediaType.Video) + }) } } - } else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers) { - if (!chat.chatInfo.incognito) { + } else if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canAddMembers) { + if (!chatInfo.incognito) { barButtons.add { IconButton({ showMenu.value = false - addMembers(chat.chatInfo.groupInfo) + addMembers(chatInfo.groupInfo) }) { Icon(painterResource(MR.images.ic_person_add_500), stringResource(MR.strings.icon_descr_add_members), tint = MaterialTheme.colors.primary) } @@ -683,65 +996,83 @@ fun ChatInfoToolbar( barButtons.add { IconButton({ showMenu.value = false - openGroupLink(chat.chatInfo.groupInfo) + openGroupLink(chatInfo.groupInfo) }) { Icon(painterResource(MR.images.ic_add_link), stringResource(MR.strings.group_link), tint = MaterialTheme.colors.primary) } } } } - if ((chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.ready && chat.chatInfo.contact.active) || chat.chatInfo is ChatInfo.Group) { - val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) } + + val enableNtfs = chatInfo.chatSettings?.enableNtfs + if (((chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) || chatInfo is ChatInfo.Group) && enableNtfs != null) { + val ntfMode = remember { mutableStateOf(enableNtfs) } + val nextNtfMode by remember { derivedStateOf { ntfMode.value.nextMode(chatInfo.hasMentions) } } menuItems.add { ItemAction( - if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat), - if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications), + stringResource(nextNtfMode.text(chatInfo.hasMentions)), + painterResource(nextNtfMode.icon), onClick = { showMenu.value = false // Just to make a delay before changing state of ntfsEnabled, otherwise it will redraw menu item with new value before closing the menu scope.launch { delay(200) - changeNtfsState(!ntfsEnabled.value, ntfsEnabled) + changeNtfsState(nextNtfMode, ntfMode) } } ) } } - barButtons.add { - IconButton({ showMenu.value = true }) { - Icon(MoreVertFilled, stringResource(MR.strings.icon_descr_more_button), tint = MaterialTheme.colors.primary) + if (menuItems.isNotEmpty()) { + barButtons.add { + IconButton({ showMenu.value = true }) { + Icon(MoreVertFilled, stringResource(MR.strings.icon_descr_more_button), tint = MaterialTheme.colors.primary) + } } } - - DefaultTopAppBar( - navigationButton = { if (appPlatform.isAndroid || showSearch) { NavigationButtonBack(onBackClicked) } }, - title = { ChatInfoToolbarTitle(chat.chatInfo) }, - onTitleClick = info, - showSearch = showSearch, + val oneHandUI = remember { appPrefs.oneHandUI.state } + val chatBottomBar = remember { appPrefs.chatBottomBar.state } + DefaultAppBar( + navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } }, + title = { ChatInfoToolbarTitle(chatInfo) }, + onTitleClick = if (chatInfo is ChatInfo.Local) null else info, + showSearch = showSearch.value, + onTop = !oneHandUI.value || !chatBottomBar.value, onSearchValueChanged = onSearchValueChanged, - buttons = barButtons + buttons = { barButtons.forEach { it() } } ) - - Divider(Modifier.padding(top = AppBarHeight)) - - Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight)) { - DefaultDropdownMenu(showMenu) { - menuItems.forEach { it() } + Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) { + val density = LocalDensity.current + val width = remember { mutableStateOf(250.dp) } + val height = remember { mutableStateOf(0.dp) } + DefaultDropdownMenu( + showMenu, + modifier = Modifier.onSizeChanged { with(density) { + width.value = it.width.toDp().coerceAtLeast(250.dp) + if (oneHandUI.value && chatBottomBar.value && (appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 30)) height.value = it.height.toDp() + } }, + offset = DpOffset(-width.value, if (oneHandUI.value && chatBottomBar.value) -height.value else AppBarHeight) + ) { + if (oneHandUI.value && chatBottomBar.value) { + menuItems.asReversed().forEach { it() } + } else { + menuItems.forEach { it() } + } } } } @Composable -fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant) { +fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f)) { Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { if (cInfo.incognito) { - IncognitoImage(size = 36.dp, Indigo) + IncognitoImage(size = 36.dp * fontSizeSqrtMultiplier, Indigo) } - ChatInfoImage(cInfo, size = imageSize, iconColor) + ChatInfoImage(cInfo, size = imageSize * fontSizeSqrtMultiplier, iconColor) Column( Modifier.padding(start = 8.dp), horizontalAlignment = Alignment.CenterHorizontally @@ -766,41 +1097,72 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo } @Composable -private fun ContactVerifiedShield() { - Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(18.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary) -} - -data class CIListState(val scrolled: Boolean, val itemCount: Int, val keyboardState: KeyboardState) - -val CIListStateSaver = run { - val scrolledKey = "scrolled" - val countKey = "itemCount" - val keyboardKey = "keyboardState" - mapSaver( - save = { mapOf(scrolledKey to it.scrolled, countKey to it.itemCount, keyboardKey to it.keyboardState) }, - restore = { CIListState(it[scrolledKey] as Boolean, it[countKey] as Int, it[keyboardKey] as KeyboardState) } - ) +private fun ReportedCountToolbar( + reportsCount: Int, + withStatusBar: Boolean, + showGroupReports: () -> Unit +) { + Box { + val statusBarPadding = if (withStatusBar) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else 0.dp + Row( + Modifier + .fillMaxWidth() + .height(AppBarHeight * fontSizeSqrtMultiplier + statusBarPadding) + .background(MaterialTheme.colors.background) + .clickable(onClick = showGroupReports) + .padding(top = statusBarPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = MaterialTheme.colors.error) + Spacer(Modifier.width(4.dp)) + Text( + if (reportsCount == 1) { + stringResource(MR.strings.group_reports_active_one) + } else { + stringResource(MR.strings.group_reports_active).format(reportsCount) + }, + style = MaterialTheme.typography.button + ) + } + Divider(Modifier.align(Alignment.BottomStart)) + } } @Composable -fun BoxWithConstraintsScope.ChatItemsList( - chat: Chat, +private fun ContactVerifiedShield() { + Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(18.dp * fontSizeSqrtMultiplier).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary) +} + +/** Saves current scroll position when group reports are open and user opens [ChatItemInfoView], for example, and goes back */ +private var reportsListState: LazyListState? = null + +@Composable +fun BoxScope.ChatItemsList( + chatsCtx: ChatModel.ChatsContext, + remoteHostId: Long?, + chatInfo: ChatInfo, unreadCount: State<Int>, composeState: MutableState<ComposeState>, - chatItems: List<ChatItem>, + composeViewHeight: State<Dp>, searchValue: State<String>, useLinkPreviews: Boolean, linkMode: SimplexLinkMode, + scrollToItemId: MutableState<Long?>, + selectedChatItems: MutableState<Set<Long>?>, showMemberInfo: (GroupInfo, GroupMember) -> Unit, - loadPrevMessages: () -> Unit, + showChatInfo: () -> Unit, + loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List<Long>) -> Unit, - receiveFile: (Long, Boolean) -> Unit, + archiveReports: (List<Long>, Boolean) -> Unit, + receiveFile: (Long) -> Unit, cancelFile: (Long) -> Unit, joinGroup: (Long, () -> Unit) -> Unit, acceptCall: (Contact) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, openDirectChat: (Long) -> Unit, + forwardItem: (ChatInfo, ChatItem) -> Unit, updateContactStats: (Contact) -> Unit, updateMemberStats: (GroupInfo, GroupMember) -> Unit, syncContactConnection: (Contact) -> Unit, @@ -809,337 +1171,699 @@ fun BoxWithConstraintsScope.ChatItemsList( findModelMember: (String) -> GroupMember?, setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, showItemDetails: (ChatInfo, ChatItem) -> Unit, - markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, - setFloatingButton: (@Composable () -> Unit) -> Unit, + markItemsRead: (List<Long>) -> Unit, + markChatRead: () -> Unit, + closeSearch: () -> Unit, onComposed: suspend (chatId: String) -> Unit, developerTools: Boolean, + showViaProxy: Boolean ) { - val listState = rememberLazyListState() - val scope = rememberCoroutineScope() - ScrollToBottom(chat.id, listState, chatItems) - var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) } - // Scroll to bottom when search value changes from something to nothing and back - LaunchedEffect(searchValue.value.isEmpty()) { - // They are equal when orientation was changed, don't need to scroll. - // LaunchedEffect unaware of this event since it uses remember, not rememberSaveable - if (prevSearchEmptiness == searchValue.value.isEmpty()) return@LaunchedEffect - prevSearchEmptiness = searchValue.value.isEmpty() - - if (listState.firstVisibleItemIndex != 0) { - scope.launch { listState.scrollToItem(0) } + val loadingTopItems = remember { mutableStateOf(false) } + val loadingBottomItems = remember { mutableStateOf(false) } + // just for changing local var here based on request + val loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit = { chatId, pagination, visibleItemIndexesNonReversed -> + val loadingSide = when (pagination) { + is ChatPagination.Before -> loadingTopItems + is ChatPagination.Last -> loadingBottomItems + is ChatPagination.After, is ChatPagination.Around, is ChatPagination.Initial -> null + } + loadingSide?.value = true + try { + loadMessages(chatId, pagination, visibleItemIndexesNonReversed) + } finally { + loadingSide?.value = false } } - - PreloadItems(listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages) - - Spacer(Modifier.size(8.dp)) - val reversedChatItems by remember { derivedStateOf { chatItems.reversed().toList() } } - val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() } - val scrollToItem: (Long) -> Unit = { itemId: Long -> - val index = reversedChatItems.indexOfFirst { it.id == itemId } - if (index != -1) { - scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.lastIndex, index + 1), -maxHeightRounded) } - } - } - LaunchedEffect(chat.id) { - var stopListening = false - snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastIndex } - .distinctUntilChanged() - .filter { !stopListening } - .collect { - onComposed(chat.id) - stopListening = true + val searchValueIsEmpty = remember { derivedStateOf { searchValue.value.isEmpty() } } + val searchValueIsNotBlank = remember { derivedStateOf { searchValue.value.isNotBlank() } } + val revealedItems = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(setOf<Long>()) } + // not using reversedChatItems inside to prevent possible derivedState bug in Compose when one derived state access can cause crash asking another derived state + if (chatsCtx != null) { + val mergedItems = remember { + derivedStateOf { + MergedItems.create(chatsCtx.chatItems.value.asReversed(), unreadCount, revealedItems.value, chatsCtx.chatState) } - } - DisposableEffectOnGone( - whenGone = { - VideoPlayerHolder.releaseAll() } - ) - LazyColumn(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) { - itemsIndexed(reversedChatItems, key = { _, item -> item.id }) { i, cItem -> + val reversedChatItems = remember { derivedStateOf { chatsCtx.chatItems.value.asReversed() } } + val reportsCount = reportsCount(chatInfo.id) + val topPaddingToContent = topPaddingToContent( + chatView = chatsCtx.contentTag == null, + additionalTopBar = chatsCtx.contentTag == null && reportsCount > 0 + ) + val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent.roundToPx() }) + val numberOfBottomAppBars = numberOfBottomAppBars() + + /** determines height based on window info and static height of two AppBars. It's needed because in the first graphic frame height of + * [composeViewHeight] is unknown, but we need to set scroll position for unread messages already so it will be correct before the first frame appears + * */ + val maxHeightForList = rememberUpdatedState( + with(LocalDensity.current) { LocalWindowHeight().roundToPx() - topPaddingToContentPx.value - (AppBarHeight * fontSizeSqrtMultiplier * numberOfBottomAppBars).roundToPx() } + ) + val resetListState = remember { mutableStateOf(false) } + remember(chatModel.openAroundItemId.value) { + if (chatModel.openAroundItemId.value != null) { + closeSearch() + resetListState.value = !resetListState.value + } + } + val highlightedItems = remember { mutableStateOf(setOf<Long>()) } + val hoveredItemId = remember { mutableStateOf(null as Long?) } + val listState = rememberUpdatedState(rememberSaveable(chatInfo.id, searchValueIsEmpty.value, resetListState.value, saver = LazyListState.Saver) { + val openAroundItemId = chatModel.openAroundItemId.value + val index = mergedItems.value.indexInParentItems[openAroundItemId] ?: mergedItems.value.items.indexOfLast { it.hasUnread() } + val reportsState = reportsListState + if (openAroundItemId != null) { + highlightedItems.value += openAroundItemId + chatModel.openAroundItemId.value = null + } + hoveredItemId.value = null + if (reportsState != null) { + reportsListState = null + reportsState + } else if (index <= 0 || !searchValueIsEmpty.value) { + LazyListState(0, 0) + } else { + LazyListState(index + 1, -maxHeightForList.value) + } + }) + SaveReportsStateOnDispose(chatsCtx, listState) + val maxHeight = remember { derivedStateOf { listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } } + val loadingMoreItems = remember { mutableStateOf(false) } + val animatedScrollingInProgress = remember { mutableStateOf(false) } + val ignoreLoadingRequests = remember(remoteHostId) { mutableSetOf<Long>() } + LaunchedEffect(chatInfo.id, searchValueIsEmpty.value) { + if (searchValueIsEmpty.value && reversedChatItems.value.size < ChatPagination.INITIAL_COUNT) + ignoreLoadingRequests.add(reversedChatItems.value.lastOrNull()?.id ?: return@LaunchedEffect) + } + PreloadItems(chatsCtx, chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), loadingMoreItems, resetListState, mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination -> + if (loadingMoreItems.value || chatId != chatModel.chatId.value) return@PreloadItems false + loadingMoreItems.value = true + withContext(NonCancellable) { + try { + loadMessages(chatId, pagination) { + visibleItemIndexesNonReversed(mergedItems, reversedChatItems.value.size, listState.value) + } + } finally { + loadingMoreItems.value = false + } + } + true + } + val remoteHostIdUpdated = rememberUpdatedState(remoteHostId) + val chatInfoUpdated = rememberUpdatedState(chatInfo) + val scope = rememberCoroutineScope() + val scrollToItem: (Long) -> Unit = remember { + // In group reports just set the itemId to scroll to so the main ChatView will handle scrolling + if (chatsCtx.contentTag == MsgContentTag.Report) return@remember { scrollToItemId.value = it } + scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) + } + val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(chatsCtx, remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem) } + if (chatsCtx.contentTag == null) { + LaunchedEffect(Unit) { + snapshotFlow { scrollToItemId.value }.filterNotNull().collect { + if (appPlatform.isAndroid) { + ModalManager.end.closeModals() + } + scrollToItem(it) + scrollToItemId.value = null + } + } + } + SmallScrollOnNewMessage(listState, reversedChatItems) + val finishedInitialComposition = remember { mutableStateOf(false) } + NotifyChatListOnFinishingComposition(finishedInitialComposition, chatInfo, revealedItems, listState, onComposed) + + DisposableEffectOnGone( + whenGone = { + VideoPlayerHolder.releaseAll() + } + ) + + @Composable + fun ChatViewListItem( + itemAtZeroIndexInWholeList: Boolean, + range: State<IntRange?>, + showAvatar: Boolean, + cItem: ChatItem, + itemSeparation: ItemSeparation, + previousItemSeparationLargeGap: Boolean, + revealed: State<Boolean>, + reveal: (Boolean) -> Unit + ) { + val itemScope = rememberCoroutineScope() CompositionLocalProvider( // Makes horizontal and vertical scrolling to coexist nicely. // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop() ) { - val dismissState = rememberDismissState(initialValue = DismissValue.Default) { false } - val directions = setOf(DismissDirection.EndToStart) - val swipeableModifier = SwipeToDismissModifier( - state = dismissState, - directions = directions, - swipeDistance = with(LocalDensity.current) { 30.dp.toPx() }, - ) - val swipedToEnd = (dismissState.overflow.value > 0f && directions.contains(DismissDirection.StartToEnd)) - val swipedToStart = (dismissState.overflow.value < 0f && directions.contains(DismissDirection.EndToStart)) - if (dismissState.isAnimationRunning && (swipedToStart || swipedToEnd)) { - LaunchedEffect(Unit) { - scope.launch { - if (cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) { - if (composeState.value.editing) { - composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) - } else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { - composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem)) - } - } - } - } - } val provider = { - providerForGallery(i, chatItems, cItem.id) { indexInReversed -> - scope.launch { - listState.scrollToItem( - kotlin.math.min(reversedChatItems.lastIndex, indexInReversed + 1), - -maxHeightRounded + providerForGallery(reversedChatItems.value.asReversed(), cItem.id) { indexInReversed -> + itemScope.launch { + listState.value.scrollToItem( + min(reversedChatItems.value.lastIndex, indexInReversed + 1), + -maxHeight.value ) } } } - val revealed = remember { mutableStateOf(false) } - @Composable - fun ChatItemViewShortHand(cItem: ChatItem, range: IntRange?) { + fun ChatItemViewShortHand(cItem: ChatItem, itemSeparation: ItemSeparation, range: State<IntRange?>, fillMaxWidth: Boolean = true) { tryOrShowError("${cItem.id}ChatItem", error = { CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) }) { - ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools) + val highlighted = remember { derivedStateOf { highlightedItems.value.contains(cItem.id) } } + LaunchedEffect(Unit) { + snapshotFlow { highlighted.value } + .distinctUntilChanged() + .filter { it } + .collect { + delay(500) + highlightedItems.value = setOf() + } + } + ChatItemView(chatsCtx, remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, hoveredItemId = hoveredItemId, range = range, searchIsNotBlank = searchValueIsNotBlank, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, archiveReports = archiveReports, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) } } @Composable - fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?) { - val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null - if (chat.chatInfo is ChatInfo.Group) { - if (cItem.chatDir is CIDirection.GroupRcv) { - val member = cItem.chatDir.groupMember - val (prevMember, memCount) = - if (range != null) { - chatModel.getPrevHiddenMember(member, range) - } else { - null to 1 - } - if (prevItem == null || showMemberImage(member, prevItem) || prevMember != null) { - Column( - Modifier - .padding(top = 8.dp) - .padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), - horizontalAlignment = Alignment.Start - ) { - if (cItem.content.showMemberName) { - Text( - memberNames(member, prevMember, memCount), - Modifier.padding(start = MEMBER_IMAGE_SIZE + 10.dp), - style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary) - ) + fun ChatItemView(cItem: ChatItem, range: State<IntRange?>, itemSeparation: ItemSeparation, previousItemSeparationLargeGap: Boolean) { + val dismissState = rememberDismissState(initialValue = DismissValue.Default) { + if (it == DismissValue.DismissedToStart) { + itemScope.launch { + if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local && !cItem.isReport) { + if (composeState.value.editing) { + composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) + } else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { + composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem)) } - Row( - swipeableModifier, - horizontalArrangement = Arrangement.spacedBy(4.dp) + } + } + } + false + } + val swipeableModifier = SwipeToDismissModifier( + state = dismissState, + directions = setOf(DismissDirection.EndToStart), + swipeDistance = with(LocalDensity.current) { 30.dp.toPx() }, + ) + val sent = cItem.chatDir.sent + + @Composable + fun ChatItemBox(modifier: Modifier = Modifier, content: @Composable () -> Unit = { }) { + Box( + modifier = modifier.padding( + bottom = if (itemSeparation.largeGap) { + if (itemAtZeroIndexInWholeList) { + 8.dp + } else { + 4.dp + } + } else 1.dp, top = if (previousItemSeparationLargeGap) 4.dp else 1.dp + ), + contentAlignment = Alignment.CenterStart + ) { + content() + } + } + + @Composable + fun adjustTailPaddingOffset(originalPadding: Dp, start: Boolean): Dp { + val chatItemTail = remember { appPreferences.chatItemTail.state } + val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true) + val tailRendered = style is ShapeStyle.Bubble && style.tailVisible + + return originalPadding + (if (tailRendered) 0.dp else if (start) msgTailWidthDp * 2 else msgTailWidthDp) + } + + Box { + val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null + val selectionVisible = selectedChatItems.value != null && cItem.canBeDeletedForSelf + val selectionOffset by animateDpAsState(if (selectionVisible && !sent) 4.dp + 22.dp * fontSizeMultiplier else 0.dp) + val swipeableOrSelectionModifier = (if (selectionVisible) Modifier else swipeableModifier).graphicsLayer { translationX = selectionOffset.toPx() } + if (chatInfo is ChatInfo.Group) { + if (cItem.chatDir is CIDirection.GroupRcv) { + if (showAvatar) { + Column( + Modifier + .padding(top = 8.dp) + .padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) + .fillMaxWidth() + .then(swipeableModifier), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.Start ) { - Box( - Modifier - .clip(CircleShape) - .clickable { - showMemberInfo(chat.chatInfo.groupInfo, member) + @Composable + fun MemberNameAndRole(range: State<IntRange?>) { + Row(Modifier.padding(bottom = 2.dp).graphicsLayer { translationX = selectionOffset.toPx() }, horizontalArrangement = Arrangement.SpaceBetween) { + val member = cItem.chatDir.groupMember + val rangeValue = range.value + val (prevMember, memCount) = + if (rangeValue != null) { + chatModel.getPrevHiddenMember(member, rangeValue, reversedChatItems.value) + } else { + null to 1 + } + Text( + memberNames(member, prevMember, memCount), + Modifier + .padding(start = (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + DEFAULT_PADDING_HALF) + .weight(1f, false), + fontSize = 13.5.sp, + color = MaterialTheme.colors.secondary, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + if (memCount == 1 && member.memberRole > GroupMemberRole.Member) { + val chatItemTail = remember { appPreferences.chatItemTail.state } + val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true) + val tailRendered = style is ShapeStyle.Bubble && style.tailVisible + + Text( + member.memberRole.text, + Modifier.padding(start = DEFAULT_PADDING_HALF * 1.5f, end = DEFAULT_PADDING_HALF + if (tailRendered) msgTailWidthDp else 0.dp), + fontSize = 13.5.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colors.secondary, + maxLines = 1 + ) } - ) { - MemberImage(member) + } + } + + @Composable + fun Item() { + ChatItemBox(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID)) { + androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) { + SelectedListItem(Modifier, cItem.id, selectedChatItems) + } + Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() }) { + val member = cItem.chatDir.groupMember + Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) { + MemberImage(member) + } + Box(modifier = Modifier.padding(top = 2.dp, start = 4.dp).chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)) { + ChatItemViewShortHand(cItem, itemSeparation, range, false) + } + } + } + } + if (cItem.content.showMemberName) { + DependentLayout(Modifier, CHAT_BUBBLE_LAYOUT_ID) { + MemberNameAndRole(range) + Item() + } + } else { + Item() + } + } + } else { + ChatItemBox { + AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) { + SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems) + } + Row( + Modifier + .padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) + .chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value) + .then(swipeableOrSelectionModifier) + ) { + ChatItemViewShortHand(cItem, itemSeparation, range) } - ChatItemViewShortHand(cItem, range) } } } else { - Row( - Modifier - .padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp) - .then(swipeableModifier) - ) { - ChatItemViewShortHand(cItem, range) + ChatItemBox { + AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) { + SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems) + } + Box( + Modifier + .padding(start = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(104.dp, start = true), end = 12.dp) + .chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value) + .then(if (selectionVisible) Modifier else swipeableModifier) + ) { + ChatItemViewShortHand(cItem, itemSeparation, range) + } } } - } else { - Box( - Modifier - .padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp) - .then(swipeableModifier) - ) { - ChatItemViewShortHand(cItem, range) + } else { // direct message + ChatItemBox { + AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) { + SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems) + } + + Box( + Modifier.padding( + start = if (sent && !voiceWithTransparentBack) adjustTailPaddingOffset(76.dp, start = true) else 12.dp, + end = if (sent || voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(76.dp, start = false), + ) + .chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value) + .then(if (!selectionVisible || !sent) swipeableOrSelectionModifier else Modifier) + ) { + ChatItemViewShortHand(cItem, itemSeparation, range) + } } } - } else { // direct message - val sent = cItem.chatDir.sent - Box( - Modifier.padding( - start = if (sent && !voiceWithTransparentBack) 76.dp else 12.dp, - end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp, - ).then(swipeableModifier) - ) { - ChatItemViewShortHand(cItem, range) + if (selectionVisible) { + Box(Modifier.matchParentSize().clickable { + val checked = selectedChatItems.value?.contains(cItem.id) == true + selectUnselectChatItem(select = !checked, cItem, revealed, selectedChatItems, reversedChatItems) + }) } } } - - val (currIndex, nextItem) = chatModel.getNextChatItem(cItem) - val ciCategory = cItem.mergeCategory - if (ciCategory != null && ciCategory == nextItem?.mergeCategory) { - // memberConnected events and deleted items are aggregated at the last chat item in a row, see ChatItemView + if (itemSeparation.date != null) { + DateSeparator(itemSeparation.date) + } + ChatItemView(cItem, range, itemSeparation, previousItemSeparationLargeGap) + } + } + LazyColumnWithScrollBar( + Modifier.align(Alignment.BottomCenter), + state = listState.value, + contentPadding = PaddingValues( + top = topPaddingToContent, + bottom = composeViewHeight.value + ), + reverseLayout = true, + additionalBarOffset = composeViewHeight, + additionalTopBar = rememberUpdatedState(chatsCtx.contentTag == null && reportsCount > 0), + chatBottomBar = remember { appPrefs.chatBottomBar.state } + ) { + val mergedItemsValue = mergedItems.value + itemsIndexed(mergedItemsValue.items, key = { _, merged -> keyForItem(merged.newest().item) }) { index, merged -> + val isLastItem = index == mergedItemsValue.items.lastIndex + val last = if (isLastItem) reversedChatItems.value.lastOrNull() else null + val listItem = merged.newest() + val item = listItem.item + val range = if (merged is MergedItem.Grouped) { + merged.rangeInReversed.value } else { - val (prevHidden, prevItem) = chatModel.getPrevShownChatItem(currIndex, ciCategory) - val range = chatViewItemsRange(currIndex, prevHidden) - if (revealed.value && range != null) { - reversedChatItems.subList(range.first, range.last + 1).forEachIndexed { index, ci -> - val prev = if (index + range.first == prevHidden) prevItem else reversedChatItems[index + range.first + 1] - ChatItemView(ci, null, prev) - } - } else { - ChatItemView(cItem, range, prevItem) - } + null + } + val showAvatar = shouldShowAvatar(item, listItem.nextItem) + val isRevealed = remember { derivedStateOf { revealedItems.value.contains(item.id) } } + val itemSeparation: ItemSeparation + val prevItemSeparationLargeGap: Boolean + if (merged is MergedItem.Single || isRevealed.value) { + val prev = listItem.prevItem + itemSeparation = getItemSeparation(item, prev) + val nextForGap = if ((item.mergeCategory != null && item.mergeCategory == prev?.mergeCategory) || isLastItem) null else listItem.nextItem + prevItemSeparationLargeGap = if (nextForGap == null) false else getItemSeparationLargeGap(nextForGap, item) + } else { + itemSeparation = getItemSeparation(item, null) + prevItemSeparationLargeGap = false + } + ChatViewListItem(index == 0, rememberUpdatedState(range), showAvatar, item, itemSeparation, prevItemSeparationLargeGap, isRevealed) { + if (merged is MergedItem.Grouped) merged.reveal(it, revealedItems) } - if (cItem.isRcvNew && chat.id == ChatModel.chatId.value) { - LaunchedEffect(cItem.id) { - scope.launch { - delay(600) - markRead(CC.ItemRange(cItem.id, cItem.id), null) - } + if (last != null) { + // no using separate item(){} block in order to have total number of items in LazyColumn match number of merged items + DateSeparator(last.meta.itemTs) + } + if (item.isRcvNew) { + val itemIds = when (merged) { + is MergedItem.Single -> listOf(merged.item.item.id) + is MergedItem.Grouped -> merged.items.map { it.item.id } } + MarkItemsReadAfterDelay(keyForItem(item), itemIds, finishedInitialComposition, chatInfo.id, listState, markItemsRead) } } } + FloatingButtons( + chatsCtx, + reversedChatItems, + chatInfoUpdated, + topPaddingToContent, + topPaddingToContentPx, + loadingMoreItems, + loadingTopItems, + loadingBottomItems, + animatedScrollingInProgress, + mergedItems, + unreadCount, + maxHeight, + composeViewHeight, + searchValue, + markChatRead, + listState, + loadMessages + ) + FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent).align(Alignment.TopCenter), topPaddingToContentPx, mergedItems, listState) + + LaunchedEffect(Unit) { + snapshotFlow { listState.value.isScrollInProgress } + .collect { + chatViewScrollState.value = it + } + } + LaunchedEffect(Unit) { + snapshotFlow { listState.value.isScrollInProgress } + .filter { !it } + .collect { + if (animatedScrollingInProgress.value) { + animatedScrollingInProgress.value = false + } + } + } } - FloatingButtons(chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState) } +private suspend fun loadLastItems(chatsCtx: ChatModel.ChatsContext, chatId: State<ChatId>, listState: State<LazyListState>, loadItems: State<suspend (ChatId, ChatPagination) -> Boolean>) { + val lastVisible = listState.value.layoutInfo.visibleItemsInfo.lastOrNull() + val itemsCanCoverScreen = lastVisible != null && listState.value.layoutInfo.viewportEndOffset - listState.value.layoutInfo.afterContentPadding <= lastVisible.offset + lastVisible.size + if (!itemsCanCoverScreen) return + + if (lastItemsLoaded(chatsCtx)) return + + delay(500) + loadItems.value(chatId.value, ChatPagination.Last(ChatPagination.INITIAL_COUNT)) +} + +private fun lastItemsLoaded(chatsCtx: ChatModel.ChatsContext): Boolean { + val chatState = chatsCtx.chatState + return chatState.splits.value.isEmpty() || chatState.splits.value.firstOrNull() != chatsCtx.chatItems.value.lastOrNull()?.id +} + +// TODO: in extra rare case when after loading last items only 1 item is loaded, the view will jump like when receiving new message +// can be reproduced by forwarding a message to notes that is (ChatPagination.INITIAL_COUNT - 1) away from bottom and going to that message @Composable -private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: List<ChatItem>) { - val scope = rememberCoroutineScope() - // Helps to scroll to bottom after moving from Group to Direct chat - // and prevents scrolling to bottom on orientation change - var shouldAutoScroll by rememberSaveable { mutableStateOf(true to chatId) } - LaunchedEffect(chatId, shouldAutoScroll) { - if ((shouldAutoScroll.first || shouldAutoScroll.second != chatId) && listState.firstVisibleItemIndex != 0) { - scope.launch { listState.scrollToItem(0) } - } - // Don't autoscroll next time until it will be needed - shouldAutoScroll = false to chatId - } +private fun SmallScrollOnNewMessage(listState: State<LazyListState>, reversedChatItems: State<List<ChatItem>>) { val scrollDistance = with(LocalDensity.current) { -39.dp.toPx() } - /* - * Since we use key with each item in LazyColumn, LazyColumn will not autoscroll to bottom item. We need to do it ourselves. - * When the first visible item (from bottom) is visible (even partially) we can autoscroll to 0 item. Or just scrollBy small distance otherwise - * */ LaunchedEffect(Unit) { - snapshotFlow { chatItems.lastOrNull()?.id } + var prevTotalItems = listState.value.layoutInfo.totalItemsCount + var newestItemId = reversedChatItems.value.firstOrNull()?.id + snapshotFlow { listState.value.layoutInfo.totalItemsCount } .distinctUntilChanged() - .filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it } + .drop(1) .collect { + val diff = listState.value.layoutInfo.totalItemsCount - prevTotalItems + val sameNewestItem = newestItemId == reversedChatItems.value.firstOrNull()?.id + prevTotalItems = listState.value.layoutInfo.totalItemsCount + newestItemId = reversedChatItems.value.firstOrNull()?.id + if (diff < 1 || diff > 2 || sameNewestItem) { + return@collect + } try { - if (listState.firstVisibleItemIndex == 0 || (listState.firstVisibleItemIndex == 1 && listState.layoutInfo.totalItemsCount == chatItems.size)) { - listState.animateScrollToItem(0) + if (listState.value.firstVisibleItemIndex == 0 || listState.value.firstVisibleItemIndex == 1) { + if (appPlatform.isAndroid) listState.value.animateScrollToItem(0) else listState.value.scrollToItem(0) } else { - listState.animateScrollBy(scrollDistance) + if (appPlatform.isAndroid) listState.value.animateScrollBy(scrollDistance) else listState.value.scrollBy(scrollDistance) } } catch (e: CancellationException) { /** - * When you tap and hold a finger on a lazy column with chatItems, and then you receive a message, + * When you tap and hold a finger on a lazy column with reversedChatItems, and then you receive a message, * this coroutine will be canceled with the message "Current mutation had a higher priority" because of animatedScroll. * Which breaks auto-scrolling to bottom. So just ignoring the exception * */ + } catch (e: IllegalArgumentException) { + Log.e(TAG, "Failed to scroll: ${e.stackTraceToString()}") } } } } @Composable -fun BoxWithConstraintsScope.FloatingButtons( - chatItems: List<ChatItem>, +private fun NotifyChatListOnFinishingComposition( + finishedInitialComposition: MutableState<Boolean>, + chatInfo: ChatInfo, + revealedItems: MutableState<Set<Long>>, + listState: State<LazyListState>, + onComposed: suspend (chatId: String) -> Unit +) { + LaunchedEffect(chatInfo.id) { + revealedItems.value = emptySet() + snapshotFlow { listState.value.layoutInfo.visibleItemsInfo.lastIndex } + .distinctUntilChanged() + .collect { + onComposed(chatInfo.id) + finishedInitialComposition.value = true + cancel() + } + } +} + +@Composable +fun BoxScope.FloatingButtons( + chatsCtx: ChatModel.ChatsContext, + reversedChatItems: State<List<ChatItem>>, + chatInfo: State<ChatInfo>, + topPaddingToContent: Dp, + topPaddingToContentPx: State<Int>, + loadingMoreItems: MutableState<Boolean>, + loadingTopItems: MutableState<Boolean>, + loadingBottomItems: MutableState<Boolean>, + animatedScrollingInProgress: MutableState<Boolean>, + mergedItems: State<MergedItems>, unreadCount: State<Int>, - minUnreadItemId: Long, + maxHeight: State<Int>, + composeViewHeight: State<Dp>, searchValue: State<String>, - markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, - setFloatingButton: (@Composable () -> Unit) -> Unit, - listState: LazyListState + markChatRead: () -> Unit, + listState: State<LazyListState>, + loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit ) { val scope = rememberCoroutineScope() - var firstVisibleIndex by remember { mutableStateOf(listState.firstVisibleItemIndex) } - var lastIndexOfVisibleItems by remember { mutableStateOf(listState.layoutInfo.visibleItemsInfo.lastIndex) } - var firstItemIsVisible by remember { mutableStateOf(firstVisibleIndex == 0) } - - LaunchedEffect(listState) { - snapshotFlow { listState.firstVisibleItemIndex } - .distinctUntilChanged() - .collect { - firstVisibleIndex = it - firstItemIsVisible = firstVisibleIndex == 0 - } - } - - LaunchedEffect(listState) { - // When both snapshotFlows located in one LaunchedEffect second block will never be called because coroutine is paused on first block - // so separate them into two LaunchedEffects - snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastIndex } - .distinctUntilChanged() - .collect { - lastIndexOfVisibleItems = it - } - } - val bottomUnreadCount by remember { - derivedStateOf { - if (unreadCount.value == 0) return@derivedStateOf 0 - val from = chatItems.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems - if (chatItems.size <= from || from < 0) return@derivedStateOf 0 - - chatItems.subList(from, chatItems.size).count { it.isRcvNew } + fun scrollToBottom() { + scope.launch { + animatedScrollingInProgress.value = true + tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(0) } } } - val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt() - - LaunchedEffect(bottomUnreadCount, firstItemIsVisible) { - val showButtonWithCounter = bottomUnreadCount > 0 && !firstItemIsVisible && searchValue.value.isEmpty() - val showButtonWithArrow = !showButtonWithCounter && !firstItemIsVisible - setFloatingButton( - bottomEndFloatingButton( - bottomUnreadCount, - showButtonWithCounter, - showButtonWithArrow, - onClickArrowDown = { - scope.launch { listState.animateScrollToItem(0) } - }, - onClickCounter = { - scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount - 1), firstVisibleOffset) } + fun scrollToTopUnread() { + scope.launch { + tryBlockAndSetLoadingMore(loadingMoreItems) { + if (chatsCtx.chatState.splits.value.isNotEmpty()) { + val pagination = ChatPagination.Initial(ChatPagination.INITIAL_COUNT) + val oldSize = reversedChatItems.value.size + loadMessages(chatInfo.value.id, pagination) { + visibleItemIndexesNonReversed(mergedItems, reversedChatItems.value.size, listState.value) + } + var repeatsLeft = 100 + while (oldSize == reversedChatItems.value.size && repeatsLeft > 0) { + delay(10) + repeatsLeft-- + } + if (oldSize == reversedChatItems.value.size) { + return@tryBlockAndSetLoadingMore + } } - )) + val index = mergedItems.value.items.indexOfLast { it.hasUnread() } + if (index != -1) { + // scroll to the top unread item + animatedScrollingInProgress.value = true + listState.value.animateScrollToItem(index + 1, -maxHeight.value) + } + } + } + } + + val bottomUnreadCount = remember { + derivedStateOf { + if (unreadCount.value == 0) return@derivedStateOf 0 + val lastVisibleItem = oldestPartiallyVisibleListItemInListStateOrNull(topPaddingToContentPx, mergedItems, listState) ?: return@derivedStateOf -1 + unreadCount.value - lastVisibleItem.unreadBefore + } + } + + val allowToShowBottomWithCounter = remember { mutableStateOf(true) } + val showBottomButtonWithCounter = remember { derivedStateOf { + val allow = allowToShowBottomWithCounter.value + val shouldShow = bottomUnreadCount.value > 0 && listState.value.firstVisibleItemIndex != 0 && searchValue.value.isEmpty() + // this tricky idea is to prevent showing button with arrow in the next frame after creating/receiving new message because the list will + // scroll to that message but before this happens, that button will show up and then will hide itself after scroll finishes. + // This workaround prevents it + allowToShowBottomWithCounter.value = shouldShow + shouldShow && allow + } } + val allowToShowBottomWithArrow = remember { mutableStateOf(true) } + val showBottomButtonWithArrow = remember { derivedStateOf { + val allow = allowToShowBottomWithArrow.value + val shouldShow = !showBottomButtonWithCounter.value && listState.value.firstVisibleItemIndex != 0 + allowToShowBottomWithArrow.value = shouldShow + shouldShow && allow + } } + + val requestedTopScroll = remember { mutableStateOf(false) } + val requestedBottomScroll = remember { mutableStateOf(false) } + + BottomEndFloatingButton( + bottomUnreadCount, + showBottomButtonWithCounter, + showBottomButtonWithArrow, + requestedBottomScroll, + animatedScrollingInProgress, + composeViewHeight, + onClick = { + if (loadingBottomItems.value || !lastItemsLoaded(chatsCtx)) { + requestedTopScroll.value = false + requestedBottomScroll.value = true + } else { + scrollToBottom() + } + } + ) + LaunchedEffect(Unit) { + launch { + snapshotFlow { loadingTopItems.value } + .drop(1) + .collect { top -> + if (!top && requestedTopScroll.value) { + requestedTopScroll.value = false + scrollToTopUnread() + } + } + } + launch { + snapshotFlow { loadingBottomItems.value } + .drop(1) + .collect { bottom -> + if (!bottom && requestedBottomScroll.value) { + requestedBottomScroll.value = false + scrollToBottom() + } + } + } } // Don't show top FAB if is in search if (searchValue.value.isNotEmpty()) return val fabSize = 56.dp - val topUnreadCount by remember { - derivedStateOf { unreadCount.value - bottomUnreadCount } + val topUnreadCount = remember { derivedStateOf { + if (bottomUnreadCount.value >= 0) (unreadCount.value - bottomUnreadCount.value).coerceAtLeast(0) else 0 } } - val showButtonWithCounter = topUnreadCount > 0 - val height = with(LocalDensity.current) { maxHeight.toPx() } val showDropDown = remember { mutableStateOf(false) } TopEndFloatingButton( - Modifier.padding(end = DEFAULT_PADDING, top = 24.dp).align(Alignment.TopEnd), + Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent).align(Alignment.TopEnd), topUnreadCount, - showButtonWithCounter, - onClick = { scope.launch { listState.animateScrollBy(height) } }, + requestedTopScroll, + animatedScrollingInProgress, + onClick = { + if (loadingTopItems.value) { + requestedBottomScroll.value = false + requestedTopScroll.value = true + } else { + scrollToTopUnread() + } + }, onLongClick = { showDropDown.value = true } ) - Box { - DefaultDropdownMenu(showDropDown, offset = DpOffset(this@FloatingButtons.maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) { + Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).align(Alignment.TopEnd)) { + val density = LocalDensity.current + val width = remember { mutableStateOf(250.dp) } + DefaultDropdownMenu( + showDropDown, + modifier = Modifier.onSizeChanged { with(density) { width.value = it.width.toDp().coerceAtLeast(250.dp) } }, + offset = DpOffset(-DEFAULT_PADDING - width.value, 24.dp + fabSize + topPaddingToContent) + ) { ItemAction( generalGetString(MR.strings.mark_read), painterResource(MR.images.ic_check), onClick = { - markRead( - CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), - bottomUnreadCount - ) + markChatRead() showDropDown.value = false }) } @@ -1148,126 +1872,725 @@ fun BoxWithConstraintsScope.FloatingButtons( @Composable fun PreloadItems( - listState: LazyListState, - remaining: Int = 10, - onLoadMore: () -> Unit, + chatsCtx: ChatModel.ChatsContext, + chatId: String, + ignoreLoadingRequests: MutableSet<Long>, + loadingMoreItems: State<Boolean>, + resetListState: State<Boolean>, + mergedItems: State<MergedItems>, + listState: State<LazyListState>, + remaining: Int, + loadItems: suspend (ChatId, ChatPagination) -> Boolean, ) { // Prevent situation when initial load and load more happens one after another after selecting a chat with long scroll position from previous selection val allowLoad = remember { mutableStateOf(false) } + val chatId = rememberUpdatedState(chatId) + val loadItems = rememberUpdatedState(loadItems) + val ignoreLoadingRequests = rememberUpdatedState(ignoreLoadingRequests) LaunchedEffect(Unit) { - snapshotFlow { chatModel.chatId.value } + snapshotFlow { chatId.value } + .distinctUntilChanged() .filterNotNull() .collect { - allowLoad.value = listState.layoutInfo.totalItemsCount == listState.layoutInfo.visibleItemsInfo.size + allowLoad.value = false delay(500) allowLoad.value = true } } - KeyChangeEffect(allowLoad.value) { - snapshotFlow { - val lInfo = listState.layoutInfo - val totalItemsNumber = lInfo.totalItemsCount - val lastVisibleItemIndex = (lInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 - if (allowLoad.value && lastVisibleItemIndex > (totalItemsNumber - remaining) && totalItemsNumber >= ChatPagination.INITIAL_COUNT) - totalItemsNumber + ChatPagination.PRELOAD_COUNT - else - 0 + if (allowLoad.value && !loadingMoreItems.value) { + LaunchedEffect(chatId.value, resetListState.value) { + snapshotFlow { listState.value.firstVisibleItemIndex } + .distinctUntilChanged() + .collect { firstVisibleIndex -> + if (!preloadItemsBefore(chatsCtx, firstVisibleIndex, chatId, ignoreLoadingRequests, mergedItems, listState, remaining, loadItems)) { + preloadItemsAfter(chatsCtx, firstVisibleIndex, chatId, mergedItems, remaining, loadItems) + } + loadLastItems(chatsCtx, chatId, listState, loadItems) + } } - .filter { it > 0 } - .collect { - onLoadMore() - } } } -private fun showMemberImage(member: GroupMember, prevItem: ChatItem?): Boolean = - when (val dir = prevItem?.chatDir) { - is CIDirection.GroupSnd -> true - is CIDirection.GroupRcv -> dir.groupMember.groupMemberId != member.groupMemberId - else -> false +private suspend fun preloadItemsBefore( + chatsCtx: ChatModel.ChatsContext, + firstVisibleIndex: Int, + chatId: State<String>, + ignoreLoadingRequests: State<MutableSet<Long>>, + mergedItems: State<MergedItems>, + listState: State<LazyListState>, + remaining: Int, + loadItems: State<suspend (ChatId, ChatPagination) -> Boolean>, +): Boolean { + val splits = mergedItems.value.splits + val lastVisibleIndex = (listState.value.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + var lastIndexToLoadFrom: Int? = findLastIndexToLoadFromInSplits(firstVisibleIndex, lastVisibleIndex, remaining, splits) + val items = reversedChatItemsStatic(chatsCtx) + if (splits.isEmpty() && items.isNotEmpty() && lastVisibleIndex > mergedItems.value.items.size - remaining) { + lastIndexToLoadFrom = items.lastIndex } + if (lastIndexToLoadFrom != null) { + val loadFromItemId = items.getOrNull(lastIndexToLoadFrom)?.id ?: return false + if (!ignoreLoadingRequests.value.contains(loadFromItemId)) { + val items = reversedChatItemsStatic(chatsCtx) + val sizeWas = items.size + val oldestItemIdWas = items.lastOrNull()?.id + val triedToLoad = loadItems.value(chatId.value, ChatPagination.Before(loadFromItemId, ChatPagination.PRELOAD_COUNT)) + val itemsUpdated = reversedChatItemsStatic(chatsCtx) + if (triedToLoad && sizeWas == itemsUpdated.size && oldestItemIdWas == itemsUpdated.lastOrNull()?.id) { + ignoreLoadingRequests.value.add(loadFromItemId) + return false + } + return triedToLoad + } + } + return false +} -val MEMBER_IMAGE_SIZE: Dp = 38.dp +private suspend fun preloadItemsAfter( + chatsCtx: ChatModel.ChatsContext, + firstVisibleIndex: Int, + chatId: State<String>, + mergedItems: State<MergedItems>, + remaining: Int, + loadItems: State<suspend (ChatId, ChatPagination) -> Boolean>, +) { + val items = reversedChatItemsStatic(chatsCtx) + val splits = mergedItems.value.splits + val split = splits.lastOrNull { it.indexRangeInParentItems.contains(firstVisibleIndex) } + // we're inside a splitRange (top --- [end of the splitRange --- we're here --- start of the splitRange] --- bottom) + if (split != null && split.indexRangeInParentItems.first + remaining > firstVisibleIndex) { + val loadFromItemId = items.getOrNull(split.indexRangeInReversed.first)?.id ?: return + loadItems.value(chatId.value, ChatPagination.After(loadFromItemId, ChatPagination.PRELOAD_COUNT)) + } +} + +val MEMBER_IMAGE_SIZE: Dp = 37.dp @Composable fun MemberImage(member: GroupMember) { - ProfileImage(MEMBER_IMAGE_SIZE, member.memberProfile.image) + MemberProfileImage(MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier, member, backgroundColor = MaterialTheme.colors.background) } @Composable private fun TopEndFloatingButton( modifier: Modifier = Modifier, - unreadCount: Int, - showButtonWithCounter: Boolean, + unreadCount: State<Int>, + requestedTopScroll: State<Boolean>, + animatedScrollingInProgress: State<Boolean>, onClick: () -> Unit, onLongClick: () -> Unit -) = when { - showButtonWithCounter -> { +) { + if (remember { derivedStateOf { unreadCount.value > 0 && !animatedScrollingInProgress.value } }.value) { val interactionSource = interactionSourceWithDetection(onClick, onLongClick) FloatingActionButton( {}, // no action here - modifier.size(48.dp), + modifier.size(48.dp).onRightClick(onLongClick), backgroundColor = MaterialTheme.colors.secondaryVariant, elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp), interactionSource = interactionSource, ) { - Text( - unreadCountStr(unreadCount), - color = MaterialTheme.colors.primary, - fontSize = 14.sp, - ) - } - } - else -> { - } -} - -private fun bottomEndFloatingButton( - unreadCount: Int, - showButtonWithCounter: Boolean, - showButtonWithArrow: Boolean, - onClickArrowDown: () -> Unit, - onClickCounter: () -> Unit -): @Composable () -> Unit = when { - showButtonWithCounter -> { - { - FloatingActionButton( - onClick = onClickCounter, - elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), - modifier = Modifier.size(48.dp), - backgroundColor = MaterialTheme.colors.secondaryVariant, - ) { + if (requestedTopScroll.value) { + LoadingProgressIndicator() + } else { Text( - unreadCountStr(unreadCount), + unreadCountStr(unreadCount.value), color = MaterialTheme.colors.primary, fontSize = 14.sp, ) } } } - showButtonWithArrow -> { - { - FloatingActionButton( - onClick = onClickArrowDown, - elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), - modifier = Modifier.size(48.dp), - backgroundColor = MaterialTheme.colors.secondaryVariant, - ) { - Icon( - painter = painterResource(MR.images.ic_keyboard_arrow_down), - contentDescription = null, - tint = MaterialTheme.colors.primary - ) - } - } - } - else -> { - {} +} + +@Composable +fun topPaddingToContent(chatView: Boolean, additionalTopBar: Boolean = false): Dp { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val chatBottomBar = remember { appPrefs.chatBottomBar.state } + val reportsPadding = if (additionalTopBar) AppBarHeight * fontSizeSqrtMultiplier else 0.dp + return if (oneHandUI.value && (!chatView || chatBottomBar.value)) { + WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + reportsPadding + } else { + AppBarHeight * fontSizeSqrtMultiplier + WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + reportsPadding } } -private fun markUnreadChatAsRead(activeChat: MutableState<Chat?>, chatModel: ChatModel) { - val chat = activeChat.value +@Composable +private fun numberOfBottomAppBars(): Int { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val chatBottomBar = remember { appPrefs.chatBottomBar.state } + return if (oneHandUI.value && chatBottomBar.value) { + 2 + } else { + 1 + } +} + +@Composable +private fun FloatingDate( + modifier: Modifier, + topPaddingToContentPx: State<Int>, + mergedItems: State<MergedItems>, + listState: State<LazyListState>, +) { + val isNearBottom = remember(chatModel.chatId) { mutableStateOf(listState.value.firstVisibleItemIndex == 0) } + val nearBottomIndex = remember(chatModel.chatId) { mutableStateOf(if (isNearBottom.value) -1 else 0) } + val showDate = remember(chatModel.chatId) { mutableStateOf(false) } + val density = LocalDensity.current.density + val fontSizeSqrtMultiplier = fontSizeSqrtMultiplier + val lastVisibleItemDate = remember { + derivedStateOf { + if (listState.value.layoutInfo.visibleItemsInfo.lastIndex >= 0) { + val lastVisibleChatItem = lastFullyVisibleIemInListState(topPaddingToContentPx, density, fontSizeSqrtMultiplier, mergedItems, listState) + val timeZone = TimeZone.currentSystemDefault() + lastVisibleChatItem?.meta?.itemTs?.toLocalDateTime(timeZone)?.date?.atStartOfDayIn(timeZone) + } else { + null + } + } + } + + LaunchedEffect(Unit) { + snapshotFlow { listState.value.layoutInfo.visibleItemsInfo } + .collect { visibleItemsInfo -> + if (visibleItemsInfo.find { it.index == 0 } != null) { + var elapsedOffset = 0 + + for (it in visibleItemsInfo) { + if (elapsedOffset >= listState.value.layoutInfo.viewportSize.height / 2.5) { + nearBottomIndex.value = it.index + break; + } + elapsedOffset += it.size + } + } + + isNearBottom.value = if (nearBottomIndex.value == -1) true else (visibleItemsInfo.firstOrNull()?.index ?: 0) <= nearBottomIndex.value + } + } + + fun setDateVisibility(isVisible: Boolean) { + if (isVisible) { + val now = Clock.System.now() + val date = lastVisibleItemDate.value + if (!isNearBottom.value && !showDate.value && date != null && getTimestampDateText(date) != getTimestampDateText(now)) { + showDate.value = true + } + } else if (showDate.value) { + showDate.value = false + } + } + + LaunchedEffect(Unit) { + var hideDateWhenNotScrolling: Job = Job() + snapshotFlow { listState.value.firstVisibleItemScrollOffset } + .collect { + setDateVisibility(true) + hideDateWhenNotScrolling.cancel() + hideDateWhenNotScrolling = launch { + delay(1000) + setDateVisibility(false) + } + } + } + + AnimatedVisibility( + modifier = modifier, + visible = showDate.value, + enter = fadeIn(tween(durationMillis = 350)), + exit = fadeOut(tween(durationMillis = 350)) + ) { + val date = lastVisibleItemDate.value + Column { + Text( + text = if (date != null) getTimestampDateText(date) else "", + Modifier + .background( + color = MaterialTheme.colors.secondaryVariant, + RoundedCornerShape(25.dp) + ) + .padding(vertical = 4.dp, horizontal = 8.dp) + .clip(RoundedCornerShape(25.dp)), + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + color = MaterialTheme.colors.secondary + ) + } + } +} + +@Composable +private fun SaveReportsStateOnDispose(chatsCtx: ChatModel.ChatsContext, listState: State<LazyListState>) { + DisposableEffect(Unit) { + onDispose { + reportsListState = if (chatsCtx.contentTag == MsgContentTag.Report && ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) listState.value else null + } + } +} + +@Composable +private fun DownloadFilesButton( + forwardConfirmation: ForwardConfirmation.FilesNotAccepted, + rhId: Long?, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding +) { + val user = chatModel.currentUser.value + + if (user != null) { + TextButton( + contentPadding = contentPadding, + modifier = modifier, + onClick = { + AlertManager.shared.hideAlert() + + withBGApi { + controller.receiveFiles( + rhId = rhId, + fileIds = forwardConfirmation.fileIds, + user = user + ) + } + } + ) { + Text(stringResource(MR.strings.forward_files_not_accepted_receive_files), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } +} + +@Composable +private fun ForwardButton( + forwardPlan: CR.ForwardPlan, + chatInfo: ChatInfo, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding +) { + TextButton( + onClick = { + forwardContent(forwardPlan.chatItemIds, chatInfo) + AlertManager.shared.hideAlert() + }, + modifier = modifier, + contentPadding = contentPadding + ) { + Text(stringResource(MR.strings.forward_chat_item), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } +} + +@Composable +private fun ButtonRow(horizontalArrangement: Arrangement.Horizontal, content: @Composable() (RowScope.() -> Unit)) { + Row( + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), + horizontalArrangement = horizontalArrangement + ) { + content() + } +} + +@Composable +private fun DateSeparator(date: Instant) { + Text( + text = getTimestampDateText(date), + Modifier.padding(vertical = DEFAULT_PADDING_HALF + 4.dp, horizontal = DEFAULT_PADDING_HALF).fillMaxWidth(), + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + color = MaterialTheme.colors.secondary + ) +} + +@Composable +private fun MarkItemsReadAfterDelay( + itemKey: ChatViewItemKey, + itemIds: List<Long>, + finishedInitialComposition: State<Boolean>, + chatId: ChatId, + listState: State<LazyListState>, + markItemsRead: (List<Long>) -> Unit +) { + // items can be "visible" in terms of LazyColumn but hidden behind compose view/appBar. So don't count such item as visible and not mark read + val itemIsPartiallyAboveCompose = remember { derivedStateOf { + val item = listState.value.layoutInfo.visibleItemsInfo.firstOrNull { it.key == itemKey } + if (item != null) { + item.offset >= 0 || -item.offset < item.size + } else { + false + } + } } + LaunchedEffect(itemIsPartiallyAboveCompose.value, itemIds, finishedInitialComposition.value, chatId) { + if (chatId != ChatModel.chatId.value || !itemIsPartiallyAboveCompose.value || !finishedInitialComposition.value) return@LaunchedEffect + + delay(600L) + markItemsRead(itemIds) + } +} + +@Composable +fun reportsCount(staleChatId: String?): Int { + return if (staleChatId?.startsWith("#") != true) { + 0 + } else { + remember(staleChatId) { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId }?.chatStats } }.value?.reportsCount ?: 0 + } +} + +private fun reversedChatItemsStatic(chatsCtx: ChatModel.ChatsContext): List<ChatItem> = + chatsCtx.chatItems.value.asReversed() + +private fun oldestPartiallyVisibleListItemInListStateOrNull(topPaddingToContentPx: State<Int>, mergedItems: State<MergedItems>, listState: State<LazyListState>): ListItem? { + val lastFullyVisibleOffset = listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value + val visibleKey: ChatViewItemKey? = listState.value.layoutInfo.visibleItemsInfo.lastOrNull { item -> + item.offset <= lastFullyVisibleOffset + }?.key as? ChatViewItemKey + return mergedItems.value.items.getOrNull((mergedItems.value.indexInParentItems[visibleKey?.first] ?: listState.value.layoutInfo.visibleItemsInfo.lastOrNull()?.index) ?: -1)?.oldest() +} + +private fun lastFullyVisibleIemInListState(topPaddingToContentPx: State<Int>, density: Float, fontSizeSqrtMultiplier: Float, mergedItems: State<MergedItems>, listState: State<LazyListState>): ChatItem? { + val lastFullyVisibleOffsetMinusFloatingHeight = listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value - 50 * density * fontSizeSqrtMultiplier + val visibleKey: ChatViewItemKey? = listState.value.layoutInfo.visibleItemsInfo.lastOrNull { item -> + item.offset <= lastFullyVisibleOffsetMinusFloatingHeight && item.size > 0 + }?.key as? ChatViewItemKey + + return mergedItems.value.items.getOrNull( + (mergedItems.value.indexInParentItems[visibleKey?.first] + ?: listState.value.layoutInfo.visibleItemsInfo.lastOrNull()?.index) + ?: -1)?.newest()?.item +} + +private fun scrollToItem( + searchValue: State<String>, + loadingMoreItems: MutableState<Boolean>, + animatedScrollingInProgress: MutableState<Boolean>, + highlightedItems: MutableState<Set<Long>>, + chatInfo: State<ChatInfo>, + maxHeight: State<Int>, + scope: CoroutineScope, + reversedChatItems: State<List<ChatItem>>, + mergedItems: State<MergedItems>, + listState: State<LazyListState>, + loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, +): (Long) -> Unit = { itemId: Long -> + withApi { + try { + var index = mergedItems.value.indexInParentItems[itemId] ?: -1 + // Don't try to load messages while in search + if (index == -1 && searchValue.value.isNotBlank()) return@withApi + // setting it to 'loading' even if the item is loaded because in rare cases when the resulting item is near the top, scrolling to + // it will trigger loading more items and will scroll to incorrect position (because of trimming) + loadingMoreItems.value = true + if (index == -1) { + val pagination = ChatPagination.Around(itemId, ChatPagination.PRELOAD_COUNT * 2) + val oldSize = reversedChatItems.value.size + withContext(Dispatchers.Default) { + loadMessages(chatInfo.value.id, pagination) { + visibleItemIndexesNonReversed(mergedItems, reversedChatItems.value.size, listState.value) + } + } + var repeatsLeft = 50 + while (oldSize == reversedChatItems.value.size && repeatsLeft > 0) { + delay(20) + repeatsLeft-- + } + index = mergedItems.value.indexInParentItems[itemId] ?: -1 + } + if (index != -1) { + if (listState.value.layoutInfo.visibleItemsInfo.any { it.index == index && it.offset + it.size <= maxHeight.value }) { + highlightedItems.value = setOf(itemId) + } else { + withContext(scope.coroutineContext) { + animatedScrollingInProgress.value = true + listState.value.animateScrollToItem(min(reversedChatItems.value.lastIndex, index + 1), -maxHeight.value) + highlightedItems.value = setOf(itemId) + } + } + } + } finally { + loadingMoreItems.value = false + } + } +} + +private fun findQuotedItemFromItem( + chatsCtx: ChatModel.ChatsContext, + rhId: State<Long?>, + chatInfo: State<ChatInfo>, + scope: CoroutineScope, + scrollToItem: (Long) -> Unit +): (Long) -> Unit = { itemId: Long -> + scope.launch(Dispatchers.Default) { + val item = apiLoadSingleMessage(chatsCtx, rhId.value, chatInfo.value.chatType, chatInfo.value.apiId, itemId) + if (item != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatItem(chatInfo.value, item) + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.updateChatItem(chatInfo.value, item) + } + if (item.quotedItem?.itemId != null) { + scrollToItem(item.quotedItem.itemId) + } else { + showQuotedItemDoesNotExistAlert() + } + } + } +} + +val chatViewScrollState = MutableStateFlow(false) + +fun addGroupMembers(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) { + hideKeyboard(view) + withBGApi { + setGroupMembers(rhId, groupInfo, chatModel) + close?.invoke() + ModalManager.end.showModalCloseable(true) { close -> + AddGroupMembersView(rhId, groupInfo, false, chatModel, close) + } + } +} + +fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) { + hideKeyboard(view) + withBGApi { + val link = chatModel.controller.apiGetGroupLink(rhId, groupInfo.groupId) + close?.invoke() + ModalManager.end.showModalCloseable(true) { + GroupLinkView(chatModel, rhId, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null) + } + } +} + +@Composable +private fun BoxScope.BottomEndFloatingButton( + unreadCount: State<Int>, + showButtonWithCounter: State<Boolean>, + showButtonWithArrow: State<Boolean>, + requestedBottomScroll: State<Boolean>, + animatedScrollingInProgress: State<Boolean>, + composeViewHeight: State<Dp>, + onClick: () -> Unit +) { + when { + showButtonWithCounter.value && !animatedScrollingInProgress.value -> { + FloatingActionButton( + onClick = onClick, + elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), + modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp), + backgroundColor = MaterialTheme.colors.secondaryVariant, + ) { + if (requestedBottomScroll.value) { + LoadingProgressIndicator() + } else { + Text( + unreadCountStr(unreadCount.value), + color = MaterialTheme.colors.primary, + fontSize = 14.sp, + ) + } + } + } + showButtonWithArrow.value && !animatedScrollingInProgress.value -> { + FloatingActionButton( + onClick = onClick, + elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), + modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp), + backgroundColor = MaterialTheme.colors.secondaryVariant, + ) { + if (requestedBottomScroll.value) { + LoadingProgressIndicator() + } else { + Icon( + painter = painterResource(MR.images.ic_keyboard_arrow_down), + contentDescription = null, + tint = MaterialTheme.colors.primary + ) + } + } + } + else -> {} + } +} + +@Composable +fun SelectedListItem( + modifier: Modifier, + id: Long, + selectedItems: State<Set<Long>?>, +) { + val checked = remember { derivedStateOf { selectedItems.value?.contains(id) == true } } + Icon( + painterResource(if (checked.value) MR.images.ic_check_circle_filled else MR.images.ic_radio_button_unchecked), + null, + modifier.size(22.dp * fontSizeMultiplier), + tint = if (checked.value) { + MaterialTheme.colors.primary + } else if (isInDarkTheme()) { + // .tertiaryLabel instead of .secondary + Color(red = 235f / 255f, 235f / 255f, 245f / 255f, 76f / 255f) + } else { + // .tertiaryLabel instead of .secondary + Color(red = 60f / 255f, 60f / 255f, 67f / 255f, 76f / 255f) + } + ) +} + +@Composable +private fun LoadingProgressIndicator() { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + Modifier.size(30.dp), + color = MaterialTheme.colors.secondary, + strokeWidth = 2.dp + ) + } +} + +private fun selectUnselectChatItem( + select: Boolean, + ci: ChatItem, + revealed: State<Boolean>, + selectedChatItems: MutableState<Set<Long>?>, + reversedChatItems: State<List<ChatItem>> +) { + val itemIds = mutableSetOf<Long>() + if (!revealed.value) { + val currIndex = chatModel.getChatItemIndexOrNull(ci, reversedChatItems.value) + val ciCategory = ci.mergeCategory + if (currIndex != null && ciCategory != null) { + val (prevHidden, _) = chatModel.getPrevShownChatItem(currIndex, ciCategory, reversedChatItems.value) + val range = chatViewItemsRange(currIndex, prevHidden) + if (range != null) { + val reversed = reversedChatItems.value + for (i in range) { + itemIds.add(reversed[i].id) + } + } else { + itemIds.add(ci.id) + } + } else { + itemIds.add(ci.id) + } + } else { + itemIds.add(ci.id) + } + if (select) { + val sel = selectedChatItems.value ?: setOf() + selectedChatItems.value = sel.union(itemIds) + } else { + val sel = (selectedChatItems.value ?: setOf()).toMutableSet() + sel.removeAll(itemIds) + selectedChatItems.value = sel + } +} + +private fun deleteMessages(chatRh: Long?, chatInfo: ChatInfo, itemIds: List<Long>, forAll: Boolean, moderate: Boolean, onSuccess: () -> Unit = {}) { + if (itemIds.isNotEmpty()) { + withBGApi { + val deleted = if (chatInfo is ChatInfo.Group && forAll && moderate) { + chatModel.controller.apiDeleteMemberChatItems( + chatRh, + groupId = chatInfo.groupInfo.groupId, + itemIds = itemIds + ) + } else { + chatModel.controller.apiDeleteChatItems( + chatRh, + type = chatInfo.chatType, + id = chatInfo.apiId, + itemIds = itemIds, + mode = if (forAll) CIDeleteMode.cidmBroadcast else CIDeleteMode.cidmInternal + ) + } + if (deleted != null) { + withContext(Dispatchers.Main) { + for (di in deleted) { + val toChatItem = di.toChatItem?.chatItem + if (toChatItem != null) { + chatModel.chatsContext.upsertChatItem(chatRh, chatInfo, toChatItem) + } else { + chatModel.chatsContext.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem) + } + val deletedItem = di.deletedChatItem.chatItem + if (deletedItem.isActiveReport) { + chatModel.chatsContext.decreaseGroupReportsCounter(chatRh, chatInfo.id) + } + } + } + withContext(Dispatchers.Main) { + for (di in deleted) { + if (di.deletedChatItem.chatItem.isReport) { + val toChatItem = di.toChatItem?.chatItem + if (toChatItem != null) { + chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem) + } else { + chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem) + } + } + } + } + onSuccess() + } + } + } +} + +private fun archiveReports(chatRh: Long?, chatInfo: ChatInfo, itemIds: List<Long>, forAll: Boolean, onSuccess: () -> Unit = {}) { + if (itemIds.isNotEmpty()) { + withBGApi { + val deleted = chatModel.controller.apiDeleteReceivedReports( + chatRh, + groupId = chatInfo.apiId, + itemIds = itemIds, + mode = if (forAll) CIDeleteMode.cidmBroadcast else CIDeleteMode.cidmInternalMark + ) + if (deleted != null) { + withContext(Dispatchers.Main) { + for (di in deleted) { + val toChatItem = di.toChatItem?.chatItem + if (toChatItem != null) { + chatModel.chatsContext.upsertChatItem(chatRh, chatInfo, toChatItem) + } else { + chatModel.chatsContext.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem) + } + val deletedItem = di.deletedChatItem.chatItem + if (deletedItem.isActiveReport) { + chatModel.chatsContext.decreaseGroupReportsCounter(chatRh, chatInfo.id) + } + } + } + withContext(Dispatchers.Main) { + for (di in deleted) { + if (di.deletedChatItem.chatItem.isReport) { + val toChatItem = di.toChatItem?.chatItem + if (toChatItem != null) { + chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem) + } else { + chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem) + } + } + } + } + onSuccess() + } + } + } +} + +private fun archiveItems(rhId: Long?, chatInfo: ChatInfo, selectedChatItems: MutableState<Set<Long>?>) { + val itemIds = selectedChatItems.value + if (itemIds != null) { + showArchiveReportsAlert(itemIds.sorted(), chatInfo is ChatInfo.Group && chatInfo.groupInfo.membership.memberActive, archiveReports = { ids, forAll -> + archiveReports(rhId, chatInfo, ids, forAll) { + selectedChatItems.value = null + } + }) + } +} + +private fun markUnreadChatAsRead(chatId: String) { + val chat = chatModel.chats.value.firstOrNull { it.id == chatId } if (chat?.chatStats?.unreadChat != true) return withApi { val chatRh = chat.remoteHostId @@ -1277,9 +2600,11 @@ private fun markUnreadChatAsRead(activeChat: MutableState<Chat?>, chatModel: Cha chat.chatInfo.apiId, false ) - if (success && chat.id == activeChat.value?.id) { - activeChat.value = chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)) - chatModel.replaceChat(chatRh, chat.id, activeChat.value!!) + if (success) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.replaceChat(chatRh, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))) + chatModel.chatsContext.markChatTagRead(chat) + } } } } @@ -1299,6 +2624,46 @@ private fun memberNames(member: GroupMember, prevMember: GroupMember?, memCount: } } +fun Modifier.chatViewBackgroundModifier( + colors: Colors, + wallpaper: AppWallpaper, + backgroundGraphicsLayerSize: MutableState<IntSize>?, + backgroundGraphicsLayer: GraphicsLayer?, + drawWallpaper: Boolean +): Modifier { + val wallpaperImage = wallpaper.type.image + val wallpaperType = wallpaper.type + val backgroundColor = if (drawWallpaper) wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, colors.background) else colors.background + val tintColor = wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base) + + return this + .then(if (wallpaperImage != null && drawWallpaper) + Modifier.drawWithCache { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor, backgroundGraphicsLayerSize, backgroundGraphicsLayer) } + else + Modifier.drawWithCache { onDrawBehind { copyBackgroundToAppBar(backgroundGraphicsLayerSize, backgroundGraphicsLayer) { drawRect(backgroundColor) } } } + ) +} + +private fun findLastIndexToLoadFromInSplits(firstVisibleIndex: Int, lastVisibleIndex: Int, remaining: Int, splits: List<SplitRange>): Int? { + for (split in splits) { + // before any split + if (split.indexRangeInParentItems.first > firstVisibleIndex) { + if (lastVisibleIndex > (split.indexRangeInParentItems.first - remaining)) { + return split.indexRangeInReversed.first - 1 + } + break + } + val containsInRange = split.indexRangeInParentItems.contains(firstVisibleIndex) + if (containsInRange) { + if (lastVisibleIndex > (split.indexRangeInParentItems.last - remaining)) { + return split.indexRangeInReversed.last + } + break + } + } + return null +} + fun chatViewItemsRange(currIndex: Int?, prevHidden: Int?): IntRange? = if (currIndex != null && prevHidden != null && prevHidden > currIndex) { currIndex..prevHidden @@ -1306,14 +2671,23 @@ fun chatViewItemsRange(currIndex: Int?, prevHidden: Int?): IntRange? = null } +private suspend fun tryBlockAndSetLoadingMore(loadingMoreItems: MutableState<Boolean>, block: suspend () -> Unit) { + try { + loadingMoreItems.value = true + block() + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + } finally { + loadingMoreItems.value = false + } +} sealed class ProviderMedia { data class Image(val data: ByteArray, val image: ImageBitmap): ProviderMedia() - data class Video(val uri: URI, val preview: String): ProviderMedia() + data class Video(val uri: URI, val fileSource: CryptoFile?, val preview: String): ProviderMedia() } -private fun providerForGallery( - listStateIndex: Int, +fun providerForGallery( chatItems: List<ChatItem>, cItemId: Long, scrollTo: (Int) -> Unit @@ -1324,6 +2698,8 @@ private fun providerForGallery( fun item(skipInternalIndex: Int, initialChatId: Long): Pair<Int, ChatItem>? { var processedInternalIndex = -skipInternalIndex.sign val indexOfFirst = chatItems.indexOfFirst { it.id == initialChatId } + // The first was deleted or moderated + if (indexOfFirst == -1) return null for (chatItemsIndex in if (skipInternalIndex >= 0) indexOfFirst downTo 0 else indexOfFirst..chatItems.lastIndex) { val item = chatItems[chatItemsIndex] if (canShowMedia(item)) { @@ -1336,11 +2712,12 @@ private fun providerForGallery( return null } - var initialIndex = Int.MAX_VALUE / 2 + // Pager has a bug with overflowing when total pages is around Int.MAX_VALUE. Using smaller value + var initialIndex = 10000 / 2 var initialChatId = cItemId return object: ImageGalleryProvider { override val initialIndex: Int = initialIndex - override val totalMediaSize = mutableStateOf(Int.MAX_VALUE) + override val totalMediaSize = mutableStateOf(10000) override fun getMedia(index: Int): ProviderMedia? { val internalIndex = initialIndex - index val item = item(internalIndex, initialChatId)?.second ?: return null @@ -1357,7 +2734,7 @@ private fun providerForGallery( val filePath = if (chatModel.connectedToRemote() && item.file?.loaded == true) getAppFilePath(item.file.fileName) else getLoadedFilePath(item.file) if (filePath != null) { val uri = getAppFileUri(filePath.substringAfterLast(File.separator)) - ProviderMedia.Video(uri, (item.content.msgContent as MsgContent.MCVideo).image) + ProviderMedia.Video(uri, item.file?.fileSource, (item.content.msgContent as MsgContent.MCVideo).image) } else null } else -> null @@ -1373,20 +2750,25 @@ private fun providerForGallery( override fun scrollToStart() { initialIndex = 0 - initialChatId = chatItems.first { canShowMedia(it) }.id + initialChatId = chatItems.firstOrNull { canShowMedia(it) }?.id ?: return } override fun onDismiss(index: Int) { val internalIndex = initialIndex - index - val indexInChatItems = item(internalIndex, initialChatId)?.first ?: return + val item = item(internalIndex, initialChatId) + val indexInChatItems = item?.first ?: return val indexInReversed = chatItems.lastIndex - indexInChatItems // Do not scroll to active item, just to different items - if (indexInReversed == listStateIndex) return + if (item.second.id == cItemId) return scrollTo(indexInReversed) } } } +typealias ChatViewItemKey = Pair<Long, Long> + +private fun keyForItem(item: ChatItem): ChatViewItemKey = ChatViewItemKey(item.id, item.meta.createdAt.toEpochMilliseconds()) + private fun ViewConfiguration.bigTouchSlop(slop: Float = 50f) = object: ViewConfiguration { override val longPressTimeoutMillis get() = @@ -1400,6 +2782,115 @@ private fun ViewConfiguration.bigTouchSlop(slop: Float = 50f) = object: ViewConf override val touchSlop: Float get() = slop } +private fun forwardContent(chatItemsIds: List<Long>, chatInfo: ChatInfo) { + chatModel.chatId.value = null + chatModel.sharedContent.value = SharedContent.Forward( + chatModel.chatsContext.chatItems.value.filter { chatItemsIds.contains(it.id) }, + chatInfo + ) +} + +private fun forwardConfirmationAlertDescription(forwardConfirmation: ForwardConfirmation): String { + return when (forwardConfirmation) { + is ForwardConfirmation.FilesNotAccepted -> String.format(generalGetString(MR.strings.forward_files_not_accepted_desc), forwardConfirmation.fileIds.count()) + is ForwardConfirmation.FilesInProgress -> String.format(generalGetString(MR.strings.forward_files_in_progress_desc), forwardConfirmation.filesCount) + is ForwardConfirmation.FilesFailed -> String.format(generalGetString(MR.strings.forward_files_failed_to_receive_desc), forwardConfirmation.filesCount) + is ForwardConfirmation.FilesMissing -> String.format(generalGetString(MR.strings.forward_files_missing_desc), forwardConfirmation.filesCount) + } +} + +private fun handleForwardConfirmation( + rhId: Long?, + forwardPlan: CR.ForwardPlan, + chatInfo: ChatInfo +) { + var alertDescription = if (forwardPlan.forwardConfirmation != null) forwardConfirmationAlertDescription(forwardPlan.forwardConfirmation) else "" + + if (forwardPlan.chatItemIds.isNotEmpty()) { + alertDescription += "\n${generalGetString(MR.strings.forward_alert_forward_messages_without_files)}" + } + + AlertManager.shared.showAlertDialogButtonsColumn( + title = if (forwardPlan.chatItemIds.isNotEmpty()) + String.format(generalGetString(MR.strings.forward_alert_title_messages_to_forward), forwardPlan.chatItemIds.count()) else + generalGetString(MR.strings.forward_alert_title_nothing_to_forward), + text = alertDescription, + buttons = { + if (forwardPlan.chatItemIds.isNotEmpty()) { + when (val confirmation = forwardPlan.forwardConfirmation) { + is ForwardConfirmation.FilesNotAccepted -> { + val fillMaxWidthModifier = Modifier.fillMaxWidth() + val contentPadding = PaddingValues(vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL) + Column { + ForwardButton(forwardPlan, chatInfo, fillMaxWidthModifier, contentPadding) + DownloadFilesButton(confirmation, rhId, fillMaxWidthModifier, contentPadding) + TextButton(onClick = { AlertManager.shared.hideAlert() }, modifier = fillMaxWidthModifier, contentPadding = contentPadding) { + Text(stringResource(MR.strings.cancel_verb), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + else -> { + ButtonRow(Arrangement.SpaceBetween) { + TextButton(onClick = { AlertManager.shared.hideAlert() }) { + Text(stringResource(MR.strings.cancel_verb), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + ForwardButton(forwardPlan, chatInfo) + } + } + } + } else { + when (val confirmation = forwardPlan.forwardConfirmation) { + is ForwardConfirmation.FilesNotAccepted -> { + ButtonRow(Arrangement.SpaceBetween) { + TextButton(onClick = { AlertManager.shared.hideAlert() }) { + Text(stringResource(MR.strings.cancel_verb), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + DownloadFilesButton(confirmation, rhId) + } + } + else -> ButtonRow(Arrangement.Center) { + TextButton(onClick = { AlertManager.shared.hideAlert() }) { + Text(stringResource(MR.strings.ok), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + } + } + ) +} + +private fun getItemSeparation(chatItem: ChatItem, prevItem: ChatItem?): ItemSeparation { + if (prevItem == null) { + return ItemSeparation(timestamp = true, largeGap = true, date = null) + } + + val sameMemberAndDirection = if (prevItem.chatDir is GroupRcv && chatItem.chatDir is GroupRcv) { + chatItem.chatDir.groupMember.groupMemberId == prevItem.chatDir.groupMember.groupMemberId + } else chatItem.chatDir.sent == prevItem.chatDir.sent + val largeGap = !sameMemberAndDirection || (abs(prevItem.meta.createdAt.epochSeconds - chatItem.meta.createdAt.epochSeconds) >= 60) + + return ItemSeparation( + timestamp = largeGap || prevItem.meta.timestampText != chatItem.meta.timestampText, + largeGap = largeGap, + date = if (getTimestampDateText(chatItem.meta.itemTs) == getTimestampDateText(prevItem.meta.itemTs)) null else prevItem.meta.itemTs + ) +} + +private fun getItemSeparationLargeGap(chatItem: ChatItem, nextItem: ChatItem?): Boolean { + if (nextItem == null) { + return true + } + + val sameMemberAndDirection = if (nextItem.chatDir is GroupRcv && chatItem.chatDir is GroupRcv) { + chatItem.chatDir.groupMember.groupMemberId == nextItem.chatDir.groupMember.groupMemberId + } else chatItem.chatDir.sent == nextItem.chatDir.sent + return !sameMemberAndDirection || (abs(nextItem.meta.createdAt.epochSeconds - chatItem.meta.createdAt.epochSeconds) >= 60) +} + +private fun shouldShowAvatar(current: ChatItem, older: ChatItem?) = + current.chatDir is CIDirection.GroupRcv && (older == null || (older.chatDir !is CIDirection.GroupRcv || older.chatDir.groupMember.memberId != current.chatDir.groupMember.memberId)) + + @Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, @@ -1429,28 +2920,28 @@ fun PreviewChatLayout() { val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) } val searchValue = remember { mutableStateOf("") } ChatLayout( - chat = Chat( - remoteHostId = null, - chatInfo = ChatInfo.Direct.sampleData, - chatItems = chatItems, - chatStats = Chat.ChatStats() - ), + chatsCtx = ChatModel.ChatsContext(contentTag = null), + remoteHostId = remember { mutableStateOf(null) }, + chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) }, unreadCount = unreadCount, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, - composeView = {}, + composeView = { _ -> }, + scrollToItemId = remember { mutableStateOf(null) }, attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) }, attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), - chatItems = chatItems, searchValue, useLinkPreviews = true, linkMode = SimplexLinkMode.DESCRIPTION, + selectedChatItems = remember { mutableStateOf(setOf()) }, back = {}, info = {}, + showGroupReports = {}, showMemberInfo = { _, _ -> }, - loadPrevMessages = {}, + loadMessages = { _, _, _ -> }, deleteMessage = { _, _ -> }, deleteMessages = { _ -> }, - receiveFile = { _, _ -> }, + archiveReports = { _, _ -> }, + receiveFile = { _ -> }, cancelFile = {}, joinGroup = { _, _ -> }, startCall = {}, @@ -1458,6 +2949,7 @@ fun PreviewChatLayout() { acceptCall = { _ -> }, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, + forwardItem = { _, _ -> }, updateContactStats = { }, updateMemberStats = { _, _ -> }, syncContactConnection = { }, @@ -1468,11 +2960,15 @@ fun PreviewChatLayout() { showItemDetails = { _, _ -> }, addMembers = { _ -> }, openGroupLink = {}, - markRead = { _, _ -> }, + markItemsRead = { _ -> }, + markChatRead = {}, changeNtfsState = { _, _ -> }, onSearchValueChanged = {}, + closeSearch = {}, onComposed = {}, developerTools = false, + showViaProxy = false, + showSearch = remember { mutableStateOf(false) } ) } } @@ -1502,28 +2998,28 @@ fun PreviewGroupChatLayout() { val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) } val searchValue = remember { mutableStateOf("") } ChatLayout( - chat = Chat( - remoteHostId = null, - chatInfo = ChatInfo.Group.sampleData, - chatItems = chatItems, - chatStats = Chat.ChatStats() - ), + chatsCtx = ChatModel.ChatsContext(contentTag = null), + remoteHostId = remember { mutableStateOf(null) }, + chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) }, unreadCount = unreadCount, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, - composeView = {}, + composeView = { _ -> }, + scrollToItemId = remember { mutableStateOf(null) }, attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) }, attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), - chatItems = chatItems, searchValue, useLinkPreviews = true, linkMode = SimplexLinkMode.DESCRIPTION, + selectedChatItems = remember { mutableStateOf(setOf()) }, back = {}, info = {}, + showGroupReports = {}, showMemberInfo = { _, _ -> }, - loadPrevMessages = {}, + loadMessages = { _, _, _ -> }, deleteMessage = { _, _ -> }, deleteMessages = {}, - receiveFile = { _, _ -> }, + archiveReports = { _, _ -> }, + receiveFile = { _ -> }, cancelFile = {}, joinGroup = { _, _ -> }, startCall = {}, @@ -1531,6 +3027,7 @@ fun PreviewGroupChatLayout() { acceptCall = { _ -> }, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, + forwardItem = { _, _ -> }, updateContactStats = { }, updateMemberStats = { _, _ -> }, syncContactConnection = { }, @@ -1541,11 +3038,15 @@ fun PreviewGroupChatLayout() { showItemDetails = { _, _ -> }, addMembers = { _ -> }, openGroupLink = {}, - markRead = { _, _ -> }, + markItemsRead = { _ -> }, + markChatRead = {}, changeNtfsState = { _, _ -> }, onSearchValueChanged = {}, + closeSearch = {}, onComposed = {}, developerTools = false, + showViaProxy = false, + showSearch = remember { mutableStateOf(false) } ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt index 20316dd524..bc82bc593f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt @@ -16,7 +16,7 @@ import dev.icerock.moko.resources.compose.stringResource @Composable fun ComposeContextInvitingContactMemberView() { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val sentColor = MaterialTheme.appColors.sentMessage Row( Modifier .height(60.dp) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt index 83076f885b..7ab7963547 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt @@ -16,7 +16,7 @@ import chat.simplex.res.MR @Composable fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boolean) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val sentColor = MaterialTheme.appColors.sentMessage Row( Modifier .height(60.dp) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeImageView.kt index 906065f741..97b6f9afda 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeImageView.kt @@ -20,7 +20,7 @@ import chat.simplex.common.views.helpers.UploadContent @Composable fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val sentColor = MaterialTheme.appColors.sentMessage Row( Modifier .padding(top = 8.dp) 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 b230d261f1..894bcf3b37 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) +@file:UseSerializers(UriSerializer::class, ComposeMessageSerializer::class) package chat.simplex.common.views.chat +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* @@ -10,26 +11,37 @@ 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.focus.FocusRequester +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.font.FontStyle import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.util.* import chat.simplex.common.model.* import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.filesToDelete import chat.simplex.common.platform.* -import chat.simplex.common.ui.theme.Indigo -import chat.simplex.common.ui.theme.isSystemInDarkTheme +import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.item.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import kotlinx.coroutines.* import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import java.io.File import java.net.URI import java.nio.file.Files +const val MAX_NUMBER_OF_MENTIONS = 3 + @Serializable sealed class ComposePreview { @Serializable object NoPreview: ComposePreview() @@ -44,6 +56,8 @@ sealed class ComposeContextItem { @Serializable object NoContextItem: ComposeContextItem() @Serializable class QuotedItem(val chatItem: ChatItem): ComposeContextItem() @Serializable class EditingItem(val chatItem: ChatItem): ComposeContextItem() + @Serializable class ForwardingItems(val chatItems: List<ChatItem>, val fromChatInfo: ChatInfo): ComposeContextItem() + @Serializable class ReportedItem(val chatItem: ChatItem, val reason: ReportReason): ComposeContextItem() } @Serializable @@ -54,41 +68,95 @@ data class LiveMessage( val sent: Boolean ) +typealias MentionedMembers = Map<String, CIMention> + +@Serializable +data class ComposeMessage( + val text: String = "", + val selection: TextRange = TextRange.Zero +) { + constructor(text: String): this(text, TextRange(text.length)) +} + +@Serializer(forClass = TextRange::class) +object ComposeMessageSerializer : KSerializer<TextRange> { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("TextRange", PrimitiveKind.LONG) + override fun serialize(encoder: Encoder, value: TextRange) = + encoder.encodeLong(packInts(value.start, value.end)) + override fun deserialize(decoder: Decoder): TextRange = + decoder.decodeLong().let { value -> TextRange(unpackInt1(value), unpackInt2(value)) } +} + @Serializable data class ComposeState( - val message: String = "", + val message: ComposeMessage = ComposeMessage(), + val parsedMessage: List<FormattedText> = emptyList(), val liveMessage: LiveMessage? = null, val preview: ComposePreview = ComposePreview.NoPreview, val contextItem: ComposeContextItem = ComposeContextItem.NoContextItem, val inProgress: Boolean = false, - val useLinkPreviews: Boolean + val useLinkPreviews: Boolean, + val mentions: MentionedMembers = emptyMap() ) { constructor(editingItem: ChatItem, liveMessage: LiveMessage? = null, useLinkPreviews: Boolean): this( - editingItem.content.text, + ComposeMessage(editingItem.content.text), + editingItem.formattedText ?: FormattedText.plain(editingItem.content.text), liveMessage, chatItemPreview(editingItem), ComposeContextItem.EditingItem(editingItem), - useLinkPreviews = useLinkPreviews + useLinkPreviews = useLinkPreviews, + mentions = editingItem.mentions ?: emptyMap() ) + val memberMentions: Map<String, Long> + get() = this.mentions.mapNotNull { + val memberRef = it.value.memberRef + + if (memberRef != null) { + it.key to memberRef.groupMemberId + } else { + null + } + }.toMap() + val editing: Boolean get() = when (contextItem) { is ComposeContextItem.EditingItem -> true else -> false } + val forwarding: Boolean + get() = when (contextItem) { + is ComposeContextItem.ForwardingItems -> true + else -> false + } + val reporting: Boolean + get() = when (contextItem) { + is ComposeContextItem.ReportedItem -> true + else -> false + } + val submittingValidReport: Boolean + get() = when (contextItem) { + is ComposeContextItem.ReportedItem -> { + when (contextItem.reason) { + is ReportReason.Other -> message.text.isNotEmpty() + else -> true + } + } + else -> false + } val sendEnabled: () -> Boolean get() = { val hasContent = when (preview) { is ComposePreview.MediaPreview -> true is ComposePreview.VoicePreview -> true is ComposePreview.FilePreview -> true - else -> message.isNotEmpty() || liveMessage != null + else -> message.text.isNotEmpty() || forwarding || liveMessage != null || submittingValidReport } hasContent && !inProgress } val endLiveDisabled: Boolean - get() = liveMessage != null && message.isEmpty() && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem + get() = liveMessage != null && message.text.isEmpty() && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem val linkPreviewAllowed: Boolean get() = @@ -107,7 +175,7 @@ data class ComposeState( val attachmentDisabled: Boolean get() { - if (editing || liveMessage != null || inProgress) return true + if (editing || forwarding || liveMessage != null || inProgress || reporting) return true return when (preview) { ComposePreview.NoPreview -> false is ComposePreview.CLinkPreview -> false @@ -115,8 +183,23 @@ data class ComposeState( } } + val attachmentPreview: Boolean + get() = when (preview) { + ComposePreview.NoPreview -> false + is ComposePreview.CLinkPreview -> false + is ComposePreview.MediaPreview -> preview.content.isNotEmpty() + is ComposePreview.VoicePreview -> false + is ComposePreview.FilePreview -> true + } + + val placeholder: String + get() = when (contextItem) { + is ComposeContextItem.ReportedItem -> contextItem.reason.text + else -> generalGetString(MR.strings.compose_message_placeholder) + } + val empty: Boolean - get() = message.isEmpty() && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem + get() = message.text.isEmpty() && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem companion object { fun saver(): Saver<MutableState<ComposeState>, *> = Saver( @@ -126,6 +209,18 @@ data class ComposeState( } ) } + + fun mentionMemberName(name: String): String { + var n = 0 + var tryName = name + + while (mentions.containsKey(tryName)) { + n++ + tryName = "${name}_$n" + } + + return tryName + } } private val maxFileSize = getMaxFileSize(FileProtocol.XFTP) @@ -149,6 +244,7 @@ fun chatItemPreview(chatItem: ChatItem): ComposePreview { is MsgContent.MCVideo -> ComposePreview.MediaPreview(images = listOf(mc.image), listOf(UploadContent.SimpleImage(getAppFileUri(fileName)))) is MsgContent.MCVoice -> ComposePreview.VoicePreview(voice = fileName, mc.duration / 1000, true) is MsgContent.MCFile -> ComposePreview.FilePreview(fileName, getAppFileUri(fileName)) + is MsgContent.MCReport -> ComposePreview.NoPreview is MsgContent.MCUnknown, null -> ComposePreview.NoPreview } } @@ -178,7 +274,7 @@ fun MutableState<ComposeState>.processPickedFile(uri: URI?, text: String?) { if (fileSize != null && fileSize <= maxFileSize) { val fileName = getFileName(uri) if (fileName != null) { - value = value.copy(message = text ?: value.message, preview = ComposePreview.FilePreview(fileName, uri)) + value = value.copy(message = if (text != null) ComposeMessage(text) else value.message, preview = ComposePreview.FilePreview(fileName, uri)) } } else if (fileSize != null) { AlertManager.shared.showAlertMsg( @@ -231,7 +327,7 @@ suspend fun MutableState<ComposeState>.processPickedMedia(uris: List<URI>, text: } } if (imagesPreview.isNotEmpty()) { - value = value.copy(message = text ?: value.message, preview = ComposePreview.MediaPreview(imagesPreview, content)) + value = value.copy(message = if (text != null) ComposeMessage(text) else value.message, preview = ComposePreview.MediaPreview(imagesPreview, content)) } } @@ -241,33 +337,36 @@ fun ComposeView( chat: Chat, composeState: MutableState<ComposeState>, attachmentOption: MutableState<AttachmentOption?>, - showChooseAttachment: () -> Unit + showChooseAttachment: () -> Unit, + focusRequester: FocusRequester?, ) { + val cancelledLinks = rememberSaveable { mutableSetOf<String>() } + fun isSimplexLink(link: String): Boolean = + link.startsWith("https://simplex.chat", true) || link.startsWith("http://simplex.chat", true) + + fun getSimplexLink(parsedMsg: List<FormattedText>?): Pair<String?, Boolean> { + if (parsedMsg == null) return null to false + val link = parsedMsg.firstOrNull { ft -> ft.format is Format.Uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) } + val simplexLink = parsedMsg.any { ft -> ft.format is Format.SimplexLink } + return link?.text to simplexLink + } + val linkUrl = rememberSaveable { mutableStateOf<String?>(null) } + // default value parsed because of draft + val hasSimplexLink = rememberSaveable { mutableStateOf(getSimplexLink(parseToMarkdown(composeState.value.message.text)).second) } val prevLinkUrl = rememberSaveable { mutableStateOf<String?>(null) } val pendingLinkUrl = rememberSaveable { mutableStateOf<String?>(null) } - val cancelledLinks = rememberSaveable { mutableSetOf<String>() } val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() val saveLastDraft = chatModel.controller.appPrefs.privacySaveLastDraft.get() val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) val textStyle = remember(MaterialTheme.colors.isLight) { mutableStateOf(smallFont) } val recState: MutableState<RecordingState> = remember { mutableStateOf(RecordingState.NotStarted) } - AttachmentSelection(composeState, attachmentOption, composeState::processPickedFile) { uris, text -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(uris, text) } } - fun isSimplexLink(link: String): Boolean = - link.startsWith("https://simplex.chat", true) || link.startsWith("http://simplex.chat", true) - - fun parseMessage(msg: String): String? { - val parsedMsg = parseToMarkdown(msg) - val link = parsedMsg?.firstOrNull { ft -> ft.format is Format.Uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) } - return link?.text - } - fun loadLinkPreview(url: String, wait: Long? = null) { if (pendingLinkUrl.value == url) { composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(null)) - withApi { + withLongRunningApi(slow = 60_000) { if (wait != null) delay(wait) val lp = getLinkPreview(url) if (lp != null && pendingLinkUrl.value == url) { @@ -281,9 +380,11 @@ fun ComposeView( } } - fun showLinkPreview(s: String) { + fun showLinkPreview(parsedMessage: List<FormattedText>?) { prevLinkUrl.value = linkUrl.value - linkUrl.value = parseMessage(s) + val linkParsed = getSimplexLink(parsedMessage) + linkUrl.value = linkParsed.first + hasSimplexLink.value = linkParsed.second val url = linkUrl.value if (url != null) { if (url != composeState.value.linkPreview?.uri && url != pendingLinkUrl.value) { @@ -338,6 +439,7 @@ fun ComposeView( is SharedContent.Media -> shared.uris.map { it.toString() } is SharedContent.File -> listOf(shared.uri.toString()) is SharedContent.Text -> emptyList() + is SharedContent.Forward -> 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 -> @@ -351,40 +453,78 @@ fun ComposeView( } } - suspend fun send(chat: Chat, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? { + suspend fun send(chat: Chat, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?, mentions: Map<String, Long>): ChatItem? { val cInfo = chat.chatInfo - val aChatItem = chatModel.controller.apiSendMessage( - rh = chat.remoteHostId, - type = cInfo.chatType, - id = cInfo.apiId, - file = file, - quotedItemId = quoted, - mc = mc, - live = live, - ttl = ttl - ) - if (aChatItem != null) { - chatModel.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem) - return aChatItem.chatItem + val chatItems = if (chat.chatInfo.chatType == ChatType.Local) + chatModel.controller.apiCreateChatItems( + rh = chat.remoteHostId, + noteFolderId = chat.chatInfo.apiId, + composedMessages = listOf(ComposedMessage(file, null, mc, mentions)) + ) + else + chatModel.controller.apiSendMessages( + rh = chat.remoteHostId, + type = cInfo.chatType, + id = cInfo.apiId, + live = live, + ttl = ttl, + composedMessages = listOf(ComposedMessage(file, quoted, mc, mentions)) + ) + if (!chatItems.isNullOrEmpty()) { + chatItems.forEach { aChatItem -> + withContext(Dispatchers.Main) { + chatModel.chatsContext.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem) + } + } + return chatItems.first().chatItem } if (file != null) removeFile(file.filePath) return null } - suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? { + suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): List<ChatItem>? { val cInfo = chat.chatInfo val cs = composeState.value - var sent: ChatItem? - val msgText = text ?: cs.message + var sent: List<ChatItem>? + var lastMessageFailedToSend: ComposeState? = null + val msgText = text ?: cs.message.text fun sending() { composeState.value = composeState.value.copy(inProgress = true) } + suspend fun forwardItem(rhId: Long?, forwardedItem: List<ChatItem>, fromChatInfo: ChatInfo, ttl: Int?): List<ChatItem>? { + val chatItems = controller.apiForwardChatItems( + rh = rhId, + toChatType = chat.chatInfo.chatType, + toChatId = chat.chatInfo.apiId, + fromChatType = fromChatInfo.chatType, + fromChatId = fromChatInfo.apiId, + itemIds = forwardedItem.map { it.id }, + ttl = ttl + ) + + withContext(Dispatchers.Main) { + chatItems?.forEach { chatItem -> + chatModel.chatsContext.addChatItem(rhId, chat.chatInfo, chatItem) + } + } + + if (chatItems != null && chatItems.count() < forwardedItem.count()) { + AlertManager.shared.showAlertMsg( + title = String.format(generalGetString(MR.strings.forward_files_messages_deleted_after_selection_title), forwardedItem.count() - chatItems.count()), + text = generalGetString(MR.strings.forward_files_messages_deleted_after_selection_desc) + ) + } + + return chatItems + } + fun checkLinkPreview(): MsgContent { return when (val composePreview = cs.preview) { is ComposePreview.CLinkPreview -> { - val url = parseMessage(msgText) + val parsedMsg = parseToMarkdown(msgText) + val url = getSimplexLink(parsedMsg).first val lp = composePreview.linkPreview if (lp != null && url == lp.uri) { MsgContent.MCLink(msgText, preview = lp) @@ -397,6 +537,19 @@ fun ComposeView( } } + fun constructFailedMessage(cs: ComposeState): ComposeState { + val preview = when (cs.preview) { + is ComposePreview.MediaPreview -> { + ComposePreview.MediaPreview( + if (cs.preview.images.isNotEmpty()) listOf(cs.preview.images.last()) else emptyList(), + if (cs.preview.content.isNotEmpty()) listOf(cs.preview.content.last()) else emptyList() + ) + } + else -> cs.preview + } + return cs.copy(inProgress = false, preview = preview) + } + fun updateMsgContent(msgContent: MsgContent): MsgContent { return when (msgContent) { is MsgContent.MCText -> checkLinkPreview() @@ -405,15 +558,31 @@ fun ComposeView( is MsgContent.MCVideo -> MsgContent.MCVideo(msgText, image = msgContent.image, duration = msgContent.duration) is MsgContent.MCVoice -> MsgContent.MCVoice(msgText, duration = msgContent.duration) is MsgContent.MCFile -> MsgContent.MCFile(msgText) + is MsgContent.MCReport -> MsgContent.MCReport(msgText, reason = msgContent.reason) is MsgContent.MCUnknown -> MsgContent.MCUnknown(type = msgContent.type, text = msgText, json = msgContent.json) } } + suspend fun sendReport(reportReason: ReportReason, chatItemId: Long): List<ChatItem>? { + val cItems = chatModel.controller.apiReportMessage(chat.remoteHostId, chat.chatInfo.apiId, chatItemId, reportReason, msgText) + if (cItems != null) { + withContext(Dispatchers.Main) { + cItems.forEach { chatItem -> + chatModel.chatsContext.addChatItem(chat.remoteHostId, chat.chatInfo, chatItem.chatItem) + } + } + } + + return cItems?.map { it.chatItem } + } + suspend fun sendMemberContactInvitation() { val mc = checkLinkPreview() val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc) if (contact != null) { - chatModel.updateContact(chat.remoteHostId, contact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(chat.remoteHostId, contact) + } } } @@ -426,10 +595,14 @@ fun ComposeView( type = cInfo.chatType, id = cInfo.apiId, itemId = ei.meta.itemId, - mc = updateMsgContent(oldMsgContent), + updatedMessage = UpdatedMessage(updateMsgContent(oldMsgContent), cs.memberMentions), live = live ) - if (updatedItem != null) chatModel.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem) + if (updatedItem != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem) + } + } return updatedItem?.chatItem } return null @@ -440,16 +613,38 @@ fun ComposeView( if (liveMessage != null) composeState.value = cs.copy(liveMessage = null) sending() } - clearCurrentDraft() + if (!cs.forwarding || chatModel.draft.value?.forwarding == true) { + clearCurrentDraft() + } if (chat.nextSendGrpInv) { sendMemberContactInvitation() sent = null - } else if (cs.contextItem is ComposeContextItem.EditingItem) { + } else if (cs.contextItem is ComposeContextItem.ForwardingItems) { + sent = forwardItem(chat.remoteHostId, cs.contextItem.chatItems, cs.contextItem.fromChatInfo, ttl = ttl) + if (sent == null) { + lastMessageFailedToSend = constructFailedMessage(cs) + } + if (cs.message.text.isNotEmpty()) { + sent?.mapIndexed { index, message -> + if (index == sent!!.lastIndex) { + send(chat, checkLinkPreview(), quoted = message.id, live = false, ttl = ttl, mentions = cs.memberMentions) + } else { + message + } + } + } + } + else if (cs.contextItem is ComposeContextItem.EditingItem) { val ei = cs.contextItem.chatItem - sent = updateMessage(ei, chat, live) + val updatedMessage = updateMessage(ei, chat, live) + sent = if (updatedMessage != null) listOf(updatedMessage) else null + lastMessageFailedToSend = if (updatedMessage == null) constructFailedMessage(cs) else null } else if (liveMessage != null && liveMessage.sent) { - sent = updateMessage(liveMessage.chatItem, chat, live) + val updatedMessage = updateMessage(liveMessage.chatItem, chat, live) + sent = if (updatedMessage != null) listOf(updatedMessage) else null + } else if (cs.contextItem is ComposeContextItem.ReportedItem) { + sent = sendReport(cs.contextItem.reason, cs.contextItem.chatItem.id) } else { val msgs: ArrayList<MsgContent> = ArrayList() val files: ArrayList<CryptoFile> = ArrayList() @@ -458,17 +653,17 @@ fun ComposeView( ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText)) is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview()) is ComposePreview.MediaPreview -> { + // TODO batch send: batch media previews preview.content.forEachIndexed { index, it -> - val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get() val file = when (it) { is UploadContent.SimpleImage -> - if (remoteHost == null) saveImage(it.uri, encrypted = encrypted) + if (remoteHost == null) saveImage(it.uri) else desktopSaveImageInTmp(it.uri) is UploadContent.AnimatedImage -> - if (remoteHost == null) saveAnimImage(it.uri, encrypted = encrypted) + if (remoteHost == null) saveAnimImage(it.uri) else CryptoFile.desktopPlain(it.uri) is UploadContent.Video -> - if (remoteHost == null) saveFileFromUri(it.uri, encrypted = false) + if (remoteHost == null) saveFileFromUri(it.uri, hiddenFileNamePrefix = "video") else CryptoFile.desktopPlain(it.uri) } if (file != null) { @@ -486,27 +681,38 @@ fun ComposeView( AudioPlayer.stop(tmpFile.absolutePath) if (remoteHost == null) { val actualFile = File(getAppFilePath(tmpFile.name.replaceAfter(RecorderInterface.extension, ""))) - files.add(withContext(Dispatchers.IO) { + val file = withContext(Dispatchers.IO) { if (chatController.appPrefs.privacyEncryptLocalFiles.get()) { - val args = encryptCryptoFile(tmpFile.absolutePath, actualFile.absolutePath) - tmpFile.delete() + val args = try { + encryptCryptoFile(tmpFile.absolutePath, actualFile.absolutePath) + } catch (e: Exception) { + Log.e(TAG, "Unable to encrypt plain file: " + e.stackTraceToString()) + AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString()) + return@withContext null + } finally { + tmpFile.delete() + } CryptoFile(actualFile.name, args) } else { Files.move(tmpFile.toPath(), actualFile.toPath()) CryptoFile.plain(actualFile.name) } - }) + } + if (file != null) { + files.add(file) + msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", preview.durationMs / 1000)) + } deleteUnusedFiles() } else { files.add(CryptoFile.plain(tmpFile.absolutePath)) // It will be deleted on JVM shutdown or next start (if the app crashes unexpectedly) filesToDelete.remove(tmpFile) + msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", preview.durationMs / 1000)) } - msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", preview.durationMs / 1000)) } is ComposePreview.FilePreview -> { val file = if (remoteHost == null) { - saveFileFromUri(preview.uri, encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get()) + saveFileFromUri(preview.uri) } else { CryptoFile.desktopPlain(preview.uri) } @@ -531,38 +737,60 @@ fun ComposeView( localPath = file.filePath ) } - sent = send(chat, content, if (index == 0) quotedItemId else null, file, + val sendResult = send(chat, content, if (index == 0) quotedItemId else null, file, live = if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false, - ttl = ttl + ttl = ttl, + mentions = cs.memberMentions ) - } - if (sent == null && - (cs.preview is ComposePreview.MediaPreview || - cs.preview is ComposePreview.FilePreview || - cs.preview is ComposePreview.VoicePreview) - ) { - sent = send(chat, MsgContent.MCText(msgText), quotedItemId, null, live, ttl) + sent = if (sendResult != null) listOf(sendResult) else null + if (sent == null && index == msgs.lastIndex && cs.liveMessage == null) { + constructFailedMessage(cs) + // it's the last message in the series so if it fails, restore it in ComposeView for editing + lastMessageFailedToSend = constructFailedMessage(cs) + } } } - clearState(live) + val wasForwarding = cs.forwarding + val forwardingFromChatId = (cs.contextItem as? ComposeContextItem.ForwardingItems)?.fromChatInfo?.id + val lastFailed = lastMessageFailedToSend + if (lastFailed == null) { + clearState(live) + } else { + composeState.value = lastFailed + } + val draft = chatModel.draft.value + if (wasForwarding && chatModel.draftChatId.value == chat.chatInfo.id && forwardingFromChatId != chat.chatInfo.id && draft != null) { + composeState.value = draft + } else { + clearCurrentDraft() + } return sent } fun sendMessage(ttl: Int?) { - withBGApi { + withLongRunningApi(slow = 120_000) { sendMessageAsync(null, false, ttl) } } - fun onMessageChange(s: String) { - composeState.value = composeState.value.copy(message = s) - if (isShortEmoji(s)) { - textStyle.value = if (s.codePoints().count() < 4) largeEmojiFont else mediumEmojiFont + fun onMessageChange(s: ComposeMessage) { + val parsedMessage = parseToMarkdown(s.text) + composeState.value = composeState.value.copy(message = s, parsedMessage = parsedMessage ?: FormattedText.plain(s.text)) + if (isShortEmoji(s.text)) { + textStyle.value = if (s.text.codePoints().count() < 4) largeEmojiFont else mediumEmojiFont } else { textStyle.value = smallFont if (composeState.value.linkPreviewAllowed) { - if (s.isNotEmpty()) showLinkPreview(s) - else resetLinkPreview() + if (s.text.isNotEmpty()) { + showLinkPreview(parsedMessage) + } else { + resetLinkPreview() + hasSimplexLink.value = false + } + } else if (s.text.isNotEmpty() && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks)) { + hasSimplexLink.value = getSimplexLink(parsedMessage).second + } else { + hasSimplexLink.value = false } } } @@ -575,12 +803,16 @@ fun ComposeView( fun allowVoiceToContact() { val contact = (chat.chatInfo as ChatInfo.Direct?)?.contact ?: return - withApi { + withBGApi { chatModel.controller.allowFeatureToContact(chat.remoteHostId, contact, ChatFeature.Voice) } } fun cancelLinkPreview() { + val pendingLink = pendingLinkUrl.value + if (pendingLink != null) { + cancelledLinks.add(pendingLink) + } val uri = composeState.value.linkPreview?.uri if (uri != null) { cancelledLinks.add(uri) @@ -624,11 +856,11 @@ fun ComposeView( suspend fun sendLiveMessage() { val cs = composeState.value - val typedMsg = cs.message + val typedMsg = cs.message.text if ((cs.sendEnabled() || cs.contextItem is ComposeContextItem.QuotedItem) && (cs.liveMessage == null || !cs.liveMessage.sent)) { val ci = sendMessageAsync(typedMsg, live = true, ttl = null) - if (ci != null) { - composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = typedMsg, sent = true)) + if (!ci.isNullOrEmpty()) { + composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci.last(), typedMsg = typedMsg, sentMsg = typedMsg, sent = true)) } } else if (cs.liveMessage == null) { val cItem = chatModel.addLiveDummy(chat.chatInfo) @@ -645,21 +877,21 @@ fun ComposeView( val typedMsg = composeState.value.message val liveMessage = composeState.value.liveMessage if (liveMessage != null) { - val sentMsg = liveMessageToSend(liveMessage, typedMsg) + val sentMsg = liveMessageToSend(liveMessage, typedMsg.text) if (sentMsg != null) { val ci = sendMessageAsync(sentMsg, live = true, ttl = null) - if (ci != null) { - composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg, sent = true)) + if (!ci.isNullOrEmpty()) { + composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci.last(), typedMsg = typedMsg.text, sentMsg = sentMsg, sent = true)) } - } else if (liveMessage.typedMsg != typedMsg) { - composeState.value = composeState.value.copy(liveMessage = liveMessage.copy(typedMsg = typedMsg)) + } else if (liveMessage.typedMsg != typedMsg.text) { + composeState.value = composeState.value.copy(liveMessage = liveMessage.copy(typedMsg = typedMsg.text)) } } } fun editPrevMessage() { if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return - val lastEditable = chatModel.chatItems.findLast { it.meta.editable } + val lastEditable = chatModel.chatsContext.chatItems.value.findLast { it.meta.editable } if (lastEditable != null) { composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews) } @@ -694,16 +926,51 @@ fun ComposeView( } } + @Composable + fun MsgNotAllowedView(reason: String, icon: Painter) { + val color = MaterialTheme.appColors.receivedQuote + Row(Modifier.fillMaxWidth().background(color).padding(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF * 1.5f), verticalAlignment = Alignment.CenterVertically) { + Icon(icon, null, tint = MaterialTheme.colors.secondary) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Text(reason, fontStyle = FontStyle.Italic) + } + } + + @Composable + fun ReportReasonView(reason: ReportReason) { + val reportText = when (reason) { + is ReportReason.Spam -> generalGetString(MR.strings.report_compose_reason_header_spam) + is ReportReason.Illegal -> generalGetString(MR.strings.report_compose_reason_header_illegal) + is ReportReason.Profile -> generalGetString(MR.strings.report_compose_reason_header_profile) + is ReportReason.Community -> generalGetString(MR.strings.report_compose_reason_header_community) + is ReportReason.Other -> generalGetString(MR.strings.report_compose_reason_header_other) + is ReportReason.Unknown -> null // should never happen + } + + if (reportText != null) { + val color = MaterialTheme.appColors.receivedQuote + Row(Modifier.fillMaxWidth().background(color).padding(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF * 1.5f), verticalAlignment = Alignment.CenterVertically) { + Text(reportText, fontStyle = FontStyle.Italic, fontSize = 12.sp) + } + } + } + @Composable fun contextItemView() { when (val contextItem = composeState.value.contextItem) { ComposeContextItem.NoContextItem -> {} - is ComposeContextItem.QuotedItem -> ContextItemView(contextItem.chatItem, painterResource(MR.images.ic_reply)) { + is ComposeContextItem.QuotedItem -> ContextItemView(listOf(contextItem.chatItem), painterResource(MR.images.ic_reply), chatInfo = chat.chatInfo) { composeState.value = composeState.value.copy(contextItem = ComposeContextItem.NoContextItem) } - is ComposeContextItem.EditingItem -> ContextItemView(contextItem.chatItem, painterResource(MR.images.ic_edit_filled)) { + is ComposeContextItem.EditingItem -> ContextItemView(listOf(contextItem.chatItem), painterResource(MR.images.ic_edit_filled), chatInfo = chat.chatInfo) { clearState() } + is ComposeContextItem.ForwardingItems -> ContextItemView(contextItem.chatItems, painterResource(MR.images.ic_forward), showSender = false, chatInfo = chat.chatInfo) { + composeState.value = composeState.value.copy(contextItem = ComposeContextItem.NoContextItem) + } + is ComposeContextItem.ReportedItem -> ContextItemView(listOf(contextItem.chatItem), painterResource(MR.images.ic_flag), chatInfo = chat.chatInfo, contextIconColor = Color.Red) { + composeState.value = composeState.value.copy(contextItem = ComposeContextItem.NoContextItem) + } } } @@ -720,24 +987,41 @@ fun ComposeView( if (chatModel.chatId.value == null) return@LaunchedEffect when (val shared = chatModel.sharedContent.value) { - is SharedContent.Text -> onMessageChange(shared.text) + is SharedContent.Text -> onMessageChange(ComposeMessage(shared.text)) is SharedContent.Media -> composeState.processPickedMedia(shared.uris, shared.text) is SharedContent.File -> composeState.processPickedFile(shared.uri, shared.text) + is SharedContent.Forward -> composeState.value = composeState.value.copy( + contextItem = ComposeContextItem.ForwardingItems(shared.chatItems, shared.fromChatInfo), + preview = if (composeState.value.preview is ComposePreview.CLinkPreview) composeState.value.preview else ComposePreview.NoPreview + ) null -> {} } chatModel.sharedContent.value = null } - val userCanSend = rememberUpdatedState(chat.userCanSend) val sendMsgEnabled = rememberUpdatedState(chat.chatInfo.sendMsgEnabled) - val userIsObserver = rememberUpdatedState(chat.userIsObserver) + val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason) val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv) Column { if (nextSendGrpInv.value) { ComposeContextInvitingContactMemberView() } + val ctx = composeState.value.contextItem + if (ctx is ComposeContextItem.ReportedItem) { + ReportReasonView(ctx.reason) + } + val simplexLinkProhibited = hasSimplexLink.value && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks) + val fileProhibited = composeState.value.attachmentPreview && !chat.groupFeatureEnabled(GroupFeature.Files) + val voiceProhibited = composeState.value.preview is ComposePreview.VoicePreview && !chat.chatInfo.featureEnabled(ChatFeature.Voice) if (composeState.value.preview !is ComposePreview.VoicePreview || composeState.value.editing) { + if (simplexLinkProhibited) { + MsgNotAllowedView(generalGetString(MR.strings.simplex_links_not_allowed), icon = painterResource(MR.images.ic_link)) + } else if (fileProhibited) { + MsgNotAllowedView(generalGetString(MR.strings.files_and_media_not_allowed), icon = painterResource(MR.images.ic_draft)) + } else if (voiceProhibited) { + MsgNotAllowedView(generalGetString(MR.strings.voice_messages_not_allowed), icon = painterResource(MR.images.ic_mic)) + } contextItemView() when { composeState.value.editing && composeState.value.preview is ComposePreview.VoicePreview -> {} @@ -754,141 +1038,152 @@ fun ComposeView( } } } - Row( - modifier = Modifier.padding(end = 8.dp), - verticalAlignment = Alignment.Bottom, - ) { - val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on - val attachmentClicked = if (isGroupAndProhibitedFiles) { - { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.files_and_media_prohibited), - text = generalGetString(MR.strings.only_owners_can_enable_files_and_media) + Surface(color = MaterialTheme.colors.background, contentColor = MaterialTheme.colors.onBackground) { + Divider() + Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) { + val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership) + val attachmentClicked = if (isGroupAndProhibitedFiles) { + { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.files_and_media_prohibited), + text = generalGetString(MR.strings.only_owners_can_enable_files_and_media) + ) + } + } else { + showChooseAttachment + } + val attachmentEnabled = + !composeState.value.attachmentDisabled + && sendMsgEnabled.value + && !isGroupAndProhibitedFiles + && !nextSendGrpInv.value + IconButton( + attachmentClicked, + Modifier.padding(start = 3.dp, end = 1.dp, bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier), + enabled = attachmentEnabled + ) { + Icon( + painterResource(MR.images.ic_attach_file_filled_500), + contentDescription = stringResource(MR.strings.attach), + tint = if (attachmentEnabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + modifier = Modifier + .size(28.dp) + .clip(CircleShape) ) } - } else { - showChooseAttachment - } - val attachmentEnabled = - !composeState.value.attachmentDisabled - && sendMsgEnabled.value - && userCanSend.value - && !isGroupAndProhibitedFiles - && !nextSendGrpInv.value - IconButton( - attachmentClicked, - Modifier.padding(bottom = if (appPlatform.isAndroid) 0.dp else 7.dp), - enabled = attachmentEnabled - ) { - Icon( - painterResource(MR.images.ic_attach_file_filled_500), - contentDescription = stringResource(MR.strings.attach), - tint = if (attachmentEnabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, - modifier = Modifier - .size(28.dp) - .clip(CircleShape) - ) - } - val allowedVoiceByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Voice) } - LaunchedEffect(allowedVoiceByPrefs) { - if (!allowedVoiceByPrefs && composeState.value.preview is ComposePreview.VoicePreview) { - // Voice was disabled right when this user records it, just cancel it - cancelVoice() + val allowedVoiceByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Voice) } + LaunchedEffect(allowedVoiceByPrefs) { + if (!allowedVoiceByPrefs && composeState.value.preview is ComposePreview.VoicePreview) { + // Voice was disabled right when this user records it, just cancel it + cancelVoice() + } } - } - val needToAllowVoiceToContact = remember(chat.chatInfo) { - chat.chatInfo is ChatInfo.Direct && with(chat.chatInfo.contact.mergedPreferences.voice) { - ((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) && - contactPreference.allow == FeatureAllowed.YES + val needToAllowVoiceToContact = remember(chat.chatInfo) { + chat.chatInfo is ChatInfo.Direct && with(chat.chatInfo.contact.mergedPreferences.voice) { + ((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) && + contactPreference.allow == FeatureAllowed.YES + } } - } - LaunchedEffect(Unit) { - snapshotFlow { recState.value } - .distinctUntilChanged() - .collect { - when(it) { - is RecordingState.Started -> onAudioAdded(it.filePath, it.progressMs, false) - is RecordingState.Finished -> if (it.durationMs > 300) { - onAudioAdded(it.filePath, it.durationMs, true) - } else { - cancelVoice() + LaunchedEffect(Unit) { + snapshotFlow { recState.value } + .distinctUntilChanged() + .collect { + when (it) { + is RecordingState.Started -> onAudioAdded(it.filePath, it.progressMs, false) + is RecordingState.Finished -> if (it.durationMs > 300) { + onAudioAdded(it.filePath, it.durationMs, true) + } else { + cancelVoice() + } + is RecordingState.NotStarted -> {} + } + } + } + + LaunchedEffect(rememberUpdatedState(chat.chatInfo.sendMsgEnabled).value) { + if (!chat.chatInfo.sendMsgEnabled) { + clearCurrentDraft() + clearState() + } + } + + KeyChangeEffect(chatModel.chatId.value) { prevChatId -> + val cs = composeState.value + if (cs.liveMessage != null && (cs.message.text.isNotEmpty() || cs.liveMessage.sent)) { + sendMessage(null) + resetLinkPreview() + clearPrevDraft(prevChatId) + deleteUnusedFiles() + } else if (cs.inProgress) { + clearPrevDraft(prevChatId) + } else if (!cs.empty) { + if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) { + composeState.value = cs.copy(preview = cs.preview.copy(finished = true)) + } + if (saveLastDraft) { + chatModel.draft.value = composeState.value + chatModel.draftChatId.value = prevChatId + } + composeState.value = ComposeState(useLinkPreviews = useLinkPreviews) + } else if (chatModel.draftChatId.value == chatModel.chatId.value && chatModel.draft.value != null) { + composeState.value = chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews) + } else { + clearPrevDraft(prevChatId) + deleteUnusedFiles() + } + chatModel.removeLiveDummy() + CIFile.cachedRemoteFileRequests.clear() + } + if (appPlatform.isDesktop) { + // Don't enable this on Android, it breaks it, This method only works on desktop. For Android there is a `KeyChangeEffect(chatModel.chatId.value)` + DisposableEffect(Unit) { + onDispose { + if (chatModel.sharedContent.value is SharedContent.Forward && saveLastDraft && !composeState.value.empty) { + chatModel.draft.value = composeState.value + chatModel.draftChatId.value = chat.id } - is RecordingState.NotStarted -> {} } } - } - - LaunchedEffect(rememberUpdatedState(chat.userCanSend).value) { - if (!chat.userCanSend) { - clearCurrentDraft() - clearState() } + val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) } + val sendButtonColor = + if (chat.chatInfo.incognito) + if (isInDarkTheme()) Indigo else Indigo.copy(alpha = 0.7F) + else MaterialTheme.colors.primary + SendMsgView( + composeState, + showVoiceRecordIcon = true, + recState, + chat.chatInfo is ChatInfo.Direct, + liveMessageAlertShown = chatModel.controller.appPrefs.liveMessageAlertShown, + sendMsgEnabled = sendMsgEnabled.value, + userCantSendReason = userCantSendReason.value, + sendButtonEnabled = sendMsgEnabled.value && !(simplexLinkProhibited || fileProhibited || voiceProhibited), + nextSendGrpInv = nextSendGrpInv.value, + needToAllowVoiceToContact, + allowedVoiceByPrefs, + allowVoiceToContact = ::allowVoiceToContact, + sendButtonColor = sendButtonColor, + timedMessageAllowed = timedMessageAllowed, + customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime, + placeholder = composeState.value.placeholder, + sendMessage = { ttl -> + sendMessage(ttl) + resetLinkPreview() + }, + sendLiveMessage = if (chat.chatInfo.chatType != ChatType.Local) ::sendLiveMessage else null, + updateLiveMessage = ::updateLiveMessage, + cancelLiveMessage = { + composeState.value = composeState.value.copy(liveMessage = null) + chatModel.removeLiveDummy() + }, + editPrevMessage = ::editPrevMessage, + onFilesPasted = { composeState.onFilesAttached(it) }, + onMessageChange = ::onMessageChange, + textStyle = textStyle, + focusRequester = focusRequester, + ) } - - KeyChangeEffect(chatModel.chatId.value) { prevChatId -> - val cs = composeState.value - if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) { - sendMessage(null) - resetLinkPreview() - clearPrevDraft(prevChatId) - deleteUnusedFiles() - } else if (cs.inProgress) { - clearPrevDraft(prevChatId) - } else if (!cs.empty) { - if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) { - composeState.value = cs.copy(preview = cs.preview.copy(finished = true)) - } - if (saveLastDraft) { - chatModel.draft.value = composeState.value - chatModel.draftChatId.value = prevChatId - } - composeState.value = ComposeState(useLinkPreviews = useLinkPreviews) - } else if (chatModel.draftChatId.value == chatModel.chatId.value && chatModel.draft.value != null) { - composeState.value = chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews) - } else { - clearPrevDraft(prevChatId) - deleteUnusedFiles() - } - chatModel.removeLiveDummy() - CIFile.cachedRemoteFileRequests.clear() - } - - val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) } - val sendButtonColor = - if (chat.chatInfo.incognito) - if (isSystemInDarkTheme()) Indigo else Indigo.copy(alpha = 0.7F) - else MaterialTheme.colors.primary - SendMsgView( - composeState, - showVoiceRecordIcon = true, - recState, - chat.chatInfo is ChatInfo.Direct, - liveMessageAlertShown = chatModel.controller.appPrefs.liveMessageAlertShown, - sendMsgEnabled = sendMsgEnabled.value, - nextSendGrpInv = nextSendGrpInv.value, - needToAllowVoiceToContact, - allowedVoiceByPrefs, - allowVoiceToContact = ::allowVoiceToContact, - userIsObserver = userIsObserver.value, - userCanSend = userCanSend.value, - sendButtonColor = sendButtonColor, - timedMessageAllowed = timedMessageAllowed, - customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime, - sendMessage = { ttl -> - sendMessage(ttl) - resetLinkPreview() - }, - sendLiveMessage = ::sendLiveMessage, - updateLiveMessage = ::updateLiveMessage, - cancelLiveMessage = { - composeState.value = composeState.value.copy(liveMessage = null) - chatModel.removeLiveDummy() - }, - editPrevMessage = ::editPrevMessage, - onFilesPasted = { composeState.onFilesAttached(it) }, - onMessageChange = ::onMessageChange, - textStyle = textStyle - ) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt index a4c90d30dd..b070dce1d1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt @@ -35,7 +35,7 @@ fun ComposeVoiceView( ) { val progress = rememberSaveable { mutableStateOf(0) } val duration = rememberSaveable(recordedDurationMs) { mutableStateOf(recordedDurationMs) } - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val sentColor = MaterialTheme.appColors.sentMessage Box { Box( Modifier @@ -53,7 +53,7 @@ fun ComposeVoiceView( IconButton( onClick = { if (!audioPlaying.value) { - AudioPlayer.play(CryptoFile.plain(filePath), audioPlaying, progress, duration, false) + AudioPlayer.play(CryptoFile.plain(filePath), audioPlaying, progress, duration, resetOnEnd = false, smallView = false) } else { AudioPlayer.pause(audioPlaying, progress) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt index c12982ada5..7c04c30f67 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt @@ -12,14 +12,16 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.PreferenceToggle import chat.simplex.common.model.* +import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.chatModel import chat.simplex.res.MR +import kotlinx.coroutines.* @Composable fun ContactPreferencesView( @@ -31,16 +33,18 @@ fun ContactPreferencesView( ) { val contact = remember { derivedStateOf { (m.getContactChat(contactId)?.chatInfo as? ChatInfo.Direct)?.contact } } val ct = contact.value ?: return - var featuresAllowed by rememberSaveable(ct, stateSaver = serializableSaver()) { mutableStateOf(contactUserPrefsToFeaturesAllowed(ct.mergedPreferences)) } - var currentFeaturesAllowed by rememberSaveable(ct, stateSaver = serializableSaver()) { mutableStateOf(featuresAllowed) } + var featuresAllowed by rememberSaveable(ct, user, stateSaver = serializableSaver()) { mutableStateOf(contactUserPrefsToFeaturesAllowed(ct.mergedPreferences)) } + var currentFeaturesAllowed by rememberSaveable(ct, user, stateSaver = serializableSaver()) { mutableStateOf(featuresAllowed) } fun savePrefs(afterSave: () -> Unit = {}) { - withApi { + withBGApi { val prefs = contactFeaturesAllowedToPrefs(featuresAllowed) val toContact = m.controller.apiSetContactPrefs(rhId, ct.contactId, prefs) if (toContact != null) { - m.updateContact(rhId, toContact) - currentFeaturesAllowed = featuresAllowed + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, toContact) + currentFeaturesAllowed = featuresAllowed + } } afterSave() } @@ -77,11 +81,7 @@ private fun ContactPreferencesLayout( reset: () -> Unit, savePrefs: () -> Unit, ) { - Column( - Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.contact_preferences)) val timedMessages: MutableState<Boolean> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) } val onTTLUpdated = { ttl: Int? -> @@ -90,22 +90,22 @@ private fun ContactPreferencesLayout( TimedMessagesFeatureSection(featuresAllowed, contact.mergedPreferences.timedMessages, timedMessages, onTTLUpdated) { allowed, ttl -> applyPrefs(featuresAllowed.copy(timedMessagesAllowed = allowed, timedMessagesTTL = ttl ?: currentFeaturesAllowed.timedMessagesTTL)) } - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced(true) val allowFullDeletion: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.fullDelete) } FeatureSection(ChatFeature.FullDelete, user.fullPreferences.fullDelete.allow, contact.mergedPreferences.fullDelete, allowFullDeletion) { applyPrefs(featuresAllowed.copy(fullDelete = it)) } - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced(true) val allowReactions: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.reactions) } FeatureSection(ChatFeature.Reactions, user.fullPreferences.reactions.allow, contact.mergedPreferences.reactions, allowReactions) { applyPrefs(featuresAllowed.copy(reactions = it)) } - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced(true) val allowVoice: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.voice) } FeatureSection(ChatFeature.Voice, user.fullPreferences.voice.allow, contact.mergedPreferences.voice, allowVoice) { applyPrefs(featuresAllowed.copy(voice = it)) } - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced(true) val allowCalls: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.calls) } FeatureSection(ChatFeature.Calls, user.fullPreferences.calls.allow, contact.mergedPreferences.calls, allowCalls) { applyPrefs(featuresAllowed.copy(calls = it)) 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 49203c7cfb..1501fb7938 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 @@ -4,45 +4,94 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.ui.text.TextStyle +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.* +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.item.* import chat.simplex.common.model.* +import chat.simplex.common.platform.getLoadedFilePath +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import dev.icerock.moko.resources.ImageResource import kotlinx.datetime.Clock @Composable fun ContextItemView( - contextItem: ChatItem, + contextItems: List<ChatItem>, contextIcon: Painter, - cancelContextItem: () -> Unit + showSender: Boolean = true, + chatInfo: ChatInfo, + contextIconColor: Color = MaterialTheme.colors.secondary, + cancelContextItem: () -> Unit, ) { - val sent = contextItem.chatDir.sent - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage @Composable - fun msgContentView(lines: Int) { + fun MessageText(contextItem: ChatItem, attachment: ImageResource?, lines: Int) { + val inlineContent: Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>>? = if (attachment != null) { + remember(contextItem.id) { + val inlineContentBuilder: AnnotatedString.Builder.() -> Unit = { + appendInlineContent(id = "attachmentIcon") + append(" ") + } + val inlineContent = mapOf( + "attachmentIcon" to InlineTextContent( + Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter) + ) { + Icon(painterResource(attachment), null, tint = MaterialTheme.colors.secondary) + } + ) + inlineContentBuilder to inlineContent + } + } else null MarkdownText( contextItem.text, contextItem.formattedText, + sender = null, + toggleSecrets = false, maxLines = lines, + inlineContent = inlineContent, linkMode = SimplexLinkMode.DESCRIPTION, modifier = Modifier.fillMaxWidth(), + mentions = contextItem.mentions, + userMemberId = when { + chatInfo is ChatInfo.Group -> chatInfo.groupInfo.membership.memberId + else -> null + } ) } + fun attachment(contextItem: ChatItem): ImageResource? { + val fileIsLoaded = getLoadedFilePath(contextItem.file) != null + + return when (contextItem.content.msgContent) { + 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 + else -> null + } + } + + @Composable + fun ContextMsgPreview(contextItem: ChatItem, lines: Int) { + MessageText(contextItem, remember(contextItem.id) { attachment(contextItem) }, lines) + } + + val sent = contextItems[0].chatDir.sent + Row( Modifier - .padding(top = 8.dp) .background(if (sent) sentColor else receivedColor), verticalAlignment = Alignment.CenterVertically ) { @@ -60,22 +109,29 @@ fun ContextItemView( .height(20.dp) .width(20.dp), contentDescription = stringResource(MR.strings.icon_descr_context), - tint = MaterialTheme.colors.secondary, - ) - val sender = contextItem.memberDisplayName - if (sender != null) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - Text( - sender, - style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary) - ) - msgContentView(lines = 2) + tint = contextIconColor, + ) + + if (contextItems.count() == 1) { + val contextItem = contextItems[0] + val sender = contextItem.memberDisplayName + + if (showSender && sender != null) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + sender, + style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary) + ) + ContextMsgPreview(contextItem, lines = 2) + } + } else { + ContextMsgPreview(contextItem, lines = 3) } - } else { - msgContentView(lines = 3) + } else if (contextItems.isNotEmpty()) { + Text(String.format(generalGetString(if (chatInfo.chatType == ChatType.Local) MR.strings.compose_save_messages_n else MR.strings.compose_forward_messages_n), contextItems.count()), fontStyle = FontStyle.Italic) } } IconButton(onClick = cancelContextItem) { @@ -94,8 +150,9 @@ fun ContextItemView( fun PreviewContextItemView() { SimpleXTheme { ContextItemView( - contextItem = ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), "hello"), - contextIcon = painterResource(MR.images.ic_edit_filled) + contextItems = listOf(ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), "hello")), + contextIcon = painterResource(MR.images.ic_edit_filled), + chatInfo = Chat.sampleData.chatInfo ) {} } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt index bb479d8eb3..428d4b1b8f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt @@ -1,9 +1,11 @@ package chat.simplex.common.views.chat +import SectionBottomSpacer import androidx.compose.foundation.layout.* import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.QRCodeScanner @@ -11,31 +13,21 @@ import chat.simplex.res.MR import dev.icerock.moko.resources.compose.stringResource @Composable -fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) { - Column( - Modifier - .fillMaxSize() - .padding(horizontal = DEFAULT_PADDING) - ) { - AppBarTitle(stringResource(MR.strings.scan_code), withPadding = false) - Box( - Modifier - .fillMaxWidth() - .aspectRatio(ratio = 1F) - .padding(bottom = DEFAULT_PADDING) - ) { - QRCodeScanner { text -> - verifyCode(text) { - if (it) { - close() - } else { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.incorrect_code) - ) - } - } +fun ScanCodeView(verifyCode: suspend (String?) -> Boolean, close: () -> Unit) { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.scan_code)) + QRCodeScanner { text -> + val success = verifyCode(text) + if (success) { + close() + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.incorrect_code) + ) } + success } - Text(stringResource(MR.strings.scan_code_from_contacts_app)) + Text(stringResource(MR.strings.scan_code_from_contacts_app), Modifier.padding(horizontal = DEFAULT_PADDING)) + SectionBottomSpacer() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt new file mode 100644 index 0000000000..b9538bc691 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt @@ -0,0 +1,175 @@ +package chat.simplex.common.views.chat + +import androidx.compose.foundation.background +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.draw.alpha +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.* +import chat.simplex.common.platform.BackHandler +import chat.simplex.common.platform.chatModel +import chat.simplex.common.views.helpers.* +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource + +@Composable +fun BoxScope.SelectedItemsCounterToolbar(selectedItems: MutableState<Set<Long>?>, onTop: Boolean, selectAll: (() -> Unit)? = null) { + val onBackClicked = { selectedItems.value = null } + BackHandler(onBack = onBackClicked) + val count = selectedItems.value?.size ?: 0 + Box(if (onTop) Modifier else Modifier.imePadding()) { + DefaultAppBar( + navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) }, + title = { + Text( + if (count == 0) { + stringResource(MR.strings.selected_chat_items_nothing_selected) + } else { + stringResource(MR.strings.selected_chat_items_selected_n).format(count) + }, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + onTitleClick = null, + onTop = onTop, + onSearchValueChanged = {}, + buttons = if (selectAll != null) { { SelectAllButton(selectAll) } } else {{}} + ) + } +} + +@Composable +private fun SelectAllButton(onClick: () -> Unit) { + IconButton(onClick) { + Icon( + painterResource(MR.images.ic_checklist), stringResource(MR.strings.back), Modifier.height(24.dp), tint = MaterialTheme.colors.primary + ) + } +} + +@Composable +fun SelectedItemsButtonsToolbar( + chatsCtx: ChatModel.ChatsContext, + chatInfo: ChatInfo, + selectedChatItems: MutableState<Set<Long>?>, + deleteItems: (Boolean) -> Unit, // Boolean - delete for everyone is possible + archiveItems: () -> Unit, + moderateItems: () -> Unit, + forwardItems: () -> Unit, +) { + val deleteEnabled = remember { mutableStateOf(false) } + val deleteForEveryoneEnabled = remember { mutableStateOf(false) } + val canArchiveReports = remember { mutableStateOf(false) } + val canModerate = remember { mutableStateOf(false) } + val moderateEnabled = remember { mutableStateOf(false) } + val forwardEnabled = remember { mutableStateOf(false) } + val deleteCountProhibited = remember { mutableStateOf(false) } + val forwardCountProhibited = remember { mutableStateOf(false) } + Box { + // It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty + ComposeView(chatModel = chatModel, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() }) + Row( + Modifier + .matchParentSize() + .background(MaterialTheme.colors.background) + .padding(horizontal = 2.dp) + .height(AppBarHeight * fontSizeSqrtMultiplier) + .pointerInput(Unit) { + detectGesture { + true + } + }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton({ if (canArchiveReports.value) archiveItems() else deleteItems(deleteForEveryoneEnabled.value) }, enabled = deleteEnabled.value && !deleteCountProhibited.value) { + Icon( + painterResource(MR.images.ic_delete), + null, + Modifier.size(22.dp), + tint = if (!deleteEnabled.value || deleteCountProhibited.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error + ) + } + + IconButton({ moderateItems() }, Modifier.alpha(if (canModerate.value) 1f else 0f), enabled = moderateEnabled.value && !deleteCountProhibited.value) { + Icon( + painterResource(MR.images.ic_flag), + null, + Modifier.size(22.dp), + tint = if (!moderateEnabled.value || deleteCountProhibited.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error + ) + } + + IconButton({ forwardItems() }, enabled = forwardEnabled.value && !forwardCountProhibited.value) { + Icon( + painterResource(MR.images.ic_forward), + null, + Modifier.size(22.dp), + tint = if (!forwardEnabled.value || forwardCountProhibited.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + } + Divider(Modifier.align(Alignment.TopStart)) + } + val chatItems = remember { derivedStateOf { chatsCtx.chatItems.value } } + LaunchedEffect(chatInfo, chatItems.value, selectedChatItems.value) { + recheckItems(chatInfo, chatItems.value, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canArchiveReports, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited) + } +} + +private fun recheckItems(chatInfo: ChatInfo, + chatItems: List<ChatItem>, + selectedChatItems: MutableState<Set<Long>?>, + deleteEnabled: MutableState<Boolean>, + deleteForEveryoneEnabled: MutableState<Boolean>, + canArchiveReports: MutableState<Boolean>, + canModerate: MutableState<Boolean>, + moderateEnabled: MutableState<Boolean>, + forwardEnabled: MutableState<Boolean>, + deleteCountProhibited: MutableState<Boolean>, + forwardCountProhibited: MutableState<Boolean> +) { + val count = selectedChatItems.value?.size ?: 0 + deleteCountProhibited.value = count == 0 || count > 200 + forwardCountProhibited.value = count == 0 || count > 20 + canModerate.value = possibleToModerate(chatInfo) + val selected = selectedChatItems.value ?: return + var rDeleteEnabled = true + var rDeleteForEveryoneEnabled = true + var rCanArchiveReports = true + var rModerateEnabled = true + var rOnlyOwnGroupItems = true + var rForwardEnabled = true + val rSelectedChatItems = mutableSetOf<Long>() + for (ci in chatItems) { + if (selected.contains(ci.id)) { + rDeleteEnabled = rDeleteEnabled && ci.canBeDeletedForSelf + rDeleteForEveryoneEnabled = rDeleteForEveryoneEnabled && ci.meta.deletable && !ci.localNote && !ci.isReport + rCanArchiveReports = rCanArchiveReports && ci.isActiveReport && ci.chatDir !is CIDirection.GroupSnd && chatInfo is ChatInfo.Group && chatInfo.groupInfo.membership.memberRole >= GroupMemberRole.Moderator + rOnlyOwnGroupItems = rOnlyOwnGroupItems && ci.chatDir is CIDirection.GroupSnd && !ci.isReport + rModerateEnabled = rModerateEnabled && ci.content.msgContent != null && ci.memberToModerate(chatInfo) != null && !ci.isReport + rForwardEnabled = rForwardEnabled && ci.content.msgContent != null && ci.meta.itemDeleted == null && !ci.isLiveDummy && !ci.isReport + rSelectedChatItems.add(ci.id) // we are collecting new selected items here to account for any changes in chat items list + } + } + rModerateEnabled = rModerateEnabled && !rOnlyOwnGroupItems + deleteEnabled.value = rDeleteEnabled + deleteForEveryoneEnabled.value = rDeleteForEveryoneEnabled + canArchiveReports.value = rCanArchiveReports + moderateEnabled.value = rModerateEnabled + forwardEnabled.value = rForwardEnabled + selectedChatItems.value = rSelectedChatItems +} + +private fun possibleToModerate(chatInfo: ChatInfo): Boolean = + chatInfo is ChatInfo.Group && chatInfo.groupInfo.membership.memberRole >= GroupMemberRole.Moderator 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 e566cf30d3..5710f09ed5 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 @@ -7,12 +7,12 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.* import androidx.compose.material.* -import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.* import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.semantics.Role @@ -39,35 +39,29 @@ fun SendMsgView( isDirectChat: Boolean, liveMessageAlertShown: SharedPreference<Boolean>, sendMsgEnabled: Boolean, + userCantSendReason: Pair<String, String?>?, + sendButtonEnabled: Boolean, nextSendGrpInv: Boolean, needToAllowVoiceToContact: Boolean, allowedVoiceByPrefs: Boolean, - userIsObserver: Boolean, - userCanSend: Boolean, sendButtonColor: Color = MaterialTheme.colors.primary, allowVoiceToContact: () -> Unit, timedMessageAllowed: Boolean = false, customDisappearingMessageTimePref: SharedPreference<Int>? = null, + placeholder: String, sendMessage: (Int?) -> Unit, sendLiveMessage: (suspend () -> Unit)? = null, updateLiveMessage: (suspend () -> Unit)? = null, cancelLiveMessage: (() -> Unit)? = null, editPrevMessage: () -> Unit, onFilesPasted: (List<URI>) -> Unit, - onMessageChange: (String) -> Unit, - textStyle: MutableState<TextStyle> -) { + onMessageChange: (ComposeMessage) -> Unit, + textStyle: MutableState<TextStyle>, + focusRequester: FocusRequester? = null, + ) { val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) } - - if (showCustomDisappearingMessageDialog.value) { - CustomDisappearingMessageDialog( - sendMessage = sendMessage, - setShowDialog = { showCustomDisappearingMessageDialog.value = it }, - customDisappearingMessageTimePref = customDisappearingMessageTimePref - ) - } - - Box(Modifier.padding(vertical = 8.dp)) { + val padding = if (appPlatform.isAndroid) PaddingValues(vertical = 8.dp) else PaddingValues(top = 3.dp, bottom = 4.dp) + Box(Modifier.padding(padding)) { val cs = composeState.value var progressByTimeout by rememberSaveable { mutableStateOf(false) } LaunchedEffect(composeState.value.inProgress) { @@ -78,34 +72,55 @@ fun SendMsgView( false } } - val showVoiceButton = !nextSendGrpInv && cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing && - cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started) + val showVoiceButton = !nextSendGrpInv && cs.message.text.isEmpty() && showVoiceRecordIcon && !composeState.value.editing && + !composeState.value.forwarding && cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started) && (cs.contextItem !is ComposeContextItem.ReportedItem) val showDeleteTextButton = rememberSaveable { mutableStateOf(false) } val sendMsgButtonDisabled = !sendMsgEnabled || !cs.sendEnabled() || (!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) || - cs.endLiveDisabled - PlatformTextField(composeState, sendMsgEnabled, sendMsgButtonDisabled, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage, onFilesPasted) { + cs.endLiveDisabled || + !sendButtonEnabled + val clicksOnTextFieldDisabled = !sendMsgEnabled || cs.preview is ComposePreview.VoicePreview || cs.inProgress + PlatformTextField( + composeState, + sendMsgEnabled, + disabledText = userCantSendReason?.first, + sendMsgButtonDisabled, + textStyle, + showDeleteTextButton, + if (clicksOnTextFieldDisabled) "" else placeholder, + showVoiceButton, + onMessageChange, + editPrevMessage, + onFilesPasted, + focusRequester + ) { if (!cs.inProgress) { sendMessage(null) } } - // Disable clicks on text field - if (!sendMsgEnabled || cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress) { - Box( - Modifier - .matchParentSize() - .clickable(enabled = !userCanSend, indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.observer_cant_send_message_title), - text = generalGetString(MR.strings.observer_cant_send_message_desc) - ) - }) - ) + if (clicksOnTextFieldDisabled) { + if (userCantSendReason != null) { + Box( + Modifier + .matchParentSize() + .clickable(indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.cant_send_message_alert_title), + text = userCantSendReason.second + ) + }) + ) + } else { + Box( + Modifier + .matchParentSize() + ) + } } if (showDeleteTextButton.value) { DeleteTextButton(composeState) } - Box(Modifier.align(Alignment.BottomEnd).padding(bottom = if (appPlatform.isAndroid) 0.dp else 5.dp)) { + Box(Modifier.align(Alignment.BottomEnd).padding(bottom = if (appPlatform.isAndroid) 0.dp else 5.sp.toDp() * fontSizeSqrtMultiplier)) { val sendButtonSize = remember { Animatable(36f) } val sendButtonAlpha = remember { Animatable(1f) } val scope = rememberCoroutineScope() @@ -117,15 +132,18 @@ fun SendMsgView( } when { progressByTimeout -> ProgressIndicator() + cs.contextItem is ComposeContextItem.ReportedItem -> { + SendMsgButton(painterResource(MR.images.ic_check_filled), sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage) + } showVoiceButton && sendMsgEnabled -> { Row(verticalAlignment = Alignment.CenterVertically) { val stopRecOnNextClick = remember { mutableStateOf(false) } when { - needToAllowVoiceToContact || !allowedVoiceByPrefs || !userCanSend -> { - DisallowedVoiceButton(userCanSend) { + needToAllowVoiceToContact || !allowedVoiceByPrefs -> { + DisallowedVoiceButton { if (needToAllowVoiceToContact) { showNeedToAllowVoiceAlert(allowVoiceToContact) - } else if (!allowedVoiceByPrefs) { + } else { showDisabledVoiceAlert(isDirectChat) } } @@ -140,8 +158,8 @@ fun SendMsgView( && (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value) && cs.contextItem is ComposeContextItem.NoContextItem ) { - Spacer(Modifier.width(10.dp)) - StartLiveMessageButton(userCanSend) { + Spacer(Modifier.width(12.dp)) + StartLiveMessageButton { if (composeState.value.preview is ComposePreview.NoPreview) { startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown) } @@ -149,7 +167,7 @@ fun SendMsgView( } } } - cs.liveMessage?.sent == false && cs.message.isEmpty() -> { + cs.liveMessage?.sent == false && cs.message.text.isEmpty() -> { CancelLiveMessageButton { cancelLiveMessage?.invoke() } @@ -203,6 +221,11 @@ fun SendMsgView( DefaultDropdownMenu(showDropdown) { menuItems.forEach { composable -> composable() } } + CustomDisappearingMessageDialog( + showCustomDisappearingMessageDialog, + sendMessage = sendMessage, + customDisappearingMessageTimePref = customDisappearingMessageTimePref + ) } else { SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage) } @@ -220,92 +243,43 @@ expect fun VoiceButtonWithoutPermissionByPlatform() @Composable private fun CustomDisappearingMessageDialog( + showMenu: MutableState<Boolean>, sendMessage: (Int?) -> Unit, - setShowDialog: (Boolean) -> Unit, customDisappearingMessageTimePref: SharedPreference<Int>? ) { - val showCustomTimePicker = remember { mutableStateOf(false) } - - if (showCustomTimePicker.value) { - val selectedDisappearingMessageTime = remember { - mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300) - } - CustomTimePickerDialog( - selectedDisappearingMessageTime, - title = generalGetString(MR.strings.delete_after), - confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send), - confirmButtonAction = { ttl -> - sendMessage(ttl) - customDisappearingMessageTimePref?.set?.invoke(ttl) - setShowDialog(false) - }, - cancel = { setShowDialog(false) } + DefaultDropdownMenu(showMenu) { + Text( + generalGetString(MR.strings.send_disappearing_message), + Modifier.padding(vertical = DEFAULT_PADDING_HALF, horizontal = DEFAULT_PADDING * 1.5f), + fontSize = 16.sp, + color = MaterialTheme.colors.secondary ) - } else { - @Composable - fun ChoiceButton( - text: String, - onClick: () -> Unit - ) { - TextButton(onClick) { - Text( - text, - fontSize = 18.sp, - color = MaterialTheme.colors.primary - ) - } - } - DefaultDialog(onDismissRequest = { setShowDialog(false) }) { - Surface( - shape = RoundedCornerShape(corner = CornerSize(25.dp)) - ) { - Box( - contentAlignment = Alignment.Center - ) { - Column( - modifier = Modifier.padding(DEFAULT_PADDING), - verticalArrangement = Arrangement.spacedBy(6.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text(" ") // centers title - Text( - generalGetString(MR.strings.send_disappearing_message), - fontSize = 16.sp, - color = MaterialTheme.colors.secondary - ) - Icon( - painterResource(MR.images.ic_close), - generalGetString(MR.strings.icon_descr_close_button), - tint = MaterialTheme.colors.secondary, - modifier = Modifier - .size(25.dp) - .clickable { setShowDialog(false) } - ) - } - ChoiceButton(generalGetString(MR.strings.send_disappearing_message_30_seconds)) { - sendMessage(30) - setShowDialog(false) - } - ChoiceButton(generalGetString(MR.strings.send_disappearing_message_1_minute)) { - sendMessage(60) - setShowDialog(false) - } - ChoiceButton(generalGetString(MR.strings.send_disappearing_message_5_minutes)) { - sendMessage(300) - setShowDialog(false) - } - ChoiceButton(generalGetString(MR.strings.send_disappearing_message_custom_time)) { - showCustomTimePicker.value = true - } - } - } - } + ItemAction(generalGetString(MR.strings.send_disappearing_message_30_seconds)) { + sendMessage(30) + showMenu.value = false + } + ItemAction(generalGetString(MR.strings.send_disappearing_message_1_minute)) { + sendMessage(60) + showMenu.value = false + } + ItemAction(generalGetString(MR.strings.send_disappearing_message_5_minutes)) { + sendMessage(300) + showMenu.value = false + } + ItemAction(generalGetString(MR.strings.send_disappearing_message_custom_time)) { + showMenu.value = false + val selectedDisappearingMessageTime = mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300) + showCustomTimePickerDialog( + selectedDisappearingMessageTime, + title = generalGetString(MR.strings.delete_after), + confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send), + confirmButtonAction = { ttl -> + sendMessage(ttl) + customDisappearingMessageTimePref?.set?.invoke(ttl) + }, + cancel = { showMenu.value = false } + ) } } } @@ -313,7 +287,7 @@ private fun CustomDisappearingMessageDialog( @Composable private fun BoxScope.DeleteTextButton(composeState: MutableState<ComposeState>) { IconButton( - { composeState.value = composeState.value.copy(message = "") }, + { composeState.value = composeState.value.copy(message = ComposeMessage()) }, Modifier.align(Alignment.TopEnd).size(36.dp) ) { Icon(painterResource(MR.images.ic_close), null, Modifier.padding(7.dp).size(36.dp), tint = MaterialTheme.colors.secondary) @@ -373,8 +347,8 @@ private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNex } @Composable -private fun DisallowedVoiceButton(enabled: Boolean, onClick: () -> Unit) { - IconButton(onClick, Modifier.size(36.dp), enabled = enabled) { +private fun DisallowedVoiceButton(onClick: () -> Unit) { + IconButton(onClick, Modifier.size(36.dp)) { Icon( painterResource(MR.images.ic_keyboard_voice), stringResource(MR.strings.icon_descr_record_voice_message), @@ -460,6 +434,7 @@ private fun SendMsgButton( onLongClick: (() -> Unit)? = null ) { val interactionSource = remember { MutableInteractionSource() } + val ripple = remember { ripple(bounded = false, radius = 24.dp) } Box( modifier = Modifier.requiredSize(36.dp) .combinedClickable( @@ -468,7 +443,7 @@ private fun SendMsgButton( enabled = enabled, role = Role.Button, interactionSource = interactionSource, - indication = rememberRipple(bounded = false, radius = 24.dp) + indication = ripple ) .onRightClick { onLongClick?.invoke() }, contentAlignment = Alignment.Center @@ -482,30 +457,30 @@ private fun SendMsgButton( .padding(4.dp) .alpha(alpha.value) .clip(CircleShape) - .background(if (enabled) sendButtonColor else MaterialTheme.colors.secondary) + .background(if (enabled) sendButtonColor else MaterialTheme.colors.secondary.copy(alpha = 0.75f)) .padding(3.dp) ) } } @Composable -private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) { +private fun StartLiveMessageButton(onClick: () -> Unit) { val interactionSource = remember { MutableInteractionSource() } + val ripple = remember { ripple(bounded = false, radius = 24.dp) } Box( modifier = Modifier.requiredSize(36.dp) .clickable( onClick = onClick, - enabled = enabled, role = Role.Button, interactionSource = interactionSource, - indication = rememberRipple(bounded = false, radius = 24.dp) + indication = ripple ), contentAlignment = Alignment.Center ) { Icon( BoltFilled, stringResource(MR.strings.icon_descr_send_message), - tint = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + tint = MaterialTheme.colors.primary, modifier = Modifier .size(36.dp) .padding(4.dp) @@ -604,18 +579,19 @@ fun PreviewSendMsgView() { isDirectChat = true, liveMessageAlertShown = SharedPreference(get = { true }, set = { }), sendMsgEnabled = true, + userCantSendReason = null, + sendButtonEnabled = true, nextSendGrpInv = false, needToAllowVoiceToContact = false, allowedVoiceByPrefs = true, - userIsObserver = false, - userCanSend = true, allowVoiceToContact = {}, timedMessageAllowed = false, + placeholder = "", sendMessage = {}, editPrevMessage = {}, onMessageChange = { _ -> }, onFilesPasted = {}, - textStyle = textStyle + textStyle = textStyle, ) } } @@ -638,18 +614,19 @@ fun PreviewSendMsgViewEditing() { isDirectChat = true, liveMessageAlertShown = SharedPreference(get = { true }, set = { }), sendMsgEnabled = true, + userCantSendReason = null, + sendButtonEnabled = true, nextSendGrpInv = false, needToAllowVoiceToContact = false, allowedVoiceByPrefs = true, - userIsObserver = false, - userCanSend = true, allowVoiceToContact = {}, timedMessageAllowed = false, + placeholder = "", sendMessage = {}, editPrevMessage = {}, onMessageChange = { _ -> }, onFilesPasted = {}, - textStyle = textStyle + textStyle = textStyle, ) } } @@ -672,18 +649,19 @@ fun PreviewSendMsgViewInProgress() { isDirectChat = true, liveMessageAlertShown = SharedPreference(get = { true }, set = { }), sendMsgEnabled = true, + userCantSendReason = null, + sendButtonEnabled = true, nextSendGrpInv = false, needToAllowVoiceToContact = false, allowedVoiceByPrefs = true, - userIsObserver = false, - userCanSend = true, allowVoiceToContact = {}, timedMessageAllowed = false, + placeholder = "", sendMessage = {}, editPrevMessage = {}, onMessageChange = { _ -> }, onFilesPasted = {}, - textStyle = textStyle + textStyle = textStyle, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt index e1840dd885..e670fae5ef 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt @@ -16,8 +16,7 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.common.platform.appPlatform -import chat.simplex.common.platform.shareText +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.QRCode @@ -36,14 +35,14 @@ fun VerifyCodeView( displayName, connectionCode, connectionVerified, - verifyCode = { newCode, cb -> - withBGApi { - val res = verify(newCode) - if (res != null) { - val (verified) = res - cb(verified) - if (verified) close() - } + verifyCode = { newCode -> + val res = verify(newCode) + if (res != null) { + val (verified) = res + if (verified) close() + verified + } else { + false } } ) @@ -55,14 +54,9 @@ private fun VerifyCodeLayout( displayName: String, connectionCode: String, connectionVerified: Boolean, - verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, + verifyCode: suspend (String?) -> Boolean, ) { - Column( - Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(horizontal = DEFAULT_PADDING) - ) { + ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) { AppBarTitle(stringResource(MR.strings.security_code), withPadding = false) val splitCode = splitToParts(connectionCode, 24) Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) { @@ -74,9 +68,7 @@ private fun VerifyCodeLayout( } } - SectionView { - QRCode(connectionCode, Modifier.aspectRatio(1f)) - } + QRCode(connectionCode, padding = PaddingValues(vertical = DEFAULT_PADDING_HALF)) Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Spacer(Modifier.weight(2f)) @@ -108,7 +100,7 @@ private fun VerifyCodeLayout( ) { if (connectionVerified) { SimpleButton(generalGetString(MR.strings.clear_verification), painterResource(MR.images.ic_shield)) { - verifyCode(null) {} + withApi { verifyCode(null) } } } else { if (appPlatform.isAndroid) { @@ -119,7 +111,8 @@ private fun VerifyCodeLayout( } } SimpleButton(generalGetString(MR.strings.mark_code_verified), painterResource(MR.images.ic_verified_user)) { - verifyCode(connectionCode) { verified -> + withApi { + val verified = verifyCode(connectionCode) if (!verified) { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.incorrect_code) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt index 37ee9729f5..10694d13bf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt @@ -4,6 +4,7 @@ import SectionBottomSpacer import SectionCustomFooter import SectionDividerSpaced import SectionItemView +import SectionItemViewWithoutMinPadding import SectionSpacer import SectionView import androidx.compose.foundation.* @@ -31,6 +32,8 @@ import chat.simplex.common.views.usersettings.SettingsActionItem import chat.simplex.common.model.GroupInfo import chat.simplex.common.platform.* import chat.simplex.res.MR +import dev.icerock.moko.resources.StringResource +import kotlinx.coroutines.* @Composable fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolean = false, chatModel: ChatModel, close: () -> Unit) { @@ -54,11 +57,16 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea }, inviteMembers = { allowModifyMembers = false - withApi { + withLongRunningApi(slow = 120_000) { for (contactId in selectedContacts) { val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value) if (member != null) { - chatModel.upsertGroupMember(rhId, groupInfo, member) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, member) + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, member) + } } else { break } @@ -68,7 +76,7 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea }, clearSelection = { selectedContacts.clear() }, addContact = { contactId -> if (contactId !in selectedContacts) selectedContacts.add(contactId) }, - removeContact = { contactId -> selectedContacts.removeIf { it == contactId } }, + removeContact = { contactId -> selectedContacts.removeAll { it == contactId } }, close = close, ) KeyChangeEffect(chatModel.chatId.value) { @@ -78,15 +86,16 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> { val s = search.trim().lowercase() - val memberContactIds = chatModel.groupMembers + val memberContactIds = chatModel.groupMembers.value .filter { it.memberCurrent } .mapNotNull { it.memberContactId } - return chatModel.chats + return chatModel.chats.value .asSequence() .map { it.chatInfo } .filterIsInstance<ChatInfo.Direct>() .map { it.contact } - .filter { it.contactId !in memberContactIds && it.chatViewName.lowercase().contains(s) } + .filter { c -> c.sendMsgEnabled && !c.nextSendGrpInv && c.contactId !in memberContactIds && c.anyNameContains(s) + } .sortedBy { it.displayName.lowercase() } .toList() } @@ -121,15 +130,12 @@ fun AddGroupMembersLayout( tint = MaterialTheme.colors.secondary, modifier = Modifier.padding(end = 10.dp).size(20.dp) ) - Text(generalGetString(MR.strings.group_main_profile_sent), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2) + val textId = if (groupInfo.businessChat == null) MR.strings.group_main_profile_sent else MR.strings.chat_main_profile_sent + Text(generalGetString(textId), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2) } } - Column( - Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.button_add_members)) profileText() Spacer(Modifier.size(DEFAULT_PADDING)) @@ -167,14 +173,15 @@ fun AddGroupMembersLayout( if (creatingGroup && selectedContacts.isEmpty()) { SkipInvitingButton(close) } else { - InviteMembersButton(inviteMembers, disabled = selectedContacts.isEmpty() || !allowModifyMembers) + val titleId = if (groupInfo.businessChat == null) MR.strings.invite_to_group_button else MR.strings.invite_to_chat_button + InviteMembersButton(titleId, inviteMembers, disabled = selectedContacts.isEmpty() || !allowModifyMembers) } } SectionCustomFooter { InviteSectionFooter(selectedContactsCount = selectedContacts.size, allowModifyMembers, clearSelection) } SectionDividerSpaced(maxTopPadding = true) - SectionView(stringResource(MR.strings.select_contacts)) { + SectionView(stringResource(MR.strings.select_contacts).uppercase()) { SectionItemView(padding = PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF)) { SearchRowView(searchText) } @@ -205,8 +212,8 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - val values = GroupMemberRole.values() - .filter { it <= groupInfo.membership.memberRole && it != GroupMemberRole.Author } + val values = GroupMemberRole.selectableRoles + .filter { it <= groupInfo.membership.memberRole } .map { it to it.text } ExposedDropDownSettingRow( generalGetString(MR.strings.new_member_role), @@ -219,10 +226,10 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr } @Composable -fun InviteMembersButton(onClick: () -> Unit, disabled: Boolean) { +fun InviteMembersButton(titleId: StringResource, onClick: () -> Unit, disabled: Boolean) { SettingsActionItem( painterResource(MR.images.ic_check), - stringResource(MR.strings.invite_to_group_button), + stringResource(titleId), click = onClick, textColor = MaterialTheme.colors.primary, iconColor = MaterialTheme.colors.primary, @@ -252,7 +259,8 @@ fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelec Text( String.format(generalGetString(MR.strings.num_contacts_selected), selectedContactsCount), color = MaterialTheme.colors.secondary, - fontSize = 12.sp + lineHeight = 18.sp, + fontSize = 14.sp ) Box( Modifier.clickable { if (enabled) clearSelection() } @@ -260,14 +268,16 @@ fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelec Text( stringResource(MR.strings.clear_contacts_selection_button), color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, - fontSize = 12.sp + lineHeight = 18.sp, + fontSize = 14.sp, ) } } else { Text( stringResource(MR.strings.no_contacts_selected), color = MaterialTheme.colors.secondary, - fontSize = 12.sp + lineHeight = 18.sp, + fontSize = 14.sp, ) } } @@ -315,7 +325,7 @@ fun ContactCheckRow( icon = painterResource(MR.images.ic_circle) iconColor = MaterialTheme.colors.secondary } - SectionItemView( + SectionItemViewWithoutMinPadding( click = if (enabled) { { if (prohibitedToInviteIncognito) { 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 d3b1841fe0..22956738e7 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 @@ -8,6 +8,8 @@ import SectionItemViewLongClickable import SectionSpacer import SectionTextFooter import SectionView +import androidx.compose.animation.* +import androidx.compose.animation.core.animateDpAsState import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* @@ -17,66 +19,103 @@ 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.alpha import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* import chat.simplex.common.model.GroupInfo import chat.simplex.common.platform.* import chat.simplex.common.views.chat.* -import chat.simplex.common.views.chat.item.ItemAction +import chat.simplex.common.views.chat.item.* import chat.simplex.common.views.chatlist.* +import chat.simplex.common.views.database.TtlOptions import chat.simplex.res.MR -import kotlinx.coroutines.launch +import dev.icerock.moko.resources.StringResource +import kotlinx.coroutines.* const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20 +val MEMBER_ROW_AVATAR_SIZE = 42.dp +val MEMBER_ROW_VERTICAL_PADDING = 8.dp @Composable -fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit) { +fun ModalData.GroupChatInfoView( + chatsCtx: ChatModel.ChatsContext, + rhId: Long?, + chatId: String, + groupLink: CreatedConnLink?, + groupLinkMemberRole: GroupMemberRole?, + selectedItems: MutableState<Set<Long>?>, + appBar: MutableState<@Composable (BoxScope.() -> Unit)?>, + scrollToItemId: MutableState<Long?>, + onGroupLinkUpdated: (Pair<CreatedConnLink, GroupMemberRole>?) -> Unit, + close: () -> Unit, + onSearchClicked: () -> Unit +) { BackHandler(onBack = close) // TODO derivedStateOf? - val chat = chatModel.chats.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId } + val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId } val currentUser = chatModel.currentUser.value val developerTools = chatModel.controller.appPrefs.developerTools.get() if (chat != null && chat.chatInfo is ChatInfo.Group && currentUser != null) { val groupInfo = chat.chatInfo.groupInfo val sendReceipts = remember { mutableStateOf(SendReceipts.fromBool(groupInfo.chatSettings.sendRcpts, currentUser.sendRcptsSmallGroups)) } + val chatItemTTL = remember(groupInfo.id) { mutableStateOf(if (groupInfo.chatItemTTL != null) ChatItemTTL.fromSeconds(groupInfo.chatItemTTL) else null) } + val deletingItems = rememberSaveable(groupInfo.id) { mutableStateOf(false) } + val scope = rememberCoroutineScope() + GroupChatInfoLayout( chat, groupInfo, currentUser, sendReceipts = sendReceipts, setSendReceipts = { sendRcpts -> - withApi { - val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool) - updateChatSettings(chat, chatSettings, chatModel) - sendReceipts.value = sendRcpts - } + val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool) + updateChatSettings(chat.remoteHostId, chat.chatInfo, chatSettings, chatModel) + sendReceipts.value = sendRcpts }, - members = chatModel.groupMembers + chatItemTTL = chatItemTTL, + setChatItemTTL = { + if (it == chatItemTTL.value) { + return@GroupChatInfoLayout + } + val previousChatTTL = chatItemTTL.value + chatItemTTL.value = it + + setChatTTLAlert(chatsCtx, chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems) + }, + activeSortedMembers = remember { chatModel.groupMembers }.value .filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved } - .sortedBy { it.displayName.lowercase() }, + .sortedByDescending { it.memberRole }, developerTools, + onLocalAliasChanged = { setGroupAlias(chat, it, chatModel) }, groupLink, + selectedItems, + appBar, + scrollToItemId, addMembers = { - withApi { + scope.launch(Dispatchers.Default) { setGroupMembers(rhId, groupInfo, chatModel) + if (!isActive) return@launch + ModalManager.end.showModalCloseable(true) { close -> AddGroupMembersView(rhId, groupInfo, false, chatModel, close) } } }, showMemberInfo = { member -> - withApi { + withBGApi { val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, member.groupMemberId) val stats = r?.second val (_, code) = if (member.memberActive) { @@ -112,35 +151,45 @@ fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLi } }, deleteGroup = { deleteGroupDialog(chat, groupInfo, chatModel, close) }, - clearChat = { clearChatDialog(chat, chatModel, close) }, + clearChat = { clearChatDialog(chat, close) }, leaveGroup = { leaveGroupDialog(rhId, groupInfo, chatModel, close) }, manageGroupLink = { ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) } - } + }, + onSearchClicked = onSearchClicked, + deletingItems = deletingItems ) } } fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { val chatInfo = chat.chatInfo - val alertTextKey = - if (groupInfo.membership.memberCurrent) MR.strings.delete_group_for_all_members_cannot_undo_warning - else MR.strings.delete_group_for_self_cannot_undo_warning + val titleId = if (groupInfo.businessChat == null) MR.strings.delete_group_question else MR.strings.delete_chat_question + val messageId = + if (groupInfo.businessChat == null) { + if (groupInfo.membership.memberCurrent) MR.strings.delete_group_for_all_members_cannot_undo_warning + else MR.strings.delete_group_for_self_cannot_undo_warning + } else { + if (groupInfo.membership.memberCurrent) MR.strings.delete_chat_for_all_members_cannot_undo_warning + else MR.strings.delete_chat_for_self_cannot_undo_warning + } AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.delete_group_question), - text = generalGetString(alertTextKey), + title = generalGetString(titleId), + text = generalGetString(messageId), confirmText = generalGetString(MR.strings.delete_verb), onConfirm = { - withApi { + withBGApi { val r = chatModel.controller.apiDeleteChat(chat.remoteHostId, chatInfo.chatType, chatInfo.apiId) if (r) { - chatModel.removeChat(chat.remoteHostId, chatInfo.id) - if (chatModel.chatId.value == chatInfo.id) { - chatModel.chatId.value = null - ModalManager.end.closeModals() + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(chat.remoteHostId, chatInfo.id) + if (chatModel.chatId.value == chatInfo.id) { + chatModel.chatId.value = null + ModalManager.end.closeModals() + } + ntfManager.cancelNotificationsForChat(chatInfo.id) + close?.invoke() } - ntfManager.cancelNotificationsForChat(chatInfo.id) - close?.invoke() } } }, @@ -149,12 +198,17 @@ fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, cl } fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { + val titleId = if (groupInfo.businessChat == null) MR.strings.leave_group_question else MR.strings.leave_chat_question + val messageId = if (groupInfo.businessChat == null) + MR.strings.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved + else + MR.strings.you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.leave_group_question), - text = generalGetString(MR.strings.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved), + title = generalGetString(titleId), + text = generalGetString(messageId), confirmText = generalGetString(MR.strings.leave_group_button), onConfirm = { - withApi { + withLongRunningApi(60_000) { chatModel.controller.leaveGroup(rhId, groupInfo.groupId) close?.invoke() } @@ -164,32 +218,121 @@ fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl } private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMember) { + 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.showAlertDialog( title = generalGetString(MR.strings.button_remove_member_question), - text = generalGetString(MR.strings.member_will_be_removed_from_group_cannot_be_undone), + text = generalGetString(messageId), confirmText = generalGetString(MR.strings.remove_member_confirmation), onConfirm = { - withApi { - val updatedMember = chatModel.controller.apiRemoveMember(rhId, groupInfo.groupId, mem.groupMemberId) - if (updatedMember != null) { - chatModel.upsertGroupMember(rhId, groupInfo, updatedMember) - } - } + removeMembers(rhId, groupInfo, listOf(mem.groupMemberId)) + }, + destructive = true, + ) +} + +private fun removeMembersAlert(rhId: Long?, groupInfo: GroupInfo, memberIds: List<Long>, onSuccess: () -> Unit = {}) { + val messageId = if (groupInfo.businessChat == null) + MR.strings.members_will_be_removed_from_group_cannot_be_undone + else + MR.strings.members_will_be_removed_from_chat_cannot_be_undone + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.button_remove_members_question), + text = generalGetString(messageId), + confirmText = generalGetString(MR.strings.remove_member_confirmation), + onConfirm = { + removeMembers(rhId, groupInfo, memberIds, onSuccess) }, destructive = true, ) } @Composable -fun GroupChatInfoLayout( +fun SearchButton( + modifier: Modifier, + chat: Chat, + group: GroupInfo, + close: () -> Unit, + onSearchClicked: () -> Unit +) { + val disabled = !group.ready || chat.chatItems.isEmpty() + + InfoViewActionButton( + modifier = modifier, + icon = painterResource(MR.images.ic_search), + title = generalGetString(MR.strings.info_view_search_button), + disabled = disabled, + disabledLook = disabled, + onClick = { + if (appPlatform.isAndroid) { + close.invoke() + } + onSearchClicked() + } + ) +} + +@Composable +fun MuteButton( + modifier: Modifier, + chat: Chat, + groupInfo: GroupInfo +) { + val notificationMode = remember { mutableStateOf(groupInfo.chatSettings.enableNtfs) } + val nextNotificationMode by remember { derivedStateOf { notificationMode.value.nextMode(true) } } + + InfoViewActionButton( + modifier = modifier, + icon = painterResource(nextNotificationMode.icon), + title = generalGetString(nextNotificationMode.text(true)), + disabled = !groupInfo.ready, + disabledLook = !groupInfo.ready, + onClick = { + toggleNotifications(chat.remoteHostId, chat.chatInfo, nextNotificationMode, chatModel, notificationMode) + } + ) +} + +@Composable +fun AddGroupMembersButton( + modifier: Modifier, + chat: Chat, + groupInfo: GroupInfo +) { + InfoViewActionButton( + modifier = modifier, + icon = if (groupInfo.incognito) painterResource(MR.images.ic_add_link) else painterResource(MR.images.ic_person_add_500), + title = stringResource(MR.strings.action_button_add_members), + disabled = !groupInfo.ready, + disabledLook = !groupInfo.ready, + onClick = { + if (groupInfo.incognito) { + openGroupLink(groupInfo = groupInfo, rhId = chat.remoteHostId) + } else { + addGroupMembers(groupInfo = groupInfo, rhId = chat.remoteHostId) + } + } + ) +} + +@Composable +fun ModalData.GroupChatInfoLayout( chat: Chat, groupInfo: GroupInfo, currentUser: User, sendReceipts: State<SendReceipts>, setSendReceipts: (SendReceipts) -> Unit, - members: List<GroupMember>, + chatItemTTL: MutableState<ChatItemTTL?>, + setChatItemTTL: (ChatItemTTL?) -> Unit, + activeSortedMembers: List<GroupMember>, developerTools: Boolean, - groupLink: String?, + onLocalAliasChanged: (String) -> Unit, + groupLink: CreatedConnLink?, + selectedItems: MutableState<Set<Long>?>, + appBar: MutableState<@Composable (BoxScope.() -> Unit)?>, + scrollToItemId: MutableState<Long?>, addMembers: () -> Unit, showMemberInfo: (GroupMember) -> Unit, editGroupProfile: () -> Unit, @@ -199,72 +342,181 @@ fun GroupChatInfoLayout( clearChat: () -> Unit, leaveGroup: () -> Unit, manageGroupLink: () -> Unit, + close: () -> Unit = { ModalManager.closeAllModalsEverywhere()}, + onSearchClicked: () -> Unit, + deletingItems: State<Boolean> ) { - val listState = rememberLazyListState() + val listState = remember { appBarHandler.listState } val scope = rememberCoroutineScope() KeyChangeEffect(chat.id) { scope.launch { listState.scrollToItem(0) } } - val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) } - val filteredMembers = remember(members) { derivedStateOf { members.filter { it.chatViewName.lowercase().contains(searchText.value.text.trim()) } } } - LazyColumn( - Modifier - .fillMaxWidth(), - state = listState + val searchText = remember { stateGetOrPut("searchText") { TextFieldValue() } } + val filteredMembers = remember(activeSortedMembers) { + derivedStateOf { + val s = searchText.value.text.trim().lowercase() + if (s.isEmpty()) activeSortedMembers else activeSortedMembers.filter { m -> m.anyNameContains(s) } + } + } + Box { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val selectedItemsBarHeight = if (selectedItems.value != null) AppBarHeight * fontSizeSqrtMultiplier else 0.dp + val navBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + val imePadding = WindowInsets.ime.asPaddingValues().calculateBottomPadding() + LazyColumnWithScrollBar( + state = listState, + contentPadding = if (oneHandUI.value) { + PaddingValues( + top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp, + bottom = navBarPadding + + imePadding + + selectedItemsBarHeight + + // TODO: that's workaround but works. Actually, something in the codebase doesn't consume padding for AppBar and it produce + // different padding when the user has NavigationBar and doesn't have it with ime shown (developer options helps to test it nav bars) + (if (navBarPadding > 0.dp && imePadding > 0.dp) 0.dp else AppBarHeight * fontSizeSqrtMultiplier) + ) + } else { + PaddingValues( + top = topPaddingToContent(false), + bottom = if (imePadding > 0.dp) { + imePadding + selectedItemsBarHeight + } else { + navBarPadding + selectedItemsBarHeight + } + ) + } ) { item { Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center ) { - GroupChatInfoHeader(chat.chatInfo) + GroupChatInfoHeader(chat.chatInfo, groupInfo) } + + LocalAliasEditor(chat.id, groupInfo.localAlias, isContact = false, updateValue = onLocalAliasChanged) + + SectionSpacer() + + Box( + Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Row( + Modifier + .widthIn(max = if (groupInfo.canAddMembers) 320.dp else 230.dp) + .padding(horizontal = DEFAULT_PADDING), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + if (groupInfo.canAddMembers) { + SearchButton(modifier = Modifier.fillMaxWidth(0.33f), chat, groupInfo, close, onSearchClicked) + AddGroupMembersButton(modifier = Modifier.fillMaxWidth(0.5f), chat, groupInfo) + MuteButton(modifier = Modifier.fillMaxWidth(1f), chat, groupInfo) + } else { + SearchButton(modifier = Modifier.fillMaxWidth(0.5f), chat, groupInfo, close, onSearchClicked) + MuteButton(modifier = Modifier.fillMaxWidth(1f), chat, groupInfo) + } + } + } + SectionSpacer() SectionView { - if (groupInfo.canEdit) { + if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) { EditGroupProfileButton(editGroupProfile) } - if (groupInfo.groupProfile.description != null || groupInfo.canEdit) { + if (groupInfo.groupProfile.description != null || (groupInfo.isOwner && groupInfo.businessChat?.chatType == null)) { AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage) } - GroupPreferencesButton(openPreferences) - if (members.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) { + val prefsTitleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences + GroupPreferencesButton(prefsTitleId, openPreferences) + if (groupInfo.canModerate) { + GroupReportsButton { + scope.launch { + showGroupReportsView(chatModel.chatId, scrollToItemId, chat.chatInfo) + } + } + } + if (activeSortedMembers.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) { SendReceiptsOption(currentUser, sendReceipts, setSendReceipts) } else { SendReceiptsOptionDisabled() } - } - SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs)) - SectionDividerSpaced(maxTopPadding = true) - SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), members.count() + 1)) { + WallpaperButton { + ModalManager.end.showModal { + val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } } + val c = chat.value + if (c != null) { + ChatWallpaperEditorModal(c) + } + } + } + } + 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)) + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + + ChatTTLSection(chatItemTTL, setChatItemTTL, deletingItems) + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = true) + + SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), activeSortedMembers.count() + 1)) { if (groupInfo.canAddMembers) { - if (groupLink == null) { - CreateGroupLinkButton(manageGroupLink) - } else { - GroupLinkButton(manageGroupLink) + if (groupInfo.businessChat == null) { + if (groupLink == null) { + CreateGroupLinkButton(manageGroupLink) + } else { + GroupLinkButton(manageGroupLink) + } } val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary - AddMembersButton(tint, onAddMembersClick) + val addMembersTitleId = when (groupInfo.businessChat?.chatType) { + BusinessChatType.Customer -> MR.strings.button_add_team_members + BusinessChatType.Business -> MR.strings.button_add_friends + null -> MR.strings.button_add_members + } + AddMembersButton(addMembersTitleId, tint, onAddMembersClick) } - if (members.size > 8) { + if (activeSortedMembers.size > 8) { SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) { SearchRowView(searchText) } } - SectionItemView(minHeight = 54.dp) { + SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) { MemberRow(groupInfo.membership, user = true) } } } - items(filteredMembers.value) { member -> + items(filteredMembers.value, key = { it.groupMemberId }) { member -> Divider() val showMenu = remember { mutableStateOf(false) } - SectionItemViewLongClickable({ showMemberInfo(member) }, { showMenu.value = true }, minHeight = 54.dp) { - DropDownMenuForMember(chat.remoteHostId, member, groupInfo, showMenu) - MemberRow(member, onClick = { showMemberInfo(member) }) + val canBeSelected = groupInfo.membership.memberRole >= member.memberRole && member.memberRole < GroupMemberRole.Moderator + SectionItemViewLongClickable( + click = { + if (selectedItems.value != null) { + if (canBeSelected) { + toggleItemSelection(member.groupMemberId, selectedItems) + } + } else { + showMemberInfo(member) + } + }, + longClick = { showMenu.value = true }, + minHeight = 54.dp, + padding = PaddingValues(horizontal = DEFAULT_PADDING) + ) { + Box(contentAlignment = Alignment.CenterStart) { + androidx.compose.animation.AnimatedVisibility(selectedItems.value != null, enter = fadeIn(), exit = fadeOut()) { + SelectedListItem(Modifier.alpha(if (canBeSelected) 1f else 0f).padding(start = 2.dp), member.groupMemberId, selectedItems) + } + val selectionOffset by animateDpAsState(if (selectedItems.value != null) 20.dp + 22.dp * fontSizeMultiplier else 0.dp) + DropDownMenuForMember(chat.remoteHostId, member, groupInfo, selectedItems, showMenu) + Box(Modifier.padding(start = selectionOffset)) { + MemberRow(member) + } + } } } item { @@ -272,10 +524,12 @@ fun GroupChatInfoLayout( SectionView { ClearChatButton(clearChat) if (groupInfo.canDelete) { - DeleteGroupButton(deleteGroup) + val titleId = if (groupInfo.businessChat == null) MR.strings.button_delete_group else MR.strings.button_delete_chat + DeleteGroupButton(titleId, deleteGroup) } if (groupInfo.membership.memberCurrent) { - LeaveGroupButton(leaveGroup) + val titleId = if (groupInfo.businessChat == null) MR.strings.button_leave_group else MR.strings.button_leave_chat + LeaveGroupButton(titleId, leaveGroup) } } @@ -289,39 +543,160 @@ fun GroupChatInfoLayout( SectionBottomSpacer() } } + if (!oneHandUI.value) { + NavigationBarBackground(oneHandUI.value, oneHandUI.value) + } + SelectedItemsButtonsToolbar(chat, groupInfo, selectedItems, rememberUpdatedState(activeSortedMembers)) + SelectedItemsCounterToolbarSetter(groupInfo, selectedItems, filteredMembers, appBar) + } } @Composable -private fun GroupChatInfoHeader(cInfo: ChatInfo) { +private fun BoxScope.SelectedItemsButtonsToolbar(chat: Chat, groupInfo: GroupInfo, selectedItems: MutableState<Set<Long>?>, activeMembers: State<List<GroupMember>>) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + Column(Modifier.align(Alignment.BottomCenter)) { + AnimatedVisibility(selectedItems.value != null) { + SelectedItemsMembersToolbar( + selectedItems = selectedItems, + activeMembers = activeMembers, + groupInfo = groupInfo, + delete = { + removeMembersAlert(chat.remoteHostId, groupInfo, selectedItems.value!!.sorted()) { + selectedItems.value = null + } + }, + blockForAll = { block -> + if (block) { + blockForAllAlert(chat.remoteHostId, groupInfo, selectedItems.value!!.sorted()) { + selectedItems.value = null + } + } else { + unblockForAllAlert(chat.remoteHostId, groupInfo, selectedItems.value!!.sorted()) { + selectedItems.value = null + } + } + }, + changeRole = { toRole -> + updateMembersRoleDialog(toRole, groupInfo) { + updateMembersRole(toRole, chat.remoteHostId, groupInfo, selectedItems.value!!.sorted()) { + selectedItems.value = null + } + } + } + ) + } + if (oneHandUI.value) { + // That's placeholder to take some space for bottom app bar in oneHandUI + Box(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier)) + } + } +} + +@Composable +private fun SelectedItemsCounterToolbarSetter( + groupInfo: GroupInfo, + selectedItems: MutableState<Set<Long>?>, + filteredMembers: State<List<GroupMember>>, + appBar: MutableState<@Composable (BoxScope.() -> Unit)?> +) { + LaunchedEffect( + groupInfo, + /* variable, not value - intentionally - to reduce work but handle variable change because it changes in remember(members) { derivedState {} } */ + filteredMembers + ) { + snapshotFlow { selectedItems.value == null } + .collect { nullItems -> + if (!nullItems) { + appBar.value = { + SelectedItemsCounterToolbar(selectedItems, !remember { appPrefs.oneHandUI.state }.value) { + if (!groupInfo.membership.memberActive) return@SelectedItemsCounterToolbar + val ids: MutableSet<Long> = mutableSetOf() + for (mem in filteredMembers.value) { + if (groupInfo.membership.memberActive && groupInfo.membership.memberRole >= mem.memberRole && mem.memberRole < GroupMemberRole.Moderator) { + ids.add(mem.groupMemberId) + } + } + if (ids.isNotEmpty() && (selectedItems.value ?: setOf()).containsAll(ids)) { + selectedItems.value = (selectedItems.value ?: setOf()).minus(ids) + } else { + selectedItems.value = (selectedItems.value ?: setOf()).union(ids) + } + } + } + } else { + appBar.value = null + } + } + } +} + +@Composable +fun ChatTTLSection(chatItemTTL: State<ChatItemTTL?>, setChatItemTTL: (ChatItemTTL?) -> Unit, deletingItems: State<Boolean>) { + Box { + SectionView { + TtlOptions( + chatItemTTL, + enabled = remember { derivedStateOf { !deletingItems.value } }, + onSelected = setChatItemTTL, + default = chatModel.chatItemTTL + ) + SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer)) + } + if (deletingItems.value) { + Box(Modifier.matchParentSize()) { + ProgressIndicator() + } + } + } +} + +@Composable +private fun GroupChatInfoHeader(cInfo: ChatInfo, groupInfo: GroupInfo) { Column( Modifier.padding(horizontal = 8.dp), horizontalAlignment = Alignment.CenterHorizontally ) { ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight) + val clipboard = LocalClipboardManager.current + val copyNameToClipboard = { + clipboard.setText(AnnotatedString(groupInfo.groupProfile.displayName)) + showToast(generalGetString(MR.strings.copied)) + } Text( - cInfo.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), + groupInfo.groupProfile.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), color = MaterialTheme.colors.onBackground, textAlign = TextAlign.Center, maxLines = 4, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard) ) - if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName) { + if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != groupInfo.groupProfile.displayName) { Text( cInfo.fullName, style = MaterialTheme.typography.h2, color = MaterialTheme.colors.onBackground, textAlign = TextAlign.Center, maxLines = 8, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard) ) } } } @Composable -private fun GroupPreferencesButton(onClick: () -> Unit) { +private fun GroupPreferencesButton(titleId: StringResource, onClick: () -> Unit) { SettingsActionItem( painterResource(MR.images.ic_toggle_on), - stringResource(MR.strings.group_preferences), + stringResource(titleId), + click = onClick + ) +} + +@Composable +private fun GroupReportsButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_flag), + stringResource(MR.strings.group_reports_member_reports), click = onClick ) } @@ -358,10 +733,10 @@ fun SendReceiptsOptionDisabled() { } @Composable -private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, onClick: () -> Unit) { +private fun AddMembersButton(titleId: StringResource, tint: Color = MaterialTheme.colors.primary, onClick: () -> Unit) { SettingsActionItem( painterResource(MR.images.ic_add), - stringResource(MR.strings.button_add_members), + stringResource(titleId), onClick, iconColor = tint, textColor = tint @@ -369,18 +744,40 @@ private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, onClick } @Composable -private fun MemberRow(member: GroupMember, user: Boolean = false, onClick: (() -> Unit)? = null) { +fun MemberRow(member: GroupMember, user: Boolean = false, infoPage: Boolean = true, showlocalAliasAndFullName: Boolean = false, selected: Boolean = false) { + @Composable + fun MemberInfo() { + if (member.blocked) { + Text(stringResource(MR.strings.member_info_member_blocked), color = MaterialTheme.colors.secondary) + } else { + val role = member.memberRole + if (role in listOf(GroupMemberRole.Owner, GroupMemberRole.Admin, GroupMemberRole.Moderator, GroupMemberRole.Observer)) { + Text(role.text, color = MaterialTheme.colors.secondary) + } + } + } + + fun memberConnStatus(): String { + return if (member.activeConn?.connDisabled == true) { + generalGetString(MR.strings.member_info_member_disabled) + } else if (member.activeConn?.connDisabled == true) { + generalGetString(MR.strings.member_info_member_inactive) + } else { + member.memberStatus.shortText + } + } + Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Row( - Modifier.weight(1f).padding(end = DEFAULT_PADDING), + Modifier.weight(1f).padding(top = MEMBER_ROW_VERTICAL_PADDING, end = DEFAULT_PADDING, bottom = MEMBER_ROW_VERTICAL_PADDING), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - ProfileImage(size = 46.dp, member.image) + MemberProfileImage(size = MEMBER_ROW_AVATAR_SIZE, member) Spacer(Modifier.width(DEFAULT_PADDING_HALF)) Column { Row(verticalAlignment = Alignment.CenterVertically) { @@ -388,24 +785,36 @@ private fun MemberRow(member: GroupMember, user: Boolean = false, onClick: (() - MemberVerifiedShield() } Text( - member.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis, + if (showlocalAliasAndFullName) member.localAliasAndFullName else member.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis, color = if (member.memberIncognito) Indigo else Color.Unspecified ) } - val s = member.memberStatus.shortText - val statusDescr = if (user) String.format(generalGetString(MR.strings.group_info_member_you), s) else s - Text( - statusDescr, - color = MaterialTheme.colors.secondary, - fontSize = 12.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + + if (infoPage) { + val statusDescr = + if (user) String.format(generalGetString(MR.strings.group_info_member_you), member.memberStatus.shortText) else memberConnStatus() + Text( + statusDescr, + color = MaterialTheme.colors.secondary, + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } } } - val role = member.memberRole - if (role == GroupMemberRole.Owner || role == GroupMemberRole.Admin) { - Text(role.text, color = MaterialTheme.colors.secondary) + if (infoPage) { + MemberInfo() + } + if (selected) { + Icon( + painterResource( + MR.images.ic_check + ), + null, + Modifier.size(20.dp), + tint = MaterialTheme.colors.primary, + ) } } } @@ -416,24 +825,50 @@ private fun MemberVerifiedShield() { } @Composable -private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState<Boolean>) { - DefaultDropdownMenu(showMenu) { - if (member.canBeRemoved(groupInfo)) { - ItemAction(stringResource(MR.strings.remove_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = { - removeMemberAlert(rhId, groupInfo, member) - showMenu.value = false - }) +private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, selectedItems: MutableState<Set<Long>?>, showMenu: MutableState<Boolean>) { + if (groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { + val canBlockForAll = member.canBlockForAll(groupInfo) + val canRemove = member.canBeRemoved(groupInfo) + if (canBlockForAll || canRemove) { + DefaultDropdownMenu(showMenu) { + if (canBlockForAll) { + if (member.blockedByAdmin) { + ItemAction(stringResource(MR.strings.unblock_for_all), painterResource(MR.images.ic_do_not_touch), onClick = { + unblockForAllAlert(rhId, groupInfo, member) + showMenu.value = false + }) + } else { + ItemAction(stringResource(MR.strings.block_for_all), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = { + blockForAllAlert(rhId, groupInfo, member) + showMenu.value = false + }) + } + } + if (canRemove) { + ItemAction(stringResource(MR.strings.remove_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = { + removeMemberAlert(rhId, groupInfo, member) + showMenu.value = false + }) + } + if (selectedItems.value == null && member.memberRole < GroupMemberRole.Moderator) { + Divider() + SelectItemAction(showMenu) { toggleItemSelection(member.groupMemberId, selectedItems) } + } + } } - if (member.memberSettings.showMessages) { - ItemAction(stringResource(MR.strings.block_member_button), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = { - blockMemberAlert(rhId, groupInfo, member) - showMenu.value = false - }) - } else { - ItemAction(stringResource(MR.strings.unblock_member_button), painterResource(MR.images.ic_do_not_touch), onClick = { - unblockMemberAlert(rhId, groupInfo, member) - showMenu.value = false - }) + } else if (!member.blockedByAdmin) { + DefaultDropdownMenu(showMenu) { + if (member.memberSettings.showMessages) { + ItemAction(stringResource(MR.strings.block_member_button), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = { + blockMemberAlert(rhId, groupInfo, member) + showMenu.value = false + }) + } else { + ItemAction(stringResource(MR.strings.unblock_member_button), painterResource(MR.images.ic_do_not_touch), onClick = { + unblockMemberAlert(rhId, groupInfo, member) + showMenu.value = false + }) + } } } } @@ -484,10 +919,10 @@ private fun AddOrEditWelcomeMessage(welcomeMessage: String?, onClick: () -> Unit } @Composable -private fun LeaveGroupButton(onClick: () -> Unit) { +private fun LeaveGroupButton(titleId: StringResource, onClick: () -> Unit) { SettingsActionItem( painterResource(MR.images.ic_logout), - stringResource(MR.strings.button_leave_group), + stringResource(titleId), onClick, iconColor = Color.Red, textColor = Color.Red @@ -495,10 +930,10 @@ private fun LeaveGroupButton(onClick: () -> Unit) { } @Composable -private fun DeleteGroupButton(onClick: () -> Unit) { +private fun DeleteGroupButton(titleId: StringResource, onClick: () -> Unit) { SettingsActionItem( painterResource(MR.images.ic_delete), - stringResource(MR.strings.button_delete_group), + stringResource(titleId), onClick, iconColor = Color.Red, textColor = Color.Red @@ -518,11 +953,51 @@ private fun SearchRowView( } } +private fun setGroupAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi { + val chatRh = chat.remoteHostId + chatModel.controller.apiSetGroupAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(chatRh, it) + } + } +} + +fun removeMembers(rhId: Long?, groupInfo: GroupInfo, memberIds: List<Long>, onSuccess: () -> Unit = {}) { + withBGApi { + val updatedMembers = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, memberIds) + if (updatedMembers != null) { + withContext(Dispatchers.Main) { + updatedMembers.forEach { updatedMember -> + chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, updatedMember) + } + } + withContext(Dispatchers.Main) { + updatedMembers.forEach { updatedMember -> + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, updatedMember) + } + } + onSuccess() + } + } +} + +fun <T> toggleItemSelection(itemId: T, selectedItems: MutableState<Set<T>?>) { + val select = selectedItems.value?.contains(itemId) != true + if (select) { + val sel = selectedItems.value ?: setOf() + selectedItems.value = sel + itemId + } else { + val sel = (selectedItems.value ?: setOf()).toMutableSet() + sel.remove(itemId) + selectedItems.value = sel + } +} + @Preview @Composable fun PreviewGroupChatInfoLayout() { SimpleXTheme { - GroupChatInfoLayout( + ModalData().GroupChatInfoLayout( chat = Chat( remoteHostId = null, chatInfo = ChatInfo.Direct.sampleData, @@ -532,10 +1007,16 @@ fun PreviewGroupChatInfoLayout() { User.sampleData, sendReceipts = remember { mutableStateOf(SendReceipts.Yes) }, setSendReceipts = {}, - members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData), + chatItemTTL = remember { mutableStateOf(ChatItemTTL.fromSeconds(0)) }, + setChatItemTTL = {}, + activeSortedMembers = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData), developerTools = false, + onLocalAliasChanged = {}, groupLink = null, - addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, + selectedItems = remember { mutableStateOf(null) }, + appBar = remember { mutableStateOf(null) }, + scrollToItemId = remember { mutableStateOf(null) }, + addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, onSearchClicked = {}, deletingItems = remember { mutableStateOf(true) } ) } } 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 02ce90243c..6e1b9a731d 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 @@ -1,9 +1,9 @@ package chat.simplex.common.views.chat.group import SectionBottomSpacer +import SectionViewWithButton +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -16,10 +16,11 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* -import chat.simplex.common.platform.shareText +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.* +import chat.simplex.common.views.usersettings.SettingsActionItem import chat.simplex.res.MR @Composable @@ -27,18 +28,18 @@ fun GroupLinkView( chatModel: ChatModel, rhId: Long?, groupInfo: GroupInfo, - connReqContact: String?, + connLinkContact: CreatedConnLink?, memberRole: GroupMemberRole?, - onGroupLinkUpdated: ((Pair<String, GroupMemberRole>?) -> Unit)?, + onGroupLinkUpdated: ((Pair<CreatedConnLink, GroupMemberRole>?) -> Unit)?, creatingGroup: Boolean = false, close: (() -> Unit)? = null ) { - var groupLink by rememberSaveable { mutableStateOf(connReqContact) } + var groupLink by rememberSaveable(stateSaver = CreatedConnLink.nullableStateSaver) { mutableStateOf(connLinkContact) } val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) } var creatingLink by rememberSaveable { mutableStateOf(false) } fun createLink() { creatingLink = true - withApi { + withBGApi { val link = chatModel.controller.apiCreateGroupLink(rhId, groupInfo.groupId) if (link != null) { groupLink = link.first @@ -78,7 +79,7 @@ fun GroupLinkView( text = generalGetString(MR.strings.all_group_members_will_remain_connected), confirmText = generalGetString(MR.strings.delete_verb), onConfirm = { - withApi { + withBGApi { val r = chatModel.controller.apiDeleteGroupLink(rhId, groupInfo.groupId) if (r) { groupLink = null @@ -99,7 +100,7 @@ fun GroupLinkView( @Composable fun GroupLinkLayout( - groupLink: String?, + groupLink: CreatedConnLink?, groupInfo: GroupInfo, groupLinkMemberRole: MutableState<GroupMemberRole?>, creatingLink: Boolean, @@ -118,10 +119,7 @@ fun GroupLinkLayout( ) } - Column( - Modifier - .verticalScroll(rememberScrollState()), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.group_link)) Text( stringResource(MR.strings.you_can_share_group_link_anybody_will_be_able_to_connect), @@ -153,7 +151,15 @@ fun GroupLinkLayout( } initialLaunch = false } - SimpleXLinkQRCode(groupLink, Modifier.aspectRatio(1f).padding(horizontal = DEFAULT_PADDING)) + val showShortLink = remember { mutableStateOf(true) } + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + if (groupLink.connShortLink == null) { + SimpleXCreatedLinkQRCode(groupLink, short = false) + } else { + SectionViewWithButton(titleButton = { ToggleShortLinkButton(showShortLink) }) { + SimpleXCreatedLinkQRCode(groupLink, short = showShortLink.value) + } + } Row( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, @@ -163,7 +169,7 @@ fun GroupLinkLayout( SimpleButton( stringResource(MR.strings.share_link), icon = painterResource(MR.images.ic_share), - click = { clipboard.shareText(simplexChatLink(groupLink)) } + click = { clipboard.shareText(groupLink.simplexChatUri(short = showShortLink.value)) } ) if (creatingGroup && close != null) { ContinueButton(close) 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 00b236c7dd..285c96165c 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 @@ -3,12 +3,12 @@ package chat.simplex.common.views.chat.group import InfoRow import SectionBottomSpacer import SectionDividerSpaced +import SectionItemView import SectionSpacer import SectionTextFooter import SectionView import androidx.compose.desktop.ui.tooling.preview.Preview -import java.net.URI -import androidx.compose.foundation.* +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent @@ -24,9 +24,9 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.helpers.* @@ -36,7 +36,9 @@ import chat.simplex.common.model.GroupInfo import chat.simplex.common.platform.* import chat.simplex.common.views.chatlist.openLoadedChat import chat.simplex.res.MR +import dev.icerock.moko.resources.StringResource import kotlinx.datetime.Clock +import kotlinx.coroutines.* @Composable fun GroupMemberInfoView( @@ -50,14 +52,31 @@ fun GroupMemberInfoView( closeAll: () -> Unit, // Close all open windows up to ChatView ) { BackHandler(onBack = close) - val chat = chatModel.chats.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId } + val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId } val connStats = remember { mutableStateOf(connectionStats) } val developerTools = chatModel.controller.appPrefs.developerTools.get() var progressIndicator by remember { mutableStateOf(false) } + fun syncMemberConnection() { + withBGApi { + val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false) + if (r != null) { + connStats.value = r.second + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } + close.invoke() + } + } + } + if (chat != null) { val newRole = remember { mutableStateOf(member.memberRole) } GroupMemberInfoLayout( + rhId = rhId, groupInfo, member, connStats, @@ -66,32 +85,43 @@ fun GroupMemberInfoView( connectionCode, getContactChat = { chatModel.getContactChat(it) }, openDirectChat = { - withApi { - val c = chatModel.controller.apiGetChat(rhId, ChatType.Direct, it) - if (c != null) { - if (chatModel.getContactChat(it) == null) { - chatModel.addChat(c) - } - chatModel.chatItems.clear() - chatModel.chatItemStatuses.clear() - chatModel.chatItems.addAll(c.chatItems) - chatModel.chatId.value = c.id + withBGApi { + apiLoadMessages(chatModel.chatsContext, rhId, ChatType.Direct, it, ChatPagination.Initial(ChatPagination.INITIAL_COUNT)) + if (chatModel.getContactChat(it) != null) { closeAll() } } }, createMemberContact = { - withApi { - progressIndicator = true - val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId) - if (memberContact != null) { - val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf()) - chatModel.addChat(memberChat) - openLoadedChat(memberChat, chatModel) - closeAll() - chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected()) + if (member.sendMsgEnabled) { + withBGApi { + progressIndicator = true + val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId) + if (memberContact != null) { + val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf()) + withContext(Dispatchers.Main) { + chatModel.chatsContext.addChat(memberChat) + } + openLoadedChat(memberChat) + closeAll() + chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected()) + } + progressIndicator = false + } + } else if (connectionStats != null) { + if (connectionStats.ratchetSyncAllowed) { + showFixConnectionAlert(syncConnection = { syncMemberConnection() }) + } else if (connectionStats.ratchetSyncInProgress) { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.cant_send_message_to_member_alert_title), + generalGetString(MR.strings.encryption_renegotiation_in_progress) + ) + } else { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.cant_send_message_to_member_alert_title), + generalGetString(MR.strings.connection_not_ready) + ) } - progressIndicator = false } }, connectViaAddress = { connReqUri -> @@ -99,31 +129,31 @@ fun GroupMemberInfoView( }, blockMember = { blockMemberAlert(rhId, groupInfo, member) }, unblockMember = { unblockMemberAlert(rhId, groupInfo, member) }, + blockForAll = { blockForAllAlert(rhId, groupInfo, member) }, + unblockForAll = { unblockForAllAlert(rhId, groupInfo, member) }, removeMember = { removeMemberDialog(rhId, groupInfo, member, chatModel, close) }, onRoleSelected = { if (it == newRole.value) return@GroupMemberInfoLayout val prevValue = newRole.value newRole.value = it - updateMemberRoleDialog(it, member, onDismiss = { + updateMemberRoleDialog(it, groupInfo, member.memberCurrent, onDismiss = { newRole.value = prevValue }) { - withApi { - kotlin.runCatching { - val mem = chatModel.controller.apiMemberRole(rhId, groupInfo.groupId, member.groupMemberId, it) - chatModel.upsertGroupMember(rhId, groupInfo, mem) - }.onFailure { - newRole.value = prevValue - } - } + updateMembersRole(newRole.value, rhId, groupInfo, listOf(member.groupMemberId), onFailure = { newRole.value = prevValue }) } }, switchMemberAddress = { showSwitchAddressAlert(switchAddress = { - withApi { + withBGApi { val r = chatModel.controller.apiSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId) if (r != null) { connStats.value = r.second - chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } close.invoke() } } @@ -131,33 +161,36 @@ fun GroupMemberInfoView( }, abortSwitchMemberAddress = { showAbortSwitchAddressAlert(abortSwitchAddress = { - withApi { + withBGApi { val r = chatModel.controller.apiAbortSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId) if (r != null) { connStats.value = r.second - chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } close.invoke() } } }) }, syncMemberConnection = { - withApi { - val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false) - if (r != null) { - connStats.value = r.second - chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) - close.invoke() - } - } + syncMemberConnection() }, syncMemberConnectionForce = { showSyncConnectionForceAlert(syncConnectionForce = { - withApi { + withBGApi { val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = true) if (r != null) { connStats.value = r.second - chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } close.invoke() } } @@ -173,15 +206,17 @@ fun GroupMemberInfoView( verify = { code -> chatModel.controller.apiVerifyGroupMember(rhId, mem.groupId, mem.groupMemberId, code)?.let { r -> val (verified, existingCode) = r - chatModel.upsertGroupMember( - rhId, - groupInfo, - mem.copy( - activeConn = mem.activeConn?.copy( - connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null - ) + val copy = mem.copy( + activeConn = mem.activeConn?.copy( + connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null ) ) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, copy) + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, copy) + } r } }, @@ -199,15 +234,28 @@ 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.showAlertDialog( title = generalGetString(MR.strings.button_remove_member), - text = generalGetString(MR.strings.member_will_be_removed_from_group_cannot_be_undone), + text = generalGetString(messageId), confirmText = generalGetString(MR.strings.remove_member_confirmation), onConfirm = { - withApi { - val removedMember = chatModel.controller.apiRemoveMember(rhId, member.groupId, member.groupMemberId) - if (removedMember != null) { - chatModel.upsertGroupMember(rhId, groupInfo, removedMember) + withBGApi { + val removedMembers = chatModel.controller.apiRemoveMembers(rhId, member.groupId, listOf(member.groupMemberId)) + if (removedMembers != null) { + withContext(Dispatchers.Main) { + removedMembers.forEach { removedMember -> + chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, removedMember) + } + } + withContext(Dispatchers.Main) { + removedMembers.forEach { removedMember -> + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, removedMember) + } + } } close?.invoke() } @@ -218,6 +266,7 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c @Composable fun GroupMemberInfoLayout( + rhId: Long?, groupInfo: GroupInfo, member: GroupMember, connStats: MutableState<ConnectionStats?>, @@ -230,6 +279,8 @@ fun GroupMemberInfoLayout( connectViaAddress: (String) -> Unit, blockMember: () -> Unit, unblockMember: () -> Unit, + blockForAll: () -> Unit, + unblockForAll: () -> Unit, removeMember: () -> Unit, onRoleSelected: (GroupMemberRole) -> Unit, switchMemberAddress: () -> Unit, @@ -239,20 +290,56 @@ fun GroupMemberInfoLayout( verifyClicked: () -> Unit, ) { val cStats = connStats.value - fun knownDirectChat(contactId: Long): Chat? { + fun knownDirectChat(contactId: Long): Pair<Chat, Contact>? { val chat = getContactChat(contactId) return if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) { - chat + chat to chat.chatInfo.contact } else { null } } - Column( - Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()), - ) { + @Composable + fun ModeratorDestructiveSection() { + val canBlockForAll = member.canBlockForAll(groupInfo) + val canRemove = member.canBeRemoved(groupInfo) + if (canBlockForAll || canRemove) { + SectionDividerSpaced(maxBottomPadding = false) + SectionView { + if (canBlockForAll) { + if (member.blockedByAdmin) { + UnblockForAllButton(unblockForAll) + } else { + BlockForAllButton(blockForAll) + } + } + if (canRemove) { + RemoveMemberButton(removeMember) + } + } + } + } + + @Composable + fun NonAdminBlockSection() { + SectionDividerSpaced(maxBottomPadding = false) + SectionView { + if (member.blockedByAdmin) { + SettingsActionItem( + painterResource(MR.images.ic_back_hand), + stringResource(MR.strings.member_blocked_by_admin), + click = null, + disabled = true + ) + } else if (member.memberSettings.showMessages) { + BlockMemberButton(blockMember) + } else { + UnblockMemberButton(unblockMember) + } + } + } + + ColumnWithScrollBar { Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center @@ -263,17 +350,69 @@ fun GroupMemberInfoLayout( val contactId = member.memberContactId + Box( + Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Row( + Modifier + .widthIn(max = 320.dp) + .padding(horizontal = DEFAULT_PADDING), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + val knownChat = if (contactId != null) knownDirectChat(contactId) else null + if (knownChat != null) { + val (chat, contact) = knownChat + val knownContactConnectionStats: MutableState<ConnectionStats?> = remember { mutableStateOf(null) } + + LaunchedEffect(contact.contactId) { + withBGApi { + val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, chat.chatInfo.apiId) + if (contactInfo != null) { + knownContactConnectionStats.value = contactInfo.first + } + } + } + + OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { openDirectChat(contact.contactId) }) + AudioCallButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact, knownContactConnectionStats) + VideoButton(modifier = Modifier.fillMaxWidth(1f), chat, contact, knownContactConnectionStats) + } else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) { + if (contactId != null) { + OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { openDirectChat(contactId) }) // legacy - only relevant for direct contacts created when joining group + } else { + OpenChatButton( + modifier = Modifier.fillMaxWidth(0.33f), + disabledLook = !(member.sendMsgEnabled || (member.activeConn?.connectionStats?.ratchetSyncAllowed ?: false)), + onClick = { createMemberContact() } + ) + } + InfoViewActionButton(modifier = Modifier.fillMaxWidth(0.5f), painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = { + showSendMessageToEnableCallsAlert() + }) + InfoViewActionButton(modifier = Modifier.fillMaxWidth(1f), painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = { + showSendMessageToEnableCallsAlert() + }) + } else { // no known contact chat && directMessages are off + val messageId = if (groupInfo.businessChat == null) MR.strings.direct_messages_are_prohibited_in_group else MR.strings.direct_messages_are_prohibited_in_chat + InfoViewActionButton(modifier = Modifier.fillMaxWidth(0.33f), painterResource(MR.images.ic_chat_bubble), generalGetString(MR.strings.info_view_message_button), disabled = false, disabledLook = true, onClick = { + showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_send_message_to_member_alert_title), messageId) + }) + InfoViewActionButton(modifier = Modifier.fillMaxWidth(0.5f), painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = { + showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title), messageId) + }) + InfoViewActionButton(modifier = Modifier.fillMaxWidth(1f), painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = { + showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title), messageId) + }) + } + } + } + + SectionSpacer() + if (member.memberActive) { SectionView { - if (contactId != null && knownDirectChat(contactId) != null) { - OpenChatButton(onClick = { openDirectChat(contactId) }) - } else if (groupInfo.fullGroupPreferences.directMessages.on) { - if (contactId != null) { - OpenChatButton(onClick = { openDirectChat(contactId) }) - } else if (member.activeConn?.peerChatVRange?.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) == true) { - OpenChatButton(onClick = { createMemberContact() }) - } - } if (connectionCode != null) { VerifyCodeButton(member.verified, verifyClicked) } @@ -289,11 +428,11 @@ fun GroupMemberInfoLayout( if (member.contactLink != null) { SectionView(stringResource(MR.strings.address_section_title).uppercase()) { - SimpleXLinkQRCode(member.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) + SimpleXLinkQRCode(member.contactLink) val clipboard = LocalClipboardManager.current ShareAddressButton { clipboard.shareText(simplexChatLink(member.contactLink)) } if (contactId != null) { - if (knownDirectChat(contactId) == null && !groupInfo.fullGroupPreferences.directMessages.on) { + if (knownDirectChat(contactId) == null && !groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) { ConnectViaAddressButton(onClick = { connectViaAddress(member.contactLink) }) } } else { @@ -305,31 +444,25 @@ fun GroupMemberInfoLayout( } SectionView(title = stringResource(MR.strings.member_info_section_title_member)) { - InfoRow(stringResource(MR.strings.info_row_group), groupInfo.displayName) + val titleId = if (groupInfo.businessChat == null) MR.strings.info_row_group else MR.strings.info_row_chat + InfoRow(stringResource(titleId), groupInfo.displayName) val roles = remember { member.canChangeRoleTo(groupInfo) } if (roles != null) { RoleSelectionRow(roles, newRole, onRoleSelected) } else { InfoRow(stringResource(MR.strings.role_in_group), member.memberRole.text) } - val conn = member.activeConn - if (conn != null) { - val connLevelDesc = - if (conn.connLevel == 0) stringResource(MR.strings.conn_level_desc_direct) - else String.format(generalGetString(MR.strings.conn_level_desc_indirect), conn.connLevel) - InfoRow(stringResource(MR.strings.info_row_connection), connLevelDesc) - } } if (cStats != null) { SectionDividerSpaced() SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) { SwitchAddressButton( - disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || cStats.ratchetSyncSendProhibited, + disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || !member.sendMsgEnabled, switchAddress = switchMemberAddress ) if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) { AbortSwitchAddressButton( - disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch } || cStats.ratchetSyncSendProhibited, + disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch } || !member.sendMsgEnabled, abortSwitchAddress = abortSwitchMemberAddress ) } @@ -344,16 +477,10 @@ fun GroupMemberInfoLayout( } } - SectionDividerSpaced(maxBottomPadding = false) - SectionView { - if (member.memberSettings.showMessages) { - BlockMemberButton(blockMember) - } else { - UnblockMemberButton(unblockMember) - } - if (member.canBeRemoved(groupInfo)) { - RemoveMemberButton(removeMember) - } + if (groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { + ModeratorDestructiveSection() + } else { + NonAdminBlockSection() } if (developerTools) { @@ -361,19 +488,53 @@ fun GroupMemberInfoLayout( SectionView(title = stringResource(MR.strings.section_title_for_console)) { InfoRow(stringResource(MR.strings.info_row_local_name), member.localDisplayName) InfoRow(stringResource(MR.strings.info_row_database_id), member.groupMemberId.toString()) + val conn = member.activeConn + if (conn != null) { + val connLevelDesc = + if (conn.connLevel == 0) stringResource(MR.strings.conn_level_desc_direct) + else String.format(generalGetString(MR.strings.conn_level_desc_indirect), conn.connLevel) + InfoRow(stringResource(MR.strings.info_row_connection), connLevelDesc) + } + SectionItemView({ + withBGApi { + val info = controller.apiGroupMemberQueueInfo(rhId, groupInfo.apiId, member.groupMemberId) + if (info != null) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.message_queue_info), + text = queueInfoText(info) + ) + } + } + }) { + Text(stringResource(MR.strings.info_row_debug_delivery)) + } } } SectionBottomSpacer() } } +private fun showSendMessageToEnableCallsAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.cant_call_member_alert_title), + text = generalGetString(MR.strings.cant_call_member_send_message_alert_text) + ) +} + +private fun showDirectMessagesProhibitedAlert(title: String, messageId: StringResource) { + AlertManager.shared.showAlertMsg( + title = title, + text = generalGetString(messageId) + ) +} + @Composable fun GroupMemberInfoHeader(member: GroupMember) { Column( - Modifier.padding(horizontal = 8.dp), + Modifier.padding(horizontal = 16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - ProfileImage(size = 192.dp, member.image, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight) + MemberProfileImage(size = 192.dp, member, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight) val text = buildAnnotatedString { if (member.verified) { appendInlineContent(id = "shieldIcon") @@ -387,13 +548,19 @@ fun GroupMemberInfoHeader(member: GroupMember) { Icon(painterResource(MR.images.ic_verified_user), null, tint = MaterialTheme.colors.secondary) } ) + val clipboard = LocalClipboardManager.current + val copyNameToClipboard = { + clipboard.setText(AnnotatedString(member.displayName)) + showToast(generalGetString(MR.strings.copied)) + } Text( text, inlineContent = inlineContent, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal), textAlign = TextAlign.Center, maxLines = 3, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard) ) if (member.fullName != "" && member.fullName != member.displayName) { Text( @@ -401,7 +568,8 @@ fun GroupMemberInfoHeader(member: GroupMember) { color = MaterialTheme.colors.onBackground, textAlign = TextAlign.Center, maxLines = 4, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard) ) } } @@ -427,6 +595,26 @@ fun UnblockMemberButton(onClick: () -> Unit) { ) } +@Composable +fun BlockForAllButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_back_hand), + stringResource(MR.strings.block_for_all), + click = onClick, + textColor = Color.Red, + iconColor = Color.Red, + ) +} + +@Composable +fun UnblockForAllButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_do_not_touch), + stringResource(MR.strings.unblock_for_all), + click = onClick + ) +} + @Composable fun RemoveMemberButton(onClick: () -> Unit) { SettingsActionItem( @@ -439,13 +627,18 @@ fun RemoveMemberButton(onClick: () -> Unit) { } @Composable -fun OpenChatButton(onClick: () -> Unit) { - SettingsActionItem( - painterResource(MR.images.ic_chat), - stringResource(MR.strings.button_send_direct_message), - click = onClick, - textColor = MaterialTheme.colors.primary, - iconColor = MaterialTheme.colors.primary, +fun OpenChatButton( + modifier: Modifier, + disabledLook: Boolean = false, + onClick: () -> Unit +) { + InfoViewActionButton( + modifier = modifier, + icon = painterResource(MR.images.ic_chat_bubble), + title = generalGetString(MR.strings.info_view_message_button), + disabled = false, + disabledLook = disabledLook, + onClick = onClick ) } @@ -483,17 +676,58 @@ private fun RoleSelectionRow( } } -private fun updateMemberRoleDialog( +@Composable +fun MemberProfileImage( + size: Dp, + mem: GroupMember, + color: Color = MaterialTheme.colors.secondaryVariant, + backgroundColor: Color? = null +) { + ProfileImage( + size = size, + image = mem.image, + color = color, + backgroundColor = backgroundColor, + blurred = mem.blocked + ) +} + +fun updateMembersRole(newRole: GroupMemberRole, rhId: Long?, groupInfo: GroupInfo, memberIds: List<Long>, onFailure: () -> Unit = {}, onSuccess: () -> Unit = {}) { + withBGApi { + kotlin.runCatching { + val members = chatModel.controller.apiMembersRole(rhId, groupInfo.groupId, memberIds, newRole) + withContext(Dispatchers.Main) { + members.forEach { member -> + chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, member) + } + } + withContext(Dispatchers.Main) { + members.forEach { member -> + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, member) + } + } + onSuccess() + }.onFailure { + onFailure() + } + } +} + +fun updateMemberRoleDialog( newRole: GroupMemberRole, - member: GroupMember, + groupInfo: GroupInfo, + memberCurrent: Boolean, onDismiss: () -> Unit, onConfirm: () -> Unit ) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.change_member_role_question), - text = if (member.memberCurrent) - String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text) - else + text = if (memberCurrent) { + if (groupInfo.businessChat == null) + String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text) + else + String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_chat), newRole.text) + } else String.format(generalGetString(MR.strings.member_role_will_be_changed_with_invitation), newRole.text), confirmText = generalGetString(MR.strings.change_verb), onDismiss = onDismiss, @@ -502,11 +736,26 @@ private fun updateMemberRoleDialog( ) } +fun updateMembersRoleDialog( + newRole: GroupMemberRole, + groupInfo: GroupInfo, + onConfirm: () -> Unit +) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.change_member_role_question), + text = if (groupInfo.businessChat == null) + String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text) + else + String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_chat), newRole.text), + confirmText = generalGetString(MR.strings.change_verb), + onConfirm = onConfirm, + ) +} + fun connectViaMemberAddressAlert(rhId: Long?, connReqUri: String) { try { - val uri = URI(connReqUri) - withApi { - planAndConnect(chatModel, rhId, uri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() }) + withBGApi { + planAndConnect(rhId, connReqUri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() }) } } catch (e: RuntimeException) { AlertManager.shared.showAlertMsg( @@ -548,16 +797,85 @@ fun updateMemberSettings(rhId: Long?, gInfo: GroupInfo, member: GroupMember, mem withBGApi { val success = ChatController.apiSetMemberSettings(rhId, gInfo.groupId, member.groupMemberId, memberSettings) if (success) { - ChatModel.upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings)) + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings)) + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings)) + } } } } +fun blockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.block_for_all_question), + text = generalGetString(MR.strings.block_member_desc).format(mem.chatViewName), + confirmText = generalGetString(MR.strings.block_for_all), + onConfirm = { + blockMemberForAll(rhId, gInfo, listOf(mem.groupMemberId), true) + }, + destructive = true, + ) +} + +fun blockForAllAlert(rhId: Long?, gInfo: GroupInfo, memberIds: List<Long>, onSuccess: () -> Unit = {}) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.block_members_for_all_question), + text = generalGetString(MR.strings.block_members_desc), + confirmText = generalGetString(MR.strings.block_for_all), + onConfirm = { + blockMemberForAll(rhId, gInfo, memberIds, true, onSuccess) + }, + destructive = true, + ) +} + +fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.unblock_for_all_question), + text = generalGetString(MR.strings.unblock_member_desc).format(mem.chatViewName), + confirmText = generalGetString(MR.strings.unblock_for_all), + onConfirm = { + blockMemberForAll(rhId, gInfo, listOf(mem.groupMemberId), false) + }, + ) +} + +fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, memberIds: List<Long>, onSuccess: () -> Unit = {}) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.unblock_members_for_all_question), + text = generalGetString(MR.strings.unblock_members_desc), + confirmText = generalGetString(MR.strings.unblock_for_all), + onConfirm = { + blockMemberForAll(rhId, gInfo, memberIds, false, onSuccess) + }, + ) +} + +fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, memberIds: List<Long>, blocked: Boolean, onSuccess: () -> Unit = {}) { + withBGApi { + val updatedMembers = ChatController.apiBlockMembersForAll(rhId, gInfo.groupId, memberIds, blocked) + withContext(Dispatchers.Main) { + updatedMembers.forEach { updatedMember -> + chatModel.chatsContext.upsertGroupMember(rhId, gInfo, updatedMember) + } + } + withContext(Dispatchers.Main) { + updatedMembers.forEach { updatedMember -> + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, gInfo, updatedMember) + } + } + onSuccess() + } +} + @Preview @Composable fun PreviewGroupMemberInfoLayout() { SimpleXTheme { GroupMemberInfoLayout( + rhId = null, groupInfo = GroupInfo.sampleData, member = GroupMember.sampleData, connStats = remember { mutableStateOf(null) }, @@ -570,6 +888,8 @@ fun PreviewGroupMemberInfoLayout() { connectViaAddress = {}, blockMember = {}, unblockMember = {}, + blockForAll = {}, + unblockForAll = {}, removeMember = {}, onRoleSelected = {}, switchMemberAddress = {}, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt new file mode 100644 index 0000000000..2c4d4b16a8 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt @@ -0,0 +1,129 @@ +package chat.simplex.common.views.chat.group + +import androidx.compose.foundation.background +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.draw.alpha +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.max +import chat.simplex.common.model.* +import chat.simplex.common.platform.chatModel +import chat.simplex.common.ui.theme.WarningOrange +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource + +@Composable +fun SelectedItemsMembersToolbar( + selectedItems: MutableState<Set<Long>?>, + activeMembers: State<List<GroupMember>>, + groupInfo: GroupInfo, + delete: () -> Unit, + blockForAll: (Boolean) -> Unit, // Boolean - block or unlock + changeRole: (GroupMemberRole) -> Unit, +) { + val deleteEnabled = remember { mutableStateOf(false) } + val blockForAllEnabled = remember { mutableStateOf(false) } + val unblockForAllEnabled = remember { mutableStateOf(false) } + val blockForAllButtonEnabled = remember { derivedStateOf { (blockForAllEnabled.value && !unblockForAllEnabled.value) || (!blockForAllEnabled.value && unblockForAllEnabled.value) } } + + val roleToMemberEnabled = remember { mutableStateOf(false) } + val roleToObserverEnabled = remember { mutableStateOf(false) } + val roleButtonEnabled = remember { derivedStateOf { (roleToMemberEnabled.value && !roleToObserverEnabled.value) || (!roleToMemberEnabled.value && roleToObserverEnabled.value) } } + Box( + Modifier + .background(MaterialTheme.colors.background) + .navigationBarsPadding() + .imePadding() + ) { + // It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty + Box(Modifier.alpha(0f)) { + ComposeView(chatModel = chatModel, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() }) + } + Row( + Modifier + .matchParentSize() + .padding(horizontal = 2.dp) + .height(AppBarHeight * fontSizeSqrtMultiplier) + .pointerInput(Unit) { + detectGesture { + true + } + }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton(delete, enabled = deleteEnabled.value) { + Icon( + painterResource(MR.images.ic_delete), + null, + Modifier.size(22.dp), + tint = if (!deleteEnabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error + ) + } + + IconButton({ blockForAll(blockForAllEnabled.value) }, enabled = blockForAllButtonEnabled.value) { + Icon( + painterResource(if (unblockForAllEnabled.value && blockForAllButtonEnabled.value) MR.images.ic_do_not_touch else MR.images.ic_back_hand), + null, + Modifier.size(22.dp), + tint = if (!blockForAllButtonEnabled.value) MaterialTheme.colors.secondary else if (blockForAllEnabled.value) MaterialTheme.colors.error else WarningOrange + ) + } + + IconButton({ changeRole(if (roleToMemberEnabled.value) GroupMemberRole.Member else GroupMemberRole.Observer) }, enabled = roleButtonEnabled.value) { + Icon( + painterResource(if (roleToObserverEnabled.value || !roleButtonEnabled.value) MR.images.ic_person else MR.images.ic_person_edit), + null, + Modifier.size(22.dp), + tint = if (!roleButtonEnabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + } + Divider(Modifier.align(Alignment.TopStart)) + } + LaunchedEffect(groupInfo, activeMembers.value.toList(), selectedItems.value) { + recheckItems(groupInfo, selectedItems, activeMembers.value, deleteEnabled, blockForAllEnabled, unblockForAllEnabled, roleToMemberEnabled, roleToObserverEnabled) + } +} + +private fun recheckItems( + groupInfo: GroupInfo, + selectedItems: MutableState<Set<Long>?>, + activeMembers: List<GroupMember>, + deleteEnabled: MutableState<Boolean>, + blockForAllEnabled: MutableState<Boolean>, + unblockForAllEnabled: MutableState<Boolean>, + roleToMemberEnabled: MutableState<Boolean>, + roleToObserverEnabled: MutableState<Boolean>, +) { + val selected = selectedItems.value ?: return + var rDeleteEnabled = true + var rBlockForAllEnabled = true + var rUnblockForAllEnabled = true + var rRoleToMemberEnabled = true + var rRoleToObserverEnabled = true + val rSelectedItems = mutableSetOf<Long>() + for (mem in activeMembers) { + if (selected.contains(mem.groupMemberId) && groupInfo.membership.memberRole >= mem.memberRole && mem.memberRole < GroupMemberRole.Moderator && groupInfo.membership.memberActive) { + rDeleteEnabled = rDeleteEnabled && mem.memberStatus != GroupMemberStatus.MemRemoved && mem.memberStatus != GroupMemberStatus.MemLeft + rBlockForAllEnabled = rBlockForAllEnabled && !mem.blockedByAdmin + rUnblockForAllEnabled = rUnblockForAllEnabled && mem.blockedByAdmin + rRoleToMemberEnabled = rRoleToMemberEnabled && mem.memberRole != GroupMemberRole.Member + rRoleToObserverEnabled = rRoleToObserverEnabled && mem.memberRole != GroupMemberRole.Observer + rSelectedItems.add(mem.groupMemberId) // we are collecting new selected items here to account for any changes in members list + } + } + deleteEnabled.value = rDeleteEnabled + blockForAllEnabled.value = rBlockForAllEnabled + unblockForAllEnabled.value = rUnblockForAllEnabled + roleToMemberEnabled.value = rRoleToMemberEnabled + roleToObserverEnabled.value = rRoleToObserverEnabled + selectedItems.value = rSelectedItems +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt new file mode 100644 index 0000000000..91f4f5173c --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt @@ -0,0 +1,306 @@ +package chat.simplex.common.views.chat.group + +import androidx.compose.animation.core.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.unit.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.chatlist.setGroupMembers +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import kotlinx.coroutines.launch +import kotlin.text.CharCategory.* + +val punctuation = setOf( + DASH_PUNCTUATION, START_PUNCTUATION, END_PUNCTUATION, + CONNECTOR_PUNCTUATION, OTHER_PUNCTUATION +) + +private val PICKER_ROW_SIZE = MEMBER_ROW_AVATAR_SIZE + (MEMBER_ROW_VERTICAL_PADDING * 2f) +private val MAX_PICKER_HEIGHT = (PICKER_ROW_SIZE * 4) + (MEMBER_ROW_AVATAR_SIZE + MEMBER_ROW_VERTICAL_PADDING - 4.dp) + +@Composable +fun GroupMentions( + rhId: Long?, + composeState: MutableState<ComposeState>, + composeViewFocusRequester: FocusRequester?, + chatInfo: ChatInfo.Group +) { + val maxHeightInPx = with(LocalDensity.current) { windowHeight().toPx() } + val isVisible = remember { mutableStateOf(false) } + val offsetY = remember { Animatable(maxHeightInPx) } + + val currentMessage = remember { mutableStateOf(composeState.value.message) } + val mentionName = remember { mutableStateOf("") } + val mentionRange = remember { mutableStateOf<TextRange?>(null) } + val mentionMemberId = remember { mutableStateOf<String?>(null) } + val filteredMembers = remember { + derivedStateOf { + val members = chatModel.groupMembers.value + .filter { + val status = it.memberStatus + status != GroupMemberStatus.MemLeft && status != GroupMemberStatus.MemRemoved && status != GroupMemberStatus.MemInvited + } + .sortedByDescending { it.memberRole } + + if (mentionName.value.isEmpty()) { + members + } else { + members.filter { it.memberProfile.anyNameContains(mentionName.value) } + } + } + } + val scope = rememberCoroutineScope() + + suspend fun closeMembersPicker() { + isVisible.value = false + if (offsetY.value != 0f) { + return + } + + offsetY.animateTo( + targetValue = maxHeightInPx, + animationSpec = mentionPickerAnimSpec() + ) + mentionName.value = "" + mentionRange.value = null + mentionMemberId.value = null + } + + fun messageChanged(msg: ComposeMessage, parsedMsg: List<FormattedText>) { + removeUnusedMentions(composeState, parsedMsg) + val selected = selectedMarkdown(parsedMsg, msg.selection) + + if (selected != null) { + val (ft, r) = selected + + when (ft.format) { + is Format.Mention -> { + isVisible.value = true + mentionName.value = ft.format.memberName + mentionRange.value = r + mentionMemberId.value = composeState.value.mentions[mentionName.value]?.memberId + if (!chatModel.membersLoaded.value) { + scope.launch { + setGroupMembers(rhId, chatInfo.groupInfo, chatModel) + } + } + return + } + null -> { + val pos = msg.selection.start + if (msg.selection.length == 0 && getCharacter(msg.text, pos - 1)?.first == "@") { + val prevChar = getCharacter(msg.text, pos - 2)?.first + if (prevChar == null || prevChar == " " || prevChar == "\n") { + isVisible.value = true + mentionName.value = "" + mentionRange.value = TextRange(pos - 1, pos) + mentionMemberId.value = null + scope.launch { + setGroupMembers(rhId, chatInfo.groupInfo, chatModel) + } + return + } + } + } + else -> {} + } + } + scope.launch { + closeMembersPicker() + } + } + + fun addMemberMention(member: GroupMember, range: TextRange) { + val mentions = composeState.value.mentions.toMutableMap() + val existingMention = mentions.entries.firstOrNull { + it.value.memberId == member.memberId + } + val newName = existingMention?.key ?: composeState.value.mentionMemberName(member.memberProfile.displayName) + mentions[newName] = CIMention(member) + var msgMention = if (newName.contains(" ") || (newName.lastOrNull()?.category in punctuation)) + "@'$newName'" + else "@$newName" + var newPos = range.start + msgMention.length + val newMsgLength = composeState.value.message.text.length + msgMention.length - range.length + if (newPos == newMsgLength) { + msgMention += " " + newPos += 1 + } + + val msg = composeState.value.message.text.replaceRange( + range.start, + range.end, + msgMention + ) + composeState.value = composeState.value.copy( + message = ComposeMessage(msg, TextRange(newPos)), + parsedMessage = parseToMarkdown(msg) ?: FormattedText.plain(msg), + mentions = mentions + ) + + composeViewFocusRequester?.requestFocus() + + scope.launch { + closeMembersPicker() + } + } + + LaunchedEffect(composeState.value.parsedMessage) { + currentMessage.value = composeState.value.message + messageChanged(currentMessage.value, composeState.value.parsedMessage) + } + +// KeyChangeEffect(composeState.value.message.selection) { +// // This condition is needed to prevent messageChanged called twice, +// // because composeState.formattedText triggers later when message changes. +// // The condition is only true if position changed without text change +// if (currentMessage.value.text == composeState.value.message.text) { +// messageChanged(currentMessage.value, composeState.value.parsedMessage) +// } +// } + + LaunchedEffect(isVisible.value) { + if (isVisible.value) { + offsetY.animateTo( + targetValue = 0f, + animationSpec = mentionPickerAnimSpec() + ) + } + } + Box( + modifier = Modifier + .fillMaxSize() + .offset { IntOffset(0, offsetY.value.toInt()) } + .clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) { + scope.launch { closeMembersPicker() } + }, + contentAlignment = Alignment.BottomStart + ) { + val showMaxReachedBox = composeState.value.mentions.size >= MAX_NUMBER_OF_MENTIONS && isVisible.value && composeState.value.mentions[mentionName.value] == null + LazyColumnWithScrollBarNoAppBar( + Modifier + .heightIn(max = MAX_PICKER_HEIGHT) + .background(MaterialTheme.colors.surface), + maxHeight = remember { mutableStateOf(MAX_PICKER_HEIGHT) }, + containerAlignment = Alignment.BottomEnd + ) { + if (showMaxReachedBox) { + stickyHeader { + MaxMentionsReached() + } + } + itemsIndexed(filteredMembers.value, key = { _, item -> item.groupMemberId }) { i, member -> + if (i != 0 || !showMaxReachedBox) { + Divider() + } + val mentioned = mentionMemberId.value == member.memberId + val disabled = composeState.value.mentions.size >= MAX_NUMBER_OF_MENTIONS && !mentioned + Row( + Modifier + .fillMaxWidth() + .alpha(if (disabled) 0.6f else 1f) + .clickable(enabled = !disabled) { + val range = mentionRange.value ?: return@clickable + val mentionMemberValue = mentionMemberId.value + + if (mentionMemberValue != null) { + if (mentionMemberValue != member.memberId) { + addMemberMention(member, range) + } else { + return@clickable + } + } else { + addMemberMention(member, range) + } + } + .padding(horizontal = DEFAULT_PADDING_HALF), + verticalAlignment = Alignment.CenterVertically + ) { + MemberRow( + member, + infoPage = false, + showlocalAliasAndFullName = true, + selected = mentioned + ) + } + } + } + } +} + +@Composable +private fun MaxMentionsReached() { + Column(Modifier.background(MaterialTheme.colors.surface)) { + Divider() + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + String.format(generalGetString(MR.strings.max_group_mentions_per_message_reached), MAX_NUMBER_OF_MENTIONS), + Modifier.padding(12.dp), + ) + } + Divider() + } +} + +private fun getCharacter(s: String, pos: Int): Pair<CharSequence, IntRange>? { + return if (pos in s.indices) { + val char = s.subSequence(pos, pos + 1) + char to (pos until pos + 1) + } else { + null + } +} + +private fun selectedMarkdown( + parsedMsg: List<FormattedText>, + range: TextRange +): Pair<FormattedText, TextRange>? { + if (parsedMsg.isEmpty()) return null + + var i = 0 + var pos = 0 + + while (i < parsedMsg.size && pos + parsedMsg[i].text.length < range.start) { + pos += parsedMsg[i].text.length + i++ + } + + return if (i >= parsedMsg.size || range.end > pos + parsedMsg[i].text.length) { + null + } else { + parsedMsg[i] to TextRange(pos, pos + parsedMsg[i].text.length) + } +} + +private fun removeUnusedMentions(composeState: MutableState<ComposeState>, parsedMsg: List<FormattedText>) { + val usedMentions = parsedMsg.mapNotNull { ft -> + when (ft.format) { + is Format.Mention -> ft.format.memberName + else -> null + } + }.toSet() + + if (usedMentions.size < composeState.value.mentions.size) { + composeState.value = composeState.value.copy( + mentions = composeState.value.mentions.filterKeys { it in usedMentions } + ) + } +} 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 3cdfaad2d9..12c5b65769 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 @@ -6,38 +6,50 @@ import SectionDividerSpaced import SectionItemView import SectionTextFooter import SectionView -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.PreferenceToggleWithIcon import chat.simplex.common.model.* +import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.chatModel import chat.simplex.res.MR +import kotlinx.coroutines.* + +private val featureRoles: List<Pair<GroupMemberRole?, String>> = listOf( + null to generalGetString(MR.strings.feature_roles_all_members), + GroupMemberRole.Moderator to generalGetString(MR.strings.feature_roles_moderators), + GroupMemberRole.Admin to generalGetString(MR.strings.feature_roles_admins), + GroupMemberRole.Owner to generalGetString(MR.strings.feature_roles_owners) +) @Composable -fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> Unit,) { +fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> Unit) { val groupInfo = remember { derivedStateOf { val ch = m.getChat(chatId) val g = (ch?.chatInfo as? ChatInfo.Group)?.groupInfo - if (g == null || ch?.remoteHostId != rhId) null else g + if (g == null || ch.remoteHostId != rhId) null else g }} val gInfo = groupInfo.value ?: return var preferences by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(gInfo.fullGroupPreferences) } var currentPreferences by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(preferences) } fun savePrefs(afterSave: () -> Unit = {}) { - withApi { + withBGApi { val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences()) val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp) if (g != null) { - m.updateGroup(rhId, g) - currentPreferences = preferences + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, g) + currentPreferences = preferences + } + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, g) + } } afterSave() } @@ -72,15 +84,14 @@ private fun GroupPreferencesLayout( reset: () -> Unit, savePrefs: () -> Unit, ) { - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - ) { - AppBarTitle(stringResource(MR.strings.group_preferences)) + ColumnWithScrollBar { + val titleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences + AppBarTitle(stringResource(titleId)) 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, groupInfo, preferences, onTTLUpdated) { enable -> + 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))) } else { @@ -89,30 +100,51 @@ private fun GroupPreferencesLayout( } SectionDividerSpaced(true, maxBottomPadding = false) val allowDirectMessages = remember(preferences) { mutableStateOf(preferences.directMessages.enable) } - FeatureSection(GroupFeature.DirectMessages, allowDirectMessages, groupInfo, preferences, onTTLUpdated) { - applyPrefs(preferences.copy(directMessages = GroupPreference(enable = it))) + 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) val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.enable) } - FeatureSection(GroupFeature.FullDelete, allowFullDeletion, groupInfo, preferences, onTTLUpdated) { - applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = it))) + FeatureSection(GroupFeature.FullDelete, allowFullDeletion, null, groupInfo, preferences, onTTLUpdated) { enable, _ -> + applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = enable))) } SectionDividerSpaced(true, maxBottomPadding = false) val allowReactions = remember(preferences) { mutableStateOf(preferences.reactions.enable) } - FeatureSection(GroupFeature.Reactions, allowReactions, groupInfo, preferences, onTTLUpdated) { - applyPrefs(preferences.copy(reactions = GroupPreference(enable = it))) + FeatureSection(GroupFeature.Reactions, allowReactions, null, groupInfo, preferences, onTTLUpdated) { enable, _ -> + applyPrefs(preferences.copy(reactions = GroupPreference(enable = enable))) } SectionDividerSpaced(true, maxBottomPadding = false) val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.enable) } - FeatureSection(GroupFeature.Voice, allowVoice, groupInfo, preferences, onTTLUpdated) { - applyPrefs(preferences.copy(voice = GroupPreference(enable = it))) + 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) val allowFiles = remember(preferences) { mutableStateOf(preferences.files.enable) } - FeatureSection(GroupFeature.Files, allowFiles, groupInfo, preferences, onTTLUpdated) { - applyPrefs(preferences.copy(files = GroupPreference(enable = it))) + 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))) } - if (groupInfo.canEdit) { + + SectionDividerSpaced(true, maxBottomPadding = false) + 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) + val enableReports = remember(preferences) { mutableStateOf(preferences.reports.enable) } + FeatureSection(GroupFeature.Reports, enableReports, null, groupInfo, preferences, onTTLUpdated) { enable, _ -> + applyPrefs(preferences.copy(reports = GroupPreference(enable = enable))) + } + SectionDividerSpaced(true, maxBottomPadding = false) + val enableHistory = remember(preferences) { mutableStateOf(preferences.history.enable) } + FeatureSection(GroupFeature.History, enableHistory, null, groupInfo, preferences, onTTLUpdated) { enable, _ -> + applyPrefs(preferences.copy(history = GroupPreference(enable = enable))) + } + if (groupInfo.isOwner) { SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) ResetSaveButtons( reset = reset, @@ -128,24 +160,26 @@ private fun GroupPreferencesLayout( private fun FeatureSection( feature: GroupFeature, enableFeature: State<GroupFeatureEnabled>, + enableForRole: State<GroupMemberRole?>? = null, groupInfo: GroupInfo, preferences: FullGroupPreferences, onTTLUpdated: (Int?) -> Unit, - onSelected: (GroupFeatureEnabled) -> Unit + onSelected: (GroupFeatureEnabled, GroupMemberRole?) -> Unit ) { SectionView { val on = enableFeature.value == GroupFeatureEnabled.ON val icon = if (on) feature.iconFilled() else feature.icon val iconTint = if (on) SimplexGreen else MaterialTheme.colors.secondary val timedOn = feature == GroupFeature.TimedMessages && enableFeature.value == GroupFeatureEnabled.ON - if (groupInfo.canEdit) { + if (groupInfo.isOwner) { PreferenceToggleWithIcon( feature.text, icon, iconTint, - enableFeature.value == GroupFeatureEnabled.ON, + disabled = feature == GroupFeature.Reports, // remove in 6.4 + checked = enableFeature.value == GroupFeatureEnabled.ON, ) { checked -> - onSelected(if (checked) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF) + onSelected(if (checked) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF, enableForRole?.value) } if (timedOn) { val ttl = rememberSaveable(preferences.timedMessages) { mutableStateOf(preferences.timedMessages.ttl) } @@ -159,6 +193,16 @@ private fun FeatureSection( onSelected = onTTLUpdated ) } + if (enableFeature.value == GroupFeatureEnabled.ON && enableForRole != null) { + ExposedDropDownSettingRow( + generalGetString(MR.strings.feature_enabled_for), + featureRoles, + enableForRole, + onSelected = { value -> + onSelected(enableFeature.value, value) + } + ) + } } else { InfoRow( feature.text, @@ -169,9 +213,17 @@ private fun FeatureSection( if (timedOn) { InfoRow(generalGetString(MR.strings.delete_after), timeText(preferences.timedMessages.ttl)) } + if (enableFeature.value == GroupFeatureEnabled.ON && enableForRole != null) { + InfoRow(generalGetString(MR.strings.feature_enabled_for), featureRoles.firstOrNull { it.first == enableForRole.value }?.second ?: generalGetString(MR.strings.feature_roles_all_members), textColor = MaterialTheme.colors.secondary) + } } } - SectionTextFooter(feature.enableDescription(enableFeature.value, groupInfo.canEdit)) + KeyChangeEffect(enableFeature.value) { + if (enableFeature.value == GroupFeatureEnabled.OFF) { + onSelected(enableFeature.value, null) + } + } + SectionTextFooter(feature.enableDescription(enableFeature.value, groupInfo.isOwner)) } @Composable 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 f92fd88dc0..fb24c028b2 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 @@ -25,8 +25,7 @@ import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import java.net.URI @Composable @@ -35,10 +34,12 @@ fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl close = close, groupProfile = groupInfo.groupProfile, saveProfile = { p -> - withApi { + withBGApi { val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p) if (gInfo != null) { - chatModel.updateGroup(rhId, gInfo) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, gInfo) + } close.invoke() } } @@ -79,10 +80,9 @@ fun GroupProfileLayout( }, close) } } - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { ModalBottomSheetLayout( scrimColor = Color.Black.copy(alpha = 0.12F), - modifier = Modifier.navigationBarsWithImePadding(), + modifier = Modifier.imePadding(), sheetContent = { GetImageBottomSheet( chosenImage, @@ -95,10 +95,7 @@ fun GroupProfileLayout( sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) ) { ModalView(close = closeWithAlert) { - Column( - Modifier - .verticalScroll(scrollState) - ) { + ColumnWithScrollBar { Column( Modifier.fillMaxWidth() .padding(horizontal = DEFAULT_PADDING) @@ -175,7 +172,6 @@ fun GroupProfileLayout( } } } - } } private fun canUpdateProfile(displayName: String, groupProfile: GroupProfile): Boolean = diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt new file mode 100644 index 0000000000..1eeeb99c93 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt @@ -0,0 +1,90 @@ +package chat.simplex.common.views.chat.group + +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.chatlist.* +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.flow.* + +@Composable +private fun GroupReportsView(reportsChatsCtx: ChatModel.ChatsContext, staleChatId: State<String?>, scrollToItemId: MutableState<Long?>) { + ChatView(reportsChatsCtx, staleChatId, scrollToItemId, onComposed = {}) +} + +@Composable +fun GroupReportsAppBar( + chatsCtx: ChatModel.ChatsContext, + close: () -> Unit, + onSearchValueChanged: (String) -> Unit +) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val showSearch = rememberSaveable { mutableStateOf(false) } + val onBackClicked = { + if (!showSearch.value) { + close() + } else { + onSearchValueChanged("") + showSearch.value = false + } + } + BackHandler(onBack = onBackClicked) + DefaultAppBar( + navigationButton = { NavigationButtonBack(onBackClicked) }, + fixedTitleText = stringResource(MR.strings.group_reports_member_reports), + onTitleClick = null, + onTop = !oneHandUI.value, + showSearch = showSearch.value, + onSearchValueChanged = onSearchValueChanged, + buttons = { + IconButton({ showSearch.value = true }) { + Icon(painterResource(MR.images.ic_search), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary) + } + } + ) + ItemsReload(chatsCtx) +} + +@Composable +private fun ItemsReload(chatsCtx: ChatModel.ChatsContext,) { + LaunchedEffect(Unit) { + snapshotFlow { chatModel.chatId.value } + .distinctUntilChanged() + .drop(1) + .filterNotNull() + .map { chatModel.getChat(it) } + .filterNotNull() + .filter { it.chatInfo is ChatInfo.Group } + .collect { chat -> + reloadItems(chatsCtx, chat) + } + } +} + +suspend fun showGroupReportsView(staleChatId: State<String?>, scrollToItemId: MutableState<Long?>, chatInfo: ChatInfo) { + val reportsChatsCtx = ChatModel.ChatsContext(contentTag = MsgContentTag.Report) + openChat(secondaryChatsCtx = reportsChatsCtx, chatModel.remoteHostId(), chatInfo) + ModalManager.end.showCustomModal(true, id = ModalViewId.SECONDARY_CHAT) { close -> + ModalView({}, showAppBar = false) { + val chatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chatModel.chatId.value }?.chatInfo } }.value + if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canModerate) { + GroupReportsView(reportsChatsCtx, staleChatId, scrollToItemId) + } else { + LaunchedEffect(Unit) { + close() + } + } + } + } +} + +private suspend fun reloadItems(chatsCtx: ChatModel.ChatsContext, chat: Chat) { + apiLoadMessages(chatsCtx, chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId, ChatPagination.Initial(ChatPagination.INITIAL_COUNT)) +} 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 577c19648d..1e99c7f527 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 @@ -3,18 +3,19 @@ package chat.simplex.common.views.chat.group import SectionBottomSpacer import SectionDividerSpaced import SectionItemView +import SectionTextFooter import SectionView import TextIconSpaced import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.AnnotatedString import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -26,8 +27,11 @@ import chat.simplex.common.views.chat.item.MarkdownText import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel import chat.simplex.common.model.GroupInfo +import chat.simplex.common.platform.* import chat.simplex.res.MR -import kotlinx.coroutines.delay +import kotlinx.coroutines.* + +private const val maxByteCount = 1200 @Composable fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () -> Unit) { @@ -35,7 +39,7 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () val welcomeText = remember { mutableStateOf(gInfo.groupProfile.description ?: "") } fun save(afterSave: () -> Unit = {}) { - withApi { + withBGApi { var welcome: String? = welcomeText.value.trim('\n', ' ') if (welcome?.length == 0) { welcome = null @@ -44,7 +48,9 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated) if (res != null) { gInfo = res - m.updateGroup(rhId, res) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, res) + } welcomeText.value = welcome ?: "" } afterSave() @@ -53,8 +59,11 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () ModalView( close = { - if (welcomeText.value == gInfo.groupProfile.description || (welcomeText.value == "" && gInfo.groupProfile.description == null)) close() - else showUnsavedChangesAlert({ save(close) }, close) + when { + welcomeTextUnchanged(welcomeText, gInfo) -> close() + !welcomeTextFitsLimit(welcomeText) -> showUnsavedChangesTooLongAlert(close) + else -> showUnsavedChangesAlert({ save(close) }, close) + } }, ) { GroupWelcomeLayout( @@ -66,6 +75,14 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () } } +private fun welcomeTextUnchanged(welcomeText: MutableState<String>, groupInfo: GroupInfo): Boolean { + return welcomeText.value == groupInfo.groupProfile.description || (welcomeText.value == "" && groupInfo.groupProfile.description == null) +} + +private fun welcomeTextFitsLimit(welcomeText: MutableState<String>): Boolean { + return chatJsonLength(welcomeText.value) <= maxByteCount +} + @Composable private fun GroupWelcomeLayout( welcomeText: MutableState<String>, @@ -73,13 +90,11 @@ private fun GroupWelcomeLayout( linkMode: SimplexLinkMode, save: () -> Unit, ) { - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - ) { + ColumnWithScrollBar { val editMode = remember { mutableStateOf(true) } AppBarTitle(stringResource(MR.strings.group_welcome_title)) val wt = rememberSaveable { welcomeText } - if (groupInfo.canEdit) { + if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) { if (editMode.value) { val focusRequester = remember { FocusRequester() } TextEditor( @@ -94,6 +109,13 @@ private fun GroupWelcomeLayout( } else { TextPreview(wt.value, linkMode) } + SectionTextFooter( + if (!welcomeTextFitsLimit(wt)) { generalGetString(MR.strings.message_too_large) } else "", + color = if (welcomeTextFitsLimit(wt)) MaterialTheme.colors.secondary else Color.Red + ) + + Spacer(Modifier.size(8.dp)) + ChangeModeButton( editMode.value, click = { @@ -103,10 +125,12 @@ private fun GroupWelcomeLayout( ) val clipboard = LocalClipboardManager.current CopyTextButton { clipboard.setText(AnnotatedString(wt.value)) } + SectionDividerSpaced(maxBottomPadding = false) + SaveButton( save = save, - disabled = wt.value == groupInfo.groupProfile.description || (wt.value == "" && groupInfo.groupProfile.description == null) + disabled = welcomeTextUnchanged(wt, groupInfo) || !welcomeTextFitsLimit(wt) ) } else { val clipboard = LocalClipboardManager.current @@ -119,13 +143,15 @@ private fun GroupWelcomeLayout( @Composable private fun TextPreview(text: String, linkMode: SimplexLinkMode, markdown: Boolean = true) { + val uriHandler = LocalUriHandler.current Column { SelectionContainer(Modifier.fillMaxWidth()) { MarkdownText( text, formattedText = if (markdown) remember(text) { parseToMarkdown(text) } else null, + toggleSecrets = false, modifier = Modifier.fillMaxHeight().padding(horizontal = DEFAULT_PADDING), - linkMode = linkMode, + linkMode = linkMode, uriHandler = uriHandler, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground, lineHeight = 22.sp) ) } @@ -179,3 +205,11 @@ private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { onDismiss = revert, ) } + +private fun showUnsavedChangesTooLongAlert(revert: () -> Unit) { + AlertManager.shared.showAlertDialogStacked( + title = generalGetString(MR.strings.welcome_message_is_too_long), + confirmText = generalGetString(MR.strings.exit_without_saving), + onConfirm = revert, + ) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt index 6f165515ec..744bcf7b66 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt @@ -9,14 +9,21 @@ import androidx.compose.ui.graphics.Color import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import chat.simplex.common.ui.theme.* import chat.simplex.common.model.* import chat.simplex.common.views.helpers.SimpleButton import chat.simplex.res.MR @Composable -fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, duration: Int, acceptCall: (Contact) -> Unit) { +fun CICallItemView( + cInfo: ChatInfo, + cItem: ChatItem, + status: CICallStatus, + duration: Int, + showTimestamp: Boolean, + acceptCall: (Contact) -> Unit, + timedMessagesTTL: Int? +) { val sent = cItem.chatDir.sent Column( Modifier @@ -41,12 +48,7 @@ fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, durat CICallStatus.Error -> {} } - Text( - cItem.timestampText, - color = MaterialTheme.colors.secondary, - fontSize = 14.sp, - modifier = Modifier.padding(start = 3.dp) - ) + CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false, showTimestamp = showTimestamp) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatFeatureView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatFeatureView.kt index a9a4963c96..c743d78d1c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatFeatureView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatFeatureView.kt @@ -16,14 +16,16 @@ import chat.simplex.common.platform.onRightClick @Composable fun CIChatFeatureView( + chatsCtx: ChatModel.ChatsContext, + chatInfo: ChatInfo, chatItem: ChatItem, feature: Feature, iconColor: Color, icon: Painter? = null, - revealed: MutableState<Boolean>, + revealed: State<Boolean>, showMenu: MutableState<Boolean>, ) { - val merged = if (!revealed.value) mergedFeatures(chatItem) else emptyList() + val merged = if (!revealed.value) mergedFeatures(chatsCtx, chatItem, chatInfo) else emptyList() Box( Modifier .combinedClickable( @@ -70,15 +72,14 @@ private fun Feature.toFeatureInfo(color: Color, param: Int?, type: String): Feat ) @Composable -private fun mergedFeatures(chatItem: ChatItem): List<FeatureInfo>? { - val m = ChatModel +private fun mergedFeatures(chatsCtx: ChatModel.ChatsContext, chatItem: ChatItem, chatInfo: ChatInfo): List<FeatureInfo>? { val fs: ArrayList<FeatureInfo> = arrayListOf() val icons: MutableSet<PainterBox> = mutableSetOf() - var i = getChatItemIndexOrNull(chatItem) + val reversedChatItems = chatsCtx.chatItems.value.asReversed() + var i = getChatItemIndexOrNull(chatItem, reversedChatItems) if (i != null) { - val reversedChatItems = m.chatItems.asReversed() while (i < reversedChatItems.size) { - val f = featureInfo(reversedChatItems[i]) ?: break + val f = featureInfo(reversedChatItems[i], chatInfo) ?: break if (!icons.contains(f.icon)) { fs.add(0, f) icons.add(f.icon) @@ -90,12 +91,12 @@ private fun mergedFeatures(chatItem: ChatItem): List<FeatureInfo>? { } @Composable -private fun featureInfo(ci: ChatItem): FeatureInfo? = +private fun featureInfo(ci: ChatItem, chatInfo: ChatInfo): FeatureInfo? = when (ci.content) { is CIContent.RcvChatFeature -> ci.content.feature.toFeatureInfo(ci.content.enabled.iconColor, ci.content.param, ci.content.feature.name) is CIContent.SndChatFeature -> ci.content.feature.toFeatureInfo(ci.content.enabled.iconColor, ci.content.param, ci.content.feature.name) - is CIContent.RcvGroupFeature -> ci.content.groupFeature.toFeatureInfo(ci.content.preference.enable.iconColor, ci.content.param, ci.content.groupFeature.name) - is CIContent.SndGroupFeature -> ci.content.groupFeature.toFeatureInfo(ci.content.preference.enable.iconColor, ci.content.param, ci.content.groupFeature.name) + is CIContent.RcvGroupFeature -> ci.content.groupFeature.toFeatureInfo(ci.content.preference.enabled(ci.content.memberRole_, (chatInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, ci.content.param, ci.content.groupFeature.name) + is CIContent.SndGroupFeature -> ci.content.groupFeature.toFeatureInfo(ci.content.preference.enabled(ci.content.memberRole_, (chatInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, ci.content.param, ci.content.groupFeature.name) else -> null } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIEventView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIEventView.kt index 1e20a372ec..529a0e458b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIEventView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIEventView.kt @@ -12,7 +12,7 @@ import chat.simplex.common.ui.theme.* @Composable fun CIEventView(text: AnnotatedString) { - Text(text, Modifier.padding(horizontal = 6.dp, vertical = 6.dp), style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)) + Text(text, Modifier.padding(horizontal = 6.dp, vertical = 6.dp), style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), maxLines = 4) } @Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, 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 0d439f1235..542623028a 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 @@ -1,21 +1,24 @@ package chat.simplex.common.views.chat.item -import androidx.compose.foundation.clickable +import SectionItemView +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CornerSize 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.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* @@ -28,14 +31,18 @@ import java.net.URI fun CIFileView( file: CIFile?, edited: Boolean, - receiveFile: (Long, Boolean) -> Unit + showMenu: MutableState<Boolean>, + smallView: Boolean = false, + receiveFile: (Long) -> Unit ) { val saveFileLauncher = rememberSaveFileLauncher(ciFile = file) - + val sizeMultiplier = 1f + val progressSizeMultiplier = if (smallView) 0.7f else 1f @Composable fun fileIcon( innerIcon: Painter? = null, - color: Color = if (isInDarkTheme()) FileDark else FileLight + color: Color = if (isInDarkTheme()) FileDark else FileLight, + topPadding: Dp = 12.sp.toDp() ) { Box( contentAlignment = Alignment.Center @@ -51,28 +58,21 @@ fun CIFileView( innerIcon, stringResource(MR.strings.icon_descr_file), Modifier - .size(32.dp) - .padding(top = 12.dp), + .padding(top = topPadding * sizeMultiplier) + .height(20.sp.toDp() * sizeMultiplier) + .width(32.sp.toDp() * sizeMultiplier), tint = Color.White ) } } } - fun fileSizeValid(): Boolean { - if (file != null) { - return file.fileSize <= getMaxFileSize(file.fileProtocol) - } - return false - } - fun fileAction() { if (file != null) { - when (file.fileStatus) { - is CIFileStatus.RcvInvitation -> { - if (fileSizeValid()) { - val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get() - receiveFile(file.fileId, encrypted) + when { + file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted -> { + if (fileSizeValid(file)) { + receiveFile(file.fileId) } else { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.large_file), @@ -80,7 +80,7 @@ fun CIFileView( ) } } - is CIFileStatus.RcvAccepted -> + file.fileStatus is CIFileStatus.RcvAccepted -> when (file.fileProtocol) { FileProtocol.XFTP -> AlertManager.shared.showAlertMsg( @@ -92,16 +92,25 @@ fun CIFileView( generalGetString(MR.strings.waiting_for_file), generalGetString(MR.strings.file_will_be_received_when_contact_is_online) ) + FileProtocol.LOCAL -> {} } - is CIFileStatus.RcvComplete -> { - withBGApi { + file.fileStatus is CIFileStatus.RcvError -> + showFileErrorAlert(file.fileStatus.rcvFileError) + file.fileStatus is CIFileStatus.RcvWarning -> + showFileErrorAlert(file.fileStatus.rcvFileError, temporary = true) + file.fileStatus is CIFileStatus.SndError -> + showFileErrorAlert(file.fileStatus.sndFileError) + file.fileStatus is CIFileStatus.SndWarning -> + showFileErrorAlert(file.fileStatus.sndFileError, temporary = true) + file.forwardingAllowed() -> { + withLongRunningApi(slow = 600_000) { var filePath = getLoadedFilePath(file) if (chatModel.connectedToRemote() && filePath == null) { file.loadRemoteFile(true) filePath = getLoadedFilePath(file) } if (filePath != null) { - withApi { + withLongRunningApi { saveFileLauncher.launch(file.fileName) } } else { @@ -114,68 +123,50 @@ fun CIFileView( } } - @Composable - fun progressIndicator() { - CircularProgressIndicator( - Modifier.size(32.dp), - color = if (isInDarkTheme()) FileDark else FileLight, - strokeWidth = 3.dp - ) - } - - @Composable - fun progressCircle(progress: Long, total: Long) { - val angle = 360f * (progress.toDouble() / total.toDouble()).toFloat() - val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() } - val strokeColor = if (isInDarkTheme()) FileDark else FileLight - Surface( - Modifier.drawRingModifier(angle, strokeColor, strokeWidth), - color = Color.Transparent, - shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)) - ) { - Box(Modifier.size(32.dp)) - } - } - @Composable fun fileIndicator() { Box( Modifier - .size(42.dp) - .clip(RoundedCornerShape(4.dp)) - .clickable(onClick = { fileAction() }), + .size(42.sp.toDp() * sizeMultiplier) + .clip(RoundedCornerShape(4.sp.toDp() * sizeMultiplier)), contentAlignment = Alignment.Center ) { if (file != null) { when (file.fileStatus) { is CIFileStatus.SndStored -> when (file.fileProtocol) { - FileProtocol.XFTP -> progressIndicator() + FileProtocol.XFTP -> CIFileViewScope.progressIndicator(progressSizeMultiplier) FileProtocol.SMP -> fileIcon() + FileProtocol.LOCAL -> fileIcon() } is CIFileStatus.SndTransfer -> when (file.fileProtocol) { - FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal) - FileProtocol.SMP -> progressIndicator() + FileProtocol.XFTP -> CIFileViewScope.progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal, progressSizeMultiplier) + FileProtocol.SMP -> CIFileViewScope.progressIndicator(progressSizeMultiplier) + FileProtocol.LOCAL -> {} } - is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(MR.images.ic_check_filled)) + is CIFileStatus.SndComplete -> fileIcon(innerIcon = if (!smallView) painterResource(MR.images.ic_check_filled) else null) is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) + is CIFileStatus.SndWarning -> fileIcon(innerIcon = painterResource(MR.images.ic_warning_filled)) is CIFileStatus.RcvInvitation -> - if (fileSizeValid()) - fileIcon(innerIcon = painterResource(MR.images.ic_arrow_downward), color = MaterialTheme.colors.primary) + if (fileSizeValid(file)) + fileIcon(innerIcon = painterResource(MR.images.ic_arrow_downward), color = MaterialTheme.colors.primary, topPadding = 10.sp.toDp()) else fileIcon(innerIcon = painterResource(MR.images.ic_priority_high), color = WarningOrange) is CIFileStatus.RcvAccepted -> fileIcon(innerIcon = painterResource(MR.images.ic_more_horiz)) is CIFileStatus.RcvTransfer -> if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) { - progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal) + CIFileViewScope.progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal, progressSizeMultiplier) } else { - progressIndicator() + CIFileViewScope.progressIndicator(progressSizeMultiplier) } + is CIFileStatus.RcvAborted -> + fileIcon(innerIcon = painterResource(MR.images.ic_sync_problem), color = MaterialTheme.colors.primary) is CIFileStatus.RcvComplete -> fileIcon() is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) + is CIFileStatus.RcvWarning -> fileIcon(innerIcon = painterResource(MR.images.ic_warning_filled)) is CIFileStatus.Invalid -> fileIcon(innerIcon = painterResource(MR.images.ic_question_mark)) } } else { @@ -184,35 +175,98 @@ fun CIFileView( } } + val showOpenSaveMenu = rememberSaveable(file?.fileId) { mutableStateOf(false) } + val ext = file?.fileSource?.filePath?.substringAfterLast(".")?.takeIf { it.isNotBlank() } + val loadedFilePath = if (appPlatform.isAndroid && file?.fileSource != null) getLoadedFilePath(file) else null + if (loadedFilePath != null && file?.fileSource != null) { + val encrypted = file.fileSource.cryptoArgs != null + SaveOrOpenFileMenu(showOpenSaveMenu, encrypted, ext, File(loadedFilePath).toURI(), file.fileSource, saveFile = { fileAction() }) + } Row( - Modifier.padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp), + Modifier + .combinedClickable( + onClick = { + if (appPlatform.isAndroid && loadedFilePath != null) { + showOpenSaveMenu.value = true + } else { + fileAction() + } + }, + onLongClick = { showMenu.value = true } + ) + .padding(if (smallView) PaddingValues() else PaddingValues(top = 4.sp.toDp(), bottom = 6.sp.toDp(), start = 6.sp.toDp(), end = 12.sp.toDp())), verticalAlignment = Alignment.Bottom, - horizontalArrangement = Arrangement.spacedBy(2.dp) + horizontalArrangement = Arrangement.spacedBy(2.sp.toDp()) ) { fileIndicator() - val metaReserve = if (edited) - " " - else - " " - if (file != null) { - Column { - Text( - file.fileName, - maxLines = 1 - ) - Text( - formatBytes(file.fileSize) + metaReserve, - color = MaterialTheme.colors.secondary, - fontSize = 14.sp, - maxLines = 1 - ) + if (!smallView) { + val metaReserve = if (edited) + " " + else + " " + if (file != null) { + Column { + Text( + file.fileName, + maxLines = 1 + ) + Text( + formatBytes(file.fileSize) + metaReserve, + color = MaterialTheme.colors.secondary, + fontSize = 14.sp, + maxLines = 1 + ) + } + } else { + Text(metaReserve) } - } else { - Text(metaReserve) } } } +fun fileSizeValid(file: CIFile): Boolean = file.fileSize <= getMaxFileSize(file.fileProtocol) + +fun showFileErrorAlert(err: FileError, temporary: Boolean = false) { + val title: String = generalGetString(if (temporary) MR.strings.temporary_file_error else MR.strings.file_error) + val btn = err.moreInfoButton + if (btn != null) { + showContentBlockedAlert(title, err.errorInfo) + } else { + AlertManager.shared.showAlertMsg(title, err.errorInfo) + } +} + +val contentModerationPostLink = "https://simplex.chat/blog/20250114-simplex-network-large-groups-privacy-preserving-content-moderation.html#preventing-server-abuse-without-compromising-e2e-encryption" + +fun showContentBlockedAlert(title: String, message: String) { + AlertManager.shared.showAlertDialogButtonsColumn(title, text = message, buttons = { + val uriHandler = LocalUriHandler.current + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + uriHandler.openUriCatching(contentModerationPostLink) + }) { + Text(generalGetString(MR.strings.how_it_works), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(generalGetString(MR.strings.ok), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + }) +} + +@Composable +expect fun SaveOrOpenFileMenu( + showMenu: MutableState<Boolean>, + encrypted: Boolean, + ext: String?, + encryptedUri: URI, + fileSource: CryptoFile, + saveFile: () -> Unit +) + @Composable fun rememberSaveFileLauncher(ciFile: CIFile?): FileChooserLauncher = rememberFileChooserLauncher(false, ciFile) { to: URI? -> @@ -224,6 +278,7 @@ fun rememberSaveFileLauncher(ciFile: CIFile?): FileChooserLauncher = decryptCryptoFile(filePath, ciFile.fileSource.cryptoArgs, tmpFile.absolutePath) } catch (e: Exception) { Log.e(TAG, "Unable to decrypt crypto file: " + e.stackTraceToString()) + AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString()) tmpFile.delete() return@createTmpFileAndDelete } @@ -236,6 +291,32 @@ fun rememberSaveFileLauncher(ciFile: CIFile?): FileChooserLauncher = } } +object CIFileViewScope { + @Composable + fun progressIndicator(sizeMultiplier: Float = 1f) { + CircularProgressIndicator( + Modifier.size(32.sp.toDp() * sizeMultiplier), + color = if (isInDarkTheme()) FileDark else FileLight, + strokeWidth = 3.sp.toDp() * sizeMultiplier + ) + } + + @Composable + fun progressCircle(progress: Long, total: Long, sizeMultiplier: Float = 1f) { + val angle = 360f * (progress.toDouble() / total.toDouble()).toFloat() + val strokeWidth = with(LocalDensity.current) { 3.sp.toPx() } + val strokeColor = if (isInDarkTheme()) FileDark else FileLight + Surface( + Modifier.drawRingModifier(angle, strokeColor, strokeWidth), + color = Color.Transparent, + shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)), + contentColor = LocalContentColor.current + ) { + Box(Modifier.size(32.sp.toDp() * sizeMultiplier)) + } + } +} + /* class ChatItemProvider: PreviewParameterProvider<ChatItem> { private val sentFile = ChatItem( 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 56dd7a360a..2bcbbe29e0 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 @@ -9,11 +9,12 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.buildAnnotatedString import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.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.* @@ -25,8 +26,10 @@ fun CIGroupInvitationView( ci: ChatItem, groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole, + showTimestamp: Boolean, chatIncognito: Boolean = false, - joinGroup: (Long, () -> Unit) -> Unit + joinGroup: (Long, () -> Unit) -> Unit, + timedMessagesTTL: Int? ) { val sent = ci.chatDir.sent val action = !sent && groupInvitation.status == CIGroupInvitationStatus.Pending @@ -69,18 +72,20 @@ fun CIGroupInvitationView( } @Composable - fun groupInvitationText() { - when { - sent -> Text(stringResource(MR.strings.you_sent_group_invitation)) - !sent && groupInvitation.status == CIGroupInvitationStatus.Pending -> Text(stringResource(MR.strings.you_are_invited_to_group)) - !sent && groupInvitation.status == CIGroupInvitationStatus.Accepted -> Text(stringResource(MR.strings.you_joined_this_group)) - !sent && groupInvitation.status == CIGroupInvitationStatus.Rejected -> Text(stringResource(MR.strings.you_rejected_group_invitation)) - !sent && groupInvitation.status == CIGroupInvitationStatus.Expired -> Text(stringResource(MR.strings.group_invitation_expired)) + fun groupInvitationStr(): String { + return when { + sent -> stringResource(MR.strings.you_sent_group_invitation) + else -> when(groupInvitation.status) { + CIGroupInvitationStatus.Pending -> stringResource(MR.strings.you_are_invited_to_group) + CIGroupInvitationStatus.Accepted -> stringResource(MR.strings.you_joined_this_group) + CIGroupInvitationStatus.Rejected -> stringResource(MR.strings.you_rejected_group_invitation) + CIGroupInvitationStatus.Expired -> stringResource(MR.strings.group_invitation_expired) + } } } - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( modifier = if (action && !inProgress.value) Modifier.clickable(onClick = { inProgress.value = true @@ -88,6 +93,7 @@ fun CIGroupInvitationView( }) else Modifier, shape = RoundedCornerShape(18.dp), color = if (sent) sentColor else receivedColor, + contentColor = LocalContentColor.current ) { Box( Modifier @@ -105,23 +111,28 @@ fun CIGroupInvitationView( .padding(bottom = 4.dp), ) { groupInfoView() + val secondaryColor = MaterialTheme.colors.secondary Column(Modifier.padding(top = 2.dp, start = 5.dp)) { Divider(Modifier.fillMaxWidth().padding(bottom = 4.dp)) if (action) { - groupInvitationText() + Text(groupInvitationStr()) Text( - stringResource( - if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join - ), + buildAnnotatedString { + append(generalGetString(if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join)) + withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor, showTimestamp = showTimestamp)) } + }, color = if (inProgress.value) MaterialTheme.colors.secondary else if (chatIncognito) Indigo else MaterialTheme.colors.primary ) } else { - Box(Modifier.padding(end = 48.dp)) { - groupInvitationText() - } + Text( + buildAnnotatedString { + append(groupInvitationStr()) + withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor, showTimestamp = showTimestamp)) } + } + ) } } } @@ -135,12 +146,7 @@ fun CIGroupInvitationView( } } - Text( - ci.timestampText, - color = MaterialTheme.colors.secondary, - fontSize = 14.sp, - modifier = Modifier.padding(start = 3.dp) - ) + CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false, showTimestamp = showTimestamp) } } } @@ -156,7 +162,9 @@ fun PendingCIGroupInvitationViewPreview() { ci = ChatItem.getGroupInvitationSample(), groupInvitation = CIGroupInvitation.getSample(), memberRole = GroupMemberRole.Admin, - joinGroup = { _, _ -> } + joinGroup = { _, _ -> }, + timedMessagesTTL = null, + showTimestamp = true, ) } } @@ -172,8 +180,10 @@ fun CIGroupInvitationViewAcceptedPreview() { ci = ChatItem.getGroupInvitationSample(), groupInvitation = CIGroupInvitation.getSample(status = CIGroupInvitationStatus.Accepted), memberRole = GroupMemberRole.Admin, - joinGroup = { _, _ -> } - ) + joinGroup = { _, _ -> }, + timedMessagesTTL = null, + showTimestamp = true, + ) } } @@ -188,7 +198,9 @@ fun CIGroupInvitationViewLongNamePreview() { status = CIGroupInvitationStatus.Accepted ), memberRole = GroupMemberRole.Admin, - joinGroup = { _, _ -> } - ) + joinGroup = { _, _ -> }, + timedMessagesTTL = null, + showTimestamp = true, + ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt index 1e5919c0b6..1be2110b1f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt @@ -1,12 +1,14 @@ package chat.simplex.common.views.chat.item import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.HoverInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.Icon +import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale @@ -18,26 +20,24 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH +import chat.simplex.common.views.chat.chatViewScrollState import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.runBlocking -import java.io.File -import java.net.URI @Composable fun CIImageView( image: String, file: CIFile?, - encryptLocalFile: Boolean, - metaColor: Color, imageProvider: () -> ImageGalleryProvider, showMenu: MutableState<Boolean>, - receiveFile: (Long, Boolean) -> Unit + smallView: Boolean, + receiveFile: (Long) -> Unit ) { + val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) } @Composable fun progressIndicator() { CircularProgressIndicator( @@ -53,7 +53,7 @@ fun CIImageView( icon, stringResource(stringId), Modifier.fillMaxSize(), - tint = metaColor + tint = Color.White ) } @@ -62,7 +62,7 @@ fun CIImageView( if (file != null) { Box( Modifier - .padding(8.dp) + .padding(if (smallView) 0.dp else 8.dp) .size(20.dp), contentAlignment = Alignment.Center ) { @@ -71,29 +71,27 @@ fun CIImageView( when (file.fileProtocol) { FileProtocol.XFTP -> progressIndicator() FileProtocol.SMP -> {} + FileProtocol.LOCAL -> {} } is CIFileStatus.SndTransfer -> progressIndicator() is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_image_snd_complete) is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) + is CIFileStatus.SndWarning -> fileIcon(painterResource(MR.images.ic_warning_filled), MR.strings.icon_descr_file) is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive) is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_image) is CIFileStatus.RcvTransfer -> progressIndicator() + is CIFileStatus.RcvComplete -> {} + is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file) is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) + is CIFileStatus.RcvWarning -> fileIcon(painterResource(MR.images.ic_warning_filled), MR.strings.icon_descr_file) is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file) - else -> {} } } } } - @Composable - fun imageViewFullWidth(): Dp { - val approximatePadding = 100.dp - return with(LocalDensity.current) { minOf(DEFAULT_MAX_IMAGE_WIDTH, LocalWindowWidth() - approximatePadding) } - } - @Composable fun imageView(imageBitmap: ImageBitmap, onClick: () -> Unit) { Image( @@ -107,27 +105,58 @@ fun CIImageView( onLongClick = { showMenu.value = true }, onClick = onClick ) - .onRightClick { showMenu.value = true }, - contentScale = ContentScale.FillWidth, + .onRightClick { showMenu.value = true } + .privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }), + contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth, ) } @Composable - fun ImageView(painter: Painter, onClick: () -> Unit) { - Image( - painter, - contentDescription = stringResource(MR.strings.image_descr), - // .width(DEFAULT_MAX_IMAGE_WIDTH) is a hack for image to increase IntrinsicSize of FramedItemView - // if text is short and take all available width if text is long - modifier = Modifier - .width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH) + fun ImageView(painter: Painter, image: String, fileSource: CryptoFile?, onClick: () -> Unit) { + // On my Android device Compose fails to display 6000x6000 px WebP image with exception: + // IllegalStateException: Recording currently in progress - missing #endRecording() call? + // but can display 5000px image. Using even lower value here just to feel safer. + // It happens to WebP because it's not compressed while sending since it can be animated. + if (painter.intrinsicSize != Size.Unspecified && painter.intrinsicSize.width <= 4320 && painter.intrinsicSize.height <= 4320) { + Image( + painter, + contentDescription = stringResource(MR.strings.image_descr), + // .width(DEFAULT_MAX_IMAGE_WIDTH) is a hack for image to increase IntrinsicSize of FramedItemView + // if text is short and take all available width if text is long + modifier = Modifier + .width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH) + .combinedClickable( + onLongClick = { showMenu.value = true }, + onClick = onClick + ) + .onRightClick { showMenu.value = true } + .privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }), + contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth, + ) + } else { + Box(Modifier + .width(if (painter.intrinsicSize != Size.Unspecified && painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH) .combinedClickable( onLongClick = { showMenu.value = true }, - onClick = onClick + onClick = {} ) - .onRightClick { showMenu.value = true }, - contentScale = ContentScale.FillWidth, - ) + .onRightClick { showMenu.value = true } + .privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }), + contentAlignment = Alignment.Center + ) { + imageView(base64ToBitmap(image), onClick = { + if (fileSource != null) { + openFile(fileSource) + } + }) + Icon( + painterResource(MR.images.ic_open_in_new), + contentDescription = stringResource(MR.strings.image_descr), + modifier = Modifier.size(30.dp), + tint = MaterialTheme.colors.primary, + ) + } + } } fun fileSizeValid(): Boolean { @@ -148,7 +177,8 @@ fun CIImageView( } Box( - Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID), + Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID) + .desktopModifyBlurredState(!smallView, blurred, showMenu), contentAlignment = Alignment.TopEnd ) { val res: MutableState<Triple<ImageBitmap, ByteArray, String>?> = remember { @@ -166,29 +196,29 @@ fun CIImageView( } } else { KeyChangeEffect(file) { - if (res.value == null) { + if (res.value == null || res.value!!.third != getLoadedFilePath(file)) { res.value = imageAndFilePath(file) } } } val loaded = res.value - if (loaded != null) { + if (loaded != null && file != null) { val (imageBitmap, data, _) = loaded - SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, @Composable { painter, onClick -> ImageView(painter, onClick) }) + SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, smallView, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, onClick) }) } else { imageView(base64ToBitmap(image), onClick = { if (file != null) { - when (file.fileStatus) { - CIFileStatus.RcvInvitation -> + when { + file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted -> if (fileSizeValid()) { - receiveFile(file.fileId, encryptLocalFile) + receiveFile(file.fileId) } else { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.large_file), String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol))) ) } - CIFileStatus.RcvAccepted -> + file.fileStatus is CIFileStatus.RcvAccepted -> when (file.fileProtocol) { FileProtocol.XFTP -> AlertManager.shared.showAlertMsg( @@ -200,24 +230,50 @@ fun CIImageView( generalGetString(MR.strings.waiting_for_image), generalGetString(MR.strings.image_will_be_received_when_contact_is_online) ) + FileProtocol.LOCAL -> {} } - CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ? - CIFileStatus.RcvComplete -> {} // ? - CIFileStatus.RcvCancelled -> {} // TODO + file.fileStatus is CIFileStatus.RcvError -> + showFileErrorAlert(file.fileStatus.rcvFileError) + file.fileStatus is CIFileStatus.RcvWarning -> + showFileErrorAlert(file.fileStatus.rcvFileError, temporary = true) + file.fileStatus is CIFileStatus.SndError -> + showFileErrorAlert(file.fileStatus.sndFileError) + file.fileStatus is CIFileStatus.SndWarning -> + showFileErrorAlert(file.fileStatus.sndFileError, temporary = true) + file.fileStatus is CIFileStatus.RcvTransfer -> {} // ? + file.fileStatus is CIFileStatus.RcvComplete -> {} // ? + file.fileStatus is CIFileStatus.RcvCancelled -> {} // TODO else -> {} } } }) } - loadingIndicator() + // Do not show download icon when the view is blurred + if (!smallView && (!showDownloadButton(file?.fileStatus) || !blurred.value)) { + loadingIndicator() + } else if (smallView && file?.showStatusIconInSmallView == true) { + Box(Modifier.matchParentSize(), contentAlignment = Alignment.Center) { + loadingIndicator() + } + } } } +@Composable +fun imageViewFullWidth(): Dp { + val approximatePadding = 100.dp + return with(LocalDensity.current) { minOf(DEFAULT_MAX_IMAGE_WIDTH, LocalWindowWidth() - approximatePadding) } +} + +private fun showDownloadButton(status: CIFileStatus?): Boolean = + status is CIFileStatus.RcvInvitation || status is CIFileStatus.RcvAborted + @Composable expect fun SimpleAndAnimatedImageView( data: ByteArray, imageBitmap: ImageBitmap, file: CIFile?, imageProvider: () -> ImageGalleryProvider, + smallView: Boolean, ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIInvalidJSONView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIInvalidJSONView.kt index 21c27fa34e..d297d5ef76 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIInvalidJSONView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIInvalidJSONView.kt @@ -1,6 +1,5 @@ package chat.simplex.common.views.chat.item -import SectionView import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -8,23 +7,23 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager -import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp +import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.shareText import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.usersettings.SettingsActionItem import chat.simplex.res.MR @Composable fun CIInvalidJSONView(json: String) { + val clipboard = LocalClipboardManager.current Row(Modifier .clickable { ModalManager.center.closeModals() ModalManager.end.closeModals() - ModalManager.center.showModal(true) { InvalidJSONView(json) } + ModalManager.center.showModal(true, endButtons = { ShareButton { clipboard.shareText(json) } }) { InvalidJSONView(json) } } .padding(horizontal = 10.dp, vertical = 6.dp) ) { @@ -34,15 +33,8 @@ fun CIInvalidJSONView(json: String) { @Composable fun InvalidJSONView(json: String) { - Column { - Spacer(Modifier.height(DEFAULT_PADDING)) - SectionView { - val clipboard = LocalClipboardManager.current - SettingsActionItem(painterResource(MR.images.ic_share), generalGetString(MR.strings.share_verb), click = { - clipboard.shareText(json) - }) - } - Column(Modifier.padding(DEFAULT_PADDING).fillMaxWidth().verticalScroll(rememberScrollState())) { + ColumnWithScrollBar { + Column(Modifier.padding(DEFAULT_PADDING).fillMaxWidth()) { Text(json) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt index 72f7137b55..4ec2a885e7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.model.* import chat.simplex.common.ui.theme.isInDarkTheme import chat.simplex.res.MR @@ -33,7 +32,11 @@ fun CIMetaView( red = minOf(metaColor.red * 1.33F, 1F), green = minOf(metaColor.green * 1.33F, 1F), blue = minOf(metaColor.red * 1.33F, 1F)) - } + }, + showStatus: Boolean = true, + showEdited: Boolean = true, + showTimestamp: Boolean, + showViaProxy: Boolean, ) { Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) { if (chatItem.isDeletedContent) { @@ -44,17 +47,36 @@ fun CIMetaView( modifier = Modifier.padding(start = 3.dp) ) } else { - CIMetaText(chatItem.meta, timedMessagesTTL, encrypted = chatItem.encryptedFile, metaColor, paleMetaColor) + CIMetaText( + chatItem.meta, + timedMessagesTTL, + encrypted = chatItem.encryptedFile, + metaColor, + paleMetaColor, + showStatus = showStatus, + showEdited = showEdited, + showViaProxy = showViaProxy, + showTimestamp = showTimestamp + ) } } } @Composable // changing this function requires updating reserveSpaceForMeta -private fun CIMetaText(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?, color: Color, paleColor: Color) { - if (meta.itemEdited) { +private fun CIMetaText( + meta: CIMeta, + chatTTL: Int?, + encrypted: Boolean?, + color: Color, + paleColor: Color, + showStatus: Boolean = true, + showEdited: Boolean = true, + showTimestamp: Boolean, + showViaProxy: Boolean, +) { + if (showEdited && meta.itemEdited) { StatusIconText(painterResource(MR.images.ic_edit), color) - Spacer(Modifier.width(3.dp)) } if (meta.disappearing) { StatusIconText(painterResource(MR.images.ic_timer), color) @@ -62,47 +84,94 @@ private fun CIMetaText(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?, color: if (ttl != chatTTL) { Text(shortTimeText(ttl), color = color, fontSize = 12.sp) } - Spacer(Modifier.width(4.dp)) } - val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color, paleColor) - if (statusIcon != null) { - val (icon, statusColor) = statusIcon - if (meta.itemStatus is CIStatus.SndSent || meta.itemStatus is CIStatus.SndRcvd) { - Icon(painterResource(icon), null, Modifier.height(17.dp), tint = statusColor) - } else { - StatusIconText(painterResource(icon), statusColor) + if (showViaProxy && meta.sentViaProxy == true) { + Spacer(Modifier.width(4.dp)) + Icon(painterResource(MR.images.ic_arrow_forward), null, Modifier.height(17.dp), tint = MaterialTheme.colors.secondary) + } + if (showStatus) { + Spacer(Modifier.width(4.dp)) + val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color, paleColor) + if (statusIcon != null) { + val (icon, statusColor) = statusIcon + if (meta.itemStatus is CIStatus.SndSent || meta.itemStatus is CIStatus.SndRcvd) { + Icon(painterResource(icon), null, Modifier.height(17.dp), tint = statusColor) + } else { + StatusIconText(painterResource(icon), statusColor) + } + } else if (!meta.disappearing) { + StatusIconText(painterResource(MR.images.ic_circle_filled), Color.Transparent) } - Spacer(Modifier.width(4.dp)) - } else if (!meta.disappearing) { - StatusIconText(painterResource(MR.images.ic_circle_filled), Color.Transparent) - Spacer(Modifier.width(4.dp)) } if (encrypted != null) { - StatusIconText(painterResource(if (encrypted) MR.images.ic_lock else MR.images.ic_lock_open_right), color) Spacer(Modifier.width(4.dp)) + StatusIconText(painterResource(if (encrypted) MR.images.ic_lock else MR.images.ic_lock_open_right), color) + } + + if (showTimestamp) { + Spacer(Modifier.width(4.dp)) + Text(meta.timestampText, color = color, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis) } - Text(meta.timestampText, color = color, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis) } // the conditions in this function should match CIMetaText -fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?): String { - val iconSpace = " " - var res = "" - if (meta.itemEdited) res += iconSpace +fun reserveSpaceForMeta( + meta: CIMeta, + chatTTL: Int?, + encrypted: Boolean?, + secondaryColor: Color, + showStatus: Boolean = true, + showEdited: Boolean = true, + showViaProxy: Boolean = false, + showTimestamp: Boolean +): String { + val iconSpace = " \u00A0\u00A0\u00A0" + val whiteSpace = "\u00A0" + var res = if (showTimestamp) "" else iconSpace + var space: String? = null + + fun appendSpace() { + if (space != null) { + res += space + space = null + } + } + + if (showEdited && meta.itemEdited) { + res += iconSpace + } if (meta.itemTimed != null) { res += iconSpace val ttl = meta.itemTimed.ttl if (ttl != chatTTL) { res += shortTimeText(ttl) } + space = whiteSpace } - if (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing) { + if (showViaProxy && meta.sentViaProxy == true) { + appendSpace() res += iconSpace } + if (showStatus) { + appendSpace() + if (meta.statusIcon(secondaryColor) != null) { + res += iconSpace + } else if (!meta.disappearing) { + res += iconSpace + } + space = whiteSpace + } + if (encrypted != null) { + appendSpace() res += iconSpace + space = whiteSpace } - return res + meta.timestampText + if (showTimestamp) { + appendSpace() + res += meta.timestampText + } + return res } @Composable @@ -117,7 +186,9 @@ fun PreviewCIMetaView() { chatItem = ChatItem.getSampleData( 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" ), - null + null, + showViaProxy = false, + showTimestamp = true ) } @@ -129,7 +200,9 @@ fun PreviewCIMetaViewUnread() { 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.RcvNew() ), - null + null, + showViaProxy = false, + showTimestamp = true ) } @@ -139,9 +212,11 @@ fun PreviewCIMetaViewSendFailed() { CIMetaView( chatItem = ChatItem.getSampleData( 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", - status = CIStatus.SndError("CMD SYNTAX") + status = CIStatus.CISSndError(SndError.Other("CMD SYNTAX")) ), - null + null, + showViaProxy = false, + showTimestamp = true ) } @@ -152,7 +227,9 @@ fun PreviewCIMetaViewSendNoAuth() { chatItem = ChatItem.getSampleData( 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndErrorAuth() ), - null + null, + showViaProxy = false, + showTimestamp = true ) } @@ -163,7 +240,9 @@ fun PreviewCIMetaViewSendSent() { chatItem = ChatItem.getSampleData( 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent(SndCIStatusProgress.Complete) ), - null + null, + showViaProxy = false, + showTimestamp = true ) } @@ -175,7 +254,9 @@ fun PreviewCIMetaViewEdited() { 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", itemEdited = true ), - null + null, + showViaProxy = false, + showTimestamp = true ) } @@ -188,7 +269,9 @@ fun PreviewCIMetaViewEditedUnread() { itemEdited = true, status= CIStatus.RcvNew() ), - null + null, + showViaProxy = false, + showTimestamp = true ) } @@ -201,7 +284,9 @@ fun PreviewCIMetaViewEditedSent() { itemEdited = true, status= CIStatus.SndSent(SndCIStatusProgress.Complete) ), - null + null, + showViaProxy = false, + showTimestamp = true ) } @@ -210,6 +295,8 @@ fun PreviewCIMetaViewEditedSent() { fun PreviewCIMetaViewDeletedContent() { CIMetaView( chatItem = ChatItem.getDeletedContentSampleData(), - null + null, + showViaProxy = false, + showTimestamp = true ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt index 318735d73e..d58fd7553f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.ui.theme.CurrentColors +import chat.simplex.common.ui.theme.appColors import chat.simplex.common.views.helpers.AlertManager import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR @@ -137,11 +138,12 @@ fun DecryptionErrorItemFixButton( onClick: () -> Unit, syncSupported: Boolean ) { - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( Modifier.clickable(onClick = onClick), shape = RoundedCornerShape(18.dp), color = receivedColor, + contentColor = LocalContentColor.current ) { Box( Modifier.padding(vertical = 6.dp, horizontal = 12.dp), @@ -163,17 +165,18 @@ fun DecryptionErrorItemFixButton( tint = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary ) Spacer(Modifier.padding(2.dp)) + val secondaryColor = MaterialTheme.colors.secondary Text( buildAnnotatedString { append(generalGetString(MR.strings.fix_connection)) - withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null)) } + withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor, showTimestamp = true)) } withStyle(reserveTimestampStyle) { append(" ") } // for icon }, color = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary ) } } - CIMetaView(ci, timedMessagesTTL = null) + CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false, showTimestamp = true) } } } @@ -183,24 +186,26 @@ fun DecryptionErrorItem( ci: ChatItem, onClick: () -> Unit ) { - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( Modifier.clickable(onClick = onClick), shape = RoundedCornerShape(18.dp), color = receivedColor, + contentColor = LocalContentColor.current ) { Box( Modifier.padding(vertical = 6.dp, horizontal = 12.dp), contentAlignment = Alignment.BottomEnd, ) { + val secondaryColor = MaterialTheme.colors.secondary Text( buildAnnotatedString { withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) } - withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null)) } + withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor, showTimestamp = true)) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp) ) - CIMetaView(ci, timedMessagesTTL = null) + CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false, showTimestamp = true) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt deleted file mode 100644 index 04ec307358..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt +++ /dev/null @@ -1,334 +0,0 @@ -package chat.simplex.common.views.chat.item - -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CornerSize -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.* -import androidx.compose.ui.graphics.* -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.layout.* -import androidx.compose.ui.platform.* -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.unit.* -import chat.simplex.res.MR -import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.helpers.* -import chat.simplex.common.model.* -import chat.simplex.common.platform.* -import dev.icerock.moko.resources.StringResource -import kotlinx.coroutines.flow.* -import java.io.File -import java.net.URI - -@Composable -fun CIVideoView( - image: String, - duration: Int, - file: CIFile?, - imageProvider: () -> ImageGalleryProvider, - showMenu: MutableState<Boolean>, - receiveFile: (Long, Boolean) -> Unit -) { - Box( - Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID), - contentAlignment = Alignment.TopEnd - ) { - val preview = remember(image) { base64ToBitmap(image) } - val filePath = remember(file, CIFile.cachedRemoteFileRequests.toList()) { mutableStateOf(getLoadedFilePath(file)) } - if (chatModel.connectedToRemote()) { - LaunchedEffect(file) { - withBGApi { - if (file != null && file.loaded && getLoadedFilePath(file) == null) { - file.loadRemoteFile(false) - filePath.value = getLoadedFilePath(file) - } - } - } - } - val f = filePath.value - if (file != null && f != null) { - val uri = remember(filePath) { getAppFileUri(f.substringAfterLast(File.separator)) } - val view = LocalMultiplatformView() - VideoView(uri, file, preview, duration * 1000L, showMenu, onClick = { - hideKeyboard(view) - ModalManager.fullscreen.showCustomModal(animated = false) { close -> - ImageFullScreenView(imageProvider, close) - } - }) - } else { - Box { - VideoPreviewImageView(preview, onClick = { - if (file != null) { - when (file.fileStatus) { - CIFileStatus.RcvInvitation -> - receiveFileIfValidSize(file, encrypted = false, receiveFile) - CIFileStatus.RcvAccepted -> - when (file.fileProtocol) { - FileProtocol.XFTP -> - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.waiting_for_video), - generalGetString(MR.strings.video_will_be_received_when_contact_completes_uploading) - ) - - FileProtocol.SMP -> - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.waiting_for_video), - generalGetString(MR.strings.video_will_be_received_when_contact_is_online) - ) - } - CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ? - CIFileStatus.RcvComplete -> {} // ? - CIFileStatus.RcvCancelled -> {} // TODO - else -> {} - } - } - }, - onLongClick = { - showMenu.value = true - }) - if (file != null) { - DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/) - } - if (file?.fileStatus is CIFileStatus.RcvInvitation) { - PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, encrypted = false, receiveFile) } - } - } - } - loadingIndicator(file) - } -} - -@Composable -private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defaultDuration: Long, showMenu: MutableState<Boolean>, onClick: () -> Unit) { - val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, false, defaultPreview, defaultDuration, true) } - val videoPlaying = remember(uri.path) { player.videoPlaying } - val progress = remember(uri.path) { player.progress } - val duration = remember(uri.path) { player.duration } - val preview by remember { player.preview } - // val soundEnabled by rememberSaveable(uri.path) { player.soundEnabled } - val brokenVideo by rememberSaveable(uri.path) { player.brokenVideo } - val play = { - player.enableSound(true) - player.play(true) - } - val stop = { - player.enableSound(false) - player.stop() - } - val showPreview = remember { derivedStateOf { !videoPlaying.value || progress.value == 0L } } - DisposableEffect(Unit) { - onDispose { - stop() - } - } - val onLongClick = { showMenu.value = true } - Box { - val windowWidth = LocalWindowWidth() - val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH } - PlayerView( - player, - width, - onClick = onClick, - onLongClick = onLongClick, - stop - ) - if (showPreview.value) { - VideoPreviewImageView(preview, onClick, onLongClick) - PlayButton(brokenVideo, onLongClick = onLongClick, play) - } - DurationProgress(file, videoPlaying, duration, progress/*, soundEnabled*/) - } -} - -@Composable -expect fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) - -@Composable -private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit, onClick: () -> Unit) { - Surface( - Modifier.align(Alignment.Center), - color = Color.Black.copy(alpha = 0.25f), - shape = RoundedCornerShape(percent = 50) - ) { - Box( - Modifier - .defaultMinSize(minWidth = 40.dp, minHeight = 40.dp) - .combinedClickable(onClick = onClick, onLongClick = onLongClick) - .onRightClick { onLongClick.invoke() }, - contentAlignment = Alignment.Center - ) { - Icon( - painterResource(MR.images.ic_play_arrow_filled), - contentDescription = null, - tint = if (error) WarningOrange else Color.White - ) - } - } -} - -@Composable -private fun DurationProgress(file: CIFile, playing: MutableState<Boolean>, duration: MutableState<Long>, progress: MutableState<Long>/*, soundEnabled: MutableState<Boolean>*/) { - if (duration.value > 0L || progress.value > 0) { - Row { - Box( - Modifier - .padding(DEFAULT_PADDING_HALF) - .background(Color.Black.copy(alpha = 0.35f), RoundedCornerShape(percent = 50)) - .padding(vertical = 2.dp, horizontal = 4.dp) - ) { - val time = if (progress.value > 0) progress.value else duration.value - val timeStr = durationText((time / 1000).toInt()) - val width = if (timeStr.length <= 5) 44 else 50 - Text( - timeStr, - Modifier.widthIn(min = with(LocalDensity.current) { width.sp.toDp() }).padding(horizontal = 4.dp), - fontSize = 13.sp, - color = Color.White - ) - /*if (!soundEnabled.value) { - Icon(painterResource(MR.images.ic_volume_off_filled), null, - Modifier.padding(start = 5.dp).size(10.dp), - tint = Color.White - ) - }*/ - } - if (!playing.value) { - Box( - Modifier - .padding(top = DEFAULT_PADDING_HALF) - .background(Color.Black.copy(alpha = 0.35f), RoundedCornerShape(percent = 50)) - .padding(vertical = 2.dp, horizontal = 4.dp) - ) { - Text( - formatBytes(file.fileSize), - Modifier.padding(horizontal = 4.dp), - fontSize = 13.sp, - color = Color.White - ) - } - } - } - } -} - -@Composable -fun VideoPreviewImageView(preview: ImageBitmap, onClick: () -> Unit, onLongClick: () -> Unit) { - val windowWidth = LocalWindowWidth() - val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH } - Image( - preview, - contentDescription = stringResource(MR.strings.video_descr), - modifier = Modifier - .width(width) - .combinedClickable( - onLongClick = onLongClick, - onClick = onClick - ) - .onRightClick(onLongClick), - contentScale = ContentScale.FillWidth, - ) -} - -@Composable -expect fun LocalWindowWidth(): Dp - -@Composable -private fun progressIndicator() { - CircularProgressIndicator( - Modifier.size(16.dp), - color = Color.White, - strokeWidth = 2.dp - ) -} - -@Composable -private fun fileIcon(icon: Painter, stringId: StringResource) { - Icon( - icon, - stringResource(stringId), - Modifier.fillMaxSize(), - tint = Color.White - ) -} - -@Composable -private fun progressCircle(progress: Long, total: Long) { - val angle = 360f * (progress.toDouble() / total.toDouble()).toFloat() - val strokeWidth = with(LocalDensity.current) { 2.dp.toPx() } - val strokeColor = Color.White - Surface( - Modifier.drawRingModifier(angle, strokeColor, strokeWidth), - color = Color.Transparent, - shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)) - ) { - Box(Modifier.size(16.dp)) - } -} - -@Composable -private fun loadingIndicator(file: CIFile?) { - if (file != null) { - Box( - Modifier - .padding(8.dp) - .size(20.dp), - contentAlignment = Alignment.Center - ) { - when (file.fileStatus) { - is CIFileStatus.SndStored -> - when (file.fileProtocol) { - FileProtocol.XFTP -> progressIndicator() - FileProtocol.SMP -> {} - } - is CIFileStatus.SndTransfer -> - when (file.fileProtocol) { - FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal) - FileProtocol.SMP -> progressIndicator() - } - is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_video_snd_complete) - is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) - is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) - is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_video_asked_to_receive) - is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_video) - is CIFileStatus.RcvTransfer -> - if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) { - progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal) - } else { - progressIndicator() - } - is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) - is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) - is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file) - else -> {} - } - } - } -} - -private fun fileSizeValid(file: CIFile?): Boolean { - if (file != null) { - return file.fileSize <= getMaxFileSize(file.fileProtocol) - } - return false -} - -private fun receiveFileIfValidSize(file: CIFile, encrypted: Boolean, receiveFile: (Long, Boolean) -> Unit) { - if (fileSizeValid(file)) { - receiveFile(file.fileId, encrypted) - } else { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.large_file), - String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol))) - ) - } -} - -private fun videoViewFullWidth(windowWidth: Dp): Dp { - val approximatePadding = 100.dp - return minOf(DEFAULT_MAX_IMAGE_WIDTH, windowWidth - approximatePadding) -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt new file mode 100644 index 0000000000..8289149ad9 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt @@ -0,0 +1,570 @@ +package chat.simplex.common.views.chat.item + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.* +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.* +import androidx.compose.ui.platform.* +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.unit.* +import chat.simplex.res.MR +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.chatViewScrollState +import dev.icerock.moko.resources.StringResource +import java.io.File +import java.net.URI + +@Composable +fun CIVideoView( + image: String, + duration: Int, + file: CIFile?, + imageProvider: () -> ImageGalleryProvider, + showMenu: MutableState<Boolean>, + smallView: Boolean = false, + receiveFile: (Long) -> Unit +) { + val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) } + Box( + Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID) + .desktopModifyBlurredState(!smallView, blurred, showMenu), + contentAlignment = Alignment.TopEnd + ) { + val preview = remember(image) { base64ToBitmap(image) } + val filePath = remember(file, CIFile.cachedRemoteFileRequests.toList()) { mutableStateOf(getLoadedFilePath(file)) } + val sizeMultiplier = if (smallView) 0.38f else 1f + if (chatModel.connectedToRemote()) { + LaunchedEffect(file) { + withLongRunningApi(slow = 600_000) { + if (file != null && file.loaded && getLoadedFilePath(file) == null) { + file.loadRemoteFile(false) + filePath.value = getLoadedFilePath(file) + } + } + } + } + val f = filePath.value + if (file != null && f != null) { + val view = LocalMultiplatformView() + val openFullscreen = { + hideKeyboard(view) + ModalManager.fullscreen.showCustomModal(animated = false) { close -> + ImageFullScreenView(imageProvider, close) + } + } + + val uri = remember(filePath) { getAppFileUri(f.substringAfterLast(File.separator)) } + val autoPlay = remember { mutableStateOf(false) } + val uriDecrypted = remember(filePath) { mutableStateOf(if (file.fileSource?.cryptoArgs == null) uri else file.fileSource.decryptedGet()) } + val decrypted = uriDecrypted.value + if (decrypted != null && smallView) { + SmallVideoView(decrypted, file, preview, duration * 1000L, autoPlay, sizeMultiplier, openFullscreen = openFullscreen) + } else if (decrypted != null) { + VideoView(decrypted, file, preview, duration * 1000L, autoPlay, showMenu, blurred, openFullscreen = openFullscreen) + } else if (smallView) { + SmallVideoViewEncrypted(uriDecrypted, file, preview, autoPlay, showMenu, sizeMultiplier, openFullscreen = openFullscreen) + } else { + VideoViewEncrypted(uriDecrypted, file, preview, duration * 1000L, autoPlay, showMenu, blurred, openFullscreen = openFullscreen) + } + } else { + Box { + VideoPreviewImageView(preview, blurred = blurred, onClick = { + if (file != null) { + when (file.fileStatus) { + CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted -> + receiveFileIfValidSize(file, receiveFile) + CIFileStatus.RcvAccepted -> + when (file.fileProtocol) { + FileProtocol.XFTP -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.waiting_for_video), + generalGetString(MR.strings.video_will_be_received_when_contact_completes_uploading) + ) + FileProtocol.SMP -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.waiting_for_video), + generalGetString(MR.strings.video_will_be_received_when_contact_is_online) + ) + FileProtocol.LOCAL -> {} + } + CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ? + CIFileStatus.RcvComplete -> {} // ? + CIFileStatus.RcvCancelled -> {} // TODO + else -> {} + } + } + }, + smallView = smallView, + onLongClick = { + showMenu.value = true + }) + if (file != null && !smallView) { + DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/) + } + if (showDownloadButton(file?.fileStatus) && !blurred.value && file != null) { + PlayButton(error = false, sizeMultiplier, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) } + } + } + } + // Do not show download icon when the view is blurred + if (!smallView && (!showDownloadButton(file?.fileStatus) || !blurred.value)) { + fileStatusIcon(file, false) + } else if (smallView && file?.showStatusIconInSmallView == true) { + Box(Modifier.align(Alignment.Center)) { + fileStatusIcon(file, true) + } + } + } +} + +@Composable +private fun VideoViewEncrypted( + uriUnencrypted: MutableState<URI?>, + file: CIFile, + defaultPreview: ImageBitmap, + defaultDuration: Long, + autoPlay: MutableState<Boolean>, + showMenu: MutableState<Boolean>, + blurred: MutableState<Boolean>, + openFullscreen: () -> Unit, +) { + var decryptionInProgress by rememberSaveable(file.fileName) { mutableStateOf(false) } + val onLongClick = { showMenu.value = true } + Box { + VideoPreviewImageView(defaultPreview, smallView = false, blurred = blurred, if (decryptionInProgress) {{}} else openFullscreen, onLongClick) + if (decryptionInProgress) { + VideoDecryptionProgress(1f, onLongClick = onLongClick) + } else if (!blurred.value) { + PlayButton(false, 1f, onLongClick = onLongClick) { + decryptionInProgress = true + withBGApi { + try { + uriUnencrypted.value = file.fileSource?.decryptedGetOrCreate() + autoPlay.value = uriUnencrypted.value != null + } finally { + decryptionInProgress = false + } + } + } + } + DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(defaultDuration) }, remember { mutableStateOf(0L) }) + } +} + +@Composable +private fun SmallVideoViewEncrypted( + uriUnencrypted: MutableState<URI?>, + file: CIFile, + defaultPreview: ImageBitmap, + autoPlay: MutableState<Boolean>, + showMenu: MutableState<Boolean>, + sizeMultiplier: Float, + openFullscreen: () -> Unit, +) { + var decryptionInProgress by rememberSaveable(file.fileName) { mutableStateOf(false) } + val onLongClick = { showMenu.value = true } + Box { + VideoPreviewImageView(defaultPreview, smallView = true, blurred = remember { mutableStateOf(false) }, onClick = if (decryptionInProgress) {{}} else openFullscreen, onLongClick = onLongClick) + if (decryptionInProgress) { + VideoDecryptionProgress(sizeMultiplier, onLongClick = onLongClick) + } else if (!file.showStatusIconInSmallView) { + PlayButton(false, sizeMultiplier, onLongClick = onLongClick) { + decryptionInProgress = true + withBGApi { + try { + uriUnencrypted.value = file.fileSource?.decryptedGetOrCreate() + autoPlay.value = uriUnencrypted.value != null + } finally { + decryptionInProgress = false + } + } + } + } + } +} + +@Composable +private fun SmallVideoView( + uri: URI, + file: CIFile, + defaultPreview: ImageBitmap, + defaultDuration: Long, + autoPlay: MutableState<Boolean>, + sizeMultiplier: Float, + openFullscreen: () -> Unit +) { + val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, true, defaultPreview, defaultDuration, true) } + val preview by remember { player.preview } + // val soundEnabled by rememberSaveable(uri.path) { player.soundEnabled } + val brokenVideo by rememberSaveable(uri.path) { player.brokenVideo } + Box { + val windowWidth = LocalWindowWidth() + val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH } + PlayerView( + player, + width, + onClick = openFullscreen, + onLongClick = {}, + {} + ) + VideoPreviewImageView(preview, smallView = true, blurred = remember { mutableStateOf(false) }, onClick = openFullscreen, onLongClick = {}) + if (!file.showStatusIconInSmallView) { + PlayButton(brokenVideo, sizeMultiplier, onLongClick = {}, onClick = openFullscreen) + } + } + LaunchedEffect(uri) { + if (autoPlay.value) openFullscreen() + } +} + +@Composable +private fun VideoView( + uri: URI, + file: CIFile, + defaultPreview: ImageBitmap, + defaultDuration: Long, + autoPlay: MutableState<Boolean>, + showMenu: MutableState<Boolean>, + blurred: MutableState<Boolean>, + openFullscreen: () -> Unit +) { + val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, false, defaultPreview, defaultDuration, true) } + val videoPlaying = remember(uri.path) { player.videoPlaying } + val progress = remember(uri.path) { player.progress } + val duration = remember(uri.path) { player.duration } + val preview by remember { player.preview } + // val soundEnabled by rememberSaveable(uri.path) { player.soundEnabled } + val brokenVideo by rememberSaveable(uri.path) { player.brokenVideo } + val play = { + player.enableSound(true) + player.play(true) + } + val stop = { + player.enableSound(false) + player.stop() + } + val showPreview = remember { derivedStateOf { !videoPlaying.value || progress.value == 0L } } + LaunchedEffect(uri) { + if (autoPlay.value) play() + } + // Drop autoPlay only when show preview changes to prevent blinking of the view + KeyChangeEffect(showPreview.value) { + autoPlay.value = false + } + DisposableEffect(Unit) { + onDispose { + stop() + } + } + val onLongClick = { showMenu.value = true } + Box { + val windowWidth = LocalWindowWidth() + val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH } + PlayerView( + player, + width, + onClick = openFullscreen, + onLongClick = onLongClick, + stop + ) + if (showPreview.value) { + VideoPreviewImageView(preview, smallView = false, blurred = blurred, openFullscreen, onLongClick) + if (!autoPlay.value && !blurred.value) { + PlayButton(brokenVideo, onLongClick = onLongClick, onClick = play) + } + } + DurationProgress(file, videoPlaying, duration, progress/*, soundEnabled*/) + } +} + +@Composable +expect fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) + +@Composable +private fun BoxScope.PlayButton(error: Boolean = false, sizeMultiplier: Float = 1f, onLongClick: () -> Unit, onClick: () -> Unit) { + Surface( + Modifier.align(if (sizeMultiplier != 1f) Alignment.TopStart else Alignment.Center), + color = Color.Black.copy(alpha = 0.25f), + shape = RoundedCornerShape(percent = 50), + contentColor = LocalContentColor.current + ) { + Box( + Modifier + .defaultMinSize(minWidth = if (sizeMultiplier != 1f) 40.sp.toDp() * sizeMultiplier else 40.sp.toDp(), minHeight = if (sizeMultiplier != 1f) 40.sp.toDp() * sizeMultiplier else 40.sp.toDp()) + .combinedClickable(onClick = onClick, onLongClick = onLongClick) + .onRightClick { onLongClick.invoke() }, + contentAlignment = Alignment.Center + ) { + Icon( + painterResource(MR.images.ic_play_arrow_filled), + contentDescription = null, + Modifier.size(if (sizeMultiplier != 1f) 24.sp.toDp() * sizeMultiplier * 1.6f else 24.sp.toDp()), + tint = if (error) WarningOrange else Color.White + ) + } + } +} + +@Composable +fun BoxScope.VideoDecryptionProgress(sizeMultiplier: Float = 1f, onLongClick: () -> Unit) { + Surface( + Modifier.align(if (sizeMultiplier != 1f) Alignment.TopStart else Alignment.Center), + color = Color.Black.copy(alpha = 0.25f), + shape = RoundedCornerShape(percent = 50), + contentColor = LocalContentColor.current + ) { + Box( + Modifier + .defaultMinSize(minWidth = if (sizeMultiplier != 1f) 40.sp.toDp() * sizeMultiplier else 40.sp.toDp(), minHeight = if (sizeMultiplier != 1f) 40.sp.toDp() * sizeMultiplier else 40.sp.toDp()) + .combinedClickable(onClick = {}, onLongClick = onLongClick) + .onRightClick { onLongClick.invoke() }, + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + Modifier + .size(if (sizeMultiplier != 1f) 30.sp.toDp() * sizeMultiplier else 30.sp.toDp()), + color = Color.White, + strokeWidth = 2.5.sp.toDp() * sizeMultiplier + ) + } + } +} + +@Composable +private fun DurationProgress(file: CIFile, playing: MutableState<Boolean>, duration: MutableState<Long>, progress: MutableState<Long>/*, soundEnabled: MutableState<Boolean>*/) { + if (duration.value > 0L || progress.value > 0) { + Row { + Box( + Modifier + .padding(DEFAULT_PADDING_HALF) + .background(Color.Black.copy(alpha = 0.35f), RoundedCornerShape(percent = 50)) + .padding(vertical = 2.dp, horizontal = 4.dp) + ) { + val time = if (progress.value > 0) progress.value else duration.value + val timeStr = durationText((time / 1000).toInt()) + val width = if (timeStr.length <= 5) 44 else 50 + Text( + timeStr, + Modifier.widthIn(min = with(LocalDensity.current) { width.sp.toDp() }).padding(horizontal = 4.dp), + fontSize = 13.sp, + color = Color.White + ) + /*if (!soundEnabled.value) { + Icon(painterResource(MR.images.ic_volume_off_filled), null, + Modifier.padding(start = 5.dp).size(10.dp), + tint = Color.White + ) + }*/ + } + if (!playing.value) { + Box( + Modifier + .padding(top = DEFAULT_PADDING_HALF) + .background(Color.Black.copy(alpha = 0.35f), RoundedCornerShape(percent = 50)) + .padding(vertical = 2.dp, horizontal = 4.dp) + ) { + Text( + formatBytes(file.fileSize), + Modifier.padding(horizontal = 4.dp), + fontSize = 13.sp, + color = Color.White + ) + } + } + } + } +} + +@Composable +fun VideoPreviewImageView( + preview: ImageBitmap, + smallView: Boolean, + blurred: MutableState<Boolean>, + onClick: () -> Unit, + onLongClick: () -> Unit +) { + val windowWidth = LocalWindowWidth() + val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH } + Image( + preview, + contentDescription = stringResource(MR.strings.video_descr), + modifier = Modifier + .width(width) + .combinedClickable( + onLongClick = onLongClick, + onClick = onClick + ) + .onRightClick(onLongClick) + .privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = onLongClick), + contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth, + ) +} + +@Composable +fun VideoPreviewImageViewFullScreen(preview: ImageBitmap, onClick: () -> Unit, onLongClick: () -> Unit) { + Image( + preview, + contentDescription = stringResource(MR.strings.video_descr), + modifier = Modifier + .fillMaxSize() + .combinedClickable( + onLongClick = onLongClick, + onClick = onClick + ) + .onRightClick(onLongClick), + contentScale = ContentScale.FillWidth, + ) +} + +@Composable +expect fun LocalWindowWidth(): Dp + +@Composable +expect fun LocalWindowHeight(): Dp + +@Composable +private fun progressIndicator() { + CircularProgressIndicator( + Modifier.size(16.dp), + color = Color.White, + strokeWidth = 2.dp + ) +} + +@Composable +private fun fileIcon(icon: Painter, stringId: StringResource, onClick: (() -> Unit)? = null) { + var modifier = Modifier.fillMaxSize() + modifier = if (onClick != null) { modifier.clickable { onClick() } } else { modifier } + Icon( + icon, + stringResource(stringId), + modifier, + tint = Color.White + ) +} + +@Composable +private fun progressCircle(progress: Long, total: Long) { + val angle = 360f * (progress.toDouble() / total.toDouble()).toFloat() + val strokeWidth = with(LocalDensity.current) { 2.dp.toPx() } + val strokeColor = Color.White + Surface( + Modifier.drawRingModifier(angle, strokeColor, strokeWidth), + color = Color.Transparent, + shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)), + contentColor = LocalContentColor.current + ) { + Box(Modifier.size(16.dp)) + } +} + +@Composable +private fun fileStatusIcon(file: CIFile?, smallView: Boolean) { + if (file != null) { + Box( + Modifier + .padding(if (smallView) 0.dp else 8.dp) + .size(20.dp), + contentAlignment = Alignment.Center + ) { + when (file.fileStatus) { + is CIFileStatus.SndStored -> + when (file.fileProtocol) { + FileProtocol.XFTP -> progressIndicator() + FileProtocol.SMP -> {} + FileProtocol.LOCAL -> {} + } + is CIFileStatus.SndTransfer -> + when (file.fileProtocol) { + FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal) + FileProtocol.SMP -> progressIndicator() + FileProtocol.LOCAL -> {} + } + is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_video_snd_complete) + is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) + is CIFileStatus.SndError -> + fileIcon( + painterResource(MR.images.ic_close), + MR.strings.icon_descr_file, + onClick = { + showFileErrorAlert(file.fileStatus.sndFileError) + } + ) + is CIFileStatus.SndWarning -> + fileIcon( + painterResource(MR.images.ic_warning_filled), + MR.strings.icon_descr_file, + onClick = { + showFileErrorAlert(file.fileStatus.sndFileError, temporary = true) + } + ) + is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_video_asked_to_receive) + is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_video) + is CIFileStatus.RcvTransfer -> + if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) { + progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal) + } else { + progressIndicator() + } + is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file) + is CIFileStatus.RcvComplete -> {} + is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) + is CIFileStatus.RcvError -> + fileIcon( + painterResource(MR.images.ic_close), + MR.strings.icon_descr_file, + onClick = { + showFileErrorAlert(file.fileStatus.rcvFileError) + } + ) + is CIFileStatus.RcvWarning -> + fileIcon( + painterResource(MR.images.ic_warning_filled), + MR.strings.icon_descr_file, + onClick = { + showFileErrorAlert(file.fileStatus.rcvFileError, temporary = true) + } + ) + is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file) + } + } + } +} + +private fun showDownloadButton(status: CIFileStatus?): Boolean = + status is CIFileStatus.RcvInvitation || status is CIFileStatus.RcvAborted + +private fun fileSizeValid(file: CIFile?): Boolean { + if (file != null) { + return file.fileSize <= getMaxFileSize(file.fileProtocol) + } + return false +} + +private fun receiveFileIfValidSize(file: CIFile, receiveFile: (Long) -> Unit) { + if (fileSizeValid(file)) { + receiveFile(file.fileId) + } else { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.large_file), + String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol))) + ) + } +} + +private fun videoViewFullWidth(windowWidth: Dp): Dp { + val approximatePadding = 100.dp + return minOf(DEFAULT_MAX_IMAGE_WIDTH, windowWidth - approximatePadding) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt index 0c8487458f..136300e4ed 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt @@ -22,7 +22,9 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.res.MR +import dev.icerock.moko.resources.ImageResource import kotlinx.coroutines.flow.* +import kotlin.math.* // TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901 @@ -35,11 +37,20 @@ fun CIVoiceView( hasText: Boolean, ci: ChatItem, timedMessagesTTL: Int?, + showViaProxy: Boolean, + showTimestamp: Boolean, + smallView: Boolean = false, longClick: () -> Unit, - receiveFile: (Long, Boolean) -> Unit, + receiveFile: (Long) -> Unit, ) { + val sizeMultiplier = if (smallView) voiceMessageSizeBasedOnSquareSize(36f) / 56f else 1f + val padding = when { + smallView -> PaddingValues() + hasText -> PaddingValues(top = 14.sp.toDp() * sizeMultiplier, bottom = 14.sp.toDp() * sizeMultiplier, start = 6.sp.toDp() * sizeMultiplier, end = 6.sp.toDp() * sizeMultiplier) + else -> PaddingValues(top = 4.sp.toDp() * sizeMultiplier, bottom = 6.sp.toDp() * sizeMultiplier, start = 0.dp, end = 0.dp) + } Row( - Modifier.padding(top = if (hasText) 14.dp else 4.dp, bottom = if (hasText) 14.dp else 6.dp, start = if (hasText) 6.dp else 0.dp, end = if (hasText) 6.dp else 0.dp), + Modifier.padding(padding), verticalAlignment = Alignment.CenterVertically ) { if (file != null) { @@ -52,7 +63,7 @@ fun CIVoiceView( val play: () -> Unit = { val playIfExists = { if (fileSource.value != null) { - AudioPlayer.play(fileSource.value!!, audioPlaying, progress, duration, true) + AudioPlayer.play(fileSource.value!!, audioPlaying, progress, duration, resetOnEnd = true, smallView = smallView) brokenAudio = !audioPlaying.value } } @@ -67,7 +78,7 @@ fun CIVoiceView( val pause = { AudioPlayer.pause(audioPlaying, progress) } - val text = remember { + val text = remember(ci.file?.fileId, ci.file?.fileStatus) { derivedStateOf { val time = when { audioPlaying.value || progress.value != 0 -> progress.value @@ -76,11 +87,18 @@ fun CIVoiceView( durationText(time / 1000) } } - VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick, receiveFile) { + VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, showTimestamp, sizeMultiplier, play, pause, longClick, receiveFile) { AudioPlayer.seekTo(it, progress, fileSource.value?.filePath) } + if (smallView) { + KeyChangeEffect(chatModel.chatId.value, chatModel.currentUser.value?.userId, chatModel.currentRemoteHost.value) { + AudioPlayer.stop() + } + } + } else if (smallView) { + VoiceMsgIndicator(null, false, sent, hasText, null, null, false, sizeMultiplier, {}, {}, longClick, receiveFile) } else { - VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick, receiveFile) + VoiceMsgIndicator(null, false, sent, hasText, null, null, false, 1f, {}, {}, longClick, receiveFile) val metaReserve = if (edited) " " else @@ -102,10 +120,13 @@ private fun VoiceLayout( sent: Boolean, hasText: Boolean, timedMessagesTTL: Int?, + showViaProxy: Boolean, + showTimestamp: Boolean, + sizeMultiplier: Float, play: () -> Unit, pause: () -> Unit, longClick: () -> Unit, - receiveFile: (Long, Boolean) -> Unit, + receiveFile: (Long) -> Unit, onProgressChanged: (Int) -> Unit, ) { @Composable @@ -113,15 +134,16 @@ private fun VoiceLayout( var movedManuallyTo by rememberSaveable(file.fileId) { mutableStateOf(-1) } if (audioPlaying.value || progress.value > 0 || movedManuallyTo == progress.value) { val dp4 = with(LocalDensity.current) { 4.dp.toPx() } - val dp10 = with(LocalDensity.current) { 10.dp.toPx() } val primary = MaterialTheme.colors.primary val inactiveTrackColor = MaterialTheme.colors.primary.mixWith( backgroundColor.copy(1f).mixWith(MaterialTheme.colors.background, backgroundColor.alpha), 0.24f) val width = LocalWindowWidth() + // Built-in slider has rounded corners but we need square corners, so drawing a track manually val colors = SliderDefaults.colors( - inactiveTrackColor = inactiveTrackColor + inactiveTrackColor = Color.Transparent, + activeTrackColor = Color.Transparent ) Slider( progress.value.toFloat(), @@ -130,12 +152,12 @@ private fun VoiceLayout( movedManuallyTo = it.toInt() }, Modifier - .size(width, 48.dp) + .size(width, 48.sp.toDp()) .weight(1f) .padding(padding) .drawBehind { - drawRect(primary, Offset(0f, (size.height - dp4) / 2), size = androidx.compose.ui.geometry.Size(dp10, dp4)) - drawRect(inactiveTrackColor, Offset(size.width - dp10, (size.height - dp4) / 2), size = androidx.compose.ui.geometry.Size(dp10, dp4)) + drawRect(inactiveTrackColor, Offset(0f, (size.height - dp4) / 2), size = Size(size.width, dp4)) + drawRect(primary, Offset(0f, (size.height - dp4) / 2), size = Size(progress.value.toFloat() / max(0.00001f, duration.value.toFloat()) * size.width, dp4)) }, valueRange = 0f..duration.value.toFloat(), colors = colors @@ -150,13 +172,22 @@ private fun VoiceLayout( } } when { - hasText -> { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage - Spacer(Modifier.width(6.dp)) - VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile) + sizeMultiplier != 1f -> { Row(verticalAlignment = Alignment.CenterVertically) { - DurationText(text, PaddingValues(start = 12.dp)) + VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, sizeMultiplier, play, pause, longClick, receiveFile) + Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically) { + DurationText(text, PaddingValues(start = 8.sp.toDp()), true) + Slider(MaterialTheme.colors.background, PaddingValues(start = 7.sp.toDp())) + } + } + } + hasText -> { + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage + Spacer(Modifier.width(6.sp.toDp() * sizeMultiplier)) + VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, 1f, play, pause, longClick, receiveFile) + Row(verticalAlignment = Alignment.CenterVertically) { + DurationText(text, PaddingValues(start = 12.sp.toDp() * sizeMultiplier)) Slider(if (ci.chatDir.sent) sentColor else receivedColor) } } @@ -164,29 +195,29 @@ private fun VoiceLayout( Column(horizontalAlignment = Alignment.End) { Row { Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End) { - Spacer(Modifier.height(56.dp)) + Spacer(Modifier.height(56.sp.toDp() * sizeMultiplier)) Slider(MaterialTheme.colors.background, PaddingValues(end = DEFAULT_PADDING_HALF + 3.dp)) - DurationText(text, PaddingValues(end = 12.dp)) + DurationText(text, PaddingValues(end = 12.sp.toDp() * sizeMultiplier)) } - VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile) + VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, 1f, play, pause, longClick, receiveFile) } - Box(Modifier.padding(top = 6.dp, end = 6.dp)) { - CIMetaView(ci, timedMessagesTTL) + Box(Modifier.padding(top = 6.sp.toDp() * sizeMultiplier, end = 6.sp.toDp() * sizeMultiplier)) { + CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } } else -> { Column(horizontalAlignment = Alignment.Start) { Row { - VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile) + VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, 1f, play, pause, longClick, receiveFile) Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) { - DurationText(text, PaddingValues(start = 12.dp)) + DurationText(text, PaddingValues(start = 12.sp.toDp() * sizeMultiplier)) Slider(MaterialTheme.colors.background, PaddingValues(start = DEFAULT_PADDING_HALF + 3.dp)) - Spacer(Modifier.height(56.dp)) + Spacer(Modifier.height(56.sp.toDp() * sizeMultiplier)) } } - Box(Modifier.padding(top = 6.dp)) { - CIMetaView(ci, timedMessagesTTL) + Box(Modifier.padding(top = 6.sp.toDp() * sizeMultiplier)) { + CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } } @@ -194,7 +225,7 @@ private fun VoiceLayout( } @Composable -private fun DurationText(text: State<String>, padding: PaddingValues) { +private fun DurationText(text: State<String>, padding: PaddingValues, smallView: Boolean = false) { val minWidth = with(LocalDensity.current) { 45.sp.toDp() } Text( text.value, @@ -202,7 +233,7 @@ private fun DurationText(text: State<String>, padding: PaddingValues) { .padding(padding) .widthIn(min = minWidth), color = MaterialTheme.colors.secondary, - fontSize = 16.sp, + fontSize = if (smallView) 15.sp else 16.sp, maxLines = 1 ) } @@ -216,32 +247,119 @@ private fun PlayPauseButton( strokeColor: Color, enabled: Boolean, error: Boolean, + sizeMultiplier: Float = 1f, play: () -> Unit, pause: () -> Unit, - longClick: () -> Unit + longClick: () -> Unit, + icon: ImageResource = MR.images.ic_play_arrow_filled, ) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( Modifier.drawRingModifier(angle, strokeColor, strokeWidth), color = if (sent) sentColor else receivedColor, - shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)) + shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)), + contentColor = LocalContentColor.current ) { Box( Modifier - .defaultMinSize(minWidth = 56.dp, minHeight = 56.dp) + .defaultMinSize(minWidth = 56.sp.toDp() * sizeMultiplier, minHeight = 56.sp.toDp() * sizeMultiplier) .combinedClickable( onClick = { if (!audioPlaying) play() else pause() }, onLongClick = longClick ) .onRightClick { longClick() }, contentAlignment = Alignment.Center + ) { + Icon( + if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(icon), + contentDescription = null, + Modifier.size(36.sp.toDp() * sizeMultiplier), + tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + } +} + +@Composable +private fun PlayablePlayPauseButton( + audioPlaying: Boolean, + sent: Boolean, + hasText: Boolean, + progress: State<Int>, + duration: State<Int>, + strokeWidth: Float, + strokeColor: Color, + error: Boolean, + sizeMultiplier: Float = 1f, + play: () -> Unit, + pause: () -> Unit, + longClick: () -> Unit, +) { + val angle = 360f * (progress.value.toDouble() / duration.value).toFloat() + if (hasText) { + Box( + Modifier + .defaultMinSize(minWidth = 56.sp.toDp() * sizeMultiplier, minHeight = 56.sp.toDp() * sizeMultiplier) + .clip(MaterialTheme.shapes.small.copy(CornerSize(percent = 50))) + .combinedClickable(onClick = { if (!audioPlaying) play() else pause() } ) + .drawRingModifier(angle, strokeColor, strokeWidth), + contentAlignment = Alignment.Center ) { Icon( if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled), contentDescription = null, - Modifier.size(36.dp), - tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + Modifier.size(36.sp.toDp() * sizeMultiplier), + tint = MaterialTheme.colors.primary + ) + } + } else { + PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, sizeMultiplier, play, pause, longClick = longClick) + } +} + +@Composable +private fun VoiceMsgLoadingProgressIndicator(sizeMultiplier: Float) { + Box( + Modifier + .size(56.sp.toDp() * sizeMultiplier) + .clip(RoundedCornerShape(4.sp.toDp() * sizeMultiplier)), + contentAlignment = Alignment.Center + ) { + ProgressIndicator(sizeMultiplier) + } +} + +@Composable +private fun FileStatusIcon( + sent: Boolean, + icon: ImageResource, + sizeMultiplier: Float, + longClick: () -> Unit, + onClick: () -> Unit, +) { + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage + Surface( + color = if (sent) sentColor else receivedColor, + shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)), + contentColor = LocalContentColor.current + ) { + Box( + Modifier + .defaultMinSize(minWidth = 56.sp.toDp() * sizeMultiplier, minHeight = 56.sp.toDp() * sizeMultiplier) + .combinedClickable( + onClick = onClick, + onLongClick = longClick + ) + .onRightClick { longClick() }, + contentAlignment = Alignment.Center + ) { + Icon( + painterResource(icon), + contentDescription = null, + Modifier.size(36.sp.toDp() * sizeMultiplier), + tint = MaterialTheme.colors.secondary ) } } @@ -256,44 +374,73 @@ private fun VoiceMsgIndicator( progress: State<Int>?, duration: State<Int>?, error: Boolean, + sizeMultiplier: Float, play: () -> Unit, pause: () -> Unit, longClick: () -> Unit, - receiveFile: (Long, Boolean) -> Unit, + receiveFile: (Long) -> Unit, ) { - val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() } + val strokeWidth = with(LocalDensity.current) { 3.sp.toPx() } * sizeMultiplier val strokeColor = MaterialTheme.colors.primary - if (file != null && file.loaded && progress != null && duration != null) { - val angle = 360f * (progress.value.toDouble() / duration.value).toFloat() - if (hasText) { - IconButton({ if (!audioPlaying) play() else pause() }, Modifier.size(56.dp).drawRingModifier(angle, strokeColor, strokeWidth)) { - Icon( - if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled), - contentDescription = null, - Modifier.size(36.dp), - tint = MaterialTheme.colors.primary - ) + when { + file?.fileStatus is CIFileStatus.SndStored -> + if (file.fileProtocol == FileProtocol.LOCAL && progress != null && duration != null) { + PlayablePlayPauseButton(audioPlaying, sent, hasText, progress, duration, strokeWidth, strokeColor, error, sizeMultiplier, play, pause, longClick = longClick) + } else { + VoiceMsgLoadingProgressIndicator(sizeMultiplier) } - } else { - PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick) - } - } else { - if (file?.fileStatus is CIFileStatus.RcvInvitation) { - PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId, chatController.appPrefs.privacyEncryptLocalFiles.get()) }, {}, longClick = longClick) - } else if (file?.fileStatus is CIFileStatus.RcvTransfer - || file?.fileStatus is CIFileStatus.RcvAccepted - ) { - Box( - Modifier - .size(56.dp) - .clip(RoundedCornerShape(4.dp)), - contentAlignment = Alignment.Center - ) { - ProgressIndicator() - } - } else { - PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick) - } + file?.fileStatus is CIFileStatus.SndTransfer -> + VoiceMsgLoadingProgressIndicator(sizeMultiplier) + file != null && file.fileStatus is CIFileStatus.SndError -> + FileStatusIcon( + sent, + MR.images.ic_close, + sizeMultiplier, + longClick, + onClick = { + showFileErrorAlert(file.fileStatus.sndFileError) + } + ) + file != null && file.fileStatus is CIFileStatus.SndWarning -> + FileStatusIcon( + sent, + MR.images.ic_warning_filled, + sizeMultiplier, + longClick, + onClick = { + showFileErrorAlert(file.fileStatus.sndFileError, temporary = true) + } + ) + file?.fileStatus is CIFileStatus.RcvInvitation -> + PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, sizeMultiplier, { receiveFile(file.fileId) }, {}, longClick = longClick) + file?.fileStatus is CIFileStatus.RcvTransfer || file?.fileStatus is CIFileStatus.RcvAccepted -> + VoiceMsgLoadingProgressIndicator(sizeMultiplier) + file?.fileStatus is CIFileStatus.RcvAborted -> + PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, sizeMultiplier, { receiveFile(file.fileId) }, {}, longClick = longClick, icon = MR.images.ic_sync_problem) + file != null && file.fileStatus is CIFileStatus.RcvError -> + FileStatusIcon( + sent, + MR.images.ic_close, + sizeMultiplier, + longClick, + onClick = { + showFileErrorAlert(file.fileStatus.rcvFileError) + } + ) + file != null && file.fileStatus is CIFileStatus.RcvWarning -> + FileStatusIcon( + sent, + MR.images.ic_warning_filled, + sizeMultiplier, + longClick, + onClick = { + showFileErrorAlert(file.fileStatus.rcvFileError, temporary = true) + } + ) + file != null && file.loaded && progress != null && duration != null -> + PlayablePlayPauseButton(audioPlaying, sent, hasText, progress, duration, strokeWidth, strokeColor, error, sizeMultiplier, play, pause, longClick = longClick) + else -> + PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, sizeMultiplier, {}, {}, longClick) } } @@ -319,11 +466,16 @@ fun Modifier.drawRingModifier(angle: Float, color: Color, strokeWidth: Float) = } } +fun voiceMessageSizeBasedOnSquareSize(squareSize: Float): Float { + val squareToCircleRatio = 0.935f + return squareSize + squareSize * (1 - squareToCircleRatio) +} + @Composable -private fun ProgressIndicator() { +private fun ProgressIndicator(sizeMultiplier: Float) { CircularProgressIndicator( - Modifier.size(32.dp), + Modifier.size(32.sp.toDp() * sizeMultiplier), color = if (isInDarkTheme()) FileDark else FileLight, - strokeWidth = 4.dp + strokeWidth = 4.sp.toDp() * sizeMultiplier ) } 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 daf887e8c3..2e789df7bc 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 @@ -1,15 +1,18 @@ package chat.simplex.common.views.chat.item +import SectionItemView import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.shape.* 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.graphics.Color +import androidx.compose.ui.geometry.* +import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.* @@ -17,17 +20,30 @@ import androidx.compose.ui.text.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.ChatModel.currentUser import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* +import chat.simplex.common.views.chatlist.openChat import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import dev.icerock.moko.resources.ImageResource import kotlinx.datetime.Clock +import kotlin.math.* // TODO refactor so that FramedItemView can show all CIContent items if they're deleted (see Swift code) +private val msgRectMaxRadius = 18.dp +private val msgBubbleMaxRadius = msgRectMaxRadius * 1.2f +val msgTailWidthDp = 9.dp +private val msgTailMinHeightDp = msgTailWidthDp * 1.254f // ~56deg +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 = @@ -38,25 +54,42 @@ fun chatEventText(eventText: String, ts: String): AnnotatedString = withStyle(chatEventStyle) { append("$eventText $ts") } } +data class ChatItemReactionMenuItem ( + val name: String, + val image: String?, + val onClick: (() -> Unit)? +) + @Composable fun ChatItemView( + chatsCtx: ChatModel.ChatsContext, + rhId: Long?, cInfo: ChatInfo, cItem: ChatItem, composeState: MutableState<ComposeState>, imageProvider: (() -> ImageGalleryProvider)? = null, useLinkPreviews: Boolean, linkMode: SimplexLinkMode, - revealed: MutableState<Boolean>, - range: IntRange?, + revealed: State<Boolean>, + highlighted: State<Boolean>, + hoveredItemId: MutableState<Long?>, + range: State<IntRange?>, + selectedChatItems: MutableState<Set<Long>?>, + searchIsNotBlank: State<Boolean>, + fillMaxWidth: Boolean = true, + selectChatItem: () -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List<Long>) -> Unit, - receiveFile: (Long, Boolean) -> Unit, + archiveReports: (List<Long>, Boolean) -> Unit, + receiveFile: (Long) -> Unit, cancelFile: (Long) -> Unit, joinGroup: (Long, () -> Unit) -> Unit, acceptCall: (Contact) -> Unit, scrollToItem: (Long) -> Unit, + scrollToQuotedItemFromItem: (Long) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, openDirectChat: (Long) -> Unit, + forwardItem: (ChatInfo, ChatItem) -> Unit, updateContactStats: (Contact) -> Unit, updateMemberStats: (GroupInfo, GroupMember) -> Unit, syncContactConnection: (Contact) -> Unit, @@ -65,7 +98,14 @@ fun ChatItemView( findModelMember: (String) -> GroupMember?, setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, showItemDetails: (ChatInfo, ChatItem) -> Unit, + reveal: (Boolean) -> Unit, + showMemberInfo: (GroupInfo, GroupMember) -> Unit, + showChatInfo: () -> Unit, developerTools: Boolean, + showViaProxy: Boolean, + showTimestamp: Boolean, + itemSeparation: ItemSeparation, + preview: Boolean = false, ) { val uriHandler = LocalUriHandler.current val sent = cItem.chatDir.sent @@ -73,38 +113,110 @@ fun ChatItemView( val showMenu = remember { mutableStateOf(false) } val fullDeleteAllowed = remember(cInfo) { cInfo.featureEnabled(ChatFeature.FullDelete) } val onLinkLongClick = { _: String -> showMenu.value = true } - val live = composeState.value.liveMessage != null + val live = remember { derivedStateOf { composeState.value.liveMessage != null } }.value Box( - modifier = Modifier - .padding(bottom = 4.dp) - .fillMaxWidth(), + modifier = (if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier), contentAlignment = alignment, ) { - val onClick = { - when (cItem.meta.itemStatus) { - is CIStatus.SndErrorAuth -> { - showMsgDeliveryErrorAlert(generalGetString(MR.strings.message_delivery_error_desc)) - } - is CIStatus.SndError -> { - showMsgDeliveryErrorAlert(generalGetString(MR.strings.unknown_error) + ": ${cItem.meta.itemStatus.agentError}") - } - else -> {} + val info = cItem.meta.itemStatus.statusInto + val onClick = if (info != null) { + { + AlertManager.shared.showAlertMsg( + title = info.first, + text = info.second, + ) } - } + } else { {} } @Composable fun ChatItemReactions() { - Row(verticalAlignment = Alignment.CenterVertically) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.chatItemOffset(cItem, itemSeparation.largeGap, inverted = true, revealed = true)) { cItem.reactions.forEach { r -> - var modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).clip(RoundedCornerShape(8.dp)) - if (cInfo.featureEnabled(ChatFeature.Reactions) && (cItem.allowAddReaction || r.userReacted)) { - modifier = modifier.clickable { - setReaction(cInfo, cItem, !r.userReacted, r.reaction) + val showReactionMenu = remember { mutableStateOf(false) } + val reactionMenuItems = remember { mutableStateOf(emptyList<ChatItemReactionMenuItem>()) } + val interactionSource = remember { MutableInteractionSource() } + val enterInteraction = remember { HoverInteraction.Enter() } + KeyChangeEffect(highlighted.value) { + if (highlighted.value) { + interactionSource.emit(enterInteraction) + } else { + interactionSource.emit(HoverInteraction.Exit(enterInteraction)) } } - Row(modifier.padding(2.dp)) { + + var modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).clip(RoundedCornerShape(8.dp)) + if (cInfo.featureEnabled(ChatFeature.Reactions)) { + fun showReactionsMenu() { + when (cInfo) { + is ChatInfo.Group -> { + withBGApi { + try { + val members = controller.apiGetReactionMembers(rhId, cInfo.groupInfo.groupId, cItem.id, r.reaction) + if (members != null) { + showReactionMenu.value = true + reactionMenuItems.value = members.map { + val enabled = cInfo.groupInfo.membership.groupMemberId != it.groupMember.groupMemberId + val click = if (enabled) ({ showMemberInfo(cInfo.groupInfo, it.groupMember) }) else null + ChatItemReactionMenuItem(it.groupMember.displayName, it.groupMember.image, click) + } + } + } catch (e: Exception) { + Log.d(TAG, "chatItemView ChatItemReactions onLongClick: unexpected exception: ${e.stackTraceToString()}") + } + } + } + is ChatInfo.Direct -> { + showReactionMenu.value = true + val reactions = mutableListOf<ChatItemReactionMenuItem>() + + if (!r.userReacted || r.totalReacted > 1) { + val contact = cInfo.contact + reactions.add(ChatItemReactionMenuItem(contact.displayName, contact.image, showChatInfo)) + } + + if (r.userReacted) { + reactions.add(ChatItemReactionMenuItem(generalGetString(MR.strings.sender_you_pronoun), currentUser.value?.image, null)) + } + reactionMenuItems.value = reactions + } + else -> {} + } + } + modifier = modifier + .combinedClickable( + onClick = { + if (cItem.allowAddReaction || r.userReacted) { + setReaction(cInfo, cItem, !r.userReacted, r.reaction) + } + }, + onLongClick = { + showReactionsMenu() + }, + interactionSource = interactionSource, + indication = LocalIndication.current + ) + .onRightClick { showReactionsMenu() } + } + Row(modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) { ReactionIcon(r.reaction.text, fontSize = 12.sp) + DefaultDropdownMenu(showMenu = showReactionMenu) { + reactionMenuItems.value.forEach { m -> + ItemAction( + text = m.name, + composable = { ProfileImage(44.dp, m.image) }, + onClick = { + val click = m.onClick + if (click != null) { + click() + showReactionMenu.value = false + } + }, + lineLimit = 1, + color = if (m.onClick == null) MaterialTheme.colors.secondary else MenuTextColor + ) + } + } if (r.totalReacted > 1) { Spacer(Modifier.width(4.dp)) Text( @@ -112,7 +224,6 @@ fun ChatItemView( fontSize = 11.5.sp, fontWeight = if (r.userReacted) FontWeight.Bold else FontWeight.Normal, color = if (r.userReacted) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, - modifier = if (appPlatform.isAndroid) Modifier else Modifier.padding(top = 4.dp) ) } } @@ -120,341 +231,553 @@ fun ChatItemView( } } - Column(horizontalAlignment = if (cItem.chatDir.sent) Alignment.End else Alignment.Start) { - Column( - Modifier - .clip(RoundedCornerShape(18.dp)) - .combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick) - .onRightClick { showMenu.value = true }, - ) { - @Composable - fun framedItemView() { - FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem) + @Composable + fun GoToItemInnerButton(alignStart: Boolean, icon: ImageResource, iconSize: Dp = 22.dp, parentActivated: State<Boolean>, onClick: () -> Unit) { + val buttonInteractionSource = remember { MutableInteractionSource() } + val buttonHovered = buttonInteractionSource.collectIsHoveredAsState() + val buttonPressed = buttonInteractionSource.collectIsPressedAsState() + val buttonActivated = remember { derivedStateOf { buttonHovered.value || buttonPressed.value } } + + val fullyVisible = parentActivated.value || buttonActivated.value || hoveredItemId.value == cItem.id + val mixAlpha = 0.6f + val mixedBackgroundColor = if (fullyVisible) { + if (MaterialTheme.colors.isLight) { + MaterialTheme.colors.secondary.mixWith(Color.White, mixAlpha) + } else { + MaterialTheme.colors.secondary.mixWith(Color.Black, mixAlpha) } - - fun deleteMessageQuestionText(): String { - return if (!sent || fullDeleteAllowed) { - generalGetString(MR.strings.delete_message_cannot_be_undone_warning) - } else { - generalGetString(MR.strings.delete_message_mark_deleted_warning) - } - } - - fun moderateMessageQuestionText(): String { - return if (fullDeleteAllowed) { - generalGetString(MR.strings.moderate_message_will_be_deleted_warning) - } else { - generalGetString(MR.strings.moderate_message_will_be_marked_warning) - } - } - - @Composable - fun MsgReactionsMenu() { - val rs = MsgReaction.values.mapNotNull { r -> - if (null == cItem.reactions.find { it.userReacted && it.reaction.text == r.text }) { - r - } else { - null - } - } - if (rs.isNotEmpty()) { - Row(modifier = Modifier.padding(horizontal = DEFAULT_PADDING).horizontalScroll(rememberScrollState()), verticalAlignment = Alignment.CenterVertically) { - rs.forEach() { r -> - Box( - Modifier.size(36.dp).clickable { - setReaction(cInfo, cItem, true, r) - showMenu.value = false - }, - contentAlignment = Alignment.Center - ) { - ReactionIcon(r.text, 12.sp) - } - } - } - } - } - - @Composable - fun MsgContentItemDropdownMenu() { - val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file) - when { - cItem.content.msgContent != null -> { - DefaultDropdownMenu(showMenu) { - if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) { - MsgReactionsMenu() - } - if (cItem.meta.itemDeleted == null && !live) { - 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) - } else { - composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem)) - } - showMenu.value = false - }) - } - val clipboard = LocalClipboardManager.current - val cachedRemoteReqs = remember { CIFile.cachedRemoteFileRequests } - val copyAndShareAllowed = when { - cItem.content.text.isNotEmpty() -> true - cItem.file != null && chatModel.connectedToRemote() && cachedRemoteReqs[cItem.file.fileSource] != false && cItem.file.loaded -> true - getLoadedFilePath(cItem.file) != null -> true - else -> false - } - - if (copyAndShareAllowed) { - ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = { - var fileSource = getLoadedFileSource(cItem.file) - val shareIfExists = { - when (val f = fileSource) { - null -> clipboard.shareText(cItem.content.text) - else -> shareFile(cItem.text, f) - } - showMenu.value = false - } - if (chatModel.connectedToRemote() && fileSource == null) { - withBGApi { - cItem.file?.loadRemoteFile(true) - fileSource = getLoadedFileSource(cItem.file) - shareIfExists() - } - } else shareIfExists() - }) - } - if (copyAndShareAllowed) { - ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = { - copyItemToClipboard(cItem, clipboard) - showMenu.value = false - }) - } - if ((cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) && (getLoadedFilePath(cItem.file) != null || (chatModel.connectedToRemote() && cachedRemoteReqs[cItem.file?.fileSource] != false && cItem.file?.loaded == true))) { - SaveContentItemAction(cItem, saveFileLauncher, showMenu) - } - if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) { - ItemAction(stringResource(MR.strings.edit_verb), painterResource(MR.images.ic_edit_filled), onClick = { - composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews) - showMenu.value = false - }) - } - ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - if (revealed.value) { - HideItemAction(revealed, showMenu) - } - if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null) { - CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction) - } - if (!(live && cItem.meta.isLive)) { - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) - } - val groupInfo = cItem.memberToModerate(cInfo)?.first - if (groupInfo != null) { - ModerateItemAction(cItem, questionText = moderateMessageQuestionText(), showMenu, deleteMessage) - } - } - } - cItem.meta.itemDeleted != null -> { - DefaultDropdownMenu(showMenu) { - if (revealed.value) { - HideItemAction(revealed, showMenu) - } else if (!cItem.isDeletedContent) { - RevealItemAction(revealed, showMenu) - } else if (range != null) { - ExpandItemAction(revealed, showMenu) - } - ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) - } - } - cItem.isDeletedContent -> { - DefaultDropdownMenu(showMenu) { - ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) - } - } - cItem.mergeCategory != null && ((range?.count() ?: 0) > 1 || revealed.value) -> { - DefaultDropdownMenu(showMenu) { - if (revealed.value) { - ShrinkItemAction(revealed, showMenu) - } else { - ExpandItemAction(revealed, showMenu) - } - } - } - else -> { - showMenu.value = false - } - } - } - - @Composable - fun MarkedDeletedItemDropdownMenu() { - DefaultDropdownMenu(showMenu) { - if (!cItem.isDeletedContent) { - RevealItemAction(revealed, showMenu) - } - ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) - } - } - - @Composable - fun ContentItem() { - val mc = cItem.content.msgContent - if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) { - MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed) - MarkedDeletedItemDropdownMenu() - } else { - if (cItem.quotedItem == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) { - if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) { - EmojiItemView(cItem, cInfo.timedMessagesTTL) - } else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) { - CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, longClick = { onLinkLongClick("") }, receiveFile) - } else { - framedItemView() - } - } else { - framedItemView() - } - MsgContentItemDropdownMenu() - } - } - - @Composable fun DeletedItem() { - DeletedItemView(cItem, cInfo.timedMessagesTTL) - DefaultDropdownMenu(showMenu) { - ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) - } - } - - @Composable fun CallItem(status: CICallStatus, duration: Int) { - CICallItemView(cInfo, cItem, status, duration, acceptCall) - } - - fun mergedGroupEventText(chatItem: ChatItem): String? { - val (count, ns) = chatModel.getConnectedMemberNames(chatItem) - val members = when { - ns.size == 1 -> String.format(generalGetString(MR.strings.rcv_group_event_1_member_connected), ns[0]) - ns.size == 2 -> String.format(generalGetString(MR.strings.rcv_group_event_2_members_connected), ns[0], ns[1]) - ns.size == 3 -> String.format(generalGetString(MR.strings.rcv_group_event_3_members_connected), ns[0], ns[1], ns[2]) - ns.size > 3 -> String.format(generalGetString(MR.strings.rcv_group_event_n_members_connected), ns[0], ns[1], ns.size - 2) - else -> "" - } - return if (count <= 1) { - null - } else if (ns.isEmpty()) { - generalGetString(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 { - members - } - } - - fun eventItemViewText(): AnnotatedString { - val memberDisplayName = cItem.memberDisplayName - val t = mergedGroupEventText(cItem) - return if (!revealed.value && t != null) { - chatEventText(t, cItem.timestampText) - } else if (memberDisplayName != null) { - buildAnnotatedString { - withStyle(chatEventStyle) { append(memberDisplayName) } - append(" ") - }.plus(chatEventText(cItem)) - } else { - chatEventText(cItem) - } - } - - @Composable fun EventItemView() { - CIEventView(eventItemViewText()) - } - - @Composable - fun ModeratedItem() { - MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed) - DefaultDropdownMenu(showMenu) { - ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) - DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages) - } - } - - when (val c = cItem.content) { - is CIContent.SndMsgContent -> ContentItem() - is CIContent.RcvMsgContent -> ContentItem() - is CIContent.SndDeleted -> DeletedItem() - is CIContent.RcvDeleted -> DeletedItem() - is CIContent.SndCall -> CallItem(c.status, c.duration) - is CIContent.RcvCall -> CallItem(c.status, c.duration) - is CIContent.RcvIntegrityError -> if (developerTools) { - IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL) - } else { - Box(Modifier.size(0.dp)) {} - } - is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember) - is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito) - is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito) - is CIContent.RcvDirectEventContent -> { - EventItemView() - MsgContentItemDropdownMenu() - } - is CIContent.RcvGroupEventContent -> { - when (c.rcvGroupEvent) { - is RcvGroupEvent.MemberCreatedContact -> CIMemberCreatedContactView(cItem, openDirectChat) - else -> EventItemView() - } - MsgContentItemDropdownMenu() - } - is CIContent.SndGroupEventContent -> { - EventItemView() - MsgContentItemDropdownMenu() - } - is CIContent.RcvConnEventContent -> { - EventItemView() - MsgContentItemDropdownMenu() - } - is CIContent.SndConnEventContent -> { - EventItemView() - MsgContentItemDropdownMenu() - } - is CIContent.RcvChatFeature -> { - CIChatFeatureView(cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu) - MsgContentItemDropdownMenu() - } - is CIContent.SndChatFeature -> { - CIChatFeatureView(cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu) - MsgContentItemDropdownMenu() - } - is CIContent.RcvChatPreference -> { - val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null - CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature) - } - is CIContent.SndChatPreference -> { - CIChatFeatureView(cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon, revealed, showMenu = showMenu) - MsgContentItemDropdownMenu() - } - is CIContent.RcvGroupFeature -> { - CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor, revealed = revealed, showMenu = showMenu) - MsgContentItemDropdownMenu() - } - is CIContent.SndGroupFeature -> { - CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor, revealed = revealed, showMenu = showMenu) - MsgContentItemDropdownMenu() - } - is CIContent.RcvChatFeatureRejected -> { - CIChatFeatureView(cItem, c.feature, Color.Red, revealed = revealed, showMenu = showMenu) - MsgContentItemDropdownMenu() - } - is CIContent.RcvGroupFeatureRejected -> { - CIChatFeatureView(cItem, c.groupFeature, Color.Red, revealed = revealed, showMenu = showMenu) - MsgContentItemDropdownMenu() - } - is CIContent.SndModerated -> ModeratedItem() - is CIContent.RcvModerated -> ModeratedItem() - is CIContent.InvalidJSON -> CIInvalidJSONView(c.json) + } else { + Color.Unspecified + } + val iconTint = if (fullyVisible) { + Color.White + } else { + if (MaterialTheme.colors.isLight) { + MaterialTheme.colors.secondary.mixWith(Color.White, mixAlpha) + } else { + MaterialTheme.colors.secondary.mixWith(Color.Black, mixAlpha) } } + IconButton( + onClick, + Modifier + .padding(start = if (alignStart) 0.dp else DEFAULT_PADDING_HALF + 3.dp, end = if (alignStart) DEFAULT_PADDING_HALF + 3.dp else 0.dp) + .then(if (fullyVisible) Modifier.background(mixedBackgroundColor, CircleShape) else Modifier) + .size(22.dp), + interactionSource = buttonInteractionSource + ) { + Icon(painterResource(icon), null, Modifier.size(iconSize), tint = iconTint) + } + } + + @Composable + fun GoToItemButton(alignStart: Boolean, parentActivated: State<Boolean>) { + val chatTypeApiIdMsgId = cItem.meta.itemForwarded?.chatTypeApiIdMsgId + if (searchIsNotBlank.value) { + GoToItemInnerButton(alignStart, MR.images.ic_search, 17.dp, parentActivated) { + withBGApi { + openChat(secondaryChatsCtx = null, rhId, cInfo.chatType, cInfo.apiId, cItem.id) + closeReportsIfNeeded() + } + } + } else if (chatTypeApiIdMsgId != null) { + GoToItemInnerButton(alignStart, MR.images.ic_arrow_forward, 22.dp, parentActivated) { + val (chatType, apiId, msgId) = chatTypeApiIdMsgId + withBGApi { + openChat(secondaryChatsCtx = null, rhId, chatType, apiId, msgId) + closeReportsIfNeeded() + } + } + } + } + + Column(horizontalAlignment = if (cItem.chatDir.sent) Alignment.End else Alignment.Start) { + Row(verticalAlignment = Alignment.CenterVertically) { + val bubbleInteractionSource = remember { MutableInteractionSource() } + val bubbleHovered = bubbleInteractionSource.collectIsHoveredAsState() + if (cItem.chatDir.sent) { + GoToItemButton(true, bubbleHovered) + } + Column(Modifier.weight(1f, fill = false)) { + val enterInteraction = remember { HoverInteraction.Enter() } + LaunchedEffect(highlighted.value, hoveredItemId.value) { + if (highlighted.value || hoveredItemId.value == cItem.id) { + bubbleInteractionSource.emit(enterInteraction) + } else { + bubbleInteractionSource.emit(HoverInteraction.Exit(enterInteraction)) + } + } + Column( + Modifier + .clipChatItem(cItem, itemSeparation.largeGap, revealed.value) + .hoverable(bubbleInteractionSource) + .combinedClickable( + onLongClick = { showMenu.value = true }, + onClick = { + if (appPlatform.isAndroid && (searchIsNotBlank.value || cItem.meta.itemForwarded?.chatTypeApiIdMsgId != null)) { + hoveredItemId.value = if (hoveredItemId.value == cItem.id) null else cItem.id + } + onClick() + }, interactionSource = bubbleInteractionSource, indication = LocalIndication.current) + .onRightClick { showMenu.value = true }, + ) { + @Composable + fun framedItemView() { + FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem, scrollToQuotedItemFromItem) + } + + fun deleteMessageQuestionText(): String { + return if (!sent || fullDeleteAllowed || cInfo is ChatInfo.Local) { + generalGetString(MR.strings.delete_message_cannot_be_undone_warning) + } else { + generalGetString(MR.strings.delete_message_mark_deleted_warning) + } + } + + @Composable + fun MsgReactionsMenu() { + val rs = MsgReaction.supported.mapNotNull { r -> + if (null == cItem.reactions.find { it.userReacted && it.reaction.text == r.text }) { + r + } else { + null + } + } + if (rs.isNotEmpty()) { + Row(modifier = Modifier.padding(horizontal = DEFAULT_PADDING).horizontalScroll(rememberScrollState()), verticalAlignment = Alignment.CenterVertically) { + rs.forEach() { r -> + Box( + Modifier.size(36.dp).clip(CircleShape).clickable { + setReaction(cInfo, cItem, true, r) + showMenu.value = false + }, + contentAlignment = Alignment.Center + ) { + ReactionIcon(r.text, 12.sp) + } + } + } + } + } + + @Composable + fun DeleteItemMenu() { + DefaultDropdownMenu(showMenu) { + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } + } + } + + @Composable + fun MsgContentItemDropdownMenu() { + val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file) + when { + // cItem.id check is a special case for live message chat item which has negative ID while not sent yet + cItem.isReport && cItem.meta.itemDeleted == null && cInfo is ChatInfo.Group -> { + DefaultDropdownMenu(showMenu) { + if (cItem.chatDir !is CIDirection.GroupSnd && cInfo.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { + ArchiveReportItemAction(cItem.id, cInfo.groupInfo.membership.memberActive, showMenu, archiveReports) + } + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages, buttonText = stringResource(MR.strings.delete_report)) + Divider() + SelectItemAction(showMenu, selectChatItem) + } + } + cItem.content.msgContent != null && cItem.id >= 0 && !cItem.isReport -> { + DefaultDropdownMenu(showMenu) { + if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) { + MsgReactionsMenu() + } + if (cItem.meta.itemDeleted == null && !live && !cItem.localNote) { + 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) + } else { + composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem)) + } + showMenu.value = false + }) + } + val clipboard = LocalClipboardManager.current + val cachedRemoteReqs = remember { CIFile.cachedRemoteFileRequests } + val copyAndShareAllowed = when { + cItem.content.text.isNotEmpty() -> true + cItem.file?.forwardingAllowed() == true -> true + else -> false + } + + if (copyAndShareAllowed) { + ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = { + var fileSource = getLoadedFileSource(cItem.file) + val shareIfExists = { + when (val f = fileSource) { + null -> clipboard.shareText(cItem.content.text) + else -> shareFile(cItem.text, f) + } + showMenu.value = false + } + if (chatModel.connectedToRemote() && fileSource == null) { + withLongRunningApi(slow = 600_000) { + cItem.file?.loadRemoteFile(true) + fileSource = getLoadedFileSource(cItem.file) + shareIfExists() + } + } else shareIfExists() + }) + } + if (copyAndShareAllowed) { + ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = { + copyItemToClipboard(cItem, clipboard) + showMenu.value = false + }) + } + if (cItem.file != null && (getLoadedFilePath(cItem.file) != null || (chatModel.connectedToRemote() && cachedRemoteReqs[cItem.file.fileSource] != false && cItem.file.loaded))) { + SaveContentItemAction(cItem, saveFileLauncher, showMenu) + } else if (cItem.file != null && cItem.file.fileStatus is CIFileStatus.RcvInvitation && fileSizeValid(cItem.file)) { + ItemAction(stringResource(MR.strings.download_file), painterResource(MR.images.ic_arrow_downward), onClick = { + withBGApi { + Log.d(TAG, "ChatItemView downloadFileAction") + val user = chatModel.currentUser.value + if (user != null) { + controller.receiveFile(rhId, user, cItem.file.fileId) + } + } + showMenu.value = false + }) + } + if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) { + ItemAction(stringResource(MR.strings.edit_verb), painterResource(MR.images.ic_edit_filled), onClick = { + composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews) + showMenu.value = false + }) + } + if (cItem.meta.itemDeleted == null && + (cItem.file == null || cItem.file.forwardingAllowed()) && + !cItem.isLiveDummy && !live + ) { + ItemAction(stringResource(MR.strings.forward_chat_item), painterResource(MR.images.ic_forward), onClick = { + forwardItem(cInfo, cItem) + showMenu.value = false + }) + } + ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) + if (revealed.value) { + HideItemAction(revealed, showMenu, reveal) + } + if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null && !cItem.localNote) { + CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction) + } + if (!(live && cItem.meta.isLive) && !preview) { + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + } + if (cItem.chatDir !is CIDirection.GroupSnd) { + val groupInfo = cItem.memberToModerate(cInfo)?.first + if (groupInfo != null) { + ModerateItemAction(cItem, questionText = moderateMessageQuestionText(cInfo.featureEnabled(ChatFeature.FullDelete), 1), showMenu, deleteMessage) + } else if (cItem.meta.itemDeleted == null && cInfo is ChatInfo.Group && cInfo.groupInfo.groupFeatureEnabled(GroupFeature.Reports) && cInfo.groupInfo.membership.memberRole == GroupMemberRole.Member && !live) { + ReportItemAction(cItem, composeState, showMenu) + } + } + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } + } + } + cItem.meta.itemDeleted != null -> { + DefaultDropdownMenu(showMenu) { + if (revealed.value) { + HideItemAction(revealed, showMenu, reveal) + } else if (!cItem.isDeletedContent) { + RevealItemAction(revealed, showMenu, reveal) + } else if (range.value != null) { + ExpandItemAction(revealed, showMenu, reveal) + } + ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } + } + } + cItem.isDeletedContent -> { + DefaultDropdownMenu(showMenu) { + ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } + } + } + cItem.mergeCategory != null && ((range.value?.count() ?: 0) > 1 || revealed.value) -> { + DefaultDropdownMenu(showMenu) { + if (revealed.value) { + ShrinkItemAction(revealed, showMenu, reveal) + } else { + ExpandItemAction(revealed, showMenu, reveal) + } + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } + } + } + else -> { + DefaultDropdownMenu(showMenu) { + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (selectedChatItems.value == null) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } + } + } + } + } + + @Composable + fun MarkedDeletedItemDropdownMenu() { + DefaultDropdownMenu(showMenu) { + if (!cItem.isDeletedContent) { + RevealItemAction(revealed, showMenu, reveal) + } + ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } + } + } + + @Composable + fun ContentItem() { + val mc = cItem.content.msgContent + if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) { + MarkedDeletedItemView(chatsCtx, cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + MarkedDeletedItemDropdownMenu() + } else { + if (cItem.quotedItem == null && cItem.meta.itemForwarded == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) { + if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) { + EmojiItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + } else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) { + CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp, longClick = { onLinkLongClick("") }, receiveFile = receiveFile) + } else { + framedItemView() + } + } else { + framedItemView() + } + MsgContentItemDropdownMenu() + } + } + + @Composable fun LegacyDeletedItem() { + DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + DefaultDropdownMenu(showMenu) { + ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } + } + } + + @Composable fun CallItem(status: CICallStatus, duration: Int) { + CICallItemView(cInfo, cItem, status, duration, showTimestamp = showTimestamp, acceptCall, cInfo.timedMessagesTTL) + DeleteItemMenu() + } + + fun mergedGroupEventText(chatItem: ChatItem, reversedChatItems: List<ChatItem>): String? { + val (count, ns) = chatModel.getConnectedMemberNames(chatItem, reversedChatItems) + val members = when { + ns.size == 1 -> String.format(generalGetString(MR.strings.rcv_group_event_1_member_connected), ns[0]) + ns.size == 2 -> String.format(generalGetString(MR.strings.rcv_group_event_2_members_connected), ns[0], ns[1]) + ns.size == 3 -> String.format(generalGetString(MR.strings.rcv_group_event_3_members_connected), ns[0], ns[1], ns[2]) + ns.size > 3 -> String.format(generalGetString(MR.strings.rcv_group_event_n_members_connected), ns[0], ns[1], ns.size - 2) + else -> "" + } + return if (count <= 1) { + null + } else if (ns.isEmpty()) { + generalGetString(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 { + members + } + } + + fun eventItemViewText(reversedChatItems: List<ChatItem>): AnnotatedString { + val memberDisplayName = cItem.memberDisplayName + val t = mergedGroupEventText(cItem, reversedChatItems) + return if (!revealed.value && t != null) { + chatEventText(t, cItem.timestampText) + } else if (memberDisplayName != null) { + buildAnnotatedString { + withStyle(chatEventStyle) { append(memberDisplayName) } + append(" ") + }.plus(chatEventText(cItem)) + } else { + chatEventText(cItem) + } + } + + @Composable fun EventItemView() { + val reversedChatItems = chatsCtx.chatItems.value.asReversed() + CIEventView(eventItemViewText(reversedChatItems)) + } + + @Composable + fun DeletedItem() { + MarkedDeletedItemView(chatsCtx, cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + DefaultDropdownMenu(showMenu) { + if (revealed.value) { + HideItemAction(revealed, showMenu, reveal) + } else if (!cItem.isDeletedContent) { + RevealItemAction(revealed, showMenu, reveal) + } else if (range.value != null) { + ExpandItemAction(revealed, showMenu, reveal) + } + ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) + DeleteItemAction(chatsCtx, cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } + } + } + + @Composable + fun E2EEInfoNoPQText() { + Text( + buildAnnotatedString { + withStyle(chatEventStyle) { append(annotatedStringResource(MR.strings.e2ee_info_no_pq)) } + }, + Modifier.padding(horizontal = 6.dp, vertical = 6.dp) + ) + } + + @Composable + fun DirectE2EEInfoText(e2EEInfo: E2EEInfo) { + if (e2EEInfo.pqEnabled) { + Text( + buildAnnotatedString { + withStyle(chatEventStyle) { append(annotatedStringResource(MR.strings.e2ee_info_pq)) } + }, + Modifier.padding(horizontal = 6.dp, vertical = 6.dp) + ) + } else { + E2EEInfoNoPQText() + } + } + + when (val c = cItem.content) { + is CIContent.SndMsgContent -> ContentItem() + is CIContent.RcvMsgContent -> ContentItem() + is CIContent.SndDeleted -> LegacyDeletedItem() + is CIContent.RcvDeleted -> LegacyDeletedItem() + is CIContent.SndCall -> CallItem(c.status, c.duration) + is CIContent.RcvCall -> CallItem(c.status, c.duration) + is CIContent.RcvIntegrityError -> if (developerTools) { + IntegrityErrorItemView(c.msgError, cItem, showTimestamp, cInfo.timedMessagesTTL) + DeleteItemMenu() + } else { + Box(Modifier.size(0.dp)) {} + } + is CIContent.RcvDecryptionError -> { + CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember) + DeleteItemMenu() + } + is CIContent.RcvGroupInvitation -> { + CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, showTimestamp = showTimestamp, timedMessagesTTL = cInfo.timedMessagesTTL) + DeleteItemMenu() + } + is CIContent.SndGroupInvitation -> { + CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, showTimestamp = showTimestamp, timedMessagesTTL = cInfo.timedMessagesTTL) + DeleteItemMenu() + } + is CIContent.RcvDirectEventContent -> { + EventItemView() + MsgContentItemDropdownMenu() + } + is CIContent.RcvGroupEventContent -> { + when (c.rcvGroupEvent) { + is RcvGroupEvent.MemberCreatedContact -> CIMemberCreatedContactView(cItem, openDirectChat) + else -> EventItemView() + } + MsgContentItemDropdownMenu() + } + is CIContent.SndGroupEventContent -> { + EventItemView() + MsgContentItemDropdownMenu() + } + is CIContent.RcvConnEventContent -> { + EventItemView() + MsgContentItemDropdownMenu() + } + is CIContent.SndConnEventContent -> { + EventItemView() + MsgContentItemDropdownMenu() + } + is CIContent.RcvChatFeature -> { + CIChatFeatureView(chatsCtx, cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu) + MsgContentItemDropdownMenu() + } + is CIContent.SndChatFeature -> { + CIChatFeatureView(chatsCtx, cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu) + MsgContentItemDropdownMenu() + } + is CIContent.RcvChatPreference -> { + val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null + CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature) + DeleteItemMenu() + } + is CIContent.SndChatPreference -> { + CIChatFeatureView(chatsCtx, cInfo, cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon, revealed, showMenu = showMenu) + MsgContentItemDropdownMenu() + } + is CIContent.RcvGroupFeature -> { + CIChatFeatureView(chatsCtx, cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, revealed = revealed, showMenu = showMenu) + MsgContentItemDropdownMenu() + } + is CIContent.SndGroupFeature -> { + CIChatFeatureView(chatsCtx, cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, revealed = revealed, showMenu = showMenu) + MsgContentItemDropdownMenu() + } + is CIContent.RcvChatFeatureRejected -> { + CIChatFeatureView(chatsCtx, cInfo, cItem, c.feature, Color.Red, revealed = revealed, showMenu = showMenu) + MsgContentItemDropdownMenu() + } + is CIContent.RcvGroupFeatureRejected -> { + CIChatFeatureView(chatsCtx, cInfo, cItem, c.groupFeature, Color.Red, revealed = revealed, showMenu = showMenu) + MsgContentItemDropdownMenu() + } + is CIContent.SndModerated -> DeletedItem() + is CIContent.RcvModerated -> DeletedItem() + 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.InvalidJSON -> { + CIInvalidJSONView(c.json) + DeleteItemMenu() + } + } + } + } + if (!cItem.chatDir.sent) { + GoToItemButton(false, bubbleHovered) + } + } if (cItem.content.msgContent != null && (cItem.meta.itemDeleted == null || revealed.value) && cItem.reactions.isNotEmpty()) { ChatItemReactions() } @@ -506,30 +829,38 @@ fun ItemInfoAction( @Composable fun DeleteItemAction( + chatsCtx: ChatModel.ChatsContext, cItem: ChatItem, - revealed: MutableState<Boolean>, + revealed: State<Boolean>, showMenu: MutableState<Boolean>, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List<Long>) -> Unit, + buttonText: String = stringResource(MR.strings.delete_verb), ) { ItemAction( - stringResource(MR.strings.delete_verb), + buttonText, painterResource(MR.images.ic_delete), onClick = { showMenu.value = false - if (!revealed.value && cItem.meta.itemDeleted != null) { - val currIndex = chatModel.getChatItemIndexOrNull(cItem) + if (!revealed.value) { + val reversedChatItems = chatsCtx.chatItems.value.asReversed() + val currIndex = chatModel.getChatItemIndexOrNull(cItem, reversedChatItems) val ciCategory = cItem.mergeCategory if (currIndex != null && ciCategory != null) { - val (prevHidden, _) = chatModel.getPrevShownChatItem(currIndex, ciCategory) + val (prevHidden, _) = chatModel.getPrevShownChatItem(currIndex, ciCategory, reversedChatItems) val range = chatViewItemsRange(currIndex, prevHidden) if (range != null) { val itemIds: ArrayList<Long> = arrayListOf() for (i in range) { - itemIds.add(chatModel.chatItems.asReversed()[i].id) + itemIds.add(reversedChatItems[i].id) } - deleteMessagesAlertDialog(itemIds, generalGetString(MR.strings.delete_message_mark_deleted_warning), deleteMessages = deleteMessages) + deleteMessagesAlertDialog( + itemIds, + generalGetString(MR.strings.delete_messages_cannot_be_undone_warning), + forAll = false, + deleteMessages = { ids, _ -> deleteMessages(ids) } + ) } else { deleteMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage) } @@ -563,57 +894,169 @@ fun ModerateItemAction( } @Composable -private fun RevealItemAction(revealed: MutableState<Boolean>, showMenu: MutableState<Boolean>) { +fun SelectItemAction( + showMenu: MutableState<Boolean>, + selectItem: () -> Unit, +) { + ItemAction( + stringResource(MR.strings.select_verb), + painterResource(MR.images.ic_check_circle), + onClick = { + showMenu.value = false + selectItem() + } + ) +} + +@Composable +private fun RevealItemAction(revealed: State<Boolean>, showMenu: MutableState<Boolean>, reveal: (Boolean) -> Unit) { ItemAction( stringResource(MR.strings.reveal_verb), painterResource(MR.images.ic_visibility), onClick = { - revealed.value = true + reveal(true) showMenu.value = false } ) } @Composable -private fun HideItemAction(revealed: MutableState<Boolean>, showMenu: MutableState<Boolean>) { +private fun HideItemAction(revealed: State<Boolean>, showMenu: MutableState<Boolean>, reveal: (Boolean) -> Unit) { ItemAction( stringResource(MR.strings.hide_verb), painterResource(MR.images.ic_visibility_off), onClick = { - revealed.value = false + reveal(false) showMenu.value = false } ) } @Composable -private fun ExpandItemAction(revealed: MutableState<Boolean>, showMenu: MutableState<Boolean>) { +private fun ExpandItemAction(revealed: State<Boolean>, showMenu: MutableState<Boolean>, reveal: (Boolean) -> Unit) { ItemAction( stringResource(MR.strings.expand_verb), painterResource(MR.images.ic_expand_all), onClick = { - revealed.value = true + reveal(true) showMenu.value = false }, ) } @Composable -private fun ShrinkItemAction(revealed: MutableState<Boolean>, showMenu: MutableState<Boolean>) { +private fun ShrinkItemAction(revealed: State<Boolean>, showMenu: MutableState<Boolean>, reveal: (Boolean) -> Unit) { ItemAction( stringResource(MR.strings.hide_verb), painterResource(MR.images.ic_collapse_all), onClick = { - revealed.value = false + reveal(false) showMenu.value = false }, ) } +@Composable +private fun ReportItemAction( + cItem: ChatItem, + composeState: MutableState<ComposeState>, + showMenu: MutableState<Boolean>, +) { + ItemAction( + stringResource(MR.strings.report_verb), + painterResource(MR.images.ic_flag), + onClick = { + AlertManager.shared.showAlertDialogButtons( + title = generalGetString(MR.strings.report_reason_alert_title), + buttons = { + ReportReason.supportedReasons.forEach { reason -> + SectionItemView({ + if (composeState.value.editing) { + composeState.value = ComposeState( + contextItem = ComposeContextItem.ReportedItem(cItem, reason), + useLinkPreviews = false, + preview = ComposePreview.NoPreview, + ) + } else { + composeState.value = composeState.value.copy( + contextItem = ComposeContextItem.ReportedItem(cItem, reason), + useLinkPreviews = false, + preview = ComposePreview.NoPreview, + ) + } + AlertManager.shared.hideAlert() + }) { + Text(reason.text, Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + ) + showMenu.value = false + }, + color = Color.Red + ) +} + +@Composable +private fun ArchiveReportItemAction(id: Long, allowForAll: Boolean, showMenu: MutableState<Boolean>, archiveReports: (List<Long>, Boolean) -> Unit) { + ItemAction( + stringResource(MR.strings.archive_report), + painterResource(MR.images.ic_inventory_2), + onClick = { + showArchiveReportsAlert(listOf(id), allowForAll, archiveReports) + showMenu.value = false + } + ) +} + +fun showArchiveReportsAlert(ids: List<Long>, allowForAll: Boolean, archiveReports: (List<Long>, Boolean) -> Unit) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = if (ids.size == 1) { + generalGetString(MR.strings.report_archive_alert_title) + } else { + generalGetString(MR.strings.report_archive_alert_title_nth).format(ids.size) + }, + text = null, + buttons = { + // Archive for me + SectionItemView({ + AlertManager.shared.hideAlert() + archiveReports(ids, false) + }) { + Text( + generalGetString(MR.strings.report_archive_for_me), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.error + ) + } + if (allowForAll) { + // Archive for all moderators + SectionItemView({ + AlertManager.shared.hideAlert() + archiveReports(ids, true) + }) { + Text( + stringResource(MR.strings.report_archive_for_all_moderators), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.error + ) + } + } + } + ) +} + @Composable fun ItemAction(text: String, icon: Painter, color: Color = Color.Unspecified, onClick: () -> Unit) { val finalColor = if (color == Color.Unspecified) { - if (isInDarkTheme()) MenuTextColorDark else Color.Black + MenuTextColor } else color DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) { Row(verticalAlignment = Alignment.CenterVertically) { @@ -630,10 +1073,64 @@ fun ItemAction(text: String, icon: Painter, color: Color = Color.Unspecified, on } } +@Composable +fun ItemAction(text: String, icon: ImageBitmap, textColor: Color = Color.Unspecified, iconColor: Color = Color.Unspecified, onClick: () -> Unit) { + val finalColor = if (textColor == Color.Unspecified) { + MenuTextColor + } else textColor + DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text, + modifier = Modifier + .fillMaxWidth() + .weight(1F) + .padding(end = 15.dp), + color = finalColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + if (iconColor == Color.Unspecified) { + Image(icon, text, Modifier.size(22.dp)) + } else { + Icon(icon, text, Modifier.size(22.dp), tint = iconColor) + } + } + } +} + +@Composable +fun ItemAction( + text: String, + composable: @Composable () -> Unit, + color: Color = Color.Unspecified, + onClick: () -> Unit, + lineLimit: Int = Int.MAX_VALUE +) { + val finalColor = if (color == Color.Unspecified) { + MenuTextColor + } else color + DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text, + modifier = Modifier + .fillMaxWidth() + .weight(1F) + .padding(end = 15.dp), + color = finalColor, + maxLines = lineLimit, + overflow = TextOverflow.Ellipsis + ) + composable() + } + } +} + @Composable fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = Color.Unspecified) { val finalColor = if (color == Color.Unspecified) { - if (isInDarkTheme()) MenuTextColorDark else Color.Black + MenuTextColor } else color DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) { Row(verticalAlignment = Alignment.CenterVertically) { @@ -650,6 +1147,177 @@ fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Colo } } +@Composable +fun ItemAction(text: String, color: Color = Color.Unspecified, onClick: () -> Unit) { + val finalColor = if (color == Color.Unspecified) { + MenuTextColor + } else color + DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) { + Text( + text, + modifier = Modifier + .fillMaxWidth() + .weight(1F) + .padding(end = 15.dp), + color = finalColor + ) + } +} + +@Composable +fun Modifier.chatItemOffset(cItem: ChatItem, tailVisible: Boolean, inverted: Boolean = false, revealed: Boolean): Modifier { + val chatItemTail = remember { appPreferences.chatItemTail.state } + val style = shapeStyle(cItem, chatItemTail.value, tailVisible, revealed) + + val offset = if (style is ShapeStyle.Bubble) { + if (style.tailVisible) { + if (cItem.chatDir.sent) msgTailWidthDp else -msgTailWidthDp + } else { + 0.dp + } + } else 0.dp + + return this.offset(x = if (inverted) (-1f * offset) else offset) +} + +@Composable +fun Modifier.clipChatItem(chatItem: ChatItem? = null, tailVisible: Boolean = false, revealed: Boolean = false): Modifier { + val chatItemRoundness = remember { appPreferences.chatItemRoundness.state } + val chatItemTail = remember { appPreferences.chatItemTail.state } + val style = shapeStyle(chatItem, chatItemTail.value, tailVisible, revealed) + val cornerRoundness = chatItemRoundness.value.coerceIn(0f, 1f) + + val shape = when (style) { + is ShapeStyle.Bubble -> chatItemShape(cornerRoundness, LocalDensity.current, style.tailVisible, chatItem?.chatDir?.sent == true) + is ShapeStyle.RoundRect -> RoundedCornerShape(style.radius * cornerRoundness) + } + + return this.clip(shape) +} + +private fun chatItemShape(roundness: Float, density: Density, tailVisible: Boolean, sent: Boolean = false): GenericShape = GenericShape { size, _ -> + val (msgTailWidth, msgBubbleMaxRadius) = with(density) { Pair(msgTailWidthDp.toPx(), msgBubbleMaxRadius.toPx()) } + val width = size.width + val height = size.height + val rxMax = min(msgBubbleMaxRadius, width / 2) + val ryMax = min(msgBubbleMaxRadius, height / 2) + val rx = roundness * rxMax + val ry = roundness * ryMax + val tailHeight = with(density) { + min( + msgTailMinHeightDp.toPx() + roundness * (msgTailMaxHeightDp.toPx() - msgTailMinHeightDp.toPx()), + height / 2 + ) + } + moveTo(rx, 0f) + lineTo(width - rx, 0f) // Top Line + if (roundness > 0) { + quadraticBezierTo(width, 0f, width, ry) // Top-right corner + } + if (height > 2 * ry) { + lineTo(width, height - ry) // Right side + } + if (roundness > 0) { + quadraticBezierTo(width, height, width - rx, height) // Bottom-right corner + } + if (tailVisible) { + lineTo(0f, height) // Bottom line + if (roundness > 0) { + val d = tailHeight - msgTailWidth * msgTailWidth / tailHeight + val controlPoint = Offset(msgTailWidth, height - tailHeight + d * sqrt(roundness)) + quadraticBezierTo(controlPoint.x, controlPoint.y, msgTailWidth, height - tailHeight) + } else { + lineTo(msgTailWidth, height - tailHeight) + } + + if (height > ry + tailHeight) { + lineTo(msgTailWidth, ry) + } + } else { + lineTo(rx, height) // Bottom line + if (roundness > 0) { + quadraticBezierTo(0f, height, 0f, height - ry) // Bottom-left corner + } + if (height > 2 * ry) { + lineTo(0f, ry) // Left side + } + } + if (roundness > 0) { + val bubbleInitialX = if (tailVisible) msgTailWidth else 0f + quadraticBezierTo(bubbleInitialX, 0f, bubbleInitialX + rx, 0f) // Top-left corner + } + + if (sent) { + val matrix = Matrix() + matrix.scale(-1f, 1f) + this.transform(matrix) + this.translate(Offset(size.width, 0f)) + } +} + +sealed class ShapeStyle { + data class Bubble(val tailVisible: Boolean, val startPadding: Boolean) : ShapeStyle() + data class RoundRect(val radius: Dp) : ShapeStyle() +} + +fun shapeStyle(chatItem: ChatItem? = null, tailEnabled: Boolean, tailVisible: Boolean, revealed: Boolean): ShapeStyle { + if (chatItem == null) { + return ShapeStyle.RoundRect(msgRectMaxRadius) + } + + when (chatItem.content) { + is CIContent.SndMsgContent, + is CIContent.RcvMsgContent, + is CIContent.RcvDecryptionError, + is CIContent.SndDeleted, + is CIContent.RcvDeleted, + is CIContent.RcvIntegrityError, + is CIContent.SndModerated, + is CIContent.RcvModerated, + is CIContent.RcvBlocked, + is CIContent.InvalidJSON -> { + if (chatItem.meta.itemDeleted != null && (!revealed || chatItem.isDeletedContent)) { + return ShapeStyle.RoundRect(msgRectMaxRadius) + } + + val tail = when (val content = chatItem.content.msgContent) { + is MsgContent.MCImage, + is MsgContent.MCVideo, + is MsgContent.MCVoice -> { + if (content.text.isEmpty()) { + false + } else { + tailVisible + } + } + is MsgContent.MCText -> { + if (isShortEmoji(content.text)) { + false + } else { + tailVisible + } + } + else -> tailVisible + } + return if (tailEnabled) { + ShapeStyle.Bubble(tail, !chatItem.chatDir.sent) + } else { + ShapeStyle.RoundRect(msgRectMaxRadius) + } + } + + is CIContent.RcvGroupInvitation, + is CIContent.SndGroupInvitation -> return ShapeStyle.RoundRect(msgRectMaxRadius) + else -> return ShapeStyle.RoundRect(8.dp) + } +} + +private fun closeReportsIfNeeded() { + if (appPlatform.isAndroid && ModalManager.end.isLastModalOpen(ModalViewId.SECONDARY_CHAT)) { + ModalManager.end.closeModals() + } +} + fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) { AlertManager.shared.showAlertDialog( title = generalGetString(cancelAction.alert.titleId), @@ -677,7 +1345,7 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMes deleteMessage(chatItem.id, CIDeleteMode.cidmInternal) AlertManager.shared.hideAlert() }) { Text(stringResource(MR.strings.for_me_only), color = MaterialTheme.colors.error) } - if (chatItem.meta.editable) { + if (chatItem.meta.deletable && !chatItem.localNote && !chatItem.isReport) { Spacer(Modifier.padding(horizontal = 4.dp)) TextButton(onClick = { deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast) @@ -689,7 +1357,7 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMes ) } -fun deleteMessagesAlertDialog(itemIds: List<Long>, questionText: String, deleteMessages: (List<Long>) -> Unit) { +fun deleteMessagesAlertDialog(itemIds: List<Long>, questionText: String, forAll: Boolean, deleteMessages: (List<Long>, Boolean) -> Unit) { AlertManager.shared.showAlertDialogButtons( title = generalGetString(MR.strings.delete_messages__question).format(itemIds.size), text = questionText, @@ -701,14 +1369,29 @@ fun deleteMessagesAlertDialog(itemIds: List<Long>, questionText: String, deleteM horizontalArrangement = Arrangement.Center, ) { TextButton(onClick = { - deleteMessages(itemIds) + deleteMessages(itemIds, false) AlertManager.shared.hideAlert() }) { Text(stringResource(MR.strings.for_me_only), color = MaterialTheme.colors.error) } + + if (forAll) { + TextButton(onClick = { + deleteMessages(itemIds, true) + AlertManager.shared.hideAlert() + }) { Text(stringResource(MR.strings.for_everybody), color = MaterialTheme.colors.error) } + } } } ) } +fun moderateMessageQuestionText(fullDeleteAllowed: Boolean, count: Int): String { + return if (fullDeleteAllowed) { + generalGetString(if (count == 1) MR.strings.moderate_message_will_be_deleted_warning else MR.strings.moderate_messages_will_be_deleted_warning) + } else { + generalGetString(if (count == 1) MR.strings.moderate_message_will_be_marked_warning else MR.strings.moderate_messages_will_be_marked_warning) + } +} + fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.delete_member_message__question), @@ -721,10 +1404,13 @@ fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteM ) } -private fun showMsgDeliveryErrorAlert(description: String) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.message_delivery_error_title), - text = description, +fun moderateMessagesAlertDialog(itemIds: List<Long>, questionText: String, deleteMessages: (List<Long>) -> Unit) { + AlertManager.shared.showAlertDialog( + title = if (itemIds.size == 1) generalGetString(MR.strings.delete_member_message__question) else generalGetString(MR.strings.delete_members_messages__question).format(itemIds.size), + text = questionText, + confirmText = generalGetString(MR.strings.delete_verb), + destructive = true, + onConfirm = { deleteMessages(itemIds) } ) } @@ -732,38 +1418,53 @@ expect fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) @Preview @Composable -fun PreviewChatItemView() { - SimpleXTheme { - ChatItemView( - ChatInfo.Direct.sampleData, - ChatItem.getSampleData( - 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" - ), - useLinkPreviews = true, - linkMode = SimplexLinkMode.DESCRIPTION, - composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, - revealed = remember { mutableStateOf(false) }, - range = 0..1, - deleteMessage = { _, _ -> }, - deleteMessages = { _ -> }, - receiveFile = { _, _ -> }, - cancelFile = {}, - joinGroup = { _, _ -> }, - acceptCall = { _ -> }, - scrollToItem = {}, - acceptFeature = { _, _, _ -> }, - openDirectChat = { _ -> }, - updateContactStats = { }, - updateMemberStats = { _, _ -> }, - syncContactConnection = { }, - syncMemberConnection = { _, _ -> }, - findModelChat = { null }, - findModelMember = { null }, - setReaction = { _, _, _, _ -> }, - showItemDetails = { _, _ -> }, - developerTools = false, - ) - } +fun PreviewChatItemView( + chatItem: ChatItem = ChatItem.getSampleData(1, CIDirection.DirectSnd(), Clock.System.now(), "hello") +) { + ChatItemView( + chatsCtx = ChatModel.ChatsContext(contentTag = null), + rhId = null, + ChatInfo.Direct.sampleData, + chatItem, + useLinkPreviews = true, + linkMode = SimplexLinkMode.DESCRIPTION, + composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, + revealed = remember { mutableStateOf(false) }, + highlighted = remember { mutableStateOf(false) }, + hoveredItemId = remember { mutableStateOf(null) }, + range = remember { mutableStateOf(0..1) }, + selectedChatItems = remember { mutableStateOf(setOf()) }, + searchIsNotBlank = remember { mutableStateOf(false) }, + selectChatItem = {}, + deleteMessage = { _, _ -> }, + deleteMessages = { _ -> }, + archiveReports = { _, _ -> }, + receiveFile = { _ -> }, + cancelFile = {}, + joinGroup = { _, _ -> }, + acceptCall = { _ -> }, + scrollToItem = {}, + scrollToQuotedItemFromItem = {}, + acceptFeature = { _, _, _ -> }, + openDirectChat = { _ -> }, + forwardItem = { _, _ -> }, + updateContactStats = { }, + updateMemberStats = { _, _ -> }, + syncContactConnection = { }, + syncMemberConnection = { _, _ -> }, + findModelChat = { null }, + findModelMember = { null }, + setReaction = { _, _, _, _ -> }, + showItemDetails = { _, _ -> }, + reveal = {}, + showMemberInfo = { _, _ ->}, + showChatInfo = {}, + developerTools = false, + showViaProxy = false, + showTimestamp = true, + preview = true, + itemSeparation = ItemSeparation(timestamp = true, largeGap = true, null) + ) } @Preview @@ -771,22 +1472,32 @@ fun PreviewChatItemView() { fun PreviewChatItemViewDeletedContent() { SimpleXTheme { ChatItemView( + chatsCtx = ChatModel.ChatsContext(contentTag = null), + rhId = null, ChatInfo.Direct.sampleData, ChatItem.getDeletedContentSampleData(), useLinkPreviews = true, linkMode = SimplexLinkMode.DESCRIPTION, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, revealed = remember { mutableStateOf(false) }, - range = 0..1, + highlighted = remember { mutableStateOf(false) }, + hoveredItemId = remember { mutableStateOf(null) }, + range = remember { mutableStateOf(0..1) }, + selectedChatItems = remember { mutableStateOf(setOf()) }, + searchIsNotBlank = remember { mutableStateOf(false) }, + selectChatItem = {}, deleteMessage = { _, _ -> }, deleteMessages = { _ -> }, - receiveFile = { _, _ -> }, + archiveReports = { _, _ -> }, + receiveFile = { _ -> }, cancelFile = {}, joinGroup = { _, _ -> }, acceptCall = { _ -> }, scrollToItem = {}, + scrollToQuotedItemFromItem = {}, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, + forwardItem = { _, _ -> }, updateContactStats = { }, updateMemberStats = { _, _ -> }, syncContactConnection = { }, @@ -795,7 +1506,14 @@ fun PreviewChatItemViewDeletedContent() { findModelMember = { null }, setReaction = { _, _, _, _ -> }, showItemDetails = { _, _ -> }, + reveal = {}, + showMemberInfo = { _, _ ->}, + showChatInfo = {}, developerTools = false, + showViaProxy = false, + preview = true, + showTimestamp = true, + itemSeparation = ItemSeparation(timestamp = true, largeGap = true, null) ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt index 2d949e1737..17245c4e75 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt @@ -16,13 +16,14 @@ import chat.simplex.common.model.ChatItem import chat.simplex.common.ui.theme.* @Composable -fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) { +fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean, showTimestamp: Boolean) { val sent = ci.chatDir.sent - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( shape = RoundedCornerShape(18.dp), color = if (sent) sentColor else receivedColor, + contentColor = LocalContentColor.current ) { Row( Modifier.padding(horizontal = 12.dp, vertical = 6.dp), @@ -35,7 +36,7 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) { style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), modifier = Modifier.padding(end = 8.dp) ) - CIMetaView(ci, timedMessagesTTL) + CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } } @@ -49,7 +50,9 @@ fun PreviewDeletedItemView() { SimpleXTheme { DeletedItemView( ChatItem.getDeletedContentSampleData(), - null + null, + showViaProxy = false, + showTimestamp = true ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt index 3ede737ffa..7aca0466f9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt @@ -12,18 +12,19 @@ import androidx.compose.ui.unit.sp import chat.simplex.common.model.ChatItem import chat.simplex.common.model.MREmojiChar import chat.simplex.common.ui.theme.EmojiFont +import java.sql.Timestamp val largeEmojiFont: TextStyle = TextStyle(fontSize = 48.sp, fontFamily = EmojiFont) val mediumEmojiFont: TextStyle = TextStyle(fontSize = 36.sp, fontFamily = EmojiFont) @Composable -fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?) { +fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean, showTimestamp: Boolean) { Column( Modifier.padding(vertical = 8.dp, horizontal = 12.dp), horizontalAlignment = Alignment.CenterHorizontally ) { EmojiText(chatItem.content.text) - CIMetaView(chatItem, timedMessagesTTL) + CIMetaView(chatItem, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } 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 c391200c2d..fd8a32af64 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 @@ -2,16 +2,15 @@ package chat.simplex.common.views.chat.item import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.* +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.UriHandler import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -23,9 +22,8 @@ import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.chat.MEMBER_IMAGE_SIZE import chat.simplex.res.MR -import kotlin.math.min +import kotlin.math.ceil @Composable fun FramedItemView( @@ -34,10 +32,14 @@ fun FramedItemView( uriHandler: UriHandler? = null, imageProvider: (() -> ImageGalleryProvider)? = null, linkMode: SimplexLinkMode, + showViaProxy: Boolean, showMenu: MutableState<Boolean>, - receiveFile: (Long, Boolean) -> Unit, + showTimestamp: Boolean, + tailVisible: Boolean = false, + receiveFile: (Long) -> Unit, onLinkLongClick: (link: String) -> Unit = {}, scrollToItem: (Long) -> Unit = {}, + scrollToQuotedItemFromItem: (Long) -> Unit = {}, ) { val sent = ci.chatDir.sent val chatTTL = chatInfo.timedMessagesTTL @@ -47,25 +49,28 @@ fun FramedItemView( } @Composable - fun Color.toQuote(): Color = if (isInDarkTheme()) lighter(0.12f) else darker(0.12f) - - @Composable - fun ciQuotedMsgTextView(qi: CIQuote, lines: Int) { + fun ciQuotedMsgTextView(qi: CIQuote, lines: Int, showTimestamp: Boolean) { MarkdownText( qi.text, qi.formattedText, + toggleSecrets = true, maxLines = lines, overflow = TextOverflow.Ellipsis, style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface), linkMode = linkMode, - uriHandler = if (appPlatform.isDesktop) uriHandler else null + uriHandler = if (appPlatform.isDesktop) uriHandler else null, + showTimestamp = showTimestamp, ) } @Composable fun ciQuotedMsgView(qi: CIQuote) { Box( - Modifier.padding(vertical = 6.dp, horizontal = 12.dp), + Modifier + // this width limitation prevents crash on calculating constraints that may happen if you post veeeery long message and then quote it. + // Top level layout wants `IntrinsicWidth.Max` and very long layout makes the crash in this case + .widthIn(max = 50000.dp) + .padding(vertical = 6.dp, horizontal = 12.dp), contentAlignment = Alignment.TopStart ) { val sender = qi.sender(membership()) @@ -75,33 +80,35 @@ fun FramedItemView( ) { Text( sender, - style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary) + 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) + ciQuotedMsgTextView(qi, lines = 2, showTimestamp = showTimestamp) } } else { - ciQuotedMsgTextView(qi, lines = 3) + ciQuotedMsgTextView(qi, lines = 3, showTimestamp = showTimestamp) } } } @Composable - fun FramedItemHeader(caption: String, italic: Boolean, icon: Painter? = null) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + fun FramedItemHeader(caption: String, italic: Boolean, icon: Painter? = null, pad: Boolean = false, iconColor: Color? = null) { + val sentColor = MaterialTheme.appColors.sentQuote + val receivedColor = MaterialTheme.appColors.receivedQuote Row( Modifier - .background(if (sent) sentColor.toQuote() else receivedColor.toQuote()) + .background(if (sent) sentColor else receivedColor) .fillMaxWidth() - .padding(start = 8.dp, top = 6.dp, end = 12.dp, bottom = if (ci.quotedItem == null) 6.dp else 0.dp), + .padding(start = 8.dp, top = 6.dp, end = 12.dp, bottom = if (pad || (ci.quotedItem == null && ci.meta.itemForwarded == null)) 6.dp else 0.dp), horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically ) { if (icon != null) { Icon( icon, caption, Modifier.size(18.dp), - tint = if (isInDarkTheme()) FileDark else FileLight + tint = iconColor ?: if (isInDarkTheme()) FileDark else FileLight ) } Text( @@ -119,17 +126,12 @@ fun FramedItemView( @Composable fun ciQuoteView(qi: CIQuote) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentQuote + val receivedColor = MaterialTheme.appColors.receivedQuote Row( Modifier - .background(if (sent) sentColor.toQuote() else receivedColor.toQuote()) + .background(if (sent) sentColor else receivedColor) .fillMaxWidth() - .combinedClickable( - onLongClick = { showMenu.value = true }, - onClick = { scrollToItem(qi.itemId?: return@combinedClickable) } - ) - .onRightClick { showMenu.value = true } ) { when (qi.content) { is MsgContent.MCImage -> { @@ -176,19 +178,19 @@ fun FramedItemView( @Composable fun ciFileView(ci: ChatItem, text: String) { - CIFileView(ci.file, ci.meta.itemEdited, receiveFile) + CIFileView(ci.file, ci.meta.itemEdited, showMenu, false, receiveFile) if (text != "" || ci.meta.isLive) { - CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler) + CIMarkdownText(ci, chatInfo, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } val transparentBackground = (ci.content.msgContent is MsgContent.MCImage || ci.content.msgContent is MsgContent.MCVideo) && - !ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null + !ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null && ci.meta.itemForwarded == null - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Box(Modifier - .clip(RoundedCornerShape(18.dp)) + .clipChatItem(ci, tailVisible, revealed = true) .background( when { transparentBackground -> Color.Transparent @@ -198,24 +200,76 @@ fun FramedItemView( )) { var metaColor = MaterialTheme.colors.secondary Box(contentAlignment = Alignment.BottomEnd) { - Column(Modifier.width(IntrinsicSize.Max)) { + val chatItemTail = remember { appPreferences.chatItemTail.state } + val style = shapeStyle(ci, chatItemTail.value, tailVisible, revealed = true) + val tailRendered = style is ShapeStyle.Bubble && style.tailVisible + Column( + Modifier + .width(IntrinsicSize.Max) + .padding(start = if (!sent && tailRendered) msgTailWidthDp else 0.dp, end = if (sent && tailRendered) msgTailWidthDp else 0.dp) + ) { PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) { - if (ci.meta.itemDeleted != null) { - when (ci.meta.itemDeleted) { - is CIDeleted.Moderated -> { - FramedItemHeader(String.format(stringResource(MR.strings.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName), true, painterResource(MR.images.ic_flag)) + @Composable + fun Header() { + if (ci.isReport) { + if (ci.meta.itemDeleted == null) { + FramedItemHeader( + stringResource(if (ci.chatDir.sent) MR.strings.report_item_visibility_submitter else MR.strings.report_item_visibility_moderators), + true, + painterResource(MR.images.ic_flag), + iconColor = Color.Red + ) + } else { + val text = if (ci.meta.itemDeleted is CIDeleted.Moderated && ci.meta.itemDeleted.byGroupMember.groupMemberId != (chatInfo as ChatInfo.Group?)?.groupInfo?.membership?.groupMemberId) { + stringResource(MR.strings.report_item_archived_by).format(ci.meta.itemDeleted.byGroupMember.displayName) + } else { + stringResource(MR.strings.report_item_archived) + } + FramedItemHeader(text, true, painterResource(MR.images.ic_flag)) } - is CIDeleted.Blocked -> { - FramedItemHeader(stringResource(MR.strings.blocked_item_description), true, painterResource(MR.images.ic_back_hand)) - } - else -> { - FramedItemHeader(stringResource(MR.strings.marked_deleted_description), true, painterResource(MR.images.ic_delete)) + } else if (ci.meta.itemDeleted != null) { + when (ci.meta.itemDeleted) { + is CIDeleted.Moderated -> { + FramedItemHeader(String.format(stringResource(MR.strings.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName), true, painterResource(MR.images.ic_flag)) + } + is CIDeleted.Blocked -> { + FramedItemHeader(stringResource(MR.strings.blocked_item_description), true, painterResource(MR.images.ic_back_hand)) + } + is CIDeleted.BlockedByAdmin -> { + FramedItemHeader(stringResource(MR.strings.blocked_by_admin_item_description), true, painterResource(MR.images.ic_back_hand)) + } + is CIDeleted.Deleted -> { + FramedItemHeader(stringResource(MR.strings.marked_deleted_description), true, painterResource(MR.images.ic_delete)) + } } + } else if (ci.meta.isLive) { + FramedItemHeader(stringResource(MR.strings.live), false) + } + } + if (ci.quotedItem != null) { + Column( + Modifier + .combinedClickable( + onLongClick = { showMenu.value = true }, + onClick = { + if (ci.quotedItem.itemId != null) { + scrollToItem(ci.quotedItem.itemId) + } else { + scrollToQuotedItemFromItem(ci.id) + } + } + ) + .onRightClick { showMenu.value = true } + ) { + Header() + ciQuoteView(ci.quotedItem) + } + } else { + Header() + if (ci.meta.itemForwarded != null) { + FramedItemHeader(ci.meta.itemForwarded.text(chatInfo.chatType), true, painterResource(MR.images.ic_forward), pad = true) } - } else if (ci.meta.isLive) { - FramedItemHeader(stringResource(MR.strings.live), false) } - ci.quotedItem?.let { ciQuoteView(it) } if (ci.file == null && ci.formattedText == null && !ci.meta.isLive && isShortEmoji(ci.content.text)) { Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) { Column( @@ -231,47 +285,61 @@ fun FramedItemView( } else { when (val mc = ci.content.msgContent) { is MsgContent.MCImage -> { - CIImageView(image = mc.image, file = ci.file, ci.encryptLocalFile, metaColor = metaColor, imageProvider ?: return@PriorityLayout, showMenu, receiveFile) + CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, false, receiveFile) if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler) + CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } is MsgContent.MCVideo -> { - CIVideoView(image = mc.image, mc.duration, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile) + CIVideoView(image = mc.image, mc.duration, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, smallView = false, receiveFile = receiveFile) if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler) + CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } is MsgContent.MCVoice -> { - CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") }, receiveFile) + CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp, longClick = { onLinkLongClick("") }, receiveFile = receiveFile) if (mc.text != "") { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler) + CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } is MsgContent.MCFile -> ciFileView(ci, mc.text) is MsgContent.MCUnknown -> if (ci.file == null) { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) + CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } else { ciFileView(ci, mc.text) } is MsgContent.MCLink -> { - ChatItemLinkView(mc.preview) + ChatItemLinkView(mc.preview, showMenu, onLongClick = { showMenu.value = true }) Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) + CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } - else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) + 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}: ") + } + } + CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp, prefix = prefix) + } + else -> CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } } } - Box(Modifier.padding(bottom = 6.dp, end = 12.dp)) { - CIMetaView(ci, chatTTL, metaColor) + Box( + Modifier + .padding( + bottom = 6.dp, + end = 12.dp + if (tailRendered && sent) msgTailWidthDp else 0.dp, + ) + ) { + CIMetaView(ci, chatTTL, metaColor, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } } @@ -280,28 +348,43 @@ fun FramedItemView( @Composable fun CIMarkdownText( ci: ChatItem, + chatInfo: ChatInfo, chatTTL: Int?, linkMode: SimplexLinkMode, uriHandler: UriHandler?, - onLinkLongClick: (link: String) -> Unit = {} + onLinkLongClick: (link: String) -> Unit = {}, + showViaProxy: Boolean, + showTimestamp: Boolean, + prefix: AnnotatedString? = null ) { - Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) { + Box(Modifier.padding(vertical = 7.dp, horizontal = 12.dp)) { val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text MarkdownText( - text, if (text.isEmpty()) emptyList() else ci.formattedText, + text, if (text.isEmpty()) emptyList() else ci.formattedText, toggleSecrets = true, meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode, - uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick + mentions = ci.mentions, userMemberId = when { + chatInfo is ChatInfo.Group -> chatInfo.groupInfo.membership.memberId + else -> null + }, + uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp, prefix = prefix ) } } const val CHAT_IMAGE_LAYOUT_ID = "chatImage" +const val CHAT_BUBBLE_LAYOUT_ID = "chatBubble" +const val CHAT_COMPOSE_LAYOUT_ID = "chatCompose" +const val CONSOLE_COMPOSE_LAYOUT_ID = "consoleCompose" + /** - * Equal to [androidx.compose.ui.unit.Constraints.MaxFocusMask], which is 0x3FFFF - 1 - * Other values make a crash `java.lang.IllegalArgumentException: Can't represent a width of 123456 and height of 9909 in Constraints` - * See [androidx.compose.ui.unit.Constraints.createConstraints] - * */ -const val MAX_SAFE_WIDTH = 0x3FFFF - 1 + * Compose shows "Can't represent a width of ... and height ... in Constraints" even when using built-in method for measuring max + * available size. It seems like padding around such layout prevents showing them in parent layout when such child layouts are placed. + * So calculating the expected padding here based on the values Compose printed in the exception (removing some pixels from + * [Constraints.fitPrioritizingHeight] result makes it working well) +*/ +private fun horizontalPaddingAroundCustomLayouts(density: Float): Int = + // currently, it's 18. Doubling it just to cover possible changes in the future + 36 * ceil(density).toInt() @Composable fun PriorityLayout( @@ -309,17 +392,6 @@ fun PriorityLayout( priorityLayoutId: String, content: @Composable () -> Unit ) { - /** - * Limiting max value for height + width in order to not crash the app, see [androidx.compose.ui.unit.Constraints.createConstraints] - * */ - fun maxSafeHeight(width: Int) = when { // width bits + height bits should be <= 31 - width < 0x1FFF /*MaxNonFocusMask*/ -> 0x3FFFF - 1 /* MaxFocusMask */ // 13 bits width + 18 bits height - width < 0x7FFF /*MinNonFocusMask*/ -> 0xFFFF - 1 /* MinFocusMask */ // 15 bits width + 16 bits height - width < 0xFFFF /*MinFocusMask*/ -> 0x7FFF - 1 /* MinFocusMask */ // 16 bits width + 15 bits height - width < 0x3FFFF /*MaxFocusMask*/ -> 0x1FFF - 1 /* MaxNonFocusMask */ // 18 bits width + 13 bits height - else -> 0x1FFF // shouldn't happen since width is limited already - } - Layout( content = content, modifier = modifier @@ -331,11 +403,15 @@ fun PriorityLayout( if (it.layoutId == priorityLayoutId) imagePlaceable!! else - it.measure(constraints.copy(maxWidth = imagePlaceable?.width ?: min(MAX_SAFE_WIDTH, constraints.maxWidth))) } + it.measure(constraints.copy(maxWidth = imagePlaceable?.width ?: constraints.maxWidth)) } // Limit width for every other element to width of important element and height for a sum of all elements. - val width = imagePlaceable?.measuredWidth ?: min(MAX_SAFE_WIDTH, placeables.maxOf { it.width }) - val height = minOf(maxSafeHeight(width), placeables.sumOf { it.height }) - layout(width, height) { + val width = imagePlaceable?.measuredWidth ?: placeables.maxOf { it.width } + val height = placeables.sumOf { it.height } + val adjustedConstraints = Constraints.fitPrioritizingHeight(constraints.minWidth, width, constraints.minHeight, height) + layout( + if (width > adjustedConstraints.maxWidth) adjustedConstraints.maxWidth - horizontalPaddingAroundCustomLayouts(density) else adjustedConstraints.maxWidth, + adjustedConstraints.maxHeight + ) { var y = 0 placeables.forEach { it.place(0, y) @@ -344,6 +420,111 @@ fun PriorityLayout( } } } + +@Composable +fun DependentLayout( + modifier: Modifier = Modifier, + mainLayoutId: String, + content: @Composable () -> Unit +) { + Layout( + content = content, + modifier = modifier + ) { measureable, constraints -> + // Find important element which should tell what min width it needs to draw itself. + // Expecting only one such element. Can be less than one but not more + val mainPlaceable = measureable.firstOrNull { it.layoutId == mainLayoutId }?.measure(constraints) + val placeables: List<Placeable> = measureable.map { + if (it.layoutId == mainLayoutId) + mainPlaceable!! + else + it.measure(constraints.copy(minWidth = mainPlaceable?.width ?: 0, maxWidth = constraints.maxWidth)) } + val width = mainPlaceable?.measuredWidth ?: placeables.maxOf { it.width } + val height = placeables.sumOf { it.height } + val adjustedConstraints = Constraints.fitPrioritizingHeight(constraints.minWidth, width, constraints.minHeight, height) + layout( + if (width > adjustedConstraints.maxWidth) adjustedConstraints.maxWidth - horizontalPaddingAroundCustomLayouts(density) else adjustedConstraints.maxWidth, + adjustedConstraints.maxHeight + ) { + var y = 0 + placeables.forEach { + it.place(0, y) + y += it.measuredHeight + } + } + } +} + +// The purpose of this layout is to make measuring of bottom compose view and adapt top lazy column to its size in the same frame (not on the next frame as you would expect). +// So, steps are: +// - measuring the layout: measured height of compose view before this step is 0, it's added to content padding of lazy column (so it's == 0) +// - measured the layout: measured height of compose view now is correct, but it's not yet applied to lazy column content padding (so it's == 0) and lazy column is placed higher than compose view in view with respect to compose view's height +// - on next frame measured height is correct and content padding is the same, lazy column placed to occupy all parent view's size +// - every added/removed line in compose view goes through the same process. +@Composable +fun AdaptingBottomPaddingLayout( + modifier: Modifier = Modifier, + mainLayoutId: String, + expectedHeight: MutableState<Dp>, + content: @Composable () -> Unit +) { + val expected = with(LocalDensity.current) { expectedHeight.value.roundToPx() } + Layout( + content = content, + modifier = modifier + ) { measureable, constraints -> + require(measureable.size <= 2) { "Should be exactly one or two elements in this layout, you have ${measureable.size}" } + val mainPlaceable = measureable.firstOrNull { it.layoutId == mainLayoutId }!!.measure(constraints) + val placeables: List<Placeable> = measureable.map { + if (it.layoutId == mainLayoutId) + mainPlaceable + else + it.measure(constraints.copy(maxHeight = if (expected != mainPlaceable.measuredHeight) constraints.maxHeight - mainPlaceable.measuredHeight + expected else constraints.maxHeight)) } + expectedHeight.value = mainPlaceable.measuredHeight.toDp() + layout(constraints.maxWidth, constraints.maxHeight) { + var y = 0 + placeables.forEach { + if (it !== mainPlaceable) { + it.place(0, y) + y += it.measuredHeight + } else { + it.place(0, constraints.maxHeight - mainPlaceable.measuredHeight) + y += it.measuredHeight + } + } + } + } +} + +@Composable +fun CenteredRowLayout( + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Layout( + content = content, + modifier = modifier + ) { measureable, constraints -> + require(measureable.size == 3) { "Should be exactly three elements in this layout, you have ${measureable.size}" } + val first = measureable[0].measure(constraints.copy(minWidth = 0, minHeight = 0)) + val third = measureable[2].measure(constraints.copy(minWidth = first.measuredWidth, minHeight = 0)) + val second = measureable[1].measure(constraints.copy(minWidth = 0, minHeight = 0, maxWidth = (constraints.maxWidth - first.measuredWidth - third.measuredWidth).coerceAtLeast(0))) + // Limit width for every other element to width of important element and height for a sum of all elements. + layout(constraints.maxWidth, constraints.maxHeight) { + first.place(0, ((constraints.maxHeight - first.measuredHeight) / 2).coerceAtLeast(0)) + second.place((constraints.maxWidth - second.measuredWidth) / 2, ((constraints.maxHeight - second.measuredHeight) / 2).coerceAtLeast(0)) + third.place(constraints.maxWidth - third.measuredWidth, ((constraints.maxHeight - third.measuredHeight) / 2).coerceAtLeast(0)) + } + } +} + +fun showQuotedItemDoesNotExistAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.message_deleted_or_not_received_error_title), + text = generalGetString(MR.strings.message_deleted_or_not_received_error_desc) + ) +} + /* class EditedProvider: PreviewParameterProvider<Boolean> { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt index c7268592bf..70d6fa4aa8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt @@ -12,7 +12,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.* import androidx.compose.ui.input.pointer.* import androidx.compose.ui.layout.onGloballyPositioned +import chat.simplex.common.model.CryptoFile import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.views.chat.ProviderMedia import chat.simplex.common.views.helpers.* import kotlinx.coroutines.flow.distinctUntilChanged @@ -38,24 +40,41 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> ) { provider.totalMediaSize.value } + val firstValidPageBeforeScrollingToStart = remember { mutableStateOf(0) } val goBack = { provider.onDismiss(pagerState.currentPage); close() } BackHandler(onBack = goBack) // Pager doesn't ask previous page at initialization step who knows why. By not doing this, prev page is not checked and can be blank, // which makes this blank page visible for a moment. Prevent it by doing the check ourselves LaunchedEffect(Unit) { if (provider.getMedia(provider.initialIndex - 1) == null) { + firstValidPageBeforeScrollingToStart.value = provider.initialIndex provider.scrollToStart() pagerState.scrollToPage(0) + firstValidPageBeforeScrollingToStart.value = 0 } } val scope = rememberCoroutineScope() val playersToRelease = rememberSaveable { mutableSetOf<URI>() } DisposableEffectOnGone( - whenGone = { playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) } } + always = { + platform.androidSetStatusAndNavigationBarAppearance(false, false, blackNavBar = true) + chatModel.fullscreenGalleryVisible.value = true + }, + whenDispose = { + val c = CurrentColors.value.colors + platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight) + chatModel.fullscreenGalleryVisible.value = false + }, + whenGone = { + playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) } + } ) @Composable fun Content(index: Int) { + // Index can be huge but in reality at that moment pager state scrolls to 0 and that page should have index 0 too if it's the first one. + // Or index 1 if it's the second page + val index = index - firstValidPageBeforeScrollingToStart.value Column( Modifier .fillMaxSize() @@ -86,6 +105,8 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> provider.scrollToStart() pagerState.scrollToPage(0) } + // Current media was deleted or moderated, close gallery + index -> close() } } } @@ -134,9 +155,15 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> FullScreenImageView(modifier, data, imageBitmap) } else if (media is ProviderMedia.Video) { val preview = remember(media.uri.path) { base64ToBitmap(media.preview) } - VideoView(modifier, media.uri, preview, index == settledCurrentPage, close) - DisposableEffect(Unit) { - onDispose { playersToRelease.add(media.uri) } + val uriDecrypted = remember(media.uri.path) { mutableStateOf(if (media.fileSource?.cryptoArgs == null) media.uri else media.fileSource.decryptedGet()) } + val decrypted = uriDecrypted.value + if (decrypted != null) { + VideoView(modifier, decrypted, preview, index == settledCurrentPage, close) + DisposableEffect(Unit) { + onDispose { playersToRelease.add(decrypted) } + } + } else if (media.fileSource != null) { + VideoViewEncrypted(uriDecrypted, media.fileSource, preview, close) } } } @@ -152,6 +179,22 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> @Composable expect fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) +@Composable +private fun VideoViewEncrypted(uriUnencrypted: MutableState<URI?>, fileSource: CryptoFile, defaultPreview: ImageBitmap, close: () -> Unit) { + LaunchedEffect(Unit) { + withBGApi { + uriUnencrypted.value = fileSource.decryptedGetOrCreate() + if (uriUnencrypted.value == null) { + close() + } + } + } + Box(contentAlignment = Alignment.Center) { + VideoPreviewImageViewFullScreen(defaultPreview, {}, {}) + VideoDecryptionProgress() {} + } +} + @Composable private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap, currentPage: Boolean, close: () -> Unit) { val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, true, defaultPreview, 0L, true) } 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 582730b8f5..d528396193 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,15 +17,14 @@ 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.ui.theme.CurrentColors -import chat.simplex.common.ui.theme.SimpleXTheme +import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.AlertManager import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR @Composable -fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?) { - CIMsgError(ci, timedMessagesTTL) { +fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, showTimestamp: Boolean, timedMessagesTTL: Int?) { + CIMsgError(ci, showTimestamp, timedMessagesTTL) { when (msgError) { is MsgErrorType.MsgSkipped -> AlertManager.shared.showAlertMsg( @@ -50,12 +49,13 @@ fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTT } @Composable -fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) { - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage +fun CIMsgError(ci: ChatItem, showTimestamp: Boolean, timedMessagesTTL: Int?, onClick: () -> Unit) { + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( Modifier.clickable(onClick = onClick), shape = RoundedCornerShape(18.dp), color = receivedColor, + contentColor = LocalContentColor.current ) { Row( Modifier.padding(horizontal = 12.dp, vertical = 6.dp), @@ -68,7 +68,7 @@ fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) { style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), modifier = Modifier.padding(end = 8.dp) ) - CIMetaView(ci, timedMessagesTTL) + CIMetaView(ci, timedMessagesTTL, showViaProxy = false, showTimestamp = showTimestamp) } } } @@ -83,7 +83,8 @@ fun IntegrityErrorItemViewPreview() { IntegrityErrorItemView( MsgErrorType.MsgBadHash(), ChatItem.getDeletedContentSampleData(), - null + showTimestamp = true, + null, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt index 50d905ef76..84bc14fee3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt @@ -20,33 +20,35 @@ import dev.icerock.moko.resources.compose.stringResource import kotlinx.datetime.Clock @Composable -fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState<Boolean>) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage +fun MarkedDeletedItemView(chatsCtx: ChatModel.ChatsContext, ci: ChatItem, chatInfo: ChatInfo, timedMessagesTTL: Int?, revealed: State<Boolean>, showViaProxy: Boolean, showTimestamp: Boolean) { + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( shape = RoundedCornerShape(18.dp), color = if (ci.chatDir.sent) sentColor else receivedColor, + contentColor = LocalContentColor.current ) { Row( Modifier.padding(horizontal = 12.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically ) { Box(Modifier.weight(1f, false)) { - MergedMarkedDeletedText(ci, revealed) + MergedMarkedDeletedText(chatsCtx, ci, chatInfo, revealed) } - CIMetaView(ci, timedMessagesTTL) + CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } } @Composable -private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: MutableState<Boolean>) { - var i = getChatItemIndexOrNull(chatItem) +private fun MergedMarkedDeletedText(chatsCtx: ChatModel.ChatsContext, chatItem: ChatItem, chatInfo: ChatInfo, revealed: State<Boolean>) { + val reversedChatItems = chatsCtx.chatItems.value.asReversed() + var i = getChatItemIndexOrNull(chatItem, reversedChatItems) val ciCategory = chatItem.mergeCategory val text = if (!revealed.value && ciCategory != null && i != null) { - val reversedChatItems = ChatModel.chatItems.asReversed() var moderated = 0 var blocked = 0 + var blockedByAdmin = 0 var deleted = 0 val moderatedBy: MutableSet<String> = mutableSetOf() while (i < reversedChatItems.size) { @@ -58,21 +60,24 @@ private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: MutableState<B moderatedBy.add(itemDeleted.byGroupMember.displayName) } is CIDeleted.Blocked -> blocked += 1 + is CIDeleted.BlockedByAdmin -> blockedByAdmin +=1 is CIDeleted.Deleted -> deleted += 1 } i++ } - val total = moderated + blocked + deleted + val total = moderated + blocked + blockedByAdmin + deleted if (total <= 1) - markedDeletedText(chatItem.meta) + markedDeletedText(chatItem, chatInfo) else if (total == moderated) stringResource(MR.strings.moderated_items_description).format(total, moderatedBy.joinToString(", ")) - else if (total == blocked) + else if (total == blockedByAdmin) + stringResource(MR.strings.blocked_by_admin_items_description).format(total) + else if (total == blocked + blockedByAdmin) stringResource(MR.strings.blocked_items_description).format(total) else stringResource(MR.strings.marked_deleted_items_description).format(total) } else { - markedDeletedText(chatItem.meta) + markedDeletedText(chatItem, chatInfo) } Text( @@ -86,13 +91,22 @@ private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: MutableState<B ) } -private fun markedDeletedText(meta: CIMeta): String = - when (meta.itemDeleted) { +fun markedDeletedText(cItem: ChatItem, chatInfo: ChatInfo): String = + if (cItem.meta.itemDeleted != null && cItem.isReport) { + if (cItem.meta.itemDeleted is CIDeleted.Moderated && cItem.meta.itemDeleted.byGroupMember.groupMemberId != (chatInfo as? ChatInfo.Group)?.groupInfo?.membership?.groupMemberId) { + generalGetString(MR.strings.report_item_archived_by).format(cItem.meta.itemDeleted.byGroupMember.displayName) + } else { + generalGetString(MR.strings.report_item_archived) + } + } + else when (cItem.meta.itemDeleted) { is CIDeleted.Moderated -> - String.format(generalGetString(MR.strings.moderated_item_description), meta.itemDeleted.byGroupMember.displayName) + String.format(generalGetString(MR.strings.moderated_item_description), cItem.meta.itemDeleted.byGroupMember.displayName) is CIDeleted.Blocked -> generalGetString(MR.strings.blocked_item_description) - else -> + is CIDeleted.BlockedByAdmin -> + generalGetString(MR.strings.blocked_by_admin_item_description) + is CIDeleted.Deleted, null -> generalGetString(MR.strings.marked_deleted_description) } @@ -105,7 +119,9 @@ fun PreviewMarkedDeletedItemView() { SimpleXTheme { DeletedItemView( ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted(Clock.System.now())), - null + null, + showViaProxy = false, + showTimestamp = true ) } } 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 ff1267d0fa..ad11eb4897 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 @@ -13,7 +13,6 @@ import androidx.compose.ui.platform.* import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.sp @@ -22,7 +21,6 @@ import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.views.helpers.* import kotlinx.coroutines.* -import java.awt.* val reserveTimestampStyle = SpanStyle(color = Color.Transparent) val boldFont = SpanStyle(fontWeight = FontWeight.Medium) @@ -60,6 +58,9 @@ fun MarkdownText ( sender: String? = null, meta: CIMeta? = null, chatTTL: Int? = null, + mentions: Map<String, CIMention>? = null, + userMemberId: String? = null, + toggleSecrets: Boolean, style: TextStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface, lineHeight = 22.sp), maxLines: Int = Int.MAX_VALUE, overflow: TextOverflow = TextOverflow.Clip, @@ -67,8 +68,11 @@ fun MarkdownText ( senderBold: Boolean = false, modifier: Modifier = Modifier, linkMode: SimplexLinkMode, - inlineContent: Map<String, InlineTextContent>? = null, - onLinkLongClick: (link: String) -> Unit = {} + inlineContent: Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>>? = null, + onLinkLongClick: (link: String) -> Unit = {}, + showViaProxy: Boolean = false, + showTimestamp: Boolean = true, + prefix: AnnotatedString? = null ) { val textLayoutDirection = remember (text) { if (isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr @@ -76,7 +80,7 @@ fun MarkdownText ( val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) { "\n" } else if (meta != null) { - reserveSpaceForMeta(meta, chatTTL, null) // LALAL + reserveSpaceForMeta(meta, chatTTL, null, secondaryColor = MaterialTheme.colors.secondary, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } else { " " } @@ -89,6 +93,7 @@ fun MarkdownText ( ) { var timer: Job? by remember { mutableStateOf(null) } var typingIdx by rememberSaveable { mutableStateOf(0) } + val showSecrets = remember { mutableStateMapOf<String, Boolean>() } fun stopTyping() { timer?.cancel() timer = null @@ -117,7 +122,9 @@ fun MarkdownText ( } if (formattedText == null) { val annotatedText = buildAnnotatedString { + inlineContent?.first?.invoke(this) appendSender(this, sender, senderBold) + if (prefix != null) append(prefix) if (text is String) append(text) else if (text is AnnotatedString) append(text) if (meta?.isLive == true) { @@ -125,17 +132,46 @@ fun MarkdownText ( } if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) } } - Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, inlineContent = inlineContent ?: mapOf()) + Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, inlineContent = inlineContent?.second ?: mapOf()) } else { - var hasLinks = false + var hasAnnotations = false val annotatedText = buildAnnotatedString { + inlineContent?.first?.invoke(this) appendSender(this, sender, senderBold) - for (ft in formattedText) { + if (prefix != null) append(prefix) + for ((i, ft) in formattedText.withIndex()) { if (ft.format == null) append(ft.text) - else { + else if (toggleSecrets && ft.format is Format.Secret) { + val ftStyle = ft.format.style + hasAnnotations = true + val key = i.toString() + withAnnotation(tag = "SECRET", annotation = key) { + if (showSecrets[key] == true) append(ft.text) else withStyle(ftStyle) { append(ft.text) } + } + } else if (ft.format is Format.Mention) { + val mention = mentions?.get(ft.format.memberName) + + if (mention != null) { + if (mention.memberRef != null) { + val displayName = mention.memberRef.displayName + val name = if (mention.memberRef.localAlias.isNullOrEmpty()) { + displayName + } else { + "${mention.memberRef.localAlias} ($displayName)" + } + val mentionStyle = if (mention.memberId == userMemberId) ft.format.style.copy(color = MaterialTheme.colors.primary) else ft.format.style + + withStyle(mentionStyle) { append(mentionText(name)) } + } else { + withStyle( ft.format.style) { append(mentionText(ft.format.memberName)) } + } + } else { + append(ft.text) + } + } else { val link = ft.link(linkMode) if (link != null) { - hasLinks = true + hasAnnotations = true val ftStyle = ft.format.style withAnnotation(tag = if (ft.format is Format.SimplexLink) "SIMPLEX_URL" else "URL", annotation = link) { withStyle(ftStyle) { append(ft.viewText(linkMode)) } @@ -153,7 +189,7 @@ fun MarkdownText ( withStyle(reserveTimestampStyle) { append("\n" + metaText) } else */if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) } } - if (hasLinks && uriHandler != null) { + if (hasAnnotations && uriHandler != null) { val icon = remember { mutableStateOf(PointerIcon.Default) } ClickableText(annotatedText, style = style, modifier = modifier.pointerHoverIcon(icon.value), maxLines = maxLines, overflow = overflow, onLongClick = { offset -> @@ -177,12 +213,20 @@ fun MarkdownText ( .firstOrNull()?.let { annotation -> uriHandler.openVerifiedSimplexUri(annotation.item) } + annotatedText.getStringAnnotations(tag = "SECRET", start = offset, end = offset) + .firstOrNull()?.let { annotation -> + val key = annotation.item + showSecrets[key] = !(showSecrets[key] ?: false) + } }, onHover = { offset -> icon.value = annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset) .firstOrNull()?.let { PointerIcon.Hand } ?: annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) + .firstOrNull()?.let { + PointerIcon.Hand + } ?: annotatedText.getStringAnnotations(tag = "SECRET", start = offset, end = offset) .firstOrNull()?.let { PointerIcon.Hand } ?: PointerIcon.Default @@ -193,7 +237,7 @@ fun MarkdownText ( } ) } else { - Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow) + Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, inlineContent = inlineContent?.second ?: mapOf()) } } } @@ -267,3 +311,5 @@ private fun isRtl(s: CharSequence): Boolean { } return false } + +private fun mentionText(name: String): String = if (name.contains(" @")) "@'$name'" else "@$name" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatHelpView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatHelpView.kt index 866ad04b02..f36978cd3c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatHelpView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatHelpView.kt @@ -50,8 +50,8 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) { ) Text(stringResource(MR.strings.above_then_preposition_continuation)) } - Text(annotatedStringResource(MR.strings.add_new_contact_to_create_one_time_QR_code), lineHeight = 22.sp) - Text(annotatedStringResource(MR.strings.scan_QR_code_to_connect_to_contact_who_shows_QR_code), lineHeight = 22.sp) + Text(annotatedStringResource(MR.strings.add_contact_button_to_create_link_or_connect_via_link), lineHeight = 22.sp) + Text(annotatedStringResource(MR.strings.create_group_button_to_create_new_group), lineHeight = 22.sp) } Column( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 8d5446aa53..958b794bd7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -4,8 +4,6 @@ import SectionItemView import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -13,42 +11,44 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.* +import androidx.compose.ui.graphics.* import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontStyle 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.model.ChatModel.controller import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* -import chat.simplex.common.views.chat.group.deleteGroupDialog -import chat.simplex.common.views.chat.group.leaveGroupDialog -import chat.simplex.common.views.chat.item.InvalidJSONView +import chat.simplex.common.views.chat.group.* import chat.simplex.common.views.chat.item.ItemAction +import chat.simplex.common.views.contacts.onRequestAccepted import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.* import chat.simplex.res.MR -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.datetime.Clock -import java.net.URI @Composable -fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { +fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) { val showMenu = remember { mutableStateOf(false) } val showMarkRead = remember(chat.chatStats.unreadCount, chat.chatStats.unreadChat) { chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat } - val stopped = chatModel.chatRunning.value == false + val disabled = chatModel.chatRunning.value == false || chatModel.deletedChats.value.contains(chat.remoteHostId to chat.chatInfo.id) val linkMode by remember { chatModel.controller.appPrefs.simplexLinkMode.state } LaunchedEffect(chat.id) { showMenu.value = false delay(500L) } - val selectedChat = remember(chat.id) { derivedStateOf { chat.id == ChatModel.chatId.value } } + val selectedChat = remember(chat.id) { derivedStateOf { chat.id == chatModel.chatId.value } } val showChatPreviews = chatModel.showChatPreviews.value val inProgress = remember { mutableStateOf(false) } var progressByTimeout by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(inProgress.value) { progressByTimeout = if (inProgress.value) { delay(1000) @@ -58,43 +58,70 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { } } + val scope = rememberCoroutineScope() + when (chat.chatInfo) { is ChatInfo.Direct -> { val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact) + val defaultClickAction = { if (chatModel.chatId.value != chat.id) scope.launch { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) } } ChatListNavLinkLayout( chatLinkPreview = { tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { - ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode, inProgress = false, progressByTimeout = false) + ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, disabled, linkMode, inProgress = false, progressByTimeout = false, defaultClickAction) } }, - click = { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) }, + click = defaultClickAction, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { ContactMenuItems(chat, chat.chatInfo.contact, chatModel, showMenu, showMarkRead) } }, showMenu, - stopped, - selectedChat + disabled, + selectedChat, + nextChatSelected, ) } - is ChatInfo.Group -> + is ChatInfo.Group -> { + val defaultClickAction = { if (!inProgress.value && chatModel.chatId.value != chat.id) scope.launch { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel, inProgress) } } ChatListNavLinkLayout( chatLinkPreview = { tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { - ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode, inProgress.value, progressByTimeout) + ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, disabled, linkMode, inProgress.value, progressByTimeout, defaultClickAction) } }, - click = { if (!inProgress.value) groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel, inProgress) }, + click = defaultClickAction, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, inProgress, showMarkRead) } }, showMenu, - stopped, - selectedChat + disabled, + selectedChat, + nextChatSelected, ) + } + is ChatInfo.Local -> { + val defaultClickAction = { if (chatModel.chatId.value != chat.id) scope.launch { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) } } + ChatListNavLinkLayout( + chatLinkPreview = { + tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { + ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, disabled, linkMode, inProgress = false, progressByTimeout = false, defaultClickAction) + } + }, + click = defaultClickAction, + dropdownMenuItems = { + tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { + NoteFolderMenuItems(chat, showMenu, showMarkRead) + } + }, + showMenu, + disabled, + selectedChat, + nextChatSelected, + ) + } is ChatInfo.ContactRequest -> ChatListNavLinkLayout( chatLinkPreview = { @@ -102,15 +129,16 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { ContactRequestView(chat.chatInfo) } }, - click = { contactRequestAlertDialog(chat.remoteHostId, chat.chatInfo, chatModel) }, + click = { contactRequestAlertDialog(chat.remoteHostId, chat.chatInfo, chatModel) { onRequestAccepted(it) } }, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { ContactRequestMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) } }, showMenu, - stopped, - selectedChat + disabled, + selectedChat, + nextChatSelected, ) is ChatInfo.ContactConnection -> ChatListNavLinkLayout( @@ -120,11 +148,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { } }, click = { - ModalManager.center.closeModals() - ModalManager.end.closeModals() - ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close -> - ContactConnectionInfoView(chatModel, chat.remoteHostId, chat.chatInfo.contactConnection.connReqInv, chat.chatInfo.contactConnection, false, close) - } + chatModel.chatId.value = chat.id }, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { @@ -132,8 +156,9 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { } }, showMenu, - stopped, - selectedChat + disabled, + selectedChat, + nextChatSelected, ) is ChatInfo.InvalidJSON -> ChatListNavLinkLayout( @@ -143,88 +168,92 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { } }, click = { - ModalManager.end.closeModals() - ModalManager.center.showModal(true) { InvalidJSONView(chat.chatInfo.json) } + chatModel.chatId.value = chat.id }, dropdownMenuItems = null, showMenu, - stopped, - selectedChat + disabled, + selectedChat, + nextChatSelected, ) } } @Composable -private fun ErrorChatListItem() { +fun ErrorChatListItem() { Box(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp)) { Text(stringResource(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic) } } -fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) { +suspend fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) { when { - contact.activeConn == null && contact.profile.contactLink != null -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true) - else -> withBGApi { openChat(rhId, ChatInfo.Direct(contact), chatModel) } + contact.activeConn == null && contact.profile.contactLink != null && contact.active -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true) + else -> openDirectChat(rhId, contact.contactId) } } -fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) { +suspend fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) { when (groupInfo.membership.memberStatus) { GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(rhId, groupInfo, chatModel, inProgress) GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert(rhId) - else -> withBGApi { openChat(rhId, ChatInfo.Group(groupInfo), chatModel) } + else -> openGroupChat(rhId, groupInfo.groupId) } } -suspend fun openDirectChat(rhId: Long?, contactId: Long, chatModel: ChatModel) { - val chat = chatModel.controller.apiGetChat(rhId, ChatType.Direct, contactId) - if (chat != null) { - openLoadedChat(chat, chatModel) +suspend fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) = openChat(secondaryChatsCtx = null, rhId, ChatInfo.Local(noteFolder)) + +suspend fun openDirectChat(rhId: Long?, contactId: Long) = openChat(secondaryChatsCtx = null, rhId, ChatType.Direct, contactId) + +suspend fun openGroupChat(rhId: Long?, groupId: Long) = openChat(secondaryChatsCtx = null, rhId, ChatType.Group, groupId) + +suspend fun openChat(secondaryChatsCtx: ChatModel.ChatsContext?, rhId: Long?, chatInfo: ChatInfo) = openChat(secondaryChatsCtx, rhId, chatInfo.chatType, chatInfo.apiId) + +suspend fun openChat( + secondaryChatsCtx: ChatModel.ChatsContext?, + rhId: Long?, + chatType: ChatType, + apiId: Long, + openAroundItemId: Long? = null +) { + if (secondaryChatsCtx != null) { + chatModel.secondaryChatsContext.value = secondaryChatsCtx + } + apiLoadMessages( + chatsCtx = secondaryChatsCtx ?: chatModel.chatsContext, + rhId, + chatType, + apiId, + if (openAroundItemId != null) { + ChatPagination.Around(openAroundItemId, ChatPagination.INITIAL_COUNT) + } else { + ChatPagination.Initial(ChatPagination.INITIAL_COUNT) + }, + "", + openAroundItemId + ) +} + +suspend fun openLoadedChat(chat: Chat) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.chatItemStatuses.clear() + chatModel.chatsContext.chatItems.replaceAll(chat.chatItems) + chatModel.chatId.value = chat.chatInfo.id + chatModel.chatsContext.chatState.clear() } } -suspend fun openGroupChat(rhId: Long?, groupId: Long, chatModel: ChatModel) { - val chat = chatModel.controller.apiGetChat(rhId, ChatType.Group, groupId) - if (chat != null) { - openLoadedChat(chat, chatModel) +suspend fun apiFindMessages(chatsCtx: ChatModel.ChatsContext, ch: Chat, search: String) { + withContext(Dispatchers.Main) { + chatsCtx.chatItems.clearAndNotify() } + apiLoadMessages(chatsCtx, ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, pagination = if (search.isNotEmpty()) ChatPagination.Last(ChatPagination.INITIAL_COUNT) else ChatPagination.Initial(ChatPagination.INITIAL_COUNT), search = search) } -suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) { - Log.d(TAG, "TODOCHAT: openChat: opening ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}") - val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId) - if (chat != null) { - openLoadedChat(chat, chatModel) - Log.d(TAG, "TODOCHAT: openChat: opened ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}") - } -} - -fun openLoadedChat(chat: Chat, chatModel: ChatModel) { - chatModel.chatItems.clear() - chatModel.chatItemStatuses.clear() - chatModel.chatItems.addAll(chat.chatItems) - chatModel.chatId.value = chat.chatInfo.id -} - -suspend fun apiLoadPrevMessages(ch: Chat, chatModel: ChatModel, beforeChatItemId: Long, search: String) { - val chatInfo = ch.chatInfo - val pagination = ChatPagination.Before(beforeChatItemId, ChatPagination.PRELOAD_COUNT) - val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, pagination, search) ?: return - if (chatModel.chatId.value != chat.id) return - chatModel.chatItems.addAll(0, chat.chatItems) -} - -suspend fun apiFindMessages(ch: Chat, chatModel: ChatModel, search: String) { - val chatInfo = ch.chatInfo - val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, search = search) ?: return - if (chatModel.chatId.value != chat.id) return - chatModel.chatItems.clear() - chatModel.chatItems.addAll(0, chat.chatItems) -} - -suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) { +suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) = coroutineScope { + // groupMembers loading can take a long time and if the user already closed the screen, coroutine may be canceled val groupMembers = chatModel.controller.apiListMembers(rhId, groupInfo.groupId) - val currentMembers = chatModel.groupMembers + val currentMembers = chatModel.groupMembers.value val newMembers = groupMembers.map { newMember -> val currentMember = currentMembers.find { it.id == newMember.id } val currentMemberStats = currentMember?.activeConn?.connectionStats @@ -235,21 +264,24 @@ suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatMo newMember } } - chatModel.groupMembers.clear() - chatModel.groupMembers.addAll(newMembers) + chatModel.groupMembersIndexes.value = emptyMap() + chatModel.groupMembers.value = newMembers + chatModel.membersLoaded.value = true + chatModel.populateGroupMembersIndexes() } @Composable fun ContactMenuItems(chat: Chat, contact: Contact, chatModel: ChatModel, showMenu: MutableState<Boolean>, showMarkRead: Boolean) { if (contact.activeConn != null) { if (showMarkRead) { - MarkReadChatAction(chat, chatModel, showMenu) + MarkReadChatAction(chat, showMenu) } else { MarkUnreadChatAction(chat, chatModel, showMenu) } ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu) - ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu) - ClearChatAction(chat, chatModel, showMenu) + ToggleNotificationsChatAction(chat, chatModel, contact.chatSettings.enableNtfs.nextMode(false), showMenu) + TagListAction(chat, showMenu) + ClearChatAction(chat, showMenu) } DeleteContactAction(chat, chatModel, showMenu) } @@ -282,13 +314,19 @@ fun GroupMenuItems( } else -> { if (showMarkRead) { - MarkReadChatAction(chat, chatModel, showMenu) + MarkReadChatAction(chat, showMenu) } else { MarkUnreadChatAction(chat, chatModel, showMenu) } ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu) - ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu) - ClearChatAction(chat, chatModel, showMenu) + ToggleNotificationsChatAction(chat, chatModel, groupInfo.chatSettings.enableNtfs.nextMode(true), showMenu) + TagListAction(chat, showMenu) + if (chat.chatStats.reportsCount > 0 && groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { + ArchiveAllReportsItemAction(showMenu) { + archiveAllReportsForMe(chat.remoteHostId, chat.chatInfo.apiId) + } + } + ClearChatAction(chat, showMenu) if (groupInfo.membership.memberCurrent) { LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu) } @@ -300,12 +338,22 @@ fun GroupMenuItems( } @Composable -fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) { +fun NoteFolderMenuItems(chat: Chat, showMenu: MutableState<Boolean>, showMarkRead: Boolean) { + if (showMarkRead) { + MarkReadChatAction(chat, showMenu) + } else { + MarkUnreadChatAction(chat, chatModel, showMenu) + } + ClearNoteFolderAction(chat, showMenu) +} + +@Composable +fun MarkReadChatAction(chat: Chat, showMenu: MutableState<Boolean>) { ItemAction( stringResource(MR.strings.mark_read), painterResource(MR.images.ic_check), onClick = { - markChatRead(chat, chatModel) + markChatRead(chat) ntfManager.cancelNotificationsForChat(chat.id) showMenu.value = false } @@ -324,37 +372,72 @@ fun MarkUnreadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableStat ) } +@Composable +fun TagListAction( + chat: Chat, + showMenu: MutableState<Boolean> +) { + val userTags = remember { chatModel.userTags } + ItemAction( + stringResource(if (chat.chatInfo.chatTags.isNullOrEmpty()) MR.strings.add_to_list else MR.strings.change_list), + painterResource(MR.images.ic_label), + onClick = { + ModalManager.start.showModalCloseable { close -> + if (userTags.value.isEmpty()) { + TagListEditor(rhId = chat.remoteHostId, chat = chat, close = close) + } else { + TagListView(rhId = chat.remoteHostId, chat = chat, close = close, reorderMode = false) + } + } + showMenu.value = false + } + ) +} + @Composable fun ToggleFavoritesChatAction(chat: Chat, chatModel: ChatModel, favorite: Boolean, showMenu: MutableState<Boolean>) { ItemAction( if (favorite) stringResource(MR.strings.unfavorite_chat) else stringResource(MR.strings.favorite_chat), if (favorite) painterResource(MR.images.ic_star_off) else painterResource(MR.images.ic_star), onClick = { - toggleChatFavorite(chat, !favorite, chatModel) + toggleChatFavorite(chat.remoteHostId, chat.chatInfo, !favorite, chatModel) showMenu.value = false } ) } @Composable -fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, ntfsEnabled: Boolean, showMenu: MutableState<Boolean>) { +fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, nextMsgFilter: MsgFilter, showMenu: MutableState<Boolean>) { ItemAction( - if (ntfsEnabled) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat), - if (ntfsEnabled) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications), + generalGetString(nextMsgFilter.text(chat.chatInfo.hasMentions)), + painterResource(nextMsgFilter.icon), onClick = { - toggleNotifications(chat, !ntfsEnabled, chatModel) + toggleNotifications(chat.remoteHostId, chat.chatInfo, nextMsgFilter, chatModel) showMenu.value = false } ) } @Composable -fun ClearChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) { +fun ClearChatAction(chat: Chat, showMenu: MutableState<Boolean>) { ItemAction( stringResource(MR.strings.clear_chat_menu_action), painterResource(MR.images.ic_settings_backup_restore), onClick = { - clearChatDialog(chat, chatModel) + clearChatDialog(chat) + showMenu.value = false + }, + color = WarningOrange + ) +} + +@Composable +fun ClearNoteFolderAction(chat: Chat, showMenu: MutableState<Boolean>) { + ItemAction( + stringResource(MR.strings.clear_chat_menu_action), + painterResource(MR.images.ic_settings_backup_restore), + onClick = { + clearNoteFolderDialog(chat) showMenu.value = false }, color = WarningOrange @@ -396,7 +479,7 @@ fun JoinGroupAction( inProgress: MutableState<Boolean> ) { val joinGroup: () -> Unit = { - withApi { + withBGApi { inProgress.value = true chatModel.controller.apiJoinGroup(chat.remoteHostId, groupInfo.groupId) inProgress.value = false @@ -427,13 +510,13 @@ fun LeaveGroupAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, sh } @Composable -fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState<Boolean>) { +fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState<Boolean>, onSuccess: ((chat: Chat) -> Unit)? = null) { ItemAction( stringResource(MR.strings.accept_contact_button), painterResource(MR.images.ic_check), color = MaterialTheme.colors.onBackground, onClick = { - acceptContactRequest(rhId, incognito = false, chatInfo.apiId, chatInfo, true, chatModel) + acceptContactRequest(rhId, incognito = false, chatInfo.apiId, chatInfo, true, chatModel, onSuccess) showMenu.value = false } ) @@ -442,7 +525,7 @@ fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chat painterResource(MR.images.ic_theater_comedy), color = MaterialTheme.colors.onBackground, onClick = { - acceptContactRequest(rhId, incognito = true, chatInfo.apiId, chatInfo, true, chatModel) + acceptContactRequest(rhId, incognito = true, chatInfo.apiId, chatInfo, true, chatModel, onSuccess) showMenu.value = false } ) @@ -466,7 +549,7 @@ fun ContactConnectionMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactConnection ModalManager.center.closeModals() ModalManager.end.closeModals() ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close -> - ContactConnectionInfoView(chatModel, rhId, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, true, close) + ContactConnectionInfoView(chatModel, rhId, chatInfo.contactConnection.connLinkInv, chatInfo.contactConnection, true, close) } showMenu.value = false }, @@ -476,10 +559,7 @@ fun ContactConnectionMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactConnection painterResource(MR.images.ic_delete), onClick = { deleteContactConnectionAlert(rhId, chatInfo.contactConnection, chatModel) { - if (chatModel.chatId.value == null) { - ModalManager.center.closeModals() - ModalManager.end.closeModals() - } + chatModel.dismissConnReqView(chatInfo.contactConnection.id) } showMenu.value = false }, @@ -510,17 +590,32 @@ private fun InvalidDataView() { } } -fun markChatRead(c: Chat, chatModel: ChatModel) { +@Composable +private fun ArchiveAllReportsItemAction(showMenu: MutableState<Boolean>, archiveReports: () -> Unit) { + ItemAction( + stringResource(MR.strings.archive_reports), + painterResource(MR.images.ic_inventory_2), + onClick = { + showArchiveAllReportsForMeAlert(archiveReports) + showMenu.value = false + } + ) +} + +fun markChatRead(c: Chat) { var chat = c withApi { if (chat.chatStats.unreadCount > 0) { - val minUnreadItemId = chat.chatStats.minUnreadItemId - chatModel.markChatItemsRead(chat) + withContext(Dispatchers.Main) { + chatModel.chatsContext.markChatItemsRead(chat.remoteHostId, chat.chatInfo.id) + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.markChatItemsRead(chat.remoteHostId, chat.chatInfo.id) + } chatModel.controller.apiChatRead( chat.remoteHostId, chat.chatInfo.chatType, - chat.chatInfo.apiId, - CC.ItemRange(minUnreadItemId, chat.chatItems.last().id) + chat.chatInfo.apiId ) chat = chatModel.getChat(chat.id) ?: return@withApi } @@ -532,7 +627,10 @@ fun markChatRead(c: Chat, chatModel: ChatModel) { false ) if (success) { - chatModel.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))) + withContext(Dispatchers.Main) { + chatModel.chatsContext.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))) + chatModel.chatsContext.markChatTagRead(chat) + } } } } @@ -543,6 +641,7 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) { if (chat.chatStats.unreadChat) return withApi { + val wasUnread = chat.unreadTag val success = chatModel.controller.apiChatUnread( chat.remoteHostId, chat.chatInfo.chatType, @@ -550,12 +649,15 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) { true ) if (success) { - chatModel.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true))) + withContext(Dispatchers.Main) { + chatModel.chatsContext.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true))) + chatModel.chatsContext.updateChatTagReadNoContentTag(chat, wasUnread) + } } } } -fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) { +fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel, onSucess: ((chat: Chat) -> Unit)? = null) { AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.accept_connection_request__question), text = AnnotatedString(generalGetString(MR.strings.if_you_choose_to_reject_the_sender_will_not_be_notified)), @@ -563,13 +665,13 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque Column { SectionItemView({ AlertManager.shared.hideAlert() - acceptContactRequest(rhId, incognito = false, contactRequest.apiId, contactRequest, true, chatModel) + acceptContactRequest(rhId, incognito = false, contactRequest.apiId, contactRequest, true, chatModel, onSucess) }) { Text(generalGetString(MR.strings.accept_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } SectionItemView({ AlertManager.shared.hideAlert() - acceptContactRequest(rhId, incognito = true, contactRequest.apiId, contactRequest, true, chatModel) + acceptContactRequest(rhId, incognito = true, contactRequest.apiId, contactRequest, true, chatModel, onSucess) }) { Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } @@ -585,20 +687,26 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque ) } -fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel) { - withApi { +fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel, close: ((chat: Chat) -> Unit)? = null ) { + withBGApi { val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, apiId) if (contact != null && isCurrentUser && contactRequest != null) { val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf()) - chatModel.replaceChat(rhId, contactRequest.id, chat) + withContext(Dispatchers.Main) { + chatModel.chatsContext.replaceChat(rhId, contactRequest.id, chat) + } + chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected()) + close?.invoke(chat) } } } fun rejectContactRequest(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) { - withApi { + withBGApi { chatModel.controller.apiRejectContactRequest(rhId, contactRequest.apiId) - chatModel.removeChat(rhId, contactRequest.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(rhId, contactRequest.id) + } } } @@ -611,10 +719,12 @@ fun deleteContactConnectionAlert(rhId: Long?, connection: PendingContactConnecti ), confirmText = generalGetString(MR.strings.delete_verb), onConfirm = { - withApi { + withBGApi { AlertManager.shared.hideAlert() if (chatModel.controller.apiDeleteChat(rhId, ChatType.ContactConnection, connection.apiId)) { - chatModel.removeChat(rhId, connection.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(rhId, connection.id) + } onSuccess() } } @@ -630,10 +740,12 @@ fun pendingContactAlertDialog(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatMo text = generalGetString(MR.strings.alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry), confirmText = generalGetString(MR.strings.button_delete_contact), onConfirm = { - withApi { + withBGApi { val r = chatModel.controller.apiDeleteChat(rhId, chatInfo.chatType, chatInfo.apiId) if (r) { - chatModel.removeChat(rhId, chatInfo.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(rhId, chatInfo.id) + } if (chatModel.chatId.value == chatInfo.id) { chatModel.chatId.value = null ModalManager.end.closeModals() @@ -659,11 +771,11 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( Column { SectionItemView({ AlertManager.privacySensitive.hideAlert() - withApi { + withBGApi { close?.invoke() val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = false) if (ok && openChat) { - openDirectChat(rhId, contact.contactId, chatModel) + openDirectChat(rhId, contact.contactId) } } }) { @@ -671,11 +783,11 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( } SectionItemView({ AlertManager.privacySensitive.hideAlert() - withApi { + withBGApi { close?.invoke() val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = true) if (ok && openChat) { - openDirectChat(rhId, contact.contactId, chatModel) + openDirectChat(rhId, contact.contactId) } } }) { @@ -695,7 +807,9 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( suspend fun connectContactViaAddress(chatModel: ChatModel, rhId: Long?, contactId: Long, incognito: Boolean): Boolean { val contact = chatModel.controller.apiConnectContactViaAddress(rhId, incognito, contactId) if (contact != null) { - chatModel.updateContact(rhId, contact) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, contact) + } AlertManager.privacySensitive.showAlertMsg( title = generalGetString(MR.strings.connection_request_sent), text = generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted), @@ -712,7 +826,7 @@ fun acceptGroupInvitationAlertDialog(rhId: Long?, groupInfo: GroupInfo, chatMode text = generalGetString(MR.strings.you_are_invited_to_group_join_to_connect_with_group_members), confirmText = if (groupInfo.membership.memberIncognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), onConfirm = { - withApi { + withBGApi { inProgress?.value = true chatModel.controller.apiJoinGroup(rhId, groupInfo.groupId) inProgress?.value = false @@ -733,10 +847,12 @@ fun cantInviteIncognitoAlert() { } fun deleteGroup(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) { - withApi { + withBGApi { val r = chatModel.controller.apiDeleteChat(rhId, ChatType.Group, groupInfo.apiId) if (r) { - chatModel.removeChat(rhId, groupInfo.id) + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(rhId, groupInfo.id) + } if (chatModel.chatId.value == groupInfo.id) { chatModel.chatId.value = null ModalManager.end.closeModals() @@ -754,57 +870,89 @@ fun groupInvitationAcceptedAlert(rhId: Long?) { ) } -fun toggleNotifications(chat: Chat, enableAllNtfs: Boolean, chatModel: ChatModel, currentState: MutableState<Boolean>? = null) { - val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(enableNtfs = if (enableAllNtfs) MsgFilter.All else MsgFilter.None) - updateChatSettings(chat, chatSettings, chatModel, currentState) +fun toggleNotifications(remoteHostId: Long?, chatInfo: ChatInfo, filter: MsgFilter, chatModel: ChatModel, currentState: MutableState<MsgFilter>? = null) { + val chatSettings = (chatInfo.chatSettings ?: ChatSettings.defaults).copy(enableNtfs = filter) + updateChatSettings(remoteHostId, chatInfo, chatSettings, chatModel, currentState) } -fun toggleChatFavorite(chat: Chat, favorite: Boolean, chatModel: ChatModel) { - val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(favorite = favorite) - updateChatSettings(chat, chatSettings, chatModel) +fun toggleChatFavorite(remoteHostId: Long?, chatInfo: ChatInfo, favorite: Boolean, chatModel: ChatModel) { + val chatSettings = (chatInfo.chatSettings ?: ChatSettings.defaults).copy(favorite = favorite) + updateChatSettings(remoteHostId, chatInfo, chatSettings, chatModel) } -fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatModel, currentState: MutableState<Boolean>? = null) { - val newChatInfo = when(chat.chatInfo) { - is ChatInfo.Direct -> with (chat.chatInfo) { +fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: ChatSettings, chatModel: ChatModel, currentState: MutableState<MsgFilter>? = null) { + val newChatInfo = when(chatInfo) { + is ChatInfo.Direct -> with (chatInfo) { ChatInfo.Direct(contact.copy(chatSettings = chatSettings)) } - is ChatInfo.Group -> with(chat.chatInfo) { + is ChatInfo.Group -> with(chatInfo) { ChatInfo.Group(groupInfo.copy(chatSettings = chatSettings)) } else -> null } - withApi { + withBGApi { val res = when (newChatInfo) { is ChatInfo.Direct -> with(newChatInfo) { - chatModel.controller.apiSetSettings(chat.remoteHostId, chatType, apiId, contact.chatSettings) + chatModel.controller.apiSetSettings(remoteHostId, chatType, apiId, contact.chatSettings) } is ChatInfo.Group -> with(newChatInfo) { - chatModel.controller.apiSetSettings(chat.remoteHostId, chatType, apiId, groupInfo.chatSettings) + chatModel.controller.apiSetSettings(remoteHostId, chatType, apiId, groupInfo.chatSettings) } else -> false } if (res && newChatInfo != null) { - chatModel.updateChatInfo(chat.remoteHostId, newChatInfo) - if (chatSettings.enableNtfs != MsgFilter.All) { - ntfManager.cancelNotificationsForChat(chat.id) + val chat = chatModel.getChat(chatInfo.id) + val wasUnread = chat?.unreadTag ?: false + val wasFavorite = chatInfo.chatSettings?.favorite ?: false + chatModel.updateChatFavorite(favorite = chatSettings.favorite, wasFavorite) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatInfo(remoteHostId, newChatInfo) + } + if (chatSettings.enableNtfs == MsgFilter.None) { + ntfManager.cancelNotificationsForChat(chatInfo.id) + } + val updatedChat = chatModel.getChat(chatInfo.id) + if (updatedChat != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatTagReadNoContentTag(updatedChat, wasUnread) + } } val current = currentState?.value if (current != null) { - currentState.value = !current + currentState.value = chatSettings.enableNtfs } } } } +private fun showArchiveAllReportsForMeAlert(archiveReports: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.report_archive_alert_title_all), + text = generalGetString(MR.strings.report_archive_alert_desc_all), + onConfirm = archiveReports, + destructive = true, + confirmText = generalGetString(MR.strings.archive_verb), + ) +} + +private fun archiveAllReportsForMe(chatRh: Long?, apiId: Long) { + withBGApi { + val r = chatModel.controller.apiArchiveReceivedReports(chatRh, apiId) + if (r != null) { + controller.groupChatItemsDeleted(chatRh, r) + } + } +} + @Composable expect fun ChatListNavLinkLayout( chatLinkPreview: @Composable () -> Unit, click: () -> Unit, dropdownMenuItems: (@Composable () -> Unit)?, showMenu: MutableState<Boolean>, - stopped: Boolean, - selectedChat: State<Boolean> + disabled: Boolean, + selectedChat: State<Boolean>, + nextChatSelected: State<Boolean>, ) @Preview/*( @@ -836,17 +984,19 @@ fun PreviewChatListNavLinkDirect() { null, null, null, - stopped = false, + disabled = false, linkMode = SimplexLinkMode.DESCRIPTION, inProgress = false, - progressByTimeout = false + progressByTimeout = false, + {} ) }, click = {}, dropdownMenuItems = null, showMenu = remember { mutableStateOf(false) }, - stopped = false, - selectedChat = remember { mutableStateOf(false) } + disabled = false, + selectedChat = remember { mutableStateOf(false) }, + nextChatSelected = remember { mutableStateOf(false) }, ) } } @@ -880,17 +1030,19 @@ fun PreviewChatListNavLinkGroup() { null, null, null, - stopped = false, + disabled = false, linkMode = SimplexLinkMode.DESCRIPTION, inProgress = false, - progressByTimeout = false + progressByTimeout = false, + {} ) }, click = {}, dropdownMenuItems = null, showMenu = remember { mutableStateOf(false) }, - stopped = false, - selectedChat = remember { mutableStateOf(false) } + disabled = false, + selectedChat = remember { mutableStateOf(false) }, + nextChatSelected = remember { mutableStateOf(false) }, ) } } @@ -910,8 +1062,9 @@ fun PreviewChatListNavLinkContactRequest() { click = {}, dropdownMenuItems = null, showMenu = remember { mutableStateOf(false) }, - stopped = false, - selectedChat = remember { mutableStateOf(false) } + disabled = false, + selectedChat = remember { mutableStateOf(false) }, + nextChatSelected = remember { mutableStateOf(false) }, ) } } 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 cf12727d74..87c02f038c 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 @@ -1,6 +1,8 @@ package chat.simplex.common.views.chatlist import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* import androidx.compose.foundation.shape.CircleShape @@ -8,146 +10,318 @@ 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.* +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.* import androidx.compose.ui.graphics.* import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.platform.* +import androidx.compose.ui.text.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* -import chat.simplex.common.SettingsViewState +import chat.simplex.common.AppLock import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.onboarding.WhatsNewView -import chat.simplex.common.views.onboarding.shouldShowWhatsNew -import chat.simplex.common.views.usersettings.SettingsView import chat.simplex.common.platform.* +import chat.simplex.common.views.call.Call +import chat.simplex.common.views.chat.item.* +import chat.simplex.common.views.chat.topPaddingToContent import chat.simplex.common.views.newchat.* +import chat.simplex.common.views.onboarding.* +import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR +import dev.icerock.moko.resources.ImageResource +import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow -import java.net.URI +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.serialization.json.Json +import kotlin.time.Duration.Companion.seconds -@Composable -fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerformLA: (Boolean) -> Unit, stopped: Boolean) { - val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) } - val showNewChatSheet = { - newChatSheetState.value = AnimatedViewState.VISIBLE - } - val hideNewChatSheet: (animated: Boolean) -> Unit = { animated -> - if (animated) newChatSheetState.value = AnimatedViewState.HIDING - else newChatSheetState.value = AnimatedViewState.GONE - } - LaunchedEffect(Unit) { - if (shouldShowWhatsNew(chatModel)) { - delay(1000L) - ModalManager.center.showCustomModal { close -> WhatsNewView(close = close) } +enum class PresetTagKind { GROUP_REPORTS, FAVORITES, CONTACTS, GROUPS, BUSINESS, NOTES } + +sealed class ActiveFilter { + data class PresetTag(val tag: PresetTagKind) : ActiveFilter() + data class UserTag(val tag: ChatTag) : ActiveFilter() + data object Unread: ActiveFilter() +} + +private fun showNewChatSheet(oneHandUI: State<Boolean>) { + ModalManager.start.closeModals() + ModalManager.end.closeModals() + chatModel.newChatSheetVisible.value = true + ModalManager.start.showCustomModal { close -> + val close = { + // It will set it faster than in onDispose. It's important to catch the actual state before + // closing modal for reacting with status bar changes in [App] + chatModel.newChatSheetVisible.value = false + close() } - } - LaunchedEffect(chatModel.clearOverlays.value) { - if (chatModel.clearOverlays.value && newChatSheetState.value.isVisible()) hideNewChatSheet(false) - } - if (appPlatform.isDesktop) { - KeyChangeEffect(chatModel.chatId.value) { - if (chatModel.chatId.value != null) { - ModalManager.end.closeModalsExceptFirst() - } - AudioPlayer.stop() - VideoPlayerHolder.stopAll() - } - } - val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp - var searchInList by rememberSaveable { mutableStateOf("") } - val scope = rememberCoroutineScope() - val (userPickerState, scaffoldState ) = settingsState - Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } } }, - scaffoldState = scaffoldState, - drawerContent = { - tryOrShowError("Settings", error = { ErrorSettingsView() }) { - SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) - } - }, - drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f), - drawerGesturesEnabled = appPlatform.isAndroid, - floatingActionButton = { - if (searchInList.isEmpty() && !chatModel.desktopNoUserNoRemote) { - FloatingActionButton( - onClick = { - if (!stopped) { - if (newChatSheetState.value.isVisible()) hideNewChatSheet(true) else showNewChatSheet() - } - }, - Modifier.padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = DEFAULT_PADDING - 16.dp), - elevation = FloatingActionButtonDefaults.elevation( - defaultElevation = 0.dp, - pressedElevation = 0.dp, - hoveredElevation = 0.dp, - focusedElevation = 0.dp, - ), - backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, - contentColor = Color.White - ) { - Icon(if (!newChatSheetState.collectAsState().value.isVisible()) painterResource(MR.images.ic_edit_filled) else painterResource(MR.images.ic_close), stringResource(MR.strings.add_contact_or_create_group)) + ModalView(close, showAppBar = !oneHandUI.value) { + if (appPlatform.isAndroid) { + BackHandler { + close() } } - } - ) { - Box(Modifier.padding(it).padding(end = endPadding)) { - Column( - modifier = Modifier - .fillMaxSize() - ) { - if (chatModel.chats.isNotEmpty()) { - ChatList(chatModel, search = searchInList) - } else if (!chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { - Box(Modifier.fillMaxSize()) { - if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) { - OnboardingButtons(showNewChatSheet) - } - Text(stringResource(MR.strings.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary) - } + NewChatSheet(rh = chatModel.currentRemoteHost.value, close) + DisposableEffect(Unit) { + onDispose { + chatModel.newChatSheetVisible.value = false } } } } - if (searchInList.isEmpty()) { - DesktopActiveCallOverlayLayout(newChatSheetState) - // TODO disable this button and sheet for the duration of the switch - tryOrShowError("NewChatSheet", error = {}) { - NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet) - } - } - if (appPlatform.isAndroid) { - tryOrShowError("UserPicker", error = {}) { - UserPicker(chatModel, userPickerState) { - scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } - userPickerState.value = AnimatedViewState.GONE - } - } - } } @Composable -private fun OnboardingButtons(openNewChatSheet: () -> Unit) { - Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Bottom) { - ConnectButton(generalGetString(MR.strings.tap_to_start_new_chat), openNewChatSheet) - val color = MaterialTheme.colors.primaryVariant - Canvas(modifier = Modifier.width(40.dp).height(10.dp), onDraw = { - val trianglePath = Path().apply { - moveTo(0.dp.toPx(), 0f) - lineTo(16.dp.toPx(), 0.dp.toPx()) - lineTo(8.dp.toPx(), 10.dp.toPx()) - lineTo(0.dp.toPx(), 0.dp.toPx()) - } - drawPath( - color = color, - path = trianglePath +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), ) - }) - Spacer(Modifier.height(62.dp)) + } + ) { + 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 + ) + } + } + } +} + +@Composable +fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, setPerformLA: (Boolean) -> Unit, stopped: Boolean) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + + LaunchedEffect(Unit) { + val showWhatsNew = shouldShowWhatsNew(chatModel) + val showUpdatedConditions = chatModel.conditions.value.conditionsAction?.shouldShowNotice ?: false + if (showWhatsNew || showUpdatedConditions) { + delay(1000L) + ModalManager.center.showCustomModal { close -> WhatsNewView(close = close, updatedConditions = showUpdatedConditions) } + } + } + + if (appPlatform.isDesktop) { + KeyChangeEffect(chatModel.chatId.value) { + if (chatModel.chatId.value != null && !ModalManager.end.isLastModalOpen(ModalViewId.SECONDARY_CHAT)) { + ModalManager.end.closeModalsExceptFirst() + } + AudioPlayer.stop() + VideoPlayerHolder.stopAll() + } + } + val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } + val listState = rememberLazyListState(lazyListState.first, lazyListState.second) + Box(Modifier.fillMaxSize()) { + if (oneHandUI.value) { + ChatListWithLoadingScreen(searchText, listState) + Column(Modifier.align(Alignment.BottomCenter)) { + ChatListToolbar( + userPickerState, + listState, + stopped, + setPerformLA, + ) + } + } else { + ChatListWithLoadingScreen(searchText, listState) + Column { + ChatListToolbar( + userPickerState, + listState, + stopped, + setPerformLA, + ) + } + if (searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) { + NewChatSheetFloatingButton(oneHandUI, stopped) + } + } + } + + if (searchText.value.text.isEmpty()) { + if (appPlatform.isDesktop && !oneHandUI.value) { + val call = remember { chatModel.activeCall }.value + if (call != null) { + ActiveCallInteractiveArea(call) + } + } + } + if (appPlatform.isAndroid) { + val wasAllowedToSetupNotifications = rememberSaveable { mutableStateOf(false) } + val canEnableNotifications = remember { derivedStateOf { chatModel.chatRunning.value == true } } + if (wasAllowedToSetupNotifications.value || canEnableNotifications.value) { + SetNotificationsModeAdditions() + LaunchedEffect(Unit) { wasAllowedToSetupNotifications.value = true } + } + tryOrShowError("UserPicker", error = {}) { + UserPicker( + chatModel = chatModel, + userPickerState = userPickerState, + setPerformLA = AppLock::setPerformLA + ) + } + } +} + +@Composable +private fun ChatListCard( + close: () -> Unit, + onCardClick: (() -> Unit)? = null, + content: @Composable BoxScope.() -> Unit +) { + Column( + modifier = Modifier.clip(RoundedCornerShape(18.dp)) + ) { + Box( + modifier = Modifier + .background(MaterialTheme.appColors.sentMessage) + .clickable { + onCardClick?.invoke() + } + ) { + Box( + modifier = Modifier.fillMaxWidth().matchParentSize().padding(5.dp), + contentAlignment = Alignment.TopEnd + ) { + IconButton( + onClick = { + close() + } + ) { + Icon( + painterResource(MR.images.ic_close), stringResource(MR.strings.back), tint = MaterialTheme.colors.secondary + ) + } + } + content() + } + } +} + +@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), + ) + }, + onCardClick = { + ModalManager.start.showModal { + UserAddressLearnMore(showCreateAddressButton = true) + } + } + ) { + 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) + } + } + Row( + Modifier + .fillMaxWidth() + .padding(DEFAULT_PADDING), + verticalAlignment = Alignment.CenterVertically + ) { + 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 = 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, + ) + } + } + } + } +} + +@Composable +private fun BoxScope.ChatListWithLoadingScreen(searchText: MutableState<TextFieldValue>, listState: LazyListState) { + 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 + ) + } +} + +@Composable +private fun BoxScope.NewChatSheetFloatingButton(oneHandUI: State<Boolean>, stopped: Boolean) { + FloatingActionButton( + onClick = { + if (!stopped) { + showNewChatSheet(oneHandUI) + } + }, + Modifier + .navigationBarsPadding() + .padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING) + .align(Alignment.BottomEnd) + .size(AppBarHeight * fontSizeSqrtMultiplier), + elevation = FloatingActionButtonDefaults.elevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp, + hoveredElevation = 0.dp, + focusedElevation = 0.dp, + ), + backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + contentColor = Color.White + ) { + Icon(painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(22.dp * fontSizeSqrtMultiplier)) } } @@ -168,21 +342,69 @@ private fun ConnectButton(text: String, onClick: () -> Unit) { } @Composable -private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, onSearchValueChanged: (String) -> Unit) { - var showSearch by rememberSaveable { mutableStateOf(false) } - val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false } - if (showSearch) { - BackHandler(onBack = hideSearchOnBack) - } +private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>, listState: LazyListState, stopped: Boolean, setPerformLA: (Boolean) -> Unit) { + val serversSummary: MutableState<PresentedServersSummary?> = remember { mutableStateOf(null) } val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() - if (chatModel.chats.size > 0) { - barButtons.add { - IconButton({ showSearch = true }) { - Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary) + val updatingProgress = remember { chatModel.updatingProgress }.value + val oneHandUI = remember { appPrefs.oneHandUI.state } + + if (oneHandUI.value) { + val sp16 = with(LocalDensity.current) { 16.sp.toDp() } + + if (appPlatform.isDesktop && oneHandUI.value) { + val call = remember { chatModel.activeCall } + if (call.value != null) { + barButtons.add { + val c = call.value + if (c != null) { + ActiveCallInteractiveArea(c) + Spacer(Modifier.width(5.dp)) + } + } + } + } + if (!stopped) { + barButtons.add { + IconButton( + onClick = { + showNewChatSheet(oneHandUI) + }, + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .background(MaterialTheme.colors.primary, shape = CircleShape) + .size(33.dp * fontSizeSqrtMultiplier) + ) { + Icon( + painterResource(MR.images.ic_edit_filled), + stringResource(MR.strings.add_contact_or_create_group), + Modifier.size(sp16), + tint = Color.White + ) + } + } } } } - if (stopped) { + + if (updatingProgress != null) { + barButtons.add { + val interactionSource = remember { MutableInteractionSource() } + val hovered = interactionSource.collectIsHoveredAsState().value + IconButton(onClick = { + chatModel.updatingRequest?.close() + }, Modifier.hoverable(interactionSource)) { + if (hovered) { + Icon(painterResource(MR.images.ic_close), null, tint = WarningOrange) + } else if (updatingProgress == -1f) { + CIFileViewScope.progressIndicator() + } else { + CIFileViewScope.progressCircle((updatingProgress * 100).toLong(), 100) + } + } + } + } else if (stopped) { barButtons.add { IconButton(onClick = { AlertManager.shared.showAlertMsg( @@ -198,45 +420,96 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user } } } + val clipboard = LocalClipboardManager.current val scope = rememberCoroutineScope() - DefaultTopAppBar( + val canScrollToZero = remember { derivedStateOf { listState.firstVisibleItemIndex != 0 || listState.firstVisibleItemScrollOffset != 0 } } + DefaultAppBar( navigationButton = { - if (showSearch) { - NavigationButtonBack(hideSearchOnBack) - } else if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) { - NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } } + if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) { + NavigationButtonMenu { + ModalManager.start.showModalCloseable { close -> + SettingsView(chatModel, setPerformLA, close) + } + } } else { val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } } val allRead = users .filter { u -> !u.user.activeUser && !u.user.hidden } .all { u -> u.unreadCount == 0 } UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) { - if (users.size == 1 && chatModel.remoteHosts.isEmpty()) { - scope.launch { drawerState.open() } - } else { userPickerState.value = AnimatedViewState.VISIBLE - } } } }, title = { - Row(verticalAlignment = Alignment.CenterVertically) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON)) { Text( stringResource(MR.strings.your_chats), color = MaterialTheme.colors.onBackground, fontWeight = FontWeight.SemiBold, ) - if (chatModel.chats.size > 0) { - ToggleFilterButton() - } + 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) + } + } + } + ) { ServersSummaryView(chatModel.currentRemoteHost.value, serversSummary) } + } + ) } }, - onTitleClick = null, - showSearch = showSearch, - onSearchValueChanged = onSearchValueChanged, - buttons = barButtons + onTitleClick = if (canScrollToZero.value) { { scrollToBottom(scope, listState) } } else null, + onTop = !oneHandUI.value, + onSearchValueChanged = {}, + buttons = { barButtons.forEach { it() } } ) - Divider(Modifier.padding(top = AppBarHeight)) +} + +@Composable +fun SubscriptionStatusIndicator(click: (() -> Unit)) { + var subs by remember { mutableStateOf(SMPServerSubs.newSMPServerSubs) } + var hasSess by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + + suspend fun setSubsTotal() { + if (chatModel.currentUser.value != null && chatModel.controller.hasChatCtrl() && chatModel.chatRunning.value == true) { + val r = chatModel.controller.getAgentSubsTotal(chatModel.remoteHostId()) + if (r != null) { + subs = r.first + hasSess = r.second + } + } + } + + LaunchedEffect(Unit) { + setSubsTotal() + scope.launch { + while (isActive) { + delay(1.seconds) + if ((appPlatform.isDesktop || chatModel.chatId.value == null) && !ModalManager.start.hasModalsOpen() && !ModalManager.fullscreen.hasModalsOpen() && isAppVisibleAndFocused()) { + setSubsTotal() + } + } + } + } + + SimpleButtonFrame( + click = click, + disabled = chatModel.chatRunning.value != true + ) { + SubscriptionStatusIndicatorView(subs = subs, hasSess = hasSess) + } } @Composable @@ -246,7 +519,8 @@ fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> U Box { ProfileImage( image = image, - size = 37.dp + size = 37.dp * fontSizeSqrtMultiplier, + color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f) ) if (!allRead) { unreadBadge() @@ -265,6 +539,7 @@ fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> U } } + @Composable private fun BoxScope.unreadBadge(text: String? = "") { Text( @@ -281,37 +556,136 @@ private fun BoxScope.unreadBadge(text: String? = "") { } @Composable -private fun ToggleFilterButton() { - val pref = remember { ChatController.appPrefs.showUnreadAndFavorites } - IconButton(onClick = { pref.set(!pref.get()) }) { +private fun ToggleFilterEnabledButton() { + val showUnread = remember { chatModel.activeChatTagFilter }.value == ActiveFilter.Unread + + IconButton(onClick = { + if (showUnread) { + chatModel.activeChatTagFilter.value = null + } else { + chatModel.activeChatTagFilter.value = ActiveFilter.Unread + } + }) { + val sp16 = with(LocalDensity.current) { 16.sp.toDp() } Icon( painterResource(MR.images.ic_filter_list), null, - tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.primary, + tint = if (showUnread) MaterialTheme.colors.background else MaterialTheme.colors.secondary, modifier = Modifier .padding(3.dp) - .background(color = if (pref.state.value) MaterialTheme.colors.primary else MaterialTheme.colors.background, shape = RoundedCornerShape(50)) - .border(width = 1.dp, color = MaterialTheme.colors.primary, shape = RoundedCornerShape(50)) + .background(color = if (showUnread) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50)) + .border(width = 1.dp, color = if (showUnread) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50)) .padding(3.dp) - .size(16.dp) + .size(sp16) ) } } @Composable -expect fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>) +expect fun ActiveCallInteractiveArea(call: Call) -fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) { +fun connectIfOpenedViaUri(rhId: Long?, uri: String, chatModel: ChatModel) { Log.d(TAG, "connectIfOpenedViaUri: opened via link") if (chatModel.currentUser.value == null) { chatModel.appOpenUrl.value = rhId to uri } else { - withApi { - planAndConnect(chatModel, rhId, uri, incognito = null, close = null) + withBGApi { + planAndConnect(rhId, uri, incognito = null, close = null) } } } +@Composable +private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>) { + Box { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + val focusRequester = remember { FocusRequester() } + var focused by remember { mutableStateOf(false) } + Icon( + painterResource(MR.images.ic_search), + contentDescription = null, + Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(22.dp * fontSizeSqrtMultiplier), + tint = MaterialTheme.colors.secondary + ) + SearchTextField( + Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester), + placeholder = stringResource(MR.strings.search_or_paste_simplex_link), + alwaysVisible = true, + searchText = searchText, + enabled = !remember { searchShowingSimplexLink }.value, + trailingContent = null, + ) { + searchText.value = searchText.value.copy(it) + } + val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } } + if (hasText.value) { + val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() } + BackHandler(onBack = hideSearchOnBack) + KeyChangeEffect(chatModel.currentRemoteHost.value) { + hideSearchOnBack() + } + } else { + val padding = if (appPlatform.isDesktop) 0.dp else 7.dp + if (chatModel.chats.value.isNotEmpty()) { + ToggleFilterEnabledButton() + } + Spacer(Modifier.width(padding)) + } + val focusManager = LocalFocusManager.current + val keyboardState = getKeyboardState() + LaunchedEffect(keyboardState.value) { + if (keyboardState.value == KeyboardState.Closed && focused) { + focusManager.clearFocus() + } + } + val view = LocalMultiplatformView() + LaunchedEffect(Unit) { + snapshotFlow { searchText.value.text } + .distinctUntilChanged() + .collect { + val link = strHasSingleSimplexLink(it.trim()) + if (link != null) { + // if SimpleX link is pasted, show connection dialogue + hideKeyboard(view) + if (link.format is Format.SimplexLink) { + val linkText = link.simplexLinkText(link.format.linkType, link.format.smpHosts) + searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero) + } + searchShowingSimplexLink.value = true + searchChatFilteredBySimplexLink.value = null + connect(link.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() } + } else if (!searchShowingSimplexLink.value || it.isEmpty()) { + if (it.isNotEmpty()) { + // if some other text is pasted, enter search mode + focusRequester.requestFocus() + } else if (listState.layoutInfo.totalItemsCount > 0) { + listState.scrollToItem(0) + } + searchShowingSimplexLink.value = false + searchChatFilteredBySimplexLink.value = null + } + } + } + } + val oneHandUI = remember { appPrefs.oneHandUI.state } + Divider(Modifier.align(if (oneHandUI.value) Alignment.TopStart else Alignment.BottomStart)) + } +} + +private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<String?>, cleanup: (() -> Unit)?) { + withBGApi { + planAndConnect( + chatModel.remoteHostId(), + link, + incognito = null, + filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id }, + filterKnownGroup = { searchChatFilteredBySimplexLink.value = it.id }, + close = null, + cleanup = cleanup, + ) + } +} + @Composable private fun ErrorSettingsView() { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { @@ -321,62 +695,558 @@ private fun ErrorSettingsView() { private var lazyListState = 0 to 0 +enum class ScrollDirection { + Up, Down, Idle +} + @Composable -private fun ChatList(chatModel: ChatModel, search: String) { - val listState = rememberLazyListState(lazyListState.first, lazyListState.second) +fun BoxScope.StatusBarBackground() { + if (appPlatform.isAndroid) { + val finalColor = MaterialTheme.colors.background.copy(0.88f) + Box(Modifier.fillMaxWidth().windowInsetsTopHeight(WindowInsets.statusBars).background(finalColor)) + } +} + +@Composable +fun BoxScope.NavigationBarBackground(appBarOnBottom: Boolean = false, mixedColor: Boolean, noAlpha: Boolean = false) { + if (appPlatform.isAndroid) { + val barPadding = WindowInsets.navigationBars.asPaddingValues() + val paddingBottom = barPadding.calculateBottomPadding() + val color = if (mixedColor) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) else MaterialTheme.colors.background + val finalColor = color.copy(if (noAlpha) 1f else if (appBarOnBottom) remember { appPrefs.inAppBarsAlpha.state }.value else 0.6f) + Box(Modifier.align(Alignment.BottomStart).height(paddingBottom).fillMaxWidth().background(finalColor)) + } +} + +@Composable +fun BoxScope.NavigationBarBackground(modifier: Modifier, color: Color = MaterialTheme.colors.background) { + val keyboardState = getKeyboardState() + if (appPlatform.isAndroid && keyboardState.value == KeyboardState.Closed) { + val barPadding = WindowInsets.navigationBars.asPaddingValues() + val paddingBottom = barPadding.calculateBottomPadding() + val finalColor = color.copy(0.6f) + Box(modifier.align(Alignment.BottomStart).height(paddingBottom).fillMaxWidth().background(finalColor)) + } +} + +@Composable +private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listState: LazyListState) { + var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) } + var previousIndex by remember { mutableStateOf(0) } + var previousScrollOffset by remember { mutableStateOf(0) } + val keyboardState by getKeyboardState() + val oneHandUI = remember { appPrefs.oneHandUI.state } + val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state } + val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state } + val activeFilter = remember { chatModel.activeChatTagFilter } + + LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) { + val currentIndex = listState.firstVisibleItemIndex + val currentScrollOffset = listState.firstVisibleItemScrollOffset + val threshold = 25 + + scrollDirection = when { + currentIndex > previousIndex -> ScrollDirection.Down + currentIndex < previousIndex -> ScrollDirection.Up + currentScrollOffset > previousScrollOffset + threshold -> ScrollDirection.Down + currentScrollOffset < previousScrollOffset - threshold -> ScrollDirection.Up + currentScrollOffset == previousScrollOffset -> ScrollDirection.Idle + else -> scrollDirection + } + + previousIndex = currentIndex + previousScrollOffset = currentScrollOffset + } + DisposableEffect(Unit) { onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } } - val showUnreadAndFavorites = remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value val allChats = remember { chatModel.chats } // In some not always reproducible situations this code produce IndexOutOfBoundsException on Compose's side // which is related to [derivedStateOf]. Using safe alternative instead // val chats by remember(search, showUnreadAndFavorites) { derivedStateOf { filteredChats(showUnreadAndFavorites, search, allChats.toList()) } } - val chats = filteredChats(showUnreadAndFavorites, search, allChats.toList()) - LazyColumn( - modifier = Modifier.fillMaxWidth(), - listState + val searchShowingSimplexLink = remember { mutableStateOf(false) } + val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) } + val chats = filteredChats(searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList(), activeFilter.value) + val topPaddingToContent = topPaddingToContent(false) + val blankSpaceSize = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else topPaddingToContent + LazyColumnWithScrollBar( + if (!oneHandUI.value) Modifier.imePadding() else Modifier, + listState, + reverseLayout = oneHandUI.value ) { - items(chats) { chat -> - ChatListNavLinkView(chat, chatModel) + item { Spacer(Modifier.height(blankSpaceSize)) } + stickyHeader { + Column( + Modifier + .zIndex(1f) + .offset { + val offsetMultiplier = if (oneHandUI.value) 1 else -1 + val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened) || scrollDirection == ScrollDirection.Up) { + if (listState.firstVisibleItemIndex == 0) -offsetMultiplier * listState.firstVisibleItemScrollOffset + else -offsetMultiplier * blankSpaceSize.roundToPx() + } else { + when (listState.firstVisibleItemIndex) { + 0 -> 0 + 1 -> offsetMultiplier * listState.firstVisibleItemScrollOffset + else -> offsetMultiplier * 1000 + } + } + IntOffset(0, y) + } + .background(MaterialTheme.colors.background) + ) { + if (oneHandUI.value) { + Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) { + Divider() + TagsView(searchText) + ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) + } + } else { + ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) + TagsView(searchText) + Divider() + } + } + } + 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) { + item { + ChatListFeatureCards() + } + } + if (appPlatform.isAndroid) { + item { Spacer(if (oneHandUI.value) Modifier.windowInsetsTopHeight(WindowInsets.statusBars) else Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) } } } - if (chats.isEmpty() && !chatModel.chats.isEmpty()) { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary) + if (chats.isEmpty() && chatModel.chats.value.isNotEmpty()) { + Box(Modifier.fillMaxSize().imePadding().padding(horizontal = DEFAULT_PADDING), contentAlignment = Alignment.Center) { + NoChatsView(searchText = searchText) + } + } + if (oneHandUI.value) { + StatusBarBackground() + } else { + NavigationBarBackground(oneHandUI.value, true) + } + if (!oneHandUICardShown.value) { + LaunchedEffect(chats.size) { + if (chats.size >= 3) { + appPrefs.oneHandUICardShown.set(true) + } + } + } + + if (!addressCreationCardShown.value) { + LaunchedEffect(chatModel.userAddress.value) { + if (chatModel.userAddress.value != null) { + appPrefs.addressCreationCardShown.set(true) + } + } + } + + LaunchedEffect(activeFilter.value) { + searchText.value = TextFieldValue("") + } +} + +@Composable +private fun NoChatsView(searchText: MutableState<TextFieldValue>) { + val activeFilter = remember { chatModel.activeChatTagFilter }.value + + if (searchText.value.text.isBlank()) { + when (activeFilter) { + is ActiveFilter.PresetTag -> Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) // this should not happen + is ActiveFilter.UserTag -> Text(String.format(generalGetString(MR.strings.no_chats_in_list), activeFilter.tag.chatTagText), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) + is ActiveFilter.Unread -> { + Row( + Modifier.clip(shape = CircleShape).clickable { chatModel.activeChatTagFilter.value = null }.padding(DEFAULT_PADDING_HALF), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painterResource(MR.images.ic_filter_list), + null, + tint = MaterialTheme.colors.secondary + ) + Text(generalGetString(MR.strings.no_unread_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) + } + } + null -> { + Text(generalGetString(MR.strings.no_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) + } + } + } else { + Text(generalGetString(MR.strings.no_chats_found), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) + } +} + +@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() } } } -private fun filteredChats(showUnreadAndFavorites: Boolean, searchText: String, chats: List<Chat>): List<Chat> { - val s = searchText.trim().lowercase() - return if (s.isEmpty() && !showUnreadAndFavorites) - chats - else { - chats.filter { chat -> - when (val cInfo = chat.chatInfo) { - is ChatInfo.Direct -> if (s.isEmpty()) { - filtered(chat) - } else { - (viewNameContains(cInfo, s) || - cInfo.contact.profile.displayName.lowercase().contains(s) || - cInfo.contact.fullName.lowercase().contains(s)) +private val TAG_MIN_HEIGHT = 35.dp + +@Composable +private fun TagsView(searchText: MutableState<TextFieldValue>) { + val userTags = remember { chatModel.userTags } + val presetTags = remember { chatModel.presetTags } + val collapsiblePresetTags = presetTags.filter { presetCanBeCollapsed(it.key) && it.value > 0 } + val alwaysShownPresetTags = presetTags.filter { !presetCanBeCollapsed(it.key) && it.value > 0 } + val activeFilter = remember { chatModel.activeChatTagFilter } + val unreadTags = remember { chatModel.unreadTags } + val rhId = chatModel.remoteHostId() + + val rowSizeModifier = Modifier.sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier) + + TagsRow { + if (collapsiblePresetTags.size > 1) { + if (collapsiblePresetTags.size + alwaysShownPresetTags.size + userTags.value.size <= 3) { + PresetTagKind.entries.filter { t -> (presetTags[t] ?: 0) > 0 }.forEach { tag -> + ExpandedTagFilterView(tag) } - is ChatInfo.Group -> if (s.isEmpty()) { - (filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited) - } else { - viewNameContains(cInfo, s) + } else { + CollapsedTagsFilterView(searchText) + alwaysShownPresetTags.forEach { tag -> + ExpandedTagFilterView(tag.key) } - is ChatInfo.ContactRequest -> s.isEmpty() || viewNameContains(cInfo, s) - is ChatInfo.ContactConnection -> s.isNotEmpty() && cInfo.contactConnection.localAlias.lowercase().contains(s) - is ChatInfo.InvalidJSON -> false + } + } + + userTags.value.forEach { tag -> + val current = when (val af = activeFilter.value) { + is ActiveFilter.UserTag -> af.tag == tag + else -> false + } + val interactionSource = remember { MutableInteractionSource() } + val showMenu = rememberSaveable { mutableStateOf(false) } + val saving = remember { mutableStateOf(false) } + Box { + Row( + rowSizeModifier + .clip(shape = CircleShape) + .combinedClickable( + onClick = { + if (chatModel.activeChatTagFilter.value == ActiveFilter.UserTag(tag)) { + chatModel.activeChatTagFilter.value = null + } else { + chatModel.activeChatTagFilter.value = ActiveFilter.UserTag(tag) + } + }, + onLongClick = { showMenu.value = true }, + interactionSource = interactionSource, + indication = LocalIndication.current, + enabled = !saving.value + ) + .onRightClick { showMenu.value = true } + .padding(4.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + if (tag.chatTagEmoji != null) { + ReactionIcon(tag.chatTagEmoji, fontSize = 14.sp) + } else { + Icon( + painterResource(if (current) MR.images.ic_label_filled else MR.images.ic_label), + null, + Modifier.size(18.sp.toDp()), + tint = if (current) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground + ) + } + Spacer(Modifier.width(4.dp)) + Box { + val badgeText = if ((unreadTags[tag.chatTagId] ?: 0) > 0) " ●" else "" + val invisibleText = buildAnnotatedString { + append(tag.chatTagText) + withStyle(SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.SemiBold)) { + append(badgeText) + } + } + Text( + text = invisibleText, + fontWeight = FontWeight.Medium, + fontSize = 15.sp, + color = Color.Transparent, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + // Visible text with styles + val visibleText = buildAnnotatedString { + append(tag.chatTagText) + withStyle(SpanStyle(fontSize = 12.5.sp, color = MaterialTheme.colors.primary)) { + append(badgeText) + } + } + Text( + text = visibleText, + fontWeight = if (current) FontWeight.Medium else FontWeight.Normal, + fontSize = 15.sp, + color = if (current) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + TagsDropdownMenu(rhId, tag, showMenu, saving) + } + } + val plusClickModifier = Modifier + .clickable { + ModalManager.start.showModalCloseable { close -> + TagListEditor(rhId = rhId, close = close) + } + } + + if (userTags.value.isEmpty()) { + Row(rowSizeModifier.clip(shape = CircleShape).then(plusClickModifier).padding(start = 2.dp, top = 4.dp, end = 6.dp, bottom = 4.dp), verticalAlignment = Alignment.CenterVertically) { + Icon(painterResource(MR.images.ic_add), stringResource(MR.strings.chat_list_add_list), Modifier.size(18.sp.toDp()), tint = MaterialTheme.colors.secondary) + Spacer(Modifier.width(2.dp)) + Text(stringResource(MR.strings.chat_list_add_list), color = MaterialTheme.colors.secondary, fontSize = 15.sp) + } + } else { + Box(rowSizeModifier, contentAlignment = Alignment.Center) { + Icon( + painterResource(MR.images.ic_add), stringResource(MR.strings.chat_list_add_list), Modifier.clip(shape = CircleShape).then(plusClickModifier).padding(2.dp), tint = MaterialTheme.colors.secondary + ) } } } } -private fun filtered(chat: Chat): Boolean = - (chat.chatInfo.chatSettings?.favorite ?: false) || chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat +@Composable +expect fun TagsRow(content: @Composable() (() -> Unit)) -private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean = - cInfo.chatViewName.lowercase().contains(s.lowercase()) +@Composable +private fun ExpandedTagFilterView(tag: PresetTagKind) { + val activeFilter = remember { chatModel.activeChatTagFilter } + val active = when (val af = activeFilter.value) { + is ActiveFilter.PresetTag -> af.tag == tag + else -> false + } + val (icon, text) = presetTagLabel(tag, active) + val color = if (active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + + Row( + modifier = Modifier + .sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier) + .clip(shape = CircleShape) + .clickable { + if (activeFilter.value == ActiveFilter.PresetTag(tag)) { + chatModel.activeChatTagFilter.value = null + } else { + chatModel.activeChatTagFilter.value = ActiveFilter.PresetTag(tag) + } + } + .padding(horizontal = 5.dp, vertical = 4.dp) + , + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + painterResource(icon), + stringResource(text), + Modifier.size(18.sp.toDp()), + tint = color + ) + Spacer(Modifier.width(4.dp)) + Box { + Text( + stringResource(text), + color = if (active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + fontWeight = if (active) FontWeight.Medium else FontWeight.Normal, + fontSize = 15.sp + ) + Text( + stringResource(text), + color = Color.Transparent, + fontWeight = FontWeight.Medium, + fontSize = 15.sp + ) + } + } +} + + +@Composable +private fun CollapsedTagsFilterView(searchText: MutableState<TextFieldValue>) { + val activeFilter = remember { chatModel.activeChatTagFilter } + val presetTags = remember { chatModel.presetTags } + val showMenu = remember { mutableStateOf(false) } + + val selectedPresetTag = when (val af = activeFilter.value) { + is ActiveFilter.PresetTag -> if (presetCanBeCollapsed(af.tag)) af.tag else null + else -> null + } + + Box(Modifier + .clip(shape = CircleShape) + .size(TAG_MIN_HEIGHT * fontSizeSqrtMultiplier) + .clickable { showMenu.value = true }, + contentAlignment = Alignment.Center + ) { + if (selectedPresetTag != null) { + val (icon, text) = presetTagLabel(selectedPresetTag, true) + Icon( + painterResource(icon), + stringResource(text), + Modifier.size(18.sp.toDp()), + tint = MaterialTheme.colors.primary + ) + } else { + Icon( + painterResource(MR.images.ic_menu), + stringResource(MR.strings.chat_list_all), + tint = MaterialTheme.colors.secondary + ) + } + + val onCloseMenuAction = remember { mutableStateOf<(() -> Unit)>({}) } + + DefaultDropdownMenu(showMenu = showMenu, onClosed = onCloseMenuAction) { + if (activeFilter.value != null || searchText.value.text.isNotBlank()) { + ItemAction( + stringResource(MR.strings.chat_list_all), + painterResource(MR.images.ic_menu), + onClick = { + onCloseMenuAction.value = { + searchText.value = TextFieldValue() + chatModel.activeChatTagFilter.value = null + onCloseMenuAction.value = {} + } + showMenu.value = false + } + ) + } + PresetTagKind.entries.forEach { tag -> + if ((presetTags[tag] ?: 0) > 0 && presetCanBeCollapsed(tag)) { + ItemPresetFilterAction(tag, tag == selectedPresetTag, showMenu, onCloseMenuAction) + } + } + } + } +} + +@Composable +fun ItemPresetFilterAction( + presetTag: PresetTagKind, + active: Boolean, + showMenu: MutableState<Boolean>, + onCloseMenuAction: MutableState<(() -> Unit)> +) { + val (icon, text) = presetTagLabel(presetTag, active) + ItemAction( + stringResource(text), + painterResource(icon), + color = if (active) MaterialTheme.colors.primary else Color.Unspecified, + onClick = { + onCloseMenuAction.value = { + chatModel.activeChatTagFilter.value = ActiveFilter.PresetTag(presetTag) + onCloseMenuAction.value = {} + } + showMenu.value = false + } + ) +} + +fun filteredChats( + searchShowingSimplexLink: State<Boolean>, + searchChatFilteredBySimplexLink: State<String?>, + searchText: String, + chats: List<Chat>, + activeFilter: ActiveFilter? = null, +): List<Chat> { + val linkChatId = searchChatFilteredBySimplexLink.value + return if (linkChatId != null) { + chats.filter { it.id == linkChatId } + } else { + val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase() + if (s.isEmpty()) + chats.filter { chat -> chat.id == chatModel.chatId.value || (!chat.chatInfo.chatDeleted && !chat.chatInfo.contactCard && filtered(chat, activeFilter)) } + else { + chats.filter { chat -> + chat.id == chatModel.chatId.value || + when (val cInfo = chat.chatInfo) { + is ChatInfo.Direct -> !cInfo.contact.chatDeleted && !chat.chatInfo.contactCard && cInfo.anyNameContains(s) + is ChatInfo.Group -> cInfo.anyNameContains(s) + is ChatInfo.Local -> cInfo.anyNameContains(s) + is ChatInfo.ContactRequest -> cInfo.anyNameContains(s) + is ChatInfo.ContactConnection -> cInfo.contactConnection.localAlias.lowercase().contains(s) + is ChatInfo.InvalidJSON -> false + } + } + } + } +} + +private fun filtered(chat: Chat, activeFilter: ActiveFilter?): Boolean = + when (activeFilter) { + is ActiveFilter.PresetTag -> presetTagMatchesChat(activeFilter.tag, chat.chatInfo, chat.chatStats) + is ActiveFilter.UserTag -> chat.chatInfo.chatTags?.contains(activeFilter.tag.chatTagId) ?: false + is ActiveFilter.Unread -> chat.unreadTag + else -> true + } + +fun presetTagMatchesChat(tag: PresetTagKind, chatInfo: ChatInfo, chatStats: Chat.ChatStats): Boolean = + when (tag) { + PresetTagKind.GROUP_REPORTS -> chatStats.reportsCount > 0 + PresetTagKind.FAVORITES -> chatInfo.chatSettings?.favorite == true + PresetTagKind.CONTACTS -> when (chatInfo) { + is ChatInfo.Direct -> !(chatInfo.contact.activeConn == null && chatInfo.contact.profile.contactLink != null && chatInfo.contact.active) && !chatInfo.contact.chatDeleted + is ChatInfo.ContactRequest -> true + is ChatInfo.ContactConnection -> true + is ChatInfo.Group -> chatInfo.groupInfo.businessChat?.chatType == BusinessChatType.Customer + else -> false + } + PresetTagKind.GROUPS -> when (chatInfo) { + is ChatInfo.Group -> chatInfo.groupInfo.businessChat == null + else -> false + } + PresetTagKind.BUSINESS -> when (chatInfo) { + is ChatInfo.Group -> chatInfo.groupInfo.businessChat?.chatType == BusinessChatType.Business + else -> false + } + PresetTagKind.NOTES -> when (chatInfo) { + is ChatInfo.Local -> !chatInfo.noteFolder.chatDeleted + else -> false + } + } + +private fun presetTagLabel(tag: PresetTagKind, active: Boolean): Pair<ImageResource, StringResource> = + 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 + } + +private fun presetCanBeCollapsed(tag: PresetTagKind): Boolean = when (tag) { + PresetTagKind.GROUP_REPORTS -> false + else -> true +} + +fun scrollToBottom(scope: CoroutineScope, listState: LazyListState) { + scope.launch { try { listState.animateScrollToItem(0) } catch (e: Exception) { Log.e(TAG, e.stackTraceToString()) } } +} 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 af1f49a088..93d512507a 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 @@ -1,6 +1,6 @@ package chat.simplex.common.views.chatlist -import androidx.compose.foundation.background +import SectionItemView import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.InlineTextContent @@ -15,16 +15,23 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.* import androidx.compose.ui.unit.* import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.chat.ComposePreview -import chat.simplex.common.views.chat.ComposeState -import chat.simplex.common.views.chat.item.MarkdownText import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.GroupInfo +import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.chat.item.* import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource @@ -36,10 +43,11 @@ fun ChatPreviewView( chatModelDraftChatId: ChatId?, currentUserProfileDisplayName: String?, contactNetworkStatus: NetworkStatus?, - stopped: Boolean, + disabled: Boolean, linkMode: SimplexLinkMode, inProgress: Boolean, - progressByTimeout: Boolean + progressByTimeout: Boolean, + defaultClickAction: () -> Unit ) { val cInfo = chat.chatInfo @@ -48,7 +56,7 @@ fun ChatPreviewView( Icon( painterResource(MR.images.ic_cancel_filled), stringResource(MR.strings.icon_descr_group_inactive), - Modifier.size(18.dp).background(MaterialTheme.colors.background, CircleShape), + Modifier.size(18.sp.toDp()).background(MaterialTheme.colors.background, CircleShape), tint = MaterialTheme.colors.secondary ) } @@ -62,6 +70,7 @@ fun ChatPreviewView( } is ChatInfo.Group -> when (cInfo.groupInfo.membership.memberStatus) { + GroupMemberStatus.MemRejected -> inactiveIcon() GroupMemberStatus.MemLeft -> inactiveIcon() GroupMemberStatus.MemRemoved -> inactiveIcon() GroupMemberStatus.MemGroupDeleted -> inactiveIcon() @@ -85,10 +94,10 @@ fun ChatPreviewView( @Composable fun VerifiedIcon() { - Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary) + Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(19.sp.toDp()).padding(end = 3.sp.toDp(), top = 1.sp.toDp()), tint = MaterialTheme.colors.secondary) } - fun messageDraft(draft: ComposeState): Pair<AnnotatedString, Map<String, InlineTextContent>> { + fun messageDraft(draft: ComposeState, sp20: Dp): Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>> { fun attachment(): Pair<ImageResource, String?>? = when (draft.preview) { is ComposePreview.FilePreview -> MR.images.ic_draft_filled to draft.preview.fileName @@ -98,7 +107,7 @@ fun ChatPreviewView( } val attachment = attachment() - val text = buildAnnotatedString { + val inlineContentBuilder: AnnotatedString.Builder.() -> Unit = { appendInlineContent(id = "editIcon") append(" ") if (attachment != null) { @@ -108,43 +117,53 @@ fun ChatPreviewView( } append(" ") } - append(draft.message) } val inlineContent: Map<String, InlineTextContent> = mapOf( "editIcon" to InlineTextContent( Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter) ) { - Icon(painterResource(MR.images.ic_edit_note), null, tint = MaterialTheme.colors.primary) + Icon(painterResource(MR.images.ic_edit_note), null, Modifier.size(sp20), tint = MaterialTheme.colors.primary) }, "attachmentIcon" to InlineTextContent( Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter) ) { - Icon(if (attachment?.first != null) painterResource(attachment.first) else painterResource(MR.images.ic_edit_note), null, tint = MaterialTheme.colors.secondary) + Icon(if (attachment?.first != null) painterResource(attachment.first) else painterResource(MR.images.ic_edit_note), null, Modifier.size(sp20), tint = MaterialTheme.colors.secondary) } ) - return text to inlineContent + return inlineContentBuilder to inlineContent } @Composable fun chatPreviewTitle() { + val deleting by remember(disabled, chat.id) { mutableStateOf(chatModel.deletedChats.value.contains(chat.remoteHostId to chat.chatInfo.id)) } when (cInfo) { is ChatInfo.Direct -> Row(verticalAlignment = Alignment.CenterVertically) { if (cInfo.contact.verified) { VerifiedIcon() } - chatPreviewTitleText() + chatPreviewTitleText( + if (deleting) + MaterialTheme.colors.secondary + else + Color.Unspecified + ) } is ChatInfo.Group -> when (cInfo.groupInfo.membership.memberStatus) { GroupMemberStatus.MemInvited -> chatPreviewTitleText( - if (inProgress) + if (inProgress || deleting) MaterialTheme.colors.secondary else if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary ) - GroupMemberStatus.MemAccepted -> chatPreviewTitleText(MaterialTheme.colors.secondary) - else -> chatPreviewTitleText() + GroupMemberStatus.MemAccepted, GroupMemberStatus.MemRejected -> chatPreviewTitleText(MaterialTheme.colors.secondary) + else -> chatPreviewTitleText( + if (deleting) + MaterialTheme.colors.secondary + else + Color.Unspecified + ) } else -> chatPreviewTitleText() } @@ -155,16 +174,27 @@ fun ChatPreviewView( val ci = chat.chatItems.lastOrNull() if (ci != null) { if (showChatPreviews || (chatModelDraftChatId == chat.id && chatModelDraft != null)) { + val sp20 = with(LocalDensity.current) { 20.sp.toDp() } val (text: CharSequence, inlineTextContent) = when { - chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) } + chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { chatModelDraft.message.text to messageDraft(chatModelDraft, sp20) } ci.meta.itemDeleted == null -> ci.text to null - else -> generalGetString(MR.strings.marked_deleted_description) to null + else -> markedDeletedText(ci, chat.chatInfo) to null } val formattedText = when { chatModelDraftChatId == chat.id && chatModelDraft != null -> null ci.meta.itemDeleted == null -> ci.formattedText else -> null } + val prefix = when (val mc = 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}: ") + } + } + else -> null + } + MarkdownText( text, formattedText, @@ -173,21 +203,33 @@ fun ChatPreviewView( cInfo is ChatInfo.Group && !ci.chatDir.sent -> ci.memberDisplayName else -> null }, + mentions = ci.mentions, + userMemberId = when { + cInfo is ChatInfo.Group -> cInfo.groupInfo.membership.memberId + else -> null + }, + toggleSecrets = false, linkMode = linkMode, senderBold = true, maxLines = 2, overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.body1.copy(color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp), + style = TextStyle( + fontFamily = Inter, + fontSize = 15.sp, + color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, + lineHeight = 21.sp + ), inlineContent = inlineTextContent, modifier = Modifier.fillMaxWidth(), + prefix = prefix ) } } else { when (cInfo) { is ChatInfo.Direct -> - if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null) { + if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null && cInfo.contact.active) { Text(stringResource(MR.strings.contact_tap_to_connect), color = MaterialTheme.colors.primary) - } else if (!cInfo.ready && cInfo.contact.activeConn != null) { + } else if (!cInfo.contact.sndReady && cInfo.contact.activeConn != null) { if (cInfo.contact.nextSendGrpInv) { Text(stringResource(MR.strings.member_contact_send_direct_message), color = MaterialTheme.colors.secondary) } else if (cInfo.contact.active) { @@ -196,6 +238,7 @@ fun ChatPreviewView( } is ChatInfo.Group -> when (cInfo.groupInfo.membership.memberStatus) { + GroupMemberStatus.MemRejected -> Text(stringResource(MR.strings.group_preview_rejected)) GroupMemberStatus.MemInvited -> Text(groupInvitationPreviewText(currentUserProfileDisplayName, cInfo.groupInfo)) GroupMemberStatus.MemAccepted -> Text(stringResource(MR.strings.group_connection_pending), color = MaterialTheme.colors.secondary) else -> {} @@ -205,12 +248,87 @@ fun ChatPreviewView( } } + @Composable + fun chatItemContentPreview(chat: Chat, ci: ChatItem?) { + val mc = ci?.content?.msgContent + val provider by remember(chat.id, ci?.id, ci?.file?.fileStatus) { + mutableStateOf({ providerForGallery(chat.chatItems, ci?.id ?: 0) {} }) + } + val uriHandler = LocalUriHandler.current + when (mc) { + is MsgContent.MCLink -> SmallContentPreview { + val linkClicksEnabled = remember { appPrefs.privacyChatListOpenLinks.state }.value != PrivacyChatListOpenLinksMode.NO + IconButton({ + when (appPrefs.privacyChatListOpenLinks.get()) { + PrivacyChatListOpenLinksMode.YES -> uriHandler.openUriCatching(mc.preview.uri) + PrivacyChatListOpenLinksMode.NO -> defaultClickAction() + PrivacyChatListOpenLinksMode.ASK -> AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.privacy_chat_list_open_web_link_question), + text = mc.preview.uri, + buttons = { + Column { + if (chatModel.chatId.value != chat.id) { + SectionItemView({ + AlertManager.shared.hideAlert() + defaultClickAction() + }) { + Text(stringResource(MR.strings.open_chat), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + SectionItemView({ + AlertManager.shared.hideAlert() + uriHandler.openUriCatching(mc.preview.uri) + } + ) { + Text(stringResource(MR.strings.privacy_chat_list_open_web_link), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) + } + }, + if (linkClicksEnabled) Modifier.desktopPointerHoverIconHand() else Modifier, + ) { + Image(base64ToBitmap(mc.preview.image), null, contentScale = ContentScale.Crop) + } + Box(Modifier.align(Alignment.TopEnd).size(15.sp.toDp()).background(Color.Black.copy(0.25f), CircleShape), contentAlignment = Alignment.Center) { + Icon(painterResource(MR.images.ic_arrow_outward), null, Modifier.size(13.sp.toDp()), tint = Color.White) + } + } + is MsgContent.MCImage -> SmallContentPreview { + CIImageView(image = mc.image, file = ci.file, provider, remember { mutableStateOf(false) }, smallView = true) { + val user = chatModel.currentUser.value ?: return@CIImageView + withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) } + } + } + is MsgContent.MCVideo -> SmallContentPreview { + CIVideoView(image = mc.image, mc.duration, file = ci.file, provider, remember { mutableStateOf(false) }, smallView = true) { + val user = chatModel.currentUser.value ?: return@CIVideoView + withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) } + } + } + is MsgContent.MCVoice -> SmallContentPreviewVoice() { + CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = false, ci, cInfo.timedMessagesTTL, showViaProxy = false, showTimestamp = true, smallView = true, longClick = {}) { + val user = chatModel.currentUser.value ?: return@CIVoiceView + withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) } + } + } + is MsgContent.MCFile -> SmallContentPreviewFile { + CIFileView(ci.file, false, remember { mutableStateOf(false) }, smallView = true) { + val user = chatModel.currentUser.value ?: return@CIFileView + withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) } + } + } + else -> {} + } + } + @Composable fun progressView() { CircularProgressIndicator( Modifier - .padding(horizontal = 2.dp) - .size(15.dp), + .size(15.sp.toDp()) + .offset(y = 2.sp.toDp()), color = MaterialTheme.colors.secondary, strokeWidth = 1.5.dp ) @@ -231,7 +349,8 @@ fun ChatPreviewView( contentDescription = descr, tint = MaterialTheme.colors.secondary, modifier = Modifier - .size(19.dp) + .size(19.sp.toDp()) + .offset(x = 2.sp.toDp()) ) else -> @@ -243,6 +362,8 @@ fun ChatPreviewView( } else if (cInfo is ChatInfo.Group) { if (progressByTimeout) { progressView() + } else if (chat.chatStats.reportsCount > 0) { + GroupReportsIcon() } else { IncognitoIcon(chat.chatInfo.incognito) } @@ -251,94 +372,167 @@ fun ChatPreviewView( } } - Row { - Box(contentAlignment = Alignment.BottomEnd) { - ChatInfoImage(cInfo, size = 72.dp) - Box(Modifier.padding(end = 6.dp, bottom = 6.dp)) { - chatPreviewImageOverlayIcon() + Box(contentAlignment = Alignment.Center) { + Row { + Box(contentAlignment = Alignment.BottomEnd) { + ChatInfoImage(cInfo, size = 72.dp * fontSizeSqrtMultiplier) + Box(Modifier.padding(end = 6.sp.toDp(), bottom = 6.sp.toDp())) { + chatPreviewImageOverlayIcon() + } } - } - Column( - modifier = Modifier - .padding(horizontal = 8.dp) - .weight(1F) - ) { - chatPreviewTitle() - val height = with(LocalDensity.current) { 46.sp.toDp() } - Row(Modifier.heightIn(min = height)) { - chatPreviewText() - } - } + Spacer(Modifier.width(8.dp)) + Column(Modifier.weight(1f)) { + Row { + Box(Modifier.weight(1f)) { + chatPreviewTitle() + } + Spacer(Modifier.width(8.sp.toDp())) + val ts = getTimestampText(chat.chatItems.lastOrNull()?.meta?.itemTs ?: chat.chatInfo.chatTs) + ChatListTimestampView(ts) + } + Row(Modifier.heightIn(min = 46.sp.toDp()).fillMaxWidth()) { + Row(Modifier.padding(top = 3.sp.toDp()).weight(1f)) { + val activeVoicePreview: MutableState<(ActiveVoicePreview)?> = remember(chat.id) { mutableStateOf(null) } + val chat = activeVoicePreview.value?.chat ?: chat + val ci = activeVoicePreview.value?.ci ?: chat.chatItems.lastOrNull() + val mc = ci?.content?.msgContent + val deleted = ci?.isDeletedContent == true || ci?.meta?.itemDeleted != null + val showContentPreview = (showChatPreviews && chatModelDraftChatId != chat.id && !deleted) || activeVoicePreview.value != null + if (ci != null && showContentPreview) { + chatItemContentPreview(chat, ci) + } + if (mc !is MsgContent.MCVoice || !showContentPreview || mc.text.isNotEmpty() || chatModelDraftChatId == chat.id) { + Box(Modifier.offset(x = if (mc is MsgContent.MCFile && ci.meta.itemDeleted == null) -15.sp.toDp() else 0.dp)) { + chatPreviewText() + } + } + LaunchedEffect(AudioPlayer.currentlyPlaying.value, activeVoicePreview.value) { + val playing = AudioPlayer.currentlyPlaying.value + when { + playing == null -> activeVoicePreview.value = null + activeVoicePreview.value == null -> if (mc is MsgContent.MCVoice && playing.fileSource.filePath == ci.file?.fileSource?.filePath) { + activeVoicePreview.value = ActiveVoicePreview(chat, ci, mc) + } - Box( - contentAlignment = Alignment.TopEnd - ) { - val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.updatedAt) - Text( - ts, - color = MaterialTheme.colors.secondary, - style = MaterialTheme.typography.body2, - modifier = Modifier.padding(bottom = 5.dp) - ) - val n = chat.chatStats.unreadCount - val showNtfsIcon = !chat.chatInfo.ntfsEnabled && (chat.chatInfo is ChatInfo.Direct || chat.chatInfo is ChatInfo.Group) - if (n > 0 || chat.chatStats.unreadChat) { - Box( - Modifier.padding(top = 24.dp), - contentAlignment = Alignment.Center - ) { - Text( - if (n > 0) unreadCountStr(n) else "", - color = Color.White, - fontSize = 11.sp, - modifier = Modifier - .background(if (stopped || showNtfsIcon) MaterialTheme.colors.secondary else MaterialTheme.colors.primaryVariant, shape = CircleShape) - .badgeLayout() - .padding(horizontal = 3.dp) - .padding(vertical = 1.dp) - ) + else -> if (playing.fileSource.filePath != ci?.file?.fileSource?.filePath) { + activeVoicePreview.value = null + } + } + } + LaunchedEffect(chatModel.deletedChats.value) { + val voicePreview = activeVoicePreview.value + // Stop voice when deleting the chat + if (chatModel.deletedChats.value.contains(chatModel.remoteHostId() to chat.id) && voicePreview?.ci != null) { + AudioPlayer.stop(voicePreview.ci) + } + } + } + + Spacer(Modifier.width(8.sp.toDp())) + + Box(Modifier.widthIn(min = 34.sp.toDp()), contentAlignment = Alignment.TopEnd) { + val n = chat.chatStats.unreadCount + val ntfsMode = chat.chatInfo.chatSettings?.enableNtfs + val showNtfsIcon = !chat.chatInfo.ntfsEnabled(false) && (chat.chatInfo is ChatInfo.Direct || chat.chatInfo is ChatInfo.Group) + if (n > 0 || chat.chatStats.unreadChat) { + val unreadMentions = chat.chatStats.unreadMentions + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.sp.toDp())) { + val mentionColor = when { + disabled -> MaterialTheme.colors.secondary + cInfo is ChatInfo.Group -> { + val enableNtfs = cInfo.groupInfo.chatSettings.enableNtfs + if (enableNtfs == MsgFilter.All || enableNtfs == MsgFilter.Mentions) MaterialTheme.colors.primaryVariant else MaterialTheme.colors.secondary + } + + else -> if (showNtfsIcon) MaterialTheme.colors.secondary else MaterialTheme.colors.primaryVariant + } + if (unreadMentions > 0 && n > 1) { + Icon( + painterResource(MR.images.ic_alternate_email), + contentDescription = generalGetString(MR.strings.notifications), + tint = mentionColor, + modifier = Modifier.size(12.sp.toDp()).offset(y = 3.sp.toDp()) + ) + } + + if (unreadMentions > 0 && n == 1) { + Box(modifier = Modifier.offset(y = 2.sp.toDp()).size(15.sp.toDp()).background(mentionColor, shape = CircleShape), contentAlignment = Alignment.Center) { + Icon( + painterResource(MR.images.ic_alternate_email), + contentDescription = generalGetString(MR.strings.notifications), + tint = Color.White, + modifier = Modifier.size(9.sp.toDp()) + ) + } + } else { + Text( + if (n > 0) unreadCountStr(n) else "", + color = Color.White, + fontSize = 10.sp, + style = TextStyle(textAlign = TextAlign.Center), + modifier = Modifier + .offset(y = 3.sp.toDp()) + .background(if (disabled || showNtfsIcon) MaterialTheme.colors.secondary else MaterialTheme.colors.primaryVariant, shape = CircleShape) + .badgeLayout() + .padding(horizontal = 2.sp.toDp()) + .padding(vertical = 1.sp.toDp()) + ) + } + } + } else if (showNtfsIcon && ntfsMode != null) { + Icon( + painterResource(ntfsMode.iconFilled), + contentDescription = generalGetString(MR.strings.notifications), + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .padding(start = 2.sp.toDp()) + .size(18.sp.toDp()) + .offset(x = 2.5.sp.toDp(), y = 2.sp.toDp()) + ) + } else if (chat.chatInfo.chatSettings?.favorite == true) { + Icon( + painterResource(MR.images.ic_star_filled), + contentDescription = generalGetString(MR.strings.favorite_chat), + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .size(20.sp.toDp()) + .offset(x = 2.5.sp.toDp()) + ) + } + Box( + Modifier.offset(y = 28.sp.toDp()), + contentAlignment = Alignment.Center + ) { + chatStatusImage() + } + } } - } else if (showNtfsIcon) { - Box( - Modifier.padding(top = 24.dp), - contentAlignment = Alignment.Center - ) { - Icon( - painterResource(MR.images.ic_notifications_off_filled), - contentDescription = generalGetString(MR.strings.notifications), - tint = MaterialTheme.colors.secondary, - modifier = Modifier - .padding(horizontal = 3.dp) - .padding(vertical = 1.dp) - .size(17.dp) - ) - } - } else if (chat.chatInfo.chatSettings?.favorite == true) { - Box( - Modifier.padding(top = 24.dp), - contentAlignment = Alignment.Center - ) { - Icon( - painterResource(MR.images.ic_star_filled), - contentDescription = generalGetString(MR.strings.favorite_chat), - tint = MaterialTheme.colors.secondary, - modifier = Modifier - .padding(horizontal = 3.dp) - .padding(vertical = 1.dp) - .size(17.dp) - ) - } - } - Box( - Modifier.padding(top = 50.dp), - contentAlignment = Alignment.Center - ) { - chatStatusImage() } } } } +@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))) { + content() + } +} + +@Composable +private fun SmallContentPreviewVoice(content: @Composable () -> Unit) { + Box(Modifier.padding(top = 2.sp.toDp(), end = 8.sp.toDp()).height(voiceMessageSizeBasedOnSquareSize(36f).sp.toDp())) { + content() + } +} + +@Composable +private fun SmallContentPreviewFile(content: @Composable () -> Unit) { + Box(Modifier.padding(top = 3.sp.toDp(), end = 8.sp.toDp()).offset(x = -8.sp.toDp(), y = -4.sp.toDp()).height(41.sp.toDp())) { + content() + } +} + @Composable fun IncognitoIcon(incognito: Boolean) { if (incognito) { @@ -347,11 +541,24 @@ fun IncognitoIcon(incognito: Boolean) { contentDescription = null, tint = MaterialTheme.colors.secondary, modifier = Modifier - .size(21.dp) + .size(21.sp.toDp()) + .offset(x = 1.sp.toDp()) ) } } +@Composable +fun GroupReportsIcon() { + Icon( + painterResource(MR.images.ic_flag), + contentDescription = null, + tint = MaterialTheme.colors.error, + modifier = Modifier + .size(21.sp.toDp()) + .offset(x = 2.sp.toDp()) + ) +} + @Composable private fun groupInvitationPreviewText(currentUserProfileDisplayName: String?, groupInfo: GroupInfo): String { return if (groupInfo.membership.memberIncognito) @@ -365,6 +572,29 @@ fun unreadCountStr(n: Int): String { return if (n < 1000) "$n" else "${n / 1000}" + stringResource(MR.strings.thousand_abbreviation) } +@Composable fun ChatListTimestampView(ts: String) { + Box(contentAlignment = Alignment.BottomStart) { + // This should be the same font style as in title to make date located on the same line as title + Text( + " ", + style = MaterialTheme.typography.h3, + fontWeight = FontWeight.Bold, + ) + Text( + ts, + Modifier.padding(bottom = 5.sp.toDp()).offset(x = if (appPlatform.isDesktop) 1.5.sp.toDp() else 0.dp), + color = MaterialTheme.colors.secondary, + style = MaterialTheme.typography.body2.copy(fontSize = 13.sp), + ) + } +} + +private data class ActiveVoicePreview( + val chat: Chat, + val ci: ChatItem, + val mc: MsgContent.MCVoice +) + @Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, @@ -373,6 +603,6 @@ fun unreadCountStr(n: Int): String { @Composable fun PreviewChatPreviewView() { SimpleXTheme { - ChatPreviewView(Chat.sampleData, true, null, null, "", contactNetworkStatus = NetworkStatus.Connected(), stopped = false, linkMode = SimplexLinkMode.DESCRIPTION, inProgress = false, progressByTimeout = false) + ChatPreviewView(Chat.sampleData, true, null, null, "", contactNetworkStatus = NetworkStatus.Connected(), disabled = false, linkMode = SimplexLinkMode.DESCRIPTION, inProgress = false, progressByTimeout = false, {}) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactConnectionView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactConnectionView.kt index 99d6c5db15..349f1f12d4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactConnectionView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactConnectionView.kt @@ -1,5 +1,6 @@ package chat.simplex.common.views.chatlist +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -7,25 +8,26 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle 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.ui.theme.* -import chat.simplex.common.views.helpers.ProfileImage import chat.simplex.common.model.PendingContactConnection import chat.simplex.common.model.getTimestampText +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @Composable fun ContactConnectionView(contactConnection: PendingContactConnection) { Row { - Box(Modifier.size(72.dp), contentAlignment = Alignment.Center) { - ProfileImage(size = 54.dp, null, if (contactConnection.initiated) MR.images.ic_add_link else MR.images.ic_link) + Box(Modifier.size(72.dp * fontSizeSqrtMultiplier), contentAlignment = Alignment.Center) { + ProfileImage(size = 54.dp * fontSizeSqrtMultiplier, null, if (contactConnection.initiated) MR.images.ic_add_link else MR.images.ic_link) } Column( modifier = Modifier - .padding(horizontal = 8.dp) + .padding(start = 8.dp / fontSizeSqrtMultiplier, end = 8.sp.toDp()) .weight(1F) ) { Text( @@ -36,21 +38,25 @@ fun ContactConnectionView(contactConnection: PendingContactConnection) { fontWeight = FontWeight.Bold, color = MaterialTheme.colors.secondary ) - val height = with(LocalDensity.current) { 46.sp.toDp() } - Text(contactConnection.description, Modifier.heightIn(min = height), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight) + Text( + contactConnection.description, + Modifier.heightIn(min = 46.sp.toDp()).padding(top = 3.sp.toDp()), + maxLines = 2, + style = TextStyle( + fontFamily = Inter, + fontSize = 15.sp, + color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, + lineHeight = 21.sp + ) + ) } Box( contentAlignment = Alignment.TopEnd ) { val ts = getTimestampText(contactConnection.updatedAt) - Text( - ts, - color = MaterialTheme.colors.secondary, - style = MaterialTheme.typography.body2, - modifier = Modifier.padding(bottom = 5.dp) - ) + ChatListTimestampView(ts) Box( - Modifier.padding(top = 50.dp), + Modifier.padding(top = 50.sp.toDp()), contentAlignment = Alignment.Center ) { IncognitoIcon(contactConnection.incognito) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactRequestView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactRequestView.kt index 8debcce98c..901761f65c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactRequestView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactRequestView.kt @@ -6,24 +6,25 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.helpers.ChatInfoImage import chat.simplex.common.model.ChatInfo import chat.simplex.common.model.getTimestampText +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @Composable fun ContactRequestView(contactRequest: ChatInfo.ContactRequest) { Row { - ChatInfoImage(contactRequest, size = 72.dp) + ChatInfoImage(contactRequest, size = 72.dp * fontSizeSqrtMultiplier) Column( modifier = Modifier - .padding(horizontal = 8.dp) + .padding(start = 8.dp, end = 8.sp.toDp()) .weight(1F) ) { Text( @@ -32,21 +33,21 @@ fun ContactRequestView(contactRequest: ChatInfo.ContactRequest) { overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold, - color = MaterialTheme.colors.primary + color = MaterialTheme.colors.primary, + ) + Text( + stringResource(MR.strings.contact_wants_to_connect_with_you), + Modifier.heightIn(min = 46.sp.toDp()).padding(top = 3.sp.toDp()), + maxLines = 2, + style = TextStyle( + fontFamily = Inter, + fontSize = 15.sp, + color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, + lineHeight = 21.sp + ) ) - val height = with(LocalDensity.current) { 46.sp.toDp() } - Text(stringResource(MR.strings.contact_wants_to_connect_with_you), Modifier.heightIn(min = height), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight) } val ts = getTimestampText(contactRequest.contactRequest.updatedAt) - Column( - Modifier.fillMaxHeight(), - ) { - Text( - ts, - color = MaterialTheme.colors.secondary, - style = MaterialTheme.typography.body2, - modifier = Modifier.padding(bottom = 5.dp) - ) - } + ChatListTimestampView(ts) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt new file mode 100644 index 0000000000..acbc72ff48 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt @@ -0,0 +1,980 @@ +package chat.simplex.common.views.chatlist + +import InfoRow +import InfoRowTwoValues +import SectionBottomSpacer +import SectionDividerSpaced +import SectionItemView +import SectionTextFooter +import SectionView +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.common.model.AgentSMPServerStatsData +import chat.simplex.common.model.AgentXFTPServerStatsData +import chat.simplex.common.model.ChatController.chatModel +import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.PresentedServersSummary +import chat.simplex.common.model.RemoteHostInfo +import chat.simplex.common.model.SMPServerSubs +import chat.simplex.common.model.SMPServerSummary +import chat.simplex.common.model.SMPTotals +import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress +import chat.simplex.common.model.ServerProtocol +import chat.simplex.common.model.ServerSessions +import chat.simplex.common.model.XFTPServerSummary +import chat.simplex.common.model.localTimestamp +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.SettingsPreferenceItem +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.* +import kotlinx.datetime.Instant +import numOrDash +import java.text.DecimalFormat +import kotlin.math.floor +import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.seconds + +enum class SubscriptionColorType { + ACTIVE, ACTIVE_SOCKS_PROXY, DISCONNECTED, ACTIVE_DISCONNECTED +} + +data class SubscriptionStatus( + val color: SubscriptionColorType, + val variableValue: Float, + val statusPercent: Float +) + +fun subscriptionStatusColorAndPercentage( + online: Boolean, + socksProxy: String?, + subs: SMPServerSubs, + hasSess: Boolean +): SubscriptionStatus { + fun roundedToQuarter(n: Float): Float = when { + n >= 1 -> 1f + n <= 0 -> 0f + else -> (n * 4).roundToInt() / 4f + } + + val activeColor: SubscriptionColorType = if (socksProxy != null) SubscriptionColorType.ACTIVE_SOCKS_PROXY else SubscriptionColorType.ACTIVE + val noConnColorAndPercent = SubscriptionStatus(SubscriptionColorType.DISCONNECTED, 1f, 0f) + val activeSubsRounded = roundedToQuarter(subs.shareOfActive) + + return if (!online) + noConnColorAndPercent + else if (subs.total == 0 && !hasSess) + // On freshly installed app (without chats) and on app start + SubscriptionStatus(activeColor, 0f, 0f) + else if (subs.ssActive == 0) { + if (hasSess) + SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive) + else + noConnColorAndPercent + } else { // ssActive > 0 + if (hasSess) + SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive) + else + // This would mean implementation error + SubscriptionStatus(SubscriptionColorType.ACTIVE_DISCONNECTED, activeSubsRounded, subs.shareOfActive) + } +} + +@Composable +private fun SubscriptionStatusIndicatorPercentage(percentageText: String) { + Text( + percentageText, + color = MaterialTheme.colors.secondary, + fontSize = 12.sp, + style = MaterialTheme.typography.caption + ) +} + +@Composable +fun SubscriptionStatusIndicatorView(subs: SMPServerSubs, hasSess: Boolean, leadingPercentage: Boolean = false) { + val netCfg = rememberUpdatedState(chatModel.controller.getNetCfg()) + val statusColorAndPercentage = subscriptionStatusColorAndPercentage(chatModel.networkInfo.value.online, netCfg.value.socksProxy, subs, hasSess) + val pref = remember { chatModel.controller.appPrefs.networkShowSubscriptionPercentage } + val percentageText = "${(floor(statusColorAndPercentage.statusPercent * 100)).toInt()}%" + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON) + ) { + if (pref.state.value && leadingPercentage) SubscriptionStatusIndicatorPercentage(percentageText) + val sp16 = with(LocalDensity.current) { 16.sp.toDp() } + SubscriptionStatusIcon( + color = when(statusColorAndPercentage.color) { + SubscriptionColorType.ACTIVE -> MaterialTheme.colors.primary + SubscriptionColorType.ACTIVE_SOCKS_PROXY -> Indigo + SubscriptionColorType.ACTIVE_DISCONNECTED -> WarningOrange + SubscriptionColorType.DISCONNECTED -> MaterialTheme.colors.secondary + }, + modifier = Modifier.size(sp16), + variableValue = statusColorAndPercentage.variableValue) + if (pref.state.value && !leadingPercentage) SubscriptionStatusIndicatorPercentage(percentageText) + } +} + +enum class PresentedUserCategory { + CURRENT_USER, ALL_USERS +} + +enum class PresentedServerType { + SMP, XFTP +} + +@Composable +private fun ServerSessionsView(sess: ServerSessions) { + SectionView(generalGetString(MR.strings.servers_info_transport_sessions_section_header).uppercase()) { + InfoRow( + generalGetString(MR.strings.servers_info_sessions_connected), + numOrDash(sess.ssConnected) + ) + InfoRow( + generalGetString(MR.strings.servers_info_sessions_errors), + numOrDash(sess.ssErrors) + ) + InfoRow( + generalGetString(MR.strings.servers_info_sessions_connecting), + numOrDash(sess.ssConnecting) + ) + } +} + +private fun serverAddress(server: String): String { + val address = parseServerAddress(server) + + return address?.hostnames?.first() ?: server +} + +@Composable +private fun SMPServerView(srvSumm: SMPServerSummary, statsStartedAt: Instant, rh: RemoteHostInfo?) { + SectionItemView( + click = { + ModalManager.start.showCustomModal { close -> + SMPServerSummaryView( + rh = rh, + close = close, + summary = srvSumm, + statsStartedAt = statsStartedAt + ) + } + } + ) { + Text( + serverAddress(srvSumm.smpServer), + modifier = Modifier.weight(10f, fill = true) + ) + if (srvSumm.subs != null) { + Spacer(Modifier.fillMaxWidth().weight(1f)) + SubscriptionStatusIndicatorView(subs = srvSumm.subs, hasSess = srvSumm.sessionsOrNew.hasSess, leadingPercentage = true) + } else if (srvSumm.sessions != null) { + Spacer(Modifier.fillMaxWidth().weight(1f)) + Icon(painterResource(MR.images.ic_arrow_upward), contentDescription = null, tint = SessIconColor(srvSumm.sessions)) + } + } +} + +@Composable +private fun SessIconColor(sess: ServerSessions): Color { + val online = chatModel.networkInfo.value.online + return if (online && sess.ssConnected > 0) SessionActiveColor() else MaterialTheme.colors.secondary +} + +@Composable +private fun SessionActiveColor(): Color { + val netCfg = rememberUpdatedState(chatModel.controller.getNetCfg()) + return if (netCfg.value.socksProxy != null) Indigo else MaterialTheme.colors.primary +} + +@Composable +private fun SMPServersListView(servers: List<SMPServerSummary>, statsStartedAt: Instant, header: String? = null, footer: String? = null, rh: RemoteHostInfo?) { + val sortedServers = servers.sortedWith(compareBy<SMPServerSummary> { !it.hasSubs } + .thenBy { serverAddress(it.smpServer) }) + + SectionView(header) { + sortedServers.map { svr -> SMPServerView(srvSumm = svr, statsStartedAt = statsStartedAt, rh = rh) } + } + if (footer != null) { + SectionTextFooter( + footer + ) + } +} + +fun prettySize(sizeInKB: Long): String { + if (sizeInKB == 0L) { + return "-" + } + + val sizeInBytes = sizeInKB * 1024 + val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") + var size = sizeInBytes.toDouble() + var unitIndex = 0 + + while (size >= 1024 && unitIndex < units.size - 1) { + size /= 1024 + unitIndex++ + } + + val formatter = DecimalFormat("#,##0.#") + return "${formatter.format(size)} ${units[unitIndex]}" +} + +@Composable +private fun XFTPServerView(srvSumm: XFTPServerSummary, statsStartedAt: Instant, rh: RemoteHostInfo?) { + SectionItemView( + click = { + ModalManager.start.showCustomModal { close -> + XFTPServerSummaryView( + rh = rh, + close = close, + summary = srvSumm, + statsStartedAt = statsStartedAt + ) + } + } + ) { + Text( + serverAddress(srvSumm.xftpServer), + modifier = Modifier.weight(10f, fill = true) + ) + if (srvSumm.rcvInProgress || srvSumm.sndInProgress || srvSumm.delInProgress) { + Spacer(Modifier.fillMaxWidth().weight(1f)) + XFTPServerInProgressIcon(srvSumm) + } + } +} + +@Composable +private fun XFTPServerInProgressIcon(srvSumm: XFTPServerSummary) { + return when { + srvSumm.rcvInProgress && !srvSumm.sndInProgress && !srvSumm.delInProgress -> Icon(painterResource(MR.images.ic_arrow_downward),"download", tint = SessionActiveColor()) + !srvSumm.rcvInProgress && srvSumm.sndInProgress && !srvSumm.delInProgress -> Icon(painterResource(MR.images.ic_arrow_upward), "upload", tint = SessionActiveColor()) + !srvSumm.rcvInProgress && !srvSumm.sndInProgress && srvSumm.delInProgress -> Icon(painterResource(MR.images.ic_delete), "deletion", tint = SessionActiveColor()) + else -> Icon(painterResource(MR.images.ic_expand_all), "upload and download", tint = SessionActiveColor()) + } +} + +@Composable +private fun XFTPServersListView(servers: List<XFTPServerSummary>, statsStartedAt: Instant, header: String? = null, rh: RemoteHostInfo?) { + val sortedServers = servers.sortedBy { serverAddress(it.xftpServer) } + + SectionView(header) { + sortedServers.map { svr -> XFTPServerView(svr, statsStartedAt, rh) } + } +} + +@Composable +private fun SMPStatsView(stats: AgentSMPServerStatsData, statsStartedAt: Instant, remoteHostInfo: RemoteHostInfo?) { + SectionView(generalGetString(MR.strings.servers_info_statistics_section_header).uppercase()) { + InfoRow( + generalGetString(MR.strings.servers_info_messages_sent), + numOrDash(stats._sentDirect + stats._sentViaProxy) + ) + InfoRow( + generalGetString(MR.strings.servers_info_messages_received), + numOrDash(stats._recvMsgs) + ) + SectionItemView( + click = { + ModalManager.start.showCustomModal { close -> DetailedSMPStatsView( + rh = remoteHostInfo, + close = close, + stats = stats, + statsStartedAt = statsStartedAt) + } + } + ) { + Text(text = generalGetString(MR.strings.servers_info_details), color = MaterialTheme.colors.onBackground) + } + } + SectionTextFooter( + String.format(stringResource(MR.strings.servers_info_private_data_disclaimer), localTimestamp(statsStartedAt)) + ) +} + +@Composable +private fun SMPSubscriptionsSection(totals: SMPTotals) { + Column { + Row( + Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON * 2) + ) { + Text( + generalGetString(MR.strings.servers_info_subscriptions_section_header).uppercase(), + color = MaterialTheme.colors.secondary, + style = MaterialTheme.typography.body2, + fontSize = 12.sp + ) + SubscriptionStatusIndicatorView(totals.subs, totals.sessions.hasSess) + } + Column(Modifier.padding(PaddingValues()).fillMaxWidth()) { + InfoRow( + generalGetString(MR.strings.servers_info_subscriptions_connections_subscribed), + numOrDash(totals.subs.ssActive) + ) + InfoRow( + generalGetString(MR.strings.servers_info_subscriptions_total), + numOrDash(totals.subs.total) + ) + SettingsPreferenceItem(null, stringResource(MR.strings.subscription_percentage), chatModel.controller.appPrefs.networkShowSubscriptionPercentage) + } + } +} + +@Composable +private fun SMPSubscriptionsSection(subs: SMPServerSubs, summary: SMPServerSummary, rh: RemoteHostInfo?) { + Column { + Row( + Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON * 2) + ) { + Text( + generalGetString(MR.strings.servers_info_subscriptions_section_header).uppercase(), + color = MaterialTheme.colors.secondary, + style = MaterialTheme.typography.body2, + fontSize = 12.sp + ) + SubscriptionStatusIndicatorView(subs, summary.sessionsOrNew.hasSess) + } + Column(Modifier.padding(PaddingValues()).fillMaxWidth()) { + InfoRow( + generalGetString(MR.strings.servers_info_subscriptions_connections_subscribed), + numOrDash(subs.ssActive) + ) + InfoRow( + generalGetString(MR.strings.servers_info_subscriptions_connections_pending), + numOrDash(subs.ssPending) + ) + InfoRow( + generalGetString(MR.strings.servers_info_subscriptions_total), + numOrDash(subs.total) + ) + ReconnectServerButton(rh, summary.smpServer) + } + } +} + +@Composable +private fun ReconnectServerButton(rh: RemoteHostInfo?, server: String) { + SectionItemView(click = { reconnectServerAlert(rh, server) }) { + Text( + stringResource(MR.strings.reconnect), + color = MaterialTheme.colors.primary + ) + } +} + +private fun reconnectServerAlert(rh: RemoteHostInfo?, server: String) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.servers_info_reconnect_server_title), + text = generalGetString(MR.strings.servers_info_reconnect_server_message), + onConfirm = { + withBGApi { + val success = controller.reconnectServer(rh?.remoteHostId, server) + + if (!success) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.servers_info_modal_error_title), + text = generalGetString(MR.strings.servers_info_reconnect_server_error) + ) + } + } + } + ) +} + +@Composable +fun XFTPStatsView(stats: AgentXFTPServerStatsData, statsStartedAt: Instant, rh: RemoteHostInfo?) { + SectionView(generalGetString(MR.strings.servers_info_statistics_section_header).uppercase()) { + InfoRow( + generalGetString(MR.strings.servers_info_uploaded), + prettySize(stats._uploadsSize) + ) + InfoRow( + generalGetString(MR.strings.servers_info_downloaded), + prettySize(stats._downloadsSize) + ) + SectionItemView ( + click = { + ModalManager.start.showCustomModal { close -> DetailedXFTPStatsView( + rh = rh, + close = close, + stats = stats, + statsStartedAt = statsStartedAt) + } + } + ) { + Text(text = generalGetString(MR.strings.servers_info_details), color = MaterialTheme.colors.onBackground) + } + } + SectionTextFooter( + String.format(stringResource(MR.strings.servers_info_private_data_disclaimer), localTimestamp(statsStartedAt)) + ) +} + +@Composable +private fun IndentedInfoRow(title: String, desc: String) { + InfoRow(title, desc, padding = PaddingValues(start = 24.dp + DEFAULT_PADDING, end = DEFAULT_PADDING)) +} + +@Composable +fun DetailedSMPStatsLayout(stats: AgentSMPServerStatsData, statsStartedAt: Instant) { + SectionView(generalGetString(MR.strings.servers_info_detailed_statistics_sent_messages_header).uppercase()) { + InfoRow(generalGetString(MR.strings.servers_info_detailed_statistics_sent_messages_total), numOrDash(stats._sentDirect + stats._sentViaProxy)) + InfoRowTwoValues(generalGetString(MR.strings.sent_directly), generalGetString(MR.strings.attempts_label), stats._sentDirect, stats._sentDirectAttempts) + InfoRowTwoValues(generalGetString(MR.strings.sent_via_proxy), generalGetString(MR.strings.attempts_label), stats._sentViaProxy, stats._sentViaProxyAttempts) + InfoRowTwoValues(generalGetString(MR.strings.proxied), generalGetString(MR.strings.attempts_label), stats._sentProxied, stats._sentProxiedAttempts) + SectionItemView { + Text(generalGetString(MR.strings.send_errors), color = MaterialTheme.colors.onBackground) + } + IndentedInfoRow("AUTH", numOrDash(stats._sentAuthErrs)) + IndentedInfoRow("QUOTA", numOrDash(stats._sentQuotaErrs)) + IndentedInfoRow(generalGetString(MR.strings.expired_label), numOrDash(stats._sentExpiredErrs)) + IndentedInfoRow(generalGetString(MR.strings.other_label), numOrDash(stats._sentOtherErrs)) + } + + SectionDividerSpaced() + + SectionView(generalGetString(MR.strings.servers_info_detailed_statistics_received_messages_header).uppercase()) { + InfoRow(generalGetString(MR.strings.servers_info_detailed_statistics_received_total), numOrDash(stats._recvMsgs)) + SectionItemView { + Text(generalGetString(MR.strings.servers_info_detailed_statistics_receive_errors), color = MaterialTheme.colors.onBackground) + } + IndentedInfoRow(generalGetString(MR.strings.duplicates_label), numOrDash(stats._recvDuplicates)) + IndentedInfoRow(generalGetString(MR.strings.decryption_errors), numOrDash(stats._recvCryptoErrs)) + IndentedInfoRow(generalGetString(MR.strings.other_errors), numOrDash(stats._recvErrs)) + InfoRowTwoValues(generalGetString(MR.strings.acknowledged), generalGetString(MR.strings.attempts_label), stats._ackMsgs, stats._ackAttempts) + SectionItemView { + Text(generalGetString(MR.strings.acknowledgement_errors), color = MaterialTheme.colors.onBackground) + } + IndentedInfoRow("NO_MSG errors", numOrDash(stats._ackNoMsgErrs)) + IndentedInfoRow(generalGetString(MR.strings.other_errors), numOrDash(stats._ackOtherErrs)) + } + + SectionDividerSpaced() + + SectionView(generalGetString(MR.strings.connections).uppercase()) { + InfoRow(generalGetString(MR.strings.created), numOrDash(stats._connCreated)) + InfoRow(generalGetString(MR.strings.secured), numOrDash(stats._connSecured)) + InfoRow(generalGetString(MR.strings.completed), numOrDash(stats._connCompleted)) + InfoRowTwoValues(generalGetString(MR.strings.deleted), generalGetString(MR.strings.attempts_label), stats._connDeleted, stats._connDelAttempts) + InfoRow(generalGetString(MR.strings.deletion_errors), numOrDash(stats._connDelErrs)) + InfoRowTwoValues(generalGetString(MR.strings.subscribed), generalGetString(MR.strings.attempts_label), stats._connSubscribed, stats._connSubAttempts) + InfoRow(generalGetString(MR.strings.subscription_results_ignored), numOrDash(stats._connSubIgnored)) + InfoRow(generalGetString(MR.strings.subscription_errors), numOrDash(stats._connSubErrs)) + } + SectionTextFooter( + String.format(stringResource(MR.strings.servers_info_starting_from), localTimestamp(statsStartedAt)) + ) + + SectionBottomSpacer() +} + +@Composable +fun DetailedXFTPStatsLayout(stats: AgentXFTPServerStatsData, statsStartedAt: Instant) { + SectionView(generalGetString(MR.strings.uploaded_files).uppercase()) { + InfoRow(generalGetString(MR.strings.size), prettySize(stats._uploadsSize)) + InfoRowTwoValues(generalGetString(MR.strings.chunks_uploaded), generalGetString(MR.strings.attempts_label), stats._uploads, stats._uploadAttempts) + InfoRow(generalGetString(MR.strings.upload_errors), numOrDash(stats._uploadErrs)) + InfoRowTwoValues(generalGetString(MR.strings.chunks_deleted), generalGetString(MR.strings.attempts_label), stats._deletions, stats._deleteAttempts) + InfoRow(generalGetString(MR.strings.deletion_errors), numOrDash(stats._deleteErrs)) + } + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.downloaded_files).uppercase()) { + InfoRow(generalGetString(MR.strings.size), prettySize(stats._downloadsSize)) + InfoRowTwoValues(generalGetString(MR.strings.chunks_downloaded), generalGetString(MR.strings.attempts_label), stats._downloads, stats._downloadAttempts) + SectionItemView { + Text(generalGetString(MR.strings.download_errors), color = MaterialTheme.colors.onBackground) + } + IndentedInfoRow("AUTH", numOrDash(stats._downloadAuthErrs)) + IndentedInfoRow(generalGetString(MR.strings.other_label), numOrDash(stats._downloadErrs)) + } + SectionTextFooter( + String.format(stringResource(MR.strings.servers_info_starting_from), localTimestamp(statsStartedAt)) + ) + + SectionBottomSpacer() +} + +@Composable +fun XFTPServerSummaryLayout(summary: XFTPServerSummary, statsStartedAt: Instant, rh: RemoteHostInfo?) { + SectionView(generalGetString(MR.strings.server_address).uppercase()) { + SelectionContainer { + Text( + summary.xftpServer, + Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp), + style = TextStyle( + fontFamily = FontFamily.Monospace, fontSize = 16.sp, + color = MaterialTheme.colors.secondary + ) + ) + } + if (summary.stats != null || summary.sessions != null) { + SectionDividerSpaced() + } + + if (summary.stats != null) { + XFTPStatsView(stats = summary.stats, rh = rh, statsStartedAt = statsStartedAt) + if (summary.sessions != null) { + SectionDividerSpaced(maxTopPadding = true) + } + } + + if (summary.sessions != null) { + ServerSessionsView(summary.sessions) + } + } + + SectionBottomSpacer() +} + +@Composable +fun SMPServerSummaryLayout(summary: SMPServerSummary, statsStartedAt: Instant, rh: RemoteHostInfo?) { + SectionView(generalGetString(MR.strings.server_address).uppercase()) { + SelectionContainer { + Text( + summary.smpServer, + Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp), + style = TextStyle( + fontFamily = FontFamily.Monospace, fontSize = 16.sp, + color = MaterialTheme.colors.secondary + ) + ) + } + if (summary.stats != null || summary.subs != null || summary.sessions != null) { + SectionDividerSpaced() + } + + if (summary.stats != null) { + SMPStatsView(stats = summary.stats, remoteHostInfo = rh, statsStartedAt = statsStartedAt) + if (summary.subs != null || summary.sessions != null) { + SectionDividerSpaced(maxTopPadding = true) + } + } + + if (summary.subs != null) { + SMPSubscriptionsSection(subs = summary.subs, summary = summary, rh = rh) + if (summary.sessions != null) { + SectionDividerSpaced() + } + } + + if (summary.sessions != null) { + ServerSessionsView(summary.sessions) + } + } + + SectionBottomSpacer() +} + +@Composable +fun ModalData.SMPServerSummaryView( + rh: RemoteHostInfo?, + close: () -> Unit, + summary: SMPServerSummary, + statsStartedAt: Instant +) { + ModalView( + close = close + ) { + ColumnWithScrollBar { + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.smp_server), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding + ) + SMPServerSummaryLayout(summary, statsStartedAt, rh) + } + } +} + + +@Composable +fun ModalData.DetailedXFTPStatsView( + rh: RemoteHostInfo?, + close: () -> Unit, + stats: AgentXFTPServerStatsData, + statsStartedAt: Instant +) { + ModalView( + close = close + ) { + ColumnWithScrollBar { + Box(contentAlignment = Alignment.Center) { + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.servers_info_detailed_statistics), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding + ) + } + DetailedXFTPStatsLayout(stats, statsStartedAt) + } + } +} + +@Composable +fun ModalData.DetailedSMPStatsView( + rh: RemoteHostInfo?, + close: () -> Unit, + stats: AgentSMPServerStatsData, + statsStartedAt: Instant +) { + ModalView( + close = close + ) { + ColumnWithScrollBar { + Box(contentAlignment = Alignment.Center) { + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.servers_info_detailed_statistics), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding + ) + } + DetailedSMPStatsLayout(stats, statsStartedAt) + } + } +} + +@Composable +fun ModalData.XFTPServerSummaryView( + rh: RemoteHostInfo?, + close: () -> Unit, + summary: XFTPServerSummary, + statsStartedAt: Instant +) { + ModalView( + close = close + ) { + ColumnWithScrollBar { + Box(contentAlignment = Alignment.Center) { + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.xftp_server), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding + ) + } + XFTPServerSummaryLayout(summary, statsStartedAt, rh) + } + } +} + +@Composable +fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableState<PresentedServersSummary?>) { + ColumnWithScrollBar { + var showUserSelection by remember { mutableStateOf(false) } + val selectedUserCategory = + remember { stateGetOrPut("selectedUserCategory") { PresentedUserCategory.ALL_USERS } } + val selectedServerType = + remember { stateGetOrPut("serverTypeSelection") { PresentedServerType.SMP } } + val scope = rememberCoroutineScope() + + suspend fun setServersSummary() { + if (chatModel.currentUser.value != null) { + serversSummary.value = chatModel.controller.getAgentServersSummary(chatModel.remoteHostId()) + } + } + + LaunchedEffect(Unit) { + if (chatModel.users.count { u -> u.user.activeUser || !u.user.hidden } == 1 + ) { + selectedUserCategory.value = PresentedUserCategory.CURRENT_USER + } else { + showUserSelection = true + } + setServersSummary() + scope.launch { + while (isActive) { + delay(1.seconds) + if ((appPlatform.isDesktop || chat.simplex.common.platform.chatModel.chatId.value == null) && isAppVisibleAndFocused()) { + setServersSummary() + } + } + } + } + + fun resetStats() { + withBGApi { + val success = controller.resetAgentServersStats(rh?.remoteHostId) + if (success) { + setServersSummary() + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.servers_info_modal_error_title), + text = generalGetString(MR.strings.servers_info_reset_stats_alert_error_title) + ) + } + } + } + + Column( + Modifier.fillMaxSize(), + ) { + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.servers_info), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding + ) + if (serversSummary.value == null) { + Box( + modifier = Modifier + .fillMaxSize() + ) { + Text(generalGetString(MR.strings.servers_info_missing), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary) + } + } else { + val userOptions by remember { + mutableStateOf( + listOf( + PresentedUserCategory.ALL_USERS to generalGetString(MR.strings.all_users), + PresentedUserCategory.CURRENT_USER to generalGetString(MR.strings.current_user), + ) + ) + } + val serverTypeTabTitles = PresentedServerType.entries.map { + when (it) { + PresentedServerType.SMP -> + stringResource(MR.strings.messages_section_title) + + PresentedServerType.XFTP -> + stringResource(MR.strings.servers_info_files_tab) + } + } + val serverTypePagerState = rememberPagerState( + initialPage = selectedServerType.value.ordinal, + initialPageOffsetFraction = 0f + ) { PresentedServerType.entries.size } + + KeyChangeEffect(serverTypePagerState.currentPage) { + selectedServerType.value = PresentedServerType.entries[serverTypePagerState.currentPage] + } + TabRow( + selectedTabIndex = serverTypePagerState.currentPage, + backgroundColor = Color.Transparent, + contentColor = MaterialTheme.colors.primary, + ) { + serverTypeTabTitles.forEachIndexed { index, it -> + Tab( + selected = serverTypePagerState.currentPage == index, + onClick = { + scope.launch { + serverTypePagerState.animateScrollToPage(index) + } + }, + text = { Text(it, fontSize = 13.sp) }, + selectedContentColor = MaterialTheme.colors.primary, + unselectedContentColor = MaterialTheme.colors.secondary, + ) + } + } + + HorizontalPager( + state = serverTypePagerState, + Modifier.fillMaxSize(), + verticalAlignment = Alignment.Top, + userScrollEnabled = appPlatform.isAndroid + ) { index -> + Column( + Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Top + ) { + Spacer(Modifier.height(DEFAULT_PADDING)) + if (showUserSelection) { + ExposedDropDownSettingRow( + generalGetString(MR.strings.servers_info_target), + userOptions, + selectedUserCategory, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = { + selectedUserCategory.value = it + } + ) + SectionDividerSpaced() + } + when (index) { + PresentedServerType.SMP.ordinal -> { + serversSummary.value?.let { + val smpSummary = + if (selectedUserCategory.value == PresentedUserCategory.CURRENT_USER) it.currentUserSMP else it.allUsersSMP; + val totals = smpSummary.smpTotals + val currentlyUsedSMPServers = smpSummary.currentlyUsedSMPServers + val previouslyUsedSMPServers = smpSummary.previouslyUsedSMPServers + val proxySMPServers = smpSummary.onlyProxiedSMPServers + val statsStartedAt = it.statsStartedAt + + SMPStatsView(totals.stats, statsStartedAt, rh) + SectionDividerSpaced(maxTopPadding = true) + SMPSubscriptionsSection(totals) + SectionDividerSpaced() + + if (currentlyUsedSMPServers.isNotEmpty()) { + SMPServersListView( + servers = currentlyUsedSMPServers, + statsStartedAt = statsStartedAt, + header = generalGetString(MR.strings.servers_info_connected_servers_section_header).uppercase(), + rh = rh + ) + SectionDividerSpaced() + } + + if (previouslyUsedSMPServers.isNotEmpty()) { + SMPServersListView( + servers = previouslyUsedSMPServers, + statsStartedAt = statsStartedAt, + header = generalGetString(MR.strings.servers_info_previously_connected_servers_section_header).uppercase(), + rh = rh + ) + SectionDividerSpaced() + } + + if (proxySMPServers.isNotEmpty()) { + SMPServersListView( + servers = proxySMPServers, + statsStartedAt = statsStartedAt, + header = generalGetString(MR.strings.servers_info_proxied_servers_section_header).uppercase(), + footer = generalGetString(MR.strings.servers_info_proxied_servers_section_footer), + rh = rh + ) + SectionDividerSpaced(maxTopPadding = true) + } + + ServerSessionsView(totals.sessions) + } + } + + PresentedServerType.XFTP.ordinal -> { + serversSummary.value?.let { + val xftpSummary = + if (selectedUserCategory.value == PresentedUserCategory.CURRENT_USER) it.currentUserXFTP else it.allUsersXFTP + val totals = xftpSummary.xftpTotals + val statsStartedAt = it.statsStartedAt + val currentlyUsedXFTPServers = xftpSummary.currentlyUsedXFTPServers + val previouslyUsedXFTPServers = xftpSummary.previouslyUsedXFTPServers + + XFTPStatsView(totals.stats, statsStartedAt, rh) + SectionDividerSpaced(maxTopPadding = true) + + if (currentlyUsedXFTPServers.isNotEmpty()) { + XFTPServersListView( + currentlyUsedXFTPServers, + statsStartedAt, + generalGetString(MR.strings.servers_info_connected_servers_section_header).uppercase(), + rh + ) + SectionDividerSpaced() + } + + if (previouslyUsedXFTPServers.isNotEmpty()) { + XFTPServersListView( + previouslyUsedXFTPServers, + statsStartedAt, + generalGetString(MR.strings.servers_info_previously_connected_servers_section_header).uppercase(), + rh + ) + SectionDividerSpaced() + } + + ServerSessionsView(totals.sessions) + } + } + } + + SectionDividerSpaced(maxBottomPadding = false) + + SectionView { + ReconnectAllServersButton(rh) + ResetStatisticsButton(rh, resetStats = { resetStats() }) + } + + SectionBottomSpacer() + } + } + } + } + } +} + +@Composable +private fun ReconnectAllServersButton(rh: RemoteHostInfo?) { + SectionItemView(click = { reconnectAllServersAlert(rh) }) { + Text( + stringResource(MR.strings.servers_info_reconnect_all_servers_button), + color = MaterialTheme.colors.primary + ) + } +} + +private fun reconnectAllServersAlert(rh: RemoteHostInfo?) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.servers_info_reconnect_servers_title), + text = generalGetString(MR.strings.servers_info_reconnect_servers_message), + onConfirm = { + withBGApi { + val success = controller.reconnectAllServers(rh?.remoteHostId) + + if (!success) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.servers_info_modal_error_title), + text = generalGetString(MR.strings.servers_info_reconnect_servers_error) + ) + } + } + } + ) +} + +@Composable +private fun ResetStatisticsButton(rh: RemoteHostInfo?, resetStats: () -> Unit) { + SectionItemView(click = { resetStatisticsAlert(rh, resetStats) }) { + Text( + stringResource(MR.strings.servers_info_reset_stats), + color = MaterialTheme.colors.primary + ) + } +} + +private fun resetStatisticsAlert(rh: RemoteHostInfo?, resetStats: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.servers_info_reset_stats_alert_title), + text = generalGetString(MR.strings.servers_info_reset_stats_alert_message), + confirmText = generalGetString(MR.strings.servers_info_reset_stats_alert_confirm), + destructive = true, + onConfirm = resetStats + ) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt index ad8f93990f..47668c4fb3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt @@ -9,44 +9,85 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import chat.simplex.common.ui.theme.Indigo -import chat.simplex.common.views.helpers.ProfileImage import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import kotlinx.coroutines.launch @Composable -fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) { +fun ShareListNavLinkView( + chat: Chat, + chatModel: ChatModel, + isMediaOrFileAttachment: Boolean, + isVoice: Boolean, + hasSimplexLink: Boolean +) { val stopped = chatModel.chatRunning.value == false + val scope = rememberCoroutineScope() when (chat.chatInfo) { - is ChatInfo.Direct -> + is ChatInfo.Direct -> { + val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice) ShareListNavLinkLayout( - chatLinkPreview = { SharePreviewView(chat) }, - click = { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) }, + chatLinkPreview = { SharePreviewView(chat, disabled = voiceProhibited) }, + click = { + if (voiceProhibited) { + showForwardProhibitedByPrefAlert() + } else { + scope.launch { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) } + } + }, stopped ) - is ChatInfo.Group -> + } + is ChatInfo.Group -> { + val simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks) + val fileProhibited = isMediaOrFileAttachment && !chat.groupFeatureEnabled(GroupFeature.Files) + val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice) + val prohibitedByPref = simplexLinkProhibited || fileProhibited || voiceProhibited ShareListNavLinkLayout( - chatLinkPreview = { SharePreviewView(chat) }, - click = { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel) }, + chatLinkPreview = { SharePreviewView(chat, disabled = prohibitedByPref) }, + click = { + if (prohibitedByPref) { + showForwardProhibitedByPrefAlert() + } else { + scope.launch { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel) } + } + }, + stopped + ) + } + is ChatInfo.Local -> + ShareListNavLinkLayout( + chatLinkPreview = { SharePreviewView(chat, disabled = false) }, + click = { scope.launch { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) } }, stopped ) is ChatInfo.ContactRequest, is ChatInfo.ContactConnection, is ChatInfo.InvalidJSON -> {} } } +private fun showForwardProhibitedByPrefAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.cannot_share_message_alert_title), + text = generalGetString(MR.strings.cannot_share_message_alert_text), + ) +} + @Composable private fun ShareListNavLinkLayout( chatLinkPreview: @Composable () -> Unit, click: () -> Unit, - stopped: Boolean + stopped: Boolean, ) { - SectionItemView(minHeight = 50.dp, click = click, disabled = stopped) { + SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = 8.dp), click = click, disabled = stopped) { chatLinkPreview() } Divider(Modifier.padding(horizontal = 8.dp)) } @Composable -private fun SharePreviewView(chat: Chat) { +private fun SharePreviewView(chat: Chat, disabled: Boolean) { Row( Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceBetween, @@ -56,10 +97,16 @@ private fun SharePreviewView(chat: Chat) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - ProfileImage(size = 46.dp, chat.chatInfo.image) + if (chat.chatInfo is ChatInfo.Local) { + ProfileImage(size = 42.dp, null, icon = MR.images.ic_folder_filled, color = NoteFolderIconColor) + } else if (chat.chatInfo is ChatInfo.Group) { + ProfileImage(size = 42.dp, chat.chatInfo.image, icon = MR.images.ic_supervised_user_circle_filled) + } else { + ProfileImage(size = 42.dp, chat.chatInfo.image) + } Text( chat.chatInfo.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = if (chat.chatInfo.incognito) Indigo else Color.Unspecified + color = if (disabled) MaterialTheme.colors.secondary else if (chat.chatInfo.incognito) Indigo else Color.Unspecified ) } } 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 ac8331007e..aa9847c98a 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 @@ -1,7 +1,6 @@ package chat.simplex.common.views.chatlist import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.* import androidx.compose.runtime.* @@ -13,47 +12,72 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import chat.simplex.common.SettingsViewState -import chat.simplex.common.ui.theme.* +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.views.helpers.* -import chat.simplex.common.model.Chat -import chat.simplex.common.model.ChatModel -import chat.simplex.common.platform.BackHandler -import chat.simplex.common.platform.appPlatform +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.themedBackground +import chat.simplex.common.views.chat.topPaddingToContent +import chat.simplex.common.views.newchat.ActiveProfilePicker import chat.simplex.res.MR -import kotlinx.coroutines.flow.MutableStateFlow @Composable -fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stopped: Boolean) { +fun ShareListView(chatModel: ChatModel, stopped: Boolean) { var searchInList by rememberSaveable { mutableStateOf("") } - val (userPickerState, scaffoldState) = settingsState - val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp - Scaffold( - Modifier.padding(end = endPadding), - scaffoldState = scaffoldState, - topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } }, - ) { - Box(Modifier.padding(it)) { - Column( - modifier = Modifier - .fillMaxSize() - ) { - if (chatModel.chats.isNotEmpty()) { - ShareList(chatModel, search = searchInList) - } else { - EmptyList() + val oneHandUI = remember { appPrefs.oneHandUI.state } + Box(Modifier.fillMaxSize().themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) { + val sharedContent = chatModel.sharedContent.value + var isMediaOrFileAttachment = false + var isVoice = false + var hasSimplexLink = false + when (sharedContent) { + is SharedContent.Text -> + hasSimplexLink = hasSimplexLink(sharedContent.text) + is SharedContent.Media -> { + isMediaOrFileAttachment = true + hasSimplexLink = hasSimplexLink(sharedContent.text) + } + is SharedContent.File -> { + isMediaOrFileAttachment = true + hasSimplexLink = hasSimplexLink(sharedContent.text) + } + is SharedContent.Forward -> { + sharedContent.chatItems.forEach { ci -> + val mc = ci.content.msgContent + if (mc != null) { + isMediaOrFileAttachment = isMediaOrFileAttachment || mc.isMediaOrFileAttachment + isVoice = isVoice || mc.isVoice + hasSimplexLink = hasSimplexLink || hasSimplexLink(mc.text) + } } } + null -> {} + } + if (chatModel.chats.value.isNotEmpty()) { + ShareList( + chatModel, + search = searchInList, + isMediaOrFileAttachment = isMediaOrFileAttachment, + isVoice = isVoice, + hasSimplexLink = hasSimplexLink, + ) + } else { + EmptyList() + } + if (oneHandUI.value) { + StatusBarBackground() + } else { + NavigationBarBackground(oneHandUI.value, true) + } + Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) { + ShareListToolbar(chatModel, stopped) { searchInList = it.trim() } } } - if (appPlatform.isAndroid) { - tryOrShowError("UserPicker", error = {}) { - UserPicker(chatModel, userPickerState, showSettings = false, showCancel = true, cancelClicked = { - chatModel.sharedContent.value = null - userPickerState.value = AnimatedViewState.GONE - }) - } - } +} + +private fun hasSimplexLink(msg: String): Boolean { + val parsedMsg = parseToMarkdown(msg) ?: return false + return parsedMsg.any { ft -> ft.format is Format.SimplexLink } } @Composable @@ -64,62 +88,63 @@ private fun EmptyList() { } @Composable -private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, onSearchValueChanged: (String) -> Unit) { +private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchValueChanged: (String) -> Unit) { var showSearch by rememberSaveable { mutableStateOf(false) } val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false } if (showSearch) { BackHandler(onBack = hideSearchOnBack) } - val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } } val navButton: @Composable RowScope.() -> Unit = { when { showSearch -> NavigationButtonBack(hideSearchOnBack) - users.size > 1 || chatModel.remoteHosts.isNotEmpty() -> { + (users.size > 1 || chatModel.remoteHosts.isNotEmpty()) && remember { chatModel.sharedContent }.value !is SharedContent.Forward -> { val allRead = users .filter { u -> !u.user.activeUser && !u.user.hidden } .all { u -> u.unreadCount == 0 } UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) { - userPickerState.value = AnimatedViewState.VISIBLE + ModalManager.start.showCustomModal(keyboardCoversBar = false) { close -> + val search = rememberSaveable { mutableStateOf("") } + ModalView( + { close() }, + showSearch = true, + searchAlwaysVisible = true, + onSearchValueChanged = { search.value = it }, + content = { + ActiveProfilePicker( + search = search, + rhId = chatModel.remoteHostId, + close = close, + contactConnection = null, + showIncognito = false + ) + } + ) + } } } - else -> NavigationButtonBack { chatModel.sharedContent.value = null } - } - } - if (chatModel.chats.size >= 8) { - barButtons.add { - IconButton({ showSearch = true }) { - Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary) - } - } - } - if (stopped) { - barButtons.add { - IconButton(onClick = { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.chat_is_stopped_indication), - generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app) - ) - }) { - Icon( - painterResource(MR.images.ic_report_filled), - generalGetString(MR.strings.chat_is_stopped_indication), - tint = Color.Red, - ) - } + else -> NavigationButtonBack(onButtonClicked = { + val sharedContent = chatModel.sharedContent.value + // Drop shared content + chatModel.sharedContent.value = null + if (sharedContent is SharedContent.Forward) { + chatModel.chatId.value = sharedContent.fromChatInfo.id + } + }) } } - DefaultTopAppBar( + DefaultAppBar( navigationButton = navButton, title = { Row(verticalAlignment = Alignment.CenterVertically) { Text( - when (chatModel.sharedContent.value) { + when (val v = chatModel.sharedContent.value) { is SharedContent.Text -> stringResource(MR.strings.share_message) is SharedContent.Media -> stringResource(MR.strings.share_image) is SharedContent.File -> stringResource(MR.strings.share_file) - else -> stringResource(MR.strings.share_message) + is SharedContent.Forward -> if (v.chatItems.size > 1) stringResource(MR.strings.forward_multiple) else stringResource(MR.strings.forward_message) + null -> stringResource(MR.strings.share_message) }, color = MaterialTheme.colors.onBackground, fontWeight = FontWeight.SemiBold, @@ -128,27 +153,64 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState }, onTitleClick = null, showSearch = showSearch, + onTop = !remember { appPrefs.oneHandUI.state }.value, onSearchValueChanged = onSearchValueChanged, - buttons = barButtons + buttons = { + if (chatModel.chats.value.size >= 8) { + IconButton({ showSearch = true }) { + Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary) + } + } + if (stopped) { + IconButton(onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.chat_is_stopped_indication), + generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app) + ) + }) { + Icon( + painterResource(MR.images.ic_report_filled), + generalGetString(MR.strings.chat_is_stopped_indication), + tint = Color.Red, + ) + } + } + } ) - Divider() } @Composable -private fun ShareList(chatModel: ChatModel, search: String) { - val filter: (Chat) -> Boolean = { chat: Chat -> - chat.chatInfo.chatViewName.lowercase().contains(search.lowercase()) - } +private fun ShareList( + chatModel: ChatModel, + search: String, + isMediaOrFileAttachment: Boolean, + isVoice: Boolean, + hasSimplexLink: Boolean, +) { + val oneHandUI = remember { appPrefs.oneHandUI.state } val chats by remember(search) { derivedStateOf { - if (search.isEmpty()) chatModel.chats.filter { it.chatInfo.ready } else chatModel.chats.filter { it.chatInfo.ready }.filter(filter) + val sorted = chatModel.chats.value.toList().filter { it.chatInfo.ready }.sortedByDescending { it.chatInfo is ChatInfo.Local } + filteredChats(mutableStateOf(false), mutableStateOf(null), search, sorted) } } - LazyColumn( - modifier = Modifier.fillMaxWidth() + val topPaddingToContent = topPaddingToContent(false) + LazyColumnWithScrollBar( + modifier = Modifier.then(if (oneHandUI.value) Modifier.consumeWindowInsets(WindowInsets.navigationBars.only(WindowInsetsSides.Vertical)) else Modifier).imePadding(), + contentPadding = PaddingValues( + top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else topPaddingToContent, + bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp + ), + reverseLayout = oneHandUI.value ) { items(chats) { chat -> - ShareListNavLinkView(chat, chatModel) + ShareListNavLinkView( + chat, + chatModel, + isMediaOrFileAttachment = isMediaOrFileAttachment, + isVoice = isVoice, + hasSimplexLink = hasSimplexLink, + ) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt new file mode 100644 index 0000000000..8dfe138da1 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt @@ -0,0 +1,506 @@ +package chat.simplex.common.views.chatlist + +import SectionCustomFooter +import SectionDivider +import SectionItemView +import TextIconSpaced +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.* +import androidx.compose.material.MaterialTheme.colors +import androidx.compose.material.TextFieldDefaults.indicatorLine +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.* +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +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.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.apiDeleteChatTag +import chat.simplex.common.model.ChatController.apiSetChatTags +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ChatModel.clearActiveChatFilterIfNeeded +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.item.ItemAction +import chat.simplex.common.views.chat.item.ReactionIcon +import chat.simplex.common.views.chat.topPaddingToContent +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.* + +@Composable +fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, reorderMode: Boolean) { + val userTags = remember { chatModel.userTags } + val oneHandUI = remember { appPrefs.oneHandUI.state } + val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState() + val saving = remember { mutableStateOf(false) } + val chatTagIds = derivedStateOf { chat?.chatInfo?.chatTags ?: emptyList() } + + fun reorderTags(tagIds: List<Long>) { + saving.value = true + withBGApi { + try { + chatModel.controller.apiReorderChatTags(rhId, tagIds) + } catch (e: Exception) { + Log.d(TAG, "ChatListTag reorderTags error: ${e.message}") + } finally { + saving.value = false + } + } + } + + val dragDropState = + rememberDragDropState(listState) { fromIndex, toIndex -> + userTags.value = userTags.value.toMutableList().apply { add(toIndex, removeAt(fromIndex)) } + reorderTags(userTags.value.map { it.chatTagId }) + } + val topPaddingToContent = topPaddingToContent(false) + + LazyColumnWithScrollBar( + modifier = if (reorderMode) Modifier.dragContainer(dragDropState) else Modifier, + state = listState, + contentPadding = PaddingValues( + top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else topPaddingToContent, + bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp + ), + verticalArrangement = if (oneHandUI.value) Arrangement.Bottom else Arrangement.Top, + ) { + @Composable fun CreateList() { + SectionItemView({ + ModalManager.start.showModalCloseable { close -> + TagListEditor(rhId = rhId, close = close, chat = chat) + } + }) { + Icon(painterResource(MR.images.ic_add), stringResource(MR.strings.create_list), tint = MaterialTheme.colors.primary) + Spacer(Modifier.padding(horizontal = 4.dp)) + Text(stringResource(MR.strings.create_list), color = MaterialTheme.colors.primary) + } + } + + if (oneHandUI.value && !reorderMode) { + item { + CreateList() + } + } + itemsIndexed(userTags.value, key = { _, item -> item.chatTagId }) { index, tag -> + DraggableItem(dragDropState, index) { isDragging -> + val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp) + + Card( + elevation = elevation, + backgroundColor = if (isDragging) colors.surface else Color.Unspecified + ) { + Column { + val selected = chatTagIds.value.contains(tag.chatTagId) + + Row( + Modifier + .fillMaxWidth() + .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT) + .clickable( + enabled = !saving.value && !reorderMode, + onClick = { + if (chat == null) { + ModalManager.start.showModalCloseable { close -> + TagListEditor( + rhId = rhId, + tagId = tag.chatTagId, + close = close, + emoji = tag.chatTagEmoji, + name = tag.chatTagText, + ) + } + } else { + saving.value = true + setTag(rhId = rhId, tagId = if (selected) null else tag.chatTagId, chat = chat, close = { + saving.value = false + close() + }) + } + }, + ) + .padding(PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL)), + verticalAlignment = Alignment.CenterVertically + ) { + if (tag.chatTagEmoji != null) { + ReactionIcon(tag.chatTagEmoji, fontSize = 14.sp) + } else { + Icon(painterResource(MR.images.ic_label), null, Modifier.size(18.sp.toDp()), tint = MaterialTheme.colors.onBackground) + } + Spacer(Modifier.padding(horizontal = 4.dp)) + Text( + tag.chatTagText, + color = MenuTextColor, + fontWeight = if (selected) FontWeight.Medium else FontWeight.Normal + ) + if (selected) { + Spacer(Modifier.weight(1f)) + Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) + } else if (reorderMode) { + Spacer(Modifier.weight(1f)) + Icon(painterResource(MR.images.ic_drag_handle), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) + } + } + SectionDivider() + } + } + } + } + if (!oneHandUI.value && !reorderMode) { + item { + CreateList() + } + } + } +} + +@Composable +fun ModalData.TagListEditor( + rhId: Long?, + chat: Chat? = null, + tagId: Long? = null, + emoji: String? = null, + name: String = "", + close: () -> Unit +) { + val userTags = remember { chatModel.userTags } + val oneHandUI = remember { appPrefs.oneHandUI.state } + val newEmoji = remember { stateGetOrPutNullable("chatTagEmoji") { emoji } } + val newName = remember { stateGetOrPut("chatTagName") { name } } + val saving = remember { mutableStateOf<Boolean?>(null) } + val trimmedName = remember { derivedStateOf { newName.value.trim() } } + val isDuplicateEmojiOrName = remember { + derivedStateOf { + userTags.value.any { tag -> + tag.chatTagId != tagId && + ((newEmoji.value != null && tag.chatTagEmoji == newEmoji.value) || tag.chatTagText == trimmedName.value) + } + } + } + + fun createTag() { + saving.value = true + withBGApi { + try { + val updatedTags = chatModel.controller.apiCreateChatTag(rhId, ChatTagData(newEmoji.value, trimmedName.value)) + if (updatedTags != null) { + saving.value = false + userTags.value = updatedTags + close() + } else { + saving.value = null + return@withBGApi + } + + if (chat != null) { + val createdTag = updatedTags.firstOrNull() { it.chatTagText == trimmedName.value && it.chatTagEmoji == newEmoji.value } + + if (createdTag != null) { + setTag(rhId, createdTag.chatTagId, chat, close = { + saving.value = false + close() + }) + } + } + } catch (e: Exception) { + Log.d(TAG, "createChatTag tag error: ${e.message}") + saving.value = null + } + } + } + + fun updateTag() { + saving.value = true + withBGApi { + try { + if (chatModel.controller.apiUpdateChatTag(rhId, tagId!!, ChatTagData(newEmoji.value, trimmedName.value))) { + userTags.value = userTags.value.map { tag -> + if (tag.chatTagId == tagId) { + tag.copy(chatTagEmoji = newEmoji.value, chatTagText = trimmedName.value) + } else { + tag + } + } + } else { + saving.value = null + return@withBGApi + } + saving.value = false + close() + } catch (e: Exception) { + Log.d(TAG, "ChatListTagEditor updateChatTag tag error: ${e.message}") + saving.value = null + } + } + } + + val showError = derivedStateOf { isDuplicateEmojiOrName.value && saving.value != false } + + ColumnWithScrollBar(Modifier.consumeWindowInsets(PaddingValues(bottom = if (oneHandUI.value) WindowInsets.ime.asPaddingValues().calculateBottomPadding().coerceIn(0.dp, WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()) else 0.dp))) { + if (oneHandUI.value) { + Spacer(Modifier.weight(1f)) + } + ChatTagInput(newName, showError, newEmoji) + val disabled = saving.value == true || + (trimmedName.value == name && newEmoji.value == emoji) || + trimmedName.value.isEmpty() || + isDuplicateEmojiOrName.value + + SectionItemView(click = { if (tagId == null) createTag() else updateTag() }, disabled = disabled) { + Text( + generalGetString(if (chat != null) MR.strings.add_to_list else MR.strings.save_list), + color = if (disabled) colors.secondary else colors.primary + ) + } + val showErrorMessage = isDuplicateEmojiOrName.value && saving.value != false + SectionCustomFooter { + Row( + Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painterResource(MR.images.ic_error), + contentDescription = stringResource(MR.strings.error), + tint = if (showErrorMessage) Color.Red else Color.Transparent, + modifier = Modifier + .size(19.sp.toDp()) + .offset(x = 2.sp.toDp()) + ) + TextIconSpaced() + Text( + generalGetString(MR.strings.duplicated_list_error), + color = if (showErrorMessage) colors.secondary else Color.Transparent, + lineHeight = 18.sp, + fontSize = 14.sp + ) + } + } + } +} + +@Composable +fun TagsDropdownMenu(rhId: Long?, tag: ChatTag, showMenu: MutableState<Boolean>, saving: MutableState<Boolean>) { + DefaultDropdownMenu(showMenu, dropdownMenuItems = { + EditTagAction(rhId, tag, showMenu) + DeleteTagAction(rhId, tag, showMenu, saving) + ChangeOrderTagAction(rhId, showMenu) + }) +} + +@Composable +private fun DeleteTagAction(rhId: Long?, tag: ChatTag, showMenu: MutableState<Boolean>, saving: MutableState<Boolean>) { + ItemAction( + stringResource(MR.strings.delete_chat_list_menu_action), + painterResource(MR.images.ic_delete), + onClick = { + deleteTagDialog(rhId, tag, saving) + showMenu.value = false + }, + color = Color.Red + ) +} + +@Composable +private fun EditTagAction(rhId: Long?, tag: ChatTag, showMenu: MutableState<Boolean>) { + ItemAction( + stringResource(MR.strings.edit_chat_list_menu_action), + painterResource(MR.images.ic_edit), + onClick = { + showMenu.value = false + ModalManager.start.showModalCloseable { close -> + TagListEditor( + rhId = rhId, + tagId = tag.chatTagId, + close = close, + emoji = tag.chatTagEmoji, + name = tag.chatTagText + ) + } + }, + color = MenuTextColor + ) +} + +@Composable +private fun ChangeOrderTagAction(rhId: Long?, showMenu: MutableState<Boolean>) { + ItemAction( + stringResource(MR.strings.change_order_chat_list_menu_action), + painterResource(MR.images.ic_drag_handle), + onClick = { + showMenu.value = false + ModalManager.start.showModalCloseable { close -> + TagListView(rhId = rhId, close = close, reorderMode = true) + } + }, + color = MenuTextColor + ) +} + +@Composable +expect fun ChatTagInput(name: MutableState<String>, showError: State<Boolean>, emoji: MutableState<String?>) + +@Composable +fun TagListNameTextField(name: MutableState<String>, showError: State<Boolean>) { + var focused by rememberSaveable { mutableStateOf(false) } + val focusRequester = remember { FocusRequester() } + val interactionSource = remember { MutableInteractionSource() } + val colors = TextFieldDefaults.textFieldColors( + backgroundColor = Color.Unspecified, + focusedIndicatorColor = MaterialTheme.colors.secondary.copy(alpha = 0.6f), + unfocusedIndicatorColor = CurrentColors.value.colors.secondary.copy(alpha = 0.3f), + cursorColor = MaterialTheme.colors.secondary, + ) + BasicTextField( + value = name.value, + onValueChange = { name.value = it }, + interactionSource = interactionSource, + modifier = Modifier + .fillMaxWidth() + .indicatorLine(true, showError.value, interactionSource, colors) + .heightIn(min = TextFieldDefaults.MinHeight) + .onFocusChanged { focused = it.isFocused } + .focusRequester(focusRequester), + textStyle = TextStyle(fontSize = 18.sp, color = MaterialTheme.colors.onBackground), + singleLine = true, + cursorBrush = SolidColor(MaterialTheme.colors.secondary), + decorationBox = @Composable { innerTextField -> + TextFieldDefaults.TextFieldDecorationBox( + value = name.value, + innerTextField = innerTextField, + placeholder = { + Text(generalGetString(MR.strings.list_name_field_placeholder), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary, lineHeight = 22.sp)) + }, + contentPadding = PaddingValues(), + label = null, + visualTransformation = VisualTransformation.None, + leadingIcon = null, + singleLine = true, + enabled = true, + isError = false, + interactionSource = remember { MutableInteractionSource() }, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified) + ) + } + ) +} + +private fun setTag(rhId: Long?, tagId: Long?, chat: Chat, close: () -> Unit) { + withBGApi { + val tagIds: List<Long> = if (tagId == null) { + emptyList() + } else { + listOf(tagId) + } + + try { + val result = apiSetChatTags(rh = rhId, type = chat.chatInfo.chatType, id = chat.chatInfo.apiId, tagIds = tagIds) + + if (result != null) { + val oldTags = chat.chatInfo.chatTags + chatModel.userTags.value = result.first + when (val cInfo = chat.chatInfo) { + is ChatInfo.Direct -> { + val contact = cInfo.contact.copy(chatTags = result.second) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, contact) + } + } + + is ChatInfo.Group -> { + val group = cInfo.groupInfo.copy(chatTags = result.second) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, group) + } + } + + else -> {} + } + chatModel.moveChatTagUnread(chat, oldTags, result.second) + close() + } + } catch (e: Exception) { + Log.d(TAG, "setChatTag error: ${e.message}") + } + } +} + +private fun deleteTag(rhId: Long?, tag: ChatTag, saving: MutableState<Boolean>) { + withBGApi { + saving.value = true + + try { + val tagId = tag.chatTagId + if (apiDeleteChatTag(rhId, tagId)) { + chatModel.userTags.value = chatModel.userTags.value.filter { it.chatTagId != tagId } + clearActiveChatFilterIfNeeded() + chatModel.chats.value.forEach { c -> + when (val cInfo = c.chatInfo) { + is ChatInfo.Direct -> { + val contact = cInfo.contact.copy(chatTags = cInfo.contact.chatTags.filter { it != tagId }) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, contact) + } + } + is ChatInfo.Group -> { + val group = cInfo.groupInfo.copy(chatTags = cInfo.groupInfo.chatTags.filter { it != tagId }) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, group) + } + } + else -> {} + } + } + } + + } catch (e: Exception) { + Log.d(TAG, "deleteTag error: ${e.message}") + } finally { + saving.value = false + } + } +} + +private fun deleteTagDialog(rhId: Long?, tag: ChatTag, saving: MutableState<Boolean>) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.delete_chat_list_question), + text = String.format(generalGetString(MR.strings.delete_chat_list_warning), tag.chatTagText), + buttons = { + SectionItemView({ + AlertManager.shared.hideAlert() + deleteTag(rhId, tag, saving) + }) { + Text( + generalGetString(MR.strings.confirm_verb), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = colors.error + ) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text( + stringResource(MR.strings.cancel_verb), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = colors.primary + ) + } + } + ) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt index d87c05a913..185ec3925f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt @@ -1,7 +1,7 @@ package chat.simplex.common.views.chatlist import SectionItemView -import androidx.compose.animation.core.* +import TextIconSpaced import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState @@ -12,41 +12,41 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.* import androidx.compose.ui.draw.* -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.TextStyle import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.platform.* import chat.simplex.common.views.CreateProfile +import chat.simplex.common.views.localauth.VerticalDivider import chat.simplex.common.views.remote.* -import chat.simplex.common.views.usersettings.doWithAuth +import chat.simplex.common.views.usersettings.* +import chat.simplex.common.views.usersettings.AppearanceScope.ColorModeSwitcher import chat.simplex.res.MR import dev.icerock.moko.resources.compose.stringResource -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlin.math.roundToInt + +private val USER_PICKER_SECTION_SPACING = 32.dp @Composable fun UserPicker( chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, - showSettings: Boolean = true, - showCancel: Boolean = false, - cancelClicked: () -> Unit = {}, - useFromDesktopClicked: () -> Unit = {}, - settingsClicked: () -> Unit = {}, + setPerformLA: (Boolean) -> Unit, ) { - val scope = rememberCoroutineScope() var newChat by remember { mutableStateOf(userPickerState.value) } if (newChat.isVisible()) { BackHandler { @@ -57,7 +57,7 @@ fun UserPicker( derivedStateOf { chatModel.users .filter { u -> u.user.activeUser || !u.user.hidden } - .sortedByDescending { it.user.activeUser } + .sortedByDescending { it.user.activeOrder } } } val remoteHosts by remember { @@ -66,18 +66,32 @@ fun UserPicker( .sortedBy { it.hostDeviceName } } } - val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) } + + val view = LocalMultiplatformView() LaunchedEffect(Unit) { launch { userPickerState.collect { newChat = it + if (it.isVisible()) { + hideKeyboard(view) + } launch { - animatedFloat.animateTo(if (newChat.isVisible()) 1f else 0f, newChatSheetAnimSpec()) if (newChat.isHiding()) userPickerState.value = AnimatedViewState.GONE } } } } + + LaunchedEffect(Unit) { + launch { + snapshotFlow { ModalManager.start.modalCount.value } + .filter { it > 0 } + .collect { + closePicker(userPickerState) + } + } + } + LaunchedEffect(Unit) { snapshotFlow { newChat.isVisible() } .distinctUntilChanged() @@ -116,176 +130,333 @@ fun UserPicker( } } LaunchedEffect(Unit) { - controller.reloadRemoteHosts() + // Controller.ctrl can be null when self-destructing activates + if (controller.ctrl != null && controller.ctrl != -1L) { + withBGApi { + controller.reloadRemoteHosts() + } + } } - val UsersView: @Composable ColumnScope.() -> Unit = { - users.forEach { u -> - UserProfilePickerItem(u.user, u.unreadCount, openSettings = settingsClicked) { + + val oneHandUI = remember { appPrefs.oneHandUI.state } + val iconColor = MaterialTheme.colors.secondaryVariant + val background = if (appPlatform.isAndroid) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha()) else MaterialTheme.colors.surface + PlatformUserPicker( + modifier = Modifier + .height(IntrinsicSize.Min) + .fillMaxWidth() + .then(if (newChat.isVisible()) Modifier.shadow(8.dp, clip = true, ambientColor = background) else Modifier) + .padding(top = if (appPlatform.isDesktop && oneHandUI.value) 7.dp else 0.dp) + .background(background) + .padding(bottom = USER_PICKER_SECTION_SPACING - DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL), + pickerState = userPickerState + ) { + val showCustomModal: (@Composable() (ModalData.(ChatModel, () -> Unit) -> Unit)) -> () -> Unit = { modalView -> + { + ModalManager.start.showCustomModal { close -> modalView(chatModel, close) } + } + } + val stopped = remember { chatModel.chatRunning }.value == false + val onUserClicked: (user: User) -> Unit = { user -> + if (!user.activeUser) { userPickerState.value = AnimatedViewState.HIDING - if (!u.user.activeUser) { - scope.launch { - controller.showProgressIfNeeded { - ModalManager.closeAllModalsEverywhere() - chatModel.controller.changeActiveUser(u.user.remoteHostId, u.user.userId, null) - } + withBGApi { + controller.showProgressIfNeeded { + ModalManager.closeAllModalsEverywhere() + chatModel.controller.changeActiveUser(user.remoteHostId, user.userId, null) } } + } else { + showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }() + withBGApi { + closePicker(userPickerState) + } } - Divider(Modifier.requiredHeight(1.dp)) - if (u.user.activeUser) Divider(Modifier.requiredHeight(0.5.dp)) } - } - val xOffset = with(LocalDensity.current) { 10.dp.roundToPx() } - val maxWidth = with(LocalDensity.current) { windowWidth() * density } - Box(Modifier - .fillMaxSize() - .offset { IntOffset(if (newChat.isGone()) -maxWidth.value.roundToInt() else xOffset, 0) } - .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { userPickerState.value = AnimatedViewState.HIDING }) - .padding(bottom = 10.dp, top = 10.dp) - .graphicsLayer { - alpha = animatedFloat.value - translationY = (animatedFloat.value - 1) * xOffset - } - ) { - Column( - Modifier - .widthIn(min = 260.dp) - .width(IntrinsicSize.Min) - .height(IntrinsicSize.Min) - .shadow(8.dp, RoundedCornerShape(corner = CornerSize(25.dp)), clip = true) - .background(MaterialTheme.colors.surface, RoundedCornerShape(corner = CornerSize(25.dp))) - .clip(RoundedCornerShape(corner = CornerSize(25.dp))) - ) { - val currentRemoteHost = remember { chatModel.currentRemoteHost }.value - Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) { - if (remoteHosts.isNotEmpty()) { - if (currentRemoteHost == null && chatModel.localUserCreated.value == true) { - LocalDevicePickerItem(true) { - userPickerState.value = AnimatedViewState.HIDING - switchToLocalDevice() - } - Divider(Modifier.requiredHeight(1.dp)) - } else if (currentRemoteHost != null) { - val connecting = rememberSaveable { mutableStateOf(false) } - RemoteHostPickerItem(currentRemoteHost, - actionButtonClick = { - userPickerState.value = AnimatedViewState.HIDING - stopRemoteHostAndReloadHosts(currentRemoteHost, true) - }) { - userPickerState.value = AnimatedViewState.HIDING - switchToRemoteHost(currentRemoteHost, connecting) - } - Divider(Modifier.requiredHeight(1.dp)) - } - } - UsersView() + @Composable + fun FirstSection() { + if (remoteHosts.isNotEmpty()) { + val currentRemoteHost = remember { chatModel.currentRemoteHost }.value + val localDeviceActive = currentRemoteHost == null && chatModel.localUserCreated.value == true - if (remoteHosts.isNotEmpty() && currentRemoteHost != null && chatModel.localUserCreated.value == true) { - LocalDevicePickerItem(false) { + DevicePickerRow( + localDeviceActive = localDeviceActive, + remoteHosts = remoteHosts, + onRemoteHostClick = { h, connecting -> + userPickerState.value = AnimatedViewState.HIDING + switchToRemoteHost(h, connecting) + }, + onLocalDeviceClick = { userPickerState.value = AnimatedViewState.HIDING switchToLocalDevice() + }, + onRemoteHostActionButtonClick = { h -> + userPickerState.value = AnimatedViewState.HIDING + stopRemoteHostAndReloadHosts(h, true) } - Divider(Modifier.requiredHeight(1.dp)) + ) + } + val currentUser = remember { chatModel.currentUser }.value + if (appPlatform.isAndroid) { + Column(modifier = Modifier.padding(top = USER_PICKER_SECTION_SPACING, bottom = USER_PICKER_SECTION_SPACING - DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL - 3.dp)) { + UserPickerUsersSection( + users = users, + onUserClicked = onUserClicked, + iconColor = iconColor, + stopped = stopped + ) } - remoteHosts.filter { !it.activeHost }.forEach { h -> - val connecting = rememberSaveable { mutableStateOf(false) } - RemoteHostPickerItem(h, - actionButtonClick = { - userPickerState.value = AnimatedViewState.HIDING - stopRemoteHostAndReloadHosts(h, false) - }) { - userPickerState.value = AnimatedViewState.HIDING - switchToRemoteHost(h, connecting) - } - Divider(Modifier.requiredHeight(1.dp)) + } else if (currentUser != null) { + SectionItemView({ onUserClicked(currentUser) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) { + ProfilePreview(currentUser.profile, iconColor = iconColor, stopped = stopped) } } - if (appPlatform.isAndroid) { - UseFromDesktopPickerItem { - ModalManager.start.showCustomModal { close -> - ConnectDesktopView(close) + } + + @Composable + fun SecondSection() { + UserPickerOptionRow( + painterResource(MR.images.ic_qr_code), + if (chatModel.userAddress.value != null) generalGetString(MR.strings.your_simplex_contact_address) else generalGetString(MR.strings.create_simplex_address), + showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped + ) + UserPickerOptionRow( + painterResource(MR.images.ic_toggle_on), + stringResource(MR.strings.chat_preferences), + click = if (stopped) null else ({ + showCustomModal { m, close -> + PreferencesView(m, m.currentUser.value ?: return@showCustomModal, close) + }() + }), + disabled = stopped + ) + if (appPlatform.isDesktop) { + Divider(Modifier.padding(DEFAULT_PADDING)) + + val inactiveUsers = users.filter { !it.user.activeUser } + + if (inactiveUsers.isNotEmpty()) { + Column(modifier = Modifier.padding(vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL)) { + UserPickerUsersSection( + users = inactiveUsers, + iconColor = iconColor, + onUserClicked = onUserClicked, + stopped = stopped + ) } - userPickerState.value = AnimatedViewState.GONE } - Divider(Modifier.requiredHeight(1.dp)) - } else if (remoteHosts.isEmpty()) { - LinkAMobilePickerItem { - ModalManager.start.showModal { - ConnectMobileView() - } - userPickerState.value = AnimatedViewState.GONE - } - Divider(Modifier.requiredHeight(1.dp)) - } else if (chatModel.desktopNoUserNoRemote) { - CreateInitialProfile { - doWithAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { - ModalManager.center.showModalCloseable { close -> - LaunchedEffect(Unit) { - userPickerState.value = AnimatedViewState.HIDING + } + + if (chatModel.desktopNoUserNoRemote) { + UserPickerOptionRow( + painterResource(MR.images.ic_manage_accounts), + generalGetString(MR.strings.create_chat_profile), + { + doWithAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { + ModalManager.center.showModalCloseable { close -> + LaunchedEffect(Unit) { + userPickerState.value = AnimatedViewState.HIDING + } + CreateProfile(chat.simplex.common.platform.chatModel, close) } - CreateProfile(chat.simplex.common.platform.chatModel, close) + } + } + ) + } else { + UserPickerOptionRow( + painterResource(MR.images.ic_manage_accounts), + stringResource(MR.strings.your_chat_profiles), + { + ModalManager.start.showCustomModal(keyboardCoversBar = false) { close -> + val search = rememberSaveable { mutableStateOf("") } + val profileHidden = rememberSaveable { mutableStateOf(false) } + val authorized = remember { stateGetOrPut("authorized") { false } } + ModalView( + { close() }, + showSearch = true, + searchAlwaysVisible = true, + onSearchValueChanged = { + search.value = it + }, + content = { + UserProfilesView(chatModel, search, profileHidden) { block -> + if (authorized.value) { + block() + } else { + doWithAuth( + generalGetString(MR.strings.auth_open_chat_profiles), + generalGetString(MR.strings.auth_log_in_using_credential) + ) { + authorized.value = true + block() + } + } + } + }) + } + }, + disabled = stopped + ) + } + } + + if (appPlatform.isDesktop || windowOrientation() == WindowOrientation.PORTRAIT) { + Column { + FirstSection() + SecondSection() + GlobalSettingsSection( + userPickerState = userPickerState, + setPerformLA = setPerformLA, + ) + } + } else { + Column { + FirstSection() + Row { + Box(Modifier.weight(1f)) { + Column { + SecondSection() + } + } + VerticalDivider() + Box(Modifier.weight(1f)) { + Column { + GlobalSettingsSection( + userPickerState = userPickerState, + setPerformLA = setPerformLA, + ) } } } - Divider(Modifier.requiredHeight(1.dp)) - } - if (showSettings) { - SettingsPickerItem(settingsClicked) - } - if (showCancel) { - CancelPickerItem(cancelClicked) } } } } +fun userPickerAlpha(): Float { + return when (CurrentColors.value.base) { + DefaultTheme.LIGHT -> 0.05f + DefaultTheme.DARK -> 0.05f + DefaultTheme.BLACK -> 0.075f + DefaultTheme.SIMPLEX -> 0.035f + } +} + @Composable -fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> Unit = {}, openSettings: () -> Unit = {}, onClick: () -> Unit) { +private fun GlobalSettingsSection( + userPickerState: MutableStateFlow<AnimatedViewState>, + setPerformLA: (Boolean) -> Unit, +) { + val stopped = remember { chatModel.chatRunning }.value == false + + if (appPlatform.isAndroid) { + val text = generalGetString(MR.strings.settings_section_title_use_from_desktop).lowercase().capitalize(Locale.current) + + UserPickerOptionRow( + painterResource(MR.images.ic_desktop), + text, + click = { + ModalManager.start.showCustomModal { close -> + ConnectDesktopView(close) + } + }, + disabled = stopped + ) + } else { + UserPickerOptionRow( + icon = painterResource(MR.images.ic_smartphone_300), + text = stringResource(if (remember { chat.simplex.common.platform.chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), + click = { + userPickerState.value = AnimatedViewState.HIDING + ModalManager.start.showModal { + ConnectMobileView() + } + }, + disabled = stopped + ) + } + + SectionItemView( + click = { + ModalManager.start.showModalCloseable { close -> + SettingsView(chatModel, setPerformLA, close) + } + }, + padding = if (appPlatform.isDesktop) PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING + 2.dp) else PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF) + ) { + val text = generalGetString(MR.strings.settings_section_title_settings).lowercase().capitalize(Locale.current) + Icon(painterResource(MR.images.ic_settings), text, tint = MaterialTheme.colors.secondary) + TextIconSpaced() + Text(text, color = Color.Unspecified) + Spacer(Modifier.weight(1f)) + ColorModeSwitcher() + } +} + +@Composable +fun UserProfilePickerItem( + u: User, + unreadCount: Int = 0, + enabled: Boolean = remember { chatModel.chatRunning }.value == true || chatModel.connectedToRemote, + onLongClick: () -> Unit = {}, + openSettings: () -> Unit = {}, + onClick: () -> Unit +) { Row( Modifier .fillMaxWidth() - .sizeIn(minHeight = 46.dp) + .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT) .combinedClickable( + enabled = enabled, onClick = if (u.activeUser) openSettings else onClick, onLongClick = onLongClick, interactionSource = remember { MutableInteractionSource() }, indication = if (!u.activeUser) LocalIndication.current else null ) - .onRightClick { onLongClick() } + .onRightClick { if (enabled) onLongClick() } .padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - UserProfileRow(u) + UserProfileRow(u, enabled) if (u.activeUser) { Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) - } else if (u.hidden) { - Icon(painterResource(MR.images.ic_lock), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) - } else if (unreadCount > 0) { - Box( - contentAlignment = Alignment.Center - ) { - Text( - unreadCountStr(unreadCount), - color = Color.White, - fontSize = 11.sp, - modifier = Modifier - .background(MaterialTheme.colors.primaryVariant, shape = CircleShape) - .padding(2.dp) - .badgeLayout() - ) + } else { + Row(verticalAlignment = Alignment.CenterVertically) { + if (unreadCount > 0) { + Box( + contentAlignment = Alignment.Center, + ) { + Text( + unreadCountStr(unreadCount), + color = Color.White, + fontSize = 10.sp, + modifier = Modifier + .background(if (u.showNtfs) MaterialTheme.colors.primaryVariant else MaterialTheme.colors.secondary, shape = CircleShape) + .padding(2.dp) + .badgeLayout() + ) + } + + if (u.hidden) { + Spacer(Modifier.width(8.dp)) + Icon(painterResource(MR.images.ic_lock), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) + } + } else if (u.hidden) { + Icon(painterResource(MR.images.ic_lock), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) + } else if (!u.showNtfs) { + Icon(painterResource(MR.images.ic_notifications_off), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) + } else { + Box(Modifier.size(20.dp)) + } } - } else if (!u.showNtfs) { - Icon(painterResource(MR.images.ic_notifications_off), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) - } else { - Box(Modifier.size(20.dp)) } } } @Composable -fun UserProfileRow(u: User) { +fun UserProfileRow(u: User, enabled: Boolean = remember { chatModel.chatRunning }.value == true || chatModel.connectedToRemote) { Row( Modifier .widthIn(max = windowWidth() * 0.7f) @@ -294,149 +465,157 @@ fun UserProfileRow(u: User) { ) { ProfileImage( image = u.image, - size = 54.dp + size = 54.dp * fontSizeSqrtMultiplier ) Text( u.displayName, modifier = Modifier .padding(start = 10.dp, end = 8.dp), - color = if (isInDarkTheme()) MenuTextColorDark else Color.Black, + color = if (enabled) MenuTextColor else MaterialTheme.colors.secondary, fontWeight = if (u.activeUser) FontWeight.Medium else FontWeight.Normal ) } } @Composable -fun RemoteHostPickerItem(h: RemoteHostInfo, onLongClick: () -> Unit = {}, actionButtonClick: () -> Unit = {}, onClick: () -> Unit) { - Row( +fun UserPickerOptionRow(icon: Painter, text: String, click: (() -> Unit)? = null, disabled: Boolean = false) { + SectionItemView(click, disabled = disabled, extraPadding = appPlatform.isDesktop) { + Icon(icon, text, tint = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.secondary) + TextIconSpaced() + Text(text = text, color = if (disabled) MaterialTheme.colors.secondary else Color.Unspecified) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun DevicePickerRow( + localDeviceActive: Boolean, + remoteHosts: List<RemoteHostInfo>, + onLocalDeviceClick: () -> Unit, + onRemoteHostClick: (rh: RemoteHostInfo, connecting: MutableState<Boolean>) -> Unit, + onRemoteHostActionButtonClick: (rh: RemoteHostInfo) -> Unit, +) { + FlowRow( Modifier .fillMaxWidth() - .background(color = if (h.activeHost) MaterialTheme.colors.surface.mixWith(MaterialTheme.colors.onBackground, 0.95f) else Color.Unspecified) - .sizeIn(minHeight = 46.dp) - .combinedClickable( - onClick = onClick, - onLongClick = onLongClick - ) - .onRightClick { onLongClick() } - .padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT) + .padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING, top = DEFAULT_PADDING + DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) ) { - RemoteHostRow(h) - if (h.sessionState is RemoteHostSessionState.Connected) { - HostDisconnectButton(actionButtonClick) - } else { - Box(Modifier.size(20.dp)) + val activeHost = remoteHosts.firstOrNull { h -> h.activeHost } + + if (activeHost != null) { + val connecting = rememberSaveable { mutableStateOf(false) } + + DevicePill( + active = true, + icon = painterResource(MR.images.ic_smartphone_300), + text = activeHost.hostDeviceName, + actionButtonVisible = activeHost.sessionState is RemoteHostSessionState.Connected, + onActionButtonClick = { onRemoteHostActionButtonClick(activeHost) } + ) { + onRemoteHostClick(activeHost, connecting) + } + } + + DevicePill( + active = localDeviceActive, + icon = painterResource(MR.images.ic_desktop), + text = stringResource(MR.strings.this_device), + actionButtonVisible = false + ) { + onLocalDeviceClick() + } + + remoteHosts.filter { h -> h.sessionState is RemoteHostSessionState.Connected && !h.activeHost }.forEach { h -> + val connecting = rememberSaveable { mutableStateOf(false) } + + DevicePill( + active = h.activeHost, + icon = painterResource(MR.images.ic_smartphone_300), + text = h.hostDeviceName, + actionButtonVisible = h.sessionState is RemoteHostSessionState.Connected, + onActionButtonClick = { onRemoteHostActionButtonClick(h) } + ) { + onRemoteHostClick(h, connecting) + } } } } @Composable -fun RemoteHostRow(h: RemoteHostInfo) { - Row( - Modifier - .widthIn(max = windowWidth() * 0.7f) - .padding(start = 17.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(painterResource(MR.images.ic_smartphone_300), h.hostDeviceName, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) - Text( - h.hostDeviceName, - modifier = Modifier.padding(start = 26.dp, end = 8.dp), - color = if (h.activeHost) MaterialTheme.colors.onBackground else if (isInDarkTheme()) MenuTextColorDark else Color.Black, - fontSize = 14.sp, - ) - } -} +expect fun UserPickerUsersSection( + users: List<UserInfo>, + iconColor: Color, + stopped: Boolean, + onUserClicked: (user: User) -> Unit, +) @Composable -fun LocalDevicePickerItem(active: Boolean, onLongClick: () -> Unit = {}, onClick: () -> Unit) { +expect fun PlatformUserPicker( + modifier: Modifier, + pickerState: MutableStateFlow<AnimatedViewState>, + content: @Composable () -> Unit +) + +@Composable +fun DevicePill( + active: Boolean, + icon: Painter, + text: String, + actionButtonVisible: Boolean, + onActionButtonClick: (() -> Unit)? = null, + onClick: () -> Unit) { Row( Modifier - .fillMaxWidth() - .background(color = if (active) MaterialTheme.colors.surface.mixWith(MaterialTheme.colors.onBackground, 0.95f) else Color.Unspecified) - .sizeIn(minHeight = 46.dp) - .combinedClickable( - onClick = if (active) {{}} else onClick, - onLongClick = onLongClick, - interactionSource = remember { MutableInteractionSource() }, - indication = if (!active) LocalIndication.current else null + .clip(RoundedCornerShape(8.dp)) + .border( + BorderStroke(1.dp, MaterialTheme.colors.secondaryVariant), + shape = RoundedCornerShape(8.dp) ) - .onRightClick { onLongClick() } - .padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING), - horizontalArrangement = Arrangement.SpaceBetween, + .background(if (active) MaterialTheme.colors.secondaryVariant else Color.Transparent) + .clickable( + enabled = !active, + onClick = onClick, + interactionSource = remember { MutableInteractionSource() }, + indication = LocalIndication.current + ), verticalAlignment = Alignment.CenterVertically ) { - LocalDeviceRow(active) - Box(Modifier.size(20.dp)) - } -} - -@Composable -fun LocalDeviceRow(active: Boolean) { - Row( - Modifier - .widthIn(max = windowWidth() * 0.7f) - .padding(start = 17.dp, end = DEFAULT_PADDING), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(painterResource(MR.images.ic_desktop), stringResource(MR.strings.this_device), Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) - Text( - stringResource(MR.strings.this_device), - modifier = Modifier.padding(start = 26.dp, end = 8.dp), - color = if (active) MaterialTheme.colors.onBackground else if (isInDarkTheme()) MenuTextColorDark else Color.Black, - fontSize = 14.sp, - ) - } -} - -@Composable -private fun UseFromDesktopPickerItem(onClick: () -> Unit) { - SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { - val text = generalGetString(MR.strings.settings_section_title_use_from_desktop).lowercase().capitalize(Locale.current) - Icon(painterResource(MR.images.ic_desktop), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) - Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) - Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black) - } -} - -@Composable -private fun LinkAMobilePickerItem(onClick: () -> Unit) { - SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { - val text = generalGetString(MR.strings.link_a_mobile) - Icon(painterResource(MR.images.ic_smartphone_300), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) - Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) - Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black) - } -} - -@Composable -private fun CreateInitialProfile(onClick: () -> Unit) { - SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { - val text = generalGetString(MR.strings.create_chat_profile) - Icon(painterResource(MR.images.ic_manage_accounts), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) - Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) - Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black) - } -} - -@Composable -private fun SettingsPickerItem(onClick: () -> Unit) { - SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { - val text = generalGetString(MR.strings.settings_section_title_settings).lowercase().capitalize(Locale.current) - Icon(painterResource(MR.images.ic_settings), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) - Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) - Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black) - } -} - -@Composable -private fun CancelPickerItem(onClick: () -> Unit) { - SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { - val text = generalGetString(MR.strings.cancel_verb) - Icon(painterResource(MR.images.ic_close), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) - Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) - Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black) + Row( + Modifier.padding(horizontal = 6.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + icon, + text, + Modifier.size(16.dp * fontSizeSqrtMultiplier), + tint = MaterialTheme.colors.onSurface + ) + Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON * fontSizeSqrtMultiplier)) + Text( + text, + color = MaterialTheme.colors.onSurface, + fontSize = 12.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + modifier = if (onActionButtonClick != null && actionButtonVisible) Modifier.widthIn(max = 300.dp * fontSizeSqrtMultiplier) else Modifier + ) + if (onActionButtonClick != null && actionButtonVisible) { + val interactionSource = remember { MutableInteractionSource() } + val hovered = interactionSource.collectIsHoveredAsState().value + Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON * fontSizeSqrtMultiplier)) + IconButton(onActionButtonClick, Modifier.requiredSize(16.dp * fontSizeSqrtMultiplier)) { + Icon( + painterResource(if (hovered) MR.images.ic_wifi_off else MR.images.ic_wifi), + null, + Modifier.size(16.dp * fontSizeSqrtMultiplier).hoverable(interactionSource), + tint = if (hovered) WarningOrange else MaterialTheme.colors.onBackground + ) + } + } + } } } @@ -444,7 +623,7 @@ private fun CancelPickerItem(onClick: () -> Unit) { fun HostDisconnectButton(onClick: (() -> Unit)?) { val interactionSource = remember { MutableInteractionSource() } val hovered = interactionSource.collectIsHoveredAsState().value - IconButton(onClick ?: {}, Modifier.requiredSize(20.dp), enabled = onClick != null) { + IconButton(onClick ?: {}, Modifier.requiredSize(20.dp * fontSizeSqrtMultiplier), enabled = onClick != null) { Icon( painterResource(if (onClick == null) MR.images.ic_desktop else if (hovered) MR.images.ic_wifi_off else MR.images.ic_wifi), null, @@ -454,6 +633,28 @@ fun HostDisconnectButton(onClick: (() -> Unit)?) { } } +@Composable +fun BoxScope.unreadBadge(unreadCount: Int, userMuted: Boolean, hasPadding: Boolean) { + Text( + if (unreadCount > 0) unreadCountStr(unreadCount) else "", + color = Color.White, + fontSize = 10.sp, + style = TextStyle(textAlign = TextAlign.Center), + modifier = Modifier + .offset(y = if (hasPadding) 3.sp.toDp() else -4.sp.toDp(), x = if (hasPadding) 0.dp else 4.sp.toDp()) + .background(if (userMuted) MaterialTheme.colors.primaryVariant else MaterialTheme.colors.secondary, shape = CircleShape) + .badgeLayout() + .padding(horizontal = 2.sp.toDp()) + .padding(vertical = 2.sp.toDp()) + .align(Alignment.TopEnd) + ) +} + +private suspend fun closePicker(userPickerState: MutableStateFlow<AnimatedViewState>) { + delay(500) + userPickerState.value = AnimatedViewState.HIDING +} + private fun switchToLocalDevice() { withBGApi { chatController.switchUIRemoteHost(null) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt new file mode 100644 index 0000000000..4e65a3649e --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt @@ -0,0 +1,138 @@ +package chat.simplex.common.views.contacts + +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.chat.item.ItemAction +import chat.simplex.common.views.chatlist.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.ContactType +import chat.simplex.common.views.newchat.chatContactType +import chat.simplex.res.MR +import kotlinx.coroutines.delay + +fun onRequestAccepted(chat: Chat) { + val chatInfo = chat.chatInfo + if (chatInfo is ChatInfo.Direct) { + ModalManager.start.closeModals() + if (chatInfo.contact.sndReady) { + withApi { + openLoadedChat(chat) + } + } + } +} + +@Composable +fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, showDeletedChatIcon: Boolean) { + val showMenu = remember { mutableStateOf(false) } + val rhId = chat.remoteHostId + val disabled = chatModel.chatRunning.value == false || chatModel.deletedChats.value.contains(rhId to chat.chatInfo.id) + val contactType = chatContactType(chat) + + LaunchedEffect(chat.id) { + showMenu.value = false + delay(500L) + } + + val selectedChat = remember(chat.id) { derivedStateOf { chat.id == chatModel.chatId.value } } + val view = LocalMultiplatformView() + + when (chat.chatInfo) { + is ChatInfo.Direct -> { + ChatListNavLinkLayout( + chatLinkPreview = { + tryOrShowError("${chat.id}ContactListNavLink", error = { ErrorChatListItem() }) { + ContactPreviewView(chat, disabled, showDeletedChatIcon) + } + }, + click = { + hideKeyboard(view) + when (contactType) { + ContactType.RECENT -> { + withApi { + openChat(secondaryChatsCtx = null, rhId, chat.chatInfo) + ModalManager.start.closeModals() + } + } + ContactType.CHAT_DELETED -> { + withApi { + openChat(secondaryChatsCtx = null, rhId, chat.chatInfo) + ModalManager.start.closeModals() + } + } + ContactType.CARD -> { + askCurrentOrIncognitoProfileConnectContactViaAddress( + chatModel, + rhId, + chat.chatInfo.contact, + close = { ModalManager.start.closeModals() }, + openChat = true + ) + } + else -> {} + } + }, + dropdownMenuItems = { + tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) { + DeleteContactAction(chat, chatModel, showMenu) + } + }, + showMenu, + disabled, + selectedChat, + nextChatSelected, + ) + } + is ChatInfo.ContactRequest -> { + ChatListNavLinkLayout( + chatLinkPreview = { + tryOrShowError("${chat.id}ContactListNavLink", error = { ErrorChatListItem() }) { + ContactPreviewView(chat, disabled, showDeletedChatIcon) + } + }, + click = { + hideKeyboard(view) + contactRequestAlertDialog( + rhId, + chat.chatInfo, + chatModel, + onSucess = { onRequestAccepted(it) } + ) + }, + dropdownMenuItems = { + tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) { + ContactRequestMenuItems( + rhId = chat.remoteHostId, + chatInfo = chat.chatInfo, + chatModel = chatModel, + showMenu = showMenu, + onSuccess = { onRequestAccepted(it) } + ) + } + }, + showMenu, + disabled, + selectedChat, + nextChatSelected) + } + else -> {} + } +} + +@Composable +fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) { + ItemAction( + stringResource(MR.strings.delete_contact_menu_action), + painterResource(MR.images.ic_delete), + onClick = { + deleteContactDialog(chat, chatModel) + showMenu.value = false + }, + color = Color.Red + ) +} \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt new file mode 100644 index 0000000000..dd03bca921 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt @@ -0,0 +1,143 @@ +package chat.simplex.common.views.contacts + +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.graphics.Color +import dev.icerock.moko.resources.compose.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.chatModel +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.newchat.ContactType +import chat.simplex.common.views.newchat.chatContactType +import chat.simplex.res.MR + +@Composable +fun ContactPreviewView( + chat: Chat, + disabled: Boolean, + showDeletedChatIcon: Boolean +) { + val cInfo = chat.chatInfo + val contactType = chatContactType(chat) + + @Composable + fun VerifiedIcon() { + Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary) + } + + @Composable + fun chatPreviewTitle() { + val deleting by remember(disabled, chat.id) { mutableStateOf(chatModel.deletedChats.value.contains(chat.remoteHostId to chat.chatInfo.id)) } + + val textColor = when { + deleting -> MaterialTheme.colors.secondary + contactType == ContactType.CARD -> MaterialTheme.colors.primary + contactType == ContactType.REQUEST -> MaterialTheme.colors.primary + contactType == ContactType.RECENT && chat.chatInfo.incognito -> Indigo + else -> Color.Unspecified + } + + when (cInfo) { + is ChatInfo.Direct -> + Row(verticalAlignment = Alignment.CenterVertically) { + if (cInfo.contact.verified) { + VerifiedIcon() + } + Text( + cInfo.chatViewName, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = textColor + ) + } + is ChatInfo.ContactRequest -> + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + cInfo.chatViewName, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = textColor + ) + } + else -> {} + } + } + + Row( + modifier = Modifier.padding(PaddingValues(horizontal = DEFAULT_PADDING_HALF)), + verticalAlignment = Alignment.CenterVertically, + ) { + Box(contentAlignment = Alignment.BottomEnd) { + ChatInfoImage(cInfo, size = 42.dp) + } + + Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON)) + + Box(modifier = Modifier.weight(10f, fill = true)) { + chatPreviewTitle() + } + + Spacer(Modifier.fillMaxWidth().weight(1f)) + + if (chat.chatInfo is ChatInfo.ContactRequest) { + Icon( + painterResource(MR.images.ic_check), + contentDescription = null, + tint = MaterialTheme.colors.primary, + modifier = Modifier + .size(23.dp) + ) + } + + if (contactType == ContactType.CARD) { + Icon( + painterResource(MR.images.ic_mail), + contentDescription = null, + tint = MaterialTheme.colors.primary, + modifier = Modifier + .size(21.dp) + ) + } + + if (showDeletedChatIcon && chat.chatInfo.chatDeleted) { + Icon( + painterResource(MR.images.ic_inventory_2), + contentDescription = null, + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .size(17.dp) + ) + if (chat.chatInfo.incognito) { + Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON)) + } + } else if (chat.chatInfo.chatSettings?.favorite == true) { + Icon( + painterResource(MR.images.ic_star_filled), + contentDescription = null, + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .size(17.dp) + ) + if (chat.chatInfo.incognito) { + Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON)) + } + } + + + if (chat.chatInfo.incognito) { + Icon( + painterResource(MR.images.ic_theater_comedy), + contentDescription = null, + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .size(21.dp) + ) + } + } +} \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/ChatArchiveView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/ChatArchiveView.kt deleted file mode 100644 index 8738db2df1..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/ChatArchiveView.kt +++ /dev/null @@ -1,110 +0,0 @@ -package chat.simplex.common.views.database - -import SectionBottomSpacer -import SectionTextFooter -import SectionView -import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource -import chat.simplex.common.model.ChatModel -import chat.simplex.common.platform.* -import chat.simplex.common.ui.theme.SimpleXTheme -import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.usersettings.* -import chat.simplex.res.MR -import kotlinx.datetime.* -import java.io.File -import java.net.URI -import java.text.SimpleDateFormat -import java.util.* - -@Composable -fun ChatArchiveView(m: ChatModel, title: String, archiveName: String, archiveTime: Instant) { - val archivePath = filesDir.absolutePath + File.separator + archiveName - val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? -> - if (to != null) { - copyFileToFile(File(archivePath), to) {} - } - } - ChatArchiveLayout( - title, - archiveTime, - saveArchive = { withApi { saveArchiveLauncher.launch(archivePath.substringAfterLast(File.separator)) }}, - deleteArchiveAlert = { deleteArchiveAlert(m, archivePath) } - ) -} - -@Composable -fun ChatArchiveLayout( - title: String, - archiveTime: Instant, - saveArchive: () -> Unit, - deleteArchiveAlert: () -> Unit -) { - Column( - Modifier.fillMaxWidth(), - ) { - AppBarTitle(title) - SectionView(stringResource(MR.strings.chat_archive_section)) { - SettingsActionItem( - painterResource(MR.images.ic_ios_share), - stringResource(MR.strings.save_archive), - saveArchive, - textColor = MaterialTheme.colors.primary, - iconColor = MaterialTheme.colors.primary, - ) - SettingsActionItem( - painterResource(MR.images.ic_delete), - stringResource(MR.strings.delete_archive), - deleteArchiveAlert, - textColor = Color.Red, - iconColor = Color.Red, - ) - } - val archiveTs = SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US).format(Date.from(archiveTime.toJavaInstant())) - SectionTextFooter( - String.format(generalGetString(MR.strings.archive_created_on_ts), archiveTs) - ) - SectionBottomSpacer() - } -} - -private fun deleteArchiveAlert(m: ChatModel, archivePath: String) { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.delete_chat_archive_question), - confirmText = generalGetString(MR.strings.delete_verb), - onConfirm = { - val fileDeleted = File(archivePath).delete() - if (fileDeleted) { - m.controller.appPrefs.chatArchiveName.set(null) - m.controller.appPrefs.chatArchiveTime.set(null) - ModalManager.start.closeModal() - } else { - Log.e(TAG, "deleteArchiveAlert delete() error") - } - }, - destructive = true, - ) -} - -@Preview/*( - uiMode = Configuration.UI_MODE_NIGHT_YES, - showBackground = true, - name = "Dark Mode" -)*/ -@Composable -fun PreviewChatArchiveLayout() { - SimpleXTheme { - ChatArchiveLayout( - "New database archive", - archiveTime = Clock.System.now(), - saveArchive = {}, - deleteArchiveAlert = {} - ) - } -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt index e34f80a7ef..1c1c37b7ac 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt @@ -1,9 +1,8 @@ package chat.simplex.common.views.database import SectionBottomSpacer -import SectionItemView import SectionItemViewSpaceBetween -import SectionTextFooter +import SectionSpacer import SectionView import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource @@ -24,36 +23,38 @@ import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.* import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.* 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.model.* -import chat.simplex.common.platform.appPlatform +import chat.simplex.common.views.usersettings.SettingsActionItem import chat.simplex.res.MR +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.datetime.Clock import kotlin.math.log2 @Composable -fun DatabaseEncryptionView(m: ChatModel) { +fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) { val progressIndicator = remember { mutableStateOf(false) } - val prefs = m.controller.appPrefs - val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } - val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) } + val useKeychain = remember { mutableStateOf(appPrefs.storeDBPassphrase.get()) } + val initialRandomDBPassphrase = remember { mutableStateOf(appPrefs.initialRandomDBPassphrase.get()) } val storedKey = remember { val key = DatabaseUtils.ksDatabasePassword.get(); mutableStateOf(key != null && key != "") } // Do not do rememberSaveable on current key to prevent saving it on disk in clear text val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") } val newKey = rememberSaveable { mutableStateOf("") } val confirmNewKey = rememberSaveable { mutableStateOf("") } + val chatLastStart = remember { mutableStateOf(appPrefs.chatLastStart.get()) } Box( Modifier.fillMaxSize(), ) { DatabaseEncryptionLayout( useKeychain, - prefs, m.chatDbEncrypted.value, currentKey, newKey, @@ -61,9 +62,21 @@ fun DatabaseEncryptionView(m: ChatModel) { storedKey, initialRandomDBPassphrase, progressIndicator, + migration, onConfirmEncrypt = { - withApi { - encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator) + // it will try to stop and start the chat in case of: non-migration && successful encryption. In migration the chat will remain stopped + stopChatRunBlockStartChat(migration, chatLastStart, progressIndicator, ) { + val success = encryptDatabase( + currentKey = currentKey, + newKey = newKey, + confirmNewKey = confirmNewKey, + initialRandomDBPassphrase = initialRandomDBPassphrase, + useKeychain = useKeychain, + storedKey = storedKey, + progressIndicator = progressIndicator, + migration = migration + ) + success && !migration } } ) @@ -87,7 +100,6 @@ fun DatabaseEncryptionView(m: ChatModel) { @Composable fun DatabaseEncryptionLayout( useKeychain: MutableState<Boolean>, - prefs: AppPreferences, chatDbEncrypted: Boolean?, currentKey: MutableState<String>, newKey: MutableState<String>, @@ -95,88 +107,108 @@ fun DatabaseEncryptionLayout( storedKey: MutableState<Boolean>, initialRandomDBPassphrase: MutableState<Boolean>, progressIndicator: MutableState<Boolean>, + migration: Boolean, onConfirmEncrypt: () -> Unit, ) { - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - ) { - AppBarTitle(stringResource(MR.strings.database_passphrase)) - SectionView(null) { - SavePassphraseSetting(useKeychain.value, initialRandomDBPassphrase.value, storedKey.value, progressIndicator.value) { checked -> - if (checked) { - setUseKeychain(true, useKeychain, prefs) - } else if (storedKey.value) { - removePassphraseAlert { - DatabaseUtils.ksDatabasePassword.remove() - setUseKeychain(false, useKeychain, prefs) - storedKey.value = false - } - } else { - setUseKeychain(false, useKeychain, prefs) - } + @Composable + fun Layout() { + Column { + if (!migration) { + AppBarTitle(stringResource(MR.strings.database_passphrase)) + } else { + ChatStoppedView() + SectionSpacer() } + SectionView(if (migration) generalGetString(MR.strings.database_passphrase).uppercase() else null) { + SavePassphraseSetting( + useKeychain.value, + initialRandomDBPassphrase.value, + storedKey.value, + enabled = (!initialRandomDBPassphrase.value && !progressIndicator.value) || migration + ) { checked -> + if (checked) { + setUseKeychain(true, useKeychain, migration) + } else if (storedKey.value && !migration) { + // Don't show in migration process since it will remove the key after successful encryption + removePassphraseAlert { + removePassphraseFromKeyChain(useKeychain, storedKey, false) + } + } else { + setUseKeychain(false, useKeychain, migration) + } + } + + if (!initialRandomDBPassphrase.value && chatDbEncrypted == true) { + PassphraseField( + currentKey, + generalGetString(MR.strings.current_passphrase), + modifier = Modifier.padding(horizontal = DEFAULT_PADDING), + isValid = ::validKey, + keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), + ) + } - if (!initialRandomDBPassphrase.value && chatDbEncrypted == true) { PassphraseField( - currentKey, - generalGetString(MR.strings.current_passphrase), + newKey, + generalGetString(MR.strings.new_passphrase), modifier = Modifier.padding(horizontal = DEFAULT_PADDING), + showStrength = true, isValid = ::validKey, keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), ) - } - - PassphraseField( - newKey, - generalGetString(MR.strings.new_passphrase), - modifier = Modifier.padding(horizontal = DEFAULT_PADDING), - showStrength = true, - isValid = ::validKey, - keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), - ) - val onClickUpdate = { - // Don't do things concurrently. Shouldn't be here concurrently, just in case - if (!progressIndicator.value) { - if (currentKey.value == "") { - if (useKeychain.value) - encryptDatabaseSavedAlert(onConfirmEncrypt) - else - encryptDatabaseAlert(onConfirmEncrypt) - } else { - if (useKeychain.value) - changeDatabaseKeySavedAlert(onConfirmEncrypt) - else - changeDatabaseKeyAlert(onConfirmEncrypt) + val onClickUpdate = { + // Don't do things concurrently. Shouldn't be here concurrently, just in case + if (!progressIndicator.value) { + if (currentKey.value == "") { + if (useKeychain.value) + encryptDatabaseSavedAlert(onConfirmEncrypt) + else + encryptDatabaseAlert(onConfirmEncrypt) + } else { + if (useKeychain.value) + changeDatabaseKeySavedAlert(onConfirmEncrypt) + else + changeDatabaseKeyAlert(onConfirmEncrypt) + } } } + val disabled = currentKey.value == newKey.value || + newKey.value != confirmNewKey.value || + newKey.value.isEmpty() || + !validKey(currentKey.value) || + !validKey(newKey.value) || + progressIndicator.value + + PassphraseField( + confirmNewKey, + generalGetString(MR.strings.confirm_new_passphrase), + modifier = Modifier.padding(horizontal = DEFAULT_PADDING), + isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value }, + keyboardActions = KeyboardActions(onDone = { + if (!disabled) onClickUpdate() + defaultKeyboardAction(ImeAction.Done) + }), + ) + + SectionItemViewSpaceBetween(onClickUpdate, disabled = disabled, minHeight = TextFieldDefaults.MinHeight) { + Text(generalGetString(if (migration) MR.strings.set_passphrase else MR.strings.update_database_passphrase), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + } } - val disabled = currentKey.value == newKey.value || - newKey.value != confirmNewKey.value || - newKey.value.isEmpty() || - !validKey(currentKey.value) || - !validKey(newKey.value) || - progressIndicator.value - PassphraseField( - confirmNewKey, - generalGetString(MR.strings.confirm_new_passphrase), - modifier = Modifier.padding(horizontal = DEFAULT_PADDING), - isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value }, - keyboardActions = KeyboardActions(onDone = { - if (!disabled) onClickUpdate() - defaultKeyboardAction(ImeAction.Done) - }), - ) - - SectionItemViewSpaceBetween(onClickUpdate, disabled = disabled, minHeight = TextFieldDefaults.MinHeight) { - Text(generalGetString(MR.strings.update_database_passphrase), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + Column { + DatabaseEncryptionFooter(useKeychain, chatDbEncrypted, storedKey, initialRandomDBPassphrase, migration) } + SectionBottomSpacer() } - - Column { - DatabaseEncryptionFooter(useKeychain, chatDbEncrypted, storedKey, initialRandomDBPassphrase) + } + if (migration) { + Column(Modifier.fillMaxWidth()) { + Layout() + } + } else { + ColumnWithScrollBar(maxIntrinsicSize = true) { + Layout() } - SectionBottomSpacer() } } @@ -211,8 +243,9 @@ expect fun SavePassphraseSetting( useKeychain: Boolean, initialRandomDBPassphrase: Boolean, storedKey: Boolean, - progressIndicator: Boolean, minHeight: Dp = TextFieldDefaults.MinHeight, + enabled: Boolean, + smallPadding: Boolean = true, onCheckedChange: (Boolean) -> Unit, ) @@ -222,8 +255,18 @@ expect fun DatabaseEncryptionFooter( chatDbEncrypted: Boolean?, storedKey: MutableState<Boolean>, initialRandomDBPassphrase: MutableState<Boolean>, + migration: Boolean, ) +@Composable +fun ChatStoppedView() { + SettingsActionItem( + icon = painterResource(MR.images.ic_report_filled), + text = stringResource(MR.strings.chat_is_stopped), + iconColor = Color.Red, + ) +} + fun resetFormAfterEncryption( m: ChatModel, initialRandomDBPassphrase: MutableState<Boolean>, @@ -233,18 +276,27 @@ fun resetFormAfterEncryption( storedKey: MutableState<Boolean>, stored: Boolean = false, ) { - m.chatDbEncrypted.value = true - initialRandomDBPassphrase.value = false - m.controller.appPrefs.initialRandomDBPassphrase.set(false) currentKey.value = "" newKey.value = "" confirmNewKey.value = "" storedKey.value = stored + m.chatDbEncrypted.value = true + initialRandomDBPassphrase.value = false + m.controller.appPrefs.initialRandomDBPassphrase.set(false) } -fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, prefs: AppPreferences) { +fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, migration: Boolean) { useKeychain.value = value - prefs.storeDBPassphrase.set(value) + // Postpone it when migrating to the end of encryption process + if (!migration) { + appPrefs.storeDBPassphrase.set(value) + } +} + +private fun removePassphraseFromKeyChain(useKeychain: MutableState<Boolean>, storedKey: MutableState<Boolean>, migration: Boolean) { + DatabaseUtils.ksDatabasePassword.remove() + setUseKeychain(false, useKeychain, migration) + storedKey.value = false } fun storeSecurelySaved() = generalGetString(MR.strings.store_passphrase_securely) @@ -257,7 +309,6 @@ private fun operationEnded(m: ChatModel, progressIndicator: MutableState<Boolean alert.invoke() } -@OptIn(ExperimentalComposeUiApi::class) @Composable fun PassphraseField( key: MutableState<String>, @@ -267,6 +318,7 @@ fun PassphraseField( isValid: (String) -> Boolean, keyboardActions: KeyboardActions = KeyboardActions(), dependsOn: State<Any?>? = null, + requestFocus: Boolean = false, ) { var valid by remember { mutableStateOf(validKey(key.value)) } var showKey by remember { mutableStateOf(false) } @@ -295,6 +347,7 @@ fun PassphraseField( val color = MaterialTheme.colors.onBackground val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize) val interactionSource = remember { MutableInteractionSource() } + val focusRequester = remember { FocusRequester() } BasicTextField( value = state.value, modifier = modifier @@ -304,7 +357,8 @@ fun PassphraseField( .defaultMinSize( minWidth = TextFieldDefaults.MinWidth, minHeight = TextFieldDefaults.MinHeight - ), + ) + .focusRequester(focusRequester), onValueChange = { state.value = it key.value = it.text @@ -347,6 +401,12 @@ fun PassphraseField( ) } ) + LaunchedEffect(Unit) { + if (requestFocus) { + delay(200) + focusRequester.requestFocus() + } + } LaunchedEffect(Unit) { snapshotFlow { dependsOn?.value } .distinctUntilChanged() @@ -363,16 +423,19 @@ suspend fun encryptDatabase( initialRandomDBPassphrase: MutableState<Boolean>, useKeychain: MutableState<Boolean>, storedKey: MutableState<Boolean>, - progressIndicator: MutableState<Boolean> + progressIndicator: MutableState<Boolean>, + migration: Boolean, ): Boolean { val m = ChatModel - val prefs = ChatController.appPrefs progressIndicator.value = true return try { - prefs.encryptionStartedAt.set(Clock.System.now()) + appPrefs.encryptionStartedAt.set(Clock.System.now()) + if (!m.chatDbChanged.value) { + m.controller.apiSaveAppSettings(AppSettings.current.prepareForExport()) + } val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value) - prefs.encryptionStartedAt.set(null) - val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError + appPrefs.encryptionStartedAt.set(null) + val sqliteError = ((error as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError when { sqliteError is SQLiteError.ErrorNotADatabase -> { operationEnded(m, progressIndicator) { @@ -386,18 +449,22 @@ suspend fun encryptDatabase( error != null -> { operationEnded(m, progressIndicator) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database), - "failed to set storage encryption: ${error.responseType} ${error.details}" + "failed to set storage encryption: error ${error.string}" ) } false } else -> { - prefs.initialRandomDBPassphrase.set(false) - initialRandomDBPassphrase.value = false - if (useKeychain.value) { - DatabaseUtils.ksDatabasePassword.set(newKey.value) + val new = newKey.value + if (migration) { + appPreferences.storeDBPassphrase.set(useKeychain.value) } resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value) + if (useKeychain.value) { + DatabaseUtils.ksDatabasePassword.set(new) + } else { + removePassphraseFromKeyChain(useKeychain, storedKey, migration) + } operationEnded(m, progressIndicator) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted)) } @@ -467,7 +534,6 @@ fun PreviewDatabaseEncryptionLayout() { SimpleXTheme { DatabaseEncryptionLayout( useKeychain = remember { mutableStateOf(true) }, - prefs = AppPreferences(), chatDbEncrypted = true, currentKey = remember { mutableStateOf("") }, newKey = remember { mutableStateOf("") }, @@ -475,6 +541,7 @@ fun PreviewDatabaseEncryptionLayout() { storedKey = remember { mutableStateOf(true) }, initialRandomDBPassphrase = remember { mutableStateOf(true) }, progressIndicator = remember { mutableStateOf(false) }, + migration = false, onConfirmEncrypt = {}, ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt index 22d69de1cc..9264ca69af 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt @@ -5,9 +5,7 @@ import SectionSpacer import SectionView import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -22,8 +20,11 @@ import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.AppVersionText +import chat.simplex.common.views.usersettings.SettingsActionItem import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import kotlinx.datetime.Clock import java.io.File @@ -44,7 +45,7 @@ fun DatabaseErrorView( fun callRunChat(confirmMigrations: MigrationConfirmation? = null) { val useKey = if (useKeychain) null else dbKey.value - runChat(useKey, confirmMigrations, chatDbStatus, progressIndicator, appPreferences) + runChat(useKey, confirmMigrations, chatDbStatus, progressIndicator) } fun saveAndRunChatOnClick() { @@ -63,7 +64,7 @@ fun DatabaseErrorView( Modifier.padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING, bottom = DEFAULT_PADDING), style = MaterialTheme.typography.h1 ) - SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content) + SectionView(null, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content = content) } @Composable @@ -76,10 +77,7 @@ fun DatabaseErrorView( Text(String.format(generalGetString(MR.strings.database_migrations), ms.joinToString(", "))) } - Column( - Modifier.fillMaxSize().verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.Center, - ) { + ColumnWithScrollBarNoAppBar(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) { val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value when (val status = chatDbStatus.value) { is DBMigrationResult.ErrorNotADatabase -> @@ -106,7 +104,7 @@ fun DatabaseErrorView( } } is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) { - is MigrationError.Upgrade -> + is MigrationError.Upgrade -> { DatabaseErrorDetails(MR.strings.database_upgrade) { TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUp) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) { Text(generalGetString(MR.strings.upgrade_and_open_chat)) @@ -116,7 +114,9 @@ fun DatabaseErrorView( MigrationsText(err.upMigrations.map { it.upName }) AppVersionText() } - is MigrationError.Downgrade -> + OpenDatabaseDirectoryButton() + } + is MigrationError.Downgrade -> { DatabaseErrorDetails(MR.strings.database_downgrade) { TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUpDown) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) { Text(generalGetString(MR.strings.downgrade_and_open_chat)) @@ -127,29 +127,41 @@ fun DatabaseErrorView( MigrationsText(err.downMigrations) AppVersionText() } - is MigrationError.Error -> + OpenDatabaseDirectoryButton() + } + is MigrationError.Error -> { DatabaseErrorDetails(MR.strings.incompatible_database_version) { FileNameText(status.dbFile) Text(String.format(generalGetString(MR.strings.error_with_info), mtrErrorDescription(err.mtrError))) } + OpenDatabaseDirectoryButton() + } } - is DBMigrationResult.ErrorSQL -> + is DBMigrationResult.ErrorSQL -> { DatabaseErrorDetails(MR.strings.database_error) { FileNameText(status.dbFile) Text(String.format(generalGetString(MR.strings.error_with_info), status.migrationSQLError)) } - is DBMigrationResult.ErrorKeychain -> + OpenDatabaseDirectoryButton() + } + is DBMigrationResult.ErrorKeychain -> { DatabaseErrorDetails(MR.strings.keychain_error) { Text(generalGetString(MR.strings.cannot_access_keychain)) } - is DBMigrationResult.InvalidConfirmation -> + OpenDatabaseDirectoryButton() + } + is DBMigrationResult.InvalidConfirmation -> { DatabaseErrorDetails(MR.strings.invalid_migration_confirmation) { // this can only happen if incorrect parameter is passed } - is DBMigrationResult.Unknown -> + OpenDatabaseDirectoryButton() + } + is DBMigrationResult.Unknown -> { DatabaseErrorDetails(MR.strings.database_error) { Text(String.format(generalGetString(MR.strings.unknown_database_error_with_info), status.json)) } + OpenDatabaseDirectoryButton() + } is DBMigrationResult.OK -> {} null -> {} } @@ -190,13 +202,14 @@ private fun runChat( confirmMigrations: MigrationConfirmation? = null, chatDbStatus: State<DBMigrationResult?>, progressIndicator: MutableState<Boolean>, - prefs: AppPreferences ) = CoroutineScope(Dispatchers.Default).launch { // Don't do things concurrently. Shouldn't be here concurrently, just in case if (progressIndicator.value) return@launch progressIndicator.value = true try { - initChatController(dbKey, confirmMigrations) + initChatController(dbKey, confirmMigrations, + startChat = if (appPreferences.chatStopped.get()) ::showStartChatAfterRestartAlert else { { CompletableDeferred(true) } } + ) } catch (e: Exception) { Log.d(TAG, "initializeChat ${e.stackTraceToString()}") } @@ -205,6 +218,14 @@ private fun runChat( is DBMigrationResult.OK -> { platform.androidChatStartedAfterBeingOff() } + null -> {} + else -> showErrorOnMigrationIfNeeded(status) + } +} + +fun showErrorOnMigrationIfNeeded(status: DBMigrationResult) = + when (status) { + is DBMigrationResult.OK -> {} is DBMigrationResult.ErrorNotADatabase -> AlertManager.shared.showAlertMsg(generalGetString(MR.strings.wrong_passphrase_title), generalGetString(MR.strings.enter_correct_passphrase)) is DBMigrationResult.ErrorSQL -> @@ -216,9 +237,7 @@ private fun runChat( is DBMigrationResult.InvalidConfirmation -> AlertManager.shared.showAlertMsg(generalGetString(MR.strings.invalid_migration_confirmation)) is DBMigrationResult.ErrorMigration -> {} - null -> {} } -} private fun shouldShowRestoreDbButton(prefs: AppPreferences): Boolean { val startedAt = prefs.encryptionStartedAt.get() ?: return false @@ -245,7 +264,7 @@ private fun restoreDb(restoreDbFromBackup: MutableState<Boolean>, prefs: AppPref } } -private fun mtrErrorDescription(err: MTRError): String = +fun mtrErrorDescription(err: MTRError): String = when (err) { is MTRError.NoDown -> String.format(generalGetString(MR.strings.mtr_error_no_down_migration), err.dbMigrations.joinToString(", ")) @@ -287,6 +306,18 @@ private fun ColumnScope.SaveAndOpenButton(enabled: Boolean, onClick: () -> Unit) } } +@Composable +private fun OpenDatabaseDirectoryButton() { + if (appPlatform.isDesktop) { + Spacer(Modifier.padding(top = DEFAULT_PADDING)) + SettingsActionItem( + painterResource(MR.images.ic_folder_open), + stringResource(MR.strings.open_database_folder), + ::desktopOpenDatabaseDir + ) + } +} + @Composable private fun ColumnScope.OpenChatButton(enabled: Boolean, onClick: () -> Unit) { TextButton(onClick, Modifier.align(Alignment.CenterHorizontally), enabled = enabled) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index 224317f949..4a911fa6f0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -4,65 +4,65 @@ import SectionBottomSpacer import SectionDividerSpaced import SectionTextFooter import SectionItemView -import SectionSpacer import SectionView import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller -import chat.simplex.common.model.ChatModel.updatingChatsMutex import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* import chat.simplex.common.platform.* import chat.simplex.res.MR -import kotlinx.coroutines.sync.withLock import kotlinx.datetime.* import java.io.* import java.net.URI import java.nio.file.Files +import java.nio.file.StandardCopyOption import java.text.SimpleDateFormat import java.util.* import kotlin.collections.ArrayList +import kotlinx.coroutines.* @Composable -fun DatabaseView( - m: ChatModel, - showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit) -) { - val currentRemoteHost by remember { chatModel.currentRemoteHost } +fun DatabaseView() { + val m = chatModel val progressIndicator = remember { mutableStateOf(false) } val prefs = m.controller.appPrefs val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } - val chatArchiveName = remember { mutableStateOf(prefs.chatArchiveName.get()) } - val chatArchiveTime = remember { mutableStateOf(prefs.chatArchiveTime.get()) } val chatLastStart = remember { mutableStateOf(prefs.chatLastStart.get()) } val chatArchiveFile = remember { mutableStateOf<String?>(null) } + val stopped = remember { m.chatRunning }.value == false val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? -> - val file = chatArchiveFile.value - if (file != null && to != null) { - copyFileToFile(File(file), to) { - chatArchiveFile.value = null - } + val archive = chatArchiveFile.value + if (archive != null && to != null) { + copyFileToFile(File(archive), to) {} + } + // delete no matter the database was exported or canceled the export process + if (archive != null) { + File(archive).delete() + chatArchiveFile.value = null } } val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) } val importArchiveLauncher = rememberFileChooserLauncher(true) { to: URI? -> if (to != null) { - importArchiveAlert(m, to, appFilesCountAndSize, progressIndicator) { - startChat(m, chatLastStart, m.chatDbChanged) + importArchiveAlert { + stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { + importArchive(to, appFilesCountAndSize, progressIndicator, false) + } } } } @@ -73,28 +73,44 @@ fun DatabaseView( val user = m.currentUser.value val rhId = user?.remoteHostId DatabaseLayout( - currentRemoteHost = currentRemoteHost, progressIndicator.value, - remember { m.chatRunning }.value != false, - m.chatDbChanged.value, + stopped, useKeychain.value, m.chatDbEncrypted.value, m.controller.appPrefs.storeDBPassphrase.state.value, m.controller.appPrefs.initialRandomDBPassphrase, importArchiveLauncher, - chatArchiveName, - chatArchiveTime, - chatLastStart, appFilesCountAndSize, chatItemTTL, user, m.users, - startChat = { startChat(m, chatLastStart, m.chatDbChanged) }, - stopChatAlert = { stopChatAlert(m) }, - exportArchive = { exportArchive(m, progressIndicator, chatArchiveName, chatArchiveTime, chatArchiveFile, saveArchiveLauncher) }, - deleteChatAlert = { deleteChatAlert(m, progressIndicator) }, - deleteAppFilesAndMedia = { deleteFilesAndMediaAlert(appFilesCountAndSize) }, + startChat = { startChat(m, chatLastStart, m.chatDbChanged, progressIndicator) }, + stopChatAlert = { stopChatAlert(m, progressIndicator) }, + exportArchive = { + stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { + exportArchive(m, progressIndicator, chatArchiveFile, saveArchiveLauncher) + } + }, + deleteChatAlert = { + deleteChatAlert { + stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { + deleteChat(m, progressIndicator) + true + } + } + }, + deleteAppFilesAndMedia = { + deleteFilesAndMediaAlert { + stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { + deleteFiles(appFilesCountAndSize) + true + } + } + }, onChatItemTTLSelected = { + if (it == null) { + return@DatabaseLayout + } val oldValue = chatItemTTL.value chatItemTTL.value = it if (it < oldValue) { @@ -103,7 +119,6 @@ fun DatabaseView( setCiTTL(m, rhId, chatItemTTL, progressIndicator, appFilesCountAndSize) } }, - showSettingsModal, disconnectAllHosts = { val connected = chatModel.remoteHosts.filter { it.sessionState is RemoteHostSessionState.Connected } connected.forEachIndexed { index, h -> @@ -130,18 +145,13 @@ fun DatabaseView( @Composable fun DatabaseLayout( - currentRemoteHost: RemoteHostInfo?, progressIndicator: Boolean, - runChat: Boolean, - chatDbChanged: Boolean, + stopped: Boolean, useKeyChain: Boolean, chatDbEncrypted: Boolean?, passphraseSaved: Boolean, initialRandomDBPassphrase: SharedPreference<Boolean>, importArchiveLauncher: FileChooserLauncher, - chatArchiveName: MutableState<String?>, - chatArchiveTime: MutableState<Instant?>, - chatLastStart: MutableState<Instant?>, appFilesCountAndSize: MutableState<Pair<Int, Long>>, chatItemTTL: MutableState<ChatItemTTL>, currentUser: User?, @@ -151,16 +161,12 @@ fun DatabaseLayout( exportArchive: () -> Unit, deleteChatAlert: () -> Unit, deleteAppFilesAndMedia: () -> Unit, - onChatItemTTLSelected: (ChatItemTTL) -> Unit, - showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + onChatItemTTLSelected: (ChatItemTTL?) -> Unit, disconnectAllHosts: () -> Unit, ) { - val stopped = !runChat - val operationsDisabled = (!stopped || progressIndicator) && !chatModel.desktopNoUserNoRemote + val operationsDisabled = progressIndicator && !chatModel.desktopNoUserNoRemote - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_chat_database)) if (!chatModel.desktopNoUserNoRemote) { @@ -182,22 +188,19 @@ fun DatabaseLayout( } val toggleEnabled = remember { chatModel.remoteHosts }.none { it.sessionState is RemoteHostSessionState.Connected } if (chatModel.localUserCreated.value == true) { + // still show the toggle in case database was stopped when the user opened this screen because it can be in the following situations: + // - database was stopped after migration and the app relaunched + // - something wrong happened with database operations and the database couldn't be launched when it should SectionView(stringResource(MR.strings.run_chat_section)) { if (!toggleEnabled) { SectionItemView(disconnectAllHosts) { Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange) } } - RunChatSetting(runChat, stopped, toggleEnabled, startChat, stopChatAlert) + RunChatSetting(stopped, toggleEnabled && !progressIndicator, startChat, stopChatAlert) } - SectionTextFooter( - if (stopped) { - stringResource(MR.strings.you_must_use_the_most_recent_version_of_database) - } else { - stringResource(MR.strings.stop_chat_to_enable_database_actions) - } - ) - SectionDividerSpaced() + if (stopped) SectionTextFooter(stringResource(MR.strings.you_must_use_the_most_recent_version_of_database)) + SectionDividerSpaced(maxTopPadding = true) } SectionView(stringResource(MR.strings.chat_database_section)) { @@ -211,7 +214,7 @@ fun DatabaseLayout( if (unencrypted) painterResource(MR.images.ic_lock_open_right) else if (useKeyChain) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_lock), stringResource(MR.strings.database_passphrase), - click = showSettingsModal() { DatabaseEncryptionView(it) }, + click = { ModalManager.start.showModal { DatabaseEncryptionView(chatModel, false) } }, iconColor = if (unencrypted || (appPlatform.isDesktop && passphraseSaved)) WarningOrange else MaterialTheme.colors.secondary, disabled = operationsDisabled ) @@ -229,6 +232,9 @@ fun DatabaseLayout( click = { if (initialRandomDBPassphrase.get()) { exportProhibitedAlert() + ModalManager.start.showModal { + DatabaseEncryptionView(chatModel, false) + } } else { exportArchive() } @@ -240,23 +246,11 @@ fun DatabaseLayout( SettingsActionItem( painterResource(MR.images.ic_download), stringResource(MR.strings.import_database), - { withApi { importArchiveLauncher.launch("application/zip") } }, + { withLongRunningApi { importArchiveLauncher.launch("application/zip") } }, textColor = Color.Red, iconColor = Color.Red, disabled = operationsDisabled ) - val chatArchiveNameVal = chatArchiveName.value - val chatArchiveTimeVal = chatArchiveTime.value - val chatLastStartVal = chatLastStart.value - if (chatArchiveNameVal != null && chatArchiveTimeVal != null && chatLastStartVal != null) { - val title = chatArchiveTitle(chatArchiveTimeVal, chatLastStartVal) - SettingsActionItem( - painterResource(MR.images.ic_inventory_2), - title, - click = showSettingsModal { ChatArchiveView(it, title, chatArchiveNameVal, chatArchiveTimeVal) }, - disabled = operationsDisabled - ) - } SettingsActionItem( painterResource(MR.images.ic_delete_forever), stringResource(MR.strings.delete_database), @@ -266,7 +260,7 @@ fun DatabaseLayout( disabled = operationsDisabled ) } - SectionDividerSpaced(maxTopPadding = true) + SectionDividerSpaced() SectionView(stringResource(MR.strings.files_and_media_section).uppercase()) { val deleteFilesDisabled = operationsDisabled || appFilesCountAndSize.value.first == 0 @@ -303,26 +297,31 @@ private fun setChatItemTTLAlert( confirmText = generalGetString(MR.strings.delete_messages), onConfirm = { setCiTTL(m, rhId, selectedChatItemTTL, progressIndicator, appFilesCountAndSize) }, onDismiss = { selectedChatItemTTL.value = m.chatItemTTL.value }, + onDismissRequest = { selectedChatItemTTL.value = m.chatItemTTL.value }, destructive = true, ) } @Composable -private fun TtlOptions(current: State<ChatItemTTL>, enabled: State<Boolean>, onSelected: (ChatItemTTL) -> Unit) { +fun TtlOptions( + current: State<ChatItemTTL?>, + enabled: State<Boolean>, + onSelected: (ChatItemTTL?) -> Unit, + default: State<ChatItemTTL>? = null +) { val values = remember { - val all: ArrayList<ChatItemTTL> = arrayListOf(ChatItemTTL.None, ChatItemTTL.Month, ChatItemTTL.Week, ChatItemTTL.Day) - if (current.value is ChatItemTTL.Seconds) { - all.add(current.value) + val all: ArrayList<ChatItemTTL> = arrayListOf(ChatItemTTL.None, ChatItemTTL.Year, ChatItemTTL.Month, ChatItemTTL.Week, ChatItemTTL.Day) + val currentValue = current.value + if (currentValue is ChatItemTTL.Seconds) { + all.add(currentValue) } - all.map { - when (it) { - is ChatItemTTL.None -> it to generalGetString(MR.strings.chat_item_ttl_none) - is ChatItemTTL.Day -> it to generalGetString(MR.strings.chat_item_ttl_day) - is ChatItemTTL.Week -> it to generalGetString(MR.strings.chat_item_ttl_week) - is ChatItemTTL.Month -> it to generalGetString(MR.strings.chat_item_ttl_month) - is ChatItemTTL.Seconds -> it to String.format(generalGetString(MR.strings.chat_item_ttl_seconds), it.secs) - } + val options: MutableList<Pair<ChatItemTTL?, String>> = all.map { it to it.text }.toMutableList() + + if (default != null) { + options.add(null to String.format(generalGetString(MR.strings.chat_item_ttl_default), default.value.text)) } + + options } ExposedDropDownSettingRow( generalGetString(MR.strings.delete_messages_after), @@ -336,7 +335,6 @@ private fun TtlOptions(current: State<ChatItemTTL>, enabled: State<Boolean>, onS @Composable fun RunChatSetting( - runChat: Boolean, stopped: Boolean, enabled: Boolean, startChat: () -> Unit, @@ -349,7 +347,7 @@ fun RunChatSetting( iconColor = if (stopped) Color.Red else MaterialTheme.colors.primary, ) { DefaultSwitch( - checked = runChat, + checked = !stopped, onCheckedChange = { runChatSwitch -> if (runChatSwitch) { startChat() @@ -362,14 +360,15 @@ fun RunChatSetting( } } -@Composable -fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String { - return stringResource(if (chatArchiveTime < chatLastStart) MR.strings.old_database_archive else MR.strings.new_database_archive) -} - -private fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) { - withApi { +fun startChat( + m: ChatModel, + chatLastStart: MutableState<Instant?>, + chatDbChanged: MutableState<Boolean>, + progressIndicator: MutableState<Boolean>? = null +) { + withLongRunningApi { try { + progressIndicator?.value = true if (chatDbChanged.value) { initChatController() chatDbChanged.value = false @@ -377,36 +376,40 @@ private fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatD if (m.chatDbStatus.value !is DBMigrationResult.OK) { /** Hide current view and show [DatabaseErrorView] */ ModalManager.closeAllModalsEverywhere() - return@withApi + return@withLongRunningApi } - if (m.currentUser.value == null) { + val user = m.currentUser.value + if (user == null) { ModalManager.closeAllModalsEverywhere() - return@withApi + return@withLongRunningApi } else { - m.controller.apiStartChat() - m.chatRunning.value = true + m.controller.startChat(user) } val ts = Clock.System.now() m.controller.appPrefs.chatLastStart.set(ts) chatLastStart.value = ts platform.androidChatStartedAfterBeingOff() - } catch (e: Error) { + } catch (e: Throwable) { m.chatRunning.value = false AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_starting_chat), e.toString()) + } finally { + progressIndicator?.value = false } } } -private fun stopChatAlert(m: ChatModel) { +private fun stopChatAlert(m: ChatModel, progressIndicator: MutableState<Boolean>? = null) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.stop_chat_question), text = generalGetString(MR.strings.stop_chat_to_export_import_or_delete_chat_database), confirmText = generalGetString(MR.strings.stop_chat_confirmation), - onConfirm = { authStopChat(m) }, + onConfirm = { authStopChat(m, progressIndicator = progressIndicator) }, onDismiss = { m.chatRunning.value = true } ) } +expect fun restartChatOrApp() + private fun exportProhibitedAlert() { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.set_password_to_export), @@ -414,18 +417,20 @@ private fun exportProhibitedAlert() { ) } -private fun authStopChat(m: ChatModel) { +fun authStopChat(m: ChatModel, progressIndicator: MutableState<Boolean>? = null, onStop: (() -> Unit)? = null) { if (m.controller.appPrefs.performLA.get()) { authenticate( generalGetString(MR.strings.auth_stop_chat), generalGetString(MR.strings.auth_log_in_using_credential), + oneTime = true, completed = { laResult -> when (laResult) { LAResult.Success, is LAResult.Unavailable -> { - stopChat(m) + stopChat(m, progressIndicator, onStop) } is LAResult.Error -> { m.chatRunning.value = true + laFailedAlert() } is LAResult.Failed -> { m.chatRunning.value = true @@ -434,18 +439,27 @@ private fun authStopChat(m: ChatModel) { } ) } else { - stopChat(m) + stopChat(m, progressIndicator, onStop) } } -private fun stopChat(m: ChatModel) { - withApi { +private fun stopChat(m: ChatModel, progressIndicator: MutableState<Boolean>? = null, onStop: (() -> Unit)? = null) { + withBGApi { try { + progressIndicator?.value = true stopChatAsync(m) platform.androidChatStopped() + // close chat view for desktop + chatModel.chatId.value = null + if (appPlatform.isDesktop) { + ModalManager.end.closeModals() + } + onStop?.invoke() } catch (e: Error) { m.chatRunning.value = true AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_stopping_chat), e.toString()) + } finally { + progressIndicator?.value = false } } } @@ -453,132 +467,241 @@ private fun stopChat(m: ChatModel) { suspend fun stopChatAsync(m: ChatModel) { m.controller.apiStopChat() m.chatRunning.value = false + controller.appPrefs.chatStopped.set(true) +} + +fun stopChatRunBlockStartChat( + stopped: Boolean, + chatLastStart: MutableState<Instant?>, + progressIndicator: MutableState<Boolean>, + block: suspend () -> Boolean +) { + // if the chat was running, the sequence is: stop chat, run block, start chat. + // Otherwise, just run block and do nothing - the toggle will be visible anyway and the user can start the chat or not + if (stopped) { + withLongRunningApi { + try { + block() + } catch (e: Throwable) { + Log.e(TAG, e.stackTraceToString()) + } + } + } else { + authStopChat(chatModel, progressIndicator) { + withLongRunningApi { + // if it throws, let's start chat again anyway + val canStart = try { + block() + } catch (e: Throwable) { + Log.e(TAG, e.stackTraceToString()) + true + } + if (canStart) { + startChat(chatModel, chatLastStart, chatModel.chatDbChanged, progressIndicator) + } + } + } + } } suspend fun deleteChatAsync(m: ChatModel) { m.controller.apiDeleteStorage() DatabaseUtils.ksDatabasePassword.remove() m.controller.appPrefs.storeDBPassphrase.set(true) + deleteChatDatabaseFilesAndState() } -private fun exportArchive( - m: ChatModel, - progressIndicator: MutableState<Boolean>, - chatArchiveName: MutableState<String?>, - chatArchiveTime: MutableState<Instant?>, - chatArchiveFile: MutableState<String?>, - saveArchiveLauncher: FileChooserLauncher -) { - progressIndicator.value = true - withApi { - try { - val archiveFile = exportChatArchive(m, chatArchiveName, chatArchiveTime, chatArchiveFile) - chatArchiveFile.value = archiveFile - saveArchiveLauncher.launch(archiveFile.substringAfterLast(File.separator)) - progressIndicator.value = false - } catch (e: Error) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_exporting_chat_database), e.toString()) - progressIndicator.value = false +fun deleteChatDatabaseFilesAndState() { + val chat = File(dataDir, chatDatabaseFileName) + val chatBak = File(dataDir, "$chatDatabaseFileName.bak") + val agent = File(dataDir, agentDatabaseFileName) + val agentBak = File(dataDir, "$agentDatabaseFileName.bak") + chat.delete() + chatBak.delete() + agent.delete() + agentBak.delete() + filesDir.deleteRecursively() + filesDir.mkdir() + remoteHostsDir.deleteRecursively() + tmpDir.deleteRecursively() + getMigrationTempFilesDirectory().deleteRecursively() + tmpDir.mkdir() + wallpapersDir.deleteRecursively() + wallpapersDir.mkdirs() + DatabaseUtils.ksDatabasePassword.remove() + appPrefs.newDatabaseInitialized.set(false) + chatModel.desktopOnboardingRandomPassword.value = false + controller.appPrefs.storeDBPassphrase.set(true) + controller.ctrl = null + + // Clear sensitive data on screen just in case ModalManager will fail to prevent hiding its modals while database encrypts itself + chatModel.chatId.value = null + withLongRunningApi { + withContext(Dispatchers.Main) { + chatModel.chatsContext.chatItems.clearAndNotify() + chatModel.chatsContext.chats.clear() + chatModel.chatsContext.popChatCollector.clear() + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.chatItems?.clearAndNotify() + chatModel.secondaryChatsContext.value?.chats?.clear() + chatModel.secondaryChatsContext.value?.popChatCollector?.clear() } } + chatModel.users.clear() + ntfManager.cancelAllNotifications() } -private suspend fun exportChatArchive( +private suspend fun exportArchive( m: ChatModel, - chatArchiveName: MutableState<String?>, - chatArchiveTime: MutableState<Instant?>, + progressIndicator: MutableState<Boolean>, + chatArchiveFile: MutableState<String?>, + saveArchiveLauncher: FileChooserLauncher +): Boolean { + progressIndicator.value = true + try { + val (archiveFile, archiveErrors) = exportChatArchive(m, null, chatArchiveFile) + chatArchiveFile.value = archiveFile + if (archiveErrors.isEmpty()) { + saveArchiveLauncher.launch(archiveFile.substringAfterLast(File.separator)) + } else { + showArchiveExportedWithErrorsAlert(generalGetString(MR.strings.chat_database_exported_save), archiveErrors) { + withLongRunningApi { + saveArchiveLauncher.launch(archiveFile.substringAfterLast(File.separator)) + } + } + } + progressIndicator.value = false + } catch (e: Throwable) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_exporting_chat_database), e.toString()) + progressIndicator.value = false + } + return false +} + +suspend fun exportChatArchive( + m: ChatModel, + storagePath: File?, chatArchiveFile: MutableState<String?> -): String { +): Pair<String, List<ArchiveError>> { val archiveTime = Clock.System.now() val ts = SimpleDateFormat("yyyy-MM-dd'T'HHmmss", Locale.US).format(Date.from(archiveTime.toJavaInstant())) val archiveName = "simplex-chat.$ts.zip" - val archivePath = "${filesDir.absolutePath}${File.separator}$archiveName" + val archivePath = "${(storagePath ?: databaseExportDir).absolutePath}${File.separator}$archiveName" val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString()) - m.controller.apiExportArchive(config) - deleteOldArchive(m) - m.controller.appPrefs.chatArchiveName.set(archiveName) - chatArchiveName.value = archiveName - m.controller.appPrefs.chatArchiveTime.set(archiveTime) - chatArchiveTime.value = archiveTime + // Settings should be saved before changing a passphrase, otherwise the database needs to be migrated first + if (!m.chatDbChanged.value) { + controller.apiSaveAppSettings(AppSettings.current.prepareForExport()) + } + wallpapersDir.mkdirs() + val archiveErrors = m.controller.apiExportArchive(config) + if (storagePath == null) { + deleteOldChatArchive() + m.controller.appPrefs.chatArchiveName.set(archiveName) + m.controller.appPrefs.chatArchiveTime.set(archiveTime) + } chatArchiveFile.value = archivePath - return archivePath + return archivePath to archiveErrors } -private fun deleteOldArchive(m: ChatModel) { - val chatArchiveName = m.controller.appPrefs.chatArchiveName.get() +// Deprecated. Remove in the end of 2025. All unused archives should be deleted for the most users til then. +/** Remove [AppPreferences.chatArchiveName] and [AppPreferences.chatArchiveTime] as well */ +fun deleteOldChatArchive() { + val chatArchiveName = chatModel.controller.appPrefs.chatArchiveName.get() if (chatArchiveName != null) { - val file = File("${filesDir.absolutePath}${File.separator}$chatArchiveName") - val fileDeleted = file.delete() - if (fileDeleted) { - m.controller.appPrefs.chatArchiveName.set(null) - m.controller.appPrefs.chatArchiveTime.set(null) + val file1 = File("${filesDir.absolutePath}${File.separator}$chatArchiveName") + val file2 = File("${databaseExportDir.absolutePath}${File.separator}$chatArchiveName") + val fileDeleted = file1.delete() || file2.delete() + if (fileDeleted || (!file1.exists() && !file2.exists())) { + chatModel.controller.appPrefs.chatArchiveName.set(null) + chatModel.controller.appPrefs.chatArchiveTime.set(null) } else { Log.e(TAG, "deleteOldArchive file.delete() error") } } } -private fun importArchiveAlert( - m: ChatModel, - importedArchiveURI: URI, - appFilesCountAndSize: MutableState<Pair<Int, Long>>, - progressIndicator: MutableState<Boolean>, - startChat: () -> Unit, -) { +private fun importArchiveAlert(onConfirm: () -> Unit, ) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.import_database_question), text = generalGetString(MR.strings.your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one), confirmText = generalGetString(MR.strings.import_database_confirmation), - onConfirm = { importArchive(m, importedArchiveURI, appFilesCountAndSize, progressIndicator, startChat) }, + onConfirm = onConfirm, destructive = true, ) } -private fun importArchive( - m: ChatModel, +fun showArchiveImportedWithErrorsAlert(archiveErrors: List<ArchiveError>) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.chat_database_imported), + text = generalGetString(MR.strings.restart_the_app_to_use_imported_chat_database) + "\n\n" + generalGetString(MR.strings.non_fatal_errors_occured_during_import) + archiveErrorsText(archiveErrors)) +} + +fun showArchiveExportedWithErrorsAlert(description: String, archiveErrors: List<ArchiveError>, onConfirm: () -> Unit) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.chat_database_exported_title), + text = description + "\n\n" + generalGetString(MR.strings.chat_database_exported_not_all_files) + archiveErrorsText(archiveErrors), + confirmText = generalGetString(MR.strings.chat_database_exported_continue), + onConfirm = onConfirm + ) +} + +private fun archiveErrorsText(errs: List<ArchiveError>): String = "\n" + errs.map { + when (it) { + is ArchiveError.ArchiveErrorImport -> it.importError + is ArchiveError.ArchiveErrorFile -> "${it.file}: ${it.fileError}" + } +}.joinToString(separator = "\n") + +suspend fun importArchive( importedArchiveURI: URI, appFilesCountAndSize: MutableState<Pair<Int, Long>>, progressIndicator: MutableState<Boolean>, - startChat: () -> Unit, -) { + migration: Boolean +): Boolean { + val m = chatModel progressIndicator.value = true val archivePath = saveArchiveFromURI(importedArchiveURI) if (archivePath != null) { - withApi { + try { + m.controller.apiDeleteStorage() + wallpapersDir.mkdirs() try { - m.controller.apiDeleteStorage() - try { - val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString()) - val archiveErrors = m.controller.apiImportArchive(config) - DatabaseUtils.ksDatabasePassword.remove() - appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath) - if (archiveErrors.isEmpty()) { - operationEnded(m, progressIndicator) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_imported), text = generalGetString(MR.strings.restart_the_app_to_use_imported_chat_database)) - } - if (chatModel.localUserCreated.value == false) { - chatModel.chatRunning.value = false - startChat() - } - } else { - operationEnded(m, progressIndicator) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_imported), text = generalGetString(MR.strings.restart_the_app_to_use_imported_chat_database) + "\n" + generalGetString(MR.strings.non_fatal_errors_occured_during_import)) - } - } - } catch (e: Error) { + val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString()) + val archiveErrors = m.controller.apiImportArchive(config) + appPrefs.shouldImportAppSettings.set(true) + DatabaseUtils.ksDatabasePassword.remove() + appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath) + if (archiveErrors.isEmpty()) { operationEnded(m, progressIndicator) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_importing_database), e.toString()) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_imported), text = generalGetString(MR.strings.restart_the_app_to_use_imported_chat_database)) } + if (chatModel.localUserCreated.value == false) { + chatModel.chatRunning.value = false + } + return true + } else { + operationEnded(m, progressIndicator) { + showArchiveImportedWithErrorsAlert(archiveErrors) + } + return migration } } catch (e: Error) { operationEnded(m, progressIndicator) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_deleting_database), e.toString()) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_importing_database), e.toString()) } - } finally { - File(archivePath).delete() } + } catch (e: Error) { + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_deleting_database), e.toString()) + } + } finally { + File(archivePath).delete() } + } else { + progressIndicator.value = false } + return false } private fun saveArchiveFromURI(importedArchiveURI: URI): String? { @@ -588,40 +711,42 @@ private fun saveArchiveFromURI(importedArchiveURI: URI): String? { if (inputStream != null && archiveName != null) { val archivePath = "$databaseExportDir${File.separator}$archiveName" val destFile = File(archivePath) - Files.copy(inputStream, destFile.toPath()) + Files.copy(inputStream, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING) archivePath } else { Log.e(TAG, "saveArchiveFromURI null inputStream") null } } catch (e: Exception) { - Log.e(TAG, "saveArchiveFromURI error: ${e.message}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_database), e.stackTraceToString()) + Log.e(TAG, "saveArchiveFromURI error: ${e.stackTraceToString()}") null } } -private fun deleteChatAlert(m: ChatModel, progressIndicator: MutableState<Boolean>) { +private fun deleteChatAlert(onConfirm: () -> Unit) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.delete_chat_profile_question), text = generalGetString(MR.strings.delete_chat_profile_action_cannot_be_undone_warning), confirmText = generalGetString(MR.strings.delete_verb), - onConfirm = { deleteChat(m, progressIndicator) }, + onConfirm = onConfirm, destructive = true, ) } -private fun deleteChat(m: ChatModel, progressIndicator: MutableState<Boolean>) { +private suspend fun deleteChat(m: ChatModel, progressIndicator: MutableState<Boolean>) { + if (!DatabaseUtils.hasAtLeastOneDatabase(dataDir.absolutePath)) { + return + } progressIndicator.value = true - withApi { - try { - deleteChatAsync(m) - operationEnded(m, progressIndicator) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_deleted), generalGetString(MR.strings.restart_the_app_to_create_a_new_chat_profile)) - } - } catch (e: Error) { - operationEnded(m, progressIndicator) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_deleting_database), e.toString()) - } + try { + deleteChatAsync(m) + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_deleted), generalGetString(MR.strings.restart_the_app_to_create_a_new_chat_profile)) + } + } catch (e: Throwable) { + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_deleting_database), e.toString()) } } } @@ -635,7 +760,7 @@ private fun setCiTTL( ) { Log.d(TAG, "DatabaseView setChatItemTTL ${chatItemTTL.value.seconds ?: -1}") progressIndicator.value = true - withApi { + withBGApi { try { m.controller.setChatItemTTL(rhId, chatItemTTL.value) // Update model on success @@ -659,10 +784,10 @@ private fun afterSetCiTTL( appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath) withApi { try { - updatingChatsMutex.withLock { + withContext(Dispatchers.Main) { // this is using current remote host on purpose - if it changes during update, it will load correct chats val chats = m.controller.apiGetChats(m.remoteHostId()) - m.updateChats(chats) + chatModel.chatsContext.updateChats(chats) } } catch (e: Exception) { Log.e(TAG, "apiGetChats error: ${e.message}") @@ -670,12 +795,12 @@ private fun afterSetCiTTL( } } -private fun deleteFilesAndMediaAlert(appFilesCountAndSize: MutableState<Pair<Int, Long>>) { +private fun deleteFilesAndMediaAlert(onConfirm: () -> Unit) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.delete_files_and_media_question), text = generalGetString(MR.strings.delete_files_and_media_desc), confirmText = generalGetString(MR.strings.delete_verb), - onConfirm = { deleteFiles(appFilesCountAndSize) }, + onConfirm = onConfirm, destructive = true ) } @@ -700,18 +825,13 @@ private fun operationEnded(m: ChatModel, progressIndicator: MutableState<Boolean fun PreviewDatabaseLayout() { SimpleXTheme { DatabaseLayout( - currentRemoteHost = null, progressIndicator = false, - runChat = true, - chatDbChanged = false, + stopped = false, useKeyChain = false, chatDbEncrypted = false, passphraseSaved = false, initialRandomDBPassphrase = SharedPreference({ true }, {}), importArchiveLauncher = rememberFileChooserLauncher(true) {}, - chatArchiveName = remember { mutableStateOf("dummy_archive") }, - chatArchiveTime = remember { mutableStateOf(Clock.System.now()) }, - chatLastStart = remember { mutableStateOf(Clock.System.now()) }, appFilesCountAndSize = remember { mutableStateOf(0 to 0L) }, chatItemTTL = remember { mutableStateOf(ChatItemTTL.None) }, currentUser = User.sampleData, @@ -721,7 +841,6 @@ fun PreviewDatabaseLayout() { exportArchive = {}, deleteChatAlert = {}, deleteAppFilesAndMedia = {}, - showSettingsModal = { {} }, onChatItemTTLSelected = {}, disconnectAllHosts = {}, ) 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 177efbfddc..6bfcf2809f 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 @@ -12,6 +12,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.* import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign @@ -22,24 +23,29 @@ import chat.simplex.common.ui.theme.* import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow class AlertManager { - private var alertViews = mutableStateListOf<(@Composable () -> Unit)>() + // Don't use mutableStateOf() here, because it produces this if showing from SimpleXAPI.startChat(): + // java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied + private var alertViews = MutableStateFlow(listOf<(@Composable () -> Unit)>()) fun showAlert(alert: @Composable () -> Unit) { Log.d(TAG, "AlertManager.showAlert") - alertViews.add(alert) + alertViews.value += alert } fun hideAlert() { - alertViews.removeLastOrNull() + alertViews.value = ArrayList(alertViews.value).also { it.removeLastOrNull() } } fun hideAllAlerts() { - alertViews.clear() + alertViews.value = listOf() } - fun hasAlertsShown() = alertViews.isNotEmpty() + fun hasAlertsShown() = alertViews.value.isNotEmpty() fun showAlertDialogButtons( title: String, @@ -62,7 +68,31 @@ class AlertManager { fun showAlertDialogButtonsColumn( title: String, - text: AnnotatedString? = null, + text: String? = null, + textAlign: TextAlign = TextAlign.Center, + dismissible: Boolean = true, + onDismissRequest: (() -> Unit)? = null, + hostDevice: Pair<Long?, String>? = null, + belowTextContent: @Composable (() -> Unit) = {}, + buttons: @Composable () -> Unit, + ) { + showAlert { + AlertDialog( + onDismissRequest = { onDismissRequest?.invoke(); if (dismissible) hideAlert() }, + title = alertTitle(title), + buttons = { + AlertContent(text, hostDevice, extraPadding = true, textAlign = textAlign, belowTextContent = belowTextContent) { + buttons() + } + }, + shape = RoundedCornerShape(corner = CornerSize(25.dp)) + ) + } + } + + fun showAlertDialogButtonsColumn( + title: String, + text: AnnotatedString, onDismissRequest: (() -> Unit)? = null, hostDevice: Pair<Long?, String>? = null, buttons: @Composable () -> Unit, @@ -104,6 +134,8 @@ class AlertManager { ) { val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { + // Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard + delay(200) focusRequester.requestFocus() } TextButton(onClick = { @@ -161,7 +193,9 @@ class AlertManager { fun showAlertMsg( title: String, text: String? = null, confirmText: String = generalGetString(MR.strings.ok), + onConfirm: (() -> Unit)? = null, hostDevice: Pair<Long?, String>? = null, + shareText: Boolean? = null ) { showAlert { AlertDialog( @@ -171,14 +205,26 @@ class AlertManager { AlertContent(text, hostDevice, extraPadding = true) { val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { + // Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard + delay(200) focusRequester.requestFocus() } + // Can pass shareText = false to prevent showing Share button if it's needed in a specific case + val showShareButton = text != null && (shareText == true || (shareText == null && text.length > 500)) Row( Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), - horizontalArrangement = Arrangement.Center + horizontalArrangement = if (showShareButton) Arrangement.SpaceBetween else Arrangement.Center ) { + val clipboard = LocalClipboardManager.current + if (showShareButton && text != null) { + TextButton(onClick = { + clipboard.shareText(text) + hideAlert() + }) { Text(stringResource(MR.strings.share_verb)) } + } TextButton( onClick = { + onConfirm?.invoke() hideAlert() }, Modifier.focusRequester(focusRequester) @@ -216,12 +262,13 @@ class AlertManager { title: StringResource, text: StringResource? = null, confirmText: StringResource = MR.strings.ok, + onConfirm: (() -> Unit)? = null, hostDevice: Pair<Long?, String>? = null, - ) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText), hostDevice) + ) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText), onConfirm, hostDevice) @Composable fun showInView() { - remember { alertViews }.lastOrNull()?.invoke() + alertViews.collectAsState().value.lastOrNull()?.invoke() } companion object { @@ -242,7 +289,14 @@ private fun alertTitle(title: String): (@Composable () -> Unit)? { } @Composable -private fun AlertContent(text: String?, hostDevice: Pair<Long?, String>?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) { +private fun AlertContent( + text: String?, + hostDevice: Pair<Long?, String>?, + extraPadding: Boolean = false, + textAlign: TextAlign = TextAlign.Center, + belowTextContent: @Composable (() -> Unit) = {}, + content: @Composable (() -> Unit) +) { BoxWithConstraints { Column( Modifier @@ -256,17 +310,20 @@ private fun AlertContent(text: String?, hostDevice: Pair<Long?, String>?, extraP CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { if (text != null) { Column(Modifier.heightIn(max = this@BoxWithConstraints.maxHeight * 0.7f) + .padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING) .verticalScroll(rememberScrollState()) ) { SelectionContainer { Text( escapedHtmlToAnnotatedString(text, LocalDensity.current), - Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), + Modifier.fillMaxWidth(), fontSize = 16.sp, - textAlign = TextAlign.Center, + textAlign = textAlign, color = MaterialTheme.colors.secondary ) } + belowTextContent() + Spacer(Modifier.height(DEFAULT_PADDING * 1.5f)) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt index 6a400295ed..078cdde9da 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt @@ -7,3 +7,7 @@ fun <T> chatListAnimationSpec() = tween<T>(durationMillis = 250, easing = FastOu fun <T> newChatSheetAnimSpec() = tween<T>(256, 0, LinearEasing) fun <T> audioProgressBarAnimationSpec() = tween<T>(durationMillis = 30, easing = LinearEasing) + +fun <T> userPickerAnimSpec() = tween<T>(256, 0, FastOutSlowInEasing) + +fun <T> mentionPickerAnimSpec() = tween<T>(256, 0, FastOutSlowInEasing) 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 new file mode 100644 index 0000000000..afb557cc78 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt @@ -0,0 +1,82 @@ +package chat.simplex.common.views.helpers + +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.geometry.Offset +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.graphics.* +import androidx.compose.ui.unit.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chatlist.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import kotlin.math.absoluteValue + +@Composable +fun AppBarTitle( + title: String, + hostDevice: Pair<Long?, String>? = null, + withPadding: Boolean = true, + bottomPadding: Dp = DEFAULT_PADDING * 1.5f + 8.dp, + enableAlphaChanges: Boolean = true +) { + val handler = LocalAppBarHandler.current + val connection = if (enableAlphaChanges) handler?.connection else null + LaunchedEffect(title) { + if (enableAlphaChanges) { + handler?.title?.value = title + } else { + handler?.connection?.scrollTrackingEnabled = false + } + } + val theme = CurrentColors.collectAsState() + val titleColor = MaterialTheme.appColors.title + val brush = 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 + Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) + Column { + Text( + title, + Modifier + .padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, top = DEFAULT_PADDING_HALF, end = if (withPadding) DEFAULT_PADDING else 0.dp,) + .graphicsLayer { + alpha = bottomTitleAlpha(connection) + }, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.h1.copy(brush = brush), + color = MaterialTheme.colors.primaryVariant, + textAlign = TextAlign.Start + ) + if (hostDevice != null) { + Box(Modifier.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp).graphicsLayer { + alpha = bottomTitleAlpha(connection) + }) { + HostDeviceTitle(hostDevice) + } + } + Spacer(Modifier.height(bottomPadding)) + } +} + +private fun bottomTitleAlpha(connection: CollapsingAppBarNestedScrollConnection?) = + if (connection?.scrollTrackingEnabled == false) 1f + else if ((connection?.appBarOffset ?: 0f).absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 1f + else ((AppBarHandler.appBarMaxHeightPx) + (connection?.appBarOffset ?: 0f) / 1.5f).coerceAtLeast(0f) / AppBarHandler.appBarMaxHeightPx + +@Composable +private fun HostDeviceTitle(hostDevice: Pair<Long?, String>, extraPadding: Boolean = false) { + Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) { + DevicePill( + active = true, + onClick = {}, + actionButtonVisible = false, + icon = painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300), + text = hostDevice.second + ) + } +} 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 new file mode 100644 index 0000000000..096b6c55ac --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/BlurModifier.kt @@ -0,0 +1,139 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.clipRect +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.unit.* +import chat.simplex.common.platform.appPlatform +import chat.simplex.common.ui.theme.CurrentColors + +fun Modifier.blurredBackgroundModifier( + keyboardInset: WindowInsets, + handler: AppBarHandler?, + blurRadius: State<Int>, + prefAlpha: State<Float>, + keyboardCoversBar: Boolean, + onTop: Boolean, + density: Density +): Modifier { + val graphicsLayer = handler?.graphicsLayer + val backgroundGraphicsLayer = handler?.backgroundGraphicsLayer + val backgroundGraphicsLayerSize = handler?.backgroundGraphicsLayerSize + if (handler == null || graphicsLayer == null || backgroundGraphicsLayer == null || blurRadius.value == 0 || prefAlpha.value == 1f || backgroundGraphicsLayerSize === null) + return this + + return if (appPlatform.isAndroid) { + this.androidBlurredModifier(keyboardInset, blurRadius.value, keyboardCoversBar, onTop, graphicsLayer, backgroundGraphicsLayer, backgroundGraphicsLayerSize, density) + } else { + this.desktopBlurredModifier(keyboardInset, blurRadius, keyboardCoversBar, onTop, graphicsLayer, backgroundGraphicsLayer, backgroundGraphicsLayerSize, density) + } +} + +// this is more performant version than for Android but can't be used on desktop because on first frame it shows transparent view +// which is very noticeable on desktop and unnoticeable on Android +private fun Modifier.androidBlurredModifier( + keyboardInset: WindowInsets, + blurRadius: Int, + keyboardCoversBar: Boolean, + onTop: Boolean, + graphicsLayer: GraphicsLayer, + backgroundGraphicsLayer: GraphicsLayer, + backgroundGraphicsLayerSize: State<IntSize>, + density: Density +): Modifier = this + .graphicsLayer { + renderEffect = if (blurRadius > 0) BlurEffect(blurRadius.dp.toPx(), blurRadius.dp.toPx()) else null + clip = blurRadius > 0 + } + .graphicsLayer { + if (!onTop) { + val bgSize = when { + backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height + backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height + else -> backgroundGraphicsLayerSize.value.height + } + val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0 + translationY = -bgSize + size.height + keyboardHeightCovered + } + } + .drawBehind { + drawRect(Color.Black) + if (onTop) { + clipRect { + if (backgroundGraphicsLayer.size != IntSize.Zero) { + drawLayer(backgroundGraphicsLayer) + } else { + drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat())) + } + drawLayer(graphicsLayer) + } + } else { + if (backgroundGraphicsLayer.size != IntSize.Zero) { + drawLayer(backgroundGraphicsLayer) + } else { + drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat())) + } + drawLayer(graphicsLayer) + } + } + .graphicsLayer { + if (!onTop) { + val bgSize = when { + backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height + backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height + else -> backgroundGraphicsLayerSize.value.height + } + val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0 + translationY -= -bgSize + size.height + keyboardHeightCovered + } + } + +private fun Modifier.desktopBlurredModifier( + keyboardInset: WindowInsets, + blurRadius: State<Int>, + keyboardCoversBar: Boolean, + onTop: Boolean, + graphicsLayer: GraphicsLayer, + backgroundGraphicsLayer: GraphicsLayer, + backgroundGraphicsLayerSize: State<IntSize>, + density: Density +): Modifier = this + .graphicsLayer { + renderEffect = if (blurRadius.value > 0) BlurEffect(blurRadius.value.dp.toPx(), blurRadius.value.dp.toPx()) else null + clip = blurRadius.value > 0 + } + .drawBehind { + drawRect(Color.Black) + if (onTop) { + clipRect { + if (backgroundGraphicsLayer.size != IntSize.Zero) { + drawLayer(backgroundGraphicsLayer) + } else { + drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat())) + } + drawLayer(graphicsLayer) + } + } else { + val bgSize = when { + backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height + backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height + else -> backgroundGraphicsLayerSize.value.height + } + val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0 + translate(top = -bgSize + size.height + keyboardHeightCovered) { + if (backgroundGraphicsLayer.size != IntSize.Zero) { + drawLayer(backgroundGraphicsLayer) + } else { + drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat())) + } + drawLayer(graphicsLayer) + } + } + } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt index abc894942f..c3e97dd27b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt @@ -2,31 +2,45 @@ package chat.simplex.common.views.helpers import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.* import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.* +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.* import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.InspectableValue +import androidx.compose.ui.unit.* +import chat.simplex.common.model.BusinessChatType import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import chat.simplex.common.model.ChatInfo -import chat.simplex.common.platform.base64ToBitmap -import chat.simplex.common.ui.theme.SimpleXTheme +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource +import kotlin.math.max @Composable -fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVariant) { +fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVariant, shadow: Boolean = false) { val icon = - if (chatInfo is ChatInfo.Group) MR.images.ic_supervised_user_circle_filled - else MR.images.ic_account_circle_filled - ProfileImage(size, chatInfo.image, icon, iconColor) + when (chatInfo) { + is ChatInfo.Group -> + when (chatInfo.groupInfo.businessChat?.chatType) { + BusinessChatType.Business -> MR.images.ic_work_filled_padded + BusinessChatType.Customer -> MR.images.ic_account_circle_filled + null -> MR.images.ic_supervised_user_circle_filled + } + is ChatInfo.Local -> MR.images.ic_folder_filled + else -> MR.images.ic_account_circle_filled + } + ProfileImage(size, chatInfo.image, icon, if (chatInfo is ChatInfo.Local) NoteFolderIconColor else iconColor) } @Composable @@ -45,7 +59,9 @@ fun ProfileImage( size: Dp, image: String? = null, icon: ImageResource = MR.images.ic_account_circle_filled, - color: Color = MaterialTheme.colors.secondaryVariant + color: Color = MaterialTheme.colors.secondaryVariant, + backgroundColor: Color? = null, + blurred: Boolean = false ) { Box(Modifier.size(size)) { if (image == null) { @@ -55,6 +71,9 @@ fun ProfileImage( else -> null } if (iconToReplace != null) { + if (backgroundColor != null) { + Box(Modifier.size(size * 0.7f).align(Alignment.Center).background(backgroundColor, CircleShape)) + } Icon( iconToReplace, contentDescription = stringResource(MR.strings.icon_descr_profile_image_placeholder), @@ -75,12 +94,47 @@ fun ProfileImage( imageBitmap, stringResource(MR.strings.image_descr_profile_image), contentScale = ContentScale.Crop, - modifier = Modifier.size(size).padding(size / 12).clip(CircleShape) + modifier = ProfileIconModifier(size, blurred = blurred) ) } } } +@Composable +fun ProfileImage(size: Dp, image: ImageResource) { + Image( + painterResource(image), + stringResource(MR.strings.image_descr_profile_image), + contentScale = ContentScale.Crop, + modifier = ProfileIconModifier(size) + ) +} + +private const val squareToCircleRatio = 0.935f + +private const val radiusFactor = (1 - squareToCircleRatio) / 50 + +@Composable +fun ProfileIconModifier(size: Dp, padding: Boolean = true, blurred: Boolean = false): Modifier { + val percent = remember { appPreferences.profileImageCornerRadius.state } + val r = max(0f, percent.value) + val pad = if (padding) size / 12 else 0.dp + val m = Modifier.size(size) + val m1 = when { + r >= 50 -> + m.padding(pad).clip(CircleShape) + r <= 0 -> { + val sz = (size - 2 * pad) * squareToCircleRatio + m.padding((size - sz) / 2) + } + else -> { + val sz = (size - 2 * pad) * (squareToCircleRatio + r * radiusFactor) + m.padding((size - sz) / 2).clip(RoundedCornerShape(size = sz * r / 100)) + } + } + return if (blurred) m1.blur(size / 4) else m1 +} + /** [AccountCircleFilled] has its inner padding which leads to visible border if there is background underneath. * This is workaround * */ @@ -89,9 +143,10 @@ fun ProfileImageForActiveCall( size: Dp, image: String? = null, color: Color = MaterialTheme.colors.secondaryVariant, -) { + backgroundColor: Color? = null, + ) { if (image == null) { - Box(Modifier.requiredSize(size).clip(CircleShape)) { + Box(Modifier.requiredSize(size).clip(CircleShape).then(if (backgroundColor != null) Modifier.background(backgroundColor) else Modifier)) { Icon( AccountCircleFilled, contentDescription = stringResource(MR.strings.icon_descr_profile_image_placeholder), @@ -105,12 +160,11 @@ fun ProfileImageForActiveCall( imageBitmap, stringResource(MR.strings.image_descr_profile_image), contentScale = ContentScale.Crop, - modifier = Modifier.size(size).clip(CircleShape) + modifier = ProfileIconModifier(size, padding = false) ) } } - @Preview @Composable fun PreviewChatInfoImage() { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt new file mode 100644 index 0000000000..c1a76d7bf8 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt @@ -0,0 +1,459 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.runtime.* +import androidx.compose.ui.draw.CacheDrawScope +import androidx.compose.ui.draw.DrawResult +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.* +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex +import chat.simplex.res.MR +import dev.icerock.moko.resources.ImageResource +import dev.icerock.moko.resources.StringResource +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.io.File +import kotlin.math.* + +enum class PresetWallpaper( + val res: ImageResource, + val filename: String, + val scale: Float, + val background: Map<DefaultTheme, Color>, + val tint: Map<DefaultTheme, Color>, + val colors: Map<DefaultTheme, ThemeColors>, +) { + CATS(MR.images.wallpaper_cats, "cats", 0.63f, + wallpaperBackgrounds(light = "#ffF8F6EA"), + tint = mapOf( + DefaultTheme.LIGHT to "#ffefdca6".colorFromReadableHex(), + DefaultTheme.DARK to "#ff4b3b0e".colorFromReadableHex(), + DefaultTheme.SIMPLEX to "#ff51400f".colorFromReadableHex(), + DefaultTheme.BLACK to "#ff4b3b0e".colorFromReadableHex() + ), + mapOf( + DefaultTheme.LIGHT to ThemeColors( + sentMessage = "#fffffaed", + sentQuote = "#fffaf0d6", + receivedMessage = "#ffF8F7F4", + receivedQuote = "#ffefede9", + ), + DefaultTheme.DARK to ThemeColors( + sentMessage = "#ff2f2919", + sentQuote = "#ff473a1d", + receivedMessage = "#ff272624", + receivedQuote = "#ff373633", + ), + DefaultTheme.SIMPLEX to ThemeColors( + sentMessage = "#ff41371b", + sentQuote = "#ff654f1c", + receivedMessage = "#ff272624", + receivedQuote = "#ff373633", + ), + DefaultTheme.BLACK to ThemeColors( + sentMessage = "#ff41371b", + sentQuote = "#ff654f1c", + receivedMessage = "#ff1f1e1b", + receivedQuote = "#ff2f2d27", + ), + ) + ), + FLOWERS(MR.images.wallpaper_flowers, "flowers", 0.53f, + wallpaperBackgrounds(light = "#ffE2FFE4"), + tint = mapOf( + DefaultTheme.LIGHT to "#ff9CEA59".colorFromReadableHex(), + DefaultTheme.DARK to "#ff31560D".colorFromReadableHex(), + DefaultTheme.SIMPLEX to "#ff36600f".colorFromReadableHex(), + DefaultTheme.BLACK to "#ff31560D".colorFromReadableHex() + ), + mapOf( + DefaultTheme.LIGHT to ThemeColors( + sentMessage = "#fff1ffe5", + sentQuote = "#ffdcf9c4", + receivedMessage = "#ffF4F8F2", + receivedQuote = "#ffe7ece7", + ), + DefaultTheme.DARK to ThemeColors( + sentMessage = "#ff163521", + sentQuote = "#ff1B5330", + receivedMessage = "#ff242523", + receivedQuote = "#ff353733", + ), + DefaultTheme.SIMPLEX to ThemeColors( + sentMessage = "#ff184739", + sentQuote = "#ff1F6F4B", + receivedMessage = "#ff242523", + receivedQuote = "#ff353733", + ), + DefaultTheme.BLACK to ThemeColors( + sentMessage = "#ff184739", + sentQuote = "#ff1F6F4B", + receivedMessage = "#ff1c1f1a", + receivedQuote = "#ff282b25", + ), + ) + ), + HEARTS(MR.images.wallpaper_hearts, "hearts", 0.59f, + wallpaperBackgrounds(light = "#ffFDECEC"), + tint = mapOf( + DefaultTheme.LIGHT to "#fffde0e0".colorFromReadableHex(), + DefaultTheme.DARK to "#ff3c0f0f".colorFromReadableHex(), + DefaultTheme.SIMPLEX to "#ff411010".colorFromReadableHex(), + DefaultTheme.BLACK to "#ff3C0F0F".colorFromReadableHex() + ), + mapOf( + DefaultTheme.LIGHT to ThemeColors( + sentMessage = "#fffff4f4", + sentQuote = "#ffffdfdf", + receivedMessage = "#fff8f6f6", + receivedQuote = "#ffefebeb", + ), + DefaultTheme.DARK to ThemeColors( + sentMessage = "#ff301515", + sentQuote = "#ff4C1818", + receivedMessage = "#ff242121", + receivedQuote = "#ff3b3535", + ), + DefaultTheme.SIMPLEX to ThemeColors( + sentMessage = "#ff491A28", + sentQuote = "#ff761F29", + receivedMessage = "#ff242121", + receivedQuote = "#ff3b3535", + ), + DefaultTheme.BLACK to ThemeColors( + sentMessage = "#ff491A28", + sentQuote = "#ff761F29", + receivedMessage = "#ff1f1b1b", + receivedQuote = "#ff2e2626", + ), + ) + ), + KIDS(MR.images.wallpaper_kids, "kids", 0.53f, + wallpaperBackgrounds(light = "#ffdbfdfb"), + tint = mapOf( + DefaultTheme.LIGHT to "#ffadeffc".colorFromReadableHex(), + DefaultTheme.DARK to "#ff16404B".colorFromReadableHex(), + DefaultTheme.SIMPLEX to "#ff184753".colorFromReadableHex(), + DefaultTheme.BLACK to "#ff16404B".colorFromReadableHex() + ), + mapOf( + DefaultTheme.LIGHT to ThemeColors( + sentMessage = "#ffeafeff", + sentQuote = "#ffcbf4f7", + receivedMessage = "#fff3fafa", + receivedQuote = "#ffe4efef", + ), + DefaultTheme.DARK to ThemeColors( + sentMessage = "#ff16302F", + sentQuote = "#ff1a4a49", + receivedMessage = "#ff252626", + receivedQuote = "#ff373A39", + ), + DefaultTheme.SIMPLEX to ThemeColors( + sentMessage = "#ff1a4745", + sentQuote = "#ff1d6b69", + receivedMessage = "#ff252626", + receivedQuote = "#ff373a39", + ), + DefaultTheme.BLACK to ThemeColors( + sentMessage = "#ff1a4745", + sentQuote = "#ff1d6b69", + receivedMessage = "#ff1e1f1f", + receivedQuote = "#ff262b29", + ), + ) + ), + SCHOOL(MR.images.wallpaper_school, "school", 0.53f, + wallpaperBackgrounds(light = "#ffE7F5FF"), + tint = mapOf( + DefaultTheme.LIGHT to "#ffCEEBFF".colorFromReadableHex(), + DefaultTheme.DARK to "#ff0F293B".colorFromReadableHex(), + DefaultTheme.SIMPLEX to "#ff112f43".colorFromReadableHex(), + DefaultTheme.BLACK to "#ff0F293B".colorFromReadableHex() + ), + mapOf( + DefaultTheme.LIGHT to ThemeColors( + sentMessage = "#ffeef9ff", + sentQuote = "#ffD6EDFA", + receivedMessage = "#ffF3F5F9", + receivedQuote = "#ffe4e8ee", + ), + DefaultTheme.DARK to ThemeColors( + sentMessage = "#ff172833", + sentQuote = "#ff1C3E4F", + receivedMessage = "#ff26282c", + receivedQuote = "#ff393c40", + ), + DefaultTheme.SIMPLEX to ThemeColors( + sentMessage = "#ff1A3C5D", + sentQuote = "#ff235b80", + receivedMessage = "#ff26282c", + receivedQuote = "#ff393c40", + ), + DefaultTheme.BLACK to ThemeColors( + sentMessage = "#ff1A3C5D", + sentQuote = "#ff235b80", + receivedMessage = "#ff1d1e22", + receivedQuote = "#ff292b2f", + ), + ) + ), + TRAVEL(MR.images.wallpaper_travel, "travel", 0.68f, + wallpaperBackgrounds(light = "#fff9eeff"), + tint = mapOf( + DefaultTheme.LIGHT to "#ffeedbfe".colorFromReadableHex(), + DefaultTheme.DARK to "#ff311E48".colorFromReadableHex(), + DefaultTheme.SIMPLEX to "#ff35204e".colorFromReadableHex(), + DefaultTheme.BLACK to "#ff311E48".colorFromReadableHex() + ), + mapOf( + DefaultTheme.LIGHT to ThemeColors( + sentMessage = "#fffcf6ff", + sentQuote = "#fff2e0fc", + receivedMessage = "#ffF6F4F7", + receivedQuote = "#ffede9ee", + ), + DefaultTheme.DARK to ThemeColors( + sentMessage = "#ff33263B", + sentQuote = "#ff53385E", + receivedMessage = "#ff272528", + receivedQuote = "#ff3B373E", + ), + DefaultTheme.SIMPLEX to ThemeColors( + sentMessage = "#ff3C255D", + sentQuote = "#ff623485", + receivedMessage = "#ff26273B", + receivedQuote = "#ff3A394F", + ), + DefaultTheme.BLACK to ThemeColors( + sentMessage = "#ff3C255D", + sentQuote = "#ff623485", + receivedMessage = "#ff231f23", + receivedQuote = "#ff2c2931", + ), + ) + ); + + fun toType(base: DefaultTheme, scale: Float? = null): WallpaperType = + WallpaperType.Preset( + filename, + scale ?: appPrefs.themeOverrides.get().firstOrNull { it.wallpaper != null && it.wallpaper.preset == filename && it.base == base }?.wallpaper?.scale ?: 1f + ) + + companion object { + fun from(filename: String): PresetWallpaper? = + entries.firstOrNull { it.filename == filename } + } +} + +fun wallpaperBackgrounds(light: String): Map<DefaultTheme, Color> = + mapOf( + DefaultTheme.LIGHT to light.colorFromReadableHex(), + DefaultTheme.DARK to "#ff121212".colorFromReadableHex(), + DefaultTheme.SIMPLEX to "#ff111528".colorFromReadableHex(), + DefaultTheme.BLACK to "#ff070707".colorFromReadableHex() + ) + +@Serializable +enum class WallpaperScaleType(val contentScale: ContentScale, val text: StringResource) { + @SerialName("fill") FILL(ContentScale.Crop, MR.strings.wallpaper_scale_fill), + @SerialName("fit") FIT(ContentScale.Fit, MR.strings.wallpaper_scale_fit), + @SerialName("repeat") REPEAT(ContentScale.Fit, MR.strings.wallpaper_scale_repeat), +} + +sealed class WallpaperType { + abstract val scale: Float? + + val image by lazy { + val filename = when (this) { + is Preset -> filename + is Image -> filename + else -> return@lazy null + } + if (filename == "") return@lazy null + if (cachedImages[filename] != null) { + cachedImages[filename] + } else { + val res = if (this is Preset) { + (PresetWallpaper.from(filename) ?: PresetWallpaper.CATS).res.toComposeImageBitmap()!! + } else { + try { + // In case of unintentional image deletion don't crash the app + File(getWallpaperFilePath(filename)).inputStream().use { loadImageBitmap(it) } + } catch (e: Exception) { + Log.e(TAG, "Error while loading wallpaper file: ${e.stackTraceToString()}") + null + } + } + res?.prepareToDraw() + cachedImages[filename] = res ?: return@lazy null + res + } + } + + fun sameType(other: WallpaperType?): Boolean = + if (this is Preset && other is Preset) this.filename == other.filename + else this.javaClass == other?.javaClass + + fun samePreset(other: PresetWallpaper?): Boolean = this is Preset && filename == other?.filename + + data class Preset( + val filename: String, + override val scale: Float?, + ): WallpaperType() { + val predefinedImageScale = PresetWallpaper.from(filename)?.scale ?: 1f + } + + data class Image( + val filename: String, + override val scale: Float?, + val scaleType: WallpaperScaleType?, + ): WallpaperType() + + object Empty: WallpaperType() { + override val scale: Float? + get() = null + } + + fun defaultBackgroundColor(theme: DefaultTheme, materialBackground: Color): Color = + if (this is Preset) { + (PresetWallpaper.from(filename) ?: PresetWallpaper.CATS).background[theme]!! + } else { + materialBackground + } + + fun defaultTintColor(theme: DefaultTheme): Color = + if (this is Preset) { + (PresetWallpaper.from(filename) ?: PresetWallpaper.CATS).tint[theme]!! + } else if (this is Image && scaleType == WallpaperScaleType.REPEAT) { + Color.Transparent + } else { + Color.Transparent + } + + companion object { + var cachedImages: MutableMap<String, ImageBitmap> = mutableMapOf() + + fun from(wallpaper: ThemeWallpaper?): WallpaperType? { + return if (wallpaper == null) { + null + } else if (wallpaper.preset != null) { + Preset(wallpaper.preset, wallpaper.scale) + } else if (wallpaper.imageFile != null) { + Image(wallpaper.imageFile, wallpaper.scale, wallpaper.scaleType) + } else { + Empty + } + } + } +} + +private fun drawToBitmap(image: ImageBitmap, imageScale: Float, tint: Color, size: Size, density: Float, layoutDirection: LayoutDirection): ImageBitmap { + val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low + val drawScope = CanvasDrawScope() + // Don't allow to make zero size because it crashes the app when reducing a size of a window on desktop + val bitmap = ImageBitmap(size.width.toInt().coerceAtLeast(1), size.height.toInt().coerceAtLeast(1)) + val canvas = Canvas(bitmap) + drawScope.draw( + density = Density(density), + layoutDirection = layoutDirection, + canvas = canvas, + size = size, + ) { + val scale = imageScale * density + for (h in 0..(size.height / image.height / scale).roundToInt()) { + for (w in 0..(size.width / image.width / scale).roundToInt()) { + drawImage( + image, + dstOffset = IntOffset(x = (w * image.width * scale).roundToInt(), y = (h * image.height * scale).roundToInt()), + dstSize = IntSize((image.width * scale).roundToInt(), (image.height * scale).roundToInt()), + colorFilter = ColorFilter.tint(tint, BlendMode.SrcIn), + filterQuality = quality + ) + } + } + } + return bitmap +} + +fun CacheDrawScope.chatViewBackground( + image: ImageBitmap, + imageType: WallpaperType, + background: Color, + tint: Color, + graphicsLayerSize: MutableState<IntSize>? = null, + backgroundGraphicsLayer: GraphicsLayer? = null +): DrawResult { + val imageScale = if (imageType is WallpaperType.Preset) { + (imageType.scale ?: 1f) * imageType.predefinedImageScale + } else if (imageType is WallpaperType.Image && imageType.scaleType == WallpaperScaleType.REPEAT) { + imageType.scale ?: 1f + } else { + 1f + } + val image = if (imageType is WallpaperType.Preset || (imageType is WallpaperType.Image && imageType.scaleType == WallpaperScaleType.REPEAT)) { + drawToBitmap(image, imageScale, tint, size, density, layoutDirection) + } else { + image + } + + return onDrawBehind { + copyBackgroundToAppBar(graphicsLayerSize, backgroundGraphicsLayer) { + val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low + drawRect(background) + when (imageType) { + is WallpaperType.Preset -> drawImage(image) + is WallpaperType.Image -> when (val scaleType = imageType.scaleType ?: WallpaperScaleType.FILL) { + WallpaperScaleType.REPEAT -> drawImage(image) + WallpaperScaleType.FILL, WallpaperScaleType.FIT -> { + clipRect { + val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height)) + val scaledWidth = (image.width * scale.scaleX).roundToInt() + val scaledHeight = (image.height * scale.scaleY).roundToInt() + // Large image will cause freeze + if (image.width > 4320 || image.height > 4320) return@clipRect + + drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + if (scaleType == WallpaperScaleType.FIT) { + if (scaledWidth < size.width) { + // has black lines at left and right sides + var x = (size.width - scaledWidth) / 2 + while (x > 0) { + drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + x -= scaledWidth + } + x = size.width - (size.width - scaledWidth) / 2 + while (x < size.width) { + drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + x += scaledWidth + } + } else { + // has black lines at top and bottom sides + var y = (size.height - scaledHeight) / 2 + while (y > 0) { + drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + y -= scaledHeight + } + y = size.height - (size.height - scaledHeight) / 2 + while (y < size.height) { + drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + y += scaledHeight + } + } + } + } + drawRect(tint) + } + } + is WallpaperType.Empty -> {} + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.kt index aa3c4560ea..33cf7c2263 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.kt @@ -19,6 +19,8 @@ fun ChooseAttachmentView(attachmentOption: MutableState<AttachmentOption?>, hide Box( modifier = Modifier .fillMaxWidth() + .navigationBarsPadding() + .imePadding() .wrapContentHeight() .onFocusChanged { focusState -> if (!focusState.hasFocus) hide() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt deleted file mode 100644 index 1a29a334a8..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt +++ /dev/null @@ -1,96 +0,0 @@ -package chat.simplex.common.views.helpers - -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.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import chat.simplex.common.ui.theme.* -import chat.simplex.res.MR -import dev.icerock.moko.resources.compose.painterResource - -@Composable -fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}) { - Column( - Modifier - .fillMaxWidth() - .heightIn(min = AppBarHeight) - .padding(horizontal = AppBarHorizontalPadding), - ) { - Row( - Modifier - .padding(top = 4.dp), // Like in DefaultAppBar - content = { - Row( - Modifier.fillMaxWidth().height(TextFieldDefaults.MinHeight), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - if (showClose) { - NavigationButtonBack(onButtonClicked = close) - } else { - Spacer(Modifier) - } - Row { - endButtons() - } - } - } - ) - } -} - -@Composable -fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f) { - val theme = CurrentColors.collectAsState() - val titleColor = CurrentColors.collectAsState().value.appColors.title - val brush = 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 - Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) - Column { - Text( - title, - Modifier - .fillMaxWidth() - .padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp,), - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.h1.copy(brush = brush), - color = MaterialTheme.colors.primaryVariant, - textAlign = TextAlign.Center - ) - if (hostDevice != null) { - HostDeviceTitle(hostDevice) - } - Spacer(Modifier.height(bottomPadding)) - } -} - -@Composable -private fun HostDeviceTitle(hostDevice: Pair<Long?, String>, extraPadding: Boolean = false) { - Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { - Icon(painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300), null, Modifier.size(15.dp), tint = MaterialTheme.colors.secondary) - Spacer(Modifier.width(10.dp)) - Text(hostDevice.second, color = MaterialTheme.colors.secondary) - } -} - -@Preview/*( - uiMode = Configuration.UI_MODE_NIGHT_YES, - showBackground = true, - name = "Dark Mode" -)*/ -@Composable -fun PreviewCloseSheetBar() { - SimpleXTheme { - CloseSheetBar(close = {}) - } -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CollapsingAppBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CollapsingAppBar.kt new file mode 100644 index 0000000000..ad6611b9d9 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CollapsingAppBar.kt @@ -0,0 +1,99 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.* +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.graphics.rememberGraphicsLayer +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.unit.IntSize +import chat.simplex.common.model.ChatController.appPrefs + +val LocalAppBarHandler: ProvidableCompositionLocal<AppBarHandler?> = staticCompositionLocalOf { null } + +@Composable +fun rememberAppBarHandler(key1: Any? = null, key2: Any? = null, keyboardCoversBar: Boolean = true): AppBarHandler { + val graphicsLayer = rememberGraphicsLayer() + val backgroundGraphicsLayer = rememberGraphicsLayer() + return remember(key1, key2) { AppBarHandler(graphicsLayer, backgroundGraphicsLayer, keyboardCoversBar) } +} + +@Composable +fun adjustAppBarHandler(handler: AppBarHandler): AppBarHandler { + val graphicsLayer = rememberGraphicsLayer() + val backgroundGraphicsLayer = rememberGraphicsLayer() + if (handler.graphicsLayer == null || handler.graphicsLayer?.isReleased == true || handler.backgroundGraphicsLayer?.isReleased == true) { + handler.graphicsLayer = graphicsLayer + handler.backgroundGraphicsLayer = backgroundGraphicsLayer + } + return handler +} + +fun Modifier.copyViewToAppBar(blurRadius: Int, graphicsLayer: GraphicsLayer?): Modifier { + return if (blurRadius > 0 && graphicsLayer != null) { + this.drawWithContent { + graphicsLayer.record { + this@drawWithContent.drawContent() + } + drawLayer(graphicsLayer) + } + } else this +} + +fun DrawScope.copyBackgroundToAppBar(graphicsLayerSize: MutableState<IntSize>?, backgroundGraphicsLayer: GraphicsLayer?, scope: DrawScope.() -> Unit) { + val blurRadius = appPrefs.appearanceBarsBlurRadius.get() + if (blurRadius > 0 && graphicsLayerSize != null && backgroundGraphicsLayer != null) { + graphicsLayerSize.value = backgroundGraphicsLayer.size + backgroundGraphicsLayer.record { + scope() + } + drawLayer(backgroundGraphicsLayer) + } else { + scope() + } +} + +@Stable +class AppBarHandler( + var graphicsLayer: GraphicsLayer?, + var backgroundGraphicsLayer: GraphicsLayer?, + val keyboardCoversBar: Boolean = true, + listState: LazyListState = LazyListState(0, 0), + scrollState: ScrollState = ScrollState(initial = 0) +) { + val title = mutableStateOf("") + var listState by mutableStateOf(listState, structuralEqualityPolicy()) + internal set + + var scrollState by mutableStateOf(scrollState, structuralEqualityPolicy()) + internal set + + val connection = CollapsingAppBarNestedScrollConnection() + + val backgroundGraphicsLayerSize: MutableState<IntSize> = mutableStateOf(IntSize.Zero) + + companion object { + var appBarMaxHeightPx: Int = 0 + } +} + +class CollapsingAppBarNestedScrollConnection(): NestedScrollConnection { + var scrollTrackingEnabled = true + var appBarOffset: Float by mutableFloatStateOf(0f) + + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + appBarOffset += available.y + return Offset(0f, 0f) + } + + override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { + appBarOffset -= available.y + return Offset(x = 0f, 0f) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.kt index fb1df940d9..3c44cbb4dd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.kt @@ -1,116 +1,21 @@ package chat.simplex.common.views.helpers -import androidx.compose.foundation.clickable +import SectionItemView import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CornerSize -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 dev.icerock.moko.resources.compose.painterResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog -import chat.simplex.common.ui.theme.DEFAULT_PADDING +import androidx.compose.ui.text.style.TextAlign import chat.simplex.common.model.CustomTimeUnit import chat.simplex.common.model.timeText import chat.simplex.res.MR -import com.sd.lib.compose.wheel_picker.* @Composable -fun CustomTimePicker( +expect fun CustomTimePicker( selection: MutableState<Int>, timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits -) { - fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List<Int> { - val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit) - val regularUnitValues = (unitLimits.minValue..unitLimits.maxValue).toList() - return regularUnitValues + if (regularUnitValues.contains(selectedValue)) emptyList() else listOf(selectedValue) - } - - val (unit, duration) = CustomTimeUnit.toTimeUnit(selection.value) - val selectedUnit: MutableState<CustomTimeUnit> = remember { mutableStateOf(unit) } - val selectedDuration = remember { mutableStateOf(duration) } - val selectedUnitValues = remember { mutableStateOf(getUnitValues(selectedUnit.value, selectedDuration.value)) } - val isTriggered = remember { mutableStateOf(false) } - - LaunchedEffect(selectedUnit.value) { - // on initial composition, if passed selection doesn't fit into picker bounds, so that selectedDuration is bigger than selectedUnit maxValue - // (e.g., for selection = 121 seconds: selectedUnit would be Second, selectedDuration would be 121 > selectedUnit maxValue of 120), - // selectedDuration would've been replaced by maxValue - isTriggered check prevents this by skipping LaunchedEffect on initial composition - if (isTriggered.value) { - val maxValue = timeUnitsLimits.firstOrNull { it.timeUnit == selectedUnit.value }?.maxValue - if (maxValue != null && selectedDuration.value > maxValue) { - selectedDuration.value = maxValue - selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value) - } else { - selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value) - selection.value = selectedUnit.value.toSeconds * selectedDuration.value - } - } else { - isTriggered.value = true - } - } - - LaunchedEffect(selectedDuration.value) { - selection.value = selectedUnit.value.toSeconds * selectedDuration.value - } - - Row( - Modifier - .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING), - horizontalArrangement = Arrangement.spacedBy(0.dp) - ) { - Column(Modifier.weight(1f)) { - val durationPickerState = rememberFWheelPickerState(selectedUnitValues.value.indexOf(selectedDuration.value)) - FVerticalWheelPicker( - count = selectedUnitValues.value.count(), - state = durationPickerState, - unfocusedCount = 2, - focus = { - FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary) - } - ) { index -> - Text( - selectedUnitValues.value[index].toString(), - fontSize = 18.sp, - color = MaterialTheme.colors.primary - ) - } - LaunchedEffect(durationPickerState) { - snapshotFlow { durationPickerState.currentIndex } - .collect { - selectedDuration.value = selectedUnitValues.value[it] - } - } - } - Column(Modifier.weight(1f)) { - val unitPickerState = rememberFWheelPickerState(timeUnitsLimits.indexOfFirst { it.timeUnit == selectedUnit.value }) - FVerticalWheelPicker( - count = timeUnitsLimits.count(), - state = unitPickerState, - unfocusedCount = 2, - focus = { - FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary) - } - ) { index -> - Text( - timeUnitsLimits[index].timeUnit.text, - fontSize = 18.sp, - color = MaterialTheme.colors.primary - ) - } - LaunchedEffect(unitPickerState) { - snapshotFlow { unitPickerState.currentIndex } - .collect { - selectedUnit.value = timeUnitsLimits[it].timeUnit - } - } - } - } -} +) data class TimeUnitLimits( val timeUnit: CustomTimeUnit, @@ -141,8 +46,7 @@ data class TimeUnitLimits( } } -@Composable -fun CustomTimePickerDialog( +fun showCustomTimePickerDialog( selection: MutableState<Int>, timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits, title: String, @@ -150,52 +54,26 @@ fun CustomTimePickerDialog( confirmButtonAction: (Int) -> Unit, cancel: () -> Unit ) { - DefaultDialog(onDismissRequest = cancel) { - Surface( - shape = RoundedCornerShape(corner = CornerSize(25.dp)) - ) { - Box( - contentAlignment = Alignment.Center + AlertManager.shared.showAlertDialogButtonsColumn( + title = title, + onDismissRequest = cancel + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + CustomTimePicker( + selection, + timeUnitsLimits + ) + SectionItemView({ + AlertManager.shared.hideAlert() + confirmButtonAction(selection.value) + } ) { - Column( - modifier = Modifier.padding(DEFAULT_PADDING), - verticalArrangement = Arrangement.spacedBy(6.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text(" ") // centers title - Text( - title, - fontSize = 16.sp, - color = MaterialTheme.colors.secondary - ) - Icon( - painterResource(MR.images.ic_close), - generalGetString(MR.strings.icon_descr_close_button), - tint = MaterialTheme.colors.secondary, - modifier = Modifier - .size(25.dp) - .clickable { cancel() } - ) - } - - CustomTimePicker( - selection, - timeUnitsLimits - ) - - TextButton(onClick = { confirmButtonAction(selection.value) }) { - Text( - confirmButtonText, - fontSize = 18.sp, - color = MaterialTheme.colors.primary - ) - } - } + Text( + confirmButtonText, + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.primary + ) } } } @@ -219,7 +97,6 @@ fun DropdownCustomTimePickerSettingRow( val dropdownSelection: MutableState<DropdownSelection> = remember { mutableStateOf(DropdownSelection.DropdownValue(selection.value)) } val values: MutableState<List<DropdownSelection>> = remember { mutableStateOf(getValues(selection.value)) } - val showCustomTimePicker = remember { mutableStateOf(false) } fun updateValue(selectedValue: Int?) { values.value = getValues(selectedValue) @@ -246,28 +123,22 @@ fun DropdownCustomTimePickerSettingRow( onSelected = { sel: DropdownSelection -> when (sel) { is DropdownSelection.DropdownValue -> updateValue(sel.value) - DropdownSelection.Custom -> showCustomTimePicker.value = true + DropdownSelection.Custom -> { + val selectedCustomTime = mutableStateOf(selection.value ?: 86400) + showCustomTimePickerDialog( + selectedCustomTime, + timeUnitsLimits = customPickerTimeUnitsLimits, + title = customPickerTitle, + confirmButtonText = customPickerConfirmButtonText, + confirmButtonAction = ::updateValue, + cancel = { + dropdownSelection.value = DropdownSelection.DropdownValue(selection.value) + } + ) + } } } ) - - if (showCustomTimePicker.value) { - val selectedCustomTime = remember { mutableStateOf(selection.value ?: 86400) } - CustomTimePickerDialog( - selectedCustomTime, - timeUnitsLimits = customPickerTimeUnitsLimits, - title = customPickerTitle, - confirmButtonText = customPickerConfirmButtonText, - confirmButtonAction = { time -> - updateValue(time) - showCustomTimePicker.value = false - }, - cancel = { - dropdownSelection.value = DropdownSelection.DropdownValue(selection.value) - showCustomTimePicker.value = false - } - ) - } } private sealed class DropdownSelection { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt index e7da47f8f0..4827e6ae61 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt @@ -39,22 +39,25 @@ object DatabaseUtils { } } - private fun hasDatabase(rootDir: String): Boolean = - File(rootDir + File.separator + chatDatabaseFileName).exists() && File(rootDir + File.separator + agentDatabaseFileName).exists() + fun hasAtLeastOneDatabase(rootDir: String): Boolean = + File(rootDir + File.separator + chatDatabaseFileName).exists() || File(rootDir + File.separator + agentDatabaseFileName).exists() + + fun hasOnlyOneDatabase(rootDir: String): Boolean = + File(rootDir + File.separator + chatDatabaseFileName).exists() != File(rootDir + File.separator + agentDatabaseFileName).exists() fun useDatabaseKey(): String { Log.d(TAG, "useDatabaseKey ${appPreferences.storeDBPassphrase.get()}") var dbKey = "" val useKeychain = appPreferences.storeDBPassphrase.get() if (useKeychain) { - if (!hasDatabase(dataDir.absolutePath)) { + if (!hasAtLeastOneDatabase(dataDir.absolutePath)) { dbKey = randomDatabasePassword() ksDatabasePassword.set(dbKey) appPreferences.initialRandomDBPassphrase.set(true) } else { dbKey = ksDatabasePassword.get() ?: "" } - } else if (appPlatform.isDesktop && !hasDatabase(dataDir.absolutePath)) { + } else if (appPlatform.isDesktop && !hasAtLeastOneDatabase(dataDir.absolutePath)) { // In case of database was deleted by hand dbKey = randomDatabasePassword() ksDatabasePassword.set(dbKey) @@ -64,7 +67,7 @@ object DatabaseUtils { return dbKey } - private fun randomDatabasePassword(): String { + fun randomDatabasePassword(): String { val s = ByteArray(32) SecureRandom().nextBytes(s) return s.toBase64StringForPassphrase().replace("\n", "") @@ -75,14 +78,13 @@ object DatabaseUtils { sealed class DBMigrationResult { @Serializable @SerialName("ok") object OK: DBMigrationResult() @Serializable @SerialName("invalidConfirmation") object InvalidConfirmation: DBMigrationResult() - @Serializable @SerialName("errorNotADatabase") class ErrorNotADatabase(val dbFile: String): DBMigrationResult() - @Serializable @SerialName("errorMigration") class ErrorMigration(val dbFile: String, val migrationError: MigrationError): DBMigrationResult() - @Serializable @SerialName("errorSQL") class ErrorSQL(val dbFile: String, val migrationSQLError: String): DBMigrationResult() + @Serializable @SerialName("errorNotADatabase") data class ErrorNotADatabase(val dbFile: String): DBMigrationResult() + @Serializable @SerialName("errorMigration") data class ErrorMigration(val dbFile: String, val migrationError: MigrationError): DBMigrationResult() + @Serializable @SerialName("errorSQL") data class ErrorSQL(val dbFile: String, val migrationSQLError: String): DBMigrationResult() @Serializable @SerialName("errorKeychain") object ErrorKeychain: DBMigrationResult() - @Serializable @SerialName("unknown") class Unknown(val json: String): DBMigrationResult() + @Serializable @SerialName("unknown") data class Unknown(val json: String): DBMigrationResult() } - enum class MigrationConfirmation(val value: String) { YesUp("yesUp"), YesUpDown ("yesUpDown"), @@ -90,7 +92,7 @@ enum class MigrationConfirmation(val value: String) { } fun defaultMigrationConfirmation(appPrefs: AppPreferences): MigrationConfirmation = - if (appPrefs.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp + if (appPrefs.developerTools.get() && appPrefs.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp @Serializable sealed class MigrationError { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt index a6f0d2c9b6..b0366cceb3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt @@ -3,7 +3,6 @@ package chat.simplex.common.views.helpers import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.ZeroCornerSize import androidx.compose.foundation.text.* import androidx.compose.material.* @@ -22,13 +21,11 @@ import androidx.compose.ui.text.input.* import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import chat.simplex.common.views.database.PassphraseStrength -import chat.simplex.common.views.database.validKey import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch -@OptIn(ExperimentalComposeUiApi::class) @Composable fun DefaultBasicTextField( modifier: Modifier, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt index 267fc86462..c6a566c6f6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt @@ -5,8 +5,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp @@ -14,7 +13,9 @@ import androidx.compose.ui.unit.dp @Composable fun DefaultDropdownMenu( showMenu: MutableState<Boolean>, + modifier: Modifier = Modifier, offset: DpOffset = DpOffset(0.dp, 0.dp), + onClosed: State<() -> Unit> = remember { mutableStateOf({}) }, dropdownMenuItems: (@Composable () -> Unit)? ) { MaterialTheme( @@ -23,13 +24,18 @@ fun DefaultDropdownMenu( DropdownMenu( expanded = showMenu.value, onDismissRequest = { showMenu.value = false }, - modifier = Modifier + modifier = modifier .widthIn(min = 250.dp) .background(MaterialTheme.colors.surface) .padding(vertical = 4.dp), offset = offset, ) { dropdownMenuItems?.invoke() + DisposableEffect(Unit) { + onDispose { + onClosed.value() + } + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultProgressBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultProgressBar.kt new file mode 100644 index 0000000000..675584ae13 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultProgressBar.kt @@ -0,0 +1,28 @@ +package chat.simplex.common.views.helpers + +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.TextAlign +import androidx.compose.ui.unit.dp +import chat.simplex.common.ui.theme.DEFAULT_PADDING + +@Composable +fun DefaultProgressView(description: String?) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + CircularProgressIndicator( + Modifier + .padding(bottom = if (description != null) DEFAULT_PADDING else 0.dp) + .size(30.dp), + color = MaterialTheme.colors.secondary, + strokeWidth = 2.5.dp + ) + if (description != null) { + Text(description, textAlign = TextAlign.Center) + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultSwitch.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultSwitch.kt index 75abc67b46..79255fb0bf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultSwitch.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultSwitch.kt @@ -25,9 +25,9 @@ fun DefaultSwitch( ) ) { val color = if (checked) MaterialTheme.colors.primary.copy(alpha = 0.3f) else MaterialTheme.colors.secondary.copy(alpha = 0.3f) - val size = with(LocalDensity.current) { Size(46.dp.toPx(), 28.dp.toPx()) } - val offset = with(LocalDensity.current) { Offset(1.dp.toPx(), 10.dp.toPx()) } - val radius = with(LocalDensity.current) { 28.dp.toPx() } + val size = with(LocalDensity.current) { Size(40.dp.toPx(), 26.dp.toPx()) } + val offset = with(LocalDensity.current) { Offset(4.dp.toPx(), 11.dp.toPx()) } + val radius = with(LocalDensity.current) { 13.dp.toPx() } Switch( checked = checked, onCheckedChange = onCheckedChange, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt index 0162ac7e78..1c5f86c8b5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt @@ -3,51 +3,138 @@ package chat.simplex.common.views.helpers import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.* import androidx.compose.ui.graphics.* +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.item.CenteredRowLayout import chat.simplex.res.MR +import kotlin.math.absoluteValue @Composable -fun DefaultTopAppBar( - navigationButton: @Composable RowScope.() -> Unit, - title: (@Composable () -> Unit)?, +fun DefaultAppBar( + navigationButton: (@Composable RowScope.() -> Unit)? = null, + title: (@Composable () -> Unit)? = null, + fixedTitleText: String? = null, onTitleClick: (() -> Unit)? = null, - showSearch: Boolean, - onSearchValueChanged: (String) -> Unit, - buttons: List<@Composable RowScope.() -> Unit> = emptyList(), + onTop: Boolean, + showSearch: Boolean = false, + searchAlwaysVisible: Boolean = false, + onSearchValueChanged: (String) -> Unit = {}, + buttons: @Composable RowScope.() -> Unit = {}, ) { // If I just disable clickable modifier when don't need it, it will stop passing clicks to search. Replacing the whole modifier val modifier = if (!showSearch) { Modifier.clickable(enabled = onTitleClick != null, onClick = onTitleClick ?: { }) - } else Modifier + } else if (!onTop) Modifier.imePadding() + else Modifier - TopAppBar( - modifier = modifier, - title = { - if (!showSearch) { - title?.invoke() - } else { - SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = false, onValueChange = onSearchValueChanged) + val themeBackgroundMix = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) + val prefAlpha = remember { appPrefs.inAppBarsAlpha.state } + val handler = LocalAppBarHandler.current + val connection = LocalAppBarHandler.current?.connection + val titleText = remember(handler?.title?.value, fixedTitleText) { + if (fixedTitleText != null) { + mutableStateOf(fixedTitleText) + } else { + handler?.title ?: mutableStateOf("") + } + } + val keyboardInset = WindowInsets.ime + Box(modifier) { + val density = LocalDensity.current + val blurRadius = remember { appPrefs.appearanceBarsBlurRadius.state } + Box(Modifier + .matchParentSize() + .blurredBackgroundModifier(keyboardInset, handler, blurRadius, prefAlpha, handler?.keyboardCoversBar == true, onTop, density) + .drawWithCache { + // store it as a variable, don't put it inside if without holding it here. Compiler don't see it changes otherwise + val alpha = prefAlpha.value + val backgroundColor = if (title != null || fixedTitleText != null || connection == null || !onTop) { + themeBackgroundMix.copy(alpha) + } else { + themeBackgroundMix.copy(topTitleAlpha(false, connection)) + } + onDrawBehind { + drawRect(backgroundColor) + } } - }, - backgroundColor = if (isInDarkTheme()) ToolbarDark else ToolbarLight, - navigationIcon = navigationButton, - buttons = if (!showSearch) buttons else emptyList(), - centered = !showSearch, + ) + Box( + Modifier + .fillMaxWidth() + .then(if (!onTop) Modifier.navigationBarsPadding() else Modifier) + .heightIn(min = AppBarHeight * fontSizeSqrtMultiplier) + ) { + AppBar( + title = { + if (showSearch) { + SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = searchAlwaysVisible, reducedCloseButtonPadding = 12.dp, onValueChange = onSearchValueChanged) + } else if (title != null) { + title() + } else if (titleText.value.isNotEmpty() && connection != null) { + Row( + Modifier + .graphicsLayer { + alpha = if (fixedTitleText != null) 1f else topTitleAlpha(true, connection) + } + ) { + Text( + titleText.value, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + }, + navigationIcon = navigationButton, + buttons = if (!showSearch) buttons else {{}}, + centered = !showSearch && (title != null || !onTop), + onTop = onTop, + ) + AppBarDivider(onTop, title != null || fixedTitleText != null, connection) + } + } +} + + +@Composable +fun CallAppBar( + title: @Composable () -> Unit, + onBack: () -> Unit +) { + AppBar( + title, + navigationIcon = { NavigationButtonBack(tintColor = Color(0xFFFFFFD8), onButtonClicked = onBack) }, + centered = false, + onTop = true ) } @Composable -fun NavigationButtonBack(onButtonClicked: (() -> Unit)?) { +fun NavigationButtonBack(onButtonClicked: (() -> Unit)?, tintColor: Color = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, height: Dp = 24.dp) { IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) { Icon( - painterResource(MR.images.ic_arrow_back_ios_new), stringResource(MR.strings.back), tint = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + painterResource(MR.images.ic_arrow_back_ios_new), stringResource(MR.strings.back), Modifier.height(height), tint = tintColor + ) + } +} + +@Composable +fun NavigationButtonClose(onButtonClicked: (() -> Unit)?, tintColor: Color = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, height: Dp = 24.dp) { + IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) { + Icon( + painterResource(MR.images.ic_close), stringResource(MR.strings.back), Modifier.height(height), tint = tintColor ) } } @@ -73,57 +160,108 @@ fun NavigationButtonMenu(onButtonClicked: () -> Unit) { } @Composable -private fun TopAppBar( +private fun BoxScope.AppBarDivider(onTop: Boolean, fixedAlpha: Boolean, connection: CollapsingAppBarNestedScrollConnection?) { + if (connection != null) { + Divider( + Modifier + .align(if (onTop) Alignment.BottomStart else Alignment.TopStart) + .graphicsLayer { + alpha = if (!onTop || fixedAlpha) 1f else topTitleAlpha(false, connection, 1f) + } + ) + } else { + Divider(Modifier.align(if (onTop) Alignment.BottomStart else Alignment.TopStart)) + } +} + +@Composable +private fun AppBar( title: @Composable () -> Unit, modifier: Modifier = Modifier, navigationIcon: @Composable (RowScope.() -> Unit)? = null, - buttons: List<@Composable RowScope.() -> Unit> = emptyList(), - backgroundColor: Color = MaterialTheme.colors.primarySurface, + buttons: @Composable RowScope.() -> Unit = {}, centered: Boolean, + onTop: Boolean, ) { - Box( - modifier - .fillMaxWidth() - .height(AppBarHeight) - .background(backgroundColor) - .padding(horizontal = 4.dp), - contentAlignment = Alignment.CenterStart, + val adjustedModifier = modifier + .then(if (onTop) Modifier.statusBarsPadding() else Modifier) + .height(AppBarHeight * fontSizeSqrtMultiplier) + .fillMaxWidth() + .padding(horizontal = AppBarHorizontalPadding) + if (centered) { + AppBarCenterAligned(adjustedModifier, title, navigationIcon, buttons) + } else { + AppBarStartAligned(adjustedModifier, title, navigationIcon, buttons) + } +} + +@Composable +private fun AppBarStartAligned( + modifier: Modifier, + title: @Composable () -> Unit, + navigationIcon: @Composable (RowScope.() -> Unit)? = null, + buttons: @Composable RowScope.() -> Unit +) { + Row( + modifier, + verticalAlignment = Alignment.CenterVertically ) { if (navigationIcon != null) { - Row( - Modifier - .fillMaxHeight() - .width(TitleInsetWithIcon - AppBarHorizontalPadding), - verticalAlignment = Alignment.CenterVertically, - content = navigationIcon - ) + navigationIcon() + Spacer(Modifier.width(AppBarHorizontalPadding)) + } else { + Spacer(Modifier.width(DEFAULT_PADDING)) + } + Row(Modifier + .weight(1f) + .padding(end = DEFAULT_PADDING_HALF) + ) { + title() } Row( - Modifier - .fillMaxHeight() - .fillMaxWidth(), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, ) { - buttons.forEach { it() } - } - val startPadding = if (navigationIcon != null) TitleInsetWithIcon else TitleInsetWithoutIcon - val endPadding = (buttons.size * 50f).dp - Box( - Modifier - .fillMaxWidth() - .padding( - start = if (centered) kotlin.math.max(startPadding.value, endPadding.value).dp else startPadding, - end = if (centered) kotlin.math.max(startPadding.value, endPadding.value).dp else endPadding - ), - contentAlignment = Alignment.Center - ) { - title() + buttons() } } } +@Composable +private fun AppBarCenterAligned( + modifier: Modifier, + title: @Composable () -> Unit, + navigationIcon: @Composable (RowScope.() -> Unit)? = null, + buttons: @Composable RowScope.() -> Unit, +) { + CenteredRowLayout(modifier) { + if (navigationIcon != null) { + Row( + Modifier.padding(end = AppBarHorizontalPadding), + verticalAlignment = Alignment.CenterVertically, + content = navigationIcon + ) + } else { + Spacer(Modifier) + } + Row( + Modifier.padding(end = DEFAULT_PADDING_HALF) + ) { + title() + } + Row( + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + buttons() + } + } +} + +private fun topTitleAlpha(text: Boolean, connection: CollapsingAppBarNestedScrollConnection, alpha: Float = appPrefs.inAppBarsAlpha.get()) = + if (!connection.scrollTrackingEnabled) 0f + else if (connection.appBarOffset.absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 0f + else ((-connection.appBarOffset * 1.5f) / (AppBarHandler.appBarMaxHeightPx)).coerceIn(0f, if (text) 1f else alpha) + val AppBarHeight = 56.dp -val AppBarHorizontalPadding = 4.dp -private val TitleInsetWithoutIcon = DEFAULT_PADDING - AppBarHorizontalPadding -val TitleInsetWithIcon = 72.dp +val AppBarHorizontalPadding = 2.dp diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DragAndDrop.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DragAndDrop.kt new file mode 100644 index 0000000000..cded400892 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DragAndDrop.kt @@ -0,0 +1,177 @@ +package chat.simplex.common.views.helpers + +/* + * This was adapted from google example of drag and drop for Jetpack Compose + * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt + */ + +import androidx.compose.animation.core.* +import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.lazy.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.zIndex +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch + +@Composable +fun rememberDragDropState(lazyListState: LazyListState, onMove: (Int, Int) -> Unit): DragDropState { + val scope = rememberCoroutineScope() + val state = + remember(lazyListState) { + DragDropState(state = lazyListState, onMove = onMove, scope = scope) + } + LaunchedEffect(state) { + while (true) { + val diff = state.scrollChannel.receive() + lazyListState.scrollBy(diff) + } + } + return state +} + +class DragDropState +internal constructor( + private val state: LazyListState, + private val scope: CoroutineScope, + private val onMove: (Int, Int) -> Unit +) { + var draggingItemIndex by mutableStateOf<Int?>(null) + private set + + internal val scrollChannel = Channel<Float>() + + private var draggingItemDraggedDelta by mutableFloatStateOf(0f) + private var draggingItemInitialOffset by mutableIntStateOf(0) + internal val draggingItemOffset: Float + get() = + draggingItemLayoutInfo?.let { item -> + draggingItemInitialOffset + draggingItemDraggedDelta - item.offset + } ?: 0f + + private val draggingItemLayoutInfo: LazyListItemInfo? + get() = state.layoutInfo.visibleItemsInfo.firstOrNull { it.index == draggingItemIndex } + + internal var previousIndexOfDraggedItem by mutableStateOf<Int?>(null) + private set + + internal var previousItemOffset = Animatable(0f) + private set + + internal fun onDragStart(offset: Offset) { + val touchY = offset.y.toInt() + val item = state.layoutInfo.visibleItemsInfo.minByOrNull { + val itemCenter = (it.offset - state.layoutInfo.viewportStartOffset) + it.size / 2 + kotlin.math.abs(touchY - itemCenter) // Find the item closest to the touch position, needs to take viewportStartOffset into account + } + + if (item != null) { + draggingItemIndex = item.index + draggingItemInitialOffset = item.offset + } + } + + + internal fun onDragInterrupted() { + if (draggingItemIndex != null) { + previousIndexOfDraggedItem = draggingItemIndex + val startOffset = draggingItemOffset + scope.launch { + previousItemOffset.snapTo(startOffset) + previousItemOffset.animateTo( + 0f, + spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = 1f) + ) + previousIndexOfDraggedItem = null + } + } + draggingItemDraggedDelta = 0f + draggingItemIndex = null + draggingItemInitialOffset = 0 + } + + internal fun onDrag(offset: Offset) { + draggingItemDraggedDelta += offset.y + + val draggingItem = draggingItemLayoutInfo ?: return + val startOffset = draggingItem.offset + draggingItemOffset + val endOffset = startOffset + draggingItem.size + val middleOffset = startOffset + (endOffset - startOffset) / 2f + + val targetItem = + state.layoutInfo.visibleItemsInfo.find { item -> + middleOffset.toInt() in item.offset..item.offsetEnd && + draggingItem.index != item.index + } + if (targetItem != null) { + if ( + draggingItem.index == state.firstVisibleItemIndex || + targetItem.index == state.firstVisibleItemIndex + ) { + state.requestScrollToItem( + state.firstVisibleItemIndex, + state.firstVisibleItemScrollOffset + ) + } + onMove.invoke(draggingItem.index, targetItem.index) + draggingItemIndex = targetItem.index + } else { + val overscroll = + when { + draggingItemDraggedDelta > 0 -> + (endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f) + draggingItemDraggedDelta < 0 -> + (startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f) + else -> 0f + } + if (overscroll != 0f) { + scrollChannel.trySend(overscroll) + } + } + } + + private val LazyListItemInfo.offsetEnd: Int + get() = this.offset + this.size +} + +fun Modifier.dragContainer(dragDropState: DragDropState): Modifier { + return pointerInput(dragDropState) { + detectDragGesturesAfterLongPress( + onDrag = { change, offset -> + change.consume() + dragDropState.onDrag(offset = offset) + }, + onDragStart = { offset -> dragDropState.onDragStart(offset) }, + onDragEnd = { dragDropState.onDragInterrupted() }, + onDragCancel = { dragDropState.onDragInterrupted() } + ) + } +} + +@Composable +fun LazyItemScope.DraggableItem( + dragDropState: DragDropState, + index: Int, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.(isDragging: Boolean) -> Unit +) { + val dragging = index == dragDropState.draggingItemIndex + val draggingModifier = + if (dragging) { + Modifier.zIndex(1f).graphicsLayer { translationY = dragDropState.draggingItemOffset } + } else if (index == dragDropState.previousIndexOfDraggedItem) { + Modifier.zIndex(1f).graphicsLayer { + translationY = dragDropState.previousItemOffset.value + } + } else { + Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) + } + Column(modifier = modifier.then(draggingModifier)) { content(dragging) } +} 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 67f82e5279..30811d5c94 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 @@ -2,6 +2,7 @@ package chat.simplex.common.views.helpers import androidx.compose.runtime.saveable.Saver +import chat.simplex.common.model.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.serialization.* import kotlinx.serialization.descriptors.* @@ -13,6 +14,7 @@ sealed class SharedContent { data class Text(val text: String): SharedContent() data class Media(val text: String, val uris: List<URI>): SharedContent() data class File(val text: String, val uri: URI): SharedContent() + data class Forward(val chatItems: List<ChatItem>, val fromChatInfo: ChatInfo): SharedContent() } enum class AnimatedViewState { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt index 290bc2cd07..7ed91adbd9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt @@ -1,6 +1,10 @@ package chat.simplex.common.views.helpers +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* import dev.icerock.moko.resources.compose.painterResource import androidx.compose.runtime.* @@ -9,11 +13,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import chat.simplex.res.MR import chat.simplex.common.ui.theme.* import chat.simplex.common.views.usersettings.SettingsActionItemWithContent +import dev.icerock.moko.resources.ImageResource @Composable fun <T> ExposedDropDownSetting( @@ -49,7 +55,7 @@ fun <T> ExposedDropDownSetting( ) Spacer(Modifier.size(12.dp)) Icon( - if (!expanded.value) painterResource(MR.images.ic_expand_more) else painterResource(MR.images.ic_expand_less), + if (!expanded.value) painterResource(MR.images.ic_arrow_drop_down) else painterResource(MR.images.ic_arrow_drop_up), generalGetString(MR.strings.icon_descr_more_button), tint = MaterialTheme.colors.secondary ) @@ -70,7 +76,72 @@ fun <T> ExposedDropDownSetting( selectionOption.second + (if (label != null) " $label" else ""), maxLines = 1, overflow = TextOverflow.Ellipsis, - color = if (isInDarkTheme()) MenuTextColorDark else Color.Black, + color = MenuTextColor, + fontSize = fontSize, + ) + } + } + } + } +} + +@Composable +fun <T> ExposedDropDownSettingWithIcon( + values: List<Triple<T, ImageResource, String>>, + selection: State<T>, + fontSize: TextUnit = 16.sp, + iconPaddingPercent: Float = 0.2f, + listIconSize: Dp = 30.dp, + boxSize: Dp = 60.dp, + iconColor: Color = MenuTextColor, + enabled: State<Boolean> = mutableStateOf(true), + background: Color, + minWidth: Dp = 200.dp, + onSelected: (T) -> Unit +) { + val expanded = remember { mutableStateOf(false) } + ExposedDropdownMenuBox( + expanded = expanded.value, + onExpandedChange = { + expanded.value = !expanded.value && enabled.value + } + ) { + val ripple = remember { ripple(bounded = false, radius = boxSize / 2, color = background.lighter(0.1f)) } + Box( + Modifier + .background(background, CircleShape) + .size(boxSize) + .clickable( + onClick = {}, + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = ripple, + enabled = enabled.value + ), + contentAlignment = Alignment.Center + ) { + val choice = values.first { it.first == selection.value } + Icon(painterResource(choice.second), choice.third, Modifier.padding(boxSize * iconPaddingPercent).fillMaxSize(), tint = iconColor) + } + DefaultExposedDropdownMenu( + modifier = Modifier.widthIn(min = minWidth), + expanded = expanded, + ) { + values.forEach { selectionOption -> + DropdownMenuItem( + onClick = { + onSelected(selectionOption.first) + expanded.value = false + }, + contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f) + ) { + Icon(painterResource(selectionOption.second), selectionOption.third, Modifier.size(listIconSize)) + Spacer(Modifier.width(15.dp)) + Text( + selectionOption.third, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MenuTextColor, fontSize = fontSize, ) } 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 93d0a56766..9c529e547a 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 @@ -1,23 +1,27 @@ package chat.simplex.common.views.helpers import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.Image -import androidx.compose.foundation.background +import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.layoutId import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.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.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.chatViewScrollState +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.withContext @@ -39,11 +43,21 @@ suspend fun getLinkPreview(url: String): LinkPreview? { url } else -> { - val response = Jsoup.connect(url) + val connection = Jsoup.connect(url) .ignoreContentType(true) .timeout(10000) .followRedirects(true) - .execute() + + 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() @@ -76,7 +90,7 @@ suspend fun getLinkPreview(url: String): LinkPreview? { @Composable fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancelEnabled: Boolean) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val sentColor = MaterialTheme.appColors.sentMessage Row( Modifier.fillMaxWidth().padding(top = 8.dp).background(sentColor), verticalAlignment = Alignment.CenterVertically @@ -121,12 +135,21 @@ fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancel } @Composable -fun ChatItemLinkView(linkPreview: LinkPreview) { - Column(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { +fun ChatItemLinkView(linkPreview: LinkPreview, showMenu: State<Boolean>, onLongClick: () -> Unit) { + val image = base64ToBitmap(linkPreview.image) + Column( + Modifier + .layoutId(CHAT_IMAGE_LAYOUT_ID) + .width(if (image.width * 0.97 <= image.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH) + ) { + val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) } Image( - base64ToBitmap(linkPreview.image), + image, stringResource(MR.strings.image_descr_link_preview), - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .desktopModifyBlurredState(true, blurred, showMenu) + .privacyBlur(true, blurred, chatViewScrollState.collectAsState(), onLongClick = onLongClick), contentScale = ContentScale.FillWidth, ) Column(Modifier.padding(top = 6.dp).padding(horizontal = 12.dp)) { @@ -179,7 +202,7 @@ private fun normalizeImageUri(u: URL, imageUri: String) = when { @Composable fun PreviewChatItemLinkView() { SimpleXTheme { - ChatItemLinkView(LinkPreview.sampleData) + ChatItemLinkView(LinkPreview.sampleData, remember { mutableStateOf(false) }) {} } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.kt index 8e6c9fffc1..1f2b5485f2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.kt @@ -1,8 +1,7 @@ package chat.simplex.common.views.helpers import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface +import androidx.compose.material.* import androidx.compose.ui.Modifier import chat.simplex.common.model.ChatController import chat.simplex.common.model.ChatModel @@ -35,6 +34,7 @@ expect fun authenticate( promptSubtitle: String, selfDestruct: Boolean = false, usingLAMode: LAMode = ChatModel.controller.appPrefs.laMode.get(), + oneTime: Boolean, completed: (LAResult) -> Unit ) @@ -42,15 +42,16 @@ fun authenticateWithPasscode( promptTitle: String, promptSubtitle: String, selfDestruct: Boolean, + oneTime: Boolean, completed: (LAResult) -> Unit ) { val password = DatabaseUtils.ksAppPassword.get() ?: return completed(LAResult.Unavailable(generalGetString(MR.strings.la_no_app_password))) - ModalManager.fullscreen.showPasscodeCustomModal { close -> + ModalManager.fullscreen.showPasscodeCustomModal(oneTime) { close -> BackHandler { close() completed(LAResult.Error(generalGetString(MR.strings.authentication_cancelled))) } - Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background.copy(1f), contentColor = LocalContentColor.current) { LocalAuthView(ChatModel, LocalAuthRequest(promptTitle, promptSubtitle, password, selfDestruct && ChatController.appPrefs.selfDestruct.get()) { close() completed(it) 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 703b6f905f..af207d1381 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 @@ -6,30 +6,65 @@ import androidx.compose.foundation.background 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.graphics.Color +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chatlist.StatusBarBackground +import chat.simplex.common.views.onboarding.OnboardingStage +import kotlinx.coroutines.flow.MutableStateFlow import java.util.concurrent.atomic.AtomicBoolean import kotlin.math.min +import kotlin.math.sqrt @Composable fun ModalView( close: () -> Unit, showClose: Boolean = true, - background: Color = MaterialTheme.colors.background, + showAppBar: Boolean = true, + enableClose: Boolean = true, + background: Color = Color.Unspecified, modifier: Modifier = Modifier, + showSearch: Boolean = false, + searchAlwaysVisible: Boolean = false, + onSearchValueChanged: (String) -> Unit = {}, endButtons: @Composable RowScope.() -> Unit = {}, - content: @Composable () -> Unit, + appBar: @Composable (BoxScope.() -> Unit)? = null, + content: @Composable BoxScope.() -> Unit, ) { - if (showClose) { - BackHandler(onBack = close) + if (showClose && showAppBar) { + BackHandler(enabled = enableClose, onBack = close) } - Surface(Modifier.fillMaxSize()) { - Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) { - CloseSheetBar(close, showClose, endButtons) - Box(modifier) { content() } + val oneHandUI = remember { derivedStateOf { if (appPrefs.onboardingStage.state.value == OnboardingStage.OnboardingComplete) appPrefs.oneHandUI.state.value else false } } + Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) { + Box(if (background != Color.Unspecified) Modifier.background(background) else Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) { + Box(modifier = modifier) { + content() + } + if (showAppBar) { + if (oneHandUI.value) { + StatusBarBackground() + } + Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) { + if (appBar != null) { + appBar() + } else { + DefaultAppBar( + navigationButton = if (showClose) { + { NavigationButtonBack(onButtonClicked = if (enableClose) close else null) } + } else null, + onTop = !oneHandUI.value, + showSearch = showSearch, + searchAlwaysVisible = searchAlwaysVisible, + onSearchValueChanged = onSearchValueChanged, + buttons = endButtons + ) + } + } + } } } } @@ -38,27 +73,58 @@ enum class ModalPlacement { START, CENTER, END, FULLSCREEN } +class ModalData(val keyboardCoversBar: Boolean = true) { + private val state = mutableMapOf<String, MutableState<Any?>>() + fun <T> stateGetOrPut (key: String, default: () -> T): MutableState<T> = + state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState<T> + + fun <T> stateGetOrPutNullable (key: String, default: () -> T?): MutableState<T?> = + state.getOrPut(key) { mutableStateOf(default() as Any?) } as MutableState<T?> + + val appBarHandler = AppBarHandler(null, null, keyboardCoversBar = keyboardCoversBar) +} + +enum class ModalViewId { + SECONDARY_CHAT +} + class ModalManager(private val placement: ModalPlacement? = null) { - private val modalViews = arrayListOf<Pair<Boolean, (@Composable (close: () -> Unit) -> Unit)>>() - private val modalCount = mutableStateOf(0) + data class ModalViewHolder( + val id: ModalViewId?, + val animated: Boolean, + val data: ModalData, + val modal: @Composable ModalData.(close: () -> Unit) -> Unit + ) + + private val modalViews = arrayListOf<ModalViewHolder>() + private val _modalCount = mutableStateOf(0) + val modalCount: State<Int> = _modalCount private val toRemove = mutableSetOf<Int>() private var oldViewChanging = AtomicBoolean(false) - private var passcodeView: MutableState<(@Composable (close: () -> Unit) -> Unit)?> = mutableStateOf(null) + // Don't use mutableStateOf() here, because it produces this if showing from SimpleXAPI.startChat(): + // java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied + private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null) + private var onTimePasscodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null) - fun showModal(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit) { - showCustomModal { close -> - ModalView(close, showClose = showClose, endButtons = endButtons, content = content) + fun hasModalOpen(id: ModalViewId): Boolean = modalViews.any { it.id == id } + + 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 -> + ModalView(close, showClose = showClose, endButtons = endButtons, content = { content() }) } } - fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, content: @Composable (close: () -> Unit) -> Unit) { - showCustomModal { close -> - ModalView(close, showClose = showClose, content = { content(close) }) + fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, id: ModalViewId? = null, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) { + showCustomModal(id = id) { close -> + ModalView(close, showClose = showClose, endButtons = endButtons, content = { content(close) }) } } - fun showCustomModal(animated: Boolean = true, modal: @Composable (close: () -> Unit) -> Unit) { + fun showCustomModal(animated: Boolean = true, keyboardCoversBar: Boolean = true, id: ModalViewId? = null, 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. // This is useful when invoking close() and ShowCustomModal one after another without delay. Otherwise, screen will hold prev view if (toRemove.isNotEmpty()) { @@ -67,35 +133,51 @@ 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) - modalViews.add(anim to modal) - modalCount.value = modalViews.size - toRemove.size + modalViews.add(ModalViewHolder(id, anim, data, modal)) + _modalCount.value = modalViews.size - toRemove.size if (placement == ModalPlacement.CENTER) { ChatModel.chatId.value = null } else if (placement == ModalPlacement.END) { - desktopExpandWindowToWidth(DEFAULT_START_MODAL_WIDTH + DEFAULT_MIN_CENTER_MODAL_WIDTH + DEFAULT_END_MODAL_WIDTH) + desktopExpandWindowToWidth(DEFAULT_START_MODAL_WIDTH * sqrt(appPrefs.fontScale.get()) + DEFAULT_MIN_CENTER_MODAL_WIDTH + DEFAULT_END_MODAL_WIDTH * sqrt(appPrefs.fontScale.get())) } } - fun showPasscodeCustomModal(modal: @Composable (close: () -> Unit) -> Unit) { - Log.d(TAG, "ModalManager.showPasscodeCustomModal") - passcodeView.value = modal + fun showPasscodeCustomModal(oneTime: Boolean, modal: @Composable (close: () -> Unit) -> Unit) { + Log.d(TAG, "ModalManager.showPasscodeCustomModal, oneTime: $oneTime") + if (oneTime) { + onTimePasscodeView.value = modal + } else { + passcodeView.value = modal + } } fun hasModalsOpen() = modalCount.value > 0 + val hasModalsOpen: Boolean + @Composable get () = remember { modalCount }.value > 0 + + fun openModalCount() = modalCount.value + fun closeModal() { if (modalViews.isNotEmpty()) { - if (modalViews.lastOrNull()?.first == false) modalViews.removeAt(modalViews.lastIndex) - else runAtomically { toRemove.add(modalViews.lastIndex - min(toRemove.size, modalViews.lastIndex)) } + val lastModal = modalViews.lastOrNull() + if (lastModal != null) { + if (lastModal.id == ModalViewId.SECONDARY_CHAT) chatModel.secondaryChatsContext.value = null + if (!lastModal.animated) + modalViews.removeAt(modalViews.lastIndex) + else + runAtomically { toRemove.add(modalViews.lastIndex - min(toRemove.size, modalViews.lastIndex)) } + } } - modalCount.value = modalViews.size - toRemove.size + _modalCount.value = modalViews.size - toRemove.size } fun closeModals() { + chatModel.secondaryChatsContext.value = null modalViews.clear() toRemove.clear() - modalCount.value = 0 + _modalCount.value = 0 } fun closeModalsExceptFirst() { @@ -108,8 +190,12 @@ class ModalManager(private val placement: ModalPlacement? = null) { @Composable fun showInView() { // Without animation - if (modalCount.value > 0 && modalViews.lastOrNull()?.first == false) { - modalViews.lastOrNull()?.second?.invoke(::closeModal) + if (modalCount.value > 0 && modalViews.lastOrNull()?.animated == false) { + modalViews.lastOrNull()?.let { + CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.data.appBarHandler)) { + it.modal(it.data, ::closeModal) + } + } return } AnimatedContent(targetState = modalCount.value, @@ -121,7 +207,11 @@ class ModalManager(private val placement: ModalPlacement? = null) { }.using(SizeTransform(clip = false)) } ) { - modalViews.getOrNull(it - 1)?.second?.invoke(::closeModal) + modalViews.getOrNull(it - 1)?.let { + CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.data.appBarHandler)) { + it.modal(it.data, ::closeModal) + } + } // This is needed because if we delete from modalViews immediately on request, animation will be bad if (toRemove.isNotEmpty() && it == modalCount.value && transition.currentState == EnterExitState.Visible && !transition.isRunning) { runAtomically { toRemove.removeIf { elem -> modalViews.removeAt(elem); true } } @@ -131,7 +221,12 @@ class ModalManager(private val placement: ModalPlacement? = null) { @Composable fun showPasscodeInView() { - remember { passcodeView }.value?.invoke { passcodeView.value = null } + passcodeView.collectAsState().value?.invoke { passcodeView.value = null } + } + + @Composable + fun showOneTimePasscodeInView() { + onTimePasscodeView.collectAsState().value?.invoke { onTimePasscodeView.value = null } } /** @@ -144,28 +239,6 @@ class ModalManager(private val placement: ModalPlacement? = null) { block() atomicBoolean.set(false) } - - @OptIn(ExperimentalAnimationApi::class) - private fun fromStartToEndTransition() = - slideInHorizontally( - initialOffsetX = { fullWidth -> -fullWidth }, - animationSpec = animationSpec() - ) with slideOutHorizontally( - targetOffsetX = { fullWidth -> fullWidth }, - animationSpec = animationSpec() - ) - - @OptIn(ExperimentalAnimationApi::class) - private fun fromEndToStartTransition() = - slideInHorizontally( - initialOffsetX = { fullWidth -> fullWidth }, - animationSpec = animationSpec() - ) with slideOutHorizontally( - targetOffsetX = { fullWidth -> -fullWidth }, - animationSpec = animationSpec() - ) - -private fun <T> animationSpec() = tween<T>(durationMillis = 250, easing = FastOutSlowInEasing) // private fun <T> animationSpecFromStart() = tween<T>(durationMillis = 150, easing = FastOutLinearInEasing) // private fun <T> animationSpecFromEnd() = tween<T>(durationMillis = 100, easing = FastOutSlowInEasing) @@ -176,11 +249,36 @@ private fun <T> animationSpec() = tween<T>(durationMillis = 250, easing = FastOu val end = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.END) val fullscreen = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.FULLSCREEN) + val floatingTerminal = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.START) + fun closeAllModalsEverywhere() { start.closeModals() center.closeModals() end.closeModals() fullscreen.closeModals() + floatingTerminal.closeModals() } + + @OptIn(ExperimentalAnimationApi::class) + fun fromStartToEndTransition() = + slideInHorizontally( + initialOffsetX = { fullWidth -> -fullWidth }, + animationSpec = animationSpec() + ) with slideOutHorizontally( + targetOffsetX = { fullWidth -> fullWidth }, + animationSpec = animationSpec() + ) + + @OptIn(ExperimentalAnimationApi::class) + fun fromEndToStartTransition() = + slideInHorizontally( + initialOffsetX = { fullWidth -> fullWidth }, + animationSpec = animationSpec() + ) with slideOutHorizontally( + targetOffsetX = { fullWidth -> -fullWidth }, + animationSpec = animationSpec() + ) + + private fun <T> animationSpec() = tween<T>(durationMillis = 250, easing = FastOutSlowInEasing) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Modifiers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Modifiers.kt index 6990a69ebd..8ad877d879 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Modifiers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Modifiers.kt @@ -37,7 +37,7 @@ fun SwipeToDismissModifier( return Modifier.swipeable( state = state, anchors = anchors, - thresholds = { _, _ -> FractionalThreshold(0.5f) }, + thresholds = { _, _ -> FractionalThreshold(0.99f) }, orientation = Orientation.Horizontal, reverseDirection = isRtl, ).offset { IntOffset(state.offset.value.roundToInt(), 0) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProcessedErrors.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProcessedErrors.kt new file mode 100644 index 0000000000..5fa097fb6b --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProcessedErrors.kt @@ -0,0 +1,63 @@ +package chat.simplex.common.views.helpers + +import chat.simplex.common.model.AgentErrorType +import chat.simplex.common.platform.Log +import chat.simplex.common.platform.TAG +import chat.simplex.common.platform.ntfManager +import chat.simplex.common.views.database.restartChatOrApp +import chat.simplex.res.MR +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay + +class ProcessedErrors <T: AgentErrorType>(val interval: Long) { + private var lastShownTimestamp: Long = -1 + private var lastShownOfferRestart: Boolean = false + private var timer: Job = Job() + + fun newError(error: T, offerRestart: Boolean) { + timer.cancel() + timer = withLongRunningApi(slow = 130_000) { + val delayBeforeNext = (lastShownTimestamp + interval) - System.currentTimeMillis() + if ((lastShownOfferRestart || !offerRestart) && delayBeforeNext >= 0) { + delay(delayBeforeNext) + } + lastShownTimestamp = System.currentTimeMillis() + lastShownOfferRestart = offerRestart + AlertManager.shared.hideAllAlerts() + showMessage(error, offerRestart) + } + } + + private fun showMessage(error: T, offerRestart: Boolean) { + when (error) { + is AgentErrorType.CRITICAL -> { + val title = generalGetString(MR.strings.agent_critical_error_title) + val text = generalGetString(MR.strings.agent_critical_error_desc).format(error.criticalErr) + try { + ntfManager.showMessage(title, text) + } catch (e: Throwable) { + Log.e(TAG, e.stackTraceToString()) + } + if (offerRestart) { + AlertManager.shared.showAlertDialog( + title = title, + text = text, + confirmText = generalGetString(MR.strings.restart_chat_button), + onConfirm = ::restartChatOrApp + ) + } else { + AlertManager.shared.showAlertMsg( + title = title, + text = text, + ) + } + } + is AgentErrorType.INTERNAL -> { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.agent_internal_error_title), + text = generalGetString(MR.strings.agent_internal_error_desc).format(error.internalErr), + ) + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt index 4b6c70df44..7124f34ac0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt @@ -2,7 +2,7 @@ package chat.simplex.common.views.helpers import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.ZeroCornerSize import androidx.compose.foundation.text.* import androidx.compose.material.* @@ -10,7 +10,6 @@ import androidx.compose.material.TextFieldDefaults.indicatorLine import androidx.compose.material.TextFieldDefaults.textFieldWithLabelPadding import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -19,21 +18,24 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController 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.* -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.* +import chat.simplex.common.platform.* import chat.simplex.res.MR import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch -@OptIn(ExperimentalComposeUiApi::class) @Composable fun SearchTextField( modifier: Modifier, alwaysVisible: Boolean, searchText: MutableState<TextFieldValue> = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }, placeholder: String = stringResource(MR.strings.search_verb), + enabled: Boolean = true, + trailingContent: @Composable (() -> Unit)? = null, + reducedCloseButtonPadding: Dp = 0.dp, onValueChange: (String) -> Unit ) { val focusRequester = remember { FocusRequester() } @@ -47,52 +49,68 @@ fun SearchTextField( keyboard?.show() } } - - DisposableEffect(Unit) { - onDispose { - if (searchText.value.text.isNotEmpty()) onValueChange("") + if (appPlatform.isAndroid) { + LaunchedEffect(Unit) { + val modalCountOnOpen = ModalManager.start.modalCount.value + launch { + snapshotFlow { ModalManager.start.modalCount.value } + .filter { it > modalCountOnOpen } + .collect { + keyboard?.hide() + } + } + } + KeyChangeEffect(chatModel.chatId.value) { + if (chatModel.chatId.value != null) { + // Delay is needed here because when ChatView is being opened and keyboard is hiding, bottom sheet (to choose attachment) is visible on a screen + delay(300) + keyboard?.hide() + } } } - val enabled = true val colors = TextFieldDefaults.textFieldColors( backgroundColor = Color.Unspecified, textColor = MaterialTheme.colors.onBackground, focusedIndicatorColor = Color.Unspecified, unfocusedIndicatorColor = Color.Unspecified, + disabledIndicatorColor = Color.Unspecified, + placeholderColor = MaterialTheme.colors.secondary, ) val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize) val interactionSource = remember { MutableInteractionSource() } + val textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) + // sizing is done differently on Android and desktop in order to have the same height of search and compose view on desktop + // see PlatformTextField.desktop + SendMsgView + val padding = if (appPlatform.isAndroid) PaddingValues() else PaddingValues(top = 3.dp, bottom = 4.dp) BasicTextField( value = searchText.value, modifier = modifier .background(colors.backgroundColor(enabled).value, shape) .indicatorLine(enabled, false, interactionSource, colors) .focusRequester(focusRequester) + .padding(padding) .defaultMinSize( minWidth = TextFieldDefaults.MinWidth, - minHeight = TextFieldDefaults.MinHeight + minHeight = if (appPlatform.isAndroid) TextFieldDefaults.MinHeight else 0.dp ), onValueChange = { searchText.value = it onValueChange(it.text) }, + enabled = rememberUpdatedState(enabled).value, cursorBrush = SolidColor(colors.cursorColor(false).value), visualTransformation = VisualTransformation.None, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), singleLine = true, - textStyle = TextStyle( - color = MaterialTheme.colors.onBackground, - fontWeight = FontWeight.Normal, - fontSize = 16.sp - ), + textStyle = textStyle, interactionSource = interactionSource, decorationBox = @Composable { innerTextField -> TextFieldDefaults.TextFieldDecorationBox( value = searchText.value.text, innerTextField = innerTextField, placeholder = { - Text(placeholder) + Text(placeholder, style = textStyle.copy(color = MaterialTheme.colors.secondary), maxLines = 1, overflow = TextOverflow.Ellipsis) }, trailingIcon = if (searchText.value.text.isNotEmpty()) {{ IconButton({ @@ -102,10 +120,10 @@ fun SearchTextField( } searchText.value = TextFieldValue(""); onValueChange("") - }) { + }, Modifier.offset(x = reducedCloseButtonPadding)) { Icon(painterResource(MR.images.ic_close), stringResource(MR.strings.icon_descr_close_button), tint = MaterialTheme.colors.primary,) } - }} else null, + }} else trailingContent, singleLine = true, enabled = enabled, interactionSource = interactionSource, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt index ba0edb98d1..0d188bb73c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt @@ -1,6 +1,5 @@ import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -17,19 +16,20 @@ import chat.simplex.common.platform.onRightClick import chat.simplex.common.platform.windowWidth import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.SelectableCard import chat.simplex.common.views.usersettings.SettingsActionItemWithContent import chat.simplex.res.MR @Composable -fun SectionView(title: String? = null, padding: PaddingValues = PaddingValues(), content: (@Composable ColumnScope.() -> Unit)) { +fun SectionView(title: String? = null, contentPadding: PaddingValues = PaddingValues(), headerBottomPadding: Dp = DEFAULT_PADDING, content: (@Composable ColumnScope.() -> Unit)) { Column { if (title != null) { Text( title, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, - modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), fontSize = 12.sp + modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = headerBottomPadding), fontSize = 12.sp ) } - Column(Modifier.padding(padding).fillMaxWidth()) { content() } + Column(Modifier.padding(contentPadding).fillMaxWidth()) { content() } } } @@ -53,6 +53,24 @@ fun SectionView( } } +@Composable +fun SectionViewWithButton(title: String? = null, titleButton: (@Composable () -> Unit)?, contentPadding: PaddingValues = PaddingValues(), headerBottomPadding: Dp = DEFAULT_PADDING, content: (@Composable ColumnScope.() -> Unit)) { + Column { + if (title != null || titleButton != null) { + Row(modifier = Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = headerBottomPadding).fillMaxWidth()) { + if (title != null) { + Text(title, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, fontSize = 12.sp) + } + if (titleButton != null) { + Spacer(modifier = Modifier.weight(1f)) + titleButton() + } + } + } + Column(Modifier.padding(contentPadding).fillMaxWidth()) { content() } + } +} + @Composable fun <T> SectionViewSelectable( title: String?, @@ -61,9 +79,8 @@ fun <T> SectionViewSelectable( onSelected: (T) -> Unit, ) { SectionView(title) { - LazyColumn { - items(values.size) { index -> - val item = values[index] + Column { + values.forEach { item -> SectionItemViewSpaceBetween({ onSelected(item.value) }) { Text(item.title) if (currentValue.value == item.value) { @@ -77,16 +94,36 @@ fun <T> SectionViewSelectable( SectionTextFooter(values.first { it.value == currentValue.value }.description) } +@Composable +fun <T> SectionViewSelectableCards( + title: String?, + currentValue: State<T>, + values: List<ValueTitleDesc<T>>, + onSelected: (T) -> Unit, +) { + SectionView(title) { + Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { + if (title != null) { + Text(title, Modifier.fillMaxWidth(), textAlign = TextAlign.Center) + Spacer(Modifier.height(DEFAULT_PADDING * 2f)) + } + values.forEach { item -> + SelectableCard(currentValue, item.value, item.title, item.description, onSelected) + } + } + } +} + @Composable fun SectionItemView( click: (() -> Unit)? = null, - minHeight: Dp = 46.dp, + minHeight: Dp = DEFAULT_MIN_SECTION_ITEM_HEIGHT, disabled: Boolean = false, extraPadding: Boolean = false, padding: PaddingValues = if (extraPadding) - PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING) + PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING, top = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL, bottom = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL) else - PaddingValues(horizontal = DEFAULT_PADDING), + PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL), content: (@Composable RowScope.() -> Unit) ) { val modifier = Modifier @@ -101,10 +138,9 @@ fun SectionItemView( } @Composable -fun SectionItemViewLongClickable( - click: () -> Unit, - longClick: () -> Unit, - minHeight: Dp = 46.dp, +fun SectionItemViewWithoutMinPadding( + click: (() -> Unit)? = null, + minHeight: Dp = DEFAULT_MIN_SECTION_ITEM_HEIGHT, disabled: Boolean = false, extraPadding: Boolean = false, padding: PaddingValues = if (extraPadding) @@ -112,6 +148,22 @@ fun SectionItemViewLongClickable( else PaddingValues(horizontal = DEFAULT_PADDING), content: (@Composable RowScope.() -> Unit) +) { + SectionItemView(click, minHeight, disabled, extraPadding, padding, content) +} + +@Composable +fun SectionItemViewLongClickable( + click: () -> Unit, + longClick: () -> Unit, + minHeight: Dp = DEFAULT_MIN_SECTION_ITEM_HEIGHT, + disabled: Boolean = false, + extraPadding: Boolean = false, + padding: PaddingValues = if (extraPadding) + PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING, top = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL, bottom = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL) + else + PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL), + content: (@Composable RowScope.() -> Unit) ) { val modifier = Modifier .fillMaxWidth() @@ -128,30 +180,11 @@ fun SectionItemViewLongClickable( } } -@Composable -fun SectionItemViewWithIcon( - click: (() -> Unit)? = null, - minHeight: Dp = 46.dp, - disabled: Boolean = false, - padding: PaddingValues = PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING), - content: (@Composable RowScope.() -> Unit) -) { - val modifier = Modifier - .fillMaxWidth() - .sizeIn(minHeight = minHeight) - Row( - if (click == null || disabled) modifier.padding(padding) else modifier.clickable(onClick = click).padding(padding), - verticalAlignment = Alignment.CenterVertically - ) { - content() - } -} - @Composable fun SectionItemViewSpaceBetween( click: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null, - minHeight: Dp = 46.dp, + minHeight: Dp = DEFAULT_MIN_SECTION_ITEM_HEIGHT, padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING), disabled: Boolean = false, content: (@Composable RowScope.() -> Unit) @@ -160,7 +193,7 @@ fun SectionItemViewSpaceBetween( .fillMaxWidth() .sizeIn(minHeight = minHeight) Row( - if (click == null || disabled) modifier.padding(padding) else modifier + if (click == null || disabled) modifier.padding(padding).padding(vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL) else modifier .combinedClickable(onClick = click, onLongClick = onLongClick).padding(padding) .onRightClick { onLongClick?.invoke() }, horizontalArrangement = Arrangement.SpaceBetween, @@ -198,16 +231,16 @@ fun <T> SectionItemWithValue( } @Composable -fun SectionTextFooter(text: String) { - SectionTextFooter(AnnotatedString(text)) +fun SectionTextFooter(text: String, color: Color = MaterialTheme.colors.secondary) { + SectionTextFooter(AnnotatedString(text), color = color) } @Composable -fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start) { +fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start, color: Color = MaterialTheme.colors.secondary) { Text( text, Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F), - color = MaterialTheme.colors.secondary, + color = color, lineHeight = 18.sp, fontSize = 14.sp, textAlign = textAlign @@ -233,9 +266,9 @@ fun SectionDividerSpaced(maxTopPadding: Boolean = false, maxBottomPadding: Boole Divider( Modifier.padding( start = DEFAULT_PADDING_HALF, - top = if (maxTopPadding) 37.dp else 27.dp, + top = if (maxTopPadding) DEFAULT_PADDING + 18.dp else DEFAULT_PADDING + 2.dp, end = DEFAULT_PADDING_HALF, - bottom = if (maxBottomPadding) 37.dp else 27.dp) + bottom = if (maxBottomPadding) DEFAULT_PADDING + 18.dp else DEFAULT_PADDING + 2.dp) ) } @@ -255,17 +288,72 @@ fun TextIconSpaced(extraPadding: Boolean = false) { } @Composable -fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color? = null) { - SectionItemViewSpaceBetween { +fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color? = null, textColor: Color = MaterialTheme.colors.onBackground, padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING)) { + SectionItemViewSpaceBetween(padding = padding) { Row { val iconSize = with(LocalDensity.current) { 21.sp.toDp() } if (icon != null) Icon(icon, title, Modifier.padding(end = 8.dp).size(iconSize), tint = iconTint ?: MaterialTheme.colors.secondary) - Text(title) + Text(title, color = textColor) } Text(value, color = MaterialTheme.colors.secondary) } } +fun numOrDash(n: Number): String = if (n.toLong() == 0L) "-" else n.toString() + +@Composable +fun InfoRowTwoValues( + title: String, + title2: String, + value: Int, + value2: Int, + textColor: Color = MaterialTheme.colors.onBackground +) { + SectionItemViewSpaceBetween { + Row( + verticalAlignment = Alignment.Bottom + ) { + Text( + text = title, + color = textColor, + ) + Text( + text = " / ", + fontSize = 12.sp, + ) + Text( + text = title2, + color = textColor, + fontSize = 12.sp, + ) + } + Row(verticalAlignment = Alignment.Bottom) { + if (value == 0 && value2 == 0) { + Text( + text = "-", + color = MaterialTheme.colors.secondary + ) + } else { + Text( + text = numOrDash(value), + color = MaterialTheme.colors.secondary, + ) + Text( + text = " / ", + color = MaterialTheme.colors.secondary, + fontSize = 12.sp, + ) + Text( + text = numOrDash(value2), + color = MaterialTheme.colors.secondary, + fontSize = 12.sp, + ) + } + } + } +} + + @Composable fun InfoRowEllipsis(title: String, value: String, onClick: () -> Unit) { SectionItemViewSpaceBetween(onClick) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SimpleButton.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SimpleButton.kt index 7db001a4bd..1c4879dedd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SimpleButton.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SimpleButton.kt @@ -1,8 +1,10 @@ package chat.simplex.common.views.helpers import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import dev.icerock.moko.resources.compose.painterResource @@ -16,6 +18,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp +import chat.simplex.common.platform.appPlatform import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.res.MR diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SubscriptionStatusIcon.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SubscriptionStatusIcon.kt new file mode 100644 index 0000000000..e0e61b598e --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SubscriptionStatusIcon.kt @@ -0,0 +1,41 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.Box +import androidx.compose.material.Icon +import androidx.compose.ui.graphics.Color +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource + +@Composable +fun SubscriptionStatusIcon( + color: Color, + variableValue: Float, + modifier: Modifier = Modifier +) { + @Composable + fun ZeroIcon() { + Icon(painterResource(MR.images.ic_radiowaves_up_forward_4_bar), null, tint = color.copy(alpha = 0.33f), modifier = modifier) + } + + when { + variableValue <= 0f -> ZeroIcon() + variableValue > 0f && variableValue <= 0.25f -> Box { + ZeroIcon() + Icon(painterResource(MR.images.ic_radiowaves_up_forward_1_bar), null, tint = color, modifier = modifier) + } + + variableValue > 0.25f && variableValue <= 0.5f -> Box { + ZeroIcon() + Icon(painterResource(MR.images.ic_radiowaves_up_forward_2_bar), null, tint = color, modifier = modifier) + } + + variableValue > 0.5f && variableValue <= 0.75f -> Box { + ZeroIcon() + Icon(painterResource(MR.images.ic_radiowaves_up_forward_3_bar), null, tint = color, modifier = modifier) + } + + else -> Icon(painterResource(MR.images.ic_radiowaves_up_forward_4_bar), null, tint = color, modifier = modifier) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt index 45accccc59..da16e2b7e7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt @@ -57,7 +57,6 @@ fun TextEditor( ) { val textFieldModifier = modifier .fillMaxWidth() - .navigationBarsWithImePadding() .onFocusChanged { focused = it.isFocused } .padding(10.dp) @@ -87,6 +86,7 @@ fun TextEditor( enabled = true, isError = false, interactionSource = remember { MutableInteractionSource() }, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified) ) } ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt new file mode 100644 index 0000000000..d7cdf0e2e3 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt @@ -0,0 +1,516 @@ +package chat.simplex.common.views.helpers + +import SectionBottomSpacer +import SectionDividerSpaced +import SectionItemView +import SectionSpacer +import SectionView +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.MaterialTheme.colors +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.yaml +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.usersettings.* +import chat.simplex.common.views.usersettings.AppearanceScope.WallpaperPresetSelector +import chat.simplex.common.views.usersettings.AppearanceScope.editColor +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.serialization.encodeToString +import java.net.URI + +@Composable +fun ModalData.UserWallpaperEditor( + theme: ThemeModeOverride, + applyToMode: DefaultThemeMode?, + globalThemeUsed: MutableState<Boolean>, + save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit +) { + ColumnWithScrollBar { + val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } } + var showMore by remember { stateGetOrPut("showMore") { false } } + val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } } + val currentTheme by CurrentColors.collectAsState() + + AppBarTitle(stringResource(MR.strings.settings_section_title_user_theme)) + val wallpaperImage = MaterialTheme.wallpaper.type.image + val wallpaperType = MaterialTheme.wallpaper.type + + val onTypeCopyFromSameTheme = { type: WallpaperType? -> + if (type is WallpaperType.Image && chatModel.remoteHostId() != null) { + false + } else { + ThemeManager.copyFromSameThemeOverrides(type, null, themeModeOverride) + withBGApi { save(applyToMode.value, themeModeOverride.value) } + globalThemeUsed.value = false + true + } + } + val preApplyGlobalIfNeeded = { type: WallpaperType? -> + if (globalThemeUsed.value) { + onTypeCopyFromSameTheme(type) + } + } + val onTypeChange: (WallpaperType?) -> Unit = { type: WallpaperType? -> + if (globalThemeUsed.value) { + preApplyGlobalIfNeeded(type) + // Saves copied static image instead of original from global theme + ThemeManager.applyWallpaper(themeModeOverride.value.type, themeModeOverride) + } else { + ThemeManager.applyWallpaper(type, themeModeOverride) + } + withBGApi { save(applyToMode.value, themeModeOverride.value) } + } + + val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) { + val filename = saveWallpaperFile(to) + if (filename != null) { + onTypeChange(WallpaperType.Image(filename, 1f, WallpaperScaleType.FILL)) + } + } + } + + val currentColors = { type: WallpaperType? -> + // If applying for : + // - all themes: no overrides needed + // - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected + val perUserOverride = if (wallpaperType.sameType(type)) chatModel.currentUser.value?.uiThemes else null + ThemeManager.currentColors(type, null, perUserOverride, appPrefs.themeOverrides.get()) + } + val onChooseType: (WallpaperType?) -> Unit = { type: WallpaperType? -> + when { + // don't have image in parent or already selected wallpaper with custom image + type is WallpaperType.Image && chatModel.remoteHostId() != null -> { /* do nothing */ } + type is WallpaperType.Image && (wallpaperType is WallpaperType.Image || currentColors(type).wallpaper.type.image == null) -> withLongRunningApi { importWallpaperLauncher.launch("image/*") } + type is WallpaperType.Image -> onTypeCopyFromSameTheme(currentColors(type).wallpaper.type) + themeModeOverride.value.type != type || currentTheme.wallpaper.type != type -> onTypeCopyFromSameTheme(type) + else -> onTypeChange(type) + } + } + + val editColor = { name: ThemeColor -> + editColor( + name, + wallpaperType, + wallpaperImage, + onColorChange = { color -> + preApplyGlobalIfNeeded(themeModeOverride.value.type) + ThemeManager.applyThemeColor(name, color, themeModeOverride) + withBGApi { save(applyToMode.value, themeModeOverride.value) } + } + ) + } + + WallpaperPresetSelector( + selectedWallpaper = wallpaperType, + baseTheme = currentTheme.base, + currentColors = { type -> + // If applying for : + // - all themes: no overrides needed + // - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected + val perUserOverride = if (wallpaperType.sameType(type)) chatModel.currentUser.value?.uiThemes else null + ThemeManager.currentColors(type, null, perUserOverride, appPrefs.themeOverrides.get()) + }, + onChooseType = onChooseType + ) + + WallpaperSetupView( + themeModeOverride.value.type, + CurrentColors.collectAsState().value.base, + currentTheme.wallpaper, + currentTheme.appColors.sentMessage, + currentTheme.appColors.sentQuote, + currentTheme.appColors.receivedMessage, + currentTheme.appColors.receivedQuote, + editColor = { name -> editColor(name) }, + onTypeChange = onTypeChange, + ) + + SectionSpacer() + + if (!globalThemeUsed.value) { + ResetToGlobalThemeButton(true) { + themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + globalThemeUsed.value = true + withBGApi { save(applyToMode.value, null) } + } + } + + SetDefaultThemeButton { + globalThemeUsed.value = false + val lightBase = DefaultTheme.LIGHT + val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX + val mode = themeModeOverride.value.mode + withBGApi { + // Saving for both modes in one place by changing mode once per save + if (applyToMode.value == null) { + val oppositeMode = if (mode == DefaultThemeMode.LIGHT) DefaultThemeMode.DARK else DefaultThemeMode.LIGHT + save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, if (oppositeMode == DefaultThemeMode.LIGHT) lightBase else darkBase)) + } + themeModeOverride.value = ThemeModeOverride.withFilledAppDefaults(mode, if (mode == DefaultThemeMode.LIGHT) lightBase else darkBase) + save(themeModeOverride.value.mode, themeModeOverride.value) + } + } + + KeyChangeEffect(theme.mode) { + themeModeOverride.value = theme + if (applyToMode.value != null) { + applyToMode.value = theme.mode + } + } + + // Applies updated global theme if current one tracks global theme + KeyChangeEffect(CurrentColors.collectAsState().value) { + if (globalThemeUsed.value) { + themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + globalThemeUsed.value = true + } + } + + SectionSpacer() + + if (showMore) { + val values by remember { mutableStateOf( + listOf( + null to generalGetString(MR.strings.chat_theme_apply_to_all_modes), + DefaultThemeMode.LIGHT to generalGetString(MR.strings.chat_theme_apply_to_light_mode), + DefaultThemeMode.DARK to generalGetString(MR.strings.chat_theme_apply_to_dark_mode), + ) + ) + } + ExposedDropDownSettingRow( + generalGetString(MR.strings.chat_theme_apply_to_mode), + values, + applyToMode, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = { + applyToMode.value = it + if (it != null && it != CurrentColors.value.base.mode) { + val lightBase = DefaultTheme.LIGHT + val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX + ThemeManager.applyTheme(if (it == DefaultThemeMode.LIGHT) lightBase.themeName else darkBase.themeName) + } + } + ) + + SectionDividerSpaced() + + AppearanceScope.CustomizeThemeColorsSection(currentTheme, editColor = editColor) + + SectionDividerSpaced(maxBottomPadding = false) + + ImportExportThemeSection(null, remember { chatModel.currentUser }.value?.uiThemes) { + withBGApi { + themeModeOverride.value = it + save(applyToMode.value, it) + } + } + } else { + AdvancedSettingsButton { showMore = true } + } + + SectionBottomSpacer() + } +} + +@Composable +fun ModalData.ChatWallpaperEditor( + theme: ThemeModeOverride, + applyToMode: DefaultThemeMode?, + globalThemeUsed: MutableState<Boolean>, + save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit +) { + ColumnWithScrollBar { + val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } } + var showMore by remember { stateGetOrPut("showMore") { false } } + val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } } + val currentTheme by remember(themeModeOverride.value, CurrentColors.collectAsState().value) { + mutableStateOf( + ThemeManager.currentColors(null, if (globalThemeUsed.value) null else themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get()) + ) + } + + AppBarTitle(stringResource(MR.strings.settings_section_title_chat_theme)) + + val onTypeCopyFromSameTheme: (WallpaperType?) -> Boolean = { type -> + if (type is WallpaperType.Image && chatModel.remoteHostId() != null) { + false + } else { + val success = ThemeManager.copyFromSameThemeOverrides(type, chatModel.currentUser.value?.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight), themeModeOverride) + if (success) { + withBGApi { save(applyToMode.value, themeModeOverride.value) } + globalThemeUsed.value = false + } + success + } + } + val preApplyGlobalIfNeeded = { type: WallpaperType? -> + if (globalThemeUsed.value) { + onTypeCopyFromSameTheme(type) + } + } + val onTypeChange: (WallpaperType?) -> Unit = { type -> + if (globalThemeUsed.value) { + preApplyGlobalIfNeeded(type) + // Saves copied static image instead of original from global theme + ThemeManager.applyWallpaper(themeModeOverride.value.type, themeModeOverride) + } else { + ThemeManager.applyWallpaper(type, themeModeOverride) + } + withBGApi { save(applyToMode.value, themeModeOverride.value) } + } + + val editColor: (ThemeColor) -> Unit = { name: ThemeColor -> + ModalManager.end.showModal { + val currentTheme by remember(themeModeOverride.value, CurrentColors.collectAsState().value) { + mutableStateOf( + ThemeManager.currentColors(null, themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get()) + ) + } + val initialColor: Color = when (name) { + ThemeColor.WALLPAPER_BACKGROUND -> currentTheme.wallpaper.background ?: Color.Transparent + ThemeColor.WALLPAPER_TINT -> currentTheme.wallpaper.tint ?: Color.Transparent + ThemeColor.PRIMARY -> currentTheme.colors.primary + ThemeColor.PRIMARY_VARIANT -> currentTheme.colors.primaryVariant + ThemeColor.SECONDARY -> currentTheme.colors.secondary + ThemeColor.SECONDARY_VARIANT -> currentTheme.colors.secondaryVariant + ThemeColor.BACKGROUND -> currentTheme.colors.background + ThemeColor.SURFACE -> currentTheme.colors.surface + ThemeColor.TITLE -> currentTheme.appColors.title + ThemeColor.PRIMARY_VARIANT2 -> currentTheme.appColors.primaryVariant2 + ThemeColor.SENT_MESSAGE -> currentTheme.appColors.sentMessage + ThemeColor.SENT_QUOTE -> currentTheme.appColors.sentQuote + ThemeColor.RECEIVED_MESSAGE -> currentTheme.appColors.receivedMessage + ThemeColor.RECEIVED_QUOTE -> currentTheme.appColors.receivedQuote + } + AppearanceScope.ColorEditor( + name, + initialColor, + CurrentColors.collectAsState().value.base, + themeModeOverride.value.type, + themeModeOverride.value.type?.image, + currentTheme.wallpaper.background, + currentTheme.wallpaper.tint, + currentColors = { + ThemeManager.currentColors(null, themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get()) + }, + onColorChange = { color -> + preApplyGlobalIfNeeded(themeModeOverride.value.type) + ThemeManager.applyThemeColor(name, color, themeModeOverride) + withBGApi { save(applyToMode.value, themeModeOverride.value) } + } + ) + } + } + + val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) { + val filename = saveWallpaperFile(to) + if (filename != null) { + // Delete only non-user image + if (!globalThemeUsed.value) { + removeWallpaperFile((themeModeOverride.value.type as? WallpaperType.Image)?.filename) + } + globalThemeUsed.value = false + onTypeChange(WallpaperType.Image(filename, 1f, WallpaperScaleType.FILL)) + } + } + } + + val currentColors = { type: WallpaperType? -> + ThemeManager.currentColors(type, if (type?.sameType(themeModeOverride.value.type) == true) themeModeOverride.value else null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + } + + WallpaperPresetSelector( + selectedWallpaper = currentTheme.wallpaper.type, + activeBackgroundColor = currentTheme.wallpaper.background, + activeTintColor = currentTheme.wallpaper.tint, + baseTheme = CurrentColors.collectAsState().value.base, + currentColors = { type -> currentColors(type) }, + onChooseType = { type -> + when { + type is WallpaperType.Image && chatModel.remoteHostId() != null -> { /* do nothing */ } + type is WallpaperType.Image && ((themeModeOverride.value.type is WallpaperType.Image && !globalThemeUsed.value) || currentColors(type).wallpaper.type.image == null) -> { + withLongRunningApi { importWallpaperLauncher.launch("image/*") } + } + type is WallpaperType.Image -> { + if (!onTypeCopyFromSameTheme(currentColors(type).wallpaper.type)) { + withLongRunningApi { importWallpaperLauncher.launch("image/*") } + } + } + globalThemeUsed.value || themeModeOverride.value.type != type -> { + onTypeCopyFromSameTheme(type) + } + else -> { + onTypeChange(type) + } + } + }, + ) + + WallpaperSetupView( + themeModeOverride.value.type, + CurrentColors.collectAsState().value.base, + currentTheme.wallpaper, + currentTheme.appColors.sentMessage, + currentTheme.appColors.sentQuote, + currentTheme.appColors.receivedMessage, + currentTheme.appColors.receivedQuote, + editColor = editColor, + onTypeChange = onTypeChange, + ) + + SectionSpacer() + + if (!globalThemeUsed.value) { + ResetToGlobalThemeButton(remember { chatModel.currentUser }.value?.uiThemes?.preferredMode(isInDarkTheme()) == null) { + themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + globalThemeUsed.value = true + withBGApi { save(applyToMode.value, null) } + } + } + + SetDefaultThemeButton { + globalThemeUsed.value = false + val lightBase = DefaultTheme.LIGHT + val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX + val mode = themeModeOverride.value.mode + withBGApi { + // Saving for both modes in one place by changing mode once per save + if (applyToMode.value == null) { + val oppositeMode = if (mode == DefaultThemeMode.LIGHT) DefaultThemeMode.DARK else DefaultThemeMode.LIGHT + save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, if (oppositeMode == DefaultThemeMode.LIGHT) lightBase else darkBase)) + } + themeModeOverride.value = ThemeModeOverride.withFilledAppDefaults(mode, if (mode == DefaultThemeMode.LIGHT) lightBase else darkBase) + save(themeModeOverride.value.mode, themeModeOverride.value) + } + } + + KeyChangeEffect(theme.mode) { + themeModeOverride.value = theme + if (applyToMode.value != null) { + applyToMode.value = theme.mode + } + } + + // Applies updated global theme if current one tracks global theme + KeyChangeEffect(CurrentColors.collectAsState()) { + if (globalThemeUsed.value) { + themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + globalThemeUsed.value = true + } + } + + SectionSpacer() + + if (showMore) { + val values by remember { mutableStateOf( + listOf( + null to generalGetString(MR.strings.chat_theme_apply_to_all_modes), + DefaultThemeMode.LIGHT to generalGetString(MR.strings.chat_theme_apply_to_light_mode), + DefaultThemeMode.DARK to generalGetString(MR.strings.chat_theme_apply_to_dark_mode), + ) + ) + } + ExposedDropDownSettingRow( + generalGetString(MR.strings.chat_theme_apply_to_mode), + values, + applyToMode, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = { + applyToMode.value = it + if (it != null && it != CurrentColors.value.base.mode) { + val lightBase = DefaultTheme.LIGHT + val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX + ThemeManager.applyTheme(if (it == DefaultThemeMode.LIGHT) lightBase.themeName else darkBase.themeName) + } + } + ) + + SectionDividerSpaced() + + AppearanceScope.CustomizeThemeColorsSection(currentTheme, editColor = editColor) + + SectionDividerSpaced(maxBottomPadding = false) + ImportExportThemeSection(themeModeOverride.value, remember { chatModel.currentUser }.value?.uiThemes) { + withBGApi { + themeModeOverride.value = it + save(applyToMode.value, it) + } + } + } else { + AdvancedSettingsButton { showMore = true } + } + + SectionBottomSpacer() + } +} + +@Composable +private fun ImportExportThemeSection(perChat: ThemeModeOverride?, perUser: ThemeModeOverrides?, save: (ThemeModeOverride) -> Unit) { + SectionView { + val theme = remember { mutableStateOf(null as String?) } + val exportThemeLauncher = rememberFileChooserLauncher(false) { to: URI? -> + val themeValue = theme.value + if (themeValue != null && to != null) { + copyBytesToFile(themeValue.byteInputStream(), to) { + theme.value = null + } + } + } + SectionItemView({ + val overrides = ThemeManager.currentThemeOverridesForExport(perChat, perUser) + val lines = yaml.encodeToString<ThemeOverrides>(overrides).lines() + // Removing theme id without using custom serializer or data class + theme.value = lines.subList(1, lines.size).joinToString("\n") + withLongRunningApi { exportThemeLauncher.launch("simplex.theme") } + }) { + Text(generalGetString(MR.strings.export_theme), color = colors.primary) + } + val importThemeLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) { + val theme = getThemeFromUri(to) + if (theme != null) { + val res = ThemeModeOverride(mode = theme.base.mode, colors = theme.colors, wallpaper = theme.wallpaper?.importFromString()).removeSameColors(theme.base) + save(res) + } + } + } + // Can not limit to YAML mime type since it's unsupported by Android + SectionItemView({ withLongRunningApi { importThemeLauncher.launch("*/*") } }) { + Text(generalGetString(MR.strings.import_theme), color = colors.primary) + } + } +} + +@Composable +private fun ResetToGlobalThemeButton(app: Boolean, onClick: () -> Unit) { + SectionItemView(onClick) { + Text(stringResource(if (app) MR.strings.chat_theme_reset_to_app_theme else MR.strings.chat_theme_reset_to_user_theme), color = MaterialTheme.colors.primary) + } +} + +@Composable +private fun SetDefaultThemeButton(onClick: () -> Unit) { + SectionItemView(onClick) { + Text(stringResource(MR.strings.chat_theme_set_default_theme), color = MaterialTheme.colors.primary) + } +} + +@Composable +private fun AdvancedSettingsButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_arrow_downward), + stringResource(MR.strings.wallpaper_advanced_settings), + click = onClick + ) +} 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 9a81b9f9d7..db1a0be9da 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 @@ -2,34 +2,84 @@ package chat.simplex.common.views.helpers import androidx.compose.runtime.* import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.graphics.* import androidx.compose.ui.platform.* import androidx.compose.ui.text.* import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.ThemeModeOverrides import chat.simplex.common.ui.theme.ThemeOverrides import chat.simplex.common.views.chatlist.connectIfOpenedViaUri import chat.simplex.res.MR import com.charleskorn.kaml.decodeFromStream import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.* -import kotlinx.serialization.decodeFromString +import kotlinx.coroutines.flow.* import kotlinx.serialization.encodeToString import java.io.* import java.net.URI import java.nio.file.Files +import java.nio.file.StandardCopyOption import java.text.SimpleDateFormat import java.util.* +import java.util.concurrent.Executors import kotlin.math.* -fun withApi(action: suspend CoroutineScope.() -> Unit): Job = withScope(GlobalScope, action) +private val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() -fun withScope(scope: CoroutineScope, action: suspend CoroutineScope.() -> Unit): Job = - scope.launch { withContext(Dispatchers.Main, action) } +fun withApi(action: suspend CoroutineScope.() -> Unit): Job = + Exception().let { + CoroutineScope(Dispatchers.Main).launch(block = { wrapWithLogging(action, it) }) + } fun withBGApi(action: suspend CoroutineScope.() -> Unit): Job = - CoroutineScope(Dispatchers.Default).launch(block = action) + Exception().let { + CoroutineScope(singleThreadDispatcher).launch(block = { wrapWithLogging(action, it) }) + } + +fun withLongRunningApi(slow: Long = Long.MAX_VALUE, action: suspend CoroutineScope.() -> Unit): Job = + Exception().let { + CoroutineScope(Dispatchers.Default).launch(block = { wrapWithLogging(action, it, slow = slow) }) + } + +private suspend fun wrapWithLogging(action: suspend CoroutineScope.() -> Unit, exception: java.lang.Exception, slow: Long = 20_000) = coroutineScope { + val start = System.currentTimeMillis() + action() + val end = System.currentTimeMillis() + if (end - start > slow) { + Log.e(TAG, "Possible problem with execution of the thread, took ${(end - start) / 1000}s:\n${exception.stackTraceToString()}") + if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.possible_slow_function_title), + text = generalGetString(MR.strings.possible_slow_function_desc).format((end - start) / 1000, exception.stackTraceToString()), + shareText = true + ) + } + } +} + +@OptIn(InternalCoroutinesApi::class) +suspend fun interruptIfCancelled() = coroutineScope { + if (!isActive) { + Log.d(TAG, "Coroutine was cancelled and interrupted: ${Exception().stackTraceToString()}") + throw coroutineContext.job.getCancellationException() + } +} + +/** + * This coroutine helper makes possible to cancel coroutine scope when a user goes back but not when the user rotates a screen + * */ +@Composable +fun ModalData.CancellableOnGoneJob(key: String = rememberSaveable { UUID.randomUUID().toString() }): MutableState<Job> { + val job = remember { stateGetOrPut<Job>(key) { Job() } } + DisposableEffectOnGone { + job.value.cancel() + } + return job +} enum class KeyboardState { Opened, Closed @@ -60,6 +110,9 @@ fun annotatedStringResource(id: StringResource, vararg args: Any?): AnnotatedStr } } +@Composable +expect fun SetupClipboardListener() + // maximum image file size to be auto-accepted const val MAX_IMAGE_SIZE: Long = 261_120 // 255KB const val MAX_IMAGE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2 @@ -72,6 +125,8 @@ const val MAX_FILE_SIZE_SMP: Long = 8000000 const val MAX_FILE_SIZE_XFTP: Long = 1_073_741_824 // 1GB +const val MAX_FILE_SIZE_LOCAL: Long = Long.MAX_VALUE + expect fun getAppFileUri(fileName: String): URI // https://developer.android.com/training/data-storage/shared/documents-files#bitmap @@ -94,6 +149,7 @@ fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverri runCatching { return yaml.decodeFromStream<ThemeOverrides>(it!!) }.onFailure { + Log.e(TAG, "Error while decoding theme: ${it.stackTraceToString()}") if (withAlertOnException) { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.import_theme_error), @@ -105,20 +161,27 @@ fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverri return null } -fun saveImage(uri: URI, encrypted: Boolean): CryptoFile? { +fun saveImage(uri: URI): CryptoFile? { val bitmap = getBitmapFromUri(uri) ?: return null - return saveImage(bitmap, encrypted) + return saveImage(bitmap) } -fun saveImage(image: ImageBitmap, encrypted: Boolean): CryptoFile? { +fun saveImage(image: ImageBitmap): CryptoFile? { return try { + val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get() val ext = if (image.hasAlpha()) "png" else "jpg" val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE) val destFileName = generateNewFileName("IMG", ext, File(getAppFilePath(""))) val destFile = File(getAppFilePath(destFileName)) if (encrypted) { - val args = writeCryptoFile(destFile.absolutePath, dataResized.toByteArray()) - CryptoFile(destFileName, args) + try { + val args = writeCryptoFile(destFile.absolutePath, dataResized.toByteArray()) + CryptoFile(destFileName, args) + } catch (e: Exception) { + Log.e(TAG, "Unable to write crypto file: " + e.stackTraceToString()) + AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString()) + null + } } else { val output = FileOutputStream(destFile) dataResized.writeTo(output) @@ -150,8 +213,9 @@ fun desktopSaveImageInTmp(uri: URI): CryptoFile? { } } -fun saveAnimImage(uri: URI, encrypted: Boolean): CryptoFile? { +fun saveAnimImage(uri: URI): CryptoFile? { return try { + val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get() val filename = getFileName(uri)?.lowercase() var ext = when { // remove everything but extension @@ -163,8 +227,14 @@ fun saveAnimImage(uri: URI, encrypted: Boolean): CryptoFile? { val destFileName = generateNewFileName("IMG", ext, File(getAppFilePath(""))) val destFile = File(getAppFilePath(destFileName)) if (encrypted) { - val args = writeCryptoFile(destFile.absolutePath, uri.inputStream()?.readBytes() ?: return null) - CryptoFile(destFileName, args) + try { + val args = writeCryptoFile(destFile.absolutePath, uri.inputStream()?.readBytes() ?: return null) + CryptoFile(destFileName, args) + } catch (e: Exception) { + Log.e(TAG, "Unable to read crypto file: " + e.stackTraceToString()) + AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString()) + null + } } else { Files.copy(uri.inputStream(), destFile.toPath()) CryptoFile.plain(destFileName) @@ -177,18 +247,38 @@ fun saveAnimImage(uri: URI, encrypted: Boolean): CryptoFile? { expect suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean): File? -fun saveFileFromUri(uri: URI, encrypted: Boolean, withAlertOnException: Boolean = true): CryptoFile? { +fun saveFileFromUri( + uri: URI, + withAlertOnException: Boolean = true, + hiddenFileNamePrefix: String? = null +): CryptoFile? { return try { + val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get() val inputStream = uri.inputStream() val fileToSave = getFileName(uri) return if (inputStream != null && fileToSave != null) { - val destFileName = uniqueCombine(fileToSave, File(getAppFilePath(""))) + val destFileName = if (hiddenFileNamePrefix == null) { + uniqueCombine(fileToSave, File(getAppFilePath(""))) + } else { + val ext = when { + // remove everything but extension + fileToSave.contains(".") -> fileToSave.substringAfterLast(".") + else -> null + } + generateNewFileName(hiddenFileNamePrefix, ext, File(getAppFilePath(""))) + } val destFile = File(getAppFilePath(destFileName)) if (encrypted) { createTmpFileAndDelete { tmpFile -> Files.copy(inputStream, tmpFile.toPath()) - val args = encryptCryptoFile(tmpFile.absolutePath, destFile.absolutePath) - CryptoFile(destFileName, args) + try { + val args = encryptCryptoFile(tmpFile.absolutePath, destFile.absolutePath) + CryptoFile(destFileName, args) + } catch (e: Exception) { + Log.e(TAG, "Unable to encrypt plain file: " + e.stackTraceToString()) + AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString()) + null + } } } else { Files.copy(inputStream, destFile.toPath()) @@ -208,8 +298,65 @@ fun saveFileFromUri(uri: URI, encrypted: Boolean, withAlertOnException: Boolean } } -fun <T> createTmpFileAndDelete(onCreated: (File) -> T): T { - val tmpFile = File(tmpDir, UUID.randomUUID().toString()) +fun saveWallpaperFile(uri: URI): String? { + val destFileName = generateNewFileName("wallpaper", "jpg", File(getWallpaperFilePath(""))) + val destFile = File(getWallpaperFilePath(destFileName)) + try { + val inputStream = uri.inputStream() + Files.copy(inputStream!!, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + } catch (e: Exception) { + Log.e(TAG, "Error saving wallpaper file: ${e.stackTraceToString()}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), e.stackTraceToString()) + return null + } + return destFile.name +} + +fun saveWallpaperFile(image: ImageBitmap): String { + val destFileName = generateNewFileName("wallpaper", "jpg", File(getWallpaperFilePath(""))) + val destFile = File(getWallpaperFilePath(destFileName)) + val dataResized = resizeImageToDataSize(image, false, maxDataSize = 5_000_000) + val output = FileOutputStream(destFile) + dataResized.use { + it.writeTo(output) + } + return destFile.name +} + +fun removeWallpaperFile(fileName: String? = null) { + File(getWallpaperFilePath("_")).parentFile.listFiles()?.forEach { + if (it.name == fileName) it.delete() + } + WallpaperType.cachedImages.remove(fileName) +} + +fun removeWallpaperFilesFromTheme(theme: ThemeModeOverrides?) { + if (theme != null) { + removeWallpaperFile(theme.light?.wallpaper?.imageFile) + removeWallpaperFile(theme.dark?.wallpaper?.imageFile) + } +} + +fun removeWallpaperFilesFromChat(chat: Chat) { + if (chat.chatInfo is ChatInfo.Direct) { + removeWallpaperFilesFromTheme(chat.chatInfo.contact.uiThemes) + } else if (chat.chatInfo is ChatInfo.Group) { + removeWallpaperFilesFromTheme(chat.chatInfo.groupInfo.uiThemes) + } +} + +fun removeWallpaperFilesFromAllChats(user: User) { + // Currently, only removing everything from currently active user is supported. Inactive users are TODO + if (user.userId == chatModel.currentUser.value?.userId) { + chatModel.chats.value.forEach { + removeWallpaperFilesFromChat(it) + } + } +} + +fun <T> createTmpFileAndDelete(dir: File = tmpDir, onCreated: (File) -> T): T { + val tmpFile = File(dir, UUID.randomUUID().toString()) + tmpFile.parentFile.mkdirs() tmpFile.deleteOnExit() ChatModel.filesToDelete.add(tmpFile) try { @@ -219,11 +366,12 @@ fun <T> createTmpFileAndDelete(onCreated: (File) -> T): T { } } -fun generateNewFileName(prefix: String, ext: String, dir: File): String { +fun generateNewFileName(prefix: String, ext: String?, dir: File): String { val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US) sdf.timeZone = TimeZone.getTimeZone("GMT") val timestamp = sdf.format(Date()) - return uniqueCombine("${prefix}_$timestamp.$ext", dir) + val extension = if (ext != null) ".$ext" else "" + return uniqueCombine("${prefix}_$timestamp$extension", dir) } fun uniqueCombine(fileName: String, dir: File): String { @@ -294,6 +442,7 @@ fun getMaxFileSize(fileProtocol: FileProtocol): Long { return when (fileProtocol) { FileProtocol.XFTP -> MAX_FILE_SIZE_XFTP FileProtocol.SMP -> MAX_FILE_SIZE_SMP + FileProtocol.LOCAL -> MAX_FILE_SIZE_LOCAL } } @@ -355,7 +504,7 @@ expect fun ByteArray.toBase64StringForPassphrase(): String // Android's default implementation that was used before multiplatform, adds non-needed characters at the end of string // which can be bypassed by: -// fun String.toByteArrayFromBase64(): ByteArray = Base64.getDecoder().decode(this.trimEnd { it == '\n' || it == ' ' }) +// fun String.toByteArrayFromBase64(): ByteArray = Base64.getMimeDecoder().decode(this.trimEnd { it == '\n' || it == ' ' }) expect fun String.toByteArrayFromBase64ForPassphrase(): ByteArray val LongRange.Companion.saver @@ -371,12 +520,11 @@ inline fun <reified T> serializableSaver(): Saver<T, *> = Saver( ) fun UriHandler.openVerifiedSimplexUri(uri: String) { - val URI = try { URI.create(uri) } catch (e: Exception) { null } - if (URI != null) { - connectIfOpenedViaUri(chatModel.remoteHostId(), URI, ChatModel) - } + connectIfOpenedViaUri(chatModel.remoteHostId(), uri, ChatModel) } +fun uriCreateOrNull(uri: String) = try { URI.create(uri) } catch (e: Exception) { null } + fun UriHandler.openUriCatching(uri: String) { try { openUri(uri) @@ -412,6 +560,28 @@ fun includeMoreFailedComposables() { lastExecutedComposables.clear() } +val fontSizeMultiplier: Float + @Composable get() = remember { appPrefs.fontScale.state }.value + +val fontSizeSqrtMultiplier: Float + @Composable get() = sqrt(remember { appPrefs.fontScale.state }.value) + +val desktopDensityScaleMultiplier: Float + @Composable get() = if (appPlatform.isDesktop) remember { appPrefs.densityScale.state }.value else 1f + +@Composable +fun TextUnit.toDp(): Dp { + check(type == TextUnitType.Sp) { "Only Sp can convert to Px" } + return Dp(value * LocalDensity.current.fontScale) +} + +fun <T> Flow<T>.throttleLatest(delayMillis: Long): Flow<T> = this + .conflate() + .transform { + emit(it) + delay(delayMillis) + } + @Composable fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenGone: () -> Unit) { DisposableEffect(Unit) { @@ -419,8 +589,12 @@ fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {} val orientation = windowOrientation() onDispose { whenDispose() - if (orientation == windowOrientation()) { - whenGone() + withApi { + // It needs some delay before check orientation again because it can still be not updated to actual value + delay(300) + if (orientation == windowOrientation()) { + whenGone() + } } } } @@ -453,9 +627,11 @@ fun <T> KeyChangeEffect( var anyChange by remember { mutableStateOf(false) } LaunchedEffect(key1) { if (anyChange || key1 != prevKey) { - block(prevKey) + val prev = prevKey prevKey = key1 anyChange = true + // Call it as the last statement because the coroutine can be cancelled earlier + block(prev) } } } @@ -473,10 +649,33 @@ fun KeyChangeEffect( val initialKey = remember { key1 } val initialKey2 = remember { key2 } var anyChange by remember { mutableStateOf(false) } - LaunchedEffect(key1) { + LaunchedEffect(key1, key2) { if (anyChange || key1 != initialKey || key2 != initialKey2) { - block() anyChange = true + block() + } + } +} + +/** + * Runs the [block] only after initial value of the [key1], or [key2], or [key3] changes, not after initial launch + * */ +@Composable +@NonRestartableComposable +fun KeyChangeEffect( + key1: Any?, + key2: Any?, + key3: Any?, + block: suspend CoroutineScope.() -> Unit +) { + val initialKey = remember { key1 } + val initialKey2 = remember { key2 } + val initialKey3 = remember { key3 } + var anyChange by remember { mutableStateOf(false) } + LaunchedEffect(key1, key2, key3) { + if (anyChange || key1 != initialKey || key2 != initialKey2 || key3 != initialKey3) { + anyChange = true + block() } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt index 468dd8580e..65e1864935 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt @@ -1,32 +1,45 @@ package chat.simplex.common.views.localauth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.controller import dev.icerock.moko.resources.compose.stringResource -import chat.simplex.common.views.database.deleteChatAsync -import chat.simplex.common.views.database.stopChatAsync import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.DatabaseUtils.ksSelfDestructPassword import chat.simplex.common.views.helpers.DatabaseUtils.ksAppPassword import chat.simplex.common.views.onboarding.OnboardingStage -import chat.simplex.common.model.ChatModel -import chat.simplex.common.model.Profile import chat.simplex.common.platform.* +import chat.simplex.common.views.database.* import chat.simplex.res.MR +import kotlinx.coroutines.delay @Composable fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) { val passcode = rememberSaveable { mutableStateOf("") } - PasscodeView(passcode, authRequest.title ?: stringResource(MR.strings.la_enter_app_passcode), authRequest.reason, stringResource(MR.strings.submit_passcode), + val allowToReact = rememberSaveable { mutableStateOf(true) } + if (!allowToReact.value) { + BackHandler { + // do nothing until submit action finishes to prevent concurrent removing of storage + } + } + PasscodeView(passcode, authRequest.title ?: stringResource(MR.strings.la_enter_app_passcode), authRequest.reason, stringResource(MR.strings.submit_passcode), buttonsEnabled = allowToReact, submit = { val sdPassword = ksSelfDestructPassword.get() if (sdPassword == passcode.value && authRequest.selfDestruct) { + allowToReact.value = false deleteStorageAndRestart(m, sdPassword) { r -> authRequest.completed(r) } } else { - val r: LAResult = if (passcode.value == authRequest.password) LAResult.Success else LAResult.Error(generalGetString(MR.strings.incorrect_passcode)) + val r: LAResult = if (passcode.value == authRequest.password) { + if (authRequest.selfDestruct && sdPassword != null && controller.ctrl == -1L) { + initChatControllerOnStart() + } + LAResult.Success + } else { + LAResult.Error(generalGetString(MR.strings.incorrect_passcode)) + } authRequest.completed(r) } }, @@ -36,10 +49,25 @@ fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) { } private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (LAResult) -> Unit) { - withBGApi { + withLongRunningApi { try { - stopChatAsync(m) - deleteChatAsync(m) + /** Waiting until [initChatController] finishes */ + while (m.ctrlInitInProgress.value) { + delay(50) + } + if (m.chatRunning.value == true) { + stopChatAsync(m) + } + val ctrl = m.controller.ctrl + if (ctrl != null && ctrl != -1L) { + /** + * The following sequence can bring a user here: + * the user opened the app, entered app passcode, went to background, returned back, entered self-destruct code. + * In this case database should be closed to prevent possible situation when OS can deny database removal command + * */ + chatCloseStore(ctrl) + } + deleteChatDatabaseFilesAndState() ksAppPassword.set(password) ksSelfDestructPassword.remove() ntfManager.cancelAllNotifications() @@ -48,16 +76,9 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: ( val displayName = displayNamePref.get() selfDestructPref.set(false) displayNamePref.set(null) - m.chatDbChanged.value = true - m.chatDbStatus.value = null - try { - initChatController(startChat = true) - } catch (e: Exception) { - Log.d(TAG, "initializeChat ${e.stackTraceToString()}") - } - m.chatDbChanged.value = false + reinitChatController() if (m.currentUser.value != null) { - return@withBGApi + return@withLongRunningApi } var profile: Profile? = null if (!displayName.isNullOrEmpty()) { @@ -69,12 +90,24 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: ( if (createdUser != null) { m.controller.startChat(createdUser) } - ModalManager.fullscreen.closeModals() + ModalManager.closeAllModalsEverywhere() AlertManager.shared.hideAllAlerts() AlertManager.privacySensitive.hideAllAlerts() completed(LAResult.Success) } catch (e: Exception) { + Log.e(TAG, "Unable to delete storage: ${e.stackTraceToString()}") completed(LAResult.Error(generalGetString(MR.strings.incorrect_passcode))) } } } + +suspend fun reinitChatController() { + chatModel.chatDbChanged.value = true + chatModel.chatDbStatus.value = null + try { + initChatController() + } catch (e: Exception) { + Log.d(TAG, "initializeChat ${e.stackTraceToString()}") + } + chatModel.chatDbChanged.value = false +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt index 4784951ad0..9b25e9b5e0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt @@ -12,6 +12,7 @@ import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.unit.dp import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.views.chat.group.ProgressIndicator import chat.simplex.common.views.helpers.SimpleButton import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @@ -23,6 +24,7 @@ fun PasscodeView( reason: String? = null, submitLabel: String, submitEnabled: ((String) -> Boolean)? = null, + buttonsEnabled: State<Boolean> = remember { mutableStateOf(true) }, submit: () -> Unit, cancel: () -> Unit, ) { @@ -74,9 +76,9 @@ fun PasscodeView( } PasscodeEntry(passcode, true) Row(Modifier.heightIn(min = 70.dp), verticalAlignment = Alignment.CenterVertically) { - SimpleButton(generalGetString(MR.strings.cancel_verb), icon = painterResource(MR.images.ic_close), click = cancel) + SimpleButton(generalGetString(MR.strings.cancel_verb), icon = painterResource(MR.images.ic_close), disabled = !buttonsEnabled.value, click = cancel) Spacer(Modifier.size(20.dp)) - SimpleButton(submitLabel, icon = painterResource(MR.images.ic_done_filled), disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit) + SimpleButton(submitLabel, icon = painterResource(MR.images.ic_done_filled), disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4 || !buttonsEnabled.value, click = submit) } } } @@ -117,8 +119,8 @@ fun PasscodeView( Modifier.padding(start = 30.dp).height(s * 3), verticalArrangement = Arrangement.SpaceEvenly ) { - SimpleButton(generalGetString(MR.strings.cancel_verb), icon = painterResource(MR.images.ic_close), click = cancel) - SimpleButton(submitLabel, icon = painterResource(MR.images.ic_done_filled), disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit) + SimpleButton(generalGetString(MR.strings.cancel_verb), icon = painterResource(MR.images.ic_close), disabled = !buttonsEnabled.value, click = cancel) + SimpleButton(submitLabel, icon = painterResource(MR.images.ic_done_filled), disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4 || !buttonsEnabled.value, click = submit) } } } @@ -130,7 +132,12 @@ fun PasscodeView( } else { HorizontalLayout() } + if (!buttonsEnabled.value) { + ProgressIndicator() + } + val view = LocalMultiplatformView() LaunchedEffect(Unit) { + hideKeyboard(view, true) focusRequester.requestFocus() // Disallow to steal a focus by clicking on buttons or using Tab focusRequester.captureFocus() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/SetAppPasscodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/SetAppPasscodeView.kt index 18437dbf98..1d620c915f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/SetAppPasscodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/SetAppPasscodeView.kt @@ -5,12 +5,14 @@ import androidx.compose.runtime.saveable.rememberSaveable import chat.simplex.common.platform.BackHandler import chat.simplex.common.views.helpers.DatabaseUtils import chat.simplex.common.views.helpers.DatabaseUtils.ksAppPassword +import chat.simplex.common.views.helpers.DatabaseUtils.ksSelfDestructPassword import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR @Composable fun SetAppPasscodeView( passcodeKeychain: DatabaseUtils.KeyStoreItem = ksAppPassword, + prohibitedPasscodeKeychain: DatabaseUtils.KeyStoreItem = ksSelfDestructPassword, title: String = generalGetString(MR.strings.new_passcode), reason: String? = null, submit: () -> Unit, @@ -48,7 +50,9 @@ fun SetAppPasscodeView( } } } else { - SetPasswordView(title, generalGetString(MR.strings.save_verb)) { + SetPasswordView(title, generalGetString(MR.strings.save_verb), + // Do not allow to set app passcode == selfDestruct passcode + submitEnabled = { pwd -> pwd != prohibitedPasscodeKeychain.get() }) { enteredPassword = passcode.value passcode.value = "" confirming = true diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt new file mode 100644 index 0000000000..03542ca8af --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt @@ -0,0 +1,739 @@ +package chat.simplex.common.views.migration + +import SectionBottomSpacer +import SectionSpacer +import SectionTextFooter +import SectionView +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ChatController.getNetCfg +import chat.simplex.common.model.ChatController.startChat +import chat.simplex.common.model.ChatController.startChatWithTemporaryDatabase +import chat.simplex.common.model.ChatCtrl +import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.database.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.LinkTextView +import chat.simplex.common.views.newchat.SimpleXLinkQRCode +import chat.simplex.common.views.usersettings.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.* +import kotlinx.datetime.* +import kotlinx.serialization.* +import java.io.File +import java.net.URLEncoder +import kotlin.math.max + +@Serializable +data class MigrationFileLinkData( + val networkConfig: NetworkConfig?, +) { + @Serializable + data class NetworkConfig( + // Legacy. Remove in 2025 + @SerialName("socksProxy") + val legacySocksProxy: String?, + val networkProxy: NetworkProxy?, + val hostMode: HostMode?, + val requiredHostMode: Boolean? + ) { + fun hasProxyConfigured(): Boolean = networkProxy != null || legacySocksProxy != null || hostMode == HostMode.Onion + + fun transformToPlatformSupported(): NetworkConfig { + return if (hostMode != null && requiredHostMode != null) { + NetworkConfig( + legacySocksProxy = if (hostMode == HostMode.Onion) legacySocksProxy ?: NetCfg.proxyDefaults.socksProxy else legacySocksProxy, + networkProxy = if (hostMode == HostMode.Onion) networkProxy ?: NetworkProxy() else networkProxy, + hostMode = if (hostMode == HostMode.Onion) HostMode.OnionViaSocks else hostMode, + requiredHostMode = requiredHostMode + ) + } else this + } + } + + fun addToLink(link: String) = link + "&data=" + URLEncoder.encode(jsonShort.encodeToString(this), "UTF-8") + + companion object { + suspend fun readFromLink(link: String): MigrationFileLinkData? = + try { + // val data = link.substringAfter("&data=").substringBefore("&") + // json.decodeFromString(URLDecoder.decode(data, "UTF-8")) + controller.standaloneFileInfo(link) + } catch (e: Exception) { + null + } + } +} + + + +@Serializable +private sealed class MigrationFromState { + @Serializable object ChatStopInProgress: MigrationFromState() + @Serializable data class ChatStopFailed(val reason: String): MigrationFromState() + @Serializable object PassphraseNotSet: MigrationFromState() + @Serializable object PassphraseConfirmation: MigrationFromState() + @Serializable object UploadConfirmation: MigrationFromState() + @Serializable object Archiving: MigrationFromState() + @Serializable data class DatabaseInit(val totalBytes: Long, val archivePath: String): MigrationFromState() + @Serializable data class UploadProgress(val uploadedBytes: Long, val totalBytes: Long, val fileId: Long, val archivePath: String, val ctrl: ChatCtrl, val user: User): MigrationFromState() + @Serializable data class UploadFailed(val totalBytes: Long, val archivePath: String): MigrationFromState() + @Serializable object LinkCreation: MigrationFromState() + @Serializable data class LinkShown(val fileId: Long, val link: String, val ctrl: ChatCtrl): MigrationFromState() + @Serializable data class Finished(val chatDeletion: Boolean): MigrationFromState() +} + +private var MutableState<MigrationFromState>.state: MigrationFromState + get() = value + set(v) { value = v } + +@Composable +fun MigrateFromDeviceView(close: () -> Unit) { + val migrationState = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf<MigrationFromState>(MigrationFromState.ChatStopInProgress) } + // Prevent from hiding the view until migration is finished or app deleted + val backDisabled = remember { + derivedStateOf { + when (migrationState.value) { + is MigrationFromState.ChatStopInProgress, + is MigrationFromState.DatabaseInit, + is MigrationFromState.Archiving, + is MigrationFromState.LinkShown, + is MigrationFromState.Finished -> true + + is MigrationFromState.ChatStopFailed, + is MigrationFromState.PassphraseNotSet, + is MigrationFromState.PassphraseConfirmation, + is MigrationFromState.UploadConfirmation, + is MigrationFromState.UploadProgress, + is MigrationFromState.UploadFailed, + is MigrationFromState.LinkCreation -> false + } + } + } + val chatReceiver = remember { mutableStateOf(null as MigrationFromChatReceiver?) } + ModalView( + enableClose = !backDisabled.value, + close = { + withBGApi { + migrationState.cleanUpOnBack(chatReceiver.value) + } + close() + }, + ) { + MigrateFromDeviceLayout( + migrationState = migrationState, + chatReceiver = chatReceiver + ) + } +} + +@Composable +private fun MigrateFromDeviceLayout( + migrationState: MutableState<MigrationFromState>, + chatReceiver: MutableState<MigrationFromChatReceiver?> +) { + val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) } + + ColumnWithScrollBar(maxIntrinsicSize = true) { + AppBarTitle(stringResource(MR.strings.migrate_from_device_title)) + SectionByState(migrationState, tempDatabaseFile.value, chatReceiver) + SectionBottomSpacer() + } + platform.androidLockPortraitOrientation() +} + +@Composable +private fun SectionByState( + migrationState: MutableState<MigrationFromState>, + tempDatabaseFile: File, + chatReceiver: MutableState<MigrationFromChatReceiver?> +) { + when (val s = migrationState.value) { + is MigrationFromState.ChatStopInProgress -> migrationState.ChatStopInProgressView() + is MigrationFromState.ChatStopFailed -> migrationState.ChatStopFailedView(s.reason) + is MigrationFromState.PassphraseNotSet -> migrationState.PassphraseNotSetView() + is MigrationFromState.PassphraseConfirmation -> migrationState.PassphraseConfirmationView() + is MigrationFromState.UploadConfirmation -> migrationState.UploadConfirmationView() + is MigrationFromState.Archiving -> migrationState.ArchivingView() + is MigrationFromState.DatabaseInit -> migrationState.DatabaseInitView(tempDatabaseFile, s.totalBytes, s.archivePath) + is MigrationFromState.UploadProgress -> migrationState.UploadProgressView(s.uploadedBytes, s.totalBytes, s.ctrl, s.user, tempDatabaseFile, chatReceiver, s.archivePath) + is MigrationFromState.UploadFailed -> migrationState.UploadFailedView(s.totalBytes, s.archivePath, chatReceiver.value) + is MigrationFromState.LinkCreation -> LinkCreationView() + is MigrationFromState.LinkShown -> migrationState.LinkShownView(s.fileId, s.link, s.ctrl, chatReceiver.value) + is MigrationFromState.Finished -> migrationState.FinishedView(s.chatDeletion) + } +} + +@Composable +private fun MutableState<MigrationFromState>.ChatStopInProgressView() { + Box { + SectionView(stringResource(MR.strings.migrate_from_device_stopping_chat).uppercase()) {} + ProgressView() + } + LaunchedEffect(Unit) { + stopChat() + } +} + +@Composable +private fun MutableState<MigrationFromState>.ChatStopFailedView(reason: String) { + SectionView(stringResource(MR.strings.error_stopping_chat).uppercase()) { + Text(reason) + SectionSpacer() + SettingsActionItemWithContent( + icon = painterResource(MR.images.ic_report_filled), + text = stringResource(MR.strings.auth_stop_chat), + textColor = MaterialTheme.colors.error, + click = ::stopChat + ){} + SectionTextFooter(stringResource(MR.strings.migrate_from_device_chat_should_be_stopped)) + } +} + +@Composable +private fun MutableState<MigrationFromState>.PassphraseNotSetView() { + DatabaseEncryptionView(chatModel, true) + KeyChangeEffect(appPreferences.initialRandomDBPassphrase.state.value) { + if (!appPreferences.initialRandomDBPassphrase.get()) { + state = MigrationFromState.UploadConfirmation + } + } +} + +@Composable +private fun MutableState<MigrationFromState>.PassphraseConfirmationView() { + val useKeychain = remember { appPreferences.storeDBPassphrase.get() } + val currentKey = rememberSaveable { mutableStateOf("") } + val verifyingPassphrase = rememberSaveable { mutableStateOf(false) } + Box { + val view = LocalMultiplatformView() + Column { + ChatStoppedView() + SectionSpacer() + + SectionView(stringResource(MR.strings.migrate_from_device_verify_database_passphrase).uppercase()) { + PassphraseField(currentKey, placeholder = stringResource(MR.strings.current_passphrase), Modifier.padding(horizontal = DEFAULT_PADDING), isValid = ::validKey, requestFocus = true) + + SettingsActionItemWithContent( + icon = painterResource(if (useKeychain) MR.images.ic_vpn_key_filled else MR.images.ic_lock), + text = stringResource(MR.strings.migrate_from_device_verify_passphrase), + textColor = MaterialTheme.colors.primary, + disabled = verifyingPassphrase.value || currentKey.value.isEmpty(), + click = { + verifyingPassphrase.value = true + hideKeyboard(view) + withBGApi { + verifyDatabasePassphrase(currentKey.value) + verifyingPassphrase.value = false + } + } + ) {} + SectionTextFooter(stringResource(MR.strings.migrate_from_device_confirm_you_remember_passphrase)) + } + } + if (verifyingPassphrase.value) { + ProgressView() + } + } +} + +@Composable +private fun MutableState<MigrationFromState>.UploadConfirmationView() { + SectionView(stringResource(MR.strings.migrate_from_device_confirm_upload).uppercase()) { + SettingsActionItemWithContent( + icon = painterResource(MR.images.ic_ios_share), + text = stringResource(MR.strings.migrate_from_device_archive_and_upload), + textColor = MaterialTheme.colors.primary, + click = { state = MigrationFromState.Archiving } + ){} + SectionTextFooter(stringResource(MR.strings.migrate_from_device_all_data_will_be_uploaded)) + } +} + +@Composable +private fun MutableState<MigrationFromState>.ArchivingView() { + Box { + SectionView(stringResource(MR.strings.migrate_from_device_archiving_database).uppercase()) {} + ProgressView() + } + LaunchedEffect(Unit) { + exportArchive() + } +} + +@Composable +private fun MutableState<MigrationFromState>.DatabaseInitView(tempDatabaseFile: File, totalBytes: Long, archivePath: String) { + Box { + SectionView(stringResource(MR.strings.migrate_from_device_database_init).uppercase()) {} + ProgressView() + } + LaunchedEffect(Unit) { + prepareDatabase(tempDatabaseFile, totalBytes, archivePath) + } +} + +@Composable +private fun MutableState<MigrationFromState>.UploadProgressView( + uploadedBytes: Long, + totalBytes: Long, + ctrl: ChatCtrl, + user: User, + tempDatabaseFile: File, + chatReceiver: MutableState<MigrationFromChatReceiver?>, + archivePath: String, +) { + Box { + SectionView(stringResource(MR.strings.migrate_from_device_uploading_archive).uppercase()) { + val ratio = uploadedBytes.toFloat() / max(totalBytes, 1) + LargeProgressView(ratio, "${(ratio * 100).toInt()}%", stringResource(MR.strings.migrate_from_device_bytes_uploaded).format(formatBytes(uploadedBytes))) + } + } + LaunchedEffect(Unit) { + startUploading(totalBytes, ctrl, user, tempDatabaseFile, chatReceiver, archivePath) + } +} + +@Composable +private fun MutableState<MigrationFromState>.UploadFailedView(totalBytes: Long, archivePath: String, chatReceiver: MigrationFromChatReceiver?) { + SectionView(stringResource(MR.strings.migrate_from_device_upload_failed).uppercase()) { + SettingsActionItemWithContent( + icon = painterResource(MR.images.ic_ios_share), + text = stringResource(MR.strings.migrate_from_device_repeat_upload), + textColor = MaterialTheme.colors.primary, + click = { + state = MigrationFromState.DatabaseInit(totalBytes, archivePath) + } + ) {} + SectionTextFooter(stringResource(MR.strings.migrate_from_device_try_again)) + } + LaunchedEffect(Unit) { + chatReceiver?.stopAndCleanUp() + } +} + +@Composable +private fun LinkCreationView() { + Box { + SectionView(stringResource(MR.strings.migrate_from_device_creating_archive_link).uppercase()) {} + ProgressView() + } +} + +@Composable +private fun MutableState<MigrationFromState>.LinkShownView(fileId: Long, link: String, ctrl: ChatCtrl, chatReceiver: MigrationFromChatReceiver?) { + SectionView { + SettingsActionItemWithContent( + icon = painterResource(MR.images.ic_close), + text = stringResource(MR.strings.migrate_from_device_cancel_migration), + textColor = MaterialTheme.colors.error, + click = { + cancelMigration(fileId, ctrl) + } + ) {} + SettingsActionItemWithContent( + icon = painterResource(MR.images.ic_check), + text = stringResource(MR.strings.migrate_from_device_finalize_migration), + textColor = MaterialTheme.colors.primary, + click = { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.migrate_from_device_remove_archive_question), + text = generalGetString(MR.strings.migrate_from_device_uploaded_archive_will_be_removed), + confirmText = generalGetString(MR.strings.continue_to_next_step), + destructive = true, + onConfirm = { + finishMigration(fileId, ctrl, chatReceiver) + } + ) + } + ) {} + SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_archive_will_be_deleted)) + SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_choose_migrate_from_another_device)) + } + SectionSpacer() + SectionView(stringResource(MR.strings.show_QR_code).uppercase()) { + SimpleXLinkQRCode(link, onShare = {}) + } + SectionSpacer() + SectionView(stringResource(MR.strings.migrate_from_device_or_share_this_file_link).uppercase()) { + LinkTextView(link, true) + } +} + +@Composable +private fun MutableState<MigrationFromState>.FinishedView(chatDeletion: Boolean) { + Box { + SectionView(stringResource(MR.strings.migrate_from_device_migration_complete).uppercase()) { + SettingsActionItemWithContent( + icon = painterResource(MR.images.ic_play_arrow_filled), + text = stringResource(MR.strings.migrate_from_device_start_chat), + textColor = MaterialTheme.colors.error, + click = { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.start_chat_question), + text = generalGetString(MR.strings.migrate_from_device_starting_chat_on_multiple_devices_unsupported), + confirmText = generalGetString(MR.strings.migrate_from_device_start_chat), + onConfirm = { + withLongRunningApi { startChatAndDismiss() } + }, + destructive = true + ) + } + ) {} + + SettingsActionItemWithContent( + icon = painterResource(MR.images.ic_delete_forever), + text = stringResource(MR.strings.migrate_from_device_delete_database_from_device), + textColor = MaterialTheme.colors.primary, + click = { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.delete_chat_profile_question), + text = generalGetString(MR.strings.delete_chat_profile_action_cannot_be_undone_warning), + confirmText = generalGetString(MR.strings.delete_verb), + onConfirm = { + deleteChatAndDismiss() + } + ) + } + ) {} + SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_you_must_not_start_database_on_two_device)) + SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_using_on_two_device_breaks_encryption)) + } + if (chatDeletion) { + ProgressView() + } + } +} + +@Composable +private fun ProgressView() { + DefaultProgressView(null) +} + +@Composable +fun LargeProgressView(value: Float, title: String, description: String) { + Box(Modifier.padding(DEFAULT_PADDING).fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator( + progress = value, + (if (appPlatform.isDesktop) Modifier.size(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) else Modifier.size(windowWidth() - DEFAULT_PADDING * + 2)) + .rotate(-90f), + color = MaterialTheme.colors.primary, + strokeWidth = 25.dp + ) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(description, color = Color.Transparent) + Text(title, style = MaterialTheme.typography.h1.copy(fontSize = 50.sp, fontWeight = FontWeight.Bold), color = MaterialTheme.colors.primary) + Text(description, style = MaterialTheme.typography.subtitle1) + } + } +} + +private fun MutableState<MigrationFromState>.stopChat() { + withBGApi { + try { + stopChatAsync(chatModel) + try { + controller.apiSaveAppSettings(AppSettings.current.prepareForExport()) + state = if (appPreferences.initialRandomDBPassphrase.get()) MigrationFromState.PassphraseNotSet else MigrationFromState.PassphraseConfirmation + platform.androidChatStopped() + } catch (e: Exception) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.migrate_from_device_error_saving_settings), + text = e.stackTraceToString() + ) + state = MigrationFromState.ChatStopFailed(reason = generalGetString(MR.strings.migrate_from_device_error_saving_settings)) + } + } catch (e: Exception) { + state = MigrationFromState.ChatStopFailed(reason = e.stackTraceToString().take(10)) + } + } +} + +private suspend fun MutableState<MigrationFromState>.verifyDatabasePassphrase(dbKey: String) { + val error = controller.testStorageEncryption(dbKey) + if (error == null) { + state = MigrationFromState.UploadConfirmation + } else if (((error as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorOpen)?.sqliteError is SQLiteError.ErrorNotADatabase) { + showErrorOnMigrationIfNeeded(DBMigrationResult.ErrorNotADatabase("")) + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.error), + text = generalGetString(MR.strings.migrate_from_device_error_verifying_passphrase) + " " + error.string + ) + } +} + +private fun MutableState<MigrationFromState>.exportArchive() { + withLongRunningApi { + try { + getMigrationTempFilesDirectory().mkdir() + val (archivePath, archiveErrors) = exportChatArchive(chatModel, getMigrationTempFilesDirectory(), mutableStateOf("")) + if (archiveErrors.isEmpty()) { + uploadArchive(archivePath) + } else { + showArchiveExportedWithErrorsAlert(generalGetString(MR.strings.chat_database_exported_migrate), archiveErrors) { + uploadArchive(archivePath) + } + state = MigrationFromState.UploadConfirmation + } + } catch (e: Exception) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.migrate_from_device_error_exporting_archive), + text = e.stackTraceToString() + ) + state = MigrationFromState.UploadConfirmation + } + } +} + +private fun MutableState<MigrationFromState>.uploadArchive(archivePath: String) { + val totalBytes = File(archivePath).length() + if (totalBytes > 0L) { + state = MigrationFromState.DatabaseInit(totalBytes, archivePath) + } else { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_from_device_exported_file_doesnt_exist)) + state = MigrationFromState.UploadConfirmation + } + +} + +suspend fun initTemporaryDatabase(tempDatabaseFile: File, netCfg: NetCfg): Pair<ChatCtrl, User>? { + val (status, ctrl) = chatInitTemporaryDatabase(tempDatabaseFile.absolutePath) + showErrorOnMigrationIfNeeded(status) + try { + if (ctrl != null) { + val user = startChatWithTemporaryDatabase(ctrl, netCfg) + if (user != null) return ctrl to user + chatCloseStore(ctrl) + } + File(tempDatabaseFile.absolutePath + "_chat.db").delete() + File(tempDatabaseFile.absolutePath + "_agent.db").delete() + } catch (e: Throwable) { + Log.e(TAG, "Error while starting chat in temporary database: ${e.stackTraceToString()}") + } + return null +} + +private fun MutableState<MigrationFromState>.prepareDatabase( + tempDatabaseFile: File, + totalBytes: Long, + archivePath: String, +) { + withLongRunningApi { + val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, getNetCfg()) + if (ctrlAndUser == null) { + state = MigrationFromState.UploadFailed(totalBytes, archivePath) + return@withLongRunningApi + } + + val (ctrl, user) = ctrlAndUser + state = MigrationFromState.UploadProgress(0L, totalBytes, 0L, archivePath, ctrl, user) + } +} + +private fun MutableState<MigrationFromState>.startUploading( + totalBytes: Long, + ctrl: ChatCtrl, + user: User, + tempDatabaseFile: File, + chatReceiver: MutableState<MigrationFromChatReceiver?>, + archivePath: String, +) { + withBGApi { + chatReceiver.value = MigrationFromChatReceiver(ctrl, tempDatabaseFile) { msg -> + val r = msg.result + when (r) { + is CR.SndFileProgressXFTP -> { + val s = state + if (s is MigrationFromState.UploadProgress && s.uploadedBytes != s.totalBytes) { + state = MigrationFromState.UploadProgress(r.sentSize, r.totalSize, r.fileTransferMeta.fileId, archivePath, ctrl, user) + } + } + is CR.SndFileRedirectStartXFTP -> { + delay(500) + state = MigrationFromState.LinkCreation + } + is CR.SndStandaloneFileComplete -> { + delay(500) + val cfg = getNetCfg() + val data = MigrationFileLinkData( + networkConfig = MigrationFileLinkData.NetworkConfig( + legacySocksProxy = null, + networkProxy = if (appPrefs.networkUseSocksProxy.get()) appPrefs.networkProxy.get() else null, + hostMode = cfg.hostMode, + requiredHostMode = cfg.requiredHostMode + ) + ) + state = MigrationFromState.LinkShown(r.fileTransferMeta.fileId, data.addToLink(r.rcvURIs[0]), ctrl) + } + is CR.SndFileError -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.migrate_from_device_upload_failed), + generalGetString(MR.strings.migrate_from_device_check_connection_and_try_again) + ) + state = MigrationFromState.UploadFailed(totalBytes, archivePath) + } + else -> { + Log.d(TAG, "unsupported event: ${msg.responseType}") + } + } + } + + chatReceiver.value?.start() + + val (res, error) = controller.uploadStandaloneFile(user, CryptoFile.plain(File(archivePath).name), ctrl) + if (res == null) { + state = MigrationFromState.UploadFailed(totalBytes, archivePath) + return@withBGApi AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.migrate_from_device_error_uploading_archive), + error + ) + } + state = MigrationFromState.UploadProgress(0, res.fileSize, res.fileId, archivePath, ctrl, user) + } +} + +private suspend fun cancelUploadedArchive(fileId: Long, ctrl: ChatCtrl) { + controller.apiCancelFile(null, fileId, ctrl) +} + +private fun cancelMigration(fileId: Long, ctrl: ChatCtrl) { + withBGApi { + cancelUploadedArchive(fileId, ctrl) + startChatAndDismiss() + } +} + +private fun MutableState<MigrationFromState>.finishMigration(fileId: Long, ctrl: ChatCtrl, chatReceiver: MigrationFromChatReceiver?) { + withBGApi { + cancelUploadedArchive(fileId, ctrl) + chatReceiver?.stopAndCleanUp() + getMigrationTempFilesDirectory().deleteRecursively() + state = MigrationFromState.Finished(false) + } +} + +private fun MutableState<MigrationFromState>.deleteChatAndDismiss() { + withBGApi { + try { + deleteChatAsync(chatModel) + chatModel.chatDbChanged.value = true + state = MigrationFromState.Finished(true) + try { + initChatController(startChat = { CompletableDeferred(false) }) + chatModel.chatDbChanged.value = false + ModalManager.fullscreen.closeModals() + } catch (e: Exception) { + throw Exception(generalGetString(MR.strings.error_starting_chat) + "\n" + e.stackTraceToString()) + } + } catch (e: Exception) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.migrate_from_device_error_deleting_database), + text = e.stackTraceToString() + ) + } + } +} + +private suspend fun startChatAndDismiss(dismiss: Boolean = true) { + try { + val user = chatModel.currentUser.value + if (chatModel.chatDbChanged.value) { + initChatController() + chatModel.chatDbChanged.value = false + } else if (user != null) { + startChat(user) + } + platform.androidChatStartedAfterBeingOff() + } catch (e: Exception) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.error_starting_chat), + text = e.stackTraceToString() + ) + } + // Hide settings anyway if chatDbStatus is not ok, probably passphrase needs to be entered + if (dismiss || chatModel.chatDbStatus.value != DBMigrationResult.OK) { + ModalManager.fullscreen.closeModals() + } +} + +private suspend fun MutableState<MigrationFromState>.cleanUpOnBack(chatReceiver: MigrationFromChatReceiver?) { + val s = state + if (s !is MigrationFromState.LinkShown && s !is MigrationFromState.Finished) { + chatModel.switchingUsersAndHosts.value = true + startChatAndDismiss(false) + chatModel.switchingUsersAndHosts.value = false + } + if (s is MigrationFromState.UploadProgress) { + cancelUploadedArchive(s.fileId, s.ctrl) + } + chatReceiver?.stopAndCleanUp() + getMigrationTempFilesDirectory().deleteRecursively() +} + +private fun fileForTemporaryDatabase(): File = + File(getMigrationTempFilesDirectory(), generateNewFileName("migration", "db", getMigrationTempFilesDirectory())) + +private class MigrationFromChatReceiver( + val ctrl: ChatCtrl, + val databaseUrl: File, + var receiveMessages: Boolean = true, + val processReceivedMsg: suspend (API) -> Unit +) { + fun start() { + Log.d(TAG, "MigrationChatReceiver startReceiver") + CoroutineScope(Dispatchers.IO).launch { + while (receiveMessages) { + try { + val msg = ChatController.recvMsg(ctrl) + if (msg != null && receiveMessages) { + val rhId = msg.rhId + Log.d(TAG, "processReceivedMsg: ${msg.responseType}") + chatModel.addTerminalItem(TerminalItem.resp(rhId, msg)) + val finishedWithoutTimeout = withTimeoutOrNull(60_000L) { + processReceivedMsg(msg) + } + if (finishedWithoutTimeout == null) { + Log.e(TAG, "Timeout reached while processing received message: " + msg.responseType) + if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.possible_slow_function_title), + text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.responseType + "\n" + Exception().stackTraceToString()), + shareText = true + ) + } + } + } + } catch (e: Exception) { + Log.e(TAG, "MigrationChatReceiver recvMsg/processReceivedMsg exception: " + e.stackTraceToString()) + } catch (e: Exception) { + Log.e(TAG, "MigrationChatReceiver recvMsg/processReceivedMsg throwable: " + e.stackTraceToString()) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), e.stackTraceToString()) + } + } + } + } + + fun stopAndCleanUp() { + Log.d(TAG, "MigrationChatReceiver.stop") + receiveMessages = false + chatCloseStore(ctrl) + File(databaseUrl.absolutePath + "_chat.db").delete() + File(databaseUrl.absolutePath + "_agent.db").delete() + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt new file mode 100644 index 0000000000..d74846f8a3 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt @@ -0,0 +1,786 @@ +package chat.simplex.common.views.migration + +import SectionBottomSpacer +import SectionItemView +import SectionSpacer +import SectionTextFooter +import SectionView +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import chat.simplex.common.model.* +import chat.simplex.common.model.AppPreferences.Companion.SHARED_PREFS_MIGRATION_TO_STAGE +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ChatController.getNetCfg +import chat.simplex.common.model.ChatController.startChat +import chat.simplex.common.model.ChatCtrl +import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.database.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.helpers.DatabaseUtils.ksDatabasePassword +import chat.simplex.common.views.newchat.QRCodeScanner +import chat.simplex.common.views.onboarding.OnboardingStage +import chat.simplex.common.views.usersettings.* +import chat.simplex.common.views.usersettings.networkAndServers.OnionRelatedLayout +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.* +import kotlinx.datetime.Clock +import kotlinx.datetime.toJavaInstant +import kotlinx.serialization.* +import java.io.File +import java.net.URI +import java.text.SimpleDateFormat +import java.util.* +import kotlin.math.max + +@Serializable +sealed class MigrationToDeviceState { + @Serializable @SerialName("onion") data class Onion(val link: String, val socksProxy: String?, val networkProxy: NetworkProxy?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToDeviceState() + @Serializable @SerialName("downloadProgress") data class DownloadProgress(val link: String, val archiveName: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToDeviceState() + @Serializable @SerialName("archiveImport") data class ArchiveImport(val archiveName: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToDeviceState() + @Serializable @SerialName("passphrase") data class Passphrase(val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToDeviceState() + + companion object { + // Here we check whether it's needed to show migration process after app restart or not + // It's important to NOT show the process when archive was corrupted/not fully downloaded + fun makeMigrationState(): MigrationToState? { + val stage = settings.getStringOrNull(SHARED_PREFS_MIGRATION_TO_STAGE) + val state: MigrationToDeviceState? = if (stage != null) json.decodeFromString(stage) else null + val initial: MigrationToState? = when(state) { + null -> null + is DownloadProgress -> { + // No migration happens at the moment actually since archive were not downloaded fully + Log.e(TAG, "MigrateToDevice: archive wasn't fully downloaded, removed broken file") + null + } + is Onion -> null + is ArchiveImport -> { + if (!File(getMigrationTempFilesDirectory(), state.archiveName).exists()) { + Log.e(TAG, "MigrateToDevice: archive was removed unintentionally or state is broken, dropping migration") + null + } else { + val archivePath = File(getMigrationTempFilesDirectory(), state.archiveName) + MigrationToState.ArchiveImportFailed(archivePath.absolutePath, state.netCfg, state.networkProxy) + } + } + is Passphrase -> MigrationToState.Passphrase("", state.netCfg, state.networkProxy) + } + if (initial == null) { + settings.remove(SHARED_PREFS_MIGRATION_TO_STAGE) + getMigrationTempFilesDirectory().deleteRecursively() + } + return initial + } + + fun save(state: MigrationToDeviceState?) { + if (state != null) { + appPreferences.migrationToStage.set(json.encodeToString(state)) + } else { + appPreferences.migrationToStage.set(null) + } + } + } +} + +@Serializable +sealed class MigrationToState { + @Serializable object PasteOrScanLink: MigrationToState() + @Serializable data class Onion( + val link: String, + // Legacy, remove in 2025 + @SerialName("socksProxy") + val legacySocksProxy: String?, + val networkProxy: NetworkProxy?, + val hostMode: HostMode, + val requiredHostMode: Boolean + ): MigrationToState() + @Serializable data class DatabaseInit(val link: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState() + @Serializable data class LinkDownloading(val link: String, val ctrl: ChatCtrl, val user: User, val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState() + @Serializable data class DownloadProgress(val downloadedBytes: Long, val totalBytes: Long, val fileId: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?, val ctrl: ChatCtrl?): MigrationToState() + @Serializable data class DownloadFailed(val totalBytes: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState() + @Serializable data class ArchiveImport(val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState() + @Serializable data class ArchiveImportFailed(val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState() + @Serializable data class Passphrase(val passphrase: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState() + @Serializable data class MigrationConfirmation(val status: DBMigrationResult, val passphrase: String, val useKeychain: Boolean, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState() + @Serializable data class Migration(val passphrase: String, val confirmation: chat.simplex.common.views.helpers.MigrationConfirmation, val useKeychain: Boolean, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState() +} + +private var MutableState<MigrationToState?>.state: MigrationToState? + get() = value + set(v) { value = v } + +@Composable +fun ModalData.MigrateToDeviceView(close: () -> Unit) { + val migrationState = remember { chatModel.migrationState } + // Prevent from hiding the view until migration is finished or app deleted + val backDisabled = remember { + derivedStateOf { + when (chatModel.migrationState.value) { + null, + is MigrationToState.PasteOrScanLink, + is MigrationToState.Onion, + is MigrationToState.LinkDownloading, + is MigrationToState.DownloadProgress, + is MigrationToState.DownloadFailed, + is MigrationToState.ArchiveImportFailed -> false + + is MigrationToState.ArchiveImport, + is MigrationToState.DatabaseInit, + is MigrationToState.Migration, + is MigrationToState.MigrationConfirmation, + is MigrationToState.Passphrase -> true + } + } + } + val chatReceiver = remember { mutableStateOf(null as MigrationToChatReceiver?) } + ModalView( + enableClose = !backDisabled.value, + close = { + withBGApi { + migrationState.cleanUpOnBack(chatReceiver.value) + close() + } + }, + ) { + MigrateToDeviceLayout( + migrationState = migrationState, + chatReceiver = chatReceiver, + close = close, + ) + } +} + +@Composable +private fun ModalData.MigrateToDeviceLayout( + migrationState: MutableState<MigrationToState?>, + chatReceiver: MutableState<MigrationToChatReceiver?>, + close: () -> Unit, +) { + val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) } + ColumnWithScrollBar(maxIntrinsicSize = true) { + AppBarTitle(stringResource(MR.strings.migrate_to_device_title)) + SectionByState(migrationState, tempDatabaseFile.value, chatReceiver, close) + SectionBottomSpacer() + } + platform.androidLockPortraitOrientation() +} + +@Composable +private fun ModalData.SectionByState( + migrationState: MutableState<MigrationToState?>, + tempDatabaseFile: File, + chatReceiver: MutableState<MigrationToChatReceiver?>, + close: () -> Unit +) { + when (val s = migrationState.value) { + null -> {} + is MigrationToState.PasteOrScanLink -> migrationState.PasteOrScanLinkView(close) + is MigrationToState.Onion -> OnionView(s.link, s.legacySocksProxy, s.networkProxy, s.hostMode, s.requiredHostMode, migrationState) + is MigrationToState.DatabaseInit -> migrationState.DatabaseInitView(s.link, tempDatabaseFile, s.netCfg, s.networkProxy) + is MigrationToState.LinkDownloading -> migrationState.LinkDownloadingView(s.link, s.ctrl, s.user, s.archivePath, tempDatabaseFile, chatReceiver, s.netCfg, s.networkProxy) + is MigrationToState.DownloadProgress -> DownloadProgressView(s.downloadedBytes, totalBytes = s.totalBytes) + is MigrationToState.DownloadFailed -> migrationState.DownloadFailedView(s.link, chatReceiver.value, s.archivePath, s.netCfg, s.networkProxy) + is MigrationToState.ArchiveImport -> migrationState.ArchiveImportView(s.archivePath, s.netCfg, s.networkProxy) + is MigrationToState.ArchiveImportFailed -> migrationState.ArchiveImportFailedView(s.archivePath, s.netCfg, s.networkProxy) + is MigrationToState.Passphrase -> migrationState.PassphraseEnteringView(currentKey = s.passphrase, s.netCfg, s.networkProxy) + is MigrationToState.MigrationConfirmation -> migrationState.MigrationConfirmationView(s.status, s.passphrase, s.useKeychain, s.netCfg, s.networkProxy) + is MigrationToState.Migration -> MigrationView(s.passphrase, s.confirmation, s.useKeychain, s.netCfg, s.networkProxy, close) + } +} + +@Composable +private fun MutableState<MigrationToState?>.PasteOrScanLinkView(close: () -> Unit) { + Box { + val progressIndicator = remember { mutableStateOf(false) } + Column { + if (appPlatform.isAndroid) { + SectionView(stringResource(MR.strings.scan_QR_code).replace('\n', ' ').uppercase()) { + QRCodeScanner(showQRCodeScanner = remember { mutableStateOf(true) }) { text -> + checkUserLink(text) + } + } + SectionSpacer() + } + + SectionView(stringResource(if (appPlatform.isAndroid) MR.strings.or_paste_archive_link else MR.strings.paste_archive_link).uppercase()) { + PasteLinkView() + } + SectionSpacer() + + SectionView(stringResource(MR.strings.chat_archive).uppercase()) { + ArchiveImportView(progressIndicator, close) + } + } + if (progressIndicator.value) + ProgressView() + } +} + +@Composable +private fun MutableState<MigrationToState?>.PasteLinkView() { + val clipboard = LocalClipboardManager.current + SectionItemView({ + val str = clipboard.getText()?.text ?: return@SectionItemView + withBGApi { checkUserLink(str) } + }) { + Text(stringResource(MR.strings.tap_to_paste_link)) + } +} + +@Composable +private fun ArchiveImportView(progressIndicator: MutableState<Boolean>, close: () -> Unit) { + val importArchiveLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) { + withLongRunningApi { + val success = importArchive(to, mutableStateOf(0 to 0), progressIndicator, true) + if (success) { + startChat( + chatModel, + mutableStateOf(Clock.System.now()), + chatModel.chatDbChanged, + progressIndicator + ) + hideView(close) + } + } + } + } + SectionItemView({ + withLongRunningApi { importArchiveLauncher.launch("application/zip") } + }) { + Text(stringResource(MR.strings.import_database)) + } +} + +@Composable +private fun ModalData.OnionView(link: String, legacyLinkSocksProxy: String?, linkNetworkProxy: NetworkProxy?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState<MigrationToState?>) { + val onionHosts = remember { stateGetOrPut("onionHosts") { + getNetCfg().copy(socksProxy = linkNetworkProxy?.toProxyString() ?: legacyLinkSocksProxy, hostMode = hostMode, requiredHostMode = requiredHostMode).onionHosts + } } + val networkUseSocksProxy = remember { stateGetOrPut("networkUseSocksProxy") { linkNetworkProxy != null || legacyLinkSocksProxy != null } } + val sessionMode = remember { stateGetOrPut("sessionMode") { TransportSessionMode.User} } + val networkProxy = remember { stateGetOrPut("networkProxy") { + linkNetworkProxy + ?: if (legacyLinkSocksProxy != null) { + NetworkProxy(host = legacyLinkSocksProxy.substringBefore(":").ifBlank { "localhost" }, port = legacyLinkSocksProxy.substringAfter(":").toIntOrNull() ?: 9050) + } else { + appPrefs.networkProxy.get() + } + } + } + + val netCfg = rememberSaveable(stateSaver = serializableSaver()) { + mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = linkNetworkProxy?.toProxyString() ?: legacyLinkSocksProxy, sessionMode = sessionMode.value)) + } + + SectionView(stringResource(MR.strings.migrate_to_device_confirm_network_settings).uppercase()) { + SettingsActionItemWithContent( + icon = painterResource(MR.images.ic_check), + text = stringResource(MR.strings.migrate_to_device_apply_onion), + textColor = MaterialTheme.colors.primary, + click = { + val updated = netCfg.value + .withOnionHosts(onionHosts.value) + .withProxy(if (networkUseSocksProxy.value) networkProxy.value else null, null) + .copy( + sessionMode = sessionMode.value + ) + withBGApi { + state.value = MigrationToState.DatabaseInit(link, updated, if (networkUseSocksProxy.value) networkProxy.value else null) + } + } + ){} + SectionTextFooter(stringResource(MR.strings.migrate_to_device_confirm_network_settings_footer)) + } + + SectionSpacer() + + val networkProxyPref = SharedPreference(get = { networkProxy.value }, set = { + networkProxy.value = it + }) + SectionView(stringResource(MR.strings.network_settings_title).uppercase()) { + OnionRelatedLayout( + appPreferences.developerTools.get(), + networkUseSocksProxy, + onionHosts, + sessionMode, + networkProxyPref, + toggleSocksProxy = { enable -> + networkUseSocksProxy.value = enable + }, + updateSessionMode = { + sessionMode.value = it + } + ) + } +} + +@Composable +private fun MutableState<MigrationToState?>.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg, networkProxy: NetworkProxy?) { + Box { + SectionView(stringResource(MR.strings.migrate_to_device_database_init).uppercase()) {} + ProgressView() + } + LaunchedEffect(Unit) { + prepareDatabase(link, tempDatabaseFile, netCfg, networkProxy) + } +} + +@Composable +private fun MutableState<MigrationToState?>.LinkDownloadingView( + link: String, + ctrl: ChatCtrl, + user: User, + archivePath: String, + tempDatabaseFile: File, + chatReceiver: MutableState<MigrationToChatReceiver?>, + netCfg: NetCfg, + networkProxy: NetworkProxy? +) { + Box { + SectionView(stringResource(MR.strings.migrate_to_device_downloading_details).uppercase()) {} + ProgressView() + } + LaunchedEffect(Unit) { + startDownloading(0, ctrl, user, tempDatabaseFile, chatReceiver, link, archivePath, netCfg, networkProxy) + } +} + +@Composable +private fun DownloadProgressView(downloadedBytes: Long, totalBytes: Long) { + Box { + SectionView(stringResource(MR.strings.migrate_to_device_downloading_archive).uppercase()) { + val ratio = downloadedBytes.toFloat() / max(totalBytes, 1) + LargeProgressView(ratio, "${(ratio * 100).toInt()}%", stringResource(MR.strings.migrate_to_device_bytes_downloaded).format(formatBytes(downloadedBytes))) + } + } +} + +@Composable +private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, chatReceiver: MigrationToChatReceiver?, archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { + SectionView(stringResource(MR.strings.migrate_to_device_download_failed).uppercase()) { + SettingsActionItemWithContent( + icon = painterResource(MR.images.ic_download), + text = stringResource(MR.strings.migrate_to_device_repeat_download), + textColor = MaterialTheme.colors.primary, + click = { + state = MigrationToState.DatabaseInit(link, netCfg, networkProxy) + } + ) {} + SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again)) + } + LaunchedEffect(Unit) { + chatReceiver?.stopAndCleanUp() + File(archivePath).delete() + MigrationToDeviceState.save(null) + } +} + +@Composable +private fun MutableState<MigrationToState?>.ArchiveImportView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { + Box { + SectionView(stringResource(MR.strings.migrate_to_device_importing_archive).uppercase()) {} + ProgressView() + } + LaunchedEffect(Unit) { + importArchive(archivePath, netCfg, networkProxy) + } +} + +@Composable +private fun MutableState<MigrationToState?>.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { + SectionView(stringResource(MR.strings.migrate_to_device_import_failed).uppercase()) { + SettingsActionItemWithContent( + icon = painterResource(MR.images.ic_download), + text = stringResource(MR.strings.migrate_to_device_repeat_import), + textColor = MaterialTheme.colors.primary, + click = { + state = MigrationToState.ArchiveImport(archivePath, netCfg, networkProxy) + } + ) {} + SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again)) + } +} + +@Composable +private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { + val currentKey = rememberSaveable { mutableStateOf(currentKey) } + val verifyingPassphrase = rememberSaveable { mutableStateOf(false) } + val useKeychain = rememberSaveable { mutableStateOf(appPreferences.storeDBPassphrase.get()) } + + Box { + val view = LocalMultiplatformView() + SectionView(stringResource(MR.strings.migrate_to_device_enter_passphrase).uppercase()) { + SavePassphraseSetting( + useKeychain.value, + false, + false, + enabled = !verifyingPassphrase.value, + smallPadding = false + ) { checked -> useKeychain.value = checked } + + PassphraseField(currentKey, placeholder = stringResource(MR.strings.current_passphrase), Modifier.padding(horizontal = DEFAULT_PADDING), isValid = ::validKey, requestFocus = true) + + SettingsActionItemWithContent( + icon = painterResource(MR.images.ic_vpn_key_filled), + text = stringResource(MR.strings.open_chat), + textColor = MaterialTheme.colors.primary, + disabled = verifyingPassphrase.value || currentKey.value.isEmpty(), + click = { + verifyingPassphrase.value = true + hideKeyboard(view) + withBGApi { + val (status, _) = chatInitTemporaryDatabase(dbAbsolutePrefixPath, key = currentKey.value, confirmation = MigrationConfirmation.YesUp) + val success = status == DBMigrationResult.OK || status == DBMigrationResult.InvalidConfirmation + if (success) { + state = MigrationToState.Migration(currentKey.value, MigrationConfirmation.YesUp, useKeychain.value, netCfg, networkProxy) + } else if (status is DBMigrationResult.ErrorMigration) { + state = MigrationToState.MigrationConfirmation(status, currentKey.value, useKeychain.value, netCfg, networkProxy) + } else { + showErrorOnMigrationIfNeeded(status) + } + verifyingPassphrase.value = false + } + } + ) {} + DatabaseEncryptionFooter(useKeychain, chatDbEncrypted = true, remember { mutableStateOf(false) }, remember { mutableStateOf(false) }, true) + } + if (verifyingPassphrase.value) { + ProgressView() + } + } +} + +@Composable +private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?) { + data class Tuple4<A,B,C,D>(val a: A, val b: B, val c: C, val d: D) + val (header: String, button: String?, footer: String, confirmation: MigrationConfirmation?) = when (status) { + is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) { + is MigrationError.Upgrade -> + Tuple4( + generalGetString(MR.strings.database_upgrade), + generalGetString(MR.strings.upgrade_and_open_chat), + "", + MigrationConfirmation.YesUp + ) + is MigrationError.Downgrade -> + Tuple4( + generalGetString(MR.strings.database_downgrade), + generalGetString(MR.strings.downgrade_and_open_chat), + generalGetString(MR.strings.database_downgrade_warning), + MigrationConfirmation.YesUpDown + ) + is MigrationError.Error -> + Tuple4( + generalGetString(MR.strings.incompatible_database_version), + null, + mtrErrorDescription(err.mtrError), + null + ) + } + else -> Tuple4(generalGetString(MR.strings.error), null, generalGetString(MR.strings.unknown_error), null) + } + SectionView(header.uppercase()) { + if (button != null && confirmation != null) { + SettingsActionItemWithContent( + icon = painterResource(MR.images.ic_download), + text = button, + textColor = MaterialTheme.colors.primary, + click = { + state = MigrationToState.Migration(passphrase, confirmation, useKeychain, netCfg, networkProxy) + } + ) {} + } + SectionTextFooter(footer) + } +} + +@Composable +private fun MigrationView(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?, close: () -> Unit) { + Box { + SectionView(stringResource(MR.strings.migrate_to_device_migrating).uppercase()) {} + ProgressView() + } + LaunchedEffect(Unit) { + startChat(passphrase, confirmation, useKeychain, netCfg, networkProxy, close) + } +} + +@Composable +private fun ProgressView() { + DefaultProgressView(null) +} + +private suspend fun MutableState<MigrationToState?>.checkUserLink(link: String): Boolean { + return if (strHasSimplexFileLink(link.trim())) { + val data = MigrationFileLinkData.readFromLink(link) + val hasProxyConfigured = data?.networkConfig?.hasProxyConfigured() ?: false + val networkConfig = data?.networkConfig?.transformToPlatformSupported() + // If any of iOS or Android had onion enabled, show onion screen + if (hasProxyConfigured && networkConfig?.hostMode != null && networkConfig.requiredHostMode != null) { + state = MigrationToState.Onion(link.trim(), networkConfig.legacySocksProxy, networkConfig.networkProxy, networkConfig.hostMode, networkConfig.requiredHostMode) + MigrationToDeviceState.save(MigrationToDeviceState.Onion(link.trim(), networkConfig.legacySocksProxy, networkConfig.networkProxy, networkConfig.hostMode, networkConfig.requiredHostMode)) + } else { + val current = getNetCfg() + state = MigrationToState.DatabaseInit(link.trim(), current.copy( + socksProxy = null, + hostMode = networkConfig?.hostMode ?: current.hostMode, + requiredHostMode = networkConfig?.requiredHostMode ?: current.requiredHostMode + ), + networkProxy = null + ) + } + true + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.invalid_file_link), + text = generalGetString(MR.strings.the_text_you_pasted_is_not_a_link) + ) + false + } +} + +private fun MutableState<MigrationToState?>.prepareDatabase( + link: String, + tempDatabaseFile: File, + netCfg: NetCfg, + networkProxy: NetworkProxy? +) { + withLongRunningApi { + val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, netCfg) + if (ctrlAndUser == null) { + // Probably, something wrong with network config or database initialization, let's start from scratch + state = MigrationToState.PasteOrScanLink + MigrationToDeviceState.save(null) + return@withLongRunningApi + } + + val (ctrl, user) = ctrlAndUser + state = MigrationToState.LinkDownloading(link, ctrl, user, archivePath(), netCfg, networkProxy) + } +} + +private fun MutableState<MigrationToState?>.startDownloading( + totalBytes: Long, + ctrl: ChatCtrl, + user: User, + tempDatabaseFile: File, + chatReceiver: MutableState<MigrationToChatReceiver?>, + link: String, + archivePath: String, + netCfg: NetCfg, + networkProxy: NetworkProxy? +) { + withBGApi { + chatReceiver.value = MigrationToChatReceiver(ctrl, tempDatabaseFile) { msg -> + val r = msg.result + when { + r is CR.RcvFileProgressXFTP -> { + state = MigrationToState.DownloadProgress(r.receivedSize, r.totalSize, r.rcvFileTransfer.fileId, link, archivePath, netCfg, networkProxy, ctrl) + MigrationToDeviceState.save(MigrationToDeviceState.DownloadProgress(link, File(archivePath).name, netCfg, networkProxy)) + } + r is CR.RcvStandaloneFileComplete -> { + delay(500) + // User closed the whole screen before new state was saved + if (state == null) { + MigrationToDeviceState.save(null) + } else { + state = MigrationToState.ArchiveImport(archivePath, netCfg, networkProxy) + MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg, networkProxy)) + } + } + r is CR.RcvFileError -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.migrate_to_device_download_failed), + generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid) + ) + state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy) + } + msg is API.Error -> { + if (msg.err is ChatError.ChatErrorChat && msg.err.errorType is ChatErrorType.NoRcvFileUser) { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.migrate_to_device_download_failed), + generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid) + ) + state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy) + } else { + Log.d(TAG, "unsupported error: ${msg.responseType}, ${json.encodeToString(msg.err)}") + } + } + else -> Log.d(TAG, "unsupported event: ${msg.responseType}") + } + } + chatReceiver.value?.start() + + val (res, error) = controller.downloadStandaloneFile(user, link, CryptoFile.plain(File(archivePath).path), ctrl) + if (res == null) { + state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy) + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.migrate_to_device_error_downloading_archive), + error + ) + } + } +} + +private fun MutableState<MigrationToState?>.importArchive(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { + withLongRunningApi { + try { + if (ChatController.ctrl == null || ChatController.ctrl == -1L) { + chatInitControllerRemovingDatabases() + } + controller.apiDeleteStorage() + wallpapersDir.mkdirs() + try { + val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString()) + val archiveErrors = controller.apiImportArchive(config) + if (archiveErrors.isNotEmpty()) { + showArchiveImportedWithErrorsAlert(archiveErrors) + } + state = MigrationToState.Passphrase("", netCfg, networkProxy) + MigrationToDeviceState.save(MigrationToDeviceState.Passphrase(netCfg, networkProxy)) + } catch (e: Exception) { + state = MigrationToState.ArchiveImportFailed(archivePath, netCfg, networkProxy) + AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_importing_database), e.stackTraceToString()) + } + } catch (e: Exception) { + state = MigrationToState.ArchiveImportFailed(archivePath, netCfg, networkProxy) + AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_deleting_database), e.stackTraceToString()) + } + } +} + +private suspend fun stopArchiveDownloading(fileId: Long, ctrl: ChatCtrl) { + controller.apiCancelFile(null, fileId, ctrl) +} + +private fun startChat(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?, close: () -> Unit) { + if (useKeychain) { + ksDatabasePassword.set(passphrase) + } else { + ksDatabasePassword.remove() + } + appPreferences.storeDBPassphrase.set(useKeychain) + appPreferences.initialRandomDBPassphrase.set(false) + withBGApi { + try { + initChatController(useKey = passphrase, confirmMigrations = confirmation) { CompletableDeferred(false) } + val appSettings = controller.apiGetAppSettings(AppSettings.current.prepareForExport()).copy( + networkConfig = netCfg, + networkProxy = networkProxy + ) + finishMigration(appSettings, close) + } catch (e: Exception) { + hideView(close) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_starting_chat), e.stackTraceToString()) + } + } +} + +private suspend fun finishMigration(appSettings: AppSettings, close: () -> Unit) { + try { + getMigrationTempFilesDirectory().deleteRecursively() + appSettings.importIntoApp() + val user = chatModel.currentUser.value + if (user != null) { + startChat(user) + } + platform.androidChatStartedAfterBeingOff() + hideView(close) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_to_device_chat_migrated), generalGetString(MR.strings.migrate_to_device_finalize_migration)) + } catch (e: Exception) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_starting_chat), e.stackTraceToString()) + } + MigrationToDeviceState.save(null) +} + +private fun hideView(close: () -> Unit) { + appPreferences.onboardingStage.set(OnboardingStage.OnboardingComplete) + chatModel.migrationState.value = null + close() +} + +private suspend fun MutableState<MigrationToState?>.cleanUpOnBack(chatReceiver: MigrationToChatReceiver?) { + val state = state + if (state is MigrationToState.ArchiveImportFailed) { + // Original database is not exist, nothing is set up correctly for showing to a user yet. Return to clean state + deleteChatDatabaseFilesAndState() + initChatControllerOnStart() + } else if (state is MigrationToState.DownloadProgress && state.ctrl != null) { + stopArchiveDownloading(state.fileId, state.ctrl) + } + chatReceiver?.stopAndCleanUp() + getMigrationTempFilesDirectory().deleteRecursively() + MigrationToDeviceState.save(null) + chatModel.migrationState.value = null +} + +private fun strHasSimplexFileLink(text: String): Boolean = + text.startsWith("simplex:/file") || text.startsWith("https://simplex.chat/file") + +private fun fileForTemporaryDatabase(): File = + File(getMigrationTempFilesDirectory(), generateNewFileName("migration", "db", getMigrationTempFilesDirectory())) + +private fun archivePath(): String { + val archiveTime = Clock.System.now() + val ts = SimpleDateFormat("yyyy-MM-dd'T'HHmmss", Locale.US).format(Date.from(archiveTime.toJavaInstant())) + val archiveName = "simplex-chat.$ts.zip" + val archivePath = File(getMigrationTempFilesDirectory(), archiveName) + return archivePath.absolutePath +} + +private class MigrationToChatReceiver( + val ctrl: ChatCtrl, + val databaseUrl: File, + var receiveMessages: Boolean = true, + val processReceivedMsg: suspend (API) -> Unit +) { + fun start() { + Log.d(TAG, "MigrationChatReceiver startReceiver") + CoroutineScope(Dispatchers.IO).launch { + while (receiveMessages) { + try { + val msg = ChatController.recvMsg(ctrl) + if (msg != null && receiveMessages) { + val rhId = msg.rhId + Log.d(TAG, "processReceivedMsg: ${msg.responseType}") + chatModel.addTerminalItem(TerminalItem.resp(rhId, msg)) + val finishedWithoutTimeout = withTimeoutOrNull(60_000L) { + processReceivedMsg(msg) + } + if (finishedWithoutTimeout == null) { + Log.e(TAG, "Timeout reached while processing received message: " + msg.responseType) + if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.possible_slow_function_title), + text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.responseType + "\n" + Exception().stackTraceToString()), + shareText = true + ) + } + } + } + } catch (e: Exception) { + Log.e(TAG, "MigrationChatReceiver recvMsg/processReceivedMsg exception: " + e.stackTraceToString()) + } catch (e: Exception) { + Log.e(TAG, "MigrationChatReceiver recvMsg/processReceivedMsg throwable: " + e.stackTraceToString()) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), e.stackTraceToString()) + } + } + } + } + + fun stopAndCleanUp() { + Log.d(TAG, "MigrationChatReceiver.stop") + receiveMessages = false + chatCloseStore(ctrl) + File(databaseUrl.absolutePath + "_chat.db").delete() + File(databaseUrl.absolutePath + "_agent.db").delete() + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactLearnMore.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactLearnMore.kt index 2913f6ac79..077abd1b98 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactLearnMore.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactLearnMore.kt @@ -1,23 +1,27 @@ package chat.simplex.common.views.newchat -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.chatModel +import chat.simplex.common.ui.theme.DEFAULT_PADDING import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.views.helpers.AppBarTitle +import chat.simplex.common.views.helpers.KeyChangeEffect import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.views.onboarding.ReadableTextWithLink import chat.simplex.res.MR @Composable -fun AddContactLearnMore() { - Column( - Modifier.verticalScroll(rememberScrollState()), - ) { - AppBarTitle(stringResource(MR.strings.one_time_link)) +fun AddContactLearnMore(close: () -> Unit) { + ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) { + AppBarTitle(stringResource(MR.strings.one_time_link), withPadding = false) ReadableText(MR.strings.scan_qr_to_connect_to_contact) ReadableText(MR.strings.if_you_cant_meet_in_person) ReadableTextWithLink(MR.strings.read_more_in_user_guide_with_link, "https://simplex.chat/docs/guide/readme.html#connect-to-friends") } + KeyChangeEffect(chatModel.chatId.value) { + close() + } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt deleted file mode 100644 index 84080d5b97..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt +++ /dev/null @@ -1,186 +0,0 @@ -package chat.simplex.common.views.newchat - -import SectionBottomSpacer -import SectionTextFooter -import SectionView -import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.* -import dev.icerock.moko.resources.compose.painterResource -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.unit.dp -import chat.simplex.common.model.* -import chat.simplex.common.platform.shareText -import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.usersettings.* -import chat.simplex.res.MR - -@Composable -fun AddContactView( - chatModel: ChatModel, - rh: RemoteHostInfo?, - connReqInvitation: String, - contactConnection: MutableState<PendingContactConnection?> -) { - val clipboard = LocalClipboardManager.current - AddContactLayout( - rh = rh, - chatModel = chatModel, - incognitoPref = chatModel.controller.appPrefs.incognito, - connReq = connReqInvitation, - contactConnection = contactConnection, - learnMore = { - ModalManager.center.showModal { - Column( - Modifier - .fillMaxHeight() - .padding(horizontal = DEFAULT_PADDING), - verticalArrangement = Arrangement.SpaceBetween - ) { - AddContactLearnMore() - } - } - } - ) -} - -@Composable -fun AddContactLayout( - chatModel: ChatModel, - rh: RemoteHostInfo?, - incognitoPref: SharedPreference<Boolean>, - connReq: String, - contactConnection: MutableState<PendingContactConnection?>, - learnMore: () -> Unit -) { - val incognito = remember { mutableStateOf(incognitoPref.get()) } - - LaunchedEffect(incognito.value) { - withApi { - val contactConnVal = contactConnection.value - if (contactConnVal != null) { - chatModel.controller.apiSetConnectionIncognito(rh?.remoteHostId, contactConnVal.pccConnId, incognito.value)?.let { - contactConnection.value = it - chatModel.updateContactConnection(rh?.remoteHostId, it) - } - } - } - } - - Column( - Modifier - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.SpaceBetween, - ) { - AppBarTitle(stringResource(MR.strings.add_contact), hostDevice(rh?.remoteHostId)) - - SectionView(stringResource(MR.strings.one_time_link_short).uppercase()) { - if (connReq.isNotEmpty()) { - SimpleXLinkQRCode( - connReq, Modifier - .padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF) - .aspectRatio(1f) - ) - } else { - CircularProgressIndicator( - Modifier - .size(36.dp) - .padding(4.dp) - .align(Alignment.CenterHorizontally), - color = MaterialTheme.colors.secondary, - strokeWidth = 3.dp - ) - } - - IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } } - ShareLinkButton(connReq) - OneTimeLinkLearnMoreButton(learnMore) - } - SectionTextFooter(sharedProfileInfo(chatModel, incognito.value)) - - SectionBottomSpacer() - } -} - -@Composable -fun ShareLinkButton(connReqInvitation: String) { - val clipboard = LocalClipboardManager.current - SettingsActionItem( - painterResource(MR.images.ic_share), - stringResource(MR.strings.share_invitation_link), - click = { clipboard.shareText(simplexChatLink(connReqInvitation)) }, - iconColor = MaterialTheme.colors.primary, - textColor = MaterialTheme.colors.primary, - ) -} - -@Composable -fun OneTimeLinkLearnMoreButton(onClick: () -> Unit) { - SettingsActionItem( - painterResource(MR.images.ic_info), - stringResource(MR.strings.learn_more), - onClick, - ) -} - -@Composable -fun IncognitoToggle( - incognitoPref: SharedPreference<Boolean>, - incognito: MutableState<Boolean>, - onClickInfo: () -> Unit -) { - SettingsActionItemWithContent( - icon = if (incognito.value) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_theater_comedy), - text = null, - click = onClickInfo, - iconColor = if (incognito.value) Indigo else MaterialTheme.colors.secondary, - extraPadding = false - ) { - SharedPreferenceToggleWithIcon( - stringResource(MR.strings.incognito), - painterResource(MR.images.ic_info), - stopped = false, - onClickInfo = onClickInfo, - preference = incognitoPref, - preferenceState = incognito - ) - } -} - -fun sharedProfileInfo( - chatModel: ChatModel, - incognito: Boolean -): String { - val name = chatModel.currentUser.value?.displayName ?: "" - return if (incognito) { - generalGetString(MR.strings.connect__a_new_random_profile_will_be_shared) - } else { - String.format(generalGetString(MR.strings.connect__your_profile_will_be_shared), name) - } -} - -@Preview/*( - uiMode = Configuration.UI_MODE_NIGHT_YES, - showBackground = true, - name = "Dark Mode" -)*/ -@Composable -fun PreviewAddContactView() { - SimpleXTheme { - AddContactLayout( - rh = null, - chatModel = ChatModel, - incognitoPref = SharedPreference({ false }, {}), - connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", - contactConnection = mutableStateOf(PendingContactConnection.getSampleData()), - learnMore = {}, - ) - } -} 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 4f71e81b0d..3d913cf957 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 @@ -29,29 +29,35 @@ import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.* import java.net.URI @Composable -fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) { +fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, closeAll: () -> Unit) { val rhId = rh?.remoteHostId + val view = LocalMultiplatformView() AddGroupLayout( createGroup = { incognito, groupProfile -> - withApi { + hideKeyboard(view) + withBGApi { val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile) if (groupInfo != null) { - chatModel.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Group(groupInfo), chatItems = listOf())) - chatModel.chatItems.clear() - chatModel.chatItemStatuses.clear() - chatModel.chatId.value = groupInfo.id + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId = rhId, groupInfo) + chatModel.chatsContext.chatItems.clearAndNotify() + chatModel.chatsContext.chatItemStatuses.clear() + chatModel.chatId.value = groupInfo.id + } setGroupMembers(rhId, groupInfo, chatModel) - close.invoke() + closeAll.invoke() + if (!groupInfo.incognito) { ModalManager.end.showModalCloseable(true) { close -> AddGroupMembersView(rhId, groupInfo, creatingGroup = true, chatModel, close) } } else { ModalManager.end.showModalCloseable(true) { close -> - GroupLinkView(chatModel, rhId, groupInfo, connReqContact = null, memberRole = null, onGroupLinkUpdated = null, creatingGroup = true, close) + GroupLinkView(chatModel, rhId, groupInfo, connLinkContact = null, memberRole = null, onGroupLinkUpdated = null, creatingGroup = true, close) } } } @@ -78,10 +84,9 @@ fun AddGroupLayout( val focusRequester = remember { FocusRequester() } val incognito = remember { mutableStateOf(incognitoPref.get()) } - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { ModalBottomSheetLayout( scrimColor = Color.Black.copy(alpha = 0.12F), - modifier = Modifier.navigationBarsWithImePadding(), + modifier = Modifier.imePadding(), sheetContent = { GetImageBottomSheet( chosenImage, @@ -94,12 +99,7 @@ fun AddGroupLayout( sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) ) { ModalView(close = close) { - Column( - Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(horizontal = DEFAULT_PADDING) - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.create_secret_group_title), hostDevice(rhId)) Box( Modifier @@ -117,7 +117,7 @@ fun AddGroupLayout( } } } - Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Row(Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( stringResource(MR.strings.group_display_name_field), fontSize = 16.sp @@ -129,7 +129,9 @@ fun AddGroupLayout( } } } - ProfileNameField(displayName, "", { isValidDisplayName(it.trim()) }, focusRequester) + Box(Modifier.padding(horizontal = DEFAULT_PADDING)) { + ProfileNameField(displayName, "", { isValidDisplayName(it.trim()) }, focusRequester) + } Spacer(Modifier.height(8.dp)) SettingsActionItem( @@ -139,7 +141,8 @@ fun AddGroupLayout( createGroup(incognito.value, GroupProfile( displayName = displayName.value.trim(), fullName = "", - image = profileImage.value + image = profileImage.value, + groupPreferences = GroupPreferences(history = GroupPreference(GroupFeatureEnabled.ON)) )) }, textColor = MaterialTheme.colors.primary, @@ -158,13 +161,12 @@ fun AddGroupLayout( ) LaunchedEffect(Unit) { - delay(300) + delay(1000) focusRequester.requestFocus() } } } } - } } fun canCreateProfile(displayName: String): Boolean = displayName.trim().isNotEmpty() && isValidDisplayName(displayName.trim()) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt similarity index 57% rename from apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index 9f28074aef..330c80b7a2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -1,57 +1,62 @@ package chat.simplex.common.views.newchat -import SectionBottomSpacer import SectionItemView -import SectionTextFooter -import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.* -import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.unit.dp import chat.simplex.common.model.* import chat.simplex.common.platform.* -import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.usersettings.IncognitoView import chat.simplex.res.MR -import java.net.URI - -@Composable -expect fun ScanToConnectView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) +import kotlinx.coroutines.* enum class ConnectionLinkType { INVITATION, CONTACT, GROUP } suspend fun planAndConnect( - chatModel: ChatModel, rhId: Long?, - uri: URI, + shortOrFullLink: String, incognito: Boolean?, - close: (() -> Unit)? -) { - val connectionPlan = chatModel.controller.apiConnectPlan(rhId, uri.toString()) - if (connectionPlan != null) { + close: (() -> Unit)?, + cleanup: (() -> Unit)? = null, + filterKnownContact: ((Contact) -> Unit)? = null, + filterKnownGroup: ((GroupInfo) -> Unit)? = null, +): CompletableDeferred<Boolean> { + val completable = CompletableDeferred<Boolean>() + val close: (() -> Unit) = { + close?.invoke() + // if close was called, it means the connection was created + completable.complete(true) + } + val cleanup: (() -> Unit) = { + cleanup?.invoke() + completable.complete(!completable.isActive) + } + val result = chatModel.controller.apiConnectPlan(rhId, shortOrFullLink) + if (result != null) { + val (connectionLink, connectionPlan) = result + val link = strHasSingleSimplexLink(shortOrFullLink.trim()) + val linkText = if (link?.format is Format.SimplexLink) + "<br><br><u>${link.simplexLinkText(link.format.linkType, link.format.smpHosts)}</u>" + else + "" when (connectionPlan) { is ConnectionPlan.InvitationLink -> when (connectionPlan.invitationLinkPlan) { InvitationLinkPlan.Ok -> { Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, incognito=$incognito") if (incognito != null) { - connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) + connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } else { askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan, close, + chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_via_invitation_link), - text = AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)), - connectDestructive = false + text = generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) + linkText, + connectDestructive = false, + cleanup = cleanup, ) } } @@ -60,18 +65,21 @@ suspend fun planAndConnect( if (incognito != null) { AlertManager.privacySensitive.showAlertDialog( title = generalGetString(MR.strings.connect_plan_connect_to_yourself), - text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link), + text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link) + linkText, confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), - onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } }, + onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, + onDismiss = cleanup, + onDismissRequest = cleanup, destructive = true, hostDevice = hostDevice(rhId), ) } else { askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan, close, + chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_plan_connect_to_yourself), - text = AnnotatedString(generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link)), - connectDestructive = true + text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link) + linkText, + connectDestructive = true, + cleanup = cleanup, ) } } @@ -79,42 +87,54 @@ suspend fun planAndConnect( Log.d(TAG, "planAndConnect, .InvitationLink, .Connecting, incognito=$incognito") val contact = connectionPlan.invitationLinkPlan.contact_ if (contact != null) { - openKnownContact(chatModel, rhId, close, contact) - AlertManager.privacySensitive.showAlertMsg( - generalGetString(MR.strings.contact_already_exists), - String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName), - hostDevice = hostDevice(rhId), - ) + if (filterKnownContact != null) { + filterKnownContact(contact) + } else { + openKnownContact(chatModel, rhId, close, contact) + AlertManager.privacySensitive.showAlertMsg( + generalGetString(MR.strings.contact_already_exists), + String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + linkText, + hostDevice = hostDevice(rhId), + ) + cleanup() + } } else { AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.connect_plan_already_connecting), - generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link), + generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link) + linkText, hostDevice = hostDevice(rhId), ) + cleanup() } } is InvitationLinkPlan.Known -> { Log.d(TAG, "planAndConnect, .InvitationLink, .Known, incognito=$incognito") val contact = connectionPlan.invitationLinkPlan.contact - openKnownContact(chatModel, rhId, close, contact) - AlertManager.privacySensitive.showAlertMsg( - generalGetString(MR.strings.contact_already_exists), - String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName), - hostDevice = hostDevice(rhId), - ) + if (filterKnownContact != null) { + filterKnownContact(contact) + } else { + openKnownContact(chatModel, rhId, close, contact) + AlertManager.privacySensitive.showAlertMsg( + generalGetString(MR.strings.contact_already_exists), + String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + linkText, + hostDevice = hostDevice(rhId), + ) + cleanup() + } } } is ConnectionPlan.ContactAddress -> when (connectionPlan.contactAddressPlan) { ContactAddressPlan.Ok -> { Log.d(TAG, "planAndConnect, .ContactAddress, .Ok, incognito=$incognito") if (incognito != null) { - connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) + connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } else { askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan, close, + chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_via_contact_link), - text = AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)), - connectDestructive = false + text = generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) + linkText, + connectDestructive = false, + cleanup, ) } } @@ -123,18 +143,21 @@ suspend fun planAndConnect( if (incognito != null) { AlertManager.privacySensitive.showAlertDialog( title = generalGetString(MR.strings.connect_plan_connect_to_yourself), - text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address), + text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + linkText, confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), - onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } }, + onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, destructive = true, + onDismiss = cleanup, + onDismissRequest = cleanup, hostDevice = hostDevice(rhId), ) } else { askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan, close, + chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_plan_connect_to_yourself), - text = AnnotatedString(generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address)), - connectDestructive = true + text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + linkText, + connectDestructive = true, + cleanup = cleanup, ) } } @@ -143,50 +166,64 @@ suspend fun planAndConnect( if (incognito != null) { AlertManager.privacySensitive.showAlertDialog( title = generalGetString(MR.strings.connect_plan_repeat_connection_request), - text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address), + text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + linkText, confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), - onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } }, + onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, + onDismiss = cleanup, + onDismissRequest = cleanup, destructive = true, hostDevice = hostDevice(rhId), ) } else { askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan, close, + chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_plan_repeat_connection_request), - text = AnnotatedString(generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address)), - connectDestructive = true + text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + linkText, + connectDestructive = true, + cleanup = cleanup, ) } } is ContactAddressPlan.ConnectingProhibit -> { Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingProhibit, incognito=$incognito") val contact = connectionPlan.contactAddressPlan.contact - openKnownContact(chatModel, rhId, close, contact) - AlertManager.privacySensitive.showAlertMsg( - generalGetString(MR.strings.contact_already_exists), - String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName), - hostDevice = hostDevice(rhId), - ) + if (filterKnownContact != null) { + filterKnownContact(contact) + } else { + openKnownContact(chatModel, rhId, close, contact) + AlertManager.privacySensitive.showAlertMsg( + generalGetString(MR.strings.contact_already_exists), + String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + linkText, + hostDevice = hostDevice(rhId), + ) + cleanup() + } } is ContactAddressPlan.Known -> { Log.d(TAG, "planAndConnect, .ContactAddress, .Known, incognito=$incognito") val contact = connectionPlan.contactAddressPlan.contact - openKnownContact(chatModel, rhId, close, contact) - AlertManager.privacySensitive.showAlertMsg( - generalGetString(MR.strings.contact_already_exists), - String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName), - hostDevice = hostDevice(rhId), - ) + if (filterKnownContact != null) { + filterKnownContact(contact) + } else { + openKnownContact(chatModel, rhId, close, contact) + AlertManager.privacySensitive.showAlertMsg( + generalGetString(MR.strings.contact_already_exists), + String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + linkText, + hostDevice = hostDevice(rhId), + ) + cleanup() + } } is ContactAddressPlan.ContactViaAddress -> { Log.d(TAG, "planAndConnect, .ContactAddress, .ContactViaAddress, incognito=$incognito") val contact = connectionPlan.contactAddressPlan.contact if (incognito != null) { - close?.invoke() + close() connectContactViaAddress(chatModel, rhId, contact.contactId, incognito) } else { askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close, openChat = false) } + cleanup() } } is ConnectionPlan.GroupLink -> when (connectionPlan.groupLinkPlan) { @@ -195,42 +232,52 @@ suspend fun planAndConnect( if (incognito != null) { AlertManager.privacySensitive.showAlertDialog( title = generalGetString(MR.strings.connect_via_group_link), - text = generalGetString(MR.strings.you_will_join_group), + text = generalGetString(MR.strings.you_will_join_group) + linkText, confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), - onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } }, + onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, + onDismiss = cleanup, + onDismissRequest = cleanup, hostDevice = hostDevice(rhId), ) } else { askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan, close, + chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_via_group_link), - text = AnnotatedString(generalGetString(MR.strings.you_will_join_group)), - connectDestructive = false + text = generalGetString(MR.strings.you_will_join_group) + linkText, + connectDestructive = false, + cleanup = cleanup, ) } } is GroupLinkPlan.OwnLink -> { Log.d(TAG, "planAndConnect, .GroupLink, .OwnLink, incognito=$incognito") val groupInfo = connectionPlan.groupLinkPlan.groupInfo - ownGroupLinkConfirmConnect(chatModel, rhId, uri, incognito, connectionPlan, groupInfo, close) + if (filterKnownGroup != null) { + filterKnownGroup(groupInfo) + } else { + ownGroupLinkConfirmConnect(chatModel, rhId, connectionLink, linkText, incognito, connectionPlan, groupInfo, close, cleanup) + } } GroupLinkPlan.ConnectingConfirmReconnect -> { Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingConfirmReconnect, incognito=$incognito") if (incognito != null) { AlertManager.privacySensitive.showAlertDialog( title = generalGetString(MR.strings.connect_plan_repeat_join_request), - text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link), + text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText, confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), - onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } }, + onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, + onDismiss = cleanup, + onDismissRequest = cleanup, destructive = true, hostDevice = hostDevice(rhId), ) } else { askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan, close, + chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_plan_repeat_join_request), - text = AnnotatedString(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link)), - connectDestructive = true + text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText, + connectDestructive = true, + cleanup = cleanup, ) } } @@ -238,56 +285,83 @@ suspend fun planAndConnect( Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingProhibit, incognito=$incognito") val groupInfo = connectionPlan.groupLinkPlan.groupInfo_ if (groupInfo != null) { - AlertManager.privacySensitive.showAlertMsg( - generalGetString(MR.strings.connect_plan_group_already_exists), - String.format(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_vName), groupInfo.displayName) - ) + if (groupInfo.businessChat == null) { + AlertManager.privacySensitive.showAlertMsg( + generalGetString(MR.strings.connect_plan_group_already_exists), + String.format(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_vName), groupInfo.displayName) + linkText + ) + } else { + AlertManager.privacySensitive.showAlertMsg( + generalGetString(MR.strings.connect_plan_chat_already_exists), + String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), groupInfo.displayName) + linkText + ) + } } else { AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.connect_plan_already_joining_the_group), - generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link), + generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText, hostDevice = hostDevice(rhId), ) } + cleanup() } is GroupLinkPlan.Known -> { Log.d(TAG, "planAndConnect, .GroupLink, .Known, incognito=$incognito") val groupInfo = connectionPlan.groupLinkPlan.groupInfo - openKnownGroup(chatModel, rhId, close, groupInfo) - AlertManager.privacySensitive.showAlertMsg( - generalGetString(MR.strings.connect_plan_group_already_exists), - String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName), - hostDevice = hostDevice(rhId), + if (filterKnownGroup != null) { + filterKnownGroup(groupInfo) + } else { + openKnownGroup(chatModel, rhId, close, groupInfo) + if (groupInfo.businessChat == null) { + AlertManager.privacySensitive.showAlertMsg( + generalGetString(MR.strings.connect_plan_group_already_exists), + String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName) + linkText, + hostDevice = hostDevice(rhId), + ) + } else { + AlertManager.privacySensitive.showAlertMsg( + generalGetString(MR.strings.connect_plan_chat_already_exists), + String.format(generalGetString(MR.strings.connect_plan_you_are_already_connected_with_vName), groupInfo.displayName) + linkText, + hostDevice = hostDevice(rhId), + ) + } + cleanup() + } + } + } + is ConnectionPlan.Error -> { + Log.d(TAG, "planAndConnect, error ${connectionPlan.chatError}") + if (incognito != null) { + connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan = null, close, cleanup) + } else { + askCurrentOrIncognitoProfileAlert( + chatModel, rhId, connectionLink, connectionPlan = null, close, + title = generalGetString(MR.strings.connect_plan_connect_via_link), + connectDestructive = false, + cleanup = cleanup, ) } } } - } else { - Log.d(TAG, "planAndConnect, plan error") - if (incognito != null) { - connectViaUri(chatModel, rhId, uri, incognito, connectionPlan = null, close) - } else { - askCurrentOrIncognitoProfileAlert( - chatModel, rhId, uri, connectionPlan = null, close, - title = generalGetString(MR.strings.connect_plan_connect_via_link), - connectDestructive = false - ) - } } + return completable } suspend fun connectViaUri( chatModel: ChatModel, rhId: Long?, - uri: URI, + connLink: CreatedConnLink, incognito: Boolean, connectionPlan: ConnectionPlan?, - close: (() -> Unit)? -) { - val pcc = chatModel.controller.apiConnect(rhId, incognito, uri.toString()) - val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION + close: (() -> Unit)?, + cleanup: (() -> Unit)?, +): Boolean { + val pcc = chatModel.controller.apiConnect(rhId, incognito, connLink) + val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) ?: ConnectionLinkType.INVITATION else ConnectionLinkType.INVITATION if (pcc != null) { - chatModel.updateContactConnection(rhId, pcc) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnection(rhId, pcc) + } close?.invoke() AlertManager.privacySensitive.showAlertMsg( title = generalGetString(MR.strings.connection_request_sent), @@ -300,25 +374,29 @@ suspend fun connectViaUri( hostDevice = hostDevice(rhId), ) } + cleanup?.invoke() + return pcc != null } -fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType { +fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType? { return when(connectionPlan) { is ConnectionPlan.InvitationLink -> ConnectionLinkType.INVITATION is ConnectionPlan.ContactAddress -> ConnectionLinkType.CONTACT is ConnectionPlan.GroupLink -> ConnectionLinkType.GROUP + is ConnectionPlan.Error -> null } } fun askCurrentOrIncognitoProfileAlert( chatModel: ChatModel, rhId: Long?, - uri: URI, + connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan?, close: (() -> Unit)?, title: String, - text: AnnotatedString? = null, + text: String? = null, connectDestructive: Boolean, + cleanup: (() -> Unit)?, ) { AlertManager.privacySensitive.showAlertDialogButtonsColumn( title = title, @@ -328,37 +406,39 @@ fun askCurrentOrIncognitoProfileAlert( val connectColor = if (connectDestructive) MaterialTheme.colors.error else MaterialTheme.colors.primary SectionItemView({ AlertManager.privacySensitive.hideAlert() - withApi { - connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close) + withBGApi { + connectViaUri(chatModel, rhId, connectionLink, incognito = false, connectionPlan, close, cleanup) } }) { Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor) } SectionItemView({ AlertManager.privacySensitive.hideAlert() - withApi { - connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close) + withBGApi { + connectViaUri(chatModel, rhId, connectionLink, incognito = true, connectionPlan, close, cleanup) } }) { Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor) } SectionItemView({ AlertManager.privacySensitive.hideAlert() + cleanup?.invoke() }) { Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } } }, + onDismissRequest = cleanup, hostDevice = hostDevice(rhId), ) } fun openKnownContact(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, contact: Contact) { - withApi { + withBGApi { val c = chatModel.getContactChat(contact.contactId) if (c != null) { close?.invoke() - openDirectChat(rhId, contact.contactId, chatModel) + openDirectChat(rhId, contact.contactId) } } } @@ -366,21 +446,24 @@ fun openKnownContact(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, co fun ownGroupLinkConfirmConnect( chatModel: ChatModel, rhId: Long?, - uri: URI, + connectionLink: CreatedConnLink, + linkText: String, incognito: Boolean?, connectionPlan: ConnectionPlan?, groupInfo: GroupInfo, close: (() -> Unit)?, + cleanup: (() -> Unit)?, ) { AlertManager.privacySensitive.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.connect_plan_join_your_group), - text = AnnotatedString(String.format(generalGetString(MR.strings.connect_plan_this_is_your_link_for_group_vName), groupInfo.displayName)), + text = String.format(generalGetString(MR.strings.connect_plan_this_is_your_link_for_group_vName), groupInfo.displayName) + linkText, buttons = { Column { // Open group SectionItemView({ AlertManager.privacySensitive.hideAlert() openKnownGroup(chatModel, rhId, close, groupInfo) + cleanup?.invoke() }) { Text(generalGetString(MR.strings.connect_plan_open_group), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } @@ -388,8 +471,8 @@ fun ownGroupLinkConfirmConnect( // Join incognito / Join with current profile SectionItemView({ AlertManager.privacySensitive.hideAlert() - withApi { - connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) + withBGApi { + connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }) { Text( @@ -401,8 +484,8 @@ fun ownGroupLinkConfirmConnect( // Use current profile SectionItemView({ AlertManager.privacySensitive.hideAlert() - withApi { - connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close) + withBGApi { + connectViaUri(chatModel, rhId, connectionLink, incognito = false, connectionPlan, close, cleanup) } }) { Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) @@ -410,8 +493,8 @@ fun ownGroupLinkConfirmConnect( // Use new incognito profile SectionItemView({ AlertManager.privacySensitive.hideAlert() - withApi { - connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close) + withBGApi { + connectViaUri(chatModel, rhId, connectionLink, incognito = true, connectionPlan, close, cleanup) } }) { Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) @@ -420,95 +503,23 @@ fun ownGroupLinkConfirmConnect( // Cancel SectionItemView({ AlertManager.privacySensitive.hideAlert() + cleanup?.invoke() }) { Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } } }, + onDismissRequest = cleanup, hostDevice = hostDevice(rhId), ) } fun openKnownGroup(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, groupInfo: GroupInfo) { - withApi { + withBGApi { val g = chatModel.getGroupChat(groupInfo.groupId) if (g != null) { close?.invoke() - openGroupChat(rhId, groupInfo.groupId, chatModel) + openGroupChat(rhId, groupInfo.groupId) } } } - -@Composable -fun ConnectContactLayout( - chatModel: ChatModel, - rh: RemoteHostInfo?, - incognitoPref: SharedPreference<Boolean>, - close: () -> Unit -) { - val incognito = remember { mutableStateOf(incognitoPref.get()) } - - @Composable - fun QRCodeScanner(close: () -> Unit) { - QRCodeScanner { connReqUri -> - try { - val uri = URI(connReqUri) - withApi { - planAndConnect(chatModel, rh?.remoteHostId, uri, incognito = incognito.value, close) - } - } catch (e: RuntimeException) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.invalid_QR_code), - text = generalGetString(MR.strings.this_QR_code_is_not_a_link) - ) - } - } - } - - Column( - Modifier.verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING), - verticalArrangement = Arrangement.SpaceBetween - ) { - AppBarTitle(stringResource(MR.strings.scan_QR_code), hostDevice(rh?.remoteHostId), withPadding = false) - Box( - Modifier - .fillMaxWidth() - .aspectRatio(ratio = 1F) - .padding(bottom = 12.dp) - ) { QRCodeScanner(close) } - - IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } } - - SectionTextFooter( - buildAnnotatedString { - append(sharedProfileInfo(chatModel, incognito.value)) - append("\n\n") - append(annotatedStringResource(MR.strings.if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link)) - } - ) - - SectionBottomSpacer() - } -} - -fun URI.getQueryParameter(param: String): String? { - if (!query.contains("$param=")) return null - return query.substringAfter("$param=").substringBefore("&") -} - -@Preview/*( - uiMode = Configuration.UI_MODE_NIGHT_YES, - showBackground = true, - name = "Dark Mode" -)*/ -@Composable -fun PreviewConnectContactLayout() { - SimpleXTheme { - ConnectContactLayout( - chatModel = ChatModel, - rh = null, - incognitoPref = SharedPreference({ false }, {}), - close = {}, - ) - } -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectViaLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectViaLinkView.kt deleted file mode 100644 index 0077e2849c..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectViaLinkView.kt +++ /dev/null @@ -1,12 +0,0 @@ -package chat.simplex.common.views.newchat - -import androidx.compose.runtime.* -import chat.simplex.common.model.ChatModel -import chat.simplex.common.model.RemoteHostInfo - -enum class ConnectViaLinkTab { - SCAN, PASTE -} - -@Composable -expect fun ConnectViaLinkView(m: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt index 5e9495e866..0f299b5187 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt @@ -4,8 +4,8 @@ import SectionBottomSpacer import SectionDividerSpaced import SectionTextFooter import SectionView +import SectionViewWithButton import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -23,53 +23,50 @@ import chat.simplex.common.views.chatlist.deleteContactConnectionAlert import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel import chat.simplex.common.model.PendingContactConnection -import chat.simplex.common.platform.shareText +import chat.simplex.common.platform.* import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR +import kotlinx.coroutines.* @Composable fun ContactConnectionInfoView( chatModel: ChatModel, rhId: Long?, - connReqInvitation: String?, + connLinkInvitation: CreatedConnLink?, contactConnection: PendingContactConnection, focusAlias: Boolean, close: () -> Unit ) { - LaunchedEffect(connReqInvitation) { - chatModel.connReqInv.value = connReqInvitation + LaunchedEffect(connLinkInvitation) { + if (connLinkInvitation != null) { + chatModel.showingInvitation.value = ShowingInvitation(contactConnection.id, connLinkInvitation, false, conn = contactConnection) + } } - /** When [AddContactView] is open, we don't need to drop [chatModel.connReqInv]. - * Otherwise, it will be called here AFTER [AddContactView] is launched and will clear the value too soon. + /** When [AddContactLearnMore] is open, we don't need to drop [ChatModel.showingInvitation]. + * Otherwise, it will be called here AFTER [AddContactLearnMore] is launched and will clear the value too soon. * It will be dropped automatically when connection established or when user goes away from this screen. + * It applies only to Android because on Desktop center space will not be overlapped by [AddContactLearnMore] **/ DisposableEffect(Unit) { onDispose { - if (!ModalManager.center.hasModalsOpen()) { - chatModel.connReqInv.value = null + if (!ModalManager.center.hasModalsOpen() || appPlatform.isDesktop) { + chatModel.showingInvitation.value = null } } } - val clipboard = LocalClipboardManager.current + val showShortLink = remember { mutableStateOf(true) } ContactConnectionInfoLayout( chatModel = chatModel, - connReq = connReqInvitation, + connLink = connLinkInvitation, + showShortLink = showShortLink, contactConnection = contactConnection, focusAlias = focusAlias, rhId = rhId, deleteConnection = { deleteContactConnectionAlert(rhId, contactConnection, chatModel, close) }, onLocalAliasChanged = { setContactAlias(rhId, contactConnection, it, chatModel) }, - share = { if (connReqInvitation != null) clipboard.shareText(connReqInvitation) }, learnMore = { - ModalManager.center.showModal { - Column( - Modifier - .fillMaxHeight() - .padding(horizontal = DEFAULT_PADDING), - verticalArrangement = Arrangement.SpaceBetween - ) { - AddContactLearnMore() - } + ModalManager.end.showModalCloseable { close -> + AddContactLearnMore(close) } } ) @@ -78,13 +75,13 @@ fun ContactConnectionInfoView( @Composable private fun ContactConnectionInfoLayout( chatModel: ChatModel, - connReq: String?, + connLink: CreatedConnLink?, + showShortLink: MutableState<Boolean>, contactConnection: PendingContactConnection, focusAlias: Boolean, rhId: Long?, deleteConnection: () -> Unit, onLocalAliasChanged: (String) -> Unit, - share: () -> Unit, learnMore: () -> Unit, ) { @Composable fun incognitoEnabled() { @@ -92,7 +89,7 @@ private fun ContactConnectionInfoLayout( SettingsActionItemWithContent( icon = painterResource(MR.images.ic_theater_comedy_filled), text = null, - click = { ModalManager.start.showModal { IncognitoView() } }, + click = { ModalManager.end.showModal { IncognitoView() } }, iconColor = Indigo, extraPadding = false ) { @@ -108,10 +105,7 @@ private fun ContactConnectionInfoLayout( } } - Column( - Modifier - .verticalScroll(rememberScrollState()), - ) { + ColumnWithScrollBar { AppBarTitle( stringResource( if (contactConnection.initiated) MR.strings.you_invited_a_contact @@ -133,24 +127,26 @@ private fun ContactConnectionInfoLayout( LocalAliasEditor(contactConnection.id, contactConnection.localAlias, center = false, leadingIcon = true, focus = focusAlias, updateValue = onLocalAliasChanged) } - SectionView { - if (!connReq.isNullOrEmpty() && contactConnection.initiated) { - SimpleXLinkQRCode( - connReq, Modifier - .padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF) - .aspectRatio(1f) - ) + if (connLink != null && connLink.connFullLink.isNotEmpty() && contactConnection.initiated) { + Spacer(Modifier.height(DEFAULT_PADDING)) + SectionViewWithButton( + stringResource(MR.strings.one_time_link).uppercase(), + titleButton = if (connLink.connShortLink == null) null else {{ ToggleShortLinkButton(showShortLink) }} + ) { + SimpleXCreatedLinkQRCode(connLink, short = showShortLink.value) incognitoEnabled() - ShareLinkButton(connReq) + ShareLinkButton(connLink.simplexChatUri(short = showShortLink.value)) OneTimeLinkLearnMoreButton(learnMore) - } else { + } + } else { + SectionView { incognitoEnabled() OneTimeLinkLearnMoreButton(learnMore) } } SectionTextFooter(sharedProfileInfo(chatModel, contactConnection.incognito)) - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) DeleteButton(deleteConnection) @@ -158,6 +154,30 @@ private fun ContactConnectionInfoLayout( } } +@Composable +fun ShareLinkButton(linkUri: String) { + val clipboard = LocalClipboardManager.current + SettingsActionItem( + painterResource(MR.images.ic_share), + stringResource(MR.strings.share_invitation_link), + click = { + chatModel.showingInvitation.value = chatModel.showingInvitation.value?.copy(connChatUsed = true) + clipboard.shareText(simplexChatLink(linkUri)) + }, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) +} + +@Composable +fun OneTimeLinkLearnMoreButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_info), + stringResource(MR.strings.learn_more), + onClick, + ) +} + @Composable fun DeleteButton(onClick: () -> Unit) { SettingsActionItem( @@ -169,9 +189,11 @@ fun DeleteButton(onClick: () -> Unit) { ) } -private fun setContactAlias(rhId: Long?, contactConnection: PendingContactConnection, localAlias: String, chatModel: ChatModel) = withApi { +private fun setContactAlias(rhId: Long?, contactConnection: PendingContactConnection, localAlias: String, chatModel: ChatModel) = withBGApi { chatModel.controller.apiSetConnectionAlias(rhId, contactConnection.pccConnId, localAlias)?.let { - chatModel.updateContactConnection(rhId, it) + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnection(rhId, it) + } } } @@ -185,13 +207,13 @@ private fun PreviewContactConnectionInfoView() { SimpleXTheme { ContactConnectionInfoLayout( chatModel = ChatModel, - connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", + connLink = CreatedConnLink("https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", null), + showShortLink = remember { mutableStateOf(true) }, contactConnection = PendingContactConnection.getSampleData(), focusAlias = false, rhId = null, deleteConnection = {}, onLocalAliasChanged = {}, - share = {}, learnMore = {} ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/CreateLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/CreateLinkView.kt deleted file mode 100644 index 6f3caf4674..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/CreateLinkView.kt +++ /dev/null @@ -1,118 +0,0 @@ -package chat.simplex.common.views.newchat - -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.unit.sp -import chat.simplex.common.model.* -import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.usersettings.UserAddressView -import chat.simplex.res.MR - -enum class CreateLinkTab { - ONE_TIME, LONG_TERM -} - -@Composable -fun CreateLinkView(m: ChatModel, rh: RemoteHostInfo?, initialSelection: CreateLinkTab) { - val selection = remember { mutableStateOf(initialSelection) } - val connReqInvitation = rememberSaveable { m.connReqInv } - val contactConnection: MutableState<PendingContactConnection?> = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(null) } - val creatingConnReq = rememberSaveable { mutableStateOf(false) } - LaunchedEffect(selection.value) { - if ( - selection.value == CreateLinkTab.ONE_TIME - && connReqInvitation.value.isNullOrEmpty() - && contactConnection.value == null - && !creatingConnReq.value - ) { - createInvitation(m, rh?.remoteHostId, creatingConnReq, connReqInvitation, contactConnection) - } - } - /** When [AddContactView] is open, we don't need to drop [chatModel.connReqInv]. - * Otherwise, it will be called here AFTER [AddContactView] is launched and will clear the value too soon. - * It will be dropped automatically when connection established or when user goes away from this screen. - **/ - DisposableEffect(Unit) { - onDispose { - if (!ModalManager.center.hasModalsOpen()) { - m.connReqInv.value = null - } - } - } - val tabTitles = CreateLinkTab.values().map { - when { - it == CreateLinkTab.ONE_TIME && connReqInvitation.value.isNullOrEmpty() && contactConnection.value == null -> - stringResource(MR.strings.create_one_time_link) - it == CreateLinkTab.ONE_TIME -> - stringResource(MR.strings.one_time_link) - it == CreateLinkTab.LONG_TERM -> - stringResource(MR.strings.your_simplex_contact_address) - else -> "" - } - } - Column( - Modifier - .fillMaxHeight(), - verticalArrangement = Arrangement.SpaceBetween - ) { - Column(Modifier.weight(1f)) { - when (selection.value) { - CreateLinkTab.ONE_TIME -> { - AddContactView(m, rh,connReqInvitation.value ?: "", contactConnection) - } - CreateLinkTab.LONG_TERM -> { - UserAddressView(m, viaCreateLinkView = true, close = {}) - } - } - } - TabRow( - selectedTabIndex = selection.value.ordinal, - backgroundColor = Color.Transparent, - contentColor = MaterialTheme.colors.primary, - ) { - tabTitles.forEachIndexed { index, it -> - Tab( - selected = selection.value.ordinal == index, - onClick = { - selection.value = CreateLinkTab.values()[index] - }, - text = { Text(it, fontSize = 13.sp) }, - icon = { - Icon( - if (CreateLinkTab.ONE_TIME.ordinal == index) painterResource(MR.images.ic_repeat_one) else painterResource(MR.images.ic_all_inclusive), - it - ) - }, - selectedContentColor = MaterialTheme.colors.primary, - unselectedContentColor = MaterialTheme.colors.secondary, - ) - } - } - } -} - -private fun createInvitation( - m: ChatModel, - rhId: Long?, - creatingConnReq: MutableState<Boolean>, - connReqInvitation: MutableState<String?>, - contactConnection: MutableState<PendingContactConnection?> -) { - creatingConnReq.value = true - withApi { - val r = m.controller.apiAddContact(rhId, incognito = m.controller.appPrefs.incognito.get()) - if (r != null) { - m.updateContactConnection(rhId, r.second) - connReqInvitation.value = r.first - contactConnection.value = r.second - } else { - creatingConnReq.value = false - } - } -} 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 86929584c1..cb4991c99f 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 @@ -1,175 +1,738 @@ package chat.simplex.common.views.newchat -import androidx.compose.animation.* -import androidx.compose.animation.core.* +import SectionDividerSpaced +import SectionItemView +import SectionView +import TextIconSpaced import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.* 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.drawBehind +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.* +import androidx.compose.ui.focus.* import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.* +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.TextRange import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import chat.simplex.common.model.ChatModel +import androidx.compose.ui.unit.* +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.chat.topPaddingToContent +import chat.simplex.common.views.chatlist.* +import chat.simplex.common.views.contacts.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import kotlin.math.roundToInt +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter @Composable -fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedViewState>, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) { - // TODO close new chat if remote host changes in model - if (newChatSheetState.collectAsState().value.isVisible()) BackHandler { closeNewChatSheet(true) } - NewChatSheetLayout( - newChatSheetState, - stopped, - addContact = { - closeNewChatSheet(false) - ModalManager.center.closeModals() - ModalManager.center.showModal { CreateLinkView(chatModel, chatModel.currentRemoteHost.value, CreateLinkTab.ONE_TIME) } - }, - connectViaLink = { - closeNewChatSheet(false) - ModalManager.center.closeModals() - ModalManager.center.showModalCloseable { close -> ConnectViaLinkView(chatModel, chatModel.currentRemoteHost.value, close) } - }, - createGroup = { - closeNewChatSheet(false) - ModalManager.center.closeModals() - ModalManager.center.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close) } - }, - closeNewChatSheet, - ) +fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + + Box { + val closeAll = { ModalManager.start.closeModals() } + + Column(modifier = Modifier.fillMaxSize()) { + NewChatSheetLayout( + addContact = { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll) } + }, + scanPaste = { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = closeAll) } + }, + createGroup = { + ModalManager.start.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) } + }, + rh = rh, + close = close + ) + } + if (oneHandUI.value) { + Column(Modifier.align(Alignment.BottomCenter)) { + DefaultAppBar( + navigationButton = { NavigationButtonBack(onButtonClicked = close) }, + fixedTitleText = generalGetString(MR.strings.new_message), + onTop = false, + ) + } + } + } } -private val titles = listOf( - MR.strings.share_one_time_link, - if (appPlatform.isAndroid) MR.strings.connect_via_link_or_qr else MR.strings.connect_via_link, - MR.strings.create_group -) -private val icons = listOf(MR.images.ic_add_link, MR.images.ic_qr_code, MR.images.ic_group) +enum class ContactType { + CARD, REQUEST, RECENT, CHAT_DELETED, UNLISTED +} + +fun chatContactType(chat: Chat): ContactType { + return when (val cInfo = chat.chatInfo) { + is ChatInfo.ContactRequest -> ContactType.REQUEST + is ChatInfo.Direct -> { + val contact = cInfo.contact + + when { + contact.activeConn == null && contact.profile.contactLink != null && contact.active -> ContactType.CARD + contact.chatDeleted -> ContactType.CHAT_DELETED + contact.contactStatus == ContactStatus.Active -> ContactType.RECENT + else -> ContactType.UNLISTED + } + } + else -> ContactType.UNLISTED + } +} + +private fun filterContactTypes(c: List<Chat>, contactTypes: List<ContactType>): List<Chat> { + return c.filter { chat -> contactTypes.contains(chatContactType(chat)) } +} @Composable -private fun NewChatSheetLayout( - newChatSheetState: StateFlow<AnimatedViewState>, - stopped: Boolean, +private fun ModalData.NewChatSheetLayout( + rh: RemoteHostInfo?, addContact: () -> Unit, - connectViaLink: () -> Unit, + scanPaste: () -> Unit, createGroup: () -> Unit, - closeNewChatSheet: (animated: Boolean) -> Unit, + close: () -> Unit, ) { - var newChat by remember { mutableStateOf(newChatSheetState.value) } - val resultingColor = if (isInDarkTheme()) Color.Black.copy(0.64f) else DrawerDefaults.scrimColor - val animatedColor = remember { - Animatable( - if (newChat.isVisible()) Color.Transparent else resultingColor, - Color.VectorConverter(resultingColor.colorSpace) - ) - } - val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) } + val oneHandUI = remember { appPrefs.oneHandUI.state } + val listState = remember { appBarHandler.listState } + // This is workaround of an issue when position of a list is not restored (when going back to that screen) when a header exists. + // Upon returning back, this code returns correct index and position if number of items is the same LaunchedEffect(Unit) { - launch { - newChatSheetState.collect { - newChat = it - launch { - animatedColor.animateTo(if (newChat.isVisible()) resultingColor else Color.Transparent, newChatSheetAnimSpec()) + val prevIndex = listState.firstVisibleItemIndex + val prevOffset = listState.firstVisibleItemScrollOffset + val total = listState.layoutInfo.totalItemsCount + if (prevIndex == 0 && prevOffset == 0) return@LaunchedEffect + snapshotFlow { listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } + .filter { it == 0 to 0 } + .collect { + if (total <= listState.layoutInfo.totalItemsCount) { + listState.scrollToItem(prevIndex, prevOffset) } - launch { - animatedFloat.animateTo(if (newChat.isVisible()) 1f else 0f, newChatSheetAnimSpec()) - if (newChat.isHiding()) closeNewChatSheet(false) + cancel() + } + } + val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } + val searchShowingSimplexLink = remember { mutableStateOf(false) } + val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) } + val showUnreadAndFavorites = remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value + val baseContactTypes = remember { listOf(ContactType.CARD, ContactType.RECENT, ContactType.REQUEST) } + val contactTypes by remember(searchText.value.text.isEmpty()) { + derivedStateOf { contactTypesSearchTargets(baseContactTypes, searchText.value.text.isEmpty()) } + } + val allChats by remember(chatModel.chats.value, contactTypes) { + derivedStateOf { filterContactTypes(chatModel.chats.value, contactTypes) } + } + var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) } + var previousIndex by remember { mutableStateOf(0) } + var previousScrollOffset by remember { mutableStateOf(0) } + val keyboardState by getKeyboardState() + + LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) { + val currentIndex = listState.firstVisibleItemIndex + val currentScrollOffset = listState.firstVisibleItemScrollOffset + val threshold = 25 + + scrollDirection = when { + currentIndex > previousIndex -> ScrollDirection.Down + currentIndex < previousIndex -> ScrollDirection.Up + currentScrollOffset > previousScrollOffset + threshold -> ScrollDirection.Down + currentScrollOffset < previousScrollOffset - threshold -> ScrollDirection.Up + currentScrollOffset == previousScrollOffset -> ScrollDirection.Idle + else -> scrollDirection + } + + previousIndex = currentIndex + previousScrollOffset = currentScrollOffset + } + + val filteredContactChats = filteredContactChats( + showUnreadAndFavorites = showUnreadAndFavorites, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + searchShowingSimplexLink = searchShowingSimplexLink, + searchText = searchText.value.text, + contactChats = allChats + ) + + val sectionModifier = Modifier.fillMaxWidth() + val deletedContactTypes = listOf(ContactType.CHAT_DELETED) + val deletedChats by remember(chatModel.chats.value, deletedContactTypes) { + derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) } + } + + val actionButtonsOriginal = listOf( + Triple( + painterResource(MR.images.ic_add_link), + stringResource(MR.strings.create_1_time_link), + addContact, + ), + Triple( + painterResource(MR.images.ic_qr_code), + if (appPlatform.isAndroid) stringResource(MR.strings.scan_paste_link) else stringResource(MR.strings.paste_link), + scanPaste, + ), + Triple( + painterResource(MR.images.ic_group), + stringResource(MR.strings.create_group_button), + createGroup, + ) + ) + + @Composable + fun DeletedChatsItem(actionButtons: List<Triple<Painter, String, () -> Unit>>) { + if (searchText.value.text.isEmpty()) { + Spacer(Modifier.padding(bottom = 27.dp)) + } + + if (searchText.value.text.isEmpty()) { + Row { + SectionView { + actionButtons.map { + NewChatButton( + icon = it.first, + text = it.second, + click = it.third, + ) + } + } + } + if (deletedChats.isNotEmpty()) { + SectionDividerSpaced(maxBottomPadding = false) + SectionView { + SectionItemView( + click = { + ModalManager.start.showCustomModal { closeDeletedChats -> + ModalView( + close = closeDeletedChats, + showAppBar = !oneHandUI.value, + ) { + if (oneHandUI.value) { + BackHandler(onBack = closeDeletedChats) + } + DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = { + ModalManager.start.closeModals() + }) + } + } + } + ) { + Icon( + painterResource(MR.images.ic_inventory_2), + contentDescription = stringResource(MR.strings.deleted_chats), + tint = MaterialTheme.colors.secondary, + ) + TextIconSpaced(false) + Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground) + } } } } } - val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp - val maxWidth = with(LocalDensity.current) { windowWidth() * density } - Column( - Modifier - .fillMaxSize() - .padding(end = endPadding) - .offset { IntOffset(if (newChat.isGone()) -maxWidth.value.roundToInt() else 0, 0) } - .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) { closeNewChatSheet(true) } - .drawBehind { drawRect(animatedColor.value) }, - verticalArrangement = Arrangement.Bottom, - horizontalAlignment = Alignment.End - ) { - val actions = remember { listOf(addContact, connectViaLink, createGroup) } - val backgroundColor = if (isInDarkTheme()) - blendARGB(MaterialTheme.colors.primary, Color.Black, 0.7F) - else - MaterialTheme.colors.background - LazyColumn(Modifier - .graphicsLayer { - alpha = animatedFloat.value - translationY = (1 - animatedFloat.value) * 20.dp.toPx() - }) { - items(actions.size) { index -> - Row { - Spacer(Modifier.weight(1f)) - Box(contentAlignment = Alignment.CenterEnd) { - Button( - actions[index], - shape = RoundedCornerShape(21.dp), - colors = ButtonDefaults.textButtonColors(backgroundColor = backgroundColor), - elevation = null, - contentPadding = PaddingValues(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF), - modifier = Modifier.height(42.dp) - ) { - Text( - stringResource(titles[index]), - Modifier.padding(start = DEFAULT_PADDING_HALF), - color = if (isInDarkTheme()) MaterialTheme.colors.primary else MaterialTheme.colors.primary, - fontWeight = FontWeight.Medium, - ) - Icon( - painterResource(icons[index]), - stringResource(titles[index]), - Modifier.size(42.dp), - tint = if (isInDarkTheme()) MaterialTheme.colors.primary else MaterialTheme.colors.primary + + @Composable + fun NoFilteredContactsItem() { + if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) { + Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Text( + generalGetString(MR.strings.no_filtered_contacts), + color = MaterialTheme.colors.secondary + ) + } + } + } + } + + @Composable + fun OneHandLazyColumn() { + val blankSpaceSize = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier + LazyColumnWithScrollBar( + state = listState, + reverseLayout = oneHandUI.value + ) { + item { Spacer(Modifier.height(blankSpaceSize)) } + stickyHeader { + val scrolledSomething by remember { derivedStateOf { listState.firstVisibleItemScrollOffset > 0 || listState.firstVisibleItemIndex > 0 } } + Column( + Modifier + .zIndex(1f) + .offset { + val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened)) { + if (listState.firstVisibleItemIndex == 0) -minOf(listState.firstVisibleItemScrollOffset, blankSpaceSize.roundToPx()) + else -blankSpaceSize.roundToPx() + } else { + when (listState.firstVisibleItemIndex) { + 0 -> 0 + 1 -> listState.firstVisibleItemScrollOffset + else -> 1000 + } + } + IntOffset(0, y) + } + // show background when something is scrolled because otherwise the bar is transparent. + // not using background always because of gradient in SimpleX theme + .background( + if (scrolledSomething && (keyboardState == KeyboardState.Opened || searchText.value.text.isNotEmpty())) { + MaterialTheme.colors.background + } else { + Color.Unspecified + } + ) + ) { + Divider() + Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight * fontSizeSqrtMultiplier))) { + ContactsSearchBar( + listState = listState, + searchText = searchText, + searchShowingSimplexLink = searchShowingSimplexLink, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + ) + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) + } + } + } + item { + DeletedChatsItem(actionButtonsOriginal.asReversed()) + } + item { + if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) { + SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) + SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {} + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + } + } + item { + NoFilteredContactsItem() + } + itemsIndexed(filteredContactChats) { index, chat -> + val nextChatSelected = remember(chat.id, filteredContactChats) { + derivedStateOf { + chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value + } + } + ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true) + } + if (appPlatform.isAndroid) { + item { + Spacer(Modifier.windowInsetsTopHeight(WindowInsets.statusBars)) + } + } + } + } + + @Composable + fun NonOneHandLazyColumn() { + val blankSpaceSize = topPaddingToContent(false) + LazyColumnWithScrollBar( + Modifier.imePadding(), + state = listState, + reverseLayout = false + ) { + item { + Box(Modifier.padding(top = blankSpaceSize)) { + AppBarTitle( + stringResource(MR.strings.new_message), + hostDevice(rh?.remoteHostId), + bottomPadding = DEFAULT_PADDING + ) + } + } + stickyHeader { + val scrolledSomething by remember { derivedStateOf { listState.firstVisibleItemScrollOffset > 0 || listState.firstVisibleItemIndex > 0 } } + Column( + Modifier + .zIndex(1f) + .offset { + val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened)) { + if (listState.firstVisibleItemIndex == 0) (listState.firstVisibleItemScrollOffset - (listState.layoutInfo.visibleItemsInfo[0].size - blankSpaceSize.roundToPx())).coerceAtLeast(0) + else blankSpaceSize.roundToPx() + } else { + when (listState.firstVisibleItemIndex) { + 0 -> 0 + 1 -> -listState.firstVisibleItemScrollOffset + else -> -1000 + } + } + IntOffset(0, y) + } + // show background when something is scrolled because otherwise the bar is transparent. + // not using background always because of gradient in SimpleX theme + .background( + if (scrolledSomething && (keyboardState == KeyboardState.Opened || searchText.value.text.isNotEmpty())) { + MaterialTheme.colors.background + } else { + Color.Unspecified + } + ) + ) { + Divider() + ContactsSearchBar( + listState = listState, + searchText = searchText, + searchShowingSimplexLink = searchShowingSimplexLink, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + ) + Divider() + } + } + item { + DeletedChatsItem(actionButtonsOriginal) + } + item { + if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) { + SectionDividerSpaced() + SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {} + } + } + item { + NoFilteredContactsItem() + } + itemsIndexed(filteredContactChats) { index, chat -> + val nextChatSelected = remember(chat.id, filteredContactChats) { + derivedStateOf { + chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value + } + } + ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true) + } + if (appPlatform.isAndroid) { + item { + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + } + } + } + } + + Box { + if (oneHandUI.value) { + OneHandLazyColumn() + StatusBarBackground() + } else { + NonOneHandLazyColumn() + NavigationBarBackground(oneHandUI.value, true) + } + } +} + +@Composable +private fun NewChatButton( + icon: Painter, + text: String, + click: () -> Unit, + textColor: Color = Color.Unspecified, + iconColor: Color = MaterialTheme.colors.primary, + disabled: Boolean = false +) { + SectionItemView(click, disabled = disabled) { + Row { + Icon(icon, text, tint = if (disabled) MaterialTheme.colors.secondary else iconColor) + TextIconSpaced(false) + Text(text, color = if (disabled) MaterialTheme.colors.secondary else textColor) + } + } +} + +@Composable +private fun ContactsSearchBar( + listState: LazyListState, + searchText: MutableState<TextFieldValue>, + searchShowingSimplexLink: MutableState<Boolean>, + searchChatFilteredBySimplexLink: MutableState<String?>, + close: () -> Unit, +) { + var focused by remember { mutableStateOf(false) } + + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + val focusRequester = remember { FocusRequester() } + Icon( + painterResource(MR.images.ic_search), + contentDescription = null, + Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(22.dp * fontSizeSqrtMultiplier), + tint = MaterialTheme.colors.secondary + ) + SearchTextField( + Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester), + placeholder = stringResource(MR.strings.search_or_paste_simplex_link), + alwaysVisible = true, + searchText = searchText, + trailingContent = null, + ) { + searchText.value = searchText.value.copy(it) + } + val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } } + if (hasText.value) { + val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() } + BackHandler(onBack = hideSearchOnBack) + KeyChangeEffect(chatModel.currentRemoteHost.value) { + hideSearchOnBack() + } + } else { + Row { + val padding = if (appPlatform.isDesktop) 0.dp else 7.dp + if (chatModel.chats.size > 0) { + ToggleFilterButton() + } + Spacer(Modifier.width(padding)) + } + } + val focusManager = LocalFocusManager.current + val keyboardState = getKeyboardState() + LaunchedEffect(keyboardState.value) { + if (keyboardState.value == KeyboardState.Closed && focused) { + focusManager.clearFocus() + } + } + val view = LocalMultiplatformView() + LaunchedEffect(Unit) { + snapshotFlow { searchText.value.text } + .distinctUntilChanged() + .collect { + val link = strHasSingleSimplexLink(it.trim()) + if (link != null) { + // if SimpleX link is pasted, show connection dialogue + hideKeyboard(view) + if (link.format is Format.SimplexLink) { + val linkText = + link.simplexLinkText(link.format.linkType, link.format.smpHosts) + searchText.value = + searchText.value.copy(linkText, selection = TextRange.Zero) + } + searchShowingSimplexLink.value = true + searchChatFilteredBySimplexLink.value = null + connect( + link = link.text, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + cleanup = { searchText.value = TextFieldValue() } + ) + } else if (!searchShowingSimplexLink.value || it.isEmpty()) { + if (it.isNotEmpty()) { + // if some other text is pasted, enter search mode + focusRequester.requestFocus() + } else if (listState.layoutInfo.totalItemsCount > 0) { + listState.scrollToItem(0) + } + searchShowingSimplexLink.value = false + searchChatFilteredBySimplexLink.value = null + } + } + } + } +} + +@Composable +private fun ToggleFilterButton() { + val pref = remember { appPrefs.showUnreadAndFavorites } + IconButton(onClick = { pref.set(!pref.get()) }) { + val sp16 = with(LocalDensity.current) { 16.sp.toDp() } + Icon( + painterResource(MR.images.ic_filter_list), + null, + tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.secondary, + modifier = Modifier + .padding(3.dp) + .background(color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50)) + .border(width = 1.dp, color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50)) + .padding(3.dp) + .size(sp16) + ) + } +} + +private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<String?>, close: () -> Unit, cleanup: (() -> Unit)?) { + withBGApi { + planAndConnect( + chatModel.remoteHostId(), + link, + incognito = null, + filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id }, + close = close, + cleanup = cleanup, + ) + } +} + +private fun filteredContactChats( + showUnreadAndFavorites: Boolean, + searchShowingSimplexLink: State<Boolean>, + searchChatFilteredBySimplexLink: State<String?>, + searchText: String, + contactChats: List<Chat> +): List<Chat> { + val linkChatId = searchChatFilteredBySimplexLink.value + val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase() + + return if (linkChatId != null) { + contactChats.filter { it.id == linkChatId } + } else { + contactChats.filter { chat -> + filterChat( + chat = chat, + searchText = s, + showUnreadAndFavorites = showUnreadAndFavorites + ) + } + } + .sortedWith(chatsByTypeComparator) +} + +private fun filterChat(chat: Chat, searchText: String, showUnreadAndFavorites: Boolean): Boolean { + var meetsPredicate = true + val s = searchText.trim().lowercase() + val cInfo = chat.chatInfo + + if (searchText.isNotEmpty()) { + meetsPredicate = cInfo.anyNameContains(s) + } + + if (showUnreadAndFavorites) { + meetsPredicate = meetsPredicate && (cInfo.chatSettings?.favorite ?: false) + } + + return meetsPredicate +} + +private val chatsByTypeComparator = Comparator<Chat> { chat1, chat2 -> + val chat1Type = chatContactType(chat1) + val chat2Type = chatContactType(chat2) + + when { + chat1Type.ordinal < chat2Type.ordinal -> -1 + chat1Type.ordinal > chat2Type.ordinal -> 1 + + else -> chat2.chatInfo.chatTs.compareTo(chat1.chatInfo.chatTs) + } +} + +private fun contactTypesSearchTargets(baseContactTypes: List<ContactType>, searchEmpty: Boolean): List<ContactType> { + return if (baseContactTypes.contains(ContactType.CHAT_DELETED) || searchEmpty) { + baseContactTypes + } else { + baseContactTypes + ContactType.CHAT_DELETED + } +} + +@Composable +private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Unit, close: () -> Unit) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + Box { + val listState = remember { appBarHandler.listState } + val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } + val searchShowingSimplexLink = remember { mutableStateOf(false) } + val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) } + val showUnreadAndFavorites = remember { appPrefs.showUnreadAndFavorites.state }.value + val allChats by remember(chatModel.chats.value) { + derivedStateOf { filterContactTypes(chatModel.chats.value, listOf(ContactType.CHAT_DELETED)) } + } + val filteredContactChats = filteredContactChats( + showUnreadAndFavorites = showUnreadAndFavorites, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + searchShowingSimplexLink = searchShowingSimplexLink, + searchText = searchText.value.text, + contactChats = allChats + ) + + Box { + val topPaddingToContent = topPaddingToContent(false) + LazyColumnWithScrollBar( + if (!oneHandUI.value) Modifier.imePadding() else Modifier, + contentPadding = PaddingValues( + top = if (!oneHandUI.value) topPaddingToContent else 0.dp, + bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp + ), + reverseLayout = oneHandUI.value, + ) { + item { + if (!oneHandUI.value) { + Box(contentAlignment = Alignment.Center) { + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.deleted_chats), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding ) } } - Spacer(Modifier.width(DEFAULT_PADDING)) } - Spacer(Modifier.height(DEFAULT_PADDING)) + item { + if (!oneHandUI.value) { + Divider() + ContactsSearchBar( + listState = listState, + searchText = searchText, + searchShowingSimplexLink = searchShowingSimplexLink, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + ) + } else { + Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) { + ContactsSearchBar( + listState = listState, + searchText = searchText, + searchShowingSimplexLink = searchShowingSimplexLink, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + ) + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) + } + } + Divider() + } + + item { + if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) { + Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Text( + generalGetString(MR.strings.no_filtered_contacts), + color = MaterialTheme.colors.secondary, + ) + } + } + } + } + + itemsIndexed(filteredContactChats) { index, chat -> + val nextChatSelected = remember(chat.id, filteredContactChats) { + derivedStateOf { + chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value + } + } + ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = false) + } + if (appPlatform.isAndroid) { + item { + Spacer(if (oneHandUI.value) Modifier.windowInsetsTopHeight(WindowInsets.statusBars) else Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + } + } + } + if (oneHandUI.value) { + StatusBarBackground() + } else { + NavigationBarBackground(oneHandUI.value, true) } } - FloatingActionButton( - onClick = { if (!stopped) closeNewChatSheet(true) }, - Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING), - elevation = FloatingActionButtonDefaults.elevation( - defaultElevation = 0.dp, - pressedElevation = 0.dp, - hoveredElevation = 0.dp, - focusedElevation = 0.dp, - ), - backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, - contentColor = Color.White - ) { - Icon( - painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), - Modifier.graphicsLayer { alpha = 1 - animatedFloat.value } - ) - Icon( - painterResource(MR.images.ic_close), stringResource(MR.strings.add_contact_or_create_group), - Modifier.graphicsLayer { alpha = animatedFloat.value } - ) + if (oneHandUI.value) { + Column(Modifier.align(Alignment.BottomCenter)) { + DefaultAppBar( + navigationButton = { NavigationButtonBack(onButtonClicked = closeDeletedChats) }, + fixedTitleText = generalGetString(MR.strings.deleted_chats), + onTop = false, + ) + } } } } @@ -182,7 +745,7 @@ fun ActionButton( disabled: Boolean = false, click: () -> Unit = {} ) { - Surface(shape = RoundedCornerShape(18.dp), color = Color.Transparent) { + Surface(shape = RoundedCornerShape(18.dp), color = Color.Transparent, contentColor = LocalContentColor.current) { Column( Modifier .clickable(onClick = click) @@ -227,7 +790,7 @@ fun ActionButton( disabled: Boolean = false, click: () -> Unit = {} ) { - Surface(modifier, shape = RoundedCornerShape(18.dp)) { + Surface(modifier, shape = RoundedCornerShape(18.dp), contentColor = LocalContentColor.current) { Column( Modifier .fillMaxWidth() @@ -267,13 +830,6 @@ fun ActionButton( @Composable private fun PreviewNewChatSheet() { SimpleXTheme { - NewChatSheetLayout( - MutableStateFlow(AnimatedViewState.VISIBLE), - stopped = false, - addContact = {}, - connectViaLink = {}, - createGroup = {}, - closeNewChatSheet = {}, - ) + ModalData().NewChatSheet(rh = null, close = {}) } } 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 new file mode 100644 index 0000000000..1b3138d21c --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -0,0 +1,766 @@ +package chat.simplex.common.views.newchat + +import SectionBottomSpacer +import SectionItemView +import SectionTextFooter +import SectionView +import SectionViewWithButton +import TextIconSpaced +import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicTextField +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.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.unit.sp +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.chat.topPaddingToContent +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.* +import chat.simplex.res.MR +import kotlinx.coroutines.* + +enum class NewChatOption { + INVITE, CONNECT +} + +@Composable +fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRCodeScanner: Boolean = false, close: () -> Unit) { + val selection = remember { stateGetOrPut("selection") { selection } } + val showQRCodeScanner = remember { stateGetOrPut("showQRCodeScanner") { showQRCodeScanner } } + val contactConnection: MutableState<PendingContactConnection?> = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(chatModel.showingInvitation.value?.conn) } + val connLinkInvitation by remember { derivedStateOf { chatModel.showingInvitation.value?.connLink ?: CreatedConnLink("", null) } } + val creatingConnReq = rememberSaveable { mutableStateOf(false) } + val pastedLink = rememberSaveable { mutableStateOf("") } + LaunchedEffect(selection.value) { + if ( + selection.value == NewChatOption.INVITE + && connLinkInvitation.connFullLink.isEmpty() + && contactConnection.value == null + && !creatingConnReq.value + ) { + createInvitation(rh?.remoteHostId, creatingConnReq, connLinkInvitation, contactConnection) + } + } + DisposableEffect(Unit) { + onDispose { + /** When [AddContactLearnMore] is open, we don't need to drop [ChatModel.showingInvitation]. + * Otherwise, it will be called here AFTER [AddContactLearnMore] is launched and will clear the value too soon. + * It will be dropped automatically when connection established or when user goes away from this screen. + **/ + if (chatModel.showingInvitation.value != null && ModalManager.start.openModalCount() <= 1) { + val conn = contactConnection.value + if (chatModel.showingInvitation.value?.connChatUsed == false && conn != null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.keep_unused_invitation_question), + text = generalGetString(MR.strings.you_can_view_invitation_link_again), + confirmText = generalGetString(MR.strings.delete_verb), + dismissText = generalGetString(MR.strings.keep_invitation_link), + destructive = true, + onConfirm = { + withBGApi { + val chatInfo = ChatInfo.ContactConnection(conn) + controller.deleteChat(Chat(remoteHostId = rh?.remoteHostId, chatInfo = chatInfo, chatItems = listOf())) + if (chatModel.chatId.value == chatInfo.id) { + chatModel.chatId.value = null + ModalManager.start.closeModals() + } + } + } + ) + } + chatModel.showingInvitation.value = null + } + } + } + val tabTitles = NewChatOption.values().map { + when(it) { + NewChatOption.INVITE -> + stringResource(MR.strings.one_time_link_short) + NewChatOption.CONNECT -> + stringResource(MR.strings.connect_via_link) + } + } + + 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] + } + 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, + ) + } + } + + 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() + } + } + } + } +} + +@Composable +private fun PrepareAndInviteView(rhId: Long?, contactConnection: MutableState<PendingContactConnection?>, connLinkInvitation: CreatedConnLink, creatingConnReq: MutableState<Boolean>) { + if (connLinkInvitation.connFullLink.isNotEmpty()) { + InviteView( + rhId, + connLinkInvitation = connLinkInvitation, + contactConnection = contactConnection, + ) + } else if (creatingConnReq.value) { + CreatingLinkProgressView() + } else { + RetryButton { createInvitation(rhId, creatingConnReq, connLinkInvitation, contactConnection) } + } +} + +@Composable +private fun CreatingLinkProgressView() { + DefaultProgressView(stringResource(MR.strings.creating_link)) +} + +private fun updateShownConnection(conn: PendingContactConnection) { + chatModel.showingInvitation.value = chatModel.showingInvitation.value?.copy( + conn = conn, + connId = conn.id, + connLink = conn.connLinkInv ?: CreatedConnLink("", null), + connChatUsed = true + ) +} + +@Composable +private fun RetryButton(onClick: () -> Unit) { + Column( + Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + IconButton(onClick, Modifier.size(30.dp)) { + Icon(painterResource(MR.images.ic_refresh), null) + } + Spacer(Modifier.height(DEFAULT_PADDING)) + Text(stringResource(MR.strings.retry_verb)) + } +} + +@Composable +private fun ProfilePickerOption( + title: String, + selected: Boolean, + disabled: Boolean, + onSelected: () -> Unit, + image: @Composable () -> Unit, + onInfo: (() -> Unit)? = null +) { + Row( + Modifier + .fillMaxWidth() + .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT + 8.dp) + .clickable(enabled = !disabled, onClick = onSelected) + .padding(horizontal = DEFAULT_PADDING, vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + image() + TextIconSpaced(false) + Text(title, modifier = Modifier.align(Alignment.CenterVertically)) + if (onInfo != null) { + Spacer(Modifier.padding(6.dp)) + Column(Modifier + .size(48.dp) + .clip(CircleShape) + .clickable( + enabled = !disabled, + onClick = { ModalManager.start.showModal { IncognitoView() } } + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + painterResource(MR.images.ic_info), + stringResource(MR.strings.incognito), + tint = MaterialTheme.colors.primary + ) + } + } + Spacer(Modifier.weight(1f)) + if (selected) { + Icon( + painterResource( + MR.images.ic_check + ), + title, + Modifier.size(20.dp), + tint = MaterialTheme.colors.primary, + ) + } + } + Divider( + Modifier.padding( + start = DEFAULT_PADDING_HALF, + end = DEFAULT_PADDING_HALF, + ) + ) +} + +@Composable +fun ActiveProfilePicker( + search: MutableState<String>, + contactConnection: PendingContactConnection?, + close: () -> Unit, + rhId: Long?, + showIncognito: Boolean = true +) { + val switchingProfile = remember { mutableStateOf(false) } + val incognito = remember { + chatModel.showingInvitation.value?.conn?.incognito ?: controller.appPrefs.incognito.get() + } + val selectedProfile by remember { chatModel.currentUser } + val searchTextOrPassword = rememberSaveable { search } + // Intentionally don't use derivedStateOf in order to NOT change an order after user was selected + val filteredProfiles = remember(searchTextOrPassword.value) { + filteredProfiles(chatModel.users.map { it.user }.sortedBy { !it.activeUser }, searchTextOrPassword.value) + } + + var progressByTimeout by rememberSaveable { mutableStateOf(false) } + + LaunchedEffect(switchingProfile.value) { + progressByTimeout = if (switchingProfile.value) { + delay(500) + switchingProfile.value + } else { + false + } + } + + @Composable + fun ProfilePickerUserOption(user: User) { + val selected = selectedProfile?.userId == user.userId && !incognito + + ProfilePickerOption( + title = user.chatViewName, + disabled = switchingProfile.value || selected, + selected = selected, + onSelected = { + switchingProfile.value = true + withApi { + try { + appPreferences.incognito.set(false) + var updatedConn: PendingContactConnection? = null; + + if (contactConnection != null) { + updatedConn = controller.apiChangeConnectionUser(rhId, contactConnection.pccConnId, user.userId) + if (updatedConn != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnection(rhId, updatedConn) + updateShownConnection(updatedConn) + } + } + } + + if ((contactConnection != null && updatedConn != null) || contactConnection == null) { + controller.changeActiveUser_( + rhId = user.remoteHostId, + toUserId = user.userId, + viewPwd = if (user.hidden) searchTextOrPassword.value else null + ) + + if (chatModel.currentUser.value?.userId != user.userId) { + AlertManager.shared.showAlertMsg(generalGetString( + MR.strings.switching_profile_error_title), + String.format(generalGetString(MR.strings.switching_profile_error_message), user.chatViewName) + ) + } + } + + if (updatedConn != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnection(user.remoteHostId, updatedConn) + } + } + + close() + } finally { + switchingProfile.value = false + } + } + }, + image = { ProfileImage(size = 42.dp, image = user.image) } + ) + } + + @Composable + fun IncognitoUserOption() { + ProfilePickerOption( + disabled = switchingProfile.value, + title = stringResource(MR.strings.incognito), + selected = incognito, + onSelected = { + if (incognito || switchingProfile.value || contactConnection == null) return@ProfilePickerOption + + switchingProfile.value = true + withApi { + try { + appPreferences.incognito.set(true) + val conn = controller.apiSetConnectionIncognito(rhId, contactConnection.pccConnId, true) + if (conn != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnection(rhId, conn) + updateShownConnection(conn) + } + close() + } + } finally { + switchingProfile.value = false + } + } + }, + image = { + Spacer(Modifier.width(8.dp)) + Icon( + painterResource(MR.images.ic_theater_comedy_filled), + contentDescription = stringResource(MR.strings.incognito), + Modifier.size(32.dp), + tint = Indigo, + ) + Spacer(Modifier.width(2.dp)) + }, + onInfo = { ModalManager.start.showModal { IncognitoView() } }, + ) + } + + BoxWithConstraints { + Column( + Modifier + .fillMaxSize() + .alpha(if (progressByTimeout) 0.6f else 1f) + ) { + LazyColumnWithScrollBar(Modifier.padding(top = topPaddingToContent(false)), userScrollEnabled = !switchingProfile.value) { + item { + val oneHandUI = remember { appPrefs.oneHandUI.state } + if (oneHandUI.value) { + Spacer(Modifier.padding(top = DEFAULT_PADDING + 5.dp)) + } + AppBarTitle(stringResource(MR.strings.select_chat_profile), hostDevice(rhId), bottomPadding = DEFAULT_PADDING) + } + val activeProfile = filteredProfiles.firstOrNull { it.activeUser } + + if (activeProfile != null) { + val otherProfiles = filteredProfiles.filter { it.userId != activeProfile.userId } + item { + when { + !showIncognito -> + ProfilePickerUserOption(activeProfile) + incognito -> { + IncognitoUserOption() + ProfilePickerUserOption(activeProfile) + } + else -> { + ProfilePickerUserOption(activeProfile) + IncognitoUserOption() + } + } + } + + itemsIndexed(otherProfiles) { _, p -> + ProfilePickerUserOption(p) + } + } else { + if (showIncognito) { + item { + IncognitoUserOption() + } + } + itemsIndexed(filteredProfiles) { _, p -> + ProfilePickerUserOption(p) + } + } + item { + Spacer(Modifier.imePadding().padding(bottom = DEFAULT_BOTTOM_PADDING)) + } + } + } + if (progressByTimeout) { + DefaultProgressView("") + } + } +} + +@Composable +private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contactConnection: MutableState<PendingContactConnection?>) { + val showShortLink = remember { mutableStateOf(true) } + Spacer(Modifier.height(10.dp)) + + 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() }) + } + + Spacer(Modifier.height(DEFAULT_PADDING)) + val incognito by remember(chatModel.showingInvitation.value?.conn?.incognito, controller.appPrefs.incognito.get()) { + derivedStateOf { + chatModel.showingInvitation.value?.conn?.incognito ?: controller.appPrefs.incognito.get() + } + } + val currentUser = remember { chatModel.currentUser }.value + + if (currentUser != null) { + SectionView(stringResource(MR.strings.new_chat_share_profile).uppercase(), headerBottomPadding = 5.dp) { + SectionItemView( + padding = PaddingValues( + top = 0.dp, + bottom = 0.dp, + start = 16.dp, + end = 16.dp + ), + click = { + ModalManager.start.showCustomModal(keyboardCoversBar = false) { close -> + val search = rememberSaveable { mutableStateOf("") } + ModalView( + { close() }, + showSearch = true, + searchAlwaysVisible = true, + onSearchValueChanged = { search.value = it }, + content = { + ActiveProfilePicker( + search = search, + close = close, + rhId = rhId, + contactConnection = contactConnection.value + ) + }) + } + } + ) { + if (incognito) { + Spacer(Modifier.width(8.dp)) + Icon( + painterResource(MR.images.ic_theater_comedy_filled), + contentDescription = stringResource(MR.strings.incognito), + tint = Indigo, + modifier = Modifier.size(32.dp) + ) + Spacer(Modifier.width(2.dp)) + } else { + ProfileImage(size = 42.dp, image = currentUser.image) + } + TextIconSpaced(false) + Text( + text = if (incognito) stringResource(MR.strings.incognito) else currentUser.chatViewName, + color = MaterialTheme.colors.onBackground + ) + Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.End) { + Icon( + painter = painterResource(MR.images.ic_arrow_forward_ios), + contentDescription = stringResource(MR.strings.new_chat_share_profile), + tint = MaterialTheme.colors.secondary, + ) + } + } + } + if (incognito) { + SectionTextFooter(generalGetString(MR.strings.connect__a_new_random_profile_will_be_shared)) + } + } +} + +@Composable +fun ToggleShortLinkButton(short: MutableState<Boolean>) { + Text( + stringResource(if (short.value) MR.strings.full_link_button_text else MR.strings.short_link_button_text), + modifier = Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { short.value = !short.value }, + style = MaterialTheme.typography.body2, fontSize = 14.sp, color = MaterialTheme.colors.primary + ) +} + +@Composable +fun AddContactLearnMoreButton() { + IconButton( + { + ModalManager.start.showModalCloseable { close -> + AddContactLearnMore(close) + } + } + ) { + Icon( + painterResource(MR.images.ic_info), + stringResource(MR.strings.learn_more), + tint = MaterialTheme.colors.primary + ) + } +} + +@Composable +private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState<Boolean>, pastedLink: MutableState<String>, close: () -> Unit) { + SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase(), headerBottomPadding = 5.dp) { + PasteLinkView(rhId, pastedLink, showQRCodeScanner, close) + } + + if (appPlatform.isAndroid) { + Spacer(Modifier.height(10.dp)) + + SectionView(stringResource(MR.strings.or_scan_qr_code).uppercase(), headerBottomPadding = 5.dp) { + QRCodeScanner(showQRCodeScanner) { text -> + val linkVerified = verifyOnly(text) + if (!linkVerified) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.invalid_qr_code), + text = generalGetString(MR.strings.code_you_scanned_is_not_simplex_link_qr_code) + ) + } + verifyAndConnect(rhId, text, close) + } + } + } +} + +@Composable +private fun PasteLinkView(rhId: Long?, pastedLink: MutableState<String>, showQRCodeScanner: MutableState<Boolean>, close: () -> Unit) { + if (pastedLink.value.isEmpty()) { + val clipboard = LocalClipboardManager.current + SectionItemView({ + val str = clipboard.getText()?.text ?: return@SectionItemView + val link = strHasSingleSimplexLink(str.trim()) + if (link != null) { + pastedLink.value = link.text + showQRCodeScanner.value = false + withBGApi { connect(rhId, link.text, close) { pastedLink.value = "" } } + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.invalid_contact_link), + text = generalGetString(MR.strings.the_text_you_pasted_is_not_a_link) + ) + } + }) { + Text(stringResource(MR.strings.tap_to_paste_link)) + } + } else { + LinkTextView(pastedLink.value, false) + } +} + +@Composable +fun LinkTextView(link: String, share: Boolean) { + val clipboard = LocalClipboardManager.current + Row(Modifier.fillMaxWidth().heightIn(min = DEFAULT_MIN_SECTION_ITEM_HEIGHT).padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) { + Box(Modifier.weight(1f).clickable { + chatModel.markShowingInvitationUsed() + clipboard.shareText(link) + }) { + BasicTextField( + value = link, + onValueChange = { }, + enabled = false, + textStyle = TextStyle(fontSize = 16.sp, color = MaterialTheme.colors.onBackground), + singleLine = true, + decorationBox = @Composable { innerTextField -> + TextFieldDefaults.TextFieldDecorationBox( + value = link, + innerTextField = innerTextField, + contentPadding = PaddingValues(), + label = null, + visualTransformation = VisualTransformation.None, + leadingIcon = null, + trailingIcon = null, + singleLine = true, + enabled = false, + isError = false, + interactionSource = remember { MutableInteractionSource() }, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified) + ) + }) + } + // Element Text() can add ellipsis (...) in random place of the string, sometimes even after half of width of a screen. + // So using BasicTextField + manual ... + Text("…", fontSize = 16.sp) + if (share) { + Spacer(Modifier.width(DEFAULT_PADDING)) + IconButton({ + chatModel.markShowingInvitationUsed() + clipboard.shareText(link) + }, Modifier.size(20.dp)) { + Icon(painterResource(MR.images.ic_share_filled), null, tint = MaterialTheme.colors.primary) + } + } + } +} + +private fun filteredProfiles(users: List<User>, searchTextOrPassword: String): List<User> { + val s = searchTextOrPassword.trim() + val lower = s.lowercase() + return users.filter { u -> + if ((u.activeUser || !u.hidden) && (s == "" || u.anyNameContains(lower))) { + true + } else { + correctPassword(u, s) + } + } +} + +private fun verifyOnly(text: String?): Boolean = text != null && strIsSimplexLink(text) + +private suspend fun verifyAndConnect(rhId: Long?, text: String?, close: () -> Unit): Boolean { + if (text != null && strIsSimplexLink(text)) { + return withContext(Dispatchers.Default) { + connect(rhId, text, close) + } + } + return false +} + +private suspend fun connect(rhId: Long?, link: String, close: () -> Unit, cleanup: (() -> Unit)? = null): Boolean = + planAndConnect( + rhId, + link, + close = close, + cleanup = cleanup, + incognito = null + ).await() + +private fun createInvitation( + rhId: Long?, + creatingConnReq: MutableState<Boolean>, + connLinkInvitation: CreatedConnLink, + contactConnection: MutableState<PendingContactConnection?> +) { + if (connLinkInvitation.connFullLink.isNotEmpty() || contactConnection.value != null || creatingConnReq.value) return + creatingConnReq.value = true + withBGApi { + val (r, alert) = controller.apiAddContact(rhId, incognito = controller.appPrefs.incognito.get()) + if (r != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContactConnection(rhId, r.second) + chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connLink = r.first, connChatUsed = false, conn = r.second) + contactConnection.value = r.second + } + } else { + creatingConnReq.value = false + if (alert != null) { + alert() + } + } + } +} + +fun strIsSimplexLink(str: String): Boolean { + val parsedMd = parseToMarkdown(str) + return parsedMd != null && parsedMd.size == 1 && parsedMd[0].format is Format.SimplexLink +} + +fun strHasSingleSimplexLink(str: String): FormattedText? { + val parsedMd = parseToMarkdown(str) ?: return null + val parsedLinks = parsedMd.filter { it.format?.isSimplexLink ?: false } + if (parsedLinks.size != 1) return null + + return parsedLinks[0] +} + +@Composable +fun IncognitoToggle( + incognitoPref: SharedPreference<Boolean>, + incognito: MutableState<Boolean>, + onClickInfo: () -> Unit +) { + SettingsActionItemWithContent( + icon = if (incognito.value) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_theater_comedy), + text = null, + click = onClickInfo, + iconColor = if (incognito.value) Indigo else MaterialTheme.colors.secondary, + extraPadding = false + ) { + SharedPreferenceToggleWithIcon( + stringResource(MR.strings.incognito), + painterResource(MR.images.ic_info), + stopped = false, + onClickInfo = onClickInfo, + preference = incognitoPref, + preferenceState = incognito + ) + } +} + +fun sharedProfileInfo( + chatModel: ChatModel, + incognito: Boolean +): String { + val name = chatModel.currentUser.value?.displayName ?: "" + return if (incognito) { + generalGetString(MR.strings.connect__a_new_random_profile_will_be_shared) + } else { + String.format(generalGetString(MR.strings.connect__your_profile_will_be_shared), name) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt deleted file mode 100644 index dacf937575..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt +++ /dev/null @@ -1,135 +0,0 @@ -package chat.simplex.common.views.newchat - -import SectionBottomSpacer -import SectionTextFooter -import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.unit.dp -import chat.simplex.common.model.* -import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.usersettings.IncognitoView -import chat.simplex.common.views.usersettings.SettingsActionItem -import chat.simplex.res.MR -import java.net.URI - -@Composable -fun PasteToConnectView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) { - val connectionLink = remember { mutableStateOf("") } - val clipboard = LocalClipboardManager.current - PasteToConnectLayout( - chatModel = chatModel, - rh = rh, - incognitoPref = chatModel.controller.appPrefs.incognito, - connectionLink = connectionLink, - pasteFromClipboard = { - connectionLink.value = clipboard.getText()?.text ?: return@PasteToConnectLayout - }, - close = close - ) -} - -@Composable -fun PasteToConnectLayout( - chatModel: ChatModel, - rh: RemoteHostInfo?, - incognitoPref: SharedPreference<Boolean>, - connectionLink: MutableState<String>, - pasteFromClipboard: () -> Unit, - close: () -> Unit -) { - val incognito = remember { mutableStateOf(incognitoPref.get()) } - val rhId = rh?.remoteHostId - fun connectViaLink(connReqUri: String) { - try { - val uri = URI(connReqUri) - withApi { - planAndConnect(chatModel, rhId, uri, incognito = incognito.value, close) - } - } catch (e: RuntimeException) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.invalid_connection_link), - text = generalGetString(MR.strings.this_string_is_not_a_connection_link) - ) - } - } - - Column( - Modifier.verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING), - verticalArrangement = Arrangement.SpaceBetween, - ) { - AppBarTitle(stringResource(MR.strings.connect_via_link), hostDevice(rhId), withPadding = false) - - Box(Modifier.padding(top = DEFAULT_PADDING, bottom = 6.dp)) { - TextEditor( - connectionLink, - Modifier.height(180.dp), - contentPadding = PaddingValues(), - placeholder = stringResource(MR.strings.paste_the_link_you_received_to_connect_with_your_contact) - ) - } - - if (connectionLink.value == "") { - SettingsActionItem( - painterResource(MR.images.ic_content_paste), - stringResource(MR.strings.paste_button), - click = pasteFromClipboard, - ) - } else { - SettingsActionItem( - painterResource(MR.images.ic_close), - stringResource(MR.strings.clear_verb), - click = { connectionLink.value = "" }, - ) - } - - SettingsActionItem( - painterResource(MR.images.ic_link), - stringResource(MR.strings.connect_button), - click = { connectViaLink(connectionLink.value) }, - textColor = MaterialTheme.colors.primary, - iconColor = MaterialTheme.colors.primary, - disabled = connectionLink.value.isEmpty() || connectionLink.value.trim().contains(" ") - ) - - IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } } - - SectionTextFooter( - buildAnnotatedString { - append(sharedProfileInfo(chatModel, incognito.value)) - append("\n\n") - append(annotatedStringResource(MR.strings.you_can_also_connect_by_clicking_the_link)) - } - ) - - SectionBottomSpacer() - } -} - - -@Preview/*( - uiMode = Configuration.UI_MODE_NIGHT_YES, - name = "Dark Mode" -)*/ -@Composable -fun PreviewPasteToConnectTextbox() { - SimpleXTheme { - PasteToConnectLayout( - chatModel = ChatModel, - rh = null, - incognitoPref = SharedPreference({ false }, {}), - connectionLink = remember { mutableStateOf("") }, - pasteFromClipboard = {}, - close = {} - ) - } -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt index 7f9fae60a3..bacb5ab802 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt @@ -12,56 +12,74 @@ import androidx.compose.ui.unit.dp import dev.icerock.moko.resources.compose.stringResource import boofcv.alg.drawing.FiducialImageEngine import boofcv.alg.fiducial.qrcode.* -import chat.simplex.common.model.CryptoFile +import chat.simplex.common.model.* import chat.simplex.common.platform.* -import chat.simplex.common.ui.theme.SimpleXTheme +import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import kotlinx.coroutines.launch +@Composable +fun SimpleXCreatedLinkQRCode( + connLink: CreatedConnLink, + short: Boolean, + modifier: Modifier = Modifier, + padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING * 2f, vertical = DEFAULT_PADDING_HALF), + tintColor: Color = Color(0xff062d56), + withLogo: Boolean = true, + onShare: (() -> Unit)? = null, +) { + QRCode( + connLink.simplexChatUri(short), + modifier, + padding, + tintColor, + withLogo, + onShare, + ) +} + @Composable fun SimpleXLinkQRCode( connReq: String, modifier: Modifier = Modifier, + padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING * 2f, vertical = DEFAULT_PADDING_HALF), tintColor: Color = Color(0xff062d56), - withLogo: Boolean = true + withLogo: Boolean = true, + onShare: (() -> Unit)? = null, ) { QRCode( simplexChatLink(connReq), modifier, + padding, tintColor, - withLogo + withLogo, + onShare, ) } -fun simplexChatLink(uri: String): String { - return if (uri.startsWith("simplex:/")) { - uri.replace("simplex:/", "https://simplex.chat/") - } else { - uri - } -} - @Composable fun QRCode( connReq: String, modifier: Modifier = Modifier, + padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING * 2f, vertical = DEFAULT_PADDING_HALF), tintColor: Color = Color(0xff062d56), - withLogo: Boolean = true + withLogo: Boolean = true, + onShare: (() -> Unit)? = null, ) { val scope = rememberCoroutineScope() - - BoxWithConstraints(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { - val maxWidthInPx = with(LocalDensity.current) { maxWidth.roundToPx() } - val qr = remember(maxWidthInPx, connReq, tintColor, withLogo) { - qrCodeBitmap(connReq, maxWidthInPx).replaceColor(Color.Black.toArgb(), tintColor.toArgb()) - .let { if (withLogo) it.addLogo() else it } - } + val qr = remember(connReq, tintColor, withLogo) { + qrCodeBitmap(connReq, 1024).replaceColor(Color.Black.toArgb(), tintColor.toArgb()) + .let { if (withLogo) it.addLogo() else it } + } + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Image( bitmap = qr, contentDescription = stringResource(MR.strings.image_descr_qr_code), Modifier - .widthIn(max = 360.dp) + .padding(padding) + .widthIn(max = 400.dp) + .aspectRatio(1f) .then(modifier) .clickable { scope.launch { @@ -70,6 +88,7 @@ fun QRCode( val file = saveTempImageUncompressed(image, true) if (file != null) { shareFile("", CryptoFile.plain(file.absolutePath)) + onShare?.invoke() } } } @@ -81,7 +100,9 @@ fun qrCodeBitmap(content: String, size: Int = 1024): ImageBitmap { val qrCode = QrCodeEncoder().addAutomatic(content).setError(QrCode.ErrorLevel.L).fixate() /** See [QrCodeGeneratorImage.initialize] and [FiducialImageEngine.configure] for size calculation */ val numModules = QrCode.totalModules(qrCode.version) - val borderModule = 1 + // Hide border on light themes to make it fit to the same place as camera in QRCodeScanner. + // On dark themes better to show the border + val borderModule = if (CurrentColors.value.colors.isLight) 0 else 1 // val calculatedFinalWidth = (pixelsPerModule * numModules) + 2 * (borderModule * pixelsPerModule) // size = (x * numModules) + 2 * (borderModule * x) // size / x = numModules + 2 * borderModule diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.kt index 66ba595e17..f368edea1b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.kt @@ -1,6 +1,14 @@ package chat.simplex.common.views.newchat +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.* +import androidx.compose.ui.unit.dp +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF @Composable -expect fun QRCodeScanner(onBarcode: (String) -> Unit) +expect fun QRCodeScanner( + showQRCodeScanner: MutableState<Boolean> = remember { mutableStateOf(true) }, + padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING * 2f, vertical = DEFAULT_PADDING_HALF), + onBarcode: suspend (String) -> Boolean +) 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 new file mode 100644 index 0000000000..a14f163a91 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt @@ -0,0 +1,341 @@ +package chat.simplex.common.views.onboarding + +import SectionBottomSpacer +import SectionDividerSpaced +import SectionItemView +import SectionTextFooter +import SectionView +import TextIconSpaced +import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +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.* +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.networkAndServers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +@Composable +fun ModalData.OnboardingConditionsView() { + 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() } } + val selectedOperators = remember { derivedStateOf { serverOperators.value.filter { selectedOperatorIds.value.contains(it.operatorId) } } } + + 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) + } + + 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 + ) { + ModalManager.fullscreen.showModal(endButtons = { ConditionsLinkButton() }) { + SimpleConditionsView(rhId = null) + } + } + ) + } + 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(), selectedOperators, selectedOperatorIds) + TextButtonBelowOnboardingButton(stringResource(MR.strings.onboarding_conditions_configure_server_operators)) { + ModalManager.fullscreen.showModalCloseable { close -> + ChooseServerOperators(serverOperators, selectedOperatorIds, close) + } + } + } + } + } + } +} + +@Composable +fun ModalData.ChooseServerOperators( + serverOperators: State<List<ServerOperator>>, + selectedOperatorIds: MutableState<Set<Long>>, + close: (() -> Unit) +) { + LaunchedEffect(Unit) { + prepareChatBeforeFinishingOnboarding() + } + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ModalView({}, showClose = false) { + ColumnWithScrollBar( + Modifier + .themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer), + maxIntrinsicSize = true + ) { + Box(Modifier.align(Alignment.CenterHorizontally)) { + AppBarTitle(stringResource(MR.strings.onboarding_choose_server_operators), bottomPadding = DEFAULT_PADDING) + } + + Column(Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingInformationButton( + stringResource(MR.strings.how_it_helps_privacy), + onClick = { ModalManager.fullscreen.showModal { ChooseServerOperatorsInfoView() } } + ) + } + + Spacer(Modifier.weight(1f)) + Column(( + if (appPlatform.isDesktop) Modifier.width(600.dp).align(Alignment.CenterHorizontally) else Modifier) + .fillMaxWidth() + .padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING), + horizontalAlignment = Alignment.CenterHorizontally + ) { + serverOperators.value.forEachIndexed { index, srvOperator -> + OperatorCheckView(srvOperator, selectedOperatorIds) + if (index != serverOperators.value.lastIndex) { + Spacer(Modifier.height(DEFAULT_PADDING)) + } + } + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + + SectionTextFooter(annotatedStringResource(MR.strings.onboarding_network_operators_simplex_flux_agreement), textAlign = TextAlign.Center) + SectionTextFooter(annotatedStringResource(MR.strings.onboarding_network_operators_configure_via_settings), textAlign = TextAlign.Center) + } + Spacer(Modifier.weight(1f)) + + Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + val enabled = selectedOperatorIds.value.isNotEmpty() + SetOperatorsButton(enabled, close) + // Reserve space + TextButtonBelowOnboardingButton("", null) + } + } + } + } +} + +@Composable +private fun OperatorCheckView(serverOperator: ServerOperator, selectedOperatorIds: MutableState<Set<Long>>) { + val checked = selectedOperatorIds.value.contains(serverOperator.operatorId) + TextButton({ + if (checked) { + selectedOperatorIds.value -= serverOperator.operatorId + } else { + selectedOperatorIds.value += serverOperator.operatorId + } + }, + border = BorderStroke(1.dp, color = if (checked) MaterialTheme.colors.primary else MaterialTheme.colors.secondary.copy(alpha = 0.5f)), + shape = RoundedCornerShape(18.dp) + ) { + Row(Modifier.padding(DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically) { + Image(painterResource(serverOperator.largeLogo), null, Modifier.height(48.dp)) + Spacer(Modifier.width(DEFAULT_PADDING_HALF).weight(1f)) + CircleCheckbox(checked) + } + } +} + +@Composable +private fun CircleCheckbox(checked: Boolean) { + if (checked) { + Box(contentAlignment = Alignment.Center) { + Icon( + painterResource(MR.images.ic_circle_filled), + null, + Modifier.size(26.dp), + tint = MaterialTheme.colors.primary + ) + Icon( + painterResource(MR.images.ic_check_filled), + null, + Modifier.size(20.dp), tint = MaterialTheme.colors.background + ) + } + } else { + Icon( + painterResource(MR.images.ic_circle), + null, + Modifier.size(26.dp), + tint = MaterialTheme.colors.secondary.copy(alpha = 0.5f) + ) + } +} + +@Composable +private fun SetOperatorsButton(enabled: Boolean, close: () -> Unit) { + OnboardingActionButton( + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), + labelId = MR.strings.ok, + onboarding = null, + enabled = enabled, + onclick = { + close() + } + ) +} + +@Composable +private fun AcceptConditionsButton( + enabled: Boolean, + selectedOperators: State<List<ServerOperator>>, + selectedOperatorIds: State<Set<Long>> +) { + 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 acceptForOperators = selectedOperators.value.filter { !it.conditionsAcceptance.conditionsAccepted } + val operatorIds = acceptForOperators.map { it.operatorId } + val r = chatController.acceptConditions(chatModel.remoteHostId(), conditionsId = conditionsId, operatorIds = operatorIds) + 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() + } + } + } + } + ) +} + +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 enabledOperators(operators: List<ServerOperator>, selectedOperatorIds: Set<Long>): List<ServerOperator>? { + val ops = ArrayList(operators) + if (ops.isNotEmpty()) { + for (i in ops.indices) { + val op = ops[i] + ops[i] = op.copy(enabled = selectedOperatorIds.contains(op.operatorId)) + } + val haveSMPStorage = ops.any { it.enabled && it.smpRoles.storage } + val haveSMPProxy = ops.any { it.enabled && it.smpRoles.proxy } + val haveXFTPStorage = ops.any { it.enabled && it.xftpRoles.storage } + val haveXFTPProxy = ops.any { it.enabled && it.xftpRoles.proxy } + val firstEnabledIndex = ops.indexOfFirst { it.enabled } + if (haveSMPStorage && haveSMPProxy && haveXFTPStorage && haveXFTPProxy) { + return ops + } else if (firstEnabledIndex != -1) { + var op = ops[firstEnabledIndex] + if (!haveSMPStorage) op = op.copy(smpRoles = op.smpRoles.copy(storage = true)) + if (!haveSMPProxy) op = op.copy(smpRoles = op.smpRoles.copy(proxy = true)) + if (!haveXFTPStorage) op = op.copy(xftpRoles = op.xftpRoles.copy(storage = true)) + if (!haveXFTPProxy) op = op.copy(xftpRoles = op.xftpRoles.copy(proxy = true)) + ops[firstEnabledIndex] = op + return ops + } else { // Shouldn't happen - view doesn't let to proceed if no operators are enabled + return null + } + } else { + return null + } +} + +@Composable +private fun ChooseServerOperatorsInfoView() { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.onboarding_network_operators)) + + Column( + Modifier.padding(horizontal = DEFAULT_PADDING) + ) { + ReadableText(stringResource(MR.strings.onboarding_network_operators_app_will_use_different_operators)) + ReadableText(stringResource(MR.strings.onboarding_network_operators_cant_see_who_talks_to_whom)) + ReadableText(stringResource(MR.strings.onboarding_network_operators_app_will_use_for_routing)) + } + + SectionDividerSpaced() + + SectionView(title = stringResource(MR.strings.onboarding_network_about_operators).uppercase()) { + chatModel.conditions.value.serverOperators.forEach { op -> + ServerOperatorRow(op) + } + } + SectionBottomSpacer() + } +} + +@Composable +private fun ServerOperatorRow( + operator: ServerOperator +) { + SectionItemView( + { + ModalManager.fullscreen.showModalCloseable { close -> + OperatorInfoView(operator) + } + } + ) { + Image( + painterResource(operator.logo), + operator.tradeName, + modifier = Modifier.size(24.dp) + ) + TextIconSpaced() + Text(operator.tradeName) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt deleted file mode 100644 index 8534198028..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt +++ /dev/null @@ -1,189 +0,0 @@ -package chat.simplex.common.views.onboarding - -import SectionBottomSpacer -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalUriHandler -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import chat.simplex.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.SimpleXLinkQRCode -import chat.simplex.common.views.newchat.simplexChatLink -import chat.simplex.res.MR - -@Composable -fun CreateSimpleXAddress(m: ChatModel, rhId: Long?) { - var progressIndicator by remember { mutableStateOf(false) } - val userAddress = remember { m.userAddress } - val clipboard = LocalClipboardManager.current - val uriHandler = LocalUriHandler.current - - LaunchedEffect(Unit) { - prepareChatBeforeAddressCreation(rhId) - } - - CreateSimpleXAddressLayout( - userAddress.value, - share = { address: String -> clipboard.shareText(address) }, - sendEmail = { address -> - uriHandler.sendEmail( - generalGetString(MR.strings.email_invite_subject), - generalGetString(MR.strings.email_invite_body).format(simplexChatLink(address.connReqContact)) - ) - }, - createAddress = { - withApi { - progressIndicator = true - val connReqContact = m.controller.apiCreateUserAddress(rhId) - if (connReqContact != null) { - m.userAddress.value = UserContactLinkRec(connReqContact) - progressIndicator = false - } - } - }, - nextStep = { - val next = if (appPlatform.isAndroid) { - OnboardingStage.Step4_SetNotificationsMode - } else { - OnboardingStage.OnboardingComplete - } - m.controller.appPrefs.onboardingStage.set(next) - }, - ) - - if (progressIndicator) { - ProgressIndicator() - } -} - -@Composable -private fun CreateSimpleXAddressLayout( - userAddress: UserContactLinkRec?, - share: (String) -> Unit, - sendEmail: (UserContactLinkRec) -> Unit, - createAddress: () -> Unit, - nextStep: () -> Unit, -) { - Column( - Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(top = DEFAULT_PADDING), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AppBarTitle(stringResource(MR.strings.simplex_address)) - - Spacer(Modifier.weight(1f)) - - if (userAddress != null) { - SimpleXLinkQRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) - ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) } - Spacer(Modifier.weight(1f)) - ShareViaEmailButton { sendEmail(userAddress) } - Spacer(Modifier.weight(1f)) - ContinueButton(nextStep) - } else { - CreateAddressButton(createAddress) - TextBelowButton(stringResource(MR.strings.you_can_make_address_visible_via_settings)) - Spacer(Modifier.weight(1f)) - SkipButton(nextStep) - } - SectionBottomSpacer() - } -} - -@Composable -private fun CreateAddressButton(onClick: () -> Unit) { - TextButton(onClick) { - Text(stringResource(MR.strings.create_simplex_address), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary) - } -} - -@Composable -fun ShareAddressButton(onClick: () -> Unit) { - SimpleButtonFrame(onClick) { - Icon( - painterResource(MR.images.ic_share_filled), generalGetString(MR.strings.share_verb), tint = MaterialTheme.colors.primary, - modifier = Modifier.padding(end = 8.dp).size(18.dp) - ) - Text(stringResource(MR.strings.share_verb), style = MaterialTheme.typography.caption, color = MaterialTheme.colors.primary) - } -} - -@Composable -fun ShareViaEmailButton(onClick: () -> Unit) { - SimpleButtonFrame(onClick) { - Icon( - painterResource(MR.images.ic_mail), generalGetString(MR.strings.share_verb), tint = MaterialTheme.colors.primary, - modifier = Modifier.padding(end = 8.dp).size(30.dp) - ) - Text(stringResource(MR.strings.invite_friends), style = MaterialTheme.typography.h6, color = MaterialTheme.colors.primary) - } -} - -@Composable -private fun ContinueButton(onClick: () -> Unit) { - SimpleButtonIconEnded(stringResource(MR.strings.continue_to_next_step), painterResource(MR.images.ic_chevron_right), click = onClick) -} - -@Composable -private fun SkipButton(onClick: () -> Unit) { - SimpleButtonIconEnded(stringResource(MR.strings.dont_create_address), painterResource(MR.images.ic_chevron_right), click = onClick) - TextBelowButton(stringResource(MR.strings.you_can_create_it_later)) -} - -@Composable -private fun TextBelowButton(text: String) { - Text( - text, - Modifier - .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING * 3), - style = MaterialTheme.typography.subtitle1, - textAlign = TextAlign.Center, - ) -} - -@Composable -private fun ProgressIndicator() { - Box( - Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - Modifier - .padding(horizontal = 2.dp) - .size(30.dp), - color = MaterialTheme.colors.secondary, - strokeWidth = 3.dp - ) - } -} - -private fun prepareChatBeforeAddressCreation(rhId: Long?) { - if (chatModel.users.isNotEmpty()) return - withApi { - val user = chatModel.controller.apiGetActiveUser(rhId) ?: return@withApi - chatModel.currentUser.value = user - if (chatModel.users.isEmpty()) { - if (appPlatform.isDesktop) { - // Make possible to use chat after going to remote device linking and returning back to local profile creation - chatModel.chatRunning.value = false - } - chatModel.controller.startChat(user) - } else { - val users = chatModel.controller.listUsers(rhId) - chatModel.users.clear() - chatModel.users.addAll(users) - chatModel.controller.getUserChatData(rhId) - } - } -} 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 6c76acc3e8..aff02e90f5 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,6 +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.layout.* import androidx.compose.material.* @@ -14,6 +15,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.ui.theme.* import chat.simplex.common.views.chat.item.MarkdownText import chat.simplex.common.views.helpers.* @@ -22,35 +24,30 @@ import dev.icerock.moko.resources.StringResource @Composable fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>? = null) { - Column(Modifier - .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING), - ) { + ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) { AppBarTitle(stringResource(MR.strings.how_simplex_works), withPadding = false) - ReadableText(MR.strings.many_people_asked_how_can_it_deliver) ReadableText(MR.strings.to_protect_privacy_simplex_has_ids_for_queues) - ReadableText(MR.strings.you_control_servers_to_receive_your_contacts_to_send) 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") - } else { - ReadableText(MR.strings.read_more_in_github) } Spacer(Modifier.fillMaxHeight().weight(1f)) if (onboardingStage != null) { - Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING), contentAlignment = Alignment.Center) { + Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { OnboardingActionButton(user, onboardingStage, onclick = { ModalManager.fullscreen.closeModal() }) + // Reserve space + TextButtonBelowOnboardingButton("", null) } - Spacer(Modifier.fillMaxHeight().weight(1f)) } } } @Composable -fun ReadableText(stringResId: StringResource, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp), style: TextStyle = LocalTextStyle.current) { - Text(annotatedStringResource(stringResId), modifier = Modifier.padding(padding), textAlign = textAlign, lineHeight = 22.sp, style = style) +fun ReadableText(stringResId: StringResource, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp), style: TextStyle = LocalTextStyle.current, args: Any? = null) { + Text(annotatedStringResource(stringResId, args), modifier = Modifier.padding(padding), textAlign = textAlign, lineHeight = 22.sp, style = style) } @Composable @@ -78,17 +75,6 @@ fun ReadableText(text: String, textAlign: TextAlign = TextAlign.Start, padding: Text(text, modifier = Modifier.padding(padding), textAlign = textAlign, lineHeight = 22.sp) } -@Composable -fun ReadableMarkdownText(text: String, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp)) { - MarkdownText( - text, - formattedText = remember(text) { parseToMarkdown(text) }, - modifier = Modifier.padding(padding), - style = TextStyle(textAlign = textAlign, lineHeight = 22.sp, fontSize = 16.sp), - linkMode = ChatController.appPrefs.simplexLinkMode.get(), - ) -} - @Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, 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 4e3b70405d..9e48f4b2bd 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 @@ -7,9 +7,8 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.chatModel import chat.simplex.common.ui.theme.DEFAULT_PADDING @@ -18,7 +17,6 @@ import chat.simplex.common.views.remote.AddingMobileDevice import chat.simplex.common.views.remote.DeviceNameField import chat.simplex.common.views.usersettings.PreferenceToggle import chat.simplex.res.MR -import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @Composable @@ -56,37 +54,35 @@ private fun LinkAMobileLayout( staleQrCode: MutableState<Boolean>, updateDeviceName: (String) -> Unit, ) { - Column(Modifier.padding(top = 20.dp)) { - AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) - Row(Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING * 2), verticalAlignment = Alignment.CenterVertically) { - Column( - Modifier.weight(0.3f), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - SectionView(generalGetString(MR.strings.this_device_name).uppercase()) { - DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) } - SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile)) - PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), remember { ChatModel.controller.appPrefs.offerRemoteMulticast.state }.value) { - ChatModel.controller.appPrefs.offerRemoteMulticast.set(it) + 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)) + } + Row(Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING * 2), verticalAlignment = Alignment.CenterVertically) { + Column( + Modifier.weight(0.3f), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + SectionView(generalGetString(MR.strings.this_device_name).uppercase()) { + DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) } + SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile)) + PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), checked = remember { ChatModel.controller.appPrefs.offerRemoteMulticast.state }.value) { + ChatModel.controller.appPrefs.offerRemoteMulticast.set(it) + } } } - } - Box(Modifier.weight(0.7f)) { - AddingMobileDevice(false, staleQrCode, connecting) { - // currentRemoteHost will be set instantly but remoteHosts may be delayed - if (chatModel.remoteHosts.isEmpty() && chatModel.currentRemoteHost.value == null) { - chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) - } else { - chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete) + Box(Modifier.weight(0.7f)) { + AddingMobileDevice(false, staleQrCode, connecting) { + // currentRemoteHost will be set instantly but remoteHosts may be delayed + if (chatModel.remoteHosts.isEmpty() && chatModel.currentRemoteHost.value == null) { + chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + } else { + chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete) + } } } } } - SimpleButtonDecorated( - text = stringResource(MR.strings.about_simplex), - icon = painterResource(MR.images.ic_arrow_back_ios_new), - textDecoration = TextDecoration.None, - fontWeight = FontWeight.Medium - ) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } } } 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 d4c63248e5..510df13c3d 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 @@ -5,6 +5,7 @@ enum class OnboardingStage { Step2_CreateProfile, LinkAMobile, Step2_5_SetupDatabasePassphrase, + Step3_ChooseServerOperators, Step3_CreateSimpleXAddress, Step4_SetNotificationsMode, 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 aa413016d1..84f473067f 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 @@ -8,6 +8,7 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -15,37 +16,59 @@ 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.StringResource @Composable fun SetNotificationsMode(m: ChatModel) { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(vertical = 14.dp) - ) { - //CloseSheetBar(null) - AppBarTitle(stringResource(MR.strings.onboarding_notifications_mode_title)) - val currentMode = rememberSaveable { mutableStateOf(NotificationsMode.default) } - Column(Modifier.padding(horizontal = DEFAULT_PADDING * 1f)) { - Text(stringResource(MR.strings.onboarding_notifications_mode_subtitle), Modifier.fillMaxWidth(), textAlign = TextAlign.Center) - Spacer(Modifier.height(DEFAULT_PADDING * 2f)) - NotificationButton(currentMode, NotificationsMode.OFF, MR.strings.onboarding_notifications_mode_off, MR.strings.onboarding_notifications_mode_off_desc) - NotificationButton(currentMode, NotificationsMode.PERIODIC, MR.strings.onboarding_notifications_mode_periodic, MR.strings.onboarding_notifications_mode_periodic_desc) - NotificationButton(currentMode, NotificationsMode.SERVICE, MR.strings.onboarding_notifications_mode_service, MR.strings.onboarding_notifications_mode_service_desc) - } - Spacer(Modifier.fillMaxHeight().weight(1f)) - Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), contentAlignment = Alignment.Center) { - OnboardingActionButton(MR.strings.use_chat, OnboardingStage.OnboardingComplete, false) { - changeNotificationsMode(currentMode.value, m) + LaunchedEffect(Unit) { + prepareChatBeforeFinishingOnboarding() + } + + 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), + onClick = { ModalManager.fullscreen.showModalCloseable { NotificationBatteryUsageInfo() } } + ) + } + 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)) { + currentMode.value = NotificationsMode.SERVICE + } + SelectableCard(currentMode, NotificationsMode.PERIODIC, stringResource(MR.strings.onboarding_notifications_mode_periodic), annotatedStringResource(MR.strings.onboarding_notifications_mode_periodic_desc_short)) { + currentMode.value = NotificationsMode.PERIODIC + } + SelectableCard(currentMode, NotificationsMode.OFF, stringResource(MR.strings.onboarding_notifications_mode_off), annotatedStringResource(MR.strings.onboarding_notifications_mode_off_desc_short)) { + 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) { + 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() + } + ) + // Reserve space + TextButtonBelowOnboardingButton("", null) + } } } - Spacer(Modifier.fillMaxHeight().weight(1f)) } SetNotificationsModeAdditions() } @@ -54,22 +77,22 @@ fun SetNotificationsMode(m: ChatModel) { expect fun SetNotificationsModeAdditions() @Composable -private fun NotificationButton(currentMode: MutableState<NotificationsMode>, mode: NotificationsMode, title: StringResource, description: StringResource) { +fun <T> SelectableCard(currentValue: State<T>, newValue: T, title: String, description: AnnotatedString, onSelected: (T) -> Unit) { TextButton( - onClick = { currentMode.value = mode }, - border = BorderStroke(1.dp, color = if (currentMode.value == mode) MaterialTheme.colors.primary else MaterialTheme.colors.secondary.copy(alpha = 0.5f)), + 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)) { + Column(Modifier.padding(horizontal = 10.dp).padding(top = 4.dp, bottom = 8.dp).fillMaxWidth()) { Text( - stringResource(title), + title, style = MaterialTheme.typography.h3, fontWeight = FontWeight.Medium, - color = if (currentMode.value == mode) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + color = if (currentValue.value == newValue) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, modifier = Modifier.padding(bottom = 8.dp).align(Alignment.CenterHorizontally), textAlign = TextAlign.Center ) - Text(annotatedStringResource(description), + Text(description, Modifier.align(Alignment.CenterHorizontally), fontSize = 15.sp, color = MaterialTheme.colors.onBackground, @@ -80,3 +103,28 @@ private fun NotificationButton(currentMode: MutableState<NotificationsMode>, mod } Spacer(Modifier.height(14.dp)) } + +@Composable +private 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) + ReadableText(MR.strings.onboarding_notifications_mode_service_desc) + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + Text(stringResource(MR.strings.onboarding_notifications_mode_periodic), style = MaterialTheme.typography.h3, color = MaterialTheme.colors.secondary) + ReadableText(MR.strings.onboarding_notifications_mode_periodic_desc) + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + Text(stringResource(MR.strings.onboarding_notifications_mode_off), style = MaterialTheme.typography.h3, color = MaterialTheme.colors.secondary) + ReadableText(MR.strings.onboarding_notifications_mode_off_desc) + } +} + +fun prepareChatBeforeFinishingOnboarding() { + // No visible users but may have hidden. In this case chat should be started anyway because it's stopped on this stage with hidden users + if (chatModel.users.any { u -> !u.user.hidden }) return + withBGApi { + val user = chatModel.controller.apiGetActiveUser(null) ?: return@withBGApi + chatModel.currentUser.value = user + chatModel.controller.startChat(user) + } +} 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 a51d9c8a0c..c6eceb0ce2 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 @@ -1,11 +1,7 @@ package chat.simplex.common.views.onboarding -import SectionBottomSpacer -import SectionTextFooter import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -15,7 +11,6 @@ import androidx.compose.ui.focus.* import androidx.compose.ui.input.key.* import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction -import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -31,7 +26,6 @@ import kotlinx.coroutines.delay fun SetupDatabasePassphrase(m: ChatModel) { val progressIndicator = remember { mutableStateOf(false) } val prefs = m.controller.appPrefs - val saveInPreferences = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) } // Do not do rememberSaveable on current key to prevent saving it on disk in clear text val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") } @@ -39,7 +33,7 @@ fun SetupDatabasePassphrase(m: ChatModel) { val confirmNewKey = rememberSaveable { mutableStateOf("") } fun nextStep() { if (appPlatform.isAndroid || chatModel.currentUser.value != null) { - m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress) + m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_ChooseServerOperators) } else { m.controller.appPrefs.onboardingStage.set(OnboardingStage.LinkAMobile) } @@ -50,7 +44,7 @@ fun SetupDatabasePassphrase(m: ChatModel) { confirmNewKey, progressIndicator, onConfirmEncrypt = { - withApi { + withLongRunningApi { if (m.chatRunning.value == true) { // Stop chat if it's started before doing anything stopChatAsync(m) @@ -58,7 +52,16 @@ fun SetupDatabasePassphrase(m: ChatModel) { prefs.storeDBPassphrase.set(false) val newKeyValue = newKey.value - val success = encryptDatabase(currentKey, newKey, confirmNewKey, mutableStateOf(true), saveInPreferences, mutableStateOf(true), progressIndicator) + val success = encryptDatabase( + currentKey = currentKey, + newKey = newKey, + confirmNewKey = confirmNewKey, + initialRandomDBPassphrase = mutableStateOf(true), + useKeychain = mutableStateOf(false), + storedKey = mutableStateOf(true), + progressIndicator = progressIndicator, + migration = false + ) if (success) { startChat(newKeyValue) nextStep() @@ -98,114 +101,106 @@ private fun SetupDatabasePassphraseLayout( onConfirmEncrypt: () -> Unit, nextStep: () -> Unit, ) { - Column( - Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(top = DEFAULT_PADDING), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AppBarTitle(stringResource(MR.strings.setup_database_passphrase)) + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ModalView({}, showClose = false) { + ColumnWithScrollBar( + Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer).padding(horizontal = DEFAULT_PADDING), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarTitle(stringResource(MR.strings.setup_database_passphrase)) - Spacer(Modifier.weight(1f)) + val onClickUpdate = { + // Don't do things concurrently. Shouldn't be here concurrently, just in case + if (!progressIndicator.value) { + encryptDatabaseAlert(onConfirmEncrypt) + } + } + val disabled = currentKey.value == newKey.value || + newKey.value != confirmNewKey.value || + newKey.value.isEmpty() || + !validKey(currentKey.value) || + !validKey(newKey.value) || + progressIndicator.value - Column(Modifier.width(600.dp)) { - val focusRequester = remember { FocusRequester() } - val focusManager = LocalFocusManager.current - LaunchedEffect(Unit) { - delay(100L) - focusRequester.requestFocus() - } - PassphraseField( - newKey, - generalGetString(MR.strings.new_passphrase), - modifier = Modifier - .padding(horizontal = DEFAULT_PADDING) - .focusRequester(focusRequester) - .onPreviewKeyEvent { - if ((it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyUp) { - focusManager.moveFocus(FocusDirection.Down) - true - } else { - false + Column(Modifier.width(600.dp), horizontalAlignment = Alignment.CenterHorizontally) { + val textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary) + ReadableText(MR.strings.you_have_to_enter_passphrase_every_time, TextAlign.Center, padding = PaddingValues(), style = textStyle ) + Spacer(Modifier.height(DEFAULT_PADDING)) + ReadableText(MR.strings.impossible_to_recover_passphrase, TextAlign.Center, padding = PaddingValues(), style = textStyle) + Spacer(Modifier.height(DEFAULT_PADDING)) + + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + LaunchedEffect(Unit) { + delay(100L) + focusRequester.requestFocus() + } + PassphraseField( + newKey, + generalGetString(MR.strings.new_passphrase), + modifier = Modifier + .padding(horizontal = DEFAULT_PADDING) + .focusRequester(focusRequester) + .onPreviewKeyEvent { + if ((it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyUp) { + focusManager.moveFocus(FocusDirection.Down) + true + } else { + false + } + }, + showStrength = true, + isValid = ::validKey, + keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), + ) + + PassphraseField( + confirmNewKey, + generalGetString(MR.strings.confirm_new_passphrase), + modifier = Modifier + .padding(horizontal = DEFAULT_PADDING) + .onPreviewKeyEvent { + if (!disabled && (it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyUp) { + onClickUpdate() + true + } else { + false + } + }, + isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value }, + keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }), + ) + } + Spacer(Modifier.weight(1f)) + + Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp), horizontalAlignment = Alignment.CenterHorizontally) { + SetPassphraseButton(disabled, onClickUpdate) + SkipButton(progressIndicator.value) { + randomPassphraseAlert { + chatModel.desktopOnboardingRandomPassword.value = true + nextStep() } - }, - showStrength = true, - isValid = ::validKey, - keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), - ) - val onClickUpdate = { - // Don't do things concurrently. Shouldn't be here concurrently, just in case - if (!progressIndicator.value) { - encryptDatabaseAlert(onConfirmEncrypt) + } } } - val disabled = currentKey.value == newKey.value || - newKey.value != confirmNewKey.value || - newKey.value.isEmpty() || - !validKey(currentKey.value) || - !validKey(newKey.value) || - progressIndicator.value - - PassphraseField( - confirmNewKey, - generalGetString(MR.strings.confirm_new_passphrase), - modifier = Modifier - .padding(horizontal = DEFAULT_PADDING) - .onPreviewKeyEvent { - if (!disabled && (it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyUp) { - onClickUpdate() - true - } else { - false - } - }, - isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value }, - keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }), - ) - - Box(Modifier.align(Alignment.CenterHorizontally).padding(vertical = DEFAULT_PADDING)) { - SetPassphraseButton(disabled, onClickUpdate) - } - - Column { - SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time)) - SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase)) - } } - - Spacer(Modifier.weight(1f)) - SkipButton(progressIndicator.value) { - chatModel.desktopOnboardingRandomPassword.value = true - nextStep() - } - - SectionBottomSpacer() } } @Composable private fun SetPassphraseButton(disabled: Boolean, onClick: () -> Unit) { - SimpleButtonIconEnded( - stringResource(MR.strings.set_database_passphrase), - painterResource(MR.images.ic_check), - style = MaterialTheme.typography.h2, - color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary, - disabled = disabled, - click = onClick + OnboardingActionButton( + if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), + labelId = MR.strings.set_database_passphrase, + onboarding = null, + onclick = onClick, + enabled = !disabled ) } @Composable private fun SkipButton(disabled: Boolean, onClick: () -> Unit) { - SimpleButtonIconEnded(stringResource(MR.strings.use_random_passphrase), painterResource(MR.images.ic_chevron_right), color = - if (disabled) MaterialTheme.colors.secondary else WarningOrange, disabled = disabled, click = onClick) - Text( - stringResource(MR.strings.you_can_change_it_later), - Modifier - .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING * 3), - style = MaterialTheme.typography.subtitle1, - color = MaterialTheme.colors.secondary, - textAlign = TextAlign.Center, - ) + TextButtonBelowOnboardingButton(stringResource(MR.strings.use_random_passphrase), onClick = if (disabled) null else onClick) } @Composable @@ -230,3 +225,12 @@ private suspend fun startChat(key: String?) { m.chatDbChanged.value = false m.chatRunning.value = true } + +private fun randomPassphraseAlert(onConfirm: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.use_random_passphrase), + text = generalGetString(MR.strings.you_can_change_it_later), + confirmText = generalGetString(MR.strings.ok), + onConfirm = onConfirm, + ) +} 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 2aad2556af..e5d00fddd1 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 @@ -1,76 +1,95 @@ package chat.simplex.common.views.onboarding +import androidx.compose.animation.core.animateDpAsState import androidx.compose.desktop.ui.tooling.preview.Preview 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.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.layout +import androidx.compose.ui.text.TextLayoutResult import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.* 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.migration.MigrateToDeviceView +import chat.simplex.common.views.migration.MigrationToState import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource +import kotlin.math.ceil +import kotlin.math.floor @Composable fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) { - SimpleXInfoLayout( - user = chatModel.currentUser.value, - onboardingStage = if (onboarding) chatModel.controller.appPrefs.onboardingStage else null, - showModal = { modalView -> { if (onboarding) ModalManager.fullscreen.showModal { modalView(chatModel) } else ModalManager.start.showModal { modalView(chatModel) } } }, - ) + if (onboarding) { + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ModalView({}, showClose = false, showAppBar = false) { + SimpleXInfoLayout( + user = chatModel.currentUser.value, + onboardingStage = chatModel.controller.appPrefs.onboardingStage + ) + } + } + } else { + SimpleXInfoLayout( + user = chatModel.currentUser.value, + onboardingStage = null + ) + } } @Composable fun SimpleXInfoLayout( user: User?, - onboardingStage: SharedPreference<OnboardingStage>?, - showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + onboardingStage: SharedPreference<OnboardingStage>? ) { - Column( - Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(start = DEFAULT_PADDING , end = DEFAULT_PADDING, top = DEFAULT_PADDING), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 10.dp), contentAlignment = Alignment.Center) { + 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) { SimpleXLogo() } - Text(stringResource(MR.strings.next_generation_of_private_messaging), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = 48.dp).padding(horizontal = 36.dp), textAlign = TextAlign.Center) + 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 = 80.dp) - InfoRow(painterResource(MR.images.shield), MR.strings.immune_to_spam_and_abuse, MR.strings.people_can_connect_only_via_links_you_share) + InfoRow(painterResource(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) } - Spacer(Modifier.fillMaxHeight().weight(1f)) + Column(Modifier.fillMaxHeight().weight(1f)) { } if (onboardingStage != null) { - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + 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) } + } } - Spacer(Modifier.fillMaxHeight().weight(1f)) } - - Box( - Modifier - .fillMaxWidth() - .padding(bottom = DEFAULT_PADDING.times(1.5f), top = DEFAULT_PADDING), contentAlignment = Alignment.Center - ) { - SimpleButtonDecorated(text = stringResource(MR.strings.how_it_works), icon = painterResource(MR.images.ic_info), - click = showModal { HowItWorks(user, onboardingStage) }) + } + LaunchedEffect(Unit) { + if (chatModel.migrationState.value != null && !ModalManager.fullscreen.hasModalsOpen()) { + ModalManager.fullscreen.showCustomModal(animated = false) { close -> MigrateToDeviceView(close) } } } } @@ -80,21 +99,23 @@ fun SimpleXLogo() { Image( painter = painterResource(if (isInDarkTheme()) MR.images.logo_light else MR.images.logo), contentDescription = stringResource(MR.strings.image_descr_simplex_logo), + contentScale = ContentScale.FillWidth, modifier = Modifier .padding(vertical = DEFAULT_PADDING) - .fillMaxWidth(0.60f) + .fillMaxWidth() ) } @Composable -private fun InfoRow(icon: Painter, titleId: StringResource, textId: StringResource, width: Dp = 76.dp) { +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) - .padding(top = 8.dp, start = 8.dp, end = 24.dp)) - Column { + .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) + Text(stringResource(textId), lineHeight = 24.sp, style = MaterialTheme.typography.body1, color = MaterialTheme.colors.secondary) } } } @@ -104,38 +125,103 @@ expect fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference @Composable fun OnboardingActionButton( + modifier: Modifier = Modifier, labelId: StringResource, onboarding: OnboardingStage?, - border: Boolean, + enabled: Boolean = true, icon: Painter? = null, - iconColor: Color = MaterialTheme.colors.primary, + iconColor: Color = Color.White, onclick: (() -> Unit)? ) { - val modifier = if (border) { - Modifier - .border(border = BorderStroke(1.dp, MaterialTheme.colors.primary), shape = RoundedCornerShape(50)) - .padding( - horizontal = if (icon == null) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF, - vertical = 4.dp - ) - } else { - Modifier - } - - SimpleButtonFrame(click = { - onclick?.invoke() - if (onboarding != null) { - ChatController.appPrefs.onboardingStage.set(onboarding) - } - }, modifier) { + Button( + onClick = { + onclick?.invoke() + if (onboarding != null) { + appPrefs.onboardingStage.set(onboarding) + } + }, + modifier = modifier, + shape = CircleShape, + enabled = enabled, +// elevation = ButtonDefaults.elevation(defaultElevation = 0.dp, focusedElevation = 0.dp, pressedElevation = 0.dp, hoveredElevation = 0.dp), + contentPadding = PaddingValues(horizontal = if (icon == null) DEFAULT_PADDING * 2 else DEFAULT_PADDING * 1.5f, vertical = 17.dp), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary, disabledBackgroundColor = MaterialTheme.colors.secondary) + ) { if (icon != null) { Icon(icon, stringResource(labelId), Modifier.padding(end = DEFAULT_PADDING_HALF), tint = iconColor) } - Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary, fontSize = 20.sp) - Icon( - painterResource(MR.images.ic_arrow_forward_ios), "next stage", tint = MaterialTheme.colors.primary, - modifier = Modifier.padding(start = DEFAULT_PADDING.div(4)).size(20.dp) - ) + Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Medium) + } +} + +@Composable +fun TextButtonBelowOnboardingButton(text: String, onClick: (() -> Unit)?) { + 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) { + Text( + text, + Modifier.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING_HALF, bottom = 5.dp), + color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center + ) + } + } else { + // Hide from view when keyboard is open and move the view down + Spacer(Modifier.height(DEFAULT_PADDING * 2)) + } +} + +@Composable +fun OnboardingInformationButton( + text: String, + onClick: () -> Unit, +) { + Box( + modifier = Modifier + .clip(CircleShape) + .clickable { onClick() } + ) { + Row(Modifier.padding(8.dp), horizontalArrangement = Arrangement.spacedBy(4.dp)) { + Icon( + painterResource(MR.images.ic_info), + null, + tint = MaterialTheme.colors.primary + ) + // https://issuetracker.google.com/issues/206039942#comment32 + var textLayoutResult: TextLayoutResult? by remember { mutableStateOf(null) } + Text( + text, + Modifier + .layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + val newTextLayoutResult = textLayoutResult + + if (newTextLayoutResult == null || newTextLayoutResult.lineCount == 0) { + // Default behavior if there is no text or the text layout is not measured yet + layout(placeable.width, placeable.height) { + placeable.placeRelative(0, 0) + } + } else { + val minX = (0 until newTextLayoutResult.lineCount).minOf(newTextLayoutResult::getLineLeft) + val maxX = (0 until newTextLayoutResult.lineCount).maxOf(newTextLayoutResult::getLineRight) + + layout(ceil(maxX - minX).toInt(), placeable.height) { + placeable.place(-floor(minX).toInt(), 0) + } + } + }, + onTextLayout = { + textLayoutResult = it + }, + style = MaterialTheme.typography.button, + color = MaterialTheme.colors.primary + ) + } } } @@ -149,8 +235,7 @@ fun PreviewSimpleXInfo() { SimpleXTheme { SimpleXInfoLayout( user = null, - onboardingStage = null, - showModal = { {} } + onboardingStage = null ) } } 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 e1a81d925c..52eea3dd9d 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 @@ -8,35 +8,53 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalUriHandler import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.setConditionsNotified +import chat.simplex.common.model.ServerOperator.Companion.dummyOperatorInfo +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.networkAndServers.UsageConditionsView import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource @Composable -fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { +fun ModalData.WhatsNewView(updatedConditions: Boolean = false, viaSettings: Boolean = false, close: () -> Unit) { val currentVersion = remember { mutableStateOf(versionDescriptions.lastIndex) } + val rhId = chatModel.remoteHostId() + + if (updatedConditions) { + LaunchedEffect(Unit) { + val conditionsId = chatModel.conditions.value.currentConditions.conditionsId + try { + setConditionsNotified(rh = rhId, conditionsId = conditionsId) + } catch (e: Exception) { + Log.d(TAG, "WhatsNewView setConditionsNotified error: ${e.message}") + } + } + } @Composable - fun featureDescription(icon: Painter, titleId: StringResource, descrId: StringResource, link: String?) { + fun featureDescription(icon: ImageResource?, titleId: StringResource, descrId: StringResource?, link: String?, subfeatures: List<Pair<ImageResource, StringResource>>) { @Composable fun linkButton(link: String) { val uriHandler = LocalUriHandler.current Icon( painterResource(MR.images.ic_open_in_new), stringResource(titleId), tint = MaterialTheme.colors.primary, modifier = Modifier - .clickable { uriHandler.openUriCatching(link) } + .clickable { if (link.startsWith("simplex:")) uriHandler.openVerifiedSimplexUri(link) else uriHandler.openUriCatching(link) } ) } @@ -46,19 +64,30 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(bottom = 4.dp) ) { - Icon(icon, stringResource(titleId), tint = MaterialTheme.colors.secondary) + if (icon != null) Icon(painterResource(icon), stringResource(titleId), tint = MaterialTheme.colors.secondary) Text( generalGetString(titleId), maxLines = 2, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.h4, - fontWeight = FontWeight.Medium + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(bottom = 6.dp) ) if (link != null) { linkButton(link) } } - Text(generalGetString(descrId), fontSize = 15.sp) + if (descrId != null) Text(generalGetString(descrId), fontSize = 15.sp) + for ((si, sd) in subfeatures) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(bottom = 6.dp) + ) { + Icon(painterResource(si), stringResource(sd), tint = MaterialTheme.colors.secondary) + Text(generalGetString(sd), fontSize = 15.sp) + } + } } } @@ -105,26 +134,51 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { val v = versionDescriptions[currentVersion.value] ModalView(close = close) { - Column( + ColumnWithScrollBar( Modifier - .fillMaxSize() - .padding(horizontal = DEFAULT_PADDING) - .verticalScroll(rememberScrollState()), + .padding(horizontal = DEFAULT_PADDING), verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING.times(0.75f)) ) { - AppBarTitle(String.format(generalGetString(MR.strings.new_in_version), v.version), bottomPadding = DEFAULT_PADDING) + AppBarTitle(String.format(generalGetString(MR.strings.new_in_version), v.version), withPadding = false, bottomPadding = DEFAULT_PADDING) + + val modalManager = if (viaSettings) ModalManager.start else ModalManager.center v.features.forEach { feature -> - featureDescription(painterResource(feature.icon), feature.titleId, feature.descrId, feature.link) + when (feature) { + is VersionFeature.FeatureDescription -> { + if (feature.show) { + featureDescription(feature.icon, feature.titleId, feature.descrId, feature.link, feature.subfeatures) + } + } + is VersionFeature.FeatureView -> { + feature.view(modalManager) + } + } } - val uriHandler = LocalUriHandler.current if (v.post != null) { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = DEFAULT_PADDING.div(4))) { - Text(stringResource(MR.strings.whats_new_read_more), color = MaterialTheme.colors.primary, - modifier = Modifier.clickable { uriHandler.openUriCatching(v.post) }) - Icon(painterResource(MR.images.ic_open_in_new), stringResource(MR.strings.whats_new_read_more), tint = MaterialTheme.colors.primary) - } + ReadMoreButton(v.post) + } + + if (updatedConditions) { + Text( + stringResource(MR.strings.view_updated_conditions), + color = MaterialTheme.colors.primary, + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + modalManager.showModalCloseable { close -> + UsageConditionsView( + userServers = mutableStateOf(emptyList()), + currUserServers = mutableStateOf(emptyList()), + close = close, + rhId = rhId + ) + } + } + ) } if (!viaSettings) { @@ -132,12 +186,21 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { Box( Modifier.fillMaxWidth(), contentAlignment = Alignment.Center ) { - Text( - generalGetString(MR.strings.ok), - modifier = Modifier.clickable(onClick = close), - style = MaterialTheme.typography.h3, - color = MaterialTheme.colors.primary - ) + Box(Modifier.clip(RoundedCornerShape(20.dp))) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .clickable { close() } + .padding(8.dp) + ) { + Text( + generalGetString(MR.strings.ok), + style = MaterialTheme.typography.h3, + color = MaterialTheme.colors.primary + ) + } + } } Spacer(Modifier.fillMaxHeight().weight(1f)) } @@ -149,16 +212,45 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { } } -private data class FeatureDescription( - val icon: ImageResource, - val titleId: StringResource, - val descrId: StringResource, - val link: String? = null -) +@Composable +fun ReadMoreButton(url: String) { + val uriHandler = LocalUriHandler.current + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = DEFAULT_PADDING.div(4))) { + Text( + stringResource(MR.strings.whats_new_read_more), + color = MaterialTheme.colors.primary, + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + uriHandler.openUriCatching(url) + } + ) + Icon(painterResource(MR.images.ic_open_in_new), stringResource(MR.strings.whats_new_read_more), tint = MaterialTheme.colors.primary) + } +} + +private sealed class VersionFeature { + class FeatureDescription( + val icon: ImageResource?, + val titleId: StringResource, + val descrId: StringResource?, + var subfeatures: List<Pair<ImageResource, StringResource>> = listOf(), + val link: String? = null, + val show: Boolean = true + ): VersionFeature() + + class FeatureView( + val icon: ImageResource?, + val titleId: StringResource, + val view: @Composable (modalManager: ModalManager) -> Unit + ): VersionFeature() +} private data class VersionDescription( val version: String, - val features: List<FeatureDescription>, + val features: List<VersionFeature>, val post: String? = null, ) @@ -167,18 +259,18 @@ private val versionDescriptions: List<VersionDescription> = listOf( version = "v4.2", post = "https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_verified_user, titleId = MR.strings.v4_2_security_assessment, descrId = MR.strings.v4_2_security_assessment_desc, link = "https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html" ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_group, titleId = MR.strings.v4_2_group_links, descrId = MR.strings.v4_2_group_links_desc ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_check, titleId = MR.strings.v4_2_auto_accept_contact_requests, descrId = MR.strings.v4_2_auto_accept_contact_requests_desc @@ -189,22 +281,22 @@ private val versionDescriptions: List<VersionDescription> = listOf( version = "v4.3", post = "https://simplex.chat/blog/20221206-simplex-chat-v4.3-voice-messages.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_mic, titleId = MR.strings.v4_3_voice_messages, descrId = MR.strings.v4_3_voice_messages_desc ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_delete_forever, titleId = MR.strings.v4_3_irreversible_message_deletion, descrId = MR.strings.v4_3_irreversible_message_deletion_desc ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_wifi_tethering, titleId = MR.strings.v4_3_improved_server_configuration, descrId = MR.strings.v4_3_improved_server_configuration_desc ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_visibility_off, titleId = MR.strings.v4_3_improved_privacy_and_security, descrId = MR.strings.v4_3_improved_privacy_and_security_desc @@ -215,22 +307,22 @@ private val versionDescriptions: List<VersionDescription> = listOf( version = "v4.4", post = "https://simplex.chat/blog/20230103-simplex-chat-v4.4-disappearing-messages.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_timer, titleId = MR.strings.v4_4_disappearing_messages, descrId = MR.strings.v4_4_disappearing_messages_desc ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_pending, titleId = MR.strings.v4_4_live_messages, descrId = MR.strings.v4_4_live_messages_desc ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_verified_user, titleId = MR.strings.v4_4_verify_connection_security, descrId = MR.strings.v4_4_verify_connection_security_desc ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_translate, titleId = MR.strings.v4_4_french_interface, descrId = MR.strings.v4_4_french_interface_descr @@ -241,37 +333,36 @@ private val versionDescriptions: List<VersionDescription> = listOf( version = "v4.5", post = "https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_manage_accounts, titleId = MR.strings.v4_5_multiple_chat_profiles, descrId = MR.strings.v4_5_multiple_chat_profiles_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_edit_note, titleId = MR.strings.v4_5_message_draft, descrId = MR.strings.v4_5_message_draft_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_safety_divider, titleId = MR.strings.v4_5_transport_isolation, descrId = MR.strings.v4_5_transport_isolation_descr, link = "https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation" ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_task, titleId = MR.strings.v4_5_private_filenames, descrId = MR.strings.v4_5_private_filenames_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_battery_2_bar, titleId = MR.strings.v4_5_reduced_battery_usage, descrId = MR.strings.v4_5_reduced_battery_usage_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_translate, titleId = MR.strings.v4_5_italian_interface, descrId = MR.strings.v4_5_italian_interface_descr, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), @@ -279,36 +370,35 @@ private val versionDescriptions: List<VersionDescription> = listOf( version = "v4.6", post = "https://simplex.chat/blog/20230328-simplex-chat-v4-6-hidden-profiles.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_lock, titleId = MR.strings.v4_6_hidden_chat_profiles, descrId = MR.strings.v4_6_hidden_chat_profiles_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_flag, titleId = MR.strings.v4_6_group_moderation, descrId = MR.strings.v4_6_group_moderation_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_maps_ugc, titleId = MR.strings.v4_6_group_welcome_message, descrId = MR.strings.v4_6_group_welcome_message_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_call, titleId = MR.strings.v4_6_audio_video_calls, descrId = MR.strings.v4_6_audio_video_calls_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_battery_3_bar, titleId = MR.strings.v4_6_reduced_battery_usage, descrId = MR.strings.v4_6_reduced_battery_usage_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_translate, titleId = MR.strings.v4_6_chinese_spanish_interface, descrId = MR.strings.v4_6_chinese_spanish_interface_descr, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), @@ -316,21 +406,20 @@ private val versionDescriptions: List<VersionDescription> = listOf( version = "v5.0", post = "https://simplex.chat/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_upload_file, titleId = MR.strings.v5_0_large_files_support, descrId = MR.strings.v5_0_large_files_support_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_lock, titleId = MR.strings.v5_0_app_passcode, descrId = MR.strings.v5_0_app_passcode_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_translate, titleId = MR.strings.v5_0_polish_interface, descrId = MR.strings.v5_0_polish_interface_descr, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), @@ -338,31 +427,30 @@ private val versionDescriptions: List<VersionDescription> = listOf( version = "v5.1", post = "https://simplex.chat/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_add_reaction, titleId = MR.strings.v5_1_message_reactions, descrId = MR.strings.v5_1_message_reactions_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_chat, titleId = MR.strings.v5_1_better_messages, descrId = MR.strings.v5_1_better_messages_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_light_mode, titleId = MR.strings.v5_1_custom_themes, descrId = MR.strings.v5_1_custom_themes_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_lock, titleId = MR.strings.v5_1_self_destruct_passcode, descrId = MR.strings.v5_1_self_destruct_passcode_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_translate, titleId = MR.strings.v5_1_japanese_portuguese_interface, descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), @@ -370,27 +458,27 @@ private val versionDescriptions: List<VersionDescription> = listOf( version = "v5.2", post = "https://simplex.chat/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_check, titleId = MR.strings.v5_2_message_delivery_receipts, descrId = MR.strings.v5_2_message_delivery_receipts_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_star, titleId = MR.strings.v5_2_favourites_filter, descrId = MR.strings.v5_2_favourites_filter_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_sync_problem, titleId = MR.strings.v5_2_fix_encryption, descrId = MR.strings.v5_2_fix_encryption_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_timer, titleId = MR.strings.v5_2_disappear_one_message, descrId = MR.strings.v5_2_disappear_one_message_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_redeem, titleId = MR.strings.v5_2_more_things, descrId = MR.strings.v5_2_more_things_descr @@ -401,33 +489,32 @@ private val versionDescriptions: List<VersionDescription> = listOf( version = "v5.3", post = "https://simplex.chat/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_desktop, titleId = MR.strings.v5_3_new_desktop_app, descrId = MR.strings.v5_3_new_desktop_app_descr, link = "https://simplex.chat/downloads/" ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_lock, titleId = MR.strings.v5_3_encrypt_local_files, descrId = MR.strings.v5_3_encrypt_local_files_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_search, titleId = MR.strings.v5_3_discover_join_groups, descrId = MR.strings.v5_3_discover_join_groups_descr, link = "simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion" ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_theater_comedy, titleId = MR.strings.v5_3_simpler_incognito_mode, descrId = MR.strings.v5_3_simpler_incognito_mode_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_translate, titleId = MR.strings.v5_3_new_interface_languages, descrId = MR.strings.v5_3_new_interface_languages_descr, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), @@ -435,39 +522,310 @@ private val versionDescriptions: List<VersionDescription> = listOf( version = "v5.4", post = "https://simplex.chat/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_desktop, titleId = MR.strings.v5_4_link_mobile_desktop, descrId = MR.strings.v5_4_link_mobile_desktop_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_group, titleId = MR.strings.v5_4_better_groups, descrId = MR.strings.v5_4_better_groups_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_theater_comedy, titleId = MR.strings.v5_4_incognito_groups, descrId = MR.strings.v5_4_incognito_groups_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_back_hand, titleId = MR.strings.v5_4_block_group_members, descrId = MR.strings.v5_4_block_group_members_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_redeem, titleId = MR.strings.v5_2_more_things, descrId = MR.strings.v5_4_more_things_descr ) ) ), + VersionDescription( + version = "v5.5", + post = "https://simplex.chat/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.html", + features = listOf( + VersionFeature.FeatureDescription( + icon = MR.images.ic_folder_pen, + titleId = MR.strings.v5_5_private_notes, + descrId = MR.strings.v5_5_private_notes_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_link, + titleId = MR.strings.v5_5_simpler_connect_ui, + descrId = MR.strings.v5_5_simpler_connect_ui_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_forum, + titleId = MR.strings.v5_5_join_group_conversation, + descrId = MR.strings.v5_5_join_group_conversation_descr, + link = "simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion" + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_battery_3_bar, + titleId = MR.strings.v5_5_message_delivery, + descrId = MR.strings.v5_5_message_delivery_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_translate, + titleId = MR.strings.v5_5_new_interface_languages, + descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate, + ) + ) + ), + VersionDescription( + version = "v5.6", + post = "https://simplex.chat/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.html", + features = listOf( + VersionFeature.FeatureDescription( + icon = MR.images.ic_vpn_key_filled, + titleId = MR.strings.v5_6_quantum_resistant_encryption, + descrId = MR.strings.v5_6_quantum_resistant_encryption_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_ios_share, + titleId = MR.strings.v5_6_app_data_migration, + descrId = MR.strings.v5_6_app_data_migration_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_call, + titleId = MR.strings.v5_6_picture_in_picture_calls, + descrId = MR.strings.v5_6_picture_in_picture_calls_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_back_hand, + titleId = MR.strings.v5_6_safer_groups, + descrId = MR.strings.v5_6_safer_groups_descr + ) + ) + ), + VersionDescription( + version = "v5.7", + post = "https://simplex.chat/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.html", + features = listOf( + VersionFeature.FeatureDescription( + icon = MR.images.ic_vpn_key_filled, + titleId = MR.strings.v5_6_quantum_resistant_encryption, + descrId = MR.strings.v5_7_quantum_resistant_encryption_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_forward, + titleId = MR.strings.v5_7_forward, + descrId = MR.strings.v5_7_forward_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_music_note, + titleId = MR.strings.v5_7_call_sounds, + descrId = MR.strings.v5_7_call_sounds_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_account_box, + titleId = MR.strings.v5_7_shape_profile_images, + descrId = MR.strings.v5_7_shape_profile_images_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_wifi_tethering, + titleId = MR.strings.v5_7_network, + descrId = MR.strings.v5_7_network_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_translate, + titleId = MR.strings.v5_7_new_interface_languages, + descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate, + ) + ) + ), + VersionDescription( + version = "v5.8", + post = "https://simplex.chat/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html", + features = listOf( + VersionFeature.FeatureDescription( + icon = MR.images.ic_settings_ethernet, + titleId = MR.strings.v5_8_private_routing, + descrId = MR.strings.v5_8_private_routing_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_palette, + titleId = MR.strings.v5_8_chat_themes, + descrId = MR.strings.v5_8_chat_themes_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_security, + titleId = MR.strings.v5_8_safe_files, + descrId = MR.strings.v5_8_safe_files_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_battery_3_bar, + titleId = MR.strings.v5_8_message_delivery, + descrId = MR.strings.v5_8_message_delivery_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_translate, + titleId = MR.strings.v5_8_persian_ui, + descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate + ) + ) + ), + VersionDescription( + version = "v6.0", + post = "https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html", + features = listOf( + VersionFeature.FeatureDescription( + icon = null, + titleId = MR.strings.v6_0_new_chat_experience, + descrId = null, + subfeatures = listOf( + MR.images.ic_add_link to MR.strings.v6_0_connect_faster_descr, + MR.images.ic_inventory_2 to MR.strings.v6_0_your_contacts_descr, + MR.images.ic_delete to MR.strings.v6_0_delete_many_messages_descr, + MR.images.ic_match_case to MR.strings.v6_0_increase_font_size + ) + ), + VersionFeature.FeatureDescription( + icon = null, + titleId = MR.strings.v6_0_new_media_options, + descrId = null, + subfeatures = listOf( + MR.images.ic_play_arrow_filled to MR.strings.v6_0_chat_list_media, + MR.images.ic_blur_on to MR.strings.v6_0_privacy_blur, + ) + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_toast, + titleId = MR.strings.v6_0_reachable_chat_toolbar, + descrId = MR.strings.v6_0_reachable_chat_toolbar_descr, + show = appPlatform.isAndroid + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_settings_ethernet, + titleId = MR.strings.v5_8_private_routing, + descrId = MR.strings.v6_0_private_routing_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_wifi_tethering, + titleId = MR.strings.v6_0_connection_servers_status, + descrId = MR.strings.v6_0_connection_servers_status_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_upgrade, + titleId = MR.strings.v6_0_upgrade_app, + descrId = MR.strings.v6_0_upgrade_app_descr, + show = appPlatform.isDesktop + ), + ), + ), + VersionDescription( + version = "v6.1", + post = "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html", + features = listOf( + VersionFeature.FeatureDescription( + icon = MR.images.ic_verified_user, + titleId = MR.strings.v6_1_better_security, + descrId = MR.strings.v6_1_better_security_descr, + link = "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html" + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_videocam, + titleId = MR.strings.v6_1_better_calls, + descrId = MR.strings.v6_1_better_calls_descr + ), + VersionFeature.FeatureDescription( + icon = null, + titleId = MR.strings.v6_1_better_user_experience, + descrId = null, + subfeatures = listOf( + MR.images.ic_link to MR.strings.v6_1_switch_chat_profile_descr, + MR.images.ic_chat to MR.strings.v6_1_customizable_message_descr, + MR.images.ic_calendar to MR.strings.v6_1_message_dates_descr, + MR.images.ic_forward to MR.strings.v6_1_forward_many_messages_descr, + MR.images.ic_delete to MR.strings.v6_1_delete_many_messages_descr + ) + ), + ), + ), + VersionDescription( + version = "v6.2", + post = "https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html", + features = listOf( + VersionFeature.FeatureView( + icon = null, + titleId = MR.strings.v6_2_network_decentralization, + view = { modalManager -> + Column { + val src = (operatorsInfo[OperatorTag.Flux] ?: dummyOperatorInfo).largeLogo + Image(painterResource(src), null, modifier = Modifier.height(48.dp)) + Text(stringResource(MR.strings.v6_2_network_decentralization_descr), modifier = Modifier.padding(top = 8.dp)) + Text(stringResource(MR.strings.v6_2_network_decentralization_enable_flux)) + } + } + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_work, + titleId = MR.strings.v6_2_business_chats, + descrId = MR.strings.v6_2_business_chats_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_chat, + titleId = MR.strings.v6_2_improved_chat_navigation, + descrId = MR.strings.v6_2_improved_chat_navigation_descr + ), + ), + ), + VersionDescription( + version = "v6.3", + post = "https://simplex.chat/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html", + features = listOf( + VersionFeature.FeatureDescription( + icon = MR.images.ic_at, + titleId = MR.strings.v6_3_mentions, + descrId = MR.strings.v6_3_mentions_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_flag, + titleId = MR.strings.v6_3_reports, + descrId = MR.strings.v6_3_reports_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_menu, + titleId = MR.strings.v6_3_organize_chat_lists, + descrId = MR.strings.v6_3_organize_chat_lists_descr + ), + VersionFeature.FeatureDescription( + icon = null, + titleId = MR.strings.v6_3_better_privacy_and_security, + descrId = null, + subfeatures = listOf( + MR.images.ic_visibility_off to MR.strings.v6_3_private_media_file_names, + MR.images.ic_delete to MR.strings.v6_3_set_message_expiration_in_chats + ) + ), + VersionFeature.FeatureDescription( + icon = null, + titleId = MR.strings.v6_3_better_groups_performance, + descrId = null, + subfeatures = listOf( + MR.images.ic_bolt to MR.strings.v6_3_faster_sending_messages, + MR.images.ic_group_off to MR.strings.v6_3_faster_deletion_of_groups + ) + ), + ) + ) ) private val lastVersion = versionDescriptions.last().version fun setLastVersionDefault(m: ChatModel) { - m.controller.appPrefs.whatsNewVersion.set(lastVersion) + if (appPrefs.whatsNewVersion.get() != lastVersion) { + appPrefs.whatsNewVersion.set(lastVersion) + } } fun shouldShowWhatsNew(m: ChatModel): Boolean { @@ -484,7 +842,8 @@ fun shouldShowWhatsNew(m: ChatModel): Boolean { @Composable fun PreviewWhatsNewView() { SimpleXTheme { - WhatsNewView( + val data = remember { ModalData() } + data.WhatsNewView( viaSettings = true, close = {} ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt index 44e6969b8d..8bb84060c2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt @@ -8,13 +8,12 @@ import SectionSpacer import SectionView import TextIconSpaced import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager @@ -41,6 +40,8 @@ import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext @Composable fun ConnectDesktopView(close: () -> Unit) { @@ -75,9 +76,7 @@ private fun ConnectDesktopLayout(deviceName: String, close: () -> Unit) { val sessionAddress = remember { mutableStateOf("") } val remoteCtrls = remember { mutableStateListOf<RemoteCtrlInfo>() } val session = remember { chatModel.remoteCtrlSession }.value - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - ) { + ColumnWithScrollBar { val discovery = if (session == null) null else session.sessionState is UIRemoteCtrlSessionState.Searching if (discovery == true || (discovery == null && !showConnectScreen.value)) { SearchingDesktop(deviceName, remoteCtrls) @@ -148,7 +147,7 @@ private fun ConnectDesktop(deviceName: String, remoteCtrls: SnapshotStateList<Re @Composable private fun ConnectingDesktop(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) { AppBarTitle(stringResource(MR.strings.connecting_to_desktop)) - SectionView(stringResource(MR.strings.connecting_to_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + SectionView(stringResource(MR.strings.connecting_to_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { CtrlDeviceNameText(session, rc) Spacer(Modifier.height(DEFAULT_PADDING_HALF)) CtrlDeviceVersionText(session) @@ -166,6 +165,24 @@ private fun ConnectingDesktop(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) { SectionView { DisconnectButton(onClick = ::disconnectDesktop) } + + ProgressIndicator() +} + +@Composable +private fun ProgressIndicator() { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + Modifier + .padding(horizontal = 2.dp) + .size(30.dp), + color = MaterialTheme.colors.secondary, + strokeWidth = 3.dp + ) + } } @Composable @@ -180,7 +197,7 @@ private fun SearchingDesktop(deviceName: String, remoteCtrls: SnapshotStateList< } } SectionDividerSpaced() - SectionView(stringResource(MR.strings.found_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + SectionView(stringResource(MR.strings.found_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { Text(stringResource(MR.strings.waiting_for_desktop), fontStyle = FontStyle.Italic) } SectionSpacer() @@ -207,7 +224,7 @@ private fun FoundDesktop( } } SectionDividerSpaced() - SectionView(stringResource(MR.strings.found_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + SectionView(stringResource(MR.strings.found_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { CtrlDeviceNameText(session, rc) CtrlDeviceVersionText(session) if (!compatible) { @@ -218,7 +235,7 @@ private fun FoundDesktop( SectionSpacer() if (compatible) { - SectionItemView({ confirmKnownDesktop(sessionAddress, rc) }) { + SectionItemView({ withBGApi { confirmKnownDesktop(sessionAddress, rc) } }) { Icon(painterResource(MR.images.ic_check), generalGetString(MR.strings.connect_button), tint = MaterialTheme.colors.secondary) TextIconSpaced(false) Text(generalGetString(MR.strings.connect_button)) @@ -239,7 +256,7 @@ private fun FoundDesktop( @Composable private fun VerifySession(session: RemoteCtrlSession, rc: RemoteCtrlInfo?, sessCode: String, remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) { AppBarTitle(stringResource(MR.strings.verify_connection)) - SectionView(stringResource(MR.strings.connected_to_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + SectionView(stringResource(MR.strings.connected_to_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { CtrlDeviceNameText(session, rc) Spacer(Modifier.height(DEFAULT_PADDING_HALF)) CtrlDeviceVersionText(session) @@ -294,7 +311,7 @@ private fun CtrlDeviceVersionText(session: RemoteCtrlSession) { @Composable private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo, close: () -> Unit) { AppBarTitle(stringResource(MR.strings.connected_to_desktop)) - SectionView(stringResource(MR.strings.connected_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + SectionView(stringResource(MR.strings.connected_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { Text(rc.deviceViewName) Spacer(Modifier.height(DEFAULT_PADDING_HALF)) CtrlDeviceVersionText(session) @@ -339,16 +356,9 @@ private fun DevicesView(deviceName: String, remoteCtrls: SnapshotStateList<Remot @Composable private fun ScanDesktopAddressView(sessionAddress: MutableState<String>) { SectionView(stringResource(MR.strings.scan_qr_code_from_desktop).uppercase()) { - Box( - Modifier - .fillMaxWidth() - .aspectRatio(ratio = 1F) - .padding(DEFAULT_PADDING) - ) { - QRCodeScanner { text -> - sessionAddress.value = text - processDesktopQRCode(sessionAddress, text) - } + QRCodeScanner { text -> + sessionAddress.value = text + connectDesktopAddress(sessionAddress, text) } } } @@ -390,7 +400,7 @@ private fun DesktopAddressView(sessionAddress: MutableState<String>) { stringResource(MR.strings.connect_to_desktop), disabled = sessionAddress.value.isEmpty(), click = { - connectDesktopAddress(sessionAddress, sessionAddress.value) + withBGApi { connectDesktopAddress(sessionAddress, sessionAddress.value) } }, ) } @@ -398,9 +408,7 @@ private fun DesktopAddressView(sessionAddress: MutableState<String>) { @Composable private fun LinkedDesktopsView(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) { - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.linked_desktops)) SectionView(stringResource(MR.strings.desktop_devices).uppercase()) { remoteCtrls.forEach { rc -> @@ -420,14 +428,14 @@ private fun LinkedDesktopsView(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) { SectionDividerSpaced() SectionView(stringResource(MR.strings.linked_desktop_options).uppercase()) { - PreferenceToggle(stringResource(MR.strings.verify_connections), remember { controller.appPrefs.confirmRemoteSessions.state }.value) { + PreferenceToggle(stringResource(MR.strings.verify_connections), checked = remember { controller.appPrefs.confirmRemoteSessions.state }.value) { controller.appPrefs.confirmRemoteSessions.set(it) } - PreferenceToggle(stringResource(MR.strings.discover_on_network), remember { controller.appPrefs.connectRemoteViaMulticast.state }.value) { + PreferenceToggle(stringResource(MR.strings.discover_on_network), checked = remember { controller.appPrefs.connectRemoteViaMulticast.state }.value) { controller.appPrefs.connectRemoteViaMulticast.set(it) } if (remember { controller.appPrefs.connectRemoteViaMulticast.state }.value) { - PreferenceToggle(stringResource(MR.strings.multicast_connect_automatically), remember { controller.appPrefs.connectRemoteViaMulticastAuto.state }.value) { + PreferenceToggle(stringResource(MR.strings.multicast_connect_automatically), checked = remember { controller.appPrefs.connectRemoteViaMulticastAuto.state }.value) { controller.appPrefs.connectRemoteViaMulticastAuto.set(it) } } @@ -455,10 +463,6 @@ private suspend fun updateRemoteCtrls(remoteCtrls: SnapshotStateList<RemoteCtrlI } } -private fun processDesktopQRCode(sessionAddress: MutableState<String>, resp: String) { - connectDesktopAddress(sessionAddress, resp) -} - private fun findKnownDesktop(showConnectScreen: MutableState<Boolean>) { withBGApi { if (controller.findKnownRemoteCtrl()) { @@ -472,45 +476,48 @@ private fun findKnownDesktop(showConnectScreen: MutableState<Boolean>) { } } -private fun confirmKnownDesktop(sessionAddress: MutableState<String>, rc: RemoteCtrlInfo) { - connectDesktop(sessionAddress) { - controller.confirmRemoteCtrl(rc.remoteCtrlId) +private suspend fun confirmKnownDesktop(sessionAddress: MutableState<String>, rc: RemoteCtrlInfo): Boolean { + return withContext(Dispatchers.Default) { + connectDesktop(sessionAddress) { + controller.confirmRemoteCtrl(rc.remoteCtrlId) + } } } -private fun connectDesktopAddress(sessionAddress: MutableState<String>, addr: String) { - connectDesktop(sessionAddress) { - controller.connectRemoteCtrl(addr) +private suspend fun connectDesktopAddress(sessionAddress: MutableState<String>, addr: String): Boolean { + return withContext(Dispatchers.Default) { + connectDesktop(sessionAddress) { + controller.connectRemoteCtrl(addr) + } } } -private fun connectDesktop(sessionAddress: MutableState<String>, connect: suspend () -> Pair<SomeRemoteCtrl?, CR.ChatCmdError?>) { - withBGApi { - val res = connect() - if (res.first != null) { - val (rc_, ctrlAppInfo, v) = res.first!! - sessionAddress.value = "" - chatModel.remoteCtrlSession.value = RemoteCtrlSession( - ctrlAppInfo = ctrlAppInfo, - appVersion = v, - sessionState = UIRemoteCtrlSessionState.Connecting(remoteCtrl_ = rc_) - ) - } else { - val e = res.second ?: return@withBGApi - when { - e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadInvitation -> showBadInvitationErrorAlert() - e.chatError is ChatError.ChatErrorChat && e.chatError.errorType is ChatErrorType.CommandError -> showBadInvitationErrorAlert() - e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadVersion -> showBadVersionAlert(v = e.chatError.remoteCtrlError.appVersion) - e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.VERSION -> showBadVersionAlert(v = null) - e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.CTRL_AUTH -> showDesktopDisconnectedErrorAlert() - else -> { - val errMsg = "${e.responseType}: ${e.details}" - Log.e(TAG, "bad response: $errMsg") - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), errMsg) - } +private suspend fun connectDesktop(sessionAddress: MutableState<String>, connect: suspend () -> Pair<SomeRemoteCtrl?, ChatError?>): Boolean { + val res = connect() + if (res.first != null) { + val (rc_, ctrlAppInfo, v) = res.first!! + sessionAddress.value = "" + chatModel.remoteCtrlSession.value = RemoteCtrlSession( + ctrlAppInfo = ctrlAppInfo, + appVersion = v, + sessionState = UIRemoteCtrlSessionState.Connecting(remoteCtrl_ = rc_) + ) + } else { + val e = res.second ?: return false + when { + e is ChatError.ChatErrorRemoteCtrl && e.remoteCtrlError is RemoteCtrlError.BadInvitation -> showBadInvitationErrorAlert() + e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.CommandError -> showBadInvitationErrorAlert() + e is ChatError.ChatErrorRemoteCtrl && e.remoteCtrlError is RemoteCtrlError.BadVersion -> showBadVersionAlert(v = e.remoteCtrlError.appVersion) + e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.RCP && e.agentError.rcpErr is RCErrorType.VERSION -> showBadVersionAlert(v = null) + e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.RCP && e.agentError.rcpErr is RCErrorType.CTRL_AUTH -> showDesktopDisconnectedErrorAlert() + else -> { + val errMsg = "error: ${e.string}" + Log.e(TAG, "bad response: $errMsg") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), errMsg) } } } + return res.first != null } private fun verifyDesktopSessionCode(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>, sessCode: String) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt index a3218c961b..1d01ab11ff 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt @@ -35,6 +35,7 @@ import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.QRCode import chat.simplex.common.views.usersettings.* +import chat.simplex.common.views.usersettings.networkAndServers.validPort import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -46,7 +47,9 @@ fun ConnectMobileView() { val remoteHosts = remember { chatModel.remoteHosts } val deviceName = chatModel.controller.appPrefs.deviceNameForRemoteAccess LaunchedEffect(Unit) { - controller.reloadRemoteHosts() + withBGApi { + controller.reloadRemoteHosts() + } } ConnectMobileLayout( deviceName = remember { deviceName.state }, @@ -87,18 +90,15 @@ fun ConnectMobileLayout( connectDesktop: () -> Unit, deleteHost: (RemoteHostInfo) -> Unit, ) { - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) SectionView(generalGetString(MR.strings.this_device_name).uppercase()) { DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) } SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile)) - PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), remember { controller.appPrefs.offerRemoteMulticast.state }.value) { + PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), checked = remember { controller.appPrefs.offerRemoteMulticast.state }.value) { controller.appPrefs.offerRemoteMulticast.set(it) } - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced() } SectionView(stringResource(MR.strings.devices).uppercase()) { if (chatModel.localUserCreated.value == true) { @@ -177,21 +177,22 @@ private fun ConnectMobileViewLayout( refreshQrCode: () -> Unit = {}, UnderQrLayout: @Composable () -> Unit = {}, ) { - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { + @Composable + fun ScrollableLayout(content: @Composable ColumnScope.() -> Unit) { + if (LocalAppBarHandler.current != null) { + ColumnWithScrollBar(content = content) + } else { + ColumnWithScrollBarNoAppBar(content = content) + } + } + ScrollableLayout { if (title != null) { AppBarTitle(title) } SectionView { if (invitation != null && sessionCode == null && port != null) { Box { - QRCode( - invitation, Modifier - .padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF) - .aspectRatio(1f) - ) + QRCode(invitation) if (staleQrCode) { Box(Modifier.matchParentSize().background(MaterialTheme.colors.background.copy(alpha = 0.9f)), contentAlignment = Alignment.Center) { SimpleButtonDecorated(stringResource(MR.strings.refresh_qr_code), painterResource(MR.images.ic_refresh), click = refreshQrCode) @@ -269,12 +270,20 @@ fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState<Boolean>, c var cachedR by remember { mutableStateOf<CR.RemoteHostStarted?>(null) } val customAddress = rememberSaveable { mutableStateOf<RemoteCtrlAddress?>(null) } val customPort = rememberSaveable { mutableStateOf<Int?>(null) } + var userChangedAddress by rememberSaveable { mutableStateOf(false) } + var userChangedPort by rememberSaveable { mutableStateOf(false) } val startRemoteHost = suspend { + if (customAddress.value != cachedR.address && cachedR != null) { + userChangedAddress = true + } + if (customPort.value != cachedR.port && cachedR != null) { + userChangedPort = true + } val r = chatModel.controller.startRemoteHost( rhId = null, multicast = controller.appPrefs.offerRemoteMulticast.get(), - address = if (customAddress.value?.address != cachedR.address?.address) customAddress.value else cachedR.rh?.bindAddress_, - port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_ + address = if (customAddress.value != null && userChangedAddress) customAddress.value else cachedR.rh?.bindAddress_, + port = if (customPort.value != null && userChangedPort) customPort.value else cachedR.rh?.bindPort_ ) if (r != null) { cachedR = r @@ -343,12 +352,20 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState var cachedR by remember { mutableStateOf<CR.RemoteHostStarted?>(null) } val customAddress = rememberSaveable { mutableStateOf<RemoteCtrlAddress?>(null) } val customPort = rememberSaveable { mutableStateOf<Int?>(null) } + var userChangedAddress by rememberSaveable { mutableStateOf(false) } + var userChangedPort by rememberSaveable { mutableStateOf(false) } val startRemoteHost = suspend { + if (customAddress.value != cachedR.address && cachedR != null) { + userChangedAddress = true + } + if (customPort.value != cachedR.port && cachedR != null) { + userChangedPort = true + } val r = chatModel.controller.startRemoteHost( rhId = rh.remoteHostId, multicast = controller.appPrefs.offerRemoteMulticast.get(), - address = if (customAddress.value?.address != cachedR.address?.address) customAddress.value else cachedR.rh?.bindAddress_ ?: rh.bindAddress_, - port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_ ?: rh.bindPort_ + address = if (customAddress.value != null && userChangedAddress) customAddress.value else cachedR.rh?.bindAddress_ ?: rh.bindAddress_, + port = if (customPort.value != null && userChangedPort) customPort.value else cachedR.rh?.bindPort_ ?: rh.bindPort_ ) if (r != null) { cachedR = r diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt deleted file mode 100644 index 5fb8bfb03e..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt +++ /dev/null @@ -1,433 +0,0 @@ -package chat.simplex.common.views.usersettings - -import SectionBottomSpacer -import SectionCustomFooter -import SectionItemView -import SectionView -import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import chat.simplex.common.model.* -import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.helpers.* -import chat.simplex.common.model.ChatModel -import chat.simplex.res.MR -import java.text.DecimalFormat - -@Composable -fun AdvancedNetworkSettingsView(chatModel: ChatModel) { - val currentCfg = remember { mutableStateOf(chatModel.controller.getNetCfg()) } - val currentCfgVal = currentCfg.value // used only on initialization - val networkTCPConnectTimeout = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout) } - val networkTCPTimeout = remember { mutableStateOf(currentCfgVal.tcpTimeout) } - val networkTCPTimeoutPerKb = remember { mutableStateOf(currentCfgVal.tcpTimeoutPerKb) } - val networkSMPPingInterval = remember { mutableStateOf(currentCfgVal.smpPingInterval) } - val networkSMPPingCount = remember { mutableStateOf(currentCfgVal.smpPingCount) } - val networkEnableKeepAlive = remember { mutableStateOf(currentCfgVal.enableKeepAlive) } - val networkTCPKeepIdle: MutableState<Int> - val networkTCPKeepIntvl: MutableState<Int> - val networkTCPKeepCnt: MutableState<Int> - if (currentCfgVal.tcpKeepAlive != null) { - networkTCPKeepIdle = remember { mutableStateOf(currentCfgVal.tcpKeepAlive.keepIdle) } - networkTCPKeepIntvl = remember { mutableStateOf(currentCfgVal.tcpKeepAlive.keepIntvl) } - networkTCPKeepCnt = remember { mutableStateOf(currentCfgVal.tcpKeepAlive.keepCnt) } - } else { - networkTCPKeepIdle = remember { mutableStateOf(KeepAliveOpts.defaults.keepIdle) } - networkTCPKeepIntvl = remember { mutableStateOf(KeepAliveOpts.defaults.keepIntvl) } - networkTCPKeepCnt = remember { mutableStateOf(KeepAliveOpts.defaults.keepCnt) } - } - - fun buildCfg(): NetCfg { - val enableKeepAlive = networkEnableKeepAlive.value - val tcpKeepAlive = if (enableKeepAlive) { - val keepIdle = networkTCPKeepIdle.value - val keepIntvl = networkTCPKeepIntvl.value - val keepCnt = networkTCPKeepCnt.value - KeepAliveOpts(keepIdle = keepIdle, keepIntvl = keepIntvl, keepCnt = keepCnt) - } else { - null - } - return NetCfg( - socksProxy = currentCfg.value.socksProxy, - hostMode = currentCfg.value.hostMode, - requiredHostMode = currentCfg.value.requiredHostMode, - sessionMode = currentCfg.value.sessionMode, - tcpConnectTimeout = networkTCPConnectTimeout.value, - tcpTimeout = networkTCPTimeout.value, - tcpTimeoutPerKb = networkTCPTimeoutPerKb.value, - tcpKeepAlive = tcpKeepAlive, - smpPingInterval = networkSMPPingInterval.value, - smpPingCount = networkSMPPingCount.value - ) - } - - fun updateView(cfg: NetCfg) { - networkTCPConnectTimeout.value = cfg.tcpConnectTimeout - networkTCPTimeout.value = cfg.tcpTimeout - networkTCPTimeoutPerKb.value = cfg.tcpTimeoutPerKb - networkSMPPingInterval.value = cfg.smpPingInterval - networkSMPPingCount.value = cfg.smpPingCount - networkEnableKeepAlive.value = cfg.enableKeepAlive - if (cfg.tcpKeepAlive != null) { - networkTCPKeepIdle.value = cfg.tcpKeepAlive.keepIdle - networkTCPKeepIntvl.value = cfg.tcpKeepAlive.keepIntvl - networkTCPKeepCnt.value = cfg.tcpKeepAlive.keepCnt - } else { - networkTCPKeepIdle.value = KeepAliveOpts.defaults.keepIdle - networkTCPKeepIntvl.value = KeepAliveOpts.defaults.keepIntvl - networkTCPKeepCnt.value = KeepAliveOpts.defaults.keepCnt - } - } - - fun saveCfg(cfg: NetCfg) { - withApi { - chatModel.controller.apiSetNetworkConfig(cfg) - currentCfg.value = cfg - chatModel.controller.setNetCfg(cfg) - } - } - - fun reset() { - val newCfg = if (currentCfg.value.useSocksProxy) NetCfg.proxyDefaults else NetCfg.defaults - updateView(newCfg) - saveCfg(newCfg) - } - - AdvancedNetworkSettingsLayout( - networkTCPConnectTimeout, - networkTCPTimeout, - networkTCPTimeoutPerKb, - networkSMPPingInterval, - networkSMPPingCount, - networkEnableKeepAlive, - networkTCPKeepIdle, - networkTCPKeepIntvl, - networkTCPKeepCnt, - resetDisabled = if (currentCfg.value.useSocksProxy) currentCfg.value == NetCfg.proxyDefaults else currentCfg.value == NetCfg.defaults, - reset = { showUpdateNetworkSettingsDialog(::reset) }, - footerDisabled = buildCfg() == currentCfg.value, - revert = { updateView(currentCfg.value) }, - save = { showUpdateNetworkSettingsDialog { saveCfg(buildCfg()) } } - ) -} - -@Composable fun AdvancedNetworkSettingsLayout( - networkTCPConnectTimeout: MutableState<Long>, - networkTCPTimeout: MutableState<Long>, - networkTCPTimeoutPerKb: MutableState<Long>, - networkSMPPingInterval: MutableState<Long>, - networkSMPPingCount: MutableState<Int>, - networkEnableKeepAlive: MutableState<Boolean>, - networkTCPKeepIdle: MutableState<Int>, - networkTCPKeepIntvl: MutableState<Int>, - networkTCPKeepCnt: MutableState<Int>, - resetDisabled: Boolean, - reset: () -> Unit, - footerDisabled: Boolean, - revert: () -> Unit, - save: () -> Unit -) { - val secondsLabel = stringResource(MR.strings.network_option_seconds_label) - - Column( - Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()), - ) { - AppBarTitle(stringResource(MR.strings.network_settings_title)) - SectionView { - SectionItemView { - ResetToDefaultsButton(reset, disabled = resetDisabled) - } - SectionItemView { - TimeoutSettingRow( - stringResource(MR.strings.network_option_tcp_connection_timeout), networkTCPConnectTimeout, - listOf(7_500000, 10_000000, 15_000000, 20_000000, 30_000_000, 45_000_000), secondsLabel - ) - } - SectionItemView { - TimeoutSettingRow( - stringResource(MR.strings.network_option_protocol_timeout), networkTCPTimeout, - listOf(5_000000, 7_000000, 10_000000, 15_000000, 20_000_000, 30_000_000), secondsLabel - ) - } - SectionItemView { - // can't be higher than 130ms to avoid overflow on 32bit systems - TimeoutSettingRow( - stringResource(MR.strings.network_option_protocol_timeout_per_kb), networkTCPTimeoutPerKb, - listOf(15_000, 30_000, 45_000, 60_000, 90_000, 120_000), secondsLabel - ) - } - SectionItemView { - TimeoutSettingRow( - stringResource(MR.strings.network_option_ping_interval), networkSMPPingInterval, - listOf(120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000), secondsLabel - ) - } - SectionItemView { - IntSettingRow( - stringResource(MR.strings.network_option_ping_count), networkSMPPingCount, - listOf(1, 2, 3, 5, 8), "" - ) - } - SectionItemView { - EnableKeepAliveSwitch(networkEnableKeepAlive) - } - if (networkEnableKeepAlive.value) { - SectionItemView { - IntSettingRow("TCP_KEEPIDLE", networkTCPKeepIdle, listOf(15, 30, 60, 120, 180), secondsLabel) - } - SectionItemView { - IntSettingRow("TCP_KEEPINTVL", networkTCPKeepIntvl, listOf(5, 10, 15, 30, 60), secondsLabel) - } - SectionItemView { - IntSettingRow("TCP_KEEPCNT", networkTCPKeepCnt, listOf(1, 2, 4, 6, 8), "") - } - } else { - SectionItemView { - Text("TCP_KEEPIDLE", color = MaterialTheme.colors.secondary) - } - SectionItemView { - Text("TCP_KEEPINTVL", color = MaterialTheme.colors.secondary) - } - SectionItemView { - Text("TCP_KEEPCNT", color = MaterialTheme.colors.secondary) - } - } - } - SectionCustomFooter { - SettingsSectionFooter(revert, save, footerDisabled) - } - SectionBottomSpacer() - } -} - -@Composable -fun ResetToDefaultsButton(reset: () -> Unit, disabled: Boolean) { - val modifier = if (disabled) Modifier else Modifier.clickable { reset() } - Row( - modifier.fillMaxSize(), - verticalAlignment = Alignment.CenterVertically - ) { - val color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary - Text(stringResource(MR.strings.network_options_reset_to_defaults), color = color) - } -} - -@Composable -fun EnableKeepAliveSwitch( - networkEnableKeepAlive: MutableState<Boolean> -) { - Row( - Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text(stringResource(MR.strings.network_option_enable_tcp_keep_alive)) - DefaultSwitch( - checked = networkEnableKeepAlive.value, - onCheckedChange = { networkEnableKeepAlive.value = it }, - ) - } -} - -@Composable -fun IntSettingRow(title: String, selection: MutableState<Int>, values: List<Int>, label: String) { - Row( - Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - val expanded = rememberSaveable { mutableStateOf(false) } - - Text(title) - - ExposedDropdownMenuBox( - expanded = expanded.value, - onExpandedChange = { - expanded.value = !expanded.value - } - ) { - Row( - Modifier.width(140.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End - ) { - Text( - "${selection.value} $label", - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colors.secondary - ) - Spacer(Modifier.size(4.dp)) - Icon( - if (!expanded.value) painterResource(MR.images.ic_expand_more) else painterResource(MR.images.ic_expand_less), - generalGetString(MR.strings.invite_to_group_button), - modifier = Modifier.padding(start = 8.dp), - tint = MaterialTheme.colors.secondary - ) - } - DefaultExposedDropdownMenu( - expanded = expanded, - ) { - values.forEach { selectionOption -> - DropdownMenuItem( - onClick = { - selection.value = selectionOption - expanded.value = false - }, - contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f) - ) { - Text( - "$selectionOption $label", - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - } - } - } -} - -@Composable -fun TimeoutSettingRow(title: String, selection: MutableState<Long>, values: List<Long>, label: String) { - Row( - Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - val expanded = remember { mutableStateOf(false) } - - Text(title) - - ExposedDropdownMenuBox( - expanded = expanded.value, - onExpandedChange = { - expanded.value = !expanded.value - } - ) { - val df = DecimalFormat("#.###") - Row( - Modifier.width(140.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End - ) { - Text( - "${df.format(selection.value / 1_000_000.0)} $label", - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colors.secondary - ) - Spacer(Modifier.size(4.dp)) - Icon( - if (!expanded.value) painterResource(MR.images.ic_expand_more) else painterResource(MR.images.ic_expand_less), - generalGetString(MR.strings.invite_to_group_button), - modifier = Modifier.padding(start = 8.dp), - tint = MaterialTheme.colors.secondary - ) - } - DefaultExposedDropdownMenu( - expanded = expanded - ) { - val v = selection.value - val vs = if (values.contains(v)) values else values + v - vs.forEach { selectionOption -> - DropdownMenuItem( - onClick = { - selection.value = selectionOption - expanded.value = false - }, - contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f) - ) { - Text( - "${df.format(selectionOption / 1_000_000.0)} $label", - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - } - } - } -} - -@Composable -fun SettingsSectionFooter(revert: () -> Unit, save: () -> Unit, disabled: Boolean) { - Row( - Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - FooterButton(painterResource(MR.images.ic_replay), stringResource(MR.strings.network_options_revert), revert, disabled) - FooterButton(painterResource(MR.images.ic_check), stringResource(MR.strings.network_options_save), save, disabled) - } -} - -@Composable -fun FooterButton(icon: Painter, title: String, action: () -> Unit, disabled: Boolean) { - Surface( - shape = RoundedCornerShape(20.dp), - color = Color.Black.copy(alpha = 0f) - ) { - val modifier = if (disabled) Modifier else Modifier.clickable { action() } - Row( - modifier.padding(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - icon, - title, - tint = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary - ) - Text( - title, - color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary - ) - } - } -} - -fun showUpdateNetworkSettingsDialog(action: () -> Unit) { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.update_network_settings_question), - text = generalGetString(MR.strings.updating_settings_will_reconnect_client_to_all_servers), - confirmText = generalGetString(MR.strings.update_network_settings_confirmation), - onConfirm = action - ) -} - -@Preview -@Composable -fun PreviewAdvancedNetworkSettingsLayout() { - SimpleXTheme { - AdvancedNetworkSettingsLayout( - networkTCPConnectTimeout = remember { mutableStateOf(10_000000) }, - networkTCPTimeout = remember { mutableStateOf(10_000000) }, - networkTCPTimeoutPerKb = remember { mutableStateOf(10_000) }, - networkSMPPingInterval = remember { mutableStateOf(10_000000) }, - networkSMPPingCount = remember { mutableStateOf(3) }, - networkEnableKeepAlive = remember { mutableStateOf(true) }, - networkTCPKeepIdle = remember { mutableStateOf(10) }, - networkTCPKeepIntvl = remember { mutableStateOf(10) }, - networkTCPKeepCnt = remember { mutableStateOf(10) }, - resetDisabled = false, - reset = {}, - footerDisabled = false, - revert = {}, - save = {} - ) - } -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt index 75e7d72016..7a1fc21b17 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt @@ -1,123 +1,693 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer +import SectionDividerSpaced import SectionItemView import SectionItemViewSpaceBetween +import SectionItemViewWithoutMinPadding import SectionSpacer import SectionView import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.grid.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.MaterialTheme.colors import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.* import androidx.compose.ui.graphics.* +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.unit.dp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex +import chat.simplex.common.ui.theme.ThemeManager.toReadableHex +import chat.simplex.common.views.chat.item.PreviewChatItemView +import chat.simplex.common.views.chat.item.msgTailWidthDp import chat.simplex.res.MR -import com.godaddy.android.colorpicker.* +import com.godaddy.android.colorpicker.ClassicColorPicker +import com.godaddy.android.colorpicker.HsvColor +import dev.icerock.moko.resources.StringResource +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.datetime.Clock import kotlinx.serialization.encodeToString import java.io.File import java.net.URI import java.util.* import kotlin.collections.ArrayList +import kotlin.math.* @Composable -expect fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) +expect fun AppearanceView(m: ChatModel) object AppearanceScope { @Composable - fun ThemesSection( - systemDarkTheme: SharedPreference<String?>, - showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - editColor: (ThemeColor, Color) -> Unit - ) { - val currentTheme by CurrentColors.collectAsState() - SectionView(stringResource(MR.strings.settings_section_title_themes)) { - val darkTheme = chat.simplex.common.ui.theme.isSystemInDarkTheme() - val state = remember { derivedStateOf { currentTheme.name } } - ThemeSelector(state) { - ThemeManager.applyTheme(it, darkTheme) - } - if (state.value == DefaultTheme.SYSTEM.name) { - DarkThemeSelector(remember { systemDarkTheme.state }) { - ThemeManager.changeDarkTheme(it, darkTheme) + fun ProfileImageSection() { + SectionView(stringResource(MR.strings.settings_section_title_profile_images).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { + val image = remember { chatModel.currentUser }.value?.image + Row(Modifier.padding(top = 10.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { + val size = 60 + Box(Modifier.offset(x = -(size / 12).dp)) { + if (!image.isNullOrEmpty()) { + ProfileImage(size.dp, image, MR.images.ic_simplex_light, color = Color.Unspecified) + } else { + ProfileImage(size.dp, if (isInDarkTheme()) MR.images.ic_simplex_light else MR.images.ic_simplex_dark) + } } + Spacer(Modifier.width(DEFAULT_PADDING_HALF - (size / 12).dp)) + Slider( + remember { appPreferences.profileImageCornerRadius.state }.value, + valueRange = 0f..50f, + steps = 20, + onValueChange = { + val diff = it % 2.5f + appPreferences.profileImageCornerRadius.set(it + (if (diff >= 1.25f) -diff + 2.5f else -diff)) + saveThemeToDatabase(null) + }, + colors = SliderDefaults.colors( + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, + ) + ) } } - SectionItemView(showSettingsModal { _ -> CustomizeThemeView(editColor) }) { Text(stringResource(MR.strings.customize_theme_title)) } } @Composable - fun CustomizeThemeView(editColor: (ThemeColor, Color) -> Unit) { - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), + fun AppToolbarsSection() { + BoxWithConstraints { + SectionView(stringResource(MR.strings.appearance_app_toolbars).uppercase()) { + SectionItemViewWithoutMinPadding { + Box(Modifier.weight(1f)) { + Text( + stringResource(MR.strings.appearance_in_app_bars_alpha), + Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + appPrefs.inAppBarsAlpha.set(appPrefs.inAppBarsDefaultAlpha) + }, + maxLines = 1 + ) + } + Spacer(Modifier.padding(end = 10.dp)) + Slider( + (1 - remember { appPrefs.inAppBarsAlpha.state }.value).coerceIn(0f, 0.5f), + onValueChange = { + val diff = it % 0.025f + appPrefs.inAppBarsAlpha.set(1f - (String.format(Locale.US, "%.3f", it + (if (diff >= 0.0125f) -diff + 0.025f else -diff)).toFloatOrNull() ?: 1f)) + }, + Modifier.widthIn(max = (this@BoxWithConstraints.maxWidth - DEFAULT_PADDING * 2) * 0.618f), + valueRange = 0f..0.5f, + steps = 21, + colors = SliderDefaults.colors( + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, + ) + ) + } + // In Android in OneHandUI there is a problem with setting initial value of blur if it was 0 before entering the screen. + // So doing in two steps works ok + fun saveBlur(value: Int) { + val oneHandUI = appPrefs.oneHandUI.get() + val pref = appPrefs.appearanceBarsBlurRadius + if (appPlatform.isAndroid && oneHandUI && pref.get() == 0) { + pref.set(if (value > 2) value - 1 else value + 1) + withApi { + delay(50) + pref.set(value) + } + } else { + pref.set(value) + } + } + val blur = remember { appPrefs.appearanceBarsBlurRadius.state } + if (appPrefs.deviceSupportsBlur || blur.value > 0) { + SectionItemViewWithoutMinPadding { + Box(Modifier.weight(1f)) { + Text( + stringResource(MR.strings.appearance_bars_blur_radius), + Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + saveBlur(50) + }, + maxLines = 1 + ) + } + Spacer(Modifier.padding(end = 10.dp)) + Slider( + blur.value.toFloat() / 100f, + onValueChange = { + val diff = it % 0.05f + saveBlur(((String.format(Locale.US, "%.2f", it + (if (diff >= 0.025f) -diff + 0.05f else -diff)).toFloatOrNull() ?: 1f) * 100).toInt()) + }, + Modifier.widthIn(max = (this@BoxWithConstraints.maxWidth - DEFAULT_PADDING * 2) * 0.618f), + valueRange = 0f..1f, + steps = 21, + colors = SliderDefaults.colors( + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, + ) + ) + } + } + } + } + } + + @Composable + fun MessageShapeSection() { + BoxWithConstraints { + SectionView(stringResource(MR.strings.settings_section_title_message_shape).uppercase()) { + SectionItemViewWithoutMinPadding { + Text(stringResource(MR.strings.settings_message_shape_corner), Modifier.weight(1f)) + Spacer(Modifier.width(10.dp)) + Slider( + remember { appPreferences.chatItemRoundness.state }.value, + onValueChange = { + val diff = it % 0.05f + appPreferences.chatItemRoundness.set(it + (if (diff >= 0.025f) -diff + 0.05f else -diff)) + saveThemeToDatabase(null) + }, + Modifier.widthIn(max = (this@BoxWithConstraints.maxWidth - DEFAULT_PADDING * 2) * 0.618f), + valueRange = 0f..1f, + steps = 20, + colors = SliderDefaults.colors( + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, + ) + ) + } + SettingsPreferenceItem(icon = null, stringResource(MR.strings.settings_message_shape_tail), appPreferences.chatItemTail) + } + } + } + + @Composable + fun FontScaleSection() { + val localFontScale = remember { mutableStateOf(appPrefs.fontScale.get()) } + SectionView(stringResource(MR.strings.appearance_font_size).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) { + Box(Modifier.size(50.dp) + .background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22)) + .clip(RoundedCornerShape(percent = 22)) + .clickable { + localFontScale.value = 1f + appPrefs.fontScale.set(localFontScale.value) + }, + contentAlignment = Alignment.Center) { + CompositionLocalProvider( + LocalDensity provides Density(LocalDensity.current.density, localFontScale.value) + ) { + Text("Aa", color = if (localFontScale.value == 1f) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground) + } + } + Spacer(Modifier.width(15.dp)) + // Text("${(localFontScale.value * 100).roundToInt()}%", Modifier.width(70.dp), textAlign = TextAlign.Center, fontSize = 12.sp) + if (appPlatform.isAndroid) { + Slider( + localFontScale.value, + valueRange = 0.75f..1.25f, + steps = 11, + onValueChange = { + val diff = it % 0.05f + localFontScale.value = String.format(Locale.US, "%.2f", it + (if (diff >= 0.025f) -diff + 0.05f else -diff)).toFloatOrNull() ?: 1f + }, + onValueChangeFinished = { + appPrefs.fontScale.set(localFontScale.value) + }, + colors = SliderDefaults.colors( + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, + ) + ) + } else { + Slider( + localFontScale.value, + valueRange = 0.7f..1.5f, + steps = 9, + onValueChange = { + val diff = it % 0.1f + localFontScale.value = String.format(Locale.US, "%.1f", it + (if (diff >= 0.05f) -diff + 0.1f else -diff)).toFloatOrNull() ?: 1f + }, + onValueChangeFinished = { + appPrefs.fontScale.set(localFontScale.value) + }, + colors = SliderDefaults.colors( + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, + ) + ) + } + } + } + } + + @Composable + fun ChatThemePreview( + theme: DefaultTheme, + wallpaperImage: ImageBitmap?, + wallpaperType: WallpaperType?, + backgroundColor: Color? = MaterialTheme.wallpaper.background, + tintColor: Color? = MaterialTheme.wallpaper.tint, + withMessages: Boolean = true + ) { + val themeBackgroundColor = MaterialTheme.colors.background + val backgroundColor = backgroundColor ?: wallpaperType?.defaultBackgroundColor(theme, MaterialTheme.colors.background) + val tintColor = tintColor ?: wallpaperType?.defaultTintColor(theme) + Column(Modifier + .drawWithCache { + if (wallpaperImage != null && wallpaperType != null && backgroundColor != null && tintColor != null) { + chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor, null, null) + } else { + onDrawBehind { + drawRect(themeBackgroundColor) + } + } + } + .padding(DEFAULT_PADDING_HALF) ) { + if (withMessages) { + val chatItemTail = remember { appPreferences.chatItemTail.state } + + Column(verticalArrangement = Arrangement.spacedBy(4.dp), modifier = if (chatItemTail.value) Modifier else Modifier.padding(horizontal = msgTailWidthDp)) { + val alice = remember { ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), generalGetString(MR.strings.wallpaper_preview_hello_bob)) } + PreviewChatItemView(alice) + PreviewChatItemView( + ChatItem.getSampleData(2, CIDirection.DirectSnd(), Clock.System.now(), stringResource(MR.strings.wallpaper_preview_hello_alice), + quotedItem = CIQuote(alice.chatDir, alice.id, sentAt = alice.meta.itemTs, formattedText = alice.formattedText, content = MsgContent.MCText(alice.content.text)) + ) + ) + } + } else { + Box(Modifier.fillMaxSize()) + } + } + } + + @Composable + fun WallpaperPresetSelector( + selectedWallpaper: WallpaperType?, + baseTheme: DefaultTheme, + activeBackgroundColor: Color? = null, + activeTintColor: Color? = null, + currentColors: (WallpaperType?) -> ThemeManager.ActiveTheme, + onChooseType: (WallpaperType?) -> Unit, + ) { + val cornerRadius = 22 + + @Composable + fun Plus(tint: Color = MaterialTheme.colors.primary) { + Icon(painterResource(MR.images.ic_add), null, Modifier.size(25.dp), tint = tint) + } + + val backgrounds = PresetWallpaper.entries.toList() + + fun LazyGridScope.gridContent(width: Dp, height: Dp) { + @Composable + fun BackgroundItem(background: PresetWallpaper?) { + val checked = (background == null && (selectedWallpaper == null || selectedWallpaper == WallpaperType.Empty)) || selectedWallpaper?.samePreset(background) == true + Box( + Modifier + .size(width, height) + .clip(RoundedCornerShape(percent = cornerRadius)) + .border(1.dp, if (checked) MaterialTheme.colors.primary.copy(0.8f) else MaterialTheme.colors.onBackground.copy(if (isInDarkTheme()) 0.2f else 0.1f), RoundedCornerShape(percent = cornerRadius)) + .clickable { onChooseType(background?.toType(baseTheme)) }, + contentAlignment = Alignment.Center + ) { + if (background != null) { + val type = background.toType(baseTheme, if (checked) selectedWallpaper?.scale else null) + SimpleXThemeOverride(remember(background, selectedWallpaper, CurrentColors.collectAsState().value) { currentColors(type) }) { + ChatThemePreview( + baseTheme, + type.image, + type, + withMessages = false, + backgroundColor = if (checked) activeBackgroundColor ?: MaterialTheme.wallpaper.background else MaterialTheme.wallpaper.background, + tintColor = if (checked) activeTintColor ?: MaterialTheme.wallpaper.tint else MaterialTheme.wallpaper.tint + ) + } + } + } + } + + @Composable + fun OwnBackgroundItem(type: WallpaperType?) { + val overrides = remember(type, baseTheme, CurrentColors.collectAsState().value.wallpaper) { + currentColors(WallpaperType.Image("", null, null)) + } + val appWallpaper = overrides.wallpaper + val backgroundColor = appWallpaper.background + val tintColor = appWallpaper.tint + val wallpaperImage = appWallpaper.type.image + val checked = type is WallpaperType.Image && wallpaperImage != null + val remoteHostConnected = chatModel.remoteHostId != null + Box( + Modifier + .size(width, height) + .clip(RoundedCornerShape(percent = cornerRadius)) + .border(1.dp, if (type is WallpaperType.Image) MaterialTheme.colors.primary.copy(0.8f) else MaterialTheme.colors.onBackground.copy(0.1f), RoundedCornerShape(percent = cornerRadius)) + .clickable { onChooseType(WallpaperType.Image("", null, null)) }, + contentAlignment = Alignment.Center + ) { + + if (checked || wallpaperImage != null) { + ChatThemePreview( + baseTheme, + wallpaperImage, + if (checked) type else appWallpaper.type, + backgroundColor = if (checked) activeBackgroundColor ?: backgroundColor else backgroundColor, + tintColor = if (checked) activeTintColor ?: tintColor else tintColor, + withMessages = false + ) + } else if (remoteHostConnected) { + Plus(MaterialTheme.colors.error) + } else { + Plus() + } + } + } + + item { + BackgroundItem(null) + } + items(items = backgrounds) { background -> + BackgroundItem(background) + } + item { + OwnBackgroundItem(selectedWallpaper) + } + } + + SimpleXThemeOverride(remember(selectedWallpaper, CurrentColors.collectAsState().value) { currentColors(selectedWallpaper) }) { + ChatThemePreview( + baseTheme, + MaterialTheme.wallpaper.type.image, + selectedWallpaper, + backgroundColor = activeBackgroundColor ?: MaterialTheme.wallpaper.background, + tintColor = activeTintColor ?: MaterialTheme.wallpaper.tint, + ) + } + + if (appPlatform.isDesktop) { + val itemWidth = (DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier - DEFAULT_PADDING * 2 - DEFAULT_PADDING_HALF * 3) / 4 + val itemHeight = (DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier - DEFAULT_PADDING * 2) / 4 + val rows = ceil((PresetWallpaper.entries.size + 2) / 4f).roundToInt() + LazyVerticalGrid( + columns = GridCells.Fixed(4), + Modifier.height(itemHeight * rows + DEFAULT_PADDING_HALF * (rows - 1) + DEFAULT_PADDING * 2), + contentPadding = PaddingValues(DEFAULT_PADDING), + verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF), + horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF), + ) { + gridContent(itemWidth, itemHeight) + } + } else { + LazyHorizontalGrid( + rows = GridCells.Fixed(1), + Modifier.height(80.dp + DEFAULT_PADDING * 2), + contentPadding = PaddingValues(DEFAULT_PADDING), + horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF), + ) { + gridContent(80.dp, 80.dp) + } + } + } + + @Composable + fun ThemesSection(systemDarkTheme: SharedPreference<String?>) { + val currentTheme by CurrentColors.collectAsState() + val baseTheme = currentTheme.base + val wallpaperType = MaterialTheme.wallpaper.type + val themeUserDestination: MutableState<Pair<Long, ThemeModeOverrides?>?> = rememberSaveable(stateSaver = serializableSaver()) { + val currentUser = chatModel.currentUser.value + mutableStateOf( + if (currentUser?.uiThemes?.preferredMode(!currentTheme.colors.isLight) == null) null else currentUser.userId to currentUser.uiThemes + ) + } + val perUserTheme = remember(CurrentColors.collectAsState().value.base, chatModel.currentUser.value) { + mutableStateOf( + chatModel.currentUser.value?.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) ?: ThemeModeOverride() + ) + } + + fun updateThemeUserDestination() { + var (userId, themes) = themeUserDestination.value ?: return + themes = if (perUserTheme.value.mode == DefaultThemeMode.LIGHT) { + (themes ?: ThemeModeOverrides()).copy(light = perUserTheme.value) + } else { + (themes ?: ThemeModeOverrides()).copy(dark = perUserTheme.value) + } + themeUserDestination.value = userId to themes + } + + val onTypeCopyFromSameTheme = { type: WallpaperType? -> + if (themeUserDestination.value == null) { + ThemeManager.saveAndApplyWallpaper(baseTheme, type) + } else { + val wallpaperFiles = setOf(perUserTheme.value.wallpaper?.imageFile) + ThemeManager.copyFromSameThemeOverrides(type, null, perUserTheme) + val wallpaperFilesToDelete = wallpaperFiles - perUserTheme.value.wallpaper?.imageFile + wallpaperFilesToDelete.forEach(::removeWallpaperFile) + updateThemeUserDestination() + } + saveThemeToDatabase(themeUserDestination.value) + true + } + + val onTypeChange = { type: WallpaperType? -> + if (themeUserDestination.value == null) { + ThemeManager.saveAndApplyWallpaper(baseTheme, type) + } else { + ThemeManager.applyWallpaper(type, perUserTheme) + updateThemeUserDestination() + } + saveThemeToDatabase(themeUserDestination.value) + } + + val onImport = { to: URI -> + val filename = saveWallpaperFile(to) + if (filename != null) { + if (themeUserDestination.value == null) { + removeWallpaperFile((currentTheme.wallpaper.type as? WallpaperType.Image)?.filename) + } else { + removeWallpaperFile((perUserTheme.value.type as? WallpaperType.Image)?.filename) + } + onTypeChange(WallpaperType.Image(filename, 1f, WallpaperScaleType.FILL)) + } + } + + val currentColors = { type: WallpaperType? -> + // If applying for : + // - all themes: no overrides needed + // - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected + val perUserOverride = if (themeUserDestination.value == null) null else if (wallpaperType.sameType(type)) chatModel.currentUser.value?.uiThemes else null + ThemeManager.currentColors(type, null, perUserOverride, appPrefs.themeOverrides.get()) + } + + val onChooseType: (WallpaperType?, FileChooserLauncher) -> Unit = { type: WallpaperType?, importWallpaperLauncher: FileChooserLauncher -> + when { + // don't have image in parent or already selected wallpaper with custom image + type is WallpaperType.Image && + ((wallpaperType is WallpaperType.Image && themeUserDestination.value?.second != null && chatModel.remoteHostId() == null) || + currentColors(type).wallpaper.type.image == null || + (currentColors(type).wallpaper.type.image != null && CurrentColors.value.wallpaper.type is WallpaperType.Image && themeUserDestination.value == null)) -> + withLongRunningApi { importWallpaperLauncher.launch("image/*") } + type is WallpaperType.Image && themeUserDestination.value == null -> onTypeChange(currentColors(type).wallpaper.type) + type is WallpaperType.Image && chatModel.remoteHostId() != null -> { /* do nothing when remote host connected */ } + type is WallpaperType.Image -> onTypeCopyFromSameTheme(currentColors(type).wallpaper.type) + (themeUserDestination.value != null && themeUserDestination.value?.second?.preferredMode(!CurrentColors.value.colors.isLight)?.type != type) || CurrentColors.value.wallpaper.type != type -> onTypeCopyFromSameTheme(type) + else -> onTypeChange(type) + } + } + + SectionView(stringResource(MR.strings.settings_section_title_themes)) { + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + ThemeDestinationPicker(themeUserDestination) + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + + val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) onImport(to) + } + + WallpaperPresetSelector( + selectedWallpaper = wallpaperType, + baseTheme = currentTheme.base, + currentColors = { type -> + currentColors(type) + }, + onChooseType = { onChooseType(it, importWallpaperLauncher) }, + ) + val type = MaterialTheme.wallpaper.type + if (type is WallpaperType.Image && (themeUserDestination.value == null || perUserTheme.value.wallpaper?.imageFile != null)) { + SectionItemView(disabled = chatModel.remoteHostId != null && themeUserDestination.value != null, click = { + if (themeUserDestination.value == null) { + val defaultActiveTheme = ThemeManager.defaultActiveTheme(appPrefs.themeOverrides.get()) + ThemeManager.saveAndApplyWallpaper(baseTheme, null) + ThemeManager.removeTheme(defaultActiveTheme?.themeId) + removeWallpaperFile(type.filename) + } else { + removeUserThemeModeOverrides(themeUserDestination, perUserTheme) + } + saveThemeToDatabase(themeUserDestination.value) + }) { + Text( + stringResource(MR.strings.theme_remove_image), + color = if (chatModel.remoteHostId != null && themeUserDestination.value != null) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + SectionSpacer() + } + + val state: State<DefaultThemeMode?> = remember(appPrefs.currentTheme.get()) { + derivedStateOf { + if (appPrefs.currentTheme.get() == DefaultTheme.SYSTEM_THEME_NAME) null else currentTheme.base.mode + } + } + ColorModeSelector(state) { + val newTheme = when (it) { + null -> DefaultTheme.SYSTEM_THEME_NAME + DefaultThemeMode.LIGHT -> DefaultTheme.LIGHT.themeName + DefaultThemeMode.DARK -> appPrefs.systemDarkTheme.get()!! + } + ThemeManager.applyTheme(newTheme) + saveThemeToDatabase(null) + } + + // Doesn't work on desktop when specified like remember { systemDarkTheme.state }, this is workaround + val darkModeState: State<String?> = remember(systemDarkTheme.get()) { derivedStateOf { systemDarkTheme.get() } } + DarkModeThemeSelector(darkModeState) { + ThemeManager.changeDarkTheme(it) + if (appPrefs.currentTheme.get() == DefaultTheme.SYSTEM_THEME_NAME) { + ThemeManager.applyTheme(appPrefs.currentTheme.get()!!) + } else if (appPrefs.currentTheme.get() != DefaultTheme.LIGHT.themeName) { + ThemeManager.applyTheme(appPrefs.systemDarkTheme.get()!!) + } + saveThemeToDatabase(null) + } + } + SectionItemView(click = { + val user = themeUserDestination.value + if (user == null) { + ModalManager.start.showModal { + val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) onImport(to) + } + CustomizeThemeView { onChooseType(it, importWallpaperLauncher) } + } + } else { + ModalManager.start.showModalCloseable { close -> + UserWallpaperEditorModal(chatModel.remoteHostId(), user.first, close) + } + } + }) { + Text(stringResource(MR.strings.customize_theme_title)) + } + } + + @Composable + fun CustomizeThemeView(onChooseType: (WallpaperType?) -> Unit) { + ColumnWithScrollBar { val currentTheme by CurrentColors.collectAsState() AppBarTitle(stringResource(MR.strings.customize_theme_title)) + val wallpaperImage = MaterialTheme.wallpaper.type.image + val wallpaperType = MaterialTheme.wallpaper.type + val baseTheme = CurrentColors.collectAsState().value.base - SectionView(stringResource(MR.strings.theme_colors_section_title)) { - SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY, currentTheme.colors.primary) }) { - val title = generalGetString(MR.strings.color_primary) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.primary) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY_VARIANT, currentTheme.colors.primaryVariant) }) { - val title = generalGetString(MR.strings.color_primary_variant) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.primaryVariant) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY, currentTheme.colors.secondary) }) { - val title = generalGetString(MR.strings.color_secondary) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.secondary) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY_VARIANT, currentTheme.colors.secondaryVariant) }) { - val title = generalGetString(MR.strings.color_secondary_variant) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.secondaryVariant) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.BACKGROUND, currentTheme.colors.background) }) { - val title = generalGetString(MR.strings.color_background) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.background) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.SURFACE, currentTheme.colors.surface) }) { - val title = generalGetString(MR.strings.color_surface) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.surface) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.TITLE, currentTheme.appColors.title) }) { - val title = generalGetString(MR.strings.color_title) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.title) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_MESSAGE, currentTheme.appColors.sentMessage) }) { - val title = generalGetString(MR.strings.color_sent_message) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.sentMessage) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_MESSAGE, currentTheme.appColors.receivedMessage) }) { - val title = generalGetString(MR.strings.color_received_message) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.receivedMessage) - } + val editColor = { name: ThemeColor -> + editColor( + name, + wallpaperType, + wallpaperImage, + onColorChange = { color -> + ThemeManager.saveAndApplyThemeColor(baseTheme, name, color) + saveThemeToDatabase(null) + } + ) } - val isInDarkTheme = isInDarkTheme() - if (currentTheme.base.hasChangedAnyColor(currentTheme.colors, currentTheme.appColors)) { - SectionItemView({ ThemeManager.resetAllThemeColors(darkForSystemTheme = isInDarkTheme) }) { + + WallpaperPresetSelector( + selectedWallpaper = wallpaperType, + baseTheme = currentTheme.base, + currentColors = { type -> + ThemeManager.currentColors(type, null, null, appPrefs.themeOverrides.get()) + }, + onChooseType = onChooseType + ) + + val type = MaterialTheme.wallpaper.type + if (type is WallpaperType.Image) { + SectionItemView(disabled = chatModel.remoteHostId != null, click = { + val defaultActiveTheme = ThemeManager.defaultActiveTheme(appPrefs.themeOverrides.get()) + ThemeManager.saveAndApplyWallpaper(baseTheme, null) + ThemeManager.removeTheme(defaultActiveTheme?.themeId) + removeWallpaperFile(type.filename) + saveThemeToDatabase(null) + }) { + Text( + stringResource(MR.strings.theme_remove_image), + color = if (chatModel.remoteHostId == null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + ) + } + SectionSpacer() + } + + SectionView(stringResource(MR.strings.settings_section_title_chat_colors).uppercase()) { + WallpaperSetupView( + wallpaperType, + baseTheme, + MaterialTheme.wallpaper, + MaterialTheme.appColors.sentMessage, + MaterialTheme.appColors.sentQuote, + MaterialTheme.appColors.receivedMessage, + MaterialTheme.appColors.receivedQuote, + editColor = { name -> + editColor(name) + }, + onTypeChange = { type -> + ThemeManager.saveAndApplyWallpaper(baseTheme, type) + saveThemeToDatabase(null) + }, + ) + } + SectionDividerSpaced() + + CustomizeThemeColorsSection(currentTheme) { name -> + editColor(name) + } + + SectionDividerSpaced(maxBottomPadding = false) + + val currentOverrides = remember(currentTheme) { ThemeManager.defaultActiveTheme(appPrefs.themeOverrides.get()) } + val canResetColors = currentTheme.base.hasChangedAnyColor(currentOverrides) + if (canResetColors) { + SectionItemView({ + ThemeManager.resetAllThemeColors() + saveThemeToDatabase(null) + }) { Text(generalGetString(MR.strings.reset_color), color = colors.primary) } + SectionSpacer() } - SectionSpacer() + SectionView { val theme = remember { mutableStateOf(null as String?) } val exportThemeLauncher = rememberFileChooserLauncher(false) { to: URI? -> @@ -129,9 +699,11 @@ object AppearanceScope { } } SectionItemView({ - val overrides = ThemeManager.currentThemeOverridesForExport(isInDarkTheme) - theme.value = yaml.encodeToString<ThemeOverrides>(overrides) - withApi { exportThemeLauncher.launch("simplex.theme")} + val overrides = ThemeManager.currentThemeOverridesForExport(null, null/*chatModel.currentUser.value?.uiThemes*/) + val lines = yaml.encodeToString<ThemeOverrides>(overrides).lines() + // Removing theme id without using custom serializer or data class + theme.value = lines.subList(1, lines.size).joinToString("\n") + withLongRunningApi { exportThemeLauncher.launch("simplex.theme") } }) { Text(generalGetString(MR.strings.export_theme), color = colors.primary) } @@ -139,12 +711,13 @@ object AppearanceScope { if (to != null) { val theme = getThemeFromUri(to) if (theme != null) { - ThemeManager.saveAndApplyThemeOverrides(theme, isInDarkTheme) + ThemeManager.saveAndApplyThemeOverrides(theme) + saveThemeToDatabase(null) } } } // Can not limit to YAML mime type since it's unsupported by Android - SectionItemView({ withApi { importThemeLauncher.launch("*/*") } }) { + SectionItemView({ withLongRunningApi { importThemeLauncher.launch("*/*") } }) { Text(generalGetString(MR.strings.import_theme), color = colors.primary) } } @@ -153,48 +726,368 @@ object AppearanceScope { } @Composable - fun ColorEditor( - name: ThemeColor, - initialColor: Color, - close: () -> Unit, - ) { - Column( - Modifier - .fillMaxWidth() - ) { - AppBarTitle(name.text) - var currentColor by remember { mutableStateOf(initialColor) } - ColorPicker(initialColor) { - currentColor = it - } + fun ColorModeSwitcher() { + val currentTheme by CurrentColors.collectAsState() + val themeMode = if (remember { appPrefs.currentTheme.state }.value == DefaultTheme.SYSTEM_THEME_NAME) { + if (systemInDarkThemeCurrently) DefaultThemeMode.DARK else DefaultThemeMode.LIGHT + } else { + currentTheme.base.mode + } - SectionSpacer() - val isInDarkTheme = isInDarkTheme() - TextButton( - onClick = { - ThemeManager.saveAndApplyThemeColor(name, currentColor, isInDarkTheme) - close() - }, - Modifier.align(Alignment.CenterHorizontally), - colors = ButtonDefaults.textButtonColors(contentColor = currentColor) - ) { - Text(generalGetString(MR.strings.save_color)) + val onLongClick = { + ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME) + showToast(generalGetString(MR.strings.system_mode_toast)) + + saveThemeToDatabase(null) + } + Box( + modifier = Modifier + .clip(CircleShape) + .combinedClickable( + onClick = { + ThemeManager.applyTheme(if (themeMode == DefaultThemeMode.LIGHT) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.themeName) + saveThemeToDatabase(null) + }, + onLongClick = onLongClick + ) + .onRightClick(onLongClick) + .size(44.dp), + contentAlignment = Alignment.Center + ) { + Icon(painterResource(if (themeMode == DefaultThemeMode.LIGHT) MR.images.ic_light_mode else MR.images.ic_bedtime_moon), stringResource(MR.strings.color_mode_light), tint = MaterialTheme.colors.secondary) + } + } + + private var updateBackendJob: Job = Job() + private fun saveThemeToDatabase(themeUserDestination: Pair<Long, ThemeModeOverrides?>?) { + val remoteHostId = chatModel.remoteHostId() + val oldThemes = chatModel.currentUser.value?.uiThemes + if (themeUserDestination != null) { + // Update before save to make it work seamless + chatModel.updateCurrentUserUiThemes(remoteHostId, themeUserDestination.second) + } + updateBackendJob.cancel() + updateBackendJob = withBGApi { + delay(300) + if (themeUserDestination == null) { + controller.apiSaveAppSettings(AppSettings.current.prepareForExport()) + } else if (!controller.apiSetUserUIThemes(remoteHostId, themeUserDestination.first, themeUserDestination.second)) { + // If failed to apply for some reason return the old themes + chatModel.updateCurrentUserUiThemes(remoteHostId, oldThemes) + } + } + } + + fun editColor(name: ThemeColor, wallpaperType: WallpaperType, wallpaperImage: ImageBitmap?, onColorChange: (Color?) -> Unit) { + ModalManager.start.showModal { + val baseTheme = CurrentColors.collectAsState().value.base + val wallpaperBackgroundColor = MaterialTheme.wallpaper.background ?: wallpaperType.defaultBackgroundColor(baseTheme, MaterialTheme.colors.background) + val wallpaperTintColor = MaterialTheme.wallpaper.tint ?: wallpaperType.defaultTintColor(baseTheme) + val initialColor: Color = when (name) { + ThemeColor.WALLPAPER_BACKGROUND -> wallpaperBackgroundColor + ThemeColor.WALLPAPER_TINT -> wallpaperTintColor + ThemeColor.PRIMARY -> MaterialTheme.colors.primary + ThemeColor.PRIMARY_VARIANT -> MaterialTheme.colors.primaryVariant + ThemeColor.SECONDARY -> MaterialTheme.colors.secondary + ThemeColor.SECONDARY_VARIANT -> MaterialTheme.colors.secondaryVariant + ThemeColor.BACKGROUND -> MaterialTheme.colors.background + ThemeColor.SURFACE -> MaterialTheme.colors.surface + ThemeColor.TITLE -> MaterialTheme.appColors.title + ThemeColor.PRIMARY_VARIANT2 -> MaterialTheme.appColors.primaryVariant2 + ThemeColor.SENT_MESSAGE -> MaterialTheme.appColors.sentMessage + ThemeColor.SENT_QUOTE -> MaterialTheme.appColors.sentQuote + ThemeColor.RECEIVED_MESSAGE -> MaterialTheme.appColors.receivedMessage + ThemeColor.RECEIVED_QUOTE -> MaterialTheme.appColors.receivedQuote + } + ColorEditor(name, initialColor, baseTheme, MaterialTheme.wallpaper.type, wallpaperImage, currentColors = { CurrentColors.value }, + onColorChange = onColorChange + ) + } + } + + @Composable + fun ModalData.UserWallpaperEditorModal(remoteHostId: Long?, userId: Long, close: () -> Unit) { + val themes = remember(chatModel.currentUser.value) { mutableStateOf(chatModel.currentUser.value?.uiThemes ?: ThemeModeOverrides()) } + val globalThemeUsed = remember { stateGetOrPut("globalThemeUsed") { false } } + val initialTheme = remember(CurrentColors.collectAsState().value.base) { + val preferred = themes.value.preferredMode(!CurrentColors.value.colors.isLight) + globalThemeUsed.value = preferred == null + preferred ?: ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + } + UserWallpaperEditor( + initialTheme, + applyToMode = if (themes.value.light == themes.value.dark) null else initialTheme.mode, + globalThemeUsed = globalThemeUsed, + save = { applyToMode, newTheme -> + save(applyToMode, newTheme, themes.value, userId, remoteHostId) + }) + KeyChangeEffect(chatModel.currentUser.value?.userId, chatModel.remoteHostId) { + close() + } + } + + suspend fun save( + applyToMode: DefaultThemeMode?, + newTheme: ThemeModeOverride?, + themes: ThemeModeOverrides?, + userId: Long, + remoteHostId: Long? + ) { + val unchangedThemes: ThemeModeOverrides = themes ?: ThemeModeOverrides() + val wallpaperFiles = setOf(unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile) + var changedThemes: ThemeModeOverrides? = unchangedThemes + val changed = newTheme?.copy(wallpaper = newTheme.wallpaper?.withFilledWallpaperPath()) + changedThemes = when (applyToMode) { + null -> changedThemes?.copy(light = changed?.copy(mode = DefaultThemeMode.LIGHT), dark = changed?.copy(mode = DefaultThemeMode.DARK)) + DefaultThemeMode.LIGHT -> changedThemes?.copy(light = changed?.copy(mode = applyToMode)) + DefaultThemeMode.DARK -> changedThemes?.copy(dark = changed?.copy(mode = applyToMode)) + } + changedThemes = if (changedThemes?.light != null || changedThemes?.dark != null) { + val light = changedThemes.light + val dark = changedThemes.dark + val currentMode = CurrentColors.value.base.mode + // same image file for both modes, copy image to make them as different files + if (light?.wallpaper?.imageFile != null && dark?.wallpaper?.imageFile != null && light.wallpaper.imageFile == dark.wallpaper.imageFile) { + val imageFile = if (currentMode == DefaultThemeMode.LIGHT) { + dark.wallpaper.imageFile + } else { + light.wallpaper.imageFile + } + val filePath = saveWallpaperFile(File(getWallpaperFilePath(imageFile)).toURI()) + changedThemes = if (currentMode == DefaultThemeMode.LIGHT) { + changedThemes.copy(dark = dark.copy(wallpaper = dark.wallpaper.copy(imageFile = filePath))) + } else { + changedThemes.copy(light = light.copy(wallpaper = light.wallpaper.copy(imageFile = filePath))) + } + } + changedThemes + } else { + null + } + + val wallpaperFilesToDelete = wallpaperFiles - changedThemes?.light?.wallpaper?.imageFile - changedThemes?.dark?.wallpaper?.imageFile + wallpaperFilesToDelete.forEach(::removeWallpaperFile) + + val oldThemes = chatModel.currentUser.value?.uiThemes + // Update before save to make it work seamless + chatModel.updateCurrentUserUiThemes(remoteHostId, changedThemes) + updateBackendJob.cancel() + updateBackendJob = withBGApi { + delay(300) + if (!controller.apiSetUserUIThemes(remoteHostId, userId, changedThemes)) { + // If failed to apply for some reason return the old themes + chatModel.updateCurrentUserUiThemes(remoteHostId, oldThemes) } } } @Composable - fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) { - ClassicColorPicker(modifier = Modifier - .fillMaxWidth() - .height(300.dp), - color = HsvColor.from(color = initialColor), showAlphaBar = true, - onColorChanged = { color: HsvColor -> - onColorChanged(color.toColor()) + fun ThemeDestinationPicker(themeUserDestination: MutableState<Pair<Long, ThemeModeOverrides?>?>) { + val themeUserDest = remember(themeUserDestination.value?.first) { mutableStateOf(themeUserDestination.value?.first) } + LaunchedEffect(themeUserDestination.value) { + if (themeUserDestination.value == null) { + // Easiest way to hide per-user customization. + // Otherwise, it would be needed to make global variable and to use it everywhere for making a decision to include these overrides into active theme constructing or not + chatModel.currentUser.value = chatModel.currentUser.value?.copy(uiThemes = null) + } else { + chatModel.updateCurrentUserUiThemes(chatModel.remoteHostId(), chatModel.users.firstOrNull { it.user.userId == chatModel.currentUser.value?.userId }?.user?.uiThemes) } - ) + } + DisposableEffect(Unit) { + onDispose { + // Skip when Appearance screen is not hidden yet + if (ModalManager.start.hasModalsOpen()) return@onDispose + // Restore user overrides from stored list of users + chatModel.updateCurrentUserUiThemes(chatModel.remoteHostId(), chatModel.users.firstOrNull { it.user.userId == chatModel.currentUser.value?.userId }?.user?.uiThemes) + themeUserDestination.value = if (chatModel.currentUser.value?.uiThemes == null) null else chatModel.currentUser.value?.userId!! to chatModel.currentUser.value?.uiThemes + } + } + + val values by remember(chatModel.users.toList()) { mutableStateOf( + listOf(null as Long? to generalGetString(MR.strings.theme_destination_app_theme)) + + + chatModel.users.filter { it.user.activeUser }.map { + it.user.userId to it.user.chatViewName + }, + ) + } + if (values.any { it.first == themeUserDestination.value?.first }) { + ExposedDropDownSettingRow( + generalGetString(MR.strings.chat_theme_apply_to_mode), + values, + themeUserDest, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = { userId -> + themeUserDest.value = userId + if (userId != null) { + themeUserDestination.value = userId to chatModel.users.firstOrNull { it.user.userId == userId }?.user?.uiThemes + } else { + themeUserDestination.value = null + } + if (userId != null && userId != chatModel.currentUser.value?.userId) { + withBGApi { + controller.showProgressIfNeeded { + chatModel.controller.changeActiveUser(chatModel.remoteHostId(), userId, null) + } + } + } + } + ) + } else { + themeUserDestination.value = null + } } + @Composable + fun CustomizeThemeColorsSection(currentTheme: ThemeManager.ActiveTheme, editColor: (ThemeColor) -> Unit) { + SectionView(stringResource(MR.strings.theme_colors_section_title)) { + SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY) }) { + val title = generalGetString(MR.strings.color_primary) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.colors.primary) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY_VARIANT) }) { + val title = generalGetString(MR.strings.color_primary_variant) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.colors.primaryVariant) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY_VARIANT2) }) { + val title = generalGetString(MR.strings.color_primary_variant2) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.primaryVariant2) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY) }) { + val title = generalGetString(MR.strings.color_secondary) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.colors.secondary) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY_VARIANT) }) { + val title = generalGetString(MR.strings.color_secondary_variant) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.colors.secondaryVariant) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.BACKGROUND) }) { + val title = generalGetString(MR.strings.color_background) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.colors.background) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SURFACE) }) { + val title = generalGetString(MR.strings.color_surface) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.colors.surface) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.TITLE) }) { + val title = generalGetString(MR.strings.color_title) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.title) + } + } + } + + @Composable + fun ColorEditor( + name: ThemeColor, + initialColor: Color, + theme: DefaultTheme, + wallpaperType: WallpaperType?, + wallpaperImage: ImageBitmap?, + previewBackgroundColor: Color? = MaterialTheme.wallpaper.background, + previewTintColor: Color? = MaterialTheme.wallpaper.tint, + currentColors: () -> ThemeManager.ActiveTheme, + onColorChange: (Color?) -> Unit, + ) { + ColumnWithScrollBar(Modifier.imePadding()) { + AppBarTitle(name.text) + + val supportedLiveChange = name in listOf(ThemeColor.SECONDARY, ThemeColor.BACKGROUND, ThemeColor.SURFACE, ThemeColor.RECEIVED_MESSAGE, ThemeColor.SENT_MESSAGE, ThemeColor.SENT_QUOTE, ThemeColor.WALLPAPER_BACKGROUND, ThemeColor.WALLPAPER_TINT) + if (supportedLiveChange) { + SimpleXThemeOverride(currentColors()) { + ChatThemePreview(theme, wallpaperImage, wallpaperType, previewBackgroundColor, previewTintColor) + } + SectionSpacer() + } + + var currentColor by remember { mutableStateOf(initialColor) } + val togglePicker = remember { mutableStateOf(false) } + Box(Modifier.padding(horizontal = DEFAULT_PADDING)) { + if (togglePicker.value) { + ColorPicker(currentColor, showAlphaBar = wallpaperType is WallpaperType.Image || currentColor.alpha < 1f) { + currentColor = it + onColorChange(currentColor) + } + } else { + ColorPicker(currentColor, showAlphaBar = wallpaperType is WallpaperType.Image || currentColor.alpha < 1f) { + currentColor = it + onColorChange(currentColor) + } + } + } + var allowReloadPicker by remember { mutableStateOf(false) } + KeyChangeEffect(wallpaperType) { + allowReloadPicker = true + } + KeyChangeEffect(initialColor) { + if (initialColor != currentColor && allowReloadPicker) { + currentColor = initialColor + togglePicker.value = !togglePicker.value + } + allowReloadPicker = false + } + val clipboard = LocalClipboardManager.current + val hexTrimmed = currentColor.toReadableHex().replaceFirst("#ff", "#") + val savedColor by remember(wallpaperType) { mutableStateOf(initialColor) } + + Row(Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).height(DEFAULT_MIN_SECTION_ITEM_HEIGHT)) { + Box(Modifier.weight(1f).fillMaxHeight().background(savedColor).clickable { + currentColor = savedColor + onColorChange(currentColor) + togglePicker.value = !togglePicker.value + }) + Box(Modifier.weight(1f).fillMaxHeight().background(currentColor).clickable { + clipboard.shareText(hexTrimmed) + }) + } + if (appPrefs.developerTools.get()) { + Row(Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING), horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically) { + val textFieldState = remember { mutableStateOf(TextFieldValue(hexTrimmed)) } + KeyChangeEffect(hexTrimmed) { + textFieldState.value = textFieldState.value.copy(hexTrimmed) + } + DefaultBasicTextField( + Modifier.fillMaxWidth(), + textFieldState, + leadingIcon = { + IconButton(onClick = { clipboard.shareText(hexTrimmed) }) { + Icon(painterResource(MR.images.ic_content_copy), generalGetString(MR.strings.copy_verb), Modifier.size(26.dp), tint = MaterialTheme.colors.primary) + } + }, + onValueChange = { value -> + val color = value.text.trim('#', ' ') + if (color.length == 6 || color.length == 8) { + currentColor = if (color.length == 6) ("ff$color").colorFromReadableHex() else color.colorFromReadableHex() + onColorChange(currentColor) + textFieldState.value = value.copy(currentColor.toReadableHex().replaceFirst("#ff", "#")) + togglePicker.value = !togglePicker.value + } else { + textFieldState.value = value + } + } + ) + } + } + SectionItemView({ + allowReloadPicker = true + onColorChange(null) + }) { + Text(generalGetString(MR.strings.reset_single_color), color = colors.primary) + } + SectionSpacer() + } + } + + + @Composable fun LangSelector(state: State<String>, onSelected: (String) -> Unit) { // Should be the same as in app/build.gradle's `android.defaultConfig.resConfigs` @@ -203,23 +1096,28 @@ object AppearanceScope { "en" to "English", "ar" to "العربية", "bg" to "Български", + "ca" to "Català", "cs" to "Čeština", "de" to "Deutsch", "es" to "Español", + "fa" to "فارسی", "fi" to "Suomi", "fr" to "Français", + "hu" to "Magyar", "it" to "Italiano", "iw" to "עִברִית", "ja" to "日本語", + "lt" to "Lietuvių", "nl" to "Nederlands", "pl" to "Polski", "pt-BR" to "Português, Brasil", "ru" to "Русский", "th" to "ภาษาไทย", + "tr" to "Türkçe", "uk" to "Українська", "zh-CN" to "简体中文" ) - val values by remember(ChatController.appPrefs.appLanguage.state.value) { mutableStateOf(supportedLanguages.map { it.key to it.value }) } + val values by remember(appPrefs.appLanguage.state.value) { mutableStateOf(supportedLanguages.map { it.key to it.value }) } ExposedDropDownSettingRow( generalGetString(MR.strings.settings_section_title_language).lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() }, values, @@ -231,13 +1129,18 @@ object AppearanceScope { } @Composable - private fun ThemeSelector(state: State<String>, onSelected: (String) -> Unit) { - val darkTheme = chat.simplex.common.ui.theme.isSystemInDarkTheme() - val values by remember(ChatController.appPrefs.appLanguage.state.value) { - mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second.name to it.third }) + private fun ColorModeSelector(state: State<DefaultThemeMode?>, onSelected: (DefaultThemeMode?) -> Unit) { + val values by remember(appPrefs.appLanguage.state.value) { + mutableStateOf( + listOf( + null to generalGetString(MR.strings.color_mode_system), + DefaultThemeMode.LIGHT to generalGetString(MR.strings.color_mode_light), + DefaultThemeMode.DARK to generalGetString(MR.strings.color_mode_dark) + ) + ) } ExposedDropDownSettingRow( - generalGetString(MR.strings.theme), + generalGetString(MR.strings.color_mode), values, state, icon = null, @@ -247,15 +1150,16 @@ object AppearanceScope { } @Composable - private fun DarkThemeSelector(state: State<String?>, onSelected: (String) -> Unit) { + private fun DarkModeThemeSelector(state: State<String?>, onSelected: (String) -> Unit) { val values by remember { val darkThemes = ArrayList<Pair<String, String>>() - darkThemes.add(DefaultTheme.DARK.name to generalGetString(MR.strings.theme_dark)) - darkThemes.add(DefaultTheme.SIMPLEX.name to generalGetString(MR.strings.theme_simplex)) + darkThemes.add(DefaultTheme.DARK.themeName to generalGetString(MR.strings.theme_dark)) + darkThemes.add(DefaultTheme.SIMPLEX.themeName to generalGetString(MR.strings.theme_simplex)) + darkThemes.add(DefaultTheme.BLACK.themeName to generalGetString(MR.strings.theme_black)) mutableStateOf(darkThemes.toList()) } ExposedDropDownSettingRow( - generalGetString(MR.strings.dark_theme), + generalGetString(MR.strings.dark_mode_colors), values, state, icon = null, @@ -268,3 +1172,109 @@ object AppearanceScope { //} } +@Composable +fun WallpaperSetupView( + wallpaperType: WallpaperType?, + theme: DefaultTheme, + initialWallpaper: AppWallpaper?, + initialSentColor: Color, + initialSentQuoteColor: Color, + initialReceivedColor: Color, + initialReceivedQuoteColor: Color, + editColor: (ThemeColor) -> Unit, + onTypeChange: (WallpaperType?) -> Unit, +) { + if (wallpaperType is WallpaperType.Image) { + val state = remember(wallpaperType.scaleType, initialWallpaper?.type) { mutableStateOf(wallpaperType.scaleType ?: (initialWallpaper?.type as? WallpaperType.Image)?.scaleType ?: WallpaperScaleType.FILL) } + val values = remember { + WallpaperScaleType.entries.map { it to generalGetString(it.text) } + } + ExposedDropDownSettingRow( + stringResource(MR.strings.wallpaper_scale), + values, + state, + onSelected = { scaleType -> + onTypeChange(wallpaperType.copy(scaleType = scaleType)) + } + ) + } + + if (wallpaperType is WallpaperType.Preset || (wallpaperType is WallpaperType.Image && wallpaperType.scaleType == WallpaperScaleType.REPEAT)) { + val state = remember(wallpaperType, initialWallpaper?.type?.scale) { mutableStateOf(wallpaperType.scale ?: initialWallpaper?.type?.scale ?: 1f) } + Row(Modifier.padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) { + Text("${state.value}".substring(0, min("${state.value}".length, 4)), Modifier.width(50.dp)) + Slider( + state.value, + valueRange = 0.5f..2f, + onValueChange = { + if (wallpaperType is WallpaperType.Preset) { + onTypeChange(wallpaperType.copy(scale = it)) + } else if (wallpaperType is WallpaperType.Image) { + onTypeChange(wallpaperType.copy(scale = it)) + } + } + ) + } + } + + if (wallpaperType is WallpaperType.Preset || wallpaperType is WallpaperType.Image) { + val wallpaperBackgroundColor = initialWallpaper?.background ?: wallpaperType.defaultBackgroundColor(theme, MaterialTheme.colors.background) + SectionItemViewSpaceBetween({ editColor(ThemeColor.WALLPAPER_BACKGROUND) }) { + val title = generalGetString(MR.strings.color_wallpaper_background) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = wallpaperBackgroundColor) + } + val wallpaperTintColor = initialWallpaper?.tint ?: wallpaperType.defaultTintColor(theme) + SectionItemViewSpaceBetween({ editColor(ThemeColor.WALLPAPER_TINT) }) { + val title = generalGetString(MR.strings.color_wallpaper_tint) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = wallpaperTintColor) + } + SectionSpacer() + } + + SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_MESSAGE) }) { + val title = generalGetString(MR.strings.color_sent_message) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialSentColor) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_QUOTE) }) { + val title = generalGetString(MR.strings.color_sent_quote) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialSentQuoteColor) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_MESSAGE) }) { + val title = generalGetString(MR.strings.color_received_message) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialReceivedColor) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_QUOTE) }) { + val title = generalGetString(MR.strings.color_received_quote) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialReceivedQuoteColor) + } +} + +@Composable +private fun ColorPicker(initialColor: Color, showAlphaBar: Boolean, onColorChanged: (Color) -> Unit) { + ClassicColorPicker(modifier = Modifier + .fillMaxWidth() + .height(300.dp), + color = HsvColor.from(color = initialColor), + showAlphaBar = showAlphaBar, + onColorChanged = { color: HsvColor -> + onColorChanged(color.toColor()) + } + ) +} + +private fun removeUserThemeModeOverrides(themeUserDestination: MutableState<Pair<Long, ThemeModeOverrides?>?>, perUserTheme: MutableState<ThemeModeOverride>) { + val dest = themeUserDestination.value ?: return + perUserTheme.value = ThemeModeOverride() + themeUserDestination.value = dest.first to null + val wallpaperFilesToDelete = listOf( + (chatModel.currentUser.value?.uiThemes?.light?.type as? WallpaperType.Image)?.filename, + (chatModel.currentUser.value?.uiThemes?.dark?.type as? WallpaperType.Image)?.filename + ) + wallpaperFilesToDelete.forEach(::removeWallpaperFile) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt index d4219a51c5..cb36e4ae1a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt @@ -16,6 +16,7 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* +import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.res.MR @Composable @@ -35,10 +36,7 @@ fun CallSettingsLayout( callOnLockScreen: SharedPreference<CallOnLockScreen>, editIceServers: () -> Unit, ) { - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_calls)) val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) } SectionView(stringResource(MR.strings.settings_section_title_settings)) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt index a6ac8c14ed..c5a4ae5f70 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt @@ -1,6 +1,8 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer +import SectionDividerSpaced +import SectionSpacer import SectionTextFooter import SectionView import androidx.compose.foundation.layout.Column @@ -8,48 +10,63 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.platform.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import chat.simplex.common.model.ChatModel -import chat.simplex.common.platform.appPlatform import chat.simplex.common.views.TerminalView import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @Composable -fun DeveloperView( - m: ChatModel, - showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), - withAuth: (title: String, desc: String, block: () -> Unit) -> Unit +fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) -> Unit ) { - Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) { + val m = chatModel + ColumnWithScrollBar { val uriHandler = LocalUriHandler.current AppBarTitle(stringResource(MR.strings.settings_developer_tools)) val developerTools = m.controller.appPrefs.developerTools val devTools = remember { developerTools.state } - SectionView() { + val unchangedHints = mutableStateOf(unchangedHintPreferences()) + SectionView { InstallTerminalAppItem(uriHandler) - ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential), showCustomModal { it, close -> TerminalView(it, close) })} - SettingsPreferenceItem(painterResource(MR.images.ic_drive_folder_upload), stringResource(MR.strings.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades) + ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.start.showModalCloseable { TerminalView(false) } } } + ResetHintsItem(unchangedHints) SettingsPreferenceItem(painterResource(MR.images.ic_code), stringResource(MR.strings.show_developer_options), developerTools) - if (appPlatform.isDesktop && devTools.value) { - TerminalAlwaysVisibleItem(m.controller.appPrefs.terminalAlwaysVisible) { checked -> - if (checked) { - withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential)) { - m.controller.appPrefs.terminalAlwaysVisible.set(true) + SectionTextFooter( + generalGetString(if (devTools.value) MR.strings.show_dev_options else MR.strings.hide_dev_options) + " " + + generalGetString(MR.strings.developer_options) + ) + } + if (devTools.value) { + SectionDividerSpaced(maxTopPadding = true) + SectionView(stringResource(MR.strings.developer_options_section).uppercase()) { + SettingsActionItemWithContent(painterResource(MR.images.ic_breaking_news), stringResource(MR.strings.debug_logs)) { + DefaultSwitch( + checked = remember { appPrefs.logLevel.state }.value <= LogLevel.DEBUG, + onCheckedChange = { appPrefs.logLevel.set(if (it) LogLevel.DEBUG else LogLevel.WARNING) } + ) + } + SettingsPreferenceItem(painterResource(MR.images.ic_drive_folder_upload), stringResource(MR.strings.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades) + if (appPlatform.isDesktop) { + TerminalAlwaysVisibleItem(m.controller.appPrefs.terminalAlwaysVisible) { checked -> + if (checked) { + withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential)) { + m.controller.appPrefs.terminalAlwaysVisible.set(true) + } + } else { + m.controller.appPrefs.terminalAlwaysVisible.set(false) } - } else { - m.controller.appPrefs.terminalAlwaysVisible.set(false) } } + SettingsPreferenceItem(painterResource(MR.images.ic_report), stringResource(MR.strings.show_internal_errors), appPreferences.showInternalErrors) + SettingsPreferenceItem(painterResource(MR.images.ic_avg_pace), stringResource(MR.strings.show_slow_api_calls), appPreferences.showSlowApiCalls) } } - SectionTextFooter( - generalGetString(if (devTools.value) MR.strings.show_dev_options else MR.strings.hide_dev_options) + " " + - generalGetString(MR.strings.developer_options) - ) SectionBottomSpacer() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt index 33e183aaaf..aaaef31583 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import androidx.compose.desktop.ui.tooling.preview.Preview +import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.common.views.chatlist.ChatHelpView @@ -20,12 +21,7 @@ fun HelpView(userDisplayName: String) { @Composable fun HelpLayout(userDisplayName: String) { - Column( - Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .padding(horizontal = DEFAULT_PADDING), - ){ + ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)){ AppBarTitle(String.format(stringResource(MR.strings.personal_welcome), userDisplayName), withPadding = false) ChatHelpView() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt index f3496c850f..55bd796a3b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt @@ -1,14 +1,12 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer -import SectionItemView import SectionItemViewSpaceBetween +import SectionItemViewWithoutMinPadding import SectionSpacer import SectionTextFooter import SectionView import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -17,6 +15,8 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import chat.simplex.common.model.ChatModel import chat.simplex.common.model.User +import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.ntfManager import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chatlist.UserProfileRow import chat.simplex.common.views.database.PassphraseField @@ -35,6 +35,9 @@ fun HiddenProfileView( withBGApi { try { val u = m.controller.apiHideUser(user, hidePassword) + if (!u.activeUser) { + ntfManager.cancelNotificationsForUser(u.userId) + } m.updateUser(u) close() } catch (e: Exception) { @@ -53,13 +56,9 @@ private fun HiddenProfileLayout( user: User, saveProfilePassword: (String) -> Unit ) { - Column( - Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.hide_profile)) - SectionView(padding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) { + SectionView(contentPadding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) { UserProfileRow(user) } SectionSpacer() @@ -70,10 +69,10 @@ private fun HiddenProfileLayout( val confirmValid by remember { derivedStateOf { confirmHidePassword.value == "" || hidePassword.value == confirmHidePassword.value } } val saveDisabled by remember { derivedStateOf { hidePassword.value == "" || !passwordValid || confirmHidePassword.value == "" || !confirmValid } } SectionView(stringResource(MR.strings.hidden_profile_password).uppercase()) { - SectionItemView { + SectionItemViewWithoutMinPadding { PassphraseField(hidePassword, generalGetString(MR.strings.password_to_show), isValid = { passwordValid }, showStrength = true) } - SectionItemView { + SectionItemViewWithoutMinPadding { PassphraseField(confirmHidePassword, stringResource(MR.strings.confirm_password), isValid = { confirmValid }, dependsOn = hidePassword) } SectionItemViewSpaceBetween({ saveProfilePassword(hidePassword.value) }, disabled = saveDisabled, minHeight = TextFieldDefaults.MinHeight) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/IncognitoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/IncognitoView.kt index e264172f9c..6098f66941 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/IncognitoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/IncognitoView.kt @@ -1,16 +1,17 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer -import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp +import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.AppBarTitle import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.common.views.onboarding.ReadableTextWithLink import chat.simplex.res.MR @Composable @@ -20,17 +21,17 @@ fun IncognitoView() { @Composable fun IncognitoLayout() { - Column { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.settings_section_title_incognito)) Column( Modifier - .verticalScroll(rememberScrollState()) .padding(horizontal = DEFAULT_PADDING), verticalArrangement = Arrangement.spacedBy(20.dp) ) { Text(generalGetString(MR.strings.incognito_info_protects)) Text(generalGetString(MR.strings.incognito_info_allows)) Text(generalGetString(MR.strings.incognito_info_share)) + ReadableTextWithLink(MR.strings.read_more_in_user_guide_with_link, "https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode") SectionBottomSpacer() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt deleted file mode 100644 index fcd602ee2c..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt +++ /dev/null @@ -1,472 +0,0 @@ -package chat.simplex.common.views.usersettings - -import SectionBottomSpacer -import SectionCustomFooter -import SectionItemView -import SectionItemWithValue -import SectionView -import SectionViewSelectable -import TextIconSpaced -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardActions -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 dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.text.* -import androidx.compose.ui.text.font.* -import androidx.compose.ui.text.input.* -import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import chat.simplex.common.model.* -import chat.simplex.common.platform.chatModel -import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.chat.item.ClickableText -import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.helpers.annotatedStringResource -import chat.simplex.res.MR - -@Composable -fun NetworkAndServersView( - chatModel: ChatModel, - showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), -) { - val currentRemoteHost by remember { chatModel.currentRemoteHost } - // It's not a state, just a one-time value. Shouldn't be used in any state-related situations - val netCfg = remember { chatModel.controller.getNetCfg() } - val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.useSocksProxy) } - val developerTools = chatModel.controller.appPrefs.developerTools.get() - val onionHosts = remember { mutableStateOf(netCfg.onionHosts) } - val sessionMode = remember { mutableStateOf(netCfg.sessionMode) } - - LaunchedEffect(Unit) { - chatModel.userSMPServersUnsaved.value = null - } - - val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } } - NetworkAndServersLayout( - currentRemoteHost = currentRemoteHost, - developerTools = developerTools, - networkUseSocksProxy = networkUseSocksProxy, - onionHosts = onionHosts, - sessionMode = sessionMode, - proxyPort = proxyPort, - showModal = showModal, - showSettingsModal = showSettingsModal, - showCustomModal = showCustomModal, - toggleSocksProxy = { enable -> - if (enable) { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.network_enable_socks), - text = generalGetString(MR.strings.network_enable_socks_info).format(proxyPort.value), - confirmText = generalGetString(MR.strings.confirm_verb), - onConfirm = { - withApi { - val conf = NetCfg.proxyDefaults.withHostPort(chatModel.controller.appPrefs.networkProxyHostPort.get()) - chatModel.controller.apiSetNetworkConfig(conf) - chatModel.controller.setNetCfg(conf) - networkUseSocksProxy.value = true - onionHosts.value = conf.onionHosts - } - } - ) - } else { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.network_disable_socks), - text = generalGetString(MR.strings.network_disable_socks_info), - confirmText = generalGetString(MR.strings.confirm_verb), - onConfirm = { - withApi { - val conf = NetCfg.defaults - chatModel.controller.apiSetNetworkConfig(conf) - chatModel.controller.setNetCfg(conf) - networkUseSocksProxy.value = false - onionHosts.value = conf.onionHosts - } - } - ) - } - }, - useOnion = { - if (onionHosts.value == it) return@NetworkAndServersLayout - val prevValue = onionHosts.value - onionHosts.value = it - val startsWith = when (it) { - OnionHosts.NEVER -> generalGetString(MR.strings.network_use_onion_hosts_no_desc_in_alert) - OnionHosts.PREFER -> generalGetString(MR.strings.network_use_onion_hosts_prefer_desc_in_alert) - OnionHosts.REQUIRED -> generalGetString(MR.strings.network_use_onion_hosts_required_desc_in_alert) - } - showUpdateNetworkSettingsDialog( - title = generalGetString(MR.strings.update_onion_hosts_settings_question), - startsWith, - onDismiss = { - onionHosts.value = prevValue - } - ) { - withApi { - val newCfg = chatModel.controller.getNetCfg().withOnionHosts(it) - val res = chatModel.controller.apiSetNetworkConfig(newCfg) - if (res) { - chatModel.controller.setNetCfg(newCfg) - onionHosts.value = it - } else { - onionHosts.value = prevValue - } - } - } - }, - updateSessionMode = { - if (sessionMode.value == it) return@NetworkAndServersLayout - val prevValue = sessionMode.value - sessionMode.value = it - val startsWith = when (it) { - TransportSessionMode.User -> generalGetString(MR.strings.network_session_mode_user_description) - TransportSessionMode.Entity -> generalGetString(MR.strings.network_session_mode_entity_description) - } - showUpdateNetworkSettingsDialog( - title = generalGetString(MR.strings.update_network_session_mode_question), - startsWith, - onDismiss = { sessionMode.value = prevValue } - ) { - withApi { - val newCfg = chatModel.controller.getNetCfg().copy(sessionMode = it) - val res = chatModel.controller.apiSetNetworkConfig(newCfg) - if (res) { - chatModel.controller.setNetCfg(newCfg) - sessionMode.value = it - } else { - sessionMode.value = prevValue - } - } - } - } - ) -} - -@Composable fun NetworkAndServersLayout( - currentRemoteHost: RemoteHostInfo?, - developerTools: Boolean, - networkUseSocksProxy: MutableState<Boolean>, - onionHosts: MutableState<OnionHosts>, - sessionMode: MutableState<TransportSessionMode>, - proxyPort: State<Int>, - showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), - toggleSocksProxy: (Boolean) -> Unit, - useOnion: (OnionHosts) -> Unit, - updateSessionMode: (TransportSessionMode) -> Unit, -) { - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - AppBarTitle(stringResource(MR.strings.network_and_servers)) - if (!chatModel.desktopNoUserNoRemote) { - SectionView(generalGetString(MR.strings.settings_section_title_messages)) { - SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) }) - - SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) }) - - if (currentRemoteHost == null) { - UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showSettingsModal) - UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion) - if (developerTools) { - SessionModePicker(sessionMode, showSettingsModal, updateSessionMode) - } - SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) }) - } - } - } - if (currentRemoteHost == null && networkUseSocksProxy.value) { - SectionCustomFooter { - Column { - Text(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported)) - Spacer(Modifier.height(DEFAULT_PADDING_HALF)) - Text(annotatedStringResource(MR.strings.socks_proxy_setting_limitations)) - } - } - Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp)) - } else if (!chatModel.desktopNoUserNoRemote) { - Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp)) - } - - SectionView(generalGetString(MR.strings.settings_section_title_calls)) { - SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), showModal { RTCServersView(it) }) - } - SectionBottomSpacer() - } -} - -@Composable -fun UseSocksProxySwitch( - networkUseSocksProxy: MutableState<Boolean>, - proxyPort: State<Int>, - toggleSocksProxy: (Boolean) -> Unit, - showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit) -) { - Row( - Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Row( - Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painterResource(MR.images.ic_settings_ethernet), - stringResource(MR.strings.network_socks_toggle_use_socks_proxy), - tint = MaterialTheme.colors.secondary - ) - TextIconSpaced(false) - val text = buildAnnotatedString { - append(generalGetString(MR.strings.network_socks_toggle_use_socks_proxy) + " (") - val style = SpanStyle(color = MaterialTheme.colors.primary) - withAnnotation(tag = "PORT", annotation = generalGetString(MR.strings.network_proxy_port).format(proxyPort.value)) { - withStyle(style) { append(generalGetString(MR.strings.network_proxy_port).format(proxyPort.value)) } - } - append(")") - } - ClickableText( - text, - style = TextStyle(color = MaterialTheme.colors.onBackground, fontSize = 16.sp, fontFamily = Inter, fontWeight = FontWeight.Normal), - onClick = { offset -> - text.getStringAnnotations(tag = "PORT", start = offset, end = offset) - .firstOrNull()?.let { _ -> - showSettingsModal { SockProxySettings(it) }() - } - }, - shouldConsumeEvent = { offset -> - text.getStringAnnotations(tag = "PORT", start = offset, end = offset).any() - } - ) - } - DefaultSwitch( - checked = networkUseSocksProxy.value, - onCheckedChange = toggleSocksProxy, - ) - } -} - -@Composable -fun SockProxySettings(m: ChatModel) { - Column( - Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - ) { - val defaultHostPort = remember { "localhost:9050" } - AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings)) - val hostPort by remember { m.controller.appPrefs.networkProxyHostPort.state } - val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue(hostPort?.split(":")?.firstOrNull() ?: "localhost")) - } - val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue(hostPort?.split(":")?.lastOrNull() ?: "9050")) - } - val save = { - withBGApi { - m.controller.appPrefs.networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text) - if (m.controller.appPrefs.networkUseSocksProxy.get()) { - m.controller.apiSetNetworkConfig(m.controller.getNetCfg()) - } - } - } - SectionView { - SectionItemView { - ResetToDefaultsButton({ - val reset = { - m.controller.appPrefs.networkProxyHostPort.set(defaultHostPort) - val newHost = defaultHostPort.split(":").first() - val newPort = defaultHostPort.split(":").last() - hostUnsaved.value = hostUnsaved.value.copy(newHost, TextRange(newHost.length)) - portUnsaved.value = portUnsaved.value.copy(newPort, TextRange(newPort.length)) - save() - } - if (m.controller.appPrefs.networkUseSocksProxy.get()) { - showUpdateNetworkSettingsDialog { - reset() - } - } else { - reset() - } - }, disabled = hostPort == defaultHostPort) - } - SectionItemView { - DefaultConfigurableTextField( - hostUnsaved, - stringResource(MR.strings.host_verb), - modifier = Modifier.fillMaxWidth(), - isValid = ::validHost, - keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), - keyboardType = KeyboardType.Text, - ) - } - SectionItemView { - DefaultConfigurableTextField( - portUnsaved, - stringResource(MR.strings.port_verb), - modifier = Modifier.fillMaxWidth(), - isValid = ::validPort, - keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save() }), - keyboardType = KeyboardType.Number, - ) - } - } - SectionCustomFooter { - NetworkSectionFooter( - revert = { - val prevHost = m.controller.appPrefs.networkProxyHostPort.get()?.split(":")?.firstOrNull() ?: "localhost" - val prevPort = m.controller.appPrefs.networkProxyHostPort.get()?.split(":")?.lastOrNull() ?: "9050" - hostUnsaved.value = hostUnsaved.value.copy(prevHost, TextRange(prevHost.length)) - portUnsaved.value = portUnsaved.value.copy(prevPort, TextRange(prevPort.length)) - }, - save = { if (m.controller.appPrefs.networkUseSocksProxy.get()) showUpdateNetworkSettingsDialog { save() } else save() }, - revertDisabled = hostPort == (hostUnsaved.value.text + ":" + portUnsaved.value.text), - saveDisabled = hostPort == (hostUnsaved.value.text + ":" + portUnsaved.value.text) || - remember { derivedStateOf { !validHost(hostUnsaved.value.text) } }.value || - remember { derivedStateOf { !validPort(portUnsaved.value.text) } }.value - ) - } - SectionBottomSpacer() - } -} - -@Composable -private fun UseOnionHosts( - onionHosts: MutableState<OnionHosts>, - enabled: State<Boolean>, - showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - useOnion: (OnionHosts) -> Unit, -) { - val values = remember { - OnionHosts.values().map { - when (it) { - OnionHosts.NEVER -> ValueTitleDesc(OnionHosts.NEVER, generalGetString(MR.strings.network_use_onion_hosts_no), AnnotatedString(generalGetString(MR.strings.network_use_onion_hosts_no_desc))) - OnionHosts.PREFER -> ValueTitleDesc(OnionHosts.PREFER, generalGetString(MR.strings.network_use_onion_hosts_prefer), AnnotatedString(generalGetString(MR.strings.network_use_onion_hosts_prefer_desc))) - OnionHosts.REQUIRED -> ValueTitleDesc(OnionHosts.REQUIRED, generalGetString(MR.strings.network_use_onion_hosts_required), AnnotatedString(generalGetString(MR.strings.network_use_onion_hosts_required_desc))) - } - } - } - val onSelected = showModal { - Column( - Modifier.fillMaxWidth(), - ) { - AppBarTitle(stringResource(MR.strings.network_use_onion_hosts)) - SectionViewSelectable(null, onionHosts, values, useOnion) - } - } - - SectionItemWithValue( - generalGetString(MR.strings.network_use_onion_hosts), - onionHosts, - values, - icon = painterResource(MR.images.ic_security), - enabled = enabled, - onSelected = onSelected - ) -} - -@Composable -private fun SessionModePicker( - sessionMode: MutableState<TransportSessionMode>, - showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - updateSessionMode: (TransportSessionMode) -> Unit, -) { - val density = LocalDensity.current - val values = remember { - TransportSessionMode.values().map { - when (it) { - TransportSessionMode.User -> ValueTitleDesc(TransportSessionMode.User, generalGetString(MR.strings.network_session_mode_user), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_session_mode_user_description), density)) - TransportSessionMode.Entity -> ValueTitleDesc(TransportSessionMode.Entity, generalGetString(MR.strings.network_session_mode_entity), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_session_mode_entity_description), density)) - } - } - } - - SectionItemWithValue( - generalGetString(MR.strings.network_session_mode_transport_isolation), - sessionMode, - values, - icon = painterResource(MR.images.ic_safety_divider), - onSelected = showModal { - Column( - Modifier.fillMaxWidth(), - ) { - AppBarTitle(stringResource(MR.strings.network_session_mode_transport_isolation)) - SectionViewSelectable(null, sessionMode, values, updateSessionMode) - } - } - ) -} - -@Composable -private fun NetworkSectionFooter(revert: () -> Unit, save: () -> Unit, revertDisabled: Boolean, saveDisabled: Boolean) { - Row( - Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - FooterButton(painterResource(MR.images.ic_replay), stringResource(MR.strings.network_options_revert), revert, revertDisabled) - FooterButton(painterResource(MR.images.ic_check), stringResource(MR.strings.network_options_save), save, saveDisabled) - } -} - -// https://stackoverflow.com/a/106223 -private fun validHost(s: String): Boolean { - val validIp = Regex("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$") - val validHostname = Regex("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])[.])*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$"); - return s.matches(validIp) || s.matches(validHostname) -} - -// https://ihateregex.io/expr/port/ -fun validPort(s: String): Boolean { - val validPort = Regex("^(6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4})$") - return s.isNotBlank() && s.matches(validPort) -} - -private fun showUpdateNetworkSettingsDialog( - title: String, - startsWith: String = "", - message: String = generalGetString(MR.strings.updating_settings_will_reconnect_client_to_all_servers), - onDismiss: () -> Unit, - onConfirm: () -> Unit -) { - AlertManager.shared.showAlertDialog( - title = title, - text = startsWith + "\n\n" + message, - confirmText = generalGetString(MR.strings.update_network_settings_confirmation), - onDismiss = onDismiss, - onConfirm = onConfirm, - onDismissRequest = onDismiss - ) -} - -@Preview -@Composable -fun PreviewNetworkAndServersLayout() { - SimpleXTheme { - NetworkAndServersLayout( - currentRemoteHost = null, - developerTools = true, - networkUseSocksProxy = remember { mutableStateOf(true) }, - proxyPort = remember { mutableStateOf(9050) }, - showModal = { {} }, - showSettingsModal = { {} }, - showCustomModal = { {} }, - toggleSocksProxy = {}, - onionHosts = remember { mutableStateOf(OnionHosts.PREFER) }, - sessionMode = remember { mutableStateOf(TransportSessionMode.User) }, - useOnion = {}, - updateSessionMode = {}, - ) - } -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt index ddd1b4068e..5af5d5fb90 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt @@ -1,11 +1,10 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer +import SectionTextFooter import SectionView import SectionViewSelectable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier @@ -16,6 +15,7 @@ import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow import chat.simplex.common.model.* import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import kotlin.collections.ArrayList @@ -56,9 +56,7 @@ fun NotificationsSettingsLayout( val modes = remember { notificationModes() } val previewModes = remember { notificationPreviewModes() } - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.notifications)) SectionView(null) { if (appPlatform == AppPlatform.ANDROID) { @@ -79,6 +77,9 @@ fun NotificationsSettingsLayout( color = MaterialTheme.colors.secondary ) } + if (platform.androidIsXiaomiDevice() && (notificationsMode.value == NotificationsMode.PERIODIC || notificationsMode.value == NotificationsMode.SERVICE)) { + SectionTextFooter(annotatedStringResource(MR.strings.xiaomi_ignore_battery_optimization)) + } } SectionBottomSpacer() } @@ -90,11 +91,12 @@ fun NotificationsModeView( onNotificationsModeSelected: (NotificationsMode) -> Unit, ) { val modes = remember { notificationModes() } - Column( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.settings_notifications_mode_title).lowercase().capitalize(Locale.current)) SectionViewSelectable(null, notificationsMode, modes, onNotificationsModeSelected) + if (platform.androidIsXiaomiDevice() && (notificationsMode.value == NotificationsMode.PERIODIC || notificationsMode.value == NotificationsMode.SERVICE)) { + SectionTextFooter(annotatedStringResource(MR.strings.xiaomi_ignore_battery_optimization)) + } } } @@ -104,9 +106,7 @@ fun NotificationPreviewView( onNotificationPreviewModeSelected: (NotificationPreviewMode) -> Unit, ) { val previewModes = remember { notificationPreviewModes() } - Column( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.settings_notification_preview_title)) SectionViewSelectable(null, notificationPreviewMode, previewModes, onNotificationPreviewModeSelected) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt index c375fc5459..72fa45b936 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt @@ -11,11 +11,13 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* +import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.chatModel import chat.simplex.res.MR +import kotlinx.coroutines.* @Composable fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) { @@ -26,13 +28,15 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) { close() } fun savePrefs(afterSave: () -> Unit = {}) { - withApi { + withBGApi { val newProfile = user.profile.toProfile().copy(preferences = preferences.toPreferences()) val updated = m.controller.apiUpdateProfile(user.remoteHostId, newProfile) if (updated != null) { val (updatedProfile, updatedContacts) = updated m.updateCurrentUser(user.remoteHostId, updatedProfile, preferences) - updatedContacts.forEach { m.updateContact(user.remoteHostId, it) } + withContext(Dispatchers.Main) { + updatedContacts.forEach { chatModel.chatsContext.updateContact(user.remoteHostId, it) } + } currentPreferences = preferences } afterSave() @@ -62,9 +66,7 @@ private fun PreferencesLayout( reset: () -> Unit, savePrefs: () -> Unit, ) { - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_preferences)) val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.allow) } TimedMessagesFeatureSection(timedMessages) { @@ -121,9 +123,9 @@ private fun TimedMessagesFeatureSection(allowFeature: State<FeatureAllowed>, onS ChatFeature.TimedMessages.text, ChatFeature.TimedMessages.icon, MaterialTheme.colors.secondary, - allowFeature.value == FeatureAllowed.ALWAYS || allowFeature.value == FeatureAllowed.YES, + checked = allowFeature.value == FeatureAllowed.ALWAYS || allowFeature.value == FeatureAllowed.YES, extraPadding = false, - onSelected + onChange = onSelected ) } SectionTextFooter(ChatFeature.TimedMessages.allowDescription(allowFeature.value)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index 3d2b7b7fa5..569f4ff5f8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -5,7 +5,6 @@ import SectionDividerSpaced import SectionItemView import SectionTextFooter import SectionView -import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -21,6 +20,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.res.MR import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* import chat.simplex.common.views.ProfileNameField import chat.simplex.common.views.helpers.* @@ -30,8 +30,8 @@ import chat.simplex.common.views.isValidDisplayName import chat.simplex.common.views.localauth.SetAppPasscodeView import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.model.ChatModel -import chat.simplex.common.platform.AppPlatform -import chat.simplex.common.platform.appPlatform +import chat.simplex.common.platform.* +import kotlinx.coroutines.* enum class LAMode { SYSTEM, @@ -55,20 +55,17 @@ fun PrivacySettingsView( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), setPerformLA: (Boolean) -> Unit ) { - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - ) { + ColumnWithScrollBar { val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode AppBarTitle(stringResource(MR.strings.your_privacy)) PrivacyDeviceSection(showSettingsModal, setPerformLA) SectionDividerSpaced() SectionView(stringResource(MR.strings.settings_section_title_chats)) { - SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable -> - withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) } - }) - SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages) SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews) + ChatListLinksOptions(appPrefs.privacyChatListOpenLinks.state, onSelected = { + appPrefs.privacyChatListOpenLinks.set(it) + }) SettingsPreferenceItem( painterResource(MR.images.ic_chat_bubble), stringResource(MR.strings.privacy_show_last_messages), @@ -91,26 +88,54 @@ fun PrivacySettingsView( simplexLinkMode.set(it) chatModel.simplexLinkMode.value = it }) + if (appPrefs.developerTools.get()) { + SettingsPreferenceItem( + null, + stringResource(MR.strings.privacy_short_links), + chatModel.controller.appPrefs.privacyShortLinks + ) + } } + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.settings_section_title_files)) { + SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable -> + withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) } + }) + SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages) + BlurRadiusOptions(remember { appPrefs.privacyMediaBlurRadius.state }) { + appPrefs.privacyMediaBlurRadius.set(it) + } + SettingsPreferenceItem(painterResource(MR.images.ic_security), stringResource(MR.strings.protect_ip_address), chatModel.controller.appPrefs.privacyAskToApproveRelays) + } + SectionTextFooter( + if (chatModel.controller.appPrefs.privacyAskToApproveRelays.state.value) { + stringResource(MR.strings.app_will_ask_to_confirm_unknown_file_servers) + } else { + stringResource(MR.strings.without_tor_or_vpn_ip_address_will_be_visible_to_file_servers) + } + ) val currentUser = chatModel.currentUser.value if (currentUser != null) { fun setSendReceiptsContacts(enable: Boolean, clearOverrides: Boolean) { - withApi { + withLongRunningApi(slow = 60_000) { val mrs = UserMsgReceiptSettings(enable, clearOverrides) chatModel.controller.apiSetUserContactReceipts(currentUser, mrs) chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.currentUser.value = currentUser.copy(sendRcptsContacts = enable) if (clearOverrides) { // For loop here is to prevent ConcurrentModificationException that happens with forEach - for (i in 0 until chatModel.chats.size) { - val chat = chatModel.chats[i] - if (chat.chatInfo is ChatInfo.Direct) { - var contact = chat.chatInfo.contact - val sendRcpts = contact.chatSettings.sendRcpts - if (sendRcpts != null && sendRcpts != enable) { - contact = contact.copy(chatSettings = contact.chatSettings.copy(sendRcpts = null)) - chatModel.updateContact(currentUser.remoteHostId, contact) + withContext(Dispatchers.Main) { + for (i in 0 until chatModel.chatsContext.chats.size) { + val chat = chatModel.chatsContext.chats[i] + if (chat.chatInfo is ChatInfo.Direct) { + var contact = chat.chatInfo.contact + val sendRcpts = contact.chatSettings.sendRcpts + if (sendRcpts != null && sendRcpts != enable) { + contact = contact.copy(chatSettings = contact.chatSettings.copy(sendRcpts = null)) + chatModel.chatsContext.updateContact(currentUser.remoteHostId, contact) + } } } } @@ -119,21 +144,23 @@ fun PrivacySettingsView( } fun setSendReceiptsGroups(enable: Boolean, clearOverrides: Boolean) { - withApi { + withLongRunningApi(slow = 60_000) { val mrs = UserMsgReceiptSettings(enable, clearOverrides) chatModel.controller.apiSetUserGroupReceipts(currentUser, mrs) chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.currentUser.value = currentUser.copy(sendRcptsSmallGroups = enable) if (clearOverrides) { - // For loop here is to prevent ConcurrentModificationException that happens with forEach - for (i in 0 until chatModel.chats.size) { - val chat = chatModel.chats[i] - if (chat.chatInfo is ChatInfo.Group) { - var groupInfo = chat.chatInfo.groupInfo - val sendRcpts = groupInfo.chatSettings.sendRcpts - if (sendRcpts != null && sendRcpts != enable) { - groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null)) - chatModel.updateGroup(currentUser.remoteHostId, groupInfo) + withContext(Dispatchers.Main) { + // For loop here is to prevent ConcurrentModificationException that happens with forEach + for (i in 0 until chatModel.chatsContext.chats.size) { + val chat = chatModel.chatsContext.chats[i] + if (chat.chatInfo is ChatInfo.Group) { + var groupInfo = chat.chatInfo.groupInfo + val sendRcpts = groupInfo.chatSettings.sendRcpts + if (sendRcpts != null && sendRcpts != enable) { + groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null)) + chatModel.chatsContext.updateGroup(currentUser.remoteHostId, groupInfo) + } } } } @@ -142,11 +169,11 @@ fun PrivacySettingsView( } if (!chatModel.desktopNoUserNoRemote) { - SectionDividerSpaced() + SectionDividerSpaced(maxTopPadding = true) DeliveryReceiptsSection( currentUser = currentUser, setOrAskSendReceiptsContacts = { enable -> - val contactReceiptsOverrides = chatModel.chats.fold(0) { count, chat -> + val contactReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat -> if (chat.chatInfo is ChatInfo.Direct) { val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) @@ -161,7 +188,7 @@ fun PrivacySettingsView( } }, setOrAskSendReceiptsGroups = { enable -> - val groupReceiptsOverrides = chatModel.chats.fold(0) { count, chat -> + val groupReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat -> if (chat.chatInfo is ChatInfo.Group) { val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) @@ -182,6 +209,26 @@ fun PrivacySettingsView( } } +@Composable +private fun ChatListLinksOptions(state: State<PrivacyChatListOpenLinksMode>, onSelected: (PrivacyChatListOpenLinksMode) -> Unit) { + val values = remember { + PrivacyChatListOpenLinksMode.entries.map { + when (it) { + PrivacyChatListOpenLinksMode.YES -> it to generalGetString(MR.strings.privacy_chat_list_open_links_yes) + PrivacyChatListOpenLinksMode.NO -> it to generalGetString(MR.strings.privacy_chat_list_open_links_no) + PrivacyChatListOpenLinksMode.ASK -> it to generalGetString(MR.strings.privacy_chat_list_open_links_ask) + } + } + } + ExposedDropDownSettingRow( + generalGetString(MR.strings.privacy_chat_list_open_links), + values, + state, + icon = painterResource(MR.images.ic_open_in_new), + onSelected = onSelected + ) +} + @Composable private fun SimpleXLinkOptions(simplexLinkModeState: State<SimplexLinkMode>, onSelected: (SimplexLinkMode) -> Unit) { val modeValues = listOf(SimplexLinkMode.DESCRIPTION, SimplexLinkMode.FULL) @@ -205,6 +252,30 @@ private fun SimpleXLinkOptions(simplexLinkModeState: State<SimplexLinkMode>, onS ) } +@Composable +private fun BlurRadiusOptions(state: State<Int>, onSelected: (Int) -> Unit) { + val choices = listOf(0, 12, 24, 48) + val pickerValues = choices + if (choices.contains(state.value)) emptyList() else listOf(state.value) + val values = remember { + pickerValues.map { + when (it) { + 0 -> it to generalGetString(MR.strings.privacy_media_blur_radius_off) + 12 -> it to generalGetString(MR.strings.privacy_media_blur_radius_soft) + 24 -> it to generalGetString(MR.strings.privacy_media_blur_radius_medium) + 48 -> it to generalGetString(MR.strings.privacy_media_blur_radius_strong) + else -> it to "$it" + } + } + } + ExposedDropDownSettingRow( + generalGetString(MR.strings.privacy_media_blur_radius), + values, + state, + icon = painterResource(MR.images.ic_blur_on), + onSelected = onSelected + ) +} + @Composable expect fun PrivacyDeviceSection( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), @@ -220,7 +291,7 @@ private fun DeliveryReceiptsSection( SectionView(stringResource(MR.strings.settings_section_title_delivery_receipts)) { SettingsActionItemWithContent(painterResource(MR.images.ic_person), stringResource(MR.strings.receipts_section_contacts)) { DefaultSwitch( - checked = currentUser.sendRcptsContacts ?: false, + checked = currentUser.sendRcptsContacts, onCheckedChange = { enable -> setOrAskSendReceiptsContacts(enable) } @@ -228,7 +299,7 @@ private fun DeliveryReceiptsSection( } SettingsActionItemWithContent(painterResource(MR.images.ic_group), stringResource(MR.strings.receipts_section_groups)) { DefaultSwitch( - checked = currentUser.sendRcptsSmallGroups ?: false, + checked = currentUser.sendRcptsSmallGroups, onCheckedChange = { enable -> setOrAskSendReceiptsGroups(enable) } @@ -327,7 +398,8 @@ fun SimplexLockView( currentLAMode: SharedPreference<LAMode>, setPerformLA: (Boolean) -> Unit ) { - val performLA = remember { chatModel.performLA } + val showAuthScreen = remember { chatModel.showAuthScreen } + val performLA = remember { appPrefs.performLA.state } val laMode = remember { chatModel.controller.appPrefs.laMode.state } val laLockDelay = remember { chatModel.controller.appPrefs.laLockDelay } val showChangePasscode = remember { derivedStateOf { performLA.value && currentLAMode.state.value == LAMode.PASSCODE } } @@ -335,13 +407,9 @@ fun SimplexLockView( val selfDestructDisplayName = remember { mutableStateOf(chatModel.controller.appPrefs.selfDestructDisplayName.get() ?: "") } val selfDestructDisplayNamePref = remember { chatModel.controller.appPrefs.selfDestructDisplayName } - fun resetLAEnabled(onOff: Boolean) { - chatModel.controller.appPrefs.performLA.set(onOff) - chatModel.performLA.value = onOff - } - fun disableUnavailableLA() { - resetLAEnabled(false) + chatModel.controller.appPrefs.performLA.set(false) + chatModel.showAuthScreen.value = false currentLAMode.set(LAMode.default) laUnavailableInstructionAlert() } @@ -358,7 +426,8 @@ fun SimplexLockView( } else { generalGetString(MR.strings.chat_lock) }, - generalGetString(MR.strings.change_lock_mode) + generalGetString(MR.strings.change_lock_mode), + oneTime = true, ) { laResult -> when (laResult) { is LAResult.Error -> { @@ -368,7 +437,7 @@ fun SimplexLockView( LAResult.Success -> { when (toLAMode) { LAMode.SYSTEM -> { - authenticate(generalGetString(MR.strings.auth_enable_simplex_lock), promptSubtitle = "", usingLAMode = toLAMode) { laResult -> + authenticate(generalGetString(MR.strings.auth_enable_simplex_lock), promptSubtitle = "", usingLAMode = toLAMode, oneTime = true) { laResult -> when (laResult) { LAResult.Success -> { currentLAMode.set(toLAMode) @@ -383,7 +452,7 @@ fun SimplexLockView( } LAMode.PASSCODE -> { ModalManager.fullscreen.showCustomModal { close -> - Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background.copy(1f), contentColor = LocalContentColor.current) { SetAppPasscodeView( submit = { laLockDelay.set(30) @@ -404,7 +473,7 @@ fun SimplexLockView( } fun toggleSelfDestruct(selfDestruct: SharedPreference<Boolean>) { - authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_mode)) { laResult -> + authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_mode), oneTime = true) { laResult -> when (laResult) { is LAResult.Error -> laFailedAlert() is LAResult.Failed -> { /* Can be called multiple times on every failure */ } @@ -423,12 +492,13 @@ fun SimplexLockView( } fun changeLAPassword() { - authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.la_change_app_passcode)) { laResult -> + authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.la_change_app_passcode), oneTime = true) { laResult -> when (laResult) { LAResult.Success -> { ModalManager.fullscreen.showCustomModal { close -> - Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background.copy(1f), contentColor = LocalContentColor.current) { SetAppPasscodeView( + reason = generalGetString(MR.strings.la_app_passcode), submit = { passcodeAlert(generalGetString(MR.strings.passcode_changed)) }, cancel = { @@ -446,13 +516,15 @@ fun SimplexLockView( } fun changeSelfDestructPassword() { - authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_passcode)) { laResult -> + authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_passcode), oneTime = true) { laResult -> when (laResult) { LAResult.Success -> { ModalManager.fullscreen.showCustomModal { close -> - Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background.copy(1f), contentColor = LocalContentColor.current) { SetAppPasscodeView( passcodeKeychain = ksSelfDestructPassword, + prohibitedPasscodeKeychain = ksAppPassword, + reason = generalGetString(MR.strings.self_destruct), submit = { selfDestructPasscodeAlert(generalGetString(MR.strings.self_destruct_passcode_changed)) }, cancel = { @@ -470,13 +542,11 @@ fun SimplexLockView( } } - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.chat_lock)) SectionView { - EnableLock(performLA) { performLAToggle -> - performLA.value = performLAToggle + EnableLock(remember { appPrefs.performLA.state }) { performLAToggle -> + showAuthScreen.value = performLAToggle chatModel.controller.appPrefs.laNoticeShown.set(true) if (performLAToggle) { when (currentLAMode.state.value) { @@ -485,7 +555,7 @@ fun SimplexLockView( } LAMode.PASSCODE -> { ModalManager.fullscreen.showCustomModal { close -> - Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background.copy(1f), contentColor = LocalContentColor.current) { SetAppPasscodeView( submit = { laLockDelay.set(30) @@ -493,7 +563,9 @@ fun SimplexLockView( passcodeAlert(generalGetString(MR.strings.passcode_set)) }, cancel = { - resetLAEnabled(false) + chatModel.showAuthScreen.value = false + // Don't drop auth pref in case of state inconsistency (eg, you have set passcode but somehow bypassed toggle and turned it off and then on) + // chatModel.controller.appPrefs.performLA.set(false) }, close = close ) @@ -553,7 +625,7 @@ fun SimplexLockView( fontSize = 16.sp, modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) ) - ProfileNameField(selfDestructDisplayName, "", ::isValidDisplayName) + ProfileNameField(selfDestructDisplayName, "", { isValidDisplayName(it.trim()) }) LaunchedEffect(selfDestructDisplayName.value) { val new = selfDestructDisplayName.value if (isValidDisplayName(new) && selfDestructDisplayNamePref.get() != new) { @@ -577,8 +649,8 @@ fun SimplexLockView( @Composable private fun SelfDestructInfoView() { - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING), + ColumnWithScrollBar( + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), ) { AppBarTitle(stringResource(MR.strings.self_destruct), withPadding = false) ReadableText(stringResource(MR.strings.if_you_enter_self_destruct_code)) @@ -596,9 +668,9 @@ private fun EnableSelfDestruct( selfDestruct: SharedPreference<Boolean>, close: () -> Unit ) { - Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background.copy(1f), contentColor = LocalContentColor.current) { SetAppPasscodeView( - passcodeKeychain = ksSelfDestructPassword, title = generalGetString(MR.strings.set_passcode), reason = generalGetString(MR.strings.enabled_self_destruct_passcode), + passcodeKeychain = ksSelfDestructPassword, prohibitedPasscodeKeychain = ksAppPassword, title = generalGetString(MR.strings.set_passcode), reason = generalGetString(MR.strings.enabled_self_destruct_passcode), submit = { selfDestruct.set(true) selfDestructPasscodeAlert(generalGetString(MR.strings.self_destruct_passcode_enabled)) @@ -610,7 +682,7 @@ private fun EnableSelfDestruct( } @Composable -private fun EnableLock(performLA: MutableState<Boolean>, onCheckedChange: (Boolean) -> Unit) { +private fun EnableLock(performLA: State<Boolean>, onCheckedChange: (Boolean) -> Unit) { SectionItemView { Row(verticalAlignment = Alignment.CenterVertically) { Text( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt deleted file mode 100644 index 4e8da36a7e..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt +++ /dev/null @@ -1,208 +0,0 @@ -package chat.simplex.common.views.usersettings - -import SectionBottomSpacer -import SectionDividerSpaced -import SectionItemView -import SectionItemViewSpaceBetween -import SectionView -import chat.simplex.common.platform.Log -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import 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.FontFamily -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import chat.simplex.common.platform.TAG -import chat.simplex.common.model.* -import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress -import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.newchat.QRCode -import chat.simplex.common.model.ChatModel -import chat.simplex.res.MR -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch - -@Composable -fun ProtocolServerView(m: ChatModel, server: ServerCfg, serverProtocol: ServerProtocol, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) { - var testing by remember { mutableStateOf(false) } - val scope = rememberCoroutineScope() - ProtocolServerLayout( - testing, - server, - serverProtocol, - testServer = { - testing = true - scope.launch { - val res = testServerConnection(server, m) - if (isActive) { - onUpdate(res.first) - testing = false - } - } - }, - onUpdate, - onDelete - ) - if (testing) { - Box( - Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - Modifier - .padding(horizontal = 2.dp) - .size(30.dp), - color = MaterialTheme.colors.secondary, - strokeWidth = 2.5.dp - ) - } - } -} - -@Composable -private fun ProtocolServerLayout( - testing: Boolean, - server: ServerCfg, - serverProtocol: ServerProtocol, - testServer: () -> Unit, - onUpdate: (ServerCfg) -> Unit, - onDelete: () -> Unit, -) { - Column( - Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - ) { - AppBarTitle(stringResource(if (server.preset) MR.strings.smp_servers_preset_server else MR.strings.smp_servers_your_server)) - - if (server.preset) { - PresetServer(testing, server, testServer, onUpdate, onDelete) - } else { - CustomServer(testing, server, serverProtocol, testServer, onUpdate, onDelete) - } - SectionBottomSpacer() - } -} - -@Composable -private fun PresetServer( - testing: Boolean, - server: ServerCfg, - testServer: () -> Unit, - onUpdate: (ServerCfg) -> Unit, - onDelete: () -> Unit, -) { - SectionView(stringResource(MR.strings.smp_servers_preset_address).uppercase()) { - SelectionContainer { - Text( - server.server, - Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp), - style = TextStyle( - fontFamily = FontFamily.Monospace, fontSize = 16.sp, - color = MaterialTheme.colors.secondary - ) - ) - } - } - SectionDividerSpaced(maxTopPadding = true) - UseServerSection(true, testing, server, testServer, onUpdate, onDelete) -} - -@Composable -private fun CustomServer( - testing: Boolean, - server: ServerCfg, - serverProtocol: ServerProtocol, - testServer: () -> Unit, - onUpdate: (ServerCfg) -> Unit, - onDelete: () -> Unit, -) { - val serverAddress = remember { mutableStateOf(server.server) } - val valid = remember { - derivedStateOf { - with(parseServerAddress(serverAddress.value)) { - this?.valid == true && this.serverProtocol == serverProtocol - } - } - } - SectionView( - stringResource(MR.strings.smp_servers_your_server_address).uppercase(), - icon = painterResource(MR.images.ic_error), - iconTint = if (!valid.value) MaterialTheme.colors.error else Color.Transparent, - ) { - val testedPreviously = remember { mutableMapOf<String, Boolean?>() } - TextEditor( - serverAddress, - Modifier.height(144.dp) - ) - LaunchedEffect(Unit) { - snapshotFlow { serverAddress.value } - .distinctUntilChanged() - .collect { - testedPreviously[server.server] = server.tested - onUpdate(server.copy(server = it, tested = testedPreviously[serverAddress.value])) - } - } - } - SectionDividerSpaced() - UseServerSection(valid.value, testing, server, testServer, onUpdate, onDelete) - - if (valid.value) { - SectionDividerSpaced() - SectionView(stringResource(MR.strings.smp_servers_add_to_another_device).uppercase()) { - QRCode(serverAddress.value, Modifier.aspectRatio(1f).padding(horizontal = DEFAULT_PADDING)) - } - } -} - -@Composable -private fun UseServerSection( - valid: Boolean, - testing: Boolean, - server: ServerCfg, - testServer: () -> Unit, - onUpdate: (ServerCfg) -> Unit, - onDelete: () -> Unit, -) { - SectionView(stringResource(MR.strings.smp_servers_use_server).uppercase()) { - SectionItemViewSpaceBetween(testServer, disabled = !valid || testing) { - Text(stringResource(MR.strings.smp_servers_test_server), color = if (valid && !testing) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) - ShowTestStatus(server) - } - val enabled = rememberUpdatedState(server.enabled) - PreferenceToggle(stringResource(MR.strings.smp_servers_use_server_for_new_conn), enabled.value) { onUpdate(server.copy(enabled = it)) } - SectionItemView(onDelete, disabled = testing) { - Text(stringResource(MR.strings.smp_servers_delete_server), color = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.error) - } - } -} - -@Composable -fun ShowTestStatus(server: ServerCfg, modifier: Modifier = Modifier) = - when (server.tested) { - true -> Icon(painterResource(MR.images.ic_check), null, modifier, tint = SimplexGreen) - false -> Icon(painterResource(MR.images.ic_close), null, modifier, tint = MaterialTheme.colors.error) - else -> Icon(painterResource(MR.images.ic_check), null, modifier, tint = Color.Transparent) - } - -suspend fun testServerConnection(server: ServerCfg, m: ChatModel): Pair<ServerCfg, ProtocolTestFailure?> = - try { - val r = m.controller.testProtoServer(server.remoteHostId, server.server) - server.copy(tested = r == null) to r - } catch (e: Exception) { - Log.e(TAG, "testServerConnection ${e.stackTraceToString()}") - server.copy(tested = false) to null - } - -fun serverHostname(srv: String): String = - parseServerAddress(srv)?.hostnames?.firstOrNull() ?: srv diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt deleted file mode 100644 index 66dde9f96c..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt +++ /dev/null @@ -1,372 +0,0 @@ -package chat.simplex.common.views.usersettings - -import SectionBottomSpacer -import SectionDividerSpaced -import SectionItemView -import SectionTextFooter -import SectionView -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalUriHandler -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.text.* -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress -import chat.simplex.common.views.helpers.* -import chat.simplex.common.model.* -import chat.simplex.common.platform.appPlatform -import chat.simplex.common.views.usersettings.ScanProtocolServer -import chat.simplex.res.MR -import kotlinx.coroutines.launch - -@Composable -fun ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) { - var presetServers by remember(rhId) { mutableStateOf(emptyList<String>()) } - var servers by remember(rhId) { - mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList()) - } - val currServers = remember(rhId) { mutableStateOf(servers) } - val testing = rememberSaveable(rhId) { mutableStateOf(false) } - val serversUnchanged = remember { derivedStateOf { servers == currServers.value || testing.value } } - val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } } - val saveDisabled = remember { - derivedStateOf { - servers.isEmpty() || - servers == currServers.value || - testing.value || - !servers.all { srv -> - val address = parseServerAddress(srv.server) - address != null && uniqueAddress(srv, address, servers) - } || - allServersDisabled.value - } - } - - KeyChangeEffect(rhId) { - m.userSMPServersUnsaved.value = null - servers = emptyList() - } - - LaunchedEffect(rhId) { - val res = m.controller.getUserProtoServers(rhId, serverProtocol) - if (res != null) { - currServers.value = res.protoServers - presetServers = res.presetServers - if (servers.isEmpty()) { - servers = currServers.value - } - } - } - - fun showServer(server: ServerCfg) { - ModalManager.start.showModalCloseable(true) { close -> - var old by remember { mutableStateOf(server) } - val index = servers.indexOf(old) - ProtocolServerView( - m, - old, - serverProtocol, - onUpdate = { updated -> - val newServers = ArrayList(servers) - newServers.removeAt(index) - newServers.add(index, updated) - old = updated - servers = newServers - m.userSMPServersUnsaved.value = servers - }, - onDelete = { - val newServers = ArrayList(servers) - newServers.removeAt(index) - servers = newServers - m.userSMPServersUnsaved.value = servers - close() - }) - } - } - val scope = rememberCoroutineScope() - ModalView( - close = { - if (saveDisabled.value) close() - else showUnsavedChangesAlert({ saveServers(rhId, serverProtocol, currServers, servers, m, close) }, close) - }, - ) { - ProtocolServersLayout( - serverProtocol, - testing = testing.value, - servers = servers, - serversUnchanged = serversUnchanged.value, - saveDisabled = saveDisabled.value, - allServersDisabled = allServersDisabled.value, - m.currentUser.value, - addServer = { - AlertManager.shared.showAlertDialogButtonsColumn( - title = generalGetString(MR.strings.smp_servers_add), - buttons = { - Column { - SectionItemView({ - AlertManager.shared.hideAlert() - servers = servers + ServerCfg.empty - // No saving until something will be changed on the next screen to prevent blank servers on the list - showServer(servers.last()) - }) { - Text(stringResource(MR.strings.smp_servers_enter_manually), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - if (appPlatform.isAndroid) { - SectionItemView({ - AlertManager.shared.hideAlert() - ModalManager.start.showModalCloseable { close -> - ScanProtocolServer(rhId) { - close() - servers = servers + it - m.userSMPServersUnsaved.value = servers - } - } - } - ) { - Text(stringResource(MR.strings.smp_servers_scan_qr), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - } - val hasAllPresets = hasAllPresets(presetServers, servers, m) - if (!hasAllPresets) { - SectionItemView({ - AlertManager.shared.hideAlert() - servers = (servers + addAllPresets(rhId, presetServers, servers, m)).sortedByDescending { it.preset } - }) { - Text(stringResource(MR.strings.smp_servers_preset_add), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - } - } - } - ) - }, - testServers = { - scope.launch { - testServers(testing, servers, m) { - servers = it - m.userSMPServersUnsaved.value = servers - } - } - }, - resetServers = { - servers = currServers.value ?: emptyList() - m.userSMPServersUnsaved.value = null - }, - saveSMPServers = { - saveServers(rhId, serverProtocol, currServers, servers, m) - }, - showServer = ::showServer, - ) - - if (testing.value) { - Box( - Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - Modifier - .padding(horizontal = 2.dp) - .size(30.dp), - color = MaterialTheme.colors.secondary, - strokeWidth = 2.5.dp - ) - } - } - } -} - -@Composable -private fun ProtocolServersLayout( - serverProtocol: ServerProtocol, - testing: Boolean, - servers: List<ServerCfg>, - serversUnchanged: Boolean, - saveDisabled: Boolean, - allServersDisabled: Boolean, - currentUser: User?, - addServer: () -> Unit, - testServers: () -> Unit, - resetServers: () -> Unit, - saveSMPServers: () -> Unit, - showServer: (ServerCfg) -> Unit, -) { - Column( - Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - ) { - AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.your_SMP_servers else MR.strings.your_XFTP_servers)) - - SectionView(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.smp_servers else MR.strings.xftp_servers).uppercase()) { - for (srv in servers) { - SectionItemView({ showServer(srv) }, disabled = testing) { - ProtocolServerView(serverProtocol, srv, servers, testing) - } - } - SettingsActionItem( - painterResource(MR.images.ic_add), - stringResource(MR.strings.smp_servers_add), - addServer, - disabled = testing, - textColor = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary, - iconColor = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary - ) - } - SectionTextFooter( - remember(currentUser?.displayName) { - buildAnnotatedString { - append(generalGetString(MR.strings.smp_servers_per_user) + " ") - withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - append(currentUser?.displayName ?: "") - } - append(".") - } - } - ) - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) - SectionView { - SectionItemView(resetServers, disabled = serversUnchanged) { - Text(stringResource(MR.strings.reset_verb), color = if (!serversUnchanged) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) - } - val testServersDisabled = testing || allServersDisabled - SectionItemView(testServers, disabled = testServersDisabled) { - Text(stringResource(MR.strings.smp_servers_test_servers), color = if (!testServersDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) - } - SectionItemView(saveSMPServers, disabled = saveDisabled) { - Text(stringResource(MR.strings.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) - } - } - SectionDividerSpaced(maxBottomPadding = false) - SectionView { - HowToButton() - } - SectionBottomSpacer() - } -} - -@Composable -private fun ProtocolServerView(serverProtocol: ServerProtocol, srv: ServerCfg, servers: List<ServerCfg>, disabled: Boolean) { - val address = parseServerAddress(srv.server) - when { - address == null || !address.valid || address.serverProtocol != serverProtocol || !uniqueAddress(srv, address, servers) -> InvalidServer() - !srv.enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary) - else -> ShowTestStatus(srv) - } - Spacer(Modifier.padding(horizontal = 4.dp)) - val text = address?.hostnames?.firstOrNull() ?: srv.server - if (srv.enabled) { - Text(text, color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.onBackground, maxLines = 1) - } else { - Text(text, maxLines = 1, color = MaterialTheme.colors.secondary) - } -} - -@Composable -private fun HowToButton() { - val uriHandler = LocalUriHandler.current - SettingsActionItem( - painterResource(MR.images.ic_open_in_new), - stringResource(MR.strings.how_to_use_your_servers), - { uriHandler.openUriCatching("https://simplex.chat/docs/server.html") }, - textColor = MaterialTheme.colors.primary, - iconColor = MaterialTheme.colors.primary - ) -} - -@Composable -fun InvalidServer() { - Icon(painterResource(MR.images.ic_error), null, tint = MaterialTheme.colors.error) -} - -private fun uniqueAddress(s: ServerCfg, address: ServerAddress, servers: List<ServerCfg>): Boolean = servers.all { srv -> - address.hostnames.all { host -> - srv.id == s.id || !srv.server.contains(host) - } -} - -private fun hasAllPresets(presetServers: List<String>, servers: List<ServerCfg>, m: ChatModel): Boolean = - presetServers.all { hasPreset(it, servers) } ?: true - -private fun addAllPresets(rhId: Long?, presetServers: List<String>, servers: List<ServerCfg>, m: ChatModel): List<ServerCfg> { - val toAdd = ArrayList<ServerCfg>() - for (srv in presetServers) { - if (!hasPreset(srv, servers)) { - toAdd.add(ServerCfg(remoteHostId = rhId, srv, preset = true, tested = null, enabled = true)) - } - } - return toAdd -} - -private fun hasPreset(srv: String, servers: List<ServerCfg>): Boolean = - servers.any { it.server == srv } - -private suspend fun testServers(testing: MutableState<Boolean>, servers: List<ServerCfg>, m: ChatModel, onUpdated: (List<ServerCfg>) -> Unit) { - val resetStatus = resetTestStatus(servers) - onUpdated(resetStatus) - testing.value = true - val fs = runServersTest(resetStatus, m) { onUpdated(it) } - testing.value = false - if (fs.isNotEmpty()) { - val msg = fs.map { it.key + ": " + it.value.localizedDescription }.joinToString("\n") - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.smp_servers_test_failed), - text = generalGetString(MR.strings.smp_servers_test_some_failed) + "\n" + msg - ) - } -} - -private fun resetTestStatus(servers: List<ServerCfg>): List<ServerCfg> { - val copy = ArrayList(servers) - for ((index, server) in servers.withIndex()) { - if (server.enabled) { - copy.removeAt(index) - copy.add(index, server.copy(tested = null)) - } - } - return copy -} - -private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpdated: (List<ServerCfg>) -> Unit): Map<String, ProtocolTestFailure> { - val fs: MutableMap<String, ProtocolTestFailure> = mutableMapOf() - val updatedServers = ArrayList<ServerCfg>(servers) - for ((index, server) in servers.withIndex()) { - if (server.enabled) { - val (updatedServer, f) = testServerConnection(server, m) - updatedServers.removeAt(index) - updatedServers.add(index, updatedServer) - // toList() is important. Otherwise, Compose will not redraw the screen after first update - onUpdated(updatedServers.toList()) - if (f != null) { - fs[serverHostname(updatedServer.server)] = f - } - } - } - return fs -} - -private fun saveServers(rhId: Long?, protocol: ServerProtocol, currServers: MutableState<List<ServerCfg>>, servers: List<ServerCfg>, m: ChatModel, afterSave: () -> Unit = {}) { - withApi { - if (m.controller.setUserProtoServers(rhId, protocol, servers)) { - currServers.value = servers - m.userSMPServersUnsaved.value = null - } - afterSave() - } -} - -private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { - AlertManager.shared.showAlertDialogStacked( - title = generalGetString(MR.strings.smp_save_servers_question), - confirmText = generalGetString(MR.strings.save_verb), - dismissText = generalGetString(MR.strings.exit_without_saving), - onConfirm = save, - onDismiss = revert, - ) -} 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 7cb30440d3..761a74d6e4 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 @@ -19,6 +19,7 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.parseRTCIceServers import chat.simplex.common.views.helpers.* @@ -98,12 +99,11 @@ fun RTCServersLayout( saveRTCServers: () -> Unit, editOn: () -> Unit, ) { - Column { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_ICE_servers)) Column( Modifier .fillMaxWidth() - .verticalScroll(rememberScrollState()) .padding(horizontal = DEFAULT_PADDING), verticalArrangement = Arrangement.spacedBy(8.dp) ) { @@ -155,7 +155,8 @@ fun RTCServersLayout( .height(160.dp) .fillMaxWidth(), shape = RoundedCornerShape(10.dp), - border = BorderStroke(1.dp, MaterialTheme.colors.secondaryVariant) + border = BorderStroke(1.dp, MaterialTheme.colors.secondaryVariant), + contentColor = LocalContentColor.current ) { SelectionContainer( Modifier.verticalScroll(rememberScrollState()) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt deleted file mode 100644 index 77cb0ead13..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt +++ /dev/null @@ -1,45 +0,0 @@ -package chat.simplex.common.views.usersettings - -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.unit.dp -import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress -import chat.simplex.common.model.ServerCfg -import chat.simplex.common.ui.theme.DEFAULT_PADDING -import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.newchat.QRCodeScanner -import chat.simplex.res.MR - -@Composable -expect fun ScanProtocolServer(rhId: Long?, onNext: (ServerCfg) -> Unit) - -@Composable -fun ScanProtocolServerLayout(rhId: Long?, onNext: (ServerCfg) -> Unit) { - Column( - Modifier - .fillMaxSize() - .padding(horizontal = DEFAULT_PADDING) - ) { - AppBarTitle(stringResource(MR.strings.smp_servers_scan_qr), withPadding = false) - Box( - Modifier - .fillMaxWidth() - .aspectRatio(ratio = 1F) - .padding(bottom = 12.dp) - ) { - QRCodeScanner { text -> - val res = parseServerAddress(text) - if (res != null) { - onNext(ServerCfg(remoteHostId = rhId, text, false, null, true)) - } else { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.smp_servers_invalid_address), - text = generalGetString(MR.strings.smp_servers_check_address) - ) - } - } - } - } -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt index b75f522686..ef4acdeac6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt @@ -24,7 +24,7 @@ fun SetDeliveryReceiptsView(m: ChatModel) { enableReceipts = { val currentUser = m.currentUser.value if (currentUser != null) { - withApi { + withBGApi { try { m.controller.apiSetAllContactReceipts(currentUser.remoteHostId, enable = true) m.currentUser.value = currentUser.copy(sendRcptsContacts = true) @@ -73,27 +73,25 @@ private fun SetDeliveryReceiptsLayout( skip: () -> Unit, userCount: Int, ) { - val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp - Column( - Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(top = DEFAULT_PADDING, end = endPadding), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AppBarTitle(stringResource(MR.strings.delivery_receipts_title)) + Box(Modifier.padding(top = DEFAULT_PADDING)) { + ColumnWithScrollBar(horizontalAlignment = Alignment.CenterHorizontally) { + AppBarTitle(stringResource(MR.strings.delivery_receipts_title)) - Spacer(Modifier.weight(1f)) + Spacer(Modifier.weight(1f)) - EnableReceiptsButton(enableReceipts) - if (userCount > 1) { - TextBelowButton(stringResource(MR.strings.sending_delivery_receipts_will_be_enabled_all_profiles)) - } else { - TextBelowButton(stringResource(MR.strings.sending_delivery_receipts_will_be_enabled)) + EnableReceiptsButton(enableReceipts) + if (userCount > 1) { + TextBelowButton(stringResource(MR.strings.sending_delivery_receipts_will_be_enabled_all_profiles)) + } else { + TextBelowButton(stringResource(MR.strings.sending_delivery_receipts_will_be_enabled)) + } + + Spacer(Modifier.weight(1f)) + + SkipButton(skip) + + SectionBottomSpacer() } - - Spacer(Modifier.weight(1f)) - - SkipButton(skip) - - SectionBottomSpacer() } } 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 0f37fb1979..7ea656e1e4 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 @@ -3,7 +3,6 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced import SectionItemView -import SectionItemViewWithIcon import SectionView import TextIconSpaced import androidx.compose.desktop.ui.tooling.preview.Preview @@ -22,25 +21,24 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.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.CreateProfile import chat.simplex.common.views.database.DatabaseView import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.migration.MigrateFromDeviceView import chat.simplex.common.views.onboarding.SimpleXInfo import chat.simplex.common.views.onboarding.WhatsNewView -import chat.simplex.common.views.remote.ConnectDesktopView -import chat.simplex.common.views.remote.ConnectMobileView +import chat.simplex.common.views.usersettings.networkAndServers.NetworkAndServersView import chat.simplex.res.MR -import kotlinx.coroutines.launch @Composable -fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerState: DrawerState) { +fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: () -> Unit) { val user = chatModel.currentUser.value val stopped = chatModel.chatRunning.value == false SettingsLayout( - profile = user?.profile, stopped, chatModel.chatDbEncrypted.value == true, remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value, @@ -54,15 +52,15 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerSt val search = rememberSaveable { mutableStateOf("") } ModalView( { close() }, - endButtons = { - SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } - }, + showSearch = true, + searchAlwaysVisible = true, + onSearchValueChanged = { search.value = it }, content = { modalView(chatModel, search) }) } }, showCustomModal = { modalView -> { ModalManager.start.showCustomModal { close -> modalView(chatModel, close) } } }, showVersion = { - withApi { + withBGApi { val info = chatModel.controller.apiGetVersion() if (info != null) { ModalManager.start.showModal { VersionInfoView(info) } @@ -70,8 +68,10 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerSt } }, withAuth = ::doWithAuth, - drawerState = drawerState, ) + KeyChangeEffect(chatModel.updatingProgress.value != null) { + close() + } } val simplexTeamUri = @@ -79,7 +79,6 @@ val simplexTeamUri = @Composable fun SettingsLayout( - profile: LocalProfile?, stopped: Boolean, encrypted: Boolean, passphraseSaved: Boolean, @@ -89,121 +88,80 @@ fun SettingsLayout( showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModalWithSearch: (@Composable (ChatModel, MutableState<String>) -> Unit) -> Unit, - showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), + showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showVersion: () -> Unit, withAuth: (title: String, desc: String, block: () -> Unit) -> Unit, - drawerState: DrawerState, ) { - val scope = rememberCoroutineScope() - val closeSettings: () -> Unit = { scope.launch { drawerState.close() } } - if (drawerState.isOpen) { - BackHandler { - closeSettings() - } + val view = LocalMultiplatformView() + LaunchedEffect(Unit) { + hideKeyboard(view) } - val theme = CurrentColors.collectAsState() val uriHandler = LocalUriHandler.current - Box(Modifier.fillMaxSize()) { - Column( - Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .themedBackground(theme.value.base) - .padding(top = if (appPlatform.isAndroid) DEFAULT_PADDING else DEFAULT_PADDING * 3) - ) { - AppBarTitle(stringResource(MR.strings.your_settings)) + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.your_settings)) - SectionView(stringResource(MR.strings.settings_section_title_you)) { - val profileHidden = rememberSaveable { mutableStateOf(false) } - if (profile != null) { - SectionItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) { - ProfilePreview(profile, stopped = stopped) - } - SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.your_chat_profiles), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden) } } }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped, extraPadding = true) - ChatPreferencesItem(showCustomModal, stopped = stopped) - } else if (chatModel.localUserCreated.value == false) { - SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.create_chat_profile), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.center.showModalCloseable { close -> - LaunchedEffect(Unit) { - closeSettings() - } - CreateProfile(chatModel, close) - } } }, disabled = stopped, extraPadding = true) - } - if (appPlatform.isDesktop) { - SettingsActionItem(painterResource(MR.images.ic_smartphone), stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), showModal { ConnectMobileView() }, disabled = stopped, extraPadding = true) - } else { - SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal{ it, close -> ConnectDesktopView(close) }, disabled = stopped, extraPadding = true) - } + SectionView(stringResource(MR.strings.settings_section_title_settings)) { + SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showCustomModal { _, close -> NetworkAndServersView(close) }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it) }) + } + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.settings_section_title_chat_database)) { + DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView() }, stopped) + SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_from_device_to_another_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateFromDeviceView(close) } } }, disabled = stopped) + } + + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.settings_section_title_help)) { + SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName ?: "") }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close = close) }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) }) + if (!chatModel.desktopNoUserNoRemote) { + SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped) } - SectionDividerSpaced() + SettingsActionItem(painterResource(MR.images.ic_mail), stringResource(MR.strings.send_us_an_email), { uriHandler.openUriCatching("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary) + } + SectionDividerSpaced() - SectionView(stringResource(MR.strings.settings_section_title_settings)) { - SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView(it, showModal, showSettingsModal, showCustomModal) }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it, showSettingsModal) }, extraPadding = true) - DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) - } - SectionDividerSpaced() - - SectionView(stringResource(MR.strings.settings_section_title_help)) { - SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName ?: "") }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) }, extraPadding = true) - if (!chatModel.desktopNoUserNoRemote) { - SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped, extraPadding = true) - } - SettingsActionItem(painterResource(MR.images.ic_mail), stringResource(MR.strings.send_us_an_email), { uriHandler.openUriCatching("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary, extraPadding = true) - } - SectionDividerSpaced() - - SectionView(stringResource(MR.strings.settings_section_title_support)) { + SectionView(stringResource(MR.strings.settings_section_title_support)) { + if (!BuildConfigCommon.ANDROID_BUNDLE) { ContributeItem(uriHandler) - RateAppItem(uriHandler) - StarOnGithubItem(uriHandler) } - SectionDividerSpaced() + RateAppItem(uriHandler) + StarOnGithubItem(uriHandler) + } + SectionDividerSpaced() - SettingsSectionApp(showSettingsModal, showCustomModal, showVersion, withAuth) - SectionBottomSpacer() - } - if (appPlatform.isDesktop) { - Box( - Modifier - .fillMaxWidth() - .background(MaterialTheme.colors.background) - .background(if (isInDarkTheme()) ToolbarDark else ToolbarLight) - .padding(start = 4.dp, top = 8.dp) - ) { - NavigationButtonBack(closeSettings) - } - } + SettingsSectionApp(showSettingsModal, showVersion, withAuth) + SectionBottomSpacer() } } @Composable expect fun SettingsSectionApp( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showVersion: () -> Unit, withAuth: (title: String, desc: String, block: () -> Unit) -> Unit ) @Composable private fun DatabaseItem(encrypted: Boolean, saved: Boolean, openDatabaseView: () -> Unit, stopped: Boolean) { - SectionItemViewWithIcon(openDatabaseView) { + SectionItemView(openDatabaseView) { Row( Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - Row(Modifier.weight(1f)) { + Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) { Icon( painterResource(MR.images.ic_database), contentDescription = stringResource(MR.strings.database_passphrase_and_export), tint = if (encrypted && (appPlatform.isAndroid || !saved)) MaterialTheme.colors.secondary else WarningOrange, ) - TextIconSpaced(true) + TextIconSpaced(false) Text(stringResource(MR.strings.database_passphrase_and_export)) } if (stopped) { @@ -218,19 +176,16 @@ expect fun SettingsSectionApp( } } -@Composable fun ChatPreferencesItem(showCustomModal: ((@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit)), stopped: Boolean) { +@Composable fun ChatPreferencesItem(showCustomModal: ((@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit)), stopped: Boolean) { SettingsActionItem( painterResource(MR.images.ic_toggle_on), stringResource(MR.strings.chat_preferences), click = if (stopped) null else ({ - withApi { - showCustomModal { m, close -> - PreferencesView(m, m.currentUser.value ?: return@showCustomModal, close) - }() - } + showCustomModal { m, close -> + PreferencesView(m, m.currentUser.value ?: return@showCustomModal, close) + }() }), - disabled = stopped, - extraPadding = true + disabled = stopped ) } @@ -239,33 +194,32 @@ fun ChatLockItem( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), setPerformLA: (Boolean) -> Unit ) { - val performLA = remember { ChatModel.performLA } + val performLA = remember { appPrefs.performLA.state } val currentLAMode = remember { ChatModel.controller.appPrefs.laMode } SettingsActionItemWithContent( click = showSettingsModal { SimplexLockView(ChatModel, currentLAMode, setPerformLA) }, icon = if (performLA.value) painterResource(MR.images.ic_lock_filled) else painterResource(MR.images.ic_lock), text = stringResource(MR.strings.chat_lock), - iconColor = if (performLA.value) SimplexGreen else MaterialTheme.colors.secondary, - extraPadding = false, + iconColor = if (performLA.value) SimplexGreen else MaterialTheme.colors.secondary ) { Text(if (performLA.value) remember { currentLAMode.state }.value.text else generalGetString(MR.strings.la_mode_off), color = MaterialTheme.colors.secondary) } } @Composable private fun ContributeItem(uriHandler: UriHandler) { - SectionItemViewWithIcon({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat#contribute") }) { + SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat#contribute") }) { Icon( painterResource(MR.images.ic_keyboard), contentDescription = "GitHub", tint = MaterialTheme.colors.secondary, ) - TextIconSpaced(extraPadding = true) + TextIconSpaced() Text(generalGetString(MR.strings.contribute), color = MaterialTheme.colors.primary) } } @Composable private fun RateAppItem(uriHandler: UriHandler) { - SectionItemViewWithIcon({ + SectionItemView({ runCatching { uriHandler.openUriCatching("market://details?id=chat.simplex.app") } .onFailure { uriHandler.openUriCatching("https://play.google.com/store/apps/details?id=chat.simplex.app") } } @@ -275,19 +229,19 @@ fun ChatLockItem( contentDescription = "Google Play", tint = MaterialTheme.colors.secondary, ) - TextIconSpaced(extraPadding = true) + TextIconSpaced() Text(generalGetString(MR.strings.rate_the_app), color = MaterialTheme.colors.primary) } } @Composable private fun StarOnGithubItem(uriHandler: UriHandler) { - SectionItemViewWithIcon({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat") }) { + SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat") }) { Icon( painter = painterResource(MR.images.ic_github), contentDescription = "GitHub", tint = MaterialTheme.colors.secondary, ) - TextIconSpaced(extraPadding = true) + TextIconSpaced() Text(generalGetString(MR.strings.star_on_github), color = MaterialTheme.colors.primary) } } @@ -305,7 +259,7 @@ fun ChatLockItem( } @Composable fun TerminalAlwaysVisibleItem(pref: SharedPreference<Boolean>, onChange: (Boolean) -> Unit) { - SettingsActionItemWithContent(painterResource(MR.images.ic_engineering), stringResource(MR.strings.terminal_always_visible), extraPadding = false) { + SettingsActionItemWithContent(painterResource(MR.images.ic_engineering), stringResource(MR.strings.terminal_always_visible)) { DefaultSwitch( checked = remember { pref.state }.value, onCheckedChange = onChange, @@ -325,9 +279,34 @@ fun ChatLockItem( } } +@Composable fun ResetHintsItem(unchangedHints: MutableState<Boolean>) { + SectionItemView({ + resetHintPreferences() + unchangedHints.value = true + }, disabled = unchangedHints.value) { + Icon( + painter = painterResource(MR.images.ic_lightbulb), + contentDescription = "Lightbulb", + tint = MaterialTheme.colors.secondary, + ) + TextIconSpaced() + Text(generalGetString(MR.strings.reset_all_hints), color = if (unchangedHints.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + } +} + +private fun resetHintPreferences() { + for ((pref, def) in appPreferences.hintPreferences) { + pref.set(def) + } +} + +fun unchangedHintPreferences(): Boolean = appPreferences.hintPreferences.all { (pref, def) -> + pref.state.value == def +} + @Composable fun AppVersionItem(showVersion: () -> Unit) { - SectionItemViewWithIcon(showVersion) { AppVersionText() } + SectionItemView(showVersion) { AppVersionText() } } @Composable fun AppVersionText() { @@ -368,7 +347,7 @@ fun SettingsActionItem(icon: Painter, text: String, click: (() -> Unit)? = null, } @Composable -fun SettingsActionItemWithContent(icon: Painter?, text: String? = null, click: (() -> Unit)? = null, iconColor: Color = MaterialTheme.colors.secondary, disabled: Boolean = false, extraPadding: Boolean = false, content: @Composable RowScope.() -> Unit) { +fun SettingsActionItemWithContent(icon: Painter?, text: String? = null, click: (() -> Unit)? = null, iconColor: Color = MaterialTheme.colors.secondary, textColor: Color = MaterialTheme.colors.onBackground, disabled: Boolean = false, extraPadding: Boolean = false, content: @Composable RowScope.() -> Unit) { SectionItemView( click, extraPadding = extraPadding, @@ -384,7 +363,7 @@ fun SettingsActionItemWithContent(icon: Painter?, text: String? = null, click: ( } if (text != null) { val padding = with(LocalDensity.current) { 6.sp.toDp() } - Text(text, Modifier.weight(1f).padding(vertical = padding), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.onBackground) + Text(text, Modifier.weight(1f).padding(vertical = padding), color = if (disabled) MaterialTheme.colors.secondary else textColor) Spacer(Modifier.width(DEFAULT_PADDING)) Row(Modifier.widthIn(max = (windowWidth() - DEFAULT_PADDING * 2) / 2)) { content() @@ -414,13 +393,15 @@ fun SettingsPreferenceItem( @Composable fun PreferenceToggle( text: String, + disabled: Boolean = false, checked: Boolean, onChange: (Boolean) -> Unit = {}, ) { - SettingsActionItemWithContent(null, text, extraPadding = true,) { + SettingsActionItemWithContent(null, text, disabled = disabled) { DefaultSwitch( checked = checked, onCheckedChange = onChange, + enabled = !disabled ) } } @@ -430,6 +411,7 @@ fun PreferenceToggleWithIcon( text: String, icon: Painter? = null, iconColor: Color? = MaterialTheme.colors.secondary, + disabled: Boolean = false, checked: Boolean, extraPadding: Boolean = false, onChange: (Boolean) -> Unit = {}, @@ -440,6 +422,7 @@ fun PreferenceToggleWithIcon( onCheckedChange = { onChange(it) }, + enabled = !disabled ) } } @@ -464,17 +447,19 @@ fun doWithAuth(title: String, desc: String, block: () -> Unit) { runAuth(title, desc, onFinishAuth) } } - Box( - Modifier.fillMaxSize().background(MaterialTheme.colors.background), - contentAlignment = Alignment.Center - ) { - SimpleButton( - stringResource(MR.strings.auth_unlock), - icon = painterResource(MR.images.ic_lock), - click = { - runAuth(title, desc, onFinishAuth) - } - ) + Surface(color = MaterialTheme.colors.background.copy(1f), contentColor = LocalContentColor.current) { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + SimpleButton( + stringResource(MR.strings.auth_unlock), + icon = painterResource(MR.images.ic_lock), + click = { + runAuth(title, desc, onFinishAuth) + } + ) + } } } } @@ -484,6 +469,7 @@ private fun runAuth(title: String, desc: String, onFinish: (success: Boolean) -> authenticate( title, desc, + oneTime = true, completed = { laResult -> onFinish(laResult == LAResult.Success || laResult is LAResult.Unavailable) } @@ -499,7 +485,6 @@ private fun runAuth(title: String, desc: String, onFinish: (success: Boolean) -> fun PreviewSettingsLayout() { SimpleXTheme { SettingsLayout( - profile = LocalProfile.sampleData, stopped = false, encrypted = false, passphraseSaved = false, @@ -512,7 +497,6 @@ fun PreviewSettingsLayout() { showCustomModal = { {} }, showVersion = {}, withAuth = { _, _, _ -> }, - drawerState = DrawerState(DrawerValue.Closed), ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt index 276c595435..ea0cd4fe28 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt @@ -1,24 +1,94 @@ package chat.simplex.common.views.usersettings -import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable +import androidx.compose.foundation.shape.CircleShape +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.graphics.Color +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.platform.ColumnWithScrollBar +import chat.simplex.common.platform.chatModel +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.onboarding.ReadableText -import chat.simplex.common.views.onboarding.ReadableTextWithLink +import chat.simplex.common.views.newchat.* +import chat.simplex.common.views.onboarding.* import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource @Composable -fun UserAddressLearnMore() { - Column( - Modifier.verticalScroll(rememberScrollState()), - ) { - AppBarTitle(stringResource(MR.strings.simplex_address)) - ReadableText(MR.strings.you_can_share_your_address) +fun UserAddressLearnMore(showCreateAddressButton: Boolean = false) { + ColumnWithScrollBar(Modifier .padding(horizontal = DEFAULT_PADDING)) { + AppBarTitle(stringResource(MR.strings.address_or_1_time_link), withPadding = false) + + Row { + Icon(painterResource(MR.images.ic_mail), null, tint = MaterialTheme.colors.secondary) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + ReadableText(MR.strings.share_address_publicly, style = MaterialTheme.typography.h3.copy(fontWeight = FontWeight.Bold)) + } + ReadableText(MR.strings.share_simplex_address_on_social_media) ReadableText(MR.strings.you_wont_lose_your_contacts_if_delete_address) - ReadableText(MR.strings.you_can_accept_or_reject_connection) - ReadableTextWithLink(MR.strings.read_more_in_user_guide_with_link, "https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address") + + Row(Modifier.padding(top = DEFAULT_PADDING_HALF)) { + Icon(painterResource(MR.images.ic_add_link), null, tint = MaterialTheme.colors.secondary) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + ReadableText(MR.strings.share_1_time_link_with_a_friend, style = MaterialTheme.typography.h3.copy(fontWeight = FontWeight.Bold)) + } + ReadableText(MR.strings.one_time_link_can_be_used_with_one_contact_only) + ReadableText(MR.strings.you_can_set_connection_name_to_remember) + + if (!showCreateAddressButton) { + Row(Modifier.padding(top = DEFAULT_PADDING_HALF)) { + Icon(painterResource(MR.images.ic_shield), null, tint = MaterialTheme.colors.secondary) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + ReadableText(MR.strings.connection_security, style = MaterialTheme.typography.h3.copy(fontWeight = FontWeight.Bold)) + } + ReadableText(MR.strings.simplex_address_and_1_time_links_are_safe_to_share) + ReadableText(MR.strings.to_protect_against_your_link_replaced_compare_codes) + ReadableTextWithLink(MR.strings.read_more_in_user_guide_with_link, "https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses") + } + + if (showCreateAddressButton) { + Spacer(Modifier.weight(1f)) + Column(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING * 2), horizontalAlignment = Alignment.CenterHorizontally) { + Button( + onClick = { + ModalManager.start.showModalCloseable { close -> + UserAddressView(chatModel = chatModel, shareViaProfile = false, autoCreateAddress = true, close = { ModalManager.start.closeModals() }) + } + }, + shape = CircleShape, + contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 2, vertical = DEFAULT_PADDING), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary, disabledBackgroundColor = MaterialTheme.colors.secondary) + ) { + Text(stringResource(MR.strings.create_simplex_address), style = MaterialTheme.typography.h2, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Medium) + } + + val closeAll = { ModalManager.start.closeModals() } + TextButton( + onClick = { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll) + } + }, + Modifier.padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 2).clip(CircleShape) + ) { + Text( + stringResource(MR.strings.create_1_time_link), + Modifier.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING_HALF, bottom = 5.dp), + color = MaterialTheme.colors.primary, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center + ) + } + } + } } } 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 915120d81d..8c7c2d8416 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 @@ -5,11 +5,10 @@ import SectionDividerSpaced import SectionItemView import SectionTextFooter import SectionView +import SectionViewWithButton import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -33,14 +32,13 @@ import chat.simplex.res.MR @Composable fun UserAddressView( chatModel: ChatModel, - viaCreateLinkView: Boolean = false, shareViaProfile: Boolean = false, + autoCreateAddress: Boolean = false, close: () -> Unit ) { // TODO close when remote host changes val shareViaProfile = remember { mutableStateOf(shareViaProfile) } var progressIndicator by remember { mutableStateOf(false) } - val onCloseHandler: MutableState<(close: () -> Unit) -> Unit> = remember { mutableStateOf({ _ -> }) } val user = remember { chatModel.currentUser } KeyChangeEffect(user.value?.remoteHostId, user.value?.userId) { close() @@ -60,6 +58,34 @@ fun UserAddressView( } } } + + fun createAddress() { + withBGApi { + progressIndicator = true + val short = appPreferences.privacyShortLinks.get() + val connReqContact = chatModel.controller.apiCreateUserAddress(user.value?.remoteHostId, short = short) + if (connReqContact != null) { + chatModel.userAddress.value = UserContactLinkRec(connReqContact) + + 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 = false + } + } + + LaunchedEffect(autoCreateAddress) { + if (chatModel.userAddress.value == null && autoCreateAddress) { + createAddress() + } + } val userAddress = remember { chatModel.userAddress } val clipboard = LocalClipboardManager.current val uriHandler = LocalUriHandler.current @@ -68,44 +94,17 @@ fun UserAddressView( user = user.value, userAddress = userAddress.value, shareViaProfile, - onCloseHandler, - createAddress = { - withApi { - progressIndicator = true - val connReqContact = chatModel.controller.apiCreateUserAddress(user?.value?.remoteHostId) - if (connReqContact != null) { - chatModel.userAddress.value = UserContactLinkRec(connReqContact) - - 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 = false - } - }, + createAddress = { createAddress() }, learnMore = { ModalManager.start.showModal { - Column( - Modifier - .fillMaxHeight() - .padding(horizontal = DEFAULT_PADDING), - verticalArrangement = Arrangement.SpaceBetween - ) { - UserAddressLearnMore() - } + UserAddressLearnMore() } }, share = { userAddress: String -> clipboard.shareText(userAddress) }, sendEmail = { userAddress -> uriHandler.sendEmail( generalGetString(MR.strings.email_invite_subject), - generalGetString(MR.strings.email_invite_body).format(simplexChatLink( userAddress.connReqContact)) + generalGetString(MR.strings.email_invite_body).format(simplexChatLink(userAddress.connLinkContact.connFullLink)) // TODO [short links] replace with short link ) }, setProfileAddress = ::setProfileAddress, @@ -116,7 +115,7 @@ fun UserAddressView( confirmText = generalGetString(MR.strings.delete_verb), onConfirm = { progressIndicator = true - withApi { + withBGApi { val u = chatModel.controller.apiDeleteUserAddress(user?.value?.remoteHostId) if (u != null) { chatModel.userAddress.value = null @@ -141,12 +140,8 @@ fun UserAddressView( ) } - if (viaCreateLinkView) { + ModalView(close = close) { showLayout() - } else { - ModalView(close = { onCloseHandler.value(close) }) { - showLayout() - } } if (progressIndicator) { @@ -155,7 +150,7 @@ fun UserAddressView( contentAlignment = Alignment.Center ) { if (userAddress.value != null) { - Surface(Modifier.size(50.dp), color = MaterialTheme.colors.background.copy(0.9f), shape = RoundedCornerShape(50)){} + Surface(Modifier.size(50.dp), color = MaterialTheme.colors.background.copy(0.9f), contentColor = LocalContentColor.current, shape = RoundedCornerShape(50)){} } CircularProgressIndicator( Modifier @@ -173,7 +168,6 @@ private fun UserAddressLayout( user: User?, userAddress: UserContactLinkRec?, shareViaProfile: MutableState<Boolean>, - onCloseHandler: MutableState<(close: () -> Unit) -> Unit>, createAddress: () -> Unit, learnMore: () -> Unit, share: (String) -> Unit, @@ -182,55 +176,61 @@ private fun UserAddressLayout( deleteAddress: () -> Unit, saveAas: (AutoAcceptState, MutableState<AutoAcceptState>) -> Unit, ) { - Column( - Modifier.verticalScroll(rememberScrollState()), - ) { - AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(user?.remoteHostId), withPadding = false) + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(user?.remoteHostId)) Column( Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceEvenly ) { if (userAddress == null) { - SectionView { + SectionView(generalGetString(MR.strings.for_social_media).uppercase()) { CreateAddressButton(createAddress) - SectionTextFooter(stringResource(MR.strings.create_address_and_let_people_connect)) } - SectionDividerSpaced(maxBottomPadding = false) + + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) { + CreateOneTimeLinkButton() + } + + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) SectionView { LearnMoreButton(learnMore) } - LaunchedEffect(Unit) { - onCloseHandler.value = { close -> close() } - } } else { val autoAcceptState = remember { mutableStateOf(AutoAcceptState(userAddress)) } val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) } - SectionView(stringResource(MR.strings.address_section_title).uppercase()) { - SimpleXLinkQRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) - ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) } - ShareViaEmailButton { sendEmail(userAddress) } - ShareWithContactsButton(shareViaProfile, setProfileAddress) - AutoAcceptToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) } - LearnMoreButton(learnMore) + val showShortLink = remember { mutableStateOf(true) } + + SectionViewWithButton( + stringResource(MR.strings.for_social_media).uppercase(), + titleButton = if (userAddress.connLinkContact.connShortLink != null) {{ ToggleShortLinkButton(showShortLink) }} else null + ) { + SimpleXCreatedLinkQRCode(userAddress.connLinkContact, short = showShortLink.value) + ShareAddressButton { share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value)) } + // ShareViaEmailButton { sendEmail(userAddress) } + BusinessAddressToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) } + AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAas) + + if (autoAcceptState.value.business) { + SectionTextFooter(stringResource(MR.strings.add_your_team_members_to_conversations)) + } } - if (autoAcceptState.value.enable) { - SectionDividerSpaced() - AutoAcceptSection(autoAcceptState, autoAcceptStateSaved, saveAas) + + SectionDividerSpaced(maxTopPadding = autoAcceptState.value.business) + 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)) } - LaunchedEffect(Unit) { - onCloseHandler.value = { close -> - if (autoAcceptState.value == autoAcceptStateSaved.value) close() - else showUnsavedChangesAlert({ saveAas(autoAcceptState.value, autoAcceptStateSaved); close() }, close) - } - } } } SectionBottomSpacer() @@ -248,11 +248,27 @@ private fun CreateAddressButton(onClick: () -> Unit) { ) } +@Composable +private fun CreateOneTimeLinkButton() { + val closeAll = { ModalManager.start.closeModals() } + SettingsActionItem( + painterResource(MR.images.ic_add_link), + stringResource(MR.strings.create_1_time_link), + click = { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll) + } + }, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) +} + @Composable private fun LearnMoreButton(onClick: () -> Unit) { SettingsActionItem( painterResource(MR.images.ic_info), - stringResource(MR.strings.learn_more_about_address), + stringResource(MR.strings.simplex_address_or_1_time_link), onClick, ) } @@ -268,6 +284,85 @@ fun ShareViaEmailButton(onClick: () -> Unit) { ) } +@Composable +private fun AddressSettingsButton( + user: User?, + userAddress: UserContactLinkRec, + shareViaProfile: MutableState<Boolean>, + setProfileAddress: (Boolean) -> Unit, + saveAas: (AutoAcceptState, MutableState<AutoAcceptState>) -> Unit, +) { + SettingsActionItem( + painterResource(MR.images.ic_settings), + stringResource(MR.strings.address_settings), + click = { + ModalManager.start.showCustomModal { close -> + UserAddressSettings(user, userAddress, shareViaProfile, setProfileAddress, saveAas, close = close) + } + } + ) +} + +@Composable +private fun ModalData.UserAddressSettings( + user: User?, + userAddress: UserContactLinkRec, + shareViaProfile: MutableState<Boolean>, + setProfileAddress: (Boolean) -> Unit, + saveAas: (AutoAcceptState, MutableState<AutoAcceptState>) -> Unit, + close: () -> Unit +) { + val autoAcceptState = remember { stateGetOrPut("autoAcceptState") { (AutoAcceptState(userAddress)) } } + val autoAcceptStateSaved = remember { stateGetOrPut("autoAcceptStateSaved") { (autoAcceptState.value) } } + + fun onClose(close: () -> Unit): Boolean = if (autoAcceptState.value == autoAcceptStateSaved.value) { + chatModel.centerPanelBackgroundClickHandler = null + close() + false + } else { + showUnsavedChangesAlert( + save = { + saveAas(autoAcceptState.value, autoAcceptStateSaved) + chatModel.centerPanelBackgroundClickHandler = null + close() + }, + revert = { + chatModel.centerPanelBackgroundClickHandler = null + close() + } + ) + true + } + + LaunchedEffect(Unit) { + // Enables unsaved changes alert on this view. + chatModel.centerPanelBackgroundClickHandler = { + onClose(close = { ModalManager.start.closeModals() }) + } + } + + ModalView(close = { onClose(close) }) { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.address_settings), hostDevice(user?.remoteHostId)) + Column( + Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceEvenly + ) { + SectionView { + ShareWithContactsButton(shareViaProfile, setProfileAddress) + AutoAcceptToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) } + } + + if (autoAcceptState.value.enable) { + SectionDividerSpaced() + AutoAcceptSection(autoAcceptState, autoAcceptStateSaved, saveAas) + } + } + } + } +} + @Composable fun ShareWithContactsButton(shareViaProfile: MutableState<Boolean>, setProfileAddress: (Boolean) -> Unit) { PreferenceToggleWithIcon( @@ -304,17 +399,37 @@ fun ShareWithContactsButton(shareViaProfile: MutableState<Boolean>, setProfileAd onDismissRequest = { shareViaProfile.value = !on }) + } } +} + +@Composable +private fun BusinessAddressToggle(autoAcceptState: MutableState<AutoAcceptState>, saveAas: (AutoAcceptState) -> Unit) { + PreferenceToggleWithIcon( + stringResource(MR.strings.business_address), + painterResource(MR.images.ic_work), + checked = autoAcceptState.value.business, + ) { ba -> + autoAcceptState.value = if (ba) + AutoAcceptState(enable = true, incognito = false, business = true, autoAcceptState.value.welcomeText) + else + AutoAcceptState(autoAcceptState.value.enable, autoAcceptState.value.incognito, business = false, autoAcceptState.value.welcomeText) + saveAas(autoAcceptState.value) } } @Composable private fun AutoAcceptToggle(autoAcceptState: MutableState<AutoAcceptState>, saveAas: (AutoAcceptState) -> Unit) { - PreferenceToggleWithIcon(stringResource(MR.strings.auto_accept_contact), painterResource(MR.images.ic_check), checked = autoAcceptState.value.enable) { + PreferenceToggleWithIcon( + stringResource(MR.strings.auto_accept_contact), + painterResource(MR.images.ic_check), + disabled = autoAcceptState.value.business, + checked = autoAcceptState.value.enable + ) { autoAcceptState.value = if (!it) AutoAcceptState() else - AutoAcceptState(it, autoAcceptState.value.incognito, autoAcceptState.value.welcomeText) + AutoAcceptState(it, autoAcceptState.value.incognito, autoAcceptState.value.business, autoAcceptState.value.welcomeText) saveAas(autoAcceptState.value) } } @@ -335,12 +450,15 @@ private class AutoAcceptState { private set var incognito: Boolean = false private set + var business: Boolean = false + private set var welcomeText: String = "" private set - constructor(enable: Boolean = false, incognito: Boolean = false, welcomeText: String = "") { + constructor(enable: Boolean = false, incognito: Boolean = false, business: Boolean = false, welcomeText: String = "") { this.enable = enable this.incognito = incognito + this.business = business this.welcomeText = welcomeText } @@ -348,6 +466,7 @@ private class AutoAcceptState { contactLink.autoAccept?.let { aa -> enable = true incognito = aa.acceptIncognito + business = aa.businessAddress aa.autoReply?.let { msg -> welcomeText = msg.text } ?: run { @@ -364,19 +483,20 @@ private class AutoAcceptState { if (s != "") { autoReply = MsgContent.MCText(s) } - return AutoAccept(incognito, autoReply) + return AutoAccept(business, incognito, autoReply) } return null } override fun equals(other: Any?): Boolean { if (other !is AutoAcceptState) return false - return this.enable == other.enable && this.incognito == other.incognito && this.welcomeText == other.welcomeText + return this.enable == other.enable && this.incognito == other.incognito && this.business == other.business && this.welcomeText == other.welcomeText } override fun hashCode(): Int { var result = enable.hashCode() result = 31 * result + incognito.hashCode() + result = 31 * result + business.hashCode() result = 31 * result + welcomeText.hashCode() return result } @@ -389,7 +509,9 @@ private fun AutoAcceptSection( saveAas: (AutoAcceptState, MutableState<AutoAcceptState>) -> Unit ) { SectionView(stringResource(MR.strings.auto_accept_contact).uppercase()) { - AcceptIncognitoToggle(autoAcceptState) + if (!autoAcceptState.value.business) { + AcceptIncognitoToggle(autoAcceptState) + } WelcomeMessageEditor(autoAcceptState) SaveAASButton(autoAcceptState.value == savedAutoAcceptState.value) { saveAas(autoAcceptState.value, savedAutoAcceptState) } } @@ -401,9 +523,9 @@ private fun AcceptIncognitoToggle(autoAcceptState: MutableState<AutoAcceptState> stringResource(MR.strings.accept_contact_incognito_button), if (autoAcceptState.value.incognito) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_theater_comedy), if (autoAcceptState.value.incognito) Indigo else MaterialTheme.colors.secondary, - autoAcceptState.value.incognito, + checked = autoAcceptState.value.incognito, ) { - autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, it, autoAcceptState.value.welcomeText) + autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, it, autoAcceptState.value.business, autoAcceptState.value.welcomeText) } } @@ -413,7 +535,7 @@ private fun WelcomeMessageEditor(autoAcceptState: MutableState<AutoAcceptState>) TextEditor(welcomeText, Modifier.height(100.dp), placeholder = stringResource(MR.strings.enter_welcome_message_optional)) LaunchedEffect(welcomeText.value) { if (welcomeText.value != autoAcceptState.value.welcomeText) { - autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, autoAcceptState.value.incognito, welcomeText.value) + autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, autoAcceptState.value.incognito, autoAcceptState.value.business, welcomeText.value) } } } @@ -443,7 +565,6 @@ fun PreviewUserAddressLayoutNoAddress() { setProfileAddress = { _ -> }, learnMore = {}, shareViaProfile = remember { mutableStateOf(false) }, - onCloseHandler = remember { mutableStateOf({}) }, sendEmail = {}, ) } @@ -469,7 +590,7 @@ fun PreviewUserAddressLayoutAddressCreated() { SimpleXTheme { UserAddressLayout( user = User.sampleData, - userAddress = UserContactLinkRec("https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"), + userAddress = UserContactLinkRec(CreatedConnLink("https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", null)), createAddress = {}, share = { _ -> }, deleteAddress = {}, @@ -477,7 +598,6 @@ fun PreviewUserAddressLayoutAddressCreated() { setProfileAddress = { _ -> }, learnMore = {}, shareViaProfile = remember { mutableStateOf(false) }, - onCloseHandler = remember { mutableStateOf({}) }, sendEmail = {}, ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt index bf7c4ac802..90122bd29d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt @@ -34,13 +34,14 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) { KeyChangeEffect(u.value?.remoteHostId, u.value?.userId) { close() } + if (user != null) { var profile by remember { mutableStateOf(user.profile.toProfile()) } UserProfileLayout( profile = profile, close, saveProfile = { displayName, fullName, image -> - withApi { + withBGApi { val updated = chatModel.controller.apiUpdateProfile(user.remoteHostId, profile.copy(displayName = displayName.trim(), fullName = fullName, image = image)) if (updated != null) { val (newProfile, _) = updated @@ -70,10 +71,8 @@ fun UserProfileLayout( val keyboardState by getKeyboardState() var savedKeyboardState by remember { mutableStateOf(keyboardState) } val focusRequester = remember { FocusRequester() } - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { ModalBottomSheetLayout( scrimColor = Color.Black.copy(alpha = 0.12F), - modifier = Modifier.navigationBarsWithImePadding(), sheetContent = { GetImageBottomSheet( chosenImage, @@ -89,7 +88,6 @@ fun UserProfileLayout( displayName.value == profile.displayName && fullName.value == profile.fullName && profile.image == profileImage.value - val closeWithAlert = { if (dataUnchanged || !canSaveProfile(displayName.value, profile)) { close() @@ -98,12 +96,11 @@ fun UserProfileLayout( } } ModalView(close = closeWithAlert) { - Column( + ColumnWithScrollBar( Modifier - .verticalScroll(scrollState) .padding(horizontal = DEFAULT_PADDING), ) { - AppBarTitle(stringResource(MR.strings.your_current_profile)) + AppBarTitle(stringResource(MR.strings.your_current_profile), withPadding = false) ReadableText(generalGetString(MR.strings.your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it), TextAlign.Center) Column( Modifier @@ -170,7 +167,6 @@ fun UserProfileLayout( } } } - } } @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt index 390f1cac9b..d7ddb6b950 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt @@ -4,10 +4,10 @@ import SectionBottomSpacer import SectionDivider import SectionItemView import SectionItemViewSpaceBetween +import SectionItemViewWithoutMinPadding import SectionSpacer import SectionTextFooter import SectionView -import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -27,15 +27,16 @@ import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.chatlist.UserProfilePickerItem import chat.simplex.common.views.chatlist.UserProfileRow -import chat.simplex.common.views.database.PassphraseField import chat.simplex.common.views.helpers.* import chat.simplex.common.views.CreateProfile +import chat.simplex.common.views.database.* +import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource -import kotlinx.coroutines.delay +import kotlinx.coroutines.* @Composable -fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden: MutableState<Boolean>) { +fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden: MutableState<Boolean>, withAuth: (block: () -> Unit) -> Unit) { val searchTextOrPassword = rememberSaveable { search } val users by remember { derivedStateOf { m.users.map { it.user } } } val filteredUsers by remember { derivedStateOf { filteredUsers(m, searchTextOrPassword.value) } } @@ -47,8 +48,10 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden: showHiddenProfilesNotice = m.controller.appPrefs.showHiddenProfilesNotice, visibleUsersCount = visibleUsersCount(m), addUser = { - ModalManager.center.showModalCloseable { close -> - CreateProfile(m, close) + withAuth { + ModalManager.center.showModalCloseable { close -> + CreateProfile(m, close) + } } }, activateUser = { user -> @@ -63,7 +66,7 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden: } }, removeUser = { user -> - if (m.users.size > 1 && (user.hidden || visibleUsersCount(m) > 1)) { + withAuth { val text = buildAnnotatedString { append(generalGetString(MR.strings.users_delete_all_chats_deleted) + "\n\n" + generalGetString(MR.strings.users_delete_profile_for) + " ") withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { @@ -92,50 +95,49 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden: } } ) - } else { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.cant_delete_user_profile), - text = if (m.users.size > 1) { - generalGetString(MR.strings.should_be_at_least_one_visible_profile) - } else { - generalGetString(MR.strings.should_be_at_least_one_profile) - } - ) } }, unhideUser = { user -> - if (passwordEntryRequired(user, searchTextOrPassword.value)) { - ModalManager.start.showModalCloseable(true) { close -> - ProfileActionView(UserProfileAction.UNHIDE, user) { pwd -> - withBGApi { - setUserPrivacy(m) { m.controller.apiUnhideUser(user, pwd) } - close() + withAuth { + if (passwordEntryRequired(user, searchTextOrPassword.value)) { + ModalManager.start.showModalCloseable(true) { close -> + ProfileActionView(UserProfileAction.UNHIDE, user) { pwd -> + withBGApi { + setUserPrivacy(m) { m.controller.apiUnhideUser(user, pwd) } + close() + } } } + } else { + withBGApi { setUserPrivacy(m) { m.controller.apiUnhideUser(user, searchTextOrPassword.value.trim()) } } } - } else { - withBGApi { setUserPrivacy(m) { m.controller.apiUnhideUser(user, searchTextOrPassword.value.trim()) } } } }, muteUser = { user -> - withBGApi { - setUserPrivacy(m, onSuccess = { - if (m.controller.appPrefs.showMuteProfileAlert.get()) showMuteProfileAlert(m.controller.appPrefs.showMuteProfileAlert) - }) { m.controller.apiMuteUser(user) } + withAuth { + withBGApi { + setUserPrivacy(m, onSuccess = { + if (m.controller.appPrefs.showMuteProfileAlert.get()) showMuteProfileAlert(m.controller.appPrefs.showMuteProfileAlert) + }) { m.controller.apiMuteUser(user) } + } } }, unmuteUser = { user -> - withBGApi { setUserPrivacy(m) { m.controller.apiUnmuteUser(user) } } + withAuth { + withBGApi { setUserPrivacy(m) { m.controller.apiUnmuteUser(user) } } + } }, showHiddenProfile = { user -> - ModalManager.start.showModalCloseable(true) { close -> - HiddenProfileView(m, user) { - profileHidden.value = true - withBGApi { - delay(10_000) - profileHidden.value = false + withAuth { + ModalManager.start.showModalCloseable(true) { close -> + HiddenProfileView(m, user) { + profileHidden.value = true + withBGApi { + delay(10_000) + profileHidden.value = false + } + close() } - close() } } } @@ -148,7 +150,7 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden: @Composable private fun UserProfilesLayout( users: List<User>, - filteredUsers: List<User>, + filteredUsers: List<UserInfo>, searchTextOrPassword: MutableState<String>, profileHidden: MutableState<Boolean>, visibleUsersCount: Int, @@ -161,11 +163,7 @@ private fun UserProfilesLayout( unmuteUser: (User) -> Unit, showHiddenProfile: (User) -> Unit, ) { - Column( - Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - ) { + ColumnWithScrollBar { if (profileHidden.value) { SectionView { SettingsActionItem(painterResource(MR.images.ic_lock_open_right), stringResource(MR.strings.enter_password_to_show), click = { @@ -178,7 +176,7 @@ private fun UserProfilesLayout( SectionView { for (user in filteredUsers) { - UserView(user, users, visibleUsersCount, activateUser, removeUser, unhideUser, muteUser, unmuteUser, showHiddenProfile) + UserView(user, visibleUsersCount, activateUser, removeUser, unhideUser, muteUser, unmuteUser, showHiddenProfile) SectionDivider() } if (searchTextOrPassword.value.trim().isEmpty()) { @@ -209,8 +207,7 @@ private fun UserProfilesLayout( @Composable private fun UserView( - user: User, - users: List<User>, + userInfo: UserInfo, visibleUsersCount: Int, activateUser: (User) -> Unit, removeUser: (User) -> Unit, @@ -220,7 +217,8 @@ private fun UserView( showHiddenProfile: (User) -> Unit, ) { val showMenu = remember { mutableStateOf(false) } - UserProfilePickerItem(user, onLongClick = { if (users.size > 1) showMenu.value = true }) { + val user = userInfo.user + UserProfilePickerItem(user, onLongClick = { showMenu.value = true }, unreadCount = userInfo.unreadCount) { activateUser(user) } Box(Modifier.padding(horizontal = DEFAULT_PADDING)) { @@ -264,18 +262,14 @@ enum class UserProfileAction { @Composable private fun ProfileActionView(action: UserProfileAction, user: User, doAction: (String) -> Unit) { - Column( - Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - ) { + ColumnWithScrollBar { val actionPassword = rememberSaveable { mutableStateOf("") } val passwordValid by remember { derivedStateOf { actionPassword.value == actionPassword.value.trim() } } val actionEnabled by remember { derivedStateOf { actionPassword.value != "" && passwordValid && correctPassword(user, actionPassword.value) } } @Composable fun ActionHeader(title: StringResource) { AppBarTitle(stringResource(title)) - SectionView(padding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) { + SectionView(contentPadding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) { UserProfileRow(user) } SectionSpacer() @@ -283,7 +277,7 @@ private fun ProfileActionView(action: UserProfileAction, user: User, doAction: ( @Composable fun PasswordAndAction(label: StringResource, color: Color = MaterialTheme.colors.primary) { SectionView() { - SectionItemView { + SectionItemViewWithoutMinPadding { PassphraseField(actionPassword, generalGetString(MR.strings.profile_password), isValid = { passwordValid }, showStrength = true) } SectionItemViewSpaceBetween({ doAction(actionPassword.value) }, disabled = !actionEnabled, minHeight = TextFieldDefaults.MinHeight) { @@ -309,21 +303,21 @@ private fun ProfileActionView(action: UserProfileAction, user: User, doAction: ( } } -private fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List<User> { +fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List<UserInfo> { val s = searchTextOrPassword.trim() val lower = s.lowercase() return m.users.filter { u -> - if ((u.user.activeUser || !u.user.hidden) && (s == "" || u.user.chatViewName.lowercase().contains(lower))) { + if ((u.user.activeUser || !u.user.hidden) && (s == "" || u.user.anyNameContains(lower))) { true } else { correctPassword(u.user, s) } - }.map { it.user } + } } private fun visibleUsersCount(m: ChatModel): Int = m.users.filter { u -> !u.user.hidden }.size -private fun correctPassword(user: User, pwd: String): Boolean { +fun correctPassword(user: User, pwd: String): Boolean { val ph = user.viewPwdHash return ph != null && pwd != "" && chatPasswordHash(pwd, ph.salt) == ph.hash } @@ -350,22 +344,32 @@ private fun removeUser(m: ChatModel, user: User, users: List<User>, delSMPQueues } private suspend fun doRemoveUser(m: ChatModel, user: User, users: List<User>, delSMPQueues: Boolean, viewPwd: String?) { - if (users.size < 2) return - - suspend fun deleteUser(user: User) { - m.controller.apiDeleteUser(user, delSMPQueues, viewPwd) - m.removeUser(user) - } try { - if (user.activeUser) { - val newActive = users.firstOrNull { u -> !u.activeUser && !u.hidden } - if (newActive != null) { - m.controller.changeActiveUser_(newActive.remoteHostId, newActive.userId, null) - deleteUser(user.copy(activeUser = false)) + when { + user.activeUser -> { + removeWallpaperFilesFromAllChats(user) + val newActive = users.firstOrNull { u -> !u.activeUser && !u.hidden } + if (newActive != null) { + m.controller.changeActiveUser_(user.remoteHostId, newActive.userId, null) + m.controller.apiDeleteUser(user, delSMPQueues, viewPwd) + } else { + // Deleting the last visible user while having hidden one(s) + m.controller.apiDeleteUser(user, delSMPQueues, viewPwd) + m.controller.changeActiveUser_(user.remoteHostId, null, null) + if (appPlatform.isAndroid) { + m.controller.apiStopChat() + controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + ModalManager.closeAllModalsEverywhere() + } + } + } + else -> { + m.controller.apiDeleteUser(user, delSMPQueues, viewPwd) } - } else { - deleteUser(user) } + removeWallpaperFilesFromTheme(user.uiThemes) + m.removeUser(user) + ntfManager.cancelNotificationsForUser(user.userId) } catch (e: Exception) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_deleting_user), e.stackTraceToString()) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt index 06a4762210..52addd146b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt @@ -1,6 +1,5 @@ package chat.simplex.common.views.usersettings -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -8,6 +7,7 @@ import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.BuildConfigCommon import chat.simplex.common.model.CoreVersionInfo +import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.appPlatform import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.AppBarTitle @@ -15,7 +15,7 @@ import chat.simplex.res.MR @Composable fun VersionInfoView(info: CoreVersionInfo) { - Column( + ColumnWithScrollBar( Modifier.padding(horizontal = DEFAULT_PADDING), ) { AppBarTitle(stringResource(MR.strings.app_version_title), withPadding = false) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt new file mode 100644 index 0000000000..0c38b0c045 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt @@ -0,0 +1,578 @@ +package chat.simplex.common.views.usersettings.networkAndServers + +import SectionBottomSpacer +import SectionDividerSpaced +import SectionItemView +import SectionItemWithValue +import SectionTextFooter +import SectionView +import SectionViewSelectableCards +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.chatModel +import chat.simplex.common.views.usersettings.PreferenceToggle +import chat.simplex.common.views.usersettings.SettingsPreferenceItem +import chat.simplex.res.MR +import java.text.DecimalFormat + +@Composable +fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> Unit, close: () -> Unit) { + val currentRemoteHost by remember { chatModel.currentRemoteHost } + val developerTools = remember { appPrefs.developerTools.get() } + + // Will be actual once the screen is re-opened + val savedCfg = remember { mutableStateOf(controller.getNetCfg()) } + // Will have an edited state when the screen is re-opened + val currentCfg = remember { stateGetOrPut("currentCfg") { controller.getNetCfg() } } + val currentCfgVal = currentCfg.value // used only on initialization + + val sessionMode = remember { mutableStateOf(currentCfgVal.sessionMode) } + val smpProxyMode = remember { mutableStateOf(currentCfgVal.smpProxyMode) } + val smpProxyFallback = remember { mutableStateOf(currentCfgVal.smpProxyFallback) } + val smpWebPortServers = remember { mutableStateOf(currentCfgVal.smpWebPortServers) } + + val networkTCPConnectTimeout = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout) } + val networkTCPTimeout = remember { mutableStateOf(currentCfgVal.tcpTimeout) } + val networkTCPTimeoutPerKb = remember { mutableStateOf(currentCfgVal.tcpTimeoutPerKb) } + val networkRcvConcurrency = remember { mutableStateOf(currentCfgVal.rcvConcurrency) } + val networkSMPPingInterval = remember { mutableStateOf(currentCfgVal.smpPingInterval) } + val networkSMPPingCount = remember { mutableStateOf(currentCfgVal.smpPingCount) } + val networkEnableKeepAlive = remember { mutableStateOf(currentCfgVal.enableKeepAlive) } + val networkTCPKeepIdle: MutableState<Int> + val networkTCPKeepIntvl: MutableState<Int> + val networkTCPKeepCnt: MutableState<Int> + if (currentCfgVal.tcpKeepAlive != null) { + networkTCPKeepIdle = remember { mutableStateOf(currentCfgVal.tcpKeepAlive.keepIdle) } + networkTCPKeepIntvl = remember { mutableStateOf(currentCfgVal.tcpKeepAlive.keepIntvl) } + networkTCPKeepCnt = remember { mutableStateOf(currentCfgVal.tcpKeepAlive.keepCnt) } + } else { + networkTCPKeepIdle = remember { mutableStateOf(KeepAliveOpts.defaults.keepIdle) } + networkTCPKeepIntvl = remember { mutableStateOf(KeepAliveOpts.defaults.keepIntvl) } + networkTCPKeepCnt = remember { mutableStateOf(KeepAliveOpts.defaults.keepCnt) } + } + + fun buildCfg(): NetCfg { + val enableKeepAlive = networkEnableKeepAlive.value + val tcpKeepAlive = if (enableKeepAlive) { + val keepIdle = networkTCPKeepIdle.value + val keepIntvl = networkTCPKeepIntvl.value + val keepCnt = networkTCPKeepCnt.value + KeepAliveOpts(keepIdle = keepIdle, keepIntvl = keepIntvl, keepCnt = keepCnt) + } else { + null + } + return NetCfg( + socksProxy = currentCfg.value.socksProxy, +// hostMode = currentCfg.value.hostMode, +// requiredHostMode = currentCfg.value.requiredHostMode, + sessionMode = sessionMode.value, + smpProxyMode = smpProxyMode.value, + smpProxyFallback = smpProxyFallback.value, + smpWebPortServers = smpWebPortServers.value, + tcpConnectTimeout = networkTCPConnectTimeout.value, + tcpTimeout = networkTCPTimeout.value, + tcpTimeoutPerKb = networkTCPTimeoutPerKb.value, + rcvConcurrency = networkRcvConcurrency.value, + tcpKeepAlive = tcpKeepAlive, + smpPingInterval = networkSMPPingInterval.value, + smpPingCount = networkSMPPingCount.value + ).withOnionHosts(currentCfg.value.onionHosts) + } + + fun updateView(cfg: NetCfg) { + sessionMode.value = cfg.sessionMode + smpProxyMode.value = cfg.smpProxyMode + smpProxyFallback.value = cfg.smpProxyFallback + smpWebPortServers.value = cfg.smpWebPortServers + networkTCPConnectTimeout.value = cfg.tcpConnectTimeout + networkTCPTimeout.value = cfg.tcpTimeout + networkTCPTimeoutPerKb.value = cfg.tcpTimeoutPerKb + networkRcvConcurrency.value = cfg.rcvConcurrency + networkSMPPingInterval.value = cfg.smpPingInterval + networkSMPPingCount.value = cfg.smpPingCount + networkEnableKeepAlive.value = cfg.enableKeepAlive + if (cfg.tcpKeepAlive != null) { + networkTCPKeepIdle.value = cfg.tcpKeepAlive.keepIdle + networkTCPKeepIntvl.value = cfg.tcpKeepAlive.keepIntvl + networkTCPKeepCnt.value = cfg.tcpKeepAlive.keepCnt + } else { + networkTCPKeepIdle.value = KeepAliveOpts.defaults.keepIdle + networkTCPKeepIntvl.value = KeepAliveOpts.defaults.keepIntvl + networkTCPKeepCnt.value = KeepAliveOpts.defaults.keepCnt + } + } + + fun saveCfg(cfg: NetCfg, close: (() -> Unit)? = null) { + withBGApi { + if (chatModel.controller.apiSetNetworkConfig(cfg)) { + currentCfg.value = cfg + savedCfg.value = cfg + chatModel.controller.setNetCfg(cfg) + close?.invoke() + } + } + } + + fun reset() { + val newCfg = if (currentCfg.value.useSocksProxy) NetCfg.proxyDefaults else NetCfg.defaults + updateView(newCfg) + currentCfg.value = newCfg + } + + val saveDisabled = buildCfg() == savedCfg.value + + ModalView( + close = { + if (saveDisabled) { + close() + } else { + showUnsavedChangesAlert({ + saveCfg(buildCfg(), close) + }, close) + } + }, + ) { + AdvancedNetworkSettingsLayout( + currentRemoteHost = currentRemoteHost, + developerTools = developerTools, + sessionMode = sessionMode, + smpProxyMode = smpProxyMode, + smpProxyFallback = smpProxyFallback, + smpWebPortServers, + networkTCPConnectTimeout, + networkTCPTimeout, + networkTCPTimeoutPerKb, + networkRcvConcurrency, + networkSMPPingInterval, + networkSMPPingCount, + networkEnableKeepAlive, + networkTCPKeepIdle, + networkTCPKeepIntvl, + networkTCPKeepCnt, + updateSessionMode = { sessionMode.value = it; currentCfg.value = currentCfg.value.copy(sessionMode = it) }, + updateSMPProxyMode = { smpProxyMode.value = it; currentCfg.value = currentCfg.value.copy(smpProxyMode = it) }, + updateSMPProxyFallback = { smpProxyFallback.value = it; currentCfg.value = currentCfg.value.copy(smpProxyFallback = it) }, + showModal = showModal, + resetDisabled = if (currentCfg.value.useSocksProxy) buildCfg() == NetCfg.proxyDefaults else buildCfg() == NetCfg.defaults, + reset = ::reset, + saveDisabled = saveDisabled, + save = { + showUpdateNetworkSettingsDialog { + saveCfg(buildCfg()) + } + } + ) + } +} + +@Composable fun AdvancedNetworkSettingsLayout( + currentRemoteHost: RemoteHostInfo?, + developerTools: Boolean, + sessionMode: MutableState<TransportSessionMode>, + smpProxyMode: MutableState<SMPProxyMode>, + smpProxyFallback: MutableState<SMPProxyFallback>, + smpWebPortServers: MutableState<SMPWebPortServers>, + networkTCPConnectTimeout: MutableState<Long>, + networkTCPTimeout: MutableState<Long>, + networkTCPTimeoutPerKb: MutableState<Long>, + networkRcvConcurrency: MutableState<Int>, + networkSMPPingInterval: MutableState<Long>, + networkSMPPingCount: MutableState<Int>, + networkEnableKeepAlive: MutableState<Boolean>, + networkTCPKeepIdle: MutableState<Int>, + networkTCPKeepIntvl: MutableState<Int>, + networkTCPKeepCnt: MutableState<Int>, + updateSessionMode: (TransportSessionMode) -> Unit, + updateSMPProxyMode: (SMPProxyMode) -> Unit, + updateSMPProxyFallback: (SMPProxyFallback) -> Unit, + showModal: (ModalData.() -> Unit) -> Unit, + resetDisabled: Boolean, + reset: () -> Unit, + saveDisabled: Boolean, + save: () -> Unit +) { + val secondsLabel = stringResource(MR.strings.network_option_seconds_label) + + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.network_settings_title)) + + if (currentRemoteHost == null) { + SectionView(generalGetString(MR.strings.settings_section_title_private_message_routing)) { + SMPProxyModePicker(smpProxyMode, showModal, updateSMPProxyMode) + SMPProxyFallbackPicker(smpProxyFallback, showModal, updateSMPProxyFallback, enabled = remember { derivedStateOf { smpProxyMode.value != SMPProxyMode.Never } }) + SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy) + } + SectionTextFooter(stringResource(MR.strings.private_routing_explanation)) + SectionDividerSpaced(maxTopPadding = true) + + SectionView(stringResource(MR.strings.network_session_mode_transport_isolation).uppercase()) { + SessionModePicker(sessionMode, showModal, updateSessionMode) + } + SectionDividerSpaced() + SectionView(stringResource(MR.strings.network_smp_web_port_section_title).uppercase()) { + ExposedDropDownSettingRow( + stringResource(MR.strings.network_smp_web_port_toggle), + SMPWebPortServers.entries.map { it to stringResource(it.text) }, + smpWebPortServers + ) { smpWebPortServers.value = it } + } + SectionTextFooter( + if (smpWebPortServers.value == SMPWebPortServers.Preset) stringResource(MR.strings.network_smp_web_port_preset_footer) + else String.format(stringResource(MR.strings.network_smp_web_port_footer), if (smpWebPortServers.value == SMPWebPortServers.All) "443" else "5223") + ) + SectionDividerSpaced(maxTopPadding = true) + + SectionView(stringResource(MR.strings.network_option_tcp_connection).uppercase()) { + SectionItemView { + TimeoutSettingRow( + stringResource(MR.strings.network_option_tcp_connection_timeout), networkTCPConnectTimeout, + listOf(10_000000, 15_000000, 20_000000, 30_000000, 45_000000, 60_000000, 90_000000), secondsLabel + ) + } + SectionItemView { + TimeoutSettingRow( + stringResource(MR.strings.network_option_protocol_timeout), networkTCPTimeout, + listOf(5_000000, 7_000000, 10_000000, 15_000000, 20_000_000, 30_000_000), secondsLabel + ) + } + SectionItemView { + // can't be higher than 130ms to avoid overflow on 32bit systems + TimeoutSettingRow( + stringResource(MR.strings.network_option_protocol_timeout_per_kb), networkTCPTimeoutPerKb, + listOf(2_500, 5_000, 10_000, 15_000, 20_000, 30_000), secondsLabel + ) + } + // SectionItemView { + // IntSettingRow( + // stringResource(MR.strings.network_option_rcv_concurrency), networkRcvConcurrency, + // listOf(1, 2, 4, 8, 12, 16, 24), "" + // ) + // } + SectionItemView { + TimeoutSettingRow( + stringResource(MR.strings.network_option_ping_interval), networkSMPPingInterval, + listOf(120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000), secondsLabel + ) + } + SectionItemView { + IntSettingRow( + stringResource(MR.strings.network_option_ping_count), networkSMPPingCount, + listOf(1, 2, 3, 5, 8), "" + ) + } + SectionItemView { + EnableKeepAliveSwitch(networkEnableKeepAlive) + } + if (networkEnableKeepAlive.value) { + SectionItemView { + IntSettingRow("TCP_KEEPIDLE", networkTCPKeepIdle, listOf(15, 30, 60, 120, 180), secondsLabel) + } + SectionItemView { + IntSettingRow("TCP_KEEPINTVL", networkTCPKeepIntvl, listOf(5, 10, 15, 30, 60), secondsLabel) + } + SectionItemView { + IntSettingRow("TCP_KEEPCNT", networkTCPKeepCnt, listOf(1, 2, 4, 6, 8), "") + } + } else { + SectionItemView { + Text("TCP_KEEPIDLE", color = MaterialTheme.colors.secondary) + } + SectionItemView { + Text("TCP_KEEPINTVL", color = MaterialTheme.colors.secondary) + } + SectionItemView { + Text("TCP_KEEPCNT", color = MaterialTheme.colors.secondary) + } + } + } + } + + SectionDividerSpaced(maxBottomPadding = false) + + SectionView { + SectionItemView(reset, disabled = resetDisabled) { + Text(stringResource(MR.strings.network_options_reset_to_defaults), color = if (resetDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + } + SectionItemView(save, disabled = saveDisabled) { + Text(stringResource(MR.strings.network_options_save_and_reconnect), color = if (saveDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + } + } + SectionBottomSpacer() + } +} + +@Composable +private fun SMPProxyModePicker( + smpProxyMode: MutableState<SMPProxyMode>, + showModal: (@Composable ModalData.() -> Unit) -> Unit, + updateSMPProxyMode: (SMPProxyMode) -> Unit, +) { + val density = LocalDensity.current + val values = remember { + SMPProxyMode.entries.map { + when (it) { + SMPProxyMode.Always -> ValueTitleDesc(SMPProxyMode.Always, generalGetString(MR.strings.network_smp_proxy_mode_always), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_always_description), density)) + SMPProxyMode.Unknown -> ValueTitleDesc(SMPProxyMode.Unknown, generalGetString(MR.strings.network_smp_proxy_mode_unknown), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_unknown_description), density)) + SMPProxyMode.Unprotected -> ValueTitleDesc(SMPProxyMode.Unprotected, generalGetString(MR.strings.network_smp_proxy_mode_unprotected), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_unprotected_description), density)) + SMPProxyMode.Never -> ValueTitleDesc(SMPProxyMode.Never, generalGetString(MR.strings.network_smp_proxy_mode_never), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_never_description), density)) + } + } + } + + SectionItemWithValue( + generalGetString(MR.strings.network_smp_proxy_mode_private_routing), + smpProxyMode, + values, + icon = painterResource(MR.images.ic_settings_ethernet), + onSelected = { + showModal { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.network_smp_proxy_mode_private_routing)) + SectionViewSelectableCards(null, smpProxyMode, values, updateSMPProxyMode) + } + } + } + ) +} + +@Composable +private fun SMPProxyFallbackPicker( + smpProxyFallback: MutableState<SMPProxyFallback>, + showModal: (@Composable ModalData.() -> Unit) -> Unit, + updateSMPProxyFallback: (SMPProxyFallback) -> Unit, + enabled: State<Boolean>, +) { + val density = LocalDensity.current + val values = remember { + SMPProxyFallback.entries.map { + when (it) { + SMPProxyFallback.Allow -> ValueTitleDesc(SMPProxyFallback.Allow, generalGetString(MR.strings.network_smp_proxy_fallback_allow), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_allow_description), density)) + SMPProxyFallback.AllowProtected -> ValueTitleDesc(SMPProxyFallback.AllowProtected, generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected_description), density)) + SMPProxyFallback.Prohibit -> ValueTitleDesc(SMPProxyFallback.Prohibit, generalGetString(MR.strings.network_smp_proxy_fallback_prohibit), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_prohibit_description), density)) + } + } + } + + SectionItemWithValue( + generalGetString(MR.strings.network_smp_proxy_fallback_allow_downgrade), + smpProxyFallback, + values, + icon = painterResource(MR.images.ic_arrows_left_right), + enabled = enabled, + onSelected = { + showModal { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.network_smp_proxy_fallback_allow_downgrade)) + SectionViewSelectableCards(null, smpProxyFallback, values, updateSMPProxyFallback) + } + } + } + ) +} + +@Composable +fun EnableKeepAliveSwitch( + networkEnableKeepAlive: MutableState<Boolean> +) { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(stringResource(MR.strings.network_option_enable_tcp_keep_alive)) + DefaultSwitch( + checked = networkEnableKeepAlive.value, + onCheckedChange = { networkEnableKeepAlive.value = it }, + ) + } +} + +@Composable +fun IntSettingRow(title: String, selection: MutableState<Int>, values: List<Int>, label: String) { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + val expanded = rememberSaveable { mutableStateOf(false) } + + Text(title) + + ExposedDropdownMenuBox( + expanded = expanded.value, + onExpandedChange = { + expanded.value = !expanded.value + } + ) { + Row( + Modifier.width(140.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + Text( + "${selection.value} $label", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colors.secondary + ) + Spacer(Modifier.size(4.dp)) + Icon( + if (!expanded.value) painterResource(MR.images.ic_arrow_drop_down) else painterResource(MR.images.ic_arrow_drop_up), + contentDescription = null, + modifier = Modifier.padding(start = 8.dp), + tint = MaterialTheme.colors.secondary + ) + } + DefaultExposedDropdownMenu( + expanded = expanded, + ) { + values.forEach { selectionOption -> + DropdownMenuItem( + onClick = { + selection.value = selectionOption + expanded.value = false + }, + contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f) + ) { + Text( + "$selectionOption $label", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } + } +} + +@Composable +fun TimeoutSettingRow(title: String, selection: MutableState<Long>, values: List<Long>, label: String) { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + val expanded = remember { mutableStateOf(false) } + + Text(title) + + ExposedDropdownMenuBox( + expanded = expanded.value, + onExpandedChange = { + expanded.value = !expanded.value + } + ) { + val df = DecimalFormat("#.###") + Row( + Modifier.width(140.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + Text( + "${df.format(selection.value / 1_000_000.0)} $label", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colors.secondary + ) + Spacer(Modifier.size(4.dp)) + Icon( + if (!expanded.value) painterResource(MR.images.ic_arrow_drop_down) else painterResource(MR.images.ic_arrow_drop_up), + contentDescription = null, + modifier = Modifier.padding(start = 8.dp), + tint = MaterialTheme.colors.secondary + ) + } + DefaultExposedDropdownMenu( + expanded = expanded + ) { + val v = selection.value + val vs = if (values.contains(v)) values else values + v + vs.forEach { selectionOption -> + DropdownMenuItem( + onClick = { + selection.value = selectionOption + expanded.value = false + }, + contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f) + ) { + Text( + "${df.format(selectionOption / 1_000_000.0)} $label", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } + } +} + +fun showUpdateNetworkSettingsDialog(action: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.update_network_settings_question), + text = generalGetString(MR.strings.updating_settings_will_reconnect_client_to_all_servers), + confirmText = generalGetString(MR.strings.update_network_settings_confirmation), + onConfirm = action + ) +} + +private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { + AlertManager.shared.showAlertDialogStacked( + title = generalGetString(MR.strings.update_network_settings_question), + confirmText = generalGetString(MR.strings.network_options_save_and_reconnect), + dismissText = generalGetString(MR.strings.exit_without_saving), + onConfirm = save, + onDismiss = revert, + ) +} + +@Preview +@Composable +fun PreviewAdvancedNetworkSettingsLayout() { + SimpleXTheme { + AdvancedNetworkSettingsLayout( + currentRemoteHost = null, + developerTools = false, + sessionMode = remember { mutableStateOf(TransportSessionMode.User) }, + smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) }, + smpProxyFallback = remember { mutableStateOf(SMPProxyFallback.Allow) }, + smpWebPortServers = remember { mutableStateOf(SMPWebPortServers.Preset) }, + networkTCPConnectTimeout = remember { mutableStateOf(10_000000) }, + networkTCPTimeout = remember { mutableStateOf(10_000000) }, + networkTCPTimeoutPerKb = remember { mutableStateOf(10_000) }, + networkRcvConcurrency = remember { mutableStateOf(8) }, + networkSMPPingInterval = remember { mutableStateOf(10_000000) }, + networkSMPPingCount = remember { mutableStateOf(3) }, + networkEnableKeepAlive = remember { mutableStateOf(true) }, + networkTCPKeepIdle = remember { mutableStateOf(10) }, + networkTCPKeepIntvl = remember { mutableStateOf(10) }, + networkTCPKeepCnt = remember { mutableStateOf(10) }, + updateSessionMode = {}, + updateSMPProxyMode = {}, + updateSMPProxyFallback = {}, + showModal = {}, + resetDisabled = false, + reset = {}, + saveDisabled = false, + save = {} + ) + } +} 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 new file mode 100644 index 0000000000..98f671ddc4 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt @@ -0,0 +1,993 @@ +package chat.simplex.common.views.usersettings.networkAndServers + +import SectionBottomSpacer +import SectionCustomFooter +import SectionDividerSpaced +import SectionItemView +import SectionItemWithValue +import SectionTextFooter +import SectionView +import SectionViewSelectable +import TextIconSpaced +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardActions +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 dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.text.* +import androidx.compose.ui.text.input.* +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.* +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.* +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ChatController.getServerOperators +import chat.simplex.common.model.ChatController.getUserServers +import chat.simplex.common.model.ChatController.setUserServers +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.onboarding.OnboardingActionButton +import chat.simplex.common.views.onboarding.ReadableText +import chat.simplex.common.views.usersettings.* +import chat.simplex.res.MR +import kotlinx.coroutines.* + +@Composable +fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) { + val currentRemoteHost by remember { chatModel.currentRemoteHost } + // It's not a state, just a one-time value. Shouldn't be used in any state-related situations + val netCfg = remember { chatModel.controller.getNetCfg() } + val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.useSocksProxy) } + val currUserServers = remember { stateGetOrPut("currUserServers") { emptyList<UserOperatorServers>() } } + val userServers = remember { stateGetOrPut("userServers") { emptyList<UserOperatorServers>() } } + val serverErrors = remember { stateGetOrPut("serverErrors") { emptyList<UserServersError>() } } + + val proxyPort = remember { derivedStateOf { appPrefs.networkProxy.state.value.port } } + fun onClose(close: () -> Unit): Boolean = if (!serversCanBeSaved(currUserServers.value, userServers.value, serverErrors.value)) { + chatModel.centerPanelBackgroundClickHandler = null + close() + false + } else { + showUnsavedChangesAlert( + { + CoroutineScope(Dispatchers.Default).launch { + saveServers(currentRemoteHost?.remoteHostId, currUserServers, userServers) + chatModel.centerPanelBackgroundClickHandler = null + close() + } + }, + { + chatModel.centerPanelBackgroundClickHandler = null + close() + } + ) + true + } + + LaunchedEffect(Unit) { + // Enables unsaved changes alert on this view and all children views. + chatModel.centerPanelBackgroundClickHandler = { + onClose(close = { ModalManager.start.closeModals() }) + } + } + ModalView(close = { onClose(closeNetworkAndServers) }) { + NetworkAndServersLayout( + currentRemoteHost = currentRemoteHost, + networkUseSocksProxy = networkUseSocksProxy, + onionHosts = remember { mutableStateOf(netCfg.onionHosts) }, + currUserServers = currUserServers, + userServers = userServers, + serverErrors = serverErrors, + toggleSocksProxy = { enable -> + val def = NetCfg.defaults + val proxyDef = NetCfg.proxyDefaults + if (enable) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.network_enable_socks), + text = generalGetString(MR.strings.network_enable_socks_info).format(proxyPort.value), + confirmText = generalGetString(MR.strings.confirm_verb), + onConfirm = { + withBGApi { + var conf = controller.getNetCfg().withProxy(controller.appPrefs.networkProxy.get()) + if (conf.tcpConnectTimeout == def.tcpConnectTimeout) { + conf = conf.copy(tcpConnectTimeout = proxyDef.tcpConnectTimeout) + } + if (conf.tcpTimeout == def.tcpTimeout) { + conf = conf.copy(tcpTimeout = proxyDef.tcpTimeout) + } + if (conf.tcpTimeoutPerKb == def.tcpTimeoutPerKb) { + conf = conf.copy(tcpTimeoutPerKb = proxyDef.tcpTimeoutPerKb) + } + if (conf.rcvConcurrency == def.rcvConcurrency) { + conf = conf.copy(rcvConcurrency = proxyDef.rcvConcurrency) + } + chatModel.controller.apiSetNetworkConfig(conf) + chatModel.controller.setNetCfg(conf) + networkUseSocksProxy.value = true + } + } + ) + } else { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.network_disable_socks), + text = generalGetString(MR.strings.network_disable_socks_info), + confirmText = generalGetString(MR.strings.confirm_verb), + onConfirm = { + withBGApi { + var conf = controller.getNetCfg().copy(socksProxy = null) + if (conf.tcpConnectTimeout == proxyDef.tcpConnectTimeout) { + conf = conf.copy(tcpConnectTimeout = def.tcpConnectTimeout) + } + if (conf.tcpTimeout == proxyDef.tcpTimeout) { + conf = conf.copy(tcpTimeout = def.tcpTimeout) + } + if (conf.tcpTimeoutPerKb == proxyDef.tcpTimeoutPerKb) { + conf = conf.copy(tcpTimeoutPerKb = def.tcpTimeoutPerKb) + } + if (conf.rcvConcurrency == proxyDef.rcvConcurrency) { + conf = conf.copy(rcvConcurrency = def.rcvConcurrency) + } + chatModel.controller.apiSetNetworkConfig(conf) + chatModel.controller.setNetCfg(conf) + networkUseSocksProxy.value = false + } + } + ) + } + } + ) + } +} + +@Composable fun NetworkAndServersLayout( + currentRemoteHost: RemoteHostInfo?, + networkUseSocksProxy: MutableState<Boolean>, + onionHosts: MutableState<OnionHosts>, + currUserServers: MutableState<List<UserOperatorServers>>, + serverErrors: MutableState<List<UserServersError>>, + userServers: MutableState<List<UserOperatorServers>>, + toggleSocksProxy: (Boolean) -> Unit, +) { + val m = chatModel + val conditionsAction = remember { m.conditions.value.conditionsAction } + val anyOperatorEnabled = remember { derivedStateOf { userServers.value.any { it.operator?.enabled == true } } } + val scope = rememberCoroutineScope() + + LaunchedEffect(Unit) { + if (currUserServers.value.isNotEmpty() || userServers.value.isNotEmpty()) { + return@LaunchedEffect + } + try { + val servers = getUserServers(rh = currentRemoteHost?.remoteHostId) + if (servers != null) { + currUserServers.value = servers + userServers.value = servers + } + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + } + } + + @Composable + fun ConditionsButton(conditionsAction: UsageConditionsAction, rhId: Long?) { + SectionItemView( + click = { ModalManager.start.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> + UsageConditionsView( + currUserServers, + userServers, + close, + rhId + ) + } }, + ) { + Text( + stringResource(if (conditionsAction is UsageConditionsAction.Review) MR.strings.operator_review_conditions else MR.strings.operator_conditions_accepted), + color = MaterialTheme.colors.primary + ) + } + } + + ColumnWithScrollBar { + val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) } + val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.start.showCustomModal { close -> it(close) } } + + AppBarTitle(stringResource(MR.strings.network_and_servers)) + // TODO: Review this and socks. + if (!chatModel.desktopNoUserNoRemote) { + SectionView(generalGetString(MR.strings.network_preset_servers_title).uppercase()) { + userServers.value.forEachIndexed { index, srv -> + srv.operator?.let { ServerOperatorRow(index, it, currUserServers, userServers, serverErrors, currentRemoteHost?.remoteHostId) } + } + } + if (conditionsAction != null && anyOperatorEnabled.value) { + ConditionsButton(conditionsAction, rhId = currentRemoteHost?.remoteHostId) + } + val footerText = if (conditionsAction is UsageConditionsAction.Review && conditionsAction.deadline != null && anyOperatorEnabled.value) { + String.format(generalGetString(MR.strings.operator_conditions_will_be_accepted_on), localDate(conditionsAction.deadline)) + } else null + + if (footerText != null) { + SectionTextFooter(footerText) + } + SectionDividerSpaced() + } + + SectionView(generalGetString(MR.strings.settings_section_title_messages)) { + val nullOperatorIndex = userServers.value.indexOfFirst { it.operator == null } + + if (nullOperatorIndex != -1) { + SectionItemView({ + ModalManager.start.showModal { + YourServersView( + userServers = userServers, + serverErrors = serverErrors, + operatorIndex = nullOperatorIndex, + rhId = currentRemoteHost?.remoteHostId + ) + } + }) { + Icon( + painterResource(MR.images.ic_dns), + stringResource(MR.strings.your_servers), + tint = MaterialTheme.colors.secondary + ) + TextIconSpaced() + Text(stringResource(MR.strings.your_servers), color = MaterialTheme.colors.onBackground) + + if (currUserServers.value.getOrNull(nullOperatorIndex) != userServers.value.getOrNull(nullOperatorIndex)) { + Spacer(Modifier.weight(1f)) + UnsavedChangesIndicator() + } + } + } + + if (currentRemoteHost == null) { + UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy) + SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, appPrefs.networkProxy, onionHosts, sessionMode = appPrefs.networkSessionMode.get(), false, it) } }) + SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), { ModalManager.start.showCustomModal { AdvancedNetworkSettingsView(showModal, it) } }) + if (networkUseSocksProxy.value) { + SectionTextFooter(annotatedStringResource(MR.strings.socks_proxy_setting_limitations)) + SectionDividerSpaced(maxTopPadding = true) + } else { + SectionDividerSpaced(maxBottomPadding = false) + } + } + } + val saveDisabled = !serversCanBeSaved(currUserServers.value, userServers.value, serverErrors.value) + + SectionItemView( + { scope.launch { saveServers(rhId = currentRemoteHost?.remoteHostId, currUserServers, userServers) } }, + disabled = saveDisabled, + ) { + Text(stringResource(MR.strings.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) + } + val serversErr = globalServersError(serverErrors.value) + if (serversErr != null) { + SectionCustomFooter { + ServersErrorFooter(serversErr) + } + } else if (serverErrors.value.isNotEmpty()) { + SectionCustomFooter { + ServersErrorFooter(generalGetString(MR.strings.errors_in_servers_configuration)) + } + } + + SectionDividerSpaced() + + SectionView(generalGetString(MR.strings.settings_section_title_calls)) { + SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } }) + } + + if (appPlatform.isAndroid) { + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.settings_section_title_network_connection).uppercase()) { + val info = remember { chatModel.networkInfo }.value + SettingsActionItemWithContent(icon = null, info.networkType.text) { + Icon(painterResource(MR.images.ic_circle_filled), stringResource(MR.strings.icon_descr_server_status_connected), tint = if (info.online) Color.Green else MaterialTheme.colors.error) + } + } + } + SectionBottomSpacer() + } +} + +@Composable fun OnionRelatedLayout( + developerTools: Boolean, + networkUseSocksProxy: MutableState<Boolean>, + onionHosts: MutableState<OnionHosts>, + sessionMode: MutableState<TransportSessionMode>, + networkProxy: SharedPreference<NetworkProxy>, + toggleSocksProxy: (Boolean) -> Unit, + updateSessionMode: (TransportSessionMode) -> Unit, +) { + val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.fullscreen.showModal(content = it) } + val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.fullscreen.showCustomModal { close -> it(close) }} + UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy) + SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, networkProxy, onionHosts, sessionMode.value, true, it) } }) + SessionModePicker(sessionMode, showModal, updateSessionMode) +} + +@Composable +fun UseSocksProxySwitch( + networkUseSocksProxy: MutableState<Boolean>, + toggleSocksProxy: (Boolean) -> Unit, +) { + Row( + Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painterResource(MR.images.ic_settings_ethernet), + stringResource(MR.strings.network_socks_toggle_use_socks_proxy), + tint = MaterialTheme.colors.secondary + ) + TextIconSpaced(false) + Text(generalGetString(MR.strings.network_socks_toggle_use_socks_proxy)) + } + DefaultSwitch( + checked = networkUseSocksProxy.value, + onCheckedChange = toggleSocksProxy, + ) + } +} + +@Composable +fun SocksProxySettings( + networkUseSocksProxy: Boolean, + networkProxy: SharedPreference<NetworkProxy>, + onionHosts: MutableState<OnionHosts>, + sessionMode: TransportSessionMode, + migration: Boolean, + close: () -> Unit +) { + val networkProxySaved by remember { networkProxy.state } + val onionHostsSaved = remember { mutableStateOf(onionHosts.value) } + + val usernameUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue(networkProxySaved.username)) + } + val passwordUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue(networkProxySaved.password)) + } + val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue(networkProxySaved.host)) + } + val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue(networkProxySaved.port.toString())) + } + val proxyAuthRandomUnsaved = rememberSaveable { mutableStateOf(networkProxySaved.auth == NetworkProxyAuth.ISOLATE) } + LaunchedEffect(proxyAuthRandomUnsaved.value) { + if (!proxyAuthRandomUnsaved.value && onionHosts.value != OnionHosts.NEVER) { + onionHosts.value = OnionHosts.NEVER + } + } + val proxyAuthModeUnsaved = remember(proxyAuthRandomUnsaved.value, usernameUnsaved.value.text, passwordUnsaved.value.text) { + derivedStateOf { + if (proxyAuthRandomUnsaved.value) { + NetworkProxyAuth.ISOLATE + } else { + NetworkProxyAuth.USERNAME + } + } + } + + val save: (Boolean) -> Unit = { closeOnSuccess -> + val oldValue = networkProxy.get() + usernameUnsaved.value = usernameUnsaved.value.copy(if (proxyAuthModeUnsaved.value == NetworkProxyAuth.USERNAME) usernameUnsaved.value.text.trim() else "") + passwordUnsaved.value = passwordUnsaved.value.copy(if (proxyAuthModeUnsaved.value == NetworkProxyAuth.USERNAME) passwordUnsaved.value.text.trim() else "") + hostUnsaved.value = hostUnsaved.value.copy(hostUnsaved.value.text.trim()) + portUnsaved.value = portUnsaved.value.copy(portUnsaved.value.text.trim()) + + networkProxy.set( + NetworkProxy( + username = usernameUnsaved.value.text, + password = passwordUnsaved.value.text, + host = hostUnsaved.value.text, + port = portUnsaved.value.text.toIntOrNull() ?: 9050, + auth = proxyAuthModeUnsaved.value + ) + ) + val oldCfg = controller.getNetCfg() + val cfg = oldCfg.withOnionHosts(onionHosts.value) + val oldOnionHosts = onionHostsSaved.value + onionHostsSaved.value = onionHosts.value + + if (!migration) { + controller.setNetCfg(cfg) + } + if (networkUseSocksProxy && !migration) { + withBGApi { + if (controller.apiSetNetworkConfig(cfg, showAlertOnError = false)) { + onionHosts.value = cfg.onionHosts + onionHostsSaved.value = onionHosts.value + if (closeOnSuccess) { + close() + } + } else { + controller.setNetCfg(oldCfg) + networkProxy.set(oldValue) + onionHostsSaved.value = oldOnionHosts + showWrongProxyConfigAlert() + } + } + } + } + val saveDisabled = + ( + networkProxySaved.username == usernameUnsaved.value.text.trim() && + networkProxySaved.password == passwordUnsaved.value.text.trim() && + networkProxySaved.host == hostUnsaved.value.text.trim() && + networkProxySaved.port.toString() == portUnsaved.value.text.trim() && + networkProxySaved.auth == proxyAuthModeUnsaved.value && + onionHosts.value == onionHostsSaved.value + ) || + !validCredential(usernameUnsaved.value.text) || + !validCredential(passwordUnsaved.value.text) || + !validHost(hostUnsaved.value.text) || + !validPort(portUnsaved.value.text) + val resetDisabled = hostUnsaved.value.text.trim() == "localhost" && portUnsaved.value.text.trim() == "9050" && proxyAuthRandomUnsaved.value && onionHosts.value == NetCfg.defaults.onionHosts + ModalView( + close = { + if (saveDisabled) { + close() + } else { + showUnsavedSocksHostPortAlert( + confirmText = generalGetString(if (networkUseSocksProxy && !migration) MR.strings.network_options_save_and_reconnect else MR.strings.network_options_save), + save = { save(true) }, + close = close + ) + } + }, + ) { + ColumnWithScrollBar { + AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings)) + SectionView(stringResource(MR.strings.network_socks_proxy).uppercase()) { + Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { + DefaultConfigurableTextField( + hostUnsaved, + stringResource(MR.strings.host_verb), + modifier = Modifier.fillMaxWidth(), + isValid = ::validHost, + keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), + keyboardType = KeyboardType.Text, + ) + DefaultConfigurableTextField( + portUnsaved, + stringResource(MR.strings.port_verb), + modifier = Modifier.fillMaxWidth(), + isValid = ::validPort, + keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save(false) }), + keyboardType = KeyboardType.Number, + ) + } + + UseOnionHosts(onionHosts, rememberUpdatedState(networkUseSocksProxy && proxyAuthRandomUnsaved.value)) { + onionHosts.value = it + } + SectionTextFooter(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported)) + } + + SectionDividerSpaced(maxTopPadding = true) + + SectionView(stringResource(MR.strings.network_proxy_auth).uppercase()) { + PreferenceToggle( + stringResource(MR.strings.network_proxy_random_credentials), + checked = proxyAuthRandomUnsaved.value, + onChange = { proxyAuthRandomUnsaved.value = it } + ) + if (!proxyAuthRandomUnsaved.value) { + Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { + DefaultConfigurableTextField( + usernameUnsaved, + stringResource(MR.strings.network_proxy_username), + modifier = Modifier.fillMaxWidth(), + isValid = ::validCredential, + keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), + keyboardType = KeyboardType.Text, + ) + DefaultConfigurableTextField( + passwordUnsaved, + stringResource(MR.strings.network_proxy_password), + modifier = Modifier.fillMaxWidth(), + isValid = ::validCredential, + keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), + keyboardType = KeyboardType.Password, + ) + } + } + SectionTextFooter(proxyAuthFooter(usernameUnsaved.value.text, passwordUnsaved.value.text, proxyAuthModeUnsaved.value, sessionMode)) + } + + SectionDividerSpaced(maxBottomPadding = false, maxTopPadding = true) + + SectionView { + SectionItemView({ + hostUnsaved.value = hostUnsaved.value.copy("localhost", TextRange(9)) + portUnsaved.value = portUnsaved.value.copy("9050", TextRange(4)) + usernameUnsaved.value = TextFieldValue() + passwordUnsaved.value = TextFieldValue() + proxyAuthRandomUnsaved.value = true + onionHosts.value = NetCfg.defaults.onionHosts + }, disabled = resetDisabled) { + Text(stringResource(MR.strings.network_options_reset_to_defaults), color = if (resetDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + } + SectionItemView( + click = { if (networkUseSocksProxy && !migration) showUpdateNetworkSettingsDialog { save(false) } else save(false) }, + disabled = saveDisabled + ) { + Text(stringResource(if (networkUseSocksProxy && !migration) MR.strings.network_options_save_and_reconnect else MR.strings.network_options_save), color = if (saveDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + } + } + SectionBottomSpacer() + } + } +} + +private fun proxyAuthFooter(username: String, password: String, auth: NetworkProxyAuth, sessionMode: TransportSessionMode): String = when { + auth == NetworkProxyAuth.ISOLATE -> generalGetString(if (sessionMode == TransportSessionMode.User) MR.strings.network_proxy_auth_mode_isolate_by_auth_user else MR.strings.network_proxy_auth_mode_isolate_by_auth_entity) + username.isBlank() && password.isBlank() -> generalGetString(MR.strings.network_proxy_auth_mode_no_auth) + else -> generalGetString(MR.strings.network_proxy_auth_mode_username_password) +} + +private fun showUnsavedSocksHostPortAlert(confirmText: String, save: () -> Unit, close: () -> Unit) { + AlertManager.shared.showAlertDialogStacked( + title = generalGetString(MR.strings.update_network_settings_question), + confirmText = confirmText, + dismissText = generalGetString(MR.strings.exit_without_saving), + onConfirm = save, + onDismiss = close, + ) +} + +@Composable +fun UseOnionHosts( + onionHosts: MutableState<OnionHosts>, + enabled: State<Boolean>, + useOnion: (OnionHosts) -> Unit, +) { + val values = remember { + OnionHosts.values().map { + when (it) { + OnionHosts.NEVER -> ValueTitleDesc(OnionHosts.NEVER, generalGetString(MR.strings.network_use_onion_hosts_no), AnnotatedString(generalGetString(MR.strings.network_use_onion_hosts_no_desc))) + OnionHosts.PREFER -> ValueTitleDesc(OnionHosts.PREFER, generalGetString(MR.strings.network_use_onion_hosts_prefer), AnnotatedString(generalGetString(MR.strings.network_use_onion_hosts_prefer_desc))) + OnionHosts.REQUIRED -> ValueTitleDesc(OnionHosts.REQUIRED, generalGetString(MR.strings.network_use_onion_hosts_required), AnnotatedString(generalGetString(MR.strings.network_use_onion_hosts_required_desc))) + } + } + } + + Column { + if (enabled.value) { + ExposedDropDownSettingRow( + generalGetString(MR.strings.network_use_onion_hosts), + values.map { it.value to it.title }, + onionHosts, + icon = painterResource(MR.images.ic_security), + enabled = enabled, + onSelected = useOnion + ) + } else { + // In reality, when socks proxy is disabled, this option acts like NEVER regardless of what was chosen before + ExposedDropDownSettingRow( + generalGetString(MR.strings.network_use_onion_hosts), + listOf(OnionHosts.NEVER to generalGetString(MR.strings.network_use_onion_hosts_no)), + remember { mutableStateOf(OnionHosts.NEVER) }, + icon = painterResource(MR.images.ic_security), + enabled = enabled, + onSelected = {} + ) + } + SectionTextFooter(values.first { it.value == onionHosts.value }.description) + } +} + +@Composable +fun SessionModePicker( + sessionMode: MutableState<TransportSessionMode>, + showModal: (@Composable ModalData.() -> Unit) -> Unit, + updateSessionMode: (TransportSessionMode) -> Unit, +) { + val density = LocalDensity.current + val values = remember { + val safeModes = TransportSessionMode.safeValues + val modes: Array<TransportSessionMode> = + if (appPrefs.developerTools.get()) TransportSessionMode.values() + else if (safeModes.contains(sessionMode.value)) safeModes + else safeModes + sessionMode.value + modes.map { + val userModeDescr: AnnotatedString = escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_session_mode_user_description), density) + when (it) { + TransportSessionMode.User -> ValueTitleDesc(TransportSessionMode.User, generalGetString(MR.strings.network_session_mode_user), userModeDescr) + TransportSessionMode.Session -> ValueTitleDesc(TransportSessionMode.Session, generalGetString(MR.strings.network_session_mode_session), userModeDescr + AnnotatedString("\n") + escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_session_mode_session_description), density)) + TransportSessionMode.Server -> ValueTitleDesc(TransportSessionMode.Server, generalGetString(MR.strings.network_session_mode_server), userModeDescr + AnnotatedString("\n") + escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_session_mode_server_description), density)) + TransportSessionMode.Entity -> ValueTitleDesc(TransportSessionMode.Entity, generalGetString(MR.strings.network_session_mode_entity), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_session_mode_entity_description), density)) + } + } + } + + SectionItemWithValue( + generalGetString(MR.strings.network_session_mode_transport_isolation), + sessionMode, + values, + icon = painterResource(MR.images.ic_safety_divider), + onSelected = { + showModal { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.network_session_mode_transport_isolation)) + SectionViewSelectable(null, sessionMode, values, updateSessionMode) + } + } + } + ) +} + +private fun validHost(s: String): Boolean = + !s.contains('@') + +// https://ihateregex.io/expr/port/ +fun validPort(s: String): Boolean { + val validPort = Regex("^(6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4})$") + return s.isNotBlank() && s.matches(validPort) +} + +private fun validCredential(s: String): Boolean = + !s.contains(':') && !s.contains('@') + +fun showWrongProxyConfigAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.network_proxy_incorrect_config_title), + text = generalGetString(MR.strings.network_proxy_incorrect_config_desc), + ) +} + +@Composable() +private fun ServerOperatorRow( + index: Int, + operator: ServerOperator, + currUserServers: MutableState<List<UserOperatorServers>>, + userServers: MutableState<List<UserOperatorServers>>, + serverErrors: MutableState<List<UserServersError>>, + rhId: Long? +) { + SectionItemView( + { + ModalManager.start.showModalCloseable { close -> + OperatorView( + currUserServers, + userServers, + serverErrors, + index, + rhId + ) + } + } + ) { + Image( + painterResource(operator.logo), + operator.tradeName, + modifier = Modifier.size(24.dp), + colorFilter = if (operator.enabled) null else ColorFilter.colorMatrix(ColorMatrix().apply { + setToSaturation(0f) + }) + ) + TextIconSpaced() + Text(operator.tradeName, color = if (operator.enabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) + + if (currUserServers.value.getOrNull(index) != userServers.value.getOrNull(index)) { + Spacer(Modifier.weight(1f)) + UnsavedChangesIndicator() + } + } +} + +@Composable +private fun UnsavedChangesIndicator() { + Icon( + painterResource(MR.images.ic_edit_filled), + stringResource(MR.strings.icon_descr_edited), + tint = MaterialTheme.colors.secondary, + modifier = Modifier.size(16.dp) + ) +} + +@Composable +fun UsageConditionsView( + currUserServers: MutableState<List<UserOperatorServers>>, + userServers: MutableState<List<UserOperatorServers>>, + close: () -> Unit, + rhId: Long? +) { + suspend fun acceptForOperators(rhId: Long?, operatorIds: List<Long>, close: () -> Unit) { + try { + val conditionsId = chatModel.conditions.value.currentConditions.conditionsId + val r = chatController.acceptConditions(rhId, conditionsId, operatorIds) ?: return + chatModel.conditions.value = r + updateOperatorsConditionsAcceptance(currUserServers, r.serverOperators) + updateOperatorsConditionsAcceptance(userServers, r.serverOperators) + close() + } catch (ex: Exception) { + Log.e(TAG, ex.stackTraceToString()) + } + } + + @Composable + fun AcceptConditionsButton(operatorIds: List<Long>, close: () -> Unit, bottomPadding: Dp = DEFAULT_PADDING * 2) { + val scope = rememberCoroutineScope() + Column(Modifier.fillMaxWidth().padding(bottom = bottomPadding), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingActionButton( + labelId = MR.strings.accept_conditions, + onboarding = null, + enabled = operatorIds.isNotEmpty(), + onclick = { + scope.launch { + acceptForOperators(rhId, operatorIds, close) + } + } + ) + } + } + + @Composable + fun ConditionsDiffButton() { + val uriHandler = LocalUriHandler.current + val commit = chatModel.conditions.value.currentConditions.conditionsCommit + Column ( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier + .clip(shape = CircleShape) + .clickable { + val commitUrl = "https://github.com/simplex-chat/simplex-chat/commit/$commit" + uriHandler.openUriCatching(commitUrl) + } + .padding(horizontal = 6.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text(stringResource(MR.strings.operator_open_changes), color = MaterialTheme.colors.primary) + Spacer(Modifier.width(8.dp)) + Icon(painterResource(MR.images.ic_outbound), contentDescription = null, tint = MaterialTheme.colors.primary) + } + } + } + + ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING)) { + when (val conditionsAction = chatModel.conditions.value.conditionsAction) { + is UsageConditionsAction.Review -> { + AppBarTitle(stringResource(MR.strings.operator_updated_conditions), enableAlphaChanges = false, withPadding = false, bottomPadding = DEFAULT_PADDING) + if (conditionsAction.operators.isNotEmpty()) { + ReadableText(MR.strings.operators_conditions_will_be_accepted_for, args = conditionsAction.operators.joinToString(", ") { it.legalName_ }) + } + Column(modifier = Modifier.weight(1f).padding(bottom = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF)) { + ConditionsTextView(rhId) + } + AcceptConditionsButton(conditionsAction.operators.map { it.operatorId }, close, DEFAULT_PADDING_HALF) + if (conditionsAction.deadline != null) { + SectionTextFooter( + text = AnnotatedString(String.format(generalGetString(MR.strings.operator_conditions_accepted_for_enabled_operators_on), localDate(conditionsAction.deadline))), + textAlign = TextAlign.Center + ) + Spacer(Modifier.fillMaxWidth().height(DEFAULT_PADDING)) + } + ConditionsDiffButton() + Spacer(Modifier.fillMaxWidth().height(DEFAULT_PADDING)) + } + + is UsageConditionsAction.Accepted -> { + AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), enableAlphaChanges = false, withPadding = false, bottomPadding = DEFAULT_PADDING) + if (conditionsAction.operators.isNotEmpty()) { + ReadableText(MR.strings.operators_conditions_accepted_for, args = conditionsAction.operators.joinToString(", ") { it.legalName_ }) + } + Column(modifier = Modifier.weight(1f).padding(bottom = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF)) { + ConditionsTextView(rhId) + } + } + + else -> { + 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) + } + } + } + } +} + +@Composable +fun SimpleConditionsView( + rhId: Long? +) { + 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) + } + } +} + +@Composable +fun ServersErrorFooter(errStr: String) { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painterResource(MR.images.ic_error), + contentDescription = stringResource(MR.strings.server_error), + tint = Color.Red, + modifier = Modifier + .size(19.sp.toDp()) + .offset(x = 2.sp.toDp()) + ) + TextIconSpaced() + Text( + errStr, + color = MaterialTheme.colors.secondary, + lineHeight = 18.sp, + fontSize = 14.sp + ) + } +} + +private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { + AlertManager.shared.showAlertDialogStacked( + title = generalGetString(MR.strings.smp_save_servers_question), + confirmText = generalGetString(MR.strings.save_verb), + dismissText = generalGetString(MR.strings.exit_without_saving), + onConfirm = save, + onDismiss = revert, + ) +} + +fun showUpdateNetworkSettingsDialog( + title: String, + startsWith: String = "", + message: String = generalGetString(MR.strings.updating_settings_will_reconnect_client_to_all_servers), + onDismiss: () -> Unit, + onConfirm: () -> Unit +) { + AlertManager.shared.showAlertDialog( + title = title, + text = startsWith + "\n\n" + message, + confirmText = generalGetString(MR.strings.update_network_settings_confirmation), + onDismiss = onDismiss, + onConfirm = onConfirm, + onDismissRequest = onDismiss + ) +} + +fun updateOperatorsConditionsAcceptance(usvs: MutableState<List<UserOperatorServers>>, updatedOperators: List<ServerOperator>) { + val modified = ArrayList(usvs.value) + for (i in modified.indices) { + val updatedOperator = updatedOperators.firstOrNull { it.operatorId == modified[i].operator?.operatorId } ?: continue + modified[i] = modified[i].copy(operator = modified[i].operator?.copy(conditionsAcceptance = updatedOperator.conditionsAcceptance)) + } + usvs.value = modified +} + +suspend fun validateServers_( + rhId: Long?, + userServersToValidate: List<UserOperatorServers>, + serverErrors: MutableState<List<UserServersError>> +) { + try { + val errors = chatController.validateServers(rhId, userServersToValidate) ?: return + serverErrors.value = errors + } catch (ex: Exception) { + Log.e(TAG, ex.stackTraceToString()) + } +} + +fun serversCanBeSaved( + currUserServers: List<UserOperatorServers>, + userServers: List<UserOperatorServers>, + serverErrors: List<UserServersError> +): Boolean { + return userServers != currUserServers && serverErrors.isEmpty() +} + +fun globalServersError(serverErrors: List<UserServersError>): String? { + for (err in serverErrors) { + if (err.globalError != null) { + return err.globalError + } + } + return null +} + +fun globalSMPServersError(serverErrors: List<UserServersError>): String? { + for (err in serverErrors) { + if (err.globalSMPError != null) { + return err.globalSMPError + } + } + return null +} + +fun globalXFTPServersError(serverErrors: List<UserServersError>): String? { + for (err in serverErrors) { + if (err.globalXFTPError != null) { + return err.globalXFTPError + } + } + return null +} + +fun findDuplicateHosts(serverErrors: List<UserServersError>): Set<String> { + val duplicateHostsList = serverErrors.mapNotNull { err -> + if (err is UserServersError.DuplicateServer) { + err.duplicateHost + } else { + null + } + } + return duplicateHostsList.toSet() +} + +private suspend fun saveServers( + rhId: Long?, + currUserServers: MutableState<List<UserOperatorServers>>, + userServers: MutableState<List<UserOperatorServers>> +) { + val userServersToSave = userServers.value + try { + val set = setUserServers(rhId, userServersToSave) + + if (set) { + // Get updated servers to learn new server ids (otherwise it messes up delete of newly added and saved servers) + val updatedServers = getUserServers(rhId) + // Get updated operators to update model + val updatedOperators = getServerOperators(rhId) + + if (updatedOperators != null) { + chatModel.conditions.value = updatedOperators + } + + if (updatedServers != null ) { + currUserServers.value = updatedServers + userServers.value = updatedServers + } else { + currUserServers.value = userServersToSave + } + } else { + currUserServers.value = userServersToSave + } + } catch (ex: Exception) { + Log.e(TAG, ex.stackTraceToString()) + } +} + +@Preview +@Composable +fun PreviewNetworkAndServersLayout() { + SimpleXTheme { + NetworkAndServersLayout( + currentRemoteHost = null, + networkUseSocksProxy = remember { mutableStateOf(true) }, + onionHosts = remember { mutableStateOf(OnionHosts.PREFER) }, + toggleSocksProxy = {}, + currUserServers = remember { mutableStateOf(emptyList()) }, + userServers = remember { mutableStateOf(emptyList()) }, + serverErrors = remember { mutableStateOf(emptyList()) } + ) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt new file mode 100644 index 0000000000..1ec2534ab1 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt @@ -0,0 +1,144 @@ +package chat.simplex.common.views.usersettings.networkAndServers + +import SectionBottomSpacer +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.model.* +import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress +import chat.simplex.common.views.helpers.* +import chat.simplex.common.platform.* +import chat.simplex.res.MR +import kotlinx.coroutines.* + +@Composable +fun ModalData.NewServerView( + userServers: MutableState<List<UserOperatorServers>>, + serverErrors: MutableState<List<UserServersError>>, + rhId: Long?, + close: () -> Unit +) { + val testing = remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + val newServer = remember { mutableStateOf(UserServer.empty) } + + ModalView(close = { + addServer( + scope, + newServer.value, + userServers, + serverErrors, + rhId, + close = close + ) + }) { + Box { + NewServerLayout( + newServer, + testing.value, + testServer = { + testing.value = true + withLongRunningApi { + val res = testServerConnection(newServer.value, chatModel) + if (isActive) { + newServer.value = res.first + testing.value = false + } + } + }, + ) + + if (testing.value) { + DefaultProgressView(null) + } + } + } +} + +@Composable +private fun NewServerLayout( + server: MutableState<UserServer>, + testing: Boolean, + testServer: () -> Unit, +) { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.smp_servers_new_server)) + CustomServer(server, testing, testServer, onDelete = null) + SectionBottomSpacer() + } +} + +fun serverProtocolAndOperator( + server: UserServer, + userServers: List<UserOperatorServers> +): Pair<ServerProtocol, ServerOperator?>? { + val serverAddress = parseServerAddress(server.server) + return if (serverAddress != null) { + val serverProtocol = serverAddress.serverProtocol + val hostnames = serverAddress.hostnames + val matchingOperator = userServers.mapNotNull { it.operator }.firstOrNull { op -> + op.serverDomains.any { domain -> + hostnames.any { hostname -> + hostname.endsWith(domain) + } + } + } + Pair(serverProtocol, matchingOperator) + } else { + null + } +} + +fun addServer( + scope: CoroutineScope, + server: UserServer, + userServers: MutableState<List<UserOperatorServers>>, + serverErrors: MutableState<List<UserServersError>>, + rhId: Long?, + close: () -> Unit +) { + val result = serverProtocolAndOperator(server, userServers.value) + if (result != null) { + val (serverProtocol, matchingOperator) = result + val operatorIndex = userServers.value.indexOfFirst { it.operator?.operatorId == matchingOperator?.operatorId } + if (operatorIndex != -1) { + // Create a mutable copy of the userServers list + val updatedUserServers = userServers.value.toMutableList() + val operatorServers = updatedUserServers[operatorIndex] + // Create a mutable copy of the smpServers or xftpServers and add the server + when (serverProtocol) { + ServerProtocol.SMP -> { + val updatedSMPServers = operatorServers.smpServers.toMutableList() + updatedSMPServers.add(server) + updatedUserServers[operatorIndex] = operatorServers.copy(smpServers = updatedSMPServers) + } + + ServerProtocol.XFTP -> { + val updatedXFTPServers = operatorServers.xftpServers.toMutableList() + updatedXFTPServers.add(server) + updatedUserServers[operatorIndex] = operatorServers.copy(xftpServers = updatedXFTPServers) + } + } + + userServers.value = updatedUserServers + close() + matchingOperator?.let { op -> + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.operator_server_alert_title), + text = String.format(generalGetString(MR.strings.server_added_to_operator__name), op.tradeName) + ) + } + } else { // Shouldn't happen + close() + AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error_adding_server)) + } + } else { + close() + if (server.server.trim().isNotEmpty()) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.smp_servers_invalid_address), + text = generalGetString(MR.strings.smp_servers_check_address) + ) + } + } +} 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 new file mode 100644 index 0000000000..cc72387875 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -0,0 +1,794 @@ +package chat.simplex.common.views.usersettings.networkAndServers + +import SectionBottomSpacer +import SectionCustomFooter +import SectionDividerSpaced +import SectionItemView +import SectionTextFooter +import SectionView +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.* +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.UriHandler +import androidx.compose.ui.text.* +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.* +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ChatController.getUsageConditions +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.item.ItemAction +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.* +import chat.simplex.res.MR +import com.mikepenz.markdown.compose.Markdown +import com.mikepenz.markdown.compose.components.markdownComponents +import com.mikepenz.markdown.compose.elements.MarkdownHeader +import com.mikepenz.markdown.m2.markdownColor +import com.mikepenz.markdown.m2.markdownTypography +import com.mikepenz.markdown.model.markdownPadding +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.* +import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor + +@Composable +fun OperatorView( + currUserServers: MutableState<List<UserOperatorServers>>, + userServers: MutableState<List<UserOperatorServers>>, + serverErrors: MutableState<List<UserServersError>>, + operatorIndex: Int, + rhId: Long? +) { + val testing = remember { mutableStateOf(false) } + val operator = remember { userServers.value[operatorIndex].operator_ } + val currentUser = remember { chatModel.currentUser }.value + + LaunchedEffect(userServers) { + snapshotFlow { userServers.value } + .collect { updatedServers -> + validateServers_(rhId = rhId, userServersToValidate = updatedServers, serverErrors = serverErrors) + } + } + + Box { + ColumnWithScrollBar(Modifier.alpha(if (testing.value) 0.6f else 1f)) { + AppBarTitle(String.format(stringResource(MR.strings.operator_servers_title), operator.tradeName)) + OperatorViewLayout( + currUserServers, + userServers, + serverErrors, + operatorIndex, + navigateToProtocolView = { serverIndex, server, protocol -> + navigateToProtocolView(userServers, serverErrors, operatorIndex, rhId, serverIndex, server, protocol) + }, + currentUser, + rhId, + testing + ) + } + + if (testing.value) { + DefaultProgressView(null) + } + } +} + +fun navigateToProtocolView( + userServers: MutableState<List<UserOperatorServers>>, + serverErrors: MutableState<List<UserServersError>>, + operatorIndex: Int, + rhId: Long?, + serverIndex: Int, + server: UserServer, + protocol: ServerProtocol +) { + ModalManager.start.showCustomModal { close -> + ProtocolServerView( + m = chatModel, + server = server, + serverProtocol = protocol, + userServers = userServers, + serverErrors = serverErrors, + onDelete = { + if (protocol == ServerProtocol.SMP) { + deleteSMPServer(userServers, operatorIndex, serverIndex) + } else { + deleteXFTPServer(userServers, operatorIndex, serverIndex) + } + close() + }, + onUpdate = { updatedServer -> + userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + smpServers = if (protocol == ServerProtocol.SMP) { + this[operatorIndex].smpServers.toMutableList().apply { + this[serverIndex] = updatedServer + } + } else this[operatorIndex].smpServers, + xftpServers = if (protocol == ServerProtocol.XFTP) { + this[operatorIndex].xftpServers.toMutableList().apply { + this[serverIndex] = updatedServer + } + } else this[operatorIndex].xftpServers + ) + } + }, + close = close, + rhId = rhId + ) + } +} + +@Composable +fun OperatorViewLayout( + currUserServers: MutableState<List<UserOperatorServers>>, + userServers: MutableState<List<UserOperatorServers>>, + serverErrors: MutableState<List<UserServersError>>, + operatorIndex: Int, + navigateToProtocolView: (Int, UserServer, ServerProtocol) -> Unit, + currentUser: User?, + rhId: Long?, + testing: MutableState<Boolean> +) { + val operator by remember { derivedStateOf { userServers.value[operatorIndex].operator_ } } + val scope = rememberCoroutineScope() + val duplicateHosts = findDuplicateHosts(serverErrors.value) + + Column { + SectionView(generalGetString(MR.strings.operator).uppercase()) { + SectionItemView({ ModalManager.start.showModalCloseable { _ -> OperatorInfoView(operator) } }) { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painterResource(operator.largeLogo), + operator.tradeName, + modifier = Modifier.height(48.dp), + colorFilter = if (operator.enabled) null else ColorFilter.colorMatrix(ColorMatrix().apply { + setToSaturation(0f) + }) + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + Box(Modifier.padding(horizontal = 2.dp)) { + Icon(painterResource(MR.images.ic_info), null, Modifier.size(24.dp), tint = MaterialTheme.colors.primaryVariant) + } + } + } + UseOperatorToggle( + currUserServers = currUserServers, + userServers = userServers, + serverErrors = serverErrors, + operatorIndex = operatorIndex, + rhId = rhId + ) + } + val serversErr = globalServersError(serverErrors.value) + if (serversErr != null) { + SectionCustomFooter { + ServersErrorFooter(serversErr) + } + } else { + val footerText = when (val c = operator.conditionsAcceptance) { + is ConditionsAcceptance.Accepted -> if (c.acceptedAt != null) { + String.format(generalGetString(MR.strings.operator_conditions_accepted_on), localDate(c.acceptedAt)) + } else null + is ConditionsAcceptance.Required -> if (operator.enabled && c.deadline != null) { + String.format(generalGetString(MR.strings.operator_conditions_will_be_accepted_on), localDate(c.deadline)) + } else null + } + if (footerText != null) { + SectionTextFooter(footerText) + } + } + + if (operator.enabled) { + if (userServers.value[operatorIndex].smpServers.any { !it.deleted }) { + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.operator_use_for_messages).uppercase()) { + SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Text( + stringResource(MR.strings.operator_use_for_messages_receiving), + Modifier.padding(end = 24.dp), + color = Color.Unspecified + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + DefaultSwitch( + checked = userServers.value[operatorIndex].operator_.smpRoles.storage, + onCheckedChange = { enabled -> + userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + operator = this[operatorIndex].operator?.copy( + smpRoles = this[operatorIndex].operator?.smpRoles?.copy(storage = enabled) ?: ServerRoles(storage = enabled, proxy = false) + ) + ) + } + } + ) + } + SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Text( + stringResource(MR.strings.operator_use_for_messages_private_routing), + Modifier.padding(end = 24.dp), + color = Color.Unspecified + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + DefaultSwitch( + checked = userServers.value[operatorIndex].operator_.smpRoles.proxy, + onCheckedChange = { enabled -> + userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + operator = this[operatorIndex].operator?.copy( + smpRoles = this[operatorIndex].operator?.smpRoles?.copy(proxy = enabled) ?: ServerRoles(storage = false, proxy = enabled) + ) + ) + } + } + ) + } + + } + val smpErr = globalSMPServersError(serverErrors.value) + if (smpErr != null) { + SectionCustomFooter { + ServersErrorFooter(smpErr) + } + } + } + + // Preset servers can't be deleted + if (userServers.value[operatorIndex].smpServers.any { it.preset }) { + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.message_servers).uppercase()) { + userServers.value[operatorIndex].smpServers.forEachIndexed { i, server -> + if (!server.preset) return@forEachIndexed + SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.SMP) }) { + ProtocolServerViewLink( + srv = server, + serverProtocol = ServerProtocol.SMP, + duplicateHosts = duplicateHosts + ) + } + } + } + val smpErr = globalSMPServersError(serverErrors.value) + if (smpErr != null) { + SectionCustomFooter { + ServersErrorFooter(smpErr) + } + } else { + SectionTextFooter( + remember(currentUser?.displayName) { + buildAnnotatedString { + append(generalGetString(MR.strings.smp_servers_per_user) + " ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(currentUser?.displayName ?: "") + } + append(".") + } + } + ) + } + } + + if (userServers.value[operatorIndex].smpServers.any { !it.preset && !it.deleted }) { + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.operator_added_message_servers).uppercase()) { + userServers.value[operatorIndex].smpServers.forEachIndexed { i, server -> + if (server.deleted || server.preset) return@forEachIndexed + SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.SMP) }) { + ProtocolServerViewLink( + srv = server, + serverProtocol = ServerProtocol.SMP, + duplicateHosts = duplicateHosts + ) + } + } + } + } + + if (userServers.value[operatorIndex].xftpServers.any { !it.deleted }) { + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.operator_use_for_files).uppercase()) { + SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Text( + stringResource(MR.strings.operator_use_for_sending), + Modifier.padding(end = 24.dp), + color = Color.Unspecified + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + DefaultSwitch( + checked = userServers.value[operatorIndex].operator_.xftpRoles.storage, + onCheckedChange = { enabled -> + userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + operator = this[operatorIndex].operator?.copy( + xftpRoles = this[operatorIndex].operator?.xftpRoles?.copy(storage = enabled) ?: ServerRoles(storage = enabled, proxy = false) + ) + ) + } + } + ) + } + } + val xftpErr = globalXFTPServersError(serverErrors.value) + if (xftpErr != null) { + SectionCustomFooter { + ServersErrorFooter(xftpErr) + } + } + } + + // Preset servers can't be deleted + if (userServers.value[operatorIndex].xftpServers.any { it.preset }) { + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.media_and_file_servers).uppercase()) { + userServers.value[operatorIndex].xftpServers.forEachIndexed { i, server -> + if (!server.preset) return@forEachIndexed + SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.XFTP) }) { + ProtocolServerViewLink( + srv = server, + serverProtocol = ServerProtocol.XFTP, + duplicateHosts = duplicateHosts + ) + } + } + } + val xftpErr = globalXFTPServersError(serverErrors.value) + if (xftpErr != null) { + SectionCustomFooter { + ServersErrorFooter(xftpErr) + } + } else { + SectionTextFooter( + remember(currentUser?.displayName) { + buildAnnotatedString { + append(generalGetString(MR.strings.xftp_servers_per_user) + " ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(currentUser?.displayName ?: "") + } + append(".") + } + } + ) + } + } + + if (userServers.value[operatorIndex].xftpServers.any { !it.preset && !it.deleted}) { + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.operator_added_xftp_servers).uppercase()) { + userServers.value[operatorIndex].xftpServers.forEachIndexed { i, server -> + if (server.deleted || server.preset) return@forEachIndexed + SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.XFTP) }) { + ProtocolServerViewLink( + srv = server, + serverProtocol = ServerProtocol.XFTP, + duplicateHosts = duplicateHosts + ) + } + } + } + } + + SectionDividerSpaced() + SectionView { + TestServersButton( + testing = testing, + smpServers = userServers.value[operatorIndex].smpServers, + xftpServers = userServers.value[operatorIndex].xftpServers, + ) { p, l -> + when (p) { + ServerProtocol.XFTP -> userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + xftpServers = l + ) + } + + ServerProtocol.SMP -> userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + smpServers = l + ) + } + } + } + } + + SectionBottomSpacer() + } + } +} + +@Composable +fun OperatorInfoView(serverOperator: ServerOperator) { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.operator_info_title)) + + SectionView { + SectionItemView { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Image(painterResource(serverOperator.largeLogo), null, Modifier.height(48.dp)) + if (serverOperator.legalName != null) { + Text(serverOperator.legalName) + } + } + } + } + + SectionDividerSpaced(maxBottomPadding = false) + + val uriHandler = LocalUriHandler.current + SectionView { + SectionItemView { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + serverOperator.info.description.forEach { d -> + Text(d) + } + val website = serverOperator.info.website + Text(website, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openUriCatching(website) }) + } + } + } + + val selfhost = serverOperator.info.selfhost + if (selfhost != null) { + SectionDividerSpaced(maxBottomPadding = false) + SectionView { + SectionItemView { + val (text, link) = selfhost + Text(text, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openUriCatching(link) }) + } + } + } + } +} + +@Composable +private fun UseOperatorToggle( + currUserServers: MutableState<List<UserOperatorServers>>, + userServers: MutableState<List<UserOperatorServers>>, + serverErrors: MutableState<List<UserServersError>>, + operatorIndex: Int, + rhId: Long? +) { + SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Text( + stringResource(MR.strings.operator_use_operator_toggle_description), + Modifier.padding(end = 24.dp), + color = Color.Unspecified + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + DefaultSwitch( + checked = userServers.value[operatorIndex].operator?.enabled ?: false, + onCheckedChange = { enabled -> + val operator = userServers.value[operatorIndex].operator + if (enabled) { + when (val conditionsAcceptance = operator?.conditionsAcceptance) { + is ConditionsAcceptance.Accepted -> { + changeOperatorEnabled(userServers, operatorIndex, true) + } + + is ConditionsAcceptance.Required -> { + if (conditionsAcceptance.deadline == null) { + ModalManager.start.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> + SingleOperatorUsageConditionsView( + currUserServers = currUserServers, + userServers = userServers, + serverErrors = serverErrors, + operatorIndex = operatorIndex, + rhId = rhId, + close = close + ) + } + } else { + changeOperatorEnabled(userServers, operatorIndex, true) + } + } + + else -> {} + } + } else { + changeOperatorEnabled(userServers, operatorIndex, false) + } + }, + ) + } +} + +@Composable +private fun SingleOperatorUsageConditionsView( + currUserServers: MutableState<List<UserOperatorServers>>, + userServers: MutableState<List<UserOperatorServers>>, + serverErrors: MutableState<List<UserServersError>>, + operatorIndex: Int, + rhId: Long?, + close: () -> Unit +) { + val operatorsWithConditionsAccepted = remember { chatModel.conditions.value.serverOperators.filter { it.conditionsAcceptance.conditionsAccepted } } + val operator = remember { userServers.value[operatorIndex].operator_ } + val scope = rememberCoroutineScope() + + suspend fun acceptForOperators(rhId: Long?, operatorIds: List<Long>, operatorIndexToEnable: Int, close: () -> Unit) { + try { + val conditionsId = chatModel.conditions.value.currentConditions.conditionsId + val r = chatController.acceptConditions(rhId, conditionsId, operatorIds) ?: return + + chatModel.conditions.value = r + updateOperatorsConditionsAcceptance(currUserServers, r.serverOperators) + updateOperatorsConditionsAcceptance(userServers, r.serverOperators) + changeOperatorEnabled(userServers, operatorIndex, true) + close() + } catch (ex: Exception) { + Log.e(TAG, ex.stackTraceToString()) + } + } + + @Composable + fun AcceptConditionsButton(close: () -> Unit) { + // Opened operator or Other enabled operators with conditions not accepted + val operatorIds = chatModel.conditions.value.serverOperators + .filter { it.operatorId == operator.id || (it.enabled && !it.conditionsAcceptance.conditionsAccepted) } + .map { it.operatorId } + + Column(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING * 2), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingActionButton( + labelId = MR.strings.accept_conditions, + onboarding = null, + enabled = operatorIds.isNotEmpty(), + onclick = { + scope.launch { + acceptForOperators(rhId, operatorIds, operatorIndex, close) + } + } + ) + } + } + + @Composable + fun UsageConditionsDestinationView(close: () -> Unit) { + ColumnWithScrollBar(modifier = Modifier.fillMaxSize()) { + AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), enableAlphaChanges = false) + Column(modifier = Modifier.weight(1f).padding(end = DEFAULT_PADDING, start = DEFAULT_PADDING, bottom = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF)) { + ConditionsTextView(rhId) + } + } + } + + @Composable + fun UsageConditionsNavLinkButton() { + Text( + stringResource(MR.strings.view_conditions), + color = MaterialTheme.colors.primary, + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF).clickable { + ModalManager.start.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> + UsageConditionsDestinationView(close) + } + } + ) + } + + ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING)) { + AppBarTitle(String.format(stringResource(MR.strings.use_servers_of_operator_x), operator.tradeName), enableAlphaChanges = false, withPadding = false) + if (operator.conditionsAcceptance is ConditionsAcceptance.Accepted) { + // In current UI implementation this branch doesn't get shown - as conditions can't be opened from inside operator once accepted + Column(modifier = Modifier.weight(1f).padding(top = DEFAULT_PADDING_HALF, bottom = DEFAULT_PADDING)) { + ConditionsTextView(rhId) + } + } else if (operatorsWithConditionsAccepted.isNotEmpty()) { + ReadableText( + MR.strings.operator_conditions_accepted_for_some, + args = operatorsWithConditionsAccepted.joinToString(", ") { it.legalName_ } + ) + ReadableText( + MR.strings.operator_same_conditions_will_be_applied, + args = operator.legalName_ + ) + ConditionsAppliedToOtherOperatorsText(userServers = userServers.value, operatorIndex = operatorIndex) + + UsageConditionsNavLinkButton() + Spacer(Modifier.fillMaxWidth().weight(1f)) + AcceptConditionsButton(close) + } else { + ReadableText( + MR.strings.operator_in_order_to_use_accept_conditions, + args = operator.legalName_ + ) + ConditionsAppliedToOtherOperatorsText(userServers = userServers.value, operatorIndex = operatorIndex) + Column(modifier = Modifier.weight(1f).padding(top = DEFAULT_PADDING_HALF, bottom = DEFAULT_PADDING)) { + ConditionsTextView(rhId) + } + AcceptConditionsButton(close) + } + } +} + +@Composable +fun ConditionsTextView( + rhId: Long? +) { + val conditionsData = remember { mutableStateOf<Triple<UsageConditionsDetail, String?, UsageConditionsDetail?>?>(null) } + val failedToLoad = remember { mutableStateOf(false) } + val defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md" + val scope = rememberCoroutineScope() + // can show conditions when animation between modals finishes to prevent glitches + val canShowConditionsAt = remember { System.currentTimeMillis() + 300 } + LaunchedEffect(Unit) { + scope.launch(Dispatchers.Default) { + try { + val conditions = getUsageConditions(rh = rhId) + + if (conditions != null) { + val parentLink = "https://github.com/simplex-chat/simplex-chat/blob/${conditions.first.conditionsCommit}" + val conditionsText = conditions.second + val preparedText = if (conditionsText != null) prepareMarkdown(conditionsText.trimIndent(), parentLink) else null + val modifiedConditions = Triple(conditions.first, preparedText, conditions.third) + delay((canShowConditionsAt - System.currentTimeMillis()).coerceAtLeast(0)) + conditionsData.value = modifiedConditions + } else { + failedToLoad.value = true + } + } catch (ex: Exception) { + failedToLoad.value = true + } + } + } + val conditions = conditionsData.value + + if (conditions != null) { + val (usageConditions, conditionsText, _) = conditions + + if (conditionsText != null) { + val scrollState = rememberScrollState() + ConditionsBox( + Modifier + .fillMaxSize() + .border(border = BorderStroke(1.dp, CurrentColors.value.colors.secondary.copy(alpha = 0.6f)), shape = RoundedCornerShape(12.dp)) + .clip(shape = RoundedCornerShape(12.dp)), + scrollState + ) { + val parentUriHandler = LocalUriHandler.current + CompositionLocalProvider(LocalUriHandler provides remember { internalUriHandler(parentUriHandler) }) { + ConditionsMarkdown(conditionsText) + } + } + } else { + val conditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/${usageConditions.conditionsCommit}/PRIVACY.md" + ConditionsLinkView(conditionsLink) + } + } else if (failedToLoad.value) { + ConditionsLinkView(defaultConditionsLink) + } else { + DefaultProgressView(null) + } +} + +@Composable +expect fun ConditionsBox(modifier: Modifier, scrollState: ScrollState, content: @Composable() (BoxScope.() -> Unit)) + +@Composable +private fun ConditionsMarkdown(text: String) { + Markdown(text, + markdownColor(linkText = MaterialTheme.colors.primary), + markdownTypography( + h1 = MaterialTheme.typography.body1, + h2 = MaterialTheme.typography.h3.copy(fontSize = 22.sp, fontWeight = FontWeight.Bold), + h3 = MaterialTheme.typography.h4.copy(fontWeight = FontWeight.Bold), + h4 = MaterialTheme.typography.h5.copy(fontSize = 16.sp, fontWeight = FontWeight.Bold), + h5 = MaterialTheme.typography.h6.copy(fontWeight = FontWeight.Bold), + link = MaterialTheme.typography.body1.copy( + textDecoration = TextDecoration.Underline + ) + ), + Modifier.padding(8.dp), + // using CommonMarkFlavourDescriptor instead of GFMFlavourDescriptor because it shows `https://simplex.chat/` (link inside backticks) incorrectly + flavour = CommonMarkFlavourDescriptor(), + components = markdownComponents( + heading2 = { + Spacer(Modifier.height(10.dp)) + MarkdownHeader(it.content, it.node, it.typography.h2) + Spacer(Modifier.height(5.dp)) + }, + heading3 = { + Spacer(Modifier.height(10.dp)) + MarkdownHeader(it.content, it.node, it.typography.h3) + Spacer(Modifier.height(3.dp)) + }, + heading4 = { + Spacer(Modifier.height(10.dp)) + MarkdownHeader(it.content, it.node, it.typography.h4) + Spacer(Modifier.height(4.dp)) + }, + ), + padding = markdownPadding(block = 4.dp) + ) +} + +@Composable +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) }) + } +} + +@Composable +private fun ConditionsAppliedToOtherOperatorsText(userServers: List<UserOperatorServers>, operatorIndex: Int) { + val otherOperatorsToApply = remember { + derivedStateOf { + chatModel.conditions.value.serverOperators.filter { + it.enabled && + !it.conditionsAcceptance.conditionsAccepted && + it.operatorId != userServers[operatorIndex].operator_.operatorId + } + } + } + + if (otherOperatorsToApply.value.isNotEmpty()) { + ReadableText( + MR.strings.operator_conditions_will_be_applied, + args = otherOperatorsToApply.value.joinToString(", ") { it.legalName_ } + ) + } +} + +@Composable +fun ConditionsLinkButton() { + val showMenu = remember { mutableStateOf(false) } + val uriHandler = LocalUriHandler.current + val oneHandUI = remember { appPrefs.oneHandUI.state } + Column { + DefaultDropdownMenu(showMenu, offset = if (oneHandUI.value) DpOffset(0.dp, -AppBarHeight * fontSizeSqrtMultiplier * 3) else DpOffset.Zero) { + 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 + }) + 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 + }) + } + IconButton({ showMenu.value = true }) { + Icon(painterResource(MR.images.ic_outbound), null, tint = MaterialTheme.colors.primary) + } + } +} + +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) + } + } +} + +private fun prepareMarkdown(text: String, parentLink: String): String { + val localLinkRegex = Regex("\\[([^\\)]*)\\]\\(#.*\\)", RegexOption.MULTILINE) + return text + .replace("](/", "]($parentLink/") + .replace("](./", "]($parentLink/") + .replace(localLinkRegex) { it.groupValues.getOrNull(1) ?: it.value } +} + +private fun changeOperatorEnabled(userServers: MutableState<List<UserOperatorServers>>, operatorIndex: Int, enabled: Boolean) { + userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + operator = this[operatorIndex].operator?.copy(enabled = enabled) + ) + } +} \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt new file mode 100644 index 0000000000..bebc96a28c --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt @@ -0,0 +1,246 @@ +package chat.simplex.common.views.usersettings.networkAndServers + +import SectionBottomSpacer +import SectionDividerSpaced +import SectionItemView +import SectionItemViewSpaceBetween +import SectionView +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.common.model.* +import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.QRCode +import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.* +import chat.simplex.common.views.usersettings.PreferenceToggle +import chat.simplex.res.MR +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.distinctUntilChanged + +@Composable +fun ProtocolServerView( + m: ChatModel, + server: UserServer, + serverProtocol: ServerProtocol, + userServers: MutableState<List<UserOperatorServers>>, + serverErrors: MutableState<List<UserServersError>>, + onDelete: () -> Unit, + onUpdate: (UserServer) -> Unit, + close: () -> Unit, + rhId: Long? +) { + val testing = remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + val draftServer = remember { mutableStateOf(server) } + + ModalView( + close = { + scope.launch { + val draftResult = serverProtocolAndOperator(draftServer.value, userServers.value) + val savedResult = serverProtocolAndOperator(server, userServers.value) + + if (draftResult != null && savedResult != null) { + val (serverToEditProtocol, serverToEditOperator) = draftResult + val (svProtocol, serverOperator) = savedResult + + if (serverToEditProtocol != svProtocol) { + close() + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.error_updating_server_title), + text = generalGetString(MR.strings.error_server_protocol_changed) + ) + } else if (serverToEditOperator != serverOperator) { + close() + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.error_updating_server_title), + text = generalGetString(MR.strings.error_server_operator_changed) + ) + } else { + onUpdate(draftServer.value) + close() + } + } else { + close() + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.smp_servers_invalid_address), + text = generalGetString(MR.strings.smp_servers_check_address) + ) + } + } + } + ) { + Box { + ProtocolServerLayout( + draftServer, + serverProtocol, + testing.value, + testServer = { + testing.value = true + withLongRunningApi { + val res = testServerConnection(draftServer.value, m) + if (isActive) { + draftServer.value = res.first + testing.value = false + } + } + }, + onDelete + ) + + if (testing.value) { + DefaultProgressView(null) + } + } + } +} + +@Composable +private fun ProtocolServerLayout( + server: MutableState<UserServer>, + serverProtocol: ServerProtocol, + testing: Boolean, + testServer: () -> Unit, + onDelete: () -> Unit, +) { + ColumnWithScrollBar { + AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.XFTP) MR.strings.xftp_server else MR.strings.smp_server)) + + if (server.value.preset) { + PresetServer(server, testing, testServer) + } else { + CustomServer(server, testing, testServer, onDelete) + } + SectionBottomSpacer() + } +} + +@Composable +private fun PresetServer( + server: MutableState<UserServer>, + testing: Boolean, + testServer: () -> Unit +) { + SectionView(stringResource(MR.strings.smp_servers_preset_address).uppercase()) { + SelectionContainer { + Text( + server.value.server, + Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp), + style = TextStyle( + fontFamily = FontFamily.Monospace, fontSize = 16.sp, + color = MaterialTheme.colors.secondary + ) + ) + } + } + SectionDividerSpaced() + UseServerSection(server, true, testing, testServer) +} + +@Composable +fun CustomServer( + server: MutableState<UserServer>, + testing: Boolean, + testServer: () -> Unit, + onDelete: (() -> Unit)?, +) { + val serverAddress = remember { mutableStateOf(server.value.server) } + val valid = remember { + derivedStateOf { + with(parseServerAddress(serverAddress.value)) { + this?.valid == true + } + } + } + SectionView( + stringResource(MR.strings.smp_servers_your_server_address).uppercase(), + icon = painterResource(MR.images.ic_error), + iconTint = if (!valid.value) MaterialTheme.colors.error else Color.Transparent, + ) { + val testedPreviously = remember { mutableMapOf<String, Boolean?>() } + TextEditor( + serverAddress, + Modifier.height(144.dp) + ) + LaunchedEffect(Unit) { + snapshotFlow { serverAddress.value } + .distinctUntilChanged() + .collect { + testedPreviously[server.value.server] = server.value.tested + server.value = server.value.copy(server = it, tested = testedPreviously[serverAddress.value]) + } + } + } + SectionDividerSpaced(maxTopPadding = true) + + UseServerSection(server, valid.value, testing, testServer, onDelete) + + if (valid.value) { + SectionDividerSpaced() + SectionView(stringResource(MR.strings.smp_servers_add_to_another_device).uppercase()) { + QRCode(serverAddress.value) + } + } +} + +@Composable +private fun UseServerSection( + server: MutableState<UserServer>, + valid: Boolean, + testing: Boolean, + testServer: () -> Unit, + onDelete: (() -> Unit)? = null, +) { + SectionView(stringResource(MR.strings.smp_servers_use_server).uppercase()) { + SectionItemViewSpaceBetween(testServer, disabled = !valid || testing) { + Text(stringResource(MR.strings.smp_servers_test_server), color = if (valid && !testing) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) + ShowTestStatus(server.value) + } + + val enabled = rememberUpdatedState(server.value.enabled) + PreferenceToggle( + stringResource(MR.strings.smp_servers_use_server_for_new_conn), + disabled = testing, + checked = enabled.value + ) { + server.value = server.value.copy(enabled = it) + } + + if (onDelete != null) { + SectionItemView(onDelete, disabled = testing) { + Text(stringResource(MR.strings.smp_servers_delete_server), color = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.error) + } + } + } +} + +@Composable +fun ShowTestStatus(server: UserServer, modifier: Modifier = Modifier) = + when (server.tested) { + true -> Icon(painterResource(MR.images.ic_check), null, modifier, tint = SimplexGreen) + false -> Icon(painterResource(MR.images.ic_close), null, modifier, tint = MaterialTheme.colors.error) + else -> Icon(painterResource(MR.images.ic_check), null, modifier, tint = Color.Transparent) + } + +suspend fun testServerConnection(server: UserServer, m: ChatModel): Pair<UserServer, ProtocolTestFailure?> = + try { + val r = m.controller.testProtoServer(server.remoteHostId, server.server) + server.copy(tested = r == null) to r + } catch (e: Exception) { + Log.e(TAG, "testServerConnection ${e.stackTraceToString()}") + server.copy(tested = false) to null + } + +fun serverHostname(srv: String): String = + parseServerAddress(srv)?.hostnames?.firstOrNull() ?: srv 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 new file mode 100644 index 0000000000..63bf8b1dc4 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt @@ -0,0 +1,407 @@ +package chat.simplex.common.views.usersettings.networkAndServers + +import SectionBottomSpacer +import SectionCustomFooter +import SectionDividerSpaced +import SectionItemView +import SectionTextFooter +import SectionView +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.text.* +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.views.usersettings.SettingsActionItem +import chat.simplex.res.MR +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Composable +fun ModalData.YourServersView( + userServers: MutableState<List<UserOperatorServers>>, + serverErrors: MutableState<List<UserServersError>>, + operatorIndex: Int, + rhId: Long? +) { + val testing = remember { mutableStateOf(false) } + val currentUser = remember { chatModel.currentUser }.value + val scope = rememberCoroutineScope() + + LaunchedEffect(userServers) { + snapshotFlow { userServers.value } + .collect { updatedServers -> + validateServers_(rhId = rhId, userServersToValidate = updatedServers, serverErrors = serverErrors) + } + } + + Box { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.your_servers)) + YourServersViewLayout( + scope, + userServers, + serverErrors, + operatorIndex, + navigateToProtocolView = { serverIndex, server, protocol -> + navigateToProtocolView(userServers, serverErrors, operatorIndex, rhId, serverIndex, server, protocol) + }, + currentUser, + rhId, + testing + ) + } + + if (testing.value) { + DefaultProgressView(null) + } + } +} + +@Composable +fun YourServersViewLayout( + scope: CoroutineScope, + userServers: MutableState<List<UserOperatorServers>>, + serverErrors: MutableState<List<UserServersError>>, + operatorIndex: Int, + navigateToProtocolView: (Int, UserServer, ServerProtocol) -> Unit, + currentUser: User?, + rhId: Long?, + testing: MutableState<Boolean> +) { + val duplicateHosts = findDuplicateHosts(serverErrors.value) + + Column { + if (userServers.value[operatorIndex].smpServers.any { !it.deleted }) { + SectionView(generalGetString(MR.strings.message_servers).uppercase()) { + userServers.value[operatorIndex].smpServers.forEachIndexed { i, server -> + if (server.deleted) return@forEachIndexed + SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.SMP) }) { + ProtocolServerViewLink( + srv = server, + serverProtocol = ServerProtocol.SMP, + duplicateHosts = duplicateHosts + ) + } + } + } + val smpErr = globalSMPServersError(serverErrors.value) + if (smpErr != null) { + SectionCustomFooter { + ServersErrorFooter(smpErr) + } + } else { + SectionTextFooter( + remember(currentUser?.displayName) { + buildAnnotatedString { + append(generalGetString(MR.strings.smp_servers_per_user) + " ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(currentUser?.displayName ?: "") + } + append(".") + } + } + ) + } + } + + if (userServers.value[operatorIndex].xftpServers.any { !it.deleted }) { + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.media_and_file_servers).uppercase()) { + userServers.value[operatorIndex].xftpServers.forEachIndexed { i, server -> + if (server.deleted) return@forEachIndexed + SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.XFTP) }) { + ProtocolServerViewLink( + srv = server, + serverProtocol = ServerProtocol.XFTP, + duplicateHosts = duplicateHosts + ) + } + } + } + val xftpErr = globalXFTPServersError(serverErrors.value) + if (xftpErr != null) { + SectionCustomFooter { + ServersErrorFooter(xftpErr) + } + } else { + SectionTextFooter( + remember(currentUser?.displayName) { + buildAnnotatedString { + append(generalGetString(MR.strings.xftp_servers_per_user) + " ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(currentUser?.displayName ?: "") + } + append(".") + } + } + ) + } + } + + if ( + userServers.value[operatorIndex].smpServers.any { !it.deleted } || + userServers.value[operatorIndex].xftpServers.any { !it.deleted } + ) { + SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) + } + + SectionView { + SettingsActionItem( + painterResource(MR.images.ic_add), + stringResource(MR.strings.smp_servers_add), + click = { showAddServerDialog(scope, userServers, serverErrors, rhId) }, + disabled = testing.value, + textColor = if (testing.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary, + iconColor = if (testing.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + val serversErr = globalServersError(serverErrors.value) + if (serversErr != null) { + SectionCustomFooter { + ServersErrorFooter(serversErr) + } + } + SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) + + SectionView { + TestServersButton( + testing = testing, + smpServers = userServers.value[operatorIndex].smpServers, + xftpServers = userServers.value[operatorIndex].xftpServers, + ) { p, l -> + when (p) { + ServerProtocol.XFTP -> userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + xftpServers = l + ) + } + + ServerProtocol.SMP -> userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + smpServers = l + ) + } + } + } + + HowToButton() + } + SectionBottomSpacer() + } +} + +@Composable +fun TestServersButton( + smpServers: List<UserServer>, + xftpServers: List<UserServer>, + testing: MutableState<Boolean>, + onUpdate: (ServerProtocol, List<UserServer>) -> Unit +) { + val scope = rememberCoroutineScope() + val disabled = derivedStateOf { (smpServers.none { it.enabled } && xftpServers.none { it.enabled }) || testing.value } + + SectionItemView( + { + scope.launch { + testServers(testing, smpServers, xftpServers, chatModel, onUpdate) + } + }, + disabled = disabled.value + ) { + Text(stringResource(MR.strings.smp_servers_test_servers), color = if (!disabled.value) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) + } +} + +fun showAddServerDialog( + scope: CoroutineScope, + userServers: MutableState<List<UserOperatorServers>>, + serverErrors: MutableState<List<UserServersError>>, + rhId: Long? +) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.smp_servers_add), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + ModalManager.start.showCustomModal { close -> + NewServerView(userServers, serverErrors, rhId, close) + } + }) { + Text(stringResource(MR.strings.smp_servers_enter_manually), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + if (appPlatform.isAndroid) { + SectionItemView({ + AlertManager.shared.hideAlert() + ModalManager.start.showModalCloseable { close -> + ScanProtocolServer(rhId) { server -> + addServer( + scope, + server, + userServers, + serverErrors, + rhId, + close = close + ) + } + } + } + ) { + Text(stringResource(MR.strings.smp_servers_scan_qr), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + } + ) +} + +@Composable +fun ProtocolServerViewLink(serverProtocol: ServerProtocol, srv: UserServer, duplicateHosts: Set<String>) { + val address = parseServerAddress(srv.server) + when { + address == null || !address.valid || address.serverProtocol != serverProtocol || address.hostnames.any { it in duplicateHosts } -> InvalidServer() + !srv.enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary) + else -> ShowTestStatus(srv) + } + Spacer(Modifier.padding(horizontal = 4.dp)) + val text = address?.hostnames?.firstOrNull() ?: srv.server + if (srv.enabled) { + Text(text, color = MaterialTheme.colors.onBackground, maxLines = 1) + } else { + Text(text, maxLines = 1, color = MaterialTheme.colors.secondary) + } +} + +@Composable +private fun HowToButton() { + val uriHandler = LocalUriHandler.current + SettingsActionItem( + painterResource(MR.images.ic_open_in_new), + stringResource(MR.strings.how_to_use_your_servers), + { uriHandler.openUriCatching("https://simplex.chat/docs/server.html") }, + textColor = MaterialTheme.colors.primary, + iconColor = MaterialTheme.colors.primary + ) +} + +@Composable +fun InvalidServer() { + Icon(painterResource(MR.images.ic_error), null, tint = MaterialTheme.colors.error) +} + +private suspend fun testServers( + testing: MutableState<Boolean>, + smpServers: List<UserServer>, + xftpServers: List<UserServer>, + m: ChatModel, + onUpdate: (ServerProtocol, List<UserServer>) -> Unit +) { + val smpResetStatus = resetTestStatus(smpServers) + onUpdate(ServerProtocol.SMP, smpResetStatus) + val xftpResetStatus = resetTestStatus(xftpServers) + onUpdate(ServerProtocol.XFTP, xftpResetStatus) + testing.value = true + val smpFailures = runServersTest(smpResetStatus, m) { onUpdate(ServerProtocol.SMP, it) } + val xftpFailures = runServersTest(xftpResetStatus, m) { onUpdate(ServerProtocol.XFTP, it) } + testing.value = false + val fs = smpFailures + xftpFailures + if (fs.isNotEmpty()) { + val msg = fs.map { it.key + ": " + it.value.localizedDescription }.joinToString("\n") + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.smp_servers_test_failed), + text = generalGetString(MR.strings.smp_servers_test_some_failed) + "\n" + msg + ) + } +} + +private fun resetTestStatus(servers: List<UserServer>): List<UserServer> { + val copy = ArrayList(servers) + for ((index, server) in servers.withIndex()) { + if (server.enabled) { + copy.removeAt(index) + copy.add(index, server.copy(tested = null)) + } + } + return copy +} + +private suspend fun runServersTest(servers: List<UserServer>, m: ChatModel, onUpdated: (List<UserServer>) -> Unit): Map<String, ProtocolTestFailure> { + val fs: MutableMap<String, ProtocolTestFailure> = mutableMapOf() + val updatedServers = ArrayList<UserServer>(servers) + for ((index, server) in servers.withIndex()) { + if (server.enabled) { + interruptIfCancelled() + val (updatedServer, f) = testServerConnection(server, m) + updatedServers.removeAt(index) + updatedServers.add(index, updatedServer) + // toList() is important. Otherwise, Compose will not redraw the screen after first update + onUpdated(updatedServers.toList()) + if (f != null) { + fs[serverHostname(updatedServer.server)] = f + } + } + } + return fs +} + +fun deleteXFTPServer( + userServers: MutableState<List<UserOperatorServers>>, + operatorServersIndex: Int, + serverIndex: Int +) { + val serverIsSaved = userServers.value[operatorServersIndex].xftpServers[serverIndex].serverId != null + + if (serverIsSaved) { + userServers.value = userServers.value.toMutableList().apply { + this[operatorServersIndex] = this[operatorServersIndex].copy( + xftpServers = this[operatorServersIndex].xftpServers.toMutableList().apply { + this[serverIndex] = this[serverIndex].copy(deleted = true) + } + ) + } + } else { + userServers.value = userServers.value.toMutableList().apply { + this[operatorServersIndex] = this[operatorServersIndex].copy( + xftpServers = this[operatorServersIndex].xftpServers.toMutableList().apply { + this.removeAt(serverIndex) + } + ) + } + } +} + +fun deleteSMPServer( + userServers: MutableState<List<UserOperatorServers>>, + operatorServersIndex: Int, + serverIndex: Int +) { + val serverIsSaved = userServers.value[operatorServersIndex].smpServers[serverIndex].serverId != null + + if (serverIsSaved) { + userServers.value = userServers.value.toMutableList().apply { + this[operatorServersIndex] = this[operatorServersIndex].copy( + smpServers = this[operatorServersIndex].smpServers.toMutableList().apply { + this[serverIndex] = this[serverIndex].copy(deleted = true) + } + ) + } + } else { + userServers.value = userServers.value.toMutableList().apply { + this[operatorServersIndex] = this[operatorServersIndex].copy( + smpServers = this[operatorServersIndex].smpServers.toMutableList().apply { + this.removeAt(serverIndex) + } + ) + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.kt new file mode 100644 index 0000000000..d280773976 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.kt @@ -0,0 +1,32 @@ +package chat.simplex.common.views.usersettings.networkAndServers + +import androidx.compose.runtime.Composable +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress +import chat.simplex.common.model.UserServer +import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.QRCodeScanner +import chat.simplex.res.MR + +@Composable +expect fun ScanProtocolServer(rhId: Long?, onNext: (UserServer) -> Unit) + +@Composable +fun ScanProtocolServerLayout(rhId: Long?, onNext: (UserServer) -> Unit) { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.smp_servers_scan_qr)) + QRCodeScanner { text -> + val res = parseServerAddress(text) + if (res != null) { + onNext(UserServer(remoteHostId = rhId, null, text, false, null, false, false)) + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.smp_servers_invalid_address), + text = generalGetString(MR.strings.smp_servers_check_address) + ) + } + res != null + } + } +} 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 d16892096c..4bb2244785 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -12,51 +12,51 @@ <string name="about_simplex">عن SimpleX</string> <string name="above_then_preposition_continuation">أعلاه، ثم:</string> <string name="accept_call_on_lock_screen">اقبل</string> - <string name="delete_chat_profile_action_cannot_be_undone_warning">لا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف التعريف وجهات الاتصال والرسائل والملفات الخاصة بك بشكل نهائي.</string> + <string name="delete_chat_profile_action_cannot_be_undone_warning">لا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف تعريفك وجهات اتصالك ورسائلك وملفاتك بشكل نهائي.</string> <string name="alert_message_no_group">هذه المجموعة لم تعد موجودة.</string> <string name="this_QR_code_is_not_a_link">رمز QR هذا ليس رابطًا!</string> - <string name="next_generation_of_private_messaging">الجيل القادم من الرسائل الخاصة</string> + <string name="next_generation_of_private_messaging">مستقبل المُراسلة</string> <string name="delete_files_and_media_desc">لا يمكن التراجع عن هذا الإجراء - سيتم حذف جميع الملفات والوسائط المستلمة والمرسلة. ستبقى الصور منخفضة الدقة.</string> <string name="enable_automatic_deletion_message">لا يمكن التراجع عن هذا الإجراء - سيتم حذف الرسائل المرسلة والمستلمة قبل التحديد. قد تأخذ عدة دقائق.</string> <string name="messages_section_description">ينطبق هذا الإعداد على الرسائل الموجودة في ملف تعريف الدردشة الحالي الخاص بك</string> <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">منصة الرسائل والتطبيقات تحمي خصوصيتك وأمنك.</string> <string name="profile_is_only_shared_with_your_contacts">يتم مشاركة ملف التعريف مع جهات اتصالك فقط.</string> - <string name="member_role_will_be_changed_with_notification">سيتم تغيير الدور إلى \"%s\". سيتم إبلاغ كل فرد في المجموعة.</string> - <string name="member_role_will_be_changed_with_invitation">سيتم تغيير الدور إلى \"%s\". سيتلقى العضو دعوة جديدة.</string> + <string name="member_role_will_be_changed_with_notification">سيتم تغيير الدور إلى "%s". سيتم إبلاغ كل فرد في المجموعة.</string> + <string name="member_role_will_be_changed_with_invitation">سيتم تغيير الدور إلى "%s". سيستلم العضو دعوة جديدة.</string> <string name="smp_servers_per_user">خوادم الاتصالات الجديدة لملف تعريف الدردشة الحالي الخاص بك</string> <string name="switch_receiving_address_desc">سيتم تغيير عنوان الاستلام إلى خادم مختلف. سيتم إكمال تغيير العنوان بعد اتصال المرسل بالإنترنت.</string> - <string name="this_link_is_not_a_valid_connection_link">هذا الارتباط ليس ارتباط اتصال صالح!</string> + <string name="this_link_is_not_a_valid_connection_link">هذا الرابط ليس رابط اتصال صالح!</string> <string name="allow_verb">يسمح</string> - <string name="smp_servers_preset_add">أضف خوادم محددة مسبقًا</string> - <string name="smp_servers_add_to_another_device">أضف إلى جهاز آخر</string> + <string name="smp_servers_preset_add">أضِف خوادم مُعدة مسبقًا</string> + <string name="smp_servers_add_to_another_device">أضِف إلى جهاز آخر</string> <string name="users_delete_all_chats_deleted">سيتم حذف جميع الدردشات والرسائل - لا يمكن التراجع عن هذا!</string> - <string name="network_enable_socks_info">الوصول إلى الخوادم عبر بروكسي SOCKS على المنفذ %d؟ يجب بدء تشغيل الوكيل قبل تمكين هذا الخيار.</string> - <string name="smp_servers_add">إضافة خادم …</string> + <string name="network_enable_socks_info">الوصول إلى الخوادم عبر وكيل SOCKS على المنفذ %d؟ يجب بدء تشغيل الوكيل قبل تفعيل هذا الخيار.</string> + <string name="smp_servers_add">أضِف خادم</string> <string name="network_settings">إعدادات الشبكة المتقدمة</string> <string name="all_group_members_will_remain_connected">سيبقى جميع أعضاء المجموعة على اتصال.</string> <string name="allow_disappearing_messages_only_if">السماح باختفاء الرسائل فقط إذا سمحت جهة اتصالك بذلك.</string> - <string name="allow_irreversible_message_deletion_only_if">السماح بحذف الرسائل بشكل لا رجوع فيه فقط إذا سمحت لك جهة الاتصال بذلك.</string> - <string name="group_member_role_admin">مسؤل</string> - <string name="users_add">إضافة ملف التعريف</string> + <string name="allow_irreversible_message_deletion_only_if">السماح بحذف الرسائل بشكل لا رجوع فيه فقط إذا سمحت لك جهة الاتصال بذلك. (24 ساعة)</string> + <string name="group_member_role_admin">المُدير</string> + <string name="users_add">أضِف ملف التعريف</string> <string name="allow_direct_messages">السماح بإرسال رسائل مباشرة إلى الأعضاء.</string> <string name="accept_contact_incognito_button">قبول التخفي</string> - <string name="button_add_welcome_message">أضف رسالة ترحيب</string> + <string name="button_add_welcome_message">أضِف رسالة ترحيب</string> <string name="v4_3_improved_server_configuration_desc">أضف الخوادم عن طريق مسح رموز QR.</string> - <string name="v4_2_group_links_desc">يمكن للمسؤولين إنشاء روابط للانضمام إلى المجموعات.</string> + <string name="v4_2_group_links_desc">يمكن للمُدراء إنشاء روابط للانضمام إلى المجموعات.</string> <string name="accept_connection_request__question">قبول طلب الاتصال؟</string> <string name="clear_chat_warning">سيتم حذف جميع الرسائل - لا يمكن التراجع عن هذا! سيتم حذف الرسائل فقط من أجلك.</string> <string name="callstatus_accepted">مكالمة مقبولة</string> <string name="allow_calls_only_if">السماح بالمكالمات فقط إذا سمحت جهة اتصالك بذلك.</string> <string name="allow_message_reactions_only_if">اسمح بردود الفعل على الرسائل فقط إذا سمحت جهة اتصالك بذلك.</string> <string name="keychain_is_storing_securely">يتم استخدام Android Keystore لتخزين عبارة المرور بشكل آمن - فهو يسمح لخدمة الإشعارات بالعمل.</string> - <string name="empty_chat_profile_is_created">يتم إنشاء ملف تعريف دردشة فارغ بالاسم المقدم ، ويفتح التطبيق كالمعتاد.</string> + <string name="empty_chat_profile_is_created">يتم إنشاء ملف تعريف دردشة فارغ بالاسم المقدم، ويفتح التطبيق كالمعتاد.</string> <string name="answer_call">أجب الاتصال</string> - <string name="chat_preferences_always">دائِماً</string> + <string name="chat_preferences_always">دائمًا</string> <string name="allow_to_send_disappearing">السماح بإرسال رسائل تختفي.</string> <string name="allow_to_send_voice">السماح بإرسال رسائل صوتية.</string> <string name="settings_section_title_app">تطبيق</string> <string name="color_secondary_variant">ثانوي إضافي</string> - <string name="allow_your_contacts_adding_message_reactions">السماح لجهات اتصالك بإضافة ردود الفعل الرسالة.</string> + <string name="allow_your_contacts_adding_message_reactions">السماح لجهات اتصالك بإضافة ردود الفعل للرسالة.</string> <string name="allow_your_contacts_to_call">السماح لجهات اتصالك بالاتصال بك.</string> <string name="allow_message_reactions">السماح بردود الفعل على الرسائل.</string> <string name="v5_1_self_destruct_passcode_descr">يتم مسح جميع البيانات عند إدخالها.</string> @@ -64,29 +64,29 @@ <string name="allow_your_contacts_to_send_disappearing_messages">السماح لجهات اتصالك بإرسال رسائل تختفي.</string> <string name="allow_voice_messages_only_if">اسمح بالرسائل الصوتية فقط إذا سمحت جهة اتصالك بذلك.</string> <string name="v5_0_app_passcode">رمز مرور التطبيق</string> - <string name="notifications_mode_service">دائِماً مُتاح</string> - <string name="notifications_mode_off_desc">يمكن للتطبيق تلقي الإشعارات فقط عند تشغيله ، ولن يتم بدء تشغيل أي خدمة في الخلفية</string> + <string name="notifications_mode_service">يعمل دائمًا</string> + <string name="notifications_mode_off_desc">يمكن للتطبيق استلام الإشعارات فقط عند تشغيله، ولن يتم بدء تشغيل أي خدمة في الخلفية</string> <string name="allow_voice_messages_question">السماح بالرسائل الصوتية؟</string> - <string name="all_your_contacts_will_remain_connected">ستبقى جميع جهات الاتصال الخاصة بك متصلة.</string> - <string name="always_use_relay">استخدم التتابع دائمًا</string> + <string name="all_your_contacts_will_remain_connected">ستبقى جميع جهات اتصالك متصلة.</string> + <string name="always_use_relay">استخدم الموجه دائمًا</string> <string name="full_backup">النسخ الاحتياطي لبيانات التطبيق</string> <string name="all_app_data_will_be_cleared">حُذفت جميع بيانات التطبيق.</string> - <string name="allow_to_delete_messages">السماح بحذف الرسائل المرسلة بشكل لا رجعة فيه.</string> + <string name="allow_to_delete_messages">السماح بحذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة)</string> <string name="allow_your_contacts_to_send_voice_messages">اسمح لجهات اتصالك بإرسال رسائل صوتية.</string> - <string name="learn_more_about_address">حول عنوان SimpleX</string> + <string name="learn_more_about_address">عن عنوان SimpleX</string> <string name="app_version_code">بناء التطبيق: %s</string> <string name="appearance_settings">المظهر</string> - <string name="add_address_to_your_profile">أضف عنوانًا إلى ملف التعريف الخاص بك ، حتى تتمكن جهات الاتصال الخاصة بك من مشاركته مع أشخاص آخرين. سيتم إرسال تحديث الملف الشخصي إلى جهات الاتصال الخاصة بك.</string> - <string name="all_your_contacts_will_remain_connected_update_sent">ستبقى جميع جهات الاتصال الخاصة بك متصلة. سيتم إرسال تحديث الملف الشخصي إلى جهات الاتصال الخاصة بك.</string> + <string name="add_address_to_your_profile">أضف عنوانًا إلى ملف تعريفك، حتى تتمكن جهات اتصالك من مشاركته مع أشخاص آخرين. سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك.</string> + <string name="all_your_contacts_will_remain_connected_update_sent">ستبقى جميع جهات اتصالك متصلة. سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك.</string> <string name="settings_section_title_icon">رمز التطبيق</string> <string name="address_section_title">عنوان</string> - <string name="allow_your_contacts_irreversibly_delete">اسمح لجهات اتصالك بحذف الرسائل المرسلة بشكل لا رجعة فيه.</string> + <string name="allow_your_contacts_irreversibly_delete">اسمح لجهات اتصالك بحذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة)</string> <string name="auth_unavailable">المصادقة غير متاحة</string> <string name="back">رجوع</string> <string name="invite_prohibited">لا يمكن دعوة جهة اتصال!</string> <string name="icon_descr_cancel_image_preview">إلغاء معاينة الصورة</string> <string name="use_camera_button">الكاميرا</string> - <string name="icon_descr_cancel_link_preview">إلغاء معاينة الارتباط</string> + <string name="icon_descr_cancel_link_preview">إلغاء معاينة الروابط</string> <string name="network_session_mode_user_description"><![CDATA[سيتم استخدام اتصال TCP منفصل (وبيانات اعتماد SOCKS) <b> لكل ملف تعريف دردشة لديك في التطبيق </b>.]]></string> <string name="feature_cancelled_item">ألغيت %s</string> <string name="one_time_link_short">رابط لمرة واحدة</string> @@ -96,15 +96,15 @@ <string name="both_you_and_your_contact_can_add_message_reactions">يمكنك أنت وجهة اتصالك إضافة ردود فعل الرسائل.</string> <string name="both_you_and_your_contact_can_send_disappearing">يمكنك أنت وجهة اتصالك إرسال رسائل تختفي.</string> <string name="icon_descr_call_progress">مكالمتك تحت الإجراء</string> - <string name="cannot_receive_file">لا يمكن استقبال الملف</string> - <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b> جيد للبطارية </b>. خدمة الخلفية تتحقق من الرسائل كل 10 دقائق. قد تفوتك مكالمات أو رسائل عاجلة.]]></string> + <string name="cannot_receive_file">لا يمكن استلام الملف</string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>جيد للبطارية</b>. يتحقق التطبيق من الرسائل كل 10 دقائق. قد تفوتك مكالمات أو رسائل عاجلة.]]></string> <string name="bold_text">عريض</string> - <string name="audio_call_no_encryption">مكالمات الصوت (ليست مشفرة بين الطرفين)</string> - <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b> الأفضل للبطارية </b>. ستتلقى إشعارات فقط عندما يكون التطبيق قيد التشغيل (لا توجد خدمة في الخلفية).]]></string> - <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b> تستهلك المزيد من البطارية </b>! تعمل خدمة الخلفية دائمًا - تظهر الإشعارات بمجرد توفر الرسائل.]]></string> + <string name="audio_call_no_encryption">مكالمات الصوت (ليست مُعمّاة بين الطرفين)</string> + <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b> الأفضل للبطارية </b>. ستستلم إشعارات فقط عندما يكون التطبيق قيد التشغيل (لا توجد خدمة في الخلفية).]]></string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>يستهلك المزيد من البطارية</b>! يعمل التطبيق دائمًا في الخلفية - يتم عرض الإشعارات على الفور.]]></string> <string name="call_already_ended">انتهت المكالمة بالفعل!</string> <string name="alert_title_msg_bad_hash">تجزئة رسالة سيئة</string> - <string name="integrity_msg_bad_id">معرف رسالة سيئ</string> + <string name="integrity_msg_bad_id">معرّف رسالة سيئ</string> <string name="icon_descr_call_ended">انتهت المكالمة</string> <string name="change_verb">تغير</string> <string name="color_primary_variant">لون إضافي ثانوي</string> @@ -115,16 +115,15 @@ <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b> يمكن تعطيله عبر الإعدادات</b> - سيستمر عرض الإشعارات أثناء تشغيل التطبيق.]]></string> <string name="settings_audio_video_calls">مكالمات الصوت والفيديو</string> <string name="impossible_to_recover_passphrase"><![CDATA[<b>يُرجى الملاحظة</b>: لن تتمكن من استعادة عبارة المرور أو تغييرها في حالة فقدها.]]></string> - <string name="both_you_and_your_contacts_can_delete">يمكنك أنت وجهة اتصالك حذف الرسائل المرسلة بشكل لا رجعة فيه.</string> + <string name="both_you_and_your_contacts_can_delete">يمكنك أنت وجهة اتصالك حذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة)</string> <string name="v4_2_auto_accept_contact_requests">قبول طلبات الاتصال تلقائيًا</string> <string name="la_auth_failed">فشلت المصادقة</string> <string name="la_authenticate">مصادقة</string> <string name="send_disappearing_message_1_minute">1 دقيقة</string> <string name="send_disappearing_message_30_seconds">30 ثانية</string> - <string name="icon_descr_cancel_live_message">إلغاء الرسالة المباشرة</string> + <string name="icon_descr_cancel_live_message">ألغِ الرسالة الحيّة</string> <string name="cancel_verb">إلغاء</string> - <string name="network_session_mode_entity_description">سيتم استخدام اتصال TCP منفصل (وبيانات اعتماد SOCKS) <b> لكل جهة اتصال وعضو في المجموعة</b>. -\n<b> الرجاء ملاحظة</b>: إذا كان لديك العديد من التوصيلات ، فقد يكون استهلاك البطارية وحركة المرور أعلى بكثير وقد تفشل بعض الاتصالات.</string> + <string name="network_session_mode_entity_description"><![CDATA[سيتم استخدام اتصال TCP منفصل (وبيانات اعتماد SOCKS) <b> لكل جهة اتصال وعضو في المجموعة</b>\n.<b> الرجاء ملاحظة</b>: إذا كان لديك العديد من الاتصالات، فقد يكون استهلاك البطارية وحركة المرور أعلى بكثير وقد تفشل بعض الاتصالات.]]></string> <string name="callstatus_calling">جارٍ الاتصال…</string> <string name="icon_descr_audio_call">مكالمة صوتية</string> <string name="call_on_lock_screen">المكالمات على شاشة القفل:</string> @@ -132,23 +131,20 @@ <string name="icon_descr_audio_on">الصوت مفعل</string> <string name="auto_accept_images">قبول تلقائي للصور</string> <string name="integrity_msg_bad_hash">تجزئة رسالة سيئة</string> - <string name="alert_title_msg_bad_id">معرف رسالة سيئ</string> + <string name="alert_title_msg_bad_id">معرّف رسالة سيئ</string> <string name="app_passcode_replaced_with_self_destruct">يتم استبدال رمز مرور التطبيق برمز مرور التدمير الذاتي.</string> <string name="v4_6_audio_video_calls">مكالمات الصوت والفيديو</string> <string name="callstatus_error">خطأ في الاتصال</string> - <string name="turning_off_service_and_periodic">تحسين البطارية نشط ، مما يؤدي إلى إيقاف تشغيل خدمة الخلفية والطلبات الدورية للرسائل الجديدة. يمكنك إعادة تمكينها عبر الإعدادات.</string> + <string name="turning_off_service_and_periodic">تحسين البطارية نشط، مما يؤدي إلى إيقاف تشغيل خدمة الخلفية والطلبات الدورية للرسائل الجديدة. يمكنك إعادة تفعيلها عبر الإعدادات.</string> <string name="database_initialization_error_title">لا يمكن تهيئة قاعدة البيانات</string> <string name="attach">إرفاق</string> <string name="icon_descr_asked_to_receive">طلب لاستلام الصورة</string> - <string name="app_version_name">نسخة التطبيق: v%s</string> + <string name="app_version_name">إصدار التطبيق: v%s</string> <string name="auto_accept_contact">قبول تلقائي</string> <string name="settings_section_title_calls">المكالمات</string> <string name="alert_title_cant_invite_contacts">لا يمكن دعوة جهات الاتصال!</string> <string name="rcv_conn_event_switch_queue_phase_completed">تم تغيير العنوان من أجلك</string> - <string name="cant_delete_user_profile">لا يمكن حذف ملف تعريف المستخدم!</string> <string name="icon_descr_video_asked_to_receive">طلب لاستلام الفيديو</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b> إضافة جهة اتصال جديدة </b>: لإنشاء رمز الاستجابة السريعة الخاص بك لمرة واحدة لجهة اتصالك.]]></string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b> امسح رمز الاستجابة السريعة </b>: للاتصال بجهة الاتصال التي تعرض لك رمز الاستجابة السريعة.]]></string> <string name="callstatus_in_progress">مكالمتك تحت الإجراء</string> <string name="change_database_passphrase_question">تغيير عبارة مرور قاعدة البيانات؟</string> <string name="cannot_access_keychain">لا يمكن الوصول إلى Keystore لحفظ كلمة مرور قاعدة البيانات</string> @@ -173,20 +169,20 @@ <string name="copy_verb">نسخ</string> <string name="connect_via_link_verb">اتصل</string> <string name="server_connected">متصل</string> - <string name="connect_via_group_link">تواصل عبر رابط جماعي؟</string> - <string name="connect_via_invitation_link">تواصل عبر رابط دعوة؟</string> + <string name="connect_via_group_link">انضمام إلى المجموعة؟</string> + <string name="connect_via_invitation_link">اتصل عبر رابط لمرة واحدة؟</string> <string name="switch_receiving_address_question">تغيير عنوان الاستلام؟</string> <string name="copied">نٌسخت إلى الحافظة</string> <string name="clear_verb">مسح</string> - <string name="clear_chat_button">مسح الدردشة</string> + <string name="clear_chat_button">امسح الدردشة</string> <string name="create_address">إنشاء عنوان</string> <string name="settings_section_title_chats">الدردشات</string> <string name="confirm_new_passphrase">تأكيد عبارة المرور الجديدة…</string> - <string name="encrypt_database_question">تشفير قاعدة بيانات</string> - <string name="encrypted_database">قاعدة بيانات مشفرة</string> + <string name="encrypt_database_question">تعمية قاعدة البيانات؟</string> + <string name="encrypted_database">قاعدة البيانات مُعمّاة</string> <string name="rcv_group_event_changed_member_role">غيرت دور %s إلى %s</string> <string name="switch_receiving_address">تغيير عنوان الاستلام</string> - <string name="failed_to_create_user_title">خطأ في إنشاء الملف الشخصي!</string> + <string name="failed_to_create_user_title">خطأ في إنشاء ملف التعريف!</string> <string name="connection_error">خطأ في الإتصال</string> <string name="connection_timeout">انتهت مهلة الاتصال</string> <string name="contact_already_exists">جهة الاتصال موجودة بالفعل</string> @@ -198,11 +194,10 @@ <string name="smp_server_test_compare_file">قارن الملف</string> <string name="icon_descr_server_status_error">خطأ</string> <string name="create_group">إنشاء مجموعة سرية</string> - <string name="create_one_time_link">إنشاء رابط دعوة لمرة واحدة</string> <string name="error_aborting_address_change">خطأ في إحباط تغيير العنوان</string> <string name="auth_enable_simplex_lock">تفعيل قفل SimpleX</string> - <string name="auth_confirm_credential">تأكد من بيانات الاعتماد الخاصة بك</string> - <string name="create_simplex_address">إنشاء عنوان SimpleX</string> + <string name="auth_confirm_credential">تأكد من بيانات اعتمادك</string> + <string name="create_simplex_address">أنشئ عنوان SimpleX</string> <string name="continue_to_next_step">متابعة</string> <string name="chat_with_developers">تحدث مع المطورين</string> <string name="icon_descr_context">سياق الأيقونة</string> @@ -223,7 +218,7 @@ <string name="enable_lock">تفعيل القفل</string> <string name="confirm_passcode">تأكيد رمز المرور</string> <string name="error_deleting_database">خطأ في حذف قاعدة بيانات الدردشة</string> - <string name="error_encrypting_database">خطأ في تشفير قاعدة بيانات</string> + <string name="error_encrypting_database">خطأ في تعمية قاعدة البيانات</string> <string name="chat_is_stopped_indication">توقفت الدردشة</string> <string name="group_member_status_complete">مكتمل</string> <string name="group_member_status_announced">جاري الاتصال (أعلن)</string> @@ -234,30 +229,27 @@ <string name="v4_6_chinese_spanish_interface">الواجهة الصينية والاسبانية</string> <string name="clear_chat_menu_action">مسح</string> <string name="contact_wants_to_connect_via_call">%1$s يريد التواصل معك عبر</string> - <string name="snd_conn_event_switch_queue_phase_changing">تغيير العنوان…</string> - <string name="snd_conn_event_switch_queue_phase_changing_for_member">تغيير العنوان ل%s…</string> + <string name="snd_conn_event_switch_queue_phase_changing">جارِ تغيير العنوان…</string> + <string name="snd_conn_event_switch_queue_phase_changing_for_member">جارِ تغيير العنوان ل%s…</string> <string name="allow_to_send_files">السماح بإرسال الملفات والوسائط.</string> <string name="enter_welcome_message_optional">أدخل رسالة ترحيب… (اختياري)</string> - <string name="snd_conn_event_ratchet_sync_agreed">وافق التشفير ل%s</string> - <string name="snd_conn_event_ratchet_sync_allowed">سمح بإعادة التفاوض على التشفير ل%s</string> + <string name="snd_conn_event_ratchet_sync_agreed">وافق التعمية ل%s</string> + <string name="snd_conn_event_ratchet_sync_allowed">سمح بإعادة التفاوض على التعمية ل%s</string> <string name="error_accepting_contact_request">خطأ في قبول طلب جهة الاتصال</string> - <string name="status_contact_has_no_e2e_encryption">ليس لدى جهة الاتصال التشفير بين الطريفين</string> + <string name="status_contact_has_no_e2e_encryption">ليس لدى جهة الاتصال التعمية بين الطريفين</string> <string name="change_self_destruct_mode">تغيير وضع التدمير الذاتي</string> <string name="change_self_destruct_passcode">تغيير رمز المرور التدمير الذاتي</string> <string name="confirm_database_upgrades">تأكيد ترقيات قاعدة البيانات</string> - <string name="chat_archive_header">أرشيف الدردشة</string> <string name="group_member_status_intro_invitation">الاتصال (دعوة مقدمة)</string> <string name="clear_contacts_selection_button">مسح</string> - <string name="error_creating_link_for_group">خطأ في إنشاء ارتباط المجموعة</string> + <string name="error_creating_link_for_group">خطأ في إنشاء رابط المجموعة</string> <string name="item_info_current">(حاضِر)</string> - <string name="network_option_enable_tcp_keep_alive">تمكين بقاء TCP على قيد الحياة</string> + <string name="network_option_enable_tcp_keep_alive">تفعيل أبقِ TCP على قيد الحياة</string> <string name="contact_connection_pending">جار الاتصال…</string> <string name="group_connection_pending">جار الاتصال…</string> <string name="connection_request_sent">أرسلت طلب الاتصال!</string> <string name="chat_database_deleted">حُذفت قاعدة بيانات الدردشة</string> - <string name="chat_archive_section">أرشيف الدردشة</string> - <string name="archive_created_on_ts">نشأ في %1$s</string> - <string name="rcv_conn_event_switch_queue_phase_changing">تغيير العنوان…</string> + <string name="rcv_conn_event_switch_queue_phase_changing">جارِ تغيير العنوان…</string> <string name="group_member_status_accepted">جار الاتصال (قُبِل)</string> <string name="icon_descr_contact_checked">فُحصت جهة الاتصال</string> <string name="group_info_section_title_num_members">%1$s أعضاء</string> @@ -276,7 +268,7 @@ <string name="chat_preferences">تفضيلات الدردشة</string> <string name="enter_correct_passphrase">أدخل عبارة المرور الصحيحة.</string> <string name="rcv_group_event_member_connected">متصل</string> - <string name="connect_via_contact_link">تواصل عبر رابط الاتصال؟</string> + <string name="connect_via_contact_link">اتصل عبر عنوان التواصل؟</string> <string name="error_deleting_link_for_group">خطأ في حذف رابط المجموعة</string> <string name="notifications_mode_periodic_desc">التحقق من الرسائل الجديدة كل 10 دقائق لمدة تصل إلى دقيقة واحدة</string> <string name="server_connecting">جار الاتصال</string> @@ -299,7 +291,7 @@ <string name="group_member_status_creator">المنشئ</string> <string name="error_adding_members">خطأ في إضافة الأعضاء</string> <string name="error_creating_address">خطأ في إنشاء العنوان</string> - <string name="error_deleting_pending_contact_connection">خطأ في حذف اتصال جهة الاتصال المعلق</string> + <string name="error_deleting_pending_contact_connection">خطأ في حذف اتصال جهة الاتصال المنتظر</string> <string name="enter_welcome_message">أدخل رسالة ترحيب…</string> <string name="group_member_status_connected">متصل</string> <string name="group_member_status_connecting">جار الاتصال</string> @@ -311,34 +303,34 @@ <string name="la_enter_app_passcode">أدخل عبارة المرور</string> <string name="your_chats">الدردشات</string> <string name="icon_descr_server_status_connected">متصل</string> - <string name="delete_contact_all_messages_deleted_cannot_undo_warning">سيتم حذف جهة الاتصال وجميع الرسائل - لا يمكن التراجع عن هذا الإجراء!</string> + <string name="delete_contact_all_messages_deleted_cannot_undo_warning">سيتم حذف جهة الاتصال وجميع الرسائل - لا يمكن التراجع عن هذا!</string> <string name="maximum_supported_file_size">الحد الأقصى لحجم الملف المدعوم حاليًا هو %1$s.</string> <string name="connect_via_link_or_qr">تواصل عبر الرابط / رمز QR</string> <string name="share_one_time_link">إنشاء رابط دعوة لمرة واحدة</string> <string name="smp_servers_check_address">تحقق من عنوان الخادم وحاول مرة أخرى.</string> - <string name="clear_verification">مسح التَحَقُّق</string> + <string name="clear_verification">امسح التحقُّق</string> <string name="create_address_and_let_people_connect">أنشئ عنوانًا للسماح للأشخاص بالتواصل معك.</string> <string name="smp_servers_enter_manually">أدخل الخادم يدويًا</string> <string name="colored_text">ملون</string> - <string name="status_contact_has_e2e_encryption">لدى جهة الاتصال التشفير بين الطريفين</string> + <string name="status_contact_has_e2e_encryption">لدى جهة الاتصال التعمية بين الطريفين</string> <string name="create_profile_button">إنشاء</string> - <string name="create_your_profile">إنشاء حسابك الشخصي</string> + <string name="create_your_profile">أنشئ ملف تعريفك</string> <string name="icon_descr_call_connecting">مكالمة جارية...</string> <string name="enable_self_destruct">تفعيل التدمير الذاتي</string> - <string name="conn_event_ratchet_sync_started">الموافقة على التشفير…</string> - <string name="snd_conn_event_ratchet_sync_started">الموافقة على التشفير لـ%s…</string> + <string name="conn_event_ratchet_sync_started">الموافقة على التعمية…</string> + <string name="snd_conn_event_ratchet_sync_started">الموافقة على التعمية لـ%s…</string> <string name="group_member_status_introduced">متصل (مقدم)</string> - <string name="conn_event_ratchet_sync_agreed">وافق التشفير</string> - <string name="conn_event_ratchet_sync_ok">التشفير نعم</string> - <string name="snd_conn_event_ratchet_sync_ok">التشفير نعم ل%s</string> - <string name="conn_event_ratchet_sync_allowed">سمح بإعادة التفاوض على التشفير</string> - <string name="conn_event_ratchet_sync_required">مطلوب إعادة التفاوض على التشفير</string> - <string name="snd_conn_event_ratchet_sync_required">مطلوب إعادة التفاوض على التشفير ل%s</string> + <string name="conn_event_ratchet_sync_agreed">وافق التعمية</string> + <string name="conn_event_ratchet_sync_ok">التعمية نعم</string> + <string name="snd_conn_event_ratchet_sync_ok">التعمية نعم ل%s</string> + <string name="conn_event_ratchet_sync_allowed">سمح بإعادة التفاوض على التعمية</string> + <string name="conn_event_ratchet_sync_required">مطلوب إعادة التفاوض على التعمية</string> + <string name="snd_conn_event_ratchet_sync_required">مطلوب إعادة التفاوض على التعمية ل%s</string> <string name="error_changing_message_deletion">خطأ في تغيير الإعداد</string> <string name="error_changing_role">خطأ في تغيير الدور</string> - <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d فشل فك تشفير الرسائل.</string> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d فشل فك تعمية الرسائل.</string> <string name="dark_theme">سمة داكنة</string> - <string name="deleted_description">حُذِفت</string> + <string name="deleted_description">حُذفت</string> <string name="database_passphrase_and_export">عبارة مرور قاعدة البيانات وتصديرها</string> <string name="delete_files_and_media_all">حذف جميع الملفات</string> <string name="delete_after">حذف بعد</string> @@ -348,24 +340,23 @@ <string name="delete_group_menu_action">حذف</string> <string name="delete_messages">حذف الرسائل</string> <string name="delete_messages_after">حذف الرسائل بعد</string> - <string name="database_encrypted">قاعدة بيانات مشفرة!</string> + <string name="database_encrypted">قاعدة البيانات مُعمّاة!</string> <string name="passphrase_is_different">تختلف عبارة مرور قاعدة البيانات عن تلك المحفوظة في Keystore.</string> <string name="database_error">خطأ في قاعدة البيانات</string> <string name="database_upgrade">ترقية قاعدة البيانات</string> - <string name="delete_chat_archive_question">حذف أرشيف الدردشة؟</string> <string name="num_contacts_selected">حُددت %d جهة اتصال</string> <string name="button_delete_group">حذف المجموعة</string> <string name="delete_group_question">حذف المجموعة؟</string> <string name="delete_link">حذف الرابط</string> - <string name="share_text_deleted_at">حُذِفت في: %s</string> - <string name="rcv_group_event_group_deleted">المجموعة المحذوفة</string> + <string name="share_text_deleted_at">حُذفت في: %s</string> + <string name="rcv_group_event_group_deleted">المجموعة حُذفت</string> <string name="delete_image">حذف الصورة</string> <string name="v5_1_custom_themes">تخصيص السمات</string> <string name="delete_database">حذف قاعدة البيانات</string> <string name="delete_chat_profile_question">حذف ملف تعريف الدردشة؟</string> <string name="delete_files_and_media_for_all_users">حذف الملفات لجميع ملفات تعريف الدردشة</string> - <string name="encrypted_with_random_passphrase">قاعدة البيانات مشفرة باستخدام عبارة مرور عشوائية، يمكنك تغييرها.</string> - <string name="database_will_be_encrypted_and_passphrase_stored">سيتم تشفير قاعدة البيانات وتخزين عبارة المرور في Keystore.</string> + <string name="encrypted_with_random_passphrase">قاعدة البيانات مُعمّاة باستخدام عبارة مرور عشوائية، يمكنك تغييرها.</string> + <string name="database_will_be_encrypted_and_passphrase_stored">سيتم تعمية قاعدة البيانات وتخزين عبارة المرور في Keystore.</string> <string name="database_passphrase_is_required">عبارة مرور قاعدة البيانات مطلوبة لفتح الدردشة.</string> <string name="mtr_error_no_down_migration">إصدار قاعدة البيانات أحدث من التطبيق، ولكن لا يوجد ترحيل لأسفل ل%s</string> <string name="share_text_database_id">معرّف قاعدة البيانات: %d</string> @@ -374,13 +365,12 @@ <string name="full_deletion">حذف للجميع</string> <string name="custom_time_unit_days">أيام</string> <string name="delete_address">حذف العنوان</string> - <string name="database_passphrase_will_be_updated">سيتم تحديث عبارة مرور تشفير قاعدة البيانات.</string> - <string name="delete_archive">حذف الأرشيف</string> + <string name="database_passphrase_will_be_updated">سيتم تحديث عبارة مرور تعمية قاعدة البيانات.</string> <string name="delete_link_question">حذف الرابط؟</string> <string name="database_downgrade">الرجوع إلى إصدار سابق من قاعدة البيانات</string> - <string name="set_password_to_export_desc">يتم تشفير قاعدة البيانات باستخدام عبارة مرور عشوائية. يرجى تغييره قبل التصدير.</string> + <string name="set_password_to_export_desc">قاعدة البيانات مُعمّاة باستخدام عبارة مرور عشوائية. يُرجى تغييره قبل التصدير.</string> <string name="ttl_day">%d يوم</string> - <string name="database_will_be_encrypted">سيتم تشفير قاعدة البيانات.</string> + <string name="database_will_be_encrypted">سيتم تعمية قاعدة البيانات.</string> <string name="delete_contact_menu_action">حذف</string> <string name="delete_files_and_media_question">حذف الملفات والوسائط؟</string> <string name="button_delete_contact">حذف جهة الاتصال</string> @@ -389,7 +379,7 @@ <string name="chat_preferences_default">الافتراضي %s</string> <string name="delete_pending_connection__question">حذف الاتصال قيد الانتظار؟</string> <string name="delete_chat_profile">حذف ملف تعريف الدردشة</string> - <string name="decryption_error">خطأ في فك التشفير</string> + <string name="decryption_error">خطأ في فك التعمية</string> <string name="delete_message__question">حذف الرسالة؟</string> <string name="developer_options">معرفات قاعدة البيانات وخيار عزل النقل.</string> <string name="delete_address__question">حذف العنوان؟</string> @@ -400,9 +390,9 @@ <string name="decentralized">لامركزي</string> <string name="database_passphrase">عبارة مرور قاعدة البيانات</string> <string name="current_passphrase">عبارة المرور الحالية…</string> - <string name="database_encryption_will_be_updated">سيتم تحديث عبارة مرور تشفير قاعدة البيانات وتخزينها في Keystore.</string> + <string name="database_encryption_will_be_updated">سيتم تحديث عبارة مرور تعمية قاعدة البيانات وتخزينها في Keystore.</string> <string name="info_row_database_id">معرّف قاعدة البيانات</string> - <string name="info_row_deleted_at">حُذِفت في</string> + <string name="info_row_deleted_at">حُذفت في</string> <string name="ttl_d">%d يوم</string> <string name="ttl_days">%d أيام</string> <string name="custom_time_picker_custom">مخصص</string> @@ -414,16 +404,16 @@ <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 سطح المكتب: امسح رمز الاستجابة السريعة (QR) المعروض من التطبيق، عبر <b>مسح رمز QR</b>.]]></string> <string name="delete_profile">حذف ملف التعريف</string> <string name="smp_servers_delete_server">حذف الخادم</string> - <string name="error_updating_link_for_group">خطأ في تحديث ارتباط المجموعة</string> + <string name="error_updating_link_for_group">خطأ في تحديث رابط المجموعة</string> <string name="simplex_link_mode_description">الوصف</string> - <string name="icon_descr_expand_role">توسيع اختيار الدور</string> + <string name="icon_descr_expand_role">توسيع تحديد الدور</string> <string name="group_invitation_expired">انتهت صلاحية دعوة المجموعة</string> <string name="alert_title_no_group">المجموعة غير موجودة!</string> - <string name="export_theme">تصدير السمة</string> + <string name="export_theme">صدّر السمة</string> <string name="files_and_media">الملفات والوسائط</string> <string name="icon_descr_flip_camera">قلب الكاميرا</string> <string name="delete_group_for_all_members_cannot_undo_warning">سيتم حذف المجموعة لجميع الأعضاء - لا يمكن التراجع عن هذا!</string> - <string name="group_members_can_send_dms">يمكن لأعضاء المجموعة إرسال رسائل مباشرة.</string> + <string name="group_members_can_send_dms">يمكن للأعضاء إرسال رسائل مباشرة.</string> <string name="failed_to_parse_chats_title">فشل تحميل الدردشات</string> <string name="email_invite_body">أهلاً! \nتواصل معي عبر SimpleX Chat: %s</string> @@ -431,9 +421,9 @@ <string name="files_and_media_prohibited">الملفات والوسائط ممنوعة!</string> <string name="icon_descr_file">الملف</string> <string name="snd_group_event_group_profile_updated">حُدّث ملف تعريف المجموعة</string> - <string name="group_display_name_field">اسم عرض المجموعة:</string> - <string name="group_members_can_send_voice">يمكن لأعضاء المجموعة إرسال رسائل صوتية.</string> - <string name="files_are_prohibited_in_group">الملفات والوسائط ممنوعة في هذه المجموعة.</string> + <string name="group_display_name_field">أدخل اسم المجموعة:</string> + <string name="group_members_can_send_voice">يمكن للأعضاء إرسال رسائل صوتية.</string> + <string name="files_are_prohibited_in_group">الملفات والوسائط ممنوعة.</string> <string name="v4_6_group_welcome_message">رسالة ترحيب المجموعة</string> <string name="v4_6_reduced_battery_usage">مزيد من تقليل استخدام البطارية</string> <string name="info_row_group">المجموعة</string> @@ -443,16 +433,16 @@ <string name="v4_4_french_interface">الواجهة الفرنسية</string> <string name="settings_section_title_help">المساعدة</string> <string name="group_member_status_group_deleted">حُذِفت المجموعة</string> - <string name="group_members_can_send_disappearing">يمكن لأعضاء المجموعة إرسال رسائل تختفي.</string> + <string name="group_members_can_send_disappearing">يمكن للأعضاء إرسال رسائل تختفي.</string> <string name="v4_6_group_moderation">إشراف المجموعة</string> <string name="v5_1_message_reactions_descr">أخيرا، لدينا منهم! 🚀</string> - <string name="export_database">تصدير قاعدة البيانات</string> + <string name="export_database">صدّر قاعدة البيانات</string> <string name="section_title_for_console">لوحدة التحكم</string> <string name="settings_experimental_features">الميزات التجريبية</string> <string name="settings_section_title_experimenta">تجريبي</string> <string name="icon_descr_group_inactive">المجموعة غير نشطة</string> <string name="files_and_media_section">الملفات والوسائط</string> - <string name="group_members_can_delete">يمكن لأعضاء المجموعة حذف الرسائل المرسلة بشكل لا رجعة فيه.</string> + <string name="group_members_can_delete">يمكن للأعضاء حذف الرسائل المُرسلة بشكل لا رجعة فيه. (24 ساعة)</string> <string name="fix_connection_not_supported_by_contact">الإصلاح غير مدعوم من قبل جهة الاتصال</string> <string name="group_profile_is_stored_on_members_devices">يُخزّن ملف تعريف المجموعة على أجهزة الأعضاء، وليس على الخوادم.</string> <string name="v4_2_group_links">روابط المجموعة</string> @@ -460,32 +450,32 @@ <string name="full_name__field">الاسم الكامل:</string> <string name="alert_message_group_invitation_expired">لم تعد دعوة المجموعة صالحة، تمت أُزيلت بواسطة المرسل.</string> <string name="group_link">رابط المجموعة</string> - <string name="file_will_be_received_when_contact_is_online">سيتم استلام الملف عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string> + <string name="file_will_be_received_when_contact_is_online">سيتم استلام الملف عندما تكون جهة اتصالك متصلة بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا!</string> <string name="group_full_name_field">الاسم الكامل للمجموعة:</string> <string name="simplex_link_mode_full">رابط كامل</string> - <string name="choose_file">الملف</string> + <string name="choose_file">ملف</string> <string name="delete_group_for_self_cannot_undo_warning">سيتم حذف المجموعة لك - لا يمكن التراجع عن هذا!</string> <string name="failed_to_parse_chat_title">فشل تحميل الدردشة</string> - <string name="group_members_can_add_message_reactions">يمكن لأعضاء المجموعة إضافة ردود فعل الرسالة.</string> + <string name="group_members_can_add_message_reactions">يمكن للأعضاء إضافة ردود الفعل على الرسائل.</string> <string name="favorite_chat">المفضل</string> <string name="notification_preview_mode_hidden">مخفي</string> <string name="file_saved">حُفظ الملف</string> <string name="revoke_file__message">سيتم حذف الملف من الخوادم.</string> - <string name="file_will_be_received_when_contact_completes_uploading">سيتم استلام الملف عند اكتمال تحميل جهة الاتصال الخاصة بك.</string> + <string name="file_will_be_received_when_contact_completes_uploading">سيتم استلام الملف عندما يكتمل جهة اتصالك من رفعِها.</string> <string name="icon_descr_help">المساعدة</string> <string name="file_with_path">الملف: %s</string> - <string name="fix_connection_confirm">إصلاح</string> - <string name="fix_connection">إصلاح الاتصال</string> + <string name="fix_connection_confirm">أصلح</string> + <string name="fix_connection">أصلح الاتصال</string> <string name="fix_connection_question">إصلاح الاتصال؟</string> <string name="fix_connection_not_supported_by_group_member">الإصلاح غير مدعوم من قبل أعضاء المجموعة</string> - <string name="group_members_can_send_files">يمكن لأعضاء المجموعة إرسال الملفات والوسائط.</string> + <string name="group_members_can_send_files">يمكن للأعضاء إرسال الملفات والوسائط.</string> <string name="group_preferences">تفضيلات المجموعة</string> <string name="v5_0_large_files_support_descr">سريع ولا تنتظر حتى يصبح المرسل متصلاً بالإنترنت!</string> <string name="hide_verb">إخفاء</string> <string name="how_to_use_simplex_chat">كيفية الاستخدام</string> <string name="how_simplex_works">كيف يعمل SimpleX</string> <string name="description_via_contact_address_link_incognito">التخفي عبر رابط عنوان جهة الاتصال</string> - <string name="incorrect_code">رمز الحماية غير صحيحة!</string> + <string name="incorrect_code">رمز الأمان غير صحيحة!</string> <string name="service_notifications_disabled">الإشعارات الفورية مُعطَّلة</string> <string name="service_notifications">إشعارات فورية!</string> <string name="notification_display_mode_hidden_desc">إخفاء جهة الاتصال والرسالة</string> @@ -493,8 +483,8 @@ <string name="import_database">استيراد قاعدة بيانات</string> <string name="custom_time_unit_hours">ساعات</string> <string name="edit_history">السجل</string> - <string name="image_will_be_received_when_contact_completes_uploading">سيتم استلام الصورة عند اكتمال تحميل جهة اتصالك.</string> - <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[إذا لم تتمكن من الالتقاء شخصيًا، <b>اعرض رمز الاستجابة السريعة في مكالمة الفيديو</b>، أو شارك الرابط.]]></string> + <string name="image_will_be_received_when_contact_completes_uploading">سيتم استلام الصورة عندما يكتمل جهة اتصالك من رفعِها.</string> + <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[إذا لم تتمكن من الالتقاء شخصيًا، <b>اعرض رمز QR في مكالمة الفيديو</b>، أو شارك الرابط.]]></string> <string name="install_simplex_chat_for_terminal">ثبّت SimpleX Chat لطرفية</string> <string name="network_disable_socks_info">إذا قمت بالتأكيد، فستتمكن خوادم المراسلة من رؤية عنوان IP الخاص بك ومزود الخدمة الخاص بك - أي الخوادم التي تتصل بها.</string> <string name="hide_dev_options">إخفاء:</string> @@ -511,20 +501,20 @@ <string name="conn_level_desc_indirect">غير مباشر (%1$s)</string> <string name="how_it_works">آلية العمل</string> <string name="incoming_video_call">مكالمة فيديو واردة</string> - <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">إذا تلقيت رابط دعوة SimpleX Chat، فيمكنك فتحه في متصفحك:</string> + <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">إذا استلمت رابط دعوة SimpleX Chat، فيمكنك فتحه في متصفحك:</string> <string name="incognito_info_protects">يحمي وضع التخفي خصوصيتك بأستخدام ملف تعريف عشوائي جديد لكل جهة اتصال جديدة.</string> - <string name="info_menu">المعلومات</string> + <string name="info_menu">معلومات</string> <string name="v4_3_improved_privacy_and_security_desc">إخفاء شاشة التطبيق في التطبيقات الحديثة.</string> <string name="v4_3_improved_privacy_and_security">تحسن الخصوصية والأمان</string> <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[إذا لم تتمكن من الالتقاء شخصيًا، فيمكنك <b>مسح رمز QR في مكالمة الفيديو</b>، أو يمكن لجهة الاتصال مشاركة رابط الدعوة.]]></string> - <string name="immune_to_spam_and_abuse">محصن ضد البريد العشوائي وسوء المعاملة</string> + <string name="immune_to_spam_and_abuse">محصن ضد الإزعاج (spam)</string> <string name="description_via_one_time_link_incognito">التخفي عبر رابط لمرة واحدة</string> <string name="icon_descr_image_snd_complete">أرسلت صورة</string> <string name="image_descr">صورة</string> - <string name="image_will_be_received_when_contact_is_online">سيتم استلام الصورة عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string> + <string name="image_will_be_received_when_contact_is_online">سيتم استلام الصورة عندما تكون جهة اتصالك متصلة بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا!</string> <string name="image_saved">حُفظت الصورة في المعرض</string> <string name="gallery_image_button">صورة</string> - <string name="if_you_cant_meet_in_person">إذا لم تتمكن من الالتقاء شخصيًا، اعرض رمز الاستجابة السريعة في مكالمة الفيديو، أو شارك الرابط.</string> + <string name="if_you_cant_meet_in_person">إذا لم تتمكن من الالتقاء شخصيًا، اعرض رمز QR في مكالمة الفيديو، أو شارك الرابط.</string> <string name="settings_section_title_incognito">وضع التخفي</string> <string name="incognito">التخفي</string> <string name="import_theme_error">خطأ في استيراد السمة</string> @@ -536,7 +526,7 @@ <string name="onboarding_notifications_mode_service">فوري</string> <string name="host_verb">المضيف</string> <string name="hide_notification">إخفاء</string> - <string name="turn_off_battery_optimization"><![CDATA[من أجل استخدامها، <b>يرجى السماح لSimpleX للتشغيل في الخلفية</b> في مربع الحوار التالي. وإلا، سيتم تعطيل الإخطارات.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[<b>السماح بذلك</b> في مربع الحوار التالي لتلقي الإشعارات على الفور.]]></string> <string name="in_reply_to">ردًا على</string> <string name="icon_descr_instant_notifications">إشعارات فورية</string> <string name="enter_one_ICE_server_per_line">خوادم ICE (واحد لكل سطر)</string> @@ -544,7 +534,7 @@ <string name="hide_profile">إخفاء ملف التعريف</string> <string name="how_to_use_markdown">كيفية استخدام ماركداون</string> <string name="if_you_enter_self_destruct_code">إذا أدخلت رمز مرور التدمير الذاتي أثناء فتح التطبيق:</string> - <string name="onboarding_notifications_mode_subtitle">يمكن تغييره لاحقًا عبر الإعدادات.</string> + <string name="onboarding_notifications_mode_subtitle">كيف يؤثر على البطارية</string> <string name="join_group_button">انضمام</string> <string name="theme_light">فاتح</string> <string name="display_name_invited_to_connect">مدعو للتواصل</string> @@ -553,10 +543,10 @@ <string name="icon_descr_add_members">دعوة الأعضاء</string> <string name="alert_text_skipped_messages_it_can_happen_when">يمكن أن يحدث عندما: \n1. انتهت صلاحية الرسائل في العميل المرسل بعد يومين أو على الخادم بعد 30 يومًا. -\n2. فشل فك تشفير الرسالة، لأنك أو جهة اتصالك استخدمت نسخة احتياطية قديمة من قاعدة البيانات. +\n2. فشل فك تعمية الرسالة، لأنك أو جهة اتصالك استخدمت نسخة احتياطية قديمة من قاعدة البيانات. \n3. اُخترق الاتصال.</string> <string name="v5_1_japanese_portuguese_interface">واجهة أستخدام يابانية وبرتغالية</string> - <string name="alert_text_fragment_encryption_out_of_sync_old_database">يمكن أن يحدث ذلك عندما تستخدم أنت أو اتصالك النسخة الاحتياطية القديمة لقاعدة البيانات.</string> + <string name="alert_text_fragment_encryption_out_of_sync_old_database">يمكن أن يحدث ذلك عندما تستخدم أنت أو اتصالك النُسخة الاحتياطية القديمة لقاعدة البيانات.</string> <string name="group_preview_join_as">انضمام ك%s</string> <string name="invalid_QR_code">رمز QR غير صالح</string> <string name="v4_5_italian_interface">الواجهة الإيطالية</string> @@ -564,20 +554,20 @@ <string name="invite_friends">دعوة الأصدقاء</string> <string name="keychain_error">خطأ في Keychain</string> <string name="invite_to_group_button">دعوة للمجموعة</string> - <string name="message_deletion_prohibited_in_chat">يٌمنع حذف الرسائل بشكل نهائي في هذه المجموعة.</string> + <string name="message_deletion_prohibited_in_chat">يٌمنع حذف الرسائل بشكل لا رجعة فيه.</string> <string name="invalid_message_format">تنسيق الرسالة غير صالح</string> <string name="invalid_data">البيانات غير صالحة</string> - <string name="users_delete_data_only">بيانات الملف الشخصي المحلية فقط</string> - <string name="message_deletion_prohibited">يٌمنع حذف الرسائل بشكل نهائي في هذه الدردشة.</string> + <string name="users_delete_data_only">بيانات ملف التعريف المحلية فقط</string> + <string name="message_deletion_prohibited">يٌمنع حذف الرسائل بشكل لا رجعة فيه في هذه الدردشة.</string> <string name="button_add_members">دعوة الأعضاء</string> - <string name="button_leave_group">مغادرة المجموعة</string> + <string name="button_leave_group">غادِر المجموعة</string> <string name="info_row_local_name">الاسم المحلي:</string> <string name="rcv_group_event_member_left">غادر</string> <string name="incognito_info_allows">يسمح بوجود العديد من الاتصالات المجهولة دون مشاركة أي بيانات بينهم في ملف تعريف دردشة واحد.</string> <string name="rcv_group_event_member_added">مدعو %1$s</string> <string name="rcv_group_event_invited_via_your_group_link">مدعو عبر رابط المجموعة</string> <string name="invalid_chat">الدردشة غير صالحة</string> - <string name="live">حي</string> + <string name="live">حيّ</string> <string name="invalid_connection_link">رابط اتصال غير صالح</string> <string name="large_file">الملف كبير!</string> <string name="learn_more">معرفة المزيد</string> @@ -587,7 +577,7 @@ <string name="invalid_migration_confirmation">تأكيد الترحيل غير صالح</string> <string name="group_member_status_invited">مدعو</string> <string name="image_descr_link_preview">رابط معاينة الصورة</string> - <string name="live_message">رسالة مباشرة!</string> + <string name="live_message">رسالة حيّة!</string> <string name="italic_text">مائل</string> <string name="email_invite_subject">لنتحدث في SimpleX Chat</string> <string name="lock_after">قفل بعد</string> @@ -595,14 +585,14 @@ <string name="alert_title_group_invitation_expired">انتهت صلاحية الدعوة!</string> <string name="join_group_question">انضمام إلى المجموعة؟</string> <string name="join_group_incognito_button">الانضمام المتخفي</string> - <string name="joining_group">الانضمام إلى المجموعة</string> + <string name="joining_group">جارِ الانضمام إلى المجموعة</string> <string name="leave_group_button">غادِر</string> <string name="leave_group_question">مغادرة المجموعة؟</string> <string name="group_member_status_left">غادر</string> - <string name="delivery_receipts_are_disabled">إيصالات التسليم معطلة!</string> + <string name="delivery_receipts_are_disabled">إيصالات التسليم مُعطَّلة!</string> <string name="no_call_on_lock_screen">تعطيل</string> <string name="timed_messages">رسائل تختفي</string> - <string name="auth_device_authentication_is_disabled_turning_off">عٌطل مصادقة الجهاز. جاري إيقاف تشغيل قفل SimpleX.</string> + <string name="auth_device_authentication_is_disabled_turning_off">استيثاق الجهاز مُعطَّل. جارِ إيقاف تشغيل قفل SimpleX.</string> <string name="ttl_m">%d شهر</string> <string name="ttl_weeks">%d أسابيع</string> <string name="ttl_w">%d أسبوع</string> @@ -613,29 +603,29 @@ <string name="smp_server_test_download_file">نزّل الملف</string> <string name="auth_disable_simplex_lock">تعطيل قفل SimpleX</string> <string name="edit_verb">تحرير</string> - <string name="display_name__field">اسم العرض:</string> + <string name="display_name__field">اسم ملف التعريف:</string> <string name="icon_descr_email">البريد الإلكتروني</string> - <string name="display_name">اسم العرض</string> + <string name="display_name">أدخل أسمك:</string> <string name="integrity_msg_duplicate">كرر الرسالة</string> <string name="share_text_disappears_at">يختفي في: %s</string> <string name="disappearing_prohibited_in_this_chat">الرسائل المختفية ممنوعة في هذه الدردشة.</string> - <string name="status_e2e_encrypted">مشفر بين الطريفين</string> - <string name="icon_descr_edited">حُرر</string> + <string name="status_e2e_encrypted">مُعمّى بين الطريفين</string> + <string name="icon_descr_edited">حُرّر</string> <string name="downgrade_and_open_chat">الرجوع إلى إصدار سابق وفتح الدردشة</string> <string name="direct_messages">رسائل مباشرة</string> - <string name="disappearing_messages_are_prohibited">الرسائل المختفية ممنوعة في هذه المجموعة.</string> - <string name="button_edit_group_profile">تحرير ملف تعريف المجموعة</string> + <string name="disappearing_messages_are_prohibited">الرسائل المختفية ممنوعة.</string> + <string name="button_edit_group_profile">حرّر ملف تعريف المجموعة</string> <string name="dont_show_again">لا تُظهر مرة أخرى</string> <string name="settings_section_title_device">الجهاز</string> <string name="ttl_week">%d أسبوع</string> <string name="display_name_cannot_contain_whitespace">لا يمكن أن يحتوي اسم العرض على مسافة فارغة.</string> - <string name="encrypted_video_call">مكالمة فيديو مشفرة بين الطريفين</string> - <string name="direct_messages_are_prohibited_in_chat">الرسائل المباشرة بين الأعضاء ممنوعة في هذه المجموعة.</string> + <string name="encrypted_video_call">مكالمة فيديو مُعمّاة بين الطريفين</string> + <string name="direct_messages_are_prohibited_in_group">الرسائل المباشرة بين الأعضاء ممنوعة في هذه المجموعة.</string> <string name="ttl_hour">%d ساعة</string> <string name="ttl_h">%d ساعة</string> <string name="ttl_hours">%d ساعات</string> <string name="ttl_mth">%d شهر</string> - <string name="edit_image">تحرير الصورة</string> + <string name="edit_image">حرّر الصورة</string> <string name="total_files_count_and_size">%d ملف/ات بإجمالي الحجم %s</string> <string name="ttl_s">%d ثانية</string> <string name="receipts_section_contacts">جهات الاتصال</string> @@ -654,17 +644,17 @@ <string name="dont_enable_receipts">لا تُفعل</string> <string name="la_minutes">%d دقائق</string> <string name="la_seconds">%d ثواني</string> - <string name="encrypted_audio_call">مكالمة صوتية مشفرة بين الطريفين</string> + <string name="encrypted_audio_call">مكالمة صوتية مُعمّاة بين الطريفين</string> <string name="ttl_sec">%d ثانية</string> <string name="icon_descr_server_status_disconnected">قُطع الاتصال</string> <string name="disappearing_message">رسالة تختفي</string> <string name="dont_create_address">لا تنشئ عنوانًا</string> <string name="error_setting_network_config">خطأ في تحديث تضبيط الشبكة</string> <string name="error_receiving_file">خطأ في استلام الملف</string> - <string name="failed_to_active_user_title">خطأ في تبديل الملف الشخصي!</string> + <string name="failed_to_active_user_title">خطأ في تبديل ملف التعريف!</string> <string name="v5_2_fix_encryption">حافظ على اتصالاتك</string> <string name="ensure_xftp_server_address_are_correct_format_and_unique">تأكد من أن عناوين خادم XFTP بالتنسيق الصحيح، وأن تكون مفصولة بأسطر وليست مكررة.</string> - <string name="marked_deleted_description">وضع علامة \"محذوف\"</string> + <string name="marked_deleted_description">عُلّم محذوف</string> <string name="color_surface">القوائم والتنبيهات</string> <string name="error_saving_xftp_servers">خطأ في حفظ خوادم XFTP</string> <string name="error_loading_smp_servers">خطأ في تحميل خوادم SMP</string> @@ -686,44 +676,43 @@ <string name="enable_automatic_deletion_question">تفعيل الحذف التلقائي للرسائل؟</string> <string name="error_removing_member">خطأ في إزالة العضو</string> <string name="error_setting_address">خطأ في تحديد العنوان</string> - <string name="mark_read">وضع علامة القراءة</string> + <string name="mark_read">علّم مقروءة</string> <string name="receipts_contacts_enable_for_all">تفعيل للجميع</string> <string name="receipts_contacts_enable_keep_overrides">تفعيل (الاحتفاظ بالتجاوزات)</string> <string name="receipts_contacts_title_enable">تفعيل الإيصالات؟</string> <string name="error_starting_chat">خطأ في بدء الدردشة</string> <string name="error_exporting_chat_database">خطأ في تصدير قاعدة بيانات الدردشة</string> <string name="member_will_be_removed_from_group_cannot_be_undone">ستتم إزالة العضو من المجموعة - لا يمكن التراجع عن هذا!</string> - <string name="make_profile_private">اجعل الملف الشخصي خاصًا!</string> + <string name="make_profile_private">اجعل ملف التعريف خاصًا!</string> <string name="v5_2_favourites_filter_descr">تصفية الدردشات غير المقروءة والمفضلة.</string> <string name="v5_2_favourites_filter">البحث عن الدردشات بشكل أسرع</string> <string name="enable_receipts_all">تفعيل</string> <string name="v5_2_disappear_one_message_descr">حتى عندما يتم تعطيله في المحادثة.</string> - <string name="v5_2_fix_encryption_descr">إصلاح التشفير بعد استعادة النسخ الاحتياطية.</string> + <string name="v5_2_fix_encryption_descr">إصلاح التعمية بعد استعادة النُسخ الاحتياطية.</string> <string name="v5_2_disappear_one_message">اجعل رسالة واحدة تختفي</string> <string name="error_enabling_delivery_receipts">خطأ في تفعيل إيصالات التسليم!</string> <string name="error_saving_smp_servers">خطأ في حفظ خوادم SMP</string> <string name="error_sending_message">خطأ في إرسال الرسالة</string> <string name="error_joining_group">خطأ في الانضمام إلى المجموعة</string> <string name="error_synchronizing_connection">خطأ في مزامنة الاتصال</string> - <string name="auth_log_in_using_credential">تسجيل الدخول باستخدام بيانات الاعتماد الخاصة بك</string> + <string name="auth_log_in_using_credential">سجّل الدخول باستخدام بيانات اعتمادك</string> <string name="error_saving_file">خطأ في حفظ الملف</string> - <string name="mark_unread">وضع علامة غير مقروء</string> + <string name="mark_unread">علّم غير مقروءة</string> <string name="ensure_ICE_server_address_are_correct_format_and_unique">تأكد من أن عناوين خادم WebRTC ICE بالتنسيق الصحيح، وأن تكون مفصولة بأسطر وليست مكررة.</string> - <string name="mark_code_verified">وضع علامة تَحقق منه</string> + <string name="mark_code_verified">علّم تحقق منه</string> <string name="error_saving_user_password">خطأ في حفظ كلمة مرور المستخدم</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[سأل الكثير من الناس: <i>إذا SimpleX ليس لديه معرّفات مستخدم، كيف يمكنه توصيل الرسائل؟</i>]]></string> <string name="error_saving_group_profile">خطأ في حفظ ملف تعريف المجموعة</string> <string name="notification_preview_mode_message">رسالة نصية</string> <string name="message_reactions">ردود فعل الرسائل</string> - <string name="delete_message_mark_deleted_warning">سيتم وضع علامة على الرسالة للحذف. سيتمكن المستلم/مون من الكشف عن هذه الرسالة.</string> + <string name="delete_message_mark_deleted_warning">سيتم وضع علامة على الرسالة للحذف. سيتمكن المُستلم/ون من الكشف عن هذه الرسالة.</string> <string name="delete_message_cannot_be_undone_warning">سيتم حذف الرسالة - لا يمكن التراجع عن هذا!</string> <string name="message_delivery_error_title">خطأ في تسليم الرسالة</string> <string name="network_and_servers">الشبكة والخوادم</string> <string name="moderate_verb">إشراف</string> - <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 للجوال: انقر فوق <b>فتح في تطبيق الجوال</b> ، ثم انقر فوق <b>اتصال</b> في التطبيق.]]></string> + <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 للجوال: انقر فوق <b>فتح في تطبيق الجوال</b>، ثم انقر فوق <b>اتصال</b> في التطبيق.]]></string> <string name="share_text_moderated_at">تحت الإشراف في: %s</string> <string name="message_reactions_prohibited_in_this_chat">ردود الفعل الرسائل ممنوعة في هذه الدردشة.</string> - <string name="moderated_item_description">مُشرف بواسطة %s</string> + <string name="moderated_item_description">أُشرف بواسطة %s</string> <string name="message_delivery_error_desc">من المرجح أن جهة الاتصال هذه قد حذفت الاتصال بك.</string> <string name="v5_1_message_reactions">ردود فعل الرسائل</string> <string name="callstatus_missed">مكالمة فائتة</string> @@ -740,19 +729,17 @@ <string name="v5_2_message_delivery_receipts">إيصالات تسليم الرسائل!</string> <string name="custom_time_unit_minutes">دقائق</string> <string name="custom_time_unit_months">شهور</string> - <string name="v5_2_more_things_descr">- توصيل رسائل أكثر استقرارًا. -\n- مجموعات أفضل قليلاً. -\n- و اكثر!</string> + <string name="v5_2_more_things_descr">- توصيل رسائل أكثر استقرارًا.\n- مجموعات أفضل قليلاً.\n- و اكثر!</string> <string name="network_status">حالة الشبكة</string> <string name="user_mute">كتم</string> - <string name="message_reactions_are_prohibited">ردود الفعل الرسائل ممنوعة في هذه المجموعة.</string> + <string name="message_reactions_are_prohibited">ردود الفعل الرسائل ممنوعة.</string> <string name="icon_descr_more_button">المزيد</string> - <string name="network_settings_title">إعدادات الشبكة</string> + <string name="network_settings_title">إعدادات متقدّمة</string> <string name="icon_descr_call_missed">مكالمة فائتة</string> <string name="v4_5_message_draft">مسودة الرسالة</string> <string name="v4_5_multiple_chat_profiles">ملفات تعريف دردشة متعددة</string> <string name="settings_notification_preview_title">معاينة الإشعار</string> - <string name="status_no_e2e_encryption">لا يوجد تشفير بين الطريفين</string> + <string name="status_no_e2e_encryption">لا يوجد تعمية بين الطريفين</string> <string name="chat_preferences_no">لا</string> <string name="notification_preview_new_message">رسالة جديدة</string> <string name="images_limit_desc">يمكن إرسال 10 صور فقط في نفس الوقت</string> @@ -760,7 +747,7 @@ <string name="feature_offered_item_with_param">متوفرة %s: %2s</string> <string name="notifications_will_be_hidden">سيتم تسليم الإشعارات فقط حتى يتوقف التطبيق!</string> <string name="no_filtered_chats">لا توجد محادثات مُصفاة</string> - <string name="no_received_app_files">لا توجد ملفات مستلمة أو مرسلة</string> + <string name="no_received_app_files">لا توجد ملفات مُستلمة أو مُرسلة</string> <string name="shutdown_alert_desc">ستتوقف الإشعارات عن العمل حتى تعيد تشغيل التطبيق</string> <string name="new_passcode">رمز مرور جديد</string> <string name="new_database_archive">أرشيف قاعدة بيانات جديدة</string> @@ -768,16 +755,13 @@ <string name="network_use_onion_hosts_prefer_desc">سيتم استخدام مضيفات البصل عند توفرها.</string> <string name="network_use_onion_hosts_no_desc">لن يتم استخدام مضيفات البصل.</string> <string name="no_contacts_selected">لم تٌحدد جهات اتصال</string> - <string name="v4_6_group_moderation_descr">يمكن للمسؤولين الآن: -\n- حذف رسائل الأعضاء. -\n- تعطيل الأعضاء (دور \"المراقب\")</string> + <string name="v4_6_group_moderation_descr">يمكن للمُدراء الآن:\n- حذف رسائل الأعضاء.\n- تعطيل الأعضاء (دور المراقب)</string> <string name="settings_notifications_mode_title">خدمة الإشعار</string> <string name="chat_preferences_off">غير مفعّل</string>` <string name="chat_preferences_on">مفعل</string> <string name="old_database_archive">أرشيف قاعدة البيانات القديمة</string> <string name="feature_off">غير مفعّل</string> <string name="one_time_link">رابط دعوة لمرة واحدة</string> - <string name="network_use_onion_hosts_required_desc_in_alert">سوف تكون مضيفات البصل مطلوبة للاتصال.</string> <string name="group_member_role_observer">المراقب</string> <string name="item_info_no_text">لا يوجد نص</string> <string name="new_member_role">دور عضو جديد</string> @@ -791,67 +775,62 @@ <string name="videos_limit_desc">يمكن إرسال 10 فيديوهات فقط في نفس الوقت</string> <string name="add_contact">رابط دعوة لمرة واحدة</string> <string name="network_use_onion_hosts_no">لا</string> - <string name="network_use_onion_hosts_required_desc">سوف تكون مضيفات البصل مطلوبة للاتصال. -\nيُرجى ملاحظة: أنك لن تتمكن من الاتصال بالخوادم بدون عنوان onion.</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">سيتم استخدام مضيفات البصل عند توفرها.</string> - <string name="network_use_onion_hosts_no_desc_in_alert">لن يتم استخدام مضيفات البصل.</string> + <string name="network_use_onion_hosts_required_desc">ستكون مضيفات البصل مطلوبة للاتصال. \nيُرجى ملاحظة: أنك لن تتمكن من الاتصال بالخوادم بدون عنوان onion.</string> <string name="self_destruct_new_display_name">اسم عرض جديد:</string> <string name="new_passphrase">عبارة مرور جديدة…</string> - <string name="icon_descr_server_status_pending">يرجى الانتظار</string> - <string name="enter_passphrase_notification_title">كلمة المرور مطلوبة</string> - <string name="paste_the_link_you_received">ألصق الرابط المُستلَم</string> + <string name="icon_descr_server_status_pending">قيد الانتظار</string> + <string name="enter_passphrase_notification_title">عبارة المرور مطلوبة</string> + <string name="paste_the_link_you_received">ألصِق الرابط الذي استلمته</string> <string name="only_owners_can_enable_files_and_media">فقط مالكي المجموعة يمكنهم تفعيل الملفات والوسائط.</string> <string name="only_group_owners_can_enable_voice">فقط مالكي المجموعة يمكنهم تفعيل الرسائل الصوتية.</string> <string name="only_stored_on_members_devices">(يخزن فقط بواسطة أعضاء المجموعة)</string> - <string name="la_mode_passcode">كلمة المرور</string> - <string name="passcode_set">تم تعيين كلمة المرور!</string> + <string name="la_mode_passcode">رمز المرور</string> + <string name="passcode_set">عيّنت رمز المرور!</string> <string name="group_member_role_owner">المالك</string> <string name="only_your_contact_can_send_disappearing">فقط جهة اتصالك يمكنها إرسال رسائل تختفي.</string> - <string name="only_your_contact_can_add_message_reactions">جهة اتصالك فقط يمكنها إضافة تفاعلات على الرسالة</string> + <string name="only_your_contact_can_add_message_reactions">جهة اتصالك فقط يمكنها إضافة ردود الفعل على الرسالة</string> <string name="only_group_owners_can_change_prefs">فقط مالكي المجموعة يمكنهم تغيير تفضيلات المجموعة.</string> - <string name="only_your_contact_can_delete">جهة اتصالك فقط يمكنها حذف الرسائل نهائيا (يمكنك تعليم الرسالة للحذف).</string> + <string name="only_your_contact_can_delete">جهة اتصالك فقط يمكنها حذف الرسائل بشكل لا رجعة فيه (يمكنك تعليم الرسالة للحذف). (24 ساعة)</string> <string name="only_you_can_send_voice">أنت فقط يمكنك إرسال رسائل صوتية.</string> <string name="open_verb">افتح</string> - <string name="passcode_not_changed">لم يتم تغيير كلمة المرور!</string> - <string name="passcode_changed">تم تغيير كلمة المرور</string> + <string name="passcode_not_changed">لم يتم تغيير رمز المرور!</string> + <string name="passcode_changed">تم تغيير رمز المرور</string> <string name="opening_database">جارِ فتح قاعدة البيانات…</string> <string name="only_your_contact_can_send_voice">جهة اتصالك فقط يمكنها إرسال رسائل صوتية.</string> <string name="paste_button">ألصق</string> - <string name="restore_passphrase_not_found_desc">كلمة المرور غير موجودة في مخزن المفاتيح، يرجى إدخالها يدوياً. قد يحدث هذا إذا قمت باستعادة ملفات التطبيق باستخدام أداة استرجاع بيانات. إذا لم يكن الأمر كذلك، تواصل مع المبرمجين رجاء</string> + <string name="restore_passphrase_not_found_desc">لم يتم العثور على عبارة المرور في Keystore، يُرجى إدخالها يدويًا. ربما حدث هذا إذا استعدت بيانات التطبيق باستخدام أداة النسخ الاحتياطي. إذا لم يكن الأمر كذلك، يُرجى التواصل مع المطورين.</string> <string name="open_chat">افتح الدردشة</string> - <string name="simplex_link_mode_browser_warning">فتح الرابط في المتصفح قد يقلل خصوصية وحماية اتصالك. الروابط غير الموثوقة من SimpleX ستكون باللون الأحمر</string> - <string name="only_you_can_add_message_reactions">أنت فقط يمكنك إضافة تفاعل على الرسالة.</string> - <string name="only_you_can_delete_messages">أنت فقط يمكنك حذف الرسائل نهائيا (يمكن للمستلم تعليمها للحذف)</string> + <string name="simplex_link_mode_browser_warning">قد يؤدي فتح الرابط في المتصفح إلى تقليل خصوصية الاتصال وأمانه. ستظهر روابط SimpleX غير الموثوقة باللون الأحمر.</string> + <string name="only_you_can_add_message_reactions">أنت فقط يمكنك إضافة ردود الفعل على الرسالة.</string> + <string name="only_you_can_delete_messages">أنت فقط يمكنك حذف الرسائل بشكل لا رجعة فيه (يمكن للمستلم تعليمها للحذف). (24 ساعة)</string> <string name="only_you_can_send_disappearing">أنت فقط يمكنك إرسال رسائل تختفي</string> <string name="only_you_can_make_calls">أنت فقط يمكنك إجراء المكالمات.</string> <string name="only_your_contact_can_make_calls">فقط جهة اتصالك يمكنها إجراء المكالمات.</string> <string name="auth_open_chat_console">افتح وحدة تحكم الدردشة</string> - <string name="la_lock_mode_passcode">إدخال كلمة المرور</string> - <string name="open_simplex_chat_to_accept_call">افتح SimpleX Chat للرد على المكالمة</string> - <string name="opensource_protocol_and_code_anybody_can_run_servers">بروتوكول وكود مفتوح المصدر - يمكن لأي شخص تشغيل الخوادم.</string> + <string name="la_lock_mode_passcode">إدخال رمز المرور</string> + <string name="open_simplex_chat_to_accept_call">افتح SimpleX Chat لقبول المكالمة</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">يمكن لأي شخص استضافة الخوادم.</string> <string name="password_to_show">كلمة المرور للإظهار</string> <string name="call_connection_peer_to_peer">ندّ لِندّ</string> - <string name="people_can_connect_only_via_links_you_share">يمكن للناس التواصل معك فقط عبر الرابط الذي تقوم بمشاركته</string> - <string name="icon_descr_call_pending_sent">مكالمة في الانتظار</string> - <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[تقوم أجهزة العميل فقط بتخزين ملفات تعريف المستخدمين وجهات الاتصال والمجموعات والرسائل المرسلة باستخدام <b>تشفير ثنائي الطبقات من بين الطريفين</b>.]]></string> - <string name="reset_color">إعادة تعيين الألوان</string> + <string name="people_can_connect_only_via_links_you_share">أنت تقرر مَن يمكنه الاتصال.</string> + <string name="icon_descr_call_pending_sent">مكالمة قيد الانتظار</string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">تقوم أجهزة العميل فقط بتخزين ملفات تعريف المستخدمين وجهات الاتصال والمجموعات والرسائل.</string> + <string name="reset_color">صفّر الألوان</string> <string name="save_verb">حفظ</string> - <string name="smp_servers_preset_address">عنوان الخادم المحدد مسبقًا</string> + <string name="smp_servers_preset_address">عنوان الخادم المُعد مسبقًا</string> <string name="save_and_notify_group_members">حفظ وإشعار أعضاء المجموعة</string> <string name="onboarding_notifications_mode_periodic">دوري</string> <string name="restart_the_app_to_use_imported_chat_database">أعد تشغيل التطبيق لاستخدام قاعدة بيانات الدردشة المستوردة.</string> <string name="receiving_via">الاستلام عبر</string> - <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">يرجى التحقق من استخدامك للرابط الصحيح أو اطلب من جهة اتصالك أن ترسل لك رابطًا آخر.</string> + <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">يُرجى التحقق من استخدامك للرابط الصحيح أو اطلب من جهة اتصالك أن ترسل لك رابطًا آخر.</string> <string name="periodic_notifications_disabled">الإشعارات الدورية مُعطَّلة</string> - <string name="image_descr_profile_image">صورة الملف الشخصي</string> + <string name="image_descr_profile_image">صورة ملف التعريف</string> <string name="onboarding_notifications_mode_title">الإشعارات خاصة</string> <string name="store_passphrase_securely_without_recover">يرجى تخزين عبارة المرور بشكل آمن، فلن تتمكن من الوصول إلى الدردشة إذا فقدتها.</string> - <string name="contact_developers">يرجى تحديث التطبيق والاتصال بالمطورين.</string> + <string name="contact_developers">يُرجى تحديث التطبيق والتواصل مع المطورين.</string> <string name="read_more_in_user_guide_with_link"><![CDATA[اقرأ المزيد في <font color="#0088ff">دليل المستخدم</font>.]]></string> - <string name="auth_open_chat_profiles">افتح ملفات تعريف الدردشة</string> + <string name="auth_open_chat_profiles">غيّر ملفات تعريف الدردشة</string> <string name="revoke_file__confirm">اسحب الوصول</string> - <string name="save_archive">حفظ الأرشيف</string> - <string name="save_color">حفظ اللون</string> <string name="reveal_verb">كشف</string> <string name="stop_rcv_file__message">سيتم إيقاف استلام الملف.</string> <string name="reject_contact_button">رفض</string> @@ -878,7 +857,7 @@ <string name="restore_database_alert_desc">الرجاء إدخال كلمة المرور السابقة بعد استعادة نسخة احتياطية لقاعدة البيانات. لا يمكن التراجع عن هذا الإجراء.</string> <string name="restore_database_alert_title">استعادة النسخة الاحتياطية لقاعدة البيانات؟</string> <string name="network_options_save">حفظ</string> - <string name="users_delete_with_connections">اتصالات الملف الشخصي والخادم</string> + <string name="users_delete_with_connections">اتصالات ملف التعريف والخادم</string> <string name="prohibit_message_reactions">منع ردود فعل الرسالة.</string> <string name="prohibit_sending_voice">منع إرسال الرسائل الصوتية.</string> <string name="prohibit_message_reactions_group">منع ردود فعل الرسائل.</string> @@ -897,11 +876,10 @@ <string name="info_row_updated_at">حٌديثت السجل في</string> <string name="share_text_updated_at">حٌديثت السجل في: %s</string> <string name="restore_database_alert_confirm">استعادة</string> - <string name="network_options_revert">إرجاع</string> - <string name="v4_4_live_messages_desc">يرى المستلمون التحديثات أثناء كتابتها.</string> + <string name="v4_4_live_messages_desc">يرى المُستلمون التحديثات أثناء كتابتها.</string> <string name="feature_received_prohibited">استلمت، ممنوع</string> <string name="save_servers_button">حفظ</string> - <string name="profile_update_will_be_sent_to_contacts">سيتم إرسال تحديث الملف الشخصي إلى جهات الاتصال الخاصة بك.</string> + <string name="profile_update_will_be_sent_to_contacts">سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك.</string> <string name="save_and_notify_contacts">حفظ وإشعار جهات الاتصال</string> <string name="save_and_update_group_profile">حفظ وتحديث ملف تعريف المجموعة</string> <string name="network_option_ping_count">عدد البينج</string> @@ -915,9 +893,9 @@ <string name="info_row_received_at">استلمت في</string> <string name="button_remove_member">إزالة العضو</string> <string name="remove_member_confirmation">إزالة</string> - <string name="network_options_reset_to_defaults">إعادة التعيين إلى الإعدادات الافتراضية</string> + <string name="network_options_reset_to_defaults">صفّر إلى الإعدادات الافتراضية</string> <string name="network_option_ping_interval">بينج الفاصل الزمني</string> - <string name="profile_password">كلمة مرور الملف الشخصي</string> + <string name="profile_password">كلمة مرور ملف التعريف</string> <string name="prohibit_sending_disappearing_messages">منع إرسال الرسائل التي تختفي.</string> <string name="network_option_protocol_timeout">مهلة البروتوكول</string> <string name="network_option_protocol_timeout_per_kb">مهلة البروتوكول لكل كيلوبايت</string> @@ -926,7 +904,7 @@ <string name="prohibit_sending_disappearing">منع إرسال الرسائل التي تختفي.</string> <string name="v4_5_message_draft_descr">الاحتفاظ بمسودة الرسالة الأخيرة، مع المرفقات.</string> <string name="v4_5_private_filenames">أسماء ملفات خاصة</string> - <string name="v4_6_hidden_chat_profiles_descr">حماية ملفات تعريف الدردشة الخاصة بك بكلمة مرور!</string> + <string name="v4_6_hidden_chat_profiles_descr">احمِ ملفات تعريف دردشتك بكلمة مرور!</string> <string name="callstatus_rejected">رُفضت المكالمة</string> <string name="protect_app_screen">حماية شاشة التطبيق</string> <string name="group_member_status_removed">أُزيلت</string> @@ -934,99 +912,98 @@ <string name="group_welcome_preview">معاينة</string> <string name="error_smp_test_certificate">من المحتمل أن الملف المرجعي للشهادة في عنوان الخادم غير صحيح</string> <string name="simplex_service_notification_text">يتم استلام الرسائل…</string> - <string name="observer_cant_send_message_desc">يرجى الاتصال بمسؤول المجموعة.</string> + <string name="observer_cant_send_message_desc">يُرجى الاتصال بمُدير المجموعة.</string> <string name="sync_connection_force_confirm">أعد التفاوض</string> - <string name="sync_connection_force_question">إعادة تفاوض التشفير</string> + <string name="sync_connection_force_question">إعادة تفاوض التعمية</string> <string name="revoke_file__action">سحب وصول الملف</string> <string name="revoke_file__title">سحب وصول الملف؟</string> <string name="toast_permission_denied">رٌفض الإذن!</string> <string name="ask_your_contact_to_enable_voice">يرجى مطالبة جهة اتصالك بتفعيل إرسال الرسائل الصوتية.</string> - <string name="icon_descr_profile_image_placeholder">العنصر النائب لصورة الملف الشخصي</string> + <string name="icon_descr_profile_image_placeholder">العنصر النائب لصورة ملف التعريف</string> <string name="image_descr_qr_code">رمز QR</string> - <string name="reset_verb">إعادة التعيين</string> + <string name="reset_verb">صفّر</string> <string name="network_proxy_port">المنفذ %d</string> - <string name="smp_servers_preset_server">خادم محدد مسبقًا</string> - <string name="read_more_in_github">قراءة المزيد في مستودعنا على GitHub.</string> + <string name="smp_servers_preset_server">خادم مُعد مسبقًا</string> <string name="relay_server_if_necessary">يتم استخدام خادم الترحيل فقط إذا لزم الأمر. يمكن لطرف آخر مراقبة عنوان IP الخاص بك.</string> <string name="save_and_notify_contact">حفظ وإشعار جهة الاتصال</string> <string name="settings_restart_app">إعادة التشغيل</string> <string name="share_text_received_at">استلمت في: %s</string> - <string name="renegotiate_encryption">إعادة تفاوض التشفير</string> + <string name="renegotiate_encryption">إعادة تفاوض التعمية</string> <string name="sender_at_ts">%s في %s</string> <string name="save_group_profile">حفظ ملف المجموعة</string> <string name="color_secondary">ثانوي</string> - <string name="v5_1_self_destruct_passcode">كلمة مرور التدمير الذاتي</string> + <string name="v5_1_self_destruct_passcode">رمز المرور للتدمير الذاتي</string> <string name="sending_files_not_yet_supported">إرسال الملفات غير مدعوم بعد</string> - <string name="sender_cancelled_file_transfer">قام المرسل بإلغاء إرسال الملف</string> + <string name="sender_cancelled_file_transfer">أُلغيَ المرسل نقل الملف.</string> <string name="connect_via_link_or_qr_from_clipboard_or_in_person">(امسح أو ألصق من الحافظة)</string> <string name="network_option_seconds_label">ثانية</string> - <string name="sender_may_have_deleted_the_connection_request">قد يكون المرسل قد ألغى طلب الاتصال</string> - <string name="scan_QR_code">مسح رمز الاستجابة السريعة</string> + <string name="sender_may_have_deleted_the_connection_request">ربما حذف المرسل طلب الاتصال.</string> + <string name="scan_QR_code">مسح رمز QR</string> <string name="send_us_an_email">أرسل لنا بريداً</string> <string name="scan_code_from_contacts_app">مسح رمز الأمان من تطبيق جهة الاتصال</string> <string name="share_invitation_link">مشاركة رابط ذو استخدام واحد</string> <string name="stop_snd_file__message">سيتم إيقاف إرسال الملف.</string> <string name="icon_descr_send_message">إرسال رسالة</string> <string name="send_disappearing_message_send">إرسال</string> - <string name="send_live_message">إرسال رسالة حية</string> + <string name="send_live_message">أرسل رسالة حيّة</string> <string name="smp_servers_test_failed">فشلت تجربة الخادم!</string> - <string name="save_passphrase_in_keychain">حفظ كلمة المرور في مخزن المفاتيح</string> + <string name="save_passphrase_in_keychain">احفظ عبارة المرور في Keystore</string> <string name="button_send_direct_message">أرسل رسالة مباشرة</string> <string name="sending_via">إرسال عبر</string> <string name="conn_stats_section_title_servers">الخوادم</string> <string name="v4_2_security_assessment">تقييم الأمان</string> - <string name="v4_4_disappearing_messages_desc">الرسائل المرسلة سيتم حذفها بعد المدة المحددة.</string> + <string name="v4_4_disappearing_messages_desc">الرسائل المرسلة سيتم حذفها بعد المدة المحدّدة.</string> <string name="v4_6_group_welcome_message_descr">تعيين رسالة تظهر للأعضاء الجدد!</string> - <string name="set_passcode">تعيين كلمة المرور</string> + <string name="set_passcode">عيّن رمز المرور</string> <string name="share_text_sent_at">تم إرساله في: %s</string> <string name="current_version_timestamp">%s (الحالي)</string> <string name="color_sent_message">رسالة مرسلة</string> <string name="set_group_preferences">عيّن تفضيلات المجموعة</string> - <string name="v5_0_app_passcode_descr">عيينها بدلا من توثيق النظام</string> + <string name="v5_0_app_passcode_descr">عيّنها بدلاً من استيثاق النظام.</string> <string name="share_verb">مشاركة</string> <string name="send_verb">إرسال</string> - <string name="save_passphrase_and_open_chat">حفظ كلمة المرور وفتح الدردشة</string> - <string name="select_contacts">اختيار جهات اتصال</string> + <string name="save_passphrase_and_open_chat">احفظ عبارة المرور وافتح الدردشة</string> + <string name="select_contacts">حدد جهات الاتصال</string> <string name="accept_feature_set_1_day">تعيين يوم واحد</string> <string name="custom_time_unit_seconds">ثواني</string> <string name="sent_message">رسالة مرسلة</string> <string name="send_disappearing_message">أرسل رسالة تختفي</string> - <string name="save_profile_password">حفظ كلمة مرور الحساب</string> + <string name="save_profile_password">حفظ كلمة مرور ملف التعريف</string> <string name="self_destruct">تدمير ذاتي</string> - <string name="scan_code">مسح الكود</string> - <string name="chat_with_the_founder">إرسال أسئلة وأفكار</string> + <string name="scan_code">مسح الرمز</string> + <string name="chat_with_the_founder">أرسل أسئلة وأفكار</string> <string name="share_address_with_contacts_question">مشاركة العنوان مع جهات الاتصال؟</string> <string name="share_address">مشاركة العنوان</string> <string name="save_welcome_message_question">حفظ رسالة الترحيب؟</string> - <string name="smp_servers_save">حفظ السيرفرات</string> - <string name="settings_section_title_delivery_receipts">أرسل تقارير الاستلام إلى</string> - <string name="receipts_contacts_override_disabled">إرسال تقارير الاستلام معطل لـ %d جهة اتصال.</string> - <string name="receipts_contacts_override_enabled">إرسال تقارير الاستلام مفعل لـ %d جهة اتصال</string> - <string name="set_password_to_export">تعيين كلمة المرور للتصدير</string> + <string name="smp_servers_save">احفظ الخوادم</string> + <string name="settings_section_title_delivery_receipts">أرسل إيصالات التسليم إلى</string> + <string name="receipts_contacts_override_disabled">إرسال الإيصالات مُعطَّل لـ %d جهة اتصال.</string> + <string name="receipts_contacts_override_enabled">إرسال الإيصالات مفعّل لـ %d جهة اتصال</string> + <string name="set_password_to_export">عيّن عبارة المرور للتصدير</string> <string name="rcv_conn_event_verification_code_reset">تم تغيير رمز الأمان</string> - <string name="send_receipts">تقارير الارسال</string> + <string name="send_receipts">إيصالات الإرسال</string> <string name="info_row_sent_at">تم إرساله في</string> - <string name="custom_time_picker_select">اختيار</string> - <string name="sending_delivery_receipts_will_be_enabled">إرسال تقارير الاستلام سيتم تفعيله لجميع جهات الاتصال.</string> - <string name="sending_delivery_receipts_will_be_enabled_all_profiles">سيتم تفعيل إرسال تقارير الاستلام لجميع جهات الاتصال ذات حسابات دردشة ظاهرة</string> + <string name="custom_time_picker_select">حدد</string> + <string name="sending_delivery_receipts_will_be_enabled">سيتم تفعيل إرسال إيصالات التسليم لجميع جهات الاتصال.</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">سيتم تفعيل إرسال إيصالات التسليم لجميع جهات الاتصال في جميع ملفات تعريف الدردشة المرئية.</string> <string name="smp_server_test_secure_queue">قائمة انتظار آمنة</string> <string name="icon_descr_sent_msg_status_send_failed">فشل الإرسال</string> - <string name="icon_descr_sent_msg_status_sent">تم الإرسال</string> + <string name="icon_descr_sent_msg_status_sent">أُرسلت</string> <string name="text_field_set_contact_placeholder">تعيين اسم جهة الاتصال…</string> - <string name="send_live_message_desc">إرسال رسالة حية - سيتم تحديثها للمستلم مع كتابتك لها</string> + <string name="send_live_message_desc">أرسل رسالة حيّة - سيتم تحديثها للمُستلم مع كتابتك لها</string> <string name="set_contact_name">تعيين اسم جهة الاتصال</string> <string name="icon_descr_settings">الإعدادات</string> <string name="smp_save_servers_question">حفظ الخوادم؟</string> <string name="smp_servers_scan_qr">مسح رمز QR الخادم</string> <string name="security_code">رمز الأمان</string> - <string name="save_preferences_question">حفظ الإعدادات؟</string> + <string name="save_preferences_question">حفظ التفضيلات؟</string> <string name="save_settings_question">حفظ الإعدادات؟</string> <string name="secret_text">سري</string> - <string name="self_destruct_passcode">كلمة مرور التدمير الذاتي</string> - <string name="self_destruct_passcode_changed">تم تغيير كلمة مرور التدمير الذاتي!</string> - <string name="self_destruct_passcode_enabled">تم تفعيل كلمة مرور التدمير الذاتي</string> + <string name="self_destruct_passcode">رمز المرور للتدمير الذاتي</string> + <string name="self_destruct_passcode_changed">تغيّرت رمز المرور للتدمير الذاتي!</string> + <string name="self_destruct_passcode_enabled">فعّلت رمز المرور للتدمير الذاتي!</string> <string name="settings_section_title_settings">الإعدادات</string> - <string name="simplex_link_invitation">دعوة لمرة واحدة SimpleX</string> + <string name="simplex_link_invitation">دعوة SimpleX لمرة واحدة</string> <string name="notification_preview_mode_message_desc">عرض جهة الاتصال والرسالة</string> <string name="la_notice_title_simplex_lock">قفل SimpleX</string> <string name="is_not_verified">لم يتحقق من %s</string> @@ -1034,26 +1011,26 @@ <string name="smp_servers">خوادم SMP</string> <string name="share_image">مشاركة الوسائط…</string> <string name="ntf_channel_messages">رسائل SimpleX Chat</string> - <string name="lock_not_enabled">لم يتم تمكين قفل SimpleX!</string> + <string name="lock_not_enabled">قفل SimpleX غير مفعّل!</string> <string name="auth_stop_chat">إيقاف الدردشة</string> <string name="stop_rcv_file__title">التوقف عن استلام الملف؟</string> <string name="share_file">مشاركة الملف…</string> <string name="image_descr_simplex_logo">شعار SimpleX</string> <string name="icon_descr_simplex_team">فريق SimpleX</string> <string name="show_QR_code">عرض رمز QR</string> - <string name="is_verified">%s تم التحقق منه</string> + <string name="is_verified">تم التحقق %s</string> <string name="smp_servers_test_some_failed">فشلت بعض الخوادم في الاختبار:</string> - <string name="send_link_previews">إرسال معاينات الارتباط</string> + <string name="send_link_previews">إرسال معاينات الرابط</string> <string name="skip_inviting_button">تخطي دعوة الأعضاء</string> <string name="stop_chat_question">إيقاف الدردشة؟</string> <string name="show_call_on_lock_screen">عرض</string> - <string name="non_fatal_errors_occured_during_import">حدثت بعض الأخطاء غير الفادحة أثناء الاستيراد - قد ترى وحدة تحكم الدردشة لمزيد من التفاصيل.</string> + <string name="non_fatal_errors_occured_during_import">حدثت بعض الأخطاء غير الفادحة أثناء الاستيراد:</string> <string name="settings_section_title_socks">وكيل SOCKS</string> <string name="v4_2_security_assessment_desc">تم تدقيق أمان SimpleX Chat بواسطة Trail of Bits.</string> <string name="stop_chat_confirmation">إيقاف</string> <string name="settings_notification_preview_mode_title">عرض المعاينة</string> <string name="icon_descr_speaker_off">السماعة متوقفة</string> - <string name="la_lock_mode">وضع قفل SimpleX</string> + <string name="la_lock_mode">SimpleX وضع القفل</string> <string name="share_link">مشاركة الرابط</string> <string name="alert_title_skipped_messages">الرسائل التي تم تخطيها</string> <string name="simplex_address">عنوان SimpleX</string> @@ -1066,8 +1043,7 @@ <string name="star_on_github">اضع نجمة على GitHub</string> <string name="stop_sharing_address">إيقاف مشاركة العنوان؟</string> <string name="stop_sharing">إيقاف المشاركة</string> - <string name="stop_chat_to_export_import_or_delete_chat_database">أوقف الدردشة لتصدير أو استيراد أو حذف قاعدة بيانات الدردشة. لن تتمكن من تلقي الرسائل وإرسالها أثناء إيقاف الدردشة.</string> - <string name="stop_chat_to_enable_database_actions">أوقف الدردشة لتمكين إجراءات قاعدة البيانات.</string> + <string name="stop_chat_to_export_import_or_delete_chat_database">أوقف الدردشة لتصدير أو استيراد أو حذف قاعدة بيانات الدردشة. لن تتمكّن من استلام الرسائل وإرسالها أثناء إيقاف الدردشة.</string> <string name="chat_item_ttl_seconds">%s ثانية/ثواني</string> <string name="callstate_starting">يبدأ…</string> <string name="auth_simplex_lock_turned_on">تم تشغيل القفل SimpleX</string> @@ -1075,7 +1051,7 @@ <string name="show_developer_options">عرض خيارات المطور</string> <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> <string name="error_smp_test_server_auth">يتطلب الخادم إذنًا لإنشاء قوائم انتظار، تحقق من كلمة المرور</string> - <string name="error_xftp_test_server_auth">يتطلب الخادم إذنًا للتحميل، تحقق من كلمة المرور</string> + <string name="error_xftp_test_server_auth">يتطلب الخادم إذنًا للرفع، تحقق من كلمة المرور</string> <string name="notification_preview_mode_contact_desc">عرض جهة الاتصال فقط</string> <string name="ntf_channel_calls">مكالمات SimpleX Chat</string> <string name="simplex_service_notification_title">خدمة SimpleX Chat</string> @@ -1084,116 +1060,112 @@ <string name="stop_file__action">إيقاف الملف</string> <string name="stop_snd_file__title">التوقف عن إرسال الملف؟</string> <string name="icon_descr_address">عنوان SimpleX</string> - <string name="disable_onion_hosts_when_not_supported"><![CDATA[اضبط <i>استخدم مضيفي .onion</i> إلى \"لا\" إذا كان وكيل SOCKS لا يدعمها.]]></string> + <string name="disable_onion_hosts_when_not_supported"><![CDATA[اضبط <i>استخدم مضيفي .onion</i> إلى "لا" إذا كان وكيل SOCKS لا يدعمها.]]></string> <string name="share_with_contacts">مشاركة مع جهات الاتصال</string> <string name="shutdown_alert_question">إيقاف التشغيل؟</string> <string name="network_socks_proxy_settings">إعدادات وكيل SOCKS</string> <string name="settings_shutdown">إيقاف التشغيل</string> <string name="icon_descr_speaker_on">السماعة قيد التشغيل</string> - <string name="submit_passcode">إرسال</string> + <string name="submit_passcode">أرسل</string> <string name="language_system">النظام</string> <string name="theme">السمة</string> <string name="to_start_a_new_chat_help_header">لبدء محادثة جديدة</string> - <string name="to_verify_compare">للتحقق من التشفير بين الطريفين مع جهة اتصالك، قارن (أو امسح) الرمز الموجود على أجهزتك.</string> - <string name="group_is_decentralized">المجموعة لامركزية بالكامل - فهي مرئية فقط للأعضاء.</string> + <string name="to_verify_compare">للتحقق من التعمية بين الطريفين مع جهة اتصالك، قارن (أو امسح) الرمز الموجود على أجهزتك.</string> + <string name="group_is_decentralized">لامركزية بالكامل – مرئية للأعضاء فقط.</string> <string name="theme_system">النظام</string> <string name="error_smp_test_failed_at_step">فشل الاختبار في الخطوة %s.</string> - <string name="v4_5_italian_interface_descr">بفضل المستخدمين - المساهمة عبر Weblate!</string> + <string name="v4_5_italian_interface_descr">بفضل المستخدمين - ساهِم عبر Weblate!</string> <string name="periodic_notifications_desc">يجلب التطبيق الرسائل الجديدة بشكل دوري - يستخدم نسبة قليلة من البطارية يوميًا. لا يستخدم التطبيق إشعارات الدفع - لا يتم إرسال البيانات من جهازك إلى الخوادم.</string> <string name="connection_you_accepted_will_be_cancelled">سيتم إلغاء الاتصال الذي قبلته!</string> <string name="contact_you_shared_link_with_wont_be_able_to_connect">لن تتمكن جهة الاتصال التي شاركت هذا الرابط معها من الاتصال!</string> <string name="this_text_is_available_in_settings">هذا النص متاح في الإعدادات</string> - <string name="to_protect_privacy_simplex_has_ids_for_queues">لحماية الخصوصية، بدلاً من معرفات المستخدم التي تستخدمها جميع الأنظمة الأساسية الأخرى, يحتوي SimpleX على معرفات لقوائم انتظار الرسائل، منفصلة لكل جهة من جهات اتصالك.</string> - <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">لحماية معلوماتك، قم بتشغيل قفل SimpleX -\nسيُطلب منك إكمال المصادقة قبل تمكين هذه الميزة.</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">لحماية خصوصيتك، يستخدم SimpleX معرّفات منفصلة لكل جهة اتصال لديك.</string> + <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">لحماية معلوماتك، فعّل قفل SimpleX \nسيُطلب منك إكمال المصادقة قبل تفعيل هذه الميزة.</string> <string name="network_session_mode_transport_isolation">عزل النقل</string> - <string name="v4_4_french_interface_descr">بفضل المستخدمين - المساهمة عبر Weblate!</string> + <string name="v4_4_french_interface_descr">بفضل المستخدمين - ساهِم عبر Weblate!</string> <string name="v4_6_audio_video_calls_descr">دعم البلوتوث وتحسينات أخرى.</string> - <string name="v5_0_polish_interface_descr">بفضل المستخدمين - المساهمة عبر Weblate!</string> - <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[للحفاظ على خصوصيتك، بدلاً من دفع الإشعارات، يحتوي التطبيق على <b>خدمة SimpleX تعمل في الخلفية</b> – يستخدم نسبة قليلة من البطارية يوميًا.]]></string> + <string name="v5_0_polish_interface_descr">بفضل المستخدمين - ساهِم عبر Weblate!</string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[لتحسين الخصوصية، <b>يتم تشغيل SimpleX في الخلفية</b> بدلاً من استخدام إشعارات push.]]></string> <string name="tap_to_start_new_chat">انقر لبدء محادثة جديدة</string> <string name="to_share_with_your_contact">(للمشاركة مع جهة اتصالك)</string> <string name="to_connect_via_link_title">للتواصل عبر الرابط</string> <string name="scan_qr_to_connect_to_contact">للاتصال، يمكن لجهة الاتصال مسح رمز QR أو استخدام الرابط في التطبيق.</string> - <string name="smp_servers_test_servers">خوادم الاختبار</string> - <string name="first_platform_without_user_ids">المنصة الأولى بدون أي معرفات للمستخدم - صمّمناه ليكون خاصًا.</string> + <string name="smp_servers_test_servers">اختبر الخوادم</string> + <string name="first_platform_without_user_ids">لا معرّفات مُستخدم</string> <string name="settings_section_title_support">دعم SIMPLEX CHAT</string> <string name="switch_verb">تبديل</string> <string name="color_title">العنوان الرئيسي</string> <string name="moderate_message_will_be_marked_warning">سيتم وضع علامة على الرسالة على أنها تحت الإشراف لجميع الأعضاء.</string> <string name="group_invitation_tap_to_join">انقر للانضمام</string> - <string name="to_reveal_profile_enter_password">للكشف عن ملف التعريف المخفي الخاص بك، أدخل كلمة مرور كاملة في حقل البحث في صفحة ملفات تعريف الدردشة الخاصة بك.</string> + <string name="to_reveal_profile_enter_password">للكشف عن ملف تعريفك المخفي، أدخل كلمة مرور كاملة في حقل البحث في صفحة ملفات تعريف دردشتك.</string> <string name="group_invitation_tap_to_join_incognito">انقر للانضمام إلى وضع التخفي</string> <string name="la_mode_system">النظام</string> <string name="settings_section_title_themes">السمات</string> - <string name="v4_6_chinese_spanish_interface_descr">بفضل المستخدمين - المساهمة عبر Weblate!</string> + <string name="v4_6_chinese_spanish_interface_descr">بفضل المستخدمين - ساهِم عبر Weblate!</string> <string name="database_initialization_error_desc">قاعدة البيانات لا تعمل بشكل صحيح. انقر لمعرفة المزيد</string> - <string name="theme_colors_section_title">ألوان السمة</string> - <string name="tap_to_activate_profile">انقر لتنشيط الملف الشخصي.</string> - <string name="should_be_at_least_one_profile">يجب أن يكون هناك ملف تعريف مستخدم واحد على الأقل.</string> + <string name="theme_colors_section_title">ألوان الواجهة</string> + <string name="tap_to_activate_profile">انقر لتنشيط ملف التعريف.</string> <string name="v4_5_transport_isolation">عزل النقل</string> <string name="this_string_is_not_a_connection_link">هذه السلسلة ليست رابط اتصال!</string> - <string name="receipts_section_description">هذه الإعدادات لملف التعريف الحالي الخاص بك</string> + <string name="receipts_section_description">هذه الإعدادات لملف تعريفك الحالي</string> <string name="receipts_section_description_1">يمكن تجاوزها في إعدادات الاتصال و المجموعة.</string> <string name="network_option_tcp_connection_timeout">انتهت مهلة اتصال TCP</string> <string name="v4_5_private_filenames_descr">لحماية المنطقة الزمنية، تستخدم ملفات الصور / الصوت التوقيت العالمي المنسق (UTC).</string> <string name="v5_2_message_delivery_receipts_descr">فقدنا القراد الثاني! ✅</string> - <string name="whats_new_thanks_to_users_contribute_weblate">بفضل المستخدمين - المساهمة عبر Weblate!</string> + <string name="whats_new_thanks_to_users_contribute_weblate">بفضل المستخدمين - ساهِم عبر Weblate!</string> <string name="database_backup_can_be_restored">لم تكتمل محاولة تغيير عبارة مرور قاعدة البيانات.</string> - <string name="enter_passphrase_notification_desc">لتلقي الإخطارات، يرجى إدخال عبارة مرور قاعدة البيانات</string> - <string name="should_be_at_least_one_visible_profile">يجب أن يكون هناك ملف تعريف مستخدم مرئي واحد على الأقل.</string> + <string name="enter_passphrase_notification_desc">لاستلام الإشعارات، يُرجى إدخال عبارة مرور قاعدة البيانات</string> <string name="la_lock_mode_system">مصادقة النظام</string> - <string name="sync_connection_force_desc">يعمل التشفير واتفاقية التشفير الجديدة غير مطلوبة. قد ينتج عن ذلك أخطاء في الاتصال!</string> + <string name="sync_connection_force_desc">يعمل التعمية واتفاقية التعمية الجديدة غير مطلوبة. قد ينتج عن ذلك أخطاء في الاتصال!</string> <string name="image_decoding_exception_desc">لا يمكن فك ترميز الصورة. من فضلك، جرب صورة مختلفة أو تواصل مع المطورين.</string> <string name="moderate_message_will_be_deleted_warning">سيتم حذف الرسالة لجميع الأعضاء.</string> <string name="images_limit_title">الصور كثيرة!</string> <string name="videos_limit_title">مقاطع الفيديو كثيرة!</string> <string name="chat_help_tap_button">زر النقر</string> <string name="thank_you_for_installing_simplex">شكرًا لك على تثبيت SimpleX Chat!</string> - <string name="smp_servers_test_server">خادم الاختبار</string> + <string name="smp_servers_test_server">اختبر الخادم</string> <string name="alert_text_msg_bad_hash">تجزئة الرسالة السابقة مختلفة.</string> <string name="alert_text_msg_bad_id">معرف الرسالة التالية غير صحيح (أقل أو يساوي السابق). \nيمكن أن يحدث ذلك بسبب بعض العلل أو عندما يُخترق الاتصال.</string> <string name="unfavorite_chat">إزالة من المفضلة</string> - <string name="trying_to_connect_to_server_to_receive_messages">محاولة الاتصال بالخادم المستخدم لتلقي الرسائل من جهة الاتصال هذه.</string> + <string name="trying_to_connect_to_server_to_receive_messages">محاولة الاتصال بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه.</string> <string name="choose_file_title">اختيار ملف</string> <string name="icon_descr_sent_msg_status_unauthorized_send">إرسال غير مصرح به</string> - <string name="trying_to_connect_to_server_to_receive_messages_with_error">محاولة الاتصال بالخادم المستخدم لتلقي الرسائل من جهة الاتصال هذه (خطأ: %1$s).</string> + <string name="trying_to_connect_to_server_to_receive_messages_with_error">محاولة الاتصال بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه (خطأ: %1$s).</string> <string name="la_notice_turn_on">تشغيل</string> <string name="webrtc_ice_servers">خوادم WebRTC ICE</string> - <string name="alert_title_cant_invite_contacts_descr">أنت تستخدم ملفًا شخصيًا متخفيًا لهذه المجموعة - لمنع مشاركة ملفك الشخصي الرئيسي الذي يدعو جهات الاتصال غير مسموح به</string> + <string name="alert_title_cant_invite_contacts_descr">أنت تستخدم ملف تعريف متخفي لهذه المجموعة - لمنع مشاركة ملفك التعريفي الرئيسي الذي يدعو جهات الاتصال غير مسموح به</string> <string name="snd_group_event_changed_member_role">غيّرتَ دور %s إلى %s</string> <string name="chat_preferences_yes">نعم</string> - <string name="connected_to_server_to_receive_messages_from_contact">أنت متصل بالخادم المستخدم لتلقي الرسائل من جهة الاتصال هذه.</string> + <string name="connected_to_server_to_receive_messages_from_contact">أنت متصل بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه.</string> <string name="sender_you_pronoun">أنت</string> <string name="description_you_shared_one_time_link">لقد شاركت رابط لمرة واحدة</string> - <string name="profile_will_be_sent_to_contact_sending_link">سيتم إرسال ملف التعريفك إلى جهة الاتصال التي تلقيت منها هذا الارتباط.</string> - <string name="you_will_join_group">ستنضم إلى مجموعة يشير إليها هذا الرابط وتتصل بأعضائها.</string> - <string name="your_chat_profiles">ملفات تعريف الدردشة الخاصة بك</string> + <string name="profile_will_be_sent_to_contact_sending_link">سيتم إرسال ملف التعريفك إلى جهة الاتصال التي استلمت منها هذا الرابط.</string> + <string name="you_will_join_group">ستتصل بجميع أعضاء المجموعة.</string> + <string name="your_chat_profiles">ملفات تعريف دردشتك</string> <string name="your_simplex_contact_address">عنوان SimpleX الخاص بك</string> <string name="your_SMP_servers">خوادم SMP الخاصة بك</string> - <string name="update_onion_hosts_settings_question">هل تريد تحديث إعداد مضيفي onion.؟</string> <string name="onboarding_notifications_mode_off">عندما يكون التطبيق قيد التشغيل</string> <string name="call_connection_via_relay">عبر المُرحل</string> <string name="you_joined_this_group">لقد انضممت إلى هذه المجموعة</string> <string name="you_rejected_group_invitation">لقد رفضت دعوة المجموعة</string> - <string name="incognito_info_share">عندما تشارك ملفًا شخصيًا متخفيًا مع شخص ما، فسيتم استخدام هذا الملف الشخصي للمجموعات التي يدعوك إليها.</string> + <string name="incognito_info_share">عندما تشارك ملف تعريف متخفي مع شخص ما، فسيتم استخدام هذا الملف التعريفي للمجموعات التي يدعوك إليها.</string> <string name="failed_to_create_user_duplicate_desc">لديك بالفعل ملف تعريف دردشة بنفس اسم العرض. الرجاء اختيار اسم آخر.</string> <string name="you_are_already_connected_to_vName_via_this_link">أنت متصل بالفعل بـ%1$s.</string> <string name="waiting_for_video">في انتظار الفيديو</string> - <string name="video_will_be_received_when_contact_completes_uploading">سيتم استلام الفيديو عند اكتمال تحميل جهة اتصالك.</string> - <string name="verify_security_code">تحقق من رمز الحماية</string> + <string name="video_will_be_received_when_contact_completes_uploading">سيتم استلام الفيديو عند اكتمال رفع جهة اتصالك.</string> + <string name="verify_security_code">تحقق من رمز الأمان</string> <string name="v4_3_voice_messages">رسائل صوتية</string> <string name="you_can_accept_or_reject_connection">عندما يطلب الأشخاص الاتصال، يمكنك قبوله أو رفضه.</string> - <string name="you_will_be_connected_when_group_host_device_is_online">سوف تكون متصلاً بالمجموعة عندما يكون جهاز مضيف المجموعة متصلاً بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string> - <string name="you_will_be_connected_when_your_connection_request_is_accepted">سوف تكون متصلاً عندما يتم قبول طلب الاتصال الخاص بك، يرجى الانتظار أو التحقق لاحقًا!</string> + <string name="you_will_be_connected_when_group_host_device_is_online">ستكون متصلاً بالمجموعة عندما يكون جهاز مضيف المجموعة متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا!</string> + <string name="you_will_be_connected_when_your_connection_request_is_accepted">ستكون متصلاً عندما يتم قبول طلب اتصالك، يُرجى الانتظار أو التحقق لاحقًا!</string> <string name="using_simplex_chat_servers">تستخدم خوادم SimpleX Chat.</string> <string name="network_socks_toggle_use_socks_proxy">استخدم وكيل SOCKS</string> <string name="network_use_onion_hosts">استخدم مضيفي onion.</string> <string name="network_enable_socks">استخدام وكيل SOCKS؟</string> <string name="network_use_onion_hosts_prefer">عندما تكون متاحة</string> <string name="your_contacts_will_remain_connected">ستبقى جهات اتصالك متصلة.</string> - <string name="we_do_not_store_contacts_or_messages_on_servers">لا نقوم بتخزين أي من جهات الاتصال أو الرسائل الخاصة بك (بمجرد تسليمها) على الخوادم.</string> + <string name="we_do_not_store_contacts_or_messages_on_servers">لا نقوم بتخزين أي من جهات اتصالك أو رسائلك (بمجرد تسليمها) على الخوادم.</string> <string name="you_can_use_markdown_to_format_messages__prompt">يمكنك استخدام تخفيض السعر لتنسيق الرسائل:</string> <string name="use_chat">استخدم الدردشة</string> <string name="settings_section_title_you">أنت</string> @@ -1226,24 +1198,23 @@ <string name="v4_3_irreversible_message_deletion_desc">يمكن أن تسمح جهات اتصالك بحذف الرسائل بالكامل.</string> <string name="unknown_message_format">تنسيق رسالة غير معروف</string> <string name="description_via_one_time_link">عبر رابط لمرة واحدة</string> - <string name="video_call_no_encryption">مكالمة الفيديو ليست مشفرة بين الطريفين</string> + <string name="video_call_no_encryption">مكالمة الفيديو ليست مُعمّاة بين الطريفين</string> <string name="snd_conn_event_switch_queue_phase_completed">غيّرتَ العنوان</string> - <string name="you_will_be_connected_when_your_contacts_device_is_online">سوف تكون متصلاً عندما يكون جهاز جهة الاتصال الخاصة بك متصلاً بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string> + <string name="you_will_be_connected_when_your_contacts_device_is_online">ستكون متصلاً عندما يكون جهاز جهة اتصالك متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا!</string> <string name="snd_group_event_user_left">غادرت</string> - <string name="you_must_use_the_most_recent_version_of_database">يجب عليك استخدام أحدث إصدار من قاعدة بيانات الدردشة الخاصة بك على جهاز واحد فقط، وإلا فقد تتوقف عن تلقي الرسائل من بعض جهات الاتصال.</string> - <string name="video_will_be_received_when_contact_is_online">سيتم استلام الفيديو عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[يمكنك التحكم من خلال الخادم (الخوادم) <b>لتلقي</b> الرسائل وجهات اتصالك - الخوادم التي تستخدمها لمراسلتهم.]]></string> + <string name="you_must_use_the_most_recent_version_of_database">يجب عليك استخدام أحدث إصدار من قاعدة بيانات دردشتك على جهاز واحد فقط، وإلا فقد تتوقف عن تلقي الرسائل من بعض جهات الاتصال.</string> + <string name="video_will_be_received_when_contact_is_online">سيتم استلام الفيديو عندما تكون جهة اتصالك متصلة بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا!</string> <string name="you_can_share_this_address_with_your_contacts">يمكنك مشاركة هذا العنوان مع جهات اتصالك للسماح لهم بالاتصال بـ%s.</string> <string name="snd_group_event_member_deleted">أُزيلت %1$s</string> - <string name="update_database">تحديث</string> - <string name="database_is_not_encrypted">قاعدة بيانات الدردشة الخاصة بك غير مشفرة - قم بتعيين عبارة المرور لحمايتها.</string> + <string name="update_database">حدّث</string> + <string name="database_is_not_encrypted">قاعدة بيانات دردشتك غير مُعمّاة - عيّن عبارة مرور لحمايتها.</string> <string name="wrong_passphrase">عبارة مرور قاعدة بيانات خاطئة</string> <string name="group_main_profile_sent">سيتم إرسال ملف تعريف الدردشة الخاص بك إلى أعضاء المجموعة</string> <string name="personal_welcome">مرحبًا! %1$s</string> <string name="contact_wants_to_connect_with_you">يريد الاتصال بك!</string> <string name="your_ice_servers">خوادم ICE الخاصة بك</string> <string name="your_privacy">خصوصيتك</string> - <string name="rcv_group_event_updated_group_profile">حدثت ملف تعريف المجموعة</string> + <string name="rcv_group_event_updated_group_profile">حُدثت ملف تعريف المجموعة</string> <string name="group_info_member_you">أنت: %1$s</string> <string name="update_network_settings_confirmation">تحديث</string> <string name="update_network_settings_question">تحديث إعدادات الشبكة؟</string> @@ -1261,11 +1232,11 @@ <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">سيتم حذف قاعدة بيانات الدردشة الحالية واستبدالها بالقاعدة المستوردة. \nلا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف التعريف وجهات الاتصال والرسائل والملفات الخاصة بك بشكل نهائي.</string> <string name="update_database_passphrase">تحديث عبارة مرور قاعدة البيانات</string> - <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">سوف تتوقف عن تلقي الرسائل من هذه المجموعة. سيتم الاحتفاظ سجل الدردشة.</string> + <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">ستتوقف عن تلقي الرسائل من هذه المجموعة. سيتم الاحتفاظ بسجل الدردشة.</string> <string name="custom_time_unit_weeks">أسابيع</string> <string name="you_can_hide_or_mute_user_profile">يمكنك إخفاء أو كتم ملف تعريف المستخدم - اضغط مطولاً للقائمة.</string> <string name="whats_new">ما هو الجديد</string> - <string name="your_current_profile">ملفك الشخصي الحالي</string> + <string name="your_current_profile">ملف تعريفك الحالي</string> <string name="simplex_link_connection">عبر %1$s</string> <string name="icon_descr_received_msg_status_unread">غير مقروءة</string> <string name="welcome">مرحبًا!</string> @@ -1275,9 +1246,9 @@ <string name="gallery_video_button">فيديو</string> <string name="you_can_share_your_address">يمكنك مشاركة عنوانك كرابط أو رمز QR - يمكن لأي شخص الاتصال بك.</string> <string name="you_can_create_it_later">يمكنك إنشاؤه لاحقًا</string> - <string name="invite_prohibited_description">أنت تحاول دعوة جهة اتصال قمت بمشاركة ملف تعريف متخفي معها إلى المجموعة التي تستخدم فيها ملفك الشخصي الرئيسي</string> - <string name="user_unmute">إلغاء الكتم</string> - <string name="unmute_chat">إلغاء الكتم</string> + <string name="invite_prohibited_description">أنت تحاول دعوة جهة اتصال شاركت ملف تعريف متخفي معها إلى المجموعة التي تستخدم فيها ملف تعريفك الرئيسي</string> + <string name="user_unmute">ألغِ الكتم</string> + <string name="unmute_chat">ألغِ الكتم</string> <string name="you_accepted_connection">لقد قبلت الاتصال</string> <string name="unhide_profile">إلغاء إخفاء ملف تعريف</string> <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">يجب أن تكون جهة الاتصال متصلة بالإنترنت حتى يكتمل الاتصال. @@ -1286,18 +1257,16 @@ <string name="smp_servers_use_server_for_new_conn">استخدم للاتصالات الجديدة</string> <string name="smp_servers_use_server">استخدم الخادم</string> <string name="smp_servers_your_server_address">عنوان خادمك</string> - <string name="your_chat_database">قاعدة بيانات الدردشة الخاصة بك</string> + <string name="your_chat_database">قاعدة بيانات دردشتك</string> <string name="you_are_invited_to_group_join_to_connect_with_group_members">أنت مدعو إلى المجموعة. انضم للتواصل مع أعضاء المجموعة.</string> - <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">لقد انضممت إلى هذه المجموعة. الاتصال بدعوة عضو المجموعة.</string> + <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">لقد انضممت إلى هذه المجموعة. جارِ الاتصال بدعوة عضو المجموعة.</string> <string name="snd_conn_event_switch_queue_phase_completed_for_member">غيّرتَ العنوان ل%s</string> <string name="unhide_chat_profile">إلغاء إخفاء ملف تعريف الدردشة</string> <string name="voice_prohibited_in_this_chat">الرسائل الصوتية ممنوعة في هذه الدردشة.</string> <string name="v5_0_large_files_support">مقاطع فيديو وملفات تصل إلى 1 جيجا بايت</string> - <string name="v5_1_better_messages_descr">- رسائل صوتية تصل إلى 5 دقائق. -\n- الوقت المخصص لتختفي. -\n- تحرير التاريخ.</string> + <string name="v5_1_better_messages_descr">- رسائل صوتية تصل إلى 5 دقائق.\n- الوقت المخصص لتختفي.\n- تحرير التاريخ.</string> <string name="you_can_enable_delivery_receipts_later">يمكنك تفعيلة لاحقًا عبر الإعدادات</string> - <string name="you_can_enable_delivery_receipts_later_alert">يمكنك تمكينها لاحقًا عبر إعدادات الخصوصية والأمان للتطبيق.</string> + <string name="you_can_enable_delivery_receipts_later_alert">يمكنك تفعيلها لاحقًا عبر إعدادات الخصوصية والأمان للتطبيق.</string> <string name="description_via_group_link">عبر رابط المجموعة</string> <string name="description_you_shared_one_time_link_incognito">لقد شاركت رابط لمرة واحدة متخفي</string> <string name="simplex_link_mode_browser">عبر المتصفح</string> @@ -1305,33 +1274,33 @@ <string name="upgrade_and_open_chat">قم بالترقية وفتح الدردشة</string> <string name="button_welcome_message">رسالة الترحيب</string> <string name="description_via_contact_address_link">عبر رابط عنوان الاتصال</string> - <string name="connection_error_auth_desc">ما لم يحذف جهة الاتصال الاتصال أو تم استخدام هذا الرابط بالفعل، فقد يكون خطأ - الرجاء الإبلاغ عنه. -\nللاتصال، يرجى مطالبة جهة اتصالك بإنشاء ارتباط اتصال آخر والتحقق من أن لديك اتصال شبكة ثابت.</string> + <string name="connection_error_auth_desc">ما لم يحذف جهة الاتصال الاتصال أو استُخدم هذا الرابط بالفعل، فقد يكون خطأ - الرجاء الإبلاغ عنه. +\nللاتصال، يُرجى مطالبة جهة اتصالك بإنشاء رابط اتصال آخر والتحقق من أن لديك اتصال شبكة ثابت.</string> <string name="your_chat_profile_will_be_sent_to_your_contact">سيتم إرسال ملف تعريف الدردشة الخاص بك \nإلى جهة اتصالك</string> <string name="user_unhide">إلغاء الإخفاء</string> - <string name="incognito_random_profile">ملفك الشخصي العشوائي</string> - <string name="you_will_still_receive_calls_and_ntfs">ستستمر في تلقي المكالمات والإشعارات من الملفات الشخصية المكتومة عندما تكون نشطة.</string> + <string name="incognito_random_profile">ملفك التعريفي العشوائي</string> + <string name="you_will_still_receive_calls_and_ntfs">ستستمر في استلام المكالمات والإشعارات من الملفات التعريفية المكتومة عندما تكون نشطة.</string> <string name="chat_preferences_you_allow">انت تسمح بها</string> <string name="icon_descr_video_call">مكالمة فيديو</string> - <string name="voice_messages_are_prohibited">الرسائل الصوتية ممنوعة في هذه الدردشة.</string> + <string name="voice_messages_are_prohibited">الرسائل الصوتية ممنوعة.</string> <string name="auth_unlock">فتح القفل</string> <string name="smp_server_test_upload_file">رفع الملف</string> - <string name="la_could_not_be_verified">لا يمكن التحقق منك؛ حاول مرة اخرى.</string> + <string name="la_could_not_be_verified">لا يمكن التحقق منك؛ الرجاء المحاولة مرة اخرى.</string> <string name="voice_message">رسالة صوتية</string> <string name="voice_message_send_text">رسالة صوتية…</string> <string name="group_preview_you_are_invited">أنت مدعو إلى المجموعة</string> <string name="observer_cant_send_message_title">لا يمكنك إرسال رسائل!</string> - <string name="you_need_to_allow_to_send_voice">تحتاج إلى السماح لجهة الاتصال الخاصة بك بإرسال رسائل صوتية لتتمكن من إرسالها.</string> + <string name="you_need_to_allow_to_send_voice">تحتاج إلى السماح لجهة اتصالك بإرسال رسائل صوتية لتتمكن من إرسالها.</string> <string name="contact_sent_large_file">أرسلت جهة اتصالك ملفًا أكبر من الحجم الأقصى المعتمد حاليًا (%1$s).</string> - <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[يمكنك <font color=#0088ff>الاتصال بمطوري SimpleX Chat لطرح أي أسئلة وتلقي التحديثات</font>.]]></string> + <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[يمكنك <font color="#0088ff">الاتصال بمطوري SimpleX Chat لطرح أي أسئلة وتلقي التحديثات</font>.]]></string> <string name="smp_servers_your_server">خادمك</string> <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">يُخزن ملف تعريفك على جهازك ومشاركته فقط مع جهات اتصالك. لا تستطيع خوادم SimpleX رؤية ملف تعريفك.</string> <string name="icon_descr_video_off">الفيديو مقفل</string> <string name="icon_descr_video_on">الفيديو مُشغَّل</string> <string name="v4_2_auto_accept_contact_requests_desc">مع رسالة ترحيب اختيارية.</string> <string name="in_developing_title">قريباً!</string> - <string name="in_developing_desc">هذه الميزة ليست مدعومة حتى الآن. جرب الإصدار التالي.</string> + <string name="in_developing_desc">هذه الميزة ليست مدعومة بعد. جرب الإصدار القادم.</string> <string name="receipts_groups_disable_keep_overrides">تعطيل (الاحتفاظ بتجاوزات المجموعة)</string> <string name="receipts_groups_enable_for_all">تفعيل لجميع المجموعات</string> <string name="receipts_groups_override_enabled">إرسال الإيصالات مفعّلة لـ%d مجموعات</string> @@ -1345,7 +1314,7 @@ <string name="receipts_groups_enable_keep_overrides">تفعيل (الاحتفاظ بتجاوزات المجموعة)</string> <string name="receipts_groups_title_enable">تفعيل الإيصالات للمجموعات؟</string> <string name="no_info_on_delivery">لا توجد معلومات عن التسليم</string> - <string name="no_selected_chat">لا توجد دردشة محددة</string> + <string name="no_selected_chat">لا توجد دردشة محدّدة</string> <string name="receipts_groups_override_disabled">إرسال الإيصالات مُعطَّلة لـ%d مجموعات</string> <string name="receipts_section_groups">مجموعات صغيرة (الحد الأقصى 20)</string> <string name="connect_via_member_address_alert_title">تواصل مباشرةً؟</string> @@ -1354,15 +1323,15 @@ <string name="connect_use_current_profile">استخدم ملف التعريف الحالي</string> <string name="disable_notifications_button">تعطيل الإشعارات</string> <string name="turn_off_system_restriction_button">افتح إعدادات التطبيق</string> - <string name="system_restricted_background_desc">لا يمكن تشغيل SimpleX في الخلفية. ستتلقى الإشعارات فقط عندما يكون التطبيق قيد التشغيل.</string> + <string name="system_restricted_background_desc">لا يمكن تشغيل SimpleX في الخلفية. ستستلم الإشعارات فقط عندما يكون التطبيق قيد التشغيل.</string> <string name="connect__a_new_random_profile_will_be_shared">سيتم مشاركة ملف تعريف عشوائي جديد.</string> <string name="paste_the_link_you_received_to_connect_with_your_contact">ألصق الرابط المُستلَم للتواصل مع جهة اتصالك…</string> - <string name="connect__your_profile_will_be_shared">ستتم مشاركة ملفك الشخصي %1$s.</string> + <string name="connect__your_profile_will_be_shared">ستتم مشاركة ملفك التعريفي %1$s.</string> <string name="system_restricted_background_in_call_desc">قد يغلق التطبيق بعد دقيقة واحدة في الخلفية.</string> <string name="turn_off_battery_optimization_button">سماح</string> <string name="system_restricted_background_in_call_title">لا مكالمات في الخلفية</string> - <string name="system_restricted_background_warn"><![CDATA[لتمكين الإشعارات، يرجى اختيار <b>استهلاك بطارية التطبيق</b> / <b>غير مقيد</b> في إعدادات التطبيق.]]></string> - <string name="system_restricted_background_in_call_warn"><![CDATA[لإجراء مكالمات في الخلفية، يرجى اختيار <b>استهلاك بطارية التطبيق</b> / <b>غير مقيد</b> في إعدادات التطبيق.]]></string> + <string name="system_restricted_background_warn"><![CDATA[لتفعيل الإشعارات، يُرجى اختيار <b>استهلاك بطارية التطبيق</b> / <b>غير مقيد</b> في إعدادات التطبيق.]]></string> + <string name="system_restricted_background_in_call_warn"><![CDATA[لإجراء مكالمات في الخلفية، يُرجى اختيار <b>استهلاك بطارية التطبيق</b> / <b>غير مقيد</b> في إعدادات التطبيق.]]></string> <string name="connect_use_new_incognito_profile">استخدم ملف تعريف متخفي جديد</string> <string name="you_invited_a_contact">أنت دعوت جهة اتصال</string> <string name="privacy_message_draft">مسودة الرسالة</string> @@ -1370,30 +1339,28 @@ <string name="privacy_show_last_messages">إظهار الرسائل الأخيرة</string> <string name="rcv_group_event_n_members_connected">%s، %s و %d أعضاء آخرين متصلون</string> <string name="rcv_group_event_3_members_connected">%s، %s و %s متصل</string> - <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">سيتم تشفير قاعدة البيانات وتخزين عبارة المرور في الإعدادات.</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">سيتم تعمية قاعدة البيانات وتخزين عبارة المرور في الإعدادات.</string> <string name="you_can_change_it_later">يُخزين عبارة المرور العشوائية في الإعدادات كنص عادي. \nيمكنك تغييره لاحقا.</string> - <string name="database_encryption_will_be_updated_in_settings">سيتم تحديث عبارة مرور تشفير قاعدة البيانات وتخزينها في الإعدادات.</string> + <string name="database_encryption_will_be_updated_in_settings">سيتم تحديث عبارة مرور تعمية قاعدة البيانات وتخزينها في الإعدادات.</string> <string name="remove_passphrase_from_settings">هل تريد إزالة عبارة المرور من الإعدادات؟</string> <string name="use_random_passphrase">استخدم عبارة مرور عشوائية</string> - <string name="save_passphrase_in_settings">حفظ عبارة المرور في الإعدادات</string> - <string name="setup_database_passphrase">إعداد كلمة المرور لقاعدة البيانات</string> - <string name="set_database_passphrase">تعيين عبارة مرور قاعدة البيانات</string> + <string name="save_passphrase_in_settings">احفظ عبارة المرور في الإعدادات</string> + <string name="setup_database_passphrase">إعداد عبارة المرور لقاعدة البيانات</string> + <string name="set_database_passphrase">عيّن عبارة مرور قاعدة البيانات</string> <string name="open_database_folder">افتح مجلد قاعدة البيانات</string> <string name="passphrase_will_be_saved_in_settings">سيتم تخزين عبارة المرور في الإعدادات كنص عادي بعد تغييرها أو إعادة تشغيل التطبيق.</string> - <string name="settings_is_storing_in_clear_text">يُخزين عبارة المرور في الإعدادات كنص عادي.</string> - <string name="socks_proxy_setting_limitations"><![CDATA[<b>يُرجى الملاحظة</b>: يتم توصيل مرحلات الرسائل والملفات عبر وكيل SOCKS. تستخدم المكالمات وإرسال معاينات الارتباط الاتصال المباشر.]]></string> - <string name="encrypt_local_files">تشفير الملفات المحلية</string> - <string name="v5_3_encrypt_local_files">تشفير الملفات والوسائط المخزنة</string> + <string name="settings_is_storing_in_clear_text">عبارة المرور مخزنة في الإعدادات كنص عادي.</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>يُرجى الملاحظة</b>: يتم توصيل مرحلات الرسائل والملفات عبر وكيل SOCKS. تستخدم المكالمات وإرسال معاينات الروابط الاتصال المباشر.]]></string> + <string name="encrypt_local_files">عَمِّ الملفات المحلية</string> + <string name="v5_3_encrypt_local_files">عَمِّ الملفات والوسائط المخزنة</string> <string name="v5_3_new_desktop_app">تطبيق سطح المكتب الجديد!</string> <string name="v5_3_new_interface_languages">6 لغات واجهة جديدة</string> - <string name="v5_3_encrypt_local_files_descr">يقوم التطبيق بتشفير الملفات المحلية الجديدة (باستثناء مقاطع الفيديو).</string> + <string name="v5_3_encrypt_local_files_descr">يُعمِّي الملفات المحلية الجديدة (باستثناء مقاطع الفيديو).</string> <string name="v5_3_discover_join_groups">اكتشاف والانضمام إلى المجموعات</string> <string name="v5_3_new_interface_languages_descr">العربية والبلغارية والفنلندية والعبرية والتايلاندية والأوكرانية - شكرًا للمستخدمين و Weblate.</string> <string name="v5_3_new_desktop_app_descr">إنشاء ملف تعريف جديد في تطبيق سطح المكتب. 💻</string> - <string name="v5_3_discover_join_groups_descr">- الاتصال بخدمة الدليل (تجريبي)! -\n- إيصالات التسليم (ما يصل إلى 20 عضوا). -\n- أسرع وأكثر استقرارًا.</string> + <string name="v5_3_discover_join_groups_descr">- الاتصال بخدمة الدليل (تجريبي)!\n- إيصالات التسليم (ما يصل إلى 20 عضوا).\n- أسرع وأكثر استقرارًا.</string> <string name="rcv_group_event_open_chat">افتح</string> <string name="error_creating_member_contact">حدث خطأ أثناء إنشاء جهة اتصال للعضو</string> <string name="compose_send_direct_message_to_connect">أرسل رسالة مباشرة للاتصال</string> @@ -1405,8 +1372,1008 @@ <string name="v5_4_better_groups">مجموعات أفضل</string> <string name="rcv_group_and_other_events">و%d أحداث أخرى</string> <string name="connect_plan_already_joining_the_group">جارٍ انضمام بالفعل إلى المجموعة!</string> - <string name="block_member_confirmation">حجب</string> + <string name="block_member_confirmation">حظر</string> <string name="bad_desktop_address">عنوان سطح المكتب غير صالح</string> <string name="block_member_desc">سيتم إخفاء كافة الرسائل الجديدة من %s!</string> - <string name="blocked_item_description">محجوب</string> -</resources> \ No newline at end of file + <string name="blocked_item_description">محظور</string> + <string name="v5_4_block_group_members">حظر أعضاء المجموعة</string> + <string name="rcv_direct_event_contact_deleted">جهة الاتصال حُذفت</string> + <string name="v5_4_incognito_groups_descr">أنشئ مجموعة باستخدام ملف تعريف عشوائي.</string> + <string name="create_group_button">أنشئ مجموعة</string> + <string name="create_another_profile_button">أنشئ ملف تعريف</string> + <string name="connected_desktop">سطح المكتب متصل</string> + <string name="multicast_connect_automatically">اتصل تلقائيًا</string> + <string name="desktop_address">عنوان سطح المكتب</string> + <string name="marked_deleted_items_description">وضّع علامة "محذوفة" على %d من الرسائل</string> + <string name="discover_on_network">اكتشف عبر الشبكة المحلية</string> + <string name="connect_plan_connect_via_link">اتصل عبر الرابط؟</string> + <string name="connect_plan_connect_to_yourself">اتصل بنفسك؟</string> + <string name="desktop_device">سطح المكتب</string> + <string name="connected_to_desktop">متصل بسطح المكتب</string> + <string name="connecting_to_desktop">جار الاتصال بسطح المكتب</string> + <string name="desktop_devices">أجهزة سطح المكتب</string> + <string name="correct_name_to">الاسم الصحيح لـ%s؟</string> + <string name="delete_messages__question">حذف %d رسالة؟</string> + <string name="connect_with_contact_name_question">اتصل مع %1$s؟</string> + <string name="blocked_items_description">%d رسالة محظورة</string> + <string name="block_member_button">حظر العضو</string> + <string name="connected_mobile">الجوّال متصل</string> + <string name="delete_and_notify_contact">حذف وإشعار جهة الاتصال</string> + <string name="desktop_connection_terminated">انتهى الاتصال</string> + <string name="connect_to_desktop">اتصل بسطح المكتب</string> + <string name="disconnect_remote_host">قطع الاتصال</string> + <string name="block_member_question">حظر العضو؟</string> + <string name="rcv_group_events_count">%d أحداث مجموعة</string> + <string name="group_member_role_author">الكاتب</string> + <string name="connected_to_mobile">متصل بالجوّال</string> + <string name="devices">الأجهزة</string> + <string name="multicast_discoverable_via_local_network">مُكتشف عبر الشبكة المحلية</string> + <string name="disconnect_desktop_question">قطع اتصال سطح المكتب؟</string> + <string name="desktop_app_version_is_incompatible">إصدار تطبيق سطح المكتب %s غير متوافق مع هذا التطبيق.</string> + <string name="expand_verb">توسيع</string> + <string name="connect_plan_repeat_connection_request">هل تريد تكرار طلب الاتصال؟</string> + <string name="encryption_renegotiation_error">خطأ في إعادة التفاوض بشأن التعمية</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[أنت متصل بالفعل بـ <b>%1$s</b>.]]></string> + <string name="error_alert_title">خطأ</string> + <string name="connect_plan_you_are_already_joining_the_group_via_this_link">لقد انضممت بالفعل إلى المجموعة عبر هذا الرابط.</string> + <string name="group_members_2">%s و%s</string> + <string name="new_mobile_device">جوّال جديد</string> + <string name="only_one_device_can_work_at_the_same_time">يمكن لجهاز واحد فقط العمل في نفس الوقت</string> + <string name="connect_plan_join_your_group">انضمام إلى مجموعتك؟</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[أنت تنضم بالفعل إلى المجموعة <b>%1$s</b>.]]></string> + <string name="v5_4_link_mobile_desktop">ربط تطبيقات الجوّال وسطح المكتب! 🔗</string> + <string name="connect_plan_this_is_your_own_one_time_link">هذا هو الرابط الخاص بك لمرة واحدة!</string> + <string name="connect_plan_group_already_exists">المجموعة موجودة بالفعل!</string> + <string name="v5_4_link_mobile_desktop_descr">عبر بروتوكول آمن مقاوم للكم.</string> + <string name="open_on_mobile_and_scan_qr_code"><![CDATA[افتح <i>الاستخدام من سطح المكتب</i> في تطبيق الجوّال وامسح رمز QR ضوئيًا.]]></string> + <string name="v5_4_block_group_members_descr">لإخفاء الرسائل غير المرغوب فيها.</string> + <string name="desktop_incompatible_version">نُسخة غير متوافقة</string> + <string name="new_desktop"><![CDATA[<i>(جديد)</i>]]></string> + <string name="unlink_desktop_question">فك ربط سطح المكتب؟</string> + <string name="linked_desktop_options">خيارات سطح المكتب المرتبطة</string> + <string name="video_decoding_exception_desc">لا يمكن فك تشفير الفيديو. من فضلك، جرب مقطع فيديو مختلفًا أو اتصل بالمطورين.</string> + <string name="rcv_group_event_1_member_connected">%s متصل</string> + <string name="linked_desktops">أسطح المكتب المرتبطة</string> + <string name="v5_4_incognito_groups">مجموعات التخفي</string> + <string name="group_members_n">%s و%s و%d عضو</string> + <string name="this_device">هذا الجهاز</string> + <string name="moderated_items_description">%1$d من الرسائل أُشرف عليها بواسطة %2$s</string> + <string name="unblock_member_button">إلغاء حظر العضو</string> + <string name="remote_host_was_disconnected_toast"><![CDATA[جوّال <b>%s</b> قُطع اتصاله]]></string> + <string name="waiting_for_desktop">في انتظار سطح المكتب…</string> + <string name="v5_4_better_groups_descr">انضمام أسرع ورسائل أكثر موثوقية.</string> + <string name="linked_mobiles">الجوّالات المرتبطة</string> + <string name="contact_tap_to_connect">انقر للاتصال</string> + <string name="this_device_name">اسم هذا الجهاز</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[أنت بالفعل في المجموعة <b>%1$s</b>.]]></string> + <string name="connect_plan_this_is_your_own_simplex_address">هذا هو عنوان SimpleX الخاص بك!</string> + <string name="loading_remote_file_title">جارِ تحميل الملف</string> + <string name="found_desktop">وجدت سطح المكتب</string> + <string name="alert_text_encryption_renegotiation_failed">فشلت إعادة التفاوض على التعمية.</string> + <string name="not_compatible">غير متوافق!</string> + <string name="link_a_mobile">ربط الجوّال</string> + <string name="remove_member_button">إزالة العضو</string> + <string name="unblock_member_question">إلغاء حظر العضو؟</string> + <string name="settings_section_title_use_from_desktop">استخدم من سطح المكتب</string> + <string name="session_code">رمز الجلسة</string> + <string name="connect_plan_repeat_join_request">هل تريد تكرار طلب الانضمام؟</string> + <string name="button_remove_member_question">إزالة العضو؟</string> + <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">أنت متصل بالفعل عبر هذا الرابط لمرة واحدة!</string> + <string name="connect_plan_open_group">افتح المجموعة</string> + <string name="this_device_version"><![CDATA[<i>(هذا الجهاز v%s)</i>]]></string> + <string name="unlink_desktop">فك الربط</string> + <string name="unblock_member_desc">سيتم عرض الرسائل من %s!</string> + <string name="this_device_name_shared_with_mobile">ستتم مشاركة اسم الجهاز مع عميل الجوّال متصل.</string> + <string name="error_sending_message_contact_invitation">حدث خطأ أثناء إرسال الدعوة</string> + <string name="verify_code_on_mobile">تحقق من الرمز على الجوّال</string> + <string name="enter_this_device_name">أدخل اسم الجهاز هذا…</string> + <string name="error">خطأ</string> + <string name="non_content_uri_alert_text">لقد شاركت مسار ملف غير صالح. أبلغ عن المشكلة لمطوري التطبيق.</string> + <string name="invalid_name">اسم غير صالح!</string> + <string name="paste_desktop_address">لصق عنوان سطح المكتب</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[هذا هو الرابط الخاص بك للمجموعة <b>%1$s</b>!]]></string> + <string name="verify_code_with_desktop">تحقق من الرمز مع سطح المكتب</string> + <string name="scan_qr_code_from_desktop">مسح رمز QR من سطح المكتب</string> + <string name="unblock_member_confirmation">إلغاء الحظر</string> + <string name="v5_4_more_things_descr">- إشعار اختياريًا جهات الاتصال المحذوفة.\n- أسماء الملفات التعريفية بمسافات.\n- و اكثر!</string> + <string name="non_content_uri_alert_title">مسار الملف غير صالح</string> + <string name="connect_plan_you_have_already_requested_connection_via_this_address">لقد طلبت بالفعل الاتصال عبر هذا العنوان!</string> + <string name="terminal_always_visible">إظهار وحدة التحكم في نافذة جديدة</string> + <string name="scan_from_mobile">المسح من الجوّال</string> + <string name="verify_connections">تحقق من الاتصالات</string> + <string name="loading_remote_file_desc">من فضلك، انتظر حتى يتم تحميل الملف من الجوّال المرتبط</string> + <string name="verify_connection">تحقق من الاتصال</string> + <string name="refresh_qr_code">أعِد التحميل</string> + <string name="random_port">عشوائي</string> + <string name="waiting_for_mobile_to_connect">في انتظار اتصال الجوال:</string> + <string name="open_port_in_firewall_desc">للسماح لتطبيق الجوال بالاتصال بسطح المكتب، افتح هذا المنفذ في جدار الحماية لديك، إذا فعلته</string> + <string name="create_chat_profile">أنشئ ملف تعريف الدردشة</string> + <string name="app_was_crashed">عرض التحطم</string> + <string name="open_port_in_firewall_title">فتح منفذ في جدار الحماية</string> + <string name="disconnect_remote_hosts">اقطع اتصال الجوالات</string> + <string name="no_connected_mobile">لا يوجد جوال متصل</string> + <string name="error_showing_content">خطأ في إظهار المحتوى</string> + <string name="error_showing_message">خطأ في إظهار الرسالة</string> + <string name="callstatus_ended">انتهت المكالمة %1$s</string> + <string name="you_can_make_address_visible_via_settings">يمكنك جعله مرئيًا لجهات اتصال SimpleX الخاصة بك عبر الإعدادات.</string> + <string name="recent_history_is_not_sent_to_new_members">لا يتم إرسال التاريخ إلى الأعضاء الجدد.</string> + <string name="retry_verb">حاول مجددًا</string> + <string name="camera_not_available">الكاميرا غير متوفرة</string> + <string name="enable_sending_recent_history">أرسل ما يصل إلى 100 رسالة أخيرة للأعضاء الجدد.</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>إضافة جهة اتصال</b>: لإنشاء رابط دعوة جديد، أو الاتصال عبر رابط استلمته.]]></string> + <string name="disable_sending_recent_history">لا ترسل التاريخ للأعضاء الجدد.</string> + <string name="or_show_this_qr_code">أو أظهر هذا الرمز</string> + <string name="recent_history_is_sent_to_new_members">يتم إرسال ما يصل إلى 100 رسالة أخيرة إلى الأعضاء الجدد.</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">الرمز الذي مسحته ضوئيًا ليس رمز QR لرابط SimpleX.</string> + <string name="the_text_you_pasted_is_not_a_link">النص الذي لصقته ليس رابط SimpleX.</string> + <string name="enable_camera_access">فعّل الوصول إلى الكاميرا</string> + <string name="you_can_view_invitation_link_again">يمكنك عرض رابط الدعوة مرة أخرى في تفاصيل الاتصال.</string> + <string name="keep_unused_invitation_question">أبقِ الدعوة غير المستخدمة؟</string> + <string name="share_this_1_time_link">شارك رابط الدعوة هذا لمرة واحدة</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>أنشئ مجموعة</b>: لإنشاء مجموعة جديدة.]]></string> + <string name="recent_history">التاريخ المرئي</string> + <string name="la_app_passcode">رمز مرور التطبيق</string> + <string name="new_chat">دردشة جديدة</string> + <string name="loading_chats">جارِ تحميل الدردشات…</string> + <string name="creating_link">جارِ إنشاء الرابط…</string> + <string name="or_scan_qr_code">أو امسح رمز QR</string> + <string name="invalid_qr_code">رمز QR غير صالح</string> + <string name="add_contact_tab">أضِف جهة اتصال</string> + <string name="tap_to_scan">انقر للمسح الضوئي</string> + <string name="keep_invitation_link">أبقِ</string> + <string name="tap_to_paste_link">انقر للصق الرابط</string> + <string name="search_or_paste_simplex_link">ابحث أو ألصِق رابط SimpleX</string> + <string name="start_chat_question">بدء الدردشة؟</string> + <string name="chat_is_stopped_you_should_transfer_database">توقفت الدردشة. إذا كنت قد استخدمت قاعدة البيانات هذه بالفعل على جهاز آخر، فيجب عليك نقلها مرة أخرى قبل بدء الدردشة.</string> + <string name="show_internal_errors">اعرض الأخطاء الداخلية</string> + <string name="agent_critical_error_title">خطأ فادح</string> + <string name="agent_internal_error_title">خطأ داخلي</string> + <string name="agent_internal_error_desc">يُرجى إبلاغ المطورين بذلك: +\n%s</string> + <string name="agent_critical_error_desc">يُرجى إبلاغ المطورين بذلك: +\n%s +\n +\nيوصى بإعادة تشغيل التطبيق.</string> + <string name="restart_chat_button">أعد تشغيل الدردشة</string> + <string name="remote_host_error_inactive"><![CDATA[الجوال <b>%s</b> غير نشط]]></string> + <string name="show_slow_api_calls">أظهر مكالمات API البطيئة</string> + <string name="group_member_status_unknown_short">غير معروف</string> + <string name="profile_update_event_updated_profile">حدّثت ملف التعريف</string> + <string name="remote_host_error_missing"><![CDATA[الجوال <b>%s</b> مفقود]]></string> + <string name="remote_host_error_bad_version"><![CDATA[الجوال <b>%s</b> لديه إصدار غير مدعوم. يُرجى التأكد من استخدام نفس الإصدار على كلا الجهازين]]></string> + <string name="remote_host_error_bad_state"><![CDATA[الاتصال بالجوال <b>%s</b> في حالة سيئة]]></string> + <string name="failed_to_create_user_invalid_title">اسم العرض غير صالح!</string> + <string name="failed_to_create_user_invalid_desc">اسم العرض هذا غير صالح. الرجاء اختيار اسم آخر.</string> + <string name="remote_host_was_disconnected_title">توقف الاتصال</string> + <string name="remote_ctrl_was_disconnected_title">توقف الاتصال</string> + <string name="remote_host_disconnected_from"><![CDATA[قُطع الاتصال بالجوال <b>%s</b> بسبب: %s]]></string> + <string name="remote_ctrl_disconnected_with_reason">قُطع الاتصال بسبب: %s</string> + <string name="remote_host_error_disconnected"><![CDATA[قُطع اتصال الجوال <b>%s</b>]]></string> + <string name="remote_host_error_timeout"><![CDATA[انتهت المهلة أثناء الاتصال بالجوال <b>%s</b>]]></string> + <string name="remote_ctrl_error_inactive">سطح المكتب غير نشط</string> + <string name="remote_host_error_busy"><![CDATA[الجوال <b>%s</b> مشغول]]></string> + <string name="remote_ctrl_error_timeout">انتهت المهلة أثناء الاتصال بسطح المكتب</string> + <string name="remote_ctrl_error_disconnected">قُطع اتصال سطح المكتب</string> + <string name="remote_ctrl_error_bad_state">الاتصال بسطح المكتب في حالة سيئة</string> + <string name="remote_ctrl_error_bad_invitation">يحتوي سطح المكتب على رمز دعوة خاطئ</string> + <string name="remote_ctrl_error_busy">سطح المكتب مشغول</string> + <string name="remote_ctrl_error_bad_version">يحتوي سطح المكتب على إصدار غير مدعوم. يُرجى التأكد من استخدام نفس الإصدار على كلا الجهازين</string> + <string name="past_member_vName">العضو السابق %1$s</string> + <string name="possible_slow_function_title">وظيفة بطيئة</string> + <string name="developer_options_section">خيارات المطور</string> + <string name="profile_update_event_member_name_changed">تغيّر العضو %1$s إلى %2$s</string> + <string name="profile_update_event_removed_address">أزلت عنوان الاتصال</string> + <string name="profile_update_event_removed_picture">أزلت صورة ملف التعريف</string> + <string name="profile_update_event_set_new_address">عيّن عنوان جهة اتصال جديد</string> + <string name="profile_update_event_set_new_picture">عيّن صورة تعريفية جديدة</string> + <string name="group_member_status_unknown">حالة غير معروفة</string> + <string name="profile_update_event_contact_name_changed">تغيّر جهة الاتصال %1$s إلى %2$s</string> + <string name="possible_slow_function_desc">يستغرق تنفيذ الوظيفة وقتًا طويلاً جدًا: %1$d ثانية: %2$s</string> + <string name="v5_5_private_notes">ملاحظات خاصة</string> + <string name="v5_5_join_group_conversation">انضم إلى المحادثات الجماعية</string> + <string name="v5_5_simpler_connect_ui_descr">يقبل شريط البحث روابط الدعوة.</string> + <string name="v5_5_message_delivery">تحسّن تسليم الرسائل</string> + <string name="v5_5_message_delivery_descr">مع انخفاض استخدام البطارية.</string> + <string name="clear_note_folder_warning">سيتم حذف كافة الرسائل - لا يمكن التراجع عن هذا!</string> + <string name="info_row_created_at">أُنشئ في</string> + <string name="v5_5_new_interface_languages">واجهة المستخدم المجرية والتركية</string> + <string name="v5_5_simpler_connect_ui">الصق الرابط للاتصال!</string> + <string name="v5_5_join_group_conversation_descr">التاريخ الحديث وبوت الدليل المحسن.</string> + <string name="v5_5_private_notes_descr">مع الملفات والوسائط المُعمّاة.</string> + <string name="error_creating_message">حدث خطأ أثناء إنشاء الرسالة</string> + <string name="error_deleting_note_folder">حدث خطأ أثناء حذف الملاحظات الخاصة</string> + <string name="note_folder_local_display_name">ملاحظات خاصة</string> + <string name="clear_note_folder_question">مسح الملاحظات الخاصة؟</string> + <string name="share_text_created_at">أُنشئ في: %s</string> + <string name="saved_message_title">رسالة محفوظة</string> + <string name="unblock_for_all_question">إلغاء حظر العضو للجميع؟</string> + <string name="unblock_for_all">إلغاء الحظر للجميع</string> + <string name="error_blocking_member_for_all">حدث خطأ أثناء حظر العضو للجميع</string> + <string name="blocked_by_admin_items_description">حُظرت %d رسالة من قبل المُدير</string> + <string name="rcv_group_event_member_blocked">محظور %s</string> + <string name="rcv_group_event_member_unblocked">أُلغيت حظر %s</string> + <string name="snd_group_event_member_blocked">حظرت %s</string> + <string name="snd_group_event_member_unblocked">أُلغيت حظر %s</string> + <string name="member_info_member_blocked">محظور</string> + <string name="block_for_all">حظر للجميع</string> + <string name="block_for_all_question">حظر العضو للجميع؟</string> + <string name="blocked_by_admin_item_description">محظور من قبل المُدير</string> + <string name="member_blocked_by_admin">محظور من قبل المُدير</string> + <string name="message_too_large">الرسالة كبيرة جدًا</string> + <string name="welcome_message_is_too_long">رسالة الترحيب طويلة جدًا</string> + <string name="database_migration_in_progress">ترحيل قاعدة البيانات قيد التقدم. +\nقد يستغرق بضع دقائق.</string> + <string name="call_service_notification_video_call">مكالمة فيديو</string> + <string name="call_service_notification_audio_call">مكالمة صوتية</string> + <string name="call_service_notification_end_call">أنهيّ المكالمة</string> + <string name="unable_to_open_browser_desc">متصفح الويب الافتراضي مطلوب للمكالمات. يُرجى تضبيط المتصفح الافتراضي في النظام، ومشاركة المزيد من المعلومات مع المطورين.</string> + <string name="unable_to_open_browser_title">حدث خطأ أثناء فتح المتصفح</string> + <string name="migrate_from_device_archive_and_upload">أرشفة و رفع</string> + <string name="v5_6_safer_groups_descr">يمكن للمُدراء حظر عضو للجميع.</string> + <string name="v5_6_app_data_migration">ترحيل بيانات التطبيق</string> + <string name="migrate_from_device_archiving_database">جارِ أرشفة قاعدة البيانات</string> + <string name="migrate_from_device_all_data_will_be_uploaded">جميع جهات الاتصال، المحادثات والملفات الخاصة بك سيتم تشفيرها بأمان ورفعها على شكل أجزاء إلى موجهات XFTP المُعدة.</string> + <string name="migrate_to_device_apply_onion">طبّق</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>يُرجى ملاحظة</b>: استخدام نفس قاعدة البيانات على جهازين سيؤدي إلى كسر فك تعمية الرسائل من اتصالاتك، كحماية أمنية.]]></string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>تحذير</b>: سيتم حذف الأرشيف.]]></string> + <string name="e2ee_info_no_pq"><![CDATA[تتم حماية الرسائل والملفات والمكالمات عن طريق <b>التعمية بين الطرفين</b> مع توفير السرية التامة لإعادة التوجيه والرفض واستعادة عمليات الاقتحام.]]></string> + <string name="e2ee_info_pq"><![CDATA[تتم حماية الرسائل والملفات والمكالمات عن طريق <b>التعمية بين الطرفين المقاوم للكم</b> مع توفير السرية التامة لإعادة التوجيه والرفض واستعادة عمليات الاقتحام.]]></string> + <string name="e2ee_info_no_pq_short">هذه الدردشة محمية بالتعمية بين الطرفين.</string> + <string name="e2ee_info_pq_short">هذه الدردشة محمية بالتعمية بين الطرفين المقاوم للكم.</string> + <string name="auth_open_migration_to_another_device">افتح شاشة الترحيل</string> + <string name="set_passphrase">عيّن عبارة المرور</string> + <string name="conn_event_enabled_pq">التعمية بين الطرفين المقاوم للكم</string> + <string name="conn_event_disabled_pq">التعمية القياسية بين الطرفين</string> + <string name="migrate_to_device_migrating">جارِ الترحيل</string> + <string name="migrate_to_device_downloading_details">جارِ تنزيل تفاصيل الرابط</string> + <string name="migrate_to_device_downloading_archive">جارِ تنزيل الأرشيف</string> + <string name="migrate_to_device_bytes_downloaded">نُزّل %s</string> + <string name="migrate_to_device_download_failed">فشل التنزيل</string> + <string name="migrate_to_device_repeat_download">كرر التنزيل</string> + <string name="migrate_to_device_import_failed">فشل الاستيراد</string> + <string name="migrate_to_device_repeat_import">كرر الاستيراد</string> + <string name="migrate_to_device_enter_passphrase">أدخل عبارة المرور</string> + <string name="migrate_to_device_file_delete_or_link_invalid">حُذف الملف أو الرابط غير صالح</string> + <string name="migrate_to_device_chat_migrated">رحّلت الدردشة!</string> + <string name="migrate_to_device_confirm_network_settings">أكّد إعدادات الشبكة</string> + <string name="migrate_to_device_finalize_migration">إنهاء الترحيل على جهاز آخر.</string> + <string name="migrate_from_device_to_another_device">رحّل إلى جهاز آخر</string> + <string name="migrate_from_device_database_init">جارِ تحضير الرفع</string> + <string name="migrate_from_device_error_uploading_archive">حدث خطأ أثناء رفع الأرشيف</string> + <string name="migrate_from_device_error_deleting_database">خطأ في حذف قاعدة البيانات</string> + <string name="v5_6_quantum_resistant_encryption">التعمية المقاوم للكم</string> + <string name="v5_6_quantum_resistant_encryption_descr">فعّل في الدردشات المباشرة (تجريبي)!</string> + <string name="paste_archive_link">ألصق رابط الأرشيف</string> + <string name="migrate_to_device_try_again">يمكنك إعطاء محاولة أخرى.</string> + <string name="migrate_to_device_error_downloading_archive">حدث خطأ أثناء تنزيل الأرشيف</string> + <string name="migrate_from_device_exported_file_doesnt_exist">الملف المُصدّر غير موجود</string> + <string name="migrate_from_device_verify_passphrase">تحقق من عبارة المرور</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">تأكد من أنك تتذكر عبارة مرور قاعدة البيانات لترحيلها.</string> + <string name="migrate_from_device_verify_database_passphrase">التحقق من عبارة مرور قاعدة البيانات</string> + <string name="error_showing_desktop_notification">خطأ في عرض الإشعار، تواصل بالمطورين.</string> + <string name="v5_6_picture_in_picture_calls">مكالمات صورة في صورة</string> + <string name="v5_6_picture_in_picture_calls_descr">استخدم التطبيق أثناء المكالمة.</string> + <string name="v5_6_app_data_migration_descr">رحّل إلى جهاز آخر عبر رمز QR.</string> + <string name="v5_6_safer_groups">مجموعات أكثر أمانا</string> + <string name="migrate_to_device_title">رحّل هنا</string> + <string name="migrate_to_device_database_init">جارِ تحضير التنزيل</string> + <string name="or_paste_archive_link">أو ألصِق رابط الأرشيف</string> + <string name="migrate_to_device_importing_archive">جارِ استيراد الأرشيف</string> + <string name="invalid_file_link">الرابط غير صالح</string> + <string name="migrate_from_device_error_saving_settings">خطأ في حفظ الإعدادات</string> + <string name="migrate_from_device_title">رحّل الجهاز</string> + <string name="migrate_from_device_confirm_upload">أكّد الرفع</string> + <string name="migrate_from_device_creating_archive_link">جارِ إنشاء رابط الأرشيف</string> + <string name="migrate_from_device_cancel_migration">ألغِ الترحيل</string> + <string name="migrate_from_device_delete_database_from_device">احذف قاعدة البيانات من هذا الجهاز</string> + <string name="migrate_from_device_start_chat">ابدأ الدردشة</string> + <string name="migrate_from_device_migration_complete">اكتمل الترحيل</string> + <string name="migrate_from_device_check_connection_and_try_again">تحقق من اتصالك بالإنترنت وحاول مرة أخرى</string> + <string name="migrate_from_device_error_verifying_passphrase">خطأ في التحقق من عبارة المرور:</string> + <string name="migrate_from_device_finalize_migration">إنهاء الترحيل</string> + <string name="permissions_grant">منح الإذن/ونات لإجراء المكالمات</string> + <string name="migrate_to_device_confirm_network_settings_footer">الرجاء التأكد من صحة إعدادات الشبكة لهذا الجهاز.</string> + <string name="permissions_camera">الكاميرا</string> + <string name="permissions_camera_and_record_audio">الكاميرا والميكروفون</string> + <string name="permissions_find_in_settings_and_grant">ابحث عن هذا الإذن في إعدادات أندرويد وامنحه يدويًا.</string> + <string name="permissions_grant_in_settings">منح في الإعدادات</string> + <string name="permissions_required">منح الأذونات</string> + <string name="permissions_record_audio">الميكروفون</string> + <string name="migrate_from_another_device">رحّل من جهاز آخر</string> + <string name="permissions_open_settings">افتح الإعدادات</string> + <string name="migrate_from_device_error_exporting_archive">حدث خطأ أثناء تصدير قاعدة بيانات الدردشة</string> + <string name="migrate_from_device_chat_should_be_stopped">للاستمرار، يجب إيقاف الدردشة.</string> + <string name="migrate_from_device_or_share_this_file_link">أو شارك رابط الملف هذا بشكل آمن</string> + <string name="migrate_from_device_repeat_upload">كرر الرفع</string> + <string name="migrate_from_device_stopping_chat">جارِ إيقاف الدردشة</string> + <string name="migrate_from_device_bytes_uploaded">رُفع %s</string> + <string name="migrate_from_device_upload_failed">فشل الرفع</string> + <string name="migrate_from_device_uploading_archive">جارِ الرفع الأرشيف</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[<b>لا يجوز</b> استخدام نفس قاعدة البيانات على جهازين.]]></string> + <string name="migrate_from_device_try_again">يمكنك إعطاء محاولة أخرى.</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[اختر <i>رحّل من جهاز آخر</i> على الجهاز الجديد و امسح رمز QR ضوئيًا.]]></string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">تحذير: بدء الدردشة على أجهزة متعددة غير مدعوم وسيؤدي إلى فشل تسليم الرسائل</string> + <string name="settings_section_title_network_connection">إتصال شبكة</string> + <string name="forwarded_description">محوّلة</string> + <string name="network_type_cellular">خلوي</string> + <string name="network_type_ethernet">إيثرنت سلكية</string> + <string name="network_type_no_network_connection">لا إتصال شبكة</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="network_type_other">أخرى</string> + <string name="simplex_links_not_allowed">روابط SimpleX غير مسموح بها</string> + <string name="voice_messages_not_allowed">الرسائل الصوتية غير مسموح بها</string> + <string name="simplex_links">روابط SimpleX</string> + <string name="allow_to_send_simplex_links">السماح بإرسال روابط SimpleX.</string> + <string name="prohibit_sending_simplex_links">منع إرسال روابط SimpleX</string> + <string name="feature_roles_all_members">كل الأعضاء</string> + <string name="group_members_can_send_simplex_links">يمكن للأعضاء إرسال روابط SimpleX.</string> + <string name="simplex_links_are_prohibited_in_group">روابط SimpleX محظورة.</string> + <string name="feature_roles_admins">المُدراء</string> + <string name="feature_enabled_for">مفعّل لـ</string> + <string name="feature_roles_owners">المالكون</string> + <string name="files_and_media_not_allowed">الملفات والوسائط غير مسموح بها</string> + <string name="forwarded_chat_item_info_tab">محوّلة</string> + <string name="forward_message">حوّل الرسالة…</string> + <string name="network_option_rcv_concurrency">تلقي التزامن</string> + <string name="recipients_can_not_see_who_message_from">لا يستطيع المُستلم/ون معرفة مَن أرسل هذه الرسالة.</string> + <string name="saved_description">حُفظت</string> + <string name="saved_from_chat_item_info_title">حُفظت مِن</string> + <string name="saved_from_description">حُفظت مِن %s</string> + <string name="audio_device_speaker">السماعة</string> + <string name="audio_device_earpiece">سماعة الأذن</string> + <string name="audio_device_wired_headphones">سماعات الرأس</string> + <string name="forwarded_from_chat_item_info_title">محوّلة مِن</string> + <string name="saved_chat_item_info_tab">حُفظت</string> + <string name="download_file">نزّل</string> + <string name="forward_chat_item">حوّل</string> + <string name="audio_device_bluetooth">بلوتوث</string> + <string name="v5_7_quantum_resistant_encryption_descr">سيتم تفعيله في المحادثات المباشرة!</string> + <string name="v5_7_forward">حوّل واحفظ الرسائل</string> + <string name="v5_7_call_sounds">أصوات أثناء المكالمة</string> + <string name="v5_7_forward_descr">يظل مصدر الرسالة خاصًا.</string> + <string name="v5_7_call_sounds_descr">عند اتصال بمكالمات الصوت والفيديو.</string> + <string name="v5_7_network">إدارة الشبكة</string> + <string name="v5_7_network_descr">اتصال شبكة أكثر موثوقية.</string> + <string name="settings_section_title_profile_images">صور ملف التعريف</string> + <string name="v5_7_shape_profile_images">شكّل الصور التعريفية</string> + <string name="v5_7_new_interface_languages">واجهة المستخدم الليتوانية</string> + <string name="v5_7_shape_profile_images_descr">مربع أو دائرة أو أي شيء بينهما.</string> + <string name="srv_error_host">عنوان الخادم غير متوافق مع إعدادات الشبكة.</string> + <string name="srv_error_version">إصدار الخادم غير متوافق مع إعدادات الشبكة.</string> + <string name="snd_error_auth">مفتاح خاطئ أو اتصال غير معروف - على الأرجح حُذف هذا الاتصال.</string> + <string name="snd_error_quota">تم تجاوز السعة - لم يتلق المُستلم الرسائل المُرسلة مسبقًا.</string> + <string name="snd_error_relay">خطأ في خادم الوجهة: %1$s</string> + <string name="ci_status_other_error">خطأ: %1$s</string> + <string name="snd_error_proxy_relay">خادم التحويل: %1$s +\nخطأ في الخادم الوجهة: %2$s</string> + <string name="snd_error_proxy">خادم التحويل: %1$s +\nخطأ: %2$s</string> + <string name="message_delivery_warning_title">تحذير تسليم الرسالة</string> + <string name="snd_error_expired">مشكلات الشبكة - انتهت صلاحية الرسالة بعد عِدة محاولات لإرسالها.</string> + <string name="network_smp_proxy_fallback_allow">نعم</string> + <string name="private_routing_explanation">لحماية عنوان IP الخاص بك، يستخدم التوجيه الخاص خوادم SMP الخاصة بك لتسليم الرسائل.</string> + <string name="settings_section_title_private_message_routing">توجيه الرسائل الخاصة</string> + <string name="network_smp_proxy_mode_private_routing">التوجيه الخاص</string> + <string name="network_smp_proxy_mode_unprotected">غير محمي</string> + <string name="network_smp_proxy_fallback_allow_downgrade">السماح بالرجوع إلى إصدار سابق</string> + <string name="network_smp_proxy_mode_never_description">لا تستخدم التوجيه الخاص.</string> + <string name="update_network_smp_proxy_mode_question">وضع توجيه الرسائل</string> + <string name="network_smp_proxy_fallback_prohibit">لا</string> + <string name="network_smp_proxy_fallback_allow_protected">عندما يكون IP مخفيًا</string> + <string name="network_smp_proxy_fallback_prohibit_description">لا ترسل رسائل مباشرةً، حتى لو كان خادمك أو خادم الوجهة لا يدعم التوجيه الخاص.</string> + <string name="network_smp_proxy_fallback_allow_description">أرسل الرسائل مباشرة عندما لا يدعم الخادم الوجهة الخاص بك أو الخادم الوجهة التوجيه الخاص.</string> + <string name="update_network_smp_proxy_fallback_question">احتياطي توجيه الرسالة</string> + <string name="private_routing_show_message_status">أظهِر حالة الرسالة</string> + <string name="protect_ip_address">احمِ عنوان IP</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">بدون تور أو VPN، سيكون عنوان IP الخاص بك مرئيًا لخوادم الملفات.</string> + <string name="network_smp_proxy_mode_unknown_description">استخدم التوجيه الخاص مع خوادم غير معروفة.</string> + <string name="network_smp_proxy_mode_unprotected_description">استخدم التوجيه الخاص مع خوادم غير معروفة عندما لا يكون عنوان IP محميًا.</string> + <string name="network_smp_proxy_mode_always">دائمًا</string> + <string name="network_smp_proxy_mode_always_description">استخدم دائمًا التوجيه الخاص.</string> + <string name="settings_section_title_files">الملفات</string> + <string name="network_smp_proxy_mode_never">مطلقًا</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">سيطلب التطبيق تأكيد التنزيلات من خوادم ملفات غير معروفة (باستثناء .onion أو عند تفعيل وكيل SOCKS).</string> + <string name="network_smp_proxy_fallback_allow_protected_description">أرسل الرسائل مباشرة عندما يكون عنوان IP محميًا ولا يدعم الخادم الوجهة لديك التوجيه الخاص.</string> + <string name="network_smp_proxy_mode_unknown">خوادم غير معروفة</string> + <string name="file_not_approved_title">خوادم غير معروفة!</string> + <string name="file_not_approved_descr">بدون تور أو VPN، سيكون عنوان IP الخاص بك مرئيًا لمُرحلات XFTP هذه: +\n%1$s.</string> + <string name="chat_list_always_visible">أظهِر قائمة الدردشة في نافذة جديدة</string> + <string name="settings_section_title_chat_colors">ألوان الدردشة</string> + <string name="settings_section_title_chat_theme">سمة الدردشة</string> + <string name="color_received_quote">تلقيت رد</string> + <string name="theme_remove_image">أزِل الصورة</string> + <string name="wallpaper_scale_repeat">تكرار</string> + <string name="reset_single_color">صفّر اللون</string> + <string name="color_sent_quote">أرسلت رد</string> + <string name="chat_theme_set_default_theme">تعيين السمة الافتراضية</string> + <string name="color_mode_system">النظام</string> + <string name="color_wallpaper_tint">لون تمييز خلفية الشاشة</string> + <string name="color_primary_variant2">لون إضافي ثانوي 2</string> + <string name="wallpaper_advanced_settings">الإعدادات المتقدمة</string> + <string name="chat_theme_apply_to_all_modes">جميع أوضاع الألوان</string> + <string name="theme_black">أسود</string> + <string name="color_mode">وضع اللون</string> + <string name="color_mode_dark">داكن</string> + <string name="chat_theme_apply_to_dark_mode">الوضع الداكن</string> + <string name="dark_mode_colors">ألوان الوضع الداكن</string> + <string name="wallpaper_scale_fit">ملائمة</string> + <string name="wallpaper_preview_hello_alice">طاب يومك!</string> + <string name="wallpaper_preview_hello_bob">صباح الخير!</string> + <string name="color_wallpaper_background">صورة خلفية الشاشة</string> + <string name="chat_theme_apply_to_light_mode">الوضع الفاتح</string> + <string name="settings_section_title_user_theme">سمة ملف التعريف</string> + <string name="color_mode_light">فاتح</string> + <string name="chat_theme_apply_to_mode">طبّق لِ</string> + <string name="wallpaper_scale_fill">ملء</string> + <string name="wallpaper_scale">المقياس</string> + <string name="message_queue_info_none">لا شيء</string> + <string name="v5_8_private_routing">توجيه الرسائل الخاصة 🚀</string> + <string name="v5_8_chat_themes_descr">اجعل محادثاتك تبدو مختلفة!</string> + <string name="v5_8_safe_files">تلقي الملفات بأمان</string> + <string name="v5_8_persian_ui">واجهة المستخدم الفارسية</string> + <string name="chat_theme_reset_to_app_theme">صفّر إلى سمة التطبيق</string> + <string name="theme_destination_app_theme">سمة التطبيق</string> + <string name="v5_8_safe_files_descr">تأكيد الملفات من خوادم غير معروفة.</string> + <string name="chat_theme_reset_to_user_theme">صفّر إلى سمة المستخدم</string> + <string name="message_queue_info_server_info">معلومات قائمة انتظار الخادم: %1$s +\n +\nآخر رسالة تم استلامها: %2$s</string> + <string name="info_row_debug_delivery">تسليم التصحيح</string> + <string name="message_queue_info">معلومات قائمة انتظار الرسائل</string> + <string name="v5_8_private_routing_descr">احمِ عنوان IP الخاص بك من مُرحلات المُراسلة التي اختارتها جهات اتصالك. \nفعّل في إعدادات *الشبكة والخوادم*.</string> + <string name="v5_8_chat_themes">سمات دردشة جديدة</string> + <string name="error_initializing_web_view">حدث خطأ أثناء تهيئة WebView. حدّث نظامك إلى الإصدار الجديد. يُرجى التواصل بالمطورين. +\nError: %s</string> + <string name="v5_8_message_delivery">تحسين تسليم الرسائل</string> + <string name="v5_8_message_delivery_descr">مع انخفاض استخدام البطارية.</string> + <string name="file_error_auth">مفتاح خاطئ أو عنوان مجموعة الملف غير معروف - على الأرجح حُذف الملف.</string> + <string name="file_error_relay">خطأ في خادم الملفات: %1$s</string> + <string name="file_error">خطأ في الملف</string> + <string name="temporary_file_error">خطأ في الملف مؤقت</string> + <string name="info_row_message_status">حالة الرسالة</string> + <string name="file_error_no_file">لم يتم العثور على الملف - على الأرجح حُذف الملف أو إلغاؤه.</string> + <string name="info_row_file_status">حالة الملف</string> + <string name="share_text_file_status">حالة الملف: %s</string> + <string name="share_text_message_status">حالة الرسالة: %s</string> + <string name="copy_error">خطأ في النسخ</string> + <string name="remote_ctrl_connection_stopped_identity_desc">تم استخدام هذا الرابط مع جهاز محمول آخر، يُرجى إنشاء رابط جديد على سطح المكتب.</string> + <string name="remote_ctrl_connection_stopped_desc">يُرجى التحقق من اتصال الهاتف المحمول وسطح المكتب بنفس الشبكة المحلية، وأن جدار حماية سطح المكتب يسمح بالاتصال. +\nيُرجى مشاركة أي مشاكل أُخرى مع المطورين.</string> + <string name="cannot_share_message_alert_title">لا يمكن إرسال الرسالة</string> + <string name="cannot_share_message_alert_text">تفضيلات الدردشة المحدّدة تحظر هذه الرسالة.</string> + <string name="servers_info_details">التفاصيل</string> + <string name="servers_info_private_data_disclaimer">بدءًا من %s.\nجميع البيانات خاصة على جهازك.</string> + <string name="servers_info_detailed_statistics_sent_messages_total">أرسلت الإجمالي</string> + <string name="size">الحجم</string> + <string name="uploaded_files">الملفات المرفوعة</string> + <string name="please_try_later">يُرجى المحاولة لاحقا.</string> + <string name="private_routing_error">خطأ في التوجيه الخاص</string> + <string name="network_error_broker_host_desc">عنوان الخادم غير متوافق مع إعدادات الشبكة: %1$s.</string> + <string name="network_error_broker_version_desc">إصدار الخادم غير متوافق مع تطبيقك: %1$s.</string> + <string name="member_inactive_title">العضو غير نشط</string> + <string name="message_forwarded_title">رسالة محوّلة</string> + <string name="message_forwarded_desc">لا يوجد اتصال مباشر حتى الآن، يتم تحويل من قِبل المُدير.</string> + <string name="scan_paste_link">امسح / ألصِق الرابط</string> + <string name="smp_servers_configured">خوادم SMP المهيأة</string> + <string name="smp_servers_other">خوادم SMP أخرى</string> + <string name="xftp_servers_configured">خوادم XFTP المهيأة</string> + <string name="xftp_servers_other">خوادم XFTP أخرى</string> + <string name="subscription_percentage">أظهِر النسبة المئوية</string> + <string name="app_check_for_updates_disabled">مُعطَّل</string> + <string name="app_check_for_updates_stable">مستقرّ</string> + <string name="app_check_for_updates_update_available">يتوفر تحديث: %s</string> + <string name="app_check_for_updates_notice_title">التمس التحديثات</string> + <string name="app_check_for_updates_button_download">نزّل %s (%s)</string> + <string name="app_check_for_updates_installed_successfully_title">ثُبّت بنجاح</string> + <string name="app_check_for_updates_button_open">افتح مكان الملف</string> + <string name="app_check_for_updates_installed_successfully_desc">يُرجى إعادة تشغيل التطبيق.</string> + <string name="app_check_for_updates_button_remind_later">تذكر لاحقا</string> + <string name="app_check_for_updates_button_skip">تخطي هذه النسخة</string> + <string name="app_check_for_updates_canceled">أُلغيت تنزيل التحديث</string> + <string name="member_info_member_disabled">مُعطَّل</string> + <string name="member_info_member_inactive">غير نشط</string> + <string name="servers_info">معلومات الخوادم</string> + <string name="servers_info_target">عرض المعلومات ل</string> + <string name="servers_info_sessions_errors">الأخطاء</string> + <string name="servers_info_messages_sent">الرسائل المُرسلة</string> + <string name="servers_info_subscriptions_total">الإجمالي</string> + <string name="servers_info_previously_connected_servers_section_header">الخوادم المتصلة سابقًا</string> + <string name="servers_info_reconnect_server_error">حدث خطأ أثناء إعادة الاتصال بالخادم</string> + <string name="servers_info_reconnect_server_title">أعِد توصيل الخادم؟</string> + <string name="servers_info_reconnect_server_message">أعِد التوصيل بالخادم لفرض تسليم الرسالة. يستخدم حركة مرور إضافية.</string> + <string name="servers_info_reset_stats">صفّر جميع الإحصائيات</string> + <string name="servers_info_reset_stats_alert_title">صفّر جميع الإحصائيات؟</string> + <string name="servers_info_downloaded">نُزّلت</string> + <string name="servers_info_detailed_statistics_received_messages_header">الرسائل المُستلمة</string> + <string name="servers_info_detailed_statistics_sent_messages_header">الرسائل المُرسلة</string> + <string name="servers_info_reset_stats_alert_message">سيتم تصفير إحصائيات الخوادم - لا يمكن التراجع عن هذا!</string> + <string name="servers_info_uploaded">رُفع</string> + <string name="servers_info_reset_stats_alert_confirm">صفّر</string> + <string name="servers_info_starting_from">بدءًا من %s.</string> + <string name="smp_server">خادم SMP</string> + <string name="xftp_server">خادم XFTP</string> + <string name="acknowledged">معترف به</string> + <string name="chunks_deleted">حُذفت القطع</string> + <string name="chunks_downloaded">نُزّلت القطع</string> + <string name="completed">اكتملت</string> + <string name="connections">الاتصالات</string> + <string name="created">أُنشئت</string> + <string name="decryption_errors">أخطاء فك التعمية</string> + <string name="deleted">حُذفت</string> + <string name="downloaded_files">الملفات التي نُزّلت</string> + <string name="download_errors">أخطاء التنزيل</string> + <string name="expired_label">منتهية الصلاحيّة</string> + <string name="open_server_settings_button">افتح إعدادات الخادم</string> + <string name="other_label">أخرى</string> + <string name="proxied">موّكل</string> + <string name="secured">مؤمن</string> + <string name="send_errors">أرسل الأخطاء</string> + <string name="sent_directly">أُرسلت مباشرةً</string> + <string name="sent_via_proxy">مُرسَل عبر الوكيل</string> + <string name="subscribed">مشترك</string> + <string name="subscription_errors">أخطاء الاشتراك</string> + <string name="upload_errors">رفع الأخطاء</string> + <string name="app_check_for_updates">التمس التحديثات</string> + <string name="acknowledgement_errors">أخطاء معترف بها</string> + <string name="app_check_for_updates_download_completed_title">نُزّل تحديث التطبيق</string> + <string name="all_users">جميع ملفات التعريف</string> + <string name="attempts_label">المحاولات</string> + <string name="app_check_for_updates_beta">تجريبي</string> + <string name="chunks_uploaded">رُفع القطع</string> + <string name="servers_info_sessions_connected">متصل</string> + <string name="servers_info_connected_servers_section_header">الخوادم المتصلة</string> + <string name="servers_info_sessions_connecting">جارِ الاتصال</string> + <string name="servers_info_subscriptions_connections_subscribed">الاتصالات النشطة</string> + <string name="current_user">ملف التعريف الحالي</string> + <string name="deletion_errors">أخطاء الحذف</string> + <string name="servers_info_detailed_statistics">إحصائيات مفصلة</string> + <string name="app_check_for_updates_notice_disable">عطّل</string> + <string name="servers_info_reset_stats_alert_error_title">خطأ في تصفير الإحصائيات</string> + <string name="duplicates_label">التكرارات</string> + <string name="servers_info_modal_error_title">خطأ</string> + <string name="servers_info_reconnect_servers_error">حدث خطأ أثناء إعادة الاتصال بالخوادم</string> + <string name="app_check_for_updates_download_started">جارٍ تنزيل تحديث التطبيق، لا تغلق التطبيق</string> + <string name="servers_info_files_tab">الملفات</string> + <string name="appearance_font_size">حجم الخط</string> + <string name="app_check_for_updates_button_install">ثبّت التحديث</string> + <string name="member_inactive_desc">قد يتم تسليم الرسالة لاحقًا إذا أصبح العضو نشطًا.</string> + <string name="servers_info_messages_received">الرسائل المُستلمة</string> + <string name="servers_info_subscriptions_section_header">استقبال الرسائل</string> + <string name="servers_info_missing">لا توجد معلومات، حاول إعادة التحميل</string> + <string name="other_errors">أخطاء أخرى</string> + <string name="servers_info_subscriptions_connections_pending">قيد الانتظار</string> + <string name="reconnect">أعِد التوصيل</string> + <string name="servers_info_reconnect_servers_message">أعِد توصيل كافة الخوادم المتصلة لفرض تسليم الرسالة. يستخدم حركة مرور إضافية.</string> + <string name="servers_info_proxied_servers_section_header">خوادم موّكلة</string> + <string name="servers_info_detailed_statistics_receive_errors">تلقي الأخطاء</string> + <string name="servers_info_detailed_statistics_received_total">تلقى الإجمالي</string> + <string name="servers_info_reconnect_all_servers_button">أعِد توصيل جميع الخوادم</string> + <string name="servers_info_reconnect_servers_title">أعِد توصيل الخوادم؟</string> + <string name="server_address">عنوان الخادم</string> + <string name="servers_info_statistics_section_header">الإحصائيات</string> + <string name="subscription_results_ignored">تم تجاهل الاشتراكات</string> + <string name="servers_info_transport_sessions_section_header">جلسات النقل</string> + <string name="app_check_for_updates_notice_desc">لكي يتم إعلامك بالإصدارات الجديدة، شغّل الفحص الدوري للإصدارات المستقرة أو التجريبية.</string> + <string name="servers_info_proxied_servers_section_footer">أنت غير متصل بهذه الخوادم. يتم استخدام التوجيه الخاص لتسليم الرسائل إليهم.</string> + <string name="appearance_zoom">قرّب</string> + <string name="smp_proxy_error_connecting">حدث خطأ أثناء الاتصال بخادم التحويل %1$s. يُرجى المحاولة لاحقا.</string> + <string name="smp_proxy_error_broker_host">عنوان خادم التحويل غير متوافق مع إعدادات الشبكة: %1$s.</string> + <string name="proxy_destination_error_broker_host">عنوان خادم الوجهة %1$s غير متوافق مع إعدادات خادم التحويل %2$s.</string> + <string name="proxy_destination_error_broker_version">إصدار الخادم الوجهة %1$s غير متوافق مع خادم التحويل %2$s.</string> + <string name="proxy_destination_error_failed_to_connect">فشل خادم التحويل %1$s في الاتصال بالخادم الوجهة %2$s. يُرجى المحاولة لاحقا.</string> + <string name="smp_proxy_error_broker_version">إصدار خادم التحويل غير متوافق مع إعدادات الشبكة: %1$s.</string> + <string name="privacy_media_blur_radius_off">مطفي</string> + <string name="privacy_media_blur_radius_strong">قوي</string> + <string name="privacy_media_blur_radius">تمويه الوسائط</string> + <string name="privacy_media_blur_radius_medium">متوسط</string> + <string name="privacy_media_blur_radius_soft">ناعم</string> + <string name="info_view_call_button">مكالمة</string> + <string name="info_view_connect_button">اتصل</string> + <string name="info_view_message_button">مراسلة</string> + <string name="info_view_open_button">افتح</string> + <string name="info_view_search_button">بحث</string> + <string name="toolbar_settings">الإعدادات</string> + <string name="info_view_video_button">فيديو</string> + <string name="confirm_delete_contact_question">تأكيد حذف جهة الاتصال؟</string> + <string name="contact_deleted">حُذفت جهة الاتصال!</string> + <string name="delete_contact_cannot_undo_warning">سيتم حذف جهة الاتصال - لا يمكن التراجع عن هذا!</string> + <string name="conversation_deleted">حُذفت المحادثة!</string> + <string name="delete_without_notification">احذف دون إشعار</string> + <string name="keep_conversation">أبقِ المحادثة</string> + <string name="only_delete_conversation">احذف المحادثة فقط</string> + <string name="you_can_still_send_messages_to_contact">بإمكانك إرسال رسائل إلى %1$s من جهات الاتصال المؤرشفة.</string> + <string name="paste_link">ألصق الرابط</string> + <string name="contact_list_header_title">جهات اتصالك</string> + <string name="one_hand_ui">شريط أدوات التطبيق القابلة للوصول</string> + <string name="cant_call_contact_deleted_alert_text">حُذفت جهة الاتصال.</string> + <string name="allow_calls_question">السماح بالمكالمات؟</string> + <string name="cant_call_member_send_message_alert_text">أرسل رسالة لتفعيل المكالمات.</string> + <string name="calls_prohibited_alert_title">المكالمات ممنوعة!</string> + <string name="cant_call_member_alert_title">لا يمكن مكالمة أحد أعضاء المجموعة</string> + <string name="cant_send_message_to_member_alert_title">لا يمكن إرسال رسالة إلى عضو المجموعة</string> + <string name="cant_call_contact_connecting_wait_alert_text">جارِ الاتصال بجهة الاتصال، يُرجى الانتظار أو التحقق لاحقًا!</string> + <string name="deleted_chats">جهات الاتصال المؤرشفة</string> + <string name="action_button_add_members">ادعُ</string> + <string name="no_filtered_contacts">لا توجد جهات اتصال مُصفاة</string> + <string name="cant_call_contact_alert_title">لا يمكن مكالمة جهة الاتصال</string> + <string name="you_can_still_view_conversation_with_contact">لا يزال بإمكانك عرض المحادثة مع %1$s في قائمة الدردشات.</string> + <string name="you_need_to_allow_calls">يجب عليك السماح لجهات اتصالك بالاتصال حتى تتمكن من الاتصال بها.</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">يُرجى الطلب من جهة اتصالك تفعيل المكالمات.</string> + <string name="delete_members_messages__question">حذف %d رسائل الأعضاء؟</string> + <string name="delete_messages_mark_deleted_warning">سيتم وضع علامة على الرسائل للحذف. سيتمكن المُستلم/(المُستلمون) من الكشف عن هذه الرسائل.</string> + <string name="select_verb">حدد</string> + <string name="moderate_messages_will_be_deleted_warning">سيتم حذف الرسائل لجميع الأعضاء.</string> + <string name="moderate_messages_will_be_marked_warning">سيتم وضع علامة على الرسائل على أنها تحت الإشراف لجميع الأعضاء.</string> + <string name="compose_message_placeholder">الرسالة</string> + <string name="selected_chat_items_nothing_selected">لا شيء محدد</string> + <string name="selected_chat_items_selected_n">محدّد %d</string> + <string name="media_and_file_servers">خوادم الوسائط والملفات</string> + <string name="message_servers">خوادم الرسائل</string> + <string name="chat_database_exported_continue">متابعة</string> + <string name="network_socks_proxy">وكيل SOCKS</string> + <string name="chat_database_exported_migrate">يمكنك ترحيل قاعدة البيانات المُصدرة.</string> + <string name="chat_database_exported_save">يمكنك حفظ الأرشيف المُصدر.</string> + <string name="v6_0_connection_servers_status_descr">حالة الاتصال والخوادم.</string> + <string name="v6_0_connect_faster_descr">تواصل مع أصدقائك بشكل أسرع</string> + <string name="v6_0_reachable_chat_toolbar">شريط أدوات الدردشة يمكن الوصول إليه</string> + <string name="v6_0_your_contacts_descr">أرشفة جهات الاتصال للدردشة لاحقًا.</string> + <string name="v6_0_reachable_chat_toolbar_descr">استخدم التطبيق بيد واحدة.</string> + <string name="chat_database_exported_title">صُدرت قاعدة بيانات الدردشة</string> + <string name="v6_0_connection_servers_status">التحكم في شبكتك</string> + <string name="v6_0_delete_many_messages_descr">حذف ما يصل إلى 20 رسالة في آن واحد.</string> + <string name="chat_database_exported_not_all_files">لم يتم تصدير بعض الملفات</string> + <string name="v6_0_private_routing_descr">يحمي عنوان IP الخاص بك واتصالاتك.</string> + <string name="network_option_tcp_connection">اتصال TCP</string> + <string name="network_options_save_and_reconnect">حفظ وإعادة الاتصال</string> + <string name="create_address_button">أنشئ</string> + <string name="v6_0_new_chat_experience">تجربة دردشة جديدة 🎉</string> + <string name="v6_0_privacy_blur">تمويه من أجل خصوصية أفضل.</string> + <string name="v6_0_increase_font_size">كبّر حجم الخط</string> + <string name="new_message">رسالة جديدة</string> + <string name="invite_friends_short">ادعُ</string> + <string name="v6_0_new_media_options">خيارات الوسائط الجديدة</string> + <string name="v6_0_chat_list_media">شغّل من قائمة الدردشة.</string> + <string name="one_hand_ui_card_title">تبديل قائمة الدردشة:</string> + <string name="one_hand_ui_change_instruction">يمكنك تغييره في إعدادات المظهر.</string> + <string name="v6_0_upgrade_app_descr">نزّل الإصدارات الجديدة من GitHub.</string> + <string name="v6_0_upgrade_app">ترقية التطبيق تلقائيًا</string> + <string name="reset_all_hints">صفّر كافة التلميحات</string> + <string name="error_parsing_uri_desc">يُرجى التأكد من أن رابط SimpleX صحيح.</string> + <string name="error_parsing_uri_title">الرابط غير صالح</string> + <string name="n_file_errors">%1$d خطأ في الملف:\n%2$s</string> + <string name="forward_files_failed_to_receive_desc">فشل تنزيل %1$d ملف/ات.</string> + <string name="forward_files_not_accepted_desc">لم يتم تنزيل %1$d ملف/ات.</string> + <string name="forward_files_not_accepted_receive_files">نزّل</string> + <string name="new_chat_share_profile">شارك ملف التعريف</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">استخدم بيانات اعتماد الوكيل المختلفة لكل اتصال.</string> + <string name="network_proxy_username">اسم المستخدم</string> + <string name="network_proxy_auth_mode_username_password">قد يتم إرسال بيانات اعتمادك غير مُعمَّاة.</string> + <string name="network_proxy_incorrect_config_title">خطأ في حفظ الوكيل</string> + <string name="migrate_from_device_remove_archive_question">إزالة الأرشيف؟</string> + <string name="system_mode_toast">وضع النظام</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">سيتم إزالة أرشيف قاعدة البيانات المرفوعة نهائيًا من الخوادم.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">استخدم بيانات اعتماد الوكيل المختلفة لكل ملف تعريف.</string> + <string name="network_proxy_random_credentials">استخدم بيانات اعتماد عشوائية</string> + <string name="settings_section_title_chat_database">قاعدة بيانات الدردشة</string> + <string name="forward_files_missing_desc">حُذف %1$d ملف/ات.</string> + <string name="forward_files_in_progress_desc">لا يزال يتم تنزيل %1$d ملف/ات.</string> + <string name="network_proxy_auth_mode_no_auth">لا تستخدم بيانات الاعتماد مع الوكيل.</string> + <string name="error_forwarding_messages">خطأ في تحويل الرسائل</string> + <string name="switching_profile_error_title">خطأ في تبديل ملف التعريف</string> + <string name="select_chat_profile">حدد ملف تعريف الدردشة</string> + <string name="switching_profile_error_message">لقد تم نقل اتصالك إلى %s ولكن حدث خطأ غير متوقع أثناء إعادة توجيهك إلى ملف التعريف.</string> + <string name="forward_alert_title_messages_to_forward">تحويل %1$s رسالة؟</string> + <string name="forward_files_messages_deleted_after_selection_title">لم يحوّل %1$s من الرسائل</string> + <string name="compose_forward_messages_n">جارِ تحويل %1$s رسالة</string> + <string name="forward_multiple">حوّل الرسائل…</string> + <string name="forward_alert_forward_messages_without_files">تحويل الرسائل بدون ملفات؟</string> + <string name="compose_save_messages_n">جارِ حفظ %1$s رسالة</string> + <string name="network_proxy_incorrect_config_desc">تأكد من صحة تضبيط الوكيل.</string> + <string name="n_other_file_errors">%1$d خطأ في ملف آخر.</string> + <string name="forward_files_messages_deleted_after_selection_desc">حُذفت الرسائل بعد تحديدها.</string> + <string name="forward_alert_title_nothing_to_forward">لا يوجد شيء لتحويله!</string> + <string name="network_proxy_password">كلمة المرور</string> + <string name="network_proxy_auth">استيثاق الوكيل</string> + <string name="delete_messages_cannot_be_undone_warning">سيتم حذف الرسائل - لا يمكن التراجع عن هذا!</string> + <string name="icon_descr_sound_muted">الصوت مكتوم</string> + <string name="error_initializing_web_view_wrong_arch">حدث خطأ أثناء تهيئة WebView. تأكد من تثبيت WebView وأن بنيته المدعومة هي arm64.\nالخطأ: %s</string> + <string name="settings_section_title_message_shape">شكل الرسالة</string> + <string name="settings_message_shape_tail">ذيل</string> + <string name="settings_message_shape_corner">ركن</string> + <string name="network_session_mode_session">جلسة التطبيق</string> + <string name="network_session_mode_server">الخادم</string> + <string name="network_session_mode_session_description">سيتم استخدام بيانات اعتماد SOCKS الجديدة في كل مرة تبدأ فيها تشغيل التطبيق.</string> + <string name="network_session_mode_server_description">سيتم استخدام بيانات اعتماد SOCKS الجديدة لكل خادم.</string> + <string name="call_desktop_permission_denied_chrome">انقر فوق زر المعلومات الموجود بالقرب من حقل العنوان للسماح باستخدام الميكروفون.</string> + <string name="call_desktop_permission_denied_safari">افتح إعدادات Safari / مواقع الويب / الميكروفون، ثم اختر السماح لـ localhost.</string> + <string name="call_desktop_permission_denied_title">لإجراء مكالمات، اسمح باستخدام الميكروفون. أنهِ المكالمة وحاول الاتصال مرة أخرى.</string> + <string name="v6_1_better_user_experience">تجربة مستخدم أفضل</string> + <string name="v6_1_customizable_message_descr">شكل الرسالة قابل للتخصيص.</string> + <string name="v6_1_better_calls_descr">تبديل الصوت والفيديو أثناء المكالمة.</string> + <string name="v6_1_delete_many_messages_descr">حذف أو إشراف ما يصل إلى 200 رسالة.</string> + <string name="v6_1_forward_many_messages_descr">حوّل ما يصل إلى 20 رسالة في آن واحد.</string> + <string name="v6_1_better_calls">مكالمات أفضل</string> + <string name="v6_1_message_dates_descr">تواريخ أفضل للرسائل.</string> + <string name="v6_1_better_security">أمان أفضل ✅</string> + <string name="v6_1_better_security_descr">بروتوكولات SimpleX تمت مراجعتها بواسطة Trail of Bits.</string> + <string name="v6_1_switch_chat_profile_descr">تبديل ملف تعريف الدردشة لدعوات لمرة واحدة.</string> + <string name="errors_in_servers_configuration">أخطاء في تضبيط الخوادم.</string> + <string name="for_chat_profile">لملف تعريف الدردشة %s:</string> + <string name="no_media_servers_configured">لا يوجد وسائط أو خوادم ملفات.</string> + <string name="no_media_servers_configured_for_sending">لا يوجد خوادم لإرسال الملفات.</string> + <string name="connection_error_quota_desc">لقد وصل الاتصال إلى الحد الأقصى من الرسائل غير المُسلمة، قد يكون جهة اتصالك غير متصلة بالإنترنت.</string> + <string name="connection_error_quota">الرسائل غير المُسلَّمة</string> + <string name="share_1_time_link_with_a_friend">شارك رابطًا لمرة واحدة مع صديق</string> + <string name="connection_security">أمان الاتصال</string> + <string name="to_protect_against_your_link_replaced_compare_codes">لحماية الرابط الخاص بك من الاستبدال، يمكنك مقارنة رموز أمان جهات الاتصال.</string> + <string name="smp_servers_new_server">خادم جديد</string> + <string name="for_social_media">لوسائل التواصل الاجتماعي</string> + <string name="or_to_share_privately">أو للمشاركة بشكل خاص</string> + <string name="address_settings">إعدادات العنوان</string> + <string name="create_1_time_link">أنشئ رابط لمرة واحدة</string> + <string name="simplex_address_or_1_time_link">عنوان SimpleX أو رابط لمرة واحدة؟</string> + <string name="onboarding_network_operators">مُشغلي الشبكة</string> + <string name="onboarding_network_operators_configure_via_settings">يمكنك تضبيط الخوادم عبر الإعدادات.</string> + <string name="onboarding_select_network_operators_to_use">حدد مشغلي الشبكة الذين تريد استخدامهم.</string> + <string name="onboarding_network_operators_conditions_you_can_configure">يمكنك تضبيط المُشغلين في إعدادات الشبكة والخوادم.</string> + <string name="onboarding_network_operators_update">حدّث</string> + <string name="onboarding_network_operators_continue">تابع</string> + <string name="operator_conditions_accepted">الشروط المتفق عليها</string> + <string name="operator_review_conditions">راجع الشروط</string> + <string name="network_preset_servers_title">الخوادم المُعدة مسبقًا</string> + <string name="operator_conditions_accepted_for_enabled_operators_on">سيتم قبول الشروط تلقائيًا للمُشغلين المفعّلين في: %s.</string> + <string name="your_servers">خوادمك</string> + <string name="operators_conditions_accepted_for"><![CDATA[قُبلت الشروط للمُشغل/ين: <b>%s</b>.]]></string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[سيتم قبول الشروط للمُشغل/ين: <b>%s</b>.]]></string> + <string name="operator_info_title">مُشغل الشبكة</string> + <string name="operator">المُشغل</string> + <string name="operator_servers_title">%s خوادم</string> + <string name="operator_website">الموقع الإلكتروني</string> + <string name="operator_conditions_will_be_accepted_on">سيتم قبول الشروط في: %s.</string> + <string name="operator_conditions_accepted_on">قُبل الشروط في: %s.</string> + <string name="use_servers_of_operator_x">استخدم %s</string> + <string name="operator_use_operator_toggle_description">استخدم الخوادم</string> + <string name="operator_conditions_accepted_for_some"><![CDATA[قُبلت الشروط بالفعل للمُشغل/ين التاليين: <b>%s</b>.]]></string> + <string name="operator_conditions_of_use">شروط الاستخدام</string> + <string name="operator_use_for_messages_private_routing">للتوجيه الخاص</string> + <string name="operator_use_for_messages_receiving">لتلقي</string> + <string name="operator_use_for_files">استخدم للملفات</string> + <string name="view_conditions">اعرض الشروط</string> + <string name="operators_conditions_will_also_apply"><![CDATA[ستطبق هذه الشروط أيضًا على: <b>%s</b>.]]></string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[لاستخدام خوادم <b>%s</b>، يجب قبول شروط الاستخدام.]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[ستطبق نفس الشروط على المُشغل/ين: <b>%s</b>.]]></string> + <string name="operator_added_xftp_servers">أُضيفت خوادم الوسائط والملفات</string> + <string name="operator_open_conditions">الشروط المفتوحة</string> + <string name="xftp_servers_per_user">الخوادم الخاصة بالملفات الجديدة لملف دردشتك الحالي</string> + <string name="operator_use_for_sending">لإرسال</string> + <string name="error_adding_server">خطأ في إضافة الخادم</string> + <string name="error_updating_server_title">خطأ في تحديث الخادم</string> + <string name="operator_open_changes">التغييرات المفتوحة</string> + <string name="operator_server_alert_title">خادم المُشغل</string> + <string name="server_added_to_operator__name">أُضيف الخادم إلى المُشغل %s.</string> + <string name="error_server_operator_changed">تغيّر مُشغل الخادم.</string> + <string name="appearance_app_toolbars">أشرطة أدوات التطبيق</string> + <string name="appearance_bars_blur_radius">تمويه</string> + <string name="appearance_in_app_bars_alpha">الشفافية</string> + <string name="v6_2_network_decentralization_enable_flux">فعّل flux في إعدادات الشبكة والخوادم لتحسين خصوصية البيانات الوصفية.</string> + <string name="v6_2_network_decentralization">اللامركزية الشبكية</string> + <string name="v6_2_network_decentralization_descr">المُشغل المُعد مسبقًا الثاني في التطبيق!</string> + <string name="v6_2_network_decentralization_enable_flux_reason">لتحسين خصوصية البيانات الوصفية.</string> + <string name="v6_2_improved_chat_navigation">تحسين التنقل في الدردشة</string> + <string name="view_updated_conditions">اعرض الشروط المُحدثة</string> + <string name="accept_conditions">اقبل الشروط</string> + <string name="operator_added_message_servers">أُضيفت خوادم الرسائل</string> + <string name="address_or_1_time_link">عنوان أو رابط لمرة واحدة؟</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[يمكن استخدام الرابط لمرة واحدة <i>مع جهة اتصال واحدة فقط</i> - المشاركة شخصيًا أو عبر أي مُراسل.]]></string> + <string name="onboarding_network_operators_conditions_will_be_accepted">سيتم قبول الشروط للمُشغلين المفعّلين بعد 30 يومًا.</string> + <string name="onboarding_choose_server_operators">مُشغلي الخادم</string> + <string name="operator_conditions_failed_to_load">لا يمكن تحميل نص الشروط الحالية، يمكنك مراجعة الشروط عبر هذا الرابط:</string> + <string name="error_accepting_operator_conditions">خطأ في قبول الشروط</string> + <string name="failed_to_save_servers">خطأ في حفظ الخوادم</string> + <string name="onboarding_network_operators_app_will_use_for_routing">على سبيل المثال، إذا تلقى أحد جهات اتصالك رسائل عبر خادم SimpleX Chat، فسيقوم تطبيقك بتسليمها عبر خادم Flux.</string> + <string name="no_message_servers_configured_for_private_routing">لا يوجد خوادم لتوجيه الرسائل الخاصة.</string> + <string name="no_message_servers_configured">لا يوجد خوادم رسائل.</string> + <string name="no_media_servers_configured_for_private_routing">لا يوجد خوادم لاستقبال الملفات.</string> + <string name="message_deleted_or_not_received_error_title">لا توجد رسالة</string> + <string name="no_message_servers_configured_for_receiving">لا يوجد خوادم لاستقبال الرسائل.</string> + <string name="v6_2_improved_chat_navigation_descr">- فتح الدردشة عند أول رسالة غير مقروءة.\n- الانتقال إلى الرسائل المقتبسة.</string> + <string name="you_can_set_connection_name_to_remember">يمكنك تعيين اسم الاتصال، لتذكر الأشخاص الذين تمت مشاركة الرابط معهم.</string> + <string name="onboarding_network_operators_review_later">راجع لاحقًا</string> + <string name="error_server_protocol_changed">تغيّر بروتوكول الخادم.</string> + <string name="share_address_publicly">شارك العنوان علناً</string> + <string name="share_simplex_address_on_social_media">شارك عنوان SimpleX على وسائل التواصل الاجتماعي.</string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">عنوان SimpleX والروابط لمرة واحدة آمنة للمشاركة عبر أي برنامج مُراسلة.</string> + <string name="address_creation_instruction">انقر فوق أنشئ عنوان SimpleX في القائمة لإنشائه لاحقًا.</string> + <string name="message_deleted_or_not_received_error_desc">حُذفت هذه الرسالة أو لم يتم استلامها بعد.</string> + <string name="operator_use_for_messages">استخدم للرسائل</string> + <string name="onboarding_network_operators_app_will_use_different_operators">يحمي التطبيق خصوصيتك من خلال استخدام مُشغلين مختلفين في كل محادثة.</string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[سيتم قبول الشروط للمُشغل/ين: <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_applied"><![CDATA[ستطبق هذه الشروط أيضًا على: <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[ستطبق نفس الشروط على المُشغل <b>%s</b>.]]></string> + <string name="business_address">عنوان العمل التجاري</string> + <string name="onboarding_notifications_mode_service_desc_short">يتم تشغيل التطبيق دائمًا في الخلفية</string> + <string name="v6_2_business_chats">دردشات العمل التجاري</string> + <string name="button_add_team_members">أضف أعضاء الفريق</string> + <string name="button_add_friends">أضف أصدقاء</string> + <string name="add_your_team_members_to_conversations">أضف أعضاء فريقك إلى المحادثات.</string> + <string name="direct_messages_are_prohibited_in_chat">يُحظر إرسال الرسائل المباشرة بين الأعضاء في هذه الدردشة.</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>أجهزة Xiaomi</b>: يُرجى تفعيل التشغيل التلقائي (Autostart) في إعدادات النظام لكي تعمل الإشعارات.]]></string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[يتم إرسال جميع الرسائل والملفات <b>مُعمَّاة بين الطرفين</b>، مع أمان ما بعد الكم في الرسائل المباشرة.]]></string> + <string name="onboarding_notifications_mode_periodic_desc_short">تحقق من الرسائل كل 10 دقائق</string> + <string name="direct_messages_are_prohibited">يُمنع إرسال الرسائل المباشرة بين الأعضاء.</string> + <string name="info_row_chat">الدردشة</string> + <string name="how_it_helps_privacy">كيف يساعد على الخصوصية</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">سيتم حذف الدردشة لجميع الأعضاء - لا يمكن التراجع عن هذا!</string> + <string name="delete_chat_for_self_cannot_undo_warning">سيتم حذف الدردشة لديك - لا يمكن التراجع عن هذا!</string> + <string name="button_delete_chat">احذف الدردشة</string> + <string name="connect_plan_chat_already_exists">الدردشة موجودة بالفعل!</string> + <string name="delete_chat_question">حذف الدردشة؟</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[لقد تم توصيلك بالفعل بـ <b>%1$s</b>.]]></string> + <string name="chat_archive">أو استورد ملف الأرشيف</string> + <string name="onboarding_notifications_mode_off_desc_short">لا توجد خدمة خلفية</string> + <string name="onboarding_notifications_mode_battery">الإشعارات والبطارية</string> + <string name="only_chat_owners_can_change_prefs">فقط مالكي الدردشة يمكنهم تغيير التفضيلات.</string> + <string name="v6_2_business_chats_descr">الخصوصية لعملائك.</string> + <string name="remote_hosts_section">الجوالات عن بُعد</string> + <string name="invite_to_chat_button">ادعُ للدردشة</string> + <string name="leave_chat_question">مغادرة المجموعة؟</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">سيتم إزالة العضو من الدردشة - لا يمكن التراجع عن هذا!</string> + <string name="button_leave_chat">غادِر الدردشة</string> + <string name="maximum_message_size_title">الرسالة كبيرة جدًا!</string> + <string name="maximum_message_size_reached_text">يُرجى تقليل حجم الرسالة وإرسالها مرة أخرى.</string> + <string name="chat_bottom_bar">شريط أداة الدردشة القابلة للوصول</string> + <string name="display_name_accepted_invitation">الدعوة قُبلت</string> + <string name="display_name_requested_to_connect">طلبت الاتصال</string> + <string name="maximum_message_size_reached_non_text">يُرجى تقليل حجم الرسالة أو إزالة الوسائط ثم إرسالها مرة أخرى.</string> + <string name="maximum_message_size_reached_forwarding">يمكنك نسخ الرسالة وتقليل حجمها لإرسالها.</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">عندما يتم تفعيل أكثر من مُشغل واحد، لن يكون لدى أي منهم بيانات تعريفية لمعرفة مَن يتواصل مع مَن.</string> + <string name="member_role_will_be_changed_with_notification_chat">سيتم تغيير الدور إلى %s. وسيتم إشعار الجميع في الدردشة.</string> + <string name="chat_main_profile_sent">سيتم إرسال ملف تعريفك للدردشة إلى أعضاء الدردشة</string> + <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">ستتوقف عن تلقي الرسائل من هذه الدردشة. سيتم حفظ سجل الدردشة.</string> + <string name="onboarding_network_about_operators">عن المُشغلين</string> + <string name="onboarding_network_operators_simplex_flux_agreement">توصلت SimpleX Chat وFlux إلى اتفاق لتضمين الخوادم التي تديرها Flux في التطبيق.</string> + <string name="encryption_renegotiation_in_progress">جارِ إعادة التفاوض على التعمية.</string> + <string name="sync_connection_question">إصلاح الاتصال؟</string> + <string name="sync_connection_desc">يتطلب الاتصال إعادة التفاوض على التعمية.</string> + <string name="sync_connection_confirm">إصلاح</string> + <string name="debug_logs">فعّل السجلات</string> + <string name="error_saving_database">خطأ في حفظ قاعدة البيانات</string> + <string name="strikethrough_text">شطب</string> + <string name="connection_not_ready">الاتصال غير جاهز.</string> + <string name="list_menu">القائمة</string> + <string name="no_chats">لا دردشات</string> + <string name="no_chats_in_list">لا توجد محادثات في القائمة %s.</string> + <string name="no_unread_chats">لا توجد محادثات غير مقروءة</string> + <string name="no_chats_found">لم يتم العثور على أي محادثات</string> + <string name="chat_list_favorites">المفضلات</string> + <string name="chat_list_add_list">أضف القائمة</string> + <string name="chat_list_all">الكل</string> + <string name="chat_list_groups">المجموعات</string> + <string name="open_with_app">افتح باستخدام %s</string> + <string name="add_to_list">أضف إلى القائمة</string> + <string name="delete_chat_list_menu_action">احذف</string> + <string name="delete_chat_list_question">حذف القائمة؟</string> + <string name="edit_chat_list_menu_action">حرّر</string> + <string name="list_name_field_placeholder">اسم القائمة...</string> + <string name="duplicated_list_error">يجب أن يكون اسم القائمة والرموز التعبيرية مختلفين لجميع القوائم.</string> + <string name="save_list">احفظ القائمة</string> + <string name="chat_list_contacts">جهات الاتصال</string> + <string name="error_creating_chat_tags">خطأ في إنشاء قائمة الدردشة</string> + <string name="chat_list_businesses">الشركات</string> + <string name="error_loading_chat_tags">خطأ في تحميل قوائم الدردشة</string> + <string name="delete_chat_list_warning">سيتم إزالة جميع المحادثات من القائمة %s، وسيتم حذف القائمة</string> + <string name="create_list">أنشئ قائمة</string> + <string name="error_updating_chat_tags">خطأ في تحديث قائمة الدردشة</string> + <string name="chat_list_notes">الملحوظات</string> + <string name="change_list">تغيير القائمة</string> + <string name="change_order_chat_list_menu_action">تغيير الترتيب</string> + <string name="prefs_error_saving_settings">خطأ في حفظ الإعدادات</string> + <string name="error_creating_report">خطأ في إنشاء بلاغ</string> + <string name="report_item_visibility_submitter">أنت والمشرفون فقط هم من يرون ذلك</string> + <string name="report_item_archived">بلاغ مؤرشف</string> + <string name="report_item_visibility_moderators">لا يراه إلا المُرسِل والمُشرفين</string> + <string name="archive_verb">أرشف</string> + <string name="archive_report">أرشف البلاغ</string> + <string name="delete_report">احذف البلاغ</string> + <string name="report_verb">بلّغ</string> + <string name="report_reason_profile">ملف تعريفي غير لائق</string> + <string name="report_reason_spam">إزعاج (spam)</string> + <string name="report_reason_other">سبب آخر</string> + <string name="chat_list_group_reports">البلاغات</string> + <string name="group_reports_active_one">1 بلاغ</string> + <string name="group_reports_active">%d بلاغات</string> + <string name="group_reports_member_reports">بلاغات الأعضاء</string> + <string name="report_compose_reason_header_illegal">بلّغ عن المحتوى: سيراه مشرفو المجموعة فقط.</string> + <string name="report_compose_reason_header_other">بلّغ عن أُخرى: سيراه مشرفو المجموعة فقط.</string> + <string name="group_member_role_moderator">مشرف</string> + <string name="report_item_archived_by">بلاغ مؤرشف بواسطة %s</string> + <string name="report_compose_reason_header_profile">بلّغ عن ملف تعريف العضو: سيراه مشرفو المجموعة فقط.</string> + <string name="report_reason_community">انتهاك إرشادات المجتمع</string> + <string name="report_reason_illegal">محتوى غير لائق</string> + <string name="report_compose_reason_header_community">بلّغ عن مخالفة: سيراه مشرفو المجموعة فقط.</string> + <string name="report_compose_reason_header_spam">بلّغ عن إزعاج (spam): سيراه مشرفو المجموعة فقط.</string> + <string name="report_archive_alert_title">أرشفة البلاغ؟</string> + <string name="report_reason_alert_title">سبب الإبلاغ؟</string> + <string name="report_archive_alert_desc">سيتم أرشفة البلاغ لك.</string> + <string name="blocking_reason_spam">إزعاج (spam)</string> + <string name="privacy_chat_list_open_links_yes">نعم</string> + <string name="privacy_chat_list_open_links_ask">اسأل</string> + <string name="privacy_chat_list_open_links_no">لا</string> + <string name="privacy_chat_list_open_web_link">افتح الرابط</string> + <string name="privacy_chat_list_open_web_link_question">فتح رابط الويب؟</string> + <string name="connection_error_blocked">حُظر الاتصال</string> + <string name="privacy_chat_list_open_links">افتح الروابط من قائمة الدردشة</string> + <string name="blocking_reason_content">المحتوى ينتهك شروط الاستخدام</string> + <string name="connection_error_blocked_desc">حُظر الاتصال بواسطة مُشغل الخادم:\n%1$s.</string> + <string name="file_error_blocked">حُظر الملف بواسطة مُشغل الخادم:\n%1$s.</string> + <string name="chat_item_ttl_default">الافتراضي %s</string> + <string name="chat_item_ttl_year">سنة واحدة</string> + <string name="disable_automatic_deletion">عطّل حذف الرسائل التلقائي</string> + <string name="disable_automatic_deletion_question">تعطيل حذف الرسائل التلقائي؟</string> + <string name="disable_automatic_deletion_message">لن يتم حذف الرسائل الموجودة في هذه الدردشة أبدًا.</string> + <string name="chat_ttl_options_footer">احذف رسائل الدردشة من جهازك.</string> + <string name="text_field_set_chat_placeholder">عيّن اسم الدردشة…</string> + <string name="change_automatic_chat_deletion_message">لا يمكن التراجع عن هذا الإجراء - سيتم حذف الرسائل المُرسلة والمُستلمة في هذه الدردشة قبل التاريخ المحدّد.</string> + <string name="change_automatic_deletion_question">تغيير حذف الرسائل التلقائي؟</string> + <string name="network_smp_web_port_footer">استخدم منفذ TCP %1$s عندما لا يتم تحديد أي منفذ.</string> + <string name="network_smp_web_port_toggle">استخدم منفذ الويب</string> + <string name="network_smp_web_port_section_title">منفذ TCP للمُراسلة</string> + <string name="mute_all_chat">اكتم الكل</string> + <string name="unread_mentions">ذّكورات غير مقروءة</string> + <string name="max_group_mentions_per_message_reached">يمكنك ذكر ما يصل إلى %1$s من الأعضاء في الرسالة الواحدة!</string> + <string name="enable_sending_member_reports">السماح بالإبلاغ عن الرسائل إلى المشرفين.</string> + <string name="disable_sending_member_reports">منع الإبلاغ عن الرسائل للمشرفين.</string> + <string name="report_archive_alert_title_all">أرشفة كافة البلاغات؟</string> + <string name="archive_reports">أرشف البلاغات</string> + <string name="report_archive_for_all_moderators">لكل المشرفين</string> + <string name="report_archive_for_me">لي</string> + <string name="notification_group_report">بلاغ: %s</string> + <string name="group_members_can_send_reports">يمكن للأعضاء الإبلاغ عن الرسائل إلى المشرفين.</string> + <string name="report_archive_alert_desc_all">سيتم أرشفة كافة البلاغات لك.</string> + <string name="report_archive_alert_title_nth">أرشفة %d بلاغ؟</string> + <string name="member_reports_are_prohibited">يُمنع الإبلاغ عن الرسائل في هذه المجموعة.</string> + <string name="v6_3_organize_chat_lists_descr">لا تفوت رسائل مهمة.</string> + <string name="v6_3_reports_descr">مساعدة المُدراء على إشراف مجموعاتهم.</string> + <string name="v6_3_mentions">أذكر الأعضاء 👋</string> + <string name="v6_3_organize_chat_lists">نظّم الدردشات في القوائم</string> + <string name="v6_3_reports">أرسل بلاغات خاصة</string> + <string name="v6_3_set_message_expiration_in_chats">اضبط انتهاء صلاحية الرسالة في الدردشات.</string> + <string name="v6_3_faster_deletion_of_groups">حذف أسرع من المجموعات.</string> + <string name="v6_3_private_media_file_names">أسماء ملفات الوسائط خاصة.</string> + <string name="v6_3_mentions_descr">استلم إشعارًا عند ذكرك.</string> + <string name="v6_3_better_groups_performance">أداء مجموعات أفضل</string> + <string name="v6_3_better_privacy_and_security">خصوصية وأمان أفضل</string> + <string name="v6_3_faster_sending_messages">إرسال أسرع للرسائل.</string> + <string name="group_member_status_rejected">رُفض</string> + <string name="group_preview_rejected">رُفض</string> + <string name="error_reading_passphrase">خطأ في قراءة عبارة مرور قاعدة البيانات</string> + <string name="group_member_status_pending_approval_short">ينتظر</string> + <string name="operator_updated_conditions">حُدثت الشروط</string> + <string name="button_remove_members_question">إزالة الأعضاء؟</string> + <string name="block_members_desc">سيتم إخفاء جميع الرسائل الجديدة من هؤلاء الأعضاء!</string> + <string name="members_will_be_removed_from_chat_cannot_be_undone">سيتم إزالة الأعضاء من الدردشة - لا يمكن التراجع عن هذا!</string> + <string name="unblock_members_for_all_question">إلغاء حظر الأعضاء للجميع؟</string> + <string name="block_members_for_all_question">حظر الأعضاء للجميع؟</string> + <string name="unblock_members_desc">سيتم عرض رسائل من هؤلاء الأعضاء!</string> + <string name="restore_passphrase_can_not_be_read_enter_manually_desc">لا يمكن قراءة عبارة المرور في Keystore، يُرجى إدخالها يدويًا. قد يكون هذا قد حدث بعد تحديث النظام غير متوافق مع التطبيق. إذا لم يكن الأمر كذلك، فيُرجى التواصل مع المطورين.</string> + <string name="members_will_be_removed_from_group_cannot_be_undone">سيتم إزالة الأعضاء من المجموعة - لا يمكن التراجع عن هذا!</string> + <string name="feature_roles_moderators">المشرفين</string> + <string name="restore_passphrase_can_not_be_read_desc">لا يمكن قراءة عبارة المرور في Keystore. قد يكون هذا قد حدث بعد تحديث النظام غير متوافق مع التطبيق. إذا لم يكن الأمر كذلك، فيُرجى التواصل مع المطورين.</string> + <string name="group_member_status_pending_approval">موافقة الانتظار</string> + <string name="onboarding_conditions_configure_server_operators">ضبّط مُشغلي الخادم</string> + <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">سياسة الخصوصية وشروط الاستخدام.</string> + <string name="onboarding_conditions_private_chats_not_accessible">لا يمكن الوصول إلى الدردشات الخاصة والمجموعات وجهات اتصالك لمشغلي الخادم.</string> + <string name="onboarding_conditions_by_using_you_agree">باستخدام SimpleX Chat، توافق على:\n- إرسال المحتوى القانوني فقط في المجموعات العامة.\n- احترام المستخدمين الآخرين – لا سبام.</string> + <string name="onboarding_conditions_accept">اقبل</string> + <string name="privacy_short_links">استخدم روابط قصيرة (تجريبي)</string> + <string name="link_requires_newer_app_version_please_upgrade">يتطلب هذا الرابط إصدار تطبيق أحدث. يُرجى ترقية التطبيق أو اطلب من جهة اتصالك إرسال رابط متوافق.</string> + <string name="full_link_button_text">رابط كامل</string> + <string name="short_link_button_text">رابط قصير</string> + <string name="simplex_link_channel">رابط قناة SimpleX</string> + <string name="unsupported_connection_link">رابط اتصال غير مدعوم</string> + <string name="network_smp_web_port_preset_footer">استخدم منفذ TCP 443 للخوادم المُعدة مسبقًا فقط.</string> + <string name="network_smp_web_port_off">إيقاف التشغيل</string> + <string name="network_smp_web_port_preset">الخوادم المُعدة مسبقًا</string> + <string name="network_smp_web_port_all">جميع الخوادم</string> +</resources> 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 4ad40d7a62..6726009a5f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -13,9 +13,12 @@ <string name="you_will_join_group">You will connect to all group members.</string> <string name="connect_via_link_verb">Connect</string> <string name="connect_via_link_incognito">Connect incognito</string> + <string name="error_parsing_uri_title">Invalid link</string> + <string name="error_parsing_uri_desc">Please check that SimpleX link is correct.</string> <!-- MainActivity.kt --> <string name="opening_database">Opening database…</string> + <string name="database_migration_in_progress">Database migration is in progress.\nIt may take a few minutes.</string> <string name="non_content_uri_alert_title">Invalid file path</string> <string name="non_content_uri_alert_text">You shared an invalid file path. Report the issue to the app developers.</string> <string name="app_was_crashed">View crashed</string> @@ -33,9 +36,15 @@ <string name="marked_deleted_description">marked deleted</string> <string name="marked_deleted_items_description">%d messages marked deleted</string> <string name="moderated_item_description">moderated by %s</string> - <string name="moderated_items_description">%d messages moderated by %s</string> + <string name="moderated_items_description">%1$d messages moderated by %2$s</string> + <string name="report_item_visibility_submitter">Only you and moderators see it</string> + <string name="report_item_visibility_moderators">Only sender and moderators see it</string> + <string name="report_item_archived">archived report</string> + <string name="report_item_archived_by">archived report by %s</string> <string name="blocked_item_description">blocked</string> + <string name="blocked_by_admin_item_description">blocked by admin</string> <string name="blocked_items_description">%d messages blocked</string> + <string name="blocked_by_admin_items_description">%d messages blocked by admin</string> <string name="sending_files_not_yet_supported">sending files is not supported yet</string> <string name="receiving_files_not_yet_supported">receiving files is not supported yet</string> <string name="sender_you_pronoun">you</string> @@ -43,6 +52,9 @@ <string name="invalid_message_format">invalid message format</string> <string name="live">LIVE</string> <string name="moderated_description">moderated</string> + <string name="forwarded_description">forwarded</string> + <string name="saved_description">saved</string> + <string name="saved_from_description">saved from %s</string> <string name="invalid_chat">invalid chat</string> <string name="invalid_data">invalid data</string> <string name="error_showing_message">error showing message</string> @@ -51,10 +63,20 @@ <string name="decryption_error">Decryption error</string> <string name="encryption_renegotiation_error">Encryption re-negotiation error</string> + <string name="e2ee_info_no_pq"><![CDATA[Messages, files and calls are protected by <b>end-to-end encryption</b> with perfect forward secrecy, repudiation and break-in recovery.]]></string> + <string name="e2ee_info_pq"><![CDATA[Messages, files and calls are protected by <b>quantum resistant e2e encryption</b> with perfect forward secrecy, repudiation and break-in recovery.]]></string> + <string name="e2ee_info_no_pq_short">This chat is protected by end-to-end encryption.</string> + <string name="e2ee_info_pq_short">This chat is protected by quantum resistant end-to-end encryption.</string> + + <!-- NoteFolder - ChatModel.kt --> + <string name="note_folder_local_display_name">Private notes</string> + <!-- PendingContactConnection - ChatModel.kt --> <string name="connection_local_display_name">connection %1$d</string> <string name="display_name_connection_established">connection established</string> <string name="display_name_invited_to_connect">invited to connect</string> + <string name="display_name_requested_to_connect">requested to connect</string> + <string name="display_name_accepted_invitation">accepted invitation</string> <string name="display_name_connecting">connecting…</string> <string name="description_you_shared_one_time_link">you shared one-time link</string> <string name="description_you_shared_one_time_link_incognito">you shared one-time link incognito</string> @@ -69,12 +91,21 @@ <string name="simplex_link_contact">SimpleX contact address</string> <string name="simplex_link_invitation">SimpleX one-time invitation</string> <string name="simplex_link_group">SimpleX group link</string> + <string name="simplex_link_channel">SimpleX channel link</string> <string name="simplex_link_connection">via %1$s</string> <string name="simplex_link_mode">SimpleX links</string> <string name="simplex_link_mode_description">Description</string> <string name="simplex_link_mode_full">Full link</string> <string name="simplex_link_mode_browser">Via browser</string> <string name="simplex_link_mode_browser_warning">Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.</string> + <string name="privacy_short_links">Use short links (BETA)</string> + + <!-- Reports - ChatModel.kt --> + <string name="report_reason_spam">Spam</string> + <string name="report_reason_illegal">Inappropriate content</string> + <string name="report_reason_community">Community guidelines violation</string> + <string name="report_reason_profile">Inappropriate profile</string> + <string name="report_reason_other">Another reason</string> <!-- SimpleXAPI.kt --> <string name="error_saving_smp_servers">Error saving SMP servers</string> @@ -90,30 +121,68 @@ <string name="failed_to_create_user_title">Error creating profile!</string> <string name="failed_to_create_user_duplicate_title">Duplicate display name!</string> <string name="failed_to_create_user_duplicate_desc">You already have a chat profile with the same display name. Please choose another name.</string> + <string name="failed_to_create_user_invalid_title">Invalid display name!</string> + <string name="failed_to_create_user_invalid_desc">This display name is invalid. Please choose another name.</string> <string name="failed_to_active_user_title">Error switching profile!</string> + <string name="failed_to_save_servers">Error saving servers</string> + <string name="no_message_servers_configured">No message servers.</string> + <string name="no_message_servers_configured_for_receiving">No servers to receive messages.</string> + <string name="no_message_servers_configured_for_private_routing">No servers for private message routing.</string> + <string name="no_media_servers_configured">No media & file servers.</string> + <string name="no_media_servers_configured_for_sending">No servers to send files.</string> + <string name="no_media_servers_configured_for_private_routing">No servers to receive files.</string> + <string name="for_chat_profile">For chat profile %s:</string> + <string name="errors_in_servers_configuration">Errors in servers configuration.</string> + <string name="error_accepting_operator_conditions">Error accepting conditions</string> + <string name="blocking_reason_spam">Spam</string> + <string name="blocking_reason_content">Content violates conditions of use</string> <!-- API Error Responses - SimpleXAPI.kt --> <string name="connection_timeout">Connection timeout</string> <string name="connection_error">Connection error</string> <string name="network_error_desc">Please check your network connection with %1$s and try again.</string> + <string name="network_error_broker_host_desc">Server address is incompatible with network settings: %1$s.</string> + <string name="network_error_broker_version_desc">Server version is incompatible with your app: %1$s.</string> + <string name="private_routing_error">Private routing error</string> + <string name="smp_proxy_error_connecting">Error connecting to forwarding server %1$s. Please try later.</string> + <string name="smp_proxy_error_broker_host">Forwarding server address is incompatible with network settings: %1$s.</string> + <string name="smp_proxy_error_broker_version">Forwarding server version is incompatible with network settings: %1$s.</string> + <string name="proxy_destination_error_failed_to_connect">Forwarding server %1$s failed to connect to destination server %2$s. Please try later.</string> + <string name="proxy_destination_error_broker_host">Destination server address of %1$s is incompatible with forwarding server %2$s settings.</string> + <string name="proxy_destination_error_broker_version">Destination server version of %1$s is incompatible with forwarding server %2$s.</string> + <string name="please_try_later">Please try later.</string> <string name="error_sending_message">Error sending message</string> + <string name="error_forwarding_messages">Error forwarding messages</string> + <string name="error_creating_message">Error creating message</string> + <string name="error_creating_report">Error creating report</string> <string name="error_loading_details">Error loading details</string> <string name="error_adding_members">Error adding member(s)</string> <string name="error_joining_group">Error joining group</string> <string name="cannot_receive_file">Cannot receive file</string> <string name="sender_cancelled_file_transfer">Sender cancelled file transfer.</string> + <string name="file_not_approved_title">Unknown servers!</string> + <string name="file_not_approved_descr">Without Tor or VPN, your IP address will be visible to these XFTP relays:\n%1$s.</string> + <string name="n_other_file_errors">%1$d other file error(s).</string> <string name="error_receiving_file">Error receiving file</string> + <string name="n_file_errors">%1$d file error(s):\n%2$s</string> <string name="error_creating_address">Error creating address</string> <string name="contact_already_exists">Contact already exists</string> <string name="you_are_already_connected_to_vName_via_this_link">You are already connected to %1$s.</string> <string name="invalid_connection_link">Invalid connection link</string> <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Please check that you used the correct link or ask your contact to send you another one.</string> + <string name="unsupported_connection_link">Unsupported connection link</string> + <string name="link_requires_newer_app_version_please_upgrade">This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</string> <string name="connection_error_auth">Connection error (AUTH)</string> <string name="connection_error_auth_desc">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.</string> + <string name="connection_error_blocked">Connection blocked</string> + <string name="connection_error_blocked_desc">Connection is blocked by server operator:\n%1$s.</string> + <string name="connection_error_quota">Undelivered messages</string> + <string name="connection_error_quota_desc">The connection reached the limit of undelivered messages, your contact may be offline.</string> <string name="error_accepting_contact_request">Error accepting contact request</string> <string name="sender_may_have_deleted_the_connection_request">Sender may have deleted the connection request.</string> <string name="error_deleting_contact">Error deleting contact</string> <string name="error_deleting_group">Error deleting group</string> + <string name="error_deleting_note_folder">Error deleting private notes</string> <string name="error_deleting_contact_request">Error deleting contact request</string> <string name="error_deleting_pending_contact_connection">Error deleting pending contact connection</string> <string name="error_changing_address">Error changing address</string> @@ -137,14 +206,19 @@ <string name="smp_server_test_delete_file">Delete file</string> <string name="error_deleting_user">Error deleting user profile</string> <string name="error_updating_user_privacy">Error updating user privacy</string> + <string name="possible_slow_function_title">Slow function</string> + <string name="possible_slow_function_desc">Execution of function takes too long time: %1$d seconds: %2$s</string> + <string name="error_updating_chat_tags">Error updating chat list</string> + <string name="error_creating_chat_tags">Error creating chat list</string> + <string name="error_loading_chat_tags">Error loading chat lists</string> <!-- background service notice - SimpleXAPI.kt --> <string name="icon_descr_instant_notifications">Instant notifications</string> <string name="service_notifications">Instant notifications!</string> <string name="service_notifications_disabled">Instant notifications are disabled!</string> - <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[To preserve your privacy, instead of push notifications the app has a <b>SimpleX background service</b> – it uses a few percent of the battery per day.]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[To improve privacy, <b>SimpleX runs in background</b> instead of using push notifications.]]></string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>It can be disabled via settings</b> – notifications will still be shown while the app is running.]]></string> - <string name="turn_off_battery_optimization"><![CDATA[To use it, please <b>allow SimpleX to run in background</b> in the next dialog. Otherwise, the notifications will be disabled.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[<b>Allow it</b> in the next dialog to receive notifications instantly.]]></string> <string name="turning_off_service_and_periodic">Battery optimization is active, turning off background service and periodic requests for new messages. You can re-enable them via settings.</string> <string name="periodic_notifications">Periodic notifications</string> <string name="periodic_notifications_disabled">Periodic notifications are disabled!</string> @@ -161,10 +235,14 @@ <string name="enter_passphrase_notification_desc">To receive notifications, please, enter the database passphrase</string> <string name="database_initialization_error_title">Can\'t initialize the database</string> <string name="database_initialization_error_desc">The database is not working correctly. Tap to learn more</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Xiaomi devices</b>: please enable Autostart in the system settings for notifications to work.]]></string> <!-- SimpleX Chat foreground Service --> <string name="simplex_service_notification_title">SimpleX Chat service</string> <string name="simplex_service_notification_text">Receiving messages…</string> + <string name="call_service_notification_audio_call">Audio call</string> + <string name="call_service_notification_video_call">Video call</string> + <string name="call_service_notification_end_call">End call</string> <string name="hide_notification">Hide</string> <!-- Notification channels --> @@ -191,6 +269,7 @@ <string name="notification_preview_new_message">new message</string> <string name="notification_new_contact_request">New contact request</string> <string name="notification_contact_connected">Connected</string> + <string name="error_showing_desktop_notification">Error showing notification, contact developers.</string> <!-- local authentication notice - SimpleXAPI.kt --> <string name="la_notice_title_simplex_lock">SimpleX Lock</string> @@ -224,13 +303,42 @@ <string name="auth_device_authentication_is_disabled_turning_off">Device authentication is disabled. Turning off SimpleX Lock.</string> <string name="auth_stop_chat">Stop chat</string> <string name="auth_open_chat_console">Open chat console</string> - <string name="auth_open_chat_profiles">Open chat profiles</string> + <string name="auth_open_chat_profiles">Change chat profiles</string> + <string name="auth_open_migration_to_another_device">Open migration screen</string> <string name="lock_not_enabled">SimpleX Lock not enabled!</string> <string name="you_can_turn_on_lock">You can turn on SimpleX Lock via Settings.</string> <!-- Chat Alerts - ChatItemView.kt --> <string name="message_delivery_error_title">Message delivery error</string> + <string name="message_delivery_warning_title">Message delivery warning</string> <string name="message_delivery_error_desc">Most likely this contact has deleted the connection with you.</string> + <string name="message_deleted_or_not_received_error_title">No message</string> + <string name="message_deleted_or_not_received_error_desc">This message was deleted or not received yet.</string> + <string name="report_reason_alert_title">Report reason?</string> + <string name="report_archive_alert_title">Archive report?</string> + <string name="report_archive_alert_title_nth">Archive %d reports?</string> + <string name="report_archive_alert_title_all">Archive all reports?</string> + <string name="report_archive_alert_desc">The report will be archived for you.</string> + <string name="report_archive_alert_desc_all">All reports will be archived for you.</string> + <string name="report_archive_for_me">For me</string> + <string name="report_archive_for_all_moderators">For all moderators</string> + + <!-- CIStatus errors --> + <string name="ci_status_other_error">Error: %1$s</string> + <string name="snd_error_auth">Wrong key or unknown connection - most likely this connection is deleted.</string> + <string name="snd_error_quota">Capacity exceeded - recipient did not receive previously sent messages.</string> + <string name="snd_error_expired">Network issues - message expired after many attempts to send it.</string> + <string name="snd_error_relay">Destination server error: %1$s</string> + <string name="snd_error_proxy">Forwarding server: %1$s\nError: %2$s</string> + <string name="snd_error_proxy_relay">Forwarding server: %1$s\nDestination server error: %2$s</string> + <string name="srv_error_host">Server address is incompatible with network settings.</string> + <string name="srv_error_version">Server version is incompatible with network settings.</string> + + <!-- CIFileStatus errors --> + <string name="file_error_auth">Wrong key or unknown file chunk address - most likely file is deleted.</string> + <string name="file_error_blocked">File is blocked by server operator:\n%1$s.</string> + <string name="file_error_no_file">File not found - most likely file was deleted or cancelled.</string> + <string name="file_error_relay">File server error: %1$s</string> <!-- Chat Actions - ChatItemView.kt (and general) --> <string name="reply_verb">Reply</string> @@ -240,11 +348,20 @@ <string name="edit_verb">Edit</string> <string name="info_menu">Info</string> <string name="search_verb">Search</string> + <string name="archive_verb">Archive</string> + <string name="archive_report">Archive report</string> + <string name="archive_reports">Archive reports</string> + <string name="delete_report">Delete report</string> <string name="sent_message">Sent message</string> <string name="received_message">Received message</string> <string name="edit_history">History</string> <string name="no_history">No history</string> <string name="in_reply_to">In reply to</string> + <string name="saved_chat_item_info_tab">Saved</string> + <string name="forwarded_chat_item_info_tab">Forwarded</string> + <string name="saved_from_chat_item_info_title">Saved from</string> + <string name="forwarded_from_chat_item_info_title">Forwarded from</string> + <string name="recipients_can_not_see_who_message_from">Recipient(s) can\'t see who this message is from.</string> <string name="delivery">Delivery</string> <string name="no_info_on_delivery">No delivery information</string> <string name="delete_verb">Delete</string> @@ -252,14 +369,21 @@ <string name="hide_verb">Hide</string> <string name="allow_verb">Allow</string> <string name="moderate_verb">Moderate</string> + <string name="report_verb">Report</string> + <string name="select_verb">Select</string> <string name="expand_verb">Expand</string> <string name="delete_message__question">Delete message?</string> <string name="delete_messages__question">Delete %d messages?</string> <string name="delete_message_cannot_be_undone_warning">Message will be deleted - this cannot be undone!</string> + <string name="delete_messages_cannot_be_undone_warning">Messages will be deleted - this cannot be undone!</string> <string name="delete_message_mark_deleted_warning">Message will be marked for deletion. The recipient(s) will be able to reveal this message.</string> + <string name="delete_messages_mark_deleted_warning">Messages will be marked for deletion. The recipient(s) will be able to reveal these messages.</string> <string name="delete_member_message__question">Delete member message?</string> + <string name="delete_members_messages__question">Delete %d messages of members?</string> <string name="moderate_message_will_be_deleted_warning">The message will be deleted for all members.</string> + <string name="moderate_messages_will_be_deleted_warning">The messages will be deleted for all members.</string> <string name="moderate_message_will_be_marked_warning">The message will be marked as moderated for all members.</string> + <string name="moderate_messages_will_be_marked_warning">The messages will be marked as moderated for all members.</string> <string name="for_me_only">Delete for me</string> <string name="for_everybody">For everyone</string> <string name="stop_file__action">Stop file</string> @@ -272,6 +396,14 @@ <string name="revoke_file__title">Revoke file?</string> <string name="revoke_file__message">File will be deleted from servers.</string> <string name="revoke_file__confirm">Revoke</string> + <string name="forward_chat_item">Forward</string> + <string name="download_file">Download</string> + <string name="list_menu">List</string> + + <string name="message_forwarded_title">Message forwarded</string> + <string name="message_forwarded_desc">No direct connection yet, message is forwarded by admin.</string> + <string name="member_inactive_title">Member inactive</string> + <string name="member_inactive_desc">Message may be delivered later if member becomes active.</string> <!-- CIMetaView.kt --> <string name="icon_descr_edited">edited</string> @@ -285,25 +417,62 @@ <string name="welcome">Welcome!</string> <string name="this_text_is_available_in_settings">This text is available in settings</string> <string name="your_chats">Chats</string> + <string name="toolbar_settings">Settings</string> <string name="contact_connection_pending">connecting…</string> <string name="member_contact_send_direct_message">send direct message</string> <string name="group_preview_you_are_invited">you are invited to group</string> <string name="group_preview_join_as">join as %s</string> + <string name="group_preview_rejected">rejected</string> <string name="group_connection_pending">connecting…</string> <string name="tap_to_start_new_chat">Tap to start a new chat</string> <string name="chat_with_developers">Chat with the developers</string> <string name="you_have_no_chats">You have no chats</string> + <string name="loading_chats">Loading chats…</string> <string name="no_filtered_chats">No filtered chats</string> + <string name="no_chats_in_list">No chats in list %s.</string> + <string name="no_unread_chats">No unread chats</string> + <string name="no_chats">No chats</string> + <string name="no_chats_found">No chats found</string> <string name="contact_tap_to_connect">Tap to Connect</string> <string name="connect_with_contact_name_question">Connect with %1$s?</string> + <string name="search_or_paste_simplex_link">Search or paste SimpleX link</string> + <string name="address_creation_instruction">Tap Create SimpleX address in the menu to create it later.</string> <!-- ChatView.kt --> <string name="no_selected_chat">No selected chat</string> + <string name="selected_chat_items_nothing_selected">Nothing selected</string> + <string name="selected_chat_items_selected_n">Selected %d</string> + <string name="forward_alert_title_messages_to_forward">Forward %1$s message(s)?</string> + <string name="forward_alert_title_nothing_to_forward">Nothing to forward!</string> + <string name="forward_alert_forward_messages_without_files">Forward messages without files?</string> + <string name="forward_files_messages_deleted_after_selection_desc">Messages were deleted after you selected them.</string> + <string name="forward_files_not_accepted_desc">%1$d file(s) were not downloaded.</string> + <string name="forward_files_in_progress_desc">%1$d file(s) are still being downloaded.</string> + <string name="forward_files_failed_to_receive_desc">%1$d file(s) failed to download.</string> + <string name="forward_files_missing_desc">%1$d file(s) were deleted.</string> + <string name="forward_files_not_accepted_receive_files">Download</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s messages not forwarded</string> + <string name="chat_list_favorites">Favorites</string> + <string name="chat_list_contacts">Contacts</string> + <string name="chat_list_groups">Groups</string> + <string name="chat_list_businesses">Businesses</string> + <string name="chat_list_notes">Notes</string> + <string name="chat_list_group_reports">Reports</string> + <string name="notification_group_report">Report: %s</string> + <string name="chat_list_all">All</string> + <string name="chat_list_add_list">Add list</string> + <string name="group_reports_active_one">1 report</string> + <string name="group_reports_active">%d reports</string> + <string name="group_reports_member_reports">Member reports</string> <!-- ShareListView.kt --> <string name="share_message">Share message…</string> <string name="share_image">Share media…</string> <string name="share_file">Share file…</string> + <string name="forward_message">Forward message…</string> + <string name="forward_multiple">Forward messages…</string> + <string name="cannot_share_message_alert_title">Cannot send message</string> + <string name="cannot_share_message_alert_text">Selected chat preferences prohibit this message.</string> <!-- ComposeView.kt, helpers --> <string name="attach">Attach</string> @@ -318,11 +487,37 @@ <string name="image_decoding_exception_desc">The image cannot be decoded. Please, try a different image or contact developers.</string> <string name="video_decoding_exception_desc">The video cannot be decoded. Please, try a different video or contact developers.</string> <string name="you_are_observer">you are observer</string> - <string name="observer_cant_send_message_title">You can\'t send messages!</string> - <string name="observer_cant_send_message_desc">Please contact group admin.</string> <string name="files_and_media_prohibited">Files and media prohibited!</string> <string name="only_owners_can_enable_files_and_media">Only group owners can enable files and media.</string> <string name="compose_send_direct_message_to_connect">Send direct message to connect</string> + <string name="compose_forward_messages_n">Forwarding %1$s messages</string> + <string name="compose_save_messages_n">Saving %1$s messages</string> + <string name="simplex_links_not_allowed">SimpleX links not allowed</string> + <string name="files_and_media_not_allowed">Files and media not allowed</string> + <string name="voice_messages_not_allowed">Voice messages not allowed</string> + <string name="compose_message_placeholder">Message</string> + <string name="maximum_message_size_title">Message is too large!</string> + <string name="maximum_message_size_reached_text">Please reduce the message size and send again.</string> + <string name="maximum_message_size_reached_non_text">Please reduce the message size or remove media and send again.</string> + <string name="maximum_message_size_reached_forwarding">You can copy and reduce the message size to send it.</string> + <string name="report_compose_reason_header_spam">Report spam: only group moderators will see it.</string> + <string name="report_compose_reason_header_profile">Report member profile: only group moderators will see it.</string> + <string name="report_compose_reason_header_community">Report violation: only group moderators will see it.</string> + <string name="report_compose_reason_header_illegal">Report content: only group moderators will see it.</string> + <string name="report_compose_reason_header_other">Report other: only group moderators will see it.</string> + + <string name="cant_send_message_alert_title">You can\'t send messages!</string> + <string name="cant_send_message_contact_not_ready">contact not ready</string> + <string name="cant_send_message_contact_deleted">contact deleted</string> + <string name="cant_send_message_contact_not_synchronized">not synchronized</string> + <string name="cant_send_message_contact_disabled">contact disabled</string> + <string name="observer_cant_send_message_title">you are observer</string> + <string name="observer_cant_send_message_desc">Please contact group admin.</string> + <string name="cant_send_message_rejected">request to join rejected</string> + <string name="cant_send_message_group_deleted">group is deleted</string> + <string name="cant_send_message_mem_removed">removed from group</string> + <string name="cant_send_message_you_left">you left</string> + <string name="cant_send_message_generic">can\'t send messages</string> <!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt --> <string name="image_descr">Image</string> @@ -356,6 +551,9 @@ <string name="error_saving_file">Error saving file</string> <string name="loading_remote_file_title">Loading the file </string> <string name="loading_remote_file_desc">Please, wait while the file is being loaded from the linked mobile</string> + <string name="file_error">File error</string> + <string name="temporary_file_error">Temporary file error</string> + <string name="open_with_app">Open with %s</string> <!-- Voice messages --> <string name="voice_message">Voice message</string> @@ -364,13 +562,35 @@ <!-- Chat Info Settings - ChatInfoView.kt --> <string name="notifications">Notifications</string> + <string name="disable_automatic_deletion_question">Disable automatic message deletion?</string> + <string name="change_automatic_deletion_question">Change automatic message deletion?</string> + <string name="disable_automatic_deletion_message">Messages in this chat will never be deleted.</string> + <string name="change_automatic_chat_deletion_message">This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted.</string> + <string name="disable_automatic_deletion">Disable delete messages</string> + <string name="chat_ttl_options_footer">Delete chat messages from your device.</string> <!-- Chat Info Actions - ChatInfoView.kt --> + <string name="info_view_connect_button">connect</string> + <string name="info_view_open_button">open</string> + <string name="info_view_message_button">message</string> + <string name="info_view_call_button">call</string> + <string name="info_view_search_button">search</string> + <string name="info_view_video_button">video</string> <string name="delete_contact_question">Delete contact?</string> <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Contact and all messages will be deleted - this cannot be undone!</string> + <string name="delete_contact_cannot_undo_warning">Contact will be deleted - this cannot be undone!</string> + <string name="keep_conversation">Keep conversation</string> + <string name="only_delete_conversation">Only delete conversation</string> + <string name="confirm_delete_contact_question">Confirm contact deletion?</string> <string name="delete_and_notify_contact">Delete and notify contact</string> + <string name="delete_without_notification">Delete without notification</string> <string name="button_delete_contact">Delete contact</string> + <string name="conversation_deleted">Conversation deleted!</string> + <string name="you_can_still_send_messages_to_contact">You can send messages to %1$s from Archived contacts.</string> + <string name="contact_deleted">Contact deleted!</string> + <string name="you_can_still_view_conversation_with_contact">You can still view conversation with %1$s in the list of chats.</string> <string name="text_field_set_contact_placeholder">Set contact name…</string> + <string name="text_field_set_chat_placeholder">Set chat name…</string> <string name="icon_descr_server_status_connected">Connected</string> <string name="icon_descr_server_status_disconnected">Disconnected</string> <string name="icon_descr_server_status_error">Error</string> @@ -383,6 +603,10 @@ <string name="sync_connection_force_question">Renegotiate encryption?</string> <string name="sync_connection_force_desc">The encryption is working and the new encryption agreement is not required. It may result in connection errors!</string> <string name="sync_connection_force_confirm">Renegotiate</string> + <string name="sync_connection_question">Fix connection?</string> + <string name="sync_connection_desc">Connection requires encryption renegotiation.</string> + <string name="sync_connection_confirm">Fix</string> + <string name="encryption_renegotiation_in_progress">Encryption renegotiation in progress.</string> <string name="view_security_code">View security code</string> <string name="verify_security_code">Verify security code</string> @@ -427,6 +651,11 @@ <string name="connect_via_link_or_qr_from_clipboard_or_in_person">(scan or paste from clipboard)</string> <string name="only_stored_on_members_devices">(only stored by group members)</string> + <!-- QRCodeScanner --> + <string name="enable_camera_access">Enable camera access</string> + <string name="tap_to_scan">Tap to scan</string> + <string name="camera_not_available">Camera not available</string> + <!-- GetImageView --> <string name="toast_permission_denied">Permission Denied!</string> <string name="use_camera_button">Camera</string> @@ -442,8 +671,8 @@ <string name="to_start_a_new_chat_help_header">To start a new chat</string> <string name="chat_help_tap_button">Tap button</string> <string name="above_then_preposition_continuation">above, then:</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Add new contact</b>: to create your one-time QR Code for your contact.]]></string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Scan QR code</b>: to connect to your contact who shows QR code to you.]]></string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Add contact</b>: to create a new invitation link, or connect via a link you received.]]></string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Create group</b>: to create a new group.]]></string> <string name="to_connect_via_link_title">To connect via link</string> <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">If you received SimpleX Chat invitation link, you can open it in your browser:</string> <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 desktop: scan displayed QR code from the app, via <b>Scan QR code</b>.]]></string> @@ -458,7 +687,9 @@ <!-- Clear Chat - ChatListNavLinkView.kt --> <string name="clear_chat_question">Clear chat?</string> + <string name="clear_note_folder_question">Clear private notes?</string> <string name="clear_chat_warning">All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</string> + <string name="clear_note_folder_warning">All messages will be deleted - this cannot be undone!</string> <string name="clear_verb">Clear</string> <string name="clear_chat_button">Clear chat</string> <string name="clear_chat_menu_action">Clear</string> @@ -470,10 +701,24 @@ <!-- Actions - ChatListNavLinkView.kt --> <string name="mute_chat">Mute</string> + <string name="mute_all_chat">Mute all</string> <string name="unmute_chat">Unmute</string> <string name="favorite_chat">Favorite</string> <string name="unfavorite_chat">Unfavorite</string> + <string name="unread_mentions">Unread mentions</string> + <!-- Tags - ChatListNavLinkView.kt --> + <string name="create_list">Create list</string> + <string name="add_to_list">Add to list</string> + <string name="change_list">Change list</string> + <string name="save_list">Save list</string> + <string name="list_name_field_placeholder">List name...</string> + <string name="duplicated_list_error">List name and emoji should be different for all lists.</string> + <string name="delete_chat_list_menu_action">Delete</string> + <string name="delete_chat_list_question">Delete list?</string> + <string name="delete_chat_list_warning">All chats will be removed from the list %s, and the list deleted</string> + <string name="edit_chat_list_menu_action">Edit</string> + <string name="change_order_chat_list_menu_action">Change order</string> <!-- Pending contact connection alert dialogues --> <string name="you_invited_a_contact">You invited a contact</string> @@ -534,10 +779,19 @@ <string name="if_you_cant_meet_in_person">If you can\'t meet in person, show QR code in a video call, or share the link.</string> <!-- User Address Learn More - UserAddressLearnMore.kt --> + <string name="share_address_publicly">Share address publicly</string> + <string name="share_simplex_address_on_social_media">Share SimpleX address on social media.</string> <string name="you_can_share_your_address">You can share your address as a link or QR code - anybody can connect to you.</string> <string name="you_wont_lose_your_contacts_if_delete_address">You won\'t lose your contacts if you later delete your address.</string> + <string name="share_1_time_link_with_a_friend">Share 1-time link with a friend</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[1-time link can be used <i>with one contact only</i> - share in person or via any messenger.]]></string> + <string name="you_can_set_connection_name_to_remember">You can set connection name, to remember who the link was shared with.</string> + <string name="connection_security">Connection security</string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">SimpleX address and 1-time links are safe to share via any messenger.</string> + <string name="to_protect_against_your_link_replaced_compare_codes">To protect against your link being replaced, you can compare contact security codes.</string> <string name="you_can_accept_or_reject_connection">When people request to connect, you can accept or reject it.</string> <string name="read_more_in_user_guide_with_link"><![CDATA[Read more in <font color="#0088ff">User Guide</font>.]]></string> + <string name="address_or_1_time_link">Address or 1-time link?</string> <!-- PasteToConnect.kt --> <string name="connect_via_link">Connect via link</string> @@ -546,11 +800,39 @@ <string name="this_string_is_not_a_connection_link">This string is not a connection link!</string> <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[You can also connect by clicking the link. If it opens in the browser, click <b>Open in mobile app</b> button.]]></string> - <!-- CreateLinkView.kt --> - <string name="create_one_time_link">Create one-time invitation link</string> + <!-- NewChatView.kt --> + <string name="new_chat">New chat</string> + <string name="new_message">New message</string> + <string name="add_contact_tab">Add contact</string> + <string name="scan_paste_link">Scan / Paste link</string> + <string name="paste_link">Paste link</string> <string name="one_time_link">One-time invitation link</string> <string name="one_time_link_short">1-time link</string> <string name="simplex_address">SimpleX address</string> + <string name="or_show_this_qr_code">Or show this code</string> + <string name="full_link_button_text">Full link</string> + <string name="short_link_button_text">Short link</string> + <string name="new_chat_share_profile">Share profile</string> + <string name="select_chat_profile">Select chat profile</string> + <string name="switching_profile_error_title">Error switching profile</string> + <string name="switching_profile_error_message">Your connection was moved to %s but an unexpected error occurred while redirecting you to the profile.</string> + <string name="or_scan_qr_code">Or scan QR code</string> + <string name="keep_unused_invitation_question">Keep unused invitation?</string> + <string name="you_can_view_invitation_link_again">You can view invitation link again in connection details.</string> + <string name="keep_invitation_link">Keep</string> + <string name="creating_link">Creating link…</string> + <string name="retry_verb">Retry</string> + <string name="share_this_1_time_link">Share this 1-time invite link</string> + <string name="paste_the_link_you_received">Paste the link you received</string> + <string name="the_text_you_pasted_is_not_a_link">The text you pasted is not a SimpleX link.</string> + <string name="tap_to_paste_link">Tap to paste link</string> + + <string name="invalid_qr_code">Invalid QR code</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">The code you scanned is not a SimpleX link QR code.</string> + + <string name="deleted_chats">Archived contacts</string> + <string name="no_filtered_contacts">No filtered contacts</string> + <string name="contact_list_header_title">Your contacts</string> <!-- ScanCodeView.kt --> <string name="scan_code">Scan code</string> @@ -579,10 +861,13 @@ <string name="send_us_an_email">Send us email</string> <string name="chat_lock">SimpleX Lock</string> <string name="chat_console">Chat console</string> + <string name="message_servers">Message servers</string> <string name="smp_servers">SMP servers</string> + <string name="smp_servers_configured">Configured SMP servers</string> + <string name="smp_servers_other">Other SMP servers</string> <string name="smp_servers_preset_address">Preset server address</string> <string name="smp_servers_preset_add">Add preset servers</string> - <string name="smp_servers_add">Add server…</string> + <string name="smp_servers_add">Add server</string> <string name="smp_servers_test_server">Test server</string> <string name="smp_servers_test_servers">Test servers</string> <string name="smp_servers_save">Save servers</string> @@ -590,6 +875,7 @@ <string name="smp_servers_test_some_failed">Some servers failed the test:</string> <string name="smp_servers_scan_qr">Scan server QR code</string> <string name="smp_servers_enter_manually">Enter server manually</string> + <string name="smp_servers_new_server">New server</string> <string name="smp_servers_preset_server">Preset server</string> <string name="smp_servers_your_server">Your server</string> <string name="smp_servers_your_server_address">Your server address</string> @@ -601,8 +887,13 @@ <string name="smp_servers_delete_server">Delete server</string> <string name="smp_servers_per_user">The servers for new connections of your current chat profile</string> <string name="smp_save_servers_question">Save servers?</string> + <string name="media_and_file_servers">Media & file servers</string> <string name="xftp_servers">XFTP servers</string> + <string name="xftp_servers_configured">Configured XFTP servers</string> + <string name="xftp_servers_other">Other XFTP servers</string> + <string name="subscription_percentage">Show percentage</string> <string name="install_simplex_chat_for_terminal">Install SimpleX Chat for terminal</string> + <string name="reset_all_hints">Reset all hints</string> <string name="star_on_github">Star on GitHub</string> <string name="contribute">Contribute</string> <string name="rate_the_app">Rate the app</string> @@ -621,17 +912,27 @@ <string name="save_servers_button">Save</string> <string name="network_and_servers">Network & servers</string> <string name="network_settings">Advanced network settings</string> - <string name="network_settings_title">Network settings</string> + <string name="network_settings_title">Advanced settings</string> + <string name="network_socks_proxy">SOCKS proxy</string> <string name="network_socks_proxy_settings">SOCKS proxy settings</string> <string name="network_socks_toggle_use_socks_proxy">Use SOCKS proxy</string> + <string name="network_proxy_auth">Proxy authentication</string> + <string name="network_proxy_random_credentials">Use random credentials</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">Use different proxy credentials for each profile.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">Use different proxy credentials for each connection.</string> + <string name="network_proxy_auth_mode_no_auth">Do not use credentials with proxy.</string> + <string name="network_proxy_auth_mode_username_password">Your credentials may be sent unencrypted.</string> + <string name="network_proxy_username">Username</string> + <string name="network_proxy_password">Password</string> <string name="network_proxy_port">port %d</string> + <string name="network_proxy_incorrect_config_title">Error saving proxy</string> + <string name="network_proxy_incorrect_config_desc">Make sure proxy configuration is correct.</string> <string name="host_verb">Host</string> <string name="port_verb">Port</string> <string name="network_enable_socks">Use SOCKS proxy?</string> <string name="network_enable_socks_info">Access the servers via SOCKS proxy on port %d? Proxy must be started before enabling this option.</string> <string name="network_disable_socks">Use direct Internet connection?</string> <string name="network_disable_socks_info">If you confirm, the messaging servers will be able to see your IP address, and your provider - which servers you are connecting to.</string> - <string name="update_onion_hosts_settings_question">Update .onion hosts setting?</string> <string name="network_use_onion_hosts">Use .onion hosts</string> <string name="network_use_onion_hosts_prefer">When available</string> <string name="network_use_onion_hosts_no">No</string> @@ -639,31 +940,83 @@ <string name="network_use_onion_hosts_prefer_desc">Onion hosts will be used when available.</string> <string name="network_use_onion_hosts_no_desc">Onion hosts will not be used.</string> <string name="network_use_onion_hosts_required_desc">Onion hosts will be required for connection.\nPlease note: you will not be able to connect to the servers without .onion address.</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hosts will be used when available.</string> - <string name="network_use_onion_hosts_no_desc_in_alert">Onion hosts will not be used.</string> - <string name="network_use_onion_hosts_required_desc_in_alert">Onion hosts will be required for connection.</string> <string name="network_session_mode_transport_isolation">Transport isolation</string> <string name="network_session_mode_user">Chat profile</string> + <string name="network_session_mode_session">App session</string> + <string name="network_session_mode_server">Server</string> <string name="network_session_mode_entity">Connection</string> <string name="network_session_mode_user_description"><![CDATA[A separate TCP connection (and SOCKS credential) will be used <b>for each chat profile you have in the app</b>.]]></string> + <string name="network_session_mode_session_description">New SOCKS credentials will be used every time you start the app.</string> + <string name="network_session_mode_server_description">New SOCKS credentials will be used for each server.</string> <string name="network_session_mode_entity_description"><![CDATA[A separate TCP connection (and SOCKS credential) will be used <b>for each contact and group member</b>.\n<b>Please note</b>: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.]]></string> <string name="update_network_session_mode_question">Update transport isolation mode?</string> <string name="disable_onion_hosts_when_not_supported"><![CDATA[Set <i>Use .onion hosts</i> to No if SOCKS proxy does not support them.]]></string> <string name="socks_proxy_setting_limitations"><![CDATA[<b>Please note</b>: message and file relays are connected via SOCKS proxy. Calls and sending link previews use direct connection.]]></string> + <string name="network_smp_proxy_mode_private_routing">Private routing</string> + <string name="network_smp_proxy_mode_always">Always</string> + <string name="network_smp_proxy_mode_unknown">Unknown servers</string> + <string name="network_smp_proxy_mode_unprotected">Unprotected</string> + <string name="network_smp_proxy_mode_never">Never</string> + <string name="network_smp_proxy_mode_always_description">Always use private routing.</string> + <string name="network_smp_proxy_mode_unknown_description">Use private routing with unknown servers.</string> + <string name="network_smp_proxy_mode_unprotected_description">Use private routing with unknown servers when IP address is not protected.</string> + <string name="network_smp_proxy_mode_never_description">Do NOT use private routing.</string> + <string name="update_network_smp_proxy_mode_question">Message routing mode</string> + <string name="network_smp_proxy_fallback_allow_downgrade">Allow downgrade</string> + <string name="network_smp_proxy_fallback_allow">Yes</string> + <string name="network_smp_proxy_fallback_allow_protected">When IP hidden</string> + <string name="network_smp_proxy_fallback_prohibit">No</string> + <string name="network_smp_proxy_fallback_allow_description">Send messages directly when your or destination server does not support private routing.</string> + <string name="network_smp_proxy_fallback_allow_protected_description">Send messages directly when IP address is protected and your or destination server does not support private routing.</string> + <string name="network_smp_proxy_fallback_prohibit_description">Do NOT send messages directly, even if your or destination server does not support private routing.</string> + <string name="update_network_smp_proxy_fallback_question">Message routing fallback</string> + <string name="private_routing_show_message_status">Show message status</string> + <string name="private_routing_explanation">To protect your IP address, private routing uses your SMP servers to deliver messages.</string> + <string name="network_smp_web_port_section_title">TCP port for messaging</string> + <string name="network_smp_web_port_toggle">Use web port</string> + <string name="network_smp_web_port_footer">Use TCP port %1$s when no port is specified.</string> + <string name="network_smp_web_port_preset_footer">Use TCP port 443 for preset servers only.</string> + <string name="network_smp_web_port_all">All servers</string> + <string name="network_smp_web_port_preset">Preset servers</string> + <string name="network_smp_web_port_off">Off</string> <string name="appearance_settings">Appearance</string> <string name="customize_theme_title">Customize theme</string> - <string name="theme_colors_section_title">THEME COLORS</string> + <string name="theme_colors_section_title">INTERFACE COLORS</string> <string name="app_version_title">App version</string> <string name="app_version_name">App version: v%s</string> <string name="app_version_code">App build: %s</string> <string name="core_version">Core version: v%s</string> <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> + <string name="app_check_for_updates">Check for updates</string> + <string name="app_check_for_updates_disabled">Disabled</string> + <string name="app_check_for_updates_stable">Stable</string> + <string name="app_check_for_updates_beta">Beta</string> + <string name="app_check_for_updates_update_available">Update available: %s</string> + <string name="app_check_for_updates_button_download">Download %s (%s)</string> + <string name="app_check_for_updates_button_skip">Skip this version</string> + <string name="app_check_for_updates_download_started">Downloading app update, don\'t close the app</string> + <string name="app_check_for_updates_download_completed_title">App update is downloaded</string> + <string name="app_check_for_updates_button_open">Open file location</string> + <string name="app_check_for_updates_button_install">Install update</string> + <string name="app_check_for_updates_installed_successfully_title">Installed successfully</string> + <string name="app_check_for_updates_installed_successfully_desc">Please restart the app.</string> + <string name="app_check_for_updates_canceled">Update download canceled</string> + <string name="app_check_for_updates_button_remind_later">Remind later</string> + <string name="app_check_for_updates_notice_title">Check for updates</string> + <string name="app_check_for_updates_notice_desc">To be notified about the new releases, turn on periodic check for Stable or Beta versions.</string> + <string name="app_check_for_updates_notice_disable">Disable</string> + <string name="show_dev_options">Show:</string> <string name="hide_dev_options">Hide:</string> <string name="show_developer_options">Show developer options</string> + <string name="debug_logs">Enable logs</string> <string name="developer_options">Database IDs and Transport isolation option.</string> + <string name="developer_options_section">Developer options</string> + <string name="show_internal_errors">Show internal errors</string> + <string name="show_slow_api_calls">Show slow API calls</string> <string name="shutdown_alert_question">Shutdown?</string> <string name="shutdown_alert_desc">Notifications will stop working until you re-launch the app</string> + <string name="prefs_error_saving_settings">Error saving settings</string> <!-- Address Items - UserAddressView.kt --> <string name="create_address">Create address</string> @@ -688,12 +1041,20 @@ <string name="invite_friends">Invite friends</string> <string name="email_invite_subject">Let\'s talk in SimpleX Chat</string> <string name="email_invite_body">Hi!\nConnect to me via SimpleX Chat: %s</string> + <string name="for_social_media">For social media</string> + <string name="or_to_share_privately">Or to share privately</string> + <string name="simplex_address_or_1_time_link">SimpleX address or 1-time link?</string> + <string name="create_1_time_link">Create 1-time link</string> + <string name="address_settings">Address settings</string> + <string name="business_address">Business address</string> + <string name="add_your_team_members_to_conversations">Add your team members to the conversations.</string> <!-- CreateSimpleXAddress.kt --> <string name="continue_to_next_step">Continue</string> <string name="dont_create_address">Don\'t create address</string> <string name="you_can_create_it_later">You can create it later</string> <string name="you_can_make_address_visible_via_settings">You can make it visible to your SimpleX contacts via Settings.</string> + <string name="invite_friends_short">Invite</string> <!-- User profile details - UserProfileView.kt --> <string name="display_name__field">Profile name:</string> @@ -728,6 +1089,7 @@ <string name="display_name">Enter your name:</string> <string name="create_profile_button">Create</string> <string name="create_another_profile_button">Create profile</string> + <string name="create_address_button">Create</string> <string name="invalid_name">Invalid name!</string> <string name="correct_name_to">Correct name to %s?</string> <string name="about_simplex">About SimpleX</string> @@ -762,45 +1124,85 @@ <string name="callstate_connected">connected</string> <string name="callstate_ended">ended</string> + <!-- CallView --> + <string name="unable_to_open_browser_title">Error opening browser</string> + <string name="unable_to_open_browser_desc">The default web browser is required for calls. Please configure the default browser in the system, and share more information with the developers.</string> + <string name="permissions_required">Grant permissions</string> + <string name="permissions_record_audio">Microphone</string> + <string name="permissions_camera">Camera</string> + <string name="permissions_camera_and_record_audio">Camera and microphone</string> + <string name="permissions_grant">Grant permission(s) to make calls</string> + <string name="permissions_grant_in_settings">Grant in settings</string> + <string name="permissions_find_in_settings_and_grant">Find this permission in Android settings and grant it manually.</string> + <string name="permissions_open_settings">Open settings</string> + <string name="audio_device_earpiece">Earpiece</string> + <string name="audio_device_speaker">Speaker</string> + <string name="audio_device_wired_headphones">Headphones</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="error_initializing_web_view">Error initializing WebView. Update your system to the new version. Please contact developers.\nError: %s</string> + <string name="error_initializing_web_view_wrong_arch">Error initializing WebView. Make sure you have WebView installed and it\'s supported architecture is arm64.\nError: %s</string> + <!-- SimpleXInfo --> - <string name="next_generation_of_private_messaging">The next generation of private messaging</string> + <string name="next_generation_of_private_messaging">The future of messaging</string> <string name="privacy_redefined">Privacy redefined</string> - <string name="first_platform_without_user_ids">The 1st platform without any user identifiers – private by design.</string> - <string name="immune_to_spam_and_abuse">Immune to spam and abuse</string> - <string name="people_can_connect_only_via_links_you_share">People can connect to you only via the links you share.</string> + <string name="first_platform_without_user_ids">No user identifiers.</string> + <string name="immune_to_spam_and_abuse">Immune to spam</string> + <string name="people_can_connect_only_via_links_you_share">You decide who can connect.</string> <string name="decentralized">Decentralized</string> - <string name="opensource_protocol_and_code_anybody_can_run_servers">Open-source protocol and code – anybody can run the servers.</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Anybody can host servers.</string> <string name="create_your_profile">Create your profile</string> <string name="make_private_connection">Make a private connection</string> + <string name="migrate_from_another_device">Migrate from another device</string> <string name="how_it_works">How it works</string> <!-- How SimpleX Works --> <string name="how_simplex_works">How SimpleX works</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[Many people asked: <i>if SimpleX has no user identifiers, how can it deliver messages?</i>]]></string> - <string name="to_protect_privacy_simplex_has_ids_for_queues">To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[You control through which server(s) <b>to receive</b> the messages, your contacts – the servers you use to message them.]]></string> - <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Only client devices store user profiles, contacts, groups, and messages sent with <b>2-layer end-to-end encryption</b>.]]></string> - <string name="read_more_in_github">Read more in our GitHub repository.</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">To protect your privacy, SimpleX uses separate IDs for each of your contacts.</string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Only client devices store user profiles, contacts, groups, and messages.</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[All messages and files are sent <b>end-to-end encrypted</b>, with post-quantum security in direct messages.]]></string> <string name="read_more_in_github_with_link"><![CDATA[Read more in our <font color="#0088ff">GitHub repository</font>.]]></string> <!-- SetNotificationsMode.kt --> <string name="use_chat">Use chat</string> <string name="onboarding_notifications_mode_title">Private notifications</string> - <string name="onboarding_notifications_mode_subtitle">It can be changed later via settings.</string> + <string name="onboarding_notifications_mode_subtitle">How it affects battery</string> <string name="onboarding_notifications_mode_off">When app is running</string> <string name="onboarding_notifications_mode_periodic">Periodic</string> <string name="onboarding_notifications_mode_service">Instant</string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Best for battery</b>. You will receive notifications only when the app is running (NO background service).]]></string> - <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Good for battery</b>. Background service checks messages every 10 minutes. You may miss calls or urgent messages.]]></string> - <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Uses more battery</b>! Background service always runs – notifications are shown as soon as messages are available.]]></string> + <string name="onboarding_notifications_mode_off_desc_short">No background service</string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Good for battery</b>. App checks messages every 10 minutes. You may miss calls or urgent messages.]]></string> + <string name="onboarding_notifications_mode_periodic_desc_short">Check messages every 10 minutes</string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Uses more battery</b>! App always runs in background – notifications are shown instantly.]]></string> + <string name="onboarding_notifications_mode_service_desc_short">App always runs in background</string> + <string name="onboarding_notifications_mode_battery">Notifications and battery</string> <!-- SetupDatabasePassphrase.kt --> <string name="setup_database_passphrase">Setup database passphrase</string> <string name="you_can_change_it_later">Random passphrase is stored in settings as plaintext.\nYou can change it later.</string> <string name="use_random_passphrase">Use random passphrase</string> - <!-- MakeConnection --> - <string name="paste_the_link_you_received">Paste received link</string> + <!-- ChooseServerOperators.kt --> + <string name="onboarding_conditions_private_chats_not_accessible">Private chats, groups and your contacts are not accessible to server operators.</string> + <string name="onboarding_conditions_by_using_you_agree">By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam.</string> + <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Privacy policy and conditions of use.</string> + <string name="onboarding_conditions_accept">Accept</string> + <string name="onboarding_conditions_configure_server_operators">Configure server operators</string> + <string name="onboarding_choose_server_operators">Server operators</string> + <string name="onboarding_network_operators">Network operators</string> + <string name="onboarding_network_operators_simplex_flux_agreement">SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.</string> + <string name="onboarding_network_operators_app_will_use_different_operators">The app protects your privacy by using different operators in each conversation.</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">When more than one operator is enabled, none of them has metadata to learn who communicates with whom.</string> + <string name="onboarding_network_operators_app_will_use_for_routing">For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.</string> + <string name="onboarding_network_about_operators">About operators</string> + <string name="onboarding_select_network_operators_to_use">Select network operators to use.</string> + <string name="how_it_helps_privacy">How it helps privacy</string> + <string name="onboarding_network_operators_configure_via_settings">You can configure servers via settings.</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">Conditions will be accepted for enabled operators after 30 days.</string> + <string name="onboarding_network_operators_conditions_you_can_configure">You can configure operators in Network & servers settings.</string> + <string name="onboarding_network_operators_review_later">Review later</string> + <string name="onboarding_network_operators_update">Update</string> + <string name="onboarding_network_operators_continue">Continue</string> <!-- Call --> <string name="incoming_video_call">Incoming video call</string> @@ -816,6 +1218,9 @@ <string name="call_already_ended">Call already ended!</string> <string name="icon_descr_video_call">video call</string> <string name="icon_descr_audio_call">audio call</string> + <string name="call_desktop_permission_denied_title">To make calls, allow to use your microphone. End the call and try to call again.</string> + <string name="call_desktop_permission_denied_chrome">Click info button near address field to allow using microphone.</string> + <string name="call_desktop_permission_denied_safari">Open Safari Settings / Websites / Microphone, then choose Allow for localhost.</string> <!-- Call settings --> <string name="settings_audio_video_calls">Audio & video calls</string> @@ -849,6 +1254,7 @@ <string name="icon_descr_audio_on">Audio on</string> <string name="icon_descr_speaker_off">Speaker off</string> <string name="icon_descr_speaker_on">Speaker on</string> + <string name="icon_descr_sound_muted">Sound muted</string> <string name="icon_descr_flip_camera">Flip camera</string> <!-- Call items --> @@ -883,6 +1289,9 @@ <string name="protect_app_screen">Protect app screen</string> <string name="encrypt_local_files">Encrypt local files</string> <string name="auto_accept_images">Auto-accept images</string> + <string name="protect_ip_address">Protect IP address</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">The app will ask to confirm downloads from unknown file servers (except .onion or when SOCKS proxy is enabled).</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Without Tor or VPN, your IP address will be visible to file servers.</string> <string name="send_link_previews">Send link previews</string> <string name="privacy_show_last_messages">Show last messages</string> <string name="privacy_message_draft">Message draft</string> @@ -897,6 +1306,7 @@ <string name="authentication_cancelled">Authentication cancelled</string> <string name="la_mode_system">System</string> <string name="la_mode_passcode">Passcode</string> + <string name="la_app_passcode">App passcode</string> <string name="la_mode_off">Off</string> <string name="passcode_set">Passcode set!</string> <string name="passcode_changed">Passcode changed!</string> @@ -937,26 +1347,49 @@ <string name="receipts_groups_disable_keep_overrides">Disable (keep group overrides)</string> <string name="receipts_groups_enable_for_all">Enable for all groups</string> <string name="receipts_groups_disable_for_all">Disable for all groups</string> + <string name="privacy_media_blur_radius">Blur media</string> + <string name="privacy_media_blur_radius_off">Off</string> + <string name="privacy_media_blur_radius_soft">Soft</string> + <string name="privacy_media_blur_radius_medium">Medium</string> + <string name="privacy_media_blur_radius_strong">Strong</string> + <string name="privacy_chat_list_open_links">Open links from chat list</string> + <string name="privacy_chat_list_open_links_yes">Yes</string> + <string name="privacy_chat_list_open_links_no">No</string> + <string name="privacy_chat_list_open_links_ask">Ask</string> + <string name="privacy_chat_list_open_web_link_question">Open web link?</string> + <string name="privacy_chat_list_open_web_link">Open link</string> <!-- Settings sections --> <string name="settings_section_title_you">YOU</string> <string name="settings_section_title_settings">SETTINGS</string> + <string name="settings_section_title_chat_database">CHAT DATABASE</string> <string name="settings_section_title_help">HELP</string> <string name="settings_section_title_support">SUPPORT SIMPLEX CHAT</string> <string name="settings_section_title_app">APP</string> <string name="settings_section_title_device">DEVICE</string> <string name="settings_section_title_chats">CHATS</string> + <string name="settings_section_title_files">FILES</string> <string name="settings_section_title_delivery_receipts">SEND DELIVERY RECEIPTS TO</string> <string name="settings_restart_app">Restart</string> <string name="settings_shutdown">Shutdown</string> <string name="settings_developer_tools">Developer tools</string> <string name="settings_experimental_features">Experimental features</string> <string name="settings_section_title_socks">SOCKS PROXY</string> + <string name="settings_section_title_interface" translatable="false">INTERFACE</string> <string name="settings_section_title_language" translatable="false">LANGUAGE</string> <string name="settings_section_title_icon">APP ICON</string> <string name="settings_section_title_themes">THEMES</string> + <string name="settings_section_title_profile_images">Profile images</string> + <string name="settings_section_title_message_shape">Message shape</string> + <string name="settings_message_shape_corner">Corner</string> + <string name="settings_message_shape_tail">Tail</string> + <string name="settings_section_title_chat_theme">Chat theme</string> + <string name="settings_section_title_user_theme">Profile theme</string> + <string name="settings_section_title_chat_colors">Chat colors</string> <string name="settings_section_title_messages">MESSAGES AND FILES</string> + <string name="settings_section_title_private_message_routing">PRIVATE MESSAGE ROUTING</string> <string name="settings_section_title_calls">CALLS</string> + <string name="settings_section_title_network_connection">Network connection</string> <string name="settings_section_title_incognito">Incognito mode</string> <string name="settings_section_title_experimenta">EXPERIMENTAL</string> <string name="settings_section_title_use_from_desktop">Use from desktop</string> @@ -964,6 +1397,7 @@ <!-- DatabaseView.kt --> <string name="your_chat_database">Your chat database</string> <string name="run_chat_section">RUN CHAT</string> + <string name="remote_hosts_section">Remote mobiles</string> <string name="chat_is_running">Chat is running</string> <string name="chat_is_stopped">Chat is stopped</string> <string name="chat_database_section">CHAT DATABASE</string> @@ -989,13 +1423,12 @@ <string name="error_importing_database">Error importing chat database</string> <string name="chat_database_imported">Chat database imported</string> <string name="restart_the_app_to_use_imported_chat_database">Restart the app to use imported chat database.</string> - <string name="non_fatal_errors_occured_during_import">Some non-fatal errors occurred during import - you may see Chat console for more details.</string> + <string name="non_fatal_errors_occured_during_import">Some non-fatal errors occurred during import:</string> <string name="delete_chat_profile_question">Delete chat profile?</string> <string name="delete_chat_profile_action_cannot_be_undone_warning">This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</string> <string name="chat_database_deleted">Chat database deleted</string> <string name="restart_the_app_to_create_a_new_chat_profile">Restart the app to create a new chat profile.</string> <string name="you_must_use_the_most_recent_version_of_database">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.</string> - <string name="stop_chat_to_enable_database_actions">Stop chat to enable database actions.</string> <string name="files_and_media_section">Files & media</string> <string name="delete_files_and_media_for_all_users">Delete files for all chat profiles</string> <string name="delete_files_and_media_all">Delete all files</string> @@ -1007,7 +1440,9 @@ <string name="chat_item_ttl_day">1 day</string> <string name="chat_item_ttl_week">1 week</string> <string name="chat_item_ttl_month">1 month</string> + <string name="chat_item_ttl_year">1 year</string> <string name="chat_item_ttl_seconds">%s second(s)</string> + <string name="chat_item_ttl_default">default (%s)</string> <string name="messages_section_title">Messages</string> <string name="messages_section_description">This setting applies to messages in your current chat profile</string> <string name="delete_messages_after">Delete messages after</string> @@ -1015,6 +1450,12 @@ <string name="enable_automatic_deletion_message">This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</string> <string name="delete_messages">Delete messages</string> <string name="error_changing_message_deletion">Error changing setting</string> + <string name="chat_database_exported_title">Chat database exported</string> + <string name="chat_database_exported_save">You may save the exported archive.</string> + <string name="chat_database_exported_migrate">You may migrate the exported database.</string> + <string name="chat_database_exported_not_all_files">Some file(s) were not exported</string> + <string name="chat_database_exported_continue">Continue</string> + <string name="error_saving_database">Error saving database</string> <!-- DatabaseEncryptionView.kt --> <string name="save_passphrase_in_keychain">Save passphrase in Keystore</string> @@ -1032,6 +1473,7 @@ <string name="confirm_new_passphrase">Confirm new passphrase…</string> <string name="update_database_passphrase">Update database passphrase</string> <string name="set_database_passphrase">Set database passphrase</string> + <string name="set_passphrase">Set passphrase</string> <string name="enter_correct_current_passphrase">Please enter correct current passphrase.</string> <string name="database_is_not_encrypted">Your chat database is not encrypted - set passphrase to protect it.</string> <string name="keychain_is_storing_securely">Android Keystore is used to securely store passphrase - it allows notification service to work.</string> @@ -1054,6 +1496,7 @@ <!-- DatabaseErrorView.kt --> <string name="wrong_passphrase">Wrong database passphrase</string> + <string name="error_reading_passphrase">Error reading database passphrase</string> <string name="encrypted_database">Encrypted database</string> <string name="database_error">Database error</string> <string name="keychain_error">Keychain error</string> @@ -1075,12 +1518,19 @@ <string name="restore_database_alert_desc">Please enter the previous password after restoring database backup. This action can not be undone.</string> <string name="restore_database_alert_confirm">Restore</string> <string name="database_restore_error">Restore database error</string> - <string name="restore_passphrase_not_found_desc">Passphrase not found in Keystore, please enter it manually. This may have happened if you restored the app\'s data using a backup tool. If it\'s not the case, please, contact developers.</string> + <string name="restore_passphrase_not_found_desc">Passphrase not found in Keystore, please enter it manually. This may have happened if you restored the app\'s data using a backup tool. If it\'s not the case, please contact developers.</string> + <string name="restore_passphrase_can_not_be_read_desc">Passphrase in Keystore can\'t be read. This may have happened after system update incompatible with the app. If it\'s not the case, please contact developers.</string> + <string name="restore_passphrase_can_not_be_read_enter_manually_desc">Passphrase in Keystore can\'t be read, please enter it manually. This may have happened after system update incompatible with the app. If it\'s not the case, please contact developers.</string> <string name="database_upgrade">Database upgrade</string> <string name="database_downgrade">Database downgrade</string> <string name="incompatible_database_version">Incompatible database version</string> <string name="confirm_database_upgrades">Confirm database upgrades</string> + <string name="one_hand_ui">Reachable app toolbars</string> + <string name="chat_bottom_bar">Reachable chat toolbar</string> + <string name="one_hand_ui_card_title">Toggle chat list:</string> + <string name="one_hand_ui_change_instruction">You can change it in Appearance settings.</string> <string name="terminal_always_visible">Show console in new window</string> + <string name="chat_list_always_visible">Show chat list in new window</string> <string name="invalid_migration_confirmation">Invalid migration confirmation</string> <string name="upgrade_and_open_chat">Upgrade and open chat</string> <string name="downgrade_and_open_chat">Downgrade and open chat</string> @@ -1092,14 +1542,8 @@ <!-- ChatModel.chatRunning interactions --> <string name="chat_is_stopped_indication">Chat is stopped</string> <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">You can start chat via app Settings / Database or by restarting the app.</string> - - <!-- ChatArchiveView.kt --> - <string name="chat_archive_header">Chat archive</string> - <string name="chat_archive_section">CHAT ARCHIVE</string> - <string name="save_archive">Save archive</string> - <string name="delete_archive">Delete archive</string> - <string name="archive_created_on_ts">Created on %1$s</string> - <string name="delete_chat_archive_question">Delete chat archive?</string> + <string name="start_chat_question">Start chat?</string> + <string name="chat_is_stopped_you_should_transfer_database">Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</string> <!-- Groups --> <string name="group_invitation_item_description">invitation to group %1$s</string> @@ -1111,7 +1555,9 @@ <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">You joined this group. Connecting to inviting group member.</string> <string name="leave_group_button">Leave</string> <string name="leave_group_question">Leave group?</string> + <string name="leave_chat_question">Leave chat?</string> <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">You will stop receiving messages from this group. Chat history will be preserved.</string> + <string name="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.</string> <string name="icon_descr_add_members">Invite members</string> <string name="icon_descr_group_inactive">Group inactive</string> <string name="alert_title_group_invitation_expired">Invitation expired!</string> @@ -1138,6 +1584,8 @@ <string name="rcv_group_event_member_connected">connected</string> <string name="rcv_group_event_member_left">left</string> <string name="rcv_group_event_changed_member_role">changed role of %s to %s</string> + <string name="rcv_group_event_member_blocked">blocked %s</string> + <string name="rcv_group_event_member_unblocked">unblocked %s</string> <string name="rcv_group_event_changed_your_role">changed your role to %s</string> <string name="rcv_group_event_member_deleted">removed %1$s</string> <string name="rcv_group_event_user_deleted">removed you</string> @@ -1147,6 +1595,8 @@ <string name="rcv_group_event_member_created_contact">connected directly</string> <string name="snd_group_event_changed_member_role">you changed role of %s to %s</string> <string name="snd_group_event_changed_role_for_yourself">you changed role for yourself to %s</string> + <string name="snd_group_event_member_blocked">you blocked %s</string> + <string name="snd_group_event_member_unblocked">you unblocked %s</string> <string name="snd_group_event_member_deleted">you removed %1$s</string> <string name="snd_group_event_user_left">you left</string> <string name="snd_group_event_group_profile_updated">group profile updated</string> @@ -1162,6 +1612,15 @@ <string name="rcv_group_event_open_chat">Open</string> + <!-- Profile update event chat items --> + <string name="profile_update_event_contact_name_changed">contact %1$s changed to %2$s</string> + <string name="profile_update_event_removed_picture">removed profile picture</string> + <string name="profile_update_event_set_new_picture">set new profile picture</string> + <string name="profile_update_event_removed_address">removed contact address</string> + <string name="profile_update_event_set_new_address">set new contact address</string> + <string name="profile_update_event_updated_profile">updated profile</string> + <string name="profile_update_event_member_name_changed">member %1$s changed to %2$s</string> + <!-- Conn event chat items --> <string name="rcv_conn_event_switch_queue_phase_completed">changed address for you</string> <string name="rcv_conn_event_switch_queue_phase_changing">changing address…</string> @@ -1180,19 +1639,26 @@ <string name="snd_conn_event_ratchet_sync_started">agreeing encryption for %s…</string> <string name="snd_conn_event_ratchet_sync_agreed">encryption agreed for %s</string> <string name="rcv_conn_event_verification_code_reset">security code changed</string> + <string name="conn_event_enabled_pq">quantum resistant e2e encryption</string> + <string name="conn_event_disabled_pq">standard end-to-end encryption</string> <!-- GroupMemberRole --> <string name="group_member_role_observer">observer</string> <string name="group_member_role_author">author</string> <string name="group_member_role_member">member</string> + <string name="group_member_role_moderator">moderator</string> <string name="group_member_role_admin">admin</string> <string name="group_member_role_owner">owner</string> <!-- GroupMemberStatus --> + <string name="group_member_status_rejected">rejected</string> <string name="group_member_status_removed">removed</string> <string name="group_member_status_left">left</string> <string name="group_member_status_group_deleted">group deleted</string> + <string name="group_member_status_unknown">unknown status</string> <string name="group_member_status_invited">invited</string> + <string name="group_member_status_pending_approval">pending approval</string> + <string name="group_member_status_pending_approval_short">pending</string> <string name="group_member_status_introduced">connecting (introduced)</string> <string name="group_member_status_intro_invitation">connecting (introduction invitation)</string> <string name="group_member_status_accepted">connecting (accepted)</string> @@ -1202,6 +1668,9 @@ <string name="group_member_status_creator">creator</string> <string name="group_member_status_connecting">connecting</string> + <string name="group_member_status_unknown_short">unknown</string> + + <string name="past_member_vName">Past member %1$s</string> <!-- AddGroupMembersView.kt --> <string name="no_contacts_to_add">No contacts to add</string> @@ -1209,6 +1678,7 @@ <string name="initial_member_role">Initial role</string> <string name="icon_descr_expand_role">Expand role selection</string> <string name="invite_to_group_button">Invite to group</string> + <string name="invite_to_chat_button">Invite to chat</string> <string name="skip_inviting_button">Skip inviting members</string> <string name="select_contacts">Select contacts</string> <string name="icon_descr_contact_checked">Contact checked</string> @@ -1220,13 +1690,20 @@ <!-- GroupChatInfoView.kt --> <string name="button_add_members">Invite members</string> + <string name="button_add_team_members">Add team members</string> + <string name="button_add_friends">Add friends</string> <string name="group_info_section_title_num_members">%1$s MEMBERS</string> <string name="group_info_member_you">you: %1$s</string> <string name="button_delete_group">Delete group</string> + <string name="button_delete_chat">Delete chat</string> <string name="delete_group_question">Delete group?</string> + <string name="delete_chat_question">Delete chat?</string> <string name="delete_group_for_all_members_cannot_undo_warning">Group will be deleted for all members - this cannot be undone!</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">Chat will be deleted for all members - this cannot be undone!</string> <string name="delete_group_for_self_cannot_undo_warning">Group will be deleted for you - this cannot be undone!</string> + <string name="delete_chat_for_self_cannot_undo_warning">Chat will be deleted for you - this cannot be undone!</string> <string name="button_leave_group">Leave group</string> + <string name="button_leave_chat">Leave chat</string> <string name="button_edit_group_profile">Edit group profile</string> <string name="button_add_welcome_message">Add welcome message</string> <string name="button_welcome_message">Welcome message</string> @@ -1243,6 +1720,7 @@ <string name="error_creating_member_contact">Error creating member contact</string> <string name="error_sending_message_contact_invitation">Error sending invitation</string> <string name="only_group_owners_can_change_prefs">Only group owners can change group preferences.</string> + <string name="only_chat_owners_can_change_prefs">Only chat owners can change preferences.</string> <string name="address_section_title">Address</string> <string name="share_address">Share address</string> <string name="you_can_share_this_address_with_your_contacts">You can share this address with your contacts to let them connect with %s.</string> @@ -1250,20 +1728,28 @@ <string name="send_receipts_disabled">disabled</string> <string name="send_receipts_disabled_alert_title">Receipts are disabled</string> <string name="send_receipts_disabled_alert_msg">This group has over %1$d members, delivery receipts are not sent.</string> + <string name="action_button_add_members">Invite</string> <!-- Chat / Chat item info --> <string name="section_title_for_console">FOR CONSOLE</string> <string name="info_row_local_name">Local name</string> <string name="info_row_database_id">Database ID</string> + <string name="info_row_debug_delivery">Debug delivery</string> <string name="info_row_updated_at">Record updated at</string> + <string name="info_row_message_status">Message status</string> + <string name="info_row_file_status">File status</string> <string name="info_row_sent_at">Sent at</string> + <string name="info_row_created_at">Created at</string> <string name="info_row_received_at">Received at</string> <string name="info_row_deleted_at">Deleted at</string> <string name="info_row_moderated_at">Moderated at</string> <string name="info_row_disappears_at">Disappears at</string> <string name="share_text_database_id">Database ID: %d</string> <string name="share_text_updated_at">Record updated at: %s</string> + <string name="share_text_message_status">Message status: %s</string> + <string name="share_text_file_status">File status: %s</string> <string name="share_text_sent_at">Sent at: %s</string> + <string name="share_text_created_at">Created at: %s</string> <string name="share_text_received_at">Received at: %s</string> <string name="share_text_deleted_at">Deleted at: %s</string> <string name="share_text_moderated_at">Moderated at: %s</string> @@ -1273,46 +1759,83 @@ <string name="current_version_timestamp">%s (current)</string> <string name="item_info_no_text">no text</string> <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="saved_message_title">Saved message</string> <!-- GroupMemberInfoView.kt --> <string name="button_remove_member_question">Remove member?</string> + <string name="button_remove_members_question">Remove members?</string> <string name="button_remove_member">Remove member</string> <string name="button_send_direct_message">Send direct message</string> <string name="member_will_be_removed_from_group_cannot_be_undone">Member will be removed from group - this cannot be undone!</string> + <string name="members_will_be_removed_from_group_cannot_be_undone">Members will be removed from group - this cannot be undone!</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">Member will be removed from chat - this cannot be undone!</string> + <string name="members_will_be_removed_from_chat_cannot_be_undone">Members will be removed from chat - this cannot be undone!</string> <string name="remove_member_confirmation">Remove</string> <string name="remove_member_button">Remove member</string> <string name="block_member_question">Block member?</string> <string name="block_member_button">Block member</string> <string name="block_member_confirmation">Block</string> + <string name="block_for_all_question">Block member for all?</string> + <string name="block_members_for_all_question">Block members for all?</string> + <string name="block_for_all">Block for all</string> <string name="block_member_desc">All new messages from %s will be hidden!</string> + <string name="block_members_desc">All new messages from these members will be hidden!</string> <string name="unblock_member_question">Unblock member?</string> <string name="unblock_member_button">Unblock member</string> <string name="unblock_member_confirmation">Unblock</string> + <string name="unblock_for_all_question">Unblock member for all?</string> + <string name="unblock_members_for_all_question">Unblock members for all?</string> + <string name="unblock_for_all">Unblock for all</string> <string name="unblock_member_desc">Messages from %s will be shown!</string> + <string name="unblock_members_desc">Messages from these members will be shown!</string> + <string name="member_blocked_by_admin">Blocked by admin</string> + <string name="member_info_member_blocked">blocked</string> + <string name="member_info_member_disabled">disabled</string> + <string name="member_info_member_inactive">inactive</string> <string name="member_info_section_title_member">MEMBER</string> <string name="role_in_group">Role</string> <string name="change_role">Change role</string> <string name="change_verb">Change</string> <string name="switch_verb">Switch</string> <string name="change_member_role_question">Change group role?</string> - <string name="member_role_will_be_changed_with_notification">The role will be changed to \"%s\". Everyone in the group will be notified.</string> - <string name="member_role_will_be_changed_with_invitation">The role will be changed to \"%s\". The member will receive a new invitation.</string> + <string name="member_role_will_be_changed_with_notification">The role will be changed to "%s". Everyone in the group will be notified.</string> + <string name="member_role_will_be_changed_with_notification_chat">The role will be changed to "%s". Everyone in the chat will be notified.</string> + <string name="member_role_will_be_changed_with_invitation">The role will be changed to "%s". The member will receive a new invitation.</string> <string name="connect_via_member_address_alert_title">Connect directly?</string> <string name="connect_via_member_address_alert_desc">Сonnection request will be sent to this group member.</string> <string name="error_removing_member">Error removing member</string> <string name="error_changing_role">Error changing role</string> + <string name="error_blocking_member_for_all">Error blocking member for all</string> <string name="info_row_group">Group</string> + <string name="info_row_chat">Chat</string> <string name="info_row_connection">Connection</string> <string name="conn_level_desc_direct">direct</string> <string name="conn_level_desc_indirect">indirect (%1$s)</string> + <string name="message_queue_info">Message queue info</string> + <string name="message_queue_info_none">none</string> + <string name="message_queue_info_server_info">server queue info: %1$s\n\nlast received msg: %2$s</string> + + <string name="cant_call_contact_alert_title">Can\'t call contact</string> + <string name="cant_call_contact_connecting_wait_alert_text">Connecting to contact, please wait or check later!</string> + <string name="cant_call_contact_deleted_alert_text">Contact is deleted.</string> + <string name="allow_calls_question">Allow calls?</string> + <string name="you_need_to_allow_calls">You need to allow your contact to call to be able to call them.</string> + <string name="calls_prohibited_alert_title">Calls prohibited!</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">Please ask your contact to enable calls.</string> + <string name="cant_call_member_alert_title">Can\'t call group member</string> + <string name="cant_call_member_send_message_alert_text">Send message to enable calls.</string> + <string name="cant_send_message_to_member_alert_title">Can\'t message group member</string> + <string name="connection_not_ready">Connection not ready.</string> <!-- GroupWelcomeView.kt --> <string name="group_welcome_title">Welcome message</string> <string name="save_welcome_message_question">Save welcome message?</string> + <string name="welcome_message_is_too_long">Welcome message is too long</string> <string name="save_and_update_group_profile">Save and update group profile</string> <string name="group_welcome_preview">Preview</string> <string name="enter_welcome_message">Enter welcome message…</string> + <string name="message_too_large">Message too large</string> <!-- ConnectionStats --> <string name="conn_stats_section_title_servers">SERVERS</string> @@ -1334,6 +1857,7 @@ <string name="group_display_name_field">Enter group name:</string> <string name="group_full_name_field">Group full name:</string> <string name="group_main_profile_sent">Your chat profile will be sent to group members</string> + <string name="chat_main_profile_sent">Your chat profile will be sent to chat members</string> <string name="create_group_button">Create group</string> <!-- GroupProfileView.kt --> @@ -1341,17 +1865,70 @@ <string name="save_group_profile">Save group profile</string> <string name="error_saving_group_profile">Error saving group profile</string> + <!-- NetworkAndServers.kt --> + <string name="network_preset_servers_title">Preset servers</string> + <string name="operator_review_conditions">Review conditions</string> + <string name="operator_conditions_accepted">Accepted conditions</string> + <string name="operator_conditions_accepted_for_enabled_operators_on">Conditions will be automatically accepted for enabled operators on: %s.</string> + <string name="your_servers">Your servers</string> + <string name="operators_conditions_accepted_for"><![CDATA[Conditions are accepted for the operator(s): <b>%s</b>.]]></string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[Conditions will be accepted for the operator(s): <b>%s</b>.]]></string> + + <!-- OperatorView.kt --> + <string name="operator">Operator</string> + <string name="operator_servers_title">%s servers</string> + <string name="operator_info_title">Network operator</string> + <string name="operator_website">Website</string> + <string name="operator_conditions_accepted_on">Conditions accepted on: %s.</string> + <string name="operator_conditions_will_be_accepted_on">Conditions will be accepted on: %s.</string> + <string name="operator_use_operator_toggle_description">Use servers</string> + <string name="use_servers_of_operator_x">Use %s</string> + <string name="operator_conditions_failed_to_load">Current conditions text couldn\'t be loaded, you can review conditions via this link:</string> + <string name="operator_conditions_accepted_for_some"><![CDATA[Conditions are already accepted for following operator(s): <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[The same conditions will apply to operator <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[The same conditions will apply to operator(s): <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_applied"><![CDATA[These conditions will also apply for: <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Conditions will be accepted for operator(s): <b>%s</b>.]]></string> + <string name="operators_conditions_will_also_apply"><![CDATA[These conditions will also apply for: <b>%s</b>.]]></string> + <string name="view_conditions">View conditions</string> + <string name="accept_conditions">Accept conditions</string> + <string name="operator_conditions_of_use">Conditions of use</string> + <string name="operator_updated_conditions">Updated conditions</string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[To use the servers of <b>%s</b>, accept conditions of use.]]></string> + <string name="operator_use_for_messages">Use for messages</string> + <string name="operator_use_for_messages_receiving">To receive</string> + <string name="operator_use_for_messages_private_routing">For private routing</string> + <string name="operator_added_message_servers">Added message servers</string> + <string name="operator_use_for_files">Use for files</string> + <string name="operator_use_for_sending">To send</string> + <string name="xftp_servers_per_user">The servers for new files of your current chat profile</string> + <string name="operator_added_xftp_servers">Added media & file servers</string> + <string name="operator_open_conditions">Open conditions</string> + <string name="operator_open_changes">Open changes</string> + + <!-- ProtocolServerView.kt --> + <string name="error_updating_server_title">Error updating server</string> + <string name="error_server_protocol_changed">Server protocol changed.</string> + <string name="error_server_operator_changed">Server operator changed.</string> + + <!-- NewServerView.kt --> + <string name="operator_server_alert_title">Operator server</string> + <string name="server_added_to_operator__name">Server added to operator %s.</string> + <string name="error_adding_server">Error adding server</string> + <!-- AdvancedNetworkSettings.kt --> + <string name="network_option_tcp_connection">TCP connection</string> <string name="network_options_reset_to_defaults">Reset to defaults</string> <string name="network_option_seconds_label">sec</string> <string name="network_option_tcp_connection_timeout">TCP connection timeout</string> <string name="network_option_protocol_timeout">Protocol timeout</string> <string name="network_option_protocol_timeout_per_kb">Protocol timeout per KB</string> + <string name="network_option_rcv_concurrency">Receiving concurrency</string> <string name="network_option_ping_interval">PING interval</string> <string name="network_option_ping_count">PING count</string> <string name="network_option_enable_tcp_keep_alive">Enable TCP keep-alive</string> - <string name="network_options_revert">Revert</string> <string name="network_options_save">Save</string> + <string name="network_options_save_and_reconnect">Save and reconnect</string> <string name="update_network_settings_question">Update network settings?</string> <string name="updating_settings_will_reconnect_client_to_all_servers">Updating settings will re-connect the client to all servers.</string> <string name="update_network_settings_confirmation">Update</string> @@ -1369,9 +1946,6 @@ <string name="user_unmute">Unmute</string> <string name="enter_password_to_show">Enter password in search</string> <string name="tap_to_activate_profile">Tap to activate profile.</string> - <string name="cant_delete_user_profile">Can\'t delete user profile!</string> - <string name="should_be_at_least_one_visible_profile">There should be at least one visible user profile.</string> - <string name="should_be_at_least_one_profile">There should be at least one user profile.</string> <string name="make_profile_private">Make profile private!</string> <string name="you_can_hide_or_mute_user_profile">You can hide or mute a user profile - hold it for the menu.</string> <string name="dont_show_again">Don\'t show again</string> @@ -1392,23 +1966,30 @@ <string name="incognito_info_share">When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</string> <!-- Default themes --> + <string name="color_mode_system">System</string> + <string name="color_mode_light">Light</string> + <string name="color_mode_dark">Dark</string> <string name="theme_system">System</string> <string name="theme_light">Light</string> <string name="theme_dark">Dark</string> <string name="theme_simplex">SimpleX</string> + <string name="theme_black">Black</string> <!-- Languages --> <string name="language_system">System</string> <!-- Appearance.kt --> <string name="theme">Theme</string> + <string name="color_mode">Color mode</string> <string name="dark_theme">Dark theme</string> - <string name="save_color">Save color</string> + <string name="dark_mode_colors">Dark mode colors</string> <string name="import_theme">Import theme</string> <string name="import_theme_error">Import theme error</string> <string name="import_theme_error_desc">Make sure the file has correct YAML syntax. Export theme to have an example of the theme file structure.</string> <string name="export_theme">Export theme</string> <string name="reset_color">Reset colors</string> + <string name="reset_single_color">Reset color</string> + <string name="theme_destination_app_theme">App theme</string> <string name="color_primary">Accent</string> <string name="color_primary_variant">Additional accent</string> <string name="color_secondary">Secondary</string> @@ -1416,8 +1997,37 @@ <string name="color_background">Background</string> <string name="color_surface">Menus & alerts</string> <string name="color_title">Title</string> + <string name="color_primary_variant2">Additional accent 2</string> <string name="color_sent_message">Sent message</string> + <string name="color_sent_quote">Sent reply</string> <string name="color_received_message">Received message</string> + <string name="color_received_quote">Received reply</string> + <string name="color_wallpaper_background">Wallpaper background</string> + <string name="color_wallpaper_tint">Wallpaper accent</string> + <string name="theme_remove_image">Remove image</string> + <string name="appearance_font_size">Font size</string> + <string name="appearance_zoom">Zoom</string> + <string name="appearance_app_toolbars">App toolbars</string> + <string name="appearance_in_app_bars_alpha">Transparency</string> + <string name="appearance_bars_blur_radius">Blur</string> + <string name="system_mode_toast">System mode</string> + + <!-- Wallpapers --> + <string name="wallpaper_preview_hello_alice">Good afternoon!</string> + <string name="wallpaper_preview_hello_bob">Good morning!</string> + <string name="wallpaper_scale">Scale</string> + <string name="wallpaper_scale_repeat">Repeat</string> + <string name="wallpaper_scale_fill">Fill</string> + <string name="wallpaper_scale_fit">Fit</string> + <string name="wallpaper_advanced_settings">Advanced settings</string> + <string name="chat_theme_reset_to_app_theme">Reset to app theme</string> + <string name="chat_theme_reset_to_user_theme">Reset to user theme</string> + <string name="chat_theme_set_default_theme">Set default theme</string> + <string name="chat_theme_apply_to_mode">Apply to</string> + <string name="chat_theme_apply_to_all_modes">All color modes</string> + <string name="chat_theme_apply_to_light_mode">Light mode</string> + <string name="chat_theme_apply_to_dark_mode">Dark mode</string> + <!-- Preferences.kt --> <string name="chat_preferences_you_allow">You allow</string> @@ -1439,6 +2049,8 @@ <string name="message_reactions">Message reactions</string> <string name="voice_messages">Voice messages</string> <string name="files_and_media">Files and media</string> + <string name="simplex_links">SimpleX links</string> + <string name="recent_history">Visible history</string> <string name="audio_video_calls">Audio/video calls</string> <string name="available_in_v51">\nAvailable in v5.1</string> <string name="feature_enabled">enabled</string> @@ -1451,8 +2063,8 @@ <string name="allow_your_contacts_to_send_disappearing_messages">Allow your contacts to send disappearing messages.</string> <string name="allow_disappearing_messages_only_if">Allow disappearing messages only if your contact allows them.</string> <string name="prohibit_sending_disappearing_messages">Prohibit sending disappearing messages.</string> - <string name="allow_your_contacts_irreversibly_delete">Allow your contacts to irreversibly delete sent messages.</string> - <string name="allow_irreversible_message_deletion_only_if">Allow irreversible message deletion only if your contact allows it to you.</string> + <string name="allow_your_contacts_irreversibly_delete">Allow your contacts to irreversibly delete sent messages. (24 hours)</string> + <string name="allow_irreversible_message_deletion_only_if">Allow irreversible message deletion only if your contact allows it to you. (24 hours)</string> <string name="contacts_can_mark_messages_for_deletion">Contacts can mark messages for deletion; you will be able to view them.</string> <string name="allow_your_contacts_to_send_voice_messages">Allow your contacts to send voice messages.</string> <string name="allow_voice_messages_only_if">Allow voice messages only if your contact allows them.</string> @@ -1467,9 +2079,9 @@ <string name="only_you_can_send_disappearing">Only you can send disappearing messages.</string> <string name="only_your_contact_can_send_disappearing">Only your contact can send disappearing messages.</string> <string name="disappearing_prohibited_in_this_chat">Disappearing messages are prohibited in this chat.</string> - <string name="both_you_and_your_contacts_can_delete">Both you and your contact can irreversibly delete sent messages.</string> - <string name="only_you_can_delete_messages">Only you can irreversibly delete messages (your contact can mark them for deletion).</string> - <string name="only_your_contact_can_delete">Only your contact can irreversibly delete messages (you can mark them for deletion).</string> + <string name="both_you_and_your_contacts_can_delete">Both you and your contact can irreversibly delete sent messages. (24 hours)</string> + <string name="only_you_can_delete_messages">Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)</string> + <string name="only_your_contact_can_delete">Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)</string> <string name="message_deletion_prohibited">Irreversible message deletion is prohibited in this chat.</string> <string name="both_you_and_your_contact_can_send_voice">Both you and your contact can send voice messages.</string> <string name="only_you_can_send_voice">Only you can send voice messages.</string> @@ -1487,7 +2099,7 @@ <string name="prohibit_sending_disappearing">Prohibit sending disappearing messages.</string> <string name="allow_direct_messages">Allow sending direct messages to members.</string> <string name="prohibit_direct_messages">Prohibit sending direct messages to members.</string> - <string name="allow_to_delete_messages">Allow to irreversibly delete sent messages.</string> + <string name="allow_to_delete_messages">Allow to irreversibly delete sent messages. (24 hours)</string> <string name="prohibit_message_deletion">Prohibit irreversible message deletion.</string> <string name="allow_to_send_voice">Allow to send voice messages.</string> <string name="prohibit_sending_voice">Prohibit sending voice messages.</string> @@ -1495,18 +2107,32 @@ <string name="prohibit_message_reactions_group">Prohibit messages reactions.</string> <string name="allow_to_send_files">Allow to send files and media.</string> <string name="prohibit_sending_files">Prohibit sending files and media.</string> - <string name="group_members_can_send_disappearing">Group members can send disappearing messages.</string> - <string name="disappearing_messages_are_prohibited">Disappearing messages are prohibited in this group.</string> - <string name="group_members_can_send_dms">Group members can send direct messages.</string> - <string name="direct_messages_are_prohibited_in_chat">Direct messages between members are prohibited in this group.</string> - <string name="group_members_can_delete">Group members can irreversibly delete sent messages.</string> - <string name="message_deletion_prohibited_in_chat">Irreversible message deletion is prohibited in this group.</string> - <string name="group_members_can_send_voice">Group members can send voice messages.</string> - <string name="voice_messages_are_prohibited">Voice messages are prohibited in this group.</string> - <string name="group_members_can_add_message_reactions">Group members can add message reactions.</string> - <string name="message_reactions_are_prohibited">Message reactions are prohibited in this group.</string> - <string name="group_members_can_send_files">Group members can send files and media.</string> - <string name="files_are_prohibited_in_group">Files and media are prohibited in this group.</string> + <string name="allow_to_send_simplex_links">Allow to send SimpleX links.</string> + <string name="prohibit_sending_simplex_links">Prohibit sending SimpleX links</string> + <string name="enable_sending_recent_history">Send up to 100 last messages to new members.</string> + <string name="disable_sending_recent_history">Do not send history to new members.</string> + <string name="enable_sending_member_reports">Allow to report messsages to moderators.</string> + <string name="disable_sending_member_reports">Prohibit reporting messages to moderators.</string> + <string name="group_members_can_send_disappearing">Members can send disappearing messages.</string> + <string name="disappearing_messages_are_prohibited">Disappearing messages are prohibited.</string> + <string name="group_members_can_send_dms">Members can send direct messages.</string> + <string name="direct_messages_are_prohibited">Direct messages between members are prohibited.</string> + <string name="direct_messages_are_prohibited_in_group">Direct messages between members are prohibited in this group.</string> + <string name="direct_messages_are_prohibited_in_chat">Direct messages between members are prohibited in this chat.</string> + <string name="group_members_can_delete">Members can irreversibly delete sent messages. (24 hours)</string> + <string name="message_deletion_prohibited_in_chat">Irreversible message deletion is prohibited.</string> + <string name="group_members_can_send_voice">Members can send voice messages.</string> + <string name="voice_messages_are_prohibited">Voice messages are prohibited.</string> + <string name="group_members_can_add_message_reactions">Members can add message reactions.</string> + <string name="message_reactions_are_prohibited">Message reactions are prohibited.</string> + <string name="group_members_can_send_files">Members can send files and media.</string> + <string name="files_are_prohibited_in_group">Files and media are prohibited.</string> + <string name="group_members_can_send_simplex_links">Members can send SimpleX links.</string> + <string name="simplex_links_are_prohibited_in_group">SimpleX links are prohibited.</string> + <string name="recent_history_is_sent_to_new_members">Up to 100 last messages are sent to new members.</string> + <string name="recent_history_is_not_sent_to_new_members">History is not sent to new members.</string> + <string name="group_members_can_send_reports">Members can report messsages to moderators.</string> + <string name="member_reports_are_prohibited">Reporting messages is prohibited in this group.</string> <string name="delete_after">Delete after</string> <string name="ttl_sec">%d sec</string> <string name="ttl_s">%ds</string> @@ -1527,6 +2153,11 @@ <string name="feature_offered_item">offered %s</string> <string name="feature_offered_item_with_param">offered %s: %2s</string> <string name="feature_cancelled_item">cancelled %s</string> + <string name="feature_roles_all_members">all members</string> + <string name="feature_roles_moderators">moderators</string> + <string name="feature_roles_admins">admins</string> + <string name="feature_roles_owners">owners</string> + <string name="feature_enabled_for">Enabled for</string> <!-- WhatsNewView.kt --> <string name="whats_new">What\'s new</string> @@ -1571,7 +2202,7 @@ <string name="v4_6_audio_video_calls">Audio and video calls</string> <string name="v4_6_audio_video_calls_descr">Support bluetooth and other improvements.</string> <string name="v4_6_group_moderation">Group moderation</string> - <string name="v4_6_group_moderation_descr">Now admins can:\n- delete members\' messages.\n- disable members (\"observer\" role)</string> + <string name="v4_6_group_moderation_descr">Now admins can:\n- delete members\' messages.\n- disable members ("observer" role)</string> <string name="v4_6_group_welcome_message">Group welcome message</string> <string name="v4_6_group_welcome_message_descr">Set the message shown to new members!</string> <string name="v4_6_reduced_battery_usage">Further reduced battery usage</string> @@ -1623,6 +2254,88 @@ <string name="v5_4_block_group_members">Block group members</string> <string name="v5_4_block_group_members_descr">To hide unwanted messages.</string> <string name="v5_4_more_things_descr">- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!</string> + <string name="v5_5_private_notes">Private notes</string> + <string name="v5_5_private_notes_descr">With encrypted files and media.</string> + <string name="v5_5_simpler_connect_ui">Paste link to connect!</string> + <string name="v5_5_simpler_connect_ui_descr">Search bar accepts invitation links.</string> + <string name="v5_5_join_group_conversation">Join group conversations</string> + <string name="v5_5_join_group_conversation_descr">Recent history and improved directory bot.</string> + <string name="v5_5_message_delivery">Improved message delivery</string> + <string name="v5_5_message_delivery_descr">With reduced battery usage.</string> + <string name="v5_5_new_interface_languages">Hungarian and Turkish UI</string> + <string name="v5_6_quantum_resistant_encryption">Quantum resistant encryption</string> + <string name="v5_6_quantum_resistant_encryption_descr">Enable in direct chats (BETA)!</string> + <string name="v5_6_app_data_migration">App data migration</string> + <string name="v5_6_app_data_migration_descr">Migrate to another device via QR code.</string> + <string name="v5_6_picture_in_picture_calls">Picture-in-picture calls</string> + <string name="v5_6_picture_in_picture_calls_descr">Use the app while in the call.</string> + <string name="v5_6_safer_groups">Safer groups</string> + <string name="v5_6_safer_groups_descr">Admins can block a member for all.</string> + <string name="v5_7_quantum_resistant_encryption_descr">Will be enabled in direct chats!</string> + <string name="v5_7_forward">Forward and save messages</string> + <string name="v5_7_forward_descr">Message source remains private.</string> + <string name="v5_7_call_sounds">In-call sounds</string> + <string name="v5_7_call_sounds_descr">When connecting audio and video calls.</string> + <string name="v5_7_shape_profile_images">Shape profile images</string> + <string name="v5_7_shape_profile_images_descr">Square, circle, or anything in between.</string> + <string name="v5_7_network">Network management</string> + <string name="v5_7_network_descr">More reliable network connection.</string> + <string name="v5_7_new_interface_languages">Lithuanian UI</string> + <string name="v5_8_private_routing">Private message routing 🚀</string> + <string name="v5_8_private_routing_descr">Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings.</string> + <string name="v5_8_chat_themes">New chat themes</string> + <string name="v5_8_chat_themes_descr">Make your chats look different!</string> + <string name="v5_8_safe_files">Safely receive files</string> + <string name="v5_8_safe_files_descr">Confirm files from unknown servers.</string> + <string name="v5_8_message_delivery">Improved message delivery</string> + <string name="v5_8_message_delivery_descr">With reduced battery usage.</string> + <string name="v5_8_persian_ui">Persian UI</string> + <string name="v6_0_new_chat_experience">New chat experience 🎉</string> + <string name="v6_0_new_media_options">New media options</string> + <string name="v6_0_private_routing_descr">It protects your IP address and connections.</string> + <string name="v6_0_your_contacts_descr">Archive contacts to chat later.</string> + <string name="v6_0_reachable_chat_toolbar">Reachable chat toolbar</string> + <string name="v6_0_reachable_chat_toolbar_descr">Use the app with one hand.</string> + <string name="v6_0_connect_faster_descr">Connect to your friends faster.</string> + <string name="v6_0_delete_many_messages_descr">Delete up to 20 messages at once.</string> + <string name="v6_0_chat_list_media">Play from the chat list.</string> + <string name="v6_0_privacy_blur">Blur for better privacy.</string> + <string name="v6_0_increase_font_size">Increase font size.</string> + <string name="v6_0_upgrade_app">Upgrade app automatically</string> + <string name="v6_0_upgrade_app_descr">Download new versions from GitHub.</string> + <string name="v6_0_connection_servers_status">Control your network</string> + <string name="v6_0_connection_servers_status_descr">Connection and servers status.</string> + <string name="v6_1_better_security">Better security ✅</string> + <string name="v6_1_better_security_descr">SimpleX protocols reviewed by Trail of Bits.</string> + <string name="v6_1_better_calls">Better calls</string> + <string name="v6_1_better_calls_descr">Switch audio and video during the call.</string> + <string name="v6_1_better_user_experience">Better user experience</string> + <string name="v6_1_switch_chat_profile_descr">Switch chat profile for 1-time invitations.</string> + <string name="v6_1_customizable_message_descr">Customizable message shape.</string> + <string name="v6_1_message_dates_descr">Better message dates.</string> + <string name="v6_1_forward_many_messages_descr">Forward up to 20 messages at once.</string> + <string name="v6_1_delete_many_messages_descr">Delete or moderate up to 200 messages.</string> + <string name="v6_2_network_decentralization">Network decentralization</string> + <string name="v6_2_network_decentralization_descr">The second preset operator in the app!</string> + <string name="v6_2_network_decentralization_enable_flux">Enable Flux in Network & servers settings for better metadata privacy.</string> + <string name="v6_2_network_decentralization_enable_flux_reason">for better metadata privacy.</string> + <string name="v6_2_improved_chat_navigation">Improved chat navigation</string> + <string name="v6_2_improved_chat_navigation_descr">- Open chat on the first unread message.\n- Jump to quoted messages.</string> + <string name="v6_2_business_chats">Business chats</string> + <string name="v6_2_business_chats_descr">Privacy for your customers.</string> + <string name="v6_3_mentions">Mention members 👋</string> + <string name="v6_3_mentions_descr">Get notified when mentioned.</string> + <string name="v6_3_reports">Send private reports</string> + <string name="v6_3_reports_descr">Help admins moderating their groups.</string> + <string name="v6_3_organize_chat_lists">Organize chats into lists</string> + <string name="v6_3_organize_chat_lists_descr">Don\'t miss important messages.</string> + <string name="v6_3_better_privacy_and_security">Better privacy and security</string> + <string name="v6_3_private_media_file_names">Private media file names.</string> + <string name="v6_3_set_message_expiration_in_chats">Set message expiration in chats.</string> + <string name="v6_3_better_groups_performance">Better groups performance</string> + <string name="v6_3_faster_sending_messages">Faster sending messages.</string> + <string name="v6_3_faster_deletion_of_groups">Faster deletion of groups.</string> + <string name="view_updated_conditions">View updated conditions</string> <!-- CustomTimePicker --> <string name="custom_time_unit_seconds">seconds</string> @@ -1666,6 +2379,13 @@ <string name="disconnect_remote_host">Disconnect</string> <string name="disconnect_remote_hosts">Disconnect mobiles</string> <string name="remote_host_was_disconnected_toast"><![CDATA[Mobile <b>%s</b> was disconnected]]></string> + <string name="remote_host_was_disconnected_title">Connection stopped</string> + <string name="remote_ctrl_was_disconnected_title">Connection stopped</string> + <string name="remote_host_disconnected_from"><![CDATA[Disconnected from mobile <b>%s</b> with the reason: %s]]></string> + <string name="remote_ctrl_disconnected_with_reason">Disconnected with the reason: %s</string> + <string name="remote_ctrl_connection_stopped_desc">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.</string> + <string name="remote_ctrl_connection_stopped_identity_desc">This link was used with another mobile device, please create a new link on the desktop.</string> + <string name="copy_error">Copy error</string> <string name="disconnect_desktop_question">Disconnect desktop?</string> <string name="only_one_device_can_work_at_the_same_time">Only one device can work at the same time</string> <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Use from desktop</i> in mobile app and scan QR code.]]></string> @@ -1700,6 +2420,20 @@ <string name="random_port">Random</string> <string name="open_port_in_firewall_title">Open port in firewall</string> <string name="open_port_in_firewall_desc">To allow a mobile app to connect to the desktop, open this port in your firewall, if you have it enabled</string> + <string name="remote_host_error_missing"><![CDATA[Mobile <b>%s</b> is missing]]></string> + <string name="remote_host_error_inactive"><![CDATA[Mobile <b>%s</b> is inactive]]></string> + <string name="remote_host_error_busy"><![CDATA[Mobile <b>%s</b> is busy]]></string> + <string name="remote_host_error_timeout"><![CDATA[Timeout reached while connecting to the mobile <b>%s</b>]]></string> + <string name="remote_host_error_bad_state"><![CDATA[Connection to the mobile <b>%s</b> is in a bad state]]></string> + <string name="remote_host_error_bad_version"><![CDATA[Mobile <b>%s</b> has an unsupported version. Please, make sure you use the same version on both devices]]></string> + <string name="remote_host_error_disconnected"><![CDATA[Mobile <b>%s</b> was disconnected]]></string> + <string name="remote_ctrl_error_inactive">Desktop is inactive</string> + <string name="remote_ctrl_error_bad_state">Connection to the desktop is in a bad state</string> + <string name="remote_ctrl_error_busy">Desktop is busy</string> + <string name="remote_ctrl_error_timeout">Timeout reached while connecting to the desktop</string> + <string name="remote_ctrl_error_disconnected">Desktop was disconnected</string> + <string name="remote_ctrl_error_bad_invitation">Desktop has wrong invitation code</string> + <string name="remote_ctrl_error_bad_version">Desktop has an unsupported version. Please, make sure you use the same version on both devices</string> <!-- Under development --> <string name="in_developing_title">Coming soon!</string> @@ -1708,20 +2442,186 @@ <!-- Connection plan --> <string name="connect_plan_connect_to_yourself">Connect to yourself?</string> <string name="connect_plan_this_is_your_own_one_time_link">This is your own one-time link!</string> - <string name="connect_plan_you_are_already_connecting_to_vName">You are already connecting to %1$s.</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[You are already connecting to <b>%1$s</b>.]]></string> <string name="connect_plan_already_connecting">Already connecting!</string> <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">You are already connecting via this one-time link!</string> <string name="connect_plan_this_is_your_own_simplex_address">This is your own SimpleX address!</string> <string name="connect_plan_repeat_connection_request">Repeat connection request?</string> <string name="connect_plan_you_have_already_requested_connection_via_this_address">You have already requested connection via this address!</string> <string name="connect_plan_join_your_group">Join your group?</string> - <string name="connect_plan_this_is_your_link_for_group_vName">This is your link for group %1$s!</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[This is your link for group <b>%1$s</b>!]]></string> <string name="connect_plan_open_group">Open group</string> <string name="connect_plan_repeat_join_request">Repeat join request?</string> <string name="connect_plan_group_already_exists">Group already exists!</string> - <string name="connect_plan_you_are_already_joining_the_group_vName">You are already joining the group %1$s.</string> + <string name="connect_plan_chat_already_exists">Chat already exists!</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[You are already joining the group <b>%1$s</b>.]]></string> <string name="connect_plan_already_joining_the_group">Already joining the group!</string> <string name="connect_plan_you_are_already_joining_the_group_via_this_link">You are already joining the group via this link.</string> - <string name="connect_plan_you_are_already_in_group_vName">You are already in group %1$s.</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[You are already in group <b>%1$s</b>.]]></string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[You are already connected with <b>%1$s</b>.]]></string> <string name="connect_plan_connect_via_link">Connect via link?</string> + + <!-- Errors --> + <string name="agent_critical_error_title">Critical error</string> + <string name="agent_critical_error_desc">Please report it to the developers: \n%s\n\nIt is recommended to restart the app.</string> + <string name="agent_internal_error_title">Internal error</string> + <string name="agent_internal_error_desc">Please report it to the developers: \n%s</string> + <string name="restart_chat_button">Restart chat</string> + + <!-- MigrateToDevice.kt --> + <string name="migrate_to_device_title">Migrate here</string> + <string name="or_paste_archive_link">Or paste archive link</string> + <string name="paste_archive_link">Paste archive link</string> + <string name="invalid_file_link">Invalid link</string> + <string name="chat_archive">Or import archive file</string> + <string name="migrate_to_device_migrating">Migrating</string> + <string name="migrate_to_device_database_init">Preparing download</string> + <string name="migrate_to_device_downloading_details">Downloading link details</string> + <string name="migrate_to_device_downloading_archive">Downloading archive</string> + <string name="migrate_to_device_bytes_downloaded">%s downloaded</string> + <string name="migrate_to_device_download_failed">Download failed</string> + <string name="migrate_to_device_repeat_download">Repeat download</string> + <string name="migrate_to_device_try_again">You can give another try.</string> + <string name="migrate_to_device_importing_archive">Importing archive</string> + <string name="migrate_to_device_import_failed">Import failed</string> + <string name="migrate_to_device_repeat_import">Repeat import</string> + <string name="migrate_to_device_enter_passphrase">Enter passphrase</string> + <string name="migrate_to_device_file_delete_or_link_invalid">File was deleted or link is invalid</string> + <string name="migrate_to_device_error_downloading_archive">Error downloading the archive</string> + <string name="migrate_to_device_chat_migrated">Chat migrated!</string> + <string name="migrate_to_device_finalize_migration">Finalize migration on another device.</string> + <string name="migrate_to_device_confirm_network_settings">Confirm network settings</string> + <string name="migrate_to_device_confirm_network_settings_footer">Please confirm that network settings are correct for this device.</string> + <string name="migrate_to_device_apply_onion">Apply</string> + + <!-- MigrateFromDevice.kt --> + <string name="migrate_from_device_title">Migrate device</string> + <string name="migrate_from_device_to_another_device">Migrate to another device</string> + <string name="migrate_from_device_error_saving_settings">Error saving settings</string> + <string name="migrate_from_device_exported_file_doesnt_exist">Exported file doesn\'t exist</string> + <string name="migrate_from_device_error_exporting_archive">Error exporting chat database</string> + <string name="migrate_from_device_database_init">Preparing upload</string> + <string name="migrate_from_device_error_uploading_archive">Error uploading the archive</string> + <string name="migrate_from_device_error_deleting_database">Error deleting database</string> + <string name="migrate_from_device_stopping_chat">Stopping chat</string> + <string name="migrate_from_device_chat_should_be_stopped">In order to continue, chat should be stopped.</string> + <string name="migrate_from_device_archive_and_upload">Archive and upload</string> + <string name="migrate_from_device_confirm_upload">Confirm upload</string> + <string name="migrate_from_device_all_data_will_be_uploaded">All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</string> + <string name="migrate_from_device_archiving_database">Archiving database</string> + <string name="migrate_from_device_bytes_uploaded">%s uploaded</string> + <string name="migrate_from_device_uploading_archive">Uploading archive</string> + <string name="migrate_from_device_upload_failed">Upload failed</string> + <string name="migrate_from_device_repeat_upload">Repeat upload</string> + <string name="migrate_from_device_try_again">You can give another try.</string> + <string name="migrate_from_device_creating_archive_link">Creating archive link</string> + <string name="migrate_from_device_cancel_migration">Cancel migration</string> + <string name="migrate_from_device_finalize_migration">Finalize migration</string> + <string name="migrate_from_device_remove_archive_question">Remove archive?</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">The uploaded database archive will be permanently removed from the servers.</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Choose <i>Migrate from another device</i> on the new device and scan QR code.]]></string> + <string name="migrate_from_device_or_share_this_file_link">Or securely share this file link</string> + <string name="migrate_from_device_delete_database_from_device">Delete database from this device</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Warning: starting chat on multiple devices is not supported and will cause message delivery failures</string> + <string name="migrate_from_device_start_chat">Start chat</string> + <string name="migrate_from_device_migration_complete">Migration complete</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[You <b>must not</b> use the same database on two devices.]]></string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Please note</b>: using the same database on two devices will break the decryption of messages from your connections, as a security protection.]]></string> + <string name="migrate_from_device_verify_database_passphrase">Verify database passphrase</string> + <string name="migrate_from_device_verify_passphrase">Verify passphrase</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Confirm that you remember database passphrase to migrate it.</string> + <string name="migrate_from_device_check_connection_and_try_again">Check your internet connection and try again</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Warning</b>: the archive will be deleted.]]></string> + <string name="migrate_from_device_error_verifying_passphrase">Error verifying passphrase:</string> + +<!-- NetworkObserver.kt --> + <string name="network_type_no_network_connection">No network connection</string> + <string name="network_type_cellular">Cellular</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="network_type_ethernet">Wired ethernet</string> + <string name="network_type_other">Other</string> + + <!-- ServersSummaryView.kt --> + <string name="servers_info">Servers info</string> + <string name="servers_info_files_tab">Files</string> + <string name="servers_info_missing">No info, try to reload</string> + <string name="servers_info_target">Showing info for</string> + <string name="all_users">All profiles</string> + <string name="current_user">Current profile</string> + <string name="servers_info_transport_sessions_section_header">Transport sessions</string> + <string name="servers_info_sessions_connected">Connected</string> + <string name="servers_info_sessions_connecting">Connecting</string> + <string name="servers_info_sessions_errors">Errors</string> + <string name="servers_info_statistics_section_header">Statistics</string> + <string name="servers_info_messages_sent">Messages sent</string> + <string name="servers_info_messages_received">Messages received</string> + <string name="servers_info_details">Details</string> + <string name="servers_info_private_data_disclaimer">Starting from %s.\nAll data is kept private on your device.</string> + <string name="servers_info_subscriptions_section_header">Message reception</string> + <string name="servers_info_subscriptions_connections_subscribed">Active connections</string> + <string name="servers_info_subscriptions_connections_pending">Pending</string> + <string name="servers_info_subscriptions_total">Total</string> + <string name="servers_info_connected_servers_section_header">Connected servers</string> + <string name="servers_info_previously_connected_servers_section_header">Previously connected servers</string> + <string name="servers_info_proxied_servers_section_header">Proxied servers</string> + <string name="servers_info_proxied_servers_section_footer">You are not connected to these servers. Private routing is used to deliver messages to them.</string> + <string name="servers_info_reconnect_servers_title">Reconnect servers?</string> + <string name="servers_info_reconnect_servers_message">Reconnect all connected servers to force message delivery. It uses additional traffic.</string> + <string name="servers_info_reconnect_server_title">Reconnect server?</string> + <string name="servers_info_reconnect_server_message">Reconnect server to force message delivery. It uses additional traffic.</string> + <string name="servers_info_reconnect_servers_error">Error reconnecting servers</string> + <string name="servers_info_reconnect_server_error">Error reconnecting server</string> + <string name="servers_info_modal_error_title">Error</string> + <string name="servers_info_reconnect_all_servers_button">Reconnect all servers</string> + <string name="servers_info_reset_stats">Reset all statistics</string> + <string name="servers_info_reset_stats_alert_title">Reset all statistics?</string> + <string name="servers_info_reset_stats_alert_message">Servers statistics will be reset - this cannot be undone!</string> + <string name="servers_info_reset_stats_alert_confirm">Reset</string> + <string name="servers_info_reset_stats_alert_error_title">Error resetting statistics</string> + <string name="servers_info_uploaded">Uploaded</string> + <string name="servers_info_downloaded">Downloaded</string> + <string name="servers_info_detailed_statistics">Detailed statistics</string> + <string name="servers_info_detailed_statistics_sent_messages_header">Sent messages</string> + <string name="servers_info_detailed_statistics_sent_messages_total">Sent total</string> + <string name="servers_info_detailed_statistics_received_messages_header">Received messages</string> + <string name="servers_info_detailed_statistics_received_total">Received total</string> + <string name="servers_info_detailed_statistics_receive_errors">Receive errors</string> + <string name="servers_info_starting_from">Starting from %s.</string> + <string name="smp_server">SMP server</string> + <string name="xftp_server">XFTP server</string> + <string name="reconnect">Reconnect</string> + <string name="attempts_label">attempts</string> + <string name="sent_directly">Sent directly</string> + <string name="sent_via_proxy">Sent via proxy</string> + <string name="proxied">Proxied</string> + <string name="send_errors">Send errors</string> + <string name="expired_label">expired</string> + <string name="other_label">other</string> + <string name="duplicates_label">duplicates</string> + <string name="decryption_errors">decryption errors</string> + <string name="other_errors">other errors</string> + <string name="acknowledged">Acknowledged</string> + <string name="acknowledgement_errors">Acknowledgement errors</string> + <string name="connections">Connections</string> + <string name="created">Created</string> + <string name="secured">Secured</string> + <string name="completed">Completed</string> + <string name="deleted">Deleted</string> + <string name="deletion_errors">Deletion errors</string> + <string name="subscribed">Subscribed</string> + <string name="subscription_results_ignored">Subscriptions ignored</string> + <string name="subscription_errors">Subscription errors</string> + <string name="uploaded_files">Uploaded files</string> + <string name="size">Size</string> + <string name="chunks_uploaded">Chunks uploaded</string> + <string name="upload_errors">Upload errors</string> + <string name="chunks_deleted">Chunks deleted</string> + <string name="chunks_downloaded">Chunks downloaded</string> + <string name="downloaded_files">Downloaded files</string> + <string name="download_errors">Download errors</string> + <string name="server_address">Server address</string> + <string name="open_server_settings_button">Open server settings</string> + + <!-- GroupMentions.kt --> + <string name="max_group_mentions_per_message_reached">You can mention up to %1$s members per message!</string> </resources> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml index c999d9b952..c37d4e6924 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml @@ -4,7 +4,7 @@ <string name="contact_wants_to_connect_via_call">%1$s иска да се свърже с вас чрез</string> <string name="send_disappearing_message_1_minute">1 минута</string> <string name="v4_3_improved_server_configuration_desc">Добави сървъри чрез сканиране на QR кодове.</string> - <string name="smp_servers_add">Добави сървър…</string> + <string name="smp_servers_add">Добави сървър</string> <string name="group_member_role_admin">админ</string> <string name="button_add_welcome_message">Добави съобщение при посрещане</string> <string name="v5_1_self_destruct_passcode_descr">Всички данни се изтриват при въвеждане.</string> @@ -73,7 +73,7 @@ <string name="allow_verb">Позволи</string> <string name="clear_chat_warning">Всички съобщения ще бъдат изтрити - това не може да бъде отменено! Съобщенията ще бъдат изтрити САМО за вас.</string> <string name="allow_calls_only_if">Позволи обаждания само ако вашият контакт ги разрешава.</string> - <string name="allow_irreversible_message_deletion_only_if">Позволи необратимо изтриване на съобщение само ако вашият контакт го рарешава.</string> + <string name="allow_irreversible_message_deletion_only_if">Позволи необратимо изтриване на съобщение само ако вашият контакт го рарешава. (24 часа)</string> <string name="allow_message_reactions_only_if">Позволи реакции на съобщения само ако вашият контакт ги разрешава.</string> <string name="allow_to_send_disappearing">Позволи изпращане на изчезващи съобщения.</string> <string name="allow_voice_messages_only_if">Позволи гласови съобщения само ако вашият контакт ги разрешава.</string> @@ -89,13 +89,12 @@ <string name="network_session_mode_user_description"><![CDATA[Ще се използва отделна TCP връзка (и идентификационни данни за SOCKS) <b>за всеки чат профил, който имате в приложението</b>.]]></string> <string name="icon_descr_audio_call">аудио разговор</string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Най-добро за батерията</b>. Ще получавате известия само когато приложението работи (БЕЗ фонова услуга).]]></string> - <string name="network_session_mode_entity_description">Ще се използва отделна TCP връзка (и идентификационни данни за SOCKS) <b>за всеки контакт и член на група</b>. -\n<b>Моля, обърнете внимание</b>: ако имате много връзки, консумацията на батерията и трафика може да бъде значително по-висока и някои връзки може да се провалят.</string> + <string name="network_session_mode_entity_description"><![CDATA[Ще се използва отделна TCP връзка (и идентификационни данни за SOCKS) <b>за всеки контакт и член на група</b>. \n<b>Моля, обърнете внимание</b>: ако имате много връзки, консумацията на батерията и трафика може да бъде значително по-висока и някои връзки може да се провалят.]]></string> <string name="icon_descr_asked_to_receive">Помолен да получи изображението</string> <string name="v4_6_audio_video_calls">Аудио и видео разговори</string> <string name="audio_call_no_encryption">аудио разговор (не е e2e криптиран)</string> <string name="la_auth_failed">Неуспешна идентификация</string> - <string name="both_you_and_your_contacts_can_delete">И вие, и вашият контакт можете да изтриете необратимо изпратените съобщения.</string> + <string name="both_you_and_your_contacts_can_delete">И вие, и вашият контакт можете да изтриете необратимо изпратените съобщения. (24 часа)</string> <string name="auth_unavailable">Идентификацията е недостъпна</string> <string name="both_you_and_your_contact_can_add_message_reactions">И вие, и вашият контакт можете да добавяте реакции към съобщението.</string> <string name="impossible_to_recover_passphrase"><![CDATA[<b>Моля, обърнете внимание</b>: НЯМА да можете да възстановите или промените паролата, ако я загубите.]]></string> @@ -119,8 +118,6 @@ <string name="back">Назад</string> <string name="cancel_verb">Отказ</string> <string name="icon_descr_cancel_live_message">Спри живото съобщение</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Добави нов контакт</b>: за да създадете своя еднократен QR код за вашия контакт.]]></string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Сканирай QR код</b>: за да се свържете с вашия контакт, който ви показва QR код.]]></string> <string name="use_camera_button">Камера</string> <string name="if_you_cant_meet_in_person">Ако не можете да се срещнете лично, покажете QR код във видеоразговора или споделете линка.</string> <string name="icon_descr_cancel_link_preview">спри визуализацията на линка</string> @@ -183,13 +180,12 @@ <string name="change_database_passphrase_question">Промяна на паролата на базата данни\?</string> <string name="rcv_group_event_changed_member_role">променена ролята от %s на %s</string> <string name="invite_prohibited">Не може да покани контакта!</string> - <string name="rcv_conn_event_switch_queue_phase_completed">променен е адреса за вас</string> + <string name="rcv_conn_event_switch_queue_phase_completed">адреса за изпращане е променен</string> <string name="change_verb">Промени</string> <string name="change_member_role_question">Промяна на груповата роля\?</string> <string name="you_will_still_receive_calls_and_ntfs">Все още ще получавате обаждания и известия от заглушени профили, когато са активни.</string> - <string name="cant_delete_user_profile">Потребителският профил не може да се изтрие!</string> <string name="allow_disappearing_messages_only_if">Позволи изчезващи съобщения само ако вашият контакт ги разрешава.</string> - <string name="allow_your_contacts_irreversibly_delete">Позволи на вашите контакти да изтриват необратимо изпратените съобщения.</string> + <string name="allow_your_contacts_irreversibly_delete">Позволи на вашите контакти да изтриват необратимо изпратените съобщения. (24 часа)</string> <string name="allow_your_contacts_to_send_disappearing_messages">Позволи на вашите контакти да изпращат изчезващи съобщения.</string> <string name="chat_preferences_always">винаги</string> <string name="audio_video_calls">Аудио/видео разговори</string> @@ -198,7 +194,7 @@ <string name="color_background">Фон</string> <string name="allow_message_reactions">Позволи реакции на съобщения.</string> <string name="allow_direct_messages">Позволи изпращането на лични съобщения до членовете.</string> - <string name="allow_to_delete_messages">Позволи необратимо изтриване на изпратените съобщения.</string> + <string name="allow_to_delete_messages">Позволи необратимо изтриване на изпратените съобщения. (24 часа)</string> <string name="allow_to_send_files">Позволи изпращане на файлове и медия.</string> <string name="allow_to_send_voice">Позволи изпращане на гласови съобщения.</string> <string name="allow_your_contacts_adding_message_reactions">Позволи на вашите контакти да добавят реакции към съобщения.</string> @@ -224,14 +220,13 @@ <string name="connection_error_auth">Грешка при свързване (AUTH)</string> <string name="display_name_connecting">свързване…</string> <string name="switch_receiving_address">Промени адреса за получаване</string> - <string name="chat_database_deleted">Базата данни на чата е изтрита</string> + <string name="chat_database_deleted">Базата данни е изтрита</string> <string name="chat_is_running">Чатът работи</string> <string name="chat_is_stopped">Чатът е спрян</string> - <string name="chat_database_section">БАЗА ДАННИ НА ЧАТА</string> - <string name="chat_database_imported">Базата данни на чат е импортирана</string> + <string name="chat_database_section">БАЗА ДАННИ</string> + <string name="chat_database_imported">Базата данни е импортирана</string> <string name="confirm_new_passphrase">Потвърди новата парола…</string> <string name="confirm_database_upgrades">Потвърди актуализаациите на базата данни</string> - <string name="chat_archive_header">Архив на чата</string> <string name="rcv_group_event_member_connected">свързан</string> <string name="snd_conn_event_switch_queue_phase_changing">промяна на адреса…</string> <string name="snd_conn_event_switch_queue_phase_changing_for_member">промяна на адреса за %s…</string> @@ -320,7 +315,6 @@ <string name="change_self_destruct_mode">Промени режима на самоунищожение</string> <string name="change_self_destruct_passcode">Промени кода за достъп за самоунищожение</string> <string name="settings_section_title_chats">ЧАТОВЕ</string> - <string name="chat_archive_section">АРХИВ НА ЧАТА</string> <string name="rcv_conn_event_switch_queue_phase_changing">промяна на адреса…</string> <string name="maximum_supported_file_size">В момента максималният поддържан размер на файла е %1$s.</string> <string name="info_row_database_id">ID в базата данни</string> @@ -344,10 +338,7 @@ <string name="database_downgrade">Понижаване на версията на базата данни</string> <string name="database_upgrade">Актуализация на базата данни</string> <string name="mtr_error_no_down_migration">версията на базата данни е по-нова от приложението, но няма миграция надолу за: %s</string> - <string name="archive_created_on_ts">Създаден на %1$s</string> - <string name="delete_archive">Изтрий архив</string> - <string name="delete_chat_archive_question">Изтриване на архива на чата\?</string> - <string name="rcv_group_event_group_deleted">групата изтрита</string> + <string name="rcv_group_event_group_deleted">групата е изтрита</string> <string name="icon_descr_contact_checked">Контактът е проверен</string> <string name="group_member_status_creator">създател</string> <string name="create_group_link">Създай групов линк</string> @@ -377,9 +368,9 @@ <string name="delete_after">Изтрий след</string> <string name="ttl_d">%dд</string> <string name="v5_1_custom_themes">Персонализирани теми</string> - <string name="connect_via_contact_link">Свързване чрез линк на контакта\?</string> - <string name="connect_via_group_link">Свързване чрез групов линк\?</string> - <string name="connect_via_invitation_link">Свързване чрез линк за покана\?</string> + <string name="connect_via_contact_link">Свързване чрез адрес на контакт?</string> + <string name="connect_via_group_link">Влез в групата?</string> + <string name="connect_via_invitation_link">Свързване чрез еднократен линк за връзка?</string> <string name="decryption_error">Грешка при декриптиране</string> <string name="deleted_description">изтрит</string> <string name="contact_already_exists">Контактът вече съществува</string> @@ -404,7 +395,6 @@ <string name="delete_contact_menu_action">Изтрий</string> <string name="delete_group_menu_action">Изтрий</string> <string name="connect_via_link">Свърване чрез линк</string> - <string name="create_one_time_link">Създай линк за еднократна покана</string> <string name="database_passphrase_and_export">Парола за базата данни и експортиране</string> <string name="contribute">Допринеси</string> <string name="continue_to_next_step">Продължи</string> @@ -416,11 +406,11 @@ <string name="developer_options">Идентификатори в базата данни и опция за изолация на транспорта.</string> <string name="delete_address">Изтрий адрес</string> <string name="delete_address__question">Изтрий адрес\?</string> - <string name="theme_colors_section_title">ЦВЕТОВЕ НА ТЕМАТА</string> + <string name="theme_colors_section_title">ЦВЕТОВЕ НА ИНТЕРФЕЙСА</string> <string name="create_profile_button">Създай</string> <string name="create_profile">Създай профил</string> <string name="delete_image">Изтрий изображение</string> - <string name="create_your_profile">Създай своя профил</string> + <string name="create_your_profile">Създай профил</string> <string name="decentralized">Децентрализиран</string> <string name="status_contact_has_e2e_encryption">контактът има e2e криптиране</string> <string name="status_contact_has_no_e2e_encryption">контактът няма e2e криптиране</string> @@ -439,8 +429,8 @@ <string name="custom_time_unit_days">дни</string> <string name="choose_file_title">Избери файл</string> <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Ако сте получили линк за покана за SimpleX Chat, можете да го отворите във вашия браузър:</string> - <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 настолен компютър: сканирайте показания QR код от приложението чрез <b>Сканирай QR код</b>.]]></string> - <string name="delete_pending_connection__question">Изтрий предстоящата връзка\?</string> + <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 настолно устройство: сканирай показания QR код от приложението чрез <b>Сканирай QR код</b>.]]></string> + <string name="delete_pending_connection__question">Изтрий линк за предстоящата връзка?</string> <string name="icon_descr_email">Електронна поща</string> <string name="share_invitation_link">Сподели еднократен линк</string> <string name="one_time_link">Линк за еднократна покана</string> @@ -449,8 +439,8 @@ <string name="smp_servers_enter_manually">Въведи сървъра ръчно</string> <string name="smp_servers_delete_server">Изтрий сървър</string> <string name="dont_create_address">Не създавай адрес</string> - <string name="display_name__field">Показвано име:</string> - <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Само потребителските устройства съхраняват потребителски профили, контакти, групи и съобщения, изпратени с <b>двуслойно криптиране от край до край</b>.]]></string> + <string name="display_name__field">Име на профила:</string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Само потребителските устройства съхраняват потребителски профили, контакти, групи и съобщения.</string> <string name="receipts_contacts_title_disable">Деактивирай потвърждениeто\?</string> <string name="receipts_contacts_enable_keep_overrides">Активиране (запазване на промените)</string> <string name="receipts_contacts_title_enable">Активирай потвърждениeто\?</string> @@ -470,7 +460,7 @@ <string name="timed_messages">Изчезващи съобщения</string> <string name="feature_enabled">активирано</string> <string name="feature_enabled_for_contact">активирано за контакт</string> - <string name="direct_messages_are_prohibited_in_chat">Личните съобщения между членовете са забранени в тази група.</string> + <string name="direct_messages_are_prohibited_in_group">Личните съобщения между членовете са забранени в тази група.</string> <string name="v4_5_multiple_chat_profiles_descr">Различни имена, аватари и транспортна изолация.</string> <string name="v5_2_fix_encryption_descr">Оправяне на криптирането след възстановяване от резервни копия.</string> <string name="delivery_receipts_title">Потвърждениe за доставка!</string> @@ -478,15 +468,15 @@ <string name="delivery_receipts_are_disabled">Потвърждениeто за доставка е деактивирано!</string> <string name="scan_qr_to_connect_to_contact">За да се свърже, вашият контакт може да сканира QR код или да използва линка в приложението.</string> <string name="direct_messages">Лични съобщения</string> - <string name="display_name">Показвано Име</string> - <string name="display_name_cannot_contain_whitespace">Показваното име не може да съдържа интервал.</string> + <string name="display_name">Въведи своето име:</string> + <string name="display_name_cannot_contain_whitespace">Името не може да съдържа интервал.</string> <string name="sending_delivery_receipts_will_be_enabled">Изпращането на потвърждениe за доставка ще бъде активирано за всички контакти.</string> <string name="receipts_contacts_enable_for_all">Активиране за всички</string> <string name="error_enabling_delivery_receipts">Грешка при активирането на потвърждениeто за доставка!</string> <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Изпращането на потвърждениe за доставка ще бъде активирано за всички контакти във всички видими чат профили.</string> <string name="send_receipts">Изпращане на потвърждениe за доставка</string> <string name="you_can_enable_delivery_receipts_later">Можете да активирате по-късно през Настройки</string> - <string name="you_can_enable_delivery_receipts_later_alert">Можете да ги активирате по-късно през настройките за \"Поверителност и сигурност\" на приложението.</string> + <string name="you_can_enable_delivery_receipts_later_alert">Можете да ги активирате по-късно през настройките за "Поверителност и сигурност" на приложението.</string> <string name="database_downgrade_warning">Предупреждение: Може да загубите някои данни!</string> <string name="enter_correct_passphrase">Въведи правилна парола.</string> <string name="feature_enabled_for_you">активирано за вас</string> @@ -501,7 +491,7 @@ <string name="delete_messages">Изтрий съобщенията</string> <string name="error_encrypting_database">Грешка при криптиране на базата данни</string> <string name="enable_automatic_deletion_question">Активиране на автоматично изтриване на съобщения\?</string> - <string name="database_is_not_encrypted">Вашата чат база данни не е криптирана - задайте парола, за да я защитите.</string> + <string name="database_is_not_encrypted">Вашата база данни не е криптирана - задайте парола, за да я защитите.</string> <string name="encrypt_database_question">Криптиране на база данни\?</string> <string name="group_invitation_item_description">покана за група %1$s</string> <string name="you_sent_group_invitation">Изпратихте покана за групата</string> @@ -521,10 +511,10 @@ <string name="snd_conn_event_ratchet_sync_agreed">криптирането е съгласувано за %s</string> <string name="button_edit_group_profile">Редактирай групов профил</string> <string name="share_text_disappears_at">Изчезва в: %s</string> - <string name="member_role_will_be_changed_with_invitation">Ролята ще бъде променена на \"%s\". Членът ще получи нова покана.</string> + <string name="member_role_will_be_changed_with_invitation">Ролята ще бъде променена на "%s". Членът ще получи нова покана.</string> <string name="conn_level_desc_direct">директна</string> <string name="renegotiate_encryption">Предоговори криптирането</string> - <string name="group_display_name_field">Групово показвано име:</string> + <string name="group_display_name_field">Въведи име на групата:</string> <string name="delete_profile">Изтрий профил</string> <string name="dont_show_again">Не показвай отново</string> <string name="ttl_h">%dч</string> @@ -546,12 +536,12 @@ <string name="enable_receipts_all">Активирай</string> <string name="simplex_link_mode_description">Описание</string> <string name="simplex_link_invitation">Еднократна покана за SimpleX</string> - <string name="display_name_invited_to_connect">поканен да се свърже</string> + <string name="display_name_invited_to_connect">покана за свързване</string> <string name="smp_server_test_delete_queue">Изтрий опашка</string> <string name="smp_server_test_disconnect">Прекъсни връзката</string> <string name="smp_server_test_download_file">Свали файл</string> - <string name="failed_to_create_user_duplicate_title">Дублирано показвано име!</string> - <string name="failed_to_create_user_duplicate_desc">Вече имате чат профил със същото показвано име. Моля, изберете друго име.</string> + <string name="failed_to_create_user_duplicate_title">Дублирано име!</string> + <string name="failed_to_create_user_duplicate_desc">Вече имате чат профил със същото име. Моля, изберете друго име.</string> <string name="la_minutes">%d минути</string> <string name="la_seconds">%d секунди</string> <string name="edit_verb">Редактирай</string> @@ -570,20 +560,20 @@ <string name="integrity_msg_duplicate">дублирано съобщение</string> <string name="privacy_and_security">Поверителност и сигурност</string> <string name="alert_text_fragment_encryption_out_of_sync_old_database">Това може да се случи, когато вие или вашата връзка използвате старо резервно копие на базата данни.</string> - <string name="self_destruct_new_display_name">Ново показвано име:</string> + <string name="self_destruct_new_display_name">Ново име:</string> <string name="enable_lock">Активирай заключване</string> <string name="enable_self_destruct">Активирай самоунищожение</string> <string name="chat_item_ttl_none">никога</string> <string name="encrypted_database">Криптирана база данни</string> <string name="network_option_enable_tcp_keep_alive">Активирай TCP keep-alive</string> <string name="disappearing_prohibited_in_this_chat">Изчезващите съобщения са забранени в този чат.</string> - <string name="disappearing_messages_are_prohibited">Изчезващите съобщения са забранени в тази група.</string> + <string name="disappearing_messages_are_prohibited">Изчезващите съобщения са забранени.</string> <string name="v4_4_disappearing_messages">Изчезващи съобщения</string> <string name="enter_welcome_message_optional">Въведи съобщение при посрещане…(незадължително)</string> <string name="button_welcome_message">Съобщение при посрещане</string> <string name="icon_descr_server_status_error">Грешка при свързване със сървъра</string> <string name="v4_2_auto_accept_contact_requests_desc">С незадължително съобщение при посрещане.</string> - <string name="error_deleting_database">Грешка при изтриване на чат базата данни</string> + <string name="error_deleting_database">Грешка при изтриване на базата данни</string> <string name="error_changing_message_deletion">Грешка при промяна на настройката</string> <string name="error_creating_link_for_group">Грешка при създаване на групов линк</string> <string name="error_deleting_link_for_group">Грешка при изтриване на групов линк</string> @@ -607,10 +597,10 @@ <string name="icon_descr_server_status_pending">Предстояща връзка със сървъра</string> <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Вашият контакт трябва да бъде онлайн, за да осъществите връзката. \nМожете да откажете тази връзка и да премахнете контакта (и да опитате по -късно с нов линк).</string> - <string name="error_exporting_chat_database">Грешка при експортиране на чат базата данни</string> + <string name="error_exporting_chat_database">Грешка при експортиране на базата данни</string> <string name="error_with_info">Грешка: %s</string> <string name="error_saving_file">Грешка при запазване на файл</string> - <string name="error_importing_database">Грешка при импортиране на чат базата данни</string> + <string name="error_importing_database">Грешка при импортиране на базата данни</string> <string name="error_removing_member">Грешка при отстраняване на член</string> <string name="error_saving_group_profile">Грешка при запазване на профила на групата</string> <string name="error_loading_smp_servers">Грешка при зареждане на SMP сървъри</string> @@ -651,14 +641,14 @@ <string name="file_will_be_received_when_contact_completes_uploading">Файлът ще бъде получен, когато вашият контакт завърши качването му.</string> <string name="file_will_be_received_when_contact_is_online">Файлът ще бъде получен, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно!</string> <string name="v5_2_favourites_filter_descr">Филтрирайте непрочетените и любимите чатове.</string> - <string name="group_members_can_send_dms">Членовете на групата могат да изпращат лични съобщения.</string> + <string name="group_members_can_send_dms">Членовете могат да изпращат лични съобщения.</string> <string name="icon_descr_help">помощ</string> <string name="settings_section_title_help">ПОМОЩ</string> <string name="email_invite_body">Здравей, \nСвържи се с мен през SimpleX Chat: %s</string> - <string name="group_members_can_add_message_reactions">Членовете на групата могат да добавят реакции към съобщенията.</string> - <string name="group_members_can_delete">Членовете на групата могат необратимо да изтриват изпратените съобщения.</string> - <string name="group_members_can_send_voice">Членовете на групата могат да изпращат гласови съобщения.</string> + <string name="group_members_can_add_message_reactions">Членовете могат да добавят реакции към съобщенията.</string> + <string name="group_members_can_delete">Членовете могат необратимо да изтриват изпратените съобщения. (24 часа)</string> + <string name="group_members_can_send_voice">Членовете могат да изпращат гласови съобщения.</string> <string name="v5_2_disappear_one_message_descr">Дори когато е деактивиран в разговора.</string> <string name="v5_0_large_files_support_descr">Бързо и без чакане, докато подателят е онлайн!</string> <string name="v4_4_french_interface">Френски интерфейс</string> @@ -678,8 +668,8 @@ <string name="group_profile_is_stored_on_members_devices">Груповият профил се съхранява на устройствата на членовете, а не на сървърите.</string> <string name="user_hide">Скрий</string> <string name="prohibit_sending_disappearing_messages">Забрани изпращането на изчезващи съобщения.</string> - <string name="files_are_prohibited_in_group">Файловете и медията са забранени в тази група.</string> - <string name="group_members_can_send_files">Членовете на групата могат да изпращат файлове и медия.</string> + <string name="files_are_prohibited_in_group">Файловете и медията са забранени.</string> + <string name="group_members_can_send_files">Членовете могат да изпращат файлове и медия.</string> <string name="v4_6_hidden_chat_profiles">Скрити чат профили</string> <string name="v4_6_reduced_battery_usage">Допълнително намален разход на батерията</string> <string name="v4_6_group_moderation">Групово модериране</string> @@ -703,7 +693,7 @@ <string name="only_you_can_send_disappearing">Само вие можете да изпращате изчезващи съобщения.</string> <string name="only_your_contact_can_send_disappearing">Само вашият контакт може да изпраща изчезващи съобщения.</string> <string name="prohibit_sending_disappearing">Забрани изпращането на изчезващи съобщения.</string> - <string name="group_members_can_send_disappearing">Членовете на групата могат да изпращат изчезващи съобщения.</string> + <string name="group_members_can_send_disappearing">Членовете могат да изпращат изчезващи съобщения.</string> <string name="invalid_QR_code">Невалиден QR код</string> <string name="invalid_contact_link">Невалиден линк!</string> <string name="incorrect_code">Неправилен код за сигурност!</string> @@ -715,7 +705,7 @@ <string name="invite_friends">Покани приятели</string> <string name="hide_profile">Скрий профила</string> <string name="how_to_use_markdown">Как се използва форматирането</string> - <string name="onboarding_notifications_mode_subtitle">Може да се промени по-късно през настройките.</string> + <string name="onboarding_notifications_mode_subtitle">Как се отразява на батерията</string> <string name="onboarding_notifications_mode_service">Незабавно</string> <string name="settings_section_title_incognito">Режим инкогнито</string> <string name="icon_descr_add_members">Покани членове</string> @@ -726,14 +716,14 @@ <string name="unhide_profile">Покажи профила</string> <string name="how_to_use_simplex_chat">Как се използва</string> <string name="italic_text">курсив</string> - <string name="import_database_question">Импортиране на чат база данни\?</string> + <string name="import_database_question">Импортиране на база данни?</string> <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Ако изберете да отхвърлите, подателят НЯМА да бъде уведомен.</string> <string name="image_descr">Изображение</string> <string name="image_will_be_received_when_contact_completes_uploading">Изображението ще бъде получено, когато вашият контакт завърши качването му.</string> <string name="image_saved">Изображението е запазено в Галерия</string> <string name="icon_descr_image_snd_complete">Изображението е изпратено</string> <string name="image_will_be_received_when_contact_is_online">Изображението ще бъде получено, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно!</string> - <string name="message_deletion_prohibited_in_chat">Необратимото изтриване на съобщения е забранено в тази група.</string> + <string name="message_deletion_prohibited_in_chat">Необратимото изтриване на съобщения е забранено.</string> <string name="v4_3_improved_server_configuration">Подобрена конфигурация на сървъра</string> <string name="alert_text_skipped_messages_it_can_happen_when">Това може да се случи, когато: \n1. Времето за пазене на съобщенията е изтекло - в изпращащия клиент е 2 дена а на сървъра е 30. @@ -762,7 +752,7 @@ <string name="invalid_message_format">невалиден формат на съобщението</string> <string name="invalid_connection_link">Невалиден линк за връзка</string> <string name="notification_display_mode_hidden_desc">Скриване на контакт и съобщение</string> - <string name="turn_off_battery_optimization"><![CDATA[За да го използвате, моля, <b>разрешете на SimpleX да работи във фонов режим</b> в следващия диалогов прозорец. В противен случай известията ще бъдат деактивирани.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[<b>Разрешете</b> в следващия диалогов прозорец, за да получавате незабавно известия.]]></string> <string name="icon_descr_instant_notifications">Незабавни известия</string> <string name="service_notifications">Незабавни известия!</string> <string name="service_notifications_disabled">Незабавните известия са деактивирани!</string> @@ -775,7 +765,7 @@ <string name="install_simplex_chat_for_terminal">Инсталирай SimpleX Chat за терминал</string> <string name="how_it_works">Как работи</string> <string name="how_simplex_works">Как работи SimpleX</string> - <string name="immune_to_spam_and_abuse">Защитен от спам и злоупотреби</string> + <string name="immune_to_spam_and_abuse">Защитен от спам</string> <string name="ignore">Игнорирай</string> <string name="button_add_members">Покани членове</string> <string name="message_deletion_prohibited">Необратимото изтриване на съобщения е забранено в този чат.</string> @@ -787,34 +777,32 @@ <string name="custom_time_unit_seconds">секунди</string> <string name="custom_time_unit_weeks">седмици</string> <string name="v5_1_japanese_portuguese_interface">Японски и португалски потребителски интерфейс</string> - <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 мобилно: докоснете <b>Отваряне в мобилно приложение</b>, след което докоснете <b>Свързване</b> в приложението.]]></string> + <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 мобилно устройство: докоснете <b>Отваряне в мобилно приложение</b>, след което докоснете <b>Свързване</b> в приложението.]]></string> <string name="reject_contact_button">Отхвърляне</string> <string name="mark_unread">Маркирай като непрочетено</string> <string name="mark_read">Маркирай като прочетено</string> <string name="mute_chat">Без звук</string> <string name="image_descr_qr_code">QR код</string> <string name="icon_descr_more_button">Повече</string> - <string name="read_more_in_user_guide_with_link"><![CDATA[Прочетете повече в <font color=#0088ff>Ръководство за потребителя</font>.]]></string> + <string name="read_more_in_user_guide_with_link"><![CDATA[Прочетете повече в <font color="#0088ff">Ръководство за потребителя</font>.]]></string> <string name="mark_code_verified">Маркирай като проверено</string> <string name="is_not_verified">%s не е потвърдено</string> <string name="is_verified">%s е потвърдено</string> <string name="rate_the_app">Оценете приложението</string> <string name="ensure_ICE_server_address_are_correct_format_and_unique">Уверете се, че адресите на WebRTC ICE сървъра са в правилен формат, разделени на редове и не са дублирани.</string> <string name="network_and_servers">Мрежа и сървъри</string> - <string name="network_settings_title">Мрежови настройки</string> + <string name="network_settings_title">Разширени настройки</string> <string name="port_verb">Порт</string> <string name="network_proxy_port">порт %d</string> - <string name="network_use_onion_hosts_no_desc_in_alert">Няма се използват Onion хостове.</string> <string name="network_use_onion_hosts_required">Задължително</string> <string name="network_use_onion_hosts_no">Не</string> <string name="network_use_onion_hosts_required_desc">За свързване ще са необходими Onion хостове. \nМоля, обърнете внимание: няма да можете да се свържете със сървърите без .onion адрес.</string> <string name="network_use_onion_hosts_prefer_desc">Ще се използват Onion хостове, когато са налични.</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">Ще се използват Onion хостове, когато са налични.</string> <string name="network_use_onion_hosts_no_desc">Няма се използват Onion хостове.</string> <string name="email_invite_subject">Нека да поговорим в SimpleX Chat</string> <string name="password_to_show">Парола за показване</string> - <string name="read_more_in_github_with_link"><![CDATA[Прочетете повече в нашето <font color=#0088ff>GitHub хранилище</font>.]]></string> + <string name="read_more_in_github_with_link"><![CDATA[Прочетете повече в нашето <font color="#0088ff">GitHub хранилище</font>.]]></string> <string name="onboarding_notifications_mode_off">Когато приложението работи</string> <string name="onboarding_notifications_mode_periodic">Периодично</string> <string name="paste_the_link_you_received">Постави получения линк</string> @@ -838,7 +826,7 @@ <string name="v4_3_voice_messages_desc">Макс. 40 секунди, получават се незабавно.</string> <string name="v4_4_live_messages">Съобщения на живо</string> <string name="live_message">Съобщение на живо!</string> - <string name="verify_security_code">Потвръди кода за сигурност</string> + <string name="verify_security_code">Потвърди кода за сигурност</string> <string name="no_details">няма подробности</string> <string name="ok">ОК</string> <string name="ask_your_contact_to_enable_voice">Моля, попитайте вашия контакт, за да активирате изпращане на гласови съобщения.</string> @@ -864,13 +852,13 @@ <string name="prohibit_message_reactions_group">Забрани реакциите на съобщенията.</string> <string name="restore_database_alert_desc">Моля, въведете предишната парола след възстановяване на резервното копие на базата данни. Това действие не може да бъде отменено.</string> <string name="large_file">Голям файл!</string> - <string name="message_reactions_are_prohibited">Реакциите на съобщения са забранени в тази група.</string> + <string name="message_reactions_are_prohibited">Реакциите на съобщения са забранени.</string> <string name="new_in_version">Ново в %s</string> <string name="feature_offered_item">предлага %s</string> <string name="feature_offered_item_with_param">предлага %s: %2s</string> <string name="whats_new_read_more">Прочетете още</string> <string name="v5_2_disappear_one_message">Накарайте едно съобщение да изчезне</string> - <string name="v4_4_verify_connection_security">Потвръди сигурността на връзката</string> + <string name="v4_4_verify_connection_security">Потвърди сигурността на връзката</string> <string name="v4_5_message_draft">Чернова на съобщение</string> <string name="v5_2_more_things_descr">- по-стабилна доставка на съобщения. \n- малко по-добри групи. @@ -882,7 +870,6 @@ <string name="no_selected_chat">Няма избран чат</string> <string name="notifications">Известия</string> <string name="shutdown_alert_desc">Известията ще спрат да работят, докато не стартирате отново приложението</string> - <string name="network_use_onion_hosts_required_desc_in_alert">За свързване ще са необходими Onion хостове.</string> <string name="images_limit_desc">Само 10 изображения могат да бъдат изпратени едновременно</string> <string name="only_owners_can_enable_files_and_media">Само собствениците на групата могат да активират файлове и медията.</string> <string name="only_group_owners_can_enable_voice">Само собствениците на групата могат да активират гласови съобщения.</string> @@ -897,7 +884,7 @@ <string name="switch_receiving_address_desc">Получаващият адрес ще бъде променен към друг сървър. Промяната на адреса ще завърши, след като подателят е онлайн.</string> <string name="new_database_archive">Нов архив на база данни</string> <string name="old_database_archive">Стар архив на база данни</string> - <string name="non_fatal_errors_occured_during_import">Някои не-фатални грешки са възникнали по време на импортиране - може да видите конзолата за повече подробности.</string> + <string name="non_fatal_errors_occured_during_import">Някои не-фатални грешки са възникнали по време на импортиране:</string> <string name="messages_section_title">Съобщения</string> <string name="enter_correct_current_passphrase">Моля, въведете правилната текуща парола.</string> <string name="store_passphrase_securely">Моля, съхранявайте паролата на сигурно място, НЯМА да можете да я промените, ако я загубите.</string> @@ -925,7 +912,7 @@ <string name="member_will_be_removed_from_group_cannot_be_undone">Членът ще бъде премахнат от групата - това не може да бъде отменено!</string> <string name="item_info_no_text">няма текст</string> <string name="button_remove_member">Острани член</string> - <string name="member_role_will_be_changed_with_notification">Ролята ще бъде променена на \"%s\". Всички в групата ще бъдат уведомени.</string> + <string name="member_role_will_be_changed_with_notification">Ролята ще бъде променена на "%s". Всички в групата ще бъдат уведомени.</string> <string name="users_delete_data_only">Само данни за локален профил</string> <string name="users_delete_with_connections">Профилни и сървърни връзки</string> <string name="user_mute">Без звук</string> @@ -943,11 +930,11 @@ <string name="v4_6_hidden_chat_profiles_descr">Защитете чат профилите с парола!</string> <string name="v4_6_group_moderation_descr">Сега администраторите могат: \n- да изтриват съобщения на членове. -\n- да деактивират членове (роля \"наблюдател\")</string> +\n- да деактивират членове (роля "наблюдател")</string> <string name="v4_6_reduced_battery_usage_descr">Очаквайте скоро още подобрения!</string> <string name="v5_0_polish_interface">Полски интерфейс</string> <string name="v5_1_message_reactions">Реакции на съобщения</string> - <string name="you_will_join_group">Ще се присъедините към групата, към която този линк препраща, и ще се свържете с нейните членове.</string> + <string name="you_will_join_group">Ще се свържете с всички членове на групата.</string> <string name="thousand_abbreviation">х</string> <string name="opening_database">Отваряне на база данни…</string> <string name="live">НА ЖИВО</string> @@ -968,7 +955,7 @@ <string name="simplex_service_notification_text">Получаване на съобщения…</string> <string name="notifications_mode_off">Работи, когато приложението е отворено</string> <string name="simplex_service_notification_title">Simplex Chat услуга</string> - <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[За да запази вашата поверителност, вместо да изпозлва push известия, приложението има <b> SimpleX фонова услуга </b> – използва няколко процента от батерията на ден.]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[За подобряване на поверителността <b>SimpleX работи във фонов режим</b> вместо да използва push известия.]]></string> <string name="enter_passphrase_notification_desc">За да получавате известия, моля, въведете паролата на базата данни</string> <string name="auth_log_in_using_credential">Влезте с вашите идентификационни данни</string> <string name="message_delivery_error_title">Грешка при доставката на съобщението</string> @@ -976,7 +963,7 @@ <string name="message_delivery_error_desc">Най-вероятно този контакт е изтрил връзката с вас.</string> <string name="no_history">Няма история</string> <string name="auth_open_chat_console">Отвори конзолата</string> - <string name="auth_open_chat_profiles">Отвори чат профилите</string> + <string name="auth_open_chat_profiles">Промени чат профилите</string> <string name="la_please_remember_to_store_password">Моля, запомнете я или я съхранявайте на сигурно място - няма начин да възстановите загубена парола!</string> <string name="received_message">Получено съобщение</string> <string name="reply_verb">Отговори</string> @@ -991,28 +978,25 @@ <string name="toast_permission_denied">Разрешение е отказано!</string> <string name="image_descr_profile_image">профилно изображение</string> <string name="icon_descr_profile_image_placeholder">запазено място за профилно изображение</string> - <string name="opensource_protocol_and_code_anybody_can_run_servers">Протокол и код с отворен код – всеки може да оперира собствени сървъри.</string> - <string name="people_can_connect_only_via_links_you_share">Хората могат да се свържат с вас само чрез ликовете, които споделяте.</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Всеки може да оперира сървъри.</string> + <string name="people_can_connect_only_via_links_you_share">Вие решавате кой може да се свърже с вас.</string> <string name="privacy_redefined">Поверителността преосмислена</string> - <string name="read_more_in_github">Прочетете повече в нашето хранилище в GitHub.</string> <string name="make_private_connection">Добави поверителна връзка</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[Много хора попитаха: <i>ако SimpleX няма потребителски идентификатори, как може да доставя съобщения\?</i>]]></string> <string name="open_verb">Отвори</string> <string name="relay_server_if_necessary">Реле сървър се използва само ако е необходимо. Друга страна може да наблюдава вашия IP адрес.</string> <string name="lock_after">Заключване след</string> <string name="lock_mode">Режим на заключване</string> <string name="alert_text_fragment_please_report_to_developers">Моля, докладвайте го на разработчиците.</string> <string name="protect_app_screen">Защити екрана на приложението</string> - <string name="save_archive">Запази архив</string> <string name="member_info_section_title_member">ЧЛЕН</string> <string name="remove_member_confirmation">Премахване</string> <string name="network_option_ping_count">PING бройка</string> <string name="only_your_contact_can_add_message_reactions">Само вашият контакт може да добавя реакции на съобщенията.</string> <string name="message_reactions_prohibited_in_this_chat">Реакциите на съобщения са забранени в този чат.</string> <string name="only_you_can_add_message_reactions">Само вие можете да добавяте реакции на съобщенията.</string> - <string name="only_you_can_delete_messages">Само вие можете необратимо да изтриете съобщения (вашият контакт може да ги маркира за изтриване).</string> + <string name="only_you_can_delete_messages">Само вие можете необратимо да изтриете съобщения (вашият контакт може да ги маркира за изтриване). (24 часа)</string> <string name="only_you_can_send_voice">Само вие можете да изпращате гласови съобщения.</string> - <string name="only_your_contact_can_delete">Само вашият контакт може необратимо да изтрие съобщения (можете да ги маркирате за изтриване).</string> + <string name="only_your_contact_can_delete">Само вашият контакт може необратимо да изтрие съобщения (можете да ги маркирате за изтриване). (24 часа)</string> <string name="only_your_contact_can_send_voice">Само вашият контакт може да изпраща гласови съобщения.</string> <string name="prohibit_message_reactions">Забрани реакциите на съобщенията.</string> <string name="prohibit_sending_voice_messages">Забрани изпращането на гласови съобщения.</string> @@ -1082,7 +1066,7 @@ <string name="receipts_groups_title_disable">Деактивиране на потвърждениe за доставка за групи\?</string> <string name="receipts_groups_enable_for_all">Активиране за всички групи</string> <string name="receipts_groups_override_enabled">Изпращането на потвърждениe за доставка е разрешено за %d групи</string> - <string name="restart_the_app_to_use_imported_chat_database">Рестартирайте приложението, за да използвате импортирана чат база данни.</string> + <string name="restart_the_app_to_use_imported_chat_database">Рестартирайте приложението, за да използвате импортирана база данни.</string> <string name="send_receipts_disabled_alert_msg">Тази група има над %1$d членове, потвърждениeто за доставка няма да се изпраща.</string> <string name="conn_stats_section_title_servers">СЪРВЪРИ</string> <string name="recipient_colon_delivery_status">%s: %s</string> @@ -1149,12 +1133,9 @@ <string name="save_preferences_question">Запази настройките\?</string> <string name="icon_descr_speaker_on">Високоговорителят е включен</string> <string name="icon_descr_speaker_off">Високоговорителят е изключен</string> - <string name="stop_chat_to_enable_database_actions">Спрете чата, за да активирате действията с базата данни.</string> <string name="role_in_group">Роля</string> - <string name="network_options_revert">Отмени промените</string> <string name="network_options_save">Запази</string> <string name="reset_color">Нулирай цветовете</string> - <string name="save_color">Запази цвета</string> <string name="color_secondary">Вторичен</string> <string name="custom_time_picker_select">Избери</string> <string name="to_start_a_new_chat_help_header">За да започнете нов чат</string> @@ -1164,12 +1145,10 @@ <string name="this_link_is_not_a_valid_connection_link">Този линк не е валиден линк за връзка!</string> <string name="your_chat_profiles">Вашите чат профили</string> <string name="smp_servers_per_user">Сървърите за нови връзки на текущия ви чат профил</string> - <string name="to_reveal_profile_enter_password">За да покажете скрития профил, въведете пълната парола в полето за търсене на страницата \"Вашите чат профили\".</string> + <string name="to_reveal_profile_enter_password">За да покажете скрития профил, въведете пълната парола в полето за търсене на страницата "Вашите чат профили".</string> <string name="profile_is_only_shared_with_your_contacts">Профилът се споделя само с вашите контакти.</string> <string name="delete_files_and_media_desc">Това действие не може да бъде отменено - всички получени и изпратени файлове и медия ще бъдат изтрити. Снимките с ниска разделителна способност ще бъдат запазени.</string> <string name="tap_to_activate_profile">Докосни за активиране на профил.</string> - <string name="should_be_at_least_one_profile">Трябва да има поне един потребителски профил.</string> - <string name="should_be_at_least_one_visible_profile">Трябва да има поне един видим потребителски профил.</string> <string name="language_system">Системен</string> <string name="color_title">Заглавие</string> <string name="to_share_with_your_contact">(за споделяне с вашия контакт)</string> @@ -1191,17 +1170,17 @@ <string name="delete_chat_profile_action_cannot_be_undone_warning">Това действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени.</string> <string name="enable_automatic_deletion_message">Това действие не може да бъде отменено - съобщенията, изпратени и получени по-рано от избраното, ще бъдат изтрити. Може да отнеме няколко минути.</string> <string name="database_backup_can_be_restored">Опитът за промяна на паролата на базата данни не беше завършен.</string> - <string name="group_is_decentralized">Групата е напълно децентрализирана – видима е само за членовете.</string> + <string name="group_is_decentralized">Напълно децентрализирана – видима е само за членовете.</string> <string name="v4_5_transport_isolation">Транспортна изолация</string> <string name="v5_0_polish_interface_descr">Благодарение на потребителите – допринесете през Weblate!</string> - <string name="alert_text_msg_bad_hash">Хешът на предишното съобщение е различен.</string> + <string name="alert_text_msg_bad_hash">Хешът на предишното съобщение е различен.\"</string> <string name="smp_servers_test_servers">Тествай сървърите</string> <string name="v4_5_italian_interface_descr">Благодарение на потребителите – допринесете през Weblate!</string> <string name="v4_5_private_filenames_descr">За да не се разкрива часовата зона, файловете с изображения/глас използват UTC.</string> <string name="this_string_is_not_a_connection_link">Този текст не е линк за връзка!</string> <string name="videos_limit_title">Твърде много видеоклипове!</string> <string name="messages_section_description">Тази настройка се прилага за съобщения в текущия ви профил</string> - <string name="to_protect_privacy_simplex_has_ids_for_queues">За да се защити поверителността, вместо потребителски идентификатори, използвани от всички други платформи, SimpleX има идентификатори за опашки от съобщения, отделни за всеки от вашите контакти.</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">За да се защити поверителността, SimpleX използва идентификатори за опашки от съобщения, отделни за всеки от вашите контакти.</string> <string name="trying_to_connect_to_server_to_receive_messages">Опит за свързване със сървъра, използван за получаване на съобщения от този контакт.</string> <string name="trying_to_connect_to_server_to_receive_messages_with_error">Опит за свързване със сървъра, използван за получаване на съобщения от този контакт (грешка: %1$s).</string> <string name="error_smp_test_failed_at_step">Тестът е неуспешен на стъпка %s.</string> @@ -1210,8 +1189,8 @@ <string name="chat_help_tap_button">Докосни бутона</string> <string name="thank_you_for_installing_simplex">Благодарим Ви, че инсталирахте SimpleX Chat!</string> <string name="save_and_notify_contacts">Запази и уведоми контактите</string> - <string name="next_generation_of_private_messaging">Ново поколение поверителни съобщения</string> - <string name="first_platform_without_user_ids">Първата платформа без никакви потребителски идентификатори – поверителна по дизайн.</string> + <string name="next_generation_of_private_messaging">Бъдещето на комуникацията</string> + <string name="first_platform_without_user_ids">Няма потребителски идентификатори.</string> <string name="la_mode_system">Системна</string> <string name="alert_text_msg_bad_id">Неправилно ID на следващото съобщение (по-малко или еднакво с предишното). \nТова може да се случи поради някаква грешка или когато връзката е компрометирана.</string> @@ -1223,7 +1202,7 @@ <string name="you_will_be_connected_when_your_contacts_device_is_online">Ще бъдете свързани, когато устройството на вашия контакт е онлайн, моля, изчакайте или проверете по-късно!</string> <string name="you_wont_lose_your_contacts_if_delete_address">Няма да загубите контактите си, ако по-късно изтриете адреса си.</string> <string name="your_settings">Вашите настройки</string> - <string name="your_simplex_contact_address">Вашият SimpleX адрес</string> + <string name="your_simplex_contact_address">Вашият адрес в SimpleX</string> <string name="smp_servers_use_server_for_new_conn">Използвай за нови връзки</string> <string name="your_XFTP_servers">Вашите XFTP сървъри</string> <string name="use_simplex_chat_servers__question">Използвай сървърите на SimpleX Chat\?</string> @@ -1232,13 +1211,11 @@ <string name="your_SMP_servers">Вашите SMP сървъри</string> <string name="network_socks_toggle_use_socks_proxy">Използвай SOCKS прокси</string> <string name="network_enable_socks">Използвай SOCKS прокси\?</string> - <string name="update_onion_hosts_settings_question">Актуализиране на настройката за .onion хостове\?</string> <string name="network_disable_socks">Използване на директна интернет връзка\?</string> <string name="network_use_onion_hosts">Използвай .onion хостове</string> <string name="network_use_onion_hosts_prefer">Когато са налични</string> <string name="your_profile_is_stored_on_your_device">Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство.</string> <string name="you_can_use_markdown_to_format_messages__prompt">Можете да използвате markdown за форматиране на съобщенията:</string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Вие контролирате през кой сървър(и) <b>да получавате</b> съобщенията, вашите контакти – сървърите, които използвате, за да им изпращате съобщения.]]></string> <string name="use_chat">Използвай чата</string> <string name="update_database">Актуализация</string> <string name="you_have_to_enter_passphrase_every_time">Трябва да въвеждате парола при всяко стартиране на приложението - тя не се съхранява на устройството.</string> @@ -1252,11 +1229,11 @@ <string name="voice_messages_prohibited">Гласовите съобщения са забранени!</string> <string name="you_need_to_allow_to_send_voice">Трябва да разрешите на вашия контакт да изпраща гласови съобщения, за да можете да ги изпращате.</string> <string name="you_are_invited_to_group">Поканени сте в групата</string> - <string name="snd_conn_event_switch_queue_phase_completed">променихте адреса</string> + <string name="snd_conn_event_switch_queue_phase_completed">адреса за получаване е променен</string> <string name="you_can_share_this_address_with_your_contacts">Можете да споделите този адрес с вашите контакти, за да им позволите да се свържат с %s.</string> <string name="unfavorite_chat">Премахни от любимите</string> <string name="settings_section_title_you">ВИЕ</string> - <string name="your_chat_database">Вашата чат база данни</string> + <string name="your_chat_database">Вашата база данни</string> <string name="icon_descr_waiting_for_image">Изчаква се получаването на изображението</string> <string name="waiting_for_image">Изчаква се получаването на изображението</string> <string name="icon_descr_waiting_for_video">Изчаква се получаването на видеото</string> @@ -1268,7 +1245,7 @@ <string name="voice_message">Гласово съобщение</string> <string name="voice_message_with_duration">Гласово съобщение (%1$s)</string> <string name="voice_message_send_text">Гласово съобщение…</string> - <string name="voice_messages_are_prohibited">Гласовите съобщения са забранени в тази група.</string> + <string name="voice_messages_are_prohibited">Гласовите съобщения са забранени.</string> <string name="icon_descr_received_msg_status_unread">непрочетено</string> <string name="welcome">Добре дошли!</string> <string name="personal_welcome">Добре дошли %1$s!</string> @@ -1294,12 +1271,11 @@ <string name="incognito_random_profile">Вашият автоматично генериран профил</string> <string name="user_unmute">Уведомявай</string> <string name="you_can_share_your_address">Можете да споделите адреса си като линк или QR код - всеки може да се свърже с вас.</string> - <string name="snd_conn_event_switch_queue_phase_completed_for_member">променихте адреса за %s</string> + <string name="snd_conn_event_switch_queue_phase_completed_for_member">променихте адреса получаване за %s</string> <string name="group_main_profile_sent">Вашият чат профил ще бъде изпратен на членовете на групата</string> <string name="your_chat_profile_will_be_sent_to_your_contact">Вашият чат профил ще бъде изпратен \nдо вашия контакт</string> - <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Вашата текуща чат база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната. -\nТова действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени.</string> + <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Вашата текуща база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната. \nТова действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени.</string> <string name="updating_settings_will_reconnect_client_to_all_servers">Актуализирането на настройките ще свърже отново клиента към всички сървъри.</string> <string name="rcv_group_event_updated_group_profile">актуализиран профил на групата</string> <string name="video_descr">Видео</string> @@ -1328,7 +1304,7 @@ <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Ще трябва да се идентифицирате, когато стартирате или възобновите приложението след 30 секунди във фонов режим.</string> <string name="you_are_observer">вие сте наблюдател</string> <string name="gallery_video_button">Видео</string> - <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Можете да <font color=#0088ff>се свържете с разработчиците на SimpleX Chat, за да задавате въпроси и да получавате актуализации</font>;.]]></string> + <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Можете да <font color="#0088ff">се свържете с разработчиците на SimpleX Chat, за да задавате въпроси и да получавате актуализации</font>;.]]></string> <string name="contact_wants_to_connect_with_you">иска да се свърже с вас!</string> <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Можете също да се свържете, като натиснете върху линка. Ако се отвори в браузъра, натиснете върху бутона <b>Отваряне в мобилно приложение</b>.]]></string> <string name="xftp_servers">XFTP сървъри</string> @@ -1401,4 +1377,897 @@ <string name="compose_send_direct_message_to_connect">Изпрати лично съобщение за свързване</string> <string name="member_contact_send_direct_message">изпрати лично съобщение</string> <string name="rcv_group_event_member_created_contact">свързан директно</string> -</resources> \ No newline at end of file + <string name="expand_verb">Разшири</string> + <string name="v5_4_block_group_members">Блокиране на членове на групата</string> + <string name="connect_plan_repeat_connection_request">Изпрати отново заявката за свързване?</string> + <string name="encryption_renegotiation_error">Грешка при повторно договаряне на криптиране</string> + <string name="rcv_direct_event_contact_deleted">изтрит контакт</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Вече се свързвате с <b>%1$s</b>.]]></string> + <string name="error_alert_title">Грешка</string> + <string name="v5_4_incognito_groups_descr">Създай група с автоматично генериран профилл.</string> + <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Вие вече се присъединявате към групата чрез този линк.</string> + <string name="create_group_button">Създай група</string> + <string name="refresh_qr_code">Опресняване</string> + <string name="create_another_profile_button">Създай профил</string> + <string name="connected_desktop">Свързано настолно устройство</string> + <string name="group_members_2">%s и %s</string> + <string name="new_mobile_device">Ново мобилно устройство</string> + <string name="multicast_connect_automatically">Автоматично свъзрване</string> + <string name="desktop_address">Адрес на настолно устройство</string> + <string name="only_one_device_can_work_at_the_same_time">Само едно устройство може да работи едновременно</string> + <string name="connect_plan_join_your_group">Влез в твоята група?</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Вече се присъединявате към групата <b>%1$s</b>.]]></string> + <string name="v5_4_link_mobile_desktop">Свържете мобилни и настолни приложения! 🔗</string> + <string name="connect_plan_this_is_your_own_one_time_link">Това е вашят еднократен линк за връзка!</string> + <string name="marked_deleted_items_description">%d съобщения са маркирани като изтрити</string> + <string name="connect_plan_group_already_exists">Групата вече съществува!</string> + <string name="v5_4_link_mobile_desktop_descr">Чрез сигурен квантово устойчив протокол.</string> + <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Отвори <i>Използвай от настолно устройство</i> в мобилното приложение и сканирай QR кода.]]></string> + <string name="connect_plan_already_connecting">В процес на свързване!</string> + <string name="v5_4_block_group_members_descr">Скриване на нежелани съобщения.</string> + <string name="desktop_incompatible_version">Несъвместима версия</string> + <string name="new_desktop"><![CDATA[<i>(ново)</i>]]></string> + <string name="unlink_desktop_question">Забрави настолно устройство?</string> + <string name="v5_4_better_groups">По-добри групи</string> + <string name="linked_desktop_options">Настройки на запомнени настолни устройства</string> + <string name="video_decoding_exception_desc">Видеото не може да бъде декодирано. Моля, опитайте с друг видеоклип или се свържете с разработчиците.</string> + <string name="rcv_group_event_1_member_connected">%s свързан</string> + <string name="random_port">Случаен</string> + <string name="linked_desktops">Запомнени настолни устройства</string> + <string name="discover_on_network">Откриване през локалната мрежа</string> + <string name="rcv_group_and_other_events">и %d други събития</string> + <string name="connect_plan_connect_via_link">Свърване чрез линк?</string> + <string name="v5_4_incognito_groups">Инкогнито групи</string> + <string name="connect_plan_already_joining_the_group">Вече се присъединихте към групата!</string> + <string name="group_members_n">%s, %s и %d членове</string> + <string name="this_device">Това устройство</string> + <string name="moderated_items_description">%1$d съобщения, модерирани от %2$s</string> + <string name="unblock_member_button">Отблокирай член</string> + <string name="remote_host_was_disconnected_toast"><![CDATA[Връзкарта с <b>%s</b> мобилно устройство бе прекъсната]]></string> + <string name="waiting_for_desktop">Изчакване на настолно устройство…</string> + <string name="v5_4_better_groups_descr">По-бързо присъединяване и по-надеждни съобщения.</string> + <string name="connect_plan_connect_to_yourself">Свърване със себе си?</string> + <string name="linked_mobiles">Запомнени мобилни устройства</string> + <string name="desktop_device">Настолно устройство</string> + <string name="connected_to_desktop">Свързан с настолно устройство</string> + <string name="contact_tap_to_connect">Докосни за свързване</string> + <string name="this_device_name">Името на това устройство</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Вече сте в група <b>%1$s</b>.]]></string> + <string name="connect_plan_this_is_your_own_simplex_address">Това е вашият личен SimpleX адрес!</string> + <string name="loading_remote_file_title">Зареждане на файла</string> + <string name="connecting_to_desktop">Свързване с настолно устройство</string> + <string name="found_desktop">Намерено настолно устройство</string> + <string name="alert_text_encryption_renegotiation_failed">Неуспешно повторно договаряне на криптирането.</string> + <string name="desktop_devices">Настолни устройства</string> + <string name="correct_name_to">Поправи име на %s?</string> + <string name="not_compatible">Несъвместим!</string> + <string name="waiting_for_mobile_to_connect">Изчаква се мобилното устройство да се свърже:</string> + <string name="delete_messages__question">Изтрий %d съобщения?</string> + <string name="link_a_mobile">Свързване на мобилно устройство</string> + <string name="connect_with_contact_name_question">Свързване с %1$s?</string> + <string name="remove_member_button">Премахни член</string> + <string name="block_member_confirmation">Блокирай</string> + <string name="unblock_member_question">Отблокирай член?</string> + <string name="blocked_items_description">%d блокирани съобщения</string> + <string name="open_port_in_firewall_desc">За да позволите на мобилно приложение да се свърже с настолното, отворете този порт във вашата защитна стена, ако тя е активирана</string> + <string name="block_member_button">Блокирай член</string> + <string name="settings_section_title_use_from_desktop">Използвай от настолно устройство</string> + <string name="connected_mobile">Свързано мобилно устройство</string> + <string name="session_code">Код на сесията</string> + <string name="connect_plan_repeat_join_request">Изпрати отново заявката за присъединяване?</string> + <string name="create_chat_profile">Създай чат профил</string> + <string name="button_remove_member_question">Премахни член?</string> + <string name="app_was_crashed">Покажи грешките</string> + <string name="delete_and_notify_contact">Изтрий и уведоми контакт</string> + <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">Вече се свързвате чрез този еднократен линк за връзка!</string> + <string name="connect_plan_open_group">Отвори група</string> + <string name="desktop_connection_terminated">Връзката е прекратена</string> + <string name="this_device_version"><![CDATA[<i>(това устройство v%s)</i>]]></string> + <string name="unlink_desktop">Забрави</string> + <string name="unblock_member_desc">Съобщенията от %s ще бъдат показани!</string> + <string name="this_device_name_shared_with_mobile">Името на устройството ще бъде споделено със свързания мобилен клиент.</string> + <string name="error_sending_message_contact_invitation">Грешка при изпращане на покана</string> + <string name="verify_code_on_mobile">Потвърди кода на мобилното устройство</string> + <string name="open_port_in_firewall_title">Отвори порт в защитната стена</string> + <string name="enter_this_device_name">Въведи името на това устройство…</string> + <string name="error">Грешка</string> + <string name="connect_to_desktop">Свързване с настолно устройство</string> + <string name="non_content_uri_alert_text">Споделихте невалиден файлов път. Докладвайте проблема на разработчиците на приложението.</string> + <string name="disconnect_remote_hosts">Прекъсни връзката с мобилните устройства?</string> + <string name="disconnect_remote_host">Прекъсни връзката</string> + <string name="block_member_question">Блокирай члена?</string> + <string name="rcv_group_events_count">%d групови събития</string> + <string name="invalid_name">Невалидно име!</string> + <string name="group_member_role_author">автор</string> + <string name="connected_to_mobile">Свързан с мобилно устройство</string> + <string name="bad_desktop_address">Грешен адрес на настолното устройство</string> + <string name="paste_desktop_address">Постави адрес на настолно устройство</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Това е вашят линк за група <b>%1$s</b>!]]></string> + <string name="verify_code_with_desktop">Потвърди кода с настолното устройство</string> + <string name="scan_qr_code_from_desktop">Сканирай QR код от настолното устройство</string> + <string name="unblock_member_confirmation">Отблокирай</string> + <string name="devices">Устройства</string> + <string name="multicast_discoverable_via_local_network">Откриваем през локалната мрежа</string> + <string name="v5_4_more_things_descr">- по желание уведомете изтритите контакти. +\n- имена на профили с интервали. +\n- и още!</string> + <string name="non_content_uri_alert_title">Невалиден файлов път</string> + <string name="connect_plan_you_have_already_requested_connection_via_this_address">Вече сте заявили връзка през този адрес!</string> + <string name="terminal_always_visible">Покажи конзолата в нов прозорец</string> + <string name="scan_from_mobile">Сканиране от мобилно устройство</string> + <string name="verify_connections">Потвърждение за свързване</string> + <string name="disconnect_desktop_question">Прекъсни връзката с настолното устройство?</string> + <string name="loading_remote_file_desc">Моля, изчакайте, докато файлът се зареди от свързаното мобилно устройство</string> + <string name="block_member_desc">Всички нови съобщения от %s ще бъдат скрити!</string> + <string name="desktop_app_version_is_incompatible">Версията на настолното приложение %s не е съвместима с това приложение.</string> + <string name="verify_connection">Потвърди връзка</string> + <string name="no_connected_mobile">Няма свързано мобилно устройство</string> + <string name="blocked_item_description">блокиран</string> + <string name="you_can_make_address_visible_via_settings">Можете да го направите видим за вашите контакти в SimpleX чрез Настройки.</string> + <string name="recent_history_is_not_sent_to_new_members">Историята не се изпраща на нови членове.</string> + <string name="retry_verb">Опитай отново</string> + <string name="camera_not_available">Камерата е неодстъпна</string> + <string name="enable_sending_recent_history">Изпращане до последните 100 съобщения на нови членове.</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Добави контакт</b>: за създаване на нов линк или свързване чрез получен линк за връзка.]]></string> + <string name="disable_sending_recent_history">Не изпращай история на нови членове.</string> + <string name="error_showing_content">грешка при показване на съдържанието</string> + <string name="or_show_this_qr_code">Или покажи този код</string> + <string name="recent_history_is_sent_to_new_members">На новите членове се изпращат до последните 100 съобщения.</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">QR кодът, който сканирахте, не е SimpleX линк за връзка.</string> + <string name="the_text_you_pasted_is_not_a_link">Текстът, който поставихте, не е SimpleX линк за връзка.</string> + <string name="error_showing_message">грешка при показване на съобщението</string> + <string name="enable_camera_access">Разреши достъпа до камерата</string> + <string name="you_can_view_invitation_link_again">Можете да видите отново линкът за покана в подробностите за връзката.</string> + <string name="keep_unused_invitation_question">Запази неизползваната покана за връзка?</string> + <string name="share_this_1_time_link">Сподели този еднократен линк за връзка</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Създай група</b>: за създаване на нова група.]]></string> + <string name="recent_history">Видима история</string> + <string name="la_app_passcode">Код за достъп до приложението</string> + <string name="new_chat">Нов чат</string> + <string name="loading_chats">Чатовете се зареждат…</string> + <string name="creating_link">Линкът се създава…</string> + <string name="or_scan_qr_code">Или сканирай QR код</string> + <string name="invalid_qr_code">Невалиден QR код</string> + <string name="add_contact_tab">Добави контакт</string> + <string name="tap_to_scan">Докосни за сканиране</string> + <string name="keep_invitation_link">Запази</string> + <string name="tap_to_paste_link">Докосни за поставяне на линк за връзка</string> + <string name="search_or_paste_simplex_link">Търсене или поставяне на SimpleX линк</string> + <string name="start_chat_question">Стартирай чата?</string> + <string name="chat_is_stopped_you_should_transfer_database">Чатът е спрян. Ако вече сте използвали тази база данни на друго устройство, трябва да я прехвърлите обратно, преди да стартирате чата отново.</string> + <string name="remote_ctrl_error_bad_invitation">Настолното устройство има грешен код за връзка</string> + <string name="remote_ctrl_error_bad_version">Настолното устройство е с неподдържана версия. Моля, уверете се, че използвате една и съща версия и на двете устройства</string> + <string name="possible_slow_function_title">Бавна функция</string> + <string name="possible_slow_function_desc">Изпълнението на функцията отнема твърде много време: %1$d секунди: %2$s</string> + <string name="show_internal_errors">Покажи вътрешните грешки</string> + <string name="remote_host_disconnected_from"><![CDATA[Прекъсната е връзката с мобилното устройство <b>%s</b> с причина: %s]]></string> + <string name="remote_ctrl_was_disconnected_title">Връзката е прекъсната</string> + <string name="failed_to_create_user_invalid_title">Невалидно име!</string> + <string name="failed_to_create_user_invalid_desc">Това име е невалидно. Моля, изберете друго име.</string> + <string name="remote_host_was_disconnected_title">Връзката е прекъсната</string> + <string name="remote_ctrl_disconnected_with_reason">Прекъсната е връзката с причината: %s</string> + <string name="remote_host_error_missing"><![CDATA[Мобилното устройство <b>%s</b> липсва]]></string> + <string name="remote_host_error_inactive"><![CDATA[Мобилното устройство <b>%s</b> е неактивно]]></string> + <string name="remote_host_error_busy"><![CDATA[Мобилното устройство <b>%s</b> е заето]]></string> + <string name="remote_host_error_timeout"><![CDATA[Времето за изчакване е достигнато при свързване с мобилното устройство <b>%s</b>]]></string> + <string name="remote_host_error_bad_state"><![CDATA[Връзката с мобилното устройство <b>%s</b> е в лошо състояние]]></string> + <string name="remote_host_error_bad_version"><![CDATA[Мобилното устройство <b>%s</b> е с неподдържана версия. Моля, уверете се, че използвате една и съща версия и на двете устройства]]></string> + <string name="remote_host_error_disconnected"><![CDATA[Връзката с мобилното устройство <b>%s</b> бе прекъсната]]></string> + <string name="remote_ctrl_error_inactive">Настолното устройство е неактивно</string> + <string name="remote_ctrl_error_timeout">Времето за изчакване е достигнато при свързване с настолното устройство</string> + <string name="remote_ctrl_error_bad_state">Връзката с настолното устройство е в лошо състояние</string> + <string name="remote_ctrl_error_busy">Настолното устройство е заето</string> + <string name="remote_ctrl_error_disconnected">Връзката с настолното устройство бе прекъсната</string> + <string name="agent_critical_error_title">Критична грешка</string> + <string name="past_member_vName">Бивш член %1$s</string> + <string name="group_member_status_unknown">неизвестен статус</string> + <string name="group_member_status_unknown_short">неизвестен</string> + <string name="agent_internal_error_title">Вътрешна грешка</string> + <string name="agent_internal_error_desc">Моля, докладвайте го на разработчиците: +\n%s</string> + <string name="restart_chat_button">Рестартирай чата</string> + <string name="agent_critical_error_desc">Моля, докладвайте го на разработчиците: +\n%s +\n +\nПрепоръчително е да рестартирате приложението.</string> + <string name="developer_options_section">Опции за разработчици</string> + <string name="show_slow_api_calls">Показване на бавни API заявки</string> + <string name="v5_5_private_notes_descr">С криптирани файлове и медия.</string> + <string name="v5_5_simpler_connect_ui">Поставете линк, за да се свържете!</string> + <string name="v5_5_private_notes">Лични бележки</string> + <string name="v5_5_message_delivery">Подобрена доставка на съобщения</string> + <string name="v5_5_message_delivery_descr">С намален разход на батерията.</string> + <string name="blocked_by_admin_item_description">блокиран от админ</string> + <string name="blocked_by_admin_items_description">%d съобщения, блокирани от администратора</string> + <string name="error_creating_message">Грешка при създаване на съобщение</string> + <string name="error_deleting_note_folder">Грешка при изтриване на лични бележки</string> + <string name="clear_note_folder_question">Изчистване на лични бележки?</string> + <string name="clear_note_folder_warning">Всички съобщения ще бъдат изтрити - това не може да бъде отменено!</string> + <string name="rcv_group_event_member_blocked">блокиран %s</string> + <string name="rcv_group_event_member_unblocked">отблокиран %s</string> + <string name="snd_group_event_member_blocked">вие блокирахте %s</string> + <string name="snd_group_event_member_unblocked">вие отблокирахте %s</string> + <string name="info_row_created_at">Създаден на</string> + <string name="share_text_created_at">Създаден на: %s</string> + <string name="saved_message_title">Запазено съобщение</string> + <string name="block_for_all_question">Блокиране на член за всички?</string> + <string name="block_for_all">Блокирай за всички</string> + <string name="unblock_for_all_question">Отблокиране на член за всички?</string> + <string name="unblock_for_all">Отблокирай за всички</string> + <string name="member_blocked_by_admin">Блокиран от админ</string> + <string name="member_info_member_blocked">блокиран</string> + <string name="error_blocking_member_for_all">Грешка при блокиране на член за всички</string> + <string name="v5_5_simpler_connect_ui_descr">Лентата за търсене приема линк за връзка.</string> + <string name="v5_5_join_group_conversation">Присъединяване към групи</string> + <string name="v5_5_new_interface_languages">Унгарски и турски потребителски интерфейс</string> + <string name="v5_5_join_group_conversation_descr">Скорошна история и подобрен бот за директорията за групи.</string> + <string name="profile_update_event_member_name_changed">името на члена %1$s е променено на %2$s</string> + <string name="profile_update_event_contact_name_changed">името на контакта %1$s е променено на %2$s</string> + <string name="profile_update_event_removed_picture">премахната профилна снимка</string> + <string name="profile_update_event_set_new_picture">зададена нова профилна снимка</string> + <string name="profile_update_event_removed_address">премахнат адрес за контакт</string> + <string name="profile_update_event_set_new_address">зададен нов адрес за контакт</string> + <string name="profile_update_event_updated_profile">актуализиран профил</string> + <string name="note_folder_local_display_name">Лични бележки</string> + <string name="message_too_large">Съобщението е твърде голямо</string> + <string name="welcome_message_is_too_long">Съобщението при посрещане е твърде дълго</string> + <string name="database_migration_in_progress">Извършва се миграция на базата данни. +\nМоже да отнеме няколко минути.</string> + <string name="call_service_notification_audio_call">Аудио разговор</string> + <string name="unable_to_open_browser_title">Грешка при отваряне на браузъра</string> + <string name="unable_to_open_browser_desc">За разговори е необходим уеб браузър по подразбиране. Моля, конфигурирайте браузъра по подразбиране в системните настройки и споделете повече информация с разработчиците.</string> + <string name="call_service_notification_video_call">Видео разговор</string> + <string name="call_service_notification_end_call">Край на разговора</string> + <string name="e2ee_info_pq"><![CDATA[Съобщенията, файловете и разговорите са защитени чрез <b>квантово устойчиво e2e криптиране</b> с перфектна секретност при препращане, правдоподобно опровержение и възстановяване при взлом.]]></string> + <string name="auth_open_migration_to_another_device">Отвори екрана за мигриране</string> + <string name="migrate_from_another_device">Мигриране от друго устройство</string> + <string name="set_passphrase">Задаване на парола</string> + <string name="conn_event_enabled_pq">квантово устойчиво e2e криптиране</string> + <string name="conn_event_disabled_pq">стандартно криптиране от край до край</string> + <string name="migrate_to_device_chat_migrated">Чатът е мигриран!</string> + <string name="migrate_to_device_finalize_migration">Завършете миграцията на другото устройство.</string> + <string name="migrate_to_device_confirm_network_settings">Потвърди мрежовите настройки</string> + <string name="migrate_to_device_error_downloading_archive">Грешка при изтеглянето на архива</string> + <string name="migrate_from_device_database_init">Подготвя се качване</string> + <string name="migrate_from_device_error_exporting_archive">Грешка при експортиране на базата данни</string> + <string name="migrate_from_device_start_chat">Стартиране на чата</string> + <string name="v5_6_quantum_resistant_encryption">Квантово устойчиво криптиране</string> + <string name="v5_6_app_data_migration">Миграция на данните от приложението</string> + <string name="v5_6_app_data_migration_descr">Мигрирайте към друго устройство чрез QR код.</string> + <string name="v5_6_picture_in_picture_calls">Обаждания "картина в картина"</string> + <string name="v5_6_safer_groups">По-безопасни групи</string> + <string name="v5_6_safer_groups_descr">Администраторите могат да блокират член за всички.</string> + <string name="migrate_to_device_downloading_details">Подробности за линка се изтеглят</string> + <string name="migrate_to_device_downloading_archive">Архива се изтегля</string> + <string name="migrate_to_device_repeat_download">Повтори изтеглянето</string> + <string name="migrate_to_device_download_failed">Неуспешно изтегляне</string> + <string name="migrate_to_device_try_again">Можете да опитате още веднъж.</string> + <string name="migrate_to_device_importing_archive">Импортиране на архив</string> + <string name="migrate_to_device_import_failed">Неуспешно импортиране</string> + <string name="migrate_to_device_repeat_import">Повтори импортирането</string> + <string name="migrate_to_device_file_delete_or_link_invalid">Файлът е изтрит или линкът е невалиден</string> + <string name="migrate_to_device_confirm_network_settings_footer">Моля, потвърдете, че мрежовите настройки са правилни за това устройство.</string> + <string name="migrate_to_device_apply_onion">Приложи</string> + <string name="migrate_from_device_title">Мигрирай устройството</string> + <string name="migrate_from_device_to_another_device">Миграция към друго устройство</string> + <string name="migrate_from_device_error_saving_settings">Грешка при запазване на настройките</string> + <string name="migrate_from_device_exported_file_doesnt_exist">Експортираният файл не съществува</string> + <string name="migrate_from_device_error_uploading_archive">Грешка при качването на архива</string> + <string name="migrate_from_device_error_deleting_database">Грешка при изтриване на база данни</string> + <string name="migrate_from_device_stopping_chat">Спиране на чата</string> + <string name="migrate_from_device_chat_should_be_stopped">За да продължите, чатът трябва да бъде спрян.</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Всички ваши контакти, разговори и файлове ще бъдат сигурно криптирани и качени на парчета в конфигурираните XFTP релета.</string> + <string name="migrate_from_device_uploading_archive">Архивът се качва</string> + <string name="migrate_from_device_try_again">Можете да опитате още веднъж.</string> + <string name="migrate_from_device_creating_archive_link">Създаване на архивен линк</string> + <string name="migrate_from_device_cancel_migration">Отмени миграцията</string> + <string name="migrate_from_device_finalize_migration">Завърши миграцията</string> + <string name="migrate_from_device_or_share_this_file_link">Или сигурно споделете този линк към файла</string> + <string name="migrate_from_device_delete_database_from_device">Изтриване на базата данни от това устройство</string> + <string name="migrate_from_device_migration_complete">Миграцията е завършена</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[<b>Не трябва</b> да използвате една и съща база данни на две устройства.]]></string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Моля, обърнете внимание</b>: използването на една и съща база данни на две устройства ще наруши декриптирането на съобщенията от вашите връзки като защита на сигурността.]]></string> + <string name="migrate_from_device_verify_database_passphrase">Проверете паролата на базата данни</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Потвърдете, че помните паролата на базата данни, преди да я мигрирате.</string> + <string name="migrate_from_device_verify_passphrase">Провери паролата</string> + <string name="migrate_from_device_error_verifying_passphrase">Грешка при проверката на паролата:</string> + <string name="migrate_to_device_title">Мигрирай тук</string> + <string name="paste_archive_link">Постави архивен линк</string> + <string name="invalid_file_link">Невалиден линк</string> + <string name="migrate_to_device_migrating">Мигриране</string> + <string name="migrate_to_device_database_init">Изтеглянето се подготвя</string> + <string name="migrate_to_device_bytes_downloaded">%s изтеглени</string> + <string name="v5_6_picture_in_picture_calls_descr">Използвайте приложението по време на разговора.</string> + <string name="e2ee_info_no_pq_short">Този чат е защитен чрез криптиране от край до край.</string> + <string name="e2ee_info_pq_short">Този чат е защитен от квантово устойчиво криптиране от край до край.</string> + <string name="e2ee_info_no_pq"><![CDATA[Съобщенията, файловете и разговорите са защитени чрез <b>криптиране от край до край</b> с перфектна секретност при препращане, правдоподобно опровержение и възстановяване при взлом.]]></string> + <string name="or_paste_archive_link">Или постави архивен линк</string> + <string name="migrate_to_device_enter_passphrase">Въведи парола</string> + <string name="migrate_from_device_confirm_upload">Потвърди качването</string> + <string name="migrate_from_device_archive_and_upload">Архивиране и качване</string> + <string name="migrate_from_device_archiving_database">Архивиране на база данни</string> + <string name="migrate_from_device_bytes_uploaded">%s качени</string> + <string name="migrate_from_device_upload_failed">Неуспешно качване</string> + <string name="migrate_from_device_repeat_upload">Повтори качването</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Изберете <i>Миграция от друго устройство</i> на новото устройство и сканирайте QR кода.]]></string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Внимание: стартирането на чата на множество устройства не се поддържа и ще доведе до неуспешно изпращане на съобщения</string> + <string name="migrate_from_device_check_connection_and_try_again">Проверете вашата интернет връзка и опитайте отново</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Внимание</b>: архивът ще бъде изтрит.]]></string> + <string name="v5_6_quantum_resistant_encryption_descr">Активиране в личните чатове (БЕТА)!</string> + <string name="allow_to_send_simplex_links">Разрешаване на изпращане на SimpleX линкове.</string> + <string name="forwarded_description">препратено</string> + <string name="error_showing_desktop_notification">Грешка при показване на известие, свържете се с разработчиците.</string> + <string name="permissions_open_settings">Отвори настройки</string> + <string name="permissions_find_in_settings_and_grant">Намерете това разрешение в настройките на Android и го задайте ръчно.</string> + <string name="network_type_other">Други</string> + <string name="network_type_cellular">Мобилна мрежа</string> + <string name="network_type_no_network_connection">Няма мрежова връзка</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="network_type_ethernet">Кабелен Ethernet</string> + <string name="simplex_links_not_allowed">SimpleX линковете не са разрешени</string> + <string name="voice_messages_not_allowed">Гласовите съобщения не са разрешени</string> + <string name="permissions_grant_in_settings">Предоставяне в настройките</string> + <string name="settings_section_title_network_connection">Мрежова връзка</string> + <string name="simplex_links">SimpleX линкове</string> + <string name="prohibit_sending_simplex_links">Забранете изпращането на SimpleX линкове</string> + <string name="group_members_can_send_simplex_links">Членовете могат да изпращат SimpleX линкове.</string> + <string name="simplex_links_are_prohibited_in_group">SimpleX линкове са забранени.</string> + <string name="feature_enabled_for">Активирано за</string> + <string name="feature_roles_owners">собственици</string> + <string name="permissions_camera">Камера</string> + <string name="permissions_camera_and_record_audio">Камера и микрофон</string> + <string name="permissions_required">Предоставяне на разрешения</string> + <string name="permissions_grant">Дадени разрешение(я) за извършване на повикване</string> + <string name="permissions_record_audio">Микрофон</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="audio_device_wired_headphones">Слушалки жични</string> + <string name="network_option_rcv_concurrency">Паралелност на получаване</string> + <string name="v5_7_forward">Препращане и запазване на съобщения</string> + <string name="v5_7_call_sounds">Звуци по време на разговор</string> + <string name="saved_description">запазено</string> + <string name="saved_from_description">запазено от %s</string> + <string name="saved_chat_item_info_tab">Запазено</string> + <string name="saved_from_chat_item_info_title">Запазено от</string> + <string name="recipients_can_not_see_who_message_from">Получателят(ите) не могат да видят от кого е това съобщение.</string> + <string name="download_file">Изтегли</string> + <string name="forward_message">Препращане на съобщение…</string> + <string name="files_and_media_not_allowed">Файлове и медия не са разрешени</string> + <string name="feature_roles_all_members">всички членове</string> + <string name="feature_roles_admins">администратори</string> + <string name="forward_chat_item">Препрати</string> + <string name="audio_device_earpiece">Слушалки безжични</string> + <string name="forwarded_chat_item_info_tab">Препратено</string> + <string name="forwarded_from_chat_item_info_title">Препратено от</string> + <string name="audio_device_speaker">Високоговорител</string> + <string name="v5_7_quantum_resistant_encryption_descr">Ще бъде активирано в личните чатове!</string> + <string name="v5_7_forward_descr">Източникът на съобщението остава скрит.</string> + <string name="v5_7_network">Управление на мрежата</string> + <string name="v5_7_call_sounds_descr">При свързване на аудио и видео разговори.</string> + <string name="v5_7_network_descr">По-надеждна мрежова връзка.</string> + <string name="v5_7_new_interface_languages">Литовски потребителски интерфейс</string> + <string name="settings_section_title_profile_images">Профилни изображения</string> + <string name="v5_7_shape_profile_images">Променете формата на профилните изображения</string> + <string name="v5_7_shape_profile_images_descr">Квадрат, кръг или нещо между тях.</string> + <string name="forward_files_not_accepted_desc">%1$d файл(а) не бяха изтеглени.</string> + <string name="forward_files_in_progress_desc">%1$d файл(а) все още се изтеглят.</string> + <string name="operator_conditions_accepted">Приети условия</string> + <string name="accept_conditions">Приеми условията</string> + <string name="n_file_errors">%1$d файлова грешка(и):\n%2$s</string> + <string name="forward_files_missing_desc">%1$d файл(а) бяха изтрити.</string> + <string name="forward_files_failed_to_receive_desc">Неуспешно изтегляне на %1$d файл(а).</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s съобщения не са препратени</string> + <string name="display_name_accepted_invitation">приета покана</string> + <string name="n_other_file_errors">%1$d друга(и) файлова(и) грешка(и).</string> + <string name="a_plus_b">a + b</string> + <string name="onboarding_network_about_operators">За операторите</string> + <string name="color_primary_variant2">Допълнителен акцент 2</string> + <string name="no_media_servers_configured_for_private_routing">Няма сървъри за получаване на файлове.</string> + <string name="private_routing_error">Грешка при поверително рутиране</string> + <string name="file_not_approved_descr">Без Tor или VPN вашият IP адрес ще бъде видим за тези XFTP релета:\n%1$s.</string> + <string name="forward_alert_title_messages_to_forward">Препращане на %1$s съобщението(ята)?</string> + <string name="forward_multiple">Препращане на съобщенията…</string> + <string name="compose_forward_messages_n">Препращане на %1$s съобщения</string> + <string name="network_smp_proxy_fallback_allow_downgrade">Позволи понижаване</string> + <string name="network_smp_proxy_fallback_allow_protected">Когато IP-то е скрито</string> + <string name="network_smp_proxy_fallback_prohibit_description">НЕ изпращайте съобщения директно, дори ако вашият или получаващият сървър не поддържат поверително рутиране.</string> + <string name="operator_use_for_messages_private_routing">За поверителното рутиране</string> + <string name="smp_proxy_error_connecting">Грешка при свързване към препращащият сървър %1$s. Моля, опитайте по-късно.</string> + <string name="private_routing_show_message_status">Покажи състоянието на съобщението</string> + <string name="private_routing_explanation">За да защити вашия IP адрес, поверително рутиране използва вашите SMP сървъри за доставяне на съобщения.</string> + <string name="forward_alert_forward_messages_without_files">Препращане на съобщенията без файловете?</string> + <string name="network_smp_proxy_mode_unknown">Неизвестни сървъри</string> + <string name="settings_section_title_files">ФАЙЛОВЕ</string> + <string name="chat_list_always_visible">Показване на списъка на чатовете в нов прозорец</string> + <string name="color_mode_system">Системна</string> + <string name="color_mode_dark">Тъмна</string> + <string name="theme_black">Черна</string> + <string name="reset_single_color">Нулиране на цветовете</string> + <string name="v6_0_private_routing_descr">То защитава вашия IP адрес и връзки.</string> + <string name="smp_proxy_error_broker_host">Адресът на препращащия сървър е несъвместим с мрежовите настройки: %1$s.</string> + <string name="smp_proxy_error_broker_version">Версията на препращащия сървър е несъвместима с мрежовите настройки: %1$s.</string> + <string name="proxy_destination_error_failed_to_connect">Препращащият сървър %1$s не успя да се свърже с получаващия сървър %2$s. Моля, опитайте по-късно.</string> + <string name="proxy_destination_error_broker_version">Версията на получаващия сървър %1$s е несъвместима с препращащия сървър %2$s.</string> + <string name="snd_error_relay">Грешка от получаващия сървър: %1$s</string> + <string name="srv_error_host">Адресът на сървъра е несъвместим с мрежовите настройки.</string> + <string name="message_forwarded_title">Съобщението е препратено</string> + <string name="message_forwarded_desc">Все още няма директна връзка, съобщението е препратено от администратора.</string> + <string name="theme_destination_app_theme">Тема на приложението</string> + <string name="v5_8_private_routing_descr">Защитете вашия IP адрес от реле сървърите за съобщения, избрани от вашите контакти.\nАктивирайте в настройките *Мрежа и сървъри*.</string> + <string name="v5_8_private_routing">Поверително рутиране на съобщенията 🚀</string> + <string name="v6_1_forward_many_messages_descr">Препращайте до 20 съобщения наведнъж.</string> + <string name="servers_info_proxied_servers_section_footer">Не сте свързани с тези сървъри. Поверителното рутиране се използва за доставяне на съобщенията до тях.</string> + <string name="snd_error_auth">Грешен ключ или неизвестна връзка - най-вероятно тази връзка е изтрита.</string> + <string name="snd_error_quota">Капацитетът е надвишен - получателят не е получил предишно изпратените съобщения.</string> + <string name="snd_error_proxy">Препращащ сървър: %1$s\nГрешка: %2$s</string> + <string name="srv_error_version">Версията на сървъра е несъвместима с мрежовите настройки.</string> + <string name="protect_ip_address">Защити IP адреса</string> + <string name="settings_section_title_private_message_routing">ПОВЕРИТЕЛНО РУТИРАНЕ НА СЪОБЩЕНИЯ</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">Приложението ще поиска потвърждение за изтегляния от неизвестни файлови сървъри (с изключение на .onion сървъри или когато SOCKS прокси е активирано).</string> + <string name="ci_status_other_error">Грешка: %1$s</string> + <string name="forward_files_not_accepted_receive_files">Изтегляне</string> + <string name="forward_files_messages_deleted_after_selection_desc">Съобщенията бяха изтрити, след като ги избрахте.</string> + <string name="forward_alert_title_nothing_to_forward">Нищо за препращане!</string> + <string name="network_smp_proxy_mode_private_routing">Поверително рутиране</string> + <string name="network_smp_proxy_mode_unprotected">Незащитен</string> + <string name="network_smp_proxy_fallback_allow_protected_description">Изпращайте съобщения директно, когато IP адресът е защитен и вашият или получаващият сървър не поддържа поверително рутиране.</string> + <string name="network_smp_proxy_fallback_allow_description">Изпращайте съобщения директно, когато вашият или получаващият сървър не поддържат поверително рутиране.</string> + <string name="network_smp_proxy_mode_always_description">Винаги използвай поверително рутиране.</string> + <string name="network_smp_proxy_mode_unknown_description">Използвай поверително рутиране с неизвестни сървъри.</string> + <string name="settings_section_title_user_theme">Тема на профила</string> + <string name="color_mode">Цветен режим</string> + <string name="dark_mode_colors">Цветове за тъмен режим</string> + <string name="no_message_servers_configured_for_private_routing">Няма сървъри за поверително рутиране на съобщения.</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Без Tor или VPN вашият IP адрес ще бъде видим за файловите сървъри.</string> + <string name="message_delivery_warning_title">Предупреждение за доставката на съобщението</string> + <string name="network_smp_proxy_mode_never_description">НЕ използвайте поверително рутиране.</string> + <string name="settings_section_title_chat_colors">Цветове на чата</string> + <string name="error_forwarding_messages">Грешка при препращане на съобщенията</string> + <string name="proxy_destination_error_broker_host">Адресът на получаващия сървър %1$s е несъвместим с настройките на препращащия сървър %2$s.</string> + <string name="file_not_approved_title">Неизвестни сървъри!</string> + <string name="snd_error_expired">Проблеми с мрежата - съобщението е отказано след много опити за изпращане.</string> + <string name="snd_error_proxy_relay">Препращащ сървър: %1$s\nГрешка от получаващия сървър: %2$s</string> + <string name="maximum_message_size_reached_forwarding">Може да копирате и намалите размера на съобщението, за да бъде изпратено.</string> + <string name="network_smp_proxy_mode_always">Винаги</string> + <string name="network_smp_proxy_fallback_prohibit">Не</string> + <string name="network_smp_proxy_mode_never">Никога</string> + <string name="update_network_smp_proxy_mode_question">Режим за рутиране на съобщения</string> + <string name="network_smp_proxy_mode_unprotected_description">Използвай поверително рутиране с неизвестни сървъри, когато IP адресът не е защитен.</string> + <string name="color_mode_light">Светла</string> + <string name="network_smp_proxy_fallback_allow">Да</string> + <string name="settings_section_title_chat_theme">Тема на чата</string> + <string name="color_received_quote">Получен отговор</string> + <string name="color_sent_quote">Изпратен отговор</string> + <string name="theme_remove_image">Премахни изображението</string> + <string name="color_wallpaper_background">Тапетен фон</string> + <string name="color_wallpaper_tint">Тапетен акцент</string> + <string name="wallpaper_preview_hello_alice">Добър ден!</string> + <string name="wallpaper_preview_hello_bob">Добро утро!</string> + <string name="wallpaper_scale_fill">Запълване</string> + <string name="wallpaper_scale_repeat">Повтори</string> + <string name="wallpaper_scale">Мащаб</string> + <string name="please_try_later">Моля, опитайте по-късно.</string> + <string name="file_error_no_file">Файлът не е намерен - най-вероятно файлът е бил изтрит или отказан.</string> + <string name="file_error_relay">Грешка на файловия сървър: %1$s</string> + <string name="file_error_auth">Грешен ключ или неизвестен адрес на файлово парче - най-вероятно файлът е изтрит.</string> + <string name="share_text_file_status">Файлов статус: %s</string> + <string name="all_users">Всички профили</string> + <string name="servers_info_private_data_disclaimer">Започвайки от %s.\nВсички данни се съхраняват поверително на вашето устройство..</string> + <string name="servers_info_reconnect_server_message">Свържете отново сървъра, за да принудите доставката на съобщенията. Това използва допълнителен трафик.</string> + <string name="servers_info_reset_stats_alert_error_title">Грешка при нулиране на статистиката</string> + <string name="scan_paste_link">Сканирай / Постави линк</string> + <string name="xftp_servers_configured">Конфигурирани XFTP сървъри</string> + <string name="appearance_font_size">Размер на шрифта</string> + <string name="share_text_message_status">Статус на съобщението: %s</string> + <string name="chat_theme_set_default_theme">Задай тема по подразбиране</string> + <string name="v5_8_safe_files_descr">Потвърдете файловете от неизвестни сървъри.</string> + <string name="servers_info_detailed_statistics_sent_messages_header">Изпратени съобщения</string> + <string name="error_initializing_web_view">Грешка при стартиране на WebView. Актуализирайте системата си до новата версия. Моля, свържете се с разработчиците.\nГрешка: %s</string> + <string name="xftp_servers_other">Други XFTP сървъри</string> + <string name="appearance_zoom">Мащабиране</string> + <string name="chat_theme_apply_to_all_modes">Всички цветови режими</string> + <string name="chat_theme_apply_to_light_mode">Светъл режим</string> + <string name="v5_8_message_delivery_descr">С намален разход на батерията.</string> + <string name="file_error">Файлова грешка</string> + <string name="info_row_message_status">Статус на съобщението</string> + <string name="info_row_file_status">Файлов статус</string> + <string name="v5_8_chat_themes">Нови чат теми</string> + <string name="v5_8_safe_files">Безопасно получаване на файлове</string> + <string name="v5_8_persian_ui">Персийски потребителски интерфейс</string> + <string name="servers_info_missing">Няма информация, опитайте да презаредите</string> + <string name="servers_info_target">Показване на информация за</string> + <string name="servers_info">Информация за сървърите</string> + <string name="servers_info_files_tab">Файлове</string> + <string name="servers_info_transport_sessions_section_header">Транспортни сесии</string> + <string name="servers_info_modal_error_title">Грешка</string> + <string name="message_queue_info">Информация за опашката за съобщения</string> + <string name="message_queue_info_none">няма</string> + <string name="message_queue_info_server_info">информация за опашката на сървъра: %1$s\n\nпоследно получено съобщение: %2$s</string> + <string name="servers_info_details">Подробности</string> + <string name="servers_info_reconnect_servers_title">Повторно свързване на сървърите?</string> + <string name="servers_info_previously_connected_servers_section_header">Предишни свързани сървъри</string> + <string name="servers_info_reconnect_servers_error">Грешка при повторното свързване на сървърите</string> + <string name="servers_info_reconnect_server_error">Грешка при повторното свързване на сървъра</string> + <string name="servers_info_reconnect_all_servers_button">Повторно свързване на всички сървъри</string> + <string name="servers_info_reset_stats_alert_confirm">Нулиране</string> + <string name="servers_info_downloaded">Свалено</string> + <string name="cannot_share_message_alert_text">Избраните чат настройки забраняват това съобщение.</string> + <string name="member_info_member_inactive">неактивен</string> + <string name="chat_theme_apply_to_dark_mode">Тъмен режим</string> + <string name="servers_info_subscriptions_connections_subscribed">Активни връзки</string> + <string name="servers_info_subscriptions_connections_pending">Предстоящи</string> + <string name="servers_info_connected_servers_section_header">Свързани сървъри</string> + <string name="servers_info_subscriptions_total">Общо</string> + <string name="member_info_member_disabled">деактивирано</string> + <string name="servers_info_reconnect_servers_message">Свържете се отново с всички свързани сървъри, за да принудите доставката на съобщенията. Това използва допълнителен трафик.</string> + <string name="remote_ctrl_connection_stopped_identity_desc">Този линк е използван с друго мобилно устройство, моля, създайте нова линк на настолното устройство.</string> + <string name="servers_info_sessions_connected">Свързан</string> + <string name="servers_info_messages_received">Получени съобщения</string> + <string name="servers_info_subscriptions_section_header">Покритие на съобщенията</string> + <string name="servers_info_proxied_servers_section_header">Сървъри през прокси</string> + <string name="servers_info_reset_stats">Нулиране на всички статистически данни</string> + <string name="servers_info_reset_stats_alert_title">Нулиране на всички статистически данни?</string> + <string name="servers_info_reset_stats_alert_message">Статистиката на сървърите ще бъде нулирана - това не може да бъде отменено!</string> + <string name="servers_info_detailed_statistics">Подробна статистика</string> + <string name="member_inactive_title">Членът е неактивен</string> + <string name="member_inactive_desc">Съобщението може да бъде доставено по-късно, ако членът стане активен.</string> + <string name="cannot_share_message_alert_title">Не може да се изпрати съобщението</string> + <string name="subscription_percentage">Покажи процент</string> + <string name="servers_info_reconnect_server_title">Повторно свързване на сървъра?</string> + <string name="servers_info_uploaded">Качено</string> + <string name="servers_info_detailed_statistics_sent_messages_total">Изпратени общо</string> + <string name="copy_error">Грешка при копиране</string> + <string name="network_error_broker_host_desc">Адресът на сървъра е несъвместим с мрежовите настройки: %1$s.</string> + <string name="network_error_broker_version_desc">Версията на сървъра е несъвместима с вашето приложение: %1$s.</string> + <string name="servers_info_sessions_errors">Грешки</string> + <string name="servers_info_statistics_section_header">Статистика</string> + <string name="servers_info_sessions_connecting">Свързване</string> + <string name="servers_info_messages_sent">Изпратени съобщения</string> + <string name="info_row_debug_delivery">Отстраняване на грешки за доставка</string> + <string name="wallpaper_scale_fit">Изпълване</string> + <string name="wallpaper_advanced_settings">Разширени настройки</string> + <string name="chat_theme_apply_to_mode">Приложи към</string> + <string name="chat_theme_reset_to_app_theme">Нулиране с темата за приложението</string> + <string name="chat_theme_reset_to_user_theme">Нулиране с потребителска тема</string> + <string name="v5_8_chat_themes_descr">Направете вашите чатове да изглеждат различно!</string> + <string name="current_user">Текущ профил</string> + <string name="reconnect">Повторно свързване</string> + <string name="servers_info_detailed_statistics_received_messages_header">Получени съобщения</string> + <string name="servers_info_detailed_statistics_received_total">Получени общо</string> + <string name="servers_info_detailed_statistics_receive_errors">Получени грешки</string> + <string name="remote_ctrl_connection_stopped_desc">Моля, проверете дали мобилното и настолното устройство са свързани към една и съща локална мрежа и дали защитната стена на настолното устройство позволява връзката.\nМоля, споделете всички други проблеми с разработчиците.</string> + <string name="temporary_file_error">Временна файлова грешка</string> + <string name="smp_servers_configured">Конфигурирани SMP сървъри</string> + <string name="smp_servers_other">Други SMP сървъри</string> + <string name="v5_8_message_delivery">Подобрена доставка на съобщения</string> + <string name="attempts_label">опити</string> + <string name="expired_label">изтекли</string> + <string name="other_label">други</string> + <string name="duplicates_label">дубликати</string> + <string name="acknowledged">Потвърден</string> + <string name="acknowledgement_errors">Грешки при потвърждението</string> + <string name="connections">Връзки</string> + <string name="servers_info_starting_from">Започвайки от %s.</string> + <string name="xftp_server">XFTP сървър</string> + <string name="sent_directly">Изпратени директно</string> + <string name="sent_via_proxy">Изпратени чрез прокси</string> + <string name="proxied">Чрез прокси</string> + <string name="smp_server">SMP сървър</string> + <string name="send_errors">Изпрати грешки</string> + <string name="other_errors">други грешки</string> + <string name="decryption_errors">грешки при декриптиране</string> + <string name="deletion_errors">Грешки при изтриване</string> + <string name="created">Създаден</string> + <string name="completed">Завършен</string> + <string name="deleted">Изтрит</string> + <string name="secured">Защитен</string> + <string name="subscribed">Абониран</string> + <string name="subscription_results_ignored">Абонаменти игнорирани</string> + <string name="subscription_errors">Абонаментни грешки</string> + <string name="chunks_uploaded">Качени парчета</string> + <string name="uploaded_files">Качени файлове</string> + <string name="upload_errors">Грешки при качване</string> + <string name="size">Размер</string> + <string name="chunks_deleted">Парчета изтрити</string> + <string name="chunks_downloaded">Изтеглени парчета</string> + <string name="downloaded_files">Изтеглени файлове</string> + <string name="toolbar_settings">Настройки</string> + <string name="app_check_for_updates_disabled">Деактивирано</string> + <string name="app_check_for_updates_stable">Стабилен канал</string> + <string name="app_check_for_updates_beta">Бета канал</string> + <string name="app_check_for_updates_update_available">Налична актуализация: %s</string> + <string name="app_check_for_updates_download_started">Изтегляне на актуализация на приложението, не затваряйте приложението</string> + <string name="create_address_button">Създай</string> + <string name="privacy_media_blur_radius_strong">Много</string> + <string name="settings_section_title_message_shape">Форма на съобщение</string> + <string name="allow_calls_question">Позволи обаждания?</string> + <string name="calls_prohibited_alert_title">Обажданията са забранени!</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">Моля, поискайте вашия контакт да позволи обажданията.</string> + <string name="v6_0_privacy_blur">Размазване за по-добра поверителност.</string> + <string name="v6_0_delete_many_messages_descr">Изтрийте до 20 съобщения наведнъж.</string> + <string name="v6_0_chat_list_media">Възпроизвеждане от чат списъка.</string> + <string name="v6_1_better_security_descr">SimpleX протоколите, прегледани от Trail of Bits.</string> + <string name="v6_1_better_security">По-добра сигурност ✅</string> + <string name="compose_save_messages_n">%1$s съобщения се запазват</string> + <string name="keep_conversation">Запази разговора</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">Използвай различни прокси идентификационни данни за всяка връзка.</string> + <string name="network_proxy_incorrect_config_title">Грешка за запазване на прокси</string> + <string name="app_check_for_updates_download_completed_title">Актуализацията на приложението е изтеглена</string> + <string name="cant_call_contact_alert_title">Обаждането на контакта не е позволено</string> + <string name="cant_call_contact_connecting_wait_alert_text">Тече свързване с контакт, моля изчакайте или проверете по-късно!</string> + <string name="cant_call_member_alert_title">Обаждането на груповия член е не е позволено</string> + <string name="system_mode_toast">Системен режим</string> + <string name="v6_0_your_contacts_descr">Архивирайте контактите, за да разговаряте по-късно.</string> + <string name="invite_friends_short">Покани</string> + <string name="chat_database_exported_not_all_files">Някои файлове не са били експортирани</string> + <string name="chat_database_exported_title">Базата данни е експортирана</string> + <string name="error_parsing_uri_title">Невалиден линк</string> + <string name="error_parsing_uri_desc">Моля, проверете, че SimpleX линкът е правилен.</string> + <string name="only_delete_conversation">Изтрий само разговора</string> + <string name="network_socks_proxy">SOCKS прокси</string> + <string name="v6_0_upgrade_app">Автоматични актуализации на приложението</string> + <string name="network_options_save_and_reconnect">Запази и се свържи отново</string> + <string name="v6_0_reachable_chat_toolbar">Достъпен панел</string> + <string name="v6_0_reachable_chat_toolbar_descr">Използвайте приложението с една ръка.</string> + <string name="v6_0_connect_faster_descr">Свържете се с приятелите си по-бързо.</string> + <string name="v6_0_connection_servers_status">Контролирайте вашата мрежа</string> + <string name="v6_0_upgrade_app_descr">Изтеглете новите версии от GitHub.</string> + <string name="v6_0_increase_font_size">Увеличете размера на шрифта.</string> + <string name="v6_1_better_calls">По-добри обаждания</string> + <string name="v6_1_better_calls_descr">Превключете аудио и видео по време на разговора.</string> + <string name="no_filtered_contacts">Няма филтрирани контакти</string> + <string name="contact_list_header_title">Вашите контакти</string> + <string name="chat_database_exported_save">Можете да запазите експортирания архив.</string> + <string name="chat_database_exported_migrate">Можете да мигрирате експортираната база данни.</string> + <string name="one_hand_ui_change_instruction">Можете да го промените в настройките за Изглед.</string> + <string name="network_option_tcp_connection">TCP връзка</string> + <string name="v6_0_connection_servers_status_descr">Състояние на връзката и сървърите.</string> + <string name="delete_members_messages__question">Изтриване на %d съобщения на членовете?</string> + <string name="confirm_delete_contact_question">Потвърди изтриването на контакта?</string> + <string name="network_proxy_username">Потребителско име</string> + <string name="app_check_for_updates">Провери за актуализации</string> + <string name="privacy_media_blur_radius_off">Изключено</string> + <string name="privacy_media_blur_radius_medium">Средно</string> + <string name="privacy_media_blur_radius_soft">Малко</string> + <string name="settings_message_shape_corner">Ъгъл</string> + <string name="settings_message_shape_tail">Опашка</string> + <string name="cant_call_contact_deleted_alert_text">Контактът е изтрит..</string> + <string name="you_need_to_allow_calls">Трябва да разрешите на вашия контакт да може да ви се обажда, за да можете и вие да се обаждате.</string> + <string name="paste_link">Постави линк</string> + <string name="switching_profile_error_message">Връзката ви беше преместена към %s, но възникна неочаквана грешка при пренасочването ви към профила.</string> + <string name="network_session_mode_session">Сесия на приложението</string> + <string name="network_session_mode_server">Сървър</string> + <string name="app_check_for_updates_button_download">Изтегли %s (%s)</string> + <string name="app_check_for_updates_button_skip">Пропусни тази версия</string> + <string name="app_check_for_updates_notice_title">Провери за актуализации</string> + <string name="settings_section_title_chat_database">БАЗА ДАННИ</string> + <string name="one_hand_ui_card_title">Превключване на чат списъка:</string> + <string name="you_can_still_send_messages_to_contact">Можете да изпращате съобщения до %1$s от архивираните контакти.</string> + <string name="chat_bottom_bar">Достъпен панел</string> + <string name="cant_send_message_to_member_alert_title">Изпращането на съобщения на груповия член не е налично</string> + <string name="error_initializing_web_view_wrong_arch">Грешка при стартиране на WebView. Уверете се, че сте инсталирали WebView и поддържаната архитектура е ARM64.\nГрешка: %s</string> + <string name="delete_messages_cannot_be_undone_warning">Съобщенията ще бъдат изтрити - това не може да бъде отменено!</string> + <string name="moderate_messages_will_be_deleted_warning">Съобщенията ще бъдат изтрити за всички членове.</string> + <string name="moderate_messages_will_be_marked_warning">Съобщенията ще бъдат маркирани като модерирани за всички членове.</string> + <string name="selected_chat_items_selected_n">Избрано %d</string> + <string name="selected_chat_items_nothing_selected">Нищо не е избрано</string> + <string name="compose_message_placeholder">Съобщение</string> + <string name="info_view_open_button">отвори</string> + <string name="info_view_message_button">съобщение</string> + <string name="info_view_call_button">обаждане</string> + <string name="delete_contact_cannot_undo_warning">Контактът ще бъде изтрит - това не може да бъде отменено!</string> + <string name="info_view_connect_button">свързване</string> + <string name="delete_without_notification">Изтрий без известие</string> + <string name="conversation_deleted">Разговорът е изтрит!</string> + <string name="you_can_still_view_conversation_with_contact">Все още ще можете да видите разговора с %1$s в списъка с чатовете.</string> + <string name="new_chat_share_profile">Сподели профил</string> + <string name="select_chat_profile">Избери чат профил</string> + <string name="switching_profile_error_title">Грешка при превключване на профил</string> + <string name="action_button_add_members">Покани</string> + <string name="icon_descr_sound_muted">Звукът е заглушен</string> + <string name="appearance_app_toolbars">Панели на приложението</string> + <string name="cant_call_member_send_message_alert_text">Изпратете съобщение за да се активират обажданията.</string> + <string name="info_view_search_button">търсене</string> + <string name="info_view_video_button">видео</string> + <string name="contact_deleted">Контактът е изтрит!</string> + <string name="deleted_chats">Архивирани контакти</string> + <string name="one_hand_ui">Достъпни панели</string> + <string name="open_server_settings_button">Отвори настройките на сървъра</string> + <string name="chat_database_exported_continue">Продължи</string> + <string name="app_check_for_updates_button_open">Отвори местоположението на файла</string> + <string name="app_check_for_updates_button_install">Инсталиране на актуализация</string> + <string name="app_check_for_updates_button_remind_later">Напомни по-късно</string> + <string name="app_check_for_updates_installed_successfully_title">Инсталирането е успешно</string> + <string name="app_check_for_updates_installed_successfully_desc">Моля, рестартирайте приложението.</string> + <string name="app_check_for_updates_canceled">Изтеглянето на актуализацията е отменено</string> + <string name="migrate_from_device_remove_archive_question">Премахни архив?</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">Каченият архив на базата данни ще бъде окончателно премахнат от сървърите.</string> + <string name="call_desktop_permission_denied_title">За да извършвате обаждания, разрешете да използва микрофона. Прекратете разговора и опитайте да се обадите отново.</string> + <string name="call_desktop_permission_denied_chrome">Натиснете бутона за информация близо до адресното поле, за да разрешите използването на микрофона.</string> + <string name="call_desktop_permission_denied_safari">Отворете Safari Настройки / Уеб страници / Микрофон, след което изберете Позволи за localhost.</string> + <string name="v6_0_new_chat_experience">Нов интерфейс 🎉</string> + <string name="v6_0_new_media_options">Нови медийни опции</string> + <string name="v6_1_better_user_experience">Подобрен интерфейс</string> + <string name="select_verb">Избери</string> + <string name="delete_messages_mark_deleted_warning">Съобщенията ще бъдат маркирани за изтриване. Получателят(ите) ще могат да разкрият тези съобщения.</string> + <string name="privacy_media_blur_radius">Размазване на медия</string> + <string name="message_servers">Сървъри за съобщения</string> + <string name="new_message">Ново съобщение</string> + <string name="media_and_file_servers">Сървъри за медия и файлове</string> + <string name="network_proxy_auth">Прокси идентификация</string> + <string name="network_proxy_random_credentials">Използвай случайни идентификационни данни</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">Използвай различни прокси идентификационни данни за всеки профил.</string> + <string name="network_proxy_auth_mode_no_auth">Не използвай идентификационни данни с прокси.</string> + <string name="network_proxy_auth_mode_username_password">Вашите идентификационни данни могат да бъдат изпратени некриптирани.</string> + <string name="network_proxy_password">Парола</string> + <string name="network_proxy_incorrect_config_desc">Уверете се, че прокси конфигурацията е правилна.</string> + <string name="network_session_mode_session_description">Нови идентификационни данни за SOCKS ще се използват всеки път, когато стартирате приложението.</string> + <string name="network_session_mode_server_description">Ще се използват нови SOCKS идентификационни данни за всеки сървър.</string> + <string name="app_check_for_updates_notice_desc">За да бъдете уведомени за новите версии, включете периодичната проверка за стабилни или бета версии.</string> + <string name="app_check_for_updates_notice_disable">Деактивиране</string> + <string name="reset_all_hints">Нулирай всички съвети</string> + <string name="download_errors">Грешки при изтегляне</string> + <string name="server_address">Сървърен адрес</string> + <string name="no_message_servers_configured">Няма сървъри за съобщения.</string> + <string name="no_message_servers_configured_for_receiving">Няма сървъри за получаване на съобщения.</string> + <string name="for_chat_profile">За чат профил %s:</string> + <string name="errors_in_servers_configuration">Грешки в конфигурацията на сървърите.</string> + <string name="connection_error_quota">Недоставени съобщения</string> + <string name="connection_error_quota_desc">Връзката достигна лимита за недоставени съобщения, вашият контакт може да е офлайн.</string> + <string name="share_simplex_address_on_social_media">Сподели SimpleX адресът в социалните мрежи.</string> + <string name="connection_security">Сигурност на връзката</string> + <string name="to_protect_against_your_link_replaced_compare_codes">За да се предпазите от подмяна на вашия линк, можете да сравните кодовете за сигурност на контакта.</string> + <string name="for_social_media">За социалните мрежи</string> + <string name="simplex_address_or_1_time_link">SimpleX адрес или еднократен линк?</string> + <string name="create_1_time_link">Създай еднократен линк</string> + <string name="onboarding_network_operators_app_will_use_different_operators">Приложението защитава вашата поверителност, като използва различни оператори за всеки разговор.</string> + <string name="onboarding_network_operators_app_will_use_for_routing">Например, ако контактът ви получава съобщения чрез SimpleX Chat сървър, приложението ви ще ги достави чрез Flux сървър.</string> + <string name="onboarding_choose_server_operators">Сървърни оператори</string> + <string name="onboarding_network_operators">Мрежови оператори</string> + <string name="onboarding_network_operators_review_later">Преглед по-късно</string> + <string name="onboarding_network_operators_conditions_you_can_configure">Можете да конфигурирате операторите в Мрежа и сървъри.</string> + <string name="operator_conditions_accepted_for_enabled_operators_on">Условията ще бъдат автоматично приети за активираните оператори на: %s.</string> + <string name="your_servers">Вашите сървъри</string> + <string name="operator_conditions_will_be_accepted_on">Условията ще бъдат приети на: %s.</string> + <string name="operator_use_operator_toggle_description">Използвай сървърите</string> + <string name="use_servers_of_operator_x">Използвай %s</string> + <string name="operator_conditions_accepted_for_some"><![CDATA[Условията вече са приети за следния оператор(и): <b>%s</b>.]]></string> + <string name="view_conditions">Преглед на условията</string> + <string name="operator_conditions_of_use">Условия за ползване</string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[За да използвате сървърите на <b>%s</b>, приемете условията за използване.]]></string> + <string name="operator_use_for_sending">За изпращане</string> + <string name="operator_added_message_servers">Добавени сървъри за съобщения</string> + <string name="operator_use_for_files">Използвай за файлове</string> + <string name="v6_2_improved_chat_navigation_descr">- Отворете чата при първото непрочетено съобщение.\n- Преминете към цитирани съобщения.</string> + <string name="update_network_smp_proxy_fallback_question">Резервно рутиране на съобщения</string> + <string name="debug_logs">Активирай логовете</string> + <string name="add_your_team_members_to_conversations">Добавете членовете на вашия екип към разговорите.</string> + <string name="strikethrough_text">задраскан</string> + <string name="onboarding_network_operators_continue">Продължи</string> + <string name="button_add_friends">Добави приятели</string> + <string name="operators_conditions_accepted_for"><![CDATA[ Условия са приети за оператора(ите): <b>%s</b>.]]></string> + <string name="operator_website">Уеб страница</string> + <string name="operator_conditions_accepted_on">Условия, приети на: %s.</string> + <string name="remote_hosts_section">Дистанционни мобилни устройства</string> + <string name="network_preset_servers_title">Предварително зададени сървъри</string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[Ще бъдат приети условия за оператора(ите): <b>%s</b>.]]></string> + <string name="operator_open_changes">Отвори промените</string> + <string name="error_updating_server_title">Грешка при актуализиране на сървъра</string> + <string name="error_server_protocol_changed">Сървърният протокол е променен.</string> + <string name="error_adding_server">Грешка при добавяне на сървър</string> + <string name="direct_messages_are_prohibited_in_chat">Личните съобщения между членовете са забранени в този чат.</string> + <string name="v6_1_customizable_message_descr">Персонализирана форма на съобщенията.</string> + <string name="v6_1_message_dates_descr">По-добри дати на съобщението.</string> + <string name="v6_2_network_decentralization_enable_flux">Активиране на flux</string> + <string name="v6_2_network_decentralization_enable_flux_reason">за по-добра поверителност на метаданните.</string> + <string name="v6_2_improved_chat_navigation">Подобрена навигация в чата</string> + <string name="error_saving_database">Грешка при запазване на базата данни</string> + <string name="chat_main_profile_sent">Вашият профил ще бъде изпратен до членовете</string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Същите условия ще се прилагат за оператора(ите): <b>%s</b>.]]></string> + <string name="operator_use_for_messages_receiving">За получаване</string> + <string name="operator_added_xftp_servers">Добавени медийни и файлови сървъри</string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[Същите условия ще се прилагат за оператора <b>%s</b>.]]></string> + <string name="connection_not_ready">Връзката не е готова.</string> + <string name="operator">Оператор</string> + <string name="view_updated_conditions">Вижте актуализираните условия</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Xiaomi устройства </b>: моля, активирайте Autostart в системните настройки, за да работят известията.]]></string> + <string name="message_deleted_or_not_received_error_title">Няма съобщение</string> + <string name="or_to_share_privately">Или да се сподели лично</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[Всички съобщения и файлове се изпращат <b>криптирани от край до край</b>, с постквантова сигурност в директните съобщения.]]></string> + <string name="onboarding_notifications_mode_off_desc_short">Без фонова услуга</string> + <string name="onboarding_notifications_mode_periodic_desc_short">Проверявай за съобщения на всеки 10 минути</string> + <string name="onboarding_notifications_mode_service_desc_short">Приложението винаги работи във фонов режим</string> + <string name="onboarding_notifications_mode_battery">Известия и батерия</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">Условията ще бъдат приети за активираните оператори след 30 дни.</string> + <string name="onboarding_network_operators_configure_via_settings">Можете да конфигурирате сървърите през настройките.</string> + <string name="onboarding_network_operators_update">Актуализация</string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Ще се приемат условия за оператора(ите): <b>%s</b>.]]></string> + <string name="operator_use_for_messages">Използвай за съобщения</string> + <string name="direct_messages_are_prohibited">Личните съобщения между членовете са забранени.</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Вече сте свързани с <b>%1$s</b>.]]></string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">SimpleX адресът и еднократните линкове за връзки са безопасни за споделяне чрез всеки месинджър.</string> + <string name="you_can_set_connection_name_to_remember">Можете да зададете име на връзка, за да запомните с кого е споделена.</string> + <string name="server_added_to_operator__name">Сървърът е добавен към оператор %s.</string> + <string name="error_updating_chat_tags">Грешка при актуализирането на чат списъка</string> + <string name="error_creating_chat_tags">Грешка при създаването на чат списъка</string> + <string name="message_deleted_or_not_received_error_desc">Това съобщение е изтрито или не е получено.</string> + <string name="error_loading_chat_tags">Грешка при зареждането на чат списъка</string> + <string name="no_chats_in_list">Няма чатове в списъка %s.</string> + <string name="no_unread_chats">Няма непрочетени чатове</string> + <string name="address_creation_instruction">Докосни Създаване на SimpleX адрес в менюто, за да го създадете по-късно.</string> + <string name="no_chats">Няма чатове</string> + <string name="no_chats_found">Няма намерени чатове</string> + <string name="chat_list_favorites">Любими</string> + <string name="chat_list_contacts">Контакти</string> + <string name="chat_list_groups">Групи</string> + <string name="chat_list_add_list">Добави списък</string> + <string name="open_with_app">Отвори с %s</string> + <string name="sync_connection_confirm">Поправи</string> + <string name="encryption_renegotiation_in_progress">В ход е предоговаряне на криптирането.</string> + <string name="create_list">Създай списък</string> + <string name="add_to_list">Добави към списъка</string> + <string name="save_list">Запази списъка</string> + <string name="duplicated_list_error">Името на списъка и емотиконите трябва да са различни за всички списъци.</string> + <string name="delete_chat_list_warning">Всички чатове ще бъдат премахнати от списъка %s и списъкът ще бъде изтрит</string> + <string name="edit_chat_list_menu_action">Редактирай</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[еднократният линк може да се използва <i>само с един контакт</i> - споделете лично или чрез произволен месинджър.]]></string> + <string name="address_or_1_time_link">Адрес или еднократен линк?</string> + <string name="smp_servers_new_server">Нов сървър</string> + <string name="operator_servers_title">%s сървъри</string> + <string name="operator_conditions_failed_to_load">Текстът на текущите условия не може да бъде зареден, можете да прегледате условията през този линк:</string> + <string name="error_server_operator_changed">Сървърният оператор е променен.</string> + <string name="invite_to_chat_button">Покана за чат</string> + <string name="delete_chat_for_self_cannot_undo_warning">Чатът ще бъде изтрит за вас - това не може да бъде отменено!</string> + <string name="button_leave_chat">Напусни чата</string> + <string name="button_delete_chat">Изтрий чата</string> + <string name="delete_chat_question">Изтрий чата?</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">Чатът ще бъде изтрит за всички членове - това не може да бъде отменено!</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">Членът ще бъде премахнат от чата - това не може да бъде отменено!</string> + <string name="info_row_chat">Чат</string> + <string name="operator_review_conditions">Преглед на условията</string> + <string name="xftp_servers_per_user">Сървърите за нови файлове от текущия ви профил</string> + <string name="v6_2_business_chats_descr">Поверителност за вашите клиенти.</string> + <string name="v6_2_network_decentralization">Децентрализация на мрежата</string> + <string name="v6_2_network_decentralization_descr">Вторият предварително зададен оператор в приложението!</string> + <string name="v6_2_business_chats">Бизнес чатове</string> + <string name="share_address_publicly">Сподели адресът публично</string> + <string name="chat_list_all">Всички</string> + <string name="only_chat_owners_can_change_prefs">Само собствениците на чат могат да променят настройките.</string> + <string name="delete_chat_list_question">Изтрий списъка?</string> + <string name="list_menu">Списък</string> + <string name="delete_chat_list_menu_action">Изтрий</string> + <string name="chat_list_businesses">Бизнеси</string> + <string name="list_name_field_placeholder">Име на списъка...</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">Когато е активиран повече от един оператор, никои от тях няма метаданни, за да научи кой с кого комуникира.</string> + <string name="connect_plan_chat_already_exists">Чатът вече съществува!</string> + <string name="member_role_will_be_changed_with_notification_chat">Ролята ще бъде променена на %s. Всички в чата ще бъдат уведомени.</string> + <string name="operator_info_title">Мрежов оператор</string> + <string name="operator_server_alert_title">Сървър на оператора</string> + <string name="appearance_in_app_bars_alpha">Прозрачност</string> + <string name="v6_1_delete_many_messages_descr">Изтриване или модериране до 200 съобщения.</string> + <string name="chat_archive">Или импортирайте архивен файл</string> + <string name="display_name_requested_to_connect">заявка за свързване</string> + <string name="failed_to_save_servers">Грешка при запазване на сървърите</string> + <string name="no_media_servers_configured">Няма сървъри за медия и файлове.</string> + <string name="no_media_servers_configured_for_sending">Няма сървъри за изпращане на файлове.</string> + <string name="error_accepting_operator_conditions">Грешка при приемане на условия</string> + <string name="maximum_message_size_title">Съобщението е твърде голямо!</string> + <string name="maximum_message_size_reached_text">Моля, намалете размера на съобщението и го изпратете отново.</string> + <string name="maximum_message_size_reached_non_text">Моля, намалете размера на съобщението или премахнете мултимедията и изпратете отново.</string> + <string name="business_address">Бизнес адрес</string> + <string name="leave_chat_question">Напусни чата?</string> + <string name="appearance_bars_blur_radius">Размазване</string> + <string name="onboarding_network_operators_simplex_flux_agreement">SimpleX Chat и Flux сключиха споразумение за включване на управлявани от Flux сървъри в приложението.</string> + <string name="operator_conditions_will_be_applied"><![CDATA[Тези условия ще се прилагат и за: <b>%s</b>.]]></string> + <string name="operators_conditions_will_also_apply"><![CDATA[Тези условия ще се прилагат и за: <b>%s</b>.]]></string> + <string name="operator_open_conditions">Отвори условията</string> + <string name="v6_1_switch_chat_profile_descr">Превключете чат профила за еднократен линк за връзка.</string> + <string name="sync_connection_question">Поправи връзката?</string> + <string name="sync_connection_desc">Връзката изисква предоговаряне на криптирането.</string> + <string name="share_1_time_link_with_a_friend">Сподели еднократен линк за връзка с приятел</string> + <string name="address_settings">Настройки на адреса</string> + <string name="onboarding_select_network_operators_to_use">Изберете мрежови оператори за използване.</string> + <string name="how_it_helps_privacy">Как помага за поверителността</string> + <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Ще спрете да получавате съобщения от този чат. Историята на чата ще бъде запазена.</string> + <string name="button_add_team_members">Добави членове на екипа</string> + <string name="mute_all_chat">Заглушаване на всички</string> +</resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/bn/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/bn/strings.xml index 13e322f078..bb448339bd 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/bn/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/bn/strings.xml @@ -28,7 +28,7 @@ <string name="color_secondary_variant">আনুষঙ্গিক রং</string> <string name="v4_2_group_links_desc">অ্যাডমিনরা গ্রুপে যোগদানের সংযোগ-সূত্র তৈরি করতে পারবেন।</string> <string name="v4_3_improved_server_configuration_desc">QR কোড স্ক্যান করে সার্ভার যুক্ত করুন।</string> - <string name="smp_servers_add">সার্ভার যুক্ত করুন…</string> + <string name="smp_servers_add">সার্ভার যুক্ত করুন</string> <string name="address_section_title">ঠিকানা</string> <string name="abort_switch_receiving_address_desc">ঠিকানা পরিবর্তন বাতিল করা হবে। বার্তা গ্রহণের পুরনো ঠিকানা ব্যবহার করা হবে।</string> <string name="all_app_data_will_be_cleared">অ্যাপের সকল তথ্য মুছে ফেলা হয়েছে।</string> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml new file mode 100644 index 0000000000..0e179fadd4 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml @@ -0,0 +1,2356 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="v4_3_improved_server_configuration_desc">Afegiu servidors escanejant codis QR.</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s missatges no reenviats</string> + <string name="block_member_desc">Tots els missatges nous de %s s\'amagaran!</string> + <string name="notifications_mode_off_desc">L\'aplicació només pot rebre notificacions quan s\'està executant, no s\'iniciarà cap servei en segon pla</string> + <string name="notifications_mode_service_desc">El servei en segon pla sempre s\'executa: les notificacions es mostraran tan aviat com els missatges estiguin disponibles.</string> + <string name="color_primary_variant">Color addicional</string> + <string name="icon_descr_asked_to_receive">S\'ha demanat rebre la imatge</string> + <string name="member_info_member_blocked">blocat</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Tots els vostres contactes, converses i fitxers es xifraran de manera segura i es penjaran en trossos als relés XFTP configurats.</string> + <string name="notifications_mode_service">Sempre actiu</string> + <string name="alert_title_msg_bad_id">Identificador incorrecte de missatge</string> + <string name="network_session_mode_entity_description"><![CDATA[S\'utilitzarà una connexió TCP independent (i una credencial SOCKS) <b>per a cada contacte i membre del grup</b>.\n<b>Tingueu en compte</b>: si teniu moltes connexions, el vostre el consum de bateria i trànsit pot ser substancialment més elevat i algunes connexions poden fallar.]]></string> + <string name="alert_title_msg_bad_hash">Hash incorrecte de missatge</string> + <string name="turning_off_service_and_periodic">L\'optimització de la bateria està activa, desactiva el servei en segon pla i les sol·licituds periòdiques de missatges nous. Podeu tornar-los a activar mitjançant la configuració.</string> + <string name="attempts_label">intents</string> + <string name="block_member_confirmation">Blocar</string> + <string name="remote_host_error_bad_version"><![CDATA[Mobile <b>%s</b> té una versió no compatible. Si us plau, assegureu-vos que feu servir la mateixa versió als dos dispositius]]></string> + <string name="call_service_notification_audio_call">Trucada</string> + <string name="la_auth_failed">L\'autenticació ha fallat</string> + <string name="auth_unavailable">Autenticació no disponible</string> + <string name="both_you_and_your_contact_can_send_voice">Tant tu com el teu contacte podeu enviar missatges de veu.</string> + <string name="info_view_call_button">trucar</string> + <string name="call_already_ended">La trucada ja s\'ha acabat!</string> + <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Es pot desactivar mitjançant la configuració</b>: les notificacions encara es mostraran mentre l\'aplicació s\'executa.]]></string> + <string name="color_primary">Color</string> + <string name="abort_switch_receiving_address_desc">El canvi d\'adreça s\'avortarà. S\'utilitzarà l\'adreça de recepció antiga.</string> + <string name="wallpaper_advanced_settings">Configuració avançada</string> + <string name="v4_2_auto_accept_contact_requests">Accepta automàticament les sol·licituds de contacte</string> + <string name="allow_voice_messages_question">Vols permetre els missatges de veu?</string> + <string name="bad_desktop_address">Mala adreça d\'ordinador</string> + <string name="e2ee_info_no_pq"><![CDATA[Els missatges, els fitxers i les trucades estan protegits per un <b>xifratge d\'extrem a extrem</b> amb PFS, repudi i recuperació per intrusió.]]></string> + <string name="all_your_contacts_will_remain_connected_update_sent">Tots els teus contactes romandran connectats. L\'actualització del perfil s\'enviarà als teus contactes.</string> + <string name="connect_plan_already_connecting">Ja estem connectant!</string> + <string name="connect_plan_already_joining_the_group">Ja us uniu al grup!</string> + <string name="attach">Adjuntar</string> + <string name="integrity_msg_bad_id">Identificador incorrecte de missatge</string> + <string name="icon_descr_cancel_file_preview">Cancel·la la vista prèvia del fitxer</string> + <string name="snd_error_quota">S\'ha superat la capacitat: el destinatari no ha rebut missatges enviats anteriorment.</string> + <string name="network_session_mode_user_description"><![CDATA[S\'utilitzarà una connexió TCP independent (i una credencial SOCKS) <b>per a cada perfil de xat que tingueu a l\'aplicació</b>.]]></string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Afegeix un contacte</b>: crea un enllaç d\'invitació nou o connecta\'t mitjançant un enllaç que has rebut.]]></string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Tingueu en compte</b>: utilitzar la mateixa base de dades en dos dispositius trencarà el desxifrat dels missatges de les vostres connexions, com a protecció de seguretat.]]></string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Utilitza més bateria</b>! L\'aplicació sempre s\'executa en segon pla: les notificacions es mostren a l\'instant.]]></string> + <string name="remote_host_error_bad_state"><![CDATA[La connexió al mòbil <b>%s</b> està en mal estat]]></string> + <string name="remote_host_disconnected_from"><![CDATA[Desconnectat del mòbil <b>%s</b> amb el motiu: %s]]></string> + <string name="moderated_items_description">%1$d missatge(s) moderat(s) per %2$s</string> + <string name="blocked_by_admin_item_description">blocat per l\'administrador</string> + <string name="e2ee_info_pq"><![CDATA[Els missatges, els fitxers i les trucades estan protegits per un <b>xifratge extrem a extrem resistent quàntic</b> amb PFS, repudi i recuperació d\'intrusió.]]></string> + <string name="n_other_file_errors">%1$d altre(s) error(s) del fitxer.</string> + <string name="cannot_receive_file">No es pot rebre el fitxer</string> + <string name="turn_off_battery_optimization"><![CDATA[<b>Permet-ho</b> al següent diàleg per rebre notificacions a l\'instant.]]></string> + <string name="database_initialization_error_title">No es pot inicialitzar la base de dades</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Dispositius Xiaomi</b>: activeu l\'inici automàtic a la configuració del sistema perquè les notificacions funcionin.]]></string> + <string name="icon_descr_cancel_image_preview">Cancelar vista prèvia de la imatge</string> + <string name="cannot_share_message_alert_title">No es pot enviar el missatge</string> + <string name="icon_descr_video_asked_to_receive">S\'ha demanat rebre el vídeo</string> + <string name="back">Tornar</string> + <string name="use_camera_button">Càmera</string> + <string name="camera_not_available">Càmera no disponible</string> + <string name="cancel_verb">Cancelar</string> + <string name="icon_descr_cancel_live_message">Cancel·la el missatge en directe</string> + <string name="above_then_preposition_continuation">a dalt, aleshores:</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Crea grup</b>: per crear un grup nou.]]></string> + <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 ordinador: escaneja el codi QR que es mostra des de l\'aplicació, mitjançant <b>Escaneja el codi QR</b>.]]></string> + <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 mòbil: toca <b>Obre a l\'aplicació per a mòbils</b> i després toca <b>Connecta</b> a l\'aplicació.]]></string> + <string name="clear_note_folder_warning">Tots els missatges se suprimiran; això no es pot desfer!</string> + <string name="clear_chat_warning">Tots els missatges s\'eliminaran NOMÉS per a tu; això no es pot desfer!</string> + <string name="icon_descr_cancel_link_preview">cancel·la la vista prèvia de l\'enllaç</string> + <string name="connect__a_new_random_profile_will_be_shared">Es compartirà un nou perfil aleatori.</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[l\'enllaç d\'un cop es pot utilitzar <i>només amb un contacte</i>: compartiu-lo en persona o mitjançant qualsevol missatger.]]></string> + <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Si no us podeu trobar en persona, podeu <b>escanejar el codi QR a la videotrucada</b> o el vostre contacte pot compartir un enllaç d\'invitació.]]></string> + <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Si no pots trobar-te en persona, <b>mostra el codi QR a la videotrucada</b> o comparteix l\'enllaç.]]></string> + <string name="deleted_chats">Contactes arxivats</string> + <string name="read_more_in_user_guide_with_link"><![CDATA[Llegir més a la <font color="#0088ff">Guia de l\'usuari</font>.]]></string> + <string name="network_settings">Configuració de xarxa avançada</string> + <string name="network_settings_title">Configuració avançada</string> + <string name="network_enable_socks_info">Accedir als servidors mitjançant el servidor SOCKS al port %d? S\'ha d\'iniciar abans d\'activar aquesta opció.</string> + <string name="network_session_mode_session">per sessió</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Tingueu en compte</b>: els retransmissions de missatges i fitxers es connecten mitjançant un servidor intermediari SOCKS. Les trucades i l\'enviament de visualitzacions prèvies d\'enllaç utilitzen connexió directa.]]></string> + <string name="network_smp_proxy_fallback_allow_downgrade">Permetre la versió anterior</string> + <string name="network_smp_proxy_mode_always">Sempre</string> + <string name="network_smp_proxy_mode_always_description">Utilitzeu sempre l\'encaminament privat.</string> + <string name="app_version_code">Creació de l\'aplicació: %s</string> + <string name="appearance_settings">Aparença</string> + <string name="app_check_for_updates_download_completed_title">S\'ha baixat l\'actualització de l\'aplicació</string> + <string name="app_version_title">Versió</string> + <string name="app_version_name">Versió de l\'aplicació: v%s</string> + <string name="app_check_for_updates_beta">Beta</string> + <string name="all_your_contacts_will_remain_connected">Tots els teus contactes romandran connectats.</string> + <string name="add_your_team_members_to_conversations">Afegeix els membres del teu equip a les converses.</string> + <string name="auto_accept_contact">Acceptació automàtica</string> + <string name="business_address">Adreça d\'empresa</string> + <string name="bold_text">negreta</string> + <string name="callstatus_ended">trucada acabada %1$s</string> + <string name="callstatus_error">error de trucada</string> + <string name="callstatus_calling">trucant…</string> + <string name="callstatus_in_progress">trucada en curs</string> + <string name="permissions_camera">Càmera</string> + <string name="permissions_camera_and_record_audio">Càmera i micròfon</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Qualsevol pot allotjar servidors.</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[Tots els missatges i fitxers s\'envien <b>xifrats d\'extrem a extrem</b>, amb seguretat postquàntica als missatges directes.]]></string> + <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>El millor per a la bateria</b>. Només rebràs notificacions quan l\'aplicació s\'està executant (Sense servei en segon pla).]]></string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Bo per a la bateria</b>. L\'aplicació revisa els missatges cada 10 minuts. Podeu perdre trucades o missatges urgents.]]></string> + <string name="read_more_in_github_with_link"><![CDATA[Llegir més al nostre <font color="#0088ff">repositori GitHub</font>.]]></string> + <string name="contact_wants_to_connect_via_call">%1$s vol connectar amb tu mitjançant</string> + <string name="always_use_relay">Feu servir sempre el retransmisor</string> + <string name="onboarding_notifications_mode_service_desc_short">App always runs in background</string> + <string name="icon_descr_audio_call">trucada</string> + <string name="audio_call_no_encryption">trucada (no xifrada)</string> + <string name="settings_audio_video_calls">Trucades d\'àudio i vídeo</string> + <string name="call_on_lock_screen">Trucades a la pantalla de bloqueig:</string> + <string name="alert_text_decryption_error_too_many_skipped">%1$d missatge(s) omés(os).</string> + <string name="integrity_msg_skipped">%1$d missatge(s) omés(os).</string> + <string name="answer_call">Respondre trucada</string> + <string name="icon_descr_audio_off">Àudio desactivat</string> + <string name="icon_descr_audio_on">Àudio activat</string> + <string name="integrity_msg_bad_hash">hash de missatge erroni</string> + <string name="icon_descr_call_ended">La trucada ha finalitzat</string> + <string name="icon_descr_call_progress">Trucada en curs</string> + <string name="full_backup">Còpia de seguretat de dades de l\'aplicació</string> + <string name="la_app_passcode">Contrasenya de l\'aplicació</string> + <string name="authentication_cancelled">S\'ha cancel·lat l\'autenticació</string> + <string name="auto_accept_images">Accepta imatges automàticament</string> + <string name="all_app_data_will_be_cleared">S\'eliminaran totes les dades de l\'aplicació.</string> + <string name="empty_chat_profile_is_created">Es crea un perfil de xat buit amb el nom proporcionat i l\'aplicació s\'obre com de costum.</string> + <string name="app_passcode_replaced_with_self_destruct">La contrasenya de l\'aplicació es substitueix per una contrasenya d\'autodestrucció.</string> + <string name="settings_section_title_app">APLICACIÓ</string> + <string name="settings_section_title_icon">ICONA APLICACIÓ</string> + <string name="privacy_media_blur_radius">Desenfocar els mitjans</string> + <string name="settings_section_title_calls">TRUCADES</string> + <string name="keychain_is_storing_securely">Android Keystore s\'utilitza per emmagatzemar de manera segura la frase de contrasenya: permet que el servei de notificacions funcioni.</string> + <string name="keychain_allows_to_receive_ntfs">Android Keystore s\'utilitzarà per emmagatzemar de manera segura la frase de contrasenya després de reiniciar l\'aplicació o canviar la frase de contrasenya; permetrà rebre notificacions.</string> + <string name="cannot_access_keychain">No es pot accedir a Keystore per desar la contrasenya de la base de dades</string> + <string name="impossible_to_recover_passphrase"><![CDATA[<b>Tingueu en compte</b>: NO podreu recuperar ni canviar la contrasenya si la perdeu.]]></string> + <string name="alert_title_cant_invite_contacts">No es poden convidar contactes!</string> + <string name="rcv_group_event_member_blocked">ha blocat a %s</string> + <string name="rcv_group_and_other_events">i %d esdeveniments més</string> + <string name="conn_event_ratchet_sync_started">acordant el xifratge…</string> + <string name="snd_conn_event_ratchet_sync_started">acordant el xifratge per a %s…</string> + <string name="group_member_role_author">autor</string> + <string name="group_info_section_title_num_members">%1$s MEMBRES</string> + <string name="invite_prohibited">No es pot convidar el contacte!</string> + <string name="all_group_members_will_remain_connected">Tots els membres del grup romandran connectats.</string> + <string name="block_for_all">Blocar per a tots</string> + <string name="block_member_button">Blocar membre</string> + <string name="block_member_question">Blocar membre?</string> + <string name="block_for_all_question">Blocar membre per a tots?</string> + <string name="member_blocked_by_admin">Blocat per l\'administrador</string> + <string name="cant_call_contact_alert_title">No es pot trucar al contacte</string> + <string name="allow_calls_question">Permetre trucades?</string> + <string name="calls_prohibited_alert_title">Trucades prohibides!</string> + <string name="cant_call_member_alert_title">No es pot trucar al membre del grup</string> + <string name="cant_send_message_to_member_alert_title">No es pot enviar missatges als membres del grup</string> + <string name="operators_conditions_accepted_for"><![CDATA[S\'han acceptat les condicions per a l\'operador(s): <b>%s</b>.]]></string> + <string name="operator_conditions_accepted_for_some"><![CDATA[Ja s\'han acceptat les condicions per als operadors següents: <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[S\'acceptaran condicions per als operadors: <b>%s</b>.]]></string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[S\'acceptaran condicions per als operadors: <b>%s</b>.]]></string> + <string name="operator_added_xftp_servers">S\'han afegit servidors multimèdia i de fitxers</string> + <string name="operator_added_message_servers">S\'han afegit servidors de missatges</string> + <string name="users_delete_all_chats_deleted">Tots els xats i missatges se suprimiran; això no es pot desfer!</string> + <string name="theme_black">Negre</string> + <string name="theme_destination_app_theme">Tema de l\'aplicació</string> + <string name="color_secondary_variant">Secundari addicional</string> + <string name="color_background">Fons</string> + <string name="color_primary_variant2">Color addicional 2</string> + <string name="chat_theme_apply_to_all_modes">Tots els modes</string> + <string name="chat_preferences_always">sempre</string> + <string name="chat_theme_apply_to_mode">Aplicar a</string> + <string name="appearance_app_toolbars">Barres d\'eines d\'aplicació</string> + <string name="appearance_bars_blur_radius">Desenfocar</string> + <string name="audio_video_calls">Trucades d\'àudio/vídeo</string> + <string name="available_in_v51">\nDisponible en v5.1</string> + <string name="allow_calls_only_if">Permet trucades només si el vostre contacte ho permet.</string> + <string name="allow_disappearing_messages_only_if">Permet la desaparició de missatges només si el vostre contacte ho permet.</string> + <string name="allow_irreversible_message_deletion_only_if">"Permet la supressió irreversible del missatge només si el teu contacte t\'ho permet. (24 hores)"</string> + <string name="allow_message_reactions_only_if">Permet les reaccions als missatges només si el teu contacte les permet.</string> + <string name="allow_voice_messages_only_if">Permet missatges de veu només si el vostre contacte els permet.</string> + <string name="allow_your_contacts_adding_message_reactions">Permet que els teus contactes afegeixin reaccions als missatges.</string> + <string name="allow_your_contacts_to_call">Permet que els teus contactes et truquin.</string> + <string name="allow_your_contacts_irreversibly_delete">Permet als teus contactes eliminar de manera irreversible els missatges enviats. (24 hores)</string> + <string name="allow_your_contacts_to_send_disappearing_messages">Permet que els teus contactes enviïn missatges que desapareixen.</string> + <string name="allow_your_contacts_to_send_voice_messages">Permet que els teus contactes enviïn missatges de veu.</string> + <string name="both_you_and_your_contacts_can_delete">Tant vos com els vostres contactes podeu suprimir de manera irreversible els missatges enviats. (24 hores)</string> + <string name="both_you_and_your_contact_can_send_disappearing">Tant tu com el teu contacte podeu enviar missatges que desapareguin.</string> + <string name="both_you_and_your_contact_can_add_message_reactions">Tant tu com el teu contacte podeu afegir reaccions als missatges.</string> + <string name="allow_message_reactions">Permet la supressió irreversible del missatge només si el teu contacte t\'ho permet. (24 hores)</string> + <string name="allow_direct_messages">Permet enviar missatges directes als membres.</string> + <string name="allow_to_delete_messages">Permeteu suprimir de manera irreversible els missatges enviats. (24 hores)</string> + <string name="allow_to_send_disappearing">Permet enviar missatges que desapareixen.</string> + <string name="allow_to_send_files">Permet enviar fitxers i mitjans.</string> + <string name="allow_to_send_voice">Permet enviar missatges de veu.</string> + <string name="calls_prohibited_with_this_contact">Les trucades d\'àudio/vídeo estan prohibides.</string> + <string name="both_you_and_your_contact_can_make_calls">Tant tu com el teu contacte podeu fer trucades.</string> + <string name="allow_to_send_simplex_links">Permet enviar enllaços SimpleX.</string> + <string name="feature_roles_all_members">tots els membres</string> + <string name="feature_cancelled_item">cancel·lat %s</string> + <string name="v4_2_group_links_desc">Els administradors poden crear els enllaços per unir-se als grups.</string> + <string name="v4_6_audio_video_calls">Trucades d\'àudio i vídeo</string> + <string name="v4_5_transport_isolation_descr">Per perfil de xat (per defecte) o per connexió (BETA).</string> + <string name="v5_1_self_destruct_passcode_descr">Totes les dades s\'esborren quan s\'introdueix.</string> + <string name="v5_0_app_passcode">Contrasenya de l\'aplicació</string> + <string name="v5_1_better_messages">Missatges millorats</string> + <string name="v5_2_more_things">Unes quantes coses més</string> + <string name="v5_3_encrypt_local_files_descr">Xifratge dels fitxers locals nous (excepte vídeos).</string> + <string name="v5_3_new_interface_languages_descr">Àrab, búlgar, finès, hebreu, tailandès i ucraïnès - gràcies als usuaris i Weblate.</string> + <string name="v5_4_better_groups">Grups millorats</string> + <string name="v5_4_block_group_members">Blocar membres del grup</string> + <string name="v5_6_app_data_migration">Migració de dades d\'aplicacions</string> + <string name="v6_0_your_contacts_descr">Arxiva els contactes per xatejar més tard.</string> + <string name="v6_0_privacy_blur">Desenfoca per a una millor privadesa.</string> + <string name="v6_1_better_calls">Trucades millorades</string> + <string name="v6_1_message_dates_descr">Dates de missatges millorades.</string> + <string name="v6_1_better_security">Seguretat millorada ✅</string> + <string name="v6_1_better_user_experience">Millor experiència d\'usuari</string> + <string name="v6_2_business_chats">Xats d\'empresa</string> + <string name="this_device_version"><![CDATA[<i>(aquest dispositiu v%s)</i>]]></string> + <string name="new_desktop"><![CDATA[<i>(nou)</i>]]></string> + <string name="remote_host_was_disconnected_toast"><![CDATA[El mòbil <b>%s</b> s\'ha desconnectat]]></string> + <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Obre <i>Usar des de l\'ordinador</i> a l\'aplicació mòbil i escaneja el codi QR.]]></string> + <string name="remote_host_error_busy"><![CDATA[El mòbil <b>%s</b> està ocupat]]></string> + <string name="remote_host_error_inactive"><![CDATA[El mòbil <b>%s</b> està inactiu]]></string> + <string name="remote_host_error_missing"><![CDATA[El mòbil <b>%s</b> no es troba]]></string> + <string name="remote_host_error_disconnected"><![CDATA[El mòbil <b>%s</b> s\'ha desconnectat]]></string> + <string name="migrate_to_device_apply_onion">Aplicar</string> + <string name="migrate_from_device_archive_and_upload">Arxivar i carregar</string> + <string name="migrate_from_device_archiving_database">Arxivant bases de dades</string> + <string name="migrate_from_device_cancel_migration">Cancel·la la migració</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Trieu <i>Migra des d\'un altre dispositiu</i> al dispositiu nou i escaneja el codi QR.]]></string> + <string name="all_users">Tots els perfils</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Avís</b>: l\'arxiu s\'eliminarà.]]></string> + <string name="acknowledged">Reconegut</string> + <string name="acknowledgement_errors">Errors de reconeixement</string> + <string name="forward_files_not_accepted_desc">%1$d fitxer(s) no s\'ha(n) baixat.</string> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d missatge(s) no s\'ha(n) pogut desencriptar.</string> + <string name="server_connecting">s\'està connectant</string> + <string name="blocked_item_description">blocat</string> + <string name="thousand_abbreviation">k</string> + <string name="connect_via_link_verb">Connecta</string> + <string name="server_error">error</string> + <string name="sender_you_pronoun">vostè</string> + <string name="live">EN DIRECTE</string> + <string name="moderated_description">moderat</string> + <string name="forwarded_description">reenviat</string> + <string name="saved_description">desat</string> + <string name="display_name_connecting">s\'està connectant…</string> + <string name="simplex_link_mode_description">Descripció</string> + <string name="n_file_errors">%1$d error(s) de fitxer:\n%2$s</string> + <string name="smp_server_test_disconnect">Desconnecta</string> + <string name="turn_off_battery_optimization_button">Permet</string> + <string name="la_authenticate">Autentica</string> + <string name="auth_unlock">Desbloca</string> + <string name="share_verb">Comparteix</string> + <string name="save_verb">Desa</string> + <string name="edit_verb">Edita</string> + <string name="info_menu">Info</string> + <string name="search_verb">Cerca</string> + <string name="forwarded_chat_item_info_tab">Reenviat</string> + <string name="delete_verb">Esborra</string> + <string name="reveal_verb">Mostra</string> + <string name="hide_verb">Amaga</string> + <string name="moderate_verb">Modera</string> + <string name="select_verb">Selecciona</string> + <string name="expand_verb">Expandeix</string> + <string name="forward_files_failed_to_receive_desc">La baixada ha fallat per a %1$d fitxer(s).</string> + <string name="forward_files_missing_desc">%1$d fitxer(s) ha(n) estat eliminat(s).</string> + <string name="deleted_description">esborrat</string> + <string name="notification_preview_mode_hidden">Ocult</string> + <string name="copy_verb">Còpia</string> + <string name="delivery">Lliurament</string> + <string name="forward_files_in_progress_desc">%1$d encara s\'estan baixant els fitxers.</string> + <string name="smp_server_test_connect">Connecta</string> + <string name="error_alert_title">Error</string> + <string name="reply_verb">Respon</string> + <string name="hide_notification">Amaga</string> + <string name="server_connected">connectat</string> + <string name="stop_file__confirm">Atura</string> + <string name="notification_contact_connected">Connectat</string> + <string name="la_immediately">Immediatament</string> + <string name="edit_history">Historial</string> + <string name="saved_chat_item_info_tab">Desat</string> + <string name="allow_verb">Permet</string> + <string name="revoke_file__confirm">Revoca</string> + <string name="display_name_accepted_invitation">invitació acceptada</string> + <string name="send_disappearing_message_30_seconds">30 segons</string> + <string name="accept_contact_button">Accepta</string> + <string name="accept_connection_request__question">Voleu acceptar la sol·licitud de connexió?</string> + <string name="accept_contact_incognito_button">Acceptar incògnit</string> + <string name="learn_more_about_address">Quant a l\'adreça de SimpleX</string> + <string name="one_time_link_short">Enllaç d\'un sol ús</string> + <string name="add_contact_tab">Afegiu un contacte</string> + <string name="address_or_1_time_link">Adreça o enllaç d\'un sol ús?</string> + <string name="smp_servers_preset_add">Afegiu servidors predefinits</string> + <string name="smp_servers_add">Afegeix servidor</string> + <string name="about_simplex">Quant a SimpleX</string> + <string name="callstatus_accepted">trucada acceptada</string> + <string name="accept">Accepta</string> + <string name="chat_item_ttl_day">1 dia</string> + <string name="chat_item_ttl_month">1 mes</string> + <string name="chat_item_ttl_week">1 setmana</string> + <string name="group_member_role_admin">administrador</string> + <string name="button_add_friends">Afegiu-hi amics</string> + <string name="button_add_team_members">Afegeix membres de l\'equip</string> + <string name="button_add_welcome_message">Afegeix missatge de benvinguda</string> + <string name="address_section_title">Adreça</string> + <string name="abort_switch_receiving_address">Cancel·la el canvi d\'adreça</string> + <string name="accept_conditions">Accepta les condicions</string> + <string name="operator_conditions_accepted">Condicions acceptades</string> + <string name="users_add">Afegiu un perfil</string> + <string name="abort_switch_receiving_address_question">Voleu cancel·lar el canvi d\'adreça?</string> + <string name="send_disappearing_message_1_minute">1 minut</string> + <string name="accept_feature">Accepta</string> + <string name="v5_3_new_interface_languages">6 noves llengües d\'interfície</string> + <string name="abort_switch_receiving_address_confirm">Cancel·la</string> + <string name="about_simplex_chat">Quant a SimpleX Chat</string> + <string name="v5_6_safer_groups_descr">Els administradors poden blocar un membre per a tothom.</string> + <string name="servers_info_subscriptions_connections_subscribed">Connexions actives</string> + <string name="send_disappearing_message_5_minutes">5 minuts</string> + <string name="accept_call_on_lock_screen">Accepta</string> + <string name="onboarding_network_about_operators">Quant als operadors</string> + <string name="add_address_to_your_profile">Afegiu l\'adreça al vostre perfil per tal que els vostres contactes puguin compartir-la amb la resta del món. L\'actualització del perfil s\'enviarà als vostres contactes.</string> + <string name="address_settings">Configuració d\'adreça</string> + <string name="smp_servers_add_to_another_device">Afegeix a un altre dispositiu</string> + <string name="feature_roles_admins">administradors</string> + <string name="delete_contact_question">Voleu suprimir el contacte?</string> + <string name="share_text_disappears_at">Desapareixerà a: %s</string> + <string name="auth_device_authentication_is_disabled_turning_off">L\'autenticació del dispositiu està desactivada. S\'està desactivant el bloqueig SimpleX.</string> + <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">L\'autenticació del dispositiu no està activada. Podeu activar SimpleX Lock mitjançant Configuració, un cop hàgiu activat l\'autenticació del dispositiu.</string> + <string name="send_receipts_disabled">desactivat</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Ja esteu connectat amb <b>%1$s</b>.]]></string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Ja us esteu connectant a <b>%1$s</b>.]]></string> + <string name="proxy_destination_error_broker_host">L\'adreça del servidor de destinació de %1$s és incompatible amb la configuració del servidor de reenviament %2$s.</string> + <string name="delete_chat_profile">Suprimeix el perfil</string> + <string name="ttl_hours">%d hores</string> + <string name="for_me_only">Esborrar per mi</string> + <string name="direct_messages_are_prohibited_in_chat">Els missatges directes entre membres estan prohibits en aquest xat.</string> + <string name="proxy_destination_error_broker_version">La versió del servidor de destinació de %1$s és incompatible amb el servidor de reenviament %2$s.</string> + <string name="database_migration_in_progress">La migració de la base de dades està en curs. Pot trigar uns minuts.</string> + <string name="blocked_items_description">%d missatge(s) bloquejat(s)</string> + <string name="blocked_by_admin_items_description">%d missatge(s) bloquejat(s) per l\'administrador</string> + <string name="marked_deleted_items_description">%d missatge(s) marcat(s) eliminat(s)</string> + <string name="decryption_error">Error de desxifrat</string> + <string name="smp_server_test_delete_queue">Suprimeix la cua</string> + <string name="smp_server_test_delete_file">Esborrar fitxer</string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Per millorar la privadesa, <b>SimpleX s\'executa en segon pla</b> en lloc d\'utilitzar notificacions push.]]></string> + <string name="disable_notifications_button">Desactivar notificacions</string> + <string name="system_restricted_background_warn"><![CDATA[Per habilitar les notificacions, seleccioneu <b>Ús de la bateria de l\'aplicació</b> / <b>Sense restriccions</b> a la configuració de l\'aplicació.]]></string> + <string name="system_restricted_background_in_call_warn"><![CDATA[Per fer trucades en segon pla, seleccioneu <b>Ús de la bateria de l\'aplicació</b> / <b>Sense restriccions</b> a la configuració de l\'aplicació.]]></string> + <string name="la_minutes">%d minuts</string> + <string name="auth_disable_simplex_lock">Desactiva el bloqueig SimpleX</string> + <string name="snd_error_relay">Error del servidor de destinació: %1$s</string> + <string name="delete_message__question">Voleu suprimir el missatge?</string> + <string name="delete_messages__question">Voleu suprimir els %d missatges?</string> + <string name="delete_members_messages__question">Voleu suprimir %d missatges dels membres?</string> + <string name="delete_member_message__question">Vols suprimir el missatge del membre?</string> + <string name="image_decoding_exception_title">Error de descodificació</string> + <string name="delete_and_notify_contact">Eliminar i notificar el contacte</string> + <string name="button_delete_contact">Suprimeix el contacte</string> + <string name="delete_without_notification">Suprimeix sense notificació</string> + <string name="icon_descr_server_status_disconnected">Desconnectat</string> + <string name="disappearing_message">Missatge que desapareix</string> + <string name="delete_contact_menu_action">Suprimeix</string> + <string name="delete_group_menu_action">Suprimeix</string> + <string name="delete_pending_connection__question">Voleu suprimir la connexió pendent?</string> + <string name="database_passphrase_and_export">Contrasenya i exportació de la base de dades</string> + <string name="smp_servers_delete_server">Suprimeix el servidor</string> + <string name="disable_onion_hosts_when_not_supported"><![CDATA[Estableix <i>Utilitza els hosts .onion</i> a No si el servidor SOCKS no els admet.]]></string> + <string name="app_check_for_updates_disabled">Desactivat</string> + <string name="network_smp_proxy_fallback_prohibit_description">NO envieu missatges directament, encara que el vostre servidor de destinació no admeti l\'encaminament privat.</string> + <string name="delete_address__question">Voleu suprimir l\'adreça?</string> + <string name="developer_options_section">Opcions de desenvolupador</string> + <string name="app_check_for_updates_notice_disable">Desactivar</string> + <string name="delete_address">Esborrar adreça</string> + <string name="delete_image">Suprimeix la imatge</string> + <string name="display_name_cannot_contain_whitespace">El nom mostrat no pot contenir espais en blanc.</string> + <string name="decentralized">Descentralitzada</string> + <string name="no_call_on_lock_screen">Desactivar</string> + <string name="receipts_contacts_disable_for_all">Desactivar per a tothom</string> + <string name="receipts_groups_disable_for_all">Desactiva per a tots els grups</string> + <string name="receipts_contacts_disable_keep_overrides">Desactivar (conservant anul·lacions)</string> + <string name="receipts_groups_disable_keep_overrides">Desactivar (mantenint les anul·lacions del grup)</string> + <string name="receipts_contacts_title_disable">Desactivar rebuts?</string> + <string name="receipts_groups_title_disable">Desactivar rebuts per a grups?</string> + <string name="settings_developer_tools">Eines per a desenvolupadors</string> + <string name="settings_section_title_device">DISPOSITIU</string> + <string name="set_password_to_export_desc">La base de dades es xifra amb una contrasenya aleatòria. Si us plau, canvieu-la abans d\'exportar.</string> + <string name="database_passphrase">Contrasenya de la base de dades</string> + <string name="delete_chat_profile_question">Voleu suprimir el perfil?</string> + <string name="delete_database">Esborrar la base de dades</string> + <string name="delete_files_and_media_all">Suprimeix tots els fitxers</string> + <string name="delete_files_and_media_question">Voleu suprimir els fitxers i els mitjans?</string> + <string name="delete_files_and_media_for_all_users">Suprimeix els fitxers de tots els perfils</string> + <string name="total_files_count_and_size">%d fitxer(s) amb una mida total de %s</string> + <string name="database_encrypted">Base de dades xifrada!</string> + <string name="delete_messages">Suprimir missatges</string> + <string name="encrypted_with_random_passphrase">La base de dades es xifra amb una contrasenya aleatòria, podeu canviar-la.</string> + <string name="database_passphrase_will_be_updated">La contrasenya de xifratge de la base de dades s\'actualitzarà.</string> + <string name="database_encryption_will_be_updated_in_settings">La contrasenya de xifratge de la base de dades s\'actualitzarà i s\'emmagatzemarà a la configuració.</string> + <string name="passphrase_is_different">La contrasenya de la base de dades és diferent de la desada al Keystore.</string> + <string name="database_passphrase_is_required">La contrasenya de la base de dades és necessària per obrir el xat.</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">La base de dades es xifrarà i la contrasenya s\'emmagatzemarà a la configuració.</string> + <string name="database_will_be_encrypted">La base de dades es xifrarà.</string> + <string name="database_will_be_encrypted_and_passphrase_stored">La base de dades es xifrarà i la frase de contrasenya s\'emmagatzemarà a Keystore.</string> + <string name="database_encryption_will_be_updated">La contrasenya de xifratge de la base de dades s\'actualitzarà i s\'emmagatzemarà a Keystore.</string> + <string name="database_upgrade">Actualització de la base de dades</string> + <string name="mtr_error_no_down_migration">La versió de la base de dades és més recent que l\'aplicació, però no hi ha cap migració cap avall per a: %s</string> + <string name="mtr_error_different">migració diferent a l\'aplicació/base de dades: %s / %s</string> + <string name="rcv_direct_event_contact_deleted">contacte eliminat</string> + <string name="rcv_group_event_group_deleted">grup esborrat</string> + <string name="rcv_group_events_count">%d esdeveniments de grup</string> + <string name="num_contacts_selected">%d contacte(s) seleccionat(s)</string> + <string name="button_delete_chat">Suprimir xat</string> + <string name="delete_chat_question">Suprimir xat?</string> + <string name="button_delete_group">Suprimir grup</string> + <string name="delete_group_question">Suprimir grup?</string> + <string name="delete_link">Suprimir enllaç</string> + <string name="delete_link_question">Suprimir enllaç?</string> + <string name="info_row_debug_delivery">Entrega de depuració</string> + <string name="info_row_deleted_at">Suprimit</string> + <string name="share_text_deleted_at">Suprimit: %s</string> + <string name="info_row_disappears_at">Desapareixerà a</string> + <string name="conn_level_desc_direct">directe</string> + <string name="member_info_member_disabled">inactiu</string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[S\'aplicaran les mateixes condicions a l\'operador <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[S\'aplicaran les mateixes condicions als operadors: <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_applied"><![CDATA[Aquestes condicions també s\'aplicaran a: <b>%s</b>.]]></string> + <string name="operators_conditions_will_also_apply"><![CDATA[Aquestes condicions també s\'aplicaran per a: <b>%s</b>.]]></string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[Per utilitzar els servidors de <b>%s</b>, accepteu les condicions d\'ús.]]></string> + <string name="users_delete_question">Voleu suprimir el perfil?</string> + <string name="delete_profile">Suprimeix el perfil</string> + <string name="chat_preferences_default">predeterminat (%s)</string> + <string name="full_deletion">Suprimeix per a tothom</string> + <string name="direct_messages">Missatges directes</string> + <string name="timed_messages">Missatges que desapareixen</string> + <string name="disappearing_prohibited_in_this_chat">Els missatges que desapareixen estan prohibits en aquest xat.</string> + <string name="ttl_d">%dd</string> + <string name="ttl_day">%d dia</string> + <string name="ttl_days">%d dies</string> + <string name="delete_after">Suprimeix després</string> + <string name="ttl_h">%dh</string> + <string name="ttl_hour">%d hora</string> + <string name="direct_messages_are_prohibited">Els missatges directes entre membres estan prohibits.</string> + <string name="direct_messages_are_prohibited_in_group">Els missatges directes entre membres estan prohibits en aquest grup.</string> + <string name="disappearing_messages_are_prohibited">Els missatges que desapareixen estan prohibits.</string> + <string name="ttl_m">%dm</string> + <string name="ttl_min">%d min</string> + <string name="ttl_month">%d mes</string> + <string name="ttl_months">%d mesos</string> + <string name="ttl_mth">%dmes</string> + <string name="disable_sending_recent_history">No envieu l\'historial als nous membres.</string> + <string name="v4_4_disappearing_messages">Missatges que desapareixen</string> + <string name="v4_5_multiple_chat_profiles_descr">Diferents noms, avatars i aïllament del transport.</string> + <string name="v5_3_discover_join_groups">Descobreix i uneix-te a grups</string> + <string name="v6_0_delete_many_messages_descr">Suprimir fins a 20 missatges alhora.</string> + <string name="custom_time_unit_days">dies</string> + <string name="v6_1_delete_many_messages_descr">Suprimeix o modera fins a 200 missatges.</string> + <string name="delivery_receipts_title">Rebuts de lliurament!</string> + <string name="delivery_receipts_are_disabled">Els rebuts de lliurament estan desactivats!</string> + <string name="desktop_app_version_is_incompatible">La versió de l\'aplicació d\'ordinador %s no és compatible amb aquesta aplicació.</string> + <string name="desktop_devices">Ordinadors</string> + <string name="devices">Dispositius</string> + <string name="disconnect_remote_host">Desconnectar</string> + <string name="disconnect_desktop_question">Desconnectar l\'ordinador?</string> + <string name="remote_ctrl_disconnected_with_reason">S\'ha desconnectat amb el motiu: %s</string> + <string name="disconnect_remote_hosts">Desconnectar mòbils</string> + <string name="desktop_device">Ordinador</string> + <string name="desktop_address">Adreça ordinador</string> + <string name="remote_ctrl_error_bad_version">L\'ordinador té una versió no compatible. Si us plau, assegureu-vos que feu servir la mateixa versió als dos dispositius</string> + <string name="remote_ctrl_error_bad_invitation">L\'ordinador té un codi d\'invitació incorrecte</string> + <string name="remote_ctrl_error_busy">L\'ordinador està ocupat</string> + <string name="remote_ctrl_error_inactive">L\'ordinador està inactiu</string> + <string name="remote_ctrl_error_disconnected">L\'ordinador ha estat desconnectat</string> + <string name="multicast_discoverable_via_local_network">Descobrible a través de la xarxa local</string> + <string name="discover_on_network">Descobriu a través de la xarxa local</string> + <string name="remote_host_error_timeout"><![CDATA[S\'ha arribat al temps d\'espera durant la connexió al mòbil <b>%s</b>]]></string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Aquest és el vostre enllaç per al grup <b>%1$s</b>!]]></string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Ja sou al grup <b>%1$s</b>.]]></string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Ja us esteu unint al grup <b>%1$s</b>.]]></string> + <string name="migrate_from_device_delete_database_from_device">Suprimir la base de dades d\'aquest dispositiu</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device">No heu d\'utilitzar la mateixa base de dades en dos dispositius.</string> + <string name="servers_info_details">Detalls</string> + <string name="decryption_errors">errors de desxifrat</string> + <string name="servers_info_detailed_statistics">Estadístiques detallades</string> + <string name="deleted">Suprimides</string> + <string name="deletion_errors">Errors d\'eliminació</string> + <string name="connected_mobile">Mòbil connectat</string> + <string name="connect_via_link_incognito">Connectar en mode incògnit</string> + <string name="connection_local_display_name">connexió %1$d</string> + <string name="notifications_mode_periodic_desc">Comprova si hi ha missatges cada 10 minuts durant un minut com a màxim</string> + <string name="la_change_app_passcode">Canvia el codi d\'accés</string> + <string name="auth_open_chat_profiles">Canvia el perfil</string> + <string name="auth_confirm_credential">Confirmeu les credencials</string> + <string name="your_chats">Xats</string> + <string name="chat_with_developers">Parleu amb els desenvolupadors</string> + <string name="info_view_connect_button">connecta</string> + <string name="confirm_delete_contact_question">Confirmeu l\'eliminació del contacte?</string> + <string name="icon_descr_server_status_connected">Connectat</string> + <string name="confirm_verb">Confirma</string> + <string name="clear_note_folder_question">Voleu suprimir les notes privades?</string> + <string name="icon_descr_close_button">Botó de tancament</string> + <string name="connect_button">Connecta</string> + <string name="chat_console">Consola del xat</string> + <string name="smp_servers_check_address">Comproveu l\'adreça del servidor de i torneu a provar.</string> + <string name="smp_servers_configured">Servidors SMP configurats</string> + <string name="configure_ICE_servers">Configura els servidors ICE</string> + <string name="network_session_mode_user">Perfil del xat</string> + <string name="network_session_mode_entity">Connexió</string> + <string name="app_check_for_updates">Comprova si hi ha actualitzacions</string> + <string name="app_check_for_updates_notice_title">Comprova si hi ha actualitzacions</string> + <string name="callstate_connected">connectat</string> + <string name="onboarding_notifications_mode_periodic_desc_short">Comprova missatges cada 10 minuts</string> + <string name="change_lock_mode">Canvia la modalitat de bloqueig</string> + <string name="change_self_destruct_mode">Canvia el mode l\'autodestrucció</string> + <string name="change_self_destruct_passcode">Canvia el codi d\'autodestrucció</string> + <string name="confirm_passcode">Confirmeu el codi d\'accés</string> + <string name="settings_section_title_chat_database">BASE DE DADES DELS XATS</string> + <string name="settings_section_title_chats">XATS</string> + <string name="settings_section_title_chat_theme">Tema del xat</string> + <string name="settings_section_title_chat_colors">Colors del xat</string> + <string name="chat_database_deleted">Base de dades suprimida</string> + <string name="chat_database_imported">Base de dades importada</string> + <string name="chat_database_exported_title">Base de dades exportada</string> + <string name="confirm_new_passphrase">Confirmeu el nou mot de pas…</string> + <string name="change_database_passphrase_question">Voleu canviar la frase de pas per a la base de dades?</string> + <string name="rcv_group_event_member_connected">connectat</string> + <string name="rcv_group_event_member_created_contact">connectat directament</string> + <string name="snd_conn_event_switch_queue_phase_changing_for_member">canviant d\'adreça per %s…</string> + <string name="group_member_status_connected">connectat</string> + <string name="group_member_status_complete">complet</string> + <string name="change_verb">Canvia</string> + <string name="info_row_chat">Xat</string> + <string name="operator_conditions_accepted_on">Condicions acceptades el: %s.</string> + <string name="operator_conditions_of_use">Condicions d\'ús</string> + <string name="color_mode">Mode de color</string> + <string name="chat_preferences">Preferències del xat</string> + <string name="v4_4_verify_connection_security_desc">Compareu els codis de seguretat amb els vostres contactes.</string> + <string name="v6_0_connection_servers_status_descr">Estat de la connexió i dels servidors</string> + <string name="connected_to_mobile">Connectat a un mòbil</string> + <string name="in_developing_title">Pròximament!</string> + <string name="multicast_connect_automatically">Connecta automàticament</string> + <string name="connect_plan_chat_already_exists">El xat ja existeix!</string> + <string name="migrate_to_device_chat_migrated">Xat migrat!</string> + <string name="migrate_from_device_check_connection_and_try_again">Comproveu la connexió a Internet i torneu a provar</string> + <string name="servers_info_connected_servers_section_header">Servidors connectats</string> + <string name="chunks_deleted">Blocs eliminats</string> + <string name="chunks_downloaded">Blocs descarregats</string> + <string name="error_saving_file">Error en desar el fitxer</string> + <string name="switch_receiving_address_question">Canviar l\'adreça de recepció?</string> + <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Podeu <font color="#0088ff">connectar amb els desenvolupadors de SimpleX Chat per fer qualsevol pregunta i rebre actualitzacions</font>.]]></string> + <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[També us podeu connectar fent clic a l\'enllaç. Si s\'obre al navegador, feu clic al botó <b>Obre a l\'aplicació mòbil</b>.]]></string> + <string name="error_saving_ICE_servers">Error en desar els servidors ICE</string> + <string name="network_proxy_incorrect_config_title">Error en desar el servidor intermediari</string> + <string name="chat_database_section">BASE DE DADES DELS XATS</string> + <string name="chat_is_running">El xat s\'està executant</string> + <string name="chat_is_stopped">El xat està aturat</string> + <string name="error_with_info">Error: %s</string> + <string name="chat_is_stopped_indication">El xat està aturat</string> + <string name="chat_is_stopped_you_should_transfer_database">El xat està aturat. Si ja heu utilitzat aquesta base de dades en un altre dispositiu, hauríeu de tornar-la a transferir abans d\'iniciar el xat.</string> + <string name="rcv_group_event_changed_member_role">ha canviat el rol de %s a %s</string> + <string name="rcv_group_event_changed_your_role">ha canviat el teu rol a %s</string> + <string name="rcv_conn_event_switch_queue_phase_completed">ha canviat d\'adreça per a tu</string> + <string name="rcv_conn_event_switch_queue_phase_changing">canviant d\'adreça…</string> + <string name="snd_conn_event_switch_queue_phase_changing">canviant d\'adreça…</string> + <string name="delete_chat_for_self_cannot_undo_warning">El xat se suprimirà; això no es pot desfer!</string> + <string name="change_member_role_question">Canviar el rol del grup?</string> + <string name="change_role">Canviar rol</string> + <string name="switch_receiving_address">Canvia l\'adreça de recepció</string> + <string name="error_saving_group_profile">Error en desar el perfil del grup</string> + <string name="network_type_cellular">Mòbil</string> + <string name="servers_info_sessions_errors">Errors</string> + <string name="icon_descr_contact_checked">Contacte verificat</string> + <string name="delete_contact_all_messages_deleted_cannot_undo_warning">El contacte i tots els missatges s\'eliminaran, l\'acció és irreversible!</string> + <string name="display_name_connection_established">connexió establerta</string> + <string name="connection_error">Error de connexió</string> + <string name="connection_timeout">Temps de connexió enhaurit</string> + <string name="contact_already_exists">El contacte la existeix</string> + <string name="connection_error_auth">Error de connexió (AUTH)</string> + <string name="notification_preview_mode_contact">Nom del contacte</string> + <string name="contact_deleted">Contacte eliminat!</string> + <string name="alert_title_contact_connection_pending">El contacte encara no s\'hi ha connectat!</string> + <string name="connection_request_sent">Sol·licitud de connexió tramesa!</string> + <string name="connect_via_link">Connecta via enllaç</string> + <string name="status_contact_has_e2e_encryption">el contacte admet criptografia d\'extrem a extrem</string> + <string name="status_contact_has_no_e2e_encryption">el contacte no té criptografía d\'extrem a extrem</string> + <string name="cant_call_contact_deleted_alert_text">El contacte ha estat eliminat</string> + <string name="contact_preferences">Preferències del contacte</string> + <string name="v6_0_connect_faster_descr">Connecteu amb els vostres amic més ràpidament.</string> + <string name="remote_host_was_disconnected_title">Connexió interrompuda</string> + <string name="remote_ctrl_was_disconnected_title">Connexió aturada</string> + <string name="desktop_connection_terminated">Connexió finalitzada</string> + <string name="connect_plan_connect_via_link">Voleu connectar a través d\'un enllaç?</string> + <string name="connections">Connexions</string> + <string name="migrate_from_device_error_uploading_archive">Error en carregar l\'arxiu</string> + <string name="migrate_from_device_error_saving_settings">Error en desar la configuració</string> + <string name="error_stopping_chat">Error en aturar el xat</string> + <string name="error_updating_server_title">Error en actualitzar el servidor</string> + <string name="migrate_to_device_enter_passphrase">Introduïu la contrasenya</string> + <string name="error_starting_chat">Error en iniciar el xat</string> + <string name="error_saving_user_password">Error en desar la contrasenya de l\'usuari</string> + <string name="enter_correct_passphrase">Introduïu la contrasenya correcta.</string> + <string name="enter_passphrase">Introduïu la contrasenya…</string> + <string name="group_display_name_field">Introduïu el nom del grup:</string> + <string name="migrate_from_device_error_verifying_passphrase">Error en verificar la contrasenya:</string> + <string name="callstate_ended">finalitzada</string> + <string name="enter_password_to_show">Introduïu la contrasenya</string> + <string name="error_showing_content">error en mostrar el contingut</string> + <string name="error_showing_message">error en mostrar el missatge</string> + <string name="error_saving_smp_servers">Error en desar els servidors SMP</string> + <string name="error_saving_xftp_servers">Error en desar els servidors XFTP</string> + <string name="error_sending_message">Error en enviar el missatge</string> + <string name="errors_in_servers_configuration">Errors en la configuració dels servidors.</string> + <string name="failed_to_active_user_title">Error en canviar el perfil!</string> + <string name="error_setting_network_config">Error en actualitzar la configuració de la xarxa</string> + <string name="error_setting_address">Error en establir l\'adreça</string> + <string name="error_synchronizing_connection">Error en sincronitzar la connexió</string> + <string name="error_updating_user_privacy">Error en actualitzar la privadesa de l\'usuari</string> + <string name="error_showing_desktop_notification">Error en mostrar la notificació, contacteu amb els desenvolupadors.</string> + <string name="la_enter_app_passcode">Introduïu el codi</string> + <string name="switching_profile_error_title">Error en canviar el perfil</string> + <string name="smp_servers_enter_manually">Introduïu el servidor manualment</string> + <string name="error_sending_message_contact_invitation">Error en enviar la invitació</string> + <string name="error_updating_link_for_group">Error en actualitzar l\'enllaç del grup</string> + <string name="v5_2_disappear_one_message_descr">Fins i tot quan està desactivat a la conversa.</string> + <string name="onboarding_network_operators_continue">Continua</string> + <string name="continue_to_next_step">Continua</string> + <string name="chat_database_exported_continue">Continua</string> + <string name="icon_descr_context">Icona contextual</string> + <string name="delete_contact_cannot_undo_warning">El contacte serà eliminat; l\'acció no es pot desfer!</string> + <string name="conversation_deleted">Conversa eliminada!</string> + <string name="copied">Copiat al porta-retalls</string> + <string name="contribute">Contribuïu</string> + <string name="receipts_section_contacts">Contactes</string> + <string name="servers_info_reset_stats_alert_error_title">Error en restablir les estadístiques</string> + <string name="servers_info_modal_error_title">Error</string> + <string name="servers_info_reconnect_servers_error">Error en reconnectar els servidors</string> + <string name="enter_welcome_message_optional">Introduïu el missatge de benvinguda… (opcional)</string> + <string name="display_name">Introduïu el vostre nom:</string> + <string name="unable_to_open_browser_title">Error en obrir el navegador</string> + <string name="error_exporting_chat_database">Error en exportar la base de dades de xat</string> + <string name="error_deleting_database">S\'ha produït un error en suprimir la base de dades de xat</string> + <string name="error_importing_database">Error en importar la base de dades de xat</string> + <string name="error_changing_message_deletion">Error en canviar la configuració</string> + <string name="error_removing_member">Error en eliminar el membre</string> + <string name="error_blocking_member_for_all">Error en bloquejar el membre per a tots</string> + <string name="error_changing_role">Error en canviar el rol</string> + <string name="error_adding_server">Error en afegir servidor</string> + <string name="migrate_from_device_error_deleting_database">Error en suprimir la base de dades</string> + <string name="servers_info_reconnect_server_error">Error en reconnectar el servidor</string> + <string name="error_encrypting_database">Error en xifrar la base de dades</string> + <string name="migrate_from_device_error_exporting_archive">Error en exportar la base de dades de xat</string> + <string name="chunks_uploaded">Blocs penjats</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">El xat se suprimirà per a tots els membres; això no es pot desfer!</string> + <string name="colored_text">acolorit</string> + <string name="call_desktop_permission_denied_chrome">Feu clic al botó d\'informació al costat del camp d\'adreça per permetre l\'ús del micròfon.</string> + <string name="completed">Completades</string> + <string name="error_accepting_operator_conditions">Error en acceptar les condicions</string> + <string name="smp_proxy_error_connecting">Error en connectar-se al servidor de reenviament %1$s. Si us plau, prova-ho més tard.</string> + <string name="error_creating_message">Error en crear el missatge</string> + <string name="failed_to_create_user_title">Error en crear el perfil!</string> + <string name="error_forwarding_messages">Error en reenviar els missatges</string> + <string name="error_loading_smp_servers">Error en carregar els servidors SMP</string> + <string name="error_loading_xftp_servers">Error en carregar els servidors XFTP</string> + <string name="error_adding_members">Error en afegir membre(s)</string> + <string name="error_creating_address">Error en crear l\'adreça</string> + <string name="error_joining_group">Error en unir-se al grup</string> + <string name="error_loading_details">Error en carregar els detalls</string> + <string name="error_receiving_file">Error en rebre el fitxer</string> + <string name="error_accepting_contact_request">Error en acceptar la sol·licitud de contacte</string> + <string name="error_deleting_contact">S\'ha produït un error en suprimir el contacte</string> + <string name="error_deleting_group">S\'ha produït un error en suprimir el grup</string> + <string name="error_aborting_address_change">Error en cancel·lar el canvi d\'adreça</string> + <string name="error_changing_address">Error en canviar l\'adreça</string> + <string name="error_deleting_contact_request">S\'ha produït un error en suprimir la sol·licitud de contacte</string> + <string name="error_deleting_pending_contact_connection">S\'ha produït un error en suprimir la connexió de contacte pendent</string> + <string name="error_deleting_note_folder">S\'ha produït un error en suprimir les notes privades</string> + <string name="error_deleting_user">S\'ha produït un error en suprimir el perfil d\'usuari</string> + <string name="smp_server_test_compare_file">Comparar el fitxer</string> + <string name="ci_status_other_error">Error: %1$s</string> + <string name="icon_descr_server_status_error">Error</string> + <string name="choose_file_title">Trieu un fitxer</string> + <string name="clear_verb">Netejar</string> + <string name="clear_chat_menu_action">Netejar</string> + <string name="clear_chat_button">Netejar xat</string> + <string name="clear_chat_question">Netejar xat?</string> + <string name="clear_verification">Netejar verificació</string> + <string name="error_initializing_web_view_wrong_arch">Error en inicialitzar WebView. Assegureu-vos que teniu WebView instal·lat i que sigui suportada l\'arquitectura arm64. Error: %s</string> + <string name="error_initializing_web_view">Error en inicialitzar WebView. Actualitzeu el vostre sistema a la nova versió. Poseu-vos en contacte amb els desenvolupadors. Error: %s</string> + <string name="clear_contacts_selection_button">Netejar</string> + <string name="error_creating_link_for_group">Error en crear l\'enllaç del grup</string> + <string name="error_creating_member_contact">Error en crear el contacte del membre</string> + <string name="error_deleting_link_for_group">Error en suprimir l\'enllaç del grup</string> + <string name="enter_welcome_message">Introduïu el missatge de benvinguda…</string> + <string name="v4_6_chinese_spanish_interface">Interfície en xinès i espanyol</string> + <string name="error_enabling_delivery_receipts">Error en habilitar els rebuts de lliurament.</string> + <string name="enter_this_device_name">Introduïu el nom d\'aquest dispositiu…</string> + <string name="error">Error</string> + <string name="migrate_to_device_error_downloading_archive">Error en baixar l\'arxiu</string> + <string name="create_address">Crea una adreça</string> + <string name="create_address_button">Crea</string> + <string name="v5_4_incognito_groups_descr">Crea un grup tot fent servir un perfil aleatori.</string> + <string name="current_user">Perfil actual</string> + <string name="create_1_time_link">Crea un enllaç d\'un sol ús</string> + <string name="dont_create_address">No crear cap adreça</string> + <string name="create_profile_button">Crea</string> + <string name="create_another_profile_button">Crea perfil</string> + <string name="settings_message_shape_corner">Cantonada</string> + <string name="current_passphrase">Frase de pas actual…</string> + <string name="button_create_group_link">Crea un enllaç</string> + <string name="create_secret_group_title">Crea un grup secret</string> + <string name="dont_show_again">No tornis a mostrar</string> + <string name="dark_theme">Tema fosc</string> + <string name="created">Creat</string> + <string name="create_group_button">Crea un grup</string> + <string name="create_group_link">Crea un enllaç de group</string> + <string name="agent_critical_error_title">Errada crítica</string> + <string name="custom_time_picker_custom">personalitzat</string> + <string name="chat_theme_apply_to_dark_mode">Mode fosc</string> + <string name="smp_server_test_create_queue">Crea una cua</string> + <string name="smp_server_test_create_file">Crea un fitxer</string> + <string name="la_current_app_passcode">Codi d\'accés actual</string> + <string name="maximum_supported_file_size">Ara per ara, la mida màxima per als fitxers és %1$s.</string> + <string name="share_one_time_link">Crea un enllaç d\'invitació d\'un sol ús</string> + <string name="create_group">Crea un grupo secreto</string> + <string name="send_disappearing_message_custom_time">Temps personalitzat</string> + <string name="create_chat_profile">Crea un perfil de xat</string> + <string name="create_address_and_let_people_connect">Creeu una adreça perquè la gent pugui connectar amb vós.</string> + <string name="create_simplex_address">Crea una adreça SimpleX</string> + <string name="create_profile">Crea perfil</string> + <string name="group_member_status_creator">creador</string> + <string name="info_row_database_id">ID de la base de dades</string> + <string name="share_text_database_id">ID de la base de dades: %d</string> + <string name="item_info_current">(actual)</string> + <string name="color_mode_dark">Fosc</string> + <string name="theme_dark">Fosc</string> + <string name="create_your_profile">Creeu el vostre perfil</string> + <string name="icon_descr_edited">editat</string> + <string name="onboarding_network_operators_simplex_flux_agreement">SimpleX Chat i Flux han acordat incloure els servidors operats per Flux a l\'aplicació.</string> + <string name="simplex_address_or_1_time_link">Voleu compartir l\'adreça de SimpleX o un enllaç d\'un sol ús?</string> + <string name="toolbar_settings">Configuració</string> + <string name="settings_shutdown">Tanca</string> + <string name="contact_connection_pending">s\'està connectant…</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">Les condicions s\'acceptaran per als operadors habilitats després de 30 dies.</string> + <string name="system_restricted_background_desc">SimpleX no pot funcionar en segon pla. Només rebreu les notificacions quan obriu l\'aplicació.</string> + <string name="ntf_channel_calls">Trucades de SimpleX chat</string> + <string name="ntf_channel_messages">Missatges de xat de SimpleX</string> + <string name="icon_descr_sent_msg_status_sent">enviat</string> + <string name="icon_descr_received_msg_status_unread">per llegir</string> + <string name="welcome">Benvinguts!</string> + <string name="send_disappearing_message_send">Envia</string> + <string name="send_verb">Envia</string> + <string name="reset_verb">Restableix</string> + <string name="choose_file">Fitxer</string> + <string name="gallery_image_button">Imatge</string> + <string name="gallery_video_button">Vídeo</string> + <string name="mute_chat">Silencia</string> + <string name="unmute_chat">Activa el so</string> + <string name="favorite_chat">Favorit</string> + <string name="icon_descr_settings">Configuració</string> + <string name="icon_descr_help">ajuda</string> + <string name="icon_descr_email">Correu electrònic</string> + <string name="icon_descr_address">Adreça de SimpleX</string> + <string name="icon_descr_more_button">Més</string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">Compartir els enllaços d\'un sol ús i les adreces de SimpleX és segur a través de qualsevol mitjà.</string> + <string name="paste_button">Enganxa</string> + <string name="keep_invitation_link">Desa</string> + <string name="retry_verb">Torna-ho a provar</string> + <string name="simplex_address">Adreça de SimpleX</string> + <string name="save_servers_button">Desa</string> + <string name="network_proxy_username">Nom d\'usuari</string> + <string name="network_proxy_password">Contrasenya</string> + <string name="host_verb">Amfitrió</string> + <string name="port_verb">Port</string> + <string name="network_use_onion_hosts_no">No</string> + <string name="network_use_onion_hosts_required">Requerit</string> + <string name="network_smp_proxy_mode_unprotected">Amb IP desprotegida</string> + <string name="network_smp_proxy_mode_never">Mai</string> + <string name="network_smp_proxy_fallback_allow">Sí</string> + <string name="network_smp_proxy_fallback_prohibit">No</string> + <string name="app_check_for_updates_stable">Estable</string> + <string name="show_dev_options">Mostra:</string> + <string name="hide_dev_options">Amaga:</string> + <string name="theme_simplex">SimpleX</string> + <string name="v4_2_security_assessment_desc">La seguretat de SimpleX chat ha estat auditada per Trail of Bits.</string> + <string name="email_invite_subject">Parlem a SimpleX Chat</string> + <string name="invalid_name">El nom no és vàlid!</string> + <string name="italic_text">cursiva</string> + <string name="make_private_connection">Crea una connexió privada</string> + <string name="incoming_audio_call">Trucada d\'àudio entrant</string> + <string name="incorrect_passcode">Codi d\'accés incorrecte</string> + <string name="lock_after">Bloca després</string> + <string name="lock_mode">Mode de blocatge</string> + <string name="settings_section_title_incognito">Mode d\'incògnit</string> + <string name="member_info_member_inactive">inactiu</string> + <string name="chat_theme_apply_to_light_mode">Mode clar</string> + <string name="v5_4_incognito_groups">Grups d\'incògnit</string> + <string name="member_info_section_title_member">MEMBRE</string> + <string name="join_group_question">Voleu unir-vos al grup?</string> + <string name="leave_group_button">Surt</string> + <string name="leave_chat_question">Voleu sortir del xat?</string> + <string name="leave_group_question">Voleu sortir del grup?</string> + <string name="alert_title_group_invitation_expired">Invitació caducada!</string> + <string name="icon_descr_add_members">Convida membres</string> + <string name="group_member_status_invited">convidat</string> + <string name="group_member_status_left">ha sortit</string> + <string name="v6_0_private_routing_descr">Protegeix la vostra adreça IP i les vostres connexions.</string> + <string name="v5_7_call_sounds">So de trucada</string> + <string name="incoming_video_call">Videotrucada entrant</string> + <string name="conn_level_desc_indirect">indirecte (%1$s)</string> + <string name="group_invitation_item_description">invitació al grup %1$s</string> + <string name="invite_friends_short">Convida</string> + <string name="desktop_incompatible_version">Versió incompatible</string> + <string name="v5_7_new_interface_languages">Interfície en lituà</string> + <string name="agent_internal_error_title">Error intern</string> + <string name="v6_0_increase_font_size">Augmenta la mida de la lletra.</string> + <string name="info_row_local_name">Nom local</string> + <string name="invalid_file_link">Enllaç no vàlid</string> + <string name="action_button_add_members">Convida</string> + <string name="invite_friends">Convida amics</string> + <string name="v4_5_italian_interface">Interfície en italià</string> + <string name="non_content_uri_alert_title">Ruta de fitxer no vàlida</string> + <string name="invalid_data">dades no vàlides</string> + <string name="in_reply_to">En resposta a</string> + <string name="download_file">Descarrega</string> + <string name="member_inactive_title">Membre inactiu</string> + <string name="keep_conversation">Conserva la conversa</string> + <string name="mark_unread">Marcar com ha llegit</string> + <string name="invalid_contact_link">Enllaç no vàlid</string> + <string name="invalid_QR_code">El codi QR no és vàlid</string> + <string name="learn_more">Més informació</string> + <string name="invalid_qr_code">El codi QR no és vàlid</string> + <string name="incorrect_code">Codi de seguretat incorrecte!</string> + <string name="smp_servers_invalid_address">L\'adreça del servidor no és vàlida!</string> + <string name="markdown_help">Ajuda sobre Markdown</string> + <string name="markdown_in_messages">Markdown en els missatges</string> + <string name="network_proxy_incorrect_config_desc">Assegureu-vos que la configuració del servidor intermediari és correcta.</string> + <string name="app_check_for_updates_button_install">Instal·la l\'actualització</string> + <string name="app_check_for_updates_installed_successfully_title">"Instal·lació completada"</string> + <string name="privacy_media_blur_radius_medium">Mitjà</string> + <string name="incompatible_database_version">La versió de la base de dades és incompatible</string> + <string name="rcv_group_event_member_added">ha convidat %1$s</string> + <string name="rcv_group_event_member_left">ha sortit</string> + <string name="group_member_role_member">membre</string> + <string name="invite_to_group_button">Convida al grup</string> + <string name="button_leave_chat">Surt del xat</string> + <string name="button_leave_group">Surt del grup</string> + <string name="incognito">Incògnit</string> + <string name="incognito_info_protects">El mode d\'incògnit protegeix la vostra privacitat usant un perfil aleatori nou per a cada contacte.</string> + <string name="color_mode_light">Clar</string> + <string name="theme_light">Clar</string> + <string name="group_members_can_send_dms">Els membres poden enviar missatges directes.</string> + <string name="group_members_can_send_files">Els membres poden enviar fitxers i multimèdia.</string> + <string name="correct_name_to">Corregir el nom a %s?</string> + <string name="callstate_connecting">connectant…</string> + <string name="encrypted_audio_call">Trucada amb xifratge d\'extrem a extrem</string> + <string name="encrypted_video_call">Videotrucada amb xifratge d\'extrem a extrem</string> + <string name="status_e2e_encrypted">xifrat d\'extrem a extrem</string> + <string name="allow_accepting_calls_from_lock_screen">Activa les trucades des de la pantalla de bloqueig mitjançant Configuració.</string> + <string name="integrity_msg_duplicate">missatge duplicat</string> + <string name="enable_lock">Activa bloqueig</string> + <string name="receipts_contacts_enable_keep_overrides">Activar (conservar anul·lacions)</string> + <string name="receipts_groups_enable_keep_overrides">Activa (mantenir les anul·lacions de grup)</string> + <string name="enable_automatic_deletion_question">Activar la supressió automàtica de missatges?</string> + <string name="info_row_created_at">Creat a</string> + <string name="connect_via_member_address_alert_title">Connectar-se directament?</string> + <string name="info_row_connection">Connexió</string> + <string name="operator_conditions_failed_to_load">El text de les condicions actuals no s\'ha pogut carregar, podeu revisar les condicions mitjançant aquest enllaç:</string> + <string name="icon_descr_call_connecting">Trucada en connexió</string> + <string name="migrate_to_device_downloading_archive">Baixant l\'arxiu</string> + <string name="ttl_week">%d setmana</string> + <string name="confirm_password">Confirmeu la contrasenya</string> + <string name="group_member_status_introduced">connectant (presentat)</string> + <string name="group_member_status_announced">connectant (anunciat)</string> + <string name="group_member_status_intro_invitation">connectant (invitació de presentació)</string> + <string name="ttl_w">%ds</string> + <string name="ttl_weeks">%d setmanes</string> + <string name="v6_0_upgrade_app_descr">Baixa noves versions de GitHub.</string> + <string name="v6_2_network_decentralization_enable_flux">Habiliteu Flux a la configuració de la xarxa i dels servidors per obtenir una millor privadesa de les metadades.</string> + <string name="connect_to_desktop">Connectar amb l\'ordinador</string> + <string name="servers_info_sessions_connecting">Connectant</string> + <string name="download_errors">Errors de descàrrega</string> + <string name="v5_8_safe_files_descr">Confirmeu fitxers de servidors desconeguts.</string> + <string name="operator_conditions_will_be_accepted_on">Les condicions s\'acceptaran el dia: %s.</string> + <string name="servers_info_sessions_connected">Connectades</string> + <string name="downloaded_files">Arxius descarregats</string> + <string name="servers_info_downloaded">Descarregat</string> + <string name="group_member_status_accepted">connectant (acceptat)</string> + <string name="connected_desktop">Ordinador connectat</string> + <string name="connected_to_desktop">Connectat amb l\'ordinador</string> + <string name="callstatus_connecting">trucada en connexió…</string> + <string name="migrate_from_device_creating_archive_link">Creant un enllaç a l\'arxiu</string> + <string name="connecting_to_desktop">Connectant amb l\'ordinador</string> + <string name="v5_3_discover_join_groups_descr">- Connexió al servei de directoris (BETA)!\n- Confirmacions de lliurament (fins a 20 membres).\n- Més ràpid i més estable.</string> + <string name="receipts_contacts_enable_for_all">Habilita per a tothom</string> + <string name="profile_update_event_contact_name_changed">el contacte %1$s ha canviat a %2$s</string> + <string name="chat_preferences_contact_allows">El contacte permet</string> + <string name="ttl_s">%ds</string> + <string name="duplicates_label">duplicats</string> + <string name="edit_image">Editar imatge</string> + <string name="enable_receipts_all">Activar</string> + <string name="receipts_groups_enable_for_all">Habilita per a tots els grups</string> + <string name="v5_6_quantum_resistant_encryption_descr">Habilita als xats directes (BETA)!</string> + <string name="copy_error">Copiar error</string> + <string name="migrate_to_device_download_failed">La baixada ha fallat</string> + <string name="ttl_sec">%d seg</string> + <string name="connect_via_contact_link">Connectar mitjançant l\'adreça de contacte?</string> + <string name="connect_via_invitation_link">Conectar mitjançant enllaç d\'invitació?</string> + <string name="failed_to_create_user_duplicate_title">Nom de visualització duplicat!</string> + <string name="smp_server_test_download_file">Descarregar fitxer</string> + <string name="notification_preview_somebody">Contacte ocult:</string> + <string name="la_seconds">%d segons</string> + <string name="group_connection_pending">connectant…</string> + <string name="connect_with_contact_name_question">Connectar amb %1$s?</string> + <string name="forward_files_not_accepted_receive_files">Descarregar</string> + <string name="sync_connection_desc">La connexió requereix una renegociació del xifratge.</string> + <string name="connect_via_link_or_qr">Connectar mitjançant enllaç/codi QR</string> + <string name="enable_camera_access">Habilita l\'accés a la càmera</string> + <string name="connection_security">Seguretat de connexió</string> + <string name="creating_link">Creant enllaç…</string> + <string name="xftp_servers_configured">Servidors XFTP configurats</string> + <string name="network_proxy_auth_mode_no_auth">No utilitzeu credencials amb servidor intermediari.</string> + <string name="core_version">Versió del nucli: v%s</string> + <string name="customize_theme_title">Personalitzar tema</string> + <string name="network_smp_proxy_mode_never_description">NO utilitzeu l\'encaminament privat.</string> + <string name="app_check_for_updates_download_started">S\'està baixant l\'actualització de l\'aplicació, no la tanquis</string> + <string name="app_check_for_updates_button_download">Baixa %s (%s)</string> + <string name="developer_options">ID de bases de dades i opció d\'aïllament de transport.</string> + <string name="audio_device_earpiece">Auriculars</string> + <string name="database_error">Error de base de dades</string> + <string name="confirm_database_upgrades">Confirmeu les actualitzacions de la base de dades</string> + <string name="database_downgrade">Reducció de versió de la base de dades</string> + <string name="downgrade_and_open_chat">Baixa la versió anterior i obri el xat</string> + <string name="group_member_status_connecting">connectant</string> + <string name="button_edit_group_profile">Editar perfil del grup</string> + <string name="share_text_created_at">Creat a: %s</string> + <string name="cant_call_contact_connecting_wait_alert_text">Connectant al contacte, si us plau, espereu o comproveu-ho més tard!</string> + <string name="operator_conditions_accepted_for_enabled_operators_on">Les condicions s\'acceptaran automàticament per als operadors habilitats el dia: %s.</string> + <string name="dark_mode_colors">Colors en mode fosc</string> + <string name="feature_enabled_for_contact">habilitat per al contacte</string> + <string name="feature_enabled">habilitat</string> + <string name="feature_enabled_for_you">habilitat per a tu</string> + <string name="contacts_can_mark_messages_for_deletion">Els contactes poden marcar missatges per suprimir-los. Encara els podreu veure.</string> + <string name="feature_enabled_for">Habilitat per</string> + <string name="v5_1_custom_themes_descr">Personalitza i comparteix temes de color.</string> + <string name="v5_1_custom_themes">Temes personalitzats</string> + <string name="v5_3_new_desktop_app_descr">Crea perfil nou a l\'aplicació per a ordinador. 💻</string> + <string name="v6_1_customizable_message_descr">Forma de missatge personalitzable.</string> + <string name="dont_enable_receipts">No activeu</string> + <string name="connect_plan_connect_to_yourself">Connectar amb tu mateix?</string> + <string name="migrate_to_device_confirm_network_settings">Confirmeu la configuració de la xarxa</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Confirmeu que recordeu la contrasenya de la base de dades per migrar-la.</string> + <string name="remote_ctrl_error_bad_state">La connexió a l\'ordinador és deficient</string> + <string name="migrate_from_device_confirm_upload">Confirmar la càrrega</string> + <string name="v6_0_connection_servers_status">Controla la teva xarxa</string> + <string name="migrate_to_device_downloading_details">Baixant els detalls de l\'enllaç</string> + <string name="failed_to_save_servers">Error en desar els servidors</string> + <string name="debug_logs">Habilita els registres</string> + <string name="for_social_media">Per a xarxes socials</string> + <string name="full_name__field">Nom complet:</string> + <string name="exit_without_saving">Sortir sense desar</string> + <string name="hidden_profile_password">Contrasenya de perfil amagada</string> + <string name="permissions_required">Concedir permisos</string> + <string name="onboarding_network_operators_app_will_use_for_routing">Per exemple, si el vostre contacte rep missatges mitjançant un servidor SimpleX Chat, la vostra aplicació els lliurarà mitjançant un servidor Flux.</string> + <string name="icon_descr_flip_camera">Gira la càmera</string> + <string name="enabled_self_destruct_passcode">Activar codi d\'autodestrucció</string> + <string name="receipts_contacts_title_enable">Activar els rebuts?</string> + <string name="enable_self_destruct">Activar autodestrucció</string> + <string name="receipts_groups_title_enable">Activar els rebuts per a grups?</string> + <string name="settings_section_title_files">FITXERS</string> + <string name="settings_section_title_experimenta">EXPERIMENTAL</string> + <string name="export_database">Exportar base de dades</string> + <string name="encrypt_database">Xifrar</string> + <string name="file_with_path">Fitxer: %s</string> + <string name="info_row_group">Grup</string> + <string name="fix_connection">Arreglar connexió</string> + <string name="group_is_decentralized">Totalment descentralitzat: només visible per als membres.</string> + <string name="v4_6_group_moderation">Moderació de grups</string> + <string name="v4_6_group_welcome_message">Missatge de benvinguda als grups</string> + <string name="icon_descr_group_inactive">Grup inactiu</string> + <string name="alert_message_group_invitation_expired">La invitació de grup ja no és vàlida, el remitent l\'ha eliminada.</string> + <string name="alert_title_no_group">Grup no trobat!</string> + <string name="conn_event_ratchet_sync_required">es requereix renegociar el xifratge</string> + <string name="group_member_status_group_deleted">grup esborrat</string> + <string name="section_title_for_console">PER A CONSOLA</string> + <string name="fix_connection_question">Arreglar connexió?</string> + <string name="fix_connection_not_supported_by_group_member">Correcció no suportada per membre del grup</string> + <string name="group_full_name_field">Nom complet del grup:</string> + <string name="wallpaper_scale_fill">Farciment</string> + <string name="wallpaper_scale_fit">Encaix</string> + <string name="v4_2_group_links">Enllaços de grup</string> + <string name="v4_6_hidden_chat_profiles">Perfils de xat ocults</string> + <string name="v5_3_encrypt_local_files">Xifra els fitxers i els mitjans emmagatzemats</string> + <string name="found_desktop">Ordinador trobat</string> + <string name="connect_plan_group_already_exists">El grup ja existeix!</string> + <string name="migrate_from_device_finalize_migration">Finalitzar la migració</string> + <string name="expired_label">expirats</string> + <string name="conn_event_ratchet_sync_agreed">xifratge acordat</string> + <string name="v5_4_better_groups_descr">Unió més ràpida i missatges més fiables.</string> + <string name="network_option_enable_tcp_keep_alive">Activar TCP keep-alive</string> + <string name="migrate_to_device_file_delete_or_link_invalid">S\'ha suprimit el fitxer o l\'enllaç no és vàlid</string> + <string name="error_saving_database">Error en desar la base de dades</string> + <string name="conn_event_ratchet_sync_ok">xifratge ok</string> + <string name="icon_descr_hang_up">Penjar</string> + <string name="wallpaper_preview_hello_bob">Bon dia!</string> + <string name="group_preferences">Preferències del grup</string> + <string name="servers_info_files_tab">Fitxers</string> + <string name="fix_connection_not_supported_by_contact">Correcció no suportada pel contacte</string> + <string name="fix_connection_confirm">Arreglar</string> + <string name="v6_2_network_decentralization_enable_flux_reason">per a una millor privadesa de les metadades.</string> + <string name="wallpaper_preview_hello_alice">Bona tarda!</string> + <string name="migrate_from_device_exported_file_doesnt_exist">El fitxer exportat no existeix</string> + <string name="v5_7_forward">Reenvia i desa missatges</string> + <string name="email_invite_body">Hola!\nConnecteu-vos amb mi mitjançant SimpleX Chat: %s</string> + <string name="settings_experimental_features">Característiques experimentals</string> + <string name="alert_text_encryption_renegotiation_failed">La renegociació del xifratge ha fallat.</string> + <string name="conn_event_ratchet_sync_allowed">renegociació de xifratge permesa</string> + <string name="encryption_renegotiation_error">Error de renegociació de xifratge</string> + <string name="simplex_link_mode_full">Enllaç complet</string> + <string name="failed_to_parse_chat_title">No s\'ha pogut carregar el xat</string> + <string name="failed_to_parse_chats_title">No s\'han pogut carregar els xats</string> + <string name="for_chat_profile">Per al perfil de xat %s:</string> + <string name="proxy_destination_error_failed_to_connect">El servidor de reenviament %1$s no s\'ha pogut connectar al servidor de destinació %2$s. Si us plau, prova-ho més tard.</string> + <string name="smp_proxy_error_broker_host">L\'adreça del servidor de reenviament és incompatible amb la configuració de xarxa: %1$s.</string> + <string name="smp_proxy_error_broker_version">La versió del servidor de reenviament és incompatible amb la configuració de xarxa: %1$s.</string> + <string name="possible_slow_function_desc">L\'execució de la funció triga massa temps: %1$d segons: %2$s</string> + <string name="call_service_notification_end_call">Trucada finalitzada</string> + <string name="auth_enable_simplex_lock">Activar Bloqueig SimpleX</string> + <string name="snd_error_proxy">Servidor de reenviament: %1$s\nError: %2$s</string> + <string name="snd_error_proxy_relay">Servidor de reenviament: %1$s\nError del servidor de destinació: %2$s</string> + <string name="file_error_no_file">No s\'ha trobat el fitxer: el més probable és que s\'hagi suprimit o cancel·lat.</string> + <string name="file_error_relay">Error del servidor de fitxers: %1$s</string> + <string name="forwarded_from_chat_item_info_title">Reenviat des de</string> + <string name="for_everybody">Per a tothom</string> + <string name="revoke_file__message">El fitxer s\'eliminarà dels servidors.</string> + <string name="forward_chat_item">Reenviar</string> + <string name="forward_alert_title_messages_to_forward">Reenviar %1$s missatges?</string> + <string name="forward_alert_forward_messages_without_files">Reenviar missatges sense fitxers?</string> + <string name="forward_message">Reenviar missatge…</string> + <string name="forward_multiple">Reenviar missatges…</string> + <string name="files_and_media_prohibited">Arxius i mitjans prohibits!</string> + <string name="compose_forward_messages_n">Reenviant %1$s missatges</string> + <string name="files_and_media_not_allowed">No es permeten fitxers ni suports</string> + <string name="icon_descr_file">Fitxer</string> + <string name="file_error">Error de fitxer</string> + <string name="file_not_found">No s\'ha trobat el fitxer</string> + <string name="file_saved">Fitxer desat</string> + <string name="file_will_be_received_when_contact_completes_uploading">El fitxer es rebrà quan el vostre contacte acabi de carregar-lo.</string> + <string name="file_will_be_received_when_contact_is_online">El fitxer es rebrà quan el vostre contacte estigui en línia, espereu o comproveu més tard!</string> + <string name="sync_connection_question">Arreglar connexió?</string> + <string name="sync_connection_confirm">Arreglar</string> + <string name="encryption_renegotiation_in_progress">Renegociació de xifratge en curs.</string> + <string name="from_gallery_button">De la Galeria</string> + <string name="permissions_find_in_settings_and_grant">Cerqueu aquest permís a la configuració d\'Android i concediu-lo manualment.</string> + <string name="permissions_grant_in_settings">Concedir a la configuració</string> + <string name="permissions_grant">Donar permís(os) per fer trucades</string> + <string name="audio_device_wired_headphones">Auriculars</string> + <string name="encrypt_local_files">Xifra fitxers locals</string> + <string name="settings_section_title_help">AJUT</string> + <string name="files_and_media_section">Arxius i mitjans</string> + <string name="encrypt_database_question">Xifrar base de dades?</string> + <string name="encrypted_database">Base de dades xifrada</string> + <string name="group_invitation_expired">Invitació del grup caducada</string> + <string name="snd_group_event_group_profile_updated">perfil de grup actualitzat</string> + <string name="snd_conn_event_ratchet_sync_agreed">xifratge acordat per a %s</string> + <string name="snd_conn_event_ratchet_sync_ok">xifratge correcte per a %s</string> + <string name="snd_conn_event_ratchet_sync_allowed">renegociació de xifratge permesa per a %s</string> + <string name="snd_conn_event_ratchet_sync_required">es requereix renegociar el xifratge per a %s</string> + <string name="icon_descr_expand_role">Amplieu la selecció de rols</string> + <string name="delete_group_for_all_members_cannot_undo_warning">El grup se suprimirà per a tots els membres; això no es pot desfer!</string> + <string name="group_link">Enllaç de grup</string> + <string name="delete_group_for_self_cannot_undo_warning">El grup se suprimirà per tu; això no es pot desfer!</string> + <string name="info_row_file_status">Estat del fitxer</string> + <string name="share_text_file_status">Estat del fitxer: %s</string> + <string name="group_profile_is_stored_on_members_devices">El perfil del grup s\'emmagatzema als dispositius dels membres, no als servidors.</string> + <string name="operator_use_for_messages_private_routing">Per a l\'encaminament privat</string> + <string name="export_theme">xifratge correcte</string> + <string name="appearance_font_size">Mida de la lletra</string> + <string name="files_and_media">Arxius i mitjans</string> + <string name="files_are_prohibited_in_group">Els fitxers i els mitjans estan prohibits.</string> + <string name="v4_4_french_interface">Interfície en francès</string> + <string name="v5_0_large_files_support_descr">Ràpid i sense esperar fins que el remitent estigui en línia!</string> + <string name="v5_1_message_reactions_descr">Per fi, els tenim! 🚀</string> + <string name="v4_6_reduced_battery_usage">Ús de la bateria encara més reduït</string> + <string name="v5_2_favourites_filter_descr">Filtra els xats preferits i no llegits.</string> + <string name="v5_2_favourites_filter">Trobar xats més ràpidament</string> + <string name="v5_2_fix_encryption_descr">Corregir el xifratge després de restaurar les còpies de seguretat.</string> + <string name="v6_1_forward_many_messages_descr">Reenvia fins a 20 missatges alhora.</string> + <string name="migrate_to_device_finalize_migration">Finalitzar la migració a un altre dispositiu.</string> + <string name="callstatus_missed">trucada perduda</string> + <string name="permissions_record_audio">Micròfon</string> + <string name="icon_descr_call_missed">Trucada perduda</string> + <string name="privacy_message_draft">Esborrany</string> + <string name="remote_hosts_section">Mòbils remots</string> + <string name="remove_passphrase">Elimina</string> + <string name="remove_member_confirmation">Expulsa</string> + <string name="rcv_group_event_user_deleted">t\'ha expulsat</string> + <string name="group_member_status_removed">eliminat</string> + <string name="renegotiate_encryption">Renegocia l\'encriptació</string> + <string name="custom_time_unit_months">mesos</string> + <string name="migrate_to_device_title">Migra aquí</string> + <string name="servers_info_messages_sent">Missatges enviats</string> + <string name="servers_info_messages_received">Missatges rebuts</string> + <string name="button_remove_member">Expulsar membre</string> + <string name="custom_time_unit_minutes">minuts</string> + <string name="rcv_group_event_member_deleted">ha expulsat %1$s</string> + <string name="moderated_item_description">moderat per %s</string> + <string name="message_delivery_error_title">Error en l\'enviament del missatge</string> + <string name="member_inactive_desc">El missatge podrà ser lliurat si el membre esdevé actiu.</string> + <string name="compose_message_placeholder">Missatge</string> + <string name="maximum_message_size_title">El missatge és massa llarg!</string> + <string name="info_view_message_button">missatge</string> + <string name="settings_section_title_messages">MISSATGES I FITXERS</string> + <string name="messages_section_title">Missatges</string> + <string name="info_row_message_status">Estat del missatge</string> + <string name="share_text_message_status">Estat del missatge: %s</string> + <string name="message_too_large">Missatge massa llarg</string> + <string name="color_surface">Menús i alertes</string> + <string name="theme_remove_image">Eliminar la imatge</string> + <string name="group_members_can_send_voice">Els membres poden enviar missatges de veu.</string> + <string name="save_preferences_question">Voleu desar les preferències?</string> + <string name="network_options_save">Desa</string> + <string name="save_passphrase_in_keychain">Desa la frase de pas al Keystore</string> + <string name="save_passphrase_and_open_chat">Desa la frase de pas i obre el xat</string> + <string name="network_options_save_and_reconnect">Desa i reconnecta</string> + <string name="wallpaper_scale_repeat">Repeteix</string> + <string name="v5_6_safer_groups">Grups més segurs</string> + <string name="migrate_to_device_repeat_download">Repeteix la descàrrega</string> + <string name="migrate_to_device_repeat_import">Repetir importació</string> + <string name="restore_database">Restaura la còpia de la base de dades</string> + <string name="group_members_2">%s i %s</string> + <string name="restore_database_alert_confirm">Restaura</string> + <string name="restore_database_alert_title">Voleu restaurar la còpia de la base de dades?</string> + <string name="notifications_mode_off">Solament quan l\'app és oberta</string> + <string name="revoke_file__title">Voleu revocar el fitxer?</string> + <string name="sync_connection_force_question">Voleu renegociar l\'encriptació?</string> + <string name="smp_servers_save">Desa els servidors</string> + <string name="saved_message_title">Missatge desat</string> + <string name="save_group_profile">Desa el perfil del grup</string> + <string name="waiting_for_mobile_to_connect">Esperant connexió mòbil:</string> + <string name="you_can_hide_or_mute_user_profile">Podeu amagar o silenciar un perfil d\'usuari; manteniu-lo premut per al menú.</string> + <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">Ja esteu connectant mitjançant aquest enllaç d\'un sol ús!</string> + <string name="you_can_share_group_link_anybody_will_be_able_to_connect">Pots compartir un enllaç o un codi QR; qualsevol es podrà unir al grup. No perdràs membres del grup si més tard el suprimeixes.</string> + <string name="onboarding_network_operators_configure_via_settings">Podeu configurar servidors mitjançant la configuració.</string> + <string name="migrate_to_device_try_again">Pots intentar-ho un altre cop.</string> + <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">La vostra base de dades de xat actual s\'ELIMINARÀ i SUBSTITUÏRÀ per la importada.\nAquesta acció no es pot desfer: el vostre perfil, contactes, missatges i fitxers es perdran de manera irreversible.</string> + <string name="description_you_shared_one_time_link_incognito">has compartit enllaç d\'un sol ús en mode incògnit</string> + <string name="connect_plan_repeat_connection_request">Repetir la sol·licitud de connexió?</string> + <string name="servers_info_missing">Sense informació, prova de tornar a carregar</string> + <string name="alert_text_skipped_messages_it_can_happen_when">Pot passar quan:\n1. Els missatges van caducar al client d\'enviament al cap de 2 dies o al servidor després de 30 dies.\n2. No s\'ha pogut desxifrar el missatge, perquè tu o el teu contacte feien servir una còpia de seguretat de la base de dades antiga.\n3. La connexió s\'ha compromès.</string> + <string name="users_delete_data_only">Només dades de perfil local</string> + <string name="connect_plan_join_your_group">Unir-se al teu grup?</string> + <string name="delete_message_mark_deleted_warning">El missatge es marcarà per suprimir-lo. Els destinataris podran visualitzar aquest missatge.</string> + <string name="v6_0_new_chat_experience">Nova experiència de xat 🎉</string> + <string name="join_group_button">Unir-te</string> + <string name="v5_5_simpler_connect_ui">Enganxar enllaç per connectar!</string> + <string name="old_database_archive">Arxiu de bases de dades antigues</string> + <string name="migrate_from_another_device">Migrar des d\'un altre dispositiu</string> + <string name="notifications_will_be_hidden">Les notificacions només es lliuraran fins que l\'aplicació s\'aturi!</string> + <string name="operator_open_changes">Obrir canvis</string> + <string name="profile_update_event_member_name_changed">el membre %1$s ha canviat a %2$s</string> + <string name="v5_8_chat_themes_descr">Canvia l\'aspecte dels teus xats!</string> + <string name="v4_3_voice_messages_desc">Màxim 40 segons, rebut a l\'instant.</string> + <string name="only_you_can_delete_messages">Només vos podreu suprimir els missatges de manera irreversible (el vostre contacte pot marcar-los per suprimir-los). (24 hores)</string> + <string name="open_server_settings_button">Obrir configuració del servidor</string> + <string name="other_errors">altres errors</string> + <string name="v5_4_more_things_descr">- Notificació opcional als contactes suprimits.\n- Noms de perfil amb espais.\n- I més!</string> + <string name="servers_info_subscriptions_connections_pending">Pendents</string> + <string name="delete_messages_cannot_be_undone_warning">Els missatges se suprimiran; això no es pot desfer!</string> + <string name="v5_7_network_descr">Connexió de xarxa més fiable.</string> + <string name="new_mobile_device">Nou dispositiu mòbil</string> + <string name="v6_2_improved_chat_navigation_descr">- El xat s\'obre amb al primer missatge no llegit.\n- Desplaçaments fins als missatges citats.</string> + <string name="call_desktop_permission_denied_safari">Obriu Configuració de Safari / Llocs web / Micròfon i, a continuació, trieu Permetre localhost.</string> + <string name="operator_server_alert_title">Servidor de l\'operador</string> + <string name="or_paste_archive_link">O enganxeu l\'enllaç de l\'arxiu</string> + <string name="migrate_from_device_or_share_this_file_link">O compartiu aquest enllaç de fitxer de manera segura</string> + <string name="icon_descr_sent_msg_status_send_failed">enviament fallat</string> + <string name="sending_delivery_receipts_will_be_enabled">L\'enviament de rebuts de lliurament s\'habilitarà per a tots els contactes.</string> + <string name="server_address">Adreça del servidor</string> + <string name="migrate_from_device_bytes_uploaded">%s pujades</string> + <string name="network_option_tcp_connection">Connexió TCP</string> + <string name="v5_3_simpler_incognito_mode">Mode d\'incògnit simplificat</string> + <string name="member_role_will_be_changed_with_notification_chat">El rol es canviarà a %s. Tots els participants del xat rebran una notificació.</string> + <string name="shutdown_alert_question">Tancar?</string> + <string name="shutdown_alert_desc">Les notificacions deixaran de funcionar fins que torneu a iniciar l\'aplicació</string> + <string name="your_contacts_will_remain_connected">Els teus contactes romandran connectats.</string> + <string name="share_address_with_contacts_question">Compartir l\'adreça amb els contactes?</string> + <string name="share_link">Compartir enllaç</string> + <string name="save_auto_accept_settings">Desa la configuració d\'acceptació automàtica</string> + <string name="stop_sharing">Deixar de compartir</string> + <string name="or_to_share_privately">O per compartir en privat</string> + <string name="you_can_create_it_later">Podeu crear-la més tard</string> + <string name="you_can_make_address_visible_via_settings">Podeu fer-lo visible per als vostres contactes de SimpleX mitjançant Configuració.</string> + <string name="display_name__field">Nom del perfil:</string> + <string name="save_and_notify_contact">Desa i notifica el contacte</string> + <string name="your_current_profile">El teu perfil actual</string> + <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">El teu perfil s\'emmagatzema al teu dispositiu i només es comparteix amb els teus contactes, els servidors SimpleX no poden veure\'l.</string> + <string name="save_and_notify_contacts">Desa i notifica els contactes</string> + <string name="save_and_notify_group_members">Desa i notifica als membres del grup</string> + <string name="hide_profile">Amagar el perfil</string> + <string name="password_to_show">Contrasenya per mostrar</string> + <string name="save_profile_password">Desa la contrasenya del perfil</string> + <string name="to_reveal_profile_enter_password">Per revelar el vostre perfil ocult introduïu una contrasenya completa al camp de cerca de la pàgina Els vostres perfils de xat.</string> + <string name="you_control_your_chat">Tu controles el teu xat!</string> + <string name="you_can_use_markdown_to_format_messages__prompt">Pots utilitzar la sintaxi markdown per donar format als teus missatges:</string> + <string name="how_to_use_markdown">Com utilitzar la sintaxis markdown</string> + <string name="callstatus_rejected">trucada rebutjada</string> + <string name="callstate_starting">iniciant…</string> + <string name="callstate_received_answer">resposta rebuda…</string> + <string name="callstate_waiting_for_answer">esperant resposta…</string> + <string name="callstate_waiting_for_confirmation">esperant confirmació…</string> + <string name="callstate_received_confirmation">confirmació rebuda…</string> + <string name="unable_to_open_browser_desc">Un navegador web predeterminat és necessari per a les trucades. Configura\'n un al sistema i comparteix més informació amb els desenvolupadors.</string> + <string name="privacy_redefined">Privadesa redefinida</string> + <string name="people_can_connect_only_via_links_you_share">Tu decideixes qui es pot connectar.</string> + <string name="immune_to_spam_and_abuse">Immune al correu brossa</string> + <string name="first_platform_without_user_ids">Sense identificadors d\'usuari.</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">Per protegir la vostra privadesa SimpleX utilitza identificadors separats per a cadascun dels vostres contactes..</string> + <string name="use_chat">Obrir SimpleX</string> + <string name="how_simplex_works">Com funciona SimpleX</string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Només els dispositius client emmagatzemen perfils d\'usuari, contactes, grups i missatges.</string> + <string name="onboarding_notifications_mode_title">Notificacions privades</string> + <string name="onboarding_notifications_mode_subtitle">Com afecta la bateria</string> + <string name="onboarding_notifications_mode_periodic">Periòdic</string> + <string name="onboarding_notifications_mode_service">Instantánea</string> + <string name="onboarding_notifications_mode_off">Quan l\'aplicació s\'està executant</string> + <string name="onboarding_notifications_mode_off_desc_short">Sense servei de fons</string> + <string name="onboarding_network_operators_app_will_use_different_operators">L\'aplicació protegeix la teva privadesa utilitzant diferents operadors en cada conversa.</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">Quan hi ha més d\'un operador habilitat, cap d\'ells té metadades per saber qui es comunica amb qui.</string> + <string name="how_it_helps_privacy">Com ajuda a la privadesa</string> + <string name="onboarding_network_operators_review_later">Revisar més tard</string> + <string name="onboarding_network_operators_update">Actualitzar</string> + <string name="onboarding_network_operators_conditions_you_can_configure">Podeu configurar els operadors a la configuració: Xarxa i Servidors.</string> + <string name="video_call_no_encryption">videotrucada (sense xifrar)</string> + <string name="ignore">Ignorar</string> + <string name="reject">Rebutjar</string> + <string name="call_desktop_permission_denied_title">Per fer trucades, permet utilitzar el micròfon. Finalitza la trucada i prova de tornar a trucar.</string> + <string name="icon_descr_video_call">Videotrucada</string> + <string name="relay_server_if_necessary">El servidor de retransmissió només s\'utilitza si cal. Un tercer pot observar la vostra adreça IP.</string> + <string name="show_call_on_lock_screen">Mostrar</string> + <string name="your_calls">Trucades</string> + <string name="status_no_e2e_encryption">sense xifratge e2e</string> + <string name="open_verb">Obrir</string> + <string name="call_connection_via_relay">mitjançant retransmissor</string> + <string name="icon_descr_video_off">Desactivar vídeo</string> + <string name="call_connection_peer_to_peer">p2p</string> + <string name="icon_descr_video_on">Activar vídeo</string> + <string name="icon_descr_call_rejected">Trucada rebutjada</string> + <string name="icon_descr_call_pending_sent">Trucada pendent</string> + <string name="icon_descr_sound_muted">So silenciat</string> + <string name="alert_title_skipped_messages">Missatges omesos</string> + <string name="alert_text_msg_bad_hash">El hash del missatge anterior és diferent.\"</string> + <string name="alert_text_msg_bad_id">L\'identificador del missatge següent és incorrecte (menor o igual a l\'anterior).\nPot passar per algun error o quan la connexió està compromesa.</string> + <string name="alert_text_fragment_please_report_to_developers">Informeu-ho als desenvolupadors.</string> + <string name="alert_text_fragment_encryption_out_of_sync_old_database">Pot passar quan tu o el teu contacte feu servir la còpia de seguretat de la base de dades antiga.</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">L\'aplicació us demanarà que confirmeu les baixades de servidors de fitxers desconeguts (excepte .onion o quan el servidor intermediari SOCKS estigui habilitat).</string> + <string name="send_link_previews">Envia previsualitzacions d\'enllaços</string> + <string name="privacy_show_last_messages">Mostra els últims missatges</string> + <string name="submit_passcode">Enviar</string> + <string name="new_passcode">Codi nou</string> + <string name="la_mode_off">Desactivat</string> + <string name="la_mode_passcode">Codi d\'accés</string> + <string name="passcode_set">Codi d\'accés desat!</string> + <string name="passcode_changed">Codi d\'accés canviat!</string> + <string name="self_destruct_new_display_name">Nou nom mostrat:</string> + <string name="self_destruct_passcode_changed">Codi d\'autodestrucció canviat!</string> + <string name="self_destruct_passcode_enabled">Codi d\'autodestrucció activat!</string> + <string name="receipts_contacts_override_disabled">L\'enviament de rebuts està desactivat per a %d contactes</string> + <string name="receipts_contacts_override_enabled">L\'enviament de rebuts està habilitat per a %d contactes</string> + <string name="receipts_groups_override_enabled">L\'enviament de rebuts està habilitat per a %d grups</string> + <string name="settings_section_title_delivery_receipts">ENVIAR ELS REBUS DE LLIURAMENT A</string> + <string name="receipts_groups_override_disabled">L\'enviament de rebuts està desactivat per a %d grups</string> + <string name="settings_restart_app">Reiniciar</string> + <string name="settings_section_title_socks">SERVIDOR INTERMEDIARI SOCKS</string> + <string name="settings_section_title_profile_images">Imatges de perfil</string> + <string name="settings_section_title_themes">TEMES</string> + <string name="settings_message_shape_tail">Cua</string> + <string name="settings_section_title_message_shape">Forma del missatge</string> + <string name="run_chat_section">EXECUTAR SIMPLEX</string> + <string name="settings_section_title_use_from_desktop">Usar des d\'ordinador</string> + <string name="your_chat_database">Base de dades de xat</string> + <string name="import_database">Importar base de dades</string> + <string name="new_database_archive">Nou arxiu de bases de dades</string> + <string name="open_database_folder">Obrir la carpeta de la base de dades</string> + <string name="stop_chat_question">Aturar SimpleX?</string> + <string name="stop_chat_to_export_import_or_delete_chat_database">Atura SimpleX per poder exportar, importar o suprimir la base de dades de xat. No podreu rebre ni enviar missatges mentre el xat estigui aturat.</string> + <string name="set_password_to_export">Estableix contrasenya per a exportar</string> + <string name="stop_chat_confirmation">Aturar</string> + <string name="restart_the_app_to_use_imported_chat_database">Reinicieu l\'aplicació per utilitzar la base de dades de xat importada.</string> + <string name="import_database_confirmation">Importar</string> + <string name="import_database_question">Importar la base de dades de xat?</string> + <string name="non_fatal_errors_occured_during_import">S\'han produït alguns errors no fatals durant la importació:</string> + <string name="delete_chat_profile_action_cannot_be_undone_warning">Aquesta acció no es pot desfer: el vostre perfil, contactes, missatges i fitxers es perdran de manera irreversible.</string> + <string name="enable_automatic_deletion_message">Aquesta acció no es pot desfer; els missatges enviats i rebuts anteriors al seleccionat se suprimiran. Pot trigar uns quants minuts.</string> + <string name="chat_database_exported_migrate">Podeu migrar la base de dades exportada.</string> + <string name="chat_database_exported_save">Podeu desar l\'arxiu exportat.</string> + <string name="save_passphrase_in_settings">Desa la contrasenya a la configuració</string> + <string name="update_database">Actualitzar</string> + <string name="unknown_error">Error desconegut</string> + <string name="wrong_passphrase_title">Contrasenya incorrecta!</string> + <string name="alert_message_no_group">Aquest grup ja no existeix.</string> + <string name="rcv_group_event_member_unblocked">ha desblocat a %s</string> + <string name="rcv_group_event_n_members_connected">%s, %s i %d membres més connectats</string> + <string name="snd_conn_event_switch_queue_phase_completed_for_member">has canviat d\'adreça de servidor per a %s</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">El membre s\'eliminarà del xat; això no es pot desfer!</string> + <string name="member_will_be_removed_from_group_cannot_be_undone">El membre s\'eliminarà del grup; això no es pot desfer!</string> + <string name="member_role_will_be_changed_with_notification">El rol es canviarà a %s. Tots els membres del grup seran avisats.</string> + <string name="connect_via_member_address_alert_desc">La sol·licitud de connexió s\'enviarà a aquest membre del grup.</string> + <string name="member_role_will_be_changed_with_invitation">El rol es canviarà a %s. El membre rebrà una nova invitació.</string> + <string name="network_status">Estat de la xarxa</string> + <string name="operator">Operador</string> + <string name="operator_website">Lloc web</string> + <string name="operator_use_for_sending">Per a enviar</string> + <string name="network_options_reset_to_defaults">Restablir valors predeterminats</string> + <string name="network_option_ping_count">Compte PING</string> + <string name="tap_to_activate_profile">Toqueu per activar el perfil.</string> + <string name="muted_when_inactive">Silenciat quan està inactiu!</string> + <string name="you_will_still_receive_calls_and_ntfs">Encara rebràs trucades i notificacions de perfils silenciats quan estiguin actius.</string> + <string name="only_your_contact_can_delete">Només el vostre contacte pot suprimir missatges de manera irreversible (podeu marcar-los per suprimir-los). (24 hores)</string> + <string name="message_deletion_prohibited">La supressió de missatges irreversible està prohibida en aquest xat.</string> + <string name="v4_3_irreversible_message_deletion_desc">Els vostres contactes poden permetre la supressió completa del missatge.</string> + <string name="v4_3_irreversible_message_deletion">Eliminació irreversible del missatge</string> + <string name="v4_3_improved_privacy_and_security_desc">Amaga la pantalla de l\'aplicació a les aplicacions recents.</string> + <string name="v4_3_improved_privacy_and_security">Millora de la privadesa i la seguretat</string> + <string name="v4_3_improved_server_configuration">Configuració del servidor millorada</string> + <string name="v4_4_live_messages_desc">El(s) destinatari(s) veu(en) l\'actualització mentre l\'escriviu.</string> + <string name="v4_4_live_messages">Missatges en directe</string> + <string name="v4_4_disappearing_messages_desc">Els missatges enviats se suprimiran després del temps establert.</string> + <string name="v4_5_message_draft">Esborrany de missatge</string> + <string name="v4_5_message_draft_descr">Conserva l\'últim esborrany del missatge, amb fitxers adjunts.</string> + <string name="v4_5_transport_isolation">Aïllament de transport</string> + <string name="v4_6_group_moderation_descr">Ara els administradors poden:\n- suprimir els missatges dels membres.\n- desactivar els membres (rol d\'observador)</string> + <string name="v4_6_hidden_chat_profiles_descr">Protegir els vostres perfils de xat amb contrasenya!</string> + <string name="v4_6_audio_video_calls_descr">Admet bluetooth i altres millores.</string> + <string name="v5_4_link_mobile_desktop_descr">Mitjançant protocol segur de resistència quàntica.</string> + <string name="v5_4_link_mobile_desktop">Enllaça aplicacions mòbils i d\'ordinador! 🔗</string> + <string name="migrate_from_device_to_another_device">Migrar a un altre dispositiu</string> + <string name="migrate_from_device_database_init">Preparant càrrega</string> + <string name="network_type_no_network_connection">Sense connexió de xarxa</string> + <string name="servers_info_transport_sessions_section_header">Sessions de transport</string> + <string name="secured">Assegurades</string> + <string name="v5_6_picture_in_picture_calls_descr">Usar l\'aplicació durant la trucada.</string> + <string name="unblock_for_all">Desblocar per tothom</string> + <string name="moderate_messages_will_be_marked_warning">Els missatges es marcaran com a moderats per a tots els membres.</string> + <string name="use_servers_of_operator_x">Utilitzar %s</string> + <string name="remove_passphrase_from_keychain">Suprimir la contrasenya de Keystore?</string> + <string name="remove_passphrase_from_settings">Suprimir la contrasenya de la configuració?</string> + <string name="new_passphrase">Nova contrasenya…</string> + <string name="enter_correct_current_passphrase">Introduïu la contrasenya actual correcta.</string> + <string name="set_database_passphrase">Estableix contrasenya per a la base de dades</string> + <string name="set_passphrase">Estableix contrasenya</string> + <string name="update_database_passphrase">Actualitzar la contrasenya de la base de dades</string> + <string name="settings_is_storing_in_clear_text">La frase de contrasenya s\'emmagatzema a la configuració com a text pla.</string> + <string name="database_backup_can_be_restored">L\'intent de canviar la contrasenya de la base de dades no s\'ha completat.</string> + <string name="unknown_database_error_with_info">Error de base de dades desconegut: %s</string> + <string name="database_restore_error">Error en restaurar base de dades</string> + <string name="database_migrations">Migracions: %s</string> + <string name="database_downgrade_warning">Avís: podeu perdre algunes dades!</string> + <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Podeu iniciar el xat mitjançant Configuració / Base de dades o reiniciant l\'aplicació.</string> + <string name="join_group_incognito_button">Uneix-te d\'incògnit</string> + <string name="joining_group">Entrant al grup</string> + <string name="you_are_invited_to_group_join_to_connect_with_group_members">Estàs convidat al grup. Uneix-te per connectar amb els seus membres.</string> + <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">T\'has unit a aquest grup. S\'està connectant amb l\'emissor de la invitació.</string> + <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Deixaràs de rebre missatges d\'aquest xat. L\'historial de xat es conservarà.</string> + <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Deixaràs de rebre missatges d\'aquest grup. L\'historial de xat es conservarà.</string> + <string name="rcv_group_event_2_members_connected">%s i %s connectats</string> + <string name="group_members_n">%s, %s i %d membres</string> + <string name="rcv_group_event_open_chat">Obrir</string> + <string name="rcv_group_event_3_members_connected">%s, %s i %s connectats</string> + <string name="profile_update_event_removed_address">adreça de contacte eliminat</string> + <string name="profile_update_event_removed_picture">foto de perfil eliminada</string> + <string name="profile_update_event_set_new_picture">establir una nova foto de perfil</string> + <string name="profile_update_event_set_new_address">nova adreça de contacte</string> + <string name="profile_update_event_updated_profile">perfil actualitzat</string> + <string name="snd_conn_event_switch_queue_phase_completed">has canviat d\'adreça de servidor</string> + <string name="group_member_role_owner">propietari</string> + <string name="group_member_status_unknown">estat desconegut</string> + <string name="you_can_share_this_address_with_your_contacts">Podeu compartir aquesta adreça amb els vostres contactes perquè es connectin amb %s.</string> + <string name="send_receipts_disabled_alert_msg">Aquest grup té més de %1$d membres, no s\'envien rebuts d\'entrega.</string> + <string name="info_row_received_at">Rebut</string> + <string name="sender_at_ts">%s a les %s</string> + <string name="button_send_direct_message">Enviar missatge directe</string> + <string name="share_text_moderated_at">Moderat el: %s</string> + <string name="share_text_received_at">Rebut: %s</string> + <string name="unblock_member_button">Desblocar membre</string> + <string name="unblock_member_question">Desblocar membre?</string> + <string name="unblock_for_all_question">Desblocar membre per tothom?</string> + <string name="role_in_group">Rol</string> + <string name="message_queue_info_server_info">informació de cua del servidor: %1$s\n\núltim missatge rebut: %2$s</string> + <string name="message_queue_info">Informació de la cua de missatges</string> + <string name="message_queue_info_none">cap</string> + <string name="sending_via">Enviant via</string> + <string name="chat_main_profile_sent">El teu perfil de xat s\'enviarà als membres del xat</string> + <string name="group_main_profile_sent">El teu perfil de xat s\'enviarà als membres del grup</string> + <string name="operator_info_title">Operador de xarxa</string> + <string name="operator_servers_title">%s servidors</string> + <string name="xftp_servers_per_user">Servidors per a fitxers nous al vostre perfil de xat actual</string> + <string name="view_conditions">Veure condicions</string> + <string name="operator_open_conditions">Obrir condicions</string> + <string name="server_added_to_operator__name">Servidor afegit a l\'operador %s.</string> + <string name="error_server_operator_changed">Ha canviat l\'operador del servidor.</string> + <string name="error_server_protocol_changed">El protocol del servidor ha canviat.</string> + <string name="network_option_tcp_connection_timeout">Temps d\'espera de connexió TCP</string> + <string name="network_option_protocol_timeout">Temps d\'espera del protocol</string> + <string name="network_option_protocol_timeout_per_kb">Temps d\'espera del protocol per KB</string> + <string name="network_option_rcv_concurrency">Concurrència a la recepció</string> + <string name="update_network_settings_question">Actualitzar la configuració de xarxa?</string> + <string name="update_network_settings_confirmation">Actualitzar</string> + <string name="users_delete_with_connections">Connexions de perfil i servidor</string> + <string name="user_unhide">Mostrar</string> + <string name="user_mute">Silenciar</string> + <string name="user_unmute">Activar so</string> + <string name="unhide_profile">Mostrar perfil</string> + <string name="reset_single_color">Restablir color</string> + <string name="reset_color">Restablir colors</string> + <string name="system_mode_toast">Mode de sistema</string> + <string name="chat_preferences_no">no</string> + <string name="chat_preferences_off">desactivat</string>` + <string name="chat_preferences_on">Activat</string> + <string name="chat_theme_reset_to_app_theme">Restablir al tema de l\'aplicació</string> + <string name="chat_theme_reset_to_user_theme">Restablir al tema d\'usuari</string> + <string name="chat_theme_set_default_theme">Estableix tema predeterminat</string> + <string name="chat_preferences_you_allow">Permeteu</string> + <string name="set_group_preferences">Estableix preferències de grup</string> + <string name="your_preferences">Preferències</string> + <string name="message_reactions">Reaccions a missatge</string> + <string name="voice_messages">Missatges de veu</string> + <string name="prohibit_sending_files">Prohibir l\'enviament de fitxers i mitjans.</string> + <string name="simplex_links_are_prohibited_in_group">Enllaços SimpleX no permesos.</string> + <string name="recent_history_is_sent_to_new_members">S\'envien fins a 100 darrers missatges als nous membres.</string> + <string name="recent_history_is_not_sent_to_new_members">L\'historial no s\'envia als nous membres.</string> + <string name="feature_offered_item">oferit %s</string> + <string name="feature_offered_item_with_param">oferit %s: %2s</string> + <string name="whats_new">Novetats</string> + <string name="new_in_version">Nou a %s</string> + <string name="whats_new_read_more">Llegeix més</string> + <string name="v4_3_voice_messages">Missatges de veu</string> + <string name="v4_2_auto_accept_contact_requests_desc">Amb missatge de benvinguda opcional.</string> + <string name="v4_5_private_filenames">Noms de fitxers privats</string> + <string name="v4_5_reduced_battery_usage">Ús de bateria reduït</string> + <string name="v4_5_italian_interface_descr">Gràcies als usuaris: contribuïu a través de Weblate!</string> + <string name="v4_5_reduced_battery_usage_descr">Aviat hi haurà més millores!</string> + <string name="v5_2_message_delivery_receipts">Rebuts de lliurament de missatges!</string> + <string name="v5_3_simpler_incognito_mode_descr">Activa o desactiva l\'incògnit en connectar-te.</string> + <string name="v5_4_block_group_members_descr">Per amagar missatges no desitjats.</string> + <string name="v5_5_private_notes">Notes privades</string> + <string name="v5_5_simpler_connect_ui_descr">La barra de cerca accepta enllaços d\'invitació.</string> + <string name="v5_5_private_notes_descr">Amb fitxers i mitjans xifrats.</string> + <string name="v5_5_message_delivery">Entrega de missatges millorada</string> + <string name="v5_5_message_delivery_descr">Amb ús reduït de la bateria.</string> + <string name="v5_6_app_data_migration_descr">Migra a un altre dispositiu mitjançant el codi QR.</string> + <string name="v5_6_picture_in_picture_calls">Trucades imatge-en-imatge</string> + <string name="v5_6_quantum_resistant_encryption">Xifratge resistent quàntic</string> + <string name="v5_7_shape_profile_images">Formar imatges de perfil</string> + <string name="v5_7_forward_descr">L\'autoria del missatge continua sent privada.</string> + <string name="v5_7_shape_profile_images_descr">Quadrat, cercle o qualsevol forma intermèdia.</string> + <string name="v5_7_call_sounds_descr">En connectar trucades d\'àudio i vídeo.</string> + <string name="v5_7_quantum_resistant_encryption_descr">S\'habilitarà als xats directes!</string> + <string name="v5_7_network">Gestió de la xarxa</string> + <string name="v5_8_chat_themes">Nous temes de xat</string> + <string name="v5_8_safe_files">Rebre fitxers amb seguretat</string> + <string name="v5_8_message_delivery">Entrega de missatges millorada</string> + <string name="v5_8_persian_ui">IU en persa</string> + <string name="v5_8_message_delivery_descr">Amb ús reduït de la bateria.</string> + <string name="v6_0_new_media_options">Noves opcions de mitjans</string> + <string name="v6_0_reachable_chat_toolbar">Barres d\'eines de xat accessible</string> + <string name="v6_0_reachable_chat_toolbar_descr">Fes servir l\'aplicació amb una sola mà.</string> + <string name="v6_0_chat_list_media">Reprodueix des de la llista de xat.</string> + <string name="v6_0_upgrade_app">Actualitza l\'aplicació automàticament</string> + <string name="v6_2_network_decentralization">Descentralització de la xarxa</string> + <string name="v6_2_network_decentralization_descr">El segon operador preestablert a l\'aplicació!</string> + <string name="v6_2_business_chats_descr">Privadesa per als teus clients.</string> + <string name="view_updated_conditions">Veure condicions actualitzades</string> + <string name="custom_time_unit_hours">hores</string> + <string name="custom_time_unit_weeks">setmanes</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">L\'enviament de rebuts de lliurament s\'habilitarà per a tots els contactes de tots els perfils de xat visibles.</string> + <string name="unlink_desktop">Desenllaçar</string> + <string name="unlink_desktop_question">Desenllaçar l\'ordinador?</string> + <string name="you_can_enable_delivery_receipts_later_alert">Podeu activar-los més tard mitjançant la configuració de privadesa i seguretat de l\'aplicació.</string> + <string name="remote_ctrl_connection_stopped_identity_desc">Aquest enllaç s\'ha utilitzat amb un altre dispositiu mòbil; creeu-ne un de nou a l\'ordinador.</string> + <string name="only_one_device_can_work_at_the_same_time">Només un dispositiu pot funcionar al mateix temps</string> + <string name="session_code">Codi de sessió</string> + <string name="waiting_for_desktop">Esperant ordinador…</string> + <string name="linked_desktops">Ordinadors enllaçats</string> + <string name="verify_code_with_desktop">Verifica el codi a l\'ordinador</string> + <string name="remote_ctrl_error_timeout">S\'ha arribat al temps d\'espera durant la connexió a l\'ordinador</string> + <string name="connect_plan_you_have_already_requested_connection_via_this_address">Ja heu sol·licitat connexió a través d\'aquesta adreça!</string> + <string name="connect_plan_repeat_join_request">Repetir la sol·licitud d\'unió?</string> + <string name="connect_plan_open_group">Grup obert</string> + <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Ja t\'estàs unint al grup mitjançant aquest enllaç.</string> + <string name="agent_critical_error_desc">Informeu-ho als desenvolupadors:\n%s\n\nEs recomana reiniciar l\'aplicació.</string> + <string name="chat_archive">O importar un fitxer d\'arxiu</string> + <string name="paste_archive_link">Enganxa l\'enllaç de l\'arxiu</string> + <string name="restart_chat_button">Reiniciar xat</string> + <string name="migrate_to_device_migrating">Migrant</string> + <string name="migrate_to_device_database_init">Preparant descàrrega</string> + <string name="migrate_to_device_import_failed">Importació fallida</string> + <string name="migrate_to_device_importing_archive">Important arxiu</string> + <string name="migrate_from_device_stopping_chat">Aturant el xat</string> + <string name="migrate_from_device_title">Migrar dispositiu</string> + <string name="migrate_from_device_chat_should_be_stopped">Per continuar, el xat s\'ha d\'aturar.</string> + <string name="migrate_from_device_uploading_archive">Carregant l\'arxiu</string> + <string name="migrate_from_device_remove_archive_question">Suprimir l\'arxiu?</string> + <string name="migrate_from_device_repeat_upload">Repetir la càrrega</string> + <string name="migrate_from_device_try_again">Pots intentar-ho un altre cop.</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">L\'arxiu de base de dades carregat s\'eliminarà permanentment dels servidors.</string> + <string name="migrate_from_device_migration_complete">Migració completada</string> + <string name="migrate_from_device_verify_passphrase">Verificar contrasenya</string> + <string name="network_type_other">Altre</string> + <string name="servers_info">Info servidors</string> + <string name="servers_info_target">Mostrant info per a</string> + <string name="servers_info_private_data_disclaimer">Iniciat el %s\nTotes les dades es mantenen privades al vostre dispositiu.</string> + <string name="servers_info_subscriptions_total">Total</string> + <string name="servers_info_proxied_servers_section_footer">No esteu connectats a aquests servidors. Per enviar missatges s\'usa l\'encaminament privat.</string> + <string name="servers_info_reconnect_servers_title">Reconnectar servidors?</string> + <string name="servers_info_reset_stats_alert_confirm">Restablir</string> + <string name="servers_info_reset_stats">Restablir totes les estadístiques</string> + <string name="servers_info_reset_stats_alert_title">Restablir totes les estadístiques?</string> + <string name="servers_info_reset_stats_alert_message">Les estadístiques dels servidors es restabliran; això no es pot desfer!</string> + <string name="servers_info_uploaded">Carregat</string> + <string name="servers_info_detailed_statistics_sent_messages_header">Missatges enviats</string> + <string name="servers_info_detailed_statistics_received_messages_header">Missatges rebuts</string> + <string name="servers_info_detailed_statistics_received_total">Total rebuts</string> + <string name="servers_info_detailed_statistics_sent_messages_total">Total enviats</string> + <string name="servers_info_starting_from">Iniciat el %s.</string> + <string name="servers_info_detailed_statistics_receive_errors">Errors de recepció</string> + <string name="reconnect">Reconnectar</string> + <string name="send_errors">Errors d\'enviament</string> + <string name="other_label">altres</string> + <string name="proxied">Intermediat</string> + <string name="sent_directly">Enviat directament</string> + <string name="sent_via_proxy">Enviat mitjançant servidor intermediari</string> + <string name="subscribed">Subscrit</string> + <string name="subscription_errors">Errors de subscripció</string> + <string name="subscription_results_ignored">Subscripcions ignorades</string> + <string name="uploaded_files">Fitxers carregats</string> + <string name="upload_errors">Errors de càrrega</string> + <string name="operator_use_operator_toggle_description">Utilitzar servidors</string> + <string name="send_receipts_disabled_alert_title">Rebuts desactivats</string> + <string name="servers_info_reconnect_servers_message">Reconnectar els servidors per forçar l\'entrega de missatges. Utilitza trànsit addicional.</string> + <string name="servers_info_reconnect_server_title">Reconnectar servidor?</string> + <string name="servers_info_reconnect_all_servers_button">Reconnectar tots els servidors</string> + <string name="remote_ctrl_connection_stopped_desc">Comproveu que el mòbil i l\'ordinador estiguin connectats a la mateixa xarxa local i que el tallafoc d\'escriptori permet la connexió.\nSi us plau, comparteix qualsevol altre problema amb els desenvolupadors.</string> + <string name="restore_database_alert_desc">Introduïu la contrasenya anterior després de restaurar la còpia de seguretat de la base de dades. Aquesta acció no es pot desfer.</string> + <string name="network_option_ping_interval">Interval PING</string> + <string name="agent_internal_error_desc">Informeu-ho als desenvolupadors:\n%s</string> + <string name="v5_8_private_routing">Encaminament de missatges privat 🚀</string> + <string name="user_hide">Amagar</string> + <string name="servers_info_reconnect_server_message">Reconnectar el servidor per forçar l\'entrega de missatges. Utilitza trànsit addicional.</string> + <string name="migrate_from_device_verify_database_passphrase">Verificar contrasenya de la base de dades</string> + <string name="v6_2_improved_chat_navigation">Navegació millorada al xat</string> + <string name="servers_info_subscriptions_section_header">Recepció de missatges</string> + <string name="error_parsing_uri_title">Enllaç no vàlid</string> + <string name="connect_via_group_link">Unir-te al grup?</string> + <string name="marked_deleted_description">marcat eliminat</string> + <string name="opening_database">Obrint la base de dades…</string> + <string name="error_parsing_uri_desc">Comproveu que l\'enllaç SimpleX sigui correcte.</string> + <string name="sending_files_not_yet_supported">l\'enviament de fitxers encara no està suportat</string> + <string name="trying_to_connect_to_server_to_receive_messages">Intentant connectar-se al servidor utilitzat per rebre missatges d\'aquest contacte.</string> + <string name="trying_to_connect_to_server_to_receive_messages_with_error">S\'està provant de connectar-se al servidor utilitzat per rebre missatges d\'aquest contacte (error: %1$s).</string> + <string name="connect_use_current_profile">Usar perfil actual</string> + <string name="connect_use_new_incognito_profile">Usar nou perfil incògnit</string> + <string name="app_was_crashed">Error aplicació</string> + <string name="connected_to_server_to_receive_messages_from_contact">Esteu connectat al servidor utilitzat per rebre missatges d\'aquest contacte.</string> + <string name="profile_will_be_sent_to_contact_sending_link">El teu perfil s\'enviarà al contacte del qual has rebut aquest enllaç.</string> + <string name="non_content_uri_alert_text">Heu compartit una ruta de fitxer no vàlida. Informeu-ne als desenvolupadors de l\'aplicació.</string> + <string name="you_will_join_group">Us connectareu amb tots els membres del grup.</string> + <string name="description_via_contact_address_link_incognito">d\'incògnit mitjançant l\'enllaç de l\'adreça de contacte</string> + <string name="description_via_group_link_incognito">d\'incògnit mitjançant l\'enllaç del grup</string> + <string name="description_via_one_time_link_incognito">d\'incògnit mitjançant un enllaç d\'un sol ús</string> + <string name="invalid_chat">xat no vàlid</string> + <string name="invalid_message_format">format de missatge no vàlid</string> + <string name="display_name_invited_to_connect">convidat a connectar</string> + <string name="simplex_link_mode_browser_warning">L\'obertura de l\'enllaç al navegador pot reduir la privadesa i la seguretat de la connexió. Els enllaços SimpleX no fiables seran vermells.</string> + <string name="note_folder_local_display_name">Notes privades</string> + <string name="receiving_files_not_yet_supported">la recepció de fitxers encara no està suportada</string> + <string name="display_name_requested_to_connect">sol·licitada connexió</string> + <string name="saved_from_description">desat des de %s</string> + <string name="simplex_link_contact">Adreça de contacte SimpleX</string> + <string name="simplex_link_group">Enllaç de grup SimpleX</string> + <string name="simplex_link_mode">Enllaços SimpleX</string> + <string name="e2ee_info_no_pq_short">Aquest xat està protegit per xifratge d\'extrem a extrem.</string> + <string name="e2ee_info_pq_short">Aquest xat està protegit per un xifratge d\'extrem a extrem resistent a la quàntica.</string> + <string name="unknown_message_format">format de missatge desconegut</string> + <string name="simplex_link_connection">mitjançant %1$s</string> + <string name="simplex_link_mode_browser">Mitjançant navegador</string> + <string name="description_via_contact_address_link">mitjançant enllaç d\'adreça de contacte</string> + <string name="description_via_group_link">mitjançant enllaç de grup</string> + <string name="description_via_one_time_link">mitjançant enllaç d\'un sol ús</string> + <string name="description_you_shared_one_time_link">has compartit un enllaç d\'un sol ús</string> + <string name="failed_to_create_user_invalid_title">Nom mostrat no vàlid!</string> + <string name="ensure_smp_server_address_are_correct_format_and_unique">Assegureu-vos que les adreces del servidor SMP estiguin en el format correcte, que estiguin separades per línies i que no estiguin duplicades.</string> + <string name="ensure_xftp_server_address_are_correct_format_and_unique">Assegureu-vos que les adreces del servidor XFTP estiguin en el format correcte, que estiguin separades per línies i que no estiguin duplicades.</string> + <string name="no_media_servers_configured">No hi ha servidors multimèdia ni de fitxers.</string> + <string name="no_message_servers_configured">No hi ha servidors de missatges.</string> + <string name="no_message_servers_configured_for_private_routing">No hi ha servidors per a l\'encaminament de missatges privats.</string> + <string name="no_media_servers_configured_for_private_routing">No hi ha servidors per rebre fitxers.</string> + <string name="no_message_servers_configured_for_receiving">No hi ha servidors per rebre missatges.</string> + <string name="no_media_servers_configured_for_sending">No hi ha servidors per enviar fitxers.</string> + <string name="network_error_desc">Comproveu la vostra connexió de xarxa amb %1$s i torneu-ho a provar.</string> + <string name="please_try_later">Si us plau, prova-ho més tard.</string> + <string name="contact_developers">Actualitzeu l\'aplicació i contacteu amb els desenvolupadors.</string> + <string name="private_routing_error">Error d\'encaminament privat</string> + <string name="network_error_broker_host_desc">L\'adreça del servidor és incompatible amb la configuració de xarxa: %1$s.</string> + <string name="network_error_broker_version_desc">La versió del servidor és incompatible amb la vostra aplicació: %1$s.</string> + <string name="failed_to_create_user_invalid_desc">Aquest nom mostrat no és vàlid. Si us plau, trieu-ne un altre.</string> + <string name="failed_to_create_user_duplicate_desc">Ja teniu un perfil de xat amb el mateix nom mostrat Si us plau, trieu un altre nom.</string> + <string name="invalid_connection_link">Enllaç de connexió no vàlid</string> + <string name="sender_cancelled_file_transfer">El remitent ha cancel·lat la transferència de fitxers.</string> + <string name="file_not_approved_title">Servidors desconeguts!</string> + <string name="you_are_already_connected_to_vName_via_this_link">Ja esteu connectat a %1$s.</string> + <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Comproveu que heu utilitzat l\'enllaç correcte o demaneu al vostre contacte que us n\'enviï un altre.</string> + <string name="sender_may_have_deleted_the_connection_request">És possible que el remitent hagi suprimit la sol·licitud de connexió.</string> + <string name="connection_error_quota_desc">La connexió ha arribat al límit de missatges no lliurats, és possible que el vostre contacte estigui fora de línia.</string> + <string name="connection_error_quota">Missatges no lliurats</string> + <string name="connection_error_auth_desc">A menys que el vostre contacte hagi suprimit la connexió o aquest enllaç ja s\'ha utilitzat, pot ser que sigui un error; si us plau, informeu-ho.\nPer connectar-vos, demaneu al vostre contacte que creï un altre enllaç de connexió i comproveu que teniu una connexió de xarxa estable.</string> + <string name="error_smp_test_certificate">Possiblement, l\'empremta digital del certificat a l\'adreça del servidor és incorrecta</string> + <string name="error_smp_test_server_auth">El servidor requereix autorització per crear cues, comproveu la contrasenya</string> + <string name="error_xftp_test_server_auth">El servidor requereix autorització per carregar, comproveu la contrasenya</string> + <string name="error_smp_test_failed_at_step">La prova ha fallat al pas %s.</string> + <string name="smp_server_test_secure_queue">Cua segura</string> + <string name="smp_server_test_upload_file">Carrega fitxer</string> + <string name="icon_descr_instant_notifications">Notificacions instantànies</string> + <string name="service_notifications">Notificacions instantànies!</string> + <string name="service_notifications_disabled">Les notificacions instantànies estan desactivades!</string> + <string name="periodic_notifications">Notificacions periòdiques</string> + <string name="periodic_notifications_disabled">Notificacions periòdiques desactivades</string> + <string name="periodic_notifications_desc">L\'aplicació obté missatges nous periòdicament: utilitza un percentatge de la bateria al dia. L\'aplicació no utilitza notificacions push: les dades del vostre dispositiu no s\'envien als servidors.</string> + <string name="turn_off_system_restriction_button">Obriu la configuració de l\'aplicació</string> + <string name="system_restricted_background_in_call_title">Sense trucades en segon pla</string> + <string name="system_restricted_background_in_call_desc">És possible que l\'aplicació es tanqui al cap d\'un minut en segon pla.</string> + <string name="enter_passphrase_notification_title">Cal contrasenya</string> + <string name="enter_passphrase_notification_desc">Per rebre notificacions introduïu la contrasenya de la base de dades</string> + <string name="simplex_service_notification_text">Rebent missatges…</string> + <string name="simplex_service_notification_title">Servei de SimpleX Xat</string> + <string name="database_initialization_error_desc">La base de dades no funciona correctament. Toca per obtenir més informació</string> + <string name="call_service_notification_video_call">Videotrucada</string> + <string name="settings_notifications_mode_title">Servei de notificacions</string> + <string name="settings_notification_preview_title">Vista prèvia de la notificació</string> + <string name="settings_notification_preview_mode_title">Vista prèvia</string> + <string name="notifications_mode_periodic">Cíclic</string> + <string name="notification_display_mode_hidden_desc">Amaga el contacte i el missatge</string> + <string name="notification_preview_mode_message">Text del missatge</string> + <string name="notification_new_contact_request">Nova sol·licitud de contacte</string> + <string name="notification_preview_new_message">missatge nou</string> + <string name="notification_preview_mode_message_desc">Mostrar el contacte i el missatge</string> + <string name="notification_preview_mode_contact_desc">Mostrar només el contacte</string> + <string name="la_no_app_password">Sense codi d\'accés de l\'aplicació</string> + <string name="la_lock_mode_passcode">Camp de codi d\'accés</string> + <string name="la_notice_title_simplex_lock">Bloqueig SimpleX</string> + <string name="la_lock_mode">Mode de Bloqueig SimpleX</string> + <string name="la_lock_mode_system">Autenticació del sistema</string> + <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Per protegir la vostra informació activeu SimpleX Lock.\nSe us demanarà que completeu l\'autenticació abans que aquesta funció estigui habilitada.</string> + <string name="la_notice_turn_on">Activar</string> + <string name="la_could_not_be_verified">No has pogut ser verificat; si us plau, torna-ho a provar.</string> + <string name="la_please_remember_to_store_password">Recordeu-la o emmagatzemeu-la de manera segura: no hi ha manera de recuperar una contrasenya perduda!</string> + <string name="auth_simplex_lock_turned_on">Bloqueig SimpleX habilitat</string> + <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Se us demanarà que us autentiqueu quan inicieu o reinicieu l\'aplicació després de 30 segons en segon pla.</string> + <string name="auth_log_in_using_credential">Inicieu sessió amb la vostra credencial</string> + <string name="message_delivery_warning_title">Avís d\'entrega de missatge</string> + <string name="message_delivery_error_desc">El més probable és que aquest contacte hagi suprimit la connexió amb tu.</string> + <string name="message_deleted_or_not_received_error_title">Cap missatge</string> + <string name="auth_open_chat_console">Obrir consola de xat</string> + <string name="auth_open_migration_to_another_device">Obrir pantalla de migració</string> + <string name="lock_not_enabled">Bloqueig SimpleX no habilitat!</string> + <string name="auth_stop_chat">Aturar SimpleX</string> + <string name="you_can_turn_on_lock">Podeu activar el Bloqueig SimpleX mitjançant Configuració.</string> + <string name="message_deleted_or_not_received_error_desc">Aquest missatge s\'ha suprimit o encara no s\'ha rebut.</string> + <string name="snd_error_expired">Problemes de xarxa: el missatge ha caducat després de molts intents d\'enviar-lo.</string> + <string name="snd_error_auth">Clau incorrecta o connexió desconeguda: el més probable és que aquesta connexió s\'hagi suprimit.</string> + <string name="srv_error_host">L\'adreça del servidor és incompatible amb la configuració de la xarxa.</string> + <string name="srv_error_version">La versió del servidor és incompatible amb la configuració de la xarxa.</string> + <string name="file_error_auth">Clau incorrecta o adreça de bloc de fitxer desconeguda: el més probable és que el fitxer s\'hagi suprimit.</string> + <string name="received_message">Missatge rebut</string> + <string name="sent_message">Missatge enviat</string> + <string name="no_history">Sense historial</string> + <string name="saved_from_chat_item_info_title">Desat des de</string> + <string name="recipients_can_not_see_who_message_from">El(s) destinatari(s) no veu(en) de qui és aquest missatge.</string> + <string name="no_info_on_delivery">Sense informació de lliurament</string> + <string name="delete_message_cannot_be_undone_warning">El missatge se suprimirà; això no es pot desfer!</string> + <string name="delete_messages_mark_deleted_warning">Els missatges es marcaran per eliminar-los. Els destinataris podran revelar aquests missatges.</string> + <string name="moderate_messages_will_be_deleted_warning">Els missatges s\'eliminaran per a tots els membres.</string> + <string name="moderate_message_will_be_deleted_warning">El missatge s\'eliminarà per a tots els membres.</string> + <string name="moderate_message_will_be_marked_warning">El missatge es marcarà com a moderat per a tots els membres.</string> + <string name="stop_rcv_file__message">S\'aturarà la recepció del fitxer.</string> + <string name="stop_snd_file__message">L\'enviament del fitxer s\'aturarà.</string> + <string name="stop_file__action">Aturar fitxer</string> + <string name="stop_rcv_file__title">Deixar de rebre fitxer?</string> + <string name="stop_snd_file__title">Deixar d\'enviar el fitxer?</string> + <string name="message_forwarded_title">Missatge reenviat</string> + <string name="message_forwarded_desc">Encara no hi ha connexió directa, el missatge el reenvia l\'administrador.</string> + <string name="revoke_file__action">Revocar fitxer</string> + <string name="icon_descr_sent_msg_status_unauthorized_send">enviament no autoritzat</string> + <string name="this_text_is_available_in_settings">Aquest text està disponible a la configuració</string> + <string name="personal_welcome">Benvingut %1$s!</string> + <string name="group_preview_join_as">unir-te com a %s</string> + <string name="member_contact_send_direct_message">enviar missatge directe</string> + <string name="tap_to_start_new_chat">Toca per iniciar un xat nou</string> + <string name="loading_chats">Carregant xats…</string> + <string name="no_filtered_chats">No hi ha xats filtrats</string> + <string name="search_or_paste_simplex_link">Cerqueu o enganxeu l\'enllaç SimpleX</string> + <string name="contact_tap_to_connect">Toqueu per connectar</string> + <string name="you_have_no_chats">No tens cap xat</string> + <string name="no_selected_chat">No hi ha cap xat seleccionat</string> + <string name="selected_chat_items_nothing_selected">Res seleccionat</string> + <string name="selected_chat_items_selected_n">Seleccionats %d</string> + <string name="forward_alert_title_nothing_to_forward">Res a reenviar!</string> + <string name="forward_files_messages_deleted_after_selection_desc">Els missatges s\'han suprimit després de seleccionar-los.</string> + <string name="share_file">Compartir fitxer…</string> + <string name="share_image">Compartir mitjans…</string> + <string name="share_message">Compartir missatge…</string> + <string name="cannot_share_message_alert_text">Les preferències de xat seleccionades prohibeixen aquest missatge.</string> + <string name="images_limit_title">Massa imatges!</string> + <string name="videos_limit_title">Massa vídeos!</string> + <string name="images_limit_desc">Només es poden enviar 10 imatges al mateix temps</string> + <string name="videos_limit_desc">Només es poden enviar 10 vídeos al mateix temps</string> + <string name="image_decoding_exception_desc">La imatge no es pot descodificar. Si us plau, proveu amb una imatge diferent o contacteu amb els desenvolupadors.</string> + <string name="video_decoding_exception_desc">El vídeo no es pot descodificar. Si us plau, prova amb un vídeo diferent o contacta amb els desenvolupadors.</string> + <string name="you_are_observer">ets observador</string> + <string name="observer_cant_send_message_title">No pots enviar missatges!</string> + <string name="observer_cant_send_message_desc">Poseu-vos en contacte amb l\'administrador del grup.</string> + <string name="only_owners_can_enable_files_and_media">Només els propietaris del grup poden activar fitxers i mitjans.</string> + <string name="compose_save_messages_n">Desant %1$s missatges</string> + <string name="compose_send_direct_message_to_connect">Envia missatge directe per connectar</string> + <string name="simplex_links_not_allowed">Enllaços SimpleX no permesos.</string> + <string name="maximum_message_size_reached_text">Si us plau, redueix la mida del missatge i torna a enviar-lo.</string> + <string name="maximum_message_size_reached_non_text">Si us plau, reduïu la mida del missatge o elimineu mitjans i torneu a enviar-lo.</string> + <string name="voice_messages_not_allowed">Missatges de veu no permesos</string> + <string name="maximum_message_size_reached_forwarding">Podeu copiar i reduir la mida del missatge per enviar-lo.</string> + <string name="image_descr">Imatge</string> + <string name="icon_descr_image_snd_complete">Imatge enviada</string> + <string name="icon_descr_waiting_for_image">Esperant la imatge</string> + <string name="waiting_for_image">Esperant la imatge</string> + <string name="image_saved">Imatge desada a la Galeria</string> + <string name="image_will_be_received_when_contact_completes_uploading">La imatge es rebrà quan el vostre contacte acabi de carregar-la.</string> + <string name="image_will_be_received_when_contact_is_online">La imatge es rebrà quan el vostre contacte estigui en línia, espereu o comproveu més tard!</string> + <string name="icon_descr_waiting_for_video">Esperant el vídeo</string> + <string name="large_file">Arxiu gran!</string> + <string name="icon_descr_video_snd_complete">Vídeo enviat</string> + <string name="video_will_be_received_when_contact_completes_uploading">El vídeo es rebrà quan el contacte acabi de pujar-lo.</string> + <string name="video_will_be_received_when_contact_is_online">El vídeo es rebrà quan el contacte estigui en línia, espereu o comproveu més tard!</string> + <string name="waiting_for_video">Esperant el vídeo</string> + <string name="contact_sent_large_file">El vostre contacte ha enviat un fitxer més gran que la mida màxima admesa actualment (%1$s).</string> + <string name="loading_remote_file_title">Carregant el fitxer</string> + <string name="notifications">Notificacions</string> + <string name="info_view_open_button">obrir</string> + <string name="loading_remote_file_desc">Si us plau, espereu mentre es carrega el fitxer des del mòbil enllaçat</string> + <string name="temporary_file_error">Error de fitxer temporal</string> + <string name="voice_message">Missatge de veu</string> + <string name="voice_message_send_text">Missatge de veu…</string> + <string name="voice_message_with_duration">Missatge de veu (%1$s )</string> + <string name="waiting_for_file">Esperant el fitxer</string> + <string name="only_delete_conversation">Només suprimeix la conversa</string> + <string name="icon_descr_server_status_pending">Pendente</string> + <string name="switch_receiving_address_desc">L\'adreça de recepció es canviarà per un servidor diferent. El canvi d\'adreça es completarà quan el remitent estigui en línia.</string> + <string name="text_field_set_contact_placeholder">Estableix el nom del contacte…</string> + <string name="you_can_still_send_messages_to_contact">Pots enviar missatges a %1$s des dels contactes arxivats.</string> + <string name="you_can_still_view_conversation_with_contact">Encara podeu veure la conversa amb %1$s a la llista de xats.</string> + <string name="sync_connection_force_desc">El xifratge funciona i el nou acord de xifratge no és necessari. Pot resultar en errors de connexió!</string> + <string name="sync_connection_force_confirm">Renegociar</string> + <string name="view_security_code">Veure codi de seguretat</string> + <string name="verify_security_code">Comprovar codi de seguretat</string> + <string name="icon_descr_send_message">Enviar Missatge</string> + <string name="icon_descr_record_voice_message">Grava el missatge de veu</string> + <string name="you_need_to_allow_to_send_voice">Heu de permetre que el vostre contacte enviï missatges de veu per poder-los enviar.</string> + <string name="live_message">Missatge en directe!</string> + <string name="no_details">cap detall</string> + <string name="ok">OK</string> + <string name="add_contact">Enllaç d\'invitació d\'un sol ús</string> + <string name="only_group_owners_can_enable_voice">Només els propietaris de grups poden activar els missatges de veu.</string> + <string name="only_stored_on_members_devices">(només emmagatzemat pels membres del grup)</string> + <string name="toast_permission_denied">Permís denegat!</string> + <string name="ask_your_contact_to_enable_voice">Demaneu al vostre contacte que habiliti l\'enviament de missatges de veu.</string> + <string name="send_live_message_desc">Envia un missatge en directe: s\'actualitzarà per al(s) destinatari(s) a mesura que l\'escrius</string> + <string name="send_disappearing_message">Envia un missatge que desapareix</string> + <string name="send_live_message">Envia missatge en directe</string> + <string name="tap_to_scan">Toqueu per escanejar</string> + <string name="to_share_with_your_contact">(per compartir amb el teu contacte)</string> + <string name="voice_messages_prohibited">Missatges de veu prohibits!</string> + <string name="thank_you_for_installing_simplex">Gràcies per instal·lar SimpleX Xat!</string> + <string name="to_start_a_new_chat_help_header">Per iniciar un xat nou</string> + <string name="chat_help_tap_button">Toqueu el botó</string> + <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Si decideixes rebutjar, el remitent NO rebrà notificació.</string> + <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Si heu rebut l\'enllaç d\'invitació de SimpleX Chat, podeu obrir-lo al vostre navegador:</string> + <string name="reject_contact_button">Rebutjar</string> + <string name="to_connect_via_link_title">Per connectar-se mitjançant enllaç</string> + <string name="image_descr_link_preview">vista prèvia de l\'enllaç</string> + <string name="mark_read">Marcar com a llegit</string> + <string name="image_descr_profile_image">imatge de perfil</string> + <string name="icon_descr_profile_image_placeholder">espai per a la imatge de perfil</string> + <string name="image_descr_qr_code">Codi QR</string> + <string name="set_contact_name">Estableix el nom del contacte</string> + <string name="connection_you_accepted_will_be_cancelled">La connexió que heu acceptat es cancel·larà!</string> + <string name="contact_you_shared_link_with_wont_be_able_to_connect">El contacte amb qui has compartit aquest enllaç NO es podrà connectar!</string> + <string name="unfavorite_chat">Desfavorit</string> + <string name="contact_wants_to_connect_with_you">vol contactar amb tu!</string> + <string name="you_invited_a_contact">Has convidat un contacte</string> + <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">El vostre contacte ha d\'estar en línia perquè la connexió es completi.\nPots cancel·lar aquesta connexió i eliminar el contacte (i provar-ho més tard amb un enllaç nou).</string> + <string name="show_QR_code">Mostrar codi QR</string> + <string name="this_link_is_not_a_valid_connection_link">Aquest no és un enllaç de connexió vàlid!</string> + <string name="this_QR_code_is_not_a_link">Aquest codi QR no és un enllaç!</string> + <string name="you_will_be_connected_when_group_host_device_is_online">Et connectaràs al grup quan el dispositiu de l\'amfitrió estigui en línia. Espereu o comproveu més tard!</string> + <string name="you_will_be_connected_when_your_connection_request_is_accepted">Et connectaràs quan s\'accepti la teva sol·licitud de connexió, si us plau, espera o consulta més tard!</string> + <string name="if_you_cant_meet_in_person">Si no pots trobar-te en persona, mostra el codi QR en una videotrucada o comparteix l\'enllaç.</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Enganxeu l\'enllaç que heu rebut per connectar amb el vostre contacte…</string> + <string name="share_invitation_link">Compartir enllaç d\'un sol ús</string> + <string name="share_1_time_link_with_a_friend">Compartir enllaç d\'un sol ús amb un amic</string> + <string name="share_address_publicly">Compartir adreça públicament</string> + <string name="share_simplex_address_on_social_media">Comparteix l\'adreça SimpleX a les xarxes socials.</string> + <string name="scan_qr_to_connect_to_contact">Per connectar-se, el vostre contacte pot escanejar el codi QR o utilitzar l\'enllaç de l\'aplicació.</string> + <string name="to_protect_against_your_link_replaced_compare_codes">Per protegir-vos de la substitució del vostre enllaç, podeu comparar els codis de seguretat de contacte.</string> + <string name="you_can_accept_or_reject_connection">Quan algú sol·liciti la connexió, pots acceptar-la o rebutjar-la.</string> + <string name="you_can_set_connection_name_to_remember">Podeu definir el nom de la connexió per recordar amb qui s\'ha compartit l\'enllaç.</string> + <string name="you_can_share_your_address">Pots compartir la teva adreça com a enllaç o codi QR; qualsevol es pot connectar amb tu.</string> + <string name="your_chat_profile_will_be_sent_to_your_contact">S\'enviarà el teu perfil de xat\nal teu contacte</string> + <string name="connect__your_profile_will_be_shared">El teu perfil %1$s es compartirà.</string> + <string name="you_will_be_connected_when_your_contacts_device_is_online">Et connectaràs quan el dispositiu del teu contacte estigui en línia, si us plau, espera o consulta més tard!</string> + <string name="you_wont_lose_your_contacts_if_delete_address">Si més tard decideixes eliminar la teva adreça els contactes no es perdran.</string> + <string name="keep_unused_invitation_question">Mantenir la invitació no utilitzada?</string> + <string name="new_chat">Nou xat</string> + <string name="new_message">Missatge nou</string> + <string name="no_filtered_contacts">No hi ha contactes filtrats</string> + <string name="one_time_link">Enllaç d\'invitació d\'un sol ús</string> + <string name="or_scan_qr_code">O escaneja el codi QR</string> + <string name="or_show_this_qr_code">O mostra aquest codi</string> + <string name="paste_link">Enganxar enllaç</string> + <string name="paste_the_link_you_received">Enganxar l\'enllaç rebut</string> + <string name="select_chat_profile">Seleccioneu el perfil de xat</string> + <string name="new_chat_share_profile">Compartir perfil</string> + <string name="share_this_1_time_link">Compartir aquest enllaç d\'un sol ús</string> + <string name="tap_to_paste_link">Toca per enganxar l\'enllaç</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">El codi QR que heu escanejat no és un enllaç de SimpleX.</string> + <string name="the_text_you_pasted_is_not_a_link">El text enganxat no és un enllaç SimpleX.</string> + <string name="this_string_is_not_a_connection_link">Aquesta cadena no és un enllaç de connexió!</string> + <string name="you_can_view_invitation_link_again">Podeu tornar a veure l\'enllaç d\'invitació als detalls de connexió.</string> + <string name="switching_profile_error_message">La teva connexió s\'ha mogut a %s, però s\'ha produït un error inesperat en redirigir-te al perfil.</string> + <string name="mark_code_verified">Marcar com a verificat</string> + <string name="scan_code_from_contacts_app">Escaneja el codi de seguretat des de l\'aplicació del teu contacte.</string> + <string name="contact_list_header_title">Contactes</string> + <string name="how_to_use_simplex_chat">Com utilitzar-lo</string> + <string name="media_and_file_servers">Servidors de fitxers i mitjans</string> + <string name="message_servers">Servidors de missatges</string> + <string name="smp_servers_new_server">Nou servidor</string> + <string name="smp_servers_other">Altres servidors SMP</string> + <string name="xftp_servers_other">Altres servidors XFTP</string> + <string name="smp_servers_preset_server">Servidor preestablert</string> + <string name="smp_servers_preset_address">Adreça predeterminada del servidor</string> + <string name="chat_with_the_founder">Preguntes i idees</string> + <string name="send_us_an_email">Contacta via email</string> + <string name="smp_servers_test_failed">La prova del servidor ha fallat!</string> + <string name="chat_lock">Bloqueig SimpleX</string> + <string name="is_not_verified">%s no està verificat</string> + <string name="is_verified">%s està verificat</string> + <string name="smp_servers_test_some_failed">Alguns servidors han fallat la prova:</string> + <string name="smp_servers_test_server">Provar servidor</string> + <string name="smp_servers_test_servers">Provar servidors</string> + <string name="smp_servers_per_user">Servidors per a noves connexions del vostre perfil de xat actual</string> + <string name="smp_servers_use_server_for_new_conn">Utilitzar per a noves connexions</string> + <string name="smp_servers_use_server">Utilitzar servidor</string> + <string name="your_chat_profiles">Perfils de xat</string> + <string name="smp_servers_your_server">El teu servidor</string> + <string name="smp_servers_your_server_address">L\'adreça del teu servidor</string> + <string name="your_settings">Configuració</string> + <string name="your_simplex_contact_address">La teva adreça SimpleX</string> + <string name="how_to">Com</string> + <string name="how_to_use_your_servers">Com utilitzar els vostres servidors</string> + <string name="enter_one_ICE_server_per_line">Servidors ICE (un per línia)</string> + <string name="install_simplex_chat_for_terminal">Instal·lar SimpleX Chat per al terminal</string> + <string name="ensure_ICE_server_address_are_correct_format_and_unique">Assegureu-vos que les adreces del servidor WebRTC ICE estiguin en el format correcte, que estiguin separades per línies i que no estiguin duplicades.</string> + <string name="rate_the_app">Valoreu l\'aplicació</string> + <string name="reset_all_hints">Restablir totes les pistes</string> + <string name="saved_ICE_servers_will_be_removed">Els servidors WebRTC ICE desats s\'eliminaran.</string> + <string name="subscription_percentage">Mostrar percentatge</string> + <string name="star_on_github">Estrela a GitHub</string> + <string name="use_simplex_chat_servers__question">Utilitzar servidors SimpleX Xat?</string> + <string name="using_simplex_chat_servers">Usant servidors SimpleX Xat.</string> + <string name="your_ICE_servers">Servidors ICE</string> + <string name="your_SMP_servers">Servidors SMP</string> + <string name="your_XFTP_servers">Servidors XFTP</string> + <string name="network_and_servers">Xarxa i servidors</string> + <string name="network_proxy_auth">Autenticació d\'intermediari</string> + <string name="network_socks_proxy">Servidor intermediari SOCKS</string> + <string name="network_socks_proxy_settings">Configuració SOCKS</string> + <string name="network_proxy_random_credentials">Utilitzar credencials aleatòries</string> + <string name="network_socks_toggle_use_socks_proxy">Utilitzar servidor intermediari SOCKS</string> + <string name="network_disable_socks_info">Si confirmeu, els servidors de missatgeria podran veure la vostra adreça IP i el vostre proveïdor, a quins servidors us esteu connectant.</string> + <string name="network_session_mode_session_description">Les noves credencials de SOCKS s\'utilitzaran cada vegada que inicieu l\'aplicació.</string> + <string name="network_session_mode_server_description">S\'utilitzaran noves credencials SOCKS per a cada servidor.</string> + <string name="network_use_onion_hosts_required_desc">Es necessitaran hosts .onion per a la connexió.\nTingueu en compte: no us podreu connectar als servidors sense l\'adreça .onion.</string> + <string name="network_use_onion_hosts_prefer_desc">S\'utilitzaran amfitrions .onion quan estiguin disponibles.</string> + <string name="network_use_onion_hosts_no_desc">No es faran servir hosts .onion</string> + <string name="network_proxy_port">port %d</string> + <string name="network_smp_proxy_mode_private_routing">Encaminament privat</string> + <string name="network_session_mode_server">Servidor</string> + <string name="network_session_mode_transport_isolation">Aïllament de transport</string> + <string name="update_network_session_mode_question">Actualitzar el mode d\'aïllament de transport?</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">Utilitza credencials de servidors intermediari diferents per a cada connexió.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">Utilitza credencials de servidor intermediari diferents per a cada perfil.</string> + <string name="network_disable_socks">Utilitzar connexió a Internet directa?</string> + <string name="network_use_onion_hosts">Utilitzar servidors .onion</string> + <string name="network_enable_socks">Utilitzar servidor intermediari SOCKS?</string> + <string name="network_use_onion_hosts_prefer">Si disponibles</string> + <string name="network_proxy_auth_mode_username_password">Les vostres credencials es podrien enviar sense xifrar.</string> + <string name="theme_colors_section_title">COLORS DE LA INTERFÍCIE</string> + <string name="update_network_smp_proxy_fallback_question">Alternativa d\'encaminament de missatges</string> + <string name="update_network_smp_proxy_mode_question">Mode d\'encaminament de missatges</string> + <string name="app_check_for_updates_button_open">Obrir ubicació del fitxer</string> + <string name="network_smp_proxy_fallback_allow_protected_description">Envieu missatges directament quan l\'adreça IP està protegida i el vostre servidor de destinació no admet l\'encaminament privat.</string> + <string name="network_smp_proxy_fallback_allow_description">Enviar missatges directament quan el vostre servidor de destinació no admet l\'encaminament privat.</string> + <string name="private_routing_show_message_status">Mostrar l\'estat del missatge</string> + <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> + <string name="app_check_for_updates_button_skip">Ometre aquesta versió</string> + <string name="private_routing_explanation">Per protegir la vostra adreça IP l\'encaminament privat utilitza els vostres servidors SMP per enviar missatges.</string> + <string name="network_smp_proxy_mode_unknown">Servidors desconeguts</string> + <string name="app_check_for_updates_update_available">Actualització disponible: %s</string> + <string name="network_smp_proxy_mode_unknown_description">Utilitzar l\'encaminament privat amb servidors desconeguts.</string> + <string name="network_smp_proxy_mode_unprotected_description">Utilitzeu l\'encaminament privat amb servidors desconeguts quan l\'adreça IP no estigui protegida.</string> + <string name="network_smp_proxy_fallback_allow_protected">Amb IP oculta</string> + <string name="app_check_for_updates_canceled">Descàrrega d\'actualització cancel·lada</string> + <string name="app_check_for_updates_installed_successfully_desc">Si us plau, reinicieu l\'aplicació.</string> + <string name="app_check_for_updates_button_remind_later">Recorda més tard</string> + <string name="show_developer_options">Mostrar opcions de desenvolupador</string> + <string name="show_internal_errors">Mostrar errors interns</string> + <string name="show_slow_api_calls">Mostrar trucades lentes d\'API</string> + <string name="app_check_for_updates_notice_desc">Per rebre notificacions sobre les noves versions activeu la comprovació periòdica de les versions Estable o Beta.</string> + <string name="share_with_contacts">Compartir amb contactes</string> + <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">La plataforma de missatgeria i aplicacions que protegeix la vostra privadesa i seguretat.</string> + <string name="profile_is_only_shared_with_your_contacts">El perfil només es comparteix amb els teus contactes.</string> + <string name="we_do_not_store_contacts_or_messages_on_servers">No emmagatzemem cap dels vostres contactes o missatges (un cop lliurats) als servidors.</string> + <string name="your_profile_is_stored_on_your_device">El vostre perfil, contactes i missatges lliurats s\'emmagatzemen al vostre dispositiu.</string> + <string name="permissions_open_settings">Obrir configuració</string> + <string name="next_generation_of_private_messaging">El futur de la missatgeria</string> + <string name="onboarding_network_operators">Operadors de xarxa</string> + <string name="onboarding_notifications_mode_battery">Notificacions i bateria</string> + <string name="you_can_change_it_later">La contrasenya aleatòria s\'emmagatzema a la configuració com a text pla.\nPodeu canviar-ho més tard.</string> + <string name="onboarding_select_network_operators_to_use">Seleccioneu els operadors de xarxa que voleu utilitzar.</string> + <string name="onboarding_choose_server_operators">Operadors de servidor</string> + <string name="setup_database_passphrase">Configura la contrasenya de la base de dades</string> + <string name="use_random_passphrase">Utilitzar contrasenya aleatòria</string> + <string name="open_simplex_chat_to_accept_call">Obrir SimpleX Chat per acceptar la trucada</string> + <string name="relay_server_protects_ip">El servidor de retransmissió protegeix la vostra adreça IP, però pot veure la durada de la trucada.</string> + <string name="webrtc_ice_servers">Servidors WebRTC ICE</string> + <string name="your_ice_servers">Servidors ICE</string> + <string name="privacy_and_security">Privacitat i seguretat</string> + <string name="protect_app_screen">Protegeix la pantalla de l\'aplicació</string> + <string name="protect_ip_address">Protegir l\'adreça IP</string> + <string name="your_privacy">Privacitat</string> + <string name="if_you_enter_passcode_data_removed">Si introduïu aquesta contrasenya en obrir l\'aplicació, totes les dades de l\'aplicació s\'eliminaran de manera irreversible.</string> + <string name="if_you_enter_self_destruct_code">Si introduïu el vostre codi d\'autodestrucció mentre obriu l\'aplicació:</string> + <string name="set_passcode">Estableix codi</string> + <string name="receipts_section_description">Aquesta configuració és per al vostre perfil actual</string> + <string name="receipts_section_description_1">Es pot canviar a la configuració de contacte i grup.</string> + <string name="privacy_media_blur_radius_off">No</string> + <string name="settings_section_title_settings">CONFIGURACIÓ</string> + <string name="privacy_media_blur_radius_soft">Tou</string> + <string name="privacy_media_blur_radius_strong">Fort</string> + <string name="settings_section_title_support">SUPORT SIMPLEX XAT</string> + <string name="settings_section_title_network_connection">Connexió a la xarxa</string> + <string name="settings_section_title_private_message_routing">ENCAMINAMENT DE MISSATGES PRIVAT</string> + <string name="chat_item_ttl_none">mai</string> + <string name="no_received_app_files">No s\'han rebut ni enviats fitxers</string> + <string name="restart_the_app_to_create_a_new_chat_profile">Reinicieu l\'aplicació per crear un perfil de xat nou.</string> + <string name="delete_files_and_media_desc">Aquesta acció no es pot desfer: se suprimiran tots els fitxers i els mitjans rebuts i enviats. Les imatges de baixa resolució es mantindran.</string> + <string name="you_must_use_the_most_recent_version_of_database">Heu d\'utilitzar la versió més recent de la vostra base de dades de xat NOMÉS en un dispositiu, en cas contrari, podeu deixar de rebre els missatges d\'alguns contactes.</string> + <string name="messages_section_description">Aquesta configuració s\'aplica als missatges del vostre perfil de xat actual</string> + <string name="database_is_not_encrypted">La vostra base de dades de xat no està xifrada; definiu una contrasenya per protegir-la.</string> + <string name="passphrase_will_be_saved_in_settings">La frase de contrasenya s\'emmagatzemarà a la configuració com a text pla després de canviar-la o reiniciar l\'aplicació.</string> + <string name="wrong_passphrase">Contrasenya de la base de dades incorrecta</string> + <string name="you_have_to_enter_passphrase_every_time">Heu d\'introduir la contrasenya cada vegada que s\'inicia l\'aplicació: no s\'emmagatzema al dispositiu.</string> + <string name="keychain_error">Error en Keystore</string> + <string name="open_chat">Obrir xat</string> + <string name="store_passphrase_securely_without_recover">Si us plau, emmagatzemeu la contrasenya de manera segura, NO podreu accedir al xat si la perdeu.</string> + <string name="store_passphrase_securely">Si us plau, emmagatzemeu la contrasenya de manera segura, NO la podreu canviar si la perdeu.</string> + <string name="invalid_migration_confirmation">Confirmació de migració no vàlida</string> + <string name="restore_passphrase_not_found_desc">No s\'ha trobat la contrasenya a Keystore, introduïu-la manualment. Això pot haver passat si vau restaurar les dades de l\'aplicació mitjançant una eina de còpia de seguretat. Si no és el cas, poseu-vos en contacte amb els desenvolupadors.</string> + <string name="one_hand_ui">Barres d\'eines d\'aplicacions accessible</string> + <string name="chat_bottom_bar">Barres d\'eines de xat accessible</string> + <string name="chat_list_always_visible">Mostra la llista de xat en una finestra nova</string> + <string name="one_hand_ui_card_title">Commuta la llista de xat:</string> + <string name="terminal_always_visible">Mostrar consola en finestra nova</string> + <string name="one_hand_ui_change_instruction">Podeu canviar-la a la configuració de l\'aparença.</string> + <string name="upgrade_and_open_chat">Actualitzar i obrir el xat</string> + <string name="group_invitation_tap_to_join">Toca per unir-te</string> + <string name="group_invitation_tap_to_join_incognito">Toca per unir-te d\'incògnit</string> + <string name="you_joined_this_group">T\'has unit a aquest grup</string> + <string name="you_rejected_group_invitation">Has rebutjat la invitació del grup</string> + <string name="alert_title_cant_invite_contacts_descr">Esteu utilitzant un perfil d\'incògnit en aquest grup; per evitar mostrar el vostre perfil principal no es permet convidar contactes</string> + <string name="you_sent_group_invitation">Has enviat la invitació del grup</string> + <string name="rcv_group_event_invited_via_your_group_link">convidat mitjançant l\'enllaç del vostre grup</string> + <string name="rcv_group_event_updated_group_profile">ha actualitzat el perfil del grup</string> + <string name="snd_group_event_changed_member_role">has canviat el rol de %s a %s</string> + <string name="snd_group_event_member_blocked">has blocat a %s</string> + <string name="snd_group_event_changed_role_for_yourself">has canviat el teu rol a %s</string> + <string name="snd_group_event_user_left">has marxat</string> + <string name="snd_group_event_member_deleted">has foragitat a %1$s</string> + <string name="snd_group_event_member_unblocked">has desblocat a %s</string> + <string name="group_member_role_observer">observador</string> + <string name="conn_event_enabled_pq">xifratge extrem a extrem resistent quàntic</string> + <string name="initial_member_role">Rol inicial</string> + <string name="invite_to_chat_button">Convidar a xatejar</string> + <string name="new_member_role">Nou rol de membre</string> + <string name="no_contacts_to_add">No hi ha contactes per afegir</string> + <string name="past_member_vName">Membre anterior %1$s</string> + <string name="skip_inviting_button">Ometre convidar membres</string> + <string name="group_member_status_unknown_short">desconegut</string> + <string name="button_add_members">Convidar membres</string> + <string name="no_contacts_selected">Cap contacte seleccionat</string> + <string name="invite_prohibited_description">Esteu provant de convidar el contacte amb qui heu compartit un perfil d\'incògnit al grup en què feu servir el vostre perfil principal</string> + <string name="button_welcome_message">Missatge de benvinguda</string> + <string name="only_chat_owners_can_change_prefs">Només els propietaris del xat poden canviar les preferències.</string> + <string name="only_group_owners_can_change_prefs">Només els propietaris del grup poden canviar-ne les preferències.</string> + <string name="share_address">Compartir adreça</string> + <string name="send_receipts">Enviar rebuts</string> + <string name="info_row_sent_at">Enviat</string> + <string name="info_row_updated_at">Registre actualitzat</string> + <string name="info_row_moderated_at">Moderat el</string> + <string name="share_text_updated_at">Registre actualitzat: %s</string> + <string name="share_text_sent_at">Enviat: %s</string> + <string name="item_info_no_text">sense text</string> + <string name="button_remove_member_question">Eliminar membre?</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="remove_member_button">Eliminar membre</string> + <string name="unblock_member_desc">Els missatges de %s es mostraran!</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">Demaneu al vostre contacte que habiliti les trucades.</string> + <string name="cant_call_member_send_message_alert_text">Enviar un missatge per activar trucades.</string> + <string name="you_need_to_allow_calls">Heu de permetre que el vostre contacte truqui per poder trucar-los.</string> + <string name="group_welcome_preview">Vista prèvia</string> + <string name="receiving_via">Rebent via</string> + <string name="save_and_update_group_profile">Desa i actualitza el perfil del grup</string> + <string name="conn_stats_section_title_servers">SERVIDORS</string> + <string name="group_welcome_title">Missatge de benvinguda</string> + <string name="welcome_message_is_too_long">El missatge de benvinguda és massa llarg</string> + <string name="your_servers">Els teus servidors</string> + <string name="network_preset_servers_title">Servidors preestablerts</string> + <string name="operator_review_conditions">Revisar condicions</string> + <string name="operator_use_for_messages_receiving">Per a rebre</string> + <string name="operator_use_for_files">Utilitzar per a fitxers</string> + <string name="operator_use_for_messages">Utilitzar per a missatges</string> + <string name="make_profile_private">Fes el perfil privat!</string> + <string name="incognito_info_allows">Permet tenir moltes connexions anònimes sense cap dada compartida entre elles en un únic perfil de xat.</string> + <string name="incognito_random_profile">El teu perfil aleatori</string> + <string name="unhide_chat_profile">Mostrar perfil ocult</string> + <string name="incognito_info_share">Quan comparteixes un perfil d\'incògnit amb algú, aquest perfil s\'utilitzarà per als grups als quals et conviden.</string> + <string name="theme">Tema</string> + <string name="import_theme">Importar tema</string> + <string name="import_theme_error">Error en importar tema</string> + <string name="import_theme_error_desc">Assegureu-vos que el fitxer tingui la sintaxi YAML correcta. Exporta el tema per tenir un exemple de l\'estructura del fitxer del tema.</string> + <string name="color_sent_message">Missatge enviat</string> + <string name="color_sent_quote">Resposta enviada</string> + <string name="color_title">Títol</string> + <string name="color_received_message">Missatge rebut</string> + <string name="color_received_quote">Resposta rebuda</string> + <string name="color_wallpaper_tint">Color imatge de fons</string> + <string name="color_wallpaper_background">Color de fons</string> + <string name="appearance_in_app_bars_alpha">Transparència</string> + <string name="appearance_zoom">Zoom</string> + <string name="simplex_links">Enllaços SimpleX</string> + <string name="recent_history">Historial visible</string> + <string name="feature_off">desactivat</string> + <string name="feature_received_prohibited">recepció no permesa</string> + <string name="accept_feature_set_1_day">Establir 1 dia</string> + <string name="only_you_can_send_disappearing">Només tu pots enviar missatges que desapareixen.</string> + <string name="only_your_contact_can_send_disappearing">Només el vostre contacte pot enviar missatges que desapareixen.</string> + <string name="prohibit_sending_voice_messages">Prohibir l\'enviament de missatges de veu.</string> + <string name="only_you_can_send_voice">Només tu pots enviar missatges de veu.</string> + <string name="only_your_contact_can_send_voice">Només el vostre contacte pot enviar missatges de veu.</string> + <string name="voice_prohibited_in_this_chat">Els missatges de veu estan prohibits en aquest xat.</string> + <string name="only_you_can_add_message_reactions">Només tu pots afegir reaccions als missatges.</string> + <string name="only_your_contact_can_add_message_reactions">Només el vostre contacte pot afegir reaccions als missatges.</string> + <string name="message_reactions_prohibited_in_this_chat">Les reaccions als missatges estan prohibides en aquest xat.</string> + <string name="only_you_can_make_calls">Només tu pots fer trucades.</string> + <string name="only_your_contact_can_make_calls">Només el vostre contacte pot fer trucades.</string> + <string name="prohibit_sending_voice">Prohibir l\'enviament de missatges de veu.</string> + <string name="prohibit_sending_simplex_links">Prohibir l\'enviament d\'enllaços SimpleX</string> + <string name="message_deletion_prohibited_in_chat">Es prohibeix la supressió irreversible de missatges.</string> + <string name="group_members_can_add_message_reactions">Els membres poden afegir reaccions als missatges.</string> + <string name="group_members_can_delete">Els membres poden suprimir de manera irreversible els missatges enviats. (24 hores)</string> + <string name="group_members_can_send_disappearing">Els membres poden enviar missatges que desapareixen.</string> + <string name="group_members_can_send_simplex_links">Els membres poden enviar enllaços SimpleX.</string> + <string name="message_reactions_are_prohibited">Les reaccions als missatges estan prohibides.</string> + <string name="enable_sending_recent_history">Envia fins a 100 darrers missatges als nous membres.</string> + <string name="voice_messages_are_prohibited">Els missatges de veu estan prohibits.</string> + <string name="feature_roles_owners">propietaris</string> + <string name="v4_5_multiple_chat_profiles">Múltiples perfils de xat</string> + <string name="v4_4_french_interface_descr">Gràcies als usuaris: contribuïu a través de Weblate!</string> + <string name="v4_4_verify_connection_security">Comprovar la seguretat de la connexió</string> + <string name="v4_6_group_welcome_message_descr">Estableix el missatge que es mostra als nous membres!</string> + <string name="v5_1_japanese_portuguese_interface">Interfície d\'usuari en japonès i portuguès</string> + <string name="v5_1_message_reactions">Reaccions a missatges</string> + <string name="v4_6_reduced_battery_usage_descr">Aviat hi haurà més millores!</string> + <string name="v5_0_polish_interface">Interfície en polonès</string> + <string name="v5_1_self_destruct_passcode">Codi d\'autodestrucció</string> + <string name="v5_0_app_passcode_descr">Configureu-lo en lloc de l\'autenticació del sistema.</string> + <string name="v4_6_chinese_spanish_interface_descr">Gràcies als usuaris: contribuïu a través de Weblate!</string> + <string name="v5_0_polish_interface_descr">Gràcies als usuaris: contribuïu a través de Weblate!</string> + <string name="whats_new_thanks_to_users_contribute_weblate">Gràcies als usuaris: contribuïu a través de Weblate!</string> + <string name="v5_0_large_files_support">Vídeos i fitxers de fins a 1 GB</string> + <string name="v5_1_better_messages_descr">- Missatges de veu fins a 5 minuts.\n- Temps personalitzat per a missatges temporals.\n- Historial d\'edició.</string> + <string name="v5_2_more_things_descr">- Lliurament de missatges més estable.\n- Grups millorats.\n- I més!</string> + <string name="v5_3_new_desktop_app">Nova aplicació per a ordinador!</string> + <string name="v5_2_message_delivery_receipts_descr">La doble comprovació que ens mancava! ✅</string> + <string name="v5_2_fix_encryption">Manté les vostres connexions</string> + <string name="v5_2_disappear_one_message">Fer desaparèixer un missatge</string> + <string name="v5_5_join_group_conversation">Unir-se a converses de grup</string> + <string name="v5_5_join_group_conversation_descr">Historial recent i bot de directori millorat.</string> + <string name="v6_1_better_calls_descr">Canvia l\'àudio i el vídeo durant la trucada.</string> + <string name="v6_1_switch_chat_profile_descr">Canvia el perfil de xat per a invitacions d\'un sol ús.</string> + <string name="you_can_enable_delivery_receipts_later">Podeu activar-ho més tard mitjançant Configuració</string> + <string name="link_a_mobile">Enllaçar un mòbil</string> + <string name="linked_mobiles">Mòbils enllaçats</string> + <string name="this_device_name">Nom d\'aquest dispositiu</string> + <string name="verify_code_on_mobile">Verificar codi al mòbil</string> + <string name="verify_connection">Verificar connexió</string> + <string name="this_device_name_shared_with_mobile">El nom del dispositiu es compartirà amb el client mòbil connectat.</string> + <string name="this_device">Aquest dispositiu</string> + <string name="linked_desktop_options">Opcions ordinador enllaçat</string> + <string name="no_connected_mobile">Cap mòbil connectat</string> + <string name="not_compatible">No compatible!</string> + <string name="open_port_in_firewall_title">Obrir port al tallafoc</string> + <string name="paste_desktop_address">Enganxar adreça d\'ordinador</string> + <string name="random_port">Aleatori</string> + <string name="refresh_qr_code">Recarregar</string> + <string name="scan_qr_code_from_desktop">Escaneja el codi QR des de l\'ordinador</string> + <string name="open_port_in_firewall_desc">Per permetre que una aplicació mòbil es connecti a l\'ordinador obriu aquest port al vostre tallafoc, si el teniu habilitat</string> + <string name="verify_connections">Verificar connexions</string> + <string name="in_developing_desc">Aquesta funció encara no està disponible. Prova la propera versió.</string> + <string name="connect_plan_this_is_your_own_one_time_link">Aquest és el vostre enllaç d\'un sol ús!</string> + <string name="connect_plan_this_is_your_own_simplex_address">Aquesta és la teva pròpia adreça SimpleX!</string> + <string name="migrate_to_device_confirm_network_settings_footer">Confirmeu que la configuració de xarxa és correcta per a aquest dispositiu.</string> + <string name="servers_info_previously_connected_servers_section_header">Servidors connectats prèviament</string> + <string name="servers_info_proxied_servers_section_header">Servidors intermediats</string> + <string name="how_it_works">Com funciona</string> + <string name="v5_5_new_interface_languages">IU en hongarès i turc</string> + <string name="passcode_not_changed">Codi d\'accés no canviat!</string> + <string name="v5_8_private_routing_descr">Protegiu la vostra adreça IP dels servidors de retransmissió de missatgeria escollits pels vostres contactes.\nActiva a la configuració de *Xarxa i servidors*.</string> + <string name="address_creation_instruction">Toqueu Crea adreça SimpleX al menú per crear-la més tard.</string> + <string name="v4_5_private_filenames_descr">Per protegir la zona horària els fitxers d\'imatge/veu utilitzen UTC.</string> + <string name="unblock_member_confirmation">Desbloca</string> + <string name="migrate_from_device_upload_failed">La càrrega ha fallat</string> + <string name="to_verify_compare">Per verificar el xifratge d\'extrem a extrem amb el vostre contacte compareu (o escanegeu) el codi dels vostres dispositius.</string> + <string name="updating_settings_will_reconnect_client_to_all_servers">L\'actualització de la configuració reconnectarà el client a tots els servidors.</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Atenció: l\'inici del xat a diversos dispositius és incompatible i provocaria errors en el lliurament de missatges.</string> + <string name="switch_verb">Canvia</string> + <string name="save_settings_question">Voleu desar les preferències?</string> + <string name="stop_sharing_address">Voleu deixar de compartir l\'adreça?</string> + <string name="secret_text">secret</string> + <string name="icon_descr_speaker_off">Altaveu desactivat</string> + <string name="icon_descr_speaker_on">Altaveu activat</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Sense Tor o una VPN, el servidors de fitxers podran veure la vostra adreça.</string> + <string name="la_mode_system">Sistema</string> + <string name="self_destruct">Autodestrucció</string> + <string name="self_destruct_passcode">Codi d\'autodestrucció</string> + <string name="receipts_section_groups">Grups petits (màx. 20)</string> + <string name="chat_database_exported_not_all_files">Alguns fitxers no han estat exportats</string> + <string name="network_option_seconds_label">s</string> + <string name="size">Mida</string> + <string name="start_chat_question">Voleu iniciar un xat?</string> + <string name="wallpaper_scale">Escala</string> + <string name="chat_preferences_yes">sí</string> + <string name="custom_time_picker_select">Selecciona</string> + <string name="migrate_to_device_bytes_downloaded">%s baixades</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="network_type_ethernet">Ethernet per cable</string> + <string name="servers_info_statistics_section_header">Estadistiques</string> + <string name="xftp_server">Servidor XFTP</string> + <string name="v4_2_security_assessment">Avaluació de seguretat</string> + <string name="smp_server">Servidor SMP</string> + <string name="custom_time_unit_seconds">segons</string> + <string name="migrate_from_device_start_chat">Inicia un xat</string> + <string name="simplex_link_invitation">"Invitació d\'un sol ús per SimpleX"</string> + <string name="file_not_approved_descr">Sense Tor o una VPN, la vostra adreça serà visible per als següents relays XFTP: %1$s.</string> + <string name="possible_slow_function_title">Funció lenta</string> + <string name="group_preview_you_are_invited">heu estat convidat a un grup</string> + <string name="info_view_search_button">cerca</string> + <string name="connect_via_link_or_qr_from_clipboard_or_in_person">(escaneja o enganxa del porta-retalls)</string> + <string name="scan_QR_code">Escaneja un codi QR</string> + <string name="add_contact_or_create_group">Comença una conversa nova</string> + <string name="image_descr_simplex_logo">Logo de SimpleX</string> + <string name="icon_descr_simplex_team">Equip SimpleX</string> + <string name="you_accepted_connection">Heu acceptat la connexió</string> + <string name="scan_paste_link">Escaneja / Enganxa un enllaç</string> + <string name="scan_code">Escaneja codi</string> + <string name="security_code">Codi de seguretat</string> + <string name="smp_save_servers_question">Voleu desar els servidors?</string> + <string name="smp_servers_scan_qr">Escaneja el codi QR del servidor</string> + <string name="smp_servers">Servidors SMP</string> + <string name="xftp_servers">Servidors XFTP</string> + <string name="audio_device_speaker">Altaveu</string> + <string name="settings_section_title_you">VÓS</string> + <string name="chat_item_ttl_seconds">%s segon(s)</string> + <string name="you_are_invited_to_group">"Heu estat convidat a un grup"</string> + <string name="rcv_group_event_1_member_connected">%s connectat</string> + <string name="rcv_conn_event_verification_code_reset">codi de seguretat modificat</string> + <string name="conn_event_disabled_pq">encriptació extrem a extrem estàndard</string> + <string name="select_contacts">Selecciona contactes</string> + <string name="group_info_member_you">vós : %1$s</string> + <string name="current_version_timestamp">%s (actual)</string> + <string name="save_welcome_message_question">Voleu desar el missatge de benvinguda?</string> + <string name="color_mode_system">Sistema</string> + <string name="theme_system">Sistema</string> + <string name="language_system">Sistema</string> + <string name="color_secondary">Secundari</string> + <string name="v6_1_better_security_descr">Protocols de SimpleX revisats per Trail of Bits.</string> + <string name="scan_from_mobile">Escaneja amb el mòbil</string> + <string name="video_descr">Vídeo</string> + <string name="info_view_video_button">vídeo</string> + <string name="settings_section_title_user_theme">Tema del perfil</string> + <string name="profile_password">Contrasenya del perfil</string> + <string name="prohibit_calls">No es permeten trucades ni videotrucades.</string> + <string name="prohibit_message_reactions">No es permeten reaccions als missatges.</string> + <string name="prohibit_sending_disappearing_messages">No es permet l\'enviament de missatges temporals.</string> + <string name="prohibit_message_deletion">No es permet l\'eliminació irreversible de missatges.</string> + <string name="prohibit_message_reactions_group">No es permeten reaccions als missatges.</string> + <string name="prohibit_direct_messages">No es permet l\'enviament de missatges directes als membres.</string> + <string name="prohibit_sending_disappearing">No es permet l\'enviament de missatges temporals.</string> + <string name="profile_update_will_be_sent_to_contacts">L\'actualització del perfil s\'enviarà als vostres contactes.</string> + <string name="a_plus_b">a + b</string> + <string name="strikethrough_text">ratllat</string> + <string name="delete_messages_after">Suprimeix els missatges després</string> + <string name="users_delete_profile_for">Eliminar el perfil de xat per</string> + <string name="connection_not_ready">La connexió no està preparada.</string> + <string name="error_creating_chat_tags">Error en crear la llista de xat</string> + <string name="error_loading_chat_tags">Error en carregar llistes de xat</string> + <string name="error_updating_chat_tags">Error en actualitzar les llistes de xat</string> + <string name="chat_list_contacts">Contactes</string> + <string name="chat_list_favorites">Preferits</string> + <string name="no_unread_chats">Cap xat no llegit</string> + <string name="chat_list_add_list">Afegir llista</string> + <string name="chat_list_all">Tot</string> + <string name="chat_list_businesses">Negocis</string> + <string name="open_with_app">Obrir amb %s</string> + <string name="no_chats">Sense xats</string> + <string name="no_chats_found">No s\'han trobat xats</string> + <string name="change_list">Canviar llista</string> + <string name="delete_chat_list_menu_action">Eliminar</string> + <string name="chat_list_groups">Grups</string> + <string name="no_chats_in_list">No hi ha cap xat a la llista %s.</string> + <string name="list_menu">Llista</string> + <string name="duplicated_list_error">El nom de la llista i l\'emoji haurien de ser diferents per a totes les llistes.</string> + <string name="chat_list_notes">Notes</string> + <string name="add_to_list">Afegir a la llista</string> + <string name="create_list">Crear llista</string> + <string name="save_list">Desar llista</string> + <string name="delete_chat_list_warning">Tots els xats s\'eliminaran de la llista %s i aquesta serà suprimida</string> + <string name="change_order_chat_list_menu_action">Canviar ordre</string> + <string name="edit_chat_list_menu_action">Editar</string> + <string name="delete_chat_list_question">Eliminar llista?</string> + <string name="list_name_field_placeholder">Nom de la llista...</string> + <string name="report_reason_spam">Spam</string> + <string name="archive_verb">Arxivar</string> + <string name="archive_report">Arxivar informe</string> + <string name="report_compose_reason_header_spam">Informar de spam: només ho veurà la moderació del grup.</string> + <string name="report_compose_reason_header_profile">Informar del perfil d\'un/a membre: només ho veurà la moderació del grup.</string> + <string name="report_compose_reason_header_community">Informar de violació: només ho veurà la moderació del grup.</string> + <string name="report_compose_reason_header_illegal">Informar de contingut: només ho veurà la moderació del grup.</string> + <string name="report_compose_reason_header_other">Informar d\'altres: només ho veurà la moderació del grup.</string> + <string name="report_item_archived_by">informe arxivat per %s</string> + <string name="report_reason_other">Un altre motiu</string> + <string name="report_reason_community">Violació de les normes de la comunitat</string> + <string name="report_reason_illegal">Contingut inadequat</string> + <string name="report_reason_profile">Perfil inadequat</string> + <string name="delete_report">Suprimeix l\'informe</string> + <string name="report_archive_alert_desc">L\'informe s\'arxivarà.</string> + <string name="report_verb">Informar</string> + <string name="blocking_reason_spam">Spam</string> + <string name="blocking_reason_content">El contingut infringeix les condicions d\'ús</string> + <string name="connection_error_blocked">Connexió blocada</string> + <string name="connection_error_blocked_desc">La connexió està bloquejada per l\'operador del servidor:\n%1$s.</string> + <string name="report_archive_alert_title">Arxivar informe?</string> + <string name="file_error_blocked">El fitxer està blocat per l\'operador del servidor:\n%1$s.</string> + <string name="report_reason_alert_title">Motiu de l\'informe?</string> + <string name="group_reports_active_one">1 informe</string> + <string name="group_reports_active">%d informes</string> + <string name="group_reports_member_reports">Informes de membres</string> + <string name="chat_list_group_reports">Informes</string> + <string name="prefs_error_saving_settings">Error en desar configuració</string> + <string name="privacy_chat_list_open_links_no">No</string> + <string name="privacy_chat_list_open_web_link_question">Obrir l\'enllaç web?</string> + <string name="privacy_chat_list_open_links_yes">Sí</string> + <string name="privacy_chat_list_open_web_link">Obrir enllaç</string> + <string name="group_member_role_moderator">moderador</string> + <string name="privacy_chat_list_open_links_ask">Preguntar</string> + <string name="privacy_chat_list_open_links">Obrir enllaços de la llista de xat</string> + <string name="report_item_archived">informes arxivats</string> + <string name="report_item_visibility_moderators">Només ho veuen remitents i moderació</string> + <string name="report_item_visibility_submitter">Només ho veieu vosaltres i moderació</string> + <string name="error_creating_report">Error en crear informe</string> + <string name="text_field_set_chat_placeholder">Establir nom del xat…</string> + <string name="chat_ttl_options_footer">Suprimiu els missatges de xat del vostre dispositiu.</string> + <string name="change_automatic_deletion_question">Voleu canviar la supressió automàtica de missatges?</string> + <string name="chat_item_ttl_default">predeterminat (%s)</string> + <string name="chat_item_ttl_year">1 any</string> + <string name="disable_automatic_deletion">Desactivar la supressió de missatges</string> + <string name="disable_automatic_deletion_question">Desactivar la supressió automàtica de missatges?</string> + <string name="change_automatic_chat_deletion_message">Aquesta acció no es pot desfer; els missatges enviats i rebuts en aquest xat anteriors al seleccionat se suprimiran.</string> + <string name="disable_automatic_deletion_message">Els missatges d\'aquest xat no se suprimiran mai.</string> + <string name="network_smp_web_port_section_title">Port TCP per a missatgeria</string> + <string name="network_smp_web_port_toggle">Emprar port web</string> + <string name="network_smp_web_port_footer">Emprar el port TCP %1$s quan no se n\'especifica cap.</string> + <string name="mute_all_chat">Silenciar tot</string> + <string name="unread_mentions">Mencions no llegides</string> + <string name="max_group_mentions_per_message_reached">Podeu mencionar fins a %1$s membres per missatge!</string> + <string name="enable_sending_member_reports">Permetre denunciar missatges a moderació.</string> + <string name="disable_sending_member_reports">No permetre denunciar missatges a moderació.</string> + <string name="report_archive_alert_title_all">Arxivar totes les denúncies?</string> + <string name="report_archive_alert_title_nth">Arxivar %d denúncies?</string> + <string name="archive_reports">Arxivar denúncies</string> + <string name="report_archive_for_all_moderators">Per a totes les moderadores</string> + <string name="report_archive_for_me">Per a mi</string> + <string name="notification_group_report">Denúncia: %s</string> + <string name="member_reports_are_prohibited">En aquest grup no es permet denunciar missatges.</string> + <string name="group_members_can_send_reports">Els/les membres poden denunciar els missatges a moderació.</string> + <string name="report_archive_alert_desc_all">Totes les denúncies s\'arxivaran.</string> + <string name="v6_3_mentions">Mencionar els membres 👋</string> + <string name="v6_3_reports">Enviar denúncies privades</string> + <string name="v6_3_better_privacy_and_security">Millor privadesa i seguretat</string> + <string name="v6_3_organize_chat_lists_descr">No us perdeu missatges importants.</string> + <string name="v6_3_faster_sending_messages">Enviament de missatges més ràpid.</string> + <string name="v6_3_organize_chat_lists">Organitzar els xats en llistes</string> + <string name="v6_3_private_media_file_names">Noms de fitxers multimèdia privats.</string> + <string name="v6_3_set_message_expiration_in_chats">Establir la caducitat del missatge als xats.</string> + <string name="v6_3_mentions_descr">Rebeu notificació quan se us menciona.</string> + <string name="v6_3_better_groups_performance">Millor rendiment dels grups</string> + <string name="v6_3_faster_deletion_of_groups">Supressió més ràpida de grups.</string> + <string name="v6_3_reports_descr">Ajudar els administradors a moderar els seus grups.</string> + <string name="group_preview_rejected">rebutjat</string> + <string name="group_member_status_rejected">rebutjat</string> + <string name="group_member_status_pending_approval_short">pendent</string> + <string name="group_member_status_pending_approval">pendent d\'aprovació</string> + <string name="block_members_desc">Tots els missatges nous d\'aquests/es membres s\'amagaran!</string> + <string name="block_members_for_all_question">Blocar membres per a tots/es?</string> + <string name="operator_updated_conditions">Condicions actualitzades</string> + <string name="error_reading_passphrase">Error en llegir la contrasenya de la base de dades</string> + <string name="members_will_be_removed_from_chat_cannot_be_undone">Els/les membres s\'eliminaran del xat; això no es pot desfer!</string> + <string name="unblock_members_desc">Els missatges d\'aquests/es membres es mostraran!</string> + <string name="unblock_members_for_all_question">Desblocar membres per a tots/es?</string> + <string name="feature_roles_moderators">moderació</string> + <string name="restore_passphrase_can_not_be_read_desc">La frase de contrasenya a Keystore no es pot llegir. Això pot haver passat després que l\'actualització del sistema sigui incompatible amb l\'aplicació. Si no és el cas, poseu-vos en contacte amb els desenvolupadors.</string> + <string name="members_will_be_removed_from_group_cannot_be_undone">Els/les membres s\'eliminaran del grup; això no es pot desfer!</string> + <string name="restore_passphrase_can_not_be_read_enter_manually_desc">La frase de contrasenya a Keystore no es pot llegir, introduïu-la manualment. Això pot haver passat després que l\'actualització del sistema sigui incompatible amb l\'aplicació. Si no és el cas, poseu-vos en contacte amb els desenvolupadors.</string> + <string name="button_remove_members_question">Expulsar membres?</string> + <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Política de privadesa i condicions d\'ús.</string> + <string name="onboarding_conditions_private_chats_not_accessible">Els xats privats, els grups i els vostres contactes no són accessibles per als operadors de servidor.</string> + <string name="onboarding_conditions_accept">Acceptar</string> + <string name="onboarding_conditions_by_using_you_agree">En utilitzar SimpleX Chat accepteu:\n- enviar només contingut legal en grups públics.\n- Respectar els altres usuaris, sense correu brossa.</string> + <string name="onboarding_conditions_configure_server_operators">Configurar els operadors de servidor</string> + <string name="simplex_link_channel">Enllaç al canal SimpleX</string> + <string name="link_requires_newer_app_version_please_upgrade">Aquest enllaç requereix una versió de l\'aplicació més recent. Actualitzeu l\'aplicació o demaneu al vostre contacte que enviï un enllaç compatible.</string> + <string name="unsupported_connection_link">Enllaç de connexió no compatible</string> + <string name="privacy_short_links">Emprar enllaços curts (BETA)</string> + <string name="full_link_button_text">Enllaç complet</string> + <string name="short_link_button_text">Enllaç curt</string> + <string name="network_smp_web_port_all">Tots els servidors</string> + <string name="network_smp_web_port_off">Apagat</string> + <string name="network_smp_web_port_preset_footer">Feu servir el port TCP 443 només per a servidors predefinits.</string> + <string name="network_smp_web_port_preset">Servidors predefinits</string> +</resources> 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 75d09ecfb2..439b9df18d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -8,10 +8,10 @@ <string name="smp_servers_preset_add">Přidat přednastavené servery</string> <string name="network_settings">Pokročilá nastavení sítě</string> <string name="accept">Přijmout</string> - <string name="smp_servers_add">Přidat server…</string> + <string name="smp_servers_add">Přidat server</string> <string name="network_enable_socks_info">Přistupovat k serverům přes SOCKS proxy na portu %d\? Před povolením této možnosti musí být spuštěna proxy.</string> <string name="accept_feature">Přijmout</string> - <string name="allow_your_contacts_to_send_disappearing_messages">Povolte svým kontaktům odesílat mizící zprávy.</string> + <string name="allow_your_contacts_to_send_disappearing_messages">Povolit svým kontaktům odesílat mizící zprávy.</string> <string name="about_simplex_chat">O SimpleX Chat</string> <string name="smp_servers_add_to_another_device">Přidat do jiného zařízení</string> <string name="allow_verb">Povolit</string> @@ -31,11 +31,11 @@ <string name="accept_contact_incognito_button">Přijmout inkognito</string> <string name="accept_connection_request__question">Přijmout žádost o připojení\?</string> <string name="all_group_members_will_remain_connected">Všichni členové skupiny zůstanou připojeni.</string> - <string name="allow_irreversible_message_deletion_only_if">Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí.</string> + <string name="allow_irreversible_message_deletion_only_if">Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt povolí. (24 hodin)</string> <string name="allow_direct_messages">Přímé zprávy členům povoleny.</string> - <string name="allow_to_delete_messages">Nevratné mazání odeslaných zpráv povoleno.</string> + <string name="allow_to_delete_messages">Povolit nevratné mazání odeslaných zpráv. (24 hodin)</string> <string name="clear_chat_warning">Všechny zprávy budou smazány – tuto akci nelze vrátit zpět! Zprávy budou smazány POUZE pro vás.</string> - <string name="allow_your_contacts_irreversibly_delete">Umožněte svým kontaktům nevratně odstranit odeslané zprávy.</string> + <string name="allow_your_contacts_irreversibly_delete">Povolit svým kontaktům nevratně odstranit odeslané zprávy. (24 hodin)</string> <string name="allow_your_contacts_to_send_voice_messages">Povolte svým kontaktům odesílání hlasových zpráv.</string> <string name="button_create_group_link">Vytvořit odkaz</string> <string name="delete_link_question">Smazat odkaz\?</string> @@ -45,9 +45,9 @@ <string name="info_row_connection">Připoj</string> <string name="conn_level_desc_indirect">nepřímé (%1$s)</string> <string name="conn_stats_section_title_servers">SERVERY</string> - <string name="receiving_via">Příjímáno přez</string> + <string name="receiving_via">Příjímáno přes</string> <string name="create_secret_group_title">Vytvoření tajné skupiny</string> - <string name="group_display_name_field">Zobrazený název skupiny:</string> + <string name="group_display_name_field">Zadejte název skupiny:</string> <string name="group_full_name_field">Úplný název skupiny:</string> <string name="group_main_profile_sent">Váš chat profil bude zaslán členům skupiny</string> <string name="group_profile_is_stored_on_members_devices">Profil skupiny je uložen v zařízeních členů, nikoli na serverech.</string> @@ -55,7 +55,6 @@ <string name="update_network_settings_question">Aktualizovat nastavení sítě\?</string> <string name="incognito">Inkognito</string> <string name="incognito_random_profile">Váš náhodný profil</string> - <string name="save_color">Uložit barvu</string> <string name="reset_color">Obnovit barvu</string> <string name="color_primary">Zbarvení</string> <string name="chat_preferences_you_allow">Povolujete</string> @@ -65,18 +64,18 @@ <string name="chat_preferences_always">vždy</string> <string name="set_group_preferences">Nastavení skupinových předvoleb</string> <string name="your_preferences">Vaše preference</string> - <string name="timed_messages">Mizící zprávy</string> + <string name="timed_messages">Mizející zprávy</string> <string name="feature_enabled_for_contact">povoleno pro kontakt</string> <string name="feature_received_prohibited">přijaté, zakázané</string> <string name="both_you_and_your_contact_can_send_disappearing">Vy i váš kontakt můžete posílat mizící zprávy.</string> <string name="only_your_contact_can_send_disappearing">Zmizelé zprávy může odesílat pouze váš kontakt.</string> - <string name="only_you_can_delete_messages">Nevratně mazat zprávy můžete pouze vy (váš kontakt je může označit ke smazání).</string> + <string name="only_you_can_delete_messages">Nevratně mazat zprávy můžete pouze vy (váš kontakt je může označit ke smazání). (24 hodin)</string> <string name="message_deletion_prohibited">Nevratné mazání zpráv je v tomto chatu zakázáno.</string> <string name="prohibit_direct_messages">Přímé zprávy členům zakázány.</string> - <string name="ttl_sec">%d sec</string> + <string name="ttl_sec">%d s</string> <string name="ttl_s">%ds</string> <string name="ttl_min">%d min</string> - <string name="ttl_hour">%d hodinu</string> + <string name="ttl_hour">%d hodina</string> <string name="feature_offered_item_with_param">nabízeno %s: %2s</string> <string name="v4_2_group_links">Odkazy na skupiny</string> <string name="v4_3_voice_messages">Hlasové zprávy</string> @@ -85,9 +84,9 @@ <string name="v4_4_verify_connection_security_desc">Porovnejte bezpečnostní kódy se svými kontakty.</string> <string name="app_name">SimpleX</string> <string name="thousand_abbreviation">k</string> - <string name="connect_via_contact_link">Připojit se odkazem\?</string> - <string name="connect_via_invitation_link">Připojit se pozvánkou\?</string> - <string name="connect_via_group_link">Připojit se odkazem skupiny\?</string> + <string name="connect_via_contact_link">Připojit přes kontaktní adresu?</string> + <string name="connect_via_invitation_link">Připojit přes jednorázovou pozvánku?</string> + <string name="connect_via_group_link">Připojit se do skupiny?</string> <string name="profile_will_be_sent_to_contact_sending_link">Váš profil bude odeslán kontaktu, od kterého jste obdrželi tento odkaz.</string> <string name="server_connected">připojeno</string> <string name="server_error">chyba</string> @@ -191,8 +190,6 @@ <string name="to_start_a_new_chat_help_header">Pro zahájení nové konverzace</string> <string name="chat_help_tap_button">Klepněte na tlačítko</string> <string name="above_then_preposition_continuation">potom:</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Přidejte nový kontakt</b>: vytvořte jednorázý QR kód pro váš kontakt.]]></string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Naskenujte QR kód</b>: připojíte se ke kontaktu, který vám QR kód ukázal.]]></string> <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 počítač: naskenujte QR kód z aplikace přez <b>Skenovat QR kód</b>.]]></string> <string name="clear_chat_question">Vyčistit chat\?</string> <string name="clear_verb">Vyčistit</string> @@ -209,10 +206,9 @@ <string name="you_will_be_connected_when_group_host_device_is_online">Ke skupině budete připojeni, až bude zařízení hostitele skupiny online, vyčkejte prosím nebo se podívejte později!</string> <string name="you_will_be_connected_when_your_connection_request_is_accepted">Budete připojeni, jakmile bude vaše žádost o připojení přijata, vyčkejte prosím nebo se podívejte později!</string> <string name="connection_request_sent">Požadavek na připojení byl odeslán!</string> - <string name="create_one_time_link">Vytvořit jednorázovou pozvánku</string> <string name="one_time_link">Jednorázová pozvánka</string> <string name="security_code">Bezpečnostní kód</string> - <string name="is_verified">%s je ověřeno</string> + <string name="is_verified">%s ověřen</string> <string name="chat_console">Chat konzole</string> <string name="smp_servers">SMP servery</string> <string name="smp_servers_preset_address">Přednastavená adresa serveru</string> @@ -228,11 +224,10 @@ <string name="how_to_use_your_servers">Jak používat servery</string> <string name="your_ICE_servers">Vaše servery ICE</string> <string name="configure_ICE_servers">Konfigurace serverů ICE</string> - <string name="network_settings_title">Nastavení sítě</string> + <string name="network_settings_title">Pokročilé nastavení</string> <string name="network_enable_socks">Použít proxy server SOCKS\?</string> <string name="network_disable_socks">Použít přímé připojení k internetu\?</string> <string name="network_use_onion_hosts_no">Ne</string> - <string name="network_use_onion_hosts_no_desc_in_alert">Onion hostitelé nebudou použiti.</string> <string name="network_session_mode_user">Chat profil</string> <string name="network_session_mode_entity">Připojení</string> <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> @@ -242,13 +237,13 @@ <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Platforma pro zasílání zpráv a aplikace chránící vaše soukromí a bezpečnost.</string> <string name="create_profile">Vytvořit profil</string> <string name="profile_is_only_shared_with_your_contacts">Profil je sdílen pouze s vašimi kontakty.</string> - <string name="display_name_cannot_contain_whitespace">Zobrazované jméno nesmí obsahovat mezery.</string> + <string name="display_name_cannot_contain_whitespace">Zobrazované jméno nesmí obsahovat bílé znaky.</string> <string name="bold_text">tučně</string> <string name="callstatus_in_progress">probíhající hovor</string> <string name="decentralized">Decentralizovaná</string> <string name="how_it_works">Jak to funguje</string> <string name="how_simplex_works">Jak funguje SimpleX</string> - <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Pouze klientská zařízení ukládají uživatelské profily, kontakty, skupiny a zprávy odesílané pomocí <b>2 vrstvého koncového šifrování</b>.]]></string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Pouze klientská zařízení ukládají uživatelské profily, kontakty, skupiny a zprávy.</string> <string name="onboarding_notifications_mode_title">Soukromé oznámení</string> <string name="onboarding_notifications_mode_periodic">Pravidelné</string> <string name="ignore">Ignorovat</string> @@ -268,7 +263,7 @@ <string name="settings_section_title_settings">NASTAVENÍ</string> <string name="settings_section_title_help">NÁPOVĚDA</string> <string name="settings_section_title_device">ZAŘÍZENÍ</string> - <string name="settings_section_title_chats">CHATY</string> + <string name="settings_section_title_chats">KONVERZACE</string> <string name="settings_experimental_features">Experimentální funkce</string> <string name="settings_section_title_socks">SOCKS PROXY</string> <string name="settings_section_title_icon">IKONA APLIKACE</string> @@ -278,29 +273,28 @@ <string name="export_database">Export databáze</string> <string name="import_database">Import databáze</string> <string name="delete_database">Smazat databázi</string> - <string name="error_exporting_chat_database">Chyba exportu chat databáze</string> + <string name="error_exporting_chat_database">Chyba při exportu databáze chatu</string> <string name="import_database_confirmation">Import</string> - <string name="restart_the_app_to_use_imported_chat_database">Restartujte aplikaci, abyste mohli používat importovanou chat databázi.</string> + <string name="restart_the_app_to_use_imported_chat_database">Restartujte aplikaci, abyste mohli používat importovanou databázi chatu.</string> <string name="delete_chat_profile_question">Smazat chat profil\?</string> <string name="delete_chat_profile_action_cannot_be_undone_warning">Tuto akci nelze vzít zpět! Váš profil, kontakty, zprávy a soubory budou nenávratně ztraceny.</string> <string name="restart_the_app_to_create_a_new_chat_profile">Restartujte aplikaci a vytvořte nový chat profil.</string> - <string name="you_must_use_the_most_recent_version_of_database">Nejnovější verzi chat databáze 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ů.</string> - <string name="stop_chat_to_enable_database_actions">Zastavte chat a povolte akce s databází.</string> + <string name="you_must_use_the_most_recent_version_of_database">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ů.</string> <string name="files_and_media_section">Soubory a média</string> <string name="delete_files_and_media_question">Smazat soubory a média\?</string> <string name="delete_messages">Odstranit zprávy</string> - <string name="remove_passphrase_from_keychain">Odstranit frázi z úložiště klíčů\?</string> + <string name="remove_passphrase_from_keychain">Odstranit přístupovou frázi z úložiště klíčů?</string> <string name="notifications_will_be_hidden">Oznámení budou doručována pouze do doby, než se aplikace zastaví!</string> <string name="remove_passphrase">Odstranit</string> <string name="update_database">Aktualizovat</string> - <string name="current_passphrase">Aktuální fráze…</string> + <string name="current_passphrase">Aktuální přístupová fráze…</string> <string name="update_database_passphrase">Aktualizovat přístupovou frázi</string> <string name="enter_correct_current_passphrase">Zadejte prosím správnou aktuální přístupovou frázi.</string> - <string name="database_is_not_encrypted">Chat databáze není šifrována - nastavte přístupovou frázi pro její ochranu.</string> + <string name="database_is_not_encrypted">Databáze chatu není šifrována - nastavte přístupovou frázi pro její ochranu.</string> <string name="keychain_is_storing_securely">K bezpečnému uložení přístupové fráze slouží úložiště klíčů Android - umožňuje fungování služby oznámení.</string> <string name="impossible_to_recover_passphrase"><![CDATA[<b>Upozornění</b>: pokud přístupovou frázi ztratíte, NEBUDE možné ji obnovit ani změnit.]]></string> - <string name="database_will_be_encrypted_and_passphrase_stored">Databáze bude šifrována a přístupová fráze bude uložena v úložišti klíčů.</string> - <string name="store_passphrase_securely">Heslo uložte bezpečně, v případě jeho ztráty jej NEBUDE možné změnit.</string> + <string name="database_will_be_encrypted_and_passphrase_stored">Databáze bude zašifrována a přístupová fráze bude uložena v úložišti klíčů.</string> + <string name="store_passphrase_securely">Přístupovou frázi uložte bezpečně, v případě její ztráty ji NEBUDE možné změnit.</string> <string name="file_with_path">Soubor: %s</string> <string name="database_passphrase_is_required">Pro otevření chatu je vyžadována přístupová fráze.</string> <string name="unknown_error">Neznámá chyba</string> @@ -308,8 +302,6 @@ <string name="restore_database">Obnovte zálohu databáze</string> <string name="restore_database_alert_desc">Po obnovení zálohy databáze zadejte předchozí frázi. Tuto akci nelze vrátit zpět.</string> <string name="chat_is_stopped_indication">Chat je zastaven</string> - <string name="chat_archive_header">Chat se archivuje</string> - <string name="delete_chat_archive_question">Smazat chat archiv\?</string> <string name="join_group_question">Připojit se ke skupině\?</string> <string name="join_group_button">Připojte se na</string> <string name="leave_group_button">Opustit</string> @@ -328,7 +320,7 @@ <string name="smp_server_test_secure_queue">Zabezpečit frontu</string> <string name="service_notifications">Okamžitá oznámení!</string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>V nastavení ji lze vypnout</b> - oznámení se budou zobrazovat pokud aplikace běží.]]></string> - <string name="turn_off_battery_optimization"><![CDATA[Pro použití, prosím <b>povolte pro SimpleX běh na pozadí</b> v dalším dialogu. Jinak budou oznámení vypnuta.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[<b>Povolte</b> v příštím dotazu okamžité přijímání notifikací.]]></string> <string name="periodic_notifications_desc">Aplikace pravidelně načítá nové zprávy - denně spotřebuje několik procent baterie. Aplikace nepoužívá push oznámení - data ze zařízení nejsou odesílána na servery.</string> <string name="enter_passphrase_notification_title">Je vyžadována přístupová fráze</string> <string name="enter_passphrase_notification_desc">Chcete-li dostávat oznámení, zadejte přístupovou frázi do databáze.</string> @@ -351,17 +343,15 @@ <string name="callstatus_calling">volání…</string> <string name="callstate_connected">připojen</string> <string name="callstate_ended">ukončen</string> - <string name="next_generation_of_private_messaging">Příští generace soukromé komunikace</string> - <string name="people_can_connect_only_via_links_you_share">Lidé se s vámi mohou spojit pouze prostřednictvím odkazu, který sdílíte.</string> + <string name="next_generation_of_private_messaging">Budoucnost soukromé komunikace</string> + <string name="people_can_connect_only_via_links_you_share">Rozhodněte, kdo se může připojit.</string> <string name="integrity_msg_bad_hash">špatný kontrolní součet zprávy</string> - <string name="chat_database_imported">Chat databáze importována</string> + <string name="chat_database_imported">Databáze chatu importována</string> <string name="new_passphrase">Nová přístupová fráze…</string> - <string name="save_passphrase_and_open_chat">Uložte frázi a otevřete chat</string> - <string name="chat_archive_section">CHAT ARCHIV</string> + <string name="save_passphrase_and_open_chat">Uložte přístupovou frázi a otevřete chat</string> <string name="no_contacts_selected">Nebyl vybrán žádný kontakt</string> - <string name="invite_prohibited_description">Snažíte se pozvat kontakt se kterým jste sdíleli inkognito profil, do skupiny ve které používáte svůj hlavní profil</string> + <string name="invite_prohibited_description">Snažíte se pozvat kontakt, se kterým jste sdíleli inkognito profil, do skupiny, ve které používáte svůj hlavní profil</string> <string name="info_row_group">Skupina</string> - <string name="network_options_revert">Vrátit</string> <string name="updating_settings_will_reconnect_client_to_all_servers">Aktualizací nastavení se klient znovu připojí ke všem serverům.</string> <string name="accept_feature_set_1_day">Nastavit 1 den</string> <string name="connection_error_auth">Chyba spojení (AUTH)</string> @@ -381,8 +371,8 @@ <string name="to_verify_compare">Chcete-li ověřit koncové šifrování u svého kontaktu, porovnejte (nebo naskenujte) kód na svých zařízeních.</string> <string name="your_settings">Vaše nastavení</string> <string name="your_simplex_contact_address">Vaše SimpleX adresa</string> - <string name="database_passphrase_and_export">Heslo databáze a export</string> - <string name="your_chat_profiles">Chat profily</string> + <string name="database_passphrase_and_export">Přístupová fráze k databázi a export</string> + <string name="your_chat_profiles">Vaše chat profily</string> <string name="chat_with_the_founder">Zaslat otázky a nápady</string> <string name="smp_servers_test_server">Test serveru</string> <string name="enter_one_ICE_server_per_line">Servery ICE (jeden na řádek)</string> @@ -394,7 +384,7 @@ <string name="delete_address">Smazat adresu</string> <string name="full_name__field">Celé jméno:</string> <string name="your_current_profile">Váš současný profil</string> - <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Pro zachování vašeho soukromí používá aplikace místo push oznámení <b>SimpleX službu na pozadí</b> - denně využije několik procent baterie.]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Pro zvýšení soukromí, <b>SimpleX běží na pozadí</b> místo používání oznámení.]]></string> <string name="periodic_notifications">Pravidelná oznámení</string> <string name="simplex_service_notification_title">SimpleX Chat služba</string> <string name="simplex_service_notification_text">Příjem zpráv…</string> @@ -422,9 +412,9 @@ <string name="image_saved">Obrázek uložen do galerie</string> <string name="icon_descr_file">Soubor</string> <string name="large_file">Velký soubor!</string> - <string name="file_will_be_received_when_contact_is_online">Soubor bude přijat, až bude váš kontakt online, vyčkejte prosím, nebo se podívejte později!</string> + <string name="file_will_be_received_when_contact_is_online">Soubor bude přijat, až bude váš kontakt online, vyčkejte prosím nebo se podívejte později!</string> <string name="file_saved">Soubor uložen</string> - <string name="file_not_found">Soubor nenalezen</string> + <string name="file_not_found">Soubor nebyl nalezen</string> <string name="voice_message_with_duration">Hlasová zpráva (%1$s)</string> <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Kontakt a všechny zprávy budou smazány - nelze to vzít zpět!</string> <string name="button_delete_contact">Smazat kontakt</string> @@ -440,7 +430,7 @@ <string name="edit_image">Upravit obrázek</string> <string name="delete_image">Smazat obrázek</string> <string name="callstatus_error">chyba volání</string> - <string name="opensource_protocol_and_code_anybody_can_run_servers">Protokol a kód s otevřeným zdrojovým kódem - servery může provozovat kdokoli.</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Servery může provozovat kdokoli.</string> <string name="create_your_profile">Vytvořte si svůj profil</string> <string name="make_private_connection">Vytvořte si soukromé připojení</string> <string name="encrypted_video_call">Videohovor šifrovaný e2e</string> @@ -456,29 +446,29 @@ <string name="integrity_msg_bad_id">špatné ID zprávy</string> <string name="integrity_msg_duplicate">duplicitní zpráva</string> <string name="alert_title_skipped_messages">Přeskočené zprávy</string> - <string name="privacy_and_security">Ochrana osobních údajů a zabezpečení</string> + <string name="privacy_and_security">Soukromí a zabezpečení</string> <string name="your_privacy">Vaše soukromí</string> <string name="protect_app_screen">Skrývat aplikaci</string> <string name="send_link_previews">Odesílat náhledy odkazů</string> <string name="full_backup">Zálohovat data aplikace</string> - <string name="confirm_new_passphrase">Potvrdit frázi…</string> + <string name="confirm_new_passphrase">Potvrdit novou přístupovou frázi…</string> <string name="error_with_info">Chyba: %s</string> <string name="leave_group_question">Opustit skupinu\?</string> <string name="icon_descr_group_inactive">Skupina je neaktivní</string> <string name="rcv_group_event_member_left">odešel</string> <string name="clear_contacts_selection_button">Vyčistit</string> <string name="switch_verb">Přepnout</string> - <string name="member_role_will_be_changed_with_notification">Role bude změněna na \"%s\". Všichni ve skupině budou informováni.</string> - <string name="error_removing_member">Chyba odebírání člena</string> - <string name="error_saving_group_profile">Chyba ukládání profilu skupiny</string> + <string name="member_role_will_be_changed_with_notification">Role bude změněna na "%s". Všichni ve skupině budou informováni.</string> + <string name="error_removing_member">Chyba při odebrání člena</string> + <string name="error_saving_group_profile">Chyba při ukládání profilu skupiny</string> <string name="network_option_seconds_label">vteřiny</string> <string name="incognito_info_allows">Umožňuje mít v jednom chat profilu mnoho anonymních spojení bez sdílení údajů mezi nimi.</string> <string name="incognito_info_share">Pokud s někým sdílíte inkognito profil, bude použit pro skupiny, do kterých vás pozve.</string> <string name="theme_system">Systémové</string> <string name="voice_messages">Hlasové zprávy</string> - <string name="both_you_and_your_contacts_can_delete">Vy i váš kontakt můžete nevratně mazat odeslané zprávy.</string> + <string name="both_you_and_your_contacts_can_delete">Vy i váš kontakt můžete nevratně mazat odeslané zprávy. (24 hodin)</string> <string name="ttl_m">%dm</string> - <string name="ttl_mth">%dmth</string> + <string name="ttl_mth">%dm</string> <string name="ttl_hours">%d hodin</string> <string name="ttl_h">%dh</string> <string name="ttl_d">%dd</string> @@ -487,7 +477,7 @@ <string name="v4_3_irreversible_message_deletion">Nevratné mazání zpráv</string> <string name="v4_3_improved_server_configuration">Vylepšena konfigurace serveru</string> <string name="v4_3_improved_privacy_and_security">Vylepšena ochrana soukromí a zabezpečení</string> - <string name="v4_3_improved_privacy_and_security_desc">Skryta obrazovka aplikace v posledních aplikacích.</string> + <string name="v4_3_improved_privacy_and_security_desc">Skrytí obrazovky aplikace v posledních aplikacích.</string> <string name="v4_4_disappearing_messages_desc">Odeslané zprávy se po uplynutí nastavené doby odstraní.</string> <string name="v4_4_live_messages">Živé zprávy</string> <string name="v4_4_live_messages_desc">Příjemci uvidí aktualizaci během psaní.</string> @@ -496,17 +486,17 @@ <string name="v4_4_french_interface_descr">Díky uživatelům - překládejte prostřednictvím Weblate!</string> <string name="v4_5_multiple_chat_profiles">Více chat profilů</string> <string name="v4_5_multiple_chat_profiles_descr">Různá jména, avataři a izolace přenosu.</string> - <string name="v4_5_message_draft">Návrh zpráv</string> + <string name="v4_5_message_draft">Návrh zprávy</string> <string name="v4_5_message_draft_descr">Zachování posledního návrhu zprávy s přílohami.</string> <string name="v4_5_transport_isolation">Izolace přenosu</string> - <string name="v4_5_transport_isolation_descr">Podle chat profilu (výchozí) nebo podle připojení (BETA).</string> - <string name="you_will_join_group">Připojíte se ke skupině, na kterou odkazuje tento odkaz, a spojíte se s jejími členy.</string> + <string name="v4_5_transport_isolation_descr">Podle profilu chatu (výchozí) nebo podle připojení (BETA).</string> + <string name="you_will_join_group">Připojtíte se ke všem členům skupiny.</string> <string name="connect_via_link_verb">Připojení</string> <string name="connected_to_server_to_receive_messages_from_contact">Jste připojeni k serveru, který se používá k přijímání zpráv od tohoto kontaktu.</string> <string name="trying_to_connect_to_server_to_receive_messages_with_error">Pokoušíte se připojit k serveru používaném pro příjem zpráv od tohoto kontaktu (chyba: %1$s).</string> - <string name="marked_deleted_description">označit jako smazáno</string> + <string name="marked_deleted_description">označeno jako smazáno</string> <string name="sending_files_not_yet_supported">Odesílání souborů zatím není podporováno</string> - <string name="receiving_files_not_yet_supported">Přijímání souborů zatím není podporováno</string> + <string name="receiving_files_not_yet_supported">přijímání souborů zatím není podporováno</string> <string name="sender_you_pronoun">ty</string> <string name="unknown_message_format">neznámý formát zprávy</string> <string name="invalid_message_format">neplatný formát zprávy</string> @@ -519,8 +509,8 @@ <string name="error_joining_group">Chyba připojování ke skupině</string> <string name="cannot_receive_file">Nelze přijmout soubor</string> <string name="sender_cancelled_file_transfer">Odesílatel zrušil přenos souboru.</string> - <string name="error_receiving_file">Chyba příjmu souboru</string> - <string name="error_creating_address">Chyba vytváření adresy</string> + <string name="error_receiving_file">Chyba při příjmu souboru</string> + <string name="error_creating_address">Chyba při vytváření adresy</string> <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Zkontrolujte, zda jste použili správný odkaz, nebo požádejte kontakt, aby vám poslal jiný.</string> <string name="connection_error_auth_desc">Pokud váš kontakt neodstranil připojení nebo tento odkaz již nebyl použit, může se jednat o chybu – nahlaste ji. \nChcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu na připojení a zkontrolujte, zda máte stabilní připojení k síti.</string> @@ -584,7 +574,7 @@ <string name="to_connect_via_link_title">Připojení prostřednictvím odkazu</string> <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Pokud jste dostali SimpleX Chat pozvánku, můžete ji otevřít v prohlížeči:</string> <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Pokud zvolíte odmítnutí, odesílatel NEBUDE upozorněn.</string> - <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 telefon: <b>Otevřete v mobilní aplikaci</b>, potom klikněte na <b>Připojit</b>.]]></string> + <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 telefon: <b>Otevřete v mobilní aplikaci</b>, potom v aplikaci klepněte na <b>Připojit</b>.]]></string> <string name="reject_contact_button">Odmítnout</string> <string name="clear_chat_button">Smazat chat</string> <string name="clear_chat_menu_action">Vyčistit</string> @@ -614,12 +604,12 @@ <string name="paste_button">Vložit</string> <string name="this_string_is_not_a_connection_link">Tento řetězec není odkazem na připojení!</string> <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Můžete se také připojit kliknutím na odkaz. Pokud se otevře v prohlížeči, klikněte na tlačítko <b>Otevřít v mobilní aplikaci</b>.]]></string> - <string name="is_not_verified">%s není ověřeno</string> + <string name="is_not_verified">%s neověřen</string> <string name="how_to_use_simplex_chat">Návod k použití</string> <string name="markdown_help">Nápověda k markdown</string> <string name="smp_servers_save">Uložit servery</string> <string name="markdown_in_messages">Markdown ve zprávách</string> - <string name="smp_servers_test_servers">Testovat servery</string> + <string name="smp_servers_test_servers">Testovací servery</string> <string name="smp_servers_preset_server">Přednastavený server</string> <string name="smp_servers_your_server">Váš server</string> <string name="smp_servers_your_server_address">Adresa vašeho serveru</string> @@ -633,33 +623,29 @@ <string name="ensure_ICE_server_address_are_correct_format_and_unique">Ujistěte se, že adresy serverů WebRTC ICE jsou ve správném formátu, oddělené na řádcích a nejsou duplicitní.</string> <string name="save_servers_button">Uložit</string> <string name="network_and_servers">Síť a servery</string> - <string name="update_onion_hosts_settings_question">Aktualizovat nastavení hostitelů .onion\?</string> <string name="network_use_onion_hosts">Použít hostitele .onion</string> <string name="network_use_onion_hosts_prefer">Když bude dostupný</string> <string name="network_use_onion_hosts_required">Povinné</string> <string name="network_use_onion_hosts_prefer_desc">Onion hostitelé budou použiti, pokud jsou k dispozici.</string> <string name="network_use_onion_hosts_no_desc">Onion hostitelé nebudou použiti.</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hostitelé budou použiti, pokud jsou k dispozici.</string> - <string name="network_use_onion_hosts_required_desc_in_alert">Pro připojení budou vyžadováni Onion hostitelé.</string> <string name="network_session_mode_transport_isolation">Izolace přenosu</string> <string name="network_session_mode_user_description"><![CDATA[A separate TCP connection (and SOCKS credential) will be used <b>for each chat profile you have in the app</b>.]]></string> - <string name="network_session_mode_entity_description">Oddělit TCP připojení (a SOCKS pověření) bude použito <b>pro všechny kontakty a členy skupin</b>. -\n<b>Upozornění</b>: Pokud máte mnoho připojení, může být spotřeba baterie a provoz podstatně vyšší a některá připojení mohou selhat.</string> + <string name="network_session_mode_entity_description"><![CDATA[Oddělit TCP připojení (a SOCKS pověření) bude použito <b>pro všechny kontakty a členy skupin</b>. \n<b>Upozornění</b>: Pokud máte mnoho připojení, může být spotřeba baterie a provoz podstatně vyšší a některá připojení mohou selhat.]]></string> <string name="appearance_settings">Vzhled</string> <string name="app_version_title">Verze aplikace</string> <string name="app_version_name">Verze aplikace: v%s</string> <string name="core_version">Verze jádra: v%s</string> <string name="delete_address__question">Smazat adresu\?</string> <string name="all_your_contacts_will_remain_connected">Všechny vaše kontakty zůstanou připojeny.</string> - <string name="display_name__field">Zobrazované jméno:</string> + <string name="display_name__field">Profilové jméno:</string> <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Váš profil je uložen v zařízení a je sdílen pouze s vašimi kontakty. SimpleX servery váš profil vidět nemohou.</string> <string name="save_preferences_question">Uložit předvolby\?</string> <string name="save_and_notify_contact">Uložit a upozornit kontakt</string> <string name="save_and_notify_contacts">Uložit a upozornit kontakty</string> - <string name="you_control_your_chat">Kontrolujete chat!</string> + <string name="you_control_your_chat">Kontrolujete konverzaci!</string> <string name="we_do_not_store_contacts_or_messages_on_servers">Na serverech neukládáme žádné vaše kontakty ani zprávy (po doručení).</string> <string name="your_profile_is_stored_on_your_device">Váš profil, kontakty a doručené zprávy jsou uloženy ve vašem zařízení.</string> - <string name="display_name">Zobrazované jméno</string> + <string name="display_name">Zadejte vaše jméno:</string> <string name="create_profile_button">Vytvořit</string> <string name="how_to_use_markdown">Jak používat markdown</string> <string name="you_can_use_markdown_to_format_messages__prompt">K formátování zpráv můžete použít markdown:</string> @@ -676,26 +662,23 @@ <string name="callstate_received_confirmation">obdržel potvrzení…</string> <string name="callstate_connecting">připojování…</string> <string name="privacy_redefined">Nové vymezení soukromí</string> - <string name="first_platform_without_user_ids">1. platforma bez jakýchkoliv uživatelských identifikátorů – soukromá již od návrhu.</string> - <string name="immune_to_spam_and_abuse">Odolná vůči spamu a zneužití</string> - <string name="to_protect_privacy_simplex_has_ids_for_queues">K ochraně soukromí, místo uživatelských ID užívaných všemi ostatními platformami, SimpleX používá identifikátory pro fronty zpráv, zvlášť pro každý z vašich kontaktů.</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[Mnoho lidí se ptá: <i>když SimpleX nemá žádný identifikátor uživatelů, jak může doručovat zprávy\?</i>]]></string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Vy určujete, přes které servery <b>přijímat</b> zprávy, vaše kontakty – servery, které používáte k zasílání zpráv.]]></string> - <string name="read_more_in_github">Další informace najdete v našem repozitáři na GitHubu.</string> + <string name="first_platform_without_user_ids">Bez uživatelských identifikátorů</string> + <string name="immune_to_spam_and_abuse">Odolná vůči spamu</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">K ochraně soukromí, SimpleX používá ID pro každý z vašich kontaktů.</string> <string name="read_more_in_github_with_link"><![CDATA[Další informace najdete v našem <font color="#0088ff">úložišti GitHub</font>.]]></string> <string name="use_chat">Použijte chat</string> - <string name="onboarding_notifications_mode_subtitle">Lze změnit později v nastavení.</string> + <string name="onboarding_notifications_mode_subtitle">Jak ovlivňuje baterii</string> <string name="onboarding_notifications_mode_off">Když aplikace běží</string> <string name="onboarding_notifications_mode_service">Okamžité</string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Nejlepší pro baterii</b>. Budete přijímat oznámení pouze když aplikace běží (žádná služba na pozadí).]]></string> - <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Dobré pro baterii</b>. Služba na pozadí bude kontrolovat každých 10 minut. Můžete zmeškat hovory nebo naléhavé zprávy.]]></string> - <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Využívá více baterie</b>! Služba na pozadí je spuštěna vždy - oznámení se zobrazí, jakmile jsou zprávy k dispozici.]]></string> - <string name="paste_the_link_you_received">Vložení přijatého odkazu</string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Dobré pro baterii</b>. Apka bude kontrolovat zprávy každých 10 minut. Můžete zmeškat volání nebo naléhavé zprávy.]]></string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Využívá více baterie</b>! Apka stále běží na pozadí - oznámení se zobrazí okamžitě.]]></string> + <string name="paste_the_link_you_received">Vložte přijatý odkaz</string> <string name="incoming_video_call">Příchozí videohovor</string> <string name="incoming_audio_call">Příchozí zvukový hovor</string> <string name="contact_wants_to_connect_via_call">%1$s se s vámi chce spojit prostřednictvím</string> <string name="video_call_no_encryption">videohovoru (nešifrovaného e2e).</string> - <string name="audio_call_no_encryption">zvukový hovor (nešifrováno e2e)</string> + <string name="audio_call_no_encryption">zvukový hovor (nešifrován e2e)</string> <string name="reject">Odmítnout</string> <string name="your_calls">Vaše hovory</string> <string name="always_use_relay">Spojení přes relé</string> @@ -713,7 +696,7 @@ <string name="icon_descr_call_connecting">Spojování hovoru</string> <string name="icon_descr_call_ended">Skončený hovor</string> <string name="answer_call">Přijmout hovor</string> - <string name="integrity_msg_skipped">%1$d přeskočená zpráva (zprávy)</string> + <string name="integrity_msg_skipped">%1$d přeskočených zpráv</string> <string name="alert_text_skipped_messages_it_can_happen_when">Může se to stát, když: \n1. Zprávy vypršely v odesílajícím klientovi po 2 dnech nebo na serveru po 30 dnech. \n2. Dešifrování zprávy se nezdařilo, protože vy nebo váš kontakt jste použili starou zálohu databáze. @@ -721,13 +704,13 @@ <string name="settings_section_title_you">VY</string> <string name="settings_section_title_support">PODPOŘIT SIMPLEX CHAT</string> <string name="settings_developer_tools">Nástroje pro vývojáře</string> - <string name="settings_section_title_incognito">inkognito mód</string> - <string name="your_chat_database">Chat databáze</string> + <string name="settings_section_title_incognito">Inkognito mód</string> + <string name="your_chat_database">Vaše chat databáze</string> <string name="run_chat_section">SPUSTIT CHAT</string> <string name="chat_is_running">Chat je spuštěn</string> <string name="chat_is_stopped">Chat je zastaven</string> - <string name="chat_database_section">CHAT DATABÁZE</string> - <string name="database_passphrase">přístupová fráze databáze</string> + <string name="chat_database_section">DATABÁZE CHATU</string> + <string name="database_passphrase">přístupová fráze k databázi</string> <string name="new_database_archive">Archiv nové databáze</string> <string name="old_database_archive">Archiv staré databáze</string> <string name="error_starting_chat">Chyba spouštění chatu</string> @@ -737,44 +720,44 @@ <string name="set_password_to_export">Nastavte přístupovou frázi pro export</string> <string name="set_password_to_export_desc">Databáze je šifrována pomocí náhodné přístupové fráze. Před exportem ji změňte.</string> <string name="error_stopping_chat">Chyba zastavování chatu</string> - <string name="import_database_question">Importovat chat databázi\?</string> + <string name="import_database_question">Importovat databázi chatu?</string> <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Vaše aktuální chat databáze bude SMAZÁNA a NAHRAZENA importovanou databází. \nTuto akci nelze vzít zpět - vaše profily, kontakty, zprávy a soubory budou nenávratně ztraceny.</string> - <string name="error_deleting_database">Chyba mazání chat databáze</string> - <string name="error_importing_database">Chyba importu chat databáze</string> - <string name="chat_database_deleted">Chat databáze odstraněna</string> + <string name="error_deleting_database">Chyba při mazání databáze chatu</string> + <string name="error_importing_database">Chyba při importu databáze chatu</string> + <string name="chat_database_deleted">Databáze chatu odstraněna</string> <string name="delete_files_and_media_for_all_users">Odstranit soubory všech chat profilů</string> <string name="delete_files_and_media_all">Odstranit všechny soubory</string> <string name="delete_files_and_media_desc">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.</string> <string name="no_received_app_files">Žádné přijaté ani odeslané soubory</string> - <string name="total_files_count_and_size">%d soubor(ů) s celkovou velikostí %s</string> + <string name="total_files_count_and_size">%d soubor(y) s celkovou velikostí %s</string> <string name="chat_item_ttl_none">nikdy</string> - <string name="chat_item_ttl_seconds">%s vteřin(y)</string> + <string name="chat_item_ttl_seconds">%s sekund(y)</string> <string name="messages_section_title">Zprávy</string> <string name="messages_section_description">Toto nastavení se vztahuje na zprávy ve vašem aktuálním chat profilu.</string> <string name="delete_messages_after">Smazat zprávy po</string> <string name="enable_automatic_deletion_question">Povolit automatické mazání zpráv\?</string> <string name="enable_automatic_deletion_message">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.</string> <string name="error_changing_message_deletion">Chyba změny nastavení</string> - <string name="save_passphrase_in_keychain">Uložit frázi do úložiště klíčů</string> - <string name="database_encrypted">Databáze šifrována!</string> - <string name="error_encrypting_database">Chyba šifrování databáze</string> + <string name="save_passphrase_in_keychain">Uložit přístupovou frázi do úložiště klíčů</string> + <string name="database_encrypted">Databáze zašifrována!</string> + <string name="error_encrypting_database">Chyba při šifrování databáze</string> <string name="encrypt_database">Šifrovat</string> <string name="encrypted_with_random_passphrase">Databáze je šifrována pomocí náhodné přístupové fráze, musíte ji změnit.</string> <string name="keychain_allows_to_receive_ntfs">K bezpečnému uložení přístupové fráze se použije úložiště klíčů Android, po restartování aplikace nebo změně přístupové fráze - umožní přijímání oznámení.</string> <string name="you_have_to_enter_passphrase_every_time">Musíte zadat přístupovou frázi při každém spuštění aplikace - není uložena v zařízení.</string> - <string name="encrypt_database_question">Šifrovat databázi\?</string> - <string name="change_database_passphrase_question">Změnit přístupovou frázi databáze\?</string> - <string name="database_will_be_encrypted">Databáze bude šifrována.</string> - <string name="database_encryption_will_be_updated">Přístupová fráze databáze bude aktualizována a uložena do úložiště klíčů.</string> - <string name="database_passphrase_will_be_updated">Přístupová fráze databáze bude aktualizována.</string> + <string name="encrypt_database_question">Zašifrovat databázi?</string> + <string name="change_database_passphrase_question">Změnit přístupovou frázi k databázi?</string> + <string name="database_will_be_encrypted">Databáze bude zašifrována.</string> + <string name="database_encryption_will_be_updated">Přístupová fráze k databázi bude aktualizována a uložena do úložiště klíčů.</string> + <string name="database_passphrase_will_be_updated">Přístupová fráze k databázi bude aktualizována.</string> <string name="store_passphrase_securely_without_recover">Uložte prosím bezpečně přístupovou frázi, pokud ji ztratíte, NEBUDE možné přistupovat k chatu.</string> <string name="wrong_passphrase">Špatná přístupová fráze k databázi</string> - <string name="encrypted_database">Šifrovaná databáze</string> + <string name="encrypted_database">Zašifrovaná databáze</string> <string name="database_error">Chyba databáze</string> <string name="keychain_error">Chyba klíčenky</string> - <string name="passphrase_is_different">Přístupová fráze databáze se liší od té uložené v klíčence.</string> - <string name="cannot_access_keychain">Nelze získat přístup ke klíčence pro uložení přístupová fráze k databázi.</string> + <string name="passphrase_is_different">Přístupová fráze k databázi se liší od té uložené v klíčence.</string> + <string name="cannot_access_keychain">Nelze získat přístup ke klíčence pro uložení přístupové fráze k databázi.</string> <string name="unknown_database_error_with_info">Neznámá chyba databáze: %s</string> <string name="wrong_passphrase_title">Špatná přístupová fráze!</string> <string name="enter_correct_passphrase">Zadejte správnou přístupovou frázi.</string> @@ -782,18 +765,15 @@ <string name="database_backup_can_be_restored">Pokus o změnu přístupové fráze databáze nebyl dokončen.</string> <string name="restore_database_alert_title">Obnovit zálohu databáze\?</string> <string name="restore_database_alert_confirm">Obnovit</string> - <string name="database_restore_error">Chyba obnovení databáze</string> - <string name="restore_passphrase_not_found_desc">Heslo nebylo v klíčence nalezeno, zadejte jej prosím ručně. K této situaci mohlo dojít, pokud jste obnovili data aplikace pomocí zálohovacího nástroje. Pokud tomu tak není, obraťte se na vývojáře.</string> + <string name="database_restore_error">Chyba při obnovování databáze</string> + <string name="restore_passphrase_not_found_desc">Přístupová fráze nebyla v klíčence nalezena, zadejte ji prosím ručně. K této situaci mohlo dojít, pokud jste obnovili data aplikace pomocí zálohovacího nástroje. Pokud tomu tak není, kontaktujte prosím vývojáře.</string> <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Chat můžete spustit v Nastavení / Databáze nebo restartováním aplikace.</string> - <string name="save_archive">Uložit archiv</string> - <string name="delete_archive">Smazat archiv</string> <string name="group_invitation_item_description">pozvánka do skupiny %1$s</string> - <string name="archive_created_on_ts">Vytvořeno dne %1$s</string> <string name="you_are_invited_to_group_join_to_connect_with_group_members">Jste zváni do skupiny. Připojte se k členům skupiny.</string> <string name="join_group_incognito_button">Připojit se inkognito</string> <string name="joining_group">Připojit ke skupině</string> <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Připojili jste se k této skupině. Připojení k pozvání člena skupiny.</string> - <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Přestanete dostávat zprávy z této skupiny. Chat historie bude zachována.</string> + <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Přestanete dostávat zprávy z této skupiny. Historie chatu bude zachována.</string> <string name="alert_title_group_invitation_expired">Platnost pozvánky vypršela!</string> <string name="alert_message_group_invitation_expired">Pozvánka do skupiny již není platná, byla odstraněna odesílatelem.</string> <string name="alert_message_no_group">Tato skupina již neexistuje.</string> @@ -808,7 +788,7 @@ <string name="rcv_group_event_member_added">pozval %1$s</string> <string name="rcv_group_event_member_connected">připojen</string> <string name="rcv_group_event_changed_member_role">změnil roli %s na %s</string> - <string name="rcv_group_event_changed_your_role">změnil svou roli na %s</string> + <string name="rcv_group_event_changed_your_role">změnil vaši roli na %s</string> <string name="rcv_group_event_member_deleted">odstraněn %1$s</string> <string name="rcv_group_event_user_deleted">odstranil vás</string> <string name="rcv_group_event_group_deleted">skupina odstraněna</string> @@ -828,8 +808,8 @@ <string name="group_member_status_invited">pozván</string> <string name="group_member_status_introduced">představen (zaveden)</string> <string name="group_member_status_intro_invitation">připojení (pozvánka na představení)</string> - <string name="group_member_status_accepted">připojen (přijat)</string> - <string name="group_member_status_announced">připojen (oznámen)</string> + <string name="group_member_status_accepted">připojení (přijato)</string> + <string name="group_member_status_announced">připojení (oznámeno)</string> <string name="group_member_status_connected">připojen</string> <string name="group_member_status_complete">komplet</string> <string name="group_member_status_creator">tvůrce</string> @@ -842,7 +822,7 @@ <string name="icon_descr_contact_checked">Zkontrolované kontakty</string> <string name="num_contacts_selected">%d kontakt(y) vybrán(y)</string> <string name="button_add_members">Pozvat členy</string> - <string name="group_info_section_title_num_members">%1$s MEMBERS</string> + <string name="group_info_section_title_num_members">%1$s ČLENŮ</string> <string name="group_info_member_you">vy: %1$s</string> <string name="button_delete_group">Smazat skupinu</string> <string name="delete_group_question">Smazat skupinu\?</string> @@ -854,8 +834,8 @@ <string name="create_group_link">Vytvořit odkaz skupiny</string> <string name="delete_link">Smazat odkaz</string> <string name="you_can_share_group_link_anybody_will_be_able_to_connect">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.</string> - <string name="error_creating_link_for_group">Chyba vytváření odkazu skupiny</string> - <string name="error_deleting_link_for_group">Chyba odstraňování odkazu skupiny</string> + <string name="error_creating_link_for_group">Chyba při vytváření odkazu skupiny</string> + <string name="error_deleting_link_for_group">Chyba při odstraňování odkazu skupiny</string> <string name="only_group_owners_can_change_prefs">Předvolby skupiny mohou měnit pouze vlastníci skupiny.</string> <string name="section_title_for_console">PRO KONSOLE</string> <string name="info_row_local_name">Místní název</string> @@ -866,13 +846,13 @@ <string name="role_in_group">Role</string> <string name="change_role">Změnit roli</string> <string name="change_verb">Změnit</string> - <string name="member_role_will_be_changed_with_invitation">Role bude změněna na \"%s\". Člen obdrží novou pozvánku.</string> - <string name="error_changing_role">Chyba změny role</string> + <string name="member_role_will_be_changed_with_invitation">Role bude změněna na "%s". Člen obdrží novou pozvánku.</string> + <string name="error_changing_role">Chyba při změně role</string> <string name="conn_level_desc_direct">přímo</string> - <string name="sending_via">Odesíláno přez</string> + <string name="sending_via">Odesíláno přes</string> <string name="network_status">Stav sítě</string> <string name="switch_receiving_address">Přepínač přijímací adresy</string> - <string name="group_is_decentralized">Skupina je plně decentralizovaná - viditelná pouze pro členy.</string> + <string name="group_is_decentralized">Plně decentralizovaná - viditelná pouze členům.</string> <string name="save_group_profile">Uložení profilu skupiny</string> <string name="network_options_reset_to_defaults">Obnovit výchozí nastavení</string> <string name="network_option_tcp_connection_timeout">Časový limit připojení TCP</string> @@ -881,7 +861,7 @@ <string name="network_option_ping_count">Počet PING</string> <string name="network_option_enable_tcp_keep_alive">Povolit TCP keep-alive</string> <string name="update_network_settings_confirmation">Aktualizovat</string> - <string name="users_delete_question">Smazat chat profil\?</string> + <string name="users_delete_question">Smazat profil chatu?</string> <string name="users_delete_profile_for">Smazat chat profil pro</string> <string name="users_delete_with_connections">Profil a připojení k serveru</string> <string name="users_delete_data_only">Pouze místní data profilu</string> @@ -905,7 +885,7 @@ <string name="prohibit_sending_voice_messages">Hlasové zprávy zakázány.</string> <string name="only_you_can_send_disappearing">Pouze vy můžete odesílat mizící zprávy.</string> <string name="disappearing_prohibited_in_this_chat">Mizící zprávy jsou v tomto chatu zakázány.</string> - <string name="only_your_contact_can_delete">Nevratně mazat zprávy může pouze váš kontakt (vy je můžete označit ke smazání).</string> + <string name="only_your_contact_can_delete">Nevratně mazat zprávy může pouze váš kontakt (vy je můžete označit ke smazání). (24 hodin)</string> <string name="both_you_and_your_contact_can_send_voice">Hlasové zprávy můžete posílat vy i váš kontakt.</string> <string name="only_you_can_send_voice">Hlasové zprávy můžete posílat pouze vy.</string> <string name="only_your_contact_can_send_voice">Hlasové zprávy může odesílat pouze váš kontakt.</string> @@ -913,22 +893,22 @@ <string name="prohibit_sending_disappearing">Posílání mizících zpráv zakázáno.</string> <string name="prohibit_message_deletion">Nevratné mazání odeslaných zpráv zakázáno.</string> <string name="prohibit_sending_voice">Hlasové zprávy zakázány.</string> - <string name="group_members_can_send_disappearing">Členové skupiny mohou posílat mizící zprávy.</string> - <string name="disappearing_messages_are_prohibited">Mizící zprávy jsou v této skupině zakázány.</string> - <string name="group_members_can_send_dms">Členové skupiny mohou posílat přímé zprávy.</string> - <string name="direct_messages_are_prohibited_in_chat">Přímé zprávy mezi členy jsou v této skupině zakázány.</string> - <string name="group_members_can_delete">Členové skupiny mohou nevratně mazat odeslané zprávy.</string> - <string name="message_deletion_prohibited_in_chat">Nevratné mazání zpráv je v této skupině zakázáno.</string> - <string name="group_members_can_send_voice">Členové skupiny mohou posílat hlasové zprávy.</string> - <string name="voice_messages_are_prohibited">Hlasové zprávy jsou v této skupině zakázány.</string> + <string name="group_members_can_send_disappearing">Členové mohou posílat mizící zprávy.</string> + <string name="disappearing_messages_are_prohibited">Mizící zprávy jsou zakázány.</string> + <string name="group_members_can_send_dms">Členové mohou posílat přímé zprávy.</string> + <string name="direct_messages_are_prohibited_in_group">Přímé zprávy mezi členy jsou v této skupině zakázány.</string> + <string name="group_members_can_delete">Členové mohou nevratně mazat odeslané zprávy. (24 hodin)</string> + <string name="message_deletion_prohibited_in_chat">Nevratné mazání zpráv je zakázáno.</string> + <string name="group_members_can_send_voice">Členové mohou posílat hlasové zprávy.</string> + <string name="voice_messages_are_prohibited">Hlasové zprávy jsou zakázány.</string> <string name="delete_after">Smazat za</string> <string name="ttl_month">%d měsíc</string> <string name="ttl_months">%d měsíců</string> <string name="ttl_day">%d den</string> - <string name="ttl_days">%d dnů</string> + <string name="ttl_days">%d dní</string> <string name="ttl_week">%d týden</string> <string name="ttl_weeks">%d týdnů</string> - <string name="ttl_w">%dw</string> + <string name="ttl_w">%dt</string> <string name="feature_offered_item">nabízeno %s</string> <string name="feature_cancelled_item">zrušeno %s</string> <string name="whats_new">Co je nového</string> @@ -963,7 +943,6 @@ <string name="language_system">Systém</string> <string name="smp_save_servers_question">Uložit servery\?</string> <string name="dont_show_again">Znovu neukazuj</string> - <string name="cant_delete_user_profile">Nemohu smazat uživatelský profil!</string> <string name="button_add_welcome_message">Přidat uvítací zprávu</string> <string name="v4_6_chinese_spanish_interface">Čínské a Španělské rozhranní</string> <string name="v4_6_audio_video_calls">Hlasové a video hovory</string> @@ -982,7 +961,7 @@ <string name="v4_6_reduced_battery_usage_descr">Další vylepšení již brzy!</string> <string name="v4_6_group_moderation_descr">Nyní mohou správci: \n- mazat zprávy členů. -\n- zakázat členy (role \"pozorovatel\")</string> +\n- zakázat členy (role "pozorovatel")</string> <string name="save_profile_password">Uložit heslo profilu</string> <string name="user_mute">Ztlumit</string> <string name="v4_6_hidden_chat_profiles_descr">Chraňte své chat profily heslem!</string> @@ -994,12 +973,10 @@ <string name="v4_6_audio_video_calls_descr">Podpora bluetooth a další vylepšení.</string> <string name="tap_to_activate_profile">Klepnutím aktivujete profil.</string> <string name="v4_6_chinese_spanish_interface_descr">Díky uživatelům - překládejte prostřednictvím Weblate!</string> - <string name="should_be_at_least_one_profile">Měl by tam být alespoň jeden uživatelský profil.</string> <string name="button_welcome_message">Uvítací zpráva</string> <string name="group_welcome_title">Uvítací zpráva</string> <string name="user_unmute">Zrušit ztlumení</string> <string name="to_reveal_profile_enter_password">Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce Chat profily.</string> - <string name="should_be_at_least_one_visible_profile">Měl by tam být alespoň jeden viditelný uživatelský profil.</string> <string name="you_will_still_receive_calls_and_ntfs">Stále budete přijímat volání a upozornění od umlčených profilů pokud budou aktivní.</string> <string name="you_can_hide_or_mute_user_profile">Můžete skrýt nebo ztlumit uživatelský profil - Podržte pro menu.</string> <string name="user_unhide">Odkrýt</string> @@ -1061,7 +1038,7 @@ <string name="la_no_app_password">Bez hesla aplikace</string> <string name="la_could_not_be_verified">Nemohli jste být ověřeni; Zkuste to prosím znovu.</string> <string name="la_minutes">%d minut</string> - <string name="la_seconds">%d vteřin</string> + <string name="la_seconds">%d sekund</string> <string name="la_immediately">Ihned</string> <string name="la_please_remember_to_store_password">Zapamatujte si jej nebo bezpečně uložte - neexistuje způsob, jak obnovit ztracené heslo!</string> <string name="lock_not_enabled">Zámek SimpleX není povolen!</string> @@ -1072,7 +1049,7 @@ <string name="enable_lock">Povolit zámek</string> <string name="lock_after">Zamknout po</string> <string name="lock_mode">Režim zámku</string> - <string name="authentication_cancelled">Autentizace zrušena</string> + <string name="authentication_cancelled">Ověření zrušeno</string> <string name="confirm_passcode">Potvrdit heslo</string> <string name="incorrect_passcode">Nesprávné heslo</string> <string name="new_passcode">Nové heslo</string> @@ -1086,11 +1063,11 @@ <string name="decryption_error">Chyba dešifrování</string> <string name="alert_title_msg_bad_hash">Špatný hash zprávy</string> <string name="alert_title_msg_bad_id">Špatné ID zprávy</string> - <string name="alert_text_msg_bad_hash">Hash předchozí zprávy se liší.</string> + <string name="alert_text_msg_bad_hash">Kontrolní součet předchozí zprávy se liší.</string> <string name="alert_text_msg_bad_id">ID další zprávy je nesprávné (menší nebo rovno předchozí). \nMůže se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitováno.</string> - <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d zprávy se nepodařilo dešifrovat.</string> - <string name="alert_text_decryption_error_too_many_skipped">%1$d zprývy přeskočeny.</string> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d zpráv se nepodařilo dešifrovat.</string> + <string name="alert_text_decryption_error_too_many_skipped">%1$d zpráv přeskočeno.</string> <string name="alert_text_fragment_please_report_to_developers">Nahlaste to prosím vývojářům.</string> <string name="alert_text_fragment_encryption_out_of_sync_old_database">Může se to stát, když vy nebo vaše připojení použijete starou zálohu databáze.</string> <string name="revoke_file__message">Soubor bude smazán ze serverů.</string> @@ -1137,7 +1114,7 @@ <string name="error_loading_details">Chyba načítání podrobností</string> <string name="info_menu">Info</string> <string name="search_verb">Hledat</string> - <string name="auth_open_chat_profiles">Otevřít chat profily</string> + <string name="auth_open_chat_profiles">Změnit chat profily</string> <string name="edit_history">Historie</string> <string name="received_message">Přijatá zpráva</string> <string name="sent_message">Poslaná zpráva</string> @@ -1164,7 +1141,7 @@ <string name="share_text_sent_at">Posláno: %s</string> <string name="share_text_disappears_at">Zmizí: %s</string> <string name="share_text_moderated_at">Upraveno: %s</string> - <string name="current_version_timestamp">%s (aktuální)</string> + <string name="current_version_timestamp">%s (aktuálně)</string> <string name="dark_theme">Tmavý motiv</string> <string name="import_theme">Import motivu</string> <string name="theme_simplex">SimpleX</string> @@ -1177,13 +1154,13 @@ <string name="message_reactions_prohibited_in_this_chat">Reakce na zprávy jsou v tomto chatu zakázány.</string> <string name="prohibit_message_reactions">Zakázat reakce na zprávy.</string> <string name="prohibit_message_reactions_group">Zakázat reakce na zprávy.</string> - <string name="group_members_can_add_message_reactions">Členové skupin mohou přidávat reakce na zprávy.</string> - <string name="message_reactions_are_prohibited">Reakce na zprávy jsou v této skupině zakázány.</string> + <string name="group_members_can_add_message_reactions">Členové mohou přidávat reakce na zprávy.</string> + <string name="message_reactions_are_prohibited">Reakce na zprávy jsou zakázány.</string> <string name="custom_time_unit_months">měsíců</string> <string name="learn_more">Zjistit více</string> <string name="share_with_contacts">Sdílet s kontakty</string> <string name="group_welcome_preview">Náhled</string> - <string name="opening_database">Otvírání databáze…</string> + <string name="opening_database">Otevírání databáze…</string> <string name="error_setting_address">Chyba nastavení adresy</string> <string name="scan_qr_to_connect_to_contact">Pro připojení může váš kontakt naskenovat QR kód, nebo použít odkaz v aplikaci.</string> <string name="you_can_accept_or_reject_connection">Když někdo požádá o připojení, můžete žádost přijmout nebo odmítnout.</string> @@ -1215,9 +1192,7 @@ <string name="custom_time_unit_minutes">minut</string> <string name="custom_time_unit_seconds">vteřin</string> <string name="whats_new_thanks_to_users_contribute_weblate">Díky uživatelům - překládejte prostřednictvím Weblate!</string> - <string name="v5_1_better_messages_descr">- 5 minutové hlasové zprávy. -\n- vlastní čas mizení. -\n- historie úprav.</string> + <string name="v5_1_better_messages_descr">- 5 minutové hlasové zprávy.\n- volitelný čas mizení zpráv.\n- historie úprav.</string> <string name="custom_time_unit_days">dní</string> <string name="custom_time_unit_hours">hodin</string> <string name="custom_time_unit_weeks">týdnů</string> @@ -1251,7 +1226,7 @@ <string name="you_wont_lose_your_contacts_if_delete_address">Pokud později adresu odstraníte, o kontakty nepřijdete.</string> <string name="app_passcode_replaced_with_self_destruct">Přístupový kód aplikace je nahrazen sebedestrukčním přístupovým heslem.</string> <string name="item_info_no_text">žádný text</string> - <string name="non_fatal_errors_occured_during_import">Během importu došlo k nezávažným chybám - podrobnosti naleznete v chat konzoli.</string> + <string name="non_fatal_errors_occured_during_import">Během importu došlo k nezávažným chybám:</string> <string name="shutdown_alert_question">Vypnout\?</string> <string name="shutdown_alert_desc">Oznámení přestanou fungovat až do nového spuštění aplikace</string> <string name="settings_shutdown">Vypnout</string> @@ -1269,9 +1244,7 @@ <string name="fix_connection_not_supported_by_group_member">Opravit nepodporované členem skupiny</string> <string name="fix_connection_not_supported_by_contact">Opravit nepodporované kontaktem</string> <string name="v5_2_fix_encryption">Zachovat vaše připojení</string> - <string name="v5_2_more_things_descr">- více stabilní doručovaní zpráv. -\n- o trochu lepší skupiny. -\n- a více!</string> + <string name="v5_2_more_things_descr">- stabilnější doručování zpráv.\n- trochu lepší skupiny.\n- a více!</string> <string name="snd_conn_event_ratchet_sync_agreed">šifrování povoleno pro %s</string> <string name="snd_conn_event_ratchet_sync_required">vyžadováno opětovné vyjednávání šifrování pro %s</string> <string name="receipts_contacts_override_disabled">Odesílání potvrzení o doručení je vypnuto pro %d kontakty.</string> @@ -1290,9 +1263,9 @@ <string name="unfavorite_chat">Odoblíbit</string> <string name="abort_switch_receiving_address">Přerušit změnu adresy</string> <string name="allow_to_send_files">Povolit odesílání souborů a médii.</string> - <string name="group_members_can_send_files">Členové skupiny mohou posílat soubory a média.</string> + <string name="group_members_can_send_files">Členové mohou posílat soubory a média.</string> <string name="only_owners_can_enable_files_and_media">Pouze majitelé skupiny mohou povolit soubory a média.</string> - <string name="files_are_prohibited_in_group">Soubory a média jsou zakázány v této skupině.</string> + <string name="files_are_prohibited_in_group">Soubory a média jsou zakázány.</string> <string name="receipts_contacts_disable_keep_overrides">Vypnout (zachovat přepsání)</string> <string name="receipts_contacts_enable_for_all">Povolit pro všechny</string> <string name="receipts_contacts_enable_keep_overrides">Povolit (zachovat přepisování)</string> @@ -1368,22 +1341,22 @@ <string name="recipient_colon_delivery_status">%s: %s</string> <string name="rcv_group_event_2_members_connected">%s a %s připojen</string> <string name="rcv_group_event_3_members_connected">%s, %s a %s připojen</string> - <string name="rcv_group_event_n_members_connected">%s, %s a %d další členové připojeni</string> + <string name="rcv_group_event_n_members_connected">%s, %s a %d jiných členů připojeno</string> <string name="privacy_message_draft">Rozepsáno</string> <string name="privacy_show_last_messages">Zobrazit poslední zprávy</string> <string name="send_receipts_disabled_alert_msg">Tato skupina má více než %1$d členů, doručenky nejsou odeslány.</string> <string name="in_developing_desc">Tato funkce zatím není podporována. Vyzkoušejte další vydání.</string> - <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Databáze bude zašifrována a heslo bude uloženo v klíčence.</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Databáze bude zašifrována a přístupová fráze bude uloženo v klíčence.</string> <string name="socks_proxy_setting_limitations"><![CDATA[<b>Všimněte si prosím</b>: relé zpráv a souborů jsou připojeny prostřednictvím proxy SOCKS. Volání a odesílání náhledů odkazů používá přímé připojení.]]></string> <string name="encrypt_local_files">Šifrovat místní soubory</string> <string name="you_can_change_it_later">Náhodná přístupová fráze je uložena v nastavení jako prostý text. \nMůžete ji později změnit.</string> - <string name="database_encryption_will_be_updated_in_settings">Heslo pro šifrování databáze bude aktualizováno a uloženo v klíčence.</string> + <string name="database_encryption_will_be_updated_in_settings">Přístupová fráze k databáze bude aktualizována a uložena v klíčence.</string> <string name="remove_passphrase_from_settings">Odebrat přístupovou frázi z nastavení\?</string> <string name="use_random_passphrase">Použít náhodnou přístupovou frázi</string> <string name="save_passphrase_in_settings">Uložit přístupovou frázi v nastavení</string> - <string name="setup_database_passphrase">Nastavení přístupové fráze databáze</string> - <string name="set_database_passphrase">Nastavit přístupovou frázi databáze</string> + <string name="setup_database_passphrase">Nastavení přístupové fráze k databázi</string> + <string name="set_database_passphrase">Nastavit přístupovou frázi k databázi</string> <string name="open_database_folder">Otevřít složku databáze</string> <string name="passphrase_will_be_saved_in_settings">Přístupová fráze bude uložena v nastavení jako prostý text až ji změníte nebo po restartu aplikace.</string> <string name="settings_is_storing_in_clear_text">Přístupová fráze je uložena v nastavení jako prostý text.</string> @@ -1400,8 +1373,1011 @@ <string name="v5_3_simpler_incognito_mode">Zjednodušený režim inkognito</string> <string name="v5_3_new_desktop_app_descr">Vytvořit nový profil v desktopové aplikaci. 💻</string> <string name="v5_3_simpler_incognito_mode_descr">Změnit inkognito při připojování.</string> - <string name="v5_3_discover_join_groups_descr">- připojit k adresáři skupin (BETA)! -\n- doručenky (až 20 členů). -\n- rychlejší a stabilnější.</string> + <string name="v5_3_discover_join_groups_descr">- připojení k adresáři skupin (BETA)!\n- doručenky (až 20 členů).\n- rychlejší a stabilnější.</string> <string name="member_contact_send_direct_message">odeslat přímou zprávu</string> -</resources> \ No newline at end of file + <string name="rcv_direct_event_contact_deleted">smazaný kontakt</string> + <string name="error_alert_title">Chyba</string> + <string name="create_another_profile_button">Vytvořit profil</string> + <string name="connected_desktop">Připojený počítač</string> + <string name="group_members_2">%s a %s</string> + <string name="multicast_connect_automatically">Připojit se automaticky</string> + <string name="desktop_address">Adresa počítače</string> + <string name="connect_plan_join_your_group">Připojit se do skupiny?</string> + <string name="connect_plan_group_already_exists">Skupina již existuje!</string> + <string name="rcv_group_event_1_member_connected">%s připojen</string> + <string name="this_device">Toto zařízení</string> + <string name="desktop_device">Počítač</string> + <string name="this_device_name">Jméno tohoto zařízení</string> + <string name="found_desktop">Počítač nalezen</string> + <string name="delete_messages__question">Smazat %d zpráv?</string> + <string name="remove_member_button">Odebrat člena</string> + <string name="connected_mobile">Připojený telefon</string> + <string name="button_remove_member_question">Odebrat člena?</string> + <string name="connect_plan_open_group">Otevřít skupinu</string> + <string name="desktop_connection_terminated">Spojení ukončeno</string> + <string name="error">Chyba</string> + <string name="connect_to_desktop">Připojit se k počítači</string> + <string name="disconnect_remote_host">Odpojit</string> + <string name="invalid_name">Neplatné jméno!</string> + <string name="group_member_role_author">Autor</string> + <string name="connected_to_mobile">Připojený k telefonu</string> + <string name="bad_desktop_address">Špatná adresa počítače</string> + <string name="devices">Zařízení</string> + <string name="disconnect_desktop_question">Odpojit počítač?</string> + <string name="create_group_button">Vytvořit skupinu</string> + <string name="session_code">Kód relace</string> + <string name="paste_desktop_address">Vložit adres počítače</string> + <string name="blocked_item_description">Blokovaný</string> + <string name="v5_4_better_groups">Lepší skupiny</string> + <string name="expand_verb">Rozbalit</string> + <string name="v5_4_block_group_members">Blokovat členy skupin</string> + <string name="connect_plan_repeat_connection_request">Opakovat žádost o připojení?</string> + <string name="encryption_renegotiation_error">Chyba opětovného vyjednávání šifrování</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Již se připojujete k <b>%1$s</b>.]]></string> + <string name="v5_4_incognito_groups_descr">Vytvořit skupinu pomocí náhodného profilu.</string> + <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Již se připojujete ke skupině prostřednictvím tohoto odkazu.</string> + <string name="refresh_qr_code">Obnovit</string> + <string name="new_mobile_device">Nový telefon</string> + <string name="only_one_device_can_work_at_the_same_time">Pouze jedno zařízení může pracovat současně</string> + <string name="you_can_make_address_visible_via_settings">Můžete ji svým SimpleX kontaktům zviditelnit v Nastavení.</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Již se připojujete ke skupině <b>%1$s</b>.]]></string> + <string name="v5_4_link_mobile_desktop">Propojit mobilní a stolní aplikace! 🔗</string> + <string name="connect_plan_this_is_your_own_one_time_link">To je váš vlastní jednorázový odkaz!</string> + <string name="marked_deleted_items_description">%d zpráv označeno jako smazané</string> + <string name="v5_4_link_mobile_desktop_descr">Díky kvantům odolnému protokolu zabezpečení.</string> + <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Otevřete <i>Použijte z PC</i> v aplikaci telefonu a naskenujte QR kód.]]></string> + <string name="connect_plan_already_connecting">Již připojuji!</string> + <string name="v5_4_block_group_members_descr">Skrýt nežádoucí zprávy.</string> + <string name="desktop_incompatible_version">Nekompatibilní verze</string> + <string name="new_desktop"><![CDATA[<i>(nový)</i>]]></string> + <string name="unlink_desktop_question">Odpojit PC?</string> + <string name="linked_desktop_options">Možnosti připojeného PC</string> + <string name="video_decoding_exception_desc">Video nelze dekódovat. +\nProsím vyzkoušejte jiné video, nebo kontaktujte vývojáře.</string> + <string name="random_port">Náhodný</string> + <string name="linked_desktops">Připojená PC</string> + <string name="discover_on_network">Objevte přes lokální síť</string> + <string name="rcv_group_and_other_events">a %d jiných událostí</string> + <string name="connect_plan_connect_via_link">Připojit přes odkaz?</string> + <string name="v5_4_incognito_groups">Inkognito skupiny</string> + <string name="connect_plan_already_joining_the_group">Už se ke skupině připojujete!</string> + <string name="group_members_n">%s, %s a %d členů</string> + <string name="moderated_items_description">%1$d zpráv moderoval %2$s</string> + <string name="unblock_member_button">Odblokovat člena</string> + <string name="remote_host_was_disconnected_toast"><![CDATA[Telefon <b>%s</b> byl odpojen]]></string> + <string name="waiting_for_desktop">Čekání na PC…</string> + <string name="v5_4_better_groups_descr">Rychlejší připojování a spolehlivější zprávy.</string> + <string name="error_showing_content">chyba zobrazení obsahu</string> + <string name="connect_plan_connect_to_yourself">Připojit se k sobě?</string> + <string name="linked_mobiles">Připojené telefony</string> + <string name="connected_to_desktop">Připojeno k PC</string> + <string name="contact_tap_to_connect">Klepněte na Připojit se</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Již jste ve skupině <b>%1$s</b>.]]></string> + <string name="connect_plan_this_is_your_own_simplex_address">To je vaše vlastní SimpleX adresa!</string> + <string name="loading_remote_file_title">Načítám soubor</string> + <string name="connecting_to_desktop">Připojuji k PC</string> + <string name="alert_text_encryption_renegotiation_failed">Opětovné vyjednávání šifrování selhalo.</string> + <string name="error_showing_message">chyba zobrazení zprávy</string> + <string name="desktop_devices">PC</string> + <string name="correct_name_to">Upravit jméno na %s?</string> + <string name="not_compatible">Nekompatibilní!</string> + <string name="waiting_for_mobile_to_connect">Čekání na připojení telefonu:</string> + <string name="link_a_mobile">Připojit telefon</string> + <string name="connect_with_contact_name_question">Připojit s %1$s?</string> + <string name="block_member_confirmation">Blokovat</string> + <string name="unblock_member_question">Odblokovat člena?</string> + <string name="blocked_items_description">%d zpráv blokováno</string> + <string name="open_port_in_firewall_desc">Chcete-li povolit telefonu připojení k PC, otevřete tento port ve vašem firewallu, pokud je povolen</string> + <string name="block_member_button">Blokovat člena</string> + <string name="settings_section_title_use_from_desktop">Použít z PC</string> + <string name="connect_plan_repeat_join_request">Opakovat požadavek na připojení?</string> + <string name="create_chat_profile">Vytvořit chat profil</string> + <string name="app_was_crashed">Zobrazit havarované</string> + <string name="delete_and_notify_contact">Smazat a informovat kontakt</string> + <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">Již se připojujete přes tento jednorázový odkaz!</string> + <string name="this_device_version"><![CDATA[<i>(toto zařízení v%s)</i>]]></string> + <string name="unlink_desktop">Odpojit</string> + <string name="unblock_member_desc">Zprávy od %s budou zobrazeny!</string> + <string name="this_device_name_shared_with_mobile">Název zařízení bude sdílen s připojeným mobilním klientem.</string> + <string name="error_sending_message_contact_invitation">Chyba odesílání pozvánky</string> + <string name="verify_code_on_mobile">Ověřit kód v telefonu</string> + <string name="open_port_in_firewall_title">Otevřít port ve firewallu</string> + <string name="enter_this_device_name">Zadejte jméno tohoto zařízení…</string> + <string name="non_content_uri_alert_text">Sdíleli jste neplatnou cestu souboru. Nahlaste problém vývojářům aplikace.</string> + <string name="disconnect_remote_hosts">Odpojit mobilní telefony</string> + <string name="block_member_question">Blokovat člena?</string> + <string name="rcv_group_events_count">%d událostí skupiny</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[To je váš odkaz pro skupinu <b>%1$s</b>!]]></string> + <string name="verify_code_with_desktop">Ověřit kód s PC</string> + <string name="scan_qr_code_from_desktop">Skenovat QR kód z PC</string> + <string name="unblock_member_confirmation">Odblokovat</string> + <string name="multicast_discoverable_via_local_network">Objevitelný přes lokální síť</string> + <string name="v5_4_more_things_descr">- volitelné oznámení odstraněným kontaktům.\n- profilová jména s mezeramy.\n- a více!</string> + <string name="non_content_uri_alert_title">Neplatná cesta souboru</string> + <string name="connect_plan_you_have_already_requested_connection_via_this_address">Již jste požádali o spojení přes tuto adresu!</string> + <string name="terminal_always_visible">Zobrazit konzoli v novém okně</string> + <string name="scan_from_mobile">Skenování z telefonu</string> + <string name="verify_connections">Ověřit připojení</string> + <string name="loading_remote_file_desc">Prosím počkejte, než bude soubor načten z propojeného telefonu</string> + <string name="block_member_desc">Všechny nové zprávy od %s budou skryté!</string> + <string name="desktop_app_version_is_incompatible">Verze aplikace na PC %s není kompatibilní s touto aplikací.</string> + <string name="verify_connection">Ověřit připojení</string> + <string name="no_connected_mobile">Žádný připojený telefon</string> + <string name="loading_chats">Nahrávám chat..</string> + <string name="search_or_paste_simplex_link">Hledat nebo vložit SimpleX odkaz</string> + <string name="camera_not_available">Kamera není k dispozici</string> + <string name="enable_camera_access">Povolit fotoaparát</string> + <string name="tap_to_scan">Klepnutím skenovat</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Vytvořit skupinu</b>: vytvořit novou skupinu.]]></string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Přidat kontakt</b>: vytvořit novou pozvánku nebo připojit přes odkaz, který jste obdrželi.]]></string> + <string name="add_contact_tab">Přidat kontakt</string> + <string name="new_chat">Nový chat</string> + <string name="creating_link">Vytvořit odkaz…</string> + <string name="keep_invitation_link">Ponechat</string> + <string name="keep_unused_invitation_question">Ponechat nepoužitou pozvánku?</string> + <string name="or_scan_qr_code">Nebo skenovat QR kód</string> + <string name="or_show_this_qr_code">Nebo ukázat tento kód</string> + <string name="you_can_view_invitation_link_again">Pozvánku můžete znovu zobrazit v detailech připojení.</string> + <string name="invalid_qr_code">Neplatný QR kód</string> + <string name="retry_verb">Zkusit znovu</string> + <string name="share_this_1_time_link">Sdílejte tento jedorázový odkaz</string> + <string name="tap_to_paste_link">Klepnutím vložte odkaz</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">Kód který jste naskenovali, není SimpleX QR odkaz.</string> + <string name="the_text_you_pasted_is_not_a_link">Text který jste vložili, není SimpleX odkaz.</string> + <string name="recent_history">Viditelná historie</string> + <string name="disable_sending_recent_history">Nezasílat historii novým členům.</string> + <string name="la_app_passcode">Heslo aplikace</string> + <string name="start_chat_question">Začít chat?</string> + <string name="enable_sending_recent_history">Odešlete až 100 posledních zpráv novým členům.</string> + <string name="recent_history_is_not_sent_to_new_members">Historie není odeslána novým členům.</string> + <string name="recent_history_is_sent_to_new_members">Až 100 poslední zprávy je odesláno novým členům.</string> + <string name="chat_is_stopped_you_should_transfer_database">Chat je zastaven. Pokud jste již použili tuto databázi na jiném zařízení, měli byste ji před zahájením chatu převést zpět.</string> + <string name="possible_slow_function_desc">Provedení funkce trvá příliš dlouho: %1$d vteřin: %2$s</string> + <string name="v5_5_join_group_conversation">Připojit se ke skupině</string> + <string name="blocked_by_admin_item_description">blokováno adminem</string> + <string name="blocked_by_admin_items_description">%d zpráv blokováno správcem</string> + <string name="error_creating_message">Chyba vytváření zprávy</string> + <string name="error_deleting_note_folder">Chyba odstranění soukromých poznámek</string> + <string name="clear_note_folder_question">Smazat soukromé poznámky?</string> + <string name="clear_note_folder_warning">Všechny zprávy budou smazány - nemůže být zvráceno!</string> + <string name="developer_options_section">Možnosti vývojáře</string> + <string name="rcv_group_event_member_blocked">blokováno %s</string> + <string name="profile_update_event_contact_name_changed">kontakt %1$s změnen na %2$s</string> + <string name="info_row_created_at">Vytvořeno v</string> + <string name="block_for_all">Blok všem</string> + <string name="block_for_all_question">Blokovat člena všem?</string> + <string name="error_blocking_member_for_all">Chyba blokování člena všem</string> + <string name="v5_5_message_delivery">Vylepšené doručovaní zpráv</string> + <string name="v5_5_new_interface_languages">Maďarské a Turecké uživatelské rozhraní</string> + <string name="remote_host_error_bad_state"><![CDATA[Připojení k mobilu <b>%s</b> je ve špatném stavu]]></string> + <string name="remote_ctrl_error_inactive">PC je neaktivní</string> + <string name="remote_ctrl_error_bad_state">Připojení k PC je ve špatném stavu</string> + <string name="remote_ctrl_error_busy">PC je zaneprázdněno</string> + <string name="remote_ctrl_error_disconnected">PC byl odpojeno</string> + <string name="agent_critical_error_title">Kritická chyba</string> + <string name="remote_ctrl_error_bad_version">PC má nepodporovanou verzi. Ujistěte se, že používáte stejnou verzi na obou zařízeních</string> + <string name="remote_ctrl_error_bad_invitation">PC má chybný kód pozvánky</string> + <string name="failed_to_create_user_invalid_title">Neplatné jméno!</string> + <string name="database_migration_in_progress">Databáze migrace běží. +\nMůže to trvat několik minut.</string> + <string name="profile_update_event_member_name_changed">člen %1$s změněn na %2$s</string> + <string name="member_info_member_blocked">blokováno</string> + <string name="member_blocked_by_admin">Blokováno adminem</string> + <string name="share_text_created_at">Vytvořen v: %s</string> + <string name="message_too_large">Zpráva příliš velká</string> + <string name="remote_host_disconnected_from"><![CDATA[Odpojeno z mobilu <b>%s</b> z důvodu: %s]]></string> + <string name="remote_host_was_disconnected_title">Spojení zastaveno</string> + <string name="remote_ctrl_was_disconnected_title">Spojení zastaveno</string> + <string name="remote_ctrl_disconnected_with_reason">Odpojeno z důvodu: %s</string> + <string name="agent_internal_error_title">Interní chyba</string> + <string name="v5_5_simpler_connect_ui_descr">Vyhledávání přijímá pozvánky.</string> + <string name="show_slow_api_calls">Zobrazit pomalé API volání</string> + <string name="profile_update_event_removed_address">odstraněna kontaktní adresa</string> + <string name="past_member_vName">Vložte člena %1$s</string> + <string name="v5_5_simpler_connect_ui">Vložte odkaz na připojení!</string> + <string name="v5_5_join_group_conversation_descr">Nejnovější historie a vylepšený directory bot.</string> + <string name="v5_5_private_notes">Soukromé poznámky</string> + <string name="remote_host_error_inactive"><![CDATA[Mobilní <b>%s</b> je neaktivní]]></string> + <string name="remote_host_error_busy"><![CDATA[Mobilní <b>%s</b> je zaneprázdněn]]></string> + <string name="remote_host_error_bad_version"><![CDATA[Mobile <b>%s</b> má nepodporovanou verzi. Ujistěte se, že používáte stejnou verzi na obou zařízeních]]></string> + <string name="agent_internal_error_desc">Nahlaste to prosím vývojářům: +\n%s</string> + <string name="show_internal_errors">Zobrazit interní chyby</string> + <string name="agent_critical_error_desc">Nahlaste to prosím vývojářům: +\n%s +\n +\nDoporučuje se restartovat aplikaci.</string> + <string name="restart_chat_button">Restartovat chat</string> + <string name="remote_host_error_disconnected"><![CDATA[Mobilní <b>%s</b> byl odpojen]]></string> + <string name="unable_to_open_browser_title">Chyba otevření prohlížeče</string> + <string name="profile_update_event_removed_picture">odstraněn profilový obrázek</string> + <string name="profile_update_event_set_new_address">nastavit novou kontaktní adresu</string> + <string name="profile_update_event_set_new_picture">nastavit nový profilový obrázek</string> + <string name="saved_message_title">Uložené zprávy</string> + <string name="possible_slow_function_title">Pomalá funkce</string> + <string name="note_folder_local_display_name">Soukromé poznámky</string> + <string name="call_service_notification_audio_call">Volání</string> + <string name="call_service_notification_end_call">Konec volání</string> + <string name="remote_host_error_missing"><![CDATA[Mobilní <b>%s</b> chybí]]></string> + <string name="snd_group_event_member_blocked">zablokovali jste %s</string> + <string name="snd_group_event_member_unblocked">odblokovaly jste %s</string> + <string name="group_member_status_unknown_short">neznámý</string> + <string name="v5_5_message_delivery_descr">Se sníženou spotřebou baterie.</string> + <string name="group_member_status_unknown">neznámý stav</string> + <string name="unblock_for_all_question">Odblokovat člena všem?</string> + <string name="unblock_for_all">Odblokovat pro všechny</string> + <string name="welcome_message_is_too_long">Uvítací zpráva je příliš dlouhá</string> + <string name="remote_host_error_timeout"><![CDATA[Vypršel čas při připojování k mobilnímu <b>%s</b>]]></string> + <string name="call_service_notification_video_call">Videohovor</string> + <string name="profile_update_event_updated_profile">profil aktualizován</string> + <string name="unable_to_open_browser_desc">Výchozí webový prohlížeč je vyžadován pro volání. Nakonfigurujte výchozí prohlížeč v systému a sdílejte s vývojáři více informací.</string> + <string name="rcv_group_event_member_unblocked">odblokované %s</string> + <string name="v5_5_private_notes_descr">S šifrovanými soubory a médii.</string> + <string name="remote_ctrl_error_timeout">Vypršel čas při připojování k desktopu</string> + <string name="failed_to_create_user_invalid_desc">Zobrazené jméno není platné. Prosím vyberte jiné.</string> + <string name="migrate_to_device_chat_migrated">Chat přesunut!</string> + <string name="migrate_to_device_apply_onion">Použít</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Zvolte <i>Přesunot z jiného zařízení</i> na novém zařízení a skenujte QR kód.]]></string> + <string name="migrate_to_device_confirm_network_settings">Potvrdit nastavení sítě</string> + <string name="migrate_from_device_confirm_upload">Potvrďte nahrání</string> + <string name="migrate_from_device_archiving_database">Archivuji databázi</string> + <string name="migrate_from_device_cancel_migration">Zrušit migraci</string> + <string name="migrate_from_device_check_connection_and_try_again">Zkontrolujte připojení k internetu a zkuste to znovu</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Pro migraci potvrďte, že si pamatujete heslo databáze.</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Varování</b>: archiv bude smazán.]]></string> + <string name="migrate_from_device_archive_and_upload">Archivovat a nahrát</string> + <string name="v5_6_safer_groups_descr">Správci mohou blokovat člen pro všechny.</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Všechny vaše kontakty, konverzace a soubory budou bezpečně šifrovány a rozdělené nahrány na zvolené XFTP relé.</string> + <string name="v5_6_app_data_migration">Migrace dat aplikace</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Čtěte prosím</b>: Jako ochrana zabezpečení, se použitím stejné databáze na dvou zařízeních rozbije dešifrování zpráv z vašich připojení.]]></string> + <string name="auth_open_migration_to_another_device">Otevřít obrazovku přesunu</string> + <string name="set_passphrase">Nastavit přístupovou frázi</string> + <string name="migrate_to_device_migrating">Přesouvám</string> + <string name="migrate_to_device_confirm_network_settings_footer">Prosím potvrďte, že nastavení sítě pro toto zařízení jsou správná.</string> + <string name="migrate_from_device_error_exporting_archive">Chyba při exportu chat databáze</string> + <string name="migrate_from_device_database_init">Příprava nahrávání</string> + <string name="migrate_from_device_repeat_upload">Opakovat nahrávání</string> + <string name="migrate_from_device_upload_failed">Nahrání neúspěšné</string> + <string name="migrate_from_device_try_again">Můžete zkusit znovu.</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Upozornění: zahájení chatu na více zařízeních není podporováno a způsobí selhání doručování zpráv</string> + <string name="migrate_from_device_migration_complete">Přesun kompletní</string> + <string name="v5_6_app_data_migration_descr">Migrovat na jiné zařízení pomocí QR kódu.</string> + <string name="v5_6_picture_in_picture_calls">Volání obraz v obraze</string> + <string name="v5_6_safer_groups">Bezpečnější skupiny</string> + <string name="v5_6_picture_in_picture_calls_descr">Používat aplikaci při volání.</string> + <string name="migrate_to_device_downloading_archive">Stáhnuji archiv</string> + <string name="migrate_to_device_downloading_details">Stáhuji podrobnosti o odkazu</string> + <string name="invalid_file_link">Neplatný odkaz</string> + <string name="or_paste_archive_link">Nebo vložte odkaz archivu</string> + <string name="paste_archive_link">Vložit odkaz archivu</string> + <string name="migrate_to_device_database_init">Příprava stahování</string> + <string name="migrate_to_device_bytes_downloaded">%s staženo</string> + <string name="migrate_to_device_import_failed">Import selhal</string> + <string name="migrate_to_device_importing_archive">Importuji archiv</string> + <string name="migrate_to_device_try_again">Můžete zkusit znovu.</string> + <string name="migrate_to_device_error_downloading_archive">Chyba stahování archivu</string> + <string name="migrate_from_device_error_saving_settings">Chyba ukládání nastavení</string> + <string name="migrate_from_device_error_uploading_archive">Chyba nahrávání archivu</string> + <string name="migrate_from_device_exported_file_doesnt_exist">Exportovaný soubor neexistuje</string> + <string name="migrate_to_device_file_delete_or_link_invalid">Soubor byl odstraněn nebo je odkaz neplatný</string> + <string name="migrate_to_device_finalize_migration">Dokončit migraci na jiném zařízení.</string> + <string name="migrate_from_device_title">Migrovat zařízení</string> + <string name="migrate_from_device_to_another_device">Migrovat na jiné zařízení</string> + <string name="migrate_from_device_error_deleting_database">Chyba mazání databáze</string> + <string name="migrate_from_device_chat_should_be_stopped">Chcete-li pokračovat, zastavte chat.</string> + <string name="migrate_from_device_stopping_chat">Zastavuji chat</string> + <string name="migrate_from_device_creating_archive_link">Vytvořit odkaz archivu</string> + <string name="migrate_from_device_bytes_uploaded">%s nahráno</string> + <string name="migrate_from_device_uploading_archive">Nahrávám archiv</string> + <string name="migrate_from_device_finalize_migration">Dokončit migraci</string> + <string name="migrate_from_device_or_share_this_file_link">Nebo bezpečně sdílejte tento odkaz na soubor</string> + <string name="migrate_from_device_start_chat">Začít chat</string> + <string name="migrate_from_device_verify_database_passphrase">Ověřit přístupovou frázi databáze</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[<b>nesmíte</b> použít stejnou databázi na dvou zařízeních.]]></string> + <string name="migrate_from_device_verify_passphrase">Ověřit přístupovou frázi</string> + <string name="migrate_from_device_error_verifying_passphrase">Chyba ověření přístupové fráze:</string> + <string name="migrate_from_device_delete_database_from_device">Odstranit databázi z tohoto zařízení</string> + <string name="migrate_to_device_download_failed">Stažení selhalo</string> + <string name="v5_6_quantum_resistant_encryption_descr">Povolit v přímém chatu (BETA)!</string> + <string name="migrate_to_device_enter_passphrase">Zadejte přístupovou frázi</string> + <string name="migrate_from_another_device">Migrovat z jiného zařízení</string> + <string name="migrate_to_device_title">Migrovat zde</string> + <string name="migrate_to_device_repeat_download">Opakovat stahování</string> + <string name="migrate_to_device_repeat_import">Opakovat import</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="settings_section_title_network_connection">Síťové připojení</string> + <string name="prohibit_sending_simplex_links">Zákaz odesílání SimpleX odkazů</string> + <string name="audio_device_earpiece">Sluchátka</string> + <string name="audio_device_wired_headphones">Sluchátka</string> + <string name="audio_device_speaker">Reproduktor</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="v5_7_forward">Přeposlat a uložit zprávy</string> + <string name="v5_7_quantum_resistant_encryption_descr">Bude povoleno v přímém chatu!</string> + <string name="v5_7_new_interface_languages">Litevské UI</string> + <string name="v5_7_network_descr">Spolehlivější síťové připojení.</string> + <string name="allow_to_send_simplex_links">Povolit odesílat SimpleX odkazy.</string> + <string name="saved_description">uloženo</string> + <string name="saved_from_description">Uloženo z %s</string> + <string name="saved_chat_item_info_tab">Uloženo</string> + <string name="forwarded_chat_item_info_tab">Přeposláno</string> + <string name="saved_from_chat_item_info_title">Uloženo z</string> + <string name="forwarded_from_chat_item_info_title">Přeposláno z</string> + <string name="recipients_can_not_see_who_message_from">Příjemce nevidí, od koho zpráva je.</string> + <string name="forward_chat_item">Přeposlat</string> + <string name="download_file">Stáhnout</string> + <string name="forward_message">Přeposlat zprávu…</string> + <string name="forwarded_description">přeposláno</string> + <string name="e2ee_info_no_pq_short">Tento chat je chráněn koncovým šifrováním.</string> + <string name="e2ee_info_pq_short">Tento chat je chráněn kvantům odolným koncovým šifrováním.</string> + <string name="e2ee_info_pq"><![CDATA[Zprávy, soubory a hovory jsou chráněny <b>kvantům odolným koncovým šifrováním</b> s dokonalým dopředným utajením, odpíráním a obnovou po vloupání.]]></string> + <string name="network_type_cellular">Mobilní</string> + <string name="network_type_no_network_connection">Žádné připojení k síti</string> + <string name="network_type_other">Ostatní</string> + <string name="network_type_ethernet">Kabelový ethernet</string> + <string name="error_showing_desktop_notification">Chyba zobrazení oznámení, kontaktujte vývojáře.</string> + <string name="files_and_media_not_allowed">Soubory a média nejsou povoleny</string> + <string name="simplex_links_not_allowed">SimpleX odkazy jsou zakázány</string> + <string name="voice_messages_not_allowed">Hlasové zprávy nejsou povoleny</string> + <string name="conn_event_enabled_pq">kvantům odolné e2e šifrování</string> + <string name="conn_event_disabled_pq">standardní koncové šifrování</string> + <string name="feature_roles_admins">správci</string> + <string name="feature_roles_all_members">všichni členové</string> + <string name="feature_roles_owners">majitelé</string> + <string name="settings_section_title_profile_images">Obrázky profilů</string> + <string name="network_option_rcv_concurrency">Souběžné přijímání</string> + <string name="simplex_links">SimpleX odkazy</string> + <string name="feature_enabled_for">Povoleno pro</string> + <string name="group_members_can_send_simplex_links">Členové mohou odesílat SimpleX odkazy.</string> + <string name="v5_7_call_sounds">Zvuky v hovoru</string> + <string name="v5_7_forward_descr">Zdroje zpráv zůstávají důvěrné.</string> + <string name="v5_7_network">Správa sítě</string> + <string name="v5_7_shape_profile_images">Tvar obrázku profilů</string> + <string name="v5_7_shape_profile_images_descr">Čtverec, kruh, nebo něco mezi.</string> + <string name="v5_7_call_sounds_descr">Při spojování audio a video hovorů.</string> + <string name="permissions_grant_in_settings">Udělit v nastavení</string> + <string name="permissions_open_settings">Otevřít nastavení</string> + <string name="permissions_find_in_settings_and_grant">Najděte toto oprávnění v nastavení Android a povolte ho ručně.</string> + <string name="permissions_record_audio">Mikrofon</string> + <string name="permissions_required">Udělit oprávnění</string> + <string name="permissions_grant">Udělit oprávnění pro volání</string> + <string name="v5_6_quantum_resistant_encryption">Kvantům odolné šifrování</string> + <string name="permissions_camera">Kamera</string> + <string name="permissions_camera_and_record_audio">Kamera a mikrofon</string> + <string name="simplex_links_are_prohibited_in_group">SimpleX odkazy jsou zakázány.</string> + <string name="e2ee_info_no_pq"><![CDATA[Zprávy, soubory a hovory jsou chráněny <b>koncovým</b> šifrováním s dokonalým dopředným utajením, odmítnutím a obnovením po vloupání.]]></string> + <string name="wallpaper_advanced_settings">Pokročilé nastavení</string> + <string name="chat_theme_apply_to_all_modes">Všechny barevné režimy</string> + <string name="snd_error_quota">Překročená kapacita - příjemci neobdrží dříve poslané zprávy.</string> + <string name="network_smp_proxy_mode_always">Vždy</string> + <string name="chat_theme_apply_to_mode">Použít na</string> + <string name="v5_8_safe_files_descr">Potvrdit soubory z neznámých serverů.</string> + <string name="chat_theme_apply_to_dark_mode">Tmavý mód</string> + <string name="theme_destination_app_theme">Téma aplikace</string> + <string name="network_smp_proxy_mode_always_description">Vždy užít soukromé směrování.</string> + <string name="network_smp_proxy_fallback_allow_downgrade">Povolit downgrade</string> + <string name="copy_error">Kopírovat chybu</string> + <string name="settings_section_title_chat_colors">Barvy chatu</string> + <string name="settings_section_title_chat_theme">Téma chatu</string> + <string name="color_primary_variant2">Další zbarvení 2</string> + <string name="theme_black">Černé</string> + <string name="color_mode">Mód barvy</string> + <string name="color_mode_dark">Tmavé</string> + <string name="dark_mode_colors">Mód tmavých barev</string> + <string name="message_queue_info">Informace o frontě zpráv</string> + <string name="chat_theme_apply_to_light_mode">Světlý mód</string> + <string name="v5_8_chat_themes_descr">Upravtesi svůj chat, aby vypadal jinak!</string> + <string name="snd_error_relay">Chyba cílového serveru: %1$s</string> + <string name="ci_status_other_error">Chyba: %1$s</string> + <string name="snd_error_proxy_relay">Předávací server: %1$s +\nChyba cílového serveru: %2$s</string> + <string name="snd_error_proxy">Předávací server: %1$s +\nChyba: %2$s</string> + <string name="message_delivery_warning_title">Upozornění doručování zpráv</string> + <string name="snd_error_expired">Problémy se sítí - zpráva vypršela po mnoha pokusech o odeslání.</string> + <string name="network_smp_proxy_mode_private_routing">Soukromé směrování</string> + <string name="file_error_no_file">Soubor nebyl nalezen - s největší pravděpodobností byl soubor odstraněn nebo zrušen.</string> + <string name="file_error_relay">Chyba souboru serveru: %1$s</string> + <string name="cannot_share_message_alert_title">Nelze odeslat zprávu</string> + <string name="file_error">Chyba souboru</string> + <string name="network_smp_proxy_mode_never_description">NEpoužívat soukromé směrování.</string> + <string name="update_network_smp_proxy_fallback_question">Záložní směrování zpráv</string> + <string name="update_network_smp_proxy_mode_question">Režim přeposílání zpráv</string> + <string name="network_smp_proxy_mode_never">Nikdy</string> + <string name="error_initializing_web_view">Chyba inicializace WebView. Aktualizujte systém na novou verzi. Prosím kontaktujte vývojáře. +\nChyba: %s</string> + <string name="protect_ip_address">Ochrana IP adresy</string> + <string name="info_row_file_status">Status souboru</string> + <string name="info_row_message_status">Status zprávy</string> + <string name="share_text_file_status">Status souboru: %s</string> + <string name="share_text_message_status">Stav zprávy: %s</string> + <string name="message_queue_info_none">žádné</string> + <string name="wallpaper_scale_fill">Vyplnit</string> + <string name="wallpaper_scale_repeat">Opakovat</string> + <string name="chat_theme_reset_to_user_theme">Obnovit uživatelské téma</string> + <string name="chat_theme_reset_to_app_theme">Obnovit téma aplikace</string> + <string name="v5_8_safe_files">Bezpečné přijímání souborů</string> + <string name="v5_8_chat_themes">Nové motivy chatu</string> + <string name="v5_8_private_routing">Soukromé směrování zpráv 🚀</string> + <string name="v5_8_private_routing_descr">Chraňte vaši IP adresu před relé zpráv vašich kontaktů.\nPovolte v nastavení *Síť & servery*.</string> + <string name="v5_8_message_delivery">Vylepšené doručování zpráv</string> + <string name="v5_8_persian_ui">Perské UI</string> + <string name="remote_ctrl_connection_stopped_desc">Prosím zkontrolujte, že mobil a desktop jsou připojeny ke stejné místní síti, a že stolní firewall umožňuje připojení. +\nProsím sdělte jakékoli další problémy vývojářům.</string> + <string name="network_smp_proxy_fallback_prohibit">Ne</string> + <string name="network_smp_proxy_fallback_prohibit_description">NEposílejte zprávy přímo, i když váš nebo cílový server nepodporuje soukromé směrování.</string> + <string name="settings_section_title_files">SOUBORY</string> + <string name="settings_section_title_private_message_routing">SOUKROMÉ SMĚROVÁNÍ ZPRÁV</string> + <string name="settings_section_title_user_theme">Téma profilu</string> + <string name="color_received_quote">Přijata odpověď</string> + <string name="reset_single_color">Obnovit barvu</string> + <string name="wallpaper_preview_hello_alice">Dobré odpoledne!</string> + <string name="theme_remove_image">Odebrat obrázek</string> + <string name="wallpaper_preview_hello_bob">Dobré ráno!</string> + <string name="color_mode_light">Světlé</string> + <string name="snd_error_auth">Špatný klíč nebo neznámé spojení - pravděpodobně je spojení smazáno.</string> + <string name="file_error_auth">Špatný klíč nebo neznámá adresa části souboru - soubor je pravděpodobně odstraněn.</string> + <string name="network_smp_proxy_mode_unprotected">Nechráněno</string> + <string name="network_smp_proxy_mode_unknown_description">Použít soukromé směrování s neznámými servery.</string> + <string name="network_smp_proxy_mode_unprotected_description">Použit soukromé směrování s neznámými servery, když IP adresa není chráněna.</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Bez Tor nebo VPN bude vaše IP adresa viditelná souborovým serverům.</string> + <string name="info_row_debug_delivery">Ladit doručování</string> + <string name="v5_8_message_delivery_descr">Snížena spotřeba baterie.</string> + <string name="file_not_approved_title">Neznámé servery!</string> + <string name="file_not_approved_descr">Bez Tor nebo VPN bude vaše IP adresa viditelná pro tyto XFTP relé: +\n%1$s.</string> + <string name="network_smp_proxy_fallback_allow_protected">Když je IP adresa skryta</string> + <string name="network_smp_proxy_fallback_allow">Ano</string> + <string name="color_wallpaper_tint">Zbarvení tapety</string> + <string name="color_wallpaper_background">Pozadí tapety</string> + <string name="app_check_for_updates_beta">Beta</string> + <string name="info_view_call_button">volání</string> + <string name="privacy_media_blur_radius">Rozmazat media</string> + <string name="cant_call_contact_alert_title">Kontaktu nelze volat</string> + <string name="all_users">Všechny profily</string> + <string name="attempts_label">pokusy</string> + <string name="app_check_for_updates_download_completed_title">Aktualizace aplikace je stažena</string> + <string name="servers_info_subscriptions_connections_subscribed">Aktivní spojení</string> + <string name="allow_calls_question">Povolit volání?</string> + <string name="calls_prohibited_alert_title">Volání zakázáno!</string> + <string name="cant_call_member_alert_title">Nelze zavolat člena skupiny</string> + <string name="deleted_chats">Archivované kontakty</string> + <string name="v6_0_your_contacts_descr">Archivujte kontakty pro pozdější chatování.</string> + <string name="smp_proxy_error_broker_host">Adresa předávacího serveru je nekompatibilní s nastavením sítě: %1$s.</string> + <string name="smp_proxy_error_broker_version">Verze předávacího serveru je nekompatibilní s nastavením sítě: %1$s.</string> + <string name="proxy_destination_error_broker_host">Cílová adresa serveru %1$s je nekompatibilní s nastavením přeposílajícího serveru %2$s.</string> + <string name="smp_proxy_error_connecting">Chyba připojení k přeposílajícímu serveru %1$s. Prosím, zkuste to později.</string> + <string name="proxy_destination_error_failed_to_connect">Předávacímu serveru %1$s se nepodařilo připojit k cílovému serveru %2$s. Prosím, zkuste to později.</string> + <string name="cannot_share_message_alert_text">Vybrané nastavení chatu zakazuje tuto zprávu.</string> + <string name="smp_servers_other">Jiné SMP servery</string> + <string name="smp_servers_configured">Nastavené SMP servery</string> + <string name="servers_info_subscriptions_connections_pending">Probíhá</string> + <string name="chunks_uploaded">Části nahrány</string> + <string name="n_file_errors">%1$d chyba souboru:\n%2$s</string> + <string name="n_other_file_errors">%1$d jiná chyba souboru(ů).</string> + <string name="error_forwarding_messages">Chyba přeposílaní zpráv</string> + <string name="srv_error_host">Adresa serveru není kompatibilní s nastavením sítě.</string> + <string name="forward_alert_title_messages_to_forward">Předat %1$s zpráv(u)?</string> + <string name="forward_alert_title_nothing_to_forward">Nic k předání!</string> + <string name="forward_alert_forward_messages_without_files">Předat zprávy bez souborů?</string> + <string name="forward_files_failed_to_receive_desc">%1$d soubor(y) se nepodařilo stáhnout.</string> + <string name="forward_files_not_accepted_desc">%1$d soubor(y) nestažen(y).</string> + <string name="forward_files_missing_desc">%1$d soubor(y) smazán(y).</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s zprávy nepředány</string> + <string name="forward_files_not_accepted_receive_files">Stáhnout</string> + <string name="compose_forward_messages_n">Předávám %1$s zpráv</string> + <string name="forward_multiple">Předat zprávy…</string> + <string name="compose_save_messages_n">Uložit %1$s zpráv</string> + <string name="network_proxy_auth_mode_no_auth">Nepoužívat autorizaci s proxy.</string> + <string name="network_proxy_incorrect_config_title">Chyba ukládání proxy</string> + <string name="network_proxy_incorrect_config_desc">Ujistěte se, že nastavení proxy je správné.</string> + <string name="network_proxy_password">Heslo</string> + <string name="network_proxy_auth">Proxy autentizace</string> + <string name="app_check_for_updates_button_install">Instalovat aktualizace</string> + <string name="app_check_for_updates_button_open">Otevřít umístění souboru</string> + <string name="app_check_for_updates_download_started">Stahování aktualizace, nezavírejte aplikaci</string> + <string name="app_check_for_updates_installed_successfully_desc">Prosím restartujte aplikaci.</string> + <string name="app_check_for_updates_installed_successfully_title">Instalovány úspěšně</string> + <string name="app_check_for_updates_button_remind_later">Připomenout později</string> + <string name="app_check_for_updates_notice_title">Zkontrolovat aktualizace</string> + <string name="privacy_media_blur_radius_off">Vypnuto</string> + <string name="settings_section_title_chat_database">CHAT DATABÁZE</string> + <string name="member_info_member_disabled">vypnut</string> + <string name="message_queue_info_server_info">info fronty serveru: %1$s\n\nposlední obdržená zpráva: %2$s</string> + <string name="network_options_save_and_reconnect">Uložit a připojit znovu</string> + <string name="v6_0_chat_list_media">Hrajte ze seznamu chatů.</string> + <string name="servers_info_messages_received">Přijatých zprávy</string> + <string name="servers_info_reconnect_servers_title">Znovu připojit servery?</string> + <string name="completed">Kompletní</string> + <string name="secured">Zabezpečeno</string> + <string name="error_parsing_uri_desc">Prosím zkontrolujte, že SimpleX odkaz je správný.</string> + <string name="error_parsing_uri_title">Chybný odkaz</string> + <string name="proxy_destination_error_broker_version">Verze cílového serveru %1$s je nekompatibilní s nastavením přeposílajícího serveru %2$s.</string> + <string name="message_forwarded_title">Zpráva předána</string> + <string name="keep_conversation">Udržujte konverzaci</string> + <string name="only_delete_conversation">Jen smazat konverzaci</string> + <string name="confirm_delete_contact_question">Potvrdit smazání kontaktu?</string> + <string name="delete_contact_cannot_undo_warning">Kontakt bude smazán - nelze vrátit!</string> + <string name="conversation_deleted">Konverzace odstraněna!</string> + <string name="paste_link">Vložit odkaz</string> + <string name="chat_database_exported_title">Chat databáze exportována</string> + <string name="cant_send_message_to_member_alert_title">Členu skupiny nelze odeslat zprávu</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">Požádejte váš kontakt ať povolí volání.</string> + <string name="color_sent_quote">Odeslaných odpovědí</string> + <string name="wallpaper_scale">Škálovat</string> + <string name="wallpaper_scale_fit">Přizpůsobit</string> + <string name="v6_0_privacy_blur">Rozmazání pro lepší soukromí.</string> + <string name="v6_0_connect_faster_descr">Připojte se k vašim přátelům rychleji.</string> + <string name="v6_0_delete_many_messages_descr">Smazat až 20 zpráv najednou.</string> + <string name="v6_0_increase_font_size">Zvětšit velikost písma.</string> + <string name="v6_0_connection_servers_status_descr">Stav připojení a serverů.</string> + <string name="v6_0_connection_servers_status">Kontrolujte svou síť</string> + <string name="servers_info_sessions_errors">Chyby</string> + <string name="servers_info_sessions_connected">Připojen</string> + <string name="servers_info_sessions_connecting">Připojování</string> + <string name="servers_info_connected_servers_section_header">Připojené servery</string> + <string name="servers_info_previously_connected_servers_section_header">Dříve připojené servery</string> + <string name="acknowledged">Potvrzeno</string> + <string name="duplicates_label">duplikáty</string> + <string name="deleted">Smazán</string> + <string name="open_server_settings_button">Otevřít nastavení serveru</string> + <string name="v6_0_new_chat_experience">Nový zážitek z chatu 🎉</string> + <string name="v6_0_new_media_options">Nové možnosti médií</string> + <string name="new_message">Nová zpráva</string> + <string name="scan_paste_link">Skenovat / Vložit odkaz</string> + <string name="no_filtered_contacts">Žádné filtrované kontakty</string> + <string name="error_initializing_web_view_wrong_arch">Chyba inicializace WebView. Ujistěte se, že máte nainstalován WebView podporující architekturu arm64.\nChyba: %s</string> + <string name="v6_0_private_routing_descr">Chrání vaši IP adresu a připojení.</string> + <string name="forward_files_messages_deleted_after_selection_desc">Zprávy byly odstraněny poté, co jste je vybrali.</string> + <string name="network_session_mode_session_description">Nové přihlašovací údaje SOCKS budou použity pokaždé, když zapnete aplikaci.</string> + <string name="network_session_mode_server_description">Nové přihlašovací údaje SOCKS budou použity pro každý server.</string> + <string name="servers_info_reconnect_servers_message">Znovu připojte všechny připojené servery pro vynucení doručení. Využívá další provoz.</string> + <string name="reset_all_hints">Resetovat všechny tipy</string> + <string name="forward_files_in_progress_desc">%1$d soubor(y) se stále stahují.</string> + <string name="v6_1_message_dates_descr">Lepší datování zpráv.</string> + <string name="v6_1_better_security">Lepší zabezpečení ✅</string> + <string name="chunks_deleted">Části odstraněny</string> + <string name="info_view_connect_button">připojení</string> + <string name="current_user">Aktuální profil</string> + <string name="servers_info_reconnect_server_error">Chyba znovu připojení serveru</string> + <string name="servers_info_reconnect_servers_error">Chyba při opětovném připojování serverů</string> + <string name="servers_info_reconnect_server_message">Znovu připojte server pro vynucení doručení. Využívá další provoz.</string> + <string name="servers_info_modal_error_title">Chyba</string> + <string name="delete_messages_cannot_be_undone_warning">Zprávy budou smazány - nelze vrátit!</string> + <string name="delete_without_notification">Smazat bez upozornění</string> + <string name="info_view_search_button">hledat</string> + <string name="switching_profile_error_title">Chyba přepínání profilu</string> + <string name="select_chat_profile">Vyberte chat profil</string> + <string name="info_view_message_button">zpráva</string> + <string name="info_view_open_button">otevřít</string> + <string name="contact_deleted">Kontakt smazán!</string> + <string name="member_info_member_inactive">neaktivní</string> + <string name="servers_info_details">Detaily</string> + <string name="servers_info_reset_stats">Resetovat všechny statistiky</string> + <string name="please_try_later">Prosím zkuste později.</string> + <string name="private_routing_error">Chyba soukromého směrování</string> + <string name="network_error_broker_host_desc">Adresa serveru není kompatibilní s nastavením sítě: %1$s.</string> + <string name="member_inactive_title">Člen neaktivní</string> + <string name="member_inactive_desc">Zpráva může být doručena později až bude člen aktivní.</string> + <string name="message_forwarded_desc">Zatím bez přímého spojení, zpráva je předána adminem.</string> + <string name="cant_call_contact_connecting_wait_alert_text">Připojování ke kontaktu, počkejte nebo se podívejte později!</string> + <string name="cant_call_contact_deleted_alert_text">Kontakt odstraněn.</string> + <string name="deletion_errors">Chyby mazání</string> + <string name="servers_info_detailed_statistics">Podrobné statistiky</string> + <string name="chunks_downloaded">Části staženy</string> + <string name="network_session_mode_server">Server</string> + <string name="network_smp_proxy_fallback_allow_protected_description">Odesílat zprávy přímo, když je IP adresa chráněna a váš nebo cílový server nepodporuje soukromé směrování.</string> + <string name="network_smp_proxy_fallback_allow_description">Odeslat zprávy přímo, když váš nebo cílový server nepodporuje soukromé směrování.</string> + <string name="app_check_for_updates">Zkontrolovat aktualizace</string> + <string name="app_check_for_updates_notice_disable">Vypnout</string> + <string name="app_check_for_updates_disabled">Vypnut</string> + <string name="app_check_for_updates_button_download">Stáhnout %s (%s)</string> + <string name="invite_friends_short">Pozvat</string> + <string name="create_address_button">Vytvořit</string> + <string name="settings_message_shape_corner">Roh</string> + <string name="settings_section_title_message_shape">Tvar zpráv</string> + <string name="chat_database_exported_continue">Pokračovat</string> + <string name="call_desktop_permission_denied_chrome">Klikněte na info tlačítko blízko pole adresy, pro použití mikrofonu.</string> + <string name="call_desktop_permission_denied_safari">Otevřete nastavení Safari / Webové stránky / mikrofon, vyberte možnost Povolit pro localhost.</string> + <string name="appearance_font_size">Velikost písma</string> + <string name="v6_0_upgrade_app_descr">Stáhnout nové verze z GitHubu.</string> + <string name="v6_0_reachable_chat_toolbar">Dosažitelný panel nástrojů chatu</string> + <string name="migrate_from_device_remove_archive_question">Odebrat archiv?</string> + <string name="servers_info_files_tab">Soubory</string> + <string name="servers_info_messages_sent">Odeslaných zpráv</string> + <string name="servers_info_downloaded">Staženo</string> + <string name="servers_info_reconnect_server_title">Znovu připojit server?</string> + <string name="decryption_errors">chyba dešifrování</string> + <string name="v6_1_better_calls">Lepší volání</string> + <string name="v6_1_better_user_experience">Větší přívětivost</string> + <string name="v6_1_customizable_message_descr">Přizpůsobitelný tvar zpráv.</string> + <string name="v6_1_delete_many_messages_descr">Smazat nebo moderovat až 200 zpráv.</string> + <string name="v6_1_forward_many_messages_descr">Předat až 20 zpráv najednou.</string> + <string name="servers_info_missing">Žádné info, zkuste načíst znovu</string> + <string name="servers_info">Informace o serverech</string> + <string name="downloaded_files">Stažené soubory</string> + <string name="download_errors">Chyby stahování</string> + <string name="servers_info_reset_stats_alert_error_title">Chyba resetování statistik</string> + <string name="servers_info_detailed_statistics_received_messages_header">Přijaté zprávy</string> + <string name="servers_info_detailed_statistics_received_total">Přijato celkem</string> + <string name="servers_info_detailed_statistics_receive_errors">Chyb přijmutí</string> + <string name="servers_info_reconnect_all_servers_button">Připojte znovu všechny servery</string> + <string name="servers_info_reset_stats_alert_confirm">Reset</string> + <string name="servers_info_reset_stats_alert_title">Resetovat všechny statistiky?</string> + <string name="servers_info_detailed_statistics_sent_messages_header">Odeslané zprávy</string> + <string name="servers_info_detailed_statistics_sent_messages_total">Odeslaných celkem</string> + <string name="server_address">Adresa serveru</string> + <string name="one_hand_ui">Dosažitelné panely nástrojů</string> + <string name="acknowledgement_errors">Chyba potvrzení</string> + <string name="connections">Připojení</string> + <string name="created">Vytvořen</string> + <string name="expired_label">prošlý</string> + <string name="other_label">jiné</string> + <string name="other_errors">jiné chyby</string> + <string name="reconnect">Znovu připojit</string> + <string name="send_errors">Chyby odesílání</string> + <string name="sent_directly">Odesláno přímo</string> + <string name="sent_via_proxy">Odeslaných přes proxy</string> + <string name="delete_members_messages__question">Odstranit %d zpráv členů?</string> + <string name="delete_messages_mark_deleted_warning">Zprávy budou označeny pro smazání. Příjemci budou moci tyto zprávy odhalit.</string> + <string name="select_verb">Vybrat</string> + <string name="compose_message_placeholder">Zpráva</string> + <string name="selected_chat_items_nothing_selected">Nic nevybráno</string> + <string name="selected_chat_items_selected_n">Vybrány %d</string> + <string name="privacy_media_blur_radius_medium">Střední</string> + <string name="servers_info_subscriptions_section_header">Příjem zpráv</string> + <string name="xftp_servers_configured">Nastavené XFTP servery</string> + <string name="media_and_file_servers">Servery médií a souborů</string> + <string name="message_servers">Servery zpráv</string> + <string name="xftp_servers_other">Jiné FXTP servery</string> + <string name="action_button_add_members">Pozvat</string> + <string name="cant_call_member_send_message_alert_text">Pošlete zprávu pro povolení volání.</string> + <string name="accept_conditions">Přijmout podmínky</string> + <string name="operator_conditions_accepted">Přijaté podmínky</string> + <string name="chat_list_add_list">Přidat seznam</string> + <string name="report_item_visibility_moderators">Vidí to pouze odesílatelé a moderátoři</string> + <string name="report_item_visibility_submitter">Vidite to pouze vy a moderátor</string> + <string name="report_item_archived">archivovaná hlášení</string> + <string name="errors_in_servers_configuration">Chyba v konfiguraci serverů.</string> + <string name="error_accepting_operator_conditions">Chyba při příjímání podmínek</string> + <string name="no_media_servers_configured_for_sending">Žádné servery pro odesílání souborů.</string> + <string name="onboarding_network_operators_continue">Pokračovat</string> + <string name="operator_open_changes">Otevřít změny</string> + <string name="no_chats_found">Nenalezen žádný chat</string> + <string name="report_item_archived_by">archivovaná hlášení podle %s</string> + <string name="display_name_accepted_invitation">přijmuté pozvání</string> + <string name="report_reason_community">Porušení pokynů komunity</string> + <string name="report_reason_illegal">Nevhodný obsah</string> + <string name="report_reason_other">Jiný důvod</string> + <string name="failed_to_save_servers">Chyba změny serverů</string> + <string name="report_reason_profile">Nevhodný profil</string> + <string name="for_chat_profile">Pro profil chatu %s:</string> + <string name="no_message_servers_configured">Žádné chatovací servery.</string> + <string name="no_message_servers_configured_for_private_routing">Žádné servery pro soukromé směrování chatů.</string> + <string name="no_media_servers_configured_for_private_routing">Žádné servery pro příjem souborů.</string> + <string name="no_message_servers_configured_for_receiving">Žádné servery pro příjem zpráv.</string> + <string name="delete_chat_list_warning">Všechny chaty budou ze seznamu odebrány %s, a seznam bude smazán</string> + <string name="for_social_media">Pro sociální sítě</string> + <string name="remote_hosts_section">Vzdálené telefony</string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[Podmínky budou přijaty pro operátora(y): <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Stejné podmínky budou platit pro operátora(y): <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[Stejné podmínky budou platit pro operátora <b>%s</b>.]]></string> + <string name="operator_use_for_messages_private_routing">Pro soukromé směrování</string> + <string name="operator_open_conditions">Otevřít podmínky</string> + <string name="chat_archive">Nebo importovat soubor archivu</string> + <string name="blocking_reason_content">Obsah porušuje podmínky používání</string> + <string name="connection_error_blocked">Spojení blokováno</string> + <string name="connection_error_blocked_desc">Připojení je blokováno serverovým operátorem:\n%1$s.</string> + <string name="report_archive_alert_title">Archivovat hlášení?</string> + <string name="message_deleted_or_not_received_error_title">Žádná zpráva</string> + <string name="archive_verb">Archivovat</string> + <string name="archive_report">Archivovat hlášení</string> + <string name="delete_report">Vymazat hlášení</string> + <string name="file_error_blocked">Soubor je blokován operátorem serveru:\n%1$s.</string> + <string name="report_verb">Nahlásit</string> + <string name="no_unread_chats">Žádné nepřečtené chaty</string> + <string name="no_chats">Žádné chaty</string> + <string name="no_chats_in_list">Žádné chaty v seznamu %s.</string> + <string name="chat_list_contacts">Kontakty</string> + <string name="chat_list_favorites">Oblíbené</string> + <string name="chat_list_all">Vše</string> + <string name="report_compose_reason_header_illegal">Nahlásit obsah: uvidí ho pouze moderátoři skupiny.</string> + <string name="open_with_app">Otevřít pomocí %s</string> + <string name="create_list">Vytvořit seznam</string> + <string name="button_add_friends">Přidat prátele</string> + <string name="mute_all_chat">Ztlumit vše</string> + <string name="duplicated_list_error">Název seznamu a emoji by mělo být různé pro všechny seznamy.</string> + <string name="change_list">Změnit seznam</string> + <string name="list_name_field_placeholder">Název seznamu...</string> + <string name="debug_logs">Povolit logování</string> + <string name="onboarding_network_operators">Síťoví operátoři</string> + <string name="privacy_chat_list_open_links">Otevřít odkaz ze seznamu chatu</string> + <string name="privacy_chat_list_open_web_link_question">Otevřít webový odkaz?</string> + <string name="privacy_chat_list_open_links_ask">Zeptat se</string> + <string name="privacy_chat_list_open_links_no">Ne</string> + <string name="privacy_chat_list_open_web_link">Otevřít odkaz</string> + <string name="error_saving_database">Chyba ukládání databáze</string> + <string name="button_add_team_members">Přidat členy týmu</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">Chat bude smazán pro všechny členy - tato akce je nevratná!</string> + <string name="delete_chat_for_self_cannot_undo_warning">Chat bude pro vás smazán - tato akce je nevratná!</string> + <string name="button_leave_chat">Opustit chat</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">Člen bude odstraněn z chatu - tato akce je nevratná!</string> + <string name="connection_not_ready">Připojení není připraveno.</string> + <string name="operator_conditions_accepted_for_enabled_operators_on">Podmínky budou automaticky přijaty pro povolené operátory dne: %s.</string> + <string name="operator_info_title">Síťový operátor</string> + <string name="operators_conditions_accepted_for"><![CDATA[Podmínky jsou přijaty pro operátora(y): <b>%s</b>.]]></string> + <string name="network_preset_servers_title">Přednastavené servery</string> + <string name="operator_conditions_failed_to_load">Text aktuálních podmínek se nepodařilo načíst, podmínky si můžete prohlédnout prostřednictvím tohoto odkazu:</string> + <string name="operator_conditions_will_be_accepted_on">Podmínky budou přijaty dne: %s.</string> + <string name="operator_conditions_will_be_applied"><![CDATA[Tyto podmínky budou také platit pro: <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Podmínky budou přijaty pro operátor(y): <b>%s</b>.]]></string> + <string name="operator_added_xftp_servers">Přidány servery pro média & soubory</string> + <string name="v6_2_network_decentralization_enable_flux">Povolte Flux v nastavení sítě a serverů pro lepší ochranu metadat.</string> + <string name="servers_info_proxied_servers_section_header">Servery přes proxy</string> + <string name="v6_2_business_chats_descr">Soukromí pro vaše zákazníky.</string> + <string name="group_member_role_moderator">moderátor</string> + <string name="operator_added_message_servers">Přidány chatovací servery</string> + <string name="operator">Operátor</string> + <string name="disable_automatic_deletion_question">Zakázat automatické mazání zpráv?</string> + <string name="chat_ttl_options_footer">Vymazat zprávy chatu z tohoto zařízení.</string> + <string name="disable_automatic_deletion">Zakázat mazání zpráv</string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[Chcete-li používat servery <b>%s</b>, přijměte podmínky používání.]]></string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Xiaomi zařízení</b>: aby fungovaly notifikace, povolte prosím Autostart v systémovém nastavení.]]></string> + <string name="maximum_message_size_title">Zpráva je moc velká!</string> + <string name="maximum_message_size_reached_text">Zmenšete prosím velikost zprávy a odešlete ji znovu.</string> + <string name="maximum_message_size_reached_non_text">Zmenšete prosím velikost zprávy nebo odeberte média a odešlete ji znovu.</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[jednorázový odkaz může být použit <i>pouze s jedním kontaktem</i> - sdílejte osobně nebo přes jakoukoliv chatovací službu.]]></string> + <string name="connection_security">Zabezpečení připojení</string> + <string name="onboarding_notifications_mode_service_desc_short">Aplikace vždy poběží na pozadí</string> + <string name="onboarding_notifications_mode_battery">Upozornění a baterie</string> + <string name="onboarding_network_operators_app_will_use_for_routing">Například, pokud váš kontakt dostane zprávu pres Chat server SimpleX, vaše aplikace ji doručí pomocí Flux serveru.</string> + <string name="error_loading_chat_tags">Chyba načtení seznamů chatu</string> + <string name="error_creating_chat_tags">Chyba vytváření seznamu chatu</string> + <string name="error_updating_chat_tags">Chyba aktualizace seznamu chatů</string> + <string name="chat_list_businesses">Firmy</string> + <string name="chat_bottom_bar">Dosažitelný panel nástrojů chatu</string> + <string name="invite_to_chat_button">Pozvat do chatu</string> + <string name="only_chat_owners_can_change_prefs">Pouze vlastníci chatu mohou upravit předvolby.</string> + <string name="info_row_chat">Chat</string> + <string name="operator_conditions_of_use">Podmínky použití</string> + <string name="error_adding_server">Chyba přidání serveru</string> + <string name="appearance_app_toolbars">Panel nástrojů aplikace</string> + <string name="appearance_bars_blur_radius">Rozmazání</string> + <string name="direct_messages_are_prohibited">Přímé zprávy mezi členy jsou zakázány.</string> + <string name="v6_2_network_decentralization_enable_flux_reason">pro lepší ochranu metadat.</string> + <string name="connect_plan_chat_already_exists">Chat již existuje</string> + <string name="proxied">Přes proxy</string> + <string name="v6_2_business_chats">Firemní chaty</string> + <string name="v6_2_improved_chat_navigation">Vylepšená navigace chatu</string> + <string name="v6_2_improved_chat_navigation_descr">- Otevřít chat na první nepřečtené zprávě.\n- Přejít na citované zprávy.</string> + <string name="report_archive_alert_title_all">Archivovat všechna hlášení?</string> + <string name="report_archive_alert_title_nth">Archivovat %d hlášení?</string> + <string name="report_archive_for_all_moderators">Pro všechny moderátory</string> + <string name="report_archive_for_me">Pro mě</string> + <string name="group_reports_active">%d reportů</string> + <string name="chat_list_groups">Skupiny</string> + <string name="list_menu">Seznam</string> + <string name="group_reports_member_reports">Hlášení členů</string> + <string name="chat_list_notes">Poznámky</string> + <string name="disable_automatic_deletion_message">Zprávy v tomto chatu nebudou nikdy smazány.</string> + <string name="change_order_chat_list_menu_action">Změnit řazení</string> + <string name="delete_chat_list_menu_action">Vymazat</string> + <string name="delete_chat_list_question">Vymazat seznam?</string> + <string name="edit_chat_list_menu_action">Upravit</string> + <string name="prefs_error_saving_settings">Chyba ukládání nastavení</string> + <string name="or_to_share_privately">Nebo sdílet soukromě</string> + <string name="business_address">Firemní adresa</string> + <string name="operator_conditions_accepted_on">Podmínky přijaté dne: %s.</string> + <string name="direct_messages_are_prohibited_in_chat">Přímé zprávy mezi členy jsou v tomto chatu zakázány.</string> + <string name="group_members_can_send_reports">Členové mohou zprávy nahlásit moderátorům.</string> + <string name="disable_sending_member_reports">Zakázat nahlašování zpráv moderátorům.</string> + <string name="error_updating_server_title">Chyba aktualizace serveru</string> + <string name="operator_server_alert_title">Operátor serveru</string> + <string name="v6_2_network_decentralization">Decentralizace sítě</string> + <string name="archive_reports">Archivovat hlášení</string> + <string name="leave_chat_question">Opustit chat?</string> + <string name="address_settings">Nastavení adres</string> + <string name="address_or_1_time_link">Adresa nebo jednorázový odkaz?</string> + <string name="add_to_list">Přidat na seznam</string> + <string name="add_your_team_members_to_conversations">Přidat členy týmu ke konverzaci</string> + <string name="report_archive_alert_desc_all">Všechna hlášení vám budou archivována</string> + <string name="enable_sending_member_reports">Povolit nahlašování zpráv moderátorům</string> + <string name="change_automatic_deletion_question">Změnit automatické mazání zpráv?</string> + <string name="create_1_time_link">Vytvořit jednorázový odkaz</string> + <string name="how_it_helps_privacy">Jak to pomáhá soukromí</string> + <string name="button_delete_chat">Vymazat chat</string> + <string name="delete_chat_question">Vymazat chat?</string> + <string name="operator_conditions_accepted_for_some"><![CDATA[Podmínky jsou již přijaty pro tohoto/tyto operátora(y): <b>%s</b>.]]></string> + <string name="operators_conditions_will_also_apply"><![CDATA[Tyto podmínky budou také platit pro: <b>%s</b>.]]></string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Již jste spojeni s <b>%1$s</b>.]]></string> + <string name="network_session_mode_session">Relace aplikace</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">Podmínky budou přijaty pro povolené operátory po 30 dnech.</string> + <string name="sync_connection_desc">Připojení vyžaduje opětovné vyjednání šifrování.</string> + <string name="encryption_renegotiation_in_progress">Probíhá opětovné vyjednávání o šifrování.</string> + <string name="sync_connection_confirm">Opravit</string> + <string name="sync_connection_question">Opravit připojení?</string> + <string name="smp_servers_new_server">Nový server</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[Všechny zprávy a soubory jsou odeslány s <b>koncovým šifrováním</b>, s post-quantovým zabezpečením v přímých zprávách.]]></string> + <string name="onboarding_notifications_mode_off_desc_short">Žádné služba na pozadí</string> + <string name="onboarding_notifications_mode_periodic_desc_short">Kontrolovat zprávy každých 10 minut</string> + <string name="error_creating_report">Chyba vytváření hlášení</string> + <string name="chat_item_ttl_year">1 rok</string> + <string name="chat_item_ttl_default">výchozí (%s)</string> + <string name="network_proxy_random_credentials">Použijte náhodné přihlašovací údaje</string> + <string name="report_compose_reason_header_spam">Nahlásit spam: uvidí pouze skupinový moderátoři.</string> + <string name="operator_review_conditions">Přečíst podmínky</string> + <string name="operator_website">Webové stránky</string> + <string name="subscribed">Odebírán</string> + <string name="display_name_requested_to_connect">požádáno o připojení</string> + <string name="report_reason_spam">Spam</string> + <string name="no_media_servers_configured">Žádné mediální a souborové servery.</string> + <string name="temporary_file_error">Chyba dočasného souboru</string> + <string name="servers_info_transport_sessions_section_header">Přesunout sezení</string> + <string name="network_option_tcp_connection">TCP připojení</string> + <string name="operator_use_operator_toggle_description">Použité servery</string> + <string name="use_servers_of_operator_x">Použit %s</string> + <string name="operator_use_for_messages_receiving">Pro příjem</string> + <string name="color_mode_system">Systém</string> + <string name="blocking_reason_spam">Spam</string> + <string name="connection_error_quota">Nedoručené zprávy</string> + <string name="message_deleted_or_not_received_error_desc">Tato zpráva byla smazána, nebo dosud nebyla přijata.</string> + <string name="report_reason_alert_title">Důvod nahlášení?</string> + <string name="report_archive_alert_desc">Report bude archivován.</string> + <string name="moderate_messages_will_be_marked_warning">Zprávy budou pro všechny členy označeny jako moderované.</string> + <string name="chat_list_group_reports">Reporty</string> + <string name="report_compose_reason_header_profile">Nahlašte profil člena: uvidí pouze skupinový moderátoři.</string> + <string name="report_compose_reason_header_community">Nahlásit porušení: Uvidí pouze moderátoři skupiny.</string> + <string name="private_routing_show_message_status">Zobrazit stav zpráv</string> + <string name="app_check_for_updates_button_skip">Přeskočit tuto verzi</string> + <string name="chat_list_always_visible">Zobrazit seznam v novém okně</string> + <string name="v6_0_upgrade_app">Aktualizovat aplikaci automaticky</string> + <string name="chat_database_exported_not_all_files">Některé soubory nebyly exportovány</string> + <string name="unread_mentions">Nepřečtené zmínky</string> + <string name="share_address_publicly">Sdílet adresu veřejně</string> + <string name="share_simplex_address_on_social_media">Sdílent SimpleX adresu na sociálních médiích.</string> + <string name="share_1_time_link_with_a_friend">Sdílejte 1 rázový odkaz s přítelem</string> + <string name="network_socks_proxy">SOCKS Proxy</string> + <string name="app_check_for_updates_update_available">Dostupná aktualizace: %s</string> + <string name="app_check_for_updates_stable">Stabilní</string> + <string name="onboarding_network_operators_conditions_you_can_configure">Můžete nastavit operátory v nastavení sítě a serverů.</string> + <string name="settings_message_shape_tail">Ocas</string> + <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Zastavíte přijímání zpráv z tohoto chatu. Chat historie bude zachována.</string> + <string name="xftp_servers_per_user">Servery pro nové soubory vašeho aktuálního chat profilu</string> + <string name="error_server_protocol_changed">Protokolu serveru se změnil.</string> + <string name="error_server_operator_changed">Operátor serveru se změnil.</string> + <string name="appearance_zoom">Zoom</string> + <string name="chat_theme_set_default_theme">Nastavit výchozí téma</string> + <string name="servers_info_uploaded">Nahráno</string> + <string name="privacy_chat_list_open_links_yes">Ano</string> + <string name="one_hand_ui_card_title">Přepnout chat seznam:</string> + <string name="change_automatic_chat_deletion_message">Tuto akci nelze zrušit - zprávy odeslané a přijaté v tomto chatu dříve než vybraná, budou smazány.</string> + <string name="servers_info_reset_stats_alert_message">Statistiky serverů budou obnoveny - nemůže být vráceno!</string> + <string name="v6_3_reports">Odešlete soukromý report</string> + <string name="v6_3_reports_descr">Pomozte administrátorům moderovat své skupiny.</string> + <string name="v6_3_faster_deletion_of_groups">Rychlejší mazání skupin.</string> + <string name="servers_info_starting_from">Od %s.</string> + <string name="max_group_mentions_per_message_reached">Můžete zmínit až %1$s členů ve zprávě!</string> + <string name="maximum_message_size_reached_forwarding">Musíte kopírovat a snížit velikost zprávy, abyste ji poslali.</string> + <string name="network_proxy_username">Jméno</string> + <string name="simplex_address_or_1_time_link">SimpleX adresa nebo 1 rázový odkaz?</string> + <string name="toolbar_settings">Nastavení</string> + <string name="save_list">Uložit seznam</string> + <string name="app_check_for_updates_canceled">Stažení aktualizace zrušeno</string> + <string name="chat_database_exported_save">Můžete uložit exportovaný archiv.</string> + <string name="operator_use_for_messages">Použit pro zprávy</string> + <string name="server_added_to_operator__name">Server přidán k operátoru %s.</string> + <string name="appearance_in_app_bars_alpha">Průhlednost</string> + <string name="v6_1_switch_chat_profile_descr">Přepínání chat profilu pro 1-rázové pozvánky.</string> + <string name="info_view_video_button">video</string> + <string name="new_chat_share_profile">Sdílet profil</string> + <string name="member_reports_are_prohibited">Reportování zpráv je zakázáno v této skupině.</string> + <string name="xftp_server">XFTP servr</string> + <string name="uploaded_files">Nahrané soubory</string> + <string name="subscription_results_ignored">Odběr ignorován</string> + <string name="srv_error_version">Verze serveru není kompatibilní s nastavením sítě.</string> + <string name="address_creation_instruction">Pro pozdější vytvoření adresy, klepněte v menu na Vytvořit SimpleX adresu.</string> + <string name="your_servers">Vaše servery</string> + <string name="view_updated_conditions">Zobrazit aktualizované podmínky</string> + <string name="group_reports_active_one">1 report</string> + <string name="group_preview_rejected">zamítnuto</string> + <string name="notification_group_report">Report: %s</string> + <string name="report_compose_reason_header_other">Nahlásit další: uvidí pouze skupinový moderátoři.</string> + <string name="you_can_set_connection_name_to_remember">Můžete nastavit název připojení, pro pamatování, s kým byl odkaz sdílen.</string> + <string name="switching_profile_error_message">Vaše připojení bylo přesunuto na %s, ale došlo k neočekávané chybě při přesměrování na profil.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">Pro každý profil použijte různé přihlašovací údaje</string> + <string name="network_smp_proxy_mode_unknown">Neznámé servery</string> + <string name="private_routing_explanation">Pro ochranu vaší IP adresy, soukromé směrování používá vaše servery SMP k doručování zpráv.</string> + <string name="network_smp_web_port_toggle">Použít web portu</string> + <string name="network_smp_web_port_section_title">TCP port pro zprávy</string> + <string name="chat_main_profile_sent">Váš chat profil bude zaslán členům</string> + <string name="system_mode_toast">Režim systému</string> + <string name="v6_3_mentions">Zmínky členů 👋</string> + <string name="v6_3_organize_chat_lists">Organizujte konverzace do seznamů</string> + <string name="v6_3_better_groups_performance">Lepší výkon skupin</string> + <string name="v6_3_better_privacy_and_security">Lepší soukromí a bezpečnost</string> + <string name="v6_3_organize_chat_lists_descr">Nenechte si ujít důležité zprávy.</string> + <string name="v6_3_faster_sending_messages">Rychlejší odesílání zpráv.</string> + <string name="v6_3_private_media_file_names">Vlastní názvy souborů médií.</string> + <string name="servers_info_target">Zobrazuji informace pro</string> + <string name="upload_errors">Chyby nahrávání</string> + <string name="servers_info_proxied_servers_section_footer">Soukromé směrování se používá k doručování zpráv těmto serverům, protože k nim nejste připojeni.</string> + <string name="size">Velikost</string> + <string name="onboarding_network_operators_review_later">Přečíst později</string> + <string name="onboarding_network_operators_update">Aktualizovat</string> + <string name="operator_servers_title">%s servery</string> + <string name="servers_info_subscriptions_total">Celkem</string> + <string name="network_error_broker_version_desc">Verze serveru není kompatibilní s vaší aplikací: %1$s.</string> + <string name="connection_error_quota_desc">Připojení dosáhlo limitu nedoručených zpráv, váš kontakt je asi offline.</string> + <string name="operator_use_for_sending">Pro odeslílání</string> + <string name="operator_use_for_files">Použit pro soubory</string> + <string name="v6_2_network_decentralization_descr">Druhý přednastavený operátor v aplikaci!</string> + <string name="call_desktop_permission_denied_title">Chcete-li volat, povolte použití mikrofonu. Ukončete hovor a zkuste to znovu.</string> + <string name="servers_info_statistics_section_header">Statistiky</string> + <string name="subscription_errors">Chyby odběru</string> + <string name="onboarding_network_about_operators">O operátorech</string> + <string name="onboarding_network_operators_simplex_flux_agreement">SimpleX Chat a Flux udělali dohodu, a zahrnuly servery spravované Flux do aplikace.</string> + <string name="smp_server">SMP server</string> + <string name="contact_list_header_title">Vaše kontakty</string> + <string name="servers_info_private_data_disclaimer">Od %s.\nJsou všechna data uchovávána ve vašem zařízení.</string> + <string name="onboarding_choose_server_operators">Serverový operátoři</string> + <string name="onboarding_select_network_operators_to_use">Vybrat provozovatele sítě pro použití.</string> + <string name="onboarding_network_operators_configure_via_settings">Můžete nakonfigurovat servery v nastavení.</string> + <string name="icon_descr_sound_muted">Zvuk ztlumen</string> + <string name="v6_0_reachable_chat_toolbar_descr">Používat aplikaci jednou rukou.</string> + <string name="v6_1_better_security_descr">SinpleX protokoly přezkoumány Trail Bits.</string> + <string name="v6_1_better_calls_descr">Přepnínání zvuku a videa během hovoru.</string> + <string name="network_proxy_auth_mode_username_password">Vaše přihlašovací údaje mohou být zaslány nešifrované.</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">Pokud je povolen více než jeden operátor, nikdo z nich nemá metadata, aby poznal, kdo s kým komunikuje.</string> + <string name="text_field_set_chat_placeholder">Nastavit název chatu..</string> + <string name="network_smp_web_port_footer">Použít TCP port %1$s, když není zadán žádný port.</string> + <string name="remote_ctrl_connection_stopped_identity_desc">Tento odkaz byl použit s jiným mobilním zařízením, vytvořte na počítači nový odkaz.</string> + <string name="v6_3_mentions_descr">Získejte upozornění, když jste zmíněni.</string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">SimpleX adresa a 1 rázové odkazy je bezpečné sdílet přes všechny komunikátory.</string> + <string name="moderate_messages_will_be_deleted_warning">Zprávy budou smazány pro všechny členy.</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">Aplikace vyžaduje potvrzení stahování z neznámých serverů (s výjimkou .onion nebo při aktivaci SOCKS proxy).</string> + <string name="you_need_to_allow_calls">Musíte povolit kontaktům volání, abyste jim mohli zavolat.</string> + <string name="v6_3_set_message_expiration_in_chats">Nastavení expirace zpráv.</string> + <string name="subscription_percentage">Zobrazit procenta</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">Nahraný archiv databáze bude ze serverů trvale odstraněn.</string> + <string name="to_protect_against_your_link_replaced_compare_codes">Pro ochranu před záměnou odkazů, můžete porovnat bezpečnostní kódy.</string> + <string name="you_can_still_view_conversation_with_contact">Stále si můžete prohlédnout rozhovor s %1$s v chat seznamu.</string> + <string name="onboarding_network_operators_app_will_use_different_operators">Aplikace chrání vaše soukromí pomocí různých operátorů v každé konverzaci.</string> + <string name="one_hand_ui_change_instruction">Můžete jej změnit v nastavení Vzhledu.</string> + <string name="member_role_will_be_changed_with_notification_chat">Role se změní na %s. Každý v chatu bude upozorněn.</string> + <string name="app_check_for_updates_notice_desc">Chcete-li být informováni o nových verzích, zapněte periodickou kontrolu pro Stabilní nebo Beta verze.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">Pro každé připojení použijte různé přihlašovací údaje.</string> + <string name="privacy_media_blur_radius_strong">Silný</string> + <string name="view_conditions">Zobrazit podmínky</string> + <string name="you_can_still_send_messages_to_contact">Můžete posílat zprávy %1$s z archivovaných kontaktů.</string> + <string name="chat_database_exported_migrate">Můžete přenést exportovanou databázi.</string> + <string name="group_member_status_rejected">zamítnuto</string> + <string name="privacy_media_blur_radius_soft">Jemný</string> + <string name="error_reading_passphrase">Chyba čtení přístupové fráze databáze</string> + <string name="restore_passphrase_can_not_be_read_enter_manually_desc">Přístupová fráze v úlozišti klíčů nelze načíst, prosím zadejte ji ručně. To se může stát po aktualizaci systému nekompatibilní s aplikací. Pokud to tak není, kontaktujte prosím vývojáře.</string> + <string name="restore_passphrase_can_not_be_read_desc">Přístupovou frázi v ůložišti klíčů nelze načíst. Mohlo se to stát po aktualizaci systému nekompatibilní s aplikací. Pokud to není váš případ, kontaktujte prosím vývojáře.</string> + <string name="operator_updated_conditions">Aktualizované podmínky</string> + <string name="group_member_status_pending_approval">čekám na schválení</string> + <string name="group_member_status_pending_approval_short">čekám</string> + <string name="block_members_for_all_question">Blokovat členy všem?</string> + <string name="block_members_desc">Všechny nové zprávy od těchto členů budou skryty!</string> + <string name="unblock_members_for_all_question">Odblokovat členy všem?</string> + <string name="feature_roles_moderators">moderátoři</string> + <string name="unblock_members_desc">Zprávy od těchto členů budou zobrazeny!</string> + <string name="members_will_be_removed_from_group_cannot_be_undone">Členové budou odstraněny ze skupiny - toto nelze zvrátit!</string> + <string name="button_remove_members_question">Odebrat členy?</string> + <string name="members_will_be_removed_from_chat_cannot_be_undone">Členové budou odstraněny z chatu - toto nelze zvrátit!</string> + <string name="onboarding_conditions_by_using_you_agree">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.</string> + <string name="onboarding_conditions_accept">Přijmout</string> + <string name="onboarding_conditions_configure_server_operators">Nastavit operátora serveru</string> + <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Zásady ochrany soukromí a podmínky používání.</string> + <string name="onboarding_conditions_private_chats_not_accessible">Soukromé konverzace, skupiny a kontakty nejsou přístupné provozovatelům serverů.</string> + <string name="unsupported_connection_link">Nepodporovaný odkaz k připojení</string> + <string name="privacy_short_links">Používejte krátké odkazy (BETA)</string> + <string name="link_requires_newer_app_version_please_upgrade">Tento odkaz vyžaduje novější verzi aplikace. Prosím aktualizujte aplikaci nebo požádejte kontakt o odeslání kompatibilního odkazu.</string> + <string name="simplex_link_channel">odkaz SimpleX kanálu</string> + <string name="full_link_button_text">Úplný odkaz</string> + <string name="short_link_button_text">Krátký odkaz</string> + <string name="network_smp_web_port_all">Všechny servery</string> + <string name="network_smp_web_port_off">Vypnut</string> + <string name="network_smp_web_port_preset">Přednastavené servery</string> + <string name="network_smp_web_port_preset_footer">Použít TCP port 443 jen pro přednastavené servery.</string> +</resources> 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 a36eca6663..6646720c5c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -3,8 +3,8 @@ <string name="app_name">SimpleX</string> <string name="thousand_abbreviation">k</string> <!-- Connect via Link - MainActivity.kt --> - <string name="connect_via_contact_link">Über die Kontakt-Adresse verbinden?</string> - <string name="connect_via_invitation_link">Über den Einmal-Link verbinden?</string> + <string name="connect_via_contact_link">Über den Kontaktadressen-Link verbinden?</string> + <string name="connect_via_invitation_link">Über den Einmal-Einladungslink verbinden?</string> <string name="connect_via_group_link">Der Gruppe beitreten?</string> <string name="profile_will_be_sent_to_contact_sending_link">Ihr Profil wird an den Kontakt gesendet, von dem Sie diesen Link erhalten haben.</string> <string name="you_will_join_group">Sie werden mit allen Gruppenmitgliedern verbunden.</string> @@ -20,10 +20,10 @@ <string name="deleted_description">Gelöscht</string> <string name="marked_deleted_description">als gelöscht markiert</string> <string name="sending_files_not_yet_supported">Das Senden von Dateien wird noch nicht unterstützt</string> - <string name="receiving_files_not_yet_supported">Der Empfang von Dateien wird noch nicht unterstützt</string> - <string name="sender_you_pronoun">Ihre Daten</string> + <string name="receiving_files_not_yet_supported">Das Herunterladen von Dateien wird noch nicht unterstützt</string> + <string name="sender_you_pronoun">Profil</string> <string name="unknown_message_format">Unbekanntes Nachrichtenformat</string> - <string name="invalid_message_format">Unzulässiges Nachrichtenformat</string> + <string name="invalid_message_format">Ungültiges Nachrichtenformat</string> <!-- PendingContactConnection - ChatModel.kt --> <string name="connection_local_display_name">verbindung %1$d</string> <string name="display_name_connection_established">verbindung hergestellt</string> @@ -39,7 +39,7 @@ <string name="description_via_one_time_link_incognito">Inkognito über einen Einmal-Link</string> <!-- FormattedText, SimpleX links - ChatModel.kt --> <string name="simplex_link_contact">SimpleX-Kontaktadressen-Link</string> - <string name="simplex_link_invitation">SimpleX-Einmal-Einladung</string> + <string name="simplex_link_invitation">SimpleX-Einmal-Einladungslink</string> <string name="simplex_link_group">SimpleX-Gruppen-Link</string> <string name="simplex_link_connection">über %1$s</string> <string name="simplex_link_mode">SimpleX-Links</string> @@ -50,7 +50,7 @@ <!-- SimpleXAPI.kt --> <string name="error_saving_smp_servers">Fehler beim Speichern der SMP-Server</string> <string name="ensure_smp_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die SMP-Server-Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind.</string> - <string name="error_setting_network_config">Fehler bei der Aktualisierung der Netzwerk-Konfiguration.</string> + <string name="error_setting_network_config">Fehler bei der Aktualisierung der Netzwerkkonfiguration.</string> <!-- API Error Responses - SimpleXAPI.kt --> <string name="connection_timeout">Verbindungszeitüberschreitung</string> <string name="connection_error">Verbindungsfehler</string> @@ -58,24 +58,24 @@ <string name="error_sending_message">Fehler beim Senden der Nachricht</string> <string name="error_adding_members">Fehler beim Hinzufügen von Mitgliedern</string> <string name="error_joining_group">Fehler beim Beitritt zur Gruppe</string> - <string name="cannot_receive_file">Datei kann nicht empfangen werden</string> + <string name="cannot_receive_file">Datei kann nicht heruntergeladen werden</string> <string name="sender_cancelled_file_transfer">Der Absender hat die Dateiübertragung abgebrochen.</string> - <string name="error_receiving_file">Fehler beim Empfangen der Datei</string> + <string name="error_receiving_file">Fehler beim Herunterladen der Datei</string> <string name="error_creating_address">Fehler beim Erstellen der Adresse</string> - <string name="contact_already_exists">Kontakt ist bereits vorhanden</string> + <string name="contact_already_exists">Kontakt besteht bereits</string> <string name="you_are_already_connected_to_vName_via_this_link">Sie sind bereits mit %1$s verbunden.</string> <string name="invalid_connection_link">Ungültiger Verbindungslink</string> <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Überprüfen Sie bitte, ob Sie den richtigen Link genutzt haben, oder bitten Sie Ihren Kontakt darum, Ihnen nochmal einen Link zuzusenden.</string> <string name="connection_error_auth">Verbindungsfehler (AUTH)</string> <string name="connection_error_auth_desc">Entweder hat Ihr Kontakt die Verbindung gelöscht, oder dieser Link wurde bereits verwendet, es könnte sich um einen Fehler handeln – bitte melden Sie ihn uns. -\nBitten Sie Ihren Kontakt darum, einen weiteren Verbindungs-Link zu erzeugen, um sich neu verbinden zu können, und stellen Sie sicher, dass Sie eine stabile Netzwerk-Verbindung haben.</string> +\nBitten Sie Ihren Kontakt darum, einen weiteren Verbindungs-Link zu erzeugen, um sich neu verbinden zu können, und stellen Sie sicher, dass Sie eine stabile Netzwerkverbindung haben.</string> <string name="error_accepting_contact_request">Fehler beim Akzeptieren der Kontaktanfrage</string> <string name="sender_may_have_deleted_the_connection_request">Der Absender hat möglicherweise die Verbindungsanfrage gelöscht.</string> <string name="error_deleting_contact">Fehler beim Löschen des Kontakts</string> <string name="error_deleting_group">Fehler beim Löschen der Gruppe</string> <string name="error_deleting_contact_request">Fehler beim Löschen der Kontaktanfrage</string> - <string name="error_deleting_pending_contact_connection">Fehler beim Löschen der anstehenden Kontaktaufnahme</string> - <string name="error_changing_address">Fehler beim Wechseln der Adresse</string> + <string name="error_deleting_pending_contact_connection">Fehler beim Löschen der ausstehenden Kontaktaufnahme</string> + <string name="error_changing_address">Fehler beim Wechseln der Empfängeradresse</string> <string name="error_smp_test_failed_at_step">Der Test ist beim Schritt %s fehlgeschlagen.</string> <string name="error_smp_test_server_auth">Um Warteschlangen zu erzeugen, benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort.</string> <string name="error_smp_test_certificate">Der Fingerabdruck des Zertifikats in der Serveradresse ist wahrscheinlich ungültig.</string> @@ -88,9 +88,9 @@ <string name="icon_descr_instant_notifications">Sofortige Benachrichtigungen</string> <string name="service_notifications">Sofortige Benachrichtigungen!</string> <string name="service_notifications_disabled">Sofortige Benachrichtigungen sind deaktiviert!</string> - <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Um Ihre Privatsphäre zu schützen, kann statt der Push-Benachrichtigung der <b>SimpleX-Hintergrunddienst genutzt werden</b> – dieser benötigt ein paar Prozent Akkuleistung am Tag.]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Um Ihre Privatsphäre zu schützen, <b>läuft SimpleX im Hintergrund ab</b>, anstatt Push-Benachrichtigungen zu nutzen.]]></string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Diese können über die Einstellungen deaktiviert werden</b> – solange die App läuft, werden Benachrichtigungen weiterhin angezeigt.]]></string> - <string name="turn_off_battery_optimization"><![CDATA[Um diese Funktion zu nutzen, wählen Sie im nächsten Dialog bitte die Einstellung <b>Erlauben Sie SimpleX im Hintergrund abzulaufen</b>. Ansonsten werden die Benachrichtigungen deaktiviert.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[Um Benachrichtigungen sofort zu erhalten, <b>Erlauben Sie es</b> im nächsten Dialog.]]></string> <string name="turning_off_service_and_periodic">Die Akkuoptimierung ist aktiv, der Hintergrunddienst und die periodische Nachfrage nach neuen Nachrichten ist abgeschaltet. Sie können diese Funktion in den Einstellungen wieder aktivieren.</string> <string name="periodic_notifications">Periodische Benachrichtigungen</string> <string name="periodic_notifications_disabled">Periodische Benachrichtigungen sind deaktiviert!</string> @@ -98,7 +98,7 @@ <string name="enter_passphrase_notification_title">Passwort wird benötigt</string> <string name="enter_passphrase_notification_desc">Geben Sie bitte das Datenbank-Passwort ein, um Benachrichtigungen zu erhalten.</string> <string name="database_initialization_error_title">Die Datenbank kann nicht initialisiert werden</string> - <string name="database_initialization_error_desc">Die Datenbank arbeitet nicht richtig. Tippen Sie für weitere Informationen.</string> + <string name="database_initialization_error_desc">Die Datenbank arbeitet nicht richtig. Für weitere Informationen antippen.</string> <!-- SimpleX Chat foreground Service --> <string name="simplex_service_notification_title">SimpleX-Chat-Hintergrunddienst</string> <string name="simplex_service_notification_text">Empfange Nachrichten …</string> @@ -157,7 +157,7 @@ <string name="hide_verb">Verbergen</string> <string name="allow_verb">Erlauben</string> <string name="delete_message__question">Die Nachricht löschen?</string> - <string name="delete_message_cannot_be_undone_warning">Nachricht wird gelöscht – dies kann nicht rückgängig gemacht werden!</string> + <string name="delete_message_cannot_be_undone_warning">Nachricht wird gelöscht. Dies kann nicht rückgängig gemacht werden!</string> <string name="delete_message_mark_deleted_warning">Die Nachricht wird zum Löschen markiert. Der/die Empfänger kann/können diese Nachricht aufdecken.</string> <string name="for_me_only">Für mich löschen</string> <string name="for_everybody">Für alle</string> @@ -176,7 +176,7 @@ <string name="group_preview_you_are_invited">Sie sind zu der Gruppe eingeladen</string> <string name="group_preview_join_as">Beitreten als %s</string> <string name="group_connection_pending">verbinde …</string> - <string name="tap_to_start_new_chat">Tippen Sie, um einen neuen Chat zu starten</string> + <string name="tap_to_start_new_chat">Zum Starten eines neuen Chats tippen</string> <string name="chat_with_developers">Chatten Sie mit den Entwicklern</string> <string name="you_have_no_chats">Sie haben keine Chats</string> <!-- ShareListView.kt --> @@ -195,10 +195,10 @@ <!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt --> <string name="image_descr">Bild</string> <string name="icon_descr_waiting_for_image">Warten auf ein Bild</string> - <string name="icon_descr_asked_to_receive">Es wird um den Empfang eines Bildes gebeten</string> + <string name="icon_descr_asked_to_receive">Es wird um das Herunterladen eines Bildes gebeten</string> <string name="icon_descr_image_snd_complete">Bild gesendet</string> <string name="waiting_for_image">Warten auf ein Bild</string> - <string name="image_will_be_received_when_contact_is_online">Das Bild wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</string> + <string name="image_will_be_received_when_contact_is_online">Das Bild wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</string> <string name="image_saved">Bild wurde im Fotoalbum gespeichert</string> <!-- Files - CIFileView.kt --> <string name="icon_descr_file">Datei</string> @@ -206,7 +206,7 @@ <string name="contact_sent_large_file">Ihr Kontakt hat eine Datei gesendet, die größer ist als die derzeit unterstützte maximale Größe (%1$s).</string> <string name="maximum_supported_file_size">Die derzeit maximal unterstützte Dateigröße beträgt %1$s.</string> <string name="waiting_for_file">Warte auf Datei</string> - <string name="file_will_be_received_when_contact_is_online">Die Datei wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später noch mal nach!</string> + <string name="file_will_be_received_when_contact_is_online">Die Datei wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später noch mal nach!</string> <string name="file_saved">Datei gespeichert</string> <string name="file_not_found">Datei nicht gefunden</string> <string name="error_saving_file">Fehler beim Speichern der Datei</string> @@ -218,7 +218,7 @@ <string name="notifications">Benachrichtigungen</string> <!-- Chat Info Actions - ChatInfoView.kt --> <string name="delete_contact_question">Kontakt löschen?</string> - <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Der Kontakt und alle Nachrichten werden gelöscht – dies kann nicht rückgängig gemacht werden!</string> + <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Es wird der Kontakt und alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden!</string> <string name="button_delete_contact">Kontakt löschen</string> <string name="text_field_set_contact_placeholder">Kontaktname festlegen…</string> <string name="icon_descr_server_status_connected">Verbunden</string> @@ -262,16 +262,14 @@ <string name="gallery_video_button">Video</string> <!-- help - ChatHelpView.kt --> <string name="thank_you_for_installing_simplex">Danke, dass Sie SimpleX Chat installiert haben!</string> - <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Sie können sich <font color=#0088ff>mit SimpleX-Chat-Entwicklern verbinden, um Fragen zu stellen und Updates zu erhalten</font>.]]></string> + <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Sie können sich <font color="#0088ff">mit den SimpleX-Chat-Entwicklern verbinden, um Fragen zu stellen und aktuelle Informationen zu erhalten</font>.]]></string> <string name="to_start_a_new_chat_help_header">Um einen neuen Chat zu starten</string> - <string name="chat_help_tap_button">Schaltfläche antippen</string> + <string name="chat_help_tap_button">Schaltfläche tippen</string> <string name="above_then_preposition_continuation">Danach die gewünschte Aktion auswählen:</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Neuen Kontakt hinzufügen</b>: Um Ihren Einmal-QR-Code für Ihren Kontakt zu erstellen.]]></string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>QR-Code scannen</b>: Um sich mit Ihrem Kontakt zu verbinden, der Ihnen seinen QR-Code zeigt.]]></string> <string name="to_connect_via_link_title">Über Link verbinden</string> <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Wenn Sie einen SimpleX-Chat-Einladungslink erhalten haben, können Sie ihn in Ihrem Browser öffnen:</string> <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 Desktop: Angezeigten QR-Code aus der App scannen, über <b>QR-Code scannen</b>.]]></string> - <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 Handy: Tippen Sie in der App auf „<b>In mobiler App öffnen</b>“ und dann auf „<b>Verbinden</b>“.]]></string> + <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 Mobiltelefon: Tippen Sie in der App auf „<b>In mobiler App öffnen</b>“ und dann auf „<b>Verbinden</b>“.]]></string> <!-- Contact Request Alert Dialogue - ChatListNavLinkView.kt --> <string name="accept_connection_request__question">Die Verbindungsanfrage akzeptieren?</string> <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Wenn Sie ablehnen, wird der Absender NICHT benachrichtigt.</string> @@ -279,11 +277,11 @@ <string name="accept_contact_incognito_button">Inkognito akzeptieren</string> <string name="reject_contact_button">Ablehnen</string> <!-- Clear Chat - ChatListNavLinkView.kt --> - <string name="clear_chat_question">Chatinhalte löschen?</string> - <string name="clear_chat_warning">Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht.</string> - <string name="clear_verb">Löschen</string> - <string name="clear_chat_button">Chatinhalte löschen</string> - <string name="clear_chat_menu_action">Chatinhalte löschen</string> + <string name="clear_chat_question">Chat-Inhalte entfernen?</string> + <string name="clear_chat_warning">Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht.</string> + <string name="clear_verb">Entfernen</string> + <string name="clear_chat_button">Chat-Inhalte entfernen</string> + <string name="clear_chat_menu_action">Chat-Inhalte entfernen</string> <string name="delete_contact_menu_action">Löschen</string> <string name="delete_group_menu_action">Löschen</string> <string name="mark_read">Als gelesen markieren</string> @@ -300,7 +298,8 @@ <string name="connection_you_accepted_will_be_cancelled">Die von Ihnen akzeptierte Verbindung wird abgebrochen!</string> <!-- Contact Pending Alert Dialogue - ChatListNavLinkView.kt --> <string name="alert_title_contact_connection_pending">Ihr Kontakt ist noch nicht verbunden!</string> - <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Ihr Kontakt muss online sein, damit die Verbindung hergestellt werden kann.\nSie können diese Verbindung abbrechen und den Kontakt entfernen (und es später nochmals mit einem neuen Link versuchen).</string> + <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Ihr Kontakt muss online sein, damit die Verbindung hergestellt werden kann. +\nSie können diese Verbindung abbrechen, den Kontakt entfernen und es später nochmals mit einem neuen Link versuchen.</string> <!-- Contact Request Information - ContactRequestView.kt --> <string name="contact_wants_to_connect_with_you">möchte sich mit Ihnen verbinden!</string> <!-- Image Placeholder - ChatInfoImage.kt --> @@ -333,7 +332,7 @@ <string name="your_chat_profile_will_be_sent_to_your_contact">Ihr Chat-Profil wird \nan Ihren Kontakt gesendet</string> <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Wenn Sie sich nicht persönlich treffen können, können Sie <b>den QR-Code während eines Videoanrufs scannen</b> oder Ihr Kontakt kann einen Einladungslink über einen anderen Kanal mit Ihnen teilen.]]></string> - <string name="share_invitation_link">Einmal-Link teilen</string> + <string name="share_invitation_link">Einmal-Einladungslink teilen</string> <!-- PasteToConnect.kt --> <string name="connect_via_link">Über einen Link verbinden</string> <string name="connect_button">Verbinden</string> @@ -341,10 +340,9 @@ <string name="this_string_is_not_a_connection_link">Diese Zeichenfolge entspricht keinem gültigen Verbindungslink!</string> <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Sie können sich auch verbinden, indem Sie auf den Link klicken. Wenn er im Browser geöffnet wird, klicken Sie auf die Schaltfläche „<b>In mobiler App öffnen</b>“.]]></string> <!-- CreateLinkView.kt --> - <string name="create_one_time_link">Einmal-Einladungslink erstellen</string> <string name="one_time_link">Einmal-Einladungslink</string> <!-- settings - SettingsView.kt --> - <string name="your_settings">Ihre Einstellungen</string> + <string name="your_settings">Einstellungen</string> <string name="your_simplex_contact_address">Ihre SimpleX-Adresse</string> <string name="database_passphrase_and_export">Datenbank-Passwort & -Export</string> <string name="about_simplex_chat">Über SimpleX Chat</string> @@ -357,8 +355,8 @@ <string name="chat_console">Chat-Konsole</string> <string name="smp_servers">SMP-Server</string> <string name="smp_servers_preset_address">Voreingestellte Serveradresse</string> - <string name="smp_servers_preset_add">Füge voreingestellte Server hinzu</string> - <string name="smp_servers_add">Füge Server hinzu…</string> + <string name="smp_servers_preset_add">Voreingestellte Server hinzufügen</string> + <string name="smp_servers_add">Server hinzufügen</string> <string name="smp_servers_test_server">Teste Server</string> <string name="smp_servers_test_servers">Teste alle Server</string> <string name="smp_servers_save">Alle Server speichern</string> @@ -389,27 +387,23 @@ <string name="configure_ICE_servers">ICE-Server konfigurieren</string> <string name="enter_one_ICE_server_per_line">ICE-Server (einer pro Zeile)</string> <string name="error_saving_ICE_servers">Fehler beim Speichern der ICE-Server</string> - <string name="ensure_ICE_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die WebRTC-ICE-Server-Adressen das richtige Format haben, zeilenweise separiert und nicht doppelt vorhanden sind.</string> + <string name="ensure_ICE_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die WebRTC-ICE-Server-Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind.</string> <string name="save_servers_button">Speichern</string> <string name="network_and_servers">Netzwerk & Server</string> <string name="network_settings">Erweiterte Netzwerkeinstellungen</string> - <string name="network_settings_title">Netzwerkeinstellungen</string> + <string name="network_settings_title">Erweiterte Einstellungen</string> <string name="network_enable_socks">SOCKS-Proxy verwenden?</string> <string name="network_enable_socks_info">Zugriff auf die Server über SOCKS-Proxy auf Port %d? Der Proxy muss gestartet werden, bevor diese Option aktiviert wird.</string> <string name="network_disable_socks">Direkte Internetverbindung verwenden?</string> <string name="network_disable_socks_info">Wenn Sie dies bestätigen, können die Messaging-Server Ihre IP-Adresse sowie Ihren Provider sehen und mit welchen Servern Sie sich verbinden.</string> - <string name="update_onion_hosts_settings_question">Einstellung für .onion-Hosts aktualisieren?</string> <string name="network_use_onion_hosts">Verwende .onion-Hosts</string> <string name="network_use_onion_hosts_prefer">Wenn verfügbar</string> <string name="network_use_onion_hosts_no">Nein</string> <string name="network_use_onion_hosts_required">Erforderlich</string> - <string name="network_use_onion_hosts_prefer_desc">Onion-Hosts werden verwendet, wenn sie verfügbar sind.</string> + <string name="network_use_onion_hosts_prefer_desc">Wenn Onion-Hosts verfügbar sind, werden sie verwendet.</string> <string name="network_use_onion_hosts_no_desc">Onion-Hosts werden nicht verwendet.</string> <string name="network_use_onion_hosts_required_desc">Für die Verbindung werden Onion-Hosts benötigt. \nBitte beachten Sie: Ohne .onion-Adresse können Sie keine Verbindung mit den Servern herstellen.</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">Onion-Hosts werden verwendet, wenn sie verfügbar sind.</string> - <string name="network_use_onion_hosts_no_desc_in_alert">Onion-Hosts werden nicht verwendet.</string> - <string name="network_use_onion_hosts_required_desc_in_alert">Für die Verbindung werden Onion-Hosts benötigt.</string> <string name="appearance_settings">Erscheinungsbild</string> <!-- Address Items - UserAddressView.kt --> <string name="create_address">Adresse erstellen</string> @@ -468,23 +462,20 @@ <string name="callstate_connected">Verbunden</string> <string name="callstate_ended">Beendet</string> <!-- SimpleXInfo --> - <string name="next_generation_of_private_messaging">Die nächste Generation von privatem Messaging</string> + <string name="next_generation_of_private_messaging">Die Zukunft des Messagings</string> <string name="privacy_redefined">Datenschutz neu definiert</string> - <string name="first_platform_without_user_ids">Die erste Plattform ohne Benutzerkennungen – privat per Design</string> - <string name="immune_to_spam_and_abuse">Immun gegen Spam und Missbrauch</string> - <string name="people_can_connect_only_via_links_you_share">Verbindungen mit Kontakten sind nur über Links möglich, die Sie oder Ihre Kontakte untereinander teilen.</string> + <string name="first_platform_without_user_ids">Keine Benutzerkennungen.</string> + <string name="immune_to_spam_and_abuse">Immun gegen Spam</string> + <string name="people_can_connect_only_via_links_you_share">Sie entscheiden, wer sich mit Ihnen verbinden kann.</string> <string name="decentralized">Dezentral</string> - <string name="opensource_protocol_and_code_anybody_can_run_servers">Open-Source-Protokoll und -Code – Jede Person kann ihre eigenen Server aufsetzen und nutzen.</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Jeder kann seine eigenen Server aufsetzen.</string> <string name="create_your_profile">Erstellen Sie Ihr Profil</string> <string name="make_private_connection">Stellen Sie eine private Verbindung her</string> <string name="how_it_works">Wie es funktioniert</string> <!-- How SimpleX Works --> <string name="how_simplex_works">Wie SimpleX funktioniert</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[Viele Menschen haben gefragt: <i>Wie kann SimpleX Nachrichten zustellen, wenn es keine Benutzerkennungen gibt?</i>]]></string> - <string name="to_protect_privacy_simplex_has_ids_for_queues">Zum Schutz Ihrer Privatsphäre verwendet SimpleX anstelle von Benutzerkennungen, die von allen anderen Plattformen verwendet werden, Kennungen für Nachrichtenwarteschlangen, die für jeden Ihrer Kontakte individuell sind.</string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Sie können selbst festlegen, über welche Server Sie Ihre Nachrichten <b>empfangen</b> und an Ihre Kontakte <b>senden</b> wollen.]]></string> - <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Nur die Endgeräte speichern die Benutzerprofile, Kontakte, Gruppen und Nachrichten, welche über eine <b>zweischichtige Ende-zu-Ende-Verschlüsselung</b> gesendet werden.]]></string> - <string name="read_more_in_github">Erfahren Sie in unserem GitHub-Repository mehr dazu.</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">SimpleX nutzt individuelle Kennungen für jeden Ihrer Kontakte, um Ihre Privatsphäre zu schützen.</string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Nur die Endgeräte speichern Benutzerprofile, Kontakte, Gruppen und Nachrichten.</string> <string name="read_more_in_github_with_link"><![CDATA[Erfahren Sie in unserem <font color="#0088ff">GitHub-Repository</font> mehr dazu.]]></string> <!-- MakeConnection --> <string name="paste_the_link_you_received">Fügen Sie den erhaltenen Link ein</string> @@ -505,7 +496,7 @@ <!-- Call settings --> <string name="settings_audio_video_calls">Audio- & Videoanrufe</string> <string name="your_calls">Ihre Anrufe</string> - <string name="always_use_relay">Über ein Relais verbinden</string> + <string name="always_use_relay">Immer über ein Relais verbinden</string> <string name="call_on_lock_screen">Anrufe auf Sperrbildschirm:</string> <string name="accept_call_on_lock_screen">Akzeptieren</string> <string name="show_call_on_lock_screen">Anzeigen</string> @@ -534,7 +525,7 @@ <string name="icon_descr_speaker_on">Lautsprecher an</string> <string name="icon_descr_flip_camera">Kamera umdrehen</string> <!-- Call items --> - <string name="icon_descr_call_pending_sent">Anstehender Anruf</string> + <string name="icon_descr_call_pending_sent">Ausstehender Anruf</string> <string name="icon_descr_call_missed">Verpasster Anruf</string> <string name="icon_descr_call_rejected">Abgelehnter Anruf</string> <string name="icon_descr_call_connecting">Anruf wird verbunden</string> @@ -553,7 +544,7 @@ \n3. Die Verbindung wurde kompromittiert.</string> <!-- Privacy settings --> <string name="privacy_and_security">Datenschutz & Sicherheit</string> - <string name="your_privacy">Ihre Privatsphäre</string> + <string name="your_privacy">Privatsphäre</string> <string name="protect_app_screen">App-Bildschirm schützen</string> <string name="auto_accept_images">Bilder automatisch akzeptieren</string> <string name="send_link_previews">Link-Vorschau senden</string> @@ -594,31 +585,29 @@ <string name="error_stopping_chat">Fehler beim Beenden des Chats</string> <string name="error_exporting_chat_database">Fehler beim Exportieren der Chat-Datenbank</string> <string name="import_database_question">Chat-Datenbank importieren?</string> - <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Ihre aktuelle Chat-Datenbank wird GELÖSCHT und durch die importierte ERSETZT. -\nDiese Aktion kann nicht rückgängig gemacht werden – Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string> + <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Ihre aktuelle Chat-Datenbank wird GELÖSCHT und durch die importierte ERSETZT.\nIhr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. Diese Aktion kann nicht rückgängig gemacht werden!</string> <string name="import_database_confirmation">Importieren</string> <string name="error_deleting_database">Fehler beim Löschen der Chat-Datenbank</string> <string name="error_importing_database">Fehler beim Importieren der Chat-Datenbank</string> <string name="chat_database_imported">Chat-Datenbank importiert</string> <string name="restart_the_app_to_use_imported_chat_database">Starten Sie die App neu, um die importierte Chat-Datenbank zu verwenden.</string> <string name="delete_chat_profile_question">Chat-Profil löschen?</string> - <string name="delete_chat_profile_action_cannot_be_undone_warning">Diese Aktion kann nicht rückgängig gemacht werden – Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string> + <string name="delete_chat_profile_action_cannot_be_undone_warning">Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. Diese Aktion kann nicht rückgängig gemacht werden!</string> <string name="chat_database_deleted">Chat-Datenbank gelöscht</string> <string name="restart_the_app_to_create_a_new_chat_profile">Starten Sie die App neu, um ein neues Chat-Profil zu erstellen.</string> <string name="you_must_use_the_most_recent_version_of_database">Sie dürfen die neueste Version Ihrer Chat-Datenbank NUR auf einem Gerät verwenden, andernfalls erhalten Sie möglicherweise keine Nachrichten mehr von einigen Ihrer Kontakte.</string> - <string name="stop_chat_to_enable_database_actions">Chat beenden, um Datenbankaktionen zu erlauben.</string> <string name="delete_files_and_media_question">Dateien und Medien löschen?</string> - <string name="delete_files_and_media_desc">Diese Aktion kann nicht rückgängig gemacht werden – alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten.</string> - <string name="no_received_app_files">Keine empfangenen oder gesendeten Dateien</string> + <string name="delete_files_and_media_desc">Es werden alle herunter- und hochgeladenen Dateien und Medien gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. Diese Aktion kann nicht rückgängig gemacht werden!</string> + <string name="no_received_app_files">Keine herunter- oder hochgeladenen Dateien</string> <string name="total_files_count_and_size">%d Datei(en) mit einem Gesamtspeicherverbrauch von %s</string> <string name="chat_item_ttl_none">nie</string> - <string name="chat_item_ttl_day">täglich</string> - <string name="chat_item_ttl_week">wöchentlich</string> - <string name="chat_item_ttl_month">monatlich</string> + <string name="chat_item_ttl_day">Älter als ein Tag</string> + <string name="chat_item_ttl_week">Älter als eine Woche</string> + <string name="chat_item_ttl_month">Älter als ein Monat</string> <string name="chat_item_ttl_seconds">%s Sekunde(n)</string> - <string name="delete_messages_after">Löschen der Nachrichten</string> + <string name="delete_messages_after">Nachrichten löschen</string> <string name="enable_automatic_deletion_question">Automatisches Löschen von Nachrichten aktivieren?</string> - <string name="enable_automatic_deletion_message">Diese Aktion kann nicht rückgängig gemacht werden – alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dieser Vorgang kann mehrere Minuten dauern.</string> + <string name="enable_automatic_deletion_message">Es werden alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, gelöscht. Dieser Vorgang kann mehrere Minuten dauern. Diese Aktion kann nicht rückgängig gemacht werden!</string> <string name="delete_messages">Nachrichten löschen</string> <string name="error_changing_message_deletion">Fehler beim Ändern der Einstellung</string> <!-- DatabaseEncryptionView.kt --> @@ -669,20 +658,14 @@ <string name="database_backup_can_be_restored">Der Versuch, das Passwort der Datenbank zu ändern, konnte nicht abgeschlossen werden.</string> <string name="restore_database">Datenbanksicherung wiederherstellen</string> <string name="restore_database_alert_title">Datenbanksicherung wiederherstellen?</string> - <string name="restore_database_alert_desc">Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden.</string> + <string name="restore_database_alert_desc">Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden!</string> <string name="restore_database_alert_confirm">Wiederherstellen</string> <string name="database_restore_error">Fehler bei der Wiederherstellung der Datenbank</string> - <string name="restore_passphrase_not_found_desc">Das Passwort wurde nicht im Schlüsselbund gefunden. Bitte geben Sie es manuell ein. Das kann passieren, wenn Sie die App-Daten mit einem Backup-Programm wieder hergestellt haben. Bitte nehmen Sie Kontakt mit den Entwicklern auf, wenn das nicht der Fall ist.</string> + <string name="restore_passphrase_not_found_desc">Das Passwort wurde nicht im Schlüsselbund gefunden. Bitte geben Sie es manuell ein. Dies kann passieren, wenn Sie die App-Daten mit einem Backup-Programm wieder hergestellt haben. Wenden Sie sich bitte an die Entwickler, wenn dies nicht der Fall ist.</string> <!-- ChatModel.chatRunning interactions --> - <string name="chat_is_stopped_indication">Chat wurde beendet</string> + <string name="chat_is_stopped_indication">Der Chat wurde beendet</string> <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Sie können den Chat über die App-Einstellungen/Datenbank oder durch Neustart der App starten.</string> <!-- ChatArchiveView.kt --> - <string name="chat_archive_header">Datenbank-Archiv</string> - <string name="chat_archive_section">CHAT-ARCHIV</string> - <string name="save_archive">Archiv speichern</string> - <string name="delete_archive">Archiv löschen</string> - <string name="archive_created_on_ts">Erstellt am %1$s</string> - <string name="delete_chat_archive_question">Chat-Archiv löschen\?</string> <!-- Groups --> <string name="group_invitation_item_description">Einladung zur Gruppe %1$s</string> <string name="join_group_question">Der Gruppe beitreten?</string> @@ -693,20 +676,20 @@ <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Sie sind dieser Gruppe beigetreten. Sie werden mit dem einladenden Gruppenmitglied verbunden.</string> <string name="leave_group_button">Verlassen</string> <string name="leave_group_question">Die Gruppe verlassen?</string> - <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Chatverlauf wird beibehalten.</string> + <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Nachrichtenverlauf wird beibehalten.</string> <string name="icon_descr_add_members">Mitglieder einladen</string> <string name="icon_descr_group_inactive">Gruppe inaktiv</string> - <string name="alert_title_group_invitation_expired">Einladung abgelaufen!</string> + <string name="alert_title_group_invitation_expired">Die Einladung ist abgelaufen!</string> <string name="alert_message_group_invitation_expired">Die Gruppeneinladung ist nicht mehr gültig, da sie vom Absender entfernt wurde.</string> <string name="alert_title_no_group">Die Gruppe wurde nicht gefunden!</string> - <string name="alert_message_no_group">Diese Gruppe existiert nicht mehr.</string> + <string name="alert_message_no_group">Diese Gruppe ist nicht mehr vorhanden.</string> <string name="alert_title_cant_invite_contacts">Kontakte können nicht eingeladen werden!</string> <string name="alert_title_cant_invite_contacts_descr">Sie verwenden ein Inkognito-Profil für diese Gruppe. Um zu verhindern, dass Sie Ihr Hauptprofil teilen, ist in diesem Fall das Einladen von Kontakten nicht erlaubt.</string> <!-- CIGroupInvitationView.kt --> <string name="you_sent_group_invitation">Sie haben eine Gruppeneinladung gesendet</string> <string name="you_are_invited_to_group">Sie sind zu der Gruppe eingeladen</string> <string name="group_invitation_tap_to_join">Zum Beitreten tippen</string> - <string name="group_invitation_tap_to_join_incognito">Tippen, um Inkognito beizutreten</string> + <string name="group_invitation_tap_to_join_incognito">Zum Inkognito beitreten tippen</string> <string name="you_joined_this_group">Sie sind dieser Gruppe beigetreten</string> <string name="you_rejected_group_invitation">Sie haben die Gruppeneinladung abgelehnt</string> <string name="group_invitation_expired">Die Gruppeneinladung ist abgelaufen</string> @@ -727,12 +710,12 @@ <string name="snd_group_event_user_left">hat die Gruppe verlassen</string> <string name="snd_group_event_group_profile_updated">Gruppenprofil aktualisiert</string> <!-- Conn event chat items --> - <string name="rcv_conn_event_switch_queue_phase_completed">wechselte die Adresse für Sie</string> - <string name="rcv_conn_event_switch_queue_phase_changing">Wechsel der Adresse…</string> - <string name="snd_conn_event_switch_queue_phase_completed_for_member">Sie haben die Adresse für %s gewechselt</string> - <string name="snd_conn_event_switch_queue_phase_changing_for_member">Wechsel der Adresse für %s…</string> - <string name="snd_conn_event_switch_queue_phase_completed">Sie haben die Adresse gewechselt</string> - <string name="snd_conn_event_switch_queue_phase_changing">Wechsel der Adresse…</string> + <string name="rcv_conn_event_switch_queue_phase_completed">wechselte die Empfängeradresse für Sie</string> + <string name="rcv_conn_event_switch_queue_phase_changing">Wechsel der Empfängeradresse wurde gestartet…</string> + <string name="snd_conn_event_switch_queue_phase_completed_for_member">Die Empfängeradresse für %s wurde gewechselt</string> + <string name="snd_conn_event_switch_queue_phase_changing_for_member">Wechsel der Empfängeradresse für %s wurde gestartet…</string> + <string name="snd_conn_event_switch_queue_phase_completed">Die Empfängeradresse wurde gewechselt</string> + <string name="snd_conn_event_switch_queue_phase_changing">Wechsel der Empfängeradresse wurde gestartet…</string> <!-- GroupMemberRole --> <string name="group_member_role_member">Mitglied</string> <string name="group_member_role_admin">Admin</string> @@ -758,7 +741,7 @@ <string name="skip_inviting_button">Mitgliedereinladungen überspringen</string> <string name="select_contacts">Kontakte auswählen</string> <string name="icon_descr_contact_checked">Kontakt geprüft</string> - <string name="clear_contacts_selection_button">Löschen</string> + <string name="clear_contacts_selection_button">Entfernen</string> <string name="num_contacts_selected">%d Kontakt(e) ausgewählt</string> <string name="no_contacts_selected">Keine Kontakte ausgewählt</string> <string name="invite_prohibited">Kontakt kann nicht eingeladen werden!</string> @@ -769,8 +752,8 @@ <string name="group_info_member_you">Sie: %1$s</string> <string name="button_delete_group">Gruppe löschen</string> <string name="delete_group_question">Gruppe löschen?</string> - <string name="delete_group_for_all_members_cannot_undo_warning">Die Gruppe wird für alle Mitglieder gelöscht – dies kann nicht rückgängig gemacht werden!</string> - <string name="delete_group_for_self_cannot_undo_warning">Die Gruppe wird für Sie gelöscht – dies kann nicht rückgängig gemacht werden!</string> + <string name="delete_group_for_all_members_cannot_undo_warning">Die Gruppe wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden!</string> + <string name="delete_group_for_self_cannot_undo_warning">Die Gruppe wird nur bei Ihnen gelöscht. Dies kann nicht rückgängig gemacht werden!</string> <string name="button_leave_group">Gruppe verlassen</string> <string name="button_edit_group_profile">Gruppenprofil bearbeiten</string> <string name="group_link">Gruppen-Link</string> @@ -789,7 +772,7 @@ <!-- GroupMemberInfoView.kt --> <string name="button_remove_member">Mitglied entfernen</string> <string name="button_send_direct_message">Direktnachricht senden</string> - <string name="member_will_be_removed_from_group_cannot_be_undone">Das Mitglied wird aus der Gruppe entfernt – dies kann nicht rückgängig gemacht werden!</string> + <string name="member_will_be_removed_from_group_cannot_be_undone">Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden!</string> <string name="remove_member_confirmation">Entfernen</string> <string name="member_info_section_title_member">MITGLIED</string> <string name="role_in_group">Rolle</string> @@ -797,8 +780,8 @@ <string name="change_verb">Ändern</string> <string name="switch_verb">Wechseln</string> <string name="change_member_role_question">Die Mitgliederrolle ändern?</string> - <string name="member_role_will_be_changed_with_notification">Die Mitgliederrolle wird auf \"%s\" geändert. Alle Mitglieder der Gruppe werden benachrichtigt.</string> - <string name="member_role_will_be_changed_with_invitation">Die Mitgliederrolle wird auf \"%s\" geändert. Das Mitglied wird eine neue Einladung erhalten.</string> + <string name="member_role_will_be_changed_with_notification">Die Rolle wird auf %s geändert. Alle Mitglieder der Gruppe werden benachrichtigt.</string> + <string name="member_role_will_be_changed_with_invitation">Die Rolle wird auf %s geändert. Das Mitglied wird eine neue Einladung erhalten.</string> <string name="error_removing_member">Fehler beim Entfernen des Mitglieds</string> <string name="error_changing_role">Fehler beim Ändern der Rolle</string> <string name="info_row_group">Gruppe</string> @@ -828,7 +811,6 @@ <string name="network_option_protocol_timeout">Protokollzeitüberschreitung</string> <string name="network_option_ping_interval">PING-Intervall</string> <string name="network_option_enable_tcp_keep_alive">TCP-Keep-Alive aktivieren</string> - <string name="network_options_revert">Zurückkehren</string> <string name="network_options_save">Speichern</string> <string name="update_network_settings_question">Netzwerkeinstellungen aktualisieren?</string> <string name="updating_settings_will_reconnect_client_to_all_servers">Die Aktualisierung der Einstellungen wird den Client wieder mit allen Servern verbinden.</string> @@ -845,7 +827,6 @@ <string name="theme_dark">Dunkel</string> <!-- Appearance.kt --> <string name="theme">Design</string> - <string name="save_color">Farbe speichern</string> <string name="reset_color">Farben zurücksetzen</string> <string name="color_primary">Akzent</string> <!-- Preferences.kt --> @@ -860,7 +841,7 @@ <string name="chat_preferences">Chat-Präferenzen</string> <string name="contact_preferences">Kontakt-Präferenzen</string> <string name="group_preferences">Gruppen-Präferenzen</string> - <string name="set_group_preferences">Gruppen-Präferenzen einstellen</string> + <string name="set_group_preferences">Gruppen-Präferenzen festlegen</string> <string name="your_preferences">Ihre Präferenzen</string> <string name="direct_messages">Direkte Nachrichten</string> <string name="full_deletion">Für jeden löschen</string> @@ -870,15 +851,15 @@ <string name="feature_enabled_for_contact">Für Kontakt aktiviert</string> <string name="feature_off">Aus</string> <string name="feature_received_prohibited">empfangen, nicht erlaubt</string> - <string name="allow_your_contacts_irreversibly_delete">Erlauben Sie Ihren Kontakten gesendete Nachrichten unwiederbringlich zu löschen.</string> - <string name="allow_irreversible_message_deletion_only_if">Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt.</string> + <string name="allow_your_contacts_irreversibly_delete">Erlauben Sie Ihren Kontakten gesendete Nachrichten unwiederbringlich zu löschen (bis zu 24 Stunden).</string> + <string name="allow_irreversible_message_deletion_only_if">Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt (bis zu 24 Stunden).</string> <string name="contacts_can_mark_messages_for_deletion">Ihre Kontakte können Nachrichten zum Löschen markieren. Sie können diese Nachrichten trotzdem anschauen.</string> <string name="allow_your_contacts_to_send_voice_messages">Erlauben Sie Ihre Kontakten Sprachnachrichten zu versenden.</string> <string name="allow_voice_messages_only_if">Erlauben Sie Sprachnachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt.</string> <string name="prohibit_sending_voice_messages">Das Senden von Sprachnachrichten nicht erlauben.</string> - <string name="both_you_and_your_contacts_can_delete">Sowohl Ihr Kontakt als auch Sie können Nachrichten unwiederbringlich löschen.</string> - <string name="only_you_can_delete_messages">Nur Sie können Nachrichten unwiederbringlich löschen (Ihr Kontakt kann sie zum Löschen markieren).</string> - <string name="only_your_contact_can_delete">Nur Ihr Kontakt kann Nachrichten unwiederbringlich löschen (Sie können sie zum Löschen markieren).</string> + <string name="both_you_and_your_contacts_can_delete">Sowohl Ihr Kontakt als auch Sie können Nachrichten unwiederbringlich löschen (bis zu 24 Stunden).</string> + <string name="only_you_can_delete_messages">Nur Sie können Nachrichten unwiederbringlich löschen (Ihr Kontakt kann sie zum Löschen markieren) (bis zu 24 Stunden).</string> + <string name="only_your_contact_can_delete">Nur Ihr Kontakt kann Nachrichten unwiederbringlich löschen (Sie können sie zum Löschen markieren) (bis zu 24 Stunden).</string> <string name="message_deletion_prohibited">In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt.</string> <string name="both_you_and_your_contact_can_send_voice">Sowohl Ihr Kontakt als auch Sie können Sprachnachrichten versenden.</string> <string name="only_you_can_send_voice">Nur Sie können Sprachnachrichten versenden.</string> @@ -886,26 +867,26 @@ <string name="voice_prohibited_in_this_chat">In diesem Chat sind Sprachnachrichten nicht erlaubt.</string> <string name="allow_direct_messages">Das Senden von Direktnachrichten an Gruppenmitglieder erlauben.</string> <string name="prohibit_direct_messages">Das Senden von Direktnachrichten an Gruppenmitglieder nicht erlauben.</string> - <string name="allow_to_delete_messages">Unwiederbringliches löschen von gesendeten Nachrichten erlauben.</string> + <string name="allow_to_delete_messages">Unwiederbringliches löschen von gesendeten Nachrichten erlauben (bis zu 24 Stunden).</string> <string name="prohibit_message_deletion">Unwiederbringliches Löschen von Nachrichten nicht erlauben.</string> <string name="allow_to_send_voice">Das Senden von Sprachnachrichten erlauben.</string> <string name="prohibit_sending_voice">Das Senden von Sprachnachrichten nicht erlauben.</string> - <string name="group_members_can_send_dms">Gruppenmitglieder können Direktnachrichten versenden.</string> - <string name="direct_messages_are_prohibited_in_chat">In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt.</string> - <string name="group_members_can_delete">Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen.</string> - <string name="message_deletion_prohibited_in_chat">In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt.</string> - <string name="group_members_can_send_voice">Gruppenmitglieder können Sprachnachrichten versenden.</string> - <string name="voice_messages_are_prohibited">In dieser Gruppe sind Sprachnachrichten nicht erlaubt.</string> + <string name="group_members_can_send_dms">Mitglieder können Direktnachrichten versenden.</string> + <string name="direct_messages_are_prohibited_in_group">In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt.</string> + <string name="group_members_can_delete">Mitglieder können gesendete Nachrichten unwiederbringlich löschen (bis zu 24 Stunden).</string> + <string name="message_deletion_prohibited_in_chat">Das unwiederbringliche Löschen von Nachrichten ist nicht erlaubt.</string> + <string name="group_members_can_send_voice">Mitglieder können Sprachnachrichten versenden.</string> + <string name="voice_messages_are_prohibited">Sprachnachrichten sind nicht erlaubt.</string> <string name="live">LIVE</string> <string name="view_security_code">Schauen Sie sich den Sicherheitscode an</string> <string name="onboarding_notifications_mode_service">Sofort</string> - <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Gute Option für die Batterieausdauer</b>. Der Hintergrundservice überprüft alle 10 Minuten nach Nachrichten. Sie können eventuell Anrufe oder dringende Nachrichten verpassen.]]></string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Gute Option für die Batterieausdauer</b>. Die App prüft alle 10 Minuten auf neue Nachrichten. Sie können eventuell Anrufe oder dringende Nachrichten verpassen.]]></string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Beste Option für die Akkulaufzeit</b>. Sie empfangen Benachrichtigungen nur, solange die App läuft (kein aktiver Hintergrundservice).]]></string> <string name="send_verb">Senden</string> <string name="is_verified">%s wurde erfolgreich überprüft</string> <string name="clear_verification">Verifikation zurücknehmen</string> <string name="onboarding_notifications_mode_off">Solange die App abläuft</string> - <string name="onboarding_notifications_mode_subtitle">Kann später über die Einstellungen geändert werden.</string> + <string name="onboarding_notifications_mode_subtitle">Auswirkung auf den Akku</string> <string name="delete_after">Löschen nach</string> <string name="ttl_hour">%d Stunde</string> <string name="ttl_hours">%d Stunden</string> @@ -931,18 +912,18 @@ <string name="onboarding_notifications_mode_periodic">Periodisch</string> <string name="allow_to_send_disappearing">Erlauben Sie das Senden von verschwindenden Nachrichten.</string> <string name="disappearing_prohibited_in_this_chat">In diesem Chat sind verschwindende Nachrichten nicht erlaubt.</string> - <string name="only_you_can_send_disappearing">Nur Sie können verschwindende Nachrichten senden.</string> - <string name="only_your_contact_can_send_disappearing">Nur Ihr Kontakt kann verschwindende Nachrichten senden.</string> + <string name="only_you_can_send_disappearing">Nur Sie können verschwindende Nachrichten versenden.</string> + <string name="only_your_contact_can_send_disappearing">Nur Ihr Kontakt kann verschwindende Nachrichten versenden.</string> <string name="failed_to_parse_chat_title">Fehler beim Laden des Chats</string> <string name="failed_to_parse_chats_title">Fehler beim Laden der Chats</string> <string name="contact_developers">Bitte aktualisieren Sie die App und nehmen Sie Kontakt mit den Entwicklern auf.</string> - <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Benötigt mehr Leistung Ihrer Batterie</b>! Der Hintergrundservice läuft permanent ab. Benachrichtigungen werden Ihnen angezeigt, sobald Sie neue Nachrichten erhalten haben.]]></string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Benötigt mehr Leistung Ihrer Batterie</b>! Die App läuft permanent im Hintergrund ab. Benachrichtigungen werden Ihnen sofort angezeigt.]]></string> <string name="create_group_link">Gruppenlink erstellen</string> <string name="allow_your_contacts_to_send_disappearing_messages">Erlauben Sie Ihren Kontakten das Senden von verschwindenden Nachrichten.</string> - <string name="prohibit_sending_disappearing_messages">Das Senden von verschwindenden Nachrichten verbieten.</string> - <string name="disappearing_messages_are_prohibited">In dieser Gruppe sind verschwindende Nachrichten nicht erlaubt.</string> - <string name="group_members_can_send_disappearing">Gruppenmitglieder können verschwindende Nachrichten senden.</string> - <string name="v4_3_improved_server_configuration_desc">Fügen Sie Server durch Scannen der QR-Codes hinzu.</string> + <string name="prohibit_sending_disappearing_messages">Das Senden von verschwindenden Nachrichten nicht erlauben.</string> + <string name="disappearing_messages_are_prohibited">Verschwindende Nachrichten sind nicht erlaubt.</string> + <string name="group_members_can_send_disappearing">Mitglieder können verschwindende Nachrichten versenden.</string> + <string name="v4_3_improved_server_configuration_desc">Server durch Scannen von QR-Codes hinzufügen.</string> <string name="v4_4_disappearing_messages">Verschwindende Nachrichten</string> <string name="accept_feature">Übernehmen</string> <string name="accept_feature_set_1_day">Einen Tag festlegen</string> @@ -955,11 +936,11 @@ <string name="to_verify_compare">Um die Ende-zu-Ende-Verschlüsselung mit Ihrem Kontakt zu überprüfen, müssen Sie den Sicherheitscode in Ihren Apps vergleichen oder scannen.</string> <string name="onboarding_notifications_mode_title">Private Benachrichtigungen</string> <string name="use_chat">Chat verwenden</string> - <string name="both_you_and_your_contact_can_send_disappearing">Ihr Kontakt und Sie können beide verschwindende Nachrichten senden.</string> + <string name="both_you_and_your_contact_can_send_disappearing">Ihr Kontakt und Sie können beide verschwindende Nachrichten versenden.</string> <string name="ttl_h">%dh</string> <string name="v4_2_group_links">Gruppen-Links</string> <string name="new_in_version">Neu in %s</string> - <string name="prohibit_sending_disappearing">Das Senden von verschwindenden Nachrichten verbieten.</string> + <string name="prohibit_sending_disappearing">Das Senden von verschwindenden Nachrichten nicht erlauben.</string> <string name="v4_2_security_assessment">Sicherheits-Gutachten</string> <string name="v4_2_security_assessment_desc">Die Sicherheit von SimpleX Chat wurde von Trail of Bits überprüft.</string> <string name="whats_new">Was ist neu</string> @@ -992,11 +973,11 @@ <string name="app_version_name">App-Version: v%s</string> <string name="core_version">Core Version: v%s</string> <string name="users_add">Profil hinzufügen</string> - <string name="users_delete_all_chats_deleted">Alle Chats und Nachrichten werden gelöscht! Dies kann nicht rückgängig gemacht werden!</string> + <string name="users_delete_all_chats_deleted">Es werden alle Chats und Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden!</string> <string name="users_delete_profile_for">Chat-Profil löschen für</string> <string name="network_option_ping_count">PING-Zähler</string> <string name="update_network_session_mode_question">Transport-Isolations-Modus aktualisieren\?</string> - <string name="smp_servers_per_user">Server der neuen Verbindungen von Ihrem aktuellen Chat-Profil</string> + <string name="smp_servers_per_user">Nachrichten-Server für neue Verbindungen über Ihr aktuelles Chat-Profil</string> <string name="files_and_media_section">Dateien & Medien</string> <string name="network_session_mode_transport_isolation">Transport-Isolation</string> <string name="users_delete_question">Chat-Profil löschen\?</string> @@ -1005,9 +986,7 @@ <string name="network_session_mode_entity">Verbindung</string> <string name="network_session_mode_user">Chat-Profil</string> <string name="delete_files_and_media_for_all_users">Dateien für alle Chat-Profile löschen</string> - <string name="network_session_mode_entity_description"><b>Für jeden Kontakt und jedes Gruppenmitglied</b> wird eine separate TCP-Verbindung (und SOCKS-Berechtigung) genutzt. -\n -\n<b>Bitte beachten Sie</b>: Wenn Sie viele Verbindungen haben, können Akkuverbrauch und Datennutzung wesentlich höher ausfallen und einige Verbindungen scheitern.</string> + <string name="network_session_mode_entity_description"><![CDATA[<b>Für jeden Kontakt und jedes Gruppenmitglied</b> wird eine separate TCP-Verbindung (und SOCKS-Berechtigung) genutzt.\n<b>Bitte beachten Sie</b>: Wenn Sie viele Verbindungen haben, können Akkuverbrauch und Datennutzung wesentlich höher ausfallen und einige Verbindungen scheitern.]]></string> <string name="network_session_mode_user_description"><![CDATA[<b>Für jedes von Ihnen in der App genutzte Chat-Profil</b> wird eine separate TCP-Verbindung (und SOCKS-Berechtigung) genutzt.]]></string> <string name="users_delete_data_only">Nur lokale Profildaten</string> <string name="users_delete_with_connections">Profil und Serververbindungen</string> @@ -1044,7 +1023,6 @@ <string name="moderate_message_will_be_deleted_warning">Diese Nachricht wird für alle Gruppenmitglieder gelöscht.</string> <string name="language_system">System</string> <string name="confirm_password">Passwort bestätigen</string> - <string name="cant_delete_user_profile">Das Benutzerprofil kann nicht gelöscht werden!</string> <string name="dont_show_again">Nicht nochmals anzeigen</string> <string name="v4_6_chinese_spanish_interface">Chinesische und spanische Bedienoberfläche</string> <string name="v4_6_audio_video_calls">Audio- und Videoanrufe</string> @@ -1062,16 +1040,14 @@ <string name="enter_password_to_show">Für die Anzeige das Passwort im Suchfeld eingeben</string> <string name="make_profile_private">Privates Profil erzeugen!</string> <string name="user_mute">Stummschalten</string> - <string name="tap_to_activate_profile">Tippen Sie auf das Profil, um es zu aktivieren.</string> - <string name="should_be_at_least_one_profile">Es muss mindestens ein Benutzer-Profil vorhanden sein.</string> - <string name="should_be_at_least_one_visible_profile">Es muss mindestens ein sichtbares Benutzer-Profil vorhanden sein.</string> + <string name="tap_to_activate_profile">Zum Aktivieren des Profils tippen.</string> <string name="user_unmute">Stummschaltung aufheben</string> <string name="muted_when_inactive">Bei Inaktivität stummgeschaltet!</string> <string name="v4_6_hidden_chat_profiles_descr">Schützen Sie Ihre Chat-Profile mit einem Passwort!</string> <string name="v4_6_audio_video_calls_descr">Bluetooth-Unterstützung und weitere Verbesserungen.</string> <string name="v4_6_group_moderation_descr">Administratoren können nun \n- Nachrichten von Gruppenmitgliedern löschen -\n- Gruppenmitglieder deaktivieren („Beobachter“-Rolle)</string> +\n- Gruppenmitglieder deaktivieren (Beobachter-Rolle)</string> <string name="v4_6_group_welcome_message">Gruppen-Begrüßungsmeldung</string> <string name="v4_6_reduced_battery_usage">Weiter reduzierter Batterieverbrauch</string> <string name="v4_6_reduced_battery_usage_descr">Weitere Verbesserungen sind bald verfügbar!</string> @@ -1084,7 +1060,7 @@ <string name="you_will_still_receive_calls_and_ntfs">Sie können Anrufe und Benachrichtigungen auch von stummgeschalteten Profilen empfangen, solange diese aktiv sind.</string> <string name="group_welcome_title">Begrüßungsmeldung</string> <string name="you_can_hide_or_mute_user_profile">Sie können ein Benutzerprofil verbergen oder stummschalten – für das Menü gedrückt halten.</string> - <string name="to_reveal_profile_enter_password">Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite \"Ihre Chat-Profile\" ein, um Ihr verborgenes Profil zu sehen.</string> + <string name="to_reveal_profile_enter_password">Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite "Ihre Chat-Profile" ein, um Ihr verborgenes Profil zu sehen.</string> <string name="invalid_migration_confirmation">Migrations-Bestätigung ungültig</string> <string name="upgrade_and_open_chat">Aktualisieren und den Chat öffnen</string> <string name="confirm_database_upgrades">Datenbank-Aktualisierungen bestätigen</string> @@ -1094,29 +1070,29 @@ <string name="database_upgrade">Datenbank-Aktualisierung</string> <string name="mtr_error_different">Unterschiedlicher Migrationsstand in der App/Datenbank: %s / %s</string> <string name="downgrade_and_open_chat">Datenbank herabstufen und den Chat öffnen</string> - <string name="incompatible_database_version">Inkompatible Datenbank-Version</string> + <string name="incompatible_database_version">Datenbank-Version nicht kompatibel</string> <string name="database_downgrade_warning">Warnung: Sie könnten einige Daten verlieren!</string> <string name="database_downgrade">Datenbank auf alte Version herabstufen</string> <string name="developer_options">Datenbank-IDs und Transport-Isolationsoption.</string> <string name="mtr_error_no_down_migration">Die Datenbank-Version ist neuer als die App, keine Abwärts-Migration für: %s</string> <string name="hide_dev_options">Verberge:</string> <string name="database_migrations">Migrationen: %s</string> - <string name="image_will_be_received_when_contact_completes_uploading">Das Bild wird empfangen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist.</string> - <string name="file_will_be_received_when_contact_completes_uploading">Die Datei wird empfangen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist.</string> + <string name="image_will_be_received_when_contact_completes_uploading">Das Bild wird heruntergeladen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist.</string> + <string name="file_will_be_received_when_contact_completes_uploading">Die Datei wird heruntergeladen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist.</string> <string name="delete_chat_profile">Chat-Profil löschen</string> <string name="delete_profile">Profil löschen</string> <string name="unhide_profile">Verbergen des Profils aufheben</string> <string name="profile_password">Passwort für Profil</string> <string name="unhide_chat_profile">Verbergen des Chat-Profils aufheben</string> - <string name="icon_descr_video_asked_to_receive">Aufforderung zum Empfang des Videos</string> + <string name="icon_descr_video_asked_to_receive">Aufforderung zum Herunterladen des Videos</string> <string name="videos_limit_desc">Es können nur 10 Videos zur gleichen Zeit versendet werden</string> <string name="videos_limit_title">Zu viele Videos auf einmal!</string> <string name="video_descr">Video</string> <string name="icon_descr_video_snd_complete">Video gesendet</string> - <string name="video_will_be_received_when_contact_completes_uploading">Das Video wird empfangen, sobald Ihr Kontakt das Hochladen beendet hat.</string> + <string name="video_will_be_received_when_contact_completes_uploading">Das Video wird heruntergeladen, sobald Ihr Kontakt das Hochladen beendet hat.</string> <string name="icon_descr_waiting_for_video">Auf das Video warten</string> <string name="waiting_for_video">Auf das Video warten</string> - <string name="video_will_be_received_when_contact_is_online">Das Video wird empfangen, wenn Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später!</string> + <string name="video_will_be_received_when_contact_is_online">Das Video wird heruntergeladen, wenn Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später!</string> <string name="your_XFTP_servers">Ihre XFTP-Server</string> <string name="host_verb">Host</string> <string name="error_saving_xftp_servers">Fehler beim Speichern der XFTP-Server</string> @@ -1157,11 +1133,11 @@ <string name="alert_title_msg_bad_hash">Ungültiger Nachrichten-Hash</string> <string name="la_auth_failed">Authentifizierung fehlgeschlagen</string> <string name="alert_title_msg_bad_id">Falsche Nachrichten-ID</string> - <string name="ensure_xftp_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die XFTP-Server-Adressen das richtige Format haben, zeilenweise separiert und nicht doppelt vorhanden sind.</string> + <string name="ensure_xftp_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die XFTP-Server-Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind.</string> <string name="network_socks_toggle_use_socks_proxy">SOCKS-Proxy nutzen</string> <string name="la_lock_mode">SimpleX-Sperrmodus</string> <string name="lock_not_enabled">SimpleX-Sperre ist nicht aktiviert!</string> - <string name="disable_onion_hosts_when_not_supported"><![CDATA[Setzen Sie <i>Verwende .onion-Hosts</i> auf \"Nein\", wenn der SOCKS-Proxy sie nicht unterstützt.]]></string> + <string name="disable_onion_hosts_when_not_supported"><![CDATA[Setzen Sie <i>Verwende .onion-Hosts</i> auf "Nein", wenn der SOCKS-Proxy sie nicht unterstützt.]]></string> <string name="submit_passcode">Bestätigen</string> <string name="la_mode_system">System</string> <string name="la_could_not_be_verified">Sie können nicht überprüft werden – bitte versuchen Sie es nochmal.</string> @@ -1183,11 +1159,11 @@ <string name="both_you_and_your_contact_can_make_calls">Sowohl Sie als auch Ihr Kontakt können Anrufe tätigen.</string> <string name="only_you_can_make_calls">Nur Sie können Anrufe tätigen.</string> <string name="prohibit_calls">Audio-/Video-Anrufe nicht erlauben.</string> - <string name="stop_rcv_file__title">Den Empfang der Datei beenden\?</string> - <string name="stop_snd_file__title">Das Senden der Datei beenden\?</string> - <string name="stop_rcv_file__message">Der Empfang der Datei wird beendet.</string> + <string name="stop_rcv_file__title">Herunterladen der Datei beenden?</string> + <string name="stop_snd_file__title">Das Hochladen der Datei beenden?</string> + <string name="stop_rcv_file__message">Das Herunterladen der Datei wird beendet.</string> <string name="stop_snd_file__message">Das Senden der Datei wird beendet.</string> - <string name="stop_file__action">Datei beenden</string> + <string name="stop_file__action">Download beenden</string> <string name="revoke_file__message">Die Datei wird von den Servern gelöscht.</string> <string name="revoke_file__confirm">Widerrufen</string> <string name="revoke_file__action">Datei widerrufen</string> @@ -1212,15 +1188,15 @@ <string name="scan_qr_to_connect_to_contact">Um eine Verbindung herzustellen, kann Ihr Kontakt den QR-Code scannen oder den Link in der App verwenden.</string> <string name="simplex_address">SimpleX-Adresse</string> <string name="you_can_accept_or_reject_connection">Wenn Personen eine Verbindung anfordern, können Sie diese annehmen oder ablehnen.</string> - <string name="you_wont_lose_your_contacts_if_delete_address">Sie werden Ihre Kontakte nicht verlieren, falls Sie Ihre Adresse später löschen.</string> + <string name="you_wont_lose_your_contacts_if_delete_address">Sie werden Ihre damit verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.</string> <string name="customize_theme_title">Design anpassen</string> - <string name="theme_colors_section_title">DESIGN-FARBEN</string> - <string name="add_address_to_your_profile">Fügen Sie die Adresse zu Ihrem Profil hinzu, damit Ihre Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet.</string> + <string name="theme_colors_section_title">INTERFACE-FARBEN</string> + <string name="add_address_to_your_profile">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.</string> <string name="all_your_contacts_will_remain_connected_update_sent">Alle Ihre Kontakte bleiben verbunden. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet.</string> <string name="create_address_and_let_people_connect">Erstellen Sie eine Adresse, damit sich Personen mit Ihnen verbinden können.</string> <string name="create_simplex_address">SimpleX-Adresse erstellen</string> <string name="share_with_contacts">Mit Kontakten teilen</string> - <string name="your_contacts_will_remain_connected">Ihre Kontakte bleiben verbunden.</string> + <string name="your_contacts_will_remain_connected">Ihre Kontakte bleiben weiterhin verbunden.</string> <string name="auto_accept_contact">Automatisch akzeptieren</string> <string name="enter_welcome_message_optional">Geben Sie eine Begrüßungsmeldung ein … (optional)</string> <string name="invite_friends">Freunde einladen</string> @@ -1254,7 +1230,7 @@ <string name="if_you_cant_meet_in_person">Falls Sie sich nicht persönlich treffen können, zeigen Sie den QR-Code in einem Videoanruf oder teilen Sie den Link.</string> <string name="read_more_in_user_guide_with_link"><![CDATA[Lesen Sie mehr dazu in der <font color="#0088ff">Benutzeranleitung</font>.]]></string> <string name="import_theme_error_desc">Stellen Sie sicher, dass die Datei die korrekte YAML-Syntax hat. Exportieren Sie das Design, um ein Beispiel für die Dateistruktur des Designs zu erhalten.</string> - <string name="auth_open_chat_profiles">Offene Chat-Profile</string> + <string name="auth_open_chat_profiles">Chat-Profile wechseln</string> <string name="you_can_share_your_address">Sie können Ihre Adresse als Link oder QR-Code teilen – jede Person kann sich mit Ihnen verbinden.</string> <string name="all_app_data_will_be_cleared">Werden die App-Daten komplett gelöscht.</string> <string name="empty_chat_profile_is_created">Es wurde ein leeres Chat-Profil mit dem eingegebenen Namen erstellt und die App öffnet wie gewohnt.</string> @@ -1270,8 +1246,8 @@ <string name="enable_self_destruct">Selbstzerstörung aktivieren</string> <string name="if_you_enter_passcode_data_removed">Wenn Sie diesen Zugangscode während des Öffnens der App eingeben, werden alle App-Daten unwiederbringlich gelöscht!</string> <string name="self_destruct_passcode">Selbstzerstörungs-Zugangscode</string> - <string name="set_passcode">Zugangscode einstellen</string> - <string name="message_reactions_are_prohibited">In dieser Gruppe sind Reaktionen auf Nachrichten nicht erlaubt.</string> + <string name="set_passcode">Zugangscode festlegen</string> + <string name="message_reactions_are_prohibited">Reaktionen auf Nachrichten sind nicht erlaubt.</string> <string name="error_loading_details">Fehler beim Laden von Details</string> <string name="received_message">Empfangene Nachricht</string> <string name="info_menu">Information</string> @@ -1302,7 +1278,7 @@ <string name="only_your_contact_can_add_message_reactions">Nur Ihr Kontakt kann Reaktionen auf Nachrichten geben.</string> <string name="allow_message_reactions">Reaktionen auf Nachrichten erlauben.</string> <string name="prohibit_message_reactions_group">Reaktionen auf Nachrichten nicht erlauben.</string> - <string name="group_members_can_add_message_reactions">Gruppenmitglieder können eine Reaktion auf Nachrichten geben.</string> + <string name="group_members_can_add_message_reactions">Mitglieder können eine Reaktion auf Nachrichten geben.</string> <string name="whats_new_read_more">Mehr erfahren</string> <string name="v5_1_message_reactions_descr">Endlich haben wir sie! 🚀</string> <string name="v5_1_message_reactions">Reaktionen auf Nachrichten</string> @@ -1317,9 +1293,7 @@ <string name="v5_1_custom_themes_descr">Farbdesigns anpassen und weitergeben.</string> <string name="custom_time_unit_days">Tage</string> <string name="custom_time_unit_hours">Stunden</string> - <string name="v5_1_better_messages_descr">- Bis zu 5 Minuten lange Sprachnachrichten -\n- Zeitdauer für verschwindende Nachrichten anpassen -\n- Nachrichten-Historie bearbeiten</string> + <string name="v5_1_better_messages_descr">- Bis zu 5 Minuten lange Sprachnachrichten\n- Zeitdauer für verschwindende Nachrichten anpassen\n- Nachrichtenverlauf bearbeiten</string> <string name="custom_time_picker_custom">benutzerdefiniert</string> <string name="custom_time_unit_months">Monate</string> <string name="custom_time_picker_select">Auswählen</string> @@ -1330,27 +1304,27 @@ <string name="share_text_deleted_at">Gelöscht um: %s</string> <string name="info_row_disappears_at">Verschwindet um</string> <string name="share_text_disappears_at">Verschwindet um: %s</string> - <string name="edit_history">Vergangenheit</string> + <string name="edit_history">Nachrichtenverlauf</string> <string name="message_reactions_prohibited_in_this_chat">In diesem Chat sind Reaktionen auf Nachrichten nicht erlaubt.</string> <string name="item_info_no_text">Kein Text</string> - <string name="non_fatal_errors_occured_during_import">Während des Imports sind einige nicht schwerwiegende Fehler aufgetreten – weitere Details finden Sie in der Chat-Konsole.</string> + <string name="non_fatal_errors_occured_during_import">Während des Imports sind nicht schwerwiegende Fehler aufgetreten:</string> <string name="shutdown_alert_question">Herunterfahren\?</string> <string name="shutdown_alert_desc">Bis zum Neustart der App erhalten Sie keine Benachrichtigungen mehr</string> <string name="settings_section_title_app">APP</string> <string name="settings_restart_app">Neustart</string> <string name="settings_shutdown">Herunterfahren</string> - <string name="error_aborting_address_change">Fehler beim Abbrechen des Adresswechsels</string> - <string name="abort_switch_receiving_address_confirm">Abbrechen</string> + <string name="error_aborting_address_change">Fehler beim Beenden des Adresswechsels</string> + <string name="abort_switch_receiving_address_confirm">Beenden</string> <string name="files_and_media">Dateien und Medien</string> <string name="allow_to_send_files">Das Senden von Dateien und Medien erlauben.</string> <string name="prohibit_sending_files">Das Senden von Dateien und Medien nicht erlauben.</string> - <string name="abort_switch_receiving_address">Wechsel der Adresse abbrechen</string> - <string name="abort_switch_receiving_address_question">Wechsel der Adresse abbrechen\?</string> + <string name="abort_switch_receiving_address">Wechsel der Empfängeradresse beenden</string> + <string name="abort_switch_receiving_address_question">Wechsel der Empfängeradresse beenden?</string> <string name="files_and_media_prohibited">Dateien und Medien sind nicht erlaubt!</string> <string name="only_owners_can_enable_files_and_media">Nur Gruppenbesitzer können Dateien und Medien aktivieren.</string> - <string name="group_members_can_send_files">Gruppenmitglieder können Dateien und Medien senden.</string> - <string name="abort_switch_receiving_address_desc">Der Wechsel der Adresse wird abgebrochen. Die bisherige Adresse wird weiter verwendet.</string> - <string name="files_are_prohibited_in_group">In dieser Gruppe sind Dateien und Medien nicht erlaubt.</string> + <string name="group_members_can_send_files">Mitglieder können Dateien und Medien versenden.</string> + <string name="abort_switch_receiving_address_desc">Der Wechsel der Empfängeradresse wird beendet. Die bisherige Adresse wird weiter verwendet.</string> + <string name="files_are_prohibited_in_group">Dateien und Medien sind nicht erlaubt.</string> <string name="unfavorite_chat">Favorit entfernen</string> <string name="favorite_chat">Favorit</string> <string name="no_filtered_chats">Keine gefilterten Chats</string> @@ -1362,7 +1336,7 @@ <string name="error_synchronizing_connection">Fehler beim Synchronisieren der Verbindung</string> <string name="sync_connection_force_question">Verschlüsselung neu aushandeln\?</string> <string name="fix_connection_question">Verbindung reparieren\?</string> - <string name="no_history">Keine Vergangenheit</string> + <string name="no_history">Kein Nachrichtenverlauf</string> <string name="sync_connection_force_confirm">Neu aushandeln</string> <string name="sync_connection_force_desc">Die Verschlüsselung funktioniert und ein neues Verschlüsselungsabkommen ist nicht erforderlich. Es kann zu Verbindungsfehlern kommen!</string> <string name="renegotiate_encryption">Verschlüsselung neu aushandeln</string> @@ -1392,7 +1366,7 @@ <string name="v5_2_favourites_filter_descr">Nach ungelesenen und favorisierten Chats filtern.</string> <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Das Senden von Empfangsbestätigungen an alle Kontakte in allen sichtbaren Chat-Profilen wird aktiviert.</string> <string name="receipts_contacts_override_disabled">Das Senden von Bestätigungen an %d Kontakte ist deaktiviert</string> - <string name="receipts_section_description">Diese Einstellungen gelten für Ihr aktuelles Profil</string> + <string name="receipts_section_description">Diese Einstellungen gelten für Ihr aktuelles Chat-Profil</string> <string name="receipts_section_description_1">Sie können in den Kontakt- und Gruppeneinstellungen überschrieben werden.</string> <string name="receipts_section_contacts">Kontakte</string> <string name="receipts_contacts_title_disable">Bestätigungen deaktivieren\?</string> @@ -1407,10 +1381,8 @@ <string name="v5_2_message_delivery_receipts_descr">Wir haben das zweite Häkchen vermisst! ✅</string> <string name="v5_2_fix_encryption_descr">Reparatur der Verschlüsselung nach Wiedereinspielen von Backups.</string> <string name="v5_2_more_things">Ein paar weitere Dinge</string> - <string name="v5_2_disappear_one_message_descr">Auch wenn sie im Chat deaktiviert sind.</string> - <string name="v5_2_more_things_descr">- stabilere Zustellung von Nachrichten. -\n- ein bisschen verbesserte Gruppen. -\n- und mehr!</string> + <string name="v5_2_disappear_one_message_descr">Auch wenn sie in den Unterhaltungen deaktiviert sind.</string> + <string name="v5_2_more_things_descr">- Stabilere Zustellung von Nachrichten.\n- Ein bisschen verbesserte Gruppen.\n- Und mehr!</string> <string name="dont_enable_receipts">Nicht aktivieren</string> <string name="sending_delivery_receipts_will_be_enabled">Das Senden von Empfangsbestätigungen an alle Kontakte wird aktiviert.</string> <string name="you_can_enable_delivery_receipts_later_alert">Sie können diese später in den Datenschutz- und Sicherheits-Einstellungen der App aktivieren.</string> @@ -1436,8 +1408,8 @@ <string name="connect_via_member_address_alert_desc">An dieses Gruppenmitglied wird eine Verbindungsanfrage gesendet.</string> <string name="connect_via_member_address_alert_title">Direkt verbinden\?</string> <string name="connect_via_link_incognito">Inkognito verbinden</string> - <string name="connect_use_current_profile">Nutzen Sie das aktuelle Profil</string> - <string name="connect_use_new_incognito_profile">Nutzen Sie das neue Inkognito-Profil</string> + <string name="connect_use_current_profile">Aktuelles Chat-Profil nutzen</string> + <string name="connect_use_new_incognito_profile">Neues Inkognito-Profil nutzen</string> <string name="system_restricted_background_in_call_warn"><![CDATA[Wählen Sie bitte in den App-Einstellungen <b>App-Akkuverbrauch</b> / <b>Unbeschränkt</b> , um Anrufe im Hintergrund zu führen.]]></string> <string name="paste_the_link_you_received_to_connect_with_your_contact">Fügen Sie den erhaltenen Link ein, um sich mit Ihrem Kontakt zu verbinden…</string> <string name="connect__a_new_random_profile_will_be_shared">Es wird ein neues Zufallsprofil geteilt.</string> @@ -1466,7 +1438,7 @@ <string name="open_database_folder">Datenbank-Ordner öffnen</string> <string name="passphrase_will_be_saved_in_settings">Das Passwort wird in Klartext in den Einstellungen gespeichert, nachdem Sie es geändert oder die App neu gestartet haben.</string> <string name="settings_is_storing_in_clear_text">Das Passwort wurde in Klartext in den Einstellungen gespeichert.</string> - <string name="socks_proxy_setting_limitations"><![CDATA[<b>Bitte beachten Sie</b>: Die Nachrichten- und Dateirelais sind per SOCKS Proxy verbunden. Anrufe und gesendete Link-Vorschaubilder nutzen eine direkte Verbindung.]]></string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Bitte beachten Sie</b>: Die Nachrichten- und Datei-Relais sind per SOCKS-Proxy verbunden. Anrufe und gesendete Link-Vorschaubilder nutzen eine direkte Verbindung.]]></string> <string name="encrypt_local_files">Lokale Dateien verschlüsseln</string> <string name="rcv_group_event_open_chat">Öffnen</string> <string name="v5_3_encrypt_local_files">Gespeicherte Dateien & Medien verschlüsseln</string> @@ -1480,22 +1452,20 @@ <string name="v5_3_new_interface_languages_descr">Arabisch, Bulgarisch, Finnisch, Hebräisch, Thailändisch und Ukrainisch - Dank der Nutzer und Weblate.</string> <string name="v5_3_new_desktop_app_descr">Erstellen eines neuen Profils in der Desktop-App. 💻</string> <string name="v5_3_simpler_incognito_mode_descr">Inkognito beim Verbinden einschalten.</string> - <string name="v5_3_discover_join_groups_descr">- Verbindung mit dem Directory-Service (BETA)! -\n- Empfangsbestätigungen (für bis zu 20 Mitglieder). -\n- Schneller und stabiler.</string> + <string name="v5_3_discover_join_groups_descr">- Verbindung mit dem Directory-Service (BETA)!\n- Empfangsbestätigungen (für bis zu 20 Mitglieder).\n- Schneller und stabiler.</string> <string name="member_contact_send_direct_message">Direktnachricht senden</string> <string name="rcv_group_event_member_created_contact">Direkt miteinander verbunden</string> <string name="expand_verb">Erweitern</string> <string name="connect_plan_repeat_connection_request">Verbindungsanfrage wiederholen?</string> <string name="rcv_direct_event_contact_deleted">Gelöschter Kontakt</string> - <string name="connect_plan_you_are_already_connecting_to_vName">Sie sind bereits mit %1$s verbunden.</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Sie sind bereits mit <b>%1$s</b> verbunden.]]></string> <string name="error_alert_title">Fehler</string> <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Sie sind über diesen Link bereits Mitglied der Gruppe.</string> <string name="create_group_button">Gruppe erstellen</string> <string name="create_another_profile_button">Profil erstellen</string> <string name="group_members_2">%s und %s</string> <string name="connect_plan_join_your_group">Ihrer Gruppe beitreten?</string> - <string name="connect_plan_you_are_already_joining_the_group_vName">Sie sind bereits Mitglied in der Gruppe %1$s.</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Sie sind bereits Mitglied in der Gruppe <b>%1$s</b>.]]></string> <string name="connect_plan_this_is_your_own_one_time_link">Das ist Ihr eigener Einmal-Link!</string> <string name="marked_deleted_items_description">%d Nachrichten als gelöscht markiert</string> <string name="connect_plan_group_already_exists">Gruppe besteht bereits!</string> @@ -1506,19 +1476,19 @@ <string name="connect_plan_connect_via_link">Über einen Link verbinden?</string> <string name="connect_plan_already_joining_the_group">Sie sind bereits Mitglied der Gruppe!</string> <string name="group_members_n">%s, %s und %d Mitglieder</string> - <string name="moderated_items_description">%d Nachrichten von %s moderiert</string> + <string name="moderated_items_description">%1$d Nachrichten von %2$s moderiert</string> <string name="unblock_member_button">Mitglied freigeben</string> <string name="connect_plan_connect_to_yourself">Mit Ihnen selbst verbinden?</string> - <string name="contact_tap_to_connect">Zum Verbinden antippen</string> - <string name="connect_plan_you_are_already_in_group_vName">Sie sind bereits Mitglied in der Gruppe %1$s.</string> + <string name="contact_tap_to_connect">Zum Verbinden tippen</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Sie sind bereits Mitglied in der Gruppe <b>%1$s</b>.]]></string> <string name="connect_plan_this_is_your_own_simplex_address">Das ist Ihre eigene SimpleX-Adresse!</string> <string name="correct_name_to">Richtiger Name für %s?</string> - <string name="delete_messages__question">%d Nachrichten löschen?</string> + <string name="delete_messages__question">Sollen %d Nachrichten gelöscht werden?</string> <string name="connect_with_contact_name_question">Mit %1$s verbinden?</string> <string name="remove_member_button">Mitglied entfernen</string> <string name="block_member_confirmation">Blockieren</string> <string name="unblock_member_question">Mitglied freigeben?</string> - <string name="blocked_items_description">%d Nachrichten blockiert</string> + <string name="blocked_items_description">%d Nachrichten wurden blockiert</string> <string name="block_member_button">Mitglied blockieren</string> <string name="connect_plan_repeat_join_request">Verbindungsanfrage wiederholen?</string> <string name="button_remove_member_question">Mitglied entfernen?</string> @@ -1531,13 +1501,13 @@ <string name="block_member_question">Mitglied blockieren?</string> <string name="rcv_group_events_count">%d Gruppenereignisse</string> <string name="invalid_name">Ungültiger Name!</string> - <string name="connect_plan_this_is_your_link_for_group_vName">Das ist Ihr Link für die Gruppe %1$s!</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Das ist Ihr Link für die Gruppe <b>%1$s</b>!]]></string> <string name="unblock_member_confirmation">Freigeben</string> <string name="non_content_uri_alert_title">Ungültiger Datei-Pfad</string> <string name="connect_plan_you_have_already_requested_connection_via_this_address">Sie haben über diese Adresse bereits eine Verbindung beantragt!</string> - <string name="terminal_always_visible">Die Konsole in einem neuen Fenster anzeigen</string> - <string name="block_member_desc">Alle neuen Nachrichten von %s werden ausgeblendet!</string> - <string name="blocked_item_description">blockiert</string> + <string name="terminal_always_visible">Konsole in einem neuen Fenster anzeigen</string> + <string name="block_member_desc">Von %s werden alle neuen Nachrichten ausgeblendet!</string> + <string name="blocked_item_description">Blockiert</string> <string name="encryption_renegotiation_error">Fehler bei der Neuverhandlung der Verschlüsselung</string> <string name="alert_text_encryption_renegotiation_failed">Neuverhandlung der Verschlüsselung fehlgeschlagen</string> <string name="v5_4_block_group_members">Gruppenmitglieder blockieren</string> @@ -1561,14 +1531,14 @@ <string name="bad_desktop_address">Falsche Desktop-Adresse</string> <string name="devices">Geräte</string> <string name="disconnect_desktop_question">Desktop-Verbindung trennen?</string> - <string name="desktop_app_version_is_incompatible">Desktop-App-Version %s ist mit dieser App nicht kompatibel.</string> + <string name="desktop_app_version_is_incompatible">Die Desktop-App-Version %s ist nicht mit dieser App kompatibel.</string> <string name="new_mobile_device">Neues Mobiltelefon-Gerät</string> <string name="only_one_device_can_work_at_the_same_time">Nur ein Gerät kann gleichzeitig genutzt werden</string> <string name="v5_4_link_mobile_desktop">Verknüpfe Mobiltelefon- und Desktop-Apps! 🔗</string> <string name="v5_4_link_mobile_desktop_descr">Über ein sicheres quantenbeständiges Protokoll</string> <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Öffnen Sie in den Einstellungen der Mobiltelefon-App <i>Vom Desktop aus nutzen</i> und scannen Sie den QR-Code.]]></string> <string name="v5_4_block_group_members_descr">Um unerwünschte Nachrichten zu verbergen.</string> - <string name="desktop_incompatible_version">Inkompatible Version</string> + <string name="desktop_incompatible_version">Version nicht kompatibel</string> <string name="new_desktop"><![CDATA[<i>(Neu)</i>]]></string> <string name="unlink_desktop_question">Desktop entkoppeln?</string> <string name="linked_desktop_options">Verknüpfte Desktop-Optionen</string> @@ -1579,7 +1549,7 @@ <string name="v5_4_better_groups_descr">Schnellerer Gruppenbeitritt und zuverlässigere Nachrichtenzustellung.</string> <string name="linked_mobiles">Verknüpfte Mobiltelefone</string> <string name="this_device_name">Dieser Gerätename</string> - <string name="waiting_for_mobile_to_connect">Auf die Mobiltelefonverbindung warten:</string> + <string name="waiting_for_mobile_to_connect">Auf die Verbindung des Mobiltelefons warten:</string> <string name="loading_remote_file_title">Laden der Datei</string> <string name="link_a_mobile">Zu einem Mobiltelefon verbinden</string> <string name="settings_section_title_use_from_desktop">Vom Desktop aus nutzen</string> @@ -1591,9 +1561,7 @@ <string name="paste_desktop_address">Desktop-Adresse einfügen</string> <string name="verify_code_with_desktop">Code mit dem Desktop überprüfen</string> <string name="scan_qr_code_from_desktop">Den QR-Code vom Desktop scannen</string> - <string name="v5_4_more_things_descr">- Optionale Benachrichtigung von gelöschten Kontakten. -\n- Profilnamen mit Leerzeichen. -\n- Und mehr!</string> + <string name="v5_4_more_things_descr">- Optionale Benachrichtigung von gelöschten Kontakten.\n- Profilnamen mit Leerzeichen.\n- Und mehr!</string> <string name="scan_from_mobile">Vom Mobiltelefon scannen</string> <string name="verify_connections">Verbindungen überprüfen</string> <string name="loading_remote_file_desc">Bitte warten Sie, solange die Datei von dem verknüpften Mobiltelefon geladen wird</string> @@ -1603,4 +1571,895 @@ <string name="found_desktop">Gefundener Desktop</string> <string name="not_compatible">Nicht kompatibel!</string> <string name="multicast_discoverable_via_local_network">Über das lokale Netzwerk auffindbar</string> -</resources> \ No newline at end of file + <string name="refresh_qr_code">Aktualisieren</string> + <string name="create_chat_profile">Chat-Profil erstellen</string> + <string name="disconnect_remote_hosts">Mobiltelefone trennen</string> + <string name="no_connected_mobile">Kein Mobiltelefon verbunden</string> + <string name="random_port">Zufällig</string> + <string name="open_port_in_firewall_desc">Um einer Mobiltelefon-App die Verbindung zum Desktop zu ermöglichen, öffnen Sie diesen Port in Ihrer Firewall, falls Sie diese aktiviert haben</string> + <string name="open_port_in_firewall_title">Port in Firewall öffnen</string> + <string name="app_was_crashed">Ansicht abgestürzt</string> + <string name="error_showing_content">Fehler beim Anzeigen des Inhalts</string> + <string name="error_showing_message">Fehler beim Anzeigen der Nachricht</string> + <string name="you_can_make_address_visible_via_settings">Sie können sie über Einstellungen für Ihre SimpleX-Kontakte sichtbar machen.</string> + <string name="recent_history_is_not_sent_to_new_members">Der Nachrichtenverlauf wird nicht an neue Gruppenmitglieder gesendet.</string> + <string name="retry_verb">Wiederholen</string> + <string name="camera_not_available">Kamera nicht verfügbar</string> + <string name="enable_sending_recent_history">Bis zu 100 der letzten Nachrichten an neue Gruppenmitglieder senden.</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Kontakt hinzufügen</b>: Um einen neuen Einladungslink zu erstellen oder eine Verbindung über einen Link herzustellen, den Sie erhalten haben.]]></string> + <string name="disable_sending_recent_history">Den Nachrichtenverlauf nicht an neue Mitglieder senden.</string> + <string name="or_show_this_qr_code">Oder diesen QR-Code anzeigen</string> + <string name="recent_history_is_sent_to_new_members">Bis zu 100 der letzten Nachrichten werden an neue Mitglieder gesendet.</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">Der von Ihnen gescannte Code ist kein SimpleX-Link-QR-Code.</string> + <string name="the_text_you_pasted_is_not_a_link">Der von Ihnen eingefügte Text ist kein SimpleX-Link.</string> + <string name="enable_camera_access">Kamera-Zugriff aktivieren</string> + <string name="you_can_view_invitation_link_again">Den Einladungslink können Sie in den Details der Verbindung nochmals sehen.</string> + <string name="keep_unused_invitation_question">Nicht genutzte Einladung behalten?</string> + <string name="share_this_1_time_link">Teilen Sie diesen Einmal-Einladungslink</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Gruppe erstellen</b>: Um eine neue Gruppe zu erstellen.]]></string> + <string name="recent_history">Sichtbarer Nachrichtenverlauf</string> + <string name="la_app_passcode">App-Zugangscode</string> + <string name="new_chat">Neuer Chat</string> + <string name="loading_chats">Chats werden geladen…</string> + <string name="creating_link">Link wird erstellt…</string> + <string name="or_scan_qr_code">Oder den QR-Code scannen</string> + <string name="invalid_qr_code">Ungültiger QR-Code</string> + <string name="add_contact_tab">Kontakt hinzufügen</string> + <string name="tap_to_scan">Zum Scannen tippen</string> + <string name="keep_invitation_link">Behalten</string> + <string name="tap_to_paste_link">Zum Link einfügen tippen</string> + <string name="search_or_paste_simplex_link">Suchen oder SimpleX-Link einfügen</string> + <string name="chat_is_stopped_you_should_transfer_database">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.</string> + <string name="start_chat_question">Chat starten?</string> + <string name="show_internal_errors">Interne Fehler anzeigen</string> + <string name="remote_ctrl_error_timeout">Während der Verbindung zum Desktop erfolgte eine Zeitüberschreitung</string> + <string name="failed_to_create_user_invalid_title">Ungültiger Anzeigename!</string> + <string name="remote_host_disconnected_from"><![CDATA[Vom Mobiltelefon <b>%s</b> aus folgendem Grund getrennt: %s]]></string> + <string name="remote_host_error_inactive"><![CDATA[Mobiltelefon <b>%s</b> ist nicht aktiv]]></string> + <string name="remote_ctrl_error_bad_state">Die Verbindung zum Desktop ist in einem schlechten Zustand</string> + <string name="remote_host_error_bad_state"><![CDATA[Die Verbindung zum Mobiltelefon <b>%s</b> ist in einem schlechten Zustand]]></string> + <string name="remote_ctrl_error_busy">Desktop ist besetzt</string> + <string name="remote_ctrl_error_inactive">Desktop ist nicht aktiv</string> + <string name="remote_ctrl_error_disconnected">Desktop ist getrennt</string> + <string name="remote_host_error_disconnected"><![CDATA[Mobiltelefon <b>%s</b> wurde getrennt]]></string> + <string name="remote_ctrl_error_bad_version">Auf dem Desktop wird eine nicht unterstützte Version verwendet. Bitte stellen Sie sicher, dass beide Geräte die selbe Version nutzen</string> + <string name="remote_ctrl_error_bad_invitation">Desktop hat einen falschen Einladungscode</string> + <string name="agent_critical_error_title">Kritischer Fehler</string> + <string name="remote_host_error_missing"><![CDATA[Mobiltelefon <b>%s</b> ist nicht vorhanden]]></string> + <string name="agent_internal_error_desc">Bitte melden Sie diesen Fehler den Entwicklern: +\n%s</string> + <string name="agent_critical_error_desc">Bitte melden Sie diesen Fehler den Entwicklern: +\n%s +\n +\nEs wird empfohlen die App neu zu starten.</string> + <string name="restart_chat_button">Chat neu starten</string> + <string name="failed_to_create_user_invalid_desc">Der Anzeigename ist ungültig. Bitte wählen Sie einen anderen Namen.</string> + <string name="remote_host_error_timeout"><![CDATA[Während der Verbindung zum Mobiltelefon <b>%s</b> erfolgte eine Zeitüberschreitung]]></string> + <string name="remote_host_was_disconnected_title">Verbindung wurde getrennt</string> + <string name="remote_ctrl_was_disconnected_title">Verbindung wurde getrennt</string> + <string name="remote_ctrl_disconnected_with_reason">Aus folgendem Grund getrennt: %s</string> + <string name="agent_internal_error_title">Interner Fehler</string> + <string name="remote_host_error_bad_version"><![CDATA[Auf dem Mobiltelefon <b>%s</b> wird eine nicht unterstützte Version verwendet. Bitte stellen Sie sicher, dass beide Geräte die selbe Version nutzen]]></string> + <string name="remote_host_error_busy"><![CDATA[Mobiltelefon <b>%s</b> ist besetzt]]></string> + <string name="past_member_vName">Ehemaliges Mitglied %1$s</string> + <string name="possible_slow_function_desc">Die Ausführung dieser Funktion dauert zu lange: %1$d Sekunden: %2$s</string> + <string name="possible_slow_function_title">Langsame Funktion</string> + <string name="show_slow_api_calls">Langsame API-Aufrufe anzeigen</string> + <string name="group_member_status_unknown_short">unbekannt</string> + <string name="developer_options_section">Optionen für Entwickler</string> + <string name="group_member_status_unknown">unbekannter Gruppenmitglieds-Status</string> + <string name="v5_5_private_notes_descr">Mit verschlüsselten Dateien und Medien.</string> + <string name="v5_5_private_notes">Private Notizen</string> + <string name="clear_note_folder_warning">Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden!</string> + <string name="clear_note_folder_question">Private Notizen entfernen?</string> + <string name="rcv_group_event_member_blocked">es wurde %s blockiert</string> + <string name="rcv_group_event_member_unblocked">Es wurden %s freigegeben</string> + <string name="snd_group_event_member_blocked">Sie haben %s blockiert</string> + <string name="snd_group_event_member_unblocked">Sie haben %s freigegeben</string> + <string name="block_for_all_question">Mitglied für Alle blockieren?</string> + <string name="saved_message_title">Gespeicherte Nachricht</string> + <string name="v5_5_simpler_connect_ui">Zum Verbinden den Link einfügen!</string> + <string name="v5_5_message_delivery_descr">Mit reduziertem Akkuverbrauch.</string> + <string name="v5_5_join_group_conversation_descr">Aktueller Nachrichtenverlauf und verbesserter Gruppenverzeichnis-Bot.</string> + <string name="v5_5_simpler_connect_ui_descr">In der Suchleiste werden nun auch Einladungslinks akzeptiert.</string> + <string name="unblock_for_all">Für Alle freigeben</string> + <string name="unblock_for_all_question">Mitglied für Alle freigeben?</string> + <string name="member_info_member_blocked">wurde blockiert</string> + <string name="blocked_by_admin_item_description">ist vom Administrator blockiert worden</string> + <string name="member_blocked_by_admin">wurde vom Administrator blockiert</string> + <string name="block_for_all">Für Alle blockiert</string> + <string name="info_row_created_at">Erstellt um</string> + <string name="share_text_created_at">Erstellt um: %s</string> + <string name="blocked_by_admin_items_description">%d Nachrichten wurden vom Administrator blockiert</string> + <string name="error_blocking_member_for_all">Fehler beim Blockieren des Mitglieds für Alle</string> + <string name="error_creating_message">Fehler beim Erstellen der Nachricht</string> + <string name="error_deleting_note_folder">Fehler beim Löschen der privaten Notizen</string> + <string name="v5_5_message_delivery">Verbesserte Zustellung von Nachrichten</string> + <string name="v5_5_join_group_conversation">Gruppenunterhaltungen beitreten</string> + <string name="v5_5_new_interface_languages">Ungarische und türkische Bedienoberfläche</string> + <string name="profile_update_event_contact_name_changed">Der Kontaktname wurde von %1$s auf %2$s geändert</string> + <string name="profile_update_event_member_name_changed">Der Mitgliedsname von %1$s wurde auf %2$s geändert</string> + <string name="profile_update_event_set_new_address">Es wurde eine neue Kontaktadresse festgelegt</string> + <string name="profile_update_event_set_new_picture">Es wurde ein neues Profil-Bild festgelegt</string> + <string name="profile_update_event_updated_profile">Das Profil wurde aktualisiert</string> + <string name="profile_update_event_removed_picture">Das Profil-Bild wurde entfernt</string> + <string name="profile_update_event_removed_address">Die Kontaktadresse wurde entfernt</string> + <string name="note_folder_local_display_name">Private Notizen</string> + <string name="database_migration_in_progress">Momentan wird eine Datenbank-Migration durchgeführt. +\nDies kann einige Minuten andauern.</string> + <string name="message_too_large">Die Nachricht ist zu lang</string> + <string name="welcome_message_is_too_long">Die Begrüßungsmeldung ist zu lang</string> + <string name="call_service_notification_audio_call">Audioanruf</string> + <string name="call_service_notification_end_call">Anruf beenden</string> + <string name="call_service_notification_video_call">Videoanruf</string> + <string name="unable_to_open_browser_title">Fehler beim Öffnen des Browsers</string> + <string name="unable_to_open_browser_desc">Für Anrufe ist ein Default-Webbrowser erforderlich. Bitte konfigurieren Sie einen Default-Browser für das System und teilen Sie den Entwicklern mehr Informationen dazu mit.</string> + <string name="migrate_to_device_chat_migrated">Chat wurde migriert!</string> + <string name="migrate_from_device_creating_archive_link">Archiv-Link erzeugen</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Wählen Sie auf dem neuen Gerät <i>Von einem anderen Gerät migrieren</i> und scannen Sie den QR-Code.]]></string> + <string name="migrate_from_device_delete_database_from_device">Datenbank auf diesem Gerät löschen</string> + <string name="v5_6_app_data_migration">App-Daten-Migration</string> + <string name="v5_6_safer_groups_descr">Administratoren können ein Gruppenmitglied für Alle blockieren.</string> + <string name="migrate_to_device_downloading_details">Link-Details werden heruntergeladen</string> + <string name="migrate_to_device_downloading_archive">Archiv wird heruntergeladen</string> + <string name="migrate_to_device_apply_onion">Anwenden</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Alle Ihre Kontakte, Unterhaltungen und Dateien werden sicher verschlüsselt und in Daten-Paketen auf die konfigurierten XTFP-Relais hochgeladen.</string> + <string name="migrate_from_device_archive_and_upload">Archivieren und Hochladen</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Warnung</b>: Das Archiv wird gelöscht.]]></string> + <string name="migrate_from_device_check_connection_and_try_again">Überprüfen Sie Ihre Internetverbindung und probieren Sie es nochmals</string> + <string name="migrate_from_device_archiving_database">Datenbank wird archiviert</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Bitte beachten Sie</b>: Aus Sicherheitsgründen wird die Nachrichtenentschlüsselung Ihrer Verbindungen abgebrochen, wenn Sie die gleiche Datenbank auf zwei Geräten nutzen.]]></string> + <string name="migrate_from_device_cancel_migration">Migration abbrechen</string> + <string name="migrate_to_device_confirm_network_settings">Bestätigen Sie die Netzwerkeinstellungen</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Bitte bestätigen Sie für die Migration, dass Sie sich an Ihr Datenbank-Passwort erinnern.</string> + <string name="migrate_from_device_confirm_upload">Hochladen bestätigen</string> + <string name="migrate_to_device_download_failed">Herunterladen fehlgeschlagen</string> + <string name="e2ee_info_no_pq"><![CDATA[Nachrichten, Dateien und Anrufe sind durch <b>Ende-zu-Ende-Verschlüsselung</b> mit Perfect Forward Secrecy, Abstreitbarkeit und Wiederherstellung nach einer Kompromittierung geschützt.]]></string> + <string name="e2ee_info_pq"><![CDATA[Nachrichten, Dateien und Anrufe sind durch <b>Quantum-resistente E2E-Verschlüsselung</b> mit Perfect Forward Secrecy, Abstreitbarkeit und Wiederherstellung nach einer Kompromittierung geschützt.]]></string> + <string name="e2ee_info_no_pq_short">Dieser Chat ist durch Ende-zu-Ende-Verschlüsselung geschützt.</string> + <string name="e2ee_info_pq_short">Dieser Chat ist durch Quantum-resistente Ende-zu-Ende-Verschlüsselung geschützt.</string> + <string name="auth_open_migration_to_another_device">Migrationsansicht öffnen</string> + <string name="set_passphrase">Passwort festlegen</string> + <string name="conn_event_enabled_pq">Quantum-resistente E2E-Verschlüsselung</string> + <string name="or_paste_archive_link">Oder fügen Sie den Archiv-Link ein</string> + <string name="paste_archive_link">Archiv-Link einfügen</string> + <string name="invalid_file_link">Ungültiger Link</string> + <string name="migrate_to_device_migrating">Migrieren</string> + <string name="migrate_to_device_bytes_downloaded">%s heruntergeladen</string> + <string name="migrate_to_device_finalize_migration">Die Migration auf dem anderen Gerät wird abgeschlossen.</string> + <string name="migrate_to_device_error_downloading_archive">Fehler beim Herunterladen des Archivs</string> + <string name="migrate_from_device_finalize_migration">Die Migration wird abgeschlossen</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Warnung: Das Starten des Chats auf mehreren Geräten wird nicht unterstützt und wird zu Fehlern bei der Nachrichtenübermittlung führen</string> + <string name="migrate_from_device_start_chat">Chat starten</string> + <string name="v5_6_quantum_resistant_encryption_descr">Kann in direkten Chats aktiviert werden (BETA)!</string> + <string name="v5_6_quantum_resistant_encryption">Quantum-resistente Verschlüsselung</string> + <string name="v5_6_app_data_migration_descr">Daten können über einen QR-Code auf ein anderes Gerät migriert werden.</string> + <string name="v5_6_picture_in_picture_calls">Bild-in-Bild-Anrufe</string> + <string name="v5_6_safer_groups">Sicherere Gruppen</string> + <string name="v5_6_picture_in_picture_calls_descr">Die App kann während eines Anrufs genutzt werden.</string> + <string name="migrate_to_device_title">Hierher migrieren</string> + <string name="migrate_to_device_database_init">Das Herunterladen wird vorbereitet</string> + <string name="migrate_to_device_repeat_download">Herunterladen wiederholen</string> + <string name="migrate_to_device_try_again">Sie können es nochmal probieren.</string> + <string name="migrate_to_device_enter_passphrase">Passwort eingeben</string> + <string name="migrate_to_device_importing_archive">Archiv wird importiert</string> + <string name="migrate_to_device_repeat_import">Import wiederholen</string> + <string name="migrate_to_device_file_delete_or_link_invalid">Die Datei wurde gelöscht oder der Link ist ungültig</string> + <string name="migrate_from_device_title">Gerät migrieren</string> + <string name="migrate_from_device_to_another_device">Auf ein anderes Gerät migrieren</string> + <string name="migrate_from_device_error_exporting_archive">Fehler beim Exportieren der Chat-Datenbank</string> + <string name="migrate_from_device_error_saving_settings">Fehler beim Abspeichern der Einstellungen</string> + <string name="migrate_from_device_exported_file_doesnt_exist">Die exportierte Datei ist nicht vorhanden</string> + <string name="migrate_from_device_error_deleting_database">Fehler beim Löschen der Datenbank</string> + <string name="migrate_from_device_error_uploading_archive">Fehler beim Hochladen des Archivs</string> + <string name="migrate_from_device_database_init">Das Hochladen wird vorbereitet</string> + <string name="migrate_from_device_chat_should_be_stopped">Um fortzufahren, sollte der Chat beendet werden.</string> + <string name="migrate_from_device_bytes_uploaded">%s hochgeladen</string> + <string name="migrate_from_device_uploading_archive">Archiv wird hochgeladen</string> + <string name="migrate_from_device_stopping_chat">Chat wird beendet</string> + <string name="migrate_from_device_or_share_this_file_link">Oder teilen Sie diesen Datei-Link sicher</string> + <string name="migrate_from_device_repeat_upload">Hochladen wiederholen</string> + <string name="migrate_from_device_upload_failed">Hochladen fehlgeschlagen</string> + <string name="migrate_from_device_verify_database_passphrase">Überprüfen Sie das Datenbank-Passwort</string> + <string name="migrate_from_device_try_again">Sie können es nochmal probieren.</string> + <string name="migrate_from_device_error_verifying_passphrase">Fehler bei der Überprüfung des Passworts:</string> + <string name="migrate_from_device_verify_passphrase">Überprüfen Sie das Passwort</string> + <string name="migrate_from_another_device">Von einem anderen Gerät migrieren</string> + <string name="migrate_to_device_import_failed">Import ist fehlgeschlagen</string> + <string name="migrate_from_device_migration_complete">Migration abgeschlossen</string> + <string name="migrate_to_device_confirm_network_settings_footer">Bitte bestätigen Sie, dass die Netzwerkeinstellungen auf diesem Gerät richtig sind.</string> + <string name="conn_event_disabled_pq">Standard-Ende-zu-Ende-Verschlüsselung</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[Sie dürfen die selbe Datenbank <b>nicht</b> auf zwei Geräten nutzen.]]></string> + <string name="error_showing_desktop_notification">Fehler beim Anzeigen der Benachrichtigung. Bitte kontaktieren Sie die Entwickler.</string> + <string name="permissions_find_in_settings_and_grant">Sie finden diese Berechtigung in den Android Einstellungen vor und müssen Sie manuell erteilen.</string> + <string name="permissions_grant_in_settings">In den Einstellungen erteilen</string> + <string name="permissions_required">Berechtigungen erteilen</string> + <string name="permissions_record_audio">Mikrofon</string> + <string name="permissions_open_settings">Einstellungen öffnen</string> + <string name="permissions_camera">Kamera</string> + <string name="permissions_camera_and_record_audio">Kamera und Mikrofon</string> + <string name="permissions_grant">Erteilen Sie die Berechtigung(en), um Anrufe zu tätigen</string> + <string name="forwarded_description">weitergeleitet</string> + <string name="settings_section_title_network_connection">Netzwerkverbindung</string> + <string name="network_type_no_network_connection">Keine Netzwerkverbindung</string> + <string name="network_type_cellular">Mobilfunknetz</string> + <string name="network_type_other">Andere</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="network_type_ethernet">Kabelgebundenes Netzwerk</string> + <string name="files_and_media_not_allowed">Dateien und Medien sind nicht erlaubt</string> + <string name="simplex_links_not_allowed">SimpleX-Links sind nicht erlaubt</string> + <string name="voice_messages_not_allowed">Sprachnachrichten sind nicht erlaubt</string> + <string name="simplex_links">SimpleX-Links</string> + <string name="group_members_can_send_simplex_links">Mitglieder können SimpleX-Links versenden.</string> + <string name="feature_roles_admins">Administratoren</string> + <string name="feature_roles_all_members">Alle Mitglieder</string> + <string name="feature_enabled_for">Aktiviert für</string> + <string name="feature_roles_owners">Eigentümer</string> + <string name="simplex_links_are_prohibited_in_group">SimpleX-Links sind nicht erlaubt.</string> + <string name="prohibit_sending_simplex_links">Das Senden von SimpleX-Links nicht erlauben.</string> + <string name="allow_to_send_simplex_links">Das Senden von SimpleX-Links erlauben.</string> + <string name="audio_device_speaker">Lautsprecher</string> + <string name="audio_device_earpiece">Kopfhörer</string> + <string name="forward_chat_item">Weiterleiten</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="forwarded_from_chat_item_info_title">Weitergeleitet aus</string> + <string name="forward_message">Nachricht weiterleiten…</string> + <string name="audio_device_wired_headphones">Kopfhörer</string> + <string name="network_option_rcv_concurrency">Gelijktijdige ontvangst</string> + <string name="recipients_can_not_see_who_message_from">Empfänger können nicht sehen, von wem die Nachricht stammt.</string> + <string name="saved_from_description">abgespeichert von %s</string> + <string name="saved_chat_item_info_tab">Abgespeichert</string> + <string name="saved_description">abgespeichert</string> + <string name="forwarded_chat_item_info_tab">Weitergeleitet</string> + <string name="saved_from_chat_item_info_title">Abgespeichert von</string> + <string name="download_file">Herunterladen</string> + <string name="v5_7_forward">Nachrichten weiterleiten und speichern</string> + <string name="v5_7_call_sounds">Klingeltöne</string> + <string name="v5_7_forward_descr">Die Nachrichtenquelle bleibt privat.</string> + <string name="v5_7_network">Netzwerk-Verwaltung</string> + <string name="v5_7_call_sounds_descr">Bei der Verbindung über Audio- und Video-Anrufe.</string> + <string name="v5_7_new_interface_languages">Litauische Bedienoberfläche</string> + <string name="v5_7_network_descr">Zuverlässigere Netzwerkverbindung.</string> + <string name="v5_7_quantum_resistant_encryption_descr">Wird in direkten Chats automatisch aktiviert!</string> + <string name="settings_section_title_profile_images">Profil-Bilder</string> + <string name="v5_7_shape_profile_images">Form der Profil-Bilder</string> + <string name="v5_7_shape_profile_images_descr">Quadratisch, kreisförmig oder irgendetwas dazwischen.</string> + <string name="snd_error_relay">Fehler auf dem Ziel-Server: %1$s</string> + <string name="ci_status_other_error">Fehler: %1$s</string> + <string name="message_delivery_warning_title">Warnung bei der Nachrichtenzustellung</string> + <string name="snd_error_auth">Falscher Schlüssel oder unbekannte Verbindung - höchstwahrscheinlich ist diese Verbindung gelöscht.</string> + <string name="srv_error_version">Die Server-Version ist nicht mit den Netzwerkeinstellungen kompatibel.</string> + <string name="snd_error_quota">Kapazität überschritten - der Empfänger hat die zuvor gesendeten Nachrichten nicht empfangen.</string> + <string name="snd_error_proxy_relay">Weiterleitungs-Server: %1$s +\nFehler auf dem Ziel-Server: %2$s</string> + <string name="snd_error_proxy">Weiterleitungs-Server: %1$s +\nFehler: %2$s</string> + <string name="snd_error_expired">Netzwerk-Fehler - die Nachricht ist nach vielen Sende-Versuchen abgelaufen.</string> + <string name="srv_error_host">Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel.</string> + <string name="network_smp_proxy_mode_always">Immer</string> + <string name="network_smp_proxy_mode_private_routing">Privates Routing</string> + <string name="network_smp_proxy_mode_never">Nie</string> + <string name="network_smp_proxy_mode_unknown">Unbekannte Server</string> + <string name="network_smp_proxy_mode_unprotected">Ungeschützt</string> + <string name="network_smp_proxy_mode_unknown_description">Sie nutzen privates Routing mit unbekannten Servern.</string> + <string name="network_smp_proxy_mode_never_description">Sie nutzen KEIN privates Routing.</string> + <string name="update_network_smp_proxy_mode_question">Modus für das Nachrichten-Routing</string> + <string name="network_smp_proxy_fallback_allow">Ja</string> + <string name="network_smp_proxy_fallback_prohibit">Nein</string> + <string name="network_smp_proxy_fallback_allow_protected">Wenn die IP-Adresse versteckt ist</string> + <string name="update_network_smp_proxy_fallback_question">Fallback für das Nachrichten-Routing</string> + <string name="private_routing_show_message_status">Nachrichtenstatus anzeigen</string> + <string name="network_smp_proxy_fallback_allow_downgrade">Herabstufung erlauben</string> + <string name="network_smp_proxy_mode_always_description">Sie nutzen immer privates Routing.</string> + <string name="network_smp_proxy_fallback_prohibit_description">Nachrichten werden nicht direkt versendet, selbst wenn Ihr oder der Ziel-Server kein privates Routing unterstützt.</string> + <string name="settings_section_title_private_message_routing">PRIVATES NACHRICHTEN-ROUTING</string> + <string name="network_smp_proxy_fallback_allow_protected_description">Nachrichten werden direkt versendet, wenn die IP-Adresse geschützt ist, und Ihr oder der Ziel-Server kein privates Routing unterstützt.</string> + <string name="network_smp_proxy_fallback_allow_description">Nachrichten werden direkt versendet, wenn Ihr oder der Ziel-Server kein privates Routing unterstützt.</string> + <string name="private_routing_explanation">Zum Schutz Ihrer IP-Adresse, wird für die Nachrichten-Auslieferung privates Routing über Ihre konfigurierten SMP-Server genutzt.</string> + <string name="network_smp_proxy_mode_unprotected_description">Sie nutzen privates Routing mit unbekannten Servern, wenn Ihre IP-Adresse nicht geschützt ist.</string> + <string name="protect_ip_address">IP-Adresse schützen</string> + <string name="settings_section_title_files">DATEIEN</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">Die App wird bei unbekannten Datei-Servern nach einer Download-Bestätigung fragen (außer bei .onion oder wenn ein SOCKS-Proxy aktiviert ist).</string> + <string name="file_not_approved_title">Unbekannte Server!</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für Datei-Server sichtbar sein.</string> + <string name="file_not_approved_descr">Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für diese XFTP-Relais sichtbar sein: +\n%1$s.</string> + <string name="settings_section_title_user_theme">Profil-Design</string> + <string name="theme_black">Schwarz</string> + <string name="color_mode">Farbvariante</string> + <string name="color_mode_dark">Dunkel</string> + <string name="color_mode_light">Hell</string> + <string name="reset_single_color">Farbe zurücksetzen</string> + <string name="color_sent_quote">Gesendete Antwort</string> + <string name="color_mode_system">System</string> + <string name="chat_theme_apply_to_dark_mode">Dunkle Variante</string> + <string name="wallpaper_scale_fill">Füllen</string> + <string name="chat_theme_apply_to_light_mode">Helle Variante</string> + <string name="color_received_quote">Empfangene Antwort</string> + <string name="theme_remove_image">Bild entfernen</string> + <string name="wallpaper_scale_repeat">Wiederholen</string> + <string name="wallpaper_scale">Skalieren</string> + <string name="chat_theme_set_default_theme">Default-Design festlegen</string> + <string name="color_wallpaper_tint">Wallpaper-Akzent</string> + <string name="color_wallpaper_background">Wallpaper-Hintergrund</string> + <string name="chat_theme_apply_to_mode">Anwenden auf</string> + <string name="color_primary_variant2">Zusätzlicher Akzent 2</string> + <string name="wallpaper_advanced_settings">Erweiterte Einstellungen</string> + <string name="chat_theme_apply_to_all_modes">Alle Farbvarianten</string> + <string name="settings_section_title_chat_colors">Chat-Farben</string> + <string name="settings_section_title_chat_theme">Chat-Design</string> + <string name="wallpaper_scale_fit">Passend</string> + <string name="chat_list_always_visible">Chat-Liste in einem neuen Fenster anzeigen</string> + <string name="wallpaper_preview_hello_alice">Guten Nachmittag!</string> + <string name="wallpaper_preview_hello_bob">Guten Morgen!</string> + <string name="dark_mode_colors">Farben für die dunkle Variante</string> + <string name="theme_destination_app_theme">App-Design</string> + <string name="v5_8_persian_ui">Persische Bedienoberfläche</string> + <string name="chat_theme_reset_to_user_theme">Auf das Benutzer-spezifische Design zurücksetzen</string> + <string name="error_initializing_web_view">Fehler bei der Initialisierung von Webview. Aktualisieren Sie Ihr System auf die neue Version. Bitte kontaktieren Sie die Entwickler. +\nFehler: %s</string> + <string name="chat_theme_reset_to_app_theme">Auf das App-Design zurücksetzen</string> + <string name="v5_8_safe_files_descr">Dateien von unbekannten Servern bestätigen.</string> + <string name="v5_8_message_delivery">Verbesserte Zustellung von Nachrichten</string> + <string name="v5_8_chat_themes_descr">Gestalten Sie Ihre Chats unterschiedlich!</string> + <string name="v5_8_chat_themes">Neue Chat-Designs</string> + <string name="v5_8_private_routing">Privates Nachrichten-Routing 🚀</string> + <string name="v5_8_private_routing_descr">Schützen Sie Ihre IP-Adresse vor den Nachrichten-Relais , die Ihr Kontakt ausgewählt hat. +\nAktivieren Sie es in den *Netzwerk & Server* Einstellungen.</string> + <string name="v5_8_safe_files">Dateien sicher herunterladen</string> + <string name="v5_8_message_delivery_descr">Mit reduziertem Akkuverbrauch.</string> + <string name="message_queue_info_none">Keine Information</string> + <string name="info_row_debug_delivery">Debugging-Zustellung</string> + <string name="message_queue_info">Nachrichten-Warteschlangen-Information</string> + <string name="message_queue_info_server_info">Server-Warteschlangen-Information: %1$s +\n +\nZuletzt empfangene Nachricht: %2$s</string> + <string name="file_error_no_file">Datei nicht gefunden - höchstwahrscheinlich wurde die Datei gelöscht oder der Transfer abgebrochen.</string> + <string name="file_error_relay">Datei-Server Fehler: %1$s</string> + <string name="file_error_auth">Falscher Schlüssel oder unbekannte Daten-Paketadresse der Datei - höchstwahrscheinlich wurde die Datei gelöscht.</string> + <string name="file_error">Datei-Fehler</string> + <string name="info_row_message_status">Nachrichten-Status</string> + <string name="share_text_message_status">Nachrichten-Status: %s</string> + <string name="info_row_file_status">Datei-Status</string> + <string name="share_text_file_status">Datei-Status: %s</string> + <string name="temporary_file_error">Temporärer Datei-Fehler</string> + <string name="copy_error">Fehlermeldung kopieren</string> + <string name="remote_ctrl_connection_stopped_identity_desc">Dieser Link wurde schon mit einem anderen Mobiltelefon genutzt. Bitte erstellen sie einen neuen Link in der Desktop-App.</string> + <string name="remote_ctrl_connection_stopped_desc">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.</string> + <string name="cannot_share_message_alert_title">Nachricht wurde nicht gesendet</string> + <string name="cannot_share_message_alert_text">Diese Nachricht ist wegen der gewählten Chat-Einstellungen nicht erlaubt.</string> + <string name="please_try_later">Bitte versuchen Sie es später erneut.</string> + <string name="private_routing_error">Fehler beim privaten Routing</string> + <string name="member_inactive_desc">Die Nachricht kann später zugestellt werden, wenn das Mitglied aktiv wird.</string> + <string name="message_forwarded_desc">Bisher keine direkte Verbindung. Nachricht wird von einem Admin weitergeleitet.</string> + <string name="smp_servers_configured">Konfigurierte SMP-Server</string> + <string name="xftp_servers_configured">Konfigurierte XFTP-Server</string> + <string name="smp_servers_other">Andere SMP-Server</string> + <string name="xftp_servers_other">Andere XFTP-Server</string> + <string name="completed">Abgeschlossen</string> + <string name="member_info_member_inactive">Inaktiv</string> + <string name="servers_info_sessions_connected">Verbunden</string> + <string name="servers_info_sessions_connecting">Verbinden</string> + <string name="servers_info_subscriptions_connections_subscribed">Aktive Verbindungen</string> + <string name="current_user">Aktuelles Profil</string> + <string name="servers_info_detailed_statistics">Detaillierte Statistiken</string> + <string name="servers_info_details">Details</string> + <string name="servers_info_downloaded">Heruntergeladen</string> + <string name="servers_info_modal_error_title">Fehler</string> + <string name="servers_info_reconnect_servers_error">Fehler beim Wiederherstellen der Verbindungen zu den Servern</string> + <string name="servers_info_reset_stats_alert_error_title">Fehler beim Zurücksetzen der Statistiken</string> + <string name="servers_info_sessions_errors">Fehler</string> + <string name="servers_info_messages_received">Empfangene Nachrichten</string> + <string name="servers_info_subscriptions_section_header">Nachrichtenempfang</string> + <string name="servers_info_subscriptions_connections_pending">Ausstehend</string> + <string name="servers_info_previously_connected_servers_section_header">Bisher verbundene Server</string> + <string name="servers_info_proxied_servers_section_header">Proxy-Server</string> + <string name="servers_info_detailed_statistics_received_messages_header">Empfangene Nachrichten</string> + <string name="servers_info_detailed_statistics_received_total">Summe aller empfangenen Nachrichten</string> + <string name="acknowledgement_errors">Fehler bei der Bestätigung</string> + <string name="attempts_label">Versuche</string> + <string name="chunks_deleted">Daten-Pakete gelöscht</string> + <string name="chunks_uploaded">Daten-Pakete hochgeladen</string> + <string name="connections">Verbindungen</string> + <string name="created">Erstellt</string> + <string name="decryption_errors">Entschlüsselungs-Fehler</string> + <string name="deletion_errors">Fehler beim Löschen</string> + <string name="downloaded_files">Heruntergeladene Dateien</string> + <string name="download_errors">Fehler beim Herunterladen</string> + <string name="duplicates_label">Duplikate</string> + <string name="expired_label">Abgelaufen</string> + <string name="open_server_settings_button">Server-Einstellungen öffnen</string> + <string name="other_errors">Andere Fehler</string> + <string name="proxied">Proxy</string> + <string name="servers_info_detailed_statistics_receive_errors">Fehler beim Empfang</string> + <string name="reconnect">Neu verbinden</string> + <string name="app_check_for_updates_disabled">Deaktiviert</string> + <string name="app_check_for_updates_beta">Beta</string> + <string name="app_check_for_updates_download_started">App-Aktualisierung wird heruntergeladen. App nicht schließen!</string> + <string name="app_check_for_updates_button_download">Heruntergeladen %s (%s)</string> + <string name="app_check_for_updates_notice_title">Nach Aktualisierungen suchen</string> + <string name="app_check_for_updates_notice_disable">Deaktivieren</string> + <string name="app_check_for_updates_installed_successfully_title">Erfolgreich installiert</string> + <string name="app_check_for_updates_button_install">Aktualisierung installieren</string> + <string name="app_check_for_updates_installed_successfully_desc">Bitte starten Sie die App neu.</string> + <string name="acknowledged">Bestätigt</string> + <string name="all_users">Alle Profile</string> + <string name="app_check_for_updates_download_completed_title">App-Aktualisierung wurde heruntergeladen</string> + <string name="app_check_for_updates">Nach Aktualisierungen suchen</string> + <string name="chunks_downloaded">Daten-Pakete heruntergeladen</string> + <string name="servers_info_connected_servers_section_header">Verbundene Server</string> + <string name="deleted">Gelöscht</string> + <string name="member_info_member_disabled">deaktiviert</string> + <string name="servers_info_reconnect_server_error">Fehler beim Wiederherstellen der Verbindung zum Server</string> + <string name="message_forwarded_title">Nachricht weitergeleitet</string> + <string name="servers_info_files_tab">Dateien</string> + <string name="appearance_font_size">Schriftgröße</string> + <string name="member_inactive_title">Mitglied inaktiv</string> + <string name="servers_info_messages_sent">Gesendete Nachrichten</string> + <string name="servers_info_missing">Keine Information - es wird versucht neu zu laden</string> + <string name="app_check_for_updates_button_open">Dateispeicherort öffnen</string> + <string name="other_label">andere</string> + <string name="network_error_broker_host_desc">Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel: %1$s.</string> + <string name="scan_paste_link">Link scannen / einfügen</string> + <string name="servers_info_reconnect_all_servers_button">Alle Server neu verbinden</string> + <string name="servers_info_reconnect_server_title">Server neu verbinden?</string> + <string name="servers_info_reconnect_servers_title">Alle Server neu verbinden?</string> + <string name="servers_info_reconnect_servers_message">Um die Auslieferung von Nachrichten zu erzwingen, werden alle Server neu verbunden. Dafür wird weiterer Datenverkehr benötigt.</string> + <string name="servers_info_reconnect_server_message">Um die Auslieferung von Nachrichten zu erzwingen, wird der Server neu verbunden. Dafür wird weiterer Datenverkehr benötigt.</string> + <string name="servers_info_reset_stats_alert_confirm">Zurücksetzen</string> + <string name="servers_info_reset_stats">Alle Statistiken zurücksetzen</string> + <string name="servers_info_reset_stats_alert_title">Alle Statistiken zurücksetzen?</string> + <string name="servers_info_detailed_statistics_sent_messages_header">Gesendete Nachrichten</string> + <string name="servers_info_detailed_statistics_sent_messages_total">Summe aller gesendeten Nachrichten</string> + <string name="secured">Abgesichert</string> + <string name="send_errors">Fehler beim Senden</string> + <string name="sent_directly">Direkt gesendet</string> + <string name="sent_via_proxy">Über einen Proxy gesendet</string> + <string name="server_address">Server-Adresse</string> + <string name="app_check_for_updates_button_remind_later">Später erinnern</string> + <string name="servers_info">Server-Informationen</string> + <string name="network_error_broker_version_desc">Ihre App ist nicht mit der Server-Version kompatibel: %1$s.</string> + <string name="subscription_percentage">Prozentualen Anteil anzeigen</string> + <string name="appearance_zoom">Zoom</string> + <string name="smp_server">SMP-Server</string> + <string name="servers_info_target">Informationen zeigen für</string> + <string name="servers_info_private_data_disclaimer">Beginnend mit %s.\nAlle Daten werden nur auf Ihrem Gerät gespeichert.</string> + <string name="servers_info_statistics_section_header">Statistiken</string> + <string name="servers_info_transport_sessions_section_header">Transport-Sitzungen</string> + <string name="servers_info_uploaded">Hochgeladen</string> + <string name="servers_info_proxied_servers_section_footer">Sie sind nicht mit diesen Servern verbunden. Zur Auslieferung von Nachrichten an diese Server wird privates Routing genutzt.</string> + <string name="servers_info_subscriptions_total">Summe aller Abonnements</string> + <string name="servers_info_reset_stats_alert_message">Die Server-Statistiken werden zurückgesetzt. Dies kann nicht rückgängig gemacht werden!</string> + <string name="size">Größe</string> + <string name="servers_info_starting_from">Beginnend mit %s.</string> + <string name="subscribed">Abonniert</string> + <string name="subscription_errors">Fehler beim Abonnieren</string> + <string name="subscription_results_ignored">Nicht beachtete Abonnements</string> + <string name="uploaded_files">Hochgeladene Dateien</string> + <string name="upload_errors">Fehler beim Hochladen</string> + <string name="xftp_server">XFTP-Server</string> + <string name="app_check_for_updates_stable">Stabil</string> + <string name="app_check_for_updates_button_skip">Diese Version überspringen</string> + <string name="app_check_for_updates_update_available">Aktualisierung verfügbar: %s</string> + <string name="app_check_for_updates_notice_desc">Aktivieren Sie die periodische Überprüfung auf stabile oder Beta-Versionen der App, um über neue Versionen benachrichtigt zu werden.</string> + <string name="app_check_for_updates_canceled">Herunterladen der Aktualisierung abgebrochen</string> + <string name="smp_proxy_error_broker_host">Die Weiterleitungs-Server-Adresse ist nicht kompatibel mit den Netzwerkeinstellungen: %1$s.</string> + <string name="proxy_destination_error_failed_to_connect">Die Verbindung des Weiterleitungs-Servers %1$s zum Ziel-Server %2$s schlug fehl. Bitte versuchen Sie es später erneut.</string> + <string name="proxy_destination_error_broker_version">Die Ziel-Server-Version von %1$s ist nicht mit dem Weiterleitungs-Server %2$s kompatibel.</string> + <string name="smp_proxy_error_broker_version">Die Weiterleitungs-Server-Version ist nicht kompatibel mit den Netzwerkeinstellungen: %1$s.</string> + <string name="proxy_destination_error_broker_host">Die Ziel-Server-Adresse von %1$s ist nicht mit den Einstellungen des Weiterleitungs-Servers %2$s kompatibel.</string> + <string name="smp_proxy_error_connecting">Fehler beim Verbinden zum Weiterleitungs-Server %1$s. Bitte versuchen Sie es später erneut.</string> + <string name="privacy_media_blur_radius">Medium verpixeln</string> + <string name="privacy_media_blur_radius_soft">Weich</string> + <string name="privacy_media_blur_radius_strong">Stark</string> + <string name="privacy_media_blur_radius_medium">Mittel</string> + <string name="privacy_media_blur_radius_off">Aus</string> + <string name="info_view_connect_button">Verbinden</string> + <string name="info_view_message_button">Nachricht</string> + <string name="info_view_open_button">Öffnen</string> + <string name="conversation_deleted">Chat-Inhalte gelöscht!</string> + <string name="only_delete_conversation">Nur die Chat-Inhalte löschen</string> + <string name="info_view_search_button">Suchen</string> + <string name="info_view_video_button">Video</string> + <string name="you_can_still_view_conversation_with_contact">Sie können in der Chat-Liste weiterhin die Unterhaltung mit %1$s einsehen.</string> + <string name="paste_link">Link einfügen</string> + <string name="deleted_chats">Archivierte Kontakte</string> + <string name="no_filtered_contacts">Keine gefilterten Kontakte</string> + <string name="contact_list_header_title">Ihre Kontakte</string> + <string name="one_hand_ui">App-Symbolleiste unten</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">Bitten Sie Ihren Kontakt darum, Anrufe zu aktivieren.</string> + <string name="you_need_to_allow_calls">Sie müssen Ihrem Kontakt Anrufe zu Ihnen erlauben, bevor Sie ihn selbst anrufen können.</string> + <string name="allow_calls_question">Anrufe erlauben?</string> + <string name="info_view_call_button">Anrufen</string> + <string name="cant_call_contact_alert_title">Kontakt kann nicht angerufen werden</string> + <string name="calls_prohibited_alert_title">Anrufe nicht zugelassen!</string> + <string name="confirm_delete_contact_question">Löschen des Kontakts bestätigen?</string> + <string name="contact_deleted">Kontakt gelöscht!</string> + <string name="cant_call_member_alert_title">Gruppenmitglied kann nicht angerufen werden</string> + <string name="cant_send_message_to_member_alert_title">Nachricht an Gruppenmitglied nicht möglich</string> + <string name="cant_call_contact_connecting_wait_alert_text">Verbinde mit Kontakt, bitte warten oder später erneut überprüfen!</string> + <string name="cant_call_contact_deleted_alert_text">Kontakt wurde gelöscht.</string> + <string name="delete_contact_cannot_undo_warning">Kontakt wird gelöscht. Dies kann nicht rückgängig gemacht werden!</string> + <string name="delete_without_notification">Ohne Benachrichtigung löschen</string> + <string name="action_button_add_members">Einladen</string> + <string name="keep_conversation">Chat-Inhalte beibehalten</string> + <string name="cant_call_member_send_message_alert_text">Nachricht senden, um Anrufe zu aktivieren.</string> + <string name="you_can_still_send_messages_to_contact">Sie können aus den archivierten Kontakten heraus Nachrichten an %1$s versenden.</string> + <string name="toolbar_settings">Einstellungen</string> + <string name="moderate_messages_will_be_deleted_warning">Die Nachrichten werden für alle Gruppenmitglieder gelöscht.</string> + <string name="moderate_messages_will_be_marked_warning">Die Nachrichten werden für alle Mitglieder als moderiert markiert.</string> + <string name="delete_members_messages__question">Sollen %d Nachrichten von Mitgliedern gelöscht werden?</string> + <string name="compose_message_placeholder">Nachricht</string> + <string name="delete_messages_mark_deleted_warning">Nachrichten werden zur Löschung markiert. Der/Die Empfänger hat/haben die Möglichkeit, diese Nachrichten aufzudecken.</string> + <string name="selected_chat_items_selected_n">Es wurden %d ausgewählt</string> + <string name="selected_chat_items_nothing_selected">Es wurde Nichts ausgewählt</string> + <string name="select_verb">Auswählen</string> + <string name="invite_friends_short">Einladen</string> + <string name="network_option_tcp_connection">TCP-Verbindung</string> + <string name="v6_0_increase_font_size">Schriftgröße anpassen.</string> + <string name="v6_0_new_chat_experience">Neue Chat-Erfahrung 🎉</string> + <string name="v6_0_upgrade_app">Die App automatisch aktualisieren</string> + <string name="v6_0_connection_servers_status_descr">Verbindungs- und Server-Status.</string> + <string name="v6_0_privacy_blur">Für bessere Privatsphäre verpixeln.</string> + <string name="v6_0_connection_servers_status">Kontrollieren Sie Ihr Netzwerk</string> + <string name="v6_0_new_media_options">Neue Medien-Optionen</string> + <string name="network_options_save_and_reconnect">Speichern und neu verbinden</string> + <string name="create_address_button">Erstellen</string> + <string name="v6_0_upgrade_app_descr">Laden Sie neue Versionen von GitHub herunter.</string> + <string name="v6_0_chat_list_media">Direkt aus der Chat-Liste abspielen.</string> + <string name="one_hand_ui_card_title">Chat-Liste umschalten:</string> + <string name="one_hand_ui_change_instruction">Kann von Ihnen in den Erscheinungsbild-Einstellungen geändert werden.</string> + <string name="v6_0_your_contacts_descr">Kontakte für spätere Chats archivieren.</string> + <string name="v6_0_private_routing_descr">Ihre IP-Adresse und Verbindungen werden geschützt.</string> + <string name="v6_0_delete_many_messages_descr">Löschen Sie bis zu 20 Nachrichten auf einmal.</string> + <string name="v6_0_reachable_chat_toolbar">Chat-Symbolleiste unten</string> + <string name="v6_0_reachable_chat_toolbar_descr">Die App mit einer Hand bedienen.</string> + <string name="v6_0_connect_faster_descr">Schneller mit Ihren Freunden verbinden.</string> + <string name="chat_database_exported_title">Chat-Datenbank wurde exportiert</string> + <string name="chat_database_exported_continue">Weiter</string> + <string name="media_and_file_servers">Medien- und Datei-Server</string> + <string name="message_servers">Nachrichten-Server</string> + <string name="network_socks_proxy">SOCKS-Proxy</string> + <string name="chat_database_exported_not_all_files">Es wurden nicht alle Dateien exportiert</string> + <string name="chat_database_exported_migrate">Sie können die exportierte Datenbank migrieren.</string> + <string name="chat_database_exported_save">Sie können das exportierte Archiv speichern.</string> + <string name="reset_all_hints">Alle Hinweise zurücksetzen</string> + <string name="new_message">Neue Nachricht</string> + <string name="error_parsing_uri_desc">Bitte überprüfen Sie, ob der SimpleX-Link korrekt ist.</string> + <string name="error_parsing_uri_title">Ungültiger Link</string> + <string name="settings_section_title_chat_database">CHAT-DATENBANK</string> + <string name="switching_profile_error_title">Fehler beim Wechseln des Profils</string> + <string name="delete_messages_cannot_be_undone_warning">Die Nachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden!</string> + <string name="new_chat_share_profile">Profil teilen</string> + <string name="system_mode_toast">System-Modus</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">Das hochgeladene Datenbank-Archiv wird dauerhaft von den Servern entfernt.</string> + <string name="select_chat_profile">Chat-Profil auswählen</string> + <string name="migrate_from_device_remove_archive_question">Archiv entfernen?</string> + <string name="network_proxy_auth_mode_username_password">Ihre Anmeldeinformationen können unverschlüsselt versendet werden.</string> + <string name="network_proxy_auth_mode_no_auth">Verwenden Sie keine Anmeldeinformationen mit einem Proxy.</string> + <string name="switching_profile_error_message">Ihre Verbindung wurde auf %s verschoben, aber während der Weiterleitung auf das Profil trat ein unerwarteter Fehler auf.</string> + <string name="network_proxy_incorrect_config_desc">Stellen Sie sicher, dass die Proxy-Konfiguration richtig ist.</string> + <string name="network_proxy_incorrect_config_title">Fehler beim Speichern des Proxys</string> + <string name="network_proxy_password">Passwort</string> + <string name="network_proxy_auth">Proxy-Authentifizierung</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">Verwenden Sie für jede Verbindung unterschiedliche Proxy-Anmeldeinformationen.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">Verwenden Sie für jedes Profil unterschiedliche Proxy-Anmeldeinformationen.</string> + <string name="network_proxy_random_credentials">Verwenden Sie zufällige Anmeldeinformationen</string> + <string name="network_proxy_username">Benutzername</string> + <string name="n_file_errors">%1$d Datei-Fehler:\n%2$s</string> + <string name="forward_files_in_progress_desc">%1$d Datei(en) wird/werden immer noch heruntergeladen.</string> + <string name="forward_files_failed_to_receive_desc">Bei %1$d Datei(en) ist das Herunterladen fehlgeschlagen.</string> + <string name="error_forwarding_messages">Fehler beim Weiterleiten der Nachrichten</string> + <string name="forward_files_messages_deleted_after_selection_desc">Die Nachrichten wurden gelöscht, nachdem Sie sie ausgewählt hatten.</string> + <string name="forward_alert_title_nothing_to_forward">Es gibt nichts zum Weiterleiten!</string> + <string name="n_other_file_errors">%1$d andere(r) Datei-Fehler.</string> + <string name="forward_alert_title_messages_to_forward">Soll(en) %1$s Nachricht(en) weitergeleitet werden?</string> + <string name="forward_files_missing_desc">%1$d Datei(en) wurde(n) gelöscht.</string> + <string name="forward_files_not_accepted_desc">%1$d Datei(en) wurde(n) nicht heruntergeladen.</string> + <string name="forward_alert_forward_messages_without_files">Nachrichten ohne Dateien weiterleiten?</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s Nachrichten wurden nicht weitergeleitet</string> + <string name="forward_files_not_accepted_receive_files">Herunterladen</string> + <string name="compose_forward_messages_n">Es wird/werden %1$s Nachricht(en) weitergeleitet</string> + <string name="forward_multiple">Nachrichten werden weitergeleitet…</string> + <string name="compose_save_messages_n">Es wird/werden %1$s Nachricht(en) gesichert</string> + <string name="icon_descr_sound_muted">Ton stummgeschaltet</string> + <string name="error_initializing_web_view_wrong_arch">Fehler bei der Initialisierung von WebView. Stellen Sie sicher, dass Sie WebView installiert haben, und es die ARM64-Architektur unterstützt.\nFehler: %s</string> + <string name="settings_section_title_message_shape">Form der Nachricht</string> + <string name="settings_message_shape_tail">Sprechblase</string> + <string name="settings_message_shape_corner">Abrundung Ecken</string> + <string name="network_session_mode_session">App-Sitzung</string> + <string name="network_session_mode_server_description">Für jeden Server werden neue SOCKS-Anmeldeinformationen genutzt</string> + <string name="network_session_mode_server">Server</string> + <string name="call_desktop_permission_denied_chrome">Klicken Sie auf die Info-Schaltfläche neben dem Adressfeld, um die Verwendung des Mikrofons zu erlauben.</string> + <string name="call_desktop_permission_denied_title">Um Anrufe durchzuführen, erlauben Sie die Nutzung Ihres Mikrofons. Beenden Sie den Anruf und versuchen Sie es erneut.</string> + <string name="call_desktop_permission_denied_safari">Öffnen Sie die Safari-Einstellungen / Webseiten / Mikrofon und wählen Sie dann \"Für Localhost erlauben\".</string> + <string name="v6_1_better_calls">Verbesserte Anrufe</string> + <string name="v6_1_customizable_message_descr">Anpassbares Format des Nachrichtenfelds</string> + <string name="v6_1_delete_many_messages_descr">Bis zu 200 Nachrichten löschen oder moderieren</string> + <string name="v6_1_forward_many_messages_descr">Bis zu 20 Nachrichten auf einmal weiterleiten</string> + <string name="v6_1_better_security_descr">Die SimpleX-Protokolle wurden von Trail of Bits überprüft.</string> + <string name="v6_1_better_calls_descr">Während des Anrufs zwischen Audio und Video wechseln</string> + <string name="v6_1_switch_chat_profile_descr">Das Chat-Profil für Einmal-Einladungen wechseln</string> + <string name="network_session_mode_session_description">Jedes Mal wenn Sie die App starten, werden neue SOCKS-Anmeldeinformationen genutzt</string> + <string name="v6_1_better_security">Verbesserte Sicherheit ✅</string> + <string name="v6_1_message_dates_descr">Verbesserte Nachrichten-Datumsinformation</string> + <string name="v6_1_better_user_experience">Verbesserte Nutzer-Erfahrung</string> + <string name="failed_to_save_servers">Fehler beim Speichern der Server</string> + <string name="no_message_servers_configured">Keine Nachrichten-Server.</string> + <string name="no_message_servers_configured_for_receiving">Keine Server für den Empfang von Nachrichten.</string> + <string name="errors_in_servers_configuration">Fehler in der Server-Konfiguration.</string> + <string name="for_chat_profile">Für das Chat-Profil %s:</string> + <string name="no_media_servers_configured">Keine Medien- und Dateiserver.</string> + <string name="no_media_servers_configured_for_private_routing">Keine Server für das Herunterladen von Dateien.</string> + <string name="no_media_servers_configured_for_sending">Keine Server für das Versenden von Dateien.</string> + <string name="connection_error_quota">Nicht ausgelieferte Nachrichten</string> + <string name="share_simplex_address_on_social_media">Die SimpleX-Adresse auf sozialen Medien teilen.</string> + <string name="connection_security">Verbindungs-Sicherheit</string> + <string name="share_1_time_link_with_a_friend">Den Einmal-Einladungslink mit einem Freund teilen</string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">Die SimpleX-Adresse und Einmal-Links können sicher über beliebige Messenger geteilt werden.</string> + <string name="you_can_set_connection_name_to_remember">Sie können einen Verbindungsnamen festlegen, um sich zu merken, mit wem der Link geteilt wurde.</string> + <string name="address_settings">Adress-Einstellungen</string> + <string name="create_1_time_link">Einmal-Link erstellen</string> + <string name="for_social_media">Für soziale Medien</string> + <string name="or_to_share_privately">Oder zum privaten Teilen</string> + <string name="simplex_address_or_1_time_link">SimpleX-Adresse oder Einmal-Link?</string> + <string name="onboarding_choose_server_operators">Server-Betreiber</string> + <string name="onboarding_network_operators">Netzwerk-Betreiber</string> + <string name="onboarding_network_operators_app_will_use_different_operators">Die App verwendet für jede Unterhaltung Server von unterschiedlichen Betreibern, um Ihre Privatsphäre zu schützen.</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">Die Nutzungsbedingungen der aktivierten Betreiber werden nach 30 Tagen akzeptiert.</string> + <string name="onboarding_network_operators_app_will_use_for_routing">Wenn Ihr Kontakt beispielsweise Nachrichten über einen SimpleX-Chat-Server empfängt, wird Ihre App diese über einen Flux-Server versenden.</string> + <string name="onboarding_network_operators_review_later">Später einsehen</string> + <string name="onboarding_select_network_operators_to_use">Wählen sie die zu nutzenden Netzwerk-Betreiber aus.</string> + <string name="onboarding_network_operators_conditions_you_can_configure">Sie können die Betreiber in den Netzwerk- und Servereinstellungen konfigurieren.</string> + <string name="onboarding_network_operators_configure_via_settings">Sie können die Server über die Einstellungen konfigurieren.</string> + <string name="onboarding_network_operators_continue">Weiter</string> + <string name="onboarding_network_operators_update">Aktualisieren</string> + <string name="network_preset_servers_title">Voreingestellte Server</string> + <string name="operator_review_conditions">Nutzungsbedingungen einsehen</string> + <string name="operator_conditions_accepted_for_enabled_operators_on">Die Nutzungsbedingungen der aktivierten Betreiber werden automatisch akzeptiert am: %s.</string> + <string name="your_servers">Ihre Server</string> + <string name="operator">Betreiber</string> + <string name="operator_servers_title">%s Server</string> + <string name="operator_info_title">Netzwerk-Betreiber</string> + <string name="operator_use_operator_toggle_description">Verwende Server</string> + <string name="operator_website">Webseite</string> + <string name="use_servers_of_operator_x">Verwende %s</string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[Dieselben Nutzungsbedingungen gelten auch für den Betreiber <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Dieselben Nutzungsbedingungen gelten auch für den/die Betreiber: <b>%s</b>.]]></string> + <string name="operator_conditions_of_use">Nutzungsbedingungen</string> + <string name="view_conditions">Nutzungsbedingungen anschauen</string> + <string name="accept_conditions">Nutzungsbedingungen akzeptieren</string> + <string name="operators_conditions_will_also_apply"><![CDATA[Diese Nutzungsbedingungen gelten auch für: <b>%s</b>.]]></string> + <string name="operator_use_for_messages_receiving">Für den Empfang</string> + <string name="operator_use_for_messages">Für Nachrichten verwenden</string> + <string name="operator_added_message_servers">Nachrichtenserver hinzugefügt</string> + <string name="operator_use_for_messages_private_routing">Für privates Routing</string> + <string name="xftp_servers_per_user">Medien- und Datei-Server für neue Daten über Ihr aktuelles Chat-Profil</string> + <string name="operator_use_for_sending">Für das Senden</string> + <string name="operator_use_for_files">Für Dateien verwenden</string> + <string name="error_adding_server">Fehler beim Hinzufügen des Servers</string> + <string name="operator_open_changes">Änderungen öffnen</string> + <string name="operator_open_conditions">Nutzungsbedingungen öffnen</string> + <string name="operator_server_alert_title">Betreiber-Server</string> + <string name="server_added_to_operator__name">Der Server wurde dem Betreiber %s hinzugefügt.</string> + <string name="error_server_operator_changed">Der Server-Betreiber wurde geändert.</string> + <string name="error_server_protocol_changed">Das Server-Protokoll wurde geändert.</string> + <string name="appearance_in_app_bars_alpha">Transparenz</string> + <string name="v6_2_network_decentralization_enable_flux">Für einen besseren Metadatenschutz Flux in den Netzwerk- und Servereinstellungen aktivieren.</string> + <string name="v6_2_network_decentralization">Dezentralisiertes Netzwerk</string> + <string name="v6_2_network_decentralization_descr">Der zweite voreingestellte Netzwerk-Betreiber in der App!</string> + <string name="v6_2_improved_chat_navigation">Verbesserte Chat-Navigation</string> + <string name="v6_2_improved_chat_navigation_descr">- Den Chat bei der ersten ungelesenen Nachricht öffnen.\n- Zu zitierten Nachrichten springen.</string> + <string name="view_updated_conditions">Aktualisierte Nutzungsbedingungen anschauen</string> + <string name="operator_conditions_accepted">Akzeptierte Nutzungsbedingungen</string> + <string name="operator_added_xftp_servers">Medien- und Dateiserver hinzugefügt</string> + <string name="address_or_1_time_link">Adress- oder Einmal-Link?</string> + <string name="appearance_app_toolbars">App-Symbolleiste</string> + <string name="appearance_bars_blur_radius">Verpixeln</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[Ein Einmal-Link kann <i>nur mit einem Kontakt</i> genutzt werden - teilen Sie in nur persönlich oder über einen beliebigen Messenger.]]></string> + <string name="operator_conditions_accepted_for_some"><![CDATA[Die Nutzungsbedingungen der/des folgenden Betreiber(s) wurden schon akzeptiert: <b>%s</b>.]]></string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_accepted_on">Die Nutzungsbedingungen wurden akzeptiert am: %s</string> + <string name="operator_conditions_will_be_applied"><![CDATA[Diese Nutzungsbedingungen gelten auch für: <b>%s</b>.]]></string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[Um die Server von <b>%s</b> zu nutzen, müssen Sie dessen Nutzungsbedingungen akzeptieren.]]></string> + <string name="error_accepting_operator_conditions">Fehler beim Akzeptieren der Nutzungsbedingungen</string> + <string name="error_updating_server_title">Fehler beim Aktualisieren des Servers</string> + <string name="v6_2_network_decentralization_enable_flux_reason">für einen besseren Metadatenschutz.</string> + <string name="smp_servers_new_server">Neuer Server</string> + <string name="message_deleted_or_not_received_error_title">Keine Nachricht</string> + <string name="no_message_servers_configured_for_private_routing">Keine Server für privates Nachrichten-Routing.</string> + <string name="share_address_publicly">Die Adresse öffentlich teilen</string> + <string name="address_creation_instruction">Tippen Sie im Menü auf SimpleX-Adresse erstellen, um sie später zu erstellen.</string> + <string name="connection_error_quota_desc">Diese Verbindung hat das Limit der nicht ausgelieferten Nachrichten erreicht. Ihr Kontakt ist möglicherweise offline.</string> + <string name="message_deleted_or_not_received_error_desc">Diese Nachricht wurde gelöscht oder bisher noch nicht empfangen.</string> + <string name="to_protect_against_your_link_replaced_compare_codes">Zum Schutz vor dem Austausch Ihres Links können Sie die Sicherheitscodes Ihrer Kontakte vergleichen.</string> + <string name="operators_conditions_accepted_for"><![CDATA[Die Nutzungsbedingungen der/des Betreiber(s) wurden akzeptiert: <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: <b>%s</b>.]]></string> + <string name="operator_conditions_accepted_on">Die Nutzungsbedingungen wurden akzeptiert am: %s.</string> + <string name="operator_conditions_failed_to_load">Der Text der aktuellen Nutzungsbedingungen konnte nicht geladen werden. Sie können die Nutzungsbedingungen unter diesem Link einsehen:</string> + <string name="remote_hosts_section">Ferngesteuerte Mobiltelefone</string> + <string name="chat_archive">Oder importieren Sie eine Archiv-Datei</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Hinweis für Geräte von Xiaomi</b>: Bitte aktivieren Sie in den System-Einstellungen die Option "Autostart", damit Benachrichtigungen funktionieren.]]></string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[Alle Nachrichten und Dateien werden <b>Ende-zu-Ende-verschlüsselt</b> versendet. In Direktnachrichten sogar mit Post-Quantum-Security.]]></string> + <string name="button_add_team_members">Team-Mitglieder aufnehmen</string> + <string name="button_add_friends">Freunde aufnehmen</string> + <string name="display_name_accepted_invitation">Einladung angenommen</string> + <string name="business_address">Geschäftliche Adresse</string> + <string name="v6_2_business_chats">Geschäftliche Chats</string> + <string name="add_your_team_members_to_conversations">Nehmen Sie Team-Mitglieder in Ihre Unterhaltungen auf.</string> + <string name="onboarding_notifications_mode_service_desc_short">Die App läuft immer im Hintergrund ab</string> + <string name="direct_messages_are_prohibited_in_chat">In diesem Chat sind Direktnachrichten zwischen Mitgliedern nicht erlaubt.</string> + <string name="onboarding_notifications_mode_off_desc_short">Kein Hintergrund-Service</string> + <string name="onboarding_notifications_mode_periodic_desc_short">Nachrichten alle 10 Minuten überprüfen</string> + <string name="onboarding_notifications_mode_battery">Benachrichtigungen und Akku</string> + <string name="invite_to_chat_button">Zum Chat einladen</string> + <string name="connect_plan_chat_already_exists">Chat besteht bereits!</string> + <string name="chat_bottom_bar">Chat-Symbolleiste unten</string> + <string name="button_leave_chat">Chat verlassen</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">Das Mitglied wird aus dem Chat entfernt. Dies kann nicht rückgängig gemacht werden!</string> + <string name="chat_main_profile_sent">Ihr Chat-Profil wird an die Chat-Mitglieder gesendet.</string> + <string name="direct_messages_are_prohibited">Direktnachrichten zwischen Mitgliedern sind nicht erlaubt.</string> + <string name="how_it_helps_privacy">Wie die Privatsphäre geschützt wird</string> + <string name="leave_chat_question">Chat verlassen?</string> + <string name="delete_chat_question">Chat löschen?</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">Der Chat wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden!</string> + <string name="v6_2_business_chats_descr">Schutz der Privatsphäre Ihrer Kunden.</string> + <string name="display_name_requested_to_connect">Zur Verbindung aufgefordert</string> + <string name="maximum_message_size_reached_non_text">Bitte verkleinern Sie die Nachrichten-Größe oder entfernen Sie Medien und versenden Sie diese erneut.</string> + <string name="only_chat_owners_can_change_prefs">Nur Chat-Eigentümer können die Präferenzen ändern.</string> + <string name="maximum_message_size_reached_text">Bitte verkleinern Sie die Nachrichten-Größe und versenden Sie diese erneut.</string> + <string name="member_role_will_be_changed_with_notification_chat">Die Rolle wird auf %s geändert. Im Chat wird Jeder darüber informiert.</string> + <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Sie werden von diesem Chat keine Nachrichten mehr erhalten. Der Nachrichtenverlauf wird beibehalten.</string> + <string name="maximum_message_size_reached_forwarding">Sie können die Nachricht kopieren und verkleinern, um sie zu versenden.</string> + <string name="button_delete_chat">Chat löschen</string> + <string name="delete_chat_for_self_cannot_undo_warning">Der Chat wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden!</string> + <string name="maximum_message_size_title">Die Nachricht ist zu umfangreich!</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">Wenn mehr als ein Betreiber aktiviert ist, hat keiner von ihnen Metadaten, um zu erfahren, wer mit wem kommuniziert.</string> + <string name="info_row_chat">Chat</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Sie sind bereits mit <b>%1$s</b> verbunden.]]></string> + <string name="onboarding_network_about_operators">Über die Betreiber</string> + <string name="onboarding_network_operators_simplex_flux_agreement">SimpleX-Chat und Flux haben vereinbart, die von Flux betriebenen Server in die App aufzunehmen.</string> + <string name="sync_connection_desc">Die Verbindung erfordert eine Neuverhandlung der Verschlüsselung.</string> + <string name="encryption_renegotiation_in_progress">Die Neuverhandlung der Verschlüsselung läuft.</string> + <string name="sync_connection_confirm">Reparieren</string> + <string name="sync_connection_question">Verbindung reparieren?</string> + <string name="debug_logs">Log-Daten aktivieren</string> + <string name="error_saving_database">Fehler beim Speichern der Datenbank</string> + <string name="connection_not_ready">Verbindung noch nicht bereit.</string> + <string name="chat_list_all">Alle</string> + <string name="chat_list_businesses">Unternehmen</string> + <string name="error_creating_chat_tags">Fehler beim Erstellen der Chat-Liste</string> + <string name="error_loading_chat_tags">Fehler beim Laden der Chat-Listen</string> + <string name="error_updating_chat_tags">Fehler beim Aktualisieren der Chat-Liste</string> + <string name="chat_list_favorites">Favoriten</string> + <string name="list_menu">Liste</string> + <string name="no_chats">Keine Chats</string> + <string name="no_chats_found">Keine Chats gefunden</string> + <string name="no_chats_in_list">Keine Chats in der Liste %s.</string> + <string name="no_unread_chats">Keine ungelesenen Chats</string> + <string name="create_list">Liste erstellen</string> + <string name="delete_chat_list_menu_action">Löschen</string> + <string name="delete_chat_list_question">Liste löschen?</string> + <string name="edit_chat_list_menu_action">Bearbeiten</string> + <string name="list_name_field_placeholder">Listenname...</string> + <string name="duplicated_list_error">Der Listenname und das Emoji sollen für alle Listen unterschiedlich sein.</string> + <string name="save_list">Liste speichern</string> + <string name="delete_chat_list_warning">Alle Chats werden von der Liste %s entfernt und die Liste wird gelöscht</string> + <string name="chat_list_groups">Gruppen</string> + <string name="chat_list_contacts">Kontakte</string> + <string name="chat_list_add_list">Liste hinzufügen</string> + <string name="add_to_list">Zur Liste hinzufügen</string> + <string name="open_with_app">Mit %s öffnen</string> + <string name="chat_list_notes">Anmerkungen</string> + <string name="change_order_chat_list_menu_action">Anordnung ändern</string> + <string name="change_list">Liste ändern</string> + <string name="error_creating_report">Fehler beim Erstellen der Meldung</string> + <string name="prefs_error_saving_settings">Fehler beim Abspeichern der Einstellungen</string> + <string name="report_item_archived">Archivierte Meldung</string> + <string name="report_compose_reason_header_spam">Spam melden: Nur Gruppenmoderatoren werden es sehen.</string> + <string name="report_verb">Melden</string> + <string name="report_reason_other">Anderer Grund</string> + <string name="report_archive_alert_title">Meldung archivieren?</string> + <string name="group_member_role_moderator">Moderator</string> + <string name="report_compose_reason_header_illegal">Inhalt melden: Nur Gruppenmoderatoren werden es sehen.</string> + <string name="report_reason_illegal">Unangemessener Inhalt</string> + <string name="report_reason_profile">Unangemessenes Profil</string> + <string name="report_item_visibility_moderators">Nur Absender und Moderatoren sehen es</string> + <string name="report_item_visibility_submitter">Nur Sie und Moderatoren sehen es</string> + <string name="report_reason_spam">Spam</string> + <string name="archive_verb">Archiv</string> + <string name="report_reason_alert_title">Grund der Meldung?</string> + <string name="report_archive_alert_desc">Die Meldung wird für Sie archiviert.</string> + <string name="report_compose_reason_header_profile">Mitgliederprofil melden: Nur Gruppenmoderatoren werden es sehen.</string> + <string name="report_compose_reason_header_other">Anderes melden: Nur Gruppenmoderatoren werden es sehen.</string> + <string name="report_compose_reason_header_community">Verstoß melden: Nur Gruppenmoderatoren werden es sehen.</string> + <string name="archive_report">Meldung archivieren</string> + <string name="delete_report">Meldung löschen</string> + <string name="report_reason_community">Verstoß gegen die Gemeinschaftsrichtlinien</string> + <string name="report_item_archived_by">Archivierte Meldung von %s</string> + <string name="group_reports_active_one">Eine Meldung</string> + <string name="group_reports_active">%d Meldungen</string> + <string name="group_reports_member_reports">Mitglieder-Meldungen</string> + <string name="chat_list_group_reports">Meldungen</string> + <string name="blocking_reason_content">Inhalt verletzt Nutzungsbedingungen</string> + <string name="blocking_reason_spam">Spam</string> + <string name="connection_error_blocked">Verbindung blockiert</string> + <string name="file_error_blocked">Die Datei wird vom Serverbetreiber blockiert:\n%1$s.</string> + <string name="connection_error_blocked_desc">Die Verbindung wird vom Serverbetreiber blockiert:\n%1$s.</string> + <string name="privacy_chat_list_open_links_ask">Fragen</string> + <string name="privacy_chat_list_open_links_no">Nein</string> + <string name="privacy_chat_list_open_web_link">Web-Link öffnen</string> + <string name="privacy_chat_list_open_links">Web-Links aus der Chat-Liste öffnen</string> + <string name="privacy_chat_list_open_web_link_question">Web-Link öffnen?</string> + <string name="privacy_chat_list_open_links_yes">Ja</string> + <string name="text_field_set_chat_placeholder">Chat-Name festlegen…</string> + <string name="change_automatic_chat_deletion_message">Die älteren als die ausgewählten gesendeten und empfangenen Nachrichten in diesem Chat werden gelöscht. Diese Aktion kann nicht rückgängig gemacht werden!</string> + <string name="change_automatic_deletion_question">Automatisches Löschen von Nachrichten ändern?</string> + <string name="chat_ttl_options_footer">Chat-Nachrichten von Ihrem Gerät löschen</string> + <string name="disable_automatic_deletion_question">Automatisches Löschen von Nachrichten deaktivieren?</string> + <string name="disable_automatic_deletion">Löschen von Nachrichten deaktivieren</string> + <string name="chat_item_ttl_year">Älter als ein Jahr</string> + <string name="chat_item_ttl_default">Default (%s)</string> + <string name="disable_automatic_deletion_message">Nachrichten in diesem Chat werden nie gelöscht.</string> + <string name="network_smp_web_port_footer">Solange kein Port konfiguriert ist, wird TCP-Port %1$s genutzt.</string> + <string name="network_smp_web_port_toggle">Web-Port nutzen</string> + <string name="network_smp_web_port_section_title">TCP-Port für Nachrichtenübermittlung</string> + <string name="max_group_mentions_per_message_reached">Sie können bis zu %1$s Mitglieder pro Nachricht erwähnen!</string> + <string name="mute_all_chat">Alle stummschalten</string> + <string name="unread_mentions">Ungelesene Erwähnungen</string> + <string name="member_reports_are_prohibited">Melden von Nachrichten ist in dieser Gruppe nicht erlaubt.</string> + <string name="report_archive_alert_title_all">Alle Meldungen archivieren?</string> + <string name="report_archive_alert_title_nth">Archiviere %d Meldungen?</string> + <string name="report_archive_for_all_moderators">Für alle Moderatoren</string> + <string name="report_archive_for_me">Für mich</string> + <string name="archive_reports">Meldungen archivieren</string> + <string name="group_members_can_send_reports">Mitglieder können Nachrichten an Moderatoren melden.</string> + <string name="disable_sending_member_reports">Melden von Nachrichten an Moderatoren nicht erlauben.</string> + <string name="enable_sending_member_reports">Melden von Nachrichten an Moderatoren erlauben.</string> + <string name="report_archive_alert_desc_all">Alle Meldungen werden für Sie archiviert.</string> + <string name="notification_group_report">Meldung: %s</string> + <string name="v6_3_reports_descr">Helfen Sie Administratoren bei der Moderation ihrer Gruppen.</string> + <string name="v6_3_mentions">Erwähnung von Mitgliedern 👋</string> + <string name="v6_3_reports">Private Meldungen senden</string> + <string name="v6_3_mentions_descr">Bei Erwähnung benachrichtigt werden.</string> + <string name="v6_3_private_media_file_names">Medien mit anonymisierten Dateinamen.</string> + <string name="v6_3_set_message_expiration_in_chats">Verfallsdatum von Nachrichten in Chats festlegen.</string> + <string name="v6_3_better_groups_performance">Bessere Leistung von Gruppen</string> + <string name="v6_3_faster_deletion_of_groups">Schnelleres löschen von Gruppen</string> + <string name="v6_3_faster_sending_messages">Schnelleres versenden von Nachrichten.</string> + <string name="group_preview_rejected">abgelehnt</string> + <string name="v6_3_better_privacy_and_security">Bessere(r) Security und Datenschutz</string> + <string name="v6_3_organize_chat_lists_descr">Verpassen Sie keine wichtigen Nachrichten.</string> + <string name="v6_3_organize_chat_lists">Chats in Listen verwalten</string> + <string name="group_member_status_rejected">abgelehnt</string> + <string name="restore_passphrase_can_not_be_read_desc">Das Passwort kann nicht aus dem Schlüsselbund gelesen werden. Dies kann nach einer Systemaktualisierung passiert sein, die nicht mit der App kompatibel war. Wenden Sie sich bitte an die Entwickler, wenn dies nicht der Fall ist.</string> + <string name="group_member_status_pending_approval_short">ausstehend</string> + <string name="error_reading_passphrase">Fehler beim Lesen des Datenbank-Passworts</string> + <string name="operator_updated_conditions">Aktualisierte Nutzungsbedingungen</string> + <string name="group_member_status_pending_approval">ausstehende Genehmigung</string> + <string name="restore_passphrase_can_not_be_read_enter_manually_desc">Das Passwort kann nicht aus dem Schlüsselbund gelesen werden. Bitte geben Sie es manuell ein. Dies kann nach einer Systemaktualisierung passiert sein, die nicht mit der App kompatibel war. Wenden Sie sich bitte an die Entwickler, wenn dies nicht der Fall ist.</string> + <string name="button_remove_members_question">Mitglieder entfernen?</string> + <string name="members_will_be_removed_from_chat_cannot_be_undone">Mitglieder werden aus dem Chat entfernt. Dies kann nicht rückgängig gemacht werden!</string> + <string name="members_will_be_removed_from_group_cannot_be_undone">Mitglieder werden aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden!</string> + <string name="unblock_members_for_all_question">Mitglieder für Alle freigeben?</string> + <string name="unblock_members_desc">Nachrichten dieser Mitglieder werden angezeigt!</string> + <string name="feature_roles_moderators">Moderatoren</string> + <string name="block_members_for_all_question">Mitglieder für Alle blockieren?</string> + <string name="block_members_desc">Alle neuen Nachrichten dieser Mitglieder werden nicht angezeigt!</string> + <string name="onboarding_conditions_by_using_you_agree">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.</string> + <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Datenschutz- und Nutzungsbedingungen.</string> + <string name="onboarding_conditions_accept">Akzeptieren</string> + <string name="onboarding_conditions_configure_server_operators">Server-Betreiber konfigurieren</string> + <string name="onboarding_conditions_private_chats_not_accessible">Private Chats, Gruppen und Ihre Kontakte sind für Server-Betreiber nicht zugänglich.</string> + <string name="unsupported_connection_link">Verbindungs-Link wird nicht unterstützt</string> + <string name="privacy_short_links">Kurze Links verwenden (BETA)</string> + <string name="short_link_button_text">Verkürzter Link</string> + <string name="full_link_button_text">Vollständiger Link</string> + <string name="simplex_link_channel">SimpleX-Kanal-Link</string> + <string name="link_requires_newer_app_version_please_upgrade">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.</string> + <string name="network_smp_web_port_all">Alle Server</string> + <string name="network_smp_web_port_off">Aus</string> + <string name="network_smp_web_port_preset_footer">TCP-Port 443 nur für voreingestellte Server verwenden.</string> + <string name="network_smp_web_port_preset">Voreingestellte Server</string> +</resources> 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 7063eb9007..179c7fec52 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml @@ -42,7 +42,7 @@ <string name="accept_connection_request__question">Αποδοχή αιτήματος σύνδεσης;</string> <string name="callstatus_accepted">αποδεκτή κλήση</string> <string name="network_enable_socks_info">Πρόσβαση στους διακομιστές μέσω SOCKS proxy στην πόρτα %d; Ο διακομιστής μεσολάβησης (proxy server) πρέπει να είναι ενεργός πριν ενεργοποιηθεί αυτή η ρύθμιση.</string> - <string name="smp_servers_add">Προσθήκη διακομιστή…</string> + <string name="smp_servers_add">Προσθήκη διακομιστή</string> <string name="network_settings">Προχωρημένες ρυθμίσεις δικτύου</string> <string name="v4_3_improved_server_configuration_desc">Προσθήκη διακομιστών μέσω σάρωσης QR κωδικών.</string> <string name="v4_2_group_links_desc">Οι διαχειριστές μπορούν να δημιουργήσουν τους συνδέσμους συμμετοχής σε ομάδες.</string> @@ -56,4 +56,343 @@ <string name="always_use_relay">Να χρησιμοποιείται πάντα αναμεταδότη</string> <string name="search_verb">Αναζήτηση</string> <string name="la_mode_off">Ανενεργό</string> + <string name="connect__your_profile_will_be_shared">"Το προφίλ σας %1$s θα μοιραστεί"</string> + <string name="your_simplex_contact_address">Η SimpleX διεύθυνση σας</string> + <string name="full_backup">Αντίγραφο δεδομένων εφαρμογής</string> + <string name="send_disappearing_message_5_minutes">5 λεπτά</string> + <string name="you_will_be_connected_when_your_contacts_device_is_online">Θα συνδεθείτε όταν η συσκευή της επαφής σας είναι συνδεμένει, παρακαλώ περιμένετε ή ελέγξτε αργότερα!</string> + <string name="your_ice_servers">Ο ICE διακομιστής σας</string> + <string name="app_version_title">Έκδοση εφαρμογής</string> + <string name="you_sent_group_invitation">Στείλατε πρόσκληση ομάδας</string> + <string name="send_disappearing_message_1_minute">1 λεπτό</string> + <string name="smp_servers_your_server">Ο διακομιστής σας</string> + <string name="address_section_title">Διεύθυνση</string> + <string name="abort_switch_receiving_address_confirm">Ακύρωση</string> + <string name="back">Πίσω</string> + <string name="send_disappearing_message_30_seconds">30 δευτερόλεπτα</string> + <string name="you_will_join_group">Θα συνδεθείτε με όλα τα μέλη της ομάδας.</string> + <string name="contact_wants_to_connect_via_call">%1$s θέλει να συνδεθεί μαζί σου μέσω</string> + <string name="allow_message_reactions">Επιτρέπονται αντιδράσεις μηνύματος.</string> + <string name="your_XFTP_servers">Ο διακομιστής XFTP σας</string> + <string name="smp_servers_your_server_address">Η διεύθυνση του διακομιστή σας</string> + <string name="your_profile_is_stored_on_your_device">Το προφίλ, επαφές και παραδομένα μηνύματα σας είναι αποθηκευμένα στην συσκευή σας.</string> + <string name="your_SMP_servers">Ο διακομιστής SMP σας</string> + <string name="allow_to_send_files">Επιτρέπεται να σταλούν αρχεία και μέσα.</string> + <string name="incognito_random_profile">Το τυχαίο προφίλ σας</string> + <string name="group_info_section_title_num_members">%1$s ΜΕΛΗ</string> + <string name="turn_off_battery_optimization_button">Επιτρέπεται</string> + <string name="your_preferences">Οι προτιμήσεις σας</string> + <string name="group_member_role_author">Συντάκτης</string> + <string name="your_ICE_servers">Ο ΙCE διακομιστής σας</string> + <string name="v5_0_app_passcode">Κωδικός εφαρμογής</string> + <string name="connect_via_member_address_alert_desc">Αίτημα σύνδεσης θα σταλεί σε αυτό το μέλος της ομάδας.</string> + <string name="settings_section_title_icon">ΟΙΚΟΝΑ ΕΦΑΡΜΟΓΗΣ</string> + <string name="settings_section_title_app">Εφαρμογή</string> + <string name="your_settings">Οι ρυθμίσεις σας</string> + <string name="app_version_name">Έκδοση εφαρμογής: v%s</string> + <string name="callstatus_error">σφάλμα κλήσης</string> + <string name="feature_cancelled_item">"ακυρώθηκε %s"</string> + <string name="icon_descr_cancel_link_preview">ακύρωση πρόβλεψη συνδέσμου</string> + <string name="change_database_passphrase_question">Αλλαγή κωδικού πρόσβασης βάση δεδομένων?</string> + <string name="la_change_app_passcode">Αλλαγή κωδικού πρόσβασης</string> + <string name="rcv_group_event_changed_member_role">Αλλαγή ρόλου του %s σε %s</string> + <string name="color_background">Φόντο</string> + <string name="database_initialization_error_title">Δεν είναι δυνατή η προετοιμασία της βάσης δεδομένων</string> + <string name="connect__a_new_random_profile_will_be_shared">Ένα νέο τυχαίο προφίλ θα μοιραστεί.</string> + <string name="alert_title_cant_invite_contacts">Δεν είναι δυνατή η πρόσκληση επαφών!</string> + <string name="switch_receiving_address">Αλλαγή διεύθυνσης λήψης</string> + <string name="auth_unavailable">Πιστοποίηση μη διαθέσιμη</string> + <string name="change_verb">Αλλαγή</string> + <string name="available_in_v51">" +\nΔιαθέσιμο στην έκδοση 5.1"</string> + <string name="icon_descr_call_ended">Τέλος κλήσης</string> + <string name="settings_section_title_calls">ΚΛΗΣΕΙΣ</string> + <string name="auto_accept_contact">Αυτόματη αποδοχή</string> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d αποτυχία κρυπτογράφησης μηνύματος</string> + <string name="snd_conn_event_switch_queue_phase_changing_for_member">αλλαγή διεύθυνσης για %s…</string> + <string name="change_member_role_question">Αλλαγή ρόλου ομάδας;</string> + <string name="invite_prohibited">Δεν είναι δυνατή η πρόσκληση επαφής!</string> + <string name="v4_2_auto_accept_contact_requests">Αυτόματη αποδοχή αιτήματος επαφής</string> + <string name="callstatus_calling">Κλήση…</string> + <string name="icon_descr_cancel_file_preview">Ακύρωση πρόβλεψη αρχείου</string> + <string name="block_member_confirmation">Φραγή</string> + <string name="icon_descr_cancel_image_preview">Ακύρωση πρόβλεψη εικόνας</string> + <string name="snd_conn_event_switch_queue_phase_changing">αλλαγή διεύθυνσης…</string> + <string name="v5_2_more_things">Μερικά παραπάνω πράγματα</string> + <string name="authentication_cancelled">Πιστοποίηση ακυρώθηκε</string> + <string name="change_self_destruct_passcode">Αλλαγή κωδικού αυτοκαταστροφής</string> + <string name="change_role">Αλλαγή ρολου</string> + <string name="icon_descr_cancel_live_message">Ακύρωση ζωντανών μυνημάτων</string> + <string name="all_app_data_will_be_cleared">Όλα τα δεδομένα της εφαρμογής διαγράφηκαν.</string> + <string name="appearance_settings">Εμφάνιση</string> + <string name="callstatus_ended">Τέλος κλήσης %1$s</string> + <string name="cancel_verb">Ακύρωση</string> + <string name="switch_receiving_address_question">Αλλαγή διεύθυνσης λήψης;</string> + <string name="rcv_conn_event_switch_queue_phase_changing">αλλαγή διεύθυνσης…</string> + <string name="change_self_destruct_mode">Αλλαγή λειτουργίας αυτοκαταστροφής</string> + <string name="use_camera_button">Κάμερα</string> + <string name="callstatus_in_progress">κλήση σε εξέλιξη</string> + <string name="auto_accept_images">Αυτόματη αποδοχή εικόνων</string> + <string name="rcv_group_event_changed_your_role">Αλλαγή του ρόλου σας σε %s</string> + <string name="icon_descr_call_progress">Κλήση σε εξέλιξη</string> + <string name="la_auth_failed">Πιστοποίηση απέτυχε</string> + <string name="connection_local_display_name">"σύνδεση %1$d"</string> + <string name="create_simplex_address">Δημιουργία διεύθυνση SimpleX</string> + <string name="status_contact_has_e2e_encryption">επαφή έχει κρυπτογράφηση από άκρο σε άκρο</string> + <string name="v5_4_incognito_groups_descr">Δημιουργία μια ομάδας χρησιμοποιώντας ένα τυχαίο προφίλ.</string> + <string name="create_group_button">Δημιουργία ομάδας</string> + <string name="create_another_profile_button">Δημιουργία προφίλ</string> + <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Η επαφή και όλα τα μηνύματα θα διαγραφούν - αυτό δεν μπορεί να αναιρεθεί!</string> + <string name="create_profile">Δημιουργία προφίλ</string> + <string name="contacts_can_mark_messages_for_deletion">Οι επαφές μπορούν να επισημάνουν μηνύματα προς διαγραφή; θα μπορείτε να τα δείτε.</string> + <string name="connect_via_invitation_link">Σύνδεση μέσω μιας εφάπαξ σύνδεσης;</string> + <string name="button_create_group_link">Δημιουργία σύνδεσμου</string> + <string name="connect_via_link_or_qr">Σύνδεση μέσω σύνδεσμο/κωδικό γρήγορης ανταπόκρισης</string> + <string name="connection_error_auth">Σφάλμα σύνδεσης (πιστοποίηση)</string> + <string name="group_member_status_connecting">συνδέεται</string> + <string name="group_connection_pending">συνδέεται…</string> + <string name="notification_preview_mode_contact">Όνομα επαφής</string> + <string name="create_address">Δημιουργία διεύθυνσης</string> + <string name="copy_verb">Αντιγραφή</string> + <string name="continue_to_next_step">Συνέχεια</string> + <string name="connect_plan_connect_via_link">Σύνδεση μέσω σύνδεσμο;</string> + <string name="contact_already_exists">Επαφή υπάρχει ήδη</string> + <string name="connect_plan_connect_to_yourself">Σύνδεση στον εαυτό σας;</string> + <string name="create_group">Δημιουργία μυστικής ομάδας</string> + <string name="connected_to_desktop">Συνδεδεμένη σε επιφάνεια εργασίας</string> + <string name="group_member_status_creator">δημιουργός</string> + <string name="connection_request_sent">Αίτημα σύνδεσης στάλθηκε!</string> + <string name="connecting_to_desktop">Συνδεμένο σε επιφάνεια εργασίας</string> + <string name="network_session_mode_entity">Σύνδεση</string> + <string name="correct_name_to">Διόρθωση ονόματος σε %s;</string> + <string name="connection_timeout">Λήξη χρονικού ορίου σύνδεσης</string> + <string name="group_member_status_accepted">σύνδεση (αποδεκτή)</string> + <string name="connect_with_contact_name_question">Σύνδεση με %1$s?</string> + <string name="create_your_profile">Δημιουργία του προφίλ σου</string> + <string name="display_name_connecting">συνδέεται…</string> + <string name="create_profile_button">Δημιουργία</string> + <string name="contact_preferences">Προτιμήσεις επαφής</string> + <string name="connected_mobile">Συνδεδεμένο κινητό</string> + <string name="info_row_connection">Σύνδεση</string> + <string name="create_chat_profile">Δημιουργία προφίλ συνομιλίας</string> + <string name="callstate_connecting">συνδέεται…</string> + <string name="desktop_connection_terminated">Σύνδεση τερματίστηκε</string> + <string name="server_connecting">Συνδε</string> + <string name="connect_via_link_incognito">Σύνδεση ανώνυμης περιήγησης</string> + <string name="display_name_connection_established">σύνδεση επετεύχθη</string> + <string name="status_contact_has_no_e2e_encryption">επαφή δεν έχει κρυπτογράφηση από άκρο σε άκρο</string> + <string name="chat_preferences_contact_allows">Επαφή επιτρέπει</string> + <string name="group_member_status_announced">σύνδεση (ανακοινώθηκε)</string> + <string name="icon_descr_server_status_connected">Συνδεδεμένος</string> + <string name="contact_connection_pending">συνδέεται…</string> + <string name="create_group_link">Δημιουργία σύνδεσμο ομάδας</string> + <string name="connect_to_desktop">Σύνδεση σε επιφάνεια εργασίας</string> + <string name="connected_to_mobile">Συνδεδεμένο στο κινητό</string> + <string name="connect_via_link">Σύνδεση μέσω σύνδεσμο</string> + <string name="receipts_section_contacts">Επαφές</string> + <string name="smp_server_test_create_file">Δημιουργία αρχείου</string> + <string name="create_secret_group_title">Δημιουργία μυστικής ομάδας</string> + <string name="connection_error">Σφάλμα σύνδεσης</string> + <string name="alert_title_contact_connection_pending">Η επαφή δεν είναι συνδράμει αυτή τη στιγμή!</string> + <string name="rcv_group_event_member_created_contact">Συνδεδεμένο απευθείας</string> + <string name="your_privacy">Η ιδιωτικότητά σας</string> + <string name="contact_sent_large_file">Η επαφή σας έστειλε ένα αρχείο το οποίο είναι μεγαλύτερο από το παρόν υποστηριζόμενο μέγεθος (%1$s).</string> + <string name="your_chat_profile_will_be_sent_to_your_contact">Το προφίλ της συνομιλίας σας θα σταλεί στην επαφή σας</string> + <string name="connect_use_new_incognito_profile">Χρήση νέου ανώνυμου προφίλ</string> + <string name="connect_plan_already_connecting">Ήδη συνδέεται</string> + <string name="you_rejected_group_invitation">Απορρίψατε την πρόσκληση της ομάδας</string> + <string name="connect_via_contact_link">Σύνδεση μέσω διεύθυνση επαφής</string> + <string name="connect_use_current_profile">Χρήση του τρέχων προφίλ</string> + <string name="connect_via_link_verb">Σύνδεση</string> + <string name="your_current_profile">Το τρέχον προφίλ σας</string> + <string name="snd_group_event_member_deleted">αφαιρέσατε %1$s</string> + <string name="connect_via_group_link">"Συμμετοχή ομάδας;"</string> + <string name="your_contacts_will_remain_connected">Οι επαφες σας θα παραμένουν συνδεδεμένες.</string> + <string name="trying_to_connect_to_server_to_receive_messages">Προσπάθεια σύνδεσης με τον διακομιστή που χρησιμοποιείται για τη λήψη μηνυμάτων από αυτήν την επαφή.</string> + <string name="profile_will_be_sent_to_contact_sending_link">Το προφίλ σου θα σταλεί στην επαφή από την οποία έλαβες αυτόν τον σύνδεσμο.</string> + <string name="server_error">σφάλμα</string> + <string name="opening_database">Άνοιγμα βάση δεδομένων…</string> + <string name="app_was_crashed">Η προβολή συνετρίβη</string> + <string name="server_connected">συνδεδεμένο</string> + <string name="non_content_uri_alert_text">Κοινοποιήσατε μια μη έγκυρη διαδρομή αρχείου. Αναφέρετε το πρόβλημα στους προγραμματιστές της εφαρμογής.</string> + <string name="non_content_uri_alert_title">Μη έγκυρη διαδρομή αρχείου</string> + <string name="connected_to_server_to_receive_messages_from_contact">Είστε συνδεδεμένοι στον διακομιστή που χρησιμοποιείται για τη λήψη μηνυμάτων από αυτήν την επαφή.</string> + <string name="marked_deleted_items_description">%d μύνημα επισημάνθηκε ως διαγραμμένο</string> + <string name="moderated_items_description">%1$d μυνήματα συντονίζονται από %2$s</string> + <string name="marked_deleted_description">επισημάνθηκε ως διαγραμμένο</string> + <string name="blocked_items_description">%d μυνήματα φραγμένα</string> + <string name="deleted_description">διαγράφτηκε</string> + <string name="moderated_item_description">με συντονιστή %s</string> + <string name="blocked_item_description">φραγμένος</string> + <string name="learn_more_about_address">Σχετικά με τη διεύθυνση SimpleX</string> + <string name="conn_event_ratchet_sync_started">συμφωνία κρυπτογράφησης…</string> + <string name="chat_theme_apply_to_all_modes">Όλες οι χρωματικές λειτουργίες</string> + <string name="abort_switch_receiving_address_desc">Η αλλαγή διεύθυνσης θα ακυρωθεί. Θα χρησιμοποιηθεί η παλιά διεύθυνση παραλαβής.</string> + <string name="servers_info_subscriptions_connections_subscribed">Ενεργές συνδέσεις</string> + <string name="network_settings_title">Προχωρημένες ρυθμίσεις</string> + <string name="color_primary_variant">Πρόσθετη προφορά</string> + <string name="add_contact_tab">Προσθήκη επαφής</string> + <string name="abort_switch_receiving_address">Διακοπή αλλαγής διεύθυνσης</string> + <string name="wallpaper_advanced_settings">Προχωρημένες ρυθμίσεις</string> + <string name="v5_6_safer_groups_descr">Οι διαχειριστές μπορούν να αποκλείσουν ένα μέλος για όλους.</string> + <string name="acknowledged">Αναγνωρισμένο</string> + <string name="above_then_preposition_continuation">παραπάνω, λοιπόν:</string> + <string name="add_address_to_your_profile">Προσθέστε τη διεύθυνση στο προφίλ σας, έτσι ώστε οι επαφές σας να μπορούν να τη μοιραστούν με άλλα άτομα. Το ενημέρωμένο προφίλ θα σταλεί στις επαφές σας.</string> + <string name="feature_roles_admins">διαχειριστές</string> + <string name="acknowledgement_errors">Λάθη αναγνώρισης</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Προειδοποίηση</b>: το αρχείο θα διαγραφεί.]]></string> + <string name="snd_error_quota">Υπέρβαση χωρητικότητας - ο παραλήπτης δεν έλαβε μηνύματα που στάλθηκαν προηγουμένως.</string> + <string name="blocked_by_admin_item_description">αποκλεισμένος από τον διαχειριστή</string> + <string name="your_chats">Συνομιλίες</string> + <string name="feature_roles_all_members">όλα τα μέλη</string> + <string name="all_your_contacts_will_remain_connected_update_sent">Όλες οι επαφές σας θα παραμείνουν ενεργές. Το ανανεωμένο προφίλ σας θα αποσταλεί στις επαφές σας.</string> + <string name="network_smp_proxy_mode_always_description">Να χρησιμοποιείται πάντα ιδιωτική δρομολόγηση.</string> + <string name="empty_chat_profile_is_created">Ένα κενό προφίλ συνομιλίας με το παρεχόμενο όνομα δημιουργείται και η εφαρμογή ανοίγει ως συνήθως.</string> + <string name="chat_database_deleted">Η βάση δεδομένων της συνομιλίας διαγράφηκε</string> + <string name="icon_descr_audio_off">Απενεργοποίηση ήχου</string> + <string name="icon_descr_audio_on">Eνεργοποίηση ήχου</string> + <string name="alert_title_msg_bad_hash">Κακό μήνυμα hash</string> + <string name="privacy_media_blur_radius">Θάμπωση των μέσων</string> + <string name="settings_section_title_chat_database">ΒΑΣΗ ΔΕΔΟΜΕΝΩΝ ΣΥΝΟΜΙΛΙΑΣ</string> + <string name="keychain_is_storing_securely">Το Android Keystore χρησιμοποιείται για την ασφαλή αποθήκευση της φράσης πρόσβασης - επιτρέπει την υπηρεσία ειδοποιήσεων να λειτουργεί.</string> + <string name="member_info_member_blocked">αποκλεισμένος</string> + <string name="member_blocked_by_admin">Αποκλεισμένος από τον διαχειριστή</string> + <string name="cant_call_contact_alert_title">Δεν είναι δυνατή η κλήση επαφής</string> + <string name="theme_destination_app_theme">Θέμα εφαρμογής</string> + <string name="chat_theme_apply_to_mode">Εφαρμογή σε</string> + <string name="v5_3_encrypt_local_files_descr">Η εφαρμογή κρυπτογραφεί νέα τοπικά αρχεία (εκτός απο βίντεο).</string> + <string name="v5_4_better_groups">Καλύτερες ομάδες</string> + <string name="connect_plan_already_joining_the_group">Γίνεται ήδη συμμετοχή στην ομάδα!</string> + <string name="migrate_from_device_archive_and_upload">Αρχειοθέτηση και αποστολή</string> + <string name="n_other_file_errors">%1$d διαφορετικό/κα σφάλμα/τα αρχείου/ων.</string> + <string name="notifications_mode_service_desc">Η υπηρεσία παρασκηνίου λειτουργεί πάντα - οι ειδοποιήσεις θα εμφανίζονται μόλις τα μηνύματα είναι διαθέσιμα.</string> + <string name="forward_files_in_progress_desc">%1$d αρχείο/α ακόμα κατεβαίνουν.</string> + <string name="forward_files_failed_to_receive_desc">%1$d αρχείο/α απέτυχε/χαν να παραληφθεί/ουν</string> + <string name="forward_files_missing_desc">%1$d αρχείο/α διαγράφηκε/καν.</string> + <string name="forward_files_not_accepted_desc">%1$d αρχείο/α δεν κατέβηκε/καν.</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s μήνυμα/τα δεν προωθήθηκε/καν</string> + <string name="network_session_mode_user">Προφίλ συνομιλίας</string> + <string name="network_session_mode_user_description"><![CDATA[Μια ξεχωριστή σύνδεση TCP (και διαπιστευτήριο SOCKS) θα χρησιμοποιηθεί <b>για κάθε προφίλ συνομιλίας που έχετε στην εφαρμογή</b>.]]></string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Παρακαλώ σημειώστε</b>: οι αναμεταδότες μηνυμάτων και αρχείων συνδέονται μέσω διακομιστή μεσολάβησης SOCKS. Οι κλήσεις και οι προεπισκοπήσεις συνδέσμων αποστολής χρησιμοποιούν άμεση σύνδεση.]]></string> + <string name="network_smp_proxy_mode_always">Πάντα</string> + <string name="app_check_for_updates_download_completed_title">Η ενημέρωση της εφαρμογής κατεβαίνει</string> + <string name="app_check_for_updates_notice_title">Έλεγχος για ενημερώσεις</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Οποιοσδήποτε μπορεί να φιλοξενήσει διακομιστές.</string> + <string name="audio_call_no_encryption">κλήση ήχου (χωρίς κρυπτογράφηση e2e)</string> + <string name="call_on_lock_screen">Κλήσεις στην οθόνη κλειδώματος:</string> + <string name="call_service_notification_audio_call">Κλήση ήχου</string> + <string name="app_version_code">\'Εκδοση Εφαρμογής: %s</string> + <string name="calls_prohibited_with_this_contact">Απαγορεύονται οι κλήσεις ήχου/βίντεο.</string> + <string name="v5_4_block_group_members">Αποκλεισμός μελών ομάδας</string> + <string name="chunks_deleted">Τα chunks διαγράφηκαν</string> + <string name="v5_1_self_destruct_passcode_descr">Όλα τα δεδομένα διαγράφονται κατά την εισαγωγή.</string> + <string name="deleted_chats">Αρχειοθετημένες επαφές</string> + <string name="migrate_from_device_cancel_migration">Ακύρωση μεταφοράς</string> + <string name="settings_section_title_chat_colors">Χρώματα συνομιλίας</string> + <string name="chat_database_section">ΒΑΣΗ ΔΕΔΟΜΕΝΩΝ ΣΥΝΟΜΙΛΙΑΣ</string> + <string name="chat_is_running">Η συνομιλία εκτελείται</string> + <string name="impossible_to_recover_passphrase"><![CDATA[<b>Παρακαλώ σημειώστε</b>: ΔΕΝ θα μπορείτε να ανακτήσετε ή να αλλάξετε τη φράση πρόσβασης εάν τη χάσετε.]]></string> + <string name="block_for_all">Αποκλεισμός για όλους</string> + <string name="both_you_and_your_contact_can_add_message_reactions">Και εσείς και η επαφή σας μπορείτε να προσθέστε αντιδράσεις μηνυμάτων.</string> + <string name="both_you_and_your_contact_can_make_calls">Και εσείς και η επαφή σας μπορείτε να κάνετε κλήσεις.</string> + <string name="allow_to_send_simplex_links">Επιτρέψτε την αποστολή συνδέσμων SimpleX.</string> + <string name="v5_3_new_interface_languages_descr">Αραβικά, Βουλγαρικά, Φινλανδικά, Εβραϊκά, Ταϊλανδέζικα και Ουκρανικά - χάρη στους χρήστες και το Weblate.</string> + <string name="v5_6_app_data_migration">Μεταφορά δεδομένων εφαρμογής</string> + <string name="v6_0_privacy_blur">Θάμπωμα για καλύτερη ιδιωτικότητα.</string> + <string name="migrate_to_device_chat_migrated">Η συνομιλία έχει μεταφερθεί!</string> + <string name="migrate_from_device_archiving_database">Αρχειοθέτηση της βάσης δεδομένων</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Όλες οι επαφές, συζητήσεις και αρχεία θα κρυπτογραφηθούν με ασφάλεια και θα μεταφορτωθούν σε διαμορφωμένα κομμάτια αναμετάδοσης XFTP.</string> + <string name="network_type_cellular">Κινητή τηλεφωνία</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b> Δημιουργία ομάδας </b>: για την δημιουργίας νέας ομάδας.]]></string> + <string name="migrate_from_device_check_connection_and_try_again">Ελέγξτε τη σύνδεσή σας στο διαδίκτυο και δοκιμάστε ξανά</string> + <string name="chat_with_developers">Συζήτηση με τους προγραμματιστές</string> + <string name="icon_descr_video_asked_to_receive">Ζήτησε να λάβει το βίντεο</string> + <string name="cant_send_message_to_member_alert_title">Δεν είναι δυνατή η αποστολή μηνυμάτων στο μέλος της ομάδας</string> + <string name="change_lock_mode">Αλλαγή λειτουργίας κλειδώματος</string> + <string name="rcv_group_event_member_blocked">αποκλεισμένος %s</string> + <string name="rcv_conn_event_switch_queue_phase_completed">άλλαξε η διεύθυνση για εσάς</string> + <string name="rcv_group_and_other_events">και %d άλλες εκδηλώσεις</string> + <string name="theme_black">Μαύρο</string> + <string name="color_secondary_variant">Πρόσθετο δευτερεύον</string> + <string name="both_you_and_your_contacts_can_delete">Και εσείς και η επαφή σας μπορείτε να διαγράψετε απεσταλμένα μηνύματα χωρίς ανατροπή. (24 ώρες)</string> + <string name="both_you_and_your_contact_can_send_voice">Και εσείς και η επαφή σας μπορείτε να στείλετε ηχητικά μηνύματα.</string> + <string name="chat_is_stopped_indication">Η συνομιλία σταμάτησε</string> + <string name="chat_is_stopped_you_should_transfer_database">Η συνομιλία έχει διακοπεί. Εάν χρησιμοποιήσατε ήδη αυτήν τη βάση δεδομένων σε άλλη συσκευή, θα πρέπει να τη μεταφέρετε πίσω προτού ξεκινήσετε τη συνομιλία.</string> + <string name="turning_off_service_and_periodic">Η λειτουργία βελτιστοποίησης της μπαταρίας είναι ενεργή, η υπηρεσία παρασκηνίου και τα περιοδικά αιτήματα για νέα μηνύματα θα απενεργοποιηθούν. Μπορείτε να τα ενεργοποιήσετε ξανά μέσω των ρυθμίσεων.</string> + <string name="one_time_link_short">σύνδεσμος μιας χρήσης</string> + <string name="settings_audio_video_calls">Κλήσεις ήχου & βίντεο</string> + <string name="audio_video_calls">Κλήσεις ήχου/βίντεο</string> + <string name="la_app_passcode">Κωδικός εφαρμογής</string> + <string name="network_session_mode_session">Συνεδρία εφαρμογής</string> + <string name="chat_is_stopped">Η συνομιλία σταμάτησε</string> + <string name="app_check_for_updates">Έλεγχος για ενημερώσεις</string> + <string name="v4_6_chinese_spanish_interface">Κινεζική και Ισπανική διεπαφή</string> + <string name="v6_1_message_dates_descr">Καλύτερες ημερομηνίες μηνυμάτων</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="bold_text">έντονο</string> + <string name="chat_console">Κονσόλα συνομιλίας</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Παρακαλώ σημειώστε</b>: η χρήση της ίδιας βάσης δεδομένων σε δύο συσκευές θα διακόψει την αποκρυπτογράφηση των μηνυμάτων από τις συνδέσεις σας, ως προστασία ασφαλείας.]]></string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Χρησιμοποιεί περισσότερη μπαταρία</b>! Η εφαρμογή εκτελείται πάντα στο παρασκήνιο - οι ειδοποιήσεις εμφανίζονται αμέσως.]]></string> + <string name="chat_database_exported_title">Η βάση δεδομένων της συνομιλίας εξάχθηκε</string> + <string name="info_view_call_button">κλήση</string> + <string name="bad_desktop_address">Κακή διεύθυνση Desktop</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Επιλέξτε <i> Μεταφορά απο άλλη συσκευή </i> στη νέα συσκευή και σαρώστε τον κωδικό QR.]]></string> + <string name="v4_5_transport_isolation_descr">Με προφίλ συνομιλίας (προεπιλογή) ή μέσω σύνδεσης (BETA).</string> + <string name="permissions_camera_and_record_audio">Κάμερα και μικρόφωνο</string> + <string name="v5_3_new_interface_languages">6 νέες γλώσσες διεπαφής</string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Καλό για την μπαταρία</b>. Η εφαρμογή ελέγχει για την παραλαβή μηνυμάτων κάθε 10 λεπτά. Ενδέχεται να χάσετε κλήσεις ή επείγοντα μηνύματα.]]></string> + <string name="attach">Επισύναψη</string> + <string name="abort_switch_receiving_address_question">Διακοπή αλλαγής διεύθυνσης;</string> + <string name="choose_file_title">Επιλέξτε ένα αρχείο</string> + <string name="block_member_desc">Όλα τα νέα μηνύνματα απο %s θα αποκρυφθούν!</string> + <string name="cannot_receive_file">Δεν είναι δυνατή η λήψη του αρχείου</string> + <string name="la_authenticate">Πιστοποίηση</string> + <string name="clear_note_folder_warning">Όλα τα μηνύματα θα διαγραφούν - αυτή η ενέργεια δεν μπορεί να αντιστραφεί!</string> + <string name="notifications_mode_periodic_desc">Ελέγχει νέα μηνύματα κάθε 10 λεπτά για έως και 1 λεπτό</string> + <string name="notifications_mode_off_desc">Η εφαρμογή μπορεί να λαμβάνει ειδοποιήσεις μόνο όταν εκτελείται, καμία υπηρεσία δεν θα ξεκινήσει στο παρασκήνιο</string> + <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Μπορεί να απενεργοποιηθεί μέσω των ρυθμίσεων</b> – οι ειδοποιήσεις θα εξακολουθούν να εμφανίζονται ενώ η εφαρμογή εκτελείται.]]></string> + <string name="allow_your_contacts_adding_message_reactions">Επιτρέψτε τις επαφές σας να χρησιμοποιούν αντιδράσεις μηνυμάτων.</string> + <string name="both_you_and_your_contact_can_send_disappearing">Και εσείς και η επαφή σας μπορείτε να στείλετε μηνύματα που εξαφανίζονται.</string> + <string name="camera_not_available">Κάμερα μη διαθέσιμη</string> + <string name="smp_servers_check_address">Ελέγξτε την διεύθυνση του διακομιστή και δοκιμάστε ξανά.</string> + <string name="allow_message_reactions_only_if">Επιτρέψτε αντιδράσεις μηνυμάτων εφόσον οι επαφές σας το επιτρέπουν.</string> + <string name="alert_text_decryption_error_too_many_skipped">%1$d μήνυμα/τα παραλήφθηκε/καν.</string> + <string name="calls_prohibited_alert_title">Κλήσεις απογορευμένες!</string> + <string name="cannot_share_message_alert_title">Δεν είναι δυνατή η αποστολή μηνύματος</string> + <string name="call_already_ended">Η κλήση έχει ήδη τερματιστεί!</string> + <string name="app_passcode_replaced_with_self_destruct">Ο κωδικός πρόσβασης της εφαρμογής αντικαθίσταται με κωδικό πρόσβασης αυτοκαταστροφής.</string> + <string name="keychain_allows_to_receive_ntfs">Το Android Keystore θα χρησιμοποιηθεί για την ασφαλή αποθήκευση της φράσης πρόσβασης μετά την επανεκκίνηση της εφαρμογής ή την αλλαγή της φράσης πρόσβασης - θα επιτρέπει τη λήψη ειδοποιήσεων.</string> + <string name="cannot_access_keychain">Δεν είναι δυνατή η πρόσβαση στο Keystore για αποθήκευση του κωδικού πρόσβασης της βάσης δεδομένων</string> + <string name="block_member_button">Αποκλεισμός μέλους</string> + <string name="block_member_question">Αποκλεισμός μέλους;</string> + <string name="chat_preferences">Προτιμήσεις συνομιλίας</string> + <string name="v5_1_better_messages">Καλύτερα μηνύματα</string> + <string name="migrate_to_device_apply_onion">Εφαρμογή</string> + <string name="network_smp_proxy_fallback_allow_downgrade">Συνέναιση υποβάθμισης</string> + <string name="permissions_camera">Κάμερα</string> + <string name="icon_descr_audio_call">κλήση ήχου</string> + <string name="v6_0_your_contacts_descr">Αρχειοθετήστε τις επαφές για να συνομιλήσετε αργότερα.</string> + <string name="all_users">Όλα τα προφίλ</string> + <string name="integrity_msg_skipped">%1$d μηνύμα/τα παραλείφθηκε/καν</string> + <string name="integrity_msg_bad_hash">κακό μήνυμα hash</string> + <string name="integrity_msg_bad_id">κακό αναγνωριστικό μηνύματος</string> + <string name="answer_call">Απάντηση κλήσης</string> + <string name="alert_title_msg_bad_id">Κακό αναγνωριστικό μηνύματος</string> + <string name="settings_section_title_chats">ΣΥΝΟΜΙΛΙΕΣ</string> + <string name="chat_database_imported">Η βάση δεδεδομένων της συνομιλίας εισάχθηκε</string> + <string name="snd_conn_event_ratchet_sync_started">"συμφωνία κρυπτογράφησης για %s…"</string> + <string name="allow_calls_question">Να επιτραπούν οι κλήσεις;</string> + <string name="block_for_all_question">Αποκλεισμός μέλους για όλους;</string> + <string name="v4_6_audio_video_calls">Κλήσεις ήχου και βίντεο</string> + <string name="attempts_label">προσπάθειες</string> + <string name="settings_section_title_chat_theme">Θέμα συνομιλίας</string> + <string name="v6_1_better_security">Καλύτερη ασφάλεια✅</string> + <string name="v6_1_better_user_experience">Καλύτερη εμπειρία χρήστη</string> + <string name="cant_call_member_alert_title">Δεν είναι δυνατή η κλήση μέλους ομάδας</string> + <string name="icon_descr_asked_to_receive">Ζήτησε να λάβει την εικόνα</string> + <string name="network_session_mode_entity_description"><![CDATA[Μια ξεχωριστή σύνδεση TCP (και διαπιστευτήριο SOCKS) θα χρησιμοποιηθεί <b> για κάθε επαφή και μέλος ομάδας </b>.\n<b>Λάβετε υπόψη</b>: εάν έχετε πολλές συνδέσεις, η κατανάλωση της μπαταρίας και της κυκλοφορίας μπορεί να είναι σημαντικά υψηλότερη και ορισμένες συνδέσεις μπορεί να αποτύχουν.]]></string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b> Προσθήκη επαφής </b>: για να δημιουργήσετε έναν νέο σύνδεσμο πρόσκλησης ή να συνδεθείτε μέσω ενός συνδέσμου που λάβατε.]]></string> + <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b> Καλύτερο για τη ζωή της μπαταρίας </b>. Θα λαμβάνετε ειδοποιήσεις μόνο όταν εκτελείται η εφαρμογή (ΧΩΡΙΣ υπηρεσία παρασκηνίου).]]></string> + <string name="app_check_for_updates_beta">Beta</string> + <string name="v6_1_better_calls">Καλύτερες κλήσεις</string> </resources> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index 246ed36859..5667c42d2d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -3,42 +3,40 @@ <string name="auth_unavailable">Autenticación no disponible</string> <string name="accept_contact_button">Aceptar</string> <string name="network_settings">Configuración avanzada de red</string> - <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Mejor para la batería</b>. Recibirás notificaciones sólo cuando la aplicación se esté ejecutando, SIN servicio en segundo plano.]]></string> - <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Bueno para la batería</b>. El servicio en segundo plano comprueba si hay mensajes cada 10 minutos. Puedes perder llamadas o mensajes urgentes.]]></string> + <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Óptimo para la batería</b>. Recibirás notificaciones sólo cuando la aplicación esté abierta (SIN servicio en segundo plano).]]></string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Bueno para la batería</b>. La aplicación comprueba si hay mensajes cada 10 minutos. Podrías perderte llamadas o mensajes urgentes.]]></string> <string name="accept_call_on_lock_screen">Aceptar</string> <string name="full_backup">Copia de seguridad de los datos de la aplicación</string> - <string name="chat_item_ttl_day">un dia</string> - <string name="chat_item_ttl_month">un mes</string> - <string name="chat_item_ttl_week">una semana</string> + <string name="chat_item_ttl_day">1 día</string> + <string name="chat_item_ttl_month">1 mes</string> + <string name="chat_item_ttl_week">1 semana</string> <string name="allow_disappearing_messages_only_if">Se permiten los mensajes temporales pero sólo si tu contacto también los permite.</string> <string name="v4_3_improved_server_configuration_desc">Añadir servidores mediante el escaneo de códigos QR.</string> <string name="smp_servers_preset_add">Añadir servidores predefinidos</string> <string name="all_group_members_will_remain_connected">Todos los miembros del grupo permanecerán conectados.</string> - <string name="allow_irreversible_message_deletion_only_if">Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también lo permite para tí.</string> - <string name="keychain_allows_to_receive_ntfs">Android Keystore se usará para almacenar de forma segura la contraseña después de reiniciar la aplicación o cambiar la frase de contraseña - permitirá recibir notificaciones.</string> + <string name="allow_irreversible_message_deletion_only_if">Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también lo permite para tí. (24 horas)</string> + <string name="keychain_allows_to_receive_ntfs">Android Keystore se usará para almacenar de forma segura la frase de contraseña después de cambiarla o reiniciar la aplicación - permitirá recibir notificaciones.</string> <string name="allow_your_contacts_to_send_disappearing_messages">Permites a tus contactos enviar mensajes temporales</string> <string name="allow_your_contacts_to_send_voice_messages">Permites a tus contactos enviar mensajes de voz.</string> <string name="chat_preferences_always">siempre</string> <string name="notifications_mode_off_desc">La aplicación sólo puede recibir notificaciones cuando se está ejecutando. No se iniciará ningún servicio en segundo plano.</string> - <string name="settings_section_title_icon">ICONO APLICACIÓN</string> + <string name="settings_section_title_icon">ICONO DE LA APLICACIÓN</string> <string name="turning_off_service_and_periodic">La optimización de la batería está activa, desactivando el servicio en segundo plano y las solicitudes periódicas de nuevos mensajes. Puedes volver a activarlos en Configuración.</string> <string name="notifications_mode_service_desc">El servicio está siempre en funcionamiento en segundo plano. Las notificaciones se muestran en cuanto haya mensajes nuevos.</string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Se puede desactivar en Configuración</b> – las notificaciones se seguirán mostrando mientras la app esté en funcionamiento.]]></string> <string name="notifications_mode_service">Siempre activo</string> <string name="allow_verb">Permitir</string> <string name="above_then_preposition_continuation">y después:</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Añadir nuevo contacto</b>: para crear tu código QR de un solo uso para tu contacto.]]></string> <string name="accept_connection_request__question">¿Aceptar solicitud de conexión\?</string> <string name="accept_contact_incognito_button">Aceptar incógnito</string> - <string name="clear_chat_warning">Se eliminarán todos los mensajes SOLO para tí. ¡No podrá deshacerse!</string> - <string name="smp_servers_add">Añadir servidor…</string> + <string name="clear_chat_warning">Se eliminarán todos los mensajes SOLO para tí. ¡No puede deshacerse!</string> + <string name="smp_servers_add">Añadir servidor</string> <string name="network_enable_socks_info">¿Acceder a los servidores a través del proxy SOCKS en el puerto %d\? El proxy debe iniciarse antes de activar esta opción.</string> <string name="all_your_contacts_will_remain_connected">Todos tus contactos permanecerán conectados.</string> <string name="appearance_settings">Apariencia</string> <string name="app_version_title">Versión</string> <string name="network_session_mode_user_description"><![CDATA[Se usará una conexión TCP (y credenciales SOCKS) independiente <b>por cada perfil que tengas en la aplicación</b>.]]></string> - <string name="network_session_mode_entity_description">Se usará una conexión TCP (y credenciales SOCKS) independiente <b>por cada contacto y miembro del grupo</b>. -\n<b>Atención</b>: si tienes muchas conexiones, el consumo de batería y tráfico pueden ser sustancialmente mayores y algunas conexiones pueden fallar.</string> + <string name="network_session_mode_entity_description"><![CDATA[Se usará una conexión TCP (y credenciales SOCKS) independiente <b>por cada contacto y miembro del grupo</b>. \n<b>Recuerda</b>: si tienes muchas conexiones, el consumo de batería y tráfico pueden aumentar bastante y algunas conexiones pueden fallar.]]></string> <string name="a_plus_b">a + b</string> <string name="about_simplex">Acerca de SimpleX</string> <string name="bold_text">negrita</string> @@ -51,16 +49,16 @@ <string name="icon_descr_audio_on">Audio activado</string> <string name="integrity_msg_bad_id">ID de mensaje erróneo</string> <string name="auto_accept_images">Auto aceptar imágenes</string> - <string name="users_delete_all_chats_deleted">Se eliminarán todos los chats y mensajes. ¡No podrá deshacerse!</string> + <string name="users_delete_all_chats_deleted">Se eliminarán todos los chats y mensajes. ¡No puede deshacerse!</string> <string name="accept_feature">Aceptar</string> <string name="allow_to_send_disappearing">Se permiten mensajes temporales.</string> - <string name="keychain_is_storing_securely">Android Keystore se usará para almacenar de forma segura la contraseña - permite que el servicio de notificación funcione.</string> + <string name="keychain_is_storing_securely">Android Keystore se usará para almacenar de forma segura la frase de contraseña - permite que el servicio de notificación funcione.</string> <string name="users_add">Añadir perfil</string> <string name="color_primary">Color</string> - <string name="allow_your_contacts_irreversibly_delete">Permites a tus contactos eliminar irreversiblemente los mensajes enviados.</string> + <string name="allow_your_contacts_irreversibly_delete">Permites a tus contactos eliminar irreversiblemente los mensajes enviados. (24 horas)</string> <string name="allow_voice_messages_only_if">Se permiten los mensajes de voz pero sólo si tu contacto también los permite.</string> <string name="allow_direct_messages">Se permiten mensajes directos entre miembros.</string> - <string name="allow_to_delete_messages">Se permite la eliminación irreversible de mensajes.</string> + <string name="allow_to_delete_messages">Se permite la eliminación irreversible de mensajes. (24 horas)</string> <string name="allow_to_send_voice">Se permiten mensajes de voz.</string> <string name="v4_2_group_links_desc">Los administradores pueden crear enlaces para unirse a grupos.</string> <string name="v4_2_auto_accept_contact_requests">Auto aceptar solicitudes de contacto</string> @@ -74,30 +72,28 @@ <string name="smp_servers_add_to_another_device">Añadir a otro dispositivo</string> <string name="app_version_name">Versión de la aplicación: v%s</string> <string name="icon_descr_asked_to_receive">Solicita recibir la imagen</string> - <string name="impossible_to_recover_passphrase"><![CDATA[<b>Atención</b>: NO podrás recuperar o cambiar la contraseña si la pierdes.]]></string> + <string name="impossible_to_recover_passphrase"><![CDATA[<b>Recuerda</b>: Si la pierdes NO podrás recuperar o cambiar la frase de contraseña.]]></string> <string name="both_you_and_your_contact_can_send_voice">Tanto tú como tu contacto podéis enviar mensajes de voz.</string> - <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>¡Consume más batería!</b> El servicio en segundo plano se ejecuta siempre y las notificaciones se mostrarán en cuanto haya mensajes disponibles.]]></string> - <string name="both_you_and_your_contacts_can_delete">Tanto tú como tu contacto podéis eliminar de forma irreversible los mensajes enviados.</string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>¡Consume más energía!</b> La aplicación está siempre en segundo plano y las notificaciones se muestran de inmediato.]]></string> + <string name="both_you_and_your_contacts_can_delete">Tanto tú como tu contacto podéis eliminar los mensajes enviados de forma irreversible. (24 horas)</string> <string name="both_you_and_your_contact_can_send_disappearing">Tanto tú como tu contacto podéis enviar mensajes temporales.</string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Escanear código QR</b>: para conectar con tu contacto mediante su código QR.]]></string> <string name="create_profile_button">Crear</string> - <string name="create_one_time_link">Crea enlace de invitación de un uso</string> <string name="create_group">Crea grupo secreto</string> <string name="database_passphrase_will_be_updated">La contraseña de cifrado de la base de datos será actualizada.</string> <string name="info_row_database_id">ID base de datos</string> - <string name="direct_messages_are_prohibited_in_chat">Los mensajes directos entre miembros del grupo no están permitidos.</string> - <string name="passphrase_is_different">La contraseña es distinta a la almacenada en Keystore</string> + <string name="direct_messages_are_prohibited_in_group">Los mensajes directos entre miembros del grupo no están permitidos.</string> + <string name="passphrase_is_different">La contraseña de la base de datos es diferente a la almacenada en Keystore.</string> <string name="database_will_be_encrypted_and_passphrase_stored">La base de datos será cifrada y la contraseña se guardará en Keystore.</string> <string name="delete_contact_question">¿Eliminar contacto\?</string> <string name="delete_message__question">¿Eliminar mensaje\?</string> - <string name="delete_chat_profile_question">¿Eliminar el perfil de chat\?</string> - <string name="rcv_group_event_group_deleted">grupo eliminado</string> + <string name="delete_chat_profile_question">¿Eliminar perfil?</string> + <string name="rcv_group_event_group_deleted">ha eliminado el grupo</string> <string name="delete_group_question">¿Eliminar grupo\?</string> <string name="delete_messages_after">Eliminar en</string> <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Autenticación de dispositivo desactivada. Puedes habilitar Bloqueo SimpleX en Configuración, después de activar la autenticación de dispositivo.</string> <string name="no_call_on_lock_screen">Desactivar</string> <string name="disappearing_prohibited_in_this_chat">Los mensajes temporales no están permitidos en este chat.</string> - <string name="disappearing_messages_are_prohibited">Los mensajes temporales no están permitidos en este grupo.</string> + <string name="disappearing_messages_are_prohibited">Mensajes temporales no permitidos.</string> <string name="display_name_cannot_contain_whitespace">El nombre mostrado no puede contener espacios en blanco.</string> <string name="encrypted_video_call">Videollamada con cifrado de extremo a extremo</string> <string name="display_name_connection_established">conexión establecida</string> @@ -112,13 +108,13 @@ <string name="encrypted_audio_call">Llamada con cifrado de extremo a extremo</string> <string name="status_e2e_encrypted">cifrado de extremo a extremo</string> <string name="integrity_msg_duplicate">mensaje duplicado</string> - <string name="settings_developer_tools">Herramientas desarrollo</string> + <string name="settings_developer_tools">Herramientas para desarrolladores</string> <string name="delete_files_and_media_for_all_users">Eliminar los archivos de todos los perfiles</string> - <string name="delete_messages">Eliminar mensaje</string> + <string name="delete_messages">Activar</string> <string name="database_encrypted">¡Base de datos cifrada!</string> <string name="encrypted_with_random_passphrase">La base de datos está cifrada con una contraseña aleatoria, puedes cambiarla.</string> - <string name="database_error">Error base de datos</string> - <string name="database_passphrase_is_required">Para abrir la aplicación se requiere la contraseña de la base de datos</string> + <string name="database_error">Error en base de datos</string> + <string name="database_passphrase_is_required">Se requiere la contraseña de la base de datos para abrir la aplicación.</string> <string name="rcv_group_event_member_connected">conectado</string> <string name="button_create_group_link">Crear enlace</string> <string name="delete_link_question">¿Eliminar enlace\?</string> @@ -133,14 +129,14 @@ <string name="ttl_week">%d semana</string> <string name="ttl_weeks">%d semanas</string> <string name="v4_4_disappearing_messages">Mensajes temporales</string> - <string name="auth_confirm_credential">Canfirma tus credenciales</string> + <string name="auth_confirm_credential">Confirma tus credenciales</string> <string name="group_member_status_introduced">conectando (presentado)</string> <string name="group_member_status_intro_invitation">conectando (invitación de presentación )</string> <string name="group_member_status_accepted">conectando (aceptado)</string> <string name="group_member_status_announced">conectando (anunciado)</string> <string name="connection_local_display_name">conexión %1$d</string> <string name="connect_via_link_or_qr">Conecta vía enlace / Código QR</string> - <string name="delete_contact_all_messages_deleted_cannot_undo_warning">El contacto y todos los mensajes serán eliminados. ¡No podrá deshacerse!</string> + <string name="delete_contact_all_messages_deleted_cannot_undo_warning">El contacto y todos los mensajes serán eliminados. ¡No puede deshacerse!</string> <string name="icon_descr_contact_checked">Contacto verificado</string> <string name="status_contact_has_e2e_encryption">el contacto dispone de cifrado de extremo a extremo</string> <string name="smp_server_test_disconnect">Desconectar</string> @@ -156,27 +152,27 @@ <string name="delete_after">Eliminar en</string> <string name="ttl_sec">%d seg</string> <string name="contact_already_exists">El contácto ya existe</string> - <string name="connection_error_auth">Error conexión (Autenticación)</string> + <string name="connection_error_auth">Error de conexión (Autenticación)</string> <string name="for_me_only">Eliminar para mí</string> <string name="icon_descr_server_status_disconnected">Desconectado</string> <string name="icon_descr_server_status_connected">Conectado</string> <string name="copied">Copiado en portapapeles</string> <string name="share_one_time_link">Crea enlace de invitación de un uso.</string> - <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 PC: escanéa el código QR desde la aplicación mediante <b>Escanear código QR </b>]]></string> + <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 Ordenador: escanea el código QR desde la aplicación mediante <b>Escanear código QR </b>]]></string> <string name="delete_contact_menu_action">Eliminar</string> <string name="delete_group_menu_action">Eliminar</string> <string name="alert_title_contact_connection_pending">¡El contacto aun no se ha conectado!</string> - <string name="delete_pending_connection__question">¿Eliminar la conexion pendiente\?</string> + <string name="delete_pending_connection__question">¿Eliminar conexión pendiente?</string> <string name="icon_descr_close_button">Botón cerrar</string> <string name="connection_request_sent">¡Solicitud de conexión enviada!</string> <string name="network_session_mode_entity">por conexión</string> <string name="callstatus_connecting">conectando llamada…</string> <string name="allow_accepting_calls_from_lock_screen">Activar llamadas desde la pantalla de bloqueo en Configuración</string> <string name="database_encryption_will_be_updated">La contraseña de cifrado de la base de datos será actualizada y almacenada en Keystore.</string> - <string name="group_member_status_connecting">conectando</string> + <string name="group_member_status_connecting">conectando...</string> <string name="group_member_status_creator">creador</string> <string name="ttl_min">%d min</string> - <string name="connection_timeout">Tiempo de conexión expirado</string> + <string name="connection_timeout">Tiempo de conexión agotado</string> <string name="failed_to_create_user_duplicate_title">¡Nombre mostrado duplicado!</string> <string name="connection_error">Error conexión</string> <string name="smp_server_test_create_queue">Crear cola</string> @@ -184,20 +180,19 @@ <string name="notification_preview_somebody">Contacto oculto:</string> <string name="delete_verb">Eliminar</string> <string name="edit_verb">Editar</string> - <string name="delete_member_message__question">¿Eliminar el mensaje de miembro\?</string> + <string name="delete_member_message__question">¿Eliminar el mensaje del miembro?</string> <string name="icon_descr_edited">editado</string> <string name="image_decoding_exception_title">Error de decodificación</string> <string name="create_address">Crear dirección</string> <string name="delete_address">Eliminar dirección</string> <string name="delete_address__question">¿Eliminar la dirección\?</string> - <string name="display_name__field">Nombre mostrado:</string> + <string name="display_name__field">Nombre del perfil:</string> <string name="callstate_connecting">conectando…</string> <string name="decentralized">Descentralizada</string> <string name="database_will_be_encrypted">La base de datos será cifrada.</string> - <string name="delete_chat_archive_question">¿Eliminar archivo del chat\?</string> <string name="create_group_link">Crear enlace de grupo</string> <string name="delete_link">Eliminar enlace</string> - <string name="users_delete_question">¿Eliminar el perfil de chat\?</string> + <string name="users_delete_question">¿Eliminar perfil?</string> <string name="ttl_hour">%d hora</string> <string name="ttl_day">%d día</string> <string name="v4_5_multiple_chat_profiles_descr">Nombre y avatar diferentes, aislamiento de transporte.</string> @@ -213,13 +208,11 @@ <string name="button_delete_group">Eliminar grupo</string> <string name="button_edit_group_profile">Editar perfil de grupo</string> <string name="info_row_connection">Conexión</string> - <string name="users_delete_profile_for">Eliminar el perfil de chat para</string> + <string name="users_delete_profile_for">Eliminar el perfil</string> <string name="theme_dark">Oscuro</string> <string name="ttl_d">%dd</string> <string name="ttl_days">%d días</string> <string name="delete_files_and_media_question">¿Eliminar archivos y multimedia\?</string> - <string name="archive_created_on_ts">Creado: %1$s</string> - <string name="delete_archive">Eliminar archivo</string> <string name="group_member_status_connected">conectado</string> <string name="conn_level_desc_direct">directa</string> <string name="chat_preferences_contact_allows">El contacto permite</string> @@ -230,7 +223,7 @@ <string name="ttl_w">%ds</string> <string name="deleted_description">eliminado</string> <string name="connect_via_contact_link">¿Conectar mediante dirección de contacto?</string> - <string name="connect_via_group_link">¿Unirse al grupo?</string> + <string name="connect_via_group_link">¿Unirte al grupo?</string> <string name="connect_via_invitation_link">¿Conectar mediante enlace de invitación?</string> <string name="connect_via_link_verb">Conectar</string> <string name="server_connected">conectado</string> @@ -245,8 +238,7 @@ <string name="icon_descr_email">Email</string> <string name="connect_button">Conectar</string> <string name="connect_via_link">Conectar mediante enlace</string> - <string name="database_passphrase_and_export">Base de Datos y -\nContraseña</string> + <string name="database_passphrase_and_export">Base de Datos y Contraseña</string> <string name="contribute">Contribuye</string> <string name="core_version">Core versión: v%s</string> <string name="delete_image">Eliminar imagen</string> @@ -255,7 +247,7 @@ <string name="change_verb">Cambiar</string> <string name="notifications_mode_periodic_desc">Se realizan comprobaciones de mensajes nuevos periódicas de hasta un minuto de duración cada 10 minutos</string> <string name="clear_contacts_selection_button">Limpiar</string> - <string name="change_member_role_question">¿Cambiar rol de grupo\?</string> + <string name="change_member_role_question">¿Cambiar rol?</string> <string name="v4_4_verify_connection_security_desc">Compara los códigos de seguridad con tus contactos</string> <string name="choose_file">Archivo</string> <string name="clear_verb">Vaciar</string> @@ -267,24 +259,24 @@ <string name="callstatus_in_progress">llamada en curso</string> <string name="colored_text">coloreado</string> <string name="rcv_group_event_changed_your_role">ha cambiado tu rol a %s</string> - <string name="snd_conn_event_switch_queue_phase_changing_for_member">cambiando de servidor para %s</string> + <string name="snd_conn_event_switch_queue_phase_changing_for_member">cambiando el servidor para %s</string> <string name="group_member_status_complete">completado</string> <string name="invite_prohibited">¡No se puede invitar el contacto!</string> <string name="cannot_receive_file">No se puede recibir el archivo</string> <string name="database_initialization_error_title">No se puede iniciar la base de datos</string> <string name="clear_chat_question">¿Vaciar chat\?</string> <string name="network_session_mode_user">por perfil</string> - <string name="chat_is_stopped_indication">Chat está detenido</string> + <string name="chat_is_stopped_indication">SimpleX está parado</string> <string name="rcv_group_event_changed_member_role">rol de %s cambiado a %s</string> <string name="change_role">Cambiar rol</string> - <string name="v4_5_transport_isolation_descr">Mediante perfil (por defecto) o por conexión (BETA)</string> + <string name="v4_5_transport_isolation_descr">Mediante perfil (predeterminado) o por conexión (BETA)</string> <string name="snd_conn_event_switch_queue_phase_changing">cambiando de servidor…</string> - <string name="chat_preferences">Preferencias de Chat</string> + <string name="chat_preferences">Preferencias generales</string> <string name="feature_cancelled_item">cancelado %s</string> - <string name="chat_is_stopped">Chat está detenido</string> + <string name="chat_is_stopped">SimpleX está parado</string> <string name="settings_section_title_calls">LLAMADAS</string> - <string name="chat_is_running">Chat está en ejecución</string> - <string name="rcv_conn_event_switch_queue_phase_changing">cambiando de servidor…</string> + <string name="chat_is_running">SimpleX está en ejecución</string> + <string name="rcv_conn_event_switch_queue_phase_changing">está cambiando de servidor…</string> <string name="chat_with_developers">habla con los desarrolladores</string> <string name="icon_descr_cancel_file_preview">Cancelar vista previa del archivo</string> <string name="icon_descr_cancel_image_preview">Cancelar vista previa de la imagen</string> @@ -292,87 +284,85 @@ <string name="icon_descr_call_progress">Llamada en curso</string> <string name="change_database_passphrase_question">¿Cambiar contraseña de la base de datos\?</string> <string name="cannot_access_keychain">No se puede acceder a Keystore para guardar la base de datos de contraseñas</string> - <string name="chat_archive_header">Archivo del chat</string> - <string name="chat_archive_section">ARCHIVOS DE CHAT</string> <string name="cancel_verb">Cancelar</string> <string name="icon_descr_cancel_live_message">Cancelar mensaje en directo</string> <string name="confirm_verb">Confirmar</string> <string name="clear_chat_menu_action">Vaciar</string> <string name="app_version_code">Build de la aplicación: %s</string> <string name="call_already_ended">¡La llamada ha terminado!</string> - <string name="rcv_conn_event_switch_queue_phase_completed">el servidor de envío ha cambiado para tí</string> + <string name="rcv_conn_event_switch_queue_phase_completed">ha cambiado tu servidor de envío</string> <string name="icon_descr_cancel_link_preview">cancelar vista previa del enlace</string> <string name="call_on_lock_screen">Llamadas en la ventana de bloqueo</string> - <string name="alert_title_cant_invite_contacts">¡No se puede invitar a los contactos!</string> + <string name="alert_title_cant_invite_contacts">¡No se pueden invitar contactos!</string> <string name="chat_console">Consola de Chat</string> - <string name="chat_database_section">BASE DE DATOS DE CHAT</string> + <string name="chat_database_section">BASE DE DATOS DE SIMPLEX</string> <string name="chat_database_deleted">Base de datos eliminada</string> <string name="chat_database_imported">Base de datos importada</string> <string name="smp_servers_check_address">Comprueba la dirección del servidor e inténtalo de nuevo.</string> <string name="confirm_new_passphrase">Confirma la contraseña nueva…</string> <string name="network_disable_socks_info">Si confirmas los servidores de mensajería podrán ver tu IP, y tu proveedor de acceso a internet a qué servidores te estás conectando.</string> <string name="image_saved">Imagen guardada en la Galería</string> - <string name="file_will_be_received_when_contact_is_online">El archivo se recibirá cuando tu contacto esté en línea, por favor espera o compruébalo más tarde.</string> + <string name="file_will_be_received_when_contact_is_online">El archivo se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde.</string> <string name="add_contact">Enlace de invitación de un uso</string> - <string name="paste_the_link_you_received">Pegar enlace recibido</string> + <string name="paste_the_link_you_received">Pegar el enlace recibido</string> <string name="error_saving_group_profile">Error al guardar perfil de grupo</string> <string name="exit_without_saving">Salir sin guardar</string> <string name="file_saved">Archivo guardado</string> - <string name="icon_descr_flip_camera">Voltear la cámara</string> + <string name="icon_descr_flip_camera">Girar la cámara</string> <string name="group_invitation_expired">Invitación de grupo caducada</string> <string name="alert_message_group_invitation_expired">La invitación al grupo ya no es válida, ha sido eliminada por el remitente.</string> - <string name="delete_group_for_self_cannot_undo_warning">El grupo será eliminado para tí. ¡No podrá deshacerse!</string> + <string name="delete_group_for_self_cannot_undo_warning">El grupo será eliminado para tí. ¡No puede deshacerse!</string> <string name="how_to_use_markdown">Cómo usar la sintaxis markdown</string> <string name="description_via_one_time_link_incognito">en modo incógnito mediante enlace de un solo uso</string> <string name="simplex_link_contact">Dirección de contacto SimpleX</string> <string name="error_saving_smp_servers">Error al guardar servidores SMP</string> - <string name="simplex_link_mode_browser_warning">Abrir el enlace en el navegador puede reducir la privacidad y seguridad de la conexión. Los enlaces SimpleX que no son de confianza aparecerán en rojo.</string> + <string name="simplex_link_mode_browser_warning">Abrir el enlace en el navegador puede reducir la privacidad y seguridad de la conexión. Los enlaces de SimpleX que no son de confianza aparecerán en rojo.</string> <string name="error_setting_network_config">Error al actualizar la configuración de red</string> <string name="error_creating_address">Error al crear dirección</string> <string name="error_deleting_user">Error al eliminar perfil</string> <string name="auth_enable_simplex_lock">Activar Bloqueo SimpleX</string> - <string name="one_time_link">Enlace de invitación de un uso</string> + <string name="one_time_link">Enlace de invitación de un solo uso</string> <string name="smp_servers">Servidores SMP</string> <string name="settings_experimental_features">Características experimentales</string> <string name="error_importing_database">Error al importar base de datos</string> <string name="error_changing_message_deletion">Error al cambiar configuración</string> <string name="file_with_path">Archivo: %s</string> <string name="failed_to_active_user_title">¡Error al cambiar perfil!</string> - <string name="smp_servers_enter_manually">Introduce el servidor manualmente</string> + <string name="smp_servers_enter_manually">Añadir manualmente</string> <string name="how_to_use_your_servers">Cómo usar los servidores</string> - <string name="error_stopping_chat">Error al detener Chat</string> + <string name="error_stopping_chat">Error al parar SimpleX</string> <string name="enter_correct_passphrase">Introduce la contraseña correcta.</string> <string name="enter_passphrase">Introduce la contraseña…</string> <string name="icon_descr_group_inactive">Grupo inactivo</string> <string name="group_member_status_group_deleted">grupo eliminado</string> - <string name="group_members_can_send_disappearing">Los miembros del grupo pueden enviar mensajes temporales.</string> + <string name="group_members_can_send_disappearing">Los miembros pueden enviar mensajes temporales.</string> <string name="v4_2_group_links">Enlaces de grupo</string> <string name="invalid_connection_link">Enlace de conexión no válido</string> <string name="error_accepting_contact_request">Error al aceptar solicitud del contacto</string> <string name="error_changing_address">Error al cambiar dirección</string> <string name="error_saving_file">Error al guardar archivo</string> <string name="icon_descr_server_status_error">Error</string> - <string name="from_gallery_button">De la Galería</string> + <string name="from_gallery_button">De la galería</string> <string name="gallery_image_button">Imagen</string> <string name="gallery_video_button">Vídeo</string> <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Si has recibido un enlace de invitación a SimpleX Chat puedes abrirlo en tu navegador:</string> - <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Si eliges rechazar, el remitente NO será notificado.</string> + <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Si seleccionas rechazar, el remitente NO será notificado.</string> <string name="invalid_contact_link">¡Enlace no válido!</string> - <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Si no puedes reunirte en persona, puedes <b>escanear el código QR en la videollamada</b>, o tu contacto puede compartir un enlace de invitación.]]></string> - <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Si no puedes reunirte en persona, <b>muestra el código QR en la videollamada</b>, o comparte el enlace.]]></string> + <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Si no puedes reunirte en persona, puedes <b>escanear el código QR por videollamada</b>, o tu contacto puede compartir un enlace de invitación.]]></string> + <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Si no puedes reunirte en persona, <b>muestra el código QR por videollamada</b>, o comparte el enlace.]]></string> <string name="how_to">Cómo</string> <string name="ignore">Ignorar</string> <string name="error_deleting_database">Error al eliminar base de datos</string> <string name="encrypted_database">Base de datos cifrada</string> - <string name="error_removing_member">Error al eliminar miembro</string> - <string name="group_members_can_send_voice">Los miembros del grupo pueden enviar mensajes de voz.</string> + <string name="error_removing_member">Error al expulsar miembro</string> + <string name="group_members_can_send_voice">Los miembros pueden enviar mensajes de voz.</string> <string name="description_via_contact_address_link_incognito">en modo incógnito mediante enlace de dirección del contacto</string> <string name="failed_to_create_user_title">¡Error al crear perfil!</string> <string name="failed_to_parse_chat_title">No se pudo cargar el chat</string> - <string name="failed_to_parse_chats_title">No se pudieron cargar los chats</string> + <string name="failed_to_parse_chats_title">Fallo en la carga de chats</string> <string name="simplex_link_mode_full">Enlace completo</string> <string name="error_deleting_contact">Error al eliminar contacto</string> - <string name="error_joining_group">Error al unirse al grupo</string> + <string name="error_joining_group">Error al unirte al grupo</string> <string name="error_receiving_file">Error al recibir archivo</string> <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Comprueba que has usado el enlace correcto o pide a tu contacto que te envíe otro.</string> <string name="error_deleting_contact_request">Error al eliminar solicitud del contacto</string> @@ -385,12 +375,12 @@ <string name="for_everybody">Para todos</string> <string name="icon_descr_file">Archivo</string> <string name="icon_descr_image_snd_complete">Imagen enviada</string> - <string name="image_will_be_received_when_contact_is_online">La imagen se recibirá cuando tu contacto esté en línea, por favor espera o compruébalo más tarde.</string> + <string name="image_will_be_received_when_contact_is_online">La imagen se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde.</string> <string name="image_descr_link_preview">vista previa del enlace</string> <string name="error_saving_ICE_servers">Error al guardar servidores ICE</string> <string name="enter_one_ICE_server_per_line">Servidores ICE (uno por línea)</string> <string name="full_name__field">Nombre completo:</string> - <string name="people_can_connect_only_via_links_you_share">Las personas pueden conectarse contigo sólo mediante los enlaces que compartes.</string> + <string name="people_can_connect_only_via_links_you_share">Tu decides quién se conecta.</string> <string name="how_simplex_works">Cómo funciona SimpleX</string> <string name="icon_descr_hang_up">Colgar</string> <string name="files_and_media_section">Archivos y multimedia</string> @@ -413,14 +403,14 @@ <string name="section_title_for_console">PARA CONSOLA</string> <string name="error_changing_role">Error al cambiar rol</string> <string name="conn_stats_section_title_servers">SERVIDORES</string> - <string name="group_display_name_field">Introduce un nombre para el grupo:</string> - <string name="group_preferences">Preferencias de grupo</string> - <string name="group_members_can_send_dms">Los miembros del grupo pueden enviar mensajes directos.</string> - <string name="group_members_can_delete">Los miembros del grupo pueden eliminar mensajes de forma irreversible.</string> + <string name="group_display_name_field">Nombre del grupo:</string> + <string name="group_preferences">Preferencias del grupo</string> + <string name="group_members_can_send_dms">Los miembros pueden enviar mensajes directos.</string> + <string name="group_members_can_delete">Los miembros pueden eliminar mensajes enviados de forma irreversible. (24 horas)</string> <string name="v4_3_improved_privacy_and_security_desc">Ocultar pantalla de aplicaciones en aplicaciones recientes.</string> <string name="encrypt_database">Cifrar</string> <string name="icon_descr_expand_role">Ampliar la selección de roles</string> - <string name="delete_group_for_all_members_cannot_undo_warning">El grupo será eliminado para todos los miembros. ¡No podrá deshacerse!</string> + <string name="delete_group_for_all_members_cannot_undo_warning">El grupo será eliminado para todos los miembros. ¡No puede deshacerse!</string> <string name="network_option_enable_tcp_keep_alive">Activar TCP keep-alive</string> <string name="feature_enabled_for_you">activado para tí</string> <string name="server_error">error</string> @@ -437,39 +427,37 @@ <string name="icon_descr_help">ayuda</string> <string name="share_link">Compartir enlace</string> <string name="how_it_works">Cómo funciona</string> - <string name="delete_message_cannot_be_undone_warning">El mensaje será eliminado. ¡No podrá deshacerse!</string> + <string name="delete_message_cannot_be_undone_warning">El mensaje será eliminado. ¡No puede deshacerse!</string> <string name="incognito_info_protects">El modo incógnito protege tu privacidad creando un perfil aleatorio por cada contacto.</string> - <string name="turn_off_battery_optimization"><![CDATA[Para usar SimpleX, por favor <b>permite que SimpleX se ejecute en segundo plano</b> en el siguiente cuadro de diálogo. De lo contrario las notificaciones se desactivarán.]]></string> - <string name="install_simplex_chat_for_terminal">Instalar terminal para SimpleX Chat</string> + <string name="turn_off_battery_optimization"><![CDATA[<b>Da permiso</b> en el siguiente diálogo para recibir notificaciones instantáneas.]]></string> + <string name="install_simplex_chat_for_terminal">Instalar terminal de SimpleX Chat</string> <string name="group_invitation_item_description">invitación al grupo %1$s</string> <string name="rcv_group_event_member_added">ha invitado a %1$s</string> <string name="incognito_info_allows">Permite tener varias conexiones anónimas sin datos compartidos entre estas dentro del mismo perfil.</string> <string name="invite_to_group_button">Invitar al grupo</string> - <string name="to_verify_compare">Para comprobar el cifrado de extremo a extremo con tu contacto compara (o escanea) el código en tus dispositivos.</string> + <string name="to_verify_compare">Para verificar el cifrado de extremo a extremo con tu contacto, compara (o escanea) el código en ambos dispositivos.</string> <string name="database_is_not_encrypted">La base de datos no está cifrada. Escribe una contraseña para protegerla.</string> <string name="ensure_smp_server_address_are_correct_format_and_unique">Asegúrate de que las direcciones del servidor SMP tienen el formato correcto, están separadas por líneas y no están duplicadas.</string> <string name="icon_descr_instant_notifications">Notificación instantánea</string> - <string name="network_settings_title">Configuración de red</string> - <string name="network_use_onion_hosts_no_desc_in_alert">No se usarán hosts .onion</string> - <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes enviados con <b>cifrado de extremo a extremo de 2 capas</b> .]]></string> - <string name="onboarding_notifications_mode_subtitle">Puede cambiarse más tarde en Configuración.</string> + <string name="network_settings_title">Configuración avanzada</string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes.</string> + <string name="onboarding_notifications_mode_subtitle">Cómo afecta a la batería</string> <string name="onboarding_notifications_mode_service">Instantánea</string> - <string name="join_group_button">Únete</string> - <string name="join_group_incognito_button">Únete en modo incógnito</string> + <string name="join_group_button">Unirte</string> + <string name="join_group_incognito_button">Unirte en modo incógnito</string> <string name="conn_level_desc_indirect">indirecta (%1$s)</string> <string name="theme_light">Claro</string> <string name="chat_preferences_on">Activado</string> <string name="message_deletion_prohibited">La eliminación irreversible de mensajes no está permitida en este chat.</string> - <string name="message_deletion_prohibited_in_chat">La eliminación irreversible de mensajes no está permitida en este grupo.</string> + <string name="message_deletion_prohibited_in_chat">Eliminación irreversible no permitida.</string> <string name="v4_3_improved_server_configuration">Configuración del servidor mejorada</string> - <string name="alert_text_skipped_messages_it_can_happen_when">Esto puede suceder cuando: -\n1. Los mensajes caducan en el cliente saliente tras 2 días o en el servidor tras 30 días. + <string name="alert_text_skipped_messages_it_can_happen_when">Esto puede ocurrir cuando: +\n1. Los mensajes hayan caducado en el cliente saliente tras 2 días o en el servidor tras 30 días. \n2. El descifrado ha fallado porque tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos. \n3. La conexión ha sido comprometida.</string> <string name="notification_preview_mode_message">Contacto y texto</string> <string name="member_info_section_title_member">MIEMBRO</string> <string name="chat_item_ttl_none">nunca</string> - <string name="network_use_onion_hosts_required_desc_in_alert">Se requieren hosts .onion para la conexión</string> <string name="network_use_onion_hosts_no_desc">No se usarán hosts .onion</string> <string name="settings_notification_preview_title">Vista previa de notificaciones</string> <string name="alert_title_group_invitation_expired">¡Invitación caducada!</string> @@ -479,7 +467,7 @@ <string name="settings_notifications_mode_title">Servicio</string> <string name="delete_message_mark_deleted_warning">El mensaje se marcará para eliminar. El destinatario o destinatarios podrán revelar este mensaje.</string> <string name="live_message">¡Mensaje en vivo!</string> - <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 móvil: pulse <b>Abrir en aplicación móvil</b>, después pulse <b>Conectar</b> en la aplicación.]]></string> + <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 móvil: pulsa <b>Abrir en aplicación móvil</b>, después pulsa <b>Conectar</b> en la aplicación.]]></string> <string name="mark_read">Marcar como leído</string> <string name="mark_unread">Marcar como no leído</string> <string name="invalid_QR_code">Código QR no válido</string> @@ -499,27 +487,25 @@ <string name="invalid_message_format">formato de mensaje no válido</string> <string name="live">EN VIVO</string> <string name="moderated_description">eliminado por el moderador</string> - <string name="display_name_invited_to_connect">invitado a conectarse</string> + <string name="display_name_invited_to_connect">invitación a conectarse</string> <string name="service_notifications_disabled">¡Las notificaciones instantáneas están desactivadas!</string> - <string name="notification_preview_new_message">mensaje nuevo</string> + <string name="notification_preview_new_message">nuevo mensaje</string> <string name="notification_new_contact_request">Nueva solicitud de contacto</string> <string name="auth_log_in_using_credential">Inicie sesión con sus credenciales</string> <string name="message_delivery_error_title">Error en la entrega del mensaje</string> <string name="message_delivery_error_desc">Lo más probable es que este contacto haya eliminado la conexión contigo.</string> <string name="moderate_verb">Moderar</string> - <string name="group_preview_join_as">unirse como %s</string> + <string name="group_preview_join_as">unirte como %s</string> <string name="images_limit_desc">Sólo se pueden enviar 10 imágenes al mismo tiempo</string> <string name="large_file">¡Archivo grande!</string> <string name="mute_chat">Silenciar</string> <string name="ensure_ICE_server_address_are_correct_format_and_unique">Asegúrate de que las direcciones del servidor WebRTC ICE tienen el formato correcto, están separadas por líneas y no duplicadas.</string> <string name="network_use_onion_hosts_required_desc">Se requieren hosts .onion para la conexión -\nAtención: no podrás conectarte a servidores que no tengan dirección .onion.</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">Se usarán hosts .onion si están disponibles.</string> - <string name="immune_to_spam_and_abuse">Inmune a spam y abuso</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[Muchos se preguntarán: <i>si SimpleX no tiene identificadores de usuario, ¿cómo puede entregar los mensajes\?</i>]]></string> +\nRecuerda: no podrás conectarte a servidores que no tengan dirección .onion.</string> + <string name="immune_to_spam_and_abuse">Inmune al spam</string> <string name="incoming_video_call">Videollamada entrante</string> <string name="snd_group_event_user_left">has salido</string> - <string name="snd_conn_event_switch_queue_phase_completed">has cambiado la dirección</string> + <string name="snd_conn_event_switch_queue_phase_completed">has cambiado de servidor</string> <string name="feature_off">desactivado</string> <string name="v4_3_irreversible_message_deletion">Eliminación irreversible del mensaje</string> <string name="v4_3_voice_messages_desc">Máximo 40 segundos, recibido al instante.</string> @@ -532,7 +518,7 @@ <string name="ok">OK</string> <string name="only_stored_on_members_devices">(sólo almacenado por miembros del grupo)</string> <string name="markdown_help">Ayuda sintaxis markdown</string> - <string name="network_and_servers">Servidores y Redes</string> + <string name="network_and_servers">Servidores y Red</string> <string name="network_use_onion_hosts_prefer_desc">Se usarán hosts .onion si están disponibles.</string> <string name="italic_text">cursiva</string> <string name="incoming_audio_call">Llamada audio entrante</string> @@ -544,16 +530,16 @@ <string name="no_received_app_files">Sin archivos recibidos o enviados</string> <string name="messages_section_title">Mensajes</string> <string name="new_passphrase">Contraseña nueva…</string> - <string name="join_group_question">¿Unirse al grupo\?</string> + <string name="join_group_question">¿Unirte al grupo?</string> <string name="joining_group">Entrando al grupo</string> - <string name="keychain_error">Error Keystore</string> + <string name="keychain_error">Error en Keystore</string> <string name="icon_descr_add_members">Invitar miembros</string> <string name="group_member_role_observer">observador</string> <string name="group_member_role_member">miembro</string> <string name="group_member_status_removed">expulsado</string> <string name="group_member_status_invited">ha sido invitado</string> <string name="no_contacts_to_add">Sin contactos que añadir</string> - <string name="new_member_role">Nuevo rol de miembro</string> + <string name="new_member_role">Nuevo rol del miembro</string> <string name="initial_member_role">Rol inicial</string> <string name="info_row_local_name">Nombre local</string> <string name="network_status">Estado de la red</string> @@ -564,20 +550,20 @@ <string name="settings_section_title_incognito">Modo incógnito</string> <string name="new_database_archive">Nuevo archivo de base de datos</string> <string name="old_database_archive">Archivo de bases de datos antiguas</string> - <string name="snd_conn_event_switch_queue_phase_completed_for_member">has cambiado la dirección por %s</string> + <string name="snd_conn_event_switch_queue_phase_completed_for_member">has cambiado el servidor para %s</string> <string name="rcv_group_event_member_left">ha salido</string> <string name="button_leave_group">Salir del grupo</string> - <string name="only_group_owners_can_change_prefs">Sólo los propietarios pueden modificar las preferencias de grupo.</string> - <string name="users_delete_data_only">Sólo datos del perfil local</string> + <string name="only_group_owners_can_change_prefs">Sólo los propietarios del grupo pueden cambiar las preferencias.</string> + <string name="users_delete_data_only">Eliminar sólo el perfil</string> <string name="chat_preferences_no">no</string> <string name="thousand_abbreviation">k</string> <string name="marked_deleted_description">marcado eliminado</string> <string name="icon_descr_call_missed">Llamada perdida</string> - <string name="notifications_will_be_hidden">¡Las notificaciones seguirán enviándose hasta que la aplicación se detenga!</string> + <string name="notifications_will_be_hidden">¡Las notificaciones seguirán enviándose hasta que la aplicación se pare!</string> <string name="leave_group_button">Salir</string> <string name="leave_group_question">¿Salir del grupo\?</string> <string name="group_member_role_owner">propietario</string> - <string name="member_will_be_removed_from_group_cannot_be_undone">El miembro será expulsado del grupo. ¡No podrá deshacerse!</string> + <string name="member_will_be_removed_from_group_cannot_be_undone">El miembro será expulsado del grupo. ¡No puede deshacerse!</string> <string name="only_group_owners_can_enable_voice">Sólo los propietarios del grupo pueden activar los mensajes de voz.</string> <string name="icon_descr_more_button">Más</string> <string name="mark_code_verified">Marcar como verificado</string> @@ -585,13 +571,13 @@ <string name="make_private_connection">Establecer una conexión privada</string> <string name="network_error_desc">Comprueba tu conexión de red con %1$s e inténtalo de nuevo.</string> <string name="sender_may_have_deleted_the_connection_request">El remitente puede haber eliminado la solicitud de conexión.</string> - <string name="error_smp_test_certificate">Posiblemente la huella digital del certificado en la dirección del servidor es incorrecta</string> + <string name="error_smp_test_certificate">Posiblemente la huella del certificado en la dirección del servidor es incorrecta</string> <string name="reply_verb">Responder</string> <string name="save_passphrase_in_keychain">Guardar contraseña en Keystore</string> <string name="database_restore_error">Error al restaurar base de datos</string> <string name="select_contacts">Seleccionar contactos</string> <string name="save_group_profile">Guardar perfil de grupo</string> - <string name="reset_color">Restablecer colores</string> + <string name="reset_color">Reiniciar colores</string> <string name="only_you_can_send_disappearing">Sólo tú puedes enviar mensajes temporales.</string> <string name="only_your_contact_can_send_disappearing">Sólo tu contacto puede enviar mensajes temporales.</string> <string name="prohibit_sending_voice">No se permiten mensajes de voz.</string> @@ -609,8 +595,7 @@ <string name="callstatus_rejected">llamada rechazada</string> <string name="secret_text">secreto</string> <string name="open_simplex_chat_to_accept_call">Abrir SimpleX Chat para aceptar llamada</string> - <string name="network_options_reset_to_defaults">Restablecer valores por defecto</string> - <string name="save_color">Guardar color</string> + <string name="network_options_reset_to_defaults">Reiniciar a valores predetarminados</string> <string name="icon_descr_server_status_pending">Pendiente</string> <string name="periodic_notifications">Notificaciones periódicas</string> <string name="store_passphrase_securely">Guarda la contraseña de forma segura, NO podrás cambiarla si la pierdes.</string> @@ -619,12 +604,12 @@ <string name="onboarding_notifications_mode_title">Notificaciones privadas</string> <string name="image_descr_profile_image">imagen del perfil</string> <string name="prohibit_sending_voice_messages">No se permiten mensajes de voz.</string> - <string name="protect_app_screen">Proteger la pantalla de la aplicación</string> - <string name="read_more_in_github_with_link"><![CDATA[Más información en nuestro <font color="#0088ff">repositorio GitHub</font> .]]></string> + <string name="protect_app_screen">Proteger pantalla de la aplicación</string> + <string name="read_more_in_github_with_link"><![CDATA[Conoce más en nuestro <font color="#0088ff">repositorio GitHub</font> .]]></string> <string name="icon_descr_record_voice_message">Grabar mensaje de voz</string> <string name="rcv_group_event_member_deleted">ha expulsado a %1$s</string> <string name="send_link_previews">Enviar previsualizacion de enlaces</string> - <string name="send_live_message_desc">Envía un mensaje en vivo: se actualizará para el(los) destinatario(s) a medida que se escribe</string> + <string name="send_live_message_desc">Envía un mensaje en vivo: se actualizará para el (los) destinatario(s) a medida que se escribe</string> <string name="icon_descr_sent_msg_status_send_failed">error de envío</string> <string name="sending_via">Enviando vía</string> <string name="contact_developers">Por favor, actualiza la aplicación y ponte en contacto con los desarrolladores.</string> @@ -638,16 +623,15 @@ <string name="reject_contact_button">Rechazar</string> <string name="network_use_onion_hosts_required">Obligatorio</string> <string name="save_and_notify_contacts">Guardar y notificar contactos</string> - <string name="opensource_protocol_and_code_anybody_can_run_servers">Protocolo y código abiertos: cualquiera puede usar los servidores.</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Cualquiera puede alojar servidores.</string> <string name="role_in_group">Rol</string> - <string name="network_options_revert">Revertir</string> <string name="network_option_ping_interval">Intervalo PING</string> <string name="network_option_ping_count">Contador PING</string> - <string name="only_your_contact_can_delete">Sólo tu contacto puede eliminar mensajes de forma irreversible (tu puedes marcarlos para eliminar).</string> + <string name="only_your_contact_can_delete">Sólo tu contacto puede eliminar mensajes de forma irreversible (tu puedes marcarlos para eliminar). (24 horas)</string> <string name="v4_5_message_draft_descr">Conserva el último borrador del mensaje con los datos adjuntos.</string> <string name="v4_5_private_filenames">Nombres de archivos privados</string> <string name="v4_5_reduced_battery_usage">Reducción del uso de la batería</string> - <string name="observer_cant_send_message_desc">Póngase en contacto con el administrador del grupo.</string> + <string name="observer_cant_send_message_desc">Ponte en contacto con el administrador del grupo.</string> <string name="ask_your_contact_to_enable_voice">Solicita que tu contacto habilite el envío de mensajes de voz.</string> <string name="send_live_message">Mensaje en vivo</string> <string name="scan_QR_code">Escanear código QR</string> @@ -656,7 +640,7 @@ <string name="icon_descr_profile_image_placeholder">Espacio reservado para la imagen del perfil</string> <string name="image_descr_qr_code">Código QR</string> <string name="chat_with_the_founder">Consultas y sugerencias</string> - <string name="smp_servers_preset_address">Dirección del servidor predefinida</string> + <string name="smp_servers_preset_address">Dirección predefinida del servidor</string> <string name="send_us_an_email">Contacta vía email</string> <string name="rate_the_app">Valora la aplicación</string> <string name="save_servers_button">Guardar</string> @@ -664,27 +648,25 @@ <string name="callstate_received_confirmation">confirmación recibida…</string> <string name="onboarding_notifications_mode_periodic">Periódico</string> <string name="privacy_redefined">Privacidad redefinida</string> - <string name="read_more_in_github">Más información en nuestro repositorio GitHub.</string> <string name="reject">Rechazar</string> <string name="open_verb">Abrir</string> <string name="icon_descr_call_pending_sent">Llamada pendiente</string> <string name="privacy_and_security">Privacidad y Seguridad</string> <string name="store_passphrase_securely_without_recover">Guarda la contraseña de forma segura, NO podrás acceder al chat si la pierdes.</string> - <string name="save_archive">Guardar archivo</string> <string name="restore_database_alert_desc">Introduce la contraseña anterior después de restaurar la copia de seguridad de la base de datos. Esta acción no se puede deshacer.</string> <string name="rcv_group_event_user_deleted">te ha expulsado</string> <string name="receiving_via">Recibiendo vía</string> - <string name="network_option_protocol_timeout">Tiempo de espera del protocolo</string> + <string name="network_option_protocol_timeout">Timeout protocolo</string> <string name="network_option_seconds_label">seg</string> - <string name="users_delete_with_connections">Perfil y conexiones de servidor</string> + <string name="users_delete_with_connections">Eliminar perfil y conexiones</string> <string name="prohibit_sending_disappearing_messages">No se permiten mensajes temporales.</string> <string name="only_you_can_send_voice">Sólo tú puedes enviar mensajes de voz.</string> <string name="only_your_contact_can_send_voice">Sólo tu contacto puede enviar mensajes de voz.</string> - <string name="run_chat_section">EJECUTAR CHAT</string> + <string name="run_chat_section">EJECUTAR SIMPLEX</string> <string name="restart_the_app_to_use_imported_chat_database">Reinicia la aplicación para poder usar la base de datos importada.</string> <string name="enter_correct_current_passphrase">Introduce la contraseña actual correcta.</string> - <string name="feature_received_prohibited">recepción prohibida</string> - <string name="only_you_can_delete_messages">Sólo tú puedes eliminar mensajes de forma irreversible (tu contacto puede marcarlos para eliminar).</string> + <string name="feature_received_prohibited">recepción no permitida</string> + <string name="only_you_can_delete_messages">Sólo tú puedes eliminar mensajes de forma irreversible (tu contacto puede marcarlos para eliminar). (24 horas)</string> <string name="prohibit_direct_messages">No se permiten mensajes directos entre miembros.</string> <string name="prohibit_sending_disappearing">No se permiten mensajes temporales.</string> <string name="v4_2_security_assessment">Evaluación de la seguridad</string> @@ -697,23 +679,22 @@ <string name="open_chat">Abrir chat</string> <string name="restore_database">Restaurar copia de seguridad de la base de datos</string> <string name="save_passphrase_and_open_chat">Guardar contraseña y abrir el chat</string> - <string name="restore_passphrase_not_found_desc">La contraseña no se ha encontrado en Keystore, introdúzcala manualmente. Esto puede haber ocurrido si has restaurado los datos de la aplicación con una herramienta de copia de seguridad. Si no es así, por favor ponte en contacto con los desarrolladores.</string> + <string name="restore_passphrase_not_found_desc">La frase de contraseña no se ha encontrado en Keystore. Por favor, introdúcela manualmente. Puede deberse a que hayas restaurado los datos de la aplicación mediante alguna herramienta para copias de seguridad. Si no es así, por favor, ponte en contacto con los desarrolladores.</string> <string name="remove_member_confirmation">Expulsar</string> <string name="button_remove_member">Expulsar miembro</string> <string name="button_send_direct_message">Enviar mensaje directo</string> - <string name="reset_verb">Restablecer</string> + <string name="reset_verb">Reiniciar</string> <string name="paste_button">Pegar</string> <string name="security_code">Código de seguridad</string> <string name="scan_code_from_contacts_app">Escanea el código de seguridad desde la aplicación de tu contacto.</string> <string name="smp_servers_save">Guardar servidores</string> - <string name="smp_servers_scan_qr">Escanear código QR del servidor</string> + <string name="smp_servers_scan_qr">Escanear código QR</string> <string name="smp_servers_preset_server">Servidor predefinido</string> <string name="save_and_notify_contact">Guardar y notificar contacto</string> <string name="save_preferences_question">¿Guardar preferencias\?</string> - <string name="save_and_notify_group_members">Guardar y notificar a los miembros del grupo</string> - <string name="connection_error_auth_desc">A menos que tu contacto haya eliminado la conexión o que este enlace ya se haya usado, podría ser un error. Por favor, notifícalo. -\nPara conectarte, pide a tu contacto que cree otro enlace de conexión y comprueba que tienes buena conexión de red.</string> - <string name="periodic_notifications_desc">La aplicación recoge nuevos mensajes periódicamente por lo que usa un pequeño porcentaje de la batería al día. La aplicación no hace uso de notificaciones automáticas: los datos de tu dispositivo no se envían a los servidores.</string> + <string name="save_and_notify_group_members">Guardar y notificar grupo</string> + <string name="connection_error_auth_desc">A menos que tu contacto haya eliminado la conexión o el enlace se haya usado, podría ser un error. Por favor, notifícalo. \nPara conectarte pide a tu contacto que cree otro enlace y comprueba la conexión de red.</string> + <string name="periodic_notifications_desc">La aplicación recoge nuevos mensajes periódicamente lo que consume un pequeño porcentaje de batería al día. La aplicación no usa notificaciones push por tanto los datos de tu dispositivo no se envían a los servidores push.</string> <string name="la_notice_title_simplex_lock">Bloqueo SimpleX</string> <string name="auth_unlock">Desbloquear</string> <string name="this_text_is_available_in_settings">Este texto está disponible en Configuración</string> @@ -722,11 +703,11 @@ <string name="using_simplex_chat_servers">Usando servidores SimpleX Chat.</string> <string name="network_session_mode_transport_isolation">Aislamiento de transporte</string> <string name="strikethrough_text">tachado</string> - <string name="use_chat">Usar Chat</string> + <string name="use_chat">Abrir SimpleX</string> <string name="settings_section_title_socks">PROXY SOCKS</string> <string name="settings_section_title_themes">TEMAS</string> - <string name="stop_chat_confirmation">Detener</string> - <string name="delete_chat_profile_action_cannot_be_undone_warning">Esta acción no se puede deshacer. Tu perfil, contactos, mensajes y archivos se perderán irreversiblemente.</string> + <string name="stop_chat_confirmation">Parar</string> + <string name="delete_chat_profile_action_cannot_be_undone_warning">Esta acción es irreversible. Tu perfil, contactos, mensajes y archivos se perderán.</string> <string name="skip_inviting_button">Omitir invitación a miembros</string> <string name="settings_notification_preview_mode_title">Vista previa</string> <string name="la_notice_turn_on">Activar</string> @@ -734,7 +715,7 @@ <string name="icon_descr_sent_msg_status_unauthorized_send">envío no autorizado</string> <string name="set_contact_name">Escribe un nombre para el contacto</string> <string name="unknown_error">Error desconocido</string> - <string name="member_role_will_be_changed_with_notification">El rol cambiará a \"%s\". Se notificará a todos los miembros del grupo.</string> + <string name="member_role_will_be_changed_with_notification">El rol cambiará a %s. Todos serán notificados.</string> <string name="v4_2_security_assessment_desc">La seguridad de SimpleX Chat ha sido auditada por Trail of Bits.</string> <string name="v4_4_disappearing_messages_desc">Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido.</string> <string name="ntf_channel_messages">Mensajes de chat SimpleX</string> @@ -749,28 +730,27 @@ <string name="share_invitation_link">Compartir enlace de un uso</string> <string name="update_network_session_mode_question">¿Actualizar el modo de aislamiento de transporte\?</string> <string name="icon_descr_speaker_on">Altavoz activado</string> - <string name="stop_chat_to_enable_database_actions">Detén SimpleX para habilitar las acciones sobre la base de datos.</string> <string name="connection_you_accepted_will_be_cancelled">¡La conexión que has aceptado se cancelará!</string> - <string name="database_initialization_error_desc">La base de datos no funciona correctamente. Pulsa para saber más</string> + <string name="database_initialization_error_desc">La base de datos no funciona correctamente. Pulsa para conocer más</string> <string name="moderate_message_will_be_marked_warning">El mensaje será marcado como moderado para todos los miembros.</string> - <string name="next_generation_of_private_messaging">La nueva generación de mensajería privada</string> - <string name="delete_files_and_media_desc">Esta acción no se puede deshacer. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán.</string> - <string name="enable_automatic_deletion_message">Esta acción no se puede deshacer. Se eliminarán los mensajes enviados y recibidos anteriores a la selección. Puede tardar varios minutos.</string> + <string name="next_generation_of_private_messaging">El futuro de la mensajería</string> + <string name="delete_files_and_media_desc">Esta acción es irreversible. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán.</string> + <string name="enable_automatic_deletion_message">Esta acción es irreversible. Los mensajes enviados y recibidos anteriores a la selección serán eliminados. Podría tardar varios minutos.</string> <string name="messages_section_description">Esta configuración se aplica a los mensajes del perfil actual</string> <string name="this_string_is_not_a_connection_link">¡Esta cadena no es un enlace de conexión!</string> - <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Para preservar tu privacidad, en lugar de notificaciones automáticas la aplicación cuenta con un <b>servicio en segundo planoSimpleX</b>, usa un pequeño porcentaje de la batería al día.]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Para mejorar la privacidad <b>SimpleX se ejecuta en segundo plano</b> en lugar de usar notificaciones push.]]></string> <string name="icon_descr_settings">Configuración</string> <string name="icon_descr_speaker_off">Altavoz desactivado</string> <string name="add_contact_or_create_group">Inciar chat nuevo</string> - <string name="stop_chat_to_export_import_or_delete_chat_database">Para exportar, importar o eliminar la base de datos debes detener Chat. Durante la parada no podrás recibir o enviar mensajes.</string> - <string name="thank_you_for_installing_simplex">Gracias por instalar SimpleX Chat!</string> - <string name="to_protect_privacy_simplex_has_ids_for_queues">Para proteger la privacidad, en lugar de los identificadores de usuario que usan el resto de plataformas, SimpleX dispone de identificadores para las colas de mensajes, independientes para cada uno de tus contactos.</string> - <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Para proteger tu información, activa Bloqueo SimpleX. + <string name="stop_chat_to_export_import_or_delete_chat_database">Para exportar, importar o eliminar la base de datos debes parar SimpleX. Mientra tanto no podrás enviar ni recibir mensajes.</string> + <string name="thank_you_for_installing_simplex">¡Gracias por instalar SimpleX Chat!</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">Para proteger tu privacidad, SimpleX usa identificadores distintos para cada uno de tus contactos.</string> + <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Para proteger tu información, activa el Bloqueo SimpleX. \nSe te pedirá que completes la autenticación antes de activar esta función.</string> - <string name="updating_settings_will_reconnect_client_to_all_servers">Al actualizar la configuración, el cliente se reconectará a todos los servidores.</string> + <string name="updating_settings_will_reconnect_client_to_all_servers">Para actualizar la configuración el cliente se reconectará a todos los servidores.</string> <string name="use_simplex_chat_servers__question">¿Usar servidores SimpleX Chat\?</string> <string name="simplex_link_group">Enlace de grupo SimpleX</string> - <string name="simplex_link_invitation">Invitación única SimpleX</string> + <string name="simplex_link_invitation">Invitación SimpleX de un uso</string> <string name="simplex_link_mode">Enlaces SimpleX</string> <string name="error_smp_test_server_auth">El servidor requiere autorización para crear colas, comprueba la contraseña</string> <string name="enter_passphrase_notification_desc">Para recibir notificaciones, introduce la contraseña de la base de datos</string> @@ -779,7 +759,7 @@ <string name="notification_preview_mode_message_desc">Se muestran el nombre del contacto y el mensaje</string> <string name="notification_preview_mode_contact_desc">Se muestra sólo el nombre del contacto</string> <string name="auth_simplex_lock_turned_on">Bloqueo SimpleX activado</string> - <string name="auth_stop_chat">Detener chat</string> + <string name="auth_stop_chat">Parar SimpleX</string> <string name="moderate_message_will_be_deleted_warning">El mensaje se eliminará para todos los miembros.</string> <string name="share_file">Compartir archivo…</string> <string name="images_limit_title">¡Demasiadas imágenes!</string> @@ -788,32 +768,31 @@ <string name="network_use_onion_hosts">Usar hosts .onion</string> <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">La plataforma de mensajería y aplicaciones que protege tu privacidad y seguridad.</string> - <string name="first_platform_without_user_ids">La primera plataforma sin identificadores de usuario: diseñada para la privacidad.</string> + <string name="first_platform_without_user_ids">Sin identificadores de usuario.</string> <string name="alert_message_no_group">Este grupo ya no existe.</string> <string name="accept_feature_set_1_day">Establecer 1 día</string> - <string name="v4_4_french_interface_descr">¡Gracias a los colaboradores! Contribuye a través de Weblate.</string> - <string name="v4_5_italian_interface_descr">¡Gracias a los colaboradores! Contribuye a través de Weblate.</string> + <string name="v4_4_french_interface_descr">¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate.</string> + <string name="v4_5_italian_interface_descr">¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate.</string> <string name="v4_5_private_filenames_descr">Para proteger la zona horaria, los archivos de imagen/voz usan la hora UTC.</string> <string name="v4_5_transport_isolation">Aislamiento de transporte</string> <string name="to_share_with_your_contact">(para compartir con tu contacto)</string> - <string name="unmute_chat">Activar audio</string> + <string name="unmute_chat">Activar sonido</string> <string name="is_verified">%s está verificado</string> <string name="is_not_verified">%s no está verificado</string> <string name="smp_servers_test_server">Probar servidor</string> <string name="smp_servers_test_servers">Probar servidores</string> - <string name="star_on_github">Estrella en GitHub</string> - <string name="smp_servers_per_user">Lista de servidores para las conexiones nuevas de tu perfil actual</string> + <string name="star_on_github">Califica en GitHub</string> + <string name="smp_servers_per_user">Servidores para conexiones nuevas en tu perfil</string> <string name="network_disable_socks">¿Usar conexión directa a Internet\?</string> - <string name="update_onion_hosts_settings_question">¿Actualizar la configuración de los hosts .onion\?</string> <string name="profile_is_only_shared_with_your_contacts">El perfil sólo se comparte con tus contactos.</string> <string name="callstate_starting">inicializando…</string> <string name="alert_title_skipped_messages">Mensajes omitidos</string> <string name="settings_section_title_settings">CONFIGURACIÓN</string> - <string name="stop_chat_question">¿Detener Chat\?</string> + <string name="stop_chat_question">¿Parar SimpleX?</string> <string name="chat_item_ttl_seconds">%s segundo(s)</string> - <string name="group_invitation_tap_to_join">Pulsa para unirse</string> + <string name="group_invitation_tap_to_join">Pulsa para unirte</string> <string name="rcv_group_event_updated_group_profile">ha actualizado el perfil del grupo</string> - <string name="network_option_tcp_connection_timeout">Tiempo de espera de la conexión TCP agotado</string> + <string name="network_option_tcp_connection_timeout">Timeout de la conexión TCP</string> <string name="theme">Tema</string> <string name="set_group_preferences">Establece preferencias de grupo</string> <string name="settings_section_title_support">SOPORTE SIMPLEX CHAT</string> @@ -822,46 +801,45 @@ <string name="update_database_passphrase">Actualizar contraseña base de datos</string> <string name="group_invitation_tap_to_join_incognito">Pulsa para unirte en modo incógnito</string> <string name="switch_verb">Cambiar</string> - <string name="member_role_will_be_changed_with_invitation">El rol cambiará a \"%s\". El miembro recibirá una nueva invitación.</string> + <string name="member_role_will_be_changed_with_invitation">El rol cambiará a %s y el miembro recibirá una invitación nueva.</string> <string name="update_network_settings_confirmation">Actualizar</string> <string name="update_network_settings_question">¿Actualizar la configuración de red\?</string> - <string name="trying_to_connect_to_server_to_receive_messages">Intentando conectar con el servidor usado para recibir mensajes de este contacto.</string> + <string name="trying_to_connect_to_server_to_receive_messages">Intentando conectar con el servidor para recibir mensajes de este contacto.</string> <string name="unknown_message_format">formato de mensaje desconocido</string> - <string name="trying_to_connect_to_server_to_receive_messages_with_error">Intentando conectar con el servidor usado para recibir mensajes de este contacto (error: %1$s ).</string> - <string name="error_smp_test_failed_at_step">Prueba fallida en el paso %s.</string> + <string name="trying_to_connect_to_server_to_receive_messages_with_error">Intentando conectar con el servidor para recibir mensajes de este contacto (error: %1$s).</string> + <string name="error_smp_test_failed_at_step">Prueba no superada en el paso %s.</string> <string name="tap_to_start_new_chat">Pulsa para iniciar chat nuevo</string> <string name="share_message">Compartir mensaje…</string> <string name="share_image">Compartir medios…</string> <string name="show_call_on_lock_screen">Mostrar</string> - <string name="unknown_database_error_with_info">Error desconocido base de datos: %s</string> + <string name="unknown_database_error_with_info">Error desconocido en la base de datos: %s</string> <string name="database_backup_can_be_restored">El intento de cambiar la contraseña de la base de datos no se ha completado.</string> <string name="chat_help_tap_button">Pulsa el botón</string> <string name="to_start_a_new_chat_help_header">Para iniciar un chat nuevo</string> <string name="switch_receiving_address">Cambiar servidor de recepción</string> - <string name="group_is_decentralized">Completamente descentralizado: sólo visible para los miembros.</string> + <string name="group_is_decentralized">Totalmente descentralizado. Visible sólo para los miembros.</string> <string name="to_connect_via_link_title">Para conectarte mediante enlace</string> - <string name="smp_servers_test_failed">¡Error en prueba del servidor!</string> - <string name="smp_servers_test_some_failed">Algunos servidores no superaron la prueba:</string> + <string name="smp_servers_test_failed">¡Prueba no superada!</string> + <string name="smp_servers_test_some_failed">Algunos servidores no han superado la prueba:</string> <string name="smp_servers_use_server">Usar servidor</string> - <string name="smp_servers_use_server_for_new_conn">Usar para conexiones nuevas</string> + <string name="smp_servers_use_server_for_new_conn">Para conexiones nuevas</string> <string name="theme_system">Sistema</string> <string name="description_via_one_time_link">mediante enlace de un solo uso</string> <string name="your_chats">Chats</string> <string name="voice_message_send_text">Mensaje de voz…</string> <string name="icon_descr_video_off">Desactivar vídeo</string> <string name="icon_descr_video_on">Activar vídeo</string> - <string name="wrong_passphrase">Contraseña de base de datos incorrecta</string> + <string name="wrong_passphrase">Contraseña incorrecta de la base de datos</string> <string name="wrong_passphrase_title">¡Contraseña incorrecta!</string> <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Te has unido a este grupo. Conectando con el emisor de la invitacíon.</string> - <string name="alert_title_cant_invite_contacts_descr">Estás usando un perfil incógnito para este grupo, por tanto para evitar compartir tu perfil principal no se permite invitar a contactos</string> + <string name="alert_title_cant_invite_contacts_descr">Estás usando un perfil incógnito en este grupo. Para evitar descubrir tu perfil principal no se permite invitar contactos</string> <string name="you_are_invited_to_group">Has sido invitado al grupo</string> <string name="v4_3_voice_messages">Mensajes de voz</string> <string name="v4_3_irreversible_message_deletion_desc">Tus contactos pueden permitir la eliminación completa de mensajes.</string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Tú controlas a través de qué servidor(es) <b>recibes</b> los mensajes. Tus contactos controlan a través de qué servidor(es) <b>envías</b> tus mensajes.]]></string> <string name="voice_messages">Mensajes de voz</string> - <string name="voice_messages_are_prohibited">Los mensajes de voz no están permitidos en este grupo.</string> + <string name="voice_messages_are_prohibited">Mensajes de voz no permitidos.</string> <string name="v4_4_verify_connection_security">Comprobar la seguridad de la conexión</string> - <string name="you_are_already_connected_to_vName_via_this_link">¡Ya estás conectado a %1$s.</string> + <string name="you_are_already_connected_to_vName_via_this_link">¡Ya estás conectado con %1$s.</string> <string name="welcome">¡Bienvenido!</string> <string name="your_chat_profile_will_be_sent_to_your_contact">Tu perfil será enviado \na tu contacto</string> @@ -874,35 +852,35 @@ <string name="waiting_for_image">Esperando imagen</string> <string name="webrtc_ice_servers">Servidores WebRTC ICE</string> <string name="contact_wants_to_connect_via_call">%1$s quiere conectarse contigo mediante</string> - <string name="failed_to_create_user_duplicate_desc">Ya tienes un perfil con este nombre mostrado. Por favor, elige otro nombre.</string> + <string name="failed_to_create_user_duplicate_desc">Ya tienes un perfil con este nombre mostrado. Por favor, selecciona otro nombre.</string> <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[También puedes conectarte pulsando el enlace. Si se abre en el navegador, pulsa en <b>Abrir en aplicación móvil</b>.]]></string> <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Puedes <font color="#0088ff">ponerte en contacto con los desarrolladores de SimpleX Chat para consultas y para recibir actualizaciones</font>.]]></string> <string name="observer_cant_send_message_title">¡No puedes enviar mensajes!</string> <string name="you_can_use_markdown_to_format_messages__prompt">Puedes usar la sintaxis markdown para dar formato a tus mensajes:</string> <string name="you_must_use_the_most_recent_version_of_database">Debes usar la versión más reciente de tu base de datos ÚNICAMENTE en un dispositivo, de lo contrario podrías dejar de recibir mensajes de algunos contactos.</string> - <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Tu contacto debe estar en línea para que se complete la conexión. -\nPuedes cancelar esta conexión y eliminar el contacto (e intentarlo más tarde con un enlace nuevo).</string> + <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">El contacto debe estar en línea para completar la conexión. +\nPuedes cancelarla y eliminar el contacto (e intentarlo más tarde con un enlace nuevo).</string> <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">La base de datos actual será ELIMINADA y SUSTITUIDA por la importada. -\nEsta acción no se puede deshacer. Tu perfil, contactos, mensajes y archivos se perderán irreversiblemente.</string> +\nEsta acción es irreversible. Tu perfil, contactos, mensajes y archivos actuales se perderán.</string> <string name="incognito_random_profile">Tu perfil aleatorio</string> - <string name="you_will_be_connected_when_your_connection_request_is_accepted">Te conectarás cuando tu solicitud se acepte, por favor espera o compruébalo más tarde.</string> - <string name="you_will_be_connected_when_your_contacts_device_is_online">Te conectarás cuando el dispositivo de tu contacto esté en línea, por favor espera o compruébalo más tarde.</string> - <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Se te pedirá identificarte cuándo inicies o continues usando la aplicación tras 30 segundos en segundo plano.</string> + <string name="you_will_be_connected_when_your_connection_request_is_accepted">Te conectarás cuando tu solicitud se acepte, por favor espera o revisa más tarde.</string> + <string name="you_will_be_connected_when_your_contacts_device_is_online">Te conectarás cuando el dispositivo de tu contacto esté en línea, por favor espera o revisa más tarde.</string> + <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Se te pedirá autenticarte cuando inicies la aplicación o sigas usándola tras 30 segundos en segundo plano.</string> <string name="invite_prohibited_description">Estás intentando invitar a un contacto con el que compartes un perfil incógnito a un grupo en el que usas tu perfil principal</string> <string name="simplex_link_mode_browser">Mediante navegador</string> <string name="simplex_link_connection">mediante %1$s</string> <string name="simplex_service_notification_title">Servicio SimpleX Chat</string> - <string name="personal_welcome">¡Bienvenido %1$s !</string> + <string name="personal_welcome">¡Bienvenido %1$s!</string> <string name="group_preview_you_are_invited">has sido invitado al grupo</string> <string name="waiting_for_file">Esperando archivo</string> <string name="icon_descr_waiting_for_image">Esperando imagen</string> <string name="voice_message_with_duration">Mensaje de voz (%1$s )</string> <string name="network_use_onion_hosts_prefer">Si disponibles</string> <string name="we_do_not_store_contacts_or_messages_on_servers">No almacenamos ninguno de tus contactos o mensajes (una vez entregados) en los servidores.</string> - <string name="you_are_invited_to_group_join_to_connect_with_group_members">Has sido invitado al grupo. Únete para conectarte con los miembros del grupo.</string> + <string name="you_are_invited_to_group_join_to_connect_with_group_members">Has sido invitado a un grupo. Únete para conectar con sus miembros.</string> <string name="snd_group_event_member_deleted">has expulsado a %1$s</string> <string name="group_info_member_you">Tú: %1$s</string> - <string name="you_can_share_group_link_anybody_will_be_able_to_connect">Puedes compartir un enlace o un código QR: cualquiera podrá unirse al grupo. Si lo eliminas más tarde los miembros del grupo no se perderán.</string> + <string name="you_can_share_group_link_anybody_will_be_able_to_connect">Puedes compartir un enlace o código QR para que cualquiera pueda unirse al grupo. Si decides eliminarlo más tarde, los miembros del grupo se mantendrán.</string> <string name="incognito_info_share">Cuando compartes un perfil incógnito con alguien, este perfil también se usará para los grupos a los que te inviten.</string> <string name="your_preferences">Mis preferencias</string> <string name="v4_2_auto_accept_contact_requests_desc">Con mensaje de bienvenida opcional.</string> @@ -910,7 +888,7 @@ <string name="verify_security_code">Comprobar código de seguridad</string> <string name="you_accepted_connection">Has aceptado la conexión</string> <string name="you_invited_a_contact">Has invitado a tu contacto</string> - <string name="you_will_be_connected_when_group_host_device_is_online">Te conectarás al grupo cuando el dispositivo anfitrión esté en línea, por favor espera o compruébalo más tarde.</string> + <string name="you_will_be_connected_when_group_host_device_is_online">Te conectarás al grupo cuando el dispositivo anfitrión esté en línea, por favor espera o revisa más tarde.</string> <string name="your_settings">Configuración</string> <string name="your_SMP_servers">Servidores SMP</string> <string name="you_control_your_chat">¡Tú controlas tu chat!</string> @@ -936,12 +914,12 @@ <string name="chat_preferences_you_allow">Permites</string> <string name="app_name">SimpleX</string> <string name="profile_will_be_sent_to_contact_sending_link">Tu perfil se enviará al contacto del que has recibido este enlace.</string> - <string name="you_will_join_group">Te unirás al grupo al que hace referencia este enlace y te conectarás con sus miembros.</string> - <string name="sender_you_pronoun">tú</string> + <string name="you_will_join_group">Te conectarás con todos los miembros del grupo.</string> + <string name="sender_you_pronoun">tu</string> <string name="connected_to_server_to_receive_messages_from_contact">Estás conectado al servidor usado para recibir mensajes de este contacto.</string> <string name="description_via_contact_address_link">mediante enlace de dirección de contacto</string> <string name="description_via_group_link">mediante enlace de grupo</string> - <string name="description_you_shared_one_time_link">has compartido enlace de un solo uso</string> + <string name="description_you_shared_one_time_link">enlace de un solo uso</string> <string name="description_you_shared_one_time_link_incognito">has compartido enlace de un solo uso en módo incógnito</string> <string name="you_have_no_chats">No tienes chats</string> <string name="contact_sent_large_file">El contacto ha enviado un archivo mayor al máximo admitido (%1$s ).</string> @@ -959,7 +937,7 @@ <string name="smp_servers_your_server">Tu servidor</string> <string name="smp_servers_your_server_address">Dirección de tu servidor</string> <string name="your_current_profile">Tu perfil actual</string> - <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Tu perfil se almacena en tu dispositivo y sólo se comparte con tus contactos. Los servidores SimpleX no pueden ver tu perfil.</string> + <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos. Los servidores SimpleX no pueden ver tu perfil.</string> <string name="language_system">Sistema</string> <string name="button_add_welcome_message">Añadir mensaje de bienvenida</string> <string name="v4_6_audio_video_calls">Llamadas y videollamadas</string> @@ -968,21 +946,18 @@ <string name="save_profile_password">Guardar contraseña de perfil</string> <string name="password_to_show">Contraseña para hacerlo visible</string> <string name="error_saving_user_password">Error al guardar contraseña de usuario</string> - <string name="relay_server_if_necessary">El retransmisor sólo se usa en caso de necesidad. Un tercero podría ver tu IP.</string> + <string name="relay_server_if_necessary">El servidor de retransmisión sólo se usa en caso de necesidad. Un tercero podría ver tu IP.</string> <string name="relay_server_protects_ip">El servidor de retransmisión protege tu IP pero puede ver la duración de la llamada.</string> - <string name="cant_delete_user_profile">¡No se puede eliminar el perfil!</string> <string name="enter_password_to_show">Introduce la contraseña</string> <string name="user_hide">Ocultar</string> <string name="user_mute">Silenciar</string> - <string name="save_and_update_group_profile">Guardar y actualizar perfil de grupo</string> + <string name="save_and_update_group_profile">Guardar y actualizar perfil del grupo</string> <string name="tap_to_activate_profile">Pulsa sobre un perfil para activarlo.</string> - <string name="should_be_at_least_one_visible_profile">Debe haber al menos un perfil de usuario visible.</string> <string name="user_unhide">Mostrar</string> <string name="button_welcome_message">Mensaje de bienvenida</string> <string name="group_welcome_title">Mensaje de bienvenida</string> - <string name="should_be_at_least_one_profile">Debe haber al menos un perfil de usuario.</string> <string name="make_profile_private">¡Hacer perfil privado!</string> - <string name="dont_show_again">No mostrar de nuevo</string> + <string name="dont_show_again">No volver a mostrar</string> <string name="muted_when_inactive">¡Silenciado cuando está inactivo!</string> <string name="v4_6_group_moderation">Moderación de grupos</string> <string name="v4_6_hidden_chat_profiles">Perfiles ocultos</string> @@ -990,7 +965,7 @@ <string name="v4_6_audio_video_calls_descr">Ahora con soporte bluetooth y otras mejoras.</string> <string name="v4_6_group_welcome_message_descr">¡Guarda un mensaje para ser mostrado a los miembros nuevos!</string> <string name="v4_6_chinese_spanish_interface">Interfaz en chino y español</string> - <string name="v4_6_chinese_spanish_interface_descr">¡Gracias a los colaboradores! Contribuye a través de Weblate.</string> + <string name="v4_6_chinese_spanish_interface_descr">¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate.</string> <string name="error_updating_user_privacy">Error al actualizar privacidad de usuario</string> <string name="confirm_password">Confirmar contraseña</string> <string name="v4_6_reduced_battery_usage">Reducción consumo de batería</string> @@ -998,12 +973,12 @@ <string name="v4_6_reduced_battery_usage_descr">¡Más mejoras en camino!</string> <string name="hidden_profile_password">Contraseña del perfil oculto</string> <string name="save_welcome_message_question">¿Guardar mensaje de bienvenida\?</string> - <string name="user_unmute">Activar audio</string> + <string name="user_unmute">Activar sonido</string> <string name="you_can_hide_or_mute_user_profile">Puedes ocultar o silenciar un perfil. Mantenlo pulsado para abrir el menú.</string> <string name="you_will_still_receive_calls_and_ntfs">Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos.</string> <string name="v4_6_group_moderation_descr">Ahora los administradores pueden: -\n- borrar mensajes de los miembros. -\n- desactivar el rol miembro (a rol \"observador\")</string> +\n- eliminar mensajes de los miembros. +\n- desactivar el rol miembro (a rol "observador")</string> <string name="to_reveal_profile_enter_password">Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda del menú Mis perfiles.</string> <string name="database_upgrade">Actualización de la base de datos</string> <string name="database_downgrade">Volviendo a versión anterior de la base de datos</string> @@ -1017,24 +992,24 @@ <string name="confirm_database_upgrades">Confirmar actualizaciones de la bases de datos</string> <string name="mtr_error_no_down_migration">la versión de la base de datos es más reciente que la aplicación, pero no hay migración hacia versión anterior para: %s</string> <string name="settings_section_title_experimenta">EXPERIMENTAL</string> - <string name="developer_options">IDs de la base de datos y opciónes de aislamiento de transporte.</string> - <string name="file_will_be_received_when_contact_completes_uploading">El archivo se recibirá cuando tu contacto termine de subirlo.</string> - <string name="image_will_be_received_when_contact_completes_uploading">La imagen se recibirá cuando tu contacto termine de subirla.</string> - <string name="show_developer_options">Mostrar opciones de desarrollador</string> + <string name="developer_options">IDs de la base de datos y opciones de aislamiento de transporte.</string> + <string name="file_will_be_received_when_contact_completes_uploading">El archivo se recibirá cuando el contacto termine de subirlo.</string> + <string name="image_will_be_received_when_contact_completes_uploading">La imagen se recibirá cuando el contacto termine de subirla.</string> + <string name="show_developer_options">Mostrar opciones para desarrolladores</string> <string name="hide_dev_options">Ocultar:</string> <string name="show_dev_options">Mostrar:</string> - <string name="delete_chat_profile">Eliminar perfil de chat</string> + <string name="delete_chat_profile">Eliminar perfil</string> <string name="profile_password">Contraseña del perfil</string> <string name="unhide_chat_profile">Mostrar perfil oculto</string> <string name="unhide_profile">Mostrar perfil</string> <string name="delete_profile">Eliminar perfil</string> <string name="video_descr">Vídeo</string> - <string name="video_will_be_received_when_contact_is_online">El vídeo se recibirá cuando tu contacto esté en línea, por favor espera o compruébalo más tarde.</string> + <string name="video_will_be_received_when_contact_is_online">El vídeo se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde.</string> <string name="waiting_for_video">Esperando el vídeo</string> <string name="icon_descr_video_asked_to_receive">Ha pedido recibir el video</string> <string name="videos_limit_title">¡Demasiados vídeos!</string> <string name="icon_descr_video_snd_complete">Vídeo enviado</string> - <string name="video_will_be_received_when_contact_completes_uploading">El vídeo se recibirá cuando tu contacto termine de subirlo.</string> + <string name="video_will_be_received_when_contact_completes_uploading">El vídeo se recibirá cuando el contacto termine de subirlo.</string> <string name="videos_limit_desc">Sólo se pueden enviar 10 vídeos de forma simultánea</string> <string name="icon_descr_waiting_for_video">Esperando el vídeo</string> <string name="error_saving_xftp_servers">Error al guardar servidores SMP</string> @@ -1050,7 +1025,7 @@ <string name="your_XFTP_servers">Servidores XFTP</string> <string name="port_verb">Puerto</string> <string name="network_proxy_port">puerto %d</string> - <string name="disable_onion_hosts_when_not_supported"><![CDATA[Configura <i>Usar hosts .onion</i> a No si el proxy SOCKS no los admite.]]></string> + <string name="disable_onion_hosts_when_not_supported"><![CDATA[<i>Usar hosts .onion</i> debe estar a <i>No</i> si el proxy SOCKS no los admite.]]></string> <string name="smp_server_test_download_file">Descargar archivo</string> <string name="network_socks_toggle_use_socks_proxy">Usar proxy SOCKS</string> <string name="host_verb">Host</string> @@ -1087,12 +1062,12 @@ <string name="la_mode_system">Sistema</string> <string name="decryption_error">Error descifrado</string> <string name="alert_text_msg_bad_id">El ID del siguiente mensaje es incorrecto (menor o igual que el anterior). -\nPuede ocurrir por algún bug o cuando la conexión está comprometida.</string> +\nPodría ocurrir por algún error o si la conexión está comprometida.</string> <string name="alert_text_fragment_please_report_to_developers">Por favor, informa a los desarrolladores.</string> <string name="alert_text_decryption_error_too_many_skipped">%1$d mensaje(s) omitido(s).</string> <string name="alert_title_msg_bad_hash">Hash de mensaje incorrecto</string> <string name="alert_title_msg_bad_id">ID de mensaje incorrecto</string> - <string name="alert_text_fragment_encryption_out_of_sync_old_database">Puede ocurrir cuando tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos.</string> + <string name="alert_text_fragment_encryption_out_of_sync_old_database">Puede ocurrir si tu contacto o tu usáis una copia de seguridad antigua de la base de datos.</string> <string name="alert_text_msg_bad_hash">El hash del mensaje anterior es diferente.</string> <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d mensaje(s) no ha(n) podido ser descifrado(s).</string> <string name="stop_file__action">Detener archivo</string> @@ -1118,20 +1093,20 @@ <string name="v5_0_app_passcode">Código de acceso de la aplicación</string> <string name="v5_0_polish_interface">Interfaz en polaco</string> <string name="v5_0_app_passcode_descr">Úsalo en lugar de la autenticación del sistema.</string> - <string name="v5_0_polish_interface_descr">¡Gracias a los colaboradores! Contribuye a través de Weblate.</string> + <string name="v5_0_polish_interface_descr">¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate.</string> <string name="v5_0_large_files_support">Vídeos y archivos de hasta 1Gb</string> <string name="v5_0_large_files_support_descr">¡Rápido y sin necesidad de esperar a que el remitente esté en línea!</string> - <string name="auth_open_chat_profiles">Abrir perfiles</string> + <string name="auth_open_chat_profiles">Cambiar perfil</string> <string name="learn_more">Más información</string> - <string name="if_you_cant_meet_in_person">Si no puedes reunirte en persona, **muestra el código QR por videollamada**, o comparte el enlace.</string> + <string name="if_you_cant_meet_in_person">Si no puedes reunirte en persona, muestra el código QR por videollamada o comparte el enlace.</string> <string name="scan_qr_to_connect_to_contact">Para conectarse, tu contacto puede escanear el código QR o usar el enlace en la aplicación.</string> - <string name="create_simplex_address">Crear tu dirección SimpleX</string> + <string name="create_simplex_address">Crear dirección SimpleX</string> <string name="auto_accept_contact">Auto aceptar</string> <string name="group_welcome_preview">Vista previa</string> <string name="opening_database">Abriendo base de datos…</string> <string name="error_setting_address">Error al introducir dirección</string> - <string name="read_more_in_user_guide_with_link"><![CDATA[Más información en la <font color="#0088ff">Guía de usuario</font>.]]></string> - <string name="one_time_link_short">Enlace un uso</string> + <string name="read_more_in_user_guide_with_link"><![CDATA[Conoce más en la <font color="#0088ff">Guía de Usuario</font>.]]></string> + <string name="one_time_link_short">Enlace de un solo uso</string> <string name="simplex_address">Dirección SimpleX</string> <string name="you_can_accept_or_reject_connection">Cuando alguien solicite conectarse podrás aceptar o rechazar su solicitud.</string> <string name="share_address">Compartir dirección</string> @@ -1162,13 +1137,13 @@ <string name="profile_update_will_be_sent_to_contacts">La actualización del perfil se enviará a tus contactos.</string> <string name="color_received_message">Mensaje recibido</string> <string name="save_auto_accept_settings">Guardar configuración de auto aceptar</string> - <string name="you_can_share_your_address">Puedes compartir tu dirección como enlace o como código QR: cualquiera podrá conectarse contigo.</string> + <string name="you_can_share_your_address">Puedes compartir tu dirección como enlace o código QR para que cualquiera pueda conectarse contigo.</string> <string name="save_settings_question">¿Guardar configuración\?</string> <string name="color_secondary">Secundario</string> <string name="color_sent_message">Mensaje enviado</string> <string name="stop_sharing">Dejar de compartir</string> <string name="stop_sharing_address">¿Dejar de compartir la dirección\?</string> - <string name="theme_colors_section_title">COLORES DEL TEMA</string> + <string name="theme_colors_section_title">COLORES DE LA INTERFAZ</string> <string name="you_can_create_it_later">Puedes crearla más tarde</string> <string name="share_address_with_contacts_question">¿Compartir la dirección con los contactos\?</string> <string name="share_with_contacts">Compartir con contactos</string> @@ -1216,9 +1191,9 @@ <string name="current_version_timestamp">%s (actual)</string> <string name="share_text_sent_at">Enviado: %s</string> <string name="info_row_disappears_at">Desaparecerá</string> - <string name="message_reactions">Reacciones a los mensajes</string> + <string name="message_reactions">Reacciones a mensajes</string> <string name="prohibit_message_reactions">No se permiten reacciones a los mensajes.</string> - <string name="allow_message_reactions">Permitir reacciones a los mensajes.</string> + <string name="allow_message_reactions">Se permiten reacciones a los mensajes.</string> <string name="message_reactions_prohibited_in_this_chat">Las reacciones a los mensajes no están permitidas en este chat.</string> <string name="only_you_can_add_message_reactions">Sólo tú puedes añadir reacciones a los mensajes.</string> <string name="prohibit_message_reactions_group">No se permiten reacciones a los mensajes</string> @@ -1230,7 +1205,7 @@ <string name="custom_time_unit_weeks">semanas</string> <string name="error_loading_details">Error al cargar detalles</string> <string name="group_members_can_add_message_reactions">Los miembros pueden añadir reacciones a los mensajes.</string> - <string name="message_reactions_are_prohibited">Las reacciones a los mensajes no están permitidas en este grupo.</string> + <string name="message_reactions_are_prohibited">Reacciones a los mensajes no permitidas.</string> <string name="only_your_contact_can_add_message_reactions">Sólo tu contacto puede añadir reacciones a los mensajes.</string> <string name="send_disappearing_message_1_minute">1 minuto</string> <string name="info_row_updated_at">Registro actualiz</string> @@ -1244,7 +1219,7 @@ <string name="v5_1_better_messages_descr">- mensajes de voz de hasta 5 minutos. \n- tiempo personalizado para mensajes temporales. \n- historial de edición.</string> - <string name="whats_new_thanks_to_users_contribute_weblate">¡Gracias a los colaboradores! Contribuye a través de Weblate.</string> + <string name="whats_new_thanks_to_users_contribute_weblate">¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate.</string> <string name="v5_1_better_messages">Mensajes mejorados</string> <string name="v5_1_self_destruct_passcode_descr">Al introducirlo todos los datos son eliminados.</string> <string name="v5_1_custom_themes_descr">Personalizar y compartir temas de color.</string> @@ -1253,12 +1228,12 @@ <string name="whats_new_read_more">Saber más</string> <string name="v5_1_japanese_portuguese_interface">Interfaz en japonés y portugués</string> <string name="item_info_no_text">sin texto</string> - <string name="non_fatal_errors_occured_during_import">Algunos errores no críticos ocurrieron durante la importación - para más detalles puedes ver la consola de Chat.</string> + <string name="non_fatal_errors_occured_during_import">Han ocurrido algunos errores no críticos durante la importación:</string> <string name="shutdown_alert_question">¿Cerrar\?</string> - <string name="settings_section_title_app">Aplicación</string> + <string name="settings_section_title_app">APLICACIÓN</string> <string name="settings_restart_app">Reiniciar</string> <string name="settings_shutdown">Cerrar</string> - <string name="shutdown_alert_desc">Las notificaciones dejarán de funcionar hasta que reinicies la aplicación</string> + <string name="shutdown_alert_desc">Las notificaciones dejarán de funcionar hasta que vuelvas a iniciar la aplicación</string> <string name="la_mode_off">Desactivado</string> <string name="error_aborting_address_change">Error al cancelar cambio de dirección</string> <string name="no_filtered_chats">Sin chats filtrados</string> @@ -1270,12 +1245,12 @@ <string name="abort_switch_receiving_address">Cancelar cambio de dirección</string> <string name="files_and_media">Archivos y multimedia</string> <string name="prohibit_sending_files">No se permite el envío de archivos y multimedia.</string> - <string name="files_are_prohibited_in_group">No se permiten archivos y multimedia en este grupo.</string> - <string name="group_members_can_send_files">Los miembros del grupo pueden enviar archivos y multimedia.</string> + <string name="files_are_prohibited_in_group">Archivos y multimedia no permitidos.</string> + <string name="group_members_can_send_files">Los miembros pueden enviar archivos y multimedia.</string> <string name="allow_to_send_files">Se permite enviar archivos y multimedia</string> <string name="favorite_chat">Favorito</string> <string name="only_owners_can_enable_files_and_media">Sólo los propietarios del grupo pueden activar los archivos y multimedia.</string> - <string name="network_option_protocol_timeout_per_kb">Timeout de protocolo por KB</string> + <string name="network_option_protocol_timeout_per_kb">Timeout protocolo por KB</string> <string name="snd_conn_event_ratchet_sync_allowed">renegociación de cifrado permitida para %s</string> <string name="conn_event_ratchet_sync_agreed">cifrado acordado</string> <string name="conn_event_ratchet_sync_ok">cifrado ok</string> @@ -1313,7 +1288,7 @@ <string name="enable_receipts_all">Activar</string> <string name="receipts_contacts_title_disable">¿Desactivar confirmaciones\?</string> <string name="receipts_contacts_title_enable">¿Activar confirmaciones\?</string> - <string name="receipts_section_description_1">Se pueden anular en la configuración de grupos y contactos.</string> + <string name="receipts_section_description_1">Se puede modificar desde la configuración particular de cada grupo o contacto.</string> <string name="receipts_contacts_enable_for_all">Activar para todos</string> <string name="receipts_contacts_enable_keep_overrides">Activar (conservar anulaciones)</string> <string name="receipts_contacts_disable_for_all">Desactivar para todos</string> @@ -1328,7 +1303,7 @@ <string name="v5_2_message_delivery_receipts">¡Confirmación de entrega de mensajes!</string> <string name="v5_2_more_things_descr">- entrega de mensajes más estable. \n- grupos un poco mejores. -\n- ¡y más!</string> +\n- ¡...y más!</string> <string name="sending_delivery_receipts_will_be_enabled">El envío de confirmaciones de entrega se activará para todos los contactos.</string> <string name="v5_2_message_delivery_receipts_descr">¡El doble check que nos faltaba! ✅</string> <string name="you_can_enable_delivery_receipts_later">Puedes activar más tarde en Configuración</string> @@ -1358,9 +1333,9 @@ <string name="connect_via_link_incognito">Conectar en incógnito</string> <string name="turn_off_battery_optimization_button">Permitir</string> <string name="turn_off_system_restriction_button">Abrir configuración</string> - <string name="connect__a_new_random_profile_will_be_shared">Se compartirá un perfil nuevo aleatorio.</string> + <string name="connect__a_new_random_profile_will_be_shared">Compartirás un perfil nuevo aleatorio.</string> <string name="paste_the_link_you_received_to_connect_with_your_contact">Pega el enlace recibido para conectar con tu contacto…</string> - <string name="connect__your_profile_will_be_shared">Tu perfil %1$s será compartido.</string> + <string name="connect__your_profile_will_be_shared">El perfil %1$s será compartido.</string> <string name="disable_notifications_button">Desactivar notificaciones</string> <string name="system_restricted_background_in_call_title">Sin llamadas en segundo plano.</string> <string name="system_restricted_background_desc">SimpleX no puede funcionar en segundo plano. Sólo recibirás notificaciones con la aplicación abierta.</string> @@ -1378,7 +1353,7 @@ <string name="you_can_change_it_later">La contraseña aleatoria se almacenará en Configuración como texto plano. \nPuedes cambiarlo más tarde.</string> <string name="database_encryption_will_be_updated_in_settings">La contraseña para el cifrado de la base de datos se actualizará y almacenará en Configuración</string> - <string name="remove_passphrase_from_settings">Eliminar contraseña de configuración\?</string> + <string name="remove_passphrase_from_settings">¿Eliminar contraseña de configuración?</string> <string name="use_random_passphrase">Usar contraseña aleatoria</string> <string name="save_passphrase_in_settings">Guardar contraseña en configuración</string> <string name="setup_database_passphrase">Configuración contraseña base de datos</string> @@ -1389,16 +1364,16 @@ <string name="rcv_group_event_open_chat">Abrir</string> <string name="v5_3_encrypt_local_files">Cifra archivos almacenados y multimedia</string> <string name="error_creating_member_contact">Error al establecer contacto con el miembro</string> - <string name="socks_proxy_setting_limitations"><![CDATA[<b>Atención</b>: los servidores de retransmisión están conectados mediante SOCKS proxy. Las llamadas y las previsualizaciones de enlaces usan conexión directa.]]></string> - <string name="encrypt_local_files">Cifra archivos locales</string> - <string name="v5_3_new_desktop_app">Nueva aplicación para PC!</string> - <string name="v5_3_new_interface_languages">6 idiomas nuevos para el interfaz</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Recuerda</b>: los servidores están conectados mediante proxy SOCKS, pero las llamadas y las previsualizaciones de enlaces usan conexión directa.]]></string> + <string name="encrypt_local_files">Cifrar archivos locales</string> + <string name="v5_3_new_desktop_app">Nueva aplicación para ordenador!</string> + <string name="v5_3_new_interface_languages">6 nuevos idiomas para la interfaz</string> <string name="v5_3_encrypt_local_files_descr">Cifrado de los nuevos archivos locales (excepto vídeos).</string> - <string name="compose_send_direct_message_to_connect">Enviar mensaje directo para conectar</string> + <string name="compose_send_direct_message_to_connect">Envía un mensaje para conectar</string> <string name="v5_3_discover_join_groups">Descubre y únete a grupos</string> <string name="v5_3_simpler_incognito_mode">Modo incógnito simplificado</string> <string name="v5_3_new_interface_languages_descr">Árabe, Búlgaro, Finlandés, Hebreo, Tailandés y Ucraniano - gracias a los usuarios y Weblate.</string> - <string name="v5_3_new_desktop_app_descr">Crea perfil nuevo en la aplicación para PC. 💻</string> + <string name="v5_3_new_desktop_app_descr">Crea perfil nuevo en la aplicación para ordenador. 💻</string> <string name="error_sending_message_contact_invitation">Error al enviar invitación</string> <string name="v5_3_simpler_incognito_mode_descr">Activa incógnito al conectar.</string> <string name="v5_3_discover_join_groups_descr">- conexión al servicio de directorio (BETA)! @@ -1407,62 +1382,1009 @@ <string name="member_contact_send_direct_message">Enviar mensaje directo</string> <string name="rcv_group_event_member_created_contact">conectado directamente</string> <string name="expand_verb">Expandir</string> - <string name="encryption_renegotiation_error">Error en renegociación de cifrado</string> + <string name="encryption_renegotiation_error">Error de renegociación de cifrado</string> <string name="rcv_direct_event_contact_deleted">contacto eliminado</string> <string name="error_alert_title">Error</string> <string name="create_group_button">Crear grupo</string> <string name="create_another_profile_button">Crear perfil</string> - <string name="connected_desktop">Escritorio conectado</string> + <string name="connected_desktop">Ordenador conectado</string> <string name="new_mobile_device">Nuevo dispositivo móvil</string> - <string name="desktop_address">Dirección desktop</string> + <string name="desktop_address">Dirección ordenador</string> <string name="only_one_device_can_work_at_the_same_time">Sólo un dispositivo puede funcionar al mismo tiempo</string> <string name="connect_plan_join_your_group">¿Unirse a tu grupo?</string> - <string name="marked_deleted_items_description">%d mensajes marcados como borrados</string> + <string name="marked_deleted_items_description">%d mensaje(s) marcado(s) eliminado(s)</string> <string name="connect_plan_group_already_exists">¡El grupo ya existe!</string> - <string name="connect_plan_already_connecting">¡Ya está en proceso de conexión!</string> + <string name="connect_plan_already_connecting">¡Ya en proceso de conexión!</string> <string name="desktop_incompatible_version">Versión incompatible</string> <string name="new_desktop"><![CDATA[<i>(nuevo)</i>]]></string> - <string name="linked_desktop_options">Opciones escritorio enlazado</string> - <string name="linked_desktops">Desktop enlazados</string> - <string name="discover_on_network">Descubrir en red</string> - <string name="rcv_group_and_other_events">, y hay %d eventos más</string> + <string name="linked_desktop_options">Opciones ordenador enlazado</string> + <string name="linked_desktops">Ordenadores enlazados</string> + <string name="discover_on_network">Descubrir en red local</string> + <string name="rcv_group_and_other_events">y %d evento(s) más</string> <string name="connect_plan_connect_via_link">¿Conectar vía enlace?</string> - <string name="connect_plan_already_joining_the_group">¡Ya está en proceso de unirse al grupo!</string> - <string name="moderated_items_description">%d mensajes moderados por %s</string> + <string name="connect_plan_already_joining_the_group">¡En proceso de unirte al grupo!</string> + <string name="moderated_items_description">%1$d mensaje(s) moderado(s) por %2$s</string> <string name="connect_plan_connect_to_yourself">¿Conectarte a tí mismo?</string> <string name="linked_mobiles">Móviles enlazados</string> - <string name="desktop_device">Desktop</string> - <string name="connected_to_desktop">Conectado al escritorio</string> + <string name="desktop_device">Ordenador</string> + <string name="connected_to_desktop">Conectado con ordenador</string> <string name="loading_remote_file_title">Cargando archivo</string> - <string name="connecting_to_desktop">Conectando a desktop</string> - <string name="alert_text_encryption_renegotiation_failed">La renegociación de cifrado ha fallado.</string> - <string name="desktop_devices">Dispositivo desktop</string> + <string name="connecting_to_desktop">Conectando con ordenador</string> + <string name="alert_text_encryption_renegotiation_failed">Renegociación de cifrado fallida.</string> + <string name="desktop_devices">Ordenadores</string> <string name="correct_name_to">¿Corregir el nombre a %s?</string> - <string name="delete_messages__question">Elimina %d mensajes?</string> + <string name="delete_messages__question">¿Eliminar %d mensajes?</string> <string name="link_a_mobile">Enlazar móvil</string> <string name="connect_with_contact_name_question">¿Conectar con %1$s?</string> <string name="block_member_confirmation">Bloquear</string> - <string name="blocked_items_description">%d mensajes bloqueados</string> + <string name="blocked_items_description">%d mensaje(s) bloqueado(s)</string> <string name="block_member_button">Bloquear miembro</string> <string name="connected_mobile">Móvil conectado</string> <string name="delete_and_notify_contact">Eliminar y notificar contacto</string> <string name="connect_plan_open_group">Grupo abierto</string> - <string name="desktop_connection_terminated">Conexión terminada</string> + <string name="desktop_connection_terminated">Conexión finalizada</string> <string name="this_device_version"><![CDATA[<i>(este dispositivo v%s)</i>]]></string> <string name="unblock_member_desc">¡Los mensajes de %s serán mostrados!</string> - <string name="enter_this_device_name">Introduce el nombre de este dispositivo…</string> + <string name="enter_this_device_name">Nombre de este dispositivo…</string> <string name="error">Error</string> - <string name="connect_to_desktop">Conectar a desktop</string> + <string name="connect_to_desktop">Conectar con ordenador</string> <string name="disconnect_remote_host">Desconectar</string> - <string name="block_member_question">Bloquear miembro?</string> - <string name="rcv_group_events_count">%d eventos de grupo</string> + <string name="block_member_question">¿Bloquear miembro?</string> + <string name="rcv_group_events_count">%d evento(s) de grupo</string> <string name="invalid_name">¡Nombre no válido!</string> <string name="connected_to_mobile">Conectado a móvil</string> - <string name="bad_desktop_address">Dirección de escritorio incorrecta</string> + <string name="bad_desktop_address">Dirección ordenador incorrecta</string> <string name="devices">Dispositivo</string> <string name="non_content_uri_alert_title">Ruta archivo no valida.</string> - <string name="disconnect_desktop_question">¿Desconectar desktop?</string> - <string name="block_member_desc">Los mensajes nuevos de %s estarán ocultos!</string> - <string name="desktop_app_version_is_incompatible">La versión de aplicación del desktop %s no es compatible con esta aplicación.</string> + <string name="disconnect_desktop_question">¿Desconectar del ordenador?</string> + <string name="block_member_desc">¡Los mensajes nuevos de %s estarán ocultos!</string> + <string name="desktop_app_version_is_incompatible">La versión de aplicación del ordenador %s no es compatible con esta aplicación.</string> <string name="blocked_item_description">bloqueado</string> -</resources> \ No newline at end of file + <string name="v5_4_block_group_members">Bloquear miembros del grupo</string> + <string name="connect_plan_repeat_connection_request">¿Repetir solicitud de conexión?</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Ya estás conectando con <b>%1$s</b>.]]></string> + <string name="v5_4_incognito_groups_descr">Crear grupo usando perfil aleatorio.</string> + <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Ya estás uniéndote al grupo mediante este enlace.</string> + <string name="refresh_qr_code">Recargar</string> + <string name="group_members_2">%s y %s</string> + <string name="multicast_connect_automatically">Conectar automáticamente</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Ya estás uniéndote al grupo <b>%1$s</b>.]]></string> + <string name="v5_4_link_mobile_desktop">¡Enlazar aplicación móvil con ordenador!</string> + <string name="connect_plan_this_is_your_own_one_time_link">¡Este es tu propio enlace de un solo uso!</string> + <string name="v5_4_link_mobile_desktop_descr">Mediante protocolo seguro de resistencia cuántica.</string> + <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Abre <i>Usar desde ordenador</i> en la aplicación móvil y escanea el código QR.]]></string> + <string name="v5_4_block_group_members_descr">Para ocultar mensajes no deseados.</string> + <string name="unlink_desktop_question">¿Desenlazar ordenador?</string> + <string name="v5_4_better_groups">Grupos mejorados</string> + <string name="video_decoding_exception_desc">El vídeo no puede ser decodificado. Por favor, prueba con otro vídeo o contacta con los desarrolladores.</string> + <string name="rcv_group_event_1_member_connected">%s conectado</string> + <string name="random_port">Aleatorio</string> + <string name="v5_4_incognito_groups">Grupos incógnito</string> + <string name="group_members_n">%s, %s y %d miembros</string> + <string name="this_device">Este dispositivo</string> + <string name="unblock_member_button">Desbloquear miembro</string> + <string name="remote_host_was_disconnected_toast"><![CDATA[Móvil <b>%s</b> desconectado]]></string> + <string name="waiting_for_desktop">Esperando ordenador…</string> + <string name="v5_4_better_groups_descr">Mensajería más segura y conexión más rápida.</string> + <string name="contact_tap_to_connect">Pulsa para conectar</string> + <string name="this_device_name">Nombre del dispositivo</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Ya estás en el grupo <b>%1$s</b>.]]></string> + <string name="connect_plan_this_is_your_own_simplex_address">¡Esta es tu propia dirección SimpleX!</string> + <string name="found_desktop">Ordenador encontrado</string> + <string name="not_compatible">¡No compatible!</string> + <string name="waiting_for_mobile_to_connect">Esperando conexión móvil:</string> + <string name="remove_member_button">Expulsar miembro</string> + <string name="unblock_member_question">¿Desbloquear miembro?</string> + <string name="open_port_in_firewall_desc">Para permitir que la aplicación móvil se conecte al ordenador, abre este puerto en el firewall si está habilitado</string> + <string name="settings_section_title_use_from_desktop">Usar desde ordenador</string> + <string name="session_code">Código de sesión</string> + <string name="connect_plan_repeat_join_request">¿Repetir solicitud de admisión?</string> + <string name="create_chat_profile">Crear perfil de chat</string> + <string name="button_remove_member_question">¿Expulsar miembro?</string> + <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">¡Ya estás conectando mediante este enlace de un solo uso!</string> + <string name="unlink_desktop">Desenlazar</string> + <string name="this_device_name_shared_with_mobile">El nombre del dispositivo será compartido con el cliente móvil conectado.</string> + <string name="verify_code_on_mobile">Verificar código en móvil</string> + <string name="open_port_in_firewall_title">Abrir puerto en firewall</string> + <string name="non_content_uri_alert_text">Has compartido una ruta no válida. Informa a los desarrolladores del problema.</string> + <string name="disconnect_remote_hosts">Desconectar móviles</string> + <string name="group_member_role_author">autor</string> + <string name="paste_desktop_address">Pegar dirección de ordenador</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[¡Este es tu enlace para el grupo <b>%1$s</b>!]]></string> + <string name="verify_code_with_desktop">Verifica el código en el ordenador</string> + <string name="scan_qr_code_from_desktop">Escanear código QR desde ordenador</string> + <string name="unblock_member_confirmation">Desbloquear</string> + <string name="multicast_discoverable_via_local_network">Detectable mediante red local</string> + <string name="v5_4_more_things_descr">- notificar opcionalmente a los contactos eliminados. +\n- nombres de perfil con espacios. +\n- ¡...y más!</string> + <string name="connect_plan_you_have_already_requested_connection_via_this_address">¡Ya has solicitado la conexión mediante esta dirección!</string> + <string name="terminal_always_visible">Mostrar consola en ventana nueva</string> + <string name="scan_from_mobile">Escáner desde móvil</string> + <string name="verify_connections">Verificar conexiones</string> + <string name="loading_remote_file_desc">Por favor, espera mientras el archivo se carga desde el móvil enlazado</string> + <string name="verify_connection">Verificar conexión</string> + <string name="no_connected_mobile">Ningún móvil conectado</string> + <string name="app_was_crashed">Error aplicación</string> + <string name="error_showing_content">error al mostrar el contenido</string> + <string name="error_showing_message">error al mostrar mensaje</string> + <string name="you_can_make_address_visible_via_settings">Puedes hacerlo visible para tus contactos de SimpleX en Configuración.</string> + <string name="recent_history_is_not_sent_to_new_members">El historial no se envía a miembros nuevos.</string> + <string name="retry_verb">Reintentar</string> + <string name="camera_not_available">Cámara no disponible</string> + <string name="enable_sending_recent_history">Se envían hasta 100 mensajes más recientes a los miembros nuevos.</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Añadir contacto</b>: crea un enlace de invitación nuevo o usa un enlace recibido.]]></string> + <string name="disable_sending_recent_history">No se envía el historial a los miembros nuevos.</string> + <string name="or_show_this_qr_code">O muestra el código QR</string> + <string name="recent_history_is_sent_to_new_members">Hasta 100 últimos mensajes son enviados a los miembros nuevos.</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">El código QR escaneado no es un enlace de SimpleX.</string> + <string name="the_text_you_pasted_is_not_a_link">El texto pegado no es un enlace de SimpleX.</string> + <string name="enable_camera_access">Permitir acceso a la cámara</string> + <string name="you_can_view_invitation_link_again">Podrás ver el enlace de invitación en detalles de conexión.</string> + <string name="keep_unused_invitation_question">¿Guardar invitación no usada?</string> + <string name="share_this_1_time_link">Comparte este enlace de un solo uso</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Crear grupo</b>: crea un grupo nuevo.]]></string> + <string name="recent_history">Historial visible</string> + <string name="la_app_passcode">Código acceso app</string> + <string name="new_chat">Nuevo chat</string> + <string name="loading_chats">Cargando chats…</string> + <string name="creating_link">Creando enlace…</string> + <string name="or_scan_qr_code">O escanea el código QR</string> + <string name="invalid_qr_code">Código QR no válido</string> + <string name="add_contact_tab">Añadir contacto</string> + <string name="tap_to_scan">Pulsa para escanear</string> + <string name="keep_invitation_link">Guardar</string> + <string name="tap_to_paste_link">Pulsa para pegar el enlace</string> + <string name="search_or_paste_simplex_link">Buscar o pegar enlace SimpleX</string> + <string name="v5_5_message_delivery_descr">Con uso reducido de batería.</string> + <string name="blocked_by_admin_item_description">bloqueado por administrador</string> + <string name="blocked_by_admin_items_description">%d mensajes bloqueados por el administrador</string> + <string name="error_creating_message">Error al crear mensaje</string> + <string name="error_deleting_note_folder">Error al eliminar notas privadas</string> + <string name="clear_note_folder_question">¿Eliminar notas privadas?</string> + <string name="developer_options_section">Opciones para desarrolladores</string> + <string name="rcv_group_event_member_blocked">ha bloqueado a %s</string> + <string name="rcv_group_event_member_unblocked">ha desbloqueado a %s</string> + <string name="snd_group_event_member_blocked">has bloqueado a %s</string> + <string name="snd_group_event_member_unblocked">has desbloqueado a %s</string> + <string name="group_member_status_unknown_short">desconocido</string> + <string name="info_row_created_at">Creado</string> + <string name="block_for_all_question">¿Bloqear miembro para todos?</string> + <string name="share_text_created_at">Creado: %s</string> + <string name="block_for_all">Bloquear para todos</string> + <string name="unblock_for_all_question">¿Desbloquear el miembro para todos?</string> + <string name="unblock_for_all">Desbloquear para todos</string> + <string name="member_info_member_blocked">bloqueado</string> + <string name="member_blocked_by_admin">bloqueado por administrador</string> + <string name="error_blocking_member_for_all">Error al bloqear el miembro para todos</string> + <string name="v5_5_private_notes_descr">Con cifrado de archivos y multimedia.</string> + <string name="remote_host_error_timeout"><![CDATA[Tiempo de espera para conectar con el móvil <b>%s</b> agotado]]></string> + <string name="chat_is_stopped_you_should_transfer_database">SimpleX está parado. Si has usado esta base de datos en otro dispositivo, debes transferirla de vuelta antes de iniciar SimpleX.</string> + <string name="failed_to_create_user_invalid_desc">Este nombre mostrado no es válido. Por favor, selecciona otro nombre.</string> + <string name="remote_host_was_disconnected_title">Conexión parada</string> + <string name="remote_ctrl_was_disconnected_title">Conexión parada</string> + <string name="remote_host_disconnected_from"><![CDATA[Desconectado del móvil <b>%s</b> debido a: %s]]></string> + <string name="remote_ctrl_disconnected_with_reason">Desconectado debido a: %s</string> + <string name="remote_host_error_bad_state"><![CDATA[La conexión con el móvil <b>%s</b> es deficiente]]></string> + <string name="remote_ctrl_error_bad_state">La conexión al ordenador es deficiente</string> + <string name="remote_ctrl_error_inactive">El ordenador está inactivo</string> + <string name="remote_ctrl_error_busy">El ordenador está ocupado</string> + <string name="agent_critical_error_title">Error crítico</string> + <string name="clear_note_folder_warning">Todos los mensajes serán eliminados. ¡No puede deshacerse!</string> + <string name="start_chat_question">¿Iniciar chat?</string> + <string name="welcome_message_is_too_long">Mensaje de bienvenida demasiado largo</string> + <string name="remote_ctrl_error_timeout">Tiempo de espera para conectar con el ordenador agotado</string> + <string name="remote_ctrl_error_bad_invitation">El ordenador tiene un código de invitación incorrecto</string> + <string name="remote_ctrl_error_disconnected">El ordenador ha sido desconectado</string> + <string name="group_member_status_unknown">estado desconocido</string> + <string name="database_migration_in_progress">Migración de la base de datos en curso. \nPodría tardar varios minutos.</string> + <string name="remote_ctrl_error_bad_version">El ordenador tiene una versión sin soporte. Por favor, asegúrate de usar la misma versión en ambos dispositivos</string> + <string name="profile_update_event_contact_name_changed">el contacto %1$s ha cambiado a %2$s</string> + <string name="profile_update_event_updated_profile">perfil actualizado</string> + <string name="v5_5_private_notes">Notas privadas</string> + <string name="v5_5_message_delivery">Entrega de mensajes mejorada</string> + <string name="remote_host_error_bad_version"><![CDATA[El móvil <b>%s</b> tiene una versión sin soporte. Por favor, asegúrate de usar la misma versión en ambos dispositivos]]></string> + <string name="note_folder_local_display_name">Notas privadas</string> + <string name="possible_slow_function_desc">La ejecución de la función está tardando mucho: %1$d segundos: %2$s</string> + <string name="possible_slow_function_title">Función lenta</string> + <string name="show_slow_api_calls">Mostrar llamadas lentas de API</string> + <string name="saved_message_title">Mensaje guardado</string> + <string name="v5_5_simpler_connect_ui">Pegar enlace para conectar!</string> + <string name="v5_5_simpler_connect_ui_descr">La barra de búsqueda acepta enlaces de invitación.</string> + <string name="v5_5_join_group_conversation">Unirse a la conversación del grupo</string> + <string name="v5_5_join_group_conversation_descr">Historial reciente y bot del directorio mejorado.</string> + <string name="v5_5_new_interface_languages">Interfaz en turco y húngaro</string> + <string name="remote_host_error_inactive"><![CDATA[El móvil <b>%s</b> está inactivo]]></string> + <string name="remote_host_error_missing"><![CDATA[El móvil <b>%s</b> no se encuentra]]></string> + <string name="remote_host_error_busy"><![CDATA[El móvil <b>%s</b> está ocupado]]></string> + <string name="agent_internal_error_title">Error interno</string> + <string name="show_internal_errors">Mostrar errores internos</string> + <string name="failed_to_create_user_invalid_title">¡Nombre mostrado no válido!</string> + <string name="agent_critical_error_desc">Por favor, informa a los desarrolladores: +\n%s +\n +\nSe recomienda reiniciar la aplicación.</string> + <string name="message_too_large">Mensaje demasiado largo</string> + <string name="remote_host_error_disconnected"><![CDATA[El móvil <b>%s</b> se ha desconectado]]></string> + <string name="agent_internal_error_desc">Por favor, informa a los desarrolladores: +\n%s</string> + <string name="restart_chat_button">Reiniciar chat</string> + <string name="past_member_vName">Miembro pasado %1$s</string> + <string name="profile_update_event_member_name_changed">el miembro %1$s ha cambiado a %2$s</string> + <string name="profile_update_event_removed_address">dirección de contacto eliminada</string> + <string name="profile_update_event_removed_picture">ha eliminado la imagen del perfil</string> + <string name="profile_update_event_set_new_address">nueva dirección de contacto</string> + <string name="profile_update_event_set_new_picture">tiene nueva imagen del perfil</string> + <string name="call_service_notification_audio_call">Llamada</string> + <string name="call_service_notification_end_call">Llamada finalizada</string> + <string name="call_service_notification_video_call">Videollamada</string> + <string name="unable_to_open_browser_title">Error al abrir navegador</string> + <string name="unable_to_open_browser_desc">Para llamadas se requiere el navegador web predeterminado. Por favor, configura el navegador predeterminado en el sistema y comparte más información con los desarrolladores.</string> + <string name="migrate_from_device_archiving_database">Archivando base de datos</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Todos tus contactos, conversaciones y archivos serán cifrados, divididos y subidos de forma segura a los servidores XFTP configurados.</string> + <string name="v5_6_safer_groups_descr">Los administradores pueden bloquear a un miembro para los demás</string> + <string name="migrate_to_device_chat_migrated">¡Chat migrado!</string> + <string name="v5_6_app_data_migration">Migrar datos de la aplicación</string> + <string name="migrate_to_device_apply_onion">Aplicar</string> + <string name="migrate_from_device_archive_and_upload">Archivar y subir</string> + <string name="migrate_from_device_cancel_migration">Cancelar migración</string> + <string name="e2ee_info_no_pq_short">Este chat está protegido por cifrado de extremo a extremo.</string> + <string name="migrate_from_another_device">Migrar desde otro dispositivo</string> + <string name="conn_event_enabled_pq">cifrado e2e resistente a tecnología cuántica</string> + <string name="conn_event_disabled_pq">cifrado estándar de extremo a extremo</string> + <string name="migrate_to_device_bytes_downloaded">%s descargado</string> + <string name="migrate_from_device_or_share_this_file_link">O comparte de forma segura este enlace al archivo</string> + <string name="migrate_from_device_migration_complete">Migración completada</string> + <string name="migrate_from_device_start_chat">Iniciar chat</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Atención: el inicio del chat en varios dispositivos es incompatible y provocará fallos en la entrega de mensajes.</string> + <string name="migrate_from_device_verify_passphrase">Verificar frase de contraseña</string> + <string name="migrate_from_device_verify_database_passphrase">Verificar contraseña de la base de datos</string> + <string name="v5_6_quantum_resistant_encryption_descr">¡Activar en chats directos (BETA)!</string> + <string name="v5_6_picture_in_picture_calls_descr">Usar la aplicación durante la llamada.</string> + <string name="v5_6_safer_groups">Grupos más seguros</string> + <string name="migrate_to_device_downloading_details">Descargando detalles del enlace</string> + <string name="invalid_file_link">Enlace no válido</string> + <string name="migrate_to_device_migrating">Migrando</string> + <string name="paste_archive_link">Pegar enlace de archivo</string> + <string name="migrate_to_device_database_init">Preparando descarga</string> + <string name="migrate_to_device_repeat_download">Repetir descarga</string> + <string name="migrate_to_device_try_again">Puedes intentarlo de nuevo.</string> + <string name="migrate_to_device_enter_passphrase">Introduce la frase de contraseña</string> + <string name="migrate_to_device_error_downloading_archive">Error al descargar el archivo</string> + <string name="migrate_to_device_file_delete_or_link_invalid">El archivo ha sido eliminado o el enlace no es válido</string> + <string name="migrate_to_device_finalize_migration">Finalizar migración en otro dispostivo.</string> + <string name="migrate_to_device_import_failed">Error de importación</string> + <string name="migrate_to_device_importing_archive">Importando archivo</string> + <string name="migrate_to_device_repeat_import">Repetir importación</string> + <string name="migrate_from_device_title">Migrar dispositivo</string> + <string name="migrate_to_device_confirm_network_settings_footer">Por favor, confirma que la configuración de red es correcta para este dispositivo.</string> + <string name="migrate_from_device_error_deleting_database">Error al eliminar base de datos</string> + <string name="migrate_from_device_error_uploading_archive">Error al subir el archivo</string> + <string name="migrate_from_device_stopping_chat">Parando chat</string> + <string name="migrate_from_device_repeat_upload">Repetir subida</string> + <string name="migrate_from_device_bytes_uploaded">%s subido</string> + <string name="migrate_from_device_upload_failed">Error de subida</string> + <string name="migrate_from_device_uploading_archive">Subiendo archivo</string> + <string name="migrate_from_device_try_again">Puedes intentarlo de nuevo.</string> + <string name="migrate_from_device_creating_archive_link">Creando enlace al archivo</string> + <string name="migrate_from_device_delete_database_from_device">Eliminar base de datos de este dispositivo</string> + <string name="migrate_from_device_finalize_migration">Finalizar migración</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Atención</b>: el archivo será eliminado.]]></string> + <string name="migrate_from_device_check_connection_and_try_again">Comprueba tu conexión a internet y vuelve a intentarlo</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Para migrar la base de datos confirma que recuerdas la frase de contraseña.</string> + <string name="migrate_from_device_error_verifying_passphrase">Error al verificar la frase de contraseña:</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Recuerda</b>: usar la misma base de datos en dos dispositivos hará que falle el descifrado de mensajes como protección de seguridad.]]></string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[En el nuevo dispositivo selecciona <i>Migrar desde otro dispositivo</i> y escanea el código QR.]]></string> + <string name="migrate_to_device_confirm_network_settings">Confirmar configuración de red</string> + <string name="migrate_from_device_confirm_upload">Confirmar subida</string> + <string name="migrate_to_device_download_failed">Descarga fallida</string> + <string name="migrate_to_device_downloading_archive">Descargando archivo</string> + <string name="migrate_from_device_error_exporting_archive">Error al exportar base de datos del chat</string> + <string name="migrate_from_device_error_saving_settings">Error al guardar ajustes</string> + <string name="migrate_from_device_exported_file_doesnt_exist">El archivo exportado no existe</string> + <string name="migrate_from_device_chat_should_be_stopped">Para continuar, SimpleX debe estar parado.</string> + <string name="e2ee_info_no_pq"><![CDATA[Los mensajes, archivos y llamadas están protegidos mediante <b>cifrado de extremo a extremo</b> con secreto perfecto hacia adelante, repudio y recuperación tras ataque.]]></string> + <string name="e2ee_info_pq"><![CDATA[Los mensajes, archivos y llamadas están protegidos mediante <b>cifrado de extremo a extremo resistente a tecnología cuántica</b> con secreto perfecto hacia adelante, repudio y recuperación tras ataque.]]></string> + <string name="migrate_to_device_title">Migrar aquí</string> + <string name="migrate_from_device_to_another_device">Migrar a otro dispositivo</string> + <string name="v5_6_app_data_migration_descr">Migrar a otro dispositivo mediante código QR.</string> + <string name="auth_open_migration_to_another_device">Abrir menú migración</string> + <string name="or_paste_archive_link">O pegar enlace del archivo</string> + <string name="v5_6_picture_in_picture_calls">Llamadas picture-in-picture</string> + <string name="migrate_from_device_database_init">Preparando subida</string> + <string name="v5_6_quantum_resistant_encryption">Cifrado resistente a tecnología cuántica</string> + <string name="set_passphrase">Establecer frase de contraseña</string> + <string name="e2ee_info_pq_short">Este chat está protegido por cifrado de extremo a extremo resistente a tecnología cuántica.</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[<b>No debes</b> usar la misma base de datos en dos dispositivos.]]></string> + <string name="error_showing_desktop_notification">Error al mostrar notificación, contacta con los desarrolladores.</string> + <string name="permissions_camera_and_record_audio">Cámara y micrófono</string> + <string name="permissions_grant_in_settings">Concédelo en ajustes.</string> + <string name="permissions_required">Conceder permisos</string> + <string name="permissions_camera">Cámara</string> + <string name="permissions_find_in_settings_and_grant">Busca este permiso en los ajustes de Android y concédelo manualmente.</string> + <string name="permissions_grant">Conceder permiso(s) para hacer llamadas</string> + <string name="permissions_record_audio">Micrófono</string> + <string name="permissions_open_settings">Abrir ajustes</string> + <string name="settings_section_title_network_connection">Conexión de red</string> + <string name="forwarded_description">reenviado</string> + <string name="files_and_media_not_allowed">Archivos y multimedia no permitidos</string> + <string name="simplex_links_not_allowed">Enlaces SimpleX no permitidos</string> + <string name="voice_messages_not_allowed">Mensajes de voz no permitidos</string> + <string name="simplex_links">Enlaces SimpleX</string> + <string name="group_members_can_send_simplex_links">Los miembros pueden enviar enlaces de SimpleX.</string> + <string name="simplex_links_are_prohibited_in_group">Enlaces SimpleX no permitidos.</string> + <string name="feature_roles_owners">propietarios</string> + <string name="network_type_cellular">Móvil</string> + <string name="network_type_no_network_connection">Sin conexión de red</string> + <string name="network_type_other">Otro</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="network_type_ethernet">Ethernet por cable</string> + <string name="feature_roles_admins">administradores</string> + <string name="feature_enabled_for">Activado para</string> + <string name="prohibit_sending_simplex_links">No se permite enviar enlaces SimpleX</string> + <string name="feature_roles_all_members">todos los miembros</string> + <string name="allow_to_send_simplex_links">Se permite enviar enlaces SimpleX.</string> + <string name="saved_description">guardado</string> + <string name="saved_from_description">guardado desde %s</string> + <string name="saved_chat_item_info_tab">Guardado</string> + <string name="saved_from_chat_item_info_title">Guardado desde</string> + <string name="forwarded_from_chat_item_info_title">Reenviado por</string> + <string name="download_file">Descargar</string> + <string name="forward_chat_item">Reenviar</string> + <string name="forwarded_chat_item_info_tab">Reenviado</string> + <string name="forward_message">Mensaje reenviado…</string> + <string name="recipients_can_not_see_who_message_from">Los destinatarios no ven de quién procede este mensaje.</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="network_option_rcv_concurrency">Concurrencia en la recepción</string> + <string name="audio_device_earpiece">Auriculares</string> + <string name="audio_device_wired_headphones">Auriculares</string> + <string name="audio_device_speaker">Altavoz</string> + <string name="v5_7_forward">Reenviar y guardar mensajes</string> + <string name="v5_7_call_sounds">Sonido de llamada</string> + <string name="v5_7_new_interface_languages">Interfaz en lituano</string> + <string name="v5_7_network">Gestión de la red</string> + <string name="v5_7_forward_descr">El autor del mensaje se mantiene privado.</string> + <string name="v5_7_call_sounds_descr">Al iniciar llamadas de audio y vídeo.</string> + <string name="v5_7_quantum_resistant_encryption_descr">¡Será habilitado en los chats directos!</string> + <string name="v5_7_network_descr">Conexión de red más fiable.</string> + <string name="settings_section_title_profile_images">Forma de los perfiles</string> + <string name="v5_7_shape_profile_images">Dar forma a las imágenes de perfil</string> + <string name="v5_7_shape_profile_images_descr">Cuadrada, circular o cualquier forma intermedia.</string> + <string name="snd_error_quota">Capacidad excedida - el destinatario no ha recibido los mensajes previos.</string> + <string name="snd_error_relay">Error del servidor de destino: %1$s</string> + <string name="ci_status_other_error">Error: %1$s</string> + <string name="snd_error_proxy_relay">Servidor de reenvío: %1$s +\nError del servidor de destino: %2$s</string> + <string name="snd_error_proxy">Servidor de reenvío: %1$s +\nError: %2$s</string> + <string name="snd_error_expired">Problema en la red - el mensaje ha caducado tras muchos intentos de envío.</string> + <string name="srv_error_version">La versión del servidor es incompatible con la configuración de la red.</string> + <string name="network_smp_proxy_mode_private_routing">Enrutamiento privado</string> + <string name="network_smp_proxy_mode_unknown">Servidores desconocidos</string> + <string name="network_smp_proxy_mode_never_description">NO usar enrutamiento privado.</string> + <string name="update_network_smp_proxy_fallback_question">Enrutamiento de mensajes alternativo</string> + <string name="update_network_smp_proxy_mode_question">Modo de enrutamiento de mensajes</string> + <string name="network_smp_proxy_fallback_prohibit">No</string> + <string name="private_routing_show_message_status">Estado del mensaje</string> + <string name="network_smp_proxy_mode_unprotected_description">Usar enrutamiento privado con servidores desconocidos cuando tu dirección IP no está protegida.</string> + <string name="network_smp_proxy_fallback_allow_protected">Con IP oculta</string> + <string name="network_smp_proxy_fallback_allow">Si</string> + <string name="network_smp_proxy_fallback_allow_description">Enviar mensajes directamente cuando tu servidor o el de destino no admitan enrutamiento privado.</string> + <string name="private_routing_explanation">Para proteger tu dirección IP, el enrutamiento privado usa tu lista de servidores SMP para enviar mensajes.</string> + <string name="network_smp_proxy_fallback_prohibit_description">NO enviar mensajes directamente incluso si tu servidor o el de destino no soportan enrutamiento privado.</string> + <string name="network_smp_proxy_mode_always">Siempre</string> + <string name="network_smp_proxy_fallback_allow_downgrade">Permitir versión anterior</string> + <string name="network_smp_proxy_mode_always_description">Usar siempre enrutamiento privado.</string> + <string name="message_delivery_warning_title">Aviso de entrega de mensaje</string> + <string name="network_smp_proxy_mode_never">Nunca</string> + <string name="settings_section_title_private_message_routing">ENRUTAMIENTO PRIVADO DE MENSAJES</string> + <string name="srv_error_host">La dirección del servidor es incompatible con la configuración de la red.</string> + <string name="network_smp_proxy_mode_unprotected">Con IP desprotegida</string> + <string name="snd_error_auth">Clave incorrecta o conexión desconocida - probablemente esta conexión fue eliminada</string> + <string name="network_smp_proxy_mode_unknown_description">Usar enrutamiento privado con servidores de mensaje desconocidos.</string> + <string name="network_smp_proxy_fallback_allow_protected_description">Enviar mensajes directamente cuando tu dirección IP está protegida y tu servidor o el de destino no admitan enrutamiento privado.</string> + <string name="file_not_approved_title">¡Servidores desconocidos!</string> + <string name="file_not_approved_descr">Sin Tor o VPN, tu dirección IP será visible para estos relés XFTP: +\n%1$s.</string> + <string name="protect_ip_address">Proteger dirección IP</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Sin Tor o VPN, tu dirección IP será visible para los servidores de archivos.</string> + <string name="settings_section_title_files">ARCHIVOS</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">La aplicación pedirá que confirmes las descargas desde servidores de archivos desconocidos (excepto si son .onion o cuando esté habilitado el proxy SOCKS).</string> + <string name="settings_section_title_chat_colors">Colores del chat</string> + <string name="settings_section_title_chat_theme">Tema del chat</string> + <string name="chat_theme_apply_to_mode">Aplicar a</string> + <string name="theme_black">Negro</string> + <string name="chat_theme_apply_to_all_modes">Todos los modos</string> + <string name="color_mode">Modo de color</string> + <string name="color_mode_dark">Oscuro</string> + <string name="chat_theme_apply_to_dark_mode">Modo oscuro</string> + <string name="dark_mode_colors">Colores en modo oscuro</string> + <string name="wallpaper_scale_fill">Relleno</string> + <string name="wallpaper_preview_hello_alice">¡Buenas tardes!</string> + <string name="wallpaper_preview_hello_bob">¡Buenos días!</string> + <string name="color_mode_light">Claro</string> + <string name="chat_theme_apply_to_light_mode">Modo claro</string> + <string name="color_received_quote">Respuesta recibida</string> + <string name="wallpaper_scale_repeat">Mosaico</string> + <string name="theme_remove_image">Quitar imagen</string> + <string name="reset_single_color">Reiniciar color</string> + <string name="wallpaper_scale">Escala</string> + <string name="color_sent_quote">Respuesta enviada</string> + <string name="chat_theme_set_default_theme">Establecer tema predefinido</string> + <string name="color_mode_system">Sistema</string> + <string name="color_wallpaper_background">Color de fondo</string> + <string name="wallpaper_scale_fit">Encaje</string> + <string name="color_primary_variant2">Color adicional 2</string> + <string name="wallpaper_advanced_settings">Configuración avanzada</string> + <string name="settings_section_title_user_theme">Tema del perfil</string> + <string name="chat_list_always_visible">Listado del chat en ventana nueva</string> + <string name="color_wallpaper_tint">Color imagen de fondo</string> + <string name="message_queue_info_server_info">información cola del servidor: %1$s +\n +\núltimo mensaje recibido: %2$s</string> + <string name="chat_theme_reset_to_app_theme">Reiniciar al tema de la aplicación</string> + <string name="v5_8_private_routing">Enrutamiento privado de mensajes 🚀</string> + <string name="v5_8_safe_files">Recibe archivos de forma segura</string> + <string name="v5_8_message_delivery">Mejora del envío de mensajes</string> + <string name="v5_8_message_delivery_descr">Con uso reducido de la batería.</string> + <string name="theme_destination_app_theme">Tema de la app</string> + <string name="v5_8_safe_files_descr">Confirma archivos de servidores desconocidos.</string> + <string name="info_row_debug_delivery">Informe debug</string> + <string name="v5_8_chat_themes_descr">¡Cambia el aspecto de tus chats!</string> + <string name="v5_8_chat_themes">Nuevos temas de chat</string> + <string name="message_queue_info">Información cola de mensajes</string> + <string name="message_queue_info_none">ninguno</string> + <string name="v5_8_private_routing_descr">Protege tu dirección IP de los servidores elegidos por tus contactos.\nActívalo en *Servidores y Red*.</string> + <string name="chat_theme_reset_to_user_theme">Reiniciar al tema del usuario</string> + <string name="error_initializing_web_view">Error al inicializar WebView. Actualiza tu sistema a la última versión. Por favor, ponte en contacto con los desarrolladores. +\nError: %s</string> + <string name="v5_8_persian_ui">Interfaz en persa</string> + <string name="file_error_auth">Clave incorrecta o dirección del bloque del archivo desconocida. Es probable que el archivo se haya eliminado.</string> + <string name="file_error_no_file">Archivo no encontrado, probablemente haya sido eliminado o cancelado.</string> + <string name="file_error_relay">Error del servidor de archivos: %1$s</string> + <string name="file_error">Error de archivo</string> + <string name="temporary_file_error">Error en archivo temporal</string> + <string name="share_text_message_status">Estado del mensaje: %s</string> + <string name="info_row_message_status">Estado del mensaje</string> + <string name="share_text_file_status">Estado del archivo: %s</string> + <string name="info_row_file_status">Estado del archivo</string> + <string name="remote_ctrl_connection_stopped_desc">Comprueba que el móvil y el ordenador están conectados a la misma red local y que el cortafuegos del ordenador permite la conexión. +\nPor favor, comparte cualquier otro problema con los desarrolladores.</string> + <string name="copy_error">Copiar error</string> + <string name="remote_ctrl_connection_stopped_identity_desc">Este enlace ha sido usado en otro dispositivo móvil, por favor crea un enlace nuevo en el ordenador.</string> + <string name="cannot_share_message_alert_title">No se puede enviar el mensaje</string> + <string name="cannot_share_message_alert_text">Las preferencias seleccionadas no permiten este mensaje.</string> + <string name="servers_info">Estadísticas servidores</string> + <string name="servers_info_files_tab">Archivos</string> + <string name="servers_info_target">Mostrando</string> + <string name="subscribed">Suscritas</string> + <string name="subscription_errors">Errores de suscripción</string> + <string name="subscription_results_ignored">Suscripciones ignoradas</string> + <string name="app_check_for_updates_notice_desc">Para ser notificado sobre versiones nuevas, activa el chequeo periódico para las versiones Estable o Beta.</string> + <string name="app_check_for_updates_beta">Beta</string> + <string name="smp_servers_configured">Servidores SMP configurados</string> + <string name="xftp_servers_configured">Servidores XFTP configurados</string> + <string name="servers_info_connected_servers_section_header">Servidores conectados</string> + <string name="servers_info_sessions_connecting">Conectando</string> + <string name="current_user">Perfil actual</string> + <string name="appearance_zoom">Zoom</string> + <string name="servers_info_uploaded">Subido</string> + <string name="app_check_for_updates_update_available">Actualización disponible: %s</string> + <string name="app_check_for_updates_canceled">Descarga de actualización cancelada</string> + <string name="app_check_for_updates_download_completed_title">Actualización descargada</string> + <string name="app_check_for_updates">Buscar actualizaciones</string> + <string name="app_check_for_updates_notice_title">Buscar actualizaciones</string> + <string name="servers_info_statistics_section_header">Estadísticas</string> + <string name="servers_info_subscriptions_total">Total</string> + <string name="servers_info_transport_sessions_section_header">Sesiones de transporte</string> + <string name="xftp_server">Servidor XFTP</string> + <string name="servers_info_proxied_servers_section_footer">No tienes conexión directa a estos servidores. Los mensajes destinados a estos usan enrutamiento privado.</string> + <string name="all_users">Todos los perfiles</string> + <string name="servers_info_sessions_connected">Conectadas</string> + <string name="servers_info_detailed_statistics">Estadísticas detalladas</string> + <string name="servers_info_details">Detalles</string> + <string name="uploaded_files">Archivos subidos</string> + <string name="upload_errors">Errores de subida</string> + <string name="attempts_label">intentos</string> + <string name="completed">Completadas</string> + <string name="connections">Conexiones</string> + <string name="created">Creadas</string> + <string name="decryption_errors">errores de descifrado</string> + <string name="deleted">Eliminadas</string> + <string name="deletion_errors">Errores de eliminación</string> + <string name="member_info_member_disabled">inactivo</string> + <string name="message_forwarded_title">Mensaje reenviado</string> + <string name="member_inactive_desc">El mensaje puede ser entregado más tarde si el miembro vuelve a estar activo.</string> + <string name="member_inactive_title">Miembro inactivo</string> + <string name="please_try_later">Por favor, inténtalo más tarde.</string> + <string name="private_routing_error">Error de enrutamiento privado</string> + <string name="network_error_broker_host_desc">La dirección del servidor es incompatible con la configuración de red: %1$s.</string> + <string name="network_error_broker_version_desc">La versión del servidor es incompatible con tu aplicación: %1$s.</string> + <string name="appearance_font_size">Tamaño de la fuente</string> + <string name="servers_info_reset_stats_alert_error_title">Error al reiniciar las estadísticas</string> + <string name="servers_info_reset_stats_alert_confirm">Reiniciar</string> + <string name="servers_info_reset_stats_alert_message">Las estadísticas de los servidores serán restablecidas. ¡No puede deshacerse!</string> + <string name="servers_info_downloaded">Descargado</string> + <string name="smp_server">Servidor SMP</string> + <string name="message_forwarded_desc">Aún no hay conexión directa, el mensaje es reenviado por el administrador.</string> + <string name="smp_servers_other">Otros servidores SMP</string> + <string name="xftp_servers_other">Otros servidores XFTP</string> + <string name="scan_paste_link">Escanear / Pegar enlace</string> + <string name="subscription_percentage">Mostrar porcentaje</string> + <string name="app_check_for_updates_notice_disable">Desactivar</string> + <string name="app_check_for_updates_disabled">Desactivado</string> + <string name="app_check_for_updates_download_started">Descargando actualización, por favor no cierres la aplicación</string> + <string name="app_check_for_updates_button_download">Descarga %s (%s)</string> + <string name="app_check_for_updates_installed_successfully_title">Instalación completada</string> + <string name="app_check_for_updates_button_install">Instalar actualización</string> + <string name="app_check_for_updates_button_open">Abrir ubicación del archivo</string> + <string name="app_check_for_updates_installed_successfully_desc">Por favor, reinicia la aplicación.</string> + <string name="app_check_for_updates_button_remind_later">Recordar más tarde</string> + <string name="app_check_for_updates_button_skip">Omitir esta versión</string> + <string name="app_check_for_updates_stable">Estable</string> + <string name="member_info_member_inactive">inactivo</string> + <string name="servers_info_modal_error_title">Error</string> + <string name="servers_info_reconnect_server_error">Error al reconectar con el servidor</string> + <string name="servers_info_reconnect_servers_error">Error al reconectar con los servidores</string> + <string name="servers_info_sessions_errors">Errores</string> + <string name="servers_info_subscriptions_section_header">Recepción de mensajes</string> + <string name="servers_info_messages_received">Mensajes recibidos</string> + <string name="servers_info_messages_sent">Mensajes enviados</string> + <string name="servers_info_missing">Sin información, intenta recargar</string> + <string name="servers_info_subscriptions_connections_pending">Pendientes</string> + <string name="servers_info_previously_connected_servers_section_header">Servidores conectados previamente</string> + <string name="servers_info_proxied_servers_section_header">Servidores con proxy</string> + <string name="servers_info_detailed_statistics_received_messages_header">Mensajes recibidos</string> + <string name="servers_info_detailed_statistics_received_total">Total recibidos</string> + <string name="servers_info_detailed_statistics_receive_errors">Errores de recepción</string> + <string name="servers_info_reconnect_all_servers_button">Reconectar todos los servidores</string> + <string name="servers_info_reconnect_server_title">¿Reconectar servidor?</string> + <string name="servers_info_reconnect_servers_title">¿Reconectar servidores?</string> + <string name="servers_info_reconnect_server_message">Reconectar con el servidor para forzar la entrega de mensajes. Se usa tráfico adicional.</string> + <string name="servers_info_reconnect_servers_message">Reconectar todos los servidores para forzar la entrega de mensajes. Usa tráfico adicional.</string> + <string name="servers_info_reset_stats">Reiniciar estadísticas</string> + <string name="servers_info_reset_stats_alert_title">¿Reiniciar todas las estadísticas?</string> + <string name="servers_info_detailed_statistics_sent_messages_header">Mensajes enviados</string> + <string name="servers_info_detailed_statistics_sent_messages_total">Total enviados</string> + <string name="downloaded_files">Archivos descargados</string> + <string name="download_errors">Errores de descarga</string> + <string name="duplicates_label">duplicados</string> + <string name="expired_label">caducados</string> + <string name="open_server_settings_button">Abrir configuración del servidor</string> + <string name="other_label">otros</string> + <string name="other_errors">otros errores</string> + <string name="proxied">Como proxy</string> + <string name="reconnect">Reconectar</string> + <string name="secured">Aseguradas</string> + <string name="send_errors">Errores de envío</string> + <string name="sent_directly">Directamente</string> + <string name="sent_via_proxy">Mediante proxy</string> + <string name="server_address">Dirección del servidor</string> + <string name="size">Tamaño</string> + <string name="servers_info_subscriptions_connections_subscribed">Conexiones activas</string> + <string name="servers_info_starting_from">Iniciado el %s.</string> + <string name="servers_info_private_data_disclaimer">Iniciado el %s \nLos datos son privados en tu dispositivo.</string> + <string name="chunks_deleted">Bloques eliminados</string> + <string name="chunks_downloaded">Bloques descargados</string> + <string name="chunks_uploaded">Bloques subidos</string> + <string name="acknowledged">Confirmaciones</string> + <string name="acknowledgement_errors">Errores de confirmación</string> + <string name="cant_call_contact_connecting_wait_alert_text">Conectando con el contacto, por favor espera o revisa más tarde.</string> + <string name="v6_0_connection_servers_status_descr">Estado de tu conexión y servidores.</string> + <string name="v6_0_connect_faster_descr">Conecta más rápido con tus amigos</string> + <string name="v6_0_connection_servers_status">Controla tu red</string> + <string name="v6_0_private_routing_descr">Protege tu dirección IP y tus conexiones.</string> + <string name="allow_calls_question">¿Permitir llamadas?</string> + <string name="privacy_media_blur_radius">Difuminar multimedia</string> + <string name="calls_prohibited_alert_title">¡Llamadas no permitidas!</string> + <string name="chat_database_exported_title">Base de datos exportada</string> + <string name="network_options_save_and_reconnect">Guardar y reconectar</string> + <string name="reset_all_hints">Reiniciar todas las pistas</string> + <string name="paste_link">Pegar enlace</string> + <string name="privacy_media_blur_radius_medium">Medio</string> + <string name="privacy_media_blur_radius_soft">Suave</string> + <string name="one_hand_ui">Barra de herramientas accesible</string> + <string name="info_view_call_button">llamar</string> + <string name="info_view_connect_button">conectar</string> + <string name="delete_members_messages__question">¿Eliminar %d mensajes de miembros?</string> + <string name="info_view_message_button">mensaje</string> + <string name="compose_message_placeholder">Mensaje</string> + <string name="selected_chat_items_nothing_selected">Nada seleccionado</string> + <string name="delete_messages_mark_deleted_warning">Los mensajes se marcarán para eliminar. El destinatario o destinatarios podrán revelar estos mensajes.</string> + <string name="info_view_open_button">abrir</string> + <string name="select_verb">Seleccionar</string> + <string name="selected_chat_items_selected_n">Seleccionados %d</string> + <string name="toolbar_settings">Configuración</string> + <string name="confirm_delete_contact_question">¿Confirmas la eliminación del contacto?</string> + <string name="contact_deleted">¡Contacto eliminado!</string> + <string name="delete_contact_cannot_undo_warning">El contacto será eliminado. ¡No puede deshacerse!</string> + <string name="conversation_deleted">¡Conversación eliminada!</string> + <string name="delete_without_notification">Elimina sin notificar</string> + <string name="only_delete_conversation">Eliminar sólo la conversación</string> + <string name="keep_conversation">Conservar conversación</string> + <string name="info_view_search_button">buscar</string> + <string name="deleted_chats">Contactos archivados</string> + <string name="message_servers">Servidores de mensajes</string> + <string name="media_and_file_servers">Servidores de archivos y multimedia</string> + <string name="network_socks_proxy">Proxy SOCKS</string> + <string name="privacy_media_blur_radius_off">No</string> + <string name="chat_database_exported_continue">Continuar</string> + <string name="chat_database_exported_not_all_files">Algunos archivos no han sido exportados</string> + <string name="cant_call_contact_alert_title">No se puede llamar al contacto</string> + <string name="cant_call_member_alert_title">No se puede llamar al miembro del grupo</string> + <string name="cant_send_message_to_member_alert_title">No se pueden enviar mensajes al miembro del grupo</string> + <string name="cant_call_contact_deleted_alert_text">El contacto está eliminado.</string> + <string name="action_button_add_members">Invitar</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">Por favor, pide a tu contacto que active las llamadas.</string> + <string name="cant_call_member_send_message_alert_text">Enviar mensaje para activar llamadas.</string> + <string name="v6_0_delete_many_messages_descr">Elimina hasta 20 mensajes a la vez.</string> + <string name="v6_0_reachable_chat_toolbar">Barra de menú accesible</string> + <string name="v6_0_your_contacts_descr">Archiva contactos para charlar más tarde.</string> + <string name="chat_database_exported_save">Puedes guardar el archivo exportado.</string> + <string name="privacy_media_blur_radius_strong">Fuerte</string> + <string name="moderate_messages_will_be_deleted_warning">Los mensajes serán eliminados para todos los miembros.</string> + <string name="moderate_messages_will_be_marked_warning">Los mensajes serán marcados como moderados para todos los miembros.</string> + <string name="info_view_video_button">video</string> + <string name="you_can_still_send_messages_to_contact">Puedes enviar mensajes a %1$s desde Contactos archivados</string> + <string name="you_can_still_view_conversation_with_contact">Aún puedes ver la conversación con %1$s en la lista de chats.</string> + <string name="chat_database_exported_migrate">Puedes migrar la base de datos exportada.</string> + <string name="network_option_tcp_connection">Conexión TCP</string> + <string name="v6_0_reachable_chat_toolbar_descr">Usa la aplicación con una sola mano.</string> + <string name="proxy_destination_error_broker_host">La dirección del servidor de destino de %1$s es incompatible con la configuración del servidor de reenvío %2$s.</string> + <string name="proxy_destination_error_broker_version">La versión del servidor de destino de %1$s es incompatible con el servidor de reenvío %2$s.</string> + <string name="contact_list_header_title">Tus contactos</string> + <string name="you_need_to_allow_calls">Debes permitir que tus contacto te llamen para poder llamarles.</string> + <string name="smp_proxy_error_connecting">Error al conectar con el servidor de reenvío %1$s. Por favor, inténtalo más tarde.</string> + <string name="smp_proxy_error_broker_host">La dirección del servidor de reenvío es incompatible con la configuración de red: %1$s.</string> + <string name="proxy_destination_error_failed_to_connect">El servidor de reenvío %1$s no ha podido conectarse al servidor de destino %2$s. Por favor, intentalo más tarde.</string> + <string name="smp_proxy_error_broker_version">La versión del servidor de reenvío es incompatible con la configuración de red: %1$s.</string> + <string name="no_filtered_contacts">Ningún contacto filtrado</string> + <string name="v6_0_privacy_blur">Difumina para mayor privacidad</string> + <string name="create_address_button">Crear</string> + <string name="one_hand_ui_card_title">Alternar lista de chats:</string> + <string name="v6_0_increase_font_size">Ajusta el tamaño de la fuente.</string> + <string name="v6_0_chat_list_media">Reproduce desde la lista de chats.</string> + <string name="v6_0_upgrade_app">Actualizar la aplicación automáticamente</string> + <string name="invite_friends_short">Invitar</string> + <string name="v6_0_new_chat_experience">Nueva experiencia de chat 🎉</string> + <string name="v6_0_new_media_options">Nuevas opciones multimedia</string> + <string name="one_hand_ui_change_instruction">Puedes cambiar la posición de la barra desde el menú Apariencia.</string> + <string name="v6_0_upgrade_app_descr">Descarga nuevas versiones desde GitHub.</string> + <string name="new_message">Mensaje nuevo</string> + <string name="error_parsing_uri_title">Enlace no válido</string> + <string name="error_parsing_uri_desc">Por favor, comprueba que el enlace SimpleX es correcto.</string> + <string name="forward_files_in_progress_desc">%1$d archivo(s) se está(n) descargando todavía.</string> + <string name="n_other_file_errors">%1$d otro(s) error(es) de archivo.</string> + <string name="settings_section_title_chat_database">BASE DE DATOS</string> + <string name="error_forwarding_messages">Error en reenvío de mensajes</string> + <string name="forward_alert_title_messages_to_forward">¿Reenviar %1$s mensaje(s)?</string> + <string name="forward_multiple">Reenviar mensajes…</string> + <string name="system_mode_toast">Modo de sistema</string> + <string name="forward_alert_forward_messages_without_files">¿Reenviar mensajes sin los archivos?</string> + <string name="network_proxy_incorrect_config_desc">Asegúrate de que la configuración del proxy es correcta.</string> + <string name="forward_files_missing_desc">%1$d archivo(s) ha(n) sido eliminado(s).</string> + <string name="n_file_errors">%1$d error(es) de archivo:\n%2$s</string> + <string name="forward_files_failed_to_receive_desc">La descarga ha fallado para %1$d archivo(s).</string> + <string name="forward_files_not_accepted_desc">%1$d archivo(s) no se ha(n) descargado.</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s mensajes no enviados</string> + <string name="forward_files_not_accepted_receive_files">Descargar</string> + <string name="compose_forward_messages_n">Reenviando %1$s mensajes</string> + <string name="forward_files_messages_deleted_after_selection_desc">Los mensajes han sido eliminados después de seleccionarlos.</string> + <string name="forward_alert_title_nothing_to_forward">¡Nada para reenviar!</string> + <string name="compose_save_messages_n">Guardando %1$s mensajes</string> + <string name="network_proxy_auth_mode_no_auth">No uses credenciales con proxy.</string> + <string name="network_proxy_incorrect_config_title">Error guardando proxy</string> + <string name="network_proxy_password">Contraseña</string> + <string name="network_proxy_auth">Autenticación proxy</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">Se usan credenciales proxy diferentes para cada conexión.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">Se usan credenciales proxy diferentes para cada perfil.</string> + <string name="network_proxy_random_credentials">Credenciales aleatorias</string> + <string name="network_proxy_username">Nombre de usuario</string> + <string name="network_proxy_auth_mode_username_password">Tus credenciales podrían ser enviadas sin cifrar.</string> + <string name="migrate_from_device_remove_archive_question">¿Eliminar archivo?</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">El archivo de bases de datos subido será eliminado permanentemente de los servidores.</string> + <string name="delete_messages_cannot_be_undone_warning">Los mensajes serán eliminados. ¡No puede deshacerse!</string> + <string name="switching_profile_error_title">Error al cambiar perfil</string> + <string name="select_chat_profile">Selecciona perfil de chat</string> + <string name="new_chat_share_profile">Perfil a compartir</string> + <string name="switching_profile_error_message">Tu conexión ha sido trasladada a %s pero ha ocurrido un error inesperado al redirigirte al perfil.</string> + <string name="icon_descr_sound_muted">Sonido silenciado</string> + <string name="error_initializing_web_view_wrong_arch">Error al iniciar WebView. Asegúrate de tener WebView instalado y que sea compatible con la arquitectura amr64.\nError: %s</string> + <string name="settings_section_title_message_shape">Forma del mensaje</string> + <string name="settings_message_shape_corner">Esquinas</string> + <string name="settings_message_shape_tail">Cola</string> + <string name="network_session_mode_session_description">Se usarán credenciales SOCKS nuevas cada vez que inicies la aplicación.</string> + <string name="network_session_mode_session">por sesión</string> + <string name="call_desktop_permission_denied_safari">Abre la configuración de Safari / Sitios Web / Micrófono y a continuación selecciona Permitir para localhost.</string> + <string name="call_desktop_permission_denied_chrome">Pulsa el botón info del campo dirección para permitir el uso del micrófono.</string> + <string name="call_desktop_permission_denied_title">Para hacer llamadas, permite el uso del micrófono. Cuelga e intenta llamar de nuevo.</string> + <string name="network_session_mode_server_description">Se usarán credenciales SOCKS nuevas para cada servidor.</string> + <string name="network_session_mode_server">por servidor</string> + <string name="v6_1_better_calls">Llamadas mejoradas</string> + <string name="v6_1_message_dates_descr">Sistema de fechas mejorado.</string> + <string name="v6_1_better_user_experience">Experiencia de usuario mejorada</string> + <string name="v6_1_customizable_message_descr">Forma personalizable de los mensajes.</string> + <string name="v6_1_forward_many_messages_descr">Desplazamiento de hasta 20 mensajes.</string> + <string name="v6_1_better_security_descr">Protocolos de SimpleX auditados por Trail of Bits.</string> + <string name="v6_1_better_calls_descr">Intercambia audio y video durante la llamada.</string> + <string name="v6_1_better_security">Seguridad mejorada ✅</string> + <string name="v6_1_delete_many_messages_descr">Elimina o modera hasta 200 mensajes a la vez.</string> + <string name="v6_1_switch_chat_profile_descr">Cambia el perfil de chat para invitaciones de un solo uso.</string> + <string name="failed_to_save_servers">Error al guardar servidores</string> + <string name="errors_in_servers_configuration">Error en la configuración del servidor.</string> + <string name="for_chat_profile">Para el perfil de chat %s:</string> + <string name="no_message_servers_configured">Sin servidores para mensajes.</string> + <string name="no_media_servers_configured_for_private_routing">Sin servidores para recibir archivos.</string> + <string name="no_media_servers_configured_for_sending">Sin servidores para enviar archivos.</string> + <string name="connection_security">Seguridad de conexión</string> + <string name="share_1_time_link_with_a_friend">Compartir enlace de un uso con un amigo</string> + <string name="share_simplex_address_on_social_media">Comparte tu dirección SimpleX en redes sociales.</string> + <string name="address_settings">Ajustes de dirección</string> + <string name="create_1_time_link">Crear enlace de un uso</string> + <string name="for_social_media">Para redes sociales</string> + <string name="simplex_address_or_1_time_link">¿Dirección SimpleX o enlace de un uso?</string> + <string name="onboarding_choose_server_operators">Operadores de servidores</string> + <string name="onboarding_network_operators">Operadores de red</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">Las condiciones de los operadores habilitados serán aceptadas después de 30 días.</string> + <string name="onboarding_network_operators_review_later">Revisar más tarde</string> + <string name="operator_conditions_accepted_on">Condiciones aceptadas el: %s.</string> + <string name="operator_info_title">Operador de red</string> + <string name="operator">Operador</string> + <string name="network_preset_servers_title">Servidores predefinidos</string> + <string name="operator_review_conditions">Revisar condiciones</string> + <string name="operator_servers_title">%s servidores</string> + <string name="operator_conditions_will_be_accepted_on">Las condiciones serán aceptadas el: %s.</string> + <string name="operator_conditions_of_use">Condiciones de uso</string> + <string name="operator_use_for_messages_private_routing">Para enrutamiento privado</string> + <string name="error_adding_server">Error al añadir servidor</string> + <string name="operator_open_changes">Abrir cambios</string> + <string name="operator_open_conditions">Abrir condiciones</string> + <string name="server_added_to_operator__name">Servidor añadido al operador %s.</string> + <string name="error_server_operator_changed">El operador del servidor ha cambiado.</string> + <string name="error_server_protocol_changed">El protocolo del servidor ha cambiado.</string> + <string name="appearance_app_toolbars">Barras de herramientas</string> + <string name="appearance_bars_blur_radius">Difuminar</string> + <string name="v6_2_improved_chat_navigation">Navegación en el chat mejorada</string> + <string name="v6_2_network_decentralization">Descentralización de la red</string> + <string name="v6_2_improved_chat_navigation_descr">- El chat abre en el primer mensaje no leído.\n- Desplazamiento hasta los mensajes citados.</string> + <string name="accept_conditions">Aceptar condiciones</string> + <string name="operator_conditions_accepted">Condiciones aceptadas</string> + <string name="operator_added_xftp_servers">Servidores de archivos y multimedia añadidos</string> + <string name="operator_added_message_servers">Servidores de mensajes añadidos</string> + <string name="address_or_1_time_link">¿Dirección o enlace de un solo uso?</string> + <string name="operator_conditions_accepted_for_enabled_operators_on">Las condiciones serán aceptadas automáticamente para los operadores habilitados el: %s.</string> + <string name="onboarding_network_operators_continue">Continuar</string> + <string name="operator_conditions_failed_to_load">El texto con las condiciones actuales no se ha podido cargar, puedes revisar las condiciones en el siguiente enlace:</string> + <string name="v6_2_network_decentralization_enable_flux">Activa Flux en Servidores y Red para mejorar la privacidad de los metadatos.</string> + <string name="error_accepting_operator_conditions">Error al aceptar las condiciones</string> + <string name="error_updating_server_title">Error al actualizar el servidor</string> + <string name="v6_2_network_decentralization_enable_flux_reason">para mejorar la privacidad de los metadatos.</string> + <string name="message_deleted_or_not_received_error_title">Ningún mensaje</string> + <string name="smp_servers_new_server">Servidor nuevo</string> + <string name="no_media_servers_configured">Sin servidores para archivos y multimedia.</string> + <string name="no_message_servers_configured_for_private_routing">Sin servidores para enrutamiento privado.</string> + <string name="no_message_servers_configured_for_receiving">Sin servidores para recibir mensajes.</string> + <string name="operator_server_alert_title">Servidor del operador</string> + <string name="or_to_share_privately">O para compartir en privado</string> + <string name="onboarding_select_network_operators_to_use">Selecciona los operadores de red a utilizar</string> + <string name="share_address_publicly">Compartir dirección públicamente</string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">Compartir enlaces de un solo uso y direcciones SimpleX es seguro a través de cualquier medio.</string> + <string name="onboarding_network_operators_update">Actualizar</string> + <string name="operator_website">Sitio web</string> + <string name="your_servers">Tus servidores</string> + <string name="use_servers_of_operator_x">Usar %s</string> + <string name="operator_use_operator_toggle_description">Usar servidores</string> + <string name="operator_use_for_messages">Uso para mensajes</string> + <string name="view_conditions">Ver condiciones</string> + <string name="operator_use_for_messages_receiving">Para recibir</string> + <string name="operator_use_for_sending">Para enviar</string> + <string name="operator_use_for_files">Uso para archivos</string> + <string name="appearance_in_app_bars_alpha">Transparencia</string> + <string name="view_updated_conditions">Ver condiciones actualizadas</string> + <string name="connection_error_quota">Mensajes no entregados</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[Los enlaces de un uso pueden ser usados <i>solamente con un contacto</i> - comparte en persona o mediante cualquier aplicación de mensajería.]]></string> + <string name="you_can_set_connection_name_to_remember">Puedes añadir un nombre a la conexión para recordar a quién corresponde.</string> + <string name="onboarding_network_operators_app_will_use_different_operators">La aplicación protege tu privacidad mediante el uso de diferentes operadores en cada conversación.</string> + <string name="onboarding_network_operators_conditions_you_can_configure">Puedes configurar los operadores desde los ajustes de Servidores y Red.</string> + <string name="operators_conditions_accepted_for"><![CDATA[Las condiciones se han aceptado para el(los) operador(s): <b>%s</b>.]]></string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[Las condiciones serán aceptadas para el/los operador(es): <b>%s</b>.]]></string> + <string name="operator_conditions_accepted_for_some"><![CDATA[Las condiciones ya se han aceptado para el/los siguiente(s) operador(s): <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[Las mismas condiciones se aplican al operador <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Las condiciones serán aceptadas para el/los operador(es): <b>%s</b>.]]></string> + <string name="operators_conditions_will_also_apply"><![CDATA[Estas condiciones también se aplican para: <b>%s</b>.]]></string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[Para usar los servidores de <b>%s</b>, debes aceptar las condiciones de uso.]]></string> + <string name="xftp_servers_per_user">Servidores para enviar archivos en tu perfil</string> + <string name="v6_2_network_decentralization_descr">¡Segundo operador predefinido!</string> + <string name="onboarding_network_operators_configure_via_settings">Puedes configurar los servidores a través de su configuración.</string> + <string name="to_protect_against_your_link_replaced_compare_codes">Para protegerte contra una sustitución del enlace, puedes comparar los códigos de seguridad con tu contacto.</string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Las mismas condiciones se aplican a el/los operador(es) <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_applied"><![CDATA[Estas condiciones también se aplican para: <b>%s</b>.]]></string> + <string name="onboarding_network_operators_app_will_use_for_routing">Por ejemplo, si tu contacto recibe a través de un servidor de SimpleX Chat, tu aplicación enviará a través de un servidor de Flux.</string> + <string name="address_creation_instruction">Pulsa Crear dirección SimpleX en el menú para crearla más tarde.</string> + <string name="connection_error_quota_desc">La conexión ha alcanzado el límite de mensajes no entregados. es posible que tu contacto esté desconectado.</string> + <string name="message_deleted_or_not_received_error_desc">El mensaje ha sido eliminado o aún no se ha recibido.</string> + <string name="remote_hosts_section">Móvil remoto</string> + <string name="chat_archive">O importa desde un archivo</string> + <string name="direct_messages_are_prohibited_in_chat">Mensajes directos entre miembros de este chat no permitidos.</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>En dispositivos Xiaomi:</b> por favor, habilita el Autoinicio en los ajustes del sistema para que las notificaciones funcionen.]]></string> + <string name="maximum_message_size_reached_text">Por favor, reduce el tamaño del mensaje y envíalo de nuevo.</string> + <string name="maximum_message_size_reached_non_text">Por favor, reduce el tamaño del mensaje o elimina los archivos y envíalo de nuevo.</string> + <string name="maximum_message_size_reached_forwarding">Puedes copiar y reducir el tamaño del mensaje para enviarlo.</string> + <string name="add_your_team_members_to_conversations">Añade a miembros de tu equipo a las conversaciones.</string> + <string name="onboarding_notifications_mode_battery">Notificaciones y batería</string> + <string name="invite_to_chat_button">Invitar al chat</string> + <string name="button_add_friends">Añadir amigos</string> + <string name="button_add_team_members">Añadir miembros del equipo</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">El chat será eliminado para todos los miembros. ¡No puede deshacerse!</string> + <string name="button_delete_chat">Eliminar chat</string> + <string name="delete_chat_question">¿Eliminar chat?</string> + <string name="button_leave_chat">Salir del chat</string> + <string name="delete_chat_for_self_cannot_undo_warning">El chat será eliminado para tí. ¡No puede deshacerse!</string> + <string name="only_chat_owners_can_change_prefs">Sólo los propietarios del chat pueden cambiar las preferencias.</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">El miembro será eliminado del chat. ¡No puede deshacerse!</string> + <string name="member_role_will_be_changed_with_notification_chat">El rol cambiará a %s. Todos serán notificados.</string> + <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Dejarás de recibir mensajes de este chat. El historial del chat se conserva.</string> + <string name="how_it_helps_privacy">Cómo ayuda a la privacidad</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">Cuando está habilitado más de un operador, ninguno dispone de los metadatos para conocer quién se comunica con quién.</string> + <string name="chat_main_profile_sent">Tu perfil de chat será enviado a los miembros de chat</string> + <string name="v6_2_business_chats">Chats empresariales</string> + <string name="leave_chat_question">¿Salir del chat?</string> + <string name="v6_2_business_chats_descr">Privacidad para tus clientes.</string> + <string name="display_name_accepted_invitation">invitación aceptada</string> + <string name="display_name_requested_to_connect">solicitado para conectar</string> + <string name="business_address">Dirección empresarial</string> + <string name="onboarding_notifications_mode_periodic_desc_short">Comprobar mensajes cada 10 min.</string> + <string name="onboarding_notifications_mode_off_desc_short">Sin servicio en segundo plano</string> + <string name="info_row_chat">Chat</string> + <string name="chat_bottom_bar">Barra de chat accesible</string> + <string name="direct_messages_are_prohibited">Mensajes directos entre miembros no permitidos.</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Ya estás conectado con <b>%1$s</b>.]]></string> + <string name="connect_plan_chat_already_exists">¡El chat ya existe!</string> + <string name="onboarding_network_about_operators">Acerca de los operadores</string> + <string name="onboarding_notifications_mode_service_desc_short">La aplicación siempre funciona en segundo plano</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[Todos los mensajes y archivos son enviados <b>cifrados de extremo a extremo</b> y con seguridad postcuántica en mensajes directos.]]></string> + <string name="maximum_message_size_title">¡Mensaje demasiado largo!</string> + <string name="onboarding_network_operators_simplex_flux_agreement">Simplex Chat y Flux han acordado incluir en la aplicación servidores operados por Flux.</string> + <string name="debug_logs">Activar registros</string> + <string name="error_saving_database">Error al guardar base de datos</string> + <string name="sync_connection_desc">La conexión requiere renegociar el cifrado.</string> + <string name="encryption_renegotiation_in_progress">Renegociación de cifrado en curso.</string> + <string name="sync_connection_confirm">Reparar</string> + <string name="sync_connection_question">¿Reparar conexión?</string> + <string name="connection_not_ready">Conexión no establecida.</string> + <string name="error_loading_chat_tags">Error al cargar listas de chat</string> + <string name="error_updating_chat_tags">Error al actualizar las listas de chat</string> + <string name="chat_list_favorites">Favoritos</string> + <string name="list_menu">Lista</string> + <string name="no_chats">Sin chats</string> + <string name="no_chats_found">Ningún chat encontrado</string> + <string name="no_chats_in_list">Sin chats en la lista %s.</string> + <string name="chat_list_add_list">Añadir lista</string> + <string name="chat_list_all">Todo</string> + <string name="chat_list_businesses">Empresas</string> + <string name="chat_list_contacts">Contactos</string> + <string name="chat_list_groups">Grupos</string> + <string name="open_with_app">Abrir con %s</string> + <string name="add_to_list">Añadir a la lista</string> + <string name="delete_chat_list_warning">Todos los chats se quitarán de la lista %s y esta será eliminada</string> + <string name="create_list">Crear lista</string> + <string name="delete_chat_list_menu_action">Eliminar</string> + <string name="delete_chat_list_question">¿Eliminar lista?</string> + <string name="edit_chat_list_menu_action">Editar</string> + <string name="list_name_field_placeholder">Nombre de la lista...</string> + <string name="duplicated_list_error">El nombre y el emoji deben ser diferentes en todas las listas.</string> + <string name="save_list">Guardar lista</string> + <string name="no_unread_chats">Ningún chat sin leer</string> + <string name="error_creating_chat_tags">Error al crear lista de chat</string> + <string name="chat_list_notes">Notas</string> + <string name="change_order_chat_list_menu_action">Reordenar</string> + <string name="change_list">Cambiar lista</string> + <string name="error_creating_report">Error al crear informe</string> + <string name="prefs_error_saving_settings">Error al guardar la configuración</string> + <string name="report_item_archived_by">informe archivado por %s</string> + <string name="report_archive_alert_title">¿Archivar informe?</string> + <string name="report_archive_alert_desc">El informe será archivado para ti.</string> + <string name="report_verb">Informar</string> + <string name="delete_report">Eliminar informe</string> + <string name="group_reports_active_one">1 informe</string> + <string name="chat_list_group_reports">Informes</string> + <string name="report_compose_reason_header_spam">Informar de spam: sólo los moderadores del grupo lo verán.</string> + <string name="report_compose_reason_header_community">Informar de violación: sólo los moderadores del grupo lo verán.</string> + <string name="report_compose_reason_header_illegal">Informar de contenido: sólo los moderadores del grupo lo verán.</string> + <string name="report_compose_reason_header_other">Informar de otros: sólo los moderadores del grupo lo verán.</string> + <string name="group_member_role_moderator">moderador</string> + <string name="blocking_reason_content">El contenido viola las condiciones de uso.</string> + <string name="blocking_reason_spam">Spam</string> + <string name="connection_error_blocked">Conexión bloqueada</string> + <string name="connection_error_blocked_desc">Conexión bloqueada por el operador del servidor:\n%1$s.</string> + <string name="report_reason_alert_title">¿Motivo del informe?</string> + <string name="file_error_blocked">Archivo bloqueado por el operador del servidor:\n%1$s</string> + <string name="archive_verb">Archivar</string> + <string name="archive_report">Archivar informe</string> + <string name="group_reports_member_reports">Informes de miembros</string> + <string name="group_reports_active">%d informes</string> + <string name="report_compose_reason_header_profile">Informar del perfil de un miembro: sólo los moderadores del grupo lo verán.</string> + <string name="report_reason_other">Otro motivo</string> + <string name="report_item_archived">informes archivados</string> + <string name="report_reason_community">Violación de las normas de la comunidad</string> + <string name="report_reason_illegal">Contenido inapropiado</string> + <string name="report_reason_profile">Perfil inapropiado</string> + <string name="report_item_visibility_moderators">Solo el remitente y el moderador pueden verlo</string> + <string name="report_item_visibility_submitter">Solo tú y los moderadores podéis verlo</string> + <string name="report_reason_spam">Spam</string> + <string name="privacy_chat_list_open_links">Abrir enlaces desde listado de chats</string> + <string name="privacy_chat_list_open_links_yes">Si</string> + <string name="privacy_chat_list_open_links_no">No</string> + <string name="privacy_chat_list_open_web_link_question">¿Abrir enlace web?</string> + <string name="privacy_chat_list_open_links_ask">Preguntar</string> + <string name="privacy_chat_list_open_web_link">Abrir enlace</string> + <string name="text_field_set_chat_placeholder">Nombre para el chat…</string> + <string name="disable_automatic_deletion_question">¿Desactivar la eliminación automática de mensajes?</string> + <string name="disable_automatic_deletion">Desactivar</string> + <string name="change_automatic_deletion_question">¿Modificar la eliminación automática de mensajes?</string> + <string name="chat_ttl_options_footer">Elimina los mensajes del dispositivo</string> + <string name="disable_automatic_deletion_message">Los mensajes de esta conversación nunca se eliminan.</string> + <string name="change_automatic_chat_deletion_message">Todos los mensajes previos al período seleccionado serán eliminados del chat. ¡No puede deshacerse!</string> + <string name="chat_item_ttl_year">1 año</string> + <string name="chat_item_ttl_default">predefinido (%s)</string> + <string name="network_smp_web_port_section_title">Puerto TCP para mensajes</string> + <string name="network_smp_web_port_footer">Se usa el puerto TCP %1$s cuando no se ha especificado otro.</string> + <string name="network_smp_web_port_toggle">Usar puerto web</string> + <string name="mute_all_chat">Silenciar todo</string> + <string name="unread_mentions">Menciones sin leer</string> + <string name="max_group_mentions_per_message_reached">¡Puedes mencionar hasta %1$s miembros por mensaje!</string> + <string name="group_members_can_send_reports">Los miembros pueden informar de mensajes a los moderadores.</string> + <string name="disable_sending_member_reports">No se permite informar de mensajes a los moderadores.</string> + <string name="enable_sending_member_reports">Se permite informar de mensajes a los moderadores.</string> + <string name="notification_group_report">Informe: %s</string> + <string name="report_archive_alert_title_nth">¿Archivar %d informes?</string> + <string name="archive_reports">Archivar informes</string> + <string name="report_archive_for_all_moderators">Para todos los moderadores</string> + <string name="report_archive_for_me">para mí</string> + <string name="member_reports_are_prohibited">No se permite informar de mensajes en este grupo.</string> + <string name="report_archive_alert_desc_all">Todos los informes serán archivados para ti.</string> + <string name="report_archive_alert_title_all">¿Archivar todos los informes?</string> + <string name="v6_3_mentions_descr">Las menciones ahora se notifican.</string> + <string name="v6_3_mentions">Menciona a miembros 👋</string> + <string name="v6_3_organize_chat_lists_descr">No pierdas los mensajes importantes.</string> + <string name="v6_3_organize_chat_lists">Organiza tus chats en listas</string> + <string name="v6_3_private_media_file_names">Nombres privados en archivos de media.</string> + <string name="v6_3_faster_deletion_of_groups">Eliminación más rápida de grupos.</string> + <string name="v6_3_faster_sending_messages">Envío más rápido de mensajes.</string> + <string name="v6_3_reports_descr">Ayuda a los admins a moderar sus grupos.</string> + <string name="v6_3_better_groups_performance">Rendimiento de grupos mejorado</string> + <string name="v6_3_better_privacy_and_security">Privacidad y seguridad mejoradas</string> + <string name="v6_3_reports">Envía informes privados</string> + <string name="v6_3_set_message_expiration_in_chats">Establece el vencimiento para los mensajes en los chats.</string> + <string name="group_preview_rejected">rechazado</string> + <string name="group_member_status_rejected">rechazado</string> + <string name="button_remove_members_question">¿Expulsar miembros?</string> + <string name="unblock_members_desc">¡Los mensajes de estos miembros serán mostrados!</string> + <string name="unblock_members_for_all_question">¿Desbloquear los miembros para todos?</string> + <string name="block_members_desc">¡Todos los mensajes nuevos de estos miembros estarán ocultos!</string> + <string name="block_members_for_all_question">¿Bloquear miembros para todos?</string> + <string name="members_will_be_removed_from_chat_cannot_be_undone">Los miembros serán expulsados del chat. ¡No puede deshacerse!</string> + <string name="operator_updated_conditions">Condiciones actualizadas</string> + <string name="feature_roles_moderators">moderadores</string> + <string name="members_will_be_removed_from_group_cannot_be_undone">Los miembros serán expulsados del grupo. ¡No puede deshacerse!</string> + <string name="group_member_status_pending_approval">pendiente de aprobación</string> + <string name="group_member_status_pending_approval_short">pendiente</string> + <string name="error_reading_passphrase">Error al leer la frase de contraseña de la base de datos</string> + <string name="restore_passphrase_can_not_be_read_desc">La frase de contraseña no se ha podido leer en Keystore. 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.</string> + <string name="restore_passphrase_can_not_be_read_enter_manually_desc">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.</string> + <string name="onboarding_conditions_accept">Aceptar</string> + <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Política de privacidad y condiciones de uso.</string> + <string name="onboarding_conditions_private_chats_not_accessible">Los chats privados, los grupos y tus contactos no son accesibles para los operadores de servidores.</string> + <string name="onboarding_conditions_by_using_you_agree">Al usar SimpleX Chat, aceptas:\n- enviar únicamente contenido legal en los grupos públicos.\n- respetar a los demás usuarios - spam prohibido.</string> + <string name="onboarding_conditions_configure_server_operators">Configurar operadores de servidores</string> + <string name="simplex_link_channel">Enlace de canal SimpleX</string> + <string name="full_link_button_text">Enlace completo</string> + <string name="short_link_button_text">Enlace corto</string> + <string name="link_requires_newer_app_version_please_upgrade">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.</string> + <string name="unsupported_connection_link">Enlace de conexión no compatible</string> + <string name="privacy_short_links">Usar enlaces cortos (BETA)</string> + <string name="network_smp_web_port_preset_footer">Usar puerto TCP 443 solo en servidores predefinidos.</string> + <string name="network_smp_web_port_all">Todos los servidores</string> + <string name="network_smp_web_port_preset">Servidores predefinidos</string> + <string name="network_smp_web_port_off">No</string> +</resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml new file mode 100644 index 0000000000..b8e99587f7 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml @@ -0,0 +1,1854 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">رمزگشایی %1$d پیام ناموفق بود.</string> + <string name="moderated_items_description">%1$d پیام توسط %2$s حذف شد</string> + <string name="integrity_msg_skipped">%1$d پیام از قلم افتاده</string> + <string name="group_info_section_title_num_members">%1$s عضو</string> + <string name="contact_wants_to_connect_via_call">%1$s می‌خواهد به شما متصل شود، به وسیله</string> + <string name="chat_item_ttl_day">۱ روز</string> + <string name="send_disappearing_message_1_minute">۱ دقیقه</string> + <string name="abort_switch_receiving_address_confirm">لغو</string> + <string name="abort_switch_receiving_address">لغو تغییر نشانی</string> + <string name="abort_switch_receiving_address_question">تغییر نشانی را لغو می‌کنید؟</string> + <string name="about_simplex">درباره سیمپل‌اکس(SimpleX)</string> + <string name="connect_via_contact_link">به وسیله نشانی مخاطب متصل می‌شوید؟</string> + <string name="connect_via_invitation_link">به وسیله لینک یک بار مصرف متصل می‌شوید؟</string> + <string name="connect_use_new_incognito_profile">از نمایه ناشناس جدید استفاده کن</string> + <string name="opening_database">در حال گشودن پایگاه داده…</string> + <string name="profile_will_be_sent_to_contact_sending_link">نمایه شما به مخاطبی که این لینک را از او دریافت کردید، فرستاده خواهد شد.</string> + <string name="connect_via_link_incognito">متصل شدن به صورت ناشناس</string> + <string name="non_content_uri_alert_text">شما یک مسیر نامعتبر پرونده به اشتراک گذاشتید. موضوع را به توسعه‌دهندگان برنامه گزارش دهید.</string> + <string name="receiving_files_not_yet_supported">هنوز از دریافت پرونده پشتیبانی نمی‌شود</string> + <string name="invalid_message_format">قالب پیام نامعتبر</string> + <string name="moderated_description">حذف شده</string> + <string name="error_showing_message">خطا در نمایش پیام</string> + <string name="trying_to_connect_to_server_to_receive_messages">در حال تلاش برای اتصال به سرور مورد استفاده برای دریافت پیام‌ها از این مخاطب.</string> + <string name="marked_deleted_items_description">%d پیام به عنوان حذف شده علامت گذاشته شد</string> + <string name="connected_to_server_to_receive_messages_from_contact">به سرور مورد استفاده برای دریافت پیام‌ها از این مخاطب متصل شده‌اید.</string> + <string name="server_connected">متصل</string> + <string name="server_error">خطا</string> + <string name="server_connecting">در حال اتصال</string> + <string name="thousand_abbreviation">k</string> + <string name="connect_via_group_link">به گروه می‌پیوندید؟</string> + <string name="connect_use_current_profile">از نمایه کنونی استفاده کن</string> + <string name="you_will_join_group">به تمام اعضای گروه متصل خواهید شد.</string> + <string name="connect_via_link_verb">متصل شدن</string> + <string name="non_content_uri_alert_title">مسیر نامعتبر پرونده</string> + <string name="app_was_crashed">برنامه از کار افتاد</string> + <string name="trying_to_connect_to_server_to_receive_messages_with_error">در حال تلاش برای اتصال به سرور مورد استفاده برای دریافت پیام‌ها از این مخاطب (خطا: %1$s).</string> + <string name="deleted_description">حذف شد</string> + <string name="marked_deleted_description">علامت گذاشته شده به عنوان حذف شده</string> + <string name="moderated_item_description">توسط %s حذف شد</string> + <string name="blocked_item_description">مسدود</string> + <string name="blocked_items_description">%d پیام مسدود شده</string> + <string name="sending_files_not_yet_supported">هنوز از ارسال پرونده پشتیبانی نمی‌شود</string> + <string name="sender_you_pronoun">شما</string> + <string name="unknown_message_format">قالب پیام ناشناخته</string> + <string name="live">زنده</string> + <string name="invalid_chat">گپ نامعتبر</string> + <string name="invalid_data">داده نامعتبر</string> + <string name="error_showing_content">خطا در نمایش محتوا</string> + <string name="decryption_error">خطا در رمزگشایی</string> + <string name="send_disappearing_message_5_minutes">۵ دقیقه</string> + <string name="learn_more_about_address">درباره نشانی سیمپل‌اکس(SimpleX)</string> + <string name="one_time_link_short">لینک یک بار مصرف</string> + <string name="about_simplex_chat">درباره سیمپل‌اکس چت(SimpleX Chat)</string> + <string name="alert_text_decryption_error_too_many_skipped">%1$d پیام از قلم افتاد.</string> + <string name="chat_item_ttl_month">۱ ماه</string> + <string name="chat_item_ttl_week">۱ هفته</string> + <string name="v5_3_new_interface_languages">۶ زبان جدید برای رابط کاربری</string> + <string name="send_disappearing_message_30_seconds">۳۰ ثانیه</string> + <string name="description_you_shared_one_time_link_incognito">شما لینک یک بار مصرف ناشناس به اشتراک گذاشتید</string> + <string name="description_via_group_link">به وسیله لینک گروه</string> + <string name="description_via_group_link_incognito">ناشناس به وسیله لینک گروه</string> + <string name="description_via_contact_address_link">به وسیله لینک نشانی مخاطب</string> + <string name="description_via_one_time_link_incognito">ناشناس به وسیله لینک یک بار مصرف</string> + <string name="simplex_link_mode_description">توصیف</string> + <string name="simplex_link_mode_full">لینک کامل</string> + <string name="simplex_link_mode">لینک‌های SimpleX</string> + <string name="error_saving_smp_servers">خطا در ذخیره کردن سرورهای SMP</string> + <string name="error_saving_xftp_servers">خطا در ذخیره کردن سرورهای XFTP</string> + <string name="encryption_renegotiation_error">خطا در مذاکره مجدد رمزگذاری</string> + <string name="connection_local_display_name">اتصال %1$d</string> + <string name="display_name_connection_established">اتصال برقرار شد</string> + <string name="description_you_shared_one_time_link">شما لینک یک بار مصرف به اشتراک گذاشتید</string> + <string name="display_name_invited_to_connect">برای اتصال دعوت شده</string> + <string name="description_via_contact_address_link_incognito">ناشناس به وسیله لینک نشانی مخاطب</string> + <string name="description_via_one_time_link">به وسیله لینک یک بار مصرف</string> + <string name="simplex_link_group">لینک گروه SimpleX</string> + <string name="simplex_link_connection">به وسیله %1$s</string> + <string name="simplex_link_mode_browser">به وسیله مرورگر</string> + <string name="simplex_link_contact">نشانی مخاطب SimpleX</string> + <string name="simplex_link_invitation">دعوت یک بار مصرف SimpleX</string> + <string name="display_name_connecting">در حال اتصال…</string> + <string name="simplex_link_mode_browser_warning">باز کردن لینک در مرورگر ممکن است حریم خصوصی و امنیت اتصال را کاهش دهد. لینک‌های SimpleX ناموثق قرمز خواهند بود.</string> + <string name="error_loading_xftp_servers">خطا در بارگیری سرورهای XFTP</string> + <string name="failed_to_create_user_title">خطا در ایجاد نمایه!</string> + <string name="failed_to_active_user_title">خطا در تعویض نمایه!</string> + <string name="connection_timeout">توقف اتصال</string> + <string name="connection_error">خطا در اتصال</string> + <string name="error_sending_message">خطا در ارسال پیام</string> + <string name="error_creating_message">خطا در ایجاد پیام</string> + <string name="error_loading_details">خطا در بارگیری جزئیات</string> + <string name="error_joining_group">خطا در پیوستن به گروه</string> + <string name="sender_cancelled_file_transfer">فرستنده انتقال پرونده را لغو کرد.</string> + <string name="you_are_already_connected_to_vName_via_this_link">شما از قبل به %1$s متصل هستید.</string> + <string name="connection_error_auth">خطا در اتصال (تصدیق)</string> + <string name="error_loading_smp_servers">خطا در بارگیری سرورهای SMP</string> + <string name="failed_to_parse_chats_title">عدم موفقیت در بارگیری گپ‌ها</string> + <string name="contact_developers">لطفا برنامه را به‌روزرسانی کنید و با توسعه‌دهندگان تماس بگیرید.</string> + <string name="failed_to_create_user_duplicate_title">نام نمایشی همسان!</string> + <string name="failed_to_create_user_invalid_desc">نام نمایشی نامعتبر است. لطفا نام دیگری انتخاب کنید.</string> + <string name="error_adding_members">خطا در افزودن اعضا</string> + <string name="cannot_receive_file">امکان دریافت پرونده وجود ندارد</string> + <string name="network_error_desc">لطفا اتصال خود را با %1$s بررسی کنید و دوباره امتحان کنید.</string> + <string name="error_receiving_file">خطا در دریافت پرونده</string> + <string name="contact_already_exists">مخاطب از قبل وجود دارد</string> + <string name="invalid_connection_link">لینک اتصال نامعتبر</string> + <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">لطفا بررسی کنید که از لینک صحیح استفاده کردید یا از مخاطبتان بخواهید لینک دیگری برایتان بفرستد.</string> + <string name="ensure_smp_server_address_are_correct_format_and_unique">مطمئن شوید قالب نشانی‌های سرور SMP صحیح است، در خط‌های جدا نوشته شده و تکرار نشده‌اند.</string> + <string name="error_setting_network_config">خطا در به‌روزرسانی پیکربندی شبکه</string> + <string name="failed_to_parse_chat_title">عدم موفقیت در بارگیری گپ</string> + <string name="failed_to_create_user_invalid_title">نام نمایشی نامعتبر!</string> + <string name="failed_to_create_user_duplicate_desc">شما یک نمایه گپ با نام نمایشی یکسان دارید، لطفا نام دیگری انتخاب کنید.</string> + <string name="error_creating_address">خطا در ایجاد نشانی</string> + <string name="ensure_xftp_server_address_are_correct_format_and_unique">مطمئن شوید قالب نشانی‌های سرور XFTP صحیح است، در خط‌های جدا نوشته شده و تکرار نشده‌اند.</string> + <string name="error_accepting_contact_request">خطا در پذیرش درخواست مخاطب</string> + <string name="sender_may_have_deleted_the_connection_request">فرستنده ممکن است درخواست اتصال را حذف کرده باشد.</string> + <string name="error_deleting_note_folder">خطا در حذف یادداشت‌های خصوصی</string> + <string name="error_aborting_address_change">خطا در لغو تغییر نشانی</string> + <string name="error_synchronizing_connection">خطا در انطباق زمانی اتصال</string> + <string name="error_smp_test_failed_at_step">آزمایش در گام %s ناموفق بود.</string> + <string name="error_xftp_test_server_auth">سرور برای بارگذازی به اجازه نیاز دارد، گذرواژه را بررسی کنید</string> + <string name="error_smp_test_certificate">احتمال دارد اثر انگشت گواهینامه در نشانی سرور نادرست باشد</string> + <string name="smp_server_test_create_queue">ایجاد صف</string> + <string name="smp_server_test_upload_file">بارگذاری پرونده</string> + <string name="smp_server_test_download_file">بارگیری پرونده</string> + <string name="smp_server_test_compare_file">مقایسه پرونده</string> + <string name="smp_server_test_delete_file">حذف پرونده</string> + <string name="error_deleting_user">خطا در حذف نمایه کاربر</string> + <string name="error_updating_user_privacy">خطا در به‌روزرسانی حریم خصوصی کاربر</string> + <string name="error_deleting_contact">خطا در حذف مخاطب</string> + <string name="error_deleting_group">خطا در حذف گروه</string> + <string name="error_deleting_contact_request">خطا در حذف درخواست مخاطب</string> + <string name="error_deleting_pending_contact_connection">خطا در حذف اتصال معلق مخاطب</string> + <string name="error_changing_address">خطا در تغییر نشانی</string> + <string name="error_setting_address">خطا در تنظیم نشانی</string> + <string name="error_alert_title">خطا</string> + <string name="smp_server_test_connect">اتصال</string> + <string name="smp_server_test_disconnect">قطع اتصال</string> + <string name="smp_server_test_secure_queue">ایمن‌سازی صف</string> + <string name="smp_server_test_delete_queue">حذف صف</string> + <string name="smp_server_test_create_file">ایجاد پرونده</string> + <string name="error_smp_test_server_auth">سرور برای ایجاد صف داده‌ها به اجازه نیاز دارد، گذرواژه را بررسی کنید</string> + <string name="connection_error_auth_desc">مگر اینکه مخاطبتان اتصال را حذف کرده یا این لینک قبلا استفاده شده باشد، ممکن است این یک اشکال باشد - لطفا آن را گزارش دهید. +\nبرای متصل شدن، لطفا از مخاطبتان بخواهید لینک اتصال دیگری ایجاد کند و بررسی کنید که اتصال شبکه باثباتی دارید.</string> + <string name="possible_slow_function_title">عملکرد کند</string> + <string name="possible_slow_function_desc">اجرای این عملکرد زمان زیادی می‌گیرد: %1$d ثانیه: %2$s</string> + <string name="icon_descr_instant_notifications">اعلان‌های آنی</string> + <string name="service_notifications_disabled">اعلان‌های آنی غیرفعال شده‌اند!</string> + <string name="service_notifications">اعلان‌های آنی!</string> + <string name="note_folder_local_display_name">یادداشت‌های خصوصی</string> + <string name="blocked_by_admin_item_description">مسدود شده توسط مدیر</string> + <string name="blocked_by_admin_items_description">%d پیام توسط مدیر مسدود شده</string> + <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>به وسیله تنظیمات می‌تواند غیرفعال شود</b> – اعلان‌ها تا زمانی که برنامه در حال اجراست، همچنان نمایش داده می‌شوند.]]></string> + <string name="database_migration_in_progress">جابه‌جایی پایگاه داده در حال جریان است. +\nممکن است دقایقی زمان ببرد.</string> + <string name="turn_off_battery_optimization"><![CDATA[برای استفاده از آن، لطفا در دیالوگ بعدی<b>به SimpleX اجازه دهید در پس‌زمینه اجرا شود</b>. در غیر این صورت، اعلان‌ها غیرفعال خواهند شد.]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[برای حفاظت از حریم خصوصیتان، به جای اعلان‌های رانشی، برنامه یک <b>سرویس پس‌زمنیه SimpleX</b> دارد – سرویس، هر روز درصد معدودی از باتری را استفاده می‌کند.]]></string> + <string name="saved_description">ذخیره شده</string> + <string name="saved_from_description">ذخیره شده از %s</string> + <string name="saved_from_chat_item_info_title">ذخیره شده از</string> + <string name="forwarded_description">فرستاده شده</string> + <string name="e2ee_info_no_pq"><![CDATA[پیام‌ها، پرونده‌ها و تماس‌ها به وسیله <b>رمزگذاری سرتاسر</b> با محرمانگی پیشرو، مردودسازی و بازیابی ورود غیرمجاز محافظت شده‌اند.]]></string> + <string name="turning_off_service_and_periodic">بهینه‌سازی باتری فعال است، سرویس پس‌زمینه و درخواست‌های متناوب برای پیام‌های جدید خاموش می‌شوند. می‌توانید آن‌ها را از طریق تنظیمات باز فعال کنید.</string> + <string name="periodic_notifications">اعلان‌های متناوب</string> + <string name="periodic_notifications_disabled">اعلان‌های متناوب غیرفعالند!</string> + <string name="e2ee_info_pq"><![CDATA[پیام‌ها، پرونده‌ها و تماس‌ها به وسیله <b>رمزگذاری سرتاسر مقاوم در برابر کوانتوم</b> با محرمانگی پیشرو، مردودسازی و بازیابی ورود غیرمجاز محافظت شده‌اند.]]></string> + <string name="e2ee_info_no_pq_short">این گپ به وسیله رمزگذاری سرتاسر محافظت شده است.</string> + <string name="e2ee_info_pq_short">این گپ به وسیله رمزگذاری سرتاسر مقاوم در برابر کوانتوم محافظت شده است.</string> + <string name="disable_notifications_button">غیرفعال کردن اعلان‌ها</string> + <string name="system_restricted_background_desc">SimpleX نمی‌تواند در پس‌زمینه اجرا شود. فقط وقتی برنامه در حال اجراست اعلان‌ها را دریافت خواهید کرد.</string> + <string name="periodic_notifications_desc">برنامه پیام‌های جدید را به طور متناوب دریافت می‌کند - درصد معدودی از باتری در روز استفاده می‌کند. برنامه از اعلان‌های رانشی استفاده نمی‌کند - داده‌ای از دستگاه شما به سرورها فرستاده نمی‌شود.</string> + <string name="turn_off_battery_optimization_button">اجازه دادن</string> + <string name="turn_off_system_restriction_button">باز کردن تنظیمات برنامه</string> + <string name="settings_notification_preview_mode_title">نمایش پیش‌نما</string> + <string name="notifications_mode_off">وقتی برنامه باز است، اجرا می‌شود</string> + <string name="notifications_mode_periodic_desc">هر ۱۰ دقیقه به مدت ۱ دقیقه پیام‌های جدید را بررسی می‌کند</string> + <string name="notification_preview_mode_contact_desc">فقط نمایش مخاطب</string> + <string name="auth_open_chat_console">گشودن کنسول گپ</string> + <string name="hide_notification">پنهان کردن</string> + <string name="ntf_channel_messages">پیام‌های SimpleX Chat</string> + <string name="ntf_channel_calls">تماس‌های SimpleX Chat</string> + <string name="settings_notifications_mode_title">سرویس اعلان</string> + <string name="notification_preview_mode_contact">نام مخاطب</string> + <string name="notification_preview_mode_hidden">پنهان</string> + <string name="notification_display_mode_hidden_desc">پنهان کردن مخاطب و پیام</string> + <string name="notification_preview_somebody">مخاطب پنهان:</string> + <string name="auth_open_chat_profiles">گشودن نمایه‌های گپ</string> + <string name="notifications_mode_service_desc">سرویس پس‌زمینه همیشه در حال اجراست - اعلان‌ها به محض موجود شدن پیام‌ها به نمایش درمی‌آیند.</string> + <string name="la_notice_title_simplex_lock">قفل SimpleX</string> + <string name="call_service_notification_end_call">پایان تماس</string> + <string name="settings_notification_preview_title">پیش‌نمایش اعلان</string> + <string name="notifications_mode_periodic">به طور متناوب شروع می‌شود</string> + <string name="notifications_mode_service">همیشه روشن</string> + <string name="notifications_mode_off_desc">برنامه فقط وقتی در حال اجراست می‌تواند اعلان‌ها را دریافت کند، هیچ سرویس پس‌زمینه‌ای شروع نخواهد شد</string> + <string name="notification_preview_mode_message">متن پیام</string> + <string name="notification_contact_connected">متصل</string> + <string name="notification_preview_new_message">پیام جدید</string> + <string name="notification_preview_mode_message_desc">نمایش مخاطب و پیام</string> + <string name="notification_new_contact_request">درخواست مخاطب جدید</string> + <string name="auth_stop_chat">توقف گپ</string> + <string name="auth_open_migration_to_another_device">گشودن صفحه جابه‌جایی</string> + <string name="you_can_turn_on_lock">می‌توانید قفل SimpleX را از طریق تنظیمات روشن کنید.</string> + <string name="error_showing_desktop_notification">خطا در نمایش اعلان، با توسعه‌دهندگان تماس بگیرید.</string> + <string name="lock_not_enabled">قفل SimpleX فعال نیست!</string> + <string name="auth_device_authentication_is_disabled_turning_off">تصدیق دستگاه غیرفعال است. قفل SimpleX خاموش می‌شود.</string> + <string name="icon_descr_sent_msg_status_sent">ارسال شده</string> + <string name="icon_descr_context">نماد زمینه</string> + <string name="icon_descr_cancel_image_preview">لغو پیش‌نمایش تصویر</string> + <string name="icon_descr_cancel_file_preview">لغو پیش‌نمایش پرونده</string> + <string name="system_restricted_background_warn"><![CDATA[برای فعال کردن اعلان‌ها، لطفا <b>مصرف باتری برنامه</b> / <b>نامحدود</b> را در تنظیمات برنامه انتخاب کنید.]]></string> + <string name="system_restricted_background_in_call_desc">برنامه ممکن است بعد از ۱ دقیقه در پس‌زمینه بسته شود.</string> + <string name="database_initialization_error_title">مقداردهی اولیه پایگاه داده ممکن نیست</string> + <string name="enter_passphrase_notification_title">عبارت عبور الزامی است</string> + <string name="enter_passphrase_notification_desc">برای دریافت اعلان‌ها، لطفا، عبارت عبور پایگاه داده را وارد کنید</string> + <string name="database_initialization_error_desc">پایگاه داده به درستی کار نمی‌کند. برای آگاهی بیشتر لمس کنید</string> + <string name="simplex_service_notification_title">سرویس SimpleX Chat</string> + <string name="simplex_service_notification_text">در حال دریافت پیام‌ها…</string> + <string name="la_notice_turn_on">روشن کردن</string> + <string name="la_auth_failed">عدم موفقیت تصدیق</string> + <string name="auth_unlock">گشودن قفل</string> + <string name="auth_confirm_credential">اطلاعات ورودتان را تایید کنید</string> + <string name="reply_verb">پاسخ</string> + <string name="edit_history">تاریخچه</string> + <string name="received_message">پیام دریافتی</string> + <string name="moderate_verb">حذف</string> + <string name="expand_verb">بسط دادن</string> + <string name="moderate_message_will_be_marked_warning">پیام برای تمام اعضا به عنوان حذف شده علامت‌گذاری خواهد شد.</string> + <string name="stop_file__confirm">توقف</string> + <string name="stop_rcv_file__message">دریافت پرونده متوقف خواهد شد.</string> + <string name="welcome">خوش آمدید!</string> + <string name="image_decoding_exception_title">خطا در کدبرداری</string> + <string name="compose_send_direct_message_to_connect">ارسال پیام مستقیم برای اتصال</string> + <string name="loading_remote_file_desc">لطفا، تا زمانی که پرونده در حال بارگیری از موبایل متصل است، منتظر باشید.</string> + <string name="button_delete_contact">حذف مخاطب</string> + <string name="view_security_code">مشاهده کد امنیتی</string> + <string name="verify_security_code">تایید کد امنیتی</string> + <string name="send_disappearing_message">ارسال پیام ناپدید شونده</string> + <string name="add_contact">لینک دعوت یک‌بارمصرف</string> + <string name="create_group">ایجاد گروه محرمانه</string> + <string name="to_share_with_your_contact">(برای اشتراک‌گذاری با مخاطبتان)</string> + <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">اگر لینک دعوت SimpleX Chat دریافت کردید، می‌توانید آن را در مرورگر خود باز کنید:</string> + <string name="la_lock_mode_passcode">ورود کد عبور</string> + <string name="la_seconds">%d ثانیه</string> + <string name="la_current_app_passcode">کد عبور فعلی</string> + <string name="copy_verb">کپی</string> + <string name="delete_message_mark_deleted_warning">پیام به عنوان حذف شده علامت‌گذاری خواهد شد. گیرنده‌ها قادر خواهند بود این پیام را آشکار کنند.</string> + <string name="icon_descr_sent_msg_status_send_failed">ارسال غیرموفق</string> + <string name="contact_tap_to_connect">برای اتصال لمس کنید</string> + <string name="video_decoding_exception_desc">امکان کدبرداری ویدئو وجود ندارد. لطفا، ویدئوی دیگری را امتحان کنید یا با توسعه‌دهندگان تماس بگیرید.</string> + <string name="image_descr">تصویر</string> + <string name="file_not_found">پرونده پیدا نشد</string> + <string name="chat_help_tap_button">لمس دکمه</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>ایجاد گروه</b>: برای ایجاد یک گروه جدید.]]></string> + <string name="system_restricted_background_in_call_title">بدون تماس‌های پس‌زمینه</string> + <string name="system_restricted_background_in_call_warn"><![CDATA[برای برقراری تماس‌ها در پس‌زمینه، لطفا <b>مصرف باتری برنامه</b> / <b>نامحدود</b> را در تنظیمات برنامه انتخاب کنید.]]></string> + <string name="la_authenticate">تصدیق</string> + <string name="edit_verb">ویرایش</string> + <string name="no_info_on_delivery">بدون اطلاعات تحویل</string> + <string name="revoke_file__action">لغو پرونده</string> + <string name="group_connection_pending">در حال اتصال…</string> + <string name="you_have_no_chats">شما هیچ گپی ندارید</string> + <string name="loading_chats">بارگذاری گپ‌ها…</string> + <string name="video_will_be_received_when_contact_completes_uploading">ویدئو وقتی دریافت خواهد شد که مخاطبتان بارگذاری آن را تکمیل کند.</string> + <string name="maximum_supported_file_size">بیشترین اندازه پشتیبانی شده فعلی پرونده %1$s است.</string> + <string name="error_saving_file">خطا در ذخیره‌سازی پرونده</string> + <string name="loading_remote_file_title">در حال بارگیری پرونده</string> + <string name="delete_and_notify_contact">حذف و مخاطب را باخبر کن</string> + <string name="icon_descr_server_status_connected">متصل</string> + <string name="sync_connection_force_question">مذاکره مجدد رمزگذاری؟</string> + <string name="you_need_to_allow_to_send_voice">شما نیاز دارید به مخاطبتان اجازه ارسال پیام‌های صوتی دهید تا بتوانید آن‌ها را ارسال کنید.</string> + <string name="send_live_message">ارسال پیام زنده</string> + <string name="disappearing_message">پیام ناپدید شونده</string> + <string name="send_disappearing_message_custom_time">زمان سفارشی</string> + <string name="copied">به حافظه کپی شد</string> + <string name="add_contact_or_create_group">شروع گپ جدید</string> + <string name="from_gallery_button">از گالری</string> + <string name="call_service_notification_video_call">تماس تصویری</string> + <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">برای محافظت از اطلاعاتتان، قفل SimpleX را روشن کنید. +\nاز شما خواسته خواهد شد قبل از فعال شدن این ویژگی، تصدیق را تکمیل کنید.</string> + <string name="la_no_app_password">عدم وجود کد عبور</string> + <string name="la_minutes">%d دقیقه</string> + <string name="auth_enable_simplex_lock">فعال‌سازی قفل SimpleX</string> + <string name="la_enter_app_passcode">وارد کردن کد عبور</string> + <string name="saved_chat_item_info_tab">ذخیره شده</string> + <string name="forwarded_chat_item_info_tab">فرستاده شده</string> + <string name="forwarded_from_chat_item_info_title">فرستاده شده از</string> + <string name="recipients_can_not_see_who_message_from">گیرنده‌ها نمی‌توانند ببینند این پیام از طرف چه کسی است.</string> + <string name="reveal_verb">آشکار کردن</string> + <string name="hide_verb">پنهان کردن</string> + <string name="delete_messages__question">%d پیام حذف شود؟</string> + <string name="delete_message_cannot_be_undone_warning">پیام حذف خواهد شد - این عمل قابل برگشت نیست!</string> + <string name="moderate_message_will_be_deleted_warning">پیام برای تمام اعضا حذف خواهد شد.</string> + <string name="for_me_only">حذف برای من</string> + <string name="forward_chat_item">فرستادن</string> + <string name="download_file">بارگیری</string> + <string name="icon_descr_edited">ویرایش شده</string> + <string name="icon_descr_sent_msg_status_unauthorized_send">ارسال غیرمجاز</string> + <string name="icon_descr_received_msg_status_unread">خوانده نشده</string> + <string name="personal_welcome">خوش آمدید، %1$s!</string> + <string name="this_text_is_available_in_settings">این متن در تنظیمات دردسترس است</string> + <string name="chat_with_developers">گپ با توسعه‌دهندگان</string> + <string name="no_filtered_chats">گپ پالایش شده‌ای نیست</string> + <string name="connect_with_contact_name_question">به %1$s متصل شوید؟</string> + <string name="no_selected_chat">گپی انتخاب نشده</string> + <string name="forward_message">فرستادن پیام…</string> + <string name="videos_limit_title">تعداد ویدئوی بیش از اندازه!</string> + <string name="videos_limit_desc">فقط ۱۰ ویدئو در هر زمان می‌توان ارسال کرد</string> + <string name="observer_cant_send_message_title">شما نمی‌توانید پیامی بفرستید!</string> + <string name="observer_cant_send_message_desc">لطفا با مدیر گروه تماس بگیرید.</string> + <string name="only_owners_can_enable_files_and_media">فقط صاحبان گروه می‌توانند پرونده‌ها و رسانه را فعال کنند.</string> + <string name="icon_descr_asked_to_receive">دریافت تصویر درخواست شده</string> + <string name="waiting_for_image">در انتظار تصویر</string> + <string name="image_saved">تصویر در گالری ذخیره شد</string> + <string name="video_descr">ویدئو</string> + <string name="video_will_be_received_when_contact_is_online">ویدئو وقتی دریافت خواهد شد که مخاطبتان آنلاین شود، لطفا صبر کنید یا بعدا بررسی کنید.</string> + <string name="icon_descr_file">پرونده</string> + <string name="large_file">پرونده حجیم!</string> + <string name="waiting_for_file">در انتظار پرونده</string> + <string name="file_will_be_received_when_contact_completes_uploading">پرونده وقتی دریافت خواهد شد که مخاطبتان بارگذاری آن را تکمیل کند.</string> + <string name="voice_message_with_duration">پیام صوتی (%1$s)</string> + <string name="notifications">اعلان‌ها</string> + <string name="delete_contact_all_messages_deleted_cannot_undo_warning">مخاطب و تمام پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست!</string> + <string name="allow_voice_messages_question">پیام‌های صوتی مجازند؟</string> + <string name="voice_messages_prohibited">پیام‌های صوتی ممنوع هستند!</string> + <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 کامپیوتر: از برنامه کد QR نمایش داده شده را اسکن کنید، از طریق <b>اسکن کد QR</b>.]]></string> + <string name="share_file">اشتراک‌گذاری پرونده…</string> + <string name="images_limit_title">تعداد تصویر بیش از اندازه!</string> + <string name="tap_to_scan">برای اسکن لمس کنید</string> + <string name="choose_file_title">انتخاب پرونده</string> + <string name="to_start_a_new_chat_help_header">برای شروع گپ جدید</string> + <string name="delivery">تحویل</string> + <string name="delete_member_message__question">پیام عضو حذف شود؟</string> + <string name="for_everybody">برای همه</string> + <string name="stop_snd_file__message">ارسال پرونده متوقف خواهد شد.</string> + <string name="stop_rcv_file__title">دریافت پرونده متوقف شود؟</string> + <string name="revoke_file__message">پرونده از سرورها حذف خواهد شد.</string> + <string name="member_contact_send_direct_message">ارسال پیام مستقیم</string> + <string name="share_image">اشتراک‌گذاری رسانه…</string> + <string name="images_limit_desc">فقط ۱۰ تصویر در هر زمان می‌توان ارسال کرد</string> + <string name="call_service_notification_audio_call">تماس صوتی</string> + <string name="la_please_remember_to_store_password">لطفا آن را به خاطر داشته باشید یا به طور امن ذخیره کنید - راهی برای بازیابی کلمه عبور وجود ندارد!</string> + <string name="auth_simplex_lock_turned_on">قفل SimpleX روشن است</string> + <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">وقتی برنامه را شروع می‌کنید یا بعد از ۳۰ ثانیه در پس‌زمینه آن از سر می‌گیرید، نیاز به تصدیق خواهید داشت.</string> + <string name="search_verb">جستجو</string> + <string name="auth_disable_simplex_lock">غیرفعال کردن قفل SimpleX</string> + <string name="share_verb">اشتراک‌گذاری</string> + <string name="save_verb">ذخیره</string> + <string name="send_disappearing_message_send">ارسال</string> + <string name="icon_descr_cancel_live_message">لغو پیام زنده</string> + <string name="back">بازگشت</string> + <string name="cancel_verb">لغو</string> + <string name="ok">موافقت</string> + <string name="live_message">پیام زنده!</string> + <string name="send_live_message_desc">ارسال پیام زنده - همزمان با تایپ کردن برای گیرنده‌ها به‌روز می‌شود</string> + <string name="auth_log_in_using_credential">ورود با استفاده از اطلاعات ورودتان</string> + <string name="auth_unavailable">تصدیق موجود نیست</string> + <string name="message_delivery_error_title">خطا در تحویل پیام</string> + <string name="info_menu">اطلاعات</string> + <string name="sent_message">پیام ارسالی</string> + <string name="message_delivery_error_desc">به احتمال زیاد این مخاطب اتصال با شما را حذف کرده است.</string> + <string name="no_history">بدون تاریخچه</string> + <string name="delete_verb">حذف</string> + <string name="delete_message__question">پیام حذف شود؟</string> + <string name="revoke_file__title">پرونده لغو شود؟</string> + <string name="stop_file__action">توقف پرونده</string> + <string name="stop_snd_file__title">ارسال پرونده متوقف شود؟</string> + <string name="revoke_file__confirm">لغو</string> + <string name="contact_connection_pending">در حال اتصال…</string> + <string name="your_chats">گپ‌ها</string> + <string name="group_preview_you_are_invited">شما به گروه دعوت شده‌اید</string> + <string name="group_preview_join_as">پیوستن به عنوان %s</string> + <string name="search_or_paste_simplex_link">جستجو یا الصاق لینک SimpleX</string> + <string name="tap_to_start_new_chat">برای شروع گپ جدید لمس کنید</string> + <string name="share_message">اشتراک‌گذاری پیام…</string> + <string name="icon_descr_waiting_for_image">در انتظار تصویر</string> + <string name="voice_message_send_text">پیام صوتی…</string> + <string name="text_field_set_contact_placeholder">تعیین نام مخاطب…</string> + <string name="icon_descr_server_status_disconnected">قطع شد</string> + <string name="icon_descr_server_status_pending">در حال انتظار</string> + <string name="switch_receiving_address_question">نشانی‌ دریافت تغییر کند؟</string> + <string name="abort_switch_receiving_address_desc">تغییر نشانی‌ لغو خواهد شد. نشانی‌ دریافت پیشین استفاده خواهد شد.</string> + <string name="sync_connection_force_confirm">مذاکره مجدد</string> + <string name="only_group_owners_can_enable_voice">فقط صاحبان گروه می‌توانند پیام‌های صوتی را فعال کنند.</string> + <string name="confirm_verb">تایید</string> + <string name="reset_verb">بازنشاندن</string> + <string name="scan_QR_code">اسکن کد QR</string> + <string name="connect_via_link_or_qr_from_clipboard_or_in_person">(اسکن یا الصاق از حافظه)</string> + <string name="only_stored_on_members_devices">(تنها ذخیره شده توسط اعضای گروه)</string> + <string name="enable_camera_access">فعال کردن دسترسی دوربین</string> + <string name="toast_permission_denied">اجازه داده نشد!</string> + <string name="use_camera_button">دوربین</string> + <string name="thank_you_for_installing_simplex">سپاس برای نصب SimpleX Chat!</string> + <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 موبایل: <b>گشودن در موبایل</b> را لمس کنید، سپس <b>اتصال</b> را در برنامه لمس کنید.]]></string> + <string name="accept_connection_request__question">درخواست اتصال پذیرفته شود؟</string> + <string name="voice_messages_not_allowed">پیام‌های صوتی مجاز نیستند</string> + <string name="simplex_links_not_allowed">لینک‌های SimpleX مجاز نیستند</string> + <string name="files_and_media_not_allowed">پرونده‌ها و رسانه مجاز نیست</string> + <string name="icon_descr_waiting_for_video">در انتظار ویدئو</string> + <string name="icon_descr_video_asked_to_receive">دریافت ویدئو درخواست شده</string> + <string name="icon_descr_video_snd_complete">ویدئو ارسال شد</string> + <string name="image_will_be_received_when_contact_completes_uploading">تصویر وقتی دریافت خواهد شد که مخاطبتان بارگذاری آن را تکمیل کند.</string> + <string name="voice_message">پیام صوتی</string> + <string name="icon_descr_record_voice_message">ضبط پیام صوتی</string> + <string name="icon_descr_send_message">ارسال پیام</string> + <string name="no_details">بدون جزئیات</string> + <string name="connect_via_link_or_qr">اتصال به وسیله لینک / کد QR</string> + <string name="camera_not_available">دوربین موجود نیست</string> + <string name="gallery_image_button">تصویر</string> + <string name="share_one_time_link">ایجاد لینک دعوت یک‌بارمصرف</string> + <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[می‌توانید <font color="#0088ff">برای پرسش هر سوال و دریافت اطلاعات به توسعه‌دهندگان SimpleX Chat متصل شوید</font>.]]></string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>افزودن مخاطب</b>: برای ایجاد لینک دعوت جدید، یا اتصال از طریق لینکی که دریافت کردید.]]></string> + <string name="to_connect_via_link_title">اتصال از طریق لینک</string> + <string name="image_will_be_received_when_contact_is_online">تصویر وقتی دریافت خواهد شد که مخاطبتان آنلاین شود، لطفا صبر کنید یا بعدا بررسی کنید!</string> + <string name="la_lock_mode">نوع قفل SimpleX</string> + <string name="la_lock_mode_system">تصدیق سیستم</string> + <string name="la_change_app_passcode">تغییر کد عبور</string> + <string name="in_reply_to">در پاسخ به</string> + <string name="la_immediately">فوری</string> + <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">تصدیق دستگاه فعال نیست. زمانی که تصدیق دستگاه را فعال کنید می‌توانید قفل SimpleX را از طریق تنظیمات روشن کنید.</string> + <string name="allow_verb">اجازه دادن</string> + <string name="attach">ضمیمه</string> + <string name="image_decoding_exception_desc">امکان کدبرداری تصویر وجود ندارد. لطفا، تصویر دیگری را امتحان کنید یا با توسعه‌دهندگان تماس بگیرید.</string> + <string name="files_and_media_prohibited">پرونده‌ها و رسانه ممنوع است!</string> + <string name="icon_descr_image_snd_complete">تصویر ارسال شد</string> + <string name="waiting_for_video">در انتظار ویدئو</string> + <string name="contact_sent_large_file">مخاطبتان پرونده‌ای ارسال کرد که از بیشترین اندازه پشتیبانی شده (%1$s) بزرگتر است.</string> + <string name="file_will_be_received_when_contact_is_online">پرونده وقتی دریافت خواهد شد که مخاطبتان آنلاین شود، لطفا صبر کنید یا بعدا بررسی کنید.</string> + <string name="file_saved">پرونده ذخیره شد</string> + <string name="sync_connection_force_desc">رمزگذاری در حال کار است و نیازی به توافق رمزگذاری جدید نیست. ممکن است باعث بروز خطاهای اتصال شود!</string> + <string name="ask_your_contact_to_enable_voice">لطفا از مخاطبتان بخواهید ارسال پیام‌های صوتی را فعال کند.</string> + <string name="send_verb">ارسال</string> + <string name="gallery_video_button">ویدئو</string> + <string name="above_then_preposition_continuation">بالا، سپس:</string> + <string name="delete_contact_question">مخاطب حذف شود؟</string> + <string name="icon_descr_server_status_error">خطا</string> + <string name="switch_receiving_address_desc">آدرس دریافت به سرور متفاوتی تغییر خواهد یافت. تغییر نشانی وقتی تکمیل خواهد شد که فرستنده آنلاین شود.</string> + <string name="la_could_not_be_verified">تایید شما ممکن نیست؛ لطفا دوباره امتحان کنید.</string> + <string name="choose_file">پرونده</string> + <string name="you_are_observer">شما ناظر هستید</string> + <string name="clear_chat_question">گپ پاک شود؟</string> + <string name="clear_note_folder_warning">تمام پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست!</string> + <string name="delete_contact_menu_action">حذف</string> + <string name="accept_contact_incognito_button">پذیرش ناشناس</string> + <string name="favorite_chat">برگزیده</string> + <string name="accept_contact_button">پذیرفتن</string> + <string name="reject_contact_button">رد کردن</string> + <string name="clear_note_folder_question">یادداشت‌های خصوصی پاک شود؟</string> + <string name="clear_verb">پاک کردن</string> + <string name="clear_chat_menu_action">پاک‌سازی</string> + <string name="delete_group_menu_action">حذف</string> + <string name="mark_unread">علامت‌گذاری به عنوان خوانده نشده</string> + <string name="set_contact_name">تعیین نام مخاطب</string> + <string name="contact_you_shared_link_with_wont_be_able_to_connect">مخاطبی که این لینک را با او به اشتراک گذاشتید قادر به اتصال نخواهد بود!</string> + <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">اگر نپذیرید، فرستنده باخبر نخواهد شد.</string> + <string name="clear_chat_warning">تمام پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست! پیام‌ها فقط برای شما حذف خواهند شد.</string> + <string name="clear_chat_button">پاک‌سازی گپ</string> + <string name="you_accepted_connection">اتصال را پذیرفتید</string> + <string name="alert_title_contact_connection_pending">مخاطب هنوز متصل نشده است!</string> + <string name="you_invited_a_contact">شما مخاطبی را دعوت کردید</string> + <string name="delete_pending_connection__question">اتصال در حال انتظار حذف شود؟</string> + <string name="connection_you_accepted_will_be_cancelled">اتصالی که پذیرفتید لغو خواهد شد!</string> + <string name="mute_chat">بی‌صدا</string> + <string name="unmute_chat">لغو بی‌صدا</string> + <string name="mark_read">علامت‌گذاری به عنوان خوانده شده</string> + <string name="unfavorite_chat">خلع برگزیده</string> + <string name="icon_descr_simplex_team">تیم SimpleX</string> + <string name="new_chat">گپ جدید</string> + <string name="how_to_use_simplex_chat">روش استفاده</string> + <string name="markdown_help">راهنمای مارکداون</string> + <string name="invalid_contact_link">لینک نامعتبر!</string> + <string name="share_invitation_link">اشتراک‌گذاری لینک یک بار مصرف</string> + <string name="or_scan_qr_code">یا کد QR را اسکن کنید</string> + <string name="retry_verb">تلاش مجدد</string> + <string name="database_passphrase_and_export">عبارت عبور و صدور پایگاه داده</string> + <string name="smp_servers_preset_add">افزودن سرورهای از پیش تنظیم شده</string> + <string name="smp_servers_add_to_another_device">افزودن به دستگاه دیگر</string> + <string name="smp_servers_delete_server">حذف سرور</string> + <string name="how_to_use_your_servers">چگونه از سرورهای خود استفاده کنید</string> + <string name="image_descr_link_preview">تصویر پیش‌نمایش لینک</string> + <string name="your_chat_profiles">نمایه‌های گپ شما</string> + <string name="smp_servers">سرورهای SMP</string> + <string name="install_simplex_chat_for_terminal">SimpleX Chat را برای ترمینال نصب کنید</string> + <string name="star_on_github">در GitHub ستاره بزنید</string> + <string name="contribute">همکاری کنید</string> + <string name="image_descr_qr_code">کد QR</string> + <string name="connect__a_new_random_profile_will_be_shared">یک نمایه تصادفی جدید به اشتراک گذاشته خواهد شد.</string> + <string name="connect_via_link">اتصال از طریق لینک</string> + <string name="one_time_link">لینک دعوت یک‌بارمصرف</string> + <string name="is_verified">%s تایید شده است</string> + <string name="create_chat_profile">ایجاد نمایه گپ</string> + <string name="chat_console">کنسول گپ</string> + <string name="smp_servers_test_server">آزمایش سرور</string> + <string name="smp_servers_enter_manually">وارد کردن دستی سرور</string> + <string name="enter_one_ICE_server_per_line">سرورهای ICE (یکی در هر خط)</string> + <string name="error_saving_ICE_servers">خطا در ذخیره کردن سرورهای ICE</string> + <string name="icon_descr_profile_image_placeholder">جایگزین تصویر نمایه</string> + <string name="icon_descr_close_button">دکمه بستن</string> + <string name="icon_descr_cancel_link_preview">لغو پیش‌نمایش لینک</string> + <string name="icon_descr_settings">تنظیمات</string> + <string name="icon_descr_help">کمک</string> + <string name="image_descr_simplex_logo">لوگوی SimpleX</string> + <string name="icon_descr_email">ایمیل</string> + <string name="this_link_is_not_a_valid_connection_link">این لینک، یک لینک اتصال معتبر نیست!</string> + <string name="connection_request_sent">درخواست اتصال ارسال شد!</string> + <string name="you_will_be_connected_when_group_host_device_is_online">وقتی دستگاه میزبان گروه آنلاین شد، به گروه متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید!</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">لینکی که دریافت کردید را الصاق کنید تا به مخاطبتان متصل شوید…</string> + <string name="connect__your_profile_will_be_shared">نمایه شما %1$s به اشتراک گذاشته خواهد شد.</string> + <string name="scan_qr_to_connect_to_contact">برای اتصال، مخاطبتان می‌تواند کد QR را اسکن یا از لینک در برنامه استفاده کند.</string> + <string name="if_you_cant_meet_in_person">اگر نمی‌توانید ملاقات حضوری داشته باشید، کد QR را در یک تماس تصویری نمایش دهید، یا لینک را به اشتراک بگذارید.</string> + <string name="you_can_share_your_address">می‌توانید نشانی خود را به صورت لینک یا کد QR به اشتراک بگذارید - هر کسی می‌تواند به شما متصل شود.</string> + <string name="you_can_accept_or_reject_connection">وقتی اشخاص درخواست اتصال کنند، شما می‌توانید آن را بپذیرید یا رد کنید.</string> + <string name="or_show_this_qr_code">یا این کد را نشان دهید</string> + <string name="you_can_view_invitation_link_again">می‌توانید دوباره لینک دعوت را در جزئیات اتصال مشاهده کنید.</string> + <string name="keep_invitation_link">نگه‌داشتن</string> + <string name="creating_link">در حال ایجاد لینک…</string> + <string name="paste_the_link_you_received">لینکی که دریافت کردید را الصاق کنید</string> + <string name="the_text_you_pasted_is_not_a_link">متن الصاقی شما یک لینک SimpleX نیست.</string> + <string name="smp_servers_your_server">سرور شما</string> + <string name="smp_servers_preset_server">سرور از پیش تنظیم شده</string> + <string name="smp_servers_invalid_address">نشانی‌ سرور نامعتبر!</string> + <string name="smp_servers_use_server_for_new_conn">به‌کارگیری برای اتصال‌های جدید</string> + <string name="invalid_qr_code">کد QR نامعتبر</string> + <string name="incorrect_code">کد امنیتی نادرست!</string> + <string name="scan_code_from_contacts_app">کد امنیتی را از برنامه مخاطبتان اسکن کنید.</string> + <string name="mark_code_verified">علامت‌گذاری به عنوان تایید شده</string> + <string name="is_not_verified">%s تایید نشده است</string> + <string name="to_verify_compare">برای تایید رمزگذاری سرتاسر، روی دستگاه‌های خود، کد را با مخاطبتان مقایسه(یا اسکن) کنید.</string> + <string name="your_XFTP_servers">سرورهای XFTP شما</string> + <string name="how_to">روش استفاده</string> + <string name="using_simplex_chat_servers">در حال استفاده از سرورهای SimpleX Chat.</string> + <string name="configure_ICE_servers">تنظیم سرورهای ICE</string> + <string name="contact_wants_to_connect_with_you">می‌خواهد به شما متصل شود!</string> + <string name="you_will_be_connected_when_your_contacts_device_is_online">وقتی دستگاه مخاطبتان آنلاین شد، متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید!</string> + <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[اگر نمی‌توانید ملاقات حضوری داشته باشید، <b>کد QR را در تماس تصویری اسکن کنید</b>، یا مخاطبتان می‌تواند یک لینک دعوت به اشتراک بگذارد.]]></string> + <string name="keep_unused_invitation_question">دعوت استعمال نشده نگه داشته شود؟</string> + <string name="your_simplex_contact_address">نشانی‌ SimpleX شما</string> + <string name="markdown_in_messages">مارکداون در پیام‌ها</string> + <string name="chat_with_the_founder">ایده‌ها و سوالات را ارسال کنید</string> + <string name="send_us_an_email">به ما ایمیل بفرستید</string> + <string name="chat_lock">قفل SimpleX</string> + <string name="smp_servers_add">افزودن سرور</string> + <string name="smp_servers_test_servers">آزمایش سرورها</string> + <string name="smp_servers_save">ذخیره سرورها</string> + <string name="smp_servers_test_failed">عدم موفقیت آزمایش سرور!</string> + <string name="smp_servers_test_some_failed">عدم موفقیت آزمایش چند سرور:</string> + <string name="smp_servers_scan_qr">اسکن کد QR سرور</string> + <string name="your_SMP_servers">سرورهای SMP شما</string> + <string name="xftp_servers">سرورهای XFTP</string> + <string name="use_simplex_chat_servers__question">از سرورهای SimpleX Chat استفاده شود؟</string> + <string name="saved_ICE_servers_will_be_removed">سرورهای WebRTC ICE ذخیره شده حذف خواهند شد.</string> + <string name="your_ICE_servers">سرورهای ICE شما</string> + <string name="connect_button">اتصال</string> + <string name="paste_button">الصاق</string> + <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[شما همچنین می‌توانید با کلیک روی لینک متصل شوید. اگر در مرورگر باز شد، روی دکمه <b>گشودن در برنامه موبایل</b> کلیک کنید.]]></string> + <string name="add_contact_tab">افزودن مخاطب</string> + <string name="simplex_address">نشانی‌ SimpleX</string> + <string name="clear_verification">پاک‌سازی تایید</string> + <string name="smp_servers_preset_address">نشانی‌ سرور از پیش تنظیم شده</string> + <string name="security_code">کد امنیتی</string> + <string name="smp_servers_use_server">به‌کارگیری از سرور</string> + <string name="smp_servers_per_user">سرورها برای اتصال‌های جدید نمایه گپ فعلی شما</string> + <string name="smp_save_servers_question">سرورها ذخیره شوند؟</string> + <string name="rate_the_app">به برنامه امتیاز بدهید</string> + <string name="smp_servers_your_server_address">نشانی‌ سرور شما</string> + <string name="smp_servers_check_address">نشانی‌ سرور را بررسی و دوباره امتحان کنید.</string> + <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">لازم است مخاطبتان آنلاین باشد تا اتصال کامل شود. +\nمی‌توانید این اتصال را لغو و مخاطب را حذف کنید (و بعدا با یک لینک جدید امتحان کنید).</string> + <string name="icon_descr_address">نشانی‌ SimpleX</string> + <string name="this_QR_code_is_not_a_link">این کد QR یک لینک نیست!</string> + <string name="read_more_in_user_guide_with_link"><![CDATA[مطالعه بیشتر در <font color="#0088ff">راهنمای کاربر</font>.]]></string> + <string name="this_string_is_not_a_connection_link">این رشته متن، یک لینک اتصال نیست!</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">کدی که اسکن کردید یک کد QR لینک SimpleX نیست.</string> + <string name="scan_code">اسکن کد</string> + <string name="image_descr_profile_image">تصویر نمایه</string> + <string name="icon_descr_more_button">بیشتر</string> + <string name="show_QR_code">نمایش کد QR</string> + <string name="invalid_QR_code">کد QR نامعتبر</string> + <string name="you_will_be_connected_when_your_connection_request_is_accepted">وقتی درخواست اتصال شما پذیرفته شد، متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید!</string> + <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[اگر نمی‌توانید ملاقات حضوری داشته باشید، <b>کد QR را در تماس تصویری نمایش دهید</b>، یا لینک را به اشتراک بگذارید.]]></string> + <string name="your_chat_profile_will_be_sent_to_your_contact">نمایه گپ شما ارسال خواهد شد +\nبه مخاطبتان</string> + <string name="learn_more">اطلاعات بیشتر</string> + <string name="you_wont_lose_your_contacts_if_delete_address">اگر بعدا نشانی‌ خود را حذف کنید، مخاطبان خود را از دست نخواهید داد.</string> + <string name="share_this_1_time_link">این لینک دعوت یک‌بارمصرف را به اشتراک بگذارید</string> + <string name="tap_to_paste_link">برای الصاق لینک لمس کنید</string> + <string name="your_settings">تنظیمات شما</string> + <string name="network_use_onion_hosts_required_desc">میزبان‌های Onion برای اتصال الزامی خواهد بود. +\nلطفا توجه داشته باشید: شما بدون نشانی‌ onion. قادر نخواهید بود به سرورها متصل شوید.</string> + <string name="save_servers_button">ذخیره</string> + <string name="port_verb">پورت</string> + <string name="network_enable_socks">از پروکسی SOCKS استفاده شود؟</string> + <string name="network_disable_socks">از اتصال مستقیم اینترنت استفاده شود؟</string> + <string name="network_settings">تنظیمات پیشرفته شبکه</string> + <string name="network_socks_proxy_settings">تنظیمات پروکسی SOCKS</string> + <string name="host_verb">هاست</string> + <string name="network_use_onion_hosts_required">الزامی</string> + <string name="network_use_onion_hosts_prefer_desc">از میزبان‌های Onion وقتی موجود باشند استفاده خواهد شد.</string> + <string name="appearance_settings">ظاهر</string> + <string name="app_version_name">نسخه برنامه: v%s</string> + <string name="show_developer_options">نمایش گزینه‌های توسعه‌دهنده</string> + <string name="developer_options">شناسه‌های پایگاه داده و گزینه انزوای ترابری</string> + <string name="your_contacts_will_remain_connected">مخاطبانتان متصل باقی خواهند ماند.</string> + <string name="share_link">اشتراک‌گذاری لینک</string> + <string name="network_enable_socks_info">دسترسی به سرورها از طریق پروکسی SOCKS روی پورت %d؟ پروکسی باید قبل از فعال کردن این گزینه، راه اندازی شده باشد.</string> + <string name="network_session_mode_user_description"><![CDATA[یک اتصال جدای TCP (و اطلاعات ورود SOCKS) <b>برای هر نمایه گپی که در برنامه دارید</b> استفاده خواهد شد.]]></string> + <string name="show_internal_errors">نمایش خطاهای داخلی</string> + <string name="show_slow_api_calls">نمایش تماس‌های کند API</string> + <string name="network_use_onion_hosts_no">خیر</string> + <string name="disable_onion_hosts_when_not_supported"><![CDATA[<i>استفاده از میزبان‌های onion.</i> را روی «خیر» تنظیم کنید اگر پروکسی SOCKS از آنها پشتیبانی نمی‌کند.]]></string> + <string name="customize_theme_title">سفارشی کردن تم</string> + <string name="app_version_title">نسخه برنامه</string> + <string name="theme_colors_section_title">رنگ‌های رابط کاربری</string> + <string name="core_version">نسخه هسته: v%s</string> + <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> + <string name="shutdown_alert_desc">اعلان‌ها از کار خواهند افتاد تا زمانی که برنامه را دوباره راه‌اندازی کنید</string> + <string name="create_address">ایجاد نشانی</string> + <string name="delete_address__question">نشانی حذف شود؟</string> + <string name="network_and_servers">شبکه و سرورها</string> + <string name="network_socks_toggle_use_socks_proxy">استفاده از پروکسی SOCKS</string> + <string name="network_proxy_port">پورت %d</string> + <string name="network_use_onion_hosts">استفاده از میزبان‌های onion.</string> + <string name="network_use_onion_hosts_prefer">وقتی موجود بود</string> + <string name="network_use_onion_hosts_no_desc">از میزبان‌های Onion استفاده نخواهد شد.</string> + <string name="network_session_mode_transport_isolation">انزوای ترابری</string> + <string name="network_session_mode_user">نمایه گپ</string> + <string name="network_session_mode_entity">اتصال</string> + <string name="app_version_code">ساختار برنامه: %s</string> + <string name="network_settings_title">تنظیمات شبکه</string> + <string name="show_dev_options">نمایش:</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>لطفا توجه داشته باشید</b>: واسطه‌های پیام و پرونده از طریق پروکسی SOCKS متصل می‌شوند. تماس‌ها و ارسال پیش‌نمایش‌های لینک از اتصال مستقیم استفاده می‌کنند.]]></string> + <string name="developer_options_section">گزینه‌های توسعه‌دهنده</string> + <string name="hide_dev_options">پنهان کردن:</string> + <string name="shutdown_alert_question">بسته شود؟</string> + <string name="all_your_contacts_will_remain_connected">تمام مخاطبانتان متصل باقی خواهند ماند.</string> + <string name="all_your_contacts_will_remain_connected_update_sent">تمام مخاطبانتان متصل باقی خواهند ماند. به‌روزرسانی نمایه به مخاطبانتان ارسال خواهد شد.</string> + <string name="network_disable_socks_info">اگر تایید کنید، سرورهای پیام‌رسانی خواهند توانست نشانی‌ IP، و فراهم‌کننده شما را ببینند - و این که به چه سرورهایی متصل می‌شوید.</string> + <string name="ensure_ICE_server_address_are_correct_format_and_unique">مطمئن شوید قالب نشانی‌های سرور WebRTC ICE صحیح است، در خط‌های جدا نوشته شده و تکرار نشده‌اند.</string> + <string name="network_session_mode_entity_description"><![CDATA[یک اتصال جدای TCP (و اطلاعات ورود SOCKS) <b>برای هر مخاطب و عضو گروه</b> استفاده خواهد شد. \n<b>لطفا توجه داشته باشید</b>: اگر اتصال‌های زیادی داشته باشید، مصرف باتری و ترافیک شما می‌تواند به شکل قابل توجه بالاتر باشد و بعضی اتصال‌ها ممکن است با موفقیت انجام نشوند.]]></string> + <string name="update_network_session_mode_question">حالت انزوای ترابری به روز شود؟</string> + <string name="edit_image">ویرایش تصویر</string> + <string name="create_simplex_address">ایجاد نشانی‌ SimpleX</string> + <string name="share_with_contacts">اشتراک‌گذاری با مخاطبان</string> + <string name="profile_update_will_be_sent_to_contacts">به‌روزرسانی نمایه به مخاطبانتان ارسال خواهد شد.</string> + <string name="auto_accept_contact">پذیرفتن خودکار</string> + <string name="enter_welcome_message_optional">ورود پیام خوشامدگویی…(اختیاری)</string> + <string name="save_settings_question">تنظیمات ذخیره شوند؟</string> + <string name="save_auto_accept_settings">ذخیره تنظیمات پذیرفتن خودکار</string> + <string name="delete_address">حذف نشانی</string> + <string name="email_invite_body">سلام! +\nبه وسیله SimpleX Chat به من متصل شوید: %s</string> + <string name="dont_create_address">نشانی ایجاد نشود</string> + <string name="continue_to_next_step">ادامه</string> + <string name="your_current_profile">نمایه فعلی شما</string> + <string name="full_name__field">نام کامل:</string> + <string name="save_and_notify_contacts">ذخیره و مخاطبان مطلع شوند</string> + <string name="email_invite_subject">بیایید در SimpleX Chat گفتگو کنیم</string> + <string name="save_preferences_question">تنظیمات ذخیره شوند؟</string> + <string name="hide_profile">پنهان کردن نمایه</string> + <string name="password_to_show">کلمه عبور برای نمایش</string> + <string name="save_profile_password">ذخیره کلمه عبور نمایه</string> + <string name="stop_sharing_address">اشتراک‌گذاری نشانی متوقف شود؟</string> + <string name="stop_sharing">توقف اشتراک‌گذاری</string> + <string name="you_can_create_it_later">می‌توانید بعدا آن را ایجاد کنید</string> + <string name="you_can_make_address_visible_via_settings">می‌توانید آن را از طریق تنظیمات برای مخاطبان SimpleX خود قابل رویت کنید.</string> + <string name="display_name__field">نام نمایه:</string> + <string name="delete_image">حذف تصویر</string> + <string name="save_and_notify_group_members">ذخیره و اعضای گروه مطلع شوند</string> + <string name="save_and_notify_contact">ذخیره و مخاطب مطلع شود</string> + <string name="exit_without_saving">خروج بدون ذخیره کردن</string> + <string name="to_reveal_profile_enter_password">برای آشکار کردن نمایه پنهان خود، یک کلمه عبور کامل در فیلد جستجو در صفحه نمایه‌های گپتان وارد کنید.</string> + <string name="invite_friends">دعوت از دوستان</string> + <string name="confirm_password">تایید کلمه عبور</string> + <string name="hidden_profile_password">کلمه عبور نمایه پنهان</string> + <string name="add_address_to_your_profile">به نمایه خود نشانی اضافه کنید، تا مخاطبانتان بتوانند آن را با اشخاص دیگر به اشتراک بگذارند. به‌روزرسانی نمایه به مخاطبانتان ارسال خواهد شد.</string> + <string name="share_address_with_contacts_question">نشانی با مخاطبان به اشتراک گذاشته شود؟</string> + <string name="create_address_and_let_people_connect">یک نشانی ایجاد کنید تا اشخاص بتوانند به شما متصل شوند.</string> + <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">نمایه شما روی دستگاهتان ذخیره شده و فقط با مخاطبانتان به اشتراک گذاشته می‌شود. سرورهای SimpleX قادر به دیدن نمایه شما نیستند.</string> + <string name="error_saving_user_password">خطا در ذخیره کردن کلمه عبور کاربر</string> + <string name="settings_audio_video_calls">تماس‌های صوتی و تصویری</string> + <string name="your_calls">تماس‌های شما</string> + <string name="always_use_relay">همیشه از واسطه استفاده شود</string> + <string name="call_connection_peer_to_peer">همتا به همتا</string> + <string name="onboarding_notifications_mode_periodic">متناوب</string> + <string name="icon_descr_call_pending_sent">تماس در حال انتظار</string> + <string name="icon_descr_call_missed">تماس ناموفق</string> + <string name="icon_descr_call_ended">تماس پایان یافت</string> + <string name="answer_call">پاسخ به تماس</string> + <string name="onboarding_notifications_mode_service">فوری</string> + <string name="call_connection_via_relay">از طریق واسطه</string> + <string name="icon_descr_audio_on">صدا روشن</string> + <string name="icon_descr_flip_camera">چرخش دوربین</string> + <string name="privacy_message_draft">پیش‌نویس پیام</string> + <string name="onboarding_notifications_mode_off">وقتی برنامه در حال اجراست</string> + <string name="encrypted_audio_call">تماس صوتی رمزگذاری سرتاسر شده</string> + <string name="accept">پذیرفتن</string> + <string name="your_privacy">حریم خصوصی شما</string> + <string name="lock_mode">حالت قفل</string> + <string name="submit_passcode">ارسال</string> + <string name="incorrect_passcode">کد عبور نادرست</string> + <string name="la_mode_passcode">کد عبور</string> + <string name="receipts_section_contacts">مخاطبان</string> + <string name="receipts_contacts_disable_keep_overrides">غیرفعال کردن (نگه‌داشتن مقدارهای جایگزین شده)</string> + <string name="receipts_groups_enable_keep_overrides">فعال کردن (نگه‌داشتن مقدارهای جایگزین شده گروه)</string> + <string name="receipts_groups_disable_for_all">غیرفعال برای همه گروه‌ها</string> + <string name="settings_section_title_help">کمک</string> + <string name="settings_section_title_chats">گپ‌ها</string> + <string name="run_chat_section">اجرای گپ</string> + <string name="chat_is_running">گپ در حال اجراست</string> + <string name="stop_chat_confirmation">توقف</string> + <string name="delete_files_and_media_all">حذف تمام پرونده‌ها</string> + <string name="correct_name_to">اصلاح نام به %s؟</string> + <string name="onboarding_notifications_mode_subtitle">بعدا از طریق تنظیمات قابل تغییر است.</string> + <string name="icon_descr_call_rejected">تماس پذیرفته نشده</string> + <string name="alert_title_msg_bad_hash">هش پیام ناصحیح</string> + <string name="alert_text_msg_bad_hash">هش پیام قبلی متفاوت است.</string> + <string name="alert_title_msg_bad_id">شناسه پیام ناصحیح</string> + <string name="receipts_contacts_title_enable">رسیدها فعال شوند؟</string> + <string name="settings_section_title_you">شما</string> + <string name="settings_section_title_messages">پیام‌ها و پرونده‌ها</string> + <string name="settings_section_title_calls">تماس‌ها</string> + <string name="settings_section_title_incognito">حالت ناشناس</string> + <string name="database_passphrase">عبارت عبور پایگاه داده</string> + <string name="export_database">صدور پایگاه داده</string> + <string name="delete_chat_profile_question">نمایه گپ حذف شود؟</string> + <string name="display_name">نام خود را وارد کنید:</string> + <string name="create_profile_button">ایجاد</string> + <string name="how_to_use_markdown">روش استفاده از مارکداون</string> + <string name="you_can_use_markdown_to_format_messages__prompt">می‌توانید از مارکداون برای آرایش پیام‌ها استفاده کنید:</string> + <string name="callstatus_rejected">تماس بی‌پاسخ</string> + <string name="callstatus_accepted">تماس پذیرفته</string> + <string name="decentralized">نامتمرکز</string> + <string name="create_your_profile">نمایه خود را ایجاد کنید</string> + <string name="how_simplex_works">SimpleX چگونه کار می‌کند</string> + <string name="read_more_in_github_with_link"><![CDATA[مطالعه بیشتر در <font color="#0088ff">مخزن GitHub</font> ما.]]></string> + <string name="use_chat">استفاده از گپ</string> + <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>بهترین گزینه برای باتری</b>. شما اعلان‌ها را فقط وقتی دریافت می‌کنید که برنامه در حال اجراست (بدون سرویس پس‌زمینه).]]></string> + <string name="call_on_lock_screen">تماس‌ها روی صفحه قفل:</string> + <string name="accept_call_on_lock_screen">پذیرفتن</string> + <string name="relay_server_protects_ip">سرور واسط از نشانی IP شما محافظت می‌کند، اما سرور می‌تواند مدت تماس را مشاهده کند.</string> + <string name="open_verb">گشودن</string> + <string name="status_e2e_encrypted">رمزگذاری سرتاسر شده</string> + <string name="icon_descr_hang_up">قطع تماس</string> + <string name="icon_descr_video_off">ویدئو خاموش</string> + <string name="icon_descr_video_on">ویدئو روشن</string> + <string name="icon_descr_audio_off">صدا خاموش</string> + <string name="icon_descr_speaker_off">بلندگو خاموش</string> + <string name="icon_descr_speaker_on">بلندگو روشن</string> + <string name="icon_descr_call_connecting">در حال اتصال تماس</string> + <string name="enable_lock">فعال کردن قفل</string> + <string name="passcode_set">کد عبور تعیین شد!</string> + <string name="passcode_changed">کد عبور تغییر کرد!</string> + <string name="change_lock_mode">تغییر حالت قفل</string> + <string name="self_destruct">خودتخریبی</string> + <string name="enabled_self_destruct_passcode">فعال کردن کد عبور خودتخریبی</string> + <string name="change_self_destruct_mode">تغییر حالت خودتخریبی</string> + <string name="self_destruct_passcode_enabled">کد عبور خودتخریبی فعال شد!</string> + <string name="self_destruct_passcode">کد عبور خودتخریبی</string> + <string name="self_destruct_new_display_name">نام نمایشی جدید:</string> + <string name="if_you_enter_self_destruct_code">اگر کد عبور خودتخریبی خود را زمان باز کردن برنامه وارد کنید:</string> + <string name="all_app_data_will_be_cleared">تمام اطلاعات برنامه حذف می‌شود.</string> + <string name="receipts_section_description">این تنظیمات برای نمایه فعلی شما هستند</string> + <string name="receipts_contacts_override_enabled">ارسال رسید برای %d مخاطب فعال است</string> + <string name="receipts_contacts_disable_for_all">غیرفعال برای همه</string> + <string name="receipts_groups_enable_for_all">فعال برای همه گروه‌ها</string> + <string name="receipts_groups_disable_keep_overrides">غیرفعال کردن (نگه‌داشتن مقدارهای جایگزین شده گروه)</string> + <string name="settings_shutdown">توقف برنامه</string> + <string name="settings_section_title_themes">تم‌ها</string> + <string name="delete_database">حذف پایگاه داده</string> + <string name="chat_database_imported">پایگاه داده گپ وارد شد</string> + <string name="total_files_count_and_size">%d پرونده با اندازه کل %s</string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[فقط دستگاه‌های کلاینت نمایه‌های کاربر، مخاطبان، گروه‌ها و پیام‌های ارسالی با <b>رمزگذاری سرتاسر دو لایه</b> را ذخیره می‌کنند.]]></string> + <string name="ignore">نادیده گرفتن</string> + <string name="audio_device_bluetooth">بلوتوث</string> + <string name="import_database">وارد کردن پایگاه داده</string> + <string name="people_can_connect_only_via_links_you_share">اشخاص فقط از طریق لینک‌هایی که به اشتراک می‌گذارید می‌توانند به شما متصل شوند.</string> + <string name="call_already_ended">تماس از پیش پایان یافته!</string> + <string name="integrity_msg_bad_hash">هش پیام ناصحیح</string> + <string name="auto_accept_images">پذیرفتن خودکار تصاویر</string> + <string name="send_link_previews">ارسال پیش‌نمایش‌های لینک</string> + <string name="la_mode_system">سیستم</string> + <string name="la_app_passcode">کد عبور برنامه</string> + <string name="la_mode_off">خاموش</string> + <string name="receipts_groups_override_enabled">ارسال رسید برای %d گروه فعال است</string> + <string name="receipts_groups_override_disabled">ارسال رسید برای %d گروه غیرفعال است</string> + <string name="settings_section_title_support">حمایت از SIMPLEX CHAT</string> + <string name="settings_section_title_socks">پروکسی SOCKS</string> + <string name="settings_section_title_use_from_desktop">استفاده از کامپیوتر</string> + <string name="new_database_archive">آرشیو پایگاه داده جدید</string> + <string name="old_database_archive">آرشیو پایگاه داده قدیمی</string> + <string name="error_starting_chat">خطا در شروع گپ</string> + <string name="immune_to_spam_and_abuse">ایمن در برابر اسپم و سو استفاده</string> + <string name="how_it_works">چگونه کار می‌کند</string> + <string name="icon_descr_video_call">تماس تصویری</string> + <string name="audio_device_earpiece">گوشی</string> + <string name="audio_device_speaker">بلندگو</string> + <string name="audio_device_wired_headphones">هدفون‌ها</string> + <string name="next_generation_of_private_messaging">نسل بعدی پیام‌رسانی خصوصی</string> + <string name="callstate_ended">پایان یافت</string> + <string name="unable_to_open_browser_title">خطا در باز کردن مرورگر</string> + <string name="set_password_to_export">جهت صدور عبارت عبور تعیین کنید</string> + <string name="protect_app_screen">محافظت از صفحه برنامه</string> + <string name="settings_restart_app">شروع مجدد</string> + <string name="settings_experimental_features">ویژگی‌های آزمایشی</string> + <string name="open_database_folder">باز کردن پوشه پایگاه داده</string> + <string name="chat_is_stopped">گپ متوقف شده است</string> + <string name="error_deleting_database">خطا در حذف پایگاه داده گپ</string> + <string name="error_importing_database">خطا در وارد کردن پایگاه داده گپ</string> + <string name="you_control_your_chat">شما گپ خود را کنترل می‌کنید!</string> + <string name="your_profile_is_stored_on_your_device">نمایه، مخاطبان و پیام‌های تحویل داده شده شما روی دستگاهتان ذخیره می‌شوند.</string> + <string name="profile_is_only_shared_with_your_contacts">نمایه فقط با مخاطبانتان به اشتراک گذاشته می‌شود.</string> + <string name="display_name_cannot_contain_whitespace">نام نمایشی نمی‌تواند شامل نویسه‌های فاصله باشد.</string> + <string name="create_another_profile_button">ایجاد نمایه</string> + <string name="invalid_name">نام نامعتبر!</string> + <string name="bold_text">برجسته</string> + <string name="callstate_waiting_for_confirmation">در انتظار تایید…</string> + <string name="callstate_received_answer">پاسخ دریافت شد…</string> + <string name="callstate_received_confirmation">تایید دریافت شد…</string> + <string name="callstate_connecting">در حال اتصال…</string> + <string name="callstate_waiting_for_answer">در انتظار پاسخ…</string> + <string name="setup_database_passphrase">تعیین عبارت عبور پایگاه داده</string> + <string name="use_random_passphrase">استفاده از عبارت عبور تصادفی</string> + <string name="icon_descr_audio_call">تماس صوتی</string> + <string name="your_ice_servers">سرورهای ICE شما</string> + <string name="icon_descr_call_progress">تماس در جریان است</string> + <string name="integrity_msg_bad_id">شناسه پیام ناصحیح</string> + <string name="privacy_and_security">حریم خصوصی و امنیت</string> + <string name="new_passcode">کد عبور جدید</string> + <string name="authentication_cancelled">تصدیق لغو شد</string> + <string name="privacy_show_last_messages">نمایش آخرین پیام‌ها</string> + <string name="enable_self_destruct">فعال کردن خودتخریبی</string> + <string name="app_passcode_replaced_with_self_destruct">کد عبور برنامه با کد عبور خودتخریبی جایگزین می‌شود.</string> + <string name="if_you_enter_passcode_data_removed">اگر کد عبور خودتخریبی خود را زمان باز کردن برنامه وارد کنید، تمام اطلاعات برنامه به صورت غیر قابل بازگشت حذف خواهد شد!</string> + <string name="self_destruct_passcode_changed">کد عبور خودتخریبی تغییر کرد!</string> + <string name="receipts_groups_title_enable">رسیدها برای گروه‌ها فعال شوند؟</string> + <string name="receipts_contacts_enable_for_all">فعال برای همه</string> + <string name="receipts_section_description_1">آن‌ها در تنظیمات مخاطب و گروه قابل جایگزینی هستند.</string> + <string name="receipts_section_groups">گروه‌های کوچک (حداکثر ۲۰)</string> + <string name="settings_section_title_settings">تنظیمات</string> + <string name="first_platform_without_user_ids">اولین بن‌سازه بدون هیچ شناسه کاربری - با طرح‌ریزی خصوصی</string> + <string name="onboarding_notifications_mode_title">اعلان‌های خصوصی</string> + <string name="you_can_change_it_later">عبارت عبور تصادفی در تنظیمات به صورت متن آشکار ذخیره می‌شود. +\nمی‌توانید بعدا آن را تغییر دهید.</string> + <string name="incoming_video_call">تماس تصویری رسیده</string> + <string name="incoming_audio_call">تماس صوتی رسیده</string> + <string name="encrypted_video_call">تماس تصویری رمزگذاری سرتاسر شده</string> + <string name="audio_call_no_encryption">تماس صوتی (رمزگذاری سرتاسر نشده)</string> + <string name="status_no_e2e_encryption">بدون رمزگذاری سرتاسر</string> + <string name="status_contact_has_e2e_encryption">مخاطب رمزگذاری سرتاسر دارد</string> + <string name="alert_text_fragment_encryption_out_of_sync_old_database">این اتفاق وقتی می‌افتد که شما یا اتصالتان از پشتیبان پایگاه داده قدیمی استفاده کرده باشید.</string> + <string name="alert_text_encryption_renegotiation_failed">مذاکره مجدد رمزگذاری ناموفق بود.</string> + <string name="encrypt_local_files">رمزگذاری پرونده‌های محلی</string> + <string name="full_backup">پشتیبان‌گیری اطلاعات برنامه</string> + <string name="lock_after">قفل بعد از</string> + <string name="confirm_passcode">تایید کد عبور</string> + <string name="passcode_not_changed">کد عبور تغییر نکرد!</string> + <string name="create_profile">ایجاد نمایه</string> + <string name="italic_text">مورب</string> + <string name="we_do_not_store_contacts_or_messages_on_servers">ما هیچکدام از مخاطبان و پیام‌های(وقتی تحویل داده شدند) شما را روی سرورها ذخیره نمی‌کنیم.</string> + <string name="colored_text">رنگی</string> + <string name="secret_text">محرمانه</string> + <string name="callstatus_calling">در حال تماس…</string> + <string name="callstatus_missed">تماس ناموفق</string> + <string name="callstatus_in_progress">تماس در جریان است</string> + <string name="callstatus_ended">تماس پایان یافت %1$s</string> + <string name="callstatus_error">خطا در تماس</string> + <string name="callstate_starting">در حال راه‌اندازی…</string> + <string name="callstate_connected">متصل</string> + <string name="callstatus_connecting">در حال برقراری تماس…</string> + <string name="video_call_no_encryption">تماس تصویری (رمزگذاری سرتاسر نشده)</string> + <string name="reject">رد کردن</string> + <string name="set_passcode">تعیین کد عبور</string> + <string name="receipts_groups_title_disable">رسیدها برای گروه‌ها غیرفعال شوند؟</string> + <string name="change_self_destruct_passcode">تغییر کد عبور خودتخریبی</string> + <string name="settings_section_title_delivery_receipts">رسیدهای تحویل ارسال شوند به</string> + <string name="settings_developer_tools">ابزارهای توسعه‌دهنده</string> + <string name="settings_section_title_app">برنامه</string> + <string name="settings_section_title_device">دستگاه</string> + <string name="settings_section_title_experimenta">آزمایشی</string> + <string name="settings_section_title_network_connection">اتصال شبکه</string> + <string name="settings_section_title_icon">آیکون برنامه</string> + <string name="chat_database_section">پایگاه داده گپ</string> + <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">بن‌سازه پیام‌رسانی و کاربردی که از حریم خصوصی و امنیت شما محافظت می‌کند.</string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>گزینه خوب برای باتری</b>. سرویس پس‌زمینه هر ۱۰ دقیقه پیام‌ها را بررسی می‌کند. ممکن است تماس‌ها یا پیام‌های ضروری را از دست دهید.]]></string> + <string name="alert_title_skipped_messages">پیام‌ها از قلم افتادند</string> + <string name="unable_to_open_browser_desc">مرورگر وب پیش‌فرض برای تماس‌ها لازم است. لطفا مرورگر پیش‌فرض را در سیستم تنظیم کنید، و اطلاعات بیشتر را با توسعه‌دهندگان به اشتراک بگذارید.</string> + <string name="relay_server_if_necessary">سرور واسط فقط در زمان نیاز مورد استفاده قرار می‌گیرد. طرف دیگری قادر به مشاهده نشانی IP شما خواهد بود.</string> + <string name="show_call_on_lock_screen">نمایش</string> + <string name="no_call_on_lock_screen">غیرفعال</string> + <string name="open_simplex_chat_to_accept_call">گشودن SimpleX Chat برای پذیرفتن تماس</string> + <string name="allow_accepting_calls_from_lock_screen">تماس‌ها از صفحه قفل را از طریق تنظیمات فعال کنید.</string> + <string name="status_contact_has_no_e2e_encryption">مخاطب رمزگذاری سرتاسر ندارد</string> + <string name="webrtc_ice_servers">سرورهای WebRTC ICE</string> + <string name="alert_text_msg_bad_id">شناسه پیام بعدی نادرست است (کمتر یا برابر است با قبلی). +\nبروز این اتفاق می‌تواند به دلیل وجود اشکال نرم‌افزاری یا مورد حمله قرار گرفتن اتصال باشد.</string> + <string name="alert_text_fragment_please_report_to_developers">لطفا آن را به توسعه‌دهندگان گزارش دهید.</string> + <string name="integrity_msg_duplicate">پیام همسان</string> + <string name="stop_chat_question">گپ متوقف شود؟</string> + <string name="set_password_to_export_desc">پایگاه داده با استفاده از یک عبارت عبور تصادفی رمزگذاری شده، لطفا پیش از صدور آن را تغییر دهید.</string> + <string name="error_stopping_chat">خطا در متوقف کردن گپ</string> + <string name="error_exporting_chat_database">خطا در صدور پایگاه داده گپ</string> + <string name="import_database_question">پایگاه داده گپ وارد شود؟</string> + <string name="import_database_confirmation">وارد کردن</string> + <string name="restart_the_app_to_use_imported_chat_database">به منظور استفاده از پایگاه داده گپ وارد شده، برنامه را شروع مجدد کنید.</string> + <string name="non_fatal_errors_occured_during_import">چند خطای غیر مهلک هنگام وارد کردن رخ داد - برای اطلاعات بیشتر می‌توانید کنسول گپ را ببینید.</string> + <string name="chat_database_deleted">پایگاه داده گپ حذف شد</string> + <string name="restart_the_app_to_create_a_new_chat_profile">به منظور ایجاد نمایه گپ جدید، برنامه را شروع مجدد کنید.</string> + <string name="files_and_media_section">پرونده‌ها و رسانه</string> + <string name="delete_files_and_media_for_all_users">حذف پرونده‌ها برای تمام نمایه‌های گپ</string> + <string name="delete_files_and_media_question">پرونده‌ها و رسانه حذف شوند؟</string> + <string name="no_received_app_files">هیچ پرونده دریافتی یا ارسالی وجود ندارد</string> + <string name="chat_item_ttl_none">هرگز</string> + <string name="chat_item_ttl_seconds">%s ثانیه</string> + <string name="make_private_connection">اجرای اتصال خصوصی</string> + <string name="migrate_from_another_device">جابه‌جایی از دستگاهی دیگر</string> + <string name="empty_chat_profile_is_created">یک نمایه گپ خالی با نام فراهم شده ایجاد می‌شود، و برنامه به طور معمول باز می‌شود.</string> + <string name="permissions_grant_in_settings">اعطای اجازه‌ها در تنظیمات</string> + <string name="permissions_find_in_settings_and_grant">این مجوز را در تنظیمات اندروید پیدا و به صورت دستی آن را اعطا کنید.</string> + <string name="permissions_open_settings">باز کردن تنظیمات</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">پروتکل و کد متن‌باز - هر کسی می‌تواند سرورها را راه‌اندازی کند.</string> + <string name="receipts_contacts_title_disable">رسیدها غیرفعال شوند؟</string> + <string name="receipts_contacts_enable_keep_overrides">فعال کردن (نگه‌داشتن مقدارهای جایگزین شده)</string> + <string name="permissions_required">اعطای اجازه‌ها</string> + <string name="permissions_record_audio">میکروفون</string> + <string name="permissions_camera">دوربین</string> + <string name="permissions_camera_and_record_audio">دوربین و میکروفون</string> + <string name="permissions_grant">اعطای اجازه‌ها برای برقراری تماس‌ها</string> + <string name="privacy_redefined">حریم خصوصی باز تعریف شده</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">برای حفاظت از حریم خصوصی، به جای شناسه‌های کاربری مورد استفاده در بن‌سازه‌های دیگر، SimpleX شناسه‌هایی برای صفوف پیام دارد، جدا برای هر کدام از مخاطبان شما.</string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>از باتری بیشتر استفاده می‌کند</b>! سرویس پس‌زمینه همیشه در حال اجراست - اعلان‌ها به محض موجود شدن، نمایش داده می‌شوند.]]></string> + <string name="alert_text_skipped_messages_it_can_happen_when">وقتی می‌تواند اتفاق بیفتد که: +\n۱. پیام‌ها در کلاینت فرستنده بعد از ۲ روز یا روی سرور بعد از ۳۰ روز منقضی شده باشند. +\n۲. رمزگشایی پیام ناموفق بود، چون شما یا مخاطبتان از پشتیبان پایگاه داده قدیمی استفاده استفاده کردید. +\n۳. اتصال مورد حمله قرار گرفته باشد.</string> + <string name="settings_section_title_profile_images">تصاویر نمایه</string> + <string name="stop_chat_to_export_import_or_delete_chat_database">به منظور صدور، ورود و حذف پایگاه داده گپ، گپ را متوقف کنید. هنگامی که گپ متوقف شده است، شما قادر به دریافت و ارسال پیام نخواهید بود.</string> + <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">پایگاه داده گپ فعلی شما حذف و توسط پایگاه داده وارد شده جایگزین خواهد شد. +\nاین عمل قابل برگشت نیست - نمایه، مخاطبان، پیام‌ها و پرونده‌های شما به صورت غیر قابل بازگشت از بین خواهند رفت.</string> + <string name="your_chat_database">پایگاه داده گپ شما</string> + <string name="delete_chat_profile_action_cannot_be_undone_warning">این عمل قابل برگشت نیست - نمایه، مخاطبان، پیام‌ها و پرونده‌های شما به صورت غیر قابل بازگشت از بین خواهند رفت.</string> + <string name="receipts_contacts_override_disabled">ارسال رسید برای %d مخاطب غیرفعال است</string> + <string name="delete_files_and_media_desc">این عمل قابل برگشت نیست - تمام پرونده‌ها و رسانه دریافتی حذف خواهند شد. عکس‌های با کیفیت پایین باقی خواهند ماند.</string> + <string name="you_must_use_the_most_recent_version_of_database">شما باید از تازه‌ترین نسخه پایگاه داده گپ خود روی فقط یک دستگاه استفاده کنید، در غیر این صورت ممکن است از بعضی از مخاطب‌ها ‌دیگر پیامی دریافت نکنید.</string> + <string name="messages_section_title">پیام‌ها</string> + <string name="enable_automatic_deletion_message">این عمل قابل برگشت نیست - پیام‌های ارسالی و دریافتی قدیمی‌تر از زمان انتخابی حذف خواهند شد. این کار ممکن است چندین دقیقه زمان ببرد.</string> + <string name="error_changing_message_deletion">خطا در تغییر تنظیمات</string> + <string name="save_passphrase_in_settings">ذخیره عبارت عبور در تنظیمات</string> + <string name="delete_messages">حذف پیام‌ها</string> + <string name="save_passphrase_in_keychain">ذخیره عبارت عبور در مخزن کلید</string> + <string name="messages_section_description">این تنظیمات بر پیام‌های موجود در نمایه گپ فعلی شما اعمال می‌شود</string> + <string name="enable_automatic_deletion_question">حذف خودکار پیام فعال شود؟</string> + <string name="restore_database_alert_confirm">برگرداندن</string> + <string name="upgrade_and_open_chat">ارتقا و گشودن گپ</string> + <string name="group_invitation_item_description">دعوت به گروه %1$s</string> + <string name="join_group_question">به گروه می‌پیوندید؟</string> + <string name="leave_group_button">ترک</string> + <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">دریافت پیام‌ها از این گروه برای شما متوقف خواهد شد. تاریخچه گپ حفظ خواهد شد.</string> + <string name="icon_descr_add_members">دعوت اعضا</string> + <string name="rcv_group_events_count">%d رویداد گروه</string> + <string name="rcv_group_event_n_members_connected">%s، %s و %d عضو دیگر متصل شدند</string> + <string name="group_members_2">%s و %s</string> + <string name="group_members_n">%s، %s و %d عضو</string> + <string name="rcv_group_and_other_events">و %d رویداد دیگر</string> + <string name="rcv_group_event_open_chat">گشودن</string> + <string name="rcv_conn_event_verification_code_reset">کد امنیتی تغییر پیدا کرد</string> + <string name="group_member_status_unknown">وضعیت ناشناخته</string> + <string name="group_member_status_creator">سازنده</string> + <string name="new_passphrase">عبارت عبور جدید…</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">پایگاه داده رمزگذاری و عبارت عبور در تنظیمات ذخیره خواهد شد.</string> + <string name="database_encryption_will_be_updated_in_settings">عبارت عبور رمزگذاری پایگاه داده به‌روز و در تنظیمات ذخیره خواهد شد.</string> + <string name="cannot_access_keychain">امکان دسترسی مخزن کلید برای ذخیره کلمه عبور پایگاه داده وجود ندارد</string> + <string name="unknown_database_error_with_info">خطای پایگاه داده ناشناخته: %s</string> + <string name="invalid_migration_confirmation">تایید جابه‌جایی نامعتبر</string> + <string name="you_joined_this_group">شما به این گروه پیوستید</string> + <string name="snd_group_event_user_left">شما ترک کردید</string> + <string name="snd_group_event_group_profile_updated">نمایه گروه به‌روز شد</string> + <string name="group_member_role_member">عضو</string> + <string name="group_member_role_admin">مدیر</string> + <string name="group_member_role_owner">صاحب</string> + <string name="group_member_status_removed">حذف شد</string> + <string name="new_member_role">نقش عضو جدید</string> + <string name="remove_passphrase_from_settings">عبارت عبور از تنظیمات پاک شود؟</string> + <string name="impossible_to_recover_passphrase"><![CDATA[<b>لطفا توجه داشته باشید</b>: اگر عبارت عبور را از دست بدهید، قادر نخواهید بود آن را بازیابی کنید یا تغییر دهید.]]></string> + <string name="wrong_passphrase">عبارت عبور پایگاه داده اشتباه</string> + <string name="file_with_path">پرونده: %s</string> + <string name="database_passphrase_is_required">عبارت عبور پایگاه داده برای گشودن گپ الزامی است.</string> + <string name="unknown_error">خطای ناشناخته</string> + <string name="open_chat">گشودن گپ</string> + <string name="database_backup_can_be_restored">تلاش برای تغییر عبارت عبور پایگاه داده کامل نشد.</string> + <string name="database_upgrade">ارتقای پایگاه داده</string> + <string name="database_downgrade">تنزل پایگاه داده</string> + <string name="confirm_database_upgrades">تایید ارتقای پایگاه داده</string> + <string name="leave_group_question">گروه را ترک می‌کنید؟</string> + <string name="alert_title_cant_invite_contacts">امکان دعوت مخاطبان وجود ندارد!</string> + <string name="alert_title_cant_invite_contacts_descr">در حال استفاده از نمایه ناشناس برای این گروه هستید - برای جلوگیری از اشتراک‌گذاری نمایه اصلی شما، دعوت مخاطبان مجاز نیست</string> + <string name="rcv_direct_event_contact_deleted">مخاطب حذف شد</string> + <string name="rcv_group_event_member_connected">متصل شد</string> + <string name="rcv_group_event_member_left">ترک کرد</string> + <string name="rcv_group_event_member_deleted">%1$s حذف شد</string> + <string name="rcv_group_event_user_deleted">شما حذف شدید</string> + <string name="conn_event_ratchet_sync_agreed">توافق رمزگذاری</string> + <string name="group_member_role_observer">ناظر</string> + <string name="past_member_vName">عضو پیشین %1$s</string> + <string name="icon_descr_expand_role">بسط دادن انتخاب نقش</string> + <string name="database_passphrase_will_be_updated">عبارت عبور رمزگذاری پایگاه داده به‌روز خواهد شد.</string> + <string name="rcv_group_event_member_created_contact">مستقیما متصل شد</string> + <string name="rcv_group_event_1_member_connected">%s متصل شد</string> + <string name="joining_group">پیوستن به گروه</string> + <string name="update_database">به‌روزرسانی</string> + <string name="confirm_new_passphrase">تایید عبارت عبور جدید…</string> + <string name="encrypt_database_question">پایگاه داده رمزگذاری شود؟</string> + <string name="incompatible_database_version">نسخه پایگاه داده ناسازگار</string> + <string name="rcv_group_event_updated_group_profile">نمایه گروه به‌روز شد</string> + <string name="snd_conn_event_switch_queue_phase_completed_for_member">شما نشانی را برای %s تغییر دادید</string> + <string name="snd_conn_event_switch_queue_phase_completed">شما نشانی را تغییر دادید</string> + <string name="conn_event_ratchet_sync_allowed">مذاکره مجدد رمزگذاری مجاز است</string> + <string name="group_member_status_group_deleted">گروه حذف شد</string> + <string name="group_member_status_intro_invitation">در حال اتصال (دعوت معرفی)</string> + <string name="conn_event_disabled_pq">رمزگذاری سرتاسر استاندارد</string> + <string name="group_member_status_accepted">در حال اتصال (پذیرفته شد)</string> + <string name="group_member_status_announced">در حال اتصال (اعلام شد)</string> + <string name="group_member_status_connected">متصل شد</string> + <string name="group_member_status_complete">کامل</string> + <string name="group_member_status_connecting">در حال اتصال</string> + <string name="group_member_status_unknown_short">ناشناخته</string> + <string name="invite_to_group_button">دعوت به گروه</string> + <string name="num_contacts_selected">%d مخاطب انتخاب شد</string> + <string name="invite_prohibited">امکان دعوت مخاطب وجود ندارد!</string> + <string name="join_group_button">پیوستن</string> + <string name="icon_descr_group_inactive">گروه غیرفعال</string> + <string name="alert_message_group_invitation_expired">دعوت‌نامه گروه دیگر اعتبار ندارد، توسط فرستنده پاک شد.</string> + <string name="alert_title_no_group">گروه پیدا نشد!</string> + <string name="you_are_invited_to_group">شما به گروه دعوت شیده‌اید</string> + <string name="you_rejected_group_invitation">شما دعوت گروه را رد کردید</string> + <string name="rcv_group_event_member_added">از %1$s دعوت شده</string> + <string name="rcv_group_event_changed_member_role">نقش %s به %s تغییر کرد</string> + <string name="rcv_group_event_group_deleted">گروه حذف شد</string> + <string name="conn_event_ratchet_sync_required">مذاکره مجدد رمزگذاری الزامی است</string> + <string name="snd_conn_event_ratchet_sync_ok">رمزگذاری برای %s بی‌عیب است</string> + <string name="snd_conn_event_ratchet_sync_allowed">مذاکره مجدد رمزگذاری برای %s مجاز است</string> + <string name="conn_event_enabled_pq">رمزگذاری سرتاسر مقاوم در برابر کوانتوم</string> + <string name="no_contacts_to_add">مخاطبی برای افزودن وجود ندارد</string> + <string name="skip_inviting_button">رد شدن از دعوت اعضا</string> + <string name="select_contacts">انتخاب مخاطبان</string> + <string name="database_encrypted">پایگاه داده رمزگذاری شده!</string> + <string name="current_passphrase">عبارت عبور فعلی…</string> + <string name="profile_update_event_member_name_changed">عضو %1$s به %2$s تغییر کرد</string> + <string name="encrypt_database">رمزگذاری</string> + <string name="update_database_passphrase">به‌روزرسانی عبارت عبور پایگاه داده</string> + <string name="set_passphrase">تعیین عبارت عبور</string> + <string name="keychain_is_storing_securely">از مخزن کلید اندروید برای ذخیره امن عبارت عبور استفاده می‌شود - به سرویس اعلان اجازه عمل می‌دهد.</string> + <string name="you_have_to_enter_passphrase_every_time">باید هر بار که برنامه شروع می‌شود عبارت عبور را وارد کنید - در دستگاه ذخیره نمی‌شود.</string> + <string name="encrypted_database">پایگاه داده رمزگذاری شده</string> + <string name="icon_descr_contact_checked">مخاطب بررسی شد</string> + <string name="clear_contacts_selection_button">پاک کردن</string> + <string name="terminal_always_visible">نمایش کنسول در پنجره جدید</string> + <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">می‌توانید گپ را از طریق تنظیمات برنامه / پایگاه داده یا با شروع مجدد برنامه شروع کنید.</string> + <string name="start_chat_question">گپ شروع شود؟</string> + <string name="database_downgrade_warning">هشدار: ممکن است بعضی از اطلاعات را از دست بدهید!</string> + <string name="chat_is_stopped_indication">گپ متوقف شده است</string> + <string name="error_encrypting_database">خطا در رمزگذاری پایگاه داده</string> + <string name="remove_passphrase_from_keychain">عبارت عبور از مخزن کلید پاک شود؟</string> + <string name="notifications_will_be_hidden">اعلان‌ها فقط تا زمان توقف برنامه تحویل داده خواهند شد!</string> + <string name="remove_passphrase">پاک کردن</string> + <string name="set_database_passphrase">تعیین عبارت عبور پایگاه داده</string> + <string name="enter_correct_current_passphrase">لطفا عبارت عبور فعلی درست را وارد کنید.</string> + <string name="database_is_not_encrypted">پایگاه داده گپ شما رمزگذاری نشده است - برای محافظت از آن عبارت عبور تعیین کنید.</string> + <string name="settings_is_storing_in_clear_text">عبارت عبور به صورت متن آشکار در تنظیمات ذخیره شده است.</string> + <string name="passphrase_will_be_saved_in_settings">بعد از تغییر عبارت عبور یا شروع مجدد برنامه، عبارت عبور به صورت متن آشکار در تنظیمات ذخیره خواهد شد.</string> + <string name="change_database_passphrase_question">عبارت عبور پایگاه داده تغییر داده شود؟</string> + <string name="database_will_be_encrypted">پایگاه داده رمزگذاری خواهد شد.</string> + <string name="store_passphrase_securely_without_recover">لطفا عبارت عبور را به صورت امن ذخیره کنید، اگر آن را از دست دهید، قادر نخواهید بود به گپ دسترسی پیدا کنید.</string> + <string name="database_error">خطا در پایگاه داده</string> + <string name="keychain_error">خطا در Keychain</string> + <string name="passphrase_is_different">عبارت عبور پایگاه داده با آنچه در مخزن کلید ذخیره شده متفاوت است.</string> + <string name="error_with_info">خطا: %s</string> + <string name="wrong_passphrase_title">عبارت عبور اشتباه!</string> + <string name="enter_passphrase">ورود عبارت عبور…</string> + <string name="save_passphrase_and_open_chat">ذخیره عبارت عبور و گشودن گپ</string> + <string name="restore_database">برگرداندن پشتیبان پایگاه داده</string> + <string name="restore_database_alert_title">پشتیبان پایگاه داده برگردانده شود؟</string> + <string name="restore_database_alert_desc">لطفا بعد از برگرداندن پشتیبان پایگاه داده، کلمه عبور قبلی را وارد کنید. این عمل قابل برگشت نیست.</string> + <string name="database_restore_error">خطا در برگرداندن پایگاه داده</string> + <string name="mtr_error_no_down_migration">نسخه پایگاه داده از برنامه جدیدتر است، اما بدون جابه‌جایی تنزلی برای: %s</string> + <string name="mtr_error_different">جابه‌جایی متفاوت در برنامه/پایگاه داده: %s / %s</string> + <string name="database_migrations">جابه‌جایی‌ها: %s</string> + <string name="you_are_invited_to_group_join_to_connect_with_group_members">شما به گروه دعوت شده‌اید. برای متصل شدن به اعضای گروه، به گروه بپیوندید.</string> + <string name="join_group_incognito_button">پیوستن به صورت ناشناس</string> + <string name="alert_message_no_group">این گروه دیگر وجود ندارد.</string> + <string name="you_sent_group_invitation">شما دعوت گروه ارسال کردید</string> + <string name="group_invitation_tap_to_join">برای پیوستن لمس کنید</string> + <string name="group_invitation_tap_to_join_incognito">برای پیوستن به صورت ناشناس لمس کنید</string> + <string name="rcv_group_event_member_blocked">%s مسدود شد</string> + <string name="rcv_group_event_member_unblocked">مسدود سازی %s لغو شد</string> + <string name="rcv_group_event_changed_your_role">نقش شما به %s تغییر کرد</string> + <string name="snd_group_event_changed_role_for_yourself">شما نقش خود را به %s تغییر دادید</string> + <string name="snd_group_event_member_unblocked">شما مسدود سازی %s را لغو کردید</string> + <string name="rcv_group_event_2_members_connected">%s و %s متصل شدند</string> + <string name="rcv_group_event_3_members_connected">%s، %s و %s متصل شدند</string> + <string name="rcv_conn_event_switch_queue_phase_completed">نشانی برای شما تغییر داده شد</string> + <string name="rcv_conn_event_switch_queue_phase_changing">در حال تغییر نشانی…</string> + <string name="snd_conn_event_switch_queue_phase_changing_for_member">در حال تغییر نشانی برای %s…</string> + <string name="snd_conn_event_switch_queue_phase_changing">در حال تغییر نشانی…</string> + <string name="conn_event_ratchet_sync_ok">رمزگذاری بی‌عیب است</string> + <string name="snd_conn_event_ratchet_sync_started">در حال توافق رمزگذاری برای %s…</string> + <string name="snd_conn_event_ratchet_sync_agreed">توافق رمزگذاری برای %s</string> + <string name="group_member_role_author">نویسنده</string> + <string name="group_member_status_invited">دعوت شد</string> + <string name="group_member_status_introduced">در حال اتصال (معرفی شد)</string> + <string name="initial_member_role">نقش آغازین</string> + <string name="no_contacts_selected">مخاطبی انتخاب نشده</string> + <string name="conn_event_ratchet_sync_started">در حال توافق رمزگذاری…</string> + <string name="group_member_status_left">ترک کرد</string> + <string name="profile_update_event_contact_name_changed">مخاطب %1$s به %2$s تغییر کرد</string> + <string name="profile_update_event_removed_address">نشانی مخاطب حذف شد</string> + <string name="profile_update_event_set_new_address">تعیین نشانی مخاطب جدید</string> + <string name="encrypted_with_random_passphrase">پایگاه داده با استفاده از عبارت عبور تصادفی رمزگذاری شده، می‌توانید آن را تغییر دهید.</string> + <string name="keychain_allows_to_receive_ntfs">بعد از شروع مجدد برنامه یا تغییر عبارت عبور، از مخزن کلید اندروید برای ذخیره امن عبارت عبور استفاده خواهد شد - اجازه دریافت اعلان‌ها را خواهد داد.</string> + <string name="database_will_be_encrypted_and_passphrase_stored">پایگاه داده رمزگذاری و عبارت عبور در مخزن کلید ذخیره خواهد شد.</string> + <string name="database_encryption_will_be_updated">عبارت عبور رمزگذاری پایگاه داده به‌روز و در مخزن کلید ذخیره خواهد شد.</string> + <string name="enter_correct_passphrase">عبارت عبور درست را وارد کنید.</string> + <string name="store_passphrase_securely">لطفا عبارت عبور را به صورت امن ذخیره کنید، اگر آن را از دست دهید، قادر به تغییرش نخواهید بود.</string> + <string name="restore_passphrase_not_found_desc">عبارت عبور در مخزن کلید پیدا نشد، لطفا به صورت دستی آن را وارد کنید. دلیل این اتفاق ممکن است برگرداندن اطلاعات برنامه با استفاده از یک ابزار پشتیبان‌گیری باشد. اگر این طور نیست، لطفا با توسعه دهندگان تماس بگیرید.</string> + <string name="downgrade_and_open_chat">تنزل و گشودن گپ</string> + <string name="chat_is_stopped_you_should_transfer_database">گپ متوقف شده است. اگر از پیش از این پایگاه داده روی دستگاه دیگری استفاده می‌کردید، بهتر است قبل از شروع گپ، آن را برگردانید.</string> + <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">به این گروه پیوستید. در حال اتصال به عضوی از گروه که از شما دعوت کرد.</string> + <string name="alert_title_group_invitation_expired">دعوت منقضی شد!</string> + <string name="group_invitation_expired">دعوت گروه منقضی شد</string> + <string name="rcv_group_event_invited_via_your_group_link">از طریق لینک گروهتان دعوت شد</string> + <string name="snd_group_event_changed_member_role">شما نقش %s را به %s تغییر دادید</string> + <string name="snd_group_event_member_blocked">شما %s را مسدود کردید</string> + <string name="snd_group_event_member_deleted">شما %1$s را حذف کردید</string> + <string name="profile_update_event_removed_picture">عکس نمایه حذف شد</string> + <string name="profile_update_event_set_new_picture">تعیین عکس نمایه جدید</string> + <string name="profile_update_event_updated_profile">نمایه به‌روز شد</string> + <string name="snd_conn_event_ratchet_sync_required">مذاکره مجدد رمزگذاری برای %s الزامی است</string> + <string name="sending_via">در حال ارسال از طریق</string> + <string name="fix_connection_question">اتصال اصلاح شود؟</string> + <string name="fix_connection_not_supported_by_group_member">اصلاح توسط عضو گروه پشتیبانی نمی‌شود</string> + <string name="group_main_profile_sent">نمایه گپ شما به اعضای گروه ارسال خواهد شد</string> + <string name="button_create_group_link">ایجاد لینک</string> + <string name="error_updating_link_for_group">خطا در به‌روزرسانی لینک گروه</string> + <string name="network_option_protocol_timeout">زمان توقف پروتکل</string> + <string name="you_can_share_this_address_with_your_contacts">می‌توانید این نشانی را با مخاطبان خود به اشتراک بگذارید تا به آن‌ها اجازه دهید به %s متصل شوند.</string> + <string name="send_receipts_disabled">غیرفعال</string> + <string name="send_receipts_disabled_alert_title">رسیدها غیرفعال هستند</string> + <string name="section_title_for_console">برای کنسول</string> + <string name="remove_member_confirmation">حذف</string> + <string name="updating_settings_will_reconnect_client_to_all_servers">به‌روزرسانی تنظیمات کلاینت را دوباره به سرورها متصل خواهد کرد.</string> + <string name="users_delete_question">نمایه گپ حذف شود؟</string> + <string name="make_profile_private">خصوصی کردن نمایه!</string> + <string name="you_can_hide_or_mute_user_profile">می‌توانید نمایه کاربر را پنهان یا بی‌صدا کنید - برای نمایش منو لمس کنید و‍ نگه دارید.</string> + <string name="unhide_profile">لغو پنهان‌سازی نمایه</string> + <string name="unhide_chat_profile">لغو پنهان‌سازی نمایه گپ</string> + <string name="profile_password">کلمه عبور نمایه</string> + <string name="incognito_random_profile">نمایه تصادفی شما</string> + <string name="color_primary">ابتدایی</string> + <string name="color_sent_message">پیام ارسالی</string> + <string name="chat_preferences_no">خیر</string> + <string name="chat_preferences_always">همیشه</string> + <string name="chat_preferences_on">روشن</string> + <string name="button_welcome_message">پیام خوشامدگویی</string> + <string name="remove_member_button">حذف عضو</string> + <string name="theme_system">سیستم</string> + <string name="theme">تم</string> + <string name="color_received_message">پیام دریافتی</string> + <string name="delete_link_question">لینک حذف شود؟</string> + <string name="you_can_share_group_link_anybody_will_be_able_to_connect">می‌توانید یک لینک یا کد QR به اشتراک بگذارید - هر کسی می‌تواند به گروه بپیوندد. اگر بعدا گروه را حذف کنید، اعضای گروه را از دست نخواهید داد.</string> + <string name="error_creating_link_for_group">خطا در ایجاد لینک گروه</string> + <string name="error_sending_message_contact_invitation">خطا در ارسال دعوت</string> + <string name="share_address">اشتراک‌گذاری نشانی</string> + <string name="send_receipts_disabled_alert_msg">این گروه بیش از %1$d عضو دارد، رسیدهای تحویل ارسال نمی‌شوند.</string> + <string name="info_row_local_name">نام محلی</string> + <string name="info_row_database_id">شناسه پایگاه داده</string> + <string name="info_row_created_at">ایجاد شد در</string> + <string name="share_text_received_at">دریافت شد در: %s</string> + <string name="share_text_deleted_at">حذف شد در: %s</string> + <string name="share_text_moderated_at">توسط مدیر حذف شد در: %s</string> + <string name="share_text_disappears_at">ناپدید می‌شود در: %s</string> + <string name="button_remove_member_question">عضو حذف شود؟</string> + <string name="button_remove_member">حذف عضو</string> + <string name="block_member_button">مسدودسازی عضو</string> + <string name="block_member_confirmation">مسدودسازی</string> + <string name="block_for_all_question">عضو برای همه مسدود شود؟</string> + <string name="unblock_member_confirmation">لغو مسدودسازی</string> + <string name="unblock_for_all_question">مسدودسازی عضو برای همه لغو شود؟</string> + <string name="member_blocked_by_admin">مسدود شده توسط مدیر</string> + <string name="member_info_member_blocked">مسدود</string> + <string name="change_verb">تغییر</string> + <string name="switch_verb">تعویض</string> + <string name="change_member_role_question">نقش گروه تغییر داده شود؟</string> + <string name="connect_via_member_address_alert_title">اتصال مستقیم؟</string> + <string name="connect_via_member_address_alert_desc">درخواست اتصال به این عضو گروه ارسال خواهد شد.</string> + <string name="save_welcome_message_question">پیام خوشامدگویی ذخیره شود؟</string> + <string name="renegotiate_encryption">مذاکره مجدد رمزگذاری</string> + <string name="save_group_profile">ذخیره نمایه گروه</string> + <string name="update_network_settings_confirmation">به‌روزرسانی</string> + <string name="you_will_still_receive_calls_and_ntfs">شما هنوز تماس‌ها و اعلان‌های نمایه‌های بی‌صدا را وقتی فعال هستند دریافت می‌کنید.</string> + <string name="incognito">ناشناس</string> + <string name="language_system">سیستم</string> + <string name="import_theme">وارد کردن تم</string> + <string name="chat_preferences_you_allow">شما اجازه می‌دهید</string> + <string name="chat_preferences_default">پیش‌فرض (%s)</string> + <string name="delete_group_for_self_cannot_undo_warning">گروه برای شما حذف خواهد شد - این عمل قابل برگشت نیست!</string> + <string name="button_leave_group">ترک گروه</string> + <string name="group_link">لینک گروه</string> + <string name="address_section_title">نشانی</string> + <string name="receiving_via">در حال دریافت از طریق</string> + <string name="user_mute">بی‌صدا</string> + <string name="reset_color">بازنشاندن رنگ‌ها</string> + <string name="fix_connection_confirm">اصلاح</string> + <string name="share_text_database_id">شناسه پایگاه داده: %d</string> + <string name="share_text_updated_at">رکورد به‌روز شد در: %s</string> + <string name="item_info_no_text">بدون متن</string> + <string name="button_send_direct_message">ارسال پیام مستقیم</string> + <string name="user_hide">پنهان کردن</string> + <string name="info_row_sent_at">ارسال شد در</string> + <string name="theme_light">روشن</string> + <string name="import_theme_error">خطا در وارد کردن تم</string> + <string name="chat_preferences_contact_allows">مخاطب اجازه می‌دهد</string> + <string name="invite_prohibited_description">شما در حال دعوت از مخاطبی که با او نمایه ناشناسی به اشتراک گذاشته‌اید به گروهی هستید که در آن از نمایه اصلی خود استفاده می‌کنید</string> + <string name="button_delete_group">حذف گروه</string> + <string name="button_add_welcome_message">افزودن پیام خوشامدگویی</string> + <string name="network_status">وضعیت شبکه</string> + <string name="group_profile_is_stored_on_members_devices">نمایه گروه روی دستگاه‌های اعضا ذخیره می‌شود، نه روی سرورها.</string> + <string name="users_delete_with_connections">اتصال‌های نمایه و سرور</string> + <string name="item_info_current">(فعلی)</string> + <string name="member_info_section_title_member">عضو</string> + <string name="member_will_be_removed_from_group_cannot_be_undone">عضو از گروه حذف خواهد شد - این عمل قابل برگشت نیست!</string> + <string name="block_for_all">مسدود برای همه</string> + <string name="fix_connection_not_supported_by_contact">اصلاح توسط مخاطب پشتیبانی نمی‌شود</string> + <string name="network_option_seconds_label">ثانیه</string> + <string name="users_delete_all_chats_deleted">تمام گپ‌ها و پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست!</string> + <string name="users_delete_data_only">فقط اطلاعات نمایه محلی</string> + <string name="button_add_members">دعوت اعضا</string> + <string name="delete_group_question">گروه حذف شود؟</string> + <string name="delete_group_for_all_members_cannot_undo_warning">گروه برای تمام اعضا حذف خواهد شد - این عمل قابل برگشت نیست!</string> + <string name="create_group_link">ایجاد لینک گروه</string> + <string name="error_deleting_link_for_group">خطا در حذف لینک گروه</string> + <string name="info_row_received_at">دریافت شد در</string> + <string name="info_row_deleted_at">حذف شد در</string> + <string name="info_row_moderated_at">توسط مدیر حذف شد در</string> + <string name="share_text_created_at">ایجاد شد در: %s</string> + <string name="block_member_question">عضو مسدود شود؟</string> + <string name="button_edit_group_profile">ویرایش نمایه گروه</string> + <string name="delete_link">حذف لینک</string> + <string name="error_creating_member_contact">خطا در ایجاد مخاطب عضو</string> + <string name="sender_at_ts">%s در %s</string> + <string name="current_version_timestamp">%s (فعلی)</string> + <string name="role_in_group">نقش</string> + <string name="save_and_update_group_profile">ذخیره و به‌روزرسانی نمایه گروه</string> + <string name="incognito_info_share">وقتی نمایه ناشناسی را با کسی به اشتراک می‌گذارید، این نمایه برای گروه‌هایی که شما را به آن‌ها دعوت می‌کند استفاده خواهد شد.</string> + <string name="group_display_name_field">نام گروه را وارد کنید:</string> + <string name="create_group_button">ایجاد گروه</string> + <string name="error_saving_group_profile">خطا در ذخیره نمایه گروه</string> + <string name="network_options_reset_to_defaults">بازنشاندن به پیش‌فرض‌ها</string> + <string name="export_theme">صدور تم</string> + <string name="error_removing_member">خطا در حذف عضو</string> + <string name="error_changing_role">خطا در تغییر نقش</string> + <string name="color_secondary">ثانوی</string> + <string name="share_text_sent_at">ارسال شد در: %s</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="saved_message_title">پیام ذخیره شده</string> + <string name="change_role">تغییر نقش</string> + <string name="delete_profile">حذف نمایه</string> + <string name="group_info_member_you">شما: %1$s</string> + <string name="all_group_members_will_remain_connected">تمام اعضای گروه متصل باقی خواهند ماند.</string> + <string name="only_group_owners_can_change_prefs">تنها صاحبان گروه می‌توانند تنظیمات گروه را تغییر دهند.</string> + <string name="send_receipts">رسیدهای ارسال</string> + <string name="info_row_updated_at">رکورد به‌روز شد در</string> + <string name="info_row_disappears_at">ناپدید می‌شود در</string> + <string name="block_member_desc">تمام پیام‌های %s پنهان خواهند شد!</string> + <string name="unblock_member_question">مسدودسازی عضو لغو شود؟</string> + <string name="unblock_member_button">لغو مسدودسازی عضو</string> + <string name="unblock_for_all">لغو مسدودسازی برای همه</string> + <string name="unblock_member_desc">پیام‌های %s نشان داده خواهند شد!</string> + <string name="member_role_will_be_changed_with_notification">نقش به «%s» تغییر داده خواهد شد. تمام افراد گروه مطلع خواهند شد.</string> + <string name="member_role_will_be_changed_with_invitation">نقش به «%s» تغییر داده خواهد شد. عضو یک دعوت جدید دریافت خواهد کرد.</string> + <string name="error_blocking_member_for_all">خطا در مسدودسازی عضو برای همه</string> + <string name="info_row_group">گروه</string> + <string name="info_row_connection">اتصال</string> + <string name="conn_level_desc_direct">مستقیم</string> + <string name="conn_level_desc_indirect">غیرمستقیم (%1$s)</string> + <string name="group_welcome_title">پیام خوشامدگویی</string> + <string name="welcome_message_is_too_long">پیام خوشامدگویی بیش از حد طولانی است</string> + <string name="group_welcome_preview">پیش‌نمایش</string> + <string name="enter_welcome_message">پیام خوشامدگویی را وارد کنید…</string> + <string name="message_too_large">پیام بیش از حد بزرگ است</string> + <string name="conn_stats_section_title_servers">سرورها</string> + <string name="switch_receiving_address">تغییر نشانی دریافتی</string> + <string name="fix_connection">اصلاح اتصال</string> + <string name="create_secret_group_title">ایجاد گروه محرمانه</string> + <string name="group_is_decentralized">تماما نامتمرکز - قابل مشاهده فقط توسط اعضا.</string> + <string name="group_full_name_field">نام کامل گروه:</string> + <string name="network_option_protocol_timeout_per_kb">زمان توقف پروتکل در کیلوبایت</string> + <string name="network_option_rcv_concurrency">دریافت همزمان</string> + <string name="network_option_tcp_connection_timeout">زمان توقف اتصال TCP</string> + <string name="network_option_ping_interval">وقفه پینگ</string> + <string name="network_option_ping_count">شمار پینگ</string> + <string name="network_option_enable_tcp_keep_alive">فعال کردن زنده نگه‌داشتن TCP</string> + <string name="network_options_save">ذخیره</string> + <string name="update_network_settings_question">تنظیمات شبکه به‌روزرسانی شود؟</string> + <string name="users_add">افزودن نمایه</string> + <string name="user_unhide">لغو پنهان‌سازی</string> + <string name="user_unmute">لغو بی‌صدا</string> + <string name="enter_password_to_show">کلمه عبور را در جستجو وارد کنید</string> + <string name="tap_to_activate_profile">برای فعال‌سازی نمایه لمس کنید.</string> + <string name="dont_show_again">دوباره نمایش داده نشود</string> + <string name="muted_when_inactive">بی‌صدا هنگام غیرفعال بودن!</string> + <string name="delete_chat_profile">حذف نمایه گپ</string> + <string name="incognito_info_protects">حالت ناشناس از حریم خصوصی شما با استفاده از یک نمایه تصادفی جدید برای هر مخاطب محافظت می‌کند.</string> + <string name="incognito_info_allows">اجازه می‌دهد اتصال‌های بی‌نام زیادی داشته باشید بدون اطلاعات مشترک بین آن‌ها در تنها یک نمایه گپ.</string> + <string name="theme_dark">تاریک</string> + <string name="theme_simplex">SimpleX</string> + <string name="dark_theme">تم تاریک</string> + <string name="import_theme_error_desc">مطمئن شوید پرونده دارای ترکیب YAML صحیح است. برای داشتن یک نمونه از ساختار پرونده تم، تم را صادر کنید.</string> + <string name="color_secondary_variant">ثانوی اضافی</string> + <string name="color_primary_variant">ابتدایی اضافی</string> + <string name="color_background">پس‌زمینه</string> + <string name="color_surface">منوها و هشدارها</string> + <string name="color_title">عنوان</string> + <string name="chat_preferences_yes">بله</string> + <string name="contact_preferences">تنظیمات مخاطب</string> + <string name="full_deletion">حذف برای همه</string> + <string name="allow_your_contacts_irreversibly_delete">به مخاطبان خود اجازه حذف پیام‌های ارسالی به صورت غیرقابل برگشت دهید. (۲۴ ساعت)</string> + <string name="allow_your_contacts_adding_message_reactions">به مخاطبان خود اجازه افزودن واکنش‌های پیام می‌دهید.</string> + <string name="only_your_contact_can_send_disappearing">فقط مخاطب شما می‌توانید پیام‌های ناپدید شونده ارسال کنید.</string> + <string name="only_you_can_delete_messages">فقط شما می‌توانید پیام‌ها را به صورت غیرقابل برگشت حذف کنید (مخاطبتان می‌تواند آن‌ها را برای حذف علامت‌گذاری کند). (۲۴ ساعت)</string> + <string name="only_your_contact_can_delete">فقط مخاطبتان می‌تواند پیام‌ها را به صورت غیرقابل برگشت حذف کند (شما می‌توانید آن‌ها را برای حذف علامت‌گذاری کنید). (۲۴ ساعت)</string> + <string name="only_you_can_send_voice">فقط شما می‌توانید پیام‌های صوتی ارسال کنید.</string> + <string name="both_you_and_your_contact_can_add_message_reactions">هر دوی شما و مخاطبتان می‌توانید واکنش‌های پیام اضافه کنید.</string> + <string name="only_you_can_add_message_reactions">فقط شما می‌توانید واکنش‌های پیام اضافه کنید.</string> + <string name="prohibit_sending_disappearing">ارسال پیام‌های ناپدید شونده را منع می‌کنید.</string> + <string name="allow_direct_messages">اجازه ارسال پیام‌های مستقیم را به اعضا می‌دهید.</string> + <string name="allow_to_send_files">اجازه ارسال پرونده‌ها و رسانه را می‌دهید.</string> + <string name="group_members_can_delete">اعضای گروه می‌توانند پیام‌های ارسالی را به صورت غیرقابل برگشت حذف کنند. (۲۴ ساعت)</string> + <string name="delete_after">حذف بعد از</string> + <string name="chat_preferences">تنظیمات گفت‌و‌گو</string> + <string name="files_and_media">پرونده‌ها و رسانه</string> + <string name="audio_video_calls">تماس‌های صوتی/تصویری</string> + <string name="available_in_v51">" +\nموجود در نسخه 5.1"</string> + <string name="allow_your_contacts_to_send_voice_messages">به مخاطبان خود اجازه ارسال پیام‌های صوتی می‌دهید.</string> + <string name="allow_irreversible_message_deletion_only_if">فقط زمانی اجازه حذف پیام‌ها به صورت غیرقابل برگشت را می‌دهید که مخاطب شما این اجازه را به شما بدهد. (۲۴ ساعت)</string> + <string name="disappearing_messages_are_prohibited">پیام‌های ناپدید شونده در این گروه ممنوع هستند.</string> + <string name="set_group_preferences">تعیین تنظیمات گروه</string> + <string name="message_reactions">واکنش‌های پیام</string> + <string name="message_deletion_prohibited">حذف پیام به صورت غیرقابل برگشت در این گپ ممنوع است.</string> + <string name="ttl_week">%d هفته</string> + <string name="allow_your_contacts_to_send_disappearing_messages">به مخاطبان خود اجازه ارسال پیام‌های ناپدید شونده دهید.</string> + <string name="allow_voice_messages_only_if">فقط وقتی پیام‌های صوتی را مجاز می‌دانید که مخاطب شما آن‌ها را مجاز می‌داند.</string> + <string name="message_reactions_prohibited_in_this_chat">واکنش‌های پیام در این گپ ممنوع هستند.</string> + <string name="prohibit_sending_voice">ارسال پیام‌های صوتی را منع می‌کنید.</string> + <string name="group_members_can_send_voice">اعضای گروه می‌توانند پیام‌های صوتی ارسال کنند.</string> + <string name="accept_feature">پذیرفتن</string> + <string name="both_you_and_your_contact_can_send_disappearing">هر دوی شما و مخاطبتان می‌توانید پیام‌های ناپدید شونده ارسال کنید.</string> + <string name="only_you_can_send_disappearing">فقط شما می‌توانید پیام‌های ناپدید شونده ارسال کنید.</string> + <string name="prohibit_message_deletion">حذف پیام به صورت غیرقابل برگشت را منع می‌کنید.</string> + <string name="prohibit_message_reactions_group">واکنش‌های پیام‌ها را منع می‌کنید.</string> + <string name="enable_sending_recent_history">ارسال ۱۰۰ پیام آخر به اعضای جدید.</string> + <string name="recent_history_is_sent_to_new_members">تا ۱۰۰ پیام آخر به اعضای جدید ارسال خواهد شد.</string> + <string name="ttl_months">%d ماه</string> + <string name="ttl_mth">%d ماه</string> + <string name="ttl_m">%d دقیقه</string> + <string name="allow_calls_only_if">فقط وقتی تماس‌ها را مجاز می‌دانید که مخاطب شما آن‌ها را مجاز می‌داند.</string> + <string name="prohibit_calls">منع تماس‌های صوتی/تصویری.</string> + <string name="prohibit_sending_files">ارسال پرونده‌ها و رسانه را منع می‌کنید.</string> + <string name="allow_to_send_simplex_links">اجازه ارسال لینک‌های SimpleX را می‌دهید.</string> + <string name="recent_history_is_not_sent_to_new_members">تاریخچه به اعضای جدید ارسال نمی‌شود.</string> + <string name="group_members_can_add_message_reactions">اعضای گروه می‌توانند واکنش‌های پیام اضافه کنند.</string> + <string name="group_members_can_send_simplex_links">اعضای گروه می‌توانند لینک‌های SimpleX ارسال کنند.</string> + <string name="simplex_links_are_prohibited_in_group">لینک‌های SimpleX در این گروه ممنوع هستند.</string> + <string name="ttl_s">%d ثانیه</string> + <string name="ttl_min">%d دقیقه</string> + <string name="ttl_month">%d ماه</string> + <string name="feature_roles_admins">مدیران</string> + <string name="feature_roles_owners">صاحبان</string> + <string name="new_in_version">جدید در %s</string> + <string name="whats_new_read_more">مطالعه بیشتر</string> + <string name="both_you_and_your_contact_can_send_voice">هر دوی شما و مخاطبتان می‌توانید پیام‌های صوتی ارسال کنید.</string> + <string name="allow_message_reactions_only_if">فقط وقتی واکنش‌های پیام را مجاز می‌دانید که مخاطب شما آن‌ها را مجاز می‌داند.</string> + <string name="prohibit_message_reactions">منع واکنش‌های پیام.</string> + <string name="allow_disappearing_messages_only_if">فقط زمانی پیام‌های ناپدید شونده را مجاز می‌دانید که مخاطب شما آن‌ها را مجاز بداند.</string> + <string name="prohibit_sending_disappearing_messages">ارسال پیام‌های ناپدید شونده را منع می‌کنید.</string> + <string name="only_your_contact_can_send_voice">فقط مخاطبتان می‌تواند پیام‌های صوتی ارسال کند.</string> + <string name="ttl_d">%d روز</string> + <string name="contacts_can_mark_messages_for_deletion">مخاطبان می‌توانند پیام‌ها را برای حذف علامت بگذارند؛ شما قادر به مشاهده آن‌ها خواهید بود.</string> + <string name="chat_preferences_off">خاموش</string>` + <string name="group_preferences">تنظیمات گروه</string> + <string name="your_preferences">تنظیمات شما</string> + <string name="timed_messages">پیام‌های ناپدید شونده</string> + <string name="direct_messages">پیام‌های مستقیم</string> + <string name="voice_messages">پیام‌های صوتی</string> + <string name="simplex_links">لینک‌های SimpleX</string> + <string name="recent_history">تاریخچه قابل رویت</string> + <string name="feature_enabled">فعال</string> + <string name="feature_enabled_for_you">فعال برای شما</string> + <string name="feature_enabled_for_contact">فعال برای مخاطب</string> + <string name="feature_off">خاموش</string> + <string name="feature_received_prohibited">دریافتی، ممنوع</string> + <string name="accept_feature_set_1_day">تعیین ۱ روز</string> + <string name="prohibit_sending_voice_messages">منع ارسال پیام‌ها صوتی.</string> + <string name="allow_your_contacts_to_call">به مخاطبان خود اجازه تماس با شما را می‌دهید.</string> + <string name="disappearing_prohibited_in_this_chat">پیام‌های ناپدید شونده در این گپ ممنوع هستند.</string> + <string name="both_you_and_your_contacts_can_delete">هر دوی شما و مخاطبتان می‌توانید پیام‌ها را به صورتی غیرقابل برگشت حذف کنید. (۲۴ ساعت)</string> + <string name="both_you_and_your_contact_can_make_calls">هر دوی شما و مخاطبتان می‌توانید تماس برقرار کنید.</string> + <string name="only_you_can_make_calls">فقط شما می‌توانید تماس برقرار کنید.</string> + <string name="only_your_contact_can_make_calls">فقط مخاطبتان می‌تواند تماس برقرار کند.</string> + <string name="calls_prohibited_with_this_contact">تماس‌های صوتی/تصویری ممنوع هستند.</string> + <string name="allow_to_send_disappearing">اجازه ارسال پیام‌های ناپدید شونده می‌دهید.</string> + <string name="allow_to_send_voice">اجازه ارسال پیام‌های صوتی را می‌دهید.</string> + <string name="allow_message_reactions">اجازه واکنش‌های پیام را می‌دهید.</string> + <string name="prohibit_sending_simplex_links">ارسال لینک‌های SimpleX را منع می‌کنید</string> + <string name="disable_sending_recent_history">عدم ارسال تاریخچه به اعضای جدید.</string> + <string name="group_members_can_send_disappearing">اعضای گروه می‌توانند پیام‌های ناپدید شونده ارسال کنند.</string> + <string name="group_members_can_send_dms">اعضای گروه می‌توانند پیام‌های مستقیم ارسال کنند.</string> + <string name="direct_messages_are_prohibited_in_group">پیام‌های مستقیم بین اعضا در این گروه ممنوع هستند.</string> + <string name="message_deletion_prohibited_in_chat">حذف غیرقابل برگشت در این گروه ممنوع است.</string> + <string name="voice_messages_are_prohibited">پیام‌های صوتی در این گروه ممنوع هستند.</string> + <string name="message_reactions_are_prohibited">واکنش‌های پیام در این گروه ممنوع هستند.</string> + <string name="group_members_can_send_files">اعضای گروه می‌توانند پرونده‌ها و رسانه ارسال کنند.</string> + <string name="files_are_prohibited_in_group">پرونده‌ها و رسانه در این گروه ممنوع هستند.</string> + <string name="ttl_sec">%d ثانیه</string> + <string name="ttl_hour">%d ساعت</string> + <string name="ttl_hours">%d ساعت</string> + <string name="ttl_h">%d ساعت</string> + <string name="ttl_day">%d روز</string> + <string name="ttl_days">%d روز</string> + <string name="ttl_weeks">%d هفته</string> + <string name="ttl_w">%d هفته</string> + <string name="feature_offered_item">پیشنهاد %s</string> + <string name="feature_offered_item_with_param">پیشنهاد %s: %2s</string> + <string name="feature_cancelled_item">لغو %s</string> + <string name="feature_roles_all_members">تمام اعضا</string> + <string name="feature_enabled_for">فعال برای</string> + <string name="whats_new">چی جدید است</string> + <string name="voice_prohibited_in_this_chat">پیام‌های صوتی در این گپ ممنوع هستند.</string> + <string name="only_your_contact_can_add_message_reactions">فقط مخاطبتان می‌تواند واکنش‌های پیام اضافه کند.</string> + <string name="allow_to_delete_messages">اجازه حذف پیام‌های ارسالی به صورت غیرقابل برگشت را می‌دهید. (۲۴ ساعت)</string> + <string name="prohibit_direct_messages">ارسال پیام‌های مستقیم به اعضا را منع می‌کنید.</string> + <string name="snd_error_quota">ظرفیت از محدودیت فراتر رفت - گیرنده پیام‌های ارسالی پیشین را دریافت نکرد.</string> + <string name="snd_error_relay">خطای سرور مقصد: %1$s</string> + <string name="ci_status_other_error">خطا: %1$s</string> + <string name="connect_plan_already_joining_the_group">هم اکنون در حال پیوستن به گروه هستید!</string> + <string name="agent_critical_error_title">خطای بسیار مهم</string> + <string name="desktop_connection_terminated">اتصال خاتمه یافت</string> + <string name="network_smp_proxy_mode_never_description">از مسیریابی خصوصی استفاده نشود.</string> + <string name="delivery_receipts_title">رسیدهای تحویل!</string> + <string name="dont_enable_receipts">فعال نشود</string> + <string name="error_enabling_delivery_receipts">خطا در فعال‌سازی رسیدهای تحویل!</string> + <string name="devices">دستگاه‌ها</string> + <string name="disconnect_desktop_question">اتصال کامپیوتر قطع شود؟اتصال کامپیوتر قطع شود؟</string> + <string name="remote_ctrl_disconnected_with_reason">قطع شد، به دلیل: %s</string> + <string name="desktop_app_version_is_incompatible">نسخه برنامه کامپیوتر %s با این برنامه سازگار نیست.</string> + <string name="waiting_for_desktop">در انتظار کامپیوتر…</string> + <string name="connect_to_desktop">اتصال به کامپیوتر</string> + <string name="linked_desktops">کامپیوترهای متصل</string> + <string name="remote_ctrl_error_disconnected">اتصال کامپیوتر قطع شد</string> + <string name="remote_host_error_bad_state"><![CDATA[اتصال به موبایل <b>%s</b> در وضع بدی است]]></string> + <string name="connect_plan_connect_to_yourself">به خودتان متصل می‌شوید؟</string> + <string name="migrate_to_device_error_downloading_archive">خطا در بارگیری آرشیو</string> + <string name="migrate_from_device_cancel_migration">لغو جابه‌جایی</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>هشدار</b>: آرشیو حذف خواهد شد.]]></string> + <string name="connect_plan_already_connecting">هم اکنون در حال اتصال هستید!</string> + <string name="network_smp_proxy_mode_always">همیشه</string> + <string name="network_smp_proxy_mode_always_description">همیشه از مسیریابی خصوصی استفاده شود.</string> + <string name="migrate_to_device_apply_onion">اعمال</string> + <string name="v5_0_app_passcode">کد عبور برنامه</string> + <string name="v4_6_audio_video_calls">تماس‌های صوتی و تصویری</string> + <string name="v4_3_improved_server_configuration_desc">سرورها را به وسیله اسکن کد QR اضافه کنید.</string> + <string name="v5_6_safer_groups_descr">مدیران می‌توانند یک عضو را برای همه مسدود کنند.</string> + <string name="network_smp_proxy_fallback_allow_downgrade">اجازه تنزل</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>لطفا توجه داشته باشید</b>: به منظور حفاظت امنیت، استفاده از پایگاه داده یکسان در دو دستگاه، رمزگشایی پیام‌های اتصال‌های شما را از کار خواهد انداخت.]]></string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[از دستگاه جدید <i>جابه‌جایی از دستگاهی دیگر</i> را انتخاب و کد QR را اسکن کنید.]]></string> + <string name="migrate_to_device_confirm_network_settings">تایید تنظیمات شبکه</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">تایید کنید که عبارت عبور پایگاه داده رای برای جابه‌جایی آن به خاطر دارید.</string> + <string name="connected_desktop">کامپیوتر متصل شد</string> + <string name="connecting_to_desktop">در حال اتصال به کامپیوتر</string> + <string name="remote_host_was_disconnected_title">اتصال متوقف شد</string> + <string name="remote_ctrl_error_inactive">کامپیوتر غیرفعال است</string> + <string name="remote_ctrl_was_disconnected_title">اتصال متوقف شد</string> + <string name="desktop_devices">دستگاه‌های کامپیوتر</string> + <string name="found_desktop">کامپیوتر پیدا شد</string> + <string name="connected_to_desktop">به کامپیوتر متصل شد</string> + <string name="linked_desktop_options">تنظیمات کامپیوتر متصل</string> + <string name="scan_qr_code_from_desktop">اسکن کد QR از کامپیوتر</string> + <string name="remote_ctrl_error_bad_invitation">کامپیوتر دارای کد دعوت اشتباه است</string> + <string name="remote_ctrl_error_bad_version">کامپیوتر دارای نسخه پشتیبانی نشده است. لطفا، اطمینان حاصل کنید که از نسخه یکسان روی هر دو دستگاه استفاده می‌کنید.</string> + <string name="connect_plan_connect_via_link">از طریق لینک متصل می‌شوید؟</string> + <string name="delivery_receipts_are_disabled">رسیدهای تحویل غیرفعال هستند!</string> + <string name="migrate_to_device_downloading_details">در حال بارگیری جزئیات لینک</string> + <string name="migrate_to_device_enter_passphrase">عبارت عبور را وارد کنید</string> + <string name="migrate_from_device_error_deleting_database">خطا در حذف پایگاه داده</string> + <string name="v4_4_verify_connection_security_desc">کدهای امنیتی را با مخاطبان خود مقایسه کنید.</string> + <string name="v5_3_encrypt_local_files_descr">برنامه پرونده‌های جدید محلی (به جز ویدئوها) را رمزگذاری می‌کند.</string> + <string name="v5_3_encrypt_local_files">رمزگذاری پرونده‌ها و رسانه ذخیره شده</string> + <string name="v5_4_link_mobile_desktop_descr">به وسیله پروتکل امن مقاوم در برابر کوانتوم.</string> + <string name="v5_4_block_group_members">مسدودسازی اعضای گروه</string> + <string name="error">خطا</string> + <string name="remote_host_disconnected_from"><![CDATA[اتصال از موبایل <b>%s</b> قطع شد، به دلیل: %s]]></string> + <string name="desktop_device">کامپیوتر</string> + <string name="v4_6_chinese_spanish_interface">رابط چینی و اسپانیایی</string> + <string name="v4_5_transport_isolation_descr">به وسیله نمایه گپ (پیش‌فرض) یا به وسیله اتصال (آزمایشی).</string> + <string name="remote_ctrl_error_timeout">در حین اتصال به کامپیوتر، مهلت زمان اتصال تمام شد.</string> + <string name="custom_time_unit_days">روز‍</string> + <string name="enable_receipts_all">فعال کردن</string> + <string name="custom_time_picker_custom">سفارشی</string> + <string name="disconnect_remote_host">قطع اتصال</string> + <string name="verify_code_with_desktop">تایید کد با کامپیوتر</string> + <string name="migrate_from_device_error_uploading_archive">خطا در بارگذاری آرشیو</string> + <string name="migrate_from_device_archiving_database">در حال آرشیو پایگاه داده</string> + <string name="migrate_from_device_creating_archive_link">ایجاد لینک آرشیو</string> + <string name="migrate_from_device_delete_database_from_device">حذف پایگاه داده از این دستگاه</string> + <string name="remote_ctrl_error_bad_state">اتصال به کامپیوتر در وضع بدی است</string> + <string name="v4_5_multiple_chat_profiles_descr">نام‌ها، آواتارها و انزوای ترابری متفاوت.</string> + <string name="v5_2_more_things">چند چیز دیگر</string> + <string name="remote_ctrl_error_busy">کامپیوتر مشغول است</string> + <string name="v4_4_disappearing_messages">پیام‌های ناپدید شونده</string> + <string name="unlink_desktop">قطع اتصال</string> + <string name="v5_1_better_messages">پیام‌های بهتر</string> + <string name="multicast_connect_automatically">اتصال به صورت خودکار</string> + <string name="multicast_discoverable_via_local_network">قابل کشف از طریق شبکه محلی</string> + <string name="network_type_cellular">سلولی</string> + <string name="v5_3_new_desktop_app_descr">ایجاد نمایه جدید در برنامه کامپیوتر. 💻</string> + <string name="desktop_address">نشانی کامپیوتر</string> + <string name="paste_desktop_address">الصاق نشانی کامپیوتر</string> + <string name="discover_on_network">یافتن از طریق شبکه محلی</string> + <string name="v5_1_self_destruct_passcode_descr">تمام اطلاعات وقتی وارد می‌شوند پاک می‌شوند.</string> + <string name="v5_1_custom_themes_descr">سفارشی کردن و اشتراک‌گذاری تم‌های رنگ.</string> + <string name="v5_1_custom_themes">تم‌های سفارشی</string> + <string name="v5_3_discover_join_groups_descr">- اتصال به سرویس فهرست راهنما (آزمایشی)! +\n- رسیدهای تحویل (تا ۲۰ دقیقه). +\n- سریع‌تر و پایداری بیشتر.</string> + <string name="v5_4_better_groups">گروه‌های بهتر</string> + <string name="v5_6_quantum_resistant_encryption_descr">فعال کردن در گپ های مستقیم (آزمایشی)!</string> + <string name="unlink_desktop_question">اتصال با کامپیوتر قطع شود؟</string> + <string name="in_developing_title">به زودی!</string> + <string name="migrate_to_device_download_failed">بارگیری موفق نبود</string> + <string name="migrate_to_device_chat_migrated">گپ جابه‌جا شد!</string> + <string name="migrate_from_device_archive_and_upload">آرشیو و بارگذاری</string> + <string name="migrate_from_device_confirm_upload">تایید بارگذاری</string> + <string name="migrate_from_device_check_connection_and_try_again">اتصال اینترنت خود را بررسی و دوباره امتحان کنید</string> + <string name="v5_3_new_desktop_app">برنامه کامپیوتر جدید</string> + <string name="v5_4_link_mobile_desktop">وصل کردن برنامه‌های موبایل و کامپیوتر! 🔗</string> + <string name="v5_3_discover_join_groups">یافتن و پیوستن به گروه‌ها</string> + <string name="v5_4_incognito_groups_descr">ایجاد یک گروه با استفاده از یک نمایه تصادفی.</string> + <string name="v5_6_app_data_migration">جابه‌جایی اطلاعات برنامه</string> + <string name="open_on_mobile_and_scan_qr_code"><![CDATA[<i>استفاده از کامپیوتر</i> را در برنامه موبایل باز و کد QR را اسکن کنید.]]></string> + <string name="migrate_to_device_downloading_archive">در حال بارگیری آرشیو</string> + <string name="migrate_from_device_error_exporting_archive">خطا در صدور پایگاه داده گپ</string> + <string name="migrate_from_device_error_saving_settings">خطا در ذخیره تنظیمات</string> + <string name="migrate_from_device_all_data_will_be_uploaded">تمام مخاطبان، مکالمات و پرونده‌های شما به صورت امن، رمزگذاری و به صورت بسته‌های داده به واسطه‌های XFTP تنظیم شده، بارگذاری خواهند شد.</string> + <string name="connected_mobile">موبایل متصل شد</string> + <string name="v4_2_group_links_desc">مدیران می‌توانند لینک‌ها را برای پیوستن به گروه‌ها ایجاد کنند.</string> + <string name="disconnect_remote_hosts">قطع اتصال موبایل‌ها</string> + <string name="connected_to_mobile">به موبایل متصل شد</string> + <string name="enter_this_device_name">نام این دستگاه را وارد کنید…</string> + <string name="new_desktop"><![CDATA[<i>(جدید)</i>]]></string> + <string name="v4_2_auto_accept_contact_requests">پذیرفتن خودکار درخواست‌های مخاطب</string> + <string name="bad_desktop_address">نشانی کامپیوتر ناصحیح</string> + <string name="desktop_incompatible_version">نسخه ناسازگار</string> + <string name="v5_3_new_interface_languages_descr">عربی، بلغاری، فنلاندی، عبری، تایلندی و اوکراینی - با سپاس از کاربران و Weblate</string> + <string name="snd_error_proxy_relay">سرور فرستادن: %1$s +\nخطای سرور مقصد: %2$s</string> + <string name="snd_error_proxy">سرور فرستادن: %1$s +\nخطا: %2$s</string> + <string name="message_delivery_warning_title">هشدار تحویل پیام</string> + <string name="snd_error_expired">مشکلات شبکه - پیام بعد از تلاش‌های زیاد برای ارسالش منقضی شد.</string> + <string name="srv_error_host">نشانی سرور با تنظیمات شبکه ناسازگار است.</string> + <string name="srv_error_version">نسخه سرور با تنظیمات شبکه ناسازگار است.</string> + <string name="snd_error_auth">کلید اشتباه یا اتصال ناشناخته - به احتمال زیاد این اتصال حذف شده است.</string> + <string name="agent_internal_error_desc">لطفا آن را به توسعه‌دهندگان گزارش دهید: +\n%s</string> + <string name="v5_2_fix_encryption">نگه‌داشتن اتصال‌های خود</string> + <string name="network_smp_proxy_mode_never">هرگز</string> + <string name="network_smp_proxy_mode_unknown_description">استفاده از مسیریابی خصوصی با سرورهای ناشناخته.</string> + <string name="network_smp_proxy_mode_unprotected_description">استفاده از مسیریابی خصوصی با سرورهای ناشناخته وقتی نشانی IP محافظت نشده است.</string> + <string name="update_network_smp_proxy_mode_question">حالت مسیریابی پیام</string> + <string name="network_smp_proxy_fallback_allow">بله</string> + <string name="network_smp_proxy_fallback_prohibit_description">پیام‌ها مستقیما فرستاده نشود، حتی اگر سرور مقصد شما از مسیریابی خصوصی پشتیبانی نکند.</string> + <string name="update_network_smp_proxy_fallback_question">گزینه پس‌رفت مسیریابی پیام</string> + <string name="private_routing_show_message_status">نمایش وضعیت پیام</string> + <string name="private_routing_explanation">برای محافظت از نشانی IP شما، مسیریابی خصوصی از سرورهای SMP شما به منظور تحویل پیام‌ها استفاده می‌کند.</string> + <string name="settings_section_title_private_message_routing">مسیریابی پیام خصوصی</string> + <string name="network_smp_proxy_mode_private_routing">مسیریابی خصوصی</string> + <string name="v4_3_improved_server_configuration">تنظیمات سرور بهبودیافته</string> + <string name="v4_2_auto_accept_contact_requests_desc">با پیام خوشامدگویی اختیاری.</string> + <string name="v4_3_irreversible_message_deletion_desc">مخاطبان شما می‌توانند اجازه حذف کامل پیام را بدهند.</string> + <string name="v4_5_multiple_chat_profiles">چندین نمایه گپ</string> + <string name="v4_5_private_filenames">نام‌های پرونده خصوصی</string> + <string name="v5_2_favourites_filter_descr">پالایش گپ‌های خوانده نشده و برگزیده.</string> + <string name="v5_2_fix_encryption_descr">اصلاح رمزگذاری بعد از برگرداندن پشتیبان‌ها.</string> + <string name="v4_6_group_moderation">مدیریت گروه</string> + <string name="v5_1_japanese_portuguese_interface">رابط کاربری ژاپنی و پرتقالی</string> + <string name="v5_2_disappear_one_message">ناپدید کردن یک پیام</string> + <string name="v4_5_reduced_battery_usage">استفاده باتری کاهش یافته</string> + <string name="v5_0_app_passcode_descr">به جای تصدیق سیستم آن را تعیین کنید.</string> + <string name="v4_6_audio_video_calls_descr">پشتیبانی از بلوتوث و دیگر بهبودها.</string> + <string name="v5_1_better_messages_descr">- پیام‌های صوتی تا ۵ دقیقه. +\n- زمان سفارشی برای ناپدید کردن. +\n- ویرایش تاریخچه.</string> + <string name="v5_4_better_groups_descr">پیوستن سریع‌تر و پیام‌های قابل اطمینان تر.</string> + <string name="v5_7_forward">باز فرستادن و ذخیره پیام‌ها</string> + <string name="v5_7_shape_profile_images_descr">مربع، دایره، و هر چیزی در این بین.</string> + <string name="v5_7_quantum_resistant_encryption_descr">در گپ‌های مستقیم فعال خواهد شد!</string> + <string name="sending_delivery_receipts_will_be_enabled">ارسال رسیدهای تحویل برای تمام مخاطبان فعال خواهد شد.</string> + <string name="you_can_enable_delivery_receipts_later">می‌توانید بعدا از طریق تنظیمات آن را فعال کنید</string> + <string name="this_device_name">نام این دستگاه</string> + <string name="verify_code_on_mobile">تایید کد در موبایل</string> + <string name="remote_host_was_disconnected_toast"><![CDATA[موبایل <b>%s</b> قطع شد]]></string> + <string name="this_device">این دستگاه</string> + <string name="waiting_for_mobile_to_connect">در انتظار متصل شدن موبایل:</string> + <string name="remote_host_error_busy"><![CDATA[موبایل <b>%s</b> مشغول است]]></string> + <string name="random_port">تصادفی</string> + <string name="refresh_qr_code">تجدید</string> + <string name="remote_host_error_bad_version"><![CDATA[موبایل <b>%s</b> نسخه پشتیبانی نشده دارد. لطفا، اطمینان حاصل کنید که از نسخه یکسان روی هر دو دستگاه استفاده می‌کنید]]></string> + <string name="remote_host_error_disconnected"><![CDATA[اتصال موبایل <b>%s</b> قطع شد]]></string> + <string name="in_developing_desc">این ویژگی هنوز پشتیبانی نمی‌شود. انتشار بعدی را امتحان کنید.</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[شما هم اکنون در حال اتصال به <b>%1$s</b> هستید.]]></string> + <string name="connect_plan_group_already_exists">گروه از قبل وجود دارد!</string> + <string name="connect_plan_you_are_already_joining_the_group_via_this_link">شما هم اکنون در حال پیوستن به گروه از طریق این لینک هستید.</string> + <string name="migrate_to_device_importing_archive">در حال وارد کردن آرشیو</string> + <string name="or_paste_archive_link">یا لینک آرشیو را الصاق کنید</string> + <string name="paste_archive_link">الصاق لینک آرشیو</string> + <string name="migrate_to_device_try_again">می‌توانید دوباره امتحان کنید.</string> + <string name="migrate_to_device_file_delete_or_link_invalid">پرونده حذف شد یا لینک نامعتبر است</string> + <string name="migrate_from_device_title">جابه‌جایی دستگاه</string> + <string name="migrate_from_device_database_init">در حال آماده‌سازی بارگذاری</string> + <string name="migrate_from_device_finalize_migration">نهایی‌سازی جابه‌جایی</string> + <string name="migrate_from_device_or_share_this_file_link">یا لینک پرونده را به صورت امن به اشتراک بگذارید</string> + <string name="migrate_from_device_start_chat">شروع گپ</string> + <string name="network_type_ethernet">اترنت باسیم</string> + <string name="v4_5_reduced_battery_usage_descr">بهبودهای بیشتر به زودی!</string> + <string name="not_compatible">سازگار نیست!</string> + <string name="verify_connections">تایید اتصال‌ها</string> + <string name="remote_host_error_inactive"><![CDATA[موبایل <b>%s</b> غیرفعال است]]></string> + <string name="connect_plan_you_have_already_requested_connection_via_this_address">شما از پیش اتصال به وسیله این نشانی را درخواست کرده‌اید!</string> + <string name="restart_chat_button">شروع مجدد گپ</string> + <string name="v5_2_disappear_one_message_descr">حتی وقتی در مکالمه غیرفعال باشند.</string> + <string name="network_smp_proxy_fallback_prohibit">خیر</string> + <string name="no_connected_mobile">موبایل متصلی وجود ندارد</string> + <string name="v4_6_group_moderation_descr">مدیران حالا می‌توانند: +\n- پیام‌های اعضا را حذف کنند. +\n- اعضا را غیرفعال کنند ( نقش «ناظر»)</string> + <string name="migrate_to_device_confirm_network_settings_footer">لطفا تایید کنید که تنظیمات شبکه برای این دستگاه درست هستند.</string> + <string name="v4_5_message_draft_descr">محفوظ نگه داشتن پیش‌نویس پیام آخر، به همراه ضمیمه‌ها.</string> + <string name="connect_plan_repeat_connection_request">درخواست اتصال تکرار شود؟</string> + <string name="scan_from_mobile">از موبایل اسکن کنید</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">ارسال رسیدهای تحویل برای تمام مخاطبان در تمام نمایه‌های گپ قابل مشاهده، فعال خواهد شد.</string> + <string name="network_smp_proxy_fallback_allow_protected_description">پیام‌ها مستقیما ارسال شود وقتی نشانی IP محافظت می‌شود و سرور مقصد شما از مسیریابی خصوصی پشتیبانی نمی‌کند.</string> + <string name="network_smp_proxy_fallback_allow_description">پیام‌ها مستقیما ارسال شود وقتی سرور مقصد شما از مسیریابی خصوصی پشتیبانی نمی‌کند.</string> + <string name="v5_7_shape_profile_images">شکل دادن به تصاویر نمایه</string> + <string name="v4_4_french_interface_descr">با سپاس از کاربران - از طریق Weblate همکاری کنید!</string> + <string name="v4_5_italian_interface_descr">با سپاس از کاربران - از طریق Weblate همکاری کنید!</string> + <string name="connect_plan_this_is_your_own_one_time_link">این لینک یک‌بارمصرف خودتان است!</string> + <string name="connect_plan_this_is_your_own_simplex_address">این نشانی‌ SimpleX خودتان است!</string> + <string name="network_smp_proxy_mode_unknown">واسطه‌های ناشناخته</string> + <string name="network_smp_proxy_mode_unprotected">محافظت نشده</string> + <string name="verify_connection">تایید اتصال</string> + <string name="migrate_from_device_verify_database_passphrase">تایید عبارت عبور پایگاه داده</string> + <string name="network_smp_proxy_fallback_allow_protected">وقتی IP پنهان است</string> + <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">شما هم اکنون در حال اتصال از طریق لینک یک‌بارمصرف هستید!</string> + <string name="v4_6_hidden_chat_profiles">نمایه‌های گپ پنهان</string> + <string name="this_device_version"><![CDATA[<i>(این دستگاه v%s)</i>]]></string> + <string name="link_a_mobile">متصل کردن یک موبایل</string> + <string name="v4_5_message_draft">پیش‌نویس پیام</string> + <string name="v4_3_voice_messages_desc">حداکثر ۴۰ ثانیه، دریافت فوری.</string> + <string name="v4_3_voice_messages">پیام‌های صوتی</string> + <string name="v4_4_disappearing_messages_desc">پیام‌های ارسال شده بعد زمان تعیین شده حذف خواهند شد.</string> + <string name="v4_4_french_interface">رابط فرانسوی</string> + <string name="v4_6_hidden_chat_profiles_descr">به وسیله یک کلمه عبور از نمایه‌های گپ خود محافظت کنید!</string> + <string name="v4_6_reduced_battery_usage_descr">بهبودهای بیشتر به زودی!</string> + <string name="whats_new_thanks_to_users_contribute_weblate">با سپاس از کاربران - از طریق Weblate همکاری کنید!</string> + <string name="v5_2_message_delivery_receipts">رسیدهای تحویل پیام!</string> + <string name="remote_host_error_timeout"><![CDATA[مهلت زمانی هنگام اتصال به موبایل <b>%s</b> به پایان رسید]]></string> + <string name="agent_critical_error_desc">لطفا آن را به توسعه‌دهندگان گزارش دهید: +\n%s +\n +\nپیشنهاد می‌شود که برنامه را شروع مجدد کنید.</string> + <string name="v4_5_italian_interface">رابط ایتالیایی</string> + <string name="v5_2_more_things_descr">- تحویل پیام پایدارتر +\n- گروه‌های کمی بهتر +\n- و بیشتر!</string> + <string name="v5_1_self_destruct_passcode">کد عبور خودتخریبی</string> + <string name="v4_6_chinese_spanish_interface_descr">با سپاس از کاربران - از طریق Weblate همکاری کنید!</string> + <string name="v5_0_large_files_support">ویدئوها و پرونده‌ها تا ۱ گیگابایت</string> + <string name="migrate_from_device_verify_passphrase">تایید عبارت عبور</string> + <string name="invalid_file_link">لینک نامعتبر</string> + <string name="v5_7_call_sounds_descr">هنگام برقراری تماس‌های صوتی و تصویری.</string> + <string name="v5_7_call_sounds">صداهای تماس</string> + <string name="v5_7_forward_descr">منبع پیام خصوصی باقی خواهد ماند.</string> + <string name="v5_7_new_interface_languages">رابط کاربری لیتوانی</string> + <string name="custom_time_unit_minutes">دقیقه</string> + <string name="v5_7_network_descr">اتصال شبکه پایدارتر.</string> + <string name="v5_7_network">مدیریت شبکه</string> + <string name="custom_time_unit_weeks">هفته</string> + <string name="custom_time_unit_months">ماه</string> + <string name="custom_time_picker_select">انتخاب</string> + <string name="new_mobile_device">دستگاه موبایل جدید</string> + <string name="migrate_from_device_stopping_chat">در حال متوقف کردن گپ</string> + <string name="network_type_no_network_connection">بدون اتصال شبکه</string> + <string name="network_type_other">دیگر</string> + <string name="v4_4_verify_connection_security">تایید امنیت اتصال</string> + <string name="migrate_to_device_database_init">در حال آماده‌سازی بارگیری</string> + <string name="migrate_from_device_migration_complete">جابه‌جایی کامل شد</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[شما <b>نباید</b> از یک پایگاه داده روی دو دستگاه استفاده کنید.]]></string> + <string name="v4_6_group_welcome_message">پیام خوشامدگویی گروه</string> + <string name="v5_4_more_things_descr">- مطلع کردن اختیاری مخاطبان حذف شده. +\n- نام‌های نمایه شامل فاصله. +\n- و بیشتر!</string> + <string name="v5_5_new_interface_languages">رابط کاربری مجارستانی و ترکی</string> + <string name="v5_6_app_data_migration_descr">جابه‌جایی به دستگاه دیگر از طریق کد QR.</string> + <string name="connect_plan_open_group">گشودن گروه</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="v5_6_picture_in_picture_calls">تماس‌های تصویر در تصویر</string> + <string name="v5_6_picture_in_picture_calls_descr">استفاده از برنامه در حین مکالمه.</string> + <string name="migrate_to_device_migrating">در حال جابه‌جایی</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[این لینک شما است برای گروه <b>%1$s</b>!]]></string> + <string name="v4_3_improved_privacy_and_security">امنیت و حریم خصوصی بهبودیافته</string> + <string name="v4_3_improved_privacy_and_security_desc">پنهان کردن صفحه برنامه در برنامه‌های اخیر.</string> + <string name="v4_4_live_messages">پیام‌های زنده</string> + <string name="v4_4_live_messages_desc">گیرنده‌ها به‌روزرسانی‌ها را هم‌زمان با تایپ کردن شما مشاهده می‌کنند.</string> + <string name="v4_5_private_filenames_descr">برای محافظت از منطقه زمانی، پرونده‌های تصویر/صدا از UTC استفاده می‌کنند.</string> + <string name="v5_0_large_files_support_descr">سریع و بدون منتظر ماندن تا زمانی که فرستنده آنلاین شود.</string> + <string name="v4_6_reduced_battery_usage">کاهش بیشتر استفاده باتری</string> + <string name="v4_6_group_welcome_message_descr">تعیین پیام نمایش داده شده به اعضای جدید!</string> + <string name="v5_0_polish_interface">رابط لهستانی</string> + <string name="v5_0_polish_interface_descr">با سپاس از کاربران - از طریق Weblate همکاری کنید!</string> + <string name="v5_1_message_reactions">واکنش‌های پیام</string> + <string name="v5_1_message_reactions_descr">بالاخره، ما آن‌ها را داریم! 🚀</string> + <string name="v5_2_message_delivery_receipts_descr">تیک دومی که ما نداشتیم! ✅</string> + <string name="v5_4_block_group_members_descr">برای پنهان کردن پیام‌های ناخواسته.</string> + <string name="v5_5_private_notes">یادداشت‌های خصوصی</string> + <string name="v5_5_private_notes_descr">با پرونده‌ها و رسانه رمزگذاری شده.</string> + <string name="v5_5_message_delivery">تحویل پیام بهبود یافته</string> + <string name="custom_time_unit_hours">ساعت</string> + <string name="you_can_enable_delivery_receipts_later_alert">می‌توانید بعدا از طریق تنظیمات حریم خصوصی و امنیت برنامه آن‌ها را فعال کنید.</string> + <string name="open_port_in_firewall_title">گشودن پورت در فایروال</string> + <string name="remote_host_error_missing"><![CDATA[موبایل <b>%s</b> مفقود است]]></string> + <string name="open_port_in_firewall_desc">برای اجازه دادن به برنامه موبایل به کامپیوتر متصل شوید، این پورت را در فایروال خود باز کنید، اگر فعال است</string> + <string name="agent_internal_error_title">خطای داخلی</string> + <string name="migrate_to_device_title">جابه‌جایی به اینجا</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[شما هم اکنون در گروه <b>%1$s</b> هستید.]]></string> + <string name="migrate_to_device_repeat_download">تکرار بارگیری</string> + <string name="migrate_to_device_import_failed">وارد کردن ناموفق بود</string> + <string name="migrate_to_device_repeat_import">تکرار وارد کردن</string> + <string name="migrate_to_device_finalize_migration">نهایی‌سازی جابه‌جایی در دستگاه دیگر.</string> + <string name="migrate_from_device_chat_should_be_stopped">برای ادامه دادن، گپ باید متوقف شود.</string> + <string name="migrate_from_device_repeat_upload">تکرار بارگذاری</string> + <string name="migrate_from_device_bytes_uploaded">%s بارگذاری شد</string> + <string name="migrate_from_device_upload_failed">بارگذاری ناموفق بود</string> + <string name="migrate_from_device_uploading_archive">در حال بارگذاری آرشیو</string> + <string name="migrate_from_device_try_again">می‌توانید دوباره امتحان کنید.</string> + <string name="migrate_from_device_error_verifying_passphrase">خطا در تایید عبارت عبور:</string> + <string name="v4_2_security_assessment">ارزیابی امنیت</string> + <string name="v5_4_incognito_groups">گروه‌های ناشناس</string> + <string name="v5_3_simpler_incognito_mode">حالت ناشناس ساده‌شده</string> + <string name="v5_3_simpler_incognito_mode_descr">تغییر حالت ناشناس هنگام اتصال.</string> + <string name="v5_5_join_group_conversation">پیوستن به مکالمات گروه</string> + <string name="v5_5_simpler_connect_ui">جهت اتصال لینک را الصاق کنید</string> + <string name="v5_5_join_group_conversation_descr">تاریخچه اخیر و روبات فهرست راهنمای بهبودیافته.</string> + <string name="v5_5_simpler_connect_ui_descr">نوار جستجو لینک‌های دعوت قبول می‌کند.</string> + <string name="v5_5_message_delivery_descr">با استفاده باتری کاهش یافته.</string> + <string name="custom_time_unit_seconds">ثانیه</string> + <string name="v5_6_quantum_resistant_encryption">رمزگذاری مقاوم در برابر کوانتوم</string> + <string name="v5_6_safer_groups">گروه‌های امن‌تر</string> + <string name="only_one_device_can_work_at_the_same_time">تنها یک دستگاه در هر زمان می‌تواند مورد استفاده قرار گیرد</string> + <string name="connect_plan_repeat_join_request">درخواست پیوستن تکرار شود؟</string> + <string name="connect_plan_join_your_group">به گروه خود می‌پیوندید؟</string> + <string name="migrate_to_device_bytes_downloaded">%s بارگیری شد</string> + <string name="migrate_from_device_exported_file_doesnt_exist">پرونده صادر شده وجود ندارد</string> + <string name="migrate_from_device_to_another_device">جابه‌جایی به دستگاه دیگر</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">هشدار: شروع گپ روی چندین دستگاه پشتیبانی نمی‌شود و باعث عدم موفقیت در تحویل پیام خواهد شد</string> + <string name="this_device_name_shared_with_mobile">نام دستگاه با کلاینت موبایل متصل شده به اشتراک گذاشته خواهد شد.</string> + <string name="v5_2_favourites_filter">پیدا کردن سریع‌تر گپ‌ها</string> + <string name="v4_2_group_links">لینک‌های گروه</string> + <string name="v4_3_irreversible_message_deletion">حذف غیرقابل برگشت پیام</string> + <string name="v4_2_security_assessment_desc">امنیت SimpleX Chat به وسیله Tails of Bits مورد سنجش قرار گرفت.</string> + <string name="v4_5_transport_isolation">انزوای ترابری</string> + <string name="session_code">کد نشست</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[شما هم اکنون در حال پیوستن به گروه <b>%1$s</b> هستید.]]></string> + <string name="linked_mobiles">موبایل‌های متصل</string> + <string name="file_not_approved_title">سرورهای ناشناخته!</string> + <string name="protect_ip_address">حفاظت از نشانی IP</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">برنامه از شما خواهد خواست تا بارگیری‌ها از سرورهای پرونده ناشناخته را تایید کنید (به جز .onion یا وقتی پروکسی SOCKS فعال است).</string> + <string name="settings_section_title_files">پرونده‌ها</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">بدون تور یا VPN، نشانی IP شما برای سرورهای پرونده قابل رویت خواهد بود.</string> + <string name="file_not_approved_descr">بدون تور یا VPN، نشانی IP شما برای این واسطه‌های XFTP قابل رویت خواهد بود: +\n%1$s.</string> + <string name="message_queue_info_none">هیچ</string> + <string name="v5_8_message_delivery">تحویل پیام بهبود یافته</string> + <string name="v5_8_persian_ui">رابط کاربری فارسی</string> + <string name="v5_8_message_delivery_descr">با استفاده باتری کاهش یافته.</string> + <string name="info_row_debug_delivery">اشکال‌زدایی تحویل</string> + <string name="message_queue_info">اطلاعات صف پیام</string> + <string name="theme_destination_app_theme">تم برنامه</string> + <string name="v5_8_safe_files_descr">تایید پرونده‌ها از سرورهای ناشناخته.</string> + <string name="message_queue_info_server_info">اطلاعات صف سرور: %1$s +\n +\nآخرین پیام دریافتی: %2$s</string> + <string name="chat_list_always_visible">نمایش فهرست گپ در پنجره جدید</string> + <string name="chat_theme_apply_to_dark_mode">حالت تاریک</string> + <string name="theme_black">سیاه</string> + <string name="color_mode">حالت رنگ</string> + <string name="color_mode_dark">تاریک</string> + <string name="dark_mode_colors">رنگ‌های حالت تاریک</string> + <string name="color_mode_light">روشن</string> + <string name="reset_single_color">بازنشاندن رنگ</string> + <string name="color_mode_system">سیستم</string> + <string name="error_initializing_web_view">خطا در مقداردهی اولیه WebView. سیستم خود را به نسخه جدید به روز کنید. لطفا با توسعه‌دهنگان تماس بگیرید. +\nخطا: 9%s</string> + <string name="settings_section_title_chat_colors">رنگ‌های گپ</string> + <string name="settings_section_title_chat_theme">تم گپ</string> + <string name="settings_section_title_user_theme">تم نمایه</string> + <string name="color_wallpaper_background">پس‌زمینه کاغذدیواری</string> + <string name="color_primary_variant2">ابتدایی اضافی ۲</string> + <string name="wallpaper_advanced_settings">تنظیمات پیشرفته</string> + <string name="wallpaper_scale_fill">پر کردن</string> + <string name="wallpaper_scale_fit">گنجاندن</string> + <string name="wallpaper_preview_hello_alice">عصر به خیر!</string> + <string name="wallpaper_preview_hello_bob">صبح به خیر!</string> + <string name="color_received_quote">پاسخ دریافتی</string> + <string name="theme_remove_image">حذف تصویر</string> + <string name="wallpaper_scale_repeat">تکرار</string> + <string name="wallpaper_scale">مقیاس</string> + <string name="color_sent_quote">پاسخ ارسالی</string> + <string name="color_wallpaper_tint">ابتدایی کاغذدیواری</string> + <string name="chat_theme_set_default_theme">تعیین تم پیش‌فرض</string> + <string name="chat_theme_reset_to_app_theme">بازنشاندن به تم برنامه</string> + <string name="chat_theme_reset_to_user_theme">بازنشاندن به تم کاربر</string> + <string name="chat_theme_apply_to_all_modes">تمام حالت‌های رنگ</string> + <string name="chat_theme_apply_to_mode">اعمال بر</string> + <string name="chat_theme_apply_to_light_mode">حالت روشن</string> + <string name="v5_8_private_routing">مسیریابی پیام خصوصی 🚀</string> + <string name="v5_8_chat_themes_descr">ظاهر گپ‌های خود را متمایز کنید!</string> + <string name="v5_8_chat_themes">تم‌های جدید گپ</string> + <string name="v5_8_private_routing_descr">از نشانی IP خود در برابر واسطه‌های پیام‌رسانی انتخاب شده توسط مخاطبانتان محافظت کنید. +\nدر تنظیمات «شبکه و سرورها» فعال کنید.</string> + <string name="v5_8_safe_files">دریافت امن پرونده‌ها</string> + <string name="file_error_no_file">پرونده یافت نشد - احتمالا حذف یا لغو شده.</string> + <string name="file_error_auth">کلید اشتباه یا نشانی پرونده ناشناخته - به احتمال زیاد پرونده حذف شده است.</string> + <string name="file_error">خطای پرونده</string> + <string name="temporary_file_error">خطای پرونده موقت</string> + <string name="info_row_file_status">وضعیت پرونده</string> + <string name="info_row_message_status">وضعیت پیام</string> + <string name="share_text_file_status">وضعیت پرونده: %s</string> + <string name="share_text_message_status">وضعیت پیام: %s</string> + <string name="copy_error">خطای کپی</string> + <string name="remote_ctrl_connection_stopped_desc">لطفا بررسی کنید که تلفن همراه و کامپیوتر به شبکه محلی یکسانی متصل هستند، و فایروال کامپیوتر شما اجازه اتصال را میدهد. +\nلطفا هر مشکل دیگری را با توسعه‌دهندگان به اشتراک بگذارید.</string> + <string name="remote_ctrl_connection_stopped_identity_desc">این لینک توسط موبایل دیگری استفاده شده است، لطفا لینک جدیدی در کامپیوتر بسازید.</string> + <string name="file_error_relay">خطای سرور پرونده:%1$s</string> + <string name="n_file_errors">%1$d خطای پرونده:\n%2$s</string> +</resources> 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 be4072c4bf..1dd6598ef3 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml @@ -16,7 +16,7 @@ <string name="send_disappearing_message_30_seconds">30 sekuntia</string> <string name="send_disappearing_message_5_minutes">5 minuuttia</string> <string name="allow_disappearing_messages_only_if">Salli katoavat viestit vain, jos kontaktisi sallii ne.</string> - <string name="smp_servers_add">Lisää palvelin…</string> + <string name="smp_servers_add">Lisää palvelin</string> <string name="button_add_welcome_message">Lisää tervetuloviesti</string> <string name="allow_to_send_voice">Salli ääniviestien lähettäminen.</string> <string name="one_time_link_short">Kertakäyttölinkki</string> @@ -94,7 +94,7 @@ <string name="call_already_ended">Puhelu on jo päättynyt!</string> <string name="dark_theme">Tumma teema</string> <string name="connect_via_contact_link">Yhdistetäänkö kontaktilinkin kautta\?</string> - <string name="connect_via_group_link">Yhdistetäänkö ryhmälinkin kautta\?</string> + <string name="connect_via_group_link">Liitytäänkö ryhmään?</string> <string name="connect_via_invitation_link">Yhdistetäänkö kutsulinkin kautta\?</string> <string name="connect_via_link_verb">Yhdistä</string> <string name="server_connecting">yhdistää</string> @@ -108,7 +108,6 @@ <string name="about_simplex">Tietoja SimpleX:stä</string> <string name="decentralized">Hajautettu</string> <string name="icon_descr_audio_off">Ääni pois päältä</string> - <string name="chat_archive_section">ARKISTO</string> <string name="change_role">Vaihda rooli</string> <string name="full_deletion">Poista kaikilta</string> <string name="empty_chat_profile_is_created">Luodaan tyhjä chat-profiili annetulla nimellä, ja sovellus avautuu normaalisti.</string> @@ -168,9 +167,6 @@ <string name="theme_dark">Tumma</string> <string name="la_auth_failed">Tunnistautuminen epäonnistui</string> <string name="smp_servers_preset_add">Lisää esiasetettuja palvelimia</string> - <string name="chat_archive_header">Arkisto</string> - <string name="delete_chat_archive_question">Poista keskusteluarkisto\?</string> - <string name="archive_created_on_ts">Luotu %1$s</string> <string name="rcv_group_event_group_deleted">poistettu ryhmä</string> <string name="group_member_status_connecting">yhdistää</string> <string name="group_member_status_accepted">yhdistäminen (hyväksytty)</string> @@ -188,7 +184,6 @@ <string name="allow_your_contacts_to_send_disappearing_messages">Salli kontaktiesi lähettää katoavia viestejä.</string> <string name="timed_messages">Katoavat viestit</string> <string name="icon_descr_context">Kontekstikuvake</string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Skannaa QR-koodi</b>: muodostaaksesi yhteyden kontaktiisi, joka näyttää QR-koodin sinulle.]]></string> <string name="icon_descr_cancel_live_message">Peruuta live-viesti</string> <string name="configure_ICE_servers">Määritä ICE-palvelimet</string> <string name="add_address_to_your_profile">Lisää osoite profiiliisi, jotta kontaktisi voivat jakaa sen muiden kanssa. Profiilipäivitys lähetetään kontakteillesi.</string> @@ -231,7 +226,6 @@ <string name="clear_verb">Tyhjennä</string> <string name="clear_chat_button">Tyhjennä keskustelu</string> <string name="clear_chat_question">Tyhjennä keskustelu\?</string> - <string name="create_one_time_link">Luo kertaluonteinen kutsulinkki</string> <string name="app_version_name">Sovellusversio: v%s</string> <string name="callstatus_calling">soittaa…</string> <string name="delete_chat_profile">Poista keskusteluprofiili</string> @@ -257,8 +251,7 @@ <string name="button_delete_contact">Poista kontakti</string> <string name="above_then_preposition_continuation">Yllä, sitten:</string> <string name="network_session_mode_user">Keskustelujen profiili</string> - <string name="network_session_mode_entity_description"><b>Jokaiselle kontaktille ja ryhmän jäsenelle</b> käytetään erillistä TCP-yhteyttä (ja SOCKS-tunnistetietoja). -\n<b>Huomaa</b>: jos sinulla on useita yhteyksiä, akun ja data-liikenteen määrä voi olla huomattavasti korkeampi ja jotkin yhteydet voivat epäonnistua.</string> + <string name="network_session_mode_entity_description"><![CDATA[<b>Jokaiselle kontaktille ja ryhmän jäsenelle</b> käytetään erillistä TCP-yhteyttä (ja SOCKS-tunnistetietoja). \n<b>Huomaa</b>: jos sinulla on useita yhteyksiä, akun ja data-liikenteen määrä voi olla huomattavasti korkeampi ja jotkin yhteydet voivat epäonnistua.]]></string> <string name="icon_descr_audio_call">Äänipuhelu</string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Paras akulle</b>. Saat ilmoituksia vain, kun sovellus on käynnissä (EI taustapalvelua).]]></string> <string name="ttl_hours">%d tuntia</string> @@ -271,7 +264,6 @@ <string name="icon_descr_server_status_disconnected">Katkaistu</string> <string name="back">Takaisin</string> <string name="connect_via_link_or_qr">Yhdistä linkillä / QR-koodilla</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Lisää uusi kontakti</b>: luo kertakäyttöinen QR-koodi kontaktille.]]></string> <string name="clear_chat_menu_action">Tyhjennä</string> <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 työpöytä: skannaa esitetty QR-koodi sovelluksesta käyttämällä <b>Skannaa QR-koodi</b>.]]></string> <string name="delete_pending_connection__question">Poistetaanko odottava yhteys\?</string> @@ -329,7 +321,6 @@ <string name="database_downgrade">Tietokannan alentaminen</string> <string name="database_upgrade">Tietokannan päivitys</string> <string name="database_will_be_encrypted">Tietokanta salataan.</string> - <string name="delete_archive">Poista arkisto</string> <string name="group_member_status_intro_invitation">yhdistäminen (esittelykutsu)</string> <string name="info_row_deleted_at">Poistettu klo</string> <string name="change_verb">Muuta</string> @@ -338,7 +329,6 @@ <string name="share_text_disappears_at">Katoaa klo: %s</string> <string name="create_secret_group_title">Luo salainen ryhmä</string> <string name="chat_preferences_always">aina</string> - <string name="cant_delete_user_profile">Käyttäjäprofiilia ei voi poistaa!</string> <string name="allow_your_contacts_to_call">Salli kontaktiesi soittaa sinulle.</string> <string name="allow_your_contacts_irreversibly_delete">Salli kontaktiesi poistaa lähetetyt viestit peruuttamattomasti.</string> <string name="allow_your_contacts_to_send_voice_messages">Salli kontaktiesi lähettää ääniviestejä.</string> @@ -349,7 +339,7 @@ <string name="both_you_and_your_contact_can_add_message_reactions">Sekä sinä että kontaktisi voivat käyttää viestireaktioita.</string> <string name="both_you_and_your_contacts_can_delete">Sekä sinä että kontaktisi voitte peruuttamattomasti poistaa lähetetyt viestit.</string> <string name="both_you_and_your_contact_can_make_calls">Sekä sinä että kontaktisi voitte soittaa puheluita.</string> - <string name="direct_messages_are_prohibited_in_chat">Yksityisviestit jäsenten välillä ovat kiellettyjä tässä ryhmässä.</string> + <string name="direct_messages_are_prohibited_in_group">Yksityisviestit jäsenten välillä ovat kiellettyjä tässä ryhmässä.</string> <string name="v4_5_transport_isolation_descr">Chat-profiilin (oletus) tai yhteyden (BETA) perusteella.</string> <string name="feature_cancelled_item">peruttu %s</string> <string name="ttl_day">%d päivä</string> @@ -460,7 +450,7 @@ <string name="error_changing_role">Virhe roolin vaihdossa</string> <string name="ttl_w">%dw</string> <string name="v4_6_group_moderation">Ryhmän moderointi</string> - <string name="display_name__field">Näyttönimi:</string> + <string name="display_name__field">Profiilin nimi:</string> <string name="error_starting_chat">Virhe käynnistettäessä keskustelua</string> <string name="error_stopping_chat">Virhe keskustelun lopettamisessa</string> <string name="error_changing_message_deletion">Virhe asetuksen muuttamisessa</string> @@ -562,7 +552,7 @@ <string name="invalid_QR_code">Virheellinen QR-koodi</string> <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Jos et voi tavata henkilökohtaisesti, <b>näytä QR-koodi videopuhelussa</b> tai jaa linkki.]]></string> <string name="smp_servers_invalid_address">Virheellinen palvelimen osoite!</string> - <string name="display_name">Näyttönimi</string> + <string name="display_name">Kirjoita nimesi:</string> <string name="icon_descr_flip_camera">Käännä kamera</string> <string name="alert_text_fragment_encryption_out_of_sync_old_database">Se voi tapahtua, kun sinä tai kontaktisi käytitte vanhaa varmuuskopiota tietokannasta.</string> <string name="incorrect_passcode">Väärä pääsykoodi</string> @@ -593,7 +583,7 @@ <string name="alert_title_group_invitation_expired">Vanhentunut kutsu!</string> <string name="group_member_status_invited">kutsuttu</string> <string name="conn_level_desc_indirect">epäsuora (%1$s)</string> - <string name="group_display_name_field">Ryhmän näyttönimi:</string> + <string name="group_display_name_field">Kirjoita ryhmän nimi:</string> <string name="enter_password_to_show">Syötä salasana hakuun</string> <string name="group_preferences">Ryhmän asetukset</string> <string name="group_members_can_add_message_reactions">Ryhmän jäsenet voivat lisätä viestireaktioita.</string> @@ -627,7 +617,6 @@ <string name="text_field_set_contact_placeholder">Aseta kontaktin nimi…</string> <string name="save_passphrase_in_keychain">Tallenna salasana Keystoreen</string> <string name="only_your_contact_can_send_disappearing">Vain kontaktisi voi lähettää katoavia viestejä.</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">Onion-isäntiä käytetään, kun niitä on saatavilla.</string> <string name="port_verb">Portti</string> <string name="network_proxy_port">portti %d</string> <string name="disable_onion_hosts_when_not_supported"><![CDATA[Aseta <i>Use .onion hosts</i> arvoon Ei, jos SOCKS-välityspalvelin ei tue niitä.]]></string> @@ -665,10 +654,8 @@ <string name="network_option_tcp_connection_timeout">TCP-yhteyden aikakatkaisu</string> <string name="network_option_ping_count">PING-määrä</string> <string name="network_option_ping_interval">PING-väli</string> - <string name="network_options_revert">Palauta</string> <string name="users_delete_with_connections">Profiili- ja palvelinyhteydet</string> <string name="set_group_preferences">Aseta ryhmän asetukset</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[Monet ovat kysyneet: <i>jos SimpleX ei sisällä käyttäjätunnuksia, kuinka se voi toimittaa viestejä\?</i>]]></string> <string name="conn_stats_section_title_servers">PALVELIMET</string> <string name="save_and_notify_contact">Tallenna ja ilmoita kontaktille</string> <string name="save_and_notify_contacts">Tallenna ja ilmoita kontakteille</string> @@ -679,7 +666,6 @@ <string name="self_destruct_passcode">Itsetuhoutuva pääsykoodi</string> <string name="restart_the_app_to_use_imported_chat_database">Käynnistä sovellus uudelleen käyttääksesi tuotua keskustelutietokantaa.</string> <string name="old_database_archive">Vanha tietokanta-arkisto</string> - <string name="stop_chat_to_enable_database_actions">Pysäytä keskustelu, jotta tietokantatoiminnot voidaan ottaa käyttöön.</string> <string name="restart_the_app_to_create_a_new_chat_profile">Luo uusi keskusteluprofiili käynnistämällä sovellus uudelleen.</string> <string name="messages_section_title">Viestit</string> <string name="chat_item_ttl_none">ei koskaan</string> @@ -763,7 +749,7 @@ <string name="v4_6_reduced_battery_usage_descr">Lisää parannuksia on tulossa pian!</string> <string name="v4_6_group_moderation_descr">Nyt järjestelmänvalvojat voivat: \n- poistaa jäsenten viestit. -\n- poista jäsenet käytöstä (\"tarkkailija\" rooli)</string> +\n- poista jäsenet käytöstä ("tarkkailija" rooli)</string> <string name="save_and_notify_group_members">Tallenna ja ilmoita ryhmän jäsenille</string> <string name="stop_chat_confirmation">Lopeta</string> <string name="stop_chat_to_export_import_or_delete_chat_database">Pysäytä keskustelut viedäksesi, tuodaksesi tai poistaaksesi keskustelujen tietokannan. Et voi vastaanottaa ja lähettää viestejä, kun keskustelut on pysäytetty.</string> @@ -846,7 +832,6 @@ <string name="read_more_in_github_with_link"><![CDATA[Lue lisää <font color="#0088ff">GitHub-arkistostamme</font>.]]></string> <string name="onboarding_notifications_mode_periodic">Säännölliset</string> <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Vain asiakaslaitteet tallentavat käyttäjäprofiileja, yhteystietoja, ryhmiä ja viestejä, jotka on lähetetty <b>2-kerroksisella päästä päähän -salauksella</b>.]]></string> - <string name="read_more_in_github">Lue lisää GitHub-tietovarastostamme.</string> <string name="paste_the_link_you_received">Liitä vastaanotettu linkki</string> <string name="relay_server_protects_ip">Välityspalvelin suojaa IP-osoitteesi, mutta se voi tarkkailla puhelun kestoa.</string> <string name="open_simplex_chat_to_accept_call">Avaa SimpleX Chat hyväksyäksesi puhelun</string> @@ -877,7 +862,6 @@ <string name="v4_6_chinese_spanish_interface_descr">Kiitos käyttäjille – osallistu Weblaten kautta!</string> <string name="profile_password">Profiilin salasana</string> <string name="reset_color">Oletusvärit</string> - <string name="save_color">Tallenna väri</string> <string name="prohibit_sending_voice_messages">Estä ääniviestien lähettäminen.</string> <string name="v4_4_disappearing_messages_desc">Lähetetyt viestit poistetaan asetetun ajan kuluttua.</string> <string name="add_contact_or_create_group">Aloita uusi keskustelu</string> @@ -921,8 +905,6 @@ <string name="thank_you_for_installing_simplex">Kiitos SimpleX Chatin asentamisesta!</string> <string name="icon_descr_settings">Asetukset</string> <string name="icon_descr_more_button">Lisää</string> - <string name="network_use_onion_hosts_no_desc_in_alert">Onion-isäntiä ei käytetä.</string> - <string name="network_use_onion_hosts_required_desc_in_alert">Yhteyden muodostamiseen tarvitaan Onion-isäntiä.</string> <string name="onboarding_notifications_mode_title">Yksityiset ilmoitukset</string> <string name="icon_descr_speaker_off">Kaiutin pois päältä</string> <string name="icon_descr_speaker_on">Kaiutin päällä</string> @@ -961,7 +943,7 @@ <string name="theme_system">Järjestelmä</string> <string name="only_you_can_make_calls">Vain sinä voit soittaa puheluita.</string> <string name="ensure_ICE_server_address_are_correct_format_and_unique">Varmista, että WebRTC ICE -palvelinosoitteet ovat oikeassa muodossa, rivieroteltuina ja että ne eivät ole päällekkäisiä.</string> - <string name="network_and_servers">Verkko &; palvelimet</string> + <string name="network_and_servers">Verkko & palvelimet</string> <string name="image_descr_qr_code">QR-koodi</string> <string name="scan_code">Skannaa koodi</string> <string name="callstate_starting">alkaa…</string> @@ -978,7 +960,6 @@ <string name="smp_server_test_secure_queue">Turvallinen jono</string> <string name="restore_passphrase_not_found_desc">Tunnuslausetta ei löydy Keystoresta, kirjoita se manuaalisesti. Tämä on saattanut tapahtua, jos olet palauttanut sovelluksen tiedot varmuuskopiointityökalulla. Jos näin ei ole, ota yhteyttä kehittäjiin.</string> <string name="restore_database_alert_desc">Anna edellinen salasana tietokannan varmuuskopion palauttamisen jälkeen. Tätä toimintoa ei voi kumota.</string> - <string name="save_archive">Tallenna arkisto</string> <string name="non_fatal_errors_occured_during_import">Tuonnin aikana tapahtui joitakin ei-vakavia virheitä – saatat nähdä Chat-konsolissa lisätietoja.</string> <string name="info_row_updated_at">Tietue päivitetty klo</string> <string name="info_row_moderated_at">Moderoitu klo</string> @@ -1066,7 +1047,6 @@ <string name="waiting_for_file">Odottaa tiedostoa</string> <string name="to_start_a_new_chat_help_header">Aloita uusi keskustelu</string> <string name="using_simplex_chat_servers">Käyttää SimpleX Chat -palvelimia.</string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Sinä päätät, minkä palvelimien kautta viestit <b>vastaanotetaan</b>, kontaktiesi – palvelimet, joita käytät viestien lähettämiseen.]]></string> <string name="your_privacy">Yksityisyytesi</string> <string name="snd_group_event_member_deleted">poistit %1$s</string> <string name="chat_preferences_yes">kyllä</string> @@ -1117,8 +1097,8 @@ <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Viestintä- ja sovellusalusta, joka suojaa yksityisyyttäsi ja tietoturvaasi.</string> <string name="icon_descr_video_call">videopuhelu</string> <string name="group_info_section_title_num_members"> %1$s JÄSENET</string> - <string name="member_role_will_be_changed_with_notification">Rooli muuttuu muotoon \"%s\". Kaikille ryhmän jäsenille ilmoitetaan asiasta.</string> - <string name="member_role_will_be_changed_with_invitation">Rooli muuttuu muotoon \"%s\". Jäsen saa uuden kutsun.</string> + <string name="member_role_will_be_changed_with_notification">Rooli muuttuu muotoon "%s". Kaikille ryhmän jäsenille ilmoitetaan asiasta.</string> + <string name="member_role_will_be_changed_with_invitation">Rooli muuttuu muotoon "%s". Jäsen saa uuden kutsun.</string> <string name="voice_prohibited_in_this_chat">Ääniviestit ovat kiellettyjä tässä keskustelussa.</string> <string name="v4_3_voice_messages">Ääniviestit</string> <string name="trying_to_connect_to_server_to_receive_messages">Yritetään muodostaa yhteys palvelimeen, jota käytetään viestien vastaanottamiseen tältä kontaktilta.</string> @@ -1137,7 +1117,7 @@ <string name="your_SMP_servers">SMP-palvelimesi</string> <string name="your_XFTP_servers">XFTP-palvelimesi</string> <string name="use_simplex_chat_servers__question">Käytä SimpleX Chat palvelimia\?</string> - <string name="theme_colors_section_title">TEEMAN VÄRIT</string> + <string name="theme_colors_section_title">KÄYTTÖLIITTYMÄN VÄRIT</string> <string name="update_network_session_mode_question">Päivitä kuljetuksen eristystila\?</string> <string name="you_can_create_it_later">Voit luoda sen myöhemmin</string> <string name="to_reveal_profile_enter_password">Voit paljastaa piilotetun profiilisi kirjoittamalla koko salasanan Keskusteluprofiilit-sivun hakukenttään.</string> @@ -1161,7 +1141,6 @@ \nkontakteillesi</string> <string name="your_ICE_servers">ICE-palvelimesi</string> <string name="network_use_onion_hosts_prefer">Kun saatavilla</string> - <string name="update_onion_hosts_settings_question">Päivitä .onion-isäntien asetus\?</string> <string name="network_use_onion_hosts">Käytä .onion-isäntiä</string> <string name="your_calls">Puhelusi</string> <string name="alert_message_no_group">Tätä ryhmää ei enää ole olemassa.</string> @@ -1241,8 +1220,6 @@ <string name="snd_conn_event_switch_queue_phase_completed_for_member">muutit osoitteeksi %s</string> <string name="invite_prohibited_description">Yrität kutsua kontaktia, jonka kanssa olet jakanut inkognito-profiilin, ryhmään, jossa käytät pääprofiiliasi</string> <string name="group_welcome_title">Tervetuloviesti</string> - <string name="should_be_at_least_one_profile">Käyttäjäprofiileja tulee olla vähintään yksi.</string> - <string name="should_be_at_least_one_visible_profile">Näkyviä käyttäjäprofiileja tulee olla vähintään yksi.</string> <string name="incognito_info_share">Kun jaat inkognitoprofiilin jonkun kanssa, tätä profiilia käytetään ryhmissä, joihin tämä sinut kutsuu.</string> <string name="group_main_profile_sent">Keskusteluprofiilisi lähetetään ryhmän jäsenille</string> <string name="incognito_random_profile">Satunnainen profiilisi</string> @@ -1389,4 +1366,139 @@ <string name="v5_3_new_interface_languages">6 uutta käyttöliittymän kieltä</string> <string name="v5_3_discover_join_groups">Löydä ryhmiä ja liity niihin</string> <string name="v5_3_new_desktop_app_descr">Luo uusi profiili työpöytäsovelluksessa. 💻</string> -</resources> \ No newline at end of file + <string name="invalid_file_link">Virheellinen linkki</string> + <string name="invalid_qr_code">Virheellinen QR-koodi</string> + <string name="network_type_no_network_connection">Ei verkkoyhteyttä</string> + <string name="migrate_to_device_bytes_downloaded">%s ladattu</string> + <string name="refresh_qr_code">Päivitä</string> + <string name="group_members_2">%s ja %s</string> + <string name="session_code">Istuntokoodi</string> + <string name="migrate_from_device_verify_database_passphrase">Vahvista tietokannan tunnuslause</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Tämä on linkkisi ryhmään <b>%1$s</b>!]]></string> + <string name="connect_plan_this_is_your_own_simplex_address">Tämä on oma SimpleX-osoitteesi!</string> + <string name="migrate_from_device_verify_passphrase">Vahvista tunnuslause</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Olet jo ryhmässä <b>%1$s</b>.]]></string> + <string name="allow_to_send_simplex_links">Salli SimpleX-linkkien lähettäminen.</string> + <string name="add_contact_tab">Lisää kontakti</string> + <string name="or_show_this_qr_code">Tai näytä tämä koodi</string> + <string name="migrate_to_device_download_failed">Lataus epäonnistui</string> + <string name="recent_history_is_not_sent_to_new_members">Historiaa ei lähetetä uusille jäsenille.</string> + <string name="migrate_from_device_archiving_database">Arkistoidaan tietokanta</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Varoitus</b>: arkisto poistetaan.]]></string> + <string name="permissions_camera_and_record_audio">Kamera ja mikrofoni</string> + <string name="migrate_from_device_check_connection_and_try_again">Tarkista internetyhteys ja yritä uudelleen</string> + <string name="multicast_connect_automatically">Yhdistä automaattisesti</string> + <string name="migrate_from_device_delete_database_from_device">Poista tietokanta tältä laitteelta</string> + <string name="unable_to_open_browser_title">Virhe selainta avatessa</string> + <string name="share_text_file_status">Tiedoston tila: %s</string> + <string name="expand_verb">Laajenna</string> + <string name="wallpaper_preview_hello_bob">Hyvää huomenta!</string> + <string name="wallpaper_advanced_settings">Lisäasetukset</string> + <string name="chat_theme_set_default_theme">Aseta oletusteema</string> + <string name="chat_theme_apply_to_light_mode">Vaalea tila</string> + <string name="ci_status_other_error">Virhe: %1$s</string> + <string name="feature_roles_all_members">kaikki jäsenet</string> + <string name="set_passphrase">Aseta tunnuslause</string> + <string name="disable_sending_recent_history">Älä lähetä historiaa uusille jäsenille.</string> + <string name="network_smp_proxy_fallback_allow">Kyllä</string> + <string name="migrate_to_device_file_delete_or_link_invalid">Tiedosto poistettiin tai linkki on virheellinen</string> + <string name="call_service_notification_audio_call">Äänipuhelu</string> + <string name="call_service_notification_end_call">Lopeta puhelu</string> + <string name="call_service_notification_video_call">Videopuhelu</string> + <string name="tap_to_scan">Napauta skannataksesi</string> + <string name="developer_options_section">Kehittäjävalinnat</string> + <string name="download_file">Lataa</string> + <string name="file_error">Tiedostovirhe</string> + <string name="private_routing_show_message_status">Näytä viestin tila</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="audio_device_speaker">Kaiutin</string> + <string name="audio_device_wired_headphones">Kuulokkeet</string> + <string name="verify_connections">Vahvista yhteydet</string> + <string name="theme_destination_app_theme">Sovelluksen teema</string> + <string name="chat_theme_apply_to_all_modes">Kaikki väritilat</string> + <string name="group_members_can_send_simplex_links">Ryhmäjäsenet voivat lähettää SimpleX-linkkejä.</string> + <string name="new_desktop"><![CDATA[<i>(uusi)</i>]]></string> + <string name="cannot_share_message_alert_title">VIestiä ei voi lähettää</string> + <string name="simplex_links_not_allowed">SimpleX-linkit eivät ole sallittuja</string> + <string name="loading_remote_file_title">Ladataan tiedostoa</string> + <string name="clear_note_folder_warning">Kaikki viestit poistetaan - tätä ei voi perua!</string> + <string name="invalid_name">Virheellinen nimi!</string> + <string name="settings_section_title_user_theme">Profiilin teema</string> + <string name="rcv_group_event_1_member_connected">%s yhdistetty</string> + <string name="group_members_n">%s, %s ja %d jäsentä</string> + <string name="group_member_status_unknown">tuntematon tila</string> + <string name="error_sending_message_contact_invitation">Virhe kutsua lähettäessä</string> + <string name="info_row_file_status">Tiedoston tila</string> + <string name="info_row_message_status">Viestin tila</string> + <string name="share_text_message_status">Viestin tila: %s</string> + <string name="button_remove_member_question">Poistetaanko jäsen?</string> + <string name="create_group_button">Luo ryhmä</string> + <string name="theme_black">Musta</string> + <string name="color_mode">Väritila</string> + <string name="color_mode_dark">Tumma</string> + <string name="dark_mode_colors">Tumman tilan värit</string> + <string name="chat_theme_apply_to_dark_mode">Tumma tila</string> + <string name="wallpaper_preview_hello_alice">Hyvää iltapäivää!</string> + <string name="devices">Laitteet</string> + <string name="migrate_to_device_enter_passphrase">Kirjoita tunnuslause</string> + <string name="migrate_from_device_error_deleting_database">Virhe tietokantaa poistaessa</string> + <string name="migrate_to_device_finalize_migration">Viimeistele migraatio toisella laitteella.</string> + <string name="saved_description">tallennettu</string> + <string name="saved_chat_item_info_tab">Tallennettu</string> + <string name="migrate_to_device_apply_onion">Toteuta</string> + <string name="migrate_to_device_import_failed">Tuonti epäonnistui</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="network_type_ethernet">Kiinteä ethernet</string> + <string name="agent_internal_error_title">Sisäinen virhe</string> + <string name="migrate_to_device_confirm_network_settings">Vahvista verkkoasetukset</string> + <string name="migrate_from_device_finalize_migration">Viimeistele migraatio</string> + <string name="failed_to_create_user_invalid_title">Virheellinen näyttönimi!</string> + <string name="tap_to_paste_link">Napauta liittääksesi linkin</string> + <string name="non_content_uri_alert_title">Virheellinen tiedostopolku</string> + <string name="settings_section_title_network_connection">Verkkoyhteys</string> + <string name="disconnect_remote_host">Katkaise yhteys</string> + <string name="camera_not_available">Kamera ei saatavilla</string> + <string name="creating_link">Luodaan linkki…</string> + <string name="or_scan_qr_code">Tai skannaa QR-koodi</string> + <string name="network_smp_proxy_mode_always">Aina</string> + <string name="network_smp_proxy_mode_never">Ei koskaan</string> + <string name="network_smp_proxy_fallback_prohibit">Ei</string> + <string name="permissions_camera">Kamera</string> + <string name="permissions_open_settings">Avaa asetukset</string> + <string name="protect_ip_address">Suojaa IP-osoite</string> + <string name="settings_section_title_files">TIEDOSTOT</string> + <string name="settings_section_title_profile_images">Profiilikuvat</string> + <string name="group_member_status_unknown_short">tuntematon</string> + <string name="remove_member_button">Poista jäsen</string> + <string name="simplex_links">SimpleX-linkit</string> + <string name="v5_6_quantum_resistant_encryption">Kvanttiturvallinen salaus</string> + <string name="v5_7_network">Verkon hallinta</string> + <string name="enter_this_device_name">Kirjoita tämän laitteen nimi…</string> + <string name="verify_connection">Vahvista yhteys</string> + <string name="desktop_devices">Työpöytälaitteet</string> + <string name="not_compatible">Ei yhteensopiva!</string> + <string name="random_port">Satunnainen</string> + <string name="connect_plan_group_already_exists">Ryhmä on jo olemassa!</string> + <string name="agent_critical_error_title">Kriittinen virhe</string> + <string name="migrate_from_device_error_saving_settings">Virhe asetuksia tallentaessa</string> + <string name="error_alert_title">Virhe</string> + <string name="database_migration_in_progress">Tietokannan migraatio on käynnissä. +\nTämä saattaa kestää muutaman minuutin.</string> + <string name="retry_verb">Yritä uudelleen</string> + <string name="show_internal_errors">Näytä sisäiset virheet</string> + <string name="show_slow_api_calls">Näytä hitaat API-kutsut</string> + <string name="create_another_profile_button">Luo profiili</string> + <string name="connect_plan_this_is_your_own_one_time_link">Tämä on oma kertakäyttöinen linkkisi!</string> + <string name="error">Virhe</string> + <string name="possible_slow_function_title">Hidas funktio</string> + <string name="contact_tap_to_connect">Napauta yhdistääksesi</string> + <string name="message_too_large">Viesti on liian pitkä</string> + <string name="error_creating_message">Virhe viestiä luotaessa</string> + <string name="permissions_record_audio">Mikrofoni</string> + <string name="color_mode_light">Vaalea</string> + <string name="color_mode_system">Järjestelmä</string> + <string name="theme_remove_image">Poista kuva</string> + <string name="desktop_incompatible_version">Yhteensopimaton versio</string> + <string name="new_mobile_device">Uusi mobiililaite</string> + <string name="this_device">Tämä laite</string> +</resources> 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 849daaeefe..43fcb2f5f7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -80,16 +80,16 @@ <string name="service_notifications">Notifications instantanées !</string> <string name="service_notifications_disabled">Les notifications instantanées sont désactivées !</string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Il peut être désactivé via les paramètres</b> - les notifications seront toujours affichées lorsque l\'application est en cours d\'exécution.]]></string> - <string name="turning_off_service_and_periodic">L\'optimisation de la batterie est active et désactive le service de fond et les demandes périodiques de nouveaux messages. Vous pouvez les réactiver via les paramètres.</string> + <string name="turning_off_service_and_periodic">Lorsque l\'optimisation de la batterie est activée, le service en arrière-plan et les demandes périodiques de nouveaux messages sont désactivés. Il est possible de les réactiver dans les paramètres.</string> <string name="periodic_notifications">Notifications périodiques</string> <string name="periodic_notifications_disabled">Les notifications périodiques sont désactivées !</string> <string name="enter_passphrase_notification_title">Une phrase secrète est nécessaire</string> - <string name="turn_off_battery_optimization"><![CDATA[Pour l\'utiliser, veuillez <b>autoriser SimpleX à fonctionner en arrière-plan</b> dans la fenêtre de dialogue suivante. Sinon, les notifications seront désactivées.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[<b>Autoriser le</b> dans la boîte de dialogue suivante pour recevoir des notifications instantanément.]]></string> <string name="error_smp_test_server_auth">Le serveur requiert une autorisation pour créer des files d\'attente, vérifiez le mot de passe</string> <string name="periodic_notifications_desc">L\'application récupère périodiquement les nouveaux messages - elle utilise un peu votre batterie chaque jour. L\'application n\'utilise pas les notifications push - les données de votre appareil ne sont pas envoyées aux serveurs.</string> - <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Pour protéger votre vie privée, au lieu des notifications push, l\'application possède un <b>SimpleX service de fond</b> - il utilise quelques pour cent de la batterie par jour.]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Pour protéger votre vie privée, <b>SimpleX fonctionne en arrière-plan</b> au lieu d\'utiliser les notifications push.]]></string> <string name="hide_notification">Cacher</string> - <string name="settings_notification_preview_mode_title">Montrer l\'aperçu</string> + <string name="settings_notification_preview_mode_title">Aperçu affiché</string> <string name="notification_preview_mode_contact">Nom du contact</string> <string name="notification_preview_somebody">Contact masqué :</string> <string name="notification_preview_new_message">nouveau message</string> @@ -111,14 +111,14 @@ <string name="share_verb">Partager</string> <string name="copy_verb">Copier</string> <string name="delete_verb">Supprimer</string> - <string name="save_verb">Sauvegarder</string> + <string name="save_verb">Enregistrer</string> <string name="edit_verb">Modifier</string> <string name="reveal_verb">Révéler</string> <string name="hide_verb">Cacher</string> <string name="allow_verb">Autoriser</string> <string name="delete_message__question">Supprimer le message \?</string> <string name="for_me_only">Supprimer pour moi</string> - <string name="your_chats">Chats</string> + <string name="your_chats">Discussions</string> <string name="notification_preview_mode_message">Texte du message</string> <string name="notification_preview_mode_hidden">Caché</string> <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Pour protéger vos informations, activez la fonction SimpleX Lock. @@ -132,7 +132,7 @@ <string name="notifications_mode_off">Exécuter lorsque l’app est ouverte</string> <string name="notifications_mode_service">Toujours activé</string> <string name="failed_to_parse_chat_title">Échec du chargement du chat</string> - <string name="failed_to_parse_chats_title">Échec du chargement des chats</string> + <string name="failed_to_parse_chats_title">Échec du chargement des discussions</string> <string name="contact_developers">Veuillez mettre à jour l’app et contacter les développeurs.</string> <string name="simplex_service_notification_text">Récupération des messages…</string> <string name="settings_notification_preview_title">Aperçu de notification</string> @@ -147,8 +147,8 @@ <string name="you_have_no_chats">Vous n\'avez aucune discussion</string> <string name="images_limit_title">Trop d’images !</string> <string name="share_file">Partager le fichier…</string> - <string name="attach">Attacher</string> - <string name="icon_descr_cancel_image_preview">Annuler l’aperçu d’image</string> + <string name="attach">Joindre</string> + <string name="icon_descr_cancel_image_preview">Annuler l\'aperçu de l\'image</string> <string name="icon_descr_cancel_file_preview">Annuler l’aperçu du fichier</string> <string name="icon_descr_sent_msg_status_send_failed">échec d’envoi</string> <string name="icon_descr_received_msg_status_unread">non lu</string> @@ -206,7 +206,6 @@ <string name="gallery_video_button">Vidéo</string> <string name="to_start_a_new_chat_help_header">Pour démarrer une nouvelle discussion</string> <string name="chat_help_tap_button">Appuyez sur le bouton</string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Scanner un code QR</b> : pour vous connecter à votre contact qui vous montre un code QR.]]></string> <string name="to_connect_via_link_title">Pour se connecter via un lien</string> <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Si vous avez reçu un lien d\'invitation SimpleX Chat, vous pouvez l\'ouvrir dans votre navigateur :</string> <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 bureau : scanner le code QR affiché depuis l\'app, via <b>Scanner le code QR</b>.]]></string> @@ -246,7 +245,6 @@ <string name="paste_button">Coller</string> <string name="this_string_is_not_a_connection_link">Cette chaîne n\'est pas un lien de connexion !</string> <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Vous pouvez aussi vous connecter en cliquant sur le lien. Si il s\'ouvre dans le navigateur, cliquez sur <b>Ouvrir dans l\'app mobile</b>.]]></string> - <string name="create_one_time_link">Créer un lien d\'invitation unique</string> <string name="text_field_set_contact_placeholder">Définir le nom du contact…</string> <string name="icon_descr_server_status_disconnected">Déconnecté</string> <string name="icon_descr_server_status_error">Erreur</string> @@ -277,7 +275,7 @@ <string name="icon_descr_email">E-mail</string> <string name="connect_button">Se connecter</string> <string name="notifications_mode_off_desc">L\'application peut recevoir des notifications uniquement lorsqu\'elle est en cours d\'exécution, aucun service d\'arrière-plan ne sera lancé.</string> - <string name="notifications_mode_service_desc">Le service d\'arrière-plan fonctionne en permanence. Les notifications s\'affichent dès que les messages sont disponibles.</string> + <string name="notifications_mode_service_desc">Le service est toujours en cours d’exécution en arrière-plan, les notifications s’afficheront dès que les messages seront disponibles.</string> <string name="notification_preview_mode_message_desc">Afficher le contact et le message</string> <string name="notification_display_mode_hidden_desc">Masquer le contact et le message</string> <string name="auth_log_in_using_credential">Connectez-vous en utilisant votre identifiant</string> @@ -296,7 +294,6 @@ <string name="thank_you_for_installing_simplex">Merci d\'avoir installé SimpleX Chat !</string> <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Vous pouvez <font color="#0088ff">vous connecter aux développeurs de SimpleX Chat pour leur poser des questions et recevoir des réponses :</font>.]]></string> <string name="above_then_preposition_continuation">ci-dessus, puis :</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Ajouter un nouveau contact</b> : afin de créer un code QR à usage unique pour votre contact.]]></string> <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Si vous choisissez de la rejeter, l\'expéditeur·rice NE sera PAS notifié·e.</string> <string name="accept_contact_button">Accepter</string> <string name="mute_chat">Muet</string> @@ -305,7 +302,7 @@ <string name="invalid_contact_link">Lien invalide !</string> <string name="this_link_is_not_a_valid_connection_link">Ce lien n\'est pas un lien de connexion valide !</string> <string name="connection_request_sent">Demande de connexion envoyée !</string> - <string name="file_will_be_received_when_contact_is_online">Le fichier sera reçu quand votre contact sera en ligne, merci d\'attendre ou de revenir plus tard !</string> + <string name="file_will_be_received_when_contact_is_online">Le fichier sera reçu lorsque votre contact sera en ligne, veuillez patienter ou vérifier plus tard !</string> <string name="voice_message_send_text">Message vocal…</string> <string name="maximum_supported_file_size">La taille maximale supportés des fichiers actuellement est de %1$s.</string> <string name="voice_message_with_duration">Message vocal (%1$s)</string> @@ -316,7 +313,7 @@ <string name="scan_code_from_contacts_app">Scannez le code de sécurité depuis l\'application de votre contact.</string> <string name="to_verify_compare">Pour vérifier le chiffrement de bout en bout avec votre contact, comparez (ou scannez) le code sur vos appareils.</string> <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Si vous ne pouvez pas vous rencontrer en personne, vous pouvez <b>scanner un code QR lors d\'un appel vidéo</b>, ou votre contact peut partager un lien d\'invitation.]]></string> - <string name="smp_servers_add">Ajouter un serveur…</string> + <string name="smp_servers_add">Ajouter un serveur</string> <string name="markdown_in_messages">Markdown dans les messages</string> <string name="smp_servers_preset_add">Ajouter des serveurs prédéfinis</string> <string name="use_simplex_chat_servers__question">Utiliser les serveurs SimpleX Chat \?</string> @@ -324,15 +321,12 @@ <string name="ensure_ICE_server_address_are_correct_format_and_unique">Assurez-vous que les adresses des serveurs WebRTC ICE sont au bon format et ne sont pas dupliquées, un par ligne.</string> <string name="network_enable_socks_info">Accéder aux serveurs via un proxy SOCKS sur le port %d \? Le proxy doit être démarré avant d\'activer cette option.</string> <string name="network_use_onion_hosts">Utiliser les hôtes .onions</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">Les hôtes .onion seront utilisés lorsqu\'ils sont disponibles.</string> - <string name="network_use_onion_hosts_required_desc_in_alert">Les hôtes .onion seront nécessaires pour la connexion.</string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Vous contrôlez par quel·s serveur·s vous pouvez <b>transmettre</b> ainsi que par quel·s serveur·s vous pouvez <b>recevoir</b> les messages de vos contacts.]]></string> <string name="your_settings">Vos paramètres</string> <string name="chat_lock">SimpleX Lock</string> <string name="chat_console">Console du chat</string> <string name="smp_servers">Serveurs SMP</string> <string name="smp_servers_test_servers">Tester les serveurs</string> - <string name="smp_servers_save">Sauvegarder les serveurs</string> + <string name="smp_servers_save">Enregistrer les serveurs</string> <string name="smp_servers_scan_qr">Scanner un code QR de serveur</string> <string name="smp_servers_use_server">Utiliser ce serveur</string> <string name="smp_servers_use_server_for_new_conn">Utiliser pour les nouvelles connexions</string> @@ -347,7 +341,7 @@ <string name="your_ICE_servers">Vos serveurs ICE</string> <string name="configure_ICE_servers">Configurer les serveurs ICE</string> <string name="network_settings">Paramètres réseau avancés</string> - <string name="network_settings_title">Paramètres réseau</string> + <string name="network_settings_title">Paramètres avancés</string> <string name="network_enable_socks">Utiliser un proxy SOCKS \?</string> <string name="network_disable_socks">Utiliser une connexion Internet directe \?</string> <string name="network_disable_socks_info">Si vous confirmez, les serveurs de messagerie seront en mesure de voir votre adresse IP, votre fournisseur ainsi que les serveurs auxquels vous vous connectez.</string> @@ -358,8 +352,8 @@ <string name="create_address">Créer une adresse</string> <string name="your_current_profile">Votre profil de chat</string> <string name="edit_image">Modifier l\'image</string> - <string name="save_and_notify_contacts">Sauvegarder et notifier les contacts</string> - <string name="save_and_notify_group_members">Sauvegarder et en informer les membres du groupe</string> + <string name="save_and_notify_contacts">Enregistrer et en informer les contacts</string> + <string name="save_and_notify_group_members">Enregistrer et en informer les membres du groupe</string> <string name="your_profile_is_stored_on_your_device">Votre profil, vos contacts et les messages reçus sont stockés sur votre appareil.</string> <string name="profile_is_only_shared_with_your_contacts">Le profil n\'est partagé qu\'avec vos contacts.</string> <string name="display_name_cannot_contain_whitespace">Le nom d\'affichage ne peut pas contenir d\'espace.</string> @@ -375,13 +369,12 @@ <string name="callstate_received_answer">réponse reçu…</string> <string name="callstate_received_confirmation">confimation reçu…</string> <string name="callstate_connecting">connexion…</string> - <string name="opensource_protocol_and_code_anybody_can_run_servers">Protocole et code open-source – n\'importe qui peut heberger un serveur.</string> - <string name="to_protect_privacy_simplex_has_ids_for_queues">Pour protéger votre vie privée, au lieu d\'IDs utilisés par toutes les autres plateformes, SimpleX possède des IDs pour les queues de messages, distinctes pour chacun de vos contacts.</string> - <string name="read_more_in_github">Plus d\'informations sur notre GitHub.</string> - <string name="paste_the_link_you_received">Coller le lien reçu</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">N\'importe qui peut heberger un serveur.</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">Pour protéger votre vie privée, SimpleX utilise des identifiants distincts pour chacun de vos contacts.</string> + <string name="paste_the_link_you_received">Collez le lien que vous avez reçu</string> <string name="use_chat">Utiliser le chat</string> <string name="onboarding_notifications_mode_title">Notifications privées</string> - <string name="onboarding_notifications_mode_subtitle">Peut être modifié ultérieurement via les paramètres.</string> + <string name="onboarding_notifications_mode_subtitle">Comment il affecte la batterie</string> <string name="onboarding_notifications_mode_off">Quand l\'application fonctionne</string> <string name="onboarding_notifications_mode_periodic">Périodique</string> <string name="onboarding_notifications_mode_service">Instantanée</string> @@ -389,10 +382,10 @@ <string name="about_simplex_chat">À propos de SimpleX Chat</string> <string name="how_to_use_simplex_chat">Comment l\'utiliser</string> <string name="markdown_help">Aide Markdown</string> - <string name="save_servers_button">Sauvegarder</string> + <string name="save_servers_button">Enregistrer</string> <string name="network_and_servers">Réseau et serveurs</string> - <string name="save_and_notify_contact">Sauvegarder et en informer les contacts</string> - <string name="exit_without_saving">Quitter sans sauvegarder</string> + <string name="save_and_notify_contact">Enregistrer et en informer le contact</string> + <string name="exit_without_saving">Quitter sans enregistrer</string> <string name="callstatus_rejected">appel rejeté</string> <string name="callstatus_in_progress">appel en cours</string> <string name="callstatus_ended">appel terminé %1$s</string> @@ -413,16 +406,14 @@ <string name="smp_servers_your_server_address">Votre adresse de serveur</string> <string name="smp_servers_invalid_address">Adresse de serveur invalide !</string> <string name="smp_servers_check_address">Vérifiez l\'adresse du serveur et réessayez.</string> - <string name="using_simplex_chat_servers">Utilise les serveurs SimpleX Chat.</string> + <string name="using_simplex_chat_servers">Vous utilisez les serveurs SimpleX.</string> <string name="how_to">Comment faire</string> <string name="enter_one_ICE_server_per_line">Serveurs ICE (un par ligne)</string> <string name="error_saving_ICE_servers">Erreur lors de la sauvegarde des serveurs ICE</string> - <string name="update_onion_hosts_settings_question">Mettre à jour le paramètre des hôtes .onion \?</string> <string name="network_use_onion_hosts_prefer">Quand disponible</string> <string name="network_use_onion_hosts_no_desc">Les hôtes .onion ne seront pas utilisés.</string> <string name="network_use_onion_hosts_required_desc">Les hôtes .onion seront nécessaires pour la connexion. \nAttention : vous ne pourrez pas vous connecter aux serveurs sans adresse .onion.</string> - <string name="network_use_onion_hosts_no_desc_in_alert">Les hôtes .onion ne seront pas utilisés.</string> <string name="delete_address__question">Supprimer l\'adresse \?</string> <string name="all_your_contacts_will_remain_connected">Tous vos contacts resteront connectés.</string> <string name="share_link">Partager le lien</string> @@ -431,7 +422,7 @@ <string name="full_name__field">Nom complet :</string> <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Votre profil est stocké sur votre appareil et partagé uniquement avec vos contacts. Les serveurs SimpleX ne peuvent pas voir votre profil.</string> <string name="delete_image">Supprimer l\'image</string> - <string name="save_preferences_question">Sauvegarder les préférences \?</string> + <string name="save_preferences_question">Enregistrer les préférences ?</string> <string name="you_control_your_chat">Vous maîtrisez vos discussions !</string> <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">La plateforme de messagerie et d\'applications qui protège votre vie privée et votre sécurité.</string> <string name="we_do_not_store_contacts_or_messages_on_servers">Nous ne stockons aucun de vos contacts ou messages (une fois délivrés) sur les serveurs.</string> @@ -447,23 +438,22 @@ <string name="callstate_waiting_for_confirmation">en attente de confirmation…</string> <string name="callstate_connected">connecté</string> <string name="callstate_ended">terminé</string> - <string name="next_generation_of_private_messaging">La nouvelle génération de messagerie privée</string> + <string name="next_generation_of_private_messaging">L\'avenir de la messagerie</string> <string name="privacy_redefined">La vie privée redéfinie</string> - <string name="first_platform_without_user_ids">La 1ère plateforme sans aucun identifiant d\'utilisateur – privée par design.</string> - <string name="immune_to_spam_and_abuse">Protégé du spam et des abus</string> - <string name="people_can_connect_only_via_links_you_share">On ne peut se connecter à vous qu’avec les liens que vous partagez.</string> + <string name="first_platform_without_user_ids">Aucun identifiant d\'utilisateur.</string> + <string name="immune_to_spam_and_abuse">Protégé du spam</string> + <string name="people_can_connect_only_via_links_you_share">Vous choisissez qui peut se connecter.</string> <string name="decentralized">Décentralisé</string> <string name="create_your_profile">Créez votre profil</string> <string name="make_private_connection">Établir une connexion privée</string> <string name="how_it_works">Comment ça fonctionne</string> <string name="how_simplex_works">Comment SimpleX fonctionne</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[Beaucoup se demandent : <i>si SimpleX n\'a pas d\'identifiant d\'utilisateur, comment peut-il transmettre des messages \?</i>]]></string> - <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Seuls les appareils clients stockent les profils des utilisateurs, les contacts, les groupes et les messages envoyés avec un <b>chiffrement de bout en bout à deux couches</b>.]]></string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Seuls les appareils clients stockent les profils des utilisateurs, les contacts, les groupes et les messages.</string> <string name="read_more_in_github_with_link"><![CDATA[Pour en savoir plus, consultez notre <font color="#0088ff">GitHub repository</font>.]]></string> - <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Batterie peu utilisée</b>. Le service de fond vérifie les messages toutes les 10 minutes. Vous risquez de manquer des appels ou des messages urgents.]]></string> - <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Batterie plus utilisée </b> ! Le service de fond est toujours en cours d\'exécution - les notifications s\'affichent dès que les messages sont disponibles.]]></string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Batterie peu utilisée</b>. L\'app vérifie les messages toutes les 10 minutes. Vous risquez de manquer des appels ou des messages urgents.]]></string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Consomme davantage de batterie </b> L\'app fonctionne toujours en arrière-plan - les notifications s\'affichent instantanément.]]></string> <string name="integrity_msg_skipped">%1$d message(s) manqué(s)</string> - <string name="integrity_msg_bad_id">ID de message incorrecte</string> + <string name="integrity_msg_bad_id">ID du message incorrect</string> <string name="settings_section_title_settings">PARAMÈTRES</string> <string name="alert_text_skipped_messages_it_can_happen_when">Cela peut arriver quand : \n1. Les messages ont expiré dans le client expéditeur après 2 jours ou sur le serveur après 30 jours. @@ -481,7 +471,6 @@ <string name="restore_passphrase_not_found_desc">La phrase secrète n\'a pas été trouvée dans le Keystore, veuillez la saisir manuellement. Cela a pu se produire si vous avez restauré les données de l\'app à l\'aide d\'un outil de sauvegarde. Si ce n\'est pas le cas, veuillez contacter les développeurs.</string> <string name="restore_database_alert_desc">Veuillez entrer le mot de passe précédent après avoir restauré la sauvegarde de la base de données. Cette action ne peut pas être annulée.</string> <string name="database_restore_error">Erreur de restauration de la base de données</string> - <string name="archive_created_on_ts">Créé le %1$s</string> <string name="encrypted_video_call">appel vidéo (chiffrement de bout en bout)</string> <string name="audio_call_no_encryption">appel audio (sans chiffrement)</string> <string name="encrypted_audio_call">appel audio (chiffrement de bout en bout)</string> @@ -500,7 +489,7 @@ <string name="icon_descr_call_ended">Appel terminé</string> <string name="your_privacy">Votre vie privée</string> <string name="settings_section_title_device">APPAREIL</string> - <string name="settings_section_title_chats">CHATS</string> + <string name="settings_section_title_chats">DISCUSSIONS</string> <string name="settings_developer_tools">Outils du développeur</string> <string name="settings_section_title_icon">ICONE DE L\'APP</string> <string name="your_chat_database">Votre base de données de chat</string> @@ -509,7 +498,7 @@ <string name="restart_the_app_to_use_imported_chat_database">Redémarrez l\'application pour utiliser la base de données de chat importée.</string> <string name="chat_item_ttl_day">1 jour</string> <string name="delete_messages">Supprimer les messages</string> - <string name="save_passphrase_in_keychain">Sauvegarder la phrase secrète dans le keystore</string> + <string name="save_passphrase_in_keychain">Enregistrer la phrase secrète dans le Keystore</string> <string name="database_encrypted">Base de données chiffrée !</string> <string name="error_encrypting_database">Erreur lors du chiffrement de la base de données</string> <string name="update_database">Mise à jour</string> @@ -574,7 +563,7 @@ <string name="you_sent_group_invitation">Vous avez envoyé une invitation de groupe</string> <string name="rcv_group_event_member_left">a quitté</string> <string name="icon_descr_speaker_on">Haut-parleur ON</string> - <string name="send_link_previews">Envoi d\'aperçus de liens</string> + <string name="send_link_previews">Aperçu des liens</string> <string name="error_deleting_database">Erreur lors de la suppression de la base de données du chat</string> <string name="error_stopping_chat">Erreur lors de l\'arrêt du chat</string> <string name="error_exporting_chat_database">Erreur lors de l\'exportation de la base de données du chat</string> @@ -586,8 +575,8 @@ <string name="contact_wants_to_connect_via_call">%1$s veut se connecter à vous via</string> <string name="your_calls">Vos appels</string> <string name="always_use_relay">Se connecter via relais</string> - <string name="call_on_lock_screen">Appels en écran verrouillé :</string> - <string name="show_call_on_lock_screen">Montrer</string> + <string name="call_on_lock_screen">Appels sur l\'écran de verrouillage :</string> + <string name="show_call_on_lock_screen">Afficher</string> <string name="no_call_on_lock_screen">Désactiver</string> <string name="your_ice_servers">Vos serveurs ICE</string> <string name="webrtc_ice_servers">Serveurs WebRTC ICE</string> @@ -606,12 +595,12 @@ <string name="icon_descr_call_missed">Appel manqué</string> <string name="icon_descr_call_connecting">Appel en connexion</string> <string name="answer_call">Répondre à l\'appel</string> - <string name="integrity_msg_bad_hash">hash de message incorrect</string> + <string name="integrity_msg_bad_hash">mauvais hachage du message</string> <string name="integrity_msg_duplicate">message dupliqué</string> <string name="alert_title_skipped_messages">Messages manqués</string> <string name="privacy_and_security">Vie privée et sécurité</string> <string name="protect_app_screen">Protéger l\'écran de l\'app</string> - <string name="auto_accept_images">Images auto-acceptées</string> + <string name="auto_accept_images">Acceptation automatique des images</string> <string name="full_backup">Sauvegarde des données de l\'app</string> <string name="settings_section_title_you">VOUS</string> <string name="settings_section_title_help">AIDE</string> @@ -628,7 +617,6 @@ <string name="error_importing_database">Erreur lors de l\'importation de la base de données du chat</string> <string name="chat_database_imported">Base de données du chat importée</string> <string name="delete_chat_profile_question">Supprimer le profil du chat \?</string> - <string name="stop_chat_to_enable_database_actions">Arrêter le chat pour agir sur la base de données.</string> <string name="delete_files_and_media_question">Supprimer les fichiers et médias \?</string> <string name="delete_files_and_media_desc">Cette action ne peut être annulée - tous les fichiers et médias reçus et envoyés seront supprimés. Les photos à faible résolution seront conservées.</string> <string name="no_received_app_files">Aucun fichier reçu ou envoyé</string> @@ -650,20 +638,15 @@ <string name="change_database_passphrase_question">Changer la phrase secrète de la base de données \?</string> <string name="database_will_be_encrypted">La base de données sera chiffrée.</string> <string name="keychain_error">Erreur de la keychain</string> - <string name="file_with_path">Fichier : %s</string> + <string name="file_with_path">Fichier : %s</string> <string name="database_passphrase_is_required">La phrase secrète de la base de données est nécessaire pour ouvrir le chat.</string> - <string name="save_passphrase_and_open_chat">Sauvegarder la phrase secrète et ouvrir le chat</string> + <string name="save_passphrase_and_open_chat">Enregistrer la phrase secrète et ouvrir le chat</string> <string name="open_chat">Ouvrir le chat</string> <string name="database_backup_can_be_restored">La tentative de modification de la phrase secrète de la base de données n\'a pas abouti.</string> <string name="restore_database_alert_title">Restaurer la sauvegarde de la base de données \?</string> <string name="restore_database_alert_confirm">Restaurer</string> <string name="chat_is_stopped_indication">Le chat est arrêté</string> <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Vous pouvez lancer le chat via les Paramètres / la Base de données de l\'app ou en la redémarrant.</string> - <string name="chat_archive_header">Archives du chat</string> - <string name="chat_archive_section">ARCHIVE DU CHAT</string> - <string name="save_archive">Sauvegarder l\'archive</string> - <string name="delete_archive">Supprimer l\'archive</string> - <string name="delete_chat_archive_question">Supprimer l\'archive du chat \?</string> <string name="group_invitation_item_description">Invitation au groupe %1$s</string> <string name="join_group_question">Rejoindre le groupe \?</string> <string name="you_are_invited_to_group_join_to_connect_with_group_members">Vous êtes invité·e dans un groupe. Rejoignez le pour vous connecter avec ses membres.</string> @@ -687,7 +670,7 @@ <string name="snd_group_event_changed_role_for_yourself">vous avez modifié votre rôle pour %s</string> <string name="snd_group_event_user_left">vous avez quitté</string> <string name="snd_group_event_group_profile_updated">mise à jour du profil de groupe</string> - <string name="rcv_conn_event_switch_queue_phase_completed">adresse modifiée pour vous</string> + <string name="rcv_conn_event_switch_queue_phase_completed">changement de l\'adresse du contact</string> <string name="snd_conn_event_switch_queue_phase_changing_for_member">changement d\'adresse pour %s…</string> <string name="snd_conn_event_switch_queue_phase_changing">changement d\'adresse…</string> <string name="group_member_role_member">membre</string> @@ -717,7 +700,7 @@ <string name="send_live_message">Envoyer un message dynamique</string> <string name="send_live_message_desc">Envoyez un message dynamique - il sera mis à jour pour le⸱s destinataire⸱s au fur et à mesure que vous le tapez</string> <string name="send_verb">Envoyer</string> - <string name="member_role_will_be_changed_with_invitation">Le rôle sera changé pour «%s». Le membre va recevoir une nouvelle invitation.</string> + <string name="member_role_will_be_changed_with_invitation">Son rôle est désormais %s. Le membre recevra une nouvelle invitation.</string> <string name="live">LIVE</string> <string name="button_add_members">Inviter des membres</string> <string name="you_can_share_group_link_anybody_will_be_able_to_connect">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.</string> @@ -728,7 +711,7 @@ <string name="only_group_owners_can_change_prefs">Seuls les propriétaires du groupe peuvent modifier les préférences du groupe.</string> <string name="section_title_for_console">POUR TERMINAL</string> <string name="change_member_role_question">Changer le rôle du groupe \?</string> - <string name="member_role_will_be_changed_with_notification">Le rôle sera changé pour «%s». Les membres du groupe seront notifiés.</string> + <string name="member_role_will_be_changed_with_notification">Son rôle est désormais %s. Tous les membres du groupe en seront informés.</string> <string name="icon_descr_contact_checked">Contact vérifié⸱e</string> <string name="clear_contacts_selection_button">Effacer</string> <string name="num_contacts_selected">%d contact·s sélectionné·e·s</string> @@ -764,21 +747,21 @@ <string name="chat_preferences_off">off</string> <string name="direct_messages">Messages directs</string> <string name="full_deletion">Supprimer pour tous</string> - <string name="only_you_can_delete_messages">Vous êtes le seul à pouvoir supprimer des messages de manière irréversible (votre contact peut les marquer comme supprimé).</string> + <string name="only_you_can_delete_messages">Vous êtes le seul à pouvoir supprimer des messages de manière irréversible (votre contact peut les marquer comme supprimé). (24 heures)</string> <string name="conn_stats_section_title_servers">SERVEURS</string> <string name="receiving_via">Réception via</string> <string name="theme_system">Système</string> <string name="allow_direct_messages">Autoriser l\'envoi de messages directs aux membres.</string> <string name="prohibit_direct_messages">Interdire l\'envoi de messages directs aux membres.</string> - <string name="group_members_can_delete">Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés.</string> - <string name="message_deletion_prohibited_in_chat">La suppression irréversible de messages est interdite dans ce groupe.</string> + <string name="group_members_can_delete">Les membres peuvent supprimer de manière irréversible les messages envoyés. (24 heures)</string> + <string name="message_deletion_prohibited_in_chat">La suppression irréversible de messages est interdite.</string> <string name="sending_via">Envoi via</string> <string name="network_status">État du réseau</string> <string name="switch_receiving_address">Changer d\'adresse de réception</string> <string name="create_secret_group_title">Créer un groupe secret</string> <string name="group_main_profile_sent">Votre profil de chat sera envoyé aux membres du groupe</string> <string name="network_option_enable_tcp_keep_alive">Activer le TCP keep-alive</string> - <string name="network_options_save">Sauvegarder</string> + <string name="network_options_save">Enregistrer</string> <string name="update_network_settings_question">Mettre à jour les paramètres réseau \?</string> <string name="incognito">Incognito</string> <string name="incognito_random_profile">Votre profil aléatoire</string> @@ -786,7 +769,6 @@ <string name="theme_light">Clair</string> <string name="theme_dark">Sombre</string> <string name="theme">Thème</string> - <string name="save_color">Sauvegarder la couleur</string> <string name="reset_color">Réinitialisation des couleurs</string> <string name="color_primary">Principale</string> <string name="chat_preferences_you_allow">Vous autorisez</string> @@ -799,16 +781,16 @@ <string name="group_preferences">Préférences du groupe</string> <string name="set_group_preferences">Définir les préférences du groupe</string> <string name="your_preferences">Vos préférences</string> - <string name="allow_your_contacts_irreversibly_delete">Autorise vos contacts à supprimer de manière irréversible les messages envoyés.</string> + <string name="allow_your_contacts_irreversibly_delete">Autorise vos contacts à supprimer de manière irréversible les messages envoyés. (24 heures)</string> <string name="contacts_can_mark_messages_for_deletion">Vos contacts peuvent marquer les messages pour les supprimer ; vous pourrez les consulter.</string> <string name="allow_your_contacts_to_send_voice_messages">Autorise vos contacts à envoyer des messages vocaux.</string> <string name="allow_voice_messages_only_if">Autoriser les messages vocaux uniquement si votre contact les autorise.</string> <string name="prohibit_sending_voice_messages">Interdire l\'envoi de messages vocaux.</string> <string name="only_you_can_send_disappearing">Seulement vous pouvez envoyer des messages éphémères.</string> <string name="only_you_can_send_voice">Vous seul pouvez envoyer des messages vocaux.</string> - <string name="allow_to_delete_messages">Autoriser la suppression irréversible de messages envoyés.</string> - <string name="disappearing_messages_are_prohibited">Les messages éphémères sont interdits dans ce groupe.</string> - <string name="group_members_can_send_voice">Les membres du groupe peuvent envoyer des messages vocaux.</string> + <string name="allow_to_delete_messages">Autoriser la suppression irréversible de messages envoyés. (24 heures)</string> + <string name="disappearing_messages_are_prohibited">Les messages éphémères sont interdits.</string> + <string name="group_members_can_send_voice">Les membres peuvent envoyer des messages vocaux.</string> <string name="delete_after">Supprimer après</string> <string name="ttl_sec">%d sec</string> <string name="ttl_s">%ds</string> @@ -836,19 +818,18 @@ <string name="allow_your_contacts_to_send_disappearing_messages">Autorise votre contact à envoyer des messages éphémères.</string> <string name="conn_level_desc_direct">directe</string> <string name="group_is_decentralized">Entièrement décentralisé – visible que par ses membres.</string> - <string name="group_members_can_send_disappearing">Les membres du groupes peuvent envoyer des messages éphémères.</string> - <string name="network_options_revert">Revenir en arrière</string> + <string name="group_members_can_send_disappearing">Les membres peuvent envoyer des messages éphémères.</string> <string name="prohibit_sending_disappearing_messages">Interdire l’envoi de messages éphémères.</string> <string name="incognito_info_protects">Le mode incognito protège votre vie privée en utilisant un nouveau profil aléatoire pour chaque contact.</string> <string name="updating_settings_will_reconnect_client_to_all_servers">La mise à jour des ces paramètres reconnectera le client à tous les serveurs.</string> <string name="incognito_info_share">Lorsque vous partagez un profil incognito avec quelqu\'un, ce profil sera utilisé pour les groupes auxquels il vous invite.</string> <string name="chat_preferences_yes">oui</string> <string name="allow_disappearing_messages_only_if">Autorise les messages éphémères seulement si votre contact les autorises.</string> - <string name="allow_irreversible_message_deletion_only_if">Autoriser la suppression irréversible des messages uniquement si votre contact vous l\'autorise.</string> - <string name="only_your_contact_can_delete">Seul votre contact peut supprimer de manière irréversible des messages (vous pouvez les marquer comme supprimé).</string> + <string name="allow_irreversible_message_deletion_only_if">Autoriser la suppression irréversible des messages uniquement si votre contact vous l\'autorise. (24 heures)</string> + <string name="only_your_contact_can_delete">Seul votre contact peut supprimer de manière irréversible des messages (vous pouvez les marquer comme supprimé). (24 heures)</string> <string name="only_your_contact_can_send_disappearing">Seulement votre contact peut envoyer des messages éphémères.</string> - <string name="both_you_and_your_contact_can_send_disappearing">Vous et votre contact êtes tous deux en mesure d\'envoyer des messages éphémères.</string> - <string name="voice_messages_are_prohibited">Les messages vocaux sont interdits dans ce groupe.</string> + <string name="both_you_and_your_contact_can_send_disappearing">Vous et votre contact pouvez envoyer des messages éphémères.</string> + <string name="voice_messages_are_prohibited">Les messages vocaux sont interdits.</string> <string name="group_display_name_field">Saisir le nom du groupe :</string> <string name="conn_level_desc_indirect">indirecte (%1$s)</string> <string name="info_row_group">Groupe</string> @@ -856,14 +837,14 @@ <string name="network_option_seconds_label">sec</string> <string name="network_option_tcp_connection_timeout">Délai de connexion TCP</string> <string name="group_profile_is_stored_on_members_devices">Le profil du groupe est stocké sur les appareils des membres, pas sur les serveurs.</string> - <string name="save_group_profile">Sauvegarder le profil du groupe</string> + <string name="save_group_profile">Enregistrer le profil du groupe</string> <string name="error_saving_group_profile">Erreur lors de la sauvegarde du profil de groupe</string> <string name="network_options_reset_to_defaults">Réinitialisation des valeurs par défaut</string> <string name="network_option_protocol_timeout">Délai du protocole</string> <string name="network_option_ping_interval">Intervalle de PING</string> - <string name="both_you_and_your_contacts_can_delete">Vous et votre contact êtes tous deux en mesure de supprimer de manière irréversible les messages envoyés.</string> + <string name="both_you_and_your_contacts_can_delete">Vous et votre contact pouvez supprimer de manière irréversible les messages envoyés. (24 heures)</string> <string name="message_deletion_prohibited">La suppression irréversible de message est interdite dans ce chat.</string> - <string name="both_you_and_your_contact_can_send_voice">Vous et votre contact êtes tous deux en mesure d\'envoyer des messages vocaux.</string> + <string name="both_you_and_your_contact_can_send_voice">Vous et votre contact pouvez envoyer des messages vocaux.</string> <string name="only_your_contact_can_send_voice">Seul votre contact peut envoyer des messages vocaux.</string> <string name="voice_prohibited_in_this_chat">Les messages vocaux sont interdits dans ce chat.</string> <string name="disappearing_prohibited_in_this_chat">Les messages éphémères sont interdits dans cette discussion.</string> @@ -872,8 +853,8 @@ <string name="allow_to_send_disappearing">Autorise l’envoi de messages éphémères.</string> <string name="prohibit_sending_disappearing">Interdire l’envoi de messages éphémères.</string> <string name="prohibit_message_deletion">Interdire la suppression irréversible des messages.</string> - <string name="group_members_can_send_dms">Les membres du groupe peuvent envoyer des messages directs.</string> - <string name="direct_messages_are_prohibited_in_chat">Les messages directs entre membres sont interdits dans ce groupe.</string> + <string name="group_members_can_send_dms">Les membres peuvent envoyer des messages directs.</string> + <string name="direct_messages_are_prohibited_in_group">Les messages directs entre membres sont interdits dans ce groupe.</string> <string name="v4_4_live_messages_desc">Les destinataires voient les mises à jour au fur et à mesure que vous les tapez.</string> <string name="v4_4_verify_connection_security">Vérifier la sécurité de la connexion</string> <string name="v4_4_verify_connection_security_desc">Comparez les codes de sécurité avec vos contacts.</string> @@ -891,7 +872,7 @@ <string name="v4_4_disappearing_messages_desc">Les messages envoyés seront supprimés après une durée déterminée.</string> <string name="v4_4_live_messages">Messages dynamiques</string> <string name="accept_feature">Accepter</string> - <string name="v4_2_auto_accept_contact_requests">Demandes de contact auto-acceptées</string> + <string name="v4_2_auto_accept_contact_requests">Acceptation automatique des demandes de contact</string> <string name="whats_new">Quoi de neuf \?</string> <string name="v4_2_group_links_desc">Les admins peuvent créer les liens qui permettent de rejoindre les groupes.</string> <string name="accept_feature_set_1_day">Définir 1 jour</string> @@ -901,8 +882,8 @@ <string name="invalid_data">données invalides</string> <string name="invalid_chat">chat invalide</string> <string name="icon_descr_cancel_live_message">Annuler le message dynamique</string> - <string name="feature_offered_item">offert %s</string> - <string name="feature_offered_item_with_param">offert %s: %2s</string> + <string name="feature_offered_item">propose %s</string> + <string name="feature_offered_item_with_param">propose %s : %2s</string> <string name="feature_cancelled_item">annulé %s</string> <string name="app_version_title">Version de l\'app</string> <string name="core_simplexmq_version">simplexmq : v%s (%2s)</string> @@ -924,8 +905,7 @@ <string name="users_delete_with_connections">Profil et connexions au serveur</string> <string name="network_session_mode_transport_isolation">Transport isolé</string> <string name="update_network_session_mode_question">Mettre à jour le mode d\'isolement du transport \?</string> - <string name="network_session_mode_entity_description">Une connexion TCP distincte (et identifiant SOCKS) sera utilisée <b>pour chaque contact et membre de groupe</b>. -\n<b>Veuillez noter</b> : si vous avez de nombreuses connexions, votre consommation de batterie et de réseau peut être nettement plus élevée et certaines liaisons peuvent échouer.</string> + <string name="network_session_mode_entity_description"><![CDATA[Une connexion TCP distincte (et identifiant SOCKS) sera utilisée <b>pour chaque contact et membre de groupe</b>. \n<b>Veuillez noter</b> : si vous avez de nombreuses connexions, votre consommation de batterie et de réseau peut être nettement plus élevée et certaines liaisons peuvent échouer.]]></string> <string name="network_session_mode_user">Profil de chat</string> <string name="users_add">Ajouter un profil</string> <string name="users_delete_data_only">Données de profil local uniquement</string> @@ -936,11 +916,11 @@ <string name="failed_to_create_user_duplicate_desc">Vous avez déjà un profil de chat avec ce même nom affiché. Veuillez choisir un autre nom.</string> <string name="failed_to_create_user_duplicate_title">Nom d\'affichage en double !</string> <string name="v4_4_french_interface">Interface en français</string> - <string name="v4_5_transport_isolation_descr">Par profil de chat (par défaut) ou par connexion (BETA).</string> + <string name="v4_5_transport_isolation_descr">Par profil de discussion (par défaut) ou par connexion (BETA).</string> <string name="v4_5_italian_interface">Interface en italien</string> <string name="v4_5_message_draft">Brouillon de message</string> - <string name="v4_5_reduced_battery_usage_descr">Plus d\'améliorations à venir !</string> - <string name="v4_5_multiple_chat_profiles">Différents profils de chat</string> + <string name="v4_5_reduced_battery_usage_descr">D\'autres améliorations sont à venir !</string> + <string name="v4_5_multiple_chat_profiles">Profils de discussion multiples</string> <string name="v4_5_message_draft_descr">Conserver le brouillon du dernier message, avec les pièces jointes.</string> <string name="v4_5_reduced_battery_usage">Réduction de la consommation de batterie</string> <string name="v4_5_private_filenames">Noms de fichiers privés</string> @@ -962,13 +942,12 @@ <string name="observer_cant_send_message_title">Vous ne pouvez pas envoyer de messages !</string> <string name="group_member_role_observer">observateur</string> <string name="language_system">Système</string> - <string name="smp_save_servers_question">Sauvegarder les serveurs \?</string> + <string name="smp_save_servers_question">Enregistrer les serveurs ?</string> <string name="dont_show_again">Ne plus afficher</string> <string name="button_add_welcome_message">Ajouter un message d\'accueil</string> - <string name="cant_delete_user_profile">Impossible de supprimer le profil d\'utilisateur !</string> <string name="v4_6_group_moderation">Modération de groupe</string> <string name="user_hide">Cacher</string> - <string name="muted_when_inactive">Mute en cas d\'inactivité !</string> + <string name="muted_when_inactive">Mise en sourdine en cas d\'inactivité !</string> <string name="confirm_password">Confirmer le mot de passe</string> <string name="v4_6_reduced_battery_usage">Réduction accrue de l\'utilisation de la batterie</string> <string name="v4_6_chinese_spanish_interface">Interface en chinois et en espagnol</string> @@ -981,8 +960,8 @@ <string name="v4_6_hidden_chat_profiles">Profils de chat cachés</string> <string name="v4_6_group_moderation_descr">Désormais, les administrateurs peuvent : \n- supprimer les messages des membres. -\n- désactiver des membres (rôle \"observateur\")</string> - <string name="save_welcome_message_question">Sauvegarder le message d\'accueil \?</string> +\n- désactiver des membres (rôle "observateur")</string> + <string name="save_welcome_message_question">Enregistrer le message d\'accueil ?</string> <string name="v4_6_group_welcome_message_descr">Choisissez un message à l\'attention des nouveaux membres !</string> <string name="hide_profile">Masquer le profil</string> <string name="v4_6_reduced_battery_usage_descr">D\'autres améliorations sont à venir !</string> @@ -991,13 +970,11 @@ <string name="user_mute">Mute</string> <string name="v4_6_hidden_chat_profiles_descr">Protégez vos profils de chat par un mot de passe !</string> <string name="tap_to_activate_profile">Appuyez pour activer un profil.</string> - <string name="save_and_update_group_profile">Sauvegarder et mettre à jour le profil du groupe</string> + <string name="save_and_update_group_profile">Enregistrer et mettre à jour le profil du groupe</string> <string name="save_profile_password">Enregistrer le mot de passe du profil</string> <string name="to_reveal_profile_enter_password">Pour révéler votre profil caché, entrez le mot de passe dans le champ de recherche de la page Profils de chat.</string> <string name="v4_6_audio_video_calls_descr">Prise en charge du Bluetooth et autres améliorations.</string> <string name="v4_6_chinese_spanish_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string> - <string name="should_be_at_least_one_profile">Il doit y avoir au moins un profil d\'utilisateur.</string> - <string name="should_be_at_least_one_visible_profile">Il doit y avoir au moins un profil d\'utilisateur visible.</string> <string name="user_unhide">Dévoiler</string> <string name="user_unmute">Démute</string> <string name="button_welcome_message">Message d\'accueil</string> @@ -1030,7 +1007,7 @@ <string name="videos_limit_title">Trop de vidéos !</string> <string name="video_descr">Vidéo</string> <string name="icon_descr_video_snd_complete">Vidéo envoyée</string> - <string name="video_will_be_received_when_contact_completes_uploading">La vidéo ne sera reçue que lorsque votre contact aura fini de la transférer.</string> + <string name="video_will_be_received_when_contact_completes_uploading">La vidéo ne sera reçue que lorsque votre contact aura fini la mettre en ligne.</string> <string name="icon_descr_waiting_for_video">En attente de la vidéo</string> <string name="waiting_for_video">En attente de la vidéo</string> <string name="video_will_be_received_when_contact_is_online">La vidéo ne sera reçue que lorsque votre contact sera en ligne. Veuillez patienter ou vérifier plus tard !</string> @@ -1040,8 +1017,8 @@ <string name="smp_server_test_delete_file">Supprimer le fichier</string> <string name="error_saving_xftp_servers">Erreur lors de la sauvegarde des serveurs XFTP</string> <string name="ensure_xftp_server_address_are_correct_format_and_unique">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.</string> - <string name="error_xftp_test_server_auth">Le serveur requiert une autorisation pour uploader, vérifiez le mot de passe</string> - <string name="smp_server_test_upload_file">Transférer le fichier</string> + <string name="error_xftp_test_server_auth">Le serveur requiert une autorisation pour téléverser, vérifiez le mot de passe</string> + <string name="smp_server_test_upload_file">Téléverser le fichier</string> <string name="xftp_servers">Serveurs XFTP</string> <string name="your_XFTP_servers">Vos serveurs XFTP</string> <string name="smp_server_test_compare_file">Comparer le fichier</string> @@ -1058,7 +1035,7 @@ <string name="lock_not_enabled">SimpleX Lock n\'est pas activé !</string> <string name="la_lock_mode_system">Authentification du système</string> <string name="la_authenticate">Authentification</string> - <string name="la_auth_failed">Echec de l\'authentification</string> + <string name="la_auth_failed">Échec de l’authentification</string> <string name="la_change_app_passcode">Modifier le code d\'accès</string> <string name="la_current_app_passcode">Code d\'accès actuel</string> <string name="la_minutes">%d minutes</string> @@ -1078,18 +1055,17 @@ <string name="passcode_set">Code d\'accès défini !</string> <string name="la_mode_system">Système</string> <string name="authentication_cancelled">Authentification annulée</string> - <string name="alert_title_msg_bad_id">Mauvais ID de message</string> - <string name="alert_text_msg_bad_hash">Le hash du message précédent est différent.</string> + <string name="alert_title_msg_bad_id">ID du message incorrect</string> + <string name="alert_text_msg_bad_hash">Le hash du message précédent est différent.\"</string> <string name="alert_text_msg_bad_id">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.</string> <string name="decryption_error">Erreur de déchiffrement</string> <string name="alert_text_fragment_encryption_out_of_sync_old_database">Cela peut se produire lorsque vous ou votre contact avez utilisé une ancienne sauvegarde de base de données.</string> - <string name="alert_title_msg_bad_hash">Mauvais hash de message</string> + <string name="alert_title_msg_bad_hash">Mauvais hachage du message</string> <string name="allow_calls_only_if">Autoriser les appels que si votre contact les autorise.</string> <string name="allow_your_contacts_to_call">Autorise vos contacts à vous appeler.</string> <string name="audio_video_calls">Appels audio/vidéo</string> - <string name="available_in_v51">" -\nDisponible dans la v5.1"</string> + <string name="available_in_v51">\nDisponible dans la v5.1</string> <string name="prohibit_calls">Interdire les appels audio/vidéo.</string> <string name="revoke_file__message">Le fichier sera supprimé des serveurs.</string> <string name="revoke_file__confirm">Révoquer</string> @@ -1098,7 +1074,7 @@ <string name="stop_snd_file__message">L\'envoi du fichier sera interrompu.</string> <string name="alert_text_fragment_please_report_to_developers">Veuillez le signaler aux développeurs.</string> <string name="submit_passcode">Soumettre</string> - <string name="both_you_and_your_contact_can_make_calls">Vous et votre contact pouvez tous deux passer des appels.</string> + <string name="both_you_and_your_contact_can_make_calls">Vous et votre contact pouvez passer des appels.</string> <string name="only_you_can_make_calls">Vous seul pouvez passer des appels.</string> <string name="calls_prohibited_with_this_contact">Les appels audio/vidéo sont interdits.</string> <string name="confirm_passcode">Confirmer le code d\'accès</string> @@ -1123,11 +1099,11 @@ <string name="opening_database">Ouverture de la base de données…</string> <string name="learn_more_about_address">À propos de l\'adresse SimpleX</string> <string name="learn_more">En savoir plus</string> - <string name="you_can_share_your_address">Vous pouvez partager votre adresse sous la forme d\'un lien ou d\'un code QR - tout le monde peut se connecter à vous.</string> + <string name="you_can_share_your_address">Vous pouvez partager votre adresse sous la forme d\'un lien ou d\'un code QR - tout le monde peut l\'utiliser pour vous contacter.</string> <string name="you_wont_lose_your_contacts_if_delete_address">Vous ne perdrez pas vos contacts si vous supprimez votre adresse ultérieurement.</string> <string name="simplex_address">Adresse SimpleX</string> - <string name="you_can_accept_or_reject_connection">Lorsque des personnes demandent à se connecter, vous pouvez les accepter ou les refuser.</string> - <string name="theme_colors_section_title">COULEURS DU THÈME</string> + <string name="you_can_accept_or_reject_connection">Vous pouvez accepter ou refuser les demandes de contacts.</string> + <string name="theme_colors_section_title">COULEURS DE L\'INTERFACE</string> <string name="your_contacts_will_remain_connected">Vos contacts resteront connectés.</string> <string name="share_address_with_contacts_question">Partager l\'adresse avec vos contacts \?</string> <string name="share_with_contacts">Partager avec vos contacts</string> @@ -1136,13 +1112,13 @@ <string name="stop_sharing_address">Cesser le partage d\'adresse \?</string> <string name="invite_friends">Inviter des amis</string> <string name="email_invite_subject">Discutons sur SimpleX Chat</string> - <string name="save_settings_question">Sauvegarder les paramètres \?</string> + <string name="save_settings_question">Enregistrer les paramètres ?</string> <string name="dont_create_address">Ne pas créer d\'adresse</string> <string name="address_section_title">Adresse</string> <string name="share_address">Partager l\'adresse</string> <string name="you_can_share_this_address_with_your_contacts">Vous pouvez partager cette adresse avec vos contacts pour leur permettre de se connecter avec %s.</string> <string name="group_welcome_preview">Aperçu</string> - <string name="color_background">Fond d\'écran</string> + <string name="color_background">Arrière-plan</string> <string name="dark_theme">Thème sombre</string> <string name="export_theme">Exporter le thème</string> <string name="import_theme">Importer un thème</string> @@ -1156,25 +1132,25 @@ <string name="add_address_to_your_profile">Ajoutez une adresse à votre profil, afin que vos contacts puissent la partager avec d\'autres personnes. La mise à jour du profil sera envoyée à vos contacts.</string> <string name="color_secondary_variant">Secondaire supplémentaire</string> <string name="all_your_contacts_will_remain_connected_update_sent">Tous vos contacts resteront connectés. La mise à jour du profil sera envoyée à vos contacts.</string> - <string name="auto_accept_contact">Auto-accepter</string> + <string name="auto_accept_contact">Acceptation automatique</string> <string name="create_simplex_address">Créer une adresse SimpleX</string> <string name="customize_theme_title">Personnaliser le thème</string> <string name="continue_to_next_step">Continuer</string> <string name="error_setting_address">Erreur lors du réglage de l\'adresse</string> - <string name="create_address_and_let_people_connect">Créez une adresse pour permettre aux gens de vous contacter.</string> + <string name="create_address_and_let_people_connect">Vous pouvez créer une adresse pour permettre aux autres utilisateurs de vous contacter.</string> <string name="enter_welcome_message">Entrez un message de bienvenue…</string> <string name="you_can_create_it_later">Vous pouvez la créer plus tard</string> <string name="email_invite_body">Bonjour ! \nContactez-moi via SimpleX Chat : %s</string> <string name="if_you_cant_meet_in_person">Si vous ne pouvez pas vous rencontrer en personne, montrez le code QR lors d\'un appel vidéo ou partagez le lien.</string> - <string name="auth_open_chat_profiles">Ouvrir les profils de chat</string> + <string name="auth_open_chat_profiles">Changer de profil de discussion</string> <string name="color_surface">Menus et alertes</string> <string name="color_received_message">Message reçu</string> <string name="import_theme_error_desc">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.</string> <string name="profile_update_will_be_sent_to_contacts">La mise à jour du profil sera envoyée à vos contacts.</string> <string name="read_more_in_user_guide_with_link"><![CDATA[Pour en savoir plus, consultez le <font color="#0088ff">Guide de l\'utilisateur</font>.]]></string> - <string name="save_auto_accept_settings">Sauvegarder les paramètres d\'acceptation automatique</string> - <string name="scan_qr_to_connect_to_contact">Pour se connecter, votre contact peut scanner le code QR ou utiliser le lien dans l\'application.</string> + <string name="save_auto_accept_settings">Enregistrer les paramètres de validation automatique</string> + <string name="scan_qr_to_connect_to_contact">Pour se connecter, votre contact peut scanner un code QR ou utiliser un lien dans l\'app.</string> <string name="app_passcode_replaced_with_self_destruct">Le code d\'accès de l\'application est remplacé par un code d\'autodestruction.</string> <string name="enable_self_destruct">Activer l\'autodestruction</string> <string name="empty_chat_profile_is_created">Un profil de chat vierge portant le nom fourni est créé et l\'application s\'ouvre normalement.</string> @@ -1204,8 +1180,8 @@ <string name="only_you_can_add_message_reactions">Vous seul pouvez ajouter des réactions aux messages.</string> <string name="allow_message_reactions">Autoriser les réactions aux messages.</string> <string name="prohibit_message_reactions_group">Interdire les réactions aux messages.</string> - <string name="group_members_can_add_message_reactions">Les membres du groupe peuvent ajouter des réactions aux messages.</string> - <string name="message_reactions_are_prohibited">Les réactions aux messages sont interdites dans ce groupe.</string> + <string name="group_members_can_add_message_reactions">Les membres peuvent ajouter des réactions aux messages.</string> + <string name="message_reactions_are_prohibited">Les réactions aux messages sont interdites.</string> <string name="custom_time_unit_hours">heures</string> <string name="custom_time_unit_minutes">minutes</string> <string name="custom_time_unit_seconds">secondes</string> @@ -1218,7 +1194,7 @@ <string name="error_loading_details">Erreur de chargement des détails</string> <string name="edit_history">Historique</string> <string name="info_menu">Info</string> - <string name="search_verb">Recherche</string> + <string name="search_verb">Rechercher</string> <string name="received_message">Message reçu</string> <string name="custom_time_picker_custom">personnalisé</string> <string name="custom_time_picker_select">Choisir</string> @@ -1252,13 +1228,13 @@ <string name="share_text_sent_at">Envoyé le : %s</string> <string name="sent_message">Message envoyé</string> <string name="item_info_no_text">aucun texte</string> - <string name="non_fatal_errors_occured_during_import">Des erreurs non fatales se sont produites lors de l\'importation - vous pouvez consulter la console de chat pour plus de détails.</string> + <string name="non_fatal_errors_occured_during_import">L\'importation a entraîné des erreurs non fatales :</string> <string name="shutdown_alert_desc">Les notifications ne fonctionnent pas tant que vous ne relancez pas l\'application</string> <string name="shutdown_alert_question">Arrêt \?</string> <string name="settings_shutdown">Mise à l\'arrêt</string> <string name="settings_restart_app">Redémarrer</string> <string name="settings_section_title_app">APP</string> - <string name="abort_switch_receiving_address_confirm">Annuler</string> + <string name="abort_switch_receiving_address_confirm">Abandonner</string> <string name="error_aborting_address_change">Erreur lors de l\'annulation du changement d\'adresse</string> <string name="abort_switch_receiving_address_question">Abandonner le changement d\'adresse \?</string> <string name="abort_switch_receiving_address">Annuler le changement d\'adresse</string> @@ -1267,14 +1243,14 @@ <string name="only_owners_can_enable_files_and_media">Seuls les propriétaires du groupe peuvent activer les fichiers et les médias.</string> <string name="favorite_chat">Favoris</string> <string name="prohibit_sending_files">Interdire l\'envoi de fichiers et de médias.</string> - <string name="no_filtered_chats">Pas de chats filtrés</string> + <string name="no_filtered_chats">Aucune discussion filtrés</string> <string name="files_and_media">Fichiers et médias</string> <string name="unfavorite_chat">Défavoris</string> <string name="network_option_protocol_timeout_per_kb">Délai d\'attente du protocole par KB</string> <string name="files_and_media_prohibited">Fichiers et médias interdits !</string> <string name="allow_to_send_files">Permet l\'envoi de fichiers et de médias.</string> - <string name="group_members_can_send_files">Les membres du groupe peuvent envoyer des fichiers et des médias.</string> - <string name="files_are_prohibited_in_group">Les fichiers et les médias sont interdits dans ce groupe.</string> + <string name="group_members_can_send_files">Les membres peuvent envoyer des fichiers et des médias.</string> + <string name="files_are_prohibited_in_group">Les fichiers et les médias sont interdits.</string> <string name="fix_connection_not_supported_by_group_member">Correction non prise en charge par un membre du groupe</string> <string name="settings_section_title_delivery_receipts">ENVOYER DES ACCUSÉS DE RÉCEPTION AUX</string> <string name="sync_connection_force_desc">Le chiffrement fonctionne et le nouvel accord de chiffrement n\'est pas nécessaire. Cela peut provoquer des erreurs de connexion !</string> @@ -1288,7 +1264,7 @@ <string name="snd_conn_event_ratchet_sync_required">renégociation de chiffrement requise pour %s</string> <string name="renegotiate_encryption">Renégocier le chiffrement</string> <string name="receipts_contacts_override_disabled">L\'envoi d\'accusés de réception est désactivé pour les contacts de %d</string> - <string name="snd_conn_event_ratchet_sync_started">accord sur le chiffrement pour %s…</string> + <string name="snd_conn_event_ratchet_sync_started">négociation du chiffrement avec %s…</string> <string name="receipts_section_contacts">Contacts</string> <string name="receipts_contacts_disable_for_all">Désactiver pour tous</string> <string name="receipts_contacts_disable_keep_overrides">Désactiver (conserver les remplacements)</string> @@ -1296,8 +1272,8 @@ <string name="receipts_contacts_enable_for_all">Activer pour tous</string> <string name="receipts_contacts_enable_keep_overrides">Activer (conserver les remplacements)</string> <string name="snd_conn_event_ratchet_sync_agreed">chiffrement accepté pour %s</string> - <string name="v5_2_favourites_filter_descr">Filtrer les messages non lus et favoris.</string> - <string name="v5_2_favourites_filter">Trouver des messages plus rapidement</string> + <string name="v5_2_favourites_filter_descr">Filtrer les favoris et les messages non lus.</string> + <string name="v5_2_favourites_filter">Recherche de message plus rapide</string> <string name="in_reply_to">En réponse à</string> <string name="v5_2_more_things_descr">- une diffusion plus stable des messages. \n- des groupes un peu plus performants. @@ -1311,7 +1287,7 @@ <string name="receipts_contacts_title_enable">Activer les accusés de réception \?</string> <string name="receipts_contacts_title_disable">Désactiver les accusés de réception \?</string> <string name="receipts_contacts_override_enabled">L\'envoi d\'accusés de réception est activé pour les contacts de %d</string> - <string name="conn_event_ratchet_sync_started">accord sur le chiffrement…</string> + <string name="conn_event_ratchet_sync_started">négociation du chiffrement…</string> <string name="conn_event_ratchet_sync_agreed">chiffrement accepté</string> <string name="conn_event_ratchet_sync_ok">chiffrement OK</string> <string name="conn_event_ratchet_sync_allowed">renégociation de chiffrement autorisée</string> @@ -1319,7 +1295,7 @@ <string name="snd_conn_event_ratchet_sync_ok">chiffrement ok pour %s</string> <string name="snd_conn_event_ratchet_sync_allowed">renégociation de chiffrement autorisée pour %s</string> <string name="sender_at_ts">%s à %s</string> - <string name="send_receipts">Envoyer les justificatifs</string> + <string name="send_receipts">Envoi de justificatifs</string> <string name="fix_connection_confirm">Réparer</string> <string name="fix_connection">Réparer la connexion</string> <string name="fix_connection_question">Réparer la connexion\?</string> @@ -1344,7 +1320,7 @@ <string name="receipts_groups_override_enabled">L\'envoi de reçus est activé pour %d groupes</string> <string name="receipts_groups_disable_for_all">Désactivé pour tous les groupes</string> <string name="send_receipts_disabled_alert_title">Les accusés de réception sont désactivés</string> - <string name="in_developing_desc">Cette fonctionnalité n\'est pas encore prise en charge. Essayez la dans la prochaine version.</string> + <string name="in_developing_desc">Cette fonctionnalité est indisponible. Essayez la une prochaine version.</string> <string name="delivery">Distribution</string> <string name="send_receipts_disabled">désactivé</string> <string name="no_info_on_delivery">Pas d\'information sur la distribution</string> @@ -1369,7 +1345,7 @@ <string name="connect_use_current_profile">Utiliser le profil actuel</string> <string name="connect_use_new_incognito_profile">Utiliser un nouveau profil incognito</string> <string name="privacy_message_draft">Brouillon de message</string> - <string name="privacy_show_last_messages">Voir les derniers messages</string> + <string name="privacy_show_last_messages">Aperçu des derniers messages</string> <string name="rcv_group_event_2_members_connected">%s et %s sont connecté.es</string> <string name="rcv_group_event_n_members_connected">%s, %s et %d autres membres sont connectés</string> <string name="rcv_group_event_3_members_connected">%s, %s et %s sont connecté.es</string> @@ -1404,17 +1380,17 @@ <string name="compose_send_direct_message_to_connect">Envoyer un message direct pour vous connecter</string> <string name="member_contact_send_direct_message">envoyer un message direct</string> <string name="rcv_group_event_member_created_contact">s\'est connecté.e de manière directe</string> - <string name="expand_verb">Développer</string> + <string name="expand_verb">Étendre</string> <string name="connect_plan_repeat_connection_request">Répéter la demande de connexion ?</string> <string name="rcv_direct_event_contact_deleted">contact supprimé</string> - <string name="connect_plan_you_are_already_connecting_to_vName">Vous êtes déjà connecté(e) à %1$s.</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Vous êtes déjà connecté(e) à <b>%1$s</b>.]]></string> <string name="error_alert_title">Erreur</string> <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Vous êtes déjà en train de rejoindre le groupe via ce lien.</string> <string name="create_group_button">Créer un groupe</string> <string name="create_another_profile_button">Créer le profil</string> <string name="group_members_2">%s et %s</string> <string name="connect_plan_join_your_group">Rejoindre votre groupe ?</string> - <string name="connect_plan_you_are_already_joining_the_group_vName">Vous êtes déjà en train de rejoindre le groupe %1$s.</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Vous êtes déjà en train de rejoindre le groupe <b>%1$s</b>.]]></string> <string name="connect_plan_this_is_your_own_one_time_link">Voici votre propre lien unique !</string> <string name="marked_deleted_items_description">%d messages marqués comme supprimés</string> <string name="connect_plan_group_already_exists">Ce groupe existe déjà !</string> @@ -1425,11 +1401,11 @@ <string name="connect_plan_connect_via_link">Se connecter via un lien ?</string> <string name="connect_plan_already_joining_the_group">Groupe déjà rejoint !</string> <string name="group_members_n">%s, %s et %d membres</string> - <string name="moderated_items_description">%d messages modérés par %s</string> + <string name="moderated_items_description">%1$d messages modérés par %2$s</string> <string name="unblock_member_button">Débloquer ce membre</string> <string name="connect_plan_connect_to_yourself">Se connecter à soi-même ?</string> <string name="contact_tap_to_connect">Tapez pour vous connecter</string> - <string name="connect_plan_you_are_already_in_group_vName">Vous êtes déjà dans le groupe %1$s.</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Vous êtes déjà dans le groupe <b>%1$s</b>.]]></string> <string name="connect_plan_this_is_your_own_simplex_address">Voici votre propre adresse SimpleX !</string> <string name="correct_name_to">Corriger le nom pour %s ?</string> <string name="delete_messages__question">Supprimer %d messages ?</string> @@ -1447,10 +1423,10 @@ <string name="unblock_member_desc">Les messages de %s seront affichés !</string> <string name="error_sending_message_contact_invitation">Erreur lors de l\'envoi de l\'invitation</string> <string name="non_content_uri_alert_text">Vous avez partagé un chemin de fichier non valide. Signalez le problème aux développeurs de l\'application.</string> - <string name="block_member_question">Bloquer ce membre ?</string> + <string name="block_member_question">Bloquer ce membre ?</string> <string name="rcv_group_events_count">%d événements de groupe</string> <string name="invalid_name">Nom invalide !</string> - <string name="connect_plan_this_is_your_link_for_group_vName">Voici votre lien pour le groupe %1$s !</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Voici votre lien pour le groupe <b>%1$s</b> !]]></string> <string name="unblock_member_confirmation">Débloquer</string> <string name="non_content_uri_alert_title">Chemin du fichier invalide</string> <string name="connect_plan_you_have_already_requested_connection_via_this_address">Vous avez déjà demandé une connexion via cette adresse !</string> @@ -1483,13 +1459,13 @@ <string name="linked_mobiles">Mobiles liés</string> <string name="desktop_device">Bureau</string> <string name="connected_to_desktop">Connecté au bureau</string> - <string name="this_device_name">Ce nom d\'appareil</string> + <string name="this_device_name">Nom de cet appareil</string> <string name="waiting_for_mobile_to_connect">En attente d\'une connexion mobile:</string> <string name="loading_remote_file_title">Chargement du fichier</string> <string name="connecting_to_desktop">Connexion au bureau</string> <string name="desktop_devices">Appareils de bureau</string> <string name="link_a_mobile">Lier un portable</string> - <string name="settings_section_title_use_from_desktop">Utilisation depuis le bureau</string> + <string name="settings_section_title_use_from_desktop">Accès au bureau</string> <string name="connected_mobile">Mobile connecté</string> <string name="session_code">Code de session</string> <string name="desktop_connection_terminated">Connexion terminée</string> @@ -1499,14 +1475,14 @@ <string name="verify_code_on_mobile">Vérifier le code sur le mobile</string> <string name="enter_this_device_name">Entrez le nom de l\'appareil…</string> <string name="error">Erreur</string> - <string name="connect_to_desktop">Se connecter au bureau</string> + <string name="connect_to_desktop">Connexion au bureau</string> <string name="disconnect_remote_host">Se déconnecter</string> <string name="group_member_role_author">auteur</string> <string name="connected_to_mobile">Connecté au portable</string> - <string name="bad_desktop_address">Mauvaise adresse de bureau</string> + <string name="bad_desktop_address">Adresse de bureau incorrecte</string> <string name="paste_desktop_address">Coller l\'adresse du bureau</string> <string name="verify_code_with_desktop">Vérifier le code avec le bureau</string> - <string name="scan_qr_code_from_desktop">Scanner le code QR du bureau</string> + <string name="scan_qr_code_from_desktop">Scannez le code QR du bureau</string> <string name="devices">Appareils</string> <string name="v5_4_more_things_descr">- option pour notifier les contacts supprimés. \n- noms de profil avec espaces. @@ -1522,4 +1498,850 @@ <string name="found_desktop">Bureau trouvé</string> <string name="not_compatible">Non compatible !</string> <string name="multicast_discoverable_via_local_network">Accessible via le réseau local</string> -</resources> \ No newline at end of file + <string name="refresh_qr_code">Rafraîchir</string> + <string name="create_chat_profile">Créer un profil de chat</string> + <string name="no_connected_mobile">Pas de mobile connecté</string> + <string name="disconnect_remote_hosts">Déconnecter les mobiles</string> + <string name="random_port">Aléatoire</string> + <string name="open_port_in_firewall_desc">Afin de permettre à une application mobile de se connecter à votre ordinateur, ouvrez ce port dans votre pare-feu, si vous l\'avez activé</string> + <string name="app_was_crashed">Crash d\'aperçu</string> + <string name="open_port_in_firewall_title">Ouvrir le port de votre pare-feu</string> + <string name="error_showing_content">erreur d\'affichage de contenu</string> + <string name="error_showing_message">erreur d\'affichage de message</string> + <string name="you_can_make_address_visible_via_settings">Vous pouvez le rendre visible à vos contacts SimpleX via Paramètres.</string> + <string name="recent_history_is_not_sent_to_new_members">L\'historique n\'est pas envoyé aux nouveaux membres.</string> + <string name="retry_verb">Réessayer</string> + <string name="camera_not_available">Caméra non disponible</string> + <string name="enable_sending_recent_history">Envoi des 100 derniers messages aux nouveaux membres.</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Ajouter un contact</b> : pour créer un nouveau lien d\'invitation ou se connecter via un lien que vous avez reçu.]]></string> + <string name="disable_sending_recent_history">Ne pas envoyer d\'historique aux nouveaux membres.</string> + <string name="or_show_this_qr_code">Ou montrez ce code</string> + <string name="recent_history_is_sent_to_new_members">Les 100 derniers messages sont envoyés aux nouveaux membres.</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">Le code scanné n\'est pas un code QR de lien SimpleX.</string> + <string name="the_text_you_pasted_is_not_a_link">Le texte collé n\'est pas un lien SimpleX.</string> + <string name="enable_camera_access">Autoriser l\'accès à la caméra</string> + <string name="you_can_view_invitation_link_again">Vous pouvez à nouveau consulter le lien d\'invitation dans les détails de la connexion.</string> + <string name="keep_unused_invitation_question">Conserver l\'invitation inutilisée ?</string> + <string name="share_this_1_time_link">Partagez ce lien d\'invitation unique</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Créer un groupe</b> : pour créer un nouveau groupe.]]></string> + <string name="recent_history">Historique visible</string> + <string name="la_app_passcode">Code d\'accès à l\'app</string> + <string name="new_chat">Nouveau chat</string> + <string name="loading_chats">Chargement des discussions…</string> + <string name="creating_link">Création d\'un lien…</string> + <string name="or_scan_qr_code">Ou scanner le code QR</string> + <string name="invalid_qr_code">Code QR invalide</string> + <string name="add_contact_tab">Ajouter un contact</string> + <string name="tap_to_scan">Appuyez pour scanner</string> + <string name="keep_invitation_link">Conserver</string> + <string name="tap_to_paste_link">Appuyez pour coller le lien</string> + <string name="search_or_paste_simplex_link">Rechercher ou coller un lien SimpleX</string> + <string name="chat_is_stopped_you_should_transfer_database">Le chat est arrêté. Si vous avez déjà utilisé cette base de données sur un autre appareil, vous devez la transférer à nouveau avant de démarrer le chat.</string> + <string name="start_chat_question">Lancer le chat ?</string> + <string name="remote_host_error_bad_state"><![CDATA[État médiocre de la connexion au mobile <b>%s</b>.]]></string> + <string name="remote_ctrl_was_disconnected_title">Connexion interrompue</string> + <string name="remote_ctrl_error_bad_state">État médiocre de la connexion avec le bureau</string> + <string name="remote_ctrl_error_bad_version">La version de l\'ordinateur de bureau n\'est pas prise en charge. Veillez à utiliser la même version sur les deux appareils.</string> + <string name="remote_ctrl_error_disconnected">Le bureau a été déconnecté</string> + <string name="developer_options_section">Options pour les développeurs</string> + <string name="agent_internal_error_title">Erreur interne</string> + <string name="remote_host_error_bad_version"><![CDATA[La version du mobile <b>%s</b> n\'est pas prise en charge. Veillez à utiliser la même version sur les deux appareils.]]></string> + <string name="show_internal_errors">Afficher les erreurs internes</string> + <string name="remote_ctrl_disconnected_with_reason">Déconnecté pour la raison suivante : %s</string> + <string name="failed_to_create_user_invalid_title">Nom d\'affichage invalide !</string> + <string name="failed_to_create_user_invalid_desc">Ce nom d\'affichage est invalide. Veuillez choisir un autre nom.</string> + <string name="remote_ctrl_error_timeout">Délai d\'attente dépassé lors de la connexion au bureau</string> + <string name="agent_critical_error_title">Erreur critique</string> + <string name="agent_critical_error_desc">Veuillez le signaler aux développeurs : +\n%s +\n +\nIl est recommandé de redémarrer l\'application.</string> + <string name="agent_internal_error_desc">Veuillez le signaler aux développeurs : +\n%s</string> + <string name="restart_chat_button">Redémarrer le chat</string> + <string name="group_member_status_unknown_short">inconnu</string> + <string name="group_member_status_unknown">statut inconnu</string> + <string name="remote_host_was_disconnected_title">Connexion interrompue</string> + <string name="past_member_vName">Ancien membre %1$s</string> + <string name="possible_slow_function_desc">La fonctions prend trop de temps à s\'exécuter : %1$d secondes : %2$s</string> + <string name="possible_slow_function_title">Fonction lente</string> + <string name="show_slow_api_calls">Afficher les appels d\'API lents</string> + <string name="remote_host_disconnected_from"><![CDATA[Déconnecté du mobile <b>%s</b> en raison de : %s]]></string> + <string name="remote_host_error_busy"><![CDATA[Mobile <b>%s</b> est occupé]]></string> + <string name="remote_host_error_inactive"><![CDATA[Le mobile <b>%s</b> est inactif]]></string> + <string name="remote_host_error_missing"><![CDATA[Mobile <b>%s</b> est manquant]]></string> + <string name="remote_host_error_disconnected"><![CDATA[Le mobile <b>%s</b> a été déconnecté]]></string> + <string name="remote_host_error_timeout"><![CDATA[Délai d\'attente expiré lors de la connexion au mobile <b>%s</b>]]></string> + <string name="remote_ctrl_error_bad_invitation">Le bureau ne possède pas le bon code d\'invitation</string> + <string name="remote_ctrl_error_busy">Le bureau est occupé</string> + <string name="remote_ctrl_error_inactive">Le bureau est inactif</string> + <string name="v5_5_private_notes_descr">Avec les fichiers et les médias chiffrés.</string> + <string name="v5_5_join_group_conversation_descr">Historique récent et bot d\'annuaire amélioré.</string> + <string name="v5_5_simpler_connect_ui_descr">La barre de recherche accepte les liens d\'invitation.</string> + <string name="v5_5_message_delivery_descr">Consommation réduite de la batterie.</string> + <string name="clear_note_folder_warning">Tous les messages seront supprimés - il n\'est pas possible de revenir en arrière !</string> + <string name="v5_5_new_interface_languages">Interface utilisateur en hongrois et en turc</string> + <string name="v5_5_private_notes">Notes privées</string> + <string name="info_row_created_at">Créé à</string> + <string name="error_creating_message">Erreur lors de la création du message</string> + <string name="v5_5_message_delivery">Amélioration de la transmission des messages</string> + <string name="v5_5_join_group_conversation">Participez aux conversations de groupe</string> + <string name="share_text_created_at">Créé à : %s</string> + <string name="error_deleting_note_folder">Erreur lors de la suppression de notes privées</string> + <string name="v5_5_simpler_connect_ui">Collez le lien pour vous connecter !</string> + <string name="note_folder_local_display_name">Notes privées</string> + <string name="profile_update_event_contact_name_changed">le contact %1$s est devenu %2$s</string> + <string name="profile_update_event_member_name_changed">le membre %1$s est devenu %2$s</string> + <string name="profile_update_event_removed_address">suppression de l\'adresse de contact</string> + <string name="profile_update_event_removed_picture">suppression de la photo de profil</string> + <string name="profile_update_event_set_new_address">a changé d\'adresse de contact</string> + <string name="profile_update_event_set_new_picture">a changé d\'image de profil</string> + <string name="profile_update_event_updated_profile">profil mis à jour</string> + <string name="clear_note_folder_question">Effacer les notes privées ?</string> + <string name="saved_message_title">Message enregistré</string> + <string name="block_for_all">Bloqué pour tous</string> + <string name="member_blocked_by_admin">Bloqué par l\'administrateur</string> + <string name="member_info_member_blocked">bloqué</string> + <string name="error_blocking_member_for_all">Erreur lors du blocage du membre pour tous</string> + <string name="blocked_by_admin_items_description">%d messages bloqués par l\'administrateur</string> + <string name="blocked_by_admin_item_description">bloqué par l\'administrateur</string> + <string name="rcv_group_event_member_blocked">%s bloqué</string> + <string name="rcv_group_event_member_unblocked">%s débloqué</string> + <string name="snd_group_event_member_blocked">vous avez bloqué %s</string> + <string name="snd_group_event_member_unblocked">vous avez débloqué %s</string> + <string name="message_too_large">Message trop volumineux</string> + <string name="unblock_for_all">Débloquer pour tous</string> + <string name="block_for_all_question">Bloquer le membre pour tous ?</string> + <string name="database_migration_in_progress">La migration de la base de données est en cours. +\nCela peut prendre quelques minutes.</string> + <string name="unblock_for_all_question">Débloquer le membre pour tous ?</string> + <string name="welcome_message_is_too_long">Le message de bienvenue est trop long</string> + <string name="call_service_notification_video_call">Appel vidéo</string> + <string name="call_service_notification_end_call">Fin de l\'appel</string> + <string name="call_service_notification_audio_call">Appel audio</string> + <string name="unable_to_open_browser_desc">Le navigateur web par défaut est requis pour les appels. Veuillez configurer le navigateur par défaut dans le système et partager plus d\'informations avec les développeurs.</string> + <string name="unable_to_open_browser_title">Erreur lors de l\'ouverture du navigateur</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Confirmer que vous vous souvenez de la phrase secrète de la base de données pour la transférer.</string> + <string name="v5_6_app_data_migration">Transfert des données de l\'application</string> + <string name="v5_6_safer_groups_descr">Les admins peuvent bloquer un membre pour tous.</string> + <string name="migrate_from_device_exported_file_doesnt_exist">Le fichier exporté n\'existe pas</string> + <string name="migrate_from_device_error_uploading_archive">Erreur lors de l\'envoi de l\'archive</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Tous vos contacts, conversations et fichiers seront chiffrés en toute sécurité et transférés par morceaux vers les relais XFTP configurés.</string> + <string name="migrate_to_device_apply_onion">Appliquer</string> + <string name="migrate_from_device_archive_and_upload">Archiver et téléverser</string> + <string name="migrate_from_device_archiving_database">Archivage de la base de données</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Avertissement</b> : l\'archive sera supprimée.]]></string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Remarque</b> : l\'utilisation d\'une même base de données sur deux appareils interrompra le déchiffrement des messages provenant de vos connexions, par mesure de sécurité.]]></string> + <string name="migrate_from_device_cancel_migration">Annuler le transfert</string> + <string name="migrate_to_device_chat_migrated">Messagerie transférée !</string> + <string name="migrate_from_device_check_connection_and_try_again">Vérifiez votre connexion internet et réessayez</string> + <string name="migrate_to_device_download_failed">Échec du téléchargement</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Sélectionnez <i>Transférer depuis un autre appareil</i> sur le nouvel appareil et scanner le code QR.]]></string> + <string name="migrate_to_device_confirm_network_settings">Confirmer les paramètres réseau</string> + <string name="migrate_from_device_confirm_upload">Confirmer la transmission</string> + <string name="migrate_from_device_creating_archive_link">Création d\'un lien d\'archive</string> + <string name="migrate_from_device_delete_database_from_device">Supprimer la base de données de cet appareil</string> + <string name="migrate_to_device_downloading_archive">Téléchargement de l\'archive</string> + <string name="migrate_to_device_downloading_details">Téléchargement des détails du lien</string> + <string name="v5_6_quantum_resistant_encryption_descr">Activé dans les conversations directes (BETA) !</string> + <string name="migrate_to_device_enter_passphrase">Entrer la phrase secrète</string> + <string name="migrate_from_device_error_deleting_database">Erreur lors de la suppression de la base de données</string> + <string name="migrate_from_device_error_exporting_archive">Erreur lors de l\'exportation de la base de données des chats</string> + <string name="migrate_to_device_error_downloading_archive">Erreur lors du téléchargement de l\'archive</string> + <string name="migrate_from_device_error_saving_settings">Erreur lors de l\'enregistrement des paramètres</string> + <string name="migrate_from_device_error_verifying_passphrase">Erreur lors de la vérification de la phrase secrète :</string> + <string name="e2ee_info_no_pq"><![CDATA[Les messages, fichiers et appels sont protégés par un <b>chiffrement de bout en bout</b> avec une confidentialité persistante, une répudiation et une récupération en cas d\'effraction.]]></string> + <string name="e2ee_info_pq"><![CDATA[Les messages, fichiers et appels sont protégés par un <b>chiffrement e2e résistant post-quantique</b> avec une confidentialité persistante, une répudiation et une récupération en cas d\'effraction.]]></string> + <string name="e2ee_info_no_pq_short">Cette discussion est protégée par un chiffrement de bout en bout.</string> + <string name="e2ee_info_pq_short">Cette discussion est protégée par un chiffrement de bout en bout résistant post-quantique.</string> + <string name="auth_open_migration_to_another_device">Accéder à l\'écran de transfert</string> + <string name="migrate_from_another_device">Transférer depuis un autre appareil</string> + <string name="set_passphrase">Définir une phrase secrète</string> + <string name="conn_event_enabled_pq">chiffrement e2e résistant post-quantique</string> + <string name="migrate_from_device_upload_failed">Échec de l\'envoi</string> + <string name="migrate_from_device_try_again">Vous pouvez faire un nouvel essai.</string> + <string name="migrate_from_device_repeat_upload">Répéter l\'envoi</string> + <string name="v5_6_app_data_migration_descr">Transférer vers un autre appareil via un code QR.</string> + <string name="v5_6_quantum_resistant_encryption">Chiffrement résistant post-quantique</string> + <string name="v5_6_picture_in_picture_calls">Appels picture-in-picture</string> + <string name="v5_6_safer_groups">Groupes plus sûrs</string> + <string name="v5_6_picture_in_picture_calls_descr">Utiliser l\'application pendant l\'appel.</string> + <string name="migrate_to_device_database_init">Préparation du téléchargement</string> + <string name="migrate_to_device_bytes_downloaded">%s téléchargés</string> + <string name="migrate_to_device_importing_archive">Importation de l\'archive</string> + <string name="migrate_to_device_repeat_import">Répéter l\'importation</string> + <string name="migrate_to_device_finalize_migration">Finalisez le transfert sur l\'autre appareil.</string> + <string name="migrate_from_device_title">Transférer l\'appareil</string> + <string name="migrate_from_device_to_another_device">Transférer vers un autre appareil</string> + <string name="migrate_from_device_database_init">Préparation de l\'envoi</string> + <string name="migrate_from_device_chat_should_be_stopped">Pour continuer, le chat doit être interrompu.</string> + <string name="migrate_from_device_stopping_chat">Arrêt du chat</string> + <string name="migrate_from_device_bytes_uploaded">%s envoyé</string> + <string name="migrate_from_device_uploading_archive">Envoi de l\'archive</string> + <string name="migrate_from_device_finalize_migration">Finaliser le transfert</string> + <string name="migrate_from_device_migration_complete">Migration terminée</string> + <string name="migrate_from_device_start_chat">Démarrer le chat</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[Vous <b>ne devez pas</b> utiliser la même base de données sur deux appareils.]]></string> + <string name="migrate_from_device_verify_database_passphrase">Vérifier la phrase secrète de la base de données</string> + <string name="migrate_to_device_repeat_download">Répéter le téléchargement</string> + <string name="migrate_to_device_confirm_network_settings_footer">Veuillez confirmer que les paramètres réseau de cet appareil sont corrects.</string> + <string name="conn_event_disabled_pq">chiffrement de bout en bout standard</string> + <string name="migrate_to_device_file_delete_or_link_invalid">Le fichier a été supprimé ou le lien est invalide</string> + <string name="migrate_to_device_import_failed">Échec de l\'importation</string> + <string name="invalid_file_link">Lien invalide</string> + <string name="migrate_to_device_title">Transférer ici</string> + <string name="migrate_to_device_migrating">Migration</string> + <string name="or_paste_archive_link">Ou coller le lien de l\'archive</string> + <string name="migrate_from_device_or_share_this_file_link">Ou partagez en toute sécurité le lien de ce fichier</string> + <string name="paste_archive_link">Coller le lien de l\'archive</string> + <string name="migrate_from_device_verify_passphrase">Vérifier la phrase secrète</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Attention : démarrer une session de chat sur plusieurs appareils n\'est pas pris en charge et entraînera des dysfonctionnements au niveau de la transmission des messages</string> + <string name="migrate_to_device_try_again">Vous pouvez faire un nouvel essai.</string> + <string name="error_showing_desktop_notification">Erreur lors de l\'affichage de la notification, contacter les développeurs.</string> + <string name="permissions_camera_and_record_audio">Caméra et microphone</string> + <string name="permissions_grant_in_settings">Accorder dans les paramètres</string> + <string name="permissions_required">Octroyer les permissions</string> + <string name="permissions_grant">Accorder la/les permission(s) pour passer des appels</string> + <string name="permissions_record_audio">Microphone</string> + <string name="permissions_open_settings">Ouvrir les paramètres</string> + <string name="permissions_camera">Caméra</string> + <string name="permissions_find_in_settings_and_grant">Retrouvez cette permission dans les paramètres d\'Android et octroyez-la manuellement.</string> + <string name="forwarded_description">transféré</string> + <string name="settings_section_title_network_connection">Connexion au réseau</string> + <string name="network_type_cellular">Cellulaire</string> + <string name="files_and_media_not_allowed">Fichiers et médias non autorisés</string> + <string name="simplex_links_not_allowed">Les liens SimpleX ne sont pas autorisés</string> + <string name="voice_messages_not_allowed">Les messages vocaux ne sont pas autorisés</string> + <string name="feature_roles_admins">admins</string> + <string name="feature_roles_all_members">tous les membres</string> + <string name="allow_to_send_simplex_links">Autorise l\'envoi de liens SimpleX.</string> + <string name="feature_enabled_for">Activé pour</string> + <string name="group_members_can_send_simplex_links">Les membres peuvent envoyer des liens SimpleX.</string> + <string name="feature_roles_owners">propriétaires</string> + <string name="prohibit_sending_simplex_links">Interdire l\'envoi de liens SimpleX</string> + <string name="simplex_links_are_prohibited_in_group">Les liens SimpleX sont interdits.</string> + <string name="network_type_no_network_connection">Pas de connexion au réseau</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="network_type_ethernet">Ethernet câblé</string> + <string name="simplex_links">Liens SimpleX</string> + <string name="network_type_other">Autres</string> + <string name="v5_7_network_descr">Connexion réseau plus fiable.</string> + <string name="v5_7_quantum_resistant_encryption_descr">Activé dans les discussions directes !</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="audio_device_earpiece">Écouteurs</string> + <string name="audio_device_speaker">Haut-parleur</string> + <string name="audio_device_wired_headphones">Casque audio</string> + <string name="v5_7_forward_descr">La source du message reste privée.</string> + <string name="saved_description">enregistré</string> + <string name="saved_from_description">enregistré depuis %s</string> + <string name="forwarded_chat_item_info_tab">Transféré</string> + <string name="forwarded_from_chat_item_info_title">Transféré depuis</string> + <string name="recipients_can_not_see_who_message_from">Le(s) destinataire(s) ne peut(vent) pas voir de qui provient ce message.</string> + <string name="saved_from_chat_item_info_title">Enregistré depuis</string> + <string name="saved_chat_item_info_tab">Enregistré</string> + <string name="download_file">Télécharger</string> + <string name="forward_chat_item">Transférer</string> + <string name="forward_message">Transfert du message…</string> + <string name="settings_section_title_profile_images">Images de profil</string> + <string name="network_option_rcv_concurrency">Réception simultanée</string> + <string name="v5_7_forward">Transférer et sauvegarder des messages</string> + <string name="v5_7_call_sounds">Sons d\'appel</string> + <string name="v5_7_network">Gestion du réseau</string> + <string name="v5_7_shape_profile_images">Images de profil modelable</string> + <string name="v5_7_shape_profile_images_descr">Carré, circulaire, ou toute autre forme intermédiaire.</string> + <string name="v5_7_call_sounds_descr">Lors des appels audio et vidéo.</string> + <string name="v5_7_new_interface_languages">Interface utilisateur en lituanien</string> + <string name="message_delivery_warning_title">Avertissement sur la distribution des messages</string> + <string name="srv_error_host">L\'adresse du serveur est incompatible avec les paramètres du réseau.</string> + <string name="srv_error_version">La version du serveur est incompatible avec les paramètres du réseau.</string> + <string name="network_smp_proxy_mode_always_description">Toujours utiliser le routage privé.</string> + <string name="network_smp_proxy_mode_never_description">Ne pas utiliser de routage privé.</string> + <string name="network_smp_proxy_mode_unprotected_description">Utiliser le routage privé avec des serveurs inconnus lorsque l\'adresse IP n\'est pas protégée.</string> + <string name="network_smp_proxy_fallback_allow_description">Envoyez les messages de manière directe lorsque votre serveur ou le serveur de destination ne prend pas en charge le routage privé.</string> + <string name="network_smp_proxy_fallback_allow_protected_description">Envoyer les messages de manière directe lorsque l\'adresse IP est protégée et que votre serveur ou le serveur de destination ne prend pas en charge le routage privé.</string> + <string name="snd_error_expired">Problèmes de réseau - le message a expiré après plusieurs tentatives d\'envoi.</string> + <string name="snd_error_auth">Clé erronée ou connexion non identifiée - il est très probable que cette connexion soit supprimée.</string> + <string name="snd_error_proxy_relay">Serveur de transfert : %1$s +\nErreur au niveau du serveur de destination : %2$s</string> + <string name="snd_error_proxy">Serveur de transfert : %1$s +\nErreur : %2$s</string> + <string name="network_smp_proxy_mode_always">Toujours</string> + <string name="network_smp_proxy_mode_private_routing">Routage privé</string> + <string name="network_smp_proxy_fallback_allow_downgrade">Autoriser la rétrogradation</string> + <string name="update_network_smp_proxy_mode_question">Mode de routage des messages</string> + <string name="network_smp_proxy_mode_never">Jamais</string> + <string name="network_smp_proxy_mode_unknown">Serveurs inconnus</string> + <string name="network_smp_proxy_fallback_prohibit">Non</string> + <string name="network_smp_proxy_fallback_allow_protected">Lorsque l\'IP est masquée</string> + <string name="network_smp_proxy_fallback_allow">Oui</string> + <string name="update_network_smp_proxy_fallback_question">Rabattement du routage des messages</string> + <string name="private_routing_show_message_status">Afficher le statut du message</string> + <string name="protect_ip_address">Protection de l\'adresse IP</string> + <string name="settings_section_title_files">FICHIERS</string> + <string name="settings_section_title_private_message_routing">ROUTAGE PRIVÉ DES MESSAGES</string> + <string name="snd_error_relay">Erreur au niveau du serveur de destination : %1$s</string> + <string name="ci_status_other_error">Erreur : %1$s</string> + <string name="snd_error_quota">Capacité dépassée - le destinataire n\'a pas pu recevoir les messages envoyés précédemment.</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">L\'app demandera une confirmation pour les téléchargements depuis des serveurs de fichiers inconnus (sauf .onion ou lorsque le proxy SOCKS est activé).</string> + <string name="network_smp_proxy_fallback_prohibit_description">Ne pas envoyer de messages directement, même si votre serveur ou le serveur de destination ne prend pas en charge le routage privé.</string> + <string name="private_routing_explanation">Pour protéger votre adresse IP, le routage privé utilise vos serveurs SMP pour délivrer les messages.</string> + <string name="network_smp_proxy_mode_unprotected">Non protégé</string> + <string name="file_not_approved_title">Serveurs inconnus !</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Sans Tor ou un VPN, votre adresse IP sera visible par les serveurs de fichiers.</string> + <string name="network_smp_proxy_mode_unknown_description">Utiliser le routage privé avec des serveurs inconnus.</string> + <string name="file_not_approved_descr">Sans Tor ou un VPN, votre adresse IP sera visible par ces relais XFTP : +\n%1$s.</string> + <string name="color_primary_variant2">Accentuation supplémentaire 2</string> + <string name="wallpaper_advanced_settings">Paramètres avancés</string> + <string name="theme_black">Noir</string> + <string name="chat_theme_apply_to_mode">Appliquer à</string> + <string name="info_row_debug_delivery">Debug de la distribution</string> + <string name="wallpaper_scale_fill">Remplir</string> + <string name="wallpaper_preview_hello_bob">Bonjour Alice !</string> + <string name="v5_8_message_delivery">Amélioration de la transmission des messages</string> + <string name="v5_8_chat_themes_descr">Donnez à vos discussions un style différent !</string> + <string name="message_queue_info">Info sur la file des messages</string> + <string name="v5_8_chat_themes">Nouveaux thèmes de discussion</string> + <string name="message_queue_info_none">aucun</string> + <string name="v5_8_private_routing">Routage privé des messages 🚀</string> + <string name="v5_8_private_routing_descr">Protégez votre adresse IP des relais de messagerie choisis par vos contacts. +\nActivez-le dans les paramètres *Réseau et serveurs*.</string> + <string name="chat_theme_reset_to_user_theme">Réinitialiser au thème de l\'utilisateur</string> + <string name="chat_list_always_visible">Afficher la liste des chats dans une nouvelle fenêtre</string> + <string name="color_wallpaper_tint">Teinte du fond d\'écran</string> + <string name="color_wallpaper_background">Fond d\'écran</string> + <string name="message_queue_info_server_info">info sur la file du serveur : %1$s +\n +\ndernier message reçu : %2$s</string> + <string name="theme_destination_app_theme">Thème de l\'app</string> + <string name="color_mode">Mode de couleur</string> + <string name="color_mode_dark">Sombre</string> + <string name="dark_mode_colors">Couleurs du mode sombre</string> + <string name="wallpaper_preview_hello_alice">Salut Bob !</string> + <string name="color_received_quote">Réponse reçue</string> + <string name="theme_remove_image">Retirer l\'image</string> + <string name="reset_single_color">Réinitialiser la couleur</string> + <string name="color_sent_quote">Réponse envoyée</string> + <string name="wallpaper_scale_repeat">Répéter</string> + <string name="wallpaper_scale">Dimension</string> + <string name="chat_theme_apply_to_all_modes">Tous les modes de couleur</string> + <string name="chat_theme_apply_to_dark_mode">Mode sombre</string> + <string name="wallpaper_scale_fit">Adapter</string> + <string name="chat_theme_apply_to_light_mode">Mode clair</string> + <string name="chat_theme_reset_to_app_theme">Réinitialiser au thème de l\'app</string> + <string name="chat_theme_set_default_theme">Définir le thème par défaut</string> + <string name="v5_8_safe_files_descr">Confirmer les fichiers provenant de serveurs inconnus.</string> + <string name="v5_8_persian_ui">UI en persan</string> + <string name="v5_8_safe_files">Réception de fichiers en toute sécurité</string> + <string name="v5_8_message_delivery_descr">Consommation réduite de la batterie.</string> + <string name="settings_section_title_chat_colors">Couleurs de la discussion</string> + <string name="settings_section_title_chat_theme">Thème de la discussion</string> + <string name="settings_section_title_user_theme">Thème de profil</string> + <string name="color_mode_light">Clair</string> + <string name="color_mode_system">Système</string> + <string name="error_initializing_web_view">Erreur d\'initialisation de WebView. Mettez votre système à jour avec la nouvelle version. Veuillez contacter les développeurs. +\nErreur : %s</string> + <string name="file_error_no_file">Fichier introuvable - le fichier a probablement été supprimé ou annulé.</string> + <string name="file_error_auth">Mauvaise clé ou adresse inconnue du bloc de données du fichier - le fichier est probablement supprimé.</string> + <string name="file_error_relay">Erreur du serveur de fichiers : %1$s</string> + <string name="file_error">Erreur de fichier</string> + <string name="temporary_file_error">Erreur de fichier temporaire</string> + <string name="info_row_file_status">Statut du fichier</string> + <string name="share_text_file_status">Statut du fichier : %s</string> + <string name="info_row_message_status">Statut du message</string> + <string name="share_text_message_status">Statut du message: %s</string> + <string name="copy_error">Erreur de copie</string> + <string name="remote_ctrl_connection_stopped_desc">Veuillez vérifier que le mobile et l\'ordinateur sont connectés au même réseau local et que le pare-feu de l\'ordinateur autorise la connexion. +\nVeuillez faire part de tout autre problème aux développeurs.</string> + <string name="remote_ctrl_connection_stopped_identity_desc">Ce lien a été utilisé avec un autre appareil mobile, veuillez créer un nouveau lien sur le desktop.</string> + <string name="cannot_share_message_alert_title">Impossible d\'envoyer le message</string> + <string name="cannot_share_message_alert_text">Les paramètres de chat sélectionnés ne permettent pas l\'envoi de ce message.</string> + <string name="servers_info_subscriptions_connections_subscribed">Connections actives</string> + <string name="all_users">Tous les profiles</string> + <string name="acknowledged">Reçu avec accusé de réception</string> + <string name="app_check_for_updates_download_completed_title">La mise à jour de l\'app est téléchargée</string> + <string name="completed">Complétées</string> + <string name="current_user">Profil actuel</string> + <string name="decryption_errors">Erreurs de déchiffrement</string> + <string name="member_info_member_disabled">désactivé</string> + <string name="deleted">Supprimées</string> + <string name="app_check_for_updates_notice_disable">Désactiver</string> + <string name="app_check_for_updates_download_started">Téléchargement de la mise à jour de l\'appli, ne pas fermer l\'appli</string> + <string name="app_check_for_updates_button_download">Téléchargement %s (%s)</string> + <string name="servers_info_reconnect_server_error">Erreur de reconnexion au serveur</string> + <string name="member_info_member_inactive">inactif</string> + <string name="scan_paste_link">Scanner / Coller un lien</string> + <string name="member_inactive_desc">Le message peut être transmis plus tard si le membre devient actif.</string> + <string name="servers_info_reconnect_servers_message">Reconnecter tous les serveurs connectés pour forcer la livraison des messages. Cette méthode utilise du trafic supplémentaire.</string> + <string name="servers_info_transport_sessions_section_header">Sessions de transport</string> + <string name="network_error_broker_host_desc">L\'adresse du serveur est incompatible avec les paramètres réseau : %1$s.</string> + <string name="network_error_broker_version_desc">La version du serveur est incompatible avec votre application : %1$s.</string> + <string name="private_routing_error">Erreur de routage privé</string> + <string name="please_try_later">Veuillez essayer plus tard.</string> + <string name="member_inactive_title">Membre inactif</string> + <string name="message_forwarded_title">Message transféré</string> + <string name="message_forwarded_desc">Pas de connexion directe pour l\'instant, le message est transmis par l\'administrateur.</string> + <string name="servers_info_connected_servers_section_header">Serveurs connectés</string> + <string name="servers_info_previously_connected_servers_section_header">Serveurs précédemment connectés</string> + <string name="servers_info_proxied_servers_section_header">Serveurs routés via des proxy</string> + <string name="servers_info_proxied_servers_section_footer">Vous n\'êtes pas connecté à ces serveurs. Le routage privé est utilisé pour leur délivrer des messages.</string> + <string name="servers_info_reconnect_server_title">Reconnecter le serveur ?</string> + <string name="servers_info_reconnect_servers_title">Reconnecter les serveurs ?</string> + <string name="servers_info_reconnect_server_message">Reconnecter le serveur pour forcer la livraison des messages. Utilise du trafic supplémentaire.</string> + <string name="servers_info_reset_stats_alert_error_title">Erreur de réinitialisation des statistiques</string> + <string name="servers_info_reset_stats_alert_confirm">Réinitialiser</string> + <string name="servers_info_reset_stats_alert_message">Les statistiques des serveurs seront réinitialisées - il n\'est pas possible de revenir en arrière !</string> + <string name="servers_info_uploaded">Téléversé</string> + <string name="servers_info_detailed_statistics">Statistiques détaillées</string> + <string name="servers_info_detailed_statistics_sent_messages_header">Messages envoyés</string> + <string name="smp_server">Serveur SMP</string> + <string name="chunks_deleted">Chunks supprimés</string> + <string name="chunks_downloaded">Chunks téléchargés</string> + <string name="downloaded_files">Fichiers téléchargés</string> + <string name="download_errors">Erreurs de téléchargement</string> + <string name="server_address">Adresse du serveur</string> + <string name="upload_errors">Erreurs de téléversement</string> + <string name="app_check_for_updates_beta">Bêta</string> + <string name="app_check_for_updates">Vérifier les mises à jour</string> + <string name="app_check_for_updates_notice_title">Vérifier les mises à jour</string> + <string name="smp_servers_configured">Serveurs SMP configurés</string> + <string name="xftp_servers_configured">Serveurs XFTP configurés</string> + <string name="app_check_for_updates_disabled">Désactivé</string> + <string name="app_check_for_updates_installed_successfully_title">Installé avec succès</string> + <string name="app_check_for_updates_button_install">Installer la mise à jour</string> + <string name="app_check_for_updates_button_open">Ouvrir l\'emplacement du fichier</string> + <string name="smp_servers_other">Autres serveurs SMP</string> + <string name="xftp_servers_other">Autres serveurs XFTP</string> + <string name="app_check_for_updates_installed_successfully_desc">Veuillez redémarrer l\'application.</string> + <string name="app_check_for_updates_button_remind_later">Rappeler plus tard</string> + <string name="subscription_percentage">Afficher le pourcentage</string> + <string name="app_check_for_updates_button_skip">Sauter cette version</string> + <string name="app_check_for_updates_stable">Stable</string> + <string name="app_check_for_updates_notice_desc">Pour être informé des nouvelles versions, activez la vérification périodique des versions Stable ou Bêta.</string> + <string name="app_check_for_updates_update_available">Mise à jour disponible : %s</string> + <string name="app_check_for_updates_canceled">Téléchargement de la mise à jour annulé</string> + <string name="appearance_font_size">Taille de police</string> + <string name="appearance_zoom">Zoom</string> + <string name="attempts_label">tentatives</string> + <string name="servers_info_sessions_connected">Connecté</string> + <string name="servers_info_sessions_connecting">Connexion</string> + <string name="servers_info_details">Détails</string> + <string name="servers_info_downloaded">Téléchargé</string> + <string name="servers_info_modal_error_title">Erreur</string> + <string name="servers_info_reconnect_servers_error">Erreur de reconnexion des serveurs</string> + <string name="servers_info_sessions_errors">Erreurs</string> + <string name="servers_info_files_tab">Fichiers</string> + <string name="servers_info_subscriptions_section_header">Réception de message</string> + <string name="servers_info_messages_received">Messages reçus</string> + <string name="servers_info_messages_sent">Messages envoyés</string> + <string name="servers_info_missing">Pas d\'information, essayez de recharger</string> + <string name="servers_info_subscriptions_connections_pending">En attente</string> + <string name="proxied">Routé via un proxy</string> + <string name="servers_info_detailed_statistics_received_messages_header">Messages reçus</string> + <string name="servers_info_detailed_statistics_received_total">Total reçu</string> + <string name="servers_info_detailed_statistics_receive_errors">Erreurs de réception</string> + <string name="reconnect">Reconnecter</string> + <string name="servers_info_reconnect_all_servers_button">Reconnecter tous les serveurs</string> + <string name="servers_info_reset_stats">Réinitialiser toutes les statistiques</string> + <string name="servers_info_reset_stats_alert_title">Réinitialiser toutes les statistiques ?</string> + <string name="sent_directly">Envoyé directement</string> + <string name="servers_info_detailed_statistics_sent_messages_total">Total envoyé</string> + <string name="sent_via_proxy">Envoyé via un proxy</string> + <string name="servers_info">Infos serveurs</string> + <string name="servers_info_target">Afficher les informations pour</string> + <string name="servers_info_starting_from">À partir de %s.</string> + <string name="servers_info_private_data_disclaimer">À partir de %s. \nToutes les données restent confinées dans votre appareil.</string> + <string name="servers_info_statistics_section_header">Statistiques</string> + <string name="servers_info_subscriptions_total">Total</string> + <string name="xftp_server">Serveur XFTP</string> + <string name="acknowledgement_errors">Erreurs d\'accusé de réception</string> + <string name="chunks_uploaded">Chunks téléversés</string> + <string name="connections">Connexions</string> + <string name="created">Créées</string> + <string name="deletion_errors">Erreurs de suppression</string> + <string name="duplicates_label">doublons</string> + <string name="expired_label">expiré</string> + <string name="open_server_settings_button">Ouvrir les paramètres du serveur</string> + <string name="other_label">autre</string> + <string name="other_errors">autres erreurs</string> + <string name="secured">Sécurisées</string> + <string name="send_errors">Erreurs d\'envoi</string> + <string name="size">Taille</string> + <string name="subscribed">Inscriptions</string> + <string name="subscription_errors">Erreurs d\'inscription</string> + <string name="subscription_results_ignored">Inscriptions ignorées</string> + <string name="uploaded_files">Fichiers téléversés</string> + <string name="privacy_media_blur_radius_medium">Modéré</string> + <string name="privacy_media_blur_radius">Flouter les médias</string> + <string name="proxy_destination_error_broker_host">L\'adresse du serveur de destination %1$s est incompatible avec les paramètres du serveur de redirection %2$s.</string> + <string name="proxy_destination_error_broker_version">La version du serveur de destination %1$s est incompatible avec le serveur de redirection %2$s.</string> + <string name="proxy_destination_error_failed_to_connect">Le serveur de redirection %1$s n\'a pas réussi à se connecter au serveur de destination %2$s. Veuillez réessayer plus tard.</string> + <string name="smp_proxy_error_broker_host">L\'adresse du serveur de redirection est incompatible avec les paramètres du réseau : %1$s.</string> + <string name="smp_proxy_error_broker_version">La version du serveur de redirection est incompatible avec les paramètres du réseau : %1$s.</string> + <string name="privacy_media_blur_radius_strong">Fort</string> + <string name="smp_proxy_error_connecting">Erreur de connexion au serveur de redirection %1$s. Veuillez réessayer plus tard.</string> + <string name="privacy_media_blur_radius_off">Off</string> + <string name="privacy_media_blur_radius_soft">Léger</string> + <string name="toolbar_settings">Paramètres</string> + <string name="info_view_connect_button">se connecter</string> + <string name="info_view_message_button">message</string> + <string name="info_view_open_button">ouvrir</string> + <string name="confirm_delete_contact_question">Confirmer la suppression du contact ?</string> + <string name="delete_contact_cannot_undo_warning">Le contact sera supprimé - il n\'est pas possible de revenir en arrière !</string> + <string name="delete_without_notification">Supprimer sans notification</string> + <string name="keep_conversation">Garder la conversation</string> + <string name="only_delete_conversation">Ne supprimer que la conversation</string> + <string name="info_view_search_button">rechercher</string> + <string name="info_view_video_button">vidéo</string> + <string name="contact_deleted">Contact supprimé !</string> + <string name="you_can_still_send_messages_to_contact">Vous pouvez envoyer des messages à %1$s à partir des contacts archivés.</string> + <string name="deleted_chats">Contacts archivés</string> + <string name="no_filtered_contacts">Pas de contacts filtrés</string> + <string name="paste_link">Coller le lien</string> + <string name="contact_list_header_title">Vos contacts</string> + <string name="one_hand_ui">Barre d\'app accessible</string> + <string name="cant_call_contact_deleted_alert_text">Le contact est supprimé.</string> + <string name="calls_prohibited_alert_title">Les appels ne sont pas autorisés !</string> + <string name="you_need_to_allow_calls">Vous devez autoriser votre contact à appeler pour pouvoir l\'appeler.</string> + <string name="cant_send_message_to_member_alert_title">Impossible d\'envoyer un message à ce membre du groupe</string> + <string name="you_can_still_view_conversation_with_contact">Vous pouvez toujours consulter la conversation avec %1$s dans la liste des conversation.</string> + <string name="allow_calls_question">Autoriser les appels ?</string> + <string name="info_view_call_button">appeler</string> + <string name="cant_call_contact_alert_title">Impossible d\'appeler le contact</string> + <string name="cant_call_contact_connecting_wait_alert_text">Connexion au contact, veuillez patienter ou vérifier plus tard !</string> + <string name="cant_call_member_alert_title">Impossible d\'appeler ce membre du groupe</string> + <string name="conversation_deleted">Conversation supprimée !</string> + <string name="action_button_add_members">Inviter</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">Veuillez demander à votre contact d\'autoriser les appels.</string> + <string name="cant_call_member_send_message_alert_text">Envoyer un message pour activer les appels.</string> + <string name="v6_0_your_contacts_descr">Archiver les contacts pour discuter plus tard.</string> + <string name="v6_0_privacy_blur">Flouter pour une meilleure confidentialité.</string> + <string name="v6_0_connect_faster_descr">Connectez-vous à vos amis plus rapidement.</string> + <string name="v6_0_connection_servers_status_descr">État de la connexion et des serveurs.</string> + <string name="chat_database_exported_title">Exportation de la base de données des discussions</string> + <string name="chat_database_exported_continue">Poursuivre</string> + <string name="delete_messages_mark_deleted_warning">Les messages seront marqués comme étant à supprimer. Le(s) destinataire(s) pourra(ont) révéler ces messages.</string> + <string name="delete_members_messages__question">Supprimer %d messages de membres ?</string> + <string name="compose_message_placeholder">Message</string> + <string name="v6_0_increase_font_size">Augmenter la taille de la police.</string> + <string name="create_address_button">Créer</string> + <string name="selected_chat_items_nothing_selected">Rien n\'est sélectionné</string> + <string name="error_parsing_uri_title">Lien invalide</string> + <string name="new_message">Nouveau message</string> + <string name="v6_0_new_media_options">Nouvelles options de médias</string> + <string name="invite_friends_short">Inviter</string> + <string name="v6_0_new_chat_experience">Nouvelle expérience de discussion 🎉</string> + <string name="v6_0_upgrade_app_descr">Téléchargez les nouvelles versions depuis GitHub.</string> + <string name="v6_0_private_routing_descr">Il protège votre adresse IP et vos connexions.</string> + <string name="v6_0_connection_servers_status">Maîtrisez votre réseau</string> + <string name="v6_0_delete_many_messages_descr">Supprimez jusqu\'à 20 messages à la fois.</string> + <string name="media_and_file_servers">Serveurs de fichiers et de médias</string> + <string name="message_servers">Serveurs de messages</string> + <string name="error_parsing_uri_desc">Veuillez vérifier que le lien SimpleX est exact.</string> + <string name="moderate_messages_will_be_deleted_warning">Les messages seront supprimés pour tous les membres.</string> + <string name="selected_chat_items_selected_n">%d sélectionné(s)</string> + <string name="v6_0_chat_list_media">Aperçu depuis la liste de conversation.</string> + <string name="moderate_messages_will_be_marked_warning">Les messages seront marqués comme modérés pour tous les membres.</string> + <string name="one_hand_ui_card_title">Afficher la liste des conversations :</string> + <string name="one_hand_ui_change_instruction">Vous pouvez choisir de le modifier dans les paramètres d\'apparence.</string> + <string name="reset_all_hints">Rétablir tous les conseils</string> + <string name="v6_0_upgrade_app">Mise à jour automatique de l\'app</string> + <string name="v6_0_reachable_chat_toolbar">Barre d\'outils accessible</string> + <string name="v6_0_reachable_chat_toolbar_descr">Utiliser l\'application d\'une main.</string> + <string name="select_verb">Choisir</string> + <string name="network_socks_proxy">proxy SOCKS</string> + <string name="chat_database_exported_save">Vous pouvez enregistrer l\'archive exportée.</string> + <string name="network_options_save_and_reconnect">Sauvegarder et se reconnecter</string> + <string name="network_option_tcp_connection">Connexion TCP</string> + <string name="chat_database_exported_not_all_files">Certains fichiers n\'ont pas été exportés</string> + <string name="chat_database_exported_migrate">Vous pouvez migrer la base de données exportée.</string> + <string name="n_file_errors">%1$d erreur(s) de fichier :\n%2$s</string> + <string name="n_other_file_errors">%1$d autre(s) erreur(s) de fichier.</string> + <string name="error_forwarding_messages">Erreur lors du transfert de messages</string> + <string name="forward_files_in_progress_desc">%1$d fichier(s) est(sont) en cours de téléchargement.</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s messages non transférés</string> + <string name="forward_files_not_accepted_receive_files">Télécharger</string> + <string name="forward_multiple">Transfert de messages…</string> + <string name="forward_files_messages_deleted_after_selection_desc">Les messages ont été supprimés après avoir été sélectionnés.</string> + <string name="switching_profile_error_title">Erreur lors du changement de profil</string> + <string name="select_chat_profile">Sélectionner un profil de discussion</string> + <string name="new_chat_share_profile">Partager le profil</string> + <string name="switching_profile_error_message">Votre connexion a été déplacée vers %s mais une erreur inattendue s\'est produite lors de la redirection vers le profil.</string> + <string name="network_proxy_auth_mode_no_auth">Ne pas utiliser d\'identifiants avec le proxy.</string> + <string name="network_proxy_incorrect_config_title">Erreur lors de l\'enregistrement du proxy</string> + <string name="network_proxy_password">Mot de passe</string> + <string name="network_proxy_auth">Authentification proxy</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">Utilisez des identifiants de proxy différents pour chaque connexion.</string> + <string name="network_proxy_auth_mode_username_password">Vos informations d\'identification peuvent être envoyées non chiffrées.</string> + <string name="forward_files_failed_to_receive_desc">Le téléchargement de %1$d fichier(s) a échoué.</string> + <string name="forward_files_missing_desc">%1$d fichier(s) a(ont) été supprimé(s).</string> + <string name="v6_1_better_security">Sécurité accrue ✅</string> + <string name="v6_1_better_user_experience">Une meilleure expérience pour l\'utilisateur</string> + <string name="forward_files_not_accepted_desc">%1$d fichier(s) n\'a (n\'ont) pas été téléchargé(s).</string> + <string name="network_session_mode_session">Session de l\'app</string> + <string name="v6_1_message_dates_descr">Meilleures dates de messages.</string> + <string name="forward_alert_title_messages_to_forward">Transférer %1$s message(s) ?</string> + <string name="compose_forward_messages_n">Transfert de %1$s messages</string> + <string name="network_proxy_incorrect_config_desc">Assurez-vous que la configuration du proxy est correcte.</string> + <string name="forward_alert_forward_messages_without_files">Transférer les messages sans les fichiers ?</string> + <string name="network_session_mode_session_description">De nouveaux identifiants SOCKS seront utilisés chaque fois que vous démarrerez l\'application.</string> + <string name="forward_alert_title_nothing_to_forward">Rien à transférer !</string> + <string name="call_desktop_permission_denied_safari">Ouvrez Safari Paramètres / Sites web / Microphone, puis choisissez Autoriser pour localhost.</string> + <string name="compose_save_messages_n">Sauvegarde de %1$s messages</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">L\'archive de la base de données envoyée sera définitivement supprimée des serveurs.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">Utilisez des identifiants de proxy différents pour chaque profil.</string> + <string name="network_proxy_random_credentials">Utiliser des identifiants aléatoires</string> + <string name="network_proxy_username">Nom d\'utilisateur</string> + <string name="delete_messages_cannot_be_undone_warning">Les messages seront supprimés - il n\'est pas possible de revenir en arrière !</string> + <string name="settings_section_title_chat_database">BASE DE DONNÉES DU CHAT</string> + <string name="system_mode_toast">Mode système</string> + <string name="network_session_mode_server">Serveur</string> + <string name="network_session_mode_server_description">De nouveaux identifiants SOCKS seront utilisées pour chaque serveur.</string> + <string name="error_initializing_web_view_wrong_arch">Erreur lors de l\'initialisation de WebView. Assurez-vous que WebView est installé et que l\'architecture supportée est arm64.\nErreur : %s</string> + <string name="icon_descr_sound_muted">Son muet</string> + <string name="settings_message_shape_corner">Coin</string> + <string name="settings_section_title_message_shape">Forme du message</string> + <string name="settings_message_shape_tail">Queue</string> + <string name="call_desktop_permission_denied_chrome">Cliquez sur le bouton info près du champ d\'adresse pour autoriser l\'utilisation du microphone.</string> + <string name="call_desktop_permission_denied_title">Pour passer des appels, autorisez l\'utilisation de votre microphone. Mettez fin à l\'appel et essayez d\'appeler à nouveau.</string> + <string name="migrate_from_device_remove_archive_question">Supprimer l\'archive ?</string> + <string name="v6_1_better_calls">Appels améliorés</string> + <string name="v6_1_customizable_message_descr">Forme des messages personnalisable.</string> + <string name="v6_1_delete_many_messages_descr">Supprimer ou modérer jusqu\'à 200 messages.</string> + <string name="v6_1_forward_many_messages_descr">Transférez jusqu\'à 20 messages à la fois.</string> + <string name="v6_1_better_security_descr">Protocoles SimpleX audité par Trail of Bits.</string> + <string name="v6_1_better_calls_descr">Passer de l\'audio à la vidéo pendant l\'appel.</string> + <string name="v6_1_switch_chat_profile_descr">Changer de profil de chat pour les invitations à usage unique.</string> + <string name="report_item_archived">rapport archivé</string> + <string name="add_your_team_members_to_conversations">Ajoutez les membres de votre équipe aux conversations.</string> + <string name="onboarding_notifications_mode_service_desc_short">L\'application tourne toujours en arrière-plan</string> + <string name="report_item_archived_by">rapport archivé par %s</string> + <string name="report_reason_other">Autre raison</string> + <string name="archive_verb">Archive</string> + <string name="group_reports_active_one">1 rapport</string> + <string name="address_settings">Paramètres de l\'adresse</string> + <string name="appearance_app_toolbars">Barres d\'outils de l\'application</string> + <string name="display_name_accepted_invitation">invitation acceptée</string> + <string name="archive_report">Archiver le signalement</string> + <string name="privacy_chat_list_open_links_ask">Demander</string> + <string name="add_to_list">Ajouter à la liste</string> + <string name="delete_chat_list_warning">Toutes les discussions seront supprimées de la liste %s, et la liste sera supprimée</string> + <string name="button_add_team_members">Ajouter des membres à l\'équipe</string> + <string name="operator_conditions_accepted">Conditions acceptées</string> + <string name="button_add_friends">Ajouter des amis</string> + <string name="accept_conditions">Accepter les conditions</string> + <string name="chat_list_add_list">Ajouter une liste</string> + <string name="chat_list_all">Tous</string> + <string name="onboarding_network_about_operators">À propos des opérateurs</string> + <string name="report_archive_alert_title">Archiver le signalement ?</string> + <string name="business_address">Adresse professionnelle</string> + <string name="v6_2_business_chats">Discussions professionnelles</string> + <string name="appearance_bars_blur_radius">Flouter</string> + <string name="operator_conditions_accepted_on">Conditions acceptées le : %s.</string> + <string name="report_reason_community">Violation des directives communautaires</string> + <string name="operator_conditions_of_use">Conditions d\'utilisation</string> + <string name="create_1_time_link">Créer un lien unique</string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Les conditions seront acceptées pour le(s) opérateur(s) : <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_accepted_on">Les conditions seront acceptées le : %s.</string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[Pour utiliser les serveurs de <b>%s</b>, acceptez les conditions d\'utilisation.]]></string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[Tous les messages et fichiers sont envoyés <b> chiffrés de bout en bout</b>, avec une sécurité post-quantique dans les messages directs.]]></string> + <string name="onboarding_notifications_mode_periodic_desc_short">Réception des messages toutes les 10 minutes</string> + <string name="operators_conditions_accepted_for"><![CDATA[Les conditions sont acceptées pour le(s) opérateur(s) : <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Les mêmes conditions s\'appliquent aux opérateurs : <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_applied"><![CDATA[Ces conditions s\'appliquent également à : <b>%s</b>.]]></string> + <string name="connect_plan_chat_already_exists">La discussion existe déjà !</string> + <string name="connection_error_blocked_desc">La connexion est bloquée par l\'opérateur du serveur :\n%1$s.</string> + <string name="operator_conditions_accepted_for_enabled_operators_on">Les conditions seront automatiquement acceptées pour les opérateurs activés le : %s.</string> + <string name="blocking_reason_content">Le contenu viole les conditions d\'utilisation</string> + <string name="delete_report">Supprimer le rapport</string> + <string name="connection_not_ready">La connexion n\'est pas prête.</string> + <string name="connection_error_blocked">Connexion bloquée</string> + <string name="operators_conditions_will_also_apply"><![CDATA[Ces conditions s\'appliquent également à : <b>%s</b>.]]></string> + <string name="chat_list_contacts">Contacts</string> + <string name="onboarding_network_operators_continue">Continuer</string> + <string name="create_list">Créer une liste</string> + <string name="delete_chat_list_menu_action">Supprimer</string> + <string name="delete_chat_list_question">Supprimer la liste ?</string> + <string name="button_delete_chat">Supprimer la discussion</string> + <string name="info_row_chat">Discussions</string> + <string name="operator_conditions_accepted_for_some"><![CDATA[Les conditions sont déjà acceptées pour le(s) opérateur(s) suivant(s) : <b>%s</b>.]]></string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[Les conditions seront acceptées pour le(s) opérateur(s) : <b>%s</b>.]]></string> + <string name="operator_conditions_failed_to_load">Le texte sur les conditions actuelles n\'a pas pu être chargé. Vous pouvez consulter les conditions en cliquant sur ce lien :</string> + <string name="direct_messages_are_prohibited">Les messages directs entre membres sont interdits.</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Vous êtes déjà connecté avec <b>%1$s</b>.]]></string> + <string name="delete_chat_question">Supprimer la discussion ?</string> + <string name="operator_added_xftp_servers">Ajout de serveurs de médias et de fichiers</string> + <string name="operator_added_message_servers">Ajout de serveurs de messages</string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[Les mêmes conditions s\'appliquent à l\'opérateur <b>%s</b>.]]></string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Appareils Xiaomi</b> : veuillez activer le démarrage automatique dans les paramètres du système pour que les notifications fonctionnent.]]></string> + <string name="delete_chat_for_all_members_cannot_undo_warning">La discussion sera supprimé pour tous les membres - cela ne peut pas être annulé !</string> + <string name="delete_chat_for_self_cannot_undo_warning">Le discussion sera supprimé pour vous - il n\'est pas possible de revenir en arrière !</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">Les conditions seront acceptées pour les opérateurs activés après 30 jours.</string> + <string name="sync_connection_desc">La connexion nécessite une renégociation du chiffrement.</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[Lien utilisable une seule fois <i>avec un seul contact</i> - partagez en personne ou via n\'importe quelle messagerie.]]></string> + <string name="address_or_1_time_link">Adresse ou lien unique ?</string> + <string name="connection_security">Sécurité des connexions</string> + <string name="chat_list_businesses">Professionnels</string> + <string name="file_error_blocked">Le fichier est bloqué par l\'opérateur du serveur :\n%1$s.</string> + <string name="chat_list_favorites">Favoris</string> + <string name="group_reports_active">%d rapports</string> + <string name="group_member_role_moderator">modérateur(trice)</string> + <string name="v6_2_network_decentralization">Décentralisation du réseau</string> + <string name="report_item_visibility_submitter">Seuls vous et les modérateurs le voient</string> + <string name="report_item_visibility_moderators">Seuls l\'expéditeur et les modérateurs le voient</string> + <string name="failed_to_save_servers">Erreur d\'enregistrement des serveurs</string> + <string name="no_message_servers_configured_for_receiving">Pas de serveurs pour recevoir des messages.</string> + <string name="message_deleted_or_not_received_error_title">Pas de message</string> + <string name="for_social_media">Pour les réseaux sociaux</string> + <string name="onboarding_network_operators_review_later">Vérifier plus tard</string> + <string name="network_preset_servers_title">Serveurs prédéfinis</string> + <string name="report_compose_reason_header_other">Signaler autre : seuls les modérateurs du groupe le verront.</string> + <string name="debug_logs">Activer les journaux (logs)</string> + <string name="onboarding_notifications_mode_battery">Notifications et batterie</string> + <string name="display_name_requested_to_connect">demande à se connecter</string> + <string name="no_media_servers_configured">Pas de serveurs de médias et de fichiers.</string> + <string name="no_message_servers_configured">Pas de serveurs de messages.</string> + <string name="no_media_servers_configured_for_private_routing">Pas de serveurs pour recevoir des fichiers.</string> + <string name="report_reason_spam">Spam</string> + <string name="prefs_error_saving_settings">Erreur d\'enregistrement des paramètres</string> + <string name="error_creating_report">Erreur lors de la création du rapport</string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">Les adresses SimpleX et les liens à usage unique peuvent être partagés en toute sécurité via n\'importe quelle messagerie.</string> + <string name="blocking_reason_spam">Spam</string> + <string name="report_verb">Signaler</string> + <string name="no_unread_chats">Pas de discussions non lues</string> + <string name="chat_list_groups">Groupes</string> + <string name="chat_list_group_reports">Signalements</string> + <string name="report_compose_reason_header_profile">Signaler le profil d\'un membre : seuls les modérateurs du groupe le verront.</string> + <string name="report_compose_reason_header_spam">Signaler le spam : seuls les modérateurs du groupe le verront.</string> + <string name="privacy_chat_list_open_web_link">Ouvrir le lien</string> + <string name="privacy_chat_list_open_links">Ouvrir des liens depuis la liste de discussion</string> + <string name="error_updating_server_title">Erreur de mise à jour du serveur</string> + <string name="xftp_servers_per_user">Les serveurs pour les nouveaux fichiers de votre profil de discussion actuel</string> + <string name="operator_server_alert_title">Serveur de l\'opérateur</string> + <string name="error_server_protocol_changed">Le protocole du serveur a été modifié.</string> + <string name="v6_2_network_decentralization_enable_flux">Activer Flux</string> + <string name="message_deleted_or_not_received_error_desc">Ce message a été supprimé ou n\'a pas encore été reçu.</string> + <string name="address_creation_instruction">Appuyez sur Créer une adresse SimpleX dans le menu pour la créer ultérieurement.</string> + <string name="share_address_publicly">Partager publiquement votre adresse</string> + <string name="onboarding_network_operators_app_will_use_for_routing">Par exemple, si votre contact reçoit des messages via un serveur SimpleX Chat, votre application les transmettra via un serveur Flux.</string> + <string name="only_chat_owners_can_change_prefs">Seuls les propriétaires peuvent modifier les préférences.</string> + <string name="member_role_will_be_changed_with_notification_chat">Le rôle deviendra %s. Toutes les personnes présentes dans le discussion en seront informées.</string> + <string name="error_creating_chat_tags">Erreur lors de la création d\'une liste de discussion</string> + <string name="error_loading_chat_tags">Erreur de chargement des listes de discussion</string> + <string name="error_updating_chat_tags">Erreur de mise à jour de la liste des discussions</string> + <string name="no_chats">Pas de discussions</string> + <string name="no_chats_found">Pas de discussions trouvées</string> + <string name="open_with_app">Ouvrir avec %s</string> + <string name="save_list">Sauvegarder la liste</string> + <string name="edit_chat_list_menu_action">Modifier</string> + <string name="share_simplex_address_on_social_media">Partagez votre adresse SimpleX sur les réseaux sociaux.</string> + <string name="onboarding_notifications_mode_off_desc_short">Pas de service d\'arrière-plan</string> + <string name="chat_bottom_bar">Barre de discussion accessible</string> + <string name="operator_review_conditions">Vérifier les conditions</string> + <string name="operator">Opérateur</string> + <string name="operator_servers_title">Serveurs %s</string> + <string name="chat_archive">Ou importer un fichier d\'archive</string> + <string name="operator_open_conditions">Ouvrir les conditions</string> + <string name="operator_use_for_messages_private_routing">Pour le routage privé</string> + <string name="no_message_servers_configured_for_private_routing">Pas de serveurs pour le routage privé des messages.</string> + <string name="error_accepting_operator_conditions">Erreur lors de la validation des conditions</string> + <string name="errors_in_servers_configuration">Erreurs dans la configuration des serveurs.</string> + <string name="for_chat_profile">Pour le profil de discussion %s :</string> + <string name="no_media_servers_configured_for_sending">Pas de serveurs pour envoyer des fichiers.</string> + <string name="maximum_message_size_reached_text">Veuillez réduire la taille du message et envoyer le à nouveau.</string> + <string name="maximum_message_size_reached_non_text">Veuillez réduire la taille du message ou supprimer le média et renvoyer le message.</string> + <string name="v6_2_network_decentralization_enable_flux_reason">pour une meilleure protection des métadonnées.</string> + <string name="v6_2_improved_chat_navigation_descr">- Premier message non lu à l\'ouverture.\n- Sauter aux messages cités.</string> + <string name="v6_2_business_chats_descr">Respect de la vie privée de vos clients.</string> + <string name="v6_2_network_decentralization_descr">Le deuxième opérateur prédéfini de l\'application !</string> + <string name="error_adding_server">Erreur lors de l\'ajout du serveur</string> + <string name="error_saving_database">Erreur d\'enregistrement de la base de données</string> + <string name="report_compose_reason_header_illegal">Signaler un contenu : seuls les modérateurs du groupe le verront.</string> + <string name="onboarding_choose_server_operators">Opérateurs de serveur</string> + <string name="no_chats_in_list">Pas de discussions dans la liste %s.</string> + <string name="error_server_operator_changed">L\'opérateur du serveur a changé.</string> + <string name="privacy_chat_list_open_web_link_question">Ouvrir le lien web ?</string> + <string name="report_compose_reason_header_community">Signaler une infraction : seuls les modérateurs du groupe le verront.</string> + <string name="connection_error_quota_desc">La connexion a atteint la limite des messages non délivrés, votre contact est peut-être hors ligne.</string> + <string name="server_added_to_operator__name">Serveur ajouté à l\'opérateur %s.</string> + <string name="onboarding_network_operators_app_will_use_different_operators">L\'application protège votre vie privée en utilisant des opérateurs différents pour chaque conversation.</string> + <string name="onboarding_select_network_operators_to_use">Sélectionnez les opérateurs de réseau à utiliser.</string> + <string name="onboarding_network_operators_simplex_flux_agreement">SimpleX Chat et Flux ont conclu un accord pour inclure les serveurs exploités par Flux dans l\'application.</string> + <string name="chat_list_notes">Notes</string> + <string name="or_to_share_privately">Ou à partager en privé</string> + <string name="simplex_address_or_1_time_link">Adresse SimpleX ou lien unique ?</string> + <string name="encryption_renegotiation_in_progress">Renégociation du chiffrement en cours.</string> + <string name="sync_connection_confirm">Réparer</string> + <string name="sync_connection_question">Réparer la connexion ?</string> + <string name="share_1_time_link_with_a_friend">Partager un lien unique avec un ami</string> + <string name="to_protect_against_your_link_replaced_compare_codes">Pour vous protéger contre le remplacement de votre lien, vous pouvez comparer les codes de sécurité des contacts.</string> + <string name="how_it_helps_privacy">Comment il contribue à la protection de la vie privée</string> + <string name="report_reason_alert_title">Motif du signalement ?</string> + <string name="report_archive_alert_desc">Le rapport sera archivé pour vous.</string> + <string name="report_reason_illegal">Contenu inapproprié</string> + <string name="smp_servers_new_server">Nouveau serveur</string> + <string name="onboarding_network_operators_configure_via_settings">Vous pouvez configurer les serveurs via les paramètres.</string> + <string name="operator_use_for_sending">Pour envoyer</string> + <string name="report_reason_profile">Profil inapproprié</string> + <string name="operator_website">Site web</string> + <string name="list_menu">Liste</string> + <string name="group_reports_member_reports">Signalement des membres</string> + <string name="change_order_chat_list_menu_action">Modifier l\'ordre</string> + <string name="onboarding_network_operators_update">Mise à jour</string> + <string name="direct_messages_are_prohibited_in_chat">Les messages directs entre membres sont interdits dans cette discussion.</string> + <string name="privacy_chat_list_open_links_no">Non</string> + <string name="privacy_chat_list_open_links_yes">Oui</string> + <string name="operator_info_title">Opérateur de réseau</string> + <string name="operator_use_for_files">Utiliser pour les fichiers</string> + <string name="operator_use_for_messages">Utiliser pour les messages</string> + <string name="view_conditions">Voir les conditions</string> + <string name="onboarding_network_operators">Opérateurs de réseau</string> + <string name="invite_to_chat_button">Inviter à discuter</string> + <string name="duplicated_list_error">Le nom de liste et l\'emoji doivent être différents pour toutes les listes.</string> + <string name="list_name_field_placeholder">Nom de la liste...</string> + <string name="leave_chat_question">Quitter la discussion ?</string> + <string name="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é.</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">Le membre sera retiré de la discussion - cela ne peut pas être annulé !</string> + <string name="chat_main_profile_sent">Votre profil de discussion sera envoyé aux autres membres</string> + <string name="your_servers">Vos serveurs</string> + <string name="use_servers_of_operator_x">Utiliser %s</string> + <string name="operator_use_operator_toggle_description">Utiliser les serveurs</string> + <string name="operator_use_for_messages_receiving">Pour recevoir</string> + <string name="operator_open_changes">Ouvrir les modifications</string> + <string name="connection_error_quota">Messages non distribués</string> + <string name="maximum_message_size_reached_forwarding">Vous pouvez copier et réduire la taille du message pour l\'envoyer.</string> + <string name="button_leave_chat">Quitter la discussion</string> + <string name="view_updated_conditions">Voir les conditions mises à jour</string> + <string name="v6_2_improved_chat_navigation">Navigation améliorée dans les discussions</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">Lorsque plusieurs opérateurs sont activés, aucun d\'entre eux ne dispose de métadonnées permettant de savoir qui communique avec qui.</string> + <string name="maximum_message_size_title">Le message est trop volumineux !</string> + <string name="remote_hosts_section">Mobiles à distance</string> + <string name="change_list">Modifier la liste</string> + <string name="onboarding_network_operators_conditions_you_can_configure">Vous pouvez définir les opérateurs dans les paramètres Réseau et serveurs.</string> + <string name="appearance_in_app_bars_alpha">Transparence</string> + <string name="you_can_set_connection_name_to_remember">Vous pouvez définir un nom de connexion pour vous rappeler avec qui le lien a été partagé.</string> + <string name="enable_sending_member_reports">Autoriser le signalement des messages aux modérateurs.</string> + <string name="chat_item_ttl_year">1 an</string> + <string name="report_archive_alert_desc_all">Tous les signalements seront archivés pour vous.</string> + <string name="report_archive_alert_title_all">Archiver tous les signalements ?</string> + <string name="report_archive_alert_title_nth">Archiver %d signalements ?</string> + <string name="change_automatic_deletion_question">Modifier la suppression automatique des messages ?</string> + <string name="chat_ttl_options_footer">Supprimer les messages de discussion de votre appareil.</string> + <string name="chat_item_ttl_default">par défaut (%s)</string> + <string name="block_members_for_all_question">Bloquer ces membres pour tous ?</string> + <string name="disable_automatic_deletion_question">Désactiver la suppression automatique des messages ?</string> + <string name="disable_automatic_deletion">Désactiver la suppression des messages</string> + <string name="v6_3_organize_chat_lists_descr">Ne manquez pas les messages importants.</string> + <string name="v6_3_mentions_descr">Recevoir une notification en cas de mention.</string> + <string name="v6_3_better_privacy_and_security">Vie privée et sécurité renforcées</string> + <string name="v6_3_faster_deletion_of_groups">Suppression plus rapide des groupes.</string> + <string name="v6_3_faster_sending_messages">Envoi plus rapide des messages.</string> + <string name="report_archive_for_me">Pour moi</string> + <string name="archive_reports">Archiver les rapports</string> + <string name="report_archive_for_all_moderators">Pour tous les modérateurs</string> + <string name="group_members_can_send_reports">Les membres peuvent signaler des messages aux modérateurs.</string> + <string name="v6_3_better_groups_performance">Groupes plus performants</string> + <string name="error_reading_passphrase">Erreur lors de la lecture de la phase secrète de la base de données</string> + <string name="v6_3_reports_descr">Aider les administrateurs à modérer leurs groupes.</string> + <string name="block_members_desc">Tous les nouveaux messages de ces membres seront cachés !</string> +</resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml index 56dfb51e2e..9e0d476dc1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml @@ -36,7 +36,7 @@ <string name="accept_contact_incognito_button">गुप्त स्वीकार करें</string> <string name="smp_servers_preset_add">पूर्वनिर्धारित सर्वर जोड़ें</string> <string name="users_add">प्रोफ़ाइल जोड़ें</string> - <string name="smp_servers_add">सर्वर जोड़े…</string> + <string name="smp_servers_add">सर्वर जोड़े</string> <string name="notifications_mode_service">हमेशा बने रहें</string> <string name="attach">संलग्न करना</string> <string name="network_settings">उन्नत संजाल समायोजन</string> @@ -69,7 +69,6 @@ <string name="personal_welcome">स्वागत %1$s!</string> <string name="callstate_starting">शुरुआत</string> <string name="send_verb">भेजना</string> - <string name="save_color">रंग बचाओ</string> <string name="share_verb">साझा करना</string> <string name="reject_contact_button">अस्वीकार</string> <string name="network_use_onion_hosts_required">आवश्यक</string> @@ -118,7 +117,6 @@ <string name="network_status">नेटवर्क की स्थिति</string> <string name="notification_new_contact_request">नया संपर्क अनुरोध</string> <string name="delete_files_and_media_all">सभी फाइलों को मिटा दें</string> - <string name="delete_archive">संग्रह हटा देना</string> <string name="new_database_archive">नया डेटाबेस संग्रह</string> <string name="new_member_role">नए सदस्य की भूमिका</string> <string name="settings_notifications_mode_title">अधिसूचना सेवा</string> @@ -130,7 +128,6 @@ <string name="settings_notification_preview_title">अधिसूचना पूर्वावलोकन</string> <string name="notifications">सूचनाएं</string> <string name="full_deletion">सभी के लिए हटाएं</string> - <string name="delete_chat_archive_question">लिखचीत संग्रह हटा दे\?</string> <string name="delete_chat_profile_question">चैट प्रोफ़ाइल हटाएं\?</string> <string name="users_delete_question">चैट प्रोफ़ाइल हटाएं\?</string> <string name="users_delete_profile_for">के लिए चैट प्रोफ़ाइल हटाएं</string> @@ -219,9 +216,7 @@ <string name="icon_descr_call_ended">कॉल समाप्त</string> <string name="icon_descr_call_progress">कॉल चल रहा है</string> <string name="auto_accept_images">छवियों को स्वत: स्वीकार करें</string> - <string name="chat_archive_header">चैट संग्रह</string> <string name="chat_is_stopped_indication">चैट रोक दी गई है</string> - <string name="chat_archive_section">चैट संग्रह</string> <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">आप इस समूह से संदेश प्राप्त करना बंद कर देंगे। चैट इतिहास संरक्षित किया जाएगा।</string> <string name="rcv_group_event_changed_member_role">%s की भूमिका को %s में बदला</string> <string name="group_member_status_complete">पूर्ण</string> 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 bd1875a788..3084b8569b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hr/strings.xml @@ -1,5 +1,1487 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <string name="search_verb">Pretraži</string> - <string name="la_mode_off">Isključeno</string> -</resources> \ No newline at end of file + <string name="search_verb">Pretraži</string> + <string name="la_mode_off">Isključeno</string> + <string name="abort_switch_receiving_address_confirm">Prekini</string> + <string name="v6_1_forward_many_messages_descr">Prosledi do 20 poruka odjednom.</string> + <string name="notification_display_mode_hidden_desc">Sakriti kontakt i poruku</string> + <string name="n_file_errors">%1$d greska(e) datoteka:\n%2$s</string> + <string name="forward_files_in_progress_desc">%1$d datoteka se i dalje preuzima.</string> + <string name="forward_files_failed_to_receive_desc">%1$d datoteka neuspešno preuzeta.</string> + <string name="forward_files_missing_desc">%1$d odstranjene datoteke.</string> + <string name="forward_files_not_accepted_desc">%1$d datoteka nije preuzeta.</string> + <string name="callstate_ended">završen</string> + <string name="contact_wants_to_connect_via_call">%1$s želi da se poveže sa tobom putem</string> + <string name="accept">Prihvatiti</string> + <string name="group_invitation_expired">Pozivnica za grupu je istekla</string> + <string name="group_link">Grupni link</string> + <string name="user_hide">Sakriti</string> + <string name="wallpaper_preview_hello_alice">Dobar dan!</string> + <string name="recent_history_is_not_sent_to_new_members">Istorija nije poslana novim članovima.</string> + <string name="hide_verb">Sakriti</string> + <string name="send_disappearing_message_5_minutes">5 minuta</string> + <string name="simplex_link_mode_full">Ceo Link</string> + <string name="forward_multiple">Proslediti poruke…</string> + <string name="abort_switch_receiving_address_question">Prekini menjanje adrese?</string> + <string name="if_you_cant_meet_in_person">Ako se ne možeš sastati, pokaži QR kod u video pozivu ili podeli link.</string> + <string name="hide_profile">Sakriti profil</string> + <string name="a_plus_b">a + b</string> + <string name="callstatus_accepted">prihvati poziv</string> + <string name="permissions_required">Dodeliti dozvolu</string> + <string name="audio_device_wired_headphones">Slušalice</string> + <string name="settings_section_title_help">POMOĆ</string> + <string name="delete_group_for_self_cannot_undo_warning">Grupa će biti obrisana za Vas – ovo ne može da se poništi!</string> + <string name="color_primary">Akcenat</string> + <string name="v4_2_group_links">Grupni linkovi</string> + <string name="v5_5_new_interface_languages">Madjarski i Turski UI</string> + <string name="how_to_use_markdown">Kako koristiti markdown</string> + <string name="from_gallery_button">Iz Galerije</string> + <string name="group_reports_active_one">1 izveštaj</string> + <string name="learn_more_about_address">O SimpleX adresama</string> + <string name="above_then_preposition_continuation">iznad, onda:</string> + <string name="accept_connection_request__question">Prihvati zahtev za vezu?</string> + <string name="email_invite_body">Zdravo!\nPoveži se sa mnom pomoću SimpleX Razgovora: %s</string> + <string name="icon_descr_group_inactive">Grupa neaktivna</string> + <string name="group_is_decentralized">U potpunosti decentralizovan - vidljivo samo za članove grupe.</string> + <string name="group_full_name_field">Celo ime grupe:</string> + <string name="icon_descr_help">pomoć</string> + <string name="full_name__field">Celo Ime:</string> + <string name="icon_descr_hang_up">Prekini poziv</string> + <string name="accept_feature">Prihvatiti</string> + <string name="v4_4_french_interface">Francuski interfejs</string> + <string name="v4_3_improved_privacy_and_security_desc">Sakriti ekran aplikacije u nedavnim aplikacijama.</string> + <string name="custom_time_unit_hours">sati</string> + <string name="send_disappearing_message_30_seconds">30 sekundi</string> + <string name="hide_dev_options">Sakriti:</string> + <string name="hide_notification">Sakriti</string> + <string name="forward_alert_forward_messages_without_files">Prosledi poruke bez datoteka?</string> + <string name="how_to">Kako da</string> + <string name="accept_call_on_lock_screen">Prihvatiti</string> + <string name="alert_text_decryption_error_too_many_skipped">%1$d poruke preskočene.</string> + <string name="integrity_msg_skipped">%1$d preskočena poruka(e)</string> + <string name="chat_item_ttl_month">1 mesec</string> + <string name="chat_item_ttl_week">1 nedelja</string> + <string name="alert_message_group_invitation_expired">Pozivnica za grupu više nije validna. Pošiljalac je uklonio.</string> + <string name="alert_title_no_group">Grupa nije pronadjena!</string> + <string name="accept_conditions">Prihvati uslove</string> + <string name="connect_plan_group_already_exists">Grupa već postoji!</string> + <string name="call_service_notification_end_call">Prekini razgovor</string> + <string name="edit_history">Istorija</string> + <string name="permissions_grant">Dodeliti dozvolu(e) za uspostavljanje poziva</string> + <string name="how_it_works">Kako radi</string> + <string name="group_info_section_title_num_members">%1$s ČLANOVI</string> + <string name="group_display_name_field">Upiši ime grupe:</string> + <string name="how_to_use_your_servers">Kako koristiti svoje servere</string> + <string name="enter_one_ICE_server_per_line">ICE serveri (jedan po liniji)</string> + <string name="abort_switch_receiving_address">Prekini menjanje adrese</string> + <string name="found_desktop">Nadji Radnu Površinu</string> + <string name="notification_preview_mode_hidden">Skriveno</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s poruke nisu prosledjene</string> + <string name="chat_list_groups">Grupe</string> + <string name="how_to_use_simplex_chat">Kako se koristi</string> + <string name="about_simplex_chat">O SimpleX Razgovoru</string> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d neuspešno u dešifrovanje poruke.</string> + <string name="snd_group_event_group_profile_updated">grupni profil ažuriran</string> + <string name="group_member_status_group_deleted">grupa odstranjena</string> + <string name="info_row_group">Grupa</string> + <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Ako izabereš da odbiješ pošiljalac NEĆE biti obavešten.</string> + <string name="v4_6_hidden_chat_profiles">Skriveni razgovori</string> + <string name="delete_group_for_all_members_cannot_undo_warning">Grupa će biti obrisana za sve članove – ovo se ne može poništiti!</string> + <string name="host_verb">Domaćin</string> + <string name="onboarding_notifications_mode_subtitle">Kako utiče na bateriju</string> + <string name="how_it_helps_privacy">Kako pomaže privatnosti</string> + <string name="how_simplex_works">Kako SimpleX radi</string> + <string name="group_profile_is_stored_on_members_devices">Grupni profil je uskladnjen na uredjajima korisnika, ne na serverima.</string> + <string name="onboarding_network_about_operators">O operatorima</string> + <string name="hidden_profile_password">Skrivena šifra profila</string> + <string name="chat_item_ttl_day">1 dan</string> + <string name="enter_correct_passphrase">Upiši ispravnu pristupnu frazu.</string> + <string name="send_disappearing_message_1_minute">1 minut</string> + <string name="accept_contact_button">Prihvatiti</string> + <string name="wallpaper_preview_hello_bob">Dobro jutro!</string> + <string name="v4_6_reduced_battery_usage">Dodatno smanjena potrošnja baterije.</string> + <string name="about_simplex">O SimpleX</string> + <string name="display_name_accepted_invitation">prihvaćena pozivnica</string> + <string name="info_view_message_button">poruka</string> + <string name="delete_and_notify_contact">Odstraniti i obavestiti kontakte</string> + <string name="delete_contact_question">Odstraniti kontakt?</string> + <string name="migrate_from_device_delete_database_from_device">Odstraniti bazu podataka sa ovog uređaja</string> + <string name="button_delete_contact">Odstraniti kontakt</string> + <string name="icon_descr_server_status_error">Greška</string> + <string name="info_row_deleted_at">Odstranjeno u</string> + <string name="network_proxy_password">Šifra</string> + <string name="app_check_for_updates_update_available">Ažuriranje dostupno: %s</string> + <string name="create_address">Napravi adresu</string> + <string name="rcv_direct_event_contact_deleted">odstranjen kontakt</string> + <string name="button_add_welcome_message">Dodaj poruku dobrodošlice</string> + <string name="share_text_sent_at">Poslano u: %s</string> + <string name="update_network_settings_confirmation">Ažuriraj</string> + <string name="profile_password">Šifra profila</string> + <string name="v5_6_safer_groups_descr">Administratori mogu da blokiraju.</string> + <string name="servers_info_modal_error_title">Greška</string> + <string name="create_1_time_link">Napravi jednokratnu poveznicu</string> + <string name="paste_button">Nalepiti</string> + <string name="settings_section_title_settings">PODEŠAVANJE</string> + <string name="settings_section_title_profile_images">Profilne slike</string> + <string name="acknowledged">Razumeo</string> + <string name="deleted">Odstranjeno</string> + <string name="deleted_description">odstranjeno</string> + <string name="create_profile_button">Napraviti</string> + <string name="settings_section_title_messages">PORUKE I DATOTEKE</string> + <string name="compose_message_placeholder">Poruka</string> + <string name="conn_stats_section_title_servers">SERVERI</string> + <string name="delete_chat_profile">Odstraniti profil razgovora</string> + <string name="feature_roles_admins">administratori</string> + <string name="random_port">Nasumično</string> + <string name="server_error">greška</string> + <string name="ci_status_other_error">Greška: %1$s</string> + <string name="delete_messages__question">Odstraniti %d poruke?</string> + <string name="delete_verb">Odstraniti</string> + <string name="toolbar_settings">Podešavanje</string> + <string name="onboarding_network_operators_update">Ažuriraj</string> + <string name="delete_files_and_media_all">Odstraniti sve datoteke</string> + <string name="delete_chat_profile_question">Odstraniti profil razgovora?</string> + <string name="messages_section_title">Poruke</string> + <string name="group_member_role_admin">administrator</string> + <string name="share_text_deleted_at">Odstranjeno u: %s</string> + <string name="v5_7_shape_profile_images">Oblikuj profilne slike</string> + <string name="servers_info_subscriptions_connections_subscribed">Aktivne veze</string> + <string name="smp_servers_add_to_another_device">Dodaj na drugi uredjaj</string> + <string name="error">Greška</string> + <string name="error_alert_title">Greška</string> + <string name="address_section_title">Adresa</string> + <string name="acknowledgement_errors">Greške pri potvrdi</string> + <string name="list_menu">Lista</string> + <string name="list_name_field_placeholder">Ime liste...</string> + <string name="delete_chat_list_menu_action">Odstraniti</string> + <string name="delete_after">Odstraniti nakon</string> + <string name="add_address_to_your_profile">Dodaj adresu na svoj profil da bi tvoji kontakti mogli da dele sa ostalima. Ažuriranje profila biće poslano tvojim kontaktima.</string> + <string name="add_contact_tab">Dodaj kontakt</string> + <string name="chat_list_add_list">Dodaj listu</string> + <string name="password_to_show">Prikazati šifru</string> + <string name="delete_contact_menu_action">Odstraniti</string> + <string name="abort_switch_receiving_address_desc">Promena adrese će biti prekinuta. Biće upotrebljena stara adresa.</string> + <string name="sent_directly">Poslano direktno</string> + <string name="chat_list_all">Sve</string> + <string name="info_row_sent_at">Poslano u</string> + <string name="icon_descr_sent_msg_status_sent">poslano</string> + <string name="users_delete_profile_for">Odstraniti profil razgovora za</string> + <string name="accept_contact_incognito_button">Prihvati anonimno</string> + <string name="share_verb">Podeliti</string> + <string name="add_to_list">Dodaj na listu</string> + <string name="network_enable_socks_info">Pristup na servere pomoću SOCKS proxy na portu %d? Proxy mora da bude uključen pre omogućavanja ove opcije.</string> + <string name="operator_conditions_accepted">Prihvaćeni uslovi</string> + <string name="smp_servers_add">Dodaj server</string> + <string name="smp_servers_preset_add">Dodaj unapred postavljene servere</string> + <string name="delete_address__question">Odstraniti adresu?</string> + <string name="rcv_group_event_group_deleted">odstranjena grupa</string> + <string name="users_add">Dodaj nalog</string> + <string name="users_delete_question">Odstraniti profil razgovora?</string> + <string name="create_address_button">Napraviti</string> + <string name="delete_chat_question">Odstraniti razgovor?</string> + <string name="delete_address">Odstranjena adresa</string> + <string name="delete_database">Odstranjena baza podataka</string> + <string name="migrate_from_device_archive_and_upload">Arhiviraj i otpremi</string> + <string name="button_delete_chat">Odstraniti razgovor</string> + <string name="display_name__field">Ime profila:</string> + <string name="v4_3_improved_server_configuration_desc">Dodaj server skeniranjem QR koda.</string> + <string name="update_database">Ažuriraj</string> + <string name="color_primary_variant">Dodatni akcent</string> + <string name="delete_group_menu_action">Odstraniti</string> + <string name="settings_section_title_user_theme">Tema profila</string> + <string name="operator_added_message_servers">Dodat serveri za poruke</string> + <string name="color_primary_variant2">Dodatni akcent 2</string> + <string name="icon_descr_settings">Podešavanje</string> + <string name="address_settings">Podešavanje adrese</string> + <string name="add_your_team_members_to_conversations">Dodaj tvoje članove tima u konverzaciju.</string> + <string name="button_add_friends">Dodaj prijatelja</string> + <string name="button_add_team_members">Dodaj člana tima</string> + <string name="archive_verb">Arhiva</string> + <string name="info_view_connect_button">poveži</string> + <string name="use_camera_button">Kamera</string> + <string name="icon_descr_more_button">Više</string> + <string name="auth_unlock">Otključaj</string> + <string name="network_smp_proxy_mode_unprotected">Nezaštićeno</string> + <string name="network_smp_proxy_mode_unknown">Nepoznati serveri</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="call_on_lock_screen">Pozivi na zaključanom ekranu:</string> + <string name="ttl_days">%d dana</string> + <string name="multicast_connect_automatically">Poveži automatski</string> + <string name="servers_info_sessions_connected">Povezano</string> + <string name="servers_info_sessions_errors">Greške</string> + <string name="servers_info_downloaded">Preuzeto</string> + <string name="callstate_connecting">povezivanje…</string> + <string name="permissions_camera">Kamera</string> + <string name="ttl_month">%d mesec</string> + <string name="file_not_found">Datoteka nije pronađena</string> + <string name="blocked_by_admin_items_description">%d poruke blokirane administratorom</string> + <string name="group_reports_active">%d izvještaji</string> + <string name="info_view_call_button">poziv</string> + <string name="camera_not_available">Kamera nije dostupna</string> + <string name="callstatus_calling">pozivanje…</string> + <string name="ttl_min">%d min</string> + <string name="ttl_sec">%d sec</string> + <string name="downloaded_files">Preuzeti datoteke</string> + <string name="blocked_by_admin_item_description">blokirano administratorom</string> + <string name="sender_you_pronoun">Vi</string> + <string name="group_member_status_unknown">nepoznat status</string> + <string name="connect_via_link_incognito">Poveži se anonimno</string> + <string name="callstatus_error">greška u pozivu</string> + <string name="connect_via_link_verb">Poveži</string> + <string name="unknown_message_format">nepoznat format poruke</string> + <string name="notification_contact_connected">Povezano</string> + <string name="ttl_s">%ds</string> + <string name="ttl_hours">%d sata(i)</string> + <string name="servers_info_sessions_connecting">Povezivanje</string> + <string name="display_name_connecting">povezivanje…</string> + <string name="smp_server_test_connect">Poveži</string> + <string name="connect_button">Poveži</string> + <string name="rcv_group_event_member_connected">povezano</string> + <string name="server_connected">povezano</string> + <string name="forward_files_not_accepted_receive_files">Preuzimanje</string> + <string name="network_settings_title">Napredna podešavanja</string> + <string name="icon_descr_call_progress">Poziv u toku</string> + <string name="settings_section_title_calls">POZIVI</string> + <string name="v5_4_block_group_members">Blokiraj članove grupe</string> + <string name="file_not_approved_title">Nepoznati serveri!</string> + <string name="icon_descr_file">Datoteka</string> + <string name="choose_file">Datoteka</string> + <string name="unmute_chat">Uključiti zvuk</string> + <string name="permissions_camera_and_record_audio">Kamera i mikrofon</string> + <string name="group_info_member_you">vi: %1$s</string> + <string name="connected_to_mobile">Povezano sa telefonom</string> + <string name="blocked_item_description">blokirano</string> + <string name="blocked_items_description">%d poruka blokirano</string> + <string name="server_connecting">povezivanje</string> + <string name="connected_mobile">Povezan telefon</string> + <string name="settings_section_title_you">VI</string> + <string name="v6_0_privacy_blur">Zamućeno za bolju privatnost.</string> + <string name="ttl_months">%d meseca(i)</string> + <string name="icon_descr_call_ended">Poziv završen</string> + <string name="cancel_verb">Poništiti</string> + <string name="connection_error_auth_desc">Osim ako vaš kontakt nije obrisao vezu ili je ova pozivnica već korišćena, moguće je da je u pitanju greška - molimo vas da je prijavite.\nDa biste se povezali, molimo vas da zamolite vaš kontakt da kreira novu pozivnicu i proverite da li imate stabilnu mrežnu konekciju.</string> + <string name="contact_connection_pending">povezivanje…</string> + <string name="user_unmute">Uključiti zvuk</string> + <string name="group_member_status_connected">povezano</string> + <string name="icon_descr_server_status_connected">Povezano</string> + <string name="download_file">Preuzimanje</string> + <string name="connect_via_member_address_alert_title">Povezati direktno?</string> + <string name="ttl_day">%d dan</string> + <string name="ttl_h">%dh</string> + <string name="ttl_m">%dm</string> + <string name="ttl_mth">%dmon</string> + <string name="group_connection_pending">povezivanje…</string> + <string name="icon_descr_received_msg_status_unread">nepročitano</string> + <string name="callstatus_ended">poziv završen %1$s</string> + <string name="callstatus_in_progress">poziv u toku</string> + <string name="rcv_group_event_member_created_contact">povezano direktno</string> + <string name="chat_preferences_on">uključeno</string> + <string name="ttl_hour">%d sat</string> + <string name="bold_text">podebljano</string> + <string name="member_info_member_blocked">blokirano</string> + <string name="wallpaper_advanced_settings">Napredna podešavanja</string> + <string name="rcv_group_event_member_blocked">blokirano %s</string> + <string name="block_for_all">Blokiraj za sve</string> + <string name="callstate_connected">povezano</string> + <string name="call_already_ended">Poziv već završen!</string> + <string name="unknown_error">Nepoznata greška</string> + <string name="group_member_status_connecting">povezivanje</string> + <string name="group_member_status_accepted">povezivanje (prihvaćeno)</string> + <string name="block_for_all_question">Blokiraj člana za sve?</string> + <string name="block_member_button">Blokiraj člana</string> + <string name="member_blocked_by_admin">Blokirano administratorom</string> + <string name="block_member_question">Blokiraj člana?</string> + <string name="appearance_bars_blur_radius">Zamućeno</string> + <string name="unlink_desktop">Otkači</string> + <string name="la_minutes">%d minut(a)</string> + <string name="app_check_for_updates">Proveri ažuriranje</string> + <string name="app_check_for_updates_stable">Stabilno</string> + <string name="settings_section_title_files">DATOTEKE</string> + <string name="migrate_from_device_bytes_uploaded">%s otpremljeno</string> + <string name="disable_notifications_button">Onemogućiti obavještenja</string> + <string name="is_not_verified">%s nije verifikovan</string> + <string name="clear_note_folder_warning">Sve poruke biće odstranjene - ovo ne može biti vraćeno!</string> + <string name="network_session_mode_user">Profil Chata</string> + <string name="app_check_for_updates_notice_disable">Onemogućiti</string> + <string name="ttl_week">%d nedelja</string> + <string name="v5_4_more_things_descr">- neobavezno obavestiti o obrisanim kontaktima.\n- imena profila sa razmacima.\n- i još mnogo toga!\"</string> + <string name="migrate_to_device_importing_archive">Uvoženje arhive</string> + <string name="servers_info_files_tab">Datoteke</string> + <string name="servers_info_uploaded">Otpremljeno</string> + <string name="is_verified">%s je verifikovan</string> + <string name="chat_console">Konzola chata</string> + <string name="scan_QR_code">Skenirati QR kod</string> + <string name="network_session_mode_server">Server</string> + <string name="no_call_on_lock_screen">Onemogućiti</string> + <string name="settings_section_title_chat_database">BAZA PODATAKA CHATA</string> + <string name="send_receipts_disabled">onemogućeno</string> + <string name="import_theme_error">Greška pri uvoženju teme</string> + <string name="files_are_prohibited_in_group">Datoteke i medijski sadržaji su zabranjeni.</string> + <string name="or_paste_archive_link">Ili nalepiti link arhive</string> + <string name="receipts_groups_disable_for_all">Onemogućeno za sve grupe</string> + <string name="info_row_disappears_at">Nestaje u</string> + <string name="files_and_media">Datoteke i medijski sadržaji</string> + <string name="feature_roles_all_members">svi članovi</string> + <string name="v4_4_disappearing_messages">Poruke koje nestaju</string> + <string name="member_info_member_disabled">onemogućeno</string> + <string name="allow_calls_question">Dozvoliti pozive?</string> + <string name="la_seconds">%d sekunde(i)</string> + <string name="or_scan_qr_code">Ili skenirati QR kod</string> + <string name="app_check_for_updates_disabled">Onemogućeno</string> + <string name="settings_section_title_app">Aplikacija</string> + <string name="settings_section_title_chats">RAZGOVORI</string> + <string name="files_and_media_prohibited">Datoteke i medijski sadržaji su zabranjeni!</string> + <string name="disappearing_prohibited_in_this_chat">Poruke koje nestaju su zabranjene u ovom razgovoru.</string> + <string name="chat_is_stopped_indication">Chat je zaustavljen</string> + <string name="file_saved">Datoteka sačuvana</string> + <string name="app_version_title">Verzija aplikacije</string> + <string name="app_version_name">Verzija aplikacije: v%s</string> + <string name="settings_section_title_chat_theme">Tema razgovora</string> + <string name="chat_item_ttl_seconds">%s sekunda(i)</string> + <string name="chat_database_exported_title">Baza podataka Chata izvezena</string> + <string name="operator">Operator</string> + <string name="operator_server_alert_title">Operacioni server</string> + <string name="timed_messages">Poruke koje nestaju</string> + <string name="disappearing_messages_are_prohibited">Poruke koje nestaju su zabranjene.</string> + <string name="server_address">Adresa servera</string> + <string name="files_and_media_not_allowed">Datoteke i medijski sadržaji nisu dozvoljeni</string> + <string name="allow_verb">Dozvoliti</string> + <string name="chat_with_developers">Razgovor sa kreatorima</string> + <string name="your_chats">Razgovori</string> + <string name="ttl_weeks">%d nedelja(e)</string> + <string name="desktop_device">Računar</string> + <string name="direct_messages_are_prohibited">Direktne poruke između članova su zabranjene.</string> + <string name="direct_messages_are_prohibited_in_group">Direktne poruke između članova su zabranjene u ovoj grupi.</string> + <string name="operator_servers_title">%s serveri</string> + <string name="turn_off_battery_optimization_button">Dozvoliti</string> + <string name="direct_messages">Direktne poruke</string> + <string name="receipts_contacts_disable_for_all">Onemogućeno za sve</string> + <string name="migrate_to_device_chat_migrated">Chat migriran!</string> + <string name="group_members_n">%s, %s i %d članovi</string> + <string name="smp_server_test_disconnect">Prekinuti vezu</string> + <string name="connect_plan_chat_already_exists">Chat već postoji!</string> + <string name="chat_database_deleted">Baza podataka Chata odstranjena</string> + <string name="app_check_for_updates_notice_title">Proveri ažuriranje</string> + <string name="chat_is_stopped_you_should_transfer_database">Chat je zaustavljen. Ako već koristiš ovu bazu podataka na drugom uređaju, trebala bi se vratiti pre nego što se pokrene chat.</string> + <string name="onboarding_notifications_mode_periodic_desc_short">Proveri poruke svakih 10 minuta</string> + <string name="rcv_group_event_1_member_connected">%s povezan</string> + <string name="rcv_group_event_3_members_connected">%s, %s i %s povezani</string> + <string name="conn_level_desc_direct">direktno</string> + <string name="info_row_chat">Chat</string> + <string name="image_descr_qr_code">QR kod</string> + <string name="chat_is_running">Chat je pokrenut</string> + <string name="import_database">Uvesti bazu podataka</string> + <string name="chat_database_section">BAZA PODATAKA CHATA</string> + <string name="chat_is_stopped">Chat je zaustavljen</string> + <string name="rcv_group_event_n_members_connected">%s, %s i %d ostali članovi povezani</string> + <string name="migrate_to_device_import_failed">Uvoz neuspešan</string> + <string name="delete_chat_for_self_cannot_undo_warning">Razgovor će biti odstranjen za tebe - ovo se ne može vratiti!</string> + <string name="import_theme">Uvesti temu</string> + <string name="chat_database_imported">Baza podataka Chata uvezena</string> + <string name="current_version_timestamp">%s (sadašnji)</string> + <string name="share_text_disappears_at">Nestaje u: %s</string> + <string name="import_database_confirmation">Uvesti</string> + <string name="settings_section_title_chat_colors">Boje Chata</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="chat_archive">Ili uvesti arhiviranu datoteku</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">Razgovor će biti odstranjen za sve članove - ovo se ne može vratiti!</string> + <string name="direct_messages_are_prohibited_in_chat">Direktne poruke između članova su zabranjene u ovim porukama.</string> + <string name="disconnect_remote_host">Prekinuti vezu</string> + <string name="migrate_to_device_bytes_downloaded">%s preuzeto</string> + <string name="you_are_already_connected_to_vName_via_this_link">Već ste povezani na %1$s.</string> + <string name="voice_messages_not_allowed">Glasovne poruke nisu dozvoljenje</string> + <string name="connection_error">Greška u vezi</string> + <string name="smp_servers_test_server">Test server</string> + <string name="audio_call_no_encryption">audio poziv (nije e2e šifrovan)</string> + <string name="new_chat">Novi razgovor</string> + <string name="save_and_notify_contact">Sačuvati i obavestiti kontakt</string> + <string name="save_and_notify_group_members">Sačuvati i obavestiti članove grupe</string> + <string name="secret_text">tajna</string> + <string name="video_call_no_encryption">video poziv (nije e2e šifrovan)</string> + <string name="icon_descr_video_on">Video uključen</string> + <string name="icon_descr_audio_off">Audio isključeno</string> + <string name="icon_descr_audio_on">Audio uključeno</string> + <string name="theme_black">Crna</string> + <string name="chat_preferences_off">isključeno</string>` + <string name="v4_5_italian_interface_descr">Zahvaljujući korisnicima – doprinesi pomoću Weblate!</string> + <string name="migrate_from_device_start_chat">Započeti razgovor</string> + <string name="italic_text">italic</string> + <string name="edit_verb">Urediti</string> + <string name="save_verb">Sačuvati</string> + <string name="video_descr">Video</string> + <string name="block_member_confirmation">Blok</string> + <string name="custom_time_unit_seconds">sekunde</string> + <string name="smp_servers_test_servers">Test serveri</string> + <string name="your_settings">Vaša podešavanja</string> + <string name="permissions_record_audio">Mikrofon</string> + <string name="settings_message_shape_corner">Ugao</string> + <string name="fix_connection_confirm">Popraviti</string> + <string name="secured">Obezbeđeno</string> + <string name="connections">Veze</string> + <string name="notification_preview_new_message">nova poruka</string> + <string name="icon_descr_email">Email</string> + <string name="network_enable_socks">Koristi SOCKS proxy?</string> + <string name="connection_error_blocked">Veza blokirana</string> + <string name="notifications">Obaveštenja</string> + <string name="gallery_video_button">Video</string> + <string name="you_accepted_connection">Prihvatili ste vezu</string> + <string name="clear_verb">Očistiti</string> + <string name="simplex_address">SimpleX adresa</string> + <string name="image_descr_simplex_logo">SimpleX Logo</string> + <string name="show_dev_options">Prikazati:</string> + <string name="settings_section_title_device">UREĐAJ</string> + <string name="new_message">Nova poruka</string> + <string name="color_secondary">Sekundarni</string> + <string name="receipts_section_contacts">Kontakti</string> + <string name="privacy_media_blur_radius_off">Isključeno</string> + <string name="v5_0_polish_interface_descr">Zahvaljujući korisnicima – doprinesi pomoću Weblate!</string> + <string name="enable_receipts_all">Omogućiti</string> + <string name="your_servers">Vaši serveri</string> + <string name="network_options_save">Sačuvati</string> + <string name="network_option_seconds_label">sek</string> + <string name="v4_3_voice_messages">Glasovne poruke</string> + <string name="ok">OK</string> + <string name="call_service_notification_audio_call">Audio poziv</string> + <string name="call_service_notification_video_call">Video poziv</string> + <string name="smp_servers_use_server">Koristi server</string> + <string name="smp_servers_your_server_address">Adresa vašeg servera</string> + <string name="your_SMP_servers">Vaši SMP serveri</string> + <string name="your_XFTP_servers">Vaši XFTP serveri</string> + <string name="onboarding_notifications_mode_battery">Obaveštenja i baterija</string> + <string name="show_call_on_lock_screen">Prikazati</string> + <string name="status_e2e_encrypted">e2e šifrovano</string> + <string name="open_verb">Otvoriti</string> + <string name="enable_automatic_deletion_question">Omogući automatsko brisanje poruka?</string> + <string name="snd_conn_event_ratchet_sync_ok">šifrovanje ok za %s</string> + <string name="user_mute">Isključiti zvuk</string> + <string name="theme_simplex">SimpleX</string> + <string name="theme">Teme</string> + <string name="voice_messages">Glasovne poruke</string> + <string name="calls_prohibited_with_this_contact">Audio/video pozivi su zabranjeni.</string> + <string name="connect_plan_open_group">Otvoriti grupu</string> + <string name="all_users">Svi profili</string> + <string name="add_contact_or_create_group">Započeti novi razgovor</string> + <string name="clear_chat_menu_action">Očistiti</string> + <string name="clear_chat_button">Očistiti razgovor</string> + <string name="smp_servers_your_server">Tvoj server</string> + <string name="snd_conn_event_ratchet_sync_agreed">šifrovanje prihvaćeno za %s</string> + <string name="voice_messages_are_prohibited">Glasovne poruke su zabranjene.</string> + <string name="connection_local_display_name">veza %1$d</string> + <string name="icon_descr_video_off">Video isključen</string> + <string name="message_queue_info_none">ništa</string> + <string name="info_row_connection">Veza</string> + <string name="chat_list_favorites">Omiljeno</string> + <string name="chat_list_contacts">Kontakti</string> + <string name="favorite_chat">Omiljen</string> + <string name="network_smp_proxy_mode_never">Nikada</string> + <string name="network_session_mode_entity">Veza</string> + <string name="settings_section_title_themes">TEME</string> + <string name="audio_video_calls">Audio/video pozivi</string> + <string name="chat_preferences_no">ne</string> + <string name="conn_event_ratchet_sync_ok">šifrovanje ok</string> + <string name="conn_event_ratchet_sync_agreed">šifrovanje prihvaćeno</string> + <string name="whats_new_thanks_to_users_contribute_weblate">Zahvaljujući korisnicima – doprinesi pomoću Weblate!</string> + <string name="back">Nazad</string> + <string name="your_simplex_contact_address">Vaša SimpleX adresa</string> + <string name="save_and_notify_contacts">Sačuvati i obavestiti kontakte</string> + <string name="v4_4_french_interface_descr">Zahvaljujući korisnicima – doprinesi pomoću Weblate!</string> + <string name="icon_descr_edited">uređeno</string> + <string name="privacy_chat_list_open_links_no">Ne</string> + <string name="network_smp_proxy_fallback_prohibit">Ne</string> + <string name="keep_conversation">Zadržite konverzaciju</string> + <string name="icon_descr_address">SimpleX Adresa</string> + <string name="save_servers_button">Sačuvati</string> + <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> + <string name="settings_section_title_experimenta">EKSPERIMENTALNO</string> + <string name="chat_item_ttl_none">nikada</string> + <string name="clear_contacts_selection_button">Očistiti</string> + <string name="v4_6_chinese_spanish_interface_descr">Zahvaljujući korisnicima – doprinesi pomoću Weblate!</string> + <string name="encrypted_audio_call">e2e šifrovan zvučni poziv</string> + <string name="icon_descr_video_call">video poziv</string> + <string name="use_servers_of_operator_x">Koristi %s</string> + <string name="operator_use_operator_toggle_description">Koristi servere</string> + <string name="contact_list_header_title">Vaši kontakti</string> + <string name="network_use_onion_hosts_no">Ne</string> + <string name="start_chat_question">Započeti razgovor?</string> + <string name="info_view_open_button">otvoriti</string> + <string name="onboarding_network_operators_continue">Nastaviti</string> + <string name="rcv_group_event_open_chat">Otvoriti</string> + <string name="color_background">Pozadina</string> + <string name="icon_descr_audio_call">audio poziv</string> + <string name="settings_audio_video_calls">Audio & video pozivi</string> + <string name="chat_database_exported_continue">Nastaviti</string> + <string name="v4_6_audio_video_calls">Audio i video pozivi</string> + <string name="edit_chat_list_menu_action">Urediti</string> + <string name="feature_off">isključeno</string> + <string name="voice_prohibited_in_this_chat">Glasovne poruke su zabranjene u ovom razgovoru.</string> + <string name="mute_chat">Isključiti zvuk</string> + <string name="keep_invitation_link">Zadržati</string> + <string name="sync_connection_confirm">Popraviti</string> + <string name="clear_chat_question">Očistiti razgovor?</string> + <string name="clear_note_folder_question">Očistiti privatne beleške?</string> + <string name="continue_to_next_step">Nastaviti</string> + <string name="report_item_archived">arhivirani izveštaj</string> + <string name="app_check_for_updates_installed_successfully_title">Uspešno instalirano</string> + <string name="scan_from_mobile">Skenirati telefonom</string> + <string name="network_option_ping_interval">PING interval</string> + <string name="servers_info_subscriptions_connections_pending">Na čekanju</string> + <string name="servers_info_detailed_statistics_sent_messages_header">Poslane poruke</string> + <string name="security_code">Bezbednostni kod</string> + <string name="chat_with_the_founder">Poslati pitanja i ideje</string> + <string name="migrate_to_device_repeat_import">Ponoviti uvoz</string> + <string name="service_notifications">Trenutna obaveštenja!</string> + <string name="icon_descr_send_message">Poslati Poruku</string> + <string name="onboarding_notifications_mode_service">Trenutno</string> + <string name="remove_member_button">Ukloniti člana</string> + <string name="send_verb">Poslati</string> + <string name="saved_chat_item_info_tab">Sačuvano</string> + <string name="gallery_image_button">Slika</string> + <string name="one_time_link_short">jednokratna veza</string> + <string name="network_proxy_random_credentials">Koristi nasumične pristupne podatke</string> + <string name="port_verb">Port</string> + <string name="network_proxy_username">Korisničko ime</string> + <string name="network_proxy_port">port %d</string> + <string name="save_settings_question">Sačuvati podešavanja?</string> + <string name="onboarding_notifications_mode_title">Privatna obaveštenja</string> + <string name="incoming_audio_call">Dolazni audio poziv</string> + <string name="status_no_e2e_encryption">bez e2e šifrovanja</string> + <string name="icon_descr_flip_camera">Okrenuti kameru</string> + <string name="rcv_group_event_member_unblocked">odblokiran %s</string> + <string name="profile_update_event_removed_address">uklonjena kontakt adresa</string> + <string name="snd_conn_event_ratchet_sync_started">prihvatiti šifrovanje za %s…</string> + <string name="group_member_status_removed">uklonjeno</string> + <string name="group_member_status_left">levo</string> + <string name="button_leave_group">Napustiti grupu</string> + <string name="role_in_group">Uloga</string> + <string name="color_mode_light">Svetlo</string> + <string name="ttl_w">%dn</string> + <string name="invalid_file_link">Nevažeća veza</string> + <string name="servers_info_statistics_section_header">Statistika</string> + <string name="reconnect">Ponovo povezati</string> + <string name="size">Veličina</string> + <string name="delete_image">Odstraniti sliku</string> + <string name="callstatus_missed">propušten poziv</string> + <string name="icon_descr_call_rejected">Odbijeni poziv</string> + <string name="icon_descr_call_pending_sent">Poziv na čekanju</string> + <string name="sent_message">Poslana poruka</string> + <string name="v5_4_incognito_groups">Anonimne grupe</string> + <string name="custom_time_picker_select">Izabrati</string> + <string name="invite_friends_short">Pozvati</string> + <string name="report_item_archived_by">arhivirani izveštaj od %s</string> + <string name="invalid_chat">nevažeći razgovor</string> + <string name="simplex_link_mode_description">Opis</string> + <string name="icon_descr_instant_notifications">Trenutna obaveštenja</string> + <string name="auth_stop_chat">Zaustaviti razgovor</string> + <string name="disappearing_message">Poruka koja nestaje</string> + <string name="no_details">bez detalja</string> + <string name="scan_code">Skenirati kod</string> + <string name="join_group_button">Pristupiti</string> + <string name="join_group_question">Pristupiti grupi?</string> + <string name="profile_update_event_member_name_changed">član %1$s promenjen u %2$s</string> + <string name="button_welcome_message">Poruka dobrodošlice</string> + <string name="theme_remove_image">Ukloniti sliku</string> + <string name="v5_5_private_notes">Privatne beleške</string> + <string name="error_parsing_uri_title">Nevažeća veza</string> + <string name="personal_welcome">Dobrodošli %1$s!</string> + <string name="periodic_notifications">Periodična obaveštenja</string> + <string name="auth_open_chat_console">Otvoriti konzolu razgovora</string> + <string name="info_view_search_button">pretraži</string> + <string name="info_view_video_button">video</string> + <string name="call_connection_peer_to_peer">peer-to-peer</string> + <string name="incognito_info_protects">Anonimni režim štiti Vašu privatnost koristeći novi nasumični profil za svaki kontakt.</string> + <string name="custom_time_unit_weeks">nedelje</string> + <string name="agent_internal_error_title">Interna greška</string> + <string name="saved_from_description">Sačuvano od %s</string> + <string name="saved_description">sačuvano</string> + <string name="group_member_status_invited">pozvan</string> + <string name="saved_message_title">Sačuvana poruka</string> + <string name="reset_color">Obnoviti boje</string> + <string name="link_a_mobile">Povezati telefon</string> + <string name="report_archive_alert_title">Arhiviraj izveštaj?</string> + <string name="delivery">Prijem</string> + <string name="report_verb">Izveštaj</string> + <string name="in_reply_to">U odgovoru na</string> + <string name="chat_list_group_reports">Izveštaji</string> + <string name="chat_list_notes">Beleške</string> + <string name="send_live_message">Poslati živu poruku</string> + <string name="delete_pending_connection__question">Odstraniti veze na čekanju?</string> + <string name="scan_paste_link">Skenirati / Nalepiti link</string> + <string name="incorrect_code">Neispravan bezbednostni kod!</string> + <string name="callstatus_rejected">odbijeni poziv</string> + <string name="action_button_add_members">Pozvati</string> + <string name="member_info_member_inactive">neaktivan</string> + <string name="network_options_save_and_reconnect">Sačuvati i ponovo povezati</string> + <string name="thousand_abbreviation">k</string> + <string name="connect_via_group_link">Pristupiti grupi?</string> + <string name="invalid_message_format">nevažeći format poruke</string> + <string name="la_notice_turn_on">Uključiti</string> + <string name="delete_message__question">Odstraniti poruku?</string> + <string name="profile_update_event_removed_picture">uklonjena profilna slika</string> + <string name="image_descr_profile_image">profilna slika</string> + <string name="color_mode_dark">Tamno</string> + <string name="linked_mobiles">Povezani telefoni</string> + <string name="subscribed">Pretplaćen</string> + <string name="privacy_chat_list_open_links_ask">Pitati</string> + <string name="invite_to_group_button">Pozvati u grupu</string> + <string name="color_mode_system">Sistem</string> + <string name="theme_system">Sistem</string> + <string name="theme_light">Svetlo</string> + <string name="reset_single_color">Obnoviti boju</string> + <string name="chat_theme_apply_to_light_mode">Svetlosni režim</string> + <string name="chat_theme_apply_to_dark_mode">Tamni režim</string> + <string name="v5_5_join_group_conversation">Pristupiti grupnoj konverzaciji</string> + <string name="desktop_address">Adresa računara</string> + <string name="connect_plan_join_your_group">Pristupiti Vašoj grupi?</string> + <string name="service_notifications_disabled">Trenutna obaveštenja su onemogućena!</string> + <string name="stop_file__action">Zaustaviti datoteku</string> + <string name="group_preview_join_as">pristupiti kao %s</string> + <string name="info_menu">Informacije</string> + <string name="reply_verb">Odgovoriti</string> + <string name="member_inactive_title">Član neaktivan</string> + <string name="network_socks_proxy_settings">SOCKS proxy podešavanje</string> + <string name="network_socks_proxy">SOCKS proxy</string> + <string name="icon_descr_call_missed">Propušten poziv</string> + <string name="delete_messages_after">Odstraniti poruke nakon</string> + <string name="delete_messages">Odstraniti poruke</string> + <string name="rcv_group_event_member_left">levo</string> + <string name="rcv_conn_event_verification_code_reset">bezbednostni kod promenjen</string> + <string name="info_row_local_name">Lokalno ime</string> + <string name="item_info_current">(trenutno)</string> + <string name="view_conditions">Pogledati uslove</string> + <string name="language_system">Sistem</string> + <string name="theme_dark">Tamno</string> + <string name="dark_theme">Tamna tema</string> + <string name="v6_0_increase_font_size">Povećati veličinu slova.</string> + <string name="servers_info_details">Detalji</string> + <string name="servers_info_reset_stats_alert_confirm">Obnoviti</string> + <string name="servers_info_reset_stats">Obnoviti statistiku</string> + <string name="la_mode_system">Sistem</string> + <string name="compose_send_direct_message_to_connect">Poslati direktnu poruku za povezivanje</string> + <string name="image_descr">Slika</string> + <string name="icon_descr_server_status_pending">Na čekanju</string> + <string name="smp_servers">SMP serveri</string> + <string name="display_name">Upišite Vaše ime:</string> + <string name="open_chat">Otvoriti razgovor</string> + <string name="rcv_group_event_member_added">pozvan %1$s</string> + <string name="conn_event_ratchet_sync_started">prihvatiti šifrovanje…</string> + <string name="sending_via">Slanje pomoću</string> + <string name="smp_server_test_delete_queue">Odstraniti redosled</string> + <string name="note_folder_local_display_name">Privatne beleške</string> + <string name="invalid_data">nevažeći podaci</string> + <string name="stop_chat_confirmation">Zaustaviti</string> + <string name="fix_connection">Popraviti vezu</string> + <string name="stop_file__confirm">Zaustaviti</string> + <string name="member_contact_send_direct_message">poslati direktnu poruku</string> + <string name="attach">Priložiti</string> + <string name="save_list">Sačuvati listu</string> + <string name="self_destruct">Samouništenje</string> + <string name="bad_desktop_address">Pogrešna adresa računara</string> + <string name="agent_critical_error_title">Kritična greška</string> + <string name="rcv_group_event_member_deleted">uklonjeno %1$s</string> + <string name="group_member_role_member">član</string> + <string name="initial_member_role">Početna uloga</string> + <string name="conn_level_desc_indirect">posredan (%1$s)</string> + <string name="saved_from_chat_item_info_title">Sačuvano od</string> + <string name="periodic_notifications_disabled">Periodična obaveštenja su onemogućena!</string> + <string name="group_welcome_title">Poruka dobrodošlice</string> + <string name="open_with_app">Otvoriti pomoću %s</string> + <string name="migrate_from_device_remove_archive_question">Ukloniti arhivu?</string> + <string name="selected_chat_items_selected_n">Izabrano %d</string> + <string name="welcome">Dobrodošli!</string> + <string name="videos_limit_title">Previše video snimaka!</string> + <string name="send_disappearing_message_send">Poslati</string> + <string name="reset_verb">Obnoviti</string> + <string name="invalid_contact_link">Nevažeća veza!</string> + <string name="select_chat_profile">Izabrati profil razgovora</string> + <string name="smp_servers_scan_qr">Skenirati QR kod servera</string> + <string name="network_settings">Napredna mrežna podešavanja</string> + <string name="settings_section_title_socks">SOCKS PROXY</string> + <string name="settings_section_title_incognito">Anonimni režim</string> + <string name="network_option_ping_count">broj PING</string> + <string name="servers_info_reset_stats_alert_title">Obnoviti statistiku?</string> + <string name="icon_descr_add_members">Pozvati članove</string> + <string name="network_option_tcp_connection">TCP veza</string> + <string name="servers_info">Informacije o serveru</string> + <string name="fix_connection_question">Popraviti vezu?</string> + <string name="deleted_chats">Arhivirani kontakti</string> + <string name="archive_report">Arhivirati izveštaj</string> + <string name="scan_qr_code_from_desktop">Skenirati QR kod sa radne površine</string> + <string name="leave_chat_question">Napustiti razgovor?</string> + <string name="incoming_video_call">Dolazni video poziv</string> + <string name="button_add_members">Pozvati članove</string> + <string name="dark_mode_colors">Tamni režim boja</string> + <string name="ttl_d">%dd</string> + <string name="linked_desktops">Povezane radne površine</string> + <string name="whats_new">Šta je novo</string> + <string name="remove_passphrase">Ukloniti</string> + <string name="attempts_label">pokušaji</string> + <string name="incognito">Anonimno</string> + <string name="delete_profile">Odstraniti profil</string> + <string name="icon_descr_video_snd_complete">Video poslan</string> + <string name="smp_servers_save">Sačuvati servere</string> + <string name="smp_save_servers_question">Sačuvati servere?</string> + <string name="button_remove_member">Ukloniti člana</string> + <string name="item_info_no_text">bez teksta</string> + <string name="welcome_message_is_too_long">Poruka dobrodošlice je preduga</string> + <string name="migrate_from_device_archiving_database">Arhiviraj bazu podataka</string> + <string name="onboarding_notifications_mode_periodic">Periodično</string> + <string name="remove_member_confirmation">Ukloniti</string> + <string name="member_info_section_title_member">ČLAN</string> + <string name="joining_group">Pristupanje grupi</string> + <string name="smp_server">SMP server</string> + <string name="invite_to_chat_button">Pozvati u razgovor</string> + <string name="unblock_member_confirmation">Odblokirati</string> + <string name="v4_5_italian_interface">Italijanski interfejs</string> + <string name="file_with_path">Datoteka: %s</string> + <string name="join_group_incognito_button">Pristupiti anonimno</string> + <string name="leave_group_question">Napustiti grupu?</string> + <string name="devices">Uređaji</string> + <string name="select_verb">Izabrati</string> + <string name="send_receipts">Poslati potvrde</string> + <string name="sync_connection_question">Popraviti vezu?</string> + <string name="leave_group_button">Napustiti</string> + <string name="button_leave_chat">Napustiti razgovor</string> + <string name="select_contacts">Izabrati kontakte</string> + <string name="button_send_direct_message">Poslati direktnu poruku</string> + <string name="button_remove_member_question">Ukloniti člana?</string> + <string name="wallpaper_scale_repeat">Ponoviti</string> + <string name="appearance_font_size">Veličina teksta</string> + <string name="v5_0_large_files_support">Videa i datoteke do 1gb</string> + <string name="desktop_incompatible_version">Nekompatibilna verzija</string> + <string name="migrate_to_device_repeat_download">Ponoviti preuzimanje</string> + <string name="stop_chat_question">Zaustaviti razgovor?</string> + <string name="delete_link">Odstraniti vezu</string> + <string name="share_text_created_at">Kreirano u: %s</string> + <string name="feature_roles_owners">vlasnici</string> + <string name="migrate_to_device_finalize_migration">Dovršiti migraciju na drugom uređaju.</string> + <string name="network_type_other">Drugo</string> + <string name="created">Kreirano</string> + <string name="other_errors">Ostale greške</string> + <string name="retry_verb">Ponoviti</string> + <string name="encrypt_database_question">Šifrovati bazu podataka?</string> + <string name="group_members_2">%s i %s</string> + <string name="group_member_role_owner">vlasnik</string> + <string name="copy_verb">Kopirati</string> + <string name="submit_passcode">Predati</string> + <string name="xftp_servers_other">Ostali XFTP serveri</string> + <string name="create_simplex_address">Napraviti SimpleX adresu</string> + <string name="encrypt_database">Šifrovati</string> + <string name="info_row_created_at">Kreirano u</string> + <string name="operator_open_conditions">Otvoriti uslove</string> + <string name="v6_0_connection_servers_status">Kontrolisanje vaše mreže</string> + <string name="copied">Kopirano</string> + <string name="message_too_large">Poruka je prevelika</string> + <string name="smp_server_test_upload_file">Otpremiti datoteku</string> + <string name="smp_server_test_create_file">Napraviti datoteku</string> + <string name="create_group">Napraviti tajnu grupu</string> + <string name="group_member_status_creator">kreator</string> + <string name="reveal_verb">Otkriti</string> + <string name="create_list">Napraviti listu</string> + <string name="copy_error">Greška pri kopiranju</string> + <string name="rcv_group_event_2_members_connected">%s i %s su povezani</string> + <string name="create_group_button">Napraviti grupu</string> + <string name="create_secret_group_title">Napraviti tajnu grupu</string> + <string name="v5_1_message_reactions_descr">Napokon ih imamo! 🚀</string> + <string name="migrate_from_device_finalize_migration">Dovršiti migraciju</string> + <string name="selected_chat_items_nothing_selected">Ništa nije izabrano</string> + <string name="conversation_deleted">Konverzacija odstranjena!</string> + <string name="smp_servers_other">Ostali SMP serveri</string> + <string name="core_version">Verzija jezgra: v%s</string> + <string name="encrypted_database">Šifrovati bazu podataka</string> + <string name="delete_link_question">Odstraniti vezu?</string> + <string name="subscription_results_ignored">Pretplata ignorisana</string> + <string name="create_chat_profile">Napraviti profil razgovora</string> + <string name="create_another_profile_button">Napraviti profil</string> + <string name="subscription_errors">Greške u pretplati</string> + <string name="wallpaper_scale_fit">Pasent</string> + <string name="delete_chat_list_question">Odstraniti listu?</string> + <string name="operator_use_for_messages_receiving">Za primanje</string> + <string name="other_label">drugo</string> + <string name="create_profile">Napraviti profil</string> + <string name="people_can_connect_only_via_links_you_share">Vi odlučujete ko se može povezati.</string> + <string name="delete_group_question">Odstraniti grupu?</string> + <string name="restore_database_alert_confirm">Obnoviti</string> + <string name="switch_verb">Prebaciti</string> + <string name="app_check_for_updates_beta">Beta</string> + <string name="edit_image">Urediti sliku</string> + <string name="receipts_section_groups">Mala grupa (max 20)</string> + <string name="info_row_received_at">Primljeno u</string> + <string name="feature_cancelled_item">otkazano %s</string> + <string name="callstate_received_confirmation">primljena potvrda…</string> + <string name="v5_4_better_groups">Bolje grupe</string> + <string name="report_reason_other">Drugi razlog</string> + <string name="v6_0_your_contacts_descr">Arhivirati kontakte za kasniji razgovor.</string> + <string name="servers_info_detailed_statistics_received_messages_header">Primljene poruke</string> + <string name="v6_1_better_security">Bolja sigurnost ✅</string> + <string name="button_create_group_link">Napraviti vezu</string> + <string name="received_message">Primljena poruka</string> + <string name="contact_tap_to_connect">Dodirnuti za Povezivanje</string> + <string name="chat_help_tap_button">Dodirnuti dugme</string> + <string name="open_port_in_firewall_title">Otvoriti port u firewallu</string> + <string name="share_text_received_at">Primljeno u: %s</string> + <string name="simplex_link_mode_browser">Pomoću pretraživača</string> + <string name="share_address_publicly">Podeliti adresu javno</string> + <string name="app_check_for_updates_button_skip">Preskočiti ovu verziju</string> + <string name="app_check_for_updates_button_remind_later">Podseti kasnije</string> + <string name="icon_descr_call_connecting">Povezivanje poziva</string> + <string name="answer_call">Odgovoriti na poziv</string> + <string name="settings_section_title_network_connection">Mrežna veza</string> + <string name="v6_2_network_decentralization">Decentralizacija mreže</string> + <string name="v6_2_business_chats">Biznis razgovori</string> + <string name="completed">Završeno</string> + <string name="expand_verb">Proširiti</string> + <string name="show_QR_code">Prikazati QR kod</string> + <string name="smp_server_test_download_file">Preuzeti datoteku</string> + <string name="create_group_link">Napraviti grupni link</string> + <string name="chat_list_businesses">Biznisi</string> + <string name="v6_1_better_user_experience">Bolje korisničko iskustvo</string> + <string name="possible_slow_function_title">Spora funkcija</string> + <string name="button_edit_group_profile">Urediti profil grupe</string> + <string name="tap_to_activate_profile">Dodirnuti za aktivaciju profila.</string> + <string name="theme_destination_app_theme">Tema aplikacije</string> + <string name="group_invitation_tap_to_join">Dodirnuti za pristup</string> + <string name="group_invitation_tap_to_join_incognito">Dodirnuti za anonimni pristup</string> + <string name="share_address">Podeliti adresu</string> + <string name="settings_message_shape_tail">Rep</string> + <string name="v6_1_better_calls">Bolji pozivi</string> + <string name="business_address">Biznis adresa</string> + <string name="color_received_message">Primljena poruka</string> + <string name="group_member_status_complete">završiti</string> + <string name="icon_descr_expand_role">Proširiti selekciju uloga</string> + <string name="opening_database">Otvaranje baze podataka…</string> + <string name="refresh_qr_code">Obnoviti</string> + <string name="icon_descr_speaker_off">Zvučnik isključen</string> + <string name="colored_text">obojen</string> + <string name="share_link">Podeli vezu</string> + <string name="report_reason_spam">Spam</string> + <string name="audio_device_speaker">Zvučnik</string> + <string name="blocking_reason_spam">Spam</string> + <string name="no_chats_found">Razgovori nisu pronađeni</string> + <string name="icon_descr_speaker_on">Zvučnik uključen</string> + <string name="v5_6_safer_groups">Bezbednije grupe</string> + <string name="reject_contact_button">Odbiti</string> + <string name="reject">Odbiti</string> + <string name="no_chats_in_list">Razgovori nisu u listi %s.</string> + <string name="images_limit_title">Previše slika!</string> + <string name="group_member_role_author">autor</string> + <string name="export_theme">Izvesti temu</string> + <string name="v4_4_live_messages">Žive poruke</string> + <string name="operator_website">Website</string> + <string name="custom_time_unit_days">dani</string> + <string name="compose_forward_messages_n">Prosleđivanje %1$s poruka</string> + <string name="network_use_onion_hosts_prefer">Kada je dostupno</string> + <string name="v5_7_forward">Proslediti i sačuvati poruke</string> + <string name="forwarded_chat_item_info_tab">Prosleđeno</string> + <string name="snd_error_proxy">Server za prosleđivanje: %1$s\nGreška: %2$s</string> + <string name="mark_read">Označiti da je pročitano</string> + <string name="snd_error_proxy_relay">Server za prosleđivanje : %1$s\nGreška odredišnog servera: %2$s</string> + <string name="verify_connections">Verifikovati veze</string> + <string name="chat_theme_apply_to_mode">Primeniti na</string> + <string name="migrate_from_device_stopping_chat">Zaustavljanje razgovora</string> + <string name="paste_the_link_you_received">Nalepiti link koji ste primili</string> + <string name="rate_the_app">Oceniti aplikaciju</string> + <string name="no_unread_chats">Nema nepročitanih razgovora</string> + <string name="report_archive_alert_desc">Izveštaj će biti arhiviran za Vas.</string> + <string name="keep_unused_invitation_question">Sačuvati neiskorišćenu pozivnicu?</string> + <string name="voice_message_send_text">Glasovna poruka…</string> + <string name="connect_use_current_profile">Koristiti trenutni profil</string> + <string name="paste_link">Nalepiti link</string> + <string name="v5_5_simpler_connect_ui">Nalepiti link za povezivanje!</string> + <string name="live">ŽIVO</string> + <string name="star_on_github">Zvezdica na GitHubu</string> + <string name="wallpaper_scale">Razmera</string> + <string name="v4_4_verify_connection_security">Verifikovati bezbednost veze</string> + <string name="current_user">Trenutni profil</string> + <string name="button_delete_group">Odstraniti grupu</string> + <string name="forwarded_description">prosleđeno</string> + <string name="notification_preview_mode_message">Tekst poruke</string> + <string name="forwarded_from_chat_item_info_title">Prosleđeno od</string> + <string name="loading_chats">Učitavanje razgovora…</string> + <string name="forward_alert_title_messages_to_forward">Proslediti %1$s poruku(e)?</string> + <string name="connection_security">Bezbednost veze</string> + <string name="onboarding_notifications_mode_off">Kada je aplikacija pokrenuta</string> + <string name="forward_chat_item">Proslediti</string> + <string name="large_file">Velika datoteka!</string> + <string name="migrate_to_device_apply_onion">Primeniti</string> + <string name="delete_message_cannot_be_undone_warning">Poruka će biti odstranjena - ovo se ne može poništiti!</string> + <string name="maximum_message_size_title">Poruka je prevelika!</string> + <string name="loading_remote_file_title">Učitavanje datoteke</string> + <string name="voice_message">Glasovna poruka</string> + <string name="voice_message_with_duration">Glasovna poruka (%1$s)</string> + <string name="learn_more">Naučiti više</string> + <string name="mark_code_verified">Označiti da je verifikovano</string> + <string name="use_chat">Koristiti razgovor</string> + <string name="mark_unread">Označiti da nije pročitano</string> + <string name="verify_connection">Verifikovati vezu</string> + <string name="display_name_requested_to_connect">zahtev za povezivanje</string> + <string name="tap_to_scan">Dodirnuti za skeniranje</string> + <string name="unknown_database_error_with_info">Nepoznata greška u bazi podataka: %s</string> + <string name="servers_info_messages_received">Primljene poruke</string> + <string name="duplicates_label">duplikati</string> + <string name="chat_preferences_yes">da</string> + <string name="migrate_from_device_migration_complete">Migracija završena</string> + <string name="network_smp_proxy_fallback_allow">Da</string> + <string name="v6_2_network_decentralization_enable_flux">Omogućiti flux</string> + <string name="error_with_info">Greška: %s</string> + <string name="error_stopping_chat">Greška u zaustavljanju razgovora</string> + <string name="contribute">Doprinesite</string> + <string name="moderated_item_description">moderirao %s</string> + <string name="la_current_app_passcode">Trenutan Pin kod</string> + <string name="waiting_for_video">Čekanje na video</string> + <string name="waiting_for_file">Čekanje na dokument</string> + <string name="network_use_onion_hosts_required">Neophodno</string> + <string name="network_smp_proxy_mode_always">Uvek</string> + <string name="app_check_for_updates_button_download">Preuzimanje %s (%s)</string> + <string name="ignore">Ignorisati</string> + <string name="new_passcode">Novi Pin kod</string> + <string name="la_app_passcode">Pin kod aplikacije</string> + <string name="enabled_self_destruct_passcode">Omogućiti pin kod za samouništenje</string> + <string name="self_destruct_passcode">Pin kod za samouništenje</string> + <string name="settings_shutdown">Isključiti</string> + <string name="info_row_moderated_at">Moderirano u</string> + <string name="recent_history">Vidljiva istorija</string> + <string name="remote_ctrl_disconnected_with_reason">Prekinuta veza iz razloga: %s</string> + <string name="network_type_cellular">Mobilna</string> + <string name="servers_info_messages_sent">Poruke poslane</string> + <string name="for_everybody">Za sve</string> + <string name="debug_logs">Omogući evidenciju</string> + <string name="la_immediately">Odmah</string> + <string name="icon_descr_sent_msg_status_send_failed">greška u slanju</string> + <string name="smp_servers_delete_server">Odstraniti server</string> + <string name="snd_group_event_member_blocked">blokirali ste %s</string> + <string name="v5_6_quantum_resistant_encryption">Kvantno otporno šifrovanje</string> + <string name="dont_create_address">Ne praviti adresu</string> + <string name="receipts_groups_enable_for_all">Omogućiti svim grupama</string> + <string name="dont_show_again">Ne prikazivati ponovo</string> + <string name="custom_time_unit_minutes">minuti</string> + <string name="custom_time_unit_months">meseci</string> + <string name="group_welcome_preview">Pregled</string> + <string name="la_enter_app_passcode">Uneti Pin kod</string> + <string name="network_session_mode_session">Sesija aplikacije</string> + <string name="notification_preview_mode_contact_desc">Prikazati samo kontakt</string> + <string name="group_member_status_unknown_short">nepoznato</string> + <string name="change_order_chat_list_menu_action">Promeniti redosled</string> + <string name="snd_conn_event_switch_queue_phase_changing_for_member">promena adrese za %s…</string> + <string name="failed_to_active_user_title">Greška u promeni profila!</string> + <string name="connect_use_new_incognito_profile">Koristiti novi anoniman profil</string> + <string name="display_name_connection_established">veza uspostavljena</string> + <string name="change_list">Promeniti listu</string> + <string name="audio_device_earpiece">Slušalica</string> + <string name="shutdown_alert_question">Isključiti?</string> + <string name="this_device">Ovaj uređaj</string> + <string name="enable_self_destruct">Omogućiti samouništenje</string> + <string name="notifications_mode_service">Uvek uključeno</string> + <string name="delete_report">Odstraniti izveštaj</string> + <string name="icon_descr_image_snd_complete">Poslana slika</string> + <string name="confirm_verb">Potvrditi</string> + <string name="v5_2_more_things">Još nekoliko stvari</string> + <string name="connect_via_invitation_link">Povezati pomoću jednokratnog linka?</string> + <string name="la_mode_passcode">Pin kod</string> + <string name="both_you_and_your_contact_can_send_disappearing">Vi i Vaš kontakt, možete da šaljete poruke koje nestaju.</string> + <string name="v5_7_call_sounds">Zvukovi u pozivu</string> + <string name="conn_event_disabled_pq">standardno od kraja-do-kraja šifrovanje</string> + <string name="group_member_role_moderator">moderator</string> + <string name="chat_preferences_always">uvek</string> + <string name="v4_5_reduced_battery_usage">Smanjiti upotrebu baterije</string> + <string name="servers_info_proxied_servers_section_header">Proxied(posrednički) serveri</string> + <string name="smp_servers_new_server">Novi server</string> + <string name="subscription_percentage">Prikazati procente</string> + <string name="exit_without_saving">Napustiti bez čuvanja</string> + <string name="run_chat_section">POKRENUTI RAZGOVOR</string> + <string name="unblock_for_all_question">Odblokirati člana za sve?</string> + <string name="migrate_to_device_database_init">Priprema za preuzimanje</string> + <string name="proxied">Proxied(posredovan)</string> + <string name="decryption_error">Greška u dešifrovanju</string> + <string name="settings_notification_preview_mode_title">Prikazati pregled</string> + <string name="database_migrations">Migracije: %s</string> + <string name="alert_message_no_group">Ova grupa više ne postoji.</string> + <string name="snd_group_event_changed_member_role">promenili ste ulogu %s u %s</string> + <string name="group_member_role_observer">posmatrač</string> + <string name="info_row_message_status">Status poruke</string> + <string name="info_row_file_status">Status datoteke</string> + <string name="chat_preferences_default">podrazumevano (%s)</string> + <string name="v5_8_persian_ui">Persijski UI</string> + <string name="servers_info_reset_stats_alert_message">Statistika servera ce biti obnovljena - ovo se ne može poništiti!</string> + <string name="la_change_app_passcode">Promeniti pin kod</string> + <string name="icon_descr_waiting_for_image">Čekanje na sliku</string> + <string name="waiting_for_image">Čekanje na sliku</string> + <string name="icon_descr_waiting_for_video">Čekanje na video</string> + <string name="to_start_a_new_chat_help_header">Za početak novog razgovora</string> + <string name="receipts_contacts_enable_for_all">Omogućiti svima</string> + <string name="error_starting_chat">Greška u pokretanju razgovora</string> + <string name="rcv_group_event_changed_member_role">uloga promenjena iz %s u %s</string> + <string name="change_member_role_question">Promeniti ulogu grupe?</string> + <string name="smp_server_test_delete_file">Odstraniti datoteku</string> + <string name="icon_descr_simplex_team">SimpleX Tim</string> + <string name="moderated_description">moderirano</string> + <string name="connection_error_blocked_desc">Operator servera blokira vezu:\n%1$s.</string> + <string name="change_role">Promeniti ulogu</string> + <string name="this_device_name">Ime ovog uređaja</string> + <string name="tap_to_start_new_chat">Dodirnuti za početak novog razgovora</string> + <string name="group_preview_you_are_invited">pozvani ste u grupu</string> + <string name="image_saved">Slika sačuvana u Galeriji</string> + <string name="rcv_group_event_changed_your_role">vaša uloga je promenjena u %s</string> + <string name="snd_conn_event_switch_queue_phase_completed">promenili ste adresu</string> + <string name="cant_call_contact_deleted_alert_text">Kontakt je odstranjen.</string> + <string name="v5_1_japanese_portuguese_interface">Japanski i Portugalski UI</string> + <string name="v5_1_self_destruct_passcode">Pin kod za samouništenje</string> + <string name="v5_3_discover_join_groups">Pronađite i pristupite grupama</string> + <string name="for_me_only">Odstraniti za mene</string> + <string name="image_decoding_exception_title">Greška u dekodiranju</string> + <string name="icon_descr_server_status_disconnected">Prekinuta veza</string> + <string name="confirm_delete_contact_question">Potvrditi brisanje kontakta?</string> + <string name="switching_profile_error_title">Greška u promeni profila</string> + <string name="self_destruct_passcode_changed">Pin kod za samouništenje promenjen!</string> + <string name="self_destruct_passcode_enabled">Pin kod za samouništenje omogućen!</string> + <string name="import_database_question">Uvesti bazu podataka razgovora?</string> + <string name="unblock_member_button">Odblokirati člana</string> + <string name="unblock_member_question">Odblokirati člana?</string> + <string name="send_errors">Poslati greške</string> + <string name="change_verb">Promeniti</string> + <string name="waiting_for_desktop">Čekanje na računar…</string> + <string name="dont_enable_receipts">Ne dozvoliti</string> + <string name="feature_enabled">omogućeno</string> + <string name="incognito_random_profile">Vaš nasumičan profil</string> + <string name="please_try_later">Molimo pokušajte kasnije.</string> + <string name="send_us_an_email">Pošaljite nam email</string> + <string name="share_text_moderated_at">Moderirano u: %s</string> + <string name="migrate_from_device_confirm_upload">Potvrditi otpremanje</string> + <string name="wallpaper_scale_fill">Popuniti</string> + <string name="decryption_errors">Greške u dešifrovanju</string> + <string name="servers_info_subscriptions_total">Ukupno</string> + <string name="confirm_database_upgrades">Potvrditi unapređenje baze podataka</string> + <string name="you_are_invited_to_group">Pozvani ste u grupu</string> + <string name="snd_group_event_changed_role_for_yourself">promenili ste vašu ulogu na %s</string> + <string name="send_receipts_disabled_alert_title">Potvrda je onemogućena</string> + <string name="immune_to_spam_and_abuse">Imunitet na spam</string> + <string name="integrity_msg_duplicate">duplikat poruke</string> + <string name="privacy_chat_list_open_links_yes">Da</string> + <string name="database_error">Greška u bazi podataka</string> + <string name="chat_item_ttl_default">podrazumevano (%s)</string> + <string name="chat_item_ttl_year">1 godina</string> + <string name="snd_group_event_member_deleted">uklonili ste %1$s</string> + <string name="snd_group_event_user_left">napustili ste</string> + <string name="snd_conn_event_switch_queue_phase_completed_for_member">promenili ste adresu za %s</string> + <string name="unblock_for_all">Odblokirati za sve</string> + <string name="share_text_message_status">Status poruke: %s</string> + <string name="share_text_file_status">Status datoteke: %s</string> + <string name="network_option_tcp_connection_timeout">TCP veza тајм-аут</string> + <string name="color_sent_message">Poslana poruka</string> + <string name="feature_enabled_for_contact">Omogućeno za kontakt</string> + <string name="feature_enabled_for_you">Omogućeno za Vas</string> + <string name="both_you_and_your_contact_can_send_voice">Vi i Vaš kontakt, možete da šaljete glasovne poruke.</string> + <string name="feature_enabled_for">Omogućeno za</string> + <string name="v6_0_upgrade_app">Nadograditi aplikaciju automatski</string> + <string name="v5_7_new_interface_languages">Litvanski UI</string> + <string name="servers_info_target">Prikazati informacije za</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="in_developing_title">Uskoro!</string> + <string name="expired_label">istekao</string> + <string name="both_you_and_your_contact_can_make_calls">Vi i Vaš kontakt, možete da obavljate pozive.</string> + <string name="contact_already_exists">Kontakt već postoji</string> + <string name="view_security_code">Pogledati bezbednostni kod</string> + <string name="share_invitation_link">Podeliti jednokratnu veza</string> + <string name="forward_message">Proslediti poruku…</string> + <string name="address_or_1_time_link">Adresa ili jednokratna veza?</string> + <string name="enable_camera_access">Omogućiti pristup kameri</string> + <string name="invalid_QR_code">Nevažeći QR kod</string> + <string name="one_time_link">Jednokratna pozivnica</string> + <string name="enter_welcome_message">Uneti poruku dobrodošlice…</string> + <string name="compose_save_messages_n">Čuvanje %1$s poruka</string> + <string name="network_session_mode_transport_isolation">Prenosna izolacija</string> + <string name="enter_welcome_message_optional">Uneti poruku dobrodošlice… (neobavezno)</string> + <string name="privacy_message_draft">Koncept poruke</string> + <string name="incorrect_passcode">Neispravan pin kod</string> + <string name="callstate_waiting_for_confirmation">čeka se potvrda…</string> + <string name="smp_servers_enter_manually">Uneti server ručno</string> + <string name="status_contact_has_no_e2e_encryption">kontakt nema e2e šifrovanje</string> + <string name="contact_deleted">Kontakt odstranjen!</string> + <string name="invalid_qr_code">Nevažeći QR kod</string> + <string name="v4_2_security_assessment">Procena bezbednosti</string> + <string name="smp_server_test_compare_file">Uporediti datoteku</string> + <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Kontakt i sve poruke biće odstranjene - ovo ne može biti vraćeno!</string> + <string name="revoke_file__message">Datoteka će biti odstranjena sa servera.</string> + <string name="revoke_file__confirm">Opozvati</string> + <string name="icon_descr_cancel_image_preview">Poništiti pregled slike</string> + <string name="sender_at_ts">%s u %s</string> + <string name="icon_descr_cancel_live_message">Poništiti živu poruku</string> + <string name="add_contact">Jednokratna pozivnica</string> + <string name="smp_servers_invalid_address">Nevažeća adresa server!</string> + <string name="saved_ICE_servers_will_be_removed">Sačuvani WebRTC ICE biće uklonjeni.</string> + <string name="la_lock_mode_passcode">Unos pin koda</string> + <string name="message_forwarded_title">Prosleđena poruka</string> + <string name="forward_alert_title_nothing_to_forward">Ništa za prosleđivanje</string> + <string name="onboarding_network_operators_review_later">Pregledati kasnije</string> + <string name="status_contact_has_e2e_encryption">kontakt ima e2e šifrovanje</string> + <string name="lock_after">Zaključati nakon</string> + <string name="notification_preview_somebody">Skriven kontakt:</string> + <string name="icon_descr_cancel_file_preview">Poništiti pregled datoteke</string> + <string name="receipts_contacts_title_enable">Omogućiti potvrde?</string> + <string name="receipts_groups_title_enable">Omogućiti potvrde za grupe?</string> + <string name="appearance_in_app_bars_alpha">Transparentnost</string> + <string name="operator_review_conditions">Pregled uslova</string> + <string name="migrate_from_device_cancel_migration">Poništiti migraciju</string> + <string name="revoke_file__action">Opozvati datoteku</string> + <string name="revoke_file__title">Opozvati datoteku?</string> + <string name="live_message">Živa poruka!</string> + <string name="download_errors">Greške u preuzimanju</string> + <string name="save_welcome_message_question">Sačuvati poruku dobrodošlice?</string> + <string name="not_compatible">Nekompatibilno!</string> + <string name="migrate_from_device_database_init">Pripremanje otpremanja</string> + <string name="save_profile_password">Sačuvati šifru profila</string> + <string name="webrtc_ice_servers">WebRTC ICE serveri</string> + <string name="settings_notification_preview_title">Pregled obaveštenja</string> + <string name="callstatus_connecting">povezivanje poziva…</string> + <string name="passcode_not_changed">Pin kod nije promenjen!</string> + <string name="export_database">Izvesti bazu podataka</string> + <string name="alert_title_group_invitation_expired">Istekla pozivnica!</string> + <string name="group_invitation_item_description">pozivnica u grupu %1$s</string> + <string name="incompatible_database_version">Nekompatibilna verzija baze podataka</string> + <string name="network_status">Status mreže</string> + <string name="operator_use_for_sending">Za slanje</string> + <string name="v4_5_message_draft">Koncept poruke</string> + <string name="v4_5_transport_isolation">Prenosna izolacija</string> + <string name="enter_this_device_name">Uneti ime ovog uređaja…</string> + <string name="servers_info_transport_sessions_section_header">Prenosne sesije</string> + <string name="migrate_to_device_download_failed">Neuspešno preuzimanje</string> + <string name="stop_sharing_address">Zaustaviti deljenje adrese?</string> + <string name="error_receiving_file">Greška u primanju datoteke</string> + <string name="error_saving_file">Greška u čuvanju datoteke</string> + <string name="new_chat_share_profile">Podeliti profil</string> + <string name="stop_sharing">Zaustaviti deljenje</string> + <string name="privacy_show_last_messages">Prikazati poslednje poruke</string> + <string name="v6_0_delete_many_messages_descr">Odstraniti do 20 poruka odjednom.</string> + <string name="remote_host_error_disconnected"><![CDATA[Mobilni <b>%s</b> prekinuta veza]]></string> + <string name="connect_plan_this_is_your_own_simplex_address">Ovo je Vaša vlastita SimpleX adresa!</string> + <string name="clear_verification">Očistiti verifikaciju</string> + <string name="unable_to_open_browser_title">Greška u otvaranju pretraživača</string> + <string name="no_chats">Nema razgovora</string> + <string name="connect_with_contact_name_question">Povezati se sa %1$s?</string> + <string name="contact_wants_to_connect_with_you">želi da se poveze sa Vama!</string> + <string name="private_routing_show_message_status">Prikazati status poruke</string> + <string name="connection_not_ready">Veza nije spremna.</string> + <string name="v6_0_connect_faster_descr">Povežite se brže sa svojim prijateljima.</string> + <string name="remote_host_was_disconnected_toast"><![CDATA[Mobilni <b>%s</b> prekinuta veza]]></string> + <string name="migrate_to_device_file_delete_or_link_invalid">Datoteka je odstranjena ill je link neispravan</string> + <string name="servers_info_detailed_statistics_sent_messages_total">Ukupno poslano</string> + <string name="for_social_media">Za društvene mreže</string> + <string name="remote_host_error_missing"><![CDATA[Mobilni <b>%s</b> nedostaje]]></string> + <string name="migrate_to_device_downloading_archive">Preuzimanje arhive</string> + <string name="servers_info_detailed_statistics_received_total">Ukupno primljeno</string> + <string name="remote_host_error_inactive"><![CDATA[Mobilni <b>%s</b> je neaktivan]]></string> + <string name="notification_preview_mode_contact">Ime kontakta</string> + <string name="message_delivery_error_title">Greška pri isporuci poruke</string> + <string name="delete_messages_mark_deleted_warning">Poruke će biti označene za odstranjivanje. Primaoci će moći da otkriju te poruke.</string> + <string name="delete_files_and_media_question">Odstraniti datoteke i medijski sadržaj?</string> + <string name="stop_rcv_file__message">Primanje datoteke biće zaustavljeno.</string> + <string name="file_will_be_received_when_contact_is_online">Datoteka će biti primljena kada vaš kontakt bude na mreži, molimo sačekajte ili proverite kasnije!</string> + <string name="choose_file_title">Izabrati datoteku</string> + <string name="migrate_to_device_migrating">Migriranje</string> + <string name="migrate_from_device_title">Migriraj uređaj</string> + <string name="stop_snd_file__title">Zaustaviti slanje datoteke?</string> + <string name="no_selected_chat">Nije izabran nijedan razgovor</string> + <string name="rcv_conn_event_switch_queue_phase_changing">promena adrese…</string> + <string name="remote_host_error_busy"><![CDATA[Mobilni <b>%s</b> je zauzet]]></string> + <string name="snd_conn_event_switch_queue_phase_changing">promena adrese…</string> + <string name="recent_history_is_sent_to_new_members">Do 100 poslednjih poruka se šalje novim članovima.</string> + <string name="v5_1_better_messages">Bolje poruke</string> + <string name="delete_messages_cannot_be_undone_warning">Poruke će biti odstranjene - ovo se ne može poništiti!</string> + <string name="stop_snd_file__message">Slanje datoteke biće zaustavljeno.</string> + <string name="stop_rcv_file__title">Zaustaviti primanje datoteke?</string> + <string name="file_error">Greška u datoteci</string> + <string name="delete_without_notification">Odstraniti bez obaveštenja</string> + <string name="migrate_from_device_upload_failed">Greška u otpremanju</string> + <string name="servers_info_detailed_statistics_receive_errors">Ukupno grešaka</string> + <string name="servers_info_detailed_statistics">Detaljna statistika</string> + <string name="deletion_errors">Greška prilikom odstranjivanja</string> + <string name="migrate_from_device_repeat_upload">Ponoviti otpremanje</string> + <string name="settings_section_title_use_from_desktop">Koristiti sa računara</string> + <string name="connect_to_desktop">Povezati sa računarom</string> + <string name="connected_to_desktop">Povezano sa računarom</string> + <string name="allow_voice_messages_question">Dozvoliti glasovne poruke?</string> + <string name="switch_receiving_address_question">Promeniti prijemnu adresu?</string> + <string name="message_reactions">Reakcije na poruku</string> + <string name="allow_message_reactions">Dozvoliti reakcije na poruku.</string> + <string name="v5_1_message_reactions">Reakcije na poruku</string> + <string name="switch_receiving_address">Promeniti prijemnu adresu</string> + <string name="message_deleted_or_not_received_error_title">Nema poruka</string> + <string name="no_history">Nema istorije</string> + <string name="message_reactions_are_prohibited">Reakcije na poruku su zabranjene.</string> + <string name="smp_server_test_secure_queue">Bezbedan redosled</string> + <string name="smp_server_test_create_queue">Napraviti redosled</string> + <string name="network_socks_toggle_use_socks_proxy">Koristiti SOCKS proxy</string> + <string name="error_encrypting_database">Greška pri šifrovanju baze podataka</string> + <string name="operator_use_for_messages">Koristiti za poruke</string> + <string name="failed_to_create_user_title">Greška pri kreiranju profila!</string> + <string name="error_loading_details">Greška pri učitavanju detalja</string> + <string name="error_saving_xftp_servers">Greška pri čuvanju XFTP servera</string> + <string name="uploaded_files">Otpremljene datoteke</string> + <string name="open_server_settings_button">Otvoriti podešavanja servera</string> + <string name="error_deleting_note_folder">Greška pri odstranjivanju privatnih beleški</string> + <string name="error_forwarding_messages">Greška pri prosleđivanju poruka</string> + <string name="forward_files_messages_deleted_after_selection_desc">Poruke su odstranjene nakon što ste ih odabrali.</string> + <string name="network_smp_proxy_mode_never_description">NE koristiti privatno usmeravanje.</string> + <string name="network_smp_proxy_fallback_allow_protected">Kada je IP skrivena</string> + <string name="appearance_settings">Izgled</string> + <string name="invite_friends">Pozvati prijatelje</string> + <string name="confirm_password">Potvrditi šifru</string> + <string name="confirm_passcode">Potvrditi pin kod</string> + <string name="set_passcode">Odrediti pin kod</string> + <string name="if_you_enter_self_destruct_code">Ako unesete Vaš pin kod za samouništenje prilikom otvaranja aplikacije:</string> + <string name="receipts_contacts_title_disable">Onemogućiti potvrde?</string> + <string name="group_member_status_introduced">povezivanje (uvedeno)</string> + <string name="enter_password_to_show">Uneti šifru u pretragu</string> + <string name="error_loading_xftp_servers">Greška pri učitavanju XFTP servera</string> + <string name="error_deleting_group">Greška pri odstranjivanju grupe</string> + <string name="cannot_share_message_alert_title">Nije moguće poslati poruku</string> + <string name="send_disappearing_message">Poslati poruku koja nestaje</string> + <string name="message_servers">Serveri za poruke</string> + <string name="whats_new_read_more">Pročitati više</string> + <string name="xftp_server">XFTP server</string> + <string name="error_creating_address">Greška pri pravljenju adrese</string> + <string name="icon_descr_sent_msg_status_unauthorized_send">neovlašćeno slanje</string> + <string name="verify_security_code">Verifikovati bezbednostni kod</string> + <string name="icon_descr_close_button">Dugme zatvoriti</string> + <string name="error_joining_group">Greška pri pristupanju grupi</string> + <string name="developer_options_section">Opcije za programere</string> + <string name="settings_developer_tools">Alati za programere</string> + <string name="chat_preferences">Preference chata</string> + <string name="migrate_to_device_confirm_network_settings_footer">Potvrdite da su mrežna podešavanja za ovaj uređaj ispravna.</string> + <string name="blocking_reason_content">Sadržaj krši uslove korišćenja</string> + <string name="cannot_receive_file">Nije moguće primiti datoteku</string> + <string name="error_creating_message">Greška pri kreiranju poruke</string> + <string name="turn_off_system_restriction_button">Otvoriti podešavanja aplikacije</string> + <string name="auth_enable_simplex_lock">Omogućiti SimpleX Zaključavanje</string> + <string name="share_file">Podeliti datoteku…</string> + <string name="send_disappearing_message_custom_time">Prilagođeno vreme</string> + <string name="servers_info_connected_servers_section_header">Povezani serveri</string> + <string name="profile_will_be_sent_to_contact_sending_link">Vaš profil će biti poslan kontaktu od koga ste primili ovu vezu.</string> + <string name="notifications_mode_periodic">Pokrenuti periodično</string> + <string name="alert_title_skipped_messages">Preskočene poruke</string> + <string name="passcode_set">Pin kod postavljen!</string> + <string name="all_app_data_will_be_cleared">Svi podaci u aplikaciji su odstranjeni.</string> + <string name="app_passcode_replaced_with_self_destruct">Pin kod aplikacije je zamenjen pin kodom za samouništenje.</string> + <string name="settings_section_title_support">POTPORI SIMPLEX CHAT</string> + <string name="settings_section_title_message_shape">Oblik poruke</string> + <string name="settings_section_title_icon">IKONA APLIKACIJE</string> + <string name="database_passphrase">Pristupna fraza baze podataka</string> + <string name="set_passphrase">Odrediti pristupnu frazu</string> + <string name="database_will_be_encrypted">Baza podataka će biti šifrovana.</string> + <string name="wrong_passphrase_title">Neispravna pristupna fraza!</string> + <string name="enter_passphrase">Uneti pristupnu frazu…</string> + <string name="operator_conditions_of_use">Uslovi korišćenja</string> + <string name="error_adding_server">Greška pri dodavanju servera</string> + <string name="color_received_quote">Primljen odgovor</string> + <string name="appearance_zoom">Uvećanje</string> + <string name="allow_your_contacts_to_send_voice_messages">Dozvoliti slanje glasovnih poruka Vašim kontaktima.</string> + <string name="allow_voice_messages_only_if">Dozvoliti glasovne poruke samo ako ih Vaš kontakt dozvoljava.</string> + <string name="allow_your_contacts_adding_message_reactions">Dozvoli svojim kontaktima dodavanje reakcija na poruke.</string> + <string name="allow_direct_messages">Dozvoliti slanje direktnih poruka članovima.</string> + <string name="prohibit_direct_messages">Zabraniti slanje direktnih poruka članovima.</string> + <string name="allow_to_send_voice">Dozvoliti slanje glasovnih poruka.</string> + <string name="allow_to_send_simplex_links">Dozvoliti slanje SimpleX veza.</string> + <string name="simplex_links_are_prohibited_in_group">SimpleX veze su zabranjene.</string> + <string name="v5_4_incognito_groups_descr">Napraviti grupu koristeći nasumičan profil.</string> + <string name="v6_0_reachable_chat_toolbar_descr">Koristiti aplikaciju jednom rukom.</string> + <string name="migrate_to_device_confirm_network_settings">Potvrditi mrežna podešavanja</string> + <string name="error_saving_database">Greška pri čuvanju baze podataka</string> + <string name="color_sent_quote">Poslan odgovor</string> + <string name="mute_all_chat">Isključiti zvuk svima</string> + <string name="network_proxy_incorrect_config_title">Greška pri čuvanju proxy</string> + <string name="or_to_share_privately">Ili deliti privatno</string> + <string name="error_saving_user_password">Greška pri čuvanju korisničke šifre</string> + <string name="your_preferences">Vaše preference</string> + <string name="servers_info_previously_connected_servers_section_header">Prethodno povezani serveri</string> + <string name="chunks_downloaded">Komadi su preuzeti</string> + <string name="disable_automatic_deletion_question">Onemogućiti automatsko brisanje poruka?</string> + <string name="thank_you_for_installing_simplex">Hvala Vam što ste instalirali SimpleX Chat!</string> + <string name="smp_servers_test_some_failed">Neki serveri nisu prošli test:</string> + <string name="operator_info_title">Mrežni operator</string> + <string name="migrate_to_device_try_again">Možete pokušati ponovo.</string> + <string name="sent_via_proxy">Poslano putem proxy</string> + <string name="display_name_invited_to_connect">pozvan za povezivanje</string> + <string name="simplex_link_connection">pomoću %1$s</string> + <string name="error_loading_smp_servers">Greška pri učitavanju SMP servera</string> + <string name="error_saving_smp_servers">Greška pri čuvanju SMP servera</string> + <string name="error_changing_address">Greška pri promeni adrese</string> + <string name="enter_passphrase_notification_title">Pristupnu frazu je potrebna</string> + <string name="v6_3_mentions">Spomenuti članove 👋</string> + <string name="v6_3_better_privacy_and_security">Bolja privatnost i bezbednost</string> + <string name="v6_3_organize_chat_lists_descr">Ne propustiti bitne poruke.</string> + <string name="custom_time_picker_custom">prilagođen</string> + <string name="discover_on_network">Otkriti pomoću lokalne mreže</string> + <string name="migrate_to_device_enter_passphrase">Uneti pristupnu frazu</string> + <string name="migrate_from_device_check_connection_and_try_again">Proverite internet vezu i pokušajte ponovo</string> + <string name="servers_info_missing">Bez Informacija, pokušajte ponovo da učitate</string> + <string name="chunks_deleted">Komadi su odstranjeni</string> + <string name="ntf_channel_calls">SimpleX Chat pozivi</string> + <string name="xftp_servers">XFTP serveri</string> + <string name="network_and_servers">Mreža i serveri</string> + <string name="configure_ICE_servers">Podesite ICE servere</string> + <string name="network_disable_socks">Koristiti direktnu internet vezu?</string> + <string name="app_check_for_updates_button_install">Instalirati ažuriranje</string> + <string name="your_calls">Vaši pozivi</string> + <string name="enable_lock">Omogućiti zaključavanje</string> + <string name="info_row_database_id">ID Baze podataka</string> + <string name="info_row_updated_at">Zapis ažuriran u</string> + <string name="operator_use_for_files">Koristiti za datoteke</string> + <string name="turning_off_service_and_periodic">Optimizacija baterije je aktivna, isključeni su usluga u pozadini i periodični zahtevi za nove poruke. Možete ih ponovo omogućiti putem podešavanja.</string> + <string name="la_authenticate">Potvrda identiteta</string> + <string name="smp_servers_test_failed">Test servera nije uspeo!</string> + <string name="error_exporting_chat_database">Greška pri izvoženju baze podataka razgovora</string> + <string name="network_option_protocol_timeout">Vremensko ograničenje protokola</string> + <string name="error_sending_message">Greška pri slanju poruke</string> + <string name="error_deleting_contact">Greška pri odstranjivanju kontakta</string> + <string name="share_one_time_link">Napraviti jednokratnu pozivnicu</string> + <string name="markdown_help">Markdown pomoć</string> + <string name="allow_to_send_files">Dozvoliti slanje datoteka i medijskog sadržaji.</string> + <string name="passcode_changed">Pin kod promenjen!</string> + <string name="share_text_database_id">ID Baze podataka: %d</string> + <string name="simplex_link_mode">SimpleX veze</string> + <string name="operator_added_xftp_servers">Dodani serveri za medije i datoteke</string> + <string name="connect__your_profile_will_be_shared">Vas profil %1$s će biti deljen.</string> + <string name="callstate_received_answer">primljen odgovor…</string> + <string name="onboarding_network_operators">Mrežni operatori</string> + <string name="network_smp_proxy_mode_private_routing">Privatno usmeravanje</string> + <string name="non_content_uri_alert_title">Nevažeća putanja datoteke</string> + <string name="moderate_verb">Moderirati</string> + <string name="moderated_items_description">%1$d poruke moderirane %2$s</string> + <string name="unblock_member_desc">Poruke od %s biće prikazane!</string> + <string name="database_passphrase_and_export">Pristupna fraza baze podataka i izvoz</string> + <string name="network_smp_proxy_mode_unknown_description">Koristite privatno usmeravanje sa nepoznatim serverima.</string> + <string name="privacy_media_blur_radius_strong">Intenzivno</string> + <string name="delete_message_mark_deleted_warning">Poruka će biti označena za ostranjivanje. Primaoci će moći da otkriju ovu poruku.</string> + <string name="privacy_and_security">Privatnost i bezbednost</string> + <string name="ntf_channel_messages">SimpleX Chat poruke</string> + <string name="migrate_from_device_try_again">Možete pokušati ponovo.</string> + <string name="migrate_from_device_error_saving_settings">Greška pri čuvanju podešavanja</string> + <string name="v4_6_group_welcome_message_descr">Postavi poruku koja će biti prikazana novim članovima!</string> + <string name="failed_to_save_servers">Greška pri čuvanju servera</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">Član će biti uklonjen iz razgovora – ovo se ne može poništiti!</string> + <string name="profile_update_event_updated_profile">ažuriran profil</string> + <string name="group_member_status_announced">povezivanje (najavljeno)</string> + <string name="error_changing_role">Greška u promeni uloge</string> + <string name="v5_1_custom_themes">Prilagođena tema</string> + <string name="v5_2_favourites_filter">Pronaći razgovore brže</string> + <string name="your_privacy">Vaša privatnost</string> + <string name="past_member_vName">Bivši član %1$s</string> + <string name="network_option_protocol_timeout_per_kb">Vremensko ograničenje protokola po KB</string> + <string name="privacy_media_blur_radius_soft">Blago</string> + <string name="share_image">Podeliti medije…</string> + <string name="network_smp_proxy_fallback_prohibit_description">NE šaljite poruke direktno, čak i ako vaš ili odredišni serveri ne podržavaju privatno usmeravanje.</string> + <string name="change_self_destruct_passcode">Promeniti pin kod za samouništenje</string> + <string name="update_database_passphrase">Ažurirati pristupna frazu baze podataka</string> + <string name="report_archive_for_me">Za mene</string> + <string name="share_message">Podeliti poruku…</string> + <string name="archive_reports">Arhivirati izveštaje</string> + <string name="simplex_links_not_allowed">SimpleX veze nisu dozvoljene</string> + <string name="change_automatic_deletion_question">Promeniti automatsko brisanje poruka?</string> + <string name="voice_messages_prohibited">Glasovne poruke su zabranjene!</string> + <string name="or_show_this_qr_code">Ili prikazati ovaj kod</string> + <string name="smp_servers_use_server_for_new_conn">Koristiti za nove konekcije</string> + <string name="your_ICE_servers">Vaši ICE serveri</string> + <string name="callstate_starting">pokretanje…</string> + <string name="permissions_open_settings">Otvoriti podešavanje</string> + <string name="call_desktop_permission_denied_title">Za odvijanje poziva, dozvoliti upotrebu mikrofona. Završite razgovor i pokušajte ponovo.</string> + <string name="database_encrypted">Baza podataka je šifrovana!</string> + <string name="share_text_updated_at">Zapis ažuriran u: %s</string> + <string name="member_will_be_removed_from_group_cannot_be_undone">Član će biti uklonjen iz grupe – ovo se ne može poništiti!</string> + <string name="calls_prohibited_alert_title">Pozivi zabranjeni!</string> + <string name="available_in_v51">\nDostupno u v5.1</string> + <string name="simplex_links">SimpleX veze</string> + <string name="full_deletion">Odstraniti za sve</string> + <string name="allow_to_send_disappearing">Dozvoliti slanje poruka koje nestaju.</string> + <string name="v5_0_app_passcode">Pin kod aplikacije</string> + <string name="v5_4_block_group_members_descr">Za sakrivanje neželjenih poruka.</string> + <string name="prefs_error_saving_settings">Greška pri čuvanju podešavanja</string> + <string name="invalid_name">Nevažeće ime!</string> + <string name="chunks_uploaded">Komadi su otpremljeni</string> + <string name="upload_errors">Greške u otpremanju</string> + <string name="network_smp_proxy_mode_always_description">Uvek koristiti privatno usmeravanje.</string> + <string name="markdown_in_messages">Markdown u porukama</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">Kada je omogućeno više od jednog operatera, nijedan od njih nema metapodatke da nauči ko komunicira sa kim.</string> + <string name="allow_your_contacts_to_call">Dozvoliti vašim kontaktima da vas zovu.</string> + <string name="servers_info_subscriptions_section_header">Prijem poruke</string> + <string name="migrate_from_device_error_exporting_archive">Greška pri izvoženju baze podataka razgovora</string> + <string name="you_can_make_address_visible_via_settings">Možete učiniti vidljivim vašim SimpleX kontaktima putem Podešavanja.</string> + <string name="error_sending_message_contact_invitation">Greška pri slanju pozivnice</string> + <string name="skip_inviting_button">Preskočiti pozivanje članova</string> + <string name="operator_use_for_messages_private_routing">Za privatno usmeravanje</string> + <string name="color_secondary_variant">Dodatni sekundarni</string> + <string name="servers_info_starting_from">Pokretanje iz %s.</string> + <string name="your_ice_servers">Vaši ICE serveri</string> + <string name="use_random_passphrase">Koristiti nasumičnu pristupnu frazu</string> + <string name="auth_disable_simplex_lock">Onemogućiti SimpleX Zaključavanje</string> + <string name="servers_info_reconnect_server_title">Ponovo povezati server?</string> + <string name="no_received_app_files">Nema primljenih niti poslanih datoteka</string> + <string name="integrity_msg_bad_hash">neispravan hash poruke</string> + <string name="alert_title_msg_bad_id">Neispravan ID poruke</string> + <string name="servers_info_reconnect_servers_title">Ponovo povezati servere?</string> + <string name="notification_preview_mode_message_desc">Prikazati kontakt i poruku</string> + <string name="show_developer_options">Prikazati opcije za programere</string> + <string name="alert_title_msg_bad_hash">Neispravan hash poruke</string> + <string name="servers_info_reconnect_server_message">Ponovo povezati server kako biste prisilili dostavu poruke. To koristi dodatni saobraćaj.</string> + <string name="integrity_msg_bad_id">neispravan ID poruke</string> + <string name="xftp_servers_configured">Konfigurisani XFTP serveri</string> + <string name="message_delivery_warning_title">Upozorenje pri isporuci poruke</string> + <string name="connect_via_link_or_qr">Povezati se pomoću linka / QR koda</string> + <string name="failed_to_create_user_duplicate_desc">Već imate chat profil sa istim prikaznim imenom. Molimo vas da odaberete drugo ime.</string> + <string name="la_auth_failed">Greška u autentifikaciji</string> + <string name="v4_3_improved_server_configuration">Unapređena konfiguracija servera</string> + <string name="n_other_file_errors">%1$d ostale greška datoteke(a).</string> + <string name="auth_unavailable">Autentifikacija nije dostupna</string> + <string name="smp_servers_configured">Konfigurisani SMP serveri</string> + <string name="auto_accept_contact">Automatsko prihvatanje</string> + <string name="save_preferences_question">Sačuvati preference?</string> + <string name="unable_to_open_browser_desc">Za pozive je potreban podrazumevani veb pretraživač. Molimo vas da konfigurišete podrazumevani pretraživač u sistemu i podelite više informacija sa programerima.</string> + <string name="snd_group_event_member_unblocked">odblokirali ste %s</string> + <string name="you_are_observer">Vi ste posmatrač.</string> + <string name="v4_3_improved_privacy_and_security">Unapređena privatnost i bezbednost</string> + <string name="v5_6_app_data_migration_descr">Migriraj na drugi uređaj pomoću QR koda.</string> + <string name="group_preview_rejected">odbijeno</string> + <string name="new_passphrase">Nova pristupna fraza…</string> + <string name="v4_2_auto_accept_contact_requests">Automatsko prihvatanje zahteva za kontakt</string> + <string name="only_your_contact_can_send_disappearing">Samo Vaši kontakti mogu slati poruke koje nestaju.</string> + <string name="only_your_contact_can_send_voice">Samo Vaši kontakti mogu slati glasovne poruke.</string> + <string name="chat_preferences_you_allow">Vi dozvoljavate</string> + <string name="open_database_folder">Otvoriti fasciklu baze podataka</string> + <string name="save_and_update_group_profile">Sačuvati i ažurirati grupni profil</string> + <string name="v5_8_message_delivery_descr">Uz smanjenu potrošnju baterije.</string> + <string name="connect_via_link_or_qr_from_clipboard_or_in_person">(skenirati ili nalepiti iz memorije)</string> + <string name="connection_error_auth">Greška u vezi (AUTH)</string> + <string name="contact_developers">Ažurirajte aplikaciju i kontaktirajte programere.</string> + <string name="non_fatal_errors_occured_during_import">Tokom uvoza došlo je do nekih nefatalnih grešaka:</string> + <string name="save_passphrase_in_settings">Sačuvati pristupnu frazu u podešavanjima</string> + <string name="you_sent_group_invitation">Poslali ste grupnu pozivnicu</string> + <string name="conn_event_enabled_pq">kvantno otporna e2e šifrovanju</string> + <string name="only_your_contact_can_make_calls">Samo Vaši kontakti mogu upućivati pozive.</string> + <string name="disable_sending_recent_history">Ne slati istoriju novim članovima.</string> + <string name="group_members_can_send_voice">Članovi mogu da šalju glasovne poruke.</string> + <string name="v5_5_message_delivery_descr">Uz smanjenu potrošnju baterije.</string> + <string name="migrate_to_device_error_downloading_archive">Greška pri preuzimanju arhive</string> + <string name="delete_chat_list_warning">Svi razgovori biće uklonjeni sa liste %s, a lista odstranjena</string> + <string name="this_QR_code_is_not_a_link">Ovaj QR kod nije link!</string> + <string name="connect_plan_already_connecting">Već se povezujete!</string> + <string name="migrate_from_device_to_another_device">Migriraj na drugi uređaj</string> + <string name="operator_open_changes">Otvoriti promene</string> + <string name="decentralized">Decentralizovano</string> + <string name="no_info_on_delivery">Nema informacija o prijem</string> + <string name="connect_via_link">Povezati se pomoću linka</string> + <string name="v5_2_fix_encryption">Zadržati Vaše konekcije</string> + <string name="migrate_to_device_title">Migriraj ovde</string> + <string name="connect_plan_connect_via_link">Povezati se pomoću linka?</string> + <string name="install_simplex_chat_for_terminal">Instaliraj SimpleX Chat za terminal</string> + <string name="network_smp_web_port_toggle">Koristiti web port</string> + <string name="only_you_can_make_calls">Samo Vi možete da upućujete pozive.</string> + <string name="app_check_for_updates_button_open">Otvoriti lokaciju datoteke</string> + <string name="group_member_status_rejected">odbijeno</string> + <string name="save_group_profile">Sačuvati grupni profil</string> + <string name="only_you_can_send_disappearing">Samo vi možete slati poruke koje nestaju.</string> + <string name="v4_4_verify_connection_security_desc">Uporedite sigurnosne kodove sa vašim kontaktima.</string> + <string name="auto_accept_images">Automatsko prihvatanje slika</string> + <string name="color_title">Naslov</string> + <string name="wrong_passphrase">Neispravna pristupna fraza baze podataka</string> + <string name="migrate_from_another_device">Migriraj sa drugog uređaja</string> + <string name="only_you_can_send_voice">Samo vi možete slati glasovne poruke</string> + <string name="only_your_contact_can_add_message_reactions">Samo vi možete stavljati reakcije na poruke.</string> +</resources> 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 21a56d7e99..c8897c4063 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -1,471 +1,2357 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d az üzenetek visszafejtése sikertelen</string> - <string name="alert_text_decryption_error_too_many_skipped">%1$d üzenetek kihagyva.</string> - <string name="integrity_msg_skipped">%1$d kihagyott üzenet(ek)</string> - <string name="group_info_section_title_num_members">%1$s TAGOK</string> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">Nem sikerült visszafejteni %1$d üzenetet.</string> + <string name="alert_text_decryption_error_too_many_skipped">%1$d üzenet kihagyva.</string> + <string name="integrity_msg_skipped">%1$d üzenet kihagyva</string> + <string name="group_info_section_title_num_members">%1$s TAG</string> <string name="chat_item_ttl_month">1 hónap</string> <string name="chat_item_ttl_week">1 hét</string> - <string name="v5_3_new_interface_languages">6 új kezelőfelület nyelv</string> + <string name="v5_3_new_interface_languages">6 új kezelőfelületi nyelv</string> <string name="send_disappearing_message_5_minutes">5 perc</string> <string name="send_disappearing_message_1_minute">1 perc</string> - <string name="learn_more_about_address">A SimpleX azonosítóról</string> - <string name="abort_switch_receiving_address_question">Címváltoztatás megszakítása?</string> + <string name="learn_more_about_address">A SimpleX-címről</string> + <string name="abort_switch_receiving_address_question">Megszakítja a cím módosítását?</string> <string name="abort_switch_receiving_address_confirm">Megszakítás</string> <string name="send_disappearing_message_30_seconds">30 másodperc</string> - <string name="one_time_link_short">Egyszer használatos link</string> - <string name="contact_wants_to_connect_via_call">%1$s szeretne kapcsolatba lépni veled</string> - <string name="about_simplex_chat">A SimpleX chatről</string> + <string name="one_time_link_short">Egyszer használható meghívó</string> + <string name="contact_wants_to_connect_via_call">%1$s szeretne kapcsolatba lépni Önnel a következőn keresztül:</string> + <string name="about_simplex_chat">A SimpleX Chat névjegye</string> <string name="chat_item_ttl_day">1 nap</string> - <string name="abort_switch_receiving_address">Címváltoztatás megszakítása</string> - <string name="about_simplex">A SimpleX-ről</string> + <string name="abort_switch_receiving_address">Cím módosításának megszakítása</string> + <string name="about_simplex">A SimpleXről</string> <string name="color_primary">Kiemelőszín</string> - <string name="callstatus_accepted">elfogadott hívás</string> - <string name="network_enable_socks_info">Kapcsolódás a szerverekhez SOCKS proxy segítségével a %d? porton? A proxyt el kell indítani mielőtt bekapcsolná ezt az opciót.</string> - <string name="accept_feature">Elfogad</string> - <string name="accept_call_on_lock_screen">Elfogad</string> - <string name="above_then_preposition_continuation">felül, majd pedig:</string> - <string name="accept_contact_incognito_button">Elfogadás inkognítóban</string> - <string name="accept_connection_request__question">Elfogadod a kapcsolatfelvételt?</string> - <string name="accept_contact_button">Elfogad</string> - <string name="accept">Elfogad</string> - <string name="add_address_to_your_profile">Add hozzá az azonosítót a profilodhoz, így a kapcsolataid megosztatják azt más emberekkel. A profilod változtatásai így frissítésre kerülnek a kapcsolataidnál is!</string> + <string name="callstatus_accepted">fogadott hívás</string> + <string name="network_enable_socks_info">Hozzáférés a kiszolgálókhoz SOCKS-proxyn a következő porton keresztül: %d? A proxyt el kell indítani, mielőtt engedélyezné ezt az opciót.</string> + <string name="accept_feature">Elfogadás</string> + <string name="accept_call_on_lock_screen">Elfogadás</string> + <string name="above_then_preposition_continuation">gombra fent, majd:</string> + <string name="accept_contact_incognito_button">Elfogadás inkognitóban</string> + <string name="accept_connection_request__question">Elfogadja a meghívási kérést?</string> + <string name="accept_contact_button">Elfogadás</string> + <string name="accept">Elfogadás</string> + <string name="add_address_to_your_profile">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.</string> <string name="color_primary_variant">További kiemelőszín</string> - <string name="callstatus_error">hiba a hívásban</string> - <string name="v5_4_block_group_members">Csoporttagok blokkolása</string> + <string name="callstatus_error">híváshiba</string> + <string name="v5_4_block_group_members">Csoporttagok letiltása</string> <string name="la_authenticate">Hitelesítés</string> - <string name="empty_chat_profile_is_created">Egy üres chat profil létre lett hozva a megadott névvel és az app normál módon megnyílik.</string> - <string name="feature_cancelled_item">megszakítva %s</string> - <string name="smp_servers_preset_add">Adj hozzá egyedi szervereket</string> - <string name="calls_prohibited_with_this_contact">Hang-/videóhívások megtiltva.</string> - <string name="network_session_mode_entity_description">Külön TCP kapcsolat (és SOCKS bejelentkezési adatok) lesznek használva <b>minden ismerősre és csoport tagra</b> -\n<b>Tudnivaló</b>: ha sok ismerősöd van, az akkumulátor- és adat használatod jelentősen megnőhet és néhány kapcsolódási kísérlet sikertelen lehet.</string> - <string name="icon_descr_cancel_link_preview">URL link előnézet megszakítása</string> - <string name="network_session_mode_user_description"><![CDATA[Külön TCP kapcsolat (és SOCKS bejelentkezési adatok) lesznek használva <b>minden chat profilodra az appban</b>.]]></string> - <string name="both_you_and_your_contact_can_send_disappearing">Mindketten, te és az ismerősöd is küldhettek eltűnő üzeneteket.</string> - <string name="keychain_is_storing_securely">Az Android Keystore-t 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.</string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>QR-kód beolvasása</b>: kapcsolódás ismerőshöz a megmutatott QR-kódja alapján]]></string> - <string name="alert_title_msg_bad_hash">Téves üzenet hash</string> - <string name="cant_delete_user_profile">Felhasználói profil törlése nem lehetséges!</string> + <string name="empty_chat_profile_is_created">Egy üres csevegési profil lesz létrehozva a megadott névvel, és az alkalmazás a szokásos módon megnyílik.</string> + <string name="feature_cancelled_item">%s visszavonva</string> + <string name="smp_servers_preset_add">Előre beállított kiszolgálók hozzáadása</string> + <string name="calls_prohibited_with_this_contact">A hívások kezdeményezése le van tiltva ebben a csevegésben.</string> + <string name="network_session_mode_entity_description"><![CDATA[<b>Az összes partneréhez és csoporttaghoz</b> külön TCP-kapcsolat (és SOCKS-hitelesítőadat) lesz használva.\n<b>Megjegyzés:</b> ha sok kapcsolata van, az akkumulátor-használat és az adatforgalom jelentősen megnövekedhet, és néhány kapcsolódási kísérlet sikertelen lehet.]]></string> + <string name="icon_descr_cancel_link_preview">hivatkozás előnézetének visszavonása</string> + <string name="network_session_mode_user_description"><![CDATA[<b>Az összes csevegési profiljához az alkalmazásban</b> külön TCP-kapcsolat (és SOCKS-hitelesítőadat) lesz használva.]]></string> + <string name="both_you_and_your_contact_can_send_disappearing">Mindkét fél küldhet eltűnő üzeneteket.</string> + <string name="keychain_is_storing_securely">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.</string> + <string name="alert_title_msg_bad_hash">Érvénytelen az üzenet hasítóértéke</string> <string name="color_background">Háttér</string> - <string name="socks_proxy_setting_limitations"><![CDATA[<b>Tudnivaló</b>: az üzenet- és fájl relay szerverek SOCKS proxy által vannak kapcsolatban. A hívások és URL link előnézetek közvetlen kapcsolatot használnak.]]></string> - <string name="full_backup">App adatmentés</string> - <string name="database_initialization_error_title">Adatbázis inicializálása nem lehetséges</string> - <string name="all_your_contacts_will_remain_connected_update_sent">A kapcsolat megmarad az összes Ismerősöddel. Profil változtatások frissítésre kerülnek az ismerőseidnél.</string> - <string name="v4_5_transport_isolation_descr">Chat profile (alap beállítás) avagy kapcsolat által (BÉTA).</string> - <string name="connect__a_new_random_profile_will_be_shared">Egy új véletlenszerű profil lesz megosztva.</string> - <string name="allow_voice_messages_only_if">Hangüzenetek küldésének engedélyezése kizárólag az esetre ha a másik fél is engedélyezi.</string> - <string name="app_version_code">App build: %s</string> - <string name="audio_video_calls">Hang-/videóhívások</string> - <string name="network_settings">Haladó hálózati beállítások</string> - <string name="allow_your_contacts_to_send_voice_messages">Ismerősök küldhetnek hangüzeneteket engedélyezése.</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Megjegyzés:</b> 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 elküldése közvetlen kapcsolatot használnak.]]></string> + <string name="full_backup">Alkalmazásadatok biztonsági mentése</string> + <string name="database_initialization_error_title">Az adatbázis előkészítése sikertelen</string> + <string name="all_your_contacts_will_remain_connected_update_sent">A partnereivel kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára.</string> + <string name="v4_5_transport_isolation_descr">A csevegési profillal (alapértelmezett), vagy a kapcsolattal (BÉTA).</string> + <string name="connect__a_new_random_profile_will_be_shared">Egy új, véletlenszerű profil lesz megosztva.</string> + <string name="allow_voice_messages_only_if">A hangüzenetek küldése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi.</string> + <string name="app_version_code">Az alkalmazás összeállítási száma: %s</string> + <string name="audio_video_calls">Hang- és videóhívások</string> + <string name="network_settings">Speciális hálózati beállítások</string> + <string name="allow_your_contacts_to_send_voice_messages">A hangüzenetek küldése engedélyezve van a partnerei számára.</string> <string name="settings_audio_video_calls">Hang- és videóhívások</string> - <string name="v5_3_encrypt_local_files_descr">Az app titkosítja a helyi fájlokat (a videók kivételével).</string> + <string name="v5_3_encrypt_local_files_descr">Az alkalmazás titkosítja a helyi fájlokat (a videók kivételével).</string> <string name="answer_call">Hívás fogadása</string> - <string name="allow_your_contacts_to_send_disappearing_messages">Ismerősök küldhetnek eltűnő üzeneteket engedélyezve.</string> - <string name="connect_plan_already_connecting">Már kapcsolódik!</string> - <string name="cannot_receive_file">Fájl fogadás nem lehetséges</string> + <string name="allow_your_contacts_to_send_disappearing_messages">Az eltűnő üzenetek küldésének engedélyezése a partnerei számára.</string> + <string name="connect_plan_already_connecting">Kapcsolódás folyamatban!</string> + <string name="cannot_receive_file">Nem lehet fogadni a fájlt</string> <string name="auth_unavailable">Hitelesítés elérhetetlen</string> - <string name="app_version_title">App verzió</string> - <string name="button_add_welcome_message">Üdvözlőszöveg hozzáadása</string> - <string name="snd_conn_event_ratchet_sync_started">titkosítás egyeztetése %s számára…</string> - <string name="available_in_v51">" -\nElérhető a v5.1-ben"</string> - <string name="both_you_and_your_contacts_can_delete">Mindketten, te és az ismerősöd is visszaállíthatatlanul törölhettek elküldött üzeneteket.</string> - <string name="v5_4_better_groups">Jobb csoportok</string> - <string name="clear_chat_warning">Minden üzenet törlésre kerül - ez visszafordíthatatlan! Az üzenetek csak NÁLAD törlődnek.</string> + <string name="app_version_title">Az alkalmazás verziója</string> + <string name="button_add_welcome_message">Üdvözlőüzenet hozzáadása</string> + <string name="snd_conn_event_ratchet_sync_started">titkosítás elfogadása %s számára…</string> + <string name="available_in_v51">\nElérhető az v5.1-es kiadásban</string> + <string name="both_you_and_your_contacts_can_delete">Mindkét fél véglegesen törölheti az elküldött üzeneteket. (24 óra)</string> + <string name="v5_4_better_groups">Továbbfejlesztett csoportok</string> + <string name="clear_chat_warning">Az összes üzenet törölve lesz – ez a művelet nem vonható vissza! Az üzenetek CSAK az Ön számára törlődnek.</string> <string name="icon_descr_call_ended">Hívás befejeződött</string> <string name="settings_section_title_calls">HÍVÁSOK</string> - <string name="rcv_group_and_other_events">és %d egyéb események</string> - <string name="address_section_title">Azonosító</string> - <string name="connect_plan_already_joining_the_group">Már csatlakozik a csoporthoz!</string> + <string name="rcv_group_and_other_events">és további %d esemény</string> + <string name="address_section_title">Cím</string> + <string name="connect_plan_already_joining_the_group">A csatlakozás folyamatban van a csoporthoz!</string> <string name="auto_accept_contact">Automatikus elfogadás</string> - <string name="notifications_mode_service_desc">Háttérszolgáltatás mindig fut - az értesítések azonnal megjelennek, amint üzenetek vannak.</string> - <string name="allow_to_delete_messages">Elküldött üzenetek visszafordíthatatlan törlésének engedélyezése.</string> - <string name="both_you_and_your_contact_can_send_voice">Mindketten, te és az ismerősöd is küldhettek hangüzeneteket.</string> + <string name="notifications_mode_service_desc">A háttérszolgáltatás mindig fut – az értesítések megjelennek, amint az üzenetek elérhetővé válnak.</string> + <string name="allow_to_delete_messages">Az elküldött üzenetek végleges törlése engedélyezve van. (24 óra)</string> + <string name="both_you_and_your_contact_can_send_voice">Mindkét fél küldhet hangüzeneteket.</string> <string name="alert_title_msg_bad_id">Téves üzenet ID</string> - <string name="allow_your_contacts_adding_message_reactions">Üzenet rakciók -emojik- engedélyezése az ismerősei számára.</string> - <string name="allow_to_send_voice">Hangüzenetek küldésének engedélyezése.</string> - <string name="allow_message_reactions_only_if">Üzenet reakciók -emojik- engedélyezése kizárólag az esetre ha a másik fél is engedélyezi.</string> + <string name="allow_your_contacts_adding_message_reactions">A reakciók hozzáadása az üzenetekhez engedélyezve van a partnerei számára.</string> + <string name="allow_to_send_voice">A hangüzenetek küldése engedélyezve van.</string> + <string name="allow_message_reactions_only_if">A reakciók hozzáadása az üzenetekhez csak abban az esetben van engedélyezve, ha a partnere is engedélyezi.</string> <string name="back">Vissza</string> - <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Kikapcsolható a beállításokban</b> – az értesítések továbbra is megjelenítésre kerülnek amíg az app fut.]]></string> - <string name="v4_2_group_links_desc">Adminok létrehozhatnak linkeket csoporthoz való csatlakozáshoz.</string> - <string name="call_on_lock_screen">Hívások a lezárási képernyőn:</string> - <string name="conn_event_ratchet_sync_started">titkosítás egyeztetése…</string> - <string name="invite_prohibited">Ismerős meghívása sikertelen!</string> + <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Kikapcsolható a beállításokban</b> – az értesítések továbbra is meg lesznek jelenítve amíg az alkalmazás fut.]]></string> + <string name="v4_2_group_links_desc">Az adminisztrátorok hivatkozásokat hozhatnak létre a csoportokhoz való csatlakozáshoz.</string> + <string name="call_on_lock_screen">Hívások a zárolási képernyőn:</string> + <string name="conn_event_ratchet_sync_started">titkosítás elfogadása…</string> + <string name="invite_prohibited">Nem lehet meghívni a partnert!</string> <string name="integrity_msg_bad_id">téves üzenet ID</string> - <string name="v4_2_auto_accept_contact_requests">Ismerősnek jelölések automatikus elfogadása</string> - <string name="impossible_to_recover_passphrase"><![CDATA[<b>Tudnivaló</b>: NEM fogod tudni helyreállítani vagy megváltoztatni a jelmondatot az esetben ha elveszíted.]]></string> + <string name="v4_2_auto_accept_contact_requests">Meghívási kérések automatikus elfogadása</string> + <string name="impossible_to_recover_passphrase"><![CDATA[<b>Megjegyzés:</b> NEM fogja tudni helyreállítani, vagy módosítani a jelmondatot abban az esetben, ha elveszíti.]]></string> <string name="callstatus_calling">hívás…</string> - <string name="color_secondary_variant">További másodlagos</string> - <string name="smp_servers_add_to_another_device">Hozzáadás másik eszközhöz</string> - <string name="allow_message_reactions">Üzenet reakciók -emojik- engedélyezése.</string> - <string name="icon_descr_cancel_file_preview">Fájl előnézet megszakítása</string> - <string name="all_group_members_will_remain_connected">Minden csoporttag kapcsolatban marad.</string> - <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Több akkumulátort használ</b>! Háttérszolgáltatás mindig fut - értesítések megjelennek azonnal, ahogy új üzenetek érkeznek.]]></string> - <string name="block_member_confirmation">Blokkolás</string> - <string name="group_member_role_admin">admin</string> - <string name="icon_descr_cancel_image_preview">Fénykép előnézet megszakítása</string> - <string name="v5_1_self_destruct_passcode_descr">Minden adat törlődik amint bevitelre kerül.</string> - <string name="icon_descr_video_asked_to_receive">Kérte a videó elfogadását</string> - <string name="block_member_button">Tag blokkolása</string> + <string name="color_secondary_variant">További másodlagos szín</string> + <string name="smp_servers_add_to_another_device">Hozzáadás egy másik eszközhöz</string> + <string name="allow_message_reactions">A reakciók hozzáadása az üzenetekhez engedélyezve van.</string> + <string name="icon_descr_cancel_file_preview">Fájlelőnézet visszavonása</string> + <string name="all_group_members_will_remain_connected">Az összes csoporttag kapcsolatban marad.</string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Több akkumulátort használ</b>! Az alkalmazás mindig fut a háttérben – az értesítések azonnal megjelennek.]]></string> + <string name="block_member_confirmation">Letiltás</string> + <string name="group_member_role_admin">adminisztrátor</string> + <string name="icon_descr_cancel_image_preview">Képelőnézet visszavonása</string> + <string name="v5_1_self_destruct_passcode_descr">A jelkód megadása után az összes adat törölve lesz.</string> + <string name="icon_descr_video_asked_to_receive">Felkérték a videó fogadására</string> + <string name="block_member_button">Letiltás</string> <string name="v5_2_more_things">Néhány további dolog</string> - <string name="authentication_cancelled">Hitelesítés megszakítva</string> - <string name="allow_to_send_files">Fájlok és fotók/videók küldésének engedélyezése.</string> - <string name="users_delete_all_chats_deleted">Minden chat illetve az összes üzenet törlésre kerül - ez visszafordíthatatlan!</string> + <string name="authentication_cancelled">Hitelesítés visszavonva</string> + <string name="allow_to_send_files">A fájlok- és a médiatartalmak küldése engedélyezve van.</string> + <string name="users_delete_all_chats_deleted">Az összes csevegés és üzenet törölve lesz – ez a művelet nem vonható vissza!</string> <string name="icon_descr_audio_call">hanghívás</string> - <string name="bold_text">vastagított</string> - <string name="app_passcode_replaced_with_self_destruct">Az app számkód helyettesítésre kerül egy önmegsemmisítő számkóddal.</string> - <string name="v5_3_new_interface_languages_descr">Arab, bulgár, finn, héber, thai és ukrán - köszönet a felhasználóknak és a Weblate-nek!</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Új ismerős hozzáadása</b>: egyszer használatos QR-kód készítése az ismerős számára.]]></string> - <string name="allow_voice_messages_question">Hangüzenetek engedélyezése?</string> - <string name="always_use_relay">Mindig használt relay szervert</string> + <string name="bold_text">félkövér</string> + <string name="app_passcode_replaced_with_self_destruct">Az alkalmazás-jelkód helyettesítve lesz egy önmegsemmisítő-jelkóddal.</string> + <string name="v5_3_new_interface_languages_descr">Arab, bolgár, finn, héber, thai és ukrán – köszönet a felhasználóknak és a Weblate-nek.</string> + <string name="allow_voice_messages_question">Engedélyezi a hangüzeneteket?</string> + <string name="always_use_relay">Mindig használjon továbbítókiszolgálót</string> <string name="chat_preferences_always">mindig</string> <string name="call_already_ended">A hívás már befejeződött!</string> - <string name="turn_off_battery_optimization_button">Engedélyez</string> - <string name="all_your_contacts_will_remain_connected">A kapcsolat megmarad az összes Ismerősöddel.</string> - <string name="icon_descr_cancel_live_message">Élő chat üzenet megszakítása</string> - <string name="allow_irreversible_message_deletion_only_if">Helyreállíthatatlan üzenet törlés engedélyezése kizárólag az esetre ha a másik fél is engedélyezi.</string> + <string name="turn_off_battery_optimization_button">Engedélyezés</string> + <string name="all_your_contacts_will_remain_connected">Az összes partnerével kapcsolatban marad.</string> + <string name="icon_descr_cancel_live_message">Élő csevegési üzenet visszavonása</string> + <string name="allow_irreversible_message_deletion_only_if">Az üzenetek végleges törlése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. (24 óra)</string> <string name="v4_6_audio_video_calls">Hang- és videóhívások</string> - <string name="integrity_msg_bad_hash">téves üzenet hash</string> - <string name="notifications_mode_service">Mindig bekapcsolva</string> - <string name="keychain_allows_to_receive_ntfs">Az Android Keystore fogja biztonságosan tárolni a jelmondatot app újraindítás vagy jelmondat változtatás után - lehetővé téve az értesítések fogadását.</string> - <string name="all_app_data_will_be_cleared">Minden app adat törölve.</string> - <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Legjobb akkumulátoridő</b>. Kizárólag akkor kapsz értesítéseket amikor fut az app (NINCS háttérszolgáltatás).]]></string> + <string name="integrity_msg_bad_hash">érvénytelen az üzenet hasítóértéke</string> + <string name="notifications_mode_service">Mindig fut</string> + <string name="keychain_allows_to_receive_ntfs">Az Android Keystore biztonságosan fogja tárolni a jelmondatot az alkalmazás újraindítása, vagy a jelmondat módosítása után – lehetővé teszi az értesítések fogadását.</string> + <string name="all_app_data_will_be_cleared">Az összes alkalmazásadat törölve.</string> + <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Legjobb akkumulátoridő</b>. Csak akkor kap értesítéseket, amikor az alkalmazás meg van nyitva. (NINCS háttérszolgáltatás.)]]></string> <string name="appearance_settings">Megjelenés</string> - <string name="turning_off_service_and_periodic">Akkumulátor optimizáció aktív, a háttérszolgáltatás és a rendszeres új üzenet ellenőrzés kikapcsolva. Újra bekapcsolhatod ezeket a beállításokban.</string> - <string name="block_member_question">Tag blokkolása?</string> - <string name="callstatus_ended">hívás befejeződött %1$s</string> - <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Jó akkumulátoridő</b>. A háttérszolgáltatás ellenőrzi az új üzeneteket 10 percenként. Hívásokról és fontos üzenetekről maradhatsz le.]]></string> + <string name="turning_off_service_and_periodic">Az akkumulátor-optimalizálás aktív, ez kikapcsolja a háttérszolgáltatást és az új üzenetek időszakos lekérdezését. Ezt a beállításokban újraengedélyezheti.</string> + <string name="block_member_question">Letiltja a tagot?</string> + <string name="callstatus_ended">%1$s hívása befejeződött</string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Jó akkumulátoridő</b>. Az alkalmazás 10 percenként ellenőrzi az új üzeneteket. Előfordulhat, hogy hívásokról, vagy a sürgős üzenetekről marad le.]]></string> <string name="group_member_role_author">szerző</string> - <string name="allow_your_contacts_irreversibly_delete">Ismerősök visszafordíthatatlanul törölhetnek elküldött üzeneteket engedélyezve.</string> - <string name="cancel_verb">Megszakítás</string> - <string name="notifications_mode_off_desc">Az app csak akkor tud értesítéseket fogadni amikor fut, héttérszolgáltatás nem kerül elindításra.</string> - <string name="v5_1_better_messages">Jobb üzenetek</string> - <string name="abort_switch_receiving_address_desc">A cím változtatás megszakításra kerül. A régi fogadó cím marad használatban.</string> - <string name="allow_verb">Engedélyez</string> - <string name="bad_desktop_address">Rossz asztal cím</string> - <string name="users_add">Adj hozzá profilt</string> - <string name="attach">Csatolás</string> - <string name="v5_0_app_passcode">App számkód</string> - <string name="icon_descr_asked_to_receive">Kérte, hogy fogfaja a képet</string> - <string name="use_camera_button">Fényképező</string> - <string name="cannot_access_keychain">A Keystore-hoz nem sikerül hozzáférni az adatbázis jelszó elmentése végett</string> + <string name="allow_your_contacts_irreversibly_delete">Az elküldött üzenetek végleges törlése engedélyezve van a partnerei számára. (24 óra)</string> + <string name="cancel_verb">Mégse</string> + <string name="notifications_mode_off_desc">Az alkalmazás csak akkor tud értesítéseket fogadni, amikor meg van nyitva. A háttérszolgáltatás nem fog elindulni</string> + <string name="v5_1_better_messages">Továbbfejlesztett üzenetek</string> + <string name="abort_switch_receiving_address_desc">A cím módosítása meg fog szakadni. A régi fogadási cím lesz használva.</string> + <string name="allow_verb">Engedélyezés</string> + <string name="bad_desktop_address">Érvénytelen számítógépcím</string> + <string name="users_add">Profil hozzáadása</string> + <string name="attach">Mellékelés</string> + <string name="v5_0_app_passcode">Alkalmazás jelkód</string> + <string name="icon_descr_asked_to_receive">Felkérték a kép fogadására</string> + <string name="use_camera_button">Kamera</string> + <string name="cannot_access_keychain">Nem érhető el a Keystore az adatbázis jelszavának mentéséhez</string> <string name="callstatus_in_progress">hívás folyamatban</string> - <string name="auto_accept_images">Fényképek automatiklus elfogadása</string> - <string name="allow_your_contacts_to_call">Hang- és videóhívás engedélyezése az ismerősei számára.</string> - <string name="settings_section_title_icon">APP IKON</string> - <string name="v4_3_improved_server_configuration_desc">Szerver hozzáadása QR kód befotózásával.</string> - <string name="allow_to_send_disappearing">Eltünő üzenetek küldésének engedélyezése.</string> - <string name="allow_disappearing_messages_only_if">Eltűnő üzenetek engedélyezése kizárólag az esetre ha a másik fél is engedélyezi.</string> + <string name="auto_accept_images">Képek automatikus elfogadása</string> + <string name="allow_your_contacts_to_call">A hívások kezdeményezése engedélyezve van a partnerei számára.</string> + <string name="settings_section_title_icon">ALKALMAZÁSIKON</string> + <string name="v4_3_improved_server_configuration_desc">Kiszolgáló hozzáadása QR-kód beolvasásával.</string> + <string name="allow_to_send_disappearing">Az eltűnő üzenetek küldése engedélyezve van.</string> + <string name="allow_disappearing_messages_only_if">Az eltűnő üzenetek küldése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi.</string> <string name="icon_descr_audio_off">Hang kikapcsolva</string> - <string name="allow_direct_messages">Közvetlen üzenetküldés tagok számára engedélyezett.</string> - <string name="settings_section_title_app">APP</string> + <string name="allow_direct_messages">A közvetlen üzenetek küldése a tagok között engedélyezve van.</string> + <string name="settings_section_title_app">ALKALMAZÁS</string> <string name="icon_descr_call_progress">Hívás folyamatban</string> - <string name="both_you_and_your_contact_can_add_message_reactions">Mindketten, te és az ismerősöd is használhattok üzenet reakciókat (emojik).</string> - <string name="both_you_and_your_contact_can_make_calls">Mindketten, te és az ismerősöd is tudok hívásokat indítani.</string> - <string name="la_auth_failed">Hitelesítés sikertelen</string> - <string name="block_member_desc">Minden új üzenet %s -tól/től elrejtésre kerül.</string> - <string name="app_version_name">App verzió: v%s</string> - <string name="allow_calls_only_if">Hívások engedélyezése kizárólag az esetre ha a másik fél is engedélyezi.</string> - <string name="smp_servers_add">Szerver hozzáadása…</string> + <string name="both_you_and_your_contact_can_add_message_reactions">Mindkét fél hozzáadhat az üzenetekhez reakciókat.</string> + <string name="both_you_and_your_contact_can_make_calls">Mindkét fél tud hívásokat kezdeményezni.</string> + <string name="la_auth_failed">Sikertelen hitelesítés</string> + <string name="block_member_desc">%s összes új üzenete el lesz rejtve!</string> + <string name="app_version_name">Az alkalmazás verziója: v%s</string> + <string name="allow_calls_only_if">A hívások kezdeményezése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi.</string> + <string name="smp_servers_add">Kiszolgáló hozzáadása</string> <string name="icon_descr_audio_on">Hang bekapcsolva</string> <string name="audio_call_no_encryption">hanghívás (nem e2e titkosított)</string> - <string name="blocked_item_description">blokkolva</string> - <string name="change_database_passphrase_question">Adatbázis jelmondat megváltoztatása?</string> + <string name="blocked_item_description">letiltva</string> + <string name="change_database_passphrase_question">Módosítja az adatbázis jelmondatát?</string> <string name="callstate_connected">kapcsolódva</string> - <string name="la_change_app_passcode">Számkód megváltoztatása</string> - <string name="rcv_group_event_changed_member_role">%s to %s megváltozott szerepköre</string> - <string name="switch_receiving_address">Fogadó szerver cím megváltoztatása</string> - <string name="change_verb">Változtatás</string> - <string name="confirm_passcode">Számkód megerősítése</string> + <string name="la_change_app_passcode">Jelkód módosítása</string> + <string name="rcv_group_event_changed_member_role">a következőre módosította %s szerepkörét: „%s”</string> + <string name="switch_receiving_address">Fogadási cím módosítása</string> + <string name="change_verb">Módosítás</string> + <string name="confirm_passcode">Jelkód megerősítése</string> <string name="confirm_password">Jelszó megerősítése</string> - <string name="change_member_role_question">Csoport szerepkör megváltoztatása?</string> - <string name="change_lock_mode">Lezárási mód megváltoztatása</string> - <string name="notification_contact_connected">Kapcsolódva</string> - <string name="rcv_group_event_member_connected">kapcsolódva</string> + <string name="change_member_role_question">Módosítja a tag szerepkörét?</string> + <string name="change_lock_mode">Zárolási mód módosítása</string> + <string name="notification_contact_connected">Kapcsolódott</string> + <string name="rcv_group_event_member_connected">kapcsolódott</string> <string name="connect_via_link_verb">Kapcsolódás</string> - <string name="group_member_status_connected">kapcsolódva</string> - <string name="connected_mobile">Összekapcsolt telefon</string> + <string name="group_member_status_connected">kapcsolódott</string> + <string name="connected_mobile">Társított hordozható eszköz</string> <string name="server_connected">kapcsolódva</string> - <string name="change_role">Szerepkör megváltoztatása</string> + <string name="change_role">Szerepkör módosítása</string> <string name="icon_descr_server_status_connected">Kapcsolódva</string> - <string name="auth_confirm_credential">Belépési adatok megerősítése</string> - <string name="switch_receiving_address_question">Fogadó szerver cím megváltoztatása</string> - <string name="rcv_conn_event_switch_queue_phase_completed">megváltozott azonosító számodra</string> - <string name="change_self_destruct_mode">Önmegsemmisítő mód megváltoztatása</string> - <string name="rcv_group_event_changed_your_role">a szerepköröd megváltoztatva %s-ra(-re)</string> + <string name="auth_confirm_credential">Hitelesítőadatok megerősítése</string> + <string name="switch_receiving_address_question">Módosítja a fogadási címet?</string> + <string name="rcv_conn_event_switch_queue_phase_completed">módosította a címet az Ön számára</string> + <string name="change_self_destruct_mode">Önmegsemmisítő-mód módosítása</string> + <string name="rcv_group_event_changed_your_role">a következőre módosította az Ön szerepkörét: „%s”</string> <string name="connect_button">Kapcsolódás</string> - <string name="connect_via_member_address_alert_title">Kapcsolódás közvetlenül?</string> + <string name="connect_via_member_address_alert_title">Közvetlenül kapcsolódik?</string> <string name="smp_server_test_connect">Kapcsolódás</string> - <string name="rcv_group_event_member_created_contact">közvetlenül kapcsolódva</string> + <string name="rcv_group_event_member_created_contact">közvetlenül kapcsolódott</string> <string name="connection_local_display_name">kapcsolat %1$d</string> - <string name="status_contact_has_e2e_encryption">az ismerősnél az e2e titkosítás elérhető</string> - <string name="v5_4_incognito_groups_descr">Csoport létrehozása véletlenszerűen létrehozott profillal.</string> - <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Az ismerős és az összes üzenet törlésre kerül - ez visszafordíthatatlan!</string> - <string name="contacts_can_mark_messages_for_deletion">Ismerősök megjelölhetik az üzeneteket törlendőként; de láthatod azokat.</string> - <string name="connect_via_invitation_link">Kapcsolódás egy Egyszer használatos linkkel?</string> - <string name="connect_via_link_or_qr">Kapcsolódás egy link / QR-kód által</string> + <string name="status_contact_has_e2e_encryption">a partner e2e titkosítással rendelkezik</string> + <string name="v5_4_incognito_groups_descr">Csoport létrehozása véletlenszerű profillal.</string> + <string name="delete_contact_all_messages_deleted_cannot_undo_warning">A partner és az összes üzenet törölve lesz – ez a művelet nem vonható vissza!</string> + <string name="contacts_can_mark_messages_for_deletion">A partnerei törlésre jelölhetnek üzeneteket; Ön majd meg tudja nézni azokat.</string> + <string name="connect_via_invitation_link">Kapcsolódik az egyszer használható meghívóval?</string> + <string name="connect_via_link_or_qr">Kapcsolódás egy hivatkozáson vagy QR-kódon keresztül</string> <string name="connection_error_auth">Kapcsolódási hiba (AUTH)</string> - <string name="notification_preview_mode_contact">Ismerős neve</string> - <string name="connect_via_contact_link">Kapcsolódás ismerős azonosítója által?</string> - <string name="create_address">Azonosító létrehozása</string> + <string name="notification_preview_mode_contact">Csak név</string> + <string name="connect_via_contact_link">Kapcsolódik a kapcsolattartási címen keresztül?</string> + <string name="create_address">Cím létrehozása</string> <string name="copy_verb">Másolás</string> <string name="continue_to_next_step">Folytatás</string> - <string name="connect_plan_connect_via_link">Kapcsolódás egy linken keresztül?</string> - <string name="contact_already_exists">Az ismerős már létezik</string> + <string name="connect_plan_connect_via_link">Kapcsolódik egy hivatkozáson keresztül?</string> + <string name="contact_already_exists">A partner már létezik</string> <string name="core_version">Fő verzió: v%s</string> - <string name="icon_descr_contact_checked">Ismerős ellenőrizve</string> - <string name="connect_plan_connect_to_yourself">Kapcsolódás saját magához?</string> + <string name="icon_descr_contact_checked">Partner ellenőrizve</string> + <string name="connect_plan_connect_to_yourself">Kapcsolódik saját magához?</string> <string name="copied">Kimásolva a vágólapra</string> - <string name="connection_request_sent">Kapcsolódási kérés elküldve!</string> - <string name="connecting_to_desktop">Kapcsolódás az asztalhoz</string> + <string name="connection_request_sent">Meghívási kérés elküldve!</string> + <string name="connecting_to_desktop">Kapcsolódás a számítógéphez</string> <string name="network_session_mode_entity">Kapcsolat</string> - <string name="correct_name_to">Név helyesbítése erre: %s?</string> - <string name="connection_timeout">Kapcsolat időtúllépés</string> - <string name="connect_with_contact_name_question">Kapcsolódás %1$s által?</string> + <string name="correct_name_to">Helyesbíti a nevet a következőre: %s?</string> + <string name="connection_timeout">Időtúllépés kapcsolódáskor</string> + <string name="connect_with_contact_name_question">Kapcsolódik vele: %1$s?</string> <string name="create_profile_button">Létrehozás</string> - <string name="contact_preferences">Ismerős beállításai</string> + <string name="contact_preferences">Partnerbeállítások</string> <string name="info_row_connection">Kapcsolat</string> <string name="desktop_connection_terminated">Kapcsolat megszakítva</string> - <string name="display_name_connection_established">Kapcsolat létrehozva</string> - <string name="status_contact_has_no_e2e_encryption">az ismerősnél az e2e titkosítás nem elérhető</string> - <string name="chat_preferences_contact_allows">Az ismerős engedélyezi</string> - <string name="notification_preview_somebody">Ismerős elrejtve:</string> - <string name="connect_to_desktop">Kapcsolódás az asztalhoz</string> - <string name="icon_descr_context">Kontextus ikon</string> - <string name="connect_via_link">Kapcsolódás egy linken keresztül</string> - <string name="receipts_section_contacts">Ismerősök</string> + <string name="display_name_connection_established">kapcsolat létrehozva</string> + <string name="status_contact_has_no_e2e_encryption">a partner nem rendelkezik e2e titkosítással</string> + <string name="chat_preferences_contact_allows">Partner engedélyezi</string> + <string name="notification_preview_somebody">Rejtett név:</string> + <string name="connect_to_desktop">Társítás számítógéppel</string> + <string name="icon_descr_context">Szövegkörnyezeti ikon</string> + <string name="connect_via_link">Kapcsolódás egy hivatkozáson keresztül</string> + <string name="receipts_section_contacts">Partnerek</string> <string name="connection_error">Kapcsolódási hiba</string> - <string name="alert_title_contact_connection_pending">Az ismerős még nem kapcsolódott!</string> - <string name="v5_3_discover_join_groups_descr">- kapcsolódás könyvtár szolgáltatáshoz (BÉTA)! -\n- kézbesítési igazolások (20 tagig). -\n- gyorsabb és stabilabb</string> - <string name="contribute">Hozzájárulás</string> - <string name="group_member_status_intro_invitation">kapcsolódás (meghívás bemutatkozásra)</string> - <string name="create_simplex_address">SimpleX azonosító létrehozása</string> - <string name="rcv_direct_event_contact_deleted">törölt ismerős</string> - <string name="delete_member_message__question">Tag üzenetének törlése?</string> - <string name="chat_is_running">A chat szolgáltatás működik (fut)</string> - <string name="share_one_time_link">Egyszer használatos meghívó link létrehozása</string> - <string name="delete_link">Link törlése</string> - <string name="notifications_mode_periodic_desc">Új üzenetek ellenőrzése 10 percenként, legfeljebb 1 percen keresztül.</string> + <string name="alert_title_contact_connection_pending">A partnere még nem kapcsolódott!</string> + <string name="v5_3_discover_join_groups_descr">- kapcsolódás könyvtár szolgáltatáshoz (BÉTA)!\n- kézbesítési jelentések (legfeljebb 20 tagig).\n- gyorsabb és stabilabb.</string> + <string name="contribute">Közreműködés</string> + <string name="group_member_status_intro_invitation">kapcsolódás (bemutatkozó meghívó)</string> + <string name="create_simplex_address">SimpleX-cím létrehozása</string> + <string name="rcv_direct_event_contact_deleted">törölt partner</string> + <string name="delete_member_message__question">Törli a tag üzenetét?</string> + <string name="chat_is_running">A csevegés fut</string> + <string name="share_one_time_link">Egyszer használható meghívó létrehozása</string> + <string name="delete_link">Törlés</string> + <string name="notifications_mode_periodic_desc">Új üzenetek ellenőrzése 10 percenként, legfeljebb 1 percen keresztül</string> <string name="delete_database">Adatbázis törlése</string> <string name="create_group_button">Csoport létrehozása</string> - <string name="network_session_mode_user">Chat profil</string> + <string name="network_session_mode_user">Csevegési profil</string> <string name="create_another_profile_button">Profil létrehozása</string> - <string name="connected_desktop">Csatlakoztatott asztal</string> - <string name="share_text_deleted_at">Törölve ekkor: %s</string> - <string name="info_row_deleted_at">Törölve ekkor</string> - <string name="v4_6_chinese_spanish_interface">Kínai és spanyol kezelőfelület.</string> - <string name="alert_title_cant_invite_contacts">Ismerősök meghívása nem lehetséges!</string> - <string name="chat_is_stopped_indication">A chat szolgáltatás leállt (nem fut)</string> + <string name="connected_desktop">Társított számítógép</string> + <string name="share_text_deleted_at">Törölve: %s</string> + <string name="info_row_deleted_at">Törölve</string> + <string name="v4_6_chinese_spanish_interface">Kínai és spanyol kezelőfelület</string> + <string name="alert_title_cant_invite_contacts">Nem lehet meghívni a partnereket!</string> + <string name="chat_is_stopped_indication">A csevegés megállt</string> <string name="theme_dark">Sötét</string> <string name="create_profile">Profil létrehozása</string> <string name="rcv_group_event_group_deleted">törölt csoport</string> - <string name="full_deletion">Törlés mindenkinek</string> - <string name="button_create_group_link">Link létrehozása</string> - <string name="chat_preferences">Chat beállítások</string> - <string name="chat_archive_header">Chat archívum</string> + <string name="full_deletion">Törlés az összes tagnál</string> + <string name="button_create_group_link">Hivatkozás létrehozása</string> + <string name="chat_preferences">Csevegési beállítások</string> <string name="delete_profile">Profil törlése</string> - <string name="la_current_app_passcode">Jelenlegi Számkód</string> + <string name="la_current_app_passcode">Jelenlegi jelkód</string> <string name="group_member_status_connecting">kapcsolódás</string> <string name="confirm_new_passphrase">Új jelmondat megerősítése…</string> <string name="group_connection_pending">kapcsolódás…</string> - <string name="delete_chat_profile">Chat profil törlés</string> - <string name="custom_time_picker_custom">egyedi</string> - <string name="callstatus_connecting">hívás kapcsolódik…</string> - <string name="customize_theme_title">Színséma személyreszabása</string> + <string name="delete_chat_profile">Csevegési profil törlése</string> + <string name="custom_time_picker_custom">egyéni</string> + <string name="callstatus_connecting">kapcsolódási hívás…</string> + <string name="customize_theme_title">Téma személyre szabása</string> <string name="maximum_supported_file_size">Jelenleg támogatott legnagyobb fájl méret: %1$s.</string> <string name="smp_server_test_delete_file">Fájl törlése</string> <string name="in_developing_title">Hamarosan!</string> - <string name="snd_conn_event_switch_queue_phase_changing_for_member">azonosító megváltoztatása %s számára…</string> - <string name="chat_database_imported">Chat adatbázis importálva</string> - <string name="chat_archive_section">CHAT ARCHÍVUM</string> - <string name="delete_messages">Üzenetek törlése?</string> + <string name="snd_conn_event_switch_queue_phase_changing_for_member">cím módosítása %s számára…</string> + <string name="chat_database_imported">Csevegési adatbázis importálva</string> + <string name="delete_messages">Üzenetek törlése</string> <string name="clear_chat_menu_action">Kiürítés</string> <string name="icon_descr_close_button">Bezárás gomb</string> - <string name="chat_is_stopped">A chat szolgáltatás leállt (nem fut)</string> + <string name="chat_is_stopped">A csevegés megállt</string> <string name="item_info_current">(jelenlegi)</string> - <string name="v5_1_custom_themes_descr">Színsémák személyreszabása és megosztása</string> - <string name="delete_chat_profile_question">Chat profil törlése?</string> + <string name="v5_1_custom_themes_descr">Témák személyre szabása és megosztása.</string> + <string name="delete_chat_profile_question">Törli a csevegési profilt?</string> <string name="create_group">Titkos csoport létrehozása</string> - <string name="connected_to_desktop">Kapcsolódva az asztalhoz</string> - <string name="configure_ICE_servers">ICE sezrverek beállítása</string> + <string name="connected_to_desktop">Kapcsolódva a számítógéphez</string> + <string name="configure_ICE_servers">ICE-kiszolgálók beállítása</string> <string name="button_delete_group">Csoport törlése</string> - <string name="clear_verification">Chat hitelesításe</string> - <string name="group_member_status_creator">szerző</string> + <string name="clear_verification">Hitelesítés törlése</string> + <string name="group_member_status_creator">készítő</string> <string name="confirm_verb">Megerősítés</string> - <string name="for_me_only">Törlés nálam</string> - <string name="delete_messages__question">%d üzenet törlése?</string> - <string name="v5_1_custom_themes">Egyedi színsémák</string> + <string name="for_me_only">Csak nálam</string> + <string name="delete_messages__question">Töröl %d üzenetet?</string> + <string name="v5_1_custom_themes">Egyéni témák</string> <string name="group_member_status_accepted">kapcsolódás (elfogadva)</string> - <string name="smp_servers_check_address">Szerver cím ellenőrzése és újrapróbálkozás.</string> - <string name="delete_group_question">Csoport törlése?</string> - <string name="confirm_database_upgrades">Adatbázis frissítés megerősítése</string> + <string name="smp_servers_check_address">Kiszolgáló címének ellenőrzése és újrapróbálkozás.</string> + <string name="delete_group_question">Törli a csoportot?</string> + <string name="confirm_database_upgrades">Adatbázis fejlesztésének megerősítése</string> <string name="create_your_profile">Saját profil létrehozása</string> - <string name="snd_conn_event_switch_queue_phase_changing">azonosító megváltoztatása…</string> + <string name="snd_conn_event_switch_queue_phase_changing">cím módosítása…</string> <string name="display_name_connecting">kapcsolódás…</string> <string name="icon_descr_call_connecting">Hívás kapcsolása</string> - <string name="delete_files_and_media_question">Fájlok illetve fotók/videók törlése?</string> - <string name="group_member_status_complete">befejezve</string> - <string name="chat_database_section">CHAT ADATBÁZIS</string> - <string name="change_self_destruct_passcode">Önmegsemmisító számkód megváltoztatása</string> - <string name="smp_server_test_create_queue">Várólista létrehozása</string> - <string name="colored_text">színes</string> + <string name="delete_files_and_media_question">Törli a fájl- és a médiatartalmakat?</string> + <string name="group_member_status_complete">befejezett</string> + <string name="chat_database_section">CSEVEGÉSI ADATBÁZIS</string> + <string name="change_self_destruct_passcode">Önmegsemmisítő-jelkód módosítása</string> + <string name="smp_server_test_create_queue">Sorba állítás létrehozása</string> + <string name="colored_text">színezett</string> <string name="callstate_connecting">kapcsolódás…</string> - <string name="dark_theme">Sötét színséma</string> + <string name="dark_theme">Sötét téma</string> <string name="deleted_description">törölve</string> - <string name="users_delete_question">Chat profil törlése?</string> - <string name="chat_with_developers">Chat a SimpleX fejlesztőivel</string> - <string name="delete_link_question">Link törlése?</string> + <string name="users_delete_question">Törli a csevegési profilt?</string> + <string name="chat_with_developers">Csevegés a fejlesztőkkel</string> + <string name="delete_link_question">Törli a hivatkozást?</string> <string name="server_connecting">kapcsolódás</string> - <string name="send_disappearing_message_custom_time">Személyreszabott idő</string> - <string name="connect_via_link_incognito">Inkognítóban csatlakozva</string> - <string name="settings_section_title_chats">CHATEK</string> - <string name="v5_3_new_desktop_app_descr">Új profil létrehozása a számítógépen futó appban. 💻</string> + <string name="send_disappearing_message_custom_time">Egyéni időköz</string> + <string name="connect_via_link_incognito">Kapcsolódás inkognitóban</string> + <string name="settings_section_title_chats">CSEVEGÉSEK</string> + <string name="v5_3_new_desktop_app_descr">Új profil létrehozása a számítógép alkalmazásban. 💻</string> <string name="group_member_status_announced">kapcsolódás (bejelentve)</string> <string name="contact_connection_pending">kapcsolódás…</string> - <string name="chat_database_deleted">Chat adatbázis törölve</string> - <string name="group_member_status_introduced">kapcsolódás (bejelentve)</string> - <string name="create_group_link">Csoporthoz link létrehozása</string> - <string name="chat_console">Chat konzol</string> - <string name="delete_files_and_media_for_all_users">Fájlok törlése minden chat profil alatt</string> - <string name="smp_server_test_delete_queue">Várólista törlése</string> - <string name="button_delete_contact">Ismerős törlése</string> - <string name="archive_created_on_ts">Létrehozva: %1$s</string> - <string name="rcv_conn_event_switch_queue_phase_changing">azonosító megváltoztatása…</string> - <string name="connected_to_mobile">Kapcsolódva a mobilhoz</string> + <string name="chat_database_deleted">Csevegési adatbázis törölve</string> + <string name="group_member_status_introduced">kapcsolódás (bemutatkozva)</string> + <string name="create_group_link">Csoporthivatkozás létrehozása</string> + <string name="chat_console">Csevegési konzol</string> + <string name="delete_files_and_media_for_all_users">Fájlok törlése az összes csevegési profilból</string> + <string name="smp_server_test_delete_queue">Sorba állítás törlése</string> + <string name="button_delete_contact">Partner törlése</string> + <string name="rcv_conn_event_switch_queue_phase_changing">cím módosítása…</string> + <string name="connected_to_mobile">Társítva a hordozható eszközhöz</string> <string name="current_passphrase">Jelenlegi jelmondat…</string> - <string name="choose_file_title">Fájl választása</string> - <string name="create_one_time_link">Egyszer használatos meghívó link létrehozása</string> + <string name="choose_file_title">Fájl kiválasztása</string> <string name="delete_image">Kép törlése</string> <string name="smp_server_test_create_file">Fájl létrehozása</string> <string name="create_secret_group_title">Tikos csoport létrehozása</string> - <string name="clear_contacts_selection_button">Kiürítés</string> - <string name="delete_contact_question">Ismerős törlése?</string> + <string name="clear_contacts_selection_button">Elvetés</string> + <string name="delete_contact_question">Törli a partnert?</string> <string name="clear_verb">Kiürítés</string> - <string name="create_address_and_let_people_connect">Hozz létre egy azonosítót, hogy az ismerősök kapcsolatba léphessenek veled.</string> - <string name="v4_4_verify_connection_security_desc">Biztonsági kódok ösezhasonlítása az ismerősökkel.</string> - <string name="smp_server_test_compare_file">Fájl összehasonlítása</string> - <string name="your_chats">Chatek</string> - <string name="delete_message__question">Üzenet törlése?</string> - <string name="delete_pending_connection__question">Függő kapcsolatfelvételi kérés törlése?</string> + <string name="create_address_and_let_people_connect">Cím létrehozása, hogy az emberek kapcsolatba léphessenek Önnel.</string> + <string name="v4_4_verify_connection_security_desc">Biztonsági kódok összehasonlítása a partnerekével.</string> + <string name="smp_server_test_compare_file">Fájl-összehasonlítás</string> + <string name="your_chats">Csevegések</string> + <string name="delete_message__question">Törli az üzenetet?</string> + <string name="delete_pending_connection__question">Törli a függőben lévő meghívót?</string> <string name="database_encrypted">Adatbázis titkosítva!</string> - <string name="clear_chat_question">Chat kiürítése?</string> - <string name="database_downgrade">Adatbázis downgrade?</string> - <string name="clear_chat_button">Chat kiürítése</string> - <string name="database_passphrase_will_be_updated">Adatbázis titkosítási jelmondat meg lesz változtatva.</string> + <string name="clear_chat_question">Kiüríti a csevegést?</string> + <string name="database_downgrade">Adatbázis visszafejlesztése</string> + <string name="clear_chat_button">Üzenetek kiürítése</string> + <string name="database_passphrase_will_be_updated">Az adatbázis titkosítási jelmondata frissítve lesz.</string> <string name="multicast_connect_automatically">Kapcsolódás automatikusan</string> - <string name="database_error">Adatbázis hiba</string> - <string name="database_encryption_will_be_updated_in_settings">Adatbázis titkosítási jelmondat frissül és eltárolásra kerül a beállításokban.</string> - <string name="info_row_database_id">Adatbázis ID</string> - <string name="share_text_database_id">Adatbázis ID: %d</string> - <string name="developer_options">Adatbázis azonosítók és \"Transport Isolation\" opciók.</string> - <string name="database_encryption_will_be_updated">Az adatbázis titkosítás jelmondata megváltoztatásra és elmentésre kerül a Keystore-ban.</string> - <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Az adatbázis titkosításra kerül és a jelmondat eltárolásra a beállításokban.</string> - <string name="smp_servers_delete_server">Szerver törlése</string> - <string name="auth_device_authentication_is_disabled_turning_off">Eszközhitelesítés kikapcsolva. SimpleX zár kikapcsolása.</string> + <string name="database_error">Adatbázishiba</string> + <string name="database_encryption_will_be_updated_in_settings">Az adatbázis titkosítási jelmondata frissülni fog és a beállításokban lesz tárolva.</string> + <string name="info_row_database_id">Adatbázis-azonosító</string> + <string name="share_text_database_id">Adatbázis-azonosító: %d</string> + <string name="developer_options">Adatbázis-azonosítók és átvitel-izolációs beállítások.</string> + <string name="database_encryption_will_be_updated">Az adatbázis titkosítási jelmondata frissülni fog és a Keystore-ban lesz tárolva.</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Az adatbázis titkosítva lesz, a jelmondat pedig a beállításokban lesz tárolva.</string> + <string name="smp_servers_delete_server">Kiszolgáló törlése</string> + <string name="auth_device_authentication_is_disabled_turning_off">Az eszközön nincs beállítva a képernyőzár. A SimpleX-zár ki van kapcsolva.</string> <string name="no_call_on_lock_screen">Letiltás</string> - <string name="receipts_groups_disable_for_all">Letiltás minden csoportnak</string> - <string name="receipts_groups_enable_for_all">Engedélyezve minden csoportnak</string> - <string name="feature_enabled_for_contact">engedélyezve az ismerősnek</string> - <string name="disappearing_messages_are_prohibited">Eltűnő üzenetek tiltottak ebben a csoportban.</string> - <string name="delete_address">Azonosító törlés</string> + <string name="receipts_groups_disable_for_all">Letiltás az összes csoport számára</string> + <string name="receipts_groups_enable_for_all">Engedélyezés az összes csoport számára</string> + <string name="feature_enabled_for_contact">engedélyezve a partner számára</string> + <string name="disappearing_messages_are_prohibited">Az eltűnő üzenetek küldése le van tiltva.</string> + <string name="delete_address">Cím törlése</string> <string name="ttl_week">%d hét</string> - <string name="desktop_address">PC címe</string> - <string name="ttl_s">%ds</string> - <string name="delivery_receipts_title">Kézbesítési izagolások!</string> - <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Eszközhitelesítés nincs bekapcsolva. Bekapcsolhatod a SimpleX zárat a Beállításokon keresztük, miután bekapcsoltad az eszközhitelesítést.</string> + <string name="desktop_address">Számítógép címe</string> + <string name="ttl_s">%dmp</string> + <string name="delivery_receipts_title">Kézbesítési jelentések!</string> + <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Az eszközön nincs beállítva a képernyőzár. A SimpleX-zár az „Adatvédelem és biztonság” menüben kapcsolható be, miután beállította a képernyőzárat az eszközén.</string> <string name="decryption_error">Titkosítás visszafejtési hiba</string> - <string name="share_text_disappears_at">Eltűnik ekkor: %s</string> + <string name="share_text_disappears_at">Eltűnik: %s</string> <string name="icon_descr_edited">szerkesztve</string> <string name="delete_verb">Törlés</string> <string name="ttl_hours">%d óra</string> <string name="ttl_months">%d hónap</string> - <string name="delete_address__question">Azonosító törlése?</string> - <string name="receipts_contacts_title_disable">Igazolások letiltása?</string> - <string name="passphrase_is_different">Az adatbázis jelmondat eltérő a Keystore-ba elmentettől.</string> + <string name="delete_address__question">Törli a címet?</string> + <string name="receipts_contacts_title_disable">Letiltja a kézbesítési jelentéseket?</string> + <string name="passphrase_is_different">Az adatbázis-jelmondat eltér a Keystore-ban lévőtől.</string> <string name="direct_messages">Közvetlen üzenetek</string> <string name="icon_descr_email">E-mail</string> - <string name="receipts_contacts_disable_for_all">Letiltás mindenkinek</string> + <string name="receipts_contacts_disable_for_all">Letiltás</string> <string name="settings_developer_tools">Fejlesztői eszközök</string> - <string name="database_passphrase">Adatbázis jelmondat</string> - <string name="ttl_days">%d napok</string> - <string name="icon_descr_server_status_disconnected">Szétkapcsolva</string> - <string name="encrypted_with_random_passphrase">Az adatbázis egy véletlenszerű jelmondattal van titkosítva, lecserélheted.</string> + <string name="database_passphrase">Adatbázis-jelmondat</string> + <string name="ttl_days">%d nap</string> + <string name="icon_descr_server_status_disconnected">Kapcsolat bontva</string> + <string name="encrypted_with_random_passphrase">Az adatbázis egy véletlenszerű jelmondattal van titkosítva, amelyet szabadon módosíthat.</string> <string name="ttl_h">%dó</string> <string name="ttl_w">%dhét</string> - <string name="discover_on_network">Felfedezés helyi hálózatomn keresztül</string> - <string name="v5_3_discover_join_groups">Helyi csoportok felfedezése és csatolakozás</string> - <string name="moderated_items_description">%d üzenet moderálva %s által</string> + <string name="discover_on_network">Felfedezés helyi hálózaton keresztül</string> + <string name="v5_3_discover_join_groups">Csoportok felfedezése és csatlakozás</string> + <string name="moderated_items_description">%2$s %1$d üzenetet moderált</string> <string name="disappearing_message">Eltűnő üzenet</string> - <string name="dont_create_address">Ne hozz létre azonosítót</string> - <string name="dont_show_again">Ne mutasd ismét</string> - <string name="auth_disable_simplex_lock">SimpleX Zár kikapcsolása</string> + <string name="dont_create_address">Ne hozzon létre címet</string> + <string name="dont_show_again">Ne mutasd újra</string> + <string name="auth_disable_simplex_lock">SimpleX-zár kikapcsolása</string> <string name="status_e2e_encrypted">e2e titkosított</string> <string name="settings_section_title_device">ESZKÖZ</string> <string name="encrypted_video_call">e2e titkosított videóhívás</string> <string name="conn_level_desc_direct">közvetlen</string> - <string name="desktop_device">PC</string> + <string name="desktop_device">Számítógép</string> <string name="la_minutes">%d perc</string> - <string name="num_contacts_selected">%d ismerős(-ök) kiválasztva</string> - <string name="enable_receipts_all">Engedélyez</string> + <string name="num_contacts_selected">%d partner kijelölve</string> + <string name="enable_receipts_all">Engedélyezés</string> <string name="ttl_mth">%dhónap</string> - <string name="direct_messages_are_prohibited_in_chat">A közvetlen üzenetek tagok között titltottak ebben a csoportban.</string> + <string name="direct_messages_are_prohibited_in_group">A közvetlen üzenetek küldése a tagok között le van tiltva ebben a csoportban.</string> <string name="ttl_min">%d perc</string> - <string name="set_password_to_export_desc">Az adatbázis egy véletlenszerű jelmondattal van titkosítva. Kérlek cseréld le exportálás előtt!</string> - <string name="receipts_groups_title_disable">Igazolások letiltása csoportoknak?</string> + <string name="set_password_to_export_desc">Az adatbázis egy véletlenszerű jelmondattal van titkosítva. Exportálás előtt módosítsa.</string> + <string name="receipts_groups_title_disable">Letiltja a kézbesítési jelentéseket a csoportok számára?</string> <string name="custom_time_unit_days">nap</string> <string name="ttl_day">%d nap</string> - <string name="delete_chat_archive_question">Chat archív törlése?</string> - <string name="failed_to_create_user_duplicate_title">Duplikálódott megjelenítési név!</string> - <string name="receipts_contacts_disable_keep_overrides">Letiltás(felülírások megtartásával)</string> - <string name="database_upgrade">"Adatbázis upgrade"</string> - <string name="blocked_items_description">%d üzenet blokkolva</string> - <string name="info_row_disappears_at">Eltűnik ekkor</string> + <string name="failed_to_create_user_duplicate_title">Duplikált megjelenítendő név!</string> + <string name="receipts_contacts_disable_keep_overrides">Letiltás (felülírások megtartásával)</string> + <string name="database_upgrade">Adatbázis fejlesztése</string> + <string name="blocked_items_description">%d üzenet letiltva</string> + <string name="info_row_disappears_at">Eltűnik</string> <string name="ttl_weeks">%d hét</string> - <string name="feature_enabled_for_you">engedélyezve számodra</string> + <string name="feature_enabled_for_you">engedélyezve az Ön számára</string> <string name="timed_messages">Eltűnő üzenetek</string> <string name="delete_group_menu_action">Törlés</string> - <string name="delete_and_notify_contact">Törlés és ismerős értesítése</string> + <string name="delete_and_notify_contact">Törlés, és a partner értesítése</string> <string name="send_receipts_disabled">letiltva</string> - <string name="la_seconds">%d másodperc</string> - <string name="delete_files_and_media_all">Minden fájl törlése</string> - <string name="database_will_be_encrypted">Az adatbázis titkosításra kerül.</string> - <string name="database_passphrase_and_export">Adatbázis jelmondat és exportálás</string> - <string name="database_will_be_encrypted_and_passphrase_stored">Az adatbázis titkosításra kerül és a jelmondat eltárolásra a Keystore-ban.</string> - <string name="enable_automatic_deletion_question">Automatikus üzenet törlés engedélyezve?</string> + <string name="la_seconds">%d mp</string> + <string name="delete_files_and_media_all">Az összes fájl törlése</string> + <string name="database_will_be_encrypted">Az adatbázis titkosítva lesz.</string> + <string name="database_passphrase_and_export">Adatbázis-jelmondat és -exportálás</string> + <string name="database_will_be_encrypted_and_passphrase_stored">Az adatbázis titkosítva lesz, a jelmondat pedig a Keystore-ban lesz tárolva.</string> + <string name="enable_automatic_deletion_question">Engedélyezi az automatikus üzenettörlést?</string> <string name="delete_contact_menu_action">Törlés</string> - <string name="mtr_error_no_down_migration">az adatbázis verzió újabb, mint az app, de nincs lefelé migráció eddig: %s</string> + <string name="mtr_error_no_down_migration">az adatbázis verziója újabb, mint az alkalmazásé, de a visszafelé történő átköltöztetés viszont nem lehetséges a következőhöz: %s</string> <string name="simplex_link_mode_description">Leírás</string> <string name="ttl_hour">%d óra</string> <string name="ttl_m">%dp</string> <string name="disconnect_remote_host">Szétkapcsolás</string> <string name="edit_verb">Szerkesztés</string> - <string name="receipts_groups_disable_keep_overrides">Letiltás(csoport felülírások megtartásával)</string> + <string name="receipts_groups_disable_keep_overrides">Letiltás (csoport felülírások megtartásával)</string> <string name="rcv_group_events_count">%d csoportesemény</string> <string name="ttl_month">%d hónap</string> - <string name="button_edit_group_profile">Csoport profil szerkesztése</string> + <string name="button_edit_group_profile">Csoportprofil szerkesztése</string> <string name="encrypted_audio_call">e2e titkosított hanghívás</string> - <string name="ttl_sec">%d s</string> + <string name="ttl_sec">%d mp</string> <string name="decentralized">Decentralizált</string> <string name="image_decoding_exception_title">Dekódolási hiba</string> <string name="edit_image">Kép szerkesztése</string> - <string name="disable_notifications_button">Értesítéásek letiltása</string> + <string name="disable_notifications_button">Értesítések letiltása</string> <string name="devices">Eszközök</string> - <string name="multicast_discoverable_via_local_network">Látható helyi hálózaton</string> - <string name="dont_enable_receipts">Ne engedélyezd</string> - <string name="delete_archive">Archívum törlése</string> - <string name="disappearing_prohibited_in_this_chat">Eltűnő üzenetek tiltottak ebben a chatben.</string> - <string name="chat_preferences_default">alap (%s)</string> - <string name="integrity_msg_duplicate">duplikálódott üzenet</string> - <string name="disconnect_desktop_question">Számítógép leválasztása?</string> - <string name="desktop_app_version_is_incompatible">A számítógépes app verzió %s inem kompatibilis ezzel az appal.</string> + <string name="multicast_discoverable_via_local_network">Látható a helyi hálózaton</string> + <string name="dont_enable_receipts">Ne engedélyezze</string> + <string name="disappearing_prohibited_in_this_chat">Az eltűnő üzenetek küldése le van tiltva ebben a csevegésben.</string> + <string name="chat_preferences_default">alapértelmezett (%s)</string> + <string name="integrity_msg_duplicate">duplikált üzenet</string> + <string name="disconnect_desktop_question">Leválasztja a számítógépet?</string> + <string name="desktop_app_version_is_incompatible">A számítógép-alkalmazás verziója (%s) nem kompatibilis ezzel az alkalmazással.</string> <string name="delivery">Kézbesítés</string> - <string name="total_files_count_and_size">%d fájl %s összméretben</string> - <string name="database_passphrase_is_required">Adatbázis jelmondat szükséges chat megnyitásához.</string> - <string name="ttl_d">%dn</string> - <string name="receipts_contacts_enable_for_all">Engedélyeve mindenki számára</string> - <string name="delivery_receipts_are_disabled">Kézbesítési izagolások kikapcsolva!</string> -</resources> \ No newline at end of file + <string name="total_files_count_and_size">%d fájl, %s összméretben</string> + <string name="database_passphrase_is_required">A csevegés megnyitásához adja meg az adatbázis jelmondatát.</string> + <string name="ttl_d">%dnap</string> + <string name="receipts_contacts_enable_for_all">Engedélyezés az összes tag számára</string> + <string name="delivery_receipts_are_disabled">A kézbesítési jelentések le vannak tiltva!</string> + <string name="expand_verb">Kibontás</string> + <string name="error_sending_message">Hiba történt az üzenet elküldésekor</string> + <string name="la_enter_app_passcode">Adja meg a jelkódot</string> + <string name="for_everybody">Mindenkinél</string> + <string name="encryption_renegotiation_error">Hiba történt a titkosítás újraegyeztetésekor</string> + <string name="error_encrypting_database">Hiba történt az adatbázis titkosításakor</string> + <string name="error_deleting_group">Hiba történt a csoport törlésekor</string> + <string name="exit_without_saving">Kilépés mentés nélkül</string> + <string name="v5_3_encrypt_local_files">A tárolt fájlok- és a médiatartalmak titkosítása</string> + <string name="error_setting_address">Hiba történt a cím beállításakor</string> + <string name="group_invitation_expired">A csoportmeghívó lejárt</string> + <string name="error_saving_ICE_servers">Hiba történt az ICE-kiszolgálók mentésekor</string> + <string name="error_alert_title">Hiba</string> + <string name="icon_descr_server_status_error">Hiba</string> + <string name="error_loading_xftp_servers">Hiba történt az XFTP-kiszolgálók betöltésekor</string> + <string name="error_loading_smp_servers">Hiba történt az SMP-kiszolgálók betöltésekor</string> + <string name="error_setting_network_config">Hiba történt a hálózat konfigurációjának frissítésekor</string> + <string name="network_option_enable_tcp_keep_alive">TCP életben tartása</string> + <string name="icon_descr_flip_camera">Kamera váltás</string> + <string name="email_invite_body">Üdvözlöm!\nCsatlakozzon hozzám a SimpleX Chaten keresztül: %s</string> + <string name="display_name_cannot_contain_whitespace">A megjelenítendő név nem tartalmazhat szóközöket.</string> + <string name="info_row_group">Csoport</string> + <string name="enter_welcome_message_optional">Adja meg az üdvözlőüzenetet… (nem kötelező)</string> + <string name="error_exporting_chat_database">Hiba történt a csevegési adatbázis exportálásakor</string> + <string name="error_saving_file">Hiba történt a fájl mentésekor</string> + <string name="encrypt_local_files">Helyi fájlok titkosítása</string> + <string name="snd_conn_event_ratchet_sync_agreed">titkosítás elfogadva %s számára</string> + <string name="marked_deleted_items_description">%d üzenet megjelölve törlésre</string> + <string name="conn_event_ratchet_sync_allowed">a titkosítás újraegyeztetése engedélyezve van</string> + <string name="enable_self_destruct">Önmegsemmisítés engedélyezése</string> + <string name="v5_2_favourites_filter_descr">Olvasatlan és kedvenc csevegésekre való szűrés.</string> + <string name="failed_to_parse_chats_title">Nem sikerült betölteni a csevegéseket</string> + <string name="connect_plan_group_already_exists">A csoport már létezik!</string> + <string name="v4_4_french_interface">Francia kezelőfelület</string> + <string name="v4_2_group_links">Csoporthivatkozások</string> + <string name="v5_1_message_reactions_descr">Végre, megvannak! 🚀</string> + <string name="error_starting_chat">Hiba történt a csevegés elindításakor</string> + <string name="group_profile_is_stored_on_members_devices">A csoport profilja a tagok eszközein tárolódik, nem a kiszolgálókon.</string> + <string name="enter_passphrase">Adja meg a jelmondatot…</string> + <string name="error_updating_user_privacy">Hiba történt a felhasználói adatvédelem frissítésekor</string> + <string name="encrypt_database">Titkosít</string> + <string name="alert_title_no_group">Csoport nem található!</string> + <string name="error_saving_smp_servers">Hiba történt az SMP-kiszolgálók mentésekor</string> + <string name="downgrade_and_open_chat">Visszafejlesztés és a csevegés megnyitása</string> + <string name="icon_descr_group_inactive">A csoport inaktív</string> + <string name="v5_0_large_files_support_descr">Gyors és nem kell várni, amíg a feladó online lesz!</string> + <string name="error_joining_group">Hiba történt a csoporthoz való csatlakozáskor</string> + <string name="favorite_chat">Kedvenc</string> + <string name="v4_6_group_moderation">Csoport moderálása</string> + <string name="choose_file">Fájl</string> + <string name="group_link">Csoporthivatkozás</string> + <string name="snd_conn_event_ratchet_sync_required">a titkosítás újraegyeztetése szükséges %s számára</string> + <string name="failed_to_active_user_title">Hiba történt a profilváltáskor!</string> + <string name="settings_experimental_features">Kísérleti funkciók</string> + <string name="receipts_contacts_enable_keep_overrides">Engedélyezés (felülírások megtartásával)</string> + <string name="enter_correct_passphrase">Adja meg a helyes jelmondatot.</string> + <string name="delete_group_for_self_cannot_undo_warning">A csoport törölve lesz az Ön számára – ez a művelet nem vonható vissza!</string> + <string name="encrypt_database_question">Titkosítja az adatbázist?</string> + <string name="allow_accepting_calls_from_lock_screen">A zárolási képernyőn megjelenő hívások engedélyezése a Beállításokban.</string> + <string name="conn_event_ratchet_sync_agreed">titkosítás elfogadva</string> + <string name="receipts_contacts_title_enable">Engedélyezi a kézbesítési jelentéseket?</string> + <string name="error_saving_group_profile">Hiba történt a csoportprofil mentésekor</string> + <string name="server_error">hiba</string> + <string name="revoke_file__message">A fájl törölve lesz a kiszolgálókról.</string> + <string name="v5_2_disappear_one_message_descr">Akkor is, ha le van tiltva a beszélgetésben.</string> + <string name="v5_4_better_groups_descr">Gyorsabb csatlakozás és megbízhatóbb üzenetkézbesítés.</string> + <string name="enable_lock">Zárolás engedélyezése</string> + <string name="settings_section_title_help">SÚGÓ</string> + <string name="group_is_decentralized">Teljesen decentralizált – csak a tagok számára látható.</string> + <string name="file_with_path">Fájl: %s</string> + <string name="icon_descr_hang_up">Hívás befejezése</string> + <string name="error_deleting_link_for_group">Hiba történt a csoporthivatkozás törlésekor</string> + <string name="file_saved">Fájl elmentve</string> + <string name="fix_connection_question">Kapcsolat javítása?</string> + <string name="files_and_media">Fájlok és médiatartalmak</string> + <string name="section_title_for_console">KONZOLHOZ</string> + <string name="alert_text_encryption_renegotiation_failed">Nem sikerült a titkosítást újraegyeztetni.</string> + <string name="error_deleting_user">Hiba történt a felhasználó-profil törlésekor</string> + <string name="fix_connection_not_supported_by_group_member">Csoporttag általi javítás nem támogatott</string> + <string name="enter_welcome_message">Adja meg az üdvözlőüzenetet…</string> + <string name="encrypted_database">Titkosított adatbázis</string> + <string name="enter_password_to_show">Adja meg a jelszót a keresőben</string> + <string name="file_will_be_received_when_contact_completes_uploading">A fájl akkor érkezik meg, amikor a küldője befejezte annak feltöltését.</string> + <string name="smp_server_test_download_file">Fájl letöltése</string> + <string name="failed_to_parse_chat_title">Nem sikerült betölteni a csevegést</string> + <string name="smp_servers_enter_manually">Adja meg a kiszolgálót kézzel</string> + <string name="file_will_be_received_when_contact_is_online">A fájl akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később!</string> + <string name="error_creating_link_for_group">Hiba történt a csoporthivatkozás létrehozásakor</string> + <string name="from_gallery_button">A galériából</string> + <string name="receipts_groups_enable_keep_overrides">Engedélyezés (csoport felülírások megtartásával)</string> + <string name="error_deleting_contact">Hiba történt a partner törlésekor</string> + <string name="group_members_can_delete">A tagok véglegesen törölhetik az elküldött üzeneteiket. (24 óra)</string> + <string name="error_changing_role">Hiba történt a szerepkör módosításakor</string> + <string name="fix_connection_confirm">Javítás</string> + <string name="group_members_can_send_disappearing">A tagok küldhetnek eltűnő üzeneteket.</string> + <string name="fix_connection">Kapcsolat javítása</string> + <string name="failed_to_create_user_title">Hiba történt a profil létrehozásakor!</string> + <string name="error_adding_members">Hiba történt a tag(ok) hozzáadásakor</string> + <string name="icon_descr_file">Fájl</string> + <string name="group_members_can_send_files">A tagok küldhetnek fájlokat és médiatartalmakat.</string> + <string name="delete_after">Törlés ennyi idő után</string> + <string name="error_changing_message_deletion">Hiba történt a beállítás módosításakor</string> + <string name="error_updating_link_for_group">Hiba történt a csoporthivatkozás frissítésekor</string> + <string name="group_member_status_group_deleted">a csoport törölve</string> + <string name="snd_group_event_group_profile_updated">csoportprofil frissítve</string> + <string name="error_deleting_pending_contact_connection">Hiba történt a függőben lévő meghívó törlésekor</string> + <string name="error_importing_database">Hiba történt a csevegési adatbázis importálásakor</string> + <string name="error_enabling_delivery_receipts">Hiba történt a kézbesítési jelentések engedélyezésekor!</string> + <string name="error_saving_xftp_servers">Hiba történt az XFTP-kiszolgálók mentésekor</string> + <string name="group_members_can_send_dms">A tagok küldhetnek egymásnak közvetlen üzeneteket.</string> + <string name="error_removing_member">Hiba történt a tag eltávolításakor</string> + <string name="callstate_ended">befejeződött</string> + <string name="v4_6_group_welcome_message">A csoport üdvözlőüzenete</string> + <string name="group_display_name_field">Adja meg a csoport nevét:</string> + <string name="error_sending_message_contact_invitation">Hiba történt a meghívó elküldésekor</string> + <string name="display_name">Adjon meg egy nevet:</string> + <string name="error_saving_user_password">Hiba történt a felhasználó jelszavának mentésekor</string> + <string name="export_theme">Téma exportálása</string> + <string name="enter_this_device_name">Adja meg ennek az eszköznek a nevét…</string> + <string name="error">Hiba</string> + <string name="alert_message_group_invitation_expired">A csoportmeghívó már nem érvényes, a küldője eltávolította.</string> + <string name="group_full_name_field">A csoport teljes neve:</string> + <string name="icon_descr_help">súgó</string> + <string name="enabled_self_destruct_passcode">Önmegsemmisítő-jelkód engedélyezése</string> + <string name="settings_section_title_experimenta">KÍSÉRLETI</string> + <string name="error_aborting_address_change">Hiba történt a cím módosításának megszakításakor</string> + <string name="error_receiving_file">Hiba történt a fájl fogadásakor</string> + <string name="conn_event_ratchet_sync_ok">titkosítás rendben</string> + <string name="error_deleting_contact_request">Hiba történt a meghívási kérés törlésekor</string> + <string name="receipts_groups_title_enable">Engedélyezi a kézbesítési jelentéseket a csoportok számára?</string> + <string name="fix_connection_not_supported_by_contact">Partner általi javítás nem támogatott</string> + <string name="file_not_found">Fájl nem található</string> + <string name="smp_server_test_disconnect">Kapcsolat bontása</string> + <string name="group_members_can_add_message_reactions">A tagok reakciókat adhatnak hozzá az üzenetekhez.</string> + <string name="export_database">Adatbázis exportálása</string> + <string name="full_name__field">Teljes név:</string> + <string name="v4_6_reduced_battery_usage">Tovább csökkentett akkumulátor-használat</string> + <string name="error_stopping_chat">Hiba történt a csevegés megállításakor</string> + <string name="snd_conn_event_ratchet_sync_ok">titkosítás rendben %s számára</string> + <string name="delete_group_for_all_members_cannot_undo_warning">A csoport törölve lesz az összes tag számára – ez a művelet nem vonható vissza!</string> + <string name="v5_2_fix_encryption_descr">Titkosítás javítása az adatmentések helyreállítása után.</string> + <string name="error_deleting_database">Hiba történt a csevegési adatbázis törlésekor</string> + <string name="simplex_link_mode_full">Teljes hivatkozás</string> + <string name="error_changing_address">Hiba történt a cím módosításakor</string> + <string name="group_members_can_send_voice">A tagok küldhetnek hangüzeneteket.</string> + <string name="group_preferences">Csoportbeállítások</string> + <string name="error_with_info">Hiba: %s</string> + <string name="v4_4_disappearing_messages">Eltűnő üzenetek</string> + <string name="auth_enable_simplex_lock">SimpleX-zár bekapcsolása</string> + <string name="error_synchronizing_connection">Hiba történt a kapcsolat szinkronizálásakor</string> + <string name="error_creating_address">Hiba történt a cím létrehozásakor</string> + <string name="feature_enabled">engedélyezve</string> + <string name="error_loading_details">Hiba történt a részletek betöltésekor</string> + <string name="error_accepting_contact_request">Hiba történt a meghívási kérés elfogadásakor</string> + <string name="snd_conn_event_ratchet_sync_allowed">a titkosítás újraegyeztetése engedélyezve van %s számára</string> + <string name="conn_event_ratchet_sync_required">a titkosítás újraegyeztetése szükséges</string> + <string name="v4_6_hidden_chat_profiles">Rejtett csevegési profilok</string> + <string name="files_and_media_section">Fájlok és médiatartalmak</string> + <string name="image_saved">A kép elmentve a „Galériába”</string> + <string name="hide_notification">Elrejtés</string> + <string name="la_immediately">Azonnal</string> + <string name="files_and_media_prohibited">A fájlok- és a médiatartalmak küldése le van tiltva!</string> + <string name="hide_profile">Profil elrejtése</string> + <string name="how_to_use_your_servers">Hogyan használja a saját kiszolgálóit</string> + <string name="v5_2_favourites_filter">Csevegési üzenetek gyorsabb megtalálása</string> + <string name="import_theme">Téma importálása</string> + <string name="import_theme_error">Hiba történt a téma importálásakor</string> + <string name="notification_display_mode_hidden_desc">Partner nevének és az üzenet tartalmának elrejtése</string> + <string name="incompatible_database_version">Nem kompatibilis adatbázis-verzió</string> + <string name="how_simplex_works">Hogyan működik a SimpleX</string> + <string name="desktop_incompatible_version">Nem kompatibilis verzió</string> + <string name="user_hide">Elrejtés</string> + <string name="incoming_video_call">Bejövő videóhívás</string> + <string name="incorrect_passcode">Téves jelkód</string> + <string name="onboarding_notifications_mode_service">Azonnali</string> + <string name="v5_4_incognito_groups">Inkognitócsoportok</string> + <string name="how_to">Hogyan</string> + <string name="hide_verb">Összecsukás</string> + <string name="gallery_image_button">Kép</string> + <string name="v4_3_improved_privacy_and_security">Fejlesztett adatvédelem és biztonság</string> + <string name="ignore">Mellőzés</string> + <string name="icon_descr_image_snd_complete">Kép elküldve</string> + <string name="notification_preview_mode_hidden">Se név, se üzenet</string> + <string name="host_verb">Kiszolgáló</string> + <string name="initial_member_role">Kezdeti szerepkör</string> + <string name="invalid_chat">érvénytelen csevegés</string> + <string name="custom_time_unit_hours">óra</string> + <string name="incognito">Inkognitó</string> + <string name="how_to_use_simplex_chat">Használati útmutató</string> + <string name="v4_3_improved_privacy_and_security_desc">Alkalmazás képernyőjének elrejtése a gyakran használt alkalmazások között.</string> + <string name="v4_3_improved_server_configuration">Javított kiszolgáló konfiguráció</string> + <string name="edit_history">Előzmények</string> + <string name="hidden_profile_password">Rejtett profiljelszó</string> + <string name="import_database">Adatbázis importálása</string> + <string name="import_database_confirmation">Importálás</string> + <string name="icon_descr_instant_notifications">Azonnali értesítések</string> + <string name="settings_section_title_incognito">Inkognitómód</string> + <string name="import_database_question">Importálja a csevegési adatbázist?</string> + <string name="service_notifications_disabled">Az azonnali értesítések le vannak tiltva!</string> + <string name="service_notifications">Azonnali értesítések!</string> + <string name="image_descr">Kép</string> + <string name="files_are_prohibited_in_group">A fájlok- és a médiatartalmak küldése le van tiltva.</string> + <string name="how_it_works">Hogyan működik</string> + <string name="hide_dev_options">Elrejtve:</string> + <string name="error_creating_member_contact">Hiba történt a partnerrel történő kapcsolat létrehozásában</string> + <string name="enter_one_ICE_server_per_line">ICE-kiszolgálók (soronként egy)</string> + <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Ha nem tud személyesen találkozni, <b>beolvashatja a QR-kódot a videohívásban</b>, vagy a partnere megoszthat egy meghívási hivatkozást.]]></string> + <string name="if_you_enter_passcode_data_removed">Ha az alkalmazás megnyitásakor megadja ezt a jelkódot, az összes alkalmazásadat véglegesen el lesz távolítva!</string> + <string name="if_you_cant_meet_in_person">Ha nem tud személyesen találkozni, mutassa meg a QR-kódot egy videohívás közben, vagy ossza meg a hivatkozást.</string> + <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Ha nem tud személyesen találkozni, <b>mutassa meg a QR-kódot a videohívásban</b>, vagy ossza meg a hivatkozást.]]></string> + <string name="network_disable_socks_info">Megerősítés esetén az üzenetváltó-kiszolgálók látni fogják az IP-címét és a szolgáltatóját – azt, hogy mely kiszolgálókhoz kapcsolódik.</string> + <string name="image_will_be_received_when_contact_completes_uploading">A kép akkor érkezik meg, amikor a küldője befejezte annak feltöltését.</string> + <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 számítógép: a megjelenített QR-kód beolvasása az alkalmazásból, a <b>QR-kód beolvasásával</b>.]]></string> + <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">A kapott SimpleX Chat-meghívási hivatkozását megnyithatja a böngészőjében:</string> + <string name="if_you_enter_self_destruct_code">Ha az alkalmazás megnyitásakor megadja az önmegsemmisítő-jelkódot:</string> + <string name="found_desktop">Megtalált számítógép</string> + <string name="desktop_devices">Számítógépek</string> + <string name="how_to_use_markdown">A markdown használata</string> + <string name="create_chat_profile">Csevegési profil létrehozása</string> + <string name="immune_to_spam_and_abuse">Védett a kéretlen tartalommal szemben</string> + <string name="disconnect_remote_hosts">Hordozható eszközök leválasztása</string> + <string name="v4_5_multiple_chat_profiles_descr">Különböző nevek, profilképek és átvitel-izoláció.</string> + <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Elutasítás esetén a feladó NEM kap értesítést.</string> + <string name="icon_descr_expand_role">Szerepkörválasztó kibontása</string> + <string name="image_will_be_received_when_contact_is_online">A kép akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később!</string> + <string name="group_member_status_invited">meghíva</string> + <string name="invalid_connection_link">Érvénytelen kapcsolattartási hivatkozás</string> + <string name="mute_chat">Némítás</string> + <string name="no_details">nincsenek részletek</string> + <string name="icon_descr_call_missed">Nem fogadott hívás</string> + <string name="theme_light">Világos</string> + <string name="delete_message_cannot_be_undone_warning">Az üzenet törölve lesz – ez a művelet nem vonható vissza!</string> + <string name="markdown_help">Markdown súgó</string> + <string name="notification_preview_new_message">új üzenet</string> + <string name="old_database_archive">Régi adatbázis-archívum</string> + <string name="network_settings_title">Speciális beállítások</string> + <string name="no_info_on_delivery">Nincs kézbesítési információ</string> + <string name="moderated_description">moderált</string> + <string name="member_will_be_removed_from_group_cannot_be_undone">A tag el lesz távolítva a csoportból – ez a művelet nem vonható vissza!</string> + <string name="ensure_xftp_server_address_are_correct_format_and_unique">Győződjön meg arról, hogy a megadott XFTP-kiszolgálók címei megfelelő formátumúak, soronként elkülönítettek, és nincsenek duplikálva.</string> + <string name="no_contacts_selected">Nincs partner kijelölve</string> + <string name="no_received_app_files">Nincsenek fogadott vagy küldött fájlok</string> + <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 hordozható eszköz: koppintson a <b>Megnyitás hordozható eszköz-alkalmazásban</b>, majd koppintson a <b>Kapcsolódás</b> gombra az alkalmazásban.]]></string> + <string name="markdown_in_messages">Markdown az üzenetekben</string> + <string name="group_invitation_item_description">meghívás a(z) %1$s csoportba</string> + <string name="lock_mode">Zárolási mód</string> + <string name="new_mobile_device">Új hordozható eszköz</string> + <string name="v5_2_fix_encryption">Kapcsolatok megtartása</string> + <string name="button_add_members">Tagok meghívása</string> + <string name="message_reactions">Üzenetreakciók</string> + <string name="only_one_device_can_work_at_the_same_time">Egyszerre csak 1 eszköz működhet</string> + <string name="connect_plan_join_your_group">Csatlakozik a csoportjához?</string> + <string name="large_file">Nagy fájl!</string> + <string name="info_row_local_name">Helyi név</string> + <string name="network_and_servers">Hálózat és kiszolgálók</string> + <string name="settings_notification_preview_title">Értesítés előnézete</string> + <string name="v5_4_link_mobile_desktop">Társítsa össze a hordozható eszköz- és számítógépes alkalmazásokat! 🔗</string> + <string name="conn_level_desc_indirect">közvetett (%1$s)</string> + <string name="v4_6_reduced_battery_usage_descr">Hamarosan további fejlesztések érkeznek!</string> + <string name="message_reactions_prohibited_in_this_chat">A reakciók hozzáadása az üzenetekhez le van tiltva ebben a csevegésben.</string> + <string name="incorrect_code">Helytelen biztonsági kód!</string> + <string name="alert_text_fragment_encryption_out_of_sync_old_database">Ez akkor fordulhat elő, ha Ön vagy a partnere régi adatbázis biztonsági mentést használt.</string> + <string name="v5_3_new_desktop_app">Új számítógép-alkalmazás!</string> + <string name="v4_6_group_moderation_descr">Most már az adminisztrátorok is:\n- törölhetik a tagok üzeneteit.\n- letilthatnak tagokat (megfigyelő szerepkör)</string> + <string name="rcv_group_event_member_added">meghívta őt: %1$s</string> + <string name="message_reactions_are_prohibited">A reakciók hozzáadása az üzenetekhez le van tiltva.</string> + <string name="network_use_onion_hosts_no">Nem</string> + <string name="item_info_no_text">nincs szöveg</string> + <string name="member_info_section_title_member">TAG</string> + <string name="onboarding_notifications_mode_subtitle">Hogyan befolyásolja az akkumulátort</string> + <string name="new_member_role">Új tag szerepköre</string> + <string name="la_mode_off">Kikapcsolva</string> + <string name="invalid_contact_link">Érvénytelen hivatkozás!</string> + <string name="new_in_version">Újdonságok a(z) %s verzióban</string> + <string name="smp_servers_invalid_address">Érvénytelen kiszolgálócím!</string> + <string name="thousand_abbreviation">k</string> + <string name="chat_item_ttl_none">soha</string> + <string name="new_desktop"><![CDATA[<i>(új)</i>]]></string> + <string name="ensure_smp_server_address_are_correct_format_and_unique">Győződjön meg arról, hogy a megadott SMP-kiszolgálók címei megfelelő formátumúak, soronként elkülönítettek, és nincsenek duplikálva.</string> + <string name="network_use_onion_hosts_no_desc">Az onion-kiszolgálók nem lesznek használva.</string> + <string name="custom_time_unit_minutes">perc</string> + <string name="learn_more">Tudjon meg többet</string> + <string name="notification_new_contact_request">Új meghívási kérés</string> + <string name="joining_group">Csatlakozás a csoporthoz</string> + <string name="linked_desktop_options">Társított számítógép beállítások</string> + <string name="rcv_group_event_invited_via_your_group_link">meghíva a saját csoporthivatkozásán keresztül</string> + <string name="rcv_group_event_member_left">elhagyta a csoportot</string> + <string name="linked_desktops">Társított számítógépek</string> + <string name="la_no_app_password">Nincs alkalmazás jelkód</string> + <string name="muted_when_inactive">Némítás, ha inaktív!</string> + <string name="alert_title_group_invitation_expired">A meghívó lejárt!</string> + <string name="only_stored_on_members_devices">(csak a csoporttagok tárolják)</string> + <string name="moderate_verb">Moderálás</string> + <string name="chat_preferences_on">bekapcsolva</string> + <string name="v5_1_japanese_portuguese_interface">Japán és portugál kezelőfelület</string> + <string name="message_deletion_prohibited_in_chat">Az üzenetek végleges törlése le van tiltva.</string> + <string name="remote_host_was_disconnected_toast"><![CDATA[Kapcsolat bontva a(z) <b>%s</b> nevű hordozható eszközzel]]></string> + <string name="custom_time_unit_months">hónap</string> + <string name="privacy_message_draft">Üzenetvázlat</string> + <string name="v5_2_disappear_one_message">Egy üzenet eltüntetése</string> + <string name="v4_3_irreversible_message_deletion">Végleges üzenettörlés</string> + <string name="videos_limit_desc">Egyszerre csak 10 videó küldhető el</string> + <string name="only_you_can_add_message_reactions">Csak Ön adhat hozzá reakciókat az üzenetekhez.</string> + <string name="group_member_status_left">elhagyta a csoportot</string> + <string name="message_deletion_prohibited">Az üzenetek végleges törlése le van tiltva ebben a csevegésben.</string> + <string name="v4_3_voice_messages_desc">Max 40 másodperc, azonnal fogadható.</string> + <string name="description_via_contact_address_link_incognito">inkognitó a kapcsolattartási címhivatkozáson keresztül</string> + <string name="network_use_onion_hosts_required_desc">Onion-kiszolgálók szükségesek a kapcsolódáshoz.\nMegjegyzés: .onion cím nélkül nem fog tudni kapcsolódni a kiszolgálókhoz.</string> + <string name="v4_5_italian_interface">Olasz kezelőfelület</string> + <string name="system_restricted_background_in_call_title">Nincsenek háttérhívások</string> + <string name="messages_section_title">Üzenetek</string> + <string name="linked_mobiles">Társított hordozható eszköz</string> + <string name="incognito_info_allows">Lehetővé teszi, hogy egyetlen csevegési profilon belül több névtelen kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük.</string> + <string name="delete_message_mark_deleted_warning">Az üzenet törlésre lesz jelölve. A címzett(ek) képes(ek) lesz(nek) felfedni ezt az üzenetet.</string> + <string name="leave_group_button">Elhagyás</string> + <string name="ok">Rendben</string> + <string name="no_filtered_chats">Nincsenek szűrt csevegések</string> + <string name="invalid_data">érvénytelen adat</string> + <string name="ensure_ICE_server_address_are_correct_format_and_unique">Győződjön meg arról, hogy a megadott WebRTC ICE-kiszolgálók címei megfelelő formátumúak, soronként elkülönítettek, és nincsenek duplikálva.</string> + <string name="only_owners_can_enable_files_and_media">Csak a csoport tulajdonosai engedélyezhetik a fájlok- és a médiatartalmak küldését.</string> + <string name="loading_remote_file_title">Fájl betöltése…</string> + <string name="no_contacts_to_add">Nincs hozzáadandó partner</string> + <string name="v4_5_message_draft">Üzenetvázlat</string> + <string name="display_name_invited_to_connect">Függőben lévő meghívó</string> + <string name="add_contact">Egyszer használható meghívó</string> + <string name="notifications">Értesítések</string> + <string name="images_limit_desc">Egyszerre csak 10 kép küldhető el</string> + <string name="feature_offered_item_with_param">ajánlotta: %s, ekkor: %2s</string> + <string name="not_compatible">Nem kompatibilis!</string> + <string name="make_profile_private">Tegye priváttá a profilját!</string> + <string name="message_delivery_error_title">Üzenetkézbesítési hiba</string> + <string name="v4_5_multiple_chat_profiles">Több csevegési profil</string> + <string name="marked_deleted_description">törlésre jelölve</string> + <string name="user_mute">Némítás</string> + <string name="link_a_mobile">Hordozható eszköz társítása</string> + <string name="settings_notifications_mode_title">Értesítési szolgáltatás</string> + <string name="only_group_owners_can_enable_voice">Csak a csoport tulajdonosai engedélyezhetik a hangüzenetek küldését.</string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">A felhasználói profilok, partnerek, csoportok és üzenetek csak az eszközön vannak tárolva a kliensen belül.</string> + <string name="invalid_migration_confirmation">Érvénytelen átköltöztetési visszaigazolás</string> + <string name="only_group_owners_can_change_prefs">Csak a csoport tulajdonosai módosíthatják a csoportbeállításokat.</string> + <string name="no_history">Nincsenek előzmények</string> + <string name="invalid_QR_code">Érvénytelen QR-kód</string> + <string name="mark_read">Megjelölés olvasottként</string> + <string name="live">ÉLŐ</string> + <string name="mark_unread">Megjelölés olvasatlanként</string> + <string name="icon_descr_more_button">Továbbiak</string> + <string name="auth_log_in_using_credential">Bejelentkezés hitelesítőadatokkal</string> + <string name="invalid_message_format">érvénytelen üzenet-formátum</string> + <string name="join_group_button">Csatlakozás</string> + <string name="shutdown_alert_desc">Az értesítések az alkalmazás elindításáig nem fognak működni</string> + <string name="chat_preferences_off">kikapcsolva</string>` + <string name="this_device_version"><![CDATA[<i>(ez az eszköz: v%s)</i>]]></string> + <string name="feature_offered_item">%s ajánlotta</string> + <string name="button_leave_group">Csoport elhagyása</string> + <string name="unblock_member_desc">%s összes üzenete meg fog jelenni!</string> + <string name="alert_text_skipped_messages_it_can_happen_when">Ez akkor fordulhat elő, ha:\n1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak.\n2. Nem sikerült visszafejteni az üzenetet, mert Ön, vagy a partnere régebbi adatbázis biztonsági mentést használt.\n3. A kapcsolat sérült.</string> + <string name="group_member_role_observer">megfigyelő</string> + <string name="description_via_group_link_incognito">inkognitó a csoporthivatkozáson keresztül</string> + <string name="network_use_onion_hosts_prefer_desc">Onion-kiszolgálók használata, ha azok rendelkezésre állnak.</string> + <string name="invite_friends">Barátok meghívása</string> + <string name="color_surface">Menük és figyelmeztetések</string> + <string name="icon_descr_add_members">Tagok meghívása</string> + <string name="group_preview_join_as">csatlakozás mint %s</string> + <string name="no_selected_chat">Nincs csevegés kijelölve</string> + <string name="users_delete_data_only">Csak helyi profiladatok</string> + <string name="description_via_one_time_link_incognito">inkognitó egy egyszer használható meghívón keresztül</string> + <string name="share_text_moderated_at">Moderálva: %s</string> + <string name="one_time_link">Egyszer használható meghívó</string> + <string name="invalid_name">Érvénytelen név!</string> + <string name="email_invite_subject">Beszélgessünk a SimpleX Chatben</string> + <string name="info_row_moderated_at">Moderálva</string> + <string name="v4_4_live_messages">Élő üzenetek</string> + <string name="mark_code_verified">Hitelesítés</string> + <string name="v5_2_message_delivery_receipts">Üzenetkézbesítési jelentések!</string> + <string name="image_descr_link_preview">hivatkozás előnézeti képe</string> + <string name="leave_group_question">Elhagyja a csoportot?</string> + <string name="chat_preferences_no">nem</string> + <string name="v4_5_reduced_battery_usage_descr">Hamarosan további fejlesztések érkeznek!</string> + <string name="feature_off">kikapcsolva</string> + <string name="install_simplex_chat_for_terminal">SimpleX Chat telepítése a terminálhoz</string> + <string name="self_destruct_new_display_name">Új megjelenítendő név:</string> + <string name="new_passphrase">Új jelmondat…</string> + <string name="callstatus_missed">nem fogadott hívás</string> + <string name="database_migrations">Átköltöztetés: %s</string> + <string name="in_reply_to">Válaszul erre</string> + <string name="notification_preview_mode_message">Név és üzenet</string> + <string name="notifications_will_be_hidden">Az értesítések csak az alkalmazás bezárásáig érkeznek!</string> + <string name="info_menu">Információ</string> + <string name="settings_section_title_messages">ÜZENETEK ÉS FÁJLOK</string> + <string name="group_member_role_member">tag</string> + <string name="make_private_connection">Privát kapcsolat létrehozása</string> + <string name="moderated_item_description">moderálva lett %s által</string> + <string name="import_theme_error_desc">Győződjön meg arról, hogy a fájl helyes YAML-szintaxist tartalmaz. Exportálja a témát, hogy legyen egy példa a témafájl szerkezetére.</string> + <string name="italic_text">dőlt</string> + <string name="non_content_uri_alert_title">Érvénytelen a fájl elérési útvonala</string> + <string name="connect_via_group_link">Csatlakozik a csoporthoz?</string> + <string name="status_no_e2e_encryption">nincs e2e titkosítás</string> + <string name="new_database_archive">Új adatbázis-archívum</string> + <string name="live_message">Élő üzenet!</string> + <string name="invite_to_group_button">Meghívás a csoportba</string> + <string name="lock_after">Zárolás miután</string> + <string name="incoming_audio_call">Bejövő hanghívás</string> + <string name="keychain_error">Kulcstartóhiba</string> + <string name="join_group_question">Csatlakozik a csoporthoz?</string> + <string name="incognito_info_protects">Az inkognitómód úgy védi a személyes adatait, hogy az összes partneréhez új, véletlenszerű profilt használ.</string> + <string name="v5_2_more_things_descr">- stabilabb üzenetkézbesítés.\n- picit továbbfejlesztett csoportok.\n- és még sok más!</string> + <string name="v5_1_message_reactions">Üzenetreakciók</string> + <string name="no_connected_mobile">Nincs társított hordozható eszköz</string> + <string name="network_status">Hálózat állapota</string> + <string name="new_passcode">Új jelkód</string> + <string name="message_delivery_error_desc">Valószínűleg ez a partner törölte Önnel a kapcsolatot.</string> + <string name="join_group_incognito_button">Csatlakozás inkognitóban</string> + <string name="open_chat">Csevegés megnyitása</string> + <string name="callstatus_rejected">elutasított hívás</string> + <string name="onboarding_notifications_mode_periodic">Időszakos</string> + <string name="feature_received_prohibited">fogadott, tiltott</string> + <string name="connect_plan_repeat_connection_request">Megismétli a meghívási kérést?</string> + <string name="only_you_can_delete_messages">Véglegesen csak Ön törölhet üzeneteket (partnere csak törlésre jelölheti meg őket ). (24 óra)</string> + <string name="role_in_group">Szerepkör</string> + <string name="simplex_link_contact">SimpleX kapcsolattartási cím</string> + <string name="stop_file__confirm">Megállítás</string> + <string name="smp_servers_preset_server">Előre beállított kiszolgáló</string> + <string name="add_contact_or_create_group">Új csevegés indítása</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Bárki üzemeltethet kiszolgálókat.</string> + <string name="rcv_group_event_open_chat">Megnyitás</string> + <string name="network_option_protocol_timeout">Protokoll időtúllépése</string> + <string name="secret_text">titok</string> + <string name="settings_notification_preview_mode_title">Értesítés előnézete</string> + <string name="callstate_waiting_for_confirmation">várakozás a visszaigazolásra…</string> + <string name="stop_file__action">Fájl megállítása</string> + <string name="description_via_group_link">a csoporthivatkozáson keresztül</string> + <string name="network_option_ping_interval">Időtartam a PING-ek között</string> + <string name="send_disappearing_message">Eltűnő üzenet küldése</string> + <string name="self_destruct_passcode">Önmegsemmisítő-jelkód</string> + <string name="save_and_update_group_profile">Mentés és a csoportprofil frissítése</string> + <string name="your_privacy">Adatvédelem</string> + <string name="your_simplex_contact_address">Profil SimpleX-címe</string> + <string name="alert_text_fragment_please_report_to_developers">Jelentse a fejlesztőknek.</string> + <string name="people_can_connect_only_via_links_you_share">Ön dönti el, hogy kivel beszélget.</string> + <string name="prohibit_sending_disappearing">Az eltűnő üzenetek küldése le van tiltva.</string> + <string name="only_you_can_send_voice">Csak Ön tud hangüzeneteket küldeni.</string> + <string name="update_network_settings_confirmation">Frissítés</string> + <string name="icon_descr_video_snd_complete">Videó elküldve</string> + <string name="update_database_passphrase">Az adatbázis jelmondatának módosítása</string> + <string name="turn_off_system_restriction_button">Alkalmazás beállítások megnyitása</string> + <string name="passcode_not_changed">A jelkód nem módosult!</string> + <string name="refresh_qr_code">Frissítés</string> + <string name="custom_time_picker_select">Kijelölés</string> + <string name="only_you_can_make_calls">Csak Ön tud hívásokat indítani.</string> + <string name="smp_server_test_secure_queue">Biztonságos sorba állítás</string> + <string name="rate_the_app">Értékelje az alkalmazást</string> + <string name="share_invitation_link">Egyszer használható meghívó megosztása</string> + <string name="database_restore_error">Hiba történt az adatbázis visszaállításakor</string> + <string name="group_members_2">%s és %s</string> + <string name="chat_preferences_you_allow">Ön engedélyezi</string> + <string name="v4_5_reduced_battery_usage">Csökkentett akkumulátor-használat</string> + <string name="save_and_notify_contacts">Mentés és a partnerek értesítése</string> + <string name="group_welcome_preview">Előnézet</string> + <string name="use_chat">SimpleX Chat használata</string> + <string name="share_verb">Megosztás</string> + <string name="received_message">Fogadott üzenet</string> + <string name="button_welcome_message">Üdvözlőüzenet</string> + <string name="rcv_group_event_n_members_connected">%s, %s és további %d tag kapcsolódott</string> + <string name="only_your_contact_can_make_calls">Csak a partnere tud hívást indítani.</string> + <string name="settings_section_title_themes">TÉMÁK</string> + <string name="videos_limit_title">Túl sok videó!</string> + <string name="welcome">Üdvözöljük!</string> + <string name="v5_1_self_destruct_passcode">Önmegsemmisítő-jelkód</string> + <string name="connect_via_link_or_qr_from_clipboard_or_in_person">(beolvasás, vagy beillesztés a vágólapról)</string> + <string name="waiting_for_video">Várakozás a videóra</string> + <string name="reply_verb">Válasz</string> + <string name="connect_plan_this_is_your_own_one_time_link">Ez a saját egyszer használható meghívója!</string> + <string name="ntf_channel_calls">SimpleX Chat hívások</string> + <string name="connect_use_new_incognito_profile">Új inkognitóprofil használata</string> + <string name="contact_developers">Frissítse az alkalmazást, és lépjen kapcsolatba a fejlesztőkkel.</string> + <string name="theme_simplex">SimpleX</string> + <string name="send_link_previews">Hivatkozás előnézete</string> + <string name="rcv_conn_event_verification_code_reset">a biztonsági kód módosult</string> + <string name="notification_preview_mode_contact_desc">Csak a partner nevének megjelenítése</string> + <string name="icon_descr_speaker_on">Hangszóró bekapcsolva</string> + <string name="restart_the_app_to_use_imported_chat_database">Importált csevegési adatbázis használatához indítsa újra az alkalmazást.</string> + <string name="icon_descr_sent_msg_status_unauthorized_send">jogosulatlan küldés</string> + <string name="only_your_contact_can_send_voice">Csak a partnere tud hangüzeneteket küldeni.</string> + <string name="icon_descr_settings">Beállítások</string> + <string name="scan_qr_to_connect_to_contact">A kapcsolódáshoz a partnere beolvashatja a QR-kódot, vagy használhatja az alkalmazásban található hivatkozást.</string> + <string name="callstate_received_confirmation">visszaigazolás fogadása…</string> + <string name="scan_code_from_contacts_app">Biztonsági kód beolvasása a partnere alkalmazásából.</string> + <string name="observer_cant_send_message_desc">Lépjen kapcsolatba a csoport adminisztrátorával.</string> + <string name="icon_descr_video_on">Videó bekapcsolva</string> + <string name="display_name__field">Profilnév:</string> + <string name="paste_button">Beillesztés</string> + <string name="thank_you_for_installing_simplex">Köszönjük, hogy telepítette a SimpleX Chatet!</string> + <string name="star_on_github">Csillagozás a GitHubon</string> + <string name="remove_member_confirmation">Eltávolítás</string> + <string name="search_verb">Keresés</string> + <string name="sync_connection_force_question">Újraegyezteti a titkosítást?</string> + <string name="self_destruct_passcode_enabled">Az önmegsemmisítő-jelkód engedélyezve!</string> + <string name="v4_2_security_assessment">Biztonsági kiértékelés</string> + <string name="color_title">Cím</string> + <string name="icon_descr_send_message">Üzenet elküldése</string> + <string name="restore_database">Adatbázismentés visszaállítása</string> + <string name="revoke_file__confirm">Visszavonás</string> + <string name="ask_your_contact_to_enable_voice">Kérje meg a partnerét, hogy engedélyezze a hangüzenetek küldését.</string> + <string name="description_you_shared_one_time_link">Ön egy egyszer használható meghívót osztott meg</string> + <string name="simplex_link_mode_browser_warning">A hivatkozás megnyitása a böngészőben gyengítheti az adatvédelmet és a biztonságot. A megbízhatatlan SimpleX-hivatkozások pirossal vannak kiemelve.</string> + <string name="your_ice_servers">Saját ICE-kiszolgálók</string> + <string name="you_accepted_connection">Kapcsolat létrehozása</string> + <string name="reject_contact_button">Elutasítás</string> + <string name="notification_preview_mode_message_desc">Partner nevének és az üzenet tartalmának megjelenítése</string> + <string name="settings_section_title_settings">BEÁLLÍTÁSOK</string> + <string name="save_profile_password">Profiljelszó mentése</string> + <string name="stop_snd_file__title">Megállítja a fájlküldést?</string> + <string name="unlink_desktop_question">Leválasztja a számítógépet?</string> + <string name="voice_messages_prohibited">A hangüzenetek le vannak tiltva!</string> + <string name="compose_send_direct_message_to_connect">Közvetlen üzenet küldése a kapcsolódáshoz</string> + <string name="network_option_ping_count">PING-ek száma</string> + <string name="show_developer_options">Fejlesztői beállítások megjelenítése</string> + <string name="rcv_group_event_1_member_connected">%s kapcsolódott</string> + <string name="theme_system">Rendszer</string> + <string name="network_use_onion_hosts_prefer">Amikor elérhető</string> + <string name="voice_message_with_duration">Hangüzenet (%1$s)</string> + <string name="current_version_timestamp">%s (jelenlegi)</string> + <string name="smp_servers_your_server">Saját SMP-kiszolgáló</string> + <string name="random_port">Véletlen</string> + <string name="share_with_contacts">Megosztás a partnerekkel</string> + <string name="sender_you_pronoun">Ön</string> + <string name="you_have_no_chats">Nincsenek csevegései</string> + <string name="send_disappearing_message_send">Küldés</string> + <string name="chat_item_ttl_seconds">%s másodperc</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="system_restricted_background_desc">A SimpleX nem tud a háttérben futni. Csak akkor fog értesítéseket kapni, amikor az alkalmazás meg van nyitva.</string> + <string name="images_limit_title">Túl sok kép!</string> + <string name="group_members_n">%s, %s és további %d tag</string> + <string name="auth_stop_chat">Csevegési szolgáltatás megállítása</string> + <string name="simplex_link_mode">SimpleX-hivatkozások</string> + <string name="v4_4_disappearing_messages_desc">Az elküldött üzenetek törölve lesznek a beállított idő után.</string> + <string name="user_unmute">Némítás megszüntetése</string> + <string name="share_text_sent_at">Elküldve: %s</string> + <string name="connect_use_current_profile">Jelenlegi profil használata</string> + <string name="this_device">Ez az eszköz</string> + <string name="share_address_with_contacts_question">Megosztja a címet a partnereivel?</string> + <string name="profile_password">Profiljelszó</string> + <string name="theme">Téma</string> + <string name="remove_passphrase_from_settings">Eltávolítja a jelmondatot a beállításokból?</string> + <string name="simplex_link_group">SimpleX-csoporthivatkozás</string> + <string name="icon_descr_waiting_for_image">Várakozás a képre</string> + <string name="self_destruct">Önmegsemmisítés</string> + <string name="callstate_waiting_for_answer">várakozás a válaszra…</string> + <string name="text_field_set_contact_placeholder">Partner nevének beállítása…</string> + <string name="unblock_member_button">Tag feloldása</string> + <string name="scan_QR_code">QR-kód beolvasása</string> + <string name="smp_servers_test_server">Kiszolgáló tesztelése</string> + <string name="send_us_an_email">Küldjön nekünk e-mailt</string> + <string name="conn_stats_section_title_servers">KISZOLGÁLÓK</string> + <string name="smp_servers_test_servers">Kiszolgálók tesztelése</string> + <string name="la_lock_mode_passcode">Jelkód bevitele</string> + <string name="la_mode_system">Rendszer</string> + <string name="submit_passcode">Elküldés</string> + <string name="security_code">Biztonsági kód</string> + <string name="enter_correct_current_passphrase">Adja meg a helyes, jelenlegi jelmondatot.</string> + <string name="prohibit_message_deletion">Az elküldött üzenetek végleges törlése le van tiltva.</string> + <string name="prohibit_message_reactions">A reakciók hozzáadása az üzenethez le van tiltva.</string> + <string name="use_random_passphrase">Véletlenszerű jelmondat használata</string> + <string name="call_connection_peer_to_peer">egyenrangú</string> + <string name="run_chat_section">CSEVEGÉSI SZOLGÁLTATÁS INDÍTÁSA</string> + <string name="paste_the_link_you_received">Kapott hivatkozás beillesztése</string> + <string name="smp_save_servers_question">Menti a kiszolgálókat?</string> + <string name="v4_2_security_assessment_desc">A SimpleX Chat biztonsága a Trail of Bits által lett auditálva.</string> + <string name="rcv_group_event_updated_group_profile">frissítette a csoport profilját</string> + <string name="settings_section_title_support">SIMPLEX CHAT TÁMOGATÁSA</string> + <string name="simplex_service_notification_title">SimpleX Chat szolgáltatás</string> + <string name="observer_cant_send_message_title">Nem lehet üzeneteket küldeni!</string> + <string name="is_verified">%s hitelesítve</string> + <string name="password_to_show">Jelszó a megjelenítéshez</string> + <string name="privacy_and_security">Adatvédelem és biztonság</string> + <string name="button_remove_member">Eltávolítás</string> + <string name="passcode_set">A jelkód beállítva!</string> + <string name="sent_message">Elküldött üzenet</string> + <string name="select_contacts">Partnerek kijelölése</string> + <string name="unknown_message_format">ismeretlen üzenetformátum</string> + <string name="smp_servers_save">Kiszolgálók mentése</string> + <string name="group_welcome_title">Üdvözlőüzenet</string> + <string name="network_option_seconds_label">mp</string> + <string name="profile_update_will_be_sent_to_contacts">A profilfrissítés el lesz küldve a partnerei számára.</string> + <string name="v5_3_simpler_incognito_mode">Egyszerűsített inkognitómód</string> + <string name="save_welcome_message_question">Menti az üdvözlőüzenetet?</string> + <string name="restart_the_app_to_create_a_new_chat_profile">Új csevegési fiók létrehozásához indítsa újra az alkalmazást.</string> + <string name="toast_permission_denied">Engedély megtagadva!</string> + <string name="icon_descr_call_pending_sent">Függőben lévő hívás</string> + <string name="opening_database">Adatbázis megnyitása…</string> + <string name="shutdown_alert_question">Leállítja az alkalmazást?</string> + <string name="enter_passphrase_notification_title">Jelmondat szükséges</string> + <string name="onboarding_notifications_mode_title">Privát értesítések</string> + <string name="you_invited_a_contact">Ön meghívta egy partnerét</string> + <string name="is_not_verified">%s nincs hitelesítve</string> + <string name="contact_tap_to_connect">Koppintson ide a kapcsolódáshoz</string> + <string name="this_device_name">Ennek az eszköznek a neve</string> + <string name="your_current_profile">Jelenlegi profil</string> + <string name="smp_server_test_upload_file">Fájl feltöltése</string> + <string name="prohibit_calls">A hívások kezdeményezése le van tiltva.</string> + <string name="network_use_onion_hosts_required">Szükséges</string> + <string name="ntf_channel_messages">SimpleX Chat üzenetek</string> + <string name="restore_database_alert_confirm">Visszaállítás</string> + <string name="setup_database_passphrase">Adatbázis-jelmondat beállítása</string> + <string name="color_sent_message">Üzenetbuborék színe</string> + <string name="notifications_mode_periodic">Időszakosan indul</string> + <string name="connect_plan_this_is_your_own_simplex_address">Ez a saját SimpleX-címe!</string> + <string name="group_member_status_removed">eltávolítva</string> + <string name="share_link">Megosztás</string> + <string name="icon_descr_simplex_team">SimpleX csapat</string> + <string name="image_descr_profile_image">profilkép</string> + <string name="your_chat_profiles">Csevegési profilok</string> + <string name="group_member_role_owner">tulajdonos</string> + <string name="la_notice_turn_on">Bekapcsolás</string> + <string name="rcv_group_event_3_members_connected">%s, %s és %s kapcsolódott</string> + <string name="simplex_link_invitation">Egyszer használható SimpleX-meghívó</string> + <string name="your_calls">Hívások</string> + <string name="icon_descr_sent_msg_status_send_failed">nem sikerült elküldeni</string> + <string name="theme_colors_section_title">KEZELŐFELÜLET SZÍNEI</string> + <string name="restore_database_alert_desc">Adja meg a korábbi jelszót az adatbázis biztonsági mentésének visszaállítása után. Ez a művelet nem vonható vissza.</string> + <string name="color_secondary">Másodlagos szín</string> + <string name="settings_section_title_socks">SOCKS-PROXY</string> + <string name="save_servers_button">Mentés</string> + <string name="settings_restart_app">Újraindítás</string> + <string name="smp_servers">SMP-kiszolgálók</string> + <string name="video_descr">Videó</string> + <string name="save_auto_accept_settings">Automatikus elfogadási beállítások mentése</string> + <string name="sync_connection_force_confirm">Újraegyeztetés</string> + <string name="icon_descr_waiting_for_video">Várakozás a videóra</string> + <string name="your_XFTP_servers">Saját XFTP-kiszolgálók</string> + <string name="icon_descr_video_off">Videó kikapcsolva</string> + <string name="v4_5_private_filenames">Privát fájlnevek</string> + <string name="save_settings_question">Menti a beállításokat?</string> + <string name="la_mode_passcode">Jelkód</string> + <string name="unknown_error">Ismeretlen hiba</string> + <string name="smp_servers_your_server_address">Saját SMP-kiszolgálójának címe</string> + <string name="auth_open_chat_console">Csevegési konzol megnyitása</string> + <string name="remove_member_button">Eltávolítás</string> + <string name="set_database_passphrase">Adatbázis-jelmondat beállítása</string> + <string name="view_security_code">Biztonsági kód megtekintése</string> + <string name="unblock_member_question">Feloldja a tag letiltását?</string> + <string name="sender_may_have_deleted_the_connection_request">A küldője törölhette a meghívási kérést.</string> + <string name="wrong_passphrase">Érvénytelen adatbázis-jelmondat</string> + <string name="your_SMP_servers">Saját SMP-kiszolgálók</string> + <string name="send_receipts_disabled_alert_title">A kézbesítési jelentések le vannak tiltva</string> + <string name="open_database_folder">Adatbázismappa megnyitása</string> + <string name="description_via_one_time_link">egy egyszer használható meghívón keresztül</string> + <string name="set_group_preferences">Csoportbeállítások megadása</string> + <string name="simplex_link_connection">a következőn keresztül: %1$s</string> + <string name="chat_preferences_yes">igen</string> + <string name="voice_message">Hangüzenet</string> + <string name="settings_section_title_use_from_desktop">Társítás számítógéppel</string> + <string name="settings_section_title_you">PROFIL</string> + <string name="network_proxy_port">%d-s port</string> + <string name="to_connect_via_link_title">Kapcsolódás egy hivatkozáson keresztül</string> + <string name="share_address">Cím megosztása</string> + <string name="smp_servers_scan_qr">A kiszolgáló QR-kódjának beolvasása</string> + <string name="stop_chat_confirmation">Megállítás</string> + <string name="stop_sharing_address">Megállítja a címmegosztást?</string> + <string name="auth_open_chat_profiles">Csevegési profilok módosítása</string> + <string name="connect_plan_repeat_join_request">Megismétli a meghívási kérést?</string> + <string name="waiting_for_image">Várakozás a képre</string> + <string name="v4_3_voice_messages">Hangüzenetek</string> + <string name="button_remove_member_question">Eltávolítja a tagot?</string> + <string name="verify_security_code">Biztonsági kód hitelesítése</string> + <string name="rcv_group_event_user_deleted">eltávolította Önt</string> + <string name="simplex_address">SimpleX-cím</string> + <string name="show_dev_options">Megjelenítve:</string> + <string name="callstate_received_answer">válasz fogadása…</string> + <string name="restore_database_alert_title">Visszaállítja az adatbázismentést?</string> + <string name="simplex_service_notification_text">Üzenetek fogadása…</string> + <string name="rcv_group_event_2_members_connected">%s és %s kapcsolódott</string> + <string name="you_are_observer">Ön megfigyelő</string> + <string name="port_verb">Port</string> + <string name="set_passcode">Jelkód beállítása</string> + <string name="whats_new">Újdonságok</string> + <string name="connect_plan_open_group">Csoport megnyitása</string> + <string name="info_row_sent_at">Elküldve</string> + <string name="prohibit_sending_voice">A hangüzenetek küldése le van tiltva.</string> + <string name="privacy_show_last_messages">Legutóbbi üzenet előnézetének megjelenítése</string> + <string name="smp_servers_preset_address">Az előre beállított kiszolgáló címe</string> + <string name="periodic_notifications_disabled">Időszakos értesítések letiltva!</string> + <string name="passcode_changed">A jelkód módosult!</string> + <string name="notifications_mode_off">Akkor fut, amikor az alkalmazás meg van nyitva</string> + <string name="this_QR_code_is_not_a_link">Ez a QR-kód nem egy hivatkozás!</string> + <string name="waiting_for_file">Várakozás a fájlra</string> + <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> + <string name="unlink_desktop">Szétkapcsolás</string> + <string name="incognito_random_profile">Véletlenszerű profil</string> + <string name="wrong_passphrase_title">Érvénytelen jelmondat!</string> + <string name="prohibit_message_reactions_group">A reakciók hozzáadása az üzenetekhez le van tiltva.</string> + <string name="language_system">Rendszer</string> + <string name="icon_descr_received_msg_status_unread">olvasatlan</string> + <string name="icon_descr_server_status_pending">Függőben</string> + <string name="personal_welcome">Üdvözöljük %1$s!</string> + <string name="remove_passphrase_from_keychain">Eltávolítja a jelmondatot a Keystrore-ból?</string> + <string name="auth_unlock">Feloldás</string> + <string name="prohibit_sending_disappearing_messages">Az eltűnő üzenetek küldése le van tiltva.</string> + <string name="gallery_video_button">Videó</string> + <string name="update_database">Frissítés</string> + <string name="open_verb">Megnyitás</string> + <string name="periodic_notifications">Időszakos értesítések</string> + <string name="alert_title_skipped_messages">Kihagyott üzenetek</string> + <string name="prohibit_sending_voice_messages">A hangüzenetek küldése le van tiltva.</string> + <string name="set_contact_name">Partner nevének beállítása</string> + <string name="only_you_can_send_disappearing">Csak Ön tud eltűnő üzeneteket küldeni.</string> + <string name="share_image">Médiatartalom megosztása…</string> + <string name="group_info_member_you">Ön: %1$s</string> + <string name="your_preferences">Beállítások</string> + <string name="reset_color">Színek visszaállítása</string> + <string name="network_options_save">Mentés</string> + <string name="switch_verb">Váltás</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">A kapott hivatkozás beillesztése a partnerhez való kapcsolódáshoz…</string> + <string name="scan_code">Beolvasás</string> + <string name="open_port_in_firewall_title">Port nyitása a tűzfalban</string> + <string name="callstate_starting">indítás…</string> + <string name="settings_shutdown">Leállítás</string> + <string name="icon_descr_sent_msg_status_sent">elküldve</string> + <string name="network_socks_toggle_use_socks_proxy">SOCKS-proxy használata</string> + <string name="send_live_message">Élő üzenet küldése</string> + <string name="privacy_redefined">Újraértelmezett adatvédelem</string> + <string name="voice_message_send_text">Hangüzenet…</string> + <string name="protect_app_screen">Alkalmazás képernyőjének védelme</string> + <string name="show_QR_code">QR-kód megjelenítése</string> + <string name="icon_descr_video_call">videóhívás</string> + <string name="unfavorite_chat">Kedvenc megszüntetése</string> + <string name="send_receipts">Kézbesítési jelentések küldése</string> + <string name="icon_descr_address">SimpleX-cím</string> + <string name="chat_help_tap_button">Koppintson a</string> + <string name="save_and_notify_contact">Mentés és a partner értesítése</string> + <string name="icon_descr_call_rejected">Elutasított hívás</string> + <string name="network_socks_proxy_settings">SOCKS-proxybeállítások</string> + <string name="image_descr_qr_code">QR-kód</string> + <string name="renegotiate_encryption">Titkosítás újraegyeztetése</string> + <string name="remove_passphrase">Eltávolítás</string> + <string name="network_use_onion_hosts">Onion-kiszolgálók használata</string> + <string name="reveal_verb">Felfedés</string> + <string name="la_lock_mode">Zárolási mód</string> + <string name="revoke_file__action">Fájl visszavonása</string> + <string name="xftp_servers">XFTP-kiszolgálók</string> + <string name="prohibit_sending_files">A fájlok- és a médiatartalmak küldése le van tiltva.</string> + <string name="share_file">Fájl megosztása…</string> + <string name="save_verb">Mentés</string> + <string name="call_connection_via_relay">egy továbbítókiszolgálón keresztül</string> + <string name="stop_sharing">Megosztás megállítása</string> + <string name="snd_group_event_member_deleted">Ön eltávolította őt: %1$s</string> + <string name="save_passphrase_and_open_chat">Jelmondat mentése és a csevegés megnyitása</string> + <string name="save_preferences_question">Menti a beállításokat?</string> + <string name="first_platform_without_user_ids">Nincsenek felhasználó-azonosítók.</string> + <string name="prohibit_direct_messages">A közvetlen üzenetek küldése a tagok között le van tiltva.</string> + <string name="network_enable_socks">SOCKS-proxy használata?</string> + <string name="icon_descr_speaker_off">Hangszóró kikapcsolva</string> + <string name="custom_time_unit_weeks">hét</string> + <string name="show_call_on_lock_screen">Megjelenítés</string> + <string name="webrtc_ice_servers">WebRTC ICE-kiszolgálók</string> + <string name="revoke_file__title">Visszavonja a fájlt?</string> + <string name="button_send_direct_message">Közvetlen üzenet küldése</string> + <string name="reject">Elutasítás</string> + <string name="send_verb">Küldés</string> + <string name="la_lock_mode_system">Rendszer-hitelesítés</string> + <string name="simplex_link_mode_browser">Böngészőn keresztül</string> + <string name="v4_6_hidden_chat_profiles_descr">Védje meg a csevegési profiljait egy jelszóval!</string> + <string name="only_your_contact_can_send_disappearing">Csak a partnere tud eltűnő üzeneteket küldeni.</string> + <string name="your_ICE_servers">Saját ICE-kiszolgálók</string> + <string name="scan_qr_code_from_desktop">QR-kód beolvasása a számítógépről</string> + <string name="image_descr_simplex_logo">SimpleX logó</string> + <string name="unblock_member_confirmation">Feloldás</string> + <string name="unmute_chat">Némítás megszüntetése</string> + <string name="open_simplex_chat_to_accept_call">SimpleX Chat megnyitása a hívás fogadásához</string> + <string name="stop_rcv_file__title">Megállítja a fájlfogadást?</string> + <string name="v5_4_more_things_descr">- 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!</string> + <string name="v5_0_polish_interface">Lengyel kezelőfelület</string> + <string name="smp_servers_use_server">Kiszolgáló használata</string> + <string name="share_text_received_at">Fogadva: %s</string> + <string name="la_notice_title_simplex_lock">SimpleX-zár</string> + <string name="save_and_notify_group_members">Mentés és a csoporttagok értesítése</string> + <string name="reset_verb">Visszaállítás</string> + <string name="only_your_contact_can_add_message_reactions">Csak a partnere adhat hozzá reakciókat az üzenetekhez.</string> + <string name="voice_messages">Hangüzenetek</string> + <string name="snd_group_event_user_left">Ön elhagyta a csoportot</string> + <string name="icon_descr_record_voice_message">Hangüzenet rögzítése</string> + <string name="auth_simplex_lock_turned_on">SimpleX-zár bekapcsolva</string> + <string name="member_contact_send_direct_message">közvetlen üzenet küldése</string> + <string name="scan_from_mobile">Beolvasás hordozható eszközről</string> + <string name="verify_connections">Kapcsolatok hitelesítése</string> + <string name="share_message">Üzenet megosztása…</string> + <string name="custom_time_unit_seconds">másodperc</string> + <string name="lock_not_enabled">A SimpleX-zár nincs bekapcsolva!</string> + <string name="chat_lock">SimpleX-zár</string> + <string name="your_settings">Beállítások</string> + <string name="your_chat_database">Csevegési adatbázis</string> + <string name="rcv_group_event_member_deleted">eltávolította őt: %1$s</string> + <string name="smp_servers_test_failed">Sikertelen kiszolgáló teszt!</string> + <string name="verify_connection">Kapcsolat hitelesítése</string> + <string name="whats_new_read_more">Tudjon meg többet</string> + <string name="sender_cancelled_file_transfer">A fájl küldője visszavonta az átvitelt.</string> + <string name="stop_chat_question">Megállítja a csevegést?</string> + <string name="info_row_received_at">Fogadva</string> + <string name="accept_feature_set_1_day">Beállítva 1 nap</string> + <string name="user_unhide">Felfedés</string> + <string name="color_received_message">Fogadott üzenetbuborék színe</string> + <string name="only_your_contact_can_delete">Csak a partnere tudja az üzeneteket véglegesen törölni (Ön csak törlésre jelölheti meg azokat). (24 óra)</string> + <string name="self_destruct_passcode_changed">Az önmegsemmisítő-jelkód módosult!</string> + <string name="using_simplex_chat_servers">SimpleX Chat-kiszolgálók használatban.</string> + <string name="use_simplex_chat_servers__question">SimpleX Chat-kiszolgálók használata?</string> + <string name="unhide_chat_profile">Csevegési profil felfedése</string> + <string name="v5_0_large_files_support">Videók és fájlok legfeljebb 1GB méretig</string> + <string name="network_option_tcp_connection_timeout">TCP-kapcsolat időtúllépése</string> + <string name="connect__your_profile_will_be_shared">A(z) %1$s nevű profilja meg lesz osztva.</string> + <string name="you_are_already_connected_to_vName_via_this_link">Ön már kapcsolódott a következőhöz: %1$s.</string> + <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">A jelenlegi csevegési adatbázis TÖRÖLVE és CSERÉLVE lesz az importáltra!\nEz a művelet nem vonható vissza – profiljai, partnerei, csevegési üzenetei és fájljai véglegesen törölve lesznek.</string> + <string name="chat_with_the_founder">Ötletek és javaslatok</string> + <string name="database_downgrade_warning">Figyelmeztetés: néhány adat elveszhet!</string> + <string name="tap_to_start_new_chat">Koppintson ide az új csevegés indításához</string> + <string name="waiting_for_desktop">Várakozás a számítógépre…</string> + <string name="next_generation_of_private_messaging">Az üzenetváltás jövője</string> + <string name="update_network_settings_question">Módosítja a hálózati beállításokat?</string> + <string name="waiting_for_mobile_to_connect">Várakozás a hordozható eszköz társítására:</string> + <string name="v4_4_verify_connection_security">Biztonságos kapcsolat hitelesítése</string> + <string name="sending_files_not_yet_supported">fájlok küldése egyelőre még nem támogatott</string> + <string name="snd_conn_event_switch_queue_phase_completed_for_member">Ön módosította a címet %s számára</string> + <string name="receiving_files_not_yet_supported">fájlok fogadása egyelőre még nem támogatott</string> + <string name="save_group_profile">Csoportprofil mentése</string> + <string name="network_options_reset_to_defaults">Visszaállítás alapértelmezettre</string> + <string name="connection_error_auth_desc">Hacsak a partnere nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt egyszer, lehet hogy ez egy hiba – jelentse a problémát.\nA kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcsolattartási hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e.</string> + <string name="video_call_no_encryption">videóhívás (nem e2e titkosított)</string> + <string name="smp_servers_use_server_for_new_conn">Alkalmazás új kapcsolatokhoz</string> + <string name="periodic_notifications_desc">Az új üzeneteket az alkalmazás időszakosan lekéri – naponta néhány százalékot használ az akkumulátorból. Az alkalmazás nem használ push-értesítéseket – az eszközről származó adatok nem lesznek elküldve a kiszolgálóknak.</string> + <string name="paste_desktop_address">Számítógép címének beillesztése</string> + <string name="description_via_contact_address_link">a kapcsolattartási címhivatkozáson keresztül</string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Az adatvédelem növelése érdekében <b> a SimpleX a háttérben fut</b> a push értesítések használata helyett.]]></string> + <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">A partnereinek online kell lennie ahhoz, hogy a kapcsolat létrejöjjön.\nVisszavonhatja ezt a partnerkérelmet és eltávolíthatja a partnert (ezt később ismét megpróbálhatja egy új hivatkozással).</string> + <string name="restore_passphrase_not_found_desc">A jelmondat nem található a Keystore-ban, ezért kézzel szükséges megadni. Ez akkor történhetett meg, ha visszaállította az alkalmazás adatait egy biztonsági mentési eszközzel. Ha nem így történt, akkor lépjen kapcsolatba a fejlesztőkkel.</string> + <string name="your_contacts_will_remain_connected">A partnerei továbbra is kapcsolódva maradnak.</string> + <string name="error_xftp_test_server_auth">A kiszolgálónak hitelesítésre van szüksége a feltöltéshez, ellenőrizze jelszavát</string> + <string name="database_initialization_error_desc">Az adatbázis nem működik megfelelően. Koppintson ide a további információkért</string> + <string name="stop_snd_file__message">A fájl küldése le fog állni.</string> + <string name="trying_to_connect_to_server_to_receive_messages">Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál.</string> + <string name="la_could_not_be_verified">Nem sikerült hitelesíteni; próbálja meg újra.</string> + <string name="moderate_message_will_be_marked_warning">Az üzenet az összes tag számára moderáltként lesz megjelölve.</string> + <string name="enter_passphrase_notification_desc">Értesítések fogadásához adja meg az adatbázis jelmondatát</string> + <string name="error_smp_test_failed_at_step">A teszt a(z) %s lépésnél sikertelen volt.</string> + <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Az alkalmazás elindításához vagy 30 másodpercnyi háttérben töltött idő után, az alkalmazáshoz való visszatéréshez hitelesítésre lesz szükség.</string> + <string name="moderate_message_will_be_deleted_warning">Az üzenet az összes tag számára törölve lesz.</string> + <string name="video_decoding_exception_desc">A videó nem dekódolható. Próbálja ki egy másik videóval, vagy lépjen kapcsolatba a fejlesztőkkel.</string> + <string name="this_text_is_available_in_settings">Ez a szöveg a „Beállításokban” érhető el</string> + <string name="profile_will_be_sent_to_contact_sending_link">A profilja el lesz küldve a partnere számára, akitől ezt a hivatkozást kapta.</string> + <string name="system_restricted_background_in_call_desc">Az alkalmazás 1 perc után bezárható a háttérben.</string> + <string name="group_preview_you_are_invited">Ön meghívást kapott a csoportba</string> + <string name="turn_off_battery_optimization"><![CDATA[<b>Engedélyezze</b> a következő párbeszédpanelen az azonnali értesítések fogadásához.]]></string> + <string name="error_smp_test_server_auth">A kiszolgálónak engedélyre van szüksége a sorba állítás létrehozásához, ellenőrizze a jelszavát</string> + <string name="you_will_join_group">Kapcsolódni fog a csoport összes tagjához.</string> + <string name="error_smp_test_certificate">Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen</string> + <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">A biztonsága érdekében kapcsolja be a SimpleX-zár funkciót.\nA funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beállítására az eszközén.</string> + <string name="video_will_be_received_when_contact_is_online">A videó akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később!</string> + <string name="network_error_desc">Ellenőrizze a hálózati kapcsolatát vele: %1$s, és próbálja újra.</string> + <string name="you_can_turn_on_lock">A SimpleX-zár az „Adatvédelem és biztonság” menüben kapcsolható be.</string> + <string name="app_was_crashed">Az alkalmazás összeomlott</string> + <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Ellenőrizze, hogy a megfelelő hivatkozást használta-e, vagy kérje meg a partnerét, hogy küldjön egy másikat.</string> + <string name="image_decoding_exception_desc">A kép nem dekódolható. Próbálja meg egy másik képpel, vagy lépjen kapcsolatba a fejlesztőkkel.</string> + <string name="non_content_uri_alert_text">Érvénytelen fájlelérési útvonalat osztott meg. Jelentse a problémát az alkalmazás fejlesztőinek.</string> + <string name="failed_to_create_user_duplicate_desc">Már van egy csevegési profil ugyanezzel a megjelenítendő névvel. Válasszon egy másik nevet.</string> + <string name="trying_to_connect_to_server_to_receive_messages_with_error">Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (hiba: %1$s).</string> + <string name="stop_rcv_file__message">A fájl fogadása le fog állni.</string> + <string name="la_please_remember_to_store_password">Ne felejtse el, vagy tárolja biztonságosan – az elveszett jelszót nem lehet visszaállítani!</string> + <string name="video_will_be_received_when_contact_completes_uploading">A videó akkor érkezik meg, amikor a küldője befejezte annak feltöltését.</string> + <string name="description_you_shared_one_time_link_incognito">Ön egy egyszer használható meghívót osztott meg inkognitóban</string> + <string name="connected_to_server_to_receive_messages_from_contact">Ön már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál.</string> + <string name="you_can_enable_delivery_receipts_later">Később engedélyezheti a „Beállításokban”</string> + <string name="you_will_be_connected_when_group_host_device_is_online">Akkor lesz kapcsolódva a csoporthoz, amikor a csoport tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később!</string> + <string name="mtr_error_different">különböző átköltöztetés az alkalmazásban/adatbázisban: %s / %s</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[A kapcsolódás már folyamatban van a következőhöz: <b>%1$s</b>.]]></string> + <string name="unhide_profile">Profil felfedése</string> + <string name="this_link_is_not_a_valid_connection_link">Ez nem egy érvényes kapcsolattartási hivatkozás!</string> + <string name="to_verify_compare">A végpontok közötti titkosítás hitelesítéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) a partnere eszközén lévő kóddal.</string> + <string name="you_must_use_the_most_recent_version_of_database">A csevegési adatbázis legfrissebb verzióját CSAK egy eszközön kell használnia, ellenkező esetben előfordulhat, hogy az üzeneteket nem fogja megkapni valamennyi partnerétől.</string> + <string name="messages_section_description">Ez a beállítás csak az Ön jelenlegi csevegési profiljában lévő üzenetekre vonatkozik</string> + <string name="you_are_invited_to_group_join_to_connect_with_group_members">Ön meghívást kapott a csoportba. Csatlakozzon, hogy kapcsolatba léphessen a csoport tagjaival.</string> + <string name="alert_message_no_group">Ez a csoport már nem létezik.</string> + <string name="connect_plan_you_are_already_joining_the_group_via_this_link">A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül.</string> + <string name="you_are_invited_to_group">Ön meghívást kapott a csoportba</string> + <string name="contact_sent_large_file">A partnere a jelenleg megengedett maximális méretű (%1$s) fájlnál nagyobbat küldött.</string> + <string name="we_do_not_store_contacts_or_messages_on_servers">A partnerei és az üzenetek (kézbesítés után) nem a SimpleX-kiszolgálókon vannak tárolva.</string> + <string name="you_can_use_markdown_to_format_messages__prompt">Üzenetek formázása a szövegbe szúrt speciális karakterekkel:</string> + <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[A hivatkozásra kattintva is kapcsolódhat. Ha megnyílik a böngészőben, kattintson a<b>Megnyitás az alkalmazásban</b> gombra.]]></string> + <string name="your_chat_profile_will_be_sent_to_your_contact">A csevegési profilja el lesz küldve\na partnere számára</string> + <string name="invite_prohibited_description">Egy olyan partnerét próbálja meghívni, akivel inkognitóprofilt osztott meg abban a csoportban, amelyben a fő profilja van használatban</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[A csatlakozás már folyamatban van a(z) <b>%1$s</b> nevű csoporthoz.]]></string> + <string name="onboarding_notifications_mode_off">Amikor az alkalmazás fut</string> + <string name="alert_title_cant_invite_contacts_descr">Inkognitóprofilt használ ehhez a csoporthoz – fő profilja megosztásának elkerülése érdekében a meghívók küldése le van tiltva</string> + <string name="v4_5_transport_isolation">Átvitel-izoláció</string> + <string name="you_will_be_connected_when_your_connection_request_is_accepted">Akkor lesz kapcsolódva, ha a meghívási kérése el lesz fogadva, várjon, vagy ellenőrizze később!</string> + <string name="voice_messages_are_prohibited">A hangüzenetek küldése le van tiltva.</string> + <string name="system_restricted_background_in_call_warn"><![CDATA[A háttérben való hívásokhoz válassza ki az <b>Alkalmazás akkumulátor-használata</b> / <b>Korlátlan</b> módot az alkalmazás beállításaiban.]]></string> + <string name="v5_4_link_mobile_desktop_descr">Biztonságos kvantumbiztos protokollon keresztül.</string> + <string name="v5_1_better_messages_descr">- legfeljebb 5 perc hosszúságú hangüzenetek.\n- egyéni üzenet-eltűnési időkorlát.\n- előzmények szerkesztése.</string> + <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Válassza a <i>Társítás számítógéppel</i> menüt a hordozható eszköz alkalmazásban és olvassa be a QR-kódot.]]></string> + <string name="sender_at_ts">%s ekkor: %s</string> + <string name="you_will_be_connected_when_your_contacts_device_is_online">Akkor lesz kapcsolódva, amikor a partnerének az eszköze online lesz, várjon, vagy ellenőrizze később!</string> + <string name="v5_4_block_group_members_descr">Kéretlen üzenetek elrejtése.</string> + <string name="disable_onion_hosts_when_not_supported"><![CDATA[Állítsa az <i>Onion-kiszolgálók használata</i> opciót „Nemre”, ha a SOCKS-proxy nem támogatja őket.]]></string> + <string name="you_can_share_your_address">Megoszthatja a címét egy hivatkozásként vagy egy QR-kódként – így bárki kapcsolódhat Önhöz.</string> + <string name="you_can_create_it_later">Létrehozás később</string> + <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">A profilja az eszközén van tárolva és csak a partnereivel van megosztva. A SimpleX-kiszolgálók nem láthatják a profilját.</string> + <string name="snd_group_event_changed_member_role">Ön a következőre módosította %s szerepkörét: „%s”</string> + <string name="you_rejected_group_invitation">Csoportmeghívó elutasítva</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">Adatainak védelme érdekében a SimpleX külön üzenet-azonosítókat használ minden egyes kapcsolatához.</string> + <string name="to_share_with_your_contact">(a megosztáshoz a partnerével)</string> + <string name="you_sent_group_invitation">Csoportmeghívó elküldve</string> + <string name="update_network_session_mode_question">Frissíti az átvitel-izoláció módját?</string> + <string name="network_session_mode_transport_isolation">Átvitel-izoláció</string> + <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Ettől a csoporttól nem fog értesítéseket kapni. A csevegési előzmények megmaradnak.</string> + <string name="database_is_not_encrypted">A csevegési adatbázis nem titkosított – állítson be egy jelmondatot annak védelméhez.</string> + <string name="network_disable_socks">Közvetlen internetkapcsolat használata?</string> + <string name="you_will_still_receive_calls_and_ntfs">Továbbra is kap hívásokat és értesítéseket a némított profiloktól, ha azok aktívak.</string> + <string name="group_main_profile_sent">A fő csevegési profilja el lesz küldve a csoporttagok számára</string> + <string name="you_can_enable_delivery_receipts_later_alert">Később engedélyezheti őket az „Adatvédelem és biztonság” menüben.</string> + <string name="to_reveal_profile_enter_password">Rejtett profilja felfedéséhez adja meg a teljes jelszót a keresőmezőben, a „Csevegési profilok” menüben.</string> + <string name="upgrade_and_open_chat">Fejlesztés és a csevegés megnyitása</string> + <string name="you_need_to_allow_to_send_voice">Engedélyeznie kell a hangüzenetek küldését a partnere számára, hogy hangüzeneteket küldhessenek egymásnak.</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Ön már a(z) <b>%1$s</b> nevű csoport tagja.]]></string> + <string name="snd_conn_event_switch_queue_phase_completed">Ön módosította a címet</string> + <string name="v4_3_irreversible_message_deletion_desc">A partnerei engedélyezhetik a teljes üzenet törlését.</string> + <string name="you_have_to_enter_passphrase_every_time">A jelmondatot minden alkalommal meg kell adnia, amikor az alkalmazás elindul – nem az eszközön van tárolva.</string> + <string name="open_port_in_firewall_desc">Ha engedélyezni szeretné a hordozható eszköz-alkalmazás társítását a számítógéphez, akkor nyissa meg ezt a portot a tűzfalában, miután engedélyezte azt</string> + <string name="your_profile_is_stored_on_your_device">A profilja, a partnerei és az elküldött üzenetei a saját eszközén vannak tárolva.</string> + <string name="system_restricted_background_warn"><![CDATA[Az értesítések engedélyezéséhez válassza ki az <b>Alkalmazás akkumulátor-használata</b> / <b>Korlátlan</b> módot az alkalmazás beállításaiban.]]></string> + <string name="this_string_is_not_a_connection_link">Ez a karakterlánc nem egy meghívási hivatkozás!</string> + <string name="to_start_a_new_chat_help_header">Új csevegés indításához</string> + <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">A kapcsolódás már folyamatban van ezen az egyszer használható meghívón keresztül!</string> + <string name="you_wont_lose_your_contacts_if_delete_address">Nem veszíti el a partnereit, ha később törli a címét.</string> + <string name="updating_settings_will_reconnect_client_to_all_servers">A beállítások frissítése a kiszolgálókhoz való újra kapcsolódással jár.</string> + <string name="contact_wants_to_connect_with_you">kapcsolatba akar lépni Önnel!</string> + <string name="snd_group_event_changed_role_for_yourself">Ön a következőre módosította a saját szerepkörét: „%s”</string> + <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">A csevegési szolgáltatás elindítható a „Beállítások / Adatbázis” menüben vagy az alkalmazás újraindításával.</string> + <string name="verify_code_on_mobile">Kód hitelesítése a hordozható eszközön</string> + <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Ön csatlakozott ehhez a csoporthoz. Kapcsolódás a meghívó csoporttaghoz.</string> + <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Kapcsolatba léphet <font color="#0088ff">a SimpleX Chat fejlesztőivel, ahol bármiről kérdezhet és értesülhet a friss hírekről</font>.]]></string> + <string name="v4_2_auto_accept_contact_requests_desc">Nem kötelező üdvözlőüzenettel.</string> + <string name="unknown_database_error_with_info">Ismeretlen adatbázishiba: %s</string> + <string name="you_can_hide_or_mute_user_profile">Elrejtheti vagy lenémíthatja a felhasználó-profiljait – koppintson (vagy számítógép-alkalmazásban kattintson) hosszan a profilra a felugró menühöz.</string> + <string name="v5_3_simpler_incognito_mode_descr">Inkognitóra váltás kapcsolódáskor.</string> + <string name="you_can_share_group_link_anybody_will_be_able_to_connect">Megoszthat egy hivatkozást vagy QR-kódot – így bárki csatlakozhat a csoporthoz. Ha a csoportot Ön később törli, akkor nem fogja elveszíteni annak tagjait.</string> + <string name="you_joined_this_group">Ön csatlakozott ehhez a csoporthoz</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Ez a saját hivatkozása a(z) <b>%1$s</b> nevű csoporthoz!]]></string> + <string name="voice_prohibited_in_this_chat">A hangüzenetek küldése le van tiltva ebben a csevegésben.</string> + <string name="you_control_your_chat">Ön irányítja csevegését!</string> + <string name="verify_code_with_desktop">Kód hitelesítése a számítógépen</string> + <string name="v4_5_private_filenames_descr">Az időzóna védelmének érdekében a kép-/hangfájlok UTC-t használnak.</string> + <string name="connect_via_member_address_alert_desc">A meghívási kérés el lesz küldve ezen csoporttag számára.</string> + <string name="incognito_info_share">Inkognitóprofil megosztása esetén a rendszer azt a profilt fogja használni azokhoz a csoportokhoz, amelyekbe meghívást kapott.</string> + <string name="connect_plan_you_have_already_requested_connection_via_this_address">Már küldött egy meghívási kérést ezen a címen keresztül!</string> + <string name="you_can_share_this_address_with_your_contacts">Megoszthatja ezt a SimpleX-címet a partnereivel, hogy kapcsolatba léphessenek vele: %s.</string> + <string name="you_can_accept_or_reject_connection">Amikor az emberek meghívót küldenek, Ön elfogadhatja vagy elutasíthatja azokat.</string> + <string name="v4_6_group_welcome_message_descr">Megjelenítendő üzenet beállítása az új tagok számára!</string> + <string name="whats_new_thanks_to_users_contribute_weblate">Köszönet a felhasználóknak a Weblate-en való közreműködésért!</string> + <string name="sending_delivery_receipts_will_be_enabled">A kézbesítési jelentések küldése az összes partnere számára engedélyezve lesz.</string> + <string name="network_option_protocol_timeout_per_kb">Protokoll időtúllépése kB-onként</string> + <string name="database_backup_can_be_restored">Az adatbázis jelmondatának módosítására tett kísérlet nem fejeződött be.</string> + <string name="enable_automatic_deletion_message">Ez a művelet nem vonható vissza – a kijelöltnél korábban küldött és fogadott üzenetek törölve lesznek. Ez több percet is igénybe vehet.</string> + <string name="profile_is_only_shared_with_your_contacts">A profilja csak a partnereivel van megosztva.</string> + <string name="smp_servers_test_some_failed">Néhány kiszolgáló megbukott a teszten:</string> + <string name="group_invitation_tap_to_join">Koppintson ide a csatlakozáshoz</string> + <string name="delete_files_and_media_desc">Ez a művelet nem vonható vissza – az összes fogadott és küldött fájl a médiatartalmakkal együtt törölve lesznek. Az alacsony felbontású képek viszont megmaradnak.</string> + <string name="receipts_contacts_override_enabled">A kézbesítési jelentések engedélyezve vannak %d partnernél</string> + <string name="sending_via">Küldés a következőn keresztül:</string> + <string name="v5_0_polish_interface_descr">Köszönet a felhasználóknak a Weblate-en való közreműködésért!</string> + <string name="sending_delivery_receipts_will_be_enabled_all_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.</string> + <string name="v4_6_audio_video_calls_descr">Bluetooth támogatás és további fejlesztések.</string> + <string name="in_developing_desc">Ez a funkció még nem támogatott. Próbálja meg a következő kiadásban.</string> + <string name="share_text_updated_at">Bejegyzés frissítve: %s</string> + <string name="skip_inviting_button">Tagok meghívásának kihagyása</string> + <string name="receipts_section_description_1">Ezek felülbírálhatók a partner- és csoportbeállításokban.</string> + <string name="contact_you_shared_link_with_wont_be_able_to_connect">A partnere, akivel megosztotta ezt a hivatkozást, NEM fog tudni kapcsolódni!</string> + <string name="you_can_change_it_later">A véletlenszerű jelmondat egyszerű szövegként van tárolva a beállításokban.\nEzt később módosíthatja.</string> + <string name="group_invitation_tap_to_join_incognito">Koppintson ide az inkognitóban való kapcsolódáshoz</string> + <string name="set_password_to_export">Jelmondat beállítása az exportáláshoz</string> + <string name="receipts_groups_override_disabled">A kézbesítési jelentések le vannak tiltva %d csoportban</string> + <string name="non_fatal_errors_occured_during_import">Néhány nem végzetes hiba történt az importáláskor:</string> + <string name="v4_5_italian_interface_descr">Köszönet a felhasználóknak a Weblate-en való közreműködésért!</string> + <string name="relay_server_if_necessary">A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címet.</string> + <string name="v5_0_app_passcode_descr">Beállítás a rendszer-hitelesítés helyett.</string> + <string name="switch_receiving_address_desc">A fogadási cím egy másik kiszolgálóra fog módosulni. A cím módosítása a feladó online állapotba kerülése után fejeződik be.</string> + <string name="stop_chat_to_export_import_or_delete_chat_database">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.</string> + <string name="save_passphrase_in_keychain">Jelmondat mentése a Keystore-ba</string> + <string name="v4_6_chinese_spanish_interface_descr">Köszönet a felhasználóknak a Weblate-en való közreműködésért!</string> + <string name="save_passphrase_in_settings">Jelmondat mentése a beállításokban</string> + <string name="send_receipts_disabled_alert_msg">Ennek a csoportnak több mint %1$d tagja van, a kézbesítési jelentések nem lesznek elküldve.</string> + <string name="v5_2_message_delivery_receipts_descr">A második jelölés, amit kihagytunk! ✅</string> + <string name="relay_server_protects_ip">A továbbítókiszolgáló megvédi az Ön IP-címét, de megfigyelheti a hívás időtartamát.</string> + <string name="v4_5_message_draft_descr">Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt.</string> + <string name="saved_ICE_servers_will_be_removed">A mentett WebRTC ICE-kiszolgálók el lesznek távolítva.</string> + <string name="receipts_groups_override_enabled">A kézbesítési jelentések engedélyezve vannak %d csoportban</string> + <string name="member_role_will_be_changed_with_notification">A tag szerepköre a következőre fog módosulni: „%s”. A csoport összes tagja értesítést fog kapni.</string> + <string name="users_delete_with_connections">Profil és kiszolgálókapcsolatok</string> + <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Egy üzenetváltó- és alkalmazásplatform, amely védi az adatait és biztonságát.</string> + <string name="tap_to_activate_profile">A profil aktiválásához koppintson az ikonra.</string> + <string name="receipts_contacts_override_disabled">A kézbesítési jelentések le vannak tiltva %d partnernél</string> + <string name="session_code">Munkamenet kód</string> + <string name="v4_4_french_interface_descr">Köszönet a felhasználóknak a Weblate-en való közreműködésért!</string> + <string name="receipts_section_groups">Kis csoportok (max. 20 tag)</string> + <string name="connection_you_accepted_will_be_cancelled">Az Ön által elfogadott kérelem vissza lesz vonva!</string> + <string name="send_live_message_desc">Élő üzenet küldése – az üzenet a címzett(ek) számára valós időben frissül, ahogy Ön beírja az üzenetet</string> + <string name="settings_section_title_delivery_receipts">A KÉZBESÍTÉSI JELENTÉSEKET A KÖVETKEZŐ CÍMRE KELL KÜLDENI</string> + <string name="alert_text_msg_bad_id">A következő üzenet azonosítója érvénytelen (kisebb vagy egyenlő az előzővel).\nEz valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő.</string> + <string name="this_device_name_shared_with_mobile">Az eszköz neve meg lesz osztva a társított hordozható eszközön használt alkalmazással.</string> + <string name="v4_4_live_messages_desc">A címzettek a beírás közben látják a szövegváltozásokat.</string> + <string name="store_passphrase_securely">Tárolja el biztonságosan jelmondatát, mert ha elveszíti azt, NEM tudja módosítani.</string> + <string name="passphrase_will_be_saved_in_settings">A jelmondat a beállításokban egyszerű szövegként lesz tárolva, miután módosította azt vagy újraindította az alkalmazást.</string> + <string name="smp_servers_per_user">A jelenlegi csevegési profiljához tartozó új kapcsolatok kiszolgálói</string> + <string name="receiving_via">Fogadás a következőn keresztül:</string> + <string name="store_passphrase_securely_without_recover">Tárolja el biztonságosan jelmondát, mert ha elveszti azt, akkor NEM férhet hozzá a csevegéshez.</string> + <string name="member_role_will_be_changed_with_invitation">A tag szerepköre a következőre fog módosulni: „%s”. A tag új meghívást fog kapni.</string> + <string name="icon_descr_profile_image_placeholder">profilkép helyőrzője</string> + <string name="sync_connection_force_desc">A titkosítás működik, és új titkosítási egyezményre nincs szükség. Ez kapcsolati hibákat eredményezhet!</string> + <string name="delete_chat_profile_action_cannot_be_undone_warning">Ez a művelet nem vonható vissza – profiljai, partnerei, üzenetei és fájljai véglegesen törölve lesznek.</string> + <string name="info_row_updated_at">Bejegyzés frissítve</string> + <string name="read_more_in_user_guide_with_link"><![CDATA[További információ a <font color="#0088ff">Használati útmutatóban</font> olvasható.]]></string> + <string name="settings_is_storing_in_clear_text">A jelmondat a beállításokban egyszerű szövegként van tárolva.</string> + <string name="terminal_always_visible">Konzol megjelenítése új ablakban</string> + <string name="alert_text_msg_bad_hash">Az előző üzenet hasítóértéke különbözik.</string> + <string name="receipts_section_description">Ezek a beállítások csak a jelenlegi csevegési profiljára vonatkoznak</string> + <string name="loading_remote_file_desc">Várjon, amíg a fájl betöltődik a társított hordozható eszközről</string> + <string name="read_more_in_github_with_link"><![CDATA[További információ a <font color="#0088ff">GitHub tárolónkban</font>.]]></string> + <string name="error_showing_content">Hiba történt a tartalom megjelenítésekor</string> + <string name="error_showing_message">Hiba történt az üzenet megjelenítésekor</string> + <string name="you_can_make_address_visible_via_settings">Láthatóvá teheti a SimpleXbeli partnerei számára a „Beállításokban”.</string> + <string name="recent_history_is_sent_to_new_members">Legfeljebb az utolsó 100 üzenet lesz elküldve az új tagok számára.</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">A beolvasott QR-kód nem egy SimpleX-QR-kód-hivatkozás.</string> + <string name="the_text_you_pasted_is_not_a_link">A beillesztett szöveg nem egy SimpleX-hivatkozás.</string> + <string name="you_can_view_invitation_link_again">A meghívási hivatkozást újra megtekintheti a kapcsolat részleteinél.</string> + <string name="start_chat_question">Elindítja a csevegést?</string> + <string name="recent_history">Látható előzmények</string> + <string name="la_app_passcode">Alkalmazás jelkód</string> + <string name="add_contact_tab">Partner hozzáadása</string> + <string name="tap_to_scan">Koppintson ide a QR-kód beolvasásához</string> + <string name="tap_to_paste_link">Koppintson ide a hivatkozás beillesztéséhez</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Partner hozzáadása</b>: új meghívási hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz.]]></string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Csoport létrehozása</b>: új csoport létrehozásához.]]></string> + <string name="chat_is_stopped_you_should_transfer_database">A csevegés megállt. Ha már használta ezt az adatbázist egy másik eszközön, úgy visszaállítás szükséges a csevegés elindítása előtt.</string> + <string name="recent_history_is_not_sent_to_new_members">Az előzmények nem lesznek elküldve az új tagok számára.</string> + <string name="retry_verb">Újrapróbálkozás</string> + <string name="camera_not_available">A kamera nem elérhető</string> + <string name="enable_sending_recent_history">Legfeljebb az utolsó 100 üzenet elküldése az új tagok számára.</string> + <string name="disable_sending_recent_history">Az előzmények ne legyenek elküldve az új tagok számára.</string> + <string name="or_show_this_qr_code">Vagy mutassa meg ezt a kódot</string> + <string name="enable_camera_access">Kamera hozzáférés engedélyezése</string> + <string name="keep_unused_invitation_question">Megtartja a fel nem használt meghívót?</string> + <string name="share_this_1_time_link">Ennek az egyszer használható meghívónak a megosztása</string> + <string name="new_chat">Új csevegés</string> + <string name="loading_chats">Csevegések betöltése…</string> + <string name="creating_link">Hivatkozás létrehozása…</string> + <string name="or_scan_qr_code">Vagy QR-kód beolvasása</string> + <string name="invalid_qr_code">Érvénytelen QR-kód</string> + <string name="keep_invitation_link">Megtartás</string> + <string name="search_or_paste_simplex_link">Keresés vagy SimpleX-hivatkozás beillesztése</string> + <string name="show_internal_errors">Belső hibák megjelenítése</string> + <string name="agent_critical_error_title">Kritikus hiba</string> + <string name="agent_internal_error_title">Belső hiba</string> + <string name="remote_ctrl_error_bad_version">Nem támogatott a számítógép által használt alkalmazás verziója. Győződjön meg arról, hogy mindkét eszközön ugyanazt a verziót használja</string> + <string name="remote_ctrl_error_bad_invitation">A számítógépes alkalmazásban érvénytelen meghívókód szerepel</string> + <string name="remote_ctrl_error_busy">Számítógép elfoglalt</string> + <string name="remote_ctrl_error_inactive">Számítógép inaktív</string> + <string name="restart_chat_button">Csevegés újraindítása</string> + <string name="remote_ctrl_error_timeout">Időtúllépés a számítógéphez való csatlakozáskor</string> + <string name="remote_ctrl_error_disconnected">Kapcsolat bontva a számítógéppel</string> + <string name="remote_host_was_disconnected_title">A kapcsolat megszakadt</string> + <string name="remote_ctrl_was_disconnected_title">A kapcsolat megszakadt</string> + <string name="remote_ctrl_error_bad_state">A kapcsolat a számítógéppel rossz állapotban van</string> + <string name="agent_critical_error_desc">Jelentse a fejlesztőknek:\n%s\n\nAz alkalmazás újraindítása javasolt.</string> + <string name="agent_internal_error_desc">Jelentse a fejlesztőknek: +\n%s</string> + <string name="remote_host_error_bad_version"><![CDATA[A(z) <b>%s</b> hordozható eszköz által használt alkalmazás verziója nem támogatott. Győződjön meg arról, hogy mindkét eszközön ugyanazt a verziót használja]]></string> + <string name="remote_host_error_disconnected"><![CDATA[Kapcsolat bontva a(z) <b>%s</b> nevű hordozható eszközzel]]></string> + <string name="failed_to_create_user_invalid_title">Érvénytelen megjelenítendő név!</string> + <string name="failed_to_create_user_invalid_desc">Ez a megjelenítendő név érvénytelen. Válasszon egy másik nevet.</string> + <string name="remote_host_disconnected_from"><![CDATA[Kapcsolat bontva a(z) <b>%s</b> nevű hordozható eszközzel, a következő okból: %s]]></string> + <string name="remote_ctrl_disconnected_with_reason">Kapcsolat bontva a következő okból: %s</string> + <string name="remote_host_error_missing"><![CDATA[A(z) <b>%s</b> hordozható eszköz nem található]]></string> + <string name="remote_host_error_bad_state"><![CDATA[A kapcsolat a(z) <b>%s</b> hordozható eszközzel rossz állapotban van]]></string> + <string name="remote_host_error_timeout"><![CDATA[Időtúllépés a(z) <b>%s</b> hordozható eszközhöz való csatlakozáskor]]></string> + <string name="group_member_status_unknown_short">ismeretlen</string> + <string name="possible_slow_function_title">Lassú funkció</string> + <string name="show_slow_api_calls">Lassú API-hívások megjelenítése</string> + <string name="remote_host_error_inactive"><![CDATA[A(z) <b>%s</b> hordozható eszköz inaktív]]></string> + <string name="developer_options_section">Fejlesztői beállítások</string> + <string name="possible_slow_function_desc">A funkció végrehajtása túl sokáig tart: %1$d másodperc: %2$s</string> + <string name="remote_host_error_busy"><![CDATA[A(z) <b>%s</b> hordozható eszköz elfoglalt]]></string> + <string name="past_member_vName">(Már nem tag) %1$s</string> + <string name="group_member_status_unknown">ismeretlen állapot</string> + <string name="profile_update_event_member_name_changed">%1$s a következőre módosította a nevét: %2$s</string> + <string name="profile_update_event_removed_address">eltávolította a kapcsolattartási címet</string> + <string name="profile_update_event_removed_picture">eltávolította a profilképét</string> + <string name="profile_update_event_set_new_address">új kapcsolattartási címet állított be</string> + <string name="profile_update_event_set_new_picture">új profilképet állított be</string> + <string name="profile_update_event_updated_profile">frissített profil</string> + <string name="profile_update_event_contact_name_changed">%1$s a következőre módosította a nevét: %2$s</string> + <string name="note_folder_local_display_name">Privát jegyzetek</string> + <string name="error_deleting_note_folder">Hiba történt a privát jegyzetek törlésekor</string> + <string name="error_creating_message">Hiba történt az üzenet létrehozásakor</string> + <string name="clear_note_folder_question">Kiüríti a privát jegyzeteket?</string> + <string name="info_row_created_at">Létrehozva</string> + <string name="saved_message_title">Mentett üzenet</string> + <string name="share_text_created_at">Létrehozva: %s</string> + <string name="clear_note_folder_warning">Az összes üzenet törölve lesz – ez a művelet nem vonható vissza!</string> + <string name="v5_5_message_delivery">Továbbfejlesztett üzenetkézbesítés</string> + <string name="v5_5_join_group_conversation">Csatlakozás csoportos beszélgetésekhez</string> + <string name="v5_5_simpler_connect_ui">Hivatkozás beillesztése a kapcsolódáshoz!</string> + <string name="v5_5_private_notes">Privát jegyzetek</string> + <string name="v5_5_simpler_connect_ui_descr">A keresősáv elfogadja a meghívási hivatkozásokat.</string> + <string name="v5_5_private_notes_descr">Titkosított fájlokkal és médiatartalmakkal.</string> + <string name="v5_5_message_delivery_descr">Csökkentett akkumulátor-használattal.</string> + <string name="v5_5_new_interface_languages">Magyar és török kezelőfelület</string> + <string name="v5_5_join_group_conversation_descr">A közelmúlt eseményei és továbbfejlesztett könyvtárbot.</string> + <string name="rcv_group_event_member_unblocked">feloldotta %s letiltását</string> + <string name="snd_group_event_member_unblocked">Ön feloldotta %s letiltását</string> + <string name="member_info_member_blocked">letiltva</string> + <string name="blocked_by_admin_item_description">letiltva az adminisztrátor által</string> + <string name="member_blocked_by_admin">Letiltva az adminisztrátor által</string> + <string name="rcv_group_event_member_blocked">letiltotta őt: %s</string> + <string name="block_for_all">Letiltás</string> + <string name="block_for_all_question">Az összes tag számára letiltja a tagot?</string> + <string name="blocked_by_admin_items_description">%d üzenetet letiltott az adminisztrátor</string> + <string name="unblock_for_all">Feloldás</string> + <string name="unblock_for_all_question">Az összes tag számára feloldja a tag letiltását?</string> + <string name="snd_group_event_member_blocked">Ön letiltotta őt: %s</string> + <string name="error_blocking_member_for_all">Hiba történt a tag az összes csoporttag számára való letiltásakor</string> + <string name="message_too_large">Az üzenet túl nagy</string> + <string name="welcome_message_is_too_long">Az üdvözlőüzenet túl hosszú</string> + <string name="database_migration_in_progress">Az adatbázis átköltöztetése folyamatban van.\nEz eltarthat néhány percig.</string> + <string name="call_service_notification_audio_call">Hanghívás</string> + <string name="call_service_notification_end_call">A hívás befejeződött</string> + <string name="call_service_notification_video_call">Videóhívás</string> + <string name="unable_to_open_browser_title">Hiba történt a böngésző megnyitásakor</string> + <string name="unable_to_open_browser_desc">A hívásokhoz egy alapértelmezett webböngésző szükséges. Állítson be egy alapértelmezett webböngészőt az eszközön, és osszon meg további információkat a SimpleX Chat fejlesztőivel.</string> + <string name="migrate_to_device_confirm_network_settings">Hálózati beállítások megerősítése</string> + <string name="migrate_from_device_error_exporting_archive">Hiba történt a csevegési adatbázis exportálásakor</string> + <string name="migrate_to_device_apply_onion">Alkalmaz</string> + <string name="migrate_from_device_archive_and_upload">Archiválás és feltöltés</string> + <string name="migrate_from_device_confirm_upload">Feltöltés megerősítése</string> + <string name="migrate_from_device_error_deleting_database">Hiba történt az adatbázis törlésekor</string> + <string name="v5_6_safer_groups_descr">Az adminisztrátorok egy tagot a csoport összes tagja számára letilthatnak.</string> + <string name="migrate_from_device_all_data_will_be_uploaded">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.</string> + <string name="v5_6_app_data_migration">Alkalmazásadatok átköltöztetése</string> + <string name="migrate_from_device_archiving_database">Adatbázis archiválása</string> + <string name="migrate_from_device_cancel_migration">Átköltöztetés visszavonása</string> + <string name="migrate_to_device_chat_migrated">A csevegés átköltöztetve!</string> + <string name="migrate_from_device_check_connection_and_try_again">Ellenőrizze az internetkapcsolatot, és próbálja újra</string> + <string name="migrate_from_device_creating_archive_link">Archívum hivatkozás létrehozása</string> + <string name="migrate_from_device_delete_database_from_device">Adatbázis törlése erről az eszközről</string> + <string name="migrate_to_device_download_failed">Sikertelen letöltés</string> + <string name="migrate_to_device_downloading_archive">Archívum letöltése</string> + <string name="migrate_to_device_downloading_details">Letöltési hivatkozás részletei</string> + <string name="v5_6_quantum_resistant_encryption_descr">Engedélyezés a közvetlen csevegésekben (BÉTA)!</string> + <string name="migrate_to_device_enter_passphrase">Adja meg a jelmondatot</string> + <string name="migrate_from_device_error_saving_settings">Hiba történt a beállítások mentésekor</string> + <string name="migrate_to_device_error_downloading_archive">Hiba történt az archívum letöltésekor</string> + <string name="migrate_from_device_error_uploading_archive">Hiba történt az archívum feltöltésekor</string> + <string name="migrate_from_device_error_verifying_passphrase">Hiba történt a jelmondat hitelesítésekor:</string> + <string name="migrate_from_device_exported_file_doesnt_exist">Az exportált fájl nem létezik</string> + <string name="migrate_to_device_file_delete_or_link_invalid">A fájl törölve lett, vagy érvénytelen a hivatkozás</string> + <string name="migrate_to_device_bytes_downloaded">%s letöltve</string> + <string name="migrate_to_device_importing_archive">Archívum importálása</string> + <string name="migrate_from_device_database_init">Feltöltés előkészítése</string> + <string name="migrate_from_device_verify_database_passphrase">Az adatbázis jelmondatának hitelesítése</string> + <string name="migrate_from_device_verify_passphrase">Jelmondat hitelesítése</string> + <string name="set_passphrase">Jelmondat beállítása</string> + <string name="v5_6_picture_in_picture_calls">Kép a képben hívások</string> + <string name="v5_6_safer_groups">Biztonságosabb csoportok</string> + <string name="v5_6_picture_in_picture_calls_descr">Használja az alkalmazást hívás közben.</string> + <string name="or_paste_archive_link">Vagy az archívum hivatkozásának beillesztése</string> + <string name="paste_archive_link">Archívum hivatkozásának beillesztése</string> + <string name="migrate_to_device_repeat_download">Letöltés ismét</string> + <string name="migrate_to_device_import_failed">Nem sikerült az importálás</string> + <string name="migrate_to_device_confirm_network_settings_footer">Ellenőrizze, hogy a hálózati beállítások megfelelők-e ehhez az eszközhöz.</string> + <string name="migrate_from_device_chat_should_be_stopped">A folytatáshoz a csevegést meg kell szakítani.</string> + <string name="migrate_from_device_stopping_chat">Csevegés megállítása folyamatban</string> + <string name="migrate_from_device_or_share_this_file_link">Vagy ossza meg biztonságosan ezt a fájlhivatkozást</string> + <string name="migrate_from_device_start_chat">Csevegés indítása</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[<b>Nem szabad</b> ugyanazt az adatbázist használni egyszerre két eszközön.]]></string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Az átköltöztetéshez erősítse meg, hogy emlékszik az adatbázis jelmondatára.</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Válassza az <i>Átköltöztetés egy másik eszközről</i> opciót az új eszközén és olvassa be a QR-kódot.]]></string> + <string name="migrate_from_device_finalize_migration">Átköltöztetés véglegesítése</string> + <string name="migrate_to_device_finalize_migration">Átköltöztetés véglegesítése egy másik eszközön.</string> + <string name="migrate_to_device_database_init">Letöltés előkészítése</string> + <string name="migrate_from_device_repeat_upload">Feltöltés ismét</string> + <string name="migrate_from_device_bytes_uploaded">%s feltöltve</string> + <string name="migrate_from_device_upload_failed">Sikertelen feltöltés</string> + <string name="migrate_from_device_uploading_archive">Archívum feltöltése</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Figyelmeztetés: a csevegés elindítása egyszerre több eszközön nem támogatott, mert üzenetkézbesítési hibákat okoz</string> + <string name="migrate_to_device_repeat_import">Importálás ismét</string> + <string name="conn_event_disabled_pq">szabványos végpontok közötti titkosítás</string> + <string name="migrate_to_device_title">Átköltöztetés ide</string> + <string name="migrate_from_device_title">Eszköz átköltöztetése</string> + <string name="migrate_from_device_to_another_device">Átköltöztetés egy másik eszközre</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Figyelmeztetés:</b> az archívum törölve lesz.]]></string> + <string name="migrate_from_another_device">Átköltöztetés egy másik eszközről</string> + <string name="v5_6_quantum_resistant_encryption">Kvantumbiztos titkosítás</string> + <string name="migrate_from_device_try_again">Megpróbálhatja még egyszer.</string> + <string name="migrate_from_device_migration_complete">Átköltöztetés befejezve</string> + <string name="v5_6_app_data_migration_descr">Átköltöztetés egy másik eszközre QR-kód használatával.</string> + <string name="migrate_to_device_migrating">Átköltöztetés</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Megjegyzés:</b> 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.]]></string> + <string name="migrate_to_device_try_again">Megpróbálhatja még egyszer.</string> + <string name="invalid_file_link">Érvénytelen hivatkozás</string> + <string name="conn_event_enabled_pq">végpontok közötti kvantumbiztos titkosítás</string> + <string name="e2ee_info_no_pq_short">Ez a csevegés végpontok közötti titkosítással védett.</string> + <string name="auth_open_migration_to_another_device">Átköltöztetési párbeszédablak megnyitása</string> + <string name="e2ee_info_pq_short">Ez a csevegés végpontok közötti kvantumbiztos titkosítással védett.</string> + <string name="e2ee_info_no_pq"><![CDATA[Az üzenetek, a fájlok és a hívások <b>végpontok közötti titkosítással</b>, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve.]]></string> + <string name="e2ee_info_pq"><![CDATA[Az üzenetek, a fájlok és a hívások <b>végpontok közötti kvantumbiztos titkosítással</b>, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve.]]></string> + <string name="error_showing_desktop_notification">Hiba történt az értesítés megjelenítésekor, lépjen kapcsolatba a fejlesztőkkel.</string> + <string name="permissions_find_in_settings_and_grant">Keresse meg ezt az engedélyt az Android beállításaiban, és adja meg kézzel.</string> + <string name="permissions_grant_in_settings">Engedélyezés a beállításokban</string> + <string name="permissions_grant">Engedély(ek) megadása a hívások kezdeményezéséhez</string> + <string name="permissions_record_audio">Mikrofon</string> + <string name="permissions_open_settings">Beállítások megnyitása</string> + <string name="permissions_camera">Kamera</string> + <string name="permissions_camera_and_record_audio">Kamera és mikrofon</string> + <string name="permissions_required">Engedélyek megadása</string> + <string name="network_type_ethernet">Vezetékes Ethernet</string> + <string name="network_type_cellular">Mobilhálózat</string> + <string name="settings_section_title_network_connection">Hálózati kapcsolat</string> + <string name="network_type_no_network_connection">Nincs hálózati kapcsolat</string> + <string name="network_type_other">További</string> + <string name="network_type_network_wifi">Wi-Fi</string> + <string name="forwarded_description">továbbított</string> + <string name="simplex_links_not_allowed">A SimpleX-hivatkozások küldése le van tiltva</string> + <string name="group_members_can_send_simplex_links">A tagok küldhetnek SimpleX-hivatkozásokat.</string> + <string name="feature_roles_owners">tulajdonosok</string> + <string name="feature_roles_admins">adminisztrátorok</string> + <string name="feature_roles_all_members">összes tag</string> + <string name="simplex_links">SimpleX-hivatkozások</string> + <string name="voice_messages_not_allowed">A hangüzenetek küldése le van tiltva</string> + <string name="simplex_links_are_prohibited_in_group">A SimpleX-hivatkozások küldése le van tiltva.</string> + <string name="prohibit_sending_simplex_links">A SimpleX-hivatkozások küldése le van tiltva</string> + <string name="files_and_media_not_allowed">A fájlok- és médiatartalmak nincsenek engedélyezve</string> + <string name="allow_to_send_simplex_links">A SimpleX-hivatkozások küldése engedélyezve van.</string> + <string name="feature_enabled_for">Számukra engedélyezve</string> + <string name="saved_description">mentett</string> + <string name="saved_from_description">elmentve innen: %s</string> + <string name="forwarded_from_chat_item_info_title">Továbbítva innen</string> + <string name="recipients_can_not_see_who_message_from">A címzett(ek) nem látja(k), hogy kitől származik ez az üzenet.</string> + <string name="saved_chat_item_info_tab">Mentett</string> + <string name="saved_from_chat_item_info_title">Elmentve innen</string> + <string name="download_file">Letöltés</string> + <string name="forward_chat_item">Továbbítás</string> + <string name="forwarded_chat_item_info_tab">Továbbított</string> + <string name="forward_message">Üzenet továbbítása…</string> + <string name="audio_device_wired_headphones">Fejhallgató</string> + <string name="audio_device_speaker">Hangszóró</string> + <string name="audio_device_earpiece">Fülhallgató</string> + <string name="network_option_rcv_concurrency">Egyidejű fogadás</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="v5_7_quantum_resistant_encryption_descr">A közvetlen csevegésekben engedélyezve lesz!</string> + <string name="v5_7_new_interface_languages">Litván kezelőfelület</string> + <string name="v5_7_network_descr">Megbízhatóbb hálózati kapcsolat.</string> + <string name="v5_7_network">Hálózatkezelés</string> + <string name="v5_7_call_sounds_descr">Amikor egy bejövő hang- vagy videóhívás érkezik.</string> + <string name="v5_7_forward">Üzenetek továbbítása és mentése</string> + <string name="v5_7_call_sounds">Bejövő hívás csengőhangja</string> + <string name="v5_7_forward_descr">Az üzenet forrása titokban marad.</string> + <string name="settings_section_title_profile_images">Profilképek</string> + <string name="v5_7_shape_profile_images">Profilkép alakzata</string> + <string name="v5_7_shape_profile_images_descr">Négyzet, kör vagy bármi a kettő között.</string> + <string name="snd_error_relay">Célkiszolgáló-hiba: %1$s</string> + <string name="snd_error_proxy">Továbbítókiszolgáló: %1$s\nHiba: %2$s</string> + <string name="snd_error_expired">Hálózati problémák – az üzenet többszöri elküldési kísérlet után lejárt.</string> + <string name="srv_error_version">A kiszolgáló verziója nem kompatibilis a hálózati beállításokkal.</string> + <string name="snd_error_auth">Érvénytelen kulcs vagy ismeretlen kapcsolat – valószínűleg ez a kapcsolat törlődött.</string> + <string name="snd_error_proxy_relay">Továbbítókiszolgáló: %1$s\nCélkiszolgáló-hiba: %2$s</string> + <string name="ci_status_other_error">Hiba: %1$s</string> + <string name="snd_error_quota">Kapacitás túllépés – a címzett nem kapta meg a korábban elküldött üzeneteket.</string> + <string name="message_delivery_warning_title">Üzenetkézbesítési figyelmeztetés</string> + <string name="srv_error_host">A kiszolgáló címe nem kompatibilis a hálózati beállításokkal.</string> + <string name="network_smp_proxy_mode_never">Soha</string> + <string name="network_smp_proxy_mode_unknown">Ismeretlen kiszolgálók</string> + <string name="network_smp_proxy_fallback_allow_protected">Ha az IP-cím rejtett</string> + <string name="private_routing_show_message_status">Üzenet állapotának megjelenítése</string> + <string name="network_smp_proxy_fallback_allow_downgrade">Visszafejlesztés engedélyezése</string> + <string name="network_smp_proxy_mode_always">Mindig</string> + <string name="network_smp_proxy_fallback_prohibit">Nem</string> + <string name="network_smp_proxy_mode_unprotected">Nem védett</string> + <string name="network_smp_proxy_fallback_allow">Igen</string> + <string name="network_smp_proxy_mode_never_description">NE használjon privát útválasztást.</string> + <string name="network_smp_proxy_mode_private_routing">Privát útválasztás</string> + <string name="network_smp_proxy_mode_unknown_description">Használjon privát útválasztást ismeretlen kiszolgálókkal.</string> + <string name="network_smp_proxy_mode_always_description">Mindig használjon privát útválasztást.</string> + <string name="update_network_smp_proxy_mode_question">Üzenet-útválasztási mód</string> + <string name="network_smp_proxy_fallback_allow_protected_description">Közvetlen üzenetküldés, ha az IP-cím védett és a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.</string> + <string name="network_smp_proxy_fallback_allow_description">Közvetlen üzenetküldés, ha a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.</string> + <string name="private_routing_explanation">Az IP-cím védelmének érdekében a privát útválasztás az SMP-kiszolgálókat használja az üzenetek kézbesítéséhez.</string> + <string name="update_network_smp_proxy_fallback_question">Üzenet-útválasztási tartalék</string> + <string name="settings_section_title_private_message_routing">PRIVÁT ÜZENET-ÚTVÁLASZTÁS</string> + <string name="network_smp_proxy_mode_unprotected_description">Használjon privát útválasztást ismeretlen kiszolgálókkal, ha az IP-cím nem védett.</string> + <string name="network_smp_proxy_fallback_prohibit_description">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.</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Tor vagy VPN nélkül az Ön IP-címe látható lesz a fájlkiszolgálók számára.</string> + <string name="settings_section_title_files">FÁJLOK</string> + <string name="protect_ip_address">IP-cím védelme</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">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).</string> + <string name="file_not_approved_title">Ismeretlen kiszolgálók!</string> + <string name="file_not_approved_descr">Tor vagy VPN nélkül az Ön IP-címe látható lesz a következő XFTP-továbbítókiszolgálók számára:\n%1$s.</string> + <string name="chat_theme_apply_to_all_modes">Összes színmód</string> + <string name="theme_black">Fekete</string> + <string name="color_mode">Színmód</string> + <string name="color_mode_dark">Sötét</string> + <string name="chat_theme_apply_to_dark_mode">Sötét mód</string> + <string name="dark_mode_colors">Sötét mód színei</string> + <string name="wallpaper_scale_fit">Illesztés</string> + <string name="wallpaper_preview_hello_alice">Jó napot!</string> + <string name="wallpaper_preview_hello_bob">Jó reggelt!</string> + <string name="wallpaper_advanced_settings">Speciális beállítások</string> + <string name="chat_theme_apply_to_mode">Alkalmazás erre</string> + <string name="settings_section_title_chat_colors">Csevegés színei</string> + <string name="settings_section_title_chat_theme">Csevegés témája</string> + <string name="wallpaper_scale_fill">Kitöltés</string> + <string name="settings_section_title_user_theme">Profiltéma</string> + <string name="chat_list_always_visible">Csevegési lista megjelenítése új ablakban</string> + <string name="color_mode_light">Világos</string> + <string name="chat_theme_apply_to_light_mode">Világos mód</string> + <string name="color_received_quote">Fogadott válaszüzenet-buborék színe</string> + <string name="theme_remove_image">Kép eltávolítása</string> + <string name="wallpaper_scale_repeat">Mozaik</string> + <string name="reset_single_color">Szín visszaállítása</string> + <string name="wallpaper_scale">Méretezés</string> + <string name="color_sent_quote">Válaszüzenet-buborék színe</string> + <string name="chat_theme_set_default_theme">Alapértelmezett téma beállítása</string> + <string name="color_mode_system">Rendszer</string> + <string name="color_wallpaper_tint">Háttérkép kiemelőszíne</string> + <string name="color_wallpaper_background">Háttérkép háttérszíne</string> + <string name="color_primary_variant2">További kiemelőszín 2</string> + <string name="theme_destination_app_theme">Alkalmazás téma</string> + <string name="v5_8_persian_ui">Perzsa kezelőfelület</string> + <string name="v5_8_private_routing_descr">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.</string> + <string name="v5_8_safe_files_descr">Ismeretlen kiszolgálókról származó fájlok megerősítése.</string> + <string name="v5_8_message_delivery">Javított üzenetkézbesítés</string> + <string name="chat_theme_reset_to_app_theme">Alkalmazás témájának visszaállítása</string> + <string name="v5_8_chat_themes_descr">Tegye egyedivé a csevegéseit!</string> + <string name="v5_8_chat_themes">Új csevegési témák</string> + <string name="v5_8_private_routing">Privát üzenet-útválasztás 🚀</string> + <string name="v5_8_safe_files">Fájlok biztonságos fogadása</string> + <string name="v5_8_message_delivery_descr">Csökkentett akkumulátor-használattal.</string> + <string name="error_initializing_web_view">Hiba történt a WebView előkészítésekor. Frissítse rendszerét az új verzióra. Lépjen kapcsolatba a fejlesztőkkel.\nHiba: %s</string> + <string name="chat_theme_reset_to_user_theme">Felhasználó által létrehozott téma visszaállítása</string> + <string name="message_queue_info">Üzenetsorbaállítási információ</string> + <string name="message_queue_info_none">nincs</string> + <string name="info_row_debug_delivery">Kézbesítési hibák felderítése</string> + <string name="message_queue_info_server_info">a kiszolgáló sorbaállítási információi: %1$s\n\nutoljára fogadott üzenet: %2$s</string> + <string name="file_error_auth">Érvénytelen kulcs vagy ismeretlen fájltöredékcím – valószínűleg a fájl törlődött.</string> + <string name="temporary_file_error">Ideiglenes fájlhiba</string> + <string name="info_row_message_status">Üzenet állapota</string> + <string name="share_text_message_status">Üzenet állapota: %s</string> + <string name="file_error">Fájlhiba</string> + <string name="file_error_no_file">A fájl nem található – valószínűleg a fájlt törölték vagy visszavonták.</string> + <string name="file_error_relay">Fájlkiszolgáló-hiba: %1$s</string> + <string name="info_row_file_status">Fájl állapota</string> + <string name="share_text_file_status">Fájl állapota: %s</string> + <string name="copy_error">Másolási hiba</string> + <string name="remote_ctrl_connection_stopped_identity_desc">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.</string> + <string name="remote_ctrl_connection_stopped_desc">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.</string> + <string name="cannot_share_message_alert_title">Nem lehet üzenetet küldeni</string> + <string name="cannot_share_message_alert_text">A kijelölt csevegési beállítások tiltják ezt az üzenetet.</string> + <string name="please_try_later">Próbálja meg később.</string> + <string name="network_error_broker_host_desc">A kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %1$s.</string> + <string name="member_inactive_title">Inaktív tag</string> + <string name="message_forwarded_title">Továbbított üzenet</string> + <string name="member_inactive_desc">Az üzenet később is kézbesíthető, ha a tag aktívvá válik.</string> + <string name="message_forwarded_desc">Még nincs közvetlen kapcsolat, az üzenetet az adminisztrátor továbbítja.</string> + <string name="scan_paste_link">Hivatkozás beolvasása / beillesztése</string> + <string name="smp_servers_configured">Konfigurált SMP-kiszolgálók</string> + <string name="smp_servers_other">Egyéb SMP-kiszolgálók</string> + <string name="xftp_servers_other">Egyéb XFTP-kiszolgálók</string> + <string name="member_info_member_disabled">letiltva</string> + <string name="member_info_member_inactive">inaktív</string> + <string name="appearance_zoom">Nagyítás</string> + <string name="servers_info">Információk a kiszolgálókról</string> + <string name="servers_info_sessions_connecting">Kapcsolódás</string> + <string name="servers_info_sessions_errors">Hibák</string> + <string name="servers_info_subscriptions_connections_pending">Függőben</string> + <string name="servers_info_private_data_disclaimer">Statisztikagyűjtés kezdete: %s.\nAz összes adat privát módon van tárolva az eszközén.</string> + <string name="servers_info_messages_sent">Elküldött üzenetek</string> + <string name="servers_info_proxied_servers_section_header">Proxyzott kiszolgálók</string> + <string name="servers_info_reconnect_servers_title">Újrakapcsolódik a kiszolgálókhoz?</string> + <string name="servers_info_reconnect_server_title">Újrakapcsolódik a kiszolgálóhoz?</string> + <string name="servers_info_reconnect_server_error">Hiba történt a kiszolgálóhoz való újrakapcsolódáskor</string> + <string name="servers_info_reconnect_all_servers_button">Újrakapcsolódás az összes kiszolgálóhoz</string> + <string name="servers_info_reset_stats_alert_error_title">Hiba történt a statisztikák visszaállításakor</string> + <string name="servers_info_reset_stats_alert_confirm">Visszaállítás</string> + <string name="servers_info_reset_stats">Az összes statisztika visszaállítása</string> + <string name="servers_info_reset_stats_alert_title">Visszaállítja az összes statisztikát?</string> + <string name="servers_info_reset_stats_alert_message">A kiszolgálók statisztikái visszaállnak – ez a művelet nem vonható vissza!</string> + <string name="servers_info_detailed_statistics">Részletes statisztikák</string> + <string name="servers_info_downloaded">Letöltve</string> + <string name="expired_label">lejárt</string> + <string name="other_label">egyéb</string> + <string name="servers_info_detailed_statistics_received_total">Összes fogadott üzenet</string> + <string name="servers_info_detailed_statistics_receive_errors">Üzenetfogadási hibák</string> + <string name="reconnect">Újrakapcsolódás</string> + <string name="send_errors">Üzenetküldési hibák</string> + <string name="sent_directly">Közvetlenül küldött</string> + <string name="servers_info_detailed_statistics_sent_messages_total">Összes elküldött üzenet</string> + <string name="sent_via_proxy">Proxyn keresztül küldött</string> + <string name="smp_server">SMP-kiszolgáló</string> + <string name="servers_info_starting_from">Statisztikagyűjtés kezdete: %s.</string> + <string name="servers_info_uploaded">Feltöltve</string> + <string name="xftp_server">XFTP-kiszolgáló</string> + <string name="proxied">Proxyzott</string> + <string name="duplicates_label">duplikációk</string> + <string name="other_errors">egyéb hibák</string> + <string name="connections">Kapcsolatok</string> + <string name="created">Létrehozva</string> + <string name="secured">Biztosítva</string> + <string name="deletion_errors">Törlési hibák</string> + <string name="size">Méret</string> + <string name="uploaded_files">Feltöltött fájlok</string> + <string name="chunks_downloaded">Letöltött töredékek</string> + <string name="downloaded_files">Letöltött fájlok</string> + <string name="open_server_settings_button">Kiszolgáló-beállítások megnyitása</string> + <string name="server_address">Kiszolgáló címe</string> + <string name="upload_errors">Feltöltési hibák</string> + <string name="acknowledged">Visszaigazolt</string> + <string name="acknowledgement_errors">Visszaigazolási hibák</string> + <string name="attempts_label">próbálkozások</string> + <string name="chunks_deleted">Törölt töredékek</string> + <string name="all_users">Összes profil</string> + <string name="chunks_uploaded">Feltöltött töredékek</string> + <string name="completed">Elkészült</string> + <string name="servers_info_connected_servers_section_header">Kapcsolódott kiszolgálók</string> + <string name="xftp_servers_configured">Konfigurált XFTP-kiszolgálók</string> + <string name="servers_info_sessions_connected">Kapcsolódott</string> + <string name="current_user">Jelenlegi profil</string> + <string name="servers_info_details">További részletek</string> + <string name="decryption_errors">visszafejtési hibák</string> + <string name="deleted">Törölve</string> + <string name="servers_info_messages_received">Fogadott üzenetek</string> + <string name="download_errors">Letöltési hibák</string> + <string name="servers_info_modal_error_title">Hiba</string> + <string name="servers_info_reconnect_servers_error">Hiba történt a kiszolgálókhoz való újrakapcsolódáskor</string> + <string name="servers_info_files_tab">Fájlok</string> + <string name="appearance_font_size">Betűméret</string> + <string name="servers_info_missing">Nincs információ, próbálja meg újratölteni</string> + <string name="servers_info_previously_connected_servers_section_header">Korábban kapcsolódott kiszolgálók</string> + <string name="private_routing_error">Privát útválasztási hiba</string> + <string name="servers_info_detailed_statistics_received_messages_header">Fogadott üzenetek</string> + <string name="servers_info_reconnect_servers_message">Az összes kiszolgálóhoz való újrakapcsolódás az üzenetkézbesítési jelentések kikényszerítéséhez. Ez további adatforgalmat használ.</string> + <string name="servers_info_reconnect_server_message">A kiszolgálóhoz való újrakapcsolódás az üzenetkézbesítési jelentések kikényszerítéséhez. Ez további adatforgalmat használ.</string> + <string name="servers_info_detailed_statistics_sent_messages_header">Elküldött üzenetek</string> + <string name="servers_info_transport_sessions_section_header">Munkamenetek átvitele</string> + <string name="servers_info_subscriptions_total">Összes kapcsolat</string> + <string name="servers_info_statistics_section_header">Statisztikák</string> + <string name="servers_info_target">Információk megjelenítése a következőhöz</string> + <string name="network_error_broker_version_desc">A kiszolgáló verziója nem kompatibilis az alkalmazással: %1$s.</string> + <string name="servers_info_proxied_servers_section_footer">Ön nem kapcsolódik ezekhez a kiszolgálókhoz. A privát útválasztás az üzenetek kézbesítésére szolgál.</string> + <string name="servers_info_subscriptions_connections_subscribed">Aktív kapcsolatok száma</string> + <string name="servers_info_subscriptions_section_header">Üzenetjelentés</string> + <string name="subscribed">Feliratkozva</string> + <string name="subscription_errors">Feliratkozási hibák</string> + <string name="subscription_results_ignored">Mellőzött feliratkozások</string> + <string name="subscription_percentage">Százalék megjelenítése</string> + <string name="app_check_for_updates_download_completed_title">Alkalmazásfrissítés letöltve</string> + <string name="app_check_for_updates">Frissítések keresése</string> + <string name="app_check_for_updates_notice_title">Frissítések keresése</string> + <string name="app_check_for_updates_download_started">Alkalmazásfrissítés letöltése, ne zárja be az alkalmazást</string> + <string name="app_check_for_updates_button_download">Letöltés – %s (%s)</string> + <string name="app_check_for_updates_installed_successfully_title">Sikeresen telepítve</string> + <string name="app_check_for_updates_button_install">Frissítés telepítése</string> + <string name="app_check_for_updates_button_open">Fájl helyének megnyitása</string> + <string name="app_check_for_updates_installed_successfully_desc">Indítsa újra az alkalmazást.</string> + <string name="app_check_for_updates_button_remind_later">Emlékeztessen később</string> + <string name="app_check_for_updates_button_skip">Ezen verzió kihagyása</string> + <string name="app_check_for_updates_notice_desc">Ha értesítést szeretne kapni az új kiadásokról, kapcsolja be a stabil vagy béta verziók időszakos ellenőrzését.</string> + <string name="app_check_for_updates_update_available">Új verzió érhető el: %s</string> + <string name="app_check_for_updates_canceled">A frissítés letöltése megszakítva</string> + <string name="app_check_for_updates_beta">Béta</string> + <string name="app_check_for_updates_notice_disable">Letiltás</string> + <string name="app_check_for_updates_disabled">Letiltva</string> + <string name="app_check_for_updates_stable">Stabil</string> + <string name="smp_proxy_error_connecting">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.</string> + <string name="proxy_destination_error_broker_version">A(z) %1$s célkiszolgáló verziója nem kompatibilis a(z) %2$s továbbítókiszolgálóval.</string> + <string name="proxy_destination_error_failed_to_connect">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.</string> + <string name="proxy_destination_error_broker_host">A(z) %1$s célkiszolgáló címe nem kompatibilis a(z) %2$s továbbítókiszolgáló beállításaival.</string> + <string name="privacy_media_blur_radius">Médiatartalom elhomályosítása</string> + <string name="privacy_media_blur_radius_medium">Közepes</string> + <string name="privacy_media_blur_radius_off">Kikapcsolva</string> + <string name="privacy_media_blur_radius_soft">Enyhe</string> + <string name="privacy_media_blur_radius_strong">Erős</string> + <string name="smp_proxy_error_broker_host">A továbbítókiszolgáló címe nem kompatibilis a hálózati beállításokkal: %1$s.</string> + <string name="smp_proxy_error_broker_version">A továbbítókiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %1$s.</string> + <string name="info_view_call_button">hívás</string> + <string name="delete_contact_cannot_undo_warning">A partner törölve lesz – ez a művelet nem vonható vissza!</string> + <string name="only_delete_conversation">Csak a beszélgetés törlése</string> + <string name="info_view_open_button">megnyitás</string> + <string name="conversation_deleted">Beszélgetés törölve!</string> + <string name="contact_deleted">Partner törölve!</string> + <string name="deleted_chats">Archivált partnerek</string> + <string name="no_filtered_contacts">Nincsenek szűrt partnerek</string> + <string name="paste_link">Hivatkozás beillesztése</string> + <string name="calls_prohibited_alert_title">A hívások le vannak tiltva!</string> + <string name="cant_call_contact_alert_title">Nem lehet felhívni a partnert</string> + <string name="cant_send_message_to_member_alert_title">Nem lehet üzenetet küldeni a csoporttagnak</string> + <string name="cant_call_contact_connecting_wait_alert_text">Kapcsolódás a partnerhez, várjon vagy ellenőrizze később!</string> + <string name="cant_call_contact_deleted_alert_text">Törölt partner.</string> + <string name="cant_call_member_alert_title">Nem lehet felhívni a csoporttagot</string> + <string name="allow_calls_question">Engedélyezi a hívásokat?</string> + <string name="action_button_add_members">Meghívás</string> + <string name="info_view_message_button">üzenet</string> + <string name="keep_conversation">Beszélgetés megtartása</string> + <string name="confirm_delete_contact_question">Biztosan törli a partnert?</string> + <string name="info_view_connect_button">kapcsolódás</string> + <string name="one_hand_ui">Könnyen elérhető alkalmazás-eszköztárak</string> + <string name="delete_without_notification">Törlés értesítés nélkül</string> + <string name="toolbar_settings">Beállítások</string> + <string name="info_view_search_button">keresés</string> + <string name="info_view_video_button">videó</string> + <string name="you_can_still_send_messages_to_contact">Az „Archivált partnerekből” továbbra is küldhet üzeneteket neki: %1$s.</string> + <string name="contact_list_header_title">Partnerek</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">Kérje meg a partnerét, hogy engedélyezze a hívásokat.</string> + <string name="cant_call_member_send_message_alert_text">Üzenet küldése a hívások engedélyezéséhez.</string> + <string name="you_need_to_allow_calls">Engedélyeznie kell a hívásokat a partnere számára, hogy fel tudják hívni egymást.</string> + <string name="you_can_still_view_conversation_with_contact">A(z) %1$s nevű partnerével folytatott beszélgetéseit továbbra is megtekintheti a csevegések listájában.</string> + <string name="compose_message_placeholder">Üzenet…</string> + <string name="select_verb">Kijelölés</string> + <string name="moderate_messages_will_be_marked_warning">Az üzenetek az összes tag számára moderáltként lesznek megjelölve.</string> + <string name="selected_chat_items_nothing_selected">Nincs semmi kijelölve</string> + <string name="delete_messages_mark_deleted_warning">Az üzenetek törlésre lesznek jelölve. A címzett(ek) képes(ek) lesz(nek) felfedni ezt az üzenetet.</string> + <string name="delete_members_messages__question">Törli a tagok %d üzenetét?</string> + <string name="selected_chat_items_selected_n">%d kijelölve</string> + <string name="moderate_messages_will_be_deleted_warning">Az üzenetek az összes tag számára törölve lesznek.</string> + <string name="chat_database_exported_title">Csevegési adatbázis exportálva</string> + <string name="v6_0_connection_servers_status_descr">Kapcsolatok- és kiszolgálók állapotának megjelenítése.</string> + <string name="v6_0_connect_faster_descr">Kapcsolódjon gyorsabban a partnereihez.</string> + <string name="chat_database_exported_continue">Folytatás</string> + <string name="v6_0_connection_servers_status">Ellenőrizze a hálózatát</string> + <string name="media_and_file_servers">Média- és fájlkiszolgálók</string> + <string name="v6_0_delete_many_messages_descr">Legfeljebb 20 üzenet egyszerre való törlése.</string> + <string name="v6_0_private_routing_descr">Védi az IP-címét és a kapcsolatait.</string> + <string name="v6_0_reachable_chat_toolbar">Könnyen elérhető eszköztár</string> + <string name="message_servers">Üzenetkiszolgálók</string> + <string name="network_socks_proxy">SOCKS-proxy</string> + <string name="chat_database_exported_not_all_files">Néhány fájl nem lett exportálva</string> + <string name="chat_database_exported_migrate">Az exportált adatbázist átköltöztetheti.</string> + <string name="network_options_save_and_reconnect">Mentés és újrakapcsolódás</string> + <string name="v6_0_reachable_chat_toolbar_descr">Használja az alkalmazást egy kézzel.</string> + <string name="v6_0_your_contacts_descr">A partnerek archiválása a későbbi csevegéshez.</string> + <string name="network_option_tcp_connection">TCP-kapcsolat</string> + <string name="chat_database_exported_save">Az exportált archívumot elmentheti.</string> + <string name="reset_all_hints">Tippek visszaállítása</string> + <string name="one_hand_ui_card_title">Csevegési lista átváltása:</string> + <string name="one_hand_ui_change_instruction">Ezt a „Megjelenés” menüben módosíthatja.</string> + <string name="v6_0_new_media_options">Új médiabeállítások</string> + <string name="v6_0_chat_list_media">Lejátszás a csevegési listából.</string> + <string name="v6_0_privacy_blur">Elhomályosítás a jobb adatvédelemért.</string> + <string name="v6_0_upgrade_app">Automatikus alkalmazás-frissítés</string> + <string name="create_address_button">Létrehozás</string> + <string name="v6_0_upgrade_app_descr">Új verziók letöltése a GitHubról.</string> + <string name="v6_0_increase_font_size">Betűméret növelése.</string> + <string name="invite_friends_short">Meghívás</string> + <string name="v6_0_new_chat_experience">Új csevegési élmény 🎉</string> + <string name="new_message">Új üzenet</string> + <string name="error_parsing_uri_title">Érvénytelen hivatkozás</string> + <string name="error_parsing_uri_desc">Ellenőrizze, hogy a SimpleX-hivatkozás helyes-e.</string> + <string name="switching_profile_error_title">Hiba történt a profilváltáskor</string> + <string name="switching_profile_error_message">A kapcsolata át lett helyezve ide: %s, de egy váratlan hiba történt a profilra való átirányításkor.</string> + <string name="delete_messages_cannot_be_undone_warning">Az üzenetek törölve lesznek – ez a művelet nem vonható vissza!</string> + <string name="migrate_from_device_remove_archive_question">Eltávolítja az archívumot?</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">A feltöltött adatbázis-archívum véglegesen el lesz távolítva a kiszolgálókról.</string> + <string name="settings_section_title_chat_database">CSEVEGÉSI ADATBÁZIS</string> + <string name="new_chat_share_profile">Profil megosztása</string> + <string name="system_mode_toast">Rendszerbeállítások használata</string> + <string name="select_chat_profile">Csevegési profil kijelölése</string> + <string name="network_proxy_auth_mode_no_auth">Ne használja a hitelesítőadatokat proxyval.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">Különböző proxy-hitelesítőadatok használata az összes profilhoz.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">Különböző proxy-hitelesítőadatok használata az összes kapcsolathoz.</string> + <string name="network_proxy_password">Jelszó</string> + <string name="network_proxy_username">Felhasználónév</string> + <string name="network_proxy_auth_mode_username_password">A hitelesítőadatai titkosítatlanul is elküldhetők.</string> + <string name="network_proxy_incorrect_config_title">Hiba történt a proxy mentésekor</string> + <string name="network_proxy_incorrect_config_desc">Győződjön meg arról, hogy a proxy konfigurációja helyes.</string> + <string name="network_proxy_auth">Proxyhitelesítés</string> + <string name="network_proxy_random_credentials">Véletlenszerű hitelesítőadatok használata</string> + <string name="n_other_file_errors">%1$d egyéb fájlhiba.</string> + <string name="forward_alert_title_nothing_to_forward">Nincs mit továbbítani!</string> + <string name="forward_files_in_progress_desc">%1$d fájl letöltése még folyamatban van.</string> + <string name="forward_files_failed_to_receive_desc">Nem sikerült letölteni %1$d fájlt.</string> + <string name="forward_files_not_accepted_desc">%1$d fájl nem lett letöltve.</string> + <string name="forward_files_not_accepted_receive_files">Letöltés</string> + <string name="forward_files_missing_desc">%1$d fájl törölve lett.</string> + <string name="compose_forward_messages_n">%1$s üzenet továbbítása</string> + <string name="forward_multiple">Üzenetek továbbítása…</string> + <string name="n_file_errors">%1$d fájlhiba:\n%2$s</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s üzenet nem lett továbbítva</string> + <string name="forward_alert_title_messages_to_forward">Továbbít %1$s üzenetet?</string> + <string name="forward_alert_forward_messages_without_files">Továbbítja az üzeneteket fájlok nélkül?</string> + <string name="forward_files_messages_deleted_after_selection_desc">Az üzeneteket törölték miután kijelölte őket.</string> + <string name="compose_save_messages_n">%1$s üzenet mentése</string> + <string name="error_forwarding_messages">Hiba történt az üzenetek továbbításakor</string> + <string name="icon_descr_sound_muted">Hang elnémítva</string> + <string name="error_initializing_web_view_wrong_arch">Hiba történt a WebView előkészítésekor. Győződjön meg arról, hogy a WebView telepítve van-e, és támogatja-e az arm64 architektúrát.\nHiba: %s</string> + <string name="settings_message_shape_corner">Sarok</string> + <string name="settings_section_title_message_shape">Üzenetbuborék alakja</string> + <string name="settings_message_shape_tail">Farok</string> + <string name="network_session_mode_server">Kiszolgáló</string> + <string name="network_session_mode_session_description">Minden alkalommal, amikor elindítja az alkalmazást, új SOCKS-hitelesítő-adatokat fog használni.</string> + <string name="network_session_mode_session">Alkalmazás munkamenete</string> + <string name="network_session_mode_server_description">Az összes kiszolgálóhoz új, SOCKS-hitelesítő-adatok legyenek használva.</string> + <string name="call_desktop_permission_denied_chrome">Kattintson a címmező melletti info gombra a mikrofon használatának engedélyezéséhez.</string> + <string name="call_desktop_permission_denied_safari">Nyissa meg a Safari Beállítások / Weboldalak / Mikrofon menüt, majd válassza a helyi kiszolgálók engedélyezése lehetőséget.</string> + <string name="call_desktop_permission_denied_title">Hívások kezdeményezéséhez engedélyezze a mikrofon használatát. Fejezze be a hívást, és próbálja meg a hívást újra.</string> + <string name="v6_1_better_calls">Továbbfejlesztett hívásélmény</string> + <string name="v6_1_message_dates_descr">Továbbfejlesztett üzenetdátumok.</string> + <string name="v6_1_better_user_experience">Továbbfejlesztett felhasználói élmény</string> + <string name="v6_1_customizable_message_descr">Személyre szabható üzenetbuborékok.</string> + <string name="v6_1_delete_many_messages_descr">Legfeljebb 200 üzenet egyszerre való törlése, vagy moderálása.</string> + <string name="v6_1_forward_many_messages_descr">Legfeljebb 20 üzenet egyszerre való továbbítása.</string> + <string name="v6_1_better_calls_descr">Hang/Videó váltása hívás közben.</string> + <string name="v6_1_switch_chat_profile_descr">Csevegési profilváltás az egyszer használható meghívókhoz.</string> + <string name="v6_1_better_security">Továbbfejlesztett biztonság ✅</string> + <string name="v6_1_better_security_descr">A SimpleX Chat biztonsága a Trail of Bits által lett felülvizsgálva.</string> + <string name="failed_to_save_servers">Hiba történt a kiszolgálók mentésekor</string> + <string name="no_message_servers_configured">Nincsenek üzenet-kiszolgálók.</string> + <string name="no_message_servers_configured_for_receiving">Nincsenek üzenetfogadási kiszolgálók.</string> + <string name="no_media_servers_configured">Nincsenek média- és fájlkiszolgálók.</string> + <string name="for_chat_profile">A(z) %s nevű csevegési profilhoz:</string> + <string name="address_or_1_time_link">Cím vagy egyszer használható meghívó?</string> + <string name="smp_servers_new_server">Új kiszolgáló</string> + <string name="address_settings">Címbeállítások</string> + <string name="network_preset_servers_title">Előre beállított kiszolgálók</string> + <string name="operator">Üzemeltető</string> + <string name="view_conditions">Feltételek megtekintése</string> + <string name="no_message_servers_configured_for_private_routing">Nincsenek kiszolgálók a privát üzenet-útválasztáshoz.</string> + <string name="no_media_servers_configured_for_sending">Nincsenek fájlküldő-kiszolgálók.</string> + <string name="no_media_servers_configured_for_private_routing">Nincsenek fájlfogadási kiszolgálók.</string> + <string name="errors_in_servers_configuration">Hibák a kiszolgálók konfigurációjában.</string> + <string name="error_accepting_operator_conditions">Hiba történt a feltételek elfogadásakor</string> + <string name="connection_error_quota">Kézbesítetlen üzenetek</string> + <string name="connection_error_quota_desc">A kapcsolat elérte a kézbesítetlen üzenetek számának határát, a partnere lehet, hogy offline állapotban van.</string> + <string name="message_deleted_or_not_received_error_title">Nincs üzenet</string> + <string name="message_deleted_or_not_received_error_desc">Ez az üzenet törölve lett vagy még nem érkezett meg.</string> + <string name="address_creation_instruction">Koppintson a SimpleX-cím létrehozása menüpontra a későbbi létrehozáshoz.</string> + <string name="share_address_publicly">Cím nyilvános megosztása</string> + <string name="share_simplex_address_on_social_media">SimpleX-cím megosztása a közösségi médiában.</string> + <string name="share_1_time_link_with_a_friend">Egyszer használható meghívó megosztása egy baráttal</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[Az egyszer használható meghívó egy hivatkozás és <i>csak egyetlen partnerrel használható</i> – személyesen vagy bármilyen üzenetváltó-alkalmazáson keresztül megosztható.]]></string> + <string name="you_can_set_connection_name_to_remember">Beállíthatja a partner nevét, hogy emlékezzen arra, hogy kivel osztotta meg a hivatkozást.</string> + <string name="connection_security">Kapcsolatbiztonság</string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">A SimpleX-cím és az egyszer használható meghívó biztonságosan megosztható bármilyen üzenetváltó-alkalmazáson keresztül.</string> + <string name="to_protect_against_your_link_replaced_compare_codes">A hivatkozás cseréje elleni védelem érdekében összehasonlíthatja a biztonsági kódokat a partnerével.</string> + <string name="for_social_media">A közösségi médiához</string> + <string name="or_to_share_privately">Vagy a privát megosztáshoz</string> + <string name="simplex_address_or_1_time_link">SimpleX-cím vagy egyszer használható meghívó?</string> + <string name="create_1_time_link">Egyszer használható meghívó létrehozása</string> + <string name="onboarding_choose_server_operators">Kiszolgálóüzemeltetők</string> + <string name="onboarding_network_operators">Hálózatüzemeltetők</string> + <string name="onboarding_network_operators_app_will_use_different_operators">Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetéshez más-más üzemeltetőt használ.</string> + <string name="onboarding_network_operators_app_will_use_for_routing">Például, ha a partnere egy SimpleX Chat-kiszolgálón keresztül fogadja az üzeneteket, akkor az Ön alkalmazása egy Flux-kiszolgálón keresztül fogja azokat kézbesíteni.</string> + <string name="onboarding_select_network_operators_to_use">Jelölje ki a használni kívánt hálózatüzemeltetőket.</string> + <string name="onboarding_network_operators_review_later">Felülvizsgálat később</string> + <string name="onboarding_network_operators_configure_via_settings">A kiszolgálókat a „Hálózat és kiszolgálók” menüben konfigurálhatja.</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">A feltételek 30 nap elteltével lesznek elfogadva az engedélyezett üzemeltetők számára.</string> + <string name="onboarding_network_operators_conditions_you_can_configure">Az üzemeltetőket a „Hálózat és kiszolgálók” menüben konfigurálhatja.</string> + <string name="onboarding_network_operators_update">Frissítés</string> + <string name="onboarding_network_operators_continue">Folytatás</string> + <string name="operator_review_conditions">Feltételek felülvizsgálata</string> + <string name="operator_conditions_accepted">Elfogadott feltételek</string> + <string name="operator_conditions_accepted_for_enabled_operators_on">A feltételek automatikusan el lesznek fogadva az engedélyezett üzemeltetők számára a következő időpontban: %s.</string> + <string name="your_servers">Saját kiszolgálók</string> + <string name="operators_conditions_accepted_for"><![CDATA[A következő üzemeltető(k) számára elfogadott feltételek: <b>%s</b>.]]></string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[A feltételek el lesznek fogadva a következő üzemeltető(k) számára: <b>%s</b>.]]></string> + <string name="operator_servers_title">%s kiszolgáló</string> + <string name="operator_info_title">Hálózatüzemeltető</string> + <string name="operator_website">Weboldal</string> + <string name="operator_conditions_accepted_on">Feltételek elfogadásának ideje: %s.</string> + <string name="operator_conditions_will_be_accepted_on">A feltételek el lesznek elfogadva a következő időpontban: %s.</string> + <string name="operator_use_operator_toggle_description">Kiszolgálók használata</string> + <string name="use_servers_of_operator_x">%s használata</string> + <string name="operator_conditions_failed_to_load">A jelenlegi feltételek szövegét nem sikerült betölteni, a feltételeket a következő hivatkozáson keresztül vizsgálhatja felül:</string> + <string name="operator_conditions_accepted_for_some"><![CDATA[A feltételek már el lettek fogadva a következő üzemeltető(k) számára: <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető számára is: <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k) számára is: <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_applied"><![CDATA[Ezek a feltételek lesznek elfogadva a következő számára is: <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[A feltételek el lesznek fogadva a következő üzemeltető(k) számára: <b>%s</b>.]]></string> + <string name="operators_conditions_will_also_apply"><![CDATA[Ezek a feltételek lesznek elfogadva a következő számára is: <b>%s</b>.]]></string> + <string name="accept_conditions">Feltételek elfogadása</string> + <string name="operator_conditions_of_use">Használati feltételek</string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[A(z) <b>%s</b> kiszolgálóinak használatához fogadja el a használati feltételeket.]]></string> + <string name="operator_use_for_messages">Használat az üzenetekhez</string> + <string name="operator_use_for_messages_receiving">A fogadáshoz</string> + <string name="operator_use_for_messages_private_routing">A privát útválasztáshoz</string> + <string name="operator_added_message_servers">Hozzáadott üzenetkiszolgálók</string> + <string name="operator_use_for_files">Használat a fájlokhoz</string> + <string name="operator_use_for_sending">A küldéshez</string> + <string name="operator_added_xftp_servers">Hozzáadott média- és fájlkiszolgálók</string> + <string name="operator_open_conditions">Feltételek megnyitása</string> + <string name="operator_open_changes">Módosítások megtekintése</string> + <string name="error_updating_server_title">Hiba történt a kiszolgáló frissítésekor</string> + <string name="error_server_protocol_changed">A kiszolgáló-protokoll módosult.</string> + <string name="error_server_operator_changed">A kiszolgáló üzemeltetője módosult.</string> + <string name="operator_server_alert_title">Kiszolgáló-üzemeltető</string> + <string name="server_added_to_operator__name">Kiszolgáló hozzáadva a következő üzemeltetőhöz: %s.</string> + <string name="error_adding_server">Hiba történt a kiszolgáló hozzáadásakor</string> + <string name="appearance_in_app_bars_alpha">Átlátszóság</string> + <string name="appearance_bars_blur_radius">Elhomályosítás</string> + <string name="v6_2_network_decentralization">Hálózati decentralizáció</string> + <string name="v6_2_network_decentralization_descr">A második előre beállított üzemeltető az alkalmazásban!</string> + <string name="v6_2_network_decentralization_enable_flux">A Flux kiszolgálókat engedélyezheti a beállításokban, a „Hálózat és kiszolgálók” menüben, a metaadatok jobb védelme érdekében.</string> + <string name="appearance_app_toolbars">Alkalmazás-eszköztárak</string> + <string name="v6_2_network_decentralization_enable_flux_reason">a metaadatok jobb védelme érdekében.</string> + <string name="v6_2_improved_chat_navigation">Javított csevegési navigáció</string> + <string name="v6_2_improved_chat_navigation_descr">- Csevegés megnyitása az első olvasatlan üzenetnél.\n- Ugrás az idézett üzenetekre.</string> + <string name="view_updated_conditions">Frissített feltételek megtekintése</string> + <string name="xftp_servers_per_user">A jelenlegi csevegési profiljához tartozó új fájlok kiszolgálói</string> + <string name="chat_archive">Vagy archívumfájl importálása</string> + <string name="remote_hosts_section">Távoli hordozható eszközök</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Xiaomi eszközök</b>: engedélyezze az automatikus indítást a rendszerbeállításokban, hogy az értesítések működjenek.]]></string> + <string name="maximum_message_size_reached_forwarding">A küldéshez másolhatja és csökkentheti az üzenet méretét.</string> + <string name="add_your_team_members_to_conversations">Adja hozzá a munkatársait a beszélgetésekhez.</string> + <string name="business_address">Üzleti cím</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[Az összes üzenet és fájl <b>végpontok közötti titkosítással</b>, a közvetlen üzenetek továbbá kvantumbiztos titkosítással is rendelkeznek.]]></string> + <string name="how_it_helps_privacy">Hogyan segíti az adatvédelmet</string> + <string name="onboarding_notifications_mode_off_desc_short">Nincs háttérszolgáltatás</string> + <string name="onboarding_notifications_mode_battery">Értesítések és akkumulátor</string> + <string name="onboarding_notifications_mode_service_desc_short">Az alkalmazás mindig fut a háttérben</string> + <string name="leave_chat_question">Elhagyja a csevegést?</string> + <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Ön nem fog több üzenetet kapni ebből a csevegésből, de a csevegés előzményei megmaradnak.</string> + <string name="button_delete_chat">Csevegés törlése</string> + <string name="invite_to_chat_button">Meghívás a csevegésbe</string> + <string name="button_add_friends">Barátok hozzáadása</string> + <string name="button_add_team_members">Munkatársak hozzáadása</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">A csevegés minden tag számára törölve lesz – ez a művelet nem vonható vissza!</string> + <string name="delete_chat_for_self_cannot_undo_warning">A csevegés törölve lesz az Ön számára – ez a művelet nem vonható vissza!</string> + <string name="delete_chat_question">Törli a csevegést?</string> + <string name="button_leave_chat">Csevegés elhagyása</string> + <string name="only_chat_owners_can_change_prefs">Csak a csevegés tulajdonosai módosíthatják a csevegési beállításokat.</string> + <string name="chat_bottom_bar">Könnyen elérhető csevegési eszköztár</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">A tag el lesz távolítva a csevegésből – ez a művelet nem vonható vissza!</string> + <string name="info_row_chat">Csevegés</string> + <string name="member_role_will_be_changed_with_notification_chat">A tag szerepköre a következőre fog módosulni: „%s”. A csevegés összes tagja értesítést fog kapni.</string> + <string name="chat_main_profile_sent">A csevegési profilja el lesz küldve a csevegésben résztvevő tagok számára</string> + <string name="direct_messages_are_prohibited">A tagok közötti közvetlen üzenetek le vannak tiltva.</string> + <string name="v6_2_business_chats">Üzleti csevegések</string> + <string name="v6_2_business_chats_descr">Saját ügyfeleinek adatvédelme.</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Ön már kapcsolódva van vele: <b>%1$s</b>.]]></string> + <string name="connect_plan_chat_already_exists">A csevegés már létezik!</string> + <string name="maximum_message_size_reached_text">Csökkentse az üzenet méretét, és küldje el újra.</string> + <string name="onboarding_notifications_mode_periodic_desc_short">Üzenetek ellenőrzése 10 percenként</string> + <string name="maximum_message_size_title">Az üzenet túl nagy!</string> + <string name="maximum_message_size_reached_non_text">Csökkentse az üzenet méretét vagy távolítsa el a médiát, és küldje el újra.</string> + <string name="direct_messages_are_prohibited_in_chat">A tagok közötti közvetlen üzenetek le vannak tiltva ebben a csevegésben.</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">Amikor egynél több üzemeltető van engedélyezve, akkor egyik sem rendelkezik olyan metaadatokkal, amelyekből megtudható, hogy ki kivel kommunikál.</string> + <string name="display_name_accepted_invitation">elfogadott meghívó</string> + <string name="display_name_requested_to_connect">Függőben lévő meghívási kérelem</string> + <string name="onboarding_network_about_operators">Az üzemeltetőkről</string> + <string name="onboarding_network_operators_simplex_flux_agreement">A SimpleX Chat és a Flux megállapodást kötött arról, hogy a Flux által üzemeltetett kiszolgálókat beépítik az alkalmazásba.</string> + <string name="encryption_renegotiation_in_progress">A titkosítás újraegyeztetése folyamatban van.</string> + <string name="sync_connection_desc">A kapcsolat titkosítása újraegyeztetést igényel.</string> + <string name="sync_connection_confirm">Javítás</string> + <string name="sync_connection_question">Kapcsolat javítása?</string> + <string name="debug_logs">Naplózás engedélyezése</string> + <string name="error_saving_database">Hiba történt az adatbázis mentésekor</string> + <string name="strikethrough_text">áthúzott</string> + <string name="users_delete_profile_for">A következő csevegési profil törlése</string> + <string name="delete_messages_after">Üzenetek törlése ennyi idő után</string> + <string name="a_plus_b">a + b</string> + <string name="connection_not_ready">A kapcsolat nem áll készen.</string> + <string name="open_with_app">Megnyitás a következővel: %s</string> + <string name="list_menu">Lista</string> + <string name="chat_list_groups">Csoportok</string> + <string name="chat_list_add_list">Lista hozzáadása</string> + <string name="chat_list_all">Összes</string> + <string name="add_to_list">Hozzáadás listához</string> + <string name="error_creating_chat_tags">Hiba történt a csevegési lista létrehozásakor</string> + <string name="error_loading_chat_tags">Hiba történt a csevegési lista betöltésekor</string> + <string name="error_updating_chat_tags">Hiba történt a csevegési lista frissítésekor</string> + <string name="chat_list_businesses">Üzleti</string> + <string name="chat_list_contacts">Partnerek</string> + <string name="chat_list_favorites">Kedvencek</string> + <string name="no_chats">Nincsenek csevegések</string> + <string name="no_chats_found">Nem találhatók csevegések</string> + <string name="no_unread_chats">Nincsenek olvasatlan csevegések</string> + <string name="create_list">Lista létrehozása</string> + <string name="save_list">Lista mentése</string> + <string name="delete_chat_list_warning">Az összes csevegés el lesz távolítva a következő listáról, és a lista is törlődik: %s</string> + <string name="delete_chat_list_menu_action">Törlés</string> + <string name="delete_chat_list_question">Törli a listát?</string> + <string name="edit_chat_list_menu_action">Szerkesztés</string> + <string name="list_name_field_placeholder">Lista neve…</string> + <string name="duplicated_list_error">Az összes lista nevének és emodzsijának különbözőnek kell lennie.</string> + <string name="no_chats_in_list">Nincsenek csevegések a(z) %s nevű listában.</string> + <string name="chat_list_notes">Jegyzetek</string> + <string name="change_list">Lista módosítása</string> + <string name="change_order_chat_list_menu_action">Elrendezés módosítása</string> + <string name="error_creating_report">Hiba történt a jelentés létrehozásakor</string> + <string name="prefs_error_saving_settings">Hiba történt a beállítások mentésekor</string> + <string name="report_archive_alert_desc">A jelentés archiválva lesz az Ön számára.</string> + <string name="report_compose_reason_header_illegal">Tartalom jelentése: csak a csoport moderátorai látják.</string> + <string name="archive_verb">Archívum</string> + <string name="report_archive_alert_title">Archiválja a jelentést?</string> + <string name="report_item_archived">archivált jelentés</string> + <string name="group_member_role_moderator">moderátor</string> + <string name="report_reason_community">Közösségi irányelvek megsértése</string> + <string name="report_compose_reason_header_spam">Kéretlen tartalom jelentése: csak a csoport moderátorai látják.</string> + <string name="report_item_visibility_moderators">Csak a küldő és a moderátorok látják</string> + <string name="report_item_visibility_submitter">Csak Ön és a moderátorok látják</string> + <string name="report_reason_alert_title">Jelentés indoklása?</string> + <string name="report_reason_spam">Kéretlen tartalom</string> + <string name="report_reason_other">Egyéb indoklás</string> + <string name="report_reason_illegal">Kifogásolt tartalom</string> + <string name="report_reason_profile">Kifogásolt profil</string> + <string name="report_verb">Jelentés</string> + <string name="report_compose_reason_header_profile">Tag profiljának jelentése: csak a csoport moderátorai látják.</string> + <string name="report_compose_reason_header_other">Egyéb jelentés: csak a csoport moderátorai látják.</string> + <string name="report_compose_reason_header_community">Szabálysértés jelentése: csak a csoport moderátorai látják.</string> + <string name="archive_report">Jelentés archiválása</string> + <string name="delete_report">Jelentés törlése</string> + <string name="group_reports_member_reports">Tagok jelentései</string> + <string name="group_reports_active_one">1 jelentés</string> + <string name="chat_list_group_reports">Jelentések</string> + <string name="report_item_archived_by">%s által archivált jelentés</string> + <string name="group_reports_active">%d jelentés</string> + <string name="blocking_reason_spam">Kéretlen tartalom</string> + <string name="blocking_reason_content">A tartalom sérti a használati feltételeket</string> + <string name="connection_error_blocked">A kapcsolat le van tiltva</string> + <string name="file_error_blocked">A kiszolgáló üzemeltetője letiltotta a fájlt:\n%1$s.</string> + <string name="connection_error_blocked_desc">A kiszolgáló üzemeltetője letiltotta a kapcsolatot:\n%1$s.</string> + <string name="privacy_chat_list_open_links_ask">Mindig kérdezzen rá</string> + <string name="privacy_chat_list_open_links_yes">Igen</string> + <string name="privacy_chat_list_open_web_link">Hivatkozás megnyitása</string> + <string name="privacy_chat_list_open_links">Hivatkozás megnyitása a csevegési listából</string> + <string name="privacy_chat_list_open_links_no">Nem</string> + <string name="privacy_chat_list_open_web_link_question">Megnyitja a webhivatkozást?</string> + <string name="text_field_set_chat_placeholder">Csevegés nevének beállítása…</string> + <string name="disable_automatic_deletion_question">Letiltja az automatikus üzenettörlést?</string> + <string name="disable_automatic_deletion">Üzenetek törlésének letiltása</string> + <string name="disable_automatic_deletion_message">Az ebben a csevegésben lévő üzenetek soha nem lesznek törölve.</string> + <string name="chat_item_ttl_year">1 év</string> + <string name="chat_item_ttl_default">alapértelmezett (%s)</string> + <string name="chat_ttl_options_footer">Csevegési üzenetek törlése a saját eszközéről.</string> + <string name="change_automatic_deletion_question">Módosítja az automatikus üzenettörlést?</string> + <string name="change_automatic_chat_deletion_message">Ez a művelet nem vonható vissza – a kijelölt üzenettől korábban küldött és fogadott üzenetek törölve lesznek a csevegésből.</string> + <string name="network_smp_web_port_footer">A következő TCP-port használata, amikor nincs port megadva: %1$s.</string> + <string name="network_smp_web_port_section_title">TCP-port az üzenetváltáshoz</string> + <string name="network_smp_web_port_toggle">Webport használata</string> + <string name="unread_mentions">Olvasatlan említések</string> + <string name="mute_all_chat">Összes némítása</string> + <string name="max_group_mentions_per_message_reached">Legfeljebb %1$s tagot említhet meg egy üzenetben!</string> + <string name="enable_sending_member_reports">Az üzenetek jelentése a moderátorok felé engedélyezve van.</string> + <string name="disable_sending_member_reports">Az üzenetek a moderátorok felé történő jelentésének megtiltása.</string> + <string name="report_archive_alert_title_all">Archiválja az összes jelentést?</string> + <string name="report_archive_alert_title_nth">Archivál %d jelentést?</string> + <string name="report_archive_for_me">Csak magamnak</string> + <string name="archive_reports">Jelentések archiválása</string> + <string name="report_archive_for_all_moderators">Az összes moderátor számára</string> + <string name="member_reports_are_prohibited">Az üzenetek jelentése ebben a csoportban le van tiltva.</string> + <string name="group_members_can_send_reports">A tagok jelenthetik az üzeneteket a moderátorok felé.</string> + <string name="report_archive_alert_desc_all">Az összes jelentés archiválva lesz az Ön számára.</string> + <string name="notification_group_report">Jelentés: %s</string> + <string name="v6_3_mentions">Tagok említése 👋</string> + <string name="v6_3_reports">Privát jelentések küldése</string> + <string name="v6_3_organize_chat_lists_descr">Ne maradjon le a fontos üzenetekről.</string> + <string name="v6_3_faster_sending_messages">Gyorsabb üzenetküldés.</string> + <string name="v6_3_faster_deletion_of_groups">Gyorsabb csoporttörlés.</string> + <string name="v6_3_reports_descr">Segítsen az adminisztrátoroknak a csoportjaik moderálásában.</string> + <string name="v6_3_private_media_file_names">Privát nevek a médiafájlokhoz.</string> + <string name="v6_3_set_message_expiration_in_chats">Üzenetek eltűnési idejének módosítása a csevegésekben.</string> + <string name="v6_3_better_groups_performance">Továbbfejlesztett, gyorsabb csoportok</string> + <string name="v6_3_better_privacy_and_security">Továbbfejlesztett adatvédelem és biztonság</string> + <string name="v6_3_mentions_descr">Kapjon értesítést, ha megemlítik.</string> + <string name="v6_3_organize_chat_lists">Csevegések listákba szervezése</string> + <string name="group_preview_rejected">elutasítva</string> + <string name="group_member_status_rejected">elutasítva</string> + <string name="restore_passphrase_can_not_be_read_enter_manually_desc">A jelmondat nem olvasható a Keystore-ban, ezért kézzel szükséges megadni. Ez az alkalmazással nem kompatibilis rendszerfrissítés után történhetett meg. Ha nem így történt, akkor lépjen kapcsolatba a fejlesztőkkel.</string> + <string name="group_member_status_pending_approval_short">függőben</string> + <string name="group_member_status_pending_approval">jóváhagyásra vár</string> + <string name="error_reading_passphrase">Hiba történt az adatbázis-jelmondat olvasásakor</string> + <string name="restore_passphrase_can_not_be_read_desc">A jelmondat nem olvasható a Keystore-ban. Ez az alkalmazással nem kompatibilis rendszerfrissítés után történhetett meg. Ha nem így történt, akkor lépjen kapcsolatba a fejlesztőkkel.</string> + <string name="operator_updated_conditions">Frissített feltételek</string> + <string name="members_will_be_removed_from_group_cannot_be_undone">A tagok el lesznek távolítva a csoportból – ez a művelet nem vonható vissza!</string> + <string name="button_remove_members_question">Eltávolítja a tagokat?</string> + <string name="block_members_for_all_question">Az összes tag számára letiltja ezeket a tagokat?</string> + <string name="members_will_be_removed_from_chat_cannot_be_undone">A tagok el lesznek távolítva a csevegésből – ez a művelet nem vonható vissza!</string> + <string name="unblock_members_for_all_question">Az összes tag számára feloldja a tagok letiltását?</string> + <string name="block_members_desc">Ezen tagok összes új üzenete el lesz rejtve!</string> + <string name="unblock_members_desc">A tagok összes üzenete meg fog jelenni!</string> + <string name="feature_roles_moderators">moderátorok</string> + <string name="onboarding_conditions_accept">Elfogadás</string> + <string name="onboarding_conditions_by_using_you_agree">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.</string> + <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Adatvédelmi szabályzat és felhasználási feltételek.</string> + <string name="onboarding_conditions_private_chats_not_accessible">A privát csevegések, a csoportok és a partnerek nem érhetők el a szerver üzemeltetői számára.</string> + <string name="onboarding_conditions_configure_server_operators">Kiszolgálóüzemeltetők beállítása</string> + <string name="unsupported_connection_link">Nem támogatott kapcsolattartási hivatkozás</string> + <string name="privacy_short_links">Rövid hivatkozások használata (béta)</string> + <string name="short_link_button_text">Rövid hivatkozás</string> + <string name="full_link_button_text">Teljes hivatkozás</string> + <string name="link_requires_newer_app_version_please_upgrade">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.</string> + <string name="simplex_link_channel">SimpleX-csatornahivatkozás</string> + <string name="network_smp_web_port_all">Összes kiszolgáló</string> + <string name="network_smp_web_port_off">Kikapcsolva</string> + <string name="network_smp_web_port_preset">Előre beállított kiszolgálók</string> + <string name="network_smp_web_port_preset_footer">A 443-as TCP-port használata kizárólag az előre beállított kiszolgálokhoz.</string> +</resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/flux_logo@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/flux_logo@4x.png new file mode 100644 index 0000000000..87f1373d75 Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/flux_logo@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/flux_logo_light@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/flux_logo_light@4x.png new file mode 100644 index 0000000000..e1d6dda4fe Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/flux_logo_light@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/flux_logo_symbol@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/flux_logo_symbol@4x.png new file mode 100644 index 0000000000..0793b0ee85 Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/flux_logo_symbol@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_account_box.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_account_box.svg new file mode 100644 index 0000000000..07f5645f79 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_account_box.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M182-218q59.315-55.57 134.804-89.785Q392.293-342 479.896-342q87.604 0 163.197 34.215Q718.685-273.57 778-218v-560H182v560Zm300.232-200.5q57.268 0 96.518-39.482Q618-497.465 618-554.732q0-57.268-39.482-96.518-39.483-39.25-96.75-39.25-57.268 0-96.518 39.482Q346-611.535 346-554.268q0 57.268 39.482 96.518 39.483 39.25 96.75 39.25ZM182-124.5q-22.969 0-40.234-17.266Q124.5-159.031 124.5-182v-596q0-22.969 17.266-40.234Q159.031-835.5 182-835.5h596q22.969 0 40.234 17.266Q835.5-800.969 835.5-778v596q0 22.969-17.266 40.234Q800.969-124.5 778-124.5H182Zm52.5-57.5h491v-9.111Q671.5-237.5 609.161-261 546.823-284.5 480-284.5q-67.177 0-129.339 23.5Q288.5-237.5 234.5-191.111V-182Zm247.441-294q-32.733 0-55.587-22.913-22.854-22.913-22.854-55.646 0-32.733 22.913-55.587Q449.326-633 482.059-633q32.733 0 55.587 22.913 22.854 22.913 22.854 55.646 0 32.733-22.913 55.587Q514.674-476 481.941-476ZM480-498.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_account_circle_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_account_circle_filled.svg index f63966fbc8..db9ba45fab 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_account_circle_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_account_circle_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M224.89 800.5Q288 761 348.25 741T480 721q71.5 0 132 20t124.5 59.5Q781 746 799.25 691.351q18.25-54.648 18.25-115.25 0-144.101-96.75-240.851T480 238.5q-144 0-240.75 96.75T142.5 576.101q0 60.602 18.75 115.25Q180 746 224.89 800.5ZM479.869 605q-57.369 0-96.619-39.381-39.25-39.38-39.25-96.75 0-57.369 39.381-96.619 39.38-39.25 96.75-39.25 57.369 0 96.619 39.381 39.25 39.38 39.25 96.75 0 57.369-39.381 96.619-39.38 39.25-96.75 39.25Zm-.274 366q-81.553 0-154.09-31.263-72.538-31.263-125.772-85Q146.5 801 115.75 729.136 85 657.272 85 575.564q0-81.789 31.263-153.789 31.263-71.999 85-125.387Q255 243 326.864 212q71.864-31 153.572-31 81.789 0 153.795 31.132 72.005 31.131 125.387 84.5Q813 350 844 422.023q31 72.023 31 153.647 0 81.705-31.013 153.629-31.013 71.925-84.5 125.563Q706 908.5 633.827 939.75 561.655 971 479.595 971Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M224.89 800.5Q288 761 348.25 741T480 721q71.5 0 132 20t124.5 59.5Q781 746 799.25 691.351q18.25-54.648 18.25-115.25 0-144.101-96.75-240.851T480 238.5q-144 0-240.75 96.75T142.5 576.101q0 60.602 18.75 115.25Q180 746 224.89 800.5ZM479.869 605q-57.369 0-96.619-39.381-39.25-39.38-39.25-96.75 0-57.369 39.381-96.619 39.38-39.25 96.75-39.25 57.369 0 96.619 39.381 39.25 39.38 39.25 96.75 0 57.369-39.381 96.619-39.38 39.25-96.75 39.25Zm-.274 366q-81.553 0-154.09-31.263-72.538-31.263-125.772-85Q146.5 801 115.75 729.136 85 657.272 85 575.564q0-81.789 31.263-153.789 31.263-71.999 85-125.387Q255 243 326.864 212q71.864-31 153.572-31 81.789 0 153.795 31.132 72.005 31.131 125.387 84.5Q813 350 844 422.023q31 72.023 31 153.647 0 81.705-31.013 153.629-31.013 71.925-84.5 125.563Q706 908.5 633.827 939.75 561.655 971 479.595 971Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add.svg index 7da1daade8..da3f68daa4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M479.825 852q-12.325 0-20.325-8.375t-8-20.625V604.5h-219q-12.25 0-20.375-8.535T204 575.575q0-11.856 8.125-20.216Q220.25 547 232.5 547h219V328q0-11.675 8.175-20.088 8.176-8.412 20.5-8.412 12.325 0 20.575 8.412Q509 316.325 509 328v219h218.5q12.25 0 20.625 8.463t8.375 20.212q0 12.325-8.375 20.575T727.5 604.5H509V823q0 12.25-8.425 20.625-8.426 8.375-20.75 8.375Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M479.825 852q-12.325 0-20.325-8.375t-8-20.625V604.5h-219q-12.25 0-20.375-8.535T204 575.575q0-11.856 8.125-20.216Q220.25 547 232.5 547h219V328q0-11.675 8.175-20.088 8.176-8.412 20.5-8.412 12.325 0 20.575 8.412Q509 316.325 509 328v219h218.5q12.25 0 20.625 8.463t8.375 20.212q0 12.325-8.375 20.575T727.5 604.5H509V823q0 12.25-8.425 20.625-8.426 8.375-20.75 8.375Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_group.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_group.svg new file mode 100644 index 0000000000..158f4cfab0 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_group.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M497-481q28-30.5 43.25-70.5T555.5-634q0-42.5-15-82.5T497-787q57.5 8 95.5 51.75t38 101.25q0 57.5-38 101.25T497-481Zm199 308q10-17 15.25-36t5.25-39.09v-38.27q0-34.45-15-65.54-15-31.1-40-55.1 48.23 17.4 89.36 44.95Q792-334.5 792-286.5v38.5q0 30.94-22.03 52.97Q747.94-173 717-173h-21Zm96-348.5h-41.5q-15.5 0-26.5-11T713-559q0-15.5 11-26.5t26.5-11H792V-638q0-15.5 11-26.5t26.5-11q15.5 0 26.5 11t11 26.5v41.5h41.5q15.5 0 26.5 11t11 26.5q0 15.5-11 26.5t-26.5 11H867v41.5q0 15.5-11 26.5t-26.5 11q-15.5 0-26.5-11T792-480v-41.5ZM325.5-479q-64.5 0-109.75-45.25T170.5-634q0-64.5 45.25-109.75T325.5-789q64.5 0 109.75 45.25T480.5-634q0 64.5-45.25 109.75T325.5-479Zm-311 231v-31.03q0-32.97 16.75-60.22t45.27-41.76Q137.5-411 199.75-426.25 262-441.5 325.5-441.5t125.75 15.25Q513.5-411 574.48-381.01q28.52 14.51 45.27 41.76Q636.5-312 636.5-279.03V-248q0 30.94-22.03 52.97Q592.44-173 561.5-173h-472q-30.94 0-52.97-22.03Q14.5-217.06 14.5-248Zm311-306q33 0 56.5-23.5t23.5-56.5q0-33-23.5-56.5T325.5-714q-33 0-56.5 23.5T245.5-634q0 33 23.5 56.5t56.5 23.5Zm-236 306h472v-31q0-11.19-5.5-20.34-5.5-9.16-15-14.16-53.5-26.5-107.17-39.75-53.68-13.25-108.33-13.25-55 0-108.5 13.25T110-313.5q-9.5 5-15 14.16-5.5 9.15-5.5 20.34v31Zm236-386Zm0 386Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_link.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_link.svg index 6e3ec1e453..1530035ead 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_link.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_link.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M281 773.5q-84 0-140.25-56.534-56.25-56.533-56.25-140Q84.5 493.5 140.75 437 197 380.5 281 380.5h139q12.25 0 20.625 8.463T449 409.175q0 12.325-8.375 20.575T420 438H281q-60 0-99.5 39.5T142 577q0 60 39.5 99.5T281 716h139q12.25 0 20.625 8.463T449 744.675q0 12.325-8.375 20.575T420 773.5H281Zm73.5-168q-12.25 0-20.375-8.535T326 576.575q0-11.856 8.125-20.216Q342.25 548 354.5 548h248q12.25 0 20.625 8.463t8.375 20.212q0 12.325-8.375 20.575T602.5 605.5h-248Zm520.5-29h-57.5q0-60-39.792-99.5-39.791-39.5-99.208-39.5H539q-12.25 0-20.375-8.535t-8.125-20.39q0-11.856 8.125-20.216Q526.75 380 539 380h139.5q83.453 0 139.976 56.524Q875 493.047 875 576.5ZM726.825 893q-12.325 0-20.325-8.375t-8-20.625v-90H608q-12.25 0-20.375-8.535t-8.125-20.39q0-11.856 8.125-20.216Q595.75 716.5 608 716.5h90.5V626q0-11.675 8.175-20.088 8.176-8.412 20.5-8.412 12.325 0 20.575 8.412Q756 614.325 756 626v90.5h90q12.25 0 20.625 8.463T875 745.175q0 12.325-8.375 20.575T846 774h-90v90q0 12.25-8.425 20.625-8.426 8.375-20.75 8.375Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M281 773.5q-84 0-140.25-56.534-56.25-56.533-56.25-140Q84.5 493.5 140.75 437 197 380.5 281 380.5h139q12.25 0 20.625 8.463T449 409.175q0 12.325-8.375 20.575T420 438H281q-60 0-99.5 39.5T142 577q0 60 39.5 99.5T281 716h139q12.25 0 20.625 8.463T449 744.675q0 12.325-8.375 20.575T420 773.5H281Zm73.5-168q-12.25 0-20.375-8.535T326 576.575q0-11.856 8.125-20.216Q342.25 548 354.5 548h248q12.25 0 20.625 8.463t8.375 20.212q0 12.325-8.375 20.575T602.5 605.5h-248Zm520.5-29h-57.5q0-60-39.792-99.5-39.791-39.5-99.208-39.5H539q-12.25 0-20.375-8.535t-8.125-20.39q0-11.856 8.125-20.216Q526.75 380 539 380h139.5q83.453 0 139.976 56.524Q875 493.047 875 576.5ZM726.825 893q-12.325 0-20.325-8.375t-8-20.625v-90H608q-12.25 0-20.375-8.535t-8.125-20.39q0-11.856 8.125-20.216Q595.75 716.5 608 716.5h90.5V626q0-11.675 8.175-20.088 8.176-8.412 20.5-8.412 12.325 0 20.575 8.412Q756 614.325 756 626v90.5h90q12.25 0 20.625 8.463T875 745.175q0 12.325-8.375 20.575T846 774h-90v90q0 12.25-8.425 20.625-8.426 8.375-20.75 8.375Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_photo.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_photo.svg index 3197ae8bb6..be7fcc8f54 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_photo.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_photo.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M181.5 931q-22.969 0-40.234-17.266Q124 896.469 124 873.5V278q0-22.969 17.266-40.234Q158.531 220.5 181.5 220.5H561q12.25 0 20.625 8.463T590 249.175q0 12.325-8.375 20.575T561 278H181.5v595.5H777V495q0-12.25 8.425-20.625 8.426-8.375 20.5-8.375 12.075 0 20.325 8.375T834.5 495v378.5q0 22.969-17.266 40.234Q799.969 931 777 931H181.5Zm546.325-493.5q-12.325 0-20.575-8.375T699 408.5V357h-51.5q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.5 0-12.075 8.375-20.325t20.625-8.25H699v-52q0-11.675 8.425-20.088 8.426-8.412 20.5-8.412 12.075 0 20.325 8.412 8.25 8.413 8.25 20.088v52h52q11.675 0 20.088 8.463Q837 316.426 837 328.175q0 12.325-8.412 20.575Q820.175 357 808.5 357h-52v51.5q0 12.25-8.463 20.625t-20.212 8.375ZM273 772.5h413.175q9.825 0 13.825-7.75T698 749L585.578 599.603q-4.703-6.103-11.463-6.103-6.759 0-11.615 6L448 749.5l-81.462-106.388q-4.692-5.612-11.5-5.612-6.807 0-11.719 5.584L261.574 749.02q-5.074 7.98-.949 15.73T273 772.5ZM181.5 495v378.5V278v217Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M181.5 931q-22.969 0-40.234-17.266Q124 896.469 124 873.5V278q0-22.969 17.266-40.234Q158.531 220.5 181.5 220.5H561q12.25 0 20.625 8.463T590 249.175q0 12.325-8.375 20.575T561 278H181.5v595.5H777V495q0-12.25 8.425-20.625 8.426-8.375 20.5-8.375 12.075 0 20.325 8.375T834.5 495v378.5q0 22.969-17.266 40.234Q799.969 931 777 931H181.5Zm546.325-493.5q-12.325 0-20.575-8.375T699 408.5V357h-51.5q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.5 0-12.075 8.375-20.325t20.625-8.25H699v-52q0-11.675 8.425-20.088 8.426-8.412 20.5-8.412 12.075 0 20.325 8.412 8.25 8.413 8.25 20.088v52h52q11.675 0 20.088 8.463Q837 316.426 837 328.175q0 12.325-8.412 20.575Q820.175 357 808.5 357h-52v51.5q0 12.25-8.463 20.625t-20.212 8.375ZM273 772.5h413.175q9.825 0 13.825-7.75T698 749L585.578 599.603q-4.703-6.103-11.463-6.103-6.759 0-11.615 6L448 749.5l-81.462-106.388q-4.692-5.612-11.5-5.612-6.807 0-11.719 5.584L261.574 749.02q-5.074 7.98-.949 15.73T273 772.5ZM181.5 495v378.5V278v217Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_reaction.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_reaction.svg index 62ee67166c..ecca4f4872 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_reaction.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_reaction.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480.33-85q-81.704 0-153.629-31.263t-125.563-85Q147.5-255 116.25-326.789 85-398.579 85-480.202q0-81.705 31.363-153.863 31.362-72.159 84.769-125.547Q254.539-813 326.79-844q72.25-31 153.135-31 46.834 0 90.763 10.514Q614.617-853.973 654-835q-3.5 8.5-4.75 17.083Q648-809.333 648-800q0 7.897.75 14.949Q649.5-778 652-771q-38-22.5-81.071-34.5t-91.031-12q-140.21 0-238.804 98.25T142.5-480.486q0 140.515 98.736 239.25 98.735 98.736 239.25 98.736 140.514 0 238.764-98.594T817.5-480q0-38.526-8.25-75.013T786-624q10.963 7.759 24.931 11.88Q824.9-608 840-608h6.75q3.393 0 6.75-.5 10.5 30.5 16 62.433t5.5 65.888q0 81.086-31.013 153.475t-84.5 125.697Q706-147.699 633.977-116.349 561.954-85 480.33-85Zm144.123-448.5q22.947 0 37.997-15.003 15.05-15.004 15.05-37.95 0-22.947-15.003-37.997-15.004-15.05-37.95-15.05-22.947 0-37.997 15.003-15.05 15.004-15.05 37.95 0 22.947 15.003 37.997 15.004 15.05 37.95 15.05Zm-289 0q22.947 0 37.997-15.003 15.05-15.004 15.05-37.95 0-22.947-15.003-37.997-15.004-15.05-37.95-15.05-22.947 0-37.997 15.003-15.05 15.004-15.05 37.95 0 22.947 15.003 37.997 15.004 15.05 37.95 15.05ZM480-264q64.5 0 119.25-34.5t79.75-95H281q26 60.5 80.25 95T480-264Zm0-216Zm331.5-291.5H760q-12.25 0-20.375-8.175-8.125-8.176-8.125-20.5 0-12.325 8.125-20.575T760-829h51.5v-51q0-12.25 8.175-20.625 8.176-8.375 20.5-8.375 12.325 0 20.575 8.375T869-880v51h51q12.25 0 20.625 8.425 8.375 8.426 8.375 20.75 0 12.325-8.375 20.325t-20.625 8h-51v51.5q0 12.25-8.425 20.375-8.426 8.125-20.75 8.125-12.325 0-20.325-8.125t-8-20.375v-51.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M480.33-85q-81.704 0-153.629-31.263t-125.563-85Q147.5-255 116.25-326.789 85-398.579 85-480.202q0-81.705 31.363-153.863 31.362-72.159 84.769-125.547Q254.539-813 326.79-844q72.25-31 153.135-31 46.834 0 90.763 10.514Q614.617-853.973 654-835q-3.5 8.5-4.75 17.083Q648-809.333 648-800q0 7.897.75 14.949Q649.5-778 652-771q-38-22.5-81.071-34.5t-91.031-12q-140.21 0-238.804 98.25T142.5-480.486q0 140.515 98.736 239.25 98.735 98.736 239.25 98.736 140.514 0 238.764-98.594T817.5-480q0-38.526-8.25-75.013T786-624q10.963 7.759 24.931 11.88Q824.9-608 840-608h6.75q3.393 0 6.75-.5 10.5 30.5 16 62.433t5.5 65.888q0 81.086-31.013 153.475t-84.5 125.697Q706-147.699 633.977-116.349 561.954-85 480.33-85Zm144.123-448.5q22.947 0 37.997-15.003 15.05-15.004 15.05-37.95 0-22.947-15.003-37.997-15.004-15.05-37.95-15.05-22.947 0-37.997 15.003-15.05 15.004-15.05 37.95 0 22.947 15.003 37.997 15.004 15.05 37.95 15.05Zm-289 0q22.947 0 37.997-15.003 15.05-15.004 15.05-37.95 0-22.947-15.003-37.997-15.004-15.05-37.95-15.05-22.947 0-37.997 15.003-15.05 15.004-15.05 37.95 0 22.947 15.003 37.997 15.004 15.05 37.95 15.05ZM480-264q64.5 0 119.25-34.5t79.75-95H281q26 60.5 80.25 95T480-264Zm0-216Zm331.5-291.5H760q-12.25 0-20.375-8.175-8.125-8.176-8.125-20.5 0-12.325 8.125-20.575T760-829h51.5v-51q0-12.25 8.175-20.625 8.176-8.375 20.5-8.375 12.325 0 20.575 8.375T869-880v51h51q12.25 0 20.625 8.425 8.375 8.426 8.375 20.75 0 12.325-8.375 20.325t-20.625 8h-51v51.5q0 12.25-8.425 20.375-8.426 8.125-20.75 8.125-12.325 0-20.325-8.125t-8-20.375v-51.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_reaction_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_reaction_filled.svg index 0475962f63..47b4790bc5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_reaction_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_reaction_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M811.5-771.5H760q-11.5 0-20-8.175-8.5-8.176-8.5-20.5 0-12.325 8.125-20.575T760-829h51.5v-51q0-12.25 8.175-20.625 8.176-8.375 20.5-8.375 12.325 0 20.575 8.375T869-880v51h51q12.25 0 20.625 8.425 8.375 8.426 8.375 20.75 0 12.325-8.375 20.325t-20.625 8h-51v51.5q0 11.5-8.425 20-8.426 8.5-20.75 8.5-12.325 0-20.325-8.125t-8-20.375v-51.5ZM480.33-85q-81.704 0-153.629-31.263t-125.563-85Q147.5-255 116.25-326.789 85-398.579 85-480.202q0-81.705 31.363-153.863 31.362-72.159 84.769-125.547Q254.539-813 326.79-844q72.25-31 153.135-31 46.834 0 90.763 10.514Q614.617-853.973 654-835q-3.5 8.5-4.75 17.083-1.25 8.584-1.25 17.486 0 38.431 23.441 68.894 23.441 30.464 61.059 39.037 9.073 37.671 39.525 61.085Q802.477-608 840.275-608h6.475q3.393 0 6.75-.5 10.5 30.5 16 62.433t5.5 65.888q0 81.086-31.013 153.475t-84.5 125.697Q706-147.699 633.977-116.349 561.954-85 480.33-85Zm144.123-448.5q22.947 0 37.997-15.003 15.05-15.004 15.05-37.95 0-22.947-15.003-37.997-15.004-15.05-37.95-15.05-22.947 0-37.997 15.003-15.05 15.004-15.05 37.95 0 22.947 15.003 37.997 15.004 15.05 37.95 15.05Zm-289 0q22.947 0 37.997-15.003 15.05-15.004 15.05-37.95 0-22.947-15.003-37.997-15.004-15.05-37.95-15.05-22.947 0-37.997 15.003-15.05 15.004-15.05 37.95 0 22.947 15.003 37.997 15.004 15.05 37.95 15.05ZM480-264q64.5 0 119.25-34.5t79.75-95H281q26 60.5 80.25 95T480-264Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M811.5-771.5H760q-11.5 0-20-8.175-8.5-8.176-8.5-20.5 0-12.325 8.125-20.575T760-829h51.5v-51q0-12.25 8.175-20.625 8.176-8.375 20.5-8.375 12.325 0 20.575 8.375T869-880v51h51q12.25 0 20.625 8.425 8.375 8.426 8.375 20.75 0 12.325-8.375 20.325t-20.625 8h-51v51.5q0 11.5-8.425 20-8.426 8.5-20.75 8.5-12.325 0-20.325-8.125t-8-20.375v-51.5ZM480.33-85q-81.704 0-153.629-31.263t-125.563-85Q147.5-255 116.25-326.789 85-398.579 85-480.202q0-81.705 31.363-153.863 31.362-72.159 84.769-125.547Q254.539-813 326.79-844q72.25-31 153.135-31 46.834 0 90.763 10.514Q614.617-853.973 654-835q-3.5 8.5-4.75 17.083-1.25 8.584-1.25 17.486 0 38.431 23.441 68.894 23.441 30.464 61.059 39.037 9.073 37.671 39.525 61.085Q802.477-608 840.275-608h6.475q3.393 0 6.75-.5 10.5 30.5 16 62.433t5.5 65.888q0 81.086-31.013 153.475t-84.5 125.697Q706-147.699 633.977-116.349 561.954-85 480.33-85Zm144.123-448.5q22.947 0 37.997-15.003 15.05-15.004 15.05-37.95 0-22.947-15.003-37.997-15.004-15.05-37.95-15.05-22.947 0-37.997 15.003-15.05 15.004-15.05 37.95 0 22.947 15.003 37.997 15.004 15.05 37.95 15.05Zm-289 0q22.947 0 37.997-15.003 15.05-15.004 15.05-37.95 0-22.947-15.003-37.997-15.004-15.05-37.95-15.05-22.947 0-37.997 15.003-15.05 15.004-15.05 37.95 0 22.947 15.003 37.997 15.004 15.05 37.95 15.05ZM480-264q64.5 0 119.25-34.5t79.75-95H281q26 60.5 80.25 95T480-264Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_all_inclusive.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_all_inclusive.svg index 4675bb6642..7628baecda 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_all_inclusive.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_all_inclusive.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M214.099 793Q125.5 793 65.25 728.425 5 663.85 5 574.179q0-89.088 60.754-152.384Q126.51 358.5 214.07 358.5q36.078 0 69.254 11.63T342.5 407l94.5 90.5-40 40.5-90.5-88q-18.5-18.5-42.516-26.25T214 416q-64.207 0-107.854 46.5Q62.5 509 62.5 574.146q0 65.733 43.247 113.543Q148.994 735.5 213.998 735.5q25.002 0 48.752-8 23.75-8 42.75-25l313.441-295.434Q644.5 382 677.608 370.25q33.108-11.75 67.681-11.75 88.939 0 149.575 63.255Q955.5 485.009 955.5 573.532q0 89.941-60.755 154.705Q833.991 793 745.43 793q-35.078 0-68.754-11.13T618 745.5l-92-91 40-40 88 88q17.5 17.5 41.75 25.25t49.818 7.75q65.179 0 108.805-48Q898 639.5 898 573.341q0-64.745-44.348-111.043Q809.303 416 745.518 416q-24.919 0-48.719 8.75Q673 433.5 655 451L341.559 746.434Q315.5 770.5 282.018 781.75 248.536 793 214.099 793Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M214.099 793Q125.5 793 65.25 728.425 5 663.85 5 574.179q0-89.088 60.754-152.384Q126.51 358.5 214.07 358.5q36.078 0 69.254 11.63T342.5 407l94.5 90.5-40 40.5-90.5-88q-18.5-18.5-42.516-26.25T214 416q-64.207 0-107.854 46.5Q62.5 509 62.5 574.146q0 65.733 43.247 113.543Q148.994 735.5 213.998 735.5q25.002 0 48.752-8 23.75-8 42.75-25l313.441-295.434Q644.5 382 677.608 370.25q33.108-11.75 67.681-11.75 88.939 0 149.575 63.255Q955.5 485.009 955.5 573.532q0 89.941-60.755 154.705Q833.991 793 745.43 793q-35.078 0-68.754-11.13T618 745.5l-92-91 40-40 88 88q17.5 17.5 41.75 25.25t49.818 7.75q65.179 0 108.805-48Q898 639.5 898 573.341q0-64.745-44.348-111.043Q809.303 416 745.518 416q-24.919 0-48.719 8.75Q673 433.5 655 451L341.559 746.434Q315.5 770.5 282.018 781.75 248.536 793 214.099 793Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_alternate_email.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_alternate_email.svg new file mode 100644 index 0000000000..39007022b8 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_alternate_email.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M480-90q-80.91 0-152.07-30.76-71.15-30.77-123.79-83.5Q151.5-257 120.75-328.09 90-399.17 90-480q0-80.91 30.76-152.07 30.77-71.15 83.5-123.79Q257-808.5 328.09-839.25 399.17-870 480-870q80.91 0 152.07 30.76 71.15 30.77 123.79 83.5Q808.5-703 839.25-631.91 870-560.83 870-480v55.97q0 56.93-39.5 96.98Q791-287 734-287q-34.5 0-64.5-15.25T619-345q-27.5 28.5-63.75 43.25T480.14-287q-80.64 0-136.89-56.25Q287-399.5 287-480t56.25-136.75Q399.5-673 480-673t136.75 56.25Q673-560.5 673-480.15V-424q0 26 17.5 44t43.5 18q26 0 43.5-18t17.5-44v-56q0-131.5-91.75-223.25T480-795q-131.5 0-223.25 91.75T165-480q0 131.5 91.75 223.25T480-165h159.5q15.5 0 26.5 11t11 26.5q0 15.5-11 26.5t-26.5 11H480Zm-.12-272q49.12 0 83.62-34.38 34.5-34.38 34.5-83.5t-34.38-83.62q-34.38-34.5-83.5-34.5t-83.62 34.38q-34.5 34.38-34.5 83.5t34.38 83.62q34.38 34.5 83.5 34.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_back_ios_new.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_back_ios_new.svg index 6940e4d004..f4f2bc772f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_back_ios_new.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_back_ios_new.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M626 944.5 277.5 596q-4.5-4.794-6.5-9.554-2-4.76-2-10.486 0-5.726 2-10.486 2-4.76 6.5-9.474l348.9-348.9q10.6-10.6 26.6-10.6t27 10.5q11 11.545 11 27.682 0 16.136-11.073 27.391L366 576l313.955 313.955Q692.5 902.5 691.5 917.75q-1 15.25-11.5 25.75-11.5 11.5-27.409 11.75-15.909.25-26.591-10.75Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M626 944.5 277.5 596q-4.5-4.794-6.5-9.554-2-4.76-2-10.486 0-5.726 2-10.486 2-4.76 6.5-9.474l348.9-348.9q10.6-10.6 26.6-10.6t27 10.5q11 11.545 11 27.682 0 16.136-11.073 27.391L366 576l313.955 313.955Q692.5 902.5 691.5 917.75q-1 15.25-11.5 25.75-11.5 11.5-27.409 11.75-15.909.25-26.591-10.75Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_downward.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_downward.svg index c09c1c4d66..e9bd252628 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_downward.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_downward.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M479.96 881q-4.804 0-10.232-2.045Q464.3 876.909 460 872L184 596q-8.5-8.4-8.5-19.95 0-11.55 8.5-20.05t20.341-8.5q11.841 0 20.188 8.5L451.5 782.5v-494q0-12.013 8.463-20.506 8.463-8.494 20.212-8.494 12.325 0 20.575 8.375T509 288.5v494l227-227q8.182-8 19.841-8T776 555.842q8.5 8.342 8.5 20t-8.587 20.245L500 872q-4.58 5-9.499 7-4.919 2-10.541 2Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M479.96 881q-4.804 0-10.232-2.045Q464.3 876.909 460 872L184 596q-8.5-8.4-8.5-19.95 0-11.55 8.5-20.05t20.341-8.5q11.841 0 20.188 8.5L451.5 782.5v-494q0-12.013 8.463-20.506 8.463-8.494 20.212-8.494 12.325 0 20.575 8.375T509 288.5v494l227-227q8.182-8 19.841-8T776 555.842q8.5 8.342 8.5 20t-8.587 20.245L500 872q-4.58 5-9.499 7-4.919 2-10.541 2Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_drop_down.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_drop_down.svg new file mode 100644 index 0000000000..f2cfb3cbdd --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_drop_down.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22px" viewBox="0 -960 960 960" width="22px" fill="#5f6368"><path d="M480-362 284-557.5h392L480-362Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_drop_up.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_drop_up.svg new file mode 100644 index 0000000000..90c085ae7b --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_drop_up.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22px" viewBox="0 -960 960 960" width="22px" fill="#5f6368"><path d="m284-402 196-197 196 197H284Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_forward.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_forward.svg new file mode 100644 index 0000000000..0993b22658 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_forward.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22" fill="#5f6368"><path d="M629-446.5H235.48q-13.79 0-23.64-9.79-9.84-9.79-9.84-23.5t9.84-23.71q9.85-10 23.64-10H629L455.79-686.71Q445.5-697 445.25-710.5t10.25-24.48q10.5-10.52 24-10.27t23.81 10.57L734.1-503.59q4.9 4.91 7.65 10.97 2.75 6.06 2.75 12.78 0 6.71-2.75 12.78Q739-461 734.5-456.5l-231 231q-11 11-23.75 10.5t-23.25-11.02Q446-237 446-250.42q0-13.41 10.5-23.58L629-446.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_forward_ios.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_forward_ios.svg index dd07f2bad9..58b93e79a1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_forward_ios.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_forward_ios.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M278.5 941.451q-11-11.05-11.25-26.5Q267 899.5 278.5 888l314-314-314-314q-11-11-11.25-26.5t11.222-27q10.472-11.5 26.25-12t27.278 11L681 554q4.5 4.794 6.5 9.554 2 4.76 2 10.486 0 5.726-2 10.486-2 4.76-6.5 9.474L332 942.5q-11.009 11-26.755 10.75Q289.5 953 278.5 941.451Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M278.5 941.451q-11-11.05-11.25-26.5Q267 899.5 278.5 888l314-314-314-314q-11-11-11.25-26.5t11.222-27q10.472-11.5 26.25-12t27.278 11L681 554q4.5 4.794 6.5 9.554 2 4.76 2 10.486 0 5.726-2 10.486-2 4.76-6.5 9.474L332 942.5q-11.009 11-26.755 10.75Q289.5 953 278.5 941.451Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_outward.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_outward.svg new file mode 100644 index 0000000000..2391aba06c --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_outward.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#000000"><path d="m255-200-91-91 377-377H224v-128h536v536H632v-317L255-200Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_upward.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_upward.svg index 88b20124f3..0a54ed27db 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_upward.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_upward.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M479.825 892q-12.325 0-20.325-8.125t-8-20.375V369l-227 227q-8.833 9-20.417 9-11.583 0-20.083-8.853-8.5-8.853-8.5-20.414 0-11.562 8.5-20.233l275.956-275.956q4.427-4.68 9.891-6.612Q475.311 271 480.575 271q5.264 0 10.094 2 4.831 2 9.331 6.5l276 276q8.5 8.671 8.5 20.233 0 11.561-8.342 20.414-8.342 8.853-20 8.853T736 596L509 369v494.5q0 12.25-8.425 20.375-8.426 8.125-20.75 8.125Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M479.825 892q-12.325 0-20.325-8.125t-8-20.375V369l-227 227q-8.833 9-20.417 9-11.583 0-20.083-8.853-8.5-8.853-8.5-20.414 0-11.562 8.5-20.233l275.956-275.956q4.427-4.68 9.891-6.612Q475.311 271 480.575 271q5.264 0 10.094 2 4.831 2 9.331 6.5l276 276q8.5 8.671 8.5 20.233 0 11.561-8.342 20.414-8.342 8.853-20 8.853T736 596L509 369v494.5q0 12.25-8.425 20.375-8.426 8.125-20.75 8.125Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrows_left_right.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrows_left_right.svg new file mode 100644 index 0000000000..4b5a982e41 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrows_left_right.svg @@ -0,0 +1,4 @@ +<svg height="22" viewBox="0 -960 960 960" width="22" fill="#000000" xmlns="http://www.w3.org/2000/svg"> + <path d="M 831.5 -480 L 657 -656.5 C 651.667 -662.167 648.917 -668.833 648.75 -676.5 C 648.583 -684.167 651.333 -690.833 657 -696.5 C 662.667 -702.5 669.333 -705.5 677 -705.5 C 684.667 -705.5 691.5 -702.667 697.5 -697 L 894 -500.5 C 897 -497.167 899.25 -493.917 900.75 -490.75 C 902.25 -487.583 903 -484 903 -480 C 903 -476 902.25 -472.417 900.75 -469.25 C 899.25 -466.083 897 -463 894 -460 L 697.5 -263.5 C 691.5 -257.5 684.667 -254.583 677 -254.75 C 669.333 -254.917 662.667 -257.833 657 -263.5 C 651 -269.5 648.167 -276.25 648.5 -283.75 C 648.833 -291.25 651.667 -297.833 657 -303.5 L 831.5 -480 Z M 128.5 -480 L 303 -303.5 C 308.333 -297.833 311.083 -291.167 311.25 -283.5 C 311.417 -275.833 308.667 -269.167 303 -263.5 C 297.333 -257.5 290.667 -254.5 283 -254.5 C 275.333 -254.5 268.667 -257.5 263 -263.5 L 66.5 -460 C 63.167 -463 60.833 -466.083 59.5 -469.25 C 58.167 -472.417 57.5 -476 57.5 -480 C 57.5 -484 58.167 -487.583 59.5 -490.75 C 60.833 -493.917 63.167 -497.167 66.5 -500.5 L 263 -697 C 268.667 -702.667 275.333 -705.417 283 -705.25 C 290.667 -705.083 297.5 -702.167 303.5 -696.5 C 309.167 -690.5 311.833 -683.75 311.5 -676.25 C 311.167 -668.75 308.333 -662.167 303 -656.5 L 128.5 -480 Z" transform="matrix(0.9999999999999999, 0, 0, 0.9999999999999999, 0, 0)"/> + <rect x="123" y="-514" width="711.266" height="68" style="stroke: rgb(0, 0, 0);" transform="matrix(0.9999999999999999, 0, 0, 0.9999999999999999, 0, 0)"/> +</svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_article.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_article.svg index 05d4d6fe6e..4cd46fc7cb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_article.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_article.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M308.5 774H522q11.675 0 20.088-8.425 8.412-8.426 8.412-20.5 0-12.075-8.412-20.325-8.413-8.25-20.088-8.25H308.5q-12.25 0-20.625 8.463t-8.375 20.212q0 12.325 8.375 20.575T308.5 774Zm0-169.5H652q11.675 0 20.088-8.425 8.412-8.426 8.412-20.5 0-12.075-8.412-20.325Q663.675 547 652 547H308.5q-12.25 0-20.625 8.463t-8.375 20.212q0 12.325 8.375 20.575t20.625 8.25Zm0-169.5H652q11.675 0 20.088-8.425 8.412-8.426 8.412-20.5 0-12.075-8.412-20.325-8.413-8.25-20.088-8.25H308.5q-12.25 0-20.625 8.463t-8.375 20.212q0 12.325 8.375 20.575T308.5 435ZM182 931.5q-22.969 0-40.234-17.266Q124.5 896.969 124.5 874V278q0-22.969 17.266-40.234Q159.031 220.5 182 220.5h596q22.969 0 40.234 17.266Q835.5 255.031 835.5 278v596q0 22.969-17.266 40.234Q800.969 931.5 778 931.5H182Zm0-57.5h596V278H182v596Zm0 0V278v596Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M308.5 774H522q11.675 0 20.088-8.425 8.412-8.426 8.412-20.5 0-12.075-8.412-20.325-8.413-8.25-20.088-8.25H308.5q-12.25 0-20.625 8.463t-8.375 20.212q0 12.325 8.375 20.575T308.5 774Zm0-169.5H652q11.675 0 20.088-8.425 8.412-8.426 8.412-20.5 0-12.075-8.412-20.325Q663.675 547 652 547H308.5q-12.25 0-20.625 8.463t-8.375 20.212q0 12.325 8.375 20.575t20.625 8.25Zm0-169.5H652q11.675 0 20.088-8.425 8.412-8.426 8.412-20.5 0-12.075-8.412-20.325-8.413-8.25-20.088-8.25H308.5q-12.25 0-20.625 8.463t-8.375 20.212q0 12.325 8.375 20.575T308.5 435ZM182 931.5q-22.969 0-40.234-17.266Q124.5 896.969 124.5 874V278q0-22.969 17.266-40.234Q159.031 220.5 182 220.5h596q22.969 0 40.234 17.266Q835.5 255.031 835.5 278v596q0 22.969-17.266 40.234Q800.969 931.5 778 931.5H182Zm0-57.5h596V278H182v596Zm0 0V278v596Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_at.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_at.svg new file mode 100644 index 0000000000..d0d34555df --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_at.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22" fill="#000000"><path d="M480-85q-82.01 0-154.15-31.13-72.13-31.13-125.49-84.5Q147-254 116-326.02q-31-72.03-31-153.92 0-81.97 31.13-154.13 31.13-72.17 84.5-125.55Q254-813 326.02-844q72.03-31 153.92-31 81.97 0 154.13 31.13 72.17 31.13 125.55 84.5Q813-706 844-633.9q31 72.11 31 154.09v51.78q0 54.97-38.92 92.75T741-297.5q-36.08 0-67.41-17.75Q642.25-333 625.5-364.5q-25.5 34-64.33 50.5-38.84 16.5-81.02 16.5-76.26 0-129.95-53-53.7-53-53.7-129.76 0-76.77 53.7-130.5Q403.89-664.5 480-664.5q76.1 0 129.8 53.76 53.7 53.76 53.7 130.58V-428q0 30.61 22.86 51.81Q709.22-355 740.5-355t54.14-21.19q22.86-21.2 22.86-51.81v-52q0-141.38-98.06-239.44T480-817.5q-141.37 0-239.44 98.06Q142.5-621.38 142.5-480q0 141.37 98.06 239.44Q338.63-142.5 480-142.5h184q11.67 0 20.09 8.46 8.41 8.47 8.41 20.22 0 12.32-8.41 20.57Q675.67-85 664-85H480Zm0-270q52.5 0 89.25-36.44t36.75-88.5q0-53.56-36.75-90.31T480-607q-52.5 0-89.25 36.69T354-480.06q0 52.06 36.75 88.56T480-355Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_attach_file_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_attach_file_filled.svg index 5ff6754f22..dcebed008e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_attach_file_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_attach_file_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M460.058 975.5q-90.558 0-154.808-62.418Q241 850.664 241 760.739v-431.91Q241 265 286.147 220.5 331.294 176 395.25 176q64.75 0 109.5 44.75t44.75 109.115v395.221q0 37.494-25.898 63.954t-63.75 26.46q-37.852 0-63.602-28.21T370.5 720V346q0-7.5 5.75-13.25t13.5-5.75q7.75 0 13.5 5.75T409 346v377.069q0 22.431 14.75 38.181Q438.5 777 460 777t36.25-15.25Q511 746.5 511 725.039V329.103q0-48.103-33.796-81.353-33.796-33.25-81.75-33.25T313.5 247.624q-34 33.123-34 81.303v433.791q0 73.282 53.312 123.782t127.25 50.5Q535 937 587.75 885.706t52.75-125.344V346q0-7.5 5.75-13.25t13.5-5.75q7.75 0 13.5 5.75T679 346v413.5q0 90.164-64.192 153.082-64.193 62.918-154.75 62.918Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M460.058 975.5q-90.558 0-154.808-62.418Q241 850.664 241 760.739v-431.91Q241 265 286.147 220.5 331.294 176 395.25 176q64.75 0 109.5 44.75t44.75 109.115v395.221q0 37.494-25.898 63.954t-63.75 26.46q-37.852 0-63.602-28.21T370.5 720V346q0-7.5 5.75-13.25t13.5-5.75q7.75 0 13.5 5.75T409 346v377.069q0 22.431 14.75 38.181Q438.5 777 460 777t36.25-15.25Q511 746.5 511 725.039V329.103q0-48.103-33.796-81.353-33.796-33.25-81.75-33.25T313.5 247.624q-34 33.123-34 81.303v433.791q0 73.282 53.312 123.782t127.25 50.5Q535 937 587.75 885.706t52.75-125.344V346q0-7.5 5.75-13.25t13.5-5.75q7.75 0 13.5 5.75T679 346v413.5q0 90.164-64.192 153.082-64.193 62.918-154.75 62.918Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_attach_file_filled_500.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_attach_file_filled_500.svg index 04ef1d379f..d0897cd912 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_attach_file_filled_500.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_attach_file_filled_500.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M460.058-80.5q-90.558 0-154.808-62.418Q241-205.336 241-295.261v-431.91Q241-791 286.147-835.5 331.294-880 395.25-880q64.75 0 109.5 44.75t44.75 109.115v395.221q0 37.494-25.898 63.954t-63.75 26.46q-37.852 0-63.602-28.21T370.5-336v-374q0-7.5 5.75-13.25t13.5-5.75q7.75 0 13.5 5.75T409-710v377.069q0 22.431 14.75 38.181Q438.5-279 460-279t36.25-15.25Q511-309.5 511-330.961v-395.936q0-48.103-33.796-81.353-33.796-33.25-81.75-33.25T313.5-808.376q-34 33.123-34 81.303v433.791q0 73.282 53.312 123.782t127.25 50.5Q535-119 587.75-170.294q52.75-51.293 52.75-125.344V-710q0-7.5 5.75-13.25t13.5-5.75q7.75 0 13.5 5.75T679-710v413.5q0 90.164-64.192 153.082Q550.615-80.5 460.058-80.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M460.058-80.5q-90.558 0-154.808-62.418Q241-205.336 241-295.261v-431.91Q241-791 286.147-835.5 331.294-880 395.25-880q64.75 0 109.5 44.75t44.75 109.115v395.221q0 37.494-25.898 63.954t-63.75 26.46q-37.852 0-63.602-28.21T370.5-336v-374q0-7.5 5.75-13.25t13.5-5.75q7.75 0 13.5 5.75T409-710v377.069q0 22.431 14.75 38.181Q438.5-279 460-279t36.25-15.25Q511-309.5 511-330.961v-395.936q0-48.103-33.796-81.353-33.796-33.25-81.75-33.25T313.5-808.376q-34 33.123-34 81.303v433.791q0 73.282 53.312 123.782t127.25 50.5Q535-119 587.75-170.294q52.75-51.293 52.75-125.344V-710q0-7.5 5.75-13.25t13.5-5.75q7.75 0 13.5 5.75T679-710v413.5q0 90.164-64.192 153.082Q550.615-80.5 460.058-80.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_avg_pace.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_avg_pace.svg new file mode 100644 index 0000000000..78d1b86471 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_avg_pace.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M609.824-771.5q-15.824 0-27.074-11.426-11.25-11.426-11.25-27.25t11.426-27.324q11.426-11.5 27.25-11.5t27.324 11.676q11.5 11.676 11.5 27.5t-11.676 27.074q-11.676 11.25-27.5 11.25Zm0 660q-15.824 0-27.074-11.426-11.25-11.426-11.25-27.25t11.426-27.324q11.426-11.5 27.25-11.5t27.324 11.676q11.5 11.676 11.5 27.5t-11.676 27.074q-11.676 11.25-27.5 11.25Zm160-520q-15.824 0-27.074-11.426-11.25-11.426-11.25-27.25t11.426-27.324q11.426-11.5 27.25-11.5t27.324 11.676q11.5 11.676 11.5 27.5t-11.676 27.074q-11.676 11.25-27.5 11.25Zm0 380q-15.824 0-27.074-11.426-11.25-11.426-11.25-27.25t11.426-27.324q11.426-11.5 27.25-11.5t27.324 11.676q11.5 11.676 11.5 27.5t-11.676 27.074q-11.676 11.25-27.5 11.25Zm60-190q-15.824 0-27.074-11.426-11.25-11.426-11.25-27.25t11.426-27.324q11.426-11.5 27.25-11.5t27.324 11.676q11.5 11.676 11.5 27.5t-11.676 27.074q-11.676 11.25-27.5 11.25ZM480-81.5q-82.481 0-155.275-31.304-72.794-31.305-126.706-85.219-53.913-53.915-85.216-126.711Q81.5-397.531 81.5-480.016q0-82.484 31.303-155.273 31.303-72.79 85.216-126.699 53.912-53.909 126.706-85.461Q397.519-879 480-879v60q-141.5 0-240 98.562Q141.5-621.875 141.5-480t98.312 240.188Q338.125-141.5 480-141.5v60Zm-.111-330q-28.389 0-48.389-20.078-20-20.078-20-48.422 0-5.938.75-12.441.75-6.503 3.25-11.808L336-584l40-40.5 81.023 79.5q4.477-2 22.977-4 28.344 0 48.672 20.361Q549-508.279 549-479.889q0 28.389-20.361 48.389-20.36 20-48.75 20Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_back_hand.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_back_hand.svg index 41013ff66c..b9a3887c8f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_back_hand.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_back_hand.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M504.5-45q-92 0-169.75-46T210-216.5l-147.5-247 19-19.5q15-15 36-16.5T156-489l129 93v-410.5q0-10.925 8.154-19.713 8.153-8.787 20.75-8.787 11.096 0 19.846 8.787 8.75 8.788 8.75 19.713v522l-185-134L258-250q37.5 68.5 103.318 108 65.817 39.5 143.182 39.5 112.792 0 192.896-78.104Q777.5-258.708 777.5-371v-395.688q0-10.812 8.154-19.562 8.153-8.75 20.75-8.75 11.096 0 19.846 8.787Q835-777.425 835-766.5V-371q0 136-96.832 231Q641.335-45 504.5-45Zm-55-446.5v-395q0-10.925 8.654-19.713 8.653-8.787 20.25-8.787 12.096 0 20.346 8.787Q507-897.425 507-886.5v395h-57.5Zm164.5 0v-355q0-10.925 8.154-19.713 8.153-8.787 20.75-8.787 11.096 0 19.846 8.787 8.75 8.788 8.75 19.713v355H614ZM468-297Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M504.5-45q-92 0-169.75-46T210-216.5l-147.5-247 19-19.5q15-15 36-16.5T156-489l129 93v-410.5q0-10.925 8.154-19.713 8.153-8.787 20.75-8.787 11.096 0 19.846 8.787 8.75 8.788 8.75 19.713v522l-185-134L258-250q37.5 68.5 103.318 108 65.817 39.5 143.182 39.5 112.792 0 192.896-78.104Q777.5-258.708 777.5-371v-395.688q0-10.812 8.154-19.562 8.153-8.75 20.75-8.75 11.096 0 19.846 8.787Q835-777.425 835-766.5V-371q0 136-96.832 231Q641.335-45 504.5-45Zm-55-446.5v-395q0-10.925 8.654-19.713 8.653-8.787 20.25-8.787 12.096 0 20.346 8.787Q507-897.425 507-886.5v395h-57.5Zm164.5 0v-355q0-10.925 8.154-19.713 8.153-8.787 20.75-8.787 11.096 0 19.846 8.787 8.75 8.788 8.75 19.713v355H614ZM468-297Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_backspace.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_backspace.svg index 5bb12b42ca..a82b16c3ce 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_backspace.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_backspace.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M428 706.25q9 8.75 20.75 8.75t20.718-8.937L559.25 616.5l90.782 90.563Q659 716 670.25 716t20.25-8.75q9-8.75 9-20.75t-8.937-20.937L600 576l89.563-90.063Q698.5 477.5 698.5 466q0-11.5-9-20.25T668.75 437q-11.75 0-20.687 8.437L559.5 535.5l-91.063-91.063Q459.5 436 448.25 436.5 437 437 428 445t-9 20.25q0 12.25 8.937 20.687L519 576l-91 89.563q-8 8.937-8 20.904 0 11.966 8 19.783ZM362.19 852q-22.19 0-40.31-11.5Q303.761 829 291.5 811L148.135 609.333Q137 595.5 137 576.634T148 542l143.5-201q12.333-18 30.386-29.5Q339.939 300 362 300h416q23.719 0 40.609 16.891Q835.5 333.781 835.5 357.5v437q0 23.719-16.891 40.609Q801.719 852 778 852H362.19ZM197 576l152.952 218.5H778v-437H350L197 576Zm581 0V357.5v437V576Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M428 706.25q9 8.75 20.75 8.75t20.718-8.937L559.25 616.5l90.782 90.563Q659 716 670.25 716t20.25-8.75q9-8.75 9-20.75t-8.937-20.937L600 576l89.563-90.063Q698.5 477.5 698.5 466q0-11.5-9-20.25T668.75 437q-11.75 0-20.687 8.437L559.5 535.5l-91.063-91.063Q459.5 436 448.25 436.5 437 437 428 445t-9 20.25q0 12.25 8.937 20.687L519 576l-91 89.563q-8 8.937-8 20.904 0 11.966 8 19.783ZM362.19 852q-22.19 0-40.31-11.5Q303.761 829 291.5 811L148.135 609.333Q137 595.5 137 576.634T148 542l143.5-201q12.333-18 30.386-29.5Q339.939 300 362 300h416q23.719 0 40.609 16.891Q835.5 333.781 835.5 357.5v437q0 23.719-16.891 40.609Q801.719 852 778 852H362.19ZM197 576l152.952 218.5H778v-437H350L197 576Zm581 0V357.5v437V576Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_backup.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_backup.svg index 5d4b0c0dc6..e076c55993 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_backup.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_backup.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M251.5 891.5q-84.552 0-145.526-60.935Q45 769.63 45 684.253 45 608 94.25 549.5q49.25-58.5 126.25-70 18.824-95.893 92.113-156.947Q385.901 261.5 482 261.5q111.657 0 186.829 80.414Q744 422.329 744 534v26q71.5-3 121.25 44.75T915 725.392q0 67.795-49.125 116.951Q816.75 891.5 749.5 891.5H509q-22.969 0-40.234-17.266Q451.5 856.969 451.5 834V575.5l-63.5 63q-9 9-20.25 8.5T348 637.5q-9-8.5-9-20.25t9-20.75l111.973-112.473q4.607-4.527 9.6-6.777 4.994-2.25 10.7-2.25 5.707 0 10.467 2.25 4.76 2.25 9.285 6.775l113.43 113.43Q622 606 622 617.5q0 11.5-8.5 20-9.111 9-20.806 9-11.694 0-20.194-9l-63.5-62V834h240.5q43.87 0 75.935-31.645Q857.5 770.71 857.5 725.75q0-44.75-31.819-76.42-31.819-31.671-76.556-31.671H686.5V534.5q0-88.378-60.036-151.939Q566.428 319 478.087 319q-88.342 0-148.892 63.561-60.549 63.561-60.549 151.939h-19.018q-61.628 0-104.378 43T102.5 684q0 61.5 43.679 105.75Q189.857 834 251.5 834H365q12.25 0 20.625 8.463T394 862.675q0 12.325-8.375 20.575T365 891.5H251.5Zm228.5-287Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M251.5 891.5q-84.552 0-145.526-60.935Q45 769.63 45 684.253 45 608 94.25 549.5q49.25-58.5 126.25-70 18.824-95.893 92.113-156.947Q385.901 261.5 482 261.5q111.657 0 186.829 80.414Q744 422.329 744 534v26q71.5-3 121.25 44.75T915 725.392q0 67.795-49.125 116.951Q816.75 891.5 749.5 891.5H509q-22.969 0-40.234-17.266Q451.5 856.969 451.5 834V575.5l-63.5 63q-9 9-20.25 8.5T348 637.5q-9-8.5-9-20.25t9-20.75l111.973-112.473q4.607-4.527 9.6-6.777 4.994-2.25 10.7-2.25 5.707 0 10.467 2.25 4.76 2.25 9.285 6.775l113.43 113.43Q622 606 622 617.5q0 11.5-8.5 20-9.111 9-20.806 9-11.694 0-20.194-9l-63.5-62V834h240.5q43.87 0 75.935-31.645Q857.5 770.71 857.5 725.75q0-44.75-31.819-76.42-31.819-31.671-76.556-31.671H686.5V534.5q0-88.378-60.036-151.939Q566.428 319 478.087 319q-88.342 0-148.892 63.561-60.549 63.561-60.549 151.939h-19.018q-61.628 0-104.378 43T102.5 684q0 61.5 43.679 105.75Q189.857 834 251.5 834H365q12.25 0 20.625 8.463T394 862.675q0 12.325-8.375 20.575T365 891.5H251.5Zm228.5-287Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_battery_2_bar.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_battery_2_bar.svg index fcdb3dfd50..3278d1d2dc 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_battery_2_bar.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_battery_2_bar.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M313.5 964.5q-12.475 0-20.487-8.013Q285 948.475 285 936V279.5q0-12.475 8.013-20.487Q301.025 251 313.5 251H402v-35q0-12.475 8.013-20.487 8.012-8.013 20.487-8.013H530q11.975 0 20.237 8.013Q558.5 203.525 558.5 216v35H647q11.975 0 20.237 8.013 8.263 8.012 8.263 20.487V936q0 12.475-8.263 20.487Q658.975 964.5 647 964.5H313.5Zm29-228.5H618V308.5H342.5V736Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M313.5 964.5q-12.475 0-20.487-8.013Q285 948.475 285 936V279.5q0-12.475 8.013-20.487Q301.025 251 313.5 251H402v-35q0-12.475 8.013-20.487 8.012-8.013 20.487-8.013H530q11.975 0 20.237 8.013Q558.5 203.525 558.5 216v35H647q11.975 0 20.237 8.013 8.263 8.012 8.263 20.487V936q0 12.475-8.263 20.487Q658.975 964.5 647 964.5H313.5Zm29-228.5H618V308.5H342.5V736Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_battery_3_bar.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_battery_3_bar.svg index 819fe590c9..9f73bc20f9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_battery_3_bar.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_battery_3_bar.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M313.5 964.5q-12.475 0-20.487-8.013Q285 948.475 285 936V279.5q0-12.475 8.013-20.487Q301.025 251 313.5 251H402v-35q0-12.475 8.013-20.487 8.012-8.013 20.487-8.013H530q11.975 0 20.237 8.013Q558.5 203.525 558.5 216v35H647q11.975 0 20.237 8.013 8.263 8.012 8.263 20.487V936q0 12.475-8.263 20.487Q658.975 964.5 647 964.5H313.5Zm29-314H618v-342H342.5v342Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M313.5 964.5q-12.475 0-20.487-8.013Q285 948.475 285 936V279.5q0-12.475 8.013-20.487Q301.025 251 313.5 251H402v-35q0-12.475 8.013-20.487 8.012-8.013 20.487-8.013H530q11.975 0 20.237 8.013Q558.5 203.525 558.5 216v35H647q11.975 0 20.237 8.013 8.263 8.012 8.263 20.487V936q0 12.475-8.263 20.487Q658.975 964.5 647 964.5H313.5Zm29-314H618v-342H342.5v342Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bedtime_moon.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bedtime_moon.svg new file mode 100644 index 0000000000..ed5bc12d4a --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bedtime_moon.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M483.82-81.5q-83.12 0-156.43-31.82-73.32-31.81-127.85-86.25Q145-254 113.25-327.25 81.5-400.5 81.5-484q0-131.24 77-236.62t202.35-146.46q16.05-4.92 29.85 4.83t11.8 25.75q-6.5 88.5 24 170.4t94 145.35Q583.5-458 665.75-427t170.75 24q15-1.5 24.75 12.25t5.25 29.25q-40 125.5-145.35 202.75Q615.81-81.5 483.82-81.5ZM484-139q100.95 0 183.22-57.5Q749.5-254 800-343q-90.29-7.95-173.55-41.22Q543.2-417.5 479.5-481q-63.96-62.86-96.73-145.68Q350-709.5 342.5-798.5q-89 48.5-146.25 131.03Q139-584.95 139-484q0 144.38 100.31 244.69T484-139Zm-5-342Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bluetooth.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bluetooth.svg new file mode 100644 index 0000000000..d39581566e --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bluetooth.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M451-98v-313L257.5-217.5 217-258l222.5-222L217-702.5l40.5-40.5L451-549.5v-313h28.5L691-651 520-480.5l171 171L479.5-98H451Zm57.5-451.5 102-101.5-102-99.5v201Zm0 339.5 102-99.5-102-101.5v201Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_blur_on.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_blur_on.svg new file mode 100644 index 0000000000..d729371159 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_blur_on.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22" fill="#5f6368"><path d="M124-382q-9.5 0-15.25-6T103-403q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Zm0-155q-9.5 0-15.25-6T103-558q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Zm117.96 332.5q-15.96 0-26.71-10.74-10.75-10.73-10.75-26.6 0-15.87 10.59-26.77 10.59-10.89 26.5-10.89t26.91 10.74q11 10.73 11 26.6 0 15.87-10.79 26.77-10.8 10.89-26.75 10.89Zm0-161q-15.96 0-26.71-10.74-10.75-10.73-10.75-26.6 0-15.87 10.59-26.77 10.59-10.89 26.5-10.89t26.91 10.74q11 10.73 11 26.6 0 15.87-10.79 26.77-10.8 10.89-26.75 10.89Zm0-154.5q-15.96 0-26.71-10.74-10.75-10.73-10.75-26.6 0-15.87 10.59-26.77Q225.68-595 241.59-595t26.91 10.74q11 10.73 11 26.6 0 15.87-10.79 26.77Q257.91-520 241.96-520Zm0-161q-15.96 0-26.71-10.74-10.75-10.73-10.75-26.6 0-15.87 10.59-26.77Q225.68-756 241.59-756t26.91 10.74q11 10.73 11 26.6 0 15.87-10.79 26.77Q257.91-681 241.96-681Zm160.91 331q-21.87 0-37.37-15.44-15.5-15.44-15.5-37.5 0-22.06 15.42-37.56 15.43-15.5 37.46-15.5 21.62 0 37.37 15.44Q456-425.12 456-403.06q0 22.06-15.63 37.56-15.63 15.5-37.5 15.5Zm0-154.5q-21.87 0-37.37-15.44-15.5-15.44-15.5-37.5 0-22.06 15.42-37.56 15.43-15.5 37.46-15.5 21.62 0 37.37 15.44Q456-579.62 456-557.56q0 22.06-15.63 37.56-15.63 15.5-37.5 15.5Zm.09 300q-15.96 0-26.71-10.74-10.75-10.73-10.75-26.6 0-15.87 10.59-26.77 10.59-10.89 26.5-10.89t26.91 10.74q11 10.73 11 26.6 0 15.87-10.79 26.77-10.8 10.89-26.75 10.89Zm0-476.5q-15.96 0-26.71-10.74-10.75-10.73-10.75-26.6 0-15.87 10.59-26.77Q386.68-756 402.59-756t26.91 10.74q11 10.73 11 26.6 0 15.87-10.79 26.77Q418.91-681 402.96-681Zm.04 578q-9.5 0-15.25-6T382-124q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Zm0-712.5q-9.5 0-15.25-6t-5.75-15q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6ZM557.37-350q-21.87 0-37.37-15.44-15.5-15.44-15.5-37.5 0-22.06 15.42-37.56 15.43-15.5 37.46-15.5 21.62 0 37.37 15.44 15.75 15.44 15.75 37.5 0 22.06-15.63 37.56-15.63 15.5-37.5 15.5Zm0-154.5q-21.87 0-37.37-15.44-15.5-15.44-15.5-37.5 0-22.06 15.42-37.56 15.43-15.5 37.46-15.5 21.62 0 37.37 15.44 15.75 15.44 15.75 37.5 0 22.06-15.63 37.56-15.63 15.5-37.5 15.5Zm.09 300q-15.96 0-26.71-10.74Q520-225.97 520-241.84q0-15.87 10.59-26.77 10.59-10.89 26.5-10.89T584-268.76q11 10.73 11 26.6 0 15.87-10.79 26.77-10.8 10.89-26.75 10.89Zm0-476.5q-15.96 0-26.71-10.74Q520-702.47 520-718.34q0-15.87 10.59-26.77Q541.18-756 557.09-756T584-745.26q11 10.73 11 26.6 0 15.87-10.79 26.77Q573.41-681 557.46-681ZM564-103q-9.5 0-15.25-6T543-124q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Zm-6-712.5q-9.5 0-15.25-6t-5.75-15q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Zm160.46 611q-15.96 0-26.71-10.74Q681-225.97 681-241.84q0-15.87 10.59-26.77 10.59-10.89 26.5-10.89T745-268.76q11 10.73 11 26.6 0 15.87-10.79 26.77-10.8 10.89-26.75 10.89Zm0-161q-15.96 0-26.71-10.74Q681-386.97 681-402.84q0-15.87 10.59-26.77 10.59-10.89 26.5-10.89T745-429.76q11 10.73 11 26.6 0 15.87-10.79 26.77-10.8 10.89-26.75 10.89Zm0-154.5q-15.96 0-26.71-10.74Q681-541.47 681-557.34q0-15.87 10.59-26.77Q702.18-595 718.09-595T745-584.26q11 10.73 11 26.6 0 15.87-10.79 26.77Q734.41-520 718.46-520Zm0-161q-15.96 0-26.71-10.74Q681-702.47 681-718.34q0-15.87 10.59-26.77Q702.18-756 718.09-756T745-745.26q11 10.73 11 26.6 0 15.87-10.79 26.77Q734.41-681 718.46-681ZM836.5-382q-9.5 0-15.25-6t-5.75-15q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Zm0-155q-9.5 0-15.25-6t-5.75-15q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt.svg index 4c686c282a..aa276fa8c1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M391.5-161 675-501.5H493.5L530-791 272.5-419h155l-36 258Zm-30-200.5H217q-17.372 0-25.186-15.25t2.686-29.75l338.5-489q7-10.5 18.75-14.25T575-909q11 5 17.75 15.92T598-870.5L558.5-559H736q18.362 0 25.931 16.25Q769.5-526.5 758-512L386.5-66q-8.442 9.905-19.971 12.702Q355-50.5 344-55.75q-11-4.75-16.75-15.375T323.5-94l38-267.5Zm112-114Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M391.5-161 675-501.5H493.5L530-791 272.5-419h155l-36 258Zm-30-200.5H217q-17.372 0-25.186-15.25t2.686-29.75l338.5-489q7-10.5 18.75-14.25T575-909q11 5 17.75 15.92T598-870.5L558.5-559H736q18.362 0 25.931 16.25Q769.5-526.5 758-512L386.5-66q-8.442 9.905-19.971 12.702Q355-50.5 344-55.75q-11-4.75-16.75-15.375T323.5-94l38-267.5Zm112-114Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_filled.svg index 012a8e2a45..2fd1fdcb38 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M361.5 694.5H217q-17.5 0-25.25-15.25t2.75-29.75l338.5-489q7-10.5 18.75-14.25T575 147q11 5 17.75 15.75T598 185.5L558.5 497H736q18 0 25.75 16.25T758 544L386.5 990q-8 10-19.75 12.75T344 1000.5q-11-4.5-16.75-15.25T323.5 962l38-267.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M361.5 694.5H217q-17.5 0-25.25-15.25t2.75-29.75l338.5-489q7-10.5 18.75-14.25T575 147q11 5 17.75 15.75T598 185.5L558.5 497H736q18 0 25.75 16.25T758 544L386.5 990q-8 10-19.75 12.75T344 1000.5q-11-4.5-16.75-15.25T323.5 962l38-267.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_off.svg index 7b003c8e7c..4278ea764a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_off.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_off.svg @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg - height="24" + height="22" viewBox="0 -960 960 960" - width="24" + width="22" version="1.1" id="svg864" sodipodi:docname="ic_bolt_off.svg" diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_brand_awareness_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_brand_awareness_filled.svg new file mode 100644 index 0000000000..fdc1db5f80 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_brand_awareness_filled.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M653-454.5V-512h142.5v57.5H653Zm32.5 290-117-86 32.5-45 117 86-32.5 45Zm-84.5-500-32.5-45 117-86 32.5 45-117 86ZM125-363v-234h157.5l196-196v626l-196-196H125Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_breaking_news.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_breaking_news.svg new file mode 100644 index 0000000000..4688932ef9 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_breaking_news.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22px" viewBox="0 -960 960 960" width="22px" fill="#5f6368"><path d="M262.5-285q11.5 0 20.25-8.5t8.75-20q0-11.5-8.75-20.25t-20.25-8.75q-11.5 0-20 8.75T234-313.5q0 11.5 8.5 20t20 8.5Zm-29-168.5H291v-227h-57.5v227Zm217.5 174h275.5V-337H451v57.5Zm0-174h275.5V-511H451v57.5Zm0-169.5h275.5v-57.5H451v57.5ZM134.5-124.5q-22.97 0-40.23-17.27Q77-159.03 77-182v-596q0-22.97 17.27-40.23 17.26-17.27 40.23-17.27h691q22.97 0 40.23 17.27Q883-800.97 883-778v596q0 22.97-17.27 40.23-17.26 17.27-40.23 17.27h-691Zm0-57.5h691v-596h-691v596Zm0 0v-596 596Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cable.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cable.svg index 1ca40515b7..f786d0c2e9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cable.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cable.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M192 932q-13.15 0-21.075-7.888Q163 916.225 163 903.5v-50h-1q-16.5 0-27.5-11.25t-11-27.696V705.5q0-12.725 7.887-20.612Q139.275 677 152 677h51V367q0-63.175 46.376-105.588Q295.752 219 360.876 219 423 219 466 262.075q43 43.075 43 104.921V784q0 37.912 26.104 64.206 26.104 26.294 64.25 26.294 41.146 0 70.896-25.647T700 784V474.5h-51q-12.725 0-20.612-7.888Q620.5 458.725 620.5 446V337.246q0-17.246 11-28.246t28-11h.5v-50q0-12.725 7.888-20.612 7.887-7.888 20.612-7.888h80q12.575 0 20.787 7.888Q797.5 235.275 797.5 248v50h.5q16.5 0 27.75 11T837 337v109q0 12.725-8.069 20.612-8.069 7.888-20.431 7.888h-51V784q0 63.175-46.606 105.588Q664.288 932 599.164 932q-62.124 0-104.894-43.075T451.5 784.004V367q0-37.912-26.208-64.206-26.208-26.294-64.5-26.294-41.292 0-70.792 25.647T260.5 367v310h51q12.363 0 20.431 7.888Q340 692.775 340 705.5v108.808q0 16.692-11.138 27.942-11.137 11.25-27.849 11.25h-.513v50q0 12.725-8.213 20.612Q284.075 932 271.5 932H192Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M192 932q-13.15 0-21.075-7.888Q163 916.225 163 903.5v-50h-1q-16.5 0-27.5-11.25t-11-27.696V705.5q0-12.725 7.887-20.612Q139.275 677 152 677h51V367q0-63.175 46.376-105.588Q295.752 219 360.876 219 423 219 466 262.075q43 43.075 43 104.921V784q0 37.912 26.104 64.206 26.104 26.294 64.25 26.294 41.146 0 70.896-25.647T700 784V474.5h-51q-12.725 0-20.612-7.888Q620.5 458.725 620.5 446V337.246q0-17.246 11-28.246t28-11h.5v-50q0-12.725 7.888-20.612 7.887-7.888 20.612-7.888h80q12.575 0 20.787 7.888Q797.5 235.275 797.5 248v50h.5q16.5 0 27.75 11T837 337v109q0 12.725-8.069 20.612-8.069 7.888-20.431 7.888h-51V784q0 63.175-46.606 105.588Q664.288 932 599.164 932q-62.124 0-104.894-43.075T451.5 784.004V367q0-37.912-26.208-64.206-26.208-26.294-64.5-26.294-41.292 0-70.792 25.647T260.5 367v310h51q12.363 0 20.431 7.888Q340 692.775 340 705.5v108.808q0 16.692-11.138 27.942-11.137 11.25-27.849 11.25h-.513v50q0 12.725-8.213 20.612Q284.075 932 271.5 932H192Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_calendar.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_calendar.svg new file mode 100644 index 0000000000..bac344b0c8 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_calendar.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M182-85q-22.97 0-40.23-17.27-17.27-17.26-17.27-40.23v-616q0-22.97 17.27-40.23Q159.03-816 182-816h67.5v-60H312v60h336v-60h62.5v60H778q22.97 0 40.23 17.27 17.27 17.26 17.27 40.23v616q0 22.97-17.27 40.23Q800.97-85 778-85H182Zm0-57.5h596V-569H182v426.5Zm0-484h596v-132H182v132Zm0 0v-132 132Zm298.05 226q-16.76 0-28.16-11.34-11.39-11.34-11.39-28.11 0-16.76 11.34-28.16 11.34-11.39 28.11-11.39 16.76 0 28.16 11.34 11.39 11.34 11.39 28.11 0 16.76-11.34 28.16-11.34 11.39-28.11 11.39Zm-160 0q-16.76 0-28.16-11.34-11.39-11.34-11.39-28.11 0-16.76 11.34-28.16 11.34-11.39 28.11-11.39 16.76 0 28.16 11.34 11.39 11.34 11.39 28.11 0 16.76-11.34 28.16-11.34 11.39-28.11 11.39Zm320 0q-16.76 0-28.16-11.34-11.39-11.34-11.39-28.11 0-16.76 11.34-28.16 11.34-11.39 28.11-11.39 16.76 0 28.16 11.34 11.39 11.34 11.39 28.11 0 16.76-11.34 28.16-11.34 11.39-28.11 11.39Zm-160 160q-16.76 0-28.16-11.34-11.39-11.34-11.39-28.11 0-16.76 11.34-28.16 11.34-11.39 28.11-11.39 16.76 0 28.16 11.34 11.39 11.34 11.39 28.11 0 16.76-11.34 28.16-11.34 11.39-28.11 11.39Zm-160 0q-16.76 0-28.16-11.34-11.39-11.34-11.39-28.11 0-16.76 11.34-28.16 11.34-11.39 28.11-11.39 16.76 0 28.16 11.34 11.39 11.34 11.39 28.11 0 16.76-11.34 28.16-11.34 11.39-28.11 11.39Zm320 0q-16.76 0-28.16-11.34-11.39-11.34-11.39-28.11 0-16.76 11.34-28.16 11.34-11.39 28.11-11.39 16.76 0 28.16 11.34 11.39 11.34 11.39 28.11 0 16.76-11.34 28.16-11.34 11.39-28.11 11.39Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call.svg index 19c840a5cf..05577c3d0b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M790.886 931Q671.5 931 552.5 871.75t-214-154.25q-95-95-154.5-213.965t-59.5-238.647q0-18.888 12.714-31.638Q149.929 220.5 169 220.5h136.5q13.611 0 24.056 9.25Q340 239 343 255l27 124q2 13.068-.75 24.534t-10.335 19.05L260.5 522.5q56 93 124.75 161.25T542.5 801l95.544-98q9.456-10.5 21.206-14.75T683 687l117.362 25.452q15.45 3.423 25.294 15.236Q835.5 739.5 835.5 755.5v131q0 19.071-12.747 31.786Q810.006 931 790.886 931ZM231.5 470l81-82L289 278H182.5q-.5 38.5 11.75 85.25T231.5 470ZM778 873.5v-107L676 745l-79.5 83.5q40 19 88.166 31t93.334 14Zm-182-45ZM231.5 470Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M790.886 931Q671.5 931 552.5 871.75t-214-154.25q-95-95-154.5-213.965t-59.5-238.647q0-18.888 12.714-31.638Q149.929 220.5 169 220.5h136.5q13.611 0 24.056 9.25Q340 239 343 255l27 124q2 13.068-.75 24.534t-10.335 19.05L260.5 522.5q56 93 124.75 161.25T542.5 801l95.544-98q9.456-10.5 21.206-14.75T683 687l117.362 25.452q15.45 3.423 25.294 15.236Q835.5 739.5 835.5 755.5v131q0 19.071-12.747 31.786Q810.006 931 790.886 931ZM231.5 470l81-82L289 278H182.5q-.5 38.5 11.75 85.25T231.5 470ZM778 873.5v-107L676 745l-79.5 83.5q40 19 88.166 31t93.334 14Zm-182-45ZM231.5 470Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_500.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_500.svg index f2e1e9af6b..c18f78b148 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_500.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_500.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M791.125-119.5q-120.582 0-240.627-59.741-120.045-59.741-216.032-155.565Q238.478-430.63 178.5-550.791q-59.978-120.161-59.978-240.8 0-21.21 14.422-35.668 14.422-14.459 35.817-14.459h136.5q17.676 0 30.273 10.805 12.597 10.804 16.553 30.391l27 120.174q2.718 16.176-.818 29.586-3.536 13.409-13.136 22.368l-99.372 98.481q54.565 89.652 120.581 155.159Q452.358-309.247 537-262.174l96.739-98.717q11.37-12.174 24.913-16.903 13.544-4.728 28.413-1.011l112.34 24.974q19.277 4.619 30.675 18.344 11.398 13.726 11.398 33.313v132.196q0 21.633-14.455 36.056-14.455 14.422-35.898 14.422ZM232.696-589.109l80.761-80.326-23.261-106.652H185.13q-.978 37.312 10.903 82.849 11.881 45.536 36.663 104.129Zm542.913 403.5v-106.043l-98.413-21.261-79.022 82.783q38.804 18.76 85.894 30.521t91.541 14Zm-178.174-44.282ZM232.696-589.109Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M791.125-119.5q-120.582 0-240.627-59.741-120.045-59.741-216.032-155.565Q238.478-430.63 178.5-550.791q-59.978-120.161-59.978-240.8 0-21.21 14.422-35.668 14.422-14.459 35.817-14.459h136.5q17.676 0 30.273 10.805 12.597 10.804 16.553 30.391l27 120.174q2.718 16.176-.818 29.586-3.536 13.409-13.136 22.368l-99.372 98.481q54.565 89.652 120.581 155.159Q452.358-309.247 537-262.174l96.739-98.717q11.37-12.174 24.913-16.903 13.544-4.728 28.413-1.011l112.34 24.974q19.277 4.619 30.675 18.344 11.398 13.726 11.398 33.313v132.196q0 21.633-14.455 36.056-14.455 14.422-35.898 14.422ZM232.696-589.109l80.761-80.326-23.261-106.652H185.13q-.978 37.312 10.903 82.849 11.881 45.536 36.663 104.129Zm542.913 403.5v-106.043l-98.413-21.261-79.022 82.783q38.804 18.76 85.894 30.521t91.541 14Zm-178.174-44.282ZM232.696-589.109Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_end.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_end.svg index 0f21f36e83..2bcb5949f9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_end.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_end.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M480 418q125 0 238.75 50.25T914 613.5q8 9.5 8.25 21t-8.365 20.059L821 748q-8 8-22.5 8.75t-23.094-5.843L662.5 666.5q-5.75-4.5-8.625-10.25T651 643.5V504.407q-41.839-15.957-85.419-22.432Q522 475.5 480.744 475.5q-42.256 0-85.75 6.5T309.5 504.5v139q0 6.385-2.75 12.442Q304 662 298 666.5l-113.453 84.454Q173 759.5 161 758.5q-12-1-21.5-10.5l-93.442-93.441Q37.5 646 37.75 634.5 38 623 46 613.5q81.5-95 195.25-145.25T480 418ZM247 528.5q-39.5 19-74.5 46t-67 57l58 60L247 630V528.5Zm461.5-4V624l87.5 66.5 59-59q-32-33.5-68.75-59t-77.75-48Zm-461.5 4Zm461.5-4Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M480 418q125 0 238.75 50.25T914 613.5q8 9.5 8.25 21t-8.365 20.059L821 748q-8 8-22.5 8.75t-23.094-5.843L662.5 666.5q-5.75-4.5-8.625-10.25T651 643.5V504.407q-41.839-15.957-85.419-22.432Q522 475.5 480.744 475.5q-42.256 0-85.75 6.5T309.5 504.5v139q0 6.385-2.75 12.442Q304 662 298 666.5l-113.453 84.454Q173 759.5 161 758.5q-12-1-21.5-10.5l-93.442-93.441Q37.5 646 37.75 634.5 38 623 46 613.5q81.5-95 195.25-145.25T480 418ZM247 528.5q-39.5 19-74.5 46t-67 57l58 60L247 630V528.5Zm461.5-4V624l87.5 66.5 59-59q-32-33.5-68.75-59t-77.75-48Zm-461.5 4Zm461.5-4Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_end_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_end_filled.svg index ceb32f98f5..2d1e207e0e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_end_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_end_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M480 418q125 0 238.75 50.25T914 613.5q8 9.5 8.25 21t-8.25 20L821 748q-8 8-22.5 8.75t-23-5.75l-113-84.5q-6-4.5-8.75-10.25T651 643.5v-139q-42-16-85.5-22.5t-85-6.5q-42 0-85.5 6.5t-85.5 22.5v139q0 6.5-2.75 12.5T298 666.5L184.5 751q-11.5 8.5-23.5 7.5T139.5 748L46 654.5q-8.5-8.5-8.25-20t8.25-21q81.5-95 195.25-145.25T480 418Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M480 418q125 0 238.75 50.25T914 613.5q8 9.5 8.25 21t-8.25 20L821 748q-8 8-22.5 8.75t-23-5.75l-113-84.5q-6-4.5-8.75-10.25T651 643.5v-139q-42-16-85.5-22.5t-85-6.5q-42 0-85.5 6.5t-85.5 22.5v139q0 6.5-2.75 12.5T298 666.5L184.5 751q-11.5 8.5-23.5 7.5T139.5 748L46 654.5q-8.5-8.5-8.25-20t8.25-21q81.5-95 195.25-145.25T480 418Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_filled.svg index c0277fd5b6..100c933b7b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M791 931q-119.5 0-238.5-59.25t-214-154.25q-95-95-154.5-214T124.5 265q0-18.5 13-31.5t31.5-13h136.5q14 0 24.25 9.25T343 255l27 124q2 13-.75 24.5t-10.25 19l-99 100q56 93 125 161.25T542.5 801l95.5-98q9.5-10.5 21.25-14.75T683 687l117.5 25.5q15 3 25 15t10 28v131q0 18.5-13 31.5T791 931Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M791 931q-119.5 0-238.5-59.25t-214-154.25q-95-95-154.5-214T124.5 265q0-18.5 13-31.5t31.5-13h136.5q14 0 24.25 9.25T343 255l27 124q2 13-.75 24.5t-10.25 19l-99 100q56 93 125 161.25T542.5 801l95.5-98q9.5-10.5 21.25-14.75T683 687l117.5 25.5q15 3 25 15t10 28v131q0 18.5-13 31.5T791 931Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_camera_enhance.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_camera_enhance.svg index b07fbd7a87..baea2e28e9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_camera_enhance.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_camera_enhance.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M480.029 793Q551 793 602.75 741.941t51.75-122.5Q654.5 548 602.721 496q-51.78-52-122.75-52Q408 444 356.75 496.059t-51.25 123.5Q305.5 691 356.779 742q51.28 51 123.25 51ZM435.5 578.5l31-71.418Q470 498.5 480 499t13.5 9.082l30.159 70.418 65.923 27.581q9.418 4.161 9.418 13.79t-9.418 13.048L523.5 660l-30 69.918Q490 739 480 739.5t-13.5-8.582L435.5 660l-65.582-27.081Q360 629.5 360 619.871t9.918-13.79L435.5 578.5Zm-293 353q-22.969 0-40.234-17.266Q85 896.969 85 874V364.5q0-21.969 17.266-39.734Q119.531 307 142.5 307h147l54.91-66.5q7.59-10 19.106-15 11.515-5 24.984-5h183q13.469 0 24.984 5 11.516 5 19.516 15l54.5 66.5h147q21.969 0 39.734 17.766Q875 342.531 875 364.5V874q0 22.969-17.766 40.234Q839.469 931.5 817.5 931.5h-675Zm675-57.5V364.5h-675V874h675ZM480 618.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M480.029 793Q551 793 602.75 741.941t51.75-122.5Q654.5 548 602.721 496q-51.78-52-122.75-52Q408 444 356.75 496.059t-51.25 123.5Q305.5 691 356.779 742q51.28 51 123.25 51ZM435.5 578.5l31-71.418Q470 498.5 480 499t13.5 9.082l30.159 70.418 65.923 27.581q9.418 4.161 9.418 13.79t-9.418 13.048L523.5 660l-30 69.918Q490 739 480 739.5t-13.5-8.582L435.5 660l-65.582-27.081Q360 629.5 360 619.871t9.918-13.79L435.5 578.5Zm-293 353q-22.969 0-40.234-17.266Q85 896.969 85 874V364.5q0-21.969 17.266-39.734Q119.531 307 142.5 307h147l54.91-66.5q7.59-10 19.106-15 11.515-5 24.984-5h183q13.469 0 24.984 5 11.516 5 19.516 15l54.5 66.5h147q21.969 0 39.734 17.766Q875 342.531 875 364.5V874q0 22.969-17.766 40.234Q839.469 931.5 817.5 931.5h-675Zm675-57.5V364.5h-675V874h675ZM480 618.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cancel_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cancel_filled.svg index 44e901ff3c..41c39e11ef 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cancel_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cancel_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="m480 616 129 129q8.5 8.5 20 8.5t20-8.5q8.5-8.5 8.5-20t-8.5-20L520 576l129-129q8.5-8.5 8.5-20t-8.5-20q-8.5-8.5-20-8.5t-20 8.5L480 536 351 407q-8.5-8.5-20-8.5t-20 8.5q-8.5 8.5-8.5 20t8.5 20l129 129-129 129q-8.5 8.5-8.5 20t8.5 20q8.5 8.5 20 8.5t20-8.5l129-129Zm.064 355q-80.971 0-153.129-31.263-72.159-31.263-125.797-85Q147.5 801 116.25 728.977 85 656.953 85 576.064q0-81.971 31.263-154.129 31.263-72.159 85-125.547Q255 243 327.023 212q72.023-31 152.913-31 81.971 0 154.135 31.132 72.165 31.131 125.547 84.5Q813 350 844 422.023q31 72.023 31 153.913 0 80.97-31.013 153.129-31.013 72.159-84.5 125.797Q706 908.5 633.977 939.75 561.954 971 480.064 971Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="m480 616 129 129q8.5 8.5 20 8.5t20-8.5q8.5-8.5 8.5-20t-8.5-20L520 576l129-129q8.5-8.5 8.5-20t-8.5-20q-8.5-8.5-20-8.5t-20 8.5L480 536 351 407q-8.5-8.5-20-8.5t-20 8.5q-8.5 8.5-8.5 20t8.5 20l129 129-129 129q-8.5 8.5-8.5 20t8.5 20q8.5 8.5 20 8.5t20-8.5l129-129Zm.064 355q-80.971 0-153.129-31.263-72.159-31.263-125.797-85Q147.5 801 116.25 728.977 85 656.953 85 576.064q0-81.971 31.263-154.129 31.263-72.159 85-125.547Q255 243 327.023 212q72.023-31 152.913-31 81.971 0 154.135 31.132 72.165 31.131 125.547 84.5Q813 350 844 422.023q31 72.023 31 153.913 0 80.97-31.013 153.129-31.013 72.159-84.5 125.797Q706 908.5 633.977 939.75 561.954 971 480.064 971Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat.svg index c263166229..82e58ead79 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M272 654.5h252q10.936 0 19.218-8.36t8.282-20.719q0-11.921-8.412-20.171Q534.675 597 523 597H271q-11.5 0-19.75 8.457-8.25 8.456-8.25 20.218 0 12.325 8.375 20.575T272 654.5Zm0-129h417.5q10.936 0 19.218-8.36T717 496.421q0-11.921-8.412-20.171Q700.175 468 688.5 468H271q-11.5 0-19.75 8.457-8.25 8.456-8.25 20.218 0 12.325 8.375 20.575T272 525.5Zm0-129h417.5q10.936 0 19.218-8.36T717 367.421q0-11.921-8.412-20.171Q700.175 339 688.5 339H271q-11.5 0-19.75 8.457-8.25 8.456-8.25 20.218 0 12.325 8.375 20.575T272 396.5ZM85 900V238q0-21.969 17.266-39.734Q119.531 180.5 142.5 180.5h675q21.969 0 39.734 17.766Q875 216.031 875 238v516q0 21.969-17.766 39.734Q839.469 811.5 817.5 811.5H242.206L133.5 920q-13.5 13.5-31 6.172T85 900Zm57.5-70.5L218 754h599.5V238h-675v591.5Zm0-591.5v591.5V238Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M272 654.5h252q10.936 0 19.218-8.36t8.282-20.719q0-11.921-8.412-20.171Q534.675 597 523 597H271q-11.5 0-19.75 8.457-8.25 8.456-8.25 20.218 0 12.325 8.375 20.575T272 654.5Zm0-129h417.5q10.936 0 19.218-8.36T717 496.421q0-11.921-8.412-20.171Q700.175 468 688.5 468H271q-11.5 0-19.75 8.457-8.25 8.456-8.25 20.218 0 12.325 8.375 20.575T272 525.5Zm0-129h417.5q10.936 0 19.218-8.36T717 367.421q0-11.921-8.412-20.171Q700.175 339 688.5 339H271q-11.5 0-19.75 8.457-8.25 8.456-8.25 20.218 0 12.325 8.375 20.575T272 396.5ZM85 900V238q0-21.969 17.266-39.734Q119.531 180.5 142.5 180.5h675q21.969 0 39.734 17.766Q875 216.031 875 238v516q0 21.969-17.766 39.734Q839.469 811.5 817.5 811.5H242.206L133.5 920q-13.5 13.5-31 6.172T85 900Zm57.5-70.5L218 754h599.5V238h-675v591.5Zm0-591.5v591.5V238Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_bubble.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_bubble.svg index 132c514e92..78e3bf42dc 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_bubble.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_bubble.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M241.776-244.5 134-136.5q-13.5 13.5-31.25 6.359Q85-137.281 85-156.5V-818q0-22.969 17.266-40.234Q119.531-875.5 142.5-875.5h675q22.969 0 40.234 17.266Q875-840.969 875-818v516q0 22.969-17.266 40.234Q840.469-244.5 817.5-244.5H241.776ZM142.5-302h675v-516h-675v516Zm0 0v-516 516Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M241.776-244.5 134-136.5q-13.5 13.5-31.25 6.359Q85-137.281 85-156.5V-818q0-22.969 17.266-40.234Q119.531-875.5 142.5-875.5h675q22.969 0 40.234 17.266Q875-840.969 875-818v516q0 22.969-17.266 40.234Q840.469-244.5 817.5-244.5H241.776ZM142.5-302h675v-516h-675v516Zm0 0v-516 516Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check.svg index e3d6eab069..a5445b0d5f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M378.46 795.5q-5.726 0-10.486-2-4.76-2-9.474-6.5L179.09 607.59q-8.59-8.59-8.59-21.09t8.411-21q8.41-8.5 20.25-8.5 11.839 0 20.41 8.571L378.5 724.5l360.937-360.937Q747.783 355 759.902 355q12.12 0 21.098 8.5 8.5 8.5 8.5 20.609 0 12.108-8.587 20.478L398.5 787q-4.794 4.5-9.554 6.5-4.76 2-10.486 2Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M378.46 795.5q-5.726 0-10.486-2-4.76-2-9.474-6.5L179.09 607.59q-8.59-8.59-8.59-21.09t8.411-21q8.41-8.5 20.25-8.5 11.839 0 20.41 8.571L378.5 724.5l360.937-360.937Q747.783 355 759.902 355q12.12 0 21.098 8.5 8.5 8.5 8.5 20.609 0 12.108-8.587 20.478L398.5 787q-4.794 4.5-9.554 6.5-4.76 2-10.486 2Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_circle.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_circle.svg new file mode 100644 index 0000000000..813adcea2d --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_circle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22" fill="#5f6368"><path d="M421.5-300.5 701-580l-44.5-43.5-235 235-119-119L259-464l162.5 163.5ZM480.06-85q-80.97 0-153.13-31.26-72.15-31.27-125.79-85Q147.5-255 116.25-327.02 85-399.05 85-479.94q0-81.97 31.26-154.13 31.27-72.15 85-125.54Q255-813 327.02-844q72.03-31 152.92-31 81.97 0 154.13 31.13 72.17 31.13 125.55 84.5Q813-706 844-633.98q31 72.03 31 153.92 0 80.97-31.01 153.13-31.02 72.15-84.5 125.79Q706-147.5 633.98-116.25 561.95-85 480.06-85Zm-.09-57.5q140.53 0 239.03-98.97 98.5-98.96 98.5-238.5 0-140.53-98.47-239.03-98.46-98.5-239-98.5-139.53 0-238.53 98.47-99 98.46-99 239 0 139.53 98.97 238.53 98.96 99 238.5 99ZM480-480Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_circle_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_circle_filled.svg index 42f1a37725..ac2f200c6f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_circle_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_circle_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="m421.5 667.5-97.557-98.057Q315 561 302.5 561q-12.5 0-22 9.5-9 9-9 21.5t8.824 20.824L401.5 735q8.318 9 19.909 9T442 735l237-237q9.5-9.5 9.5-22t-9.5-22.5q-10-8.5-23-8t-21.368 8.868L421.5 667.5ZM480 971q-80.907 0-153.065-31.263-72.159-31.263-125.797-85Q147.5 801 116.25 728.913 85 656.825 85 576q0-81.907 31.263-154.065 31.263-72.159 85-125.547Q255 243 327.087 212 399.175 181 480 181q81.907 0 154.065 31.013 72.159 31.013 125.547 84.5Q813 350 844 422.087 875 494.175 875 576q0 80.906-31.013 153.065-31.013 72.159-84.5 125.797Q706 908.5 633.913 939.75 561.825 971 480 971Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="m421.5 667.5-97.557-98.057Q315 561 302.5 561q-12.5 0-22 9.5-9 9-9 21.5t8.824 20.824L401.5 735q8.318 9 19.909 9T442 735l237-237q9.5-9.5 9.5-22t-9.5-22.5q-10-8.5-23-8t-21.368 8.868L421.5 667.5ZM480 971q-80.907 0-153.065-31.263-72.159-31.263-125.797-85Q147.5 801 116.25 728.913 85 656.825 85 576q0-81.907 31.263-154.065 31.263-72.159 85-125.547Q255 243 327.087 212 399.175 181 480 181q81.907 0 154.065 31.013 72.159 31.013 125.547 84.5Q813 350 844 422.087 875 494.175 875 576q0 80.906-31.013 153.065-31.013 72.159-84.5 125.797Q706 908.5 633.913 939.75 561.825 971 480 971Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_filled.svg index e3d6eab069..a5445b0d5f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M378.46 795.5q-5.726 0-10.486-2-4.76-2-9.474-6.5L179.09 607.59q-8.59-8.59-8.59-21.09t8.411-21q8.41-8.5 20.25-8.5 11.839 0 20.41 8.571L378.5 724.5l360.937-360.937Q747.783 355 759.902 355q12.12 0 21.098 8.5 8.5 8.5 8.5 20.609 0 12.108-8.587 20.478L398.5 787q-4.794 4.5-9.554 6.5-4.76 2-10.486 2Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M378.46 795.5q-5.726 0-10.486-2-4.76-2-9.474-6.5L179.09 607.59q-8.59-8.59-8.59-21.09t8.411-21q8.41-8.5 20.25-8.5 11.839 0 20.41 8.571L378.5 724.5l360.937-360.937Q747.783 355 759.902 355q12.12 0 21.098 8.5 8.5 8.5 8.5 20.609 0 12.108-8.587 20.478L398.5 787q-4.794 4.5-9.554 6.5-4.76 2-10.486 2Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_checklist.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_checklist.svg new file mode 100644 index 0000000000..947e78c1e5 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_checklist.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="m225.5-216-140-140 40-40.5 100 99 179-179L445-435 225.5-216Zm0-320-140-140 40-40.5 100 99 179-179L445-755 225.5-536ZM521-291.5V-349h354v57.5H521Zm0-320V-669h354v57.5H521Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_right.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_right.svg index ac6025580b..3c33462417 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_right.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_right.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M355.5 791.5q-8-9.5-8.5-20.75t8.434-20.184L531 575 354.434 398.434Q346.5 390.5 347 377.75t8.5-20.75q9.5-9.5 20.5-9t19.859 8.859L593.5 555q4.5 4.58 6.75 9.34t2.25 10.7q0 5.94-2.25 10.7-2.25 4.76-6.75 9.26L396.859 791.641Q388 800.5 376.5 800q-11.5-.5-21-8.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M355.5 791.5q-8-9.5-8.5-20.75t8.434-20.184L531 575 354.434 398.434Q346.5 390.5 347 377.75t8.5-20.75q9.5-9.5 20.5-9t19.859 8.859L593.5 555q4.5 4.58 6.75 9.34t2.25 10.7q0 5.94-2.25 10.7-2.25 4.76-6.75 9.26L396.859 791.641Q388 800.5 376.5 800q-11.5-.5-21-8.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_right_2.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_right_2.svg new file mode 100644 index 0000000000..ef2a9e9865 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_right_2.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 960" width="20px" height="20px"> + <g transform="matrix(1, 0, 0, 1, -160, 960)"> + <path + d="m592.5-482-314-314q-10.5-10.5-10.75-26.25t10.75-27.25q11-11.5 27-11.5t27.5 11.5L661-522q8.5 8.5 12.25 18.75T677-482q0 11-3.75 21.25T661-442L332.5-113.5q-11.5 11.5-27.25 11t-26.75-12q-11-11-11.25-26.5t11.25-27l314-314Z" /> + </g> + <g transform="matrix(1, 0, 0, 1, 160, 960)"> + <path + d="m592.5-482-314-314q-10.5-10.5-10.75-26.25t10.75-27.25q11-11.5 27-11.5t27.5 11.5L661-522q8.5 8.5 12.25 18.75T677-482q0 11-3.75 21.25T661-442L332.5-113.5q-11.5 11.5-27.25 11t-26.75-12q-11-11-11.25-26.5t11.25-27l314-314Z" /> + </g> +</svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_circle.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_circle.svg index d0da456188..dd6d17343d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_circle.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_circle.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M480.064 971q-80.971 0-153.129-31.263-72.159-31.263-125.797-85Q147.5 801 116.25 728.977 85 656.953 85 576.064q0-81.971 31.263-154.129 31.263-72.159 85-125.547Q255 243 327.023 212q72.023-31 152.913-31 81.971 0 154.135 31.132 72.165 31.131 125.547 84.5Q813 350 844 422.023q31 72.023 31 153.913 0 80.97-31.013 153.129-31.013 72.159-84.5 125.797Q706 908.5 633.977 939.75 561.954 971 480.064 971Zm-.097-57.5Q620.5 913.5 719 814.533t98.5-238.5Q817.5 435.5 719.033 337t-239-98.5q-139.533 0-238.533 98.467t-99 239q0 139.533 98.967 238.533t238.5 99ZM480 576Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M480.064 971q-80.971 0-153.129-31.263-72.159-31.263-125.797-85Q147.5 801 116.25 728.977 85 656.953 85 576.064q0-81.971 31.263-154.129 31.263-72.159 85-125.547Q255 243 327.023 212q72.023-31 152.913-31 81.971 0 154.135 31.132 72.165 31.131 125.547 84.5Q813 350 844 422.023q31 72.023 31 153.913 0 80.97-31.013 153.129-31.013 72.159-84.5 125.797Q706 908.5 633.977 939.75 561.954 971 480.064 971Zm-.097-57.5Q620.5 913.5 719 814.533t98.5-238.5Q817.5 435.5 719.033 337t-239-98.5q-139.533 0-238.533 98.467t-99 239q0 139.533 98.967 238.533t238.5 99ZM480 576Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_circle_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_circle_filled.svg index e8eebbdc7c..65d942e3ad 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_circle_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_circle_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M480.064 971q-80.971 0-153.129-31.263-72.159-31.263-125.797-85Q147.5 801 116.25 728.977 85 656.953 85 576.064q0-81.971 31.263-154.129 31.263-72.159 85-125.547Q255 243 327.023 212q72.023-31 152.913-31 81.971 0 154.135 31.132 72.165 31.131 125.547 84.5Q813 350 844 422.023q31 72.023 31 153.913 0 80.97-31.013 153.129-31.013 72.159-84.5 125.797Q706 908.5 633.977 939.75 561.954 971 480.064 971Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M480.064 971q-80.971 0-153.129-31.263-72.159-31.263-125.797-85Q147.5 801 116.25 728.977 85 656.953 85 576.064q0-81.971 31.263-154.129 31.263-72.159 85-125.547Q255 243 327.023 212q72.023-31 152.913-31 81.971 0 154.135 31.132 72.165 31.131 125.547 84.5Q813 350 844 422.023q31 72.023 31 153.913 0 80.97-31.013 153.129-31.013 72.159-84.5 125.797Q706 908.5 633.977 939.75 561.954 971 480.064 971Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_close.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_close.svg index f3803e3cac..b24eaf6cb9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_close.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_close.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="m480 616.5-209 209q-9 9-20.25 9t-20.25-9q-9-9-9-20.25t9-20.25L440 575.5 230.5 366q-9-8.5-9-19.75t9-20.25q9-9 20.25-9t20.25 9l209 209.5 209-209q9-9 20.25-9t20.25 9q9 9 9 20.25t-9 20.25l-209 209L730 785.5q8.5 9 8.5 20.25T730 825.5q-9 9-20.25 9t-20.25-9L480 616.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="m480 616.5-209 209q-9 9-20.25 9t-20.25-9q-9-9-9-20.25t9-20.25L440 575.5 230.5 366q-9-8.5-9-19.75t9-20.25q9-9 20.25-9t20.25 9l209 209.5 209-209q9-9 20.25-9t20.25 9q9 9 9 20.25t-9 20.25l-209 209L730 785.5q8.5 9 8.5 20.25T730 825.5q-9 9-20.25 9t-20.25-9L480 616.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_code.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_code.svg index 8554e557b0..918bf1a005 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_code.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_code.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M657.5 792.5q-9 9-20.5 8.5t-20-9.5q-9-8.5-9-20.5t9-21l176-176-175-175.5q-9-8.5-8.5-20.5t9.5-21q8.5-8.5 20.25-8.5T660 357l196 197q8 8.5 8 20.25T856 594L657.5 792.5Zm-356-3-197-196q-8.5-8-8.5-19.75t8.5-20.25L303 355q8.5-8.5 20.5-8.5t21 8.5q8.5 9 8.5 21t-8.5 20.5L167 574l175.5 175q8.5 9 8.5 20.5t-8.5 20q-9 9-20.75 9t-20.25-9Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M657.5 792.5q-9 9-20.5 8.5t-20-9.5q-9-8.5-9-20.5t9-21l176-176-175-175.5q-9-8.5-8.5-20.5t9.5-21q8.5-8.5 20.25-8.5T660 357l196 197q8 8.5 8 20.25T856 594L657.5 792.5Zm-356-3-197-196q-8.5-8-8.5-19.75t8.5-20.25L303 355q8.5-8.5 20.5-8.5t21 8.5q8.5 9 8.5 21t-8.5 20.5L167 574l175.5 175q8.5 9 8.5 20.5t-8.5 20q-9 9-20.75 9t-20.25-9Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_collapse_all.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_collapse_all.svg index a380594e79..673cb66084 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_collapse_all.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_collapse_all.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m291.5-89.5-40.5-40 229-229 229 229-40.5 40L480-278 291.5-89.5Zm188.5-513-229-229 40.5-40.5L480-683.5 668.5-872l40.5 40.5-229 229Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="m291.5-89.5-40.5-40 229-229 229 229-40.5 40L480-278 291.5-89.5Zm188.5-513-229-229 40.5-40.5L480-683.5 668.5-872l40.5 40.5-229 229Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_content_copy.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_content_copy.svg index 2f4b964368..f8dde3e39f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_content_copy.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_content_copy.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M184 968q-22.969 0-40.234-17.266Q126.5 933.469 126.5 910.5v-567q0-12.25 8.425-20.625 8.426-8.375 20.5-8.375 12.075 0 20.325 8.375T184 343.5v567h439q11.675 0 20.088 8.463 8.412 8.463 8.412 20.212 0 12.325-8.412 20.575Q634.675 968 623 968H184Zm115-115q-22.969 0-40.234-17.266Q241.5 818.469 241.5 795.5v-556q0-22.969 17.266-40.234Q276.031 182 299 182h437.706q22.232 0 39.763 17.266Q794 216.531 794 239.5v556q0 22.969-17.531 40.234Q758.938 853 736.706 853H299Zm0-57.5h437.5v-556H299v556Zm0 0v-556 556Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M184 968q-22.969 0-40.234-17.266Q126.5 933.469 126.5 910.5v-567q0-12.25 8.425-20.625 8.426-8.375 20.5-8.375 12.075 0 20.325 8.375T184 343.5v567h439q11.675 0 20.088 8.463 8.412 8.463 8.412 20.212 0 12.325-8.412 20.575Q634.675 968 623 968H184Zm115-115q-22.969 0-40.234-17.266Q241.5 818.469 241.5 795.5v-556q0-22.969 17.266-40.234Q276.031 182 299 182h437.706q22.232 0 39.763 17.266Q794 216.531 794 239.5v556q0 22.969-17.531 40.234Q758.938 853 736.706 853H299Zm0-57.5h437.5v-556H299v556Zm0 0v-556 556Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_content_paste.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_content_paste.svg index 84a0be536b..b162257525 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_content_paste.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_content_paste.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M182.5 931q-24.969 0-41.234-16.031Q125 898.938 125 873.5V278q0-25.438 16.031-41.469Q157.062 220.5 182.5 220.5h203.062q5.938-34.5 32.589-57 26.652-22.5 62.222-22.5 34.627 0 61.377 22.5t33.25 57h203.5q24.969 0 41.234 16.031Q836 252.562 836 278v595.5q0 25.438-16.266 41.469Q803.469 931 778.5 931h-596Zm0-57.5h596V278H721v57.5q0 12.25-8.375 20.375T692 364H268.5q-12.25 0-20.375-8.125T240 335.5V278h-57.5v595.5ZM480.25 276q16.25 0 27.5-11.25t11.25-27.5q0-16.25-11.288-27.5-11.287-11.25-27.212-11.25-16.5 0-27.75 11.287Q441.5 221.075 441.5 237q0 16.5 11.25 27.75t27.5 11.25Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M182.5 931q-24.969 0-41.234-16.031Q125 898.938 125 873.5V278q0-25.438 16.031-41.469Q157.062 220.5 182.5 220.5h203.062q5.938-34.5 32.589-57 26.652-22.5 62.222-22.5 34.627 0 61.377 22.5t33.25 57h203.5q24.969 0 41.234 16.031Q836 252.562 836 278v595.5q0 25.438-16.266 41.469Q803.469 931 778.5 931h-596Zm0-57.5h596V278H721v57.5q0 12.25-8.375 20.375T692 364H268.5q-12.25 0-20.375-8.125T240 335.5V278h-57.5v595.5ZM480.25 276q16.25 0 27.5-11.25t11.25-27.5q0-16.25-11.288-27.5-11.287-11.25-27.212-11.25-16.5 0-27.75 11.287Q441.5 221.075 441.5 237q0 16.5 11.25 27.75t27.5 11.25Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_database.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_database.svg index 8b47047b60..1195169889 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_database.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_database.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480.091-121.5q-150.591 0-254.591-46T121.5-280v-400q0-65.619 105.043-112.309Q331.586-839 480-839q148.5 0 253.75 46.691Q839-745.619 839-680v400q0 66.419-104.409 112.459-104.409 46.041-254.5 46.041Zm.032-489.5q84.377 0 174.127-25.75 89.75-25.75 119.25-59-30-32.25-120.248-59T480-781.5q-87 0-175.5 25.75T186-696q30.5 35 117.863 60 87.362 25 176.26 25ZM479-395q41.5 0 83.25-4.5t80.417-13.5q38.667-9 74.372-21.75T781.5-463.5v-161q-30 17-65.25 30t-74.5 22.25Q602.5-563 561.5-558t-82.384 5q-41.384 0-83.5-5t-80.832-14.196q-38.716-9.196-73.5-22.25T179-624.5v161q27.5 16 62.034 28.851 34.534 12.852 73.25 21.75Q353-404 394.75-399.5 436.5-395 479-395Zm1 216q47.896 0 99.288-8.561 51.393-8.561 94.052-22.5Q716-224 745.771-241.5T781.5-277v-128q-29 16-64.53 28.5t-74.25 21.25Q604-346.5 562.428-342q-41.573 4.5-83.42 4.5-42.347 0-84.427-4.5-42.081-4.5-80.831-13.25t-73.037-21.153Q206.425-388.806 179-405v129q5 17.5 34 35t72 31.25q43 13.75 94.25 22.25T480-179Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M480.091-121.5q-150.591 0-254.591-46T121.5-280v-400q0-65.619 105.043-112.309Q331.586-839 480-839q148.5 0 253.75 46.691Q839-745.619 839-680v400q0 66.419-104.409 112.459-104.409 46.041-254.5 46.041Zm.032-489.5q84.377 0 174.127-25.75 89.75-25.75 119.25-59-30-32.25-120.248-59T480-781.5q-87 0-175.5 25.75T186-696q30.5 35 117.863 60 87.362 25 176.26 25ZM479-395q41.5 0 83.25-4.5t80.417-13.5q38.667-9 74.372-21.75T781.5-463.5v-161q-30 17-65.25 30t-74.5 22.25Q602.5-563 561.5-558t-82.384 5q-41.384 0-83.5-5t-80.832-14.196q-38.716-9.196-73.5-22.25T179-624.5v161q27.5 16 62.034 28.851 34.534 12.852 73.25 21.75Q353-404 394.75-399.5 436.5-395 479-395Zm1 216q47.896 0 99.288-8.561 51.393-8.561 94.052-22.5Q716-224 745.771-241.5T781.5-277v-128q-29 16-64.53 28.5t-74.25 21.25Q604-346.5 562.428-342q-41.573 4.5-83.42 4.5-42.347 0-84.427-4.5-42.081-4.5-80.831-13.25t-73.037-21.153Q206.425-388.806 179-405v129q5 17.5 34 35t72 31.25q43 13.75 94.25 22.25T480-179Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete.svg index 1709888116..ea3f90edaa 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M262.5 931q-22.969 0-40.234-17.266Q205 896.469 205 873.5v-566h-11.5q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.5 0-12.075 8.375-20.325T193.5 250h155.686q0-12.425 7.919-20.713Q365.025 221 377.5 221H583q12.475 0 20.439 8.338 7.964 8.337 7.964 20.662h156.3q11.547 0 19.922 8.463T796 278.675q0 12.325-8.412 20.575-8.413 8.25-20.088 8.25h-12v566q0 22.969-17.266 40.234Q720.969 931 698 931H262.5Zm0-623.5v566H698v-566H262.5Zm107 452q0 12.25 8.535 20.375t20.39 8.125q11.856 0 20.216-8.125Q427 771.75 427 759.5v-339q0-12.25-8.463-20.625t-20.212-8.375q-12.325 0-20.575 8.375T369.5 420.5v339Zm164 0q0 12.25 8.535 20.375t20.39 8.125q11.856 0 20.216-8.125Q591 771.75 591 759.5v-339q0-12.25-8.463-20.625t-20.212-8.375q-12.325 0-20.575 8.375T533.5 420.5v339Zm-271-452v566-566Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M262.5 931q-22.969 0-40.234-17.266Q205 896.469 205 873.5v-566h-11.5q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.5 0-12.075 8.375-20.325T193.5 250h155.686q0-12.425 7.919-20.713Q365.025 221 377.5 221H583q12.475 0 20.439 8.338 7.964 8.337 7.964 20.662h156.3q11.547 0 19.922 8.463T796 278.675q0 12.325-8.412 20.575-8.413 8.25-20.088 8.25h-12v566q0 22.969-17.266 40.234Q720.969 931 698 931H262.5Zm0-623.5v566H698v-566H262.5Zm107 452q0 12.25 8.535 20.375t20.39 8.125q11.856 0 20.216-8.125Q427 771.75 427 759.5v-339q0-12.25-8.463-20.625t-20.212-8.375q-12.325 0-20.575 8.375T369.5 420.5v339Zm164 0q0 12.25 8.535 20.375t20.39 8.125q11.856 0 20.216-8.125Q591 771.75 591 759.5v-339q0-12.25-8.463-20.625t-20.212-8.375q-12.325 0-20.575 8.375T533.5 420.5v339Zm-271-452v566-566Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete_forever.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete_forever.svg index 519fbc87fb..af3f3c5fb3 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete_forever.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete_forever.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M262.5 931q-22.969 0-40.234-17.266Q205 896.469 205 873.5v-566h-11.5q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.5 0-12.075 8.375-20.325T193.5 250h155.686q0-12.425 7.919-20.713Q365.025 221 377.5 221H583q12.475 0 20.439 8.338 7.964 8.337 7.964 20.662h156.3q11.547 0 19.922 8.463T796 278.675q0 12.325-8.412 20.575-8.413 8.25-20.088 8.25h-12v566q0 22.969-17.266 40.234Q720.969 931 698 931H262.5Zm0-623.5v566H698v-566H262.5Zm0 0v566-566Zm217.75 327L576 731q10.5 10 23.75 10t22.75-10q9.5-9.333 10-22.979.5-13.646-10-22.521L527 587.768l95.5-97.731q9.5-9.37 10-23.016.5-13.646-9.814-22.63Q613.232 435 599.433 435q-13.8 0-23.433 9.568L480.5 541.5l-95-97q-8.375-10-22.188-9.75Q349.5 435 339 444.5q-10.5 10-10 23.25T339 491l96 97-96.063 96.563Q329.5 694.5 329.5 707.75q0 13.25 9.565 23.185Q349.5 741 362.75 741q13.25 0 22.687-9.937L480.25 634.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M262.5 931q-22.969 0-40.234-17.266Q205 896.469 205 873.5v-566h-11.5q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.5 0-12.075 8.375-20.325T193.5 250h155.686q0-12.425 7.919-20.713Q365.025 221 377.5 221H583q12.475 0 20.439 8.338 7.964 8.337 7.964 20.662h156.3q11.547 0 19.922 8.463T796 278.675q0 12.325-8.412 20.575-8.413 8.25-20.088 8.25h-12v566q0 22.969-17.266 40.234Q720.969 931 698 931H262.5Zm0-623.5v566H698v-566H262.5Zm0 0v566-566Zm217.75 327L576 731q10.5 10 23.75 10t22.75-10q9.5-9.333 10-22.979.5-13.646-10-22.521L527 587.768l95.5-97.731q9.5-9.37 10-23.016.5-13.646-9.814-22.63Q613.232 435 599.433 435q-13.8 0-23.433 9.568L480.5 541.5l-95-97q-8.375-10-22.188-9.75Q349.5 435 339 444.5q-10.5 10-10 23.25T339 491l96 97-96.063 96.563Q329.5 694.5 329.5 707.75q0 13.25 9.565 23.185Q349.5 741 362.75 741q13.25 0 22.687-9.937L480.25 634.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete_forever_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete_forever_filled.svg index c8c1d4cea5..8b46878258 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete_forever_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete_forever_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M480.5 634.5 576 731q10.333 10 23.667 10 13.333 0 22.855-9.911Q632 721.732 632 708.116t-9.5-22.616L527 588l95.5-98q10.5-9.833 10.5-23.167 0-13.333-10.357-22.355-9.411-9.478-22.777-9.728Q586.5 434.5 576 444.5l-95.5 97-95-97q-8.375-10-22.188-9.75-13.812.25-24.29 9.607-9.522 8.911-9.522 22.777Q329.5 481 339 491l96 97-96 96.5q-9.5 9.875-9.5 23.188 0 13.312 9.411 23.29Q349.268 741 362.634 741t22.866-10l95-96.5ZM262.5 931q-22.938 0-40.219-17.281Q205 896.438 205 873.5v-566h-11.5q-12.5 0-20.75-8.25t-8.25-20.75q0-12 8.25-20.25T193.5 250H349q0-12.5 7.888-20.75Q364.775 221 377.5 221H583q12.725 0 20.612 8.175Q611.5 237.35 611.5 250h156q12 0 20.25 8.25T796 278.5q0 12.5-8.25 20.75t-20.25 8.25h-12v566q0 22.938-17.281 40.219Q720.938 931 698 931H262.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M480.5 634.5 576 731q10.333 10 23.667 10 13.333 0 22.855-9.911Q632 721.732 632 708.116t-9.5-22.616L527 588l95.5-98q10.5-9.833 10.5-23.167 0-13.333-10.357-22.355-9.411-9.478-22.777-9.728Q586.5 434.5 576 444.5l-95.5 97-95-97q-8.375-10-22.188-9.75-13.812.25-24.29 9.607-9.522 8.911-9.522 22.777Q329.5 481 339 491l96 97-96 96.5q-9.5 9.875-9.5 23.188 0 13.312 9.411 23.29Q349.268 741 362.634 741t22.866-10l95-96.5ZM262.5 931q-22.938 0-40.219-17.281Q205 896.438 205 873.5v-566h-11.5q-12.5 0-20.75-8.25t-8.25-20.75q0-12 8.25-20.25T193.5 250H349q0-12.5 7.888-20.75Q364.775 221 377.5 221H583q12.725 0 20.612 8.175Q611.5 237.35 611.5 250h156q12 0 20.25 8.25T796 278.5q0 12.5-8.25 20.75t-20.25 8.25h-12v566q0 22.938-17.281 40.219Q720.938 931 698 931H262.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_desktop.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_desktop.svg index e9c30f5199..aa60447d7e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_desktop.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_desktop.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M422.5-182v-99.5h-280q-22.969 0-40.234-17.266Q85-316.031 85-339v-439q0-22.969 17.266-40.234Q119.531-835.5 142.5-835.5h675q22.969 0 40.234 17.266Q875-800.969 875-778v439q0 22.969-17.266 40.234Q840.469-281.5 817.5-281.5h-280v99.5h57q11.675 0 20.088 8.463Q623-165.074 623-153.325q0 12.325-8.412 20.575-8.413 8.25-20.088 8.25H366q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.5 0-12.075 8.375-20.325T366-182h56.5Zm-280-157h675v-439h-675v439Zm0 0v-439 439Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M422.5-182v-99.5h-280q-22.969 0-40.234-17.266Q85-316.031 85-339v-439q0-22.969 17.266-40.234Q119.531-835.5 142.5-835.5h675q22.969 0 40.234 17.266Q875-800.969 875-778v439q0 22.969-17.266 40.234Q840.469-281.5 817.5-281.5h-280v99.5h57q11.675 0 20.088 8.463Q623-165.074 623-153.325q0 12.325-8.412 20.575-8.413 8.25-20.088 8.25H366q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.5 0-12.075 8.375-20.325T366-182h56.5Zm-280-157h675v-439h-675v439Zm0 0v-439 439Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_dns.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_dns.svg index c1056cefc6..932f45e6b9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_dns.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_dns.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M288.471 343q-20.638 0-34.804 14.154-14.167 14.155-14.167 34.375 0 20.638 14.309 34.804 14.309 14.167 34.75 14.167Q309 440.5 323 426.191q14-14.309 14-34.75Q337 371 322.846 357q-14.155-14-34.375-14Zm0 408.5q-20.638 0-34.804 14.309-14.167 14.309-14.167 34.75Q239.5 821 253.809 835q14.309 14 34.75 14Q309 849 323 834.846q14-14.155 14-34.375 0-20.638-14.154-34.804-14.155-14.167-34.375-14.167ZM158 223h643q15.5 0 25 9.141 9.5 9.14 9.5 24.922V521q0 17.638-9.5 29.069-9.5 11.431-25 11.431H158q-14.5 0-24-11.492t-9.5-29.22V257.116q0-15.766 9.5-24.941Q143.5 223 158 223Zm24 57.5V504h596V280.5H182Zm-24 350h639q14.5 0 26.5 12.5t12 27.87V929.5q0 20.213-12 30.356Q811.5 970 797 970H163q-15.5 0-27-10.216-11.5-10.215-11.5-30.534V671.027q0-15.527 9.5-28.027 9.5-12.5 24-12.5Zm24 57.5v224.5h596V688H182Zm0-407.5V504 280.5Zm0 407.5v224.5V688Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M288.471 343q-20.638 0-34.804 14.154-14.167 14.155-14.167 34.375 0 20.638 14.309 34.804 14.309 14.167 34.75 14.167Q309 440.5 323 426.191q14-14.309 14-34.75Q337 371 322.846 357q-14.155-14-34.375-14Zm0 408.5q-20.638 0-34.804 14.309-14.167 14.309-14.167 34.75Q239.5 821 253.809 835q14.309 14 34.75 14Q309 849 323 834.846q14-14.155 14-34.375 0-20.638-14.154-34.804-14.155-14.167-34.375-14.167ZM158 223h643q15.5 0 25 9.141 9.5 9.14 9.5 24.922V521q0 17.638-9.5 29.069-9.5 11.431-25 11.431H158q-14.5 0-24-11.492t-9.5-29.22V257.116q0-15.766 9.5-24.941Q143.5 223 158 223Zm24 57.5V504h596V280.5H182Zm-24 350h639q14.5 0 26.5 12.5t12 27.87V929.5q0 20.213-12 30.356Q811.5 970 797 970H163q-15.5 0-27-10.216-11.5-10.215-11.5-30.534V671.027q0-15.527 9.5-28.027 9.5-12.5 24-12.5Zm24 57.5v224.5h596V688H182Zm0-407.5V504 280.5Zm0 407.5v224.5V688Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_do_not_disturb_on.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_do_not_disturb_on.svg index 9a0d038ba5..14df478255 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_do_not_disturb_on.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_do_not_disturb_on.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M311.5 601.5H649q11.675 0 20.088-8.425 8.412-8.426 8.412-20.5 0-12.075-8.412-20.325Q660.675 544 649 544H311.5q-12.25 0-20.625 8.463t-8.375 20.212q0 12.325 8.375 20.575t20.625 8.25ZM480.33 971q-81.704 0-153.629-31.263t-125.563-85Q147.5 801 116.25 729.136 85 657.272 85 575.564q0-81.789 31.263-153.789 31.263-71.999 85-125.387Q255 243 326.864 212q71.864-31 153.572-31 81.789 0 153.795 31.132 72.005 31.131 125.387 84.5Q813 350 844 422.023q31 72.023 31 153.647 0 81.705-31.013 153.629-31.013 71.925-84.5 125.379-53.487 53.454-125.51 84.888Q561.954 971 480.33 971Zm.137-57.5Q620.5 913.5 719 814.533t98.5-239Q817.5 435.5 719.221 337q-98.28-98.5-239.188-98.5-139.533 0-238.533 98.279-99 98.28-99 239.188 0 139.533 98.967 238.533t239 99ZM480 576Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M311.5 601.5H649q11.675 0 20.088-8.425 8.412-8.426 8.412-20.5 0-12.075-8.412-20.325Q660.675 544 649 544H311.5q-12.25 0-20.625 8.463t-8.375 20.212q0 12.325 8.375 20.575t20.625 8.25ZM480.33 971q-81.704 0-153.629-31.263t-125.563-85Q147.5 801 116.25 729.136 85 657.272 85 575.564q0-81.789 31.263-153.789 31.263-71.999 85-125.387Q255 243 326.864 212q71.864-31 153.572-31 81.789 0 153.795 31.132 72.005 31.131 125.387 84.5Q813 350 844 422.023q31 72.023 31 153.647 0 81.705-31.013 153.629-31.013 71.925-84.5 125.379-53.487 53.454-125.51 84.888Q561.954 971 480.33 971Zm.137-57.5Q620.5 913.5 719 814.533t98.5-239Q817.5 435.5 719.221 337q-98.28-98.5-239.188-98.5-139.533 0-238.533 98.279-99 98.28-99 239.188 0 139.533 98.967 238.533t239 99ZM480 576Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_do_not_touch.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_do_not_touch.svg index 5ea1a5f2e3..698df39e9c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_do_not_touch.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_do_not_touch.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M835-207.5 777.5-265v-501.5q0-11.675 8.425-20.088 8.426-8.412 20.5-8.412 12.075 0 20.325 8.412Q835-778.175 835-766.5v559ZM342.5-699 285-756.5v-50q0-11.675 8.425-20.088 8.426-8.412 20.5-8.412 12.075 0 20.325 8.412 8.25 8.413 8.25 20.088V-699ZM507-535.5 449.5-592v-294.5q0-11.675 8.425-20.088 8.426-8.412 20.5-8.412 12.075 0 20.325 8.412Q507-898.175 507-886.5v351ZM671.5-483H614v-363.668q0-11.582 8.425-19.957 8.426-8.375 20.5-8.375 12.075 0 20.325 8.351t8.25 19.935V-483ZM751-126.5 342.5-535v250.5L165-415l199 291q7 11 17.839 16.75 10.84 5.75 24.161 5.75h280.5q17.897 0 34.948-6.25Q738.5-114 751-126.5ZM406-44q-27.049 0-51.274-12.5Q330.5-69 316-91.5L59.5-468l19-15.5q17-14.5 39-18.25t43.573 12.938L285-394.5v-198l-253-253L73.5-887 873-87l-41 41-41-40.5q-20 19.5-47.17 31T686.5-44H406Zm140.5-287.5ZM560-483Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M835-207.5 777.5-265v-501.5q0-11.675 8.425-20.088 8.426-8.412 20.5-8.412 12.075 0 20.325 8.412Q835-778.175 835-766.5v559ZM342.5-699 285-756.5v-50q0-11.675 8.425-20.088 8.426-8.412 20.5-8.412 12.075 0 20.325 8.412 8.25 8.413 8.25 20.088V-699ZM507-535.5 449.5-592v-294.5q0-11.675 8.425-20.088 8.426-8.412 20.5-8.412 12.075 0 20.325 8.412Q507-898.175 507-886.5v351ZM671.5-483H614v-363.668q0-11.582 8.425-19.957 8.426-8.375 20.5-8.375 12.075 0 20.325 8.351t8.25 19.935V-483ZM751-126.5 342.5-535v250.5L165-415l199 291q7 11 17.839 16.75 10.84 5.75 24.161 5.75h280.5q17.897 0 34.948-6.25Q738.5-114 751-126.5ZM406-44q-27.049 0-51.274-12.5Q330.5-69 316-91.5L59.5-468l19-15.5q17-14.5 39-18.25t43.573 12.938L285-394.5v-198l-253-253L73.5-887 873-87l-41 41-41-40.5q-20 19.5-47.17 31T686.5-44H406Zm140.5-287.5ZM560-483Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_done_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_done_filled.svg index e3d6eab069..a5445b0d5f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_done_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_done_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M378.46 795.5q-5.726 0-10.486-2-4.76-2-9.474-6.5L179.09 607.59q-8.59-8.59-8.59-21.09t8.411-21q8.41-8.5 20.25-8.5 11.839 0 20.41 8.571L378.5 724.5l360.937-360.937Q747.783 355 759.902 355q12.12 0 21.098 8.5 8.5 8.5 8.5 20.609 0 12.108-8.587 20.478L398.5 787q-4.794 4.5-9.554 6.5-4.76 2-10.486 2Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M378.46 795.5q-5.726 0-10.486-2-4.76-2-9.474-6.5L179.09 607.59q-8.59-8.59-8.59-21.09t8.411-21q8.41-8.5 20.25-8.5 11.839 0 20.41 8.571L378.5 724.5l360.937-360.937Q747.783 355 759.902 355q12.12 0 21.098 8.5 8.5 8.5 8.5 20.609 0 12.108-8.587 20.478L398.5 787q-4.794 4.5-9.554 6.5-4.76 2-10.486 2Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_double_check.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_double_check.svg index 2c651607df..a5c0797f3e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_double_check.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_double_check.svg @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8" standalone="no" ?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 48 48" xml:space="preserve"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="22" height="22" viewBox="0 0 48 48" xml:space="preserve"> <desc>Created with Fabric.js 5.3.0</desc> <defs> </defs> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_download.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_download.svg index 8d62bebda6..65e1af1fb6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_download.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_download.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M479.827 730.5q-5.671 0-10.499-2t-9.328-7L311.645 573.145Q302.5 564.5 303 552.5t9.25-20.5q8.75-8.5 20.5-8.5T353 532l98.5 99.5V290q0-12.25 8.175-20.625 8.176-8.375 20.5-8.375 12.325 0 20.575 8.375T509 290v341.5l99.064-99.5q8.436-8.5 20.186-8.5t20.5 8.5q8.75 8.5 8.75 20.5t-8.823 20.787L500 721.5q-4.58 5-9.542 7-4.961 2-10.631 2ZM222 889q-22.969 0-40.234-17.266Q164.5 854.469 164.5 831.5v-113q0-12.25 8.425-20.625 8.426-8.375 20.5-8.375 12.075 0 20.325 8.375T222 718.5v113h516v-113q0-12.25 8.425-20.625 8.426-8.375 20.5-8.375 12.075 0 20.325 8.375t8.25 20.625v113q0 22.969-17.266 40.234Q760.969 889 738 889H222Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M479.827 730.5q-5.671 0-10.499-2t-9.328-7L311.645 573.145Q302.5 564.5 303 552.5t9.25-20.5q8.75-8.5 20.5-8.5T353 532l98.5 99.5V290q0-12.25 8.175-20.625 8.176-8.375 20.5-8.375 12.325 0 20.575 8.375T509 290v341.5l99.064-99.5q8.436-8.5 20.186-8.5t20.5 8.5q8.75 8.5 8.75 20.5t-8.823 20.787L500 721.5q-4.58 5-9.542 7-4.961 2-10.631 2ZM222 889q-22.969 0-40.234-17.266Q164.5 854.469 164.5 831.5v-113q0-12.25 8.425-20.625 8.426-8.375 20.5-8.375 12.075 0 20.325 8.375T222 718.5v113h516v-113q0-12.25 8.425-20.625 8.426-8.375 20.5-8.375 12.075 0 20.325 8.375t8.25 20.625v113q0 22.969-17.266 40.234Q760.969 889 738 889H222Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_draft.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_draft.svg index bbe311364f..edced6eb83 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_draft.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_draft.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M222-85q-22.969 0-40.234-17.266Q164.5-119.531 164.5-142.5v-675q0-22.969 17.266-40.234Q199.031-875 222-875h335q11.911 0 22.706 4.75 10.794 4.75 18.912 12.343l179.264 179.314Q786-670.5 790.75-659.706q4.75 10.795 4.75 22.706v494.5q0 22.969-17.266 40.234Q760.969-85 738-85H222Zm330.5-579v-153.5H222v675h516V-635H581.5q-12.25 0-20.625-8.375T552.5-664ZM222-817.5V-635v-182.5 675-675Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M222-85q-22.969 0-40.234-17.266Q164.5-119.531 164.5-142.5v-675q0-22.969 17.266-40.234Q199.031-875 222-875h335q11.911 0 22.706 4.75 10.794 4.75 18.912 12.343l179.264 179.314Q786-670.5 790.75-659.706q4.75 10.795 4.75 22.706v494.5q0 22.969-17.266 40.234Q760.969-85 738-85H222Zm330.5-579v-153.5H222v675h516V-635H581.5q-12.25 0-20.625-8.375T552.5-664ZM222-817.5V-635v-182.5 675-675Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_draft_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_draft_filled.svg index 06140a055d..f40173226c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_draft_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_draft_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M222 971q-22.969 0-40.234-17.266Q164.5 936.469 164.5 913.5v-675q0-22.969 17.266-40.234Q199.031 181 222 181h335q11.498 0 22.544 4.859Q590.589 190.717 598.5 198l179.479 180.106q7.8 7.836 12.66 18.649Q795.5 407.567 795.5 419v494.5q0 22.969-17.266 40.234Q760.969 971 738 971H222Zm330.5-579q0 12.5 8.25 20.75T581.5 421H738L552.5 238.5V392Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M222 971q-22.969 0-40.234-17.266Q164.5 936.469 164.5 913.5v-675q0-22.969 17.266-40.234Q199.031 181 222 181h335q11.498 0 22.544 4.859Q590.589 190.717 598.5 198l179.479 180.106q7.8 7.836 12.66 18.649Q795.5 407.567 795.5 419v494.5q0 22.969-17.266 40.234Q760.969 971 738 971H222Zm330.5-579q0 12.5 8.25 20.75T581.5 421H738L552.5 238.5V392Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_drag_handle.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_drag_handle.svg new file mode 100644 index 0000000000..99d3e66fe8 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_drag_handle.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M170-368v-75h620v75H170Zm0-150v-75h620v75H170Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_drive_folder_upload.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_drive_folder_upload.svg index ec3d03510e..94a21b1a9d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_drive_folder_upload.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_drive_folder_upload.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M480.175 773q12.325 0 20.575-8.125T509 744.5V569l54 54q4.343 3.773 9.411 6.386Q577.478 632 583.269 632t10.761-2.5q4.97-2.5 9.47-6.5 8.5-9 8.5-20.25t-8.357-20.107L500 479q-8.61-8-20.091-8-11.48 0-19.909 8L356.904 582.548Q348 591.5 348 602.75q0 11.25 8.75 20.25t20.25 9q11.5 0 20.5-9l54-54v175.5q0 12.25 8.175 20.375 8.176 8.125 20.5 8.125ZM143.5 892q-22.969 0-40.234-17.766Q86 856.469 86 834.5V320q0-21.969 17.266-39.734Q120.531 262.5 143.5 262.5h256q11.943 0 22.766 4.739 10.823 4.739 18.727 12.754L481 320h337.5q21.969 0 39.734 17.766Q876 355.531 876 377.5v457q0 21.969-17.766 39.734Q840.469 892 818.5 892h-675Zm0-572v514.5h675v-457H457L399.5 320h-256Zm0 0v514.5V320Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M480.175 773q12.325 0 20.575-8.125T509 744.5V569l54 54q4.343 3.773 9.411 6.386Q577.478 632 583.269 632t10.761-2.5q4.97-2.5 9.47-6.5 8.5-9 8.5-20.25t-8.357-20.107L500 479q-8.61-8-20.091-8-11.48 0-19.909 8L356.904 582.548Q348 591.5 348 602.75q0 11.25 8.75 20.25t20.25 9q11.5 0 20.5-9l54-54v175.5q0 12.25 8.175 20.375 8.176 8.125 20.5 8.125ZM143.5 892q-22.969 0-40.234-17.766Q86 856.469 86 834.5V320q0-21.969 17.266-39.734Q120.531 262.5 143.5 262.5h256q11.943 0 22.766 4.739 10.823 4.739 18.727 12.754L481 320h337.5q21.969 0 39.734 17.766Q876 355.531 876 377.5v457q0 21.969-17.766 39.734Q840.469 892 818.5 892h-675Zm0-572v514.5h675v-457H457L399.5 320h-256Zm0 0v514.5V320Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit.svg index 400707272e..143f7e8e0b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M183 872.5h42l444-444-42-42-444 444v42ZM790.5 388 668 265l40.323-40.323Q725.094 208 749.297 208.5q24.203.5 40.737 17.147l41.432 41.206Q847.5 283.5 847.25 307.5q-.25 24-16.275 40.025L790.5 388ZM154.327 930q-12.614 0-20.721-8.102-8.106-8.102-8.106-20.709v-82.09q0-5.599 2-10.635 2-5.035 6.5-9.464l494.5-494L751 427.5l-494.5 494q-4.483 4.5-9.554 6.5-5.07 2-9.946 2h-82.673ZM648 407.5l-21-21 42 42-21-21Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M183 872.5h42l444-444-42-42-444 444v42ZM790.5 388 668 265l40.323-40.323Q725.094 208 749.297 208.5q24.203.5 40.737 17.147l41.432 41.206Q847.5 283.5 847.25 307.5q-.25 24-16.275 40.025L790.5 388ZM154.327 930q-12.614 0-20.721-8.102-8.106-8.102-8.106-20.709v-82.09q0-5.599 2-10.635 2-5.035 6.5-9.464l494.5-494L751 427.5l-494.5 494q-4.483 4.5-9.554 6.5-5.07 2-9.946 2h-82.673ZM648 407.5l-21-21 42 42-21-21Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit_filled.svg index e87e2eccaf..7550e08df5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M790.5 388 668 265l40.5-40.5q16.594-16.5 40.797-16 24.203.5 40.737 17.147l41.432 41.206Q847.5 283.5 847.25 307.5q-.25 24-16.312 40.062L790.5 388ZM154 930q-12.725 0-20.613-7.888-7.887-7.887-7.887-20.612v-82.463q0-5.537 2-10.573 2-5.035 6.5-9.464l494.5-494L751 427.5l-494.5 494q-4.483 4.5-9.336 6.5-4.854 2-10.164 2h-83Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M790.5 388 668 265l40.5-40.5q16.594-16.5 40.797-16 24.203.5 40.737 17.147l41.432 41.206Q847.5 283.5 847.25 307.5q-.25 24-16.312 40.062L790.5 388ZM154 930q-12.725 0-20.613-7.888-7.887-7.887-7.887-20.612v-82.463q0-5.537 2-10.573 2-5.035 6.5-9.464l494.5-494L751 427.5l-494.5 494q-4.483 4.5-9.336 6.5-4.854 2-10.164 2h-83Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit_note.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit_note.svg index 0c66d1bc4d..cb0488d834 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit_note.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit_note.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="m810 675-69-69 28-28q8.311-8 20.156-8Q801 570 809 578l29 29q8 8.311 8 20.156Q846 639 838 647l-28 28ZM481.5 934.5v-69L696 651l69 69-214.5 214.5h-69ZM124 724v-57.5h300V724H124Zm0-164v-57.5h468.5V560H124Zm0-163.5V339h468.5v57.5H124Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="m810 675-69-69 28-28q8.311-8 20.156-8Q801 570 809 578l29 29q8 8.311 8 20.156Q846 639 838 647l-28 28ZM481.5 934.5v-69L696 651l69 69-214.5 214.5h-69ZM124 724v-57.5h300V724H124Zm0-164v-57.5h468.5V560H124Zm0-163.5V339h468.5v57.5H124Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_electrical_services.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_electrical_services.svg index e6ab5bb608..03e014417d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_electrical_services.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_electrical_services.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M715 694.5V637h95q12.5 0 20.75 8.25T839 666q0 11.5-8.25 20T810 694.5h-95Zm0 160V797h95q12.5 0 20.75 8.25T839 826q0 11.5-8.25 20T810 854.5h-95Zm-155 40q-28.381 0-48.441-23.25Q491.5 848 491.5 814.487H395V677h96.5q0-33 20.059-56.5Q531.619 597 560 597h98q12.362 0 20.431 8.069 8.069 8.068 8.069 20.431V866q0 12.725-8.069 20.612Q670.362 894.5 658 894.5h-98Zm-285-120q-66.381 0-109.941-43.084-43.559-43.084-43.559-110.5t43.559-110.666Q208.619 467 275 467h65q33.625 0 55.062-21.309 21.438-21.309 21.438-54.75T395.062 336Q373.625 314.5 340 314.5H190q-11.5 0-20-8.5t-8.5-20q0-12.5 8.5-20.75t20-8.25h150q57.586 0 95.793 38.189Q474 333.379 474 390.939q0 57.561-38.207 95.561-38.207 38-95.793 38h-65q-42.413 0-69.206 26.416-26.794 26.416-26.794 70t26.794 69.834Q232.587 717 275 717h91.5v57.5H275Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M715 694.5V637h95q12.5 0 20.75 8.25T839 666q0 11.5-8.25 20T810 694.5h-95Zm0 160V797h95q12.5 0 20.75 8.25T839 826q0 11.5-8.25 20T810 854.5h-95Zm-155 40q-28.381 0-48.441-23.25Q491.5 848 491.5 814.487H395V677h96.5q0-33 20.059-56.5Q531.619 597 560 597h98q12.362 0 20.431 8.069 8.069 8.068 8.069 20.431V866q0 12.725-8.069 20.612Q670.362 894.5 658 894.5h-98Zm-285-120q-66.381 0-109.941-43.084-43.559-43.084-43.559-110.5t43.559-110.666Q208.619 467 275 467h65q33.625 0 55.062-21.309 21.438-21.309 21.438-54.75T395.062 336Q373.625 314.5 340 314.5H190q-11.5 0-20-8.5t-8.5-20q0-12.5 8.5-20.75t20-8.25h150q57.586 0 95.793 38.189Q474 333.379 474 390.939q0 57.561-38.207 95.561-38.207 38-95.793 38h-65q-42.413 0-69.206 26.416-26.794 26.416-26.794 70t26.794 69.834Q232.587 717 275 717h91.5v57.5H275Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_engineering.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_engineering.svg index 0848537041..ea2c5c3843 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_engineering.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_engineering.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M46-124v-90q0-33.5 15.516-55.596Q77.03-291.692 106-305.5q54-26 114.624-42.75Q281.249-365 359-365t138.376 16.75Q558-331.5 612-305.5q28.969 13.808 44.484 35.904Q672-247.5 672-214v90H46Zm57.5-57.5h511v-32.744q0-15.256-8.75-25.006t-20.65-14.792q-42.6-19.153-95.35-36.305Q437-307.5 359-307.5t-130.75 17.153q-52.75 17.152-95.35 36.305Q121-249 112.25-239.25q-8.75 9.75-8.75 25.006v32.744Zm255.456-243q-64.456 0-105.86-41.984Q211.692-508.469 211.692-572H201.5q-8 0-14-6t-6-14q0-8 6-14t14-6h10.185q0-39.154 19.158-70.077Q250-713 282.143-733v38.459q0 6.244 4.098 10.392Q290.339-680 296.5-680q7.225 0 10.862-4.138Q311-688.275 311-694.364v-51.631q7.845-1.98 22.172-3.492Q347.5-751 360-751t26.828 1.511q14.327 1.511 22.172 3.487v51.573q0 5.929 3.638 10.179Q416.275-680 423.5-680q6.161 0 10.259-4.149 4.098-4.148 4.098-10.2V-733q32.143 20 50.3 50.923 18.158 30.923 18.158 70.077H516.5q8 0 14 6t6 14q0 8-6 14t-14 6h-10.192q0 63.531-41.448 105.516Q423.411-424.5 358.956-424.5ZM359-482q42 0 66-25t24-65H269q0 40 24 65t66 25Zm300 119.5-1.885-29q-7.115-4-14.615-9t-13.5-10l-26 14-21.5-31 26-19q-2-4-2-7.5v-15q0-3.5 2-7.5l-26-19 21.5-31 26 14q7-5.5 13.97-10.312 6.969-4.813 13.939-8.688l1.906-29h39.37l1.906 29q6.97 3.875 13.939 8.688Q721-518 728-512.5l26-14 21.5 31-26 19q2 4 2 7.5v15q0 3.5-2 7.5l26 19-21.5 31-26-14q-6 5-13.5 10t-14.5 9l-1.769 29H659Zm19.5-61q16 0 27-11t11-27q0-16-11-27t-27-11q-16 0-27 11t-11 27q0 16 11 27t27 11ZM766.349-579l-8.164-33.933Q748-617 737.179-624q-10.822-7-17.554-15L676-624l-19-33 34-27.5q-2-5-3.5-11.25T686-708q0-6 1.5-12.25t3.5-11.25L657-759l19-33 43.562 15q6.938-8 17.782-15.5Q748.188-800 758-803l8.5-34h37.151l8.164 33.933Q822-800 832.821-792.5q10.822 7.5 17.554 15.5L894-792l19 33-34 27.5q2 5 3.5 11.25T884-708q0 6-1.5 12.25T879-684.5l34 27.5-19 33-43.562-15q-6.938 8-17.782 15-10.844 7-20.656 11l-8.5 34h-37.151ZM785-650q25 0 41.5-16.5T843-708q0-25-16.5-41.5T785-766q-25 0-41.5 16.5T727-708q0 25 16.5 41.5T785-650ZM103.5-181.5h511-511Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M46-124v-90q0-33.5 15.516-55.596Q77.03-291.692 106-305.5q54-26 114.624-42.75Q281.249-365 359-365t138.376 16.75Q558-331.5 612-305.5q28.969 13.808 44.484 35.904Q672-247.5 672-214v90H46Zm57.5-57.5h511v-32.744q0-15.256-8.75-25.006t-20.65-14.792q-42.6-19.153-95.35-36.305Q437-307.5 359-307.5t-130.75 17.153q-52.75 17.152-95.35 36.305Q121-249 112.25-239.25q-8.75 9.75-8.75 25.006v32.744Zm255.456-243q-64.456 0-105.86-41.984Q211.692-508.469 211.692-572H201.5q-8 0-14-6t-6-14q0-8 6-14t14-6h10.185q0-39.154 19.158-70.077Q250-713 282.143-733v38.459q0 6.244 4.098 10.392Q290.339-680 296.5-680q7.225 0 10.862-4.138Q311-688.275 311-694.364v-51.631q7.845-1.98 22.172-3.492Q347.5-751 360-751t26.828 1.511q14.327 1.511 22.172 3.487v51.573q0 5.929 3.638 10.179Q416.275-680 423.5-680q6.161 0 10.259-4.149 4.098-4.148 4.098-10.2V-733q32.143 20 50.3 50.923 18.158 30.923 18.158 70.077H516.5q8 0 14 6t6 14q0 8-6 14t-14 6h-10.192q0 63.531-41.448 105.516Q423.411-424.5 358.956-424.5ZM359-482q42 0 66-25t24-65H269q0 40 24 65t66 25Zm300 119.5-1.885-29q-7.115-4-14.615-9t-13.5-10l-26 14-21.5-31 26-19q-2-4-2-7.5v-15q0-3.5 2-7.5l-26-19 21.5-31 26 14q7-5.5 13.97-10.312 6.969-4.813 13.939-8.688l1.906-29h39.37l1.906 29q6.97 3.875 13.939 8.688Q721-518 728-512.5l26-14 21.5 31-26 19q2 4 2 7.5v15q0 3.5-2 7.5l26 19-21.5 31-26-14q-6 5-13.5 10t-14.5 9l-1.769 29H659Zm19.5-61q16 0 27-11t11-27q0-16-11-27t-27-11q-16 0-27 11t-11 27q0 16 11 27t27 11ZM766.349-579l-8.164-33.933Q748-617 737.179-624q-10.822-7-17.554-15L676-624l-19-33 34-27.5q-2-5-3.5-11.25T686-708q0-6 1.5-12.25t3.5-11.25L657-759l19-33 43.562 15q6.938-8 17.782-15.5Q748.188-800 758-803l8.5-34h37.151l8.164 33.933Q822-800 832.821-792.5q10.822 7.5 17.554 15.5L894-792l19 33-34 27.5q2 5 3.5 11.25T884-708q0 6-1.5 12.25T879-684.5l34 27.5-19 33-43.562-15q-6.938 8-17.782 15-10.844 7-20.656 11l-8.5 34h-37.151ZM785-650q25 0 41.5-16.5T843-708q0-25-16.5-41.5T785-766q-25 0-41.5 16.5T727-708q0 25 16.5 41.5T785-650ZM103.5-181.5h511-511Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_error.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_error.svg index 60296ca034..7a91448c9d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_error.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_error.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M479.877 773.5q14.123 0 23.123-8.877 9-8.877 9-23t-8.877-23.123q-8.877-9-23-9T457 718.377q-9 8.877-9 23t8.877 23.123q8.877 9 23 9Zm3.548-150.5q12.075 0 20.325-8.375T512 594V400.5q0-11.675-8.463-20.088Q495.074 372 483.325 372q-12.325 0-20.575 8.412-8.25 8.413-8.25 20.088V594q0 12.25 8.425 20.625 8.426 8.375 20.5 8.375Zm-3.095 348q-81.704 0-153.629-31.263t-125.563-85Q147.5 801 116.25 729.136 85 657.272 85 575.564q0-81.789 31.263-153.789 31.263-71.999 85-125.387Q255 243 326.864 212q71.864-31 153.572-31 81.789 0 153.795 31.132 72.005 31.131 125.387 84.5Q813 350 844 422.023q31 72.023 31 153.647 0 81.705-31.013 153.629-31.013 71.925-84.5 125.379-53.487 53.454-125.51 84.888Q561.954 971 480.33 971Zm.137-57.5Q620.5 913.5 719 814.533t98.5-239Q817.5 435.5 719.221 337q-98.28-98.5-239.188-98.5-139.533 0-238.533 98.279-99 98.28-99 239.188 0 139.533 98.967 238.533t239 99ZM480 576Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M479.877 773.5q14.123 0 23.123-8.877 9-8.877 9-23t-8.877-23.123q-8.877-9-23-9T457 718.377q-9 8.877-9 23t8.877 23.123q8.877 9 23 9Zm3.548-150.5q12.075 0 20.325-8.375T512 594V400.5q0-11.675-8.463-20.088Q495.074 372 483.325 372q-12.325 0-20.575 8.412-8.25 8.413-8.25 20.088V594q0 12.25 8.425 20.625 8.426 8.375 20.5 8.375Zm-3.095 348q-81.704 0-153.629-31.263t-125.563-85Q147.5 801 116.25 729.136 85 657.272 85 575.564q0-81.789 31.263-153.789 31.263-71.999 85-125.387Q255 243 326.864 212q71.864-31 153.572-31 81.789 0 153.795 31.132 72.005 31.131 125.387 84.5Q813 350 844 422.023q31 72.023 31 153.647 0 81.705-31.013 153.629-31.013 71.925-84.5 125.379-53.487 53.454-125.51 84.888Q561.954 971 480.33 971Zm.137-57.5Q620.5 913.5 719 814.533t98.5-239Q817.5 435.5 719.221 337q-98.28-98.5-239.188-98.5-139.533 0-238.533 98.279-99 98.28-99 239.188 0 139.533 98.967 238.533t239 99ZM480 576Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_error_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_error_filled.svg index 6aaa01f0d3..7940dc756a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_error_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_error_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M479.877 773.5q14.123 0 23.123-8.877 9-8.877 9-23t-8.877-23.123q-8.877-9-23-9T457 718.377q-9 8.877-9 23t8.877 23.123q8.877 9 23 9Zm3.548-150.5q12.075 0 20.325-8.375T512 594V400.5q0-11.675-8.463-20.088Q495.074 372 483.325 372q-12.325 0-20.575 8.412-8.25 8.413-8.25 20.088V594q0 12.25 8.425 20.625 8.426 8.375 20.5 8.375Zm-3.095 348q-81.704 0-153.629-31.263t-125.563-85Q147.5 801 116.25 729.136 85 657.272 85 575.564q0-81.789 31.263-153.789 31.263-71.999 85-125.387Q255 243 326.864 212q71.864-31 153.572-31 81.789 0 153.795 31.132 72.005 31.131 125.387 84.5Q813 350 844 422.023q31 72.023 31 153.647 0 81.705-31.013 153.629-31.013 71.925-84.5 125.379-53.487 53.454-125.51 84.888Q561.954 971 480.33 971Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M479.877 773.5q14.123 0 23.123-8.877 9-8.877 9-23t-8.877-23.123q-8.877-9-23-9T457 718.377q-9 8.877-9 23t8.877 23.123q8.877 9 23 9Zm3.548-150.5q12.075 0 20.325-8.375T512 594V400.5q0-11.675-8.463-20.088Q495.074 372 483.325 372q-12.325 0-20.575 8.412-8.25 8.413-8.25 20.088V594q0 12.25 8.425 20.625 8.426 8.375 20.5 8.375Zm-3.095 348q-81.704 0-153.629-31.263t-125.563-85Q147.5 801 116.25 729.136 85 657.272 85 575.564q0-81.789 31.263-153.789 31.263-71.999 85-125.387Q255 243 326.864 212q71.864-31 153.572-31 81.789 0 153.795 31.132 72.005 31.131 125.387 84.5Q813 350 844 422.023q31 72.023 31 153.647 0 81.705-31.013 153.629-31.013 71.925-84.5 125.379-53.487 53.454-125.51 84.888Q561.954 971 480.33 971Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_all.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_all.svg index 75b2874d5b..ec76dfb115 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_all.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_all.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m480-84.5-237-237 42.5-42L480-169l195-194.5 42 42-237 237Zm-195-512-42-42 237-237 237 237-42 42L480-791 285-596.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="m480-84.5-237-237 42.5-42L480-169l195-194.5 42 42-237 237Zm-195-512-42-42 237-237 237 237-42 42L480-791 285-596.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_less.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_less.svg deleted file mode 100644 index 23a6402630..0000000000 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_less.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M264.5 687.5q-9-8.5-9.5-20t8.414-20.414L459.97 450.53q4.61-4.53 9.47-6.78t10.7-2.25q5.84 0 10.6 2.25t9.307 6.781L697 646q8.5 7.744 8.5 19.872T697 686.5q-9 9-21 9t-20.401-8.901L480 512.5l-175 176q-7.744 9-19.872 8.5t-20.628-9.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_more.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_more.svg deleted file mode 100644 index 77becd0856..0000000000 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_more.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M479.96 697.5q-5.94 0-10.7-2-4.76-2-9.26-6.5L263.36 491.86q-7.86-7.36-7.36-20.36t8.5-21q9.5-9.5 20.5-8t19.901 8.901L480 627l175.598-175.598Q663 444 676 443q13-1 21 8.5 9.5 8 8 20.5t-8.859 20.859L500 689q-4.58 4.5-9.34 6.5t-10.7 2Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_filter_list.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_filter_list.svg index a54ee0dc55..4a9c6e8420 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_filter_list.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_filter_list.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M430.5-252.5q-12.25 0-20.625-8.175-8.375-8.176-8.375-20.5 0-12.325 8.375-20.575T430.5-310h99q12.25 0 20.625 8.425 8.375 8.426 8.375 20.75 0 12.325-8.375 20.325t-20.625 8h-99ZM152.5-660q-12.25 0-20.625-8.175-8.375-8.176-8.375-20.5 0-12.325 8.375-20.575t20.625-8.25h655q12.25 0 20.625 8.425 8.375 8.426 8.375 20.75 0 12.325-8.375 20.325t-20.625 8h-655Zm119 204q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075t20.625-8.25h417q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T688.5-456h-417Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M430.5-252.5q-12.25 0-20.625-8.175-8.375-8.176-8.375-20.5 0-12.325 8.375-20.575T430.5-310h99q12.25 0 20.625 8.425 8.375 8.426 8.375 20.75 0 12.325-8.375 20.325t-20.625 8h-99ZM152.5-660q-12.25 0-20.625-8.175-8.375-8.176-8.375-20.5 0-12.325 8.375-20.575t20.625-8.25h655q12.25 0 20.625 8.425 8.375 8.426 8.375 20.75 0 12.325-8.375 20.325t-20.625 8h-655Zm119 204q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075t20.625-8.25h417q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T688.5-456h-417Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flag.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flag.svg index 35dd4ac780..d2f84f3b3a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flag.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flag.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M232.825 932.5q-12.325 0-20.575-8.375T204 903.5V289q0-12.475 8.012-20.487 8.013-8.013 20.488-8.013h286.054q9.896 0 17.671 6.25Q544 273 546 283l13.974 63H767.5q12.475 0 20.487 8.013Q796 362.025 796 374.5v306q0 12.475-8.013 20.487Q779.975 709 767.5 709h-199q-10.111 0-18.056-5.75Q542.5 697.5 540.5 687l-14-62.5h-265v279q0 12.25-8.463 20.625t-20.212 8.375ZM500 484.5Zm94.373 167H738.5V403.625H511.068L492.168 318H261.5v248.875h313.977l18.896 84.625Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M232.825 932.5q-12.325 0-20.575-8.375T204 903.5V289q0-12.475 8.012-20.487 8.013-8.013 20.488-8.013h286.054q9.896 0 17.671 6.25Q544 273 546 283l13.974 63H767.5q12.475 0 20.487 8.013Q796 362.025 796 374.5v306q0 12.475-8.013 20.487Q779.975 709 767.5 709h-199q-10.111 0-18.056-5.75Q542.5 697.5 540.5 687l-14-62.5h-265v279q0 12.25-8.463 20.625t-20.212 8.375ZM500 484.5Zm94.373 167H738.5V403.625H511.068L492.168 318H261.5v248.875h313.977l18.896 84.625Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flag_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flag_filled.svg new file mode 100644 index 0000000000..a6f5a70618 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flag_filled.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M204-123.5v-672h337l19.13 85.5H796v363H545.5l-19-84.5h-265v308H204Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flip_camera_android_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flip_camera_android_filled.svg index da9d686ce9..0244be139f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flip_camera_android_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flip_camera_android_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M480.5 972q-126.116 0-227.058-71.5T110.5 712q-5-11.939.5-22.719Q116.5 678.5 128.5 675q11-3 21.936 3.141 10.935 6.14 15.564 17.359 36.5 99 122.256 159 85.755 60 192.244 60 98 0 180.25-51T788 725.5H686q-12.25 0-20.625-8.627T657 695.623q0-12.123 8.662-20.373Q674.325 667 687 667h161q11.75 0 20.125 8.375T876.5 695.5v169q0 11.825-8.022 19.912-8.022 8.088-19.25 8.088-11.728 0-20.478-8.75Q820 875 820 863.5V782q-56 88.5-145.75 139.25T480.5 972Zm-.5-736q-97 0-178.75 51.5T173.5 425H276q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 21.075T276 483.5H113q-12.25 0-20.625-8.375T84 454.5v-167q0-11.75 8.425-20.125 8.426-8.375 20.75-8.375 11.825 0 20.075 8.375t8.25 20.125v80q56-88 145.25-138t193.25-50q126.116 0 227.308 70.75T850.5 436.5q4.5 11.5-1 22.823-5.5 11.322-17 15.177-11.5 4-21.75-2t-14.75-18Q759 356 672.935 296T480 236Zm1.5 437.5q-41.5 0-70.25-28.75T382.5 574.5q0-41 28.75-70t70.25-29q41 0 70 29t29 70q0 41.5-29 70.25t-70 28.75Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M480.5 972q-126.116 0-227.058-71.5T110.5 712q-5-11.939.5-22.719Q116.5 678.5 128.5 675q11-3 21.936 3.141 10.935 6.14 15.564 17.359 36.5 99 122.256 159 85.755 60 192.244 60 98 0 180.25-51T788 725.5H686q-12.25 0-20.625-8.627T657 695.623q0-12.123 8.662-20.373Q674.325 667 687 667h161q11.75 0 20.125 8.375T876.5 695.5v169q0 11.825-8.022 19.912-8.022 8.088-19.25 8.088-11.728 0-20.478-8.75Q820 875 820 863.5V782q-56 88.5-145.75 139.25T480.5 972Zm-.5-736q-97 0-178.75 51.5T173.5 425H276q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 21.075T276 483.5H113q-12.25 0-20.625-8.375T84 454.5v-167q0-11.75 8.425-20.125 8.426-8.375 20.75-8.375 11.825 0 20.075 8.375t8.25 20.125v80q56-88 145.25-138t193.25-50q126.116 0 227.308 70.75T850.5 436.5q4.5 11.5-1 22.823-5.5 11.322-17 15.177-11.5 4-21.75-2t-14.75-18Q759 356 672.935 296T480 236Zm1.5 437.5q-41.5 0-70.25-28.75T382.5 574.5q0-41 28.75-70t70.25-29q41 0 70 29t29 70q0 41.5-29 70.25t-70 28.75Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_closed.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_closed.svg new file mode 100644 index 0000000000..0f9889083d --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_closed.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M165-170q-30.94 0-52.97-22.03Q90-214.06 90-245v-470q0-30.94 22.03-52.97Q134.06-790 165-790h209q15.14 0 28.87 5.74Q416.59-778.52 427-768l53 53h315q30.94 0 52.97 22.03Q870-670.94 870-640v395q0 30.94-22.03 52.97Q825.94-170 795-170H165Zm0-75h630v-395H449l-75-75H165v470Zm0 0v-470 470Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_closed_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_closed_filled.svg new file mode 100644 index 0000000000..6291f7ab8e --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_closed_filled.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M165-170q-30.94 0-52.97-22.03Q90-214.06 90-245v-470q0-30.94 22.03-52.97Q134.06-790 165-790h209q15.14 0 28.87 5.74Q416.59-778.52 427-768l53 53h315q30.94 0 52.97 22.03Q870-670.94 870-640v395q0 30.94-22.03 52.97Q825.94-170 795-170H165Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_filled.svg new file mode 100644 index 0000000000..ab47ff4136 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_filled.svg @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + height="22" + viewBox="0 -960 960 960" + width="22" + xmlns="http://www.w3.org/2000/svg"> + <path + id="path151" + style="display:inline;fill:#ffffff;stroke-width:58" + d="M 480,-880 A 400,400 0 0 0 80,-480 400,400 0 0 0 480,-80 400,400 0 0 0 880,-480 400,400 0 0 0 480,-880 Z m -194.65461,218.29769 h 162.0477 l 33.18257,33.18257 h 194.07895 c 8.44741,0 16.07203,3.40719 22.90296,10.23849 6.83129,6.83091 10.2796,14.49667 10.2796,22.94408 v 263.85691 c 0,8.44742 -3.44831,16.07205 -10.2796,22.90295 -6.83093,6.83132 -14.45555,10.27962 -22.90296,10.27962 H 285.34539 c -8.83193,0 -16.59324,-3.4483 -23.23191,-10.27962 -6.63903,-6.8309 -9.95065,-14.45553 -9.95065,-22.90295 v -297.03948 c 0,-8.44742 3.31162,-16.07205 9.95065,-22.90295 6.63867,-6.83132 14.39998,-10.27962 23.23191,-10.27962 z" /> + <rect + style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:19.9532;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1" + id="rect151" + width="500" + height="30" + x="220" + y="-540" /> +</svg> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_open.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_open.svg index 6503f06c47..b57add4b26 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_open.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_open.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M142.5 892q-22.5 0-40-17.5T85 834.393V320.107Q85 297.5 102.5 280t40-17.5h257q11.978 0 22.833 4.739 10.855 4.739 18.579 12.754L481 320h336.5q22.737 0 39.869 17.5Q874.5 355 875 377.5H456L398.5 320h-256v510.5l89.408-352.685q4.573-18.719 20.781-30.767Q268.898 435 288.5 435H864q27.5 0 45.75 22t10.75 49.5L832 847q-6.5 23.5-22 34.25T769.896 892H142.5Zm59-57.5h574.367L861 492.5H286.633l-85.133 342Zm0 0 85-342-85 342Zm-59-399.5V320v115Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M142.5 892q-22.5 0-40-17.5T85 834.393V320.107Q85 297.5 102.5 280t40-17.5h257q11.978 0 22.833 4.739 10.855 4.739 18.579 12.754L481 320h336.5q22.737 0 39.869 17.5Q874.5 355 875 377.5H456L398.5 320h-256v510.5l89.408-352.685q4.573-18.719 20.781-30.767Q268.898 435 288.5 435H864q27.5 0 45.75 22t10.75 49.5L832 847q-6.5 23.5-22 34.25T769.896 892H142.5Zm59-57.5h574.367L861 492.5H286.633l-85.133 342Zm0 0 85-342-85 342Zm-59-399.5V320v115Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_pen.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_pen.svg new file mode 100644 index 0000000000..7f2fb6e38e --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_pen.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M142.5-222.5v-515 515Zm0 57.5q-22.969 0-40.234-17.266Q85-199.531 85-222.5v-515q0-22.969 17.266-40.234Q119.531-795 142.5-795h257q11.943 0 22.766 4.739 10.823 4.739 18.727 12.754L481-737.5h336.5q22.969 0 40.234 17.266Q875-702.969 875-680v193.5q-13.5-7.5-27.672-10.5t-29.828-3.5V-680H457l-57.5-57.5h-257v515h358-.5v57.5H142.5Zm415 51v-81q0-5.013 2-9.964 2-4.95 6.5-10.036l211.612-210.773q9.113-8.62 20.004-12.674 10.891-4.053 21.645-4.053 11.732 0 22.485 4.25Q852.5-434 861.5-425l37 37q8.765 8.855 12.632 19.677Q915-357.5 915-346.75t-4.382 22.031q-4.383 11.281-13.201 19.843L687.5-93.5q-5.086 4.5-9.949 6.5-4.864 2-10.051 2h-81q-12.25 0-20.625-8.375T557.5-114Zm299-233-37-37 37 37Zm-240 203h37.761L776.5-267l-17.887-19-18.88-18L616.5-182v38Zm142-142-19-18 37 37-18-19Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forum.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forum.svg new file mode 100644 index 0000000000..5271a02284 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forum.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M283-245.5q-14 0-27.25-13.75T242.5-287v-98H735v-333.5h100q14 0 26.75 14T874.5-676v517.5q0 19.5-17.75 26.5t-31.25-6.5l-107-107H283Zm-40.5-197-108 108Q121-321 103.25-328T85.5-354.5V-834q0-14 12.75-27.75T125-875.5h512q14.5 0 27.5 13.5t13 28v350q0 14-13 27.75T637-442.5H242.5ZM620-500v-318H143v318h477Zm-477 0v-318 318Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forward.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forward.svg new file mode 100644 index 0000000000..174b80bcc7 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forward.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path transform="scale(-1,1) translate(-960,0)" d="m236.5-495.5 142.5 143q8.5 8.5 8.25 20.25T378.5-312q-9 8.5-21 8.5t-20.5-9L145.5-504q-9-8.5-9-20t9-20.5L338-737q9-9 20.75-8.75T379.5-737q8.5 8.5 8.5 20.5t-8.5 20.5l-143 143h403q84 0 139.75 56T835-357.5V-234q0 12.5-8.25 20.75T806.5-205q-12.5 0-20.75-8.25T777.5-234v-123.5q0-60-39-99t-99-39h-403Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_github.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_github.svg index 70baacb727..996fbd9743 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_github.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_github.svg @@ -1 +1 @@ -<svg id="vector" xmlns="http://www.w3.org/2000/svg" width="23.25" height="24" viewBox="0 0 496 512"><path fill="#000000FF" d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" id="path_0"/></svg> \ No newline at end of file +<svg id="vector" xmlns="http://www.w3.org/2000/svg" width="21.25" height="22" viewBox="0 0 496 512"><path fill="#000000FF" d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" id="path_0"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_group.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_group.svg index 74d6cdcc02..88c24ca67a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_group.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_group.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M74 889.5q-12.725 0-20.612-7.888Q45.5 873.725 45.5 861v-62.545q0-34.312 17.523-62.236 17.524-27.924 49.499-41.73Q184.989 663 242.495 649 300 635 361 635q61 0 117.924 14 56.923 14 129.554 45.489 31.975 13.806 49.999 41.73 18.023 27.924 18.023 62.236V861q0 12.725-7.888 20.612Q660.725 889.5 648 889.5H74Zm638.5 0q10.5-1 17-9.154 6.5-8.155 6.5-21.846V799q0-62.5-32.011-102.355-32.01-39.854-83.489-64.145 68 8.5 127.75 23.75t97.25 34.25q32 18.5 50.75 46T915 798.798V861q0 12.725-8.069 20.612-8.069 7.888-20.431 7.888h-174ZM361 575.5q-65.237 0-106.119-40.881Q214 493.737 214 428.5t40.881-106.119Q295.763 281.5 361 281.5t106.119 40.881Q508 363.263 508 428.5t-40.881 106.119Q426.237 575.5 361 575.5Zm351-147.314q0 64.839-40.881 105.827Q630.237 575 565 575q-10.5 0-23.178-1.344t-23.322-5.21q24.018-24.264 36.259-59.867Q567 472.976 567 428.473q0-44.502-12.25-78.451Q542.5 316.073 518.5 288q10-2.812 23-4.656 13-1.844 23.5-1.844 65.237 0 106.119 41.026Q712 363.553 712 428.186ZM103 832h516v-33.372q0-16.318-9.75-31.223Q599.5 752.5 586 746q-72.5-32-120.425-42.75-47.924-10.75-104.325-10.75-56.569 0-105.41 10.75Q207 714 135 746q-14 6.5-23 21.5t-9 31V832Zm257.938-314q39.062 0 64.312-25.188t25.25-64.25q0-39.062-25.188-64.312T361.062 339Q322 339 296.75 364.188t-25.25 64.25q0 39.062 25.188 64.312t64.25 25.25Zm.062-89.5Zm0 264Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M74 889.5q-12.725 0-20.612-7.888Q45.5 873.725 45.5 861v-62.545q0-34.312 17.523-62.236 17.524-27.924 49.499-41.73Q184.989 663 242.495 649 300 635 361 635q61 0 117.924 14 56.923 14 129.554 45.489 31.975 13.806 49.999 41.73 18.023 27.924 18.023 62.236V861q0 12.725-7.888 20.612Q660.725 889.5 648 889.5H74Zm638.5 0q10.5-1 17-9.154 6.5-8.155 6.5-21.846V799q0-62.5-32.011-102.355-32.01-39.854-83.489-64.145 68 8.5 127.75 23.75t97.25 34.25q32 18.5 50.75 46T915 798.798V861q0 12.725-8.069 20.612-8.069 7.888-20.431 7.888h-174ZM361 575.5q-65.237 0-106.119-40.881Q214 493.737 214 428.5t40.881-106.119Q295.763 281.5 361 281.5t106.119 40.881Q508 363.263 508 428.5t-40.881 106.119Q426.237 575.5 361 575.5Zm351-147.314q0 64.839-40.881 105.827Q630.237 575 565 575q-10.5 0-23.178-1.344t-23.322-5.21q24.018-24.264 36.259-59.867Q567 472.976 567 428.473q0-44.502-12.25-78.451Q542.5 316.073 518.5 288q10-2.812 23-4.656 13-1.844 23.5-1.844 65.237 0 106.119 41.026Q712 363.553 712 428.186ZM103 832h516v-33.372q0-16.318-9.75-31.223Q599.5 752.5 586 746q-72.5-32-120.425-42.75-47.924-10.75-104.325-10.75-56.569 0-105.41 10.75Q207 714 135 746q-14 6.5-23 21.5t-9 31V832Zm257.938-314q39.062 0 64.312-25.188t25.25-64.25q0-39.062-25.188-64.312T361.062 339Q322 339 296.75 364.188t-25.25 64.25q0 39.062 25.188 64.312t64.25 25.25Zm.062-89.5Zm0 264Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_group_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_group_filled.svg new file mode 100644 index 0000000000..f30bc0db2c --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_group_filled.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M54.5-279q0-33 16.75-60.25T116.5-381q61-30 123.5-45.25t126-15.25q63.5 0 125.75 15.25T615-381q28.5 14.5 45.25 41.75T677-279v31q0 31-22 53t-53 22H129.5q-31 0-53-22t-22-53v-31Zm677 106q10-17 15.25-36t5.25-39v-35q0-43.5-22.5-83.75T663-434.5q48.5 6 91.25 19.75t80.25 34.25Q869-362 887.25-338t18.25 52v38q0 31-22 53t-53 22h-99ZM366-479q-64 0-109-45t-45-109q0-64 45-109t109-45q64 0 109 45t45 109q0 64-45 109t-109 45Zm382-154.5q0 63.5-45 108.75T594-479.5q-9.5 0-25.25-2.25T543-487q26.5-30.5 40.75-68T598-633.5q0-40.5-14.25-78.25T543-780q12.5-4.5 25.5-5.75T594-787q64 0 109 45t45 108.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_group_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_group_off.svg new file mode 100644 index 0000000000..710213ff55 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_group_off.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22" fill="#000000"><path d="m811.5-67-192-191h57v33q0 24.39-17.05 41.45Q642.39-166.5 618-166.5H103q-24.44 0-40.97-16.77Q45.5-200.03 45.5-224v-33.6q0-34.57 17.46-62.33 17.46-27.75 49.32-41.53Q185-393 242.5-407t118.27-14q28.11 0 54.48 3 26.37 3 52.75 8l-74-73.5q-8.17 2-16.3 2.5t-16.73.5Q296-480.5 255-521.38T214-627.5q0-8.37.5-16.65t2.5-16.35l-164.5-165q-9-8.5-9-20.5t8.75-20.5Q61-875 73-875t20.6 8.59L853-107.5q8.5 8.67 8.5 20.23T853-66.85Q843.97-58 831.98-58 820-58 811.5-67Zm-189-355.5q68 8.5 126.8 22.9 58.79 14.39 96.2 34.1 33 17.5 51.25 45.5T915-257.12v90.62h-39L726-317q-13-36-39.75-62t-63.75-43.5Zm-261.37 59q-56.37 0-105.25 10.75T135-310q-14 6.5-23 21.5t-9 31v33.5h516v-34.5l-64.5-65q-58-23.5-100.75-31.75t-92.62-8.25ZM535-508q15.5-22.5 23.75-53.18 8.25-30.67 8.25-66.88 0-44.28-12.25-78.11T518.5-768q10-2.81 23-4.66 13-1.84 23.5-1.84 65.24 0 106.12 41.03Q712-692.45 712-627.81q0 64.84-42.6 105.82Q626.8-481 562-481l-27-27Zm-45-45-43.5-43.5q2-7 3-14.75t1-16.25q0-39.09-25.21-64.29Q400.09-717 361-717q-8.5 0-16.25 1T330-713l-43.5-43.5q15.5-8.75 34.5-13.38 19-4.62 40-4.62 65.24 0 106.12 40.88Q508-692.74 508-627.5q0 21-4.62 40-4.63 19-13.38 34.5ZM367-224Zm21.5-431Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_headphones.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_headphones.svg new file mode 100644 index 0000000000..8487893d8d --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_headphones.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M327-124.5H182q-22.969 0-40.234-17.266Q124.5-159.031 124.5-182v-298q0-74.09 28-138.795 28-64.705 76-112.705t112.684-76q64.684-28 138.75-28t138.816 28q64.75 28 112.75 76t76 112.705q28 64.705 28 138.795v298q0 22.969-17.266 40.234Q800.969-124.5 778-124.5H633v-290h145V-480q0-124.193-86.937-211.096Q604.125-778 479.884-778t-211.063 86.904Q182-604.193 182-480v65.5h145v290ZM269.5-357H182v175h87.5v-175Zm421 0v175H778v-175h-87.5Zm-421 0H182h87.5Zm421 0H778h-87.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_help.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_help.svg index 3bf2406ab8..b654c8eee5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_help.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_help.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M484.437 805.5q15.303 0 25.933-10.567Q521 784.367 521 769.063q0-15.303-10.567-25.933-10.566-10.63-25.87-10.63-15.303 0-25.933 10.567Q448 753.633 448 768.937q0 15.303 10.567 25.933 10.566 10.63 25.87 10.63ZM480.33 971q-81.704 0-153.629-31.263t-125.563-85Q147.5 801 116.25 729.136 85 657.272 85 575.564q0-81.789 31.263-153.789 31.263-71.999 85-125.387Q255 243 326.864 212q71.864-31 153.572-31 81.789 0 153.795 31.132 72.005 31.131 125.387 84.5Q813 350 844 422.023q31 72.023 31 153.647 0 81.705-31.013 153.629-31.013 71.925-84.5 125.379-53.487 53.454-125.51 84.888Q561.954 971 480.33 971Zm.137-57.5Q620.5 913.5 719 814.533t98.5-239Q817.5 435.5 719.221 337q-98.28-98.5-239.188-98.5-139.533 0-238.533 98.279-99 98.28-99 239.188 0 139.533 98.967 238.533t239 99ZM480 576Zm2.767-178.5q30.435 0 53.584 18.5 23.149 18.5 23.149 47.196 0 25.911-15.398 45.536-15.397 19.625-34.602 35.768-23 19.5-40.5 42.265t-17 52.159q0 10.576 7.875 16.826t18.743 6.25q11.645 0 19.359-7.696 7.714-7.697 9.918-19.242 3.043-21.173 15.983-37.868 12.94-16.694 29.853-30.478Q578.5 546.5 594 520t15.5-57.614q0-49.727-36.402-82.806-36.402-33.08-87.182-33.08-34.969 0-67.442 14.75Q386 376 365 404.5q-7 9.5-6.578 20.757.422 11.258 9.047 17.743 10.916 7.5 22.896 4.5 11.981-3 19.635-13.5 12.25-17.661 31.537-27.081 19.287-9.419 41.23-9.419Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M484.437 805.5q15.303 0 25.933-10.567Q521 784.367 521 769.063q0-15.303-10.567-25.933-10.566-10.63-25.87-10.63-15.303 0-25.933 10.567Q448 753.633 448 768.937q0 15.303 10.567 25.933 10.566 10.63 25.87 10.63ZM480.33 971q-81.704 0-153.629-31.263t-125.563-85Q147.5 801 116.25 729.136 85 657.272 85 575.564q0-81.789 31.263-153.789 31.263-71.999 85-125.387Q255 243 326.864 212q71.864-31 153.572-31 81.789 0 153.795 31.132 72.005 31.131 125.387 84.5Q813 350 844 422.023q31 72.023 31 153.647 0 81.705-31.013 153.629-31.013 71.925-84.5 125.379-53.487 53.454-125.51 84.888Q561.954 971 480.33 971Zm.137-57.5Q620.5 913.5 719 814.533t98.5-239Q817.5 435.5 719.221 337q-98.28-98.5-239.188-98.5-139.533 0-238.533 98.279-99 98.28-99 239.188 0 139.533 98.967 238.533t239 99ZM480 576Zm2.767-178.5q30.435 0 53.584 18.5 23.149 18.5 23.149 47.196 0 25.911-15.398 45.536-15.397 19.625-34.602 35.768-23 19.5-40.5 42.265t-17 52.159q0 10.576 7.875 16.826t18.743 6.25q11.645 0 19.359-7.696 7.714-7.697 9.918-19.242 3.043-21.173 15.983-37.868 12.94-16.694 29.853-30.478Q578.5 546.5 594 520t15.5-57.614q0-49.727-36.402-82.806-36.402-33.08-87.182-33.08-34.969 0-67.442 14.75Q386 376 365 404.5q-7 9.5-6.578 20.757.422 11.258 9.047 17.743 10.916 7.5 22.896 4.5 11.981-3 19.635-13.5 12.25-17.661 31.537-27.081 19.287-9.419 41.23-9.419Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_help_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_help_filled.svg new file mode 100644 index 0000000000..ba3d3a393a --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_help_filled.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M484.41-250.5q15.33 0 25.96-10.54T521-286.91q0-15.33-10.54-25.96t-25.87-10.63q-15.33 0-25.96 10.54T448-287.09q0 15.33 10.54 25.96t25.87 10.63ZM450-394h57q0-25.5 6.75-46.75t39.75-48.75q31-25.5 43.5-50.25T609.5-594q0-52.28-33.49-83.89-33.48-31.61-89.81-31.61-48.32 0-85.18 23.84-36.86 23.84-54.02 66.16l51.61 19q10.89-28 32.89-43 22.01-15 51.5-15 34 0 55 18.5t21 47.5q0 22-12.96 41.2-12.96 19.19-37.77 40.36Q479-486 464.5-459.93 450-433.85 450-394Zm30.06 309q-80.97 0-153.13-31.26-72.15-31.27-125.79-85Q147.5-255 116.25-327.02 85-399.05 85-479.94q0-81.97 31.26-154.13 31.27-72.15 85-125.54Q255-813 327.02-844q72.03-31 152.92-31 81.97 0 154.13 31.13 72.17 31.13 125.55 84.5Q813-706 844-633.98q31 72.03 31 153.92 0 80.97-31.01 153.13-31.02 72.15-84.5 125.79Q706-147.5 633.98-116.25 561.95-85 480.06-85Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_history.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_history.svg index f98f4db386..6edaf1512c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_history.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_history.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M476.898-124.5q-140.398 0-240.648-94.75T125-452.332q-1-11.668 7.283-20.634 8.283-8.965 20.764-9Q164.5-482 172.75-473.5t9.836 20.575Q193.5-338 276.664-260q83.165 78 200.19 78 125.646 0 213.396-88.513Q778-359.026 778-484.763 778-608 689.313-693q-88.686-85-212.813-85-67.5 0-126 30.5t-102 80.5H323q12.5 0 20.75 8.287Q352-650.425 352-638.5q0 12.5-8.25 20.75T323-609.5H177q-12.5 0-20.5-8.25t-8-20.75v-145.2q0-11.84 8-20.07T177-812q12.5 0 20.75 8.287Q206-795.425 206-783.5v77q51-60 121.194-94.5 70.195-34.5 149.306-34.5 74 0 139.325 27.528 65.324 27.527 114.25 75.5Q779-684.5 807.25-620.318q28.25 64.183 28.25 138.25 0 74.068-28.25 139.318Q779-277.5 730.25-229t-113.986 76.5q-65.237 28-139.366 28ZM510.5-493.653l113.95 112.088q9.05 9.065 9.05 21.065 0 12-9 20.75T604.25-331q-11.25 0-20.335-9.043L461.547-460.977Q457-465.5 455-470.739q-2-5.239-2-11.261v-170q0-11.925 8-20.213 8-8.287 20.5-8.287t20.75 8.287q8.25 8.288 8.25 20.213v158.347Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M476.898-124.5q-140.398 0-240.648-94.75T125-452.332q-1-11.668 7.283-20.634 8.283-8.965 20.764-9Q164.5-482 172.75-473.5t9.836 20.575Q193.5-338 276.664-260q83.165 78 200.19 78 125.646 0 213.396-88.513Q778-359.026 778-484.763 778-608 689.313-693q-88.686-85-212.813-85-67.5 0-126 30.5t-102 80.5H323q12.5 0 20.75 8.287Q352-650.425 352-638.5q0 12.5-8.25 20.75T323-609.5H177q-12.5 0-20.5-8.25t-8-20.75v-145.2q0-11.84 8-20.07T177-812q12.5 0 20.75 8.287Q206-795.425 206-783.5v77q51-60 121.194-94.5 70.195-34.5 149.306-34.5 74 0 139.325 27.528 65.324 27.527 114.25 75.5Q779-684.5 807.25-620.318q28.25 64.183 28.25 138.25 0 74.068-28.25 139.318Q779-277.5 730.25-229t-113.986 76.5q-65.237 28-139.366 28ZM510.5-493.653l113.95 112.088q9.05 9.065 9.05 21.065 0 12-9 20.75T604.25-331q-11.25 0-20.335-9.043L461.547-460.977Q457-465.5 455-470.739q-2-5.239-2-11.261v-170q0-11.925 8-20.213 8-8.287 20.5-8.287t20.75 8.287q8.25 8.288 8.25 20.213v158.347Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_id_card.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_id_card.svg new file mode 100644 index 0000000000..e2539c3608 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_id_card.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22" fill="#000000"> + <path + d="M730-455q12.25 0 20.63-8.43 8.37-8.42 8.37-20.5 0-12.07-8.37-20.32-8.38-8.25-20.63-8.25H605q-12.25 0-20.62 8.46-8.38 8.47-8.38 20.22 0 12.32 8.38 20.57Q592.75-455 605-455h125Zm0-116.5q12.25 0 20.63-8.18 8.37-8.17 8.37-20.5 0-12.32-8.37-20.57Q742.25-629 730-629H605q-12.25 0-20.62 8.43-8.38 8.42-8.38 20.75 0 12.32 8.38 20.32 8.37 8 20.62 8h125Zm-370.25 136q-38.63 0-66.58 6.5-27.94 6.5-49.17 19.5-20 11-31 28.61T202-343q0 9.5 6.39 15.5 6.38 6 15.61 6h270.5q9.23 0 15.61-6.23 6.39-6.24 6.39-15.77 0-18.69-10.75-36.1Q495-397 476-409.5q-21.5-13.5-50-19.75t-66.25-6.25Zm-.58-60q29.59 0 50.71-20.96Q431-537.41 431-566.84q0-29.42-20.98-50.54-20.98-21.12-50.44-21.12-29.84 0-50.71 20.96Q288-596.59 288-567.16q0 29.42 20.79 50.54 20.79 21.12 50.38 21.12Zm-216.67 331q-22.97 0-40.23-17.27Q85-199.03 85-222v-516q0-22.97 17.27-40.23 17.26-17.27 40.23-17.27h675q22.97 0 40.23 17.27Q875-760.97 875-738v516q0 22.97-17.27 40.23-17.26 17.27-40.23 17.27h-675Zm0-57.5h675v-516h-675v516Zm0 0v-516 516Z" /> +</svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_image.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_image.svg index 408668d204..aa42e1a978 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_image.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_image.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M269 777.5h423.175q9.825 0 13.825-7.75T704 754L588.578 600.603q-4.703-6.103-11.463-6.103-6.759 0-11.615 6L446 754.5l-80.462-109.388q-4.692-5.612-11.5-5.612-6.807 0-11.719 5.584L258.574 754.02q-5.074 7.98-1.699 15.73T269 777.5Zm-87 154q-22.969 0-40.234-17.266Q124.5 896.969 124.5 874V278q0-22.969 17.266-40.234Q159.031 220.5 182 220.5h596q22.969 0 40.234 17.266Q835.5 255.031 835.5 278v596q0 22.969-17.266 40.234Q800.969 931.5 778 931.5H182Zm0-57.5h596V278H182v596Zm0-596v596-596Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M269 777.5h423.175q9.825 0 13.825-7.75T704 754L588.578 600.603q-4.703-6.103-11.463-6.103-6.759 0-11.615 6L446 754.5l-80.462-109.388q-4.692-5.612-11.5-5.612-6.807 0-11.719 5.584L258.574 754.02q-5.074 7.98-1.699 15.73T269 777.5Zm-87 154q-22.969 0-40.234-17.266Q124.5 896.969 124.5 874V278q0-22.969 17.266-40.234Q159.031 220.5 182 220.5h596q22.969 0 40.234 17.266Q835.5 255.031 835.5 278v596q0 22.969-17.266 40.234Q800.969 931.5 778 931.5H182Zm0-57.5h596V278H182v596Zm0-596v596-596Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_info.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_info.svg index aa3e5a2f4e..b854b30359 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_info.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_info.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M483.425 773.5q12.075 0 20.325-8.375T512 744.5V565q0-11.675-8.463-20.088-8.463-8.412-20.212-8.412-12.325 0-20.575 8.412-8.25 8.413-8.25 20.088v179.5q0 12.25 8.425 20.625 8.426 8.375 20.5 8.375ZM479.93 464q13.57 0 22.82-8.882 9.25-8.883 9.25-22.013 0-14.555-9.165-23.83Q493.671 400 480.123 400 466 400 457 409.17q-9 9.169-9 23.475 0 13.455 9.18 22.405 9.18 8.95 22.75 8.95Zm.4 507q-81.704 0-153.629-31.263t-125.563-85Q147.5 801 116.25 729.136 85 657.272 85 575.564q0-81.789 31.263-153.789 31.263-71.999 85-125.387Q255 243 326.864 212q71.864-31 153.572-31 81.789 0 153.795 31.132 72.005 31.131 125.387 84.5Q813 350 844 422.023q31 72.023 31 153.647 0 81.705-31.013 153.629-31.013 71.925-84.5 125.379-53.487 53.454-125.51 84.888Q561.954 971 480.33 971Zm.137-57.5Q620.5 913.5 719 814.533t98.5-239Q817.5 435.5 719.221 337q-98.28-98.5-239.188-98.5-139.533 0-238.533 98.279-99 98.28-99 239.188 0 139.533 98.967 238.533t239 99ZM480 576Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M483.425 773.5q12.075 0 20.325-8.375T512 744.5V565q0-11.675-8.463-20.088-8.463-8.412-20.212-8.412-12.325 0-20.575 8.412-8.25 8.413-8.25 20.088v179.5q0 12.25 8.425 20.625 8.426 8.375 20.5 8.375ZM479.93 464q13.57 0 22.82-8.882 9.25-8.883 9.25-22.013 0-14.555-9.165-23.83Q493.671 400 480.123 400 466 400 457 409.17q-9 9.169-9 23.475 0 13.455 9.18 22.405 9.18 8.95 22.75 8.95Zm.4 507q-81.704 0-153.629-31.263t-125.563-85Q147.5 801 116.25 729.136 85 657.272 85 575.564q0-81.789 31.263-153.789 31.263-71.999 85-125.387Q255 243 326.864 212q71.864-31 153.572-31 81.789 0 153.795 31.132 72.005 31.131 125.387 84.5Q813 350 844 422.023q31 72.023 31 153.647 0 81.705-31.013 153.629-31.013 71.925-84.5 125.379-53.487 53.454-125.51 84.888Q561.954 971 480.33 971Zm.137-57.5Q620.5 913.5 719 814.533t98.5-239Q817.5 435.5 719.221 337q-98.28-98.5-239.188-98.5-139.533 0-238.533 98.279-99 98.28-99 239.188 0 139.533 98.967 238.533t239 99ZM480 576Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_inventory_2.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_inventory_2.svg index c7e8aa21c1..20c39b2ed2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_inventory_2.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_inventory_2.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M182 971.5q-22.969 0-40.234-18.766Q124.5 933.969 124.5 912V424q-14-1.5-26.75-19.25T85 366.5v-126q0-21.969 17.266-39.734Q119.531 183 142.5 183h675q21.969 0 39.734 17.766Q875 218.531 875 240.5v126q0 20.5-12.75 38.25T835.5 424v488q0 21.969-17.766 40.734Q799.969 971.5 778 971.5H182Zm0-547.5v490h596V424H182Zm635.5-57.5v-126h-675v126h675Zm-427 265H570q11.675 0 20.088-8.425 8.412-8.426 8.412-20.5 0-12.075-8.412-20.325Q581.675 574 570 574H390.5q-12.25 0-20.625 8.463t-8.375 20.212q0 12.325 8.375 20.575t20.625 8.25ZM182 914V424v490Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M182 971.5q-22.969 0-40.234-18.766Q124.5 933.969 124.5 912V424q-14-1.5-26.75-19.25T85 366.5v-126q0-21.969 17.266-39.734Q119.531 183 142.5 183h675q21.969 0 39.734 17.766Q875 218.531 875 240.5v126q0 20.5-12.75 38.25T835.5 424v488q0 21.969-17.766 40.734Q799.969 971.5 778 971.5H182Zm0-547.5v490h596V424H182Zm635.5-57.5v-126h-675v126h675Zm-427 265H570q11.675 0 20.088-8.425 8.412-8.426 8.412-20.5 0-12.075-8.412-20.325Q581.675 574 570 574H390.5q-12.25 0-20.625 8.463t-8.375 20.212q0 12.325 8.375 20.575t20.625 8.25ZM182 914V424v490Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_ios_share.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_ios_share.svg index 099b5149bf..33f52db54b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_ios_share.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_ios_share.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M478.825 705q-12.325 0-20.325-8.125t-8-20.375V255L382 323.5q-8.25 8-20.125 7.75t-20.266-8.553Q333 313.895 333 301.947q0-11.947 8.908-20.855l117.134-117.134Q464 159 468.928 157q4.928-2 10.185-2 5.257 0 10.072 2 4.815 2 9.792 6.977L617.5 282.5q7.5 7.609 8 19.554.5 11.946-7.803 20.446-8.802 9-20.75 9-11.947 0-20.947-9L508 255v421.5q0 12.25-8.425 20.375-8.426 8.125-20.75 8.125ZM222 1009q-22.969 0-40.234-17.266Q164.5 974.469 164.5 951.5V446q0-22.969 17.266-40.234Q199.031 388.5 222 388.5h142q12.25 0 20.625 8.463T393 417.175q0 12.325-8.375 20.575T364 446H222v505.5h516.5V446H594q-12.25 0-20.375-8.535t-8.125-20.39q0-11.856 8.125-20.216Q581.75 388.5 594 388.5h144.5q22.438 0 39.969 17.266Q796 423.031 796 446v505.5q0 22.969-17.531 40.234Q760.938 1009 738.711 1009H222Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M478.825 705q-12.325 0-20.325-8.125t-8-20.375V255L382 323.5q-8.25 8-20.125 7.75t-20.266-8.553Q333 313.895 333 301.947q0-11.947 8.908-20.855l117.134-117.134Q464 159 468.928 157q4.928-2 10.185-2 5.257 0 10.072 2 4.815 2 9.792 6.977L617.5 282.5q7.5 7.609 8 19.554.5 11.946-7.803 20.446-8.802 9-20.75 9-11.947 0-20.947-9L508 255v421.5q0 12.25-8.425 20.375-8.426 8.125-20.75 8.125ZM222 1009q-22.969 0-40.234-17.266Q164.5 974.469 164.5 951.5V446q0-22.969 17.266-40.234Q199.031 388.5 222 388.5h142q12.25 0 20.625 8.463T393 417.175q0 12.325-8.375 20.575T364 446H222v505.5h516.5V446H594q-12.25 0-20.375-8.535t-8.125-20.39q0-11.856 8.125-20.216Q581.75 388.5 594 388.5h144.5q22.438 0 39.969 17.266Q796 423.031 796 446v505.5q0 22.969-17.531 40.234Q760.938 1009 738.711 1009H222Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard.svg index 35689927b1..a39e9c7e0b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M139 796h682.5V353.5H139V796Zm0 57.5q-22.969 0-40.234-17.766Q81.5 817.969 81.5 796V353.5q0-22.969 17.266-40.234Q116.03 296 139 296h682.5q22.969 0 40.234 17.266Q879 330.531 879 353.5V796q0 21.969-17.266 39.734Q844.469 853.5 821.5 853.5H139Zm312.5-375H509V421h-57.5v57.5Zm0 125H509V546h-57.5v57.5Zm-123-125H386V421h-57.5v57.5Zm0 125H386V546h-57.5v57.5Zm-124 0H262V546h-57.5v57.5Zm0-125H262V421h-57.5v57.5Zm125 250H631q11.675 0 20.088-8.425 8.412-8.426 8.412-20.5 0-12.075-8.412-20.325Q642.675 671 631 671H329.5q-12.25 0-20.625 8.463t-8.375 20.212q0 12.325 8.375 20.575t20.625 8.25Zm246-125H633V546h-57.5v57.5Zm0-125H633V421h-57.5v57.5Zm123 125H756V546h-57.5v57.5Zm0-125H756V421h-57.5v57.5ZM139 796V353.5 796Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M139 796h682.5V353.5H139V796Zm0 57.5q-22.969 0-40.234-17.766Q81.5 817.969 81.5 796V353.5q0-22.969 17.266-40.234Q116.03 296 139 296h682.5q22.969 0 40.234 17.266Q879 330.531 879 353.5V796q0 21.969-17.266 39.734Q844.469 853.5 821.5 853.5H139Zm312.5-375H509V421h-57.5v57.5Zm0 125H509V546h-57.5v57.5Zm-123-125H386V421h-57.5v57.5Zm0 125H386V546h-57.5v57.5Zm-124 0H262V546h-57.5v57.5Zm0-125H262V421h-57.5v57.5Zm125 250H631q11.675 0 20.088-8.425 8.412-8.426 8.412-20.5 0-12.075-8.412-20.325Q642.675 671 631 671H329.5q-12.25 0-20.625 8.463t-8.375 20.212q0 12.325 8.375 20.575t20.625 8.25Zm246-125H633V546h-57.5v57.5Zm0-125H633V421h-57.5v57.5Zm123 125H756V546h-57.5v57.5Zm0-125H756V421h-57.5v57.5ZM139 796V353.5 796Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_arrow_down.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_arrow_down.svg index c92d244c17..aaa188bed7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_arrow_down.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_arrow_down.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M479.967 698q-4.8 0-9.884-2-5.083-2-10.017-6.434L264.5 493.5q-9-8.267-9-20.883 0-12.617 8.75-21.117t20.25-8.5q11.5 0 20.5 8.5L480 627l175.467-175.467q8.3-8.533 20.422-8.533T697 451.5q8.5 8.5 8.5 20.617 0 12.116-8.582 20.465L500.045 689.455q-4.628 4.772-9.453 6.659Q485.767 698 479.967 698Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M479.967 698q-4.8 0-9.884-2-5.083-2-10.017-6.434L264.5 493.5q-9-8.267-9-20.883 0-12.617 8.75-21.117t20.25-8.5q11.5 0 20.5 8.5L480 627l175.467-175.467q8.3-8.533 20.422-8.533T697 451.5q8.5 8.5 8.5 20.617 0 12.116-8.582 20.465L500.045 689.455q-4.628 4.772-9.453 6.659Q485.767 698 479.967 698Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_voice.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_voice.svg index a938a4250f..769198e13c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_voice.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_voice.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M480 630.5q-41.75 0-69.875-30.167Q382 570.167 382 527V278q0-40.417 28.566-68.708Q439.132 181 479.941 181t69.434 28.292Q578 237.583 578 278v249q0 43.167-28.125 73.333Q521.75 630.5 480 630.5Zm0-224.5Zm-.175 526q-12.325 0-20.325-8.375t-8-20.625V795.865Q354 786 285.25 719T206 557.5q-1.5-12.593 7.295-21.547Q222.091 527 235.5 527q9.917 0 18.148 7.542 8.232 7.541 9.852 18.458 10.5 80.5 72.044 134 61.543 53.5 144.347 53.5 82.805 0 144.457-53.5Q686 633.5 696.5 553q1.853-11.167 10.121-18.583Q714.89 527 725.543 527q12.91 0 21.434 8.953Q755.5 544.907 754 557.5 743.5 652 674.75 719T509 795.865V903q0 12.25-8.425 20.625-8.426 8.375-20.75 8.375ZM480 573q18.075 0 29.288-13.5Q520.5 546 520.5 527V278.335q0-16.835-11.629-28.335-11.628-11.5-28.818-11.5t-28.872 11.356Q439.5 261.212 439.5 278v248.868q0 19.132 11.212 32.632Q461.925 573 480 573Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M480 630.5q-41.75 0-69.875-30.167Q382 570.167 382 527V278q0-40.417 28.566-68.708Q439.132 181 479.941 181t69.434 28.292Q578 237.583 578 278v249q0 43.167-28.125 73.333Q521.75 630.5 480 630.5Zm0-224.5Zm-.175 526q-12.325 0-20.325-8.375t-8-20.625V795.865Q354 786 285.25 719T206 557.5q-1.5-12.593 7.295-21.547Q222.091 527 235.5 527q9.917 0 18.148 7.542 8.232 7.541 9.852 18.458 10.5 80.5 72.044 134 61.543 53.5 144.347 53.5 82.805 0 144.457-53.5Q686 633.5 696.5 553q1.853-11.167 10.121-18.583Q714.89 527 725.543 527q12.91 0 21.434 8.953Q755.5 544.907 754 557.5 743.5 652 674.75 719T509 795.865V903q0 12.25-8.425 20.625-8.426 8.375-20.75 8.375ZM480 573q18.075 0 29.288-13.5Q520.5 546 520.5 527V278.335q0-16.835-11.629-28.335-11.628-11.5-28.818-11.5t-28.872 11.356Q439.5 261.212 439.5 278v248.868q0 19.132 11.212 32.632Q461.925 573 480 573Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_voice_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_voice_filled.svg index bd28745400..44c0f89fa0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_voice_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_voice_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M480 630.5q-41.75 0-69.875-30.167Q382 570.167 382 527V278q0-40.417 28.566-68.708Q439.132 181 479.941 181t69.434 28.292Q578 237.583 578 278v249q0 43.167-28.125 73.333Q521.75 630.5 480 630.5Zm-.175 301.5q-12.325 0-20.325-8.375t-8-20.625V795.865Q354 786 285.25 719T206 557.5q-1.5-12.593 7.25-21.547Q222 527 235.5 527q10 0 18.19 7.542 8.19 7.541 9.81 18.458 10.5 80.5 71.391 134 60.892 53.5 145 53.5Q564 740.5 625 687q61-53.5 71.5-134 1.853-11.167 10.121-18.583Q714.89 527 725.543 527q12.957 0 21.457 8.953 8.5 8.954 7 21.547Q743.5 652 674.75 719T509 795.865V903q0 12.25-8.425 20.625-8.426 8.375-20.75 8.375Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M480 630.5q-41.75 0-69.875-30.167Q382 570.167 382 527V278q0-40.417 28.566-68.708Q439.132 181 479.941 181t69.434 28.292Q578 237.583 578 278v249q0 43.167-28.125 73.333Q521.75 630.5 480 630.5Zm-.175 301.5q-12.325 0-20.325-8.375t-8-20.625V795.865Q354 786 285.25 719T206 557.5q-1.5-12.593 7.25-21.547Q222 527 235.5 527q10 0 18.19 7.542 8.19 7.541 9.81 18.458 10.5 80.5 71.391 134 60.892 53.5 145 53.5Q564 740.5 625 687q61-53.5 71.5-134 1.853-11.167 10.121-18.583Q714.89 527 725.543 527q12.957 0 21.457 8.953 8.5 8.954 7 21.547Q743.5 652 674.75 719T509 795.865V903q0 12.25-8.425 20.625-8.426 8.375-20.75 8.375Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_label.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_label.svg new file mode 100644 index 0000000000..02c84c9d05 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_label.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="m859-406.5-304 305Q545.93-93 535.21-89q-10.71 4-21.71 4t-21.25-4.25Q482-93.5 473.5-101.5L102.5-474q-8-7.5-12.75-17.97Q85-502.44 85-514v-303.5q0-23.72 16.89-40.61T142.5-875h305q11.41 0 22.11 4.4 10.71 4.39 18.89 12.6L859-488q8.91 8.92 13.21 19.52 4.29 10.6 4.29 21.21 0 11.27-4.5 22.27t-13 18.5Zm-343 266L820-446 447.49-817.5H142.5v301.77L516-140.5ZM246.75-664q20.5 0 35.63-15.04 15.12-15.03 15.12-35.37 0-20.34-15.06-35.47Q267.38-765 247-765q-20.75 0-35.62 15.04-14.88 15.03-14.88 35.37 0 20.34 14.88 35.46Q226.25-664 246.75-664ZM481.5-479Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_label_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_label_filled.svg new file mode 100644 index 0000000000..3b58600ae2 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_label_filled.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="m859-406.5-304 305Q545.93-93 535.21-89q-10.71 4-21.71 4t-21.25-4.25Q482-93.5 473.5-101.5L102.5-474q-8-7.5-12.75-17.97Q85-502.44 85-514v-303.5q0-23.72 16.89-40.61T142.5-875h305q11.41 0 22.11 4.4 10.71 4.39 18.89 12.6L859-488q8.91 8.92 13.21 19.52 4.29 10.6 4.29 21.21 0 11.27-4.5 22.27t-13 18.5ZM246.75-664q20.5 0 35.63-15.04 15.12-15.03 15.12-35.37 0-20.34-15.06-35.47Q267.38-765 247-765q-20.75 0-35.62 15.04-14.88 15.03-14.88 35.37 0 20.34 14.88 35.46Q226.25-664 246.75-664Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_light_mode.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_light_mode.svg index 858ee788cf..44bad28fc9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_light_mode.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_light_mode.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M479.853 717Q538.5 717 580 675.897q41.5-41.103 41.5-99.75T580.294 476q-41.206-41.5-100-41.5T380.25 475.706q-41.25 41.206-41.25 100t41.103 100.044Q421.206 717 479.853 717Zm.234 57.5q-82.587 0-140.587-58.06-58-58.06-58-140.5t58.06-140.69Q397.62 377 480.06 377t140.69 58.163Q679 493.327 679 575.913q0 82.587-58.163 140.587-58.164 58-140.75 58ZM70 604.5q-12.25 0-20.375-8.175-8.125-8.176-8.125-20.5 0-12.325 8.125-20.575T70 547h100q12.25 0 20.625 8.425 8.375 8.426 8.375 20.75 0 12.325-8.375 20.325t-20.625 8H70Zm720 0q-12.25 0-20.375-8.175-8.125-8.176-8.125-20.5 0-12.325 8.125-20.575T790 547h100q12.25 0 20.625 8.425 8.375 8.426 8.375 20.75 0 12.325-8.375 20.325t-20.625 8H790Zm-310.175-310q-12.325 0-20.325-8.125t-8-20.375V166q0-12.25 8.175-20.625 8.176-8.375 20.5-8.375 12.325 0 20.575 8.375T509 166v100q0 12.25-8.425 20.375-8.426 8.125-20.75 8.125Zm0 720q-12.325 0-20.325-8.12-8-8.13-8-20.38V886q0-12.25 8.175-20.625 8.176-8.375 20.5-8.375 12.325 0 20.575 8.375T509 886v100q0 12.25-8.425 20.38-8.426 8.12-20.75 8.12ZM241 377l-57-56q-9-8.5-8.629-20.603.37-12.104 8.765-20.5 8.157-8.397 20.261-8.647Q216.5 271 225 279.5l56 57q8 8.853 8 20.324 0 11.47-8 19.823T261.25 385q-11.75 0-20.25-8Zm494 495-56-57q-8-8.5-8-20.375t8.5-20.125q8-8.5 19.483-8.25 11.484.25 20.517 8.75l57 56q8.5 8.5 8.129 20.603-.37 12.104-8.765 20.5-8.157 8.397-20.261 8.647Q743.5 881 735 872Zm-56.147-495q-8.853-8.5-8.603-19.983.25-11.484 8.75-20.517l56-57q8.5-8.5 20.603-8.129 12.104.37 20.5 8.765 8.397 8.157 8.647 20.261Q785 312.5 776.5 321l-57 56q-7.853 8-19.687 8-11.834 0-20.96-8ZM183.897 872.121q-8.397-8.423-8.647-20.522Q175 839.5 184 831l57-56q8.3-8.5 19.9-8.75 11.6-.25 20.209 8.75 8.891 8.5 8.641 20T281 815l-56 57q-8.5 9-20.603 8.629-12.104-.37-20.5-8.508ZM480 576Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M479.853 717Q538.5 717 580 675.897q41.5-41.103 41.5-99.75T580.294 476q-41.206-41.5-100-41.5T380.25 475.706q-41.25 41.206-41.25 100t41.103 100.044Q421.206 717 479.853 717Zm.234 57.5q-82.587 0-140.587-58.06-58-58.06-58-140.5t58.06-140.69Q397.62 377 480.06 377t140.69 58.163Q679 493.327 679 575.913q0 82.587-58.163 140.587-58.164 58-140.75 58ZM70 604.5q-12.25 0-20.375-8.175-8.125-8.176-8.125-20.5 0-12.325 8.125-20.575T70 547h100q12.25 0 20.625 8.425 8.375 8.426 8.375 20.75 0 12.325-8.375 20.325t-20.625 8H70Zm720 0q-12.25 0-20.375-8.175-8.125-8.176-8.125-20.5 0-12.325 8.125-20.575T790 547h100q12.25 0 20.625 8.425 8.375 8.426 8.375 20.75 0 12.325-8.375 20.325t-20.625 8H790Zm-310.175-310q-12.325 0-20.325-8.125t-8-20.375V166q0-12.25 8.175-20.625 8.176-8.375 20.5-8.375 12.325 0 20.575 8.375T509 166v100q0 12.25-8.425 20.375-8.426 8.125-20.75 8.125Zm0 720q-12.325 0-20.325-8.12-8-8.13-8-20.38V886q0-12.25 8.175-20.625 8.176-8.375 20.5-8.375 12.325 0 20.575 8.375T509 886v100q0 12.25-8.425 20.38-8.426 8.12-20.75 8.12ZM241 377l-57-56q-9-8.5-8.629-20.603.37-12.104 8.765-20.5 8.157-8.397 20.261-8.647Q216.5 271 225 279.5l56 57q8 8.853 8 20.324 0 11.47-8 19.823T261.25 385q-11.75 0-20.25-8Zm494 495-56-57q-8-8.5-8-20.375t8.5-20.125q8-8.5 19.483-8.25 11.484.25 20.517 8.75l57 56q8.5 8.5 8.129 20.603-.37 12.104-8.765 20.5-8.157 8.397-20.261 8.647Q743.5 881 735 872Zm-56.147-495q-8.853-8.5-8.603-19.983.25-11.484 8.75-20.517l56-57q8.5-8.5 20.603-8.129 12.104.37 20.5 8.765 8.397 8.157 8.647 20.261Q785 312.5 776.5 321l-57 56q-7.853 8-19.687 8-11.834 0-20.96-8ZM183.897 872.121q-8.397-8.423-8.647-20.522Q175 839.5 184 831l57-56q8.3-8.5 19.9-8.75 11.6-.25 20.209 8.75 8.891 8.5 8.641 20T281 815l-56 57q-8.5 9-20.603 8.629-12.104-.37-20.5-8.508ZM480 576Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lightbulb.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lightbulb.svg new file mode 100644 index 0000000000..e84b1bd476 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lightbulb.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22" fill="#5f6368"> + <path + d="M480-84.5q-33.5 0-56.75-23.25T400-164.5h160q0 33.5-23.25 56.75T480-84.5Zm-160-143V-285h320v57.5H320Zm4.5-119.5q-65-42.5-102.75-106T184-595.87q0-120.39 87.76-208.26 87.77-87.87 208-87.87Q600-892 688-804.13q88 87.87 88 208.26 0 79.37-37.25 142.87T635.5-347h-311Zm21.32-57.5h269.3q47.88-32 75.63-82.51 27.75-50.51 27.75-108.95 0-98.04-70.04-168.29-70.04-70.25-168.37-70.25-98.34 0-168.46 70.12-70.13 70.12-70.13 168.57 0 58.49 28 108.9 28 50.41 76.32 82.41Zm134.18 0Z" /> +</svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_link.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_link.svg index b8063c1c8c..7de4497d5d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_link.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_link.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M281.5 772.5q-83.595 0-140.048-56.433Q85 659.634 85 576.067 85 492.5 141.452 436q56.453-56.5 140.048-56.5H421q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T421 437H281.75q-60.271 0-99.76 39.382-39.49 39.383-39.49 99.5 0 60.118 39.49 99.618 39.489 39.5 99.76 39.5H421q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T421 772.5H281.5Zm74.5-168q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T356 547h248.5q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T604.5 604.5H356Zm183.5 168q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T539.5 715h138.75q60.271 0 99.76-39.382 39.49-39.383 39.49-99.5 0-60.118-39.49-99.618-39.489-39.5-99.76-39.5H539.5q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075t20.625-8.25h139q83.595 0 140.047 56.433Q875 492.366 875 575.933 875 659.5 818.547 716q-56.452 56.5-140.047 56.5h-139Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M281.5 772.5q-83.595 0-140.048-56.433Q85 659.634 85 576.067 85 492.5 141.452 436q56.453-56.5 140.048-56.5H421q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T421 437H281.75q-60.271 0-99.76 39.382-39.49 39.383-39.49 99.5 0 60.118 39.49 99.618 39.489 39.5 99.76 39.5H421q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T421 772.5H281.5Zm74.5-168q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T356 547h248.5q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T604.5 604.5H356Zm183.5 168q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T539.5 715h138.75q60.271 0 99.76-39.382 39.49-39.383 39.49-99.5 0-60.118-39.49-99.618-39.489-39.5-99.76-39.5H539.5q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075t20.625-8.25h139q83.595 0 140.047 56.433Q875 492.366 875 575.933 875 659.5 818.547 716q-56.452 56.5-140.047 56.5h-139Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock.svg index 2bc91dde71..cec12d0d4a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M222 971q-23.719 0-40.609-16.891Q164.5 937.219 164.5 913.5v-431q0-23.719 16.891-40.609Q198.281 425 222 425h71.5v-95.018q0-77.832 54.39-132.157Q402.279 143.5 480.14 143.5q77.86 0 132.11 54.349 54.25 54.349 54.25 132.36V425H738q23.719 0 40.609 16.891Q795.5 458.781 795.5 482.5v431q0 23.719-16.891 40.609Q761.719 971 738 971H222Zm0-57.5h516v-431H222v431Zm258.084-140q31.179 0 53.297-21.566 22.119-21.566 22.119-51.85 0-29.347-22.203-53.465-22.203-24.119-53.381-24.119-31.179 0-53.297 24.035-22.119 24.034-22.119 53.881t22.203 51.465q22.203 21.619 53.381 21.619ZM351 425h258v-95.385q0-53.782-37.265-91.198Q534.471 201 480.235 201 426 201 388.5 238.333 351 275.667 351 329.769V425ZM222 913.5v-431 431Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M222 971q-23.719 0-40.609-16.891Q164.5 937.219 164.5 913.5v-431q0-23.719 16.891-40.609Q198.281 425 222 425h71.5v-95.018q0-77.832 54.39-132.157Q402.279 143.5 480.14 143.5q77.86 0 132.11 54.349 54.25 54.349 54.25 132.36V425H738q23.719 0 40.609 16.891Q795.5 458.781 795.5 482.5v431q0 23.719-16.891 40.609Q761.719 971 738 971H222Zm0-57.5h516v-431H222v431Zm258.084-140q31.179 0 53.297-21.566 22.119-21.566 22.119-51.85 0-29.347-22.203-53.465-22.203-24.119-53.381-24.119-31.179 0-53.297 24.035-22.119 24.034-22.119 53.881t22.203 51.465q22.203 21.619 53.381 21.619ZM351 425h258v-95.385q0-53.782-37.265-91.198Q534.471 201 480.235 201 426 201 388.5 238.333 351 275.667 351 329.769V425ZM222 913.5v-431 431Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_filled.svg index 396e11fac6..798a92145a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M222 971q-23.719 0-40.609-16.891Q164.5 937.219 164.5 913.5v-431q0-23.719 16.891-40.609Q198.281 425 222 425h71.5v-95.018q0-77.832 54.39-132.157Q402.279 143.5 480.14 143.5q77.86 0 132.11 54.349 54.25 54.349 54.25 132.36V425H738q23.719 0 40.609 16.891Q795.5 458.781 795.5 482.5v431q0 23.719-16.891 40.609Q761.719 971 738 971H222Zm258.084-197.5q31.179 0 53.297-21.566 22.119-21.566 22.119-51.85 0-29.347-22.203-53.465-22.203-24.119-53.381-24.119-31.179 0-53.297 24.035-22.119 24.034-22.119 53.881t22.203 51.465q22.203 21.619 53.381 21.619ZM351 425h258v-95.385q0-53.782-37.265-91.198Q534.471 201 480.235 201 426 201 388.5 238.333 351 275.667 351 329.769V425Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M222 971q-23.719 0-40.609-16.891Q164.5 937.219 164.5 913.5v-431q0-23.719 16.891-40.609Q198.281 425 222 425h71.5v-95.018q0-77.832 54.39-132.157Q402.279 143.5 480.14 143.5q77.86 0 132.11 54.349 54.25 54.349 54.25 132.36V425H738q23.719 0 40.609 16.891Q795.5 458.781 795.5 482.5v431q0 23.719-16.891 40.609Q761.719 971 738 971H222Zm258.084-197.5q31.179 0 53.297-21.566 22.119-21.566 22.119-51.85 0-29.347-22.203-53.465-22.203-24.119-53.381-24.119-31.179 0-53.297 24.035-22.119 24.034-22.119 53.881t22.203 51.465q22.203 21.619 53.381 21.619ZM351 425h258v-95.385q0-53.782-37.265-91.198Q534.471 201 480.235 201 426 201 388.5 238.333 351 275.667 351 329.769V425Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open_right.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open_right.svg index 3188cf798e..0805267813 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open_right.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open_right.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M222-142.5h516v-431H222v431Zm258.084-140q31.179 0 53.297-21.566 22.119-21.566 22.119-51.85 0-29.347-22.203-53.465-22.203-24.119-53.381-24.119-31.179 0-53.297 24.035-22.119 24.034-22.119 53.881t22.203 51.465q22.203 21.619 53.381 21.619ZM222-142.5v-431 431Zm0 57.5q-23.719 0-40.609-16.891Q164.5-118.781 164.5-142.5v-431q0-23.719 16.891-40.609Q198.281-631 222-631h329.5v-95.018q0-77.832 54.349-132.157Q660.198-912.5 738-912.5q70 0 121.25 44T922-759q2 11.5-6.638 22.25T895.75-726q-12.66 0-20.705-6-8.045-6-9.545-18.5-9-44.5-44.55-74.5T738-855q-54.333 0-91.667 37.333Q609-780.333 609-726.231V-631h129q23.719 0 40.609 16.891Q795.5-597.219 795.5-573.5v431q0 23.719-16.891 40.609Q761.719-85 738-85H222Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M222-142.5h516v-431H222v431Zm258.084-140q31.179 0 53.297-21.566 22.119-21.566 22.119-51.85 0-29.347-22.203-53.465-22.203-24.119-53.381-24.119-31.179 0-53.297 24.035-22.119 24.034-22.119 53.881t22.203 51.465q22.203 21.619 53.381 21.619ZM222-142.5v-431 431Zm0 57.5q-23.719 0-40.609-16.891Q164.5-118.781 164.5-142.5v-431q0-23.719 16.891-40.609Q198.281-631 222-631h329.5v-95.018q0-77.832 54.349-132.157Q660.198-912.5 738-912.5q70 0 121.25 44T922-759q2 11.5-6.638 22.25T895.75-726q-12.66 0-20.705-6-8.045-6-9.545-18.5-9-44.5-44.55-74.5T738-855q-54.333 0-91.667 37.333Q609-780.333 609-726.231V-631h129q23.719 0 40.609 16.891Q795.5-597.219 795.5-573.5v431q0 23.719-16.891 40.609Q761.719-85 738-85H222Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_login.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_login.svg index be56f0f451..d675b77073 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_login.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_login.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M393.5 726.5q-8.5-8.011-8.5-20.255 0-12.245 8.5-20.745l80-79.5H154q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T154 548.5h317.5l-81-81q-8-7.567-7.75-19.481.25-11.914 8.943-20.466Q399.661 419 412.08 419q12.42 0 21.42 9l130 130q4.5 5.08 6.75 9.973 2.25 4.894 2.25 10.2 0 5.807-2.25 10.567T563.5 598L434 727.5q-7.5 8-19.5 8t-21-9Zm124 207q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T517.5 876h260V278.5h-260q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T517.5 221h260q22.938 0 40.219 17.281Q835 255.562 835 278.5V876q0 22.938-17.281 40.219Q800.438 933.5 777.5 933.5h-260Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M393.5 726.5q-8.5-8.011-8.5-20.255 0-12.245 8.5-20.745l80-79.5H154q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T154 548.5h317.5l-81-81q-8-7.567-7.75-19.481.25-11.914 8.943-20.466Q399.661 419 412.08 419q12.42 0 21.42 9l130 130q4.5 5.08 6.75 9.973 2.25 4.894 2.25 10.2 0 5.807-2.25 10.567T563.5 598L434 727.5q-7.5 8-19.5 8t-21-9Zm124 207q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T517.5 876h260V278.5h-260q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T517.5 221h260q22.938 0 40.219 17.281Q835 255.562 835 278.5V876q0 22.938-17.281 40.219Q800.438 933.5 777.5 933.5h-260Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_logout.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_logout.svg index 5808094655..734242b294 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_logout.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_logout.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M645 726.5q-8.5-8.011-8.5-20.255 0-12.245 8.5-20.745l80-79.5H405.5q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.5 0-12.075 8.375-20.325t20.625-8.25H723l-81-81q-8-7.567-7.75-19.481.25-11.914 8.943-20.466Q651.161 419 663.58 419q12.42 0 21.42 9l130 130q4.5 5.08 6.75 9.957 2.25 4.876 2.25 10.45 0 5.573-2.25 10.333t-6.85 9.36L685.5 727.5q-7.5 7.5-19.5 7.75t-21-8.75Zm-462.5 207q-22.969 0-40.234-17.266Q125 898.969 125 876V278.5q0-22.969 17.266-40.234Q159.531 221 182.5 221H443q11.675 0 20.087 8.463 8.413 8.463 8.413 20.212 0 12.325-8.413 20.575-8.412 8.25-20.087 8.25H182.5V876H443q11.675 0 20.087 8.463 8.413 8.463 8.413 20.212 0 12.325-8.413 20.575-8.412 8.25-20.087 8.25H182.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M645 726.5q-8.5-8.011-8.5-20.255 0-12.245 8.5-20.745l80-79.5H405.5q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.5 0-12.075 8.375-20.325t20.625-8.25H723l-81-81q-8-7.567-7.75-19.481.25-11.914 8.943-20.466Q651.161 419 663.58 419q12.42 0 21.42 9l130 130q4.5 5.08 6.75 9.957 2.25 4.876 2.25 10.45 0 5.573-2.25 10.333t-6.85 9.36L685.5 727.5q-7.5 7.5-19.5 7.75t-21-8.75Zm-462.5 207q-22.969 0-40.234-17.266Q125 898.969 125 876V278.5q0-22.969 17.266-40.234Q159.531 221 182.5 221H443q11.675 0 20.087 8.463 8.413 8.463 8.413 20.212 0 12.325-8.413 20.575-8.412 8.25-20.087 8.25H182.5V876H443q11.675 0 20.087 8.463 8.413 8.463 8.413 20.212 0 12.325-8.413 20.575-8.412 8.25-20.087 8.25H182.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mail.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mail.svg index 0a4b8adbd8..f45ed237f3 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mail.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mail.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M142.5 891.5q-22.969 0-40.234-17.266Q85 856.969 85 834V318q0-22.969 17.266-40.234Q119.531 260.5 142.5 260.5h675q22.969 0 40.234 17.266Q875 295.031 875 318v516q0 22.969-17.266 40.234Q840.469 891.5 817.5 891.5h-675Zm675-521-322 211q-4 2-7.5 3.5t-8 1.5q-4.5 0-8-1.5t-7-3.5l-322.5-211V834h675V370.5ZM480 534.5 813 318H148l332 216.5Zm-337.5-164v6.5-37.206.51V318v22-.662 37.732-6.537V834 370.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M142.5 891.5q-22.969 0-40.234-17.266Q85 856.969 85 834V318q0-22.969 17.266-40.234Q119.531 260.5 142.5 260.5h675q22.969 0 40.234 17.266Q875 295.031 875 318v516q0 22.969-17.266 40.234Q840.469 891.5 817.5 891.5h-675Zm675-521-322 211q-4 2-7.5 3.5t-8 1.5q-4.5 0-8-1.5t-7-3.5l-322.5-211V834h675V370.5ZM480 534.5 813 318H148l332 216.5Zm-337.5-164v6.5-37.206.51V318v22-.662 37.732-6.537V834 370.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mail_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mail_filled.svg new file mode 100644 index 0000000000..c51057b8cb --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mail_filled.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22" fill="#5f6368"><path d="M142.5-164.5q-22.97 0-40.23-17.27Q85-199.03 85-222v-516q0-22.97 17.27-40.23 17.26-17.27 40.23-17.27h675q22.97 0 40.23 17.27Q875-760.97 875-738v516q0 22.97-17.27 40.23-17.26 17.27-40.23 17.27h-675ZM480-464l337.5-221.5V-738L480-521.5 142.5-738v52.5L480-464Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_manage_accounts.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_manage_accounts.svg index e1cc6816b1..6a5b60424c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_manage_accounts.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_manage_accounts.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M400.522 571.5Q335 571.5 294 530.522t-41-106.5q0-65.522 40.978-106.522t106.5-41Q466 276.5 507 317.478t41 106.5q0 65.522-40.978 106.522t-106.5 41ZM113.5 885.5q-12.725 0-20.612-7.888Q85 869.725 85 857v-62.5q0-34.5 17.25-62t49.75-42q74-33 134.75-46.25T400.394 631H423q-5.5 14-8.5 26.688-3 12.687-5 30.812h-9q-54.5 0-109 11.5T175 742q-16.5 8.5-24.5 22.845-8 14.345-8 29.655v33.302h266.953Q414 845.5 420.75 859.25 427.5 873 437 885.5H113.5Zm287-371.5q39 0 64.5-25.5t25.5-64.5q0-39-25.5-64.5T400.5 334q-39 0-64.5 25.5T310.5 424q0 39 25.5 64.5t64.5 25.5Zm0-90Zm9 404Zm282.188-18q32.812 0 56.062-23.378T771 730.416q0-32.622-23.25-55.769T691.688 651.5q-32.812 0-56 23.304-23.188 23.305-23.188 56.03t23.188 55.946q23.188 23.22 56 23.22ZM656.5 864q-16.864-4.896-34.744-14.531Q603.875 839.833 592 827.5l-44 9.5q-5.132 2-9.816 0-4.684-2-6.255-6.478L520 809q-3-3.82-2-8.61 1-4.79 5-8.89l37.5-34.837Q558 747.5 558 731t2.5-25.646l-38-35.802Q519 666 518 661t2-9l11.929-21.022q1.571-4.478 6.474-6.478 4.903-2 10.291-.5L592 634.5q12-12.5 29.841-22.078T656.5 598l7.409-52.363q1.038-5.918 4.907-9.278Q672.684 533 678.818 533H705q5.816 0 9.684 3.359 3.869 3.36 4.816 9.141L727 598q17.636 5 35.318 14.5t29.682 22l43.677-10q4.613-1.5 9.527.25 4.914 1.75 6.796 6.25l12 21q2.5 4 1.5 9t-4.7 8.427L823.5 705.5q2 9.5 2 25.5t-2 25.5l37.5 35q3.5 4 4.5 9t-2 8.5l-11.429 21.522Q850 835 845 837t-9.72.067l-43.076-9.567Q780 840 762.318 849.578T727 864l-7.5 52q-.947 6.281-4.816 9.641Q710.816 929 705 929h-26.182q-6.134 0-10.002-3.359-3.869-3.36-4.816-9.641l-7.5-52Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M400.522 571.5Q335 571.5 294 530.522t-41-106.5q0-65.522 40.978-106.522t106.5-41Q466 276.5 507 317.478t41 106.5q0 65.522-40.978 106.522t-106.5 41ZM113.5 885.5q-12.725 0-20.612-7.888Q85 869.725 85 857v-62.5q0-34.5 17.25-62t49.75-42q74-33 134.75-46.25T400.394 631H423q-5.5 14-8.5 26.688-3 12.687-5 30.812h-9q-54.5 0-109 11.5T175 742q-16.5 8.5-24.5 22.845-8 14.345-8 29.655v33.302h266.953Q414 845.5 420.75 859.25 427.5 873 437 885.5H113.5Zm287-371.5q39 0 64.5-25.5t25.5-64.5q0-39-25.5-64.5T400.5 334q-39 0-64.5 25.5T310.5 424q0 39 25.5 64.5t64.5 25.5Zm0-90Zm9 404Zm282.188-18q32.812 0 56.062-23.378T771 730.416q0-32.622-23.25-55.769T691.688 651.5q-32.812 0-56 23.304-23.188 23.305-23.188 56.03t23.188 55.946q23.188 23.22 56 23.22ZM656.5 864q-16.864-4.896-34.744-14.531Q603.875 839.833 592 827.5l-44 9.5q-5.132 2-9.816 0-4.684-2-6.255-6.478L520 809q-3-3.82-2-8.61 1-4.79 5-8.89l37.5-34.837Q558 747.5 558 731t2.5-25.646l-38-35.802Q519 666 518 661t2-9l11.929-21.022q1.571-4.478 6.474-6.478 4.903-2 10.291-.5L592 634.5q12-12.5 29.841-22.078T656.5 598l7.409-52.363q1.038-5.918 4.907-9.278Q672.684 533 678.818 533H705q5.816 0 9.684 3.359 3.869 3.36 4.816 9.141L727 598q17.636 5 35.318 14.5t29.682 22l43.677-10q4.613-1.5 9.527.25 4.914 1.75 6.796 6.25l12 21q2.5 4 1.5 9t-4.7 8.427L823.5 705.5q2 9.5 2 25.5t-2 25.5l37.5 35q3.5 4 4.5 9t-2 8.5l-11.429 21.522Q850 835 845 837t-9.72.067l-43.076-9.567Q780 840 762.318 849.578T727 864l-7.5 52q-.947 6.281-4.816 9.641Q710.816 929 705 929h-26.182q-6.134 0-10.002-3.359-3.869-3.36-4.816-9.641l-7.5-52Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_maps_ugc.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_maps_ugc.svg index 469d44a2de..282b36ba25 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_maps_ugc.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_maps_ugc.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M476 580ZM60.5 960.5l56-201q-20-42.5-28.75-87.954Q79 626.092 79 579.119q0-82.936 31.263-155.37 31.263-72.435 85.25-126.342Q249.5 243.5 321.837 212.25 394.175 181 477 181q82.825 0 155.163 31.25Q704.5 243.5 758.487 297.4q53.987 53.9 85.25 126.325Q875 496.15 875 579.075T843.75 734.25q-31.25 72.25-85.157 126.237-53.907 53.987-126.342 85.25Q559.817 977 476.908 977q-47.156 0-92.532-8.75T296.5 939.5l-201 56q-16.5 4.5-28-7t-7-28ZM477.175 738q12.325 0 20.575-8.375T506 709v-97h98q12.25 0 20.625-8.425 8.375-8.426 8.375-20.5 0-12.075-8.375-20.325T604 554.5h-98V456q0-11.675-8.425-20.088-8.426-8.412-20.75-8.412-12.325 0-20.325 8.412-8 8.413-8 20.088v98.5H350q-12.25 0-20.375 8.535t-8.125 20.39q0 11.856 8.125 20.216Q337.75 612 350 612h98.5v97q0 12.25 8.175 20.625 8.176 8.375 20.5 8.375ZM133.5 922.5l137-37.5q4.853-2 9.177-3t9.178-1q11.145 0 20.645 2t19.5 6q36 14 72.969 22.25 36.969 8.25 74.994 8.25Q618.5 919.5 718 820.033t99.5-241Q817.5 437.5 718.033 338t-241-99.5Q335.5 238.5 236 337.769q-99.5 99.268-99.5 241.737 0 37.994 5.25 75.994T167 727q7 13.696 7.5 29.1.5 15.404-3.537 29.277L133.5 922.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M476 580ZM60.5 960.5l56-201q-20-42.5-28.75-87.954Q79 626.092 79 579.119q0-82.936 31.263-155.37 31.263-72.435 85.25-126.342Q249.5 243.5 321.837 212.25 394.175 181 477 181q82.825 0 155.163 31.25Q704.5 243.5 758.487 297.4q53.987 53.9 85.25 126.325Q875 496.15 875 579.075T843.75 734.25q-31.25 72.25-85.157 126.237-53.907 53.987-126.342 85.25Q559.817 977 476.908 977q-47.156 0-92.532-8.75T296.5 939.5l-201 56q-16.5 4.5-28-7t-7-28ZM477.175 738q12.325 0 20.575-8.375T506 709v-97h98q12.25 0 20.625-8.425 8.375-8.426 8.375-20.5 0-12.075-8.375-20.325T604 554.5h-98V456q0-11.675-8.425-20.088-8.426-8.412-20.75-8.412-12.325 0-20.325 8.412-8 8.413-8 20.088v98.5H350q-12.25 0-20.375 8.535t-8.125 20.39q0 11.856 8.125 20.216Q337.75 612 350 612h98.5v97q0 12.25 8.175 20.625 8.176 8.375 20.5 8.375ZM133.5 922.5l137-37.5q4.853-2 9.177-3t9.178-1q11.145 0 20.645 2t19.5 6q36 14 72.969 22.25 36.969 8.25 74.994 8.25Q618.5 919.5 718 820.033t99.5-241Q817.5 437.5 718.033 338t-241-99.5Q335.5 238.5 236 337.769q-99.5 99.268-99.5 241.737 0 37.994 5.25 75.994T167 727q7 13.696 7.5 29.1.5 15.404-3.537 29.277L133.5 922.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mark_chat_unread.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mark_chat_unread.svg index 5e1e746ee9..a89a956b29 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mark_chat_unread.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mark_chat_unread.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M242.206 811.5 133.5 920q-13.5 13.5-31 6.172T85 900V238q0-21.969 17.266-39.734Q119.531 180.5 142.5 180.5H578q-3.5 13.5-5.5 28.25T571 238H142.5v516h675V409.5q15.794-5 29.897-12.75Q861.5 389 875 379v375q0 21.969-17.766 39.734Q839.469 811.5 817.5 811.5H242.206ZM142.5 238v516-516Zm617.441 101Q714.5 339 683 307.353t-31.5-77.25q0-45.603 31.706-77.103 31.706-31.5 77-31.5t77.044 31.689Q869 184.877 869 230.147 869 276 837.191 307.5q-31.809 31.5-77.25 31.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M242.206 811.5 133.5 920q-13.5 13.5-31 6.172T85 900V238q0-21.969 17.266-39.734Q119.531 180.5 142.5 180.5H578q-3.5 13.5-5.5 28.25T571 238H142.5v516h675V409.5q15.794-5 29.897-12.75Q861.5 389 875 379v375q0 21.969-17.766 39.734Q839.469 811.5 817.5 811.5H242.206ZM142.5 238v516-516Zm617.441 101Q714.5 339 683 307.353t-31.5-77.25q0-45.603 31.706-77.103 31.706-31.5 77-31.5t77.044 31.689Q869 184.877 869 230.147 869 276 837.191 307.5q-31.809 31.5-77.25 31.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_match_case.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_match_case.svg new file mode 100644 index 0000000000..20adb34c10 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_match_case.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22" fill="#000000"> + <path + d="M246-382.5 209-277q-2 6.5-8.25 10.75T187-262q-12 0-18.5-9.25t-2-20.75L308-681.5q3-7.5 8.75-11.5t13.25-4h18q7.5 0 13.25 4t8.75 11.5l142.5 389q4.5 11.5-2.5 21t-19.5 9.5q-7.5 0-13.75-3.75t-8.25-11.75l-37-105H246Zm15.5-42h155l-77-217.5H338l-76.5 217.5Zm403 172.5q-50 0-82.5-26t-32.5-69.5q0-47.5 35-75.75t93-28.25q24.5 0 47 4.5t39 13v-20q0-38-20.25-58.25T684-532.5q-17.5 0-34.5 5.25T618-511q-8 5.5-15.75 3T590-517.5q-4.5-6.5-3.25-15T597-547q20-13 42.25-18.75T685-571.5q59.5 0 90.75 30.75T807-450v166.5q0 9-6.25 15.25T786-262q-9 0-15.25-6.25T764.5-283v-21H761q-16 26.5-43.5 39.25t-53 12.75Zm-.5-36.5q36.5 0 68.5-30.25t32-77.75q-16-8-38-13t-43.5-5q-42 0-65.5 17.5T594-348q0 27 20.5 43.25T664-288.5Z" /> +</svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_menu.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_menu.svg index a192e32f34..28070d177d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_menu.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_menu.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M153 813q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T153 755.5h655q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T808 813H153Zm0-208.5q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T153 547h655q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T808 604.5H153Zm0-208q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T153 339h655q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T808 396.5H153Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M153 813q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T153 755.5h655q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T808 813H153Zm0-208.5q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T153 547h655q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T808 604.5H153Zm0-208q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T153 339h655q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T808 396.5H153Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic.svg index a938a4250f..769198e13c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M480 630.5q-41.75 0-69.875-30.167Q382 570.167 382 527V278q0-40.417 28.566-68.708Q439.132 181 479.941 181t69.434 28.292Q578 237.583 578 278v249q0 43.167-28.125 73.333Q521.75 630.5 480 630.5Zm0-224.5Zm-.175 526q-12.325 0-20.325-8.375t-8-20.625V795.865Q354 786 285.25 719T206 557.5q-1.5-12.593 7.295-21.547Q222.091 527 235.5 527q9.917 0 18.148 7.542 8.232 7.541 9.852 18.458 10.5 80.5 72.044 134 61.543 53.5 144.347 53.5 82.805 0 144.457-53.5Q686 633.5 696.5 553q1.853-11.167 10.121-18.583Q714.89 527 725.543 527q12.91 0 21.434 8.953Q755.5 544.907 754 557.5 743.5 652 674.75 719T509 795.865V903q0 12.25-8.425 20.625-8.426 8.375-20.75 8.375ZM480 573q18.075 0 29.288-13.5Q520.5 546 520.5 527V278.335q0-16.835-11.629-28.335-11.628-11.5-28.818-11.5t-28.872 11.356Q439.5 261.212 439.5 278v248.868q0 19.132 11.212 32.632Q461.925 573 480 573Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M480 630.5q-41.75 0-69.875-30.167Q382 570.167 382 527V278q0-40.417 28.566-68.708Q439.132 181 479.941 181t69.434 28.292Q578 237.583 578 278v249q0 43.167-28.125 73.333Q521.75 630.5 480 630.5Zm0-224.5Zm-.175 526q-12.325 0-20.325-8.375t-8-20.625V795.865Q354 786 285.25 719T206 557.5q-1.5-12.593 7.295-21.547Q222.091 527 235.5 527q9.917 0 18.148 7.542 8.232 7.541 9.852 18.458 10.5 80.5 72.044 134 61.543 53.5 144.347 53.5 82.805 0 144.457-53.5Q686 633.5 696.5 553q1.853-11.167 10.121-18.583Q714.89 527 725.543 527q12.91 0 21.434 8.953Q755.5 544.907 754 557.5 743.5 652 674.75 719T509 795.865V903q0 12.25-8.425 20.625-8.426 8.375-20.75 8.375ZM480 573q18.075 0 29.288-13.5Q520.5 546 520.5 527V278.335q0-16.835-11.629-28.335-11.628-11.5-28.818-11.5t-28.872 11.356Q439.5 261.212 439.5 278v248.868q0 19.132 11.212 32.632Q461.925 573 480 573Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic_filled.svg index bd28745400..44c0f89fa0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M480 630.5q-41.75 0-69.875-30.167Q382 570.167 382 527V278q0-40.417 28.566-68.708Q439.132 181 479.941 181t69.434 28.292Q578 237.583 578 278v249q0 43.167-28.125 73.333Q521.75 630.5 480 630.5Zm-.175 301.5q-12.325 0-20.325-8.375t-8-20.625V795.865Q354 786 285.25 719T206 557.5q-1.5-12.593 7.25-21.547Q222 527 235.5 527q10 0 18.19 7.542 8.19 7.541 9.81 18.458 10.5 80.5 71.391 134 60.892 53.5 145 53.5Q564 740.5 625 687q61-53.5 71.5-134 1.853-11.167 10.121-18.583Q714.89 527 725.543 527q12.957 0 21.457 8.953 8.5 8.954 7 21.547Q743.5 652 674.75 719T509 795.865V903q0 12.25-8.425 20.625-8.426 8.375-20.75 8.375Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M480 630.5q-41.75 0-69.875-30.167Q382 570.167 382 527V278q0-40.417 28.566-68.708Q439.132 181 479.941 181t69.434 28.292Q578 237.583 578 278v249q0 43.167-28.125 73.333Q521.75 630.5 480 630.5Zm-.175 301.5q-12.325 0-20.325-8.375t-8-20.625V795.865Q354 786 285.25 719T206 557.5q-1.5-12.593 7.25-21.547Q222 527 235.5 527q10 0 18.19 7.542 8.19 7.541 9.81 18.458 10.5 80.5 71.391 134 60.892 53.5 145 53.5Q564 740.5 625 687q61-53.5 71.5-134 1.853-11.167 10.121-18.583Q714.89 527 725.543 527q12.957 0 21.457 8.953 8.5 8.954 7 21.547Q743.5 652 674.75 719T509 795.865V903q0 12.25-8.425 20.625-8.426 8.375-20.75 8.375Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic_off.svg index 1ea755eb4e..f7f2c074f4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic_off.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic_off.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M681.5 693 640 651.5q16.5-20.5 26-45.75T679 553q1.814-11.167 10.182-18.583Q697.551 527 707.847 527q13.153 0 21.653 8.953 8.5 8.954 7 21.547-4.5 37-18.5 71.75T681.5 693ZM554 566l-51-50V279.038q0-17.463-11.489-29.001-11.49-11.537-29.213-11.537t-29.01 11.431Q422 261.362 422 279v155l-57.5-57.5V279q0-40.833 28.515-69.417Q421.529 181 462.265 181q40.735 0 69.485 28.583Q560.5 238.167 560.5 279v248.23q0 7.103-1.5 19.186-1.5 12.084-5 19.584Zm-94.5-94.5Zm350.5 505L58.5 225q-8-7.444-8-18.222Q50.5 196 58.25 188q7.75-8 18.006-8 10.255 0 18.244 8L847 940.5q8 7.989 8 17.994 0 10.006-8 17.756-8 8.25-18.961 8.25-10.961 0-18.039-8ZM433.5 903V795.865Q336 786 267.5 719t-79-161.5q-2-12.5 7.045-21.5 9.046-9 22.455-9 9.5 0 17.75 7.5T246 553q10.053 80.713 71.588 134.107Q379.124 740.5 462.289 740.5q37.711 0 73.071-12.588Q570.721 715.325 599.5 693l41.5 41.5q-31 26-69.014 41.568Q533.972 791.635 491 796v107q0 12.25-8.463 20.625T462.325 932q-12.325 0-20.575-8.375T433.5 903Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M681.5 693 640 651.5q16.5-20.5 26-45.75T679 553q1.814-11.167 10.182-18.583Q697.551 527 707.847 527q13.153 0 21.653 8.953 8.5 8.954 7 21.547-4.5 37-18.5 71.75T681.5 693ZM554 566l-51-50V279.038q0-17.463-11.489-29.001-11.49-11.537-29.213-11.537t-29.01 11.431Q422 261.362 422 279v155l-57.5-57.5V279q0-40.833 28.515-69.417Q421.529 181 462.265 181q40.735 0 69.485 28.583Q560.5 238.167 560.5 279v248.23q0 7.103-1.5 19.186-1.5 12.084-5 19.584Zm-94.5-94.5Zm350.5 505L58.5 225q-8-7.444-8-18.222Q50.5 196 58.25 188q7.75-8 18.006-8 10.255 0 18.244 8L847 940.5q8 7.989 8 17.994 0 10.006-8 17.756-8 8.25-18.961 8.25-10.961 0-18.039-8ZM433.5 903V795.865Q336 786 267.5 719t-79-161.5q-2-12.5 7.045-21.5 9.046-9 22.455-9 9.5 0 17.75 7.5T246 553q10.053 80.713 71.588 134.107Q379.124 740.5 462.289 740.5q37.711 0 73.071-12.588Q570.721 715.325 599.5 693l41.5 41.5q-31 26-69.014 41.568Q533.972 791.635 491 796v107q0 12.25-8.463 20.625T462.325 932q-12.325 0-20.575-8.375T433.5 903Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_more_horiz.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_more_horiz.svg index cc4f97431d..8d36d622c0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_more_horiz.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_more_horiz.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M201.858 623q-19.358 0-33.108-13.892Q155 595.217 155 575.858q0-19.358 13.892-33.108Q182.783 529 202.142 529q19.358 0 33.108 13.892Q249 556.783 249 576.142q0 19.358-13.892 33.108Q221.217 623 201.858 623Zm278 0q-19.358 0-33.108-13.892Q433 595.217 433 575.858q0-19.358 13.892-33.108Q460.783 529 480.142 529q19.358 0 33.108 13.892Q527 556.783 527 576.142q0 19.358-13.892 33.108Q499.217 623 479.858 623Zm278 0q-19.358 0-33.108-13.892Q711 595.217 711 575.858q0-19.358 13.892-33.108Q738.783 529 758.142 529q19.358 0 33.108 13.892Q805 556.783 805 576.142q0 19.358-13.892 33.108Q777.217 623 757.858 623Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M201.858 623q-19.358 0-33.108-13.892Q155 595.217 155 575.858q0-19.358 13.892-33.108Q182.783 529 202.142 529q19.358 0 33.108 13.892Q249 556.783 249 576.142q0 19.358-13.892 33.108Q221.217 623 201.858 623Zm278 0q-19.358 0-33.108-13.892Q433 595.217 433 575.858q0-19.358 13.892-33.108Q460.783 529 480.142 529q19.358 0 33.108 13.892Q527 556.783 527 576.142q0 19.358-13.892 33.108Q499.217 623 479.858 623Zm278 0q-19.358 0-33.108-13.892Q711 595.217 711 575.858q0-19.358 13.892-33.108Q738.783 529 758.142 529q19.358 0 33.108 13.892Q805 556.783 805 576.142q0 19.358-13.892 33.108Q777.217 623 757.858 623Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_more_vert.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_more_vert.svg index 9a9c7d193b..dc55c23649 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_more_vert.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_more_vert.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M479.858 901q-19.358 0-33.108-13.892Q433 873.217 433 853.858q0-19.358 13.892-33.108Q460.783 807 480.142 807q19.358 0 33.108 13.892Q527 834.783 527 854.142q0 19.358-13.892 33.108Q499.217 901 479.858 901Zm0-278q-19.358 0-33.108-13.892Q433 595.217 433 575.858q0-19.358 13.892-33.108Q460.783 529 480.142 529q19.358 0 33.108 13.892Q527 556.783 527 576.142q0 19.358-13.892 33.108Q499.217 623 479.858 623Zm0-278q-19.358 0-33.108-13.892Q433 317.217 433 297.858q0-19.358 13.892-33.108Q460.783 251 480.142 251q19.358 0 33.108 13.892Q527 278.783 527 298.142q0 19.358-13.892 33.108Q499.217 345 479.858 345Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M479.858 901q-19.358 0-33.108-13.892Q433 873.217 433 853.858q0-19.358 13.892-33.108Q460.783 807 480.142 807q19.358 0 33.108 13.892Q527 834.783 527 854.142q0 19.358-13.892 33.108Q499.217 901 479.858 901Zm0-278q-19.358 0-33.108-13.892Q433 595.217 433 575.858q0-19.358 13.892-33.108Q460.783 529 480.142 529q19.358 0 33.108 13.892Q527 556.783 527 576.142q0 19.358-13.892 33.108Q499.217 623 479.858 623Zm0-278q-19.358 0-33.108-13.892Q433 317.217 433 297.858q0-19.358 13.892-33.108Q460.783 251 480.142 251q19.358 0 33.108 13.892Q527 278.783 527 298.142q0 19.358-13.892 33.108Q499.217 345 479.858 345Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_music_note.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_music_note.svg new file mode 100644 index 0000000000..9e127f18f8 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_music_note.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M393.814-125q-62.314 0-104.564-42.381Q247-209.762 247-272q0-62.237 42.25-104.619Q331.5-419 394-419q27.5 0 50.25 8t39.25 22.5v-418q0-11.925 8-20.213Q499.5-835 512-835h172.5q12.5 0 20.75 8.24 8.25 8.239 8.25 20.093V-733q0 12.5-8.25 20.75T684.5-704H541v432q0 62.238-42.436 104.619T393.814-125Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_note_add.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_note_add.svg index 8459292f6e..76ba771f33 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_note_add.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_note_add.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M451.5 691v99q0 12.25 8.425 20.625 8.426 8.375 20.5 8.375 12.075 0 20.325-8.375T509 790v-99h100.5q11.675 0 20.088-8.425 8.412-8.426 8.412-20.5 0-12.075-8.412-20.325-8.413-8.25-20.088-8.25H509V533q0-11.675-8.463-20.088-8.463-8.412-20.212-8.412-12.325 0-20.575 8.412-8.25 8.413-8.25 20.088v100.5h-100q-12.25 0-20.625 8.463t-8.375 20.212q0 12.325 8.375 20.575T351.5 691h100ZM222 971q-22.969 0-40.234-17.266Q164.5 936.469 164.5 913.5v-675q0-22.969 17.266-40.234Q199.031 181 222 181h335q11.911 0 22.706 4.75 10.794 4.75 18.912 12.343l179.264 179.314Q786 385.5 790.75 396.294q4.75 10.795 4.75 22.706v494.5q0 22.969-17.266 40.234Q760.969 971 738 971H222Zm330.5-579V238.5H222v675h516V421H581.5q-12.25 0-20.625-8.375T552.5 392ZM222 238.5V421 238.5v675-675Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M451.5 691v99q0 12.25 8.425 20.625 8.426 8.375 20.5 8.375 12.075 0 20.325-8.375T509 790v-99h100.5q11.675 0 20.088-8.425 8.412-8.426 8.412-20.5 0-12.075-8.412-20.325-8.413-8.25-20.088-8.25H509V533q0-11.675-8.463-20.088-8.463-8.412-20.212-8.412-12.325 0-20.575 8.412-8.25 8.413-8.25 20.088v100.5h-100q-12.25 0-20.625 8.463t-8.375 20.212q0 12.325 8.375 20.575T351.5 691h100ZM222 971q-22.969 0-40.234-17.266Q164.5 936.469 164.5 913.5v-675q0-22.969 17.266-40.234Q199.031 181 222 181h335q11.911 0 22.706 4.75 10.794 4.75 18.912 12.343l179.264 179.314Q786 385.5 790.75 396.294q4.75 10.795 4.75 22.706v494.5q0 22.969-17.266 40.234Q760.969 971 738 971H222Zm330.5-579V238.5H222v675h516V421H581.5q-12.25 0-20.625-8.375T552.5 392ZM222 238.5V421 238.5v675-675Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notification_important.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notification_important.svg new file mode 100644 index 0000000000..37c79840d7 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notification_important.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M204-207.5q-15.5 0-26.5-11t-11-26.5q0-15.5 11-26.5t26.5-11h37.5v-271q0-83 50.25-147.5T422-786v-26q0-23.96 16.88-40.73 16.88-16.77 41-16.77T521-852.73q17 16.77 17 40.73v26q80 20.5 130.25 85t50.25 147.5v271H756q15.5 0 26.5 11t11 26.5q0 15.5-11 26.5t-26.5 11H204ZM480-500Zm0 408q-32.5 0-55.25-22.75T402-170h156q0 32.5-22.75 55.25T480-92ZM316.5-282.5h327v-271q0-67.44-48.04-115.47T479.96-717q-67.46 0-115.46 48.03t-48 115.47v271ZM480-437q15.5 0 26.5-11t11-26.5v-123q0-15.5-11-26.5T480-635q-15.5 0-26.5 11t-11 26.5v123q0 15.5 11 26.5t26.5 11Zm-.11 114Q496-323 507-333.89q11-10.9 11-27Q518-377 507.11-388q-10.9-11-27-11Q464-399 453-388.11q-11 10.9-11 27Q442-345 452.89-334q10.9 11 27 11Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notification_important_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notification_important_filled.svg new file mode 100644 index 0000000000..75f247a21e --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notification_important_filled.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M204-207.5q-15.5 0-26.5-11t-11-26.5q0-15.5 11-26.5t26.5-11h37.5v-271q0-83 50.25-147.5T422-786v-26q0-23.96 16.88-40.73 16.88-16.77 41-16.77T521-852.73q17 16.77 17 40.73v26q80 20.5 130.25 85t50.25 147.5v271H756q15.5 0 26.5 11t11 26.5q0 15.5-11 26.5t-26.5 11H204ZM480-92q-32.5 0-55.25-22.75T402-170h156q0 32.5-22.75 55.25T480-92Zm0-345q15.5 0 26.5-11t11-26.5v-123q0-15.5-11-26.5T480-635q-15.5 0-26.5 11t-11 26.5v123q0 15.5 11 26.5t26.5 11Zm-.11 114Q496-323 507-333.89q11-10.9 11-27Q518-377 507.11-388q-10.9-11-27-11Q464-399 453-388.11q-11 10.9-11 27Q442-345 452.89-334q10.9 11 27 11Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications.svg index bcba2ae944..afe98fc544 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M192 855q-12.25 0-20.375-8.535t-8.125-20.39q0-11.856 8.125-20.216Q179.75 797.5 192 797.5h55V491q0-81 49-146.75t129-82.75V233q0-22.958 15.647-37.479t39-14.521Q503 181 519 195.521T535 233v28.5q80 17 129.75 82.75T714.5 491v306.5H768q12.25 0 20.625 8.463T797 826.175q0 12.325-8.375 20.575T768 855H192Zm288-292.5Zm-.25 410q-31.75 0-55.25-23.25T401 893.5h158q0 32.5-23.25 55.75t-56 23.25Zm-175.25-175H657V491.095Q657 416 606.5 364.75t-125-51.25q-74.294 0-125.647 51.353Q304.5 416.206 304.5 491v306.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M192 855q-12.25 0-20.375-8.535t-8.125-20.39q0-11.856 8.125-20.216Q179.75 797.5 192 797.5h55V491q0-81 49-146.75t129-82.75V233q0-22.958 15.647-37.479t39-14.521Q503 181 519 195.521T535 233v28.5q80 17 129.75 82.75T714.5 491v306.5H768q12.25 0 20.625 8.463T797 826.175q0 12.325-8.375 20.575T768 855H192Zm288-292.5Zm-.25 410q-31.75 0-55.25-23.25T401 893.5h158q0 32.5-23.25 55.75t-56 23.25Zm-175.25-175H657V491.095Q657 416 606.5 364.75t-125-51.25q-74.294 0-125.647 51.353Q304.5 416.206 304.5 491v306.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications_off.svg index 556013c364..ca1de72e82 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications_off.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications_off.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M192 855q-12.25 0-20.375-8.535t-8.125-20.39q0-11.856 8.125-20.216Q179.75 797.5 192 797.5h55V482q0-29.098 8.25-57.049T278.5 371.5l43 43q-8 16.5-12.5 33.025t-4.5 34.551V797.5l319.5-1-523-522q-9-8.5-9-19.75t8.75-20.25q8.75-9 20-9T141 234l678.404 679.404Q828 922 827.75 933.483q-.25 11.484-8.25 20.017-8.989 8.5-20.244 8.5-11.256 0-19.787-8.524L681 855H192Zm522.5-129L657 668.5V491q0-74.794-50.5-126.147Q556 313.5 482.355 313.5q-34.855 0-67.855 11.75-33 11.75-60 41.25L313 325q28.5-27 55.84-41.798 27.341-14.797 56.16-21.94v-24.758q0-22.89 16.015-38.947 16.014-16.057 38.97-16.057t38.986 16.057Q535 213.614 535 236.594v25.108Q612.5 278 663.5 342.75t51 148.25v235Zm-250-88.5Zm16 335q-32.5 0-55.75-23.25T401.5 893.5h158q0 32.5-23.25 55.75T480.5 972.5ZM506 517Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M192 855q-12.25 0-20.375-8.535t-8.125-20.39q0-11.856 8.125-20.216Q179.75 797.5 192 797.5h55V482q0-29.098 8.25-57.049T278.5 371.5l43 43q-8 16.5-12.5 33.025t-4.5 34.551V797.5l319.5-1-523-522q-9-8.5-9-19.75t8.75-20.25q8.75-9 20-9T141 234l678.404 679.404Q828 922 827.75 933.483q-.25 11.484-8.25 20.017-8.989 8.5-20.244 8.5-11.256 0-19.787-8.524L681 855H192Zm522.5-129L657 668.5V491q0-74.794-50.5-126.147Q556 313.5 482.355 313.5q-34.855 0-67.855 11.75-33 11.75-60 41.25L313 325q28.5-27 55.84-41.798 27.341-14.797 56.16-21.94v-24.758q0-22.89 16.015-38.947 16.014-16.057 38.97-16.057t38.986 16.057Q535 213.614 535 236.594v25.108Q612.5 278 663.5 342.75t51 148.25v235Zm-250-88.5Zm16 335q-32.5 0-55.75-23.25T401.5 893.5h158q0 32.5-23.25 55.75T480.5 972.5ZM506 517Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications_off_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications_off_filled.svg index 647ad7dd3d..98a24503d7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications_off_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications_off_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M779.5 953.5 681 855H192q-12.5 0-20.5-8.359-8-8.36-8-20.391 0-12.031 8-20.391 8-8.359 20.5-8.359h55V491q0-15.636 2.5-33.568t6-28.932L101.096 274.629Q92 265.5 92 254.5q0-11 8.75-20t20.25-9q11.5 0 20.094 8.594l678.312 679.312Q828 922 828 933.5q0 11.5-8.5 20-8.545 8.5-20.023 8.5-11.477 0-19.977-8.5Zm-299 19q-32.5 0-55.75-23.25T401.5 893.5h158q0 32.5-23.25 55.75T480.5 972.5Zm234-246.5L313 325q28.5-27 55.75-41.649Q396 268.702 425 261.631v-28.833Q425 210.5 441.542 196q16.541-14.5 38.958-14.5 22 0 38.25 14.5T535 232.842v28.884Q612.5 278 663.5 342.75t51 148.25v235Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M779.5 953.5 681 855H192q-12.5 0-20.5-8.359-8-8.36-8-20.391 0-12.031 8-20.391 8-8.359 20.5-8.359h55V491q0-15.636 2.5-33.568t6-28.932L101.096 274.629Q92 265.5 92 254.5q0-11 8.75-20t20.25-9q11.5 0 20.094 8.594l678.312 679.312Q828 922 828 933.5q0 11.5-8.5 20-8.545 8.5-20.023 8.5-11.477 0-19.977-8.5Zm-299 19q-32.5 0-55.75-23.25T401.5 893.5h158q0 32.5-23.25 55.75T480.5 972.5Zm234-246.5L313 325q28.5-27 55.75-41.649Q396 268.702 425 261.631v-28.833Q425 210.5 441.542 196q16.541-14.5 38.958-14.5 22 0 38.25 14.5T535 232.842v28.884Q612.5 278 663.5 342.75t51 148.25v235Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_open_in_new.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_open_in_new.svg index 9669ee635e..0edc51abef 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_open_in_new.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_open_in_new.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M182 931.5q-22.969 0-40.234-17.266Q124.5 896.969 124.5 874V278q0-22.969 17.266-40.234Q159.031 220.5 182 220.5h247q11.675 0 20.087 8.463 8.413 8.463 8.413 20.212 0 12.325-8.413 20.575Q440.675 278 429 278H182v596h596V627q0-11.675 8.425-20.088 8.426-8.412 20.5-8.412 12.075 0 20.325 8.412 8.25 8.413 8.25 20.088v247q0 22.969-17.266 40.234Q800.969 931.5 778 931.5H182Zm181.13-238.891Q355 684.5 354.5 673.25q-.5-11.25 8.438-20.188L738 278H544q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.5 0-12.075 8.375-20.325T544 220.5h263q12.475 0 20.487 8.013Q835.5 236.525 835.5 249v263q0 12.25-8.463 20.625T806.825 541q-12.325 0-20.575-8.375T778 512V319L403 694q-8.326 8-19.913 8t-19.957-9.391Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M182 931.5q-22.969 0-40.234-17.266Q124.5 896.969 124.5 874V278q0-22.969 17.266-40.234Q159.031 220.5 182 220.5h247q11.675 0 20.087 8.463 8.413 8.463 8.413 20.212 0 12.325-8.413 20.575Q440.675 278 429 278H182v596h596V627q0-11.675 8.425-20.088 8.426-8.412 20.5-8.412 12.075 0 20.325 8.412 8.25 8.413 8.25 20.088v247q0 22.969-17.266 40.234Q800.969 931.5 778 931.5H182Zm181.13-238.891Q355 684.5 354.5 673.25q-.5-11.25 8.438-20.188L738 278H544q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.5 0-12.075 8.375-20.325T544 220.5h263q12.475 0 20.487 8.013Q835.5 236.525 835.5 249v263q0 12.25-8.463 20.625T806.825 541q-12.325 0-20.575-8.375T778 512V319L403 694q-8.326 8-19.913 8t-19.957-9.391Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_outbound.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_outbound.svg new file mode 100644 index 0000000000..762c5846c1 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_outbound.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="m346-306 235.5-235v125.5H639V-639H415.5v57.5H540L306-347l40 41ZM480.06-85q-80.97 0-153.13-31.26-72.15-31.27-125.79-85Q147.5-255 116.25-327.02 85-399.05 85-479.94q0-81.97 31.26-154.13 31.27-72.15 85-125.54Q255-813 327.02-844q72.03-31 152.92-31 81.97 0 154.13 31.13 72.17 31.13 125.55 84.5Q813-706 844-633.98q31 72.03 31 153.92 0 80.97-31.01 153.13-31.02 72.15-84.5 125.79Q706-147.5 633.98-116.25 561.95-85 480.06-85Zm-.09-57.5q140.53 0 239.03-98.97 98.5-98.96 98.5-238.5 0-140.53-98.47-239.03-98.46-98.5-239-98.5-139.53 0-238.53 98.47-99 98.46-99 239 0 139.53 98.97 238.53 98.96 99 238.5 99ZM480-480Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_outline_terminal.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_outline_terminal.svg index 07f8078e0c..f384ef36bd 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_outline_terminal.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_outline_terminal.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M142.5-164.5q-22.969 0-40.234-17.266Q85-199.031 85-222v-516q0-22.969 17.266-40.234Q119.531-795.5 142.5-795.5h675q22.969 0 40.234 17.266Q875-760.969 875-738v516q0 22.969-17.266 40.234Q840.469-164.5 817.5-164.5h-675Zm0-57.5h675v-433h-675v433ZM363-438.5l-84.068-84.068q-8.432-8.432-8.168-19.977.265-11.546 9.113-20.433 8.848-8.522 20.311-8.272 11.464.25 20.312 8.75L424-459q8.5 8.727 8.5 20.364 0 11.636-8.5 20.136l-104 104q-8.5 9-20 8.75t-19.978-8.728Q271-323 271-334.25q0-11.25 9-20.25l83-84ZM520-290q-12.5 0-20.5-8t-8-20.5q0-12.5 8-20.75t20.5-8.25h160q12.5 0 20.75 8.25T709-318.5q0 12.5-8.25 20.5T680-290H520Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M142.5-164.5q-22.969 0-40.234-17.266Q85-199.031 85-222v-516q0-22.969 17.266-40.234Q119.531-795.5 142.5-795.5h675q22.969 0 40.234 17.266Q875-760.969 875-738v516q0 22.969-17.266 40.234Q840.469-164.5 817.5-164.5h-675Zm0-57.5h675v-433h-675v433ZM363-438.5l-84.068-84.068q-8.432-8.432-8.168-19.977.265-11.546 9.113-20.433 8.848-8.522 20.311-8.272 11.464.25 20.312 8.75L424-459q8.5 8.727 8.5 20.364 0 11.636-8.5 20.136l-104 104q-8.5 9-20 8.75t-19.978-8.728Q271-323 271-334.25q0-11.25 9-20.25l83-84ZM520-290q-12.5 0-20.5-8t-8-20.5q0-12.5 8-20.75t20.5-8.25h160q12.5 0 20.75 8.25T709-318.5q0 12.5-8.25 20.5T680-290H520Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_palette.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_palette.svg new file mode 100644 index 0000000000..a0da2dcca5 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_palette.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22" fill="#5f6368"> + <path + d="M480-85q-81 0-153.05-31.16-72.05-31.16-125.84-84.95-53.79-53.79-84.95-125.84Q85-399 85-480.01 85-564 116.75-636q31.75-72 86.5-125.25t128.25-83.5Q405-875 488.8-875q78.41 0 148.31 26.25Q707-822.5 759.99-776.37q52.99 46.13 84 110Q875-602.5 875-527q0 106.5-61.75 168t-164.66 61.5H574.5q-19 0-32.5 14.25T528.5-251q0 27.45 14.5 47.22 14.5 19.78 14.5 45.28 0 34-19.75 53.75T480-85Zm0-395Zm-231.5 24.5q19.7 0 34.1-14.4Q297-484.3 297-504q0-19.7-14.4-34.1-14.4-14.4-34.1-14.4-19.7 0-34.1 14.4Q200-523.7 200-504q0 19.7 14.4 34.1 14.4 14.4 34.1 14.4Zm125.5-169q19.7 0 34.1-14.4 14.4-14.4 14.4-34.1 0-19.7-14.4-34.1-14.4-14.4-34.1-14.4-19.7 0-34.1 14.4-14.4 14.4-14.4 34.1 0 19.7 14.4 34.1 14.4 14.4 34.1 14.4Zm212.5 0q19.7 0 34.1-14.4Q635-653.3 635-673q0-19.7-14.4-34.1-14.4-14.4-34.1-14.4-19.7 0-34.1 14.4Q538-692.7 538-673q0 19.7 14.4 34.1 14.4 14.4 34.1 14.4Zm130 169q19.7 0 34.1-14.4Q765-484.3 765-504q0-19.7-14.4-34.1-14.4-14.4-34.1-14.4-19.7 0-34.1 14.4Q668-523.7 668-504q0 19.7 14.4 34.1 14.4 14.4 34.1 14.4Zm-236.34 313q10.84 0 15.34-4.5t4.5-14.5q0-14-14.25-26.25T471.5-240.5q0-45.5 29.75-80T576.5-355h72q76 0 122.5-44.25T817.5-527q0-131-99.25-210.75T488.91-817.5q-144.91 0-245.66 98.05Q142.5-621.41 142.5-480q0 140 98.75 238.75t238.91 98.75Z" /> +</svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pause_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pause_filled.svg index eaa4573780..dc742eb7e8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pause_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pause_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M612.5 852q-24.188 0-40.844-16.891Q555 818.219 555 794.5v-437q0-23.719 16.656-40.609Q588.312 300 612.5 300H670q23.719 0 40.609 16.891Q727.5 333.781 727.5 357.5v437q0 23.719-16.891 40.609Q693.719 852 670 852h-57.5Zm-322 0q-24.188 0-40.844-16.891Q233 818.219 233 794.5v-437q0-23.719 16.656-40.609Q266.312 300 290.5 300H348q23.719 0 40.609 16.891Q405.5 333.781 405.5 357.5v437q0 23.719-16.891 40.609Q371.719 852 348 852h-57.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M612.5 852q-24.188 0-40.844-16.891Q555 818.219 555 794.5v-437q0-23.719 16.656-40.609Q588.312 300 612.5 300H670q23.719 0 40.609 16.891Q727.5 333.781 727.5 357.5v437q0 23.719-16.891 40.609Q693.719 852 670 852h-57.5Zm-322 0q-24.188 0-40.844-16.891Q233 818.219 233 794.5v-437q0-23.719 16.656-40.609Q266.312 300 290.5 300H348q23.719 0 40.609 16.891Q405.5 333.781 405.5 357.5v437q0 23.719-16.891 40.609Q371.719 852 348 852h-57.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pending.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pending.svg index 90b9dc7204..7ee6422616 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pending.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pending.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M267.529 624.5q20.221 0 34.346-14.154Q316 596.191 316 575.971q0-20.221-14.154-34.346-14.155-14.125-34.375-14.125-20.221 0-34.346 14.154Q219 555.809 219 576.029q0 20.221 14.154 34.346 14.155 14.125 34.375 14.125Zm212.5 0q20.221 0 34.346-14.154 14.125-14.155 14.125-34.375 0-20.221-14.154-34.346-14.155-14.125-34.375-14.125-20.221 0-34.346 14.154-14.125 14.155-14.125 34.375 0 20.221 14.154 34.346 14.155 14.125 34.375 14.125Zm211.5 0q20.221 0 34.346-14.154Q740 596.191 740 575.971q0-20.221-14.154-34.346-14.155-14.125-34.375-14.125-20.221 0-34.346 14.154Q643 555.809 643 576.029q0 20.221 14.154 34.346 14.155 14.125 34.375 14.125ZM480.33 971q-81.704 0-153.629-31.263t-125.563-85Q147.5 801 116.25 729.136 85 657.272 85 575.564q0-81.789 31.263-153.789 31.263-71.999 85-125.387Q255 243 326.864 212q71.864-31 153.572-31 81.789 0 153.795 31.132 72.005 31.131 125.387 84.5Q813 350 844 422.023q31 72.023 31 153.647 0 81.705-31.013 153.629-31.013 71.925-84.5 125.379-53.487 53.454-125.51 84.888Q561.954 971 480.33 971Zm.137-57.5Q620.5 913.5 719 814.533t98.5-239Q817.5 435.5 719.221 337q-98.28-98.5-239.188-98.5-139.533 0-238.533 98.279-99 98.28-99 239.188 0 139.533 98.967 238.533t239 99ZM480 576Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M267.529 624.5q20.221 0 34.346-14.154Q316 596.191 316 575.971q0-20.221-14.154-34.346-14.155-14.125-34.375-14.125-20.221 0-34.346 14.154Q219 555.809 219 576.029q0 20.221 14.154 34.346 14.155 14.125 34.375 14.125Zm212.5 0q20.221 0 34.346-14.154 14.125-14.155 14.125-34.375 0-20.221-14.154-34.346-14.155-14.125-34.375-14.125-20.221 0-34.346 14.154-14.125 14.155-14.125 34.375 0 20.221 14.154 34.346 14.155 14.125 34.375 14.125Zm211.5 0q20.221 0 34.346-14.154Q740 596.191 740 575.971q0-20.221-14.154-34.346-14.155-14.125-34.375-14.125-20.221 0-34.346 14.154Q643 555.809 643 576.029q0 20.221 14.154 34.346 14.155 14.125 34.375 14.125ZM480.33 971q-81.704 0-153.629-31.263t-125.563-85Q147.5 801 116.25 729.136 85 657.272 85 575.564q0-81.789 31.263-153.789 31.263-71.999 85-125.387Q255 243 326.864 212q71.864-31 153.572-31 81.789 0 153.795 31.132 72.005 31.131 125.387 84.5Q813 350 844 422.023q31 72.023 31 153.647 0 81.705-31.013 153.629-31.013 71.925-84.5 125.379-53.487 53.454-125.51 84.888Q561.954 971 480.33 971Zm.137-57.5Q620.5 913.5 719 814.533t98.5-239Q817.5 435.5 719.221 337q-98.28-98.5-239.188-98.5-139.533 0-238.533 98.279-99 98.28-99 239.188 0 139.533 98.967 238.533t239 99ZM480 576Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pending_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pending_filled.svg index 00fe0d41a7..eee4c2c323 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pending_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pending_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M267.529 624.5q20.221 0 34.346-14.154Q316 596.191 316 575.971q0-20.221-14.154-34.346-14.155-14.125-34.375-14.125-20.221 0-34.346 14.154Q219 555.809 219 576.029q0 20.221 14.154 34.346 14.155 14.125 34.375 14.125Zm212.5 0q20.221 0 34.346-14.154 14.125-14.155 14.125-34.375 0-20.221-14.154-34.346-14.155-14.125-34.375-14.125-20.221 0-34.346 14.154-14.125 14.155-14.125 34.375 0 20.221 14.154 34.346 14.155 14.125 34.375 14.125Zm211.5 0q20.221 0 34.346-14.154Q740 596.191 740 575.971q0-20.221-14.154-34.346-14.155-14.125-34.375-14.125-20.221 0-34.346 14.154Q643 555.809 643 576.029q0 20.221 14.154 34.346 14.155 14.125 34.375 14.125ZM480.33 971q-81.704 0-153.629-31.263t-125.563-85Q147.5 801 116.25 729.136 85 657.272 85 575.564q0-81.789 31.263-153.789 31.263-71.999 85-125.387Q255 243 326.864 212q71.864-31 153.572-31 81.789 0 153.795 31.132 72.005 31.131 125.387 84.5Q813 350 844 422.023q31 72.023 31 153.647 0 81.705-31.013 153.629-31.013 71.925-84.5 125.379-53.487 53.454-125.51 84.888Q561.954 971 480.33 971Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M267.529 624.5q20.221 0 34.346-14.154Q316 596.191 316 575.971q0-20.221-14.154-34.346-14.155-14.125-34.375-14.125-20.221 0-34.346 14.154Q219 555.809 219 576.029q0 20.221 14.154 34.346 14.155 14.125 34.375 14.125Zm212.5 0q20.221 0 34.346-14.154 14.125-14.155 14.125-34.375 0-20.221-14.154-34.346-14.155-14.125-34.375-14.125-20.221 0-34.346 14.154-14.125 14.155-14.125 34.375 0 20.221 14.154 34.346 14.155 14.125 34.375 14.125Zm211.5 0q20.221 0 34.346-14.154Q740 596.191 740 575.971q0-20.221-14.154-34.346-14.155-14.125-34.375-14.125-20.221 0-34.346 14.154Q643 555.809 643 576.029q0 20.221 14.154 34.346 14.155 14.125 34.375 14.125ZM480.33 971q-81.704 0-153.629-31.263t-125.563-85Q147.5 801 116.25 729.136 85 657.272 85 575.564q0-81.789 31.263-153.789 31.263-71.999 85-125.387Q255 243 326.864 212q71.864-31 153.572-31 81.789 0 153.795 31.132 72.005 31.131 125.387 84.5Q813 350 844 422.023q31 72.023 31 153.647 0 81.705-31.013 153.629-31.013 71.925-84.5 125.379-53.487 53.454-125.51 84.888Q561.954 971 480.33 971Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person.svg index 0830a9963b..0a87c6589e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M480.022 575.5q-65.522 0-106.522-40.978t-41-106.5q0-65.522 40.978-106.522t106.5-41q65.522 0 106.522 40.978t41 106.5q0 65.522-40.978 106.522t-106.5 41ZM738 889.5H222q-23.719 0-40.609-16.891Q164.5 855.719 164.5 832v-33.515q0-37.485 18.75-63.985t48.431-40.174Q298 665 358.5 650T480 635q61 0 121 15.25t126.399 44.433q30.823 13.637 49.462 39.854Q795.5 760.753 795.5 798.47v33.768q0 23.45-16.891 40.356Q761.719 889.5 738 889.5ZM222 832h516v-33.372q0-16.318-9.75-30.973Q718.5 753 705 746q-64-30.5-116.294-42t-108.575-11.5Q423.5 692.5 370.5 704t-116 42q-14 7-23.25 21.729T222 798.738V832Zm258-314q39 0 64.5-25.5T570 428q0-39-25.5-64.5T480 338q-39 0-64.5 25.5T390 428q0 39 25.5 64.5T480 518Zm0-90Zm0 404Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M480.022 575.5q-65.522 0-106.522-40.978t-41-106.5q0-65.522 40.978-106.522t106.5-41q65.522 0 106.522 40.978t41 106.5q0 65.522-40.978 106.522t-106.5 41ZM738 889.5H222q-23.719 0-40.609-16.891Q164.5 855.719 164.5 832v-33.515q0-37.485 18.75-63.985t48.431-40.174Q298 665 358.5 650T480 635q61 0 121 15.25t126.399 44.433q30.823 13.637 49.462 39.854Q795.5 760.753 795.5 798.47v33.768q0 23.45-16.891 40.356Q761.719 889.5 738 889.5ZM222 832h516v-33.372q0-16.318-9.75-30.973Q718.5 753 705 746q-64-30.5-116.294-42t-108.575-11.5Q423.5 692.5 370.5 704t-116 42q-14 7-23.25 21.729T222 798.738V832Zm258-314q39 0 64.5-25.5T570 428q0-39-25.5-64.5T480 338q-39 0-64.5 25.5T390 428q0 39 25.5 64.5T480 518Zm0-90Zm0 404Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_add.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_add.svg index 4b2a249f23..4e22be2b55 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_add.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_add.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M757.325 654.5Q745 654.5 737 646.125t-8-20.625v-100H628.5q-12.25 0-20.375-8.535T600 496.575q0-11.856 8.125-20.216Q616.25 468 628.5 468H729V367.5q0-11.675 8.175-20.088 8.176-8.412 20.5-8.412 12.325 0 20.575 8.412 8.25 8.413 8.25 20.088V468h100q12.25 0 20.625 8.463t8.375 20.212q0 12.325-8.375 20.575T886.5 525.5h-100v100q0 12.25-8.425 20.625-8.426 8.375-20.75 8.375Zm-397.075-79Q295 575.5 254 534.522t-41-106.5q0-65.522 40.984-106.522 40.985-41 106.516-41 65 0 106.25 40.978t41.25 106.5q0 65.522-41.25 106.522t-106.5 41ZM73.5 889.5q-12.475 0-20.487-8.013Q45 873.475 45 861v-62.545q0-34.312 17.523-62.236 17.524-27.924 49.499-41.73Q186 662 243.338 648.5q57.339-13.5 116.75-13.5 59.412 0 116.836 13.5 57.423 13.5 131.054 45.989Q639.5 709 657.75 736.49T676 798.371V861q0 12.475-8.194 20.487-8.194 8.013-20.306 8.013h-574Zm29-57.5h516v-33.262q0-16.28-8.25-31.009T585 746q-70-33.5-118.67-43.5t-105.825-10Q303 692.5 254.25 703q-48.75 10.5-119.75 43-15 7-23.5 21.729t-8.5 31.009V832Zm258-314q39 0 64.5-25.5t25.5-64.5q0-39-25.5-64.5T360.5 338q-39.5 0-64.75 25.5T270.5 428q0 39 25.25 64.5T360.5 518Zm0-90Zm0 264.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M757.325 654.5Q745 654.5 737 646.125t-8-20.625v-100H628.5q-12.25 0-20.375-8.535T600 496.575q0-11.856 8.125-20.216Q616.25 468 628.5 468H729V367.5q0-11.675 8.175-20.088 8.176-8.412 20.5-8.412 12.325 0 20.575 8.412 8.25 8.413 8.25 20.088V468h100q12.25 0 20.625 8.463t8.375 20.212q0 12.325-8.375 20.575T886.5 525.5h-100v100q0 12.25-8.425 20.625-8.426 8.375-20.75 8.375Zm-397.075-79Q295 575.5 254 534.522t-41-106.5q0-65.522 40.984-106.522 40.985-41 106.516-41 65 0 106.25 40.978t41.25 106.5q0 65.522-41.25 106.522t-106.5 41ZM73.5 889.5q-12.475 0-20.487-8.013Q45 873.475 45 861v-62.545q0-34.312 17.523-62.236 17.524-27.924 49.499-41.73Q186 662 243.338 648.5q57.339-13.5 116.75-13.5 59.412 0 116.836 13.5 57.423 13.5 131.054 45.989Q639.5 709 657.75 736.49T676 798.371V861q0 12.475-8.194 20.487-8.194 8.013-20.306 8.013h-574Zm29-57.5h516v-33.262q0-16.28-8.25-31.009T585 746q-70-33.5-118.67-43.5t-105.825-10Q303 692.5 254.25 703q-48.75 10.5-119.75 43-15 7-23.5 21.729t-8.5 31.009V832Zm258-314q39 0 64.5-25.5t25.5-64.5q0-39-25.5-64.5T360.5 338q-39.5 0-64.75 25.5T270.5 428q0 39 25.25 64.5T360.5 518Zm0-90Zm0 264.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_add_500.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_add_500.svg index 179dce95fb..1c67f0813a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_add_500.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_add_500.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M762.107-409.152q-13.137 0-21.448-8.788-8.311-8.787-8.311-21.647v-97.131h-97.63q-13.064 0-21.499-8.947-8.436-8.947-8.436-21.413 0-12.669 8.436-21.339 8.435-8.67 21.499-8.67h97.63v-97.631q0-12.488 8.486-21.211 8.487-8.723 21.505-8.723 13.018 0 21.698 8.723 8.681 8.723 8.681 21.211v97.631h97.13q12.86 0 21.647 8.774 8.788 8.774 8.788 21.217 0 13.018-8.788 21.698-8.787 8.68-21.647 8.68h-97.13v97.131q0 12.86-8.838 21.647-8.838 8.788-21.773 8.788Zm-400.661-74.457q-68.048 0-110.758-42.603-42.71-42.603-42.71-110.804 0-68.202 42.66-110.876 42.659-42.674 110.939-42.674 67.749 0 110.673 42.652 42.924 42.652 42.924 110.853 0 68.202-42.89 110.827-42.889 42.625-110.838 42.625ZM72.543-157.413q-14.149 0-23.357-9.208t-9.208-23.357v-64.841q0-36.154 18.527-65.345 18.526-29.19 51.843-43.977 74.456-32.729 132.502-46.348 58.046-13.62 118.434-13.62 60.368 0 118.509 13.62 58.141 13.619 132.011 46.348 33.039 15.186 52.205 43.96 19.165 28.774 19.165 65.183v65.02q0 14.149-9.389 23.357-9.39 9.208-23.176 9.208H72.543Zm33.305-65.63h511.695v-30.632q0-16.139-8.369-30.752-8.37-14.613-25.131-21.508Q515-338.717 466.929-348.478q-48.072-9.761-105.347-9.761-57.147 0-105.658 10.261t-118.076 42.043q-15.239 6.895-23.62 21.508-8.38 14.613-8.38 30.752v30.632Zm255.72-326.196q38.171 0 63.073-24.866 24.902-24.865 24.902-62.974 0-38.207-24.865-63.031-24.865-24.825-62.974-24.825-38.791 0-63.324 24.836-24.532 24.835-24.532 62.902 0 38.165 24.524 63.061 24.524 24.897 63.196 24.897Zm.128-87.848Zm0 278.848Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M762.107-409.152q-13.137 0-21.448-8.788-8.311-8.787-8.311-21.647v-97.131h-97.63q-13.064 0-21.499-8.947-8.436-8.947-8.436-21.413 0-12.669 8.436-21.339 8.435-8.67 21.499-8.67h97.63v-97.631q0-12.488 8.486-21.211 8.487-8.723 21.505-8.723 13.018 0 21.698 8.723 8.681 8.723 8.681 21.211v97.631h97.13q12.86 0 21.647 8.774 8.788 8.774 8.788 21.217 0 13.018-8.788 21.698-8.787 8.68-21.647 8.68h-97.13v97.131q0 12.86-8.838 21.647-8.838 8.788-21.773 8.788Zm-400.661-74.457q-68.048 0-110.758-42.603-42.71-42.603-42.71-110.804 0-68.202 42.66-110.876 42.659-42.674 110.939-42.674 67.749 0 110.673 42.652 42.924 42.652 42.924 110.853 0 68.202-42.89 110.827-42.889 42.625-110.838 42.625ZM72.543-157.413q-14.149 0-23.357-9.208t-9.208-23.357v-64.841q0-36.154 18.527-65.345 18.526-29.19 51.843-43.977 74.456-32.729 132.502-46.348 58.046-13.62 118.434-13.62 60.368 0 118.509 13.62 58.141 13.619 132.011 46.348 33.039 15.186 52.205 43.96 19.165 28.774 19.165 65.183v65.02q0 14.149-9.389 23.357-9.39 9.208-23.176 9.208H72.543Zm33.305-65.63h511.695v-30.632q0-16.139-8.369-30.752-8.37-14.613-25.131-21.508Q515-338.717 466.929-348.478q-48.072-9.761-105.347-9.761-57.147 0-105.658 10.261t-118.076 42.043q-15.239 6.895-23.62 21.508-8.38 14.613-8.38 30.752v30.632Zm255.72-326.196q38.171 0 63.073-24.866 24.902-24.865 24.902-62.974 0-38.207-24.865-63.031-24.865-24.825-62.974-24.825-38.791 0-63.324 24.836-24.532 24.835-24.532 62.902 0 38.165 24.524 63.061 24.524 24.897 63.196 24.897Zm.128-87.848Zm0 278.848Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_edit.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_edit.svg new file mode 100644 index 0000000000..7082a9c0d4 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_edit.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M500-224Zm-335.5 57.5v-91q0-37.75 18.75-64.12Q202-348 231.68-361.67 298-391 358.5-406t121.28-15q37.97 0 74.55 6t74.17 17l-45.43 45q-26.57-5.5-51.47-8-24.89-2.5-51.36-2.5-56.74 0-109.74 11.5-53 11.5-116 42-14 7-23.25 21.73T222-257.26V-224h278v57.5H164.5Zm393 41.5v-121.5L778-466q9-8.5 19.75-12.5 10.76-4 21.51-4 11.73 0 22.49 4.25Q852.5-474 861.5-465l37 37q8.76 8.85 12.63 19.68Q915-397.5 915-386.75t-4.38 22.03q-4.38 11.28-13.05 19.74L679-125H557.5Zm299-262-37-37 37 37Zm-240 203h37.76L776.5-307l-17.89-19-18.88-18L616.5-222v38Zm142-142-19-18 37 37-18-19ZM480-480.5q-62 0-104.75-42.75T332.5-628q0-62 42.75-104.75T480-775.5q62 0 104.75 42.75T627.5-628q0 62-42.75 104.75T480-480.5Zm0-57.5q38 0 64-26t26-64q0-38-26-64t-64-26q-38 0-64 26t-26 64q0 38 26 64t64 26Zm0-90Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_filled.svg new file mode 100644 index 0000000000..0ed867b156 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_filled.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M480-479q-64.5 0-109.75-45.25T325-634q0-64.5 45.25-109.75T480-789q64.5 0 109.75 45.25T635-634q0 64.5-45.25 109.75T480-479ZM169-248v-31.03q0-32.97 16.75-60.22t45.27-41.76Q292-411 354.25-426.25 416.5-441.5 480-441.5t125.75 15.25Q668-411 728.98-381.01q28.52 14.51 45.27 41.76Q791-312 791-279.03V-248q0 30.94-22.03 52.97Q746.94-173 716-173H244q-30.94 0-52.97-22.03Q169-217.06 169-248Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_off.svg new file mode 100644 index 0000000000..54cfd3e1c0 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_off.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22" fill="#000000"><path d="m790.5-88-79-78.5H222q-23.97 0-40.73-16.77Q164.5-200.03 164.5-224v-33.52q0-37.48 18.75-63.98t48.25-40q60-27 115.25-41.75T458-420L87.09-790.91q-8.59-8.59-8.34-20.59t9-20.5q8.75-8.5 20.75-8.5t20.61 8.61l702.78 703.28q8.61 8.43 8.61 20.27T831.75-88Q823-79.5 811-79.5T790.5-88ZM222-224h432L515.48-362.5q-8.48-.5-17.48-.75t-17.98-.25q-56.54 0-109.53 11.5T254.5-310q-14 7-23.25 21.73T222-257.26V-224Zm505.5-137.5q31 13.5 49.5 40.25T795.5-257v9.5L652-391q18 6 37 13.75t38.5 15.75ZM548-495l-46-45.8q30.23-7.2 49.11-31.2Q570-596 570-628q0-38-26-64t-64-26q-32 0-56 19t-31 49l-46-46q18.5-38 54.5-58.75T480-775.5q62 0 104.75 42.75T627.5-628q0 42.5-20.75 78.5T548-495Zm106 271H222h432ZM448-596Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_bluetooth_speaker.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_bluetooth_speaker.svg new file mode 100644 index 0000000000..48fe86d1ec --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_bluetooth_speaker.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M670-462.5v-163L569.5-525 544-550.5 667.5-674 544-791l25.5-25.5L670-716v-163h16.5l118 121-86 84 86 92.5-118 119H670Zm39.5-74 44-45-44-44v89Zm0-179.5 44-42-44-51.5v93.5Zm81 591Q677-125 558-180.5T338-338Q236-439 180.5-557.75T125-790.692q0-18.808 12.714-31.558Q150.429-835 169.5-835H306q14 0 23.75 9.75t13.75 24.75l26.929 123.641Q372-663.5 369.5-652q-2.5 11.5-10.229 19.226L261-533q26 44 54.688 81.658Q344.375-413.683 379-380q36.5 38 77.25 69.323Q497-279.353 542-255l95.544-98q9.456-10.5 21.357-14.25T682.5-369l117.362 25.438Q815-340 825-327.801q10 12.198 10 27.301v131q0 19.071-12.714 31.786Q809.571-125 790.5-125ZM232-585.5l81-82-23.5-110H183q1.5 41.5 13 88.25t36 103.75Zm364 358q40 19 88.166 31t93.334 14v-107l-102-21.5-79.5 83.5Zm-364-358Zm364 358Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_in_talk_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_in_talk_filled.svg index b43c7d9c15..45cbd61bbd 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_in_talk_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_in_talk_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M800.963 567.5q-10.463 0-18.769-7.531-8.305-7.531-9.694-19.469-14-102.5-87.25-175.75T509 277.5q-11.438-1.923-18.969-10.212Q482.5 259 482.5 247.5q0-12 8.5-20.75t21-6.75q125.5 14 214.5 103T830 537.5q1.5 13-7.319 21.5-8.82 8.5-21.718 8.5Zm-170.527 0q-9.436 0-17.936-6.5t-11-16.5q-10-35-35.75-60.5t-60.25-35.5q-10.5-3-16.75-10.25t-6.25-17.732q0-14.545 9.567-23.032Q501.633 389 515 392q53.007 12.653 91.427 51.323Q644.847 481.993 659 535q3 12.867-5.6 22.683-8.6 9.817-22.964 9.817ZM791 931q-119.5 0-238.5-59.25t-214-154.25q-95-95-154.5-214t-59.5-238.612q0-18.388 12.964-31.388 12.965-13 31.536-13h136.5q14 0 24.25 9.25T343 255l27 124q2 13-.75 24.5t-10.25 19l-99 100q56 93 125 161.25T542.5 801l95.5-98q9.5-10.5 21.25-14.75T683 687l117.5 25.5q15 3 25 15t10 28v131q0 18.571-12.964 31.536Q809.571 931 791 931Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M800.963 567.5q-10.463 0-18.769-7.531-8.305-7.531-9.694-19.469-14-102.5-87.25-175.75T509 277.5q-11.438-1.923-18.969-10.212Q482.5 259 482.5 247.5q0-12 8.5-20.75t21-6.75q125.5 14 214.5 103T830 537.5q1.5 13-7.319 21.5-8.82 8.5-21.718 8.5Zm-170.527 0q-9.436 0-17.936-6.5t-11-16.5q-10-35-35.75-60.5t-60.25-35.5q-10.5-3-16.75-10.25t-6.25-17.732q0-14.545 9.567-23.032Q501.633 389 515 392q53.007 12.653 91.427 51.323Q644.847 481.993 659 535q3 12.867-5.6 22.683-8.6 9.817-22.964 9.817ZM791 931q-119.5 0-238.5-59.25t-214-154.25q-95-95-154.5-214t-59.5-238.612q0-18.388 12.964-31.388 12.965-13 31.536-13h136.5q14 0 24.25 9.25T343 255l27 124q2 13-.75 24.5t-10.25 19l-99 100q56 93 125 161.25T542.5 801l95.5-98q9.5-10.5 21.25-14.75T683 687l117.5 25.5q15 3 25 15t10 28v131q0 18.571-12.964 31.536Q809.571 931 791 931Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_photo_camera.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_photo_camera.svg index aa99ac888a..ddb231b4bb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_photo_camera.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_photo_camera.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M479.5 786.5q71.75 0 119.625-47.875T647 619q0-71-47.875-118.75T479.5 452.5q-71.75 0-119.125 47.75T313 619q0 71.75 47.375 119.625T479.5 786.5Zm-337 145q-22.969 0-40.234-17.266Q85 896.969 85 874V364.5q0-21.969 17.266-39.734Q119.531 307 142.5 307h147l54.91-66.5q7.59-10 19.106-15 11.515-5 24.984-5h183q13.469 0 24.984 5 11.516 5 19.516 15l54.5 66.5h147q21.969 0 39.734 17.766Q875 342.531 875 364.5V874q0 22.969-17.766 40.234Q839.469 931.5 817.5 931.5h-675Zm675-57.5V364.5h-675V874h675ZM480 619.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M479.5 786.5q71.75 0 119.625-47.875T647 619q0-71-47.875-118.75T479.5 452.5q-71.75 0-119.125 47.75T313 619q0 71.75 47.375 119.625T479.5 786.5Zm-337 145q-22.969 0-40.234-17.266Q85 896.969 85 874V364.5q0-21.969 17.266-39.734Q119.531 307 142.5 307h147l54.91-66.5q7.59-10 19.106-15 11.515-5 24.984-5h183q13.469 0 24.984 5 11.516 5 19.516 15l54.5 66.5h147q21.969 0 39.734 17.766Q875 342.531 875 364.5V874q0 22.969-17.766 40.234Q839.469 931.5 817.5 931.5h-675Zm675-57.5V364.5h-675V874h675ZM480 619.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_play_arrow_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_play_arrow_filled.svg index 6b3ed2a581..9b5f303e35 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_play_arrow_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_play_arrow_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M367.5 820q-14.5 9.5-29.25 1t-14.75-26V350.5q0-17 14.75-25.5t29.25 1l349 223q13.5 9 13.5 24t-13.5 23.5L367.5 820Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M367.5 820q-14.5 9.5-29.25 1t-14.75-26V350.5q0-17 14.75-25.5t29.25 1l349 223q13.5 9 13.5 24t-13.5 23.5L367.5 820Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_power_settings_new.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_power_settings_new.svg index 0af51ad2ab..c125143e41 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_power_settings_new.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_power_settings_new.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M479.825-438.5q-12.325 0-20.325-8.375t-8-20.625v-344q0-11.675 8.175-20.088 8.176-8.412 20.5-8.412 12.325 0 20.575 8.412Q509-823.175 509-811.5v344q0 12.25-8.425 20.625-8.426 8.375-20.75 8.375Zm.243 316q-74.068 0-138.818-28-64.75-28-112.75-76t-76-112.704q-28-64.704-28-138.796 0-70 27.731-133.75 27.732-63.75 77.167-114.06 9.102-9.19 22.161-9.69 13.059-.5 21.777 8.321 9.164 9.295 7.414 21.237Q279-694 270.5-685.5 228-644 205-590.637q-23 53.364-23 112.761 0 124.233 86.937 211.055Q355.875-180 480.116-180t211.063-86.933Q778-353.867 778-478.102q0-59.398-22.394-112.74Q733.212-644.185 691.5-686q-9-9-10.25-21.25T689-727.5q9.5-9.5 22.75-7.75T735-723.81q48 50.31 74.25 113.346t26.25 132.492q0 73.972-28 138.722-28 64.75-76 112.75t-112.682 76q-64.683 28-138.75 28Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M479.825-438.5q-12.325 0-20.325-8.375t-8-20.625v-344q0-11.675 8.175-20.088 8.176-8.412 20.5-8.412 12.325 0 20.575 8.412Q509-823.175 509-811.5v344q0 12.25-8.425 20.625-8.426 8.375-20.75 8.375Zm.243 316q-74.068 0-138.818-28-64.75-28-112.75-76t-76-112.704q-28-64.704-28-138.796 0-70 27.731-133.75 27.732-63.75 77.167-114.06 9.102-9.19 22.161-9.69 13.059-.5 21.777 8.321 9.164 9.295 7.414 21.237Q279-694 270.5-685.5 228-644 205-590.637q-23 53.364-23 112.761 0 124.233 86.937 211.055Q355.875-180 480.116-180t211.063-86.933Q778-353.867 778-478.102q0-59.398-22.394-112.74Q733.212-644.185 691.5-686q-9-9-10.25-21.25T689-727.5q9.5-9.5 22.75-7.75T735-723.81q48 50.31 74.25 113.346t26.25 132.492q0 73.972-28 138.722-28 64.75-76 112.75t-112.682 76q-64.683 28-138.75 28Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_priority_high.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_priority_high.svg index 42b93fba83..408b4b4376 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_priority_high.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_priority_high.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M479.867 929q-27.367 0-47.367-20.133-20-20.133-20-47.75t20.133-47.367Q452.766 794 480.383 794t47.367 19.867q19.75 19.867 19.75 47.766Q547.5 889 527.633 909q-19.867 20-47.766 20Zm.044-234q-28.148 0-47.53-19.444Q413 656.112 413 628V287.5q0-28.112 19.47-47.556t47.619-19.444q28.148 0 47.53 19.444Q547 259.388 547 287.5V628q0 28.112-19.47 47.556T479.911 695Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M479.867 929q-27.367 0-47.367-20.133-20-20.133-20-47.75t20.133-47.367Q452.766 794 480.383 794t47.367 19.867q19.75 19.867 19.75 47.766Q547.5 889 527.633 909q-19.867 20-47.766 20Zm.044-234q-28.148 0-47.53-19.444Q413 656.112 413 628V287.5q0-28.112 19.47-47.556t47.619-19.444q28.148 0 47.53 19.444Q547 259.388 547 287.5V628q0 28.112-19.47 47.556T479.911 695Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_qr_code.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_qr_code.svg index d7f8763c87..621fe3c413 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_qr_code.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_qr_code.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M180 547q-23.719 0-40.609-16.656Q122.5 513.688 122.5 489.5V276q0-23.719 16.891-40.609Q156.281 218.5 180 218.5h214q23.719 0 40.609 16.891Q451.5 252.281 451.5 276v213.5q0 24.188-16.891 40.844Q417.719 547 394 547H180Zm0-57.5h214V276H180v213.5Zm0 444q-23.719 0-40.609-16.891Q122.5 899.719 122.5 876V662q0-23.719 16.891-40.609Q156.281 604.5 180 604.5h214q23.719 0 40.609 16.891Q451.5 638.281 451.5 662v214q0 23.719-16.891 40.609Q417.719 933.5 394 933.5H180Zm0-57.5h214V662H180v214Zm386.5-329q-24.188 0-40.844-16.656Q509 513.688 509 489.5V276q0-23.719 16.656-40.609Q542.312 218.5 566.5 218.5H780q23.719 0 40.609 16.891Q837.5 252.281 837.5 276v213.5q0 24.188-16.891 40.844Q803.719 547 780 547H566.5Zm0-57.5H780V276H566.5v213.5Zm204.048 444q-6.048 0-10.298-4.15T756 918.786v-52.572q0-5.714 4.15-10.214t10.564-4.5h52.238q6.048 0 10.298 4.5t4.25 10.214v52.655q0 6.131-4.15 10.381t-10.564 4.25h-52.238Zm-246.988-246q-6.06 0-10.31-4.15-4.25-4.15-4.25-10.564v-53.572q0-5.714 4.15-10.214t10.493-4.5h52.214q5.643 0 10.143 4.5 4.5 4.5 4.5 10.214v53.655q0 6.131-4.5 10.381t-10.143 4.25H523.56Zm81.488 81.5q-6.048 0-10.298-4.15t-4.25-10.564v-52.238q0-6.048 4.15-10.298t10.564-4.25h53.238q6.048 0 10.298 4.15t4.25 10.564v52.238q0 6.048-4.15 10.298T658.286 769h-53.238Zm-81.488 82.5q-6.06 0-10.31-4.15-4.25-4.15-4.25-10.493v-53.214q0-5.643 4.15-10.143 4.15-4.5 10.493-4.5h52.214q5.643 0 10.143 4.5 4.5 4.5 4.5 10.143v53.297q0 6.06-4.5 10.31-4.5 4.25-10.143 4.25H523.56Zm81.488 82q-6.048 0-10.298-4.15t-4.25-10.564v-52.572q0-5.714 4.15-10.214t10.564-4.5h53.238q6.048 0 10.298 4.5t4.25 10.214v52.655q0 6.131-4.15 10.381t-10.564 4.25h-53.238Zm82.583-82q-6.131 0-10.381-4.15T673 836.857v-53.214q0-5.643 4.15-10.143 4.15-4.5 10.564-4.5h53.572q5.714 0 10.214 4.5t4.5 10.143v53.297q0 6.06-4.5 10.31-4.5 4.25-10.214 4.25h-53.655Zm0-164q-6.131 0-10.381-4.15T673 672.786v-53.572q0-5.714 4.15-10.214t10.564-4.5h53.572q5.714 0 10.214 4.5t4.5 10.214v53.655q0 6.131-4.5 10.381t-10.214 4.25h-53.655Zm82.917 81.5q-6.048 0-10.298-4.15T756 754.286v-52.238q0-6.048 4.15-10.298t10.564-4.25h52.238q6.048 0 10.298 4.15t4.25 10.564v52.238q0 6.048-4.15 10.298T822.786 769h-52.238Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M180 547q-23.719 0-40.609-16.656Q122.5 513.688 122.5 489.5V276q0-23.719 16.891-40.609Q156.281 218.5 180 218.5h214q23.719 0 40.609 16.891Q451.5 252.281 451.5 276v213.5q0 24.188-16.891 40.844Q417.719 547 394 547H180Zm0-57.5h214V276H180v213.5Zm0 444q-23.719 0-40.609-16.891Q122.5 899.719 122.5 876V662q0-23.719 16.891-40.609Q156.281 604.5 180 604.5h214q23.719 0 40.609 16.891Q451.5 638.281 451.5 662v214q0 23.719-16.891 40.609Q417.719 933.5 394 933.5H180Zm0-57.5h214V662H180v214Zm386.5-329q-24.188 0-40.844-16.656Q509 513.688 509 489.5V276q0-23.719 16.656-40.609Q542.312 218.5 566.5 218.5H780q23.719 0 40.609 16.891Q837.5 252.281 837.5 276v213.5q0 24.188-16.891 40.844Q803.719 547 780 547H566.5Zm0-57.5H780V276H566.5v213.5Zm204.048 444q-6.048 0-10.298-4.15T756 918.786v-52.572q0-5.714 4.15-10.214t10.564-4.5h52.238q6.048 0 10.298 4.5t4.25 10.214v52.655q0 6.131-4.15 10.381t-10.564 4.25h-52.238Zm-246.988-246q-6.06 0-10.31-4.15-4.25-4.15-4.25-10.564v-53.572q0-5.714 4.15-10.214t10.493-4.5h52.214q5.643 0 10.143 4.5 4.5 4.5 4.5 10.214v53.655q0 6.131-4.5 10.381t-10.143 4.25H523.56Zm81.488 81.5q-6.048 0-10.298-4.15t-4.25-10.564v-52.238q0-6.048 4.15-10.298t10.564-4.25h53.238q6.048 0 10.298 4.15t4.25 10.564v52.238q0 6.048-4.15 10.298T658.286 769h-53.238Zm-81.488 82.5q-6.06 0-10.31-4.15-4.25-4.15-4.25-10.493v-53.214q0-5.643 4.15-10.143 4.15-4.5 10.493-4.5h52.214q5.643 0 10.143 4.5 4.5 4.5 4.5 10.143v53.297q0 6.06-4.5 10.31-4.5 4.25-10.143 4.25H523.56Zm81.488 82q-6.048 0-10.298-4.15t-4.25-10.564v-52.572q0-5.714 4.15-10.214t10.564-4.5h53.238q6.048 0 10.298 4.5t4.25 10.214v52.655q0 6.131-4.15 10.381t-10.564 4.25h-53.238Zm82.583-82q-6.131 0-10.381-4.15T673 836.857v-53.214q0-5.643 4.15-10.143 4.15-4.5 10.564-4.5h53.572q5.714 0 10.214 4.5t4.5 10.143v53.297q0 6.06-4.5 10.31-4.5 4.25-10.214 4.25h-53.655Zm0-164q-6.131 0-10.381-4.15T673 672.786v-53.572q0-5.714 4.15-10.214t10.564-4.5h53.572q5.714 0 10.214 4.5t4.5 10.214v53.655q0 6.131-4.5 10.381t-10.214 4.25h-53.655Zm82.917 81.5q-6.048 0-10.298-4.15T756 754.286v-52.238q0-6.048 4.15-10.298t10.564-4.25h52.238q6.048 0 10.298 4.15t4.25 10.564v52.238q0 6.048-4.15 10.298T822.786 769h-52.238Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_question_mark.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_question_mark.svg index 9c2e7e110b..4346924f9d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_question_mark.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_question_mark.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M432-332.5q1-70 17.75-104.25T511-509q40.5-35 62.25-68.25T595-648q0-46.5-30.5-76.5t-86-30q-52 0-80.5 29t-42 61l-81.5-36q23-59 75.25-99.25T478.5-840q100.5 0 154.25 55t53.75 134q0 47-21.25 88.5t-66.25 83q-48.5 45.5-58.5 70t-11 77H432Zm46.5 250q-27.5 0-47.5-20T411-150q0-28.5 20-48t47.5-19.5q28.5 0 48 19.5t19.5 48q0 27.5-19.5 47.5t-48 20Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M432-332.5q1-70 17.75-104.25T511-509q40.5-35 62.25-68.25T595-648q0-46.5-30.5-76.5t-86-30q-52 0-80.5 29t-42 61l-81.5-36q23-59 75.25-99.25T478.5-840q100.5 0 154.25 55t53.75 134q0 47-21.25 88.5t-66.25 83q-48.5 45.5-58.5 70t-11 77H432Zm46.5 250q-27.5 0-47.5-20T411-150q0-28.5 20-48t47.5-19.5q28.5 0 48 19.5t19.5 48q0 27.5-19.5 47.5t-48 20Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radio_button_unchecked.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radio_button_unchecked.svg new file mode 100644 index 0000000000..7a3a36dc89 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radio_button_unchecked.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22" fill="#5f6368"><path d="M480.06-85q-80.97 0-153.13-31.26-72.15-31.27-125.79-85Q147.5-255 116.25-327.02 85-399.05 85-479.94q0-81.97 31.26-154.13 31.27-72.15 85-125.54Q255-813 327.02-844q72.03-31 152.92-31 81.97 0 154.13 31.13 72.17 31.13 125.55 84.5Q813-706 844-633.98q31 72.03 31 153.92 0 80.97-31.01 153.13-31.02 72.15-84.5 125.79Q706-147.5 633.98-116.25 561.95-85 480.06-85Zm-.09-57.5q140.53 0 239.03-98.97 98.5-98.96 98.5-238.5 0-140.53-98.47-239.03-98.46-98.5-239-98.5-139.53 0-238.53 98.47-99 98.46-99 239 0 139.53 98.97 238.53 98.96 99 238.5 99ZM480-480Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_1_bar.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_1_bar.svg new file mode 100644 index 0000000000..a7dd37c4e6 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_1_bar.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg height="22" viewBox="-44.134 -43.207 588.823 585.294" width="22" xmlns="http://www.w3.org/2000/svg"> + <g id="Light-S" transform="matrix(8.710668563842773, 0, 0, 8.710668563842773, -85.06543731689453, 558.4567260742189)"> + <path d="M 15.124 -6.711 C 18.033 -6.711 20.399 -9.129 20.399 -11.986 C 20.399 -14.96 18.082 -17.295 15.124 -17.295 C 12.232 -17.295 9.766 -14.862 9.766 -11.889 C 9.766 -9.129 12.281 -6.711 15.124 -6.711 Z"/> + </g> +</svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_2_bar.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_2_bar.svg new file mode 100644 index 0000000000..cb6e344be9 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_2_bar.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg height="22" viewBox="-44.134 -43.207 588.823 585.294" width="22" xmlns="http://www.w3.org/2000/svg"> + <g id="Light-S" transform="matrix(8.710668563842773, 0, 0, 8.710668563842773, -85.06543731689453, 558.4567260742188)"> + <path d="M 15.124 -6.711 C 18.033 -6.711 20.399 -9.129 20.399 -11.986 C 20.399 -14.96 18.082 -17.295 15.124 -17.295 C 12.232 -17.295 9.766 -14.862 9.766 -11.889 C 9.766 -9.129 12.281 -6.711 15.124 -6.711 Z M 10.714 -30.066 C 10.714 -28.585 11.962 -27.352 13.428 -27.352 C 22.787 -27.352 30.392 -19.732 30.392 -10.388 C 30.392 -8.908 31.64 -7.675 33.106 -7.675 C 34.571 -7.675 35.819 -8.908 35.819 -10.388 C 35.819 -22.746 25.718 -32.764 13.428 -32.764 C 11.962 -32.764 10.714 -31.565 10.714 -30.066 Z"/> + </g> +</svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_3_bar.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_3_bar.svg new file mode 100644 index 0000000000..4bde8511f6 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_3_bar.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg height="22" viewBox="-44.134 -43.207 588.823 585.294" width="22" xmlns="http://www.w3.org/2000/svg"> + <g id="Light-S" transform="matrix(8.710668563842773, 0, 0, 8.710668563842773, -85.06543731689453, 558.4567260742188)"> + <path d="M 15.124 -6.711 C 18.033 -6.711 20.399 -9.129 20.399 -11.986 C 20.399 -14.96 18.082 -17.295 15.124 -17.295 C 12.232 -17.295 9.766 -14.862 9.766 -11.889 C 9.766 -9.129 12.281 -6.711 15.124 -6.711 Z M 10.714 -30.066 C 10.714 -28.585 11.962 -27.352 13.428 -27.352 C 22.787 -27.352 30.392 -19.732 30.392 -10.388 C 30.392 -8.908 31.64 -7.675 33.106 -7.675 C 34.571 -7.675 35.819 -8.908 35.819 -10.388 C 35.819 -22.746 25.718 -32.764 13.428 -32.764 C 11.962 -32.764 10.714 -31.565 10.714 -30.066 Z M 10.714 -45.251 C 10.714 -43.771 11.962 -42.538 13.428 -42.538 C 31.216 -42.538 45.578 -28.161 45.578 -10.388 C 45.578 -8.908 46.826 -7.675 48.291 -7.675 C 49.756 -7.675 51.004 -8.908 51.004 -10.388 C 51.004 -31.141 34.147 -47.95 13.428 -47.95 C 11.962 -47.95 10.714 -46.75 10.714 -45.251 Z"/> + </g> +</svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_4_bar.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_4_bar.svg new file mode 100644 index 0000000000..131569e9bc --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_4_bar.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg height="22" viewBox="-44.134 -43.207 588.823 585.294" width="22" xmlns="http://www.w3.org/2000/svg"> + <g id="Light-S" transform="matrix(8.710668563842773, 0, 0, 8.710668563842773, -85.06543731689453, 558.4567260742188)"> + <path d="M15.1235-6.71093C18.0332-6.71093 20.3989-9.1289 20.3989-11.9863C20.3989-14.96 18.082-17.2954 15.1235-17.2954C12.2324-17.2954 9.76562-14.8623 9.76562-11.8887C9.76562-9.1289 12.2813-6.71093 15.1235-6.71093ZM10.7143-30.0659C10.7143-28.5854 11.9624-27.3525 13.4277-27.3525C22.7871-27.3525 30.3921-19.7324 30.3921-10.3882C30.3921-8.90771 31.6401-7.6748 33.1055-7.6748C34.5708-7.6748 35.8189-8.90771 35.8189-10.3882C35.8189-22.7456 25.7178-32.7642 13.4277-32.7642C11.9624-32.7642 10.7143-31.5649 10.7143-30.0659ZM10.7143-45.2515C10.7143-43.771 11.9624-42.5381 13.4277-42.5381C31.2158-42.5381 45.5776-28.1611 45.5776-10.3882C45.5776-8.90771 46.8257-7.6748 48.291-7.6748C49.7564-7.6748 51.0044-8.90771 51.0044-10.3882C51.0044-31.1406 34.1465-47.9497 13.4277-47.9497C11.9624-47.9497 10.7143-46.7505 10.7143-45.2515ZM10.7143-61.4136C10.7143-59.9331 11.9624-58.7002 13.4277-58.7002C40.0874-58.7002 61.7397-37.0327 61.7397-10.3882C61.7397-8.90771 62.9878-7.6748 64.4531-7.6748C65.9185-7.6748 67.1665-8.90771 67.1665-10.3882C67.1665-40.0273 43.0181-64.1118 13.4277-64.1118C11.9624-64.1118 10.7143-62.9126 10.7143-61.4136Z"/> + </g> +</svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_redeem.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_redeem.svg index a0093692cc..3178938a86 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_redeem.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_redeem.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M142.5-279v97h675v-97h-675Zm0-437h128q-5-9-8.25-22.5T259-763q0-46.083 32.067-78.292Q323.133-873.5 367.8-873.5q30.33 0 56.015 15.25Q449.5-843 464-819.5l16.5 26 16.5-26q15.5-24.5 39.881-39.25t52.513-14.75q47.064 0 79.335 31.71Q701-810.081 701-762.849q0 10.672-3.25 21.76Q694.5-730 690-716h127.5q22.969 0 40.234 17.266Q875-681.469 875-658.5V-182q0 22.969-17.266 40.234Q840.469-124.5 817.5-124.5h-675q-22.969 0-40.234-17.266Q85-159.031 85-182v-476.5q0-22.969 17.266-40.234Q119.531-716 142.5-716Zm0 332.5h675v-275h-250L651-544q7.5 9.5 5 21.25t-12 19.25q-9.467 7.5-21.325 5.556-11.858-1.945-19.175-12.556L480-681.5l-123.5 171q-6.834 10.833-18.476 12.667Q326.382-496 316.75-503.5q-10.25-7.5-12.5-19.25T309.5-544l83-114.5h-250v275Zm227-326.5q22 0 37.5-15.5t15.5-37.5q0-22-15.5-37.5T369.5-816q-22 0-37.5 15.5T316.5-763q0 22 15.5 37.5t37.5 15.5Zm220 0q22.95 0 38.475-15.5Q643.5-741 643.5-763t-15.525-37.5Q612.45-816 589.5-816q-21 0-36.5 15.5T537.5-763q0 22 15.5 37.5t36.5 15.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M142.5-279v97h675v-97h-675Zm0-437h128q-5-9-8.25-22.5T259-763q0-46.083 32.067-78.292Q323.133-873.5 367.8-873.5q30.33 0 56.015 15.25Q449.5-843 464-819.5l16.5 26 16.5-26q15.5-24.5 39.881-39.25t52.513-14.75q47.064 0 79.335 31.71Q701-810.081 701-762.849q0 10.672-3.25 21.76Q694.5-730 690-716h127.5q22.969 0 40.234 17.266Q875-681.469 875-658.5V-182q0 22.969-17.266 40.234Q840.469-124.5 817.5-124.5h-675q-22.969 0-40.234-17.266Q85-159.031 85-182v-476.5q0-22.969 17.266-40.234Q119.531-716 142.5-716Zm0 332.5h675v-275h-250L651-544q7.5 9.5 5 21.25t-12 19.25q-9.467 7.5-21.325 5.556-11.858-1.945-19.175-12.556L480-681.5l-123.5 171q-6.834 10.833-18.476 12.667Q326.382-496 316.75-503.5q-10.25-7.5-12.5-19.25T309.5-544l83-114.5h-250v275Zm227-326.5q22 0 37.5-15.5t15.5-37.5q0-22-15.5-37.5T369.5-816q-22 0-37.5 15.5T316.5-763q0 22 15.5 37.5t37.5 15.5Zm220 0q22.95 0 38.475-15.5Q643.5-741 643.5-763t-15.525-37.5Q612.45-816 589.5-816q-21 0-36.5 15.5T537.5-763q0 22 15.5 37.5t36.5 15.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_refresh.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_refresh.svg index a1f27d5798..bcbe9c4f34 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_refresh.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_refresh.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480.433-164.5q-131.583 0-223.758-92.216Q164.5-348.932 164.5-480.082q0-131.149 92.175-223.284Q348.85-795.5 480.5-795.5q84 0 147.75 34.25T738.5-666.5v-129H796V-547H547.5v-57.5H713q-38.032-60.033-96.537-96.767Q557.959-738 480.539-738 372-738 297-663.015q-75 74.986-75 183.25 0 108.265 74.875 183.015Q371.75-222 480.331-222q82.298 0 150.734-47 68.435-47 95.623-124.5H786q-28.5 103-113.49 166-84.991 63-192.077 63Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M480.433-164.5q-131.583 0-223.758-92.216Q164.5-348.932 164.5-480.082q0-131.149 92.175-223.284Q348.85-795.5 480.5-795.5q84 0 147.75 34.25T738.5-666.5v-129H796V-547H547.5v-57.5H713q-38.032-60.033-96.537-96.767Q557.959-738 480.539-738 372-738 297-663.015q-75 74.986-75 183.25 0 108.265 74.875 183.015Q371.75-222 480.331-222q82.298 0 150.734-47 68.435-47 95.623-124.5H786q-28.5 103-113.49 166-84.991 63-192.077 63Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_repeat_one.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_repeat_one.svg index d37aa28022..014453c240 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_repeat_one.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_repeat_one.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M467 504.5h-31.721q-9.71 0-16.495-6.833-6.784-6.834-6.784-17.12 0-9.854 7.117-16.701Q426.233 457 436.5 457h53.733q10.1 0 17.184 7.117 7.083 7.116 7.083 17.383V669q0 9.845-6.965 16.672-6.965 6.828-17 6.828t-16.785-7.117Q467 678.267 467 668V504.5Zm232.5 280v-130q0-12.25 8.175-20.625 8.176-8.375 20.25-8.375 12.575 0 20.825 8.25T757 653.5v160q0 12.25-8.306 20.375T728.5 842H236l65.5 65.5q9.5 10 9.5 21.75t-8.5 20.25q-9.033 9-20.517 9.5-11.483.5-19.983-8.5L144.548 833.048Q136 824.5 136 813.244q0-11.255 8.5-20.244l116.448-116.448Q269.567 668 281.517 668t20.983 8.553q8.5 9.086 8.5 21.016 0 11.931-8.5 19.931l-66.5 67h463.5ZM261.5 366v130q0 12.25-8.425 20.625-8.426 8.375-20.5 8.375Q220 525 212 516.75T204 497V337q0-11.888 8.125-20.194T232.5 308.5H725L659 243q-8.731-8.63-9.115-21.065Q649.5 209.5 658.25 201q8.75-8.5 20.25-9t20.044 8.044l117.412 117.412Q824.5 326 824.5 337.5t-8.544 20.044L699.548 473.952q-8.615 8.548-20.581 8.548-11.967 0-20.717-8.553-8.75-8.552-8.75-20.483 0-11.931 8.5-20.464l67-67H261.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M467 504.5h-31.721q-9.71 0-16.495-6.833-6.784-6.834-6.784-17.12 0-9.854 7.117-16.701Q426.233 457 436.5 457h53.733q10.1 0 17.184 7.117 7.083 7.116 7.083 17.383V669q0 9.845-6.965 16.672-6.965 6.828-17 6.828t-16.785-7.117Q467 678.267 467 668V504.5Zm232.5 280v-130q0-12.25 8.175-20.625 8.176-8.375 20.25-8.375 12.575 0 20.825 8.25T757 653.5v160q0 12.25-8.306 20.375T728.5 842H236l65.5 65.5q9.5 10 9.5 21.75t-8.5 20.25q-9.033 9-20.517 9.5-11.483.5-19.983-8.5L144.548 833.048Q136 824.5 136 813.244q0-11.255 8.5-20.244l116.448-116.448Q269.567 668 281.517 668t20.983 8.553q8.5 9.086 8.5 21.016 0 11.931-8.5 19.931l-66.5 67h463.5ZM261.5 366v130q0 12.25-8.425 20.625-8.426 8.375-20.5 8.375Q220 525 212 516.75T204 497V337q0-11.888 8.125-20.194T232.5 308.5H725L659 243q-8.731-8.63-9.115-21.065Q649.5 209.5 658.25 201q8.75-8.5 20.25-9t20.044 8.044l117.412 117.412Q824.5 326 824.5 337.5t-8.544 20.044L699.548 473.952q-8.615 8.548-20.581 8.548-11.967 0-20.717-8.553-8.75-8.552-8.75-20.483 0-11.931 8.5-20.464l67-67H261.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_replay.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_replay.svg index 9f8c033f89..3e1b60666c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_replay.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_replay.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M480 971q-74 0-138.75-27.75t-113-76q-48.25-48.25-76-113T124.5 615.5q0-12.5 8.25-20.75t20.75-8.25q12 0 20.25 8.25T182 615.5q0 124 87 211t211 87q124 0 211-86.75t87-211.25q0-124-84.5-210.75T485 318h-22.5l53 52.5q8.5 9 8.5 20.5t-9 20q-8.5 8.5-20 8.25t-19.5-8.75L372 307q-9-8.5-9-20t9-20.5l104-104q8-8 19.75-8t19.75 7.5q7.5 8 7.5 20t-7.5 19.5l-59 59H480q74 0 138.75 27.75t113 76q48.25 48.25 76 113T835.5 615.5q0 74-27.75 138.75t-76 113q-48.25 48.25-113 76T480 971Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M480 971q-74 0-138.75-27.75t-113-76q-48.25-48.25-76-113T124.5 615.5q0-12.5 8.25-20.75t20.75-8.25q12 0 20.25 8.25T182 615.5q0 124 87 211t211 87q124 0 211-86.75t87-211.25q0-124-84.5-210.75T485 318h-22.5l53 52.5q8.5 9 8.5 20.5t-9 20q-8.5 8.5-20 8.25t-19.5-8.75L372 307q-9-8.5-9-20t9-20.5l104-104q8-8 19.75-8t19.75 7.5q7.5 8 7.5 20t-7.5 19.5l-59 59H480q74 0 138.75 27.75t113 76q48.25 48.25 76 113T835.5 615.5q0 74-27.75 138.75t-76 113q-48.25 48.25-113 76T480 971Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_reply.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_reply.svg index 7298dc984e..bffa9f33a2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_reply.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_reply.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M807.5 853q-12.5 0-20.75-8.25T778.5 824V681q0-54-38-92t-92-38H233l135 134q7.5 8 7.5 19.5t-8.5 20q-9 9-20.5 9t-20-9L144 542q-4.5-4.5-6.5-9.25t-2-10.75q0-6 2-10.75T144 502l183.5-184q7.5-7.5 19-7.5T367 319q8.5 9 8.5 20.5t-8.5 20l-134 134h415.5q77.5 0 132.5 54.75T836 681v143q0 12.5-8.25 20.75T807.5 853Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M807.5 853q-12.5 0-20.75-8.25T778.5 824V681q0-54-38-92t-92-38H233l135 134q7.5 8 7.5 19.5t-8.5 20q-9 9-20.5 9t-20-9L144 542q-4.5-4.5-6.5-9.25t-2-10.75q0-6 2-10.75T144 502l183.5-184q7.5-7.5 19-7.5T367 319q8.5 9 8.5 20.5t-8.5 20l-134 134h415.5q77.5 0 132.5 54.75T836 681v143q0 12.5-8.25 20.75T807.5 853Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report.svg new file mode 100644 index 0000000000..c4c54f0e02 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M479.895-284Q494-284 504-293.895q10-9.894 10-24Q514-332 504.105-342q-9.894-10-24-10Q466-352 456-342.105q-10 9.894-10 24Q446-304 455.895-294q9.894 10 24 10ZM451.5-425H509v-261h-57.5v261ZM332-124.5 124.5-332.176V-628l207.676-207.5H628l207.5 207.676V-332L627.824-124.5H332Zm24.222-57.5h248.243L778-356.222v-248.243L604.242-778H356L182-604.242V-356l174.222 174ZM480-480Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report_filled.svg index 914c74a40a..8148ca77f2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M479.895 772Q494 772 504 762.105q10-9.894 10-24Q514 724 504.105 714q-9.894-10-24-10Q466 704 456 713.895q-10 9.894-10 24Q446 752 455.895 762q9.894 10 24 10ZM480 631q12.5 0 20.75-8.25T509 602V398.5q0-12-8.25-20.25T480 370q-12.5 0-20.5 8.25t-8 20.25V602q0 12.5 8 20.75T480 631ZM356 931.5q-11.643 0-22.616-4.739Q322.411 922.022 314.5 914L142 741.5q-8.022-7.911-12.761-18.884Q124.5 711.643 124.5 700V452q0-11.643 4.739-22.616Q133.978 418.411 142 410.5L314.5 238q7.911-8.022 18.884-12.761Q344.357 220.5 356 220.5h248q11.643 0 22.616 4.739Q637.589 229.978 645.5 238L818 410.5q8.022 7.911 12.761 18.884Q835.5 440.357 835.5 452v248q0 11.643-4.739 22.616Q826.022 733.589 818 741.5L645.5 914q-7.911 8.022-18.884 12.761Q615.643 931.5 604 931.5H356Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M479.895 772Q494 772 504 762.105q10-9.894 10-24Q514 724 504.105 714q-9.894-10-24-10Q466 704 456 713.895q-10 9.894-10 24Q446 752 455.895 762q9.894 10 24 10ZM480 631q12.5 0 20.75-8.25T509 602V398.5q0-12-8.25-20.25T480 370q-12.5 0-20.5 8.25t-8 20.25V602q0 12.5 8 20.75T480 631ZM356 931.5q-11.643 0-22.616-4.739Q322.411 922.022 314.5 914L142 741.5q-8.022-7.911-12.761-18.884Q124.5 711.643 124.5 700V452q0-11.643 4.739-22.616Q133.978 418.411 142 410.5L314.5 238q7.911-8.022 18.884-12.761Q344.357 220.5 356 220.5h248q11.643 0 22.616 4.739Q637.589 229.978 645.5 238L818 410.5q8.022 7.911 12.761 18.884Q835.5 440.357 835.5 452v248q0 11.643-4.739 22.616Q826.022 733.589 818 741.5L645.5 914q-7.911 8.022-18.884 12.761Q615.643 931.5 604 931.5H356Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_restart_alt.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_restart_alt.svg index a28c95673e..d2f91c991c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_restart_alt.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_restart_alt.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M421-130q-110-20-183.25-107.25T164.5-439.5q0-71 30.5-134.25t86-106.75q7.5-5.5 18.25-4.75T317.5-677q9.5 9.5 8.5 22t-13 22q-43.5 35-67.25 86T222-439.5q0 93.5 58 163T427-188q10.5 2 18 10.5t7.5 19.5q0 13.5-9.75 21.75T421-130Zm120 0q-12 2-21.5-6.25T510-158q0-10.5 7.25-19.25T535.5-188q90-19 147-88.5t57-163q0-108-75-183.25T481-698h-21l41 41q8 8 8 20.25T501-617q-9 9-21.25 9T460-617l-91-89.5q-5-4.5-7-9.25t-2-10.75q0-6 2-10.75t7-9.25l91-91q7.5-8 19.75-8t21.25 8q8 9 8 21.25t-8 19.75l-41 41h21q132 0 224 92.5t92 223.5q0 115-73 202.25T541-130Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M421-130q-110-20-183.25-107.25T164.5-439.5q0-71 30.5-134.25t86-106.75q7.5-5.5 18.25-4.75T317.5-677q9.5 9.5 8.5 22t-13 22q-43.5 35-67.25 86T222-439.5q0 93.5 58 163T427-188q10.5 2 18 10.5t7.5 19.5q0 13.5-9.75 21.75T421-130Zm120 0q-12 2-21.5-6.25T510-158q0-10.5 7.25-19.25T535.5-188q90-19 147-88.5t57-163q0-108-75-183.25T481-698h-21l41 41q8 8 8 20.25T501-617q-9 9-21.25 9T460-617l-91-89.5q-5-4.5-7-9.25t-2-10.75q0-6 2-10.75t7-9.25l91-91q7.5-8 19.75-8t21.25 8q8 9 8 21.25t-8 19.75l-41 41h21q132 0 224 92.5t92 223.5q0 115-73 202.25T541-130Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_ring_volume.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_ring_volume.svg index 92bc814040..4b01530b52 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_ring_volume.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_ring_volume.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M140 908.5 46.5 815q-9-8.5-8.5-20t8.5-21q81-95 195.111-145.25 114.11-50.25 238.75-50.25 124.639 0 238.68 50.353 114.041 50.352 195.35 145.058Q922 783.5 922.5 795q.5 11.5-8.115 20.059L821.5 908.5q-8 8-22.5 8.75t-23.094-5.796L662.5 827q-5.5-4.5-8.25-10.25T651.5 804V664.907q-42-15.957-85.75-22.432Q522 636 480.744 636q-42.256 0-85.75 6.5T310 664.952V804q0 7-3 12.75t-8.532 10.282L185 911.5q-12 8.5-23.75 7.5T140 908.5Zm24-56.5 83.5-61.5V689q-39.5 19-74.75 46T106 792l58 60Zm632.5-1 59-59q-32-33.5-69-59T709 685v99.5l87.5 66.5Zm-316-484q-12.5 0-20.5-8.25T452 338V211q0-11.925 8-20.213 8-8.287 20.5-8.287t20.75 8.287q8.25 8.288 8.25 20.213v127q0 12.5-8.25 20.75T480.5 367Zm207 89.647q-9-9.853-8.25-21.823.75-11.971 9.25-20.824l88.376-85.938q8.624-8.562 20.924-8.812 12.3-.25 20.2 9.25 7.5 9 7.25 20.5T817 368l-87 88.5q-8.9 8.5-21.7 8.75-12.8.25-20.8-8.603Zm-452.61-.257-89.28-87.28q-9.61-8.61-9.86-20.655-.25-12.046 8.75-20.955 8.5-8.5 20.983-8.25 12.484.25 21.517 8.75l88 89q8 8 8 19t-8 19.5q-7.9 9.962-19.7 9.731-11.8-.231-20.41-8.841ZM709 685Zm-461.5 4Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M140 908.5 46.5 815q-9-8.5-8.5-20t8.5-21q81-95 195.111-145.25 114.11-50.25 238.75-50.25 124.639 0 238.68 50.353 114.041 50.352 195.35 145.058Q922 783.5 922.5 795q.5 11.5-8.115 20.059L821.5 908.5q-8 8-22.5 8.75t-23.094-5.796L662.5 827q-5.5-4.5-8.25-10.25T651.5 804V664.907q-42-15.957-85.75-22.432Q522 636 480.744 636q-42.256 0-85.75 6.5T310 664.952V804q0 7-3 12.75t-8.532 10.282L185 911.5q-12 8.5-23.75 7.5T140 908.5Zm24-56.5 83.5-61.5V689q-39.5 19-74.75 46T106 792l58 60Zm632.5-1 59-59q-32-33.5-69-59T709 685v99.5l87.5 66.5Zm-316-484q-12.5 0-20.5-8.25T452 338V211q0-11.925 8-20.213 8-8.287 20.5-8.287t20.75 8.287q8.25 8.288 8.25 20.213v127q0 12.5-8.25 20.75T480.5 367Zm207 89.647q-9-9.853-8.25-21.823.75-11.971 9.25-20.824l88.376-85.938q8.624-8.562 20.924-8.812 12.3-.25 20.2 9.25 7.5 9 7.25 20.5T817 368l-87 88.5q-8.9 8.5-21.7 8.75-12.8.25-20.8-8.603Zm-452.61-.257-89.28-87.28q-9.61-8.61-9.86-20.655-.25-12.046 8.75-20.955 8.5-8.5 20.983-8.25 12.484.25 21.517 8.75l88 89q8 8 8 19t-8 19.5q-7.9 9.962-19.7 9.731-11.8-.231-20.41-8.841ZM709 685Zm-461.5 4Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_safety_divider.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_safety_divider.svg index 5517bcd740..9b532e6420 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_safety_divider.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_safety_divider.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M479.825 854.5q-12.325 0-20.325-8.125t-8-20.375V326q0-12.25 8.175-20.625 8.176-8.375 20.5-8.375 12.325 0 20.575 8.375T509 326v500q0 12.25-8.425 20.375-8.426 8.125-20.75 8.125Zm-289.785-285q-30.302 0-51.921-21.579T116.5 496.04q0-30.303 21.579-51.921Q159.658 422.5 189.96 422.5t51.921 21.579Q263.5 465.658 263.5 495.96q0 30.303-21.579 51.921Q220.342 569.5 190.04 569.5ZM41.5 724.5V713q0-20.331 11.158-37.774Q63.816 657.784 83.5 649q25.078-10.022 51.353-16.011Q161.128 627 190.064 627t55.167 5.989Q271.463 638.978 296.5 649q19.653 8.584 31.077 26.126Q339 692.669 339 713v11.5H41.5Zm728.54-155q-30.303 0-51.921-21.579Q696.5 526.342 696.5 496.04q0-30.303 21.579-51.921Q739.658 422.5 769.96 422.5q30.303 0 51.921 21.579Q843.5 465.658 843.5 495.96q0 30.303-21.579 51.921Q800.342 569.5 770.04 569.5Zm-148.54 155V713q0-20.331 11.158-37.774Q643.816 657.784 663.5 649q25.078-10.022 51.353-16.011Q741.128 627 770.064 627t55.167 5.989Q851.463 638.978 876.5 649q19.653 8.584 31.077 26.126Q919 692.669 919 713v11.5H621.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M479.825 854.5q-12.325 0-20.325-8.125t-8-20.375V326q0-12.25 8.175-20.625 8.176-8.375 20.5-8.375 12.325 0 20.575 8.375T509 326v500q0 12.25-8.425 20.375-8.426 8.125-20.75 8.125Zm-289.785-285q-30.302 0-51.921-21.579T116.5 496.04q0-30.303 21.579-51.921Q159.658 422.5 189.96 422.5t51.921 21.579Q263.5 465.658 263.5 495.96q0 30.303-21.579 51.921Q220.342 569.5 190.04 569.5ZM41.5 724.5V713q0-20.331 11.158-37.774Q63.816 657.784 83.5 649q25.078-10.022 51.353-16.011Q161.128 627 190.064 627t55.167 5.989Q271.463 638.978 296.5 649q19.653 8.584 31.077 26.126Q339 692.669 339 713v11.5H41.5Zm728.54-155q-30.303 0-51.921-21.579Q696.5 526.342 696.5 496.04q0-30.303 21.579-51.921Q739.658 422.5 769.96 422.5q30.303 0 51.921 21.579Q843.5 465.658 843.5 495.96q0 30.303-21.579 51.921Q800.342 569.5 770.04 569.5Zm-148.54 155V713q0-20.331 11.158-37.774Q643.816 657.784 663.5 649q25.078-10.022 51.353-16.011Q741.128 627 770.064 627t55.167 5.989Q851.463 638.978 876.5 649q19.653 8.584 31.077 26.126Q919 692.669 919 713v11.5H621.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_schedule.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_schedule.svg new file mode 100644 index 0000000000..2e806eb315 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_schedule.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M512-491.5V-663q0-11.925-8.288-20.213-8.287-8.287-20.212-8.287-12.5 0-20.75 8.287-8.25 8.288-8.25 20.213v183q0 5.832 2 10.675 2 4.842 5.543 9.325l143.572 148.538Q614-301.5 627-302q13-.5 22-9.5 8.5-8.5 8.5-21t-8.458-21.458L512-491.5ZM480.064-85q-80.971 0-153.129-31.263-72.159-31.263-125.797-85Q147.5-255 116.25-327.023 85-399.046 85-479.936q0-80.971 31.263-153.129 31.263-72.159 85-125.797Q255-812.5 327.023-843.75 399.046-875 479.936-875q80.971 0 153.129 31.263 72.159 31.263 125.797 85Q812.5-705 843.75-632.977 875-560.954 875-480.064q0 80.971-31.263 153.129-31.263 72.159-85 125.797Q705-147.5 632.977-116.25 560.954-85 480.064-85ZM480-480Zm-.25 337.5q138.75 0 238.25-99.25t99.5-238q0-138.75-99.48-238.25-99.481-99.5-238.02-99.5-139 0-238.25 99.48Q142.5-618.539 142.5-480q0 139 99.25 238.25t238 99.25Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_schedule_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_schedule_filled.svg new file mode 100644 index 0000000000..b7afaf420b --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_schedule_filled.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M512-491.5V-663q0-11.925-8.288-20.213-8.287-8.287-20.212-8.287-12.5 0-20.75 8.287-8.25 8.288-8.25 20.213v183q0 5.832 2 10.675 2 4.842 5.543 9.325l143.572 148.538Q614-301.5 627-302q13-.5 22-9.5 8.5-8.5 8.5-21t-8.458-21.458L512-491.5ZM480.064-85q-80.971 0-153.129-31.263-72.159-31.263-125.797-85Q147.5-255 116.25-327.023 85-399.046 85-479.936q0-80.971 31.263-153.129 31.263-72.159 85-125.797Q255-812.5 327.023-843.75 399.046-875 479.936-875q80.971 0 153.129 31.263 72.159 31.263 125.797 85Q812.5-705 843.75-632.977 875-560.954 875-480.064q0 80.971-31.263 153.129-31.263 72.159-85 125.797Q705-147.5 632.977-116.25 560.954-85 480.064-85Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_search.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_search.svg index 8a74c6d43f..7a6e668cf5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_search.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_search.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M772.625 909.63 532.66 669.5Q503 696 463.041 710t-83.883 14q-106.761 0-180.709-73.977-73.949-73.977-73.949-178.5T198.204 293q73.704-74 179-74T556 293.007q73.5 74.006 73.5 178.722 0 42.271-14 82.021T574 628.5L816 868q8.5 8.186 8.5 20.093t-9.413 21.32Q806 918.5 793.278 918.5q-12.722 0-20.653-8.87ZM378 666.5q80.75 0 137.375-57.059t56.625-138q0-80.941-56.625-137.941t-137.625-57q-81.833 0-138.792 57.059Q182 390.618 182 471.559T239 609.5q57 57 139 57Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M772.625 909.63 532.66 669.5Q503 696 463.041 710t-83.883 14q-106.761 0-180.709-73.977-73.949-73.977-73.949-178.5T198.204 293q73.704-74 179-74T556 293.007q73.5 74.006 73.5 178.722 0 42.271-14 82.021T574 628.5L816 868q8.5 8.186 8.5 20.093t-9.413 21.32Q806 918.5 793.278 918.5q-12.722 0-20.653-8.87ZM378 666.5q80.75 0 137.375-57.059t56.625-138q0-80.941-56.625-137.941t-137.625-57q-81.833 0-138.792 57.059Q182 390.618 182 471.559T239 609.5q57 57 139 57Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_search_500.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_search_500.svg index 18e56def19..2e36a5dec4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_search_500.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_search_500.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M769.516-142.544 530.986-381q-29.421 25.764-69.26 39.415-39.84 13.65-84.003 13.65-108.434 0-183.579-75.172Q119-478.28 119-584.477t74.899-181.393q74.9-75.195 181.87-75.195t181.666 75.202q74.695 75.202 74.695 181.592 0 42.749-13.641 82.141-13.641 39.391-40.424 74.152L818.63-189.913q9.696 9.621 9.696 23.202 0 13.581-10.609 23.95-10.351 10.522-24.713 10.522t-23.488-10.305ZM376.65-393.565q79.23 0 134.54-55.804 55.31-55.803 55.31-135.19T511.155-719.69Q455.81-775.435 376.4-775.435q-80.244 0-136.007 55.804-55.763 55.803-55.763 135.19T240.4-449.31q55.769 55.745 136.25 55.745Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M769.516-142.544 530.986-381q-29.421 25.764-69.26 39.415-39.84 13.65-84.003 13.65-108.434 0-183.579-75.172Q119-478.28 119-584.477t74.899-181.393q74.9-75.195 181.87-75.195t181.666 75.202q74.695 75.202 74.695 181.592 0 42.749-13.641 82.141-13.641 39.391-40.424 74.152L818.63-189.913q9.696 9.621 9.696 23.202 0 13.581-10.609 23.95-10.351 10.522-24.713 10.522t-23.488-10.305ZM376.65-393.565q79.23 0 134.54-55.804 55.31-55.803 55.31-135.19T511.155-719.69Q455.81-775.435 376.4-775.435q-80.244 0-136.007 55.804-55.763 55.803-55.763 135.19T240.4-449.31q55.769 55.745 136.25 55.745Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_security.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_security.svg index 13b5c1914e..2801bacad7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_security.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_security.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M480.061 967.5q-5.061 0-10.311-.5t-9.25-2Q330 923.5 247.25 801.561T164.5 533.904V339.281q0-17.761 10.524-32.795Q185.548 291.453 202 285l258-96.5q10.25-3.5 20-3.5t20.5 3.5l258 96.5q15.952 6.453 26.476 21.486Q795.5 321.52 795.5 339.281v194.623q0 145.718-82.75 267.657T500 965q-4.5 1.5-9.689 2t-10.25.5Zm-.061-56Q585 877 653.942 784.226 722.883 691.453 735 576.171H480V243.5l-258 96.188v194.296q0 11.901.5 20.458.5 8.558 2.483 21.625H480V911.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M480.061 967.5q-5.061 0-10.311-.5t-9.25-2Q330 923.5 247.25 801.561T164.5 533.904V339.281q0-17.761 10.524-32.795Q185.548 291.453 202 285l258-96.5q10.25-3.5 20-3.5t20.5 3.5l258 96.5q15.952 6.453 26.476 21.486Q795.5 321.52 795.5 339.281v194.623q0 145.718-82.75 267.657T500 965q-4.5 1.5-9.689 2t-10.25.5Zm-.061-56Q585 877 653.942 784.226 722.883 691.453 735 576.171H480V243.5l-258 96.188v194.296q0 11.901.5 20.458.5 8.558 2.483 21.625H480V911.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings.svg index 04b250537d..edc27762a0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M544.5 971H415.318q-10.356 0-18.651-6.536Q388.372 957.929 387 947l-16-100.5q-13.5-4.5-35.25-16.75T292 802l-92 43q-10.727 5-21.453 1.203Q167.82 842.406 162.5 832l-65-115q-5.5-9.5-2.5-20.096T106.5 680l86-63q-2-8.975-2.5-19.987-.5-11.013-.5-21.013 0-8.5.5-20.25t2.5-21.75l-86-62.432Q98 465 94.75 454.471 91.5 443.942 97.5 434.5l64.914-115.206q6.086-9.794 16.58-13.544Q189.488 302 200 307l93 43q15-12.5 36.25-24.75T371 306.5L387 204q1.372-10.429 9.613-16.964 8.24-6.536 18.887-6.536h129.182q10.356 0 18.61 6.571 8.253 6.572 10.208 16.929l16 101q19 7 40.75 19T667 350l93.569-43.061q9.764-4.939 20.375-1.189 10.612 3.75 16.556 13.75l65 114q6 9.442 3.25 20.471t-12.043 17.461L766.5 532.891Q768 543 768.75 554.25q.75 11.25.75 21.75t-.75 21.425q-.75 10.925-2.25 20.623l87.071 61.89q8.596 6.485 11.512 17.047Q868 707.547 862.5 717l-65 115.5q-6 10.5-16.488 13.75-10.489 3.25-20.512-1.75l-93.5-43q-14.971 12.864-35.236 25.182Q611.5 839 589.5 846.5l-16 100.5q-1.872 10.929-10.113 17.464Q555.147 971 544.5 971Zm-65.269-267.5q52.805 0 90.037-37.275 37.232-37.276 37.232-90.25 0-52.975-37.268-90.225Q531.964 448.5 479 448.5q-53.5 0-90.5 37.275-37 37.276-37 90.25 0 52.975 37 90.225 37 37.25 90.731 37.25ZM479 646q-29.5 0-49.75-20.5T409 576q0-29 20.25-49.5T479 506q29 0 49.5 20.5T549 576q0 29-20.5 49.5T479 646Zm1-70.5Zm-43.651 338h87.085L538 801q32.585-8.066 61.085-24.443Q627.585 760.18 651.5 736L758 782.5l39.5-71.256L703 642q3.5-17 6.25-33.209 2.75-16.21 2.75-32.91Q712 559 710 543t-7-33l94.5-69.5-39-71.5-107 46.5q-22.5-25-51.25-42.5T538 350.5L524 238h-88l-13.5 112.535q-35 7.465-63 23.465t-52 41.5L202 369l-40 71.5 94 68.5q-4 17.5-6.5 33.819-2.5 16.319-2.5 32.651 0 17.03 2.25 33.28t6.25 33.75L162 711l39.859 71.5 105.641-46q24.5 24.5 53.25 40.75t61.75 24.25l13.849 112Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M544.5 971H415.318q-10.356 0-18.651-6.536Q388.372 957.929 387 947l-16-100.5q-13.5-4.5-35.25-16.75T292 802l-92 43q-10.727 5-21.453 1.203Q167.82 842.406 162.5 832l-65-115q-5.5-9.5-2.5-20.096T106.5 680l86-63q-2-8.975-2.5-19.987-.5-11.013-.5-21.013 0-8.5.5-20.25t2.5-21.75l-86-62.432Q98 465 94.75 454.471 91.5 443.942 97.5 434.5l64.914-115.206q6.086-9.794 16.58-13.544Q189.488 302 200 307l93 43q15-12.5 36.25-24.75T371 306.5L387 204q1.372-10.429 9.613-16.964 8.24-6.536 18.887-6.536h129.182q10.356 0 18.61 6.571 8.253 6.572 10.208 16.929l16 101q19 7 40.75 19T667 350l93.569-43.061q9.764-4.939 20.375-1.189 10.612 3.75 16.556 13.75l65 114q6 9.442 3.25 20.471t-12.043 17.461L766.5 532.891Q768 543 768.75 554.25q.75 11.25.75 21.75t-.75 21.425q-.75 10.925-2.25 20.623l87.071 61.89q8.596 6.485 11.512 17.047Q868 707.547 862.5 717l-65 115.5q-6 10.5-16.488 13.75-10.489 3.25-20.512-1.75l-93.5-43q-14.971 12.864-35.236 25.182Q611.5 839 589.5 846.5l-16 100.5q-1.872 10.929-10.113 17.464Q555.147 971 544.5 971Zm-65.269-267.5q52.805 0 90.037-37.275 37.232-37.276 37.232-90.25 0-52.975-37.268-90.225Q531.964 448.5 479 448.5q-53.5 0-90.5 37.275-37 37.276-37 90.25 0 52.975 37 90.225 37 37.25 90.731 37.25ZM479 646q-29.5 0-49.75-20.5T409 576q0-29 20.25-49.5T479 506q29 0 49.5 20.5T549 576q0 29-20.5 49.5T479 646Zm1-70.5Zm-43.651 338h87.085L538 801q32.585-8.066 61.085-24.443Q627.585 760.18 651.5 736L758 782.5l39.5-71.256L703 642q3.5-17 6.25-33.209 2.75-16.21 2.75-32.91Q712 559 710 543t-7-33l94.5-69.5-39-71.5-107 46.5q-22.5-25-51.25-42.5T538 350.5L524 238h-88l-13.5 112.535q-35 7.465-63 23.465t-52 41.5L202 369l-40 71.5 94 68.5q-4 17.5-6.5 33.819-2.5 16.319-2.5 32.651 0 17.03 2.25 33.28t6.25 33.75L162 711l39.859 71.5 105.641-46q24.5 24.5 53.25 40.75t61.75 24.25l13.849 112Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_backup_restore.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_backup_restore.svg index a3cf4db7e1..22e8bcbbfb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_backup_restore.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_backup_restore.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M479.528 931.5q-141.028 0-242.278-94.75T125 603.664q-1-12.005 7.179-20.858t20.616-8.83q11.705.024 19.866 8.524 8.162 8.5 9.925 20.575Q193.5 718 278 796q84.5 78 202 78 124.193 0 211.096-86.937Q778 700.125 778 575.652T691.115 364.59Q604.23 278 479.5 278q-68.27 0-127.635 30Q292.5 338 248.5 389H322q12.25 0 20.625 8.463T351 417.675q0 12.325-8.375 20.575T322 446.5H177q-12.25 0-20.375-8.375T148.5 417.5V272.3q0-11.59 8.175-19.945 8.176-8.355 20.5-8.355 12.325 0 20.575 8.412Q206 260.825 206 272.5v77q52-61 122.91-95 70.911-34 150.74-34 73.593 0 138.448 28.13 64.856 28.13 112.879 76Q779 372.5 807.25 437.184q28.25 64.684 28.25 138.75T807.25 714.75Q779 779.5 731 827.5t-112.823 76q-64.823 28-138.649 28Zm.178-282q-29.969 0-51.587-21.579Q406.5 606.342 406.5 576.04q0-30.303 21.604-51.921 21.603-21.619 51.94-21.619 29.719 0 51.587 21.579Q553.5 545.658 553.5 575.96q0 30.303-21.913 51.921-21.913 21.619-51.881 21.619Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M479.528 931.5q-141.028 0-242.278-94.75T125 603.664q-1-12.005 7.179-20.858t20.616-8.83q11.705.024 19.866 8.524 8.162 8.5 9.925 20.575Q193.5 718 278 796q84.5 78 202 78 124.193 0 211.096-86.937Q778 700.125 778 575.652T691.115 364.59Q604.23 278 479.5 278q-68.27 0-127.635 30Q292.5 338 248.5 389H322q12.25 0 20.625 8.463T351 417.675q0 12.325-8.375 20.575T322 446.5H177q-12.25 0-20.375-8.375T148.5 417.5V272.3q0-11.59 8.175-19.945 8.176-8.355 20.5-8.355 12.325 0 20.575 8.412Q206 260.825 206 272.5v77q52-61 122.91-95 70.911-34 150.74-34 73.593 0 138.448 28.13 64.856 28.13 112.879 76Q779 372.5 807.25 437.184q28.25 64.684 28.25 138.75T807.25 714.75Q779 779.5 731 827.5t-112.823 76q-64.823 28-138.649 28Zm.178-282q-29.969 0-51.587-21.579Q406.5 606.342 406.5 576.04q0-30.303 21.604-51.921 21.603-21.619 51.94-21.619 29.719 0 51.587 21.579Q553.5 545.658 553.5 575.96q0 30.303-21.913 51.921-21.913 21.619-51.881 21.619Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_ethernet.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_ethernet.svg index 828c7b8a8b..8fd3d489c9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_ethernet.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_ethernet.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M696.5 793.5q-7.625 8-19.562 7.5Q665 800.5 657 792.25q-8.5-8.25-8.5-19.733 0-11.484 8.5-20.517l174.5-176-175-177.5q-8-7.5-7.729-19.5.271-12 8.229-20.25 8.5-8.25 20-8.25t20.359 8.359l196.585 197.056Q902.5 564.5 902.5 576t-8.556 20.056L696.5 793.5Zm-433.359-.859L66.085 596.056Q57.5 587.5 57.5 576q0-11.5 8.584-20.084L264 358q7.5-7.5 19.25-7t19.5 8.25q8.75 8.75 8.75 20.25t-8.533 20.071L128.5 576l175.434 176.967Q311.5 761 311.25 773q-.25 12-8 19.75-8.75 8.75-20.25 8.75t-19.859-8.859ZM321.053 610.5q-14.053 0-24.303-10.197-10.25-10.198-10.25-24.25 0-14.053 10.197-24.303 10.198-10.25 24.25-10.25 14.053 0 24.303 10.197 10.25 10.198 10.25 24.25 0 14.053-10.197 24.303-10.198 10.25-24.25 10.25Zm159 0q-14.053 0-24.303-10.197-10.25-10.198-10.25-24.25 0-14.053 10.197-24.303 10.198-10.25 24.25-10.25 14.053 0 24.303 10.197 10.25 10.198 10.25 24.25 0 14.053-10.197 24.303-10.198 10.25-24.25 10.25Zm159 0q-14.053 0-24.303-10.197-10.25-10.198-10.25-24.25 0-14.053 10.197-24.303 10.198-10.25 24.25-10.25 14.053 0 24.303 10.197 10.25 10.198 10.25 24.25 0 14.053-10.197 24.303-10.198 10.25-24.25 10.25Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M696.5 793.5q-7.625 8-19.562 7.5Q665 800.5 657 792.25q-8.5-8.25-8.5-19.733 0-11.484 8.5-20.517l174.5-176-175-177.5q-8-7.5-7.729-19.5.271-12 8.229-20.25 8.5-8.25 20-8.25t20.359 8.359l196.585 197.056Q902.5 564.5 902.5 576t-8.556 20.056L696.5 793.5Zm-433.359-.859L66.085 596.056Q57.5 587.5 57.5 576q0-11.5 8.584-20.084L264 358q7.5-7.5 19.25-7t19.5 8.25q8.75 8.75 8.75 20.25t-8.533 20.071L128.5 576l175.434 176.967Q311.5 761 311.25 773q-.25 12-8 19.75-8.75 8.75-20.25 8.75t-19.859-8.859ZM321.053 610.5q-14.053 0-24.303-10.197-10.25-10.198-10.25-24.25 0-14.053 10.197-24.303 10.198-10.25 24.25-10.25 14.053 0 24.303 10.197 10.25 10.198 10.25 24.25 0 14.053-10.197 24.303-10.198 10.25-24.25 10.25Zm159 0q-14.053 0-24.303-10.197-10.25-10.198-10.25-24.25 0-14.053 10.197-24.303 10.198-10.25 24.25-10.25 14.053 0 24.303 10.197 10.25 10.198 10.25 24.25 0 14.053-10.197 24.303-10.198 10.25-24.25 10.25Zm159 0q-14.053 0-24.303-10.197-10.25-10.198-10.25-24.25 0-14.053 10.197-24.303 10.198-10.25 24.25-10.25 14.053 0 24.303 10.197 10.25 10.198 10.25 24.25 0 14.053-10.197 24.303-10.198 10.25-24.25 10.25Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_phone.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_phone.svg index 7280127ceb..6231a35d21 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_phone.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_phone.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M457.553 531q-16.765 0-28.159-11.341Q418 508.318 418 491.553q0-16.766 11.341-28.159Q440.682 452 457.447 452t28.159 11.341Q497 474.682 497 491.447q0 16.766-11.341 28.159Q474.318 531 457.553 531Zm172 0q-16.341 0-27.447-11.341Q591 508.318 591 491.553q0-16.766 11.054-28.159Q613.107 452 629.447 452q17.191 0 28.872 11.341Q670 474.682 670 491.447q0 16.766-11.629 28.159Q646.743 531 629.553 531Zm176 0q-17.191 0-28.872-11.341Q765 508.318 765 491.553q0-16.766 11.629-28.159Q788.257 452 805.447 452q16.341 0 27.447 11.341Q844 474.682 844 491.447q0 16.766-11.054 28.159Q821.893 531 805.553 531Zm-14.667 400Q671.5 931 552.5 871.75t-214-154.25q-95-95-154.5-213.965t-59.5-238.647q0-18.888 12.714-31.638Q149.929 220.5 169 220.5h136.5q13.611 0 24.056 9.25Q340 239 343 255l27 124q2 13.068-.75 24.534t-10.335 19.05L260.5 522.5q56 93 124.75 161.25T542.5 801l95.544-98q9.456-10.5 21.206-14.75T683 687l117.362 25.452q15.45 3.423 25.294 15.236Q835.5 739.5 835.5 755.5v131q0 19.071-12.747 31.786Q810.006 931 790.886 931ZM231.5 470l81-82L289 278H182.5q-.5 38.5 11.75 85.25T231.5 470Zm365 358.5q40 19 88.166 31t93.334 14v-107L676 745l-79.5 83.5ZM231.5 470Zm365 358.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M457.553 531q-16.765 0-28.159-11.341Q418 508.318 418 491.553q0-16.766 11.341-28.159Q440.682 452 457.447 452t28.159 11.341Q497 474.682 497 491.447q0 16.766-11.341 28.159Q474.318 531 457.553 531Zm172 0q-16.341 0-27.447-11.341Q591 508.318 591 491.553q0-16.766 11.054-28.159Q613.107 452 629.447 452q17.191 0 28.872 11.341Q670 474.682 670 491.447q0 16.766-11.629 28.159Q646.743 531 629.553 531Zm176 0q-17.191 0-28.872-11.341Q765 508.318 765 491.553q0-16.766 11.629-28.159Q788.257 452 805.447 452q16.341 0 27.447 11.341Q844 474.682 844 491.447q0 16.766-11.054 28.159Q821.893 531 805.553 531Zm-14.667 400Q671.5 931 552.5 871.75t-214-154.25q-95-95-154.5-213.965t-59.5-238.647q0-18.888 12.714-31.638Q149.929 220.5 169 220.5h136.5q13.611 0 24.056 9.25Q340 239 343 255l27 124q2 13.068-.75 24.534t-10.335 19.05L260.5 522.5q56 93 124.75 161.25T542.5 801l95.544-98q9.456-10.5 21.206-14.75T683 687l117.362 25.452q15.45 3.423 25.294 15.236Q835.5 739.5 835.5 755.5v131q0 19.071-12.747 31.786Q810.006 931 790.886 931ZM231.5 470l81-82L289 278H182.5q-.5 38.5 11.75 85.25T231.5 470Zm365 358.5q40 19 88.166 31t93.334 14v-107L676 745l-79.5 83.5ZM231.5 470Zm365 358.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_share.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_share.svg index f24d145a50..32d2875894 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_share.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_share.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M725.382 971.5q-46.382 0-79.132-32.93-32.75-32.93-32.75-79.221 0-6.678 1.5-16.33 1.5-9.652 4.5-17.373L317 650q-14.788 17.5-36.596 27.75Q258.596 688 236.038 688q-46.538 0-79.288-32.744T124 575.735q0-46.36 32.833-79.047Q189.667 464 235.935 464q22.846 0 43.705 8.75Q300.5 481.5 317 498.5l302.5-173.573q-3-7.056-4.5-15.875-1.5-8.82-1.5-17.052 0-46.333 32.868-79.167Q679.235 180 725.618 180q46.382 0 79.132 32.688 32.75 32.687 32.75 79.047 0 46.777-32.833 79.521Q771.833 404 725.315 404q-23.138 0-43.976-7.25Q660.5 389.5 645.5 372L343 539.5q2 7.786 3.502 18.66 1.502 10.875 1.502 18.098 0 7.222-1.502 14.961Q345 598.957 343 606.941L645.5 778.5q14.788-14.5 34.596-22.75 19.808-8.25 45.366-8.25 46.538 0 79.288 32.667 32.75 32.666 32.75 79.333t-32.868 79.333q-32.867 32.667-79.25 32.667Zm.153-625q22.752 0 38.609-15.891Q780 314.717 780 291.965t-15.891-38.609Q748.217 237.5 725.465 237.5t-38.609 15.891Q671 269.283 671 292.035t15.891 38.609q15.892 15.856 38.644 15.856Zm-489.5 284q22.753 0 38.609-15.891 15.856-15.892 15.856-38.644t-15.891-38.609Q258.717 521.5 235.965 521.5q-22.753 0-38.609 15.891-15.856 15.892-15.856 38.644t15.891 38.609q15.892 15.856 38.644 15.856Zm489.5 283.5q22.752 0 38.609-15.891Q780 882.217 780 859.465t-15.891-38.609Q748.217 805 725.465 805t-38.609 15.891Q671 836.783 671 859.535t15.891 38.609Q702.783 914 725.535 914Zm-.035-622ZM236 576Zm489.5 283.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M725.382 971.5q-46.382 0-79.132-32.93-32.75-32.93-32.75-79.221 0-6.678 1.5-16.33 1.5-9.652 4.5-17.373L317 650q-14.788 17.5-36.596 27.75Q258.596 688 236.038 688q-46.538 0-79.288-32.744T124 575.735q0-46.36 32.833-79.047Q189.667 464 235.935 464q22.846 0 43.705 8.75Q300.5 481.5 317 498.5l302.5-173.573q-3-7.056-4.5-15.875-1.5-8.82-1.5-17.052 0-46.333 32.868-79.167Q679.235 180 725.618 180q46.382 0 79.132 32.688 32.75 32.687 32.75 79.047 0 46.777-32.833 79.521Q771.833 404 725.315 404q-23.138 0-43.976-7.25Q660.5 389.5 645.5 372L343 539.5q2 7.786 3.502 18.66 1.502 10.875 1.502 18.098 0 7.222-1.502 14.961Q345 598.957 343 606.941L645.5 778.5q14.788-14.5 34.596-22.75 19.808-8.25 45.366-8.25 46.538 0 79.288 32.667 32.75 32.666 32.75 79.333t-32.868 79.333q-32.867 32.667-79.25 32.667Zm.153-625q22.752 0 38.609-15.891Q780 314.717 780 291.965t-15.891-38.609Q748.217 237.5 725.465 237.5t-38.609 15.891Q671 269.283 671 292.035t15.891 38.609q15.892 15.856 38.644 15.856Zm-489.5 284q22.753 0 38.609-15.891 15.856-15.892 15.856-38.644t-15.891-38.609Q258.717 521.5 235.965 521.5q-22.753 0-38.609 15.891-15.856 15.892-15.856 38.644t15.891 38.609q15.892 15.856 38.644 15.856Zm489.5 283.5q22.752 0 38.609-15.891Q780 882.217 780 859.465t-15.891-38.609Q748.217 805 725.465 805t-38.609 15.891Q671 836.783 671 859.535t15.891 38.609Q702.783 914 725.535 914Zm-.035-622ZM236 576Zm489.5 283.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_share_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_share_filled.svg index 16b4c0ecf2..86424776cb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_share_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_share_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M725.382 971.5q-46.382 0-79.132-32.93-32.75-32.93-32.75-79.221 0-6.678 1.5-16.33 1.5-9.652 4.5-17.373L317 650q-14.788 17.5-36.596 27.75Q258.596 688 236.038 688q-46.538 0-79.288-32.744T124 575.735q0-46.36 32.833-79.047Q189.667 464 235.935 464q22.846 0 43.705 8.75Q300.5 481.5 317 498.5l302.5-173.573q-3-7.056-4.5-15.875-1.5-8.82-1.5-17.052 0-46.333 32.868-79.167Q679.235 180 725.618 180q46.382 0 79.132 32.688 32.75 32.687 32.75 79.047 0 46.777-32.833 79.521Q771.833 404 725.315 404q-23.138 0-43.976-7.25Q660.5 389.5 645.5 372L343 539.5q2 7.786 3.502 18.66 1.502 10.875 1.502 18.098 0 7.222-1.502 14.961Q345 598.957 343 606.941L645.5 778.5q14.788-14.5 34.596-22.75 19.808-8.25 45.366-8.25 46.538 0 79.288 32.667 32.75 32.666 32.75 79.333t-32.868 79.333q-32.867 32.667-79.25 32.667Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M725.382 971.5q-46.382 0-79.132-32.93-32.75-32.93-32.75-79.221 0-6.678 1.5-16.33 1.5-9.652 4.5-17.373L317 650q-14.788 17.5-36.596 27.75Q258.596 688 236.038 688q-46.538 0-79.288-32.744T124 575.735q0-46.36 32.833-79.047Q189.667 464 235.935 464q22.846 0 43.705 8.75Q300.5 481.5 317 498.5l302.5-173.573q-3-7.056-4.5-15.875-1.5-8.82-1.5-17.052 0-46.333 32.868-79.167Q679.235 180 725.618 180q46.382 0 79.132 32.688 32.75 32.687 32.75 79.047 0 46.777-32.833 79.521Q771.833 404 725.315 404q-23.138 0-43.976-7.25Q660.5 389.5 645.5 372L343 539.5q2 7.786 3.502 18.66 1.502 10.875 1.502 18.098 0 7.222-1.502 14.961Q345 598.957 343 606.941L645.5 778.5q14.788-14.5 34.596-22.75 19.808-8.25 45.366-8.25 46.538 0 79.288 32.667 32.75 32.666 32.75 79.333t-32.868 79.333q-32.867 32.667-79.25 32.667Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_shield.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_shield.svg index ca1ed6828c..e99e34a8d3 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_shield.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_shield.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M480 968q-3.5 0-7-.75t-6.544-1.297Q334 926.5 249.25 803.193q-84.75-123.308-84.75-270.39V338.269q0-17.749 10.524-32.783Q185.548 290.453 202 284l258-96.5q10.25-3.5 20-3.5t20.5 3.5l258 96.5q15.952 6.453 26.476 21.486Q795.5 320.52 795.5 338.269v194.534q0 147.082-84.75 270.39Q626 926.5 493.5 966q1.5-.5-13.5 2Zm0-57.5q114-38 186-142.853Q738 662.793 738 533V338.387L480 241.5l-258 96.887V533q0 129.793 72 234.647Q366 872.5 480 910.5Zm0-334.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M480 968q-3.5 0-7-.75t-6.544-1.297Q334 926.5 249.25 803.193q-84.75-123.308-84.75-270.39V338.269q0-17.749 10.524-32.783Q185.548 290.453 202 284l258-96.5q10.25-3.5 20-3.5t20.5 3.5l258 96.5q15.952 6.453 26.476 21.486Q795.5 320.52 795.5 338.269v194.534q0 147.082-84.75 270.39Q626 926.5 493.5 966q1.5-.5-13.5 2Zm0-57.5q114-38 186-142.853Q738 662.793 738 533V338.387L480 241.5l-258 96.887V533q0 129.793 72 234.647Q366 872.5 480 910.5Zm0-334.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex.svg new file mode 100644 index 0000000000..d7019f3645 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex.svg @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + width="120" + height="120" + viewBox="121 0 40 40" + fill="none" + version="1.1" + id="svg3" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <path + fill-rule="evenodd" + clip-rule="evenodd" + d="m 126.52238,11.425398 5.80302,5.716401 5.88962,-5.889626 2.8582,2.858201 L 135.1836,20 l 5.7164,5.716402 -2.94482,2.8582 -5.7164,-5.629789 -5.88962,5.803014 -2.8582,-2.858201 5.88962,-5.803014 -5.803,-5.716402 z" + fill="#030749" + id="path1" + style="stroke-width:0.866122" /> + <path + fill-rule="evenodd" + clip-rule="evenodd" + d="m 137.86858,28.661214 2.94481,-2.944812 v 0 l 5.88963,-5.803014 -5.80302,-5.62979 v 0 l -2.8582,-2.8582 -5.7164,-5.7164023 2.94481,-2.9448129 5.7164,5.7164017 5.88963,-5.8030138 2.8582,2.8582008 -5.88962,5.8030145 5.7164,5.716401 5.88962,-5.803014 2.8582,2.858201 -5.88962,5.803014 5.803,5.716402 -2.9448,2.858201 -5.7164,-5.716402 -5.88963,5.803013 5.7164,5.716402 -2.8582,2.944813 -5.80301,-5.716402 -5.80302,5.803015 -2.8582,-2.858201 z" + fill="url(#paint0_linear_40_164)" + id="path2" + style="fill:url(#paint0_linear_40_164);stroke-width:0.866122" /> + <defs + id="defs3"> + <linearGradient + x1="135.948" + y1="-0.81632602" + x2="132.09599" + y2="36.985699" + gradientUnits="userSpaceOnUse" + id="paint0_linear_40_164" + gradientTransform="matrix(0.86612147,0,0,0.86612147,18.863485,2.6775707)"> + <stop + stop-color="#01f1ff" + id="stop2" /> + <stop + offset="1" + stop-color="#0197ff" + id="stop3" /> + </linearGradient> + </defs> +</svg> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex_dark@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex_dark@4x.png new file mode 100644 index 0000000000..45fc44af43 Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex_dark@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex_light@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex_light@4x.png new file mode 100644 index 0000000000..c372e25b76 Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex_light@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smart_display.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smart_display.svg index 195c3b2bea..4027f2f1f9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smart_display.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smart_display.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="m436.5 711.5 175.5-114q13-8.5 13-23.509T612 550L436.5 436q-14.5-9.5-29-1.272Q393 442.957 393 460v227.5q0 17.761 14.5 25.88 14.5 8.12 29-1.88Zm-294 180q-22.969 0-40.234-17.266Q85 856.969 85 834V318q0-22.969 17.266-40.234Q119.531 260.5 142.5 260.5h675q23.719 0 40.609 17.266Q875 295.031 875 318v516q0 22.969-16.891 40.234Q841.219 891.5 817.5 891.5h-675Zm0-57.5V318v516Zm0 0h675V318h-675v516Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="m436.5 711.5 175.5-114q13-8.5 13-23.509T612 550L436.5 436q-14.5-9.5-29-1.272Q393 442.957 393 460v227.5q0 17.761 14.5 25.88 14.5 8.12 29-1.88Zm-294 180q-22.969 0-40.234-17.266Q85 856.969 85 834V318q0-22.969 17.266-40.234Q119.531 260.5 142.5 260.5h675q23.719 0 40.609 17.266Q875 295.031 875 318v516q0 22.969-16.891 40.234Q841.219 891.5 817.5 891.5h-675Zm0-57.5V318v516Zm0 0h675V318h-675v516Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smartphone.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smartphone.svg index 93094d1445..bb13e7f818 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smartphone.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smartphone.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M261.5-45q-22.969 0-40.234-17.266Q204-79.53 204-102.5v-755q0-22.969 17.266-40.234Q238.531-915 261.5-915h437q22.969 0 40.234 17.266Q756-880.469 756-857.5v755q0 22.969-17.266 40.234Q721.469-45 698.5-45h-437Zm0-88.5v31h437v-31h-437Zm0-57.5h437v-578h-437v578Zm0-635.5h437v-31h-437v31Zm0 0v-31 31Zm0 693v31-31Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M261.5-45q-22.969 0-40.234-17.266Q204-79.53 204-102.5v-755q0-22.969 17.266-40.234Q238.531-915 261.5-915h437q22.969 0 40.234 17.266Q756-880.469 756-857.5v755q0 22.969-17.266 40.234Q721.469-45 698.5-45h-437Zm0-88.5v31h437v-31h-437Zm0-57.5h437v-578h-437v578Zm0-635.5h437v-31h-437v31Zm0 0v-31 31Zm0 693v31-31Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smartphone_300.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smartphone_300.svg index 7d8553db11..6d9d70650c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smartphone_300.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smartphone_300.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M278.533-63.078q-22.606 0-39.338-16.732t-16.732-39.327v-721.726q0-22.595 16.732-39.327t39.338-16.732h402.934q22.606 0 39.338 16.732t16.732 39.327v721.726q0 22.595-16.732 39.327t-39.338 16.732H278.533Zm-12.225-87.154v31q0 4.616 3.846 8.462 3.847 3.847 8.463 3.847h402.766q4.616 0 8.463-3.847 3.846-3.846 3.846-8.462v-31H266.308Zm0-43.845h427.384v-571.846H266.308v571.846Zm0-615.691h427.384v-31q0-4.616-3.846-8.462-3.847-3.847-8.463-3.847H278.617q-4.616 0-8.463 3.847-3.846 3.846-3.846 8.462v31Zm0 0V-853.077v43.309Zm0 659.536V-106.923v-43.309Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M278.533-63.078q-22.606 0-39.338-16.732t-16.732-39.327v-721.726q0-22.595 16.732-39.327t39.338-16.732h402.934q22.606 0 39.338 16.732t16.732 39.327v721.726q0 22.595-16.732 39.327t-39.338 16.732H278.533Zm-12.225-87.154v31q0 4.616 3.846 8.462 3.847 3.847 8.463 3.847h402.766q4.616 0 8.463-3.847 3.846-3.846 3.846-8.462v-31H266.308Zm0-43.845h427.384v-571.846H266.308v571.846Zm0-615.691h427.384v-31q0-4.616-3.846-8.462-3.847-3.847-8.463-3.847H278.617q-4.616 0-8.463 3.847-3.846 3.846-3.846 8.462v31Zm0 0V-853.077v43.309Zm0 659.536V-106.923v-43.309Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star.svg index 2a2733830a..72d7962366 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="m321 853.5 159-95 159 96-42.5-180 140-121.5L552 536.5l-72-170L408.5 536 224 552l140 121-43 180.5Zm159-28L293.5 938q-8 5-16.75 4.5T261.5 937q-6.5-5-10-12.5t-1-17.5l49-212.5L135 551q-7.5-6.5-9.25-14.5t.25-16q2-8 9-13.25t16.5-6.25L369 482l84.5-200.5q3.5-8.5 11-13T480 264q8 0 15.5 4.5t11.5 13L591.5 482 809 501q9 1 16 6.25t9 13.25q2 8 .25 16T825 551L660.5 694.5 710 907q2 10-1.5 17.5t-10 12.5q-6.5 5-15.25 5.5T667 938L480 825.5Zm0-205Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="m321 853.5 159-95 159 96-42.5-180 140-121.5L552 536.5l-72-170L408.5 536 224 552l140 121-43 180.5Zm159-28L293.5 938q-8 5-16.75 4.5T261.5 937q-6.5-5-10-12.5t-1-17.5l49-212.5L135 551q-7.5-6.5-9.25-14.5t.25-16q2-8 9-13.25t16.5-6.25L369 482l84.5-200.5q3.5-8.5 11-13T480 264q8 0 15.5 4.5t11.5 13L591.5 482 809 501q9 1 16 6.25t9 13.25q2 8 .25 16T825 551L660.5 694.5 710 907q2 10-1.5 17.5t-10 12.5q-6.5 5-15.25 5.5T667 938L480 825.5Zm0-205Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star_filled.svg index bf9bd2b4b5..22afbba02c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star_filled.svg @@ -1,2 +1,2 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M480-230.5 293.5-118q-8 5-16.75 4.5T261.5-119q-6.5-5-10-12.5t-1-17.5l49-212.5L135-505q-7.5-6.5-9.25-14.5t.25-16q2-8 9-13.25t16.5-6.25L369-574l84.5-200.5q3.5-8.5 11-13T480-792q8 0 15.5 4.5t11.5 13L591.5-574 809-555q9 1 16 6.25t9 13.25q2 8 .25 16T825-505L660.5-361.5 710-149q2 10-1.5 17.5t-10 12.5q-6.5 5-15.25 5.5T667-118L480-230.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star_off.svg index d17a1e37d9..f353793d2a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star_off.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star_off.svg @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg - height="24" + height="22" viewBox="0 -960 960 960" - width="24" + width="22" version="1.1" id="svg12" xmlns="http://www.w3.org/2000/svg"> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_start.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_start.svg index 42983718ac..f60449580b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_start.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_start.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M618.842 795.391q-8.342-8.891-8.342-20.858 0-11.966 8.5-20.533l149.5-149.5h-495q-12.25 0-20.625-8.175-8.375-8.176-8.375-20.5 0-12.325 8.375-20.575T273.5 547h495L619 397q-8.5-9-8.5-20.25t9-20.25q8.5-8.5 19.75-8.5t20.25 8.5l199 199.5q4.5 4.5 6.75 9.383 2.25 4.884 2.25 10.75 0 5.867-2.25 10.617-2.25 4.75-6.75 9.25l-199 199q-9 9-20.658 9-11.658 0-20-8.609ZM110.325 814.5q-12.325 0-20.575-8.125T81.5 786V366q0-12.25 8.425-20.625Q98.351 337 110.675 337q11.825 0 20.075 8.375T139 366v420q0 12.25-8.425 20.375-8.426 8.125-20.25 8.125Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M618.842 795.391q-8.342-8.891-8.342-20.858 0-11.966 8.5-20.533l149.5-149.5h-495q-12.25 0-20.625-8.175-8.375-8.176-8.375-20.5 0-12.325 8.375-20.575T273.5 547h495L619 397q-8.5-9-8.5-20.25t9-20.25q8.5-8.5 19.75-8.5t20.25 8.5l199 199.5q4.5 4.5 6.75 9.383 2.25 4.884 2.25 10.75 0 5.867-2.25 10.617-2.25 4.75-6.75 9.25l-199 199q-9 9-20.658 9-11.658 0-20-8.609ZM110.325 814.5q-12.325 0-20.575-8.125T81.5 786V366q0-12.25 8.425-20.625Q98.351 337 110.675 337q11.825 0 20.075 8.375T139 366v420q0 12.25-8.425 20.375-8.426 8.125-20.25 8.125Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_stop_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_stop_filled.svg index 0e8823dab5..bda4d1e10c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_stop_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_stop_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M301 812.5q-23.969 0-40.734-16.766Q243.5 778.969 243.5 755V397q0-23.969 16.766-40.734Q277.031 339.5 301 339.5h358q23.969 0 40.734 16.766Q716.5 373.031 716.5 397v358q0 23.969-16.766 40.734Q682.969 812.5 659 812.5H301Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M301 812.5q-23.969 0-40.734-16.766Q243.5 778.969 243.5 755V397q0-23.969 16.766-40.734Q277.031 339.5 301 339.5h358q23.969 0 40.734 16.766Q716.5 373.031 716.5 397v358q0 23.969-16.766 40.734Q682.969 812.5 659 812.5H301Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_supervised_user_circle_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_supervised_user_circle_filled.svg index ecfb7002a1..162ef06670 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_supervised_user_circle_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_supervised_user_circle_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M479.75 971q-80.657 0-152.815-31.263-72.159-31.263-125.797-85Q147.5 801 116.25 728.956 85 656.913 85 576q0-81.407 31.263-153.315 31.263-71.909 85-125.547Q255 243.5 327.044 212.25 399.087 181 480 181q81.407 0 153.315 31.263 71.909 31.263 125.547 85Q812.5 351 843.75 422.837 875 494.675 875 576.25q0 80.656-31.263 152.815-31.263 72.159-85 125.797Q705 908.5 633.163 939.75 561.325 971 479.75 971ZM374.121 634.5Q430 634.5 470 594.379q40-40.121 40-96T469.879 402.5q-40.121-40-96-40T278 402.621q-40 40.121-40 96t40.121 95.879q40.121 40 96 40ZM672.444 681q43.556 0 74.306-30.694 30.75-30.693 30.75-74.25 0-43.556-30.694-74.306Q716.113 471 672.556 471 629 471 598.25 501.694q-30.75 30.693-30.75 74.25 0 43.556 30.694 74.306Q628.887 681 672.444 681ZM480.25 913.5q85.25 0 156.5-38.5T754.5 772.542Q734 766.5 714.475 762.25T672.662 758Q618 758 544.5 789.75t-121 118.75q14 2 28.083 3.5 14.084 1.5 28.667 1.5ZM344.5 885q31-69 73.5-105t58.5-47q-21.5-9-48.25-13t-54.75-4q-46.5 0-90.5 12.75T198.5 763q26.5 41 63.75 71.75T344.5 885Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M479.75 971q-80.657 0-152.815-31.263-72.159-31.263-125.797-85Q147.5 801 116.25 728.956 85 656.913 85 576q0-81.407 31.263-153.315 31.263-71.909 85-125.547Q255 243.5 327.044 212.25 399.087 181 480 181q81.407 0 153.315 31.263 71.909 31.263 125.547 85Q812.5 351 843.75 422.837 875 494.675 875 576.25q0 80.656-31.263 152.815-31.263 72.159-85 125.797Q705 908.5 633.163 939.75 561.325 971 479.75 971ZM374.121 634.5Q430 634.5 470 594.379q40-40.121 40-96T469.879 402.5q-40.121-40-96-40T278 402.621q-40 40.121-40 96t40.121 95.879q40.121 40 96 40ZM672.444 681q43.556 0 74.306-30.694 30.75-30.693 30.75-74.25 0-43.556-30.694-74.306Q716.113 471 672.556 471 629 471 598.25 501.694q-30.75 30.693-30.75 74.25 0 43.556 30.694 74.306Q628.887 681 672.444 681ZM480.25 913.5q85.25 0 156.5-38.5T754.5 772.542Q734 766.5 714.475 762.25T672.662 758Q618 758 544.5 789.75t-121 118.75q14 2 28.083 3.5 14.084 1.5 28.667 1.5ZM344.5 885q31-69 73.5-105t58.5-47q-21.5-9-48.25-13t-54.75-4q-46.5 0-90.5 12.75T198.5 763q26.5 41 63.75 71.75T344.5 885Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_swap_horizontal_circle.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_swap_horizontal_circle.svg index 0b842e8716..05de300ed2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_swap_horizontal_circle.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_swap_horizontal_circle.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M326.5 695H491q11.925 0 20.212-8.25 8.288-8.25 8.288-20.5t-8.288-20.5q-8.287-8.25-20.212-8.25H326.5L391 573q9-8.5 8.75-20.483Q399.5 540.533 391 532q-9-8.5-20.75-8.5t-20.298 8.548L236 646q-9 8.909-9 20.455 0 11.545 9 20.045l114 114q8.5 9 20.5 8.5t21-9.5Q400 791 400 779t-9-21l-64.5-63Zm307-172.5-64.5 64q-9 9-8.75 20.983.25 11.984 8.75 20.517 9 8.5 20.75 8.5t20.337-8.587l114.319-114.319Q733 505 733 493.455q0-11.546-8.5-20.455l-113-113q-9-8.5-21-9t-20.5 8q-9 9-9 21.5t9 21l63.5 63.5h-164q-12.5 0-20.75 8.287-8.25 8.288-8.25 20.213 0 12.5 8.25 20.75t20.75 8.25h164ZM480.064 971Q399 971 327 939.75t-125.75-85q-53.75-53.75-85-125.686t-31.25-153Q85 494 116.25 422t85-125.5Q255 243 326.936 212t153-31q82.064 0 154.07 31.118 72.006 31.119 125.5 84.5Q813 350 844 421.936t31 154Q875 657 844 729t-84.5 125.75q-53.5 53.75-125.436 85t-154 31.25Zm-.097-57.5Q620.5 913.5 719 814.533t98.5-238.5Q817.5 435.5 719.033 337t-239-98.5q-139.533 0-238.533 98.467t-99 239q0 139.533 98.967 238.533t238.5 99ZM480 576Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M326.5 695H491q11.925 0 20.212-8.25 8.288-8.25 8.288-20.5t-8.288-20.5q-8.287-8.25-20.212-8.25H326.5L391 573q9-8.5 8.75-20.483Q399.5 540.533 391 532q-9-8.5-20.75-8.5t-20.298 8.548L236 646q-9 8.909-9 20.455 0 11.545 9 20.045l114 114q8.5 9 20.5 8.5t21-9.5Q400 791 400 779t-9-21l-64.5-63Zm307-172.5-64.5 64q-9 9-8.75 20.983.25 11.984 8.75 20.517 9 8.5 20.75 8.5t20.337-8.587l114.319-114.319Q733 505 733 493.455q0-11.546-8.5-20.455l-113-113q-9-8.5-21-9t-20.5 8q-9 9-9 21.5t9 21l63.5 63.5h-164q-12.5 0-20.75 8.287-8.25 8.288-8.25 20.213 0 12.5 8.25 20.75t20.75 8.25h164ZM480.064 971Q399 971 327 939.75t-125.75-85q-53.75-53.75-85-125.686t-31.25-153Q85 494 116.25 422t85-125.5Q255 243 326.936 212t153-31q82.064 0 154.07 31.118 72.006 31.119 125.5 84.5Q813 350 844 421.936t31 154Q875 657 844 729t-84.5 125.75q-53.5 53.75-125.436 85t-154 31.25Zm-.097-57.5Q620.5 913.5 719 814.533t98.5-238.5Q817.5 435.5 719.033 337t-239-98.5q-139.533 0-238.533 98.467t-99 239q0 139.533 98.967 238.533t238.5 99ZM480 576Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_swap_horizontal_circle_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_swap_horizontal_circle_filled.svg index 3509f870de..a71845eac7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_swap_horizontal_circle_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_swap_horizontal_circle_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M326.5 695H491q12 0 20.25-8.25T519.5 666q0-12-8.25-20.25T491 637.5H326.5L391 573q9-8.5 8.75-20.5T391 532q-9-8.5-20.75-8.5T350 532L236 646q-9 9-9 20.5t9 20l114 114q8.5 9 20.5 8.5t21-9.5Q400 791 400 779t-9-21l-64.5-63Zm307-172.5-64.5 64q-9 9-8.75 21T569 628q9 8.5 20.75 8.5T610 628l114.5-114.5q8.5-8.5 8.5-20t-8.5-20.5l-113-113q-9-8.5-21-9t-20.5 8q-9 9-9 21.5t9 21l63.5 63.5h-164q-12.5 0-20.75 8.25t-8.25 20.25q0 12.5 8.25 20.75t20.75 8.25h164ZM480 971q-81 0-153-31.25t-125.75-85Q147.5 801 116.25 729T85 576q0-82 31.25-154t85-125.5Q255 243 327 212t153-31q82 0 154 31t125.5 84.5Q813 350 844 422t31 154q0 81-31 153t-84.5 125.75q-53.5 53.75-125.5 85T480 971Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M326.5 695H491q12 0 20.25-8.25T519.5 666q0-12-8.25-20.25T491 637.5H326.5L391 573q9-8.5 8.75-20.5T391 532q-9-8.5-20.75-8.5T350 532L236 646q-9 9-9 20.5t9 20l114 114q8.5 9 20.5 8.5t21-9.5Q400 791 400 779t-9-21l-64.5-63Zm307-172.5-64.5 64q-9 9-8.75 21T569 628q9 8.5 20.75 8.5T610 628l114.5-114.5q8.5-8.5 8.5-20t-8.5-20.5l-113-113q-9-8.5-21-9t-20.5 8q-9 9-9 21.5t9 21l63.5 63.5h-164q-12.5 0-20.75 8.25t-8.25 20.25q0 12.5 8.25 20.75t20.75 8.25h164ZM480 971q-81 0-153-31.25t-125.75-85Q147.5 801 116.25 729T85 576q0-82 31.25-154t85-125.5Q255 243 327 212t153-31q82 0 154 31t125.5 84.5Q813 350 844 422t31 154q0 81-31 153t-84.5 125.75q-53.5 53.75-125.5 85T480 971Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_sync_problem.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_sync_problem.svg index 79a0441792..4f85d90659 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_sync_problem.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_sync_problem.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-284.5q-11.5 0-20-8.75t-8.5-20.25q0-11 8.5-19.75t20-8.75q11.5 0 20.25 8.75T509-313.5q0 11.5-8.75 20.25T480-284.5Zm2.325-145.5q-12.325 0-20.575-8.375T453.5-459v-189q0-11.75 8.425-20.125 8.426-8.375 20.75-8.375 11.825 0 20.075 8.375T511-648v189q0 12.25-8.425 20.625-8.426 8.375-20.25 8.375ZM190.5-477q0 65.183 28 118.591 28 53.409 90 93.409v-96q0-11.75 8.425-20.125 8.426-8.375 20.75-8.375 11.825 0 20.075 8.375T366-361v168.5q0 12.25-8.375 20.625T337.5-163.5H169q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T169-221h101.5q-64.5-48-101-108T133-477q0-88 46-165.75T311-762.5q10-4.5 20.25-.25T346-748q4 11 .25 21.75T332.5-711q-62.5 32-102.25 94.881Q190.5-553.237 190.5-477Zm589.5-6.5q0-64.683-28.25-118.091Q723.5-655 661.5-695v95.5q0 12.25-8.425 20.625-8.426 8.375-20.25 8.375-12.325 0-20.575-8.375T604-599.5V-768q0-11.75 8.375-20.125T633-796.5h168.5q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T801.5-739H700q64 48 100.75 108t36.75 147.5q0 88.5-46.25 166.75T659-197q-10 5-20.25.25T624.5-212q-4.5-10.5-.75-21.5t14.25-16q62-31.5 102-94.381 40-62.882 40-139.619Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M480-284.5q-11.5 0-20-8.75t-8.5-20.25q0-11 8.5-19.75t20-8.75q11.5 0 20.25 8.75T509-313.5q0 11.5-8.75 20.25T480-284.5Zm2.325-145.5q-12.325 0-20.575-8.375T453.5-459v-189q0-11.75 8.425-20.125 8.426-8.375 20.75-8.375 11.825 0 20.075 8.375T511-648v189q0 12.25-8.425 20.625-8.426 8.375-20.25 8.375ZM190.5-477q0 65.183 28 118.591 28 53.409 90 93.409v-96q0-11.75 8.425-20.125 8.426-8.375 20.75-8.375 11.825 0 20.075 8.375T366-361v168.5q0 12.25-8.375 20.625T337.5-163.5H169q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T169-221h101.5q-64.5-48-101-108T133-477q0-88 46-165.75T311-762.5q10-4.5 20.25-.25T346-748q4 11 .25 21.75T332.5-711q-62.5 32-102.25 94.881Q190.5-553.237 190.5-477Zm589.5-6.5q0-64.683-28.25-118.091Q723.5-655 661.5-695v95.5q0 12.25-8.425 20.625-8.426 8.375-20.25 8.375-12.325 0-20.575-8.375T604-599.5V-768q0-11.75 8.375-20.125T633-796.5h168.5q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T801.5-739H700q64 48 100.75 108t36.75 147.5q0 88.5-46.25 166.75T659-197q-10 5-20.25.25T624.5-212q-4.5-10.5-.75-21.5t14.25-16q62-31.5 102-94.381 40-62.882 40-139.619Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_tag.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_tag.svg index 4b7e39b8ec..535d97a168 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_tag.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_tag.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M286.81 894.5q-13.757 0-22.034-10.5-8.276-10.5-4.276-24l34-135.5H178q-13.718 0-22.609-10.82-8.891-10.82-4.808-24.456 2.05-9.071 9.47-15.647Q167.472 667 178 667h131.097L354.5 484.5H218q-13.718 0-22.609-10.82-8.891-10.82-4.808-24.456 2.05-9.071 9.47-15.647Q207.472 427 218 427h151.194L406.5 278.5q2-9.448 8.934-15.474Q422.368 257 432.575 257q13.588 0 21.256 10.75Q461.5 278.5 459 291.292L425.5 427H609l37.5-148.5q2-9.448 8.934-15.474Q662.368 257 672.576 257q13.587 0 21.255 10.75Q701.5 278.5 699 291.292L665.5 427H782q13.718 0 22.609 11.07Q813.5 449.14 810 462.5q-2.5 9.5-10.042 15.75-7.541 6.25-17.958 6.25H650.903L605.5 667H742q13.718 0 22.609 11.07Q773.5 689.14 770 702.5q-2.5 9.5-10.042 15.75-7.541 6.25-17.958 6.25H591.204L554 873.5q-2.5 7.5-8.434 14.25-5.934 6.75-18.756 6.75-13.757 0-22.034-10.5-8.276-10.5-4.276-24l34-135.5H350.987L314 873.5q-2.5 7.5-8.434 14.25-5.934 6.75-18.756 6.75ZM365.5 667h183.4l45.6-182.5H411.1L365.5 667Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M286.81 894.5q-13.757 0-22.034-10.5-8.276-10.5-4.276-24l34-135.5H178q-13.718 0-22.609-10.82-8.891-10.82-4.808-24.456 2.05-9.071 9.47-15.647Q167.472 667 178 667h131.097L354.5 484.5H218q-13.718 0-22.609-10.82-8.891-10.82-4.808-24.456 2.05-9.071 9.47-15.647Q207.472 427 218 427h151.194L406.5 278.5q2-9.448 8.934-15.474Q422.368 257 432.575 257q13.588 0 21.256 10.75Q461.5 278.5 459 291.292L425.5 427H609l37.5-148.5q2-9.448 8.934-15.474Q662.368 257 672.576 257q13.587 0 21.255 10.75Q701.5 278.5 699 291.292L665.5 427H782q13.718 0 22.609 11.07Q813.5 449.14 810 462.5q-2.5 9.5-10.042 15.75-7.541 6.25-17.958 6.25H650.903L605.5 667H742q13.718 0 22.609 11.07Q773.5 689.14 770 702.5q-2.5 9.5-10.042 15.75-7.541 6.25-17.958 6.25H591.204L554 873.5q-2.5 7.5-8.434 14.25-5.934 6.75-18.756 6.75-13.757 0-22.034-10.5-8.276-10.5-4.276-24l34-135.5H350.987L314 873.5q-2.5 7.5-8.434 14.25-5.934 6.75-18.756 6.75ZM365.5 667h183.4l45.6-182.5H411.1L365.5 667Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_task.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_task.svg index eab8e8f09d..e8120529a3 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_task.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_task.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="m434.5 723.5-85-84.5q-4.314-4-8.764-5.75-4.45-1.75-9.218-1.75t-9.643 2q-4.875 2-8.875 6.422-8 8.109-8 18.844 0 10.734 8 18.234l101 100.5q4.5 5 9.283 7 4.784 2 10.75 2 5.967 0 10.717-2 4.75-2 9.75-7L642 590q8-7.978 8-18.489Q650 561 642.017 553t-18.5-8Q613 545 605 553L434.5 723.5ZM222 971q-22.969 0-40.234-17.266Q164.5 936.469 164.5 913.5v-675q0-22.969 17.266-40.234Q199.031 181 222 181h335q11.911 0 22.706 4.75 10.794 4.75 18.912 12.343l179.264 179.314Q786 385.5 790.75 396.294q4.75 10.795 4.75 22.706v494.5q0 22.969-17.266 40.234Q760.969 971 738 971H222Zm330.5-579V238.5H222v675h516V421H581.5q-12.25 0-20.625-8.375T552.5 392ZM222 238.5V421 238.5v675-675Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="m434.5 723.5-85-84.5q-4.314-4-8.764-5.75-4.45-1.75-9.218-1.75t-9.643 2q-4.875 2-8.875 6.422-8 8.109-8 18.844 0 10.734 8 18.234l101 100.5q4.5 5 9.283 7 4.784 2 10.75 2 5.967 0 10.717-2 4.75-2 9.75-7L642 590q8-7.978 8-18.489Q650 561 642.017 553t-18.5-8Q613 545 605 553L434.5 723.5ZM222 971q-22.969 0-40.234-17.266Q164.5 936.469 164.5 913.5v-675q0-22.969 17.266-40.234Q199.031 181 222 181h335q11.911 0 22.706 4.75 10.794 4.75 18.912 12.343l179.264 179.314Q786 385.5 790.75 396.294q4.75 10.795 4.75 22.706v494.5q0 22.969-17.266 40.234Q760.969 971 738 971H222Zm330.5-579V238.5H222v675h516V421H581.5q-12.25 0-20.625-8.375T552.5 392ZM222 238.5V421 238.5v675-675Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_theater_comedy.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_theater_comedy.svg index 24267d0fa8..e6a78e6923 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_theater_comedy.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_theater_comedy.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M770.193 380q12.407 0 21.607-9.5t9.2-20.75q0-12.85-9.125-22.05-9.125-9.2-21.875-9.2-13.325 0-21.912 9.131-8.588 9.132-8.588 21.869 0 11.5 8.643 21t22.05 9.5Zm-171.14 0q12.547 0 21.747-9.5t9.2-20.75q0-12.85-9.104-22.05-9.103-9.2-21.8-9.2-12.696 0-21.646 9.131-8.95 9.132-8.95 21.869 0 11.5 9.003 21 9.002 9.5 21.55 9.5Zm9.865 145.5h149.164q14.418 0 21.418-10t0-20.5q-13.5-18.5-39.75-28.25T683.5 457q-30 0-56.25 9.75T587.5 495q-7 10.5 0 20.5t21.418 10ZM276.162 970q-95.662 0-162.912-68.979T46 739.5V534q0-23.719 16.89-40.609Q79.782 476.5 103.5 476.5H449q23.719 0 40.609 16.891Q506.5 510.281 506.5 534v205.5q0 92.542-67.338 161.521Q371.824 970 276.162 970Zm.497-57.5q71.341 0 121.841-52.25T449 739.5V534H103.5v205.5q0 68.5 50.6 120.75t122.559 52.25ZM683.5 674q-33.76 0-66.015-8.625T556 640.5V576q26 15.5 58.5 28t69 12.5q71.894 0 122.447-47.75T856.5 452.5V238h-349v191H450V238q0-23.719 16.891-40.609Q483.781 180.5 507.5 180.5h349q23.719 0 40.609 16.891Q914 214.281 914 238v214.5q0 91.542-67.562 156.521Q778.875 674 683.5 674Zm-490.947-6q12.772 0 21.61-8.797Q223 650.407 223 637.404q0-13.004-8.796-21.954-8.797-8.95-21.8-8.95-13.004 0-21.954 8.928-8.95 8.927-8.95 22.125 0 12.772 8.928 21.609Q179.355 668 192.553 668Zm171.197 0q12.85 0 21.8-8.797 8.95-8.796 8.95-21.799 0-13.004-8.987-21.954-8.988-8.95-21.513-8.95-13.1 0-22.05 8.928-8.95 8.927-8.95 22.125 0 12.772 8.95 21.609Q350.9 668 363.75 668Zm-84.08 154.5q29.83 0 56.58-9.75t40.312-29.25q6.438-10-1.062-20t-21.565-10H205.5q-14.5 0-22 10t-.562 20Q196 803 222.34 812.75q26.341 9.75 57.33 9.75Zm-3.17-100Zm401.5-296Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M770.193 380q12.407 0 21.607-9.5t9.2-20.75q0-12.85-9.125-22.05-9.125-9.2-21.875-9.2-13.325 0-21.912 9.131-8.588 9.132-8.588 21.869 0 11.5 8.643 21t22.05 9.5Zm-171.14 0q12.547 0 21.747-9.5t9.2-20.75q0-12.85-9.104-22.05-9.103-9.2-21.8-9.2-12.696 0-21.646 9.131-8.95 9.132-8.95 21.869 0 11.5 9.003 21 9.002 9.5 21.55 9.5Zm9.865 145.5h149.164q14.418 0 21.418-10t0-20.5q-13.5-18.5-39.75-28.25T683.5 457q-30 0-56.25 9.75T587.5 495q-7 10.5 0 20.5t21.418 10ZM276.162 970q-95.662 0-162.912-68.979T46 739.5V534q0-23.719 16.89-40.609Q79.782 476.5 103.5 476.5H449q23.719 0 40.609 16.891Q506.5 510.281 506.5 534v205.5q0 92.542-67.338 161.521Q371.824 970 276.162 970Zm.497-57.5q71.341 0 121.841-52.25T449 739.5V534H103.5v205.5q0 68.5 50.6 120.75t122.559 52.25ZM683.5 674q-33.76 0-66.015-8.625T556 640.5V576q26 15.5 58.5 28t69 12.5q71.894 0 122.447-47.75T856.5 452.5V238h-349v191H450V238q0-23.719 16.891-40.609Q483.781 180.5 507.5 180.5h349q23.719 0 40.609 16.891Q914 214.281 914 238v214.5q0 91.542-67.562 156.521Q778.875 674 683.5 674Zm-490.947-6q12.772 0 21.61-8.797Q223 650.407 223 637.404q0-13.004-8.796-21.954-8.797-8.95-21.8-8.95-13.004 0-21.954 8.928-8.95 8.927-8.95 22.125 0 12.772 8.928 21.609Q179.355 668 192.553 668Zm171.197 0q12.85 0 21.8-8.797 8.95-8.796 8.95-21.799 0-13.004-8.987-21.954-8.988-8.95-21.513-8.95-13.1 0-22.05 8.928-8.95 8.927-8.95 22.125 0 12.772 8.95 21.609Q350.9 668 363.75 668Zm-84.08 154.5q29.83 0 56.58-9.75t40.312-29.25q6.438-10-1.062-20t-21.565-10H205.5q-14.5 0-22 10t-.562 20Q196 803 222.34 812.75q26.341 9.75 57.33 9.75Zm-3.17-100Zm401.5-296Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_theater_comedy_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_theater_comedy_filled.svg index 401e9affac..c1a3753674 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_theater_comedy_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_theater_comedy_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M770.193 380q12.407 0 21.607-9.5t9.2-21q0-12.6-9.125-21.8-9.125-9.2-21.875-9.2-13.325 0-21.912 9.2-8.588 9.2-8.588 21.8 0 11.5 8.643 21t22.05 9.5ZM276.412 970q-95.912 0-163.162-69.062Q46 831.875 46 739.5V534q0-23.688 16.906-40.594Q79.813 476.5 103.5 476.5H449q23.688 0 40.594 16.906Q506.5 510.312 506.5 534v205.5q0 92.375-67.338 161.438Q371.824 970 276.412 970ZM192.5 668q12.825 0 21.663-8.893 8.837-8.893 8.837-21.8t-8.893-21.857q-8.893-8.95-21.8-8.95t-21.857 8.875q-8.95 8.875-8.95 22.125 0 12.825 8.875 21.662Q179.25 668 192.5 668Zm171.5 0q12.6 0 21.55-8.893 8.95-8.893 8.95-21.8t-8.95-21.857q-8.95-8.95-21.55-8.95-13.1 0-22.05 8.875Q333 624.25 333 637.5q0 12.825 8.95 21.662Q350.9 668 364 668Zm-84.5 154.5q30 0 56.75-9.75t40.25-29.25q6.5-10-1-20t-21.565-10H205.5q-14.5 0-21.75 10.167-7.25 10.166-.75 19.833 13 19.5 39.407 29.25 26.408 9.75 57.093 9.75Zm404-148.5q-33.76 0-66.015-8.75T556 640.5v-154l32 8q-7 10.5-.25 20.75t21.168 10.25h149.164q14.418 0 21.418-10t0-20.5q-13.5-18.5-39.75-28.25T683.5 457q-30 0-56 9.5t-39.5 28l-32-8q0-13-8-28.25t-26.5-23.75l56.75-63q3.75 5 9.496 6.75t11.741 1.75Q612 380 621 370.5q9-9.5 9-21 0-12.6-9.2-21.8-9.2-9.2-21.8-9.2-12.6 0-21.55 9.2-8.95 9.2-8.95 21.94 0 6.36 2.25 11.86 2.25 5.5 7.25 10l-56.5 63q-4.939-2.267-10.372-3.883Q505.696 429 498.5 429H450V238q0-23.688 16.906-40.594Q483.812 180.5 507.5 180.5h349q23.688 0 40.594 16.906Q914 214.312 914 238v214.5q0 91.375-67.562 156.438Q778.875 674 683.5 674Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M770.193 380q12.407 0 21.607-9.5t9.2-21q0-12.6-9.125-21.8-9.125-9.2-21.875-9.2-13.325 0-21.912 9.2-8.588 9.2-8.588 21.8 0 11.5 8.643 21t22.05 9.5ZM276.412 970q-95.912 0-163.162-69.062Q46 831.875 46 739.5V534q0-23.688 16.906-40.594Q79.813 476.5 103.5 476.5H449q23.688 0 40.594 16.906Q506.5 510.312 506.5 534v205.5q0 92.375-67.338 161.438Q371.824 970 276.412 970ZM192.5 668q12.825 0 21.663-8.893 8.837-8.893 8.837-21.8t-8.893-21.857q-8.893-8.95-21.8-8.95t-21.857 8.875q-8.95 8.875-8.95 22.125 0 12.825 8.875 21.662Q179.25 668 192.5 668Zm171.5 0q12.6 0 21.55-8.893 8.95-8.893 8.95-21.8t-8.95-21.857q-8.95-8.95-21.55-8.95-13.1 0-22.05 8.875Q333 624.25 333 637.5q0 12.825 8.95 21.662Q350.9 668 364 668Zm-84.5 154.5q30 0 56.75-9.75t40.25-29.25q6.5-10-1-20t-21.565-10H205.5q-14.5 0-21.75 10.167-7.25 10.166-.75 19.833 13 19.5 39.407 29.25 26.408 9.75 57.093 9.75Zm404-148.5q-33.76 0-66.015-8.75T556 640.5v-154l32 8q-7 10.5-.25 20.75t21.168 10.25h149.164q14.418 0 21.418-10t0-20.5q-13.5-18.5-39.75-28.25T683.5 457q-30 0-56 9.5t-39.5 28l-32-8q0-13-8-28.25t-26.5-23.75l56.75-63q3.75 5 9.496 6.75t11.741 1.75Q612 380 621 370.5q9-9.5 9-21 0-12.6-9.2-21.8-9.2-9.2-21.8-9.2-12.6 0-21.55 9.2-8.95 9.2-8.95 21.94 0 6.36 2.25 11.86 2.25 5.5 7.25 10l-56.5 63q-4.939-2.267-10.372-3.883Q505.696 429 498.5 429H450V238q0-23.688 16.906-40.594Q483.812 180.5 507.5 180.5h349q23.688 0 40.594 16.906Q914 214.312 914 238v214.5q0 91.375-67.562 156.438Q778.875 674 683.5 674Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_timer.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_timer.svg index e530ef1e83..8f50769f03 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_timer.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_timer.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M390.5 197.5q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.5 0-12.075 8.375-20.325T390.5 140H570q11.675 0 20.088 8.463 8.412 8.463 8.412 20.212 0 12.325-8.412 20.575-8.413 8.25-20.088 8.25H390.5ZM480.175 641q12.325 0 20.575-8.375T509 612V443q0-11.675-8.425-20.088-8.426-8.412-20.75-8.412-12.325 0-20.325 8.412-8 8.413-8 20.088v169q0 12.25 8.175 20.625 8.176 8.375 20.5 8.375Zm-.201 329q-73.026 0-137.802-28.106-64.775-28.107-113.11-76.542-48.335-48.436-76.449-113.181Q124.5 687.425 124.5 614.365q0-72.636 28.132-137.486 28.133-64.851 76.5-113.365Q277.5 315 342.201 287 406.902 259 480 259q68 0 125.5 22.5t103 62l30-30q8.5-8.5 20-8.5t20.5 8.5q8.5 9 8.5 20.25T779 354l-30 30q35.5 38.5 61 94.5t25.5 136q0 73.052-28.003 137.714-28.002 64.661-76.466 113.068-48.464 48.408-113.248 76.563Q553 970 479.974 970Zm.258-57.5q124.268 0 211.018-86.982Q778 738.535 778 614.268 778 490 691.018 403.25q-86.983-86.75-211.25-86.75-124.268 0-211.018 86.982Q182 490.465 182 614.732 182 739 268.982 825.75q86.983 86.75 211.25 86.75Zm-.232-297Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M390.5 197.5q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.5 0-12.075 8.375-20.325T390.5 140H570q11.675 0 20.088 8.463 8.412 8.463 8.412 20.212 0 12.325-8.412 20.575-8.413 8.25-20.088 8.25H390.5ZM480.175 641q12.325 0 20.575-8.375T509 612V443q0-11.675-8.425-20.088-8.426-8.412-20.75-8.412-12.325 0-20.325 8.412-8 8.413-8 20.088v169q0 12.25 8.175 20.625 8.176 8.375 20.5 8.375Zm-.201 329q-73.026 0-137.802-28.106-64.775-28.107-113.11-76.542-48.335-48.436-76.449-113.181Q124.5 687.425 124.5 614.365q0-72.636 28.132-137.486 28.133-64.851 76.5-113.365Q277.5 315 342.201 287 406.902 259 480 259q68 0 125.5 22.5t103 62l30-30q8.5-8.5 20-8.5t20.5 8.5q8.5 9 8.5 20.25T779 354l-30 30q35.5 38.5 61 94.5t25.5 136q0 73.052-28.003 137.714-28.002 64.661-76.466 113.068-48.464 48.408-113.248 76.563Q553 970 479.974 970Zm.258-57.5q124.268 0 211.018-86.982Q778 738.535 778 614.268 778 490 691.018 403.25q-86.983-86.75-211.25-86.75-124.268 0-211.018 86.982Q182 490.465 182 614.732 182 739 268.982 825.75q86.983 86.75 211.25 86.75Zm-.232-297Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_timer_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_timer_filled.svg index 4e0a99775a..a7576c6ee2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_timer_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_timer_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M390.5 197.5q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T390.5 140H570q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T570 197.5H390.5ZM480.175 641q12.325 0 20.575-8.375T509 612V443q0-11.75-8.425-20.125-8.426-8.375-20.75-8.375-12.325 0-20.325 8.375t-8 20.125v169q0 12.25 8.175 20.625 8.176 8.375 20.5 8.375ZM480 970q-73 0-137.75-28T229 865.5q-48.5-48.5-76.5-113.25t-28-137.75q0-73 28-137.75T229 363.5Q277.5 315 342.25 287T480 259q68 0 125 22t102.5 61.5l30-30Q746 304 758 304t21 8.5q8.5 9 8.5 20.75T779 354l-30 30q35.5 38.5 61 94.5t25.5 136q0 73-28 137.75T731 865.5Q682.5 914 617.75 942T480 970Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M390.5 197.5q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T390.5 140H570q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T570 197.5H390.5ZM480.175 641q12.325 0 20.575-8.375T509 612V443q0-11.75-8.425-20.125-8.426-8.375-20.75-8.375-12.325 0-20.325 8.375t-8 20.125v169q0 12.25 8.175 20.625 8.176 8.375 20.5 8.375ZM480 970q-73 0-137.75-28T229 865.5q-48.5-48.5-76.5-113.25t-28-137.75q0-73 28-137.75T229 363.5Q277.5 315 342.25 287T480 259q68 0 125 22t102.5 61.5l30-30Q746 304 758 304t21 8.5q8.5 9 8.5 20.75T779 354l-30 30q35.5 38.5 61 94.5t25.5 136q0 73-28 137.75T731 865.5Q682.5 914 617.75 942T480 970Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_toast.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_toast.svg new file mode 100644 index 0000000000..dccf6dff3d --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_toast.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22" fill="#000000"> + <path + d="M269.5-240.5h421q11.67 0 20.09-8.43 8.41-8.42 8.41-20.5 0-12.07-8.41-20.32-8.42-8.25-20.09-8.25h-421q-12.25 0-20.62 8.46-8.38 8.47-8.38 20.22 0 12.32 8.38 20.57 8.37 8.25 20.62 8.25Zm-87.5 116q-22.97 0-40.23-17.27Q124.5-159.03 124.5-182v-596q0-22.97 17.27-40.23Q159.03-835.5 182-835.5h596q22.97 0 40.23 17.27Q835.5-800.97 835.5-778v596q0 22.97-17.27 40.23Q800.97-124.5 778-124.5H182Zm0-57.5h596v-596H182v596Zm0-596v596-596Z" /> +</svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_toggle_on.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_toggle_on.svg index de240dce5f..02e068f30a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_toggle_on.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_toggle_on.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M281.5 812.5q-98.542 0-167.521-68.971Q45 674.559 45 576.029 45 477.5 113.979 408.5t167.521-69h397q98.542 0 167.521 68.971Q915 477.441 915 575.971q0 98.529-68.979 167.529t-167.521 69h-397Zm0-57.5h396.857q74.972 0 127.057-52.062 52.086-52.062 52.086-127T805.414 449q-52.085-52-127.057-52H281.5q-74.5 0-126.75 52.062t-52.25 127Q102.5 651 154.75 703t126.75 52Zm397.941-81q40.809 0 69.434-28.566t28.625-69.375q0-40.809-28.566-69.434T679.559 478q-40.809 0-69.434 28.566T581.5 575.941q0 40.809 28.566 69.434T679.441 674ZM480 576Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M281.5 812.5q-98.542 0-167.521-68.971Q45 674.559 45 576.029 45 477.5 113.979 408.5t167.521-69h397q98.542 0 167.521 68.971Q915 477.441 915 575.971q0 98.529-68.979 167.529t-167.521 69h-397Zm0-57.5h396.857q74.972 0 127.057-52.062 52.086-52.062 52.086-127T805.414 449q-52.085-52-127.057-52H281.5q-74.5 0-126.75 52.062t-52.25 127Q102.5 651 154.75 703t126.75 52Zm397.941-81q40.809 0 69.434-28.566t28.625-69.375q0-40.809-28.566-69.434T679.559 478q-40.809 0-69.434 28.566T581.5 575.941q0 40.809 28.566 69.434T679.441 674ZM480 576Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_translate.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_translate.svg index 8ad343c088..6b05619e01 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_translate.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_translate.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M530.544 971q-22.353 0-32.948-14.638Q487 941.723 495 920l145.325-385.237q5.931-15.803 21.525-26.783Q677.443 497 694.5 497q16.5 0 32.862 10.955Q743.725 518.91 750 534.5L898.5 916q8.5 23-2.593 39t-35.814 16q-11.287 0-21.522-7-10.235-7-13.77-18L792.5 846.5H602.418l-36.875 100.361Q561.557 957 551.553 964t-21.009 7ZM624.5 777h140l-69.054-191.5H693.5l-69 191.5ZM292.088 439.707q14.28 25.167 32.146 49.576Q342.1 513.693 362.69 539q44.487-45.908 74.148-95.704Q466.5 393.5 487 339H85.46q-16.77 0-28.115-11.341Q46 316.318 46 299.553q0-16.766 11.354-28.13 11.353-11.365 28.137-11.365h236.947v-39.529q0-16.8 11.353-28.164Q345.145 181 361.929 181q16.783 0 28.143 11.35 11.359 11.35 11.359 28.128v39.478H638.49q16.792 0 28.151 11.35Q678 282.656 678 299.434q0 16.779-11.356 28.172Q655.288 339 638.5 339h-72.216q-21.784 68-58.026 133.688Q472.016 538.376 420 597.5l97 97.361L487 775 362 655 191.5 825.5q-11.304 11.5-27 11.5T138 824.5q-12.5-10.804-12.5-26.5t12.5-28l173.5-173.5Q285.5 566 263 535t-41-64q-12-21.604-2.382-37.302Q229.235 418 255.5 418q10.5 0 21 6.25t15.588 15.457Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M530.544 971q-22.353 0-32.948-14.638Q487 941.723 495 920l145.325-385.237q5.931-15.803 21.525-26.783Q677.443 497 694.5 497q16.5 0 32.862 10.955Q743.725 518.91 750 534.5L898.5 916q8.5 23-2.593 39t-35.814 16q-11.287 0-21.522-7-10.235-7-13.77-18L792.5 846.5H602.418l-36.875 100.361Q561.557 957 551.553 964t-21.009 7ZM624.5 777h140l-69.054-191.5H693.5l-69 191.5ZM292.088 439.707q14.28 25.167 32.146 49.576Q342.1 513.693 362.69 539q44.487-45.908 74.148-95.704Q466.5 393.5 487 339H85.46q-16.77 0-28.115-11.341Q46 316.318 46 299.553q0-16.766 11.354-28.13 11.353-11.365 28.137-11.365h236.947v-39.529q0-16.8 11.353-28.164Q345.145 181 361.929 181q16.783 0 28.143 11.35 11.359 11.35 11.359 28.128v39.478H638.49q16.792 0 28.151 11.35Q678 282.656 678 299.434q0 16.779-11.356 28.172Q655.288 339 638.5 339h-72.216q-21.784 68-58.026 133.688Q472.016 538.376 420 597.5l97 97.361L487 775 362 655 191.5 825.5q-11.304 11.5-27 11.5T138 824.5q-12.5-10.804-12.5-26.5t12.5-28l173.5-173.5Q285.5 566 263 535t-41-64q-12-21.604-2.382-37.302Q229.235 418 255.5 418q10.5 0 21 6.25t15.588 15.457Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_travel_explore.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_travel_explore.svg index 023ce42a26..1cacbed166 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_travel_explore.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_travel_explore.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M458 970q-78-4-146.006-36.618-68.006-32.619-118.487-85.4-50.481-52.782-79.494-122.893Q85 654.978 85 575.989 85 494 116.118 421.994q31.119-72.006 84.409-125.487 53.29-53.481 125.432-84.494Q398.102 181 480.127 181q146.873 0 255.76 92.86T869.5 507h-58.436Q794 423.5 740.289 357.775 686.579 292.049 606 259v18.083q0 34.76-23.706 60.588-23.706 25.829-58.732 25.829H437.5v85.825q0 17.175-13.548 28.231-13.548 11.056-30.619 11.056H311V576h111.5v124.5h-68l-202.985-203q-5.015 19.75-7.015 39.169-2 19.419-2 39.338 0 134.729 90.25 231.611Q323 904.5 458 912.5V970Zm371.05-44.95L716 813.5q-21.5 15.5-46.08 23.25t-50.279 7.75q-70.163 0-119.152-49.088-48.989-49.089-48.989-119.5 0-70.412 49.088-119.662Q549.677 507 620.088 507q70.412 0 119.662 49.246Q789 605.493 789 676.13q0 25.87-8.5 50.37-8.5 24.5-23 46.5l112 111.55q9 8.95 9.25 20.45t-8.25 21q-9 9-21 8.5t-20.45-9.45ZM620.056 787Q666.5 787 699 754.694q32.5-32.307 32.5-78.75 0-46.444-32.408-78.944-32.409-32.5-79-32.5-46.592 0-78.842 32.408-32.25 32.409-32.25 79 0 46.592 32.306 78.842Q573.613 787 620.056 787Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M458 970q-78-4-146.006-36.618-68.006-32.619-118.487-85.4-50.481-52.782-79.494-122.893Q85 654.978 85 575.989 85 494 116.118 421.994q31.119-72.006 84.409-125.487 53.29-53.481 125.432-84.494Q398.102 181 480.127 181q146.873 0 255.76 92.86T869.5 507h-58.436Q794 423.5 740.289 357.775 686.579 292.049 606 259v18.083q0 34.76-23.706 60.588-23.706 25.829-58.732 25.829H437.5v85.825q0 17.175-13.548 28.231-13.548 11.056-30.619 11.056H311V576h111.5v124.5h-68l-202.985-203q-5.015 19.75-7.015 39.169-2 19.419-2 39.338 0 134.729 90.25 231.611Q323 904.5 458 912.5V970Zm371.05-44.95L716 813.5q-21.5 15.5-46.08 23.25t-50.279 7.75q-70.163 0-119.152-49.088-48.989-49.089-48.989-119.5 0-70.412 49.088-119.662Q549.677 507 620.088 507q70.412 0 119.662 49.246Q789 605.493 789 676.13q0 25.87-8.5 50.37-8.5 24.5-23 46.5l112 111.55q9 8.95 9.25 20.45t-8.25 21q-9 9-21 8.5t-20.45-9.45ZM620.056 787Q666.5 787 699 754.694q32.5-32.307 32.5-78.75 0-46.444-32.408-78.944-32.409-32.5-79-32.5-46.592 0-78.842 32.408-32.25 32.409-32.25 79 0 46.592 32.306 78.842Q573.613 787 620.056 787Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_upgrade.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_upgrade.svg new file mode 100644 index 0000000000..ab8fb7d951 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_upgrade.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22" fill="#000000"> + <path + d="M310-165q-12.5 0-20.5-8.25t-8-20.75q0-12 8-20.25t20.5-8.25h340q12.5 0 20.75 8.25T679-194q0 12.5-8.25 20.75T650-165H310Zm170-165q-12.5 0-20.5-8.25t-8-20.75v-327L349-583.5q-8.5 8.5-20 8.5t-20-8.5q-9-9-9-20.25t9-20.25l151-151q4.5-4.5 9.25-6.75T480-784q6 0 10.75 2.25t9.75 6.75l151 151q8.5 9 8.5 20.25T651.5-584q-9 9-20.25 9T611-584L509-686v327q0 12.5-8.25 20.75T480-330Z" /> +</svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_upload_file.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_upload_file.svg index 1840509fb1..d84e8bc513 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_upload_file.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_upload_file.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M453.5 652.5v171q0 12.25 8.535 20.375t20.39 8.125q11.856 0 20.216-8.125Q511 835.75 511 823.5v-171l62.071 61.571q2.5 2.429 14.215-2.571 11.714-5 22.214-9 10.5-4 14.15-3.25 3.65.75-10.15 14.75 8.5-8.5 8-20.25t-9.167-20.344L500.389 564.587q-4.583-4.548-9.464-6.817-4.881-2.27-10.458-2.27-5.753 0-10.753 2.25T460 564.5L349.5 675q-9 9-9 20.75t8.5 20.88q8.5 8.37 20.5 8.12t20.372-8.622L453.5 652.5ZM222 971q-22.969 0-40.234-17.266Q164.5 936.469 164.5 913.5v-675q0-22.969 17.266-40.234Q199.031 181 222 181h335q11.911 0 22.706 4.75 10.794 4.75 18.912 12.343l179.264 179.314Q786 385.5 790.75 396.294q4.75 10.795 4.75 22.706v494.5q0 22.969-17.266 40.234Q760.969 971 738 971H222Zm330.5-579V238.5H222v675h516V421H581.5q-12.25 0-20.625-8.375T552.5 392ZM222 238.5V421 238.5v675-675Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M453.5 652.5v171q0 12.25 8.535 20.375t20.39 8.125q11.856 0 20.216-8.125Q511 835.75 511 823.5v-171l62.071 61.571q2.5 2.429 14.215-2.571 11.714-5 22.214-9 10.5-4 14.15-3.25 3.65.75-10.15 14.75 8.5-8.5 8-20.25t-9.167-20.344L500.389 564.587q-4.583-4.548-9.464-6.817-4.881-2.27-10.458-2.27-5.753 0-10.753 2.25T460 564.5L349.5 675q-9 9-9 20.75t8.5 20.88q8.5 8.37 20.5 8.12t20.372-8.622L453.5 652.5ZM222 971q-22.969 0-40.234-17.266Q164.5 936.469 164.5 913.5v-675q0-22.969 17.266-40.234Q199.031 181 222 181h335q11.911 0 22.706 4.75 10.794 4.75 18.912 12.343l179.264 179.314Q786 385.5 790.75 396.294q4.75 10.795 4.75 22.706v494.5q0 22.969-17.266 40.234Q760.969 971 738 971H222Zm330.5-579V238.5H222v675h516V421H581.5q-12.25 0-20.625-8.375T552.5 392ZM222 238.5V421 238.5v675-675Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_usb.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_usb.svg new file mode 100644 index 0000000000..0663015ae4 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_usb.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M480.25-84.5q-31.013 0-50.631-19.559Q410-123.619 410-155q0-21.5 10.75-39.25t30.75-28.197V-334H303q-22.969 0-40.234-17.266Q245.5-368.531 245.5-391.5v-109q-20.5-9-31.25-26.359-10.75-17.358-10.75-39.358 0-31.664 19.535-51.224Q242.569-637 273.916-637t50.965 19.559q19.619 19.56 19.619 51.352 0 22.089-10.75 39.339T303-500.5v109h148.5v-338H373l107-145 107 145h-78v338h148V-496h-41.5v-140.5h141V-496h-42v104.5q0 22.969-17.266 40.234Q679.969-334 657-334H509v111.5q19.95 10.152 30.975 28.733Q551-175.187 551-154.912q0 31.531-19.869 50.971Q511.263-84.5 480.25-84.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_verified_user.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_verified_user.svg index cd6e89db86..7506004c1d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_verified_user.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_verified_user.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="m439.5 624-80.179-80.088Q351 535.5 338.5 535.5q-12.5 0-21 8.5-8.962 9.033-8.731 21.517Q309 578 317.5 586.5l99 99q8.591 8.5 20.545 8.5 11.955 0 20.455-8.5l183.901-183.901Q649 494 649 481.7t-7.5-20.2q-7.9-8-19.7-8-11.8 0-19.8 8L439.5 624ZM480 968q-3.5 0-7-.75t-6.544-1.297Q334 926.5 249.25 803.193q-84.75-123.308-84.75-270.39V338.269q0-17.749 10.524-32.783Q185.548 290.453 202 284l258-96.5q10.25-3.5 20-3.5t20.5 3.5l258 96.5q15.952 6.453 26.476 21.486Q795.5 320.52 795.5 338.269v194.534q0 147.082-84.75 270.39Q626 926.5 493.5 966q1.5-.5-13.5 2Zm0-57.5q114-38 186-142.853Q738 662.793 738 533V338.387L480 241.5l-258 96.887V533q0 129.793 72 234.647Q366 872.5 480 910.5Zm0-334.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="m439.5 624-80.179-80.088Q351 535.5 338.5 535.5q-12.5 0-21 8.5-8.962 9.033-8.731 21.517Q309 578 317.5 586.5l99 99q8.591 8.5 20.545 8.5 11.955 0 20.455-8.5l183.901-183.901Q649 494 649 481.7t-7.5-20.2q-7.9-8-19.7-8-11.8 0-19.8 8L439.5 624ZM480 968q-3.5 0-7-.75t-6.544-1.297Q334 926.5 249.25 803.193q-84.75-123.308-84.75-270.39V338.269q0-17.749 10.524-32.783Q185.548 290.453 202 284l258-96.5q10.25-3.5 20-3.5t20.5 3.5l258 96.5q15.952 6.453 26.476 21.486Q795.5 320.52 795.5 338.269v194.534q0 147.082-84.75 270.39Q626 926.5 493.5 966q1.5-.5-13.5 2Zm0-57.5q114-38 186-142.853Q738 662.793 738 533V338.387L480 241.5l-258 96.887V533q0 129.793 72 234.647Q366 872.5 480 910.5Zm0-334.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam.svg index 9a9edf61ce..d0ebb2fd41 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M143.5 891.5q-22.969 0-40.234-17.266Q86 856.969 86 834V318q0-22.969 17.266-40.234Q120.531 260.5 143.5 260.5h516.211q22.227 0 39.758 17.266Q717 295.031 717 318v215.5L874.5 376v400L717 618.5V834q0 22.969-17.531 40.234-17.531 17.266-39.758 17.266H143.5Zm0-57.5h516V318h-516v516Zm0 0V318v516Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M143.5 891.5q-22.969 0-40.234-17.266Q86 856.969 86 834V318q0-22.969 17.266-40.234Q120.531 260.5 143.5 260.5h516.211q22.227 0 39.758 17.266Q717 295.031 717 318v215.5L874.5 376v400L717 618.5V834q0 22.969-17.531 40.234-17.531 17.266-39.758 17.266H143.5Zm0-57.5h516V318h-516v516Zm0 0V318v516Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam_filled.svg index 0606c6a06f..9f3b877e0d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M143.5 891.5q-23.031 0-40.266-17.234Q86 857.031 86 834V318q0-23.031 17.234-40.266Q120.469 260.5 143.5 260.5h516.211q22.289 0 39.789 17.234Q717 294.969 717 318v215.5L849 401q8-7.5 16.75-3.75t8.75 13.063V741q0 10-8.75 13.75t-16.85-4.35L717 618.5V834q0 23.031-17.5 40.266Q682 891.5 659.711 891.5H143.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M143.5 891.5q-23.031 0-40.266-17.234Q86 857.031 86 834V318q0-23.031 17.234-40.266Q120.469 260.5 143.5 260.5h516.211q22.289 0 39.789 17.234Q717 294.969 717 318v215.5L849 401q8-7.5 16.75-3.75t8.75 13.063V741q0 10-8.75 13.75t-16.85-4.35L717 618.5V834q0 23.031-17.5 40.266Q682 891.5 659.711 891.5H143.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam_off.svg index 77c85ed987..992f052ed0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam_off.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam_off.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M849.5 750.5 717 618.5v114L659.5 675V318H302l-57.5-57.5h415q22.969 0 40.234 17.266Q717 295.031 717 318v215l132.5-132.5q6.5-6.5 15.75-3.167 9.25 3.334 9.25 12.667v331q0 9.625-9.25 13.062Q856 757.5 849.5 750.5Zm-26.5 250-758-758q-8-7.547-8-19.069 0-11.522 9-20.431 8.5-8.5 20-8.5t20.5 8.5l758 758q7.5 7.93 7.5 19.465t-8.5 20.035q-9 9-20.5 9t-20-9Zm-340-502Zm-319.5-238L221 318h-77.5v516h516v-77.5L716 813v21q0 22.969-17.266 40.234Q681.469 891.5 658.5 891.5h-515q-22.969 0-40.234-17.266Q86 856.969 86 834V318q0-22.969 17.266-40.234Q120.531 260.5 143.5 260.5h20Zm236 316.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M849.5 750.5 717 618.5v114L659.5 675V318H302l-57.5-57.5h415q22.969 0 40.234 17.266Q717 295.031 717 318v215l132.5-132.5q6.5-6.5 15.75-3.167 9.25 3.334 9.25 12.667v331q0 9.625-9.25 13.062Q856 757.5 849.5 750.5Zm-26.5 250-758-758q-8-7.547-8-19.069 0-11.522 9-20.431 8.5-8.5 20-8.5t20.5 8.5l758 758q7.5 7.93 7.5 19.465t-8.5 20.035q-9 9-20.5 9t-20-9Zm-340-502Zm-319.5-238L221 318h-77.5v516h516v-77.5L716 813v21q0 22.969-17.266 40.234Q681.469 891.5 658.5 891.5h-515q-22.969 0-40.234-17.266Q86 856.969 86 834V318q0-22.969 17.266-40.234Q120.531 260.5 143.5 260.5h20Zm236 316.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility.svg index 50095bc6e5..24c3544621 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M480.118 723q69.632 0 118.257-48.743Q647 625.515 647 555.882q0-69.632-48.743-118.257Q549.515 389 479.882 389q-69.632 0-118.257 48.743Q313 486.485 313 556.118q0 69.632 48.743 118.257Q410.485 723 480.118 723Zm-.294-56q-46.324 0-78.574-32.426Q369 602.147 369 555.823q0-46.323 32.426-78.573Q433.853 445 480.176 445q46.324 0 78.574 32.427Q591 509.853 591 556.177q0 46.323-32.427 78.573Q526.147 667 479.824 667Zm.298 185Q344 852 232.5 778.25T59.645 583.923Q56 578.5 54.5 570.963q-1.5-7.537-1.5-15.076 0-7.539 1.5-14.963 1.5-7.424 5.145-12.847Q121 407.5 232.378 333.75 343.756 260 479.878 260T727.5 333.75Q839 407.5 900.855 528.077q3.145 5.423 4.895 12.96 1.75 7.537 1.75 15.076 0 7.539-1.75 14.963-1.75 7.424-4.895 12.847Q839 704.5 727.622 778.25 616.244 852 480.122 852ZM480 556Zm-.169 238.5q120.669 0 221.419-65Q802 664.5 854.5 556 802 447.5 701.419 382.5q-100.582-65-221.25-65-120.669 0-221.419 65Q158 447.5 104.5 556 158 664.5 258.581 729.5q100.582 65 221.25 65Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M480.118 723q69.632 0 118.257-48.743Q647 625.515 647 555.882q0-69.632-48.743-118.257Q549.515 389 479.882 389q-69.632 0-118.257 48.743Q313 486.485 313 556.118q0 69.632 48.743 118.257Q410.485 723 480.118 723Zm-.294-56q-46.324 0-78.574-32.426Q369 602.147 369 555.823q0-46.323 32.426-78.573Q433.853 445 480.176 445q46.324 0 78.574 32.427Q591 509.853 591 556.177q0 46.323-32.427 78.573Q526.147 667 479.824 667Zm.298 185Q344 852 232.5 778.25T59.645 583.923Q56 578.5 54.5 570.963q-1.5-7.537-1.5-15.076 0-7.539 1.5-14.963 1.5-7.424 5.145-12.847Q121 407.5 232.378 333.75 343.756 260 479.878 260T727.5 333.75Q839 407.5 900.855 528.077q3.145 5.423 4.895 12.96 1.75 7.537 1.75 15.076 0 7.539-1.75 14.963-1.75 7.424-4.895 12.847Q839 704.5 727.622 778.25 616.244 852 480.122 852ZM480 556Zm-.169 238.5q120.669 0 221.419-65Q802 664.5 854.5 556 802 447.5 701.419 382.5q-100.582-65-221.25-65-120.669 0-221.419 65Q158 447.5 104.5 556 158 664.5 258.581 729.5q100.582 65 221.25 65Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_filled.svg index 2bc6fbd9a5..1f07b63790 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M480.118 723q69.632 0 118.257-48.743Q647 625.515 647 555.882q0-69.632-48.743-118.257Q549.515 389 479.882 389q-69.632 0-118.257 48.743Q313 486.485 313 556.118q0 69.632 48.743 118.257Q410.485 723 480.118 723Zm-.294-56q-46.324 0-78.574-32.426Q369 602.147 369 555.823q0-46.323 32.426-78.573Q433.853 445 480.176 445q46.324 0 78.574 32.427Q591 509.853 591 556.177q0 46.323-32.427 78.573Q526.147 667 479.824 667Zm.298 185Q344 852 232.5 778.25T59.645 583.923Q56 578.5 54.5 570.963q-1.5-7.537-1.5-15.076 0-7.539 1.5-14.963 1.5-7.424 5.145-12.847Q121 407.5 232.378 333.75 343.756 260 479.878 260T727.5 333.75Q839 407.5 900.855 528.077q3.145 5.423 4.895 12.96 1.75 7.537 1.75 15.076 0 7.539-1.75 14.963-1.75 7.424-4.895 12.847Q839 704.5 727.622 778.25 616.244 852 480.122 852Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M480.118 723q69.632 0 118.257-48.743Q647 625.515 647 555.882q0-69.632-48.743-118.257Q549.515 389 479.882 389q-69.632 0-118.257 48.743Q313 486.485 313 556.118q0 69.632 48.743 118.257Q410.485 723 480.118 723Zm-.294-56q-46.324 0-78.574-32.426Q369 602.147 369 555.823q0-46.323 32.426-78.573Q433.853 445 480.176 445q46.324 0 78.574 32.427Q591 509.853 591 556.177q0 46.323-32.427 78.573Q526.147 667 479.824 667Zm.298 185Q344 852 232.5 778.25T59.645 583.923Q56 578.5 54.5 570.963q-1.5-7.537-1.5-15.076 0-7.539 1.5-14.963 1.5-7.424 5.145-12.847Q121 407.5 232.378 333.75 343.756 260 479.878 260T727.5 333.75Q839 407.5 900.855 528.077q3.145 5.423 4.895 12.96 1.75 7.537 1.75 15.076 0 7.539-1.75 14.963-1.75 7.424-4.895 12.847Q839 704.5 727.622 778.25 616.244 852 480.122 852Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_off.svg index 459ea40242..7cacfafdbf 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_off.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_off.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M626.5 637 584 594.5q26.5-73-27.25-119.5T441.5 452L399 409.5q17-10.5 38.25-15.5t42.6-5q70.312 0 118.731 48.419T647 556.15q0 21.35-5.5 43.35t-15 37.5ZM753 764l-37.5-38q48.5-35.5 85.25-80.25t53.75-89.902Q805 446 705.5 381.75 606 317.5 490 317.5q-41.5 0-86.25 8T334.5 345L290 299.5q34-15.5 88.75-27.5t106.093-12q133.066 0 245.362 72.5Q842.5 405 902 528q3 5.5 4.25 12.75T907.5 556q0 8-1.25 15.75T902 585q-26 54-63.555 99.111Q800.889 729.223 753 764Zm39.427 200.389L648 822.5q-34.5 14-78.75 21.75T480 852q-135.813 0-248.906-72.5Q118 707 58 584q-3-6.5-4-13.25t-1-14.85q0-8.1 1.5-15.75t4-13.65q21.133-44.058 53.066-85.779Q143.5 399 187 361.5L82 255.384Q73.5 247 73.5 235.5t8.5-20q8.5-8.5 20.5-8.5t20.603 8.596L833 925q7.5 8 7.25 19t-7.75 19.5q-8 10-19.75 9.75t-20.323-8.861ZM226.5 401.5q-37 26.5-72 71t-50 83.631Q155.5 666 257.214 730.25 358.929 794.5 488 794.5q34 0 66.75-4.25T603 778l-65.234-65Q527 718 510.5 720.5t-30.628 2.5Q411 723 362 674.75t-49-118.686q0-15.064 2.5-30.403 2.5-15.338 7.5-27.161l-96.5-97Zm300 143.5ZM415 600.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M626.5 637 584 594.5q26.5-73-27.25-119.5T441.5 452L399 409.5q17-10.5 38.25-15.5t42.6-5q70.312 0 118.731 48.419T647 556.15q0 21.35-5.5 43.35t-15 37.5ZM753 764l-37.5-38q48.5-35.5 85.25-80.25t53.75-89.902Q805 446 705.5 381.75 606 317.5 490 317.5q-41.5 0-86.25 8T334.5 345L290 299.5q34-15.5 88.75-27.5t106.093-12q133.066 0 245.362 72.5Q842.5 405 902 528q3 5.5 4.25 12.75T907.5 556q0 8-1.25 15.75T902 585q-26 54-63.555 99.111Q800.889 729.223 753 764Zm39.427 200.389L648 822.5q-34.5 14-78.75 21.75T480 852q-135.813 0-248.906-72.5Q118 707 58 584q-3-6.5-4-13.25t-1-14.85q0-8.1 1.5-15.75t4-13.65q21.133-44.058 53.066-85.779Q143.5 399 187 361.5L82 255.384Q73.5 247 73.5 235.5t8.5-20q8.5-8.5 20.5-8.5t20.603 8.596L833 925q7.5 8 7.25 19t-7.75 19.5q-8 10-19.75 9.75t-20.323-8.861ZM226.5 401.5q-37 26.5-72 71t-50 83.631Q155.5 666 257.214 730.25 358.929 794.5 488 794.5q34 0 66.75-4.25T603 778l-65.234-65Q527 718 510.5 720.5t-30.628 2.5Q411 723 362 674.75t-49-118.686q0-15.064 2.5-30.403 2.5-15.338 7.5-27.161l-96.5-97Zm300 143.5ZM415 600.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_off_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_off_filled.svg index 7269dffdd3..9a52f493a7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_off_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_off_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M753 763.5 626.5 637q9.5-15.5 15-37.5T647 556q0-70-48.5-118.5T480 389q-21.5 0-42.75 5T399 409.5l-109-110q34-15.5 88.75-27.5T485 260q133 0 245.25 72.5T902 528q3 5.5 4.25 12.75T907.5 556q0 8-1.25 15.75T902 585q-26 54-63.5 98.75T753 763.5Zm39.5 201L648 822.5q-34.5 14-78.75 21.75T480 852q-136 0-249-72.5T58 584q-3-6.5-4-13.25T53 556q0-8 1.5-15.75t4-13.75q21-44 53-85.75T187 361.5l-105-106q-8.5-8.5-8.5-20t8.5-20q8.5-8.5 20.5-8.5t20.5 8.5L833 925q7.5 8 7.25 19t-7.75 19.5q-8 10-19.75 9.75t-20.25-8.75ZM480 723q14 0 30.75-2.75t27.75-7.75L323.5 498q-5 12.5-7.75 27.75T313 556q0 70.5 49 118.75T480 723Zm104-128.5L441.5 452q61.5-23.5 115.25 23T584 594.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M753 763.5 626.5 637q9.5-15.5 15-37.5T647 556q0-70-48.5-118.5T480 389q-21.5 0-42.75 5T399 409.5l-109-110q34-15.5 88.75-27.5T485 260q133 0 245.25 72.5T902 528q3 5.5 4.25 12.75T907.5 556q0 8-1.25 15.75T902 585q-26 54-63.5 98.75T753 763.5Zm39.5 201L648 822.5q-34.5 14-78.75 21.75T480 852q-136 0-249-72.5T58 584q-3-6.5-4-13.25T53 556q0-8 1.5-15.75t4-13.75q21-44 53-85.75T187 361.5l-105-106q-8.5-8.5-8.5-20t8.5-20q8.5-8.5 20.5-8.5t20.5 8.5L833 925q7.5 8 7.25 19t-7.75 19.5q-8 10-19.75 9.75t-20.25-8.75ZM480 723q14 0 30.75-2.75t27.75-7.75L323.5 498q-5 12.5-7.75 27.75T313 556q0 70.5 49 118.75T480 723Zm104-128.5L441.5 452q61.5-23.5 115.25 23T584 594.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_down.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_down.svg index 44b8f89e91..51f7d1a35d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_down.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_down.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M232.5 693q-12.5 0-20.5-8t-8-20.5v-177q0-12.5 8-20.5t20.5-8h129L509 311.5q13.5-13.5 31-6.5t17.5 26v489.5q0 19.5-17.5 26.5t-31-6.5L361.5 693h-129ZM615 742V409.5q55 17 88 63.25T736 576q0 58-33 103.25T615 742ZM500 408.5l-112.5 108h-126v119h126L500 744V408.5ZM379 576Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M232.5 693q-12.5 0-20.5-8t-8-20.5v-177q0-12.5 8-20.5t20.5-8h129L509 311.5q13.5-13.5 31-6.5t17.5 26v489.5q0 19.5-17.5 26.5t-31-6.5L361.5 693h-129ZM615 742V409.5q55 17 88 63.25T736 576q0 58-33 103.25T615 742ZM500 408.5l-112.5 108h-126v119h126L500 744V408.5ZM379 576Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_off.svg new file mode 100644 index 0000000000..497864dd56 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_off.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="m809.5-61.5-133-133q-27 19-58.25 33.25T553.5-139.5V-199q21.5-6.5 42.5-14.5t39.5-22L476-396v229L280-363H122.5v-234H274L54.5-816.5 96-858l755 754-41.5 42.5ZM770-291l-41.5-41.5q20-33 29.75-70.67Q768-440.85 768-481q0-100.82-58.75-180.41T553.5-763v-59.5q120 28 196 123.25t76 218.25q0 50.5-14 98.75T770-291ZM642.5-418.5l-89-89v-132q46.5 21.5 73.75 64.75T654.5-480q0 16-3 31.5t-9 30ZM476-585 372-689l104-104v208Zm-57.5 278v-145.5l-87-87H180v119h124.5l114 113.5ZM375-496Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_up.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_up.svg index 8d9f2ebecf..292a1fe27e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_up.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_up.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M600.5 904.5q-15.5 5.5-28.25-4T559.5 874q0-7.5 4.25-13.5t11.75-8q89-31.5 143.75-107T774 575q0-94.5-54.5-170.5T575.5 298q-7-2-11.5-8.5t-4.5-14.5q0-16 13.25-25.25t27.75-4.25Q704.5 283 768 373t63.5 202q0 112.5-63.5 202.5t-167.5 127ZM157 693q-12.5 0-20.5-8t-8-20.5v-177q0-12.5 8-20.5t20.5-8h129l147.5-147.5q13.5-13.5 31-6.25T482 331v489.5q0 19-17.5 26.25t-31-6.25L286 693H157Zm382.5 49V409.5q55 17 88 63.25t33 103.25q0 58-33 103.25t-88 62.75Zm-115-333.5L312 516.5H186v119h126L424.5 744V408.5Zm-93 167.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M600.5 904.5q-15.5 5.5-28.25-4T559.5 874q0-7.5 4.25-13.5t11.75-8q89-31.5 143.75-107T774 575q0-94.5-54.5-170.5T575.5 298q-7-2-11.5-8.5t-4.5-14.5q0-16 13.25-25.25t27.75-4.25Q704.5 283 768 373t63.5 202q0 112.5-63.5 202.5t-167.5 127ZM157 693q-12.5 0-20.5-8t-8-20.5v-177q0-12.5 8-20.5t20.5-8h129l147.5-147.5q13.5-13.5 31-6.25T482 331v489.5q0 19-17.5 26.25t-31-6.25L286 693H157Zm382.5 49V409.5q55 17 88 63.25t33 103.25q0 58-33 103.25t-88 62.75Zm-115-333.5L312 516.5H186v119h126L424.5 744V408.5Zm-93 167.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_vpn_key_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_vpn_key_filled.svg index adb8814e12..f40ab9c6e1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_vpn_key_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_vpn_key_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M281.49 812.5q-98.573 0-167.532-68.971Q45 674.559 45 576.029 45 477.5 113.94 408.5q68.939-69 167.424-69Q367 339.5 426.5 388T508 503.5h349.5q23.969 0 40.734 16.766Q915 537.031 915 561v30q0 23.5-16.25 40.5t-41.25 17h-48V755q0 23.969-16.766 40.734Q775.969 812.5 752 812.5h-10.5q-23.969 0-40.734-16.766Q684 778.969 684 755V648.5H508q-22 67-81.401 115.5-59.402 48.5-145.109 48.5Zm.116-170.5q27.894 0 46.894-19.106t19-47Q347.5 548 328.394 529t-47-19q-27.894 0-46.894 19.106t-19 47Q215.5 604 234.606 623t47 19Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M281.49 812.5q-98.573 0-167.532-68.971Q45 674.559 45 576.029 45 477.5 113.94 408.5q68.939-69 167.424-69Q367 339.5 426.5 388T508 503.5h349.5q23.969 0 40.734 16.766Q915 537.031 915 561v30q0 23.5-16.25 40.5t-41.25 17h-48V755q0 23.969-16.766 40.734Q775.969 812.5 752 812.5h-10.5q-23.969 0-40.734-16.766Q684 778.969 684 755V648.5H508q-22 67-81.401 115.5-59.402 48.5-145.109 48.5Zm.116-170.5q27.894 0 46.894-19.106t19-47Q347.5 548 328.394 529t-47-19q-27.894 0-46.894 19.106t-19 47Q215.5 604 234.606 623t47 19Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_vpn_key_off_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_vpn_key_off_filled.svg index 6ae367024e..0fe5d9a21e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_vpn_key_off_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_vpn_key_off_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M790.5 968 498 676h-7.5Q465 736 413 774.25T281.5 812.5q-98.5 0-167.5-69T45 576q0-66 35.25-124.25T183 361l-96-96q-8.5-8.5-8.5-20.5T87 224q8.5-8.5 20.5-8.5t21 8.5L832 927.5q8.5 8.5 8.5 20.25T832 968q-9 8.5-21 8.5t-20.5-8.5ZM787 801 489.5 503.5h368q24 0 40.75 16.75T915 561v30q0 24-16.75 40.75T857.5 648.5h-48V755q0 15-6.5 26.75T787 801ZM281.5 642q28 0 47-19t19-47q0-12.5-4.75-27.25T326 522.5l9 9Q324 520 309 515t-27.5-5q-28 0-47 19t-19 47q0 28 19 47t47 19Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M790.5 968 498 676h-7.5Q465 736 413 774.25T281.5 812.5q-98.5 0-167.5-69T45 576q0-66 35.25-124.25T183 361l-96-96q-8.5-8.5-8.5-20.5T87 224q8.5-8.5 20.5-8.5t21 8.5L832 927.5q8.5 8.5 8.5 20.25T832 968q-9 8.5-21 8.5t-20.5-8.5ZM787 801 489.5 503.5h368q24 0 40.75 16.75T915 561v30q0 24-16.75 40.75T857.5 648.5h-48V755q0 15-6.5 26.75T787 801ZM281.5 642q28 0 47-19t19-47q0-12.5-4.75-27.25T326 522.5l9 9Q324 520 309 515t-27.5-5q-28 0-47 19t-19 47q0 28 19 47t47 19Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_warning.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_warning.svg index 7148c5740b..065e2f883c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_warning.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_warning.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M96.63-124.5q-8.63 0-15.282-4.125Q74.696-132.75 71.25-139q-3.917-6.1-4.333-13.55Q66.5-160 71.5-168l383.936-661.885Q460-837.5 466.25-841t13.75-3.5q7.5 0 13.75 3.5T505-830l384 662q4.5 8 4.083 15.45-.416 7.45-4.333 13.55-3.446 6.25-10.098 10.375Q872-124.5 863.37-124.5H96.63ZM146.5-182h667L480-758 146.5-182Zm337.728-57.5q12.272 0 20.522-8.478 8.25-8.478 8.25-20.75t-8.478-20.522q-8.478-8.25-20.75-8.25t-20.522 8.478q-8.25 8.478-8.25 20.75t8.478 20.522q8.478 8.25 20.75 8.25Zm.197-108.5q12.075 0 20.325-8.375T513-377v-165q0-11.675-8.463-20.088-8.463-8.412-20.212-8.412-12.325 0-20.575 8.412-8.25 8.413-8.25 20.088v165q0 12.25 8.425 20.625 8.426 8.375 20.5 8.375ZM480-470Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M96.63-124.5q-8.63 0-15.282-4.125Q74.696-132.75 71.25-139q-3.917-6.1-4.333-13.55Q66.5-160 71.5-168l383.936-661.885Q460-837.5 466.25-841t13.75-3.5q7.5 0 13.75 3.5T505-830l384 662q4.5 8 4.083 15.45-.416 7.45-4.333 13.55-3.446 6.25-10.098 10.375Q872-124.5 863.37-124.5H96.63ZM146.5-182h667L480-758 146.5-182Zm337.728-57.5q12.272 0 20.522-8.478 8.25-8.478 8.25-20.75t-8.478-20.522q-8.478-8.25-20.75-8.25t-20.522 8.478q-8.25 8.478-8.25 20.75t8.478 20.522q8.478 8.25 20.75 8.25Zm.197-108.5q12.075 0 20.325-8.375T513-377v-165q0-11.675-8.463-20.088-8.463-8.412-20.212-8.412-12.325 0-20.575 8.412-8.25 8.413-8.25 20.088v165q0 12.25 8.425 20.625 8.426 8.375 20.5 8.375ZM480-470Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_warning_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_warning_filled.svg index 673b7453f6..7806a17d7f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_warning_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_warning_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M96.86 931.5q-8.86 0-15.62-4.1-6.762-4.1-9.99-10.4-3.917-6.1-4.333-13.55Q66.5 896 71.5 888l383.936-661.885Q460 218.5 466.345 215q6.345-3.5 13.655-3.5t13.655 3.5Q500 218.5 505 226l384 662q4.5 8 4.083 15.45-.416 7.45-4.333 13.55-3.446 6.25-10.132 10.375-6.687 4.125-15.248 4.125H96.86Zm387.263-115q12.377 0 20.627-8.373 8.25-8.373 8.25-20.75t-8.373-20.627q-8.373-8.25-20.75-8.25t-20.627 8.373q-8.25 8.373-8.25 20.75t8.373 20.627q8.373 8.25 20.75 8.25Zm.302-108.5q12.075 0 20.325-8.375T513 679V514q0-11.675-8.463-20.088-8.463-8.412-20.212-8.412-12.325 0-20.575 8.412-8.25 8.413-8.25 20.088v165q0 12.25 8.425 20.625 8.426 8.375 20.5 8.375Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M96.86 931.5q-8.86 0-15.62-4.1-6.762-4.1-9.99-10.4-3.917-6.1-4.333-13.55Q66.5 896 71.5 888l383.936-661.885Q460 218.5 466.345 215q6.345-3.5 13.655-3.5t13.655 3.5Q500 218.5 505 226l384 662q4.5 8 4.083 15.45-.416 7.45-4.333 13.55-3.446 6.25-10.132 10.375-6.687 4.125-15.248 4.125H96.86Zm387.263-115q12.377 0 20.627-8.373 8.25-8.373 8.25-20.75t-8.373-20.627q-8.373-8.25-20.75-8.25t-20.627 8.373q-8.25 8.373-8.25 20.75t8.373 20.627q8.373 8.25 20.75 8.25Zm.302-108.5q12.075 0 20.325-8.375T513 679V514q0-11.675-8.463-20.088-8.463-8.412-20.212-8.412-12.325 0-20.575 8.412-8.25 8.413-8.25 20.088v165q0 12.25 8.425 20.625 8.426 8.375 20.5 8.375Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi.svg index 2fb4750af5..313748293b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M479.965-133.5q-34.965 0-61.215-26.295-26.25-26.296-26.25-61.275 0-34.43 26.285-60.93t61.25-26.5q34.965 0 61.215 26.57 26.25 26.571 26.25 61.25 0 34.68-26.285 60.93t-61.25 26.25ZM234-361l-60.5-59.5Q243-490 318-524.5T480-559q87 0 162.026 34.538Q717.053-489.923 786.5-421l-60 60q-61-60.5-122.532-86.25-61.533-25.75-124-25.75Q417.5-473 356-447.25T234-361ZM70.5-524.5l-60.5-60q91-93 211.947-150t258-57Q617-791.5 738-734.5t212 150l-60 60Q802.5-607 700.1-656.25t-220-49.25q-117.6 0-220.1 49.25T70.5-524.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M479.965-133.5q-34.965 0-61.215-26.295-26.25-26.296-26.25-61.275 0-34.43 26.285-60.93t61.25-26.5q34.965 0 61.215 26.57 26.25 26.571 26.25 61.25 0 34.68-26.285 60.93t-61.25 26.25ZM234-361l-60.5-59.5Q243-490 318-524.5T480-559q87 0 162.026 34.538Q717.053-489.923 786.5-421l-60 60q-61-60.5-122.532-86.25-61.533-25.75-124-25.75Q417.5-473 356-447.25T234-361ZM70.5-524.5l-60.5-60q91-93 211.947-150t258-57Q617-791.5 738-734.5t212 150l-60 60Q802.5-607 700.1-656.25t-220-49.25q-117.6 0-220.1 49.25T70.5-524.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi_off.svg index 814077e485..31354b7ed2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi_off.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi_off.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m803.5-74-392-391.5Q357-453 312.25-424T234-361l-60.5-59.5q37-37 75.75-64.75T343.5-535l-108-108q-47 23-88.5 54t-76.5 64.5L10-584q36-37 76.5-68.75T170-707.5L76-802l41-41 727.5 727.5-41 41.5ZM480-133.5q-35 0-61.25-26T392.5-221q0-34.5 26.25-61T480-308.5q35 0 61.25 26.5t26.25 61q0 35.5-26.25 61.5t-61.25 26Zm245.5-226Q693-391 666-410.5t-68.5-38l-110-110q93 2 164.25 38.75T786.5-420.5l-61 61Zm164.5-165q-87.5-83-190.25-132T480-705.5q-37 0-69.5 4.5t-54 12l-70-70q42.5-15 91.75-23.75T480-791.5q137 0 257.5 56.75T950-584l-60 59.5Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="m803.5-74-392-391.5Q357-453 312.25-424T234-361l-60.5-59.5q37-37 75.75-64.75T343.5-535l-108-108q-47 23-88.5 54t-76.5 64.5L10-584q36-37 76.5-68.75T170-707.5L76-802l41-41 727.5 727.5-41 41.5ZM480-133.5q-35 0-61.25-26T392.5-221q0-34.5 26.25-61T480-308.5q35 0 61.25 26.5t26.25 61q0 35.5-26.25 61.5t-61.25 26Zm245.5-226Q693-391 666-410.5t-68.5-38l-110-110q93 2 164.25 38.75T786.5-420.5l-61 61Zm164.5-165q-87.5-83-190.25-132T480-705.5q-37 0-69.5 4.5t-54 12l-70-70q42.5-15 91.75-23.75T480-791.5q137 0 257.5 56.75T950-584l-60 59.5Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi_tethering.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi_tethering.svg index f6007d3f02..b6b6eedd9c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi_tethering.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi_tethering.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M205 905q-56.77-55.096-90.135-129.165Q81.5 701.766 81.5 616.161q0-82.618 31.383-155.513 31.383-72.894 85.183-126.817 53.799-53.922 126.528-85.376Q397.324 217 480.016 217q82.73 0 155.486 31.49 72.756 31.489 126.576 85.472 53.819 53.982 85.371 126.764Q879 533.507 879 616.258 879 702 845.5 776 812 850 755 905l-41-41.5q50-47 78.75-110.187 28.75-63.186 28.75-137.399Q821.5 473.5 722 374t-242-99.5q-142.5 0-241.75 99.5T139 616.16q0 73.975 28.5 137.157Q196 816.5 246 864l-41 41Zm113-113q-34.773-32.867-55.636-78.183Q241.5 668.5 241.5 615.869q0-99.302 69.588-169.086Q380.676 377 480.088 377t169.162 69.721Q719 516.442 719 616.043q0 52.457-21 97.707Q677 759 642 792l-41-41.5q27.963-24.635 44.231-58.818Q661.5 657.5 661.5 615.903q0-76.126-52.688-128.765-52.688-52.638-128.75-52.638T351.5 487.14Q299 539.781 299 615.57q0 41.43 16 75.93 16 34.5 44.5 59L318 792Zm162.062-97.5q-32.562 0-55.562-23.084t-23-55.5q0-32.416 23.084-55.666t55.5-23.25q32.416 0 55.666 23.188t23.25 55.75q0 32.562-23.188 55.562t-55.75 23Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 96 960 960" width="22"><path d="M205 905q-56.77-55.096-90.135-129.165Q81.5 701.766 81.5 616.161q0-82.618 31.383-155.513 31.383-72.894 85.183-126.817 53.799-53.922 126.528-85.376Q397.324 217 480.016 217q82.73 0 155.486 31.49 72.756 31.489 126.576 85.472 53.819 53.982 85.371 126.764Q879 533.507 879 616.258 879 702 845.5 776 812 850 755 905l-41-41.5q50-47 78.75-110.187 28.75-63.186 28.75-137.399Q821.5 473.5 722 374t-242-99.5q-142.5 0-241.75 99.5T139 616.16q0 73.975 28.5 137.157Q196 816.5 246 864l-41 41Zm113-113q-34.773-32.867-55.636-78.183Q241.5 668.5 241.5 615.869q0-99.302 69.588-169.086Q380.676 377 480.088 377t169.162 69.721Q719 516.442 719 616.043q0 52.457-21 97.707Q677 759 642 792l-41-41.5q27.963-24.635 44.231-58.818Q661.5 657.5 661.5 615.903q0-76.126-52.688-128.765-52.688-52.638-128.75-52.638T351.5 487.14Q299 539.781 299 615.57q0 41.43 16 75.93 16 34.5 44.5 59L318 792Zm162.062-97.5q-32.562 0-55.562-23.084t-23-55.5q0-32.416 23.084-55.666t55.5-23.25q32.416 0 55.666 23.188t23.25 55.75q0 32.562-23.188 55.562t-55.75 23Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_work.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_work.svg new file mode 100644 index 0000000000..4ea483b006 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_work.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22" fill="#5f6368"><path d="M142.5-125q-22.97 0-40.23-17.27Q85-159.53 85-182.5v-480q0-22.97 17.27-40.23Q119.53-720 142.5-720H323v-97.52q0-22.98 17.27-40.23Q357.53-875 380.5-875h199q22.97 0 40.23 17.27Q637-840.47 637-817.5v97.5h180.5q22.97 0 40.23 17.27Q875-685.47 875-662.5v480q0 22.97-17.27 40.23Q840.47-125 817.5-125h-675Zm0-57.5h675v-480h-675v480Zm238-537.5h199v-97.5h-199v97.5Zm-238 537.5v-480 480Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_work_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_work_filled.svg new file mode 100644 index 0000000000..fb5c122eec --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_work_filled.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M165-130q-30.94 0-52.97-22.03Q90-174.06 90-205v-440q0-30.94 22.03-52.97Q134.06-720 165-720h161v-75.04Q326-826 348.03-848T401-870h158q30.94 0 52.97 22.03Q634-825.94 634-795v75h161q30.94 0 52.97 22.03Q870-675.94 870-645v440q0 30.94-22.03 52.97Q825.94-130 795-130H165Zm236-590h158v-75H401v75Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_work_filled_padded.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_work_filled_padded.svg new file mode 100644 index 0000000000..3d8c05e2c8 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_work_filled_padded.svg @@ -0,0 +1,8 @@ +<svg width="1240" height="1240" xmlns="http://www.w3.org/2000/svg"> + + <g> + <g> + <path d="m282.5,995q-22.97,0 -40.23,-17.27q-17.27,-17.26 -17.27,-40.23l0,-480q0,-22.97 17.27,-40.23q17.26,-17.27 40.23,-17.27l180.5,0l0,-97.52q0,-22.98 17.27,-40.23q17.26,-17.25 40.23,-17.25l199,0q22.97,0 40.23,17.27q17.27,17.26 17.27,40.23l0,97.5l180.5,0q22.97,0 40.23,17.27q17.27,17.26 17.27,40.23l0,480q0,22.97 -17.27,40.23q-17.26,17.27 -40.23,17.27l-675,0zm238,-595l199,0l0,-97.5l-199,0l0,97.5z"/> + </g> + </g> +</svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_cats@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_cats@4x.png new file mode 100644 index 0000000000..9bff3eb3d0 Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_cats@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_flowers@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_flowers@4x.png new file mode 100644 index 0000000000..e0ee4b057d Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_flowers@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_hearts@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_hearts@4x.png new file mode 100644 index 0000000000..35da7c7aed Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_hearts@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_kids@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_kids@4x.png new file mode 100644 index 0000000000..f5f15d3643 Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_kids@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_school@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_school@4x.png new file mode 100644 index 0000000000..f6e1cce383 Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_school@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_travel@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_travel@4x.png new file mode 100644 index 0000000000..64ec137331 Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_travel@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml new file mode 100644 index 0000000000..a269149e99 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -0,0 +1,2362 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="group_info_section_title_num_members">%1$s ANGGOTA</string> + <string name="address_section_title">Alamat</string> + <string name="moderated_items_description">%1$d pesan dimoderasi oleh %2$s</string> + <string name="send_disappearing_message_1_minute">1 menit</string> + <string name="send_disappearing_message_30_seconds">30 detik</string> + <string name="send_disappearing_message_5_minutes">5 menit</string> + <string name="clear_chat_warning">Semua pesan akan dihapus - ini tidak bisa dikembalikan! Pesan akan HANYA dihapus untukmu.</string> + <string name="accept_connection_request__question">Terima permintaan koneksi?</string> + <string name="learn_more_about_address">Tentang alamat SimpleX</string> + <string name="add_contact_tab">Tambah kontak</string> + <string name="about_simplex">Tentang SimpleX</string> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d pesan gagal terdekripsi.</string> + <string name="alert_text_fragment_please_report_to_developers">Mohon laporkan ke pengembang.</string> + <string name="chat_item_ttl_month">1 bulan</string> + <string name="chat_item_ttl_week">1 minggu</string> + <string name="accept_feature">Terima</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Anda sudah bergabung dalam grup <b>%1$s</b>.]]></string> + <string name="call_service_notification_audio_call">Panggilan suara</string> + <string name="abort_switch_receiving_address_confirm">Batal</string> + <string name="allow_voice_messages_question">Izinkan pesan suara?</string> + <string name="accept_contact_button">Terima</string> + <string name="app_version_title">Versi aplikasi</string> + <string name="app_version_name">Versi aplikasi: v%s</string> + <string name="icon_descr_audio_call">panggilan suara</string> + <string name="chat_item_ttl_day">1 hari</string> + <string name="button_add_welcome_message">Buat pesan sambutan</string> + <string name="abort_switch_receiving_address">Batalkan penggantian alamat</string> + <string name="users_add">Tambah profil</string> + <string name="color_primary">Corak</string> + <string name="color_secondary_variant">Tambahan sekunder</string> + <string name="theme_destination_app_theme">Tema aplikasi</string> + <string name="chat_preferences_always">selalu</string> + <string name="allow_to_send_voice">Izinkan mengirim pesan suara.</string> + <string name="feature_roles_all_members">semua anggota</string> + <string name="v4_6_audio_video_calls">Panggilan suara dan video</string> + <string name="v5_2_more_things">Beberapa hal lainnya</string> + <string name="connect_plan_already_connecting">Sudah terhubungkan!</string> + <string name="connect_plan_already_joining_the_group">Sudah bergabung dengan grup!</string> + <string name="contact_wants_to_connect_via_call">%1$s ingin menghubungimu lewat</string> + <string name="abort_switch_receiving_address_question">Batalkan penggantian alamat?</string> + <string name="above_then_preposition_continuation">diatas, lalu:</string> + <string name="smp_servers_add_to_another_device">Tambahkan ke perangkat lain</string> + <string name="turn_off_battery_optimization_button">Boleh</string> + <string name="network_smp_proxy_mode_always">Selalu</string> + <string name="settings_section_title_app">APLIKASI</string> + <string name="appearance_settings">Tampilan</string> + <string name="about_simplex_chat">Tentang SimpleX Chat</string> + <string name="accept">Terima</string> + <string name="accept_call_on_lock_screen">Terima</string> + <string name="cancel_verb">Batalkan</string> + <string name="use_camera_button">Kamera</string> + <string name="bold_text">tebal</string> + <string name="callstatus_calling">memanggil…</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="icon_descr_call_ended">Panggilan diakhiri</string> + <string name="change_verb">Ubah</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Peringatan</b>: arsip akan dihapus.]]></string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Buat grup</b>: untuk membuat grup baru.]]></string> + <string name="block_member_button">Blokir anggota</string> + <string name="block_member_confirmation">Blokir</string> + <string name="permissions_camera">Kamera</string> + <string name="theme_black">Hitam</string> + <string name="block_member_question">Blokir anggota?</string> + <string name="switch_receiving_address">Ubah alamat penerima</string> + <string name="app_check_for_updates_beta">Beta</string> + <string name="block_for_all">Blokir untuk semua</string> + <string name="block_for_all_question">Blokir anggota untuk semua?</string> + <string name="call_already_ended">Panggilan telah ditutup!</string> + <string name="network_type_cellular">Seluler</string> + <string name="connect_via_invitation_link">Hubungkan melalui tautan satu kali?</string> + <string name="connect_via_group_link">Gabung Grup?</string> + <string name="connect_use_current_profile">Gunakan profil saat ini</string> + <string name="connect_use_new_incognito_profile">Gunakan profil penyamaran baru</string> + <string name="profile_will_be_sent_to_contact_sending_link">Profil Anda akan dikirim ke kontak yang menerima tautan ini.</string> + <string name="you_will_join_group">Anda akan terhubung ke semua anggota grup.</string> + <string name="connect_via_link_verb">Hubungkan</string> + <string name="connect_via_link_incognito">Hubungkan penyamaran</string> + <string name="opening_database">Membuka basis data…</string> + <string name="thousand_abbreviation">k</string> + <string name="connect_via_contact_link">Hubungkan melalui alamat kontak?</string> + <string name="clear_verb">Bersihkan</string> + <string name="clear_contacts_selection_button">Bersihkan</string> + <string name="clear_chat_menu_action">Bersihkan</string> + <string name="migrate_from_device_check_connection_and_try_again">Cek koneksi internetmu dan coba lagi</string> + <string name="connect_button">Hubungkan</string> + <string name="server_connected">terhubung</string> + <string name="smp_server_test_connect">Hubungkan</string> + <string name="notification_contact_connected">Terhubung</string> + <string name="icon_descr_server_status_connected">Terhubung</string> + <string name="rcv_group_event_member_connected">terhubung</string> + <string name="connect_via_member_address_alert_title">Hubungkan langsung?</string> + <string name="callstate_connected">tersambung</string> + <string name="group_member_status_complete">selesai</string> + <string name="group_member_status_connected">terhubung</string> + <string name="connected_desktop">Komputer yang terhubung</string> + <string name="servers_info_sessions_connected">Terhubung</string> + <string name="completed">Selesai</string> + <string name="connected_mobile">Ponsel yang terhubung</string> + <string name="multicast_connect_automatically">Hubungkan secara otomatis</string> + <string name="callstate_connecting">menyambungkan…</string> + <string name="servers_info_sessions_connecting">Menghubungkan</string> + <string name="error_parsing_uri_title">Tautan tidak valid</string> + <string name="error_parsing_uri_desc">Periksa apakah tautan SimpleX sudah benar.</string> + <string name="connected_to_server_to_receive_messages_from_contact">Anda terhubung ke server yang digunakan untuk menerima pesan dari kontak ini.</string> + <string name="trying_to_connect_to_server_to_receive_messages_with_error">Mencoba menyambung ke server yang digunakan untuk menerima pesan dari kontak ini (error: %1$s).</string> + <string name="database_migration_in_progress">Migrasi basis data sedang berlangsung, +\nmemerlukan waktu beberapa menit.</string> + <string name="server_connecting">menghubungkan</string> + <string name="non_content_uri_alert_title">Lokasi file tidak valid</string> + <string name="app_was_crashed">Tampilan macet</string> + <string name="non_content_uri_alert_text">Anda membagikan lokasi file yang tidak valid. Laporkan masalah ini ke pengembang aplikasi.</string> + <string name="server_error">error</string> + <string name="one_time_link_short">Tautan sekali</string> + <string name="integrity_msg_skipped">%1$d pesan yang terlewati</string> + <string name="alert_text_decryption_error_too_many_skipped">%1$d pesan yang dilewati</string> + <string name="remote_host_error_bad_version"><![CDATA[Versi perangkat <b>%s</b> tidak didukung. Harap pastikan kamu menggunakan versi yang sama pada kedua perangkat.]]></string> + <string name="remote_host_error_busy"><![CDATA[Perangkat <b>%s</b> sedang sibuk]]></string> + <string name="remote_host_error_disconnected"><![CDATA[Perangkat <b>%s</b> tidak terhubung]]></string> + <string name="remote_host_error_inactive"><![CDATA[Perangkat <b>%s</b> tidak aktif]]></string> + <string name="remote_host_error_missing"><![CDATA[Perangkat <b>%s</b> tidak di temukan]]></string> + <string name="accept_contact_incognito_button">Terima penyamaran</string> + <string name="callstatus_accepted">panggilan diterima</string> + <string name="v5_3_new_interface_languages">6 bahasa antarmuka baru</string> + <string name="remote_host_was_disconnected_toast"><![CDATA[Perangkat <b>%s</b> tidak terhubung]]></string> + <string name="la_no_app_password">Tidak ada kode sandi aplikasi</string> + <string name="message_forwarded_desc">Belum ada koneksi langsung, pesan diteruskan oleh admin.</string> + <string name="no_filtered_chats">Tidak ada obrolan yang difilter</string> + <string name="selected_chat_items_nothing_selected">Tidak ada yang dipilih</string> + <string name="images_limit_desc">Hanya 10 gambar dapat dikirim pada saat bersamaan</string> + <string name="notifications">Notifikasi</string> + <string name="only_delete_conversation">Hanya menghapus percakapan</string> + <string name="v5_7_network_descr">Koneksi jaringan yang lebih handal.</string> + <string name="v6_0_new_chat_experience">Pengalaman obrolan yang baru 🎉</string> + <string name="v6_0_new_media_options">Pilihan media baru</string> + <string name="allow_calls_question">Izinkan panggilan?</string> + <string name="new_message">Pesan baru</string> + <string name="only_you_can_send_disappearing">Hanya kamu yang dapat mengirim pesan menghilang.</string> + <string name="first_platform_without_user_ids">Tidak ada pengidentifikasi pengguna.</string> + <string name="only_group_owners_can_change_prefs">Hanya pemilik grup yang dapat mengubah preferensi grup.</string> + <string name="rcv_group_and_other_events">dan %d peristiwa lainnya</string> + <string name="new_member_role">Peran baru anggota</string> + <string name="no_contacts_selected">Tidak ada kontak di pilih</string> + <string name="no_contacts_to_add">Tidak ada kontak untuk ditambahkan</string> + <string name="item_info_no_text">Tidak ada teks</string> + <string name="block_member_desc">Semua pesan baru dari %s akan disembunyikan!</string> + <string name="message_queue_info_none">tidak ada</string> + <string name="network_status">Status jaringan</string> + <string name="users_delete_all_chats_deleted">Seluruh obrolan dan pesan akan dihapus - ini tidak bisa dibatalkan!</string> + <string name="no_connected_mobile">Tidak ada perangkat terkoneksi</string> + <string name="system_restricted_background_in_call_title">Tidak ada panggilan latar</string> + <string name="settings_notification_preview_title">Pratinjau notifikasi</string> + <string name="settings_notifications_mode_title">Layanan notifikasi</string> + <string name="notifications_mode_service">Selalu aktif</string> + <string name="notification_new_contact_request">Permintaan kontak baru</string> + <string name="notification_preview_new_message">Pesan baru</string> + <string name="message_delivery_error_desc">Kemungkinan besar kontak ini telah menghapus koneksi dengan kamu.</string> + <string name="snd_error_expired">Masalah jaringan - pesan kadaluwarsa setelah beberapa kali mencoba mengirim.</string> + <string name="no_selected_chat">Tidak ada obrolan dipilih</string> + <string name="only_owners_can_enable_files_and_media">Hanya pemilik grup yang dapat mengaktifkan file dan media.</string> + <string name="only_stored_on_members_devices">(hanya disimpan oleh anggota grup)</string> + <string name="icon_descr_more_button">Lagi</string> + <string name="one_time_link">Tautan undangan satu kali</string> + <string name="new_chat">Obrolan baru</string> + <string name="network_and_servers">Jaringan & server</string> + <string name="network_settings_title">Pengaturan tingkat lanjut</string> + <string name="network_use_onion_hosts_required_desc">Host Onion akan diperlukan untuk koneksi. +\nHarap diperhatikan: Anda tidak akan dapat terhubung ke server tanpa alamat .onion.</string> + <string name="network_use_onion_hosts_no_desc">Host Onion tidak akan digunakan.</string> + <string name="network_smp_proxy_mode_always_description">Selalu gunakan perutean pribadi.</string> + <string name="shutdown_alert_desc">Pemberitahuan akan berhenti bekerja sampai kamu meluncurkan ulang aplikasi</string> + <string name="all_your_contacts_will_remain_connected_update_sent">Semua kontak Anda akan tetap terhubung. Pembaruan profil akan dikirim ke kontak Anda.</string> + <string name="status_no_e2e_encryption">Tidak ada enkripsi ujung-ujung</string> + <string name="new_passcode">Kode sandi baru</string> + <string name="la_mode_off">Mati</string> + <string name="all_app_data_will_be_cleared">Seluruh data aplikasi dihapus.</string> + <string name="new_database_archive">Arsip database baru</string> + <string name="old_database_archive">Arsip database lama</string> + <string name="chat_item_ttl_none">tidak pernah</string> + <string name="no_received_app_files">Tidak ada file yang diterima atau dikirim</string> + <string name="conn_event_ratchet_sync_started">Menyetujui enkripsi…</string> + <string name="muted_when_inactive">Bisukan ketika tidak aktif!</string> + <string name="chat_theme_apply_to_all_modes">Seluruh mode warna</string> + <string name="chat_preferences_off">mati</string>` + <string name="chat_preferences_no">tidak</string> + <string name="chat_preferences_on">on</string> + <string name="feature_off">mati</string> + <string name="allow_your_contacts_irreversibly_delete">Izinkan kontak kamu menghapus pesan terkirim secara permanen. (24 jam)</string> + <string name="allow_irreversible_message_deletion_only_if">Izinkan penghapusan pesan yang tidak dapat diubah hanya jika kontak kamu mengizinkannya. (24 jam)</string> + <string name="allow_your_contacts_to_send_voice_messages">Izinkan kontak kamu mengirim pesan suara.</string> + <string name="only_you_can_delete_messages">Hanya kamu yang dapat menghapus pesan secara permanen (kontak kamu dapat menandainya untuk dihapus). (24 jam)</string> + <string name="only_your_contact_can_delete">Hanya kontak kamu yang dapat menghapus pesan secara permanen (kamu dapat menandainya untuk dihapus). (24 jam)</string> + <string name="only_you_can_add_message_reactions">Hanya kamu yang dapat menambahkan reaksi pesan.</string> + <string name="only_you_can_make_calls">Hanya kamu yang dapat melakukan panggilan.</string> + <string name="only_your_contact_can_add_message_reactions">Hanya kontak kamu yang dapat menambahkan reaksi pesan.</string> + <string name="allow_direct_messages">Izinkan pengiriman pesan langsung ke anggota.</string> + <string name="allow_to_send_disappearing">Izinkan untuk mengirim pesan menghilang.</string> + <string name="feature_offered_item_with_param">ditawarkan %s: %2s</string> + <string name="v4_5_multiple_chat_profiles">Beberapa profil obrolan</string> + <string name="v4_5_reduced_battery_usage_descr">Lebih banyak peningkatan akan segera hadir!</string> + <string name="v4_6_group_moderation_descr">Tidak ada admin yang dapat: +\n- menghapus pesan anggota. +\n- menonaktifkan anggota (peran “pengamat”)</string> + <string name="v4_6_reduced_battery_usage_descr">Lebih banyak peningkatan akan segera hadir!</string> + <string name="v5_2_more_things_descr">- pengiriman pesan yang lebih stabil. +\n- group yang sedikit lebih baik. +\n- dan lainnya!</string> + <string name="v5_3_new_desktop_app">Aplikasi desktop baru!</string> + <string name="v5_7_network">Manajemen jaringan</string> + <string name="custom_time_unit_months">bulan</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Semua kontak, percakapan, dan file kamu akan dienkripsi dengan aman dan diunggah dalam beberapa bagian ke relay XFTP yang telah dikonfigurasi.</string> + <string name="network_type_no_network_connection">Tidak ada koneksi jaringan</string> + <string name="mute_chat">Bisu</string> + <string name="settings_section_title_network_connection">Koneksi jaringan</string> + <string name="network_smp_proxy_mode_never">TIdak pernah</string> + <string name="privacy_media_blur_radius_off">Mati</string> + <string name="videos_limit_desc">Hanya 10 video dapat dikirim pada saat bersamaan</string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Hanya perangkat klien yang menyimpan profil pengguna, kontak, grup, dan pesan.</string> + <string name="only_group_owners_can_enable_voice">Hanya pemilik grup yang dapat mengaktifkan pesan suara.</string> + <string name="clear_note_folder_warning">Seluruh pesan akan dihapus - ini tidak bisa dibatalkan!</string> + <string name="network_smp_proxy_fallback_allow_downgrade">Izinkan turun versi</string> + <string name="no_info_on_delivery">Tidak ada informasi pengiriman</string> + <string name="allow_verb">Boleh</string> + <string name="ok">OK</string> + <string name="no_details">Tidak ada rincian</string> + <string name="add_contact">Tautan undangan satu kali</string> + <string name="network_use_onion_hosts_prefer_desc">Host Onion akan di pakai jika tersedia.</string> + <string name="always_use_relay">Selalu gunakan relay</string> + <string name="self_destruct_new_display_name">Nama tampilan baru:</string> + <string name="new_passphrase">Frasa sandi baru</string> + <string name="notifications_will_be_hidden">Pemberitahuan akan dikirim hanya sampai saat aplikasi berhenti!</string> + <string name="wallpaper_advanced_settings">Pengaturan tingkat lanjut</string> + <string name="allow_message_reactions_only_if">Izinkan reaksi pesan hanya jika kontak Anda mengizinkannya.</string> + <string name="allow_voice_messages_only_if">Izinkan pesan suara hanya jika kontak kamu mengizinkannya.</string> + <string name="allow_your_contacts_adding_message_reactions">Izinkan kontak kamu menambahkan reaksi pesan.</string> + <string name="allow_your_contacts_to_call">Izinkan kontak kamu untuk menghubungimu.</string> + <string name="allow_your_contacts_to_send_disappearing_messages">Izinkan kontak kamu untuk mengirim pesan menghilang.</string> + <string name="allow_calls_only_if">Izinkan panggilan hanya jika kontak kamu mengizinkannya.</string> + <string name="allow_disappearing_messages_only_if">Izinkan pesan menghilang hanya jika kontak kamu mengizinkannya.</string> + <string name="allow_message_reactions">Izinkan reaksi pesan.</string> + <string name="allow_to_delete_messages">Izinkan untuk menghapus pesan terkirim secara permanen. (24 jam)</string> + <string name="user_mute">Bisu</string> + <string name="no_history">Tidak ada riwayat</string> + <string name="v5_8_chat_themes">Tema obrolan yang baru</string> + <string name="all_users">Semua profil</string> + <string name="allow_to_send_simplex_links">Izinkan untuk mengirim tautan SimpleX.</string> + <string name="v5_1_self_destruct_passcode_descr">Semua data akan terhapus jika ini dimasukkan.</string> + <string name="not_compatible">Tidak kompatibel!</string> + <string name="new_mobile_device">Perangkat seluler baru</string> + <string name="only_one_device_can_work_at_the_same_time">Hanya satu perangkat yang dapat bekerja pada saat bersamaan</string> + <string name="network_smp_proxy_fallback_prohibit">Tidak</string> + <string name="no_filtered_contacts">Tidak ada kontak yang di filter</string> + <string name="group_member_role_observer">pengamat</string> + <string name="snd_conn_event_ratchet_sync_started">menyetujui enkripsi untuk %s…</string> + <string name="all_group_members_will_remain_connected">Semua anggota grup akan tetap terhubung.</string> + <string name="servers_info_missing">Tidak ada info, coba muat ulang</string> + <string name="network_use_onion_hosts_no">Tidak</string> + <string name="new_in_version">Baru di %s</string> + <string name="feature_offered_item">ditawarkan %s</string> + <string name="only_you_can_send_voice">Hanya kamu yang dapat mengirim pesan suara.</string> + <string name="only_your_contact_can_make_calls">Hanya kontak kamu yang dapat melakukan panggilan.</string> + <string name="allow_to_send_files">Izinkan untuk mengirim file dan media.</string> + <string name="all_your_contacts_will_remain_connected">Semua kontak kamu akan tetap terhubung.</string> + <string name="forward_files_missing_desc">%1$d berkas telah dihapus.</string> + <string name="color_primary_variant">Corak tambahan</string> + <string name="color_primary_variant2">Corak tambahan 2</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s pesan tidak diteruskan</string> + <string name="servers_info_subscriptions_connections_subscribed">Koneksi aktif</string> + <string name="feature_roles_admins">admin</string> + <string name="n_file_errors">%1$d berkas galat :\n%2$s</string> + <string name="reply_verb">Balas</string> + <string name="share_verb">Bagikan</string> + <string name="copy_verb">Salin</string> + <string name="edit_verb">Edit</string> + <string name="info_menu">Info</string> + <string name="saved_chat_item_info_tab">Tersimpan</string> + <string name="search_verb">Cari</string> + <string name="forwarded_chat_item_info_tab">Diteruskan</string> + <string name="saved_from_chat_item_info_title">Disimpan dari</string> + <string name="expand_verb">Perluas</string> + <string name="moderate_verb">Sedang</string> + <string name="delete_message__question">Hapus pesan?</string> + <string name="delete_messages__question">Hapus %d pesan?</string> + <string name="delete_message_cannot_be_undone_warning">Pesan akan dihapus - Tindakan ini tidak dapat dibatalkan!</string> + <string name="forward_files_in_progress_desc">%1$d berkas masih diunduh.</string> + <string name="answer_call">Jawab panggilan</string> + <string name="sent_message">Pesan terkirim</string> + <string name="received_message">Pesan diterima</string> + <string name="member_inactive_title">Anggota tidak aktif</string> + <string name="message_forwarded_title">Pesan diteruskan</string> + <string name="revoke_file__title">Cabut berkas?</string> + <string name="images_limit_title">Terlalu banyak gambar!</string> + <string name="info_view_search_button">cari</string> + <string name="info_view_call_button">panggilan</string> + <string name="settings_section_title_settings">PENGATURAN</string> + <string name="for_everybody">Untuk semua orang</string> + <string name="stop_file__action">Hentikan berkas</string> + <string name="revoke_file__action">Cabut berkas</string> + <string name="stop_snd_file__message">Mengirim berkas akan dihentikan.</string> + <string name="stop_file__confirm">Berhenti</string> + <string name="toolbar_settings">Pengaturan</string> + <string name="stop_snd_file__title">Berhenti kirim berkas?</string> + <string name="stop_rcv_file__message">Penerimaan berkas akan dihentikan.</string> + <string name="image_saved">Gambar disimpan ke Galeri</string> + <string name="info_view_video_button">video</string> + <string name="receipts_section_contacts">Kontak</string> + <string name="marked_deleted_items_description">%d pesan dihapus</string> + <string name="deleted_description">dihapus</string> + <string name="trying_to_connect_to_server_to_receive_messages">Mencoba terhubung ke server untuk menerima pesan dari kontak ini.</string> + <string name="saved_description">disimpan</string> + <string name="display_name_invited_to_connect">diundang untuk terhubung</string> + <string name="simplex_link_mode_description">Deskripsi</string> + <string name="failed_to_create_user_duplicate_title">Nama tampilan duplikat!</string> + <string name="delivery">Mengirim</string> + <string name="forwarded_from_chat_item_info_title">Diteruskan dari</string> + <string name="stop_rcv_file__title">Berhenti menerima berkas?</string> + <string name="revoke_file__confirm">Tarik</string> + <string name="revoke_file__message">Berkas akan dihapus dari server.</string> + <string name="icon_descr_sent_msg_status_sent">terkirim</string> + <string name="forward_chat_item">Teruskan</string> + <string name="download_file">Unduh</string> + <string name="icon_descr_edited">diedit</string> + <string name="your_chats">Obrolan</string> + <string name="welcome">Selamat Datang!</string> + <string name="chat_with_developers">Chat dengan pengembang</string> + <string name="text_field_set_contact_placeholder">Setel nama kontak…</string> + <string name="icon_descr_settings">Pengaturan</string> + <string name="save_verb">Simpan</string> + <string name="compose_send_direct_message_to_connect">Kirim pesan langsung untuk terhubung</string> + <string name="file_saved">Berkas disimpan</string> + <string name="proxy_destination_error_broker_host">Alamat server tujuan %1$s tidak kompatibel dengan pengaturan server penerusan %2$s.</string> + <string name="edit_history">Riwayat</string> + <string name="delete_verb">Hapus</string> + <string name="reveal_verb">Perlihat</string> + <string name="hide_verb">Sembunyikan</string> + <string name="receipts_section_groups">Grup kecil (maks 20)</string> + <string name="select_verb">Pilih</string> + <string name="for_me_only">Hapus untuk saya</string> + <string name="error_forwarding_messages">Gagal meneruskan pesan</string> + <string name="n_other_file_errors">%1$d berkas lainnya gagal.</string> + <string name="contact_already_exists">Kontak sudah ada</string> + <string name="contact_connection_pending">menghubungkan…</string> + <string name="member_contact_send_direct_message">kirim pesan langsung</string> + <string name="you_have_no_chats">Anda tidak memiliki obrolan</string> + <string name="loading_chats">Memuat obrolan…</string> + <string name="contact_tap_to_connect">Ketuk untuk Hubungkan</string> + <string name="connect_with_contact_name_question">Terhubung dengan %1$s?</string> + <string name="forward_alert_title_messages_to_forward">Teruskan %1$s pesan?</string> + <string name="forward_alert_title_nothing_to_forward">Tidak ada yang diteruskan!</string> + <string name="forward_alert_forward_messages_without_files">Teruskan pesan tanpa berkas?</string> + <string name="forward_files_messages_deleted_after_selection_desc">Pesan dihapus setelah Anda memilihnya.</string> + <string name="forward_files_not_accepted_desc">%1$d berkas tidak diunduh.</string> + <string name="forward_files_not_accepted_receive_files">Unduh</string> + <string name="forward_multiple">Teruskan pesan…</string> + <string name="compose_forward_messages_n">Meneruskan %1$s pesan</string> + <string name="simplex_links_not_allowed">Tautan SimpleX tidak diizinkan</string> + <string name="files_and_media_not_allowed">Berkas dan media tidak diizinkan</string> + <string name="video_descr">Video</string> + <string name="icon_descr_waiting_for_video">Menunggu video</string> + <string name="file_not_found">Berkas tidak ditemukan</string> + <string name="reject_contact_button">Tolak</string> + <string name="new_chat_share_profile">Bagikan profil</string> + <string name="select_chat_profile">Pilih profil obrolan</string> + <string name="deleted_chats">Kontak arsipan</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">Kode yang Anda pindai bukan kode QR tautan SimpleX.</string> + <string name="to_verify_compare">Untuk memverifikasi enkripsi end-to-end dengan kontak Anda, bandingkan (atau pindai) kode pada perangkat Anda.</string> + <string name="contact_list_header_title">Kontak Anda</string> + <string name="scan_code">Pindai kode</string> + <string name="mark_code_verified">Tandai terverifikasi</string> + <string name="is_verified">%s telah terverifikasi</string> + <string name="is_not_verified">%s belum terverifikasi</string> + <string name="clear_verification">Bersihkan verifikasi</string> + <string name="chat_console">Konsol obrolan</string> + <string name="message_servers">Server pesan</string> + <string name="smp_servers">Server SMP</string> + <string name="rate_the_app">Beri nilai aplikasi</string> + <string name="star_on_github">Bintang di GitHub</string> + <string name="use_simplex_chat_servers__question">Gunakan server SimpleX Chat?</string> + <string name="your_SMP_servers">Server SMP Anda</string> + <string name="xftp_servers">Server XFTP</string> + <string name="network_proxy_auth_mode_username_password">Kredensial Anda mungkin dikirim tidak terenkripsi.</string> + <string name="network_proxy_username">Username</string> + <string name="your_XFTP_servers">Server XFTP Anda</string> + <string name="using_simplex_chat_servers">Gunakan server SimpleX Chat.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">Gunakan kredensial proxy yang berbeda untuk setiap koneksi.</string> + <string name="network_proxy_password">Kata sandi</string> + <string name="network_proxy_port">port %d</string> + <string name="host_verb">Host</string> + <string name="port_verb">Port</string> + <string name="network_enable_socks">Gunakan proxy SOCKS?</string> + <string name="network_enable_socks_info">Akses server melalui proxy SOCKS pada port %d? Proxy harus dimulai sebelum mengaktifkan opsi ini.</string> + <string name="network_disable_socks">Gunakan koneksi Internet langsung?</string> + <string name="network_proxy_incorrect_config_desc">Pastikan konfigurasi proxy sudah benar.</string> + <string name="network_proxy_incorrect_config_title">Gagal simpan proxy</string> + <string name="network_use_onion_hosts_prefer">Saat tersedia</string> + <string name="network_use_onion_hosts_required">Diperlukan</string> + <string name="network_disable_socks_info">Jika Anda konfirmasi, server perpesanan akan dapat melihat alamat IP Anda, dan penyedia Anda - server mana yang Anda hubungkan.</string> + <string name="network_session_mode_user">Profil obrolan</string> + <string name="network_session_mode_entity">Koneksi</string> + <string name="network_smp_proxy_mode_unknown">Server tak dikenal</string> + <string name="network_smp_proxy_fallback_allow_protected_description">Kirim pesan secara langsung ketika alamat IP dilindungi dan server Anda atau tujuan tidak mendukung routing pribadi.</string> + <string name="network_smp_proxy_fallback_allow_description">Kirim pesan secara langsung ketika server Anda atau server tujuan tidak mendukung routing pribadi.</string> + <string name="private_routing_show_message_status">Tampilkan status pesan</string> + <string name="auto_accept_contact">Terima otomatis</string> + <string name="invite_friends">Undang teman</string> + <string name="create_profile_button">Buat</string> + <string name="invalid_name">Nama tidak valid!</string> + <string name="create_another_profile_button">Buat profil</string> + <string name="display_name">Masukkan nama Anda:</string> + <string name="secret_text">rahasia</string> + <string name="italic_text">miring</string> + <string name="colored_text">berwarna</string> + <string name="callstatus_connecting">menghubung panggilan…</string> + <string name="callstatus_ended">panggilan berakhir %1$s</string> + <string name="callstatus_in_progress">panggilan berlangsung</string> + <string name="callstate_ended">berakhir</string> + <string name="audio_device_wired_headphones">Headphone</string> + <string name="error_initializing_web_view">Kesalahan saat menginisialisasi WebView. Perbarui sistem Anda ke versi baru. Mohon hubungi pengembang.\nKesalahan: %s</string> + <string name="people_can_connect_only_via_links_you_share">Anda pilih siapa yang dapat terhubung.</string> + <string name="immune_to_spam_and_abuse">Kebal terhadap spam</string> + <string name="create_your_profile">Buat profil Anda</string> + <string name="make_private_connection">Buat koneksi pribadi</string> + <string name="ignore">Lewati</string> + <string name="your_calls">Panggilan Anda</string> + <string name="show_call_on_lock_screen">Lihat</string> + <string name="relay_server_if_necessary">Server relai hanya digunakan jika diperlukan. Pihak lain dapat mengamati alamat IP Anda.</string> + <string name="icon_descr_speaker_off">Speaker mati</string> + <string name="icon_descr_video_on">Video nyala</string> + <string name="icon_descr_audio_off">Suara mati</string> + <string name="icon_descr_audio_on">Suara nyala</string> + <string name="icon_descr_speaker_on">Speaker nyala</string> + <string name="icon_descr_flip_camera">Balik kamera</string> + <string name="icon_descr_call_progress">Panggilan berlangsung</string> + <string name="icon_descr_call_connecting">Menghubungkan panggilan</string> + <string name="alert_title_skipped_messages">Pesan yang terlewati</string> + <string name="alert_text_msg_bad_hash">Hash dari pesan sebelumnya berbeda.\"</string> + <string name="privacy_and_security">Privasi & keamanan</string> + <string name="encrypt_local_files">Enkripsi berkas lokal</string> + <string name="auto_accept_images">Terima gambar otomatis</string> + <string name="protect_app_screen">Lindungi layar aplikasi</string> + <string name="protect_ip_address">Lindungi alamat IP</string> + <string name="privacy_show_last_messages">Lihat pesan terakhir</string> + <string name="send_link_previews">Kirim pratinjau tautan</string> + <string name="privacy_message_draft">Draf pesan</string> + <string name="confirm_passcode">Konfirmasi kode sandi</string> + <string name="lock_after">Kunci setelah</string> + <string name="submit_passcode">Kirim</string> + <string name="incorrect_passcode">Kode sandi salah</string> + <string name="la_app_passcode">Kode sandi aplikasi</string> + <string name="la_mode_passcode">Kode sandi</string> + <string name="chat_item_ttl_seconds">%s detik</string> + <string name="group_invitation_tap_to_join">Ketuk untuk gabung</string> + <string name="rcv_group_event_member_blocked">diblokir %s</string> + <string name="rcv_group_event_open_chat">Buka</string> + <string name="conn_event_ratchet_sync_ok">enkripsi aman</string> + <string name="group_member_status_unknown">status tidak diketahui</string> + <string name="group_member_status_invited">diundang</string> + <string name="group_member_role_admin">admin</string> + <string name="group_member_status_removed">dihapus</string> + <string name="group_member_status_group_deleted">grup dihapus</string> + <string name="import_theme_error">Gagal impor tema</string> + <string name="import_theme">Impor tema</string> + <string name="import_theme_error_desc">Pastikan berkas memiliki sintaksis YAML yang benar. Ekspor tema untuk mendapatkan contoh struktur berkas tema.</string> + <string name="export_theme">Ekspor tema</string> + <string name="color_secondary">Sekunder</string> + <string name="color_background">Latar</string> + <string name="color_sent_message">Pesan terkirim</string> + <string name="chat_theme_reset_to_user_theme">Reset ke tema pengguna</string> + <string name="chat_theme_set_default_theme">Pasang tema bawaan</string> + <string name="chat_theme_apply_to_mode">Terapkan</string> + <string name="wallpaper_scale_fit">Sesuai</string> + <string name="chat_theme_reset_to_app_theme">Reset ke tema aplikasi</string> + <string name="chat_preferences_you_allow">Izin Anda</string> + <string name="chat_preferences_contact_allows">Izin kontak</string> + <string name="chat_preferences_default">bawaan (%s)</string> + <string name="timed_messages">Pesan sementara</string> + <string name="direct_messages">Pesan pribadi</string> + <string name="voice_messages">Pesan suara</string> + <string name="set_group_preferences">Setel preferensi grup</string> + <string name="your_preferences">Preferensi Anda</string> + <string name="full_deletion">Hapus untuk semua orang</string> + <string name="feature_received_prohibited">diterima, dilarang</string> + <string name="accept_feature_set_1_day">Pasang 1 hari</string> + <string name="contacts_can_mark_messages_for_deletion">Kontak dapat menandai pesan untuk dihapus; Anda akan dapat melihatnya.</string> + <string name="prohibit_sending_voice_messages">Melarang pengiriman pesan suara.</string> + <string name="message_deletion_prohibited">Hapus pesan tidak dapat dibatalkan dilarang dalam obrolan ini.</string> + <string name="calls_prohibited_with_this_contact">Panggilan audio/video dilarang.</string> + <string name="prohibit_message_reactions_group">Reaksi pesan dilarang.</string> + <string name="group_members_can_send_disappearing">Anggota dapat mengirim pesan sementara.</string> + <string name="disappearing_messages_are_prohibited">Pesan sementara dilarang.</string> + <string name="ttl_week">%d minggu</string> + <string name="ttl_weeks">%d minggu</string> + <string name="v4_2_security_assessment_desc">Keamanan SimpleX Chat diaudit oleh Trail of Bits.</string> + <string name="v4_2_group_links">Tautan grup</string> + <string name="v4_2_group_links_desc">Admin dapat membuat tautan untuk gabung ke grup.</string> + <string name="v4_3_voice_messages_desc">Maks 40 detik, diterima secara instan.</string> + <string name="v4_3_irreversible_message_deletion">Hapus pesan tidak dapat dibatalkan</string> + <string name="v4_4_live_messages_desc">Penerima melihat pesan langsung saat Anda mengetik.</string> + <string name="v4_5_transport_isolation">Isolasi transport</string> + <string name="v4_6_group_welcome_message">Pesan sambutan grup</string> + <string name="v4_6_chinese_spanish_interface">Antarmuka bahasa Cina dan Spanyol</string> + <string name="v5_0_large_files_support">Video dan berkas hingga 1GB</string> + <string name="v5_0_polish_interface_descr">Terima kasih kepada pengguna – berkontribusi melalui Weblate!</string> + <string name="v5_1_message_reactions">Reaksi pesan</string> + <string name="v5_2_fix_encryption">Jaga koneksi Anda</string> + <string name="v5_4_better_groups">Grup lebih baik</string> + <string name="v5_4_incognito_groups">Grup samaran</string> + <string name="v5_4_link_mobile_desktop_descr">Melalui protokol quantum resistant yang aman.</string> + <string name="v5_4_better_groups_descr">Gabung lebih cepat dan pesan lebih handal.</string> + <string name="v5_4_block_group_members_descr">Untuk sembunyikan pesan tak diinginkan.</string> + <string name="v5_6_picture_in_picture_calls_descr">Gunakan aplikasi saat dalam panggilan.</string> + <string name="v5_8_safe_files_descr">Konfirmasi berkas dari server tak dikenal.</string> + <string name="delivery_receipts_are_disabled">Tanda terima pengiriman dimatikan!</string> + <string name="custom_time_picker_custom">kustom</string> + <string name="verify_code_with_desktop">Verifikasi kode dengan desktop</string> + <string name="in_developing_desc">Fitur ini belum didukung. Coba pada versi berikutnya.</string> + <string name="agent_internal_error_title">Kesalahan internal</string> + <string name="migrate_to_device_downloading_archive">Mengunduh arsip</string> + <string name="migrate_to_device_download_failed">Unduhan gagal</string> + <string name="migrate_from_device_creating_archive_link">Buat tautan arsip</string> + <string name="migrate_from_device_start_chat">Mulai obrolan</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Pastikan Anda mengingat frasa sandi basis data untuk memindahkan.</string> + <string name="current_user">Profil saat ini</string> + <string name="create_address_button">Buat</string> + <string name="reset_color">Reset warna</string> + <string name="sending_files_not_yet_supported">kirim berkas belum didukung</string> + <string name="invalid_data">data tidak valid</string> + <string name="error_showing_message">gagal menampilkan pesan</string> + <string name="error_showing_content">gagal menampilkan konten</string> + <string name="decryption_error">Gagal dekripsi</string> + <string name="encryption_renegotiation_error">Gagal negosiasi ulang enkripsi</string> + <string name="e2ee_info_no_pq"><![CDATA[Pesan, berkas dan panggilan dilindungi oleh <b>enkripsi end-to-end</b> dengan perfect forward secrecy, penolakan dan pemulihan pembobolan.]]></string> + <string name="simplex_link_mode_browser">Via peramban</string> + <string name="failed_to_active_user_title">Gagal mengganti profil</string> + <string name="simplex_link_mode_browser_warning">Buka tautan di peramban mengurangi privasi dan keamanan koneksi. Tautan SimpleX tidak tepercaya akan berwarna merah.</string> + <string name="error_saving_smp_servers">Gagal simpan server SMP</string> + <string name="ensure_smp_server_address_are_correct_format_and_unique">Pastikan alamat server SMP dalam format yang benar, pisahkan baris dan tidak terduplikasi.</string> + <string name="ensure_xftp_server_address_are_correct_format_and_unique">Pastikan alamat server XFTP dalam format yang benar, pisahkan baris dan tidak terduplikasi.</string> + <string name="icon_descr_sent_msg_status_send_failed">gagal terkirim</string> + <string name="icon_descr_received_msg_status_unread">belum dibaca</string> + <string name="member_inactive_desc">Pesan dapat disampaikan kemudian jika anggota menjadi aktif.</string> + <string name="personal_welcome">Selamat Datang %1$s!</string> + <string name="selected_chat_items_selected_n">%d Dipilih</string> + <string name="videos_limit_title">Terlalu banyak video!</string> + <string name="voice_message">Pesan suara</string> + <string name="info_view_message_button">pesan</string> + <string name="info_view_open_button">buka</string> + <string name="la_mode_system">Sistem</string> + <string name="profile_update_event_set_new_address">setel alamat kontak baru</string> + <string name="dark_theme">Tema gelap</string> + <string name="theme">Tema</string> + <string name="color_mode">Mode warna</string> + <string name="color_sent_quote">Balasan terkirim</string> + <string name="color_received_message">Pesan diterima</string> + <string name="appearance_zoom">Perbesar</string> + <string name="prohibit_sending_disappearing_messages">Melarang kirim pesan sementara.</string> + <string name="prohibit_message_reactions">Reaksi pesan dilarang.</string> + <string name="prohibit_calls">Panggilan suara/video dilarang.</string> + <string name="both_you_and_your_contacts_can_delete">Anda dan kontak Anda dapat menghapus pesan terkirim secara permanen. (24 jam)</string> + <string name="disappearing_prohibited_in_this_chat">Pesan sementara dilarang dalam obrolan ini.</string> + <string name="voice_prohibited_in_this_chat">Pesan suara dilarang dalam obrolan ini.</string> + <string name="both_you_and_your_contact_can_add_message_reactions">Anda dan kontak dapat menambahkan reaksi pesan.</string> + <string name="group_members_can_delete">Anggota dapat hapus pesan terkirim secara permanen. (24 jam)</string> + <string name="group_members_can_send_voice">Anggota dapat mengirim pesan suara.</string> + <string name="message_deletion_prohibited_in_chat">Hapus pesan yang tidak dapat dibatalkan dilarang.</string> + <string name="direct_messages_are_prohibited_in_group">Pesan pribadi antar anggota dilarang di grup ini.</string> + <string name="group_members_can_send_simplex_links">Anggota dapat kirim tautan SimpleX.</string> + <string name="ttl_hours">%d jam</string> + <string name="ttl_hour">%d jam</string> + <string name="ttl_day">%d hari</string> + <string name="ttl_days">%d hari</string> + <string name="ttl_w">%dmg</string> + <string name="ttl_h">%dj</string> + <string name="feature_cancelled_item">dibatalkan %s</string> + <string name="v4_2_auto_accept_contact_requests_desc">Dengan pesan sambutan opsional.</string> + <string name="v4_2_auto_accept_contact_requests">Terima permintaan kontak secara otomatis</string> + <string name="whats_new_read_more">Selengkapnya</string> + <string name="v4_2_security_assessment">Penilaian keamanan</string> + <string name="v4_3_improved_server_configuration">Konfigurasi server ditingkatkan</string> + <string name="v4_3_improved_server_configuration_desc">Tambah server dengan pindai kode QR.</string> + <string name="v4_3_improved_privacy_and_security">Peningkatan privasi dan keamanan</string> + <string name="v4_4_french_interface_descr">Terima kasih kepada pengguna – berkontribusi melalui Weblate!</string> + <string name="v4_4_french_interface">Antarmuka Prancis</string> + <string name="v4_4_verify_connection_security_desc">Bandingkan kode keamanan dengan kontak Anda.</string> + <string name="v4_5_message_draft_descr">Simpan draf pesan terakhir, dengan lampiran.</string> + <string name="v4_5_transport_isolation_descr">Berdasarkan profil obrolan (bawaan) atau berdasarkan koneksi (BETA).</string> + <string name="v4_5_private_filenames_descr">Untuk melindungi zona waktu, berkas gambar/suara menggunakan UTC.</string> + <string name="v4_6_reduced_battery_usage">Penggunaan baterai semakin sedikit</string> + <string name="v4_6_group_welcome_message_descr">Atur pesan yang ditampilkan kepada anggota baru!</string> + <string name="v5_6_app_data_migration">Pindahan data aplikasi</string> + <string name="v5_8_safe_files">Terima berkas dengan aman</string> + <string name="v5_8_chat_themes_descr">Buat obrolan Anda terlihat berbeda!</string> + <string name="v5_8_message_delivery">Peningkatan pengiriman pesan</string> + <string name="v5_7_new_interface_languages">UI Lituania</string> + <string name="v6_0_your_contacts_descr">Arsip kontak untuk mengobrol nanti.</string> + <string name="v6_0_reachable_chat_toolbar_descr">Gunakan aplikasi dengan satu tangan.</string> + <string name="v6_0_connect_faster_descr">Terhubung dengan teman lebih cepat.</string> + <string name="v6_0_private_routing_descr">Ini melindungi alamat IP dan koneksi Anda.</string> + <string name="v6_0_upgrade_app_descr">Unduh versi baru dari GitHub.</string> + <string name="this_device_name">Nama perangkat ini</string> + <string name="remote_host_was_disconnected_title">Koneksi terputus</string> + <string name="remote_ctrl_error_inactive">Desktop tidak aktif</string> + <string name="connect_plan_repeat_join_request">Ingin bergabung lagi?</string> + <string name="migrate_to_device_importing_archive">Mengimpor arsipan</string> + <string name="migrate_from_device_title">Pindah perangkat</string> + <string name="migrate_from_device_error_exporting_archive">Gagal Ekspor basis data obrolan</string> + <string name="servers_info">Informasi server</string> + <string name="servers_info_target">Lihat info untuk</string> + <string name="servers_info_modal_error_title">Galat</string> + <string name="servers_info_uploaded">Diunggah</string> + <string name="servers_info_downloaded">Diunduh</string> + <string name="servers_info_detailed_statistics_sent_messages_header">Pesan terkirim</string> + <string name="servers_info_reset_stats_alert_message">Statistik server akan direset - ini tidak dapat dibatalkan!</string> + <string name="v6_0_increase_font_size">Perbesar ukuran font.</string> + <string name="v5_7_forward_descr">Sumber pesan tetap pribadi.</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Siapa pun dapat menjadi pemegang server.</string> + <string name="decentralized">Terdesentralisasi</string> + <string name="error_initializing_web_view_wrong_arch">Kesalahan saat menginisialisasi WebView. Pastikan Anda telah menginstal WebView dan arsitektur yang didukung adalah arm64.\nKesalahan: %s</string> + <string name="use_chat">Gunakan obrolan</string> + <string name="how_it_works">Bagaimana caranya</string> + <string name="how_simplex_works">Cara kerja SimpleX</string> + <string name="onboarding_notifications_mode_periodic">Berkala</string> + <string name="incoming_audio_call">Panggilan suara masuk</string> + <string name="encrypted_audio_call">panggilan suara terenkripsi e2e</string> + <string name="encrypted_video_call">panggilan video terenkripsi e2e</string> + <string name="use_random_passphrase">Gunakan frasa sandi acak</string> + <string name="settings_audio_video_calls">Panggilan suara & video</string> + <string name="reject">Tolak</string> + <string name="icon_descr_video_call">panggilan video</string> + <string name="status_contact_has_e2e_encryption">kontak memiliki enkripsi e2e</string> + <string name="icon_descr_hang_up">Tutup</string> + <string name="icon_descr_video_off">Video mati</string> + <string name="icon_descr_sound_muted">Suara dibisukan</string> + <string name="icon_descr_call_pending_sent">Panggilan tertunda</string> + <string name="enable_lock">Aktifkan kunci</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Tanpa Tor atau VPN, alamat IP Anda akan terlihat oleh server file.</string> + <string name="lock_mode">Mode kunci</string> + <string name="authentication_cancelled">Autentikasi dibatalkan</string> + <string name="change_lock_mode">Ubah mode kunci</string> + <string name="passcode_set">Kode sandi dipasang!</string> + <string name="passcode_changed">Kode sandi diubah!</string> + <string name="self_destruct_passcode_changed">Kode sandi hapus otomatis diubah!</string> + <string name="self_destruct_passcode">Kode sandi hapus otomatis</string> + <string name="enable_self_destruct">Aktifkan hapus otomatis</string> + <string name="set_passcode">Pasang kode sandi</string> + <string name="settings_section_title_help">BANTUAN</string> + <string name="settings_section_title_support">DUKUNG SIMPLEX CHAT</string> + <string name="settings_section_title_calls">PANGGILAN</string> + <string name="restart_the_app_to_create_a_new_chat_profile">Mulai ulang aplikasi untuk buat profil obrolan baru.</string> + <string name="delete_messages">Hapus pesan</string> + <string name="rcv_group_event_member_left">keluar</string> + <string name="snd_group_event_user_left">Anda keluar</string> + <string name="color_mode_light">Terang</string> + <string name="dark_mode_colors">Warna mode gelap</string> + <string name="reset_single_color">Reset warna</string> + <string name="chat_theme_apply_to_dark_mode">Mode gelap</string> + <string name="chat_theme_apply_to_light_mode">Mode terang</string> + <string name="chat_preferences">Preferensi obrolan</string> + <string name="contact_preferences">Preferensi kontak</string> + <string name="feature_enabled">diaktifkan</string> + <string name="feature_enabled_for_you">diaktifkan untuk anda</string> + <string name="available_in_v51">\nTersedia di v5.1</string> + <string name="prohibit_sending_voice">Kirim pesan suara tidak diizinkan.</string> + <string name="recent_history_is_sent_to_new_members">Hingga 100 pesan terakhir dikirim ke anggota baru.</string> + <string name="v4_3_voice_messages">Pesan suara</string> + <string name="v4_4_disappearing_messages">Pesan sementara</string> + <string name="v4_4_live_messages">Pesan langsung</string> + <string name="v4_6_hidden_chat_profiles">Profil obrolan tersembunyi</string> + <string name="v4_6_group_moderation">Moderasi grup</string> + <string name="v5_1_custom_themes_descr">Kustomisasi dan bagikan warna tema.</string> + <string name="v5_2_fix_encryption_descr">Perbaiki enkripsi setelah memulihkan cadangan.</string> + <string name="v5_3_new_interface_languages_descr">Bahasa Arab, Bulgaria, Finlandia, Ibrani, Thailand, dan Ukraina - terima kasih kepada pengguna dan Weblate.</string> + <string name="v5_5_join_group_conversation">Gabung ke percakapan grup</string> + <string name="v5_5_simpler_connect_ui_descr">Bilah pencarian menerima tautan undangan.</string> + <string name="v5_7_forward">Teruskan dan simpan pesan</string> + <string name="v5_6_picture_in_picture_calls">Panggilan gambar-dalam-gambar</string> + <string name="v5_7_shape_profile_images_descr">Persegi, lingkaran, atau apa pun di antaranya.</string> + <string name="v5_7_shape_profile_images">Bentuk gambar profil</string> + <string name="disconnect_desktop_question">Putuskan desktop?</string> + <string name="refresh_qr_code">Segarkan</string> + <string name="random_port">Acak</string> + <string name="open_port_in_firewall_title">Buka port di firewall</string> + <string name="connect_plan_connect_via_link">Hubungkan via tautan?</string> + <string name="agent_critical_error_title">Kesalahan besar</string> + <string name="image_descr">Gambar</string> + <string name="compose_message_placeholder">pesan</string> + <string name="icon_descr_waiting_for_image">Menunggu gambar</string> + <string name="icon_descr_asked_to_receive">Diminta untuk menerima gambar</string> + <string name="icon_descr_image_snd_complete">Gambar terkirim</string> + <string name="creating_link">Membuat tautan…</string> + <string name="invalid_qr_code">Kode QR tidak valid</string> + <string name="switching_profile_error_title">Kesalahan saat mengganti profil</string> + <string name="tap_to_paste_link">Ketuk untuk tempel tautan</string> + <string name="switching_profile_error_message">Koneksi Anda dipindahkan ke %s tetapi terjadi kesalahan tak terduga saat mengarahkan Anda ke profil.</string> + <string name="send_us_an_email">Kirim kami email</string> + <string name="chat_with_the_founder">Kirim pertanyaan dan ide</string> + <string name="chat_lock">Kunci SimpleX</string> + <string name="markdown_help">Bantuan Markdown</string> + <string name="media_and_file_servers">Server media & berkas</string> + <string name="smp_servers_your_server">Server Anda</string> + <string name="smp_servers_preset_address">Alamat server ditetapkan</string> + <string name="smp_servers_use_server_for_new_conn">Gunakan di koneksi baru</string> + <string name="smp_save_servers_question">Simpan server?</string> + <string name="smp_servers_per_user">Server untuk koneksi baru profil obrolan Anda saat ini</string> + <string name="network_use_onion_hosts">Gunakan host .onion</string> + <string name="network_socks_proxy">Proxy SOCKS</string> + <string name="how_to">Bagaimana</string> + <string name="configure_ICE_servers">Konfigurasi server ICE</string> + <string name="enter_one_ICE_server_per_line">Server ICE (satu per baris)</string> + <string name="share_link">Bagikan tautan</string> + <string name="save_settings_question">Simpan pengaturan?</string> + <string name="create_profile">Buat profil</string> + <string name="save_profile_password">Simpan kata sandi profil</string> + <string name="hidden_profile_password">Kata sandi profil tersembunyi</string> + <string name="permissions_record_audio">Mikrofon</string> + <string name="privacy_redefined">Privasi didefinisikan ulang</string> + <string name="onboarding_notifications_mode_subtitle">Hal yang mempengaruhi baterai</string> + <string name="onboarding_notifications_mode_off">Saat aplikasi sedang berjalan</string> + <string name="onboarding_notifications_mode_title">Notifikasi pribadi</string> + <string name="onboarding_notifications_mode_service">Instan</string> + <string name="change_self_destruct_passcode">Ubah kode sandi hapus otomatis</string> + <string name="passcode_not_changed">Kode sandi tidak diubah!</string> + <string name="self_destruct">Hapus otomatis</string> + <string name="enabled_self_destruct_passcode">Aktifkan kode sandi hapus otomatis</string> + <string name="change_self_destruct_mode">Ubah mode hapus otomatis</string> + <string name="self_destruct_passcode_enabled">Kode sandi hapus otomatis diaktifkan!</string> + <string name="receipts_contacts_title_disable">Matikan tanda terima?</string> + <string name="receipts_contacts_title_enable">Aktifkan tanda terima?</string> + <string name="privacy_media_blur_radius_medium">Sedang</string> + <string name="privacy_media_blur_radius">Buram media</string> + <string name="privacy_media_blur_radius_strong">Kuat</string> + <string name="settings_section_title_you">ANDA</string> + <string name="privacy_media_blur_radius_soft">Lunak</string> + <string name="settings_section_title_chat_database">BASIS DATA OBROLAN</string> + <string name="set_password_to_export">Setel frasa sandi untuk diekspor</string> + <string name="open_database_folder">Buka folder basis data</string> + <string name="rcv_group_event_user_deleted">menghapus anda</string> + <string name="v5_6_safer_groups_descr">Admin dapat memblokir anggota untuk semua.</string> + <string name="v5_7_quantum_resistant_encryption_descr">Akan diaktifkan dalam obrolan pribadi!</string> + <string name="v5_6_safer_groups">Grup aman</string> + <string name="session_code">Kode sesi</string> + <string name="verify_connections">Verifikasi koneksi</string> + <string name="remote_host_error_timeout"><![CDATA[Batas waktu tercapai saat menghubungkan ke ponsel <b>%s</b>]]></string> + <string name="connect_plan_this_is_your_own_simplex_address">Ini adalah alamat SimpleX Anda!</string> + <string name="connect_plan_repeat_connection_request">Permintaan koneksi berulang?</string> + <string name="connect_plan_join_your_group">Gabung ke grup Anda?</string> + <string name="servers_info_statistics_section_header">Statistik</string> + <string name="servers_info_subscriptions_total">Total</string> + <string name="marked_deleted_description">ditandai dihapus</string> + <string name="blocked_by_admin_item_description">diblokir oleh admin</string> + <string name="blocked_items_description">%d pesan diblokir</string> + <string name="blocked_by_admin_items_description">%d pesan diblokir oleh admin</string> + <string name="blocked_item_description">diblokir</string> + <string name="live">LIVE</string> + <string name="moderated_description">dimoderasi</string> + <string name="invalid_chat">obrolan tidak valid</string> + <string name="forwarded_description">diteruskan</string> + <string name="saved_from_description">disimpan dari %s</string> + <string name="receiving_files_not_yet_supported">terima berkas belum didukung</string> + <string name="sender_you_pronoun">anda</string> + <string name="unknown_message_format">format pesan tak diketahui</string> + <string name="invalid_message_format">format pesan tidak valid</string> + <string name="e2ee_info_pq_short">Obrolan ini dilindungi oleh enkripsi end-to-end quantum resistant.</string> + <string name="e2ee_info_pq"><![CDATA[Pesan, berkas dan panggilan dilindungi oleh <b>enkripsi quantum resistant e2e</b> dengan perfect forward secrecy, penolakan dan pemulihan pembobolan.]]></string> + <string name="e2ee_info_no_pq_short">Obrolan ini dilindungi oleh enkripsi end-to-end.</string> + <string name="note_folder_local_display_name">Catatan pribadi</string> + <string name="connection_local_display_name">koneksi %1$d</string> + <string name="display_name_connection_established">koneksi terjalin</string> + <string name="display_name_connecting">menghubungkan…</string> + <string name="description_you_shared_one_time_link">Anda bagikan tautan sekali</string> + <string name="description_you_shared_one_time_link_incognito">Anda bagikan tautan sekali samaran</string> + <string name="description_via_group_link">via tautan grup</string> + <string name="description_via_group_link_incognito">samaran via tautan grup</string> + <string name="description_via_contact_address_link">via tautan alamat kontak</string> + <string name="description_via_contact_address_link_incognito">samaran via tautan alamat kontak</string> + <string name="description_via_one_time_link">via tautan sekali</string> + <string name="simplex_link_contact">Alamat kontak SimpleX</string> + <string name="description_via_one_time_link_incognito">samaran via tautan sekali</string> + <string name="simplex_link_mode_full">Tautan lengkap</string> + <string name="simplex_link_group">Tautan grup SimpleX</string> + <string name="simplex_link_mode">Tautan SimpleX</string> + <string name="simplex_link_invitation">Undangan sekali SimpleX</string> + <string name="simplex_link_connection">via %1$s</string> + <string name="error_saving_xftp_servers">Gagal simpan server XFTP</string> + <string name="failed_to_create_user_invalid_title">Nama tampilan tidak valid!</string> + <string name="failed_to_create_user_title">Gagal membuat profil!</string> + <string name="connection_error">Kesalahan koneksi</string> + <string name="error_sending_message">Gagal mengirim pesan</string> + <string name="error_creating_message">Gagal membuat pesan</string> + <string name="connection_timeout">Waktu koneksi habis</string> + <string name="icon_descr_sent_msg_status_unauthorized_send">pengiriman tidak sah</string> + <string name="this_text_is_available_in_settings">Teks ini tersedia di pengaturan</string> + <string name="tap_to_start_new_chat">Ketuk untuk memulai obrolan baru</string> + <string name="group_preview_you_are_invited">Anda diundang ke grup</string> + <string name="group_preview_join_as">gabung sebagai %s</string> + <string name="group_connection_pending">menghubungkan…</string> + <string name="cannot_share_message_alert_title">Tidak dapat kirim pesan</string> + <string name="share_message">Bagikan pesan…</string> + <string name="forward_message">Teruskan pesan…</string> + <string name="share_image">Bagikan media…</string> + <string name="share_file">Bagikan berkas…</string> + <string name="image_decoding_exception_title">Kesalahan decoding</string> + <string name="icon_descr_video_snd_complete">Video terkirim</string> + <string name="icon_descr_video_asked_to_receive">Diminta untuk menerima video</string> + <string name="waiting_for_file">Menunggu berkas</string> + <string name="abort_switch_receiving_address_desc">Perubahan alamat akan dibatalkan. Alamat penerima lama akan digunakan.</string> + <string name="back">Kembali</string> + <string name="learn_more">Lebih lanjut</string> + <string name="or_scan_qr_code">Atau pindai kode QR</string> + <string name="keep_invitation_link">Simpan</string> + <string name="keep_unused_invitation_question">Simpan undangan tidak terpakai?</string> + <string name="you_can_view_invitation_link_again">Anda dapat melihat tautan undangan lagi dalam detail koneksi.</string> + <string name="simplex_address">Alamat SimpleX</string> + <string name="or_show_this_qr_code">Atau perlihatkan kode ini</string> + <string name="scan_code_from_contacts_app">Pindai kode keamanan dari aplikasi kontak Anda.</string> + <string name="security_code">Kode keamanan</string> + <string name="incorrect_code">Kode keamanan salah!</string> + <string name="how_to_use_simplex_chat">Cara menggunakannya</string> + <string name="database_passphrase_and_export">Frasa sandi & ekspor basis data</string> + <string name="create_chat_profile">Buat profil obrolan</string> + <string name="markdown_in_messages">Markdown dalam pesan</string> + <string name="smp_servers_save">Simpan server</string> + <string name="smp_servers_add">Tambah server</string> + <string name="smp_servers_preset_add">Tambah server prasetel</string> + <string name="smp_servers_test_failed">Uji server gagal!</string> + <string name="smp_servers_test_server">Server uji</string> + <string name="smp_servers_test_servers">Server uji</string> + <string name="smp_servers_enter_manually">Masukkan server manual</string> + <string name="smp_servers_preset_server">Server Prasetel</string> + <string name="smp_servers_scan_qr">Pindai kode QR server</string> + <string name="smp_servers_test_some_failed">Beberapa server gagal dalam pengujian:</string> + <string name="smp_servers_your_server_address">Alamat server Anda</string> + <string name="smp_servers_delete_server">Hapus server</string> + <string name="saved_ICE_servers_will_be_removed">Server WebRTC ICE yang disimpan akan dihapus.</string> + <string name="your_ICE_servers">Server ICE Anda</string> + <string name="how_to_use_your_servers">Cara menggunakan server Anda</string> + <string name="save_servers_button">Simpan</string> + <string name="network_settings">Pengaturan jaringan lainnya</string> + <string name="error_saving_ICE_servers">Gagal simpan server ICE</string> + <string name="ensure_ICE_server_address_are_correct_format_and_unique">Pastikan alamat server WebRTC ICE dalam format yang benar, dipisahkan baris dan tidak terduplikasi.</string> + <string name="network_socks_toggle_use_socks_proxy">Gunakan proxy SOCKS</string> + <string name="network_socks_proxy_settings">Pengaturan proxy SOCKS</string> + <string name="callstate_starting">memulai…</string> + <string name="your_privacy">Privasi Anda</string> + <string name="full_backup">Cadangan data aplikasi</string> + <string name="group_members_2">%s dan %s</string> + <string name="color_mode_system">Sistem</string> + <string name="color_received_quote">Balasan diterima</string> + <string name="wallpaper_scale">Skala</string> + <string name="only_your_contact_can_send_voice">Hanya kontak yang dapat kirim pesan suara.</string> + <string name="v4_5_private_filenames">Nama berkas pribadi</string> + <string name="v5_1_better_messages">Pesan yang lebih baik</string> + <string name="compose_save_messages_n">Menyimpan %1$s pesan</string> + <string name="forward_files_failed_to_receive_desc">%1$d berkas gagal diunduh.</string> + <string name="join_group_button">Gabung</string> + <string name="icon_descr_cancel_file_preview">Batal pratinjau berkas</string> + <string name="icon_descr_file">Berkas</string> + <string name="voice_messages_not_allowed">Pesan suara tidak diizinkan</string> + <string name="delete_address__question">Hapus alamat?</string> + <string name="leave_group_question">Keluar dari grup?</string> + <string name="this_string_is_not_a_connection_link">Rangkaian ini bukan tautan koneksi!</string> + <string name="paste_the_link_you_received">Tempel tautan yang Anda terima</string> + <string name="share_with_contacts">Bagikan dengan kontak</string> + <string name="network_smp_proxy_fallback_allow">Ya</string> + <string name="customize_theme_title">Kustomisasi tema</string> + <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Anda bergabung dengan grup melalui tautan ini.</string> + <string name="v4_6_audio_video_calls_descr">Mendukung bluetooth dan peningkatan lainnya.</string> + <string name="migrate_from_device_archiving_database">Pengarsipan basis data</string> + <string name="v6_1_better_security_descr">Protokol SimpleX ditinjau oleh Trail of Bits.</string> + <string name="v5_1_better_messages_descr">- pesan suara hingga 5 menit.\n- kustom waktu pesan sementara.\n- riwayat edit.</string> + <string name="feature_enabled_for">Diaktifkan untuk</string> + <string name="ttl_d">%dh</string> + <string name="smp_servers_invalid_address">Alamat server tidak valid!</string> + <string name="xftp_servers_other">Server XFTP lainnya</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">Gunakan kredensial proksi yang berbeda untuk setiap profil.</string> + <string name="proxy_destination_error_failed_to_connect">Server penerusan %1$s gagal terhubung ke server tujuan %2$s. Coba lagi nanti.</string> + <string name="search_or_paste_simplex_link">Cari atau tempel tautan SimpleX</string> + <string name="cannot_share_message_alert_text">Preferensi obrolan yang dipilih melarang pesan ini.</string> + <string name="attach">Lampiran</string> + <string name="icon_descr_context">Ikon konteks</string> + <string name="icon_descr_cancel_image_preview">Batal pratinjau gambar</string> + <string name="callstatus_rejected">panggilan ditolak</string> + <string name="video_call_no_encryption">panggilan video (tidak dienkripsi e2e)</string> + <string name="alert_text_encryption_renegotiation_failed">Negosiasi ulang enkripsi gagal.</string> + <string name="integrity_msg_duplicate">gandakan pesan</string> + <string name="integrity_msg_bad_hash">hash pesan buruk</string> + <string name="start_chat_question">Mulai obrolan?</string> + <string name="v5_3_discover_join_groups">Jelajah dan gabung ke grup</string> + <string name="v5_8_private_routing">Perutean pesan pribadi 🚀</string> + <string name="v5_8_private_routing_descr">Lindungi alamat IP Anda dari relai pesan yang dipilih oleh kontak Anda.\nAktifkan di pengaturan *Jaringan & server*.</string> + <string name="paste_link">Tempel tautan</string> + <string name="share_this_1_time_link">Bagikan tautan undangan 1-kali</string> + <string name="paste_button">Tempel</string> + <string name="retry_verb">Coba lagi</string> + <string name="connect_via_link">Hubungkan melalui tautan</string> + <string name="the_text_you_pasted_is_not_a_link">Teks yang Anda tempel bukan tautan SimpleX.</string> + <string name="your_settings">Pengaturan Anda</string> + <string name="your_simplex_contact_address">Alamat SimpleX Anda</string> + <string name="your_chat_profiles">Profil obrolan Anda</string> + <string name="smp_servers_configured">Server SMP dikonfigurasi</string> + <string name="smp_servers_other">Server SMP lainnya</string> + <string name="smp_servers_use_server">Gunakan server</string> + <string name="xftp_servers_configured">Server XFTP dikonfigurasi</string> + <string name="smp_servers_check_address">Periksa alamat server dan coba lagi.</string> + <string name="subscription_percentage">Tampilkan persentase</string> + <string name="contribute">Kontribusi</string> + <string name="install_simplex_chat_for_terminal">Instal SimpleX Chat untuk terminal</string> + <string name="incoming_video_call">Panggilan video masuk</string> + <string name="rcv_group_event_member_added">mengundang %1$s</string> + <string name="group_member_role_owner">pemilik</string> + <string name="v5_1_self_destruct_passcode">Kode sandi hapus otomatis</string> + <string name="v5_2_favourites_filter">Temukan obrolan lebih cepat</string> + <string name="servers_info_detailed_statistics">Statistik terperinci</string> + <string name="large_file">Berkas besar!</string> + <string name="scan_paste_link">Pindai / Tempel tautan</string> + <string name="permissions_grant">Berikan izin untuk melakukan panggilan</string> + <string name="audio_call_no_encryption">panggilan suara (tidak dienkripsi e2e)</string> + <string name="v5_1_custom_themes">Tema kustom</string> + <string name="v5_3_discover_join_groups_descr">- terhubung ke layanan direktori (BETA)!\n- tanda terima pengiriman (hingga 20 anggota).\n- lebih cepat dan stabil.</string> + <string name="v5_3_simpler_incognito_mode">Mode samaran sederhana</string> + <string name="migrate_to_device_apply_onion">Terapkan</string> + <string name="reconnect">Hubung ulang</string> + <string name="acknowledgement_errors">Kesalahan pengenalan</string> + <string name="leave_group_button">Keluar</string> + <string name="conn_event_ratchet_sync_agreed">enkripsi disetujui</string> + <string name="both_you_and_your_contact_can_send_disappearing">Anda dan kontak Anda dapat mengirim pesan sementara.</string> + <string name="only_your_contact_can_send_disappearing">Hanya kontak Anda yang dapat mengirim pesan sementara.</string> + <string name="both_you_and_your_contact_can_send_voice">Anda dan kontak Anda dapat mengirim pesan suara.</string> + <string name="v5_0_app_passcode">Kode sandi aplikasi</string> + <string name="v5_0_app_passcode_descr">Atur sebagai ganti autentikasi sistem.</string> + <string name="v5_1_message_reactions_descr">Akhirnya, kita mendapatkannya! 🚀</string> + <string name="v5_0_polish_interface">Antarmuka Polandia</string> + <string name="v5_2_disappear_one_message">Membuat satu pesan dihapus</string> + <string name="v5_2_disappear_one_message_descr">Bahkan saat dimatikan dalam percakapan.</string> + <string name="v5_2_message_delivery_receipts_descr">Centang kedua yang terlewat! ✅</string> + <string name="v5_2_favourites_filter_descr">Filter obrolan belum dibaca dan favorit.</string> + <string name="v5_3_new_desktop_app_descr">Buat profil baru di aplikasi desktop. 💻</string> + <string name="v5_4_more_things_descr">- opsional memberi tahu kontak yang dihapus.\n- nama profil dengan spasi.\n- dan masih banyak lagi!</string> + <string name="v6_0_privacy_blur">Buramkan untuk privasi lebih baik.</string> + <string name="this_device">Perangkat ini</string> + <string name="servers_info_files_tab">Berkas</string> + <string name="network_type_ethernet">Kabel ethernet</string> + <string name="prohibit_message_deletion">Hapus pesan tidak bisa dibatalkan dilarang.</string> + <string name="prohibit_sending_files">Kirim berkas dan media dilarang.</string> + <string name="v5_3_encrypt_local_files_descr">Aplikasi mengenkripsi berkas lokal baru (kecuali video).</string> + <string name="v5_3_simpler_incognito_mode_descr">Aktifkan mode samaran saat menghubungkan.</string> + <string name="v5_5_message_delivery">Peningkatan pengiriman pesan</string> + <string name="v5_5_message_delivery_descr">Penggunaan baterai yang sedikit.</string> + <string name="v5_5_new_interface_languages">UI Hongaria dan Turki</string> + <string name="v5_6_quantum_resistant_encryption">Enkripsi quantum resistant</string> + <string name="app_check_for_updates_notice_title">Periksa pembaruan</string> + <string name="please_try_later">Silakan coba lagi nanti.</string> + <string name="private_routing_error">Kesalahan perutean pribadi</string> + <string name="add_address_to_your_profile">Tambah alamat ke profil Anda, sehingga kontak dapat membagikannya dengan orang lain. Pembaruan profil akan dikirim ke kontak Anda.</string> + <string name="keychain_allows_to_receive_ntfs">Android Keystore digunakan untuk simpan frasa sandi dengan aman setelah Anda memulai ulang aplikasi atau ubah frasa sandi - ini mungkin dapat menerima notifikasi.</string> + <string name="allow_accepting_calls_from_lock_screen">Aktifkan panggilan dari layar kunci melalui Pengaturan.</string> + <string name="alert_text_skipped_messages_it_can_happen_when">Hal ini dapat terjadi ketika:\n1. Pesan kedaluwarsa di klien pengirim setelah 2 hari atau di server setelah 30 hari.\n2. Dekripsi pesan gagal, karena Anda atau kontak Anda menggunakan cadangan basis data lama.\n3. Koneksi terganggu.</string> + <string name="theme_remove_image">Hapus gambar</string> + <string name="appearance_font_size">Ukuran huruf</string> + <string name="prohibit_direct_messages">Kirim pesan pribadi ke anggota dilarang.</string> + <string name="prohibit_sending_simplex_links">Kirim tautan SimpleX dilarang</string> + <string name="message_reactions_are_prohibited">Reaksi pesan dilarang.</string> + <string name="simplex_links_are_prohibited_in_group">Tautan SimpleX dilarang.</string> + <string name="network_session_mode_server">Server</string> + <string name="network_smp_proxy_mode_unprotected">Tak terlindungi</string> + <string name="network_session_mode_session">Sesi aplikasi</string> + <string name="network_session_mode_server_description">Kredensial SOCKS baru akan digunakan untuk setiap server.</string> + <string name="app_version_code">Build aplikasi: %s</string> + <string name="core_version">Versi inti: v%s</string> + <string name="network_smp_proxy_fallback_allow_protected">Ketika IP disembunyikan</string> + <string name="theme_colors_section_title">WARNA ANTARMUKA</string> + <string name="update_network_smp_proxy_fallback_question">Fallback perutean pesan</string> + <string name="update_network_smp_proxy_mode_question">Mode routing pesan</string> + <string name="network_smp_proxy_mode_private_routing">Routing pribadi</string> + <string name="network_smp_proxy_mode_never_description">JANGAN gunakan routing pribadi.</string> + <string name="network_smp_proxy_mode_unprotected_description">Gunakan routing pribadi dengan server yang tak dikenal ketika alamat IP tidak dilindungi.</string> + <string name="app_check_for_updates_stable">Stable</string> + <string name="app_check_for_updates_update_available">Pembaruan tersedia: %s</string> + <string name="app_check_for_updates_disabled">Dimatikan</string> + <string name="app_check_for_updates_button_skip">Lewati versi ini</string> + <string name="app_check_for_updates_download_completed_title">Pembaruan aplikasi diunduh</string> + <string name="create_address">Buat alamat</string> + <string name="developer_options">ID Basis Data dan Opsi Isolasi Transport.</string> + <string name="callstatus_missed">panggilan tak terjawab</string> + <string name="you_can_make_address_visible_via_settings">Anda dapat membuatnya terlihat oleh kontak SimpleX Anda melalui Pengaturan.</string> + <string name="invite_friends_short">Undang</string> + <string name="email_invite_body">Hai!\nHubungi saya melalui SimpleX Chat: %s</string> + <string name="confirm_password">Konfirmasi kata sandi</string> + <string name="callstatus_error">panggilan gagal</string> + <string name="callstate_waiting_for_answer">menunggu jawaban…</string> + <string name="callstate_received_answer">menerima jawaban…</string> + <string name="callstate_received_confirmation">menerima konfirmasi…</string> + <string name="migrate_from_another_device">Pindah dari perangkat lain</string> + <string name="permissions_open_settings">Buka pengaturan</string> + <string name="permissions_camera_and_record_audio">Kamera dan mikrofon</string> + <string name="call_on_lock_screen">Panggilan pada layar terkunci:</string> + <string name="audio_device_speaker">Speaker</string> + <string name="permissions_grant_in_settings">Izin dalam pengaturan</string> + <string name="next_generation_of_private_messaging">Perpesanan masa depan</string> + <string name="permissions_find_in_settings_and_grant">Temukan izin ini di pengaturan Android dan ubah secara manual.</string> + <string name="audio_device_earpiece">Earpiece</string> + <string name="no_call_on_lock_screen">Matikan</string> + <string name="your_ice_servers">Server ICE Anda</string> + <string name="webrtc_ice_servers">Server ICE WebRTC</string> + <string name="if_you_enter_self_destruct_code">Jika Anda memasukkan kode sandi hapus otomatis saat membuka aplikasi:</string> + <string name="settings_section_title_icon">IKON APLIKASI</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">Aplikasi akan meminta untuk mengonfirmasi unduhan dari server berkas yang tidak dikenal (kecuali .onion atau saat proxy SOCKS diaktifkan).</string> + <string name="message_reactions_prohibited_in_this_chat">Reaksi pesan dilarang dalam obrolan ini.</string> + <string name="migrate_from_device_to_another_device">Pindah ke perangkat lain</string> + <string name="call_desktop_permission_denied_title">Untuk melakukan panggilan, izinkan penggunaan mikrofon. Akhiri panggilan dan coba panggil lagi.</string> + <string name="call_desktop_permission_denied_chrome">Klik tombol info di dekat kolom alamat untuk mengizinkan penggunaan mikrofon.</string> + <string name="relay_server_protects_ip">Server relai melindungi alamat IP Anda, tetapi dapat mengamati durasi panggilan.</string> + <string name="call_desktop_permission_denied_safari">Buka Pengaturan Safari / Situs Web / Mikrofon, lalu pilih Izinkan untuk localhost.</string> + <string name="open_simplex_chat_to_accept_call">Buka SimpleX Chat untuk terima panggilan</string> + <string name="open_verb">Buka</string> + <string name="status_e2e_encrypted">terenkripsi e2e</string> + <string name="call_connection_peer_to_peer">peer-to-peer</string> + <string name="status_contact_has_no_e2e_encryption">kontak tidak memiliki enkripsi e2e</string> + <string name="call_connection_via_relay">via relai</string> + <string name="icon_descr_call_missed">Panggilan tak terjawab</string> + <string name="icon_descr_call_rejected">Panggilan ditolak</string> + <string name="alert_text_msg_bad_id">ID pesan berikutnya salah (kurang atau sama dengan yang sebelumnya).\nHal ini dapat terjadi karena beberapa bug atau ketika koneksi terganggu.</string> + <string name="settings_section_title_themes">TEMA</string> + <string name="settings_section_title_delivery_receipts">KIRIM TANDA TERIMA KIRIMAN KE</string> + <string name="alert_text_fragment_encryption_out_of_sync_old_database">Hal ini dapat terjadi ketika Anda atau koneksi Anda menggunakan cadangan basis data lama.</string> + <string name="keychain_is_storing_securely">Android Keystore digunakan untuk menyimpan frasa sandi dengan aman - memungkinkan layanan notifikasi berfungsi.</string> + <string name="remove_passphrase">Hapus</string> + <string name="encrypt_database">Enkripsi</string> + <string name="messages_section_title">Pesan</string> + <string name="group_member_role_author">pembuat</string> + <string name="rcv_group_event_invited_via_your_group_link">diundang melalui tautan grup Anda</string> + <string name="group_member_role_member">anggota</string> + <string name="group_member_status_left">keluar</string> + <string name="color_mode_dark">Gelap</string> + <string name="theme_dark">Gelap</string> + <string name="language_system">Sistem</string> + <string name="color_surface">Menu & peringatan</string> + <string name="color_title">Judul</string> + <string name="color_wallpaper_background">Latar wallpaper</string> + <string name="color_wallpaper_tint">Aksen wallpaper</string> + <string name="system_mode_toast">Mode sistem</string> + <string name="wallpaper_preview_hello_alice">Selamat siang!</string> + <string name="wallpaper_preview_hello_bob">Selamat pagi!</string> + <string name="wallpaper_scale_repeat">Ulangi</string> + <string name="wallpaper_scale_fill">Isi</string> + <string name="chat_preferences_yes">ya</string> + <string name="group_preferences">Preferensi grup</string> + <string name="message_reactions">Reaksi pesan</string> + <string name="feature_enabled_for_contact">diaktifkan untuk kontak</string> + <string name="both_you_and_your_contact_can_make_calls">Anda dan kontak dapat melakukan panggilan.</string> + <string name="enable_sending_recent_history">Kirim hingga 100 pesan terakhir untuk anggota baru.</string> + <string name="prohibit_sending_disappearing">Kirim pesan sementara dilarang.</string> + <string name="disable_sending_recent_history">Jangan perlihat pesan riwayat ke anggota baru.</string> + <string name="group_members_can_send_dms">Anggota dapat mengirim pesan pribadi.</string> + <string name="voice_messages_are_prohibited">Pesan suara dilarang.</string> + <string name="group_members_can_add_message_reactions">Anggota dapat memberi reaksi pesan.</string> + <string name="ttl_month">%d bulan</string> + <string name="feature_roles_owners">pemilik</string> + <string name="ttl_sec">%d dtk</string> + <string name="ttl_s">%dd</string> + <string name="ttl_min">%d mnt</string> + <string name="ttl_months">%d bulan</string> + <string name="ttl_m">%db</string> + <string name="ttl_mth">%dbln</string> + <string name="whats_new">Apa yang baru</string> + <string name="group_members_can_send_files">Anggota dapat kirim berkas dan media.</string> + <string name="files_are_prohibited_in_group">Berkas dan media dilarang.</string> + <string name="recent_history_is_not_sent_to_new_members">Riwayat pesan tidak dikirim ke anggota baru.</string> + <string name="v4_3_improved_privacy_and_security_desc">Sembunyikan layar aplikasi di aplikasi terbaru.</string> + <string name="v4_3_irreversible_message_deletion_desc">Kontak Anda dapat mengizinkan hapus semua pesan.</string> + <string name="v4_4_disappearing_messages_desc">Pesan terkirim akan dihapus setelah waktu yang ditentukan.</string> + <string name="v4_4_verify_connection_security">Verifikasi keamanan koneksi</string> + <string name="v4_5_multiple_chat_profiles_descr">Nama, avatar, dan isolasi transport yang berbeda.</string> + <string name="v4_5_message_draft">Draf pesan</string> + <string name="v4_6_hidden_chat_profiles_descr">Lindungi profil obrolan Anda dengan kata sandi!</string> + <string name="v4_6_chinese_spanish_interface_descr">Terima kasih kepada pengguna – berkontribusi melalui Weblate!</string> + <string name="v5_0_large_files_support_descr">Cepat dan tidak perlu menunggu pengirim online!</string> + <string name="v5_1_japanese_portuguese_interface">UI Jepang dan Portugis</string> + <string name="v5_5_private_notes">Catatan pribadi</string> + <string name="v5_3_encrypt_local_files">Enkripsi berkas & media tersimpan</string> + <string name="v5_5_private_notes_descr">Dengan berkas dan media terenkripsi.</string> + <string name="v5_4_link_mobile_desktop">Hubungkan aplikasi ponsel dan desktop! 🔗</string> + <string name="v5_2_message_delivery_receipts">Tanda terima kirim pesan!</string> + <string name="v5_4_incognito_groups_descr">Buat grup menggunakan profil acak.</string> + <string name="v5_4_block_group_members">Blokir anggota grup</string> + <string name="v5_7_call_sounds">Suara panggilan masuk</string> + <string name="v5_7_call_sounds_descr">Saat menghubungkan panggilan suara dan video.</string> + <string name="v5_5_simpler_connect_ui">Tempel tautan untuk terhubung!</string> + <string name="v5_5_join_group_conversation_descr">Riwayat terkini dan peningkatan bot direktori.</string> + <string name="v5_6_quantum_resistant_encryption_descr">Aktifkan dalam obrolan pribadi (BETA)!</string> + <string name="v5_6_app_data_migration_descr">Pindah ke perangkat lain melalui kode QR.</string> + <string name="v6_0_delete_many_messages_descr">Hapus hingga 20 pesan sekaligus.</string> + <string name="app_check_for_updates">Periksa pembaruan</string> + <string name="app_check_for_updates_button_download">Unduh %s (%s)</string> + <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> + <string name="network_smp_proxy_mode_unknown_description">Gunakan routing pribadi dengan server yang tak dikenal.</string> + <string name="v6_1_better_calls">Panggilan lebih baik</string> + <string name="connect_plan_connect_to_yourself">Terhubung ke diri sendiri?</string> + <string name="v6_1_delete_many_messages_descr">Hapus atau moderasi hingga 200 pesan.</string> + <string name="v6_1_forward_many_messages_descr">Teruskan hingga 20 pesan sekaligus.</string> + <string name="in_developing_title">Segera hadir!</string> + <string name="v6_1_better_calls_descr">Ganti suara dan video selama panggilan.</string> + <string name="v6_1_better_user_experience">Pengalaman pengguna lebih baik</string> + <string name="servers_info_detailed_statistics_sent_messages_total">Total terkirim</string> + <string name="servers_info_detailed_statistics_received_messages_header">Pesan diterima</string> + <string name="restart_chat_button">Mulai ulang obrolan</string> + <string name="network_proxy_auth">Otentikasi proxy</string> + <string name="network_proxy_random_credentials">Gunakan kredensial acak</string> + <string name="callstate_waiting_for_confirmation">menunggu konfirmasi…</string> + <string name="permissions_required">Berikan izin</string> + <string name="unable_to_open_browser_title">Gagal membuka peramban</string> + <string name="unable_to_open_browser_desc">Peramban web bawaan diperlukan untuk panggilan. Harap konfigurasikan peramban bawaan dalam sistem, dan bagikan informasi lebih lanjut dengan pengembang.</string> + <string name="receipts_contacts_enable_for_all">Aktifkan untuk semua</string> + <string name="receipts_groups_disable_for_all">Nonaktifkan untuk semua grup</string> + <string name="icon_descr_add_members">Undang anggota</string> + <string name="theme_system">Sistem</string> + <string name="theme_light">Terang</string> + <string name="theme_simplex">SimpleX</string> + <string name="files_and_media">Berkas dan media</string> + <string name="simplex_links">Tautan SimpleX</string> + <string name="recent_history">Perlihat riwayat</string> + <string name="audio_video_calls">Panggilan suara/video</string> + <string name="delete_after">Hapus setelah</string> + <string name="v5_8_message_delivery_descr">Dengan kurangi penggunaan baterai.</string> + <string name="v6_0_reachable_chat_toolbar">Toolbar obrolan mudah diakses</string> + <string name="v5_8_persian_ui">UI Persia</string> + <string name="connect_plan_open_group">Grup terbuka</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="servers_info_private_data_disclaimer">Mulai dari %s.\nSemua data bersifat pribadi di perangkat Anda.</string> + <string name="servers_info_transport_sessions_section_header">Sesi transport</string> + <string name="servers_info_detailed_statistics_received_total">Total diterima</string> + <string name="servers_info_detailed_statistics_receive_errors">Terima galat</string> + <string name="attempts_label">upaya</string> + <string name="acknowledged">Dikenal</string> + <string name="waiting_for_image">Menunggu gambar</string> + <string name="waiting_for_video">Menunggu video</string> + <string name="settings_section_title_device">PERANGKAT</string> + <string name="settings_section_title_chats">OBROLAN</string> + <string name="settings_section_title_files">BERKAS</string> + <string name="reset_all_hints">Reset semua petunjuk</string> + <string name="error_adding_members">Gagal menambah anggota</string> + <string name="error_joining_group">Gagal gabung ke grup</string> + <string name="sender_cancelled_file_transfer">Pengirim batalkan kirim berkas.</string> + <string name="cannot_receive_file">Tak bisa menerima berkas</string> + <string name="file_not_approved_descr">Tanpa Tor atau VPN, alamat IP Anda akan terlihat oleh relay XFTP ini:\n%1$s.</string> + <string name="error_receiving_file">Gagal menerima berkas</string> + <string name="error_creating_address">Gagal membuat alamat</string> + <string name="you_are_already_connected_to_vName_via_this_link">Anda sudah terhubung ke %1$s.</string> + <string name="invalid_connection_link">Tautan koneksi tidak valid</string> + <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Harap periksa apakah tautan yang digunakan benar atau minta kontak Anda untuk kirim tautan lain.</string> + <string name="error_accepting_contact_request">Gagal menerima permintaan kontak</string> + <string name="error_alert_title">Galat</string> + <string name="error_smp_test_certificate">Mungkin sidik jari sertifikat di alamat server salah</string> + <string name="error_setting_address">Gagal mengatur alamat</string> + <string name="error_deleting_user">Gagal hapus profil pengguna</string> + <string name="smp_server_test_delete_queue">Hapus antrian</string> + <string name="smp_server_test_create_file">Buat berkas</string> + <string name="smp_server_test_delete_file">Hapus berkas</string> + <string name="error_updating_user_privacy">Gagal perbarui privasi pengguna</string> + <string name="possible_slow_function_title">Fungsi lambat</string> + <string name="icon_descr_instant_notifications">Notifikasi instan</string> + <string name="turn_off_battery_optimization"><![CDATA[<b>Izinkan</b> pada dialog berikutnya untuk menerima pemberitahuan secara instan.]]></string> + <string name="turning_off_service_and_periodic">Optimalisasi baterai aktif, mematikan layanan latar belakang dan permintaan pesan baru secara berkala. Anda dapat aktifkan kembali di pengaturan.</string> + <string name="turn_off_system_restriction_button">Buka pengaturan aplikasi</string> + <string name="periodic_notifications_disabled">Notifikasi berkala dinonaktifkan!</string> + <string name="disable_notifications_button">Matikan notifikasi</string> + <string name="system_restricted_background_in_call_warn"><![CDATA[Untuk lakukan panggilan di latar belakang, pilih <b>Penggunaan baterai aplikasi</b> / <b>Tidak dibatasi</b> di pengaturan aplikasi.]]></string> + <string name="enter_passphrase_notification_title">Frasa sandi diperlukan</string> + <string name="enter_passphrase_notification_desc">Untuk menerima notifikasi, mohon masukkan frasa sandi basis data</string> + <string name="call_service_notification_end_call">Akhiri</string> + <string name="database_initialization_error_title">Tidak dapat inisialisasi basis data</string> + <string name="database_initialization_error_desc">Basis data tidak berfungsi dengan benar. pelajari lebih lanjut</string> + <string name="hide_notification">Tutup</string> + <string name="notifications_mode_periodic">Dimulai secara berkala</string> + <string name="notifications_mode_off">Berjalan saat aplikasi terbuka</string> + <string name="notifications_mode_off_desc">Aplikasi hanya menerima notifikasi saat sedang berjalan, tidak ada layanan latar belakang yang dimulai</string> + <string name="notifications_mode_periodic_desc">Memeriksa pesan baru setiap 10 menit hingga 1 menit</string> + <string name="notification_preview_mode_hidden">Tersembunyi</string> + <string name="notification_preview_mode_message_desc">Tampilkan kontak dan pesan</string> + <string name="notification_preview_somebody">Kontak tersembunyi:</string> + <string name="la_notice_turn_on">Nyalakan</string> + <string name="la_lock_mode">Mode Kunci SimpleX</string> + <string name="auth_simplex_lock_turned_on">Kunci SimpleX diaktifkan</string> + <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Anda akan diminta untuk lakukan autentikasi saat mulai atau lanjutkan aplikasi setelah 30 detik di latar belakang.</string> + <string name="app_passcode_replaced_with_self_destruct">Kode sandi aplikasi diganti dengan kode sandi hapus otomatis.</string> + <string name="receipts_section_description_1">Mereka dapat ditimpa dalam pengaturan kontak dan grup.</string> + <string name="receipts_contacts_enable_keep_overrides">Aktifkan (tetap ditimpa)</string> + <string name="receipts_contacts_disable_keep_overrides">Matikan (tetap ditimpa)</string> + <string name="failed_to_parse_chats_title">Gagal memuat obrolan</string> + <string name="error_deleting_contact">Gagal hapus kontak</string> + <string name="error_loading_smp_servers">Gagal memuat server SMP</string> + <string name="failed_to_parse_chat_title">Gagal memuat obrolan</string> + <string name="failed_to_create_user_invalid_desc">Nama tampilan ini tidak valid. Silakan pilih nama lain.</string> + <string name="sender_may_have_deleted_the_connection_request">Pengirim mungkin telah hapus permintaan koneksi.</string> + <string name="error_smp_test_server_auth">Server perlu otorisasi untuk membuat antrian, periksa kata sandi</string> + <string name="error_deleting_contact_request">Gagal menghapus permintaan kontak</string> + <string name="error_deleting_pending_contact_connection">Gagal menghapus koneksi kontak tertunda</string> + <string name="error_changing_address">Gagal mengubah alamat</string> + <string name="error_aborting_address_change">Gagal batalkan perubahan alamat</string> + <string name="ntf_channel_messages">Pesan SimpleX Chat</string> + <string name="system_restricted_background_warn"><![CDATA[Untuk aktifkan notifikasi, pilih <b>Penggunaan baterai aplikasi</b> / <b>Tidak dibatasi</b> di pengaturan aplikasi.]]></string> + <string name="system_restricted_background_in_call_desc">Aplikasi mungkin ditutup setelah 1 menit di latar belakang.</string> + <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Untuk melindungi informasi Anda, aktifkan Kunci SimpleX.\nAnda akan diminta untuk menyelesaikan autentikasi sebelum fitur ini aktif.</string> + <string name="la_minutes">%d menit</string> + <string name="error_showing_desktop_notification">Gagal menampilkan notifikasi, hubungi pengembang.</string> + <string name="receipts_groups_title_enable">Aktifkan tanda terima untuk grup?</string> + <string name="empty_chat_profile_is_created">Profil obrolan kosong dengan nama yang disediakan dibuat, dan aplikasi terbuka seperti biasa.</string> + <string name="if_you_enter_passcode_data_removed">Jika Anda memasukkan kode sandi saat membuka aplikasi, semua data aplikasi akan dihapus secara permanen!</string> + <string name="receipts_section_description">Pengaturan ini untuk profil Anda saat ini</string> + <string name="receipts_contacts_override_enabled">Kirim tanda terima diaktifkan untuk %d kontak</string> + <string name="receipts_contacts_override_disabled">Kirim tanda terima dimatikan untuk %d kontak</string> + <string name="error_loading_xftp_servers">Gagal memuat server XFTP</string> + <string name="error_setting_network_config">Gagal memperbarui konfigurasi jaringan</string> + <string name="contact_developers">Mohon perbarui aplikasi dan hubungi pengembang.</string> + <string name="failed_to_create_user_duplicate_desc">Anda sudah memiliki nama tampilan profil obrolan yang sama. Silakan pilih nama lain.</string> + <string name="network_error_desc">Periksa koneksi jaringan Anda dengan %1$s dan coba lagi.</string> + <string name="error_loading_details">Gagal memuat detail</string> + <string name="error_deleting_group">Gagal menghapus grup</string> + <string name="error_deleting_note_folder">Gagal hapus catatan pribadi</string> + <string name="smp_server_test_disconnect">Putuskan</string> + <string name="smp_server_test_secure_queue">Amankan antrian</string> + <string name="smp_server_test_download_file">Unduh berkas</string> + <string name="smp_server_test_compare_file">Bandingkan berkas</string> + <string name="service_notifications">Notifikasi instan!</string> + <string name="service_notifications_disabled">Notifikasi instan dimatikan!</string> + <string name="periodic_notifications">Notifikasi berkala</string> + <string name="system_restricted_background_desc">SimpleX tidak dapat berjalan di latar belakang. Anda hanya menerima notifikasi saat aplikasi berjalan.</string> + <string name="call_service_notification_video_call">Panggilan video</string> + <string name="simplex_service_notification_text">Menerima pesan…</string> + <string name="settings_notification_preview_mode_title">Lihat pratinjau</string> + <string name="notification_preview_mode_message">Teks pesan</string> + <string name="notifications_mode_service_desc">Layanan latar belakang selalu berjalan – notifikasi selalu menampilkan pesan yang tersedia.</string> + <string name="notification_preview_mode_contact_desc">Tampilkan hanya kontak</string> + <string name="notification_display_mode_hidden_desc">Sembunyikan kontak dan pesan</string> + <string name="la_notice_title_simplex_lock">Kunci SimpleX</string> + <string name="la_lock_mode_system">Autentikasi sistem</string> + <string name="la_auth_failed">Autentikasi gagal</string> + <string name="la_lock_mode_passcode">Entri kode sandi</string> + <string name="la_could_not_be_verified">Anda tidak dapat diverifikasi; silakan coba lagi.</string> + <string name="smp_proxy_error_broker_version">Versi server penerusan tidak kompatibel dengan pengaturan jaringan: %1$s.</string> + <string name="proxy_destination_error_broker_version">Versi server tujuan %1$s tidak kompatibel dengan server penerusan %2$s.</string> + <string name="connection_error_auth">Kesalahan koneksi (AUTH)</string> + <string name="smp_proxy_error_connecting">Gagal menghubungkan ke server penerusan %1$s. Coba lagi nanti.</string> + <string name="smp_proxy_error_broker_host">Alamat server penerusan tidak kompatibel dengan pengaturan jaringan: %1$s.</string> + <string name="smp_server_test_upload_file">Unggah berkas</string> + <string name="possible_slow_function_desc">Eksekusi fungsi memakan waktu terlalu lama: %1$d detik: %2$s</string> + <string name="ntf_channel_calls">Panggilan SimpleX Chat</string> + <string name="network_error_broker_host_desc">Alamat server tidak kompatibel dengan pengaturan jaringan: %1$s.</string> + <string name="error_smp_test_failed_at_step">Uji gagal pada langkah %s.</string> + <string name="la_seconds">%d detik</string> + <string name="la_immediately">Segera</string> + <string name="receipts_groups_title_disable">Matikan tanda terima untuk grup?</string> + <string name="receipts_groups_override_enabled">Kirim tanda terima diaktifkan untuk %d grup</string> + <string name="receipts_groups_override_disabled">Kirim tanda terima dimatikan untuk %d grup</string> + <string name="receipts_groups_enable_keep_overrides">Aktifkan (grup tetap ditimpa)</string> + <string name="receipts_groups_disable_keep_overrides">Matikan (grup tetap ditimpa)</string> + <string name="receipts_contacts_disable_for_all">Matikan untuk semua</string> + <string name="receipts_groups_enable_for_all">Aktifkan untuk semua grup</string> + <string name="la_authenticate">Autentikasi</string> + <string name="network_error_broker_version_desc">Versi server tidak kompatibel dengan aplikasi Anda: %1$s.</string> + <string name="file_not_approved_title">Server tak dikenal!</string> + <string name="connection_error_auth_desc">Kecuali kontak Anda hapus koneksi atau tautan ini sudah digunakan, mungkin ini adalah bug - harap laporkan.\nUntuk terhubung, harap minta kontak Anda untuk buat tautan koneksi lain dan periksa apakah Anda memiliki koneksi jaringan stabil.</string> + <string name="error_synchronizing_connection">Gagal sinkronkan koneksi</string> + <string name="error_xftp_test_server_auth">Server perlu otorisasi untuk mengunggah, periksa kata sandi</string> + <string name="smp_server_test_create_queue">Buat antrian</string> + <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Dapat dimatikan melalui pengaturan</b> – notifikasi akan tetap ditampilkan saat aplikasi berjalan.]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Untuk tingkatkan privasi, <b>SimpleX berjalan di latar belakang</b> alih-alih gunakan notifikasi push.]]></string> + <string name="periodic_notifications_desc">Aplikasi terima pesan baru secara berkala — aplikasi ini memakai beberapa persen baterai per hari. Aplikasi ini tidak gunakan notifikasi push — data dari perangkat tidak dikirim ke server.</string> + <string name="simplex_service_notification_title">Layanan SimpleX Chat</string> + <string name="notification_preview_mode_contact">Nama kontak</string> + <string name="la_enter_app_passcode">Masukkan Kode Sandi</string> + <string name="la_current_app_passcode">Kode Sandi Saat Ini</string> + <string name="la_change_app_passcode">Ubah kode sandi</string> + <string name="la_please_remember_to_store_password">Harap diingat dan simpan dengan aman - tidak ada cara untuk pulihkan kata sandi yang hilang!</string> + <string name="info_row_debug_delivery">Kiriman debug</string> + <string name="remove_member_confirmation">Hapus</string> + <string name="share_text_created_at">Dibuat pada: %s</string> + <string name="member_info_member_blocked">diblokir</string> + <string name="switch_verb">Ganti</string> + <string name="unblock_member_confirmation">Buka blokir</string> + <string name="unblock_for_all_question">Buka blokir anggota untuk semua?</string> + <string name="unblock_for_all">Buka untuk semua</string> + <string name="member_blocked_by_admin">Diblokir oleh admin</string> + <string name="member_info_section_title_member">ANGGOTA</string> + <string name="remove_member_button">Hapus anggota</string> + <string name="share_text_message_status">Status pesan: %s</string> + <string name="share_text_file_status">Status berkas: %s</string> + <string name="info_row_group">Grup</string> + <string name="member_info_member_disabled">dimatikan</string> + <string name="unblock_member_question">Buka blokir anggota?</string> + <string name="unblock_member_button">Buka blokir anggota</string> + <string name="member_info_member_inactive">tidak aktif</string> + <string name="button_remove_member_question">Hapus anggota?</string> + <string name="button_remove_member">Hapus anggota</string> + <string name="saved_message_title">Pesan tersimpan</string> + <string name="cant_call_member_alert_title">Tak dapat memanggil anggota grup</string> + <string name="operators_conditions_accepted_for"><![CDATA[Ketentuan diterima untuk operator: <b>%s</b>.]]></string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[Ketentuan akan diterima untuk operator: <b>%s</b>.]]></string> + <string name="operator">Operator</string> + <string name="operator_use_operator_toggle_description">Gunakan server</string> + <string name="use_servers_of_operator_x">Gunakan %s</string> + <string name="cant_send_message_to_member_alert_title">Tak dapat kirim pesan ke anggota grup</string> + <string name="group_welcome_title">Pesan sambutan</string> + <string name="save_welcome_message_question">Simpan pesan sambutan?</string> + <string name="fix_connection_not_supported_by_group_member">Perbaikan tidak didukung oleh anggota grup</string> + <string name="create_secret_group_title">Buat grup rahasia</string> + <string name="renegotiate_encryption">Negosiasi ulang enkripsi</string> + <string name="group_display_name_field">Masukkan nama grup:</string> + <string name="group_welcome_preview">Pratinjau</string> + <string name="cant_call_member_send_message_alert_text">Kirim pesan untuk aktifkan panggilan.</string> + <string name="cant_call_contact_deleted_alert_text">Kontak dihapus.</string> + <string name="cant_call_contact_alert_title">Tak dapat memanggil kontak</string> + <string name="fix_connection_confirm">Perbaiki</string> + <string name="group_is_decentralized">Sepenuhnya terdesentralisasi – hanya terlihat oleh anggota.</string> + <string name="fix_connection">Perbaiki koneksi</string> + <string name="fix_connection_question">Perbaiki koneksi?</string> + <string name="operator_review_conditions">Tinjau ketentuan</string> + <string name="network_preset_servers_title">Server prasetel</string> + <string name="operator_conditions_accepted">Ketentuan diterima</string> + <string name="operator_conditions_accepted_for_enabled_operators_on">Ketentuan akan otomatis diterima untuk operator yang diaktifkan pada: %s.</string> + <string name="you_need_to_allow_calls">Anda perlu izinkan kontak Anda agar dapat memanggilnya.</string> + <string name="message_too_large">Pesan terlalu besar</string> + <string name="enter_welcome_message">Masukkan pesan sambutan…</string> + <string name="save_and_update_group_profile">Simpan dan perbarui profil grup</string> + <string name="welcome_message_is_too_long">Pesan sambutan terlalu panjang</string> + <string name="fix_connection_not_supported_by_contact">Perbaikan tidak didukung oleh kontak</string> + <string name="info_row_chat">Obrolan</string> + <string name="accept_conditions">Terima kondisi</string> + <string name="conn_stats_section_title_servers">SERVER</string> + <string name="create_group_button">Buat grup</string> + <string name="group_full_name_field">Nama lengkap grup:</string> + <string name="save_group_profile">Simpan profil grup</string> + <string name="operator_website">Peramban</string> + <string name="your_servers">Server Anda</string> + <string name="error_saving_group_profile">Gagal simpan profil grup</string> + <string name="operator_servers_title">%s server</string> + <string name="operator_info_title">Operator jaringan</string> + <string name="operator_conditions_accepted_on">Ketentuan diterima pada: %s.</string> + <string name="operator_conditions_will_be_accepted_on">Ketentuan akan diterima pada: %s.</string> + <string name="info_row_connection">Koneksi</string> + <string name="role_in_group">Rol</string> + <string name="change_role">Ganti rol</string> + <string name="group_main_profile_sent">Profil obrolan Anda akan dikirim ke anggota grup</string> + <string name="chat_main_profile_sent">Profil obrolan Anda akan dikirim ke anggota obrolan</string> + <string name="group_profile_is_stored_on_members_devices">Profil grup disimpan di perangkat anggota, bukan di server.</string> + <string name="conn_level_desc_direct">langsung</string> + <string name="sending_via">Kirim via</string> + <string name="receiving_via">Terima via</string> + <string name="icon_descr_contact_checked">Kontak diperiksa</string> + <string name="invite_prohibited">Tak dapat undang kontak!</string> + <string name="snd_conn_event_ratchet_sync_agreed">enkripsi disetujui untuk %s</string> + <string name="group_member_status_creator">pembuat</string> + <string name="group_member_status_connecting">menghubungkan</string> + <string name="delete_group_question">Hapus grup?</string> + <string name="delete_group_for_all_members_cannot_undo_warning">Grup akan dihapus untuk semua anggota - ini tidak dapat dibatalkan!</string> + <string name="create_group_link">Buat tautan grup</string> + <string name="group_member_status_introduced">menghubungkan (diperkenalkan)</string> + <string name="rcv_conn_event_verification_code_reset">kode keamanan berubah</string> + <string name="num_contacts_selected">%d kontak dipilih</string> + <string name="group_link">Tautan grup</string> + <string name="past_member_vName">Anggota lama %1$s</string> + <string name="conn_event_disabled_pq">enkripsi end-to-end standar</string> + <string name="conn_event_enabled_pq">enkripsi e2e quantum resistant</string> + <string name="group_member_status_intro_invitation">menghubungkan (undangan perkenalan)</string> + <string name="group_member_status_unknown_short">tidak dikenal</string> + <string name="button_delete_chat">Hapus obrolan</string> + <string name="button_add_friends">Tambah teman</string> + <string name="button_add_team_members">Tambah anggota tim</string> + <string name="button_leave_group">Keluar grup</string> + <string name="group_member_status_announced">menghubungkan (diumumkan)</string> + <string name="group_member_status_accepted">menghubungkan (diterima)</string> + <string name="button_delete_group">Hapus grup</string> + <string name="delete_group_for_self_cannot_undo_warning">Grup akan dihapus untuk Anda - ini tidak dapat dibatalkan!</string> + <string name="delete_chat_question">Hapus obrolan?</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">Obrolan akan dihapus untuk semua anggota - ini tidak dapat dibatalkan!</string> + <string name="delete_chat_for_self_cannot_undo_warning">Obrolan akan dihapus untuk Anda - ini tidak dapat dibatalkan!</string> + <string name="button_leave_chat">Tinggalkan obrolan</string> + <string name="button_edit_group_profile">Edit profil grup</string> + <string name="button_welcome_message">Pesan sambutan</string> + <string name="button_add_members">Undang anggota</string> + <string name="group_info_member_you">anda: %1$s</string> + <string name="invite_prohibited_description">Anda mencoba mengundang kontak yang telah Anda bagikan profil samaran ke grup tempat Anda menggunakan profil utama</string> + <string name="invite_to_group_button">Undang ke grup</string> + <string name="invite_to_chat_button">Undang ke obrolan</string> + <string name="select_contacts">Pilih kontak</string> + <string name="skip_inviting_button">Lewati undang anggota</string> + <string name="address_or_1_time_link">Alamat atau tautan 1-kali?</string> + <string name="developer_options_section">Opsi pengembang</string> + <string name="onboarding_choose_server_operators">Operator server</string> + <string name="onboarding_network_operators">Operator jaringan</string> + <string name="onboarding_network_operators_app_will_use_different_operators">Aplikasi ini melindungi privasi Anda dengan gunakan operator yang berbeda setiap percakapan.</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">Ketentuan akan diterima untuk operator yang diaktifkan setelah 30 hari.</string> + <string name="settings_developer_tools">Alat pengembang</string> + <string name="settings_section_title_chat_colors">Warna obrolan</string> + <string name="rcv_group_event_member_created_contact">terhubung langsung</string> + <string name="remote_ctrl_error_bad_state">Koneksi ke desktop dalam kondisi buruk</string> + <string name="confirm_delete_contact_question">Konfirmasi hapus kontak?</string> + <string name="send_disappearing_message_custom_time">Waktu kustom</string> + <string name="choose_file_title">Pilih berkas</string> + <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Anda dapat <font color="#0088ff">terhubung ke pengembang SimpleX Chat untuk mengajukan pertanyaan dan menerima pembaruan</font>.]]></string> + <string name="clear_chat_question">Hapus obrolan?</string> + <string name="network_proxy_auth_mode_no_auth">Jangan gunakan kredensial dengan proxy.</string> + <string name="disable_onion_hosts_when_not_supported"><![CDATA[Atur <i>Gunakan host .onion</i> ke Tidak jika proxy SOCKS tidak mendukung.]]></string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Harap diperhatikan</b>: relay pesan dan berkas terhubung melalui proxy SOCKS. Panggilan dan pengiriman pratinjau tautan menggunakan koneksi langsung.]]></string> + <string name="network_smp_proxy_fallback_prohibit_description">JANGAN mengirim pesan secara langsung, meskipun server Anda atau server tujuan tidak mendukung routing pribadi.</string> + <string name="display_name_cannot_contain_whitespace">Nama tampilan tidak boleh terdapat spasi.</string> + <string name="you_can_change_it_later">Frasa sandi acak disimpan dalam pengaturan sebagai teks biasa.\nAnda dapat mengubahnya nanti.</string> + <string name="setup_database_passphrase">Pasang frasa sandi basis data</string> + <string name="database_will_be_encrypted">Basis data akan dienkripsi.</string> + <string name="database_upgrade">Peningkatan basis data</string> + <string name="confirm_database_upgrades">Konfirmasi peningkatan basis data</string> + <string name="downgrade_and_open_chat">Turunkan dan buka obrolan</string> + <string name="rcv_group_event_changed_member_role">mengubah hak %s menjadi %s</string> + <string name="share_text_deleted_at">Dihapus pada: %s</string> + <string name="delete_profile">Hapus profil</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Ini adalah tautan untuk grup <b>%1$s</b>!]]></string> + <string name="migrate_from_device_cancel_migration">Batal pindah</string> + <string name="chunks_uploaded">Potongan diunggah</string> + <string name="info_view_connect_button">hubungkan</string> + <string name="conversation_deleted">Percakapan dihapus!</string> + <string name="disappearing_message">Pesan sementara</string> + <string name="clear_note_folder_question">Hapus catatan pribadi?</string> + <string name="chat_is_stopped">Obrolan dihentikan</string> + <string name="database_passphrase">Frasa sandi basis data</string> + <string name="confirm_new_passphrase">Konfirmasi frasa sandi baru…</string> + <string name="change_database_passphrase_question">Ubah frasa sandi basis data?</string> + <string name="cannot_access_keychain">Tidak dapat mengakses Keystore untuk menyimpan kata sandi basis data</string> + <string name="database_downgrade">Penurunan basis data</string> + <string name="chat_is_stopped_indication">Obrolan dihentikan</string> + <string name="button_create_group_link">Buat tautan</string> + <string name="info_row_deleted_at">Dihapus di</string> + <string name="direct_messages_are_prohibited_in_chat">Pesan pribadi antar anggota dilarang dalam obrolan ini.</string> + <string name="delivery_receipts_title">Tanda terima pengirim!</string> + <string name="this_device_version"><![CDATA[<i>(perangkat ini v%s)</i>]]></string> + <string name="devices">Perangkat</string> + <string name="remote_ctrl_was_disconnected_title">Koneksi terputus</string> + <string name="copy_error">Gagal salin</string> + <string name="discover_on_network">Temukan melalui jaringan lokal</string> + <string name="remote_ctrl_error_bad_invitation">Kode undangan desktop salah</string> + <string name="remote_ctrl_error_disconnected">Desktop terputus</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Anda sudah terhubung ke <b>%1$s</b>.]]></string> + <string name="migrate_to_device_confirm_network_settings">Konfirmasi pengaturan jaringan</string> + <string name="chunks_deleted">Potongan dihapus</string> + <string name="chunks_downloaded">Potongan diunduh</string> + <string name="downloaded_files">Berkas terunduh</string> + <string name="download_errors">Gagal unduh</string> + <string name="delete_files_and_media_all">Hapus semua berkas</string> + <string name="delete_files_and_media_question">Hapus berkas dan media?</string> + <string name="total_files_count_and_size">%d berkas dengan total ukuran %s</string> + <string name="chat_database_exported_continue">Lanjutkan</string> + <string name="rcv_direct_event_contact_deleted">kontak dihapus</string> + <string name="alert_title_cant_invite_contacts">Tak dapat undang kontak!</string> + <string name="info_row_disappears_at">Terhapus pada</string> + <string name="share_text_disappears_at">Terhapus pada: %s</string> + <string name="item_info_current">(saat ini)</string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[Ketentuan yang sama akan berlaku untuk operator <b>%s</b>.]]></string> + <string name="appearance_bars_blur_radius">Buram</string> + <string name="appearance_in_app_bars_alpha">Transparansi</string> + <string name="appearance_app_toolbars">Bilah alat aplikasi</string> + <string name="v6_0_connection_servers_status_descr">Status koneksi dan server.</string> + <string name="remote_ctrl_error_bad_version">Desktop memiliki versi tidak didukung. Pastikan Anda gunakan versi yang sama di kedua perangkat</string> + <string name="migrate_to_device_chat_migrated">Obrolan dipindahkan!</string> + <string name="migrate_from_device_delete_database_from_device">Hapus basis data dari perangkat ini</string> + <string name="delete_files_and_media_for_all_users">Hapus berkas untuk semua profil obrolan</string> + <string name="info_row_created_at">Dibuat di</string> + <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Terbaik untuk baterai</b>. Anda akan menerima notifikasi saat aplikasi sedang berjalan (TANPA layanan latar belakang).]]></string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Baik untuk baterai</b>. Aplikasi memeriksa pesan setiap 10 menit. Anda mungkin melewatkan panggilan atau pesan penting.]]></string> + <string name="settings_section_title_chat_theme">Tema obrolan</string> + <string name="chat_database_section">BASIS DATA OBROLAN</string> + <string name="set_password_to_export_desc">Basis data dienkripsi menggunakan frasa sandi acak. Harap ubah frasa sandi sebelum mengekspor.</string> + <string name="chat_database_exported_title">Basis data obrolan diekspor</string> + <string name="current_passphrase">Frasa sandi saat ini…</string> + <string name="impossible_to_recover_passphrase"><![CDATA[<b>Harap diperhatikan</b>: Anda TIDAK akan dapat pulihkan atau ubah frasa sandi jika hilang.]]></string> + <string name="encrypted_with_random_passphrase">Basis data dienkripsi menggunakan frasa sandi acak, Anda dapat mengubahnya.</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Basis data akan dienkripsi dan frasa sandi disimpan dalam pengaturan.</string> + <string name="mtr_error_no_down_migration">versi database lebih baru daripada aplikasi, tetapi tidak ada penurunan migrasi untuk: %s</string> + <string name="rcv_group_events_count">%d acara grup</string> + <string name="rcv_conn_event_switch_queue_phase_completed">mengubah alamat untuk Anda</string> + <string name="info_row_database_id">ID basis data</string> + <string name="cant_call_contact_connecting_wait_alert_text">Menghubungkan ke kontak, harap tunggu atau periksa nanti!</string> + <string name="users_delete_question">Hapus profil obrolan?</string> + <string name="deletion_errors">Gagal hapus</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Perangkat Xiaomi</b>: harap aktifkan Autostart di pengaturan sistem agar notifikasi berfungsi.]]></string> + <string name="clear_chat_button">Hapus obrolan</string> + <string name="connect_via_link_or_qr">Hubungkan via tautan / kode QR</string> + <string name="connect__a_new_random_profile_will_be_shared">Profil acak baru akan dibagikan.</string> + <string name="connection_request_sent">Permintaan koneksi terkirim!</string> + <string name="connection_security">Keamanan koneksi</string> + <string name="correct_name_to">Nama telah benar untuk %s?</string> + <string name="onboarding_network_operators_conditions_you_can_configure">Anda dapat konfigurasi operator di pengaturan Jaringan dan server.</string> + <string name="database_error">Kesalahan basis data</string> + <string name="passphrase_is_different">Frasa sandi basis data berbeda dengan yang disimpan di Keystore.</string> + <string name="change_member_role_question">Ubah hak grup?</string> + <string name="operators_conditions_will_also_apply"><![CDATA[Ketentuan ini juga berlaku untuk: <b>%s</b>.]]></string> + <string name="operator_conditions_of_use">Ketentuan Penggunaan</string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Ketentuan akan diterima untuk operator: <b>%s</b>.]]></string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[Untuk gunakan server <b>%s</b>, terima ketentuan penggunaan.]]></string> + <string name="operator_added_message_servers">Menambahkan server pesan</string> + <string name="delete_chat_profile">Hapus profil obrolan</string> + <string name="v6_2_business_chats">Obrolan bisnis</string> + <string name="remote_ctrl_disconnected_with_reason">Terputus dikarenakan: %s</string> + <string name="remote_host_disconnected_from"><![CDATA[Terputus dari ponsel <b>%s</b> dengan alasan: %s]]></string> + <string name="remote_ctrl_error_busy">Desktop sedang sibuk</string> + <string name="rcv_group_event_group_deleted">grup dihapus</string> + <string name="profile_update_event_contact_name_changed">kontak %1$s diubah menjadi %2$s</string> + <string name="calls_prohibited_alert_title">Panggilan dilarang!</string> + <string name="operator_conditions_accepted_for_some"><![CDATA[Ketentuan sudah diterima untuk operator berikut: <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Ketentuan yang sama akan berlaku untuk operator: <b>%s</b>.]]></string> + <string name="operator_conditions_failed_to_load">Teks ketentuan saat ini tidak dapat dimuat, Anda dapat meninjau ketentuan via tautan ini:</string> + <string name="direct_messages_are_prohibited">Pesan pribadi antar anggota dilarang.</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Pilih <i>Pindah dari perangkat lain</i> pada perangkat baru dan pindai kode QR.]]></string> + <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Kontak dan semua pesan akan dihapus - ini tidak dapat dibatalkan!</string> + <string name="delete_contact_cannot_undo_warning">Kontak akan dihapus - ini tidak dapat dibatalkan!</string> + <string name="delete_contact_question">Hapus kontak?</string> + <string name="contact_deleted">Kontak dihapus!</string> + <string name="delete_without_notification">Hapus tanpa notifikasi</string> + <string name="switch_receiving_address_question">Ubah alamat penerima?</string> + <string name="icon_descr_server_status_disconnected">Terputus</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Tambahkan kontak</b>: untuk buat tautan undangan baru, atau terhubung via tautan yang Anda terima.]]></string> + <string name="delete_pending_connection__question">Hapus koneksi yang tertunda?</string> + <string name="alert_title_contact_connection_pending">Kontak belum terhubung!</string> + <string name="delete_address">Hapus alamat</string> + <string name="delete_database">Hapus basis data</string> + <string name="chat_database_deleted">Basis data obrolan dihapus</string> + <string name="chat_database_imported">Basis data obrolan diimpor</string> + <string name="send_receipts_disabled">Dimatikan</string> + <string name="v6_0_connection_servers_status">Kontrol jaringan Anda</string> + <string name="connecting_to_desktop">Menghubungkan ke desktop</string> + <string name="desktop_app_version_is_incompatible">Versi aplikasi desktop %s tidak kompatibel dengan aplikasi ini.</string> + <string name="remote_host_error_bad_state"><![CDATA[Koneksi ponsel <b>%s</b> dalam kondisi buruk]]></string> + <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Autentikasi perangkat tidak diaktifkan. Anda dapat aktifkan SimpleX Lock via Pengaturan, setelah mengaktifkan autentikasi perangkat.</string> + <string name="auth_unavailable">Autentikasi tidak tersedia</string> + <string name="auth_confirm_credential">Konfirmasi kredensial Anda</string> + <string name="auth_device_authentication_is_disabled_turning_off">Autentikasi perangkat mati. Matikan SimpleX Lock.</string> + <string name="auth_disable_simplex_lock">Matikan Kunci SimpleX</string> + <string name="snd_error_relay">Kesalahan server tujuan: %1$s</string> + <string name="delete_and_notify_contact">Hapus dan beritahu kontak</string> + <string name="button_delete_contact">Hapus kontak</string> + <string name="icon_descr_cancel_live_message">Batal pesan langsung</string> + <string name="confirm_verb">Konfirmasi</string> + <string name="create_group">Buat grup rahasia</string> + <string name="camera_not_available">Kamera tidak tersedia</string> + <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 desktop: pindai kode QR yang ditampilkan dari aplikasi, via <b>Pindai kode QR</b>.]]></string> + <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 ponsel: ketuk <b>Buka di aplikasi seluler</b>, lalu ketuk <b>Hubungkan</b> di aplikasi.]]></string> + <string name="delete_contact_menu_action">Hapus</string> + <string name="delete_group_menu_action">Hapus</string> + <string name="icon_descr_cancel_link_preview">batal pratinjau tautan</string> + <string name="icon_descr_close_button">Tombol tutup</string> + <string name="icon_descr_email">Email</string> + <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Jika Anda tidak dapat bertemu langsung, Anda dapat <b>pindai kode QR dalam panggilan video</b>, atau kontak Anda dapat bagikan tautan undangan.]]></string> + <string name="read_more_in_user_guide_with_link"><![CDATA[Lebih lanjut di <font color="#0088ff">Panduan Pengguna</font>.]]></string> + <string name="enable_receipts_all">Aktifkan</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">Ketika lebih dari satu operator diaktifkan, tidak satupun dari mereka memiliki metadata untuk mengetahui siapa yang berkomunikasi.</string> + <string name="chat_is_running">Obrolan sedang berjalan</string> + <string name="database_will_be_encrypted_and_passphrase_stored">Basis data akan dienkripsi dan frasa sandi disimpan di Keystore.</string> + <string name="rcv_group_event_changed_your_role">mengubah hak Anda jadi %s</string> + <string name="snd_conn_event_switch_queue_phase_changing">mengubah alamat…</string> + <string name="snd_conn_event_switch_queue_phase_changing_for_member">mengubah alamat untuk %s…</string> + <string name="share_text_database_id">ID basis data: %d</string> + <string name="operator_added_xftp_servers">Menambahkan media dan berkas server</string> + <string name="connect_plan_chat_already_exists">Obrolan sudah ada!</string> + <string name="database_encrypted">Basis data dienkripsi!</string> + <string name="database_encryption_will_be_updated_in_settings">Frasa sandi enkripsi basis data akan diperbarui dan disimpan di pengaturan.</string> + <string name="database_encryption_will_be_updated">Frasa sandi enkripsi basis data akan diperbarui dan disimpan di Keystore.</string> + <string name="mtr_error_different">migrasi berbeda di aplikasi/basis data: %s / %s</string> + <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Jika Anda tidak dapat bertemu langsung, <b>tunjukkan kode QR dalam panggilan video</b>, atau bagikan tautan.]]></string> + <string name="dont_create_address">Jangan buat alamat</string> + <string name="servers_info_details">Rincian</string> + <string name="connections">Koneksi</string> + <string name="connected_to_mobile">Terhubung ke ponsel</string> + <string name="dont_enable_receipts">Jangan aktifkan</string> + <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Buka <i>Gunakan dari desktop</i> di aplikasi seluler dan pindai kode QR.]]></string> + <string name="desktop_device">Desktop</string> + <string name="display_name_accepted_invitation">undangan diterima</string> + <string name="display_name_requested_to_connect">diminta untuk terhubung</string> + <string name="auth_open_chat_profiles">Ubah profil obrolan</string> + <string name="snd_error_quota">Kapasitas terlampaui - penerima tidak menerima pesan yang dikirim sebelumnya.</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[tautan 1-kali dapat digunakan <i>hanya dengan satu kontak</i> - bagikan secara langsung atau melalui messenger apa pun.]]></string> + <string name="copied">Disalin ke papan klip</string> + <string name="share_one_time_link">Buat tautan undangan satu-kali</string> + <string name="maximum_supported_file_size">Saat ini maksimal ukuran berkas adalah %1$s.</string> + <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Anda juga dapat terhubung dengan ketuk tautan. Jika tautan terbuka di peramban, ketuk <b>Buka di ponsel</b>.]]></string> + <string name="address_settings">Pengaturan alamat</string> + <string name="add_your_team_members_to_conversations">Tambahkan anggota tim Anda ke percakapan.</string> + <string name="business_address">Alamat bisnis</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[Semua pesan dan berkas dikirim <b>dengan enkripsi end-to-end</b>, dengan keamanan post-quantum dalam pesan pribadi.]]></string> + <string name="create_1_time_link">Buat tautan 1-kali</string> + <string name="onboarding_notifications_mode_battery">Notifikasi dan baterai</string> + <string name="onboarding_notifications_mode_off_desc_short">Tiada layanan latar belakang</string> + <string name="onboarding_notifications_mode_service_desc_short">Aplikasi selalu berjalan di latar belakang</string> + <string name="onboarding_notifications_mode_periodic_desc_short">Periksa pesan setiap 10 menit</string> + <string name="onboarding_select_network_operators_to_use">Pilih operator jaringan yang akan digunakan.</string> + <string name="onboarding_network_operators_app_will_use_for_routing">Misalnya, jika kontak Anda menerima pesan melalui server SimpleX Chat, aplikasi Anda akan mengirimkannya melalui server Flux.</string> + <string name="how_it_helps_privacy">Bagaimana ini membantu privasi</string> + <string name="onboarding_network_operators_continue">Lanjutkan</string> + <string name="onboarding_network_operators_configure_via_settings">Anda dapat konfigurasi server di pengaturan.</string> + <string name="read_more_in_github_with_link"><![CDATA[Lebih lengkap di <font color="#0088ff">repositori GitHub</font> kami.]]></string> + <string name="onboarding_network_operators_review_later">Nilai nanti</string> + <string name="onboarding_network_operators_update">Perbarui</string> + <string name="migrate_to_device_downloading_details">Rincian tautan unduhan</string> + <string name="network_session_mode_user_description"><![CDATA[Koneksi TCP terpisah (dan kredensial SOCKS) akan digunakan <b>untuk setiap profil obrolan yang Anda miliki di aplikasi</b>.]]></string> + <string name="network_session_mode_entity_description"><![CDATA[Koneksi TCP terpisah (dan kredensial SOCKS) akan digunakan <b>untuk setiap kontak dan anggota grup</b>.\n<b>Harap diperhatikan</b>: jika Anda memiliki banyak koneksi, konsumsi baterai dan lalu lintas dapat jauh lebih tinggi dan beberapa koneksi mungkin gagal.]]></string> + <string name="app_check_for_updates_download_started">Mengunduh pembaruan aplikasi, jangan tutup aplikasi</string> + <string name="app_check_for_updates_notice_disable">Mati</string> + <string name="create_simplex_address">Buat alamat SimpleX</string> + <string name="create_address_and_let_people_connect">Buat alamat agar orang dapat terhubung dengan Anda.</string> + <string name="continue_to_next_step">Lanjutkan</string> + <string name="delete_image">Hapus gambar</string> + <string name="edit_image">Edit gambar</string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Mengonsumsi banyak baterai</b>! Aplikasi selalu berjalan di latar belakang – notifikasi ditampilkan secara instan.]]></string> + <string name="settings_message_shape_corner">Sudut</string> + <string name="dont_show_again">Jangan tampilkan lagi</string> + <string name="migrate_from_device_confirm_upload">Konfirmasi unggahan</string> + <string name="migrate_from_device_archive_and_upload">Arsip dan unggah</string> + <string name="deleted">Dihapus</string> + <string name="desktop_connection_terminated">Koneksi dihentikan</string> + <string name="desktop_address">Alamat desktop</string> + <string name="desktop_devices">Perangkat desktop</string> + <string name="onboarding_network_about_operators">Tentang operator</string> + <string name="onboarding_network_operators_simplex_flux_agreement">SimpleX Chat dan Flux membuat kesepakatan untuk sertakan server yang dioperasikan Flux ke aplikasi.</string> + <string name="delete_chat_profile_question">Hapus profil obrolan?</string> + <string name="chat_is_stopped_you_should_transfer_database">Obrolan dihentikan. Jika Anda sudah gunakan basis data ini di perangkat lain, Anda harus transfer kembali sebelum memulai obrolan.</string> + <string name="database_passphrase_will_be_updated">Frasa sandi enkripsi basis data akan diperbarui.</string> + <string name="database_passphrase_is_required">Frasa sandi basis data diperlukan untuk membuka obrolan.</string> + <string name="operator_conditions_will_be_applied"><![CDATA[Ketentuan ini juga berlaku untuk: <b>%s</b>.]]></string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[Anda <b>tidak</b> boleh menggunakan basis data yang sama pada dua perangkat.]]></string> + <string name="connected_to_desktop">Terhubung ke desktop</string> + <string name="connect_to_desktop">Hubungkan ke desktop</string> + <string name="custom_time_unit_days">hari</string> + <string name="decryption_errors">kesalahan dekripsi</string> + <string name="disconnect_remote_hosts">Putuskan ponsel</string> + <string name="multicast_discoverable_via_local_network">Dapat ditemukan melalui jaringan lokal</string> + <string name="duplicates_label">duplikat</string> + <string name="v6_1_better_security">Keamanan yang lebih baik ✅</string> + <string name="v6_1_customizable_message_descr">Bentuk pesan yang dapat disesuaikan.</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Harap diperhatikan</b>: menggunakan database yang sama pada dua perangkat akan merusak dekripsi pesan dari koneksi Anda, sebagai perlindungan keamanan.]]></string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Anda sudah bergabung dengan grup <b>%1$s</b>.]]></string> + <string name="rcv_conn_event_switch_queue_phase_changing">mengubah alamat…</string> + <string name="delete_link_question">Hapus tautan?</string> + <string name="delete_link">Hapus tautan</string> + <string name="disconnect_remote_host">Putuskan</string> + <string name="servers_info_connected_servers_section_header">Server terhubung</string> + <string name="delete_member_message__question">Hapus pesan anggota?</string> + <string name="v6_1_message_dates_descr">Tanggal pesan lebih baik.</string> + <string name="new_desktop"><![CDATA[<i>(baru)</i>]]></string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Anda sudah terhubung dengan <b>%1$s</b>.]]></string> + <string name="created">Dibuat</string> + <string name="delete_members_messages__question">Hapus %d pesan anggota?</string> + <string name="failed_to_save_servers">Gagal simpan server</string> + <string name="ci_status_other_error">Galat: %1$s</string> + <string name="image_will_be_received_when_contact_completes_uploading">Gambar akan diterima setelah kontak Anda selesai mengunggah.</string> + <string name="error_saving_file">Gagal simpan berkas</string> + <string name="invalid_contact_link">Tautan tidak valid!</string> + <string name="app_check_for_updates_button_install">Instal pembaruan</string> + <string name="hide_dev_options">Sembunyikan:</string> + <string name="for_social_media">Untuk media sosial</string> + <string name="settings_experimental_features">Fitur eksperimental</string> + <string name="auth_enable_simplex_lock">Aktifkan Kunci SimpleX</string> + <string name="file_error_no_file">Berkas tidak ditemukan - kemungkinan besar telah dihapus atau dibatalkan.</string> + <string name="file_error_relay">Kesalahan berkas server: %1$s</string> + <string name="in_reply_to">Balasan untuk</string> + <string name="choose_file">Berkas</string> + <string name="gallery_image_button">gambar</string> + <string name="favorite_chat">Favorit</string> + <string name="exit_without_saving">Keluar tanpa menyimpan</string> + <string name="file_with_path">Berkas: %s</string> + <string name="group_invitation_item_description">undangan ke grup %1$s</string> + <string name="alert_title_no_group">Grup tidak ditemukan!</string> + <string name="error_sending_message_contact_invitation">Gagal kirim undangan</string> + <string name="action_button_add_members">Undang</string> + <string name="info_row_file_status">Status berkas</string> + <string name="error_changing_role">Gagal ganti hak</string> + <string name="error_removing_member">Gagal hapus anggota</string> + <string name="incognito">Samaran</string> + <string name="incognito_info_protects">Mode samaran melindungi privasi Anda dengan menggunakan profil acak baru untuk setiap kontak.</string> + <string name="error_enabling_delivery_receipts">Gagal aktifkan tanda terima pengirim!</string> + <string name="enter_this_device_name">Masukkan nama perangkat ini…</string> + <string name="migrate_to_device_import_failed">Impor gagal</string> + <string name="migrate_to_device_error_downloading_archive">Gagal unduh arsipan</string> + <string name="migrate_from_device_exported_file_doesnt_exist">Berkas yang diekspor tidak ada</string> + <string name="migrate_from_device_finalize_migration">Selesaikan migrasi</string> + <string name="servers_info_reset_stats_alert_error_title">Gagal atur ulang statistik</string> + <string name="error_importing_database">Gagal impor basis data obrolan</string> + <string name="error_deleting_database">Gagal hapus basis data obrolan</string> + <string name="files_and_media_section">Berkas dan media</string> + <string name="encrypt_database_question">Enkripsi basis data?</string> + <string name="incompatible_database_version">Versi basis data tidak kompatibel</string> + <string name="section_title_for_console">UNTUK KONSOL</string> + <string name="connect_plan_group_already_exists">Grup sudah ada!</string> + <string name="migrate_to_device_enter_passphrase">Masukkan frasa sandi</string> + <string name="enable_automatic_deletion_question">Aktifkan hapus pesan otomatis?</string> + <string name="encrypted_database">Basis data terenkripsi</string> + <string name="invalid_migration_confirmation">Konfirmasi migrasi tidak valid</string> + <string name="group_invitation_expired">Undangan grup kedaluwarsa</string> + <string name="conn_event_ratchet_sync_allowed">negosiasi ulang enkripsi diperbolehkan</string> + <string name="conn_event_ratchet_sync_required">negosiasi ulang enkripsi diperlukan</string> + <string name="snd_conn_event_ratchet_sync_allowed">negosiasi ulang enkripsi diizinkan untuk %s</string> + <string name="v6_2_network_decentralization_enable_flux">Aktifkan Flux di pengaturan Jaringan dan server untuk privasi metadata yang lebih baik.</string> + <string name="v6_2_network_decentralization_enable_flux_reason">untuk privasi metadata lebih baik.</string> + <string name="v6_2_improved_chat_navigation">Navigasi obrolan ditingkatkan</string> + <string name="error">Galat</string> + <string name="migrate_to_device_finalize_migration">Selesaikan migrasi pada perangkat lain.</string> + <string name="migrate_from_device_error_verifying_passphrase">Gagal verifikasi frasa sandi:</string> + <string name="servers_info_reconnect_server_error">Gagal hubungkan ulang server</string> + <string name="servers_info_reconnect_servers_error">Gagal hubungkan ulang server</string> + <string name="settings_section_title_experimenta">EKSPERIMENTAL</string> + <string name="export_database">Ekspor basis data</string> + <string name="import_database">Impor basis data</string> + <string name="error_stopping_chat">Gagal hentikan obrolan</string> + <string name="error_changing_message_deletion">Gagal ganti pengaturan</string> + <string name="error_with_info">Galat: %s</string> + <string name="icon_descr_group_inactive">Grup tidak aktif</string> + <string name="alert_title_group_invitation_expired">Undangan kedaluwarsa!</string> + <string name="snd_group_event_group_profile_updated">profil grup diperbarui</string> + <string name="initial_member_role">Hak awal</string> + <string name="error_creating_link_for_group">Gagal membuat tautan grup</string> + <string name="error_deleting_link_for_group">Gagal hapus tautan grup</string> + <string name="error_updating_link_for_group">Gagal perbarui tautan grup</string> + <string name="error_creating_member_contact">Gagal membuat kontak anggota</string> + <string name="user_hide">Sembunyikan</string> + <string name="image_will_be_received_when_contact_is_online">Gambar akan diterima saat kontak Anda online, harap tunggu atau periksa nanti!</string> + <string name="file_error">Kesalahan berkas</string> + <string name="from_gallery_button">Dari Galeri</string> + <string name="how_to_use_markdown">Cara menggunakan markdown</string> + <string name="alert_message_group_invitation_expired">Undangan grup tidak lagi berlaku, telah dihapus oleh pengirim.</string> + <string name="operator_use_for_messages_private_routing">Untuk routing pribadi</string> + <string name="error_updating_server_title">Gagal perbarui server</string> + <string name="enter_password_to_show">Masukkan kata sandi dalam pencarian</string> + <string name="enter_correct_passphrase">Masukkan frasa sandi yang benar.</string> + <string name="custom_time_unit_hours">jam</string> + <string name="desktop_incompatible_version">Versi tidak kompatibel</string> + <string name="servers_info_sessions_errors">Galat</string> + <string name="snd_error_proxy">Server penerusan: %1$s\nKesalahan: %2$s</string> + <string name="snd_error_proxy_relay">Server penerusan: %1$s\nKesalahan server tujuan: %2$s</string> + <string name="files_and_media_prohibited">Berkas dan media dilarang!</string> + <string name="file_will_be_received_when_contact_completes_uploading">Berkas akan diterima setelah kontak Anda selesai mengunggah.</string> + <string name="file_will_be_received_when_contact_is_online">Berkas akan diterima saat kontak Anda online, harap tunggu atau periksa nanti!</string> + <string name="enable_camera_access">Aktifkan akses kamera</string> + <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Jika Anda menerima tautan undangan SimpleX Chat, Anda dapat buka di peramban Anda:</string> + <string name="icon_descr_help">bantuan</string> + <string name="if_you_cant_meet_in_person">Jika Anda tidak dapat bertemu langsung, tunjukkan kode QR dalam panggilan video, atau bagikan tautan.</string> + <string name="network_option_enable_tcp_keep_alive">Aktifkan TCP keep-alive</string> + <string name="error_starting_chat">Gagal memulai obrolan</string> + <string name="settings_section_title_incognito">Mode samaran</string> + <string name="migrate_from_device_error_deleting_database">Gagal hapus basis data</string> + <string name="invalid_QR_code">Kode QR tidak valid</string> + <string name="error_encrypting_database">Gagal enkripsi basis data</string> + <string name="icon_descr_server_status_error">Galat</string> + <string name="import_database_confirmation">Impor</string> + <string name="expired_label">kedaluwarsa</string> + <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Jika Anda memilih menolak, pengirim TIDAK akan diberitahu.</string> + <string name="errors_in_servers_configuration">Kesalahan dalam konfigurasi server.</string> + <string name="for_chat_profile">Untuk profil obrolan %s:</string> + <string name="error_accepting_operator_conditions">Gagal menerima ketentuan</string> + <string name="app_check_for_updates_installed_successfully_title">Berhasil diinstal</string> + <string name="hide_profile">Sembunyikan profil</string> + <string name="enter_passphrase">Masukkan frasa sandi…</string> + <string name="migrate_to_device_file_delete_or_link_invalid">Berkas telah dihapus atau tautan tidak valid</string> + <string name="enter_welcome_message_optional">Masukkan pesan sambutan… (opsional)</string> + <string name="full_name__field">Nama lengkap:</string> + <string name="migrate_from_device_chat_should_be_stopped">Untuk melanjutkan, obrolan harus dihentikan.</string> + <string name="snd_conn_event_ratchet_sync_required">negosiasi ulang enkripsi diperlukan untuk %s</string> + <string name="icon_descr_expand_role">Perluas pemilihan hak</string> + <string name="found_desktop">Ditemukan desktop</string> + <string name="migrate_from_device_error_saving_settings">Gagal simpan pengaturan</string> + <string name="snd_conn_event_ratchet_sync_ok">enkripsi ok untuk %s</string> + <string name="error_exporting_chat_database">Gagal ekspor basis data obrolan</string> + <string name="import_database_question">Impor basis data obrolan?</string> + <string name="conn_level_desc_indirect">tidak langsung (%1$s)</string> + <string name="error_adding_server">Gagal menambah server</string> + <string name="migrate_from_device_error_uploading_archive">Gagal perbarui arsipan</string> + <string name="error_blocking_member_for_all">Gagal blokir anggota untuk semua</string> + <string name="error_saving_user_password">Gagal simpan kata sandi pengguna</string> + <string name="invalid_file_link">Tautan tidak valid</string> + <string name="no_message_servers_configured">Tiada server pesan.</string> + <string name="no_message_servers_configured_for_private_routing">Tiada server untuk routing pesan pribadi.</string> + <string name="no_media_servers_configured">Tiada server media dan berkas.</string> + <string name="no_message_servers_configured_for_receiving">Tiada server untuk menerima pesan.</string> + <string name="or_to_share_privately">Atau bagikan secara pribadi</string> + <string name="save_and_notify_group_members">Simpan dan beritahu anggota grup</string> + <string name="current_version_timestamp">%s (saat ini)</string> + <string name="unblock_member_desc">Pesan dari %s akan ditampilkan!</string> + <string name="auth_log_in_using_credential">Masuk dengan kredensial Anda</string> + <string name="send_disappearing_message_send">Kirim</string> + <string name="save_and_notify_contacts">Simpan dan beritahu kontak</string> + <string name="alert_title_msg_bad_id">ID pesan salah</string> + <string name="keychain_error">Kesalahan Keychain</string> + <string name="save_passphrase_and_open_chat">Simpan frasa sandi dan buka obrolan</string> + <string name="open_chat">Buka obrolan</string> + <string name="restore_passphrase_not_found_desc">Frasa sandi tidak ditemukan di Keystore, silakan masukkan secara manual. Hal ini mungkin terjadi jika Anda memulihkan data aplikasi menggunakan alat cadangan. Jika tidak demikian, silakan hubungi pengembang.</string> + <string name="one_hand_ui">Toolbar aplikasi yang dijangkau</string> + <string name="member_will_be_removed_from_group_cannot_be_undone">Anggota akan dihapus dari grup - ini tidak dapat dibatalkan!</string> + <string name="network_option_protocol_timeout_per_kb">Batas waktu protokol per KB</string> + <string name="network_option_rcv_concurrency">Menerima konkurensi</string> + <string name="profile_password">Kata sandi profil</string> + <string name="v4_5_italian_interface">Antarmuka Italia</string> + <string name="link_a_mobile">Hubungkan ponsel</string> + <string name="you_can_enable_delivery_receipts_later_alert">Anda dapat aktifkan nanti di pengaturan Privasi dan Keamanan aplikasi.</string> + <string name="linked_desktop_options">Opsi desktop tertaut</string> + <string name="migrate_from_device_try_again">Anda dapat mencoba lagi.</string> + <string name="servers_info_reset_stats_alert_confirm">Reset</string> + <string name="settings_restart_app">Mulai ulang</string> + <string name="restart_the_app_to_use_imported_chat_database">Mulai ulang aplikasi untuk gunakan basis data obrolan yang diimpor.</string> + <string name="restore_database">Pulihkan cadangan basis data</string> + <string name="database_restore_error">Gagal pulihkan basis data</string> + <string name="you_can_share_group_link_anybody_will_be_able_to_connect">Anda dapat bagikan tautan atau kode QR - siapa pun dapat bergabung ke grup. Anda tidak akan kehilangan anggota grup jika nanti Anda hapus.</string> + <string name="send_receipts_disabled_alert_title">Tanda terima dimatikan</string> + <string name="you_can_share_this_address_with_your_contacts">Anda dapat bagikan alamat ini dengan kontak agar dapat terhubung dengan %s.</string> + <string name="you_can_enable_delivery_receipts_later">Anda dapat aktifkan nanti di Pengaturan</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Kirim tanda terima pengirim akan diaktifkan untuk semua kontak di semua profil obrolan yang terlihat.</string> + <string name="or_paste_archive_link">Atau tempel tautan arsip</string> + <string name="migrate_to_device_database_init">Mempersiapkan unduhan</string> + <string name="migrate_to_device_title">Pindah ke sini</string> + <string name="migrate_to_device_repeat_import">Impor ulang</string> + <string name="migrate_to_device_confirm_network_settings_footer">Harap konfirmasi bahwa pengaturan jaringan sudah benar untuk perangkat ini.</string> + <string name="migrate_from_device_database_init">Mempersiapkan unggahan</string> + <string name="servers_info_reconnect_servers_message">Hubungkan ulang semua server yang terhubung untuk paksa kirim pesan. Ini menggunakan lalu lintas tambahan.</string> + <string name="servers_info_reconnect_server_title">Hubungkan ulang server?</string> + <string name="servers_info_reset_stats">Reset semua statistik</string> + <string name="servers_info_reset_stats_alert_title">Reset seluruh statistik?</string> + <string name="servers_info_reconnect_all_servers_button">Hubungkan ulang semua server</string> + <string name="save_passphrase_in_keychain">Simpan frasa sandi di Keystore</string> + <string name="info_row_local_name">Nama lokal</string> + <string name="info_row_sent_at">Dikirim pada</string> + <string name="info_row_received_at">Diterima pada</string> + <string name="network_options_reset_to_defaults">Reset ke bawaan</string> + <string name="v6_2_network_decentralization">Jaringan desentralisasi</string> + <string name="sending_delivery_receipts_will_be_enabled">Kirim tanda terima pengirim akan diaktifkan untuk semua kontak.</string> + <string name="linked_desktops">Desktop terhubung</string> + <string name="migrate_to_device_bytes_downloaded">%s diunduh</string> + <string name="servers_info_messages_received">Pesan diterima</string> + <string name="info_row_updated_at">Catatan diperbarui pada</string> + <string name="settings_section_title_messages">PESAN DAN BERKAS</string> + <string name="settings_section_title_user_theme">Tema profil</string> + <string name="settings_section_title_profile_images">Gambar profil</string> + <string name="enter_correct_current_passphrase">Harap masukkan frasa sandi saat ini yang benar.</string> + <string name="database_migrations">Migrasi: %s</string> + <string name="rcv_group_event_1_member_connected">%s terhubung</string> + <string name="profile_update_event_member_name_changed">anggota %1$s berubah menjadi %2$s</string> + <string name="info_row_message_status">Status pesan</string> + <string name="info_row_moderated_at">Dimoderasi pada</string> + <string name="button_send_direct_message">Kirim pesan pribadi</string> + <string name="network_option_protocol_timeout">Batas waktu protokol</string> + <string name="network_options_save_and_reconnect">Simpan dan hubungkan ulang</string> + <string name="incognito_info_allows">Memungkinkan adanya banyak koneksi anonim tanpa ada data bersama di antara mereka dalam satu profil obrolan.</string> + <string name="servers_info_reconnect_servers_title">Hubungkan ulang server?</string> + <string name="maximum_message_size_title">Pesan terlalu besar!</string> + <string name="maximum_message_size_reached_non_text">Mohon kurangi ukuran pesan atau hapus media dan kirim lagi.</string> + <string name="maximum_message_size_reached_text">Mohon kurangi ukuran pesan dan kirim lagi.</string> + <string name="maximum_message_size_reached_forwarding">Anda dapat salin dan kurangi ukuran pesan untuk kirim.</string> + <string name="reset_verb">Reset</string> + <string name="send_live_message_desc">Kirim pesan langsung - pesan akan diperbarui pada penerima saat Anda mengetik</string> + <string name="icon_descr_send_message">Kirim Pesan</string> + <string name="mark_unread">Tandai belum dibaca</string> + <string name="you_can_set_connection_name_to_remember">Anda dapat setel nama koneksi, untuk mengingat dengan siapa tautan dibagikan.</string> + <string name="app_check_for_updates_button_open">Buka lokasi berkas</string> + <string name="app_check_for_updates_installed_successfully_desc">Silakan mulai ulang aplikasi.</string> + <string name="join_group_incognito_button">Gabung ke samaran</string> + <string name="sender_at_ts">%s pada %s</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">Anggota akan dihapus dari obrolan - ini tidak dapat dibatalkan!</string> + <string name="operator_server_alert_title">Server operator</string> + <string name="server_added_to_operator__name">Server ditambah ke operator %s.</string> + <string name="error_server_operator_changed">Operator server berubah.</string> + <string name="error_server_protocol_changed">Protokol server berubah.</string> + <string name="network_option_ping_count">Jumlah PING</string> + <string name="network_option_ping_interval">Interval PING</string> + <string name="v6_2_improved_chat_navigation_descr">- Buka obrolan pada pesan belum dibaca pertama.\n- Lompat ke pesan yang dikutip.</string> + <string name="custom_time_picker_select">Pilih</string> + <string name="servers_info_reconnect_server_message">Hubungkan ulang server untuk paksa kirim pesan. Ini menggunakan lalu lintas tambahan.</string> + <string name="chat_bottom_bar">Bilah alat obrolan yang dijangkau</string> + <string name="profile_update_event_removed_picture">foto profil dihapus</string> + <string name="rcv_group_event_2_members_connected">%s dan %s terhubung</string> + <string name="v6_2_business_chats_descr">Privasi untuk pelanggan Anda.</string> + <string name="migrate_to_device_repeat_download">Unduh ulang</string> + <string name="migrate_to_device_try_again">Anda dapat mencoba lagi.</string> + <string name="migrate_from_device_remove_archive_question">Hapus arsip?</string> + <string name="migrate_from_device_repeat_upload">Unggah ulang</string> + <string name="keep_conversation">Simpan percakapan</string> + <string name="you_can_still_send_messages_to_contact">Anda dapat kirim pesan ke %1$s dari kontak yang diarsip.</string> + <string name="switch_receiving_address_desc">Alamat penerima akan diubah ke server lain. Perubahan alamat akan selesai setelah pengirim online.</string> + <string name="ask_your_contact_to_enable_voice">Minta kontak Anda untuk aktifkan kirim pesan suara.</string> + <string name="image_descr_profile_image">gambar profil</string> + <string name="save_auto_accept_settings">Simpan pengaturan terima otomatis</string> + <string name="share_text_updated_at">Catatan diperbarui pada: %s</string> + <string name="network_option_seconds_label">dtk</string> + <string name="bad_desktop_address">Alamat desktop salah</string> + <string name="scan_qr_code_from_desktop">Pindai kode QR dari desktop</string> + <string name="servers_info_messages_sent">Pesan terkirim</string> + <string name="moderated_item_description">dimoderasi oleh %s</string> + <string name="auth_open_chat_console">Buka konsol obrolan</string> + <string name="message_delivery_warning_title">Peringatan kirim pesan</string> + <string name="message_delivery_error_title">Kesalahan kirim pesan</string> + <string name="recipients_can_not_see_who_message_from">Penerima tidak dapat melihat siapa pengirim pesan ini.</string> + <string name="observer_cant_send_message_desc">Silakan hubungi admin grup.</string> + <string name="loading_remote_file_title">Memuat berkas</string> + <string name="loading_remote_file_desc">Harap tunggu sementara berkas sedang dimuat dari ponsel yang terhubung</string> + <string name="icon_descr_server_status_pending">Tertunda</string> + <string name="sync_connection_force_question">Negosiasi ulang enkripsi?</string> + <string name="sync_connection_force_confirm">Negosiasi ulang</string> + <string name="send_disappearing_message">Kirim pesan sementara</string> + <string name="send_verb">Kirim</string> + <string name="scan_QR_code">Pindai kode QR</string> + <string name="connect_via_link_or_qr_from_clipboard_or_in_person">(pindai atau tempel dari papan klip)</string> + <string name="toast_permission_denied">Izin Ditolak!</string> + <string name="mark_read">Tandai dibaca</string> + <string name="image_descr_link_preview">tautan pratinjau gambar</string> + <string name="you_can_share_your_address">Anda dapat bagikan alamat sebagai tautan atau kode QR - siapa pun dapat terhubung dengan Anda.</string> + <string name="network_options_save">Simpan</string> + <string name="make_profile_private">Jadikan profil pribadi!</string> + <string name="remote_hosts_section">Ponsel jarak jauh</string> + <string name="run_chat_section">JALANKAN OBROLAN</string> + <string name="store_passphrase_securely_without_recover">Harap simpan frasa sandi dengan aman, Anda TIDAK akan dapat mengakses obrolan jika hilang.</string> + <string name="rcv_group_event_member_deleted">dihapus %1$s</string> + <string name="share_text_sent_at">Dikirim pada: %s</string> + <string name="integrity_msg_bad_id">ID pesan salah</string> + <string name="remove_passphrase_from_keychain">Hapus frasa sandi dari Keystore?</string> + <string name="remove_passphrase_from_settings">Hapus frasa sandi dari pengaturan?</string> + <string name="save_passphrase_in_settings">Simpan frasa sandi di pengaturan</string> + <string name="proxied">Diproxy</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Tempel tautan yang Anda terima untuk terhubung dengan kontak…</string> + <string name="other_label">lainnya</string> + <string name="send_live_message">Kirim pesan langsung</string> + <string name="live_message">Pesan langsung!</string> + <string name="image_descr_qr_code">Kode QR</string> + <string name="migrate_to_device_migrating">Memindah</string> + <string name="server_address">Alamat server</string> + <string name="open_server_settings_button">Buka pengaturan server</string> + <string name="profile_update_event_removed_address">alamat kontak dihapus</string> + <string name="v6_0_chat_list_media">Putar dari daftar obrolan.</string> + <string name="linked_mobiles">Ponsel terhubung</string> + <string name="scan_from_mobile">Pindai dari ponsel</string> + <string name="network_type_other">Lainnya</string> + <string name="no_media_servers_configured_for_private_routing">Tiada server untuk menerima berkas.</string> + <string name="no_media_servers_configured_for_sending">Tiada server untuk kirim berkas.</string> + <string name="message_deleted_or_not_received_error_title">Tiada pesan</string> + <string name="auth_open_migration_to_another_device">Buka layar migrasi</string> + <string name="delete_messages_cannot_be_undone_warning">Pesan akan dihapus - ini tidak dapat dibatalkan!</string> + <string name="icon_descr_profile_image_placeholder">tempat penampung gambar profil</string> + <string name="srv_error_host">Alamat server tidak kompatibel dengan pengaturan jaringan.</string> + <string name="password_to_show">Kata sandi ditampilkan</string> + <string name="app_check_for_updates_button_remind_later">Ingatkan nanti</string> + <string name="restore_database_alert_desc">Harap masukkan kata sandi sebelumnya setelah memulihkan cadangan basis data. Tindakan ini tidak dapat dibatalkan.</string> + <string name="restore_database_alert_confirm">Pulihkan</string> + <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Anda dapat mulai obrolan via Pengaturan aplikasi / Basis Data atau dengan mulai ulang aplikasi.</string> + <string name="only_chat_owners_can_change_prefs">Hanya pemilik obrolan yang dapat ubah preferensi.</string> + <string name="send_receipts">Kirim tanda terima</string> + <string name="custom_time_unit_seconds">detik</string> + <string name="agent_critical_error_desc">Harap laporkan ke pengembang:\n%s\n\nDisarankan untuk memulai ulang aplikasi.</string> + <string name="custom_time_unit_minutes">menit</string> + <string name="network_session_mode_session_description">Kredensial SOCKS baru akan digunakan setiap kali Anda memulai aplikasi.</string> + <string name="email_invite_subject">Mari bicara di SimpleX Chat</string> + <string name="save_and_notify_contact">Simpan dan beritahu kontak</string> + <string name="save_preferences_question">Simpan preferensi?</string> + <string name="you_can_create_it_later">Anda dapat buat nanti</string> + <string name="join_group_question">Gabung ke grup?</string> + <string name="joining_group">Bergabung dengan grup</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">Minta kontak Anda untuk aktifkan panggilan.</string> + <string name="you_can_hide_or_mute_user_profile">Anda dapat sembunyikan atau matikan profil pengguna - tekan tahan untuk buka menu.</string> + <string name="v4_5_reduced_battery_usage">Penggunaan baterai sedikit</string> + <string name="migrate_from_device_migration_complete">Berhasil dipindah</string> + <string name="icon_descr_record_voice_message">Rekam pesan suara</string> + <string name="servers_info_subscriptions_section_header">Penerima pesan</string> + <string name="alert_title_msg_bad_hash">Hash pesan salah</string> + <string name="leave_chat_question">Keluar dari obrolan?</string> + <string name="store_passphrase_securely">Harap simpan frasa sandi dengan aman, Anda TIDAK akan dapat mengubahnya jika hilang.</string> + <string name="restore_database_alert_title">Pulihkan cadangan basis data?</string> + <string name="share_text_moderated_at">Dimoderasi pada: %s</string> + <string name="share_text_received_at">Diterima pada: %s</string> + <string name="chat_archive">Atau impor berkas arsip</string> + <string name="agent_internal_error_desc">Harap laporkan ke pengembang:\n%s</string> + <string name="display_name__field">Nama profil:</string> + <string name="servers_info_subscriptions_connections_pending">Tertunda</string> + <string name="servers_info_previously_connected_servers_section_header">Server yang terhubung sebelumnya</string> + <string name="servers_info_proxied_servers_section_header">Server proxy</string> + <string name="delete_messages_mark_deleted_warning">Pesan akan ditandai sebagai dihapus. Penerima tetap dapat melihat pesan tersebut.</string> + <string name="delete_message_mark_deleted_warning">Pesan akan ditandai sebagai dihapus. Penerima tetap dapat melihat pesan ini.</string> + <string name="sync_connection_confirm">Perbaiki</string> + <string name="sync_connection_question">Perbaiki koneksi?</string> + <string name="smp_servers_new_server">Server baru</string> + <string name="message_queue_info">Info antrian pesan</string> + <string name="settings_section_title_message_shape">Bentuk pesan</string> + <string name="settings_section_title_private_message_routing">ROUTING PESAN PRIBADI</string> + <string name="message_queue_info_server_info">info antrean server: %1$s\n\npesan terakhir diterima: %2$s</string> + <string name="users_delete_data_only">Hanya data profil lokal</string> + <string name="operator_open_changes">Buka perubahan</string> + <string name="operator_open_conditions">Ketentuan terbuka</string> + <string name="migrate_from_device_or_share_this_file_link">Atau bagikan tautan berkas dengan aman</string> + <string name="paste_desktop_address">Tempel alamat desktop</string> + <string name="users_delete_with_connections">Profil dan koneksi server</string> + <string name="other_errors">Kesalahan lainnya</string> + <string name="secured">Aman</string> + <string name="send_errors">Kesalahan kirim</string> + <string name="sent_directly">Dikirim langsung</string> + <string name="sent_via_proxy">Dikirim via proxy</string> + <string name="profile_update_will_be_sent_to_contacts">Pembaruan profil akan dikirim ke kontak Anda.</string> + <string name="paste_archive_link">Tempel tautan arsip</string> + <string name="remote_ctrl_connection_stopped_desc">Harap periksa apakah perangkat seluler dan desktop terhubung ke jaringan lokal yang sama, dan firewall desktop mengizinkan koneksi.\nHarap sampaikan masalah lain kepada pengembang.</string> + <string name="sync_connection_desc">Koneksi memerlukan negosiasi ulang enkripsi.</string> + <string name="encryption_renegotiation_in_progress">Negosiasi ulang enkripsi sedang berlangsung.</string> + <string name="connection_error_quota">Pesan yang tidak terkirim</string> + <string name="message_deleted_or_not_received_error_desc">Pesan ini telah dihapus atau belum diterima.</string> + <string name="to_connect_via_link_title">Untuk terhubung via tautan</string> + <string name="app_check_for_updates_notice_desc">Untuk mendapatkan pemberitahuan tentang rilis baru, aktifkan pemeriksaan berkala untuk versi Stabil atau Beta.</string> + <string name="simplex_address_or_1_time_link">Alamat SimpleX atau tautan 1-kali?</string> + <string name="settings_shutdown">Matikan</string> + <string name="terminal_always_visible">Lihat konsol di jendela baru</string> + <string name="user_unmute">Bunyikan</string> + <string name="unlink_desktop">Putuskan</string> + <string name="auth_unlock">Buka</string> + <string name="file_error_auth">Kunci salah atau alamat potongan berkas tidak dikenal - kemungkinan berkas dihapus.</string> + <string name="srv_error_version">Versi server tidak kompatibel dengan pengaturan jaringan.</string> + <string name="you_are_observer">Anda adalah pengamat</string> + <string name="to_start_a_new_chat_help_header">Untuk memulai obrolan baru</string> + <string name="gallery_video_button">Video</string> + <string name="connection_you_accepted_will_be_cancelled">Koneksi yang Anda terima akan dibatalkan!</string> + <string name="this_QR_code_is_not_a_link">Kode QR ini bukan tautan!</string> + <string name="unmute_chat">Bunyikan</string> + <string name="network_session_mode_transport_isolation">Isolasi transport</string> + <string name="update_network_session_mode_question">Perbarui mode isolasi transport?</string> + <string name="private_routing_explanation">Untuk melindungi alamat IP, routing pribadi menggunakan server SMP untuk mengirim pesan.</string> + <string name="show_internal_errors">Perlihat kesalahan internal</string> + <string name="show_slow_api_calls">Lihat panggilan API lambat</string> + <string name="we_do_not_store_contacts_or_messages_on_servers">Kami tidak menyimpan kontak atau pesan Anda (setelah terkirim) di server.</string> + <string name="your_profile_is_stored_on_your_device">Profil, kontak, dan pesan terkirim Anda disimpan di perangkat Anda.</string> + <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Platform perpesanan dan aplikasi yang melindungi privasi dan keamanan Anda.</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">Untuk melindungi privasi Anda, SimpleX gunakan ID terpisah untuk setiap kontak.</string> + <string name="settings_section_title_socks">PROXY SOCKS</string> + <string name="one_hand_ui_card_title">Alihkan daftar obrolan:</string> + <string name="upgrade_and_open_chat">Tingkatkan dan buka obrolan</string> + <string name="group_invitation_tap_to_join_incognito">Ketuk untuk gabung ke samaran</string> + <string name="snd_group_event_member_blocked">Anda memblokir %s</string> + <string name="rcv_group_event_n_members_connected">%s, %s dan %d anggota lainnya terhubung</string> + <string name="send_receipts_disabled_alert_msg">Grup ini memiliki lebih dari %1$d anggota, tanda terima kiriman tidak dikirimkan.</string> + <string name="connect_via_member_address_alert_desc">Permintaan koneksi akan dikirim ke anggota grup ini.</string> + <string name="unhide_chat_profile">Perlihat profil obrolan</string> + <string name="incognito_random_profile">Profil random Anda</string> + <string name="migrate_from_device_bytes_uploaded">%s diunggah</string> + <string name="debug_logs">Aktifkan log</string> + <string name="profile_is_only_shared_with_your_contacts">Profil hanya dibagikan dengan kontak Anda.</string> + <string name="settings_message_shape_tail">Ekor</string> + <string name="stop_chat_confirmation">Berhenti</string> + <string name="delete_chat_profile_action_cannot_be_undone_warning">Tindakan ini tidak dapat dibatalkan - profil, kontak, pesan, dan berkas Anda akan hilang secara permanen.</string> + <string name="non_fatal_errors_occured_during_import">Beberapa kesalahan tidak fatal terjadi selama impor:</string> + <string name="chat_database_exported_save">Anda dapat menyimpan arsip yang diekspor.</string> + <string name="v4_5_italian_interface_descr">Terima kasih kepada pengguna – kontribusi via Weblate!</string> + <string name="connect_plan_this_is_your_own_one_time_link">Ini adalah tautan 1-kali milik Anda!</string> + <string name="moderate_message_will_be_marked_warning">Pesan akan ditandai sebagai dimoderasi untuk semua anggota.</string> + <string name="moderate_messages_will_be_deleted_warning">Pesan akan dihapus untuk semua anggota.</string> + <string name="moderate_messages_will_be_marked_warning">Pesan akan ditandai sebagai dimoderasi untuk semua anggota.</string> + <string name="video_will_be_received_when_contact_completes_uploading">Video akan diterima saat kontak Anda selesai mengunggah.</string> + <string name="contact_sent_large_file">Kontak Anda mengirim berkas yang lebih besar dari ukuran maksimal (%1$s).</string> + <string name="temporary_file_error">Kesalahan berkas sementara</string> + <string name="voice_message_send_text">Pesan suara…</string> + <string name="you_accepted_connection">Anda menerima koneksi</string> + <string name="you_must_use_the_most_recent_version_of_database">Anda harus menggunakan versi terbaru basis data obrolan Anda pada satu perangkat SAJA, jika tidak, Anda mungkin berhenti menerima pesan dari beberapa kontak.</string> + <string name="chat_database_exported_not_all_files">Beberapa berkas tidak diekspor</string> + <string name="chat_database_exported_migrate">Anda dapat memindahkan basis data yang diekspor.</string> + <string name="passphrase_will_be_saved_in_settings">Frasa sandi akan disimpan dalam pengaturan sebagai teks biasa setelah Anda mengubahnya atau memulai ulang aplikasi.</string> + <string name="database_backup_can_be_restored">Upaya untuk mengubah frasa sandi basis data tidak selesai.</string> + <string name="network_option_tcp_connection_timeout">Waktu koneksi TCP habis</string> + <string name="whats_new_thanks_to_users_contribute_weblate">Terima kasih kepada pengguna – kontribusi via Weblate!</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">Arsip basis data yang diunggah akan dihapus secara permanen dari server.</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Peringatan: memulai obrolan di beberapa perangkat tidak didukung dan akan menyebabkan gagal kirim pesan</string> + <string name="migrate_from_device_verify_passphrase">Verifikasi frasa sandi</string> + <string name="upload_errors">Kesalahan unggah</string> + <string name="set_passphrase">Setel frasa sandi</string> + <string name="enable_automatic_deletion_message">Tindakan ini tidak dapat dibatalkan - pesan yang dikirim dan diterima sebelum waktu yang dipilih akan dihapus. Mungkin perlu waktu beberapa menit.</string> + <string name="one_hand_ui_change_instruction">Anda dapat ubah di pengaturan Tampilan.</string> + <string name="alert_message_no_group">Grup ini tidak ada lagi.</string> + <string name="you_rejected_group_invitation">Anda menolak undangan grup</string> + <string name="snd_group_event_changed_role_for_yourself">Anda mengubah hak Anda menjadi %s</string> + <string name="snd_group_event_changed_member_role">Anda mengubah hak %s menjadi %s</string> + <string name="view_conditions">Lihat ketentuan</string> + <string name="xftp_servers_per_user">Server untuk berkas baru dari profil obrolan Anda saat ini</string> + <string name="v6_0_upgrade_app">Perbarui aplikasi secara otomatis</string> + <string name="v6_2_network_decentralization_descr">Operator prasetel kedua dalam aplikasi!</string> + <string name="view_updated_conditions">Lihat ketentuan yang diperbarui</string> + <string name="this_device_name_shared_with_mobile">Nama perangkat akan dibagikan dengan klien seluler yang terhubung.</string> + <string name="remote_ctrl_error_timeout">Batas waktu tercapai saat menghubungkan ke desktop</string> + <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">Anda sudah terhubung melalui tautan 1-kali ini!</string> + <string name="settings_section_title_use_from_desktop">Gunakan dari desktop</string> + <string name="delete_files_and_media_desc">Tindakan ini tidak dapat dibatalkan - semua berkas dan media yang diterima dan dikirim akan dihapus. Gambar beresolusi rendah akan tetap ada.</string> + <string name="member_role_will_be_changed_with_notification">Hak akan diubah menjadi %s. Semua orang dalam grup akan diberitahu.</string> + <string name="database_is_not_encrypted">Basis data obrolan Anda tidak dienkripsi - setel frasa sandi untuk melindunginya.</string> + <string name="you_are_invited_to_group">Anda diundang ke grup</string> + <string name="waiting_for_mobile_to_connect">Menunggu ponsel terhubung:</string> + <string name="set_database_passphrase">Setel frasa sandi basis data</string> + <string name="update_database">Perbarui</string> + <string name="update_database_passphrase">Perbarui frasa sandi basis data</string> + <string name="snd_group_event_member_deleted">Anda menghapus %1$s</string> + <string name="group_members_n">%s, %s dan %d anggota</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="update_network_settings_confirmation">Perbarui</string> + <string name="verify_connection">Verifikasi koneksi</string> + <string name="subscribed">Berlangganan</string> + <string name="error_saving_database">Gagal simpan basis data</string> + <string name="settings_is_storing_in_clear_text">Frasa sandi disimpan di pengaturan sebagai teks biasa.</string> + <string name="video_decoding_exception_desc">Video tidak dapat didekodekan. Silakan coba video lain atau hubungi pengembang.</string> + <string name="sync_connection_force_desc">Enkripsi berfungsi dan perjanjian enkripsi baru tidak diperlukan. Hal ini dapat mengakibatkan kesalahan koneksi!</string> + <string name="share_address_publicly">Bagikan alamat secara publik</string> + <string name="share_simplex_address_on_social_media">Bagikan alamat SimpleX di media sosial.</string> + <string name="share_1_time_link_with_a_friend">Bagikan tautan 1-kali dengan teman</string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">Alamat SimpleX dan tautan 1-kali aman untuk dibagikan ke messenger lain.</string> + <string name="you_can_use_markdown_to_format_messages__prompt">Anda dapat gunakan markdown untuk format pesan:</string> + <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Anda akan berhenti menerima pesan dari obrolan ini. Riwayat obrolan akan disimpan.</string> + <string name="operator_use_for_messages">Gunakan untuk pesan</string> + <string name="operator_use_for_files">Gunakan untuk berkas</string> + <string name="network_option_tcp_connection">Koneksi TCP</string> + <string name="you_will_still_receive_calls_and_ntfs">Anda masih akan menerima panggilan dan notifikasi dari profil yang dibisukan ketika profil tersebut aktif.</string> + <string name="incognito_info_share">Saat Anda berbagi profil samaran dengan seseorang, profil ini akan digunakan untuk grup tempat Anda diundang.</string> + <string name="remote_ctrl_connection_stopped_identity_desc">Tautan ini digunakan dengan perangkat seluler lain, silakan buat tautan baru di desktop.</string> + <string name="your_contacts_will_remain_connected">Kontak Anda akan tetap terhubung.</string> + <string name="unlink_desktop_question">Putuskan desktop?</string> + <string name="database_downgrade_warning">Peringatan: Anda akan kehilangan beberapa data!</string> + <string name="subscription_errors">Kesalahan berlangganan</string> + <string name="moderate_message_will_be_deleted_warning">Pesan akan dihapus untuk semua anggota.</string> + <string name="verify_code_on_mobile">Verifikasi kode di ponsel</string> + <string name="you_are_invited_to_group_join_to_connect_with_group_members">Anda diundang ke grup. Bergabung untuk terhubung dengan anggota grup.</string> + <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Profil Anda disimpan di perangkat dan hanya dibagikan dengan kontak. Server SimpleX tidak dapat melihat profil Anda.</string> + <string name="snd_conn_event_switch_queue_phase_completed_for_member">Anda mengubah alamat untuk %s</string> + <string name="rcv_group_event_updated_group_profile">profil grup diperbarui</string> + <string name="rcv_group_event_3_members_connected">%s, %s dan %s terhubung</string> + <string name="member_role_will_be_changed_with_notification_chat">Hak akan diubah menjadi %s. Semua orang dalam obrolan akan diberitahu.</string> + <string name="custom_time_unit_weeks">minggu</string> + <string name="migrate_from_device_upload_failed">Gagal unggah</string> + <string name="migrate_from_device_uploading_archive">Mengunggah arsip</string> + <string name="servers_info_proxied_servers_section_footer">Anda tidak terhubung ke server ini. Routing pribadi digunakan untuk kirim pesan ke server ini.</string> + <string name="video_will_be_received_when_contact_is_online">Video akan diterima saat kontak Anda online, harap tunggu atau periksa nanti!</string> + <string name="verify_security_code">Verifikasi kode keamanan</string> + <string name="you_can_still_view_conversation_with_contact">Anda masih dapat melihat percakapan dengan %1$s dalam daftar obrolan.</string> + <string name="voice_messages_prohibited">Pesan suara dilarang!</string> + <string name="chat_help_tap_button">Tombol ketuk</string> + <string name="unfavorite_chat">Batal favorit</string> + <string name="you_invited_a_contact">Anda mengundang kontak</string> + <string name="icon_descr_address">Alamat SimpleX</string> + <string name="contact_wants_to_connect_with_you">ingin terhubung dengan Anda!</string> + <string name="show_QR_code">Lihat kode QR</string> + <string name="share_address_with_contacts_question">Bagikan alamat dengan kontak?</string> + <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Basis data obrolan Anda saat ini akan DIHAPUS dan DIGANTI dengan yang diimpor.\nTindakan ini tidak dapat dibatalkan - profil, kontak, pesan, dan berkas Anda akan hilang secara permanen.</string> + <string name="unknown_error">Kesalahan tidak diketahui</string> + <string name="unknown_database_error_with_info">Kesalahan basis data tidak diketahui: %s</string> + <string name="wrong_passphrase_title">Frase sandi salah!</string> + <string name="connect_plan_you_have_already_requested_connection_via_this_address">Anda telah meminta koneksi melalui alamat ini!</string> + <string name="auth_stop_chat">Hentikan obrolan</string> + <string name="observer_cant_send_message_title">Anda tidak dapat kirim pesan!</string> + <string name="voice_message_with_duration">Pesan suara (%1$s)</string> + <string name="view_security_code">Lihat kode keamanan</string> + <string name="you_need_to_allow_to_send_voice">Anda perlu mengizinkan kontak mengirim pesan suara agar dapat mengirimkannya.</string> + <string name="to_share_with_your_contact">(untuk dibagikan dengan kontak Anda)</string> + <string name="tap_to_scan">Ketuk untuk pindai</string> + <string name="thank_you_for_installing_simplex">Terima kasih telah memasang SimpleX Chat!</string> + <string name="set_contact_name">Setel nama kontak</string> + <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Kontak Anda harus online agar koneksi dapat selesai.\nAnda dapat batalkan koneksi ini dan hapus kontak (dan coba lagi nanti dengan tautan baru).</string> + <string name="image_descr_simplex_logo">Logo SimpleX</string> + <string name="icon_descr_simplex_team">Tim SimpleX</string> + <string name="you_will_be_connected_when_your_contacts_device_is_online">Anda akan terhubung saat perangkat kontak Anda online, harap tunggu atau periksa nanti!</string> + <string name="you_will_be_connected_when_your_connection_request_is_accepted">Anda akan terhubung saat permintaan koneksi Anda diterima. Harap tunggu atau periksa nanti!</string> + <string name="share_invitation_link">Bagikan tautan 1-kali</string> + <string name="you_can_accept_or_reject_connection">Saat orang meminta untuk terhubung, Anda dapat terima atau menolaknya.</string> + <string name="you_wont_lose_your_contacts_if_delete_address">Anda tidak akan kehilangan kontak jika menghapus alamat Anda nanti.</string> + <string name="shutdown_alert_question">Matikan?</string> + <string name="stop_chat_question">Hentikan obrolan?</string> + <string name="you_joined_this_group">Anda bergabung ke grup ini</string> + <string name="stop_chat_to_export_import_or_delete_chat_database">Hentikan obrolan untuk ekspor, impor, atau hapus basis data obrolan. Anda tidak akan dapat terima dan kirim pesan saat obrolan dihentikan.</string> + <string name="messages_section_description">Pengaturan ini berlaku untuk pesan di profil obrolan Anda saat ini</string> + <string name="your_chat_database">Basis data obrolan Anda</string> + <string name="rcv_group_event_member_unblocked">buka blokir %s</string> + <string name="snd_conn_event_switch_queue_phase_completed">Anda mengubah alamat</string> + <string name="operator_use_for_sending">Untuk kirim</string> + <string name="migrate_from_device_stopping_chat">Menghentikan obrolan</string> + <string name="unhide_profile">Perlihat profil</string> + <string name="you_have_to_enter_passphrase_every_time">Anda harus masukkan frasa sandi setiap aplikasi dibuka - frasa sandi tidak disimpan di perangkat.</string> + <string name="smp_server">Server SMP</string> + <string name="your_chat_profile_will_be_sent_to_your_contact">Profil obrolan Anda akan dikirim\nke kontak Anda</string> + <string name="share_address">Bagikan alamat</string> + <string name="member_role_will_be_changed_with_invitation">Hak akan diubah menjadi %s. Anggota akan menerima undangan baru.</string> + <string name="update_network_settings_question">Perbarui pengaturan jaringan?</string> + <string name="tap_to_activate_profile">Ketuk untuk aktifkan profil.</string> + <string name="servers_info_starting_from">Mulai dari %s.</string> + <string name="you_will_be_connected_when_group_host_device_is_online">Anda akan terhubung ke grup saat perangkat pemilik grup sedang online, harap tunggu atau periksa nanti!</string> + <string name="show_dev_options">Lihat:</string> + <string name="wrong_passphrase">Frasa sandi basis data salah</string> + <string name="profile_update_event_set_new_picture">setel foto profil baru</string> + <string name="profile_update_event_updated_profile">profil diperbarui</string> + <string name="size">Ukuran</string> + <string name="uploaded_files">Berkas diunggah</string> + <string name="connection_error_quota_desc">Koneksi mencapai batas pesan yang tidak terkirim, kontak Anda mungkin sedang offline.</string> + <string name="lock_not_enabled">Kunci SimpleX tidak diaktifkan!</string> + <string name="you_can_turn_on_lock">Anda dapat aktifkan Kunci SimpleX di Pengaturan.</string> + <string name="contact_you_shared_link_with_wont_be_able_to_connect">Kontak yang Anda bagikan tautan ini TIDAK akan dapat terhubung!</string> + <string name="image_decoding_exception_desc">Gambar tidak dapat didekodekan. Silakan coba gambar lain atau hubungi pengembang.</string> + <string name="snd_error_auth">Kunci salah atau koneksi tidak dikenal - kemungkinan besar koneksi ini dihapus.</string> + <string name="add_contact_or_create_group">Mulai obrolan baru</string> + <string name="address_creation_instruction">Ketuk Buat alamat SimpleX di menu untuk membuatnya nanti.</string> + <string name="scan_qr_to_connect_to_contact">Untuk terhubung, kontak Anda dapat pindai kode QR atau gunakan tautan di aplikasi.</string> + <string name="connect__your_profile_will_be_shared">Profil Anda %1$s akan dibagikan.</string> + <string name="to_protect_against_your_link_replaced_compare_codes">Untuk melindungi tautan Anda dari penggantian, Anda dapat membandingkan kode keamanan kontak.</string> + <string name="app_check_for_updates_canceled">Pembaruan unduhan dibatalkan</string> + <string name="your_current_profile">Profil Anda saat ini</string> + <string name="alert_title_cant_invite_contacts_descr">Anda menggunakan profil samaran untuk grup ini - untuk mencegah berbagi profil utama Anda, undang kontak tidak diizinkan</string> + <string name="chat_list_always_visible">Lihat daftar obrolan di jendela baru</string> + <string name="you_sent_group_invitation">Anda mengirim undangan grup</string> + <string name="this_link_is_not_a_valid_connection_link">Tautan ini bukan tautan koneksi yang valid!</string> + <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Anda akan berhenti menerima pesan dari grup ini. Riwayat obrolan akan disimpan.</string> + <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Anda bergabung ke grup ini. Menghubungkan untuk undang anggota grup.</string> + <string name="snd_group_event_member_unblocked">Anda buka blokir %s</string> + <string name="waiting_for_desktop">Menunggu desktop…</string> + <string name="v6_1_switch_chat_profile_descr">Ganti profil obrolan untuk undangan 1-kali.</string> + <string name="to_reveal_profile_enter_password">Untuk perlihat profil tersembunyi Anda, masukkan kata sandi lengkap di kolom pencarian di halaman Profil obrolan Anda.</string> + <string name="you_control_your_chat">Anda mengendalikan obrolan Anda!</string> + <string name="operator_use_for_messages_receiving">Untuk terima</string> + <string name="user_unhide">Perlihat</string> + <string name="updating_settings_will_reconnect_client_to_all_servers">Perbarui pengaturan akan menghubungkan ulang klien ke semua server.</string> + <string name="migrate_from_device_verify_database_passphrase">Verifikasi frasa sandi basis data</string> + <string name="xftp_server">Server XFTP</string> + <string name="subscription_results_ignored">Langganan diabaikan</string> + <string name="stop_sharing">Berhenti berbagi</string> + <string name="stop_sharing_address">Berhenti berbagi alamat?</string> + <string name="show_developer_options">Tampilkan opsi pengembang</string> + <string name="open_port_in_firewall_desc">Untuk mengizinkan aplikasi seluler terhubung ke desktop, buka port ini di firewall Anda, jika Anda mengaktifkannya</string> + <string name="strikethrough_text">coret</string> + <string name="delete_messages_after">Hapus pesan setelah</string> + <string name="a_plus_b">a + b</string> + <string name="users_delete_profile_for">Hapus obrolan profil untuk</string> + <string name="connection_not_ready">Koneksi belum siap</string> + <string name="error_creating_chat_tags">Kesalahan pada pembuatan daftar percakapan</string> + <string name="error_loading_chat_tags">Kesalahan memuat daftar percakapan</string> + <string name="error_updating_chat_tags">Kesalahan memperbaharui daftar percakapan</string> + <string name="list_menu">Daftar</string> + <string name="chat_list_contacts">Kontak</string> + <string name="chat_list_favorites">Favorit</string> + <string name="no_chats">Tidak ada pesan</string> + <string name="no_chats_found">Tidak ditemukan pesan</string> + <string name="no_chats_in_list">Tidak ada pesan dalam daftar %s</string> + <string name="no_unread_chats">Tidak ada chat belum terbaca</string> + <string name="chat_list_add_list">Tambahkan daftar</string> + <string name="chat_list_businesses">Bisnis</string> + <string name="chat_list_groups">Grup</string> + <string name="open_with_app">Buka dengan %s</string> + <string name="add_to_list">Tambahkan dalam daftar</string> + <string name="create_list">Buat daftar</string> + <string name="list_name_field_placeholder">Daftar nama</string> + <string name="duplicated_list_error">Daftar nama dan emoji harus berbeda untuk semua daftar</string> + <string name="save_list">Simpan daftar</string> + <string name="delete_chat_list_warning">Semua percakapan akan dipindahkan dari daftar %s, dan daftar akan dihapus</string> + <string name="delete_chat_list_menu_action">Hapus</string> + <string name="delete_chat_list_question">Hapus daftar</string> + <string name="edit_chat_list_menu_action">Sunting</string> + <string name="chat_list_all">Semua</string> + <string name="group_reports_active_one">1 laporan</string> + <string name="archive_verb">Arsip</string> + <string name="report_reason_other">Alasan lain</string> + <string name="report_item_archived_by">laporan arsip oleh %s</string> + <string name="archive_report">Arsip laporan</string> + <string name="delete_report">Hapus laporan</string> + <string name="chat_list_group_reports">Laporan</string> + <string name="report_compose_reason_header_other">Laporkan lainnya: hanya moderator grup yang akan melihat.</string> + <string name="error_creating_report">Gagal membuat laporan</string> + <string name="connection_error_blocked">Koneksi diblokir</string> + <string name="connection_error_blocked_desc">Koneksi diblokir oleh operator server:\n%1$s.</string> + <string name="blocking_reason_content">Konten melanggar ketentuan penggunaan</string> + <string name="blocking_reason_spam">Spam</string> + <string name="report_reason_alert_title">Alasan laporan?</string> + <string name="report_verb">Laporkan</string> + <string name="group_reports_member_reports">Laporan Anggota</string> + <string name="chat_list_notes">Catatan</string> + <string name="report_compose_reason_header_spam">Laporkan spam: hanya moderator grup yang akan melihat.</string> + <string name="report_compose_reason_header_community">Laporkan pelanggaran: hanya moderator grup yang akan melihat.</string> + <string name="prefs_error_saving_settings">Gagal simpan pengaturan</string> + <string name="group_reports_active">%d laporan</string> + <string name="report_compose_reason_header_profile">Laporkan profil anggota: hanya moderator grup yang akan melihat.</string> + <string name="report_archive_alert_title">Arsip laporan?</string> + <string name="file_error_blocked">Berkas diblokir oleh operator server:\n\n%1$s.</string> + <string name="report_archive_alert_desc">Laporan akan diarsipkan untuk Anda.</string> + <string name="group_member_role_moderator">moderator</string> + <string name="report_compose_reason_header_illegal">Laporkan konten: hanya moderator grup yang akan melihat.</string> + <string name="change_list">Ubah daftar</string> + <string name="change_order_chat_list_menu_action">Ubah urutan</string> + <string name="report_item_archived">laporan arsip</string> + <string name="report_reason_community">Pelanggaran pedoman komunitas</string> + <string name="report_reason_illegal">Konten tidak pantas</string> + <string name="report_reason_profile">Profil tidak pantas</string> + <string name="report_item_visibility_moderators">Hanya pengirim dan moderator dapat melihat</string> + <string name="report_item_visibility_submitter">Hanya Anda dan moderator dapat melihat</string> + <string name="report_reason_spam">Spam</string> + <string name="text_field_set_chat_placeholder">Setel nama obrolan…</string> + <string name="privacy_chat_list_open_web_link_question">Buka tautan peramban?</string> + <string name="privacy_chat_list_open_web_link">Buka tautan</string> + <string name="change_automatic_deletion_question">Ubah hapus pesan otomatis?</string> + <string name="disable_automatic_deletion_question">Matikan hapus pesan otomatis?</string> + <string name="disable_automatic_deletion_message">Pesan dalam obrolan ini tidak akan pernah dihapus.</string> + <string name="chat_ttl_options_footer">Hapus pesan obrolan dari perangkat Anda.</string> + <string name="disable_automatic_deletion">Matikan hapus pesan</string> + <string name="change_automatic_chat_deletion_message">Tindakan tak dapat dibatalkan - pesan yang dikirim dan diterima dalam obrolan ini sebelum yang dipilih akan dihapus.</string> + <string name="privacy_chat_list_open_links_ask">Tanya</string> + <string name="privacy_chat_list_open_links_no">Tidak</string> + <string name="privacy_chat_list_open_links">Buka tautan dari daftar obrolan</string> + <string name="privacy_chat_list_open_links_yes">Ya</string> + <string name="chat_item_ttl_year">1 tahun</string> + <string name="chat_item_ttl_default">bawaan (%s)</string> + <string name="mute_all_chat">Bisukan semua</string> + <string name="unread_mentions">Sebutan belum terbaca</string> + <string name="network_smp_web_port_section_title">Port TCP untuk pesan</string> + <string name="network_smp_web_port_footer">Gunakan port TCP %1$s jika tidak ada port yang ditentukan.</string> + <string name="network_smp_web_port_toggle">Gunakan port peramban</string> + <string name="max_group_mentions_per_message_reached">Anda dapat menyebut hingga %1$s anggota per pesan!</string> + <string name="report_archive_alert_desc_all">Semua laporan akan diarsipkan untuk Anda.</string> + <string name="report_archive_alert_title_all">Arsipkan semua laporan?</string> + <string name="report_archive_for_all_moderators">Untuk semua moderator</string> + <string name="report_archive_for_me">Untuk saya</string> + <string name="archive_reports">Arsip laporan</string> + <string name="notification_group_report">Laporan: %s</string> + <string name="enable_sending_member_reports">Izinkan untuk laporkan pesan ke moderator.</string> + <string name="group_members_can_send_reports">Anggota dapat melaporkan pesan ke moderator.</string> + <string name="member_reports_are_prohibited">Laporkan pesan dilarang di grup ini.</string> + <string name="disable_sending_member_reports">Dilarang laporkan pesan ke moderator.</string> + <string name="report_archive_alert_title_nth">Arsipkan %d laporan?</string> + <string name="error_reading_passphrase">Gagal membaca frasa sandi basis data</string> + <string name="v6_3_better_privacy_and_security">Privasi dan keamanan lebih baik</string> + <string name="block_members_desc">Semua pesan baru dari anggota ini akan disembunyikan!</string> + <string name="block_members_for_all_question">Blokir anggota untuk semua?</string> + <string name="v6_3_mentions_descr">Dapat notifikasi saat disebut.</string> + <string name="v6_3_faster_sending_messages">Kirim pesan lebih cepat.</string> + <string name="v6_3_organize_chat_lists_descr">Jangan lewatkan pesan penting.</string> + <string name="v6_3_better_groups_performance">Kinerja grup yang lebih baik</string> + <string name="v6_3_faster_deletion_of_groups">Hapus grup lebih cepat.</string> + <string name="operator_updated_conditions">Syarat diperbarui</string> + <string name="members_will_be_removed_from_chat_cannot_be_undone">Anggota akan dihapus dari obrolan - hal ini tidak dapat dibatalkan!</string> + <string name="button_remove_members_question">Hapus anggota?</string> + <string name="group_member_status_rejected">ditolak</string> + <string name="group_member_status_pending_approval_short">tertunda</string> + <string name="group_member_status_pending_approval">menunggu persetujuan</string> + <string name="unblock_members_for_all_question">Buka blokir anggota untuk semua?</string> + <string name="v6_3_reports">Kirim laporan pribadi</string> + <string name="v6_3_reports_descr">Bantu admin memoderasi grup.</string> + <string name="v6_3_set_message_expiration_in_chats">Atur pesan kedaluwarsa obrolan.</string> + <string name="v6_3_private_media_file_names">Nama berkas media pribadi.</string> + <string name="group_preview_rejected">ditolak</string> + <string name="members_will_be_removed_from_group_cannot_be_undone">Anggota akan dihapus dari grup - tindakan ini tidak dapat dibatalkan!</string> + <string name="feature_roles_moderators">moderator</string> + <string name="unblock_members_desc">Pesan dari anggota ini akan ditampilkan!</string> + <string name="v6_3_mentions">Sebutkan anggota 👋</string> + <string name="v6_3_organize_chat_lists">Atur obrolan ke dalam daftar</string> + <string name="restore_passphrase_can_not_be_read_desc">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.</string> + <string name="onboarding_conditions_accept">Terima</string> + <string name="onboarding_conditions_by_using_you_agree">Dengan menggunakan SimpleX Chat, Anda setuju untuk:\n- hanya mengirim konten legal di grup publik.\n- hormati pengguna lain – tidak ada spam.</string> + <string name="onboarding_conditions_configure_server_operators">Konfigurasikan operator server</string> + <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Kebijakan privasi dan ketentuan penggunaan.</string> + <string name="onboarding_conditions_private_chats_not_accessible">Obrolan pribadi, grup, dan kontak Anda tidak dapat diakses oleh operator server.</string> + <string name="restore_passphrase_can_not_be_read_enter_manually_desc">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.</string> + <string name="network_smp_web_port_preset_footer">Gunakan port TCP 443 hanya untuk presetel server.</string> + <string name="network_smp_web_port_all">Semua server</string> + <string name="network_smp_web_port_off">Mati</string> + <string name="network_smp_web_port_preset">Presetel server</string> + <string name="full_link_button_text">Tautan lengkap</string> + <string name="link_requires_newer_app_version_please_upgrade">Tautan ini perlu versi aplikasi yang baru. Harap perbarui aplikasi atau minta kontak untuk kirim tautan kompatibel.</string> + <string name="simplex_link_channel">Tautan saluran SimpleX</string> + <string name="privacy_short_links">Gunakan tautan singkat (BETA)</string> + <string name="unsupported_connection_link">Tautan koneksi tidak didukung</string> + <string name="short_link_button_text">Tautan singkat</string> +</resources> 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 79c1897153..6c086835ea 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -41,7 +41,7 @@ <string name="description_via_contact_address_link_incognito">incognito via link indirizzo del contatto</string> <string name="description_via_one_time_link">via link una tantum</string> <string name="description_via_one_time_link_incognito">incognito via link una tantum</string> - <string name="simplex_link_contact">Indirizzo del contatto SimpleX</string> + <string name="simplex_link_contact">Indirizzo di contatto SimpleX</string> <string name="simplex_link_invitation">Invito SimpleX una tantum</string> <string name="simplex_link_group">Link gruppo SimpleX</string> <string name="simplex_link_mode_full">Link completo</string> @@ -211,8 +211,8 @@ <string name="connection_error_auth_desc">A meno che il tuo contatto non abbia eliminato la connessione o che questo link non sia già stato usato, potrebbe essere un errore; per favore segnalalo. \nPer connetterti, chiedi al tuo contatto di creare un altro link di connessione e controlla di avere una connessione di rete stabile.</string> <string name="error_smp_test_certificate">Probabilmente l\'impronta del certificato nell\'indirizzo del server è sbagliata</string> - <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Per rispettare la tua privacy, invece delle notifiche push l\'app ha un <b>servizio SimpleX in secondo piano</b>; usa una piccola percentuale di batteria al giorno.]]></string> - <string name="turn_off_battery_optimization"><![CDATA[Per usarlo, <b>consenti a SimpleX di funzionare in secondo piano</b> nella prossima schermata. Altrimenti le notifiche saranno disattivate.]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Per migliorare la privacy, <b>SimpleX funziona in secondo piano</b> invece di usare le notifiche push.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[<b>Consentilo</b> nella prossima schermata per ricevere le notifiche immediatamente.]]></string> <string name="simplex_service_notification_title">Servizio SimpleX Chat</string> <string name="notifications_mode_service_desc">Servizio in secondo piano sempre attivo. Le notifiche verranno mostrate appena i messaggi saranno disponibili.</string> <string name="la_notice_title_simplex_lock">SimpleX Lock</string> @@ -237,19 +237,19 @@ <string name="accept">Accetta</string> <string name="v4_2_group_links_desc">Gli amministratori possono creare i link per entrare nei gruppi.</string> <string name="allow_disappearing_messages_only_if">Consenti i messaggi a tempo solo se il tuo contatto li consente.</string> - <string name="allow_to_delete_messages">Permetti di eliminare irreversibilmente i messaggi inviati.</string> + <string name="allow_to_delete_messages">Permetti di eliminare irreversibilmente i messaggi inviati. (24 ore)</string> <string name="allow_your_contacts_to_send_disappearing_messages">Permetti ai tuoi contatti di inviare messaggi a tempo.</string> <string name="network_enable_socks_info">Accedere ai server via proxy SOCKS sulla porta %d\? Il proxy deve essere avviato prima di attivare questa opzione.</string> <string name="v4_3_improved_server_configuration_desc">Aggiungi server scansionando codici QR.</string> <string name="all_group_members_will_remain_connected">Tutti i membri del gruppo resteranno connessi.</string> - <string name="allow_irreversible_message_deletion_only_if">Consenti l\'eliminazione irreversibile dei messaggi solo se il contatto la consente a te.</string> + <string name="allow_irreversible_message_deletion_only_if">Consenti l\'eliminazione irreversibile dei messaggi solo se il contatto la consente a te. (24 ore)</string> <string name="above_then_preposition_continuation">sopra, quindi:</string> <string name="accept_contact_button">Accetta</string> <string name="accept_connection_request__question">Accettare la richiesta di connessione\?</string> <string name="accept_contact_incognito_button">Accetta in incognito</string> <string name="clear_chat_warning">Tutti i messaggi verranno eliminati, non è reversibile! I messaggi verranno eliminati SOLO per te.</string> <string name="smp_servers_preset_add">Aggiungi server preimpostati</string> - <string name="smp_servers_add">Aggiungi server…</string> + <string name="smp_servers_add">Aggiungi server</string> <string name="network_settings">Impostazioni di rete avanzate</string> <string name="about_simplex">Riguardo SimpleX</string> <string name="callstatus_accepted">chiamata accettata</string> @@ -257,7 +257,7 @@ <string name="color_primary">Principale</string> <string name="accept_feature">Accetta</string> <string name="allow_voice_messages_only_if">Consenti i messaggi vocali solo se il tuo contatto li consente.</string> - <string name="allow_your_contacts_irreversibly_delete">Permetti ai tuoi contatti di eliminare irreversibilmente i messaggi inviati.</string> + <string name="allow_your_contacts_irreversibly_delete">Permetti ai tuoi contatti di eliminare irreversibilmente i messaggi inviati. (24 ore)</string> <string name="allow_direct_messages">Permetti l\'invio di messaggi diretti ai membri.</string> <string name="allow_to_send_disappearing">Permetti l\'invio di messaggi a tempo.</string> <string name="allow_to_send_voice">Permetti l\'invio di messaggi vocali.</string> @@ -266,18 +266,17 @@ <string name="group_full_name_field">Nome completo del gruppo:</string> <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Se non potete incontrarvi di persona, puoi <b>scansionare il codice QR nella videochiamata</b>, oppure il tuo contatto può condividere un link di invito.]]></string> <string name="full_backup">Backup dei dati dell\'app</string> - <string name="keychain_is_storing_securely">Android Keystore è usato per memorizzare in modo sicuro la password; permette il funzionamento del servizio di notifica.</string> + <string name="keychain_is_storing_securely">L\'archivio chiavi di Android è usato per memorizzare in modo sicuro la password; permette il funzionamento del servizio di notifica.</string> <string name="allow_your_contacts_to_send_voice_messages">Permetti ai tuoi contatti di inviare messaggi vocali.</string> <string name="chat_database_deleted">Database della chat eliminato</string> <string name="settings_section_title_icon">ICONA APP</string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Ideale per la batteria</b>. Riceverai notifiche solo quando l\'app è in esecuzione (NO servizio in secondo piano).]]></string> - <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Consuma più batteria</b>! Servizio in secondo piano sempre attivo: le notifiche sono mostrate non appena i messaggi sono disponibili.]]></string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Consuma più batteria</b>! L\'app funziona sempre in secondo piano: le notifiche vengono mostrate istantaneamente.]]></string> <string name="callstatus_calling">chiamata…</string> <string name="icon_descr_cancel_link_preview">annulla anteprima link</string> - <string name="cannot_access_keychain">Impossibile accedere al Keystore per salvare la password del database</string> + <string name="cannot_access_keychain">Impossibile accedere all\'archivio chiavi per salvare la password del database</string> <string name="alert_title_cant_invite_contacts">Impossibile invitare i contatti!</string> <string name="change_role">Cambia ruolo</string> - <string name="chat_archive_section">ARCHIVIO CHAT</string> <string name="snd_conn_event_switch_queue_phase_changing">cambio indirizzo…</string> <string name="chat_is_stopped">Chat fermata</string> <string name="group_member_status_introduced">in connessione (presentato)</string> @@ -286,14 +285,12 @@ <string name="delete_link">Elimina link</string> <string name="create_address">Crea indirizzo</string> <string name="button_create_group_link">Crea link</string> - <string name="database_encryption_will_be_updated">La password di crittografia del database verrà aggiornata e conservata nel Keystore.</string> + <string name="database_encryption_will_be_updated">La password di crittografia del database verrà aggiornata e conservata nell\'archivio chiavi.</string> <string name="encrypted_with_random_passphrase">Il database è crittografato con una password casuale, puoi cambiarla.</string> <string name="database_passphrase_is_required">La password del database è necessaria per aprire la chat.</string> <string name="delete_group_menu_action">Elimina</string> - <string name="direct_messages_are_prohibited_in_chat">I messaggi diretti tra i membri sono vietati in questo gruppo.</string> + <string name="direct_messages_are_prohibited_in_group">I messaggi diretti tra i membri sono vietati in questo gruppo.</string> <string name="display_name">Inserisci il tuo nome:</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Aggiungi un contatto</b>: per creare il tuo codice QR una tantum per il tuo contatto.]]></string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Scansiona codice QR</b>: per connetterti al contatto che ti mostra il codice QR.]]></string> <string name="choose_file">File</string> <string name="clear_chat_button">Svuota chat</string> <string name="clear_chat_question">Svuotare la chat\?</string> @@ -319,7 +316,6 @@ <string name="clear_verification">Annulla la verifica</string> <string name="connect_button">Connetti</string> <string name="connect_via_link">Connetti via link</string> - <string name="create_one_time_link">Crea link di invito una tantum</string> <string name="database_passphrase_and_export">Password del database ed esportazione</string> <string name="smp_servers_enter_manually">Inserisci il server manualmente</string> <string name="how_to_use_simplex_chat">Come si usa</string> @@ -354,7 +350,7 @@ <string name="how_to_use_markdown">Come usare il markdown</string> <string name="icon_descr_audio_call">chiamata audio</string> <string name="audio_call_no_encryption">chiamata audio (non crittografata e2e)</string> - <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Buono per la batteria</b>. Il servizio in secondo piano cerca messaggi ogni 10 minuti. Potresti perdere chiamate o messaggi urgenti.]]></string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Buono per la batteria</b>. L\'app cerca messaggi ogni 10 minuti. Potresti perdere chiamate o messaggi urgenti.]]></string> <string name="call_already_ended">Chiamata già terminata!</string> <string name="create_your_profile">Crea il tuo profilo</string> <string name="decentralized">Decentralizzato</string> @@ -367,7 +363,7 @@ <string name="icon_descr_audio_off">Audio spento</string> <string name="icon_descr_audio_on">Audio acceso</string> <string name="settings_audio_video_calls">Chiamate audio e video</string> - <string name="auto_accept_images">Auto-accetta immagini</string> + <string name="auto_accept_images">Auto-accetta le immagini</string> <string name="integrity_msg_bad_hash">hash del messaggio errato</string> <string name="integrity_msg_bad_id">ID messaggio errato</string> <string name="icon_descr_call_ended">Chiamata terminata</string> @@ -401,13 +397,9 @@ <string name="settings_experimental_features">Funzionalità sperimentali</string> <string name="export_database">Esporta database</string> <string name="settings_section_title_help">AIUTO</string> - <string name="chat_archive_header">Archivio chat</string> <string name="chat_is_stopped_indication">Chat fermata</string> - <string name="archive_created_on_ts">Creato il %1$s</string> <string name="database_error">Errore del database</string> - <string name="passphrase_is_different">La password del database è diversa da quella salvata nel Keystore.</string> - <string name="delete_archive">Elimina archivio</string> - <string name="delete_chat_archive_question">Eliminare l\'archivio della chat\?</string> + <string name="passphrase_is_different">La password del database è diversa da quella salvata nell\'archivio chiavi.</string> <string name="encrypted_database">Database crittografato</string> <string name="enter_correct_passphrase">Inserisci la password giusta.</string> <string name="enter_passphrase">Inserisci la password…</string> @@ -416,7 +408,7 @@ <string name="icon_descr_group_inactive">Gruppo inattivo</string> <string name="rcv_conn_event_switch_queue_phase_completed">indirizzo cambiato per te</string> <string name="rcv_group_event_changed_member_role">ha cambiato il ruolo di %s in %s</string> - <string name="rcv_group_event_changed_your_role">cambiato il tuo ruolo in %s</string> + <string name="rcv_group_event_changed_your_role">ha cambiato il tuo ruolo in %s</string> <string name="rcv_conn_event_switch_queue_phase_changing">cambio indirizzo…</string> <string name="snd_conn_event_switch_queue_phase_changing_for_member">cambio indirizzo per %s…</string> <string name="rcv_group_event_member_connected">si è connesso/a</string> @@ -461,7 +453,7 @@ <string name="group_display_name_field">Inserisci il nome del gruppo:</string> <string name="group_profile_is_stored_on_members_devices">Il profilo del gruppo è memorizzato sui dispositivi dei membri, non sui server.</string> <string name="chat_preferences_always">sempre</string> - <string name="both_you_and_your_contacts_can_delete">Sia tu che il tuo contatto potete eliminare irreversibilmente i messaggi inviati.</string> + <string name="both_you_and_your_contacts_can_delete">Sia tu che il tuo contatto potete eliminare irreversibilmente i messaggi inviati. (24 ore)</string> <string name="both_you_and_your_contact_can_send_disappearing">Sia tu che il tuo contatto potete inviare messaggi a tempo.</string> <string name="both_you_and_your_contact_can_send_voice">Sia tu che il tuo contatto potete inviare messaggi vocali.</string> <string name="chat_preferences">Preferenze della chat</string> @@ -478,7 +470,7 @@ <string name="feature_enabled_for_contact">attivato per il contatto</string> <string name="feature_enabled_for_you">attivato per te</string> <string name="group_preferences">Preferenze del gruppo</string> - <string name="v4_2_auto_accept_contact_requests">Auto-accetta richieste di contatto</string> + <string name="v4_2_auto_accept_contact_requests">Auto-accetta le richieste di contatto</string> <string name="ttl_d">%dg</string> <string name="ttl_day">%d giorno</string> <string name="ttl_days">%d giorni</string> @@ -486,7 +478,7 @@ <string name="ttl_h">%do</string> <string name="ttl_hour">%d ora</string> <string name="ttl_hours">%d ore</string> - <string name="disappearing_messages_are_prohibited">I messaggi a tempo sono vietati in questo gruppo.</string> + <string name="disappearing_messages_are_prohibited">I messaggi a tempo sono vietati.</string> <string name="ttl_m">%dm</string> <string name="ttl_min">%d min</string> <string name="ttl_month">%d mese</string> @@ -498,22 +490,22 @@ <string name="ttl_week">%d settimana</string> <string name="ttl_weeks">%d settimane</string> <string name="v4_2_group_links">Link del gruppo</string> - <string name="group_members_can_delete">I membri del gruppo possono eliminare irreversibilmente i messaggi inviati.</string> - <string name="group_members_can_send_dms">I membri del gruppo possono inviare messaggi diretti.</string> - <string name="group_members_can_send_disappearing">I membri del gruppo possono inviare messaggi a tempo.</string> - <string name="group_members_can_send_voice">I membri del gruppo possono inviare messaggi vocali.</string> + <string name="group_members_can_delete">I membri possono eliminare irreversibilmente i messaggi inviati. (24 ore)</string> + <string name="group_members_can_send_dms">I membri possono inviare messaggi diretti.</string> + <string name="group_members_can_send_disappearing">I membri possono inviare messaggi a tempo.</string> + <string name="group_members_can_send_voice">I membri possono inviare messaggi vocali.</string> <string name="v4_4_verify_connection_security_desc">Confronta i codici di sicurezza con i tuoi contatti.</string> <string name="v4_4_disappearing_messages">Messaggi a tempo</string> <string name="v4_3_improved_privacy_and_security_desc">Nascondi la schermata dell\'app nelle app recenti.</string> - <string name="keychain_allows_to_receive_ntfs">Android Keystore verrà usato per memorizzare in modo sicuro la password dopo il riavvio dell\'app o la modifica della password; consentirà di ricevere le notifiche.</string> + <string name="keychain_allows_to_receive_ntfs">L\'archivio chiavi di Android verrà usato per memorizzare in modo sicuro la password dopo il riavvio dell\'app o la modifica della password; consentirà di ricevere le notifiche.</string> <string name="impossible_to_recover_passphrase"><![CDATA[<b>Nota bene</b>: NON potrai recuperare o cambiare la password se la perdi.]]></string> <string name="change_database_passphrase_question">Cambiare password del database\?</string> - <string name="confirm_new_passphrase">Conferma password nuova…</string> + <string name="confirm_new_passphrase">Conferma nuova password…</string> <string name="current_passphrase">Password attuale…</string> <string name="database_encrypted">Database crittografato!</string> <string name="database_passphrase_will_be_updated">La password di crittografia del database verrà aggiornata.</string> <string name="database_will_be_encrypted">Il database verrà crittografato.</string> - <string name="database_will_be_encrypted_and_passphrase_stored">Il database verrà crittografato e la password conservata nel Keystore.</string> + <string name="database_will_be_encrypted_and_passphrase_stored">Il database verrà crittografato e la password conservata nell\'archivio chiavi.</string> <string name="delete_files_and_media_question">Eliminare i file e i multimediali\?</string> <string name="delete_messages">Elimina messaggi</string> <string name="delete_messages_after">Elimina messaggi dopo</string> @@ -546,7 +538,7 @@ <string name="invalid_QR_code">Codice QR non valido</string> <string name="image_descr_link_preview">immagine di anteprima link</string> <string name="mark_read">Segna come già letto</string> - <string name="mark_unread">Segna come non letto</string> + <string name="mark_unread">Segna come non letta</string> <string name="icon_descr_more_button">Altro</string> <string name="mute_chat">Silenzia</string> <string name="image_descr_profile_image">immagine del profilo</string> @@ -609,22 +601,18 @@ <string name="install_simplex_chat_for_terminal">Installa SimpleX Chat per terminale</string> <string name="ensure_ICE_server_address_are_correct_format_and_unique">Assicurati che gli indirizzi dei server WebRTC ICE siano nel formato corretto, uno per riga e non doppi.</string> <string name="network_and_servers">Rete e server</string> - <string name="network_settings_title">Impostazioni di rete</string> + <string name="network_settings_title">Impostazioni avanzate</string> <string name="network_use_onion_hosts_no">No</string> <string name="network_use_onion_hosts_required_desc">Gli host Onion saranno necessari per la connessione. \nNota bene: non potrai connetterti ai server senza indirizzo .onion .</string> - <string name="network_use_onion_hosts_required_desc_in_alert">Gli host Onion saranno necessari per la connessione.</string> <string name="network_use_onion_hosts_prefer_desc">Gli host Onion verranno usati quando disponibili.</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">Gli host Onion verranno usati quando disponibili.</string> <string name="network_use_onion_hosts_no_desc">Gli host Onion non verranno usati.</string> - <string name="network_use_onion_hosts_no_desc_in_alert">Gli host Onion non verranno usati.</string> <string name="rate_the_app">Valuta l\'app</string> <string name="network_use_onion_hosts_required">Obbligatorio</string> <string name="save_servers_button">Salva</string> <string name="saved_ICE_servers_will_be_removed">I server WebRTC ICE salvati verranno rimossi.</string> <string name="share_link">Condividi link</string> - <string name="star_on_github">Stella su GitHub</string> - <string name="update_onion_hosts_settings_question">Aggiornare l\'impostazione degli host .onion\?</string> + <string name="star_on_github">Dai una stella su GitHub</string> <string name="network_disable_socks">Usare una connessione internet diretta\?</string> <string name="network_use_onion_hosts">Usa gli host .onion</string> <string name="network_enable_socks">Usare il proxy SOCKS\?</string> @@ -656,32 +644,29 @@ <string name="your_profile_is_stored_on_your_device">Il tuo profilo, i contatti e i messaggi recapitati sono memorizzati sul tuo dispositivo.</string> <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti. I server di SimpleX non possono vedere il tuo profilo.</string> <string name="ignore">Ignora</string> - <string name="immune_to_spam_and_abuse">Immune a spam e abusi</string> + <string name="immune_to_spam_and_abuse">Immune allo spam</string> <string name="incoming_audio_call">Chiamata in arrivo</string> <string name="incoming_video_call">Videochiamata in arrivo</string> <string name="onboarding_notifications_mode_service">Istantaneo</string> - <string name="onboarding_notifications_mode_subtitle">Può essere cambiato in seguito via impostazioni.</string> + <string name="onboarding_notifications_mode_subtitle">Come influisce sulla batteria</string> <string name="make_private_connection">Crea una connessione privata</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[Molte persone hanno chiesto: <i>se SimpleX non ha identificatori utente, come può recapitare i messaggi\?</i>]]></string> - <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Solo i dispositivi client memorizzano i profili utente, i contatti, i gruppi e i messaggi inviati con <b>crittografia end-to-end a 2 livelli</b>.]]></string> - <string name="opensource_protocol_and_code_anybody_can_run_servers">Protocollo e codice open source: chiunque può gestire i server.</string> - <string name="paste_the_link_you_received">Incolla il link ricevuto</string> - <string name="people_can_connect_only_via_links_you_share">Le persone possono connettersi a te solo tramite i link che condividi.</string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Solo i dispositivi client memorizzano i profili utente, i contatti, i gruppi e i messaggi.</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Chiunque può installare i server.</string> + <string name="paste_the_link_you_received">Incolla il link che hai ricevuto</string> + <string name="people_can_connect_only_via_links_you_share">Sei tu a decidere chi può connettersi.</string> <string name="onboarding_notifications_mode_periodic">Periodico</string> <string name="privacy_redefined">Privacy ridefinita</string> <string name="onboarding_notifications_mode_title">Notifiche private</string> <string name="read_more_in_github_with_link"><![CDATA[Maggiori informazioni nel nostro <font color="#0088ff">repository GitHub</font>.]]></string> - <string name="read_more_in_github">Maggiori informazioni nel nostro repository GitHub.</string> <string name="reject">Rifiuta</string> - <string name="first_platform_without_user_ids">La prima piattaforma senza alcun identificatore utente – privata by design.</string> - <string name="next_generation_of_private_messaging">La nuova generazione di messaggistica privata</string> - <string name="to_protect_privacy_simplex_has_ids_for_queues">Per proteggere la privacy, invece degli ID utente usati da tutte le altre piattaforme, SimpleX dispone di identificatori per le code dei messaggi, separati per ciascuno dei tuoi contatti.</string> + <string name="first_platform_without_user_ids">Nessun identificatore utente.</string> + <string name="next_generation_of_private_messaging">Il futuro dei messaggi</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">Per proteggere la tua privacy, SimpleX usa ID separati per ciascuno dei tuoi contatti.</string> <string name="use_chat">Usa la chat</string> <string name="icon_descr_video_call">videochiamata</string> <string name="video_call_no_encryption">videochiamata (non crittografata e2e)</string> <string name="onboarding_notifications_mode_off">Quando l\'app è in esecuzione</string> <string name="contact_wants_to_connect_via_call">%1$s vuole connettersi con te via</string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Puoi controllare attraverso quale/i server <b>ricevere</b> i messaggi, i tuoi contatti – i server che usi per inviare loro i messaggi.]]></string> <string name="alert_text_skipped_messages_it_can_happen_when">Può accadere quando: \n1. I messaggi sono scaduti sul client mittente dopo 2 giorni o sul server dopo 30 giorni. \n2. La decifrazione del messaggio è fallita, perché tu o il tuo contatto avete usato un backup del database vecchio. @@ -719,7 +704,7 @@ <string name="restart_the_app_to_create_a_new_chat_profile">Riavvia l\'app per creare un profilo di chat nuovo.</string> <string name="restart_the_app_to_use_imported_chat_database">Riavvia l\'app per usare il database della chat importato.</string> <string name="run_chat_section">AVVIA CHAT</string> - <string name="send_link_previews">Invia anteprime dei link</string> + <string name="send_link_previews">Invia le anteprime dei link</string> <string name="set_password_to_export">Imposta la password per esportare</string> <string name="settings_section_title_settings">IMPOSTAZIONI</string> <string name="settings_section_title_socks">PROXY SOCKS</string> @@ -744,14 +729,13 @@ <string name="leave_group_button">Esci</string> <string name="leave_group_question">Uscire dal gruppo\?</string> <string name="open_chat">Apri chat</string> - <string name="restore_passphrase_not_found_desc">Password non trovata nel Keystore, inseriscila a mano. Potrebbe essere successo se hai ripristinato i dati dell\'app usando uno strumento di backup. In caso contrario, contatta gli sviluppatori.</string> + <string name="restore_passphrase_not_found_desc">Password non trovata nell\'archivio chiavi, inseriscila a mano. Potrebbe essere successo se hai ripristinato i dati dell\'app usando uno strumento di backup. In caso contrario, contatta gli sviluppatori.</string> <string name="restore_database_alert_desc">Inserisci la password precedente dopo aver ripristinato il backup del database. Questa azione non può essere annullata.</string> <string name="store_passphrase_securely_without_recover">Conserva la password in modo sicuro, NON potrai accedere alla chat se la perdi.</string> <string name="restore_database_alert_confirm">Ripristina</string> <string name="restore_database">Ripristina backup del database</string> <string name="restore_database_alert_title">Ripristinare il backup del database\?</string> <string name="database_restore_error">Errore di ripristino del database</string> - <string name="save_archive">Salva archivio</string> <string name="save_passphrase_and_open_chat">Salva la password e apri la chat</string> <string name="database_backup_can_be_restored">Il tentativo di cambiare la password del database non è stato completato.</string> <string name="unknown_database_error_with_info">Errore del database sconosciuto: %s</string> @@ -775,7 +759,7 @@ <string name="group_invitation_tap_to_join">Tocca per entrare</string> <string name="group_invitation_tap_to_join_incognito">Toccare per entrare in incognito</string> <string name="alert_message_no_group">Questo gruppo non esiste più.</string> - <string name="rcv_group_event_updated_group_profile">profilo del gruppo aggiornato</string> + <string name="rcv_group_event_updated_group_profile">ha aggiornato il profilo del gruppo</string> <string name="you_are_invited_to_group">Sei stato/a invitato/a al gruppo</string> <string name="snd_conn_event_switch_queue_phase_completed">hai cambiato indirizzo</string> <string name="snd_conn_event_switch_queue_phase_completed_for_member">hai cambiato l\'indirizzo per %s</string> @@ -819,7 +803,6 @@ <string name="network_option_protocol_timeout">Scadenza del protocollo</string> <string name="receiving_via">Ricezione via</string> <string name="network_options_reset_to_defaults">Ripristina i predefiniti</string> - <string name="network_options_revert">Ripristina</string> <string name="network_options_save">Salva</string> <string name="save_group_profile">Salva il profilo del gruppo</string> <string name="network_option_seconds_label">sec</string> @@ -829,8 +812,8 @@ <string name="theme_system">Sistema</string> <string name="network_option_tcp_connection_timeout">Scadenza connessione TCP</string> <string name="group_is_decentralized">Completamente decentralizzato: visibile solo ai membri.</string> - <string name="member_role_will_be_changed_with_notification">Il ruolo verrà cambiato in \"%s\". Tutti i membri del gruppo riceveranno una notifica.</string> - <string name="member_role_will_be_changed_with_invitation">Il ruolo verrà cambiato in \"%s\". Il membro riceverà un nuovo invito.</string> + <string name="member_role_will_be_changed_with_notification">Il ruolo verrà cambiato in "%s". Tutti i membri del gruppo riceveranno una notifica.</string> + <string name="member_role_will_be_changed_with_invitation">Il ruolo verrà cambiato in "%s". Il membro riceverà un nuovo invito.</string> <string name="update_network_settings_confirmation">Aggiorna</string> <string name="update_network_settings_question">Aggiornare le impostazioni di rete\?</string> <string name="updating_settings_will_reconnect_client_to_all_servers">L\'aggiornamento delle impostazioni riconnetterà il client a tutti i server.</string> @@ -842,15 +825,14 @@ <string name="chat_preferences_off">off</string> <string name="feature_off">off</string> <string name="chat_preferences_on">on</string> - <string name="only_you_can_delete_messages">Solo tu puoi eliminare irreversibilmente i messaggi (il tuo contatto può contrassegnarli per l\'eliminazione).</string> + <string name="only_you_can_delete_messages">Solo tu puoi eliminare irreversibilmente i messaggi (il tuo contatto può contrassegnarli per l\'eliminazione). (24 ore)</string> <string name="only_you_can_send_disappearing">Solo tu puoi inviare messaggi a tempo.</string> - <string name="only_your_contact_can_delete">Solo il tuo contatto può eliminare irreversibilmente i messaggi (tu puoi contrassegnarli per l\'eliminazione).</string> + <string name="only_your_contact_can_delete">Solo il tuo contatto può eliminare irreversibilmente i messaggi (tu puoi contrassegnarli per l\'eliminazione). (24 ore)</string> <string name="only_your_contact_can_send_disappearing">Solo il tuo contatto può inviare messaggi a tempo.</string> <string name="prohibit_sending_disappearing_messages">Proibisci l\'invio di messaggi a tempo.</string> <string name="prohibit_sending_voice_messages">Proibisci l\'invio di messaggi vocali.</string> <string name="feature_received_prohibited">ricevuto, vietato</string> <string name="reset_color">Ripristina i colori</string> - <string name="save_color">Salva colore</string> <string name="accept_feature_set_1_day">Imposta 1 giorno</string> <string name="set_group_preferences">Imposta le preferenze del gruppo</string> <string name="theme">Tema</string> @@ -860,7 +842,7 @@ <string name="your_preferences">Le tue preferenze</string> <string name="v4_3_improved_server_configuration">Configurazione del server migliorata</string> <string name="v4_3_irreversible_message_deletion">Eliminazione irreversibile del messaggio</string> - <string name="message_deletion_prohibited_in_chat">L\'eliminazione irreversibile dei messaggi è vietata in questo gruppo.</string> + <string name="message_deletion_prohibited_in_chat">L\'eliminazione irreversibile dei messaggi è vietata.</string> <string name="v4_3_voice_messages_desc">Max 40 secondi, ricevuto istantaneamente.</string> <string name="new_in_version">Novità nella %s</string> <string name="only_you_can_send_voice">Solo tu puoi inviare messaggi vocali.</string> @@ -873,7 +855,7 @@ <string name="v4_2_security_assessment_desc">La sicurezza di SimpleX Chat è stata verificata da Trail of Bits.</string> <string name="v4_3_voice_messages">Messaggi vocali</string> <string name="voice_prohibited_in_this_chat">I messaggi vocali sono vietati in questa chat.</string> - <string name="voice_messages_are_prohibited">I messaggi vocali sono vietati in questo gruppo.</string> + <string name="voice_messages_are_prohibited">I messaggi vocali sono vietati.</string> <string name="whats_new">Novità</string> <string name="v4_2_auto_accept_contact_requests_desc">Con messaggio di benvenuto facoltativo.</string> <string name="v4_3_irreversible_message_deletion_desc">I tuoi contatti possono consentire l\'eliminazione completa dei messaggi.</string> @@ -889,10 +871,9 @@ <string name="enter_correct_current_passphrase">Inserisci la password attuale corretta.</string> <string name="store_passphrase_securely">Conserva la password in modo sicuro, NON potrai cambiarla se la perdi.</string> <string name="remove_passphrase">Rimuovi</string> - <string name="remove_passphrase_from_keychain">Rimuovere la password dal Keystore\?</string> - <string name="save_passphrase_in_keychain">Salva la password nel Keystore</string> + <string name="remove_passphrase_from_keychain">Rimuovere la password dall\'archivio chiavi?</string> + <string name="save_passphrase_in_keychain">Salva la password nell\'archivio chiavi</string> <string name="chat_item_ttl_seconds">%s secondo/i</string> - <string name="stop_chat_to_enable_database_actions">Ferma la chat per attivare le azioni del database.</string> <string name="delete_files_and_media_desc">Questa azione non può essere annullata: tutti i file e i media ricevuti e inviati verranno eliminati. Rimarranno le immagini a bassa risoluzione.</string> <string name="enable_automatic_deletion_message">Questa azione non può essere annullata: i messaggi inviati e ricevuti prima di quanto selezionato verranno eliminati. Potrebbe richiedere diversi minuti.</string> <string name="update_database">Aggiorna</string> @@ -921,8 +902,7 @@ <string name="update_network_session_mode_question">Aggiornare la modalità di isolamento del trasporto\?</string> <string name="users_delete_all_chats_deleted">Tutte le chat e i messaggi verranno eliminati. Non è reversibile!</string> <string name="network_session_mode_user">Profilo di chat</string> - <string name="network_session_mode_entity_description">Verrà usata una connessione TCP separata (e le credenziali SOCKS) <b> per ogni contatto e membro del gruppo </b>. -\n<b> Nota: </b>: se hai molte connessioni, il consumo di batteria e traffico può essere notevolmente superiore e alcune connessioni potrebbero fallire.</string> + <string name="network_session_mode_entity_description"><![CDATA[Verrà usata una connessione TCP separata (e le credenziali SOCKS) <b> per ogni contatto e membro del gruppo </b>.\n<b> Nota: </b>: se hai molte connessioni, il consumo di batteria e traffico può essere notevolmente superiore e alcune connessioni potrebbero fallire.]]></string> <string name="network_session_mode_entity">Connessione</string> <string name="messages_section_description">Questa impostazione si applica ai messaggi del profilo di chat attuale</string> <string name="network_session_mode_transport_isolation">Isolamento del trasporto</string> @@ -938,7 +918,7 @@ <string name="v4_5_italian_interface_descr">Grazie agli utenti – contribuite via Weblate!</string> <string name="v4_4_french_interface">Interfaccia francese</string> <string name="v4_5_italian_interface">Interfaccia italiana</string> - <string name="v4_5_message_draft">Bozza dei messaggi</string> + <string name="v4_5_message_draft">Bozza del messaggio</string> <string name="v4_5_message_draft_descr">Conserva la bozza dell\'ultimo messaggio, con gli allegati.</string> <string name="v4_5_private_filenames">Nomi di file privati</string> <string name="v4_5_transport_isolation_descr">Per profilo di chat (predefinito) o per connessione (BETA).</string> @@ -972,8 +952,6 @@ <string name="tap_to_activate_profile">Tocca per attivare il profilo.</string> <string name="user_unhide">Svela</string> <string name="make_profile_private">Rendi privato il profilo!</string> - <string name="should_be_at_least_one_profile">Deve esserci almeno un profilo utente.</string> - <string name="should_be_at_least_one_visible_profile">Deve esserci almeno un profilo utente visibile.</string> <string name="you_can_hide_or_mute_user_profile">Puoi nascondere o silenziare un profilo utente - tienilo premuto per il menu.</string> <string name="dont_show_again">Non mostrare più</string> <string name="muted_when_inactive">Silenzioso quando inattivo!</string> @@ -988,12 +966,11 @@ <string name="v4_6_chinese_spanish_interface_descr">Grazie agli utenti – contribuite via Weblate!</string> <string name="hidden_profile_password">Password del profilo nascosta</string> <string name="save_profile_password">Salva la password del profilo</string> - <string name="to_reveal_profile_enter_password">Per rivelare il tuo profilo nascosto, inserisci una password completa in un campo di ricerca nella pagina \"I tuoi profili di chat\".</string> + <string name="to_reveal_profile_enter_password">Per rivelare il tuo profilo nascosto, inserisci una password completa in un campo di ricerca nella pagina "I tuoi profili di chat".</string> <string name="password_to_show">Password per mostrare</string> <string name="v4_6_group_moderation_descr">Ora gli amministratori possono: \n- eliminare i messaggi dei membri. -\n- disattivare i membri (ruolo \"osservatore\")</string> - <string name="cant_delete_user_profile">Impossibile eliminare il profilo utente!</string> +\n- disattivare i membri (ruolo "osservatore")</string> <string name="hide_profile">Nascondi il profilo</string> <string name="confirm_password">Conferma password</string> <string name="error_updating_user_privacy">Errore nell\'aggiornamento della privacy dell\'utente</string> @@ -1121,12 +1098,12 @@ <string name="v5_0_polish_interface_descr">Grazie agli utenti – contribuite via Weblate!</string> <string name="v5_0_large_files_support">Video e file fino a 1 GB</string> <string name="auto_accept_contact">Accetta automaticamente</string> - <string name="auth_open_chat_profiles">Apri i profili di chat</string> + <string name="auth_open_chat_profiles">Cambia i profili di chat</string> <string name="learn_more">Maggiori informazioni</string> <string name="scan_qr_to_connect_to_contact">Per connettervi, il tuo contatto può scansionare il codice QR o usare il link nell\'app.</string> <string name="you_can_accept_or_reject_connection">Quando le persone chiedono di connettersi, puoi accettare o rifiutare.</string> <string name="simplex_address">Indirizzo SimpleX</string> - <string name="theme_colors_section_title">COLORI DEL TEMA</string> + <string name="theme_colors_section_title">COLORI DELL\'INTERFACCIA</string> <string name="your_contacts_will_remain_connected">I tuoi contatti resteranno connessi.</string> <string name="add_address_to_your_profile">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.</string> <string name="create_address_and_let_people_connect">Crea un indirizzo per consentire alle persone di connettersi con te.</string> @@ -1154,7 +1131,7 @@ <string name="color_secondary">Secondario</string> <string name="color_received_message">Messaggio ricevuto</string> <string name="color_sent_message">Messaggio inviato</string> - <string name="color_title">Titolo</string> + <string name="color_title">Titoli</string> <string name="one_time_link_short">Link una tantum</string> <string name="learn_more_about_address">Info sull\'indirizzo SimpleX</string> <string name="all_your_contacts_will_remain_connected_update_sent">Tutti i tuoi contatti resteranno connessi. L\'aggiornamento del profilo verrà inviato ai tuoi contatti.</string> @@ -1192,7 +1169,7 @@ <string name="change_self_destruct_passcode">Cambia codice di autodistruzione</string> <string name="message_reactions">Reazioni ai messaggi</string> <string name="message_reactions_prohibited_in_this_chat">Le reazioni ai messaggi sono vietate in questa chat.</string> - <string name="message_reactions_are_prohibited">Le reazioni ai messaggi sono vietate in questo gruppo.</string> + <string name="message_reactions_are_prohibited">Le reazioni ai messaggi sono vietate.</string> <string name="only_you_can_add_message_reactions">Solo tu puoi aggiungere reazioni ai messaggi.</string> <string name="prohibit_message_reactions">Proibisci le reazioni ai messaggi.</string> <string name="prohibit_message_reactions_group">Proibisci le reazioni ai messaggi.</string> @@ -1201,7 +1178,7 @@ <string name="only_your_contact_can_add_message_reactions">Solo il tuo contatto può aggiungere reazioni ai messaggi.</string> <string name="allow_your_contacts_adding_message_reactions">Consenti ai tuoi contatti di aggiungere reazioni ai messaggi.</string> <string name="allow_message_reactions_only_if">Consenti reazioni ai messaggi solo se il tuo contatto le consente.</string> - <string name="group_members_can_add_message_reactions">I membri del gruppo possono aggiungere reazioni ai messaggi.</string> + <string name="group_members_can_add_message_reactions">I membri possono aggiungere reazioni ai messaggi.</string> <string name="send_disappearing_message_30_seconds">30 secondi</string> <string name="send_disappearing_message_send">Invia</string> <string name="send_disappearing_message">Invia messaggio a tempo</string> @@ -1252,7 +1229,7 @@ <string name="info_row_disappears_at">Scompare il</string> <string name="share_text_disappears_at">Scompare il: %s</string> <string name="item_info_no_text">nessun testo</string> - <string name="non_fatal_errors_occured_during_import">Si sono verificati alcuni errori non gravi durante l\'importazione: vedi la console della chat per i dettagli.</string> + <string name="non_fatal_errors_occured_during_import">Si sono verificati alcuni errori non fatali durante l\'importazione:</string> <string name="settings_restart_app">Riavvia</string> <string name="settings_section_title_app">APP</string> <string name="shutdown_alert_desc">Le notifiche smetteranno di funzionare fino a quando non riavvierai l\'app</string> @@ -1265,10 +1242,10 @@ <string name="error_aborting_address_change">Errore nell\'interruzione del cambio di indirizzo</string> <string name="only_owners_can_enable_files_and_media">Solo i proprietari del gruppo possono attivare file e contenuti multimediali.</string> <string name="files_and_media">File e multimediali</string> - <string name="group_members_can_send_files">I membri del gruppo possono inviare file e contenuti multimediali.</string> + <string name="group_members_can_send_files">I membri possono inviare file e contenuti multimediali.</string> <string name="prohibit_sending_files">Proibisci l\'invio di file e contenuti multimediali.</string> <string name="abort_switch_receiving_address_desc">Il cambio di indirizzo verrà interrotto. Verrà usato il vecchio indirizzo di ricezione.</string> - <string name="files_are_prohibited_in_group">File e contenuti multimediali sono vietati in questo gruppo.</string> + <string name="files_are_prohibited_in_group">File e contenuti multimediali sono vietati.</string> <string name="files_and_media_prohibited">File e contenuti multimediali vietati!</string> <string name="la_mode_off">Off</string> <string name="no_filtered_chats">Nessuna chat filtrata</string> @@ -1361,17 +1338,17 @@ <string name="turn_off_system_restriction_button">Apri impostazioni app</string> <string name="system_restricted_background_in_call_desc">L\'app potrebbe venire chiusa dopo 1 minuto in secondo piano.</string> <string name="paste_the_link_you_received_to_connect_with_your_contact">Incolla il link che hai ricevuto per connetterti con il contatto…</string> - <string name="connect__your_profile_will_be_shared">Il tuo profilo %1$s verrà condiviso.</string> + <string name="connect__your_profile_will_be_shared">Verrà condiviso il tuo profilo %1$s.</string> <string name="connect__a_new_random_profile_will_be_shared">Verrà condiviso un nuovo profilo casuale.</string> <string name="disable_notifications_button">Disattiva le notifiche</string> <string name="system_restricted_background_in_call_title">Nessuna chiamata in secondo piano</string> <string name="system_restricted_background_desc">SimpleX non può funzionare in secondo piano. Riceverai le notifiche solo quando l\'app è aperta.</string> <string name="system_restricted_background_warn"><![CDATA[Per attivare le notifiche, scegli <b>Utilizzo batteria dell\'app</b> / <b>Senza restrizioni</b> nelle impostazioni.]]></string> <string name="system_restricted_background_in_call_warn"><![CDATA[Per effettuare chiamate in secondo piano, scegli <b>Utilizzo batteria dell\'app</b> / <b>Senza restrizioni</b> nelle impostazioni.]]></string> - <string name="rcv_group_event_2_members_connected">%s e %s sono connessi/e</string> - <string name="rcv_group_event_n_members_connected">%s, %s e altri %d membri sono connessi</string> - <string name="rcv_group_event_3_members_connected">%s, %s e %s sono connessi/e</string> - <string name="privacy_message_draft">Bozza</string> + <string name="rcv_group_event_2_members_connected">%s e %s si sono connessi/e</string> + <string name="rcv_group_event_n_members_connected">%s, %s e altri %d membri si sono connessi</string> + <string name="rcv_group_event_3_members_connected">%s, %s e %s si sono connessi/e</string> + <string name="privacy_message_draft">Bozza del messaggio</string> <string name="privacy_show_last_messages">Mostra gli ultimi messaggi</string> <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Il database verrà crittografato e la password conservata nelle impostazioni.</string> <string name="you_can_change_it_later">La password casuale viene conservata nelle impostazioni come testo normale. @@ -1380,7 +1357,7 @@ <string name="remove_passphrase_from_settings">Rimuovere la password dalle impostazioni\?</string> <string name="use_random_passphrase">Usa password casuale</string> <string name="save_passphrase_in_settings">Salva password nelle impostazioni</string> - <string name="setup_database_passphrase">Configura password del database</string> + <string name="setup_database_passphrase">Configura la password del database</string> <string name="set_database_passphrase">Imposta password del database</string> <string name="open_database_folder">Apri cartella del database</string> <string name="passphrase_will_be_saved_in_settings">La password verrà conservata nelle impostazioni come testo normale dopo averla cambiata o il riavvio dell\'app.</string> @@ -1407,14 +1384,14 @@ <string name="expand_verb">Espandi</string> <string name="connect_plan_repeat_connection_request">Ripetere la richiesta di connessione?</string> <string name="rcv_direct_event_contact_deleted">contatto eliminato</string> - <string name="connect_plan_you_are_already_connecting_to_vName">Ti stai già connettendo a %1$s.</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Ti stai già connettendo a <b>%1$s</b>.]]></string> <string name="error_alert_title">Errore</string> <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Stai già entrando nel gruppo tramite questo link.</string> <string name="create_group_button">Crea gruppo</string> <string name="create_another_profile_button">Crea profilo</string> <string name="group_members_2">%s e %s</string> <string name="connect_plan_join_your_group">Entrare nel tuo gruppo?</string> - <string name="connect_plan_you_are_already_joining_the_group_vName">Stai già entrando nel gruppo %1$s.</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Stai già entrando nel gruppo <b>%1$s</b>.]]></string> <string name="connect_plan_this_is_your_own_one_time_link">Questo è il tuo link una tantum!</string> <string name="marked_deleted_items_description">%d messaggi contrassegnati eliminati</string> <string name="connect_plan_group_already_exists">Il gruppo esiste già!</string> @@ -1425,11 +1402,11 @@ <string name="connect_plan_connect_via_link">Connettere via link?</string> <string name="connect_plan_already_joining_the_group">Stai già entrando nel gruppo!</string> <string name="group_members_n">%s, %s e %d membri</string> - <string name="moderated_items_description">%d messaggi moderati da %s</string> + <string name="moderated_items_description">%1$d messaggi moderati da %2$s</string> <string name="unblock_member_button">Sblocca membro</string> <string name="connect_plan_connect_to_yourself">Connettersi a te stesso?</string> <string name="contact_tap_to_connect">Tocca per connettere</string> - <string name="connect_plan_you_are_already_in_group_vName">Sei già nel gruppo %1$s.</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Sei già nel gruppo <b>%1$s</b>.]]></string> <string name="connect_plan_this_is_your_own_simplex_address">Questo è il tuo indirizzo SimpleX!</string> <string name="correct_name_to">Correggere il nome a %s?</string> <string name="delete_messages__question">Eliminare %d messaggi?</string> @@ -1450,7 +1427,7 @@ <string name="block_member_question">Bloccare il membro?</string> <string name="rcv_group_events_count">%d eventi del gruppo</string> <string name="invalid_name">Nome non valido!</string> - <string name="connect_plan_this_is_your_link_for_group_vName">Questo è il tuo link per il gruppo %1$s!</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Questo è il tuo link per il gruppo <b>%1$s</b>!]]></string> <string name="unblock_member_confirmation">Sblocca</string> <string name="non_content_uri_alert_title">Percorso file non valido</string> <string name="connect_plan_you_have_already_requested_connection_via_this_address">Hai già richiesto la connessione tramite questo indirizzo!</string> @@ -1469,7 +1446,7 @@ <string name="unlink_desktop_question">Scollegare il desktop?</string> <string name="linked_desktop_options">Opzioni del desktop collegato</string> <string name="linked_desktops">Desktop collegati</string> - <string name="discover_on_network">Trova nella rete</string> + <string name="discover_on_network">Individua via rete locale</string> <string name="this_device">Questo dispositivo</string> <string name="linked_mobiles">Cellulari collegati</string> <string name="desktop_device">Desktop</string> @@ -1517,4 +1494,901 @@ \n- e molto altro!</string> <string name="waiting_for_mobile_to_connect">In attesa che il cellulare si connette:</string> <string name="group_member_role_author">autore</string> -</resources> \ No newline at end of file + <string name="multicast_connect_automatically">Connetti automaticamente</string> + <string name="waiting_for_desktop">In attesa del desktop…</string> + <string name="found_desktop">Desktop trovato</string> + <string name="not_compatible">Non compatibile!</string> + <string name="multicast_discoverable_via_local_network">individuabile via rete locale</string> + <string name="refresh_qr_code">Ricarica</string> + <string name="create_chat_profile">Crea profilo di chat</string> + <string name="no_connected_mobile">Nessun cellulare connesso</string> + <string name="disconnect_remote_hosts">Disconnetti cellulari</string> + <string name="random_port">Casuale</string> + <string name="open_port_in_firewall_desc">Per consentire a un\'app mobile di connettersi al desktop, apri questa porta nel tuo firewall, se è attivo</string> + <string name="app_was_crashed">Vedi crash</string> + <string name="open_port_in_firewall_title">Apri porta nel firewall</string> + <string name="error_showing_content">errore di visualizzazione del contenuto</string> + <string name="error_showing_message">errore di visualizzazione del messaggio</string> + <string name="you_can_make_address_visible_via_settings">Puoi renderlo visibile ai tuoi contatti SimpleX nelle impostazioni.</string> + <string name="recent_history_is_not_sent_to_new_members">La cronologia non viene inviata ai nuovi membri.</string> + <string name="retry_verb">Riprova</string> + <string name="camera_not_available">Fotocamera non disponibile</string> + <string name="enable_sending_recent_history">Invia fino a 100 ultimi messaggi ai nuovi membri.</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Aggiungi contatto</b>: per creare un nuovo link di invito o connetterti tramite un link che hai ricevuto.]]></string> + <string name="disable_sending_recent_history">Non inviare la cronologia ai nuovi membri.</string> + <string name="or_show_this_qr_code">O mostra questo codice</string> + <string name="recent_history_is_sent_to_new_members">Vengono inviati ai nuovi membri fino a 100 ultimi messaggi.</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">Il codice che hai scansionato non è un codice QR di link SimpleX.</string> + <string name="the_text_you_pasted_is_not_a_link">Il testo che hai incollato non è un link SimpleX.</string> + <string name="enable_camera_access">Attiva l\'accesso alla fotocamera</string> + <string name="you_can_view_invitation_link_again">Puoi vedere di nuovo il link di invito nei dettagli di connessione.</string> + <string name="keep_unused_invitation_question">Tenere l\'invito inutilizzato?</string> + <string name="share_this_1_time_link">Condividi questo link di invito una tantum</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Crea gruppo</b>: per creare un nuovo gruppo.]]></string> + <string name="recent_history">Cronologia visibile</string> + <string name="la_app_passcode">Password dell\'app</string> + <string name="new_chat">Nuova chat</string> + <string name="loading_chats">Caricamento delle chat…</string> + <string name="creating_link">Creazione link…</string> + <string name="or_scan_qr_code">O scansiona il codice QR</string> + <string name="invalid_qr_code">Codice QR non valido</string> + <string name="add_contact_tab">Aggiungi contatto</string> + <string name="tap_to_scan">Tocca per scansionare</string> + <string name="keep_invitation_link">Tieni</string> + <string name="tap_to_paste_link">Tocca per incollare il link</string> + <string name="search_or_paste_simplex_link">Cerca o incolla un link SimpleX</string> + <string name="chat_is_stopped_you_should_transfer_database">La chat è ferma. Se hai già usato questo database su un altro dispositivo, dovresti trasferirlo prima di avviare la chat.</string> + <string name="start_chat_question">Avviare la chat?</string> + <string name="agent_critical_error_title">Errore critico</string> + <string name="agent_internal_error_title">Errore interno</string> + <string name="agent_critical_error_desc">Si prega di segnalarlo agli sviluppatori: +\n%s +\n +\nSi consiglia di riavviare l\'app.</string> + <string name="agent_internal_error_desc">Si prega di segnalarlo agli sviluppatori: +\n%s</string> + <string name="restart_chat_button">Riavvia la chat</string> + <string name="show_internal_errors">Mostra errori interni</string> + <string name="remote_host_disconnected_from"><![CDATA[Disconnesso dal telefono <b>%s</b> per il motivo: %s]]></string> + <string name="remote_ctrl_disconnected_with_reason">Disconnesso per il motivo: %s</string> + <string name="remote_host_was_disconnected_title">Connessione interrotta</string> + <string name="remote_ctrl_was_disconnected_title">Connessione interrotta</string> + <string name="remote_host_error_missing"><![CDATA[Telefono <b>%s</b> non trovato]]></string> + <string name="remote_ctrl_error_busy">Il desktop è occupato</string> + <string name="remote_ctrl_error_bad_version">Il desktop ha una versione non supportata. Assicurati di usare la stessa versione su entrambi i dispositivi</string> + <string name="failed_to_create_user_invalid_title">Nome da mostrare non valido!</string> + <string name="failed_to_create_user_invalid_desc">Questo nome da mostrare non è valido. Scegline un altro.</string> + <string name="remote_host_error_bad_state"><![CDATA[La connessione al telefono <b>%s</b> è in cattivo stato]]></string> + <string name="remote_host_error_bad_version"><![CDATA[Il telefono <b>%s</b> ha una versione non supportata. Assicurati di usare la stessa versione su entrambi i dispositivi]]></string> + <string name="remote_host_error_busy"><![CDATA[Il telefono <b>%s</b> è occupato]]></string> + <string name="remote_host_error_inactive"><![CDATA[Il telefono <b>%s</b> è inattivo]]></string> + <string name="remote_host_error_disconnected"><![CDATA[Il telefono <b>%s</b> è stato disconnesso]]></string> + <string name="remote_host_error_timeout"><![CDATA[Tempo scaduto durante la connessione al telefono <b>%s</b>]]></string> + <string name="remote_ctrl_error_bad_state">La connessione al desktop è in cattivo stato</string> + <string name="remote_ctrl_error_bad_invitation">Il desktop ha un codice di invito sbagliato</string> + <string name="remote_ctrl_error_inactive">Il desktop è inattivo</string> + <string name="remote_ctrl_error_disconnected">Il desktop è stato disconnesso</string> + <string name="remote_ctrl_error_timeout">Tempo scaduto durante la connessione al desktop</string> + <string name="past_member_vName">Membro passato %1$s</string> + <string name="possible_slow_function_desc">L\'esecuzione della funzione impiega troppo tempo: %1$d secondi: %2$s</string> + <string name="possible_slow_function_title">Funzione lenta</string> + <string name="show_slow_api_calls">Mostra chiamate API lente</string> + <string name="group_member_status_unknown_short">sconosciuto</string> + <string name="group_member_status_unknown">stato sconosciuto</string> + <string name="developer_options_section">Opzioni sviluppatore</string> + <string name="v5_5_private_notes">Note private</string> + <string name="v5_5_new_interface_languages">Interfaccia in ungherese e turco</string> + <string name="v5_5_join_group_conversation_descr">Cronologia recente e bot della directory migliorato.</string> + <string name="info_row_created_at">Creato il</string> + <string name="clear_note_folder_warning">Tutti i messaggi verranno eliminati, non è reversibile!</string> + <string name="share_text_created_at">Creato il: %s</string> + <string name="error_creating_message">Errore di creazione del messaggio</string> + <string name="error_deleting_note_folder">Errore di eliminazione delle note private</string> + <string name="v5_5_message_delivery">Consegna dei messaggi migliorata</string> + <string name="v5_5_join_group_conversation">Entra in conversazioni di gruppo</string> + <string name="profile_update_event_member_name_changed">il membro %1$s è diventato %2$s</string> + <string name="v5_5_simpler_connect_ui">Incolla un link per connettere!</string> + <string name="profile_update_event_removed_address">indirizzo di contatto rimosso</string> + <string name="profile_update_event_contact_name_changed">contatto %1$s cambiato in %2$s</string> + <string name="note_folder_local_display_name">Note private</string> + <string name="clear_note_folder_question">Svuotare le note private?</string> + <string name="profile_update_event_removed_picture">immagine del profilo rimossa</string> + <string name="v5_5_private_notes_descr">Con file e multimediali criptati.</string> + <string name="v5_5_simpler_connect_ui_descr">La barra di ricerca accetta i link di invito.</string> + <string name="profile_update_event_set_new_picture">ha impostato una nuova immagine del profilo</string> + <string name="profile_update_event_set_new_address">impostato nuovo indirizzo di contatto</string> + <string name="profile_update_event_updated_profile">profilo aggiornato</string> + <string name="saved_message_title">Messaggio salvato</string> + <string name="v5_5_message_delivery_descr">Con consumo di batteria ridotto.</string> + <string name="blocked_by_admin_items_description">%d messaggi bloccati dall\'amministratore</string> + <string name="rcv_group_event_member_blocked">ha bloccato %s</string> + <string name="rcv_group_event_member_unblocked">ha sbloccato %s</string> + <string name="snd_group_event_member_unblocked">hai sbloccato %s</string> + <string name="unblock_for_all">Sblocca per tutti</string> + <string name="unblock_for_all_question">Sbloccare il membro per tutti?</string> + <string name="block_for_all">Blocca per tutti</string> + <string name="member_info_member_blocked">bloccato</string> + <string name="blocked_by_admin_item_description">bloccato dall\'amministratore</string> + <string name="member_blocked_by_admin">Bloccato dall\'amministratore</string> + <string name="block_for_all_question">Bloccare il membro per tutti?</string> + <string name="error_blocking_member_for_all">Errore nel blocco del membro per tutti</string> + <string name="snd_group_event_member_blocked">hai bloccato %s</string> + <string name="welcome_message_is_too_long">Il messaggio di benvenuto è troppo lungo</string> + <string name="message_too_large">Messaggio troppo grande</string> + <string name="database_migration_in_progress">Migrazione del database in corso.\nPuò richiedere qualche minuto.</string> + <string name="call_service_notification_audio_call">Chiamata audio</string> + <string name="call_service_notification_end_call">Termina chiamata</string> + <string name="call_service_notification_video_call">Videochiamata</string> + <string name="unable_to_open_browser_title">Errore di apertura del browser</string> + <string name="unable_to_open_browser_desc">Il browser predefinito è necessario per le chiamate. Configura il browser predefinito nel sistema, poi condividi più informazioni con gli sviluppatori.</string> + <string name="e2ee_info_no_pq_short">Questa chat è protetta da crittografia end-to-end.</string> + <string name="e2ee_info_pq_short">Questa chat è protetta da crittografia end-to-end resistente alla quantistica.</string> + <string name="migrate_from_device_start_chat">Avvia chat</string> + <string name="migrate_to_device_title">Migra qui</string> + <string name="migrate_to_device_downloading_details">Scaricamento dettagli del link</string> + <string name="migrate_to_device_downloading_archive">Scaricamento archivio</string> + <string name="migrate_from_device_error_uploading_archive">Errore di invio dell\'archivio</string> + <string name="migrate_from_device_archive_and_upload">Archivia e carica</string> + <string name="v5_6_safer_groups_descr">Gli amministratori possono bloccare un membro per tutti.</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Tutti i tuoi contatti, le conversazioni e i file verranno criptati in modo sicuro e caricati in blocchi sui relay XFTP configurati.</string> + <string name="v5_6_app_data_migration">Migrazione dati dell\'app</string> + <string name="migrate_to_device_apply_onion">Applica</string> + <string name="migrate_from_device_archiving_database">Archiviazione del database</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Nota bene</b>: usare lo stesso database su due dispositivi bloccherà la decifrazione dei messaggi dalle tue connessioni, come misura di sicurezza.]]></string> + <string name="migrate_from_device_cancel_migration">Annulla migrazione</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Attenzione</b>: l\'archivio verrà eliminato.]]></string> + <string name="migrate_to_device_chat_migrated">Chat migrata!</string> + <string name="migrate_from_device_check_connection_and_try_again">Controlla la tua connessione internet e riprova</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Scegli <i>Migra da un altro dispositivo</i> sul nuovo dispositivo e scansione il codice QR]]></string> + <string name="migrate_to_device_confirm_network_settings">Conferma le impostazioni di rete</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Conferma che ricordi la password del database da migrare.</string> + <string name="migrate_from_device_confirm_upload">Conferma caricamento</string> + <string name="migrate_from_device_creating_archive_link">Creazione link dell\'archivio</string> + <string name="migrate_from_device_delete_database_from_device">Elimina il database da questo dispositivo</string> + <string name="migrate_to_device_download_failed">Scaricamento fallito</string> + <string name="v5_6_quantum_resistant_encryption_descr">Attivala nelle chat dirette (BETA)!</string> + <string name="migrate_to_device_enter_passphrase">Inserisci password</string> + <string name="migrate_from_device_error_deleting_database">Errore di eliminazione del database</string> + <string name="migrate_to_device_error_downloading_archive">Errore di scaricamento dell\'archivio</string> + <string name="migrate_from_device_error_exporting_archive">Errore di esportazione del database della chat</string> + <string name="migrate_from_device_error_saving_settings">Errore di salvataggio delle impostazioni</string> + <string name="migrate_from_device_error_verifying_passphrase">Errore di verifica della password:</string> + <string name="migrate_from_device_exported_file_doesnt_exist">Il file esportato non esiste</string> + <string name="migrate_to_device_file_delete_or_link_invalid">Il file è stato eliminato o il link non è valido</string> + <string name="migrate_to_device_import_failed">Importazione fallita</string> + <string name="migrate_from_device_finalize_migration">Finalizza la migrazione</string> + <string name="migrate_from_device_chat_should_be_stopped">Per continuare, la chat deve essere fermata.</string> + <string name="migrate_to_device_finalize_migration">Finalizza la migrazione su un altro dispositivo.</string> + <string name="migrate_to_device_importing_archive">Importazione archivio</string> + <string name="invalid_file_link">Link non valido</string> + <string name="e2ee_info_no_pq"><![CDATA[I messaggi, i file e le chiamate sono protetti da <b>crittografia end-to-end</b> con perfect forward secrecy, ripudio e recupero da intrusione.]]></string> + <string name="e2ee_info_pq"><![CDATA[I messaggi, i file e le chiamate sono protetti da <b>crittografia e2e resistente alla quantistica</b> con perfect forward secrecy, ripudio e recupero da intrusione.]]></string> + <string name="migrate_from_device_title">Migra dispositivo</string> + <string name="migrate_from_another_device">Migra da un altro dispositivo</string> + <string name="migrate_from_device_to_another_device">Migra ad un altro dispositivo</string> + <string name="v5_6_app_data_migration_descr">Migra ad un altro dispositivo via codice QR.</string> + <string name="migrate_to_device_migrating">Migrazione</string> + <string name="migrate_from_device_migration_complete">Migrazione completata</string> + <string name="auth_open_migration_to_another_device">Apri la schermata di migrazione</string> + <string name="or_paste_archive_link">O incolla il link dell\'archivio</string> + <string name="migrate_from_device_or_share_this_file_link">O condividi in modo sicuro questo link del file</string> + <string name="paste_archive_link">Incolla link dell\'archivio</string> + <string name="v5_6_picture_in_picture_calls">Chiamate picture-in-picture</string> + <string name="migrate_to_device_confirm_network_settings_footer">Conferma che le impostazioni di rete sono corrette per questo dispositivo.</string> + <string name="migrate_from_device_database_init">Preparazione del caricamento</string> + <string name="conn_event_enabled_pq">crittografia e2e resistente alla quantistica</string> + <string name="migrate_to_device_repeat_import">Ripeti importazione</string> + <string name="migrate_to_device_database_init">Preparazione dello scaricamento</string> + <string name="v5_6_quantum_resistant_encryption">Crittografia resistente alla quantistica</string> + <string name="migrate_to_device_repeat_download">Ripeti scaricamento</string> + <string name="migrate_from_device_repeat_upload">Ripeti caricamento</string> + <string name="v5_6_safer_groups">Gruppi più sicuri</string> + <string name="migrate_to_device_bytes_downloaded">%s scaricati</string> + <string name="conn_event_disabled_pq">crittografia end-to-end standard</string> + <string name="set_passphrase">Imposta password</string> + <string name="migrate_from_device_stopping_chat">Arresto della chat</string> + <string name="migrate_from_device_bytes_uploaded">%s caricati</string> + <string name="migrate_from_device_upload_failed">Invio fallito</string> + <string name="migrate_from_device_uploading_archive">Invio dell\'archivio</string> + <string name="v5_6_picture_in_picture_calls_descr">Usa l\'app mentre sei in chiamata.</string> + <string name="migrate_from_device_verify_passphrase">Verifica password</string> + <string name="migrate_from_device_verify_database_passphrase">Verifica password del database</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Attenzione: avviare la chat su più dispositivi non è supportato e provocherà problemi di recapito dei messaggi</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[<b>Non devi</b> usare lo stesso database su due dispositivi.]]></string> + <string name="migrate_to_device_try_again">Puoi fare un altro tentativo.</string> + <string name="migrate_from_device_try_again">Puoi fare un altro tentativo.</string> + <string name="error_showing_desktop_notification">Errore nel mostrare la notifica, contatta gli sviluppatori.</string> + <string name="permissions_find_in_settings_and_grant">Trova questa autorizzazione nelle impostazioni di Android e concedila a mano.</string> + <string name="permissions_camera">Fotocamera</string> + <string name="permissions_camera_and_record_audio">Fotocamera e microfono</string> + <string name="permissions_required">Concedi autorizzazioni</string> + <string name="permissions_grant_in_settings">Concedi nelle impostazioni</string> + <string name="permissions_grant">Concedi la/le autorizzazione/i per effettuare chiamate</string> + <string name="permissions_record_audio">Microfono</string> + <string name="permissions_open_settings">Apri le impostazioni</string> + <string name="forwarded_description">inoltrato</string> + <string name="settings_section_title_network_connection">Connessione di rete</string> + <string name="network_type_no_network_connection">Nessuna connessione di rete</string> + <string name="network_type_cellular">Mobile</string> + <string name="network_type_other">Altro</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="network_type_ethernet">Cavo ethernet</string> + <string name="simplex_links_not_allowed">Link di SimpleX non consentiti</string> + <string name="files_and_media_not_allowed">File e multimediali non consentiti</string> + <string name="voice_messages_not_allowed">Messaggi vocali non consentiti</string> + <string name="simplex_links">Link di SimpleX</string> + <string name="allow_to_send_simplex_links">Consenti di inviare link di SimpleX.</string> + <string name="prohibit_sending_simplex_links">Vieta l\'invio di link di SimpleX</string> + <string name="feature_enabled_for">Attivo per</string> + <string name="group_members_can_send_simplex_links">I membri possono inviare link di Simplex.</string> + <string name="feature_roles_owners">proprietari</string> + <string name="feature_roles_admins">amministratori</string> + <string name="feature_roles_all_members">tutti i membri</string> + <string name="simplex_links_are_prohibited_in_group">I link di SimpleX sono vietati.</string> + <string name="saved_description">salvato</string> + <string name="saved_from_chat_item_info_title">Salvato da</string> + <string name="forward_message">Inoltra messaggio…</string> + <string name="forwarded_chat_item_info_tab">Inoltrato</string> + <string name="forwarded_from_chat_item_info_title">Inoltrato da</string> + <string name="download_file">Scarica</string> + <string name="forward_chat_item">Inoltra</string> + <string name="recipients_can_not_see_who_message_from">I destinatari non possono vedere da chi proviene questo messaggio.</string> + <string name="saved_chat_item_info_tab">Salvato</string> + <string name="saved_from_description">salvato da %s</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="audio_device_earpiece">Auricolari</string> + <string name="audio_device_wired_headphones">Cuffie</string> + <string name="audio_device_speaker">Altoparlante</string> + <string name="network_option_rcv_concurrency">Ricezione concomitanza</string> + <string name="v5_7_quantum_resistant_encryption_descr">Viene attivata nelle chat dirette!</string> + <string name="v5_7_forward">Inoltra e salva i messaggi</string> + <string name="v5_7_call_sounds_descr">Quando si connettono le chiamate audio e video.</string> + <string name="v5_7_new_interface_languages">Interfaccia in lituano</string> + <string name="v5_7_network_descr">Connessione di rete più affidabile.</string> + <string name="v5_7_network">Gestione della rete</string> + <string name="settings_section_title_profile_images">Immagini del profilo</string> + <string name="v5_7_call_sounds">Suoni nelle chiamate</string> + <string name="v5_7_forward_descr">La fonte del messaggio resta privata.</string> + <string name="v5_7_shape_profile_images">Forma delle immagini del profilo</string> + <string name="v5_7_shape_profile_images_descr">Quadrata, circolare o qualsiasi forma tra le due</string> + <string name="snd_error_proxy_relay">Server di inoltro: %1$s +\nErrore del server di destinazione: %2$s</string> + <string name="snd_error_proxy">Server di inoltro: %1$s +\nErrore: %2$s</string> + <string name="snd_error_expired">Problemi di rete - messaggio scaduto dopo molti tentativi di inviarlo.</string> + <string name="srv_error_host">L\'indirizzo del server non è compatibile con le impostazioni di rete.</string> + <string name="srv_error_version">La versione del server non è compatibile con le impostazioni di rete.</string> + <string name="snd_error_auth">Chiave sbagliata o connessione sconosciuta - molto probabilmente questa connessione è stata eliminata.</string> + <string name="snd_error_relay">Errore del server di destinazione: %1$s</string> + <string name="ci_status_other_error">Errore: %1$s</string> + <string name="snd_error_quota">Quota superata - il destinatario non ha ricevuto i messaggi precedentemente inviati.</string> + <string name="message_delivery_warning_title">Avviso di consegna del messaggio</string> + <string name="network_smp_proxy_mode_private_routing">Instradamento privato</string> + <string name="network_smp_proxy_mode_never">Mai</string> + <string name="network_smp_proxy_mode_unknown">Server sconosciuti</string> + <string name="network_smp_proxy_mode_unknown_description">Usa l\'instradamento privato con server sconosciuti.</string> + <string name="update_network_smp_proxy_mode_question">Modalità instradamento messaggio</string> + <string name="network_smp_proxy_mode_unprotected_description">Usa l\'instradamento privato con server sconosciuti quando l\'indirizzo IP non è protetto.</string> + <string name="network_smp_proxy_fallback_allow">Sì</string> + <string name="network_smp_proxy_fallback_allow_description">Invia messaggi direttamente quando il tuo server o quello di destinazione non supporta l\'instradamento privato.</string> + <string name="network_smp_proxy_fallback_allow_protected">Quando l\'IP è nascosto</string> + <string name="update_network_smp_proxy_fallback_question">Ripiego instradamento messaggio</string> + <string name="private_routing_show_message_status">Mostra stato del messaggio</string> + <string name="network_smp_proxy_fallback_allow_downgrade">Consenti downgrade</string> + <string name="network_smp_proxy_mode_always">Sempre</string> + <string name="network_smp_proxy_mode_always_description">Usa sempre l\'instradamento privato.</string> + <string name="network_smp_proxy_fallback_prohibit_description">NON inviare messaggi direttamente, anche se il tuo server o quello di destinazione non supporta l\'instradamento privato.</string> + <string name="network_smp_proxy_mode_never_description">NON usare l\'instradamento privato.</string> + <string name="network_smp_proxy_fallback_prohibit">No</string> + <string name="settings_section_title_private_message_routing">INSTRADAMENTO PRIVATO DEI MESSAGGI</string> + <string name="network_smp_proxy_fallback_allow_protected_description">Invia messaggi direttamente quando l\'indirizzo IP è protetto e il tuo server o quello di destinazione non supporta l\'instradamento privato.</string> + <string name="private_routing_explanation">Per proteggere il tuo indirizzo IP, l\'instradamento privato usa i tuoi server SMP per consegnare i messaggi.</string> + <string name="network_smp_proxy_mode_unprotected">Non protetto</string> + <string name="file_not_approved_title">Server sconosciuti!</string> + <string name="protect_ip_address">Proteggi l\'indirizzo IP</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">L\'app chiederà di confermare i download da server di file sconosciuti (eccetto .onion o quando il proxy SOCKS è attivo).</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Senza Tor o VPN, il tuo indirizzo IP sarà visibile ai server di file.</string> + <string name="settings_section_title_files">FILE</string> + <string name="file_not_approved_descr">Senza Tor o VPN, il tuo indirizzo IP sarà visibile a questi relay XFTP: +\n%1$s.</string> + <string name="settings_section_title_chat_theme">Tema della chat</string> + <string name="theme_black">Nero</string> + <string name="color_mode">Modalità di colore</string> + <string name="color_mode_dark">Scura</string> + <string name="chat_theme_apply_to_dark_mode">Modalità scura</string> + <string name="color_primary_variant2">Principale aggiuntivo 2</string> + <string name="chat_theme_apply_to_all_modes">Tutte le modalità di colore</string> + <string name="chat_theme_apply_to_mode">Applica a</string> + <string name="dark_mode_colors">Colori modalità scura</string> + <string name="settings_section_title_user_theme">Tema del profilo</string> + <string name="color_received_quote">Risposta ricevuta</string> + <string name="settings_section_title_chat_colors">Colori della chat</string> + <string name="wallpaper_scale_fill">Riempi</string> + <string name="color_mode_light">Chiara</string> + <string name="chat_theme_apply_to_light_mode">Modalità chiara</string> + <string name="reset_single_color">Ripristina colore</string> + <string name="wallpaper_advanced_settings">Impostazioni avanzate</string> + <string name="theme_remove_image">Rimuovi immagine</string> + <string name="wallpaper_scale_repeat">Ripeti</string> + <string name="wallpaper_scale">Scala</string> + <string name="wallpaper_scale_fit">Adatta</string> + <string name="color_sent_quote">Risposta inviata</string> + <string name="chat_theme_set_default_theme">Imposta tema predefinito</string> + <string name="chat_list_always_visible">Mostra la lista di chat in una nuova finestra</string> + <string name="color_mode_system">Sistema</string> + <string name="color_wallpaper_tint">Tinta dello sfondo</string> + <string name="wallpaper_preview_hello_alice">Buon pomeriggio!</string> + <string name="wallpaper_preview_hello_bob">Buongiorno!</string> + <string name="color_wallpaper_background">Retro dello sfondo</string> + <string name="v5_8_private_routing">Instradamento privato dei messaggi 🚀</string> + <string name="v5_8_private_routing_descr">Proteggi il tuo indirizzo IP dai relay di messaggistica scelti dai tuoi contatti. +\nAttivalo nelle impostazioni *Rete e server*.</string> + <string name="error_initializing_web_view">Errore di inizializzazione di WebView. Aggiorna il sistema ad una nuova versione. Contatta gli sviluppatori. +\nErrore: %s</string> + <string name="theme_destination_app_theme">Tema dell\'app</string> + <string name="chat_theme_reset_to_app_theme">Ripristina al tema dell\'app</string> + <string name="chat_theme_reset_to_user_theme">Ripristina al tema dell\'utente</string> + <string name="v5_8_safe_files_descr">Conferma i file da server sconosciuti.</string> + <string name="v5_8_message_delivery">Consegna dei messaggi migliorata</string> + <string name="v5_8_chat_themes_descr">Cambia l\'aspetto delle tue chat!</string> + <string name="v5_8_chat_themes">Nuovi temi delle chat</string> + <string name="v5_8_persian_ui">Interfaccia in persiano</string> + <string name="v5_8_safe_files">Ricevi i file in sicurezza</string> + <string name="v5_8_message_delivery_descr">Con consumo di batteria ridotto.</string> + <string name="message_queue_info">Info coda messaggi</string> + <string name="message_queue_info_none">nessuna</string> + <string name="message_queue_info_server_info">info coda server: %1$s +\n +\nultimo msg ricevuto: %2$s</string> + <string name="info_row_debug_delivery">Debug della consegna</string> + <string name="share_text_message_status">Stato del messaggio: %s</string> + <string name="share_text_file_status">Stato del file: %s</string> + <string name="file_error_auth">Chiave sbagliata o indirizzo sconosciuto per frammento del file - probabilmente il file è stato eliminato.</string> + <string name="file_error_no_file">File non trovato - probabilmente è stato eliminato o annullato.</string> + <string name="info_row_message_status">Stato del messaggio</string> + <string name="info_row_file_status">Stato del file</string> + <string name="file_error_relay">Errore del server del file: %1$s</string> + <string name="file_error">Errore del file</string> + <string name="temporary_file_error">Errore del file temporaneo</string> + <string name="remote_ctrl_connection_stopped_desc">Controlla che mobile e desktop siano collegati alla stessa rete locale e che il firewall del desktop consenta la connessione. +\nSi prega di condividere qualsiasi altro problema con gli sviluppatori.</string> + <string name="remote_ctrl_connection_stopped_identity_desc">Questo link è stato usato con un altro dispositivo mobile, creane uno nuovo sul desktop.</string> + <string name="copy_error">Copia errore</string> + <string name="cannot_share_message_alert_title">Impossibile inviare il messaggio</string> + <string name="cannot_share_message_alert_text">Le preferenze della chat selezionata vietano questo messaggio.</string> + <string name="connections">Connessioni</string> + <string name="servers_info_subscriptions_connections_subscribed">Connessioni attive</string> + <string name="created">Creato</string> + <string name="decryption_errors">errori di decifrazione</string> + <string name="servers_info_detailed_statistics">Statistiche dettagliate</string> + <string name="duplicates_label">doppi</string> + <string name="servers_info_modal_error_title">Errore</string> + <string name="servers_info_reconnect_server_error">Errore di riconnessione al server</string> + <string name="servers_info_reconnect_servers_error">Errore di riconnessione ai server</string> + <string name="expired_label">scaduto</string> + <string name="servers_info_subscriptions_section_header">Ricezione messaggi</string> + <string name="other_label">altro</string> + <string name="other_errors">altri errori</string> + <string name="servers_info_subscriptions_connections_pending">In attesa</string> + <string name="proxied">Via proxy</string> + <string name="servers_info_proxied_servers_section_header">Server via proxy</string> + <string name="servers_info_detailed_statistics_received_total">Totale ricevuto</string> + <string name="servers_info_detailed_statistics_receive_errors">Errori di ricezione</string> + <string name="reconnect">Riconnetti</string> + <string name="servers_info_reconnect_servers_message">Riconnetti tutti i server connessi per forzare la consegna dei messaggi. Usa traffico aggiuntivo.</string> + <string name="servers_info_reconnect_server_title">Riconnettere il server?</string> + <string name="servers_info_reconnect_servers_title">Riconnettere i server?</string> + <string name="servers_info_reconnect_server_message">Riconnetti il server per forzare la consegna dei messaggi. Usa traffico aggiuntivo.</string> + <string name="send_errors">Errori di invio</string> + <string name="sent_directly">Inviato direttamente</string> + <string name="servers_info_detailed_statistics_sent_messages_header">Messaggi inviati</string> + <string name="sent_via_proxy">Inviato via proxy</string> + <string name="servers_info_reset_stats_alert_message">Le statistiche dei server verranno azzerate - è irreversibile!</string> + <string name="servers_info_reset_stats">Azzera tutte le statistiche</string> + <string name="servers_info_reset_stats_alert_title">Azzerare tutte le statistiche?</string> + <string name="servers_info_reset_stats_alert_confirm">Azzera</string> + <string name="servers_info_reset_stats_alert_error_title">Errore di azzeramento statistiche</string> + <string name="smp_server">Server SMP</string> + <string name="servers_info_starting_from">Inizio da %s.</string> + <string name="servers_info_subscriptions_total">Totale</string> + <string name="servers_info_uploaded">Inviato</string> + <string name="xftp_server">Server XFTP</string> + <string name="acknowledged">Riconosciuto</string> + <string name="acknowledgement_errors">Errori di riconoscimento</string> + <string name="chunks_deleted">Blocchi eliminati</string> + <string name="chunks_downloaded">Blocchi scaricati</string> + <string name="chunks_uploaded">Blocchi inviati</string> + <string name="deleted">Eliminato</string> + <string name="deletion_errors">Errori di eliminazione</string> + <string name="downloaded_files">File scaricati</string> + <string name="open_server_settings_button">Apri impostazioni server</string> + <string name="secured">Protetto</string> + <string name="server_address">Indirizzo server</string> + <string name="size">Dimensione</string> + <string name="subscribed">Iscritto</string> + <string name="subscription_errors">Errori di iscrizione</string> + <string name="subscription_results_ignored">Iscrizioni ignorate</string> + <string name="uploaded_files">File inviati</string> + <string name="upload_errors">Errori di invio</string> + <string name="private_routing_error">Errore di instradamento privato</string> + <string name="network_error_broker_version_desc">La versione del server non è compatibile con la tua app: %1$s.</string> + <string name="member_inactive_title">Membro inattivo</string> + <string name="member_inactive_desc">Il messaggio può essere consegnato più tardi se il membro diventa attivo.</string> + <string name="smp_servers_configured">Server SMP configurati</string> + <string name="smp_servers_other">Altri server SMP</string> + <string name="subscription_percentage">Mostra percentuale</string> + <string name="member_info_member_inactive">inattivo</string> + <string name="appearance_zoom">Zoom</string> + <string name="servers_info_sessions_connected">Connesso</string> + <string name="servers_info_sessions_connecting">In connessione</string> + <string name="current_user">Profilo attuale</string> + <string name="servers_info_details">Dettagli</string> + <string name="servers_info_sessions_errors">Errori</string> + <string name="servers_info_messages_received">Messaggi ricevuti</string> + <string name="servers_info_messages_sent">Messaggi inviati</string> + <string name="servers_info_missing">Nessuna informazione, prova a ricaricare</string> + <string name="servers_info">Info dei server</string> + <string name="servers_info_target">Informazioni di</string> + <string name="servers_info_statistics_section_header">Statistiche</string> + <string name="servers_info_transport_sessions_section_header">Sessioni di trasporto</string> + <string name="all_users">Tutti i profili</string> + <string name="attempts_label">tentativi</string> + <string name="xftp_servers_configured">Server XFTP configurati</string> + <string name="completed">Completato</string> + <string name="servers_info_connected_servers_section_header">Server connessi</string> + <string name="member_info_member_disabled">disattivato</string> + <string name="servers_info_downloaded">Scaricato</string> + <string name="servers_info_detailed_statistics_received_messages_header">Messaggi ricevuti</string> + <string name="download_errors">Errori di scaricamento</string> + <string name="servers_info_reconnect_all_servers_button">Riconnetti tutti i server</string> + <string name="network_error_broker_host_desc">L\'indirizzo del server non è compatibile con le impostazioni di rete: %1$s.</string> + <string name="servers_info_files_tab">File</string> + <string name="scan_paste_link">Scansiona / Incolla link</string> + <string name="appearance_font_size">Dimensione carattere</string> + <string name="servers_info_detailed_statistics_sent_messages_total">Totale inviato</string> + <string name="message_forwarded_title">Messaggio inoltrato</string> + <string name="servers_info_private_data_disclaimer">Partendo da %s. \nTutti i dati sono privati, nel tuo dispositivo.</string> + <string name="message_forwarded_desc">Ancora nessuna connessione diretta, il messaggio viene inoltrato dall\'amministratore.</string> + <string name="servers_info_proxied_servers_section_footer">Non sei connesso/a a questi server. L\'instradamento privato è usato per consegnare loro i messaggi.</string> + <string name="xftp_servers_other">Altri server XFTP</string> + <string name="servers_info_previously_connected_servers_section_header">Server precedentemente connessi</string> + <string name="please_try_later">Riprova più tardi.</string> + <string name="app_check_for_updates_beta">Beta</string> + <string name="app_check_for_updates_notice_disable">Disattiva</string> + <string name="app_check_for_updates_disabled">Disattivato</string> + <string name="app_check_for_updates_download_started">Scaricamento dell\'aggiornamento, non chiudere l\'app</string> + <string name="app_check_for_updates_button_download">Scarica %s (%s)</string> + <string name="app_check_for_updates_installed_successfully_title">Installato correttamente</string> + <string name="app_check_for_updates_button_open">Apri percorso file</string> + <string name="app_check_for_updates_installed_successfully_desc">Riavvia l\'app.</string> + <string name="app_check_for_updates_button_remind_later">Ricordamelo più tardi</string> + <string name="app_check_for_updates_button_skip">Salta questa versione</string> + <string name="app_check_for_updates_stable">Stabile</string> + <string name="app_check_for_updates_notice_desc">Per essere avvisato sulle nuove versioni, attiva il controllo periodico di versioni stabili o beta.</string> + <string name="app_check_for_updates_update_available">Aggiornamento disponibile: %s</string> + <string name="app_check_for_updates_canceled">Scaricamento aggiornamento annullato</string> + <string name="app_check_for_updates">Cerca aggiornamenti</string> + <string name="app_check_for_updates_download_completed_title">Aggiornamento dell\'app scaricato</string> + <string name="app_check_for_updates_notice_title">Cerca aggiornamenti</string> + <string name="app_check_for_updates_button_install">Installa aggiornamento</string> + <string name="proxy_destination_error_failed_to_connect">Il server di inoltro %1$s non è riuscito a connettersi al server di destinazione %2$s. Riprova più tardi.</string> + <string name="smp_proxy_error_broker_host">L\'indirizzo del server di inoltro è incompatibile con le impostazioni di rete: %1$s.</string> + <string name="proxy_destination_error_broker_host">L\'indirizzo del server di destinazione di %1$s è incompatibile con le impostazioni del server di inoltro %2$s.</string> + <string name="proxy_destination_error_broker_version">La versione del server di destinazione di %1$s è incompatibile con il server di inoltro %2$s.</string> + <string name="smp_proxy_error_connecting">Errore di connessione al server di inoltro %1$s. Riprova più tardi.</string> + <string name="smp_proxy_error_broker_version">La versione server di inoltro è incompatibile con le impostazioni di rete: %1$s.</string> + <string name="privacy_media_blur_radius_off">Off</string> + <string name="privacy_media_blur_radius">Sfocatura dei file multimediali</string> + <string name="privacy_media_blur_radius_soft">Leggera</string> + <string name="privacy_media_blur_radius_medium">Media</string> + <string name="privacy_media_blur_radius_strong">Forte</string> + <string name="info_view_call_button">chiama</string> + <string name="info_view_message_button">messaggio</string> + <string name="info_view_open_button">apri</string> + <string name="info_view_search_button">cerca</string> + <string name="toolbar_settings">Impostazioni</string> + <string name="delete_without_notification">Elimina senza avvisare</string> + <string name="keep_conversation">Tieni la conversazione</string> + <string name="only_delete_conversation">Elimina solo la conversazione</string> + <string name="you_can_still_send_messages_to_contact">Puoi inviare messaggi a %1$s dai contatti archiviati.</string> + <string name="deleted_chats">Contatti archiviati</string> + <string name="no_filtered_contacts">Nessun contatto filtrato</string> + <string name="paste_link">Incolla link</string> + <string name="contact_list_header_title">I tuoi contatti</string> + <string name="one_hand_ui">Barre degli strumenti dell\'app accessibili</string> + <string name="action_button_add_members">Invita</string> + <string name="allow_calls_question">Consentire le chiamate?</string> + <string name="calls_prohibited_alert_title">Chiamate proibite!</string> + <string name="cant_call_contact_alert_title">Impossibile chiamare il contatto</string> + <string name="cant_call_member_alert_title">Impossibile chiamare il membro del gruppo</string> + <string name="cant_call_contact_connecting_wait_alert_text">In collegamento con il contatto, attendi o controlla più tardi!</string> + <string name="cant_call_contact_deleted_alert_text">Il contatto è stato eliminato.</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">Chiedi al contatto di attivare le chiamate.</string> + <string name="you_need_to_allow_calls">Devi consentire le chiamate al tuo contatto per poterlo chiamare.</string> + <string name="cant_send_message_to_member_alert_title">Impossibile inviare messaggi al membro del gruppo</string> + <string name="delete_contact_cannot_undo_warning">Il contatto verrà eliminato - non è reversibile!</string> + <string name="conversation_deleted">Conversazione eliminata!</string> + <string name="cant_call_member_send_message_alert_text">Invia un messaggio per attivare le chiamate.</string> + <string name="info_view_video_button">video</string> + <string name="you_can_still_view_conversation_with_contact">Puoi ancora vedere la conversazione con %1$s nell\'elenco delle chat.</string> + <string name="info_view_connect_button">connetti</string> + <string name="contact_deleted">Contatto eliminato!</string> + <string name="confirm_delete_contact_question">Confermare l\'eliminazione del contatto?</string> + <string name="compose_message_placeholder">Messaggio</string> + <string name="selected_chat_items_nothing_selected">Nessuna selezione</string> + <string name="select_verb">Seleziona</string> + <string name="selected_chat_items_selected_n">Selezionato %d</string> + <string name="moderate_messages_will_be_deleted_warning">I messaggi verranno eliminati per tutti i membri.</string> + <string name="moderate_messages_will_be_marked_warning">I messaggi verranno contrassegnati come moderati per tutti i membri.</string> + <string name="delete_members_messages__question">Eliminare %d messaggi dei membri?</string> + <string name="delete_messages_mark_deleted_warning">I messaggi saranno contrassegnati per l\'eliminazione. Il/I destinatario/i sarà/saranno in grado di rivelare questi messaggi.</string> + <string name="chat_database_exported_title">Database della chat esportato</string> + <string name="chat_database_exported_continue">Continua</string> + <string name="media_and_file_servers">Server di multimediali e file</string> + <string name="message_servers">Server dei messaggi</string> + <string name="network_socks_proxy">Proxy SOCKS</string> + <string name="chat_database_exported_not_all_files">Alcuni file non sono stati esportati</string> + <string name="chat_database_exported_migrate">Puoi migrare il database esportato.</string> + <string name="chat_database_exported_save">Puoi salvare l\'archivio esportato.</string> + <string name="network_option_tcp_connection">Connessione TCP</string> + <string name="v6_0_connect_faster_descr">Connettiti più velocemente ai tuoi amici</string> + <string name="v6_0_connection_servers_status">Prendi il controllo della tua rete</string> + <string name="v6_0_delete_many_messages_descr">Elimina fino a 20 messaggi contemporaneamente.</string> + <string name="v6_0_reachable_chat_toolbar">Barra degli strumenti di chat accessibile</string> + <string name="v6_0_your_contacts_descr">Archivia contatti per chattare più tardi.</string> + <string name="v6_0_reachable_chat_toolbar_descr">Usa l\'app con una mano sola.</string> + <string name="v6_0_connection_servers_status_descr">Stato della connessione e dei server.</string> + <string name="v6_0_private_routing_descr">Protegge il tuo indirizzo IP e le connessioni.</string> + <string name="network_options_save_and_reconnect">Salva e riconnetti</string> + <string name="reset_all_hints">Ripristina tutti i suggerimenti</string> + <string name="one_hand_ui_card_title">Cambia l\'elenco delle chat:</string> + <string name="one_hand_ui_change_instruction">Puoi cambiarlo nelle impostazioni dell\'aspetto.</string> + <string name="v6_0_chat_list_media">Riproduci dall\'elenco delle chat.</string> + <string name="v6_0_increase_font_size">Aumenta la dimensione dei caratteri.</string> + <string name="v6_0_upgrade_app">Aggiorna l\'app automaticamente</string> + <string name="invite_friends_short">Invita</string> + <string name="v6_0_privacy_blur">Sfoca per una privacy maggiore.</string> + <string name="v6_0_new_chat_experience">Una nuova esperienza di chat 🎉</string> + <string name="v6_0_new_media_options">Nuove opzioni multimediali</string> + <string name="create_address_button">Crea</string> + <string name="v6_0_upgrade_app_descr">Scarica nuove versioni da GitHub.</string> + <string name="new_message">Nuovo messaggio</string> + <string name="error_parsing_uri_title">Link non valido</string> + <string name="error_parsing_uri_desc">Controlla che il link SimpleX sia corretto.</string> + <string name="switching_profile_error_title">Errore nel cambio di profilo</string> + <string name="select_chat_profile">Seleziona il profilo di chat</string> + <string name="new_chat_share_profile">Condividi il profilo</string> + <string name="settings_section_title_chat_database">DATABASE DELLA CHAT</string> + <string name="system_mode_toast">Modalità di sistema</string> + <string name="migrate_from_device_remove_archive_question">Rimuovere l\'archivio?</string> + <string name="delete_messages_cannot_be_undone_warning">I messaggi verranno eliminati. Non è reversibile!</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">L\'archivio del database caricato verrà rimosso definitivamente dai server.</string> + <string name="switching_profile_error_message">La tua connessione è stata spostata a %s, ma si è verificato un errore imprevisto durante il reindirizzamento al profilo.</string> + <string name="network_proxy_auth_mode_no_auth">Non usare credenziali con proxy.</string> + <string name="network_proxy_incorrect_config_desc">Assicurati che la configurazione del proxy sia corretta.</string> + <string name="network_proxy_auth">Autenticazione del proxy</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">Usa diverse credenziali del proxy per ogni connessione.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">Usa diverse credenziali del proxy per ogni profilo.</string> + <string name="network_proxy_random_credentials">Usa credenziali casuali</string> + <string name="network_proxy_auth_mode_username_password">Le credenziali potrebbero essere inviate in chiaro.</string> + <string name="network_proxy_incorrect_config_title">Errore di salvataggio del proxy</string> + <string name="network_proxy_password">Password</string> + <string name="network_proxy_username">Nome utente</string> + <string name="forward_files_in_progress_desc">%1$d file è/sono ancora in scaricamento.</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s messaggi non inoltrati</string> + <string name="n_other_file_errors">%1$d altro/i errore/i di file.</string> + <string name="error_forwarding_messages">Errore nell\'inoltro dei messaggi</string> + <string name="n_file_errors">%1$d errore/i di file: +\n%2$s</string> + <string name="forward_alert_title_messages_to_forward">Inoltrare %1$s messaggio/i?</string> + <string name="forward_alert_forward_messages_without_files">Inoltrare i messaggi senza file?</string> + <string name="forward_files_messages_deleted_after_selection_desc">I messaggi sono stati eliminati dopo che li hai selezionati.</string> + <string name="forward_alert_title_nothing_to_forward">Niente da inoltrare!</string> + <string name="forward_files_failed_to_receive_desc">%1$d file ha/hanno fallito lo scaricamento.</string> + <string name="forward_files_missing_desc">%1$d file è/sono stato/i eliminato/i.</string> + <string name="forward_files_not_accepted_desc">%1$d file non è/sono stato/i scaricato/i.</string> + <string name="forward_files_not_accepted_receive_files">Scarica</string> + <string name="compose_forward_messages_n">Inoltro di %1$s messaggi</string> + <string name="forward_multiple">Inoltra messaggi…</string> + <string name="compose_save_messages_n">Salvataggio di %1$s messaggi</string> + <string name="icon_descr_sound_muted">Audio silenziato</string> + <string name="error_initializing_web_view_wrong_arch">Errore di inizializzazione di WebView. Assicurati di avere WebView installato e che la sua architettura supportata sia arm64. +\nErrore: %s</string> + <string name="settings_message_shape_corner">Angolo</string> + <string name="settings_section_title_message_shape">Forma del messaggio</string> + <string name="settings_message_shape_tail">Coda</string> + <string name="network_session_mode_session">Sessione dell\'app</string> + <string name="network_session_mode_session_description">Le nuove credenziali SOCKS verranno usate ogni volta che avvii l\'app.</string> + <string name="network_session_mode_server_description">Le nuove credenziali SOCKS verranno usate per ogni server.</string> + <string name="network_session_mode_server">Server</string> + <string name="call_desktop_permission_denied_safari">Apri le impostazioni di Safari / Siti web / Microfono, quindi scegli Consenti per localhost.</string> + <string name="call_desktop_permission_denied_chrome">Clicca il pulsante info vicino al campo indirizzo per consentire l\'uso del microfono.</string> + <string name="call_desktop_permission_denied_title">Per effettuare chiamate, consenti di usare il microfono. Termina la chiamata e cerca di richiamare.</string> + <string name="v6_1_better_calls">Chiamate migliorate</string> + <string name="v6_1_message_dates_descr">Date dei messaggi migliorate.</string> + <string name="v6_1_better_security">Sicurezza migliorata ✅</string> + <string name="v6_1_better_user_experience">Esperienza utente migliorata</string> + <string name="v6_1_customizable_message_descr">Forma dei messaggi personalizzabile.</string> + <string name="v6_1_better_security_descr">Protocolli di SimpleX esaminati da Trail of Bits.</string> + <string name="v6_1_better_calls_descr">Cambia tra audio e video durante la chiamata.</string> + <string name="v6_1_switch_chat_profile_descr">Cambia profilo di chat per inviti una tantum.</string> + <string name="v6_1_delete_many_messages_descr">Elimina o modera fino a 200 messaggi.</string> + <string name="v6_1_forward_many_messages_descr">Inoltra fino a 20 messaggi alla volta.</string> + <string name="no_message_servers_configured">Nessun server dei messaggi.</string> + <string name="no_message_servers_configured_for_receiving">Nessun server per ricevere messaggi.</string> + <string name="errors_in_servers_configuration">Errori nella configurazione dei server.</string> + <string name="for_chat_profile">Per il profilo di chat %s:</string> + <string name="connection_error_quota">Messaggi non consegnati</string> + <string name="message_deleted_or_not_received_error_title">Nessun messaggio</string> + <string name="connection_security">Sicurezza della connessione</string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">L\'indirizzo SimpleX e i link una tantum sono sicuri da condividere tramite qualsiasi messenger.</string> + <string name="to_protect_against_your_link_replaced_compare_codes">Per proteggerti dalla sostituzione del tuo link, puoi confrontare i codici di sicurezza del contatto.</string> + <string name="you_can_set_connection_name_to_remember">Puoi impostare il nome della connessione per ricordare con chi è stato condiviso il link.</string> + <string name="share_1_time_link_with_a_friend">Condividi link una tantum con un amico</string> + <string name="create_1_time_link">Crea link una tantum</string> + <string name="for_social_media">Per i social media</string> + <string name="or_to_share_privately">O per condividere in modo privato</string> + <string name="onboarding_network_operators">Operatori di rete</string> + <string name="onboarding_network_operators_app_will_use_different_operators">L\'app protegge la tua privacy usando diversi operatori per ogni conversazione.</string> + <string name="onboarding_network_operators_conditions_you_can_configure">Puoi configurare gli operatori nelle impostazioni di rete e server.</string> + <string name="onboarding_choose_server_operators">Operatori del server</string> + <string name="onboarding_select_network_operators_to_use">Seleziona gli operatori di rete da usare.</string> + <string name="onboarding_network_operators_continue">Continua</string> + <string name="onboarding_network_operators_update">Aggiorna</string> + <string name="onboarding_network_operators_review_later">Leggi più tardi</string> + <string name="network_preset_servers_title">Server preimpostati</string> + <string name="operator_conditions_accepted">Condizioni accettate</string> + <string name="operator_conditions_accepted_for_enabled_operators_on">Le condizioni verranno accettate automaticamente per gli operatori attivati il: %s.</string> + <string name="your_servers">I tuoi server</string> + <string name="operator_review_conditions">Leggi le condizioni</string> + <string name="operators_conditions_accepted_for"><![CDATA[Condizioni accettate per gli operatori: <b>%s</b>.]]></string> + <string name="operator_conditions_accepted_for_some"><![CDATA[Condizioni già accettate per i seguenti operatori: <b>%s</b>.]]></string> + <string name="operator_conditions_failed_to_load">Il testo delle condizioni attuali testo non è stato caricato, puoi consultare le condizioni tramite questo link:</string> + <string name="operator_info_title">Operatore di rete</string> + <string name="operator_servers_title">Server di %s</string> + <string name="use_servers_of_operator_x">Usa %s</string> + <string name="operator_website">Sito web</string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[Le condizioni verranno accettate per gli operatori: <b>%s</b>.]]></string> + <string name="operator_conditions_accepted_on">Condizioni accettate il: %s.</string> + <string name="operator">Operatore</string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Le condizioni verranno accettate per gli operatori: <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[Le stesse condizioni verranno applicate per l\'operatore <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Le stesse condizioni verranno applicate per gli operatori: <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_applied"><![CDATA[Queste condizioni verranno applicate anche per: <b>%s</b>.]]></string> + <string name="accept_conditions">Accetta le condizioni</string> + <string name="error_updating_server_title">Errore di aggiornamento del server</string> + <string name="operator_use_for_messages_private_routing">Per l\'instradamento privato</string> + <string name="xftp_servers_per_user">I server per nuovi file del tuo profilo di chat attuale</string> + <string name="operator_use_for_messages_receiving">Per ricevere</string> + <string name="operator_use_for_sending">Per inviare</string> + <string name="operator_use_for_messages">Usa per i messaggi</string> + <string name="view_conditions">Vedi le condizioni</string> + <string name="operators_conditions_will_also_apply"><![CDATA[Queste condizioni verranno applicate anche per: <b>%s</b>.]]></string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[Per usare i server di <b>%s</b>, accetta le condizioni d\'uso.]]></string> + <string name="operator_conditions_of_use">Condizioni d\'uso</string> + <string name="operator_open_changes">Apri le modifiche</string> + <string name="operator_open_conditions">Apri le condizioni</string> + <string name="error_server_protocol_changed">Il protocollo del server è cambiato.</string> + <string name="error_adding_server">Errore di aggiunta del server</string> + <string name="operator_server_alert_title">Server dell\'operatore</string> + <string name="server_added_to_operator__name">Server aggiunto all\'operatore %s.</string> + <string name="error_server_operator_changed">L\'operatore del server è cambiato.</string> + <string name="appearance_app_toolbars">Barre degli strumenti</string> + <string name="appearance_in_app_bars_alpha">Trasparenza</string> + <string name="v6_2_network_decentralization">Decentralizzazione della rete</string> + <string name="v6_2_network_decentralization_descr">Il secondo operatore preimpostato nell\'app!</string> + <string name="v6_2_network_decentralization_enable_flux">Attiva Flux nelle impostazioni \"Rete e server\" per una migliore privacy dei metadati.</string> + <string name="view_updated_conditions">Vedi le condizioni aggiornate</string> + <string name="appearance_bars_blur_radius">Sfocatura</string> + <string name="operator_added_message_servers">Server dei messaggi aggiunti</string> + <string name="operator_added_xftp_servers">Server di multimediali e file aggiunti</string> + <string name="address_or_1_time_link">Indirizzo o link una tantum?</string> + <string name="address_settings">Impostazioni dell\'indirizzo</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[Il link una tantum puo\' essere usato <i>con un solo contatto</i> - condividilo di persona o tramite un messenger.]]></string> + <string name="onboarding_network_operators_conditions_will_be_accepted">Le condizioni verranno accettate per gli operatori attivati dopo 30 giorni.</string> + <string name="operator_conditions_will_be_accepted_on">Le condizioni verranno accettate il: %s.</string> + <string name="error_accepting_operator_conditions">Errore di accettazione delle condizioni</string> + <string name="failed_to_save_servers">Errore di salvataggio dei server</string> + <string name="v6_2_network_decentralization_enable_flux_reason">per una migliore privacy dei metadati.</string> + <string name="onboarding_network_operators_app_will_use_for_routing">Ad esempio, se il tuo contatto riceve i messaggi tramite un server di SimpleX Chat, la tua app li consegnerà tramite un server di Flux.</string> + <string name="v6_2_improved_chat_navigation">Navigazione della chat migliorata</string> + <string name="smp_servers_new_server">Nuovo server</string> + <string name="operator_use_for_files">Usa per i file</string> + <string name="simplex_address_or_1_time_link">Indirizzo SimpleX o link una tantum?</string> + <string name="message_deleted_or_not_received_error_desc">Questo messaggio è stato eliminato o non ancora ricevuto.</string> + <string name="address_creation_instruction">Tocca Crea indirizzo SimpleX nel menu per crearlo più tardi.</string> + <string name="connection_error_quota_desc">La connessione ha raggiunto il limite di messaggi non consegnati, il contatto potrebbe essere offline.</string> + <string name="operator_use_operator_toggle_description">Usa i server</string> + <string name="onboarding_network_operators_configure_via_settings">Puoi configurare i server nelle impostazioni.</string> + <string name="no_media_servers_configured">Nessun server di multimediali e file.</string> + <string name="no_message_servers_configured_for_private_routing">Nessun server per l\'instradamento dei messaggi privati.</string> + <string name="no_media_servers_configured_for_private_routing">Nessun server per ricevere file.</string> + <string name="no_media_servers_configured_for_sending">Nessun server per inviare file.</string> + <string name="v6_2_improved_chat_navigation_descr">- Apri la chat sul primo messaggio non letto.\n- Salta ai messaggi citati.</string> + <string name="share_address_publicly">Condividi indirizzo pubblicamente</string> + <string name="share_simplex_address_on_social_media">Condividi l\'indirizzo SimpleX sui social media.</string> + <string name="chat_archive">O importa file archivio</string> + <string name="remote_hosts_section">Telefoni remoti</string> + <string name="direct_messages_are_prohibited_in_chat">I messaggi diretti tra i membri sono vietati in questa chat.</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Dispositivi Xiaomi</b>: attiva l\'avvio automatico nelle impostazioni di sistema per fare funzionare le notifiche.]]></string> + <string name="add_your_team_members_to_conversations">Aggiungi i membri del tuo team alle conversazioni.</string> + <string name="business_address">Indirizzo di lavoro</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[Tutti i messaggi e i file sono <b>cifrati end-to-end</b>, con sicurezza quantistica nei messaggi diretti.]]></string> + <string name="onboarding_notifications_mode_periodic_desc_short">Controlla i messaggi ogni 10 minuti</string> + <string name="how_it_helps_privacy">Come aiuta la privacy</string> + <string name="invite_to_chat_button">Invita in chat</string> + <string name="button_add_team_members">Aggiungi membri del team</string> + <string name="info_row_chat">Chat</string> + <string name="direct_messages_are_prohibited">I messaggi diretti tra i membri sono vietati.</string> + <string name="connect_plan_chat_already_exists">La chat esiste già!</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Sei già connesso/a con <b>%1$s</b>.]]></string> + <string name="leave_chat_question">Uscire dalla chat?</string> + <string name="delete_chat_for_self_cannot_undo_warning">La chat verrà eliminata solo per te, non è reversibile!</string> + <string name="button_leave_chat">Esci dalla chat</string> + <string name="v6_2_business_chats">Chat di lavoro</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">La chat verrà eliminata per tutti i membri, non è reversibile!</string> + <string name="button_add_friends">Aggiungi amici</string> + <string name="onboarding_notifications_mode_service_desc_short">L\'app funziona sempre in secondo piano</string> + <string name="button_delete_chat">Elimina chat</string> + <string name="delete_chat_question">Eliminare la chat?</string> + <string name="maximum_message_size_title">Il messaggio è troppo grande!</string> + <string name="maximum_message_size_reached_text">Riduci la dimensione del messaggio e invialo di nuovo.</string> + <string name="maximum_message_size_reached_non_text">Riduci la dimensione del messaggio o rimuovi i media e invialo di nuovo.</string> + <string name="onboarding_notifications_mode_off_desc_short">Nessun servizio in secondo piano</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">Il membro verrà rimosso dalla chat, non è reversibile!</string> + <string name="v6_2_business_chats_descr">Privacy per i tuoi clienti.</string> + <string name="chat_bottom_bar">Barra degli strumenti di chat accessibile</string> + <string name="only_chat_owners_can_change_prefs">Solo i proprietari della chat possono modificarne le preferenze.</string> + <string name="onboarding_notifications_mode_battery">Notifiche e batteria</string> + <string name="maximum_message_size_reached_forwarding">Puoi copiare e ridurre la dimensione del messaggio per inviarlo.</string> + <string name="member_role_will_be_changed_with_notification_chat">Il ruolo verrà cambiato in %s. Verrà notificato a tutti nella chat.</string> + <string name="chat_main_profile_sent">Il tuo profilo di chat verrà inviato ai membri della chat</string> + <string name="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.</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">Quando più di un operatore è attivato, nessuno di essi ha metadati per capire chi comunica con chi.</string> + <string name="display_name_accepted_invitation">invito accettato</string> + <string name="display_name_requested_to_connect">richiesto di connettersi</string> + <string name="onboarding_network_operators_simplex_flux_agreement">SimpleX Chat e Flux hanno concluso un accordo per includere server gestiti da Flux nell\'app.</string> + <string name="onboarding_network_about_operators">Info sugli operatori</string> + <string name="sync_connection_desc">La connessione richiede la rinegoziazione della crittografia.</string> + <string name="sync_connection_confirm">Correggi</string> + <string name="sync_connection_question">Correggere la connessione?</string> + <string name="encryption_renegotiation_in_progress">Rinegoziazione della crittografia in corso.</string> + <string name="debug_logs">Attiva i log</string> + <string name="error_saving_database">Errore di salvataggio del database</string> + <string name="connection_not_ready">Connessione non pronta.</string> + <string name="error_updating_chat_tags">Errore di aggiornamento dell\'elenco di chat</string> + <string name="chat_list_all">Tutte</string> + <string name="chat_list_favorites">Preferite</string> + <string name="chat_list_groups">Gruppi</string> + <string name="no_chats">Nessuna chat</string> + <string name="no_chats_found">Nessuna chat trovata</string> + <string name="no_unread_chats">Nessuna chat non letta</string> + <string name="chat_list_add_list">Aggiungi elenco</string> + <string name="open_with_app">Apri con %s</string> + <string name="add_to_list">Aggiungi ad un elenco</string> + <string name="list_name_field_placeholder">Nome elenco...</string> + <string name="save_list">Salva elenco</string> + <string name="delete_chat_list_menu_action">Elimina</string> + <string name="delete_chat_list_warning">Tutte le chat verranno rimosse dall\'elenco %s, e l\'elenco eliminato</string> + <string name="delete_chat_list_question">Eliminare l\'elenco?</string> + <string name="duplicated_list_error">Il nome dell\'elenco e l\'emoji dovrebbero essere diversi per tutte le liste.</string> + <string name="error_loading_chat_tags">Errore di caricamento dell\'elenco di chat</string> + <string name="error_creating_chat_tags">Errore di creazione dell\'elenco di chat</string> + <string name="list_menu">Elenco</string> + <string name="chat_list_contacts">Contatti</string> + <string name="chat_list_businesses">Lavorative</string> + <string name="create_list">Crea elenco</string> + <string name="edit_chat_list_menu_action">Modifica</string> + <string name="no_chats_in_list">Nessuna chat nell\'elenco %s.</string> + <string name="change_list">Cambia elenco</string> + <string name="chat_list_notes">Note</string> + <string name="change_order_chat_list_menu_action">Cambia ordine</string> + <string name="prefs_error_saving_settings">Errore di salvataggio delle impostazioni</string> + <string name="error_creating_report">Errore nella creazione del resoconto</string> + <string name="report_archive_alert_title">Archiviare la segnalazione?</string> + <string name="report_item_archived">segnalazione archiviata</string> + <string name="report_compose_reason_header_illegal">Segnala contenuto: solo i moderatori del gruppo lo vedranno.</string> + <string name="archive_verb">Archivia</string> + <string name="report_reason_other">Altro motivo</string> + <string name="report_compose_reason_header_other">Segnala altro: solo i moderatori del gruppo lo vedranno.</string> + <string name="report_reason_community">Violazione delle linee guida della comunità</string> + <string name="report_reason_illegal">Contenuto inappropriato</string> + <string name="report_reason_profile">Profilo inappropriato</string> + <string name="report_item_visibility_moderators">Solo il mittente e i moderatori lo vedono</string> + <string name="report_item_visibility_submitter">Solo tu e i moderatori lo vedete</string> + <string name="report_reason_spam">Spam</string> + <string name="report_verb">Segnala</string> + <string name="report_reason_alert_title">Motivo della segnalazione?</string> + <string name="report_archive_alert_desc">La segnalazione verrà archiviata per te.</string> + <string name="report_compose_reason_header_profile">Segnala profilo: solo i moderatori del gruppo lo vedranno.</string> + <string name="report_compose_reason_header_spam">Segnala spam: solo i moderatori del gruppo lo vedranno.</string> + <string name="report_compose_reason_header_community">Segnala violazione: solo i moderatori del gruppo lo vedranno.</string> + <string name="group_member_role_moderator">moderatore</string> + <string name="archive_report">Archivia la segnalazione</string> + <string name="delete_report">Elimina la segnalazione</string> + <string name="chat_list_group_reports">Segnalazioni</string> + <string name="group_reports_active_one">1 segnalazione</string> + <string name="report_item_archived_by">segnalazione archiviata da %s</string> + <string name="group_reports_active">%d segnalazioni</string> + <string name="group_reports_member_reports">Segnalazioni dei membri</string> + <string name="blocking_reason_spam">Spam</string> + <string name="connection_error_blocked">Connessione bloccata</string> + <string name="file_error_blocked">Il file è bloccato dall\'operatore del server:\n%1$s.</string> + <string name="connection_error_blocked_desc">La connessione è bloccata dall\'operatore del server:\n%1$s.</string> + <string name="blocking_reason_content">Il contenuto viola le condizioni di utilizzo</string> + <string name="privacy_chat_list_open_web_link_question">Aprire il link?</string> + <string name="privacy_chat_list_open_links_yes">Sì</string> + <string name="privacy_chat_list_open_links">Apri i link dall\'elenco delle chat</string> + <string name="privacy_chat_list_open_links_no">No</string> + <string name="privacy_chat_list_open_web_link">Apri link</string> + <string name="privacy_chat_list_open_links_ask">Chiedi</string> + <string name="text_field_set_chat_placeholder">Imposta il nome della chat…</string> + <string name="disable_automatic_deletion">Disattiva eliminazione messaggi</string> + <string name="chat_item_ttl_year">1 anno</string> + <string name="disable_automatic_deletion_question">Disattivare l\'eliminazione automatica dei messaggi?</string> + <string name="change_automatic_deletion_question">Cambiare l\'eliminazione automatica dei messaggi?</string> + <string name="change_automatic_chat_deletion_message">Questa azione non è reversibile: i messaggi inviati e ricevuti in questa chat prima della selezione verranno eliminati.</string> + <string name="chat_ttl_options_footer">Elimina i messaggi di chat dal tuo dispositivo.</string> + <string name="disable_automatic_deletion_message">I messaggi in questa chat non verranno mai eliminati.</string> + <string name="chat_item_ttl_default">predefinito (%s)</string> + <string name="network_smp_web_port_footer">Usa la porta TCP %1$s quando nessuna porta è specificata.</string> + <string name="network_smp_web_port_section_title">Porta TCP per i messaggi</string> + <string name="network_smp_web_port_toggle">Usa porta web</string> + <string name="mute_all_chat">Silenzia tutto</string> + <string name="unread_mentions">Menzioni non lette</string> + <string name="max_group_mentions_per_message_reached">Puoi menzionare fino a %1$s membri per messaggio!</string> + <string name="group_members_can_send_reports">I membri possono segnalare messaggi ai moderatori.</string> + <string name="report_archive_alert_title_all">Archiviare tutte le segnalazioni?</string> + <string name="report_archive_alert_title_nth">Archiviare %d segnalazioni?</string> + <string name="archive_reports">Archivia segnalazioni</string> + <string name="report_archive_for_all_moderators">Per tutti i moderatori</string> + <string name="report_archive_for_me">Per me</string> + <string name="notification_group_report">Segnalazione: %s</string> + <string name="member_reports_are_prohibited">In questo gruppo è vietato segnalare messaggi.</string> + <string name="enable_sending_member_reports">Consenti di segnalare messaggi ai moderatori.</string> + <string name="report_archive_alert_desc_all">Tutte le segnalazioni verranno archiviate per te.</string> + <string name="disable_sending_member_reports">Vieta di segnalare messaggi ai moderatori.</string> + <string name="v6_3_organize_chat_lists_descr">Non perdere messaggi importanti.</string> + <string name="v6_3_faster_deletion_of_groups">Eliminazione dei gruppi più veloce.</string> + <string name="v6_3_mentions_descr">Ricevi una notifica quando menzionato.</string> + <string name="v6_3_reports_descr">Aiuta gli amministratori a moderare i loro gruppi.</string> + <string name="v6_3_mentions">Menziona i membri 👋</string> + <string name="v6_3_better_privacy_and_security">Privacy e sicurezza migliori</string> + <string name="v6_3_faster_sending_messages">Invio dei messaggi più veloce.</string> + <string name="v6_3_better_groups_performance">Prestazioni dei gruppi migliorate</string> + <string name="v6_3_organize_chat_lists">Organizza le chat in elenchi</string> + <string name="v6_3_set_message_expiration_in_chats">Imposta la scadenza dei messaggi nelle chat.</string> + <string name="v6_3_reports">Invia segnalazioni private</string> + <string name="v6_3_private_media_file_names">Nomi privati dei file multimediali.</string> + <string name="group_member_status_rejected">rifiutato</string> + <string name="group_preview_rejected">rifiutato</string> + <string name="error_reading_passphrase">Errore di lettura della password del database</string> + <string name="group_member_status_pending_approval">in attesa di approvazione</string> + <string name="group_member_status_pending_approval_short">in attesa</string> + <string name="operator_updated_conditions">Condizioni aggiornate</string> + <string name="restore_passphrase_can_not_be_read_desc">La password nell\'archivio chiavi non può essere letta. Potrebbe essere successo dopo un aggiornamento di sistema incompatibile con l\'app. In caso contrario, contatta gli sviluppatori.</string> + <string name="restore_passphrase_can_not_be_read_enter_manually_desc">La password nell\'archivio chiavi non può essere letta, inseriscila a mano. Potrebbe essere successo dopo un aggiornamento di sistema incompatibile con l\'app. In caso contrario, contatta gli sviluppatori.</string> + <string name="members_will_be_removed_from_chat_cannot_be_undone">I membri verranno rimossi dalla chat, non è reversibile!</string> + <string name="members_will_be_removed_from_group_cannot_be_undone">I membri verranno rimossi dal gruppo, non è reversibile!</string> + <string name="button_remove_members_question">Rimuovere i membri?</string> + <string name="unblock_members_desc">I messaggi di questi membri verranno mostrati!</string> + <string name="unblock_members_for_all_question">Sbloccare i membri per tutti?</string> + <string name="block_members_for_all_question">Bloccare i membri per tutti?</string> + <string name="feature_roles_moderators">moderatori</string> + <string name="block_members_desc">Tutti i nuovi messaggi di questi membri verranno nascosti!</string> + <string name="onboarding_conditions_by_using_you_agree">Usando SimpleX Chat accetti di:\n- inviare solo contenuto legale nei gruppi pubblici.\n- rispettare gli altri utenti - niente spam.</string> + <string name="onboarding_conditions_private_chats_not_accessible">Le chat private, i gruppi e i tuoi contatti non sono accessibili agli operatori dei server.</string> + <string name="onboarding_conditions_accept">Accetta</string> + <string name="onboarding_conditions_configure_server_operators">Configura gli operatori dei server</string> + <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Informativa sulla privacy e condizioni d\'uso.</string> + <string name="link_requires_newer_app_version_please_upgrade">Questo link richiede una versione più recente dell\'app. Aggiornala o chiedi al tuo contatto di inviare un link compatibile.</string> + <string name="full_link_button_text">Link completo</string> + <string name="short_link_button_text">Link breve</string> + <string name="simplex_link_channel">Link del canale SimpleX</string> + <string name="unsupported_connection_link">Link di connessione non supportato</string> + <string name="privacy_short_links">Usa link brevi (BETA)</string> + <string name="network_smp_web_port_all">Tutti i server</string> + <string name="network_smp_web_port_off">Off</string> + <string name="network_smp_web_port_preset">Server preimpostati</string> + <string name="network_smp_web_port_preset_footer">Usa la porta TCP 443 solo per i server preimpostati.</string> +</resources> 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 58bb6b0a0a..1103e3a0e6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml @@ -18,7 +18,7 @@ <string name="clear_chat_warning">כל ההודעות יימחקו – לא ניתן לבטל זאת! ההודעות יימחקו רק עבורך.</string> <string name="accept_contact_incognito_button">אשר זהות נסתרת</string> <string name="smp_servers_preset_add">הוסף שרתים מוגדרים מראש</string> - <string name="smp_servers_add">הוסף שרת…</string> + <string name="smp_servers_add">הוסף שרת</string> <string name="network_enable_socks_info">לגשת לשרתים דרך פרוקסי SOCKS בפורט %d\? הפרוקסי חייב לפעול לפני הפעלת אפשרות זו.</string> <string name="network_settings">הגדרות רשת מתקדמות</string> <string name="appearance_settings">מראה</string> @@ -37,7 +37,7 @@ <string name="allow_your_contacts_to_send_disappearing_messages">אפשר לאנשי קשר לשלוח הודעות נעלמות.</string> <string name="allow_voice_messages_only_if">אפשר הודעות קוליות רק אם איש הקשר מאפשר אותן.</string> <string name="allow_your_contacts_to_call">אפשר לאנשי קשר להתקשר אליכם.</string> - <string name="allow_to_delete_messages">אפשר מחיקה בלתי הפיכה של הודעות שנשלחו.</string> + <string name="allow_to_delete_messages">אפשר למחוק הודעות שנשלחו באופן בלתי הפיך. (24 שעות)</string> <string name="allow_to_send_disappearing">אפשר שליחת הודעות נעלמות.</string> <string name="allow_to_send_voice">אפשר שליחת הודעות קוליות.</string> <string name="group_member_role_admin">מנהל</string> @@ -47,7 +47,7 @@ <string name="v4_3_improved_server_configuration_desc">הוספת שרתים על ידי סריקת קוד QR.</string> <string name="smp_servers_add_to_another_device">הוסף למכשיר אחר</string> <string name="allow_calls_only_if">אפשר שיחות רק אם איש הקשר מאפשר אותן.</string> - <string name="allow_irreversible_message_deletion_only_if">אפשר לאנשי קשר מחיקת הודעות בלתי הפיכה רק אם הם מאפשרים לך לעשות זאת.</string> + <string name="allow_irreversible_message_deletion_only_if">אפשר מחיקת הודעות בלתי הפיכה רק אם האיש קשר מאפשר את זה (24 שעות)</string> <string name="allow_direct_messages">אפשר שליחת הודעות ישירות לחברי הקבוצה.</string> <string name="allow_voice_messages_question">לאפשר הודעות קוליות\?</string> <string name="allow_your_contacts_to_send_voice_messages">אפשר לאנשי קשר לשלוח הודעות קוליות.</string> @@ -60,8 +60,7 @@ <string name="v5_0_app_passcode">קוד גישה לאפליקציה</string> <string name="app_version_title">גרסת האפליקציה</string> <string name="network_session_mode_user_description"><![CDATA[חיבור TCP נפרד (ואישור SOCKS) ייווצר <b>לכל פרופיל צ׳אט שיש ברשותך באפליקציה</b>.]]></string> - <string name="network_session_mode_entity_description">חיבור TCP נפרד (ואישור SOCKS) ייווצר <b>לכל איש קשר וחבר קבוצה</b>. -\n<b>שימו לב</b>: אם ברשותכם חיבורים רבים, צריכת הסוללה ותעבורת האינטרנט עשויה להיות גבוהה משמעותית וחלק מהחיבורים עלולים להיכשל.</string> + <string name="network_session_mode_entity_description"><![CDATA[חיבור TCP נפרד (ואישור SOCKS) ייווצר <b>לכל איש קשר וחבר קבוצה</b>. \n<b>שימו לב</b>: אם ברשותכם חיבורים רבים, צריכת הסוללה ותעבורת האינטרנט עשויה להיות גבוהה משמעותית וחלק מהחיבורים עלולים להיכשל.]]></string> <string name="icon_descr_video_asked_to_receive">הנמען התבקש לקבל את הסרטון</string> <string name="icon_descr_asked_to_receive">הנמען התבקש לקבל את התמונה</string> <string name="attach">צרף</string> @@ -93,17 +92,15 @@ <string name="v4_2_auto_accept_contact_requests">אשר אוטומטית בקשות ליצירת קשר.</string> <string name="authentication_cancelled">אימות בוטל</string> <string name="auth_unavailable">אימות לא זמין</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>הוסיפו איש קשר חדש</b>: ליצירת קוד QR חד־פעמי עבור איש הקשר שלכם.]]></string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>הטוב ביותר לסוללה</b>. התראות יוצגו רק כאשר האפליקציה מופעלת (ללא שירות רקע).]]></string> <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>טוב לסוללה</b>. שירות הרקע ייבדוק הודעות כל 10 דקות. שיחות או הודעות דחופות עלולות להתפספס.]]></string> - <string name="both_you_and_your_contacts_can_delete">גם אתם וגם איש הקשר יכולים למחוק באופן בלתי הפיך הודעות שנשלחו.</string> + <string name="both_you_and_your_contacts_can_delete">גם אתה וגם איש הקשר שלך יכולים למחוק הודעות שנשלחו באופן בלתי הפיך. (24 שעות)</string> <string name="both_you_and_your_contact_can_send_disappearing">גם אתם וגם איש הקשר יכולים לשלוח הודעות נעלמות.</string> <string name="cannot_receive_file">לא ניתן לקבל את הקובץ</string> <string name="icon_descr_cancel_image_preview">בטל תצוגה מקדימה של תמונות</string> <string name="cancel_verb">ביטול</string> <string name="icon_descr_cancel_live_message">בטל הודעה חיה</string> <string name="use_camera_button">מצלמה</string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>סירקו קוד QR</b>: כדי להתחבר לאיש קשר המציג לכם קוד QR.]]></string> <string name="icon_descr_cancel_link_preview">בטל תצוגה מקדימה של קישורים</string> <string name="callstatus_error">שגיאת שיחה</string> <string name="callstatus_in_progress">שיחה מתמשכת</string> @@ -114,15 +111,14 @@ <string name="icon_descr_call_progress">שיחה מתמשכת</string> <string name="settings_section_title_calls">שיחות</string> <string name="cannot_access_keychain">לא ניתן לגשת ל־Keystore כדי לאחסן את סיסמת מסד הנתונים</string> - <string name="cant_delete_user_profile">לא ניתן למחוק פרופיל משתמש!</string> <string name="feature_cancelled_item">בוטל %s</string> <string name="v4_5_transport_isolation_descr">לפי פרופיל צ׳אט (ברירת מחדל) או לפי חיבור (בביטא).</string> <string name="callstatus_calling">מתקשר…</string> <string name="callstatus_ended">השיחה הסתיימה %1$s</string> <string name="icon_descr_cancel_file_preview">בטל תצוגה מקדימה של קבצים</string> - <string name="connect_via_contact_link">להתחבר באמצעות קישור ליצירת קשר\?</string> + <string name="connect_via_contact_link">להתחבר באמצעות קישור ליצירת קשר?</string> <string name="connect_via_link_verb">התחבר</string> - <string name="connect_via_group_link">להתחבר באמצעות קישור קבוצה\?</string> + <string name="connect_via_group_link">להצטרף לקבוצה?</string> <string name="server_connected">מחובר</string> <string name="server_connecting">מתחבר</string> <string name="display_name_connecting">מתחבר…</string> @@ -168,8 +164,6 @@ <string name="chat_database_deleted">מסד הנתונים של הצ׳אט נמחק</string> <string name="chat_database_imported">‬מסד הנתונים של הצ׳אט יובא</string> <string name="confirm_database_upgrades">אשר שדרוגי מסד נתונים</string> - <string name="chat_archive_header">ארכיון צ׳אט</string> - <string name="chat_archive_section">ארכיון צ׳אט</string> <string name="chat_is_stopped_indication">צ׳אט מופסק</string> <string name="alert_title_cant_invite_contacts">לא ניתן להזמין את אנשי הקשר!</string> <string name="rcv_group_event_changed_your_role">שונה תפקידך ל%s</string> @@ -200,7 +194,7 @@ <string name="icon_descr_server_status_connected">מחובר</string> <string name="display_name_connection_established">חיבור נוצר</string> <string name="connection_local_display_name">חיבור %1$d</string> - <string name="connect_via_invitation_link">להתחבר באמצעות קישור הזמנה\?</string> + <string name="connect_via_invitation_link">להתחבר דרך קישור חד-פעמי?</string> <string name="contact_already_exists">איש הקשר כבר קיים</string> <string name="delete_contact_all_messages_deleted_cannot_undo_warning">איש הקשר וכל ההודעות יימחקו – לא ניתן לבטל זאת!</string> <string name="connect_via_link_or_qr">התחברות באמצעות קישור / קוד QR</string> @@ -219,14 +213,12 @@ <string name="copied">הועתק ללוח</string> <string name="share_one_time_link">צור קישור הזמנה חד־פעמי</string> <string name="create_group">צור קבוצה סודית</string> - <string name="create_one_time_link">צור קישור הזמנה חד־פעמי</string> <string name="contribute">תרומה</string> <string name="core_version">גרסת ליבה: v%s</string> <string name="create_address">צור כתובת</string> <string name="create_profile_button">צור</string> <string name="create_profile">צור פרופיל</string> <string name="create_your_profile">יצירת הפרופיל שלך</string> - <string name="archive_created_on_ts">נוצר ב־%1$s</string> <string name="create_group_link">צור קישור קבוצה</string> <string name="button_create_group_link">צור קישור</string> <string name="group_member_status_creator">יוצר הקבוצה</string> @@ -257,7 +249,6 @@ <string name="create_address_and_let_people_connect">צרו כתובת כדי לאפשר לאנשים להתחבר אליכם.</string> <string name="decentralized">מבוזר</string> <string name="set_password_to_export_desc">מסד הנתונים מוצפן באמצעות סיסמה אקראית. אנא שנו אותה לפני הייצוא.</string> - <string name="delete_chat_archive_question">למחוק ארכיון צ׳אט\?</string> <string name="delete_chat_profile">מחק פרופיל צ׳אט</string> <string name="chat_preferences_default">ברירת מחדל (%s)</string> <string name="ttl_day">%d יום</string> @@ -311,7 +302,6 @@ <string name="database_passphrase_and_export">סיסמה וייצוא של מסד הנתונים</string> <string name="delete_after">מחק אחרי</string> <string name="delete_files_and_media_all">מחק את כל הקבצים</string> - <string name="delete_archive">מחק ארכיון</string> <string name="for_me_only">מחק עבורי</string> <string name="delete_link">מחק קישור</string> <string name="users_delete_question">למחוק פרופיל צ׳אט\?</string> @@ -337,7 +327,7 @@ <string name="la_enter_app_passcode">הזינו קוד גישה</string> <string name="auth_enable_simplex_lock">הפעלת נעילת SimpleX</string> <string name="edit_verb">ערוך</string> - <string name="display_name__field">שם תצוגה:</string> + <string name="display_name__field">שם פרופיל:</string> <string name="edit_image">ערוך תמונה</string> <string name="enter_correct_passphrase">הזינו סיסמה נכונה.</string> <string name="mtr_error_different">העברת נתונים שונה באפליקציה/מסד נתונים: %s / %s</string> @@ -381,7 +371,7 @@ <string name="feature_enabled">מופעל</string> <string name="feature_enabled_for_contact">מופעל עבור איש הקשר</string> <string name="feature_enabled_for_you">מופעל עבורך</string> - <string name="disappearing_messages_are_prohibited">הודעות נעלמות אסורות בקבוצה זו.</string> + <string name="disappearing_messages_are_prohibited">הודעות נעלמות אסורות.</string> <string name="ttl_min">%d דקה</string> <string name="ttl_s">%d שנ׳</string> <string name="v4_4_disappearing_messages">הודעות נעלמות</string> @@ -392,8 +382,8 @@ <string name="ttl_weeks">%d שבועות</string> <string name="v4_5_multiple_chat_profiles_descr">שמות שונים, אווטארים ובידוד תעבורה.</string> <string name="conn_level_desc_direct">ישיר</string> - <string name="direct_messages_are_prohibited_in_chat">הודעות ישירות בין חברי קבוצה אסורות בקבוצה זו.</string> - <string name="display_name">שם תצוגה</string> + <string name="direct_messages_are_prohibited_in_group">הודעות ישירות בין חברי קבוצה אסורות בקבוצה זו.</string> + <string name="display_name">הזן את שמך:</string> <string name="display_name_cannot_contain_whitespace">שם תצוגה אינו יכול להכיל רווחים.</string> <string name="ttl_months">%d חודשים</string> <string name="ttl_mth">%d ח׳</string> @@ -440,7 +430,7 @@ <string name="file_with_path">קובץ: %s</string> <string name="icon_descr_group_inactive">קבוצה לא פעילה</string> <string name="group_invitation_expired">פג תוקפה של ההזמנה לקבוצה</string> - <string name="group_display_name_field">שם תצוגה של הקבוצה:</string> + <string name="group_display_name_field">הזן שם של הקבוצה:</string> <string name="group_full_name_field">שם מלא של הקבוצה:</string> <string name="v4_2_group_links">קישורי קבוצה</string> <string name="icon_descr_file">קובץ</string> @@ -499,10 +489,10 @@ <string name="delete_group_for_self_cannot_undo_warning">הקבוצה תימחק עבורך – לא ניתן לבטל זאת!</string> <string name="user_hide">הסתר</string> <string name="group_preferences">העדפות קבוצה</string> - <string name="group_members_can_delete">חברי הקבוצה יכולים למחוק באופן בלתי הפיך הודעות שנשלחו.</string> - <string name="group_members_can_send_disappearing">חברי הקבוצה יכולים לשלוח הודעות נעלמות.</string> - <string name="group_members_can_send_dms">חברי הקבוצה יכולים לשלוח הודעות ישירות.</string> - <string name="group_members_can_send_voice">חברי הקבוצה יכולים לשלוח הודעות קוליות.</string> + <string name="group_members_can_delete">משתמשים יכולים למחוק הודעות שנשלחו באופן בלתי הפיך. (24 שעות)</string> + <string name="group_members_can_send_disappearing">משתמשים יכולים לשלוח הודעות נעלמות.</string> + <string name="group_members_can_send_dms">משתמשים יכולים לשלוח הודעות ישירות.</string> + <string name="group_members_can_send_voice">יכולים לשלוח הודעות קוליות.</string> <string name="enable_self_destruct">אפשר השמדה עצמית</string> <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">אם תבחרו לדחות השולח לא יקבל התראה על כך.</string> <string name="network_disable_socks_info">אם תאשרו, שרתי העברת ההודעות יוכלו לראות את ה־IP שלכם, וספק האינטרנט שלכם – את השרתים אליהם אתם מחוברים.</string> @@ -515,7 +505,7 @@ <string name="ignore">התעלם</string> <string name="la_immediately">מיד</string> <string name="import_database">ייבא מסד נתונים</string> - <string name="immune_to_spam_and_abuse">חסין מפני ספאם ושימוש לרעה</string> + <string name="immune_to_spam_and_abuse">חסין מפני ספאם</string> <string name="import_database_question">לייבא מסד נתונים של צ׳אט\?</string> <string name="icon_descr_image_snd_complete">תמונה נשלחה</string> <string name="image_will_be_received_when_contact_completes_uploading">התמונה תתקבל כאשר איש הקשר יסיים להעלות אותה.</string> @@ -531,10 +521,10 @@ <string name="description_via_group_link_incognito">זהות נסתרת באמצעות קישור קבוצה</string> <string name="description_via_one_time_link_incognito">זהות נסתרת באמצעות קישור חד־פעמי</string> <string name="invalid_connection_link">קישור חיבור לא תקין</string> - <string name="turn_off_battery_optimization"><![CDATA[בשביל להשתמש בזה, אנא <b>אפשרו ל-SimpleX לפעול ברקע</b> בתיבת הדו-שיח הבאה. אחרת, ההתראות יושבתו.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[<b>אפשר זאת</b> בתיבת הדו-שיח הבאה כדי לקבל התראות על הודעות חדשות באופן מיידי.]]></string> <string name="service_notifications_disabled">התראות מיידיות מושבתות!</string> <string name="icon_descr_add_members">הזמן חברי קבוצה</string> - <string name="group_member_status_invited">הוזמן</string> + <string name="group_member_status_invited">הזמין את</string> <string name="conn_level_desc_indirect">עקיף (%1$s)</string> <string name="incognito_info_protects">מצב זהות נסתרת מגן על הפרטיות שלך על ידי שימוש בפרופיל אקראי חדש עבור כל איש קשר.</string> <string name="incompatible_database_version">גרסת מסד נתונים לא תואמת</string> @@ -566,14 +556,14 @@ <string name="invite_to_group_button">הזמן לקבוצה</string> <string name="button_add_members">הזמן חברי קבוצה</string> <string name="message_deletion_prohibited">מחיקה בלתי הפיכה של הודעות אסורה בצ׳אט זה.</string> - <string name="message_deletion_prohibited_in_chat">מחיקה בלתי הפיכה של הודעות אסורה בקבוצה זו.</string> + <string name="message_deletion_prohibited_in_chat">מחיקת הודעות בלתי הפיכה אסורה.</string> <string name="group_preview_join_as">להצטרף בתור %s</string> <string name="incognito_info_allows">זה מאפשר חיבורים אנונימיים רבים ללא שום נתונים משותפים ביניהם בפרופיל צ׳אט יחיד.</string> <string name="alert_text_skipped_messages_it_can_happen_when">זה יכול לקרות כאשר: \n1. פג תוקפן של ההודעות בלקוח השולח לאחר 2 ימים או בשרת לאחר 30 ימים. \n2. פיענוח הצפנת הודעה נכשל, מכיוון שאתם או איש הקשר שלכם השתמשתם בגיבוי ישן של מסד הנתונים. \n3. החיבור נפגע.</string> - <string name="onboarding_notifications_mode_subtitle">ניתן לשנות זאת מאוחר יותר באמצעות ההגדרות.</string> + <string name="onboarding_notifications_mode_subtitle">איך זה משפיע על הסוללה</string> <string name="alert_text_fragment_encryption_out_of_sync_old_database">זה יכול לקרות כאשר אתם או איש הקשר שלכם השתמשתם בגיבוי ישן של מסד הנתונים.</string> <string name="join_group_question">להצטרף לקבוצה\?</string> <string name="join_group_button">הצטרף</string> @@ -596,7 +586,6 @@ <string name="learn_more">למדו עוד</string> <string name="markdown_help">עזרה במרקדאון</string> <string name="email_invite_subject">בואו נדבר ב־Simplex Chat</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[אנשים רבים שאלו: <i>אם ל־SimpleX אין מזהי משתמש, איך ניתן להעביר הודעות\?</i>]]></string> <string name="keychain_error">שגיאת Keychain</string> <string name="join_group_incognito_button">הצטרף עם זהות נסתרת</string> <string name="leave_group_question">לעזוב קבוצה\?</string> @@ -608,8 +597,6 @@ <string name="theme_light">בהיר</string> <string name="import_theme_error_desc">ודאו שלקובץ יש תחביר YAML תקין. ייצאו ערכת נושא כדי לקבל דוגמה למבנה תקין של קובץ ערכת נושא.</string> <string name="message_delivery_error_desc">ככל הנראה איש קשר זה מחק את החיבור איתך.</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">ייעשה שימוש במארחי Onion כאשר יהיו זמינים.</string> - <string name="network_use_onion_hosts_required_desc_in_alert">מארחי Onion יידרשו לחיבור.</string> <string name="moderated_item_description">נחסם על ידי %s</string> <string name="la_no_app_password">אין קוד גישה לאפליקציה</string> <string name="videos_limit_desc">ניתן לשלוח רק 10 סרטונים בו־זמנית</string> @@ -621,7 +608,7 @@ <string name="add_contact">קישור הזמנה חד־פעמי</string> <string name="markdown_in_messages">מרקדאון בהודעות</string> <string name="network_and_servers">רשת ושרתים</string> - <string name="network_settings_title">הגדרות רשת</string> + <string name="network_settings_title">הגדרות מתקדמות</string> <string name="new_database_archive">ארכיון מסד נתונים חדש</string> <string name="messages_section_title">הודעות</string> <string name="group_member_role_member">חבר קבוצה</string> @@ -631,7 +618,7 @@ <string name="chat_preferences_on">פעיל</string> <string name="chat_preferences_no">לא</string> <string name="chat_preferences_off">כבוי</string> - <string name="only_you_can_delete_messages">רק אתם יכולים למחוק הודעות באופן בלתי הפיך (איש הקשר שלכם יכול לסמן אותן למחיקה).</string> + <string name="only_you_can_delete_messages">רק אתה יכול למחוק הודעות באופן בלתי הפיך (איש הקשר שלך יכול לסמן אותן למחיקה). (24 שעות)</string> <string name="only_you_can_send_voice">רק אתם יכולים לשלוח הודעות קוליות.</string> <string name="only_your_contact_can_send_voice">רק איש הקשר שלכם יכול לשלוח הודעות קוליות.</string> <string name="only_you_can_make_calls">רק אתם יכולים לבצע שיחות.</string> @@ -644,7 +631,7 @@ <string name="no_contacts_to_add">אין אנשי קשר להוסיף</string> <string name="only_you_can_send_disappearing">רק אתם יכולים לשלוח הודעות נעלמות.</string> <string name="only_your_contact_can_send_disappearing">רק איש הקשר שלכם יכול לשלוח הודעות נעלמות.</string> - <string name="only_your_contact_can_delete">רק איש הקשר שלכם יכול למחוק הודעות באופן בלתי הפיך (אתם יכולים לסמן אותן למחיקה).</string> + <string name="only_your_contact_can_delete">רק איש הקשר שלך יכול למחוק הודעות באופן בלתי הפיך (אתה יכול לסמן אותן למחיקה). (24 שעות)</string> <string name="v4_5_message_draft">טיוטת הודעה</string> <string name="v4_5_multiple_chat_profiles">פרופילי צ׳אט מרובים</string> <string name="v4_5_reduced_battery_usage_descr">שיפורים נוספים יגיעו בקרוב!</string> @@ -678,7 +665,6 @@ <string name="network_use_onion_hosts_required_desc">יידרשו מארחי onion לחיבור. \nשימו לב: לא תוכלו להתחבר לשרתים ללא כתובת .onion.</string> <string name="network_use_onion_hosts_no_desc">לא ייעשה שימוש במארחי Onion.</string> - <string name="network_use_onion_hosts_no_desc_in_alert">לא ייעשה שימוש במארחי Onion.</string> <string name="callstatus_missed">שיחה שלא נענתה</string> <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[רק מכשירי לקוח מאחסנים פרופילי משתמש, אנשי קשר, קבוצות, והודעות שנשלחו עם <b>הצפנה מקצה־לקצה דו־שכבתית</b>.]]></string> <string name="status_no_e2e_encryption">ללא הצפנה מקצה־לקצה</string> @@ -704,7 +690,7 @@ <string name="message_reactions_are_prohibited">תגובות אמוג׳י להודעות אסורות בקבוצה זו.</string> <string name="allow_your_contacts_adding_message_reactions">אפשר לאנשי הקשר להוסיף תגובות אמוג׳י להודעות.</string> <string name="allow_message_reactions_only_if">אפשר תגובות אמוג׳י להודעות רק אם איש הקשר מאפשר אותן.</string> - <string name="group_members_can_add_message_reactions">חברי הקבוצה יכולים להוסיף תגובות אמוג׳י להודעות.</string> + <string name="group_members_can_add_message_reactions">משתמשים יכולים להוסיף תגובות אמוג׳י להודעות.</string> <string name="only_you_can_add_message_reactions">רק אתם יכולים להוסיף תגובות אמוג׳י להודעות.</string> <string name="only_your_contact_can_add_message_reactions">רק איש הקשר שלכם יכול להוסיף תגובות אמוג׳י להודעות.</string> <string name="open_verb">פתח</string> @@ -751,7 +737,7 @@ <string name="smp_servers_preset_server">שרת מוגדר מראש</string> <string name="privacy_redefined">פרטיות מוגדרת מחדש</string> <string name="people_can_connect_only_via_links_you_share">אנשים יכולים להתחבר אליכם רק דרך הקישורים שאתם משתפים.</string> - <string name="opensource_protocol_and_code_anybody_can_run_servers">פרוטוקול וקוד פתוחים – כל אחד יכול להריץ את השרתים.</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">כל אחד יכול לארח שרתים.</string> <string name="onboarding_notifications_mode_periodic">תקופתי</string> <string name="restore_database_alert_desc">נא להזין את הסיסמה הקודמת לאחר שחזור גיבוי מסד הנתונים, לא ניתן לבטל פעולה זו.</string> <string name="prohibit_message_deletion">לאסור מחיקה בלתי הפיכה של הודעות.</string> @@ -760,12 +746,12 @@ <string name="network_error_desc">אנא בידקו את חיבור האינטרנט שלכם עם %1$s ונסו שוב.</string> <string name="error_smp_test_certificate">ייתכן שטביעת האצבע של התעודה בכתובת השרת שגויה</string> <string name="auth_open_chat_console">פתיחת מסוף צ׳אט</string> - <string name="auth_open_chat_profiles">פתיחת פרופילי צ׳אט</string> + <string name="auth_open_chat_profiles">שנה פרופילי צ׳אט</string> <string name="icon_descr_server_status_pending">ממתין</string> <string name="smp_servers_preset_address">כתובת שרת מוגדר מראש</string> <string name="password_to_show">סיסמה להצגה</string> <string name="onboarding_notifications_mode_title">התראות פרטיות</string> - <string name="paste_the_link_you_received">הדבק קישור שהתקבל</string> + <string name="paste_the_link_you_received">הדבק את הקישור שקיבלת</string> <string name="call_connection_peer_to_peer">עמית־לעמית</string> <string name="icon_descr_call_pending_sent">שיחה ממתינה</string> <string name="alert_text_fragment_please_report_to_developers">אנא דווחו על כך למפתחים.</string> @@ -804,7 +790,6 @@ <string name="reject_contact_button">דחיה</string> <string name="read_more_in_user_guide_with_link"><![CDATA[קראו עוד ב<font color="#0088ff">מדריך למשתמש</font>.]]></string> <string name="rate_the_app">דרגו את האפליקציה</string> - <string name="read_more_in_github">קראו עוד ב־GitHub repository שלנו.</string> <string name="read_more_in_github_with_link"><![CDATA[קראו עוד ב־<font color="#0088ff">GitHub repository</font> שלנו.]]></string> <string name="relay_server_if_necessary">יבוצע שימוש בשרת ממסר רק במידת הצורך. גורם אחר יכול לצפות בכתובת ה־IP שלך.</string> <string name="relay_server_protects_ip">שרת ממסר מגן על כתובת ה־IP שלך, אך הוא יכול לראות את משך השיחה.</string> @@ -900,14 +885,11 @@ <string name="restore_database">שחזור גיבוי מסד נתונים</string> <string name="restore_database_alert_title">לשחזר גיבוי מסד נתונים\?</string> <string name="save_passphrase_and_open_chat">שמור סיסמה ופתח את הצ׳אט</string> - <string name="save_archive">שמור ארכיון</string> <string name="select_contacts">בחירת אנשי קשר</string> - <string name="save_color">שמור צבע</string> <string name="v5_1_self_destruct_passcode">קוד גישה להשמדה עצמית</string> <string name="custom_time_unit_seconds">שניות</string> <string name="custom_time_picker_select">אישור</string> <string name="role_in_group">תפקיד</string> - <string name="network_options_revert">ביטול</string> <string name="save_and_update_group_profile">שמור ועדכן את פרופיל הקבוצה</string> <string name="save_welcome_message_question">לשמור הודעת פתיחה\?</string> <string name="current_version_timestamp">%s (נוכחי)</string> @@ -999,7 +981,6 @@ <string name="stop_chat_question">לעצור צ׳אט\?</string> <string name="stop_chat_to_export_import_or_delete_chat_database">עיצרו את הצ׳אט כדי לייצא, לייבא או למחוק את מסד הנתונים. לא תוכלו לקבל ולשלוח הודעות בזמן שהצ׳אט מופסק.</string> <string name="stop_chat_confirmation">עצור</string> - <string name="stop_chat_to_enable_database_actions">עיצרו את הצ׳אט כדי לאפשר פעולות מסד נתונים.</string> <string name="skip_inviting_button">דלג על הזמנת חברים</string> <string name="share_address">שתף כתובת</string> <string name="theme_simplex">SimpleX</string> @@ -1024,7 +1005,7 @@ <string name="group_invitation_tap_to_join">הקישו כדי להצטרף</string> <string name="network_option_tcp_connection_timeout">תום זמן חיבור TCP</string> <string name="periodic_notifications_desc">האפליקציה בודקת הודעות חדשות מעת לעת - היא משתמשת בכמה אחוזים מהסוללה ביום. האפליקציה לא משתמשת בהתראות דחיפה - נתונים מהמכשיר שלך לא נשלחים לשרתים.</string> - <string name="member_role_will_be_changed_with_notification">התפקיד ישתנה ל־\"%s\". כל חברי הקבוצה יקבלו הודעה על כך.</string> + <string name="member_role_will_be_changed_with_notification">התפקיד ישתנה ל־"%s". כל חברי הקבוצה יקבלו הודעה על כך.</string> <string name="to_connect_via_link_title">כדי להתחבר באמצעות קישור</string> <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">פלטפורמת ההודעות והיישומים המגנה על הפרטיות והאבטחה שלך.</string> <string name="alert_text_msg_bad_id">המזהה של ההודעה הבאה שגוי (קטן או שווה להודעה הקודמת). @@ -1035,8 +1016,8 @@ <string name="images_limit_title">יותר מדי תמונות!</string> <string name="thank_you_for_installing_simplex">תודה שהתקנתם את SimpleX Chat!</string> <string name="this_link_is_not_a_valid_connection_link">קישור זה אינו קישור חיבור תקין!</string> - <string name="theme_colors_section_title">צבעי ערכת נושא</string> - <string name="member_role_will_be_changed_with_invitation">התפקיד ישתנה ל־\"%s\". החבר יקבל הזמנה חדשה.</string> + <string name="theme_colors_section_title">צבעי ממשק</string> + <string name="member_role_will_be_changed_with_invitation">התפקיד ישתנה ל־"%s". החבר יקבל הזמנה חדשה.</string> <string name="smp_servers_per_user">השרתים לחיבורים חדשים של פרופיל הצ׳אט הנוכחי שלך</string> <string name="first_platform_without_user_ids">הפלטפורמה הראשונה ללא כל מזהי משתמש - פרטית בעיצובה.</string> <string name="next_generation_of_private_messaging">הדור הבא של תקשורת פרטית</string> @@ -1058,8 +1039,6 @@ <string name="messages_section_description">הגדרה זו חלה על הודעות בפרופיל הצ׳אט הנוכחי שלך</string> <string name="database_backup_can_be_restored">הניסיון לשנות את סיסמת מסד הנתונים לא הושלם.</string> <string name="color_title">כותרת</string> - <string name="should_be_at_least_one_profile">נדרש לפחות פרופיל משתמש אחד.</string> - <string name="should_be_at_least_one_visible_profile">נדרש לפחות פרופיל משתמש אחד גלוי.</string> <string name="group_is_decentralized">הקבוצה מבוזרת לחלוטין - היא גלויה רק לחברי הקבוצה.</string> <string name="to_protect_privacy_simplex_has_ids_for_queues">כדי לשמור על הפרטיות, במקום מזהי משתמש הקיימים בכל הפלטפורמות האחרות, ל־SimpleX יש מזהים לתורי הודעות, נפרדים עבור כל אחד מאנשי הקשר שלך.</string> <string name="using_simplex_chat_servers">משתמש בשרתי SimpleX Chatז</string> @@ -1075,7 +1054,7 @@ <string name="upgrade_and_open_chat">שדרג ופתח צ׳אט</string> <string name="v4_5_private_filenames_descr">כדי להגן על אזור הזמן, קובצי תמונה/קול משתמשים ב־UTC.</string> <string name="smp_server_test_upload_file">העלה קובץ</string> - <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[כדי לשמור על פרטיותך, האפליקציה כוללת <b>שירות רקע SimpleX</b> – הוא משתמש בכמה אחוזים מהסוללה ביום.]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[כדי לשפר את הפרטיות <b>SimpleX רץ ברקע</b> במקום להשתמש בpush notifications.]]></string> <string name="enter_passphrase_notification_desc">כדי לקבל התראות, יש להזין את סיסמת מסד הנתונים</string> <string name="auth_unlock">בטל נעילה</string> <string name="icon_descr_sent_msg_status_unauthorized_send">שליחה לא מורשית</string> @@ -1084,7 +1063,6 @@ <string name="to_share_with_your_contact">(כדי לשתף עם איש הקשר שלך)</string> <string name="to_start_a_new_chat_help_header">כדי להתחיל צ׳אט חדש</string> <string name="smp_servers_use_server_for_new_conn">השתמש עבור חיבורים חדשים</string> - <string name="update_onion_hosts_settings_question">לעדכן הגדרות מארחי ‪.onion‬\?‬</string> <string name="to_verify_compare">כדי לאמת הצפנה מקצה־לקצה עם איש הקשר שלכם, יש להשוות (או לסרוק) את הקוד במכשירים שלכם.</string> <string name="your_chat_profiles">פרופילי צ׳אט</string> <string name="update_network_session_mode_question">לעדכן מצב בידוד תעבורה\?</string> @@ -1099,7 +1077,7 @@ <string name="use_chat">שימוש בצ׳אט</string> <string name="update_database">עדכן</string> <string name="unknown_database_error_with_info">שגיאת מסד נתונים לא ידועה: %s</string> - <string name="rcv_group_event_updated_group_profile">פרופיל קבוצה עודכן</string> + <string name="rcv_group_event_updated_group_profile">עידכן את פרופיל הקבוצה</string> <string name="update_network_settings_confirmation">עדכן</string> <string name="unhide_chat_profile">בטל הסתרת פרופיל צ׳אט</string> <string name="unhide_profile">בטל הסתרת פרופיל</string> @@ -1151,7 +1129,6 @@ <string name="you_need_to_allow_to_send_voice">עליכם לאפשר לאיש הקשר שלכם לשלוח הודעות קוליות כדי שתוכלו לשלוח אותן.</string> <string name="gallery_video_button">סרטון</string> <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[באפשרותך <font color="#0088ff">להתחבר למפתחי SimpleX Chat כדי לשאול כל שאלה ולקבל עדכונים</font>.]]></string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[אתם קובעים דרך אילו שרתים <b>לקבל</b> את ההודעות, אנשי הקשר שלכם – השרתים דרכם אתם שולחים להם הודעות.]]></string> <string name="webrtc_ice_servers">שרתי WebRTC ICE</string> <string name="integrity_msg_skipped">%1$d הודעות שדולגו</string> <string name="custom_time_unit_weeks">שבועות</string> @@ -1162,7 +1139,7 @@ <string name="you_accepted_connection">אישרת את החיבור</string> <string name="image_descr_simplex_logo">סמל SimpleX</string> <string name="xftp_servers">שרתי XFTP</string> - <string name="alert_text_decryption_error_too_many_skipped">%1$d הודעות דולגו.</string> + <string name="alert_text_decryption_error_too_many_skipped">דילג על %1$d הודעות.</string> <string name="wrong_passphrase">סיסמת מסד נתונים שגויה</string> <string name="you_are_invited_to_group">הוזמנת לקבוצה</string> <string name="you_joined_this_group">הצטרפת לקבוצה זו</string> @@ -1225,7 +1202,7 @@ <string name="your_ice_servers">שרתי ה־ICE שלך</string> <string name="you_will_be_connected_when_your_connection_request_is_accepted">אתם תהיו מחוברים כאשר בקשת החיבור תאושר, אנא חכו או בידקו מאוחר יותר!</string> <string name="profile_will_be_sent_to_contact_sending_link">הפרופיל שלך יישלח לאיש הקשר ממנו קיבלת קישור זה.</string> - <string name="you_will_join_group">אתם תצטרפו לקבוצה אליה קישור זה מפנה ותתחברו לחברי הקבוצה.</string> + <string name="you_will_join_group">תתחבר לכל חברי הקבוצה.</string> <string name="description_you_shared_one_time_link_incognito">שיתפת קישור חד־פעמי לזהות נסתרת</string> <string name="la_notice_turn_on">הפעלת נעילה</string> <string name="incognito_random_profile">הפרופיל האקראי שלך</string> @@ -1262,15 +1239,15 @@ <string name="settings_shutdown">כיבוי</string> <string name="settings_section_title_app">אפליקציה</string> <string name="allow_to_send_files">אפשר לשלוח קבצים ומדיה.</string> - <string name="favorite_chat">מועדף</string> + <string name="favorite_chat">הוסף למועדפים</string> <string name="files_and_media">קבצים ומדיה</string> <string name="no_filtered_chats">אין צ\'אטים מסוננים</string> <string name="shutdown_alert_question">לכבות\?</string> <string name="files_and_media_prohibited">קבצים ומדיה אסורים!</string> - <string name="files_are_prohibited_in_group">קבצים ומדיה אסורים בקבוצה זו.</string> - <string name="group_members_can_send_files">חברי הקבוצה יכולים לשלוח קבצים ומדיה.</string> + <string name="files_are_prohibited_in_group">קבצים ומדיה אסורים.</string> + <string name="group_members_can_send_files">משתמשים יכולים לשלוח קבצים ומדיה.</string> <string name="settings_restart_app">איתחול</string> - <string name="unfavorite_chat">שנוא</string> + <string name="unfavorite_chat">הסר מהמועדפים</string> <string name="la_mode_off">כבוי</string> <string name="strikethrough_text">קו חוצה</string> <string name="prohibit_sending_files">לאסור שליחת קבצים ומדיה.</string> @@ -1380,7 +1357,7 @@ <string name="error_creating_member_contact">שגיאה ביצירת איש קשר</string> <string name="socks_proxy_setting_limitations"><![CDATA[<b>שימו לב</b>: ממסרי הודעות וקבצים מחוברים דרך פרוקסי SOCKS. שיחות ושליחת תצוגות מקדימות של קישורים משתמשים בחיבור ישיר.]]></string> <string name="encrypt_local_files">הצפין קבצים מקומיים</string> - <string name="v5_3_new_desktop_app">אפליקציית שולחן עבודה חדשה!</string> + <string name="v5_3_new_desktop_app">אפליקציה חדשה למחשב השולחני!</string> <string name="v5_3_new_interface_languages">6 שפות ממשק חדשות</string> <string name="v5_3_encrypt_local_files_descr">האפליקציה מצפינה קבצים מקומיים חדשים (למעט סרטונים).</string> <string name="you_can_change_it_later">ביטוי סיסמה אקראי מאוחסן בהגדרות כטקסט רגיל. @@ -1405,4 +1382,713 @@ <string name="settings_is_storing_in_clear_text">ביטוי הסיסמה מאוחסן בהגדרות כטקסט רגיל.</string> <string name="member_contact_send_direct_message">שלח הודעה ישירה</string> <string name="rcv_group_event_member_created_contact">מחובר ישירות</string> -</resources> \ No newline at end of file + <string name="terminal_always_visible">הצג קונסולה בחלון חדש</string> + <string name="v5_5_private_notes">הערות פרטיות</string> + <string name="v5_5_private_notes_descr">עם הצפנת קבצים ומדיה.</string> + <string name="v5_5_new_interface_languages">ממשק משתמש בהונגרית ובטורקית</string> + <string name="v5_5_message_delivery_descr">עם צריכת סוללה מופחתת.</string> + <string name="remote_host_was_disconnected_title">החיבור עצר</string> + <string name="agent_critical_error_title">שגיאה קריטית</string> + <string name="agent_internal_error_title">שגיאה פנימית</string> + <string name="agent_critical_error_desc">אנא דווח על כך למפתחים: +\n%s +\n +\nמומלץ לאתחל מחדש את האפליקציה.</string> + <string name="note_folder_local_display_name">הערות פרטיות</string> + <string name="call_service_notification_audio_call">שיחה קולית</string> + <string name="call_service_notification_video_call">שיחת וידאו</string> + <string name="auth_open_migration_to_another_device">פתח מסך העברה</string> + <string name="expand_verb">הרחב</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">הקוד שסרקת אינו קוד QR של קישור SimpleX.</string> + <string name="you_can_make_address_visible_via_settings">תוכל להפוך אותו לגלוי לאנשי הקשר שלך ב-SimpleX דרך ההגדרות.</string> + <string name="invalid_name">שם לא חוקי!</string> + <string name="set_passphrase">הגדר סיסמא</string> + <string name="group_member_status_unknown_short">לא ידוע</string> + <string name="past_member_vName">משתתף לשעבר %1$s</string> + <string name="conn_event_enabled_pq">הצפנה קצה-אל-קצה עמידה בפני מחשוב קוונטי</string> + <string name="group_members_2">%s ו-%s</string> + <string name="snd_group_event_member_blocked">חסמת את %s</string> + <string name="conn_event_disabled_pq">סטנדרט הצפנה קצה-אל-קצה</string> + <string name="block_for_all">חסימה לכולם</string> + <string name="block_for_all_question">לחסום משתתף להכל?</string> + <string name="error_sending_message_contact_invitation">שגיאה בשליחת הזמנה</string> + <string name="unblock_member_confirmation">בטל נעילה</string> + <string name="recent_history">צפיה בהיסטוריה</string> + <string name="v5_4_better_groups">קבוצות טובות יותר</string> + <string name="v5_4_incognito_groups_descr">צור קבוצה באמצעות פרופיל רנדומלי.</string> + <string name="v5_4_better_groups_descr">הצטרפות מהירה יותר והודעות אמינות יותר.</string> + <string name="v5_4_incognito_groups">קבוצות חשאיות</string> + <string name="v5_4_link_mobile_desktop">קשר בין האפליקציות במכשיר הנייד והמחשב השולחני! 🔗</string> + <string name="v5_5_simpler_connect_ui">הדבק קישור כדי להתחבר!</string> + <string name="v5_5_join_group_conversation_descr">היסטוריה אחרונה ובוט משופר.</string> + <string name="v5_4_link_mobile_desktop_descr">דרך פרוטוקול מאובטח עמיד בפני מחשוב קוונטי</string> + <string name="linked_mobiles">מכשירי נייד מקושרים</string> + <string name="migrate_to_device_chat_migrated">הצ׳אט הועבר!</string> + <string name="migrate_from_device_try_again">אתה יכול לנסות שוב.</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>אזהרה</b>: הארכיון יימחק.]]></string> + <string name="retry_verb">נסה שוב</string> + <string name="v5_6_quantum_resistant_encryption">הצפנה עמידה בפני מחשוב קוונטי</string> + <string name="v5_6_app_data_migration">העברת נתוני אפליקציה</string> + <string name="v5_6_quantum_resistant_encryption_descr">אפשר בצ\'אטים ישירים (בטא)!</string> + <string name="v5_6_app_data_migration_descr">העבר למכשיר אחר באמצעות קוד QR.</string> + <string name="v5_6_picture_in_picture_calls">שיחות תמונה-בתמונה</string> + <string name="v5_6_safer_groups_descr">מנהל יכול לחסום את כל החברים בקבוצה</string> + <string name="link_a_mobile">קשר מכשיר נייד</string> + <string name="scan_from_mobile">סרוק מהנייד</string> + <string name="this_device_version"><![CDATA[<i>(גרסת המכשיר הזה %s)</i>]]></string> + <string name="unlink_desktop_question">האם לבטל את הקישור למחשב השולחני?</string> + <string name="verify_code_with_desktop">אמת קוד עם המחשב השולחני</string> + <string name="linked_desktops">מחשבים שולחניים מקושרים</string> + <string name="not_compatible">לא תואם!</string> + <string name="random_port">אקראי</string> + <string name="remote_host_error_inactive"><![CDATA[הנייד <b>%s</b> לא פעיל]]></string> + <string name="open_port_in_firewall_title">פתח פורט בחומת האש</string> + <string name="remote_ctrl_error_busy">המחשב השולחני עמוס</string> + <string name="remote_ctrl_error_disconnected">המחשב השולחני מנותק</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[אתה כבר מתחבר אל <b>%1$s</b>.]]></string> + <string name="connect_plan_this_is_your_own_one_time_link">זה הקישור חד-פעמי שלך!</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[זהו הקישור שלך לקבוצה <b>%1$s</b>!]]></string> + <string name="connect_plan_group_already_exists">קבוצה כבר קיימת!</string> + <string name="restart_chat_button">התחל צ׳אט מחדש</string> + <string name="or_paste_archive_link">או הדבק קישור ארכיון</string> + <string name="migrate_to_device_download_failed">הורדה נכשלה</string> + <string name="migrate_to_device_downloading_details">מוריד פרטי קישור</string> + <string name="invalid_file_link">קישור לא תקף</string> + <string name="migrate_to_device_migrating">מתבצעת העברה</string> + <string name="migrate_to_device_database_init">מכין את ההורדה</string> + <string name="migrate_to_device_bytes_downloaded">%s הורד</string> + <string name="migrate_to_device_repeat_download">הורדה מחדש</string> + <string name="migrate_to_device_try_again">אתה יכול לנסות שוב.</string> + <string name="migrate_to_device_error_downloading_archive">שגיאה בהורדה של ארכיון</string> + <string name="migrate_to_device_file_delete_or_link_invalid">הקובץ נמחק או שהקישור אינו תקף</string> + <string name="migrate_from_device_title">העבר מכשיר</string> + <string name="migrate_from_device_to_another_device">העבר אל מכשיר אחר</string> + <string name="migrate_from_device_error_deleting_database">שגיאה במחיקת מסד נתונים</string> + <string name="migrate_from_device_error_uploading_archive">שגיאה בהעלאה לארכיון</string> + <string name="migrate_from_device_exported_file_doesnt_exist">קובץ ייצוא אינו קיים</string> + <string name="migrate_from_device_chat_should_be_stopped">כדי להמשיך, יש לעצור את הצ\'אט.</string> + <string name="migrate_from_device_database_init">מכין את העלאה</string> + <string name="migrate_from_device_stopping_chat">עצירת צ׳אט</string> + <string name="migrate_from_device_archive_and_upload">העבר לארכיון והעלאה</string> + <string name="migrate_from_device_confirm_upload">אשר העלאה</string> + <string name="migrate_from_device_archiving_database">העברת בסיס נתונים לארכיון</string> + <string name="migrate_from_device_bytes_uploaded">%s העלה</string> + <string name="migrate_from_device_uploading_archive">העלאת ארכיון</string> + <string name="migrate_from_device_cancel_migration">בטל העברה</string> + <string name="migrate_from_device_finalize_migration">סיים את ההעברה</string> + <string name="migrate_from_device_repeat_upload">העלאה מחדש</string> + <string name="migrate_from_device_or_share_this_file_link">או שתף באופן מאובטח את קישור הקובץ הזה</string> + <string name="migrate_from_device_delete_database_from_device">מחק את המסד נתונים מהמכשיר.</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">אזהרה: התחלת צ׳אט על מספר מכשירים אינה נתמכת ויכולה לגרום בבעיות בהעברת הודעות</string> + <string name="migrate_from_device_start_chat">התחל צ׳אט</string> + <string name="migrate_from_device_migration_complete">העברה הושלמה</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[אתה <b>חייב לא</b> להשתמש באותה מסד נתונים על שני מכשירים.]]></string> + <string name="migrate_from_device_check_connection_and_try_again">תבדוק את החיבור לאינטרנט ונסה שוב</string> + <string name="migrate_from_device_error_verifying_passphrase">שגיאה באימות סיסמה:</string> + <string name="migrate_from_device_verify_database_passphrase">אימות סיסמא של מסד נתונים</string> + <string name="migrate_from_device_verify_passphrase">אימות סיסמא</string> + <string name="connect_plan_you_are_already_joining_the_group_via_this_link">אתה כבר מצטרף לקבוצה באמצעות הקישור הזה.</string> + <string name="remote_host_disconnected_from"><![CDATA[נותק מהנייד <b>%s</b> מהסיבה: %s]]></string> + <string name="remote_host_error_missing"><![CDATA[הנייד <b>%s</b> חסר]]></string> + <string name="connect_plan_already_connecting">כבר מחובר!</string> + <string name="blocked_by_admin_item_description">חסום על ידי מנהל</string> + <string name="failed_to_create_user_invalid_desc">שם התצוגה הזה אינו חוקי. אנא בחר שם אחר.</string> + <string name="error_deleting_note_folder">שגיאה במחיקת הערות פרטיות</string> + <string name="loading_chats">טעינת צ׳אטים…</string> + <string name="connect_with_contact_name_question">להתחבר עם %1$s?</string> + <string name="new_chat">צ׳אט חדש</string> + <string name="or_show_this_qr_code">או הצג את הקוד הזה</string> + <string name="or_scan_qr_code">או סרוק קוד QR</string> + <string name="keep_invitation_link">שמור</string> + <string name="snd_group_event_member_unblocked">ביטלת חסימה של %s</string> + <string name="group_member_role_author">מחבר</string> + <string name="saved_message_title">הודעה נשמרה</string> + <string name="unblock_for_all_question">האם לבטל חסימה של משתתף לכולם?</string> + <string name="unblock_for_all">בטל חסימה לכולם</string> + <string name="member_blocked_by_admin">חסום על ידי מנהל</string> + <string name="member_info_member_blocked">חסום</string> + <string name="error_blocking_member_for_all">שגיאה בחסימת משתתף לכולם</string> + <string name="v5_5_simpler_connect_ui_descr">שורת החיפוש מקבלת קישורי הזמנה.</string> + <string name="v5_5_join_group_conversation">הצטרף לשיחות קבוצתיות</string> + <string name="v5_5_message_delivery">שיפור במסירת הודעות</string> + <string name="remote_ctrl_error_inactive">מחשב שולחני לא פעיל</string> + <string name="connect_plan_connect_via_link">להתחבר דרך קישור?</string> + <string name="agent_internal_error_desc">אנא דווח על כך למפתחים: +\n%s</string> + <string name="start_chat_question">האם להתחיל צ׳אט?</string> + <string name="unlink_desktop">בטל קישור</string> + <string name="call_service_notification_end_call">סיום שיחה</string> + <string name="tap_to_scan">לחץ לסריקה</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>צור קבוצה</b>: כדי ליצור קבוצה חדשה.]]></string> + <string name="clear_note_folder_question">למחוק הערות פרטיות?</string> + <string name="add_contact_tab">הוסף איש קשר</string> + <string name="you_can_view_invitation_link_again">תוכל לראות את קישור ההזמנה שוב בפרטי החיבור.</string> + <string name="keep_unused_invitation_question">האם לשמור הזמנה שלא נעשה בה שימוש?</string> + <string name="the_text_you_pasted_is_not_a_link">הטקסט שהדבקת אינו קישור של SimpleX.</string> + <string name="show_slow_api_calls">הצג קריאות API איטיות</string> + <string name="developer_options_section">אפשרויות למפתח</string> + <string name="create_another_profile_button">צור פרופיל</string> + <string name="rcv_group_and_other_events">בנוסף ל- %d אירועים אחרים</string> + <string name="profile_update_event_set_new_address">איש הקשר הגדיר כתובת חדשה</string> + <string name="contact_tap_to_connect">לחץ לחיבור</string> + <string name="unable_to_open_browser_desc">דפדפן האינטרנט המוגדר כברירת מחדל נדרש לשיחות. אנא הגדר דפדפן ברירת מחדל במערכת, ושתף מידע נוסף עם המפתחים.</string> + <string name="e2ee_info_no_pq_short">השיחה הזו מוגנת באמצעות הצפנה קצה-אל-קצה.</string> + <string name="e2ee_info_pq_short">השיחה הזו מוגנת באמצעות הצפנה קצה-אל-קצה עמידה בפני מחשוב קוונטי.</string> + <string name="connect_plan_this_is_your_own_simplex_address">זהו כתובת ה-SimpleX שלך!</string> + <string name="remote_host_error_timeout"><![CDATA[הסתיים פסק הזמן הקצוב להתחברות למכשיר נייד <b>%s</b>]]></string> + <string name="clear_note_folder_warning">כל ההודעות יימחקו- לא ניתן לשוב לאחור</string> + <string name="block_member_desc">כל ההודעות החדשות מ %s יהיו מוחבאות</string> + <string name="remote_ctrl_error_bad_state">החיבור לשולחן העבודה נמצא במצב לקוי</string> + <string name="correct_name_to">לתקן את השם ל-%s?</string> + <string name="migrate_from_device_all_data_will_be_uploaded">כל אנשי הקשר שלך, שיחות וקבצים יהיו מוצפנים באופן מאובטח ויעלו בחתיכות לריליים של XFTP שהוגדרו.</string> + <string name="migrate_to_device_apply_onion">החל</string> + <string name="la_app_passcode">קוד עבור אפליקציה</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>הוסף איש קשר</b>: כדי ליצור קישור הזמנה חדש, או להתחבר דרך קישור שקיבלת.]]></string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>שים לב</b>: שימוש באותו מסד נתונים על שני מכשירים ישבור את הפענוח ההצפנה של ההודעות מהחיבורים שלך, נועד כהגנה בטחונית.]]></string> + <string name="chat_is_stopped_you_should_transfer_database">הצ\'אט נסגר. אם כבר השתמשת במסד נתונים זה במכשיר אחר, עליך להעבירו חזרה לפני שתתחיל בצ\'אט.</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[בחר <i>העבר ממכשיר אחר</i> במכשיר החדש וסרוק קוד QR.]]></string> + <string name="migrate_to_device_confirm_network_settings">אשר הגדרות רשת</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">אשר שאתה זוכר את סיסמת מסד הנתונים כדי להעביר אותו.</string> + <string name="connected_desktop">התחבר למחשב השולחני</string> + <string name="migrate_from_device_creating_archive_link">יוצר קישור ארכיון</string> + <string name="creating_link">יוצר קישור…</string> + <string name="delete_and_notify_contact">מחק והודע לאיש קשר</string> + <string name="delete_messages__question">האם למחוק %d הודעות?</string> + <string name="remote_ctrl_error_bad_version">למחשב השולחני יש גרסה שאינה נתמכת. אנא, ודא שאתה משתמש באותה גרסה על שני המכשירים</string> + <string name="remote_ctrl_error_bad_invitation">למחשב השולחני יש קוד הזמנה שגוי</string> + <string name="migrate_to_device_downloading_archive">מוריד ארכיון</string> + <string name="enable_camera_access">אפשר גישה למצלמה</string> + <string name="encryption_renegotiation_error">שגיאת משא ומתן מחדש של הצפנה</string> + <string name="alert_text_encryption_renegotiation_failed">המשא ומתן מחדש של ההצפנה נכשל.</string> + <string name="migrate_to_device_enter_passphrase">הזן סיסמא</string> + <string name="error_alert_title">שגיאה</string> + <string name="error_creating_message">שגיאה ביצירת הודעה</string> + <string name="migrate_from_device_error_exporting_archive">שגיאה בייצוא מסד נתוני צ\'אט</string> + <string name="unable_to_open_browser_title">שגיאה בפתיחת דפדפן</string> + <string name="migrate_from_device_error_saving_settings">שגיאה בשמירת אפשרויות</string> + <string name="migrate_to_device_finalize_migration">סיים את ההעברה על מכשיר נוסף.</string> + <string name="invalid_qr_code">קוד QR לא חוקי</string> + <string name="recent_history_is_not_sent_to_new_members">ההיסטוריה לא נשלחת לחברים חדשים.</string> + <string name="migrate_to_device_import_failed">יבוא נכשל</string> + <string name="migrate_to_device_importing_archive">מייבא ארכיון</string> + <string name="connect_plan_join_your_group">האם להצטרף אל הקבוצה?</string> + <string name="new_desktop"><![CDATA[<i>(חדש)</i>]]></string> + <string name="profile_update_event_member_name_changed">משתתף %1$s השתנה ל-%2$s</string> + <string name="e2ee_info_no_pq"><![CDATA[הודעות, קבצים ושיחות מוגנים על ידי <b>הצפנה מקצה לקצה</b> עם סודיות קדימה מושלמת, הכחשה ושחזור לאחר פריצה.]]></string> + <string name="e2ee_info_pq"><![CDATA[הודעות, קבצים ושיחות מוגנים על ידי <b>הצפנה מקצה לקצה עמידה בפני מחשוב קוונטי</b> עם סודיות קדימה מושלמת, הכחשה ושחזור לאחר פריצה.]]></string> + <string name="migrate_from_another_device">העבר ממכשיר אחר</string> + <string name="migrate_to_device_title">העבר לכאן</string> + <string name="remote_host_error_busy"><![CDATA[הנייד <b>%s</b> תפוס]]></string> + <string name="multicast_discoverable_via_local_network">ניתן לגילוי דרך רשת מקומית</string> + <string name="camera_not_available">מצלמה לא זמינה</string> + <string name="no_connected_mobile">אין מכשיר נייד מחובר</string> + <string name="paste_archive_link">הדבק לינק ארכיון</string> + <string name="migrate_to_device_confirm_network_settings_footer">אנא אשר שהגדרות הרשת נכונות למכשיר זה.</string> + <string name="refresh_qr_code">רענון</string> + <string name="migrate_to_device_repeat_import">יבוא מחדש</string> + <string name="v5_6_safer_groups">קבוצות בטוחות יותר</string> + <string name="verify_connection">אימות חיבור</string> + <string name="open_port_in_firewall_desc">כדי לאפשר לאפליקציה במכשיר הנייד להתחבר למחשב השולחני, פתח את הפורט הזה בחומת האש שלך, אם היא מופעלת</string> + <string name="connect_plan_you_have_already_requested_connection_via_this_address">כבר ביקשת להתחבר באמצעות כתובת זו!</string> + <string name="rcv_group_event_member_unblocked">ביטלת חסימה של %s</string> + <string name="unblock_member_button">בטל חסימה של משתתף</string> + <string name="unblock_member_question">האם לבטל חסימה עבור משתתף?</string> + <string name="migrate_from_device_upload_failed">העלאה נכשלה</string> + <string name="v5_6_picture_in_picture_calls_descr">השתמש באפליקציה במהלך השיחה.</string> + <string name="show_internal_errors">הצג שגיאות פנימיות</string> + <string name="enable_sending_recent_history">שלח עד 100 הודעות אחרונות לחברים חדשים.</string> + <string name="disable_sending_recent_history">אל תשלח היסטוריה לחברים חדשים.</string> + <string name="recent_history_is_sent_to_new_members">עד 100 ההודעות האחרונות נשלחות לחברים חדשים.</string> + <string name="session_code">קוד סשן</string> + <string name="remote_host_error_disconnected"><![CDATA[מכשיר נייד <b>%s</b> התנתק]]></string> + <string name="remote_host_error_bad_state"><![CDATA[החיבור לנייד <b>%s</b> נמצא במצב לקוי]]></string> + <string name="remote_host_error_bad_version"><![CDATA[הנייד <b>%s</b> משתמש בגרסה שאינה נתמכת. אנא, ודא שאתה משתמש באותה גרסה על שני המכשירים]]></string> + <string name="failed_to_create_user_invalid_title">שם תצוגה לא חוקי!</string> + <string name="search_or_paste_simplex_link">חפש או הדבק קישור של SimpleX</string> + <string name="verify_code_on_mobile">אמת קוד במכשיר הנייד</string> + <string name="remote_ctrl_was_disconnected_title">החיבור עצר</string> + <string name="non_content_uri_alert_title">נתיב קובץ לא חוקי</string> + <string name="non_content_uri_alert_text">שיתפת נתיב קובץ לא חוקי. דווח על הבעיה למפתחי האפליקציה.</string> + <string name="moderated_items_description">%1$d הודעות נחסמו על ידי %2$s</string> + <string name="marked_deleted_items_description">%d הודעות סומנו כנמחקות</string> + <string name="connect_plan_repeat_connection_request">האם לחזור על בקשת החיבור?</string> + <string name="blocked_item_description">חסום</string> + <string name="blocked_items_description">%d הודעות נחסמו</string> + <string name="loading_remote_file_desc">אנא המתן בזמן שהקובץ נטען מהמכשיר הנייד המקושר</string> + <string name="button_remove_member_question">האם להסיר משתתף?</string> + <string name="disconnect_remote_host">מנותק</string> + <string name="disconnect_desktop_question">האם לנתק את המחשב השולחני?</string> + <string name="disconnect_remote_hosts">נתק מכשירי נייד</string> + <string name="only_one_device_can_work_at_the_same_time">רק מכשיר אחד יכול לעבוד בו זמנית.</string> + <string name="waiting_for_desktop">ממתין למחשב השולחני…</string> + <string name="open_on_mobile_and_scan_qr_code"><![CDATA[פתח <i>שימוש ממחשב שולחני</i> באפליקציה בנייד וסרוק קוד QR.]]></string> + <string name="database_migration_in_progress">העברת מסד הנתונים בתהליך. +\nזה עשוי לקחת כמה דקות.</string> + <string name="blocked_by_admin_items_description">%d הודעות נחסמו על ידי מנהל</string> + <string name="app_was_crashed">מסך קרס</string> + <string name="video_decoding_exception_desc">לא ניתן לפענח את הווידאו. אנא נסה וידאו אחר או צור קשר עם המפתחים.</string> + <string name="share_this_1_time_link">שתף את הקישור הזמנה החד-פעמי הזה</string> + <string name="tap_to_paste_link">לחץ להדבקת קישור</string> + <string name="loading_remote_file_title">טעינה של הקובץ</string> + <string name="settings_section_title_use_from_desktop">שימוש ממחשב שולחני</string> + <string name="rcv_group_event_member_blocked">חסומים %s</string> + <string name="rcv_direct_event_contact_deleted">איש קשר נמחק</string> + <string name="rcv_group_events_count">%d אירועי קבוצה</string> + <string name="group_members_n">%s, %s ו-%d חברים</string> + <string name="profile_update_event_contact_name_changed">איש הקשר %1$s השתנה ל-%2$s</string> + <string name="profile_update_event_removed_address">כתובת איש קשר הוסרה</string> + <string name="profile_update_event_removed_picture">תמונת פרופיל הוסרה</string> + <string name="profile_update_event_set_new_picture">הגדיר תמונת פרופיל חדשה</string> + <string name="profile_update_event_updated_profile">עדכן פרופיל</string> + <string name="group_member_status_unknown">מצב לא ידוע</string> + <string name="info_row_created_at">נוצר ב</string> + <string name="share_text_created_at">נוצר בתאריך: %s</string> + <string name="message_too_large">הודעה ארוכה מדי</string> + <string name="welcome_message_is_too_long">הודעת ברוכים הבאים ארוכה מדי</string> + <string name="connected_mobile">התחבר לנייד</string> + <string name="connected_to_desktop">התחבר למחשב השולחני</string> + <string name="connected_to_mobile">התחבר לנייד</string> + <string name="connecting_to_desktop">התחבר למחשב השולחני</string> + <string name="connect_to_desktop">חיבור למחשב השולחני</string> + <string name="desktop_app_version_is_incompatible">גרסת אפליקציית שולחן העבודה %s אינה תואמת לאפליקציה זו.</string> + <string name="devices">מכשירים</string> + <string name="error">שגיאה</string> + <string name="desktop_incompatible_version">גירסא לא מתאימה</string> + <string name="found_desktop">מצא מחשב שולחני</string> + <string name="new_mobile_device">מכשיר נייד חדש</string> + <string name="this_device_name">השם של המכשיר הזה</string> + <string name="multicast_connect_automatically">התחבר אוטומטית</string> + <string name="error_showing_message">שגיאה בהצגת הודעה</string> + <string name="error_showing_content">שגיאה בהצגת תוכן</string> + <string name="rcv_group_event_1_member_connected">%s מחובר</string> + <string name="block_member_confirmation">חסום</string> + <string name="block_member_button">חסום משתתף</string> + <string name="block_member_question">לחסום משתתף?</string> + <string name="unblock_member_desc">הודעות מ-%s יוצגו!</string> + <string name="create_group_button">צור קבוצה</string> + <string name="this_device">המכשיר הזה</string> + <string name="v5_4_more_things_descr">- אפשרות להודיע לאנשי קשר שנמחקו. +\n- שמות פרופיל עם רווחים. +\n- ועוד!</string> + <string name="v5_4_block_group_members_descr">כדי להסתיר הודעות לא רצויות.</string> + <string name="v5_4_block_group_members">תחסום חברים בקבוצה</string> + <string name="desktop_device">מחשב שולחני</string> + <string name="desktop_address">כתובת של המחשב השולחני</string> + <string name="paste_desktop_address">הדבק כתובת של המחשב שולחני</string> + <string name="scan_qr_code_from_desktop">סרוק קוד QR מהמחשב השולחני</string> + <string name="connect_plan_connect_to_yourself">להתחבר אל עצמך?</string> + <string name="verify_connections">אימות חיבורים</string> + <string name="discover_on_network">גלה דרך רשת מקומית</string> + <string name="linked_desktop_options">האפשרויות של המחשב השולחני מקושרות</string> + <string name="desktop_devices">מחשבים שולחניים</string> + <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">אתה כבר מתחבר באמצעות קישור חד-פעמי זה!</string> + <string name="connect_plan_open_group">פתח קבוצה</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[אתה כבר בקבוצה <b>%1$s</b>.]]></string> + <string name="connect_plan_repeat_join_request">האם לחזור על בקשת הצטרפות?</string> + <string name="bad_desktop_address">הכתובת של המחשב השולחני שגויה</string> + <string name="remote_host_was_disconnected_toast"><![CDATA[מכשיר נייד <b>%s</b> התנתק]]></string> + <string name="this_device_name_shared_with_mobile">שם המכשיר ישותף עם מכשיר הנייד המחובר.</string> + <string name="enter_this_device_name">הזן את שם המכשיר הזה…</string> + <string name="possible_slow_function_desc">ביצוע הפונקציה לוקח זמן רב מדי: %1$d שניות: %2$s</string> + <string name="possible_slow_function_title">פונקציה איטית</string> + <string name="desktop_connection_terminated">החיבור הופסק</string> + <string name="remote_ctrl_disconnected_with_reason">נותק מהסיבה: %s</string> + <string name="waiting_for_mobile_to_connect">ממתין לחיבור עם מכשיר נייד:</string> + <string name="create_chat_profile">צור פרופיל צאט</string> + <string name="connect_plan_already_joining_the_group">כבר הצטרפת לקבוצה!</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[אתה כבר מצטרף לקבוצה <b>%1$s</b>.]]></string> + <string name="remove_member_button">הסר משתתף</string> + <string name="remote_ctrl_error_timeout">הסתיים פסק הזמן הקצוב להתחברות למחשב השולחני</string> + <string name="v5_7_network_descr">חיבור רשת יותר אמין.</string> + <string name="network_smp_proxy_fallback_allow_protected">מתי שהIP מוסתר</string> + <string name="network_smp_proxy_mode_unknown_description">השתמש במסלול פרטי עם שרתים לא ידועים</string> + <string name="message_delivery_warning_title">אזהרה על אופן שליחת ההודעה</string> + <string name="private_routing_show_message_status">הראה סטטוס הודעה</string> + <string name="v5_8_private_routing">מסלול פרטי להודעה 🚀</string> + <string name="share_text_message_status">סטטוס הודעה:%s</string> + <string name="remote_ctrl_connection_stopped_desc">אנא וודא שהמכשיר והמחשב מחוברים לאותה רשת מקומית, ושהפיירוול של המחשב מאפשר את החיבור. +\nאנא תשתף כל בעיות אחרות עם המפתחים.</string> + <string name="v5_8_safe_files_descr">"אשר קבלת קבצים משרתים לא מוכרים"</string> + <string name="snd_error_expired">שגיאת רשת - ההודעה פגה תוקף לאחר ריבוי ניסיונות שליחה</string> + <string name="ci_status_other_error">שגיאה:%1$s</string> + <string name="feature_roles_all_members">כל החברים</string> + <string name="v5_7_forward_descr">הודעת המקור נשארת פרטית</string> + <string name="v5_7_call_sounds">רינגטון ל שיחה נכנסת</string> + <string name="file_error_no_file">קובץ לא נמצא - ככל הנראה הקובץ נמחק או בוטל</string> + <string name="file_error_relay">שגיאת שרת קבצים:%1$s</string> + <string name="download_file">הורדה</string> + <string name="network_smp_proxy_fallback_prohibit">לא</string> + <string name="v5_8_chat_themes">ערכת נושא חדשה לצא\'ט</string> + <string name="v5_7_shape_profile_images_descr">מרובע, עיגול, או כל דבר ביניהם</string> + <string name="v5_7_forward">העבר ושמור הודעות</string> + <string name="v5_7_call_sounds_descr">מתי שמתחבר שחיות קוליות ווידאו.</string> + <string name="cannot_share_message_alert_title">לא ניתן לשלוח הודעה</string> + <string name="voice_messages_not_allowed">הודעות קוליות לא מאופשרות</string> + <string name="temporary_file_error">שגיאת קובץ זמני</string> + <string name="files_and_media_not_allowed">קבצים ומדיה לא מאופשרים</string> + <string name="file_error">שגיאת קובץ</string> + <string name="network_smp_proxy_mode_unprotected_description">השתמש במסלול פרטי עם שרתים לא ידועים מתי שכתובת הIP לא מוגנת.</string> + <string name="network_smp_proxy_fallback_allow_protected_description">שלח הודעות ישירות מתי שכתובת הIP מוגנת ואתה או שרת היעד לא תומך במסלול פרטי.</string> + <string name="private_routing_explanation">כדי להגן על כתובת הIP שלך, מסלול פרטי משתמש בשרתי הSMP שלך כדי להעביר את ההודעות.</string> + <string name="network_smp_proxy_mode_never_description">אל תשתמש במסלול פרטי</string> + <string name="network_smp_proxy_mode_always_description">תמיד השתמש בנתיב פרטי</string> + <string name="permissions_grant_in_settings">תאפשר בהגדרות</string> + <string name="permissions_grant">תאפשר הרשאות בשביל להתקשר</string> + <string name="permissions_camera_and_record_audio">מצלמה ומיקרופון</string> + <string name="permissions_open_settings">פתח הגדרות</string> + <string name="permissions_find_in_settings_and_grant">מצא את ההרשאה בהגדרות המכשיר ותאפשר אותה ידנית</string> + <string name="audio_device_speaker">רמקול</string> + <string name="audio_device_wired_headphones">אוזניות</string> + <string name="audio_device_earpiece">אוזניות</string> + <string name="settings_section_title_user_theme">ערכת נושא לפרופיל</string> + <string name="settings_section_title_chat_colors">צבעי הצא\'ט</string> + <string name="chat_list_always_visible">הראה רשימת צא\'טים בחלון חדש</string> + <string name="info_row_file_status">מצב הקובץ</string> + <string name="info_row_message_status">סטטוס הודעה</string> + <string name="share_text_file_status">מצב הקובץ:%s</string> + <string name="message_queue_info_none">ריק</string> + <string name="color_mode_dark">כהה</string> + <string name="color_mode">ערכת נושא</string> + <string name="theme_black">שחור</string> + <string name="color_mode_light">בהיר</string> + <string name="reset_single_color">אפס צבע</string> + <string name="theme_destination_app_theme">ערכת נושא לאפליקציה</string> + <string name="color_sent_quote">שלח תגובה</string> + <string name="color_received_quote">התקבל תגובה</string> + <string name="color_wallpaper_background">טפט רקע</string> + <string name="theme_remove_image">הסר תמונה</string> + <string name="wallpaper_scale_repeat">חזור</string> + <string name="wallpaper_scale_fill">מלא</string> + <string name="chat_theme_reset_to_app_theme">אפס ערכת נושא לאפליקציה</string> + <string name="wallpaper_scale_fit">התאם</string> + <string name="wallpaper_preview_hello_alice">צהריים טובים</string> + <string name="wallpaper_preview_hello_bob">בוקר טוב!</string> + <string name="wallpaper_advanced_settings">הגדרות מתקדמות</string> + <string name="chat_theme_set_default_theme">הגדר ערכת נושא ברירת מחדל</string> + <string name="chat_theme_reset_to_user_theme">אפס ערכת נושא למשתמש</string> + <string name="chat_theme_apply_to_mode">החל ל</string> + <string name="chat_theme_apply_to_all_modes">ערכת נושא</string> + <string name="group_members_can_send_simplex_links">משתמשים יכולים לשלוח קישורי SimpleXצ</string> + <string name="v5_8_chat_themes_descr">עשה שהצאט\'ים שלך יראו אחרת!</string> + <string name="v5_7_network">הגדרות רשת</string> + <string name="remote_ctrl_connection_stopped_identity_desc">הקישור הזה שומש כבר במכשיר אחר, אנא צור קישור חדש במחשב.</string> + <string name="network_type_ethernet">חיבור קווי</string> + <string name="network_type_cellular">סלולרי</string> + <string name="saved_description">נשמר</string> + <string name="saved_from_description">נשמר מ%s</string> + <string name="forwarded_chat_item_info_tab">הועבר</string> + <string name="forwarded_description">הועבר</string> + <string name="settings_section_title_network_connection">מחובר לרשת</string> + <string name="network_type_network_wifi">Wi-Fi אלחוטי</string> + <string name="copy_error">שגיאה בהעתקה</string> + <string name="chat_theme_apply_to_dark_mode">ערכת נושא כהה</string> + <string name="settings_section_title_chat_theme">ערכת נושא</string> + <string name="dark_mode_colors">צבעי מצב כהה</string> + <string name="snd_error_relay">שגיאה בשרת היעד:%1$s</string> + <string name="network_smp_proxy_fallback_prohibit_description">"אל תשלח הודעות ישירות אפילו אם שרת היעד לא תומך במסלול פרטי"</string> + <string name="v5_8_message_delivery">שיפור בשליחת הודעות</string> + <string name="permissions_camera">מצלמה</string> + <string name="allow_to_send_simplex_links">אפשר שליחת קישורי SimpleX</string> + <string name="feature_roles_owners">בעלים</string> + <string name="feature_roles_admins">מנהלים</string> + <string name="feature_enabled_for">מופעל עבור</string> + <string name="file_not_approved_title">שרתים לא ידועים!</string> + <string name="file_not_approved_descr">בלי טור או VPN, כתובת הIP שלך תהיה חשופה למתווכי XFTP האלה: +\n%1$s</string> + <string name="forwarded_from_chat_item_info_title">הועבר מ</string> + <string name="saved_chat_item_info_tab">נשמר</string> + <string name="saved_from_chat_item_info_title">נשמר מ</string> + <string name="recipients_can_not_see_who_message_from">מקבלי ההודעה לא יוכלו לראות מי שלח את ההודעה</string> + <string name="forward_chat_item">העבר</string> + <string name="forward_message">מעביר הודעה…</string> + <string name="simplex_links_not_allowed">קישורי SimpleX לא מאופשרים</string> + <string name="network_smp_proxy_mode_private_routing">מסלול פרטי</string> + <string name="network_smp_proxy_mode_unprotected">לא מוגן</string> + <string name="network_smp_proxy_mode_always">תמיד</string> + <string name="network_smp_proxy_mode_unknown">מתווכים לא ידועים</string> + <string name="network_smp_proxy_mode_never">לעולם לא</string> + <string name="network_smp_proxy_fallback_allow">כן</string> + <string name="network_smp_proxy_fallback_allow_downgrade">אפשר שינמוך</string> + <string name="network_smp_proxy_fallback_allow_description">שלח הודעות ישירות מתי שאתה או שרת היעד לא תומך במסלול פרטי</string> + <string name="permissions_record_audio">מיקרופון</string> + <string name="permissions_required">הרשאות שניתנו</string> + <string name="protect_ip_address">כתובת IP מוגנת</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">האפליקציה תשאל כדי לאשר הורדות משרתי קבצים לא ידועים (למעט שרתי טור או מתי שפרוקסי SOCKS מופעל).</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">בלי טור או VPN, כתובת הIP שלך תהיה גלויה לשרתי קבצים.</string> + <string name="settings_section_title_files">קבצים</string> + <string name="settings_section_title_profile_images">תמונות פרופיל</string> + <string name="settings_section_title_private_message_routing">מסלול הודעה פרטית</string> + <string name="simplex_links">קישורי SimpleX</string> + <string name="network_type_no_network_connection">אין חיבור לרשת</string> + <string name="network_type_other">אחר</string> + <string name="chat_theme_apply_to_light_mode">ערכת נושא בהירה</string> + <string name="color_mode_system">מערכת</string> + <string name="error_showing_desktop_notification">שגיאה בהצגת התראה, צור קשר עם המפתחים</string> + <string name="smp_proxy_error_connecting">שגיאה בהתחברות לשרת %1$s, אנא נסה מאוחר יותר</string> + <string name="message_forwarded_desc">אין עדיין חיבור ישיר, ההודעה תעובר ע"י מנהל.</string> + <string name="member_inactive_title">חבר לא פעיל</string> + <string name="xftp_servers_other">שרתי XFTP אחרים</string> + <string name="subscription_percentage">הראה אחוזים</string> + <string name="app_check_for_updates_disabled">מושבת</string> + <string name="app_check_for_updates_stable">יציבה</string> + <string name="app_check_for_updates_installed_successfully_title">הותקן בהצלחה</string> + <string name="app_check_for_updates_button_install">התקן עדכון</string> + <string name="app_check_for_updates_button_open">פתח מיקום קובץ</string> + <string name="app_check_for_updates_installed_successfully_desc">אנא הפעל מחדש את האפילקציה.</string> + <string name="app_check_for_updates_button_remind_later">הזכר מאוחר יותר</string> + <string name="app_check_for_updates_button_skip">דלג על הגרסא הזאת</string> + <string name="app_check_for_updates_notice_disable">השבת</string> + <string name="appearance_font_size">גדול פונט</string> + <string name="current_user">פרופיל נוכחי</string> + <string name="servers_info_messages_sent">הודעות שנשלחו</string> + <string name="expired_label">פג תוקף</string> + <string name="send_errors">שגיאות בשליחה</string> + <string name="size">גודל</string> + <string name="uploaded_files">קבצים שהועלו</string> + <string name="v5_7_quantum_resistant_encryption_descr">יחול בצ\'אטים ישירים!</string> + <string name="audio_device_bluetooth">בלוטוס</string> + <string name="privacy_media_blur_radius">טשטש מדיה</string> + <string name="privacy_media_blur_radius_off">כבוי</string> + <string name="privacy_media_blur_radius_medium">בינוני</string> + <string name="privacy_media_blur_radius_strong">חזק</string> + <string name="member_info_member_disabled">מושבת</string> + <string name="member_info_member_inactive">לא פעיל</string> + <string name="servers_info_details">פרטים</string> + <string name="servers_info_modal_error_title">שגיאה</string> + <string name="servers_info_reconnect_server_error">שגיאה בהתחברות מחדש לשרת</string> + <string name="servers_info_reconnect_servers_error">שגיאה בהתחברות מחדש לשרתים</string> + <string name="servers_info_sessions_errors">שגיאות</string> + <string name="servers_info_subscriptions_section_header">מקבל ההודעות</string> + <string name="servers_info_proxied_servers_section_header">שרתי פרוקסי</string> + <string name="servers_info_reconnect_all_servers_button">התחבר מחדש לכל השרתים</string> + <string name="servers_info_reconnect_server_title">להתחבר מחדש לשרת?</string> + <string name="servers_info_reconnect_servers_title">להתחבר מחדש לשרתים?</string> + <string name="servers_info_statistics_section_header">סטטיסטיקות</string> + <string name="servers_info_subscriptions_total">סך הכל</string> + <string name="attempts_label">ניסיונות</string> + <string name="completed">הושלם</string> + <string name="connections">חיבורים</string> + <string name="deleted">נמחק</string> + <string name="deletion_errors">שגיאות במחיקה</string> + <string name="servers_info_reset_stats_alert_error_title">שגיאה באיפוס הסטטיסטיקה</string> + <string name="other_label">אחר</string> + <string name="secured">מאובטח</string> + <string name="sent_directly">שלח ישירות</string> + <string name="sent_via_proxy">נשלח דרך פרוקסי</string> + <string name="downloaded_files">קבצים שהורדו</string> + <string name="download_errors">שגיאות בהורדה</string> + <string name="open_server_settings_button">פתח הגדרות שרת</string> + <string name="server_address">כתובת שרת</string> + <string name="snd_error_quota">חריגה מהקיבולת - הנמען לא קיבל הודעות שנשלחו בעבר.</string> + <string name="app_check_for_updates">בדוק עבור עדכונים</string> + <string name="servers_info_sessions_connected">מחובר</string> + <string name="servers_info_connected_servers_section_header">שרתים מחוברים</string> + <string name="app_check_for_updates_notice_title">בודק עבור עדכונים חדשים</string> + <string name="servers_info_sessions_connecting">מתחבר</string> + <string name="created">נוצר</string> + <string name="message_forwarded_title">הודעה הועברה</string> + <string name="member_inactive_desc">ההודעה תוכל להימסר מאוחר יותר אם החבר יהפוך לפעיל.</string> + <string name="servers_info_messages_received">הודעות שהתקבלו</string> + <string name="servers_info_reset_stats">אפס את כל הסטטיסטיקות</string> + <string name="wallpaper_scale">גודל</string> + <string name="simplex_links_are_prohibited_in_group">קישורי SimpleX לא מאופשרים בקבוצה הזו.</string> + <string name="scan_paste_link">סרוק/ הדבק קישור</string> + <string name="please_try_later">אנא נסה מאוחר יותר</string> + <string name="smp_servers_other">שרתי SMP אחרים</string> + <string name="servers_info_reset_stats_alert_confirm">אפס</string> + <string name="servers_info_uploaded">הועלה</string> + <string name="servers_info_detailed_statistics">סטטיסטיקה מפורטת</string> + <string name="app_check_for_updates_beta">בטא</string> + <string name="servers_info_reset_stats_alert_title">לאפס את כל הסטטיסטיקות?</string> + <string name="decryption_errors">שגיאות בפענוח</string> + <string name="other_errors">שגיאות אחרות</string> + <string name="upload_errors">שגיאה בהעלאה</string> + <string name="app_check_for_updates_download_completed_title">יורד עדכון לאפליקציה</string> + <string name="app_check_for_updates_download_started">מוריד עדכון לאפליקציה, אל תסגור את האפליקציה</string> + <string name="all_users">כל הפרופילים</string> + <string name="servers_info_files_tab">קבצים</string> + <string name="servers_info_missing">אין מידע, נסה לרענן</string> + <string name="servers_info">מידע על השרתים</string> + <string name="servers_info_detailed_statistics_received_total">התקבל סה"כ</string> + <string name="servers_info_detailed_statistics_receive_errors">התקבלו שגיאות</string> + <string name="reconnect">התחבר מחדש</string> + <string name="servers_info_detailed_statistics_sent_messages_header">שלח הודעות</string> + <string name="servers_info_detailed_statistics_sent_messages_total">נשלח בסה"כ</string> + <string name="smp_server">שרת SMP</string> + <string name="xftp_server">שרת XFTP</string> + <string name="privacy_media_blur_radius_soft">חלש</string> + <string name="deleted_chats">אנשי קשר בארכיון</string> + <string name="report_item_archived">דיווח בארכיון</string> + <string name="report_reason_community">הפרה של הנחיות קהילתיות</string> + <string name="n_file_errors">%1$d שגיאת קבצים:\n%2$s</string> + <string name="forward_files_in_progress_desc">%1$d הקבצים עדיין בהורדה.</string> + <string name="accept_conditions">הסכם לתנאים</string> + <string name="forward_files_not_accepted_desc">%1$d ההורדה של הקובץ/ים עדיין לא הסתיימה.</string> + <string name="operator_conditions_accepted">התנאים המקובלים עלי</string> + <string name="chat_item_ttl_year">שנה</string> + <string name="report_reason_other">סיבה אחרת</string> + <string name="business_address">כתובת עסקית</string> + <string name="v6_1_message_dates_descr">שיפור בסידור של הודעות לפי תאריכים.</string> + <string name="v6_3_better_groups_performance">שיפור ביצועים לקבוצות</string> + <string name="info_view_call_button">התקשר</string> + <string name="cant_call_member_alert_title">לא ניתן להתקשר לחבר קבוצה</string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[התנאי שימוש יאושרו למפעיל/ים: <b>%s</b>.]]></string> + <string name="report_archive_alert_title">להעביר דיווח לארכיון?</string> + <string name="settings_section_title_chat_database">מסד נתונים של הצא\'טים</string> + <string name="onboarding_notifications_mode_periodic_desc_short">בדוק עבור הודעות חדשות כל 10 דקות</string> + <string name="operator_conditions_accepted_on">אישרת את תנאי השימוש ב:%s.</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">הצ\'אט יימחק עבור כל החברים - לא ניתן לבטל את זה!</string> + <string name="call_desktop_permission_denied_chrome">לחץ על כפתור מידע ליד שדה כתובת כדי לאפשר שימוש במיקרופון.</string> + <string name="operator_conditions_accepted_for_some"><![CDATA[כבר הסכמת לתנאי שימוש למפעיל/ים האלו: <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[התנאי שימוש יאושרו למפעיל/ים: <b>%s</b>.]]></string> + <string name="v6_0_your_contacts_descr">העבר אנשי קשר לארכיון לשוחח מאוחר יותר</string> + <string name="change_automatic_deletion_question">לשנות את מחיקת ההודעה האוטומטית?</string> + <string name="connect_plan_chat_already_exists">הצ\'אט כבר קיים!</string> + <string name="chat_list_all">הכל</string> + <string name="n_other_file_errors">%1$d שגיאה/ות קובץ אחר/ות.</string> + <string name="forward_files_failed_to_receive_desc">%1$d הקובץ/ים נכשל/ו בהורדה.</string> + <string name="block_members_desc">כל ההודעות החדשות מחברים אלו יוסתרו!</string> + <string name="block_members_for_all_question">לחסום את חברי הקבוצה לכולם?</string> + <string name="report_item_archived_by">הדיווח הועבר לארכיון ע\"י %s</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[ניתן להשתמש בקישור חד פעמי <i> עם איש קשר אחד בלבד</i> - שתף באופן אישי או באמצעות כל מסנג\'ר.]]></string> + <string name="v6_0_privacy_blur">טשטוש בשביל שיפור הפרטיות.</string> + <string name="group_reports_active_one">דיווח 1</string> + <string name="delete_chat_list_warning">כל הצ\'אטים יוסרו מהרשימת %s, והרשימה תימחק</string> + <string name="change_list">שנה רשימה</string> + <string name="allow_calls_question">לאפשר שיחות?</string> + <string name="delete_chat_for_self_cannot_undo_warning">הצ\'אט יימחק עבורך - אי אפשר לבטל את זה!</string> + <string name="calls_prohibited_alert_title">שיחות לא מורשות!</string> + <string name="privacy_chat_list_open_links_ask">שאל</string> + <string name="operators_conditions_will_also_apply"><![CDATA[ התנאי שימוש האלו יחולו גם על: <b>%s</b>.]]></string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>מכשירי שיואמי</b>: אנא תאפשר הפעלה אוטומטית בהגדרות הטלפון שלך כדי שההתראות על הודעות חדשות יפעלו.]]></string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s ההודעות לא הועברו.</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[כל ההודעות והקבצים נשלחים <b>מוצפנים מקצה לקצה</b>, עם אבטחה פוסט-קוונטית בהודעות ישירות.]]></string> + <string name="appearance_bars_blur_radius">טישטוש</string> + <string name="chat_list_businesses">עסקי</string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[כדי להשתמש בשרתים של <b>%s</b>, קבל את תנאי השימוש.]]></string> + <string name="report_archive_alert_desc_all">כל הדיווחים אצלך יועברו לארכיון.</string> + <string name="report_archive_alert_title_nth">להעביר לארכיון %d דיווחים?</string> + <string name="report_archive_alert_title_all">להעביר לארכיון את כל הדיווחים?</string> + <string name="archive_report">העבר דיווח לארכיון</string> + <string name="archive_reports">דיווחים בארכיון</string> + <string name="enable_sending_member_reports">אפשר לדווח על הודעות למנהלים.</string> + <string name="forward_files_missing_desc">%1$d הקובץ/ים נמחקו.</string> + <string name="operators_conditions_accepted_for"><![CDATA[הסכמתי לתנאי שימוש למפעיל/ים: <b>%s</b>.]]></string> + <string name="chat_database_exported_title">המסד נתונים יוצא בהצלחה</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[יש לך את <b>%1$s</b> כבר באנשי קשר.]]></string> + <string name="cant_send_message_to_member_alert_title">לא ניתן לשלוח הודעה לחבר קבוצה</string> + <string name="display_name_accepted_invitation">קבל הזמנה</string> + <string name="onboarding_network_about_operators">על המפעילים</string> + <string name="cant_call_contact_alert_title">לא ניתן להתקשר לאיש קשר</string> + <string name="v6_1_better_calls">שיפור לשיחות</string> + <string name="change_order_chat_list_menu_action">שנה את הסדר</string> + <string name="archive_verb">ארכיון</string> + <string name="onboarding_conditions_accept">אני מסכים</string> + <string name="v6_3_better_privacy_and_security">שיפור בפרטיות ובאבטחה</string> + <string name="v6_1_better_security">אבטחה יותר טובה✅</string> + <string name="v6_1_better_user_experience">ממשק משתמש יותר נוח</string> + <string name="proxy_destination_error_broker_version">"גרסאת שרת היעד %1$s אינה תואמת עם שרת ההעברה %2$s."</string> + <string name="v6_0_delete_many_messages_descr">מחק עד 20 הודעות בבת אחת.</string> + <string name="direct_messages_are_prohibited">הודעות ישירות בין חברים חסומות.</string> + <string name="disable_automatic_deletion">השבת מחיקת הודעות</string> + <string name="network_proxy_auth_mode_no_auth">אל תשתמש בתעודות עם פרוקסי.</string> + <string name="servers_info_downloaded">ירד</string> + <string name="group_reports_active">%d דיווחים</string> + <string name="app_check_for_updates_button_download">יורד %s (%s)</string> + <string name="disable_automatic_deletion_question">השבת מחיקת הודעות אוטומטית?</string> + <string name="duplicates_label">כפילויות</string> + <string name="operator_conditions_of_use">תנאי שימוש</string> + <string name="delete_without_notification">מחק בלי להתריע</string> + <string name="forward_files_not_accepted_receive_files">הורדה</string> + <string name="direct_messages_are_prohibited_in_chat">הודעות ישירות בין חברים אסורות בצ\'אט זה.</string> + <string name="v6_3_organize_chat_lists_descr">אל תחמיץ הודעות חשובות.</string> + <string name="v6_0_upgrade_app_descr">הורד גרסאות חדשות מ GitHub.</string> + <string name="report_reason_profile">פרופיל לא הולם</string> + <string name="v6_2_network_decentralization_enable_flux_reason">לפרטיות מטא דאטא טובה יותר.</string> + <string name="v6_2_network_decentralization_enable_flux">אפשר Flux בהגדרות רשת ושרתים בשביל לשפר את הפרטיות של המטא דאטא</string> + <string name="compose_forward_messages_n">מעביר %1$s הודעות</string> + <string name="error_reading_passphrase">שגיאה בקריאת משפט-סיסמה של מסד נתונים</string> + <string name="group_members_can_send_reports">משתמשים יכולים לדווח על הודעות לאחראי תוכן</string> + <string name="error_initializing_web_view_wrong_arch">שגיאת אתחול ב WebView, וודא שיש לך WebView מותקן והוא תותך בארכיטקטורה arm64\nשגיאה: %s</string> + <string name="network_proxy_incorrect_config_title">שגיאה בשמירת פרוקסי</string> + <string name="for_chat_profile">לפרופיל צ\'אט %s:</string> + <string name="operator_use_for_messages_private_routing">למסלול פרטי</string> + <string name="prefs_error_saving_settings">שגיאה בשמירת ההגדרות</string> + <string name="error_forwarding_messages">שגיאה בהעברת ההודעות</string> + <string name="switching_profile_error_title">שגיאה בהחלפת פרופיל</string> + <string name="debug_logs">אפשר לוגים</string> + <string name="how_it_helps_privacy">איך זה משפר את הפרטיות</string> + <string name="v6_2_improved_chat_navigation">שיפור בגלילה בצ\'אט</string> + <string name="report_reason_illegal">תוכן לא הולם</string> + <string name="error_accepting_operator_conditions">שגיאה בקבלת תנאי שימוש</string> + <string name="errors_in_servers_configuration">שגיאות בתצורת השרתים.</string> + <string name="proxy_destination_error_failed_to_connect">שרת ההעברות %1$s לא הצליח להתחבר לשרת היעד %2$s.נסה שוב במועד מאוחר יותר.</string> + <string name="group_reports_member_reports">דיווחים מחברי הקבוצה</string> + <string name="media_and_file_servers">שרתי קבצים ומדיה</string> + <string name="error_parsing_uri_title">קישור לא תקין</string> + <string name="error_creating_chat_tags">שגיאה ביצירת רשימת צא\'טים חדשה</string> + <string name="error_updating_chat_tags">שגיאת עדכון רשימת צ\'אטים</string> + <string name="error_loading_chat_tags">שגיאה בטעינת רשימות הצא\'טים</string> + <string name="edit_chat_list_menu_action">ערוך</string> + <string name="network_proxy_incorrect_config_desc">ודא שתצורת ה- proxy נכונה.</string> + <string name="action_button_add_members">הזמן</string> + <string name="v6_0_private_routing_descr">זה מגן על כתובת ה- IP והחיבורים שלך.</string> + <string name="button_leave_chat">עזוב צ\'אט</string> + <string name="sync_connection_confirm">תקן</string> + <string name="sync_connection_question">לתקן חיבור?</string> + <string name="v6_3_faster_deletion_of_groups">מחיקה מהירה יותר של קבוצות.</string> + <string name="v6_3_faster_sending_messages">שליחת הודעות מהירה יותר.</string> + <string name="forward_alert_forward_messages_without_files">העבר הודעות ללא קבצים?</string> + <string name="invite_to_chat_button">הזמן לצ\'אט</string> + <string name="snd_error_proxy_relay">שרת ההעברות: %1$s\nשגיאת שרת יעד: %2$s</string> + <string name="snd_error_proxy">שרת ההעברות: %1$s\nשגיאה: %2$s</string> + <string name="for_social_media">למדיה חברתית</string> + <string name="v6_0_increase_font_size">הגדל את גודל הגופן.</string> + <string name="list_menu">רשימה</string> + <string name="chat_list_favorites">מעודפים</string> + <string name="chat_list_groups">קבוצות</string> + <string name="list_name_field_placeholder">שם הרשימה..</string> + <string name="duplicated_list_error">שם הרשימה והאמוג\'י צריכים להיות שונים משאר הרשימות.</string> + <string name="keep_conversation">שמור שיחה</string> + <string name="invite_friends_short">הזמן</string> + <string name="v5_7_new_interface_languages">ממשק בשפה הליטאית</string> + <string name="file_error_blocked">הקובץ נחסם ע\"י מפעיל השרת:\n%1$s.</string> + <string name="report_archive_for_me">בשבילי</string> + <string name="report_archive_for_all_moderators">לכל האחראי תוכן</string> + <string name="forward_multiple">העבר הודעות…</string> + <string name="error_saving_database">שגיאה בשמירת המסד נתונים</string> + <string name="error_adding_server">שגיאה בהוספת שרת</string> + <string name="error_updating_server_title">שגיאת עדכון שם שרת</string> + <string name="forward_alert_title_messages_to_forward">להעביר %1$s הודעה/ות?</string> + <string name="error_creating_report">שגיאה בשליחת הדיווח</string> + <string name="failed_to_save_servers">שגיאה בשמירת שרתים</string> + <string name="leave_chat_question">לעזוב את הצ\'אט?</string> + <string name="v6_1_forward_many_messages_descr">העבר עד 20 הודעות בבת אחת.</string> + <string name="v6_3_mentions_descr">קבל התראה כאשר מתייגים אותך</string> + <string name="v6_3_reports_descr">עזור לאחראי תוכן בדיווחים על תוכן בעייתי בקבוצות</string> + <string name="chat_list_add_list">הוסף רשימה</string> + <string name="color_primary_variant2">הדגשה נוספת 2</string> + <string name="onboarding_conditions_by_using_you_agree">בשימוש שלך ב- SimpleX Chat אתה מסכים ל:\n\n- לשלוח רק תוכן חוקי בקבוצות ציבוריות.\n\n- לכבד את שאר המשתמשים - לא לשלוח ספאם.</string> + <string name="operator_conditions_will_be_applied"><![CDATA[תנאי שימוש אלה יחולו גם על:<b>%s</b>.]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[אותם תנאי שימוש יחולו על מפעיל/ים: <b>%s</b>.]]></string> + <string name="button_add_friends">הוסף חברים</string> + <string name="operator_added_message_servers">שרתי הודעות שנוספו</string> + <string name="operator_added_xftp_servers">שרתי מדיה וקבצים שנוספו</string> + <string name="add_to_list">הוסף לרשימה</string> + <string name="servers_info_subscriptions_connections_subscribed">חיבורים פעילים</string> + <string name="add_your_team_members_to_conversations">הוסף את חברי הצוות שלך לשיחות.</string> + <string name="onboarding_notifications_mode_service_desc_short">האפליקציה תמיד רצה ברקע</string> + <string name="address_settings">הגדרות כתובת</string> + <string name="appearance_app_toolbars">סרגל הכלים של האפליקציה</string> + <string name="network_session_mode_session">סשן לאפליקציה</string> + <string name="address_or_1_time_link">כתובת או קישור חד פעמי?</string> + <string name="button_add_team_members">הוסף חברי צוות</string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[אותם תנאי שימוש יחולו על המפעיל <b>%s</b>.]]></string> + <string name="info_row_chat">צ\'אט</string> + <string name="v6_2_business_chats">צא\'טים עסקיים</string> + <string name="chunks_deleted">"הזבל נמחק"</string> + <string name="chat_list_contacts">אנשי קשר</string> + <string name="delete_contact_cannot_undo_warning">איש הקשר יימחק - לא ניתן לבטל זאת!</string> + <string name="cant_call_contact_deleted_alert_text">איש הקשר נמחק.</string> + <string name="blocking_reason_spam">ספאם</string> +</resources> 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 bebf716e0d..21c04b8473 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -21,12 +21,12 @@ <string name="v4_3_improved_server_configuration_desc">QRコードでサーバを追加</string> <string name="smp_servers_add_to_another_device">別の端末に追加</string> <string name="users_add">プロフィールを追加</string> - <string name="smp_servers_add">サーバを追加…</string> + <string name="smp_servers_add">サーバを追加</string> <string name="network_enable_socks_info">SOCKSプロキシ(ポート%d)経由で接続しますか?(※設定する前にプロキシ起動が必要※)</string> <string name="users_delete_all_chats_deleted">全チャットとメッセージが削除されます(※元に戻せません※)!</string> <string name="allow_your_contacts_to_send_voice_messages">送信相手からの音声メッセージを許可する。</string> <string name="both_you_and_your_contact_can_send_voice">あなたと連絡相手が音声メッセージを送信できます。</string> - <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>電池省エネに良い</b>:バックグラウンド機能で10分毎に新着メッセージを確認します。通話と緊急メッセージを見逃す可能性があります。]]></string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>バッテリーに優しい</b>。アプリは10分ごとにメッセージを確認します。ただし、電話や緊急のメッセージを見逃す可能性があります。]]></string> <string name="icon_descr_audio_off">音声オフ</string> <string name="attach">添付する</string> <string name="app_version_code">アプリ・ビルド番号: %s</string> @@ -41,15 +41,14 @@ <string name="app_version_title">アプリのバージョン</string> <string name="app_version_name">アプリのバージョン: v%s</string> <string name="network_session_mode_user_description"><![CDATA[<b>アプリ内の各チャットプロフィールに、</b>.連絡先毎にそれぞれのTCP接続(とSOCKS資格情報)が使われます。]]></string> - <string name="network_session_mode_entity_description"><b>各連絡先とグループに、</b>それぞれのTCP接続(とSOCKS資格情報)が使われます。 -\n<b>※注意※</b> 接続が多かったら、電池とデータの使用量が増えて、切断する可能性もあります。</string> + <string name="network_session_mode_entity_description"><![CDATA[<b>各連絡先とグループに、</b>それぞれのTCP接続(とSOCKS資格情報)が使われます。 \n<b>※注意※</b> 接続が多かったら、電池とデータの使用量が増えて、切断する可能性もあります。]]></string> <string name="bold_text">太文字</string> <string name="icon_descr_audio_call">音声通話</string> <string name="settings_audio_video_calls">音声とビデオ通話</string> - <string name="impossible_to_recover_passphrase"><![CDATA[<b>※注意※</b>:喪失したら、パスフレーズの回復・変更ができません。]]></string> + <string name="impossible_to_recover_passphrase"><![CDATA[<b>※注意※</b>:紛失するとパスフレーズの回復・変更ができません。]]></string> <string name="all_group_members_will_remain_connected">グループ全員の接続が継続します。</string> <string name="allow_your_contacts_to_send_disappearing_messages">送信相手が消えるメッセージを送るのを許可する。</string> - <string name="allow_your_contacts_irreversibly_delete">送信相手が永久メッセージ削除するのを許可する。</string> + <string name="allow_your_contacts_irreversibly_delete">送信相手が永久メッセージ削除するのを許可する。(24時間)</string> <string name="allow_voice_messages_only_if">送信相手も音声メッセージを許可する時のみに許可する。</string> <string name="both_you_and_your_contact_can_send_disappearing">あなたと連絡相手が消えるメッセージを送信できます。</string> <string name="v4_2_auto_accept_contact_requests">連絡先を自動的に承諾</string> @@ -57,8 +56,8 @@ <string name="notifications_mode_service">常にオン</string> <string name="clear_chat_warning">全てのメッセージが削除されます(※注意:元に戻せません!※)。削除されるのは片方あなたのメッセージのみ</string> <string name="allow_disappearing_messages_only_if">送信相手も消えるメッセージ機能を許可する時のみに許可する。</string> - <string name="allow_irreversible_message_deletion_only_if">送信相手も永久メッセージ削除を許可する時のみに許可する。</string> - <string name="allow_to_delete_messages">送信済みメッセージの永久削除を許可</string> + <string name="allow_irreversible_message_deletion_only_if">送信相手も永久メッセージ削除を許可する時のみに許可する。(24時間)</string> + <string name="allow_to_delete_messages">送信済みメッセージの永久削除を許可(24時間)</string> <string name="allow_to_send_disappearing">消えるメッセージの送信を許可</string> <string name="allow_direct_messages">メンバーへのダイレクトメッセージを許可</string> <string name="allow_to_send_voice">音声メッセージの送信を許可</string> @@ -73,14 +72,10 @@ <string name="auth_unavailable">認証不可能</string> <string name="auto_accept_images">画像を自動的に受信</string> <string name="notifications_mode_service_desc">バックグラウンド機能が常にオンで、メッセージが到着次第に通知が出ます。</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>新しい連絡先を追加</b>:使い捨てのQRコードを発行]]></string> <string name="turning_off_service_and_periodic">電池省エネをオンに、バックグラウンド機能と定期的な受信依頼をオフにします。設定メニューにて変更できます。</string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>電池消費が最少</b>:アプリがアクティブ時のみに通知が出ます(バックグラウンドサービス無し)。]]></string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>設定メニューにてオフにできます。</b> アプリがアクティブ時に通知が出ます。]]></string> - <string name="both_you_and_your_contacts_can_delete">あなたと連絡相手が送信済みメッセージを永久削除できます。</string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>QRコードを読み込み</b>:連絡相手のQRコードをスキャンすると繋がります。]]></string> - <string name="chat_archive_section">チャットのアーカイブ</string> - <string name="delete_chat_archive_question">チャットのアーカイブを削除しますか?</string> + <string name="both_you_and_your_contacts_can_delete">あなたと連絡相手が送信済みメッセージを永久削除できます。(24時間)</string> <string name="join_group_incognito_button">シークレットモードで参加</string> <string name="group_member_status_intro_invitation">接続待ち (招待)</string> <string name="group_member_status_accepted">接続待ち (承諾済み)</string> @@ -116,9 +111,9 @@ <string name="failed_to_parse_chat_title">チャット読み込みに失敗</string> <string name="failed_to_parse_chats_title">チャット読み込みに失敗</string> <string name="contact_developers">アプリを更新し、開発者にご連絡ください。</string> - <string name="service_notifications">即時通知!</string> - <string name="service_notifications_disabled">即時通知が無効になってます!</string> - <string name="enter_passphrase_notification_title">暗証フレーズが必要</string> + <string name="service_notifications">通知の常時受信!</string> + <string name="service_notifications_disabled">通知の常時受信が無効になってます!</string> + <string name="enter_passphrase_notification_title">パスフレーズが必要</string> <string name="notification_preview_mode_hidden">プライベート</string> <string name="notification_preview_mode_contact">連絡先の名前</string> <string name="notification_preview_somebody">連絡先が非表示:</string> @@ -131,7 +126,7 @@ <string name="how_to">使い方</string> <string name="network_use_onion_hosts_required">必須</string> <string name="incoming_video_call">ビデオ通話着信</string> - <string name="paste_the_link_you_received">頂いたリンクを貼り付ける</string> + <string name="paste_the_link_you_received">受け取ったリンクを貼り付ける</string> <string name="icon_descr_hang_up">通話を切る</string> <string name="messages_section_title">メッセージ</string> <string name="rcv_group_event_invited_via_your_group_link">グループリンク経由で招待された</string> @@ -141,9 +136,9 @@ <string name="error_changing_address">アドレス変更にエラー発生</string> <string name="error_creating_link_for_group">グループリンク発行にエラー発生</string> <string name="error_changing_role">役割変更にエラー発生</string> - <string name="error_exporting_chat_database">チャットデータベースのエキスポートにエラー発生</string> - <string name="error_saving_smp_servers">SMPサーバ保存にエラー発生</string> - <string name="error_saving_ICE_servers">ICEサーバ保存にエラー発生</string> + <string name="error_exporting_chat_database">チャットデータベースの書き出しでエラー発生</string> + <string name="error_saving_smp_servers">SMPサーバ保存でエラー発生</string> + <string name="error_saving_ICE_servers">ICEサーバ保存でエラー発生</string> <string name="error_setting_network_config">ネットワーク設定の更新にエラー発生</string> <string name="icon_descr_file">ファイル</string> <string name="simplex_link_mode_full">フルリンク</string> @@ -151,17 +146,16 @@ <string name="group_invitation_expired">グループ招待が期限切れ</string> <string name="icon_descr_group_inactive">非アクティブなグループ</string> <string name="group_members_can_send_voice">グループのメンバーが音声メッセージを送信できます。</string> - <string name="group_members_can_delete">グループのメンバーがメッセージを完全削除することができます。</string> + <string name="group_members_can_delete">送信したメッセージをグループのメンバーが完全削除することができます。(24時間)</string> <string name="group_profile_is_stored_on_members_devices">グループのプロフィールはサーバではなく、メンバーの端末に保存されます。</string> <string name="snd_group_event_group_profile_updated">グループのプロフィールが更新されました。</string> <string name="notification_display_mode_hidden_desc">連絡先とメッセージ内容をプライベートにする。</string> - <string name="icon_descr_instant_notifications">即時通知</string> + <string name="icon_descr_instant_notifications">通知の常時受信</string> <string name="ensure_smp_server_address_are_correct_format_and_unique">SMPサーバのアドレスを正しく1行ずつに分けて、重複しないように、形式もご確認ください。</string> <string name="ensure_ICE_server_address_are_correct_format_and_unique">WebRTC ICEサーバのアドレスを正しく1行ずつに分けて、重複しないように、形式もご確認ください。</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[よく聞かれるのは、 <i>if SimpleX にユーザIDがなければ、メッセージをどうやって届けるのでしょうかと。</i>]]></string> <string name="how_simplex_works">SimpleX の仕様</string> <string name="icon_descr_call_progress">通話中</string> - <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>電池消費がより高い</b>!非アクティブ時でもバックグラウンドのサービスが常に稼働します(着信次第に通知がすぐに出ます)。]]></string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>電池消費がより高い</b>!非アクティブ時でもバックグラウンドのサービスが常に稼働します(着信してすぐに通知が出ます)。]]></string> <string name="callstatus_calling">発信中</string> <string name="callstatus_ended">通話終了 %1$s</string> <string name="icon_descr_call_ended">通話が終了しました。</string> @@ -181,9 +175,9 @@ <string name="chat_is_stopped_indication">チャットが停止してます。</string> <string name="network_disable_socks_info">承諾すると、メッセージのサーバがIPアドレス、ISP、接続サーバを特定できます。</string> <string name="chat_preferences">チャット設定</string> - <string name="clear_chat_button">消す</string> + <string name="clear_chat_button">チャットを削除</string> <string name="icon_descr_close_button">閉じるボタン</string> - <string name="clear_verification">検証を消す</string> + <string name="clear_verification">未検証の状態に戻す</string> <string name="image_saved">画像をギャラリーに保存しました。</string> <string name="choose_file">ファイル</string> <string name="clear_chat_menu_action">消す</string> @@ -193,9 +187,9 @@ <string name="v4_3_improved_server_configuration">サーバ設定の向上</string> <string name="v4_3_improved_privacy_and_security">プライバシーとセキュリティ強化</string> <string name="settings_section_title_incognito">シークレットモード</string> - <string name="new_in_version">%s バージョンアップで新しい</string> - <string name="new_passphrase">新しい暗証フレーズ</string> - <string name="chat_item_ttl_none">一度も</string> + <string name="new_in_version">%s の新機能</string> + <string name="new_passphrase">新しいパスフレーズ</string> + <string name="chat_item_ttl_none">なし</string> <string name="notification_preview_new_message">新しいメッセージ</string> <string name="network_use_onion_hosts_no">いいえ</string> <string name="notifications">通知</string> @@ -206,15 +200,14 @@ <string name="display_name_connection_established">接続済み</string> <string name="connect_via_link">リンク経由で繋がる。</string> <string name="connection_error">接続エラー</string> - <string name="network_use_onion_hosts_required_desc_in_alert">接続にオニオンのホストが必要となります。</string> <string name="group_member_status_introduced">接続待ち (紹介済み)</string> <string name="connection_error_auth">接続エラー (AUTH)</string> <string name="connection_timeout">接続タイムアウト</string> <string name="connection_request_sent">接続リクエストを送信しました!</string> <string name="connection_local_display_name">接続 %1$d</string> - <string name="connect_via_contact_link">連絡先リンク経由で繋がりますか?</string> - <string name="connect_via_group_link">グループリンク経由で繋がりますか?</string> - <string name="connect_via_invitation_link">招待リンク経由で繋がりますか?</string> + <string name="connect_via_contact_link">連絡先アドレスと接続?</string> + <string name="connect_via_group_link">グループに参加?</string> + <string name="connect_via_invitation_link">ワンタイムリンクで接続?</string> <string name="status_contact_has_e2e_encryption">連絡先はエンドツーエンド暗号化があります。</string> <string name="description_via_group_link_incognito">グループリンク経由でシークレットモード</string> <string name="delete_contact_all_messages_deleted_cannot_undo_warning">連絡先と全メッセージが削除されます (※元に戻せません※)!</string> @@ -224,21 +217,20 @@ <string name="icon_descr_context">追加情報アイコン</string> <string name="network_use_onion_hosts_prefer_desc">オニオンのホストが利用可能時に使われます。</string> <string name="network_use_onion_hosts_no_desc">オニオンのホストが使われません。</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">オニオンのホストが利用可能時に使われます。</string> <string name="images_limit_desc">画像を1回で最大10枚を送信できます。</string> <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[<b>2層エンドツーエンド暗号化</b>で送信されたプロフィール、連絡先、グループ、メッセージは、クライント端末にしか保存されません。]]></string> <string name="only_group_owners_can_change_prefs">グループ設定を変えられるのはグループのオーナーだけです。</string> <string name="only_group_owners_can_enable_voice">音声メッセージを利用可能に設定できるのはグループのオーナーだけです。</string> <string name="create_profile">プロフィールを作成する</string> - <string name="only_you_can_delete_messages">メッセージの完全削除はあなたにしかできません (あなたの連絡先は削除対象とすることができます)。</string> + <string name="only_you_can_delete_messages">メッセージの完全削除はあなたにしかできません (あなたの連絡先は削除対象とすることができます)。(24時間)</string> <string name="create_your_profile">プロフィールを作成する</string> - <string name="only_your_contact_can_delete">メッセージを完全削除できるのはあなたの連絡相手だけです (あなたは削除対象とすることができます)。</string> + <string name="only_your_contact_can_delete">メッセージを完全削除できるのはあなたの連絡相手だけです (あなたは削除対象とすることができます)。(24時間)</string> <string name="open_chat">チャットを開く</string> <string name="only_your_contact_can_send_voice">音声メッセージを送れるのはあなたの連絡相手だけです。</string> <string name="database_encryption_will_be_updated">データベース暗号化のパスフレーズが更新され、キーストア (暗証キー保管庫)に保存されます。</string> <string name="database_error">データベースエラー</string> - <string name="set_password_to_export_desc">データベースはランダムな暗証フレーズで暗号化済みです。エキスポートする前に更新してください。</string> - <string name="passphrase_is_different">データベース暗証フレーズはキーストア (暗証キー保管庫) に保存された暗証フレーズと異なります。</string> + <string name="set_password_to_export_desc">データベースはランダムなパスフレーズで暗号化済みです。エクスポートする前に更新してください。</string> + <string name="passphrase_is_different">データベースパスフレーズはキーストア (暗証キー保管庫) に保存されたパスフレーズと異なります。</string> <string name="ttl_d">%d日</string> <string name="ttl_day">%d 日</string> <string name="delete_group_menu_action">削除</string> @@ -258,11 +250,11 @@ <string name="alert_title_group_invitation_expired">招待が期限切れました!</string> <string name="smp_servers_delete_server">サーバを削除</string> <string name="auth_device_authentication_is_disabled_turning_off">端末認証がオフです。SimpleXロックを解除します。</string> - <string name="direct_messages_are_prohibited_in_chat">このグループではメンバー間のダイレクトメッセージが使用禁止です。</string> - <string name="disappearing_messages_are_prohibited">このグループでは消えるメッセージが使用禁止です。</string> + <string name="direct_messages_are_prohibited_in_group">このグループではメンバー間のダイレクトメッセージが無効です。</string> + <string name="disappearing_messages_are_prohibited">このグループでは消えるメッセージが無効です。</string> <string name="ttl_min">%d 分</string> <string name="ttl_weeks">%d 週</string> - <string name="network_option_enable_tcp_keep_alive">TCP keep-aliveを有効にする</string> + <string name="network_option_enable_tcp_keep_alive">TCPキープアライブを有効にする</string> <string name="icon_descr_email">メール</string> <string name="allow_accepting_calls_from_lock_screen">設定メニューでロック画面からの通話を有効にできます。</string> <string name="people_can_connect_only_via_links_you_share">あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。</string> @@ -270,18 +262,18 @@ <string name="image_descr_qr_code">QRコード</string> <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">リンクが正しいかどうかご確認ください。または、連絡相手にもう一度リンクをお求めください。</string> <string name="network_error_desc">%1$s との接続を確認し、もう一度お試しください。</string> - <string name="enter_correct_current_passphrase">現在の正しい暗証フレーズを入力してください。</string> + <string name="enter_correct_current_passphrase">現在の正しいパスフレーズを入力してください。</string> <string name="smp_servers_preset_server">プレセットサーバ</string> <string name="smp_servers_preset_address">プレセットサーバのアドレス</string> <string name="display_name_connecting">接続待ち…</string> <string name="description_via_contact_address_link_incognito">連絡先リンク経由でシークレットモード</string> <string name="description_via_one_time_link_incognito">使い捨てリンク経由でシークレットモード</string> - <string name="moderated_description">モデレーターによって介入済み</string> + <string name="moderated_description">検閲済</string> <string name="simplex_link_mode_browser_warning">ブラウザでリンクを開くと接続のプライバシーとセキュリティが下がる可能性があります。信頼されないSimpleXリンクは読み込まれません。</string> - <string name="error_joining_group">グループ参加にエラー発生</string> + <string name="error_joining_group">グループ参加でエラー発生</string> <string name="error_sending_message">メッセージ送信にエラー発生</string> <string name="error_accepting_contact_request">連絡先リクエストの承諾にエラー発生</string> - <string name="error_receiving_file">ファイル受信にエラー発生</string> + <string name="error_receiving_file">ファイル受信でエラー発生</string> <string name="error_deleting_contact">連絡先の削除にエラー発生</string> <string name="error_deleting_group">グループ削除にエラー発生</string> <string name="error_deleting_pending_contact_connection">接続待ちの連絡先削除にエラー発生</string> @@ -295,12 +287,12 @@ <string name="auth_log_in_using_credential">ログイン情報でアクセス</string> <string name="auth_confirm_credential">アクセス情報を確認</string> <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">端末認証が起動してません。起動したら、設定でSimpleXロックをオンにできます。</string> - <string name="message_delivery_error_desc">恐らくこの連絡先があなたとの接続を削除されました。</string> + <string name="message_delivery_error_desc">この連絡先はあなたとの接続を削除した可能性があります。</string> <string name="for_everybody">全員用</string> - <string name="error_saving_file">ファイル保存にエラー発生</string> + <string name="error_saving_file">ファイル保存でエラー発生</string> <string name="icon_descr_server_status_connected">接続中</string> <string name="icon_descr_server_status_pending">確認待ち</string> - <string name="cancel_verb">中止</string> + <string name="cancel_verb">キャンセル</string> <string name="add_contact">使い捨ての招待リンク</string> <string name="icon_descr_record_voice_message">音声メッセージを録音</string> <string name="from_gallery_button">ギャラリーから</string> @@ -316,8 +308,7 @@ <string name="how_to_use_your_servers">自分のサーバの使い方</string> <string name="enter_one_ICE_server_per_line">ICEサーバ (1行に1サーバ)</string> <string name="network_and_servers">ネットワークとサーバ</string> - <string name="network_settings_title">ネットワーク設定</string> - <string name="network_use_onion_hosts_no_desc_in_alert">オニオンのホストが使われません。</string> + <string name="network_settings_title">高度な設定</string> <string name="delete_address">アドレスを削除</string> <string name="exit_without_saving">保存せずに閉じる</string> <string name="display_name_cannot_contain_whitespace">表示の名前には空白が使用できません。</string> @@ -329,13 +320,12 @@ <string name="callstate_connected">接続中</string> <string name="callstatus_connecting">発信中…</string> <string name="callstate_ended">終了</string> - <string name="opensource_protocol_and_code_anybody_can_run_servers">プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。</string> - <string name="privacy_redefined">プライバシーの基準を新境地に</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">誰でもサーバーをホストできます。</string> + <string name="privacy_redefined">プライバシーを再定義</string> <string name="how_it_works">技術の説明</string> <string name="make_private_connection">プライベートな接続をする</string> <string name="onboarding_notifications_mode_title">プライベートな通知</string> <string name="read_more_in_github_with_link"><![CDATA[<font color="#0088ff">GitHubリポジトリ</font>で詳細をご確認ください。]]></string> - <string name="read_more_in_github">GitHubリポジトリで詳細をご確認ください。</string> <string name="encrypted_video_call">エンドツーエンド暗号化済みビデオ通話</string> <string name="no_call_on_lock_screen">無効にする</string> <string name="status_no_e2e_encryption">エンドツーエンド暗号化がありません</string> @@ -354,8 +344,8 @@ <string name="settings_section_title_calls">通話</string> <string name="chat_is_running">チャットは稼働中</string> <string name="chat_is_stopped">チャットが停止してます。</string> - <string name="database_passphrase">データベース暗証フレーズ</string> - <string name="export_database">データベースをエキスポート</string> + <string name="database_passphrase">データベースパスフレーズ</string> + <string name="export_database">データベースをエクスポート</string> <string name="delete_database">データベースを削除</string> <string name="import_database">データベースを読み込みますか?</string> <string name="new_database_archive">新しいデータベースのアーカイブ</string> @@ -365,17 +355,15 @@ <string name="total_files_count_and_size">%d ファイル|合計: %s</string> <string name="encrypt_database">暗号化する</string> <string name="remove_passphrase">削除</string> - <string name="current_passphrase">現在の暗証フレーズ</string> + <string name="current_passphrase">現在のパスフレーズ</string> <string name="encrypted_with_random_passphrase">データベースはランダムなパスフレーズで暗号化済みで、変更可能です。</string> - <string name="change_database_passphrase_question">データベースの暗証フレーズを更新しますか?</string> + <string name="change_database_passphrase_question">データベースのパスフレーズを更新しますか?</string> <string name="database_will_be_encrypted">データベースが暗号化されます。</string> <string name="database_will_be_encrypted_and_passphrase_stored">データベースが暗号化され、パスフレーズがキーストア (暗証キー保管庫) にほぞんされます。</string> <string name="cannot_access_keychain">キーストア (暗証キーの保管庫) にアクセスできないため、データベースのパスワードが保存できません。</string> <string name="database_passphrase_will_be_updated">データベース暗号化のパスフレーズが更新されます。</string> - <string name="database_passphrase_is_required">チャットを開くにはデータベース暗証フレーズが必要です。</string> + <string name="database_passphrase_is_required">チャットを開くにはデータベースパスフレーズが必要です。</string> <string name="file_with_path">ファイル: %s</string> - <string name="archive_created_on_ts">作成日時 %1$s</string> - <string name="delete_archive">アーカイブを削除</string> <string name="join_group_button">参加</string> <string name="join_group_question">グループに参加しますか?</string> <string name="joining_group">グループに参加</string> @@ -391,7 +379,7 @@ <string name="group_member_status_connected">接続中</string> <string name="group_member_status_announced">接続待ち (アナウンス済み)</string> <string name="icon_descr_contact_checked">連絡先が確認済み</string> - <string name="delete_group_question">グループを削除しますか?</string> + <string name="delete_group_question">グループ削除?</string> <string name="delete_link">リンクを削除</string> <string name="group_member_status_group_deleted">グループ削除済み</string> <string name="delete_group_for_all_members_cannot_undo_warning">全員にとってグループが削除されます (※元に戻せません※)!</string> @@ -406,7 +394,7 @@ <string name="conn_level_desc_direct">ダイレクト</string> <string name="receiving_via">経由で受信</string> <string name="create_secret_group_title">シークレットグループを作成する</string> - <string name="group_display_name_field">グループの表示名前</string> + <string name="group_display_name_field">グループ名の入力:</string> <string name="network_status">ネットワーク状況</string> <string name="network_option_ping_interval">PING間合い</string> <string name="network_option_protocol_timeout">プロトコル・タイムアウト</string> @@ -450,23 +438,23 @@ <string name="core_version">コアのバージョン: v%s</string> <string name="edit_image">画像を編集</string> <string name="callstatus_missed">不在着信</string> - <string name="always_use_relay">リレー経由で繋がる。</string> + <string name="always_use_relay">常にリレーを使用</string> <string name="status_e2e_encrypted">エンドツーエンド暗号化済み</string> <string name="chat_database_deleted">チャットのデータベースが削除されました。</string> <string name="delete_messages_after">次の期間が経ったら、メッセージを削除:</string> <string name="enable_automatic_deletion_question">自動メッセージ削除を有効にしますか?</string> <string name="remove_passphrase_from_keychain">キーストア (暗証キー保管庫) を削除しますか?</string> - <string name="database_encrypted">データベースは暗号化済み!</string> - <string name="error_encrypting_database">データベース暗号化ににエラー発生</string> + <string name="database_encrypted">データベースは暗号化済!</string> + <string name="error_encrypting_database">データベース暗号化でエラー発生</string> <string name="confirm_new_passphrase">新しいパスフレーズを確認…</string> <string name="encrypt_database_question">データベースを暗号化しますか?</string> - <string name="store_passphrase_securely_without_recover">暗証フレーズを失くさないように保管してください。失くすとチャットにアクセスできなくなります。</string> - <string name="enter_correct_passphrase">正しい暗証フレーズを入力</string> - <string name="enter_passphrase">暗証フレーズを入力</string> - <string name="restore_passphrase_not_found_desc">キーストア (暗証キー保管庫) に暗証フレーズが見つかりません。入力してください。アプリのデータをバックアップのツールで復元させると暗証フレーズが見つかりかねます。そうではない場合は開発者にご連絡ください。</string> + <string name="store_passphrase_securely_without_recover">パスフレーズを失くさないように保管してください。失くすとチャットにアクセスできなくなります。</string> + <string name="enter_correct_passphrase">正しいパスフレーズを入力</string> + <string name="enter_passphrase">パスフレーズを入力</string> + <string name="restore_passphrase_not_found_desc">キーストア (パスキー保管庫) にパスフレーズが見つかりません。手動で入力してください。これは、バックアップツールを使用してアプリのデータを復元した場合に発生する可能性があります。そうではない場合は開発者にご連絡ください。</string> <string name="button_create_group_link">リンクを発行する</string> <string name="info_row_group">グループ</string> - <string name="error_saving_group_profile">グループのプロフィール保存にエラー発生</string> + <string name="error_saving_group_profile">グループのプロフィール保存でエラー発生</string> <string name="feature_enabled">有効</string> <string name="group_members_can_send_disappearing">グループのメンバーが消えるメッセージを送信できます。</string> <string name="ttl_mth">%d月</string> @@ -482,7 +470,7 @@ <string name="failed_to_active_user_title">プロフィール切り替えにエラー発生</string> <string name="error_adding_members">メンバー追加にエラー発生</string> <string name="smp_server_test_create_queue">サーバの待ち行列を作成する</string> - <string name="smp_server_test_delete_queue">待ち行列を削除</string> + <string name="smp_server_test_delete_queue">未配信のメッセージを削除</string> <string name="smp_server_test_disconnect">切断</string> <string name="turn_off_battery_optimization"><![CDATA[利用するには次の画面にてSimpleXに対する <b>SimpleX のバックグラウンドでの実行を許可</b> してください。そうしないと通知が無効になります。]]></string> <string name="database_initialization_error_title">データベースを起動できません。</string> @@ -492,7 +480,7 @@ <string name="notification_new_contact_request">新しい繋がりのリクエスト</string> <string name="copy_verb">コピー</string> <string name="delete_message__question">メッセージを削除しますか?</string> - <string name="edit_verb">編集する</string> + <string name="edit_verb">編集</string> <string name="hide_verb">プライベートにする</string> <string name="delete_message_cannot_be_undone_warning">メッセージが削除されます (※元に戻せません※)!</string> <string name="delete_message_mark_deleted_warning">メッセージが削除対象となります。宛先にはメッセージの解読ができます。</string> @@ -524,26 +512,25 @@ <string name="mute_chat">ミュート</string> <string name="delete_pending_connection__question">接続待ちの繋がりを削除しますか?</string> <string name="connect_button">接続</string> - <string name="create_one_time_link">使い捨てリンクを発行する</string> <string name="one_time_link">使い捨ての招待リンク</string> - <string name="database_passphrase_and_export">データベース暗証フレーズとエキスポート</string> + <string name="database_passphrase_and_export">データベースパスフレーズとエクスポート</string> <string name="how_to_use_simplex_chat">使い方</string> <string name="markdown_help">マークダウン (書式編集) ガイド</string> <string name="smp_servers_enter_manually">サーバを手動で入力</string> <string name="network_use_onion_hosts_required_desc">接続にオニオンのホストが必要となります。 \n注意: .onion アドレスがないとサーバーに接続できません。</string> <string name="create_address">アドレスを作成</string> - <string name="delete_address__question">アドレスを削除しますか?</string> - <string name="display_name__field">表示の名前:</string> + <string name="delete_address__question">アドレスを削除?</string> + <string name="display_name__field">プロフィール名:</string> <string name="full_name__field">フルネーム:</string> <string name="create_profile_button">作成</string> - <string name="display_name">表示の名前</string> + <string name="display_name">あなたの名前を入力:</string> <string name="colored_text">色付き</string> <string name="callstate_received_answer">応答</string> <string name="decentralized">分散型</string> - <string name="immune_to_spam_and_abuse">スパムや悪質送信を完全防止</string> - <string name="onboarding_notifications_mode_service">即時</string> - <string name="onboarding_notifications_mode_periodic">定期的</string> + <string name="immune_to_spam_and_abuse">スパム耐性</string> + <string name="onboarding_notifications_mode_service">常時受信</string> + <string name="onboarding_notifications_mode_periodic">定期的に受信</string> <string name="call_already_ended">通話は既に終了してます!</string> <string name="encrypted_audio_call">エンドツーエンド暗号化済みの音声通話</string> <string name="ignore">無視</string> @@ -557,7 +544,6 @@ <string name="settings_section_title_device">端末</string> <string name="no_received_app_files">送受信済みのファイルがありません</string> <string name="delete_messages">メッセージを削除</string> - <string name="chat_archive_header">チャットのアーカイブ</string> <string name="rcv_group_event_member_connected">接続中</string> <string name="rcv_group_event_user_deleted">あなたを除名しました。</string> <string name="group_link">グループのリンク</string> @@ -569,14 +555,14 @@ <string name="theme_light">ライトテーマ</string> <string name="chat_preferences_default">デフォルト (%s)</string> <string name="prohibit_sending_disappearing_messages">消えるメッセージを使用禁止にする。</string> - <string name="contacts_can_mark_messages_for_deletion">連絡先はメッセージを削除対象とすることができます。あなたには閲覧可能です。</string> + <string name="contacts_can_mark_messages_for_deletion">連絡先はメッセージを削除対象としてマークを付けられます。あなたには閲覧可能です。</string> <string name="only_you_can_send_disappearing">消えるメッセージを送れるのはあなただけです。</string> <string name="prohibit_message_deletion">メッセージの完全削除を使用禁止にする。</string> <string name="prohibit_sending_voice">音声メッセージを使用禁止にする。</string> <string name="feature_cancelled_item">中止されました %s</string> <string name="v4_4_disappearing_messages">消えるメッセージ</string> <string name="v4_5_transport_isolation_descr">チャットのプロフィール (既存設定)、または接続 (β機能設定) による</string> - <string name="v4_5_multiple_chat_profiles_descr">異なった名前、アバター、トランスポート隔離。</string> + <string name="v4_5_multiple_chat_profiles_descr">別名、アバター、分離送信。</string> <string name="v4_4_french_interface">フランス語UI</string> <string name="v4_5_message_draft">メッセージの下書き</string> <string name="v4_5_reduced_battery_usage_descr">まだまだ改善してまいります!</string> @@ -584,15 +570,15 @@ <string name="v4_5_message_draft_descr">添付を含めて、下書きを保存する。</string> <string name="v4_5_private_filenames">プライベートなファイル名</string> <string name="v4_5_reduced_battery_usage">電池使用量低減</string> - <string name="error_removing_member">メンバー除名にエラー発生</string> - <string name="conn_level_desc_indirect">関節 (%1$s)</string> + <string name="error_removing_member">メンバー削除でエラー発生</string> + <string name="conn_level_desc_indirect">間接 (%1$s)</string> <string name="incognito">シークレットモード</string> <string name="incognito_info_protects">シークレット モードでは、連絡先ごとに新しいランダムなプロファイルを使用してプライバシーを保護します。</string> - <string name="chat_preferences_no">いいえ</string> + <string name="chat_preferences_no">オフ</string> <string name="chat_preferences_on">オン</string> <string name="direct_messages">ダイレクトメッセージ</string> <string name="timed_messages">消えるメッセージ</string> - <string name="disappearing_prohibited_in_this_chat">このチャットでは消えるメッセージが使用禁止です。</string> + <string name="disappearing_prohibited_in_this_chat">このチャットでは消えるメッセージが無効です。</string> <string name="prohibit_direct_messages">メンバー間のダイレクトメッセージを使用禁止にする。</string> <string name="prohibit_sending_disappearing">消えるメッセージを使用禁止にする。</string> <string name="ttl_s">%d秒</string> @@ -605,7 +591,7 @@ <string name="import_database_question">チャットのデータベースを読み込みますか?</string> <string name="chat_database_imported">チャットのデータベースが読み込まれました。</string> <string name="error_deleting_database">チャットデータベース削除にエラー発生</string> - <string name="error_importing_database">チャットデータベースの読み込みにエラー発生</string> + <string name="error_importing_database">チャットデータベースの読み込みでエラー発生</string> <string name="import_database_confirmation">読み込む</string> <string name="restart_the_app_to_use_imported_chat_database">読み込んだデータベースを利用するにはアプリを再起動する必要があります。</string> <string name="delete_chat_profile_question">チャットのプロフィールを削除しますか?</string> @@ -613,17 +599,17 @@ <string name="files_and_media_section">ファイルとメディア</string> <string name="error_changing_message_deletion">設定変更にエラー発生</string> <string name="notifications_will_be_hidden">アプリが起動中のみに通知が出ます!</string> - <string name="encrypted_database">暗号化済みデータベース</string> + <string name="encrypted_database">暗号化データベース</string> <string name="keychain_error">キーチェーンのエラー</string> - <string name="store_passphrase_securely">暗証フレーズを失くさないように保管してください。失くすと変更できなくなります。</string> - <string name="error_with_info">エラー: %s</string> - <string name="restore_database_alert_desc">データベースのバックアップで復旧する際に前の暗証フレーズを入力してください。行われたら元に戻せません。</string> + <string name="store_passphrase_securely">パスフレーズを失くさないように保管してください。失くすと変更できなくなります。</string> + <string name="error_with_info">エラー: %s</string> + <string name="restore_database_alert_desc">データベースのバックアップで復旧する際に前のパスフレーズを入力してください。行われたら元に戻せません。</string> <string name="icon_descr_add_members">メンバーを招待する</string> <string name="rcv_group_event_member_left">脱退しました。</string> <string name="rcv_group_event_changed_member_role">%s の役割を %s に変えました。</string> <string name="rcv_group_event_changed_your_role">役割を %s に変えました。</string> <string name="rcv_group_event_member_added">招待された %1$s</string> - <string name="snd_conn_event_switch_queue_phase_changing_for_member">アドレスを変更いたします: %s</string> + <string name="snd_conn_event_switch_queue_phase_changing_for_member">アドレスを変更しています: %s</string> <string name="group_member_role_owner">オーナー</string> <string name="group_member_status_connecting">接続待ち</string> <string name="icon_descr_expand_role">役割の選択を拡大</string> @@ -647,7 +633,7 @@ <string name="theme">テーマ</string> <string name="voice_messages">音声メッセージ</string> <string name="app_name">SimpleX</string> - <string name="you_will_join_group">このリンクのグループに参加し、そのメンバーに繋がります。</string> + <string name="you_will_join_group">グループメンバー全員と接続する。</string> <string name="connected_to_server_to_receive_messages_from_contact">この連絡先から受信するメッセージのサーバに既に接続してます。</string> <string name="sending_files_not_yet_supported">ファイル送信機能がまだ実装されてません</string> <string name="trying_to_connect_to_server_to_receive_messages">このコンタクトから受信するメッセージのサーバに接続しようとしてます。</string> @@ -676,7 +662,6 @@ <string name="icon_descr_speaker_off">スピーカーオフ</string> <string name="your_chat_database">あなたのチャットデータベース</string> <string name="stop_chat_confirmation">停止</string> - <string name="stop_chat_to_enable_database_actions">データベース操作をするにはチャットを閉じてからです。</string> <string name="simplex_link_contact">SimpleX連絡先アドレス</string> <string name="simplex_link_invitation">SimpleX使い捨て招待リンク</string> <string name="description_via_contact_address_link">連絡先アドレスリンク経由</string> @@ -688,7 +673,7 @@ <string name="sender_may_have_deleted_the_connection_request">送信元が繋がりリクエストを削除したかもしれません。</string> <string name="error_smp_test_server_auth">このサーバで待ち行列を作るには認証が必要です。パスワードをご確認ください。</string> <string name="la_notice_title_simplex_lock">SimpleXロック</string> - <string name="enter_passphrase_notification_desc">通知を受けるには、データベースの暗証フレーズを入力してください。</string> + <string name="enter_passphrase_notification_desc">通知を受けるには、データベースのパスフレーズを入力してください。</string> <string name="simplex_service_notification_title">SimpleX Chat サービス</string> <string name="auth_simplex_lock_turned_on">SimpleXロック:オン</string> <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">起動時、または非アクティブ状態で30秒が経った後に戻ると、認証する必要となります。</string> @@ -700,7 +685,7 @@ <string name="group_preview_you_are_invited">グループ招待が届きました</string> <string name="your_chats">チャット</string> <string name="text_field_set_contact_placeholder">連絡先を設定…</string> - <string name="waiting_for_file">ファイル待ち</string> + <string name="waiting_for_file">ファイルを待機中</string> <string name="switch_receiving_address_question">受信アドレスを変えますか?</string> <string name="send_verb">送信する</string> <string name="send_live_message_desc">ライブメッセージを送信 (入力しながら宛先の画面で更新される)</string> @@ -708,7 +693,7 @@ <string name="connect_via_link_or_qr_from_clipboard_or_in_person">(クリップボードから読み込むか、貼り付ける)</string> <string name="thank_you_for_installing_simplex">SimpleX Chatをご利用いただきありがとうございます!</string> <string name="use_camera_button">カメラ</string> - <string name="you_accepted_connection">繋がりを承認しました</string> + <string name="you_accepted_connection">接続を承認しました</string> <string name="you_invited_a_contact">連絡先に招待を送りました</string> <string name="connection_you_accepted_will_be_cancelled">承認ずみの接続がキャンセルされます!</string> <string name="contact_you_shared_link_with_wont_be_able_to_connect">あなたからリンクを受けた連絡先が接続できなくなります!</string> @@ -719,7 +704,7 @@ <string name="you_will_be_connected_when_group_host_device_is_online">グループのホスト端末がオンラインになったら、接続されます。後でチェックするか、しばらくお待ちください。</string> <string name="you_will_be_connected_when_your_contacts_device_is_online">連絡先がオンラインになったら、接続されます。後でチェックするか、しばらくお待ちください。</string> <string name="your_chat_profile_will_be_sent_to_your_contact">あなたのチャットプロフィールが -\n連絡相手に送られます。</string> +\n連絡先に公開されます。</string> <string name="share_invitation_link">ワンタイムリンクを送る</string> <string name="scan_code">コードを読み込む</string> <string name="scan_code_from_contacts_app">連絡相手のアプリからセキュリティコードを読み込む</string> @@ -738,13 +723,12 @@ <string name="your_ICE_servers">あなたのICEサーバ</string> <string name="network_disable_socks">直接にインタネットに繋がりますか?</string> <string name="network_enable_socks">SOCKSプロキシを使いますか?</string> - <string name="update_onion_hosts_settings_question">.onionのホスト設定を更新しますか?</string> <string name="network_use_onion_hosts">.onionホストを使う</string> <string name="network_use_onion_hosts_prefer">利用可能時に</string> <string name="network_session_mode_transport_isolation">トランスポート隔離</string> - <string name="save_and_notify_contact">保存して、連絡先にに知らせる</string> + <string name="save_and_notify_contact">保存して連絡先に公開</string> <string name="your_current_profile">現在のプロフィール</string> - <string name="save_and_notify_contacts">保存して、連絡先にに知らせる</string> + <string name="save_and_notify_contacts">保存して連絡先に公開</string> <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">あなたのプライバシーとセキュリティを守るメッセージとアプリのプラットフォーム</string> <string name="we_do_not_store_contacts_or_messages_on_servers">連絡先情報と届けたメッセージをサーバに保存することは一切ありません。</string> <string name="you_control_your_chat">あなたのチャットはあなたが決めます!</string> @@ -754,10 +738,9 @@ <string name="secret_text">シークレット</string> <string name="strikethrough_text">取り消し線</string> <string name="callstate_starting">接続中…</string> - <string name="next_generation_of_private_messaging">次世代のプライバシー・メッセンジャー</string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[あなたがメッセージの<b>受信</b>サーバを決められます。あなたの連絡先が同じく、自分に対する受信サーバを決められます。]]></string> + <string name="next_generation_of_private_messaging">次世代のプライベートメッセンジャー</string> <string name="icon_descr_video_call">ビデオ通話</string> - <string name="onboarding_notifications_mode_off">アプリが稼働中に</string> + <string name="onboarding_notifications_mode_off">アプリがアクティブ時のみ</string> <string name="webrtc_ice_servers">WebRTC ICEサーバ</string> <string name="your_ice_servers">あなたのICEサーバ</string> <string name="settings_section_title_settings">設定</string> @@ -771,7 +754,7 @@ <string name="delete_files_and_media_desc">ファイルとメディアが全て削除されます (※元に戻せません※)。低解像度の画像が残ります。</string> <string name="enable_automatic_deletion_message">選択中の以前の送受信したメッセージが削除されます (※元に戻せません※)。数分かかります。</string> <string name="update_database">更新</string> - <string name="database_is_not_encrypted">チャットデータベースが暗号化されてません。暗証フレーズを設定して保管してください。</string> + <string name="database_is_not_encrypted">チャットデータベースが暗号化されてません。パスフレーズを設定して保管してください。</string> <string name="restore_database">データベースを復元</string> <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">アプリの設定・データベース経由で、またはアプリの再起動でチャットを始められます。</string> <string name="you_are_invited_to_group_join_to_connect_with_group_members">グループ招待が届きました。参加してグループのメンバーとお繋がりください。</string> @@ -791,13 +774,12 @@ <string name="member_role_will_be_changed_with_notification">役割が「%s」となります。グループの全員に通知が出ます。</string> <string name="conn_stats_section_title_servers">サーバ</string> <string name="member_role_will_be_changed_with_invitation">役割が「%s」となります。メンバーに新しい招待が届きます。</string> - <string name="group_is_decentralized">グループは完全分散型で、メンバーしか内容を見れません。</string> + <string name="group_is_decentralized">非中央集権型で、メンバーのみ閲覧可能。</string> <string name="network_options_save">保存</string> <string name="update_network_settings_question">ネットワーク設定を更新しますか?</string> <string name="updating_settings_will_reconnect_client_to_all_servers">設定を更新すると、全サーバにクライントの再接続が行われます。</string> - <string name="save_color">色を保存</string> <string name="chat_preferences_you_allow">あなたが次を許可しています:</string> - <string name="chat_preferences_yes">はい</string> + <string name="chat_preferences_yes">オン</string> <string name="set_group_preferences">グループ設定を行う</string> <string name="your_preferences">あなたの設定</string> <string name="voice_prohibited_in_this_chat">このチャットでは音声メッセージが使用禁止です。</string> @@ -805,15 +787,15 @@ <string name="v4_2_security_assessment">セキュリティ評価</string> <string name="whats_new">新着情報</string> <string name="v4_2_security_assessment_desc">SimpleX ChatはTrail of Bitsによるセキュリティ監査を受けました。</string> - <string name="v4_2_auto_accept_contact_requests_desc">任意的な歓迎メッセージで</string> + <string name="v4_2_auto_accept_contact_requests_desc">歓迎メッセージを追加できます。</string> <string name="v4_5_italian_interface_descr">ユーザーの皆様に感謝いたします!Weblateにて貢献できます!</string> <string name="v4_4_verify_connection_security">接続のセキュリティを確認</string> <string name="error_smp_test_failed_at_step">テストが次のステップで失敗しました:%s</string> <string name="ntf_channel_messages">SimpleX Chatメッセージ</string> <string name="ntf_channel_calls">SimpleX Chat通話</string> <string name="settings_notification_preview_mode_title">プレビューを表示</string> - <string name="notifications_mode_periodic">定期的に起動</string> - <string name="notifications_mode_off">アプリがアクティブ時に実行</string> + <string name="notifications_mode_periodic">定期的に更新</string> + <string name="notifications_mode_off">アプリがアクティブ時に通知を受ける</string> <string name="icon_descr_sent_msg_status_sent">送信済み</string> <string name="you_have_no_chats">あなたはチャットがありません。</string> <string name="share_message">メッセージを送る…</string> @@ -828,36 +810,36 @@ <string name="star_on_github">GithubでStar</string> <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">あなたのプロフィールはご自分の端末に保存され、あなたの連絡先のみに共有されます。 SimpleXのサーバには開示されません。</string> <string name="your_calls">あなたの通話</string> - <string name="integrity_msg_skipped">%1$d飛ばしたメッセージ</string> + <string name="integrity_msg_skipped">%1$dスキップしたメッセージ</string> <string name="send_link_previews">リンクのプレビューを送信</string> - <string name="set_password_to_export">暗証フレーズを設定してからエキスポート</string> + <string name="set_password_to_export">パスフレーズを設定してからエクスポート</string> <string name="stop_chat_question">チャットを閉じますか?</string> - <string name="save_passphrase_in_keychain">暗証フレーズをキーストア (暗証キー保管庫) に保存</string> - <string name="wrong_passphrase">データベースの暗証フレーズが違います</string> + <string name="save_passphrase_in_keychain">パスフレーズをキーストア (パスキー保管庫) に保存</string> + <string name="wrong_passphrase">不正なパスフレーズ</string> <string name="unknown_error">不明なエラー</string> - <string name="save_passphrase_and_open_chat">暗証フレーズをを保存して、チャットを開始</string> + <string name="save_passphrase_and_open_chat">パスフレーズを保存して、チャットを開始</string> <string name="unknown_database_error_with_info">不明なデータベースのエラー: %s</string> - <string name="wrong_passphrase_title">暗証フレーズが違います!</string> + <string name="wrong_passphrase_title">不正なパスフレーズです!</string> <string name="you_are_invited_to_group">グループ招待が届きました</string> <string name="network_option_seconds_label">秒</string> <string name="network_option_tcp_connection_timeout">TCP接続タイムアウト</string> - <string name="save_group_profile">保存グループのプロフィール</string> + <string name="save_group_profile">グループプロフィールの保存</string> <string name="v4_3_irreversible_message_deletion_desc">連絡先がメッセージの完全削除を許可できます。</string> <string name="failed_to_create_user_duplicate_desc">同じ表示名前のチャットプロフィールが既にあります。別のを選んでください。</string> - <string name="smp_server_test_secure_queue">待ち行列セキュリティ確認</string> + <string name="smp_server_test_secure_queue">セキュアな未送信メッセージ</string> <string name="database_initialization_error_desc">データベースが正しく稼働してません。詳細はタップにて</string> <string name="notification_preview_mode_message_desc">連絡先とメッセージを表示</string> <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">あなたのデータを守るために、SimpleXロックをオンにしてください。 \nオンにするには、認証ステップが行われます。</string> <string name="la_notice_turn_on">オンにする</string> <string name="auth_unlock">ロック解除</string> - <string name="save_verb">保存する</string> + <string name="save_verb">保存</string> <string name="reveal_verb">開示する</string> <string name="tap_to_start_new_chat">タップして新しいチャットを始める</string> <string name="image_decoding_exception_desc">画像が解読できません。別のイメージで試すか、開発者に伝えてください。</string> - <string name="icon_descr_waiting_for_image">画像待ち</string> - <string name="waiting_for_image">画像待ち</string> - <string name="contact_sent_large_file">向こうが最上限の大きさを超えるファイルを送ろうとしました (%1$s)。</string> + <string name="icon_descr_waiting_for_image">画像を待機中</string> + <string name="waiting_for_image">画像を待機中</string> + <string name="contact_sent_large_file">相手が上限を超える容量のファイルを送ろうとしました (%1$s)。</string> <string name="voice_messages_prohibited">音声メッセージは使用禁止です!</string> <string name="add_contact_or_create_group">新しいチャットを始める</string> <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[<font color="#0088ff">SimpleX Chatの開発者と繋がれば、質問したり、速報を受けたりすることができます</font>。]]></string> @@ -865,18 +847,18 @@ <string name="chat_help_tap_button">ボタンをタップ</string> <string name="unmute_chat">ミュート解除</string> <string name="set_contact_name">連絡先を設定</string> - <string name="contact_wants_to_connect_with_you">はあなたと繋がりたいです!</string> + <string name="contact_wants_to_connect_with_you">あなたと接続を希望しています!</string> <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[リンクをクリックすることでも接続できます。ブラウザが起動すれば <b>Open in mobile app (アプリで開く)</b>ボタンをクリックしてください。]]></string> - <string name="your_chat_profiles">あなたのチャットプロフィール</string> + <string name="your_chat_profiles">チャットプロフィール</string> <string name="smp_servers_scan_qr">サーバのQRコードを読み込む</string> <string name="smp_servers_test_some_failed">テストに失敗したサーバがあります:</string> <string name="use_simplex_chat_servers__question">SimpleX Chatサーバを使いますか?</string> - <string name="using_simplex_chat_servers">SimpleX Chatを使っています。</string> + <string name="using_simplex_chat_servers">SimpleX Chatサーバを使用中。</string> <string name="share_link">リンクを送る</string> <string name="core_simplexmq_version">simplexmq: バージョン%s (%2s)</string> - <string name="callstate_waiting_for_answer">応答待ち…</string> - <string name="callstate_waiting_for_confirmation">確認待ち…</string> - <string name="first_platform_without_user_ids">世界初のユーザーIDのないプラットフォーム|設計も元からプライベート</string> + <string name="callstate_waiting_for_answer">応答を待機中…</string> + <string name="callstate_waiting_for_confirmation">確認を待機中…</string> + <string name="first_platform_without_user_ids">世界初のユーザーIDのないプラットフォーム - プライバシーに配慮した設計</string> <string name="use_chat">チャット</string> <string name="contact_wants_to_connect_via_call">%1$sは次の方法であなたと繋がりたいです:</string> <string name="video_call_no_encryption">ビデオ通話 (非エンドツーエンド暗号化)</string> @@ -884,10 +866,9 @@ <string name="icon_descr_speaker_on">スピーカーオン</string> <string name="your_privacy">あなたのプライバシー</string> <string name="messages_section_description">この設定は現在のチャットプロフィールのメッセージに適応されます。</string> - <string name="you_have_to_enter_passphrase_every_time">アプリ起動時に暗証フレーズを入力しなければなりません。端末に保存されてません。</string> - <string name="database_backup_can_be_restored">データベースの暗証フレーズ変更が完了してません。</string> + <string name="you_have_to_enter_passphrase_every_time">アプリ起動時にパスフレーズを入力しなければなりません。端末に保存されてません。</string> + <string name="database_backup_can_be_restored">データベースのパスフレーズ変更が完了してません。</string> <string name="you_can_share_group_link_anybody_will_be_able_to_connect">リンク、またはQRコードを共有できます。誰でもグループに参加できます。後で削除しても、グループのメンバーがそのままのこります。</string> - <string name="network_options_revert">元に戻す</string> <string name="update_network_settings_confirmation">更新</string> <string name="accept_feature_set_1_day">1日に設定</string> <string name="v4_4_disappearing_messages_desc">一定時間が経ったら送信されたメッセージが削除されます。</string> @@ -896,7 +877,7 @@ <string name="v4_5_transport_isolation">トランスポート隔離</string> <string name="restore_database_alert_confirm">復元</string> <string name="restore_database_alert_title">データベースを復元しますか?</string> - <string name="save_and_notify_group_members">保存して、グループのメンバーにに知らせる</string> + <string name="save_and_notify_group_members">保存してグループのメンバーに知らせる</string> <string name="send_us_an_email">メールを送る</string> <string name="share_image">メディア共有…</string> <string name="simplex_link_mode">SimpleXリンク</string> @@ -904,7 +885,7 @@ <string name="smp_servers_test_servers">テストサーバ</string> <string name="switch_receiving_address_desc">受信アドレスは別のサーバーに変更されます。アドレス変更は送信者がオンラインになった後に完了します。</string> <string name="to_protect_privacy_simplex_has_ids_for_queues">あなたのプライバシーを守るために、他のアプリと違って、ユーザーIDの変わりに SimpleX メッセージ束毎にIDを配布し、各連絡先が別々と扱います。</string> - <string name="group_main_profile_sent">あなたのチャットプロフィールが他のグループメンバーに送られます。</string> + <string name="group_main_profile_sent">あなたのチャットプロフィールが他のグループメンバーに公開されます。</string> <string name="to_verify_compare">エンドツーエンド暗号化を確認するには、ご自分の端末と連絡先の端末のコードを比べます (スキャンします)。</string> <string name="trying_to_connect_to_server_to_receive_messages_with_error">このコンタクトから受信するメッセージのサーバに接続しようとしてます。(エラー: %1$s)。</string> <string name="connection_error_auth_desc">使用済みリンク、または連絡先による接続の削除ではなければ、バッグの可能性があります。開発者にお伝えください。 @@ -916,7 +897,7 @@ <string name="image_descr_simplex_logo">SimpleXロゴ</string> <string name="you_need_to_allow_to_send_voice">音声メッセージを送るには、連絡相手からの音声メッセージを許可しなければなりません。</string> <string name="you_must_use_the_most_recent_version_of_database">あなたの最新データベースを1つの端末にしか使わなければ、一部の連絡先からメッセージが届きかねます。</string> - <string name="you_are_already_connected_to_vName_via_this_link">既に繋がってます: %1$s</string> + <string name="you_are_already_connected_to_vName_via_this_link">すでに接続済です: %1$s</string> <string name="alert_title_cant_invite_contacts_descr">シークレットモードのプロフィールでこのグループに参加しています。メインのプロフィールを守るために、招待することができません。</string> <string name="description_you_shared_one_time_link">使い捨てリンクを送りました</string> <string name="profile_will_be_sent_to_contact_sending_link">リンクを送ってくれた連絡先にあなたのプロフィールを送ります。</string> @@ -927,10 +908,9 @@ <string name="incognito_random_profile">あなたのランダム・プロフィール</string> <string name="v4_3_voice_messages">音声メッセージ</string> <string name="settings_section_title_socks">SOCKSプロキシ</string> - <string name="stop_chat_to_export_import_or_delete_chat_database">データベースのエキスポート、読み込み、削除するにはチャットを閉じてからです。チャットを閉じると送受信ができなくなります。</string> + <string name="stop_chat_to_export_import_or_delete_chat_database">データベースのエクスポート、読み込み、削除するにはチャット機能を停止する必要があります。チャット機能を停止すると送受信ができなくなります。</string> <string name="delete_chat_profile_action_cannot_be_undone_warning">あなたのプロフィール、連絡先、メッセージ、ファイルが完全削除されます (※元に戻せません※)。</string> - <string name="update_database_passphrase">データベース暗証フレーズを更新</string> - <string name="save_archive">アーカイブを保存</string> + <string name="update_database_passphrase">データベースパスフレーズを更新</string> <string name="snd_group_event_changed_role_for_yourself">あなたが自分の役割を次に変えました:%s</string> <string name="snd_conn_event_switch_queue_phase_completed">アドレスを変えました</string> <string name="snd_conn_event_switch_queue_phase_completed_for_member">%sのアドレスを変えました</string> @@ -947,7 +927,6 @@ <string name="la_lock_mode_passcode">パスコード入力</string> <string name="la_auth_failed">認証失敗</string> <string name="la_enter_app_passcode">パスコードを入力</string> - <string name="cant_delete_user_profile">ユーザープロフィールが削除できません。</string> <string name="la_mode_system">システム</string> <string name="change_lock_mode">ロックモードを変更</string> <string name="v4_6_chinese_spanish_interface">中国語とスペイン語UI</string> @@ -964,7 +943,7 @@ <string name="read_more_in_user_guide_with_link"><![CDATA[<font color="#0088ff">ユーザーガイド</font>で詳細を見る]]></string> <string name="simplex_address">SimpleXアドレス</string> <string name="network_proxy_port">ポート %d</string> - <string name="enter_welcome_message_optional">ウェルカムメッセージを入力…(オプション)</string> + <string name="enter_welcome_message_optional">ウェルカムメッセージを入力…(任意)</string> <string name="save_settings_question">設定を保存しますか?</string> <string name="confirm_passcode">パスコードを確認</string> <string name="la_mode_passcode">パスコード</string> @@ -976,7 +955,7 @@ <string name="one_time_link_short">使い捨てのリンク</string> <string name="address_section_title">アドレス</string> <string name="color_background">背景色</string> - <string name="confirm_password">パスワードを確認</string> + <string name="confirm_password">ご確認のため、再度ご入力ください。</string> <string name="settings_section_title_experimenta">β機能</string> <string name="decryption_error">復号化エラー</string> <string name="button_add_welcome_message">ウェルカムメッセージを追加</string> @@ -985,11 +964,11 @@ <string name="database_upgrade">データベースアップグレード</string> <string name="audio_video_calls">音声/ビデオ通話</string> <string name="calls_prohibited_with_this_contact">音声/ビデオ通話は禁止されています。</string> - <string name="feature_received_prohibited">禁帯出</string> + <string name="feature_received_prohibited">受信拒絶</string> <string name="ensure_xftp_server_address_are_correct_format_and_unique">XFTPサーバーのアドレスが正しい形式で、行で区切られており、重複していないことを確認してください。</string> <string name="smp_server_test_delete_file">ファイルの削除</string> <string name="icon_descr_waiting_for_video">ビデオ待機中</string> - <string name="waiting_for_video">Waiting for video</string> + <string name="waiting_for_video">ビデオを待機中</string> <string name="group_welcome_title">ウェルカムメッセージ</string> <string name="host_verb">ホスト</string> <string name="lock_after">自動ロックまでの時間</string> @@ -1039,11 +1018,11 @@ <string name="delete_member_message__question">メンバーのメッセージを削除しますか?</string> <string name="import_theme">テーマのインポート</string> <string name="port_verb">ポート</string> - <string name="theme_colors_section_title">テーマカラー</string> + <string name="theme_colors_section_title">インターフェースカラー</string> <string name="stop_sharing">共有を停止</string> <string name="invite_friends">友人を招待する</string> <string name="you_can_create_it_later">後からでも作成できます</string> - <string name="password_to_show">パスワードを表示する</string> + <string name="password_to_show">ここにパスワードを入力してください。</string> <string name="authentication_cancelled">認証がキャンセルされました</string> <string name="share_address">アドレスを共有する</string> <string name="theme_simplex">SimpleX</string> @@ -1064,9 +1043,9 @@ <string name="v5_0_large_files_support">1GBまでのビデオとファイル</string> <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d メッセージの復号化に失敗しました。</string> <string name="alert_text_decryption_error_too_many_skipped">%1$d メッセージをスキップしました</string> - <string name="error_loading_smp_servers">SMP サーバーのロード中にエラーが発生しました</string> - <string name="error_saving_user_password">ユーザーパスワード保存エラー</string> - <string name="error_loading_xftp_servers">XFTP サーバーのロード中にエラーが発生しました</string> + <string name="error_loading_smp_servers">SMPサーバーのロード中にエラーが発生しました</string> + <string name="error_saving_user_password">ユーザーパスワードの保存中にエラー発生</string> + <string name="error_loading_xftp_servers">XFTPサーバーのロード中にエラーが発生しました</string> <string name="export_theme">テーマのエクスポート</string> <string name="revoke_file__message">ファイルはサーバーから削除されます。</string> <string name="if_you_cant_meet_in_person">直接会えない場合は、ビデオ通話で QR コードを表示するか、リンクを共有してください。</string> @@ -1086,10 +1065,10 @@ <string name="initial_member_role">初期の役割</string> <string name="alert_text_fragment_encryption_out_of_sync_old_database">古いデータベースのバックアップを使用した場合に発生する可能性があります。</string> <string name="v4_6_chinese_spanish_interface_descr">ユーザーに感謝します – Weblate 経由で貢献してください!</string> - <string name="v4_6_audio_video_calls_descr">Bluetooth およびその他の改善をサポートします。</string> + <string name="v4_6_audio_video_calls_descr">Bluetoothのサポートおよびその他の機能も改善されました。</string> <string name="relay_server_if_necessary">中継サーバーは必要な場合にのみ使用されます。 別の当事者があなたの IP アドレスを監視できます。</string> - <string name="stop_sharing_address">アドレスの共有を停止しますか?</string> - <string name="color_sent_message">送信</string> + <string name="stop_sharing_address">アドレス共有の停止?</string> + <string name="color_sent_message">送信メッセージ</string> <string name="color_received_message">受信したメッセージ</string> <string name="alert_text_msg_bad_hash">以前のメッセージとハッシュ値が異なります。</string> <string name="alert_text_msg_bad_id">次のメッセージの ID が正しくありません (前のメッセージより小さいか等しい)。 @@ -1104,20 +1083,18 @@ <string name="downgrade_and_open_chat">ダウングレードしてチャットを開く</string> <string name="la_please_remember_to_store_password">パスワードを覚えるか、安全に保管してください。失われたパスワードを回復する方法はありません。</string> <string name="smp_server_test_download_file">ファイルをダウンロード</string> - <string name="you_can_hide_or_mute_user_profile">ユーザープロフィールを非表示またはミュートすることができます(メニューを長押し)。</string> + <string name="you_can_hide_or_mute_user_profile">ユーザープロフィールを非表示またはミュートすることができます(名前を長押し)。</string> <string name="v4_6_group_moderation">グループのモデレーション</string> <string name="email_invite_body">こんにちは! \nSimpleX Chatの招待が届いています: %s</string> <string name="v4_6_group_welcome_message">グループのウェルカムメッセージ</string> - <string name="tap_to_activate_profile">タップしてプロフィールを有効化する。</string> + <string name="tap_to_activate_profile">タップでプロフィールを切り替え</string> <string name="profile_password">プロフィールのパスワード</string> <string name="v4_6_hidden_chat_profiles_descr">チャットのプロフィールをパスワードで保護します!</string> <string name="group_member_role_observer">オブザーバー</string> <string name="v5_0_large_files_support_descr">送信者がオンラインになるまでの待ち時間がなく、速い!</string> <string name="v5_0_app_passcode">アプリのパスコード</string> <string name="v5_0_app_passcode_descr">システム認証の代わりに設定します。</string> - <string name="should_be_at_least_one_visible_profile">少なくとも1つのユーザープロフィールが表示されている必要があります。</string> - <string name="should_be_at_least_one_profile">少なくとも1つのユーザープロファイルが必要です。</string> <string name="make_profile_private">プロフィールを非表示にできます!</string> <string name="relay_server_protects_ip">リレー サーバーは IP アドレスを保護しますが、通話時間は監視されます。</string> <string name="share_address_with_contacts_question">アドレスを連絡先と共有しますか\?</string> @@ -1127,9 +1104,9 @@ <string name="callstate_received_confirmation">確認を受け取りました…</string> <string name="feature_offered_item_with_param">提供された %s: %2s</string> <string name="error_xftp_test_server_auth">アップロードにはサーバーの認証が必要です。パスワードを確認してください</string> - <string name="moderate_verb">モデレート</string> + <string name="moderate_verb">検閲</string> <string name="icon_descr_video_asked_to_receive">ビデオの受信を要求されました</string> - <string name="moderated_item_description">モデレートされた %s</string> + <string name="moderated_item_description">%sにより検閲</string> <string name="error_updating_user_privacy">ユーザーのプライバシーの更新エラー</string> <string name="moderate_message_will_be_marked_warning">メッセージはすべてのメンバーに対してモデレート済みとしてマークされます。</string> <string name="hide_profile">非表示のプロフィール</string> @@ -1156,16 +1133,16 @@ <string name="mtr_error_different">アプリ/データベースの異なる移行: %s / %s</string> <string name="user_hide">非表示</string> <string name="user_unhide">表示にする</string> - <string name="hidden_profile_password">非表示のプロフィール パスワード</string> + <string name="hidden_profile_password">非表示プロフィールのパスワード</string> <string name="show_dev_options">表示する:</string> <string name="create_address_and_let_people_connect">人々があなたとつながるためのアドレスを作成します。</string> <string name="error_setting_address">アドレス設定エラー</string> <string name="you_can_share_your_address">アドレスをリンクまたは QR コードとして共有すると、誰でもあなたに接続できます。</string> <string name="you_wont_lose_your_contacts_if_delete_address">後でアドレスを削除しても、連絡先が失われることはありません。</string> - <string name="your_contacts_will_remain_connected">連絡先は接続されたままになります。</string> - <string name="all_your_contacts_will_remain_connected_update_sent">すべての連絡先は接続されたままになります。 プロフィールの更新が連絡先に送信されます。</string> + <string name="your_contacts_will_remain_connected">連絡先との接続は維持されます。</string> + <string name="all_your_contacts_will_remain_connected_update_sent">連絡先との接続は維持されます。 プロフィールの更新が連絡先に共有されます。</string> <string name="create_simplex_address">SimpleX のアドレスを作成</string> - <string name="share_with_contacts">連絡先と共有する</string> + <string name="share_with_contacts">連絡先に公開する</string> <string name="profile_update_will_be_sent_to_contacts">プロフィールの更新は連絡先に送信されます。</string> <string name="dont_create_address">アドレスを作成しない</string> <string name="email_invite_subject">SimpleXチャットで会話しよう</string> @@ -1173,7 +1150,7 @@ <string name="save_profile_password">プロフィールのパスワードを保存する</string> <string name="you_can_share_this_address_with_your_contacts">このアドレスを連絡先と共有して、%s に接続できるようにすることができます。</string> <string name="save_and_update_group_profile">グループプロフィールの保存と更新</string> - <string name="to_reveal_profile_enter_password">非表示のプロフィールを表示するには、チャット プロフィール ページの検索フィールドに完全なパスワードを入力します。</string> + <string name="to_reveal_profile_enter_password">非表示のプロフィールを戻すには、プロフィールページの検索欄にパスワードを入力します。</string> <string name="hide_dev_options">非表示 :</string> <string name="delete_chat_profile">チャット プロフィールを削除する</string> <string name="delete_profile">プロフィールの削除</string> @@ -1208,13 +1185,13 @@ <string name="share_text_database_id">データベースID: %d</string> <string name="share_text_deleted_at">削除されました: %s</string> <string name="info_row_disappears_at">に消えます</string> - <string name="info_row_moderated_at">モデレーターによって介入済み</string> + <string name="info_row_moderated_at">検閲済</string> <string name="share_text_received_at">受信: %s</string> <string name="share_text_updated_at">レコード更新日時: %s</string> <string name="share_text_sent_at">送信日時: %s</string> <string name="item_info_current">(現在)</string> <string name="share_text_disappears_at">%s :に消えます</string> - <string name="share_text_moderated_at">モデレーターによって介入済み: %s</string> + <string name="share_text_moderated_at">検閲済: %s</string> <string name="current_version_timestamp">%s (現在)</string> <string name="both_you_and_your_contact_can_add_message_reactions">自分も相手もメッセージへのリアクションを追加できます。</string> <string name="message_reactions">メッセージへのリアクション</string> @@ -1247,14 +1224,14 @@ <string name="v5_1_custom_themes_descr">カラーテーマをカスタマイズして共有することができます。</string> <string name="whats_new_thanks_to_users_contribute_weblate">ユーザーに感謝します – Weblate 経由で貢献してください!</string> <string name="v5_1_message_reactions_descr">ついに、私たちはそれらを手に入れました! 🚀</string> - <string name="v5_1_better_messages">より良いメッセージ</string> + <string name="v5_1_better_messages">改善されたメッセージ機能</string> <string name="la_mode_off">オフ</string> <string name="choose_file_title">ファイルを選択</string> <string name="unfavorite_chat">お気に入りを解除</string> <string name="favorite_chat">お気に入り</string> - <string name="receipts_contacts_override_enabled">Sending receipts is enabled for %d contacts</string> + <string name="receipts_contacts_override_enabled">配信通知の送信が%d件の連絡先で有効になっています</string> <string name="receipts_contacts_enable_for_all">すべて有効</string> - <string name="receipts_contacts_override_disabled">Sending receipts is disabled for %d contacts</string> + <string name="receipts_contacts_override_disabled">配信通知の送信が %d件の連絡先で無効になっています</string> <string name="receipts_contacts_disable_for_all">すべて無効</string> <string name="settings_shutdown">終了</string> <string name="conn_event_ratchet_sync_started">暗号化に同意しています…</string> @@ -1268,7 +1245,7 @@ <string name="rcv_conn_event_verification_code_reset">セキュリティコードが変更されました</string> <string name="fix_connection_confirm">修正</string> <string name="fix_connection_question">接続を修正しますか\?</string> - <string name="renegotiate_encryption">暗号化の再ネゴシエート</string> + <string name="renegotiate_encryption">暗号化された再接続</string> <string name="allow_to_send_files">ファイルやメディアの送信を許可</string> <string name="prohibit_sending_files">ファイルやメディアの送信を禁止します。</string> <string name="v5_2_fix_encryption">接続を維持</string> @@ -1290,7 +1267,7 @@ <string name="network_option_protocol_timeout_per_kb">KB あたりのプロトコル タイムアウト</string> <string name="group_members_can_send_files">グループメンバーはファイルやメディアを送信できます。</string> <string name="abort_switch_receiving_address">アドレス変更の中止</string> - <string name="files_are_prohibited_in_group">このグループでは、ファイルとメディアは禁止されています。</string> + <string name="files_are_prohibited_in_group">ファイルとメディアは禁止されています。</string> <string name="shutdown_alert_question">終了しますか?</string> <string name="shutdown_alert_desc">アプリを再起動するまで通知は機能しません。</string> <string name="v5_2_favourites_filter_descr">未読とお気に入りをフィルターします。</string> @@ -1298,13 +1275,13 @@ <string name="v5_2_fix_encryption_descr">バックアップの復元後に暗号化を修正します。</string> <string name="snd_conn_event_ratchet_sync_started">暗号化に同意しています: %s</string> <string name="conn_event_ratchet_sync_agreed">暗号化に同意しました</string> - <string name="receipts_contacts_title_disable">Disable receipts\?</string> + <string name="receipts_contacts_title_disable">配信通知を無効?</string> <string name="fix_connection_not_supported_by_contact">連絡先による修正はサポートされていません</string> <string name="fix_connection_not_supported_by_group_member">グループメンバーによる修正はサポートされていません</string> <string name="receipts_section_contacts">連絡先</string> <string name="receipts_section_description_1">これらは連絡先とグループの設定が優先されます。</string> <string name="receipts_section_description">これらの設定は現在のプロファイル用です</string> - <string name="receipts_contacts_title_enable">Enable receipts\?</string> + <string name="receipts_contacts_title_enable">配信通知を有効?</string> <string name="sender_at_ts">%s : %s</string> <string name="fix_connection">接続を修正</string> <string name="files_and_media">ファイルとメディア</string> @@ -1314,10 +1291,10 @@ <string name="no_history">履歴はありません</string> <string name="abort_switch_receiving_address_confirm">中止</string> <string name="sync_connection_force_desc">暗号化は機能しており、新しい暗号化への同意は必要ありません。接続エラーが発生する可能性があります!</string> - <string name="sync_connection_force_question">暗号化を再ネゴシエートしますか\?</string> + <string name="sync_connection_force_question">暗号化された再接続?</string> <string name="in_reply_to">返信先</string> <string name="only_owners_can_enable_files_and_media">ファイルやメディアを有効にできるのは、グループオーナーだけです。</string> - <string name="sync_connection_force_confirm">再ネゴシエート</string> + <string name="sync_connection_force_confirm">再接続</string> <string name="settings_restart_app">再起動</string> <string name="connect_via_link_incognito">シークレットモードで接続</string> <string name="turn_off_battery_optimization_button">許可</string> @@ -1334,10 +1311,10 @@ <string name="system_restricted_background_in_call_desc">アプリはバックグラウンドで1分経過すると終了します。</string> <string name="system_restricted_background_in_call_warn"><![CDATA[バックグラウンドで通話を行うには、アプリの設定で<b>アプリのバッテリー使用量</b> / <b>制限なし</b> を選択してください。]]></string> <string name="connect__your_profile_will_be_shared">あなたのプロフィール %1$s が共有されます。</string> - <string name="receipts_groups_title_enable">Enable receipts for groups\?</string> - <string name="receipts_groups_title_disable">Disable receipts for groups\?</string> - <string name="receipts_groups_override_enabled">Sending receipts is enabled for %d groups</string> - <string name="receipts_groups_override_disabled">Sending receipts is disabled for %d groups</string> + <string name="receipts_groups_title_enable">グループの配信通知を有効?</string> + <string name="receipts_groups_title_disable">グループの配信通知を無効?</string> + <string name="receipts_groups_override_enabled">配信通知の送信が%d件のグループで有効になっています</string> + <string name="receipts_groups_override_disabled">配信通知の送信が%d件のグループで無効になっています</string> <string name="send_receipts_disabled">無効</string> <string name="system_restricted_background_in_call_title">バックグラウンド通話なし</string> <string name="paste_the_link_you_received_to_connect_with_your_contact">受信したリンクを貼り付け、連絡先に接続する。</string> @@ -1352,25 +1329,25 @@ <string name="receipts_groups_disable_for_all">すべてのグループで無効にする</string> <string name="privacy_message_draft">メッセージの下書き</string> <string name="privacy_show_last_messages">最新のメッセージを表示</string> - <string name="send_receipts">Send receipts</string> + <string name="send_receipts">送信完了表示</string> <string name="rcv_group_event_n_members_connected">%s, %s および %d 人の他のメンバーが接続しています。</string> <string name="rcv_group_event_3_members_connected">%s, %s と %s は接続中</string> <string name="in_developing_desc">この機能はまだサポートされていません。次のリリースをお試しください。</string> <string name="receipts_contacts_enable_keep_overrides">有効にする(設定の優先を維持)</string> - <string name="receipts_groups_disable_keep_overrides">無効にする(グループの設定の優先を維持)</string> + <string name="receipts_groups_disable_keep_overrides">無効にする (グループの上書きを維持)</string> <string name="receipts_groups_enable_keep_overrides">有効にする(グループの設定の優先を維持)</string> - <string name="receipts_contacts_disable_keep_overrides">無効にする(設定の優先を維持)</string> + <string name="receipts_contacts_disable_keep_overrides">無効化(設定の優先を維持)</string> <string name="v5_2_disappear_one_message_descr">会話中に無効になっている場合でも。</string> - <string name="v5_2_message_delivery_receipts">Message delivery receipts!</string> - <string name="delivery_receipts_title">Delivery receipts!</string> - <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Sending delivery receipts will be enabled for all contacts in all visible chat profiles.</string> - <string name="message_delivery_error_title">Message delivery error</string> - <string name="send_receipts_disabled_alert_title">Receipts are disabled</string> - <string name="settings_section_title_delivery_receipts">SEND DELIVERY RECEIPTS TO</string> - <string name="sending_delivery_receipts_will_be_enabled">Sending delivery receipts will be enabled for all contacts.</string> - <string name="send_receipts_disabled_alert_msg">This group has over %1$d members, delivery receipts are not sent.</string> - <string name="error_enabling_delivery_receipts">Error enabling delivery receipts!</string> - <string name="delivery_receipts_are_disabled">Delivery receipts are disabled!</string> + <string name="v5_2_message_delivery_receipts">メッセージの配送通知</string> + <string name="delivery_receipts_title">配信通知!</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">配信通知の送信はすべてのチャットプロフィールのすべての連絡先に対して有効になります。</string> + <string name="message_delivery_error_title">メッセージ配信でエラー発生</string> + <string name="send_receipts_disabled_alert_title">通知は無効になりました</string> + <string name="settings_section_title_delivery_receipts">配信通知の送信</string> + <string name="sending_delivery_receipts_will_be_enabled">すべての連絡先で配信通知の送信が有効になります。</string> + <string name="send_receipts_disabled_alert_msg">このグループには %1$d 人以上のメンバーがいるため、配信通知は送信されません。</string> + <string name="error_enabling_delivery_receipts">配信通知を有効化する際にエラー発生!</string> + <string name="delivery_receipts_are_disabled">配信通知は無効になりました!</string> <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">データベースは暗号化され、パスフレーズは設定に保存されます。</string> <string name="you_can_change_it_later">ランダムなパスフレーズは設定に平文として保存されます。 \n後で変更できます。</string> @@ -1384,7 +1361,7 @@ <string name="passphrase_will_be_saved_in_settings">パスフレーズを変更するかアプリを再起動すると、平文として設定に保存されます。</string> <string name="settings_is_storing_in_clear_text">パスフレーズは平文として設定に保存されます。</string> <string name="socks_proxy_setting_limitations"><![CDATA[<b>注意</b>: メッセージとファイルのリレーは SOCKS プロキシ経由で接続されます。 通話とリンク プレビューの送信には直接接続が使用されます。]]></string> - <string name="encrypt_local_files">ローカルファイルを暗号化する</string> + <string name="encrypt_local_files">ローカルファイルを暗号化</string> <string name="rcv_group_event_open_chat">開く</string> <string name="v5_3_encrypt_local_files">保存されたファイルとメディアを暗号化する</string> <string name="error_creating_member_contact">メンバー連絡先の作成中にエラーが発生</string> @@ -1399,4 +1376,636 @@ <string name="v5_3_simpler_incognito_mode_descr">接続時にシークレットモードを切り替えます。</string> <string name="member_contact_send_direct_message">ダイレクトメッセージを送る</string> <string name="rcv_group_event_member_created_contact">直接接続中</string> -</resources> \ No newline at end of file + <string name="rcv_direct_event_contact_deleted">削除済の連絡先</string> + <string name="v5_4_incognito_groups_descr">ランダムなプロフィールを使用してグループを作成します。</string> + <string name="create_group_button">グループの作成</string> + <string name="create_another_profile_button">プロフィールの作成</string> + <string name="connected_desktop">デスクトップに接続済</string> + <string name="multicast_connect_automatically">自動的に接続</string> + <string name="desktop_address">デスクトップアドレス</string> + <string name="camera_not_available">カメラが見つかりません</string> + <string name="discover_on_network">ローカルネットワーク上で検出</string> + <string name="connect_plan_connect_via_link">リンクで接続?</string> + <string name="connect_plan_connect_to_yourself">自分自身と接続?</string> + <string name="desktop_device">デスクトップ</string> + <string name="connected_to_desktop">デスクトップに接続済</string> + <string name="connecting_to_desktop">デスクトップに接続中</string> + <string name="desktop_devices">デスクトップ機器</string> + <string name="delete_messages__question">%d件のメッセージを削除?</string> + <string name="connect_with_contact_name_question">%1$sと接続?</string> + <string name="blocked_items_description">%d件のメッセージをブロック</string> + <string name="connected_mobile">モバイルに接続済</string> + <string name="create_chat_profile">チャットプロフィールの作成</string> + <string name="delete_and_notify_contact">削除と連絡先への通知</string> + <string name="desktop_connection_terminated">接続停止</string> + <string name="connect_to_desktop">デスクトップに接続</string> + <string name="disconnect_remote_hosts">モバイルの接続解除</string> + <string name="disconnect_remote_host">接続解除</string> + <string name="creating_link">リンクの作成中…</string> + <string name="rcv_group_events_count">%d件のグループイベント</string> + <string name="connected_to_mobile">モバイルに接続済</string> + <string name="devices">デバイス</string> + <string name="multicast_discoverable_via_local_network">ローカルネットワーク上で検出可能</string> + <string name="add_contact_tab">連絡先に追加</string> + <string name="disconnect_desktop_question">デスクトップの接続解除?</string> + <string name="desktop_app_version_is_incompatible">デスクトップ版アプリ%sはこのアプリと互換性がありません。</string> + <string name="encryption_renegotiation_error">暗号化再接続エラー</string> + <string name="error_alert_title">エラー</string> + <string name="marked_deleted_items_description">%d件のメッセージを削除</string> + <string name="moderated_items_description">%2$sにより%1$d件のメッセージを検閲</string> + <string name="disable_sending_recent_history">新規メンバーに履歴を送信しません。</string> + <string name="error_showing_content">コンテンツ表示エラー</string> + <string name="alert_text_encryption_renegotiation_failed">暗号化再接続に失敗しました。</string> + <string name="enable_camera_access">カメラを有効化</string> + <string name="error_sending_message_contact_invitation">招待状の送信エラー</string> + <string name="enter_this_device_name">このデバイスの名前を入力…</string> + <string name="error">エラー</string> + <string name="expand_verb">展開</string> + <string name="connect_plan_join_your_group">グループに参加?</string> + <string name="connect_plan_group_already_exists">グループはすでに存在します!</string> + <string name="recent_history_is_not_sent_to_new_members">過去の会話は新規メンバーに送信されません。</string> + <string name="desktop_incompatible_version">非互換バージョン</string> + <string name="new_desktop"><![CDATA[<i>(新規)</i>]]></string> + <string name="v5_4_incognito_groups">匿名グループ</string> + <string name="v5_4_better_groups_descr">より高速な参加と、より信頼性の高いメッセージ。</string> + <string name="found_desktop">デスクトップの検出</string> + <string name="error_showing_message">メッセージ表示エラー</string> + <string name="link_a_mobile">モバイルにリンク</string> + <string name="keep_unused_invitation_question">未使用の招待状を保持?</string> + <string name="invalid_name">不正な名前!</string> + <string name="invalid_qr_code">不正なQRコード</string> + <string name="non_content_uri_alert_title">不正なファイルパス</string> + <string name="keep_invitation_link">保持</string> + <string name="v5_4_link_mobile_desktop">モバイルとデスクトップのアプリを接続!</string> + <string name="linked_desktop_options">接続済のデスクトップのオプション</string> + <string name="linked_desktops">接続済のデスクトップ</string> + <string name="linked_mobiles">接続済のモバイル</string> + <string name="loading_remote_file_title">ファイルの読み込み中</string> + <string name="loading_chats">チャットの読み込み中…</string> + <string name="v5_4_block_group_members">ブロックしたグループメンバー</string> + <string name="connect_plan_repeat_connection_request">接続要求を繰り返しますか?</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[すでに<b>%1$s</b>に接続しています。]]></string> + <string name="connect_plan_you_are_already_joining_the_group_via_this_link">あなたはすでにこのリンクでグループに参加しています。</string> + <string name="refresh_qr_code">再読み込み</string> + <string name="group_members_2">%sと%s</string> + <string name="new_mobile_device">新規モバイルデバイス</string> + <string name="only_one_device_can_work_at_the_same_time">同時に動作するのはひとつのデバイスだけです。</string> + <string name="you_can_make_address_visible_via_settings">設定でSimpleXの連絡先に表示させることができます。</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[あなたはすでにグループ<b>%1$s</b>に参加しています。]]></string> + <string name="connect_plan_this_is_your_own_one_time_link">これはあなた自身のワンタイムリンクです!</string> + <string name="chat_is_stopped_you_should_transfer_database">チャットが停止しています。このデータベースを他のデバイスで既に使用している場合は、チャットを開始する前に転送し直してください。</string> + <string name="v5_4_link_mobile_desktop_descr">セキュアな量子耐性プロトコル。</string> + <string name="open_on_mobile_and_scan_qr_code"><![CDATA[モバイルアプリで<i>デスクトップから使用</i>を開いてQRコードをスキャンします。]]></string> + <string name="connect_plan_already_connecting">すでに接続しています!</string> + <string name="retry_verb">再試行</string> + <string name="v5_4_block_group_members_descr">不要なメッセージを隠す。</string> + <string name="unlink_desktop_question">デスクトップのリンクを解除?</string> + <string name="v5_4_better_groups">改善されたグループ機能</string> + <string name="video_decoding_exception_desc">ビデオがデコードできません。別のビデオを試すか、開発者に連絡してください。</string> + <string name="rcv_group_event_1_member_connected">%sは接続済</string> + <string name="random_port">ランダム</string> + <string name="rcv_group_and_other_events">および %d件のその他イベント</string> + <string name="connect_plan_already_joining_the_group">すでにグループに参加しています!</string> + <string name="group_members_n">%sや%sなど%d人のメンバー</string> + <string name="this_device">このデバイス</string> + <string name="enable_sending_recent_history">新規メンバーに最大100通のメッセージ履歴を送信する。</string> + <string name="unblock_member_button">ブロックしていないメンバー</string> + <string name="remote_host_was_disconnected_toast"><![CDATA[モバイル<b>%s</b>が切断されました]]></string> + <string name="waiting_for_desktop">デスクトップを待機中…</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>コンタクトの追加</b>: 新しい招待リンクを作成するか、受け取ったリンクから接続します。]]></string> + <string name="or_show_this_qr_code">またはこのコードを見せる</string> + <string name="recent_history_is_sent_to_new_members">新規メンバーには最大100通のメッセージ履歴が送信されます。</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">読み取ったコードはSimpleXのリンクQRコードではありません。</string> + <string name="contact_tap_to_connect">タップして接続</string> + <string name="this_device_name">このデバイス名</string> + <string name="the_text_you_pasted_is_not_a_link">あなたがペーストしたテキストはSimpleXのリンクではありません。</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[あなたはすでにグループ<b>%1$s</b>に参加しています。]]></string> + <string name="connect_plan_this_is_your_own_simplex_address">これはあなた自身のSimpleXアドレスです!</string> + <string name="correct_name_to">正しい名前は%s?</string> + <string name="not_compatible">非互換です!</string> + <string name="waiting_for_mobile_to_connect">モバイルの接続を待機中:</string> + <string name="you_can_view_invitation_link_again">招待リンクは接続の詳細設定画面で再表示できます。</string> + <string name="remove_member_button">メンバーの削除</string> + <string name="start_chat_question">チャットを開始?</string> + <string name="block_member_confirmation">ブロック</string> + <string name="share_this_1_time_link">このワンタイム招待コードを共有する</string> + <string name="unblock_member_question">メンバーのブロックを解除?</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>グループ作成</b>:新しいグループを作成する。]]></string> + <string name="open_port_in_firewall_desc">モバイルアプリからデスクトップへの接続を許可するには、ファイアウォールでこのポートを開いてください。</string> + <string name="block_member_button">メンバーのブロック</string> + <string name="recent_history">表示可能な履歴</string> + <string name="settings_section_title_use_from_desktop">デスクトップから使用</string> + <string name="session_code">セッションコード</string> + <string name="connect_plan_repeat_join_request">参加要求を繰り返しますか?</string> + <string name="button_remove_member_question">メンバーを削除?</string> + <string name="app_was_crashed">クラッシュしました</string> + <string name="la_app_passcode">アプリのパスコード</string> + <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">あなたはすでにこのワンタイムリンクで接続しています!</string> + <string name="connect_plan_open_group">公開グループ</string> + <string name="this_device_version"><![CDATA[<i>(このデバイスv%s)</i>]]></string> + <string name="unlink_desktop">リンク解除</string> + <string name="new_chat">新規チャット</string> + <string name="unblock_member_desc">%sからのメッセージが表示されます!</string> + <string name="this_device_name_shared_with_mobile">デバイス名は接続されたモバイルクライアントと共有されます。</string> + <string name="verify_code_on_mobile">モバイルでコードを検証</string> + <string name="open_port_in_firewall_title">ファイヤーウォールのポートを開ける</string> + <string name="non_content_uri_alert_text">無効なファイルパスを共有しました。アプリ開発者に問題を報告してください。</string> + <string name="block_member_question">メンバーをブロック?</string> + <string name="group_member_role_author">作成者</string> + <string name="periodic_notifications_desc">アプリは定期的に新しいメッセージを取得します - 1日に数パーセントのバッテリーを使用します。このアプリはプッシュ通知を使用しません - あなたのデバイスからのデータはサーバーに送信されません。</string> + <string name="bad_desktop_address">不正なデスクトップアドレス</string> + <string name="or_scan_qr_code">またはQRコードでスキャンする</string> + <string name="paste_desktop_address">デスクトップアドレスを貼り付ける</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[これはグループ<b>%1$s</b>のリンクです!]]></string> + <string name="verify_code_with_desktop">デスクトップでコードを検証</string> + <string name="scan_qr_code_from_desktop">デスクトップからQRコードをスキャン</string> + <string name="unblock_member_confirmation">ブロック解除</string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[プライバシーを守るためアプリはプッシュ通知の代わりに<b>SimpleXバックグラウンドサービス</b>を備えています。- この機能は1日に数パーセントのバッテリーを消費します。]]></string> + <string name="v5_4_more_things_descr">- オプションで削除された連絡先を通知します。 +\n- プロフィール名に空白を含めることが可能になりました。 +\n- その他にも!</string> + <string name="v5_3_discover_join_groups_descr">- ディレクトリサービスに接続(ベータ)! +\n- 配信通知(最大20名)。 +\n- より速くより安定しました。</string> + <string name="tap_to_scan">タップしてスキャン</string> + <string name="connect_plan_you_have_already_requested_connection_via_this_address">このアドレスでの接続はすでに要求済です!</string> + <string name="terminal_always_visible">新規ウィンドウでコンソールを表示</string> + <string name="scan_from_mobile">モバイルからスキャン</string> + <string name="verify_connections">接続を確認</string> + <string name="tap_to_paste_link">タップしてリンクをペースト</string> + <string name="loading_remote_file_desc">リンク先のモバイルからファイルを読み込んでいますので、しばらくお待ちください。</string> + <string name="block_member_desc">%s からの新しいメッセージはすべて非表示になります!</string> + <string name="verify_connection">接続を確認</string> + <string name="no_connected_mobile">未接続のモバイル</string> + <string name="search_or_paste_simplex_link">検索またはSimpleXリンクをペースト</string> + <string name="blocked_item_description">ブロック済</string> + <string name="agent_internal_error_title">内部エラー</string> + <string name="agent_critical_error_title">重大エラー</string> + <string name="failed_to_create_user_invalid_title">表示名が無効です!</string> + <string name="remote_host_was_disconnected_title">切断されました。</string> + <string name="remote_ctrl_was_disconnected_title">切断されました。</string> + <string name="remote_host_disconnected_from"><![CDATA[携帯 <b>%s</b> が次の理由で切断されました: %s]]></string> + <string name="remote_ctrl_disconnected_with_reason">次の理由で切断されました: %s</string> + <string name="remote_ctrl_error_bad_state">PC版との接続が不安定</string> + <string name="remote_host_error_bad_state"><![CDATA[携帯 <b>%s</b> との接続が不安定]]></string> + <string name="remote_ctrl_error_inactive">PC版が非アクティブ</string> + <string name="remote_host_error_bad_version"><![CDATA[携帯版 <b>%s</b> のバージョンがサポートされてません。。両端末のバージョンが同じかどうか、ご確認ください。]]></string> + <string name="remote_ctrl_error_bad_invitation">PC版の招待コードが正しくない</string> + <string name="remote_ctrl_error_busy">PC版が処理中</string> + <string name="remote_ctrl_error_disconnected">PC版が切断されました</string> + <string name="remote_ctrl_error_bad_version">ご利用のPC版のバージョンがサポートされてません。両端末が同じバージョンかどうか、ご確認ください。</string> + <string name="developer_options_section">開発者向けの設定</string> + <string name="remote_host_error_busy"><![CDATA[携帯版 <b>%s</b> がただいま処理中]]></string> + <string name="possible_slow_function_desc">機能の処理時間が以上にかかってます: %1$d 秒: %2$s</string> + <string name="show_internal_errors">内部エラーを表示</string> + <string name="agent_internal_error_desc">開発側にお伝えください: +\n%s</string> + <string name="agent_critical_error_desc">開発側にお伝えください: +\n%s +\n +\nアプリを再起動してください。</string> + <string name="failed_to_create_user_invalid_desc">表示名が無効です。別の名前にしてください。</string> + <string name="remote_host_error_inactive"><![CDATA[携帯版 <b>%s</b> が非アクティブ]]></string> + <string name="remote_host_error_missing"><![CDATA[携帯版 <b>%s</b> が見つかりません]]></string> + <string name="remote_host_error_timeout"><![CDATA[携帯版に接続する段階で時間切れになりました <b>%s</b>]]></string> + <string name="remote_ctrl_error_timeout">PC版に接続する段階で時間切れになりました</string> + <string name="restart_chat_button">チャットを再起動</string> + <string name="remote_host_error_disconnected"><![CDATA[携帯版 <b>%s</b> が切断されました]]></string> + <string name="group_member_status_unknown_short">不明</string> + <string name="past_member_vName">過去のメンバー %1$s</string> + <string name="possible_slow_function_title">遅延が発生した機能</string> + <string name="show_slow_api_calls">遅いAPIコールを表示</string> + <string name="group_member_status_unknown">ステータス不明</string> + <string name="v5_5_private_notes">プライベートノート</string> + <string name="v5_5_message_delivery">メッセージ配信の改善</string> + <string name="e2ee_info_no_pq"><![CDATA[メッセージ、ファイル、通話は、前方秘匿性、否認防止および侵入復元性を備えた <b>エンドツーエンドの暗号化</b>によって保護されます。]]></string> + <string name="e2ee_info_pq"><![CDATA[メッセージ、ファイル、通話は、前方秘匿性、否認防止および侵入復元性を備えた <b>耐量子E2E暗号化</b>によって保護されます。]]></string> + <string name="e2ee_info_no_pq_short">このチャットはエンドツーエンド暗号化により保護されています。</string> + <string name="e2ee_info_pq_short">このチャットは耐量子エンドツーエンド暗号化により保護されています。</string> + <string name="note_folder_local_display_name">プライベートノート</string> + <string name="call_service_notification_end_call">通話終了</string> + <string name="unable_to_open_browser_title">ブラウザ起動エラー</string> + <string name="migrate_from_another_device">別の端末から移行</string> + <string name="set_passphrase">パスフレーズを設定</string> + <string name="snd_group_event_member_blocked">%s をブロック済</string> + <string name="member_blocked_by_admin">管理者によりブロック済</string> + <string name="v5_5_private_notes_descr">暗号化されたファイルとメディア</string> + <string name="v5_5_simpler_connect_ui">リンクの貼り付けで接続する!</string> + <string name="v5_5_simpler_connect_ui_descr">検索バーに招待リンクを貼り付けて接続。</string> + <string name="v5_6_quantum_resistant_encryption">耐量子暗号化チャット</string> + <string name="v5_6_picture_in_picture_calls">ピクチャー イン ピクチャー 通話</string> + <string name="v5_6_app_data_migration_descr">QRコードを利用して別のデバイスに移行できます。</string> + <string name="v5_6_safer_groups">より安全なグループ</string> + <string name="migrate_to_device_title">ここから移行</string> + <string name="migrate_to_device_migrating">移行中</string> + <string name="migrate_to_device_database_init">ダウンロードの準備中</string> + <string name="invalid_file_link">無効なリンク</string> + <string name="migrate_to_device_download_failed">ダウンロード失敗</string> + <string name="migrate_to_device_repeat_download">再ダウンロード</string> + <string name="migrate_to_device_try_again">もう一度お試し下さい。</string> + <string name="migrate_to_device_bytes_downloaded">%s ダウンロード済</string> + <string name="migrate_to_device_importing_archive">アーカイブをインポート中</string> + <string name="migrate_to_device_import_failed">インポート失敗</string> + <string name="migrate_to_device_chat_migrated">チャット移行済み!</string> + <string name="migrate_to_device_finalize_migration">別のデバイスで移行を確定してください。</string> + <string name="migrate_to_device_file_delete_or_link_invalid">ファイルが削除されているか、リンクが無効です</string> + <string name="migrate_to_device_error_downloading_archive">アーカイブダウンロードエラー</string> + <string name="migrate_to_device_apply_onion">適用</string> + <string name="migrate_from_device_title">端末の移行</string> + <string name="migrate_from_device_to_another_device">別の端末に移行</string> + <string name="migrate_from_device_error_saving_settings">設定保存エラー</string> + <string name="migrate_from_device_error_exporting_archive">チャットデータベースエクスポートエラー</string> + <string name="migrate_from_device_database_init">アップロード準備中</string> + <string name="migrate_from_device_error_deleting_database">データベース削除エラー</string> + <string name="migrate_from_device_error_uploading_archive">アーカイブアップロードエラー</string> + <string name="migrate_from_device_chat_should_be_stopped">続けるには、チャットを停止する必要があります。</string> + <string name="migrate_from_device_archive_and_upload">アーカイブとアップロード</string> + <string name="migrate_from_device_confirm_upload">アップロードの確認</string> + <string name="migrate_from_device_bytes_uploaded">%s アップロード中</string> + <string name="migrate_from_device_upload_failed">アップロード失敗</string> + <string name="migrate_from_device_archiving_database">データベースをアーカイブ中</string> + <string name="migrate_from_device_uploading_archive">アーカイブのアップロード中</string> + <string name="migrate_from_device_cancel_migration">移行の中止</string> + <string name="migrate_from_device_finalize_migration">移行の確定</string> + <string name="migrate_from_device_creating_archive_link">アーカイブリンクの作成中</string> + <string name="migrate_from_device_delete_database_from_device">この端末からデータベースを削除</string> + <string name="migrate_from_device_or_share_this_file_link">ファイルリンクで安全に共有する</string> + <string name="migrate_from_device_migration_complete">移行完了</string> + <string name="migrate_from_device_start_chat">チャット開始</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[2つの端末上で同じデータベースを使用することは<b>できません</b>。]]></string> + <string name="migrate_from_device_verify_passphrase">パスフレーズの検証</string> + <string name="migrate_from_device_verify_database_passphrase">データベースパスフレーズの検証</string> + <string name="migrate_from_device_check_connection_and_try_again">インターネット接続を確認して再試行してください</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">移行するデータベースのパスフレーズを覚えていることを確認して下さい。</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>警告</b>: アーカイブデータは削除されます。]]></string> + <string name="migrate_from_device_error_verifying_passphrase">パスフレーズ検証エラー:</string> + <string name="error_deleting_note_folder">プライベートノート削除エラー</string> + <string name="share_text_created_at">送信日: %s</string> + <string name="block_for_all">全てをブロック</string> + <string name="block_for_all_question">このメンバーをブロックしますか?</string> + <string name="member_info_member_blocked">ブロック中</string> + <string name="v5_5_join_group_conversation">グループ会話への参加</string> + <string name="call_service_notification_audio_call">音声通話</string> + <string name="call_service_notification_video_call">ビデオ通話</string> + <string name="blocked_by_admin_item_description">管理者によりブロック済</string> + <string name="v5_6_app_data_migration">アプリデータの移行</string> + <string name="v5_6_safer_groups_descr">グループ管理者は、他のすべてのメンバーに対してメンバーをブロックできます。</string> + <string name="v5_6_picture_in_picture_calls_descr">通話中にアプリが利用できます。</string> + <string name="v5_6_quantum_resistant_encryption_descr">ダイレクトチャットで有効にする(ベータ版)!</string> + <string name="welcome_message_is_too_long">ウェルカムメッセージが長すぎます</string> + <string name="clear_note_folder_warning">全てのメッセージが削除されます - これは元に戻せません!</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[新しい端末で<i>別の端末に移行</i>を選択し、QRコードをスキャンします。]]></string> + <string name="migrate_from_device_all_data_will_be_uploaded">すべての連絡先、会話、ファイルは安全に暗号化され、設定されたXFTPリレーに分割でアップロードされます。</string> + <string name="unblock_for_all">ブロックの解除</string> + <string name="unblock_for_all_question">ブロックを解除しますか?</string> + <string name="blocked_by_admin_items_description">%d のメッセージが管理者によりブロック済</string> + <string name="snd_group_event_member_unblocked">%s のブロックを解除</string> + <string name="migrate_to_device_enter_passphrase">パスフレーズの入力</string> + <string name="migrate_from_device_exported_file_doesnt_exist">エクスポートされたファイルが存在しません</string> + <string name="info_row_created_at">送信日</string> + <string name="message_too_large">メッセージが長すぎます</string> + <string name="database_migration_in_progress">データベース移行の進行中。 +\nこれには数分掛かる場合があります。</string> + <string name="clear_note_folder_question">プライベートノートを消しますか?</string> + <string name="v5_5_new_interface_languages">ハンガリー語、トルコ語</string> + <string name="v5_5_message_delivery_descr">バッテリー使用量低減</string> + <string name="v5_5_join_group_conversation_descr">最近のメッセージ履歴送信機能追加とディレクトリボットの改善が行われました。</string> + <string name="migrate_to_device_downloading_details">リンクの詳細をダウンロード中</string> + <string name="migrate_to_device_downloading_archive">アーカイブをダウンロード中</string> + <string name="error_creating_message">メッセージ作成エラー</string> + <string name="rcv_group_event_member_unblocked">%s のブロックを解除</string> + <string name="profile_update_event_removed_address">アドレスが削除されました</string> + <string name="or_paste_archive_link">アーカイブリンクを貼り付け</string> + <string name="paste_archive_link">アーカイブリンクの貼り付け</string> + <string name="migrate_to_device_repeat_import">再インポート</string> + <string name="migrate_from_device_try_again">もう一度お試し下さい。</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">警告:複数端末でのチャット利用はサポートされていません。</string> + <string name="profile_update_event_set_new_address">新しいアドレスが設定されました</string> + <string name="error_blocking_member_for_all">メンバーブロックエラー</string> + <string name="rcv_group_event_member_blocked">%s をブロック済</string> + <string name="conn_event_disabled_pq">標準的のエンドツーエンド暗号化</string> + <string name="conn_event_enabled_pq">耐量子エンドツーエンド暗号化</string> + <string name="migrate_from_device_repeat_upload">再アップロード</string> + <string name="profile_update_event_contact_name_changed">連絡先 %1$s が %2$s に変更されました</string> + <string name="profile_update_event_set_new_picture">新しいプロフィール画像が設定されました</string> + <string name="profile_update_event_removed_picture">プロフィール画像が削除されました</string> + <string name="auth_open_migration_to_another_device">移行画面を開く</string> + <string name="unable_to_open_browser_desc">通話には既定のウェブブラウザが必要です。既定のブラウザを設定し、開発者へ情報を共有してください。</string> + <string name="profile_update_event_updated_profile">プロフィール更新済</string> + <string name="profile_update_event_member_name_changed">メンバーの %1$sが %2$sに名前変更済</string> + <string name="migrate_to_device_confirm_network_settings">ネットワーク設定を確認してください</string> + <string name="migrate_to_device_confirm_network_settings_footer">この端末のネットワーク設定が正しいことを確認してください。</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>注意</b>: 2つの端末で同じデータベースを使用すると、セキュリティ保護として、あなたが接続しているメッセージの復号化が解除されます。]]></string> + <string name="saved_message_title">メッセージ保存済</string> + <string name="migrate_from_device_stopping_chat">チャット停止中</string> + <string name="error_showing_desktop_notification">エラーが表示された場合は、開発者に連絡してください。</string> + <string name="permissions_find_in_settings_and_grant">Androidの設定から権限を設定見つけ、手動で許可。</string> + <string name="permissions_grant_in_settings">設定で付与</string> + <string name="permissions_required">権限の付与</string> + <string name="permissions_camera">カメラ</string> + <string name="permissions_camera_and_record_audio">カメラとマイク</string> + <string name="permissions_record_audio">マイク</string> + <string name="permissions_grant">通話許可を付与</string> + <string name="permissions_open_settings">設定を開く</string> + <string name="network_type_cellular">モバイル回線</string> + <string name="network_type_no_network_connection">ネットワーク接続なし</string> + <string name="network_type_network_wifi">Wi-Fi</string> + <string name="network_type_ethernet">有線イーサネット</string> + <string name="settings_section_title_network_connection">ネットワーク接続</string> + <string name="network_type_other">その他</string> + <string name="group_members_can_send_simplex_links">グループメンバーはSimpleXリンクを送信できます。</string> + <string name="feature_roles_owners">オーナー</string> + <string name="simplex_links_are_prohibited_in_group">このグループではSimpleXリンクは禁止されています。</string> + <string name="simplex_links_not_allowed">SimpleXtリンクは許可されていません</string> + <string name="feature_roles_admins">管理者</string> + <string name="feature_roles_all_members">全てのメンバー</string> + <string name="allow_to_send_simplex_links">SimpleXリンクの送信を許可</string> + <string name="voice_messages_not_allowed">音声メッセージは許可されていません</string> + <string name="files_and_media_not_allowed">ファイルとメディアは許可されていません</string> + <string name="prohibit_sending_simplex_links">SimpleXリンクの送信禁止</string> + <string name="simplex_links">SimpleXリンク</string> + <string name="audio_device_bluetooth">ブルートゥース</string> + <string name="feature_enabled_for">有効化</string> + <string name="audio_device_earpiece">イヤホン</string> + <string name="audio_device_wired_headphones">ヘッドホン</string> + <string name="audio_device_speaker">スピーカー</string> + <string name="v5_7_call_sounds">着信音</string> + <string name="v5_7_call_sounds_descr">音声通話とビデオ通話の接続時</string> + <string name="v5_7_quantum_resistant_encryption_descr">ダイレクトチャットが有効化されます</string> + <string name="v5_7_network_descr">より信頼性の高いネットワーク接続</string> + <string name="v5_7_network">ネットワーク管理</string> + <string name="saved_description">保存済</string> + <string name="saved_from_description">%sから保存</string> + <string name="forwarded_chat_item_info_tab">転送済</string> + <string name="forwarded_from_chat_item_info_title">転送元</string> + <string name="saved_from_chat_item_info_title">保存元</string> + <string name="forward_chat_item">転送</string> + <string name="forwarded_description">転送済</string> + <string name="download_file">ダウンロード</string> + <string name="v5_7_forward">メッセージの転送と保存</string> + <string name="forward_message">メッセージの転送</string> + <string name="v5_7_new_interface_languages">リトアニア語UI</string> + <string name="v5_7_forward_descr">メッセージ発信者は非公開のままです</string> + <string name="network_option_rcv_concurrency">並列受信</string> + <string name="recipients_can_not_see_who_message_from">受信者は発信者を特定できません。</string> + <string name="saved_chat_item_info_tab">保存済</string> + <string name="settings_section_title_profile_images">プロフィール画像</string> + <string name="v5_7_shape_profile_images_descr">正方形、円形またはその中間</string> + <string name="v5_7_shape_profile_images">プロフィール画像をシェイプ</string> + <string name="snd_error_relay">宛先サーバエラー: %1$s</string> + <string name="ci_status_other_error">エラー: %1$s</string> + <string name="network_smp_proxy_mode_private_routing">プライベートルーティング</string> + <string name="network_smp_proxy_mode_unknown">不明なリレー</string> + <string name="network_smp_proxy_mode_unprotected">未保護</string> + <string name="network_smp_proxy_fallback_allow_downgrade">ダウングレードを許可</string> + <string name="network_smp_proxy_mode_always">常時</string> + <string name="network_smp_proxy_mode_always_description">常時プライベートルーティングを使用</string> + <string name="settings_section_title_private_message_routing">プライベートメッセージルーティング</string> + <string name="private_routing_show_message_status">メッセージステータスを表示</string> + <string name="color_mode_system">システム</string> + <string name="theme_black">ブラック</string> + <string name="color_mode">色設定</string> + <string name="color_mode_dark">ダーク</string> + <string name="dark_mode_colors">ダークモードカラー</string> + <string name="color_mode_light">ライト</string> + <string name="theme_destination_app_theme">アプリのテーマ</string> + <string name="chat_theme_apply_to_dark_mode">ダークモード</string> + <string name="chat_theme_apply_to_light_mode">ライトモード</string> + <string name="chat_theme_apply_to_mode">適用先</string> + <string name="color_primary_variant2">追加のアクセント2</string> + <string name="wallpaper_advanced_settings">高度な設定</string> + <string name="wallpaper_preview_hello_alice">こんにちは!</string> + <string name="wallpaper_preview_hello_bob">おはよう!</string> + <string name="color_wallpaper_tint">壁紙のアクセント</string> + <string name="color_wallpaper_background">壁紙の背景</string> + <string name="settings_section_title_chat_colors">チャットカラー</string> + <string name="settings_section_title_chat_theme">チャットテーマ</string> + <string name="file_error">ファイルエラー</string> + <string name="file_error_relay">ファイルサーバーエラー: %1$s</string> + <string name="info_row_file_status">ファイルステータス</string> + <string name="share_text_file_status">ファイルステータス: %s</string> + <string name="v5_8_persian_ui">ペルシャ語UI</string> + <string name="file_error_no_file">ファイルが見つかりません - 削除されたかキャンセルされた可能性があります。</string> + <string name="v5_8_chat_themes_descr">チャットの見た目を変更できます!</string> + <string name="settings_section_title_files">ファイル</string> + <string name="snd_error_expired">ネットワークエラー - 複数回送信が試行されましたが、メッセージが期限切れになりました</string> + <string name="network_smp_proxy_mode_never">無効</string> + <string name="info_row_message_status">メッセージステータス</string> + <string name="v5_8_safe_files">ファイルの安全な受け取り</string> + <string name="settings_section_title_user_theme">プロフィールテーマ</string> + <string name="share_text_message_status">メッセージステータス:%s</string> + <string name="reset_single_color">色のリセット</string> + <string name="color_received_quote">受信した返信元メッセージ</string> + <string name="color_sent_quote">送信した返信元メッセージ</string> + <string name="theme_remove_image">画像を削除</string> + <string name="wallpaper_scale">拡大縮小</string> + <string name="wallpaper_scale_fit">フィットさせる</string> + <string name="wallpaper_scale_fill">画像全体</string> + <string name="chat_theme_reset_to_app_theme">アプリのテーマをリセット</string> + <string name="chat_theme_reset_to_user_theme">ユーザーテーマをリセット</string> + <string name="chat_theme_set_default_theme">デフォルトのテーマを設定</string> + <string name="v5_8_private_routing">プライベートメッセージルーティング 🚀</string> + <string name="v5_8_safe_files_descr">不明なサーバーからのファイルを確認できます</string> + <string name="v5_8_message_delivery_descr">バッテリーの使用量が減少しました</string> + <string name="v5_8_message_delivery">メッセージ配信の改善</string> + <string name="file_not_approved_title">不明なサーバーです!</string> + <string name="network_smp_proxy_mode_never_description">プライベートルーティングを使用しない</string> + <string name="protect_ip_address">IPアドレス保護</string> + <string name="copy_error">コピーエラー</string> + <string name="v5_8_chat_themes">新しいチャットテーマ</string> + <string name="v5_8_private_routing_descr">連絡先が選択したメッセージリレーからあなたのIPアドレスを保護します。 +\n*ネットワークとサーバー*設定から有効にして下さい。</string> + <string name="network_smp_proxy_fallback_prohibit">いいえ</string> + <string name="network_smp_proxy_fallback_allow">はい</string> + <string name="update_network_smp_proxy_mode_question">メッセージルーティングモード</string> + <string name="appearance_font_size">フォントサイズ</string> + <string name="app_check_for_updates_beta">ベータ</string> + <string name="app_check_for_updates_notice_title">アップデートを確認</string> + <string name="app_check_for_updates">アップデートを確認</string> + <string name="completed">完了</string> + <string name="smp_servers_configured">SMPサーバーの構成</string> + <string name="servers_info_sessions_connected">接続中</string> + <string name="xftp_servers_configured">XFTPサーバーの構成</string> + <string name="one_hand_ui_card_title">チャトリスト切り替え</string> + <string name="contact_list_header_title">連絡先</string> + <string name="message_servers">メッセージサーバ</string> + <string name="media_and_file_servers">メディア&ファイルサーバ</string> + <string name="one_hand_ui">チャットツールバーを近づける</string> + <string name="invite_friends_short">招待</string> + <string name="create_address_button">作成</string> + <string name="compose_message_placeholder">メッセージ</string> + <string name="v6_0_reachable_chat_toolbar">チャットツールバーを近づける</string> + <string name="scan_paste_link">QRスキャン / リンクの貼り付け</string> + <string name="v6_0_reachable_chat_toolbar_descr">片手でアプリを利用できます</string> + <string name="action_button_add_members">招待</string> + <string name="paste_link">リンクの貼り付け</string> + <string name="app_check_for_updates_notice_disable">無効</string> + <string name="current_user">現在のプロフィール</string> + <string name="all_users">全てのプロフィール</string> + <string name="info_view_call_button">通話</string> + <string name="confirm_delete_contact_question">連絡先の削除を確認しますか?</string> + <string name="info_view_connect_button">接続</string> + <string name="delete_contact_cannot_undo_warning">連絡先が削除されます - この操作は取り消せません!</string> + <string name="switching_profile_error_title">プロフィールの切り替えエラー</string> + <string name="privacy_media_blur_radius">メディアのぼかし</string> + <string name="settings_section_title_chat_database">チャットデータベース</string> + <string name="chat_database_exported_continue">続ける</string> + <string name="contact_deleted">連絡先の削除完了!</string> + <string name="servers_info_details">詳細</string> + <string name="member_info_member_inactive">非アクティブ</string> + <string name="app_check_for_updates_disabled">無効</string> + <string name="network_proxy_incorrect_config_title">プロキシの保存エラー</string> + <string name="allow_calls_question">通話を許可しますか?</string> + <string name="cant_call_contact_deleted_alert_text">連絡先が削除されました。</string> + <string name="member_info_member_disabled">無効</string> + <string name="v6_0_delete_many_messages_descr">一度に最大20件のメッセージを削除できます。</string> + <string name="servers_info_connected_servers_section_header">接続中のサーバ</string> + <string name="servers_info_modal_error_title">エラー</string> + <string name="servers_info_reconnect_server_error">サーバーへの再接続エラー</string> + <string name="servers_info_sessions_errors">エラー</string> + <string name="servers_info_files_tab">ファイル</string> + <string name="decryption_errors">復号化エラー</string> + <string name="deletion_errors">削除エラー</string> + <string name="duplicates_label">重複</string> + <string name="expired_label">期限切れ</string> + <string name="servers_info_detailed_statistics">統計の詳細</string> + <string name="network_proxy_auth_mode_no_auth">プロキシで認証情報を使用しないでください。</string> + <string name="servers_info_reconnect_servers_error">サーバーへの再接続エラー</string> + <string name="servers_info_reset_stats_alert_error_title">統計のリセットエラー</string> + <string name="cannot_share_message_alert_title">メッセージを送信することができません</string> + <string name="cant_call_contact_alert_title">連絡先と通話することができません</string> + <string name="servers_info_sessions_connecting">接続待ち</string> + <string name="privacy_media_blur_radius_off">無し</string> + <string name="privacy_media_blur_radius_soft">控え目</string> + <string name="network_options_save_and_reconnect">保存して再接続</string> + <string name="privacy_media_blur_radius_strong">強め</string> + <string name="privacy_media_blur_radius_medium">普通</string> + <string name="subscription_percentage">パーセンテージを表示</string> + <string name="one_hand_ui_change_instruction">これは見た目の設定から変更できます。</string> + <string name="servers_info_previously_connected_servers_section_header">以前接続していたサーバ</string> + <string name="servers_info_subscriptions_connections_subscribed">アクティブな接続</string> + <string name="servers_info_statistics_section_header">統計情報</string> + <string name="deleted_chats">アーカイブされた連絡先</string> + <string name="settings_message_shape_corner">角丸</string> + <string name="servers_info_messages_sent">送信されたメッセージ数</string> + <string name="servers_info_reset_stats_alert_title">全ての統計情報をリセットしますか?</string> + <string name="servers_info_subscriptions_total">合計</string> + <string name="settings_message_shape_tail">しっぽ</string> + <string name="servers_info">サーバ情報</string> + <string name="servers_info_private_data_disclaimer">%sから計測されています。\nデバイス上の全てのデータはプライベートです。</string> + <string name="servers_info_messages_received">受信したメッセージ数</string> + <string name="servers_info_reset_stats_alert_message">サーバの統計情報をリセットしようとしています - これは元に戻せません!</string> + <string name="servers_info_reset_stats">全統計情報をリセットする</string> + <string name="reset_all_hints">ヒントをリセットする</string> + <string name="snd_error_quota">容量を超えました - 受信者は以前に送信されたメッセージを受け取っていません。</string> + <string name="attempts_label">試行</string> + <string name="acknowledged">確認</string> + <string name="acknowledgement_errors">確認エラー</string> + <string name="deleted">削除完了</string> + <string name="created">作成完了</string> + <string name="n_other_file_errors">%1$d件のその他のファイルエラー。</string> + <string name="error_forwarding_messages">メッセージの転送エラー</string> + <string name="n_file_errors">%1$d件のファイルエラー:\n%2$s</string> + <string name="forward_alert_title_messages_to_forward">%1$s件のメッセージを転送しますか?</string> + <string name="forward_files_in_progress_desc">%1$d件のファイルがまだダウンロード中です。</string> + <string name="forward_files_not_accepted_desc">%1$d件のファイルがダウンロードされませんでした。</string> + <string name="forward_files_failed_to_receive_desc">%1$d件のファイルのダウンロードに失敗しました。</string> + <string name="forward_files_missing_desc">%1$d件のファイルが削除されました。</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s件のメッセージが転送されませんでした。</string> + <string name="forward_files_not_accepted_receive_files">ダウンロード</string> + <string name="compose_forward_messages_n">%1$s件のメッセージを転送中</string> + <string name="conversation_deleted">会話が削除されました!</string> + <string name="app_check_for_updates_download_completed_title">アプリのアップデートがダウンロードされました</string> + <string name="app_check_for_updates_download_started">アプリの更新をダウンロード中です。アプリを閉じないでください</string> + <string name="app_check_for_updates_button_download">%s(%s)をダウンロード</string> + <string name="cant_send_message_to_member_alert_title">グループメンバーにメッセージを送信できません。</string> + <string name="v6_0_your_contacts_descr">後でチャットするために連絡先をアーカイブします。</string> + <string name="v6_0_connection_servers_status_descr">接続とサーバーのステータス</string> + <string name="call_desktop_permission_denied_chrome">アドレスフィールドの近くにある情報ボタンをクリックして、マイクの使用を許可してください。</string> + <string name="connections">接続</string> + <string name="v6_1_customizable_message_descr">カスタマイズ可能なメッセージの形。</string> + <string name="network_smp_proxy_fallback_prohibit_description">プライベートルーティングをサポートしていなくても、メッセージを直接送信しないでください。</string> + <string name="smp_proxy_error_connecting">転送サーバー%1$sへの接続エラーです。後ほど再試行してください。</string> + <string name="error_initializing_web_view_wrong_arch">WebViewの初期化エラーです。WebViewがインストールされており、サポートされているアーキテクチャがarm64であることを確認してください。\nエラー:%s</string> + <string name="error_initializing_web_view">WebViewの初期化エラーです。システムを新しいバージョンに更新してください。開発者にお問い合わせください。\nエラー:%s</string> + <string name="proxy_destination_error_broker_host">%1$sの宛先サーバーアドレスは、転送サーバー%2$sの設定と互換性がありません。</string> + <string name="proxy_destination_error_broker_version">%1$sの宛先サーバーバージョンは、転送サーバー%2$sと互換性がありません。</string> + <string name="delete_without_notification">通知なしで削除</string> + <string name="chunks_deleted">チャンクが削除されました</string> + <string name="chat_theme_apply_to_all_modes">すべてのカラーモード</string> + <string name="cant_call_member_alert_title">グループメンバーに電話できません</string> + <string name="cant_call_contact_connecting_wait_alert_text">連絡先に接続中です。しばらくお待ちいただくか、後で確認してください!</string> + <string name="v6_0_connection_servers_status">ネットワークを管理</string> + <string name="downloaded_files">ダウンロードしたファイル</string> + <string name="delete_members_messages__question">メンバーの%d件のメッセージを削除しますか?</string> + <string name="chat_database_exported_title">チャットデータベースがエクスポートされました</string> + <string name="calls_prohibited_alert_title">通話禁止!</string> + <string name="chunks_downloaded">チャンクがダウンロードされました</string> + <string name="chunks_uploaded">チャンクがアップロードされました</string> + <string name="download_errors">ダウンロードエラー</string> + <string name="network_session_mode_session">アプリセッション</string> + <string name="info_row_debug_delivery">配信のデバッグ</string> + <string name="v6_1_better_calls">改善された通話機能</string> + <string name="v6_1_message_dates_descr">メッセージの日付</string> + <string name="v6_1_better_security">より強力なセキュリティ ✅</string> + <string name="v6_1_better_user_experience">より良いユーザー体験</string> + <string name="v6_1_delete_many_messages_descr">最大200件のメッセージを削除または管理します。</string> + <string name="v6_0_privacy_blur">プライバシー向上のためのぼかし処理。</string> + <string name="v6_0_connect_faster_descr">友達ともっと速くつながりましょう。</string> + <string name="v6_0_upgrade_app_descr">新しいバージョンをGitHubからダウンロードしてください。</string> + <string name="servers_info_downloaded">ダウンロード完了</string> + <string name="reconnect">再接続</string> + <string name="send_errors">送信エラー</string> + <string name="network_socks_proxy">SOCKSプロキシ</string> + <string name="network_proxy_password">パスワード</string> + <string name="toolbar_settings">設定</string> + <string name="servers_info_missing">情報がありません、リロードしてください</string> + <string name="smp_server">SMPサーバ</string> + <string name="info_view_message_button">メッセージ</string> + <string name="operator_website">ウェブサイト</string> + <string name="info_view_video_button">ビデオ</string> + <string name="onboarding_network_operators_configure_via_settings">設定画面からサーバを構成できます。</string> + <string name="operator_open_changes">改定履歴を開く</string> + <string name="delete_chat_list_warning">全てのチャットが %s から削除され、リスト自体も削除されます</string> + <string name="appearance_app_toolbars">アプリのツールバー</string> + <string name="debug_logs">ログを有効化</string> + <string name="onboarding_notifications_mode_periodic_desc_short">メッセージ受信を10分毎に確認します</string> + <string name="onboarding_notifications_mode_service_desc_short">バックグラウンドでアプリが常時動作します</string> + <string name="onboarding_network_operators_simplex_flux_agreement">SimpleX ChatとFluxは、Fluxが運営するサーバをアプリに組み込むことに合意しました。</string> + <string name="operator_review_conditions">利用条件をレビュー</string> + <string name="network_preset_servers_title">プリセットサーバ</string> + <string name="operator_conditions_accepted">利用条件を承諾</string> + <string name="appearance_in_app_bars_alpha">透過度</string> + <string name="operators_conditions_accepted_for"><![CDATA[オペレータ <b>%s</b> の利用条件が受け入れられます。]]></string> + <string name="new_message">新しいメッセージ</string> + <string name="settings_section_title_message_shape">メッセージの形</string> + <string name="save_list">リストを保存</string> + <string name="delete_chat_list_menu_action">削除</string> + <string name="list_name_field_placeholder">リスト名...</string> + <string name="edit_chat_list_menu_action">編集</string> + <string name="delete_chat_list_question">リストを削除しますか?</string> + <string name="operator_conditions_of_use">利用条件</string> + <string name="onboarding_network_operators_continue">続ける</string> + <string name="create_list">リストを作成</string> + <string name="onboarding_choose_server_operators">サーバオペレータ</string> + <string name="operator_open_conditions">利用条件を開く</string> + <string name="onboarding_notifications_mode_off_desc_short">バックグラウンドサービスを使用しない</string> + <string name="accept_conditions">利用条件の承諾</string> + <string name="operator_conditions_accepted_for_some"><![CDATA[Conditions are already accepted for following operator(s):既にオペレータ <b>%s</b> の利用条件に承諾しています。]]></string> + <string name="address_creation_instruction">後で作成する場合はメニューから「SimpleXのアドレスを作成」を選択してください。</string> + <string name="onboarding_network_about_operators">運営者について</string> + <string name="chat_item_ttl_year">1年</string> + <string name="group_reports_active_one">1 件のレポート</string> + <string name="enable_sending_member_reports">モデレーターにメッセージを報告することを許可する</string> + <string name="block_members_desc">これらのメンバーからの新しいメッセージはすべて非表示になります!</string> + <string name="add_to_list">リストに追加</string> + <string name="address_settings">アドレス設定</string> + <string name="add_your_team_members_to_conversations">チームのメンバーを会話に追加する</string> + <string name="operator_added_message_servers">追加されたメッセージサーバー</string> + <string name="address_or_1_time_link">アドレスか使い捨てのリンク?</string> + <string name="button_add_friends">友達を追加</string> + <string name="button_add_team_members">チームメンバーを追加</string> + <string name="chat_list_add_list">リストを追加</string> + <string name="chat_list_all">すべて</string> + <string name="create_1_time_link">ワンタイムリンクを生成</string> +</resources> 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 3c7554c972..651d32518f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml @@ -2,8 +2,8 @@ <resources> <string name="server_connected">연결됨</string> <string name="server_connecting">연결 중</string> - <string name="connect_via_group_link">그룹 링크를 통해 연결하시겠습니까\?</string> - <string name="connect_via_invitation_link">초대 링크로 연결하시겠습니까\?</string> + <string name="connect_via_group_link">그룹에 참여할까요?</string> + <string name="connect_via_invitation_link">일회용 링크로 연결하시겠습니까?</string> <string name="display_name_connection_established">연결 수립됨</string> <string name="connection_timeout">연결 시간 초과</string> <string name="cannot_receive_file">파일을 받을 수 없음</string> @@ -12,7 +12,7 @@ <string name="connection_error_auth">연결 오류(인증)</string> <string name="smp_server_test_create_queue">대기열 만들기</string> <string name="database_initialization_error_title">데이터베이스를 초기화할 수 없음</string> - <string name="notifications_mode_service_desc">백그라운드 서비스가 항상 실행 됩니다. - 메시지를 받는 즉시 알림이 표시됩니다.</string> + <string name="notifications_mode_service_desc">백그라운드 서비스가 항상 실행됨 – 메시지를 받는 즉시 알림이 표시됩니다.</string> <string name="notifications_mode_periodic_desc">10분마다 최대 1분간 새 메시지 확인</string> <string name="notification_contact_connected">연결됨</string> <string name="notification_preview_somebody">숨긴 대화 상대:</string> @@ -27,16 +27,15 @@ <string name="icon_descr_context">컨텍스트 아이콘</string> <string name="icon_descr_server_status_connected">연결됨</string> <string name="back">뒤로</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>새 대화 상대 추가</b> : 대화를 위한 일회용 QR 코드 만들기]]></string> <string name="cancel_verb">취소</string> <string name="icon_descr_cancel_live_message">라이브 메시지 취소</string> - <string name="choose_file">파일 선택</string> + <string name="choose_file">파일</string> <string name="confirm_verb">확인</string> <string name="connect_via_link_or_qr">링크 / QR 코드로 연결</string> <string name="copied">클립보드로 복사됨</string> <string name="create_group">비밀 그룹 생성</string> <string name="accept_contact_button">수락</string> - <string name="clear_chat_warning">모든 메시지가 삭제됩니다 - 삭제 후 되돌릴 수 없습니다! 메시지는 나에게서만 삭제됩니다.</string> + <string name="clear_chat_warning">모든 메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다! 메시지는 나에게서만 삭제됩니다.</string> <string name="clear_verb">지우기</string> <string name="clear_chat_menu_action">지우기</string> <string name="clear_chat_button">채팅 지우기</string> @@ -44,7 +43,7 @@ <string name="connection_request_sent">연결 요청이 전송되었습니다!</string> <string name="connect_via_link">링크로 연결</string> <string name="smp_servers_preset_add">프리셋 서버 추가</string> - <string name="smp_servers_add">서버 추가…</string> + <string name="smp_servers_add">서버 추가</string> <string name="chat_console">채팅 콘솔</string> <string name="smp_servers_check_address">서버 주소를 확인 후 다시 시도해 주세요.</string> <string name="configure_ICE_servers">ICE 서버 설정</string> @@ -57,7 +56,7 @@ <string name="app_version_title">앱 버전</string> <string name="app_version_name">앱 버전 : v%s</string> <string name="core_version">코어 버전 : v%s</string> - <string name="callstatus_accepted">전화 응답</string> + <string name="callstatus_accepted">통화 응답</string> <string name="bold_text">굵게</string> <string name="callstatus_in_progress">전화 연결 중</string> <string name="colored_text">색깔</string> @@ -69,13 +68,13 @@ <string name="callstate_connected">연결됨</string> <string name="callstate_connecting">연결 중…</string> <string name="create_your_profile">내 프로필 생성</string> - <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>배터리에 좋음</b>. 백그라운드 서비스는 10분마다 메시지를 확인합니다. 전화나 긴급 메시지를 놓칠 수 있습니다.]]></string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>배터리에 좋음</b>. 앱이 10분마다 메시지를 확인합니다. 전화나 긴급 메시지를 놓칠 수 있습니다.]]></string> <string name="call_already_ended">통화가 이미 종료되었습니다!</string> <string name="always_use_relay">항상 릴레이 사용</string> <string name="icon_descr_audio_call">음성 통화</string> <string name="settings_audio_video_calls">음성 & 영상 통화</string> <string name="call_on_lock_screen">잠금 화면에서의 통화:</string> - <string name="status_contact_has_no_e2e_encryption">대화 상대와 종단간 암호화되지 않음</string> + <string name="status_contact_has_no_e2e_encryption">대화 상대와 종단 간 암호화되지 않음</string> <string name="answer_call">응답</string> <string name="icon_descr_audio_on">소리 켜기</string> <string name="icon_descr_audio_off">소리 끄기</string> @@ -88,10 +87,9 @@ <string name="chat_is_running">채팅이 작동 중</string> <string name="settings_section_title_chats">채팅</string> <string name="chat_database_imported">채팅 데이터베이스를 가져옴</string> - <string name="impossible_to_recover_passphrase"><![CDATA[<b>주의</b>: 비밀구절(passphrase)을 분실하면 복구하거나 비밀번호 변경을 할 수 없어요.]]></string> - <string name="change_database_passphrase_question">데이터베이스 암호구절(passphrase)을 바꾸시겠습니까\?</string> - <string name="confirm_new_passphrase">새로운 암호구절(passphrase) 확인…</string> - <string name="chat_archive_section">채팅 기록 보관함</string> + <string name="impossible_to_recover_passphrase"><![CDATA[<b>주의</b>: 암호를 분실하면 복구하거나 비밀번호 변경을 할 수 없어요.]]></string> + <string name="change_database_passphrase_question">데이터베이스 암호를 바꾸시겠습니까?</string> + <string name="confirm_new_passphrase">새로운 암호 확인…</string> <string name="rcv_group_event_changed_your_role">내 역할이 %s 역할로 변경됨</string> <string name="rcv_conn_event_switch_queue_phase_changing">주소 바꾸는 중…</string> <string name="snd_conn_event_switch_queue_phase_changing">주소 바꾸는 중…</string> @@ -112,18 +110,17 @@ <string name="change_member_role_question">그룹 역할을 바꾸시겠습니까\?</string> <string name="info_row_connection">연결</string> <string name="users_add">프로필 추가</string> - <string name="cant_delete_user_profile">사용자 프로필을 삭제할 수 없습니다!</string> <string name="chat_preferences_always">항상</string> <string name="chat_preferences_contact_allows">대화 상대가 허용함</string> <string name="contact_preferences">연락처 개별 설정</string> <string name="allow_voice_messages_only_if">대화 상대가 허용한 경우에만 음성 메시지를 보낼 수 있습니다.</string> - <string name="allow_your_contacts_irreversibly_delete">대화 상대가 전송한 메시지 영구 삭제를 허용합니다.</string> + <string name="allow_your_contacts_irreversibly_delete">대화 상대가 전송한 메시지 영구 삭제를 허용합니다. (24 시간)</string> <string name="allow_your_contacts_to_send_disappearing_messages">대화 상대가 사라지는 메시지를 전송할 수 있도록 허용합니다.</string> <string name="allow_your_contacts_to_send_voice_messages">대화 상대의 음성 메시지 전송을 허용합니다.</string> - <string name="both_you_and_your_contacts_can_delete">당신과 대화 상대 모두 메시지를 영구 삭제할 수 있습니다.</string> + <string name="both_you_and_your_contacts_can_delete">당신과 대화 상대 모두 메시지를 영구 삭제할 수 있습니다. (24 시간)</string> <string name="both_you_and_your_contact_can_send_voice">당신과 대화 상대 모두 음성 메시지를 보낼 수 있습니다.</string> <string name="contacts_can_mark_messages_for_deletion">상대가 메시지에 삭제 표시를 할 수 있습니다. 그러나 삭제 표시된 메시지 내용은 여전히 볼 수 있습니다.</string> - <string name="allow_to_delete_messages">보낸 메시지 영구 삭제를 허용합니다.</string> + <string name="allow_to_delete_messages">보낸 메시지 영구 삭제를 허용합니다. (24 시간)</string> <string name="feature_cancelled_item">%s 취소됨</string> <string name="v4_2_auto_accept_contact_requests">대화 요청 자동 수락</string> <string name="v4_5_transport_isolation_descr">채팅 프로필(기본값) 또는 연결(베타).</string> @@ -144,36 +141,34 @@ <string name="v4_3_improved_server_configuration_desc">QR 코드 스캔으로 서버 추가</string> <string name="button_add_welcome_message">환영 메시지 추가</string> <string name="group_member_role_admin">관리자</string> - <string name="v4_2_group_links_desc">관리자는 그룹 가입을 위한 링크를 만들 수 있습니다.</string> + <string name="v4_2_group_links_desc">관리자는 그룹 참여 링크를 만들 수 있습니다.</string> <string name="allow_to_send_disappearing">사라지는 메시지를 보낼 수 있습니다.</string> - <string name="users_delete_all_chats_deleted">모든 채팅과 메시지가 삭제됩니다 - 되돌릴 수 없습니다!</string> + <string name="users_delete_all_chats_deleted">모든 채팅과 메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다!</string> <string name="allow_to_send_voice">음성 메시지 전송을 허용합니다.</string> <string name="allow_voice_messages_question">음성 메시지를 허용하시겠습니까\?</string> <string name="allow_disappearing_messages_only_if">대화상대가 허용하는 경우에만 사라지는 메시지를 허용합니다.</string> - <string name="allow_direct_messages">그룹 구성원에게 다이렉트 메시지 보내는 것을 허용합니다.</string> - <string name="all_group_members_will_remain_connected">모든 그룹 구성원이 연결된 상태로 유지됩니다.</string> - <string name="allow_irreversible_message_deletion_only_if">대화 상대가 허용하는 경우에만 영구적인 메시지 삭제를 허용합니다.</string> + <string name="allow_direct_messages">그룹 멤버에게 다이렉트 메시지 보내는 것을 허용합니다.</string> + <string name="all_group_members_will_remain_connected">모든 그룹 멤버가 연결된 상태로 유지됩니다.</string> + <string name="allow_irreversible_message_deletion_only_if">대화 상대가 허용하는 경우에만 영구적인 메시지 삭제를 허용합니다. (24 시간)</string> <string name="all_your_contacts_will_remain_connected">모든 대화 상대가 연결된 상태로 유지됩니다.</string> <string name="notifications_mode_service">항상 켜기</string> - <string name="keychain_is_storing_securely">Android Keystore는 암호를 안전하게 저장하는 데 사용됩니다 - 알림 서비스가 작동할 수 있습니다.</string> - <string name="keychain_allows_to_receive_ntfs">앱을 다시 시작하거나 암호를 변경한 후 Android Keystore를 사용하여 암호를 안전하게 저장합니다. - 알림을 받을 수 있습니다.</string> + <string name="keychain_is_storing_securely">Android 암호 저장소는 암호를 안전하게 저장하는 데 사용됩니다 - 알림 서비스가 작동할 수 있습니다.</string> + <string name="keychain_allows_to_receive_ntfs">앱을 다시 시작하거나 암호를 변경한 후 Android 암호 저장소를 사용하여 암호를 안전하게 저장합니다. - 알림을 받을 수 있습니다.</string> <string name="notifications_mode_off_desc">앱이 실행 중일 때만 알림을 받을 수 있으며, 백그라운드 서비스는 시작되지 않습니다.</string> <string name="full_backup">앱 데이터 백업</string> <string name="settings_section_title_icon">앱 아이콘</string> <string name="network_session_mode_user_description"><![CDATA[별도로 분리된 TCP 연결(그리고 SOCKS 자격 증명)이 <b>각각의 채팅 프로필</b>에 사용될 겁니다.]]></string> - <string name="network_session_mode_entity_description">별도로 분리된 TCP 연결(및 SOCKS 자격 증명)이 <b>각각의 대화 상대 및 그룹 구성원</b>에게 사용될 겁니다. -\n<b>참고</b>: 연결이 많은 경우 배터리 및 트래픽 소비가 높을 수 있고 일부 연결이 실패할 수 있습니다.</string> + <string name="network_session_mode_entity_description"><![CDATA[별도로 분리된 TCP 연결(및 SOCKS 자격 증명)이 <b>각각의 대화 상대 및 그룹 멤버</b>에게 사용될 겁니다. \n<b>참고</b>: 연결이 많은 경우 배터리 및 트래픽 소비가 높을 수 있고 일부 연결이 실패할 수 있습니다.]]></string> <string name="icon_descr_asked_to_receive">이미지 수신 요청됨</string> <string name="v4_6_audio_video_calls">음성 및 영상 통화</string> - <string name="audio_call_no_encryption">음성 통화 (종단간 암호화 아님)</string> + <string name="audio_call_no_encryption">음성 통화 (종단 간 암호화 아님)</string> <string name="auth_unavailable">인증을 사용할 수 없음</string> <string name="turning_off_service_and_periodic">배터리 최적화가 활성화되어, 백그라운드 서비스 및 새 메시지에 대한 주기적 요청이 꺼집니다. 설정을 통해 다시 활성화할 수 있습니다.</string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>배터리에 가장 좋음</b>. 앱이 실행 중일 때만 알림을 받게 됩니다 (백그라운드에서 실행되지 않음).]]></string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>설정을 통해 비활성화할 수 있습니다.</b> – 앱이 실행되는 동안 알림이 표시됩니다.]]></string> <string name="both_you_and_your_contact_can_send_disappearing">당신과 대화 상대 모두 사라지는 메시지를 보낼 수 있습니다.</string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>QR 코드 스캔</b>: QR 코드를 보여주는 사람과 대화할 수 있습니다.]]></string> - <string name="cannot_access_keychain">데이터베이스 암호를 저장하고 있는 Keystore에 접근할 수 없습니다.</string> - <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>배터리 더욱 사용</b>! 백그라운드 서비스가 항상 실행됩니다. - 메시지를 수신되는 즉시 알림이 표시됩니다.]]></string> + <string name="cannot_access_keychain">데이터베이스 암호를 저장하고 있는 암호 저장소에 접근할 수 없습니다.</string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>배터리를 더욱 사용함</b>! 앱이 항상 백그라운드에서 실행됩니다. - 수신되는 즉시 알림이 표시됩니다.]]></string> <string name="callstatus_ended">통화 종료됨 %1$s</string> <string name="callstatus_calling">전화 중…</string> <string name="icon_descr_call_progress">전화 연결 중</string> @@ -183,7 +178,6 @@ <string name="chat_database_section">채팅 데이터베이스</string> <string name="alert_title_cant_invite_contacts">대화 상대를 초대할 수 없습니다!</string> <string name="change_verb">변경</string> - <string name="chat_archive_header">채팅 기록 보관함</string> <string name="change_role">역할 변경</string> <string name="chat_database_deleted">채팅 데이터베이스가 삭제됨</string> <string name="chat_is_stopped">채팅이 멈춤</string> @@ -197,12 +191,10 @@ <string name="group_member_status_introduced">연결 중 (도입)</string> <string name="connection_error">연결 오류</string> <string name="connection_local_display_name">연결 %1$d</string> - <string name="connect_via_contact_link">링크를 통해 연결하시겠습니까\?</string> - <string name="delete_contact_all_messages_deleted_cannot_undo_warning">대화 상대와 모든 메시지가 삭제됩니다. - 삭제 후 되돌릴 수 없습니다!</string> - <string name="status_contact_has_e2e_encryption">대화 상대와 종단간 암호화됨</string> + <string name="connect_via_contact_link">주소를 통해 연결하시겠습니까?</string> + <string name="delete_contact_all_messages_deleted_cannot_undo_warning">대화 상대와 모든 메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다!</string> + <string name="status_contact_has_e2e_encryption">대화 상대와 종단 간 암호화됨</string> <string name="alert_title_contact_connection_pending">대화 상대와 아직 연결되지 않았습니다!</string> - <string name="archive_created_on_ts">%1$s에 생성 완료</string> - <string name="create_one_time_link">일회용 초대 링크 생성</string> <string name="create_secret_group_title">비밀 그룹 생성</string> <string name="accept_contact_incognito_button">익명 수락</string> <string name="chat_item_ttl_month">1개월</string> @@ -224,7 +216,7 @@ <string name="delete_group_menu_action">삭제</string> <string name="delete_pending_connection__question">대기 중인 연결을 삭제할까요\?</string> <string name="clear_verification">인증 지우기</string> - <string name="database_passphrase_and_export">데이터베이스 비밀구절(passphrase) & 내보내기</string> + <string name="database_passphrase_and_export">데이터베이스 암호 & 내보내기</string> <string name="smp_servers_delete_server">서버 삭제</string> <string name="delete_address">주소 삭제</string> <string name="delete_address__question">주소를 삭제할까요\?</string> @@ -232,13 +224,11 @@ <string name="decentralized">탈중앙화</string> <string name="settings_developer_tools">개발자 도구</string> <string name="settings_section_title_device">기기</string> - <string name="database_passphrase">데이터베이스 비밀구절(passphrase)</string> + <string name="database_passphrase">데이터베이스 암호</string> <string name="delete_files_and_media_for_all_users">모든 채팅 프로필 파일 삭제</string> <string name="database_error">데이터베이스 에러</string> - <string name="passphrase_is_different">데이터베이스 비밀구절(passphrase)이 Keystore에 저장된 것과 일치하지 않습니다.</string> - <string name="database_passphrase_is_required">채팅을 열려면 데이터베이스 비밀구절(passphrase)이 필요합니다.</string> - <string name="delete_archive">보관된 채팅 삭제</string> - <string name="delete_chat_archive_question">보관된 채팅을 삭제할까요\?</string> + <string name="passphrase_is_different">데이터베이스 암호가 암호 저장소에 저장된 것과 일치하지 않습니다.</string> + <string name="database_passphrase_is_required">채팅을 열려면 데이터베이스 암호가 필요합니다.</string> <string name="num_contacts_selected">%d 개의 대화 상대가 선택되었습니다.</string> <string name="info_row_database_id">데이터베이스 ID</string> <string name="users_delete_profile_for">다음 채팅 프로필 삭제</string> @@ -246,12 +236,12 @@ <string name="delete_after">다음 기간 이후 자동 삭제</string> <string name="above_then_preposition_continuation">위, 다음 :</string> <string name="delete_database">데이터베이스 삭제</string> - <string name="set_password_to_export_desc">데이터베이스는 임의의 비밀구절(passphrase)로 암호화되었습니다. 내보내기 기능 사용 전 비밀구절을 변경해 주세요.</string> - <string name="delete_files_and_media_question">파일과 미디어를 삭제할까요\?</string> - <string name="current_passphrase">현재 비밀구절(passphrase)…</string> + <string name="set_password_to_export_desc">데이터베이스는 임의의 암호로 암호화되었습니다. 내보내기 기능 사용 전 암호를 변경해 주세요.</string> + <string name="delete_files_and_media_question">파일 및 미디어를 삭제하겠습니까?</string> + <string name="current_passphrase">현재 암호…</string> <string name="database_encrypted">데이터베이스 암호화 완료!</string> - <string name="database_passphrase_will_be_updated">데이터베이스 암호화 비밀구절(passphrase)이 업데이트됩니다.</string> - <string name="encrypted_with_random_passphrase">데이터베이스는 임의의 비밀구절(passphrase)로 암호화되었고, 원하시면 변경할 수 있습니다.</string> + <string name="database_passphrase_will_be_updated">데이터베이스 암호화 암호가 업데이트됩니다.</string> + <string name="encrypted_with_random_passphrase">데이터베이스는 임의의 암호로 암호화되며 변경할 수 있습니다.</string> <string name="database_will_be_encrypted">데이터베이스는 암호화될 것입니다.</string> <string name="delete_messages">메시지 삭제</string> <string name="delete_messages_after">다음 기간 이후 자동 삭제</string> @@ -264,14 +254,14 @@ <string name="ttl_days">%d일</string> <string name="button_delete_group">그룹 삭제</string> <string name="rcv_conn_event_switch_queue_phase_completed">주소 변경됨</string> - <string name="database_encryption_will_be_updated">데이터베이스 암호화 비밀구절(passphrase)이 업데이트되며 Keystore에 보관됩니다.</string> - <string name="database_will_be_encrypted_and_passphrase_stored">데이터베이스는 암호화되고, 비밀구절(passphrase)은 Keystore에 보관됩니다.</string> + <string name="database_encryption_will_be_updated">데이터베이스 암호화 암호가 업데이트되며 암호 저장소에 보관됩니다.</string> + <string name="database_will_be_encrypted_and_passphrase_stored">데이터베이스는 암호화되고, 암호는 암호 저장소에 보관됩니다.</string> <string name="users_delete_question">채팅 프로필을 삭제할까요\?</string> <string name="delete_files_and_media_all">모든 파일 삭제</string> <string name="delete_chat_profile_question">채팅 프로필을 삭제할까요\?</string> <string name="full_deletion">모두에게서 삭제</string> <string name="delete_group_question">그룹을 삭제할까요\?</string> - <string name="failed_to_create_user_duplicate_title">표시 이름이 중복되어요!</string> + <string name="failed_to_create_user_duplicate_title">표시 이름이 중복됩니다!</string> <string name="smp_server_test_disconnect">연결 끊기</string> <string name="auth_device_authentication_is_disabled_turning_off">기기 인증이 비활성화되어 SimpleX 잠금 기능이 작동하지 않아요.</string> <string name="auth_disable_simplex_lock">SimpleX 잠금 비활성화</string> @@ -280,27 +270,27 @@ <string name="add_contact_or_create_group">새로운 채팅 시작</string> <string name="display_name__field">표시 이름</string> <string name="display_name_cannot_contain_whitespace">표시 이름에는 공백문자가 쓰일 수 없어요.</string> - <string name="display_name">표시 이름</string> - <string name="encrypted_audio_call">종단간 암호화된 음성 전화</string> - <string name="encrypted_video_call">종단간 암호화된 영상 전화</string> + <string name="display_name">이름을 입력:</string> + <string name="encrypted_audio_call">종단 간 암호화된 음성 전화</string> + <string name="encrypted_video_call">종단 간 암호화된 영상 통화</string> <string name="no_call_on_lock_screen">비활성화</string> - <string name="status_e2e_encrypted">종단간 암호화</string> + <string name="status_e2e_encrypted">종단 간 암호화</string> <string name="integrity_msg_duplicate">중복된 메시지</string> <string name="accept_feature_set_1_day">1일로 설정</string> <string name="v4_4_disappearing_messages">사라지는 메시지</string> <string name="total_files_count_and_size">%d 개의 파일 총 크기 %s</string> <string name="conn_level_desc_direct">다이렉트</string> <string name="disappearing_prohibited_in_this_chat">이 채팅에서는 사라지는 메시지를 사용할 수 없습니다.</string> - <string name="disappearing_messages_are_prohibited">이 그룹에서는 사라지는 메시지를 사용할 수 없습니다.</string> + <string name="disappearing_messages_are_prohibited">사라지는 메시지를 사용할 수 없습니다.</string> <string name="ttl_m">%d분</string> <string name="ttl_months">%d 개월</string> <string name="ttl_min">%d 분</string> <string name="ttl_month">%d 개월</string> <string name="ttl_week">%d 주</string> <string name="downgrade_and_open_chat">다운그레이드하고 채팅 열기</string> - <string name="direct_messages">1:1 메시지</string> + <string name="direct_messages">다이렉트 메시지</string> <string name="timed_messages">사라지는 메시지</string> - <string name="direct_messages_are_prohibited_in_chat">이 그룹에서는 멤버들의 1:1 채팅이 금지되어 있어요.</string> + <string name="direct_messages_are_prohibited_in_group">이 그룹에서는 멤버들의 다이렉트 메시지가 금지되어 있어요.</string> <string name="ttl_s">%d초</string> <string name="ttl_sec">%d 초</string> <string name="ttl_h">%d시</string> @@ -311,7 +301,7 @@ <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">기기 인증을 하고 있지 않아요. 기기 인증을 켜면 설정에서 SimpleX 잠금 기능을 사용할 수 있어요.</string> <string name="ttl_hour">%d 시간</string> <string name="ttl_hours">%d 시간</string> - <string name="mtr_error_different">앱/데이터베이스의 다른 마이그레이션: %s / %s</string> + <string name="mtr_error_different">앱/데이터베이스의 다른 이전: %s / %s</string> <string name="v4_5_multiple_chat_profiles_descr">다른 이름, 아바타 그리고 전송 격리.</string> <string name="dont_show_again">다시 보지 않기</string> <string name="connected_to_server_to_receive_messages_from_contact">이 대화 상대로부터의 메시지를 수신할 서버와 연결되었어요.</string> @@ -340,7 +330,7 @@ <string name="button_welcome_message">환영 메시지</string> <string name="button_edit_group_profile">그룹 프로필 수정</string> <string name="button_leave_group">그룹 나가기</string> - <string name="button_send_direct_message">1:1 채팅 시작하기</string> + <string name="button_send_direct_message">다이렉트 메시지 보내기</string> <string name="conn_stats_section_title_servers">서버</string> <string name="conn_level_desc_indirect">인다이렉트 (%1$s)</string> <string name="chat_preferences_you_allow">허용함</string> @@ -361,12 +351,7 @@ <string name="allow_accepting_calls_from_lock_screen">설정에서 잠금 화면에서 바로 전화를 받을 수 있도록 설정할 수 있어요.</string> <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">연결을 완료하려면 대화 상대가 온라인 상태여야 해요. \n연결 요청을 취소하고 대화 상대를 삭제할 수 있어요 (그리고 새 링크로 재시도).</string> - <string name="alert_text_skipped_messages_it_can_happen_when">다음과 같은 경우에 발생할 수 있어요. -\n1. 대화 상대가 메시지를 보낸 지 30일 지나서 서버에서 삭제된 경우 -\n2. 메시지를 수신하는 데 사용된 서버가 업데이트되고 재부팅된 경우 -\n3. 침해된 연결의 경우 -\n서버 업데이트를 받으려면 설정에서 개발자에게 연락해 주세요. -\n저희 개발팀은 메시지 손실을 방지하기 위해 중복된 서버를 추가할 예정이에요.</string> + <string name="alert_text_skipped_messages_it_can_happen_when">다음과 같은 경우에 발생할 수 있습니다. \n1. 대화 상대가 메시지를 보낸 지 30일 지나서 서버에서 삭제된 경우 \n2. 메시지를 수신하는 데 사용된 서버가 업데이트되고 재부팅된 경우 \n3. 침해된 연결의 경우</string> <string name="auth_simplex_lock_turned_on">SimpleX 잠금 켜짐</string> <string name="callstate_received_answer">응답됨…</string> <string name="callstate_received_confirmation">확인 받음…</string> @@ -379,12 +364,12 @@ <string name="failed_to_create_user_title">프로필 생성 오류!</string> <string name="description_via_group_link_incognito">그룹 링크로 익명 채팅</string> <string name="description_via_group_link">그룹 링크로 채팅</string> - <string name="description_via_one_time_link">일회용 링크로 채팅</string> + <string name="description_via_one_time_link">일회용 링크를 통해</string> <string name="description_you_shared_one_time_link_incognito">일회용 익명 링크를 공유했어요.</string> <string name="description_you_shared_one_time_link">일회용 링크를 공유했어요.</string> <string name="description_via_contact_address_link_incognito">상대의 연락처 링크로 익명 연결</string> <string name="description_via_contact_address_link">상대의 연락처 링크로 연결</string> - <string name="description_via_one_time_link_incognito">일회용 연락처로 익명 연결</string> + <string name="description_via_one_time_link_incognito">일회용 링크로 익명 연결</string> <string name="ensure_smp_server_address_are_correct_format_and_unique">SMP 서버 주소가 올바른 형식이고 줄로 구분되어 있고 중복이 없는지 확인해 주세요.</string> <string name="error_saving_smp_servers">SMP 서버 저장 오류</string> <string name="error_setting_network_config">네트워크 설정 업데이트 오류</string> @@ -407,13 +392,13 @@ <string name="error_smp_test_failed_at_step">테스트가 %s단계에서 실패했어요.</string> <string name="error_smp_test_server_auth">서버는 대기열을 생성하고 비밀번호를 확인하려면 인증이 필요해요.</string> <string name="enter_passphrase_notification_desc">알림을 받으려면 데이터베이스 암호를 입력해 주세요.</string> - <string name="enter_passphrase_notification_title">비밀번호가 필요해요.</string> - <string name="error_deleting_user">프로필 삭제 오류</string> + <string name="enter_passphrase_notification_title">암호가 필요해요.</string> + <string name="error_deleting_user">사용자 프로필 삭제 오류</string> <string name="error_updating_user_privacy">사용자 개인정보 업데이트 오류</string> <string name="database_initialization_error_desc">데이터베이스가 올바르게 작동하지 안하요. 자세히 알아보려면 탭하세요.</string> <string name="edit_verb">수정하기</string> - <string name="delete_message_cannot_be_undone_warning">메시지가 삭제돼요. 삭제 후 복구할 수 없어요!</string> - <string name="delete_message_mark_deleted_warning">메시지가 삭제 표시될 거예요. 대화 상대는 여전히 삭제된 내용을 볼 수 있어요.</string> + <string name="delete_message_cannot_be_undone_warning">메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다!</string> + <string name="delete_message_mark_deleted_warning">메시지가 삭제 표시됩니다. 수신자는 여전히 삭제된 내용을 볼 수 있습니다.</string> <string name="ensure_ICE_server_address_are_correct_format_and_unique">WebRTC ICE 서버 주소가 올바른 형식이고 줄로 구분되고 중복이 없는지 확인해 주세요.</string> <string name="enter_one_ICE_server_per_line">ICE 서버(한 줄에 하나씩)</string> <string name="error_saving_ICE_servers">ICE 서버 저장 오류</string> @@ -429,39 +414,39 @@ <string name="export_database">데이터베이스 내보내기</string> <string name="enable_automatic_deletion_question">자동 삭제되는 메시지를 사용할까요\?</string> <string name="error_changing_message_deletion">설정 변경 오류</string> - <string name="enable_automatic_deletion_message">이 작업은 되돌릴 수 없어요. 선택한 시간보다 일찍 보내거나 받은 메시지는 삭제돼요. 이는 몇 분 걸릴 수 있어요.</string> + <string name="enable_automatic_deletion_message">이 결정은 되돌릴 수 없습니다. 선택한 시간보다 일찍 보내거나 받은 메시지는 삭제됩니다. 이는 몇 분 걸릴 수 있습니다.</string> <string name="error_with_info">오류: %s</string> - <string name="enter_correct_passphrase">올바른 비밀번호를 입력해 주세요.</string> - <string name="database_backup_can_be_restored">데이터베이스 비밀번호 변경이 완료되지 않았어요.</string> + <string name="enter_correct_passphrase">올바른 암호를 입력해 주세요.</string> + <string name="database_backup_can_be_restored">데이터베이스 암호 변경이 완료되지 않았어요.</string> <string name="database_restore_error">데이터베이스 오류 복구</string> <string name="error_creating_link_for_group">그룹 링크 생성 오류</string> <string name="error_updating_link_for_group">그룹 링크 업데이트 오류</string> <string name="error_changing_role">역할 변경 오류</string> <string name="error_removing_member">멤버 삭제 오류</string> <string name="database_downgrade">데이터베이스 다운그레이드</string> - <string name="database_migrations">마이그레이션: %s</string> - <string name="delete_group_for_all_members_cannot_undo_warning">모든 멤버에게서 그룹이 삭제돼요. 삭제 후 복구할 수 없어요!</string> - <string name="delete_group_for_self_cannot_undo_warning">나에게서만 그룹이 삭제되요. 삭제 후 복구할 수 없어요!</string> + <string name="database_migrations">이전: %s</string> + <string name="delete_group_for_all_members_cannot_undo_warning">모든 멤버에게서 그룹이 삭제됩니다. 이 결정은 되돌릴 수 없습니다!</string> + <string name="delete_group_for_self_cannot_undo_warning">나에게서만 그룹이 삭제됩니다. 이 결정은 되돌릴 수 없습니다!</string> <string name="file_not_found">파일을 찾을 수 없음</string> <string name="error_saving_user_password">사용자 비밀번호 저장 오류</string> <string name="error_stopping_chat">채팅 정지하기 오류</string> - <string name="error_exporting_chat_database">채팅 데이터베이스 내보내기 오류</string> - <string name="database_is_not_encrypted">채팅 데이터베이스가 암호화되지 않았어요. 비밀번호를 설정하여 보호해 주세요.</string> - <string name="enter_passphrase">비밀번호를 입력해 주세요…</string> + <string name="error_exporting_chat_database">채팅 데이터베이스를 내보내는 동안 오류</string> + <string name="database_is_not_encrypted">채팅 데이터베이스가 암호화되지 않았어요. 암호를 설정하여 보호해 주세요.</string> + <string name="enter_passphrase">암호를 입력해 주세요…</string> <string name="enter_password_to_show">검색에 비밀번호 입력</string> <string name="edit_image">이미지 수정하기</string> - <string name="delete_chat_profile_action_cannot_be_undone_warning">이 작업은 실행 취소될 수 없어요. 프로필, 연락처, 메시지 및 파일이 영구적으로 손실돼요.</string> + <string name="delete_chat_profile_action_cannot_be_undone_warning">이 결정은 되돌릴 수 없습니다. 프로필, 연락처, 메시지 및 파일이 영구적으로 손실됩니다.</string> <string name="error_importing_database">채팅 데이터베이스 가져오기 오류</string> <string name="encrypt_database_question">데이터베이스를 암호화할까요\?</string> <string name="developer_options">데이터베이스 ID 및 전송 격리 옵션.</string> <string name="error_starting_chat">채팅 시작하기 오류</string> <string name="error_encrypting_database">데이터베이스 암호화 오류</string> - <string name="enter_correct_current_passphrase">올바른 현재 비밀번호를 입력해 주세요.</string> + <string name="enter_correct_current_passphrase">올바른 현재 암호를 입력해 주세요.</string> <string name="delete_chat_profile">채팅 프로필 삭제</string> <string name="delete_profile">프로필 삭제</string> <string name="database_downgrade_warning">경고: 일부 데이터가 손실될 수 있어요!</string> <string name="database_upgrade">데이터베이스 업그레이드</string> - <string name="delete_files_and_media_desc">이 작업은 실행 취소될 수 없어요. 수신 및 전송된 모든 파일과 미디어가 삭제돼요. 저해상도 사진만 삭제되지 않아요.</string> + <string name="delete_files_and_media_desc">이 결정은 되돌릴 수 없습니다. 수신 및 전송된 모든 파일과 미디어가 삭제됩니다. 저해상도 사진은 삭제되지 않습니다.</string> <string name="error_deleting_database">채팅 데이터베이스 삭제 오류</string> <string name="error_deleting_link_for_group">그룹 링크 삭제 오류</string> <string name="error_saving_file">파일 저장 오류</string> @@ -492,14 +477,14 @@ <string name="group_invitation_item_description">그룹으로 초대 %1$s</string> <string name="group_link">그룹 링크</string> <string name="group_welcome_title">환영 메시지</string> - <string name="group_display_name_field">보여지는 그룹 이름</string> + <string name="group_display_name_field">그룹 이름 입력:</string> <string name="group_full_name_field">그룹 이름 :</string> - <string name="group_is_decentralized">그룹은 완전히 탈중앙화되어 있으며 구성원만 그룹을 볼 수 있어요.</string> - <string name="group_main_profile_sent">프로필이 그룹 구성원에게 전송될 거예요.</string> + <string name="group_is_decentralized">완전히 탈중앙화됨 – 멤버만 볼 수 있습니다.</string> + <string name="group_main_profile_sent">프로필이 그룹 멤버에게 전송될 거예요.</string> <string name="group_profile_is_stored_on_members_devices">그룹 프로필은 서버가 아닌 멤버들의 기기에 저장되어요.</string> <string name="group_preferences">그룹 설정</string> - <string name="group_members_can_send_disappearing">그룹 구성원은 사라지는 메시지를 보낼 수 있습니다.</string> - <string name="group_members_can_send_dms">그룹 멤버들끼리 1:1 채팅을 할 수 있어요.</string> + <string name="group_members_can_send_disappearing">그룹 멤버는 사라지는 메시지를 보낼 수 있습니다.</string> + <string name="group_members_can_send_dms">그룹 멤버들끼리 다이렉트 메시지를 보낼 수 있어요.</string> <string name="icon_descr_add_members">멤버 초대하기</string> <string name="icon_descr_group_inactive">비활성 그룹</string> <string name="group_member_role_observer">관찰자</string> @@ -517,7 +502,7 @@ <string name="how_to_use_markdown">마크다운 사용법</string> <string name="how_simplex_works">SimpleX 작동 방식</string> <string name="group_invitation_expired">그룹 초대가 만료되었어요.</string> - <string name="group_members_can_delete">그룹 멤버는 보낸 메시지를 영구 삭제할 수 있어요.</string> + <string name="group_members_can_delete">그룹 멤버는 보낸 메시지를 영구 삭제할 수 있습니다. (24 시간)</string> <string name="group_members_can_send_voice">그룹 멤버는 음성 메시지를 보낼 수 있어요.</string> <string name="hidden_profile_password">숨긴 프로필 비밀번호</string> <string name="how_it_works">작동 방식</string> @@ -549,7 +534,7 @@ <string name="images_limit_title">이미지 수가 너무 많아요!</string> <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">거절해도 상대에게 알림이 전송되지 않아요.</string> <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[직접 만날 수 없다면 <b>영상 통화에서 QR 코드를 보여주거나</b> 링크를 공유해 주세요.]]></string> - <string name="icon_descr_video_call">영상 전화</string> + <string name="icon_descr_video_call">영상 통화</string> <string name="icon_descr_video_off">영상 끄기</string> <string name="icon_descr_speaker_on">스피커 켜기</string> <string name="icon_descr_video_on">영상 켜기</string> @@ -560,7 +545,7 @@ <string name="image_will_be_received_when_contact_completes_uploading">대화 상대가 업로드를 완료하면 이미지가 수신될 거예요.</string> <string name="image_descr_profile_image">프로필 이미지</string> <string name="incognito_info_allows">하나의 프로필로 여러 사람과 연락할 필요 없이 무수히 많은 익명 프로필로 연락할 수 있어요.</string> - <string name="immune_to_spam_and_abuse">스팸 및 남용에 면역</string> + <string name="immune_to_spam_and_abuse">스팸 방지</string> <string name="ignore">무시하기</string> <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">SimpleX Chat 초대 링크를 받으면 브라우저에서 참여할 수 있어요 :</string> <string name="image_descr_link_preview">링크 미리보기 이미지</string> @@ -590,16 +575,16 @@ <string name="info_row_group">그룹</string> <string name="incompatible_database_version">호환되지 않는 데이터베이스 버전</string> <string name="joining_group">그룹에 참여 중</string> - <string name="incognito_info_protects">익명 모드는 기본 프로필 이름과 사진과 같은 개인 정보를 보호해줘요. 새 대화 상대마다 새로운 랜덤 프로필이 만들어져요.</string> + <string name="incognito_info_protects">익명 모드는 대화 상대마다 새로운 무작위 프로필을 사용하여 개인 정보를 보호합니다.</string> <string name="is_verified">%s 은(는) 인증되었어요.</string> <string name="italic_text">기울게</string> <string name="incognito_info_share">익명 프로필 사용 중 초대받은 그룹에 참여하면, 그 그룹에서도 동일한 익명 프로필이 사용되어요.</string> - <string name="incognito_random_profile">내 랜덤 프로필</string> + <string name="incognito_random_profile">내 무작위 프로필</string> <string name="incoming_audio_call">음성 전화 옴</string> <string name="is_not_verified">%s은(는) 인증되지 않았어요.</string> <string name="install_simplex_chat_for_terminal">터미널용 SimpleX Chat를 설치하세요</string> - <string name="incoming_video_call">영상 전화 옴</string> - <string name="invalid_migration_confirmation">잘못된 마이그레이션 확인</string> + <string name="incoming_video_call">영상 통화 옴</string> + <string name="invalid_migration_confirmation">잘못된 이전 확인</string> <string name="join_group_incognito_button">익명 모드로 참여</string> <string name="invalid_QR_code">잘못된 QR 코드</string> <string name="incorrect_code">잘못된 보안 코드!</string> @@ -621,7 +606,7 @@ <string name="notification_display_mode_hidden_desc">연락처 이름 및 메시지 숨기기</string> <string name="la_notice_turn_on">켜기</string> <string name="message_delivery_error_desc">대화 상대가 나와의 연결을 삭제했을 가능성이 커요.</string> - <string name="message_delivery_error_title">메시지 전달 오류</string> + <string name="message_delivery_error_title">메시지 전송 오류</string> <string name="moderate_verb">조정</string> <string name="moderate_message_will_be_deleted_warning">모든 멤버에게서 메시지가 삭제될 거예요.</string> <string name="moderate_message_will_be_marked_warning">이 메시지는 모든 멤버에게 조정됨으로 표시될 거예요.</string> @@ -638,12 +623,11 @@ <string name="messages_section_title">메시지</string> <string name="messages_section_description">이 설정은 현재 내 프로필의 메시지에 적용되어요.</string> <string name="member_info_section_title_member">멤버</string> - <string name="member_role_will_be_changed_with_invitation">역할이 \"%s\"(으)로 변경되고, 회원은 새로운 초대를 받게 될 거예요.</string> - <string name="network_options_revert">되돌리기</string> + <string name="member_role_will_be_changed_with_invitation">역할이 %s(으)로 변경되고, 멤버는 새로운 초대를 받게 될 거예요.</string> <string name="message_deletion_prohibited">이 채팅에서는 메시지 영구 삭제가 허용되지 않았어요.</string> <string name="leave_group_button">나가기</string> <string name="large_file">큰 파일!</string> - <string name="network_settings_title">네트워크 설정</string> + <string name="network_settings_title">고급 설정</string> <string name="network_use_onion_hosts_required_desc">연결하려면 Onion 호스트가 필요해요.</string> <string name="network_option_ping_count">핑 횟수</string> <string name="network_option_ping_interval">핑 간격</string> @@ -671,23 +655,19 @@ <string name="network_use_onion_hosts_prefer_desc">사용 가능한 경우 Onion 호스트가 사용될 거예요.</string> <string name="network_use_onion_hosts_no_desc">Onion 호스트가 사용되지 않을 거예요.</string> <string name="network_session_mode_transport_isolation">전송 격리</string> - <string name="network_use_onion_hosts_no_desc_in_alert">Onion 호스트가 사용되지 않을 거예요.</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">사용 가능한 경우 Onion 호스트가 사용될 거예요.</string> - <string name="network_use_onion_hosts_required_desc_in_alert">연결하려면 Onion 호스트가 필요해요.</string> <string name="next_generation_of_private_messaging">차세대 사생활 보호 메시징</string> - <string name="new_passphrase">새 비밀번호…</string> + <string name="new_passphrase">새 암호…</string> <string name="network_option_enable_tcp_keep_alive">TCP 연결 유지 활성화</string> <string name="new_in_version">%s의 새로운 기능</string> <string name="markdown_help">마크다운 도움말</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[많은 사람들의 질문 : <i>SimpleX에는 사용자 식별자가 없는데도 어떻게 메시지를 전달할 수 있어요\?</i>]]></string> <string name="leave_group_question">그룹에서 나갈까요\?</string> - <string name="mtr_error_no_down_migration">데이터베이스 버전이 앱보다 최신이지만, 다음에 대한 다운 마이그레이션 없음: %s</string> - <string name="member_will_be_removed_from_group_cannot_be_undone">멤버가 그룹에서 제거되어요. 이 작업은 되돌릴 수 없어요!</string> - <string name="member_role_will_be_changed_with_notification">역할이 \"%s\"(으)로 변경되어요. 그룹의 모든 멤버에게 알림이 전송됩니다.</string> + <string name="mtr_error_no_down_migration">앱 버전보다 최신 버전의 데이터베이스를 사용하고 있지만 데이터베이스를 다운그레이드할 수 없습니다: %s</string> + <string name="member_will_be_removed_from_group_cannot_be_undone">멤버가 그룹에서 제거됩니다. 이 결정은 되돌릴 수 없습니다!</string> + <string name="member_role_will_be_changed_with_notification">역할이 "%s"(으)로 변경되어요. 그룹의 모든 멤버에게 알림이 전송됩니다.</string> <string name="network_options_reset_to_defaults">기본값으로 재설정</string> <string name="notification_preview_mode_message">메시지 내용</string> <string name="notification_preview_mode_message_desc">대화 상대 이름 및 메시지 표시</string> - <string name="only_you_can_delete_messages">나만 메시지를 영구 삭제할 수 있어요(대화 상대는 \"삭제됨\" 표시만 할 수 있음).</string> + <string name="only_you_can_delete_messages">나만 메시지를 영구 삭제할 수 있어요(대화 상대는 "삭제됨" 표시만 할 수 있음).</string> <string name="profile_will_be_sent_to_contact_sending_link">이 링크를 보낸 상대에게 프로필이 전송될 거예요.</string> <string name="receiving_files_not_yet_supported">파일 수신은 아직 지원되지 않아요.</string> <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">올바른 링크를 사용했는지 확인하거나 상대에게 다른 링크를 보내달라고 말해 주세요</string> @@ -698,18 +678,17 @@ <string name="ntf_channel_messages">SimpleX Chat 메시지</string> <string name="observer_cant_send_message_desc">그룹 관리자에게 문의해 주세요.</string> <string name="observer_cant_send_message_title">메시지를 보낼 수 없습니다!</string> - <string name="ok">네</string> + <string name="ok">OK</string> <string name="reject_contact_button">거절</string> <string name="password_to_show">비밀번호 표시</string> <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[사용자 디바이스에만 <b>2계층 종단 간 암호화</b> 로 전송된 사용자 프로필, 연락처, 그룹 및 메시지를 저장되어요.]]></string> - <string name="read_more_in_github">자세한 내용은 GitHub에서 확인해 주세요.</string> - <string name="privacy_and_security">개인 정보 및 보안</string> + <string name="privacy_and_security">개인 정보 보호 및 보안</string> <string name="notifications_will_be_hidden">알림은 앱이 중지되기 전까지만 전달될 거예요!</string> <string name="only_you_can_send_disappearing">당신만 사라지는 메시지를 보낼 수 있습니다.</string> <string name="prohibit_sending_disappearing_messages">사라지는 메시지 전송은 허용되지 않습니다.</string> <string name="prohibit_sending_voice_messages">음성 메시지 허용되지 않음.</string> <string name="prohibit_sending_disappearing">사라지는 메시지 전송은 허용되지 않습니다.</string> - <string name="old_database_archive">이전 데이터베이스 기록</string> + <string name="old_database_archive">이전 데이터베이스 보관함</string> <string name="rcv_group_event_member_added">%1$s 초대됨</string> <string name="rcv_group_event_member_left">나감</string> <string name="rcv_group_event_user_deleted">강퇴됨</string> @@ -717,20 +696,20 @@ <string name="only_group_owners_can_change_prefs">그룹 소유자만 그룹 설정을 변경할 수 있어요.</string> <string name="receiving_via">다음을 통해 수신</string> <string name="only_your_contact_can_send_disappearing">대화 상대만 사라지는 메시지를 보낼 수 있습니다.</string> - <string name="prohibit_direct_messages">멤버들 간의 1:1 채팅이 허용되지 않음.</string> + <string name="prohibit_direct_messages">멤버들 간의 다이렉트 메시지가 허용되지 않음.</string> <string name="only_you_can_send_voice">나만 음성 메시지를 보낼 수 있어요.</string> <string name="only_your_contact_can_send_voice">대화 상대만 음성 메시지를 보낼 수 있어요.</string> <string name="prohibit_message_deletion">메시지 영구 삭제 허용되지 않음.</string> <string name="prohibit_sending_voice">음성 메시지 허용되지 않음.</string> - <string name="only_your_contact_can_delete">상대만 메시지를 영구 삭제할 수 있어요(나는 \"삭제됨\"으로 표시만 할 수 있음).</string> + <string name="only_your_contact_can_delete">상대만 메시지를 영구 삭제할 수 있어요(나는 "삭제됨"으로 표시만 할 수 있음).</string> <string name="only_group_owners_can_enable_voice">그룹 소유자만 음성 메시지를 사용 가능하도록 설정할 수 있어요.</string> <string name="one_time_link">일회성 초대 링크</string> <string name="paste_button">붙여넣기</string> <string name="profile_is_only_shared_with_your_contacts">프로필은 대화 상대들하고만 공유됩니다.</string> <string name="privacy_redefined">프라이버시의 재정의</string> - <string name="opensource_protocol_and_code_anybody_can_run_servers">오픈 소스 프로토콜과 코드 - 누구나 자신만의 서버를 구축할 수 있어요.</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">누구나 서버를 호스팅할 수 있습니다.</string> <string name="onboarding_notifications_mode_off">앱이 실행 중일 때</string> - <string name="read_more_in_github_with_link"><![CDATA[ㅍ자세한 내용은 \u0020<font color="#0088ff">GitHub</font> 에서 확인해 주세요.]]></string> + <string name="read_more_in_github_with_link"><![CDATA[자세한 내용은 <font color="#0088ff">GitHub</font> 에서 확인해 주세요.]]></string> <string name="relay_server_protects_ip">릴레이 서버는 IP 주소를 숨겨주지만, 통화 시간을 관찰 할 수 있어요.</string> <string name="rcv_group_event_invited_via_your_group_link">그룹 링크로 초대</string> <string name="onboarding_notifications_mode_subtitle">설정을 통해 나중에 변경할 수 있어요.</string> @@ -740,7 +719,7 @@ <string name="open_verb">열기</string> <string name="protect_app_screen">앱 잠금</string> <string name="personal_welcome">%1$s님, 환영합니다!</string> - <string name="only_stored_on_members_devices">(그룹 구성원에게만 저장됨)</string> + <string name="only_stored_on_members_devices">(그룹 멤버에게만 저장됨)</string> <string name="rate_the_app">앱 평가하기</string> <string name="onboarding_notifications_mode_periodic">주기적</string> <string name="onboarding_notifications_mode_service">즉시</string> @@ -760,13 +739,11 @@ <string name="save_and_notify_group_members">저장하고 그룹 멤버들에게 알리기</string> <string name="save_and_notify_contact">저장하고 대화 상대에게 알리기</string> <string name="remove_passphrase">지우기</string> - <string name="save_archive">아카이브 저장하기</string> - <string name="save_color">색상 저장하기</string> - <string name="save_passphrase_in_keychain">암호 저장소에 비밀번호 저장하기</string> + <string name="save_passphrase_in_keychain">암호 저장소에 암호 저장하기</string> <string name="restore_database">데이터베이스 백업 복원하기</string> - <string name="restore_database_alert_desc">데이터베이스 백업을 복원한 후 이전 비밀번호를 입력해 주세요. 이 작업은 되돌릴 수 없어요.</string> + <string name="restore_database_alert_desc">데이터베이스 백업을 복원한 후 이전 비밀번호를 입력해 주십시오. 이 결정은 되돌릴 수 없습니다.</string> <string name="restore_database_alert_title">데이터베이스 백업을 복원할까요\?</string> - <string name="restore_passphrase_not_found_desc">키스토어에서 암호를 찾을 수 없어요. 직접 입력해 주세요. 백업 도구를 사용하여 복원했을 때 이 문제가 발생할 수 있는데, 그런 경우가 아니라면 개발자에게 알려주세요.</string> + <string name="restore_passphrase_not_found_desc">암호 저장소에서 암호를 찾을 수 없어요. 직접 입력해 주세요. 백업 도구를 사용하여 복원했을 때 이 문제가 발생할 수 있는데, 그런 경우가 아니라면 개발자에게 알려주세요.</string> <string name="save_and_update_group_profile">저장하고 그룹 프로필 업데이트하기</string> <string name="save_welcome_message_question">환영 메시지를 저장할까요\?</string> <string name="save_group_profile">그룹 프로필 저장하기</string> @@ -775,13 +752,13 @@ <string name="scan_code">코드 스캔하기</string> <string name="scan_code_from_contacts_app">대화 상대의 앱에서 보안 코드를 스캔해 주세요.</string> <string name="saved_ICE_servers_will_be_removed">저장된 WebRTC ICE 서버가 제거될 거예요.</string> - <string name="save_passphrase_and_open_chat">비밀번호 저장하고 채팅 열기</string> + <string name="save_passphrase_and_open_chat">암호를 저장하고 채팅 열기</string> <string name="role_in_group">역할</string> <string name="save_preferences_question">설정을 저장할까요\?</string> <string name="save_profile_password">프로필 비밀번호 저장하기</string> <string name="restart_the_app_to_use_imported_chat_database">가져온 채팅 데이터베이스를 사용하려면 앱을 다시 실행해 주세요.</string> <string name="restart_the_app_to_create_a_new_chat_profile">새 프로필을 만드려면 앱을 다시 실행해 주세요.</string> - <string name="remove_passphrase_from_keychain">암호 저장소에서 비밀번호를 삭제할까요\?</string> + <string name="remove_passphrase_from_keychain">암호 저장소에서 암호를 삭제할까요?</string> <string name="run_chat_section">채팅 기능 실행하기</string> <string name="restore_database_alert_confirm">복원하기</string> <string name="sender_cancelled_file_transfer">대화 상대가 파일 전송을 취소했어요.</string> @@ -814,7 +791,7 @@ <string name="share_file">파일 공유…</string> <string name="share_image">이미지 공유…</string> <string name="share_message">메시지 공유…</string> - <string name="send_live_message_desc">라이브 메시지 보내기 - 입력 과정을 실시간으로 상대에게 보여줘요.</string> + <string name="send_live_message_desc">라이브 메시지 보내기 - 입력 과정을 실시간으로 상대에게 보여줍니다.</string> <string name="send_verb">보내기</string> <string name="share_invitation_link">초대 링크 공유</string> <string name="security_code">보안 코드</string> @@ -829,12 +806,10 @@ <string name="stop_chat_confirmation">멈추기</string> <string name="snd_group_event_changed_member_role">%s의 역할을 %s로 변경했어요.</string> <string name="section_title_for_console">콘솔용</string> - <string name="should_be_at_least_one_visible_profile">적어도 하나의 숨겨지지 않은 사용자 프로필이 있어야 해요.</string> <string name="set_group_preferences">그룹 설정 지정하기</string> <string name="snd_conn_event_switch_queue_phase_completed_for_member">%s의 주소를 바꿨어요</string> <string name="snd_conn_event_switch_queue_phase_completed">주소를 바꿨어요</string> <string name="snd_group_event_group_profile_updated">그룹 프로필 업데이트됨</string> - <string name="should_be_at_least_one_profile">적어도 하나의 사용자 프로필이 있어야 해요.</string> <string name="smp_servers_test_server">서버 테스트하기</string> <string name="smp_servers_use_server">서버 사용하기</string> <string name="smp_servers_use_server_for_new_conn">새로운 대화에 사용</string> @@ -865,18 +840,17 @@ <string name="settings_section_title_messages">메시지 및 파일</string> <string name="settings_section_title_incognito">익명 모드</string> <string name="settings_section_title_experimenta">실험적</string> - <string name="set_password_to_export">내보낼 비밀번호 설정</string> + <string name="set_password_to_export">내보낼 암호 설정</string> <string name="smp_servers">SMP 서버</string> <string name="smp_servers_preset_address">미리 설정된 서버 주소</string> <string name="smp_servers_your_server">내 서버</string> <string name="smp_servers_your_server_address">내 서버 주소</string> <string name="snd_group_event_member_deleted">%1$s을(를) 강퇴했어요.</string> <string name="stop_chat_to_export_import_or_delete_chat_database">채팅 데이터베이스를 내보내기, 가져오기 또는 삭제 하려면 채팅 기능을 중지해 주세요. 채팅 기능이 중지된 동안에는 메시지를 주고받을 수 없어요.</string> - <string name="store_passphrase_securely">비밀번호를 모르면 변경하거나 찾을 수 없으므로 비밀번호를 안전하게 보관해 주세요.</string> + <string name="store_passphrase_securely">암호를 모르면 변경하거나 찾을 수 없으므로 암호를 안전하게 보관해 주세요.</string> <string name="submit_passcode">제출하기</string> - <string name="store_passphrase_securely_without_recover">비밀번호를 모르면 채팅에 액세스할 수 없으니 비밀번호를 안전하게 보관해 주세요.</string> + <string name="store_passphrase_securely_without_recover">암호를 모르면 채팅에 액세스할 수 없으니 암호를 안전하게 보관해 주세요.</string> <string name="stop_chat_question">채팅 기능을 중지할까요\?</string> - <string name="stop_chat_to_enable_database_actions">데이터베이스 작업을 할 수 있도록 채팅 기능을 중지하기</string> <string name="switch_receiving_address">수신 주소 바꾸기</string> <string name="decryption_error">복호화 오류</string> <string name="confirm_passcode">패스코드 확인</string> @@ -895,7 +869,7 @@ <string name="custom_time_unit_days">일</string> <string name="custom_time_picker_custom">사용자화</string> <string name="send_disappearing_message_send">전송</string> - <string name="change_self_destruct_passcode">자폭 패스코드 변경</string> + <string name="change_self_destruct_passcode">자체 소멸 패스코드 변경</string> <string name="all_app_data_will_be_cleared">모든 앱 데이터가 삭제되었습니다.</string> <string name="la_authenticate">인증</string> <string name="la_change_app_passcode">패스코드 변경</string> @@ -908,12 +882,12 @@ <string name="create_address_and_let_people_connect">다른 사용자와 연결할 수 있도록 주소를 만듭니다.</string> <string name="customize_theme_title">커스텀 테마</string> <string name="auto_accept_contact">자동 수락</string> - <string name="change_self_destruct_mode">자폭 모드 변경</string> + <string name="change_self_destruct_mode">자체 소멸 모드 변경</string> <string name="item_info_current">(현재)</string> <string name="dark_theme">다크 테마</string> <string name="allow_message_reactions">메시지 반응을 허용합니다.</string> <string name="both_you_and_your_contact_can_add_message_reactions">당신과 대화 상대 모두 메시지 반응을 추가할 수 있습니다.</string> - <string name="allow_your_contacts_to_call">대화 상대가 당신에게 전화할 수 있도록 허용합니다.</string> + <string name="allow_your_contacts_to_call">대화 상대가 당신에게 통화할 수 있도록 허용합니다.</string> <string name="la_current_app_passcode">현재 패스코드</string> <string name="learn_more_about_address">SimpleX 주소에 대하여</string> <string name="authentication_cancelled">인증 취소됨</string> @@ -928,12 +902,12 @@ <string name="allow_message_reactions_only_if">대화 상대가 허용하는 경우에만 메시지 반응을 허용합니다.</string> <string name="all_your_contacts_will_remain_connected_update_sent">모든 대화가 연결된 상태로 유지됩니다. 프로필 업데이트가 대화 상대에게 전송됩니다.</string> <string name="v4_4_disappearing_messages_desc">전송된 메시지는 설정된 시간이 지나면 삭제됩니다.</string> - <string name="app_passcode_replaced_with_self_destruct">앱 패스코드가 자체소멸 패스코드로 대체되었습니다.</string> + <string name="app_passcode_replaced_with_self_destruct">앱 패스코드가 자체 소멸 패스코드로 대체되었습니다.</string> <string name="calls_prohibited_with_this_contact">음성/영상 통화가 허가되지 않았습니다.</string> <string name="alert_title_msg_bad_hash">잘못된 메시지 해쉬</string> <string name="la_auth_failed">인증 실패</string> <string name="use_camera_button">카메라</string> - <string name="allow_your_contacts_adding_message_reactions">대화 상대가 메시지 응답을 추가할 수 있도록 허용합니다.</string> + <string name="allow_your_contacts_adding_message_reactions">대화 상대가 메시지 반응을 추가할 수 있도록 허용합니다.</string> <string name="alert_title_msg_bad_id">잘못된 메시지 아이디</string> <string name="both_you_and_your_contact_can_make_calls">당신과 대화 상대 모두 전화를 걸 수 있습니다.</string> <string name="v5_0_app_passcode">앱 패스코드</string> @@ -945,4 +919,607 @@ <string name="waiting_for_image">이미지 기다리는 중</string> <string name="voice_message">음성 메시지</string> <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d개의 메시지의 해독에 실패했습니다.</string> -</resources> \ No newline at end of file + <string name="color_primary_variant2">추가 강조 색상 2</string> + <string name="v5_2_fix_encryption_descr">백업 복원 후 암호화 수정.</string> + <string name="v5_2_favourites_filter">채팅 더 빠르게 찾기</string> + <string name="v4_2_group_links">그룹 링크</string> + <string name="v5_2_favourites_filter_descr">읽지 않은 채팅과 즐겨찾기 채팅 필터링.</string> + <string name="wallpaper_advanced_settings">고급 설정</string> + <string name="v5_2_fix_encryption">연결 유지하기</string> + <string name="feature_roles_owners">소유자</string> + <string name="moderated_items_description">%1$d개의 메시지가 %2$s에 의해 삭제되었습니다</string> + <string name="v5_3_new_interface_languages">6개의 새로운 인터페이스 언어</string> + <string name="add_contact_tab">연락처 추가</string> + <string name="feature_roles_admins">관리자</string> + <string name="v5_6_safer_groups_descr">관리자는 모든 멤버를 위해 특정 멤버를 차단할 수 있습니다.</string> + <string name="v5_2_message_delivery_receipts">메시지 전송 확인서!</string> + <string name="v5_2_message_delivery_receipts_descr">우리가 놓친 두 번째 체크! ✅</string> + <string name="abort_switch_receiving_address">주소 변경 중지</string> + <string name="v5_1_better_messages_descr">- 최대 5분의 음성 메시지. +\n- 사용자 정의 소멸 시간. +\n- 편집 기록.</string> + <string name="whats_new_thanks_to_users_contribute_weblate">사용자 여러분께 감사드립니다 – Weblate를 통해 기여하세요!</string> + <string name="abort_switch_receiving_address_desc">주소 변경이 중지됩니다. 이전 수신 주소가 사용됩니다.</string> + <string name="v5_1_japanese_portuguese_interface">일본어 및 포르투갈어 UI</string> + <string name="abort_switch_receiving_address_question">주소 변경을 중지하시겠습니까?</string> + <string name="feature_roles_all_members">모든 멤버</string> + <string name="feature_enabled_for">위해 활성화됨</string> + <string name="whats_new">새로운 기능</string> + <string name="whats_new_read_more">더 보기</string> + <string name="v4_2_security_assessment">보안 평가</string> + <string name="v4_2_security_assessment_desc">SimpleX Chat 보안은 Trail of Bits에 의해 감사되었습니다.</string> + <string name="connect_plan_already_connecting">이미 연결 중입니다!</string> + <string name="n_other_file_errors">%1$d개 기타 파일 오류.</string> + <string name="forward_files_not_accepted_desc">%1$d개 파일이 다운로드되지 않았습니다.</string> + <string name="forward_files_failed_to_receive_desc">%1$d개의 파일을 다운로드하지 못했습니다.</string> + <string name="conn_event_ratchet_sync_started">암호화 동의 중…</string> + <string name="v5_2_more_things">몇 가지 더</string> + <string name="turn_off_battery_optimization_button">허용</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s개의 메시지가 전송되지 않았습니다.</string> + <string name="forward_files_in_progress_desc">%1$d개의 파일이 아직 다운로드 중입니다.</string> + <string name="forward_files_missing_desc">%1$d개 파일이 삭제되었습니다.</string> + <string name="acknowledged">확인됨</string> + <string name="acknowledgement_errors">승인 오류</string> + <string name="allow_to_send_simplex_links">SimpleX 링크 전송을 허용합니다.</string> + <string name="v5_6_app_data_migration">앱 데이터 이전</string> + <string name="migrate_from_device_all_data_will_be_uploaded">모든 연락처, 대화 및 파일은 안전하게 암호화되어 구성된 XFTP 릴레이에 일괄 업로드됩니다.</string> + <string name="servers_info_subscriptions_connections_subscribed">활성 연결</string> + <string name="all_users">모든 프로필</string> + <string name="migrate_to_device_apply_onion">적용</string> + <string name="clear_note_folder_warning">모든 메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다!</string> + <string name="allow_to_send_files">파일 및 미디어 전송을 허용합니다.</string> + <string name="connect__a_new_random_profile_will_be_shared">새로운 무작위 프로필이 공유됩니다.</string> + <string name="chat_theme_apply_to_all_modes">모든 색상 모드</string> + <string name="network_smp_proxy_mode_always">항상</string> + <string name="rcv_group_and_other_events">외 %d개 이벤트</string> + <string name="v5_3_encrypt_local_files_descr">앱이 새 로컬 파일 (비디오 제외)을 암호화합니다.</string> + <string name="connect_plan_already_joining_the_group">이미 그룹에 있습니다!</string> + <string name="network_smp_proxy_mode_always_description">항상 프라이빗 라우팅 사용.</string> + <string name="snd_conn_event_ratchet_sync_started">%s에 대한 암호화에 동의 중…</string> + <string name="block_member_desc">%s의 새 메시지는 모두 숨겨집니다!</string> + <string name="allow_calls_question">통화를 허용할까요?</string> + <string name="network_smp_proxy_fallback_allow_downgrade">다운그레이드 허용</string> + <string name="your_chats">채팅</string> + <string name="remote_ctrl_error_bad_state">데스크톱과의 연결이 잘못된 상태입니다.</string> + <string name="migrate_from_device_archive_and_upload">보관 및 업로드</string> + <string name="servers_info_sessions_connecting">연결 중</string> + <string name="n_file_errors">%1$d항목의 파일 오류:\n%2$s</string> + <string name="create_address_button">생성</string> + <string name="correct_name_to">올바른 이름을 %s 로 지정하시겠습니까?</string> + <string name="permissions_camera">카메라</string> + <string name="permissions_camera_and_record_audio">카메라와 마이크</string> + <string name="receipts_section_contacts">연락처</string> + <string name="profile_update_event_contact_name_changed">연락처 %1$s가 %2$s (으)로 변경됨</string> + <string name="connect_via_member_address_alert_title">직접 연결하시겠습니까?</string> + <string name="chat_theme_apply_to_mode">적용 대상</string> + <string name="v5_4_incognito_groups_descr">무작위 프로필을 사용하여 그룹을 만듭니다.</string> + <string name="bad_desktop_address">잘못된 데스크톱 주소</string> + <string name="desktop_connection_terminated">연결이 종료됨</string> + <string name="servers_info_sessions_connected">연결됨</string> + <string name="connections">연결</string> + <string name="call_service_notification_audio_call">음성 통화</string> + <string name="in_developing_title">곧 출시 예정입니다!</string> + <string name="settings_section_title_chat_database">채팅 데이터베이스</string> + <string name="settings_section_title_chat_theme">채팅 테마</string> + <string name="v6_0_connect_faster_descr">친구들과 더 빠르게 연결하세요.</string> + <string name="remote_ctrl_was_disconnected_title">연결이 중지됨</string> + <string name="connected_to_desktop">데스크톱에 연결됨</string> + <string name="connecting_to_desktop">데스크톱에 연결 중</string> + <string name="connect_plan_connect_to_yourself">자신과 연결하겠습니까?</string> + <string name="migrate_to_device_chat_migrated">채팅이 이전되었습니다!</string> + <string name="v5_3_discover_join_groups_descr">- 디렉터리 서비스(베타)에 연결하세요!\n- 전송 알림(최대 20명).\n- 더 빠르고 안정적입니다.</string> + <string name="migrate_from_device_cancel_migration">이전 취소</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">이전하려는 데이터베이스의 암호를 기억하고 있는지 확인합니다.</string> + <string name="connect_via_link_incognito">익명 모드로 연결</string> + <string name="app_check_for_updates_beta">베타</string> + <string name="la_app_passcode">앱 패스코드</string> + <string name="chat_database_exported_continue">계속</string> + <string name="chat_is_stopped_you_should_transfer_database">채팅이 중지되었습니다. 다른 기기에서 이 데이터베이스를 이미 사용하고 있다면, 채팅을 시작하기 전에 다시 전송해야 합니다.</string> + <string name="rcv_group_event_member_created_contact">직접 연결됨</string> + <string name="chunks_downloaded">일괄 다운로드됨</string> + <string name="connect_with_contact_name_question">%1$s과(와) 연결하시겠습니까?</string> + <string name="theme_destination_app_theme">앱 테마</string> + <string name="app_check_for_updates_download_completed_title">앱 업데이트가 다운로드되었음</string> + <string name="v5_3_new_interface_languages_descr">아랍어, 불가리아어, 핀란드어, 히브리어, 태국어 및 우크라이나어 - 사용자와 Weblate 덕분입니다.</string> + <string name="v5_4_better_groups">향상된 그룹 기능</string> + <string name="migrate_from_device_archiving_database">데이터베이스를 저장 중</string> + <string name="group_member_role_author">저자</string> + <string name="snd_error_quota">용량 초과 - 수신자가 이전에 보낸 메시지를 받지 못했습니다.</string> + <string name="network_type_cellular">셀룰러</string> + <string name="settings_section_title_chat_colors">채팅 색상</string> + <string name="app_check_for_updates_notice_title">업데이트 확인</string> + <string name="migrate_from_device_check_connection_and_try_again">인터넷 연결을 확인하고 다시 시도하십시오</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[새 기기에서 <i>다른 기기에서 이전</i>을 선택하고 QR 코드를 스캔합니다.]]></string> + <string name="completed">완료</string> + <string name="v5_8_safe_files_descr">알 수 없는 서버의 파일을 확인합니다.</string> + <string name="servers_info_connected_servers_section_header">연결된 서버</string> + <string name="v6_0_connection_servers_status_descr">연결 및 서버 상태.</string> + <string name="remote_host_error_bad_state"><![CDATA[모바일 <b>%s</b> 과의 연결이 잘못된 상태입니다.]]></string> + <string name="v6_0_connection_servers_status">네트워크 관리</string> + <string name="switch_receiving_address_question">수신 주소를 변경하겠습니까?</string> + <string name="connect_to_desktop">데스크톱에 연결</string> + <string name="cannot_share_message_alert_title">메시지를 보낼 수 없음</string> + <string name="confirm_delete_contact_question">연락처 삭제를 확인하시겠습니까?</string> + <string name="delete_contact_cannot_undo_warning">연락처가 삭제됩니다. 이 결정은 되돌릴 수 없습니다!</string> + <string name="conversation_deleted">대화가 삭제되었습니다!</string> + <string name="contact_deleted">연락처가 삭제되었어요!</string> + <string name="choose_file_title">파일 선택</string> + <string name="camera_not_available">카메라를 사용할 수 없음</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>그룹 생성</b>: 새로운 그룹을 생성합니다.]]></string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>연락처 추가</b> : 새 초대 링크를 만들거나 받은 링크를 통해 연결합니다.]]></string> + <string name="clear_note_folder_question">개인 메모를 지우시겠습니까?</string> + <string name="xftp_servers_configured">XFTP 서버 구성</string> + <string name="v6_0_your_contacts_descr">나중에 채팅할 수 있도록 연락처를 보관합니다.</string> + <string name="connected_desktop">연결된 데스크톱</string> + <string name="chunks_uploaded">일괄 업로드됨</string> + <string name="deleted_chats">보관된 연락처</string> + <string name="smp_servers_configured">SMP 서버 구성</string> + <string name="create_chat_profile">채팅 프로필 생성</string> + <string name="cant_call_member_alert_title">그룹 멤버에 전화를 걸 수 없음</string> + <string name="multicast_connect_automatically">자동 연결</string> + <string name="remote_host_was_disconnected_title">연결이 중지됨</string> + <string name="attempts_label">시도</string> + <string name="network_session_mode_session">앱 세션</string> + <string name="app_check_for_updates">업데이트 확인</string> + <string name="call_desktop_permission_denied_chrome">주소 필드 근처에 있는 정보 버튼을 클릭하여 마이크를 사용할 수 있습니다.</string> + <string name="settings_message_shape_corner">모서리</string> + <string name="chat_database_exported_title">채팅 데이터베이스를 내보냈습니다</string> + <string name="cant_call_contact_alert_title">연락처에 전화를 걸 수 없음</string> + <string name="cant_call_contact_connecting_wait_alert_text">연락처에 연결하니, 잠시 기다리거나 나중에 확인하십시오!</string> + <string name="cant_call_contact_deleted_alert_text">연락처가 삭제됩니다.</string> + <string name="color_mode">색상 모드</string> + <string name="copy_error">복사 오류</string> + <string name="v6_1_better_calls">향상된 통화 기능</string> + <string name="v6_1_message_dates_descr">향상된 메시지 날짜 기능</string> + <string name="v6_1_better_security">향상된 보안 ✅</string> + <string name="connect_plan_connect_via_link">링크로 연결하시겠습니까?</string> + <string name="migrate_to_device_confirm_network_settings">네트워크 설정 확인</string> + <string name="chunks_deleted">일괄 삭제됨</string> + <string name="migrate_from_device_confirm_upload">업로드 확인</string> + <string name="connected_to_mobile">모바일에 연결</string> + <string name="connected_mobile">연결된 모바일</string> + <string name="info_view_connect_button">연결</string> + <string name="cant_send_message_to_member_alert_title">그룹 멤버에게 메시지를 보낼 수 없음</string> + <string name="receipts_contacts_title_enable">전송 알림을 활성화하시겠습니까?</string> + <string name="conn_event_ratchet_sync_ok">암호화 OK</string> + <string name="desktop_device">데스크톱</string> + <string name="possible_slow_function_desc">기능 처리 시간이: %1$d 초 이상: %2$s</string> + <string name="dont_create_address">주소를 만들지 않음</string> + <string name="devices">기기</string> + <string name="files_and_media_prohibited">파일 및 미디어 금지됨!</string> + <string name="creating_link">링크 생성 중…</string> + <string name="smp_proxy_error_connecting">전달 서버 %1$s에 연결하는 동안 오류가 발생했습니다. 나중에 다시 시도하십시오.</string> + <string name="la_seconds">%d 초</string> + <string name="receipts_groups_disable_for_all">모든 그룹에 사용 안 함</string> + <string name="receipts_groups_disable_keep_overrides">사용 안 함 (그룹 변경 사항 유지)</string> + <string name="v5_3_new_desktop_app_descr">데스크톱 앱에서 새 프로필 생성합니다. 💻</string> + <string name="error_alert_title">오류</string> + <string name="error_aborting_address_change">주소 변경을 중단하는 중 오류</string> + <string name="error_forwarding_messages">메시지를 전송하는 동안 오류</string> + <string name="delete_members_messages__question">멤버의 %d 메시지를 삭제하시겠습니까?</string> + <string name="file_error_no_file">파일을 찾을 수 없음 - 파일이 삭제되었거나 취소되었을 가능성이 큽니다.</string> + <string name="info_view_call_button">통화</string> + <string name="forward_files_not_accepted_receive_files">다운로드</string> + <string name="network_proxy_auth_mode_no_auth">프록시에서 자격 증명을 사용하지 마십시오.</string> + <string name="network_proxy_incorrect_config_title">프록시를 저장하는 중 오류</string> + <string name="audio_device_earpiece">이어폰</string> + <string name="audio_device_bluetooth">블루투스</string> + <string name="alert_text_encryption_renegotiation_failed">암호화 재협상 실패</string> + <string name="enable_self_destruct">자체 소멸 활성화</string> + <string name="receipts_contacts_enable_keep_overrides">활성화 (변경 사항 유지)</string> + <string name="receipts_contacts_title_disable">전송 알림을 비활성화하시겠습니까?</string> + <string name="receipts_groups_title_disable">그룹 전송 알림을 비활성화하시겠습니까?</string> + <string name="receipts_contacts_disable_for_all">모두 사용 안 함</string> + <string name="receipts_contacts_enable_for_all">모두 활성화</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">데이터베이스가 암호화되고 암호가 설정에 저장됩니다.</string> + <string name="rcv_group_event_member_blocked">%s 차단됨</string> + <string name="snd_conn_event_ratchet_sync_required">%s 의 암호화 재협상 필요</string> + <string name="snd_conn_event_ratchet_sync_allowed">%s 의 암호화 재협상 허용</string> + <string name="error_blocking_member_for_all">멤버 블록 오류</string> + <string name="v5_3_discover_join_groups">그룹 검색 및 참여하기</string> + <string name="v5_4_better_groups_descr">더 빠른 참여와 더 안정적인 메시지.</string> + <string name="delivery_receipts_title">전송 알림!</string> + <string name="enable_receipts_all">활성화</string> + <string name="disconnect_desktop_question">데스크톱 연결을 끊으시겠습니까?</string> + <string name="remote_ctrl_error_bad_invitation">데스크톱에 잘못된 초대 코드가 잘못되었습니다</string> + <string name="remote_ctrl_error_disconnected">데스크톱 연결이 끊어졌습니다</string> + <string name="migrate_to_device_download_failed">다운로드 실패</string> + <string name="migrate_from_device_error_exporting_archive">채팅 데이터베이스를 내보내는 동안 오류</string> + <string name="servers_info_files_tab">파일</string> + <string name="servers_info_sessions_errors">오류</string> + <string name="servers_info_downloaded">다운로드됨</string> + <string name="expired_label">만료</string> + <string name="deleted">삭제됨</string> + <string name="deletion_errors">삭제 오류</string> + <string name="disable_sending_recent_history">새 멤버에게 내역을 보내지 마십시오.</string> + <string name="delete_and_notify_contact">삭제 및 연락처에 알림</string> + <string name="blocked_item_description">차단됨</string> + <string name="blocked_by_admin_item_description">관리자에 의해 차단됨</string> + <string name="blocked_items_description">%d 개의 메시지가 차단됨</string> + <string name="blocked_by_admin_items_description">%d 개의 메시지가 관리자에 의해 차단됨</string> + <string name="proxy_destination_error_broker_host">%1$s의 대상 서버 주소가 전달 서버 %2$s의 설정과 호환되지 않습니다.</string> + <string name="proxy_destination_error_broker_version">대상 서버 버전 %1$s이 전달 서버 %2$s와 호환되지 않습니다.</string> + <string name="expand_verb">확장</string> + <string name="enable_camera_access">카메라 권한 활성화</string> + <string name="enable_lock">잠금 활성화</string> + <string name="encrypt_local_files">로컬 파일 암호화</string> + <string name="conn_event_ratchet_sync_required">암호화 재협상 필요</string> + <string name="error_creating_member_contact">멤버 연락처를 만드는 동안 오류</string> + <string name="error_sending_message_contact_invitation">초대장을 보내는 중 오류</string> + <string name="send_receipts_disabled">비활성화됨</string> + <string name="block_for_all">모두 차단</string> + <string name="block_for_all_question">이 멤버를 차단하시겠습니까?</string> + <string name="v5_3_encrypt_local_files">저장된 파일 & 미디어 암호화</string> + <string name="v6_0_delete_many_messages_descr">한 번에 최대 20개의 메시지를 삭제할 수 있습니다.</string> + <string name="v6_0_privacy_blur">더 나은 개인정보 보호를 위한 흐리기</string> + <string name="error">오류</string> + <string name="remote_host_disconnected_from"><![CDATA[이유: %s로 인해 모바일 <b>%s</b> 에서 연결이 끊어졌습니다]]></string> + <string name="desktop_devices">데스크톱 기기</string> + <string name="remote_ctrl_error_bad_version">데스크톱 버전이 지원되지 않습니다. 두 기기가 동일한 버전에 있는지 확인하십시오.</string> + <string name="migrate_to_device_enter_passphrase">암호 입력</string> + <string name="migrate_to_device_error_downloading_archive">보관함을 다운로드하는 동안 오류</string> + <string name="migrate_from_device_error_uploading_archive">보관함을 업로드하는 중 오류</string> + <string name="migrate_from_device_error_saving_settings">설정을 저장하는 중 오류</string> + <string name="migrate_from_device_exported_file_doesnt_exist">내보낸 파일이 없음</string> + <string name="migrate_from_device_delete_database_from_device">이 기기에서 데이터베이스 삭제</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>경고</b>: 보관된 데이터가 삭제됩니다.]]></string> + <string name="servers_info_details">상세</string> + <string name="servers_info_reconnect_servers_error">서버를 다시 연결하는 중 오류</string> + <string name="download_errors">다운로드 오류</string> + <string name="dont_enable_receipts">사용 안 함</string> + <string name="error_enabling_delivery_receipts">전송 알림을 활성화하는 동안 오류!</string> + <string name="remote_ctrl_error_inactive">데스크톱이 비활성 상태입니다</string> + <string name="migrate_to_device_downloading_archive">보관함 다운로드 중</string> + <string name="migrate_from_device_creating_archive_link">보관 링크를 생성 중</string> + <string name="file_error">파일 오류</string> + <string name="create_another_profile_button">프로필 생성</string> + <string name="files_and_media">파일 및 미디어</string> + <string name="multicast_discoverable_via_local_network">로컬 네트워크를 통해 탐색 가능</string> + <string name="encryption_renegotiation_error">암호화 재협상 오류</string> + <string name="delete_messages__question">%d 메시지를 삭제하시겠습니까?</string> + <string name="download_file">다운로드</string> + <string name="v6_0_upgrade_app_descr">GitHub에서 새 버전을 다운로드합니다.</string> + <string name="desktop_address">데스크톱 주소</string> + <string name="la_mode_passcode">패스코드</string> + <string name="receipts_groups_enable_for_all">모든 그룹에 활성화</string> + <string name="receipts_groups_enable_keep_overrides">활성화 (그룹 변경 사항 유지)</string> + <string name="settings_section_title_files">파일</string> + <string name="rcv_direct_event_contact_deleted">삭제된 연락처</string> + <string name="snd_conn_event_ratchet_sync_agreed">%s 의암호화에 동의함</string> + <string name="snd_conn_event_ratchet_sync_ok">%s 의 암호화 OK</string> + <string name="v5_0_large_files_support_descr">상대가 온라인 상태가 될 때까지 기다릴 필요가 없습니다!</string> + <string name="v5_4_block_group_members">그룹 멤버 차단</string> + <string name="current_user">현재 프로필</string> + <string name="error_loading_smp_servers">SMP 서버를 로드하는 중 오류</string> + <string name="error_deleting_note_folder">개인 메모를 삭제하는 동안 오류</string> + <string name="migrate_from_device_error_deleting_database">데이터베이스를 삭제하는 동안 오류</string> + <string name="remote_ctrl_disconnected_with_reason">다음과 같은 이유로 끊어졌습니다: %s</string> + <string name="call_service_notification_end_call">통화 종료</string> + <string name="error_showing_desktop_notification">알림을 표시하는 동안 오류가 발생하였으니, 개발자에게 문의하십시오.</string> + <string name="la_minutes">%d 분</string> + <string name="marked_deleted_items_description">%d 개의 메시지가 삭제됨</string> + <string name="error_saving_xftp_servers">XFTP 서버를 저장하는 중 오류</string> + <string name="disable_notifications_button">알림 비활성화</string> + <string name="la_enter_app_passcode">패스코드 입력</string> + <string name="error_setting_address">주소를 설정하는 중 오류</string> + <string name="favorite_chat">좋아함</string> + <string name="error_synchronizing_connection">연결을 동기화하는 중 오류</string> + <string name="error_showing_content">콘텐츠를 표시하는 중 오류</string> + <string name="error_showing_message">메시지를 표시하는 중 오류</string> + <string name="privacy_media_blur_radius">미디어 흐리기</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>참고</b>: 두 기기에서 동일한 데이터베이스를 사용하면 연결된 사람들의 메시지 복호화가 깨질 수 있으며, 이는 보안 보호 조치입니다.]]></string> + <string name="color_mode_dark">다크</string> + <string name="database_migration_in_progress">데이터베이스 이전이 진행 중입니다.\n이 작업은 몇 분 정도 걸릴 수 있습니다.</string> + <string name="snd_error_relay">대상 서버 오류: %1$s</string> + <string name="delivery">전송</string> + <string name="servers_info_detailed_statistics">상세 통계</string> + <string name="developer_options_section">개발자 옵션</string> + <string name="network_smp_proxy_fallback_prohibit_description">사용자 또는 대상의 서버가 프라이빗 라우팅을 지원하지 않는 경우에도 메시지를 직접 보내지 마십시오.</string> + <string name="delete_without_notification">알림 없이 삭제</string> + <string name="switching_profile_error_title">프로필을 전환하는 중 오류</string> + <string name="info_row_deleted_at">삭제 완료</string> + <string name="info_row_disappears_at">에 사라짐</string> + <string name="chat_theme_apply_to_dark_mode">다크 모드</string> + <string name="discover_on_network">로컬 네트워크를 통해 탐색</string> + <string name="member_info_member_blocked">차단됨</string> + <string name="v5_6_quantum_resistant_encryption_descr">다이렉트 채팅에서 활성화함 (베타)!</string> + <string name="self_destruct">자체 소멸</string> + <string name="enter_welcome_message_optional">환영 메시지 입력…(선택사항)</string> + <string name="error_initializing_web_view_wrong_arch">WebView를 초기화하는 중 오류가 발생했습니다. WebView가 설치되어 있고 지원되는 아키텍처가 arm64인지 확인합니다.\n오류: %s</string> + <string name="error_loading_xftp_servers">XFTP 서버를 로드하는 중 오류가 발생했습니다.</string> + <string name="error_loading_details">세부 정보를 로드하는 중 오류</string> + <string name="servers_info_reset_stats_alert_error_title">통계를 재설정하는 중 오류</string> + <string name="v5_2_disappear_one_message_descr">대화에서 비활성화 된 경우에도 마찬가지입니다.</string> + <string name="export_theme">테마 내보내기</string> + <string name="share_text_deleted_at">삭제 완료: %s</string> + <string name="share_text_created_at">보낸 날짜: %s</string> + <string name="member_blocked_by_admin">관리자에 의해 차단됨</string> + <string name="member_info_member_disabled">비활성화됨</string> + <string name="agent_critical_error_title">치명적 오류</string> + <string name="ci_status_other_error">오류: %1$s</string> + <string name="error_initializing_web_view">WebView를 초기화하는 중 오류가 발생했습니다. 시스템을 새 버전으로 업데이트하십시오. 개발자에게 문의하세요.\n오류: %s</string> + <string name="servers_info_modal_error_title">오류</string> + <string name="created">생성 완료</string> + <string name="conn_event_ratchet_sync_agreed">암호화에 동의함</string> + <string name="conn_event_ratchet_sync_allowed">암호화 재협상 허용</string> + <string name="share_text_disappears_at">%s :에 사라짐</string> + <string name="block_member_button">멤버 차단</string> + <string name="block_member_question">멤버를 차단하시겠습니까?</string> + <string name="calls_prohibited_alert_title">통화 금지!</string> + <string name="delivery_receipts_are_disabled">전송 알림이 더 이상 유효하지 않습니다!</string> + <string name="disconnect_remote_host">연결 끊기</string> + <string name="desktop_app_version_is_incompatible">데스크톱 앱 버전 %s은(는) 이 앱과 호환되지 않습니다.</string> + <string name="disconnect_remote_hosts">모바일 연결 끊기</string> + <string name="remote_ctrl_error_busy">데스크톱이 사용 중입니다</string> + <string name="servers_info_reconnect_server_error">서버를 다시 연결하는 중 오류</string> + <string name="error_creating_message">메시지를 만드는 동안 오류</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>참고</b>: 메시지 및 파일 릴레이는 SOCKS 프록시를 통해 연결됩니다. 통화 및 전송 링크 미리 보기는 직접 연결을 사용합니다.]]></string> + <string name="app_check_for_updates_disabled">비활성화됨</string> + <string name="network_smp_proxy_mode_never_description">프라이빗 라우팅 사용안함</string> + <string name="app_check_for_updates_download_started">앱 업데이트를 다운로드하는 중입니다. 앱을 닫지 마세요</string> + <string name="app_check_for_updates_button_download">%s (%s) 를 다운로드</string> + <string name="app_check_for_updates_notice_disable">비활성화</string> + <string name="unable_to_open_browser_title">브라우저를 여는 중 오류</string> + <string name="block_member_confirmation">차단</string> + <string name="theme_black">검은색</string> + <string name="receipts_contacts_disable_keep_overrides">사용 안 함 (변경 사항 유지)</string> + <string name="enabled_self_destruct_passcode">자체 소멸 패스코드 활성화</string> + <string name="database_encryption_will_be_updated_in_settings">데이터베이스 암호화 암호가 업데이트되고 설정에 저장됩니다.</string> + <string name="enter_welcome_message">환영 메시지 입력…</string> + <string name="create_group_button">그룹 생성</string> + <string name="dark_mode_colors">다크 모드 색상</string> + <string name="files_and_media_not_allowed">파일 및 미디어가 허용되지 않음</string> + <string name="v6_1_better_user_experience">향상된 사용자 경험</string> + <string name="v6_1_customizable_message_descr">사용자 지정 가능한 메시지 모양.</string> + <string name="v6_1_delete_many_messages_descr">최대 200개의 메시지를 삭제하거나 관리할 수 있습니다.</string> + <string name="migrate_to_device_downloading_details">링크 세부 정보를 다운로드하는 중</string> + <string name="decryption_errors">암호 해독 오류</string> + <string name="downloaded_files">다운로드한 파일</string> + <string name="duplicates_label">중복</string> + <string name="migrate_from_device_error_verifying_passphrase">암호를 확인하는 중 오류:</string> + <string name="receipts_groups_title_enable">그룹 전송 알림을 활성화하시겠습니까?</string> + <string name="rcv_group_events_count">%d 그룹 이벤트</string> + <string name="info_row_created_at">보낸 날짜</string> + <string name="info_row_debug_delivery">전송 디버그</string> + <string name="files_are_prohibited_in_group">파일 및 미디어는 이 그룹에서 금지됩니다.</string> + <string name="enter_this_device_name">이 기기의 이름을 입력하십시오…</string> + <string name="found_desktop">데스크톱을 찾음</string> + <string name="snd_error_proxy_relay">전달 서버: %1$s\n대상 서버 오류: %2$s</string> + <string name="snd_error_proxy">전달 서버: %1$s\n오류: %2$s</string> + <string name="forward_alert_title_messages_to_forward">%1$s 메시지를 전송하시겠습니까?</string> + <string name="forward_alert_forward_messages_without_files">파일 없이 메시지를 전달하시겠습니까?</string> + <string name="compose_forward_messages_n">%1$s 메시지 전송 중</string> + <string name="forward_multiple">메시지 전송…</string> + <string name="permissions_find_in_settings_and_grant">Android 설정에서 이 권한을 찾아 수동으로 허용하십시오.</string> + <string name="appearance_font_size">글꼴 크기</string> + <string name="wallpaper_preview_hello_bob">안녕하세요!</string> + <string name="wallpaper_preview_hello_alice">안녕하세요!</string> + <string name="file_error_relay">파일 서버 오류: %1$s</string> + <string name="revoke_file__message">파일이 서버에서 삭제됩니다.</string> + <string name="proxy_destination_error_failed_to_connect">전달 서버 %1$s가 대상 서버 %2$s에 연결하지 못했습니다. 나중에 시도하십시오.</string> + <string name="smp_proxy_error_broker_version">전달 서버 버전이 네트워크 설정과 호환되지 않습니다: %1$s.</string> + <string name="smp_proxy_error_broker_host">전달 서버 주소가 네트워크 설정과 호환되지 않습니다: %1$s.</string> + <string name="forwarded_chat_item_info_tab">전송됨</string> + <string name="forwarded_from_chat_item_info_title">에서 전송됨</string> + <string name="share_text_file_status">파일 상태: %s</string> + <string name="wallpaper_scale_fit">맞춤</string> + <string name="v5_1_message_reactions_descr">드디어, 우리는 그것들을 얻었냈습니다! 🚀</string> + <string name="migrate_to_device_file_delete_or_link_invalid">파일이 삭제되었거나 링크가 유효하지 않음</string> + <string name="migrate_from_device_finalize_migration">이전 완료</string> + <string name="forward_chat_item">전송</string> + <string name="forwarded_description">전송됨</string> + <string name="forward_message">메시지 전송…</string> + <string name="fix_connection_not_supported_by_contact">연락처에서 지원하지 않는 수정</string> + <string name="wallpaper_scale_fill">채우기</string> + <string name="fix_connection_confirm">수정</string> + <string name="fix_connection">연결 수정</string> + <string name="fix_connection_question">연결을 수정하시겠습니까?</string> + <string name="fix_connection_not_supported_by_group_member">그룹 멤버에서 지원하지 않는 수정</string> + <string name="v5_7_forward">메시지 전송 및 저장</string> + <string name="v4_6_reduced_battery_usage">배터리 사용량을 더욱 줄임</string> + <string name="v6_1_forward_many_messages_descr">한 번에 최대 20개의 메시지를 전달할 수 있습니다.</string> + <string name="migrate_to_device_finalize_migration">다른 기기로 이전을 완료합니다.</string> + <string name="info_row_file_status">파일 상태</string> + <string name="v4_4_french_interface">프랑스어 인터페이스</string> + <string name="message_delivery_warning_title">메시지 전송 경고</string> + <string name="e2ee_info_no_pq"><![CDATA[메시지, 파일 및 통화는 <b>종단 간 암호화</b>로 보호되며, 완벽한 전방 비밀성, 부인 방지 및 침입 복구 기능이 포함되어 있습니다.]]></string> + <string name="e2ee_info_pq"><![CDATA[메시지, 파일 및 통화는 <b>양자 저항 종단 간 암호화</b>로 보호되며, 완벽한 전방 비밀성, 메시지 부인 방지 및 침입 복구 기능이 포함되어 있습니다.]]></string> + <string name="migrate_to_device_importing_archive">보관함을 가져오는 중</string> + <string name="forward_files_messages_deleted_after_selection_desc">메시지를 선택한 후 메시지가 삭제되었습니다.</string> + <string name="info_view_message_button">메시지</string> + <string name="permissions_required">권한 부여</string> + <string name="permissions_grant_in_settings">설정에서 부여</string> + <string name="permissions_grant">전화 권한 부여</string> + <string name="audio_device_wired_headphones">헤드폰</string> + <string name="privacy_message_draft">메시지 초안</string> + <string name="if_you_enter_self_destruct_code">앱을 열 때 자체 소멸 패스코드를 입력하는 경우:</string> + <string name="v4_5_message_draft">메시지 초안</string> + <string name="v5_5_message_delivery">향상된 메시지 전송</string> + <string name="v5_7_forward_descr">메시지 출처는 비공개로 유지됩니다.</string> + <string name="desktop_incompatible_version">호환되지 않는 버전</string> + <string name="migrate_to_device_import_failed">가져오기 실패</string> + <string name="servers_info_messages_received">수신된 메시지</string> + <string name="servers_info_messages_sent">보낸 메시지</string> + <string name="info_row_message_status">메시지 상태</string> + <string name="v5_4_incognito_groups">익명 그룹</string> + <string name="edit_history">내역</string> + <string name="info_menu">정보</string> + <string name="in_reply_to">답장 대상</string> + <string name="user_hide">숨기기</string> + <string name="import_theme">테마 가져오기</string> + <string name="message_reactions">메시지 반응</string> + <string name="message_reactions_prohibited_in_this_chat">이 채팅에서는 메시지 반응이 금지됩니다.</string> + <string name="v4_6_group_moderation">그룹 관리</string> + <string name="v5_5_new_interface_languages">헝가리어 및 튀르키예어 UI</string> + <string name="v5_8_message_delivery">향상된 메시지 전송</string> + <string name="custom_time_unit_hours">시간</string> + <string name="v6_0_increase_font_size">글꼴 크기 키우기</string> + <string name="v4_3_improved_privacy_and_security_desc">최근 앱 목록에서 앱 화면을 숨깁니다.</string> + <string name="new_desktop"><![CDATA[<i>(신규)</i>]]></string> + <string name="host_verb">호스트</string> + <string name="la_immediately">즉시</string> + <string name="incorrect_passcode">잘못된 패스코드</string> + <string name="if_you_enter_passcode_data_removed">앱을 열 때 이 패스코드를 입력하면 모든 앱 데이터가 되돌릴 수 없게 제거됩니다!</string> + <string name="servers_info_subscriptions_section_header">메시지 수신</string> + <string name="group_members_can_send_files">그룹 멤버는 파일 및 미디어를 보낼 수 있습니다.</string> + <string name="connect_plan_group_already_exists">그룹이 이미 존재합니다!</string> + <string name="v5_7_call_sounds">벨소리</string> + <string name="message_forwarded_title">메시지 전송됨</string> + <string name="member_inactive_desc">멤버가 활동 상태가 되면 나중에 메시지가 전달될 수 있습니다.</string> + <string name="delete_messages_cannot_be_undone_warning">메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다!</string> + <string name="gallery_image_button">이미지</string> + <string name="if_you_cant_meet_in_person">직접 만날 수 없는 경우 영상 통화에서 QR 코드를 보여주거나 링크를 공유하세요.</string> + <string name="import_theme_error">테마 가져오기 오류</string> + <string name="v4_3_improved_privacy_and_security">향상된 익명성 및 보안</string> + <string name="member_info_member_inactive">비활성</string> + <string name="service_notifications">즉시 알림!</string> + <string name="v5_1_message_reactions">메시지 반응</string> + <string name="app_check_for_updates_installed_successfully_title">성공적으로 설치됨</string> + <string name="app_check_for_updates_button_install">업데이트 설치</string> + <string name="theme_colors_section_title">인터페이스 색상</string> + <string name="update_network_smp_proxy_fallback_question">메시지 라우팅 대체</string> + <string name="group_members_can_add_message_reactions">그룹 멤버가 메시지 반응을 추가할 수 있습니다.</string> + <string name="settings_section_title_message_shape">메시지 모양</string> + <string name="message_queue_info">메시지 대기열 정보</string> + <string name="color_surface">메뉴 & 알림</string> + <string name="v4_6_group_welcome_message">그룹 환영 메시지</string> + <string name="v4_6_hidden_chat_profiles">숨겨진 채팅 프로필</string> + <string name="migrate_from_device_chat_should_be_stopped">계속하려면 채팅을 중지시켜야 합니다.</string> + <string name="share_text_message_status">메시지 상태: %s</string> + <string name="unblock_member_desc">%s에서 보낸 메시지가 표시됩니다!</string> + <string name="recent_history_is_not_sent_to_new_members">내역이 새 멤버에게 전송되지 않습니다.</string> + <string name="message_reactions_are_prohibited">이 그룹에서는 메시지 반응이 금지됩니다.</string> + <string name="group_members_can_send_simplex_links">그룹 멤버가 SimpleX 링크를 보낼 수 있습니다.</string> + <string name="v4_3_improved_server_configuration">향상된 서버 구성</string> + <string name="compose_message_placeholder">메시지</string> + <string name="message_servers">메시지 서버</string> + <string name="update_network_smp_proxy_mode_question">메시지 라우팅 모드</string> + <string name="email_invite_body">안녕하세요!\nSimpleX Chat 초대장이 도착했습니다: %s</string> + <string name="agent_internal_error_title">내부 오류</string> + <string name="invalid_name">잘못된 이름입니다!</string> + <string name="invalid_file_link">유효하지 않은 링크</string> + <string name="keep_invitation_link">유지</string> + <string name="error_parsing_uri_title">유효하지 않은 링크</string> + <string name="alert_text_fragment_encryption_out_of_sync_old_database">이 문제는 이전 데이터베이스의 백업을 사용하는 경우에 발생할 수 있습니다.</string> + <string name="v4_5_italian_interface">이탈리아어 인터페이스</string> + <string name="connect_plan_join_your_group">그룹에 참여하시겠습니까?</string> + <string name="thousand_abbreviation">k</string> + <string name="v5_5_join_group_conversation">그룹 대화에 참여</string> + <string name="this_device_version"><![CDATA[<i>(이 기기 v%s)</i>]]></string> + <string name="failed_to_create_user_invalid_title">잘못된 표시 이름입니다!</string> + <string name="non_content_uri_alert_title">잘못된 파일 경로</string> + <string name="invalid_qr_code">잘못된 QR 코드</string> + <string name="v6_0_private_routing_descr">IP 주소와 연결을 보호합니다.</string> + <string name="invite_friends_short">초대</string> + <string name="invite_friends">친구 초대</string> + <string name="v4_3_irreversible_message_deletion">메시지 영구 삭제</string> + <string name="action_button_add_members">초대</string> + <string name="onboarding_choose_server_operators">운영자 선택</string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[운영자(들)의 이용 약관이 수락될 것입니다: <b>%s</b>.]]></string> + <string name="auth_open_chat_profiles">채팅 프로필 변경</string> + <string name="disable_onion_hosts_when_not_supported"><![CDATA[Set <i>SOCKS 프록시가 지원하지 않는 경우</i> .onion 호스트를 No로 사용합니다.]]></string> + <string name="remote_host_error_missing"><![CDATA[모바일 <b>%s</b> 이 찾을 수 없음]]></string> + <string name="system_restricted_background_in_call_warn"><![CDATA[백그라운드에서 전화를 걸려면 <b>앱 설정에서 앱 배터리 사용량</b> / <b>제한 없음</b> 을 선택하세요.]]></string> + <string name="remote_host_error_disconnected"><![CDATA[모바일 <b>%s</b> 이 연결 끊김]]></string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[그룹 링크입니다 <b>%1$s</b>!]]></string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[두 기기에서 동일한 데이터베이스를<b> 사용해서는 안 됩니다</b>.]]></string> + <string name="read_more_in_user_guide_with_link"><![CDATA[<font color="#0088ff">사용자 가이드</font>에서 확인하세요.]]></string> + <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[링크를 클릭하여 연결할 수도 있습니다. 브라우저에서 열리면<b> 모바일 앱에서 열기 </b>버튼을 클릭합니다.]]></string> + <string name="operator">운영자</string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[운영자(들)의 이용 약관이 수락될 것입니다: <b>%s</b>.]]></string> + <string name="operator_conditions_accepted_on">약관 수락 날짜: %s.</string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[운영자에게도 동일한 약관이 적용됩니다 <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[운영자(들)에게도 동일한 약관이 적용됩니다: <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_applied"><![CDATA[이러한 약관은 다음에도 적용됩니다: <b>%s</b>.]]></string> + <string name="operator_added_xftp_servers">추가된 미디어 및 파일 서버</string> + <string name="appearance_app_toolbars">앱 툴바</string> + <string name="appearance_bars_blur_radius">흐리기</string> + <string name="accept_conditions">약관 수락</string> + <string name="operator_conditions_accepted">약관을 수락함</string> + <string name="operator_added_message_servers">추가된 메시지 서버</string> + <string name="address_or_1_time_link">주소 또는 일회용 링크?</string> + <string name="address_settings">주소 설정</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[일회용 링크는 <i>한 명의 연락처에만 사용할 수 있으며</i> - 직접 또는 메신저를 통해 공유하십시오.]]></string> + <string name="operators_conditions_accepted_for"><![CDATA[운영자(들)의 이용 약관이 수락됩니다(s): <b>%s</b>.]]></string> + <string name="operator_conditions_accepted_for_some"><![CDATA[다음 운영자(들)의 이용 약관이 이미 수락되었습니다: <b>%s</b>.]]></string> + <string name="remote_host_error_busy"><![CDATA[모바일 <b>%s</b> 이 현재 사용 중]]></string> + <string name="operators_conditions_will_also_apply"><![CDATA[이러한 약관은 다음에도 적용됩니다: <b>%s</b>.]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[To improve privacy, <b>개인 정보를 보호하기 위해 SimpleX는 푸시 알림을 사용하는 대신 백그라운드</b>에서 실행됩니다.]]></string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[<b>%s</b> 의 서버를 사용하려면 사용 약관에 동의하십시오.]]></string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[이미 <b>%1$s</b> 에 연결 중입니다.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[다음 대화 상자에서 <b>허용</b>을 선택하면 알림을 즉시 받을 수 있습니다.]]></string> + <string name="system_restricted_background_warn"><![CDATA[알림을 활성화하려면 <b>앱 설정에서 앱 배터리 사용량</b> / <b>제한 없음</b> 을 선택하십시오.]]></string> + <string name="remote_host_error_inactive"><![CDATA[모바일 <b>%s</b> 이 현재 비활성화됨]]></string> + <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[<font color="#0088ff">SimpleX Chat 개발자에게 연결하여 질문하고 업데이트를 받을 수 있습니다</font>.]]></string> + <string name="remote_host_was_disconnected_toast"><![CDATA[모바일 <b>%s</b> 이 연결 끊김]]></string> + <string name="open_on_mobile_and_scan_qr_code"><![CDATA[모바일 앱 <i>에서 데스크톱에서</i> 사용을 열고 QR 코드를 스캔합니다.]]></string> + <string name="remote_host_error_timeout"><![CDATA[모바일에 연결하는 동안 시간이 초과했습니다 <b>%s</b>]]></string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[이미 <b>%1$s</b> 그룹에 가입하는 중 입니다.]]></string> + <string name="remote_host_error_bad_version"><![CDATA[모바일 <b>%s</b> 버전이 지원되지 않습니다. 두 기기에서 동일한 버전을 사용하는지 확인하십시오.]]></string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[이미 <b>%1$s</b> 그룹에 속해 있습니다.]]></string> + <string name="error_accepting_operator_conditions">약관을 수락하는 중 오류 발생</string> + <string name="connection_security">연결 보안</string> + <string name="debug_logs">로그 활성화</string> + <string name="business_address">비즈니스 주소</string> + <string name="create_1_time_link">일회용 링크 생성</string> + <string name="onboarding_notifications_mode_periodic_desc_short">10분마다 메시지 확인</string> + <string name="operator_conditions_accepted_for_enabled_operators_on">%s:에서 활성화된 운영자에 대한 약관이 자동으로 수락됩니다.</string> + <string name="operator_conditions_failed_to_load">현재 약관 텍스트를 로드할 수 없습니다, 다음 링크를 통해 약관을 검토할 수 있습니다:</string> + <string name="operator_conditions_of_use">사용 약관</string> + <string name="v6_2_network_decentralization_enable_flux">Flux 활성화</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[모든 메시지와 파일은 쪽지에서 포스트 퀀텀 보안을 사용하여 <b>종단 간 암호화</b>로 전송됩니다.]]></string> + <string name="onboarding_notifications_mode_service_desc_short">앱이 항상 백그라운드에서 실행</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">활성화된 운영자에 대한 약관은 30일 후에 수락됩니다.</string> + <string name="button_add_team_members">팀 멤버 추가하기</string> + <string name="button_add_friends">친구 추가</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">모든 멤버에게서 채팅이 삭제됩니다 - 이 결정은 되돌릴 수 없습니다!</string> + <string name="button_delete_chat">채팅 삭제</string> + <string name="delete_chat_question">채팅을 삭제하시겠습니까?</string> + <string name="info_row_chat">채팅</string> + <string name="error_adding_server">서버 추가 중 오류</string> + <string name="direct_messages_are_prohibited">멤버 간의 다이렉트 메시지는 금지됩니다.</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Xiaomi 기기</b>: 알림이 작동하려면 시스템 설정에서 자동 시작을 사용하도록 설정하세요.]]></string> + <string name="direct_messages_are_prohibited_in_chat">이 채팅에서는 멤버 간의 다이렉트 메시지가 금지됩니다.</string> + <string name="delete_chat_for_self_cannot_undo_warning">당신에게서 채팅이 삭제됩니다 - 이 결정은 되돌릴 수 없습니다!</string> + <string name="operator_conditions_will_be_accepted_on">%s:에서 약관이 수락됩니다.</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[당신은 이미 <b>%1$s</b>에 연결되어 있습니다.]]></string> + <string name="connect_plan_chat_already_exists">채팅이 이미 존재합니다!</string> + <string name="onboarding_network_operators_continue">계속</string> + <string name="display_name_accepted_invitation">수락된 초대</string> + <string name="v6_2_business_chats">비즈니스 채팅</string> + <string name="onboarding_network_about_operators">운영자 소개</string> + <string name="sync_connection_desc">연결에 암호화 재협상이 필요합니다.</string> + <string name="encryption_renegotiation_in_progress">암호화 재협상이 진행 중입니다.</string> + <string name="add_your_team_members_to_conversations">팀 멤버를 대화에 추가하세요.</string> + <string name="report_reason_other">다른 이유</string> + <string name="archive_verb">보관</string> + <string name="group_reports_active_one">1 보고서</string> + <string name="chat_list_add_list">리스트 추가</string> + <string name="chat_list_all">전부</string> + <string name="delete_chat_list_warning">목록 %s의 모든 차트가 제거되었고, 목록도 삭제되었습니다.</string> + <string name="chat_item_ttl_year">1 년</string> + <string name="add_to_list">리스트에 추가하기</string> + <string name="report_item_archived">보관된 보고서</string> + <string name="report_item_archived_by">보관된 신고: %s</string> + <string name="report_reason_community">커뮤니티 가이드라인 위반</string> + <string name="report_archive_alert_title">신고를 보관하시겠습니까?</string> + <string name="archive_report">신고 보관</string> + <string name="chat_list_contacts">연락처</string> + <string name="connection_error_blocked">연결이 차단되었습니다</string> + <string name="connection_error_blocked_desc">연결이 서버 운영자에 의해 차단되었습니다:\n%1$s</string> + <string name="chat_list_businesses">비즈니스</string> + <string name="change_order_chat_list_menu_action">순서 변경</string> + <string name="privacy_chat_list_open_links_ask">문의</string> + <string name="connection_not_ready">연결이 준비되지 않았습니다.</string> + <string name="change_automatic_deletion_question">자동 메시지 삭제를 변경하시겠습니까?</string> + <string name="v6_3_better_groups_performance">그룹 성능 향상</string> + <string name="report_archive_alert_desc_all">모든 신고가 보관됩니다.</string> + <string name="report_archive_alert_title_all">모든 신고를 보관하시겠습니까?</string> + <string name="archive_reports">신고 보관</string> + <string name="change_list">목록 변경</string> + <string name="enable_sending_member_reports">메시지를 운영자에게 신고할 수 있도록 허용합니다.</string> + <string name="v6_3_better_privacy_and_security">더 나은 개인정보 보호 및 보안</string> + <string name="report_archive_alert_title_nth">신고 %d건을 보관하시겠습니까?</string> + <string name="failed_to_create_user_invalid_desc">이 표시 이름은 유효하지 않습니다. 다른 이름을 선택하세요.</string> + <string name="remote_ctrl_error_timeout">데스크톱에 연결하는 동안 시간 초과되었습니다</string> +</resources> 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 163645c7d5..adf66650f1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml @@ -6,8 +6,8 @@ <string name="chat_item_ttl_day">1 diena</string> <string name="a_plus_b">a + b</string> <string name="about_simplex">Apie SimpleX</string> - <string name="smp_servers_add">Pridėti serverį…</string> - <string name="v4_3_improved_server_configuration_desc">Pridėti serverius skenuojant QR kodus.</string> + <string name="smp_servers_add">Pridėti serverį</string> + <string name="v4_3_improved_server_configuration_desc">Pridėti serverius nuskanuojant QR kodus.</string> <string name="appearance_settings">Išvaizda</string> <string name="app_version_title">Programėlės versija</string> <string name="app_version_name">Programėlės versija: v%s</string> @@ -18,13 +18,13 @@ <string name="answer_call">Atsiliepti</string> <string name="icon_descr_call_ended">Skambutis baigtas</string> <string name="settings_section_title_calls">SKAMBUČIAI</string> - <string name="allow_your_contacts_irreversibly_delete">Leisti jūsų adresatams negrįžtamai ištrinti išsiųstas žinutes.</string> + <string name="allow_your_contacts_irreversibly_delete">Leisti jūsų kontaktams negrįžtamai ištrinti išsiųstas žinutes. (24 valandas)</string> <string name="back">Atgal</string> <string name="settings_section_title_icon">PROGRAMĖLĖS PIKTOGRAMA</string> <string name="chat_preferences_always">visada</string> - <string name="allow_your_contacts_to_send_voice_messages">Leisti jūsų adresatams siųsti balso žinutes.</string> - <string name="allow_irreversible_message_deletion_only_if">Leisti negrįžtamą žinučių ištrynimą tik tuo atveju, jei jūsų adresatas jums tai leidžia.</string> - <string name="allow_voice_messages_only_if">Leisti balso žinutes tik tuo atveju, jei jūsų adresatas jas leidžia.</string> + <string name="allow_your_contacts_to_send_voice_messages">Leisti jūsų kontaktams siųsti balso žinutes.</string> + <string name="allow_irreversible_message_deletion_only_if">Leisti negrįžtamą žinučių ištrynimą tik tuo atveju, jei jūsų kontaktas jums tai leidžia. (24 valandas)</string> + <string name="allow_voice_messages_only_if">Leisti balso žinutes tik tuo atveju, jei jūsų kontaktas jas leidžia.</string> <string name="allow_verb">Leisti</string> <string name="allow_voice_messages_question">Leisti balso žinutes\?</string> <string name="bold_text">pusjuodis</string> @@ -33,14 +33,14 @@ <string name="settings_audio_video_calls">Garso ir vaizdo skambučiai</string> <string name="integrity_msg_bad_hash">bloga žinutės maiša</string> <string name="integrity_msg_bad_id">blogas žinutės ID</string> - <string name="allow_disappearing_messages_only_if">Leisti išnykstančias žinutes tik tuo atveju, jei jūsų adresatas jas leidžia.</string> - <string name="clear_chat_warning">Visos žinutės bus ištrintos – to neįmanoma bus atšaukti! Žinutės bus ištrintos TIK jums.</string> - <string name="allow_to_delete_messages">Leisti negrįžtamai ištrinti išsiųstas žinutes.</string> + <string name="allow_disappearing_messages_only_if">Leisti išnykstančias žinutes tik tuo atveju, jei jūsų kontaktas jas leidžia.</string> + <string name="clear_chat_warning">Visos žinutės bus ištrintos – to bus neįmanoma atšaukti! Žinutės bus ištrintos TIK jums.</string> + <string name="allow_to_delete_messages">Leisti negrįžtamai ištrinti išsiųstas žinutes. (24 valandas)</string> <string name="allow_to_send_disappearing">Leisti siųsti išnykstančias žinutes.</string> <string name="allow_to_send_voice">Leisti siųsti balso žinutes.</string> <string name="allow_direct_messages">Leisti siųsti tiesiogines žinutes nariams.</string> <string name="v4_6_audio_video_calls">Garso ir vaizdo skambučiai</string> - <string name="allow_your_contacts_to_send_disappearing_messages">Leisti jūsų adresatams siųsti išnykstančias žinutes.</string> + <string name="allow_your_contacts_to_send_disappearing_messages">Leisti jūsų kontaktams siųsti išnykstančias žinutes.</string> <string name="auth_unavailable">Tapatybės nustatymas neprieinamas</string> <string name="impossible_to_recover_passphrase"><![CDATA[<b>Turėkite omenyje</b>: jeigu prarasite slaptafrazę, NEBEGALĖSITE jos atkurti ar pakeisti.]]></string> <string name="cancel_verb">Atsisakyti</string> @@ -49,14 +49,14 @@ <string name="server_connected">prisijungta</string> <string name="server_connecting">jungiamasi</string> <string name="display_name_connection_established">ryšys užmegztas</string> - <string name="display_name_connecting">jungiasi…</string> + <string name="display_name_connecting">jungiamasi…</string> <string name="connection_error">Ryšio klaida</string> <string name="smp_server_test_connect">Prisijungti</string> <string name="notification_contact_connected">Prisijungė</string> <string name="connect_button">Prisijungti</string> <string name="group_member_status_connected">prisijungė</string> - <string name="contact_connection_pending">jungiasi…</string> - <string name="group_connection_pending">jungiasi…</string> + <string name="contact_connection_pending">jungiamasi…</string> + <string name="group_connection_pending">jungiamasi…</string> <string name="icon_descr_server_status_connected">Prisijungta</string> <string name="confirm_verb">Patvirtinti</string> <string name="configure_ICE_servers">Konfigūruoti ICE serverius</string> @@ -64,7 +64,7 @@ <string name="rcv_group_event_member_connected">prisijungė</string> <string name="info_row_connection">Ryšys</string> <string name="confirm_database_upgrades">Patvirtinti duomenų bazių naujinimus</string> - <string name="group_member_status_connecting">jungiasi</string> + <string name="group_member_status_connecting">jungiamasi</string> <string name="network_session_mode_entity">Ryšys</string> <string name="database_passphrase_will_be_updated">Duomenų bazės šifravimo slaptafrazė bus atnaujinta.</string> <string name="core_version">Branduolio versija: v%s</string> @@ -97,11 +97,11 @@ <string name="error_joining_group">Klaida prisijungiant prie grupės</string> <string name="error_receiving_file">Klaida gaunant failą</string> <string name="error_changing_address">Klaida keičiant adresą</string> - <string name="error_deleting_contact">Klaida ištrinant adresatą</string> + <string name="error_deleting_contact">Klaida ištrinant kontaktą</string> <string name="error_deleting_group">Klaida ištrinant grupę</string> <string name="smp_server_test_disconnect">Atsijungti</string> <string name="error_deleting_user">Klaida ištrinant naudotojo profilį</string> - <string name="notification_display_mode_hidden_desc">Slėpti adresatą ir žinutę</string> + <string name="notification_display_mode_hidden_desc">Slėpti kontaktą ir žinutę</string> <string name="copy_verb">Kopijuoti</string> <string name="reply_verb">Atsakyti</string> <string name="delete_message__question">Ištrinti žinutę\?</string> @@ -111,13 +111,12 @@ <string name="file_saved">Failas įrašytas</string> <string name="icon_descr_server_status_disconnected">Atsijungta</string> <string name="icon_descr_server_status_error">Klaida</string> - <string name="ask_your_contact_to_enable_voice">Paprašykite adresato, kad įjungtų balso žinučių siuntimą.</string> + <string name="ask_your_contact_to_enable_voice">Paprašykite kontakto, kad įjungtų balso žinučių siuntimą.</string> <string name="reset_verb">Atstatyti</string> <string name="delete_group_menu_action">Ištrinti</string> <string name="image_descr_qr_code">QR kodas</string> <string name="icon_descr_help">pagalba</string> <string name="icon_descr_email">El. paštas</string> - <string name="create_one_time_link">Sukurti vienkartinio pakvietimo nuorodą</string> <string name="scan_code">Skenuoti kodą</string> <string name="database_passphrase_and_export">Duomenų bazės slaptafrazė ir eksportavimas</string> <string name="smp_servers_delete_server">Ištrinti serverį</string> @@ -126,12 +125,9 @@ <string name="files_and_media_section">Failai ir medija</string> <string name="delete_files_and_media_all">Ištrinti visus failus</string> <string name="delete_files_and_media_question">Ištrinti failus ir mediją\?</string> - <string name="delete_archive">Ištrinti archyvą</string> - <string name="delete_chat_archive_question">Ištrinti pokalbio archyvą\?</string> <string name="snd_group_event_group_profile_updated">grupės profilis atnaujintas</string> <string name="info_row_group">Grupė</string> <string name="users_delete_question">Ištrinti pokalbio profilį\?</string> - <string name="network_options_revert">Sugrąžinti</string> <string name="network_options_save">Įrašyti</string> <string name="feature_enabled">įjungta</string> <string name="delete_after">Ištrinti po</string> @@ -147,8 +143,8 @@ <string name="for_everybody">Visiems</string> <string name="icon_descr_edited">taisyta</string> <string name="observer_cant_send_message_desc">Susisiekite su grupės administratoriumi.</string> - <string name="button_delete_contact">Ištrinti adresatą</string> - <string name="delete_contact_question">Ištrinti adresatą\?</string> + <string name="button_delete_contact">Ištrinti kontaktą</string> + <string name="delete_contact_question">Ištrinti kontaktą?</string> <string name="create_group">Sukurti slaptą grupę</string> <string name="from_gallery_button">Iš galerijos</string> <string name="toast_permission_denied">Leidimas atmestas!</string> @@ -190,15 +186,14 @@ <string name="dont_show_again">Daugiau neberodyti</string> <string name="theme_dark">Tamsus</string> <string name="reset_color">Atstatyti spalvas</string> - <string name="save_color">Įrašyti spalvą</string> - <string name="group_preferences">Grupės nuostatos</string> + <string name="group_preferences">Grupės parinktys</string> <string name="full_deletion">Ištrinti visiems</string> <string name="direct_messages">Tiesioginės žinutės</string> <string name="v4_4_disappearing_messages">Išnykstančios žinutės</string> <string name="timed_messages">Išnykstančios žinutės</string> <string name="v4_4_french_interface">Sąsaja prancūzų kalba</string> <string name="v4_3_improved_privacy_and_security">Patobulintas privatumas ir saugumas</string> - <string name="num_contacts_selected">Pažymėta adresatų: %d</string> + <string name="num_contacts_selected">Pažymėta kontaktų: %d</string> <string name="delete_chat_profile_question">Ištrinti pokalbio profilį\?</string> <string name="error_importing_database">Klaida importuojant pokalbio duomenų bazę</string> <string name="delete_messages_after">Ištrinti žinutes po</string> @@ -235,13 +230,13 @@ <string name="delete_profile">Ištrinti profilį</string> <string name="share_one_time_link">Sukurti vienkartinio pakvietimo nuorodą</string> <string name="delete_contact_menu_action">Ištrinti</string> - <string name="save_and_notify_contact">Įrašyti ir pranešti adresatui</string> + <string name="save_and_notify_contact">Įrašyti ir pranešti kontaktui</string> <string name="enable_automatic_deletion_question">Įjungti automatinį žinučių ištrynimą\?</string> <string name="error_deleting_database">Klaida ištrinant pokalbio duomenų bazę</string> <string name="error_starting_chat">Klaida pradedant pokalbį</string> <string name="hide_profile">Slėpti profilį</string> <string name="import_database_confirmation">Importuoti</string> - <string name="save_and_notify_contacts">Įrašyti ir pranešti adresatams</string> + <string name="save_and_notify_contacts">Įrašyti ir pranešti kontaktams</string> <string name="callstate_starting">pradedama…</string> <string name="use_chat">Naudoti pokalbį</string> <string name="icon_descr_speaker_off">Išjungti garsiakalbį</string> @@ -250,7 +245,7 @@ <string name="settings_section_title_settings">NUSTATYMAI</string> <string name="theme_system">Sistemos</string> <string name="unknown_message_format">nežinomas žinutės formatas</string> - <string name="simplex_link_contact">SimpleX adresato adresas</string> + <string name="simplex_link_contact">SimpleX kontakto adresas</string> <string name="simplex_link_group">SimpleX grupės nuoroda</string> <string name="simplex_link_invitation">SimpleX vienkartinis pakvietimas</string> <string name="simplex_link_mode">SimpleX nuorodos</string> @@ -269,11 +264,11 @@ <string name="update_network_settings_question">Atnaujinti tinklo nustatymus\?</string> <string name="ntf_channel_messages">SimpleX Chat žinutės</string> <string name="ntf_channel_calls">SimpleX Chat skambučiai</string> - <string name="notification_preview_mode_message_desc">Rodyti adresatą ir žinutę</string> - <string name="notification_preview_mode_contact_desc">Rodyti tik adresatą</string> - <string name="auth_stop_chat">Stabdyti pokalbį</string> + <string name="notification_preview_mode_message_desc">Rodyti kontaktą ir žinutę</string> + <string name="notification_preview_mode_contact_desc">Rodyti tik kontaktą</string> + <string name="auth_stop_chat">Sustabdyti pokalbius</string> <string name="share_message">Bendrinti žinutę…</string> - <string name="use_camera_button">Kamera</string> + <string name="use_camera_button">Fotoaparatas</string> <string name="thank_you_for_installing_simplex">Dėkojame, kad įdiegėte SimpleX Chat!</string> <string name="smp_servers_use_server">Naudoti serverį</string> <string name="network_enable_socks">Naudoti SOCKS įgaliotąjį serverį\?</string> @@ -284,12 +279,12 @@ <string name="v4_6_audio_video_calls_descr">Bluetooth palaikymas ir kiti patobulinimai.</string> <string name="group_invitation_tap_to_join">Bakstelėkite, norėdami prisijungti</string> <string name="show_call_on_lock_screen">Rodyti</string> - <string name="stop_chat_confirmation">Stabdyti</string> + <string name="stop_chat_confirmation">Sustabdyti</string> <string name="upgrade_and_open_chat">Naujinti ir atverti pokalbį</string> <string name="switch_verb">Perjungti</string> <string name="icon_descr_received_msg_status_unread">neskaityta</string> <string name="show_dev_options">Rodyti:</string> - <string name="stop_chat_question">Stabdyti pokalbį\?</string> + <string name="stop_chat_question">Sustabdyti pokalbius?</string> <string name="update_database">Atnaujinti</string> <string name="unknown_error">Nežinoma klaida</string> <string name="add_contact_or_create_group">Pradėti naują pokalbį</string> @@ -303,7 +298,7 @@ <string name="app_name">SimpleX</string> <string name="sender_you_pronoun">jūs</string> <string name="description_via_group_link">per grupės nuorodą</string> - <string name="description_via_contact_address_link">per adresato adreso nuorodą</string> + <string name="description_via_contact_address_link">per kontakto adreso nuorodą</string> <string name="description_via_one_time_link">per vienkartinę nuorodą</string> <string name="simplex_link_connection">per %1$s</string> <string name="simplex_link_mode_browser">Per naršyklę</string> @@ -327,12 +322,12 @@ <string name="using_simplex_chat_servers">Naudojami SimpleX Chat serveriai.</string> <string name="network_settings">Išplėstiniai tinklo nustatymai</string> <string name="your_current_profile">Jūsų dabartinis profilis</string> - <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Jūsų profilis yra saugomas jūsų įrenginyje ir bendrinamas tik su jūsų adresatais. SimpleX serveriai negali matyti jūsų profilio.</string> + <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Jūsų profilis yra saugomas jūsų įrenginyje ir bendrinamas tik su jūsų kontaktais. SimpleX serveriai negali matyti jūsų profilio.</string> <string name="icon_descr_video_call">vaizdo skambutis</string> <string name="your_calls">Jūsų skambučiai</string> <string name="webrtc_ice_servers">WebRTC ICE serveriai</string> <string name="your_ice_servers">Jūsų ICE serveriai</string> - <string name="snd_group_event_user_left">jūs išėjote</string> + <string name="snd_group_event_user_left">išėjote</string> <string name="group_member_role_admin">administratorius</string> <string name="incognito_random_profile">Jūsų atsitiktinis profilis</string> <string name="chat_preferences_you_allow">Jūs leidžiate</string> @@ -353,7 +348,6 @@ <string name="whats_new">Kas naujo</string> <string name="save_and_notify_group_members">Įrašyti ir pranešti grupės nariams</string> <string name="callstate_received_confirmation">gautas patvirtinimas…</string> - <string name="read_more_in_github">Išsamiau skaitykite mūsų „GitHub“ saugykloje</string> <string name="icon_descr_call_missed">Praleistas skambutis</string> <string name="settings_section_title_chats">POKALBIAI</string> <string name="settings_section_title_themes">APIPAVIDALINIMAI</string> @@ -363,18 +357,18 @@ <string name="button_add_members">Pakviesti narius</string> <string name="disappearing_prohibited_in_this_chat">Išnykstančios žinutės šiame pokalbyje yra uždraustos.</string> <string name="voice_messages_are_prohibited">Balso žinutės šioje grupėje yra uždraustos.</string> - <string name="connect_via_group_link">Prisijungti per grupės nuorodą\?</string> - <string name="connect_via_contact_link">Prisijungti per adresato nuorodą\?</string> + <string name="connect_via_group_link">Prisijungti prie grupės?</string> + <string name="connect_via_contact_link">Prisijungti per kontakto adresą?</string> <string name="sending_files_not_yet_supported">failų siuntimas kol kas nepalaikomas</string> <string name="invalid_message_format">neteisingas žinutės formatas</string> <string name="receiving_files_not_yet_supported">failų gavimas kol kas nepalaikomas</string> <string name="invalid_chat">neteisingas pokalbis</string> <string name="invalid_data">neteisingi duomenys</string> <string name="description_via_one_time_link_incognito">inkognito per vienkartinę nuorodą</string> - <string name="contact_already_exists">Adresatas jau yra</string> + <string name="contact_already_exists">Kontaktas jau yra</string> <string name="database_initialization_error_title">Nepavyksta inicijuoti duomenų bazės</string> <string name="notification_preview_mode_message">Žinutės tekstas</string> - <string name="notification_preview_somebody">Paslėptas adresatas:</string> + <string name="notification_preview_somebody">Paslėptas kontaktas:</string> <string name="share_image">Bendrinti mediją…</string> <string name="large_file">Didelis failas!</string> <string name="voice_messages_prohibited">Balso žinutės uždraustos!</string> @@ -387,8 +381,6 @@ <string name="error_smp_test_certificate">Gali būti, kad liudijimo kontrolinis kodas serverio adrese yra neteisingas</string> <string name="smp_server_test_delete_file">Ištrinti failą</string> <string name="image_decoding_exception_title">Dekodavimo klaida</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Pridėti naują adresatą</b>: norėdami sukurti adresatui vienkartinį QR kodą.]]></string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Skenuoti QR kodą</b>: norėdami prisijungti prie adresato, kuris jums rodo QR kodą.]]></string> <string name="clear_chat_button">Išvalyti pokalbį</string> <string name="unmute_chat">Įjungti pranešimus</string> <string name="invalid_QR_code">Neteisingas QR kodas</string> @@ -398,20 +390,19 @@ <string name="mark_code_verified">Žymėti kaip patvirtintą</string> <string name="chat_lock">SimpleX užraktas</string> <string name="saved_ICE_servers_will_be_removed">Įrašyti WebRTC ICE serveriai bus pašalinti.</string> - <string name="save_archive">Įrašyti archyvą</string> <string name="button_send_direct_message">Siųsti tiesioginę žinutę</string> <string name="button_remove_member">Šalinti narį</string> <string name="theme_light">Šviesus</string> <string name="feature_enabled_for_you">įjungta jums</string> - <string name="both_you_and_your_contact_can_send_disappearing">Tiek jūs, tiek ir jūsų adresatas gali siųsti išnykstančias žinutes.</string> + <string name="both_you_and_your_contact_can_send_disappearing">Tiek jūs, tiek ir jūsų kontaktas gali siųsti išnykstančias žinutes.</string> <string name="group_members_can_send_voice">Grupės nariai gali siųsti balso žinutes.</string> <string name="v4_2_security_assessment_desc">SimpleX Chat saugumo auditą atliko „Trail of Bits“.</string> <string name="v4_2_security_assessment">Saugumo įvertinimas</string> <string name="save_group_profile">Įrašyti grupės profilį</string> - <string name="both_you_and_your_contacts_can_delete">Tiek jūs, tiek ir jūsų adresatas gali negrįžtamai ištrinti išsiųstas žinutes.</string> - <string name="both_you_and_your_contact_can_send_voice">Tiek jūs, tiek ir jūsų adresatas gali siųsti balso žinutes.</string> + <string name="both_you_and_your_contacts_can_delete">Tiek jūs, tiek ir jūsų kontaktas gali negrįžtamai ištrinti išsiųstas žinutes. (24 valandas)</string> + <string name="both_you_and_your_contact_can_send_voice">Tiek jūs, tiek ir jūsų kontaktas gali siųsti balso žinutes.</string> <string name="enter_passphrase_notification_desc">Norėdami gauti pranešimus, įveskite duomenų bazės slaptafrazę</string> - <string name="notification_preview_mode_contact">Adresato vardas</string> + <string name="notification_preview_mode_contact">Kontakto vardas</string> <string name="la_notice_title_simplex_lock">SimpleX užraktas</string> <string name="la_notice_turn_on">Įjungti</string> <string name="auth_simplex_lock_turned_on">SimpleX užraktas įjungtas</string> @@ -428,7 +419,7 @@ <string name="smp_servers_check_address">Patikrinkite serverio adresą ir bandykite dar kartą.</string> <string name="smp_servers_invalid_address">Neteisingas serverio adresas!</string> <string name="network_session_mode_user">Pokalbio profilis</string> - <string name="profile_is_only_shared_with_your_contacts">Profilis yra bendrinamas tik su jūsų adresatais.</string> + <string name="profile_is_only_shared_with_your_contacts">Profilis yra bendrinamas tik su jūsų kontaktais.</string> <string name="read_more_in_github_with_link"><![CDATA[Išsamiau skaitykite mūsų <font color="#0088ff">„GitHub“ saugykloje</font>.]]></string> <string name="settings_section_title_socks">SOCKS ĮGALIOTASIS SERVERIS</string> <string name="save_passphrase_and_open_chat">Įrašyti slaptafrazę ir atverti pokalbį</string> @@ -443,16 +434,14 @@ <string name="clear_contacts_selection_button">Išvalyti</string> <string name="button_leave_group">Išeiti iš grupės</string> <string name="role_in_group">Vaidmuo</string> - <string name="group_is_decentralized">Grupė yra pilnai decentralizuota – ji yra matoma tik nariams.</string> + <string name="group_is_decentralized">Pilnai decentralizuota – ji yra matoma tik nariams.</string> <string name="theme">Apipavidalinimas</string> - <string name="feature_enabled_for_contact">įjungta adresatui</string> + <string name="feature_enabled_for_contact">įjungta kontaktui</string> <string name="disappearing_messages_are_prohibited">Išnykstančios žinutės šioje grupėje yra uždraustos.</string> <string name="group_members_can_send_dms">Grupės nariai gali siųsti tiesiogines žinutes.</string> <string name="group_members_can_send_disappearing">Grupės nariai gali siųsti išnykstančias žinutes.</string> <string name="v4_3_improved_privacy_and_security_desc">Slėpti programėlės ekraną paskiausių programėlių sąraše.</string> - <string name="should_be_at_least_one_profile">Turėtų būti bent vienas naudotojo profilis.</string> - <string name="should_be_at_least_one_visible_profile">Turėtų būti matomas bent vienas naudotojo profilis.</string> - <string name="chat_preferences_contact_allows">Adresatas leidžia</string> + <string name="chat_preferences_contact_allows">Kontaktas leidžia</string> <string name="voice_prohibited_in_this_chat">Balso žinutės šiame pokalbyje yra uždraustos.</string> <string name="v4_4_disappearing_messages_desc">Išsiųstos žinutės bus ištrintos po nustatyto laiko.</string> <string name="user_unhide">Nebeslėpti</string> @@ -468,11 +457,10 @@ <string name="member_role_will_be_changed_with_notification">Vaidmuo bus pakeistas į „%s“. Visiems grupėje bus pranešta.</string> <string name="member_role_will_be_changed_with_invitation">Vaidmuo bus pakeistas į „%s“. Narys gaus naują pakvietimą.</string> <string name="chat_preferences">Pokalbio nuostatos</string> - <string name="contact_preferences">Adresato nuostatos</string> + <string name="contact_preferences">Kontakto nuostatos</string> <string name="join_group_button">Prisijungti</string> <string name="change_verb">Keisti</string> <string name="conn_stats_section_title_servers">SERVERIAI</string> - <string name="cant_delete_user_profile">Nepavyksta ištrinti naudotojo profilio!</string> <string name="clear_chat_menu_action">Išvalyti</string> <string name="unhide_profile">Nebeslėpti profilio</string> <string name="videos_limit_title">Per daug vaizdo įrašų!</string> @@ -503,13 +491,13 @@ <string name="mark_read">Žymėti kaip skaitytą</string> <string name="port_verb">Prievadas</string> <string name="stop_snd_file__message">Failo siuntimas bus sustabdytas.</string> - <string name="stop_file__confirm">Stabdyti</string> + <string name="stop_file__confirm">Sustabdyti</string> <string name="stop_file__action">Stabdyti failą</string> <string name="stop_rcv_file__title">Stabdyti failo gavimą\?</string> <string name="stop_snd_file__title">Stabdyti failo siuntimą\?</string> <string name="this_text_is_available_in_settings">Šis tekstas yra prieinamas nustatymuose</string> <string name="calls_prohibited_with_this_contact">Garso/vaizdo skambučiai yra uždrausti.</string> - <string name="both_you_and_your_contact_can_make_calls">Tiek jūs, tiek ir jūsų adresatas gali skambinti.</string> + <string name="both_you_and_your_contact_can_make_calls">Tiek jūs, tiek ir jūsų kontaktas gali skambinti.</string> <string name="v4_5_message_draft">Žinutės juodraštis</string> <string name="v4_6_hidden_chat_profiles_descr">Apsaugokite slaptažodžiu savo pokalbių profilius!</string> <string name="alert_title_msg_bad_id">Blogas žinutės ID</string> @@ -519,7 +507,7 @@ <string name="leave_group_question">Išeiti iš grupės\?</string> <string name="unknown_database_error_with_info">Nežinoma duomenų bazės klaida: %s</string> <string name="users_delete_with_connections">Profilis ir ryšiai su serveriu</string> - <string name="direct_messages_are_prohibited_in_chat">Tiesioginės žinutės tarp narių šioje grupėje yra uždraustos.</string> + <string name="direct_messages_are_prohibited_in_group">Tiesioginės žinutės tarp narių šioje grupėje yra uždraustos.</string> <string name="audio_video_calls">Garso/vaizdo skambučiai</string> <string name="available_in_v51">" \nPrieinama versijoje v5.1"</string> @@ -527,78 +515,73 @@ <string name="v5_0_large_files_support">Vaizdo įrašai ir failai iki 1GB</string> <string name="search_verb">Ieškoti</string> <string name="la_mode_off">Išjungta</string> - <string name="network_session_mode_user_description"><![CDATA[Atskiras TCP ryšys (ir SOCKS kredencialas) bus naudojamas <b>kiekvienam pokalbių profiliui, kurį turite programoje</b>.]]></string> - <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">Nepavyko iššifruoti %1$d pranešimų.</string> - <string name="button_add_welcome_message">Pridėti sveikinimo pranešimą</string> + <string name="network_session_mode_user_description"><![CDATA[<b>Kiekvienam programėlėje esančiam pokalbių profiliui</b> bus naudojamas atskiras TCP ryšys (ir SOCKS prisijungimo duomenys).]]></string> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">Nepavyko iššifruoti %1$d žinučių.</string> + <string name="button_add_welcome_message">Pridėti sveikinimo žinutę</string> <string name="v5_2_more_things">Dar keletas dalykų</string> <string name="all_group_members_will_remain_connected">Visi grupės nariai liks prisijungę.</string> - <string name="authentication_cancelled">Autentiškumo patvirtinimas atšauktas</string> - <string name="always_use_relay">Visada naudoti relę</string> - <string name="icon_descr_asked_to_receive">Paprašė leidimo gauti nuotrauką</string> - <string name="send_disappearing_message_5_minutes">5 minučių</string> - <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Mažiausiai bateriją eikvojantis variantas</b>. Jūs gausite sistemos pranešimus tik kai bus atidaryta programėlė (NEBUS fone veikiančios paslaugos).]]></string> - <string name="abort_switch_receiving_address">Atšaukti adreso keitimą</string> - <string name="v4_2_auto_accept_contact_requests">Automatiškai priimti susisiekimo užklausas</string> - <string name="v5_1_self_destruct_passcode_descr">Kai jį suvedate visi duomenys yra pašalinami.</string> - <string name="network_session_mode_entity_description">Atskiras TCP ryšys (ir SOCKS kredencialas) bus naudojamas <b>kiekvienam adresatui ir grupės nariui</b>. -\n<b>Prašome atkreipti dėmesį</b>: jei turite daug atskirų ryšių, akumuliatoriaus ir interneto sąnaudos gali būti žymiai didesnės, kartais ryšio gali visiškai nepavykti užmegzti.</string> + <string name="authentication_cancelled">Tapatybės patvirtinimo atsisakyta</string> + <string name="always_use_relay">Visada naudoti retransliavimą</string> + <string name="send_disappearing_message_5_minutes">5 minutės</string> + <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Mažiausios akumuliatoriaus sąnaudos</b>. Jūs gausite pranešimus tik tada, kai programėlė bus atverta (BE foninės tarnybos).]]></string> + <string name="abort_switch_receiving_address">Nutraukti adreso keitimą</string> + <string name="v4_2_auto_accept_contact_requests">Automatiškai priimti kontaktų užklausas</string> + <string name="v5_1_self_destruct_passcode_descr">Jį įrašius visi duomenys bus pašalinti.</string> + <string name="network_session_mode_entity_description"><b>Kiekvienam kontaktui ir grupės nariui</b> bus naudojamas atskiras TCP prisijungimas (ir SOCKS prisijungimo duomenys). +\n<b>Turėkite omenyje</b>: jei turite daug prisijungimų, akumuliatoriaus ir interneto duomenų sąnaudos gali būti žymiai didesnės ir, kartais, prisijungimai gali patirti nesėkmę.</string> <string name="contact_wants_to_connect_via_call">%1$s nori su jumis susisiekti per</string> - <string name="turning_off_service_and_periodic">Yra įjungtas akumuliatoriaus naudojimo optimizavimas, kuris išjungia fone veikiančią paslaugą ir periodines naujų pranešimų užklausas. Juos iš naujo įjungti galite programos nustatymuose.</string> - <string name="notifications_mode_service">Visad įjungta</string> - <string name="notifications_mode_off_desc">Programėlė gaus sistemos pranešimus tik tada, kai ji bus įjungta, jokia fone veikianti paslauga nebus paleista</string> + <string name="turning_off_service_and_periodic">Yra įjungtas akumuliatoriaus naudojimo optimizavimas, išjungiantis foninę tarnybą ir periodines užklausas apie naujas žinutes. Nustatymuose galite įjungti ją iš naujo.</string> + <string name="notifications_mode_service">Visada įjungta</string> + <string name="notifications_mode_off_desc">Programėlė galės gauti sistemos pranešimus tik tada, kai veiks. Jokia foninė tarnyba nebus paleidžiama</string> <string name="accept_contact_button">Priimti</string> - <string name="accept_contact_incognito_button">Priimti inkognito adresatą</string> - <string name="smp_servers_preset_add">Pridėti serverį iš sąrašo</string> - <string name="v4_2_group_links_desc">Administratoriai gali sukurti prisijungimo prie grupių nuorodas.</string> - <string name="notifications_mode_service_desc">Pastoviai fone veikianti paslauga. Sistemos pranešimai bus rodomi iš karto gavus žinutę.</string> - <string name="la_authenticate">Patvirtinti autentiškumą</string> - <string name="add_address_to_your_profile">Pridėkite adresą prie savo profilio, kad jūsų kontaktai galėtų bendrinti jį su kitais. Apie profilio atnaujinimą bus pranešta jūsų adresatams.</string> - <string name="icon_descr_video_asked_to_receive">Paprašė leidimo gauti vaizdo įrašą</string> - <string name="abort_switch_receiving_address_question">Ar norite atšaukti adreso keitimą\?</string> - <string name="abort_switch_receiving_address_desc">Adreso keitimas bus atšauktas. Bus naudojamas ankstusis gavimo adresas.</string> - <string name="send_disappearing_message_1_minute">1 minutės</string> + <string name="accept_contact_incognito_button">Priimti inkognito režimu</string> + <string name="smp_servers_preset_add">Pridėti iš anksto parinktus serverius</string> + <string name="v4_2_group_links_desc">Administratoriai gali kurti prisijungimo prie grupių nuorodas.</string> + <string name="notifications_mode_service_desc">Foninė tarnyba visada veikia – pranešimai bus rodomi iš karto, kai tik bus prieinamos žinutės.</string> + <string name="la_authenticate">Patvirtinti tapatybę</string> + <string name="add_address_to_your_profile">Pridėkite adresą prie savo profilio, kad jūsų kontaktai galėtų bendrinti jį su kitais žmonėmis. Apie profilio atnaujinimą bus pranešta jūsų kontaktams.</string> + <string name="abort_switch_receiving_address_question">Nutraukti adreso keitimą?</string> + <string name="abort_switch_receiving_address_desc">Adreso keitimas bus nutrauktas. Bus naudojamas senas priėmimo adresas.</string> + <string name="send_disappearing_message_1_minute">1 minutė</string> <string name="icon_descr_audio_on">Įjungti garsą</string> <string name="icon_descr_audio_off">Išjungti garsą</string> - <string name="all_app_data_will_be_cleared">Visi programos duomenys yra ištrinami.</string> - <string name="app_passcode_replaced_with_self_destruct">Programos prieigos kodas pakeičiamas susinaikinimo prieigos kodu.</string> - <string name="empty_chat_profile_is_created">Sukuriamas tuščias pokalbių profilis nurodytu pavadinimu ir programėlė atsidaro kaip įprasta.</string> + <string name="all_app_data_will_be_cleared">Visi programėlės duomenys bus ištrinti.</string> + <string name="empty_chat_profile_is_created">Sukuriamas tuščias pokalbių profilis nurodytu pavadinimu ir programėlė atveriama kaip įprasta.</string> <string name="settings_section_title_app">PROGRAMĖLĖ</string> - <string name="keychain_is_storing_securely">Saugiai saugoti slaptažodžiui yra naudojamas \"Android Keystore\". Jis jums leidžia gauti sistemos pranešimus.</string> + <string name="keychain_is_storing_securely">Saugiam slaptafrazės saugojimui yra naudojama „Android Keystore“ – tai įgalina pranešimų tarnybą veikti.</string> <string name="color_secondary_variant">Papildoma antrinė spalva</string> - <string name="color_primary_variant">Papildoma pagrindinė spalva</string> + <string name="color_primary_variant">Papildomas akcentavimas</string> <string name="allow_to_send_files">Leisti siųsti failus ir mediją.</string> - <string name="abort_switch_receiving_address_confirm">Atšaukti</string> - <string name="alert_text_decryption_error_too_many_skipped">Praleista %1$d pranešimų.</string> - <string name="all_your_contacts_will_remain_connected">Jūsų adresatai nepraras ryšio su jumis.</string> - <string name="callstatus_accepted">priimtas skambutis</string> + <string name="abort_switch_receiving_address_confirm">Nutraukti</string> + <string name="alert_text_decryption_error_too_many_skipped">Praleista %1$d žinučių.</string> + <string name="all_your_contacts_will_remain_connected">Jūsų kontaktai išliks prisijungę prie jūsų.</string> + <string name="callstatus_accepted">atsilieptas skambutis</string> <string name="accept">Priimti</string> - <string name="integrity_msg_skipped">Praleistas (-i) %1$d pranešimas (-ai)</string> - <string name="auto_accept_images">Automatiškai priimti nuotraukas</string> - <string name="full_backup">Programos duomenų atsarginė kopija</string> - <string name="color_primary">Pagrindinė spalva</string> - <string name="keychain_allows_to_receive_ntfs">Saugiai saugoti slaptažodžiui, naujai paleidus programą ar jį pakeitus, naudojamas \"Android Keystore\". Jis jums leis gauti sistemos pranešimus.</string> + <string name="integrity_msg_skipped">Praleista žinučių: %1$d.</string> + <string name="full_backup">Programėlės duomenų atsarginė kopija</string> + <string name="color_primary">Akcentavimas</string> + <string name="keychain_allows_to_receive_ntfs">Paleidus programėlę iš naujo ar pakeitus slaptafrazę, saugiam slaptafrazės saugojimui bus naudojama Android raktų saugykla – tai leis gauti pranešimus.</string> <string name="color_background">Fonas</string> <string name="send_disappearing_message_30_seconds">30 sekundžių</string> - <string name="allow_message_reactions_only_if">Leisti reaguoti į pranešimus tik, jei tai leidžia jūsų adresatas.</string> + <string name="allow_message_reactions_only_if">Leisti reaguoti į žinutes tik tuo atveju, jeigu tai leidžia jūsų kontaktas.</string> <string name="learn_more_about_address">Apie SimpleX adresą</string> - <string name="network_enable_socks_info">Ar norite prieiga prie serverių gauti per SOCKS tarpinį serverį naudojant prievadą %d\? Prieš įjungiant šią parinktį, tarpinis serveris turi būti paleistas.</string> - <string name="all_your_contacts_will_remain_connected_update_sent">Jūsų adresatai nepraras ryšio su jumis. Apie profilio pakeitimus bus pranešta jūsų adresatams.</string> - <string name="audio_call_no_encryption">garso skambutis (nėra užšifruotas nuo vieno galo iki kito galo)</string> - <string name="group_info_section_title_num_members">%1$s NARIŲ</string> + <string name="network_enable_socks_info">Ar gauti prieigą prie serverių per SOCKS įgaliotąjį serverį naudojant prievadą %d? Prieš įjungiant šią parinktį, privalo būti paleistas įgaliotasis serveris.</string> + <string name="all_your_contacts_will_remain_connected_update_sent">Jūsų kontaktai išliks prisijungę prie jūsų. Apie profilio atnaujinimą bus pranešta jūsų kontaktams.</string> + <string name="audio_call_no_encryption">garso skambutis (nėra užšifruotas visapusiškai)</string> + <string name="group_info_section_title_num_members">NARIŲ: %1$s</string> <string name="address_section_title">Adresas</string> <string name="accept_feature">Priimti</string> - <string name="allow_calls_only_if">Leisti skambučius tik, jei juos leidžia jūsų adresatas.</string> - <string name="conn_event_ratchet_sync_started">sutinkama dėl užšifravimo..</string> - <string name="snd_conn_event_ratchet_sync_started">sutinkama su %s šifravimu…</string> - <string name="allow_your_contacts_adding_message_reactions">Leisti mano adresatams reaguoti į pranešimus.</string> - <string name="allow_your_contacts_to_call">Leisti mano adresatams man skambinti.</string> - <string name="allow_message_reactions">Leisti reaguoti į pranešimus.</string> - <string name="v5_0_app_passcode">Programos prieigos kodas</string> - <string name="v5_1_better_messages">Geresni pranešimai</string> - <string name="la_auth_failed">Autentiškumo patvirtinimas nepavyko</string> - <string name="accept_connection_request__question">Ar norite priimti sujungimo užklausą\?</string> + <string name="allow_calls_only_if">Leisti skambučius tik tuo atveju, jeigu juos leidžia jūsų kontaktas.</string> + <string name="conn_event_ratchet_sync_started">sutariama dėl šifravimo…</string> + <string name="snd_conn_event_ratchet_sync_started">sutariama dėl %s šifravimo…</string> + <string name="allow_your_contacts_adding_message_reactions">Leisti jūsų kontaktams reaguoti į žinutes.</string> + <string name="allow_your_contacts_to_call">Leisti jūsų kontaktams jums skambinti.</string> + <string name="allow_message_reactions">Leisti reaguoti į žinutes.</string> + <string name="v5_1_better_messages">Geresnės žinutės</string> + <string name="la_auth_failed">Tapatybės patvirtinimas patyrė nesėkmę</string> + <string name="accept_connection_request__question">Priimti sujungimo užklausą?</string> <string name="one_time_link_short">vienkartinė nuoroda</string> - <string name="accept_call_on_lock_screen">Priimti</string> + <string name="accept_call_on_lock_screen">Atsiliepti</string> <string name="auto_accept_contact">Automatiškai priimti</string> <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Geriau baterijai</b>. Fono paslauga tikrina pranešimus kas 10 minučių. Galite praleisti skambučius arba skubius pranešimus.]]></string> <string name="callstatus_in_progress">vyksta skambutis</string> @@ -613,7 +596,1182 @@ <string name="invite_prohibited">Nepavyko pakviesti kontakto!</string> <string name="v4_5_transport_isolation_descr">Pagal pokalbių profilį (numatytieji nustatymai) arba pagal ryšį (BETA).</string> <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Naudoja daugiau baterijos</b>! Fono paslauga veikia visada - pranešimai rodomi, kai tik atsiranda žinučių.]]></string> - <string name="cannot_access_keychain">Negalima pasiekti \"Keystore\", kad išsaugotumėte duomenų bazės slaptažodį</string> + <string name="cannot_access_keychain">Negalima pasiekti "Keystore", kad išsaugotumėte duomenų bazės slaptažodį</string> <string name="icon_descr_cancel_file_preview">Atšaukti failo peržiūrą</string> <string name="icon_descr_cancel_image_preview">Atšaukti vaizdo peržiūrą</string> -</resources> \ No newline at end of file + <string name="share_text_database_id">Duomenų bazės ID: %d</string> + <string name="delete_and_notify_contact">Ištrinti ir pranešti kontaktui</string> + <string name="send_disappearing_message_custom_time">Tinkintas laikas</string> + <string name="v5_3_new_interface_languages_descr">Arabų, bulgarų, suomių, hebrajų, tajų ir ukrainiečių – dėkojame naudotojams ir Weblate.</string> + <string name="v5_3_new_interface_languages">6 naujos naudotojo sąsajos kalbos</string> + <string name="v5_1_custom_themes">Tinkinti apipavidalinimai</string> + <string name="multicast_connect_automatically">Prisijungti automatiškai</string> + <string name="chat_database_imported">Pokalbio duomenų bazė importuota</string> + <string name="connect_via_invitation_link">Prisijungti per vienkartinę nuorodą?</string> + <string name="icon_descr_close_button">Užvėrimo mygtukas</string> + <string name="devices">Įrenginiai</string> + <string name="connection_error_auth">Ryšio klaida (AUTH)</string> + <string name="disable_notifications_button">Išjungti pranešimus</string> + <string name="continue_to_next_step">Tęsti</string> + <string name="chat_database_deleted">Pokalbio duomenų bazė ištrinta</string> + <string name="choose_file_title">Pasirinkti failą</string> + <string name="contribute">Talkinti</string> + <string name="receipts_section_contacts">Kontaktai</string> + <string name="settings_developer_tools">Plėtotojo įrankiai</string> + <string name="clear_note_folder_warning">Visos žinutės bus ištrintos – to bus neįmanoma atšaukti!</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Pridėti kontaktą</b>: sukurti naują pakvietimo nuorodą arba prisijungti per gautą nuorodą.]]></string> + <string name="camera_not_available">Fotoaparatas neprieinamas</string> + <string name="contacts_can_mark_messages_for_deletion">Kontaktai gali pažymėti žinutes ištrynimui; jūs galėsite jas peržiūrėti.</string> + <string name="create_another_profile_button">Sukurti profilį</string> + <string name="creating_link">Kuriama nuoroda…</string> + <string name="deleted_description">ištrinta</string> + <string name="delivery_receipts_are_disabled">Pranešimai apie pristatymą yra išjungti!</string> + <string name="turn_off_battery_optimization_button">Leisti</string> + <string name="in_developing_title">Jau greitai!</string> + <string name="developer_options_section">Plėtotojo parinktys</string> + <string name="colored_text">spalvotas</string> + <string name="connect_plan_already_joining_the_group">Jau prisijungiama prie grupės!</string> + <string name="connect_plan_connect_via_link">Prisijungti per nuorodą?</string> + <string name="create_simplex_address">Sukurti SimpleX adresą</string> + <string name="ttl_h">%dval.</string> + <string name="agent_critical_error_title">Kritinė klaida</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Sukurti grupę</b>: sukurti naują grupę.]]></string> + <string name="add_contact_tab">Pridėti kontaktą</string> + <string name="customize_theme_title">Tinkinti apipavidalinimą</string> + <string name="chat_database_section">POKALBIO DUOMENŲ BAZĖ</string> + <string name="v4_6_chinese_spanish_interface">Naudotojo sąsaja kinų ir ispanų kalbomis</string> + <string name="delivery_receipts_title">Pranešimai apie pristatymą!</string> + <string name="auth_disable_simplex_lock">Išjungti SimpleX užraktą</string> + <string name="delivery">Pristatymas</string> + <string name="change_lock_mode">Keisti užrakto veikseną</string> + <string name="ttl_d">%dd.</string> + <string name="disappearing_message">Išnykstanti žinutė</string> + <string name="receipts_contacts_title_disable">Išjungti pranešimus apie pristatymą?</string> + <string name="send_receipts_disabled">išjungta</string> + <string name="v5_4_better_groups">Geresnės grupės</string> + <string name="v5_4_incognito_groups_descr">Sukurti grupę naudojant atsitiktinį profilį.</string> + <string name="v5_3_encrypt_local_files_descr">Programėlė šifruoja naujus vietinius failus (išskyrus vaizdo įrašus).</string> + <string name="connect_plan_already_connecting">Jau jungiamasi!</string> + <string name="connect_via_member_address_alert_title">Prisijungti tiesiogiai?</string> + <string name="create_group_button">Sukurti grupę</string> + <string name="dark_theme">Tamsus apipavidalinimas</string> + <string name="feature_off">išjungta</string> + <string name="turn_off_system_restriction_button">Atverti programėlės nustatymus</string> + <string name="chat_preferences_off">išjungta</string>` + <string name="loading_chats">Įkeliami pokalbiai…</string> + <string name="only_group_owners_can_enable_voice">Tik grupės savininkai gali įjungti balso žinutes.</string> + <string name="thousand_abbreviation">tūkst.</string> + <string name="paste_button">Įdėti</string> + <string name="v5_5_message_delivery">Pagerintas žinučių pristatymas</string> + <string name="settings_notification_preview_title">Pranešimo peržiūra</string> + <string name="loading_remote_file_title">Įkeliamas failas</string> + <string name="notifications">Pranešimai</string> + <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Jei gavote SimpleX Chat pakvietimo nuorodą, galite ją atverti naršyklėje:</string> + <string name="icon_descr_more_button">Daugiau</string> + <string name="paste_the_link_you_received">Įdėkite gautą nuorodą</string> + <string name="invalid_qr_code">Neteisingas QR kodas</string> + <string name="network_settings_title">Tinklo nustatymai</string> + <string name="rcv_group_event_member_left">išėjo</string> + <string name="v4_5_reduced_battery_usage_descr">Daugiau patobulinimų jau greitai!</string> + <string name="v5_5_join_group_conversation">Prisijunkite prie pokalbių grupėse</string> + <string name="connect_plan_open_group">Atverti grupę</string> + <string name="invite_friends">Pakviesti draugus</string> + <string name="only_you_can_make_calls">Tik jūs galite skambinti.</string> + <string name="v5_0_polish_interface">Naudotojo sąsaja lenkų kalba</string> + <string name="v5_2_more_things_descr">- stabilesnis žinučių pristatymas. +\n- šiek tiek geresnės grupės. +\n- ir daugiau!</string> + <string name="only_your_contact_can_make_calls">Tik jūsų kontaktas gali skambinti.</string> + <string name="contact_developers">Atnaujinkite programėlę ir susisiekite su plėtotojais.</string> + <string name="open_verb">Atverti</string> + <string name="chat_item_ttl_none">niekada</string> + <string name="learn_more">Sužinoti daugiau</string> + <string name="opening_database">Atveriama duomenų bazė…</string> + <string name="agent_internal_error_title">Vidinė klaida</string> + <string name="import_theme">Importuoti apipavidalinimą</string> + <string name="only_you_can_send_voice">Tik jūs galite siųsti balso žinutes.</string> + <string name="v4_5_multiple_chat_profiles">Daug pokalbio profilių</string> + <string name="ok">Gerai</string> + <string name="v4_5_private_filenames">Privatūs failų pavadinimai</string> + <string name="info_menu">Informacija</string> + <string name="open_simplex_chat_to_accept_call">Atverkite SimpleX Chat norėdami atsiliepti</string> + <string name="desktop_incompatible_version">Nesuderinama versija</string> + <string name="non_content_uri_alert_title">Neteisingas failo kelias</string> + <string name="settings_notifications_mode_title">Pranešimų tarnyba</string> + <string name="notification_preview_new_message">nauja žinutė</string> + <string name="network_use_onion_hosts_no">Ne</string> + <string name="open_database_folder">Atverti duomenų bazės aplanką</string> + <string name="joining_group">Prisijungiama prie grupės</string> + <string name="group_welcome_preview">Peržiūra</string> + <string name="v4_5_italian_interface">Naudotojo sąsaja italų kalba</string> + <string name="v4_6_reduced_battery_usage_descr">Daugiau patobulinimų jau greitai!</string> + <string name="v5_1_japanese_portuguese_interface">Naudotojo sąsaja japonų ir italų kalbomis</string> + <string name="new_chat">Naujas pokalbis</string> + <string name="display_name__field">Profilio pavadinimas:</string> + <string name="prohibit_message_deletion">Uždrausti negrįžtamai ištrinti žinutes.</string> + <string name="network_and_servers">Tinklas ir serveriai</string> + <string name="group_member_status_left">išėjo</string> + <string name="group_member_role_owner">savininkas</string> + <string name="chat_preferences_no">ne</string> + <string name="prohibit_calls">Uždrausti garso/vaizdo skambučius.</string> + <string name="only_you_can_send_disappearing">Tik jūs galite siųsti išnykstančias žinutes.</string> + <string name="only_your_contact_can_send_disappearing">Tik jūsų kontaktas gali siųsti išnykstančias žinutes.</string> + <string name="this_device_version"><![CDATA[<i>(šis įrenginys v%s)</i>]]></string> + <string name="only_one_device_can_work_at_the_same_time">Vienu metu gali veikti tik vienas įrenginys</string> + <string name="not_compatible">Nesuderinama!</string> + <string name="rcv_group_event_open_chat">Atverti</string> + <string name="open_chat">Atverti pokalbį</string> + <string name="network_status">Tinklo būsena</string> + <string name="email_invite_subject">Bendraukime per Simplex Chat</string> + <string name="message_too_large">Žinutė per didelė</string> + <string name="new_in_version">Štai kas naujo %s versijoje</string> + <string name="only_your_contact_can_send_voice">Tik jūsų kontaktas gali siųsti balso žinutes.</string> + <string name="only_owners_can_enable_files_and_media">Tik grupės savininkai gali įjungti failus ir mediją.</string> + <string name="only_group_owners_can_change_prefs">Tik grupės savininkai gali keisti grupės nuostatas.</string> + <string name="import_theme_error">Apipavidalinimo importavimo klaida</string> + <string name="network_error_desc">Prašome pasitikrinti savo tinklo ryšį su %1$s ir bandykite vėl.</string> + <string name="notifications_mode_periodic_desc">Tikrina naujas žinutes kas 10 minučių iki 1 minutės</string> + <string name="la_no_app_password">Nėra programėlės prieigos kodo</string> + <string name="group_invitation_expired">Grupės pakvietimas nebegalioja</string> + <string name="rcv_group_event_invited_via_your_group_link">pakviestas per jūsų grupės nuorodą</string> + <string name="v5_6_safer_groups_descr">Administratoriai gali užblokuoti narį visiems.</string> + <string name="block_member_desc">Visos naujos žinutės nuo %s bus paslėptos!</string> + <string name="permissions_camera_and_record_audio">Fotoaparatas ir mikrofonas</string> + <string name="rcv_group_event_changed_member_role">pakeitė %s rolę į %s</string> + <string name="rcv_group_event_changed_your_role">pakeitė tavo rolę į %s</string> + <string name="connect_plan_join_your_group">Prisijungti prie savo grupės?</string> + <string name="connected_desktop">Prijungtas darbastalis</string> + <string name="rcv_group_event_member_created_contact">prisijungė tiesiogiai</string> + <string name="connected_to_desktop">Prisijungta prie darbastalio</string> + <string name="connecting_to_desktop">Jungiamasi prie darbastalio</string> + <string name="connection_request_sent">Prisijungimo užklausa išsiųsta!</string> + <string name="connection_local_display_name">prisijungimas %1$d</string> + <string name="note_folder_local_display_name">Privatūs užrašai</string> + <string name="connection_timeout">Ryšiui skirtas laikas</string> + <string name="invalid_connection_link">Netinkama prisijungimo nuoroda</string> + <string name="smp_server_test_delete_queue">Ištrinti eilę</string> + <string name="smp_server_test_compare_file">Palyginti failą</string> + <string name="periodic_notifications">Periodiniai pranešimai</string> + <string name="periodic_notifications_disabled">Periodiniai pranešimai yra išjungti!</string> + <string name="notification_preview_mode_hidden">Paslėptas</string> + <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Įrenginio autentifikacija neįjungta. Galite įjungti SimpleX užraktą nustatymuose, įjungus įrenginio autentifikaciją.</string> + <string name="auth_open_migration_to_another_device">Atidaryti perkėlimo ekraną</string> + <string name="delete_member_message__question">Ištrinti nario žinutę?</string> + <string name="chat_with_developers">Kalbėti su kūrėjais</string> + <string name="icon_descr_asked_to_receive">Paprašyta gauti nuotrauką</string> + <string name="loading_remote_file_desc">Prašome palaukti kol failas yra kraunamas iš susieto mobiliojo</string> + <string name="favorite_chat">Mėgstamas</string> + <string name="delete_pending_connection__question">Ištrinti laukiantį prisijungimą?</string> + <string name="create_chat_profile">Sukurti pokalbių profilį</string> + <string name="network_disable_socks_info">Jei patvirtinsite, žinučių serveriai galės matyti jūsų IP adresą, ir jūsų tiekėjas - prie kurių serverių jungiatės.</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Atkreipkite dėmesį</b>: žinučių ir failų perdavimas yra prijungtas per SOCKS tarpinį serverį. Skambučiams ir nuorodų peržiūrų siuntimui naudojamas tiesioginis ryšys.]]></string> + <string name="invalid_name">Netinkamas vardas!</string> + <string name="create_your_profile">Sukurti savo profilį</string> + <string name="people_can_connect_only_via_links_you_share">Asmenys gali prisijungti prie jūsų tik per nuorodas kuriomis dalinatės.</string> + <string name="onboarding_notifications_mode_subtitle">Tai gali būti pakeista vėliau nustatymuose.</string> + <string name="alert_text_skipped_messages_it_can_happen_when">Tai gali įvykti kai: +\n1. Žinutės galiojimo laikas baigėsi siuntėjo programoje po 2 dienų arba serveryje po 30 dienų. +\n2. Žinutės atšifravimas nepavyko, nes jūs ar jūsų kontaktas naudojo seną duomenų bazės atsarginę kopiją. +\n3. Prisijungimas buvo kompromisuotas.</string> + <string name="passcode_changed">Prieigos kodas pakeistas!</string> + <string name="confirm_new_passphrase">Patvirtinti naują slaptafrazę…</string> + <string name="enter_correct_current_passphrase">Prašome įvesti tinkamą dabartinę slaptafrazę.</string> + <string name="keychain_error">Raktų grandinės klaida</string> + <string name="profile_update_event_contact_name_changed">kontaktas %1$s pasikeitė į %2$s</string> + <string name="conn_event_enabled_pq">Kvantiškai atsparus visapusis šifravimas</string> + <string name="delete_group_for_all_members_cannot_undo_warning">Grupė bus ištrinta visiems nariams - tai negali būti atšaukta!</string> + <string name="delete_group_for_self_cannot_undo_warning">Grupė bus ištrinta jums - tai negali būti atšaukta!</string> + <string name="info_row_created_at">Sukurta</string> + <string name="item_info_current">(dabartinis)</string> + <string name="share_text_deleted_at">Ištrinta: %s</string> + <string name="fix_connection_not_supported_by_group_member">Grupės narys nepalaiko sutaisymo</string> + <string name="network_option_ping_count">PING skaičius</string> + <string name="incognito_info_allows">Tai leidžia daug anonimiškų prisijungimų nesidalinant jokiais bendrais duomenimis tarp jų, tame pačiame pokalbių profilyje.</string> + <string name="prohibit_sending_voice">Uždrausti siųsti balso žinutes.</string> + <string name="group_members_can_add_message_reactions">Grupės nariai gali reaguoti į žinutes.</string> + <string name="ttl_day">%d diena</string> + <string name="v4_4_verify_connection_security_desc">Palyginkite saugumo kodus su savo kontaktais.</string> + <string name="v5_0_app_passcode_descr">Nustatykite jį vietoje sistemos autentifikacijos.</string> + <string name="v5_1_custom_themes_descr">Tinkinkite ir bendrinkite spalvų temas.</string> + <string name="v5_2_fix_encryption">Pasilikite savo prisijungimus</string> + <string name="v5_2_fix_encryption_descr">Sutaisykite šifravimą po atsarginių kopijų atstatymo.</string> + <string name="v5_4_incognito_groups">Inkognito grupės</string> + <string name="v5_5_simpler_connect_ui">Įklijuokite nuorodą, kad prisijungti!</string> + <string name="v5_5_new_interface_languages">Vengrų ir turkų UI</string> + <string name="custom_time_picker_custom">pasirinktinis</string> + <string name="desktop_device">Darbastalis</string> + <string name="invalid_file_link">Netinkama nuoroda</string> + <string name="agent_critical_error_desc">Prašome pranešti tai kūrėjams: +\n%s +\n +\nRekomenduojama paleisti programėlę iš naujo.</string> + <string name="migrate_to_device_database_init">Ruošiamas atsiuntimas</string> + <string name="migrate_to_device_import_failed">Importas nepavyko</string> + <string name="migrate_to_device_apply_onion">Taikyti</string> + <string name="migrate_from_device_exported_file_doesnt_exist">Eksportuotas failas neegzistuoja</string> + <string name="migrate_from_device_database_init">Ruošiamas įkėlimas</string> + <string name="migrate_from_device_archive_and_upload">Archyvuoti ir įkelti</string> + <string name="migrate_from_device_delete_database_from_device">Ištrinti duomenų bazę iš šio įrengino</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Įspėjimas</b>: archyvas bus ištrintas.]]></string> + <string name="or_scan_qr_code">Arba nuskanuokite QR kodą</string> + <string name="v5_0_app_passcode">Programėlės prieigos kodas</string> + <string name="connect_plan_connect_to_yourself">Prisijungti prie savęs?</string> + <string name="alert_title_contact_connection_pending">Kontaktas dar nėra susijungęs!</string> + <string name="correct_name_to">Pataisyti pavadinimą į %s?</string> + <string name="icon_descr_server_status_pending">Laukiama</string> + <string name="database_encryption_will_be_updated">Duomenų bazės šifravimo slaptafrazė bus atnaujinta ir saugoma raktų saugykloje.</string> + <string name="remote_host_error_bad_state"><![CDATA[Prisijungimas prie mobiliojo <b>%s</b> yra blogos būsenos]]></string> + <string name="blocked_by_admin_item_description">užblokuota administratoriaus</string> + <string name="simplex_link_mode_full">Pilna nuoroda</string> + <string name="immune_to_spam_and_abuse">Atsparu šlamštui ir piktnaudžiavimui</string> + <string name="alert_text_fragment_please_report_to_developers">Prašome pranešti tai kūrėjams.</string> + <string name="store_passphrase_securely_without_recover">Prašome saugoti slaptafrazę saugiai, jūs NEGALĖSITE pasiekti pokalbių, jei ją prarasite.</string> + <string name="database_passphrase_is_required">Duomenų bazės slaptafrazė yra reikalinga pokalbių atidarymui.</string> + <string name="restore_database_alert_desc">Prašome įvesti praeitą slaptažodį po duomenų bazės atsarginės kopijos atstatymo. Šis veiksmas negali būti atšauktas.</string> + <string name="mtr_error_no_down_migration">duomenų bazė naujesnė nei programėlė, bet nėra perkėlimo į senesnę versiją: %s</string> + <string name="mtr_error_different">skirtinga migracija programėlėje/duomenų bazėje: %s / %s</string> + <string name="icon_descr_group_inactive">Grupė neaktyvi</string> + <string name="info_row_received_at">Gauta</string> + <string name="member_blocked_by_admin">Užblokuota administratoriaus</string> + <string name="ttl_hour">%d valanda</string> + <string name="remote_ctrl_error_busy">Darbastalis yra užsiėmęs</string> + <string name="how_to_use_simplex_chat">Kaip tai naudoti</string> + <string name="how_to_use_your_servers">Kaip naudoti savo serverius</string> + <string name="hidden_profile_password">Paslėpto profilio slaptažodis</string> + <string name="incorrect_passcode">Neteisingas prieigos kodas</string> + <string name="if_you_enter_self_destruct_code">Jei įvesite savaiminio susinaikinimo prieigos kodą programėlės atidarymo metu:</string> + <string name="prohibit_sending_files">Uždrausti siųsti failus ir mediją.</string> + <string name="v5_1_message_reactions_descr">Pagaliau, mes juos turime! 🚀</string> + <string name="desktop_address">Darbastalio adresas</string> + <string name="call_service_notification_audio_call">Garso skambutis</string> + <string name="auth_open_chat_console">Atidaryti pokalbių konsolę</string> + <string name="auth_open_chat_profiles">Atidaryti pokalbių profilius</string> + <string name="prohibit_message_reactions_group">Uždrausti žinučių reakcijas.</string> + <string name="rcv_group_event_group_deleted">ištrynė grupę</string> + <string name="delete_messages__question">Ištrinti %d žinutes?</string> + <string name="confirm_password">Patvirtinti slaptažodį</string> + <string name="icon_descr_hang_up">Padėti ragelį</string> + <string name="auth_device_authentication_is_disabled_turning_off">Įrenginio autentifikacija išjungta. Išjungiamas SimpleX užraktas.</string> + <string name="total_files_count_and_size">%d failas (-ai), kurių bendras dydis yra %s</string> + <string name="ttl_hours">%d valandos</string> + <string name="connect_via_link_incognito">Prisijungti inkognito režimu</string> + <string name="enter_passphrase_notification_title">Reikalinga slaptafrazė</string> + <string name="prohibit_sending_voice_messages">Uždrausti siųsti balso žinutes.</string> + <string name="settings_section_title_experimenta">EKSPERIMENTINIS</string> + <string name="v5_0_large_files_support_descr">Greitai ir nelaukiant kol siuntėjas prisijungs!</string> + <string name="files_and_media">Failai ir medija</string> + <string name="files_are_prohibited_in_group">Failai ir medija yra draudžiami šioje grupėje.</string> + <string name="file_will_be_received_when_contact_completes_uploading">Failas bus gautas kai jūsų kontaktas užbaigs įkėlimą.</string> + <string name="permissions_required">Suteikti leidimus</string> + <string name="group_profile_is_stored_on_members_devices">Grupės profilis yra saugomas narių įrenginiuose, ne serveriuose.</string> + <string name="icon_descr_video_asked_to_receive">Paprašyta gauti vaizdo įrašą</string> + <string name="email_invite_body">Labas! +\nPrisijunk prie manęs per SimpleX Chat: %s</string> + <string name="privacy_redefined">Privatumas apibrėžtas iš naujo</string> + <string name="share_text_created_at">Sukurta: %s</string> + <string name="group_member_status_intro_invitation">jungiamasi (supažindinimo pakvietimas)</string> + <string name="how_to_use_markdown">Kaip naudoti markdown</string> + <string name="self_destruct_passcode_changed">Savaiminio susinaikinimo prieigos kodas pakeistas!</string> + <string name="keep_unused_invitation_question">Pasilikti nepanaudotą pakvietimą?</string> + <string name="keep_invitation_link">Pasilikti</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Atviro kodo protokolas ir kodas - bet kas gali paleisti savo serverius.</string> + <string name="v5_4_more_things_descr">- pasirinktinai praneškite ištrintiems kontaktams. +\n- profilių vardai su tarpais. +\n- ir daugiau!</string> + <string name="paste_archive_link">Įklijuoti archyvo nuorodą</string> + <string name="ttl_days">%d dienos</string> + <string name="image_descr">Nuotrauka</string> + <string name="image_will_be_received_when_contact_is_online">Nuotrauka bus gauta kai jūsų kontaktas yra prisijungęs, palaukite arba patikrinkite vėliau!</string> + <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Kontaktas ir visos žinutės bus ištrintos - tai negali būti atšaukta!</string> + <string name="clear_note_folder_question">Išvalyti privačius užrašus?</string> + <string name="clear_verification">Išvalyti patvirtinimą</string> + <string name="chat_console">Pokalbių konsolė</string> + <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 darbastalis: nuskanuokite rodomą QR kodą iš programėlės, su <b>Skanuoti QR kodą</b>.]]></string> + <string name="how_to">Kaip tai daryti</string> + <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Jei pasirinksite atmesti, siuntėjui nebus apie tai pranešta.</string> + <string name="gallery_image_button">Nuotrauka</string> + <string name="rate_the_app">Įvertinti programėlę</string> + <string name="callstatus_connecting">jungiamas skambutis…</string> + <string name="create_address_and_let_people_connect">Sukurkite adresą, kad leistumėte žmonėms prisijungti prie jūsų.</string> + <string name="developer_options">Duomenų bazės ID ir transporto izoliacijos parinktis.</string> + <string name="italic_text">kursyvinis</string> + <string name="you_can_change_it_later">Atsitiktinė slaptafrazė yra saugoma nustatymuose kaip paprastas tekstas. +\nJūs galite tai pakeisti vėliau.</string> + <string name="chat_is_running">Pokalbiai veikia</string> + <string name="chat_is_stopped_you_should_transfer_database">Pokalbiai sustabdyti. Jei jau naudojote šią duomenų bazę kitame įrenginyje, turėtumėte perkelti ją atgal prieš pradedant pokalbius.</string> + <string name="rcv_group_event_member_blocked">užblokavo %s</string> + <string name="switch_receiving_address">Keisti gavimo adresą</string> + <string name="fix_connection">Sutaisyti prisijungimą</string> + <string name="prohibit_sending_disappearing_messages">Uždrausti siųsti išnykstančias žinutes.</string> + <string name="v4_6_group_welcome_message">Grupės sveikinimo žinutė</string> + <string name="v4_6_group_moderation">Grupės moderacija</string> + <string name="v5_4_block_group_members">Blokuokite grupės narius</string> + <string name="v5_1_self_destruct_passcode">Savaiminio susinaikinimo prieigos kodas</string> + <string name="v5_4_better_groups_descr">Greitesnis prisijungimas ir patikimesnės žinutės.</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Atkreipkite dėmesį</b>: naudojant tą pačią duomenų bazę dviejuose įrenginiuose, žinučių iššifravimas iš jūsų prisijungimų bus sugadintas saugumo sumetimais.]]></string> + <string name="failed_to_create_user_invalid_title">Netinkamas rodomas vardas!</string> + <string name="la_change_app_passcode">Keisti prieigos kodą</string> + <string name="la_enter_app_passcode">Įvesti prieigos kodą</string> + <string name="in_reply_to">Atsakymas į</string> + <string name="self_destruct_passcode_enabled">Savaiminio susinaikinimo prieigos kodas įjungtas!</string> + <string name="change_self_destruct_mode">Keisti savaiminio susinaikinimo rėžimą</string> + <string name="group_preview_join_as">prisijungti kaip %s</string> + <string name="database_downgrade">Duomenų bazės pasendinimas</string> + <string name="migrate_to_device_confirm_network_settings_footer">Prašome patvirtinti, kad tinklo nustatymai yra teisingi šiam įrenginiui.</string> + <string name="blocked_item_description">užblokuotas</string> + <string name="display_name_invited_to_connect">pakviestas prisijungti</string> + <string name="description_via_contact_address_link_incognito">Inkognito per kontakto adreso nuorodą</string> + <string name="simplex_link_mode_browser_warning">Nuorodos atidarymas naršyklėje gali sumažinti ryšio privatumą ir saugumą. Nepatikimos SimpleX nuorodos bus raudonos.</string> + <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Prašome pasitikrinti, ar naudojate tinkamą nuorodą, arba paprašykite savo kontakto, kad atsiųstų jums dar vieną.</string> + <string name="la_lock_mode_passcode">Prieigos kodo įvedimas</string> + <string name="la_please_remember_to_store_password">Prašome prisiminti arba saugoti tai saugiai - čia nėra būdo atkurti prarastą slaptažodį!</string> + <string name="la_current_app_passcode">Dabartinis prieigos kodas</string> + <string name="edit_history">Istorija</string> + <string name="received_message">Gauta žinutė</string> + <string name="files_and_media_prohibited">Failai ir medija yra draudžiami!</string> + <string name="image_saved">Nuotrauka išsaugota į galeriją</string> + <string name="file_will_be_received_when_contact_is_online">Failas bus gautas kai jūsų kontaktas yra prisijungęs, palaukite arba patikrinkite vėliau!</string> + <string name="smp_servers_preset_address">Iš anksto nustatyto serverio adresas</string> + <string name="smp_servers_preset_server">Iš anksto nustatytas serveris</string> + <string name="decentralized">Decentralizuota</string> + <string name="submit_passcode">Pateikti</string> + <string name="join_group_incognito_button">Prisijungti inkognito režimu</string> + <string name="initial_member_role">Pradinė rolė</string> + <string name="share_text_received_at">Gauta: %s</string> + <string name="message_deletion_prohibited">Negrįžtamas žinučių ištrynimas yra draudžiamas šiame pokalbyje.</string> + <string name="prohibit_sending_disappearing">Uždrausti siųsti išnykstančias žinutes.</string> + <string name="v4_6_reduced_battery_usage">Toliau sumažinome akumuliatoriaus naudojimą</string> + <string name="v4_6_hidden_chat_profiles">Paslėpti pokalbių profiliai</string> + <string name="v5_2_favourites_filter_descr">Filtruokite neperskaitytus ir mėgstamus pokalbius.</string> + <string name="v5_2_favourites_filter">Raskite pokalbius greičiau</string> + <string name="database_migration_in_progress">Duomenų bazės perkėlimas yra vykdomas. +\nTai gali užtrukti kelias minutes.</string> + <string name="auth_confirm_credential">Patvirtinkite savo kredencialus</string> + <string name="icon_descr_image_snd_complete">Nuotrauka išsiųsta</string> + <string name="image_will_be_received_when_contact_completes_uploading">Nuotrauka bus gauta kai jūsų kontaktas užbaigs jos įkėlimą.</string> + <string name="switch_receiving_address_question">Pakeisti gavimo adresą?</string> + <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Jei negalite susitikti asmeniškai, galite <b>nuskanuoti QR kodą vaizdo skambutyje</b>, arba jūsų kontaktas gali pasidalinti pakvietimo nuoroda.]]></string> + <string name="or_show_this_qr_code">Arba parodykite šį kodą</string> + <string name="enter_one_ICE_server_per_line">ICE serveriai (vienas per liniją)</string> + <string name="install_simplex_chat_for_terminal">Įdiegti SimpleX Chat terminalui</string> + <string name="delete_image">Ištrinti nuotrauką</string> + <string name="password_to_show">Rodyti slaptažodį</string> + <string name="passcode_set">Prieigos kodas nustatytas!</string> + <string name="database_encryption_will_be_updated_in_settings">Duomenų bazės šifravimo slaptafrazė bus atnaujinta ir saugoma nustatymuose.</string> + <string name="invalid_migration_confirmation">Netinkamas perkelimo patvirtinimas</string> + <string name="incognito_info_protects">Inkognito režimas apsaugo jūsų privatumą, naudodamas naują atsitiktinį profilį kiekvienam kontaktui.</string> + <string name="whats_new_read_more">Skaityti daugiau</string> + <string name="group_members_can_send_files">Grupės nariai gali siųsti failus ir mediją.</string> + <string name="full_name__field">Pilnas vardas:</string> + <string name="group_member_status_complete">užbaigti</string> + <string name="group_member_status_announced">jungiamasi (paskelbtas)</string> + <string name="prohibit_direct_messages">Uždrausti siųsti tiesioginias žinutes nariams.</string> + <string name="status_contact_has_e2e_encryption">Kontaktas turi visapusį šifravimą</string> + <string name="confirm_passcode">Patvirtinti prieigos kodą</string> + <string name="auto_accept_images">Automatiškai priimti nuotraukas</string> + <string name="enabled_self_destruct_passcode">Įjungti savaiminio susinaikinimo prieigos kodą</string> + <string name="la_mode_passcode">Prieigos kodas</string> + <string name="self_destruct_passcode">Savaiminio susinaikinimo prieigos kodas</string> + <string name="la_app_passcode">Programėlės prieigos kodas</string> + <string name="app_passcode_replaced_with_self_destruct">Programėlės prieigos kodas pakeičiamas savaiminio susinaikinimo prieigos kodu.</string> + <string name="export_theme">Eksportuoti temą</string> + <string name="only_your_contact_can_add_message_reactions">Tik jūsų kontaktas gali reaguoti į žinutes.</string> + <string name="v5_3_new_desktop_app_descr">Susikurkite naują proflį darbastalio programoje. 💻</string> + <string name="connected_mobile">Prijungtas mobilusis</string> + <string name="connected_to_mobile">Prisijungta prie mobiliojo</string> + <string name="paste_desktop_address">Įklijuoti darbastalio nuorodą</string> + <string name="remote_ctrl_error_disconnected">Darbastalis buvo atjungtas</string> + <string name="remote_ctrl_error_bad_version">Darbastalis turi nepalaikomą versiją. Įsitikinkite, kad naudojate tą pačią versiją abiejuose įrenginiuose.</string> + <string name="remote_ctrl_error_bad_invitation">Darbastalis turi neteisingą pakvietimo kodą</string> + <string name="database_will_be_encrypted_and_passphrase_stored">Duomenų bazė bus ušifruota ir slaptafrazė bus saugoma raktų saugykloje.</string> + <string name="icon_descr_instant_notifications">Momentiniai pranešimai</string> + <string name="service_notifications">Momentiniai pranešimai!</string> + <string name="service_notifications_disabled">Momentiniai pranešimai yra išjungti!</string> + <string name="icon_descr_profile_image_placeholder">profilio nuotraukos vietos ženklas</string> + <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Jei negalite susitkti asmeniškai, <b>parodykite QR kodą vaizdo skambutyje</b>, arba pasidalinkite nuoroda.]]></string> + <string name="read_more_in_user_guide_with_link"><![CDATA[Skaitykite daugiau <font color="#0088ff">naudotojo vadove</font>.]]></string> + <string name="if_you_cant_meet_in_person">Jei negalite susitikti asmeniškai, parodykite QR kodą vaizdo skambutyje, arba pasidalinkite nuoroda.</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Įklijuokite nuorodą, kurią gavote prisijungimui prie savo kontakto…</string> + <string name="callstate_received_answer">gautas atsakas…</string> + <string name="icon_descr_call_connecting">Jungiamas skambutis</string> + <string name="alert_text_fragment_encryption_out_of_sync_old_database">Tai gali įvykti kai jūs ar jūsų prisijungimas naudojo seną duomenų bazės atsarginę kopiją.</string> + <string name="if_you_enter_passcode_data_removed">Jei įvesite šį prieigos kodą programėlės atidarymo metu, visa programėlės informacija bus negrįžtamai pašalinta!</string> + <string name="set_passcode">Nustatyti prieigos kodą</string> + <string name="delete_files_and_media_for_all_users">Ištrinti failus visiems pokalbių profiliams</string> + <string name="restore_passphrase_not_found_desc">Slaptafrazė nebuvo rasta raktų saugykloje, prašome įvesti ją rankiniu būdu. Taip galėjo nutikti, jei atkūrėte programėlės duomenis naudojant atsarginės kopijos įrankį. Jei tai nėra tiesa, prašome susisiekti su kūrėjais.</string> + <string name="rcv_direct_event_contact_deleted">ištrintas kontaktas</string> + <string name="group_member_status_invited">pakviestas</string> + <string name="info_row_deleted_at">Ištrinta</string> + <string name="section_title_for_console">KONSOLEI</string> + <string name="block_member_confirmation">Blokuoti</string> + <string name="network_option_protocol_timeout">Protokolui skirtas laikas</string> + <string name="chat_preferences_default">numatyta (%s)</string> + <string name="group_members_can_delete">Grupės nariai gali negrįžtamai ištrinti išsiųstas žinutes. (24 valandas)</string> + <string name="message_deletion_prohibited_in_chat">Negrįžtamas žinučių ištrynimas yra draudžiamas šioje grupėje.</string> + <string name="connect_plan_group_already_exists">Grupė jau egzistuoja!</string> + <string name="bad_desktop_address">Blogas darbastalio adresas</string> + <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Atidarykite <i>Naudoti iš darbastalio</i> mobilioje programėlėje ir nuskanuokite QR kodą.]]></string> + <string name="desktop_app_version_is_incompatible">Darbastalio programėlės versija %s nėra suderinama su šia programėle.</string> + <string name="group_member_role_author">autorius</string> + <string name="send_receipts_disabled_alert_title">Kvitai yra išjungti</string> + <string name="v5_3_discover_join_groups_descr">- prisijunkite prie katalogo tarnybos (BETA)! +\n- pristatymo kvitai (iki 20 narių). +\n- greičiau ir stabiliau.</string> + <string name="moderated_items_description">%1$d žinutė buvo moderuota %2$s</string> + <string name="found_desktop">Rastas darbastalis</string> + <string name="store_passphrase_securely">Prašome saugoti slaptafrazę saugiai, jūs NEGALĖSITE jos pakeisti, jei ją prarasite.</string> + <string name="rcv_group_events_count">%d grupės įvykiai</string> + <string name="image_descr_profile_image">profilio nuotrauka</string> + <string name="profile_update_will_be_sent_to_contacts">Profilio atnaujinimas bus nusiųstas jūsų kontaktams.</string> + <string name="v5_6_quantum_resistant_encryption">Kvantiškai atsparus visapusis šifravimas</string> + <string name="connect_with_contact_name_question">Susisiekti su %1$s?</string> + <string name="member_info_member_blocked">užblokuotas</string> + <string name="block_for_all">Blokuoti visiems</string> + <string name="block_member_question">Blokuoti narį?</string> + <string name="migrate_from_device_archiving_database">Archyvuojama duomenų bazė</string> + <string name="migrate_from_device_check_connection_and_try_again">Patikrinkite savo interneto ryšį ir bandykite vėl</string> + <string name="change_self_destruct_passcode">Keisti savaiminio susinaikinimo prieigos kodą</string> + <string name="new_passcode">Naujas prieigos kodas</string> + <string name="passcode_not_changed">Prieigos kodas nepakeistas!</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Duomenų bazės perkelimui, patvirtinkite kad jūs prisimenate jos slaptafrazę.</string> + <string name="rcv_conn_event_switch_queue_phase_completed">pakeitė adresą tau</string> + <string name="group_member_status_introduced">jungiamasi (supažindintas)</string> + <string name="fix_connection_question">Sutaisyti prisijungimą?</string> + <string name="group_full_name_field">Grupės pilnas pavadinimas:</string> + <string name="desktop_devices">Darbastalio įrenginiai</string> + <string name="only_your_contact_can_delete">Tik jūsų kontaktas gali negrįžtamai ištrinti žinutes (jūs galite pažymėti jas ištrynimui). (24 valandas)</string> + <string name="permissions_camera">Fotoaparatas</string> + <string name="status_contact_has_no_e2e_encryption">Kontaktas neturi visapusio šifravimo</string> + <string name="permissions_find_in_settings_and_grant">Raskite šį leidimą Android nustatymuose ir suteikite jį rankiniu būdu.</string> + <string name="permissions_grant_in_settings">Suteikti nustatymuose</string> + <string name="permissions_grant">Suteikti leidimą (-us) skambinimui</string> + <string name="onboarding_notifications_mode_service">Momentinis</string> + <string name="permissions_open_settings">Atidaryti nustatymus</string> + <string name="icon_descr_call_pending_sent">Laukiamas skambutis</string> + <string name="onboarding_notifications_mode_periodic">Periodiškai</string> + <string name="chat_is_stopped">Pokalbiai sustabdyti</string> + <string name="protect_app_screen">Apsaugoti programėlės ekraną</string> + <string name="receipts_contacts_override_disabled">Kvitų siuntimas išjungtas %d kontaktams</string> + <string name="chat_is_stopped_indication">Pokalbiai sustabdyti</string> + <string name="group_member_status_accepted">jungiamasi (priimtas)</string> + <string name="icon_descr_contact_checked">Kontaktas patikrintas</string> + <string name="passphrase_is_different">Duomenų bazės slaptafrazė yra kitokia nei išsaugota raktų saugykloje.</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Duomenų bazė bus užšifruota ir slaptafrazė bus saugoma nustatymuose.</string> + <string name="alert_message_group_invitation_expired">Grupės pakvietimas nebegalioja, siuntėjas jį pašalino.</string> + <string name="rcv_group_event_member_added">pakvietė %1$s</string> + <string name="past_member_vName">Buvęs narys %1$s</string> + <string name="block_member_button">Blokuoti narį</string> + <string name="block_for_all_question">Blokuoti narį visiems?</string> + <string name="change_member_role_question">Pakeisti grupės rolę?</string> + <string name="change_role">Keisti rolę</string> + <string name="fix_connection_confirm">Sutaisyti</string> + <string name="fix_connection_not_supported_by_contact">Kontaktas nepalaiko sutaisymo.</string> + <string name="network_option_ping_interval">PING intervalas</string> + <string name="network_option_protocol_timeout_per_kb">Protokolui skirtas laikas per KB</string> + <string name="recent_history_is_not_sent_to_new_members">Istorija nėra siunčiama naujiems nariams.</string> + <string name="v4_3_irreversible_message_deletion">Negrįžtamas žinučių ištrynimas</string> + <string name="prohibit_message_reactions">Uždrausti žinučių reakcijas.</string> + <string name="color_received_message">Gauta žinutė</string> + <string name="v5_6_app_data_migration">Programėlės duomenų perkėlimas</string> + <string name="custom_time_unit_days">dienos</string> + <string name="custom_time_unit_hours">valandos</string> + <string name="v5_6_picture_in_picture_calls">Vaizdas-vaizde skambučiai</string> + <string name="v5_5_private_notes">Privatūs užrašai</string> + <string name="remote_host_was_disconnected_title">Prisijungimas sustabdytas</string> + <string name="remote_ctrl_was_disconnected_title">Prisijungimas sustabdytas</string> + <string name="desktop_connection_terminated">Prisijungimas nutrauktas</string> + <string name="remote_ctrl_error_bad_state">Prisijungimas prie darbastalio yra blogos būsenos</string> + <string name="connect_to_desktop">Prisijungti prie darbastalio</string> + <string name="remote_ctrl_error_inactive">Darbastalis yra neaktyvus</string> + <string name="migrate_to_device_file_delete_or_link_invalid">Failas buvo ištrintas arba nuoroda netinkama</string> + <string name="migrate_to_device_importing_archive">Importuojamas archyvas</string> + <string name="new_desktop"><![CDATA[<i>(naujas)</i>]]></string> + <string name="open_port_in_firewall_title">Atidarykite prievadą ugniasienėje</string> + <string name="or_paste_archive_link">Arba įklijuokite archyvo nuorodą</string> + <string name="agent_internal_error_desc">Prašome pranešti tai kūrėjams: +\n%s</string> + <string name="random_port">Atsitiktinis</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Pasirinkite <i>Persikelti iš kito įrenginio</i> naujame įrenginyje ir nuskanuokite QR kodą.]]></string> + <string name="migrate_to_device_confirm_network_settings">Patvirtinti tinklo nustatymus</string> + <string name="migrate_from_device_cancel_migration">Atšaukti perkelimą</string> + <string name="migrate_to_device_chat_migrated">Pokalbiai perkelti!</string> + <string name="migrate_from_device_confirm_upload">Patvirtinti įkėlimą</string> + <string name="migrate_from_device_creating_archive_link">Kuriama archyvo nuoroda</string> + <string name="migrate_from_device_finalize_migration">Užbaigti perkėlimą</string> + <string name="migrate_to_device_finalize_migration">Užbaikite perkėlimą kitame įrenginyje.</string> + <string name="migrate_from_device_chat_should_be_stopped">Norint tęsti, pokalbiai turi būti sustabdyti.</string> + <string name="migrate_from_device_or_share_this_file_link">Arba saugiai pasidalinkite šio failo nuoroda</string> + <string name="v4_5_message_draft_descr">Išsaugokite paskutinės žinutės juodraštį su priedais.</string> + <string name="rcv_group_event_3_members_connected">%s, %s ir %s prisijungė</string> + <string name="e2ee_info_no_pq_short">Šis pokalbis yra apsaugotas su visapusiu šifravimu.</string> + <string name="e2ee_info_pq_short">Šis pokalbis yra apsaugotas kvantiškai atspariu visapusiu šifravimu.</string> + <string name="connection_you_accepted_will_be_cancelled">Prisijungimas, kurį priėmėte, bus atšauktas!</string> + <string name="tap_to_paste_link">Bakstelėkite, kad įklijuoti nuorodą</string> + <string name="smp_servers_test_server">Testuoti serverį</string> + <string name="theme_colors_section_title">TEMOS SPALVOS</string> + <string name="show_slow_api_calls">Rodyti lėtus API iškvietimus</string> + <string name="stop_sharing_address">Nustoti bendrinti adresą?</string> + <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Žinučių siuntimo ir programų platforma, apsauganti jūsų privatumą ir saugumą.</string> + <string name="first_platform_without_user_ids">Pirma platforma neturinti jokių naudotojų identifikatorių - privati pagal sumanymą.</string> + <string name="setup_database_passphrase">Nustatyti duomenų bazės slaptafrazę</string> + <string name="la_mode_system">Sistema</string> + <string name="set_password_to_export">Nustatyti slaptafrazę eksportui</string> + <string name="delete_chat_profile_action_cannot_be_undone_warning">Šis veiksmas negali būti atšauktas - jūsų profilis, kontaktai, žinutės ir failai bus negrįžtamai prarasti.</string> + <string name="profile_update_event_set_new_address">nustatyti naują kontakto adresą</string> + <string name="profile_update_event_set_new_picture">nustatė naują profilio nuotrauką</string> + <string name="conn_event_disabled_pq">Standartinis visapusis šifravimas</string> + <string name="send_receipts">Pristatymo kvitai</string> + <string name="info_row_sent_at">Nusiųsta</string> + <string name="v4_5_italian_interface_descr">Dėkojame naudotojams - prisidėkite per Weblate!</string> + <string name="is_not_verified">%s nėra patvirtintas</string> + <string name="possible_slow_function_title">Lėta funkcija</string> + <string name="system_restricted_background_desc">SimpleX negali veikti fone. Jūs gausite pranešimus tik kai programėlė yra paleista.</string> + <string name="database_initialization_error_desc">Duomenų bazė neveikia tinkamai. Bakstelėkite, kad sužinotumėte daugiau</string> + <string name="simplex_service_notification_title">SimpleX Chat tarnyba</string> + <string name="stop_sharing">Nustoti bendrinti</string> + <string name="share_with_contacts">Bendrinti su kontaktais</string> + <string name="next_generation_of_private_messaging">Naujos kartos privatus susirašinėjimas</string> + <string name="chat_item_ttl_seconds">%s sekundė(s)</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">Kodas kurį nuskanavote, nėra SimpleX nuorodos QR kodas.</string> + <string name="contact_you_shared_link_with_wont_be_able_to_connect">Kontaktas su kurio pasidalinote šia nuoroda NEGALĖS prisijungti!</string> + <string name="this_device_name_shared_with_mobile">Įrenginio vardas bus bendrinamas su susietu mobiliuoju klientu.</string> + <string name="moderate_message_will_be_marked_warning">Ši žinutė bus pažymėta visiems nariams kaip moderuota.</string> + <string name="passphrase_will_be_saved_in_settings">Slaptafrazė bus saugoma nustatymuose kaip paprastas tekstas po jos pakeitimo arba programėlės paleidimo iš naujo.</string> + <string name="smp_servers_per_user">Serveriai naujiems prisijungimams jūsų dabartiniame pokalbių profilyje</string> + <string name="the_text_you_pasted_is_not_a_link">Tekstas kurį įklijavote nėra SimpleX nuoroda.</string> + <string name="delete_files_and_media_desc">Šis veiksmas negali būti atšauktas - visi gauti ir siųsti failai ir medija bus ištrinti. Mažos rezoliucijos nuotraukos išliks.</string> + <string name="v5_6_quantum_resistant_encryption_descr">Įjunkite tiesioginiuose pokalbiuose (BETA)!</string> + <string name="this_string_is_not_a_connection_link">Ši eilutė nėra prisijungimo nuoroda!</string> + <string name="scan_qr_to_connect_to_contact">Kad prisijungti, jūsų kontaktas gali nuskanuoti QR kodą arba naudoti nuorodą programėlėje.</string> + <string name="send_live_message">Rašyti žinutę gyvai</string> + <string name="star_on_github">Pažvaigždinti ant GitHub</string> + <string name="smp_servers_test_servers">Testuoti serverius</string> + <string name="v4_6_chinese_spanish_interface_descr">Dėkojame naudotojams - prisidėkite per Weblate!</string> + <string name="chat_help_tap_button">Bakstelėkite mygtuką</string> + <string name="whats_new_thanks_to_users_contribute_weblate">Dėkojame naudotojams - prisidėkite per Weblate!</string> + <string name="la_lock_mode_system">Sistemos autentifikacija</string> + <string name="non_fatal_errors_occured_during_import">Importuojant įvyko keletas nelemtingų klaidų – daugiau informacijos galite rasti pokalbių konsolėje.</string> + <string name="show_developer_options">Rodyti kūrėjo parinktis</string> + <string name="share_address_with_contacts_question">Bendrinti adresą su kontaktais?</string> + <string name="show_internal_errors">Rodyti vidines klaidas</string> + <string name="disable_onion_hosts_when_not_supported"><![CDATA[Nustatykite <i>Naudoti .onion serverius</i> į Ne jei SOCKS tarpinis serveris nepalaiko jų.]]></string> + <string name="stop_chat_to_export_import_or_delete_chat_database">Sustabdykite pokalbius, kad eksportuoti, importuoti ar ištrinti duomenų bazę. Jūs negalėsite siųsti ir gauti žinučių kai pokalbiai yra sustabdyti.</string> + <string name="text_field_set_contact_placeholder">Nustatyti kontakto vardą…</string> + <string name="sync_connection_force_desc">Šifravimas veikia ir nauja šifravimo sutartis nėra reikalinga. Tai gali sukelti ryšio klaidų!</string> + <string name="share_this_1_time_link">Dalintis šia vienkartinio pakvietimo nuoroda</string> + <string name="is_verified">%s yra patvirtintas</string> + <string name="tap_to_scan">Bakstelėkite, kad nuskanuoti</string> + <string name="shutdown_alert_question">Išjungti?</string> + <string name="unable_to_open_browser_desc">Skambučiams reikalinga numatytoji žiniatinklio naršyklė. Sukonfigūruokite numatytąją sistemos naršyklę ir pasidalykite daugiau informacijos su kūrėjais.</string> + <string name="receipts_section_groups">Mažos grupės (maks. 20)</string> + <string name="color_title">Pavadinimas</string> + <string name="v4_6_group_welcome_message_descr">Nustatykite žinutę rodomą naujiems nariams!</string> + <string name="migrate_from_device_start_chat">Pradėti pokalbį</string> + <string name="failed_to_create_user_invalid_desc">Šis rodomas vardas netinkamas. Prašome pasirinkti kitą vardą.</string> + <string name="set_contact_name">Nustatyti kontakto vardą</string> + <string name="set_group_preferences">Nustatyti grupės parinktis</string> + <string name="error_xftp_test_server_auth">Serveris reikalauja įgaliojimų įkėlimui, patikrinkite slaptažodį</string> + <string name="error_smp_test_server_auth">Serveris reikalauja įgaliojimų eilių kūrimui, patikrinkite slaptažodį</string> + <string name="error_smp_test_failed_at_step">Testas nepavyko žingsnyje: %s</string> + <string name="system_restricted_background_in_call_desc">Programėlė gali būti uždaryta po 1 minutės būvimo fone.</string> + <string name="notifications_mode_periodic">Pasileidžia periodiškai</string> + <string name="sent_message">Nusiųsta žinutė</string> + <string name="image_decoding_exception_desc">Nuotraukos negalima atkoduoti. Bandykite kitą nuotrauką arba susisiekite su kūrėjais.</string> + <string name="video_decoding_exception_desc">Vaizdo įrašo negalima atkoduoti. Bandykite kitą vaizdo įrašą arba susisiekite su kūrėjais.</string> + <string name="smp_servers_test_failed">Serverio testas nepavyko!</string> + <string name="smp_servers_test_some_failed">Kai kurie serveriai nepraėjo testo:</string> + <string name="share_text_sent_at">Nusiųsta: %s</string> + <string name="sending_via">Siunčiama per</string> + <string name="color_sent_message">Nusiųsta žinutė</string> + <string name="v5_2_message_delivery_receipts_descr">Antra varnelė kurios mums trūko! ✅</string> + <string name="simplex_address">SimpleX adresas</string> + <string name="chat_with_the_founder">Siųsti klausimus ir idėjas</string> + <string name="send_us_an_email">Siųsti mums el. laišką</string> + <string name="terminal_always_visible">Rodyti konsolę naujame lange</string> + <string name="send_link_previews">Siųsti nuorodų peržiūras</string> + <string name="alert_text_msg_bad_id">Sekančios žinutės ID yra neteisingas (mažesnis arba lygus ankstesniam). +\nTaip gali nutikti dėl tam tikros klaidos arba kai ryšys yra sukompromisuotas.</string> + <string name="settings_shutdown">Išjungti</string> + <string name="theme_simplex">SimpleX</string> + <string name="accept_feature_set_1_day">Nustatyti 1 dieną</string> + <string name="v4_4_french_interface_descr">Dėkojame naudotojams - prisidėkite per Weblate!</string> + <string name="remote_ctrl_error_timeout">Baigėsi skirtasis laikas jungiantis prie darbastalio</string> + <string name="in_developing_desc">Ši galimybė dar nėra palaikoma. Išbandykite sekantį leidimą.</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Tai yra jūsų nuoroda grupei <b>%1$s</b>!]]></string> + <string name="to_connect_via_link_title">Kad prisijungti su nuoroda</string> + <string name="this_link_is_not_a_valid_connection_link">Ši nuoroda nėra tinkama prisijungimo nuoroda!</string> + <string name="receipts_section_description">Šie nustatymai yra jūsų dabartiniam profiliui</string> + <string name="settings_is_storing_in_clear_text">Slaptafrazė saugoma nustatymuose kaip paprastas tekstas.</string> + <string name="group_invitation_tap_to_join_incognito">Bakstelėkite, kad prisijungti kaip inkognito</string> + <string name="send_receipts_disabled_alert_msg">Ši grupė turi daugiau nei %1$d narių, pristatymo kvitai nėra siunčiami.</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="v5_4_link_mobile_desktop_descr">Su saugiu kvantiškai atspariu protokolu.</string> + <string name="this_device_name">Šio įrenginio pavadinimas</string> + <string name="v5_3_simpler_incognito_mode">Supaprastintas inkognito režimas</string> + <string name="connect_plan_this_is_your_own_simplex_address">Tai yra jūsų SimpleX adresas!</string> + <string name="group_members_n">%s, %s ir %d nariai</string> + <string name="contact_tap_to_connect">Bakstelėkite, kad prisijungti</string> + <string name="skip_inviting_button">Praleisti narių kvietimą</string> + <string name="share_address">Bendrinti adresą</string> + <string name="enable_sending_recent_history">Siųsti naujiems nariams iki 100 paskutinių žinučių.</string> + <string name="receipts_contacts_override_enabled">Kvitų siuntimas yra įjungtas %d kontaktams</string> + <string name="receipts_groups_override_enabled">Pristatymo kvitai yra įjungti %d grupėms</string> + <string name="set_database_passphrase">Nustatyti duomenų slaptafrazę</string> + <string name="set_passphrase">Nustatyti slaptafrazę</string> + <string name="privacy_show_last_messages">Rodyti paskutines žinutes</string> + <string name="settings_section_title_support">PALAIKYKITE SIMPLEX CHAT</string> + <string name="receipts_section_description_1">Jų galima nepaisyti kontaktų ir grupių nustatymuose.</string> + <string name="enable_automatic_deletion_message">Šis veiksmas negali būti atšauktas - žinutės išsiųstos ir gautos anksčiau nei pasirinkta bus ištrintos. Tai gali užtrukti kelias minutes.</string> + <string name="rcv_group_event_n_members_connected">%s, %s ir %d kiti nariai prisijungė</string> + <string name="start_chat_question">Pradėti pokalbį?</string> + <string name="database_backup_can_be_restored">Bandymas pakeisti duomenų bazės slaptafrazę nebuvo užbaigtas.</string> + <string name="network_option_tcp_connection_timeout">TCP ryšiui skirtas laikas</string> + <string name="v5_0_polish_interface_descr">Dėkojame naudotojams - prisidėkite per Weblate!</string> + <string name="session_code">Sesijos kodas</string> + <string name="this_device">Šis įrenginys</string> + <string name="connect_plan_this_is_your_own_one_time_link">Tai yra jūsų vienkartinė nuoroda!</string> + <string name="remote_host_error_timeout"><![CDATA[Baigėsi skirtasis laikas jungiantis prie mobiliojo <b>%s</b>]]></string> + <string name="open_port_in_firewall_desc">Kad leisti mobiliajai programėlei prisijungti prie darbastalio, atidarykite šį prievadą savo ugniasienėje, jei ją turite įgalinę.</string> + <string name="migrate_from_device_stopping_chat">Sustabdomi pokalbiai</string> + <string name="migrate_from_device_bytes_uploaded">%s įkeltas</string> + <string name="your_chat_profiles">Jūsų pokalbių profiliai</string> + <string name="your_chat_profile_will_be_sent_to_your_contact">Jūsų pokalbių profilis bus nusiųstas +\njūsų kontaktui</string> + <string name="your_chat_database">Jūsų duomenų bazė</string> + <string name="database_is_not_encrypted">Jūsų duomenų bazė nėra užšifruota - nustatykite slaptafrazę, kad ją apsaugoti.</string> + <string name="group_main_profile_sent">Jūsų pokalbių profilis bus nusiųstas grupės nariams</string> + <string name="encryption_renegotiation_error">Pakartotinių šifravimo derybų klaida</string> + <string name="downgrade_and_open_chat">Pasendinti ir atidaryti pokalbį</string> + <string name="error_accepting_contact_request">Priimant kontakto užklausą įvyko klaida</string> + <string name="error_alert_title">Klaida</string> + <string name="smp_servers_enter_manually">Įvesti serverį rankiniu būdu</string> + <string name="display_name">Įveskite savo vardą:</string> + <string name="receipts_contacts_enable_keep_overrides">Įjungti (palikti nepaisymus</string> + <string name="receipts_groups_enable_for_all">Įjungti visoms grupėms</string> + <string name="snd_conn_event_ratchet_sync_agreed">šifravimas sutartas su %s</string> + <string name="snd_conn_event_ratchet_sync_required">reikalingos pakartotinės šifravimo derybos su %s</string> + <string name="conn_event_ratchet_sync_ok">šifravimas gerai</string> + <string name="ttl_weeks">%d savaitės</string> + <string name="ttl_mth">%dmėn</string> + <string name="error">Klaida</string> + <string name="multicast_discoverable_via_local_network">Aptinkama vietiniame tinkle</string> + <string name="blocked_by_admin_items_description">%d žinutės užblokuotos administratoriaus</string> + <string name="error_creating_message">Įvyko klaida kuriant žinutę</string> + <string name="error_deleting_note_folder">Įvyko klaida ištrinant privačius užrašus</string> + <string name="enter_welcome_message_optional">Įveskite sveikinimo žinutę… (nebūtina)</string> + <string name="snd_group_event_member_deleted">Jūs pašalinote %1$s</string> + <string name="error_blocking_member_for_all">Įvyko klaida blokuojant narį visiems</string> + <string name="ttl_min">%d min</string> + <string name="disable_sending_recent_history">Nesiųsti istorijos naujiems nariams.</string> + <string name="status_e2e_encrypted">visapusiškai užšifruotas</string> + <string name="remote_ctrl_disconnected_with_reason">Atsijungta su priežastimi: %s</string> + <string name="your_profile_is_stored_on_your_device">Jūsų profilis, kontaktai ir gautos žinutės yra saugomi jūsų įrenginyje.</string> + <string name="info_row_disappears_at">Išnyks</string> + <string name="display_name_cannot_contain_whitespace">Rodomas vardas negali turėti tarpų.</string> + <string name="dont_create_address">Nekurti adreso</string> + <string name="migrate_to_device_download_failed">Atsiuntimas nepavyko</string> + <string name="migrate_to_device_downloading_archive">Atsiunčiamas archyvas</string> + <string name="encrypted_audio_call">visapusiškai užšifruotas balso skambutis</string> + <string name="migrate_to_device_downloading_details">Atsiunčiamos nuorodos detalės</string> + <string name="failed_to_create_user_duplicate_title">Pasikartojantis rodomas vardas!</string> + <string name="conn_event_ratchet_sync_agreed">šifravimas sutartas</string> + <string name="snd_conn_event_ratchet_sync_ok">šifravimas gerai su %s</string> + <string name="call_service_notification_end_call">Užbaigti skambutį</string> + <string name="error_enabling_delivery_receipts">Įvyko klaida įjungiant pristatymo kvitus!</string> + <string name="connect__a_new_random_profile_will_be_shared">Naujas atsitiktinis profilis bus bendrinamas.</string> + <string name="receipts_contacts_title_enable">Įjungti kvitus?</string> + <string name="enter_passphrase">Įveskite slaptafrazę…</string> + <string name="group_display_name_field">Įveskite grupės pavadinimą:</string> + <string name="la_seconds">%d sekundės</string> + <string name="enable_camera_access">Įjungti fotoaparato prieigą</string> + <string name="receipts_groups_title_enable">Įjungti kvitus grupėms?</string> + <string name="receipts_contacts_disable_for_all">Išjungti visiems</string> + <string name="receipts_groups_title_disable">Išjungti kvitus grupėms?</string> + <string name="receipts_contacts_enable_for_all">Įjungti visiems</string> + <string name="enter_welcome_message">Įveskite sveikinimo žinutę…</string> + <string name="network_option_enable_tcp_keep_alive">Įjungti TCP išlik-gyvas</string> + <string name="ttl_month">%d mėnuo</string> + <string name="v5_3_encrypt_local_files">Užšifruoti saugomus failus ir mediją</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Visi jūsų kontaktai, pokalbiai ir failai bus saugiai užšifruoti ir dalimis įkelti į sukonfigūruotus XFTP serverius.</string> + <string name="error_aborting_address_change">Klaida nutraukiant adreso keitimą</string> + <string name="encrypted_video_call">visapusiškai užšifruotas vaizdo skambutis</string> + <string name="marked_deleted_items_description">%d žinutės pažymėtos kaip ištrintos</string> + <string name="blocked_items_description">%d žinutės užblokuotos</string> + <string name="error_adding_members">Pridedant narį (-us) įvyko klaida</string> + <string name="error_deleting_contact_request">Įvyko klaida ištrinant kontakto užklausą</string> + <string name="error_deleting_pending_contact_connection">Įvyko klaida ištrinant laukiantį kontakto prisijungimą</string> + <string name="la_minutes">%d minutės</string> + <string name="enable_self_destruct">Įjungti savaiminį susinaikinimą</string> + <string name="receipts_groups_enable_keep_overrides">Įjungti (palikti grupių nepaisymus)</string> + <string name="snd_conn_event_ratchet_sync_allowed">pakartotinės šifravimo derybos leidžiamos %s</string> + <string name="ttl_week">%d savaitė</string> + <string name="edit_image">Redaguoti nuotrauką</string> + <string name="conn_event_ratchet_sync_allowed">leidžiamos pakartotinės šifravimo derybos</string> + <string name="conn_event_ratchet_sync_required">reikalingos pakartotinės šifravimo derybos</string> + <string name="ttl_sec">%d sek.</string> + <string name="alert_text_encryption_renegotiation_failed">Pakartotinės šifravimo derybos nepavyko.</string> + <string name="receipts_groups_disable_keep_overrides">Išjungti (palikti grupių nepaisymus)</string> + <string name="invite_prohibited_description">Bandote pakviesti kontaktą, su kuriuo bendrinote inkognito profilį, į grupę, kurioje naudojate savo pagrindinį profilį</string> + <string name="v4_5_multiple_chat_profiles_descr">Skirtingi vardai, avatarai ir transporto izoliacija.</string> + <string name="v5_3_discover_join_groups">Atraskite ir prisijunkite prie grupių</string> + <string name="disconnect_remote_host">Atsijungti</string> + <string name="ttl_w">%dsav.</string> + <string name="connect__your_profile_will_be_shared">Jūsų profilis %1$s bus bendrinamas.</string> + <string name="you_rejected_group_invitation">Jūs atmetėte grupės kvietimą</string> + <string name="share_text_disappears_at">Išnyks: %s</string> + <string name="rcv_group_and_other_events">ir %d kiti įvykiai</string> + <string name="enter_correct_passphrase">Įveskite tinkamą slaptafrazę.</string> + <string name="ttl_s">%ds</string> + <string name="allow_accepting_calls_from_lock_screen">Įjunkite skambučius užrakto ekrane per nustatymus.</string> + <string name="receipts_groups_disable_for_all">Išjungti visoms grupėms</string> + <string name="receipts_contacts_disable_keep_overrides">Išjungti (palikti nepaisymus)</string> + <string name="integrity_msg_duplicate">pasikartojanti žinutė</string> + <string name="encrypt_local_files">Užšifruoti vietinius failus</string> + <string name="alert_title_cant_invite_contacts_descr">Šiai grupei naudojate inkognito profilį - kad nesidalintumėte pagrindiniu profiliu, neleidžiama kviesti kontaktų</string> + <string name="enter_password_to_show">Įveskite slaptažodį paieškoje</string> + <string name="error_creating_member_contact">Įvyko klaida kuriant nario kontaktą</string> + <string name="ttl_m">%dm</string> + <string name="ttl_months">%d mėnesiai</string> + <string name="dont_enable_receipts">Neįjungti</string> + <string name="enable_receipts_all">Įjungti</string> + <string name="disconnect_desktop_question">Atjungti darbalaukį?</string> + <string name="remote_host_disconnected_from"><![CDATA[Atsijungta nuo mobiliojo: <b>%s</b> su priežaistimi: %s]]></string> + <string name="disconnect_remote_hosts">Atjungti mobiliuosius</string> + <string name="discover_on_network">Atrasti per vietinį tinklą</string> + <string name="migrate_to_device_enter_passphrase">Įvesti slaptafrazę</string> + <string name="enter_this_device_name">Įvesti šio įrenginio pavadinimą…</string> + <string name="migrate_to_device_error_downloading_archive">Įvyko klaida atsisiunčiant archyvą</string> + <string name="migrate_from_device_error_deleting_database">Įvyko klaida ištrinant duomenų bazę</string> + <string name="e2ee_info_no_pq"><![CDATA[Žinutės, failai ir skambučiai yra apsaugoti <b>visapusiu šifravimu</b> su tobulu priekiniu slaptumu, išsižadėjimu ir atsigavimu įsilaužus.]]></string> + <string name="e2ee_info_pq"><![CDATA[Žinutės, failai ir skambučiai yra apsaugoti <b>kvantiškai atspariu visapusiu šifravimu</b> su tobulu priekiniu slaptumu, išsižadėjimu ir atsigavimu įsilaužus.]]></string> + <string name="failed_to_create_user_duplicate_desc">Jau turite pokalbių profilį su tuo pačiu rodomu vardu. Pasirinkite kitą vardą.</string> + <string name="you_are_already_connected_to_vName_via_this_link">Jau esate prisijungę prie %1$s.</string> + <string name="error_setting_address">Įvyko klaida nustatant adresą</string> + <string name="error_synchronizing_connection">Įvyko klaida sinchronizuojant prisijungimą</string> + <string name="sender_may_have_deleted_the_connection_request">Siuntėjas gali būti ištrynęs prisijungimo užklausą.</string> + <string name="smp_server_test_secure_queue">Saugi eilė</string> + <string name="turn_off_battery_optimization"><![CDATA[Kad tai naudoti, prašome <b>leisti SimpleX veikti fone</b> sekančiame dialoge. Kitaip, pranešimai bus išjungti.]]></string> + <string name="smp_server_test_upload_file">Įkelti failą</string> + <string name="notifications_mode_off">Veikia kai programėlė yra atidaryta</string> + <string name="la_could_not_be_verified">Negalėjote būti patvirtintas; bandykite vėl.</string> + <string name="you_can_turn_on_lock">Galite įjungti SimpleX užraktą per nustatymus.</string> + <string name="expand_verb">Išplėsti</string> + <string name="revoke_file__action">Atšaukti failą</string> + <string name="revoke_file__title">Atšaukti failą?</string> + <string name="icon_descr_sent_msg_status_unauthorized_send">neteisėtas siuntimas</string> + <string name="search_or_paste_simplex_link">Ieškoti ar įklijuoti SimpleX nuorodą</string> + <string name="you_need_to_allow_to_send_voice">Jūs turite leisti savo kontaktui siųsti balso žinutes, kad galėtumėte siųsti jas.</string> + <string name="connect_via_link_or_qr_from_clipboard_or_in_person">(nuskanuokite ar įklijuokite iš iškarpinės)</string> + <string name="you_accepted_connection">Priėmėte prisijungimą</string> + <string name="you_invited_a_contact">Jūs pakvietėte kontaktą</string> + <string name="network_use_onion_hosts_prefer_desc">Onion serveriai bus naudojami, kai tik bus.</string> + <string name="exit_without_saving">Išeiti neišsaugant</string> + <string name="you_control_your_chat">Jūs kontroliuojate savo pokalbį!</string> + <string name="we_do_not_store_contacts_or_messages_on_servers">Mes nesaugome jokių jūsų kontaktu ar žinučių (po pristatymo) serveriuose.</string> + <string name="no_received_app_files">Nėra gautų ar išsiųstų failų</string> + <string name="save_passphrase_in_keychain">Išsaugoti slaptafrazę raktų saugykloje</string> + <string name="update_database_passphrase">Atnaujinti duomenų bazės slaptafrazę</string> + <string name="rcv_group_event_member_unblocked">atblokavo %s</string> + <string name="profile_update_event_member_name_changed">narys %1$s pasikeitė į %2$s</string> + <string name="profile_update_event_removed_address">pašalintas kontakto adresas</string> + <string name="profile_update_event_removed_picture">pašalinta profilio nuotrauka</string> + <string name="profile_update_event_updated_profile">atnaujino proflį</string> + <string name="rcv_conn_event_verification_code_reset">saugumo kodas pakeistas</string> + <string name="group_member_role_observer">stebėtojas</string> + <string name="snd_conn_event_switch_queue_phase_completed">pakeitėte adresą</string> + <string name="button_welcome_message">Sveikinimo žinutė</string> + <string name="info_row_updated_at">Įrašas atnaujintas</string> + <string name="current_version_timestamp">%s (dabartinis)</string> + <string name="share_text_moderated_at">Moderuota: %s</string> + <string name="network_options_reset_to_defaults">Atstatyti į numatytuosius</string> + <string name="recent_history">Matoma istorija</string> + <string name="only_you_can_add_message_reactions">Tik jūs galite pridėti žinučių reakcijas.</string> + <string name="v5_3_new_desktop_app">Nauja darbastalio programėlė!</string> + <string name="v5_5_simpler_connect_ui_descr">Paieškos laukas priima kvietimo nuorodas.</string> + <string name="v5_5_private_notes_descr">Su užšifruotais failais ir medija.</string> + <string name="v5_6_picture_in_picture_calls_descr">Naudokite programėlę skambučio metu.</string> + <string name="migrate_to_device_bytes_downloaded">%s atsisiuntė</string> + <string name="migrate_to_device_repeat_download">Pakartoti atsisiuntimą</string> + <string name="migrate_to_device_repeat_import">Pakartoti importą</string> + <string name="migrate_from_device_title">Persikelti iš įrenginio</string> + <string name="migrate_from_device_to_another_device">Persikelti į kitą įrenginį</string> + <string name="migrate_from_device_error_saving_settings">Įvyko klaida išsaugant nustatymus</string> + <string name="migrate_from_device_error_exporting_archive">Įvyko klaida eksportuojant pokalbių duomenų bazę</string> + <string name="migrate_from_device_error_verifying_passphrase">Įvyko klaida patvirtinant slaptafrazę:</string> + <string name="call_connection_via_relay">per tarpinį serverį</string> + <string name="v4_3_irreversible_message_deletion_desc">Jūsų kontaktai gali leisti pilną žinučiu ištrynimą.</string> + <string name="v4_4_verify_connection_security">Patvirtinkite prisijungimo saugumą</string> + <string name="profile_will_be_sent_to_contact_sending_link">Jūsų profilis bus išsiųstas kontaktui iš kurio gavote šią nuorodą.</string> + <string name="you_will_join_group">Prisijungsite prie visų grupės narių.</string> + <string name="connected_to_server_to_receive_messages_from_contact">Esate prisijungę prie serverio skirto gauti žinutes iš šio kontakto.</string> + <string name="trying_to_connect_to_server_to_receive_messages_with_error">Bandoma prisijungti prie serverio skirto žinučių gavimui iš šio kontakto (klaida: %1$s).</string> + <string name="trying_to_connect_to_server_to_receive_messages">Bandoma prisijungti prie serverio skirto žinučių gavimui iš šio kontakto.</string> + <string name="no_details">nėra detalių</string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Kad išlaikyti jūsų privatumą, vietoj tiesioginių pranešimų, programėlė turi <b>SimpleX fono tarnybą</b> - ji naudoja kelis procentus akumuliatoriaus per dieną.]]></string> + <string name="periodic_notifications_desc">Programėlė periodiškai gauna naujas žinutes — ji sunaudoja kelis procentus baterijos per dieną. Programa nenaudoja tiesioginių pranešimų — duomenys iš jūsų įrenginio į serverius nesiunčiami.</string> + <string name="one_time_link">Vienkartinė pakvietimo nuoroda</string> + <string name="you_wont_lose_your_contacts_if_delete_address">Neprarasite savo kontaktų, jei vėliau ištrinsite savo adresą.</string> + <string name="network_use_onion_hosts_prefer">Kai yra</string> + <string name="video_call_no_encryption">vaizdo skambutis (neužšifruotas visapusiškai)</string> + <string name="remove_passphrase_from_keychain">Pašalinti slaptafrazę iš raktų saugyklos?</string> + <string name="database_migrations">Persikėlimai: %s</string> + <string name="you_sent_group_invitation">Nusiuntėte grupės pakvietimą</string> + <string name="rcv_group_event_updated_group_profile">atnaujino grupės profilį</string> + <string name="snd_group_event_member_unblocked">atblokavote %s</string> + <string name="saved_message_title">Išsaugota žinutė</string> + <string name="button_remove_member_question">Pašalinti narį?</string> + <string name="unblock_for_all">Atblokuoti visiems</string> + <string name="connect_via_member_address_alert_desc">Prisijungimo užklausa bus išsiųsta šiam grupės nariui.</string> + <string name="v4_5_reduced_battery_usage">Sumažintas baterijos naudojimas</string> + <string name="v5_5_join_group_conversation_descr">Naujausia istorija ir patobulintas katalogo botas.</string> + <string name="sending_delivery_receipts_will_be_enabled">Pristatymo kvitų siuntimas bus įjungtas visiems kontaktams.</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Pristatymo kvitų siuntimas bus įjungtas visiems kontaktams visuose matomuose pokalbių profiliuose.</string> + <string name="linked_desktops">Susieti darbastaliai</string> + <string name="scan_qr_code_from_desktop">Nuskanuoti QR kodą iš darbastalio</string> + <string name="remote_host_error_disconnected"><![CDATA[Mobilusis <b>%s</b> buvo atjungtas]]></string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Jau yra prisijungiama prie <b>%1$s</b>.]]></string> + <string name="you_can_share_your_address">Galite bendrinti savo adresą kaip nuorodą ar QR kodą - bet kas galės prisijungti prie jūsų.</string> + <string name="update_network_session_mode_question">Atnaujinti transporto izoliacijos režimą?</string> + <string name="save_auto_accept_settings">Išsaugoti automatinio priėmimo nustatymus</string> + <string name="color_secondary">Antrinis</string> + <string name="message_reactions_prohibited_in_this_chat">Žinučių reakcijos yra draudžiamos šiame pokalbyje.</string> + <string name="call_service_notification_video_call">Vaizdo skambutis</string> + <string name="delete_message_mark_deleted_warning">Žinutė bus pažymėta ištrynimui. Gavėjas (-ai) galės atskleisti šią žinutę.</string> + <string name="revoke_file__confirm">Atšaukti</string> + <string name="icon_descr_sent_msg_status_send_failed">siuntimas nepavyko</string> + <string name="images_limit_title">Per daug nuotraukų!</string> + <string name="send_disappearing_message">Siųsti išnykstančią žinutę</string> + <string name="to_share_with_your_contact">(kad bendrinti su savo kontaktu)</string> + <string name="to_start_a_new_chat_help_header">Kad pradėti naują pokalbį</string> + <string name="retry_verb">Pakartoti</string> + <string name="network_use_onion_hosts">Naudoti .onion serverius</string> + <string name="v4_4_live_messages">Žinutės gyvai</string> + <string name="custom_time_unit_weeks">savaitės</string> + <string name="send_disappearing_message_send">Siųsti</string> + <string name="connect_use_new_incognito_profile">Naudoti inkognito profilį</string> + <string name="notification_new_contact_request">Nauja kontakto užklausa</string> + <string name="switch_receiving_address_desc">Gavimo adresas bus pakeistas į kitą serverį. Adreso pakeitimas bus užbaigtas kai siuntėjas prisijungs.</string> + <string name="migrate_from_device_error_uploading_archive">Įvyko klaida įkeliant archyvą</string> + <string name="auth_log_in_using_credential">Prisijunkite su savo kredencialais</string> + <string name="ensure_smp_server_address_are_correct_format_and_unique">Įsitikinkite, kad SMP serverių adresai yra tinkamu formatu, atskirose eilutėse ir nesikartojantys.</string> + <string name="ensure_xftp_server_address_are_correct_format_and_unique">Įsitikinkite, kad XFTP serverių adresai yra tinkamu formatu, atskirose eilutėse ir nesikartojantys.</string> + <string name="markdown_help">Markdown pagalba</string> + <string name="v4_3_voice_messages_desc">Maks. 40 sekundžių, gaunama iš karto.</string> + <string name="message_reactions">Žinučių reakcijos</string> + <string name="delete_message_cannot_be_undone_warning">Žinutė bus ištrinta - tai negali būti atšaukta!</string> + <string name="network_session_mode_transport_isolation">Transporto izoliacija</string> + <string name="v5_6_safer_groups">Saugesnės grupės</string> + <string name="rcv_group_event_1_member_connected">%s prisijungė</string> + <string name="connect_plan_you_have_already_requested_connection_via_this_address">Jūs jau prašėte prisijungimo per šį adresą!</string> + <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Jūsų kontaktas turi būti prisijungęs, kad užbaigti prisijungimą. +\nJūs galite atšaukti šį prisijungimą ir pašalinti kontaktą (ir vėliau bandyti su nauja nuoroda).</string> + <string name="your_contacts_will_remain_connected">Jūsų kontaktai išliks prisijungę.</string> + <string name="your_XFTP_servers">Jūsų XFTP serveriai</string> + <string name="you_will_be_connected_when_your_connection_request_is_accepted">Prisijungsite kai jūsų prisijungimo užklausa bus patvirtinta, palaukite arba patikrinkite vėliau!</string> + <string name="remote_host_error_busy"><![CDATA[Mobilusis <b>%s</b> yra užsiėmęs]]></string> + <string name="remote_host_error_inactive"><![CDATA[Mobilusis \u0020<b>%s</b> yra neaktyvus]]></string> + <string name="custom_time_unit_months">mėnesiai</string> + <string name="message_delivery_error_desc">Tikriausiai šis kontaktas ištrynė prisijungimą su jumis.</string> + <string name="user_mute">Nutildyti</string> + <string name="no_info_on_delivery">Nėra pristatymo informacijos</string> + <string name="v4_6_group_moderation_descr">Dabar administratoriai gali: +\n- ištrinti narių žinutes. +\n- išjungti narius ("stebėtojas" rolė)</string> + <string name="chat_preferences_on">įj.</string> + <string name="unfavorite_chat">Nebemėgti</string> + <string name="updating_settings_will_reconnect_client_to_all_servers">Nustatymų atnaujinimas perjungs klientą iš naujo prie visų serverių.</string> + <string name="recent_history_is_sent_to_new_members">Iki 100 paskutinių žinučių bus siunčiamos naujiems nariams.</string> + <string name="connect_use_current_profile">Naudoti dabartinį profilį</string> + <string name="v4_2_auto_accept_contact_requests_desc">Su nebūtina sveikinimo žinute.</string> + <string name="v5_5_message_delivery_descr">Su sumažintu akumuliatoriaus naudojimu.</string> + <string name="you_are_invited_to_group">Esate kviečiami į grupę</string> + <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Galite <font color="#0088ff">prisijungti prie SimpleX Chat kūrėjų, kad klausti klausimus ir gauti atnaujinimus</font>.]]></string> + <string name="you_can_make_address_visible_via_settings">Galite padaryti tai matomą savo SimpleX kontaktams per nustatymus.</string> + <string name="linked_desktop_options">Susieto darbastalio parinktys</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Jau esate grupėje <b>%1$s</b>.]]></string> + <string name="user_unmute">Pašalinti nutildymą</string> + <string name="waiting_for_mobile_to_connect">Laukiama kol mobilusis prisijungs:</string> + <string name="error_showing_desktop_notification">Įvyko klaida rodant pranešimą, susisiekite su kūrėjais.</string> + <string name="group_preview_you_are_invited">esate kviečiami į grupę</string> + <string name="images_limit_desc">Tik 10 nuotraukų gali būti siunčiamos vienu metu.</string> + <string name="videos_limit_desc">Tik 10 vaizdo įrašų gali būti siunčiami vienu metu</string> + <string name="video_will_be_received_when_contact_completes_uploading">Vaizdo įrašas bus gautas kai jūsų kontaktas baigs jo įkėlimą.</string> + <string name="video_will_be_received_when_contact_is_online">Vaizdo įrašas bus gautas kai jūsų kontaktas bus prisijungęs, palaukite arba patikrinkite vėliau!</string> + <string name="icon_descr_waiting_for_video">Laukiama vaizdo įrašo</string> + <string name="waiting_for_video">Laukiama vaizdo įrašo</string> + <string name="sync_connection_force_question">Derėtis su šifravimu per naujo?</string> + <string name="contact_sent_large_file">Jūsų kontaktas atsiuntė failą kuris yra didesnis nei dabartinis maksimalus palaikomas dydis (%1$s).</string> + <string name="icon_descr_record_voice_message">Įrašyti balso žinutę</string> + <string name="sync_connection_force_confirm">Derėtis per naujo</string> + <string name="verify_security_code">Patvirtinti saugumo kodą</string> + <string name="live_message">Žinutė gyvai!</string> + <string name="scan_QR_code">Nuskanuoti QR kodą</string> + <string name="send_live_message_desc">Siųskite žinutę gyvai - ji pasikeis gavėjui (-ams) rašymo metu</string> + <string name="add_contact">Vienkartinė pakvietimo nuoroda</string> + <string name="scan_code_from_contacts_app">Nuskanuoti saugumo kodą iš jūsų kontakto programėlės.</string> + <string name="you_will_be_connected_when_group_host_device_is_online">Prisijungsite prie grupės kai grupės nuorodos savininkas bus prisijungęs, palaukite arba patikrinkite vėliau!</string> + <string name="you_will_be_connected_when_your_contacts_device_is_online">Prisijungsite kai jūsų konktakto įrenginys bus prisijungęs, palaukite arba patikrinkite vėliau!</string> + <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 mobilusis: bakstelėkite <b>Atidaryti mobilioje programėlėje</b>, tada bakstelėkite <b>Prisijungti</b> programėlėje.]]></string> + <string name="smp_servers_use_server_for_new_conn">Naudoti naujiems prisijungimams</string> + <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Galite prisijungti paspausdami ant nuorodos. Jei ji atsidaro naršyklėje, paspauskite ant <b>Atidaryti mobilioje programėlėje</b> mygtuko.]]></string> + <string name="unable_to_open_browser_title">Įvyko klaida atveriant naršyklę</string> + <string name="secret_text">paslaptis</string> + <string name="shutdown_alert_desc">Pranešimai nustos veikti iki tol kol paleisite programėlę iš naujo</string> + <string name="you_can_use_markdown_to_format_messages__prompt">Galite naudoti markdown, kad formatuoti žinutes:</string> + <string name="run_chat_section">PALEISTI POKALBIUS</string> + <string name="settings_section_title_use_from_desktop">Naudoti iš darbastalio</string> + <string name="welcome_message_is_too_long">Sveikinimo žinutė yra per ilga</string> + <string name="v5_1_message_reactions">Žinučių reakcijos</string> + <string name="v5_6_app_data_migration_descr">Persikelti į kitą įrenginį su QR kodu.</string> + <string name="migrate_from_device_migration_complete">Persikėlimas baigtas</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[Jūs <b>turite nenaudoti</b> tos pačios duomenų bazės dviejuose įrenginiuose.]]></string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Įspėjimas: pokalbių paleidimas keliuose įrenginiuose vienu metu nėra palaikomas ir sukels žinučių pristatymo problemas</string> + <string name="migrate_from_device_verify_database_passphrase">Patvirtinti duomenų bazės slaptafrazę</string> + <string name="migrate_from_device_verify_passphrase">Patvirtinti slaptafrazę</string> + <string name="mute_chat">Nutildyti</string> + <string name="relay_server_protects_ip">Tarpinis serveris apsaugo jūsų IP adresą, bet jis gali stebėti skambučio trukmę.</string> + <string name="status_no_e2e_encryption">nėra visapusio šifravimo</string> + <string name="new_database_archive">Naujas duomenų bazės archyvas</string> + <string name="you_have_to_enter_passphrase_every_time">Jūs turite įvesti slaptafrazę kas kartą kai programėlė pasileidžia - tai nėra saugoma įrenginyje.</string> + <string name="non_content_uri_alert_text">Jūs pasidalinote netinkamu failo keliu. Praneškite šią problemą programėlės kūrėjams.</string> + <string name="member_contact_send_direct_message">siųsti tiesioginę žinutę</string> + <string name="you_have_no_chats">Jūs neturite pokalbių</string> + <string name="no_filtered_chats">Nėra filtruotų pokalbių</string> + <string name="info_row_local_name">Vietinis vardas</string> + <string name="renegotiate_encryption">Derėtis su šifravimu per naujo</string> + <string name="marked_deleted_description">pažymėta ištrinta</string> + <string name="live">GYVAI</string> + <string name="moderated_description">moderuota</string> + <string name="connection_error_auth_desc">Nebent jūsų kontaktas ištrynė šį prisijungimą arba nuoroda jau buvo panaudota, tai gali būti klaida - prašome ją pranešti. +\nKad prisijungti, paprašykite savo kontakto sukurti kitą prisijungimo nuorodą ir patikrinkite, kad turite stabilų interneto ryšį.</string> + <string name="system_restricted_background_warn"><![CDATA[Kad įjungti pranešimus, prašome pasirinkti <b>Programėlės akumuliatoriaus naudojimas</b> / <b>Neapribotas</b> programėlės nustatymuose.]]></string> + <string name="system_restricted_background_in_call_warn"><![CDATA[Kad daryti skambučius fone, prašome pasirinkti <b>Programėlės akumuliatoriaus naudojimas</b> / <b>Neapribotas</b> programėlės nustatymuose.]]></string> + <string name="system_restricted_background_in_call_title">Nėra foninių skambučių</string> + <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Jums reikės autentifikuotis kai paleidžiate programėlę arba pratęsiate jos naudojimą po 30 sekundžių fone.</string> + <string name="no_history">Nėra istorijos</string> + <string name="you_are_observer">esate stebėtojas</string> + <string name="only_stored_on_members_devices">(saugo tik grupės nariai)</string> + <string name="your_simplex_contact_address">Jūsų SimpleX adresas</string> + <string name="smp_servers_scan_qr">Nuskanuoti serverio QR kodą</string> + <string name="network_use_onion_hosts_required">Reikalingi</string> + <string name="network_use_onion_hosts_no_desc">Onion serveriai nebus naudojami.</string> + <string name="self_destruct">Savaiminis susinaikinimas</string> + <string name="old_database_archive">Senas duomenų bazės archyvas</string> + <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Jūsų pokalbių duomenų bazė bus IŠTRINTA ir PAKEISTA su importuojama. +\nŠis veiksmas negali būti atšauktas - jūsų profilis, kontaktai, žinutės ir failai bus negrįžtamai prarasti.</string> + <string name="save_passphrase_in_settings">Išsaugoti slaptafrazę nustatymuose</string> + <string name="notifications_will_be_hidden">Pranešimai bus pristatyti tik iki tol kol programėlė bus sustabdyta!</string> + <string name="you_can_share_group_link_anybody_will_be_able_to_connect">Galite bendrinti nuorodą ar QR kodą - bet kas galės prisijungti prie grupės. Neprarasite grupės narių jei vėliau tai ištrinsite.</string> + <string name="share_text_updated_at">Įrašas atnaujintas: %s</string> + <string name="receiving_via">Gaunama per</string> + <string name="v4_5_private_filenames_descr">Kad apsaugoti laiko juostą, nuotraukos/balso failai naudoja UTC.</string> + <string name="v5_2_message_delivery_receipts">Žinučių pristatymo kvitai!</string> + <string name="v5_1_better_messages_descr">- balso žinutės iki 5 minučių. +\n- pasirinktinis laikas išnykimui. +\n- istorijos redagavimas.</string> + <string name="v5_2_disappear_one_message">Padarykite, kad viena žinutė išnyktų</string> + <string name="v5_2_disappear_one_message_descr">Net jei tai išjungta pokalbyje.</string> + <string name="moderated_item_description">moderuota %s</string> + <string name="app_was_crashed">Rodinys sudužo</string> + <string name="description_you_shared_one_time_link">pasidalinote vienkartine nuoroda</string> + <string name="description_you_shared_one_time_link_incognito">pasidalinote vienkartine nuoroda inkognito režimu</string> + <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Kad apsaugoti savo informaciją, įjunkite SimpleX užraktą. +\nJūs būsite prašomi užbaigti autentifikaciją prieš įjungiant šią galimybę.</string> + <string name="icon_descr_waiting_for_image">Laukiama nuotraukos</string> + <string name="compose_send_direct_message_to_connect">Siųsti tiesioginę žinutę, kad prisijungti</string> + <string name="waiting_for_image">Laukiama nuotraukos</string> + <string name="you_can_accept_or_reject_connection">Kai asmenys išsiunčia užklausą prisijungti, galite ją priimti arba atmesti.</string> + <string name="you_can_view_invitation_link_again">Galite peržiūrėti pakvietimo nuorodą vėl, prisijungimo detalėse.</string> + <string name="markdown_in_messages">Markdown žinutėse</string> + <string name="to_verify_compare">Norėdami patvirtinti visapusį šifravimą su savo kontaktu, palyginkite (ar nuskanuokite) kodą ant savo įrenginių.</string> + <string name="xftp_servers">XFTP serveriai</string> + <string name="network_use_onion_hosts_required_desc">Onion serveriai bus reikalingi ryšiui. +\nAtkreipkite dėmesį: negalėsite prisijungti prie serverių be .onion adreso.</string> + <string name="save_settings_question">Išsaugoti nustatymus?</string> + <string name="to_reveal_profile_enter_password">Norėdami atskleisti paslėptą profilį, įveskite visą slaptažodį į paieškos laukelį puslapyje Jūsų pokalbių profiliai.</string> + <string name="no_contacts_selected">Nepasirinkti jokie kontaktai</string> + <string name="you_can_share_this_address_with_your_contacts">Galite bendrinti šią nuorodą su savo kontaktais, kad leistumėte jiems prisijungti prie %s.</string> + <string name="feature_offered_item">pasiūlė %s</string> + <string name="feature_offered_item_with_param">pasiūlė %s: %2s</string> + <string name="self_destruct_new_display_name">Naujas rodomas vardas:</string> + <string name="snd_conn_event_switch_queue_phase_completed_for_member">pakeitėte adresą %s</string> + <string name="icon_descr_expand_role">Išplėsti rolių pasirinkimą</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Jau jungiatės prie grupės <b>%1$s</b>.]]></string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Tik klientų įrenginiai saugo naudotojų profilius, kontaktus, grupes ir išsiųstas žinutes su <b>dviejų sluoksnių visapusiu šifravimu</b>.]]></string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">Kad apsaugoti privatumą, vietoj naudotojų ID naudojamų visose kitose platformose, SimpleX turi identifikatorius žinučių eilėms, skirtingus kiekvienam jūsų kontaktui.</string> + <string name="privacy_message_draft">Žinutės juodraštis</string> + <string name="settings_restart_app">Paleisti iš naujo</string> + <string name="group_member_role_member">narys</string> + <string name="feature_received_prohibited">gauta, uždrausta</string> + <string name="only_you_can_delete_messages">Tik jūs galite ištrinti žinutes negrįžtamai (jūsų kontaktas gali pažymėti jas ištrynimui). (24 valandas)</string> + <string name="v4_5_transport_isolation">Transporto izoliacija</string> + <string name="v5_3_simpler_incognito_mode_descr">Įjunkite inkognito režimą prisijungimo metu.</string> + <string name="remote_host_error_missing"><![CDATA[Mobilusis \u0020<b>%s</b> yra dingęs]]></string> + <string name="connect_plan_repeat_join_request">Pakartoti prisijungimo užklausą?</string> + <string name="error_showing_content">Įvyko klaida rodant turinį</string> + <string name="error_showing_message">Įvyko klaida rodant žinutę</string> + <string name="error_loading_details">Įvyko klaida įkeliant detales</string> + <string name="sender_cancelled_file_transfer">Siuntėjas atšaukė failo perdavimą</string> + <string name="moderate_verb">Moderuoti</string> + <string name="contact_wants_to_connect_with_you">nori prisijungti prie jūsų!</string> + <string name="image_descr_link_preview">nuorodos peržiūros nuotrauka</string> + <string name="onboarding_notifications_mode_off">Kai programėlė yra paleista</string> + <string name="lock_mode">Užrakto režimas</string> + <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Galite paleisti pokalbius per programėlės nustatymus/ duomenų bazę arba paleisdami programėlę iš naujo.</string> + <string name="rcv_group_event_user_deleted">pašalino jus</string> + <string name="info_row_moderated_at">Moderuota</string> + <string name="member_info_section_title_member">NARYS</string> + <string name="sender_at_ts">%s %s</string> + <string name="item_info_no_text">nėra teksto</string> + <string name="color_surface">Meniu ir įspėjimai</string> + <string name="v5_4_link_mobile_desktop">Susiekite mobiliasias ir darbastalio programėles! 🔗</string> + <string name="linked_mobiles">Susieti mobilieji</string> + <string name="verify_connection">Patvirtinti prisijungimą</string> + <string name="scan_from_mobile">Nuskanuoti iš mobiliojo</string> + <string name="verify_code_on_mobile">Patvirtinkite kodą mobiliąjame</string> + <string name="remote_host_was_disconnected_toast"><![CDATA[Mobilusis <b>%s</b> buvo atjungtas]]></string> + <string name="users_delete_data_only">Tik vietinio profilio duomenys</string> + <string name="connect_plan_repeat_connection_request">Pakartoti prisijungimo užklausą?</string> + <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">Jau yra prisijungiama su šia vienkartine nuoroda!</string> + <string name="new_member_role">Nauja nario rolė</string> + <string name="verify_connections">Patvirtinti prisijungimus</string> + <string name="waiting_for_desktop">Laukiama darbastalio…</string> + <string name="possible_slow_function_desc">Funkcijos vykdymas trunka per ilgai: %1$d sekundės: %2$s</string> + <string name="group_members_2">%s ir %s</string> + <string name="refresh_qr_code">Atnaujinti</string> + <string name="ensure_ICE_server_address_are_correct_format_and_unique">Įsitikinkite, kad WebRTC ICE serverių adresai yra tinkamu formatu, atskirose eilutėse ir nesikartojantys.</string> + <string name="you_can_create_it_later">Galite sukurti tai vėliau</string> + <string name="no_selected_chat">Nėra pasirinkto pokalbio</string> + <string name="group_member_status_unknown">nežinoma būsena</string> + <string name="group_member_status_removed">pašalintas</string> + <string name="group_member_status_unknown_short">nežinoma</string> + <string name="member_will_be_removed_from_group_cannot_be_undone">Narys bus pašalintas iš grupės - tai negali būti atšaukta!</string> + <string name="make_private_connection">Sukurkite privatų prisijungimą</string> + <string name="relay_server_if_necessary">Tarpinis serveris naudojamas tik jei to reikia. Kita šalis gali stebėti jūsų IP adresą.</string> + <string name="permissions_record_audio">Mikrofonas</string> + <string name="migrate_from_another_device">Persikelti iš kito įrenginio</string> + <string name="use_random_passphrase">Naudoti atsiktinę slaptafrazę</string> + <string name="call_connection_peer_to_peer">lygiaverčiai mazgai</string> + <string name="remove_passphrase_from_settings">Pašalinti slaptafrazę iš nustatymų?</string> + <string name="settings_section_title_delivery_receipts">SIŲSTI PRISTATYMO KVITUS PAS</string> + <string name="receipts_groups_override_disabled">Pristatymo kvitai yra išjungti %d grupėms</string> + <string name="you_must_use_the_most_recent_version_of_database">Turite naudoti pačią naujausią pokalbių duomenų bazės versiją TIK viename įrenginyje, kitaip galite nebegauti žinučių iš kai kurių kontaktų.</string> + <string name="new_passphrase">Nauja slaptafrazė…</string> + <string name="rcv_group_event_member_deleted">pašalino %1$s</string> + <string name="rcv_group_event_2_members_connected">%s ir %s prisijungė</string> + <string name="select_contacts">Pasirinkti kontaktus</string> + <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Jūs prisijungėte prie šios grupės. Prisijungiama prie kviečiančio grupės nario.</string> + <string name="no_contacts_to_add">Nėra kontaktų pridėjimui</string> + <string name="you_are_invited_to_group_join_to_connect_with_group_members">Esate kviečiami į grupę. Ateikite, kad prisijungti prie grupės narių.</string> + <string name="snd_group_event_member_blocked">užblokavote %s</string> + <string name="snd_group_event_changed_role_for_yourself">pakeitėte rolę sau į %s</string> + <string name="snd_group_event_changed_member_role">pakeitėte rolę %s į %s</string> + <string name="error_removing_member">Įvyko klaida pašalinant narį</string> + <string name="error_sending_message_contact_invitation">Įvyko klaida siunčiant kvietimą</string> + <string name="unblock_member_desc">Žinutės nuo %s bus rodomos!</string> + <string name="remove_member_button">Pašalinti narį</string> + <string name="save_welcome_message_question">Išsaugoti sveikinimo žinutę?</string> + <string name="unblock_member_confirmation">Atblokuoti</string> + <string name="unblock_member_button">Atblokuoti narį</string> + <string name="unblock_member_question">Atblokuoti narį?</string> + <string name="unblock_for_all_question">Atblokuoti narį visiems?</string> + <string name="group_welcome_title">Sveikinimo žinutė</string> + <string name="make_profile_private">Padarykite profilį privatų!</string> + <string name="import_theme_error_desc">Įsitikinkite, kad failas turi teisingą YAML sintaksę. Eksportuokite temą, kad gautumėte temos failo struktūros pavyzdį.</string> + <string name="message_reactions_are_prohibited">Žinučių reakcijos yra draudžiamos šioje grupėje.</string> + <string name="v4_4_live_messages_desc">Gavėjai mato pakeitimus kai juos rašote.</string> + <string name="you_will_still_receive_calls_and_ntfs">Vistiek gausite skambučius ir pranešimus iš nutildytų profilių kai jie yra aktyvūs.</string> + <string name="muted_when_inactive">Nutildyta kai neaktyvu!</string> + <string name="incognito_info_share">Kai jūs bendrinate inkognito profilį su kažkuom, šis profilis bus naudojamas grupėms į kurias jie jus pakviečia.</string> + <string name="you_can_hide_or_mute_user_profile">Galite paslėpti ar nutildyti naudotojo profilį - laikykite jį, kad atidaryti meniu.</string> + <string name="custom_time_unit_seconds">sekundės</string> + <string name="custom_time_picker_select">Pasirinkti</string> + <string name="v5_4_block_group_members_descr">Kad paslėptumėte nenorimas žinutes.</string> + <string name="custom_time_unit_minutes">minutės</string> + <string name="you_can_enable_delivery_receipts_later">Galite įjungti vėliau per nustatymus</string> + <string name="you_can_enable_delivery_receipts_later_alert">Galite įjungti juos vėliau per programėlės privatumo ir saugumo nustatymus.</string> + <string name="link_a_mobile">Susieti mobilujį</string> + <string name="restart_chat_button">Paleisti pokalbius iš naujo</string> + <string name="migrate_to_device_title">Persikelti į čia</string> + <string name="migrate_to_device_migrating">Persikėliama</string> + <string name="remote_host_error_bad_version"><![CDATA[Moblusis <b>%s</b> turi nepalaikomą versiją. Įsitikinkite, kad naudojate tą pačią versiją abiejuose įrenginiuose]]></string> + <string name="new_mobile_device">Naujas mobilusis įrenginys</string> + <string name="no_connected_mobile">Nėra prisijungusių mobiliųjų</string> + <string name="unlink_desktop">Atsieti</string> + <string name="unlink_desktop_question">Atsieti darbastalį?</string> + <string name="verify_code_with_desktop">Patvirtinkite kodą darbastalyje</string> + <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Jau jungiatės prie grupės su šia nuoroda.</string> + <string name="migrate_to_device_try_again">Galite bandyti dar kartą.</string> + <string name="migrate_from_device_repeat_upload">Pakartoti įkėlimą</string> + <string name="migrate_from_device_upload_failed">Įkėlimas nepavyko</string> + <string name="migrate_from_device_uploading_archive">Įkeliamas archyvas</string> + <string name="migrate_from_device_try_again">Galite bandyti dar kartą.</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="feature_roles_all_members">visi nariai</string> + <string name="audio_device_earpiece">Ausinė</string> + <string name="audio_device_wired_headphones">Ausinės</string> + <string name="feature_roles_admins">administratoriai</string> + <string name="forwarded_chat_item_info_tab">Persiųsta</string> + <string name="download_file">Atsisiųsti</string> + <string name="forward_chat_item">Persiųsti</string> + <string name="files_and_media_not_allowed">Failai ir medija neleidžiami</string> + <string name="forwarded_description">persiųsta</string> + <string name="allow_to_send_simplex_links">Leisti siųsti SimpleX nuorodas</string> + <string name="group_members_can_send_simplex_links">Grupės nariai gali siųsti SimpleX nuorodas.</string> + <string name="feature_enabled_for">Įjungta:</string> + <string name="network_type_cellular">Mobilusis</string> + <string name="v5_7_forward">Persiųskite ir išsaugokite žinutes</string> + <string name="forwarded_from_chat_item_info_title">Persiųsta iš</string> + <string name="forward_message">Persiųsti žinutę…</string> + <string name="v5_7_forward_descr">Žinutės šaltinis išlieka privatus.</string> + <string name="v5_7_network_descr">Patikimesnis tinklo ryšys.</string> + <string name="settings_section_title_network_connection">Tinklo ryšys</string> + <string name="v5_7_call_sounds">Garsai skambučio metu</string> + <string name="v5_7_new_interface_languages">Lietuviškas UI</string> + <string name="network_option_rcv_concurrency">Gavimas vienu metu</string> + <string name="audio_device_speaker">Garsiakalbis</string> + <string name="v5_7_network">Tinklo valdymas</string> + <string name="saved_description">išsaugota</string> + <string name="saved_from_description">išsaugota iš %s</string> + <string name="saved_chat_item_info_tab">Išsaugota</string> + <string name="voice_messages_not_allowed">Balso žinutės neleidžiamos</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="simplex_links">SimpleX nuorodos</string> + <string name="prohibit_sending_simplex_links">Drausti SimpleX nuorodų siuntimą</string> + <string name="simplex_links_are_prohibited_in_group">SimpleX nuorodos yra uždraustos šioje grupėje.</string> + <string name="feature_roles_owners">savininkai</string> + <string name="network_type_ethernet">Laidinis internetas</string> + <string name="network_type_other">Kitas</string> + <string name="network_type_no_network_connection">Nėra interneto ryšio</string> + <string name="saved_from_chat_item_info_title">Išsaugota iš</string> + <string name="recipients_can_not_see_who_message_from">Gavėjas(-ai) negali matyti kas šią žinutę išsiuntė.</string> + <string name="simplex_links_not_allowed">SimpleX nuorodos neleidžiamos</string> + <string name="v5_7_call_sounds_descr">Kai jungiami garso ir vaizdo skambučiai.</string> + <string name="v5_7_quantum_resistant_encryption_descr">Bus įjungta tiesioginiuose pokalbiuose!</string> + <string name="dark_mode_colors">Juodo režimo spalvos</string> + <string name="v5_8_safe_files_descr">Patvirtinkite failus iš nežinomų serverių.</string> + <string name="snd_error_relay">Paskirties serverio klaida: %1$s</string> + <string name="ci_status_other_error">Klaida: %1$s</string> + <string name="color_mode_dark">Tamsu</string> + <string name="color_mode">Spalvos režimas</string> + <string name="theme_destination_app_theme">Programėlės tema</string> + <string name="color_primary_variant2">Pridėtinis akcentas 2</string> + <string name="wallpaper_advanced_settings">Pažangūs nustatymai</string> + <string name="chat_theme_apply_to_all_modes">Visi spalvų režimai</string> + <string name="network_smp_proxy_mode_always">Visada</string> + <string name="settings_section_title_chat_colors">Pokalbio spalvos</string> + <string name="chat_theme_apply_to_dark_mode">Juodas režimas</string> + <string name="theme_black">Juoda</string> + <string name="snd_error_quota">Talpa viršyta – gavėjas negavo anksčiau išsiųstų žinučių.</string> + <string name="settings_section_title_chat_theme">Pokalbio tema</string> + <string name="network_smp_proxy_fallback_prohibit_description">NESIŲSTI žinučių tiesiogiai, net jei jūsų ar paskirties serveris nepalaiko privataus maršruto.</string> + <string name="network_smp_proxy_mode_always_description">Visada naudoti privatų maršrutą.</string> + <string name="network_smp_proxy_mode_never_description">NENAUDOTI privataus maršruto.</string> + <string name="network_smp_proxy_fallback_allow">Taip</string> + <string name="copy_error">Kopijavimo klaida</string> + <string name="chat_theme_apply_to_mode">Pritaikyti prie</string> + <string name="n_file_errors">%1$d failo klaida (-os):\n%2$s.</string> + <string name="forward_files_in_progress_desc">%1$d failas (-ai, -ų) vis dar atsisiunčiamas (-i, -a).</string> + <string name="forward_files_failed_to_receive_desc">Nepavyko atsisiųsti %1$d failo (-ų).</string> + <string name="selected_chat_items_selected_n">%d pasirinkta</string> +</resources> 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 92bd3e381a..dc692c1968 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ml/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ml/strings.xml @@ -109,7 +109,7 @@ <string name="back">തിരികെ</string> <string name="mute_chat">നിശബ്ദമാക്കുക</string> <string name="icon_descr_help">സഹായം</string> - <string name="smp_servers_add">സെർവർ ചേർക്കുക…</string> + <string name="smp_servers_add">സെർവർ ചേർക്കുക</string> <string name="smp_servers_add_to_another_device">മറ്റൊരു ഉപകരണത്തിലേക്ക് ചേർക്കുക</string> <string name="auto_accept_contact">സ്വയമേവ സ്വീകരിക്കുക</string> <string name="callstate_waiting_for_confirmation">സ്ഥിരീകരണത്തിനായി കാത്തിരിക്കുന്നു…</string> @@ -280,7 +280,7 @@ <string name="custom_time_unit_weeks">ആഴ്ചകൾ</string> <string name="icon_descr_sent_msg_status_unauthorized_send">അനധികൃത അയക്കുക</string> <string name="switch_receiving_address_question">സ്വീകരിക്കുന്ന വിലാസം മാറണോ\?</string> - <string name="member_role_will_be_changed_with_invitation">കര്‍ത്തവ്യം \"%s\" ആയി മാറ്റും. അംഗത്തിന് പുതിയ ക്ഷണം ലഭിക്കും.</string> + <string name="member_role_will_be_changed_with_invitation">കര്‍ത്തവ്യം "%s" ആയി മാറ്റും. അംഗത്തിന് പുതിയ ക്ഷണം ലഭിക്കും.</string> <string name="la_lock_mode_system">സംവിധാനം പ്രാമാണീകരണം</string> <string name="skip_inviting_button">അംഗങ്ങളെ ക്ഷണിക്കുന്നത് ഒഴിവാക്കുക</string> <string name="save_welcome_message_question">സ്വാഗത സന്ദേശം സംരക്ഷിക്കണോ\?</string> @@ -320,10 +320,8 @@ <string name="switch_receiving_address">സ്വീകരിക്കുന്ന വിലാസം മാറുക</string> <string name="conn_stats_section_title_servers">സെർവറുകൾ</string> <string name="network_options_save">സംരക്ഷിക്കുക</string> - <string name="network_options_revert">പഴയപടിയാക്കുക</string> <string name="theme_system">സംവിധാനം</string> <string name="language_system">സംവിധാനം</string> - <string name="save_color">നിറം സംരക്ഷിക്കുക</string> <string name="color_title">ശീർഷകം</string> <string name="color_secondary">രണ്ടാംതരമായ</string> <string name="chat_preferences_yes">അതെ</string> 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 5f45304aac..d07cb6db39 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -19,12 +19,11 @@ <string name="network_enable_socks_info">Toegang tot de servers via SOCKS proxy op poort %d\? De proxy moet worden gestart voordat u deze optie inschakelt.</string> <string name="alert_title_cant_invite_contacts">Kan geen contacten uitnodigen!</string> <string name="allow_direct_messages">Sta het verzenden van directe berichten naar leden toe.</string> - <string name="allow_to_delete_messages">Sta toe om verzonden berichten onomkeerbaar te verwijderen.</string> + <string name="allow_to_delete_messages">Sta toe om verzonden berichten definitief te verwijderen. (24 uur)</string> <string name="allow_to_send_voice">Sta toe om spraak berichten te verzenden.</string> <string name="chat_is_running">Chat is actief</string> <string name="clear_chat_menu_action">Wissen</string> <string name="chat_database_section">CHAT DATABASE</string> - <string name="chat_archive_section">CHAT ARCHIEF</string> <string name="chat_console">Chat console</string> <string name="chat_database_imported">Chat database geïmporteerd</string> <string name="chat_database_deleted">Chat database verwijderd</string> @@ -39,7 +38,7 @@ <string name="accept_contact_incognito_button">Accepteer incognito</string> <string name="smp_servers_preset_add">Vooraf ingestelde servers toevoegen</string> <string name="users_add">Profiel toevoegen</string> - <string name="smp_servers_add">Server toevoegen…</string> + <string name="smp_servers_add">Server toevoegen</string> <string name="smp_servers_add_to_another_device">Toevoegen aan een ander apparaat</string> <string name="v4_2_group_links_desc">Beheerders kunnen de uitnodiging links naar groepen aanmaken.</string> <string name="v4_3_improved_server_configuration_desc">Servers toevoegen door QR-codes te scannen.</string> @@ -48,8 +47,7 @@ <string name="allow_verb">Toestaan</string> <string name="chat_item_ttl_day">1 dag</string> <string name="accept_feature">Accepteer</string> - <string name="network_session_mode_entity_description">Er wordt een afzonderlijke TCP-verbinding (en SOCKS-referentie) gebruikt <b> voor elk contact en groepslid </b>. -\n<b>Let op</b>: als u veel verbindingen heeft, kan uw batterij- en verkeersverbruik aanzienlijk hoger zijn en kunnen sommige verbindingen uitvallen.</string> + <string name="network_session_mode_entity_description"><![CDATA[Er wordt een aparte TCP-verbinding (en SOCKS-referentie) gebruikt <b>voor elk contact en groepslid</b>.\n<b>Let op</b>: als u veel verbindingen hebt, kan het batterij- en verkeersverbruik aanzienlijk hoger zijn en kunnen sommige verbindingen mislukken.]]></string> <string name="icon_descr_audio_call">audio oproep</string> <string name="icon_descr_audio_on">Geluid aan</string> <string name="settings_audio_video_calls">Audio en video gesprekken</string> @@ -59,25 +57,24 @@ <string name="v4_2_auto_accept_contact_requests">Contact verzoeken automatisch accepteren</string> <string name="bold_text">vetgedrukt</string> <string name="attach">Bijvoegen</string> - <string name="allow_irreversible_message_deletion_only_if">Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contact dit toestaat.</string> + <string name="allow_irreversible_message_deletion_only_if">Sta het definitief verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur)</string> <string name="allow_to_send_disappearing">Sta toe om verdwijnende berichten te verzenden.</string> <string name="allow_your_contacts_to_send_voice_messages">Sta toe dat uw contacten spraak berichten verzenden.</string> <string name="all_your_contacts_will_remain_connected">Al uw contacten blijven verbonden.</string> <string name="allow_voice_messages_question">Spraak berichten toestaan\?</string> - <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Goed voor de batterij</b>. Achtergrondservice controleert berichten elke 10 minuten. Mogelijk mist u oproepen of dringende berichten.]]></string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Goed voor de batterij</b>. App controleert berichten elke 10 minuten. U kunt oproepen of urgente berichten missen.]]></string> <string name="integrity_msg_bad_hash">Onjuiste bericht hash</string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Scan QR-code</b>: om verbinding te maken met uw contact die u de QR-code laat zien.]]></string> <string name="integrity_msg_bad_id">Onjuiste bericht-ID</string> <string name="call_already_ended">Oproep al beëindigd!</string> <string name="chat_item_ttl_month">1 maand</string> <string name="about_simplex">Over SimpleX</string> <string name="about_simplex_chat">Over SimpleX Chat</string> <string name="above_then_preposition_continuation">hier boven, dan:</string> - <string name="users_delete_all_chats_deleted">Alle gesprekken en berichten worden verwijderd, dit kan niet ongedaan worden gemaakt!</string> + <string name="users_delete_all_chats_deleted">Alle chats en berichten worden verwijderd, dit kan niet ongedaan worden gemaakt!</string> <string name="clear_chat_warning">Alle berichten worden verwijderd, dit kan niet ongedaan worden gemaakt! De berichten worden ALLEEN voor jou verwijderd.</string> <string name="allow_disappearing_messages_only_if">Sta verdwijnende berichten alleen toe als uw contact dit toestaat.</string> <string name="allow_voice_messages_only_if">Sta spraak berichten alleen toe als uw contact ze toestaat.</string> - <string name="allow_your_contacts_irreversibly_delete">Laat uw contacten verzonden berichten onomkeerbaar verwijderen.</string> + <string name="allow_your_contacts_irreversibly_delete">Laat uw contacten verzonden berichten definitief verwijderen. (24 uur)</string> <string name="allow_your_contacts_to_send_disappearing_messages">Sta toe dat uw contacten verdwijnende berichten verzenden.</string> <string name="chat_preferences_always">altijd</string> <string name="icon_descr_audio_off">Geluid uit</string> @@ -91,19 +88,18 @@ <string name="settings_section_title_icon">APP ICON</string> <string name="app_version_title">App versie</string> <string name="app_version_name">App versie: v%s</string> - <string name="network_session_mode_user_description"><![CDATA[Er wordt een aparte TCP-verbinding (en SOCKS-referentie) gebruikt <b> voor elk chat profiel dat je in de app hebt </b>.]]></string> + <string name="network_session_mode_user_description"><![CDATA[Er wordt een aparte TCP-verbinding (en SOCKS-referentie) gebruikt <b> voor elk chatprofiel dat je in de app hebt </b>.]]></string> <string name="audio_call_no_encryption">audio oproep (niet e2e versleuteld)</string> <string name="notifications_mode_service_desc">Achtergrondservice is altijd actief, meldingen worden weergegeven zodra de berichten beschikbaar zijn.</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Nieuw contact toevoegen</b>: om een eenmalige QR-code voor uw contact te maken.]]></string> <string name="icon_descr_call_ended">Oproep beëindigd</string> <string name="turning_off_service_and_periodic">Batterijoptimalisatie is actief, waardoor achtergrondservice en periodieke verzoeken om nieuwe berichten worden uitgeschakeld. Je kunt ze weer inschakelen via instellingen.</string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Het beste voor de batterij</b>. U ontvangt alleen meldingen wanneer de app wordt uitgevoerd (GEEN achtergrondservice).]]></string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Het kan worden uitgeschakeld via instellingen</b>, meldingen worden nog steeds weergegeven terwijl de app actief is.]]></string> - <string name="both_you_and_your_contacts_can_delete">Zowel jij als je contact kunnen verzonden berichten onherroepelijk verwijderen.</string> + <string name="both_you_and_your_contacts_can_delete">Zowel u als uw contact kunnen verzonden berichten definitief verwijderen. (24 uur)</string> <string name="both_you_and_your_contact_can_send_disappearing">Zowel jij als je contact kunnen verdwijnende berichten sturen.</string> <string name="both_you_and_your_contact_can_send_voice">Zowel jij als je contact kunnen spraak berichten verzenden.</string> <string name="impossible_to_recover_passphrase"><![CDATA[<b>Let op</b>: u kunt het wachtwoord NIET herstellen of wijzigen als u het kwijt raakt.]]></string> - <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Gebruikt meer batterij</b>! Achtergrondservice wordt altijd uitgevoerd - meldingen worden weergegeven zodra berichten beschikbaar zijn.]]></string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Gebruikt meer batterij</b>! App draait altijd op de achtergrond – meldingen worden direct weergegeven.]]></string> <string name="icon_descr_cancel_link_preview">link voorbeeld annuleren</string> <string name="callstatus_ended">oproep beëindigd %1$s</string> <string name="database_initialization_error_title">Kan de database niet initialiseren</string> @@ -119,12 +115,11 @@ <string name="chat_is_stopped">Chat is gestopt</string> <string name="notifications_mode_periodic_desc">Controleert nieuwe berichten elke 10 minuten gedurende maximaal 1 minuut</string> <string name="rcv_group_event_changed_your_role">je rol gewijzigd in %s</string> - <string name="chat_archive_header">Gesprek archief</string> <string name="change_database_passphrase_question">Wachtwoord database wijzigen\?</string> <string name="chat_is_stopped_indication">Chat is gestopt</string> - <string name="chat_preferences">Gesprek voorkeuren</string> - <string name="network_session_mode_user">Chat profiel</string> - <string name="settings_section_title_chats">GESPREKKEN</string> + <string name="chat_preferences">Chat voorkeuren</string> + <string name="network_session_mode_user">Chatprofiel</string> + <string name="settings_section_title_chats">CHATS</string> <string name="chat_with_developers">Praat met de ontwikkelaars</string> <string name="smp_servers_check_address">Controleer het server adres en probeer het opnieuw.</string> <string name="choose_file">Bestand</string> @@ -182,7 +177,7 @@ <string name="database_will_be_encrypted_and_passphrase_stored">"De database wordt versleuteld en het wachtwoord wordt opgeslagen in de Keychain."</string> <string name="database_passphrase_will_be_updated">Het wachtwoord voor database versleuteling wordt bijgewerkt.</string> <string name="database_error">Database fout</string> - <string name="database_passphrase_is_required">Database wachtwoord is vereist om je gesprekken te openen.</string> + <string name="database_passphrase_is_required">Database wachtwoord is vereist om je chats te openen.</string> <string name="contact_already_exists">Contact bestaat al</string> <string name="icon_descr_call_connecting">Oproep verbinden</string> <string name="button_create_group_link">Maak link</string> @@ -203,13 +198,12 @@ <string name="clear_verification">Verwijderd verificatie</string> <string name="connect_button">Verbind</string> <string name="connect_via_link">Maak verbinding via link</string> - <string name="create_one_time_link">Maak een eenmalige uitnodiging link</string> <string name="colored_text">gekleurd</string> <string name="callstatus_connecting">Oproep verbinden…</string> <string name="create_profile_button">Maak</string> <string name="create_profile">Maak een profiel aan</string> <string name="delete_address">Adres verwijderen</string> - <string name="always_use_relay">Verbinden via relais</string> + <string name="always_use_relay">Altijd relay gebruiken</string> <string name="status_contact_has_e2e_encryption">contact heeft e2e-codering</string> <string name="status_contact_has_no_e2e_encryption">contact heeft geen e2e versleuteling</string> <string name="set_password_to_export_desc">De database is versleuteld met een willekeurige wachtwoord. Wijzig dit voordat u exporteert.</string> @@ -218,7 +212,6 @@ <string name="current_passphrase">Huidige wachtwoord…</string> <string name="database_encrypted">Database versleuteld!</string> <string name="rcv_group_event_member_connected">is toegetreden</string> - <string name="archive_created_on_ts">Gemaakt op %1$s</string> <string name="group_member_status_complete">compleet</string> <string name="clear_contacts_selection_button">Wissen</string> <string name="group_member_status_connected">verbonden</string> @@ -233,10 +226,8 @@ <string name="contacts_can_mark_messages_for_deletion">Contact personen kunnen berichten markeren voor verwijdering; u kunt ze wel bekijken.</string> <string name="theme_dark">Donker</string> <string name="chat_preferences_default">standaard (%s)</string> - <string name="delete_chat_archive_question">Chat archief verwijderen\?</string> - <string name="delete_archive">Archief verwijderen</string> <string name="delete_contact_question">Verwijder contact\?</string> - <string name="delete_chat_profile_question">Chat profiel verwijderen\?</string> + <string name="delete_chat_profile_question">Chatprofiel verwijderen?</string> <string name="full_deletion">Verwijderen voor iedereen</string> <string name="delete_link">Link verwijderen</string> <string name="conn_level_desc_direct">direct</string> @@ -253,7 +244,7 @@ <string name="delete_message__question">Verwijder bericht\?</string> <string name="delete_messages">Verwijder berichten</string> <string name="smp_server_test_delete_queue">Wachtrij verwijderen</string> - <string name="delete_files_and_media_for_all_users">Verwijder bestanden voor alle chat profielen</string> + <string name="delete_files_and_media_for_all_users">Verwijder bestanden voor alle chatprofielen</string> <string name="for_me_only">Verwijder voor mij</string> <string name="button_delete_group">Groep verwijderen</string> <string name="delete_link_question">Link verwijderen\?</string> @@ -263,12 +254,12 @@ <string name="auth_device_authentication_is_disabled_turning_off">Apparaatverificatie is uitgeschakeld. SimpleX Vergrendelen uitschakelen.</string> <string name="display_name">Vul uw naam in:</string> <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Apparaatverificatie is niet ingeschakeld. Je kunt SimpleX Vergrendelen inschakelen via Instellingen zodra je apparaatverificatie hebt ingeschakeld.</string> - <string name="direct_messages_are_prohibited_in_chat">Directe berichten tussen leden zijn verboden in deze groep.</string> + <string name="direct_messages_are_prohibited_in_group">Directe berichten tussen leden zijn niet toegestaan in deze groep.</string> <string name="total_files_count_and_size">%d bestand(en) met een totale grootte van %s</string> <string name="ttl_hour">%d uur</string> <string name="no_call_on_lock_screen">Uitzetten</string> <string name="v4_4_disappearing_messages">Verdwijnende berichten</string> - <string name="disappearing_prohibited_in_this_chat">Verdwijnende berichten zijn verboden in dit gesprek.</string> + <string name="disappearing_prohibited_in_this_chat">Verdwijnende berichten zijn niet toegestaan in dit gesprek.</string> <string name="auth_disable_simplex_lock">SimpleX Vergrendelen uitschakelen</string> <string name="timed_messages">Verdwijnende berichten</string> <string name="smp_server_test_disconnect">Verbinding verbreken</string> @@ -281,13 +272,13 @@ <string name="ttl_s">%ds</string> <string name="button_delete_contact">Verwijder contact</string> <string name="smp_servers_delete_server">Server verwijderen</string> - <string name="disappearing_messages_are_prohibited">Verdwijnende berichten zijn verboden in deze groep.</string> + <string name="disappearing_messages_are_prohibited">Verdwijnende berichten zijn niet toegestaan.</string> <string name="ttl_sec">%d sec</string> <string name="ttl_m">%dm</string> <string name="ttl_mth">%dmth</string> <string name="ttl_hours">%d uren</string> <string name="ttl_h">%dh</string> - <string name="users_delete_question">Chat profiel verwijderen\?</string> + <string name="users_delete_question">Chatprofiel verwijderen?</string> <string name="users_delete_profile_for">Chat profiel verwijderen voor</string> <string name="deleted_description">verwijderd</string> <string name="simplex_link_mode_description">Beschrijving</string> @@ -347,10 +338,10 @@ <string name="feature_enabled">ingeschakeld</string> <string name="feature_enabled_for_contact">ingeschakeld voor contact</string> <string name="feature_enabled_for_you">voor u ingeschakeld</string> - <string name="group_members_can_delete">Groepsleden kunnen verzonden berichten onherroepelijk verwijderen.</string> - <string name="group_members_can_send_dms">Groepsleden kunnen directe berichten sturen</string> - <string name="group_members_can_send_voice">Groepsleden kunnen spraak berichten verzenden.</string> - <string name="v4_5_transport_isolation_descr">Per chat profiel (standaard) of per verbinding (BETA).</string> + <string name="group_members_can_delete">Leden kunnen verzonden berichten definitief verwijderen. (24 uur)</string> + <string name="group_members_can_send_dms">Leden kunnen directe berichten sturen.</string> + <string name="group_members_can_send_voice">Leden kunnen spraak berichten verzenden.</string> + <string name="v4_5_transport_isolation_descr">Per chatprofiel (standaard) of per verbinding (BETA).</string> <string name="v4_5_multiple_chat_profiles_descr">Verschillende namen, avatars en transportisolatie.</string> <string name="v4_4_french_interface">Franse interface</string> <string name="error_saving_group_profile">Fout bij opslaan van groep profiel</string> @@ -382,7 +373,7 @@ <string name="gallery_video_button">Video</string> <string name="error_saving_ICE_servers">Fout bij opslaan van ICE servers</string> <string name="callstate_ended">geëindigd</string> - <string name="group_members_can_send_disappearing">Groepsleden kunnen verdwijnende berichten sturen.</string> + <string name="group_members_can_send_disappearing">Leden kunnen verdwijnende berichten sturen.</string> <string name="ttl_week">%d week</string> <string name="ttl_w">%dw</string> <string name="ttl_weeks">%d weken</string> @@ -395,7 +386,7 @@ <string name="error_saving_smp_servers">Fout bij opslaan van SMP servers</string> <string name="error_setting_network_config">Fout bij updaten van netwerk configuratie</string> <string name="failed_to_parse_chat_title">Kan het gesprek niet laden</string> - <string name="failed_to_parse_chats_title">Kan de gesprekken niet laden</string> + <string name="failed_to_parse_chats_title">Kan de chats niet laden</string> <string name="simplex_link_mode_full">Volledige link</string> <string name="integrity_msg_duplicate">dubbel bericht</string> <string name="invalid_connection_link">Ongeldige verbinding link</string> @@ -405,7 +396,7 @@ <string name="icon_descr_image_snd_complete">Afbeelding verzonden</string> <string name="live_message">Live bericht!</string> <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Als je een uitnodiging link voor SimpleX Chat hebt ontvangen, kun je deze in je browser openen:</string> - <string name="onboarding_notifications_mode_subtitle">Dit kan later worden gewijzigd via instellingen.</string> + <string name="onboarding_notifications_mode_subtitle">Hoe dit de batterij beïnvloedt</string> <string name="join_group_question">Deelnemen aan groep\?</string> <string name="icon_descr_add_members">Nodig leden uit</string> <string name="no_contacts_selected">Geen contacten geselecteerd</string> @@ -428,13 +419,12 @@ <string name="how_to_use_simplex_chat">Hoe te gebruiken</string> <string name="markdown_help">Markdown hulp</string> <string name="markdown_in_messages">Markdown in berichten</string> - <string name="network_settings_title">Netwerk instellingen</string> + <string name="network_settings_title">Geavanceerde instellingen</string> <string name="how_to_use_markdown">Markdown gebruiken</string> <string name="italic_text">cursief</string> <string name="how_it_works">Hoe het werkt</string> <string name="callstatus_missed">gemiste oproep</string> <string name="how_simplex_works">Hoe SimpleX werkt</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[Veel mensen vroegen: <i>als SimpleX geen gebruikers-ID\'s heeft, hoe kan het dan berichten bezorgen\?</i>]]></string> <string name="incoming_audio_call">Inkomende audio oproep</string> <string name="incoming_video_call">Inkomend video gesprek</string> <string name="ignore">Negeren</string> @@ -443,32 +433,32 @@ <string name="chat_item_ttl_none">nooit</string> <string name="no_received_app_files">Geen ontvangen of verzonden bestanden</string> <string name="alert_title_group_invitation_expired">Uitnodiging verlopen!</string> - <string name="rcv_group_event_member_left">verlaten</string> - <string name="group_member_status_left">verlaten</string> + <string name="rcv_group_event_member_left">is vertrokken</string> + <string name="group_member_status_left">vertrokken</string> <string name="group_member_status_invited">uitgenodigd</string> <string name="button_leave_group">Groep verlaten</string> <string name="info_row_local_name">Lokale naam</string> <string name="users_delete_data_only">Alleen lokale profielgegevens</string> - <string name="message_deletion_prohibited_in_chat">Het onomkeerbaar verwijderen van berichten is verboden in deze groep.</string> + <string name="message_deletion_prohibited_in_chat">Het definitief verwijderen van berichten is niet toegestaan.</string> <string name="v4_3_improved_privacy_and_security_desc">App scherm verbergen in de recente apps.</string> <string name="settings_section_title_incognito">Incognito modus</string> <string name="messages_section_title">Berichten</string> <string name="new_passphrase">Nieuw wachtwoord…</string> <string name="keychain_error">Keychain fout</string> - <string name="join_group_button">Word lid van</string> + <string name="join_group_button">Word lid</string> <string name="leave_group_question">Groep verlaten\?</string> <string name="new_member_role">Nieuwe leden rol</string> <string name="no_contacts_to_add">Geen contacten om toe te voegen</string> - <string name="incognito_info_allows">Het maakt het mogelijk om veel anonieme verbindingen te hebben zonder enige gedeelde gegevens tussen hen in een enkel chat profiel.</string> + <string name="incognito_info_allows">Het maakt het mogelijk om veel anonieme verbindingen te hebben zonder enige gedeelde gegevens tussen hen in een enkel chatprofiel.</string> <string name="theme_light">Licht</string> <string name="chat_preferences_no">nee</string> - <string name="v4_5_multiple_chat_profiles">Meerdere chat profielen</string> + <string name="v4_5_multiple_chat_profiles">Meerdere chatprofielen</string> <string name="v4_5_italian_interface">Italiaanse interface</string> <string name="v4_5_message_draft">Concept bericht</string> <string name="v4_5_reduced_battery_usage_descr">Meer verbeteringen volgen snel!</string> <string name="button_add_members">Nodig leden uit</string> <string name="notification_display_mode_hidden_desc">Verberg contact en bericht</string> - <string name="turn_off_battery_optimization"><![CDATA[Om het te gebruiken, kunt u Simplex in het volgende dialoogvenster \u0020<b>op de achtergrond uitvoeren</b>. Anders worden de meldingen uitgeschakeld.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[<b>Sta dit toe</b> in het volgende dialoogvenster om direct meldingen te ontvangen.]]></string> <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Als u ervoor kiest om te weigeren, wordt de afzender NIET op de hoogte gesteld.</string> <string name="onboarding_notifications_mode_service">Onmiddellijk</string> <string name="rcv_group_event_member_added">heeft %1$s uitgenodigd</string> @@ -516,9 +506,9 @@ <string name="ensure_ICE_server_address_are_correct_format_and_unique">Zorg ervoor dat WebRTC ICE server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn.</string> <string name="network_disable_socks_info">Als u bevestigt, kunnen de berichten servers en uw provider uw IP-adres zien en met welke servers u verbinding maakt.</string> <string name="network_use_onion_hosts_no">Nee</string> - <string name="immune_to_spam_and_abuse">Immuun voor spam en misbruik</string> + <string name="immune_to_spam_and_abuse">Immuun voor spam</string> <string name="make_private_connection">Maak een privéverbinding</string> - <string name="message_deletion_prohibited">Het onomkeerbaar verwijderen van berichten is verboden in dit gesprek.</string> + <string name="message_deletion_prohibited">Het definitief verwijderen van berichten is niet toegestaan in dit gesprek.</string> <string name="new_in_version">Nieuw in %s</string> <string name="v4_3_voice_messages_desc">Max 40 seconden, direct ontvangen.</string> <string name="v4_3_improved_server_configuration">Verbeterde serverconfiguratie</string> @@ -526,7 +516,7 @@ <string name="rcv_group_event_invited_via_your_group_link">uitgenodigd via je groep link</string> <string name="incognito">Incognito</string> <string name="icon_descr_call_missed">Gemiste oproep</string> - <string name="description_via_contact_address_link_incognito">incognito via contactadres link</string> + <string name="description_via_contact_address_link_incognito">incognito via contact adres link</string> <string name="description_via_group_link_incognito">incognito via groep link</string> <string name="description_via_one_time_link_incognito">incognito via eenmalige link</string> <string name="invalid_chat">ongeldige gesprek</string> @@ -549,9 +539,8 @@ <string name="one_time_link">Eenmalige uitnodiging link</string> <string name="paste_button">Plakken</string> <string name="smp_servers_preset_address">Vooraf ingesteld server adres</string> - <string name="network_use_onion_hosts_no_desc_in_alert">Onion hosts worden niet gebruikt.</string> <string name="onboarding_notifications_mode_title">Privé meldingen</string> - <string name="paste_the_link_you_received">Plak de ontvangen link</string> + <string name="paste_the_link_you_received">Plak de link die je hebt ontvangen</string> <string name="onboarding_notifications_mode_periodic">Periodiek</string> <string name="open_verb">Open</string> <string name="open_simplex_chat_to_accept_call">Open SimpleX Chat om de oproep te accepteren</string> @@ -563,7 +552,7 @@ <string name="chat_preferences_on">aan</string> <string name="only_you_can_send_disappearing">Alleen jij kunt verdwijnende berichten verzenden.</string> <string name="only_your_contact_can_send_disappearing">Alleen uw contact kan verdwijnende berichten verzenden.</string> - <string name="only_you_can_delete_messages">Alleen jij kunt berichten onomkeerbaar verwijderen (je contact kan ze markeren voor verwijdering).</string> + <string name="only_you_can_delete_messages">Alleen u kunt berichten definitief verwijderen (uw contact kan ze markeren voor verwijdering). (24 uur)</string> <string name="feature_offered_item_with_param">voorgesteld %s: %2s</string> <string name="old_database_archive">Oud database archief</string> <string name="enter_correct_current_passphrase">Voer het juiste huidige wachtwoord in.</string> @@ -576,7 +565,6 @@ <string name="enter_passphrase_notification_title">Wachtwoord is nodig</string> <string name="feature_off">uit</string> <string name="add_contact">Eenmalige uitnodiging link</string> - <string name="network_use_onion_hosts_required_desc_in_alert">Onion hosts zijn vereist voor verbinding.</string> <string name="only_group_owners_can_change_prefs">Alleen groep eigenaren kunnen groep voorkeuren wijzigen.</string> <string name="only_stored_on_members_devices">(alleen opgeslagen door groepsleden)</string> <string name="smp_servers_preset_server">Vooraf ingestelde server</string> @@ -587,23 +575,22 @@ <string name="ok">OK</string> <string name="network_use_onion_hosts_required_desc">Onion hosts zijn vereist voor verbinding.</string> <string name="network_use_onion_hosts_prefer_desc">Onion hosts worden gebruikt indien beschikbaar.</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hosts worden gebruikt indien beschikbaar.</string> <string name="network_use_onion_hosts_no_desc">Onion hosts worden niet gebruikt.</string> - <string name="opensource_protocol_and_code_anybody_can_run_servers">Open-source protocol en code. Iedereen kan de servers draaien.</string> - <string name="people_can_connect_only_via_links_you_share">Mensen kunnen alleen verbinding met u maken via de links die u deelt.</string> - <string name="only_your_contact_can_delete">Alleen uw contact kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering).</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Iedereen kan servers hosten.</string> + <string name="people_can_connect_only_via_links_you_share">Jij bepaalt wie er verbinding mag maken.</string> + <string name="only_your_contact_can_delete">Alleen uw contact kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering). (24 uur)</string> <string name="only_you_can_send_voice">Alleen jij kunt spraak berichten verzenden.</string> <string name="only_your_contact_can_send_voice">Alleen uw contact kan spraak berichten verzenden.</string> - <string name="prohibit_message_deletion">Verbied het onomkeerbaar verwijderen van berichten.</string> + <string name="prohibit_message_deletion">Verbied het definitief verwijderen van berichten.</string> <string name="feature_offered_item">voorgesteld %s</string> - <string name="store_passphrase_securely_without_recover">Sla het wachtwoord veilig op. Als u deze kwijtraakt, heeft u GEEN toegang tot de gesprekken.</string> + <string name="store_passphrase_securely_without_recover">Sla het wachtwoord veilig op. Als u deze kwijtraakt, heeft u GEEN toegang tot de chats.</string> <string name="store_passphrase_securely">Bewaar het wachtwoord veilig, u kunt deze NIET wijzigen als u deze kwijtraakt.</string> - <string name="open_chat">Gesprekken openen</string> + <string name="open_chat">Chat openen</string> <string name="restore_database_alert_desc">Voer het vorige wachtwoord in na het herstellen van de database back-up. Deze actie kan niet ongedaan gemaakt worden.</string> <string name="icon_descr_call_pending_sent">Oproep in behandeling</string> <string name="simplex_link_mode_browser_warning">Het openen van de link in de browser kan de privacy en beveiliging van de verbinding verminderen. Niet vertrouwde SimpleX links worden rood weergegeven.</string> <string name="contact_developers">Werk de app bij en neem contact op met de ontwikkelaars.</string> - <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Alleen client apparaten slaan gebruikers profielen, contacten, groepen en berichten op die zijn verzonden met <b>2-laags end-to-end codering</b>.]]></string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Alleen clientapparaten slaan gebruikersprofielen, contacten, groepen en berichten op.</string> <string name="sender_may_have_deleted_the_connection_request">De afzender heeft mogelijk het verbindingsverzoek verwijderd.</string> <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Schakel SimpleX Vergrendelen in om uw informatie te beschermen. \nU wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingeschakeld.</string> @@ -623,15 +610,13 @@ <string name="you_control_your_chat">Jij beheert je gesprek!</string> <string name="your_profile_is_stored_on_your_device">Uw profiel, contacten en afgeleverde berichten worden op uw apparaat opgeslagen.</string> <string name="callstate_starting">beginnen…</string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[U bepaalt via welke server(s) je de berichten <b>ontvangt</b>, uw contacten de servers die u gebruikt om ze berichten te sturen.]]></string> <string name="icon_descr_video_on">Video aan</string> - <string name="delete_chat_profile_action_cannot_be_undone_warning">Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren.</string> - <string name="messages_section_description">Deze instelling is van toepassing op berichten in uw huidige chat profiel</string> - <string name="save_archive">Bewaar archief</string> + <string name="delete_chat_profile_action_cannot_be_undone_warning">Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan definitief verloren.</string> + <string name="messages_section_description">Deze instelling is van toepassing op berichten in uw huidige chatprofiel</string> <string name="rcv_group_event_updated_group_profile">bijgewerkt groep profiel</string> <string name="group_member_status_removed">verwijderd</string> - <string name="group_main_profile_sent">Uw chat profiel wordt verzonden naar de groepsleden</string> - <string name="failed_to_create_user_duplicate_desc">Je hebt al een chat profiel met dezelfde weergave naam. Kies een andere naam.</string> + <string name="group_main_profile_sent">Uw chatprofiel wordt verzonden naar de groepsleden</string> + <string name="failed_to_create_user_duplicate_desc">Je hebt al een chatprofiel met dezelfde weergave naam. Kies een andere naam.</string> <string name="you_are_already_connected_to_vName_via_this_link">U bent al verbonden met %1$s.</string> <string name="error_smp_test_failed_at_step">Test mislukt bij stap %s.</string> <string name="smp_server_test_secure_queue">Veilige wachtrij</string> @@ -652,12 +637,12 @@ <string name="icon_descr_sent_msg_status_sent">verstuurd</string> <string name="icon_descr_sent_msg_status_unauthorized_send">ongeoorloofd verzenden</string> <string name="icon_descr_received_msg_status_unread">Ongelezen</string> - <string name="tap_to_start_new_chat">Tik om een nieuw gesprek te starten</string> + <string name="tap_to_start_new_chat">Tik hier om een nieuw gesprek te starten</string> <string name="this_text_is_available_in_settings">Deze tekst is beschikbaar in instellingen</string> <string name="welcome">Welkom!</string> <string name="group_preview_you_are_invited">je bent uitgenodigd voor de groep</string> - <string name="you_have_no_chats">Je hebt geen gesprekken</string> - <string name="your_chats">Gesprekken</string> + <string name="you_have_no_chats">Je hebt geen chats</string> + <string name="your_chats">Chats</string> <string name="share_file">Deel bestand…</string> <string name="share_image">Afbeelding delen…</string> <string name="icon_descr_waiting_for_image">Wachten op afbeelding</string> @@ -680,13 +665,13 @@ <string name="you_invited_a_contact">Je hebt je contact uitgenodigd</string> <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Uw contact moet online zijn om de verbinding te voltooien. \nU kunt deze verbinding verbreken en het contact verwijderen en later proberen met een nieuwe link.</string> - <string name="image_descr_qr_code">QR code</string> + <string name="image_descr_qr_code">QR-code</string> <string name="icon_descr_settings">Instellingen</string> <string name="contact_wants_to_connect_with_you">wil met je in contact komen!</string> <string name="icon_descr_address">SimpleX Adres</string> <string name="show_QR_code">Toon QR-code</string> <string name="image_descr_simplex_logo">SimpleX-Logo</string> - <string name="your_chat_profile_will_be_sent_to_your_contact">Je chat profiel wordt verzonden naar uw contact</string> + <string name="your_chat_profile_will_be_sent_to_your_contact">Je chatprofiel wordt verzonden naar uw contact</string> <string name="you_will_be_connected_when_group_host_device_is_online">Je wordt verbonden met de groep wanneer het apparaat van de groep host online is, even geduld a.u.b. of controleer het later!</string> <string name="you_will_be_connected_when_your_connection_request_is_accepted">U wordt verbonden wanneer uw verbindingsverzoek wordt geaccepteerd, even geduld a.u.b. of controleer later!</string> <string name="you_will_be_connected_when_your_contacts_device_is_online">Je wordt verbonden wanneer het apparaat van je contact online is, even geduld a.u.b. of controleer het later!</string> @@ -702,14 +687,14 @@ <string name="send_us_an_email">Stuur ons een e-mail</string> <string name="chat_lock">SimpleX Vergrendelen</string> <string name="smp_servers">SMP servers</string> - <string name="smp_servers_save">Bewaar servers</string> + <string name="smp_servers_save">Servers opslaan</string> <string name="smp_servers_test_failed">Servertest mislukt!</string> <string name="smp_servers_test_some_failed">Sommige servers hebben de test niet doorstaan:</string> <string name="smp_servers_test_servers">Servers testen</string> <string name="smp_servers_test_server">Server test</string> <string name="rate_the_app">Beoordeel de app</string> <string name="smp_servers_use_server">Gebruik server</string> - <string name="using_simplex_chat_servers">Gebruik van SimpleX Chat servers.</string> + <string name="using_simplex_chat_servers">Gebruik SimpleX Chat servers.</string> <string name="smp_servers_your_server_address">Uw server adres</string> <string name="smp_servers_your_server">Uw server</string> <string name="network_session_mode_transport_isolation">Transport isolation</string> @@ -725,9 +710,9 @@ <string name="you_can_use_markdown_to_format_messages__prompt">U kunt markdown gebruiken voor opmaak in berichten:</string> <string name="callstatus_rejected">geweigerde oproep</string> <string name="secret_text">geheim</string> - <string name="next_generation_of_private_messaging">De volgende generatie privéberichten</string> + <string name="next_generation_of_private_messaging">De toekomst van berichtenuitwisseling</string> <string name="callstate_waiting_for_answer">wachten op antwoord…</string> - <string name="to_protect_privacy_simplex_has_ids_for_queues">Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers-ID\'s die door alle andere platforms worden gebruikt, ID\'s voor berichten wachtrijen, afzonderlijk voor elk van uw contacten.</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">Om uw privacy te beschermen, gebruikt SimpleX voor elk van uw contacten afzonderlijke ID\'s.</string> <string name="use_chat">Gebruik chat</string> <string name="onboarding_notifications_mode_off">Wanneer de app actief is</string> <string name="video_call_no_encryption">video gesprek (niet e2e versleuteld)</string> @@ -741,7 +726,7 @@ <string name="relay_server_if_necessary">Relay server wordt alleen gebruikt als dat nodig is. Een andere partij kan uw IP-adres zien.</string> <string name="your_ice_servers">Uw ICE servers</string> <string name="alert_title_skipped_messages">Overgeslagen berichten</string> - <string name="call_connection_via_relay">via relais</string> + <string name="call_connection_via_relay">via relay</string> <string name="icon_descr_video_off">Video uit</string> <string name="protect_app_screen">App scherm verbergen</string> <string name="your_privacy">Uw privacy</string> @@ -752,10 +737,9 @@ <string name="run_chat_section">CHAT UITVOEREN</string> <string name="your_chat_database">Uw chat database</string> <string name="set_password_to_export">Wachtwoord instellen om te exporteren</string> - <string name="restart_the_app_to_create_a_new_chat_profile">Start de app opnieuw om een nieuw chat profiel aan te maken.</string> + <string name="restart_the_app_to_create_a_new_chat_profile">Start de app opnieuw om een nieuw chatprofiel aan te maken.</string> <string name="you_must_use_the_most_recent_version_of_database">U mag ALLEEN de meest recente versie van uw chat-database op één apparaat gebruiken, anders ontvangt u mogelijk geen berichten meer van sommige contacten.</string> <string name="restart_the_app_to_use_imported_chat_database">Start de app opnieuw om de geïmporteerde chat database te gebruiken.</string> - <string name="stop_chat_to_enable_database_actions">Stop de chat om database acties mogelijk te maken.</string> <string name="delete_files_and_media_desc">Deze actie kan niet ongedaan worden gemaakt, alle ontvangen en verzonden bestanden en media worden verwijderd. Foto\'s met een lage resolutie blijven behouden.</string> <string name="remove_passphrase_from_keychain">Wachtwoord verwijderen uit Keychain\?</string> <string name="remove_passphrase">Verwijderen</string> @@ -772,13 +756,13 @@ <string name="group_info_section_title_num_members">%1$s LEDEN</string> <string name="you_can_share_group_link_anybody_will_be_able_to_connect">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.</string> <string name="switch_verb">Wijzig</string> - <string name="member_role_will_be_changed_with_notification">De rol wordt gewijzigd in \"%s\". Iedereen in de groep wordt op de hoogte gebracht.</string> + <string name="member_role_will_be_changed_with_notification">De rol wordt gewijzigd in "%s". Iedereen in de groep wordt op de hoogte gebracht.</string> <string name="receiving_via">Ontvang via</string> <string name="save_group_profile">Groep profiel opslaan</string> <string name="group_is_decentralized">Volledig gedecentraliseerd – alleen zichtbaar voor leden.</string> <string name="network_option_tcp_connection_timeout">Timeout van TCP-verbinding</string> <string name="voice_messages">Spraak berichten</string> - <string name="voice_prohibited_in_this_chat">Spraak berichten zijn verboden in dit gesprek.</string> + <string name="voice_prohibited_in_this_chat">Spraak berichten zijn niet toegestaan in dit gesprek.</string> <string name="prohibit_sending_disappearing">Verbied het verzenden van verdwijnende berichten.</string> <string name="v4_5_reduced_battery_usage">Verminderd batterijgebruik</string> <string name="v4_5_private_filenames_descr">Om de tijdzone te beschermen, gebruiken afbeeldings-/spraakbestanden UTC.</string> @@ -792,11 +776,11 @@ <string name="unknown_error">Onbekende fout</string> <string name="wrong_passphrase_title">Verkeerd wachtwoord!</string> <string name="you_are_invited_to_group_join_to_connect_with_group_members">U bent uitgenodigd voor de groep. Word lid om in contact te komen met de groepsleden.</string> - <string name="group_invitation_tap_to_join">Tik om lid te worden</string> - <string name="group_invitation_tap_to_join_incognito">Tik om incognito lid te worden</string> + <string name="group_invitation_tap_to_join">Tik hier om lid te worden</string> + <string name="group_invitation_tap_to_join_incognito">Tik hier om incognito lid te worden</string> <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Je bent lid geworden van deze groep. Verbinding maken met uitnodigend lid.</string> <string name="you_sent_group_invitation">Je hebt een groep uitnodiging verzonden</string> - <string name="snd_group_event_user_left">jij bent vertrokken</string> + <string name="snd_group_event_user_left">je bent vertrokken</string> <string name="snd_conn_event_switch_queue_phase_completed">je bent van adres veranderd</string> <string name="select_contacts">Selecteer contacten</string> <string name="skip_inviting_button">Sla het uitnodigen van leden over</string> @@ -805,22 +789,20 @@ <string name="button_remove_member">Lid verwijderen</string> <string name="role_in_group">Rol</string> <string name="button_send_direct_message">Direct bericht sturen</string> - <string name="member_role_will_be_changed_with_invitation">De rol wordt gewijzigd in \"%s\". De gebruiker ontvangt een nieuwe uitnodiging.</string> + <string name="member_role_will_be_changed_with_invitation">De rol wordt gewijzigd in "%s". De gebruiker ontvangt een nieuwe uitnodiging.</string> <string name="sending_via">Verzenden via</string> <string name="conn_stats_section_title_servers">SERVERS</string> <string name="network_options_reset_to_defaults">Resetten naar standaardwaarden</string> <string name="switch_receiving_address">Ontvangst adres wijzigen</string> <string name="network_option_protocol_timeout">Protocol timeout</string> - <string name="network_options_revert">Terugdraaien</string> <string name="network_options_save">Opslaan</string> <string name="network_option_seconds_label">sec</string> <string name="incognito_info_share">Wanneer je een incognito profiel met iemand deelt, wordt dit profiel gebruikt voor de groepen waarvoor ze je uitnodigen.</string> <string name="theme">Thema</string> <string name="reset_color">Kleuren resetten</string> - <string name="save_color">Kleur opslaan</string> <string name="theme_system">Systeem</string> <string name="chat_preferences_yes">ja</string> - <string name="feature_received_prohibited">gekregen, verboden</string> + <string name="feature_received_prohibited">gekregen, niet toegestaan</string> <string name="your_preferences">Jouw voorkeuren</string> <string name="accept_feature_set_1_day">Stel 1 dag in</string> <string name="whats_new">Wat is er nieuw</string> @@ -840,7 +822,7 @@ <string name="voice_message_send_text">Spraakbericht…</string> <string name="voice_message_with_duration">Spraakbericht (%1$s)</string> <string name="text_field_set_contact_placeholder">Contactnaam instellen…</string> - <string name="voice_messages_prohibited">Spraak berichten verboden!</string> + <string name="voice_messages_prohibited">Spraak berichten niet toegestaan!</string> <string name="reset_verb">Resetten</string> <string name="send_verb">Verstuur</string> <string name="send_live_message_desc">Stuur een live bericht, het wordt bijgewerkt voor de ontvanger(s) terwijl u het typt</string> @@ -849,7 +831,7 @@ <string name="use_camera_button">Camera</string> <string name="smp_servers_use_server_for_new_conn">Gebruik voor nieuwe verbindingen</string> <string name="star_on_github">Star on GitHub</string> - <string name="smp_servers_per_user">De servers voor nieuwe verbindingen van je huidige chat profiel</string> + <string name="smp_servers_per_user">De servers voor nieuwe verbindingen van je huidige chatprofiel</string> <string name="your_SMP_servers">Uw SMP servers</string> <string name="saved_ICE_servers_will_be_removed">Opgeslagen WebRTC ICE servers worden verwijderd.</string> <string name="your_ICE_servers">Uw ICE servers</string> @@ -863,16 +845,16 @@ <string name="update_network_session_mode_question">Transportisolatiemodus updaten\?</string> <string name="callstate_received_confirmation">bevestiging ontvangen…</string> <string name="callstate_waiting_for_confirmation">Wachten op bevestiging…</string> - <string name="first_platform_without_user_ids">Het eerste platform zonder gebruikers-ID\'s, privé door ontwerp.</string> + <string name="first_platform_without_user_ids">Geen gebruikers-ID\'s.</string> <string name="prohibit_direct_messages">Verbied het sturen van directe berichten naar leden.</string> <string name="prohibit_sending_voice">Verbieden het verzenden van spraak berichten.</string> <string name="v4_2_security_assessment_desc">De beveiliging van SimpleX Chat is gecontroleerd door Trail of Bits.</string> - <string name="v4_2_auto_accept_contact_requests_desc">Met optioneel welkomst bericht.</string> + <string name="v4_2_auto_accept_contact_requests_desc">Met optioneel welkom bericht.</string> <string name="v4_3_voice_messages">Spraak berichten</string> <string name="v4_3_irreversible_message_deletion_desc">Uw contacten kunnen volledige verwijdering van berichten toestaan.</string> <string name="you_have_to_enter_passphrase_every_time">U moet elke keer dat de app start het wachtwoord invoeren, deze wordt niet op het apparaat opgeslagen.</string> <string name="wrong_passphrase">Verkeerd wachtwoord voor de database</string> - <string name="save_passphrase_and_open_chat">Bewaar het wachtwoord en open je gesprekken</string> + <string name="save_passphrase_and_open_chat">Wachtwoord opslaan en open je chats</string> <string name="database_backup_can_be_restored">De poging om het wachtwoord van de database te wijzigen is niet voltooid.</string> <string name="restore_database">Database back-up terugzetten</string> <string name="restore_database_alert_title">Database back-up terugzetten\?</string> @@ -902,22 +884,20 @@ <string name="this_string_is_not_a_connection_link">Deze string is geen verbinding link!</string> <string name="enable_automatic_deletion_message">Deze actie kan niet ongedaan worden gemaakt, de berichten die eerder zijn verzonden en ontvangen dan geselecteerd, worden verwijderd. Het kan enkele minuten duren.</string> <string name="switch_receiving_address_desc">Het ontvangstadres wordt gewijzigd naar een andere server. Adres wijziging wordt voltooid nadat de afzender online is.</string> - <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Om je privacy te behouden, heeft de app in plaats van push meldingen een <b>SimpleX achtergrond service</b> - deze gebruikt een paar procent van de batterij per dag.]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Om de privacy te verbeteren, <b>draait SimpleX op de achtergrond</b> in plaats van pushmeldingen te gebruiken.]]></string> <string name="chat_preferences_you_allow">Jij staat toe</string> <string name="you_are_invited_to_group">Je bent uitgenodigd voor de groep</string> <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[U kunt <font color="#0088ff">verbinding maken met SimpleX Chat ontwikkelaars om vragen te stellen en updates te ontvangen</font>.]]></string> <string name="connection_error_auth_desc">Tenzij uw contact de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft. \nOm verbinding te maken, vraagt u uw contact om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft.</string> - <string name="update_onion_hosts_settings_question">.onion hosts-instelling updaten\?</string> <string name="use_simplex_chat_servers__question">SimpleX Chat servers gebruiken\?</string> - <string name="voice_messages_are_prohibited">Spraak berichten zijn verboden in deze groep.</string> + <string name="voice_messages_are_prohibited">Spraak berichten zijn niet toegestaan.</string> <string name="personal_welcome">Welkom %1$s!</string> <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">U kunt de chat starten via app Instellingen / Database of door de app opnieuw op te starten.</string> <string name="snd_conn_event_switch_queue_phase_completed_for_member">je hebt het adres gewijzigd voor %s</string> <string name="snd_group_event_member_deleted">je hebt %1$s verwijderd</string> <string name="contact_sent_large_file">Je contact heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%1$s).</string> - <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Uw huidige chatdatabase wordt VERWIJDERD en VERVANGEN door de geïmporteerde. -\nDeze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren.</string> + <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Uw huidige chatdatabase wordt VERWIJDERD en VERVANGEN door de geïmporteerde. \nDeze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan definitief verloren.</string> <string name="invite_prohibited_description">Je probeert een contact met wie je een incognito profiel hebt gedeeld uit te nodigen voor de groep waarin je je hoofdprofiel gebruikt</string> <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten. SimpleX servers kunnen uw profiel niet zien.</string> <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">U moet zich authenticeren wanneer u de app na 30 seconden op de achtergrond start of hervat.</string> @@ -936,7 +916,7 @@ <string name="trying_to_connect_to_server_to_receive_messages_with_error">Er wordt geprobeerd verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen (fout: %1$s).</string> <string name="unknown_message_format">onbekend berichtformaat</string> <string name="simplex_link_mode_browser">Via browser</string> - <string name="description_via_contact_address_link">via contactadres link</string> + <string name="description_via_contact_address_link">via contact adres link</string> <string name="description_via_group_link">via groep link</string> <string name="description_via_one_time_link">via een eenmalige link</string> <string name="simplex_link_connection">via %1$s</string> @@ -945,7 +925,6 @@ <string name="description_you_shared_one_time_link_incognito">je hebt een eenmalige link incognito gedeeld</string> <string name="chat_help_tap_button">Tik op de knop</string> <string name="read_more_in_github_with_link"><![CDATA[Lees meer in onze <font color="#0088ff">GitHub repository</font>.]]></string> - <string name="read_more_in_github">Lees meer in onze GitHub repository.</string> <string name="integrity_msg_skipped">%1$d bericht(en) overgeslagen</string> <string name="moderated_description">gemodereerd</string> <string name="moderated_item_description">gemodereerd door %s</string> @@ -958,21 +937,20 @@ <string name="initial_member_role">Initiële rol</string> <string name="group_member_role_observer">Waarnemer</string> <string name="observer_cant_send_message_title">Je kunt geen berichten versturen!</string> - <string name="you_are_observer">jij bent waarnemer</string> + <string name="you_are_observer">je bent waarnemer</string> <string name="language_system">Systeem</string> <string name="v4_6_audio_video_calls">Audio en video oproepen</string> - <string name="cant_delete_user_profile">Kan gebruikers profiel niet verwijderen!</string> <string name="confirm_password">Bevestig wachtwoord</string> <string name="v4_6_chinese_spanish_interface">Chinese en Spaanse interface</string> <string name="enter_password_to_show">Voer wachtwoord in bij zoeken</string> <string name="error_saving_user_password">Fout bij opslaan gebruikers wachtwoord</string> - <string name="button_add_welcome_message">Welkomst bericht toevoegen</string> + <string name="button_add_welcome_message">Welkom bericht toevoegen</string> <string name="dont_show_again">Niet meer weergeven</string> <string name="v4_6_group_moderation">Groep moderatie</string> <string name="error_updating_user_privacy">Fout bij updaten van gebruikers privacy</string> <string name="v4_6_reduced_battery_usage">Verder verminderd batterij verbruik</string> - <string name="v4_6_group_welcome_message">Groep welkomst bericht</string> - <string name="v4_6_hidden_chat_profiles">Verborgen chat profielen</string> + <string name="v4_6_group_welcome_message">Groep welkom bericht</string> + <string name="v4_6_hidden_chat_profiles">Verborgen chatprofielen</string> <string name="hide_profile">Profiel verbergen</string> <string name="user_hide">Verbergen</string> <string name="hidden_profile_password">Verborgen profiel wachtwoord</string> @@ -982,25 +960,23 @@ <string name="make_profile_private">Profiel privé maken!</string> <string name="v4_6_group_moderation_descr">Nu kunnen beheerders: \n- berichten van leden verwijderen. -\n- schakel leden uit (\"waarnemer\" rol)</string> - <string name="v4_6_hidden_chat_profiles_descr">Bescherm je chat profielen met een wachtwoord!</string> +\n- schakel leden uit ("waarnemer" rol)</string> + <string name="v4_6_hidden_chat_profiles_descr">Bescherm je chatprofielen met een wachtwoord!</string> <string name="password_to_show">Wachtwoord om weer te geven</string> <string name="save_and_update_group_profile">Groep profiel opslaan en bijwerken</string> <string name="smp_save_servers_question">Servers opslaan\?</string> <string name="save_profile_password">Bewaar profiel wachtwoord</string> <string name="v4_6_group_welcome_message_descr">Stel het getoonde bericht in voor nieuwe leden!</string> - <string name="save_welcome_message_question">Welkomst bericht opslaan\?</string> + <string name="save_welcome_message_question">Welkom bericht opslaan?</string> <string name="v4_6_audio_video_calls_descr">Ondersteuning voor bluetooth en andere verbeteringen.</string> - <string name="tap_to_activate_profile">Tik om profiel te activeren.</string> + <string name="tap_to_activate_profile">Tik hier om profiel te activeren.</string> <string name="v4_6_chinese_spanish_interface_descr">Dank aan de gebruikers – draag bij via Weblate!</string> - <string name="should_be_at_least_one_profile">Er moet ten minste één gebruikers profiel zijn.</string> <string name="you_can_hide_or_mute_user_profile">U kunt een gebruikers profiel verbergen of dempen - houd het vast voor het menu.</string> <string name="user_unhide">zichtbaar maken</string> <string name="user_unmute">Dempen opheffen</string> - <string name="group_welcome_title">Welkomst bericht</string> - <string name="should_be_at_least_one_visible_profile">"Er moet ten minste één zichtbaar gebruikers profiel zijn."</string> - <string name="to_reveal_profile_enter_password">Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoekveld in op de pagina Uw chat profielen.</string> - <string name="button_welcome_message">Welkomst bericht</string> + <string name="group_welcome_title">Welkom bericht</string> + <string name="to_reveal_profile_enter_password">Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoekveld in op de pagina Uw chatprofielen.</string> + <string name="button_welcome_message">Welkom bericht</string> <string name="you_will_still_receive_calls_and_ntfs">U ontvangt nog steeds oproepen en meldingen van gedempte profielen wanneer deze actief zijn.</string> <string name="database_downgrade">Database downgraden</string> <string name="invalid_migration_confirmation">Ongeldige migratie bevestiging</string> @@ -1022,9 +998,9 @@ <string name="settings_section_title_experimenta">EXPERIMENTEEL</string> <string name="delete_profile">Verwijder profiel</string> <string name="profile_password">Profiel wachtwoord</string> - <string name="unhide_chat_profile">Chat profiel zichtbaar maken</string> + <string name="unhide_chat_profile">Chatprofiel zichtbaar maken</string> <string name="unhide_profile">Profiel zichtbaar maken</string> - <string name="delete_chat_profile">Chat profiel verwijderen\?</string> + <string name="delete_chat_profile">Chatprofiel verwijderen?</string> <string name="icon_descr_video_asked_to_receive">Gevraagd om de video te ontvangen</string> <string name="videos_limit_desc">Er kunnen slechts 10 video\'s tegelijk worden verzonden</string> <string name="videos_limit_title">Te veel video\'s!</string> @@ -1071,7 +1047,7 @@ <string name="you_can_turn_on_lock">Je kunt SimpleX Vergrendeling aanzetten via Instellingen.</string> <string name="enable_lock">Vergrendeling inschakelen</string> <string name="lock_mode">Vergrendeling modus</string> - <string name="submit_passcode">Indienen</string> + <string name="submit_passcode">Bevestigen</string> <string name="authentication_cancelled">Verificatie geannuleerd</string> <string name="change_lock_mode">Wijzig de vergrendelings modus</string> <string name="confirm_passcode">Bevestig toegangscode</string> @@ -1083,7 +1059,7 @@ <string name="passcode_set">Toegangscode ingesteld!</string> <string name="la_mode_system">Systeem</string> <string name="decryption_error">Decodering fout</string> - <string name="alert_text_msg_bad_hash">De hash van het vorige bericht is anders.</string> + <string name="alert_text_msg_bad_hash">De hash van het vorige bericht is anders.\"</string> <string name="alert_text_decryption_error_too_many_skipped">%1$d berichten overgeslagen.</string> <string name="alert_text_fragment_encryption_out_of_sync_old_database">Het kan gebeuren wanneer u of de ander een oude database back-up gebruikt.</string> <string name="alert_text_fragment_please_report_to_developers">Meld het alsjeblieft aan de ontwikkelaars.</string> @@ -1111,14 +1087,14 @@ <string name="available_in_v51">" \nBeschikbaar in v5.1"</string> <string name="prohibit_calls">Audio/video gesprekken verbieden.</string> - <string name="calls_prohibited_with_this_contact">Audio/video gesprekken zijn verboden.</string> + <string name="calls_prohibited_with_this_contact">Audio/video gesprekken zijn niet toegestaan.</string> <string name="v5_0_large_files_support_descr">Snel en niet wachten tot de afzender online is!</string> <string name="v5_0_app_passcode">App toegangscode</string> <string name="v5_0_app_passcode_descr">Stel het in in plaats van systeemverificatie.</string> <string name="v5_0_polish_interface">Poolse interface</string> <string name="v5_0_polish_interface_descr">Dank aan de gebruikers – draag bij via Weblate!</string> <string name="v5_0_large_files_support">Video\'s en bestanden tot 1 GB</string> - <string name="auth_open_chat_profiles">Chat profielen openen</string> + <string name="auth_open_chat_profiles">Chatprofielen wijzigen</string> <string name="learn_more_about_address">Over SimpleX adres</string> <string name="learn_more">Kom meer te weten</string> <string name="scan_qr_to_connect_to_contact">Om verbinding te maken, kan uw contact de QR-code scannen of de link in de app gebruiken.</string> @@ -1142,13 +1118,13 @@ <string name="email_invite_subject">Laten we praten in SimpleX Chat</string> <string name="save_auto_accept_settings">Sla instellingen voor automatisch accepteren op</string> <string name="save_settings_question">Instellingen opslaan\?</string> - <string name="enter_welcome_message_optional">Voer welkomst bericht in... (optioneel)</string> + <string name="enter_welcome_message_optional">Voer welkom bericht in... (optioneel)</string> <string name="dont_create_address">Maak geen adres aan</string> <string name="email_invite_body">Hoi! \nMaak verbinding met mij via SimpleX Chat: %s</string> <string name="you_can_create_it_later">U kan het later maken</string> <string name="share_address">Adres delen</string> - <string name="enter_welcome_message">Welkomst bericht invoeren…</string> + <string name="enter_welcome_message">Welkom bericht invoeren…</string> <string name="import_theme">Thema importeren</string> <string name="theme_simplex">SimpleX</string> <string name="color_primary_variant">Extra accent</string> @@ -1171,7 +1147,7 @@ <string name="import_theme_error_desc">Zorg ervoor dat het bestand de juiste YAML-syntaxis heeft. Exporteer het thema om een voorbeeld te hebben van de themabestandsstructuur.</string> <string name="opening_database">Database openen…</string> <string name="read_more_in_user_guide_with_link"><![CDATA[Lees meer in de <font color="#0088ff">Gebruikershandleiding</font>.]]></string> - <string name="theme_colors_section_title">THEMA KLEUREN</string> + <string name="theme_colors_section_title">INTERFACE KLEUREN</string> <string name="you_can_share_your_address">U kunt uw adres delen als een link of QR-code - iedereen kan verbinding met u maken.</string> <string name="all_app_data_will_be_cleared">Alle app-gegevens worden verwijderd.</string> <string name="empty_chat_profile_is_created">Er wordt een leeg chatprofiel met de opgegeven naam gemaakt en de app wordt zoals gewoonlijk geopend.</string> @@ -1186,20 +1162,20 @@ <string name="self_destruct_passcode_enabled">Zelfvernietigings wachtwoord ingeschakeld!</string> <string name="app_passcode_replaced_with_self_destruct">De app-toegangscode wordt vervangen door een zelfvernietigings wachtwoord.</string> <string name="if_you_enter_self_destruct_code">Als u uw zelfvernietigings wachtwoord invoert tijdens het openen van de app:</string> - <string name="if_you_enter_passcode_data_removed">Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens onomkeerbaar verwijderd!</string> + <string name="if_you_enter_passcode_data_removed">Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens definitief verwijderd!</string> <string name="set_passcode">Toegangscode instellen</string> - <string name="prohibit_message_reactions">Berichtreacties verbieden.</string> - <string name="only_you_can_add_message_reactions">Alleen jij kunt berichtreacties toevoegen.</string> - <string name="message_reactions_are_prohibited">Reacties op berichten zijn verboden in deze groep.</string> + <string name="prohibit_message_reactions">Bericht reacties verbieden.</string> + <string name="only_you_can_add_message_reactions">Alleen jij kunt bericht reacties toevoegen.</string> + <string name="message_reactions_are_prohibited">Reacties op berichten zijn niet toegestaan.</string> <string name="prohibit_message_reactions_group">Berichten reacties verbieden.</string> - <string name="allow_message_reactions_only_if">Sta berichtreacties alleen toe als uw contact dit toestaat.</string> - <string name="allow_your_contacts_adding_message_reactions">Sta uw contactpersonen toe om berichtreacties toe te voegen.</string> - <string name="allow_message_reactions">Sta berichtreacties toe.</string> - <string name="group_members_can_add_message_reactions">Groepsleden kunnen berichtreacties toevoegen.</string> - <string name="both_you_and_your_contact_can_add_message_reactions">Zowel u als uw contact kunnen berichtreacties toevoegen.</string> + <string name="allow_message_reactions_only_if">Sta bericht reacties alleen toe als uw contact dit toestaat.</string> + <string name="allow_your_contacts_adding_message_reactions">Sta uw contactpersonen toe om bericht reacties toe te voegen.</string> + <string name="allow_message_reactions">Sta bericht reacties toe.</string> + <string name="group_members_can_add_message_reactions">Leden kunnen reacties op berichten toevoegen.</string> + <string name="both_you_and_your_contact_can_add_message_reactions">Zowel u als uw contact kunnen bericht reacties toevoegen.</string> <string name="message_reactions">Reacties op berichten</string> - <string name="message_reactions_prohibited_in_this_chat">Reacties op berichten zijn verboden in deze chat.</string> - <string name="only_your_contact_can_add_message_reactions">Alleen uw contact kan berichtreacties toevoegen.</string> + <string name="message_reactions_prohibited_in_this_chat">Reacties op berichten zijn niet toegestaan in deze chat.</string> + <string name="only_your_contact_can_add_message_reactions">Alleen uw contact kan bericht reacties toevoegen.</string> <string name="custom_time_unit_days">dagen</string> <string name="custom_time_unit_hours">uren</string> <string name="custom_time_unit_minutes">minuten</string> @@ -1249,7 +1225,7 @@ \n- aangepaste tijd om te verdwijnen. \n- bewerkingsgeschiedenis.</string> <string name="item_info_no_text">geen tekst</string> - <string name="non_fatal_errors_occured_during_import">Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren - mogelijk ziet u Chatconsole voor meer informatie.</string> + <string name="non_fatal_errors_occured_during_import">Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren:</string> <string name="shutdown_alert_question">Afsluiten\?</string> <string name="settings_section_title_app">APP</string> <string name="settings_restart_app">Herstarten</string> @@ -1260,16 +1236,16 @@ <string name="abort_switch_receiving_address_desc">Adres wijziging wordt afgebroken. Het oude ontvangstadres wordt gebruikt.</string> <string name="abort_switch_receiving_address">Annuleer het wijzigen van het adres</string> <string name="abort_switch_receiving_address_confirm">Afbreken</string> - <string name="no_filtered_chats">Geen gefilterde gesprekken</string> + <string name="no_filtered_chats">Geen gefilterde chats</string> <string name="only_owners_can_enable_files_and_media">Alleen groep eigenaren kunnen bestanden en media inschakelen.</string> - <string name="files_are_prohibited_in_group">Bestanden en media zijn verboden in deze groep.</string> + <string name="files_are_prohibited_in_group">Bestanden en media zijn niet toegestaan.</string> <string name="favorite_chat">Favoriet</string> - <string name="files_and_media_prohibited">Bestanden en media verboden!</string> + <string name="files_and_media_prohibited">Bestanden en media niet toegestaan!</string> <string name="unfavorite_chat">Niet favoriet</string> <string name="files_and_media">Bestanden en media</string> <string name="prohibit_sending_files">Verbied het verzenden van bestanden en media.</string> <string name="allow_to_send_files">Sta toe om bestanden en media te verzenden.</string> - <string name="group_members_can_send_files">Groepsleden kunnen bestanden en media verzenden.</string> + <string name="group_members_can_send_files">Leden kunnen bestanden en media verzenden.</string> <string name="search_verb">Zoeken</string> <string name="la_mode_off">Uit</string> <string name="network_option_protocol_timeout_per_kb">Protocol timeout per KB</string> @@ -1314,7 +1290,7 @@ <string name="send_receipts">Ontvangst bevestiging verzenden</string> <string name="v5_2_message_delivery_receipts_descr">De tweede vink die we gemist hebben! ✅</string> <string name="v5_2_favourites_filter_descr">Filter ongelezen en favoriete chats.</string> - <string name="v5_2_favourites_filter">Vind gesprekken sneller</string> + <string name="v5_2_favourites_filter">Vind chats sneller</string> <string name="v5_2_fix_encryption_descr">Repareer versleuteling na het herstellen van back-ups.</string> <string name="v5_2_fix_encryption">Behoud uw verbindingen</string> <string name="v5_2_disappear_one_message">Eén bericht laten verdwijnen</string> @@ -1383,7 +1359,7 @@ <string name="open_database_folder">Database map openen</string> <string name="passphrase_will_be_saved_in_settings">Het wachtwoord wordt als platte tekst in de instellingen opgeslagen nadat u deze hebt gewijzigd of de app opnieuw hebt opgestart.</string> <string name="settings_is_storing_in_clear_text">Het wachtwoord wordt als leesbare tekst in de instellingen opgeslagen.</string> - <string name="socks_proxy_setting_limitations"><![CDATA[<b>Let op</b>: bericht en bestands relais zijn verbonden via SOCKS-proxy. Voor oproepen en het verzenden van link voorbeelden wordt gebruik gemaakt van een directe verbinding.]]></string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Let op</b>: bericht en bestands relays zijn verbonden via SOCKS-proxy. Voor oproepen en het verzenden van link voorbeelden wordt gebruik gemaakt van een directe verbinding.]]></string> <string name="encrypt_local_files">Versleutel lokale bestanden</string> <string name="v5_3_encrypt_local_files">Versleutel opgeslagen bestanden en media</string> <string name="v5_3_new_desktop_app">Nieuwe desktop app!</string> @@ -1402,7 +1378,7 @@ <string name="compose_send_direct_message_to_connect">Stuur een direct bericht om verbinding te maken</string> <string name="member_contact_send_direct_message">stuur een direct bericht</string> <string name="rcv_group_event_member_created_contact">direct verbonden</string> - <string name="expand_verb">Uitbreiden</string> + <string name="expand_verb">Uitklappen</string> <string name="connect_plan_repeat_connection_request">Verbindingsverzoek herhalen?</string> <string name="rcv_direct_event_contact_deleted">verwijderd contact</string> <string name="error_alert_title">Fout</string> @@ -1419,9 +1395,9 @@ <string name="connect_plan_connect_via_link">Verbinden via link?</string> <string name="connect_plan_already_joining_the_group">Al lid van de groep!</string> <string name="group_members_n">%s, %s en %d leden</string> - <string name="moderated_items_description">%d berichten gemodereerd door %s</string> + <string name="moderated_items_description">%1$d berichten gemodereerd door %2$s</string> <string name="connect_plan_connect_to_yourself">Verbinding maken met jezelf?</string> - <string name="contact_tap_to_connect">Tik om verbinding te maken</string> + <string name="contact_tap_to_connect">Tik hier om verbinding te maken</string> <string name="correct_name_to">Juiste naam voor %s?</string> <string name="delete_messages__question">%d berichten verwijderen?</string> <string name="connect_with_contact_name_question">Verbinding maken met %1$s?</string> @@ -1431,7 +1407,7 @@ <string name="block_member_button">Lid blokkeren</string> <string name="connect_plan_repeat_join_request">Deelnameverzoek herhalen?</string> <string name="button_remove_member_question">Lid verwijderen?</string> - <string name="delete_and_notify_contact">Contact verwijderen en op de hoogte stellen</string> + <string name="delete_and_notify_contact">Verwijderen en contact op de hoogte stellen</string> <string name="connect_plan_open_group">Open groep</string> <string name="unblock_member_desc">Berichten van %s worden getoond!</string> <string name="error_sending_message_contact_invitation">Fout bij verzenden van uitnodiging</string> @@ -1442,17 +1418,17 @@ <string name="terminal_always_visible">Console in nieuw venster weergeven</string> <string name="block_member_desc">Alle nieuwe berichten van %s worden verborgen!</string> <string name="blocked_item_description">geblokkeerd</string> - <string name="connect_plan_you_are_already_connecting_to_vName">Je bent al verbonden met %1$s.</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Je bent al verbonden met <b>%1$s</b>.]]></string> <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Je wordt al lid van de groep via deze link.</string> - <string name="connect_plan_you_are_already_joining_the_group_vName">Je bent al lid van de groep %1$s.</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Je bent al lid van de groep <b>%1$s</b>.]]></string> <string name="connect_plan_this_is_your_own_one_time_link">Dit is uw eigen eenmalige link!</string> <string name="unblock_member_button">Lid deblokkeren</string> - <string name="connect_plan_you_are_already_in_group_vName">Je zit al in groep %1$s.</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Je zit al in groep <b>%1$s</b>.]]></string> <string name="connect_plan_this_is_your_own_simplex_address">Dit is uw eigen SimpleX adres!</string> <string name="unblock_member_question">Lid deblokkeren?</string> <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">Je maakt al verbinding via deze eenmalige link!</string> <string name="non_content_uri_alert_text">Je hebt een ongeldig bestandslocatie gedeeld. Rapporteer het probleem aan de app-ontwikkelaars.</string> - <string name="connect_plan_this_is_your_link_for_group_vName">Dit is jouw link voor groep %1$s!</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Dit is jouw link voor groep <b>%1$s</b>!]]></string> <string name="unblock_member_confirmation">Deblokkeren</string> <string name="connect_plan_you_have_already_requested_connection_via_this_address">U heeft al een verbinding aangevraagd via dit adres!</string> <string name="encryption_renegotiation_error">Fout bij heronderhandeling van codering</string> @@ -1472,7 +1448,7 @@ <string name="linked_mobiles">Gekoppelde mobiele apparaten</string> <string name="desktop_device">Desktop</string> <string name="connected_to_desktop">Verbonden met desktop</string> - <string name="this_device_name">Deze apparaatnaam</string> + <string name="this_device_name">Naam van dit apparaat</string> <string name="loading_remote_file_title">Het bestand laden</string> <string name="connecting_to_desktop">Verbinding maken met desktop</string> <string name="desktop_devices">Desktop apparaten</string> @@ -1504,7 +1480,7 @@ <string name="v5_4_block_group_members">Groepsleden blokkeren</string> <string name="v5_4_incognito_groups_descr">Maak een groep met een willekeurig profiel.</string> <string name="v5_4_link_mobile_desktop">Koppel mobiele en desktop-apps! 🔗</string> - <string name="v5_4_link_mobile_desktop_descr">Via een beveiligd kwantumbestendig protocol.</string> + <string name="v5_4_link_mobile_desktop_descr">Via een beveiligd quantum bestendig protocol.</string> <string name="v5_4_block_group_members_descr">Om ongewenste berichten te verbergen.</string> <string name="v5_4_better_groups">Betere groepen</string> <string name="v5_4_incognito_groups">Incognitogroepen</string> @@ -1520,4 +1496,892 @@ <string name="found_desktop">Desktop gevonden</string> <string name="not_compatible">Niet compatibel!</string> <string name="multicast_discoverable_via_local_network">Vindbaar via lokaal netwerk</string> -</resources> \ No newline at end of file + <string name="refresh_qr_code">Vernieuwen</string> + <string name="create_chat_profile">Chatprofiel aanmaken</string> + <string name="disconnect_remote_hosts">Mobiele telefoons loskoppelen</string> + <string name="no_connected_mobile">Geen verbonden mobiel</string> + <string name="random_port">willekeurig</string> + <string name="open_port_in_firewall_desc">Om een mobiele app verbinding te laten maken met de desktop, opent u deze poort in uw firewall, als u deze hebt ingeschakeld</string> + <string name="app_was_crashed">Weergave gecrasht</string> + <string name="open_port_in_firewall_title">Open poort in firewall</string> + <string name="error_showing_content">Fout bij het tonen van inhoud</string> + <string name="error_showing_message">fout bij weergeven bericht</string> + <string name="you_can_make_address_visible_via_settings">Je kunt het via Instellingen zichtbaar maken voor je SimpleX contacten.</string> + <string name="recent_history_is_not_sent_to_new_members">Geschiedenis wordt niet naar nieuwe leden gestuurd.</string> + <string name="retry_verb">Opnieuw proberen</string> + <string name="camera_not_available">Camera niet beschikbaar</string> + <string name="enable_sending_recent_history">Stuur tot 100 laatste berichten naar nieuwe leden.</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Contact toevoegen</b>: om een nieuwe uitnodigings link te maken, of om verbinding te maken via een link die u heeft ontvangen.]]></string> + <string name="disable_sending_recent_history">Stuur geen geschiedenis naar nieuwe leden.</string> + <string name="or_show_this_qr_code">Of laat deze code zien</string> + <string name="recent_history_is_sent_to_new_members">Er worden maximaal 100 laatste berichten naar nieuwe leden verzonden.</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">De code die u heeft gescand is geen SimpleX link QR-code.</string> + <string name="the_text_you_pasted_is_not_a_link">De tekst die u hebt geplakt is geen SimpleX link.</string> + <string name="enable_camera_access">Schakel cameratoegang in</string> + <string name="you_can_view_invitation_link_again">U kunt de uitnodigings link opnieuw bekijken in de verbindings details.</string> + <string name="keep_unused_invitation_question">Ongebruikte uitnodiging bewaren?</string> + <string name="share_this_1_time_link">Deel deze eenmalige uitnodigings link</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Groep aanmaken</b>: om een nieuwe groep aan te maken.]]></string> + <string name="recent_history">Zichtbare geschiedenis</string> + <string name="la_app_passcode">App toegangscode</string> + <string name="new_chat">Nieuw gesprek</string> + <string name="loading_chats">Chats laden…</string> + <string name="creating_link">Link maken…</string> + <string name="or_scan_qr_code">Of scan de QR-code</string> + <string name="invalid_qr_code">Ongeldige QR-code</string> + <string name="add_contact_tab">Contact toevoegen</string> + <string name="tap_to_scan">Tik hier om te scannen</string> + <string name="keep_invitation_link">Bewaar</string> + <string name="tap_to_paste_link">Tik hier om de link te plakken</string> + <string name="search_or_paste_simplex_link">Zoeken of plak een SimpleX link</string> + <string name="chat_is_stopped_you_should_transfer_database">De chat is gestopt. Als u deze database al op een ander apparaat heeft gebruikt, moet u deze terugzetten voordat u met chatten begint.</string> + <string name="start_chat_question">Begin chat?</string> + <string name="show_internal_errors">Toon interne fouten</string> + <string name="agent_critical_error_title">Kritische fout</string> + <string name="agent_internal_error_title">Interne fout</string> + <string name="agent_internal_error_desc">Rapporteer dit alstublieft aan de ontwikkelaars: +\n%s</string> + <string name="agent_critical_error_desc">Rapporteer dit alstublieft aan de ontwikkelaars: +\n%s +\n +\nHet wordt aanbevolen om de app opnieuw te starten.</string> + <string name="remote_ctrl_error_bad_version">Desktop heeft een niet-ondersteunde versie. Zorg ervoor dat u op beide apparaten dezelfde versie gebruikt</string> + <string name="failed_to_create_user_invalid_title">Ongeldige weergavenaam!</string> + <string name="failed_to_create_user_invalid_desc">Deze weergavenaam is ongeldig. Kies een andere naam.</string> + <string name="remote_host_was_disconnected_title">Verbinding gestopt</string> + <string name="remote_ctrl_was_disconnected_title">Verbinding gestopt</string> + <string name="remote_host_disconnected_from"><![CDATA[Verbinding met mobiel <b>%s</b> \u0020is verbroken met als reden: %s]]></string> + <string name="remote_host_error_busy"><![CDATA[Mobiel <b>%s</b> is bezet]]></string> + <string name="remote_host_error_inactive"><![CDATA[Mobiel <b>%s</b> is inactief]]></string> + <string name="remote_host_error_missing"><![CDATA[Mobiel <b>%s</b> ontbreekt]]></string> + <string name="remote_host_error_bad_state"><![CDATA[De verbinding met de mobiel <b>%s</b> is in slechte staat]]></string> + <string name="remote_ctrl_error_disconnected">De verbinding met desktop is verbroken</string> + <string name="possible_slow_function_desc">Uitvoering van functie duurt te lang: %1$d seconden: %2$s</string> + <string name="possible_slow_function_title">Langzame functie</string> + <string name="developer_options_section">Ontwikkelaars opties</string> + <string name="show_slow_api_calls">Toon langzame API aanroepen</string> + <string name="past_member_vName">Voormalig lid %1$s</string> + <string name="group_member_status_unknown_short">onbekend</string> + <string name="remote_ctrl_disconnected_with_reason">Verbinding verbroken met als reden: %s</string> + <string name="remote_ctrl_error_busy">Desktop is bezet</string> + <string name="remote_ctrl_error_inactive">Desktop is inactief</string> + <string name="remote_host_error_disconnected"><![CDATA[Mobiele verbinding <b>%s</b> is verbroken]]></string> + <string name="restart_chat_button">Chat opnieuw starten</string> + <string name="remote_host_error_timeout"><![CDATA[Time-out bereikt tijdens het verbinden met de mobiel <b>%s</b>]]></string> + <string name="remote_ctrl_error_bad_state">De verbinding met de desktop is in slechte staat</string> + <string name="remote_ctrl_error_bad_invitation">Desktop heeft verkeerde uitnodigingscode</string> + <string name="remote_host_error_bad_version"><![CDATA[Mobiel <b>%s</b> heeft een niet-ondersteunde versie. Zorg ervoor dat u op beide apparaten dezelfde versie gebruikt]]></string> + <string name="remote_ctrl_error_timeout">Time-out bereikt tijdens het verbinden met de desktop</string> + <string name="group_member_status_unknown">onbekende status</string> + <string name="v5_5_message_delivery">Verbeterde berichtbezorging</string> + <string name="v5_5_join_group_conversation">Neem deel aan groepsgesprekken</string> + <string name="v5_5_simpler_connect_ui">Plak een link om te verbinden!</string> + <string name="v5_5_private_notes">Privé notities</string> + <string name="clear_note_folder_warning">Alle berichten worden verwijderd. Dit kan niet ongedaan worden gemaakt!</string> + <string name="info_row_created_at">Gemaakt op</string> + <string name="error_creating_message">Fout bij het maken van een bericht</string> + <string name="v5_5_new_interface_languages">Hongaarse en Turkse gebruikersinterface</string> + <string name="v5_5_join_group_conversation_descr">Recente geschiedenis en verbeterde directory-bot.</string> + <string name="note_folder_local_display_name">Privé notities</string> + <string name="share_text_created_at">Gemaakt op: %s</string> + <string name="profile_update_event_member_name_changed">lid %1$s gewijzigd in %2$s</string> + <string name="profile_update_event_contact_name_changed">contactpersoon %1$s gewijzigd in %2$s</string> + <string name="error_deleting_note_folder">Fout bij verwijderen van privénotities</string> + <string name="clear_note_folder_question">Privénotities verwijderen?</string> + <string name="v5_5_simpler_connect_ui_descr">Zoekbalk accepteert uitnodigings links.</string> + <string name="v5_5_private_notes_descr">‐Met versleutelde bestanden en media.</string> + <string name="v5_5_message_delivery_descr">Met verminderd batterijgebruik.</string> + <string name="saved_message_title">Opgeslagen bericht</string> + <string name="profile_update_event_updated_profile">bijgewerkt profiel</string> + <string name="profile_update_event_removed_address">contactadres verwijderd</string> + <string name="profile_update_event_removed_picture">profielfoto verwijderd</string> + <string name="profile_update_event_set_new_address">nieuw contactadres instellen</string> + <string name="profile_update_event_set_new_picture">nieuwe profielfoto</string> + <string name="member_blocked_by_admin">Geblokkeerd door beheerder</string> + <string name="block_for_all">Blokkeren voor iedereen</string> + <string name="block_for_all_question">Lid voor iedereen blokkeren?</string> + <string name="unblock_for_all">Deblokkeer voor iedereen</string> + <string name="member_info_member_blocked">geblokkeerd</string> + <string name="blocked_by_admin_item_description">geblokkeerd door beheerder</string> + <string name="rcv_group_event_member_blocked">blokkeerde %s</string> + <string name="blocked_by_admin_items_description">%d berichten geblokkeerd door beheerder</string> + <string name="rcv_group_event_member_unblocked">%s gedeblokkeerd</string> + <string name="error_blocking_member_for_all">Fout bij blokkeren van lid voor iedereen</string> + <string name="unblock_for_all_question">Lid voor iedereen deblokkeren?</string> + <string name="snd_group_event_member_blocked">je hebt %s geblokkeerd</string> + <string name="snd_group_event_member_unblocked">je hebt %s gedeblokkeerd</string> + <string name="message_too_large">Bericht te groot</string> + <string name="welcome_message_is_too_long">Welkom bericht is te lang</string> + <string name="database_migration_in_progress">De databasemigratie wordt uitgevoerd. +\nDit kan enkele minuten duren.</string> + <string name="call_service_notification_audio_call">Audio oproep</string> + <string name="call_service_notification_end_call">Ophangen</string> + <string name="call_service_notification_video_call">Video oproep</string> + <string name="unable_to_open_browser_title">Fout bij het openen van de browser</string> + <string name="unable_to_open_browser_desc">Voor oproepen is de standaard webbrowser vereist. Configureer de standaard browser in het systeem en deel meer informatie met de ontwikkelaars.</string> + <string name="migrate_to_device_confirm_network_settings">Bevestig netwerk instellingen</string> + <string name="migrate_from_device_archive_and_upload">Archiveren en uploaden</string> + <string name="migrate_from_device_archiving_database">Database archiveren</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Al uw contacten, gesprekken en bestanden worden veilig gecodeerd en in delen geüpload naar geconfigureerde XFTP-relays.</string> + <string name="migrate_to_device_apply_onion">Toepassen</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Let op</b>: als u dezelfde database op twee apparaten gebruikt, wordt de decodering van berichten van uw verbindingen verbroken, als veiligheidsmaatregel.]]></string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Waarschuwing</b>: het archief wordt verwijderd.]]></string> + <string name="migrate_from_device_cancel_migration">Migratie annuleren</string> + <string name="migrate_to_device_chat_migrated">Chat gemigreerd!</string> + <string name="migrate_from_device_check_connection_and_try_again">Controleer uw internetverbinding en probeer het opnieuw</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Kies <i>Migreren vanaf een ander apparaat</i> op het nieuwe apparaat en scan de QR-code.]]></string> + <string name="v5_6_safer_groups_descr">Beheerders kunnen een lid voor iedereen blokkeren.</string> + <string name="v5_6_app_data_migration">Migratie van app-gegevens</string> + <string name="migrate_from_device_creating_archive_link">Archief link maken</string> + <string name="migrate_from_device_delete_database_from_device">Verwijder de database van dit apparaat</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Bevestig dat u het wachtwoord voor de database onthoudt om deze te migreren.</string> + <string name="v5_6_quantum_resistant_encryption_descr">Activeer in directe chats (BETA)!</string> + <string name="migrate_to_device_downloading_details">Link gegevens downloaden</string> + <string name="migrate_from_device_error_saving_settings">Fout bij opslaan van instellingen</string> + <string name="migrate_from_device_error_deleting_database">Fout bij verwijderen database</string> + <string name="migrate_from_device_confirm_upload">Bevestig het uploaden</string> + <string name="migrate_to_device_download_failed">Download mislukt</string> + <string name="migrate_to_device_downloading_archive">Archief downloaden</string> + <string name="migrate_to_device_enter_passphrase">Voer het wachtwoord in</string> + <string name="migrate_to_device_error_downloading_archive">Fout bij het downloaden van het archief</string> + <string name="migrate_from_device_error_exporting_archive">Fout bij het exporteren van de chat database</string> + <string name="migrate_from_device_error_uploading_archive">Fout bij het uploaden van het archief</string> + <string name="e2ee_info_no_pq_short">Deze chat is beveiligd met end-to-end codering.</string> + <string name="conn_event_disabled_pq">standaard end-to-end encryptie</string> + <string name="migrate_to_device_finalize_migration">Voltooi de migratie op een ander apparaat.</string> + <string name="migrate_from_device_chat_should_be_stopped">Om verder te kunnen gaan, moet de chat worden gestopt.</string> + <string name="migrate_from_device_repeat_upload">Herhaal het uploaden</string> + <string name="migrate_from_device_upload_failed">Upload mislukt</string> + <string name="migrate_from_device_try_again">Je kunt het nog een keer proberen.</string> + <string name="migrate_from_device_finalize_migration">Voltooi de migratie</string> + <string name="migrate_from_device_start_chat">Chat starten</string> + <string name="migrate_from_device_error_verifying_passphrase">Fout bij het verifiëren van het wachtwoord:</string> + <string name="v5_6_quantum_resistant_encryption">quantum bestendige encryptie</string> + <string name="v5_6_picture_in_picture_calls">Beeld-in-beeld oproepen</string> + <string name="v5_6_safer_groups">Veiligere groepen</string> + <string name="v5_6_picture_in_picture_calls_descr">Gebruik de app tijdens het gesprek.</string> + <string name="migrate_to_device_migrating">Migreren</string> + <string name="or_paste_archive_link">Of plak de archief link</string> + <string name="migrate_to_device_database_init">Downloaden voorbereiden</string> + <string name="migrate_to_device_repeat_download">Herhaal het downloaden</string> + <string name="migrate_to_device_try_again">Je kunt het nog een keer proberen.</string> + <string name="migrate_to_device_importing_archive">Archief importeren</string> + <string name="migrate_to_device_repeat_import">Herhaal import</string> + <string name="migrate_to_device_file_delete_or_link_invalid">Bestand is verwijderd of de link is ongeldig</string> + <string name="migrate_from_device_exported_file_doesnt_exist">Geëxporteerd bestand bestaat niet</string> + <string name="migrate_from_device_title">Apparaat migreren</string> + <string name="migrate_from_device_to_another_device">Migreer naar een ander apparaat</string> + <string name="migrate_from_device_stopping_chat">Chat stoppen</string> + <string name="migrate_from_device_bytes_uploaded">%s geüpload</string> + <string name="migrate_from_device_uploading_archive">Archief uploaden</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[U <b>mag niet</b> dezelfde database op twee apparaten gebruiken.]]></string> + <string name="migrate_from_device_verify_passphrase">Controleer het wachtwoord</string> + <string name="migrate_from_device_verify_database_passphrase">Controleer het wachtwoord van de database</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Waarschuwing: het starten van de chat op meerdere apparaten wordt niet ondersteund en zal leiden tot mislukte bezorging van berichten</string> + <string name="migrate_to_device_import_failed">Importeren is mislukt</string> + <string name="invalid_file_link">Ongeldige link</string> + <string name="e2ee_info_no_pq"><![CDATA[Berichten, bestanden en oproepen worden beschermd door <b>end-to-end-codering</b> met perfecte voorwaartse geheimhouding, afwijzing en inbraakherstel.]]></string> + <string name="e2ee_info_pq"><![CDATA[Berichten, bestanden en oproepen worden beschermd door <b>quantum bestendige e2e-encryptie</b> met perfecte voorwaartse geheimhouding, afwijzing en inbraakherstel.]]></string> + <string name="migrate_from_another_device">Migreer vanaf een ander apparaat</string> + <string name="migrate_to_device_title">Migreer hierheen</string> + <string name="v5_6_app_data_migration_descr">Migreer naar een ander apparaat via QR-code.</string> + <string name="migrate_from_device_migration_complete">Migratie voltooid</string> + <string name="auth_open_migration_to_another_device">Open het migratiescherm</string> + <string name="migrate_from_device_or_share_this_file_link">Of deel deze bestands link veilig</string> + <string name="paste_archive_link">Archief link plakken</string> + <string name="migrate_to_device_confirm_network_settings_footer">Controleer of de netwerk instellingen correct zijn voor dit apparaat.</string> + <string name="migrate_from_device_database_init">Uploaden voorbereiden</string> + <string name="conn_event_enabled_pq">quantum bestendige e2e-codering</string> + <string name="migrate_to_device_bytes_downloaded">%s gedownload</string> + <string name="set_passphrase">Wachtwoord instellen</string> + <string name="e2ee_info_pq_short">Deze chat wordt beschermd door quantum bestendige end-to-end codering.</string> + <string name="error_showing_desktop_notification">Fout bij weergeven van melding. Neem contact op met ontwikkelaars.</string> + <string name="permissions_find_in_settings_and_grant">Zoek deze toestemming in de Android-instellingen en verleen deze handmatig.</string> + <string name="permissions_grant_in_settings">Verlenen in instellingen</string> + <string name="permissions_required">Machtigingen verlenen</string> + <string name="permissions_grant">Verleen toestemming(en) om te bellen</string> + <string name="permissions_record_audio">Microfoon</string> + <string name="permissions_camera">Camera</string> + <string name="permissions_camera_and_record_audio">Camera en microfoon</string> + <string name="permissions_open_settings">Open instellingen</string> + <string name="network_type_cellular">Mobiel</string> + <string name="forwarded_description">doorgestuurd</string> + <string name="settings_section_title_network_connection">Netwerkverbinding</string> + <string name="network_type_no_network_connection">Geen netwerkverbinding</string> + <string name="network_type_other">Ander</string> + <string name="network_type_network_wifi">Wifi</string> + <string name="network_type_ethernet">Bekabeld Ethernet</string> + <string name="files_and_media_not_allowed">Bestanden en media niet toegestaan</string> + <string name="simplex_links_not_allowed">SimpleX-links zijn niet toegestaan</string> + <string name="voice_messages_not_allowed">Spraakberichten niet toegestaan</string> + <string name="simplex_links">SimpleX-links</string> + <string name="prohibit_sending_simplex_links">Verbied het verzenden van SimpleX-links</string> + <string name="feature_roles_admins">beheerders</string> + <string name="feature_roles_all_members">alle leden</string> + <string name="feature_roles_owners">eigenaren</string> + <string name="simplex_links_are_prohibited_in_group">SimpleX-links zijn niet toegestaan.</string> + <string name="feature_enabled_for">Ingeschakeld voor</string> + <string name="allow_to_send_simplex_links">Sta het verzenden van SimpleX-links toe.</string> + <string name="group_members_can_send_simplex_links">Leden kunnen SimpleX-links verzenden.</string> + <string name="saved_description">opgeslagen</string> + <string name="saved_from_description">opgeslagen van %s</string> + <string name="forward_chat_item">Doorsturen</string> + <string name="forwarded_chat_item_info_tab">Doorgestuurd</string> + <string name="recipients_can_not_see_who_message_from">Ontvanger(s) kunnen niet zien van wie dit bericht afkomstig is.</string> + <string name="saved_from_chat_item_info_title">Opgeslagen van</string> + <string name="forward_message">Bericht doorsturen…</string> + <string name="download_file">Downloaden</string> + <string name="forwarded_from_chat_item_info_title">Doorgestuurd vanuit</string> + <string name="saved_chat_item_info_tab">Opgeslagen</string> + <string name="audio_device_earpiece">Oortje</string> + <string name="audio_device_wired_headphones">Koptelefoon</string> + <string name="audio_device_speaker">Luidspreker</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="v5_7_forward_descr">Berichtbron blijft privé.</string> + <string name="v5_7_network">Netwerkbeheer</string> + <string name="v5_7_call_sounds_descr">Bij het verbinden van audio- en video-oproepen.</string> + <string name="v5_7_quantum_resistant_encryption_descr">Wordt ingeschakeld in directe chats!</string> + <string name="network_option_rcv_concurrency">Gelijktijdig ontvangen</string> + <string name="v5_7_forward">Berichten doorsturen en opslaan</string> + <string name="v5_7_call_sounds">Geluiden tijdens het bellen</string> + <string name="v5_7_new_interface_languages">Litouwse gebruikersinterface</string> + <string name="v5_7_network_descr">Betrouwbaardere netwerkverbinding.</string> + <string name="v5_7_shape_profile_images">Vorm profiel afbeeldingen</string> + <string name="settings_section_title_profile_images">Profiel afbeeldingen</string> + <string name="v5_7_shape_profile_images_descr">Vierkant, cirkel of iets daartussenin.</string> + <string name="snd_error_quota">Capaciteit overschreden - ontvanger heeft eerder verzonden berichten niet ontvangen.</string> + <string name="snd_error_relay">Fout met bestemmingsserver: %1$s</string> + <string name="ci_status_other_error">Fout: %1$s</string> + <string name="snd_error_proxy_relay">Doorstuurserver: %1$s +\nBestemmingsserverfout: %2$s</string> + <string name="snd_error_proxy">Doorstuurserver: %1$s +\nFout: %2$s</string> + <string name="message_delivery_warning_title">Waarschuwing voor berichtbezorging</string> + <string name="snd_error_expired">Netwerkproblemen - bericht is verlopen na vele pogingen om het te verzenden.</string> + <string name="srv_error_host">Serveradres is niet compatibel met netwerkinstellingen.</string> + <string name="srv_error_version">Serverversie is incompatibel met netwerkinstellingen.</string> + <string name="snd_error_auth">Verkeerde sleutel of onbekende verbinding - hoogstwaarschijnlijk is deze verbinding verwijderd.</string> + <string name="network_smp_proxy_mode_always">Altijd</string> + <string name="network_smp_proxy_mode_private_routing">Privéroutering</string> + <string name="network_smp_proxy_mode_unknown">Onbekende servers</string> + <string name="network_smp_proxy_mode_never">Nooit</string> + <string name="network_smp_proxy_mode_unprotected">Onbeschermd</string> + <string name="network_smp_proxy_mode_always_description">Gebruik altijd privéroutering.</string> + <string name="network_smp_proxy_mode_unknown_description">Gebruik privéroutering met onbekende servers.</string> + <string name="network_smp_proxy_mode_never_description">Gebruik GEEN privéroutering.</string> + <string name="update_network_smp_proxy_mode_question">Berichtrouteringsmodus</string> + <string name="network_smp_proxy_mode_unprotected_description">Gebruik privéroutering met onbekende servers wanneer het IP-adres niet beveiligd is.</string> + <string name="network_smp_proxy_fallback_allow_downgrade">Downgraden toestaan</string> + <string name="network_smp_proxy_fallback_allow_protected">Wanneer IP verborgen is</string> + <string name="network_smp_proxy_fallback_allow">Ja</string> + <string name="network_smp_proxy_fallback_prohibit">Nee</string> + <string name="network_smp_proxy_fallback_allow_description">Stuur berichten rechtstreeks wanneer uw of de doelserver geen privéroutering ondersteunt.</string> + <string name="private_routing_show_message_status">Toon berichtstatus</string> + <string name="private_routing_explanation">Om uw IP-adres te beschermen, gebruikt privéroutering uw SMP-servers om berichten te bezorgen.</string> + <string name="network_smp_proxy_fallback_prohibit_description">Stuur GEEN berichten rechtstreeks, zelfs als uw of de bestemmingsserver geen privéroutering ondersteunt.</string> + <string name="update_network_smp_proxy_fallback_question">Terugval op berichtroutering</string> + <string name="settings_section_title_private_message_routing">PRIVÉBERICHT ROUTING</string> + <string name="network_smp_proxy_fallback_allow_protected_description">Stuur berichten rechtstreeks als het IP-adres beschermd is en uw of bestemmingsserver geen privéroutering ondersteunt.</string> + <string name="file_not_approved_title">Onbekende servers!</string> + <string name="file_not_approved_descr">Zonder Tor of VPN is uw IP-adres zichtbaar voor deze XFTP-relays: +\n%1$s.</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Zonder Tor of VPN is uw IP-adres zichtbaar voor bestandsservers.</string> + <string name="settings_section_title_files">BESTANDEN</string> + <string name="protect_ip_address">Bescherm het IP-adres</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">De app vraagt om downloads van onbekende bestandsservers te bevestigen (behalve .onion of wanneer SOCKS-proxy is ingeschakeld).</string> + <string name="error_initializing_web_view">Fout bij het initialiseren van WebView. Update uw systeem naar de nieuwe versie. Neem contact op met ontwikkelaars. +\nFout: %s</string> + <string name="color_wallpaper_tint">Achtergrond accent</string> + <string name="wallpaper_scale_fill">Vullen</string> + <string name="wallpaper_scale_fit">Passen</string> + <string name="wallpaper_preview_hello_alice">Goedemiddag!</string> + <string name="wallpaper_preview_hello_bob">Goedemorgen!</string> + <string name="theme_remove_image">Verwijder afbeelding</string> + <string name="wallpaper_scale">Schaal</string> + <string name="chat_theme_apply_to_all_modes">Alle kleurmodi</string> + <string name="chat_theme_apply_to_mode">Toepassen op</string> + <string name="chat_theme_apply_to_light_mode">Lichte modus</string> + <string name="v5_8_chat_themes_descr">Laat uw chats er anders uitzien!</string> + <string name="v5_8_chat_themes">Nieuwe chatthema\'s</string> + <string name="v5_8_private_routing">Routing van privéberichten🚀</string> + <string name="v5_8_safe_files_descr">Bevestig bestanden van onbekende servers.</string> + <string name="v5_8_message_delivery">Verbeterde bezorging van berichten</string> + <string name="v5_8_persian_ui">Perzische gebruikersinterface</string> + <string name="v5_8_safe_files">Veilig bestanden ontvangen</string> + <string name="v5_8_message_delivery_descr">Met verminderd batterijgebruik.</string> + <string name="theme_destination_app_theme">App thema</string> + <string name="chat_theme_reset_to_app_theme">Terugzetten naar app thema</string> + <string name="chat_theme_reset_to_user_theme">Terugzetten naar gebruikersthema</string> + <string name="chat_theme_apply_to_dark_mode">Donkere modus</string> + <string name="wallpaper_advanced_settings">Geavanceerde instellingen</string> + <string name="v5_8_private_routing_descr">Bescherm uw IP-adres tegen de berichtenrelais die door uw contacten zijn gekozen. +\nSchakel dit in in *Netwerk en servers*-instellingen.</string> + <string name="wallpaper_scale_repeat">Herhalen</string> + <string name="settings_section_title_chat_colors">Chat kleuren</string> + <string name="settings_section_title_user_theme">Profiel thema</string> + <string name="settings_section_title_chat_theme">Chat thema</string> + <string name="color_primary_variant2">Extra accent 2</string> + <string name="theme_black">Zwart</string> + <string name="color_mode">Kleur mode</string> + <string name="color_mode_dark">Donker</string> + <string name="dark_mode_colors">Kleuren in donkere modus</string> + <string name="color_mode_light">Licht</string> + <string name="color_received_quote">Antwoord ontvangen</string> + <string name="reset_single_color">Kleur opnieuw instellen</string> + <string name="color_sent_quote">Antwoord verzonden</string> + <string name="color_mode_system">Systeem</string> + <string name="color_wallpaper_background">Wallpaper achtergrond</string> + <string name="chat_theme_set_default_theme">Stel het standaard thema in</string> + <string name="chat_list_always_visible">Toon chat-lijst in nieuw venster</string> + <string name="message_queue_info_none">geen</string> + <string name="info_row_debug_delivery">Foutopsporing bezorging</string> + <string name="message_queue_info">Informatie over berichtenwachtrij</string> + <string name="message_queue_info_server_info">informatie over serverwachtrij: %1$s +\n +\nlaatst ontvangen bericht: %2$s</string> + <string name="file_error_no_file">Bestand niet gevonden - hoogstwaarschijnlijk is het bestand verwijderd of geannuleerd.</string> + <string name="file_error_auth">Verkeerde sleutel of onbekend bestanddeeladres - hoogstwaarschijnlijk is het bestand verwijderd.</string> + <string name="file_error_relay">Bestandsserverfout: %1$s</string> + <string name="file_error">Bestandsfout</string> + <string name="temporary_file_error">Tijdelijke bestandsfout</string> + <string name="copy_error">Kopieerfout</string> + <string name="remote_ctrl_connection_stopped_desc">Controleer of mobiel en desktop met hetzelfde lokale netwerk zijn verbonden en of de desktopfirewall de verbinding toestaat. +\nDeel eventuele andere problemen met de ontwikkelaars.</string> + <string name="remote_ctrl_connection_stopped_identity_desc">Deze link is gebruikt met een ander mobiel apparaat. Maak een nieuwe link op de desktop.</string> + <string name="info_row_file_status">Bestandsstatus</string> + <string name="share_text_message_status">Berichtstatus: %s</string> + <string name="share_text_file_status">Bestandsstatus: %s</string> + <string name="info_row_message_status">Berichtstatus</string> + <string name="cannot_share_message_alert_title">Kan bericht niet verzenden</string> + <string name="cannot_share_message_alert_text">Geselecteerde chat voorkeuren verbieden dit bericht.</string> + <string name="private_routing_error">Fout in privéroutering</string> + <string name="network_error_broker_version_desc">Serverversie is niet compatibel met uw app: %1$s.</string> + <string name="message_forwarded_title">Bericht doorgestuurd</string> + <string name="message_forwarded_desc">Nog geen directe verbinding, bericht wordt doorgestuurd door beheerder.</string> + <string name="xftp_servers_other">Overige XFTP servers</string> + <string name="scan_paste_link">Link scannen/plakken</string> + <string name="appearance_zoom">Zoom</string> + <string name="current_user">Huidig profiel</string> + <string name="servers_info_files_tab">Bestanden</string> + <string name="servers_info">Server informatie</string> + <string name="servers_info_target">Informatie weergeven voor</string> + <string name="servers_info_sessions_errors">Fouten</string> + <string name="servers_info_statistics_section_header">Statistieken</string> + <string name="servers_info_transport_sessions_section_header">Transportsessies</string> + <string name="servers_info_subscriptions_connections_subscribed">Actieve verbindingen</string> + <string name="servers_info_details">Details</string> + <string name="servers_info_messages_received">Berichten ontvangen</string> + <string name="servers_info_subscriptions_section_header">Bericht ontvangst</string> + <string name="servers_info_private_data_disclaimer">Vanaf %s.\nAlle gegevens blijven privé op uw apparaat.</string> + <string name="servers_info_connected_servers_section_header">Verbonden servers</string> + <string name="servers_info_subscriptions_connections_pending">In behandeling</string> + <string name="servers_info_previously_connected_servers_section_header">Eerder verbonden servers</string> + <string name="servers_info_proxied_servers_section_header">Proxied servers</string> + <string name="servers_info_subscriptions_total">Totaal</string> + <string name="servers_info_reconnect_server_title">Server opnieuw verbinden?</string> + <string name="servers_info_reconnect_servers_title">Servers opnieuw verbinden?</string> + <string name="servers_info_reconnect_server_message">Maak opnieuw verbinding met de server om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra data.</string> + <string name="servers_info_proxied_servers_section_footer">U bent niet verbonden met deze servers. Privéroutering wordt gebruikt om berichten bij hen af te leveren.</string> + <string name="servers_info_reconnect_all_servers_button">Maak opnieuw verbinding met alle servers</string> + <string name="servers_info_reset_stats_alert_confirm">Reset</string> + <string name="servers_info_reset_stats">Reset alle statistieken</string> + <string name="servers_info_reset_stats_alert_title">Alle statistieken resetten?</string> + <string name="servers_info_uploaded">Geüpload</string> + <string name="servers_info_detailed_statistics">Gedetailleerde statistieken</string> + <string name="servers_info_detailed_statistics_received_messages_header">Ontvangen berichten</string> + <string name="servers_info_detailed_statistics_sent_messages_header">Verzonden berichten</string> + <string name="servers_info_detailed_statistics_sent_messages_total">Totaal verzonden</string> + <string name="proxied">Proxied</string> + <string name="servers_info_detailed_statistics_receive_errors">Fouten ontvangen</string> + <string name="reconnect">opnieuw verbinden</string> + <string name="sent_directly">Direct verzonden</string> + <string name="sent_via_proxy">Verzonden via proxy</string> + <string name="smp_server">SMP server</string> + <string name="xftp_server">XFTP server</string> + <string name="acknowledged">Erkend</string> + <string name="acknowledgement_errors">Bevestigingsfouten</string> + <string name="connections">Verbindingen</string> + <string name="created">Gemaakt</string> + <string name="decryption_errors">decoderingsfouten</string> + <string name="duplicates_label">duplicaten</string> + <string name="expired_label">verlopen</string> + <string name="other_errors">overige fouten</string> + <string name="other_label">overig</string> + <string name="send_errors">Verzend fouten</string> + <string name="chunks_downloaded">Stukken gedownload</string> + <string name="chunks_uploaded">Stukken geüpload</string> + <string name="deleted">Verwijderd</string> + <string name="deletion_errors">Verwijderingsfouten</string> + <string name="downloaded_files">Gedownloade bestanden</string> + <string name="secured">Beveiligd</string> + <string name="size">Maat</string> + <string name="subscribed">Subscribed</string> + <string name="uploaded_files">Geüploade bestanden</string> + <string name="upload_errors">Upload fouten</string> + <string name="download_errors">Downloadfouten</string> + <string name="open_server_settings_button">Server instellingen openen</string> + <string name="server_address">Server adres</string> + <string name="all_users">Alle profielen</string> + <string name="attempts_label">pogingen</string> + <string name="chunks_deleted">Stukken verwijderd</string> + <string name="completed">Voltooid</string> + <string name="servers_info_sessions_connected">Verbonden</string> + <string name="servers_info_sessions_connecting">Verbinden</string> + <string name="servers_info_reconnect_server_error">Fout bij opnieuw verbinding maken met de server</string> + <string name="appearance_font_size">Letter grootte</string> + <string name="member_info_member_inactive">inactief</string> + <string name="member_inactive_title">Lid inactief</string> + <string name="servers_info_downloaded">Gedownload</string> + <string name="servers_info_modal_error_title">Fout</string> + <string name="servers_info_reconnect_servers_error">Fout bij opnieuw verbinden van servers</string> + <string name="servers_info_reset_stats_alert_error_title">Fout bij het resetten van statistieken</string> + <string name="member_inactive_desc">Het bericht kan later worden bezorgd als het lid actief wordt.</string> + <string name="servers_info_messages_sent">Berichten verzonden</string> + <string name="servers_info_missing">Geen info, probeer opnieuw te laden</string> + <string name="smp_servers_other">Overige SMP servers</string> + <string name="servers_info_detailed_statistics_received_total">Totaal ontvangen</string> + <string name="please_try_later">Probeer het later.</string> + <string name="servers_info_reconnect_servers_message">Maak opnieuw verbinding met alle verbonden servers om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra data.</string> + <string name="network_error_broker_host_desc">Het serveradres is niet compatibel met de netwerkinstellingen: %1$s.</string> + <string name="servers_info_reset_stats_alert_message">Serverstatistieken worden gereset - dit kan niet ongedaan worden gemaakt!</string> + <string name="servers_info_starting_from">Beginnend vanaf %s.</string> + <string name="smp_servers_configured">Geconfigureerde SMP-servers</string> + <string name="xftp_servers_configured">Geconfigureerde XFTP-servers</string> + <string name="subscription_percentage">Percentage weergeven</string> + <string name="app_check_for_updates_disabled">Uitgeschakeld</string> + <string name="app_check_for_updates_download_started">App update downloaden. Sluit de app niet</string> + <string name="app_check_for_updates_button_download">%s downloaden (%s)</string> + <string name="app_check_for_updates_installed_successfully_title">Succesvol geïnstalleerd</string> + <string name="app_check_for_updates_button_install">Installeer update</string> + <string name="app_check_for_updates_button_open">Open de bestandslocatie</string> + <string name="app_check_for_updates_installed_successfully_desc">Herstart de app.</string> + <string name="app_check_for_updates_button_remind_later">Herinner later</string> + <string name="app_check_for_updates_button_skip">Sla deze versie over</string> + <string name="member_info_member_disabled">uitgeschakeld</string> + <string name="app_check_for_updates_download_completed_title">App update is gedownload</string> + <string name="app_check_for_updates_beta">Beta</string> + <string name="app_check_for_updates">Controleer op updates</string> + <string name="app_check_for_updates_notice_title">Controleer op updates</string> + <string name="app_check_for_updates_notice_disable">Uitschakelen</string> + <string name="subscription_errors">Subscription fouten</string> + <string name="subscription_results_ignored">Subscriptions genegeerd</string> + <string name="app_check_for_updates_stable">Stabiel</string> + <string name="app_check_for_updates_update_available">Update beschikbaar: %s</string> + <string name="app_check_for_updates_canceled">Downloaden van update geannuleerd</string> + <string name="app_check_for_updates_notice_desc">Als u op de hoogte wilt worden gehouden van de nieuwe releases, schakelt u periodieke controle op stabiele of bètaversies in.</string> + <string name="privacy_media_blur_radius">Vervaag media</string> + <string name="privacy_media_blur_radius_medium">Medium</string> + <string name="privacy_media_blur_radius_off">uit</string> + <string name="privacy_media_blur_radius_soft">Soft</string> + <string name="privacy_media_blur_radius_strong">Krachtig</string> + <string name="proxy_destination_error_broker_host">Het doelserveradres van %1$s is niet compatibel met de instellingen van de doorstuurserver %2$s.</string> + <string name="proxy_destination_error_broker_version">De doelserverversie van %1$s is incompatibel met de doorstuurserver %2$s.</string> + <string name="smp_proxy_error_connecting">Fout bij verbinden met doorstuurserver %1$s. Probeer het later opnieuw.</string> + <string name="proxy_destination_error_failed_to_connect">Doorstuurserver %1$s kon geen verbinding maken met bestemmingsserver %2$s. Probeer het later opnieuw.</string> + <string name="smp_proxy_error_broker_host">Het doorstuuradres is niet compatibel met de netwerkinstellingen: %1$s.</string> + <string name="smp_proxy_error_broker_version">De doorstuurserverversie is niet compatibel met de netwerkinstellingen: %1$s.</string> + <string name="cant_call_contact_alert_title">Kan contact niet bellen</string> + <string name="allow_calls_question">Oproepen toestaan?</string> + <string name="info_view_call_button">bellen</string> + <string name="cant_call_member_alert_title">Kan geen groepslid bellen</string> + <string name="calls_prohibited_alert_title">Bellen niet toegestaan!</string> + <string name="confirm_delete_contact_question">Contact verwijderen bevestigen?</string> + <string name="conversation_deleted">Gesprek verwijderd!</string> + <string name="delete_without_notification">Verwijderen zonder melding</string> + <string name="deleted_chats">Gearchiveerde contacten</string> + <string name="cant_call_contact_deleted_alert_text">Contact is verwijderd.</string> + <string name="cant_call_contact_connecting_wait_alert_text">Er wordt verbinding gemaakt met het contact. Even geduld of controleer het later!</string> + <string name="cant_send_message_to_member_alert_title">Kan geen bericht sturen naar groepslid</string> + <string name="delete_members_messages__question">%d berichten van leden verwijderen?</string> + <string name="compose_message_placeholder">Bericht</string> + <string name="delete_messages_mark_deleted_warning">Berichten worden gemarkeerd voor verwijdering. De ontvanger(s) kunnen deze berichten onthullen.</string> + <string name="selected_chat_items_nothing_selected">Niets geselecteerd</string> + <string name="info_view_connect_button">verbinden</string> + <string name="contact_deleted">Contact verwijderd!</string> + <string name="delete_contact_cannot_undo_warning">Het contact wordt verwijderd. Dit kan niet ongedaan worden gemaakt!</string> + <string name="keep_conversation">Behoud het gesprek</string> + <string name="info_view_message_button">bericht</string> + <string name="only_delete_conversation">Alleen conversatie verwijderen</string> + <string name="info_view_open_button">open</string> + <string name="paste_link">Plak de link</string> + <string name="action_button_add_members">Uitnodiging</string> + <string name="one_hand_ui">Bereikbare app-toolbars</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">Vraag uw contactpersoon om oproepen in te schakelen.</string> + <string name="no_filtered_contacts">Geen gefilterde contacten</string> + <string name="select_verb">Selecteer</string> + <string name="selected_chat_items_selected_n">Geselecteerd %d</string> + <string name="cant_call_member_send_message_alert_text">Stuur een bericht om oproepen mogelijk te maken.</string> + <string name="info_view_search_button">zoekopdracht</string> + <string name="you_can_still_send_messages_to_contact">U kunt berichten naar %1$s sturen vanuit gearchiveerde contacten.</string> + <string name="toolbar_settings">Instellingen</string> + <string name="moderate_messages_will_be_deleted_warning">De berichten worden voor alle leden verwijderd.</string> + <string name="moderate_messages_will_be_marked_warning">De berichten worden voor alle leden als gemodereerd gemarkeerd.</string> + <string name="info_view_video_button">video</string> + <string name="you_can_still_view_conversation_with_contact">Je kunt nog steeds het gesprek met %1$s bekijken in de lijst met chats.</string> + <string name="you_need_to_allow_calls">U moet uw contactpersoon toestemming geven om te bellen, zodat hij/zij je kan bellen.</string> + <string name="contact_list_header_title">Jouw contacten</string> + <string name="message_servers">Berichtservers</string> + <string name="media_and_file_servers">Media- en bestandsservers</string> + <string name="chat_database_exported_title">Chat database geëxporteerd</string> + <string name="chat_database_exported_continue">Doorgaan</string> + <string name="network_socks_proxy">SOCKS proxy</string> + <string name="chat_database_exported_migrate">U kunt de geëxporteerde database migreren.</string> + <string name="chat_database_exported_save">U kunt het geëxporteerde archief opslaan.</string> + <string name="network_options_save_and_reconnect">Opslaan en opnieuw verbinden</string> + <string name="network_option_tcp_connection">TCP verbinding</string> + <string name="v6_0_private_routing_descr">Het beschermt uw IP-adres en verbindingen.</string> + <string name="v6_0_reachable_chat_toolbar">Toegankelijke chatwerkbalk</string> + <string name="v6_0_reachable_chat_toolbar_descr">Gebruik de app met één hand.</string> + <string name="v6_0_connection_servers_status">Beheer uw netwerk</string> + <string name="v6_0_connect_faster_descr">Maak sneller verbinding met je vrienden</string> + <string name="v6_0_your_contacts_descr">Archiveer contacten om later te chatten.</string> + <string name="v6_0_connection_servers_status_descr">Verbindings- en serverstatus.</string> + <string name="v6_0_delete_many_messages_descr">Verwijder maximaal 20 berichten tegelijk.</string> + <string name="chat_database_exported_not_all_files">Sommige bestanden zijn niet geëxporteerd</string> + <string name="reset_all_hints">Alle hints resetten</string> + <string name="one_hand_ui_card_title">Chat-lijst wisselen:</string> + <string name="one_hand_ui_change_instruction">U kunt dit wijzigen in de instellingen onder uiterlijk</string> + <string name="create_address_button">Creëren</string> + <string name="v6_0_privacy_blur">Vervagen voor betere privacy.</string> + <string name="v6_0_chat_list_media">Afspelen via de chat-lijst.</string> + <string name="v6_0_upgrade_app_descr">Download nieuwe versies van GitHub.</string> + <string name="v6_0_increase_font_size">Vergroot het lettertype.</string> + <string name="v6_0_upgrade_app">App automatisch upgraden</string> + <string name="v6_0_new_chat_experience">Nieuwe chatervaring 🎉</string> + <string name="v6_0_new_media_options">Nieuwe media-opties</string> + <string name="invite_friends_short">Uitnodigen</string> + <string name="new_message">Nieuw bericht</string> + <string name="error_parsing_uri_title">Ongeldige link</string> + <string name="error_parsing_uri_desc">Controleer of de SimpleX-link correct is.</string> + <string name="switching_profile_error_title">Fout bij wisselen van profiel</string> + <string name="select_chat_profile">Selecteer chatprofiel</string> + <string name="new_chat_share_profile">Profiel delen</string> + <string name="switching_profile_error_message">Uw verbinding is verplaatst naar %s, maar er is een onverwachte fout opgetreden tijdens het omleiden naar het profiel.</string> + <string name="settings_section_title_chat_database">CHAT DATABASE</string> + <string name="system_mode_toast">Systeemmodus</string> + <string name="migrate_from_device_remove_archive_question">Archief verwijderen?</string> + <string name="delete_messages_cannot_be_undone_warning">Berichten worden verwijderd. Dit kan niet ongedaan worden gemaakt!</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">Het geüploade databasearchief wordt permanent van de servers verwijderd.</string> + <string name="n_file_errors">%1$d bestandsfout(en): +\n%2$s</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s berichten niet doorgestuurd</string> + <string name="forward_alert_title_messages_to_forward">%1$s bericht(en) doorsturen?</string> + <string name="network_proxy_auth">Proxy-authenticatie</string> + <string name="forward_files_messages_deleted_after_selection_desc">Berichten zijn verwijderd nadat u ze had geselecteerd.</string> + <string name="network_proxy_username">Gebruikersnaam</string> + <string name="n_other_file_errors">%1$d overige bestandsfout(en).</string> + <string name="error_forwarding_messages">Fout bij het doorsturen van berichten</string> + <string name="forward_alert_title_nothing_to_forward">Niets om door te sturen!</string> + <string name="forward_files_in_progress_desc">%1$d bestand(en) worden nog gedownload.</string> + <string name="forward_files_not_accepted_desc">%1$d bestand(en) zijn niet gedownload.</string> + <string name="forward_alert_forward_messages_without_files">Berichten doorsturen zonder bestanden?</string> + <string name="forward_files_failed_to_receive_desc">%1$d bestand(en) konden niet worden gedownload.</string> + <string name="forward_files_missing_desc">%1$d bestand(en) zijn verwijderd.</string> + <string name="forward_files_not_accepted_receive_files">Download</string> + <string name="compose_forward_messages_n">%1$s berichten doorsturen</string> + <string name="forward_multiple">Berichten doorsturen…</string> + <string name="compose_save_messages_n">%1$s berichten opslaan</string> + <string name="network_proxy_auth_mode_no_auth">Gebruik geen inloggegevens met proxy.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">Gebruik voor elke verbinding verschillende proxy-inloggegevens.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">Gebruik voor elk profiel verschillende proxy-inloggegevens.</string> + <string name="network_proxy_random_credentials">Gebruik willekeurige inloggegevens</string> + <string name="network_proxy_auth_mode_username_password">Uw inloggegevens worden mogelijk niet-versleuteld verzonden.</string> + <string name="network_proxy_incorrect_config_title">Fout bij opslaan proxy</string> + <string name="network_proxy_incorrect_config_desc">Zorg ervoor dat de proxyconfiguratie correct is.</string> + <string name="network_proxy_password">Wachtwoord</string> + <string name="icon_descr_sound_muted">Geluid gedempt</string> + <string name="error_initializing_web_view_wrong_arch">Fout bij initialiseren van WebView. Zorg ervoor dat WebView geïnstalleerd is en de ondersteunde architectuur is arm64.\nFout: %s</string> + <string name="settings_message_shape_corner">Hoek</string> + <string name="settings_section_title_message_shape">Berichtvorm</string> + <string name="network_session_mode_session">Appsessie</string> + <string name="network_session_mode_session_description">Elke keer dat u de app start, worden er nieuwe SOCKS-inloggegevens gebruikt.</string> + <string name="network_session_mode_server_description">Voor elke server worden nieuwe SOCKS-inloggegevens gebruikt.</string> + <string name="network_session_mode_server">Server</string> + <string name="settings_message_shape_tail">Staart</string> + <string name="call_desktop_permission_denied_chrome">Klik op de infoknop naast het adresveld om het gebruik van de microfoon toe te staan.</string> + <string name="call_desktop_permission_denied_safari">Open Safari Instellingen / Websites / Microfoon en kies Toestaan voor localhost.</string> + <string name="call_desktop_permission_denied_title">Als u wilt bellen, geeft u toestemming om uw microfoon te gebruiken. Beëindig het gesprek en probeer opnieuw te bellen.</string> + <string name="v6_1_better_security">Betere beveiliging ✅</string> + <string name="v6_1_better_security_descr">SimpleX-protocollen beoordeeld door Trail of Bits.</string> + <string name="v6_1_message_dates_descr">Betere datums voor berichten.</string> + <string name="v6_1_better_user_experience">Betere gebruikerservaring</string> + <string name="v6_1_customizable_message_descr">Aanpasbare berichtvorm.</string> + <string name="v6_1_better_calls_descr">Wisselen tussen audio en video tijdens het gesprek.</string> + <string name="v6_1_switch_chat_profile_descr">Wijzig chatprofiel voor eenmalige uitnodigingen.</string> + <string name="v6_1_delete_many_messages_descr">Maximaal 200 berichten verwijderen of modereren.</string> + <string name="v6_1_forward_many_messages_descr">Stuur maximaal 20 berichten tegelijk door.</string> + <string name="v6_1_better_calls">Betere gesprekken</string> + <string name="accept_conditions">Accepteer voorwaarden</string> + <string name="operator_added_message_servers">Berichtservers toegevoegd</string> + <string name="operator_added_xftp_servers">Media- en bestandsservers toegevoegd</string> + <string name="operator_conditions_accepted">Geaccepteerde voorwaarden</string> + <string name="address_or_1_time_link">Adres of eenmalige link?</string> + <string name="operators_conditions_accepted_for"><![CDATA[Voorwaarden zijn geaccepteerd voor de operator(s): <b>%s</b>.]]></string> + <string name="failed_to_save_servers">Fout bij het opslaan van servers</string> + <string name="no_message_servers_configured">Geen berichtenservers.</string> + <string name="no_media_servers_configured">Geen media- en bestandsservers.</string> + <string name="no_media_servers_configured_for_private_routing">Geen servers om bestanden te ontvangen.</string> + <string name="no_message_servers_configured_for_receiving">Geen servers om berichten te ontvangen.</string> + <string name="errors_in_servers_configuration">Fouten in de serverconfiguratie.</string> + <string name="for_chat_profile">Voor chatprofiel %s:</string> + <string name="connection_error_quota">Niet afgeleverde berichten</string> + <string name="connection_error_quota_desc">De verbinding heeft de limiet van niet-afgeleverde berichten bereikt. Uw contactpersoon is mogelijk offline.</string> + <string name="message_deleted_or_not_received_error_desc">Dit bericht is verwijderd of nog niet ontvangen.</string> + <string name="address_creation_instruction">Tik op SimpleX-adres maken in het menu om het later te maken.</string> + <string name="share_address_publicly">Adres openbaar delen</string> + <string name="share_1_time_link_with_a_friend">Deel eenmalig een link met een vriend</string> + <string name="share_simplex_address_on_social_media">Deel het SimpleX-adres op sociale media.</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[Eenmalige link kan <i>slechts met één contactpersoon</i> worden gebruikt - deel persoonlijk of via een messenger.]]></string> + <string name="connection_security">Beveiliging van de verbinding</string> + <string name="you_can_set_connection_name_to_remember">U kunt een verbindingsnaam instellen, zodat u kunt onthouden met wie de link is gedeeld.</string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">SimpleX-adressen en eenmalige links kunnen veilig worden gedeeld via elke messenger.</string> + <string name="smp_servers_new_server">Nieuwe server</string> + <string name="for_social_media">Voor social media</string> + <string name="or_to_share_privately">Of om privé te delen</string> + <string name="address_settings">Adres instellingen</string> + <string name="create_1_time_link">Eenmalige link maken</string> + <string name="onboarding_choose_server_operators">Serverbeheerders</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">Voor ingeschakelde operators worden de voorwaarden na 30 dagen geaccepteerd.</string> + <string name="onboarding_network_operators">Netwerkbeheerders</string> + <string name="onboarding_network_operators_review_later">Later beoordelen</string> + <string name="onboarding_select_network_operators_to_use">Selecteer welke netwerkoperators u wilt gebruiken.</string> + <string name="onboarding_network_operators_update">Update</string> + <string name="onboarding_network_operators_app_will_use_different_operators">De app beschermt uw privacy door in elk gesprek verschillende operators te gebruiken.</string> + <string name="onboarding_network_operators_conditions_you_can_configure">U kunt operators configureren in Netwerk- en serverinstellingen.</string> + <string name="onboarding_network_operators_continue">Doorgaan</string> + <string name="operator_review_conditions">Voorwaarden bekijken</string> + <string name="your_servers">Uw servers</string> + <string name="operator_conditions_accepted_for_some"><![CDATA[Voorwaarden worden al geaccepteerd voor de volgende operator(s): <b>%s</b>.]]></string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[Voorwaarden worden geaccepteerd voor de operator(s): <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_applied"><![CDATA[Deze voorwaarden zijn ook van toepassing op: <b>%s</b>.]]></string> + <string name="operator_conditions_accepted_on">Voorwaarden geaccepteerd op: %s.</string> + <string name="operator_conditions_will_be_accepted_on">Voorwaarden worden geaccepteerd op: %s.</string> + <string name="operator_conditions_failed_to_load">De tekst van de huidige voorwaarden kon niet worden geladen. U kunt de voorwaarden bekijken via deze link:</string> + <string name="operator_info_title">Netwerkbeheerder</string> + <string name="operator">Operator</string> + <string name="operator_servers_title">%s servers</string> + <string name="use_servers_of_operator_x">Gebruik %s</string> + <string name="operator_use_operator_toggle_description">Gebruik servers</string> + <string name="operator_website">Website</string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Voorwaarden worden geaccepteerd voor operator(s): <b>%s</b>.]]></string> + <string name="operators_conditions_will_also_apply"><![CDATA[Deze voorwaarden zijn ook van toepassing op: <b>%s</b>.]]></string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[Om de servers van <b>%s</b> te gebruiken, moet u de gebruiksvoorwaarden accepteren.]]></string> + <string name="operator_conditions_of_use">Gebruiksvoorwaarden</string> + <string name="operator_use_for_messages_private_routing">Voor privé-routering</string> + <string name="operator_open_changes">Wijzigingen openen</string> + <string name="xftp_servers_per_user">De servers voor nieuwe bestanden van uw huidige chatprofiel</string> + <string name="operator_use_for_messages_receiving">Om te ontvangen</string> + <string name="operator_use_for_files">Gebruik voor bestanden</string> + <string name="operator_use_for_messages">Gebruik voor berichten</string> + <string name="view_conditions">Bekijk voorwaarden</string> + <string name="error_server_protocol_changed">Serverprotocol gewijzigd.</string> + <string name="operator_server_alert_title">Operatorserver</string> + <string name="server_added_to_operator__name">Server toegevoegd aan operator %s.</string> + <string name="appearance_in_app_bars_alpha">Transparantie</string> + <string name="v6_2_network_decentralization_enable_flux_reason">voor betere privacy van metagegevens.</string> + <string name="v6_2_improved_chat_navigation">Verbeterde chatnavigatie</string> + <string name="v6_2_network_decentralization">Netwerk decentralisatie</string> + <string name="v6_2_network_decentralization_descr">De tweede vooraf ingestelde operator in de app!</string> + <string name="onboarding_network_operators_app_will_use_for_routing">Als uw contactpersoon bijvoorbeeld berichten ontvangt via een SimpleX Chat-server, worden deze door uw app via een Flux-server verzonden.</string> + <string name="v6_2_network_decentralization_enable_flux">Schakel Flux in bij Netwerk- en serverinstellingen voor betere privacy van metagegevens.</string> + <string name="message_deleted_or_not_received_error_title">Geen bericht</string> + <string name="appearance_app_toolbars">App-werkbalken</string> + <string name="appearance_bars_blur_radius">Vervagen</string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[Dezelfde voorwaarden zijn van toepassing op operator <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Dezelfde voorwaarden zijn van toepassing op operator(s): <b>%s</b>.]]></string> + <string name="operator_conditions_accepted_for_enabled_operators_on">Voorwaarden worden automatisch geaccepteerd voor ingeschakelde operators op: %s.</string> + <string name="error_updating_server_title">Fout bij het updaten van de server</string> + <string name="error_accepting_operator_conditions">Fout bij het accepteren van voorwaarden</string> + <string name="error_adding_server">Fout bij toevoegen server</string> + <string name="no_message_servers_configured_for_private_routing">Geen servers voor het routeren van privéberichten.</string> + <string name="error_server_operator_changed">Serveroperator gewijzigd.</string> + <string name="no_media_servers_configured_for_sending">Geen servers om bestanden te verzenden.</string> + <string name="operator_open_conditions">Open voorwaarden</string> + <string name="v6_2_improved_chat_navigation_descr">- Open chat op het eerste ongelezen bericht.\n- Ga naar geciteerde berichten.</string> + <string name="network_preset_servers_title">Vooraf ingestelde servers</string> + <string name="view_updated_conditions">Bekijk de bijgewerkte voorwaarden</string> + <string name="simplex_address_or_1_time_link">SimpleX adres of eenmalige link?</string> + <string name="to_protect_against_your_link_replaced_compare_codes">Om te voorkomen dat uw link wordt vervangen, kunt u contactbeveiligingscodes vergelijken.</string> + <string name="operator_use_for_sending">Om te verzenden</string> + <string name="onboarding_network_operators_configure_via_settings">U kunt servers configureren via instellingen.</string> + <string name="chat_archive">Of importeer archiefbestand</string> + <string name="direct_messages_are_prohibited_in_chat">Directe berichten tussen leden zijn in deze chat niet toegestaan.</string> + <string name="remote_hosts_section">Externe mobiele telefoons</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Xiaomi-apparaten</b>: schakel Automatisch starten in de systeeminstellingen in om meldingen te laten werken.]]></string> + <string name="maximum_message_size_title">Bericht is te groot!</string> + <string name="maximum_message_size_reached_text">Verklein het bericht en verstuur het opnieuw.</string> + <string name="maximum_message_size_reached_forwarding">U kunt het bericht kopiëren en verkleinen om het te verzenden.</string> + <string name="add_your_team_members_to_conversations">Voeg uw teamleden toe aan de gesprekken.</string> + <string name="business_address">Zakelijk adres</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[Alle berichten en bestanden worden <b>end-to-end-versleuteld</b> verzonden, met post-kwantumbeveiliging in directe berichten.]]></string> + <string name="onboarding_notifications_mode_service_desc_short">App draait altijd op de achtergrond</string> + <string name="onboarding_notifications_mode_periodic_desc_short">Controleer berichten elke 10 minuten</string> + <string name="onboarding_notifications_mode_battery">Meldingen en batterij</string> + <string name="how_it_helps_privacy">Hoe het de privacy helpt</string> + <string name="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.</string> + <string name="leave_chat_question">Chat verlaten?</string> + <string name="button_add_friends">Vrienden toevoegen</string> + <string name="button_add_team_members">Teamleden toevoegen</string> + <string name="invite_to_chat_button">Uitnodigen voor een chat</string> + <string name="delete_chat_for_self_cannot_undo_warning">De chat wordt voor je verwijderd - dit kan niet ongedaan worden gemaakt!</string> + <string name="info_row_chat">Chat</string> + <string name="direct_messages_are_prohibited">Directe berichten tussen leden zijn niet toegestaan.</string> + <string name="connect_plan_chat_already_exists">Chat bestaat al!</string> + <string name="button_delete_chat">Chat verwijderen</string> + <string name="delete_chat_question">Chat verwijderen?</string> + <string name="button_leave_chat">Chat verlaten</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">Lid wordt verwijderd uit de chat - dit kan niet ongedaan worden gemaakt!</string> + <string name="member_role_will_be_changed_with_notification_chat">De rol wordt gewijzigd naar %s. Iedereen in de chat wordt op de hoogte gebracht.</string> + <string name="chat_main_profile_sent">Uw chatprofiel wordt naar chatleden verzonden</string> + <string name="v6_2_business_chats_descr">Privacy voor uw klanten.</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[U bent al verbonden met <b>%1$s</b>.]]></string> + <string name="delete_chat_for_all_members_cannot_undo_warning">De chat wordt voor alle leden verwijderd - dit kan niet ongedaan worden gemaakt!</string> + <string name="chat_bottom_bar">Bereikbare chat-toolbar</string> + <string name="v6_2_business_chats">Zakelijke chats</string> + <string name="onboarding_notifications_mode_off_desc_short">Geen achtergrondservice</string> + <string name="only_chat_owners_can_change_prefs">Alleen chateigenaren kunnen voorkeuren wijzigen.</string> + <string name="maximum_message_size_reached_non_text">Verklein de berichtgrootte of verwijder de media en verzend het bericht opnieuw.</string> + <string name="display_name_accepted_invitation">geaccepteerde uitnodiging</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">Wanneer er meer dan één operator is ingeschakeld, beschikt geen enkele operator over metagegevens om te achterhalen wie met wie communiceert.</string> + <string name="display_name_requested_to_connect">verzocht om verbinding te maken</string> + <string name="onboarding_network_about_operators">Over operatoren</string> + <string name="onboarding_network_operators_simplex_flux_agreement">Simplex-chat en flux hebben een overeenkomst gemaakt om door flux geëxploiteerde servers in de app op te nemen.</string> + <string name="debug_logs">Logs inschakelen</string> + <string name="error_saving_database">Fout bij het opslaan van de database</string> + <string name="sync_connection_desc">Verbinding vereist heronderhandeling over encryptie.</string> + <string name="encryption_renegotiation_in_progress">Er wordt opnieuw onderhandeld over de encryptie.</string> + <string name="sync_connection_question">Verbinding herstellen?</string> + <string name="sync_connection_confirm">Herstel</string> + <string name="connection_not_ready">Verbinding nog niet klaar</string> + <string name="error_updating_chat_tags">Fout bij het bijwerken van de chat-lijst</string> + <string name="no_chats_in_list">Geen chats in lijst %s.</string> + <string name="chat_list_favorites">Favorieten</string> + <string name="chat_list_groups">Groepen</string> + <string name="no_chats">Geen chats</string> + <string name="no_chats_found">Geen chats gevonden</string> + <string name="chat_list_add_list">Lijst toevoegen</string> + <string name="chat_list_all">alle</string> + <string name="open_with_app">Openen met %s</string> + <string name="create_list">Maak een lijst</string> + <string name="delete_chat_list_menu_action">Verwijderen</string> + <string name="delete_chat_list_question">Lijst verwijderen?</string> + <string name="edit_chat_list_menu_action">Bewerk</string> + <string name="list_name_field_placeholder">Naam van lijst...</string> + <string name="duplicated_list_error">De naam en emoji van de lijst moeten voor alle lijsten verschillend zijn.</string> + <string name="save_list">Lijst opslaan</string> + <string name="list_menu">Lijst</string> + <string name="error_loading_chat_tags">Fout bij het laden van chat-lijsten</string> + <string name="delete_chat_list_warning">Alle chats worden verwijderd uit de lijst %s, en de lijst wordt verwijderd</string> + <string name="add_to_list">Toevoegen aan lijst</string> + <string name="chat_list_businesses">bedrijven</string> + <string name="chat_list_contacts">Contacten</string> + <string name="error_creating_chat_tags">Fout bij het aanmaken van chat-lijst</string> + <string name="no_unread_chats">Geen ongelezen chats</string> + <string name="chat_list_notes">Notities</string> + <string name="change_list">Lijst wijzigen</string> + <string name="change_order_chat_list_menu_action">Wijzig volgorde</string> + <string name="prefs_error_saving_settings">‐Fout bij het opslaan van instellingen</string> + <string name="error_creating_report">Fout bij het rapporteren</string> + <string name="archive_verb">Archief</string> + <string name="report_reason_community">Schending van de communityrichtlijnen</string> + <string name="report_reason_other">Een andere reden</string> + <string name="report_compose_reason_header_profile">Rapporteer ledenprofiel: alleen groepsmoderators kunnen dit zien.</string> + <string name="group_member_role_moderator">moderator</string> + <string name="report_compose_reason_header_illegal">Inhoud melden: alleen groepsmoderators kunnen dit zien.</string> + <string name="report_item_archived">gearchiveerd rapport</string> + <string name="report_reason_illegal">Ongepaste inhoud</string> + <string name="report_reason_profile">Ongepast profiel</string> + <string name="report_item_visibility_moderators">Alleen de verzender en moderators zien het</string> + <string name="report_item_visibility_submitter">Alleen jij en moderators zien het</string> + <string name="report_reason_spam">Spam</string> + <string name="report_archive_alert_title">Rapport archiveren?</string> + <string name="report_verb">rapporteren</string> + <string name="report_reason_alert_title">Reden melding?</string> + <string name="report_archive_alert_desc">Het rapport wordt voor u gearchiveerd.</string> + <string name="report_compose_reason_header_other">Anders melden: alleen groepsmoderators kunnen het zien.</string> + <string name="report_compose_reason_header_spam">Spam melden: alleen groepsmoderators kunnen het zien.</string> + <string name="report_compose_reason_header_community">Rapporteer overtreding: alleen groepsmoderators kunnen dit zien.</string> + <string name="archive_report">Rapport archiveren</string> + <string name="delete_report">Rapport verwijderen</string> + <string name="report_item_archived_by">gearchiveerd rapport door %s</string> + <string name="group_reports_active_one">1 rapport</string> + <string name="group_reports_active">%d rapporten</string> + <string name="group_reports_member_reports">Ledenrapporten</string> + <string name="blocking_reason_content">Inhoud schendt de gebruiksvoorwaarden</string> + <string name="connection_error_blocked">Verbinding geblokkeerd</string> + <string name="connection_error_blocked_desc">Verbinding is geblokkeerd door serveroperator:\n%1$s.</string> + <string name="file_error_blocked">Bestand is geblokkeerd door server operator:\n%1$s.</string> + <string name="privacy_chat_list_open_links_no">Nee</string> + <string name="privacy_chat_list_open_web_link">Link openen</string> + <string name="privacy_chat_list_open_links">Open links vIn de chat-lijst</string> + <string name="privacy_chat_list_open_links_ask">Vragen</string> + <string name="chat_list_group_reports">Rapporten</string> + <string name="blocking_reason_spam">Spam</string> + <string name="privacy_chat_list_open_web_link_question">Weblink openen?</string> + <string name="privacy_chat_list_open_links_yes">Ja</string> + <string name="text_field_set_chat_placeholder">Stel chatnaam in</string> + <string name="change_automatic_deletion_question">Automatisch verwijderen van berichten wijzigen?</string> + <string name="chat_item_ttl_year">1 jaar</string> + <string name="disable_automatic_deletion_message">Berichten in deze chat zullen nooit worden verwijderd.</string> + <string name="change_automatic_chat_deletion_message">Deze actie kan niet ongedaan worden gemaakt. De berichten die eerder in deze chat zijn verzonden en ontvangen dan geselecteerd, worden verwijderd.</string> + <string name="disable_automatic_deletion_question">Automatisch verwijderen van berichten uitschakelen?</string> + <string name="chat_ttl_options_footer">Verwijder chatberichten van uw apparaat.</string> + <string name="disable_automatic_deletion">Berichten verwijderen uitschakelen</string> + <string name="chat_item_ttl_default">standaard (%s)</string> + <string name="network_smp_web_port_section_title">TCP-poort voor berichtenuitwisseling</string> + <string name="network_smp_web_port_toggle">Gebruik een webpoort</string> + <string name="network_smp_web_port_footer">Gebruik TCP-poort %1$s wanneer er geen poort is opgegeven.</string> + <string name="mute_all_chat">Alles dempen</string> + <string name="unread_mentions">Ongelezen vermeldingen</string> + <string name="max_group_mentions_per_message_reached">Je kunt maximaal %1$s leden per bericht vermelden!</string> + <string name="enable_sending_member_reports">Hiermee kunt u berichten rapporteren aan moderators.</string> + <string name="report_archive_alert_desc_all">Alle rapporten worden voor u gearchiveerd.</string> + <string name="report_archive_alert_title_all">Alle rapporten archiveren?</string> + <string name="report_archive_alert_title_nth">%d rapporten archiveren?</string> + <string name="report_archive_for_me">Voor mij</string> + <string name="group_members_can_send_reports">Leden kunnen berichten melden bij moderators.</string> + <string name="member_reports_are_prohibited">Het melden van berichten in deze groep is niet toegestaan</string> + <string name="notification_group_report">Rapporteer: %s</string> + <string name="disable_sending_member_reports">Het melden van berichten aan moderators is niet toegestaan.</string> + <string name="report_archive_for_all_moderators">Voor alle moderators</string> + <string name="archive_reports">Rapporten archiveren</string> + <string name="v6_3_better_groups_performance">Betere prestaties van groepen</string> + <string name="v6_3_set_message_expiration_in_chats">Stel de berichtvervaldatum in chats in.</string> + <string name="v6_3_better_privacy_and_security">Betere privacy en veiligheid</string> + <string name="v6_3_organize_chat_lists_descr">Mis geen belangrijke berichten.</string> + <string name="v6_3_faster_deletion_of_groups">Sneller verwijderen van groepen.</string> + <string name="v6_3_mentions_descr">Ontvang een melding als u vermeld wordt.</string> + <string name="v6_3_reports_descr">Help beheerders bij het modereren van hun groepen.</string> + <string name="v6_3_mentions">Vermeld leden 👋</string> + <string name="v6_3_organize_chat_lists">Organiseer chats in lijsten</string> + <string name="v6_3_private_media_file_names">Namen van persoonlijke mediabestanden.</string> + <string name="v6_3_reports">Rapporteer privé</string> + <string name="v6_3_faster_sending_messages">Sneller verzenden van berichten.</string> + <string name="group_preview_rejected">afgewezen</string> + <string name="group_member_status_rejected">afgewezen</string> + <string name="error_reading_passphrase">Fout bij het lezen van database wachtwoord</string> + <string name="block_members_desc">Alle nieuwe berichten van deze leden worden verborgen!</string> + <string name="block_members_for_all_question">Leden voor iedereen blokkeren?</string> + <string name="members_will_be_removed_from_chat_cannot_be_undone">Leden worden uit de chat verwijderd. Dit kan niet ongedaan worden gemaakt!</string> + <string name="members_will_be_removed_from_group_cannot_be_undone">Leden worden uit de groep verwijderd. Dit kan niet ongedaan worden gemaakt!</string> + <string name="unblock_members_for_all_question">Leden voor iedereen deblokkeren?</string> + <string name="unblock_members_desc">Berichten van deze leden worden getoond!</string> + <string name="feature_roles_moderators">moderatoren</string> + <string name="restore_passphrase_can_not_be_read_enter_manually_desc">Wachtwoord in Keystore kan niet worden gelezen, voer deze handmatig in. Dit kan zijn gebeurd na een systeemupdate die niet compatibel is met de app. Als dit niet het geval is, neem dan contact op met de ontwikkelaars.</string> + <string name="group_member_status_pending_approval">in afwachting van goedkeuring</string> + <string name="button_remove_members_question">Leden verwijderen?</string> + <string name="operator_updated_conditions">Bijgewerkte voorwaarden</string> + <string name="restore_passphrase_can_not_be_read_desc">Wachtwoord in Keystore kan niet worden gelezen. Dit kan zijn gebeurd na een systeemupdate die niet compatibel is met de app. Als dit niet het geval is, neem dan contact op met de ontwikkelaars.</string> + <string name="group_member_status_pending_approval_short">in behandeling</string> + <string name="onboarding_conditions_accept">Accepteer</string> + <string name="onboarding_conditions_by_using_you_agree">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.</string> + <string name="simplex_link_channel">SimpleX channel link</string> + <string name="link_requires_newer_app_version_please_upgrade">Voor deze link is een nieuwere app-versie vereist. Werk de app bij of vraag je contactpersoon om een compatibele link te sturen.</string> + <string name="full_link_button_text">Volledige link</string> + <string name="unsupported_connection_link">Niet-ondersteunde verbindingslink</string> + <string name="privacy_short_links">Gebruik korte links (BETA)</string> + <string name="short_link_button_text">Korte link</string> + <string name="onboarding_conditions_configure_server_operators">Serveroperators configureren</string> + <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Privacybeleid en gebruiksvoorwaarden.</string> + <string name="onboarding_conditions_private_chats_not_accessible">Privéchats, groepen en uw contacten zijn niet toegankelijk voor serverbeheerders.</string> +</resources> 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 e811e47ea1..5102c98563 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -21,8 +21,8 @@ <string name="thousand_abbreviation">k</string> <string name="live">NA ŻYWO</string> <string name="marked_deleted_description">zaznaczona jako usunięta</string> - <string name="moderated_description">moderowany</string> - <string name="moderated_item_description">moderowany przez %s</string> + <string name="moderated_description">zmoderowane</string> + <string name="moderated_item_description">zmoderowane przez %s</string> <string name="sending_files_not_yet_supported">wysyłanie plików nie jest jeszcze obsługiwane</string> <string name="receiving_files_not_yet_supported">odbieranie plików nie jest jeszcze obsługiwane</string> <string name="trying_to_connect_to_server_to_receive_messages">Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu.</string> @@ -181,7 +181,7 @@ <string name="you_are_observer">jesteś obserwatorem</string> <string name="observer_cant_send_message_title">Nie możesz wysyłać wiadomości!</string> <string name="icon_descr_server_status_connected">Połączony</string> - <string name="maximum_supported_file_size">Obecnie maksymalny obsługiwany rozmiar pliku to %1$s .</string> + <string name="maximum_supported_file_size">Obecnie maksymalny obsługiwany rozmiar pliku to %1$s.</string> <string name="button_delete_contact">Usuń kontakt</string> <string name="delete_contact_question">Usunąć kontakt\?</string> <string name="icon_descr_server_status_disconnected">Rozłączony</string> @@ -229,7 +229,6 @@ <string name="accept_contact_button">Akceptuj</string> <string name="accept_contact_incognito_button">Akceptuj incognito</string> <string name="clear_chat_warning">Wszystkie wiadomości zostaną usunięte - nie można tego cofnąć! Wiadomości zostaną usunięte TYLKO dla Ciebie.</string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Zeskanuj kod QR</b>: aby połączyć się z kontaktem, który pokaże Ci kod QR.]]></string> <string name="icon_descr_cancel_link_preview">anuluj podgląd linku</string> <string name="clear_verb">Wyczyść</string> <string name="clear_chat_menu_action">Wyczyść</string> @@ -278,7 +277,7 @@ <string name="scan_code_from_contacts_app">Zeskanuj kod bezpieczeństwa z aplikacji Twojego kontaktu.</string> <string name="security_code">Kod bezpieczeństwa</string> <string name="icon_descr_settings">Ustawienia</string> - <string name="share_invitation_link">Udostępnij 1-razowy link</string> + <string name="share_invitation_link">Udostępnij link jednorazowy</string> <string name="show_QR_code">Pokaż kod QR</string> <string name="is_verified">%s jest zweryfikowany</string> <string name="this_QR_code_is_not_a_link">Ten kod QR nie jest linkiem!</string> @@ -291,7 +290,7 @@ <string name="you_will_be_connected_when_group_host_device_is_online">Zostaniesz połączony do grupy, gdy urządzenie gospodarza grupy będzie online, proszę czekać lub sprawdzić później!</string> <string name="you_will_be_connected_when_your_contacts_device_is_online">Zostaniesz połączony, gdy urządzenie Twojego kontaktu będzie online, proszę czekać lub sprawdzić później!</string> <string name="smp_servers_preset_add">Dodaj gotowe serwery</string> - <string name="smp_servers_add">Dodaj serwer…</string> + <string name="smp_servers_add">Dodaj serwer</string> <string name="smp_servers_add_to_another_device">Dodaj do innego urządzenia</string> <string name="chat_console">Konsola czatu</string> <string name="smp_servers_check_address">Sprawdź adres serwera i spróbuj ponownie.</string> @@ -304,8 +303,8 @@ <string name="smp_servers_invalid_address">Nieprawidłowy adres serwera!</string> <string name="markdown_help">Pomoc markdown</string> <string name="markdown_in_messages">Markdown w wiadomościach</string> - <string name="smp_servers_preset_server">Wstępnie ustawiony serwer</string> - <string name="smp_servers_preset_address">Wstępnie ustawiony adres serwera</string> + <string name="smp_servers_preset_server">Predefiniowany serwer</string> + <string name="smp_servers_preset_address">Adres predefiniowanego serwera</string> <string name="rate_the_app">Oceń aplikację</string> <string name="saved_ICE_servers_will_be_removed">Zapisane serwery WebRTC ICE zostaną usunięte.</string> <string name="smp_servers_save">Zapisz serwery</string> @@ -336,8 +335,7 @@ <string name="app_version_title">Wersja aplikacji</string> <string name="app_version_name">Wersja aplikacji: v%s</string> <string name="network_session_mode_user_description"><![CDATA[Oddzielne połączenie TCP (i poświadczenia SOCKS) będą używane <b>dla każdego profilu czatu, który masz w aplikacji</b>.]]></string> - <string name="network_session_mode_entity_description">Oddzielne połączenie TCP (i poświadczenia SOCKS) będą używane <b>dla każdego kontaktu i członka grupy</b>. -\n<b>Uwaga</b>: jeśli masz wiele połączeń, zużycie baterii i ruchu może być znacznie wyższe, a niektóre połączenia mogą się nie udać.</string> + <string name="network_session_mode_entity_description"><![CDATA[Oddzielne połączenie TCP (i poświadczenia SOCKS) będą używane <b>dla każdego kontaktu i członka grupy</b>. \n<b>Uwaga</b>: jeśli masz wiele połączeń, zużycie baterii i ruchu może być znacznie wyższe, a niektóre połączenia mogą się nie udać.]]></string> <string name="network_session_mode_user">Profil czatu</string> <string name="network_session_mode_entity">Połączenie</string> <string name="core_version">Wersja rdzenia: v%s</string> @@ -345,18 +343,15 @@ <string name="enter_one_ICE_server_per_line">Serwery ICE (po jednym na linię)</string> <string name="network_disable_socks_info">Jeśli potwierdzisz, serwery wiadomości będą mogły zobaczyć Twój adres IP, a Twój dostawca - z jakimi serwerami się łączysz.</string> <string name="network_and_servers">Sieć i serwery</string> - <string name="network_settings_title">Ustawienia sieci</string> + <string name="network_settings_title">Zaawansowane ustawienia</string> <string name="network_use_onion_hosts_no">Nie</string> <string name="network_use_onion_hosts_required_desc">Hosty onion będą wymagane do połączenia. \nUwaga: nie będziesz mógł połączyć się z serwerami bez adresu .onion.</string> - <string name="network_use_onion_hosts_required_desc_in_alert">Hosty onion będą wymagane do połączenia.</string> <string name="network_use_onion_hosts_prefer_desc">Hosty onion będą używane, gdy będą dostępne.</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">Hosty onion będą używane, gdy będą dostępne.</string> <string name="network_use_onion_hosts_no_desc">Hosty onion nie będą używane.</string> <string name="network_use_onion_hosts_required">Wymagane</string> <string name="save_servers_button">Zapisz</string> <string name="network_session_mode_transport_isolation">Izolacja transportu</string> - <string name="update_onion_hosts_settings_question">Zaktualizować ustawienie hostów .onion\?</string> <string name="update_network_session_mode_question">Zaktualizować tryb izolacji transportu\?</string> <string name="network_disable_socks">Użyć bezpośredniego połączenia z Internetem\?</string> <string name="network_use_onion_hosts">Użyj hostów .onion</string> @@ -412,20 +407,20 @@ <string name="decentralized">Zdecentralizowane</string> <string name="callstate_ended">zakończona</string> <string name="how_to_use_markdown">Jak korzystać z markdown</string> - <string name="immune_to_spam_and_abuse">Odporność na spam i nadużycia</string> + <string name="immune_to_spam_and_abuse">Odporność na spam</string> <string name="italic_text">kursywa</string> <string name="callstatus_missed">nieodebrane połączenie</string> - <string name="opensource_protocol_and_code_anybody_can_run_servers">Otwarto źródłowy protokół i kod - każdy może uruchomić serwery.</string> - <string name="people_can_connect_only_via_links_you_share">Ludzie mogą się z Tobą połączyć tylko poprzez linki, które udostępniasz.</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Każdy może hostować serwery.</string> + <string name="people_can_connect_only_via_links_you_share">Ty decydujesz, kto może się połączyć.</string> <string name="privacy_redefined">Redefinicja prywatności</string> <string name="callstate_received_answer">otrzymano odpowiedź…</string> <string name="callstate_received_confirmation">otrzymano potwierdzenie…</string> <string name="callstatus_rejected">odrzucone połączenie</string> <string name="secret_text">sekret</string> <string name="callstate_starting">uruchamianie…</string> - <string name="strikethrough_text">strajk</string> - <string name="first_platform_without_user_ids">Pierwsza platforma bez żadnych identyfikatorów użytkowników – z założenia prywatna.</string> - <string name="next_generation_of_private_messaging">Następna generacja prywatnych wiadomości</string> + <string name="strikethrough_text">przekreślenie</string> + <string name="first_platform_without_user_ids">Brak identyfikatorów użytkownika.</string> + <string name="next_generation_of_private_messaging">Następna generacja \nprywatnych wiadomości</string> <string name="callstate_waiting_for_answer">oczekiwanie na odpowiedź…</string> <string name="callstate_waiting_for_confirmation">oczekiwanie na potwierdzenie…</string> <string name="you_can_use_markdown_to_format_messages__prompt">Możesz używać markdown do formatowania wiadomości:</string> @@ -435,23 +430,20 @@ <string name="onboarding_notifications_mode_service">Natychmiastowy</string> <string name="onboarding_notifications_mode_subtitle">Można to później zmienić w ustawieniach.</string> <string name="make_private_connection">Nawiąż prywatne połączenie</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[Wiele osób pytało: <i>jeśli SimpleX nie ma identyfikatora użytkownika, jak może dostarczać wiadomości\?</i>]]></string> - <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Tylko urządzenia klienckie przechowują profile użytkowników, kontakty, grupy i wiadomości wysyłane za pomocą <b>2-warstwowego szyfrowania end-to-end</b>.]]></string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Tylko urządzenia klienckie przechowują profile użytkowników, kontakty, grupy i wiadomości wysyłane za pomocą <b>dwuwarstwowego szyfrowania end-to-end</b>.]]></string> <string name="onboarding_notifications_mode_periodic">Okresowo</string> <string name="onboarding_notifications_mode_title">Prywatne powiadomienia</string> <string name="read_more_in_github_with_link"><![CDATA[Przeczytaj więcej na naszym <font color="#0088ff">repozytorium GitHub</font>.]]></string> - <string name="read_more_in_github">Przeczytaj więcej na naszym repozytorium GitHub.</string> <string name="use_chat">Użyj czatu</string> <string name="onboarding_notifications_mode_off">Gdy aplikacja jest uruchomiona</string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Kontrolujesz przez który serwer(y) <b>odbierać</b> wiadomości, Twoje kontakty - serwery, których używasz do wysyłania im wiadomości.]]></string> - <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Zużywa więcej baterii</b>! Usługa zawsze działa w tle - powiadomienia są wyświetlane, gdy tylko wiadomości są dostępne.]]></string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Zużywa więcej baterii</b>! Aplikacja zawsze działa w tle - powiadomienia są wyświetlane natychmiastowo.]]></string> <string name="incoming_audio_call">Przychodzące połączenie audio</string> <string name="incoming_video_call">Przychodzące połączenie wideo</string> - <string name="paste_the_link_you_received">Wklej otrzymany link</string> + <string name="paste_the_link_you_received">Wklej link, który otrzymałeś</string> <string name="video_call_no_encryption">połączenie wideo (bez szyfrowania e2e)</string> <string name="accept">Akceptuj</string> - <string name="encrypted_audio_call">zaszyfrowane e2e połączenie audio</string> - <string name="encrypted_video_call">zaszyfrowane e2e połączenie wideo</string> + <string name="encrypted_audio_call">połączenie audio zaszyfrowane e2e</string> + <string name="encrypted_video_call">połączenie wideo zaszyfrowane e2e</string> <string name="ignore">Ignoruj</string> <string name="reject">Odrzuć</string> <string name="always_use_relay">Zawsze używaj przekaźnika</string> @@ -478,7 +470,7 @@ <string name="status_contact_has_e2e_encryption">kontakt posiada szyfrowanie e2e</string> <string name="status_contact_has_no_e2e_encryption">kontakt nie posiada szyfrowania e2e</string> <string name="integrity_msg_duplicate">zduplikowana wiadomość</string> - <string name="status_e2e_encrypted">zaszyfrowany e2e</string> + <string name="status_e2e_encrypted">zaszyfrowane e2e</string> <string name="allow_accepting_calls_from_lock_screen">Włącz połączenia z ekranu blokady poprzez Ustawienia.</string> <string name="icon_descr_flip_camera">Odwróć aparat</string> <string name="icon_descr_hang_up">Rozłącz się</string> @@ -495,7 +487,7 @@ <string name="call_connection_via_relay">przez przekaźnik</string> <string name="icon_descr_video_off">Wideo wyłączone</string> <string name="icon_descr_video_on">Wideo włączone</string> - <string name="integrity_msg_skipped">%1$d wiadomość(i) pominięta(e)</string> + <string name="integrity_msg_skipped">%1$d pominięta(ych) wiadomość(i)</string> <string name="alert_text_skipped_messages_it_can_happen_when">Może to nastąpić, gdy: \n1. Wiadomości wygasły w wysyłającym kliencie po 2 dniach lub na serwerze po 30 dniach. \n2. Odszyfrowanie wiadomości nie powiodło się, ponieważ Ty lub Twój kontakt użyliście starej kopii zapasowej bazy danych. @@ -570,7 +562,6 @@ <string name="restart_the_app_to_create_a_new_chat_profile">Uruchom ponownie aplikację, aby utworzyć nowy profil czatu.</string> <string name="save_passphrase_in_keychain">Zapisz hasło w Keystore</string> <string name="chat_item_ttl_seconds">%s sekund(y)</string> - <string name="stop_chat_to_enable_database_actions">Zatrzymaj czat, aby umożliwić działania na bazie danych.</string> <string name="enable_automatic_deletion_message">Tego działania nie można cofnąć - wiadomości wysłane i odebrane wcześniej niż wybrane zostaną usunięte. Może to potrwać kilka minut.</string> <string name="delete_chat_profile_action_cannot_be_undone_warning">Tego działania nie można cofnąć - Twój profil, kontakty, wiadomości i pliki zostaną nieodwracalnie utracone.</string> <string name="messages_section_description">To ustawienie dotyczy wiadomości Twojego bieżącego profilu czatu</string> @@ -604,14 +595,11 @@ <string name="wrong_passphrase">Nieprawidłowe hasło bazy danych</string> <string name="wrong_passphrase_title">Nieprawidłowe hasło!</string> <string name="you_have_to_enter_passphrase_every_time">Musisz wprowadzić hasło przy każdym uruchomieniu aplikacji - nie jest one przechowywane na urządzeniu.</string> - <string name="chat_archive_header">Archiwum czatu</string> - <string name="chat_archive_section">ARCHIWUM CZATU</string> <string name="chat_is_stopped_indication">Czat jest zatrzymany</string> <string name="confirm_database_upgrades">Potwierdź aktualizacje bazy danych</string> <string name="database_downgrade">Obniż wersję bazy danych</string> <string name="database_upgrade">Aktualizacja bazy danych</string> <string name="mtr_error_no_down_migration">wersja bazy danych jest nowsza od aplikacji, ale nie ma migracji w dół dla: %s</string> - <string name="delete_archive">Usuń archiwum</string> <string name="mtr_error_different">różne migracje w aplikacji/bazy danych: %s / %s</string> <string name="downgrade_and_open_chat">Obniż wersję i otwórz czat</string> <string name="icon_descr_group_inactive">Grupa nieaktywna</string> @@ -633,7 +621,6 @@ <string name="restore_database">Przywróć kopię zapasową bazy danych</string> <string name="restore_database_alert_title">Przywrócić kopię zapasową bazy danych\?</string> <string name="database_restore_error">Błąd przywracania bazy danych</string> - <string name="save_archive">Zapisz archiwum</string> <string name="database_backup_can_be_restored">Próba zmiany hasła bazy danych nie została zakończona.</string> <string name="alert_message_no_group">Ta grupa już nie istnieje.</string> <string name="upgrade_and_open_chat">Zaktualizuj i otwórz czat</string> @@ -742,9 +729,8 @@ <string name="switch_verb">Przełącz</string> <string name="switch_receiving_address">Zmień adres odbioru</string> <string name="group_is_decentralized">W pełni zdecentralizowana – widoczna tylko dla członków.</string> - <string name="member_role_will_be_changed_with_invitation">Rola zostanie zmieniona na \"%s\". Członek otrzyma nowe zaproszenie.</string> + <string name="member_role_will_be_changed_with_invitation">Rola zostanie zmieniona na "%s". Członek otrzyma nowe zaproszenie.</string> <string name="group_welcome_title">Wiadomość powitalna</string> - <string name="cant_delete_user_profile">Nie można usunąć profilu użytkownika!</string> <string name="users_delete_question">Usunąć profil czatu\?</string> <string name="users_delete_profile_for">Usuń profil czatu dla</string> <string name="dont_show_again">Nie pokazuj ponownie</string> @@ -760,7 +746,6 @@ <string name="users_delete_with_connections">Profil i połączenia z serwerem</string> <string name="network_option_protocol_timeout">Limit czasu protokołu</string> <string name="network_options_reset_to_defaults">Przywróć wartości domyślne</string> - <string name="network_options_revert">Przywrócić</string> <string name="network_options_save">Zapisz</string> <string name="network_option_seconds_label">sek</string> <string name="tap_to_activate_profile">Dotknij, aby aktywować profil.</string> @@ -801,7 +786,6 @@ <string name="prohibit_sending_disappearing_messages">Zabroń wysyłania znikających wiadomości.</string> <string name="feature_received_prohibited">otrzymane, zabronione</string> <string name="reset_color">Resetuj kolory</string> - <string name="save_color">Zapisz kolor</string> <string name="accept_feature_set_1_day">Ustaw 1 dzień</string> <string name="theme_system">System</string> <string name="language_system">System</string> @@ -813,15 +797,15 @@ <string name="your_preferences">Twoje preferencje</string> <string name="incognito_random_profile">Twój losowy profil</string> <string name="allow_direct_messages">Zezwalaj na wysyłanie bezpośrednich wiadomości do członków.</string> - <string name="allow_to_delete_messages">Zezwól na nieodwracalne usunięcie wysłanych wiadomości.</string> + <string name="allow_to_delete_messages">Zezwól na nieodwracalne usunięcie wysłanych wiadomości. (24 godziny)</string> <string name="allow_to_send_voice">Zezwól na wysyłanie wiadomości głosowych.</string> <string name="allow_voice_messages_only_if">Zezwalaj na wiadomości głosowe tylko wtedy, gdy Twój kontakt na nie pozwala.</string> <string name="allow_your_contacts_to_send_voice_messages">Zezwól swoim kontaktom na wysyłanie wiadomości głosowych.</string> - <string name="both_you_and_your_contacts_can_delete">Zarówno Ty, jak i Twój kontakt możecie nieodwracalnie usunąć wysłane wiadomości.</string> + <string name="both_you_and_your_contacts_can_delete">Zarówno Ty, jak i Twój kontakt możecie nieodwracalnie usunąć wysłane wiadomości. (24 godziny)</string> <string name="both_you_and_your_contact_can_send_disappearing">Zarówno Ty, jak i Twój kontakt możecie wysyłać znikające wiadomości.</string> <string name="contacts_can_mark_messages_for_deletion">Kontakty mogą oznaczać wiadomości do usunięcia; będziesz mógł je zobaczyć.</string> <string name="delete_after">Usuń po</string> - <string name="direct_messages_are_prohibited_in_chat">Bezpośrednie wiadomości między członkami są zabronione w tej grupie.</string> + <string name="direct_messages_are_prohibited_in_group">Bezpośrednie wiadomości między członkami są zabronione w tej grupie.</string> <string name="disappearing_prohibited_in_this_chat">Znikające wiadomości są zabronione na tym czacie.</string> <string name="ttl_m">%dm</string> <string name="ttl_min">%d min</string> @@ -830,11 +814,11 @@ <string name="ttl_mth">%d mies</string> <string name="ttl_s">%ds</string> <string name="ttl_sec">%d sek</string> - <string name="group_members_can_delete">Członkowie grupy mogą nieodwracalnie usuwać wysłane wiadomości.</string> + <string name="group_members_can_delete">Członkowie grupy mogą nieodwracalnie usuwać wysłane wiadomości. (24 godziny)</string> <string name="group_members_can_send_dms">Członkowie grupy mogą wysyłać bezpośrednie wiadomości.</string> <string name="message_deletion_prohibited">Nieodwracalne usuwanie wiadomości jest na tym czacie zabronione.</string> <string name="message_deletion_prohibited_in_chat">Nieodwracalne usuwanie wiadomości jest w tej grupie zabronione.</string> - <string name="only_you_can_delete_messages">Tylko Ty możesz nieodwracalnie usunąć wiadomości (Twój kontakt może oznaczyć je do usunięcia).</string> + <string name="only_you_can_delete_messages">Tylko Ty możesz nieodwracalnie usunąć wiadomości (Twój kontakt może oznaczyć je do usunięcia). (24 godziny)</string> <string name="only_you_can_send_disappearing">Tylko Ty możesz wysyłać znikające wiadomości.</string> <string name="only_you_can_send_voice">Tylko Ty możesz wysyłać wiadomości głosowe.</string> <string name="only_your_contact_can_send_disappearing">Tylko Twój kontakt może wysyłać znikające wiadomości.</string> @@ -895,7 +879,7 @@ <string name="v4_6_audio_video_calls_descr">Obsługa bluetooth i inne ulepszenia.</string> <string name="v4_5_italian_interface_descr">Podziękowania dla użytkowników - wkład za pośrednictwem Weblate!</string> <string name="v4_6_chinese_spanish_interface_descr">Podziękowania dla użytkowników - wkład za pośrednictwem Weblate!</string> - <string name="allow_your_contacts_irreversibly_delete">Zezwól swoim kontaktom na nieodwracalne usuwanie wysłanych wiadomości.</string> + <string name="allow_your_contacts_irreversibly_delete">Zezwól swoim kontaktom na nieodwracalne usuwanie wysłanych wiadomości. (24 godziny)</string> <string name="about_simplex_chat">O SimpleX Chat</string> <string name="above_then_preposition_continuation">powyżej, wtedy:</string> <string name="accept_connection_request__question">Zaakceptować prośbę o połączenie\?</string> @@ -906,10 +890,10 @@ <string name="v4_3_improved_server_configuration_desc">Dodaj serwery, skanując kody QR.</string> <string name="button_add_welcome_message">Dodaj wiadomość powitalną</string> <string name="users_delete_all_chats_deleted">Wszystkie czaty i wiadomości zostaną usunięte - nie można tego cofnąć!</string> - <string name="allow_verb">Pozwól</string> + <string name="allow_verb">Zezwól</string> <string name="chat_preferences_always">zawsze</string> <string name="notifications_mode_service">Zawsze włączona</string> - <string name="allow_irreversible_message_deletion_only_if">Zezwalaj na nieodwracalne usuwanie wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli.</string> + <string name="allow_irreversible_message_deletion_only_if">Zezwalaj na nieodwracalne usuwanie wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli. (24 godziny)</string> <string name="allow_to_send_disappearing">Zezwól na wysyłanie znikających wiadomości.</string> <string name="allow_voice_messages_question">Zezwolić na wiadomości głosowe\?</string> <string name="notifications_mode_off_desc">Aplikacja może otrzymywać powiadomienia tylko wtedy, gdy jest uruchomiona, żadna usługa w tle nie zostanie uruchomiona</string> @@ -917,11 +901,10 @@ <string name="auth_unavailable">Uwierzytelnianie niedostępne</string> <string name="attach">Dołącz</string> <string name="back">Wstecz</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Dodaj nowy kontakt</b>: aby stworzyć swój jednorazowy kod QR dla kontaktu.]]></string> <string name="turning_off_service_and_periodic">Optymalizacja baterii jest aktywna, wyłącza usługi w tle i okresowe żądania nowych wiadomości. Możesz je ponownie włączyć za pośrednictwem ustawień.</string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Można je wyłączyć poprzez ustawienia</b> - powiadomienia nadal będą pokazywane podczas działania aplikacji.]]></string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Najlepsze dla baterii</b>. Będziesz otrzymywać powiadomienia tylko wtedy, gdy aplikacja jest uruchomiona (NIE w tle).]]></string> - <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Dobry dla baterii</b>. Usługa w tle sprawdza wiadomości co 10 minut. Możesz przegapić połączenia lub pilne wiadomości.]]></string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Dobry dla baterii</b>. Aplikacja sprawdza wiadomości co 10 minut. Możesz przegapić połączenia lub pilne wiadomości.]]></string> <string name="both_you_and_your_contact_can_send_voice">Zarówno Ty, jak i Twój kontakt możecie wysyłać wiadomości głosowe.</string> <string name="v4_5_transport_isolation_descr">Według profilu czatu (domyślnie) lub połączenia (BETA).</string> <string name="alert_title_cant_invite_contacts">Nie można zaprosić kontaktów!</string> @@ -932,14 +915,11 @@ <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Kontakt i wszystkie wiadomości zostaną usunięte - nie można tego cofnąć!</string> <string name="connection_error_auth">Błąd połączenia (UWIERZYTELNIANIE)</string> <string name="connect_via_link_or_qr">Połącz się przez link / kod QR</string> - <string name="archive_created_on_ts">Utworzony na %1$s</string> - <string name="create_one_time_link">Utwórz jednorazowy link do zaproszenia</string> <string name="create_group">Utwórz tajną grupę</string> <string name="create_secret_group_title">Utwórz tajną grupę</string> <string name="set_password_to_export_desc">Baza danych jest zaszyfrowana przy użyciu losowego hasła. Proszę zmienić je przed eksportem.</string> <string name="ttl_days">%d dni</string> <string name="delete_group_menu_action">Usuń</string> - <string name="delete_chat_archive_question">Usunąć archiwum czatu\?</string> <string name="delete_messages_after">Usuń wiadomości po</string> <string name="disappearing_messages_are_prohibited">Znikające wiadomości są zabronione w tej grupie.</string> <string name="error_deleting_contact_request">Błąd usuwania prośby o kontakt</string> @@ -952,14 +932,13 @@ <string name="failed_to_parse_chat_title">Nie udało się załadować czatu</string> <string name="v4_6_group_moderation_descr">Teraz administratorzy mogą: \n- usuwać wiadomości członków. -\n- wyłączyć członków (rola \"obserwatora\")</string> +\n- wyłączyć członków (rola "obserwatora")</string> <string name="from_gallery_button">Z Galerii</string> <string name="gallery_image_button">Obraz</string> <string name="gallery_video_button">Wideo</string> <string name="feature_offered_item_with_param">zaproponował %s: %2s</string> <string name="only_group_owners_can_enable_voice">Tylko właściciele grup mogą włączyć wiadomości głosowe.</string> - <string name="network_use_onion_hosts_no_desc_in_alert">Hosty onion nie będą używane.</string> - <string name="only_your_contact_can_delete">Tylko Twój kontakt może nieodwracalnie usunąć wiadomości (możesz oznaczyć je do usunięcia).</string> + <string name="only_your_contact_can_delete">Tylko Twój kontakt może nieodwracalnie usunąć wiadomości (możesz oznaczyć je do usunięcia). (24 godziny)</string> <string name="restore_passphrase_not_found_desc">Hasło nie zostało znalezione w Keystore, wprowadź je ręcznie. Może się tak zdarzyć, gdy przywrócisz dane aplikacji za pomocą narzędzia do kopii zapasowych. Jeśli tak nie jest, skontaktuj się z programistami.</string> <string name="group_members_can_send_disappearing">Członkowie grupy mogą wysyłać znikające wiadomości.</string> <string name="group_members_can_send_voice">Członkowie grupy mogą wysyłać wiadomości głosowe.</string> @@ -1000,11 +979,9 @@ <string name="simplex_link_contact">Adres kontaktowy SimpleX</string> <string name="stop_chat_to_export_import_or_delete_chat_database">Zatrzymaj czat, aby wyeksportować, zaimportować lub usunąć bazę danych czatu. Podczas zatrzymania chatu nie będzie można odbierać ani wysyłać wiadomości.</string> <string name="smp_servers_test_some_failed">Niektóre serwery nie przeszły testu:</string> - <string name="should_be_at_least_one_profile">Powinien istnieć co najmniej jeden profil użytkownika.</string> <string name="thank_you_for_installing_simplex">Dziękujemy za zainstalowanie SimpleX Chat!</string> - <string name="should_be_at_least_one_visible_profile">Powinien istnieć co najmniej jeden widoczny profil użytkownika.</string> - <string name="moderate_message_will_be_marked_warning">Wiadomość zostanie oznaczona jako moderowana dla wszystkich członków.</string> - <string name="member_role_will_be_changed_with_notification">Rola zostanie zmieniona na \"%s\". Wszyscy w grupie zostaną powiadomieni.</string> + <string name="moderate_message_will_be_marked_warning">Wiadomość zostanie oznaczona jako zmoderowana dla wszystkich członków.</string> + <string name="member_role_will_be_changed_with_notification">Rola zostanie zmieniona na "%s". Wszyscy w grupie zostaną powiadomieni.</string> <string name="delete_files_and_media_desc">Tego działania nie można cofnąć - wszystkie odebrane i wysłane pliki oraz media zostaną usunięte. Obrazy o niskiej rozdzielczości pozostaną.</string> <string name="switch_receiving_address_desc">Adres odbiorczy zostanie zmieniony na inny serwer. Zmiana adresu zostanie zakończona gdy nadawca będzie online.</string> <string name="this_link_is_not_a_valid_connection_link">Ten link nie jest prawidłowym linkiem połączenia!</string> @@ -1152,15 +1129,15 @@ <string name="group_welcome_preview">Podgląd</string> <string name="opening_database">Otwieranie bazy danych…</string> <string name="error_setting_address">Błąd ustawiania adresu</string> - <string name="auth_open_chat_profiles">Otwórz profile czatu</string> + <string name="auth_open_chat_profiles">Zmień profile czatu</string> <string name="learn_more_about_address">O adresie SimpleX</string> - <string name="one_time_link_short">1-razowy link</string> + <string name="one_time_link_short">link jednorazowy</string> <string name="read_more_in_user_guide_with_link"><![CDATA[Przeczytaj więcej w <font color="#0088ff">Podręczniku Użytkownika</font>.]]></string> <string name="simplex_address">Adres SimpleX</string> <string name="you_can_accept_or_reject_connection">Kiedy ludzie proszą o połączenie, możesz je zaakceptować lub odrzucić.</string> <string name="you_wont_lose_your_contacts_if_delete_address">Nie stracisz kontaktów, jeśli później usuniesz swój adres.</string> <string name="customize_theme_title">Dostosuj motyw</string> - <string name="theme_colors_section_title">KOLORY MOTYWU</string> + <string name="theme_colors_section_title">KOLORY INTERFEJSU</string> <string name="your_contacts_will_remain_connected">Twoje kontakty pozostaną połączone.</string> <string name="add_address_to_your_profile">Dodaj adres do swojego profilu, aby Twoje kontakty mogły go udostępnić innym osobom. Aktualizacja profilu zostanie wysłana do Twoich kontaktów.</string> <string name="create_address_and_let_people_connect">Utwórz adres, aby ludzie mogli się z Tobą połączyć.</string> @@ -1221,7 +1198,7 @@ <string name="received_message">Otrzymano wiadomość</string> <string name="sent_message">Wyślij wiadomość</string> <string name="info_row_deleted_at">Usunięto o</string> - <string name="info_row_moderated_at">Moderowany o</string> + <string name="info_row_moderated_at">Zmoderowane o</string> <string name="share_text_received_at">Otrzymane o: %s</string> <string name="info_row_updated_at">Rekord zaktualizowany o</string> <string name="info_row_sent_at">Wysłano o</string> @@ -1232,7 +1209,7 @@ <string name="info_row_disappears_at">Znika o</string> <string name="share_text_disappears_at">Znika o: %s</string> <string name="info_menu">Info</string> - <string name="share_text_moderated_at">Moderowany o: %s</string> + <string name="share_text_moderated_at">Zmoderowane o: %s</string> <string name="edit_history">Historia</string> <string name="info_row_received_at">Otrzymane o</string> <string name="current_version_timestamp">%s (aktualny)</string> @@ -1251,7 +1228,7 @@ \n- niestandardowy czas zniknięcia. \n- historia edycji.</string> <string name="item_info_no_text">brak tekstu</string> - <string name="non_fatal_errors_occured_during_import">Podczas importu wystąpiły niekrytyczne błędy - więcej szczegółów można znaleźć w konsoli czatu.</string> + <string name="non_fatal_errors_occured_during_import">Podczas importu wystąpiły niekrytyczne błędy:</string> <string name="settings_restart_app">Restart</string> <string name="settings_section_title_app">APLIKACJA</string> <string name="shutdown_alert_desc">Powiadomienia przestaną działać do momentu ponownego uruchomienia aplikacji.</string> @@ -1355,7 +1332,7 @@ <string name="connect_via_member_address_alert_desc">Prośba o połączenie zostanie wysłana do tego członka grupy.</string> <string name="connect_via_member_address_alert_title">Połączyć bezpośrednio\?</string> <string name="connect_use_current_profile">Użyj obecnego profilu</string> - <string name="turn_off_battery_optimization_button">Pozwól</string> + <string name="turn_off_battery_optimization_button">Zezwól</string> <string name="disable_notifications_button">Wyłącz powiadomienia</string> <string name="turn_off_system_restriction_button">Otwórz ustawienia aplikacji</string> <string name="system_restricted_background_in_call_title">Brak rozmów w tle</string> @@ -1396,9 +1373,7 @@ <string name="v5_3_new_interface_languages_descr">Arabski, bułgarski, fiński, hebrajski, tajski i ukraiński - dzięki użytkownikom i Weblate.</string> <string name="v5_3_new_desktop_app_descr">Utwórz nowy profil w aplikacji desktopowej. 💻</string> <string name="v5_3_simpler_incognito_mode_descr">Przełącz incognito przy połączeniu.</string> - <string name="v5_3_discover_join_groups_descr">- połącz się z usługą katalogową (BETA)! -\n- potwierdzenia dostaw (do 20 członków). -\n- szybszy i stabilniejszy.</string> + <string name="v5_3_discover_join_groups_descr">- połącz się z usługą katalogową (BETA)!\n- potwierdzenia dostaw (do 20 członków).\n- szybszy i stabilniejszy.</string> <string name="rcv_group_event_open_chat">Otwórz</string> <string name="error_creating_member_contact">Błąd tworzenia kontaktu członka</string> <string name="compose_send_direct_message_to_connect">Wyślij wiadomość bezpośrednią aby połączyć</string> @@ -1412,7 +1387,7 @@ <string name="rcv_group_and_other_events">i %d innych wydarzeń</string> <string name="connect_plan_connect_via_link">Połączyć przez link?</string> <string name="connect_plan_already_joining_the_group">Już dołączono do grupy!</string> - <string name="moderated_items_description">%d wiadomości zmoderowanych przez %s</string> + <string name="moderated_items_description">%1$d wiadomości zmoderowanych przez %2$s</string> <string name="connect_plan_connect_to_yourself">Połączyć się ze sobą?</string> <string name="correct_name_to">Poprawić nazwę do %s?</string> <string name="delete_messages__question">Usunąć %d wiadomości?</string> @@ -1427,12 +1402,12 @@ <string name="blocked_item_description">zablokowany</string> <string name="expand_verb">Rozszerz</string> <string name="connect_plan_repeat_connection_request">Powtórzyć prośbę połączenia?</string> - <string name="connect_plan_you_are_already_connecting_to_vName">Już jesteś połączony z %1$s.</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Już jesteś połączony z <b>%1$s</b>.]]></string> <string name="error_alert_title">Błąd</string> <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Już dołączasz do grupy przez ten link.</string> <string name="group_members_2">%s i %s</string> <string name="connect_plan_join_your_group">Dołączyć do twojej grupy?</string> - <string name="connect_plan_you_are_already_joining_the_group_vName">Już dołączasz do grupy %1$s.</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Już dołączasz do grupy <b>%1$s</b>.]]></string> <string name="connect_plan_this_is_your_own_one_time_link">To jest twój jednorazowy link!</string> <string name="connect_plan_group_already_exists">Grupa już istnieje!</string> <string name="video_decoding_exception_desc">Wideo nie może zostać zdekodowane, spróbuj inne wideo lub skontaktuj się z deweloperami.</string> @@ -1440,7 +1415,7 @@ <string name="group_members_n">%s, %s i %d członków</string> <string name="unblock_member_button">Odblokuj członka</string> <string name="contact_tap_to_connect">Dotknij aby połączyć</string> - <string name="connect_plan_you_are_already_in_group_vName">Już jesteś w grupie %1$s.</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Już jesteś w grupie <b>%1$s</b>.]]></string> <string name="connect_plan_this_is_your_own_simplex_address">To jest twój własny adres SimpleX!</string> <string name="remove_member_button">Usuń członka</string> <string name="unblock_member_question">Odblokować członka?</string> @@ -1452,7 +1427,7 @@ <string name="error_sending_message_contact_invitation">Błąd wysyłania zaproszenia</string> <string name="non_content_uri_alert_text">Udostępniłeś nieprawidłową ścieżkę pliku. Zgłoś problem do deweloperów aplikacji.</string> <string name="invalid_name">Nieprawidłowa nazwa!</string> - <string name="connect_plan_this_is_your_link_for_group_vName">To jest twój link zaproszenia do grupy %1$s!</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[To jest twój link zaproszenia do grupy <b>%1$s</b>!]]></string> <string name="unblock_member_confirmation">Odblokuj</string> <string name="non_content_uri_alert_title">Nieprawidłowa ścieżka pliku</string> <string name="connect_plan_you_have_already_requested_connection_via_this_address">Już prosiłeś o połączenie na ten adres!</string> @@ -1522,4 +1497,691 @@ <string name="disconnect_desktop_question">Rozłączyć komputer?</string> <string name="loading_remote_file_desc">Proszę poczekać na załadowanie pliku z połączonego telefonu</string> <string name="verify_connection">Zweryfikuj połączenie</string> -</resources> \ No newline at end of file + <string name="refresh_qr_code">Odśwież</string> + <string name="you_can_make_address_visible_via_settings">Możesz ustawić go jako widoczny dla swoich kontaktów SimpleX w Ustawieniach.</string> + <string name="random_port">Losowy</string> + <string name="error_showing_content">błąd wyświetlania zawartości</string> + <string name="error_showing_message">błąd wyświetlania wiadomości</string> + <string name="open_port_in_firewall_desc">Aby umożliwić aplikacji mobilnej połączenie z komputerem stacjonarnym, otwórz ten port w swojej zaporze sieciowej, jeśli jest włączona.</string> + <string name="create_chat_profile">Utwórz profil czatu</string> + <string name="app_was_crashed">Widok uległ awarii</string> + <string name="open_port_in_firewall_title">Otwórz port w zaporze sieciowej</string> + <string name="disconnect_remote_hosts">Rozłącz telefony</string> + <string name="no_connected_mobile">Brak połączonych telefonów</string> + <string name="recent_history_is_not_sent_to_new_members">Historia nie jest wysyłana do nowych członków.</string> + <string name="retry_verb">Ponów</string> + <string name="camera_not_available">Kamera nie dostępna</string> + <string name="enable_sending_recent_history">Wysyłaj do 100 ostatnich wiadomości do nowych członków.</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Dodaj kontakt</b>: aby utworzyć nowy link z zaproszeniem lub połączyć się za pomocą otrzymanego linku.]]></string> + <string name="disable_sending_recent_history">Nie wysyłaj historii do nowych członków.</string> + <string name="or_show_this_qr_code">Lub pokaż ten kod</string> + <string name="recent_history_is_sent_to_new_members">Do nowych członków wysyłanych jest do 100 ostatnich wiadomości</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">Kod, który zeskanowałeś nie jest kodem QR linku SimpleX.</string> + <string name="the_text_you_pasted_is_not_a_link">Tekst, który wkleiłeś nie jest linkiem SimpleX.</string> + <string name="enable_camera_access">Włącz dostęp do kamery</string> + <string name="you_can_view_invitation_link_again">Możesz zobaczyć link zaproszenia ponownie w szczegółach połączenia.</string> + <string name="keep_unused_invitation_question">Zachować nieużyte zaproszenie?</string> + <string name="share_this_1_time_link">Udostępnij ten jednorazowy link zaproszenia</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Utwórz grupę</b>: aby utworzyć nową grupę.]]></string> + <string name="recent_history">Widoczna historia</string> + <string name="la_app_passcode">Pin aplikacji</string> + <string name="new_chat">Nowy czat</string> + <string name="loading_chats">Ładowanie czatów…</string> + <string name="creating_link">Tworzenie linku…</string> + <string name="or_scan_qr_code">Lub zeskanuj kod QR</string> + <string name="invalid_qr_code">Nieprawidłowy kod QR</string> + <string name="add_contact_tab">Dodaj kontakt</string> + <string name="tap_to_scan">Dotknij, aby zeskanować</string> + <string name="keep_invitation_link">Zachowaj</string> + <string name="tap_to_paste_link">Dotknij, aby wkleić link</string> + <string name="search_or_paste_simplex_link">Wyszukaj lub wklej link SimpleX</string> + <string name="v5_5_private_notes">Prywatne notatki</string> + <string name="v5_5_message_delivery">Ulepszona dostawa wiadomości</string> + <string name="v5_5_new_interface_languages">Węgierski i turecki interfejs</string> + <string name="v5_5_message_delivery_descr">Ze zmniejszonym zużyciem baterii.</string> + <string name="remote_ctrl_error_bad_state">Połączenie z komputerem jest w złym stanie</string> + <string name="remote_ctrl_error_bad_invitation">Komputer ma zły kod zaproszenia</string> + <string name="remote_ctrl_error_busy">Komputer jest zajęty</string> + <string name="remote_ctrl_error_disconnected">Komputer został odłączony</string> + <string name="error_deleting_note_folder">Błąd usuwania prywatnych notatek</string> + <string name="possible_slow_function_title">Wolne funkcje</string> + <string name="clear_note_folder_question">Wyczyścić prywatne notatki?</string> + <string name="developer_options_section">Opcje deweloperskie</string> + <string name="show_slow_api_calls">Pokaż wolne zapytania API</string> + <string name="rcv_group_event_member_blocked">zablokowany %s</string> + <string name="start_chat_question">Uruchomić czat?</string> + <string name="rcv_group_event_member_unblocked">odblokuj %s</string> + <string name="snd_group_event_member_blocked">zablokowałeś %s</string> + <string name="snd_group_event_member_unblocked">odblokowałeś %s</string> + <string name="profile_update_event_member_name_changed">członek %1$s zmienił na %2$s</string> + <string name="profile_update_event_set_new_address">ustaw nowy adres kontaktu</string> + <string name="profile_update_event_updated_profile">zaktualizowano profil</string> + <string name="past_member_vName">Były członek %1$s</string> + <string name="block_for_all_question">Zablokować członka dla wszystkich?</string> + <string name="info_row_created_at">Utworzony o</string> + <string name="saved_message_title">Zachowano wiadomość</string> + <string name="unblock_for_all">Odblokuj dla wszystkich</string> + <string name="unblock_for_all_question">Odblokować członka dla wszystkich?</string> + <string name="error_blocking_member_for_all">Błąd blokowania członka dla wszystkich</string> + <string name="v5_5_simpler_connect_ui">Wklej link, aby połączyć!</string> + <string name="v5_5_private_notes_descr">Z zaszyfrowanymi plikami i multimediami.</string> + <string name="v5_5_simpler_connect_ui_descr">Pasek wyszukiwania akceptuje linki zaproszenia.</string> + <string name="v5_5_join_group_conversation_descr">Ostatnia historia i ulepszony bot adresowy.</string> + <string name="agent_critical_error_title">Krytyczny błąd</string> + <string name="agent_critical_error_desc">Proszę zgłosić to do deweloperów: +\n%s +\n +\nZalecane jest zrestartowanie aplikacji.</string> + <string name="agent_internal_error_desc">Proszę zgłosić to do deweloperów: +\n%s</string> + <string name="restart_chat_button">Zrestartuj czat</string> + <string name="clear_note_folder_warning">Wszystkie wiadomości zostaną usunięte – nie można tego cofnąć!</string> + <string name="member_info_member_blocked">zablokowany</string> + <string name="blocked_by_admin_item_description">zablokowany przez administratora</string> + <string name="member_blocked_by_admin">Zablokowany przez admina</string> + <string name="block_for_all">Zablokuj dla wszystkich</string> + <string name="chat_is_stopped_you_should_transfer_database">Czat został zatrzymany. Jeśli korzystałeś już z tej bazy danych na innym urządzeniu, powinieneś przenieść ją z powrotem przed rozpoczęciem czatu.</string> + <string name="agent_internal_error_title">Wewnętrzny błąd</string> + <string name="v5_5_join_group_conversation">Dołącz do grupowej rozmowy</string> + <string name="share_text_created_at">Utworzony o: %s</string> + <string name="remote_ctrl_error_bad_version">Komputer ma niewspieraną wersję. Proszę upewnić się, że używasz tych samych wersji na obu urządzeniach</string> + <string name="blocked_by_admin_items_description">%d wiadomości zablokowanych przez admina</string> + <string name="error_creating_message">Błąd tworzenia wiadomości</string> + <string name="possible_slow_function_desc">Wykonanie kodu zajmuje za dużo czasu: %1$d sekund: %2$s</string> + <string name="note_folder_local_display_name">Prywatne notatki</string> + <string name="group_member_status_unknown">nieznany status</string> + <string name="show_internal_errors">Pokaż błędy wewnętrzne</string> + <string name="remote_ctrl_error_timeout">Osiągnięto limit czasu podczas łączenia z komputerem</string> + <string name="failed_to_create_user_invalid_title">Nieprawidłowa nazwa wyświetlana!</string> + <string name="failed_to_create_user_invalid_desc">Nazwa wyświetlana jest nieprawidłowa. Proszę wybrać inną nazwę.</string> + <string name="remote_host_was_disconnected_title">Połączenie zatrzymane</string> + <string name="remote_ctrl_was_disconnected_title">Połączenie zatrzymane</string> + <string name="remote_host_disconnected_from"><![CDATA[Odłączono od telefonu <b>%s</b> z powodu: %s]]></string> + <string name="remote_host_error_missing"><![CDATA[Brakuje telefonu <b>%s</b>]]></string> + <string name="remote_host_error_inactive"><![CDATA[Telefon <b>%s</b> jest nieaktywny]]></string> + <string name="remote_host_error_bad_version"><![CDATA[Telefon <b>%s</b> ma niewspieraną wersję. Proszę, upewnij się, że używasz tej samej wersji na obydwu urządzeniach]]></string> + <string name="group_member_status_unknown_short">nieznany</string> + <string name="profile_update_event_contact_name_changed">kontakt %1$s zmieniony na %2$s</string> + <string name="profile_update_event_removed_address">usunięto adres kontaktu</string> + <string name="profile_update_event_removed_picture">usunięto zdjęcie profilu</string> + <string name="profile_update_event_set_new_picture">ustaw nowe zdjęcie profilu</string> + <string name="remote_host_error_bad_state"><![CDATA[Połączenie z telefonem <b>%s</b> jest w złym stanie]]></string> + <string name="remote_ctrl_disconnected_with_reason">Odłączono z powodu: %s</string> + <string name="remote_host_error_busy"><![CDATA[Telefon <b>%s</b> jest zajęty]]></string> + <string name="remote_host_error_timeout"><![CDATA[Osiągnięto limit czasu podczas łączenia z telefonem <b>%s</b>]]></string> + <string name="remote_ctrl_error_inactive">Komputer jest nieaktywny</string> + <string name="remote_host_error_disconnected"><![CDATA[Telefon <b>%s</b> został rozłączony]]></string> + <string name="welcome_message_is_too_long">Wiadomość powitalna jest zbyt długa</string> + <string name="database_migration_in_progress">Proces migracji bazy danych jest w toku. +\nMoże to potrwać kilka minut.</string> + <string name="message_too_large">Wiadomość jest zbyt duża</string> + <string name="call_service_notification_audio_call">Połączenie audio</string> + <string name="call_service_notification_end_call">Zakończ połączenie</string> + <string name="call_service_notification_video_call">Połączenie wideo</string> + <string name="unable_to_open_browser_title">Błąd podczas otwierania przeglądarki</string> + <string name="unable_to_open_browser_desc">Do połączeń wymagana jest domyślna przeglądarka. Proszę skonfigurować domyślną przeglądarkę systemową, i podzielić się informacją z twórcami.</string> + <string name="e2ee_info_pq_short">Ten czat jest chroniony przez szyfrowanie e2e odporne na ataki kwantowe.</string> + <string name="e2ee_info_no_pq"><![CDATA[Wiadomości, pliki i połączenia są chronione przez <b>szyfrowanie end-to-end</b> z perfect forward secrecy, zaprzeczalnością i odzyskiwaniem bezpieczeństwa po kompromitacji.]]></string> + <string name="auth_open_migration_to_another_device">Otwórz ekran migrowania</string> + <string name="migrate_from_another_device">Zmigruj z innego urządzenia</string> + <string name="set_passphrase">Ustaw hasło</string> + <string name="conn_event_disabled_pq">standardowe szyfrowanie end-to-end</string> + <string name="migrate_to_device_chat_migrated">Czat zmigrowany!</string> + <string name="v5_6_app_data_migration">Migracja danych aplikacji</string> + <string name="v5_6_safer_groups">Bezpieczniejsze grupy</string> + <string name="v5_6_picture_in_picture_calls_descr">Używaj aplikacji podczas połączenia.</string> + <string name="v5_6_picture_in_picture_calls">Połączenia obraz-w-obrazie</string> + <string name="v5_6_safer_groups_descr">Administratorzy mogą zablokować członka dla wszystkich członków.</string> + <string name="invalid_file_link">Nieprawidłowy link</string> + <string name="migrate_to_device_title">Zmigruj tutaj</string> + <string name="migrate_to_device_migrating">Migrowanie</string> + <string name="or_paste_archive_link">Lub wklej link archiwum</string> + <string name="paste_archive_link">Wklej link archiwum</string> + <string name="migrate_to_device_database_init">Przygotowywanie pobrania</string> + <string name="migrate_to_device_bytes_downloaded">%s pobrane</string> + <string name="migrate_to_device_try_again">Możesz spróbować ponownie.</string> + <string name="migrate_to_device_importing_archive">Importowanie archiwum</string> + <string name="migrate_to_device_repeat_download">Powtórz pobieranie</string> + <string name="migrate_to_device_import_failed">Import nie udał się</string> + <string name="migrate_to_device_repeat_import">Powtórz importowanie</string> + <string name="migrate_to_device_error_downloading_archive">Błąd pobierania archiwum</string> + <string name="migrate_to_device_finalize_migration">Dokończ migrację na innym urządzeniu.</string> + <string name="migrate_to_device_apply_onion">Zastosuj</string> + <string name="migrate_from_device_title">Zmigruj urządzenie</string> + <string name="migrate_from_device_to_another_device">Zmigruj do innego urządzenia</string> + <string name="migrate_from_device_database_init">Przygotowywanie wgrania</string> + <string name="migrate_from_device_error_deleting_database">Błąd usuwania bazy danych</string> + <string name="migrate_from_device_stopping_chat">Zatrzymywanie czatu</string> + <string name="migrate_from_device_chat_should_be_stopped">Aby konturować, czat musi zostać zatrzymany.</string> + <string name="migrate_from_device_archiving_database">Archiwizowanie bazy danych</string> + <string name="migrate_from_device_confirm_upload">Potwierdź wgranie</string> + <string name="migrate_from_device_bytes_uploaded">%s wgrane</string> + <string name="migrate_from_device_upload_failed">Wgrywanie nie udane</string> + <string name="migrate_from_device_uploading_archive">Wgrywanie archiwum</string> + <string name="migrate_from_device_repeat_upload">Powtórz wgrywanie</string> + <string name="migrate_from_device_creating_archive_link">Tworzenie linku archiwum</string> + <string name="migrate_from_device_try_again">Możesz spróbować ponownie.</string> + <string name="migrate_from_device_or_share_this_file_link">Lub bezpiecznie udostępnij ten link pliku</string> + <string name="migrate_from_device_start_chat">Rozpocznij czat</string> + <string name="migrate_from_device_migration_complete">Migracja zakończona</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Potwierdź, że pamiętasz hasło do bazy danych, aby ją zmigrować.</string> + <string name="migrate_from_device_verify_database_passphrase">Zweryfikuj hasło bazy danych</string> + <string name="migrate_from_device_check_connection_and_try_again">Sprawdź swoje połączenie z internetem i spróbuj ponownie</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Wszystkie twoje kontakty, konwersacje i pliki zostaną bezpiecznie zaszyfrowane i wgrane w kawałkach do skonfigurowanych przekaźników XFTP.</string> + <string name="migrate_from_device_archive_and_upload">Archiwizuj i prześlij</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Uwaga</b>: używanie tej samej bazy danych na dwóch urządzeniach zepsuje odszyfrowywanie wiadomości twoich połączeń, jako zabezpieczenie.]]></string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Ostrzeżenie</b>: archiwum zostanie usunięte.]]></string> + <string name="migrate_from_device_cancel_migration">Anuluj migrację</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Wybierz <i>Zmigruj z innego urządzenia</i> na nowym urządzeniu i zeskanuj kod QR.]]></string> + <string name="migrate_to_device_confirm_network_settings">Potwierdź ustawienia sieciowe</string> + <string name="migrate_from_device_delete_database_from_device">Usuń bazę danych z tego urządzenia</string> + <string name="migrate_to_device_download_failed">Pobieranie nie udane</string> + <string name="migrate_to_device_downloading_archive">Pobieranie archiwum</string> + <string name="migrate_to_device_downloading_details">Pobieranie szczegółów linku</string> + <string name="v5_6_quantum_resistant_encryption_descr">Włącz w czatach bezpośrednich (BETA)!</string> + <string name="migrate_to_device_enter_passphrase">Wprowadź hasło</string> + <string name="migrate_from_device_error_exporting_archive">Błąd eksportu bazy danych czatu</string> + <string name="migrate_from_device_error_saving_settings">Błąd zapisywania ustawień</string> + <string name="migrate_from_device_error_uploading_archive">Błąd wgrywania archiwum</string> + <string name="migrate_from_device_error_verifying_passphrase">Błąd weryfikowania hasła:</string> + <string name="migrate_from_device_exported_file_doesnt_exist">Wyeksportowany plik nie istnieje</string> + <string name="e2ee_info_no_pq_short">Ten czat jest chroniony przez szyfrowanie end-to-end.</string> + <string name="migrate_from_device_verify_passphrase">Zweryfikuj hasło</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Ostrzeżenie: rozpoczęcie czatu na wielu urządzeniach nie jest wspierane i spowoduje niepowodzenia dostarczania wiadomości</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[<b>Nie możesz</b> używać tej samej bazy na dwóch urządzeniach.]]></string> + <string name="migrate_to_device_file_delete_or_link_invalid">Plik został usunięty lub łącze jest nieprawidłowe</string> + <string name="migrate_from_device_finalize_migration">Dokończ migrację</string> + <string name="e2ee_info_pq"><![CDATA[Wiadomości, pliki i połączenia są chronione przez <b>kwantowo odporne szyfrowanie end-to-end</b> z doskonałym utajnianiem z wyprzedzeniem i odzyskiem po złamaniu.]]></string> + <string name="v5_6_app_data_migration_descr">Zmigruj do innego urządzenia przez kod QR.</string> + <string name="migrate_to_device_confirm_network_settings_footer">Proszę potwierdzić, że ustawienia sieciowe są prawidłowe dla tego urządzenia.</string> + <string name="conn_event_enabled_pq">szyfrowanie e2e odporne na ataki kwantowe</string> + <string name="v5_6_quantum_resistant_encryption">Szyfrowanie odporne na ataki kwantowe</string> + <string name="permissions_find_in_settings_and_grant">Znajdź to uprawnienie w ustawieniach Androida i przydziel je manualnie.</string> + <string name="permissions_grant_in_settings">Zezwól w ustawieniach</string> + <string name="permissions_required">Przydziel uprawnienia</string> + <string name="permissions_camera">Aparat</string> + <string name="permissions_camera_and_record_audio">Aparat i mikrofon</string> + <string name="error_showing_desktop_notification">Błąd podczas pokazywania powiadomienia, skontaktuj się z deweloperami.</string> + <string name="permissions_grant">Przydziel uprawnienia, by wykonywać połączenia</string> + <string name="permissions_record_audio">Mikrofon</string> + <string name="permissions_open_settings">Otwórz ustawienia</string> + <string name="forwarded_description">przekazane dalej</string> + <string name="settings_section_title_network_connection">Połączenie z siecią</string> + <string name="files_and_media_not_allowed">Pliki i multimedia nie są dozwolone</string> + <string name="simplex_links_not_allowed">Linki SimpleX są niedozwolone</string> + <string name="voice_messages_not_allowed">Wiadomości głosowe są niedozwolone</string> + <string name="feature_enabled_for">Włączony dla</string> + <string name="feature_roles_owners">właściciele</string> + <string name="simplex_links_are_prohibited_in_group">Linki SimpleX są zablokowane na tej grupie.</string> + <string name="network_type_other">Inne</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="network_type_ethernet">Połączenie ethernet (po kablu)</string> + <string name="feature_roles_admins">administratorzy</string> + <string name="feature_roles_all_members">wszyscy członkowie</string> + <string name="allow_to_send_simplex_links">Zezwól na wysyłanie linków SimpleX.</string> + <string name="network_type_cellular">Sieć komórkowa</string> + <string name="group_members_can_send_simplex_links">Członkowie grupy mogą wysyłać linki SimpleX.</string> + <string name="network_type_no_network_connection">Brak połączenia z siecią</string> + <string name="prohibit_sending_simplex_links">Zabroń wysyłania linków SimpleX</string> + <string name="simplex_links">Linki SimpleX</string> + <string name="forwarded_chat_item_info_tab">Przekazane dalej</string> + <string name="forwarded_from_chat_item_info_title">Przekazane dalej od</string> + <string name="recipients_can_not_see_who_message_from">Odbiorca/y nie mogą zobaczyć od kogo jest ta wiadomość.</string> + <string name="saved_from_chat_item_info_title">Zapisane od</string> + <string name="download_file">Pobierz</string> + <string name="forward_chat_item">Przekaż dalej</string> + <string name="forward_message">Przekaż wiadomość…</string> + <string name="saved_chat_item_info_tab">Zapisane</string> + <string name="saved_description">zapisane</string> + <string name="saved_from_description">zapisane od %s</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="v5_7_forward">Przesyłaj dalej i zapisuj wiadomości</string> + <string name="audio_device_earpiece">Słuchawki douszne</string> + <string name="audio_device_wired_headphones">Słuchawki</string> + <string name="audio_device_speaker">Głośnik</string> + <string name="v5_7_call_sounds_descr">Podczas łączenia połączeń audio i wideo.</string> + <string name="v5_7_call_sounds">Dźwięki w rozmowie</string> + <string name="settings_section_title_profile_images">Zdjęcia profilowe</string> + <string name="network_option_rcv_concurrency">Konkurencyjne odbieranie</string> + <string name="v5_7_new_interface_languages">Litewski interfejs użytkownika</string> + <string name="v5_7_network_descr">Bardziej niezawodne połączenia sieciowe.</string> + <string name="v5_7_network">Zarządzenie sieciowe</string> + <string name="v5_7_shape_profile_images">Kształtuj obrazy profilowe</string> + <string name="v5_7_shape_profile_images_descr">Kwadrat, okrąg lub cokolwiek pomiędzy.</string> + <string name="v5_7_forward_descr">Źródło wiadomości pozostaje prywatne.</string> + <string name="v5_7_quantum_resistant_encryption_descr">Zostanie włączone w czatach bezpośrednich!</string> + <string name="network_smp_proxy_mode_always_description">Zawsze używaj prywatnego trasowania.</string> + <string name="network_smp_proxy_fallback_allow_downgrade">Zezwól na obniżenie wersji</string> + <string name="network_smp_proxy_mode_always">Zawsze</string> + <string name="snd_error_quota">Przekroczono pojemność - odbiorca nie otrzymał wcześniej wysłanych wiadomości.</string> + <string name="snd_error_relay">Błąd serwera docelowego: %1$s</string> + <string name="ci_status_other_error">Błąd: %1$s</string> + <string name="snd_error_proxy_relay">Serwer przekazujący: %1$s +\nBłąd serwera docelowego: %2$s</string> + <string name="snd_error_proxy">Serwer przekazujący: %1$s +\nBłąd: %2$s</string> + <string name="message_delivery_warning_title">Ostrzeżenie dostarczenia wiadomości</string> + <string name="snd_error_expired">Błąd sieciowy - wiadomość wygasła po wielu próbach wysłania jej.</string> + <string name="srv_error_host">Adres serwera jest niekompatybilny z ustawieniami sieciowymi.</string> + <string name="srv_error_version">Wersja serwera jest niekompatybilna z ustawieniami sieciowymi.</string> + <string name="snd_error_auth">Zły klucz lub nieznane połączenie - najprawdopodobniej to połączenie jest usunięte.</string> + <string name="network_smp_proxy_mode_never">Nigdy</string> + <string name="network_smp_proxy_mode_unprotected">Niezabezpieczony</string> + <string name="network_smp_proxy_mode_never_description">NIE używaj prywatnego trasowania.</string> + <string name="update_network_smp_proxy_mode_question">Tryb trasowania wiadomości</string> + <string name="network_smp_proxy_fallback_allow">Tak</string> + <string name="network_smp_proxy_fallback_prohibit">Nie</string> + <string name="network_smp_proxy_fallback_allow_protected">Gdy IP ukryty</string> + <string name="private_routing_show_message_status">Pokaż status wiadomości</string> + <string name="settings_section_title_private_message_routing">TRASOWANIE PRYWATNYCH WIADOMOŚCI</string> + <string name="network_smp_proxy_fallback_prohibit_description">NIE wysyłaj wiadomości bezpośrednio, nawet jeśli serwer docelowy nie obsługuje prywatnego trasowania.</string> + <string name="private_routing_explanation">Aby chronić Twój adres IP, prywatne trasowanie używa Twoich serwerów SMP, aby dostarczyć wiadomości.</string> + <string name="network_smp_proxy_mode_unknown">Nieznane serwery</string> + <string name="network_smp_proxy_mode_unknown_description">Używaj prywatnego trasowania z nieznanymi serwerami.</string> + <string name="update_network_smp_proxy_fallback_question">Rezerwowe trasowania wiadomości</string> + <string name="network_smp_proxy_mode_private_routing">Prywatne trasowanie</string> + <string name="network_smp_proxy_fallback_allow_protected_description">Wysyłaj wiadomości bezpośrednio, gdy adres IP jest chroniony i Twój lub docelowy serwer nie obsługuje prywatnego trasowania.</string> + <string name="network_smp_proxy_fallback_allow_description">Wysyłaj wiadomości bezpośrednio, gdy Twój lub docelowy serwer nie obsługuje prywatnego trasowania.</string> + <string name="network_smp_proxy_mode_unprotected_description">Używaj prywatnego trasowania z nieznanymi serwerami, gdy adres IP nie jest chroniony.</string> + <string name="file_not_approved_title">Nieznane serwery!</string> + <string name="file_not_approved_descr">Bez Tor lub VPN, Twój adres IP będzie widoczny dla tych przekaźników XFTP: +\n%1$s.</string> + <string name="protect_ip_address">Chroń adres IP</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">Aplikacja będzie prosić o potwierdzenie pobierań z nieznanych serwerów plików (z wyjątkiem .onion lub gdy proxy SOCKS jest włączone).</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Bez Tor lub VPN, Twój adres IP będzie widoczny do serwerów plików.</string> + <string name="settings_section_title_files">PLIKI</string> + <string name="settings_section_title_user_theme">Motyw profilu</string> + <string name="chat_list_always_visible">Pokaż listę czatów w nowym oknie</string> + <string name="dark_mode_colors">Kolory ciemnego trybu</string> + <string name="color_mode_light">Jasny</string> + <string name="color_received_quote">Otrzymano odpowiedź</string> + <string name="theme_remove_image">Usuń obraz</string> + <string name="reset_single_color">Zresetuj kolory</string> + <string name="wallpaper_scale_fill">Wypełnij</string> + <string name="wallpaper_scale_fit">Dopasuj</string> + <string name="wallpaper_preview_hello_alice">Dzień dobry!</string> + <string name="wallpaper_preview_hello_bob">Dzień dobry!</string> + <string name="chat_theme_apply_to_light_mode">Jasny tryb</string> + <string name="wallpaper_scale_repeat">Powtórz</string> + <string name="color_primary_variant2">Dodatkowy akcent 2</string> + <string name="chat_theme_apply_to_all_modes">Wszystkie tryby kolorów</string> + <string name="chat_theme_apply_to_dark_mode">Ciemny tryb</string> + <string name="chat_theme_set_default_theme">Ustaw domyślny motyw</string> + <string name="color_mode_system">Systemowy</string> + <string name="color_wallpaper_background">Tło tapety</string> + <string name="wallpaper_advanced_settings">Zaawansowane ustawienia</string> + <string name="chat_theme_apply_to_mode">Zastosuj dla</string> + <string name="wallpaper_scale">Skaluj</string> + <string name="theme_black">Czarny</string> + <string name="color_wallpaper_tint">Akcent tapety</string> + <string name="color_sent_quote">Wyślij odpowiedź</string> + <string name="settings_section_title_chat_colors">Kolory czatu</string> + <string name="settings_section_title_chat_theme">Motyw czatu</string> + <string name="color_mode">Tryb koloru</string> + <string name="color_mode_dark">Ciemny</string> + <string name="error_initializing_web_view">Błąd inicjacji WebView. Zaktualizuj swój system do nowej wersji. Proszę skontaktować się z deweloperami. +\nBłąd: %s</string> + <string name="message_queue_info_none">nic</string> + <string name="v5_8_chat_themes">Nowy motywy czatu</string> + <string name="v5_8_private_routing">Trasowanie prywatnych wiadomości🚀</string> + <string name="v5_8_safe_files">Bezpiecznie otrzymuj pliki</string> + <string name="v5_8_message_delivery">Ulepszona dostawa wiadomości</string> + <string name="v5_8_persian_ui">Perski interfejs użytkownika</string> + <string name="v5_8_safe_files_descr">Potwierdzaj pliki z nieznanych serwerów.</string> + <string name="info_row_debug_delivery">Dostarczenie debugowania</string> + <string name="v5_8_chat_themes_descr">Zrób wygląd Twoich czatów inny!</string> + <string name="v5_8_private_routing_descr">Chroni Twój adres IP przed przekaźnikami wiadomości wybranych przez Twoje kontakty. +\nWłącz w ustawianiach *Sieć i serwery* .</string> + <string name="message_queue_info">Informacje kolejki wiadomości</string> + <string name="message_queue_info_server_info">Informacje kolejki serwera: %1$s +\n +\nostatnia otrzymana wiadomość: %2$s</string> + <string name="v5_8_message_delivery_descr">Ze zredukowanym zużyciem baterii.</string> + <string name="theme_destination_app_theme">Motyw aplikacji</string> + <string name="chat_theme_reset_to_app_theme">Zresetuj do motywu aplikacji</string> + <string name="chat_theme_reset_to_user_theme">Zresetuj do motywu użytkownika</string> + <string name="file_error_no_file">Nie odnaleziono pliku - najprawdopodobniej plik został usunięty lub anulowany.</string> + <string name="info_row_file_status">Status pliku</string> + <string name="info_row_message_status">Status wiadomości</string> + <string name="share_text_file_status">Status pliku: %s</string> + <string name="share_text_message_status">Status wiadomości: %s</string> + <string name="copy_error">Kopiuj błąd</string> + <string name="remote_ctrl_connection_stopped_identity_desc">Ten link dostał użyty z innym urządzeniem mobilnym, proszę stworzyć nowy link na komputerze.</string> + <string name="file_error">Błąd pliku</string> + <string name="file_error_relay">Błąd serwera plików: %1$s</string> + <string name="remote_ctrl_connection_stopped_desc">Sprawdź, czy telefon i komputer są podłączone do tej samej sieci lokalnej i czy zapora sieciowa komputera umożliwia połączenie.\nProszę zgłoś wszystkie inne problemy deweloperom.</string> + <string name="temporary_file_error">Tymczasowy błąd pliku</string> + <string name="file_error_auth">Zły klucz lub nieznany adres fragmentu pliku - najprawdopodobniej plik został usunięty.</string> + <string name="cannot_share_message_alert_title">Nie można wysłać wiadomości</string> + <string name="cannot_share_message_alert_text">Wybrane preferencje czatu zabraniają tej wiadomości.</string> + <string name="smp_proxy_error_connecting">Błąd połączenia z serwerem przekierowania %1$s. Spróbuj ponownie później.</string> + <string name="proxy_destination_error_broker_host">Adres serwera docelowego %1$s jest niekompatybilny z ustawieniami serwera przekazującego %2$s.</string> + <string name="proxy_destination_error_failed_to_connect">Serwer przekazujący %1$s nie mógł połączyć się z serwerem docelowym %2$s. Spróbuj ponownie później.</string> + <string name="smp_proxy_error_broker_version">Wersja serwera przekierowującego jest niekompatybilna z ustawieniami sieciowymi: %1$s.</string> + <string name="proxy_destination_error_broker_version">Wersja serwera docelowego %1$s jest niekompatybilna z serwerem przekierowującym %2$s.</string> + <string name="member_inactive_title">Członek nieaktywny</string> + <string name="message_forwarded_title">Wiadomość przekazana</string> + <string name="member_inactive_desc">Wiadomość może zostać dostarczona później jeśli członek stanie się aktywny.</string> + <string name="app_check_for_updates_beta">Beta</string> + <string name="app_check_for_updates">Sprawdź aktualizacje</string> + <string name="app_check_for_updates_disabled">Wyłączony</string> + <string name="app_check_for_updates_button_download">Pobierz %s (%s)</string> + <string name="app_check_for_updates_download_completed_title">Aktualizacja aplikacji jest pobrana</string> + <string name="app_check_for_updates_notice_title">Sprawdź aktualizacje</string> + <string name="app_check_for_updates_download_started">Pobieranie aktualizacji aplikacji, nie zamykaj aplikacji</string> + <string name="app_check_for_updates_installed_successfully_title">Zainstalowano pomyślnie</string> + <string name="app_check_for_updates_button_install">Zainstaluj aktualizacje</string> + <string name="app_check_for_updates_notice_disable">Wyłącz</string> + <string name="member_info_member_disabled">wyłączony</string> + <string name="member_info_member_inactive">nieaktywny</string> + <string name="appearance_font_size">Rozmiar czcionki</string> + <string name="servers_info_sessions_connected">Połączony</string> + <string name="current_user">Bieżący profil</string> + <string name="servers_info_messages_received">Otrzymane wiadomości</string> + <string name="servers_info_subscriptions_section_header">Odebranie wiadomości</string> + <string name="servers_info_reset_stats_alert_error_title">Błąd resetowania statystyk</string> + <string name="duplicates_label">duplikaty</string> + <string name="completed">Zakończono</string> + <string name="connections">Połączenia</string> + <string name="created">Utworzono</string> + <string name="deletion_errors">Błędy usuwania</string> + <string name="chunks_downloaded">Fragmenty pobrane</string> + <string name="chunks_uploaded">Fragmenty przesłane</string> + <string name="downloaded_files">Pobrane pliki</string> + <string name="smp_servers_configured">Skonfigurowane serwery SMP</string> + <string name="acknowledged">Potwierdzono</string> + <string name="acknowledgement_errors">Błędy potwierdzenia</string> + <string name="deleted">Usunięto</string> + <string name="servers_info_subscriptions_connections_subscribed">Aktywne połączenia</string> + <string name="all_users">Wszystkie profile</string> + <string name="xftp_servers_configured">Skonfigurowane serwery XFTP</string> + <string name="decryption_errors">błąd odszyfrowywania</string> + <string name="servers_info_detailed_statistics">Szczegółowe statystyki</string> + <string name="servers_info_details">Szczegóły</string> + <string name="download_errors">Błędy pobierania</string> + <string name="smp_proxy_error_broker_host">Adres serwera przekierowującego jest niekompatybilny z ustawieniami sieciowymi: %1$s.</string> + <string name="servers_info_files_tab">Pliki</string> + <string name="servers_info_sessions_connecting">Łączenie</string> + <string name="servers_info_sessions_errors">Błędy</string> + <string name="servers_info_connected_servers_section_header">Połączone serwery</string> + <string name="servers_info_modal_error_title">Błąd</string> + <string name="servers_info_reconnect_server_error">Błąd ponownego łączenia z serwerem</string> + <string name="servers_info_reconnect_servers_error">Błąd ponownego łączenia serwerów</string> + <string name="servers_info_downloaded">Pobrane</string> + <string name="attempts_label">próby</string> + <string name="expired_label">wygasły</string> + <string name="chunks_deleted">Fragmenty usunięte</string> + <string name="message_forwarded_desc">Brak bezpośredniego połączenia, wiadomość została przekazana przez administratora.</string> + <string name="smp_servers_other">Inne serwery SMP</string> + <string name="xftp_servers_other">Inne serwery XFTP</string> + <string name="subscription_percentage">Pokaż procent</string> + <string name="app_check_for_updates_stable">Stabilny</string> + <string name="app_check_for_updates_update_available">Aktualizacja dostępna: %s</string> + <string name="app_check_for_updates_button_open">Otwórz lokalizację pliku</string> + <string name="app_check_for_updates_installed_successfully_desc">Proszę zrestartować aplikację.</string> + <string name="app_check_for_updates_button_remind_later">Przypomnij później</string> + <string name="app_check_for_updates_button_skip">Pomiń tę wersję</string> + <string name="app_check_for_updates_canceled">Pobieranie aktualizacji anulowane</string> + <string name="app_check_for_updates_notice_desc">Aby otrzymywać powiadomienia o nowych wersjach, włącz okresowe sprawdzanie wersji Stabilnych lub Beta.</string> + <string name="appearance_zoom">Przybliż</string> + <string name="servers_info_missing">Brak informacji, spróbuj przeładować</string> + <string name="servers_info">Informacje o serwerach</string> + <string name="servers_info_target">Wyświetlanie informacji dla</string> + <string name="servers_info_statistics_section_header">Statystyki</string> + <string name="servers_info_transport_sessions_section_header">Sesje transportowe</string> + <string name="servers_info_private_data_disclaimer">Zaczynanie od %s. \nWszystkie dane są prywatne na Twoim urządzeniu.</string> + <string name="servers_info_reconnect_all_servers_button">Połącz ponownie wszystkie serwery</string> + <string name="servers_info_reconnect_server_title">Połączyć ponownie serwer?</string> + <string name="servers_info_proxied_servers_section_footer">Nie jesteś połączony z tymi serwerami. Prywatne trasowanie jest używane do dostarczania do nich wiadomości.</string> + <string name="servers_info_detailed_statistics_received_messages_header">Otrzymane wiadomości</string> + <string name="servers_info_reset_stats_alert_confirm">Resetuj</string> + <string name="servers_info_reset_stats">Resetuj wszystkie statystyki</string> + <string name="servers_info_detailed_statistics_sent_messages_header">Wysłane wiadomości</string> + <string name="servers_info_reset_stats_alert_message">Statystyki serwerów zostaną zresetowane - nie można tego cofnąć!</string> + <string name="servers_info_uploaded">Przesłane</string> + <string name="other_label">inne</string> + <string name="proxied">Trasowane przez proxy</string> + <string name="servers_info_detailed_statistics_received_total">Otrzymano łącznie</string> + <string name="other_errors">inne błędy</string> + <string name="secured">Zabezpieczone</string> + <string name="subscribed">Zasubskrybowano</string> + <string name="uploaded_files">Przesłane pliki</string> + <string name="open_server_settings_button">Otwórz ustawienia serwera</string> + <string name="server_address">Adres serwera</string> + <string name="servers_info_messages_sent">Wysłane wiadomości</string> + <string name="servers_info_reconnect_servers_message">Ponownie połącz ze wszystkimi połączonymi serwerami w celu wymuszenia dostarczenia wiadomości. Wykorzystuje to dodatkowy ruch.</string> + <string name="servers_info_reset_stats_alert_title">Zresetować wszystkie statystyki?</string> + <string name="subscription_errors">Błędy subskrypcji</string> + <string name="subscription_results_ignored">Subskrypcje zignorowane</string> + <string name="servers_info_detailed_statistics_sent_messages_total">Wysłano łącznie</string> + <string name="network_error_broker_host_desc">Adres serwera jest niekompatybilny z ustawieniami sieci: %1$s.</string> + <string name="please_try_later">Proszę spróbować później.</string> + <string name="private_routing_error">Błąd prywatnego trasowania</string> + <string name="network_error_broker_version_desc">Wersja serwera jest niekompatybilna z aplikacją: %1$s.</string> + <string name="scan_paste_link">Skanuj / Wklej link</string> + <string name="servers_info_subscriptions_total">Łącznie</string> + <string name="servers_info_subscriptions_connections_pending">Oczekujące</string> + <string name="servers_info_previously_connected_servers_section_header">Wcześniej połączone serwery</string> + <string name="servers_info_proxied_servers_section_header">Serwery trasowane przez proxy</string> + <string name="servers_info_reconnect_servers_title">Połączyć ponownie serwery?</string> + <string name="servers_info_reconnect_server_message">Ponownie połącz z serwerem w celu wymuszenia dostarczenia wiadomości. Wykorzystuje to dodatkowy ruch.</string> + <string name="sent_via_proxy">Wysłano przez proxy</string> + <string name="xftp_server">Serwer XFTP</string> + <string name="smp_server">Serwer SMP</string> + <string name="servers_info_detailed_statistics_receive_errors">Błędy otrzymania</string> + <string name="reconnect">Połącz ponownie</string> + <string name="sent_directly">Wysłano bezpośrednio</string> + <string name="servers_info_starting_from">Zaczynanie od %s.</string> + <string name="send_errors">Wyślij błędy</string> + <string name="size">Rozmiar</string> + <string name="upload_errors">Błędy przesłania</string> + <string name="v6_0_increase_font_size">Zwiększ rozmiar czcionki.</string> + <string name="info_view_message_button">wiadomość</string> + <string name="compose_message_placeholder">Wiadomość</string> + <string name="info_view_open_button">otwórz</string> + <string name="info_view_search_button">szukaj</string> + <string name="selected_chat_items_selected_n">Zaznaczono %d</string> + <string name="info_view_video_button">wideo</string> + <string name="no_filtered_contacts">Brak filtrowanych kontaktów</string> + <string name="invite_friends_short">Zaproś</string> + <string name="v6_0_upgrade_app_descr">Pobieraj nowe wersje z GitHub.</string> + <string name="v6_0_new_media_options">Nowe opcje mediów</string> + <string name="contact_list_header_title">Twoje kontakty</string> + <string name="new_message">Nowa wiadomość</string> + <string name="v6_0_upgrade_app">Aktualizuj aplikację automatycznie</string> + <string name="v6_0_privacy_blur">Rozmycie dla lepszej prywatności.</string> + <string name="one_hand_ui_change_instruction">Możesz to zmienić w ustawieniach wyglądu.</string> + <string name="action_button_add_members">Zaproś</string> + <string name="cant_send_message_to_member_alert_title">Nie można wysłać wiadomości do członka grupy</string> + <string name="cant_call_member_send_message_alert_text">Wyślij wiadomość aby włączyć połączenia.</string> + <string name="v6_0_new_chat_experience">Nowe możliwości czatu 🎉</string> + <string name="v6_0_reachable_chat_toolbar_descr">Korzystaj z aplikacji jedną ręką.</string> + <string name="v6_0_chat_list_media">Odtwórz z listy czatów.</string> + <string name="deleted_chats">Zarchiwizowane kontakty</string> + <string name="info_view_call_button">zadzwoń</string> + <string name="info_view_connect_button">połącz</string> + <string name="v6_0_connection_servers_status_descr">Stan połączenia i serwerów.</string> + <string name="create_address_button">Utwórz</string> + <string name="error_parsing_uri_title">Nieprawidłowy link</string> + <string name="network_options_save_and_reconnect">Zapisz i połącz ponownie</string> + <string name="toolbar_settings">Ustawienia</string> + <string name="network_option_tcp_connection">Połączenie TCP</string> + <string name="selected_chat_items_nothing_selected">Nic nie jest zaznaczone</string> + <string name="error_parsing_uri_desc">Sprawdź czy link SimpleX jest poprawny.</string> + <string name="one_hand_ui_card_title">Przełącz listę czatów:</string> + <string name="v6_0_your_contacts_descr">Archiwizuj kontakty aby porozmawiać później.</string> + <string name="v6_0_private_routing_descr">Chroni Twój adres IP i połączenia.</string> + <string name="v6_0_reachable_chat_toolbar">Osiągalny pasek narzędzi czatu</string> + <string name="privacy_media_blur_radius_strong">Silne</string> + <string name="privacy_media_blur_radius">Rozmycie mediów</string> + <string name="privacy_media_blur_radius_medium">Średni</string> + <string name="privacy_media_blur_radius_off">Wyłącz</string> + <string name="privacy_media_blur_radius_soft">Łagodny</string> + <string name="keep_conversation">Zachowaj rozmowę</string> + <string name="only_delete_conversation">Usuń tylko rozmowę</string> + <string name="you_can_still_send_messages_to_contact">Możesz wysyłać wiadomości do %1$s ze zarchiwizowanych kontaktów.</string> + <string name="you_can_still_view_conversation_with_contact">Nadal możesz przeglądać rozmowę z %1$s na liście czatów.</string> + <string name="confirm_delete_contact_question">Potwierdzić usunięcie kontaktu?</string> + <string name="contact_deleted">Kontakt usunięty!</string> + <string name="delete_contact_cannot_undo_warning">Kontakt zostanie usunięty – nie można tego cofnąć!</string> + <string name="conversation_deleted">Rozmowa usunięta!</string> + <string name="delete_without_notification">Usuń bez powiadomienia</string> + <string name="paste_link">Wklej link</string> + <string name="allow_calls_question">Zezwolić na połączenia?</string> + <string name="cant_call_contact_alert_title">Nie można zadzwonić do kontaktu</string> + <string name="cant_call_contact_connecting_wait_alert_text">Łączenie z kontaktem, poczekaj lub sprawdź później!</string> + <string name="cant_call_contact_deleted_alert_text">Kontakt jest usunięty.</string> + <string name="you_need_to_allow_calls">Aby móc dzwonić, musisz zezwolić kontaktowi na połączenia.</string> + <string name="calls_prohibited_alert_title">Połączenia zakazane!</string> + <string name="cant_call_member_alert_title">Nie można zadzwonić do członka grupy</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">Poproś kontakt o włącznie połączeń.</string> + <string name="delete_members_messages__question">Usunąć %d wiadomości członków?</string> + <string name="delete_messages_mark_deleted_warning">Wiadomości zostaną oznaczone do usunięcia. Odbiorca(y) będą mogli ujawnić te wiadomości.</string> + <string name="select_verb">Zaznacz</string> + <string name="moderate_messages_will_be_deleted_warning">Wiadomości zostaną usunięte dla wszystkich członków.</string> + <string name="moderate_messages_will_be_marked_warning">Wiadomości zostaną oznaczone jako moderowane dla wszystkich członków.</string> + <string name="one_hand_ui">Osiągalny pasek narzędzi czatu</string> + <string name="chat_database_exported_title">Wyeksportowano bazę danych czatu</string> + <string name="chat_database_exported_continue">Kontynuuj</string> + <string name="media_and_file_servers">Serwery mediów i plików</string> + <string name="message_servers">Serwery wiadomości</string> + <string name="network_socks_proxy">Proxy SOCKS</string> + <string name="chat_database_exported_not_all_files">Niektóre plik(i) nie zostały wyeksportowane</string> + <string name="chat_database_exported_migrate">Możesz zmigrować wyeksportowaną bazy danych.</string> + <string name="chat_database_exported_save">Możesz zapisać wyeksportowane archiwum.</string> + <string name="reset_all_hints">Zresetuj wszystkie wskazówki</string> + <string name="v6_0_connect_faster_descr">Szybciej łącz się ze znajomymi.</string> + <string name="v6_0_connection_servers_status">Kontroluj swoją sieć</string> + <string name="v6_0_delete_many_messages_descr">Usuń do 20 wiadomości na raz.</string> + <string name="error_forwarding_messages">Błąd przekazywania wiadomości</string> + <string name="forward_files_not_accepted_desc">%1$d plik(ów/i) nie zostały pobrane.</string> + <string name="forward_alert_title_messages_to_forward">Czy przekazać %1$s wiadomoś(ć/ci)?</string> + <string name="forward_alert_forward_messages_without_files">Przekazać wiadomości bez plików?</string> + <string name="forward_files_missing_desc">%1$d plik(ów/i) zostały usunięte.</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s wiadomoś(ć/ci) nie przekazane</string> + <string name="compose_forward_messages_n">Przekazywanie %1$s wiadomoś(ć/ci)</string> + <string name="forward_multiple">Przekazywanie wiadomości…</string> + <string name="network_proxy_auth_mode_no_auth">Nie używaj danych logowania do proxy.</string> + <string name="network_proxy_incorrect_config_title">Błąd zapisywania ustawień proxy</string> + <string name="network_proxy_incorrect_config_desc">Sprawdź czy konfiguracja serwera proxy jest poprawna.</string> + <string name="forward_files_in_progress_desc">%1$d plik(ów/i) dalej są pobierane.</string> + <string name="forward_files_failed_to_receive_desc">%1$d plik(ów/i) nie udało się pobrać.</string> + <string name="switching_profile_error_title">Błąd zmiany profilu</string> + <string name="settings_section_title_chat_database">BAZA CZATU</string> + <string name="n_file_errors">%1$d błędów plików:\n%2$s</string> + <string name="n_other_file_errors">%1$d innych błędów plików.</string> + <string name="forward_files_messages_deleted_after_selection_desc">Wiadomości zostały usunięte po wybraniu ich.</string> + <string name="forward_alert_title_nothing_to_forward">Nic do przekazania!</string> + <string name="forward_files_not_accepted_receive_files">Pobierz</string> + <string name="compose_save_messages_n">Zapisywanie %1$s wiadomości</string> + <string name="network_proxy_auth">Uwierzytelnianie proxy</string> + <string name="network_proxy_password">Hasło</string> + <string name="error_initializing_web_view_wrong_arch">Błąd inicjalizacji WebView. Upewnij się, że WebView jest zainstalowany, a jego obsługiwana architektura to arm64.\nBłąd: %s</string> + <string name="delete_messages_cannot_be_undone_warning">Wiadomości zostaną usunięte - nie można tego cofnąć!</string> + <string name="migrate_from_device_remove_archive_question">Usunąć archiwum?</string> + <string name="settings_message_shape_corner">Róg</string> + <string name="settings_section_title_message_shape">Kształt wiadomości</string> + <string name="network_session_mode_session">Sesja aplikacji</string> + <string name="network_session_mode_session_description">Nowe poświadczenia SOCKS będą używane przy każdym uruchomieniu aplikacji.</string> + <string name="network_session_mode_server_description">Dla każdego serwera zostaną użyte nowe poświadczenia SOCKS.</string> + <string name="call_desktop_permission_denied_chrome">Kliknij przycisk informacji przy polu adresu, aby zezwolić na korzystanie z mikrofonu.</string> + <string name="call_desktop_permission_denied_safari">Otwórz Safari Ustawienia / Strony internetowe / Mikrofon, a następnie wybierz opcję Zezwalaj dla localhost.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">Użyj różnych poświadczeń proxy dla każdego połączenia.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">Użyj różnych poświadczeń proxy dla każdego profilu.</string> + <string name="network_proxy_random_credentials">Użyj losowych poświadczeń</string> + <string name="network_proxy_username">Nazwa użytkownika</string> + <string name="network_proxy_auth_mode_username_password">Twoje poświadczenia mogą zostać wysłane niezaszyfrowane.</string> + <string name="icon_descr_sound_muted">Dźwięk wyciszony</string> + <string name="select_chat_profile">Wybierz profil czatu</string> + <string name="new_chat_share_profile">Udostępnij profil</string> + <string name="switching_profile_error_message">Twoje połączenie zostało przeniesione do %s, ale podczas przekierowania do profilu wystąpił nieoczekiwany błąd.</string> + <string name="system_mode_toast">Tryb systemu</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">Przesłane archiwum bazy danych zostanie trwale usunięte z serwerów.</string> + <string name="network_session_mode_server">Serwer</string> + <string name="settings_message_shape_tail">Ogon</string> + <string name="call_desktop_permission_denied_title">Aby wykonywać połączenia, zezwól na korzystanie z mikrofonu. Zakończ połączenie i spróbuj zadzwonić ponownie.</string> + <string name="v6_1_better_security">Lepsze bezpieczeństwo ✅</string> + <string name="v6_1_message_dates_descr">Lepsze daty wiadomości.</string> + <string name="v6_1_customizable_message_descr">Możliwość dostosowania kształtu wiadomości.</string> + <string name="v6_1_better_calls">Lepsze połączenia</string> + <string name="v6_1_better_user_experience">Lepsze doświadczenie użytkownika</string> + <string name="v6_1_better_security_descr">Protokoły SimpleX sprawdzone przez Trail of Bits.</string> + <string name="v6_1_better_calls_descr">Przełączanie audio i wideo podczas połączenia.</string> + <string name="v6_1_delete_many_messages_descr">Usuń lub moderuj do 200 wiadomości.</string> + <string name="v6_1_forward_many_messages_descr">Przekazywanie do 20 wiadomości jednocześnie.</string> + <string name="v6_1_switch_chat_profile_descr">Przełącz profil czatu dla zaproszeń jednorazowych.</string> + <string name="create_1_time_link">Utwórz link jednorazowy</string> + <string name="share_1_time_link_with_a_friend">Udostępnij jednorazowy link znajomemu</string> + <string name="simplex_address_or_1_time_link">Adres SimpleX czy link jednorazowy?</string> + <string name="address_or_1_time_link">Adres lub link jednorazowy?</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[Link jednorazowy może być użyty <i>tylko z jednym kontaktem</i> - udostępnij go osobiście lub przez dowolny komunikator.]]></string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">Adres SimpleX i jednorazowe linki są bezpieczne do udostępniania przez dowolny komunikator.</string> + <string name="report_item_archived_by">raport zarchiwizowany przez %s</string> + <string name="report_reason_other">Inny powód</string> + <string name="archive_report">Zarchiwizuj raport</string> + <string name="address_settings">Ustawienia adresowe</string> + <string name="onboarding_network_about_operators">O operatorach</string> + <string name="business_address">Adres służbowy</string> + <string name="operator_conditions_accepted">Zaakceptowane warunki</string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Warunki będą akceptowane dla operatora(ów): <b>%s</b>.]]></string> + <string name="operator_added_message_servers">Dodane serwery wiadomości</string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Te same warunki będą miały zastosowanie do operatora(ów): <b>%s</b>.]]></string> + <string name="report_archive_alert_title">Zarchiwizować raport?</string> + <string name="archive_verb">Archiwum</string> + <string name="chat_list_all">Wszystkie</string> + <string name="chat_list_businesses">Biznesy</string> + <string name="group_reports_active_one">raport</string> + <string name="privacy_chat_list_open_links_ask">Zapytaj</string> + <string name="operator_conditions_accepted_for_some"><![CDATA[Warunki są już akceptowane dla następujących operatorów: <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[Te same warunki będą miały zastosowanie do operatora <b>%s</b>.]]></string> + <string name="appearance_bars_blur_radius">Rozmyj</string> + <string name="add_your_team_members_to_conversations">Dodaj członków drużyny do konwersacji</string> + <string name="button_add_team_members">Dodaj członków drużyny</string> + <string name="v6_2_business_chats">Konwersacje służbowe</string> + <string name="chat_list_add_list">Dodaj listę</string> + <string name="add_to_list">Dodaj do listy</string> + <string name="delete_chat_list_warning">"Wszystkie konwersacje zostaną usunięte z list %s, oraz listy."</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[Wszystkie wiadomości i pliki wysyłane są w postaci<b>zaszyfrowanej</b>, z post-kwantowym bezpieczeństwem w bezpośrednich wiadomościach.]]></string> + <string name="button_add_friends">Dodaj znajomych</string> + <string name="operator_added_xftp_servers">Dodane serwery plików i mediów</string> + <string name="appearance_app_toolbars">Paski narzędziowe aplikacji</string> + <string name="onboarding_notifications_mode_service_desc_short">Aplikacja działa zawsze w tle</string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[Warunki będą akceptowane dla operatora(ów): <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_applied"><![CDATA[Warunki te będą również miały zastosowanie do: <b>%s</b>.]]></string> + <string name="operators_conditions_accepted_for"><![CDATA[Warunki są akceptowane dla operatora (operatorów): <b>%s</b>.]]></string> + <string name="accept_conditions">Zaakceptuj warunki</string> + <string name="display_name_accepted_invitation">zaproszenie zaakceptowane</string> + <string name="report_item_archived">zarchiwizowany raport</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Urządzenia Xiaomi</b>: Włącz Autostart w ustawieniach systemowych, aby powiadomienia działały.]]></string> + <string name="delete_report">Usuń raport</string> + <string name="chat_list_contacts">Kontakty</string> + <string name="onboarding_notifications_mode_periodic_desc_short">Sprawdzaj wiadomości co 10 minut</string> + <string name="button_delete_chat">Usuń czat</string> + <string name="report_reason_community">Naruszenie wytycznych społeczności</string> + <string name="operator_conditions_accepted_on">Warunki zaakceptowane na: %s.</string> + <string name="operator_conditions_accepted_for_enabled_operators_on">Warunki zostaną automatycznie zaakceptowane dla włączonych operatorów na: %s.</string> + <string name="operators_conditions_will_also_apply"><![CDATA[Warunki te będą również miały zastosowanie do: <b>%s</b>.]]></string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[Aby korzystać z serwerów <b>%s</b>, Zaakceptuj warunki użytkowania.]]></string> + <string name="operator_conditions_of_use">Warunki użytkowania</string> + <string name="blocking_reason_content">Treść narusza warunki użytkowania</string> + <string name="connection_error_blocked">Połączenie zablokowane</string> + <string name="connection_error_blocked_desc">Połączenie jest zablokowane przez operatora serwera:\n%1$s.</string> + <string name="change_list">Lista zmian</string> + <string name="create_list">Utwórz listę</string> + <string name="delete_chat_list_menu_action">Usuń</string> + <string name="operator_conditions_failed_to_load">Nie można załadować tekstu aktualnych warunków, możesz przejrzeć warunki za pomocą tego linku:</string> + <string name="onboarding_network_operators_continue">Kontynuuj</string> + <string name="info_row_chat">Czat</string> + <string name="connect_plan_chat_already_exists">Czat już istnieje!</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Jesteś już połączony z <b>%1$s</b>.]]></string> + <string name="delete_chat_list_question">Usunąć listę?</string> + <string name="connection_not_ready">Połączenie nie jest gotowe.</string> + <string name="delete_chat_question">Usunąć czat?</string> + <string name="delete_chat_for_self_cannot_undo_warning">Czat zostanie dla Ciebie usunięty - nie można tego cofnąć!</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">Czat zostanie usunięty dla wszystkich członków - nie można tego cofnąć!</string> + <string name="operator_conditions_will_be_accepted_on">Warunki będą akceptowane w dniu: %s.</string> + <string name="connection_security">Bezpieczeństwo połączenia</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">Warunki będą akceptowane dla operatorów włączonych po 30 dniach.</string> + <string name="change_order_chat_list_menu_action">Zmień kolejność</string> + <string name="sync_connection_desc">Połączenie wymaga renegocjacji szyfrowania.</string> +</resources> 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 622ad8b2d6..a0cce488af 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 @@ -10,9 +10,9 @@ <string name="rcv_conn_event_switch_queue_phase_changing">mudando endereço…</string> <string name="snd_conn_event_switch_queue_phase_changing_for_member">mudando endereço para %s…</string> <string name="snd_conn_event_switch_queue_phase_changing">mudando endereço…</string> - <string name="change_role">Mudar função</string> + <string name="change_role">Mudar cargo</string> <string name="change_verb">Mudar</string> - <string name="both_you_and_your_contacts_can_delete">Você e seu contato podem excluir mensagens enviadas de forma irreversível.</string> + <string name="both_you_and_your_contacts_can_delete">Você e seu contato podem excluir mensagens enviadas de forma irreversível. (24 horas)</string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Pode ser desativado nas configurações</b> – as notificações ainda serão exibidas enquanto o aplicativo estiver em execução.]]></string> <string name="both_you_and_your_contact_can_send_voice">Você e seu contato podem enviar mensagens de voz.</string> <string name="notifications_mode_off_desc">O aplicativo pode receber notificações apenas quando estiver em execução, nenhum serviço em segundo plano será iniciado</string> @@ -24,66 +24,62 @@ <string name="icon_descr_cancel_live_message">Cancelar mensagem ao vivo</string> <string name="back">Voltar</string> <string name="choose_file">Arquivo</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Adicionar novo contato</b>: para criar seu QR code de uso único para seu contato.]]></string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Escanear código QR</b>: para se conectar ao seu contato que mostra o código QR para você.]]></string> <string name="accept_contact_button">Aceitar</string> - <string name="clear_chat_question">Limpar chat\?</string> + <string name="clear_chat_question">Limpar bate-papo?</string> <string name="clear_verb">Limpar</string> - <string name="clear_chat_button">Limpar chat</string> + <string name="clear_chat_button">Limpar bate-papo</string> <string name="clear_chat_menu_action">Limpar</string> <string name="icon_descr_cancel_link_preview">cancelar pré-visualização do link</string> <string name="feature_cancelled_item">cancelado %s</string> - <string name="app_version_name">Versão do App: v%s</string> + <string name="app_version_name">Versão do Aplicativo: v%s</string> <string name="callstatus_calling">chamando…</string> <string name="callstatus_in_progress">chamada em andamento</string> <string name="accept">Aceitar</string> <string name="call_already_ended">Chamada já encerrada!</string> <string name="icon_descr_call_progress">Chamada em andamento</string> <string name="icon_descr_call_ended">Chamada encerrada</string> - <string name="answer_call">Atender ligação</string> - <string name="integrity_msg_bad_hash">hash de mensagem incorreto</string> + <string name="answer_call">Atender chamada</string> + <string name="integrity_msg_bad_hash">hash de mensagem incorreta</string> <string name="integrity_msg_bad_id">ID de mensagem incorreta</string> - <string name="impossible_to_recover_passphrase"><![CDATA[<b>Observação</b>: você NÃO poderá recuperar ou alterar a senha se a perder.]]></string> + <string name="impossible_to_recover_passphrase"><![CDATA[<b>Atenção</b>: você NÃO poderá recuperar ou alterar a senha se a perder.]]></string> <string name="cannot_receive_file">Não é possível receber o arquivo</string> <string name="icon_descr_cancel_image_preview">Cancelar visualização da imagem</string> - <string name="icon_descr_close_button">Botão Fechar</string> + <string name="icon_descr_close_button">Botão de fechar</string> <string name="clear_verification">Limpar verificação</string> - <string name="app_version_title">Versão do App</string> + <string name="app_version_title">Versão do Aplicativo</string> <string name="bold_text">negrito</string> <string name="callstatus_error">erro de chamada</string> <string name="settings_audio_video_calls">Chamadas de áudio e vídeo</string> <string name="accept_call_on_lock_screen">Aceitar</string> <string name="call_on_lock_screen">Chamadas na tela de bloqueio:</string> <string name="icon_descr_audio_on">Áudio ligado</string> - <string name="chat_database_imported">Banco de dados de chat importado</string> + <string name="chat_database_imported">Banco de dados do bate-papo importado</string> <string name="keychain_is_storing_securely">Android Keystore é usada para armazenar a senha com segurança - permite que o serviço de notificação funcione.</string> <string name="keychain_allows_to_receive_ntfs">A Android Keystore será usada para armazenar a senha com segurança depois que você reiniciar o aplicativo ou alterar a senha - isso permitirá o recebimento de notificações.</string> <string name="cannot_access_keychain">Não é possível acessar a Keystore para salvar a senha do banco de dados</string> - <string name="chat_archive_section">ARQUIVO DE CHAT</string> - <string name="chat_is_stopped_indication">O chat está parado</string> + <string name="chat_is_stopped_indication">O bate-papo está parado</string> <string name="clear_contacts_selection_button">Limpar</string> - <string name="chat_preferences">Preferências de chat</string> - <string name="network_session_mode_user">perfil de chat</string> + <string name="chat_preferences">Preferências de bate-papo</string> + <string name="network_session_mode_user">Perfil de bate-papo</string> <string name="icon_descr_audio_off">Áudio desligado</string> <string name="auto_accept_images">Aceitar imagens automaticamente</string> - <string name="chat_database_deleted">Banco de dados de chat excluído</string> + <string name="chat_database_deleted">Banco de dados do bate-papo excluído</string> <string name="invite_prohibited">Não é possível convidar o contato!</string> <string name="turning_off_service_and_periodic">A otimização da bateria está ativa, desligando o serviço em segundo plano e as solicitações periódicas de novas mensagens. Você pode reativá-los através das configurações.</string> <string name="database_initialization_error_title">Não é possível inicializar o banco de dados</string> <string name="attach">Anexar</string> <string name="cancel_verb">Cancelar</string> - <string name="chat_console">Console de chat</string> + <string name="chat_console">Console de bate-papo</string> <string name="smp_servers_check_address">Verifique o endereço do servidor e tente novamente.</string> - <string name="network_session_mode_user_description"><![CDATA[Uma conexão TCP separada (e credencial SOCKS) será usada <b>para cada perfil de chat que você tiver no aplicativo</b>.]]></string> - <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Melhor para bateria</b>. Você receberá notificações apenas quando o aplicativo estiver em execução (sem segundo plano).]]></string> - <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Consome mais bateria</b>! O serviço em segundo plano está sempre em execução - as notificações são exibidas assim que as mensagens estiverem disponíveis.]]></string> - <string name="settings_section_title_chats">CHATS</string> - <string name="settings_section_title_icon">ÍCONE DO APP</string> - <string name="chat_database_section">BANCO DE DADOS DE CHAT</string> - <string name="chat_is_running">O chat está em execução</string> - <string name="chat_is_stopped">O chat está parado</string> + <string name="network_session_mode_user_description"><![CDATA[Uma conexão TCP separada (e credencial SOCKS) será usada <b>para cada perfil de bate-papo que você tiver no aplicativo</b>.]]></string> + <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Melhor para bateria</b>. Você receberá notificações apenas quando o aplicativo estiver em execução (SEM o serviço em segundo plano).]]></string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Consome mais bateria</b>! O aplicativo em segundo plano está sempre em execução - as notificações são exibidas instantaneamente.]]></string> + <string name="settings_section_title_chats">BATE-PAPOS</string> + <string name="settings_section_title_icon">ÍCONE DO APLICATIVO</string> + <string name="chat_database_section">BANCO DE DADOS DE BATE-PAPO</string> + <string name="chat_is_running">O bate-papo está em execução</string> + <string name="chat_is_stopped">O bate-papo está parado</string> <string name="change_database_passphrase_question">Alterar senha do banco de dados\?</string> - <string name="chat_archive_header">Arquivo de chat</string> <string name="rcv_conn_event_switch_queue_phase_completed">endereço alterado para você</string> <string name="both_you_and_your_contact_can_send_disappearing">Você e seu contato podem enviar mensagens temporárias.</string> <string name="full_backup">Backup de dados do aplicativo</string> @@ -91,17 +87,17 @@ <string name="v4_2_auto_accept_contact_requests">Aceitar solicitações de contato automaticamente</string> <string name="appearance_settings">Aparência</string> <string name="notifications_mode_service_desc">O serviço em segundo plano está sempre em execução - as notificações serão exibidas assim que as mensagens estiverem disponíveis.</string> - <string name="network_session_mode_entity_description">Uma conexão TCP separada (e credencial SOCKS) será usada <b>para cada contato e membro do grupo</b>. -\n<b>Observação</b>: se você tiver muitas conexões, o consumo de bateria e tráfego pode ser substancialmente maior e algumas conexões podem falhar.</string> - <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Bom para bateria</b>. O serviço em segundo plano procura por mensagens a cada 10 minutos. Você pode perder chamadas ou mensagens urgentes.]]></string> + <string name="network_session_mode_entity_description">Uma conexão TCP separada (e credencial SOCKS) será usada <b>para cada contato e membro do grupo</b>. +\n<b>Atenção</b>: se você tiver muitas conexões, o consumo de bateria e tráfego pode ser substancialmente maior e algumas conexões podem falhar.</string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Bom para bateria</b>. O aplicativo procura por mensagens a cada 10 minutos. Você pode perder chamadas ou mensagens urgentes.]]></string> <string name="callstatus_ended">chamda encerrada %1$s</string> - <string name="chat_with_developers">Chat com os desenvolvedores</string> + <string name="chat_with_developers">Converse com os desenvolvedores</string> <string name="create_group_link">Criar link de grupo</string> <string name="button_create_group_link">Criar link</string> <string name="create_secret_group_title">Criar grupo secreto</string> <string name="theme_dark">Escuro</string> - <string name="connect_via_invitation_link">Conectar via link de convite\?</string> - <string name="connect_via_contact_link">Conectar via link de contato\?</string> + <string name="connect_via_invitation_link">Conectar via link de convite único?</string> + <string name="connect_via_contact_link">Conectar via endereço do contato?</string> <string name="smp_server_test_create_queue">Criar fila</string> <string name="notification_preview_mode_contact">Nome de contato</string> <string name="notification_preview_somebody">Contato oculto:</string> @@ -109,10 +105,10 @@ <string name="allow_verb">Permitir</string> <string name="allow_to_send_disappearing">Permitir enviar mensagens temporárias.</string> <string name="allow_direct_messages">Permitir o envio de mensagens diretas aos membros.</string> - <string name="connect_via_link_or_qr">Conectar via link/QR</string> + <string name="connect_via_link_or_qr">Conectar via link/QR code</string> <string name="clear_chat_warning">Todas as mensagens serão excluídas - isso não pode ser desfeito! As mensagens serão excluídas APENAS para você.</string> <string name="smp_servers_preset_add">Adicionar servidores pré-definidos</string> - <string name="smp_servers_add">Adicionar servidor…</string> + <string name="smp_servers_add">Adicionar servidor</string> <string name="create_your_profile">Crie seu perfil</string> <string name="icon_descr_context">Ícone de contexto</string> <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Contato e todas as mensagens serão excluídas - isso não pode ser desfeito!</string> @@ -125,7 +121,7 @@ <string name="status_contact_has_e2e_encryption">Contato tem criptografia e2e</string> <string name="status_contact_has_no_e2e_encryption">contato não tem criptografia e2e</string> <string name="contact_preferences">Preferências de contato</string> - <string name="allow_to_delete_messages">Permite excluir irreversivelmente as mensagens enviadas.</string> + <string name="allow_to_delete_messages">Permite excluir irreversivelmente as mensagens enviadas. (24 horas)</string> <string name="chat_preferences_always">sempre</string> <string name="v4_3_improved_server_configuration_desc">Adicione servidores escaneando o QR code.</string> <string name="allow_to_send_voice">Permitir enviar mensagens de voz.</string> @@ -139,21 +135,21 @@ <string name="users_delete_all_chats_deleted">Todos os chats e mensagens serão excluídos - isso não pode ser desfeito!</string> <string name="accept_feature">Aceitar</string> <string name="allow_disappearing_messages_only_if">Permitir mensagens temporárias apenas se o seu contato permitir.</string> - <string name="allow_irreversible_message_deletion_only_if">Permita a exclusão irreversível da mensagem somente se o seu contato permitir.</string> + <string name="allow_irreversible_message_deletion_only_if">Permita a exclusão irreversível da mensagem somente se o seu contato permitir. (24 horas)</string> <string name="allow_your_contacts_to_send_disappearing_messages">Permitir que seus contatos enviem mensagens temporárias.</string> <string name="allow_voice_messages_only_if">Permitir mensagens de voz somente se o seu contato permitir.</string> <string name="allow_your_contacts_to_send_voice_messages">Permitir que seus contatos enviem mensagens de voz.</string> <string name="group_member_role_admin">administrador</string> <string name="all_group_members_will_remain_connected">Todos os membros do grupo permanecerão conectados.</string> <string name="contacts_can_mark_messages_for_deletion">"Contatos podem marcar mensagens para exclusão; você será capaz de visualizá-los."</string> - <string name="connect_via_group_link">Conectar via link do grupo\?</string> + <string name="connect_via_group_link">Se conectar ao grupo?</string> <string name="contact_already_exists">Contato já existe</string> <string name="icon_descr_contact_checked">Contato verificado</string> <string name="alert_title_contact_connection_pending">Contato ainda não está conectado!</string> <string name="contribute">Contribuir</string> <string name="create_profile_button">Criar</string> <string name="network_enable_socks_info">Acessar os servidores via proxy SOCKS na porta %d\? O proxy deve ser iniciado antes de habilitar esta opção.</string> - <string name="allow_your_contacts_irreversibly_delete">Permitir que seus contatos excluam de forma irreversível as mensagens enviadas.</string> + <string name="allow_your_contacts_irreversibly_delete">Permitir que seus contatos excluam de forma irreversível as mensagens enviadas. (24 horas)</string> <string name="smp_servers_add_to_another_device">Adicionar a outro dispositivo</string> <string name="v4_2_group_links_desc">Os administradores podem criar os links para ingressar em grupos.</string> <string name="allow_voice_messages_question">Permitir mensagens de voz\?</string> @@ -170,7 +166,7 @@ <string name="connect_button">Conectar</string> <string name="callstatus_connecting">conectando chamada…</string> <string name="delete_chat_profile_question">Excluir perfil de chat\?</string> - <string name="delete_files_and_media_for_all_users">Excluir arquivos de todos os perfis de chat</string> + <string name="delete_files_and_media_for_all_users">Excluir arquivos de todos os perfis de bate-papo</string> <string name="display_name_connecting">conectando…</string> <string name="connection_error">Erro de conexão</string> <string name="button_delete_contact">Excluir contato</string> @@ -182,9 +178,7 @@ <string name="confirm_new_passphrase">Confirmar nova senha…</string> <string name="current_passphrase">Senha atual…</string> <string name="database_passphrase_is_required">Senha do banco de dados é necessária para abrir o chat.</string> - <string name="delete_archive">Excluir arquivo</string> - <string name="delete_chat_archive_question">Excluir arquivo de chat\?</string> - <string name="rcv_group_event_changed_member_role">função alterada de %s para %s</string> + <string name="rcv_group_event_changed_member_role">cargo alterado de %s para %s</string> <string name="rcv_group_event_member_connected">conectado</string> <string name="delete_link">Excluir link</string> <string name="delete_link_question">Excluir link\?</string> @@ -195,7 +189,7 @@ <string name="for_me_only">Excluir para mim</string> <string name="group_connection_pending">conectando…</string> <string name="delete_contact_question">Excluir contato\?</string> - <string name="confirm_verb">confirmar</string> + <string name="confirm_verb">Confirmar</string> <string name="database_passphrase_and_export">Senha e exportação do banco de dados</string> <string name="icon_descr_call_connecting">Conectando chamada</string> <string name="delete_messages">Excluir mensagens</string> @@ -209,9 +203,9 @@ <string name="notification_contact_connected">Conectado</string> <string name="icon_descr_server_status_connected">Conectado</string> <string name="audio_call_no_encryption">chamada de áudio (não criptografada ponta-a-ponta)</string> - <string name="change_member_role_question">Alterar a função do grupo\?</string> + <string name="change_member_role_question">Alterar a cargo do grupo?</string> <string name="icon_descr_audio_call">chamada de áudio</string> - <string name="rcv_group_event_changed_your_role">mudou sua função para %s</string> + <string name="rcv_group_event_changed_your_role">mudou sua cargo para %s</string> <string name="v4_4_verify_connection_security_desc">Compare os códigos de segurança com seus contatos.</string> <string name="auth_confirm_credential">Confirme sua credencial</string> <string name="callstate_connecting">conectando…</string> @@ -220,7 +214,6 @@ <string name="connection_error_auth">Erro de conexão (AUTH)</string> <string name="display_name_connection_established">conexão estabelecida</string> <string name="connection_local_display_name">conexão %1$d</string> - <string name="archive_created_on_ts">Criado em %1$s</string> <string name="maximum_supported_file_size">Atualmente, o tamanho máximo de arquivo suportado é %1$s.</string> <string name="delete_verb">Excluir</string> <string name="database_encryption_will_be_updated">Ssnha de criptografia do banco de dados será atualizada e armazenada na Keystore.</string> @@ -244,10 +237,10 @@ <string name="group_member_status_connected">conectado</string> <string name="group_member_status_accepted">conectando (aceito)</string> <string name="ttl_d">%dd</string> - <string name="v4_5_transport_isolation_descr">Por perfil de chat (padrão) ou por conexão (BETA).</string> + <string name="v4_5_transport_isolation_descr">Por perfil de bate-papo (padrão) ou por conexão (BETA).</string> <string name="accept_contact_incognito_button">Aceitar anônimo</string> <string name="delete_messages_after">Excluir mensagens após</string> - <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 desktop: Scaneie o código QR exibido no aplicativo, via <b>Scan QR code</b>]]></string> + <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 desktop: Escaneie o QR code exibido no aplicativo, via <b>Escanear QR code</b>]]></string> <string name="delete_pending_connection__question">Excluir conexão pendente\?</string> <string name="simplex_link_mode_description">Descrição</string> <string name="smp_servers_delete_server">Excluir servidor</string> @@ -256,14 +249,14 @@ <string name="delete_member_message__question">Excluir mensagem do membro\?</string> <string name="smp_server_test_delete_queue">Excluir fila</string> <string name="settings_section_title_device">DISPOSITIVO</string> - <string name="settings_developer_tools">Ferramentas de desenvolvimento</string> + <string name="settings_developer_tools">Ferramentas de desenvolvedor</string> <string name="group_member_status_introduced">conectando (introduzido)</string> - <string name="color_primary">Acento</string> + <string name="color_primary">Tonalidade</string> <string name="error_removing_member">Erro ao remover membro</string> - <string name="error_changing_role">Erro ao alterar função</string> + <string name="error_changing_role">Erro ao alterar cargo</string> <string name="conn_level_desc_direct">direto</string> <string name="server_error">erro</string> - <string name="failed_to_parse_chat_title">Falha ao carregar o chat</string> + <string name="failed_to_parse_chat_title">Falha ao carregar a conversa</string> <string name="error_setting_network_config">Erro ao atualizar a configuração de conexão</string> <string name="error_sending_message">Erro ao enviar mensagem</string> <string name="error_adding_members">Erro ao adicionar membro(s)</string> @@ -276,10 +269,10 @@ <string name="error_receiving_file">Erro ao receber arquivo</string> <string name="error_creating_address">Erro ao criar endereço</string> <string name="display_name__field">Nome de exibição:</string> - <string name="error_starting_chat">Erro ao iniciar o chat</string> + <string name="error_starting_chat">Erro ao iniciar o bate-papo</string> <string name="error_deleting_database">Erro ao excluir banco de dados de chat</string> <string name="encrypt_database">Criptografar</string> - <string name="network_option_enable_tcp_keep_alive">Ativar TCP keep-alive</string> + <string name="network_option_enable_tcp_keep_alive">Ativar TCP manter-vivo</string> <string name="failed_to_create_user_title">Erro ao criar perfil!</string> <string name="error_joining_group">Erro ao ingressar no grupo</string> <string name="failed_to_create_user_duplicate_title">Nome de exibição duplicado!</string> @@ -293,22 +286,22 @@ <string name="error_accepting_contact_request">Erro ao aceitar solicitação de contato</string> <string name="error_deleting_contact_request">Erro ao excluir solicitação de contato</string> <string name="failed_to_active_user_title">Erro ao trocar de perfil!</string> - <string name="auth_disable_simplex_lock">Desativar Bloqueio SimpleX</string> - <string name="auth_enable_simplex_lock">Ativar Bloqueio SimpleX</string> + <string name="auth_disable_simplex_lock">Desativar o bloqueio SimpleX</string> + <string name="auth_enable_simplex_lock">Ativar bloqueio SimpleX</string> <string name="icon_descr_edited">editado</string> <string name="icon_descr_server_status_error">Erro</string> - <string name="icon_descr_email">E-mail</string> + <string name="icon_descr_email">Email</string> <string name="error_saving_ICE_servers">Erro ao salvar servidores ICE</string> <string name="exit_without_saving">Sair sem salvar</string> - <string name="display_name">Nome de exibição</string> + <string name="display_name">Digite o seu nome:</string> <string name="encrypted_video_call">chamada de vídeo criptografada ponta-a-ponta</string> <string name="integrity_msg_duplicate">mensagem duplicada</string> <string name="status_e2e_encrypted">criptografado ponta-a-ponta</string> <string name="export_database">Exportar banco de dados</string> <string name="total_files_count_and_size">%d arquivo(s) com tamanho total de %s</string> - <string name="error_exporting_chat_database">Erro ao exportar banco de chat</string> - <string name="error_importing_database">Erro ao importar banco de dados de chat</string> - <string name="error_stopping_chat">Erro ao interromper o chat</string> + <string name="error_exporting_chat_database">Erro ao exportar banco de dados do bate-papo</string> + <string name="error_importing_database">Erro ao importar banco de dados do bate-papo</string> + <string name="error_stopping_chat">Erro ao interromper o bate-papo</string> <string name="error_changing_message_deletion">Erro ao alterar configuração</string> <string name="error_encrypting_database">Erro ao criptografar o banco de dados</string> <string name="encrypted_database">Banco de dados criptografado</string> @@ -316,11 +309,11 @@ <string name="enter_passphrase">Digite a senha…</string> <string name="error_with_info">Erro: %s</string> <string name="button_edit_group_profile">Editar perfil do grupo</string> - <string name="icon_descr_expand_role">Expandir seleção de função</string> + <string name="icon_descr_expand_role">Expandir seleção de cargo</string> <string name="error_saving_group_profile">Erro ao salvar o perfil do grupo</string> - <string name="direct_messages">DMs</string> - <string name="feature_enabled">habilitado</string> - <string name="feature_enabled_for_contact">habilitado para contato</string> + <string name="direct_messages">Mensagens diretas</string> + <string name="feature_enabled">ativado</string> + <string name="feature_enabled_for_contact">ativado para contato</string> <string name="feature_enabled_for_you">ativado para você</string> <string name="ttl_m">%dm</string> <string name="ttl_min">%d min</string> @@ -330,7 +323,7 @@ <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">A autenticação do dispositivo não está habilitada. Você pode ativar o Bloqueio SimpleX em Configurações, depois de ativar a autenticação do dispositivo.</string> <string name="no_call_on_lock_screen">Desativar</string> <string name="icon_descr_server_status_disconnected">Desconectado</string> - <string name="disappearing_messages_are_prohibited">Mensagens que temporárias são proibidas neste grupo.</string> + <string name="disappearing_messages_are_prohibited">Mensagens temporárias são proibidas.</string> <string name="error_saving_file">Erro ao salvar arquivo</string> <string name="display_name_cannot_contain_whitespace">O nome de exibição não pode conter espaços em branco.</string> <string name="encrypted_audio_call">chamada de áudio criptografada ponta-a-ponta</string> @@ -340,7 +333,7 @@ <string name="settings_experimental_features">Funcionalidades experimentais</string> <string name="error_creating_link_for_group">Erro ao criar o link de grupo</string> <string name="error_deleting_link_for_group">Erro ao excluir o link de grupo</string> - <string name="direct_messages_are_prohibited_in_chat">Mensagens diretas entre membros são proibidas neste grupo.</string> + <string name="direct_messages_are_prohibited_in_group">Mensagens diretas entre membros são proibidas neste grupo.</string> <string name="ttl_h">%dh</string> <string name="ttl_hours">%d horas</string> <string name="description_via_contact_address_link_incognito">anônimo via link de endereço de contato</string> @@ -348,7 +341,7 @@ <string name="hide_verb">Ocultar</string> <string name="from_gallery_button">Da Galeria</string> <string name="gallery_video_button">Vídeo</string> - <string name="group_members_can_send_disappearing">Os membros do grupo podem enviar mensagens temporárias.</string> + <string name="group_members_can_send_disappearing">Os membros podem enviar mensagens temporárias.</string> <string name="icon_descr_file">Arquivo</string> <string name="full_name__field">Nome completo:</string> <string name="incoming_audio_call">Chamada de áudio recebida</string> @@ -365,68 +358,67 @@ <string name="conn_level_desc_indirect">indireto (%1$s)</string> <string name="incognito">Anônimo</string> <string name="timed_messages">Mensagens que desaparecem</string> - <string name="group_preferences">Preferências de grupo</string> - <string name="disappearing_prohibited_in_this_chat">Mensagens temporárias são proibidas nesse chat.</string> - <string name="group_members_can_send_dms">Os membros do grupo podem enviar DMs.</string> + <string name="group_preferences">Preferências do grupo</string> + <string name="disappearing_prohibited_in_this_chat">Mensagens temporárias são proibidas nesse bate-papo.</string> + <string name="group_members_can_send_dms">Os membros podem enviar mensagens diretas.</string> <string name="ttl_mth">%dmês</string> <string name="simplex_link_mode_full">Link completo</string> <string name="hide_notification">Ocultar</string> <string name="auth_device_authentication_is_disabled_turning_off">A autenticação do dispositivo está desativada. Desativando o bloqueio SimpleX.</string> <string name="for_everybody">Para todos</string> <string name="notification_preview_mode_hidden">Oculto</string> - <string name="create_one_time_link">Gerar um link de convite de uso único.</string> <string name="how_to_use_your_servers">Como usar seus servidores</string> <string name="import_database_confirmation">Importar</string> - <string name="import_database_question">Importar banco de dados de chat\?</string> - <string name="group_display_name_field">Nome de exibição do grupo:</string> + <string name="import_database_question">Importar banco de dados de bate-papo?</string> + <string name="group_display_name_field">Digite o nome de exibição do grupo:</string> <string name="group_full_name_field">Nome completo do grupo:</string> <string name="v4_2_group_links">Links de grupo</string> <string name="v4_3_improved_privacy_and_security">Privacidade e segurança aprimoradas</string> - <string name="failed_to_parse_chats_title">Falha ao carregar chats</string> + <string name="failed_to_parse_chats_title">Falha ao carregar as conversas</string> <string name="file_with_path">Arquivo: %s</string> <string name="file_saved">Arquivo salvo</string> - <string name="group_members_can_send_voice">Os membros do grupo podem enviar mensagens de voz.</string> + <string name="group_members_can_send_voice">Os membros podem enviar mensagens de voz.</string> <string name="delete_group_for_all_members_cannot_undo_warning">O grupo será excluído para todos os membros - isso não pode ser desfeito!</string> <string name="settings_section_title_help">AJUDA</string> <string name="notification_display_mode_hidden_desc">Ocultar contato e mensagem</string> <string name="how_to_use_simplex_chat">Como usar</string> <string name="how_to_use_markdown">Como usar markdown</string> - <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Se você não puder se encontrar pessoalmente, <b>mostre o código QR na videochamada</b> ou compartilhe o link.]]></string> - <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Se você não puder encontrar pessoalmente, você pode <b>escanear o código QR na videochamada</b> ou seu contato pode compartilhar um link de convite.]]></string> + <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Se você não puder se encontrar pessoalmente, <b>mostre o QR code na video chamada</b> ou compartilhe o link.]]></string> + <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Se você não puder encontrar pessoalmente, você pode <b>escanear o QR code na video chamada</b> ou seu contato pode compartilhar um link de convite.]]></string> <string name="network_disable_socks_info">Se você confirmar, os servidores de mensagens poderão ver seu endereço IP e seu provedor - e quais servidores você está se conectando.</string> <string name="image_descr">Imagem</string> <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Se você recebeu o link de convite SimpleX Chat, você pode abri-lo em seu navegador:</string> - <string name="image_saved">Imagem salva na galeria</string> + <string name="image_saved">Imagem salva na Galeria</string> <string name="description_via_group_link_incognito">anônimo via link do grupo</string> <string name="incoming_video_call">Chamada de vídeo recebida</string> - <string name="turn_off_battery_optimization"><![CDATA[Para usá-lo, por favor <b>desative a otimização da bateria</b> para SimpleX na próxima caixa de diálogo. Caso contrário, as notificações serão desativadas.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[<b>Permita</b> na próxima caixa de diálogo para receber notificações instantaneamente]]></string> <string name="share_one_time_link">Gerar um link de convite de uso único</string> <string name="file_not_found">Arquivo não encontrado</string> <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Se você optar por rejeitar o remetente NÃO será notificado.</string> <string name="incorrect_code">Código de segurança incorreto!</string> <string name="install_simplex_chat_for_terminal">Instale o SimpleX para terminal</string> <string name="how_it_works">Como funciona</string> - <string name="immune_to_spam_and_abuse">Imune a spam e abuso</string> + <string name="immune_to_spam_and_abuse">Imune a spam</string> <string name="icon_descr_flip_camera">Vire a câmera</string> <string name="icon_descr_hang_up">Desligar</string> <string name="settings_section_title_incognito">Modo anônimo</string> - <string name="initial_member_role">Função inicial</string> + <string name="initial_member_role">Cargo inicial</string> <string name="snd_group_event_group_profile_updated">perfil do grupo atualizado</string> <string name="group_member_status_group_deleted">Grupo excluído</string> <string name="incognito_info_protects">O modo Incognito protege sua privacidade usando um novo perfil aleatório para cada contato.</string> - <string name="group_members_can_delete">Os membros do grupo podem excluir mensagens enviadas de forma irreversível.</string> + <string name="group_members_can_delete">Os membros grupo podem excluir mensagens enviadas de forma irreversível. (24 horas)</string> <string name="ttl_w">%dsemana</string> <string name="v4_3_improved_server_configuration">Configuração de servidor aprimorada</string> <string name="v4_4_french_interface">Interface francesa</string> <string name="callstate_ended">terminou</string> - <string name="allow_accepting_calls_from_lock_screen">Ative as chamadas pela tela de bloqueio nas Configurações.</string> + <string name="allow_accepting_calls_from_lock_screen">Ative as chamadas pela tela de bloqueio nas configurações.</string> <string name="files_and_media_section">Arquivos & mídia</string> <string name="error_updating_link_for_group">Erro ao atualizar o link do grupo</string> <string name="group_invitation_expired">O convite do grupo expirou</string> <string name="file_will_be_received_when_contact_is_online">O arquivo será recebido quando seu contato estiver online, aguarde ou verifique mais tarde!</string> <string name="group_profile_is_stored_on_members_devices">O perfil do grupo é armazenado nos dispositivos dos membros, não nos servidores.</string> <string name="icon_descr_help">ajuda</string> - <string name="how_simplex_works">Como SimpleX funciona</string> + <string name="how_simplex_works">Como o SimpleX funciona</string> <string name="enter_one_ICE_server_per_line">Servidores ICE (um por linha)</string> <string name="ignore">Ignorar</string> <string name="image_will_be_received_when_contact_is_online">A imagem será recebida quando seu contato estiver online, aguarde ou verifique mais tarde!</string> @@ -434,61 +426,58 @@ <string name="receiving_via">Recebendo via</string> <string name="network_status">Status da conexão</string> <string name="network_option_seconds_label">seg</string> - <string name="incognito_info_allows">Permite ter várias conexões anônimas sem nenhum dado compartilhado entre elas em um único perfil de chat.</string> + <string name="incognito_info_allows">Permite ter várias conexões anônimas sem nenhum dado compartilhado entre elas em um único perfil de bate-papo.</string> <string name="chat_preferences_yes">Sim</string> <string name="profile_will_be_sent_to_contact_sending_link">Seu perfil será enviado para o contato do qual você recebeu esse link.</string> <string name="accept_feature_set_1_day">Definir 1 dia</string> <string name="thousand_abbreviation">k</string> - <string name="you_will_join_group">Você se juntará a um grupo ao qual este link se refere e se conectará aos membros do seu grupo.</string> + <string name="you_will_join_group">Você se conectará a todos os membros do grupo.</string> <string name="marked_deleted_description">marcado como excluído</string> <string name="sending_files_not_yet_supported">o envio de arquivos ainda não é suportado</string> <string name="prohibit_sending_voice_messages">Proibir o envio de mensagens de voz.</string> <string name="display_name_invited_to_connect">convidado à conectar</string> <string name="description_you_shared_one_time_link_incognito">você compartilhou um link anônimo de uso único</string> <string name="simplex_link_mode">Links SimpleX</string> - <string name="contact_developers">Por favor, atualize o app e contate os desenvolvedores .</string> + <string name="contact_developers">Por favor, atualize o app e contate os desenvolvedores.</string> <string name="error_smp_test_server_auth">O servidor requer autorização para criar filas, verifique a senha</string> <string name="reveal_verb">Revelar</string> <string name="icon_descr_server_status_pending">Pendente</string> - <string name="prohibit_direct_messages">Proibir o envio de DMs para membros.</string> + <string name="prohibit_direct_messages">Proibir o envio de mensagens diretas para membros.</string> <string name="scan_QR_code">Escanear QR Code</string> <string name="reject_contact_button">Rejeitar</string> <string name="feature_offered_item">ofereceu %s</string> <string name="icon_descr_address">Endereço SimpleX</string> <string name="feature_offered_item_with_param">ofereceu %s: %2s</string> <string name="new_in_version">Novo em %s</string> - <string name="send_us_an_email">Envie-nos um e-mail</string> + <string name="send_us_an_email">Envie-nos um email</string> <string name="smp_servers_scan_qr">Escanear QR code do servidor</string> <string name="your_SMP_servers">Seus servidores SMP</string> <string name="chat_item_ttl_none">nunca</string> <string name="restore_database_alert_title">Restaurar o backup do banco de dados\?</string> <string name="moderated_description">moderado</string> <string name="sender_cancelled_file_transfer">O remetente cancelou a transferência de arquivos.</string> - <string name="network_error_desc">Por favor, cheque sua conexão com a rede com%1$s e tente de novo.</string> + <string name="network_error_desc">Por favor, cheque sua conexão com a rede com %1$s e tente de novo.</string> <string name="simplex_service_notification_text">Recebendo mensagens…</string> - <string name="large_file">Aruivo grande!</string> + <string name="large_file">Arquivo grande!</string> <string name="mark_read">Marcado como lido</string> <string name="you_invited_a_contact">Você convidou seu contato</string> - <string name="invalid_QR_code">Código QR inválido</string> + <string name="invalid_QR_code">QR code inválido</string> <string name="icon_descr_more_button">Mais</string> <string name="you_will_be_connected_when_group_host_device_is_online">Você será conectado ao grupo quando o dispositivo do host do grupo estiver online, por favor aguarde ou verifique mais tarde!</string> <string name="this_string_is_not_a_connection_link">Essa string não é um link de conexão!</string> <string name="network_use_onion_hosts_prefer">Quando disponível</string> - <string name="app_version_code">Build do app: %s</string> + <string name="app_version_code">Compilação do aplicativo: %s</string> <string name="save_and_notify_contact">Salvar e notificar contato</string> <string name="callstate_received_answer">resposta recebida…</string> - <string name="read_more_in_github">Leia mais no nosso repositório do GitHub.</string> - <string name="paste_the_link_you_received">Colar link recebido</string> - <string name="onboarding_notifications_mode_off">Quando o app está executando</string> + <string name="paste_the_link_you_received">Cole o link que você recebeu</string> + <string name="onboarding_notifications_mode_off">Quando o aplicativo está em execução</string> <string name="onboarding_notifications_mode_periodic">Periódico</string> <string name="your_calls">Suas chamadas</string> <string name="old_database_archive">Arquivo de banco de dados antigo</string> <string name="button_add_members">Convidar membros</string> <string name="no_contacts_selected">Nenhum contato selecionado</string> - <string name="network_options_revert">Reverter</string> <string name="network_options_save">Salvar</string> <string name="reset_color">Redefinir cores</string> - <string name="save_color">Salvar cor</string> <string name="v4_5_italian_interface">interface italiana</string> <string name="periodic_notifications">Notificações periódicas</string> <string name="use_camera_button">Câmera</string> @@ -497,9 +486,9 @@ <string name="your_privacy">Sua privacidade</string> <string name="join_group_question">Juntar-se ao grupo\?</string> <string name="leave_group_button">Sair</string> - <string name="failed_to_create_user_duplicate_desc">Você já possui um perfil de chat com o mesmo nome. Por favor escolha outro nome.</string> + <string name="failed_to_create_user_duplicate_desc">Você já possui um perfil de bate-papo com o mesmo nome. Por favor escolha outro nome.</string> <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Por favor, cheque se você usou o link correto ou peça ao seu contato para enviar outro.</string> - <string name="auth_open_chat_console">Abrir console de chat</string> + <string name="auth_open_chat_console">Abrir console de bate-papo</string> <string name="group_preview_join_as">juntar-se como %s</string> <string name="ask_your_contact_to_enable_voice">Por favor, peça ao seu contato para ativar o envio de mensagens de voz.</string> <string name="ok">OK</string> @@ -509,25 +498,25 @@ <string name="icon_descr_call_rejected">Chamada rejeitada</string> <string name="restore_database">Restaurar o backup do banco de dados</string> <string name="section_title_for_console">PARA CONSOLE</string> - <string name="run_chat_section">EXECUTAR CHAT</string> + <string name="run_chat_section">EXECUTAR BATE-PAPO</string> <string name="stop_chat_confirmation">Parar</string> <string name="set_password_to_export">Definir senha para exportar</string> <string name="restart_the_app_to_use_imported_chat_database">Reinicie o aplicativo para usar o banco de dados do chat importado.</string> - <string name="restart_the_app_to_create_a_new_chat_profile">Reinicie o app para criar um novo perfil de chat.</string> + <string name="restart_the_app_to_create_a_new_chat_profile">Reinicie o aplicativo para criar um novo perfil de chat.</string> <string name="delete_files_and_media_desc">Essa ação não pode ser desfeita - todos os arquivos e mídias recebidos e enviados serão excluídos. Imagens de baixa resolução permanecerão.</string> - <string name="you_must_use_the_most_recent_version_of_database">Você deve usar a versão mais recente de seu banco de dados de chat SOMENTE em um dispositivo, caso contrário, você pode parar de receber as mensagens de alguns contatos.</string> + <string name="you_must_use_the_most_recent_version_of_database">Você deve usar a versão mais recente de seu banco de dados de bate-papo SOMENTE em um dispositivo, caso contrário, você pode parar de receber as mensagens de alguns contatos.</string> <string name="no_received_app_files">Sem arquivos enviados ou recebidos</string> <string name="messages_section_title">Mensagens</string> <string name="messages_section_description">Esta configuração aplica-se às mensagens no seu perfil de chat atual</string> <string name="keychain_error">erro na Keychain</string> - <string name="store_passphrase_securely_without_recover">Por favor, guarde a senha em um local seguro, você não poderá acessar o chat se perdê-lo.</string> + <string name="store_passphrase_securely_without_recover">Por favor, guarde a senha em um local seguro, você não poderá acessar o bate-papo se perdê-lo.</string> <string name="store_passphrase_securely">Guarde a senha em um local seguro, você NÃO poderá alterá-la se a perder.</string> <string name="restore_database_alert_confirm">Restaurar</string> <string name="restore_database_alert_desc">Por favor, digite a senha antiga depois de recuperar o backup do banco de dados. Essa ação não pode ser desfeita.</string> <string name="wrong_passphrase_title">Senha incorreta</string> - <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Você pode iniciar o chat via Configurações / Banco de dados ou reiniciando o aplicativo.</string> + <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Você pode iniciar o bate-papo via Configurações / Banco de dados ou reiniciando o aplicativo.</string> <string name="alert_title_group_invitation_expired">Convite expirado!</string> - <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Você deixará de receber mensagens deste grupo. O histórico do chat será preservado.</string> + <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Você deixará de receber mensagens deste grupo. O histórico do bate-papo será preservado.</string> <string name="rcv_group_event_user_deleted">removeu você</string> <string name="rcv_group_event_member_added">convidado%1$s</string> <string name="snd_conn_event_switch_queue_phase_completed_for_member">você alterou o endereço para %s</string> @@ -535,7 +524,7 @@ <string name="group_member_role_member">membro</string> <string name="button_leave_group">Sair do grupo</string> <string name="group_info_section_title_num_members">%1$s MEMBROS</string> - <string name="group_info_member_you">você%1$s</string> + <string name="group_info_member_you">você: %1$s</string> <string name="info_row_local_name">Nome local</string> <string name="save_group_profile">Salvar perfil do grupo</string> <string name="network_option_ping_interval">Intervalo de PING</string> @@ -544,8 +533,8 @@ <string name="users_delete_with_connections">Conexões de servidor e perfil</string> <string name="your_preferences">Suas preferências</string> <string name="set_group_preferences">Definir preferências de grupo</string> - <string name="only_you_can_delete_messages">Somente você pode excluir irreversivelmente as mensagens (seu contato pode marcá-las para exclusão).</string> - <string name="message_deletion_prohibited_in_chat">A exclusão irreversível de mensagens é proibida neste grupo.</string> + <string name="only_you_can_delete_messages">Somente você pode excluir irreversivelmente as mensagens (seu contato pode marcá-las para exclusão). (24 horas)</string> + <string name="message_deletion_prohibited_in_chat">A exclusão irreversível de mensagens é proibida.</string> <string name="v4_4_live_messages_desc">Os destinatários vêem as atualizações conforme você as digita.</string> <string name="v4_5_reduced_battery_usage">Uso da bateria reduzido</string> <string name="v4_5_reduced_battery_usage_descr">Mais melhorias chegarão em breve!</string> @@ -556,18 +545,18 @@ <string name="invalid_message_format">formato de mensagem inválido</string> <string name="live">AO VIVO</string> <string name="moderated_item_description">moderado por %s</string> - <string name="invalid_chat">chat inválido</string> + <string name="invalid_chat">conversa inválida</string> <string name="simplex_link_mode_browser_warning">Abrir o link no navegador pode reduzir a privacidade e a segurança da conexão. Links SimpleX não confiáveis ficarão vermelhos.</string> <string name="invalid_connection_link">Link de conexão inválido</string> <string name="ensure_smp_server_address_are_correct_format_and_unique">Certifique-se de que os endereços do servidor SMP estejam no formato correto, separados por linhas e não estejam duplicados.</string> <string name="sender_may_have_deleted_the_connection_request">O remetente pode ter excluído a solicitação de conexão.</string> <string name="periodic_notifications_disabled">Notificações periódicas estão desativadas!</string> <string name="service_notifications_disabled">As notificações instantâneas estão desativadas!</string> - <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Para preservar sua privacidade, em vez de notificações push, o aplicativo tem um<b>SimpleXserviço em segundo plano</b> - ele usa uma porcentagem da bateria por dia.]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Para melhorar a privacidade, <b>SimpleX executa em segundo plano</b> em vez de usar notificações push.]]></string> <string name="notifications_mode_off">Executa quando o aplicativo está aberto</string> <string name="icon_descr_sent_msg_status_sent">enviado</string> <string name="icon_descr_sent_msg_status_send_failed">o envio falhou</string> - <string name="your_chats">Chats</string> + <string name="your_chats">Conversas</string> <string name="paste_button">Colar</string> <string name="one_time_link">Link de convite de uso único</string> <string name="chat_with_the_founder">Enviar perguntas e idéias</string> @@ -580,16 +569,16 @@ <string name="join_group_incognito_button">Entrar como anônimo</string> <string name="icon_descr_add_members">Convidar membros</string> <string name="rcv_group_event_invited_via_your_group_link">convidado via seu link de grupo</string> - <string name="snd_group_event_changed_role_for_yourself">você alterou sua função para %s</string> - <string name="snd_group_event_changed_member_role">você mudou a função de %s para %s</string> - <string name="new_member_role">Nova função de membro</string> + <string name="snd_group_event_changed_role_for_yourself">você alterou seu cargo para %s</string> + <string name="snd_group_event_changed_member_role">você mudou o cargo de %s para %s</string> + <string name="new_member_role">Novo cargo de membro</string> <string name="remove_member_confirmation">Remover</string> <string name="member_info_section_title_member">MEMBRO</string> <string name="member_will_be_removed_from_group_cannot_be_undone">O membro será removido do grupo - isso não pode ser desfeito!</string> - <string name="role_in_group">Função</string> + <string name="role_in_group">Cargo</string> <string name="sending_via">Enviando via</string> <string name="prohibit_sending_disappearing_messages">Proibir o envio de mensagens temporárias.</string> - <string name="message_deletion_prohibited">A exclusão irreversível de mensagens é proibida neste chat.</string> + <string name="message_deletion_prohibited">A exclusão irreversível de mensagens é proibida neste bate-papo.</string> <string name="prohibit_sending_voice">Proibir o envio de mensagens de voz.</string> <string name="v4_3_voice_messages_desc">No máximo 40 segundos, recebido instantaneamemte.</string> <string name="observer_cant_send_message_title">Você não pode enviar mensagens!</string> @@ -597,14 +586,13 @@ <string name="markdown_in_messages">Markdown em mensagens</string> <string name="smp_servers">Servidores SMP</string> <string name="smp_servers_preset_address">Endereço do servidor pré-definido</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[Muitos perguntaram:<i> se SimpleX não tem identificadores de usuários, como ele pode mandar mensagens\?</i>]]></string> <string name="reject">Rejeitar</string> - <string name="integrity_msg_skipped">%1$d mensagens ignoradas</string> - <string name="protect_app_screen">Proteger a tela do app</string> + <string name="integrity_msg_skipped">%1$d mensagem(ens) ignorada(s)</string> + <string name="protect_app_screen">Proteger a tela do aplicativo</string> <string name="send_link_previews">Enviar prévias de links</string> <string name="privacy_and_security">Privacidade e segurança</string> <string name="join_group_button">Junte-se</string> - <string name="you_can_share_group_link_anybody_will_be_able_to_connect">Você pode compartilhar um link ou um código QR - qualquer um poderá entrar no grupo. Você não perderá membros do grupo se você deletá-los mais tarde.</string> + <string name="you_can_share_group_link_anybody_will_be_able_to_connect">Você pode compartilhar um link ou um QR code - qualquer um poderá entrar no grupo. Você não perderá membros do grupo se você deletá-los mais tarde.</string> <string name="users_delete_data_only">Somente dados de perfil local</string> <string name="only_you_can_send_disappearing">Somente você pode enviar mensagens temporárias.</string> <string name="only_your_contact_can_send_disappearing">Somente seu contato pode enviar mensagens temporárias.</string> @@ -621,7 +609,7 @@ <string name="reply_verb">Responder</string> <string name="auth_log_in_using_credential">Inicie sessão com a sua credencial</string> <string name="message_delivery_error_title">Erro na entrega da mensagem</string> - <string name="message_delivery_error_desc">Porvavelmente esse contato excluiu a conexão com você.</string> + <string name="message_delivery_error_desc">Provavelmente esse contato excluiu a conexão com você.</string> <string name="save_verb">Salvar</string> <string name="delete_message_cannot_be_undone_warning">A mensagem será excluída - isso não pode ser desfeito!</string> <string name="delete_message_mark_deleted_warning">A mensagem será marcada para exclusão. O(s) destinatário(s) poderá(ão) revelar esta mensagem.</string> @@ -630,7 +618,7 @@ <string name="images_limit_desc">Apenas 10 imagens podem ser enviadas ao mesmo tempo</string> <string name="notifications">Notificações</string> <string name="text_field_set_contact_placeholder">Definir nome do contato…</string> - <string name="switch_receiving_address_desc">O endereço de recebimento será alterado para um servidor diferente. A mudança de endereço terminará após o remetente entrar on-line.</string> + <string name="switch_receiving_address_desc">O endereço de recebimento será alterado para um servidor diferente. A mudança de endereço terminará após o remetente ficar online.</string> <string name="send_verb">Enviar</string> <string name="reset_verb">Redefinir</string> <string name="live_message">Mensagem ao vivo!</string> @@ -642,18 +630,18 @@ <string name="mute_chat">Mutar</string> <string name="set_contact_name">Definir nome do contato</string> <string name="invalid_contact_link">Link inválido!</string> - <string name="this_QR_code_is_not_a_link">Esse código QR não é um link!</string> - <string name="your_chat_profile_will_be_sent_to_your_contact">Seu perfil de chat será enviado para seu -\ncontato</string> + <string name="this_QR_code_is_not_a_link">Esse QR code não é um link!</string> + <string name="your_chat_profile_will_be_sent_to_your_contact">Seu perfil de bate-papo será enviado +\npara seu contato</string> <string name="you_will_be_connected_when_your_contacts_device_is_online">Você será conectado quando o dispositivo do seu contato estiver online, aguarde ou verifique mais tarde!</string> <string name="how_to">Como</string> <string name="smp_servers_test_failed">Teste do servidor falhou!</string> <string name="network_use_onion_hosts_no_desc">Onion hosts não serão usados.</string> - <string name="network_use_onion_hosts_required_desc">Os hosts Onion serão necessários para a conexão.</string> - <string name="network_use_onion_hosts_required_desc_in_alert">Os hosts Onion serão necessários para a conexão.</string> + <string name="network_use_onion_hosts_required_desc">Os hosts Onion serão necessários para a conexão. +\nAtenção: você não será capaz de se conectar aos servidores sem um endereço .onion</string> <string name="core_version">Versão principal: v%s</string> - <string name="read_more_in_github_with_link"><![CDATA[Leia mais no nosso<font color="#0088ff"> repositório do GitHub</font>.]]></string> - <string name="onboarding_notifications_mode_subtitle">Pode ser mudado mais tarde via configurações.</string> + <string name="read_more_in_github_with_link"><![CDATA[Leia mais no nosso <font color="#0088ff">repositório do GitHub</font>.]]></string> + <string name="onboarding_notifications_mode_subtitle">Como isso afeta a bateria</string> <string name="contact_wants_to_connect_via_call">%1$s quer se conectar com você via</string> <string name="status_no_e2e_encryption">sem criptografia ponta-a-ponta</string> <string name="open_verb">Abrir</string> @@ -665,21 +653,21 @@ <string name="remove_passphrase_from_keychain">Remover senha da Keystore\?</string> <string name="new_passphrase">Nova senha…</string> <string name="database_restore_error">Erro na restauração do banco de dados</string> - <string name="open_chat">Abrir chat</string> - <string name="save_passphrase_and_open_chat">Salvar senha e abrir chat</string> - <string name="rcv_group_event_member_deleted">removido%1$s</string> + <string name="open_chat">Abrir bate-papo</string> + <string name="save_passphrase_and_open_chat">Salvar senha e abrir bate-papo</string> + <string name="rcv_group_event_member_deleted">removido %1$s</string> <string name="network_option_protocol_timeout">Tempo limite do protocolo</string> <string name="feature_received_prohibited">recebido, proibido</string> <string name="user_hide">Ocultar</string> <string name="enter_password_to_show">Digite a senha na pesquisa</string> <string name="dont_show_again">Não mostrar novamente</string> - <string name="only_your_contact_can_delete">Somente seu contato pode excluir irreversivelmente mensagens (você pode marcá-las para exclusão).</string> + <string name="only_your_contact_can_delete">Somente seu contato pode excluir irreversivelmente mensagens (você pode marcá-las para exclusão). (24 horas)</string> <string name="v4_6_chinese_spanish_interface">Interface chinesa e espanhola</string> <string name="v4_6_reduced_battery_usage">Maior redução no uso da bateria</string> <string name="v4_6_reduced_battery_usage_descr">Mais melhorias chegarão em breve!</string> <string name="settings_section_title_you">VOCÊ</string> <string name="settings_section_title_messages">MENSAGENS E ARQUIVOS</string> - <string name="your_chat_database">Seu banco de dados de chat</string> + <string name="your_chat_database">Seu banco de dados de bate-papo</string> <string name="snd_group_event_member_deleted">Você removeu %1$s</string> <string name="group_member_status_removed">removido</string> <string name="group_member_status_left">saiu</string> @@ -690,8 +678,7 @@ <string name="delete_chat_profile_action_cannot_be_undone_warning">Esta ação não pode ser desfeita - seu perfil, contatos, mensagens e arquivos serão irreversivelmente perdidos.</string> <string name="remove_passphrase">Remover</string> <string name="wrong_passphrase">Senha do banco de dados incorreta</string> - <string name="save_archive">Salvar arquivo</string> - <string name="restore_passphrase_not_found_desc">Senha não encontrada na Keystore, por favor digite-a manualmente. Isso pode ter ocorrido se você recuperou os dados do app usando uma ferramenta de backup. Se esse não é o caso, por favor contate os desenvolvedores.</string> + <string name="restore_passphrase_not_found_desc">Senha não encontrada na Keystore, por favor digite-a manualmente. Isso pode ter ocorrido se você recuperou os dados do app usando uma ferramenta de backup. Se esse não é o caso, por favor, contate os desenvolvedores.</string> <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Você se juntou a este grupo. Conectando-se a um membro convidado do grupo.</string> <string name="leave_group_question">Sair do grupo\?</string> <string name="alert_message_no_group">Este grupo não existe mais.</string> @@ -700,16 +687,15 @@ <string name="chat_preferences_no">não</string> <string name="v4_6_group_moderation_descr">Agora administradores podem: \n- excluir mensagens de membros. -\n- desativar membros (função de \"observador\")</string> +\n- desativar membros (cargo de "observador")</string> <string name="v4_6_group_moderation">Moderação do grupo</string> <string name="v4_6_group_welcome_message">Mensagem de boas-vindas do grupo</string> <string name="database_downgrade">Desatualizar banco de dados</string> <string name="mtr_error_different">migração diferente no aplicativo/banco de dados: %s / %s</string> <string name="invite_to_group_button">Convidar para o grupo</string> <string name="no_contacts_to_add">Sem contatos para adicionar</string> - <string name="member_role_will_be_changed_with_notification">A função será alterada para \"%s\". Todos no grupo serão notificados.</string> + <string name="member_role_will_be_changed_with_notification">O cargo será alterada para "%s". Todos no grupo serão notificados.</string> <string name="user_mute">Mutar</string> - <string name="should_be_at_least_one_visible_profile">Deve haver pelo menos um perfil de usuário visível.</string> <string name="only_you_can_send_voice">Somente você pode enviar mensagens de voz.</string> <string name="only_your_contact_can_send_voice">Somente seu contato pode enviar mensagens de voz.</string> <string name="prohibit_message_deletion">Proibir a exclusão irreversível de mensagens.</string> @@ -717,7 +703,7 @@ <string name="v4_3_irreversible_message_deletion">Exclusão irreversível de mensagens</string> <string name="v4_4_disappearing_messages_desc">Mensagens enviadas serão excluídas depois do tempo definido.</string> <string name="v4_4_live_messages">Mensagens ao vivo</string> - <string name="v4_5_multiple_chat_profiles">Vários perfis de chat</string> + <string name="v4_5_multiple_chat_profiles">Vários perfis de bate-papo</string> <string name="v4_6_hidden_chat_profiles">Perfis de chat ocultos</string> <string name="v4_5_message_draft">Rascunho de mensagem</string> <string name="v4_5_message_draft_descr">Preservar o último rascunho, com anexos.</string> @@ -730,7 +716,7 @@ <string name="to_verify_compare">Para verificar a criptografia de ponta-a-ponta com seu contato, compare (ou escaneie) o código em seus dispositivos.</string> <string name="smp_save_servers_question">Salvar servidores\?</string> <string name="smp_servers_per_user">Os servidores para novas conexões do seu perfil de chat atual</string> - <string name="rate_the_app">Avalie o app</string> + <string name="rate_the_app">Avalie o aplicativo</string> <string name="developer_options">IDs de banco de dados e opção de isolamento de transporte.</string> <string name="profile_is_only_shared_with_your_contacts">O perfil é compartilhado apenas com seus contatos.</string> <string name="secret_text">segredo</string> @@ -741,10 +727,8 @@ <string name="rcv_group_event_member_left">saiu</string> <string name="incompatible_database_version">Versão do banco de dados incompatível</string> <string name="button_remove_member">Remover membro</string> - <string name="group_main_profile_sent">Seu perfil de chat será enviado aos membros do grupo</string> - <string name="cant_delete_user_profile">Não é possível excluir o perfil do usuário!</string> + <string name="group_main_profile_sent">Seu perfil de bate-papo será enviado aos membros do grupo</string> <string name="make_profile_private">Torne o perfil privado!</string> - <string name="should_be_at_least_one_profile">Deve haver pelo menos um perfil de usuário.</string> <string name="v4_2_security_assessment">Avaliação de segurança</string> <string name="v4_5_multiple_chat_profiles_descr">Nomes diferentes, avatares e isolamento de transporte.</string> <string name="v4_2_auto_accept_contact_requests_desc">Com mensagem de boas-vindas opcional.</string> @@ -754,7 +738,7 @@ <string name="save_profile_password">Salvar senha do perfil</string> <string name="icon_descr_call_missed">Chamada perdida</string> <string name="save_passphrase_in_keychain">Salvar senha na Keystore</string> - <string name="database_is_not_encrypted">Seu banco de dados de chat não está criptografado - defina uma senha para protegê-lo.</string> + <string name="database_is_not_encrypted">Seu banco de dados de bate-papo não está criptografado - defina uma senha para protegê-lo.</string> <string name="group_invitation_item_description">convite para o grupo%1$s</string> <string name="alert_title_cant_invite_contacts_descr">Você esta usando um perfil anônimo para este grupo - para evitar compartilhar seu perfil principal, convidar contatos não é permitido</string> <string name="invalid_migration_confirmation">Confirmação de migração inválida</string> @@ -764,17 +748,15 @@ <string name="snd_group_event_user_left">você saiu</string> <string name="whats_new">Novidades</string> <string name="v4_6_audio_video_calls">Chamadas de áudio e vídeo</string> - <string name="v4_6_hidden_chat_profiles_descr">Proteja seus perfis de chat com uma senha!</string> + <string name="v4_6_hidden_chat_profiles_descr">Proteja seus perfis de bate-papo com uma senha!</string> <string name="this_text_is_available_in_settings">Este texto está disponível nas configurações</string> <string name="scan_code">Escanear código</string> - <string name="network_use_onion_hosts_no_desc_in_alert">Hosts Onion não serão usados.</string> <string name="network_use_onion_hosts_prefer_desc">Os hosts Onion serão usados quando disponíveis.</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">Os hosts Onion serão usados quando disponíveis.</string> <string name="your_current_profile">Seu perfil atual</string> <string name="privacy_redefined">Privacidade redefinida</string> <string name="onboarding_notifications_mode_title">Notificações privadas</string> <string name="make_private_connection">Fazer uma conexão privada</string> - <string name="people_can_connect_only_via_links_you_share">Pessoas podem se conectar com você somente via links compartilhados.</string> + <string name="people_can_connect_only_via_links_you_share">Você decide quem pode se conectar.</string> <string name="alert_text_skipped_messages_it_can_happen_when">Pode acontecer quando: \n1. As mensagens expiraram no remetente após 2 dias ou no servidor após 30 dias. \n2. A descriptografia da mensagem falhou porque você ou seu contato usou o backup do banco de dados antigo. @@ -784,7 +766,7 @@ <string name="delete_profile">Excluir perfil</string> <string name="profile_password">Senha de perfil</string> <string name="videos_limit_title">Excesso de vídeos!</string> - <string name="icon_descr_video_asked_to_receive">Solicitou receber o vídeo</string> + <string name="icon_descr_video_asked_to_receive">Pediu para receber o vídeo</string> <string name="videos_limit_desc">Apenas 10 vídeos podem ser enviados ao mesmo tempo</string> <string name="contact_sent_large_file">Seu contato enviou um arquivo maior que o tamanho máximo permitido (%1$s).</string> <string name="file_will_be_received_when_contact_completes_uploading">O arquivo será recebido quando seu contato concluir o upload.</string> @@ -795,7 +777,7 @@ <string name="icon_descr_record_voice_message">Gravar mensagem de voz</string> <string name="ensure_ICE_server_address_are_correct_format_and_unique">Certifique-se de que os endereços do servidor WebRTC ICE estão em formato correto, separados por linha e não estejam duplicados.</string> <string name="network_and_servers">Conexão e servidores</string> - <string name="network_settings_title">Configurações de conexão</string> + <string name="network_settings_title">Configurações avançadas</string> <string name="no_details">sem detalhes</string> <string name="add_contact">Link de convite de uso único</string> <string name="smp_servers_your_server_address">Seu endereço de servidor</string> @@ -805,7 +787,7 @@ <string name="add_contact_or_create_group">Começar novo chat</string> <string name="smp_servers_use_server">Usar servidor</string> <string name="you_need_to_allow_to_send_voice">Você precisa permitir que seu contato envie mensagens de voz para poder enviá-las também.</string> - <string name="your_chat_profiles">Seu perfil de chat</string> + <string name="your_chat_profiles">Seu perfil de bate-papo</string> <string name="smp_servers_preset_server">Servidor pré-definido</string> <string name="scan_code_from_contacts_app">Escaneie o código de segurança do aplicativo do seu contato.</string> <string name="icon_descr_send_message">Enviar mensagem</string> @@ -813,27 +795,28 @@ <string name="network_use_onion_hosts_no">Não</string> <string name="network_disable_socks">Usar conexão direta com a internet\?</string> <string name="hide_dev_options">Ocultar:</string> - <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Seu perfil é guardado no seu diapositivo e é compartilhado somente com seus contatos. Servidores SimpleX não podem ver seu perfil.</string> + <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Seu perfil é guardado no seu dispositivo e é compartilhado somente com seus contatos. Servidores SimpleX não podem ver seu perfil.</string> <string name="save_preferences_question">Salvar preferências\?</string> <string name="save_and_notify_group_members">Salvar e notificar membros do grupo</string> <string name="error_saving_user_password">Erro ao salvar a senha do usuário</string> <string name="hide_profile">Ocultar perfil</string> <string name="callstate_received_confirmation">confirmação recebida…</string> - <string name="relay_server_protects_ip">O servidor relay protege seu endereço IP, mas pode observar a duração da chamada.</string> + <string name="relay_server_protects_ip">O servidor de relay protege seu endereço IP, mas pode observar a duração da chamada.</string> <string name="settings_section_title_experimenta">EXPERIMENTAL</string> <string name="snd_conn_event_switch_queue_phase_completed">você alterou o endereço</string> <string name="database_upgrade">Atualização do banco de dados</string> - <string name="member_role_will_be_changed_with_invitation">A função será alterada para \"%s\". O membro receberá um novo convite.</string> + <string name="member_role_will_be_changed_with_invitation">O cargo será alterado para "%s". O membro receberá um novo convite.</string> <string name="only_group_owners_can_change_prefs">Somente os proprietários do grupo podem alterar as preferências do grupo.</string> <string name="button_add_welcome_message">Adicionar mensagem de boas-vindas</string> <string name="group_welcome_title">Mensagem de boas-vindas</string> - <string name="button_send_direct_message">Enviar DM</string> + <string name="button_send_direct_message">Enviar mensagem direta</string> <string name="muted_when_inactive">Mutado quando inativo!</string> <string name="you_will_still_receive_calls_and_ntfs">Você ainda receberá chamadas e notificações de perfis silenciados quando eles estiverem ativos.</string> <string name="delete_chat_profile">Excluir perfil de chat</string> <string name="save_servers_button">Salvar</string> <string name="star_on_github">Estrela no GitHub</string> - <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Seu contato precisa estar online para completar a conexão. Você pode cancelar esta conexão e remover o contato (e tentar mais tarde com um novo link).</string> + <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Seu contato precisa estar online para completar a conexão. +\nVocê pode cancelar esta conexão e remover o contato (e tentar mais tarde com um novo link).</string> <string name="app_name">SimpleX</string> <string name="only_group_owners_can_enable_voice">Somente o proprietários de grupo podem ativar mensagens de voz</string> <string name="description_you_shared_one_time_link">você compartilhou um link de uso único</string> @@ -851,36 +834,35 @@ <string name="share_invitation_link">Compartilhar link de uso único</string> <string name="network_use_onion_hosts">Usar hosts .onion</string> <string name="video_call_no_encryption">chamada de vídeo (não criptografada ponta-a-ponta)</string> - <string name="stop_chat_question">Parar chat\?</string> + <string name="stop_chat_question">Parar bate-papo?</string> <string name="simplex_link_mode_browser">Pelo navegador</string> - <string name="you_are_already_connected_to_vName_via_this_link">Você já está conectado a%1$s.</string> + <string name="you_are_already_connected_to_vName_via_this_link">Você já está conectado a %1$s.</string> <string name="database_initialization_error_desc">O banco de dados não está funcionando corretamente. Toque para saber mais</string> - <string name="waiting_for_image">Aguardando imagem</string> - <string name="show_QR_code">Mostrar código QR</string> + <string name="waiting_for_image">Aguardando a imagem</string> + <string name="show_QR_code">Mostrar QR code</string> <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Você também pode se conectar clicando no link. Se abrir no navegador, clique em <b>Abrir no aplicativo móvel</b>.]]></string> <string name="smp_servers_test_servers">Servidores de teste</string> <string name="update_network_session_mode_question">Atualizar o modo de isolamento de transporte\?</string> <string name="icon_descr_video_on">Vídeo ativado</string> - <string name="waiting_for_file">Aguardando arquivo</string> + <string name="waiting_for_file">Aguardando o arquivo</string> <string name="chat_help_tap_button">Toque no botão</string> - <string name="to_start_a_new_chat_help_header">Para começar um novo chat</string> + <string name="to_start_a_new_chat_help_header">Para começar um novo bate-papo</string> <string name="la_notice_turn_on">Ligar</string> - <string name="welcome">Bem-vindo!</string> - <string name="next_generation_of_private_messaging">A próxima geração de mensageiros privados</string> + <string name="welcome">Bem-vindo(a)!</string> + <string name="next_generation_of_private_messaging">O futuro da transmissão de mensagens</string> <string name="settings_section_title_socks">PROXY SOCKS</string> <string name="database_backup_can_be_restored">A tentativa de alterar a senha do banco de dados não foi concluída.</string> - <string name="stop_chat_to_export_import_or_delete_chat_database">Pare o chat para exportar, importar ou excluir o banco de dados do chat. Você não poderá receber e enviar mensagens enquanto o chat estiver interrompido.</string> - <string name="stop_chat_to_enable_database_actions">Pare o chat para ativar ações no banco de dados.</string> + <string name="stop_chat_to_export_import_or_delete_chat_database">Pare o bate-papo para exportar, importar ou excluir o banco de dados do chat. Você não poderá receber e enviar mensagens enquanto o chat estiver interrompido.</string> <string name="chat_item_ttl_seconds">%s segundo(s)</string> <string name="unknown_database_error_with_info">Erro de banco de dados desconhecido: %s</string> <string name="unknown_error">Erro desconhecido</string> - <string name="group_invitation_tap_to_join">Toque para se juntar</string> + <string name="group_invitation_tap_to_join">Toque para juntar-se</string> <string name="group_invitation_tap_to_join_incognito">Toque para entrar no modo anônimo</string> <string name="you_are_invited_to_group">Você está convidado para o grupo</string> <string name="updating_settings_will_reconnect_client_to_all_servers">A atualização das configurações reconectará o cliente a todos os servidores.</string> <string name="update_network_settings_confirmation">Atualizar</string> <string name="language_system">Sistema</string> - <string name="voice_messages_are_prohibited">Mensagens de voz são proibidas neste grupo.</string> + <string name="voice_messages_are_prohibited">Mensagens de voz são proibidas.</string> <string name="v4_4_verify_connection_security">Verificar a segurança da conexão</string> <string name="v4_5_private_filenames_descr">Para proteger o fuso horário, os arquivos de imagem/voz usam UTC.</string> <string name="unknown_message_format">formato de mensagem desconhecido</string> @@ -891,19 +873,19 @@ <string name="error_smp_test_failed_at_step">O teste falhou na etapa %s.</string> <string name="notifications_mode_periodic">Inicia periodicamente</string> <string name="icon_descr_sent_msg_status_unauthorized_send">envio não autorizado</string> - <string name="tap_to_start_new_chat">Toque para iniciar um novo chat</string> - <string name="you_have_no_chats">Você não tem chats</string> + <string name="tap_to_start_new_chat">Toque para iniciar um novo bate-papo</string> + <string name="you_have_no_chats">Você não tem conversas</string> <string name="callstate_waiting_for_answer">aguardando resposta…</string> - <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Seu banco de dados de chat atual será EXCLUÍDO e SUBSTITUÍDO pelo importado. + <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Seu banco de dados de bate-papo atual será EXCLUÍDO e SUBSTITUÍDO pelo importado. \nEsta ação não pode ser desfeita - seu perfil, contatos, mensagens e arquivos serão perdidos de forma irreversível.</string> <string name="update_database_passphrase">Atualizar senha do banco de dados</string> <string name="rcv_group_event_updated_group_profile">perfil de grupo atualizado</string> <string name="switch_verb">Trocar</string> - <string name="group_is_decentralized">O grupo é totalmente descentralizado – é visível apenas para os membros.</string> + <string name="group_is_decentralized">Totalmente decentralizado - visível apenas para os membros.</string> <string name="you_are_observer">você é um observador</string> <string name="voice_message_with_duration">Mensagem de voz (%1$s)</string> <string name="share_link">Compartilhar link</string> - <string name="to_protect_privacy_simplex_has_ids_for_queues">Para proteger a privacidade, em vez dos IDs de usuário usados por todas as outras plataformas, SimpleX tem identificadores para filas de mensagens, separados para cada um de seus contatos.</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">Para proteger a privacidade, SimpleX usa identificadores separados para cada um de seus contatos.</string> <string name="icon_descr_video_call">chamada de vídeo</string> <string name="show_call_on_lock_screen">Mostrar</string> <string name="webrtc_ice_servers">Servidores ICE WebRTC</string> @@ -911,17 +893,17 @@ <string name="update_database">Atualizar</string> <string name="periodic_notifications_desc">O app busca novas mensagens periodicamente – ele usa alguns por cento da bateria por dia. O aplicativo não usa notificações por push – os dados do seu dispositivo não são enviados para os servidores.</string> <string name="enter_passphrase_notification_desc">Para receber notificações, por favor, digite a senha do banco de dados</string> - <string name="simplex_service_notification_title">Serviço SimpleX</string> + <string name="simplex_service_notification_title">Serviço de Chat SimpleX</string> <string name="settings_notification_preview_mode_title">Mostrar prévia</string> <string name="notification_preview_mode_message_desc">Mostrar contato e mensagem</string> <string name="notification_preview_mode_contact_desc">Mostrar somente contato</string> <string name="share_verb">Compartilhar</string> - <string name="auth_stop_chat">Parar chat</string> + <string name="auth_stop_chat">Parar conversa</string> <string name="auth_unlock">Desbloquear</string> <string name="moderate_message_will_be_deleted_warning">A mensagem será excluída para todos os membros.</string> <string name="moderate_message_will_be_marked_warning">A mensagem será marcada como moderada para todos os membros.</string> <string name="share_image">Compartilhar mídia…</string> - <string name="icon_descr_waiting_for_image">Aguardando imagem</string> + <string name="icon_descr_waiting_for_image">Aguardando a imagem</string> <string name="share_file">Compartilhar arquivo…</string> <string name="image_decoding_exception_desc">A imagem não pode ser decodificada. Por favor, tente uma imagem diferente ou entre em contato com os desenvolvedores.</string> <string name="voice_message">Mensagem de voz</string> @@ -934,7 +916,7 @@ <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> <string name="network_session_mode_transport_isolation">Isolamento de transporte</string> <string name="you_control_your_chat">Você controla sua conversa!</string> - <string name="first_platform_without_user_ids">A 1ª plataforma sem nenhum identificador de usuário – privada por design.</string> + <string name="first_platform_without_user_ids">Sem identificadores de usuário.</string> <string name="icon_descr_speaker_on">Alto-falante ligado</string> <string name="icon_descr_video_off">Vídeo desativado</string> <string name="icon_descr_speaker_off">Alto-falante desligado</string> @@ -952,29 +934,28 @@ <string name="user_unhide">Mostrar</string> <string name="user_unmute">Desmutar</string> <string name="v4_3_voice_messages">Mensagens de voz</string> - <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Você pode <font color="#0088ff">se conectar aos desenvolvedores doSimpleX para fazer qualquer pergunta e receber atualizações</font>.]]></string> + <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Você pode <font color="#0088ff">se conectar aos desenvolvedores do SimpleX Chat para fazer qualquer pergunta e receber atualizações</font>.]]></string> <string name="connection_you_accepted_will_be_cancelled">A conexão que você aceitou será cancelada!</string> <string name="contact_you_shared_link_with_wont_be_able_to_connect">O contato com o qual você compartilhou este link NÃO será capaz de se conectar!</string> <string name="icon_descr_simplex_team">Equipe SimpleX</string> - <string name="use_simplex_chat_servers__question">Usar servidoresSimpleX\?</string> - <string name="using_simplex_chat_servers">Usando servidores SimpleX.</string> + <string name="use_simplex_chat_servers__question">Usar servidores SimpleX Chat?</string> + <string name="using_simplex_chat_servers">Usando servidores SimpleX Chat.</string> <string name="show_developer_options">Mostrar opções para desenvolvedores</string> <string name="v4_6_audio_video_calls_descr">Suporte bluetooth e outras melhorias.</string> - <string name="to_reveal_profile_enter_password">Para revelar seu perfil oculto, digite uma senha em um campo de busca em sua página de perfis de chat.</string> - <string name="upgrade_and_open_chat">Atualizar e abrir chat</string> - <string name="database_downgrade_warning">Aviso: você pode perder alguns dados!</string> + <string name="to_reveal_profile_enter_password">Para revelar seu perfil oculto, digite uma senha em um campo de busca em sua página de perfis de bate-papo.</string> + <string name="upgrade_and_open_chat">Atualizar e abrir bate-papo</string> + <string name="database_downgrade_warning">Atenção: você pode perder alguns dados!</string> <string name="you_rejected_group_invitation">Você rejeitou um convite de grupo</string> <string name="icon_descr_received_msg_status_unread">não lida</string> <string name="share_message">Compartilhar mensagem…</string> - <string name="personal_welcome">Bem-vindo%1$s!</string> + <string name="personal_welcome">Bem-vindo(a) %1$s!</string> <string name="group_preview_you_are_invited">você está convidado para o grupo</string> - <string name="update_onion_hosts_settings_question">Atualizar configuração de hosts .onion\?</string> - <string name="use_chat">Usar chat</string> + <string name="use_chat">Usar bate-papo</string> <string name="voice_prohibited_in_this_chat">Mensagens de voz são proibidas neste chat.</string> <string name="video_descr">Vídeo</string> <string name="icon_descr_video_snd_complete">Vídeo enviado</string> - <string name="icon_descr_waiting_for_video">Aguardando vídeo</string> - <string name="waiting_for_video">Aguardando vídeo</string> + <string name="icon_descr_waiting_for_video">Aguardando o vídeo</string> + <string name="waiting_for_video">Aguardando o vídeo</string> <string name="verify_security_code">Verificar código de segurança</string> <string name="video_will_be_received_when_contact_completes_uploading">O vídeo será recebido quando seu contato concluir o upload.</string> <string name="video_will_be_received_when_contact_is_online">O vídeo será recebido quando seu contato estiver online, aguarde ou verifique mais tarde!</string> @@ -996,39 +977,39 @@ <string name="smp_servers_your_server">Seu servidor</string> <string name="chat_lock">Bloqueio SimpleX</string> <string name="error_smp_test_certificate">Possivelmente, a impressão digital do certificado no endereço do servidor está incorreta</string> - <string name="you_sent_group_invitation">Você enviou convite de grupo</string> + <string name="you_sent_group_invitation">Você enviou um convite de grupo</string> <string name="incognito_random_profile">Seu perfil aleatório</string> <string name="auth_simplex_lock_turned_on">Bloqueio SimpleX ativado</string> <string name="your_simplex_contact_address">Seu endereço SimpleX</string> <string name="network_options_reset_to_defaults">Redefinir para os padrões</string> <string name="image_descr_link_preview">imagem de pré-visualização do link</string> - <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Para proteger suas informações, ative o bloqueio SimpleX. Você será solicitado a completar a autenticação antes que este recurso seja ativado.</string> - <string name="opensource_protocol_and_code_anybody_can_run_servers">Protocolo de código aberto – qualquer um pode executar os servidores.</string> + <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Para proteger suas informações, ative o bloqueio SimpleX. +\nVocê será solicitado a completar a autenticação antes que este recurso seja ativado.</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Qualquer um pode hospedar os servidores.</string> <string name="theme_light">Claro</string> <string name="chat_preferences_contact_allows">O contato permite</string> <string name="chat_preferences_on">ativado</string> <string name="simplex_link_contact">Endereço de contato</string> - <string name="simplex_link_group">Link do grupo</string> - <string name="simplex_link_invitation">Convite de uso único</string> + <string name="simplex_link_group">Link do grupo SimpleX</string> + <string name="simplex_link_invitation">Convite de uso único SimpleX</string> <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 móvel: toque em <b>Abrir no aplicativo móvel</b> e toque em <b>Conectar</b> no aplicativo.]]></string> <string name="ntf_channel_calls">Chamadas</string> <string name="ntf_channel_messages">Mensagens</string> <string name="la_notice_title_simplex_lock">Bloqueio SimpleX</string> <string name="markdown_help">Ajuda com Markdown</string> <string name="onboarding_notifications_mode_service">Instantânea</string> - <string name="open_simplex_chat_to_accept_call">Abrao chat do SimpleX para aceitar a chamada</string> + <string name="open_simplex_chat_to_accept_call">Abra o bate-papo do SimpleX para aceitar a chamada</string> <string name="call_connection_via_relay">via relay</string> <string name="feature_off">desativado</string> - <string name="downgrade_and_open_chat">Desatualizar e abrir o chat</string> + <string name="downgrade_and_open_chat">Desatualizar e abrir o bate-papo</string> <string name="chat_preferences_off">desativado</string> - <string name="settings_section_title_support">APOIE SIMPLEX</string> + <string name="settings_section_title_support">APOIE SIMPLEX CHAT</string> <string name="enable_automatic_deletion_message">Esta ação não pode ser desfeita - as mensagens enviadas e recebidas antes do selecionado serão excluídas. Pode levar vários minutos.</string> <string name="confirm_database_upgrades">Confirme as atualizações do banco de dados</string> - <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Perfis de usuário, contatos, grupos e mensagens enviadas com <b>criptografia de ponta-a-ponta</b> são armazenados somente no seu dispositivo.]]></string> - <string name="thank_you_for_installing_simplex">Obrigado por instalar SimpleX!</string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Somente o cliente dos dispositivos armazenam perfis de usuários, contatos, grupos e mensagens.</string> + <string name="thank_you_for_installing_simplex">Obrigado por instalar o SimpleX Chat!</string> <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">A plataforma de mensagens que protege sua privacidade e segurança.</string> <string name="invite_prohibited_description">Você está tentando convidar um contato com quem compartilhou um perfil anônimo para o grupo no qual está usando seu perfil principal</string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Você controla por meio de qual(is) servidor(es) <b>receber</b> as mensagens, seus contatos controlam os servidores que você usa para enviar mensagens.]]></string> <string name="smp_server_test_secure_queue">Fila segura</string> <string name="icon_descr_profile_image_placeholder">imagem de perfil temporária</string> <string name="error_loading_smp_servers">Erro ao carregar servidores SMP</string> @@ -1063,7 +1044,7 @@ <string name="network_socks_toggle_use_socks_proxy">Usar proxy SOCKS</string> <string name="host_verb">Hospedar</string> <string name="disable_onion_hosts_when_not_supported"><![CDATA[Definir <i>Usar hosts .onion</i> para não se o proxy SOCKS não oferecer suporte a eles.]]></string> - <string name="port_verb">Migrar</string> + <string name="port_verb">Porta</string> <string name="confirm_passcode">Confirmar senha</string> <string name="incorrect_passcode">Senha incorreta</string> <string name="lock_after">Bloquear após</string> @@ -1078,10 +1059,10 @@ <string name="authentication_cancelled">Autenticação cancelada</string> <string name="enable_lock">Habilitar bloqueio</string> <string name="passcode_changed">Senha alterada!</string> - <string name="you_can_turn_on_lock">Você pode ativar o bloqueio SimpleX via Configurações.</string> + <string name="you_can_turn_on_lock">Você pode ativar o bloqueio SimpleX via configurações.</string> <string name="alert_title_msg_bad_hash">Hash de mensagem incorreta</string> - <string name="alert_text_msg_bad_hash">O hash da mensagem anterior é diferente.</string> - <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d descriptografia das mensagens falhou</string> + <string name="alert_text_msg_bad_hash">O hash da mensagem anterior é diferente.\"</string> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d mensagens falharam em serem descriptografadas.</string> <string name="alert_title_msg_bad_id">ID de mensagem incorreta</string> <string name="alert_text_msg_bad_id">A ID da próxima mensagem está incorreta (menor ou igual à anterior). \nIsso pode acontecer por causa de algum bug ou quando a conexão está comprometida.</string> @@ -1096,7 +1077,7 @@ <string name="stop_file__confirm">Parar</string> <string name="revoke_file__message">O arquivo será excluído dos servidores.</string> <string name="revoke_file__title">Revogar arquivo\?</string> - <string name="alert_text_decryption_error_too_many_skipped">%1$d mensagens ignoradas</string> + <string name="alert_text_decryption_error_too_many_skipped">%1$d mensagens ignoradas.</string> <string name="audio_video_calls">Chamadas de áudio/vídeo</string> <string name="calls_prohibited_with_this_contact">Chamadas de áudio/vídeo são proibidas.</string> <string name="available_in_v51">" @@ -1111,14 +1092,14 @@ <string name="v5_0_large_files_support_descr">Rápido e sem esperar até que o remetente esteja online!</string> <string name="v5_0_polish_interface">interface polonesa</string> <string name="v5_0_app_passcode_descr">Defina-o em vez da autenticação do sistema.</string> - <string name="v5_0_polish_interface_descr">Obrigado aos usuários – contribuam via Weblate!</string> - <string name="v5_0_large_files_support">Vídeos e arquivos de até 1gb</string> + <string name="v5_0_polish_interface_descr">Obrigado aos usuários – contribua via Weblate!</string> + <string name="v5_0_large_files_support">Vídeos e arquivos de até 1GB</string> <string name="gallery_image_button">Imagem</string> - <string name="decryption_error">erro de descriptografia</string> + <string name="decryption_error">Erro de descriptografia</string> <string name="revoke_file__confirm">Revogar</string> <string name="learn_more_about_address">Sobre o endereço SimpleX</string> <string name="color_secondary_variant">Secundária adicional</string> - <string name="color_primary_variant">Acento adicional</string> + <string name="color_primary_variant">Tonalidade adicional</string> <string name="one_time_link_short">Link de uso único</string> <string name="add_address_to_your_profile">Adicione o endereço ao seu perfil, para que seus contatos possam compartilhá-lo com outras pessoas. A atualização do perfil será enviada aos seus contatos.</string> <string name="create_address_and_let_people_connect">Crie um endereço para permitir que as pessoas se conectem com você.</string> @@ -1128,7 +1109,7 @@ <string name="address_section_title">Endereço</string> <string name="dark_theme">Tema escuro</string> <string name="color_background">Fundo</string> - <string name="all_your_contacts_will_remain_connected_update_sent">Todos os seus contactos permanecerão ligados. A atualização do perfil será enviada para seus contatos.</string> + <string name="all_your_contacts_will_remain_connected_update_sent">Todos os seus contatos permanecerão conectados. A atualização do perfil será enviada para seus contatos.</string> <string name="auto_accept_contact">Aceitação automática</string> <string name="customize_theme_title">Personalizar o tema</string> <string name="enter_welcome_message">Digite a mensagem de boas-vindas…</string> @@ -1138,7 +1119,7 @@ <string name="import_theme_error_desc">Certifique-se que o arquivo tenha a sintaxe YAML correta. Exporte o tema para ter um exemplo da estrutura do arquivo do tema.</string> <string name="export_theme">Exportar tema</string> <string name="import_theme_error">Erro ao importar tema</string> - <string name="color_surface">Menus e alertas</string> + <string name="color_surface">Menus & Alertas</string> <string name="color_received_message">Mensagem recebida</string> <string name="color_sent_message">Mensagem enviada</string> <string name="color_title">Título</string> @@ -1148,7 +1129,7 @@ <string name="you_wont_lose_your_contacts_if_delete_address">Você não perderá seus contatos se, posteriormente, excluir seu endereço.</string> <string name="simplex_address">Endereço SimpleX</string> <string name="you_can_accept_or_reject_connection">Quando as pessoas solicitam uma conexão, você pode aceitá-la ou rejeitá-la.</string> - <string name="theme_colors_section_title">CORES DO TEMA</string> + <string name="theme_colors_section_title">CORES DA INTERFACE</string> <string name="share_with_contacts">compartilhar com os contatos</string> <string name="profile_update_will_be_sent_to_contacts">A atualização do perfil será enviada aos seus contatos.</string> <string name="save_settings_question">Salvar configurações\?</string> @@ -1168,7 +1149,7 @@ <string name="enter_welcome_message_optional">Digite a mensagem de boas-vindas... (opcional)</string> <string name="save_auto_accept_settings">Salvar configurações de aceitação automática</string> <string name="opening_database">Abrindo banco de dados…</string> - <string name="auth_open_chat_profiles">Abrir perfis de bate-papo</string> + <string name="auth_open_chat_profiles">Alterar perfis de conversa</string> <string name="share_address_with_contacts_question">Compartilhar endereço com os contatos\?</string> <string name="your_contacts_will_remain_connected">Seus contatos continuarão conectados.</string> <string name="all_app_data_will_be_cleared">Todos os dados do aplicativo serão excluídos.</string> @@ -1190,7 +1171,7 @@ <string name="allow_message_reactions">Permitir reações à mensagens.</string> <string name="only_you_can_add_message_reactions">Somente você pode adicionar reações à mensagens.</string> <string name="only_your_contact_can_add_message_reactions">Somente seu contato pode adicionar reações à mensagens.</string> - <string name="message_reactions_are_prohibited">Reações à mensagens são proibidas neste grupo.</string> + <string name="message_reactions_are_prohibited">Reações a mensagens são proibidas.</string> <string name="custom_time_unit_hours">horas</string> <string name="custom_time_unit_minutes">minutos</string> <string name="custom_time_unit_seconds">segundos</string> @@ -1208,8 +1189,8 @@ <string name="info_row_updated_at">Registro atualizado em</string> <string name="share_text_database_id">ID do banco de dados: %d</string> <string name="share_text_deleted_at">Excluído em: %s</string> - <string name="info_row_disappears_at">Desaparece em</string> - <string name="share_text_disappears_at">Desaparece em: %s</string> + <string name="info_row_disappears_at">Desaparecerá</string> + <string name="share_text_disappears_at">Desaparecerá: %s</string> <string name="info_row_moderated_at">Moderado em</string> <string name="share_text_moderated_at">Moderado em: %s</string> <string name="share_text_received_at">Recebido em: %s</string> @@ -1218,12 +1199,12 @@ <string name="current_version_timestamp">%s (atual)</string> <string name="allow_your_contacts_adding_message_reactions">Permitir que seus contatos adicionem reações à mensagens.</string> <string name="both_you_and_your_contact_can_add_message_reactions">Você e seu contato podem adicionar reações à mensagens.</string> - <string name="group_members_can_add_message_reactions">Os membros do grupo podem adicionar reações às mensagens.</string> - <string name="message_reactions_prohibited_in_this_chat">Reações à mensagens são proibidas neste chat.</string> + <string name="group_members_can_add_message_reactions">Os membros podem adicionar reações.</string> + <string name="message_reactions_prohibited_in_this_chat">Reações à mensagens são proibidas neste bate-papo.</string> <string name="prohibit_message_reactions">Proibir reações à mensagens.</string> <string name="custom_time_picker_custom">personalizado</string> <string name="whats_new_read_more">Ler mais</string> - <string name="v5_1_self_destruct_passcode_descr">Todos seus dados são apagados quando digitado</string> + <string name="v5_1_self_destruct_passcode_descr">Todos seus dados são apagados quando inserido.</string> <string name="v5_1_message_reactions_descr">Finalmente, nós os temos! 🚀</string> <string name="v5_1_message_reactions">Reações à mensagens</string> <string name="v5_1_self_destruct_passcode">Senha de auto-destruição</string> @@ -1234,7 +1215,7 @@ <string name="whats_new_thanks_to_users_contribute_weblate">Obrigado aos usuários – contribua via Weblate!</string> <string name="v5_1_better_messages_descr">- mensagens de voz de até 5 minutos \n- tempo personalizado para desaparecer. -\n- histórico de edição.</string> +\n- edição de histórico.</string> <string name="custom_time_unit_weeks">semanas</string> <string name="allow_message_reactions_only_if">Permitir reações à mensagens somente se o seu contato permitir.</string> <string name="v5_1_custom_themes_descr">Personalize e compartilhe temas de cores.</string> @@ -1246,36 +1227,36 @@ <string name="info_row_sent_at">Enviado em</string> <string name="change_self_destruct_passcode">Alterar senha de auto-destruição</string> <string name="if_you_enter_self_destruct_code">Se você digitar sua senha de auto-destruição ao abrir o aplicativo:</string> - <string name="item_info_no_text">Sem texto</string> - <string name="non_fatal_errors_occured_during_import">Alguns erros não-fatais ocurreram durante importação - pode ver o console de Chat para mais detalhes.</string> + <string name="item_info_no_text">sem texto</string> + <string name="non_fatal_errors_occured_during_import">Alguns erros não fatais ocorreram durante importação:</string> <string name="search_verb">Pesquisar</string> <string name="la_mode_off">Desativado</string> <string name="files_and_media">Arquivos e mídia</string> <string name="error_aborting_address_change">Erro ao cancelar alteração de endereço</string> - <string name="abort_switch_receiving_address_question">Anular a mudança de endereço\?</string> - <string name="abort_switch_receiving_address_confirm">Anular</string> + <string name="abort_switch_receiving_address_question">Abortar a mudança de endereço?</string> + <string name="abort_switch_receiving_address_confirm">Abortar</string> <string name="abort_switch_receiving_address_desc">A alteração de endereço será cancelada. O endereço de recebimento antigo será usado.</string> <string name="shutdown_alert_question">Desligar\?</string> - <string name="abort_switch_receiving_address">Anular alteração de endereço</string> + <string name="abort_switch_receiving_address">Abortar alteração de endereço</string> <string name="network_option_protocol_timeout_per_kb">Tempo limite do protocolo por KB</string> <string name="shutdown_alert_desc">As notificações deixarão de funcionar até que você reinicie o aplicativo</string> <string name="in_reply_to">Em resposta a</string> <string name="only_owners_can_enable_files_and_media">Somente os proprietários do grupo podem habilitar arquivos e mídia.</string> <string name="sync_connection_force_desc">A criptografia está funcionando e o novo acordo de criptografia não é necessário. Pode resultar em erros de conexão!</string> - <string name="snd_conn_event_ratchet_sync_started">concordando criptografia para %s</string> + <string name="snd_conn_event_ratchet_sync_started">concordando com criptografia para %s…</string> <string name="conn_event_ratchet_sync_agreed">criptografia concordada</string> <string name="conn_event_ratchet_sync_allowed">renegociação de criptografia permitida</string> <string name="rcv_conn_event_verification_code_reset">código de segurança alterado</string> <string name="renegotiate_encryption">Renegociar criptografia</string> <string name="sender_at_ts">%s em %s</string> - <string name="files_are_prohibited_in_group">Arquivos e mídia são proibidos neste grupo.</string> + <string name="files_are_prohibited_in_group">Arquivos e mídias são proibidos.</string> <string name="prohibit_sending_files">Proibir o envio de arquivos e mídia.</string> - <string name="snd_conn_event_ratchet_sync_ok">criptografia ok para %s</string> + <string name="snd_conn_event_ratchet_sync_ok">criptografia OK para %s</string> <string name="fix_connection_not_supported_by_group_member">Correção não suportada pelo membro do grupo</string> - <string name="conn_event_ratchet_sync_started">concordando criptografia…</string> + <string name="conn_event_ratchet_sync_started">concordando com criptografia…</string> <string name="allow_to_send_files">Permitir o envio de arquivos e mídia.</string> <string name="settings_section_title_app">APP</string> - <string name="conn_event_ratchet_sync_ok">criptografia ok</string> + <string name="conn_event_ratchet_sync_ok">criptografia OK</string> <string name="conn_event_ratchet_sync_required">renegociação de criptografia necessária</string> <string name="snd_conn_event_ratchet_sync_agreed">criptografia concordada para %s</string> <string name="snd_conn_event_ratchet_sync_allowed">renegociação de criptografia permitida para %s</string> @@ -1284,47 +1265,47 @@ <string name="error_synchronizing_connection">Erro ao sincronizar conexão</string> <string name="favorite_chat">Favorito</string> <string name="files_and_media_prohibited">Arquivos e mídia proibidos!</string> - <string name="group_members_can_send_files">Os membros do grupo podem enviar arquivos e mídia.</string> + <string name="group_members_can_send_files">Os membros podem enviar arquivos e mídias.</string> <string name="fix_connection_confirm">Corrigir</string> <string name="fix_connection_not_supported_by_contact">Correção não suportada pelo contato</string> <string name="settings_shutdown">Desligar</string> <string name="fix_connection">Corrigir conexão</string> <string name="fix_connection_question">Corrigir conexão\?</string> - <string name="no_filtered_chats">Sem chats filtrados</string> + <string name="no_filtered_chats">Sem conversas filtradas</string> <string name="sync_connection_force_confirm">Renegociar</string> <string name="unfavorite_chat">Desfavoritar</string> <string name="sync_connection_force_question">Renegociar a criptografia\?</string> <string name="settings_restart_app">Reiniciar</string> - <string name="delivery_receipts_title">Confirmações de entrega!</string> - <string name="delivery_receipts_are_disabled">As confirmações de entrega estão desabilitadas!</string> + <string name="delivery_receipts_title">Recibos de entrega!</string> + <string name="delivery_receipts_are_disabled">Os recibos de entrega estão desabilitadas!</string> <string name="receipts_contacts_enable_for_all">Ativar para todos</string> <string name="receipts_contacts_disable_for_all">Desativar para todos</string> - <string name="receipts_contacts_title_disable">Desabilitar as confirmações\?</string> + <string name="receipts_contacts_title_disable">Desabilitar os recibos?</string> <string name="receipts_contacts_disable_keep_overrides">Desativar (mantém alterações)</string> <string name="receipts_contacts_enable_keep_overrides">Ativar (mantém alterações)</string> - <string name="receipts_contacts_title_enable">Ativar as confirmações\?</string> - <string name="v5_2_favourites_filter">Encontrar conversas rápido</string> + <string name="receipts_contacts_title_enable">Ativar recibos?</string> + <string name="v5_2_favourites_filter">Encontrar conversas mais rápido</string> <string name="receipts_section_contacts">Contatos</string> - <string name="settings_section_title_delivery_receipts">ENVIAR CONFIRMAÇÕES DE ENTREGA PARA</string> + <string name="settings_section_title_delivery_receipts">ENVIAR RECIBOS DE ENTREGA PARA</string> <string name="receipts_contacts_override_disabled">Enviar confirmações está desativado para %d contatos.</string> <string name="receipts_contacts_override_enabled">Enviar confirmações está ativado para %d contatos.</string> <string name="send_receipts">Enviar confirmações</string> <string name="v5_2_more_things">Mais algumas coisas</string> <string name="v5_2_disappear_one_message_descr">Até mesmo desabilitado na conversa.</string> - <string name="v5_2_favourites_filter_descr">Filtrar chats não lidos e favoritos.</string> - <string name="v5_2_fix_encryption_descr">Corrigir encriptação depois de restaurar os backups.</string> + <string name="v5_2_favourites_filter_descr">Filtrar bate-papo não lidos e favoritos.</string> + <string name="v5_2_fix_encryption_descr">Corrigir criptografia depois de restaurar os backups.</string> <string name="v5_2_fix_encryption">Manter suas conexões</string> <string name="v5_2_disappear_one_message">Fazer uma mensagem desaparecer</string> <string name="v5_2_message_delivery_receipts">Confirmações de entrega de mensagens!</string> <string name="v5_2_more_things_descr">- entregas de mensagens mais estáveis. -\n- grupos melhores. +\n- grupos um pouco melhores. \n- e mais!</string> <string name="dont_enable_receipts">Não ative</string> <string name="enable_receipts_all">Ativar</string> <string name="sending_delivery_receipts_will_be_enabled">Enviar confirmações de entrega serão ativadas para todos os contatos.</string> - <string name="error_enabling_delivery_receipts">Ocorreu um erro ao ativar as confirmações de entrega!</string> + <string name="error_enabling_delivery_receipts">Ocorreu um erro ao ativar as recibos de entrega!</string> <string name="choose_file_title">Escolher arquivo</string> - <string name="connect_via_link_incognito">Conectar incógnito</string> + <string name="connect_via_link_incognito">Conectar anônimamente</string> <string name="turn_off_battery_optimization_button">Permitir</string> <string name="disable_notifications_button">Desativar notificações</string> <string name="system_restricted_background_in_call_title">Sem chamadas de fundo</string> @@ -1341,10 +1322,1064 @@ <string name="in_developing_title">Em breve!</string> <string name="delivery">Entrega</string> <string name="no_info_on_delivery">Nenhuma informação de entrega</string> - <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Enviar recibos de entrega será ativado para todos os contatos em todos os perfis de chat visíveis.</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Enviar recibos de entrega serão habilitados para todos os contatos em todos os perfis visíveis.</string> <string name="rcv_group_event_2_members_connected">%s e %s conectados</string> <string name="connect_via_member_address_alert_title">Conectar diretamente\?</string> - <string name="no_selected_chat">Sem chat selecionado</string> + <string name="no_selected_chat">Nenhuma conversa selecionada</string> <string name="privacy_message_draft">Rascunho de mensagem</string> <string name="send_receipts_disabled">desativado</string> -</resources> \ No newline at end of file + <string name="system_restricted_background_desc">SimpleX não pode ser executado em segundo plano. Você receberá as notificações somente quando o aplicativo estiver em execução.</string> + <string name="system_restricted_background_warn"><![CDATA[Para ativar notificações, selecione <b>Uso de bateria do aplicativo</b> / <b>Irrestrito</b> nas configurações do aplicativo.]]></string> + <string name="v5_3_new_interface_languages">6 novos idiomas de interface</string> + <string name="v5_5_private_notes">Notas privadas</string> + <string name="v5_5_private_notes_descr">Com arquivos criptografados e mídia</string> + <string name="v5_5_simpler_connect_ui">Colar o link para conectar!</string> + <string name="v5_5_simpler_connect_ui_descr">A barra de pesquisa aceita links de convite.</string> + <string name="v5_5_join_group_conversation">Participe de conversas em grupo</string> + <string name="not_compatible">Não compatível!</string> + <string name="invalid_qr_code">QR code inválido</string> + <string name="correct_name_to">Corrigir nome para %s?</string> + <string name="rcv_group_event_member_created_contact">diretamente conectado</string> + <string name="connect_plan_this_is_your_own_simplex_address">Este é o seu próprio endereço SimpleX!</string> + <string name="desktop_connection_terminated">Conexão terminada</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Este link é seu link para o grupo <b>%1$s</b>!]]></string> + <string name="remote_ctrl_was_disconnected_title">Conexão interrompida</string> + <string name="remote_ctrl_error_busy">Desktop está ocupado</string> + <string name="blocked_by_admin_items_description">%d mensagens bloqueadas pelo admnistrador</string> + <string name="blocked_by_admin_item_description">bloqueado pelo admnistrador</string> + <string name="note_folder_local_display_name">Notas privadas</string> + <string name="system_restricted_background_in_call_desc">O aplicativo pode ser fechado após 1 minuto em segundo plano.</string> + <string name="search_or_paste_simplex_link">Procurar e colar link SimpleX</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Adicionar contato</b>: para criar um novo link de convite, ou conectar pelo link que você recebeu.]]></string> + <string name="share_this_1_time_link">Compartilhe este link de convite único</string> + <string name="alert_text_encryption_renegotiation_failed">Renegociação de criptografia falhou.</string> + <string name="rcv_group_and_other_events">e %d outros eventos</string> + <string name="rcv_group_events_count">%d eventos de grupo</string> + <string name="group_members_2">%s e %s</string> + <string name="info_row_created_at">Criado em</string> + <string name="enable_sending_recent_history">Enviar até as 100 últimas mensagens para novos membros.</string> + <string name="link_a_mobile">Vincular um celular</string> + <string name="remote_host_was_disconnected_title">Conexão interrompida</string> + <string name="waiting_for_desktop">Aguardando o desktop…</string> + <string name="connected_desktop">Desktop conectado</string> + <string name="linked_desktops">Desktops vínculados</string> + <string name="remote_host_error_missing"><![CDATA[Dispositivo Móvel <b>%s</b> está faltando]]></string> + <string name="open_port_in_firewall_title">Abrir porta na firewall</string> + <string name="open_port_in_firewall_desc">Para permitir que um aplicativo móvel se conecte ao desktop, abra esta porta em seu firewall, se estiver ativado</string> + <string name="remote_host_error_timeout"><![CDATA[Tempo limite atingido durante a conexão com o móvel <b>%s</b>]]></string> + <string name="remote_host_error_busy"><![CDATA[Dispositivo Móvel <b>%s</b> está ocupado]]></string> + <string name="remote_host_error_inactive"><![CDATA[Dispositivo Móvel <b>%s</b> está inativo]]></string> + <string name="remote_host_error_bad_state"><![CDATA[A conexão com o dispositivo móvel <b>%s</b> está em mau estado]]></string> + <string name="remote_ctrl_error_bad_state">A conexão com o desktop está em mau estado</string> + <string name="remote_ctrl_error_disconnected">Desktop foi desconectado</string> + <string name="remote_ctrl_error_bad_invitation">Desktop tem um código de convite errado</string> + <string name="remote_ctrl_error_bad_version">O desktop tem uma versão não suportada. Por favor, certifique-se de usar a mesma versão em ambos os dispositivos</string> + <string name="agent_critical_error_title">Erro crítico</string> + <string name="agent_internal_error_title">Erro interno</string> + <string name="restart_chat_button">Reiniciar o bate-papo</string> + <string name="error_creating_member_contact">Erro ao criar contato de membro</string> + <string name="you_can_enable_delivery_receipts_later">Você pode habilitar mais tarde nas configurações</string> + <string name="you_can_enable_delivery_receipts_later_alert">Você pode habilitá-los mais tarde pelo aplicativo nas configurações de Privacidade & Segurança</string> + <string name="verify_code_with_desktop">Verificar código com o desktop</string> + <string name="new_desktop"><![CDATA[<i>(novo)</i>]]></string> + <string name="connect_plan_repeat_join_request">Repetir o pedido para se juntar?</string> + <string name="in_developing_desc">Este recurso ainda não é compatível. Experimente o próximo lançamento.</string> + <string name="connect_plan_you_have_already_requested_connection_via_this_address">Você já pediu para se conectar por este endereço!</string> + <string name="connect_plan_already_connecting">Você já está se conectando!</string> + <string name="connect_plan_connect_to_yourself">Conectar-se a você mesmo?</string> + <string name="connect_plan_open_group">Abrir grupo</string> + <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Você já está se conectando ao grupo por este link.</string> + <string name="connect_plan_already_joining_the_group">Você já está entrando no grupo!</string> + <string name="connect_plan_connect_via_link">Conectar via link?</string> + <string name="call_service_notification_audio_call">Chamada de áudio</string> + <string name="call_service_notification_end_call">Finalizar chamada</string> + <string name="call_service_notification_video_call">Chamada de vídeo</string> + <string name="expand_verb">Expandir</string> + <string name="compose_send_direct_message_to_connect">Enviar mensagem direta para conectar</string> + <string name="tap_to_scan">Toque para escanear</string> + <string name="new_chat">Novo bate-papo</string> + <string name="or_show_this_qr_code">Ou mostrar este código</string> + <string name="keep_invitation_link">Manter</string> + <string name="keep_unused_invitation_question">Manter convite não utilizado?</string> + <string name="or_scan_qr_code">Ou escanear o QR code</string> + <string name="tap_to_paste_link">Toque para colar o link</string> + <string name="the_text_you_pasted_is_not_a_link">O texto que você colou não é um link SimpleX.</string> + <string name="you_can_view_invitation_link_again">Você pode visualizar o código de convite novamente nos detalhes de conexão.</string> + <string name="developer_options_section">Opções de desenvolvedor</string> + <string name="show_slow_api_calls">Mostrar chamadas de API lentas</string> + <string name="you_can_make_address_visible_via_settings">Você pode tornar isso visível aos seus contatos SimpleX nas configurações.</string> + <string name="encrypt_local_files">Criptografar arquivos locais</string> + <string name="rcv_group_event_1_member_connected">%s conectou</string> + <string name="error_blocking_member_for_all">Erro ao bloquear membro para todos</string> + <string name="connect_via_member_address_alert_desc">Pedido de conexão será enviado para este membro do grupo.</string> + <string name="welcome_message_is_too_long">Mensagem de boas vindas é muito grande</string> + <string name="v5_3_simpler_incognito_mode_descr">Alterne para navegação anônima ao conectar.</string> + <string name="linked_mobiles">Dispositivos móveis vínculados</string> + <string name="remote_host_disconnected_from"><![CDATA[Desconectado do dispositivo móvel <b>%s</b> pelo motivo: %s]]></string> + <string name="bad_desktop_address">Endereço de desktop incorreto</string> + <string name="waiting_for_mobile_to_connect">Aguardando o dispositivo móvel conectar:</string> + <string name="verify_connections">Verificar conexões</string> + <string name="desktop_address">Endereço do desktop</string> + <string name="connect_use_current_profile">Usar perfil atual</string> + <string name="connect_use_new_incognito_profile">Usar novo perfil anônimo</string> + <string name="error_creating_message">Erro ao criar mensagem</string> + <string name="connect_plan_group_already_exists">Grupo já existe!</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Você já está no grupo <b>%1$s</b>.]]></string> + <string name="show_internal_errors">Mostrar erros internos</string> + <string name="receipts_groups_override_enabled">O envio de recibos está habilitado para %d grupos</string> + <string name="start_chat_question">Iniciar bate-papo?</string> + <string name="recent_history_is_not_sent_to_new_members">Histórico não é enviado para novos membros.</string> + <string name="v5_3_discover_join_groups">Descobrir e se juntar a grupos</string> + <string name="refresh_qr_code">Recarregar</string> + <string name="remote_host_error_bad_version"><![CDATA[Dispositivo Móvel <b>%s</b> tem uma versão não suportada. Por favor, certifique-se de usar a mesma versão em ambos os dispositivos]]></string> + <string name="failed_to_create_user_invalid_title">Nome de exibição inválido!</string> + <string name="failed_to_create_user_invalid_desc">Este nome de exibição é inválido. Por favor escolha outro nome.</string> + <string name="blocked_item_description">bloqueado</string> + <string name="non_content_uri_alert_title">Caminho de arquivo inválido</string> + <string name="non_content_uri_alert_text">Você compartilhou um caminho de arquivo inválido. Informe o problema para os desenvolvedores do aplicativo.</string> + <string name="message_too_large">A mensagem é muito grande</string> + <string name="app_was_crashed">Visualização travou</string> + <string name="marked_deleted_items_description">%d mensagens marcadas como excluídas</string> + <string name="delete_messages__question">Deletar %d mensagens?</string> + <string name="contact_tap_to_connect">Toque para conectar</string> + <string name="v5_3_simpler_incognito_mode">Modo anônimo simplificado</string> + <string name="found_desktop">Desktop encontrado</string> + <string name="random_port">Aleatório</string> + <string name="database_migration_in_progress">Migração do banco de dados em progresso. +\nIsso pode levar alguns minutos.</string> + <string name="moderated_items_description">%1$d mensagens moderadas por %2$s</string> + <string name="blocked_items_description">%d mensagens bloqueadas</string> + <string name="error_deleting_note_folder">Erro ao deletar notas privadas</string> + <string name="system_restricted_background_in_call_warn"><![CDATA[Para fazer ligações no plano de fundo, escolha <b>Uso de bateria do aplicativo</b> / <b>Irrestrito</b> nas configurações do aplicativo.]]></string> + <string name="send_receipts_disabled_alert_msg">Este grupo tem mais de %1$d membros, recibos de entrega não são enviados.</string> + <string name="loading_remote_file_title">Carregando o arquivo</string> + <string name="rcv_direct_event_contact_deleted">contato deletado</string> + <string name="error">Erro</string> + <string name="this_device">Este dispositivo</string> + <string name="discover_on_network">Descobrir via rede local</string> + <string name="group_member_status_unknown_short">desconhecido</string> + <string name="share_text_created_at">Criado em: %s</string> + <string name="button_remove_member_question">Remover membro?</string> + <string name="saved_message_title">Mensagem salva</string> + <string name="block_member_button">Bloquear membro</string> + <string name="remove_member_button">Remover membro</string> + <string name="block_for_all">Bloquear para todos</string> + <string name="block_for_all_question">Bloquear membro para todos?</string> + <string name="unblock_member_confirmation">Desbloquear</string> + <string name="unblock_member_button">Desbloquear membro</string> + <string name="unblock_member_question">Desbloquear membro?</string> + <string name="unblock_member_desc">Mensagens de %s serão exibidas!</string> + <string name="unblock_for_all">Desbloquear para todos</string> + <string name="unblock_for_all_question">Desbloquear membro para todos?</string> + <string name="create_group_button">Criar grupo</string> + <string name="v5_3_encrypt_local_files_descr">O aplicativo criptografa novos arquivos locais (exceto vídeos).</string> + <string name="v5_3_discover_join_groups_descr">- conecte-se ao serviço de diretório (BETA)! +\n- recibos de entrega (até 20 membros). +\n- mais rápido e mais estável.</string> + <string name="v5_5_message_delivery">Entrega de mensagens aprimorada</string> + <string name="v5_5_join_group_conversation_descr">Histórico recente e bot de diretório aprimorado.</string> + <string name="v5_2_message_delivery_receipts_descr">O segundo tick que perdemos! ✅</string> + <string name="v5_5_message_delivery_descr">Com uso de bateria reduzido.</string> + <string name="connected_to_mobile">Conectado ao dispositivo móvel</string> + <string name="disconnect_remote_hosts">Desconectar dispositivos móveis</string> + <string name="v5_5_new_interface_languages">UI húngara e turca</string> + <string name="this_device_version"><![CDATA[<i>(este dispositivo v%s)</i>]]></string> + <string name="scan_from_mobile">Escanear pelo dispositivo móvel</string> + <string name="remote_ctrl_disconnected_with_reason">Desconectado pelo motivo: %s</string> + <string name="connecting_to_desktop">Conectando ao desktop</string> + <string name="connect_to_desktop">Conectar ao desktop</string> + <string name="desktop_incompatible_version">Versão incompatível</string> + <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Abra <i>Use no desktop</i> no aplicativo móvel e escaneie o QR code.]]></string> + <string name="paste_desktop_address">Colar o endereço de desktop</string> + <string name="no_connected_mobile">Nenhum celular conectado</string> + <string name="connect_plan_join_your_group">Se juntar ao seu grupo?</string> + <string name="connect_plan_repeat_connection_request">Repetir o pedido de conexão?</string> + <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">Você já está se conectando por este código de uso único!</string> + <string name="agent_critical_error_desc">Por favor informe isto aos desenvolvedores: +\n%s +\n +\nÉ recomendado reiniciar o aplicativo.</string> + <string name="privacy_show_last_messages">Mostrar últimas mensagens</string> + <string name="receipts_section_description_1">Eles podem ser substituídos nas configurações de contato e grupo.</string> + <string name="save_passphrase_in_settings">Salvar senha nas configurações</string> + <string name="settings_is_storing_in_clear_text">A senha é armazenada nas configurações como um texto simples.</string> + <string name="passphrase_will_be_saved_in_settings">A senha será armazenada nas configurações como um texto simples após você mudar ela ou reiniciar o aplicativo.</string> + <string name="rcv_group_event_member_unblocked">desbloqueado %s</string> + <string name="group_members_n">%s, %s e %d membros</string> + <string name="rcv_group_event_n_members_connected">%s, %s e %d outros membros conectaram</string> + <string name="rcv_group_event_3_members_connected">%s, %s e %s conectaram</string> + <string name="snd_group_event_member_unblocked">você desbloqueou %s</string> + <string name="v5_3_new_interface_languages_descr">Árabe, búlgaro, finlandês, hebraico, tailandês e ucraniano - graças aos usuários e ao Weblate.</string> + <string name="use_random_passphrase">Usar senha aleatória</string> + <string name="setup_database_passphrase">Configurar senha do banco de dados</string> + <string name="la_app_passcode">Senha do aplicativo</string> + <string name="add_contact_tab">Adicionar contato</string> + <string name="clear_note_folder_warning">Todas as mensagens serão deletadas - isto não poderá ser desfeito!</string> + <string name="block_member_desc">Todas as novas mensagens de %s serão ocultadas!</string> + <string name="member_blocked_by_admin">Bloqueado pelo admnistrador</string> + <string name="rcv_group_event_member_blocked">bloqueado %s</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Criar grupo</b>: para criar um novo grupo.]]></string> + <string name="block_member_confirmation">Bloquear</string> + <string name="member_info_member_blocked">bloqueado</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">O código que você escaneou não é um QR code SimpleX.</string> + <string name="unable_to_open_browser_desc">O navegador padrão é necessário para chamadas. Configure o navegador padrão no sistema e compartilhe mais informações com os desenvolvedores.</string> + <string name="receipts_section_description">Essas configurações são para o seu perfil atual</string> + <string name="video_decoding_exception_desc">O vídeo não pode ser decodificado. Por favor, tente com um vídeo diferente ou contate os desenvolvedores.</string> + <string name="connect_plan_this_is_your_own_one_time_link">Este é o seu próprio link de uso único!</string> + <string name="remote_ctrl_error_timeout">Tempo limite atingido durante a conexão com o desktop</string> + <string name="recent_history_is_sent_to_new_members">Até as 100 últimas mensagens são enviadas para novos membros.</string> + <string name="snd_group_event_member_blocked">você bloqueou %s</string> + <string name="connect__your_profile_will_be_shared">Seu perfil %1$s será compartilhado.</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Você já está se conectando ao grupo <b>%1$s</b>.]]></string> + <string name="block_member_question">Bloquear membro?</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Atenção</b>: retransmissões de mensagens e arquivos são conectadas via proxy SOCKS. As chamadas e o envio de visualizações de links usam conexão direta.]]></string> + <string name="camera_not_available">Câmera não disponível</string> + <string name="connected_mobile">Dispositivo móvel conectado</string> + <string name="chat_is_stopped_you_should_transfer_database">O bate-papo foi interrompido. Se você já usou esse banco de dados em outro dispositivo, deverá transferi-lo de volta antes de iniciar o bate-papo.</string> + <string name="clear_note_folder_question">Limpar notas privadas?</string> + <string name="connected_to_desktop">Conectado ao desktop</string> + <string name="creating_link">Criando link…</string> + <string name="connect_with_contact_name_question">Conectar com %1$s?</string> + <string name="remote_ctrl_error_inactive">Desktop está inativo</string> + <string name="database_encryption_will_be_updated_in_settings">A senha de criptografia do banco de dados será atualizada e armazenada nas configurações.</string> + <string name="create_chat_profile">Criar perfil de bate-papo</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">O banco de dados será criado e a senha será armazenada nas configurações.</string> + <string name="delete_and_notify_contact">Deletar e notificar contato</string> + <string name="desktop_device">Desktop</string> + <string name="desktop_app_version_is_incompatible">A versão do aplicativo desktop %s não é compatível com este aplicativo.</string> + <string name="disconnect_desktop_question">Desconectar desktop?</string> + <string name="enable_camera_access">Habilitar o acesso à câmera</string> + <string name="encryption_renegotiation_error">Erro de renegociação de criptografia</string> + <string name="unable_to_open_browser_title">Erro ao abrir o navegador</string> + <string name="error_sending_message_contact_invitation">Erro ao enviar o convite</string> + <string name="loading_chats">Carregando conversas…</string> + <string name="remote_host_was_disconnected_toast"><![CDATA[Dispositivo Móvel <b>%s</b> foi desconectado]]></string> + <string name="remote_host_error_disconnected"><![CDATA[Dispositivo Móvel <b>%s</b> foi desconectado]]></string> + <string name="only_one_device_can_work_at_the_same_time">Apenas um dispositivo pode funcionar ao mesmo tempo</string> + <string name="rcv_group_event_open_chat">Abrir</string> + <string name="past_member_vName">Ex-membro %1$s</string> + <string name="agent_internal_error_desc">Por favor informe isto aos desenvolvedores: +\n%s</string> + <string name="loading_remote_file_desc">Por favor, espere até que o arquivo seja carregado pelo dispositivo móvel vínculado</string> + <string name="you_can_change_it_later">Senha aleatória é armazenadas nas configurações como um texto simples. +\nVocê pode mudar isso mais tarde.</string> + <string name="send_receipts_disabled_alert_title">Os recibos estão desativados</string> + <string name="remove_passphrase_from_settings">Remover senha das configurações?</string> + <string name="set_database_passphrase">Definir senha do banco de dados</string> + <string name="receipts_section_groups">Grupos pequenos (max 20)</string> + <string name="error_showing_message">erro ao mostrar a mensagem</string> + <string name="error_showing_content">erro ao mostrar o conteúdo</string> + <string name="error_alert_title">Erro</string> + <string name="invalid_name">Nome inválido!</string> + <string name="create_another_profile_button">Criar perfil</string> + <string name="settings_section_title_use_from_desktop">Usar pelo desktop</string> + <string name="v5_4_link_mobile_desktop">Vincule aplicativos móveis e de desktop! 🔗</string> + <string name="v5_4_better_groups">Grupos melhores</string> + <string name="v5_4_block_group_members">Bloquear membros do grupo</string> + <string name="v5_4_incognito_groups_descr">Criar um grupo usando um perfil aleatório.</string> + <string name="v5_4_better_groups_descr">Adesão mais rápida e mensagens mais confiáveis.</string> + <string name="v5_4_incognito_groups">Grupos anônimos</string> + <string name="v5_4_more_things_descr">- opcionalmente notificar contatos deletados +\n- nomes de perfil com espaços +\n- e mais!</string> + <string name="v5_4_block_group_members_descr">Para ocultar mensagens indesejadas.</string> + <string name="v5_4_link_mobile_desktop_descr">Via protocolo seguro de resistência quântica.</string> + <string name="devices">Dispositivos</string> + <string name="disconnect_remote_host">Disconectar</string> + <string name="enter_this_device_name">Digite o nome deste dispositivo…</string> + <string name="new_mobile_device">Novo dispositivo móvel</string> + <string name="session_code">Código de sessão</string> + <string name="this_device_name_shared_with_mobile">O nome do dispositivo será compartilhado com o cliente móvel conectado.</string> + <string name="this_device_name">O nome deste dispositivo</string> + <string name="unlink_desktop">Desvincular</string> + <string name="unlink_desktop_question">Desvincular desktop?</string> + <string name="verify_code_on_mobile">Verificar código no dispositivo móvel</string> + <string name="verify_connection">Verificar conexão</string> + <string name="linked_desktop_options">Opções de desktop vinculada</string> + <string name="scan_qr_code_from_desktop">Escanear QR code pelo desktop</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Você já está se conectando à <b>%1$s</b>.]]></string> + <string name="group_member_role_author">autor</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="multicast_connect_automatically">Conectar automaticamente</string> + <string name="desktop_devices">Dispositivos desktop</string> + <string name="multicast_discoverable_via_local_network">Descobrível via rede local</string> + <string name="possible_slow_function_desc">A execução da função está demorando muito: %1$d segundos: %2$s</string> + <string name="possible_slow_function_title">Função lenta</string> + <string name="member_contact_send_direct_message">enviar mensagem direta</string> + <string name="retry_verb">Tentar novamente</string> + <string name="open_database_folder">Abrir a pasta do banco de dados</string> + <string name="terminal_always_visible">Mostrar console em uma nova janela</string> + <string name="profile_update_event_contact_name_changed">contato %1$s mudou para %2$s</string> + <string name="profile_update_event_member_name_changed">membro %1$s mudou para %2$s</string> + <string name="profile_update_event_removed_address">endereço do contato removido</string> + <string name="profile_update_event_removed_picture">foto de perfil removida</string> + <string name="profile_update_event_set_new_address">definir novo endereço de contato</string> + <string name="profile_update_event_set_new_picture">definir nova foto de perfil</string> + <string name="group_member_status_unknown">status desconhecido</string> + <string name="profile_update_event_updated_profile">perfil atualizado</string> + <string name="recent_history">Histórico visível</string> + <string name="disable_sending_recent_history">Não enviar histórico para novos membros.</string> + <string name="v5_3_new_desktop_app_descr">Criar novo perfil no aplicativo de desktop. 💻</string> + <string name="v5_3_encrypt_local_files">Criptografar arquivos armazenados & arquivos de mídia</string> + <string name="v5_3_new_desktop_app">Novo aplicativo de desktop!</string> + <string name="migrate_from_device_archiving_database">Arquivando banco de dados</string> + <string name="migrate_from_device_cancel_migration">Cancelar migração</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Por Favor, note que</b>: usando o mesmo banco de dados em dois dispositivos vai quebrar a descriptografia das mensagens das suas conexões, como proteção de segurança.]]></string> + <string name="migrate_to_device_apply_onion">Aplicar</string> + <string name="theme_destination_app_theme">Tema do aplicativo</string> + <string name="theme_black">Preto</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="migrate_from_device_archive_and_upload">Arquivar e enviar</string> + <string name="chat_theme_apply_to_mode">Aplicar para</string> + <string name="v5_6_safer_groups_descr">Administradores podem bloquear um membro para todos.</string> + <string name="v5_6_app_data_migration">Migração de dados do aplicativo</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Todos os seus contatos,conversas e arquivos irão ser criptografados seguramente e enviados em partes para relays de XFTP configurados.</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Aviso</b>: o arquivo irá ser deletado.]]></string> + <string name="network_type_cellular">Rede móvel</string> + <string name="network_smp_proxy_mode_always">Sempre</string> + <string name="network_smp_proxy_mode_always_description">Sempre usar roteamento privado.</string> + <string name="permissions_camera">Câmera</string> + <string name="permissions_camera_and_record_audio">Câmera e microfone</string> + <string name="allow_to_send_simplex_links">Permitir o envio de links do SimpleX.</string> + <string name="feature_roles_all_members">Todos os membros</string> + <string name="wallpaper_advanced_settings">Configurações avançadas</string> + <string name="app_check_for_updates_notice_title">Verificar atualizações</string> + <string name="completed">Completado</string> + <string name="smp_servers_configured">Servidores SMP configurados</string> + <string name="xftp_servers_configured">Servidores XFTP configurados</string> + <string name="migrate_from_device_check_connection_and_try_again">Verifique sua conexão de internet e tente novamente</string> + <string name="app_check_for_updates">Verificar atualizações</string> + <string name="color_mode">Modo de cor</string> + <string name="migrate_from_device_delete_database_from_device">Apagar banco de dados desse dispositivo</string> + <string name="proxy_destination_error_broker_host">O endereço do servidor de destino de %1$s é incompatível com o as configurações %2$s do servidor de encaminhamento.</string> + <string name="snd_error_quota">Capacidade excedida - o destinatário não recebeu as mensagens enviadas anteriormente.</string> + <string name="snd_error_relay">Erro do servidor de destino: %1$s</string> + <string name="member_info_member_inactive">Inativo</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Escolha<i>Migre de outro dispositivo</i>no novo dispositivo e escaneie o QR code.]]></string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Confirme se você se lembra da senha do banco de dados para migrá-lo.</string> + <string name="privacy_media_blur_radius">Borrar conteúdo</string> + <string name="v6_0_privacy_blur">Borrar para melhor privacidade.</string> + <string name="confirm_delete_contact_question">Confirmar exclusão do contato?</string> + <string name="info_view_connect_button">conectar</string> + <string name="delete_without_notification">Apagar sem notificar</string> + <string name="color_primary_variant2">Tonalidade adicional 2</string> + <string name="migrate_to_device_confirm_network_settings">Confirmar configurações de rede</string> + <string name="chat_theme_apply_to_all_modes">Todos os modos de cor</string> + <string name="chat_theme_apply_to_dark_mode">Modo escuro</string> + <string name="cannot_share_message_alert_title">Não é possível enviar mensagem</string> + <string name="settings_section_title_chat_colors">Cores do chat</string> + <string name="create_address_button">Criar</string> + <string name="migrate_from_device_confirm_upload">Confirmar upload</string> + <string name="feature_roles_admins">administradores</string> + <string name="cant_call_contact_deleted_alert_text">O contato foi apagado.</string> + <string name="allow_calls_question">Permitir chamadas?</string> + <string name="cant_call_contact_alert_title">Não é possível chamar o contato</string> + <string name="cant_call_contact_connecting_wait_alert_text">Conectando ao contato, por favor aguarde ou volte depois!</string> + <string name="calls_prohibited_alert_title">Chamadas proibidas!</string> + <string name="cant_call_member_alert_title">Não é possível chamar membro do grupo</string> + <string name="cant_send_message_to_member_alert_title">Não é possível mandar mensagem para o membro do grupo</string> + <string name="v6_0_connection_servers_status">Controle sua rede</string> + <string name="v6_0_delete_many_messages_descr">Apague até 20 mensagens por vez.</string> + <string name="v6_0_your_contacts_descr">Arquivar contatos para conversar depois.</string> + <string name="v6_0_connect_faster_descr">Conecte aos seus amigos mais rapidamente.</string> + <string name="color_mode_dark">Escuro</string> + <string name="v5_8_safe_files_descr">Confirmar arquivos de servidores desconhecidos.</string> + <string name="copy_error">Copiar erro</string> + <string name="migrate_to_device_chat_migrated">Conversa migrada!</string> + <string name="info_view_call_button">chamar</string> + <string name="delete_contact_cannot_undo_warning">O contato será apagado - essa ação não pode ser desfeita!</string> + <string name="app_check_for_updates_beta">Beta</string> + <string name="app_check_for_updates_download_completed_title">A atualização do aplicativo foi baixada</string> + <string name="settings_section_title_chat_theme">Tema da conversa</string> + <string name="info_row_debug_delivery">Entrega de depuração</string> + <string name="dark_mode_colors">Cores do modo escuro</string> + <string name="migrate_from_device_creating_archive_link">Criando link de arquivo</string> + <string name="network_smp_proxy_fallback_allow_downgrade">Permitir downgrade</string> + <string name="all_users">Todos os usuários</string> + <string name="servers_info_sessions_connected">Conectado</string> + <string name="servers_info_sessions_connecting">Conectando</string> + <string name="current_user">Perfil atual</string> + <string name="servers_info_connected_servers_section_header">Servidores conectados</string> + <string name="servers_info_subscriptions_connections_subscribed">Conexões ativas</string> + <string name="attempts_label">tentativas</string> + <string name="acknowledged">Reconhecido</string> + <string name="acknowledgement_errors">Erros conhecidos</string> + <string name="connections">Conexões</string> + <string name="created">Criado</string> + <string name="decryption_errors">erros de decriptação</string> + <string name="deleted">Apagado</string> + <string name="deletion_errors">Erros de exclusão</string> + <string name="chunks_deleted">Pedaços excluídos</string> + <string name="chunks_downloaded">Pedaços baixados</string> + <string name="chunks_uploaded">Pedaços carregados</string> + <string name="delete_members_messages__question">Apagar %d mensagens dos membros?</string> + <string name="contact_deleted">Contato apagado!</string> + <string name="conversation_deleted">Conversa apagada!</string> + <string name="deleted_chats">Contatos arquivados</string> + <string name="chat_database_exported_title">Banco de dados da conversa exportado</string> + <string name="chat_database_exported_continue">Continuar</string> + <string name="v6_0_connection_servers_status_descr">Conexão e status dos servidores.</string> + <string name="migrate_to_device_repeat_download">Repetir download</string> + <string name="network_option_rcv_concurrency">Recebendo simultaneidade</string> + <string name="chat_theme_reset_to_user_theme">Redefinir para o tema do usuário</string> + <string name="v5_8_persian_ui">IU Persa</string> + <string name="message_delivery_warning_title">Aviso de entrega de mensagem</string> + <string name="ci_status_other_error">Erro: %1$s</string> + <string name="snd_error_auth">Chave incorreta ou conexão desconhecida - provavelmente esta conexão foi excluída.</string> + <string name="e2ee_info_no_pq_short">Essa conversa é protegida por criptografia ponta a ponta</string> + <string name="migrate_from_device_repeat_upload">Repetir upload</string> + <string name="smp_proxy_error_connecting">Erro de conexão ao servidor de encaminhamento %1$s. Por favor tente mais tarde.</string> + <string name="smp_proxy_error_broker_version">A versão do servidor de encaminhamento é incompatível com as configurações de rede: %1$s.</string> + <string name="proxy_destination_error_failed_to_connect">O servidor de encaminhamento %1$s falhou ao se conectar ao servidor de destino %2$s. Por favor tente mais tarde.</string> + <string name="smp_proxy_error_broker_host">O endereço do servidor de encaminhamento é incompatível com as configurações de rede: %1$s.</string> + <string name="proxy_destination_error_broker_version">A versão do servidor de destino de %1$s é incompatível com o servidor de encaminhamento %2$s.</string> + <string name="snd_error_expired">Problemas de rede - a mensagem expirou após muitas tentativas de envio.</string> + <string name="snd_error_proxy">Servidor de encaminhamento: %1$s +\nErro: %2$s</string> + <string name="recipients_can_not_see_who_message_from">Destinatário(s) não podem ver de onde essa mensagem veio.</string> + <string name="member_inactive_desc">A mensagem poderá ser entregue mais tarde se o membro se tornar ativo.</string> + <string name="smp_servers_other">Outros servidores SMP</string> + <string name="private_routing_explanation">Para proteger seu endereço IP, roteamento privado usa seus servidores SMP para entregar mensagens.</string> + <string name="app_check_for_updates_button_skip">Pular essa versão</string> + <string name="app_check_for_updates_button_download">Baixar %s (%s)</string> + <string name="app_check_for_updates_canceled">Download da atualização cancelado</string> + <string name="chat_list_always_visible">Mostrar lista de conversas em nova janela</string> + <string name="appearance_font_size">Tamanho da fonte</string> + <string name="color_wallpaper_background">Fundo do papel de parede</string> + <string name="color_wallpaper_tint">Tonalidade do papel de parede</string> + <string name="theme_remove_image">Remover imagem</string> + <string name="appearance_zoom">Zoom</string> + <string name="chat_theme_set_default_theme">Definir tema padrão</string> + <string name="prohibit_sending_simplex_links">Proibido enviar links SimpleX</string> + <string name="migrate_from_device_error_uploading_archive">Erro ao carregar o arquivo</string> + <string name="migrate_from_device_upload_failed">Falha ao carregar</string> + <string name="migrate_from_device_uploading_archive">Carregando arquivo</string> + <string name="servers_info_detailed_statistics">Estatísticas detalhadas</string> + <string name="file_error_relay">Erro no servidor de arquivo: %1$s</string> + <string name="saved_from_chat_item_info_title">Salvo de</string> + <string name="download_file">Baixar</string> + <string name="forward_chat_item">Encaminhar</string> + <string name="share_text_file_status">Status de arquivo: %s</string> + <string name="invite_friends_short">Convidar</string> + <string name="v6_0_increase_font_size">Aumentar tamanho da fonte.</string> + <string name="v6_0_upgrade_app">Aprimorar aplicativo automaticamente</string> + <string name="servers_info_reset_stats_alert_title">Redefinir todas as estatísticas?</string> + <string name="servers_info_reset_stats_alert_message">As estatísticas dos servidores serão redefinidas - isso não poderá ser desfeito!</string> + <string name="error_parsing_uri_title">Link inválido</string> + <string name="error_parsing_uri_desc">Por favor cheque se o link SimpleX está correto</string> + <string name="e2ee_info_pq"><![CDATA[Mensagens, arquivo e chamadas são protegidas por <b>criptografia quantum resistant e2e</b> com perfeito sigilo direto, repúdio e recuperação de vazamento.]]></string> + <string name="e2ee_info_pq_short">Essa conversa é protegida por criptografia quantum resistant ponta a ponta</string> + <string name="please_try_later">Por favor tente mais tarde.</string> + <string name="selected_chat_items_selected_n">Selecionado %d</string> + <string name="simplex_links_not_allowed">SimpleX links não permitidos</string> + <string name="files_and_media_not_allowed">Arquivos e mídia não permitidos</string> + <string name="compose_message_placeholder">Mensagem</string> + <string name="temporary_file_error">Erro de arquivo temporário</string> + <string name="info_view_message_button">mensagem</string> + <string name="info_view_search_button">pesquisar</string> + <string name="info_view_video_button">vídeo</string> + <string name="network_smp_proxy_mode_private_routing">Roteamento privado</string> + <string name="network_smp_proxy_mode_never_description">NÃO use roteamento privado.</string> + <string name="permissions_open_settings">Abrir configurações</string> + <string name="audio_device_earpiece">Fone de ouvido</string> + <string name="error_initializing_web_view">Erro ao iniciar o WebView. Atualize seu sistema para a nova versão. Por favor contate os desenvolvedores. +\nErro: %s</string> + <string name="privacy_media_blur_radius_off">Desativado</string> + <string name="privacy_media_blur_radius_strong">Forte</string> + <string name="v5_6_quantum_resistant_encryption_descr">Ativar em conversas diretas (BETA)!</string> + <string name="migrate_to_device_finalize_migration">Finalizar migração em outro dispositivo.</string> + <string name="color_mode_light">Claro</string> + <string name="v5_7_forward_descr">A origem da mensagem permanece privada.</string> + <string name="migrate_to_device_title">Migrar aqui</string> + <string name="migrate_to_device_migrating">Migrando</string> + <string name="v6_0_new_chat_experience">Nova experiência de conversa 🎉</string> + <string name="v6_0_new_media_options">Novas opções de mídia</string> + <string name="or_paste_archive_link">Ou cole o link do arquivo</string> + <string name="v6_0_chat_list_media">Reproduzir da lista de conversa.</string> + <string name="servers_info_reset_stats">Redefinir todas as estatísticas</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Aviso: iniciar conversa em múltiplos dispositivos não é suportado e pode causar falhas na entrega de mensagens</string> + <string name="network_type_ethernet">Internet cabeada</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[Você <b>não deve</b> usar a mesma base de dados em dois dispositivos.]]></string> + <string name="group_members_can_send_simplex_links">Membros podem enviar links SimpleX.</string> + <string name="migrate_to_device_importing_archive">Importando arquivo</string> + <string name="chat_theme_apply_to_light_mode">Modo claro</string> + <string name="feature_enabled_for">Ativado para</string> + <string name="v5_7_call_sounds_descr">Ao conectar em chamadas de áudio de vídeo.</string> + <string name="migrate_from_device_title">Migrar dispositivo</string> + <string name="migrate_from_device_error_saving_settings">Erro ao salvar configurações</string> + <string name="migrate_from_device_exported_file_doesnt_exist">Arquivo exportado não existe</string> + <string name="migrate_from_device_bytes_uploaded">%s carregados</string> + <string name="migrate_from_device_chat_should_be_stopped">Para continuar, a conversa precisa ser interrompida.</string> + <string name="network_type_other">Outro</string> + <string name="network_type_no_network_connection">Sem conexão de rede</string> + <string name="cannot_share_message_alert_text">As preferências de conversa selecionadas proíbem essa mensagem.</string> + <string name="file_error">Erro de arquivo</string> + <string name="reset_single_color">Redefinir cor</string> + <string name="v5_7_call_sounds">Sons de chamada</string> + <string name="v5_7_shape_profile_images">Formato das imagens de perfil</string> + <string name="v5_7_new_interface_languages">IU Lituana</string> + <string name="v5_8_private_routing">Roteamento de mensagem privada 🚀</string> + <string name="v5_8_chat_themes">Novos temas de conversa</string> + <string name="v5_8_message_delivery_descr">Com uso de bateria reduzida.</string> + <string name="migrate_to_device_download_failed">Falha no download</string> + <string name="migrate_to_device_import_failed">Falha na importação</string> + <string name="migrate_to_device_downloading_archive">Baixando arquivo</string> + <string name="migrate_to_device_repeat_import">Repetir importação</string> + <string name="migrate_to_device_try_again">Você pode tentar novamente.</string> + <string name="migrate_from_device_error_exporting_archive">Erro ao exportar banco de dados de conversa</string> + <string name="migrate_from_device_error_verifying_passphrase">Erro ao verificar a palavra-chave:</string> + <string name="saved_description">salvo</string> + <string name="saved_from_description">salvo de %s</string> + <string name="forwarded_from_chat_item_info_title">Encaminhado de</string> + <string name="voice_messages_not_allowed">Mensagens de voz não permitidas</string> + <string name="new_message">Nova mensagem</string> + <string name="paste_link">Colar link</string> + <string name="simplex_links">Links SimpleX</string> + <string name="v5_7_forward">Encaminhar e salvar mensagens</string> + <string name="privacy_media_blur_radius_soft">Suave</string> + <string name="privacy_media_blur_radius_medium">Médio</string> + <string name="you_need_to_allow_calls">Você precisa permitir seu contato ligue para poder ligar para ele.</string> + <string name="chat_theme_reset_to_app_theme">Redefinir para o tema do aplicativo</string> + <string name="color_received_quote">Resposta recebida</string> + <string name="wallpaper_preview_hello_alice">Boa tarde!</string> + <string name="wallpaper_preview_hello_bob">Bom dia!</string> + <string name="v6_0_upgrade_app_descr">Baixe novas versões no GitHub.</string> + <string name="forwarded_description">encaminhado</string> + <string name="migrate_to_device_file_delete_or_link_invalid">O arquivo foi deletado ou o link está inválido</string> + <string name="auth_open_migration_to_another_device">Abrir tela de migração</string> + <string name="v5_6_safer_groups">Grupos seguros</string> + <string name="migrate_from_device_database_init">Preparando upload</string> + <string name="settings_section_title_network_connection">Conexão de rede</string> + <string name="file_not_approved_title">Servidores desconhecidos!</string> + <string name="file_not_approved_descr">Sem Tor ou VPN, seu endereço de IP ficará visível para esses relays XFTP +\n%1$s.</string> + <string name="error_showing_desktop_notification">Erro ao exibir notificação, contate os desenvolvedores.</string> + <string name="saved_chat_item_info_tab">Salvo</string> + <string name="forwarded_chat_item_info_tab">Encaminhado</string> + <string name="network_smp_proxy_mode_unknown">Servidores desconhecidos</string> + <string name="network_smp_proxy_mode_unprotected">Desprotegido</string> + <string name="network_smp_proxy_mode_never">Nunca</string> + <string name="update_network_smp_proxy_mode_question">Modo de roteamento de mensagens</string> + <string name="permissions_required">Conceder permissões</string> + <string name="audio_device_speaker">Alto falante</string> + <string name="audio_device_wired_headphones">Headphones</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Sem Tor ou VPN, seu endereço de IP ficará visível para servidores de arquivo.</string> + <string name="settings_section_title_files">ARQUIVOS</string> + <string name="settings_section_title_profile_images">Fotos de perfil</string> + <string name="settings_section_title_private_message_routing">ROTEAMENTO DE MENSAGEM PRIVADA</string> + <string name="conn_event_disabled_pq">criptografia padrão ponta a ponta</string> + <string name="feature_roles_owners">proprietários</string> + <string name="migrate_from_device_to_another_device">Migrar para outro dispositivo</string> + <string name="migrate_from_device_verify_passphrase">Verificar palavra-passe</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="one_hand_ui_card_title">Alternar lista de conversa:</string> + <string name="one_hand_ui_change_instruction">Você pode mudar isso em configurações de Aparência.</string> + <string name="member_info_member_disabled">desativado</string> + <string name="message_queue_info_none">nenhum</string> + <string name="message_queue_info_server_info">informações da fila do servidor: %1$s +\n +\núltima mensagem recebida: %2$s</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">Por favor peça para seu contato ativar as chamadas.</string> + <string name="cant_call_member_send_message_alert_text">Enviar mensagem para ativar chamadas.</string> + <string name="network_options_save_and_reconnect">Salvar e reconectar</string> + <string name="network_option_tcp_connection">Conexão TCP</string> + <string name="v6_0_reachable_chat_toolbar">Barra de ferramentas de conversa acessível</string> + <string name="v6_0_private_routing_descr">Isso protege seu endereço de IP e conexões.</string> + <string name="v6_0_reachable_chat_toolbar_descr">Use o aplicativo com uma mão.</string> + <string name="uploaded_files">Arquivos carregados</string> + <string name="servers_info_downloaded">Baixado</string> + <string name="servers_info_reset_stats_alert_error_title">Erro ao redefinir estatísticas</string> + <string name="servers_info_reset_stats_alert_confirm">Redefinir</string> + <string name="info_row_file_status">Status de arquivo</string> + <string name="message_queue_info">Informações da fila de mensagens</string> + <string name="color_mode_system">Sistema</string> + <string name="wallpaper_scale_repeat">Repetir</string> + <string name="wallpaper_scale">Escala</string> + <string name="wallpaper_scale_fill">Preencher</string> + <string name="wallpaper_scale_fit">Ajustar</string> + <string name="simplex_links_are_prohibited_in_group">Links SimpleX são proibidos.</string> + <string name="v5_6_app_data_migration_descr">Migrar para outro dispositivo via QR code.</string> + <string name="v5_6_picture_in_picture_calls">Chamadas picture-in-picture</string> + <string name="v5_6_picture_in_picture_calls_descr">Use o aplicativo enquanto está em chamada.</string> + <string name="v5_7_quantum_resistant_encryption_descr">Será ativado em conversas diretas!</string> + <string name="v5_8_safe_files">Receber arquivos de forma segura</string> + <string name="v5_7_network">Gerenciamento de rede</string> + <string name="v5_8_chat_themes_descr">Faça suas conversas terem uma aparência diferente!</string> + <string name="v5_7_network_descr">Conexão de rede mais confiável.</string> + <string name="migrate_to_device_database_init">Preparando download</string> + <string name="migrate_to_device_bytes_downloaded">%s baixados</string> + <string name="paste_archive_link">Colar link de arquivo</string> + <string name="migrate_to_device_enter_passphrase">Insira a palavra-chave</string> + <string name="migrate_to_device_error_downloading_archive">Erro ao baixar o arquivo</string> + <string name="migrate_from_device_or_share_this_file_link">Ou de forma segura compartilhe esse link de arquivo</string> + <string name="migrate_from_device_verify_database_passphrase">Verifique a palavra-passe do banco de dados</string> + <string name="e2ee_info_no_pq"><![CDATA[Mensagens, arquivo e chamadas são protegidas por <b>criptografia ponta-a-ponta</b> com perfeito sigilo direto, repúdio e recuperação de vazamento.]]></string> + <string name="file_error_no_file">Arquivo não encontrado - provavelmente o arquivo foi excluído ou cancelado.</string> + <string name="file_error_auth">Chave incorreta ou arquivo de pedaço de endereço - provavelmente o arquivo foi excluído.</string> + <string name="network_smp_proxy_fallback_allow_protected_description">Mande mensagens diretamente quando o seu endereço de IP está protegido e o servidor de destino não suporta roteamento privado.</string> + <string name="network_smp_proxy_mode_unknown_description">Use roteamento privado em servidores desconhecidos.</string> + <string name="scan_paste_link">Escanear / Colar link</string> + <string name="migrate_to_device_downloading_details">Baixando detalhes de link</string> + <string name="migrate_from_device_finalize_migration">Finalizar migração</string> + <string name="v5_6_quantum_resistant_encryption">Criptografia Quantum resistant</string> + <string name="migrate_from_device_migration_complete">Migração concluída</string> + <string name="v5_8_private_routing_descr">Proteja seu endereço de IP dos retransmissores de mensagem escolhidos por seus contatos. +\nAtive nas configurações *Redes e servidores* .</string> + <string name="migrate_from_device_start_chat">Iniciar conversa</string> + <string name="toolbar_settings">Configurações</string> + <string name="info_view_open_button">abrir</string> + <string name="keep_conversation">Manter conversa</string> + <string name="only_delete_conversation">Apenas excluir conversa</string> + <string name="xftp_servers_other">Outros servidores XFTP</string> + <string name="network_smp_proxy_mode_unprotected_description">Use roteamento privado em servidores desconhecidos quando o endereço de IP não está protegido.</string> + <string name="network_smp_proxy_fallback_allow">Sim</string> + <string name="app_check_for_updates_button_install">Instalar atualização</string> + <string name="app_check_for_updates_update_available">Atualização disponível: %s</string> + <string name="app_check_for_updates_button_open">Abrir local do arquivo</string> + <string name="app_check_for_updates_notice_disable">Desativar</string> + <string name="permissions_record_audio">Microfone</string> + <string name="permissions_grant_in_settings">Conceder nas configurações</string> + <string name="permissions_find_in_settings_and_grant">Encontre essa permissão nas configurações do Android e conceda-a manualmente.</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">O aplicativo irá perguntar para confirmar os downloads de servidores de arquivo desconhecidos (exceto .onion ou quando o proxy SOCKS estiver habilitado).</string> + <string name="settings_section_title_user_theme">Tema de perfil</string> + <string name="set_passphrase">Defina uma palavra-chave</string> + <string name="conn_event_enabled_pq">criptografia quantum resistant e2e</string> + <string name="action_button_add_members">Convidar</string> + <string name="info_row_message_status">Status da mensagem</string> + <string name="v5_8_message_delivery">Entrega de mensagens aprimorada</string> + <string name="v5_7_shape_profile_images_descr">Quadrado, circulo, ou qualquer coisa entre eles.</string> + <string name="remote_ctrl_connection_stopped_desc">Por favor verifique se o celular e o computador estão conectados na mesma rede local e o firewall do computador permite a conexão. +\nPor favor compartilhe qualquer outro problema com os desenvolvedores.</string> + <string name="remote_ctrl_connection_stopped_identity_desc">Esse link foi usado em outros dispositivo móvel, por favor crie um novo link no computador.</string> + <string name="migrate_from_device_error_deleting_database">Erro ao excluir banco de dados</string> + <string name="migrate_to_device_confirm_network_settings_footer">Por favor confirme que as configurações de rede estão corretas para este dispositivo.</string> + <string name="migrate_from_device_stopping_chat">Parando conversa</string> + <string name="migrate_from_device_try_again">Você pode tentar novamente.</string> + <string name="network_error_broker_version_desc">A versão do servidor é incompatível com seu aplicativo: %1$s.</string> + <string name="private_routing_error">Erro de roteamento privado</string> + <string name="network_error_broker_host_desc">O endereço do servidor é incompatível com as configurações de rede: %1$s.</string> + <string name="snd_error_proxy_relay">Servidor de encaminhamento: %1$s +\nErro no servidor de destino: %2$s</string> + <string name="srv_error_host">Endereço do servidor é incompatível com as configurações de rede.</string> + <string name="srv_error_version">A versão do servidor é incompatível com as configurações de rede.</string> + <string name="forward_message">Encaminhar mensagem…</string> + <string name="network_smp_proxy_fallback_allow_protected">Quando IP oculto</string> + <string name="protect_ip_address">Proteger endereço IP</string> + <string name="color_sent_quote">Enviar resposta</string> + <string name="servers_info_files_tab">Arquivos</string> + <string name="network_smp_proxy_fallback_prohibit">Não</string> + <string name="app_check_for_updates_download_started">Baixando atualização do aplicativo, não feche o aplicativo</string> + <string name="permissions_grant">Conceder permissão para fazer chamadas</string> + <string name="invalid_file_link">Link inválido</string> + <string name="servers_info_details">Detalhes</string> + <string name="servers_info_sessions_errors">Erros</string> + <string name="servers_info_messages_received">Mensagens recebidas</string> + <string name="servers_info_messages_sent">Mensagens enviadas</string> + <string name="servers_info_missing">Sem informação, tente recarregar</string> + <string name="servers_info">Informação dos servidores</string> + <string name="servers_info_target">Mostrando informação para</string> + <string name="servers_info_statistics_section_header">Estatísticas</string> + <string name="servers_info_transport_sessions_section_header">Sessões de transporte</string> + <string name="servers_info_subscriptions_section_header">Recepção de mensagem</string> + <string name="servers_info_subscriptions_connections_pending">Pendente</string> + <string name="servers_info_private_data_disclaimer">Começando em %s.\nTodos os dados são mantidos privados em seu dispositivo.</string> + <string name="servers_info_subscriptions_total">Total</string> + <string name="servers_info_proxied_servers_section_header">Servidores proxiados</string> + <string name="servers_info_previously_connected_servers_section_header">Servidores conectados anteriormente</string> + <string name="servers_info_reconnect_servers_message">Reconecte todos os servidores conectados para forçar entrega de mensagem. Isso usa tráfego adicional.</string> + <string name="servers_info_reconnect_server_title">Reconectar servidor?</string> + <string name="servers_info_reconnect_servers_title">Reconectar servidores?</string> + <string name="servers_info_reconnect_server_message">Reconectar servidor para forçar entrega de mensagem. Isso usa tráfego adicional.</string> + <string name="servers_info_proxied_servers_section_footer">Você não está conectado nesses servidores. Roteamento privado é usado para entregar mensagens para eles.</string> + <string name="servers_info_modal_error_title">Erro</string> + <string name="servers_info_reconnect_server_error">Erro ao reconectar servidor</string> + <string name="servers_info_reconnect_servers_error">Erro ao reconectar servidores</string> + <string name="servers_info_reconnect_all_servers_button">Reconectar todos os servidores</string> + <string name="reconnect">Reconectar</string> + <string name="sent_directly">Enviar diretamente</string> + <string name="servers_info_detailed_statistics_sent_messages_header">Enviar mensagens</string> + <string name="servers_info_detailed_statistics_sent_messages_total">Enviar total</string> + <string name="sent_via_proxy">Enviar via proxy</string> + <string name="smp_server">Servidor SMP</string> + <string name="servers_info_detailed_statistics_received_messages_header">Mensagens recebidas</string> + <string name="servers_info_detailed_statistics_received_total">Total recebido</string> + <string name="servers_info_detailed_statistics_receive_errors">Receber erros</string> + <string name="servers_info_starting_from">Começando de %s.</string> + <string name="xftp_server">Servidor XFTP</string> + <string name="secured">Seguro</string> + <string name="send_errors">Enviar erros</string> + <string name="subscribed">Inscrito</string> + <string name="duplicates_label">duplicatas</string> + <string name="expired_label">expirada</string> + <string name="other_label">outro</string> + <string name="subscription_errors">Erros de inscrição</string> + <string name="other_errors">outros erros</string> + <string name="proxied">Proxied</string> + <string name="subscription_results_ignored">Inscrições ignoradas</string> + <string name="download_errors">Erros de download</string> + <string name="downloaded_files">Arquivos baixados</string> + <string name="server_address">Endereço do servidor</string> + <string name="size">Tamanho</string> + <string name="upload_errors">Erros de upload</string> + <string name="open_server_settings_button">Abrir configurações de servidor</string> + <string name="select_verb">Selecione</string> + <string name="moderate_messages_will_be_deleted_warning">As mensagens serão excluídas para todos os membros.</string> + <string name="moderate_messages_will_be_marked_warning">As mensagens serão marcadas como moderadas para todos os membros.</string> + <string name="message_forwarded_title">Mensagem encaminhada</string> + <string name="message_forwarded_desc">Ainda não há conexão direta, a mensagem é encaminhada pelo administrador.</string> + <string name="member_inactive_title">Membro inativo</string> + <string name="selected_chat_items_nothing_selected">Nada selecionado</string> + <string name="delete_messages_mark_deleted_warning">Mensagens serão marcadas para exclusão. O(s) destinatário(s) poderá(ão) revelar essas mensagens.</string> + <string name="you_can_still_view_conversation_with_contact">Você ainda pode ver a conversa com %1$s na lista de conversas.</string> + <string name="no_filtered_contacts">Nenhum contato filtrado</string> + <string name="contact_list_header_title">Seus contatos</string> + <string name="network_smp_proxy_fallback_prohibit_description">NÃO envie mensagens diretamente, mesmo que o seu servidor ou o servidor de destino não suporte roteamento privado.</string> + <string name="network_smp_proxy_fallback_allow_description">Mande mensagens diretamente quando o seu servidor ou o servidor de destino não suporta roteamento privado.</string> + <string name="update_network_smp_proxy_fallback_question">Retorno de roteamento de mensagens</string> + <string name="private_routing_show_message_status">Mostrar status da mensagem</string> + <string name="migrate_from_another_device">Migrar de outro dispositivo</string> + <string name="share_text_message_status">Status da mensagem: %s</string> + <string name="servers_info_uploaded">Carregado</string> + <string name="message_servers">Servidores de mensagem</string> + <string name="media_and_file_servers">Servidores de mídia e arquivo</string> + <string name="subscription_percentage">Mostrar porcentagem</string> + <string name="network_socks_proxy">Proxy SOCKS</string> + <string name="chat_database_exported_save">Você pode salvar o arquivo exportado.</string> + <string name="chat_database_exported_migrate">Você pode migrar o banco de dados exportado.</string> + <string name="chat_database_exported_not_all_files">Alguns arquivos não foram exportados</string> + <string name="you_can_still_send_messages_to_contact">Você pode enviar mensagens para %1$s de Contatos arquivados.</string> + <string name="reset_all_hints">Redefinir todas as dicas</string> + <string name="app_check_for_updates_disabled">Desativado</string> + <string name="app_check_for_updates_stable">Estável</string> + <string name="app_check_for_updates_installed_successfully_title">Instalado com sucesso</string> + <string name="app_check_for_updates_installed_successfully_desc">Por favor reinicie o aplicativo.</string> + <string name="app_check_for_updates_button_remind_later">Me lembre mais tarde</string> + <string name="app_check_for_updates_notice_desc">Para ser notificado sobre os novos lançamentos, habilite a checagem periódica de versões Estáveis e Beta.</string> + <string name="one_hand_ui">Barras de ferramentas de aplicativos acessível</string> + <string name="forward_files_failed_to_receive_desc">Falha no baixar de %1$d arquivo(s).</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s mensagens não encaminhadas.</string> + <string name="settings_section_title_chat_database">DADOS DO BATE-PAPO</string> + <string name="network_proxy_random_credentials">Utilize credenciais aleatórias</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">O arquivo de banco de dados enviado será removido permanentemente dos servidores.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">Use credenciais diferentes de proxy para cada conexão.</string> + <string name="switching_profile_error_message">Sua conexão foi movida para %s, mas um erro inesperado ocorreu ao redirecioná-lo para o seu perfil.</string> + <string name="n_file_errors">%1$d erro(s) de arquivo(s): +\n%2$s</string> + <string name="n_other_file_errors">%1$d outro(s) erro(s) de arquivo(s).</string> + <string name="error_forwarding_messages">Erro ao encaminhar mensagens.</string> + <string name="forward_alert_title_messages_to_forward">Encaminhar %1$s mensagens?</string> + <string name="forward_alert_forward_messages_without_files">Encaminhar mensagens sem arquivos?</string> + <string name="forward_files_messages_deleted_after_selection_desc">As mensagens foram excluidas após vocês selecioná-las.</string> + <string name="forward_alert_title_nothing_to_forward">Nada para encaminhar!</string> + <string name="forward_files_in_progress_desc">%1$d o(s) arquivo(s) ainda está(ão) sendo baixado(s).</string> + <string name="forward_files_missing_desc">%1$d arquivo(s) foi(ram) excluído(s).</string> + <string name="forward_files_not_accepted_desc">%1$d arquivo(s) não foi(ram) baixado(s).</string> + <string name="forward_files_not_accepted_receive_files">Baixar</string> + <string name="forward_multiple">Emcaminhar mensagens…</string> + <string name="compose_forward_messages_n">Encaminhando %1$s mensagens.</string> + <string name="compose_save_messages_n">Salvando %1$s mensagens</string> + <string name="network_proxy_auth">Autenticação de proxy</string> + <string name="network_proxy_auth_mode_no_auth">Não utilize credenciais com proxy.</string> + <string name="network_proxy_incorrect_config_desc">Certifique-se de que configuração do proxy está correta.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">Use diferentes credenciais de proxy para cada perfil.</string> + <string name="network_proxy_auth_mode_username_password">Suas credenciais podem ser enviadas sem criptografia.</string> + <string name="migrate_from_device_remove_archive_question">Remover arquivo?</string> + <string name="delete_messages_cannot_be_undone_warning">As mensagens serão excluídas - isso não pode ser desfeito!</string> + <string name="switching_profile_error_title">Erro ao alternar perfil</string> + <string name="select_chat_profile">Selecionar perfil de bate-papo</string> + <string name="new_chat_share_profile">Compartilhar perfil</string> + <string name="system_mode_toast">Modo sistema</string> + <string name="network_proxy_incorrect_config_title">Erro ao salvar proxy</string> + <string name="network_proxy_password">Senha</string> + <string name="network_proxy_username">Nome de usuário</string> + <string name="network_session_mode_session">Sessão do aplicativo</string> + <string name="address_or_1_time_link">Endereço ou link de uso único?</string> + <string name="address_settings">Configurações de endereço</string> + <string name="add_your_team_members_to_conversations">Adicione membros da sua equipe às conversas.</string> + <string name="v6_1_better_calls">Melhores ligações</string> + <string name="operator_added_message_servers">Servidores de mensagem adicionados</string> + <string name="operator_added_xftp_servers">Adicionado servidores de mídia e arquivos</string> + <string name="appearance_app_toolbars">Barra de ferramentas</string> + <string name="onboarding_notifications_mode_service_desc_short">Aplicativo sempre roda em segundo plano</string> + <string name="button_add_friends">Adicionar amigos</string> + <string name="operator_conditions_accepted">Condições aceitas</string> + <string name="display_name_accepted_invitation">Convite aceito</string> + <string name="button_add_team_members">Adicionar membros da equipe</string> + <string name="onboarding_network_about_operators">Sobre operadores</string> + <string name="accept_conditions">Aceite as condições</string> + <string name="report_item_archived_by">denúncia arquivada por %s</string> + <string name="report_reason_other">Outra razão</string> + <string name="chat_list_add_list">Adicionar lista</string> + <string name="delete_chat_list_warning">Todas as conversas serão removidas da lista %s, e a lista será apagada</string> + <string name="v6_1_better_security">Melhor segurança ✅</string> + <string name="report_archive_alert_title">Arquivar denúncia?</string> + <string name="archive_report">Arquivar denúncia</string> + <string name="chat_list_all">Todos</string> + <string name="add_to_list">Adicionar à lista</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Em dispositivos Xiaomi</b>: por favor, ative a opção Autostart nas configurações do sistema para que as notificações funcionem.]]></string> + <string name="v6_1_message_dates_descr">Datas de mensagens melhores.</string> + <string name="v6_1_better_user_experience">Melhor experiência do usuário</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Você já está conectado com <b>%1$s</b>.]]></string> + <string name="archive_verb">Arquivar</string> + <string name="privacy_chat_list_open_links_ask">Perguntar</string> + <string name="appearance_bars_blur_radius">Desfoque</string> + <string name="business_address">Endereço comercial</string> + <string name="report_item_archived">denúncia arquivada</string> + <string name="button_delete_chat">Deletar chat</string> + <string name="operator_conditions_failed_to_load">O texto das condições atuais não pôde ser carregado, você pode revisar as condições por meio deste link:</string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[As mesmas condições serão aplicadas ao operador <b>%s</b>.]]></string> + <string name="v6_1_customizable_message_descr">Formato de mensagem personalizável.</string> + <string name="v6_3_faster_sending_messages">Envio de mensagens mais rápido.</string> + <string name="onboarding_notifications_mode_periodic_desc_short">Checar mensagens a cada 10 minutos</string> + <string name="block_members_desc">Todas novas mensagens destes membros serão ocultadas</string> + <string name="error_updating_server_title">Erro ao atualizar servidor</string> + <string name="enable_sending_member_reports">Permitir denunciar mensagens aos moderadores.</string> + <string name="v6_3_better_privacy_and_security">Melhorias de privacidade e segurança</string> + <string name="v6_3_organize_chat_lists_descr">Não perca mensagens importantes.</string> + <string name="connect_plan_chat_already_exists">Chat já existente!</string> + <string name="debug_logs">Ativar logs</string> + <string name="block_members_for_all_question">Bloquear membros para todos?</string> + <string name="v6_1_delete_many_messages_descr">Deletar ou moderar até 200 mensagens.</string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[As condições serão aceitas pelo(s) operador(es): <b>%s</b>.]]></string> + <string name="operators_conditions_will_also_apply"><![CDATA[Essas condições também serão aplicadas para: <b>%s</b>.]]></string> + <string name="direct_messages_are_prohibited_in_chat">Mensagens diretas entre membros são proibidas neste chat.</string> + <string name="v6_3_better_groups_performance">Melhor desempenho de grupos</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[Todas mensagens e arquivos são enviados <b>com criptografia de ponta-a-ponta</b>, e com segurança pós-quântica em mensagens diretas.]]></string> + <string name="delete_chat_for_self_cannot_undo_warning">Chat será deletado para você - essa ação não pode ser desfeita!</string> + <string name="operator_conditions_accepted_on">Condições aceitas em: %s.</string> + <string name="direct_messages_are_prohibited">Mensagens diretas entre membros são proibidas.</string> + <string name="operator_conditions_will_be_applied"><![CDATA[Essas condições também serão aplicadas para: <b>%s</b>.]]></string> + <string name="change_order_chat_list_menu_action">Alterar ordem</string> + <string name="error_reading_passphrase">Erro ao ler a senha do banco de dados</string> + <string name="error_accepting_operator_conditions">Erro ao aceitar condições</string> + <string name="report_reason_community">Violação das diretrizes da comunidade</string> + <string name="failed_to_save_servers">Erro ao salvar servidores</string> + <string name="group_reports_active">%d denúncias</string> + <string name="group_reports_active_one">1 denúncia</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[1-link temporário pode ser usado <i>com apenas um contato</i> - compartilhe pessoalmente ou por qualquer aplicativo de mensagens.]]></string> + <string name="prefs_error_saving_settings">Erro ao salvar configurações</string> + <string name="create_1_time_link">Criar link único</string> + <string name="sync_connection_confirm">Reparar</string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[Para usar os servidores de <b>%s</b>, aceite as condições de uso.]]></string> + <string name="blocking_reason_content">O conteúdo viola as condições de uso</string> + <string name="connection_error_blocked">Conexão bloqueada</string> + <string name="connection_error_blocked_desc">A conexão está bloqueada pelo operador do servidor:\n%1$s.</string> + <string name="file_error_blocked">O arquivo está bloqueado pelo operador do servidor:\n%1$s.</string> + <string name="delete_report">Deletar denúncia</string> + <string name="chat_list_businesses">Empresas</string> + <string name="change_list">Alterar lista</string> + <string name="onboarding_network_operators_continue">Continuar</string> + <string name="error_saving_database">Erro ao salvar banco de dados</string> + <string name="operators_conditions_accepted_for"><![CDATA[As condições são aceitas pelo(s) operador(es): <b>%s</b>.]]></string> + <string name="error_initializing_web_view_wrong_arch">Erro ao inicializar o WebView. Certifique-se de que você tenha o WebView instalado e que sua arquitetura suportada seja arm64.\nErro: %s</string> + <string name="change_automatic_deletion_question">Alterar exclusão automática de mensagens?</string> + <string name="disable_automatic_deletion_question">Desativar exclusão automática de mensagens?</string> + <string name="delete_chat_list_question">Deletar lista?</string> + <string name="chat_item_ttl_year">1 ano</string> + <string name="chat_item_ttl_default">padrão (%s)</string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[As condições serão aceitas pelo(s) operador(es): <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[As mesmas condições serão aplicadas ao(s) operador(es): <b>%s</b>.]]></string> + <string name="operator_conditions_accepted_for_enabled_operators_on">As condições serão aceitas automaticamente para operadores habilitados em: %s.</string> + <string name="connection_security">Segurança de conexão</string> + <string name="operator_conditions_will_be_accepted_on">As condições serão aceitas em: %s.</string> + <string name="operator_conditions_accepted_for_some"><![CDATA[As condições já foram aceitas pelo(s) seguinte(s) operador(es): <b>%s</b>.]]></string> + <string name="operator_conditions_of_use">Condições de uso</string> + <string name="error_adding_server">Erro ao adicionar servidor</string> + <string name="v6_2_business_chats">Chats de empresas</string> + <string name="v6_2_network_decentralization_enable_flux_reason">para melhoria da privacidade de metadados.</string> + <string name="v6_3_faster_deletion_of_groups">Exclusão mais rápida de grupos.</string> + <string name="error_creating_chat_tags">Erro ao criar lista de chat</string> + <string name="error_loading_chat_tags">Erro ao carregar lista de chats</string> + <string name="error_updating_chat_tags">Erro ao atualizar a lista de chats</string> + <string name="chat_list_contacts">Contatos</string> + <string name="chat_list_favorites">Favoritos</string> + <string name="create_list">Criar lista</string> + <string name="edit_chat_list_menu_action">Editar</string> + <string name="settings_message_shape_corner">Canto</string> + <string name="v6_2_network_decentralization_enable_flux">Ativar o Flux nas Configurações de rede e servidores para melhor privacidade de metadados.</string> + <string name="report_archive_alert_desc_all">Todas denúncias serão arquivadas para você.</string> + <string name="report_archive_alert_title_all">Arquivar todas denúncias?</string> + <string name="report_archive_alert_title_nth">Arquivar %d denúncias?</string> + <string name="archive_reports">Arquivar denúncias</string> + <string name="report_archive_for_all_moderators">Para todos moderadores</string> + <string name="report_archive_for_me">Para mim</string> + <string name="chat_ttl_options_footer">Deletar mensagens de chat do seu dispositivo.</string> + <string name="delete_chat_question">Excluir chat?</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">O chat será deletado para todos os membros - essa ação não pode ser desfeita!</string> + <string name="disable_automatic_deletion">Desativar exclusão de mensagens</string> + <string name="encryption_renegotiation_in_progress">Renegociação de criptografia em andamento.</string> + <string name="delete_chat_list_menu_action">Deletar</string> + <string name="call_desktop_permission_denied_chrome">Clique no botão de informação perto do campo de endereço para permitir usar o microfone.</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">As condições serão aceitas para operadores habilitados após 30 dias.</string> + <string name="onboarding_network_operators_app_will_use_for_routing">Por exemplo, se o seu contato receber mensagens por meio de um servidor SimpleX Chat, seu aplicativo as entregará por meio de um servidor Flux.</string> + <string name="connection_not_ready">A conexão não está pronta.</string> + <string name="errors_in_servers_configuration">Erros nas configurações de servidores.</string> + <string name="for_chat_profile">Para o perfil de chat %s:</string> + <string name="sync_connection_desc">A conexão requer renegociação de criptografia.</string> + <string name="sync_connection_question">Reparar conexão?</string> + <string name="error_creating_report">Erro ao criar denúncia</string> + <string name="info_row_chat">Chat</string> + <string name="your_servers">Seus servidores</string> + <string name="group_member_status_pending_approval">aprovação pendente</string> + <string name="group_member_status_pending_approval_short">pendente</string> + <string name="group_members_can_send_reports">Os membros podem denunciar mensagens aos moderadores.</string> + <string name="onboarding_network_operators">Operadores da rede</string> + <string name="operator">Operador</string> + <string name="onboarding_notifications_mode_off_desc_short">Nenhum serviço de segundo plano</string> + <string name="operator_open_changes">Abrir mudanças</string> + <string name="feature_roles_moderators">moderadores</string> + <string name="v6_2_network_decentralization">Descentralização da rede</string> + <string name="v6_2_business_chats_descr">Privacidade para seus clientes.</string> + <string name="v6_3_mentions">Mencione membros 👋</string> + <string name="v6_3_mentions_descr">Seja notificado quando mencionado.</string> + <string name="v6_3_reports_descr">Ajude os administradores a moderar seus grupos.</string> + <string name="v6_3_organize_chat_lists">Organize os chats em listas</string> + <string name="v6_3_private_media_file_names">Nomes de arquivos de mídia privados.</string> + <string name="report_reason_illegal">Conteúdo inapropriado</string> + <string name="report_reason_profile">Perfil inapropriado</string> + <string name="no_message_servers_configured">Nenhuma mensagem de servidores.</string> + <string name="message_deleted_or_not_received_error_title">Nenhuma mensagem</string> + <string name="group_reports_member_reports">Denúncias de membros</string> + <string name="or_to_share_privately">Ou compartilhe em particular</string> + <string name="no_unread_chats">Nenhum chat não lido</string> + <string name="no_chats">Nenhum chat</string> + <string name="chat_list_notes">Notas</string> + <string name="open_with_app">Abrir com %s</string> + <string name="duplicated_list_error">O nome da lista e o emoji devem ser diferentes para todas as listas.</string> + <string name="network_session_mode_server_description">Novas credenciais SOCKS serão usadas para cada servidor.</string> + <string name="onboarding_notifications_mode_battery">Notificações e bateria</string> + <string name="unblock_members_desc">As mensagens desses membros serão exibidas!</string> + <string name="mute_all_chat">Silenciar tudo</string> + <string name="for_social_media">Para redes sociais</string> + <string name="network_preset_servers_title">Servidores predefinidos</string> + <string name="privacy_chat_list_open_links">Abrir links da lista de bate-papo</string> + <string name="privacy_chat_list_open_web_link_question">Abrir web link?</string> + <string name="invite_to_chat_button">Convidar ao chat</string> + <string name="operator_open_conditions">Abrir condições</string> + <string name="list_name_field_placeholder">Nome da lista...</string> + <string name="network_session_mode_session_description">Novas credenciais SOCKS serão usadas toda vez que você iniciar o aplicativo.</string> + <string name="privacy_chat_list_open_web_link">Abrir link</string> + <string name="settings_section_title_message_shape">Forma da mensagem</string> + <string name="group_member_role_moderator">moderador</string> + <string name="maximum_message_size_title">A mensagem é muito grande!</string> + <string name="maximum_message_size_reached_text">Por favor, reduza o tamanho da mensagem e a envie novamente.</string> + <string name="operator_info_title">Operador da rede</string> + <string name="operator_use_for_messages_private_routing">Para roteamento privado</string> + <string name="v6_2_improved_chat_navigation">Aprimorada a navegação de bate-papo</string> + <string name="v6_2_improved_chat_navigation_descr">- Abra o chat na primeira mensagem não lida.\n- Pule para mensagens citadas.</string> + <string name="members_will_be_removed_from_chat_cannot_be_undone">Os membros serão removidos do chat. Essa ação não pode ser desfeita!</string> + <string name="button_leave_chat">Said do chat</string> + <string name="members_will_be_removed_from_group_cannot_be_undone">Os membros serão removidos do grupo. Essa ação não pode ser desfeita!</string> + <string name="smp_servers_new_server">Nove servidor</string> + <string name="no_chats_found">Nenhum chat encontrado</string> + <string name="disable_automatic_deletion_message">As mensagens neste chat nunca serão excluídas.</string> + <string name="restore_passphrase_can_not_be_read_desc">A frase-senha na Keystore não pôde ser lida. Isso pode ter acontecido após uma atualização do sistema incompatível com o aplicativo. Se não for o caso, entre em contato com os desenvolvedores.</string> + <string name="only_chat_owners_can_change_prefs">Somente os proprietários do chat podem alterar as preferências.</string> + <string name="restore_passphrase_can_not_be_read_enter_manually_desc">A frase-senha na Keystore não pôde ser lida, insira-a manualmente. Isso pode ter acontecido após uma atualização do sistema incompatível com o aplicativo. Se não for o caso, entre em contato com os desenvolvedores.</string> + <string name="chat_list_groups">Grupos</string> + <string name="chat_archive">Ou importar arquivo compactado</string> + <string name="list_menu">Lista</string> + <string name="maximum_message_size_reached_non_text">Reduza o tamanho da mensagem ou remova a mídia e envie novamente.</string> + <string name="how_it_helps_privacy">Como isso ajuda na privacidade</string> + <string name="privacy_chat_list_open_links_no">Não</string> + <string name="leave_chat_question">Sair do chat?</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">O membro será removido do chat - essa ação não pode ser desfeita!</string> + <string name="v6_1_forward_many_messages_descr">Encaminhe até 20 mensagens de uma vez.</string> + <string name="no_media_servers_configured">Nenhuma mídia & nenhum arquivo de servidores.</string> + <string name="no_media_servers_configured_for_sending">Nenhum servidor para enviar arquivos.</string> + <string name="no_message_servers_configured_for_private_routing">Nenhum servidor para roteamento de mensagens privadas.</string> + <string name="no_media_servers_configured_for_private_routing">Nenhum servidor para receber arquivos.</string> + <string name="no_message_servers_configured_for_receiving">Nenhum servidor para receber mensagens.</string> + <string name="call_desktop_permission_denied_safari">Abra Configurações do Safari / Websites / Microfone, e escolha Permitir para localhost.</string> + <string name="operator_server_alert_title">Servidor do operador</string> + <string name="no_chats_in_list">Nenhum chat na lista %s.</string> + <string name="report_item_visibility_moderators">Somente o remetente e os moderadores podem vê-lo.</string> + <string name="report_item_visibility_submitter">Somente você e os moderadores podem ver isso</string> + <string name="group_member_status_rejected">rejeitado</string> + <string name="report_verb">Denunciar</string> + <string name="disable_sending_member_reports">Proibir a denúncia de mensagens aos moderadores.</string> + <string name="report_compose_reason_header_illegal">Denunciar conteúdo: somente os moderadores do grupo poderão ver.</string> + <string name="report_compose_reason_header_profile">Denunciar perfil de membro: somente moderadores do grupo poderão ver.</string> + <string name="remote_hosts_section">Dispositivos móveis remotos</string> + <string name="button_remove_members_question">Remover membros?</string> + <string name="report_compose_reason_header_other">Denunciar outro: somente os moderadores do grupo poderão ver.</string> + <string name="report_reason_alert_title">Qual é a razão da denúncia?</string> + <string name="notification_group_report">Denúncia: %s</string> + <string name="group_preview_rejected">rejeitado</string> + <string name="member_reports_are_prohibited">É proibido denunciar mensagens neste grupo.</string> + <string name="chat_bottom_bar">Barra de ferramentas de chat acessível</string> + <string name="chat_list_group_reports">Denúncias</string> + <string name="error_server_operator_changed">Operador do servidor alterado.</string> + <string name="text_field_set_chat_placeholder">Definir nome do chat…</string> + <string name="share_address_publicly">Compartilhar o endereço publicamente</string> + <string name="report_compose_reason_header_community">Denunciar violação: somente os moderadores do grupo poderão ver.</string> + <string name="operator_servers_title">%s servidores</string> + <string name="v6_1_better_security_descr">Protocolos SimpleX analisados pela Trail of Bits.</string> + <string name="v6_3_reports">Enviar denúncias privadas</string> + <string name="v6_3_set_message_expiration_in_chats">Defina a expiração de mensagens em chats.</string> + <string name="report_reason_spam">Spam</string> + <string name="blocking_reason_spam">Spam</string> + <string name="save_list">Salvar lista</string> + <string name="share_1_time_link_with_a_friend">Compartilhe um link único com um amigo</string> + <string name="simplex_address_or_1_time_link">Endereço SimpleX ou link único?</string> + <string name="icon_descr_sound_muted">Som silenciado</string> + <string name="v6_1_better_calls_descr">Alterne entre áudio e vídeo durante a chamada.</string> + <string name="onboarding_select_network_operators_to_use">Selecione as operadoras de rede a serem utilizadas.</string> + <string name="operator_review_conditions">Revisar condições</string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">Endereços SimpleX e links únicos são seguros para compartilhar por meio de qualquer mensageiro.</string> + <string name="onboarding_choose_server_operators">Operadores do servidor</string> + <string name="server_added_to_operator__name">Servidor adicionado ao operador %s.</string> + <string name="strikethrough_text">riscar</string> + <string name="report_compose_reason_header_spam">Denunciar spam: somente os moderadores do grupo poderão ver.</string> + <string name="error_server_protocol_changed">Protocolo do servidor alterado.</string> + <string name="share_simplex_address_on_social_media">Compartilhe o endereço do SimpleX nas redes sociais.</string> + <string name="onboarding_network_operators_review_later">Revisar depois</string> + <string name="network_session_mode_server">Servidor</string> + <string name="onboarding_network_operators_simplex_flux_agreement">O SimpleX Chat e o Flux fizeram um acordo para incluir servidores operados pelo Flux no aplicativo.</string> + <string name="display_name_requested_to_connect">conexão solicitada</string> + <string name="member_role_will_be_changed_with_notification_chat">The role will be changed to %s. Everyone in the chat will be notified.</string> + <string name="appearance_in_app_bars_alpha">Transparência</string> + <string name="v6_1_switch_chat_profile_descr">Alterne o perfil de chat para convites únicos.</string> + <string name="unblock_members_for_all_question">Desbloquear membros para todos?</string> + <string name="operator_use_for_sending">Para enviar</string> + <string name="operator_updated_conditions">Condições atualizadas</string> + <string name="v6_2_network_decentralization_descr">O segundo operador predefinido no aplicativo!</string> + <string name="message_deleted_or_not_received_error_desc">Esta mensagem foi excluída ou ainda não foi recebida.</string> + <string name="view_updated_conditions">Ver condições atualizadas</string> + <string name="address_creation_instruction">Toque em Criar endereço SimpleX no menu para criá-lo mais tarde.</string> + <string name="network_smp_web_port_footer">Usar porta TCP %1$s quando nenhuma porta for especificada.</string> + <string name="report_archive_alert_desc">A denúncia será arquivado para você.</string> + <string name="operator_use_for_messages_receiving">Para receber</string> + <string name="change_automatic_chat_deletion_message">Esta ação não pode ser desfeita - as mensagens enviadas e recebidas neste chat antes da selecionada serão excluídas.</string> + <string name="to_protect_against_your_link_replaced_compare_codes">Para se proteger contra a substituição do seu link, você pode comparar os códigos de segurança dos contatos.</string> + <string name="unread_mentions">Menções não lidas</string> + <string name="network_smp_web_port_section_title">Porta TCP para mensagens</string> + <string name="network_smp_web_port_toggle">Usar porta web</string> + <string name="onboarding_network_operators_app_will_use_different_operators">O aplicativo protege sua privacidade usando diferentes operadores em cada conversa.</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">Quando mais de um operador está ativado, nenhum deles têm metadados para saber quem se comunica com quem.</string> + <string name="privacy_chat_list_open_links_yes">Sim</string> + <string name="chat_main_profile_sent">Seu perfil de chat será enviado aos membros do chat</string> + <string name="view_conditions">Ver condições</string> + <string name="operator_use_for_messages">Usar para mensagens</string> + <string name="you_can_set_connection_name_to_remember">Você pode definir o nome da conexão para lembrar com quem o link foi compartilhado.</string> + <string name="onboarding_network_operators_configure_via_settings">Você pode configurar servidores nas configurações.</string> + <string name="use_servers_of_operator_x">Usar %s</string> + <string name="operator_use_operator_toggle_description">Usar servidores</string> + <string name="operator_website">Website</string> + <string name="max_group_mentions_per_message_reached">Você pode mencionar até %1$s membros por mensagem!</string> + <string name="settings_message_shape_tail">Final</string> + <string name="maximum_message_size_reached_forwarding">Você pode copiar e reduzir o tamanho da mensagem para enviá-la.</string> + <string name="onboarding_network_operators_update">Atualização</string> + <string name="onboarding_network_operators_conditions_you_can_configure">Você pode configurar operadores em Configurações de rede & servidores.</string> + <string name="operator_use_for_files">Usar para arquivos</string> + <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Você deixará de receber mensagens deste chat. O histórico do chat será preservado.</string> + <string name="call_desktop_permission_denied_title">Para fazer chamadas, permita usar seu microfone. Encerre a chamada e tente ligar novamente.</string> + <string name="xftp_servers_per_user">Os servidores para novos arquivos do seu perfil de chat atual</string> + <string name="connection_error_quota_desc">A conexão atingiu o limite de mensagens não entregues, seu contato pode estar offline.</string> + <string name="connection_error_quota">Mensagens não entregues</string> + <string name="onboarding_conditions_configure_server_operators">Configurar operadores de servidor</string> + <string name="onboarding_conditions_private_chats_not_accessible">Chats privados, grupos e seus contatos não são acessíveis aos operadores de servidor.</string> + <string name="onboarding_conditions_accept">Aceitar</string> + <string name="onboarding_conditions_by_using_you_agree">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.</string> + <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Política de privacidade e condições de uso.</string> +</resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml index 072eb97ebf..544ee8af89 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml @@ -33,13 +33,12 @@ <string name="settings_section_title_icon">ÍCONE DA APLICAÇÃO</string> <string name="chat_item_ttl_month">1 mês</string> <string name="messages_section_title">Mensagens</string> - <string name="chat_archive_section">ARQUIVO DE CONVERSA</string> <string name="button_add_welcome_message">Adicionar mensagem de boas-vindas</string> <string name="users_add">Adicional perfil</string> <string name="users_delete_data_only">Apenas dados de perfil local</string> <string name="color_primary">Realçar</string> <string name="chat_preferences_you_allow">Você permite</string> - <string name="timed_messages">Mensagens que desaparecem</string> + <string name="timed_messages">Mensagens temporárias</string> <string name="chat_preferences_always">sempre</string> <string name="chat_preferences_no">não</string> <string name="set_group_preferences">Definir preferências de grupo</string> @@ -49,7 +48,7 @@ <string name="accept_feature">Aceitar</string> <string name="allow_your_contacts_to_send_disappearing_messages">Permitir que seus contatos enviem mensagens que desaparecem.</string> <string name="accept_feature_set_1_day">Definir 1 dia</string> - <string name="allow_irreversible_message_deletion_only_if">Permitir a exclusão irreversível de mensagens apenas se o seu contato permitir.</string> + <string name="allow_irreversible_message_deletion_only_if">Permitir a exclusão irreversível de mensagens apenas se o seu contato permitir. (24 horas)</string> <string name="allow_your_contacts_irreversibly_delete">Permitir que seus contatos eliminem de forma irreversível mensagens enviadas.</string> <string name="allow_voice_messages_only_if">Permitir mensagens de voz apenas se o contato permitir.</string> <string name="allow_your_contacts_to_send_voice_messages">Permitir que seus contatos enviem mensagens de voz.</string> @@ -62,12 +61,12 @@ <string name="ttl_min">%d min</string> <string name="ttl_month">%d mês</string> <string name="ttl_months">%d meses</string> - <string name="ttl_mth">%dmês</string> + <string name="ttl_mth">%dº mês</string> <string name="v4_2_group_links_desc">Administradores podem criar as ligações para entrar em grupos.</string> <string name="v4_3_voice_messages">Mensagens de voz</string> <string name="v4_2_auto_accept_contact_requests">Aceitar automaticamente pedidos de contato</string> <string name="v4_3_improved_server_configuration_desc">Adicionar servidores lendo QR codes.</string> - <string name="v4_4_disappearing_messages">Mensagens que desaparecem</string> + <string name="v4_4_disappearing_messages">Mensagens temporárias</string> <string name="v4_4_live_messages">Mensagens ao vivo</string> <string name="v4_4_disappearing_messages_desc">As mensagens enviadas serão eliminadas após o tempo definido.</string> <string name="v4_5_message_draft">Mensagem de rascunho</string> @@ -122,7 +121,7 @@ <string name="chat_item_ttl_week">1 semana</string> <string name="accept_contact_button">Aceitar</string> <string name="callstatus_accepted">aceitar chamada</string> - <string name="smp_servers_add">Adicionar servidor…</string> + <string name="smp_servers_add">Adicionar servidor</string> <string name="accept">Aceitar</string> <string name="accept_connection_request__question">Aceitar pedido de ligação\?</string> <string name="accept_contact_incognito_button">Aceitar modo anónimo</string> @@ -130,25 +129,22 @@ <string name="network_enable_socks_info">Aceder aos servidores via proxy SOCKS no porto %d\? O proxy tem de iniciar antes de ativar esta opção.</string> <string name="smp_servers_add_to_another_device">Adicionar a outro dispositivo</string> <string name="group_member_role_admin">administrador</string> - <string name="allow_to_delete_messages">Permitir apagar irreversivelmente as mensagens enviadas.</string> + <string name="allow_to_delete_messages">Permitir apagar irreversivelmente as mensagens enviadas. (24 horas)</string> <string name="delete_address__question">Eliminar endereço\?</string> <string name="delete_after">Eliminar após</string> <string name="delete_verb">Eliminar</string> <string name="delete_contact_menu_action">Eliminar</string> - <string name="chat_archive_header">Arquivo de conversa</string> <string name="delete_group_menu_action">Eliminar</string> <string name="delete_files_and_media_all">Eliminar todos os ficheiros</string> - <string name="delete_archive">Eliminar ficheiro</string> - <string name="delete_chat_archive_question">Eliminar arquivo de conversa\?</string> <string name="delete_database">Eliminar base de dados</string> <string name="chat_database_section">BASE DE DADOS DE CONVERSA</string> <string name="chat_database_deleted">Base de dados de conversa eliminada</string> <string name="display_name">Nome para Exibição</string> <string name="show_dev_options">Mostrar:</string> - <string name="deleted_description">eliminada</string> + <string name="deleted_description">eliminado</string> <string name="rcv_group_event_group_deleted">grupo eliminado</string> <string name="group_display_name_field">Nome do grupo:</string> - <string name="display_name_cannot_contain_whitespace">O nome para exibição não pode conter espaços em branco.</string> + <string name="display_name_cannot_contain_whitespace">O nome de exibição não pode conter espaços em branco.</string> <string name="ttl_m">%dm</string> <string name="dont_show_again">Não mostrar novamente</string> <string name="show_developer_options">Mostrar opções de desenvolvedor</string> @@ -157,9 +153,9 @@ <string name="allow_to_send_disappearing">Permitir enviar mensagens que desaparecem.</string> <string name="display_name__field">Nome para Exibição:</string> <string name="show_QR_code">Mostrar código QR</string> - <string name="disappearing_messages_are_prohibited">Mensagens que desaparecem são proibidas neste grupo.</string> - <string name="connect_via_link_or_qr">Conectar via ligação / código QR</string> - <string name="disappearing_prohibited_in_this_chat">Mensagens que desaparecem são proibidas nesta conversa.</string> + <string name="disappearing_messages_are_prohibited">Mensagens temporárias são proibidas neste grupo.</string> + <string name="connect_via_link_or_qr">Conectar via link / código QR</string> + <string name="disappearing_prohibited_in_this_chat">Mensagens temporárias são proibidas nesta conversa.</string> <string name="send_verb">Enviar</string> <string name="live">AO VIVO</string> <string name="send_live_message_desc">Enviar uma mensagem ao vivo - ela será atualizada para o(s) destinatário(s) à medida que você a digita</string> @@ -198,8 +194,7 @@ <string name="v4_5_message_draft_descr">Preservar o último rascunho da mensagem, com anexos.</string> <string name="icon_descr_video_asked_to_receive">Solicitada a recepção do vídeo</string> <string name="network_session_mode_user_description"><![CDATA[Uma conexão TCP separada (e credencial SOCKS) será usada <b> para cada perfil de conversa que você tiver na aplicação </b>.]]></string> - <string name="network_session_mode_entity_description">Uma conexão TCP separada (e credencial SOCKS) será usada <b> para cada contato e membro do grupo</b>. -\n<b> Por favor note</b>: se você tiver muitas conexões, o seu consumo de bateria e consumo de tráfego pode ser substancialmente maior e algumas conexões podem falhar.</string> + <string name="network_session_mode_entity_description"><![CDATA[Uma conexão TCP separada (e credencial SOCKS) será usada <b> para cada contato e membro do grupo</b>. \n<b> Por favor note</b>: se você tiver muitas conexões, o seu consumo de bateria e consumo de tráfego pode ser substancialmente maior e algumas conexões podem falhar.]]></string> <string name="icon_descr_asked_to_receive">Solicitada a recepção da imagem</string> <string name="auth_unavailable">Autenticação indisponível</string> <string name="bold_text">negrito</string> @@ -211,7 +206,6 @@ <string name="info_row_group">Grupo</string> <string name="icon_descr_audio_on">Áudio ligado</string> <string name="authentication_cancelled">Autenticação cancelada</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b> Adicionar novo contato</b>: para criar o seu código QR de utilização única para o seu contato.]]></string> <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b> Bom para a bateria </b>. O serviço em segundo plano verifica se há mensagens a cada 10 minutos. Você pode perder chamadas ou mensagens urgentes.]]></string> <string name="turning_off_service_and_periodic">A otimização da bateria está ativa, desativando o serviço em segundo plano e os pedidos periódicos de novas mensagens. Você pode reativá-los através das definições.</string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Melhor para a bateria</b>. Apenas receberá notificações enquanto a app estiver em execução (SEM serviço em segundo plano)]]></string> @@ -225,7 +219,6 @@ <string name="calls_prohibited_with_this_contact">Chamadas de áudio/vídeo são proibidas.</string> <string name="notifications_mode_service_desc">O serviço em segundo plano está sempre em execução - as notificações serão exibidas assim que as mensagens estiverem disponíveis.</string> <string name="la_authenticate">Autenticar</string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Leia o código QR </b>: para se conectar ao seu contato que lhe mostra o código QR.]]></string> <string name="callstatus_ended">chamada finalizada %1$s</string> <string name="callstatus_calling">a chamar…</string> <string name="callstatus_error">erro de chamada</string> @@ -241,7 +234,6 @@ <string name="call_on_lock_screen">Chamadas no ecrã de bloqueio:</string> <string name="alert_title_cant_invite_contacts">Não é possível convidar contatos!</string> <string name="change_verb">Alterar</string> - <string name="cant_delete_user_profile">Não é possível eliminar o perfil do utilizador!</string> <string name="feature_cancelled_item">cancelado %s</string> <string name="cannot_receive_file">Não é possível receber o ficheiro</string> <string name="icon_descr_cancel_image_preview">Cancelar pré-visualização da imagem</string> @@ -254,7 +246,7 @@ <string name="chat_database_imported">Base de dados de conversa importada</string> <string name="chat_is_stopped">A conversa está parada</string> <string name="smp_servers_check_address">Verifique o endereço do servidor e tente novamente.</string> - <string name="profile_will_be_sent_to_contact_sending_link">O seu perfil será enviado para o contato do qual você recebeu esta ligação.</string> + <string name="profile_will_be_sent_to_contact_sending_link">O seu perfil será enviado para o contacto do qual você recebeu esta ligação.</string> <string name="error_deleting_database">Erro ao eliminar a base de dados de conversa</string> <string name="error_deleting_link_for_group">Erro ao eliminar ligação de grupo</string> <string name="network_session_mode_user">Perfil de conversa</string> @@ -273,14 +265,14 @@ <string name="change_role">Alterar função</string> <string name="change_member_role_question">Alterar a função no grupo\?</string> <string name="error_changing_role">Erro ao alterar função</string> - <string name="chat_preferences_contact_allows">O contato permite</string> + <string name="chat_preferences_contact_allows">O contacto permite</string> <string name="chat_preferences">Preferências de conversa</string> <string name="app_name">SimpleX</string> <string name="thousand_abbreviation">m</string> - <string name="connect_via_contact_link">Conectar através da ligação de contato\?</string> - <string name="connect_via_invitation_link">Conectar via convite de ligação\?</string> + <string name="connect_via_contact_link">Conectar através do endereço de contacto?</string> + <string name="connect_via_invitation_link">Conectar via link de convite?</string> <string name="connect_via_group_link">Conectar através da ligação do grupo\?</string> - <string name="you_will_join_group">Você irá juntar-se a um grupo ao qual esta ligação se refere e conectar-se aos membros do grupo.</string> + <string name="you_will_join_group">Você irá conectar-se a todos os membros do grupo.</string> <string name="server_error">erro</string> <string name="failed_to_create_user_title">Erro ao criar perfil!</string> <string name="error_adding_members">Erro ao adicionar membro(s)</string> @@ -337,7 +329,7 @@ <string name="group_member_status_accepted">conectando (aceite)</string> <string name="group_member_status_announced">conectando (anunciado)</string> <string name="group_member_status_connecting">conectando</string> - <string name="icon_descr_contact_checked">Contato verificado</string> + <string name="icon_descr_contact_checked">Contacto verificado</string> <string name="group_link">Ligação de grupo</string> <string name="display_name_connecting">conectando…</string> <string name="display_name_connection_established">conexão estabelecida</string> @@ -351,36 +343,33 @@ <string name="icon_descr_server_status_connected">Conectado</string> <string name="contact_connection_pending">conectando…</string> <string name="connection_timeout">Tempo limite de conexão</string> - <string name="contact_preferences">Preferências de contato</string> - <string name="notification_preview_somebody">Contato escondido:</string> - <string name="alert_title_contact_connection_pending">O contato ainda não está conectado!</string> - <string name="notification_preview_mode_contact">Nome do contato</string> + <string name="contact_preferences">Preferências de contacto</string> + <string name="notification_preview_somebody">contacto escondido:</string> + <string name="alert_title_contact_connection_pending">O contacto ainda não está conectado!</string> + <string name="notification_preview_mode_contact">Nome do contacto</string> <string name="contribute">Contribuir</string> <string name="copy_verb">Copiar</string> <string name="core_version">Versão principal: v%s</string> - <string name="archive_created_on_ts">Criado a %1$s</string> <string name="create_group_link">Criar ligação de grupo</string> <string name="v4_2_group_links">Ligações de grupo</string> <string name="callstatus_connecting">conectando chamada…</string> <string name="connect_via_link">Conectar via ligação</string> <string name="connection_error">Erro de conexão</string> <string name="connection_local_display_name">conexão %1$d</string> - <string name="contact_already_exists">O contato já existe</string> - <string name="create_one_time_link">Criar convite de ligação de utilização única</string> + <string name="contact_already_exists">O contacto já existe</string> <string name="one_time_link">Convite de ligação de utilização única</string> <string name="save_servers_button">Salvar</string> <string name="settings_section_title_incognito">Modo anónimo</string> <string name="rcv_group_event_invited_via_your_group_link">convidado através da ligação do seu grupo</string> - <string name="invite_prohibited_description">Você está a tentar convidar um contato com quem partilhou um perfil anónimo para o grupo no qual voçê está a usar o seu perfil principal</string> + <string name="invite_prohibited_description">Você está a tentar convidar um contacto com quem partilhou um perfil anónimo para um grupo no qual você está a usar o seu perfil principal</string> <string name="info_row_connection">Conexão</string> <string name="incognito_info_protects">O modo anónimo protege a privacidade do nome e da imagem do seu perfil principal — para cada novo contato um novo perfil aleatório é criado.</string> - <string name="save_color">Salvar cor</string> - <string name="description_you_shared_one_time_link">você partilhou ligação de utilização única</string> - <string name="description_you_shared_one_time_link_incognito">você partilhou ligação anónima de utilização única</string> + <string name="description_you_shared_one_time_link">você partilhou a ligação de utilização única</string> + <string name="description_you_shared_one_time_link_incognito">você partilhou a ligação anónima de utilização única</string> <string name="invalid_connection_link">Ligação de conexão inválida</string> <string name="save_verb">Salvar</string> <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Se você recebeu convite de ligação do SimpleX Chat, você pode abri-lo no seu navegador:</string> - <string name="group_invitation_tap_to_join_incognito">Toque para entrar em modo anónimo</string> + <string name="group_invitation_tap_to_join_incognito">Toque para entrar no modo anônimo</string> <string name="save_passphrase_in_keychain">Salvar senha na Keystore</string> <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Se não se poderem encontrar pessoalmente, <b>mostre o código QR na chamada de vídeo </b> ou partilhe a ligação.]]></string> <string name="save_auto_accept_settings">Salvar definições de aceitação automática</string> @@ -388,7 +377,6 @@ <string name="save_and_update_group_profile">Salvar e atualizar o perfil do grupo</string> <string name="network_options_save">Salvar</string> <string name="save_passphrase_and_open_chat">Salvar senha e abrir conversa</string> - <string name="save_archive">Salvar arquivo</string> <string name="join_group_incognito_button">Junte-se em modo anónimo</string> <string name="delete_link">Apagar ligação</string> <string name="address_section_title">Endereço</string> @@ -402,9 +390,9 @@ <string name="simplex_link_mode_description">Descrição</string> <string name="simplex_link_mode_full">Ligação completa</string> <string name="description_via_contact_address_link_incognito">anónimo via ligação de endereço de contato</string> - <string name="simplex_link_mode_browser_warning">Abrir a ligação no navegador pode reduzir a privacidade e a segurança da ligação. As ligações Simplex não confiáveis serão vermelhas.</string> + <string name="simplex_link_mode_browser_warning">Abrir a ligação no browser poderá reduzir a privacidade e a segurança da ligação. As ligações Simplex não confiáveis serão vermelhas.</string> <string name="auth_confirm_credential">Confirmar credenciais</string> - <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Contato e todas as mensagens serão eliminadas - esta acção não pode ser revertida!</string> + <string name="delete_contact_all_messages_deleted_cannot_undo_warning">O contacto e todas as mensagens serão eliminadas - esta ação é irreversível!</string> <string name="image_descr_link_preview">ligação de visualização de imagem</string> <string name="invalid_contact_link">Ligação inválida!</string> <string name="read_more_in_user_guide_with_link"><![CDATA[Leia mais em <font color="#0088ff">Guia de Utilizador</font>.]]></string> @@ -419,12 +407,12 @@ <string name="add_contact">Convite de ligação de utilização única</string> <string name="all_your_contacts_will_remain_connected_update_sent">Todos os seus contatos permanecerão conectados. A atualização do perfil será enviada aos seus contatos.</string> <string name="add_address_to_your_profile">Adicione endereço ao seu perfil, para que os seus contatos possam partilhá-lo com outras pessoas. A atualização do perfil será enviada aos seus contatos.</string> - <string name="contacts_can_mark_messages_for_deletion">Os contatos podem marcar mensagens para eliminar; você será capaz de as ver.</string> + <string name="contacts_can_mark_messages_for_deletion">Os contactos podem marcar mensagens para eliminar; você será capaz de as ver.</string> <string name="share_one_time_link">Criar convite de ligação de utilização única</string> <string name="description_via_one_time_link_incognito">anónimo via ligação de utilização única</string> <string name="description_via_group_link_incognito">anónimo via ligação de grupo</string> <string name="description_via_one_time_link">via ligação de utilização única</string> - <string name="alert_title_cant_invite_contacts_descr">Você está a usar um perfil anónimo para este grupo - para impedir a partilha do seu perfil principal não é permitido convidar contatos</string> + <string name="alert_title_cant_invite_contacts_descr">Você está a usar um perfil anónimo para este grupo - para impedir a partilha do seu perfil principal, não é permitido convidar contactos</string> <string name="one_time_link_short">Ligação para 1 utilização</string> <string name="button_create_group_link">Criar ligação</string> <string name="delete_link_question">Apagar ligação\?</string> @@ -438,8 +426,8 @@ <string name="share_invitation_link">Partilhar ligação de utilização única</string> <string name="read_more_in_github_with_link"><![CDATA[Leia mais no nosso repositório <font color="#0088ff">GitHub</font>.]]></string> <string name="create_profile">Criar perfil</string> - <string name="create_your_profile">Criar perfil</string> - <string name="num_contacts_selected">%d contato(s) selecionado(s)</string> + <string name="create_your_profile">Criar o seu perfil</string> + <string name="num_contacts_selected">%d contacto(s) selecionado(s)</string> <string name="users_delete_profile_for">Eliminar perfil de conversa para</string> <string name="full_deletion">Eliminar para todos</string> <string name="ttl_d">%dd</string> @@ -452,16 +440,16 @@ <string name="delete_files_and_media_for_all_users">Eliminar ficheiros de todos os perfis de conversa</string> <string name="delete_messages">Eliminar mensagens</string> <string name="total_files_count_and_size">%d ficheiros(s) com tamanho total de %s</string> - <string name="current_passphrase">Senha atual…</string> - <string name="delete_pending_connection__question">Eliminar conexão pendente\?</string> + <string name="current_passphrase">Palavra-passe atual…</string> + <string name="delete_pending_connection__question">Eliminar ligação pendente?</string> <string name="delete_messages_after">Eliminar mensagens após</string> <string name="users_delete_question">Eliminar perfil de conversa\?</string> <string name="delete_chat_profile">Eliminar perfil de conversa</string> <string name="ttl_hour">%d hora</string> <string name="ttl_hours">%d horas</string> - <string name="database_error">erro de base de dados</string> - <string name="passphrase_is_different">A senha da base de dados é diferente da guardada na Keystore.</string> - <string name="database_passphrase_is_required">A senha da base de dados é necessária para abrir a conversa.</string> + <string name="database_error">Erro de base de dados</string> + <string name="passphrase_is_different">A palavra-passe da base de dados é diferente da armazenada na Keystore.</string> + <string name="database_passphrase_is_required">A palavra-passe da base de dados é necessária para abrir a conversa.</string> <string name="button_delete_group">Eliminar grupo</string> <string name="delete_group_question">Eliminar grupo\?</string> <string name="conn_level_desc_direct">direta</string> @@ -469,20 +457,20 @@ <string name="delete_profile">Eliminar perfil</string> <string name="smp_server_test_create_queue">Criar fila</string> <string name="for_me_only">Eliminar para mim</string> - <string name="auth_disable_simplex_lock">Desabilitar o bloqueio do SimpleX</string> + <string name="auth_disable_simplex_lock">Desativar o bloqueio do SimpleX</string> <string name="maximum_supported_file_size">Atualmente o tamanho máximo de ficheiro suportado é %1$s.</string> - <string name="button_delete_contact">Eliminar contato</string> + <string name="button_delete_contact">Eliminar contacto</string> <string name="create_group">Criar grupo secreto</string> <string name="smp_servers_delete_server">Eliminar servidor</string> <string name="customize_theme_title">Personalizar tema</string> <string name="delete_image">Eliminar imagem</string> <string name="dont_create_address">Não criar endereço</string> - <string name="no_call_on_lock_screen">Desabilitar</string> - <string name="database_passphrase">Senha da base de dados</string> + <string name="no_call_on_lock_screen">Desativar</string> + <string name="database_passphrase">Palavra-passe da base de dados</string> <string name="create_simplex_address">Criar endereço SimpleX</string> <string name="info_row_database_id">ID da base de dados</string> <string name="smp_server_test_delete_file">Eliminar ficheiro</string> - <string name="delete_contact_question">Eliminar contato\?</string> + <string name="delete_contact_question">Eliminar contacto?</string> <string name="settings_section_title_device">DISPOSITIVO</string> <string name="direct_messages">Mensagens diretas</string> <string name="decentralized">Descentralizado</string> @@ -492,19 +480,19 @@ <string name="ttl_s">%ds</string> <string name="ttl_sec">%d seg</string> <string name="ttl_w">%dw</string> - <string name="failed_to_create_user_duplicate_title">Nome para exibição duplicado!</string> + <string name="failed_to_create_user_duplicate_title">Nome de exibição duplicado!</string> <string name="la_seconds">%d segundos</string> <string name="file_saved">Ficheiro guardado</string> <string name="database_upgrade">Atualização da base de dados</string> <string name="ttl_week">%d semana</string> <string name="ttl_weeks">%d semanas</string> - <string name="direct_messages_are_prohibited_in_chat">Mensagens diretas entre membros são proibidas neste grupo.</string> + <string name="direct_messages_are_prohibited_in_group">Mensagens diretas entre membros são proibidas neste grupo.</string> <string name="smp_server_test_delete_queue">Eliminar fila</string> <string name="smp_server_test_download_file">Transferir ficheiro</string> <string name="icon_descr_file">Ficheiro</string> <string name="file_not_found">Ficheiro não encontrado</string> <string name="choose_file">Ficheiro</string> - <string name="you_wont_lose_your_contacts_if_delete_address">Você não perderá os seus contatos se eliminar o seu endereço mais tarde.</string> + <string name="you_wont_lose_your_contacts_if_delete_address">Você não irá perder os seus contatos se eliminar o seu endereço mais tarde.</string> <string name="you_can_hide_or_mute_user_profile">Você pode esconder ou silenciar um perfil de utilizador - pressione-o para o menu.</string> <string name="icon_descr_video_on">Vídeo ligado</string> <string name="xftp_servers">Servidores XFTP</string> @@ -530,7 +518,7 @@ <string name="wrong_passphrase">Senha da base de dados incorreta</string> <string name="group_member_status_left">esquerda</string> <string name="wrong_passphrase_title">Senha errada!</string> - <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Você deixará de receber mensagens deste grupo. O histórico de mensagens será preservado.</string> + <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Você irá deixar de receber mensagens deste grupo. O histórico de mensagens será preservado.</string> <string name="join_group_question">Juntar-se ao grupo\?</string> <string name="v4_2_auto_accept_contact_requests_desc">Com mensagem de boas-vindas opcional.</string> <string name="simplex_link_connection">via %1$s</string> @@ -563,14 +551,14 @@ <string name="delete_chat_profile_action_cannot_be_undone_warning">Esta ação não pode ser revertida - o seu perfil, contatos, mensagens e ficheiros serão irreversivelmente perdidos.</string> <string name="enable_automatic_deletion_message">Esta ação não pode ser revertida - as mensagens enviadas e recebidas antes da seleção serão eliminadas. Pode demorar vários minutos.</string> <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">A sua base de dados atual de conversas será ELIMINADA e SUBSTITUÍDA pela importada. -\nEsta ação não pode ser revertida - o seu perfil, contatos, mensagens e ficheiros serão irreversivelmente perdidos.</string> +\nEsta ação é irreversível - o seu perfil, contactos, mensagens e ficheiros serão irreversivelmente perdidos.</string> <string name="mark_unread">Marcar como não lido</string> <string name="group_member_role_member">membro</string> <string name="member_info_section_title_member">MEMBRO</string> <string name="v4_3_voice_messages_desc">Máximo de 40 segundos, recebido instantaneamente.</string> <string name="icon_descr_more_button">Mais</string> <string name="network_and_servers">Rede e servidores</string> - <string name="network_settings_title">Definições de rede</string> + <string name="network_settings_title">Configurações avançadas</string> <string name="settings_section_title_experimenta">EXPERIMENTAL</string> <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Você pode iniciar a conversa através das Definições da aplicação / Base de Dados ou reiniciando a aplicação.</string> <string name="update_network_settings_confirmation">Atualizar</string> @@ -583,7 +571,6 @@ <string name="settings_notification_preview_title">Pré-visualização de notificação</string> <string name="message_delivery_error_desc">Muito provavelmente este contato eliminou a conexão consigo.</string> <string name="this_text_is_available_in_settings">Este texto está disponível nas definições</string> - <string name="update_onion_hosts_settings_question">Atualizar definições de servidores .onion\?</string> <string name="onboarding_notifications_mode_subtitle">Pode ser alterado mais tarde através das definições.</string> <string name="settings_section_title_help">AJUDA</string> <string name="settings_section_title_support">SUPORTE SIMPLEX CHAT</string> @@ -596,29 +583,28 @@ <string name="new_in_version">Novidades %s</string> <string name="notification_new_contact_request">Novo pedido de contato</string> <string name="new_database_archive">Novo arquivo de base de dados</string> - <string name="encrypted_with_random_passphrase">A base de dados está encriptada com uma senha aleatória, você pode alterá-la.</string> + <string name="encrypted_with_random_passphrase">A base de dados está encriptada com uma palavra-passe aleatória, você pode alterá-la.</string> <string name="enter_correct_passphrase">Insira a senha correta.</string> <string name="database_backup_can_be_restored">A tentativa de alterar a senha da base de dados não foi concluída.</string> - <string name="database_is_not_encrypted">A sua base de dados de conversas não está encriptada - defina a senha para a proteger.</string> + <string name="database_is_not_encrypted">A sua base de dados de conversas não está encriptada - defina a palavra-passe para a proteger.</string> <string name="enter_passphrase">Insira a senha…</string> - <string name="you_have_to_enter_passphrase_every_time">Você tem que inserir a senha sempre que a aplicação é iniciada - ela não é guardada no dispositivo.</string> + <string name="you_have_to_enter_passphrase_every_time">Você tem que inserir a palavra-passe sempre que a aplicação é iniciada - ela não é armazenada no dispositivo.</string> <string name="new_passphrase">Nova senha…</string> <string name="remove_passphrase">Remover</string> <string name="remove_passphrase_from_keychain">Remover senha da Keystore\?</string> <string name="enter_correct_current_passphrase">Insira a senha atual correta.</string> <string name="update_database_passphrase">Atualizar senha da base de dados</string> <string name="notification_preview_new_message">nova mensagem</string> - <string name="database_passphrase_and_export">Senha da base de dados & exportação</string> - <string name="set_password_to_export_desc">A base de dados está encriptada com uma senha aleatória. Por favor, altere-a antes de exportar.</string> - <string name="database_passphrase_will_be_updated">A senha de encriptação da base de dados será atualizada.</string> + <string name="database_passphrase_and_export">Palavra-passe da base de dados & exportação</string> + <string name="set_password_to_export_desc">A base de dados está encriptada com uma palavra-passe aleatória. Por favor, altere-a antes de exportar.</string> + <string name="database_passphrase_will_be_updated">A palavra-passe de encriptação da base de dados será atualizada.</string> <string name="store_passphrase_securely">Por favor armazene a senha de forma segura, você NÃO será capaz de a alterar se a perder.</string> - <string name="database_encryption_will_be_updated">A senha de encriptação da base de dados será atualizada e armazenada na Keystore.</string> + <string name="database_encryption_will_be_updated">A palavra-passe de encriptação da base de dados será atualizada e armazenada nas definições.</string> <string name="database_will_be_encrypted_and_passphrase_stored">A base de dados será encriptada e a senha armazenada na Keystore.</string> <string name="store_passphrase_securely_without_recover">Por favor armazene a senha de forma segura, você NÃO será capaz de aceder às conversas se a perder.</string> <string name="enter_passphrase_notification_desc">Para receber notificações, por favor, digite a senha da base de dados</string> <string name="network_use_onion_hosts_no_desc">Hosts Onion não serão usados.</string> <string name="network_use_onion_hosts_prefer_desc">Hosts Onion serão usados quando disponíveis.</string> - <string name="network_use_onion_hosts_required_desc_in_alert">Hosts Onion serão necessários para a conexão.</string> <string name="video_call_no_encryption">chamada de vídeo (sem encriptação ponta a ponta)</string> <string name="audio_call_no_encryption">chamada de áudio (não encriptada ponta a ponta)</string> <string name="encrypted_audio_call">chamada de áudio encriptada ponta a ponta</string> @@ -628,18 +614,17 @@ <string name="feature_offered_item">oferecido %s</string> <string name="old_database_archive">Arquivo de base de dados antigo</string> <string name="network_use_onion_hosts_required_desc">Hosts Onion serão necessários para a conexão.</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">Hosts Onion serão usados quando disponíveis.</string> <string name="alert_text_fragment_encryption_out_of_sync_old_database">Pode acontecer quando você ou sua conexão usaram o backup de base de dados antigo.</string> <string name="status_e2e_encrypted">encriptado ponta a ponta</string> <string name="feature_off">desligado</string> <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 desktop: leia o código QR exibido a partir do aplicação, via <b>Ler código QR</b>.]]></string> - <string name="status_contact_has_no_e2e_encryption">contato não tem encriptação ponta a ponta</string> + <string name="status_contact_has_no_e2e_encryption">o contacto não tem encriptação ponta a ponta</string> <string name="feature_offered_item_with_param">oferecido %s: %2s</string> <string name="chat_preferences_off">desligado</string> <string name="chat_preferences_on">ligado</string> <string name="v4_6_group_moderation_descr">Agora os administradores podem: \n- eliminar mensagens de membros. -\n- desativar membros (função de \"observador\")</string> +\n- desativar membros (função de "observador")</string> <string name="simplex_link_group">Ligação do grupo SimpleX</string> <string name="images_limit_desc">Apenas 10 imagens podem ser enviadas ao mesmo tempo</string> <string name="videos_limit_desc">Apenas 10 vídeos podem ser enviados ao mesmo tempo</string> @@ -649,9 +634,8 @@ <string name="to_verify_compare">Para verificar a encriptação de ponta a ponta com o seu contato, compare (ou leia) o código nos seus dispositivos.</string> <string name="scan_code_from_contacts_app">Ler o código de segurança a partir da aplicação do seu contacto.</string> <string name="smp_servers_scan_qr">Ler o código QR do servidor</string> - <string name="network_use_onion_hosts_no_desc_in_alert">Hosts Onion não serão usados.</string> - <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Apenas dispositivos cliente armazenam perfis de utilizador, contatos, grupos e mensagens enviadas com <b>encriptação de ponta a ponta de 2 camadas</b>.]]></string> - <string name="status_contact_has_e2e_encryption">contato tem encriptação ponta a ponta</string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Apenas dispositivos pessoais armazenam perfis de utilizador, contatos, grupos e mensagens.</string> + <string name="status_contact_has_e2e_encryption">o contacto tem encriptação ponta a ponta</string> <string name="status_no_e2e_encryption">sem encriptação ponta a ponta</string> <string name="group_member_status_creator">criador</string> <string name="custom_time_unit_months">meses</string> @@ -690,51 +674,301 @@ <string name="icon_descr_email">Email</string> <string name="network_session_mode_transport_isolation">Isolamento do Transporte</string> <string name="callstate_ended">terminado</string> - <string name="settings_developer_tools">Ferramentas de desenvolvedor</string> + <string name="settings_developer_tools">Ferramentos de programador</string> <string name="encrypt_database">Encriptar</string> <string name="downgrade_and_open_chat">Fazer downgrade e abrir chat</string> - <string name="network_option_enable_tcp_keep_alive">Activar TCP manter-vivo</string> + <string name="network_option_enable_tcp_keep_alive">Ativar TCP keep-alive</string> <string name="chat_preferences_default">predefinido (%s)</string> <string name="v4_5_transport_isolation">Isolamento do Transporte</string> <string name="trying_to_connect_to_server_to_receive_messages">A tentar connectar ao servidor usado para receber mensagens deste contacto.</string> <string name="image_decoding_exception_title">Erro de descodificação</string> <string name="developer_options">IDs das bases de dados e opções de isolamento do Transporte</string> - <string name="database_downgrade">Downgrade da base de dados</string> - <string name="auth_enable_simplex_lock">Activar SimpleX Lock</string> + <string name="database_downgrade">Regressão da base de dados</string> + <string name="auth_enable_simplex_lock">Ativar SimpleX Lock</string> <string name="edit_verb">Editar</string> <string name="icon_descr_edited">editado</string> <string name="disappearing_message">Mensagem temporária</string> <string name="edit_image">Editar imagem</string> - <string name="enable_lock">Activar bloqueio</string> - <string name="enabled_self_destruct_passcode">Activar código de acesso auto-destrutivo</string> - <string name="enable_self_destruct">Activar auto-destruição</string> + <string name="enable_lock">Ativar bloqueio</string> + <string name="enabled_self_destruct_passcode">Ativar código de acesso auto-destrutivo</string> + <string name="enable_self_destruct">Ativar auto-destruição</string> <string name="empty_chat_profile_is_created">Um perfil vazio é criado com o nome fornecido, e a aplicação abre como de costume.</string> - <string name="mtr_error_no_down_migration">a versão da base de dados é mais recente do que a aplicação, mas sem migração para baixo para: %s</string> - <string name="share_text_disappears_at">Desaparecerá: %s</string> + <string name="mtr_error_no_down_migration">a versão da base de dados é mais recente do que a aplicação, mas sem migração de regressão para: %s</string> + <string name="share_text_disappears_at">Desaparecerá a: %s</string> <string name="delete_files_and_media_question">Eliminar ficheiros e multimédia\?</string> <string name="share_text_deleted_at">Eliminado a: %s</string> - <string name="auth_device_authentication_is_disabled_turning_off">A autenticação do dispositivo está desactivada. A desligar SimpleX Lock.</string> - <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">A autenticação do dispositivo não está activada. Pode activar o SimpleX Lock através das Definições, depois de activar a autenticação do dispositivo.</string> + <string name="auth_device_authentication_is_disabled_turning_off">A autenticação do dispositivo está desativada. A desligar SimpleX Lock.</string> + <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">A autenticação do dispositivo não está ativa. Pode ativar o SimpleX Lock através das Definições, depois de ativar a autenticação do dispositivo.</string> <string name="mtr_error_different">migração diferente na aplicação/base de dados: %s / %s</string> - <string name="allow_accepting_calls_from_lock_screen">Activar chamadas a partir do ecrã de bloqueio através das Definições.</string> + <string name="allow_accepting_calls_from_lock_screen">Ativar chamadas a partir do ecrã de bloqueio através das Definições.</string> <string name="encrypt_database_question">Encriptar base de dados\?</string> <string name="smp_servers_enter_manually">Introduzir o servidor manualmente</string> - <string name="enable_automatic_deletion_question">Activar a eliminação automática de mensagens\?</string> + <string name="enable_automatic_deletion_question">Ativar a eliminação automática de mensagens?</string> <string name="button_edit_group_profile">Editar perfil de grupo</string> <string name="decryption_error">Erro de desencriptação</string> <string name="app_passcode_replaced_with_self_destruct">O código de acesso é substituído por um código auto-destrutivo.</string> <string name="share_text_database_id">ID da base de dados: %d</string> - <string name="v4_5_multiple_chat_profiles_descr">Nomes, avatares e isolamento de transporte diferentes.</string> + <string name="v4_5_multiple_chat_profiles_descr">Nomes, fotos de perfil e isolamento de transporte diferentes.</string> <string name="color_secondary_variant">Secundário adicional</string> <string name="color_primary_variant">Realce adicional</string> <string name="color_background">Fundo</string> - <string name="v5_1_custom_themes_descr">Customizar e partilhar temas de cor.</string> + <string name="v5_1_custom_themes_descr">Personalizar e partilhar temas de cor.</string> <string name="v5_1_custom_themes">Temas personalizados</string> <string name="info_row_deleted_at">Eliminado a</string> - <string name="info_row_disappears_at">Desaparecerá</string> - <string name="feature_enabled">activado</string> - <string name="feature_enabled_for_contact">activado para contacto</string> - <string name="feature_enabled_for_you">activado para si</string> + <string name="info_row_disappears_at">Desaparecerá a</string> + <string name="feature_enabled">Ativo</string> + <string name="feature_enabled_for_contact">ativo para contacto</string> + <string name="feature_enabled_for_you">ativo para si</string> <string name="search_verb">Pesquisar</string> <string name="la_mode_off">Desativado</string> -</resources> \ No newline at end of file + <string name="error_smp_test_failed_at_step">O teste falhou na etapa %s.</string> + <string name="smp_servers">Servidor SMP</string> + <string name="smp_servers_test_server">Servidor de teste</string> + <string name="chat_item_ttl_seconds">%s segundo(s)</string> + <string name="stop_chat_question">Parar conversa?</string> + <string name="theme_system">Sistema</string> + <string name="v4_6_audio_video_calls_descr">Suporte a bluetooth e outras melhorias.</string> + <string name="migrate_from_device_stopping_chat">Parando a conversa</string> + <string name="migrate_from_device_bytes_uploaded">%s enviado</string> + <string name="migrate_from_device_start_chat">Iniciar conversa</string> + <string name="tap_to_paste_link">Toque para colar o link</string> + <string name="stop_sharing_address">Parar de compartilhar o endereço?</string> + <string name="callstate_starting">iniciando…</string> + <string name="stop_chat_to_export_import_or_delete_chat_database">Pare a conversa para exportar, importar ou excluir o banco de dados de conversa. Você não poderá receber e enviar mensagens enquanto a conversa for interrompida.</string> + <string name="rcv_group_event_n_members_connected">%s, %s e %d outros membros conectados</string> + <string name="start_chat_question">Iniciar conversa?</string> + <string name="notifications_mode_periodic">Começa periodicamente</string> + <string name="tap_to_start_new_chat">Toque para iniciar uma nova conversa</string> + <string name="la_lock_mode_system">Sistema de autenticação</string> + <string name="stop_rcv_file__title">Parar de receber o arquivo?</string> + <string name="smp_servers_test_servers">Servidores de teste</string> + <string name="smp_servers_test_some_failed">Alguns servidores falharam no teste:</string> + <string name="stop_sharing">Parar compartilhamento</string> + <string name="alert_title_skipped_messages">Mensagens ignoradas</string> + <string name="receipts_section_groups">Grupos pequeno (máx. 20)</string> + <string name="non_fatal_errors_occured_during_import">Alguns erros não fatais ocorreram durante a importação - você pode ver o console de conversa para obter mais detalhes.</string> + <string name="conn_event_disabled_pq">encriptação end-to-end padrão</string> + <string name="chat_help_tap_button">Toque no botão</string> + <string name="tap_to_activate_profile">Toque para ativar o perfil.</string> + <string name="stop_snd_file__title">Parar de enviar o arquivo?</string> + <string name="v4_4_french_interface_descr">Obrigado aos usuários – contribuam via Weblate!</string> + <string name="stop_file__confirm">Pare</string> + <string name="stop_chat_confirmation">Pare</string> + <string name="icon_descr_call_rejected">Chamada rejeitada</string> + <string name="stop_file__action">Parar arquivo</string> + <string name="contact_tap_to_connect">Toque para Conectar</string> + <string name="group_members_n">%s, %s e %d membros</string> + <string name="add_contact_or_create_group">Iniciar nova conversa</string> + <string name="la_mode_system">Sistema</string> + <string name="group_invitation_tap_to_join">Toque para participar</string> + <string name="rcv_group_event_3_members_connected">%s, %s e %s conectado</string> + <string name="network_option_tcp_connection_timeout">Tempo esgotado da conexão TCP</string> + <string name="language_system">Sistema</string> + <string name="auth_stop_chat">Parar conversa</string> + <string name="v4_5_italian_interface_descr">Obrigado aos usuários – contribuam via Weblate!</string> + <string name="possible_slow_function_title">Função lenta</string> + <string name="delivery">Entrega</string> + <string name="your_preferences">As suas preferências</string> + <string name="disconnect_remote_hosts">Desconectar dispositivos móveis</string> + <string name="blocked_by_admin_items_description">%d mensagens bloqueadas pelo administrador</string> + <string name="open_port_in_firewall_title">Abrir porta na firewall</string> + <string name="your_privacy">A sua privacidade</string> + <string name="receipts_groups_enable_keep_overrides">Ativar (manter sobreposição de grupo)</string> + <string name="auth_open_migration_to_another_device">Abrir ecrã de migração</string> + <string name="snd_error_relay">Erro de servidor do destino: %1$s</string> + <string name="you_can_turn_on_lock">Você pode ativar o SimpleX Lock através das Definições.</string> + <string name="delete_messages__question">Eliminar %d mensagens?</string> + <string name="create_chat_profile">Criar perfil de chat</string> + <string name="permissions_open_settings">Abrir definições</string> + <string name="receipts_section_contacts">Contactos</string> + <string name="snd_group_event_user_left">você saiu</string> + <string name="snd_group_event_member_deleted">você removeu %1$s</string> + <string name="conn_event_ratchet_sync_ok">encriptação ok</string> + <string name="abort_switch_receiving_address">Cancelar mudança de endereço</string> + <string name="you_will_still_receive_calls_and_ntfs">Você continuará a receber chamadas e notificações de perfis silenciados quando estes estão ativos.</string> + <string name="v5_6_quantum_resistant_encryption_descr">Ativar em conversas diretas (BETA)!</string> + <string name="desktop_address">Endereço de computador</string> + <string name="multicast_discoverable_via_local_network">Descobrível na rede local</string> + <string name="remote_ctrl_error_bad_version">Desktop tem uma versão não suportada. Por favor, confirme que usa a mesma versão em ambos os dispositivos</string> + <string name="remote_ctrl_error_bad_invitation">Desktop tem o código de convite errado</string> + <string name="migrate_to_device_downloading_archive">A transferir arquivo</string> + <string name="migrate_to_device_downloading_details">A transferir detalhes de ligação</string> + <string name="migrate_from_device_creating_archive_link">A criar ligação de arquivo</string> + <string name="duplicates_label">duplicados(as)</string> + <string name="open_server_settings_button">Abrir definições do servidor</string> + <string name="your_chat_profile_will_be_sent_to_your_contact">O seu perfil de conversa será enviado +\npara o seu contacto</string> + <string name="connect_with_contact_name_question">Conectar com %1$s?</string> + <string name="audio_device_earpiece">Auricular</string> + <string name="devices">Dispositivos</string> + <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Abrir <i>Usar a partir de computador</i> na aplicação mobile e ler código QR.]]></string> + <string name="you_sent_group_invitation">Você enviou um convite de grupo</string> + <string name="snd_group_event_changed_role_for_yourself">você mudou o seu cargo para %s</string> + <string name="snd_conn_event_switch_queue_phase_completed_for_member">você mudou o endereço para %s</string> + <string name="v5_3_new_interface_languages">6 novos idiomas de interface</string> + <string name="desktop_app_version_is_incompatible">A versão %s da aplicação de computador não é compatível com esta aplicação.</string> + <string name="remote_host_disconnected_from"><![CDATA[Desconectado do dispositivo móvel <b>%s</b> com a razão: %s]]></string> + <string name="connect__your_profile_will_be_shared">O seu perfil %1$s vai ser partilhado.</string> + <string name="download_file">Transferir</string> + <string name="abort_switch_receiving_address_confirm">Cancelar</string> + <string name="developer_options_section">Opções de programador</string> + <string name="enable_receipts_all">Ativar</string> + <string name="desktop_device">Computador</string> + <string name="contact_sent_large_file">O seu contacto enviou um ficheiro que é maior que o tamanho máximo suportado atualmente (%1$s).</string> + <string name="smp_servers_your_server">O seu servidor</string> + <string name="smp_servers_your_server_address">O endereço do seu servidor</string> + <string name="your_calls">As tuas chamadas</string> + <string name="disable_sending_recent_history">Não enviar histórico a novos membros.</string> + <string name="non_content_uri_alert_text">Você partilhou um caminho de ficheiro inválido. Relate o problema aos programadores da aplicação.</string> + <string name="you_have_no_chats">Você não tem conversas</string> + <string name="create_group_button">Criar grupo</string> + <string name="group_main_profile_sent">O seu perfil de conversa será enviado para os membros do grupo</string> + <string name="remote_ctrl_error_disconnected">Desktop foi disconectado</string> + <string name="migrate_to_device_download_failed">Transferência falhada</string> + <string name="disable_notifications_button">Desativar notificações</string> + <string name="open_simplex_chat_to_accept_call">Abir SimpleX Chat para aceitar a chamada</string> + <string name="receipts_contacts_enable_keep_overrides">Ativar (manter sobreposições)</string> + <string name="info_row_created_at">Criado em</string> + <string name="feature_enabled_for">Ativo para</string> + <string name="v4_3_irreversible_message_deletion_desc">Os seus contactos podem permitir eliminação total de mensagens.</string> + <string name="remote_ctrl_disconnected_with_reason">Desconectado com a razão: %s</string> + <string name="connect_plan_you_have_already_requested_connection_via_this_address">Você já solicitou ligação através deste endereço!</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Código e protocolo open-source - qualquer indivíduo pode ser anfitrião dos servidores.</string> + <string name="blocked_items_description">%d mensagens bloqueadas</string> + <string name="marked_deleted_items_description">%d mensagens marcadas como eliminadas</string> + <string name="la_could_not_be_verified">Você não pode ser verificado; por favor tente novamente.</string> + <string name="rcv_direct_event_contact_deleted">contacto eliminado</string> + <string name="abort_switch_receiving_address_question">Cancelar mudança de endereço?</string> + <string name="you_invited_a_contact">Você convidou este contacto</string> + <string name="correct_name_to">Corrigir nome para %s?</string> + <string name="discover_on_network">Descobrir na rede local</string> + <string name="receipts_groups_title_enable">Ativar recibos para grupos?</string> + <string name="receipts_contacts_disable_for_all">Desativar para todos</string> + <string name="snd_group_event_member_unblocked">Você desbloqueou %s</string> + <string name="you_will_be_connected_when_your_connection_request_is_accepted">Você será conectado quando o pedido de ligação for aceite, por favor aguarde ou volte mais tarde!</string> + <string name="your_current_profile">O seu perfil atual</string> + <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">O seu perfil será armazenado no seu dispositivo e partilhado apenas com os seus contactos. Os servidores SimpleX não conseguem ver o seu perfil.</string> + <string name="you_must_use_the_most_recent_version_of_database">Você tem que usar a versão mais recente da sua base de dados de conversas em APENAS UM dispositivo, caso contrário poderá deixar de receber mensagens de alguns contactos.</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">A base de dados será encriptada e a palavra-passe armazenada nas definições.</string> + <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Você juntou-se a este group. A conectar ao membro que o convidou.</string> + <string name="snd_group_event_changed_member_role">você mudou o cargo de %s para %s</string> + <string name="profile_update_event_contact_name_changed">contacto %1$s mudou para %2$s</string> + <string name="snd_conn_event_switch_queue_phase_completed">você mudou de endereço</string> + <string name="send_receipts_disabled">desativado</string> + <string name="share_text_created_at">Criado em: %s</string> + <string name="member_info_member_disabled">desativado</string> + <string name="v5_3_discover_join_groups">Descobrir e juntar a grupos</string> + <string name="desktop_devices">Dispositivos desktop</string> + <string name="acknowledged">Compreendido</string> + <string name="decryption_errors">errors de desencriptação</string> + <string name="connections">Ligações</string> + <string name="created">Criado</string> + <string name="deleted">Eliminado</string> + <string name="deletion_errors">Erros de eliminação</string> + <string name="copy_error">Erro de cópia</string> + <string name="v5_3_new_desktop_app_descr">Criar novo perfil na aplicação de computador. 💻</string> + <string name="delivery_receipts_title">Recibos de entrega!</string> + <string name="delivery_receipts_are_disabled">Recibos de entrega desativados!</string> + <string name="dont_enable_receipts">Não ativar</string> + <string name="observer_cant_send_message_title">Você não pode enviar mensagens!</string> + <string name="delete_and_notify_contact">Eliminar e notificar contacto</string> + <string name="delete_without_notification">Eliminar sem notificação</string> + <string name="deleted_chats">Conversas eliminadas</string> + <string name="your_simplex_contact_address">O seu endereço SimpleX</string> + <string name="contact_list_header_title">Os seus contactos</string> + <string name="your_chat_profiles">Os seus perfis de conversa</string> + <string name="you_can_view_invitation_link_again">Você pode ver a ligação de convite outra vez nos detalhes da ligação.</string> + <string name="app_check_for_updates_download_started">A transferir atualização da app, não feche a aplicação</string> + <string name="app_check_for_updates_button_download">Transferir %s (%s)</string> + <string name="your_ICE_servers">Os seus servidores ICE</string> + <string name="your_profile_is_stored_on_your_device">O seu perfil, contactos e mensagens entregues são armazenados no seu dispositivo.</string> + <string name="your_ice_servers">Os seus servidores ICE</string> + <string name="receipts_groups_disable_keep_overrides">Desativar (manter as sobreposições de grupo)</string> + <string name="receipts_contacts_disable_keep_overrides">Desativar (manter sobreposições)</string> + <string name="receipts_contacts_title_disable">Desativar recibos?</string> + <string name="receipts_groups_title_disable">Desativar recibos para grupos?</string> + <string name="receipts_contacts_enable_for_all">Ativar para todos</string> + <string name="receipts_groups_enable_for_all">Ativar para todos os grupos</string> + <string name="receipts_contacts_title_enable">Ativar recibos?</string> + <string name="you_rejected_group_invitation">Você rejeitou o convite de grupo</string> + <string name="you_joined_this_group">Você juntou-se a este grupo</string> + <string name="rcv_group_events_count">%d eventos de grupo</string> + <string name="snd_conn_event_ratchet_sync_ok">encriptação ok para %s</string> + <string name="connect_via_member_address_alert_desc">O pedido de ligação vai ser enviado para este membro do grupo.</string> + <string name="cant_call_contact_deleted_alert_text">O contacto foi eliminado.</string> + <string name="you_need_to_allow_calls">Você precisa de permitir que o seu contacto inicie chamada de voz para poder iniciar uma chamada de voz.</string> + <string name="color_mode_dark">Escuro</string> + <string name="dark_mode_colors">Cores de modo escuro</string> + <string name="chat_theme_apply_to_dark_mode">Modo escuro</string> + <string name="appearance_zoom">Aproximar</string> + <string name="remote_ctrl_error_inactive">Desktop está inativo</string> + <string name="current_user">Perfil atual</string> + <string name="migrate_from_device_delete_database_from_device">Eliminar base de dados deste dispositivo</string> + <string name="servers_info_detailed_statistics">Estatísticas detalhadas</string> + <string name="servers_info_downloaded">Transferido</string> + <string name="downloaded_files">Ficheiros transferidos</string> + <string name="download_errors">Erros de transferência</string> + <string name="connect_plan_connect_via_link">Conectar via link?</string> + <string name="create_another_profile_button">Criar perfil</string> + <string name="creating_link">A criar ligação…</string> + <string name="contact_deleted">Contacto eliminado!</string> + <string name="conversation_deleted">Conversa eliminada!</string> + <string name="agent_critical_error_title">Erro crítico</string> + <string name="this_device_name">Nome deste dispositivo</string> + <string name="delete_contact_cannot_undo_warning">O contacto será eliminado - esta ação é irreversível!</string> + <string name="database_encryption_will_be_updated_in_settings">A palavra-passe de encriptação da base de dados será atualizada e armazenada nas definições.</string> + <string name="v5_4_incognito_groups_descr">Criar um grupo utilizando um perfil aleatório.</string> + <string name="database_migration_in_progress">Migração da base de dados em progresso. +\nPode demorar alguns minutos.</string> + <string name="you_will_be_connected_when_your_contacts_device_is_online">Você será conectado quando o dispositivo do seu contacto estiver online, por favor aguarde ou volte mais tarde!</string> + <string name="disconnect_remote_host">Desconectar</string> + <string name="app_check_for_updates_disabled">Desativado</string> + <string name="your_XFTP_servers">Os seus servidores XFTP</string> + <string name="you_control_your_chat">Você controla a conversa!</string> + <string name="you_can_still_view_conversation_with_contact">Você poderá ver a conversa com %1$s na lista de conversas.</string> + <string name="your_SMP_servers">Os seus servidores SMP</string> + <string name="you_can_use_markdown_to_format_messages__prompt">Você pode usar markdown para formatar mensagens:</string> + <string name="you_can_still_send_messages_to_contact">Você poderá enviar mensagens para %1$s através das conversas Eliminadas.</string> + <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">O seu contacto precisa de estar online para a ligação ser completada. +\nVocê pode cancelar esta ligação e remover o contacto (e tentar mais tarde com uma nova ligação).</string> + <string name="your_contacts_will_remain_connected">Os seus contactos permanecerão conectados.</string> + <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Você terá que autenticar-se quando iniciar ou abrir a aplicação após 30 segundos em segundo plano.</string> + <string name="enable_camera_access">Ativar acesso à câmara</string> + <string name="you_need_to_allow_to_send_voice">Você precisa de permitir que o seu contacto envie mensagens de aúdio para poder enviá-las.</string> + <string name="app_check_for_updates_notice_disable">Desativar</string> + <string name="receipts_groups_disable_for_all">Desativar para todos os grupos</string> + <string name="your_chat_database">A tua base de dados de conversas</string> + <string name="servers_info_details">Detalhes</string> + <string name="you_can_share_this_address_with_your_contacts">Você pode partilhar o seu endereço com os seus contactos para permitir que se conectem com %s.</string> + <string name="you_can_share_your_address">Você pode partilhar o seu endereço como uma ligação ou código QR - qualquer pessoa pode conectar-se a si.</string> + <string name="turn_off_battery_optimization_button">Permitir</string> + <string name="block_member_desc">Todas as novas mensagens de %s serão ocultadas!</string> + <string name="only_you_can_make_calls">Somente você pode fazer ligações.</string> + <string name="chat_theme_apply_to_all_modes">Todos os modos de cores</string> + <string name="feature_roles_admins">administradores</string> + <string name="snd_conn_event_ratchet_sync_started">"aceitando criptografia para %s…"</string> + <string name="add_contact_tab">Adicionar contato</string> + <string name="network_smp_proxy_fallback_allow_downgrade">Permitir downgrade</string> + <string name="clear_note_folder_warning">Todas as mensagens serão deletadas - isso não poderá ser desfeito!</string> + <string name="v5_2_more_things">Mais algumas coisas</string> + <string name="allow_to_send_files">Permitir envio de arquivos e mídias.</string> + <string name="v5_6_safer_groups_descr">Administradores podem bloquear um membro para todos.</string> + <string name="conn_event_ratchet_sync_started">Aceitando criptografia</string> + <string name="wallpaper_advanced_settings">Configurações avançadas</string> + <string name="feature_roles_all_members">todos os membros</string> + <string name="acknowledgement_errors">Erros de reconhecimento</string> + <string name="abort_switch_receiving_address_desc">Mudança de endereço será cancelada. Antigo endereço de recebimento será usado.</string> + <string name="allow_calls_question">Permitir ligações?</string> + <string name="servers_info_subscriptions_connections_subscribed">Conexões ativas</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Todos os seus contatos, conversas e arquivos serão encriptados e enviados em chunks para relays XFTP configurados.</string> + <string name="n_other_file_errors">%1$d erro(s) de outro arquivo.</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s mensagens não encaminhadas</string> + <string name="network_smp_proxy_mode_always">Sempre</string> + <string name="network_smp_proxy_mode_always_description">Sempre use uma rota privata.</string> + <string name="allow_to_send_simplex_links">Permitir o envio de links SimpleX.</string> + <string name="moderated_items_description">%1$d mensagens moderadas por %2$s</string> + <string name="rcv_group_and_other_events">e %d outros</string> + <string name="all_users">Todos os perfis</string> + <string name="connect_plan_already_connecting">Já conectando!</string> + <string name="connect_plan_already_joining_the_group">Já entrando no grupo!</string> +</resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml new file mode 100644 index 0000000000..92572516b9 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml @@ -0,0 +1,808 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="moderated_items_description">%1$d mesaje moderate de %2$s</string> + <string name="abort_switch_receiving_address_question">Întrerupi schimbarea adresei?</string> + <string name="send_disappearing_message_30_seconds">30 secunde</string> + <string name="accept_contact_button">Acceptă</string> + <string name="accept_contact_incognito_button">Acceptă incognito</string> + <string name="smp_servers_add">Adaugă server</string> + <string name="network_settings">Reglări avansate de rețea</string> + <string name="contact_wants_to_connect_via_call">%1$s dorește să se conecteze cu tine prin</string> + <string name="accept_call_on_lock_screen">Acceptă</string> + <string name="alert_text_decryption_error_too_many_skipped">%1$d mesaje omise.</string> + <string name="chat_item_ttl_month">1 lună</string> + <string name="chat_item_ttl_week">1 săptămână</string> + <string name="group_member_role_admin">administrator</string> + <string name="color_primary_variant">Accent adițional</string> + <string name="color_secondary_variant">Adițional secundar</string> + <string name="accept_feature">Acceptă</string> + <string name="v5_3_new_interface_languages">6 limbi noi pentru interfață</string> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d mesaje nu au putut fi descifrate.</string> + <string name="integrity_msg_skipped">%1$d mesaj(e) omis(e)</string> + <string name="group_info_section_title_num_members">%1$s MEMBRI</string> + <string name="chat_item_ttl_day">1 zi</string> + <string name="send_disappearing_message_1_minute">1 minut</string> + <string name="one_time_link_short">link de unică folosință</string> + <string name="send_disappearing_message_5_minutes">5 minute</string> + <string name="about_simplex">Despre SimpleX</string> + <string name="learn_more_about_address">Despre adresa SimpleX</string> + <string name="about_simplex_chat">Despre SimpleX Chat</string> + <string name="above_then_preposition_continuation">deasupra, apoi:</string> + <string name="color_primary">Accent</string> + <string name="accept">Acceptă</string> + <string name="accept_connection_request__question">Accepți cererea de conexiune?</string> + <string name="add_contact_tab">Adaugă contact</string> + <string name="callstatus_accepted">apel acceptat</string> + <string name="smp_servers_preset_add">Adaugă servere prestabilite</string> + <string name="users_add">Adaugă profil</string> + <string name="address_section_title">Adresă</string> + <string name="v4_3_improved_server_configuration_desc">Adaugă servere prin scanare de coduri QR.</string> + <string name="smp_servers_add_to_another_device">Adaugă la un alt dispozitiv</string> + <string name="button_add_welcome_message">Adaugă mesaj de întâmpinare</string> + <string name="conn_event_ratchet_sync_started">se acceptă cifrarea…</string> + <string name="snd_conn_event_ratchet_sync_started">se acceptă cifrarea pentru %s…</string> + <string name="v4_2_group_links_desc">Administratorii pot crea linkuri de participare la grupuri.</string> + <string name="v5_2_more_things">Câteva lucruri în plus</string> + <string name="all_app_data_will_be_cleared">Toate datele aplicației sunt șterse.</string> + <string name="abort_switch_receiving_address_desc">Schimbarea de adresă va fi întreruptă. Se va folosi vechea adresa de primire.</string> + <string name="add_address_to_your_profile">Adaugă adresa la profil, astfel încât contactele să o poată partaja cu alte persoane. Profilul reînoit va fi comunicat contactelor.</string> + <string name="network_enable_socks_info">Accesezi serverele prin proxy SOCKS cu portul %d? Proxyul trebuie pornit înaintea activării acestei opțiuni.</string> + <string name="abort_switch_receiving_address">Întrerupe schimbarea adresei</string> + <string name="abort_switch_receiving_address_confirm">Întrerupe</string> + <string name="users_delete_all_chats_deleted">Toate conversațiile și mesajele vor fi șterse - operațiunea este definitivă!</string> + <string name="clear_note_folder_warning">Toate mesajele vor fi șterse - acest lucru nu poate fi anulat!</string> + <string name="allow_message_reactions_only_if">Permite reacții la mesaje doar dacă și contactul tău le permite.</string> + <string name="allow_to_delete_messages">Permite ștergerea ireversibilă a mesajelor trimise. (24 ore)</string> + <string name="allow_to_send_files">Permite trimiterea de fișiere și media.</string> + <string name="v5_1_self_destruct_passcode_descr">Toate datele sunt șterse când este introdusă.</string> + <string name="all_group_members_will_remain_connected">Toți membrii grupului vor rămâne conectați.</string> + <string name="clear_chat_warning">Toate mesajele vor fi șterse - operațiunea este definitivă! Mesajele vor fi șterse DOAR pentru tine.</string> + <string name="block_member_desc">Toate mesajele noi de la %s vor fi ascunse!</string> + <string name="turn_off_battery_optimization_button">Permite</string> + <string name="allow_verb">Permite</string> + <string name="allow_calls_only_if">Permite apeluri doar dacă le permite contactul tău.</string> + <string name="allow_voice_messages_question">Permiți mesaje vocale?</string> + <string name="allow_voice_messages_only_if">Permite mesajele vocale numai dacă le permite și contactul tău.</string> + <string name="allow_irreversible_message_deletion_only_if">Permite ștergerea mesajelor ireversibile doar dacă și contactul tău îți permite ție. (24 ore)</string> + <string name="allow_message_reactions">Permite reacții la mesaje.</string> + <string name="allow_direct_messages">Permite trimiterea de mesaje directe membrilor.</string> + <string name="allow_to_send_disappearing">Permite trimiterea de mesaje efemere.</string> + <string name="allow_to_send_voice">Permite trimiterea de mesaje vocale.</string> + <string name="allow_disappearing_messages_only_if">Permite mesaje efemere doar dacă le permite contactul tău.</string> + <string name="allow_your_contacts_to_send_disappearing_messages">Permite contactelor tale să trimită mesaje care dispar.</string> + <string name="appearance_settings">Aspect</string> + <string name="app_version_title">Versiunea aplicației</string> + <string name="feature_roles_all_members">toți membrii</string> + <string name="allow_to_send_simplex_links">Permite trimiterea linkurilor SimpleX.</string> + <string name="feature_roles_admins">administratori</string> + <string name="v5_6_safer_groups_descr">Administratorii pot bloca un membru pentru toți.</string> + <string name="chat_preferences_always">întotdeauna</string> + <string name="app_version_name">Versiunea aplicației: v%s</string> + <string name="allow_your_contacts_irreversibly_delete">Permite contactelor tale să șteargă ireversibil mesajele trimise. (24 de ore)</string> + <string name="allow_your_contacts_adding_message_reactions">Permite contactelor tale să adauge reacții la mesaje.</string> + <string name="allow_your_contacts_to_call">Permite contactelor tale să te apeleze.</string> + <string name="allow_your_contacts_to_send_voice_messages">Permite contactelor tale să trimită mesaje vocale.</string> + <string name="all_your_contacts_will_remain_connected">Toate contactele tale vor rămâne conectate.</string> + <string name="cancel_verb">Anulare</string> + <string name="camera_not_available">Camera indisponibilă</string> + <string name="permissions_camera_and_record_audio">Camera și microfon</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Atenție</b>: arhiva va fi ștearsă.]]></string> + <string name="icon_descr_cancel_file_preview">Anulează previzualizarea fișierului</string> + <string name="empty_chat_profile_is_created">Un profil de conversație gol cu numele furnizat este creat, și aplicația se deschide ca de obicei.</string> + <string name="connect_plan_already_connecting">Se conectează deja!</string> + <string name="notifications_mode_off_desc">Aplicația poate primi notificări doar când rulează, niciun serviciu în fundal nu va fi lansat.</string> + <string name="connect_plan_already_joining_the_group">Se alătură deja grupului!</string> + <string name="notifications_mode_service">Mereu pornit</string> + <string name="connect__a_new_random_profile_will_be_shared">Un nou profil aleatoriu va fi distribuit.</string> + <string name="keychain_is_storing_securely">Android Keystore este folosit pentru a stoca în siguranță fraza de acces - permite serviciului de notificare să funcționeze.</string> + <string name="rcv_group_and_other_events">și %d alte evenimente</string> + <string name="answer_call">Răspunde la apel</string> + <string name="keychain_allows_to_receive_ntfs">Android Keystore va fi folosit pentru a stoca în siguranță fraza de acces după ce repornești aplicația sau schimbi fraza de acces - va permite primirea notificărilor.</string> + <string name="settings_section_title_app">APLICAȚIE</string> + <string name="create_group_button">Creează grup</string> + <string name="v4_6_audio_video_calls">Apeluri audio și video</string> + <string name="migrate_from_device_archive_and_upload">Arhivează și încarcă</string> + <string name="migrate_from_device_archiving_database">Bază de date de arhivare</string> + <string name="migrate_from_device_creating_archive_link">Se creează un link de arhivare</string> + <string name="share_one_time_link">Creează un link de invitare unic.</string> + <string name="auto_accept_images">Acceptă automat imagini</string> + <string name="call_service_notification_audio_call">Apel audio</string> + <string name="icon_descr_audio_call">Apel audio</string> + <string name="icon_descr_audio_off">Audio oprit</string> + <string name="settings_section_title_icon">PICTOGRAMĂ APLICAȚIE</string> + <string name="la_app_passcode">Cod de acces aplicație</string> + <string name="create_secret_group_title">Creează grup secret</string> + <string name="smp_server_test_create_queue">Creează coadă</string> + <string name="notifications_mode_service_desc">Serviciul în fundal rulează mereu - notificările vor fi afișate imediat ce sunt disponibile.</string> + <string name="la_authenticate">Autentificare</string> + <string name="la_auth_failed">Autentificare eșuată</string> + <string name="la_current_app_passcode">Codul de acces curent</string> + <string name="auth_unavailable">Autentificare indisponibilă</string> + <string name="attach">Atașează</string> + <string name="back">Înapoi</string> + <string name="create_group">Creează grup secret</string> + <string name="creating_link">Se creează link…</string> + <string name="audio_call_no_encryption">apel audio (necifrat e2e)</string> + <string name="smp_server_test_create_file">Creează fișier</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Adaugă contact</b>: pentru a crea un nou link de invitare, sau a te conecta printr-un link pe care l-ai primit.]]></string> + <string name="authentication_cancelled">Autentificare anulată</string> + <string name="app_passcode_replaced_with_self_destruct">Cod de acces aplicație este înlocuit cu cod de acces de autodistrugere.</string> + <string name="audio_video_calls">Apeluri audio/video</string> + <string name="available_in_v51">" +\nDisponibil în v5.1"</string> + <string name="v5_0_app_passcode">Cod de acces aplicație</string> + <string name="v5_6_app_data_migration">Migrare date aplicație</string> + <string name="agent_critical_error_title">Eroare critică</string> + <string name="migrate_to_device_apply_onion">Aplică</string> + <string name="create_profile">Creează profil</string> + <string name="create_another_profile_button">Creează profil</string> + <string name="v4_2_auto_accept_contact_requests">Acceptă automat cererile de contactare</string> + <string name="custom_time_picker_custom">personalizat</string> + <string name="maximum_supported_file_size">În prezent dimensiunea maximă pentru fișiere este %1$s.</string> + <string name="create_simplex_address">Creează adresă SimpleX</string> + <string name="auto_accept_contact">Acceptare automată</string> + <string name="create_your_profile">Creează-ți profilul</string> + <string name="settings_audio_video_calls">Apeluri audio și video</string> + <string name="icon_descr_audio_on">Audio pornit</string> + <string name="current_passphrase">Frază de acces curentă…</string> + <string name="create_group_link">Creează link pentru grup</string> + <string name="button_create_group_link">Creează link</string> + <string name="group_member_status_creator">creator</string> + <string name="color_background">Fundal</string> + <string name="v5_3_new_desktop_app_descr">Creează profil nou în aplicația desktop. 💻</string> + <string name="v5_3_encrypt_local_files_descr">Aplicația cifrează fișierele locale noi (cu excepția videoclipurilor).</string> + <string name="group_member_role_author">autor</string> + <string name="v5_3_new_interface_languages_descr">Arabă, Bulgară, Finlandeză, Ebraică, Thailandeză și Ucraineană - mulțumită utilizatorilor și Weblate.</string> + <string name="calls_prohibited_with_this_contact">Apelurile audio/video sunt interzise.</string> + <string name="item_info_current">(actual)</string> + <string name="profile_update_event_removed_picture">Poză de profil eliminată</string> + <string name="dark_theme">Temă întunecată</string> + <string name="group_member_status_removed">eliminat</string> + <string name="theme_dark">Întunecată</string> + <string name="profile_update_event_removed_address">Adresă de contact eliminată</string> + <string name="v5_1_custom_themes_descr">Personalizează și distribuie teme colorate.</string> + <string name="v5_1_custom_themes">Teme personalizate</string> + <string name="migrate_to_device_repeat_download">Repetă descărcarea</string> + <string name="send_disappearing_message_custom_time">Timp personalizat</string> + <string name="remove_member_button">Elimină membru</string> + <string name="migrate_to_device_repeat_import">Repetă importarea</string> + <string name="customize_theme_title">Personalizează tema</string> + <string name="remove_passphrase">Elimină</string> + <string name="remove_member_confirmation">Elimină</string> + <string name="button_remove_member">Elimină membru</string> + <string name="button_remove_member_question">Elimini membrul?</string> + <string name="network_options_reset_to_defaults">Resetează la implicit</string> + <string name="reset_single_color">Resetează culoarea</string> + <string name="reset_color">Resetează culorile</string> + <string name="theme_remove_image">Elimină imagine</string> + <string name="migrate_from_device_repeat_upload">Repetă încărcarea</string> + <string name="callstatus_rejected">apel respins</string> + <string name="remove_passphrase_from_keychain">Elimini fraza de acces din Keystore?</string> + <string name="remove_passphrase_from_settings">Elimini parola din reglări?</string> + <string name="connect_plan_repeat_connection_request">Repetă cererea de conectare?</string> + <string name="network_use_onion_hosts_required">Necesar</string> + <string name="retry_verb">Reîncearcă</string> + <string name="v5_8_safe_files">Primește fișiere în siguranță</string> + <string name="v5_6_safer_groups">Grupuri mai sigure</string> + <string name="restore_database_alert_confirm">Restabilește</string> + <string name="refresh_qr_code">Reîmprospătează</string> + <string name="revoke_file__title">Revoci fișierul?</string> + <string name="revoke_file__confirm">Revocă</string> + <string name="sync_connection_force_question">Renegociezi cifrarea?</string> + <string name="reset_verb">Resetează</string> + <string name="reject">Respinge</string> + <string name="save_passphrase_in_settings">Salvează parola din reglări</string> + <string name="save_and_update_group_profile">Salvează și actualizează profilul grupului</string> + <string name="connect_plan_repeat_join_request">Repetă cererea de alăturare?</string> + <string name="restart_chat_button">Repornește conversația</string> + <string name="saved_description">salvat</string> + <string name="saved_from_description">Salvat de la %s</string> + <string name="save_verb">Salvează</string> + <string name="saved_chat_item_info_tab">Salvat</string> + <string name="saved_from_chat_item_info_title">Salvat de la</string> + <string name="sync_connection_force_confirm">Renegociază</string> + <string name="smp_servers_save">Salvează servere</string> + <string name="saved_ICE_servers_will_be_removed">Servere WebRTC ICE salvate vor fi eliminate.</string> + <string name="save_profile_password">Salvează parola profilului</string> + <string name="save_passphrase_and_open_chat">Salvează fraza de acces și deschide conversația</string> + <string name="group_members_2">%s și %s</string> + <string name="renegotiate_encryption">Renegociază cifrarea</string> + <string name="save_and_notify_contact">Salvează și notifică contactul</string> + <string name="save_and_notify_contacts">Salvează și notifică contactele</string> + <string name="save_and_notify_group_members">Salvează și notifică membrii grupului</string> + <string name="notifications_mode_off">Rulează când aplicația este pornită</string> + <string name="reply_verb">Răspunde</string> + <string name="revoke_file__action">Revocă fișierul</string> + <string name="save_servers_button">Salvează</string> + <string name="save_settings_question">Salvezi reglările?</string> + <string name="save_preferences_question">Salvezi preferințe?</string> + <string name="settings_restart_app">Repornire</string> + <string name="restore_database">Restabilește copia de rezervă a bazei de date</string> + <string name="restore_database_alert_title">Restabilești copia de rezervă a bazei de date?</string> + <string name="database_restore_error">Eroare la restabilirea bazei de date</string> + <string name="rcv_group_event_member_deleted">%1$s eliminat</string> + <string name="rcv_group_event_2_members_connected">%s și %s conectați</string> + <string name="role_in_group">Rol</string> + <string name="network_options_save">Salvează</string> + <string name="reveal_verb">Arată</string> + <string name="reject_contact_button">Respinge</string> + <string name="smp_save_servers_question">Salvezi servere?</string> + <string name="icon_descr_call_rejected">Apel respins</string> + <string name="saved_message_title">Mesaj salvat</string> + <string name="restart_the_app_to_create_a_new_chat_profile">Repornește aplicația pentru a crea un nou profil</string> + <string name="save_passphrase_in_keychain">Salvează fraza de acces în Keystore</string> + <string name="save_group_profile">Salvează profilul grupului</string> + <string name="wallpaper_scale_repeat">Repetă</string> + <string name="send_link_previews">Trimite previzualizări ale link-ului</string> + <string name="set_passphrase">Setează frază de acces</string> + <string name="share_address">Distribuie adresă</string> + <string name="info_row_sent_at">Trimis la</string> + <string name="color_secondary">Secundar</string> + <string name="color_sent_message">Mesaj trimis</string> + <string name="set_group_preferences">Setează preferințele grupului</string> + <string name="icon_descr_sent_msg_status_sent">trimis</string> + <string name="srv_error_host">Adresa serverului este incompatibilă cu reglările rețelei.</string> + <string name="srv_error_version">Versiunea serverului este incompatibilă cu reglările rețelei.</string> + <string name="sending_via">Trimițând prin</string> + <string name="sending_files_not_yet_supported">trimiterea de fișiere nu este acceptată încă</string> + <string name="scan_code_from_contacts_app">Scanează codul de securitate din aplicația contactului tău</string> + <string name="select_contacts">Selectează contacte</string> + <string name="self_destruct">Autodistrugere</string> + <string name="color_sent_quote">Răspuns trimis</string> + <string name="share_image">Distribuie media…</string> + <string name="share_message">Distribuie mesaj…</string> + <string name="chat_list_always_visible">Arată lista conversațiilor într-o fereastră nouă</string> + <string name="terminal_always_visible">Arată consola într-o fereastră nouă</string> + <string name="setup_database_passphrase">Setează fraza de acces a bazei de date</string> + <string name="set_database_passphrase">Setează fraza de acces a bazei de date</string> + <string name="profile_update_event_set_new_address">setează adresă de contact nouă</string> + <string name="current_version_timestamp">%s (actual)</string> + <string name="session_code">Cod de sesiune</string> + <string name="sender_cancelled_file_transfer">Expeditorul a anulat transferul de fișiere.</string> + <string name="error_smp_test_server_auth">Serverul necesită autorizație pentru a crea cozi, verifică parola</string> + <string name="share_verb">Distribuie</string> + <string name="icon_descr_sent_msg_status_send_failed">trimitere eșuată</string> + <string name="search_or_paste_simplex_link">Caută sau lipește link SimpleX</string> + <string name="set_contact_name">Setează numele de contact</string> + <string name="save_welcome_message_question">Salvezi mesajul de bun venit?</string> + <string name="v4_2_security_assessment">Evaluare de securitate</string> + <string name="scan_qr_code_from_desktop">Scanează cod QR de pe desktop</string> + <string name="search_verb">Caută</string> + <string name="send_live_message_desc">Trimite un mesaj live - se va actualiza pentru destinatar(i) în timp ce îl tastezi</string> + <string name="share_link">Distribuie fișier</string> + <string name="enable_sending_recent_history">Trimite până la ultimele 100 de mesaje membrilor noi.</string> + <string name="v5_5_simpler_connect_ui_descr">Bara de căutare acceptă link-uri de invitație.</string> + <string name="custom_time_unit_seconds">secunde</string> + <string name="scan_from_mobile">Scanează de pe mobil</string> + <string name="sent_message">Mesaj trimis</string> + <string name="scan_QR_code">Scanează cod QR</string> + <string name="send_disappearing_message_send">Trimite</string> + <string name="chat_with_the_founder">Trimite întrebări și idei</string> + <string name="show_developer_options">Arată opțiuni dezvoltator</string> + <string name="network_option_seconds_label">sec</string> + <string name="v4_4_disappearing_messages_desc">Mesajele trimise vor fi șterse după timpul setat.</string> + <string name="error_xftp_test_server_auth">Serverul necesită autorizație pentru a încărca, verifică parola</string> + <string name="notification_preview_mode_message_desc">Arată contact și mesaje</string> + <string name="share_file">Distribuie fișier…</string> + <string name="text_field_set_contact_placeholder">Setează numele de contact…</string> + <string name="icon_descr_send_message">Trimite mesaj</string> + <string name="send_disappearing_message">Trimite mesaj temporar</string> + <string name="connect_via_link_or_qr_from_clipboard_or_in_person">(scanează sau lipește din clipboard)</string> + <string name="show_QR_code">Arată cod QR</string> + <string name="smp_servers_test_failed">Test server eșuat!</string> + <string name="show_dev_options">Arată:</string> + <string name="show_internal_errors">Arată erori interne</string> + <string name="secret_text">secret</string> + <string name="settings_section_title_settings">REGLĂRI</string> + <string name="rcv_group_event_1_member_connected">%s conectat</string> + <string name="profile_update_event_set_new_picture">setează imagine de profil</string> + <string name="share_text_sent_at">Trimis către: %s</string> + <string name="conn_stats_section_title_servers">SERVERE</string> + <string name="send_live_message">Trimite mesaj live</string> + <string name="migrate_to_device_bytes_downloaded">%s descărcat</string> + <string name="share_address_with_contacts_question">Distribui adresa cu contactele?</string> + <string name="settings_notification_preview_mode_title">Arată previzualizare</string> + <string name="member_contact_send_direct_message">trimite mesaj direct</string> + <string name="compose_send_direct_message_to_connect">Trimite mesaj direct pentru a te conecta</string> + <string name="custom_time_picker_select">Selectează</string> + <string name="stop_snd_file__message">Trimiterea de fișiere va fi oprită.</string> + <string name="send_verb">Trimite</string> + <string name="icon_descr_settings">Reglări</string> + <string name="scan_code">Scanează cod</string> + <string name="security_code">Cod de securitate</string> + <string name="send_us_an_email">Trimite-ne email</string> + <string name="smp_servers_scan_qr">Scanează codul QR al serverului</string> + <string name="share_with_contacts">Distribuie contactelor</string> + <string name="show_call_on_lock_screen">Arată</string> + <string name="rcv_conn_event_verification_code_reset">cod de securitate schimbat</string> + <string name="privacy_show_last_messages">Arată ultimul mesaj</string> + <string name="button_send_direct_message">Trimite mesaj direct</string> + <string name="chat_theme_set_default_theme">Setează tema implicită</string> + <string name="theme_simplex">SimpleX</string> + <string name="system_restricted_background_desc">SimpleX nu poate rula în fundal. Vei primi notificările doar când aplicația rulează.</string> + <string name="simplex_service_notification_title">Serviciu SimpleX Chat</string> + <string name="icon_descr_address">Adresă SimpleX</string> + <string name="settings_shutdown">Închide</string> + <string name="simplex_links">Link-uri SimpleX</string> + <string name="simplex_links_are_prohibited_in_group">Link-urile SimpleX sunt interzise în acest grup.</string> + <string name="v4_2_security_assessment_desc">Securitatea SimpleX Chat a fost verificată de Trail of Bits.</string> + <string name="ntf_channel_messages">Mesaje SimpleX Chat</string> + <string name="ntf_channel_calls">Apeluri SimpleX Chat</string> + <string name="simplex_address">Adresă SimpleX</string> + <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> + <string name="simplex_links_not_allowed">Link-uri SimpleX nepermise</string> + <string name="icon_descr_simplex_team">Echipa SimpleX</string> + <string name="shutdown_alert_question">Închizi?</string> + <string name="simplex_link_contact">Adresă de contact SimpleX</string> + <string name="simplex_link_group">Link pentru grup SimpleX</string> + <string name="simplex_link_mode">Link-uri SimpleX</string> + <string name="simplex_link_invitation">Invitație unică SimpleX</string> + <string name="image_descr_simplex_logo">Siglă SimpleX</string> + <string name="receipts_section_groups">Grupuri mici (max 20)</string> + <string name="v5_3_simpler_incognito_mode">Mod incognito simplificat</string> + <string name="v5_7_shape_profile_images_descr">Pătrat, cerc, sau orice între.</string> + <string name="is_not_verified">%s nu este verificat</string> + <string name="is_verified">%s este verificat</string> + <string name="smp_servers">Servere SMP</string> + <string name="rcv_group_event_n_members_connected">%s, %s și %d alți membri s-au conectat</string> + <string name="group_members_n">%s, %s și %d membri</string> + <string name="audio_device_speaker">Difuzor</string> + <string name="rcv_group_event_3_members_connected">%s, %s și %s s-au conectat</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="smp_servers_test_some_failed">Niște servere au eșuat testul:</string> + <string name="icon_descr_speaker_off">Difuzor oprit</string> + <string name="icon_descr_speaker_on">Difuzor pornit</string> + <string name="full_backup">Copie de rezervă a datelor aplicației</string> + <string name="theme_destination_app_theme">Tema aplicației</string> + <string name="color_primary_variant2">Accent suplimentar 2</string> + <string name="migrate_from_device_start_chat">Începe conversația</string> + <string name="chat_theme_apply_to_all_modes">Toate modurile de culoare</string> + <string name="always_use_relay">Folosește mereu releu</string> + <string name="chat_theme_apply_to_mode">Aplică pentru</string> + <string name="add_contact_or_create_group">Începe o nouă conversație</string> + <string name="star_on_github">Stea pe GitHub</string> + <string name="conn_event_disabled_pq">Cifrare standard de la un capăt la altul</string> + <string name="notifications_mode_periodic">Pornește periodic</string> + <string name="network_smp_proxy_mode_always">Mereu</string> + <string name="network_smp_proxy_mode_always_description">Folosește mereu rutare privată.</string> + <string name="all_your_contacts_will_remain_connected_update_sent">Toate contactele vor rămâne conectate. Actualizarea profilului va fi trimisă contactelor tale.</string> + <string name="callstate_starting">pornire…</string> + <string name="chat_item_ttl_seconds">%s secunde</string> + <string name="start_chat_question">Începi conversația?</string> + <string name="wallpaper_advanced_settings">Reglări avansate</string> + <string name="bad_desktop_address">Adresă desktop rea</string> + <string name="alert_title_msg_bad_id">ID de mesaj incorect</string> + <string name="integrity_msg_bad_hash">Hash de mesaj incorect</string> + <string name="integrity_msg_bad_id">ID de mesaj incorect</string> + <string name="alert_title_msg_bad_hash">Hash de mesaj incorect</string> + <string name="switch_receiving_address">Schimbă adresa de primire</string> + <string name="chat_is_stopped_you_should_transfer_database">Conversația este oprită. Dacă ai folosit deja această bază de date pe alt dispozitiv, ar trebui să o transferi înapoi înainte de a porni conversația.</string> + <string name="settings_section_title_calls">APELURI</string> + <string name="snd_group_event_changed_role_for_yourself">ai schimbat rolul pentru tine la %s</string> + <string name="snd_error_quota">Capacitate depășită - destinatarul nu a primit mesajele trimise anterior.</string> + <string name="change_self_destruct_passcode">Schimbă codul de acces autodistructibil</string> + <string name="chat_is_stopped">Conversația este oprită</string> + <string name="rcv_conn_event_switch_queue_phase_changing">se schimbă adresa…</string> + <string name="icon_descr_contact_checked">Contact verificat</string> + <string name="info_row_created_at">Creat la</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Alege <i>Migrează de pe alt dispozitiv</i> pe dispozitivul nou și scanează codul QR.]]></string> + <string name="connect_via_contact_link">Te conectezi prin adresa de contact?</string> + <string name="connect_via_invitation_link">Te conectezi printr-un link unic?</string> + <string name="connect_via_link_incognito">Conectare incognito</string> + <string name="contact_already_exists">Contactul deja există</string> + <string name="la_change_app_passcode">Schimbă codul de acces</string> + <string name="you_can_turn_on_lock">Poți porni Blocare SimpleX din Reglări.</string> + <string name="your_chats">Conversații</string> + <string name="choose_file_title">Alege un fișier</string> + <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Contactul tău trebuie să fie online pentru a se completa conexiunea. +\nPoți anula această conexiune și elimina contactul (și poți încerca mai târziu cu un nou link).</string> + <string name="callstatus_error">eroare apel</string> + <string name="your_calls">Apelurile tale</string> + <string name="change_verb">Schimbă</string> + <string name="change_role">Schimbă rolul</string> + <string name="chat_preferences_contact_allows">Contactul permite</string> + <string name="v5_4_better_groups">Grupuri mai bune</string> + <string name="connected_desktop">Desktop conectat</string> + <string name="contact_preferences">Preferințe contact</string> + <string name="connect_with_contact_name_question">Te conectezi cu %1$s?</string> + <string name="icon_descr_cancel_image_preview">Anulează previzualizarea imaginii</string> + <string name="chat_with_developers">Discută cu dezvoltatorii</string> + <string name="you_have_to_enter_passphrase_every_time">Trebuie să introduci fraza de acces de fiecare dată când aplicația pornește - nu este stocată pe dispozitiv.</string> + <string name="member_info_member_blocked">blocat</string> + <string name="chat_preferences">Preferințe conversație</string> + <string name="chat_theme_apply_to_dark_mode">Mod întunecat</string> + <string name="v4_6_chinese_spanish_interface">Interfață chineză și spaniolă</string> + <string name="v5_4_incognito_groups_descr">Crează un grup folosind un profil aleatoriu.</string> + <string name="v5_4_block_group_members">Blochează membrii grupului</string> + <string name="connected_mobile">Mobil conectat</string> + <string name="connected_to_desktop">Conectat la desktop</string> + <string name="migrate_from_device_cancel_migration">Anulează migrarea</string> + <string name="network_type_cellular">Celular</string> + <string name="observer_cant_send_message_title">Nu poți trimite mesaje!</string> + <string name="switch_receiving_address_question">Schimbi adresa de primire?</string> + <string name="alert_title_contact_connection_pending">Contactul nu este conectat încă!</string> + <string name="connect_via_link">Conectare prin link</string> + <string name="you_can_view_invitation_link_again">Poți vedea linkul de invitație din nou în detaliile conexiunii.</string> + <string name="your_chat_profiles">Profilurile tale de conversație</string> + <string name="create_chat_profile">Crează profil de conversație</string> + <string name="callstatus_ended">apel terminat %1$s</string> + <string name="create_profile_button">Crează</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="permissions_camera">Camera</string> + <string name="call_on_lock_screen">Apeluri pe ecranul blocat:</string> + <string name="status_contact_has_e2e_encryption">contactul are cifrare e2e</string> + <string name="status_contact_has_no_e2e_encryption">contactul nu are cifrare e2e</string> + <string name="receipts_section_contacts">Contacte</string> + <string name="settings_section_title_chats">DISCUȚII</string> + <string name="chat_database_section">BAZĂ DE DATE CONVERSAȚIE</string> + <string name="chat_database_deleted">Baza de date a conversației ștearsă</string> + <string name="chat_is_running">Conversația rulează</string> + <string name="your_chat_database">Baza ta de date a conversațiilor</string> + <string name="database_is_not_encrypted">Baza de date a conversațiilor tale nu este cifrată - pune o parolă pentru a o proteja.</string> + <string name="database_encryption_will_be_updated_in_settings">Parola de cifrare a bazei de date va fi actualizată și stocată în reglări.</string> + <string name="snd_group_event_changed_member_role">ai schimbat rolul %s la %s</string> + <string name="alert_title_cant_invite_contacts">Nu se pot invita contactele!</string> + <string name="you_joined_this_group">Te-ai alăturat grupului</string> + <string name="invite_prohibited">Nu se poate invita contactul!</string> + <string name="group_member_status_accepted">se conectează (acceptat)</string> + <string name="share_text_created_at">Creat la: %s</string> + <string name="block_member_confirmation">Blochează</string> + <string name="block_member_button">Blochează membru</string> + <string name="connect_via_member_address_alert_title">Conectare directă?</string> + <string name="group_main_profile_sent">Profilul tău de conversație va fi trimis membrilor grupului</string> + <string name="color_mode_dark">Întunecat</string> + <string name="dark_mode_colors">Culori mod întunecat</string> + <string name="both_you_and_your_contact_can_make_calls">Și tu și contactul tău puteți face apeluri.</string> + <string name="feature_cancelled_item">%s anulat</string> + <string name="connected_to_mobile">Conectat la mobil</string> + <string name="copy_error">Eroare copiere</string> + <string name="connect_plan_connect_via_link">Te conectezi prin link?</string> + <string name="la_could_not_be_verified">Nu ai putut fi verificat(ă); te rog încearcă din nou.</string> + <string name="copy_verb">Copiază</string> + <string name="icon_descr_cancel_live_message">Anulează mesajul live</string> + <string name="connect_via_link_or_qr">Conectare prin link / cod QR</string> + <string name="your_contacts_will_remain_connected">Contactele tale vor rămâne conectate.</string> + <string name="database_passphrase_will_be_updated">Parola de cifrare a bazei de date va fi actualizată.</string> + <string name="v4_3_irreversible_message_deletion_desc">Contactele tale pot permite ștergerea totală a mesajelor.</string> + <string name="you_need_to_allow_to_send_voice">Trebuie să permiți contactului tău să trimită mesaje vocale pentru a le putea trimite.</string> + <string name="core_version">Versiunede bază: v%s</string> + <string name="create_address_and_let_people_connect">Creează o adresă pentru a permite oamenilor să se conecteze cu tine.</string> + <string name="rcv_group_event_member_blocked">%s blocat</string> + <string name="snd_conn_event_switch_queue_phase_completed">ai schimbat adresa</string> + <string name="snd_conn_event_switch_queue_phase_completed_for_member">ai schimbat adresa pentru %s</string> + <string name="snd_conn_event_switch_queue_phase_changing">se schimbă adresa…</string> + <string name="snd_conn_event_switch_queue_phase_changing_for_member">se schimbă adresa pentru %s…</string> + <string name="both_you_and_your_contact_can_send_disappearing">Și tu și contactul tău puteți trimite mesaje temporare.</string> + <string name="cannot_receive_file">Nu se pot primi fișiere</string> + <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Poate fi dezactivat din reglări</b> – notificările vor fi afișate dacă aplicația este în funcțiune.]]></string> + <string name="notifications_mode_periodic_desc">Verifică mesajele noi la fiecare 10 minute timp de până la 1 minut</string> + <string name="you_have_no_chats">Nu ai conversații</string> + <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Contactul și toate mesajele vor fi șterse - acest lucru nu poate fi anulat!</string> + <string name="copied">Copiat în clipboard</string> + <string name="use_camera_button">Camera</string> + <string name="you_invited_a_contact">Ai invitat un contact</string> + <string name="your_chat_profile_will_be_sent_to_your_contact">Profilul tău de conversație va fi trimis +\ncontactului tău</string> + <string name="contribute">Contribuie</string> + <string name="network_session_mode_user">Profil conversație</string> + <string name="bold_text">aldin</string> + <string name="you_can_use_markdown_to_format_messages__prompt">Poți folosi markdown pentru a formata mesaje:</string> + <string name="callstatus_calling">se apelează…</string> + <string name="change_self_destruct_mode">Schimbă modul de autodistrugere</string> + <string name="chat_database_imported">Baza de date a conversației importată</string> + <string name="you_must_use_the_most_recent_version_of_database">Trebuie să folosești cea mai recentă versiune a bazei de date a conversațiilor DOAR pe un singur dispozitiv, altfel se poate să nu mai primești mesajele de la unele contacte.</string> + <string name="cannot_access_keychain">Nu se poate accesa Keystore pentru a salva parola bazei de date</string> + <string name="migrate_to_device_chat_migrated">Conversație migrată!</string> + <string name="continue_to_next_step">Continuă</string> + <string name="callstatus_in_progress">apel în curs</string> + <string name="icon_descr_call_progress">Apel în curs</string> + <string name="both_you_and_your_contact_can_send_voice">Și tu și contactul tău puteți trimite mesaje vocale.</string> + <string name="notification_preview_somebody">Contact ascuns:</string> + <string name="notification_preview_mode_contact">Nume contact</string> + <string name="create_address">Crează adresă</string> + <string name="database_encrypted">Bază de date cifrată!</string> + <string name="change_database_passphrase_question">Schimbi fraza de acces a bazei de date?</string> + <string name="chat_is_stopped_indication">Conversația este oprită</string> + <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Poți porni discuția din Reglările aplicației / Baza de date sau repornind aplicația.</string> + <string name="snd_group_event_user_left">ai ieșit</string> + <string name="block_member_question">Blochezi membrul?</string> + <string name="member_blocked_by_admin">Blocat de admin</string> + <string name="both_you_and_your_contact_can_add_message_reactions">Și tu și contactul tău puteți adăuga reacții la mesaje.</string> + <string name="v5_1_better_messages">Mesaje mai bune</string> + <string name="blocked_item_description">blocat</string> + <string name="blocked_by_admin_item_description">blocat de admin</string> + <string name="database_initialization_error_title">Nu se poate inițializa baza de date</string> + <string name="contact_sent_large_file">Contactul tău a trimis un fișier care este mai mare decât dimensiunea maximă suportată în prezent (%1$s).</string> + <string name="icon_descr_cancel_link_preview">anulează previzualizarea link-ului</string> + <string name="smp_servers_check_address">Verifică adresa serverului și încearcă din nou.</string> + <string name="you_control_your_chat">Tu îți controlezi conversația!</string> + <string name="call_already_ended">Apel deja terminat!</string> + <string name="icon_descr_call_ended">Apel terminat</string> + <string name="block_for_all">Blochează pentru toți</string> + <string name="connect_plan_you_have_already_requested_connection_via_this_address">Ai cerut deja conexiunea prin această adresă!</string> + <string name="connect_plan_connect_to_yourself">Te conectezi la tine?</string> + <string name="migrate_from_device_check_connection_and_try_again">Verifică conexiunea la internet și încearcă din nou</string> + <string name="settings_section_title_chat_colors">Culori conversație</string> + <string name="settings_section_title_chat_theme">Aspectul discuției</string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Bun pentru baterie</b>. Aplicația verifică mesajele la fiecare 10 minute. Pot fi pierdute apeluri sau mesaje urgente.]]></string> + <string name="theme_black">Negru</string> + <string name="block_for_all_question">Blochezi membrul pentru toți?</string> + <string name="both_you_and_your_contacts_can_delete">Și tu și contactul tău puteți șterge ireversibil mesajele trimise. (24 de ore)</string> + <string name="onboarding_notifications_mode_service_desc"><b>Folosește mai multă baterie</b>! +\nServiciul în fundal rulează mereu – notificările sunt afișate imediat ce mesajele sunt disponibile.</string> + <string name="cannot_share_message_alert_title">Nu se poate trimite mesajul</string> + <string name="clear_contacts_selection_button">Ștergeți</string> + <string name="v5_8_safe_files_descr">Confirmați fișiere de la servere necunoscute.</string> + <string name="rcv_conn_event_switch_queue_phase_completed">schimbat adresa pentru dumneavoastră</string> + <string name="confirm_new_passphrase">Confirmați parola nouă…</string> + <string name="connect_via_link_verb">Conectare</string> + <string name="connection_local_display_name">conexiune %1$d</string> + <string name="display_name_connection_established">conexiune stabilită</string> + <string name="connection_error">Eroare de conexiune</string> + <string name="connection_timeout">Conexiune expirată</string> + <string name="contact_connection_pending">se conectează</string> + <string name="confirm_verb">Confirmare</string> + <string name="in_developing_title">În curând!</string> + <string name="group_connection_pending">se conectează…</string> + <string name="change_lock_mode">Schimbați modul de blocare</string> + <string name="app_version_code">Versiunea aplicației: %s</string> + <string name="network_session_mode_user_description"><![CDATA[O conexiune separată TCP (și SOCKS credential), va fi folosită <b>pentru fiecare profil de conversație pe care le aveți în aplicație</b>]]></string> + <string name="network_smp_proxy_fallback_allow_downgrade">Permiteți downgrade-ul</string> + <string name="network_session_mode_entity_description"><![CDATA[O conexiune separată TCP (și SOCKS credential) va fi folosită <b>pentru fiecare contact și membru de grup</b> \n<b>Va rugăm considerați că:</b> dacă aveți prea multe conexiuni, consumul dumneavoastră de baterie și trafic de internet pot fi considerabil mai mari, iar unele conexiuni pot eșua.]]></string> + <string name="desktop_connection_terminated">Conexiune terminată</string> + <string name="connecting_to_desktop">Se conectează la desktop</string> + <string name="icon_descr_asked_to_receive">Rugat să primească imaginea</string> + <string name="connection_request_sent">Cerere de conexiune trimisă!</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>De reținut</b>: releele de mesaje și fișiere sunt conectate prin proxy SOCKS. Apelurile și trimiterea de previzualizări ale adreselor web utilizează conexiunea directă.]]></string> + <string name="callstate_connected">conectat</string> + <string name="confirm_passcode">Confirmați codul de access</string> + <string name="confirm_database_upgrades">Confirmare actualizare bază de date</string> + <string name="rcv_group_event_changed_member_role">"schimbat rolul lui %s la %s"</string> + <string name="rcv_group_event_member_connected">conectat</string> + <string name="group_member_status_introduced">se conectează (introdus)</string> + <string name="group_member_status_connected">conectat</string> + <string name="group_member_status_connecting">se conectează</string> + <string name="change_member_role_question">Schimbați rolul de grup?</string> + <string name="color_mode">Modul color</string> + <string name="v4_5_transport_isolation_descr">Prin profil de conversație (implicit) sau prin conexiune (BETA).</string> + <string name="v4_4_verify_connection_security_desc">Comparați codurile de securitate cu contactele dumneavoastră.</string> + <string name="remote_host_was_disconnected_title">Conexiune oprită</string> + <string name="remote_ctrl_was_disconnected_title">Conexiune oprită</string> + <string name="connect_to_desktop">Conectare la desktop</string> + <string name="remote_ctrl_error_bad_state">Conexiunea la desktop este într-o stare proastă</string> + <string name="smp_server_test_connect">Conectare</string> + <string name="network_session_mode_entity">Conexiune</string> + <string name="clear_note_folder_question">Ștergi notițele private?</string> + <string name="multicast_connect_automatically">Conectare automată</string> + <string name="callstatus_connecting">se conectează apelul…</string> + <string name="group_member_status_intro_invitation">se conectează (invetație la introducere)</string> + <string name="callstate_connecting">se conectează…</string> + <string name="remote_host_error_bad_state"><![CDATA[Conexiunea la telefonul <b>%s</b> este într-o stare proastă]]></string> + <string name="migrate_to_device_confirm_network_settings">Confirmă reglările rețelei</string> + <string name="display_name_connecting">se conectează…</string> + <string name="connection_error_auth">Eroare de conexiune (AUTENTIFICARE)</string> + <string name="turning_off_service_and_periodic">Optimizarea bateriei este activă, dezactivând serviciul de funcționare în ascuns și solicitările periodice pentru mesaje noi. Pot fi reactivate din reglări.</string> + <string name="icon_descr_server_status_connected">conectat</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Crează un grup:</b> pentru a crea un nou grup.]]></string> + <string name="clear_chat_question">Ștergeți conversația?</string> + <string name="clear_chat_menu_action">Ștergeți</string> + <string name="clear_chat_button">Ștergeți conversația</string> + <string name="connect_button">Conectare</string> + <string name="chat_console">Consolă conversație</string> + <string name="confirm_password">Confirmați parola</string> + <string name="colored_text">colorat</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Toate contactele, conversațiile și fișierele tale vor fi cifrate într-un mod sigur și încărcate pe bucăți pe releurile XFTP configurate.</string> + <string name="icon_descr_video_asked_to_receive">Rugat să primească videoclipul</string> + <string name="smp_server_test_compare_file">Comparați fișierul</string> + <string name="impossible_to_recover_passphrase"><![CDATA[<b>Vă rugăm să rețineți</b>: nu veți putea recupera sau schimba parola dacă o veți pierde.]]></string> + <string name="rcv_group_event_changed_your_role">schimbat rolul dumneavoastră la %s</string> + <string name="server_connected">conectat</string> + <string name="server_connecting">se conectează</string> + <string name="notification_contact_connected">conectat</string> + <string name="clear_verb">Ștergeți</string> + <string name="icon_descr_close_button">Buton de închidere</string> + <string name="clear_verification">Ștergeți verificarea</string> + <string name="configure_ICE_servers">Configurare servere ICE</string> + <string name="rcv_group_event_member_created_contact">conectat direct</string> + <string name="group_member_status_announced">se conectează (anunțat)</string> + <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Cel mai bun pentru baterie.</b> Veți primi notificări doar când aplicația rulează (FĂRĂ servicii de fundal).]]></string> + <string name="icon_descr_call_connecting">Se conectează apelul</string> + <string name="group_member_status_complete">complet</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>De reținut</b>: folosirea aceleiași baze de date în două aparate, va intrerupe descifrarea mesajelor din conexiunile tale, ca măsură de protecție.]]></string> + <string name="migrate_from_device_confirm_upload">Confirmați încărcarea</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Confirmați că țineți minte parola de la baza de date pentru a o migra.</string> + <string name="info_row_connection">Conexiune</string> + <string name="delete_chat_profile_question">Ștergi profilul de conversație?</string> + <string name="for_me_only">Șterge pentru mine</string> + <string name="ttl_d">%dd</string> + <string name="deleted_description">șters</string> + <string name="button_delete_contact">Șterge contact</string> + <string name="info_row_deleted_at">Șters la</string> + <string name="desktop_app_version_is_incompatible">Versiunea aplicației desktop %s nu este compatibilă cu această aplicație.</string> + <string name="delete_member_message__question">Ștergi mesajul membrului?</string> + <string name="button_delete_group">Șterge grup</string> + <string name="delete_and_notify_contact">Șterge și notifică contactele</string> + <string name="decentralized">Decentralizat</string> + <string name="rcv_group_event_group_deleted">grup șters</string> + <string name="num_contacts_selected">%d contact(e) selectat(e)</string> + <string name="share_text_deleted_at">Șters la: %s</string> + <string name="users_delete_question">Ștergi profilul de conversație?</string> + <string name="delete_profile">Șterge profil</string> + <string name="chat_preferences_default">implicit (%s)</string> + <string name="delivery_receipts_title">Confirmări de livrare!</string> + <string name="delivery_receipts_are_disabled">Confirmările de livrare sunt dezactivate!</string> + <string name="desktop_address">Adresă desktop</string> + <string name="desktop_devices">Dispozitive desktop</string> + <string name="desktop_device">Desktop</string> + <string name="decryption_error">Eroare de decriptare</string> + <string name="delete_image">Șterge imagine</string> + <string name="delete_after">Șterge după</string> + <string name="full_deletion">Șterge pentru toată lumea</string> + <string name="simplex_link_mode_description">Descriere</string> + <string name="smp_server_test_delete_file">Șterge fișier</string> + <string name="smp_server_test_delete_queue">Șterge coadă</string> + <string name="delete_contact_question">Ștergi contactul?</string> + <string name="delete_contact_menu_action">Șterge</string> + <string name="delete_group_menu_action">Șterge</string> + <string name="delete_files_and_media_question">Ștergi fișiere și media?</string> + <string name="ttl_days">%d zile</string> + <string name="ttl_day">%d zi</string> + <string name="delete_address">Șterge adresa</string> + <string name="delete_messages">Șterge mesaje</string> + <string name="delete_messages__question">Ștergi %d mesaje?</string> + <string name="delete_address__question">Ștergi adresa?</string> + <string name="delete_files_and_media_all">Șterge toate fișierele</string> + <string name="delete_link">Șterge link</string> + <string name="delete_link_question">Ștergi link?</string> + <string name="custom_time_unit_days">zile</string> + <string name="delete_verb">Șterge</string> + <string name="delete_message__question">Ștergi mesajul?</string> + <string name="delivery">Livrare</string> + <string name="delete_pending_connection__question">Ștergi conexiunea în așteptare?</string> + <string name="smp_servers_delete_server">Șterge server</string> + <string name="delete_database">Șterge baza de date</string> + <string name="rcv_direct_event_contact_deleted">contact șters</string> + <string name="delete_group_question">Ștergi grupul?</string> + <string name="migrate_from_device_delete_database_from_device">Șterge baza de date de pe acest dispozitiv</string> + <string name="delete_chat_profile">Șterge profil de conversație</string> + <string name="delete_files_and_media_for_all_users">Șterge fișiere pentru toate profilurile de conversație</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Oricine poate găzdui servere.</string> + <string name="deleted_chats">Contacte arhivate</string> + <string name="app_check_for_updates_beta">Beta</string> + <string name="calls_prohibited_alert_title">Apeluri interzise!</string> + <string name="cant_call_member_alert_title">Nu se poate apela membrul grupului</string> + <string name="chat_database_exported_title">Bază de date conversație exportată</string> + <string name="app_check_for_updates">Verifică pentru actualizări</string> + <string name="profile_update_event_contact_name_changed">Contactul %1$s a schimbat la %2$s</string> + <string name="cant_call_contact_deleted_alert_text">Contactul este șters.</string> + <string name="v6_0_connection_servers_status">Controlează-ți rețeaua</string> + <string name="database_encryption_will_be_updated">Parola pentru cifrarea bazei de date va fi actualizată și stocată în Keystore.</string> + <string name="database_passphrase">Fraza de acces a bazei de date</string> + <string name="xftp_servers_configured">Servere XFTP configurate</string> + <string name="set_password_to_export_desc">Baza de date este cifrată folosind o parolă aleatorie. Trebuie schimbată înainte de exportare.</string> + <string name="info_view_call_button">apel</string> + <string name="info_view_connect_button">conectare</string> + <string name="contact_deleted">Contact șters!</string> + <string name="conversation_deleted">Conversație ștearsă!</string> + <string name="app_check_for_updates_download_completed_title">Actualizarea aplicației este descărcată</string> + <string name="app_check_for_updates_notice_title">Verifică pentru actualizări</string> + <string name="create_address_button">Creează</string> + <string name="privacy_media_blur_radius">Estompează media</string> + <string name="settings_section_title_chat_database">BAZĂ DE DATE DISCUȚIE</string> + <string name="v6_0_connect_faster_descr">Conectează-te cu prietenii mai ușor.</string> + <string name="attempts_label">încercări</string> + <string name="completed">Completat</string> + <string name="developer_options">ID-urile bazei de date și opțiunea de izolare a transportului.</string> + <string name="info_row_database_id">ID bază de date</string> + <string name="share_text_database_id">ID bază de date: %d</string> + <string name="connections">Conexiuni</string> + <string name="created">Creat</string> + <string name="v6_0_privacy_blur">Estompează pentru intimitate mai bună.</string> + <string name="v6_0_connection_servers_status_descr">Stare conexiune și servere</string> + <string name="database_error">Eroare bază de date</string> + <string name="contacts_can_mark_messages_for_deletion">Contactele pot marca mesajele pentru ștergere; tu le vei putea vedea.</string> + <string name="database_downgrade">Downgrade al bazei de date</string> + <string name="database_migration_in_progress">Migrarea bazei de date este în proces. +\nPoate dura câteva minute.</string> + <string name="database_passphrase_and_export">Fraza de acces a bazei de date și export</string> + <string name="all_users">Toate profilurile</string> + <string name="current_user">Profil actual</string> + <string name="confirm_delete_contact_question">Confirmi ștergerea contactului?</string> + <string name="delete_contact_cannot_undo_warning">Contactul va fi șters - acest lucru nu poate fi anulat!</string> + <string name="smp_servers_configured">Servere SMP configurate</string> + <string name="servers_info_sessions_connected">Conectat</string> + <string name="correct_name_to">Corectează numele la %s?</string> + <string name="chat_database_exported_continue">Continuă</string> + <string name="encrypted_with_random_passphrase">Baza de date este cifrată folosind o parolă aleatorie; o poți schimba.</string> + <string name="cant_send_message_to_member_alert_title">Nu se pot trimite mesaje membrului grupului</string> + <string name="servers_info_sessions_connecting">Se conectează</string> + <string name="servers_info_connected_servers_section_header">Servere conectate</string> + <string name="cant_call_contact_alert_title">Nu se poate apela contactul</string> + <string name="cant_call_contact_connecting_wait_alert_text">Se conectează la contact, așteaptă sau verifică mai târziu!</string> + <string name="network_settings_title">Reglări avansate</string> + <string name="toolbar_settings">Reglări</string> + <string name="your_settings">Reglările tale</string> + <string name="encrypted_audio_call">apel vocal cifrat e2e</string> + <string name="encrypted_video_call">apel video cifrat e2e</string> + <string name="settings_section_title_device">APARAT</string> + <string name="settings_section_title_experimenta">EXPERIMENTAL</string> + <string name="encrypt_database">Cifrează</string> + <string name="decryption_errors">erori la descifrare</string> + <string name="settings_section_title_you">TU</string> + <string name="status_no_e2e_encryption">necifrat e2e</string> + <string name="status_e2e_encrypted">cifrat e2e</string> + <string name="incoming_video_call">Apel video primit</string> + <string name="no_call_on_lock_screen">Dezactivează</string> + <string name="allow_calls_question">Permiți apeluri?</string> + <string name="callstate_ended">terminat</string> + <string name="v5_7_call_sounds">Sunete la apel</string> + <string name="v6_1_better_calls">Apeluri îmbunătățite</string> + <string name="incoming_audio_call">Apel audio primit</string> + <string name="call_service_notification_end_call">Termină apelul</string> + <string name="marked_deleted_description">marcate şterse</string> + <string name="marked_deleted_items_description">%d mesaje marcate șterse</string> + <string name="moderated_item_description">moderat de %s</string> + <string name="n_other_file_errors">%1$d alte erori de fișier.</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s mesaje netransmise</string> + <string name="description_you_shared_one_time_link">ai distribuit un link de unică folosință</string> + <string name="description_you_shared_one_time_link_incognito">ai distribuit un link ascuns de unică folosință</string> + <string name="description_via_group_link">prin link pentru grup</string> + <string name="description_via_group_link_incognito">ascuns prin link pentru grup</string> + <string name="simplex_link_mode_browser">Prin browser</string> + <string name="report_reason_spam">Reclamă</string> + <string name="report_reason_illegal">Conținut inadecvat</string> + <string name="report_reason_community">Încălcă normelor comunitare</string> + <string name="report_reason_profile">Profil inadecvat</string> + <string name="report_reason_other">Alt motiv</string> + <string name="error_saving_smp_servers">Eroare la salvarea serverelor SMP</string> + <string name="ensure_xftp_server_address_are_correct_format_and_unique">Asigurați-vă că adresele serverelor XFTP sunt în format corect, pe randuri separate și nu sunt duplicate.</string> + <string name="error_loading_xftp_servers">Eroare la încărcarea serverlor XFTP</string> + <string name="error_setting_network_config">Eroare la actualizarea configurației de rețea</string> + <string name="failed_to_parse_chats_title">Eșec la încărcarea conversațiilor</string> + <string name="contact_developers">Vă rugăm să actualizați aplicația și contactați dezvoltatorii.</string> + <string name="failed_to_create_user_title">Eroare la crearea profilului!</string> + <string name="blocked_items_description">%d mesaje blocate</string> + <string name="encryption_renegotiation_error">Eroare de renegociere a criptării</string> + <string name="display_name_invited_to_connect">invitat să se conecteze</string> + <string name="description_via_contact_address_link_incognito">ascuns prin link pentru adresa de contact</string> + <string name="description_via_one_time_link">printr-un link de unică folosință</string> + <string name="group_reports_active_one">1 raport</string> + <string name="chat_item_ttl_year">1 an</string> + <string name="forwarded_description">înaintat</string> + <string name="error_showing_message">eroare de afișare a mesajului</string> + <string name="error_showing_content">eroare de afișat conținutul</string> + <string name="e2ee_info_no_pq"><![CDATA[Mesajele, fisierele si apelurile sunt protejate prin <b>criptare cap-coadă</b> cu secretizare înaintată perfecta, repudiere si recuperare în caz de spargere.]]></string> + <string name="e2ee_info_pq"><![CDATA[Mesajele, fisierele si apelurile sunt protejate prin <b>criptare cap-coadă rezistentă la algoritmi cuantici</b> cu secretizare înaintată perfecta, repudiere si recuperare în caz de spargere.]]></string> + <string name="e2ee_info_no_pq_short">Această conversație este protejată prin criptare cap-coadă.</string> + <string name="simplex_link_mode_full">Link întreg</string> + <string name="description_via_one_time_link_incognito">ascuns printr-un link de unică folosință</string> + <string name="a_plus_b">a + b</string> + <string name="simplex_link_connection">prin %1$s</string> + <string name="error_loading_smp_servers">Eroare la încărcarea serverlor SMP</string> + <string name="forward_files_missing_desc">%1$d fișier(e) șterse.</string> + <string name="report_item_archived_by">raport arhivat de %s</string> + <string name="display_name_accepted_invitation">invitație acceptată</string> + <string name="ensure_smp_server_address_are_correct_format_and_unique">Asigurați-vă că adresele serverelor SMP sunt în format corect, pe randuri separate și nu sunt duplicate.</string> + <string name="error_saving_xftp_servers">Eroare la salvarea serverelor XFTP</string> + <string name="forward_files_in_progress_desc">%1$d fișier(e) încă în descărcare.</string> + <string name="forward_files_failed_to_receive_desc">%1$d fișier(e) a eșuat să se descărcarce.</string> + <string name="forward_files_not_accepted_desc">%1$d fișier(e) nu au fost descărcate.</string> + <string name="description_via_contact_address_link">prin link pentru adresa de contact</string> + <string name="simplex_link_mode_browser_warning">Deschiderea link-ului în browser poate reduce confidențialitatea și securitatea conexiunii. Link-urile SimpleX de neîncredere vor fi roșii.</string> + <string name="display_name_requested_to_connect">solicitat să se conecteze</string> + <string name="failed_to_parse_chat_title">Eșec la încărcarea conversației</string> + <string name="report_item_visibility_submitter">Doar tu și moderatorii vedeți asta</string> + <string name="report_item_visibility_moderators">Doar expeditorul şi moderatorii văd asta</string> + <string name="report_item_archived">raport arhivat</string> + <string name="blocked_by_admin_items_description">%d mesaje blocate de admin</string> + <string name="receiving_files_not_yet_supported">primirea de fișiere nu este acceptată încă</string> + <string name="sender_you_pronoun">tu</string> + <string name="unknown_message_format">format mesaj necunoscut</string> + <string name="invalid_message_format">format mesaj invalid</string> + <string name="live">LIVE</string> + <string name="moderated_description">moderat</string> + <string name="invalid_chat">conversație nevalidă</string> + <string name="invalid_data">date nevalide</string> + <string name="e2ee_info_pq_short">Această conversație este protejată prin criptare cap-coadă, rezistentă la algoritmi cuantici.</string> + <string name="note_folder_local_display_name">Notițe private</string> +</resources> 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 9e8bd0af8c..97742f82a8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -88,9 +88,9 @@ <string name="icon_descr_instant_notifications">Мгновенные уведомления</string> <string name="service_notifications">Мгновенные уведомления!</string> <string name="service_notifications_disabled">Мгновенные уведомления выключены!</string> - <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Чтобы защитить Ваши личные данные, вместо уведомлений от сервера приложение запускает <b>фоновый сервис SimpleX</b>, который потребляет несколько процентов батареи в день.]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Чтобы улучшить конфиденциальность, <b>SimpleX выполняется в фоне</b> вместо уведомлений через сервер.]]></string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Он может быть выключен через Настройки</b> – Вы продолжите получать уведомления о сообщениях пока приложение запущено.]]></string> - <string name="turn_off_battery_optimization"><![CDATA[Для использования этой функции, <b>разрешите SimpleX выполняться в фоне</b> в следующем диалоге. Иначе уведомления будут выключены.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[<b>Разрешите это</b> в следующем окне чтобы получать нотификации мгновенно.]]></string> <string name="turning_off_service_and_periodic">Оптимизация батареи включена, поэтому сервис уведомлений выключен. Вы можете снова включить его через Настройки.</string> <string name="periodic_notifications">Периодические уведомления</string> <string name="periodic_notifications_disabled">Периодические уведомления выключены!</string> @@ -267,12 +267,10 @@ <string name="to_start_a_new_chat_help_header">Чтобы начать новый чат</string> <string name="chat_help_tap_button">Нажмите кнопку</string> <string name="above_then_preposition_continuation">сверху, затем:</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Добавить новый контакт</b>: чтобы создать одноразовый QR код/ссылку для Вашего контакта.]]></string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Сканировать QR код</b>: чтобы соединиться с контактом, который показывает Вам QR код.]]></string> <string name="to_connect_via_link_title">Чтобы соединиться через ссылку</string> <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Если Вы получили ссылку с приглашением из SimpleX Chat, Вы можете открыть ее в браузере:</string> <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 на компьютере: сосканируйте показанный QR код из приложения через <b>Сканировать QR код</b>.]]></string> - <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 на мобильном: намжите кнопку <b>Open in mobile app</b> на веб странице, затем нажмите <b>Соединиться</b> в приложении.]]></string> + <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 на мобильном: нажмите кнопку <b>Open in mobile app</b> на веб странице, затем нажмите <b>Соединиться</b> в приложении.]]></string> <!-- Contact Request Alert Dialogue - CharListNavLinkView.kt --> <string name="accept_connection_request__question">Принять запрос на соединение?</string> <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Отправителю НЕ будет послано уведомление, если Вы отклоните запрос на соединение.</string> @@ -301,7 +299,8 @@ <string name="connection_you_accepted_will_be_cancelled">Подтвержденное соединение будет отменено!</string> <!-- Connection Pending Alert Dialogue - ChatListNavLinkView.kt --> <string name="alert_title_contact_connection_pending">Соединение еще не установлено!</string> - <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Ваш контакт должен быть в сети чтобы установить соединение.\nВы можете отменить соединение и удалить контакт (и попробовать позже с другой ссылкой).</string> + <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Ваш контакт должен быть в сети, чтобы установить соединение. +\nВы можете отменить соединение и удалить контакт (и попробовать позже с другой ссылкой).</string> <!-- Contact Request Information - ContactRequestView.kt --> <string name="contact_wants_to_connect_with_you">хочет соединиться с Вами!</string> <!-- Image Placeholder - ChatInfoImage.kt --> @@ -339,13 +338,12 @@ <string name="connect_button">Соединиться</string> <string name="paste_button">Вставить</string> <!-- CreateLinkView.kt --> - <string name="create_one_time_link">Создать одноразовую ссылку</string> <string name="one_time_link">Одноразовая ссылка</string> <!-- settings - SettingsView.kt --> <string name="your_settings">Настройки</string> <string name="your_simplex_contact_address">Ваш SimpleX адрес</string> <string name="database_passphrase_and_export">База данных</string> - <string name="about_simplex_chat">Информация о SimpleX Chat</string> + <string name="about_simplex_chat">Подробнее о SimpleX Chat</string> <string name="how_to_use_simplex_chat">Как использовать</string> <string name="markdown_help">Форматирование сообщений</string> <string name="markdown_in_messages">Форматирование сообщений</string> @@ -356,7 +354,7 @@ <string name="smp_servers">SMP серверы</string> <string name="smp_servers_preset_address">Адрес сервера по умолчанию</string> <string name="smp_servers_preset_add">Добавить серверы по умолчанию</string> - <string name="smp_servers_add">Добавить сервер…</string> + <string name="smp_servers_add">Добавить сервер</string> <string name="smp_servers_test_server">Тестировать сервер</string> <string name="smp_servers_test_servers">Тестировать серверы</string> <string name="smp_servers_save">Сохранить серверы</string> @@ -396,7 +394,6 @@ <string name="network_enable_socks_info">Соединяться с серверами через SOCKS прокси через порт %d? Прокси должен быть запущен до включения этой опции.</string> <string name="network_disable_socks">Использовать прямое соединение с Интернет?</string> <string name="network_disable_socks_info">Если Вы подтвердите, серверы смогут видеть Ваш IP адрес, а провайдер - с какими серверами Вы соединяетесь.</string> - <string name="update_onion_hosts_settings_question">Обновить настройки .onion хостов?</string> <string name="network_use_onion_hosts">Использовать .onion хосты</string> <string name="network_use_onion_hosts_prefer">Когда возможно</string> <string name="network_use_onion_hosts_no">Нет</string> @@ -405,9 +402,6 @@ <string name="network_use_onion_hosts_no_desc">Onion хосты не используются.</string> <string name="network_use_onion_hosts_required_desc">Подключаться только к onion хостам. \nОбратите внимание: Вы не сможете соединиться с серверами, у которых нет .onion адреса.</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">Onion хосты используются, если возможно.</string> - <string name="network_use_onion_hosts_no_desc_in_alert">Onion хосты не используются.</string> - <string name="network_use_onion_hosts_required_desc_in_alert">Подключаться только к onion хостам.</string> <string name="appearance_settings">Интерфейс</string> <!-- Address Items - UserAddressView.kt --> <string name="create_address">Создать адрес</string> @@ -451,7 +445,7 @@ <string name="this_string_is_not_a_connection_link">Эта строка не является ссылкой-приглашением!</string> <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Вы также можете соединиться, открыв ссылку там, где Вы её получили. Если ссылка откроется в браузере, нажмите кнопку <b>Открыть в приложении</b>.]]></string> <!-- CICallStatus --> - <string name="callstatus_calling">входящий звонок…</string> + <string name="callstatus_calling">звонок…</string> <string name="callstatus_missed">пропущенный звонок</string> <string name="callstatus_rejected">отклоненный звонок</string> <string name="callstatus_accepted">принятый звонок</string> @@ -469,28 +463,25 @@ <string name="callstate_connected">соединено</string> <string name="callstate_ended">завершен</string> <!-- SimpleXInfo --> - <string name="next_generation_of_private_messaging">Новое поколение приватных сообщений</string> + <string name="next_generation_of_private_messaging">Будущее коммуникаций</string> <string name="privacy_redefined">Более конфиденциальный</string> - <string name="first_platform_without_user_ids">Первая в мире платформа без идентификаторов пользователей.</string> + <string name="first_platform_without_user_ids">Без идентификаторов пользователей.</string> <string name="immune_to_spam_and_abuse">Защищен от спама</string> - <string name="people_can_connect_only_via_links_you_share">С Вами можно соединиться только через созданные Вами ссылки.</string> + <string name="people_can_connect_only_via_links_you_share">Вы определяете, кто может соединиться.</string> <string name="decentralized">Децентрализованный</string> - <string name="opensource_protocol_and_code_anybody_can_run_servers">Открытый протокол и код - кто угодно может запустить сервер.</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Кто угодно может запустить сервер.</string> <string name="create_your_profile">Создать профиль</string> <string name="make_private_connection">Добавьте контакт</string> <string name="how_it_works">Как это работает</string> <!-- How SimpleX Works --> <string name="how_simplex_works">Как SimpleX работает</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[Много пользователей спросили: <i>как SimpleX доставляет сообщения без идентификаторов пользователей?</i>]]></string> - <string name="to_protect_privacy_simplex_has_ids_for_queues">Чтобы защитить Вашу конфиденциальность, вместо ID пользователей, которые есть в других платформах, SimpleX использует ID для очередей сообщений, разные для каждого контакта.</string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Вы определяете через какие серверы Вы <b>получаете сообщения</b>, Ваши контакты - серверы, которые Вы используете для отправки.]]></string> - <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Только пользовательские устройства хранят контакты, группы и сообщения, которые отправляются <b>с двухуровневым end-to-end шифрованием</b>.]]></string> - <string name="read_more_in_github">Узнайте больше из нашего GitHub репозитория.</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">Чтобы защитить Вашу конфиденциальность, SimpleX использует разные ID для всех ваших контактов.</string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Только пользовательские устройства хранят контакты, группы и сообщения.</string> <string name="read_more_in_github_with_link"><![CDATA[Узнайте больше из нашего <font color="#0088ff">GitHub репозитория</font>.]]></string> <!-- SetNotificationsMode.kt --> <string name="use_chat">Использовать чат</string> <!-- MakeConnection --> - <string name="paste_the_link_you_received">Вставить полученную ссылку</string> + <string name="paste_the_link_you_received">Вставьте полученную ссылку</string> <!-- Call --> <string name="incoming_video_call">Входящий видеозвонок</string> <string name="incoming_audio_call">Входящий аудиозвонок</string> @@ -609,7 +600,6 @@ <string name="chat_database_deleted">Данные чата удалены</string> <string name="restart_the_app_to_create_a_new_chat_profile">Перезапустите приложение, чтобы создать новый профиль.</string> <string name="you_must_use_the_most_recent_version_of_database">Используйте самую последнюю версию архива чата и ТОЛЬКО на одном устройстве, иначе Вы можете перестать получать сообщения от некоторых контактов.</string> - <string name="stop_chat_to_enable_database_actions">Остановите чат, чтобы разблокировать операции с архивом чата.</string> <string name="delete_files_and_media_for_all_users">Удалить файлы во всех профилях чата</string> <string name="delete_files_and_media_all">Удалить все файлы</string> <string name="delete_files_and_media_question">Удалить файлы и медиа?</string> @@ -682,12 +672,6 @@ <string name="chat_is_stopped_indication">Чат остановлен</string> <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Вы можете запустить чат через Настройки приложения или перезапустив приложение.</string> <!-- ChatArchiveView.kt --> - <string name="chat_archive_header">Архив чата</string> - <string name="chat_archive_section">АРХИВ ЧАТА</string> - <string name="save_archive">Сохранить архив</string> - <string name="delete_archive">Удалить архив</string> - <string name="archive_created_on_ts">Дата создания %1$s</string> - <string name="delete_chat_archive_question">Удалить архив чата?</string> <!-- Groups --> <string name="group_invitation_item_description">приглашение в группу %1$s</string> <string name="join_group_question">Вступить в группу?</string> @@ -770,7 +754,7 @@ <string name="invite_prohibited_description">Вы пытаетесь пригласить инкогнито контакт в группу, где Вы используете свой основной профиль</string> <!-- GroupChatInfoView.kt --> <string name="button_add_members">Пригласить членов группы</string> - <string name="group_info_section_title_num_members">ЧЛЕНОВ ГРУППЫ: %1$s</string> + <string name="group_info_section_title_num_members">%1$s ЧЛЕНОВ ГРУППЫ</string> <string name="group_info_member_you">Вы: %1$s</string> <string name="button_delete_group">Удалить группу</string> <string name="delete_group_question">Удалить группу?</string> @@ -802,8 +786,8 @@ <string name="change_verb">Поменять</string> <string name="switch_verb">Переключить</string> <string name="change_member_role_question">Поменять роль в группе?</string> - <string name="member_role_will_be_changed_with_notification">Роль будет изменена на \"%s\". Все в группе получат сообщение.</string> - <string name="member_role_will_be_changed_with_invitation">Роль будет изменена на \"%s\". Будет отправлено новое приглашение.</string> + <string name="member_role_will_be_changed_with_notification">Роль будет изменена на "%s". Все в группе получат сообщение.</string> + <string name="member_role_will_be_changed_with_invitation">Роль будет изменена на "%s". Будет отправлено новое приглашение.</string> <string name="error_removing_member">Ошибка при удалении члена группы</string> <string name="error_changing_role">Ошибка при изменении роли</string> <string name="info_row_group">Группа</string> @@ -829,11 +813,10 @@ <!-- AdvancedNetworkSettings.kt --> <string name="network_options_reset_to_defaults">Сбросить настройки</string> <string name="network_option_seconds_label">сек</string> - <string name="network_option_tcp_connection_timeout">Таймаут TCP соединения</string> + <string name="network_option_tcp_connection_timeout">Таймаут TCP-соединения</string> <string name="network_option_protocol_timeout">Таймаут протокола</string> <string name="network_option_ping_interval">Интервал PING</string> <string name="network_option_enable_tcp_keep_alive">Включить TCP keep-alive</string> - <string name="network_options_revert">Отменить изменения</string> <string name="network_options_save">Сохранить</string> <string name="update_network_settings_question">Обновить настройки сети?</string> <string name="updating_settings_will_reconnect_client_to_all_servers">Обновление настроек приведет к переподключению клиента ко всем серверам.</string> @@ -850,7 +833,6 @@ <string name="theme_dark">Темная</string> <!-- Appearance.kt --> <string name="theme">Тема</string> - <string name="save_color">Сохранить цвет</string> <string name="reset_color">Сбросить цвета</string> <string name="color_primary">Акцент</string> <!-- Preferences.kt --> @@ -875,15 +857,15 @@ <string name="feature_enabled_for_contact">включено для контакта</string> <string name="feature_off">выключено</string> <string name="feature_received_prohibited">получено, не разрешено</string> - <string name="allow_your_contacts_irreversibly_delete">Разрешить Вашим контактам необратимо удалять отправленные сообщения.</string> - <string name="allow_irreversible_message_deletion_only_if">Разрешить необратимое удаление сообщений, только если Ваш контакт разрешает это Вам.</string> + <string name="allow_your_contacts_irreversibly_delete">Разрешить Вашим контактам необратимо удалять отправленные сообщения. (24 часа)</string> + <string name="allow_irreversible_message_deletion_only_if">Разрешить необратимое удаление сообщений, только если Ваш контакт разрешает это Вам. (24 часа)</string> <string name="contacts_can_mark_messages_for_deletion">Контакты могут помечать сообщения для удаления; Вы сможете просмотреть их.</string> <string name="allow_your_contacts_to_send_voice_messages">Разрешить Вашим контактам отправлять голосовые сообщения.</string> <string name="allow_voice_messages_only_if">Разрешить голосовые сообщения, только если их разрешает Ваш контакт.</string> <string name="prohibit_sending_voice_messages">Запретить отправлять голосовые сообщений.</string> - <string name="both_you_and_your_contacts_can_delete">Вы и Ваш контакт можете необратимо удалять отправленные сообщения.</string> - <string name="only_you_can_delete_messages">Только Вы можете необратимо удалять сообщения (Ваш контакт может помечать их на удаление).</string> - <string name="only_your_contact_can_delete">Только Ваш контакт может необратимо удалять сообщения (Вы можете помечать их на удаление).</string> + <string name="both_you_and_your_contacts_can_delete">Вы и Ваш контакт можете необратимо удалять отправленные сообщения. (24 часа)</string> + <string name="only_you_can_delete_messages">Только Вы можете необратимо удалять сообщения (Ваш контакт может помечать их на удаление). (24 часа)</string> + <string name="only_your_contact_can_delete">Только Ваш контакт может необратимо удалять сообщения (Вы можете помечать их на удаление). (24 часа)</string> <string name="message_deletion_prohibited">Необратимое удаление сообщений запрещено в этой группе.</string> <string name="both_you_and_your_contact_can_send_voice">Вы и Ваш контакт можете отправлять голосовые сообщения.</string> <string name="only_you_can_send_voice">Только Вы можете отправлять голосовые сообщения.</string> @@ -891,24 +873,24 @@ <string name="voice_prohibited_in_this_chat">Голосовые сообщения запрещены в этом чате.</string> <string name="allow_direct_messages">Разрешить посылать прямые сообщения членам группы.</string> <string name="prohibit_direct_messages">Запретить посылать прямые сообщения членам группы.</string> - <string name="allow_to_delete_messages">Разрешить необратимо удалять отправленные сообщения.</string> + <string name="allow_to_delete_messages">Разрешить необратимо удалять отправленные сообщения. (24 часа)</string> <string name="prohibit_message_deletion">Запретить необратимое удаление сообщений.</string> <string name="allow_to_send_voice">Разрешить отправлять голосовые сообщения.</string> <string name="prohibit_sending_voice">Запретить отправлять голосовые сообщений.</string> - <string name="group_members_can_send_dms">Члены группы могут посылать прямые сообщения.</string> - <string name="direct_messages_are_prohibited_in_chat">Прямые сообщения между членами группы запрещены.</string> - <string name="group_members_can_delete">Члены группы могут необратимо удалять отправленные сообщения.</string> - <string name="message_deletion_prohibited_in_chat">Необратимое удаление сообщений запрещено в этой группе.</string> - <string name="group_members_can_send_voice">Члены группы могут отправлять голосовые сообщения.</string> - <string name="voice_messages_are_prohibited">Голосовые сообщения запрещены в этой группе.</string> + <string name="group_members_can_send_dms">Члены могут посылать прямые сообщения.</string> + <string name="direct_messages_are_prohibited_in_group">Прямые сообщения между членами группы запрещены.</string> + <string name="group_members_can_delete">Члены могут необратимо удалять отправленные сообщения. (24 часа)</string> + <string name="message_deletion_prohibited_in_chat">Необратимое удаление сообщений запрещено.</string> + <string name="group_members_can_send_voice">Члены могут отправлять голосовые сообщения.</string> + <string name="voice_messages_are_prohibited">Голосовые сообщения запрещены.</string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Минимальный расход батареи</b>. Вы получите уведомления только когда приложение запущено, без фонового сервиса.]]></string> <string name="onboarding_notifications_mode_title">Уведомления</string> <string name="onboarding_notifications_mode_off">Когда приложение запущено</string> <string name="onboarding_notifications_mode_periodic">Периодически</string> <string name="onboarding_notifications_mode_service">Мгновенно</string> - <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Больше расход батареи</b>! Фоновый сервис постоянно запущен - уведомления будут показаны как только есть сообщения.]]></string> - <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Меньше расход батареи</b>. Фоновый сервис проверяет сообщения каждые 10 минут. Вы можете пропустить звонки и срочные сообщения.]]></string> - <string name="onboarding_notifications_mode_subtitle">Можно изменить позже в настройках.</string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Больше расход батареи</b>! Приложение постоянно запущено в фоне - уведомления будут показаны сразу же.]]></string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Меньше расход батареи</b>. Приложение проверяет сообщения каждые 10 минут. Вы можете пропустить звонки и срочные сообщения.]]></string> + <string name="onboarding_notifications_mode_subtitle">Как это влияет на потребление энергии</string> <string name="live">LIVE</string> <string name="send_live_message">Отправить живое сообщение</string> <string name="live_message">Живое сообщение!</string> @@ -944,7 +926,7 @@ <string name="send_live_message_desc">Отправить живое сообщение — оно будет обновляться для получателей по мере того, как Вы его вводите</string> <string name="create_group_link">Создать ссылку группы</string> <string name="prohibit_sending_disappearing_messages">Запретить отправлять исчезающие сообщения.</string> - <string name="disappearing_messages_are_prohibited">Исчезающие сообщения запрещены в этой группе.</string> + <string name="disappearing_messages_are_prohibited">Исчезающие сообщения запрещены.</string> <string name="ttl_w">%dнед</string> <string name="ttl_d">%dд</string> <string name="ttl_weeks">%d нед.</string> @@ -957,7 +939,7 @@ <string name="clear_verification">Сбросить подтверждение</string> <string name="allow_disappearing_messages_only_if">Разрешить исчезающие сообщения, только если Ваш контакт разрешает их Вам.</string> <string name="prohibit_sending_disappearing">Запретить посылать исчезающие сообщения.</string> - <string name="group_members_can_send_disappearing">Члены группы могут посылать исчезающие сообщения.</string> + <string name="group_members_can_send_disappearing">Члены могут посылать исчезающие сообщения.</string> <string name="whats_new">Что нового</string> <string name="new_in_version">Новое в %s</string> <string name="v4_2_security_assessment">Аудит безопасности</string> @@ -972,7 +954,7 @@ <string name="v4_4_disappearing_messages">Исчезающие сообщения</string> <string name="v4_4_disappearing_messages_desc">Отправленные сообщения будут удалены через заданное время.</string> <string name="v4_3_improved_server_configuration">Улучшенная конфигурация серверов</string> - <string name="v4_4_live_messages">\"Живые\" сообщения</string> + <string name="v4_4_live_messages">"Живые" сообщения</string> <string name="v4_4_live_messages_desc">Получатели видят их в то время как Вы их набираете.</string> <string name="v4_4_verify_connection_security">Проверить безопасность соединения</string> <string name="v4_4_verify_connection_security_desc">Сравните код безопасности с Вашими контактами.</string> @@ -1004,8 +986,7 @@ <string name="users_delete_all_chats_deleted">Все чаты и сообщения будут удалены - это нельзя отменить!</string> <string name="app_version_code">Сборка приложения: %s</string> <string name="app_version_name">Версия приложения: v%s</string> - <string name="network_session_mode_entity_description">Отдельное TCP-соединение (и авторизация SOCKS) будет использоваться <b>для каждого контакта и члена группы</b>. -\n<b>Обратите внимание</b>: если у Вас много контактов, потребление батареи и трафика может быть значительно выше, и некоторые соединения могут не работать.</string> + <string name="network_session_mode_entity_description"><![CDATA[Отдельное TCP-соединение (и авторизация SOCKS) будет использоваться <b>для каждого контакта и члена группы</b>. \n<b>Обратите внимание</b>: если у Вас много контактов, потребление батареи и трафика может быть значительно выше, и некоторые соединения могут не работать.]]></string> <string name="network_session_mode_user_description"><![CDATA[Отдельное TCP-соединение (и авторизация SOCKS) будет использоваться <b>для каждого профиля чата, который Вы имеете в приложении</b>.]]></string> <string name="core_version">Версия ядра: v%s</string> <string name="users_delete_question">Удалить профиль чата\?</string> @@ -1047,8 +1028,6 @@ <string name="v4_6_audio_video_calls">Аудио и видео звонки</string> <string name="error_saving_user_password">Ошибка при сохранении пароля пользователя</string> <string name="smp_save_servers_question">Сохранить серверы\?</string> - <string name="should_be_at_least_one_profile">Должен быть хотя бы один профиль пользователя.</string> - <string name="should_be_at_least_one_visible_profile">Должен быть хотя бы один открытый профиль пользователя.</string> <string name="to_reveal_profile_enter_password">Чтобы показать Ваш скрытый профиль, введите пароль в поле поиска на странице Ваши профили.</string> <string name="user_unmute">Уведомлять</string> <string name="group_welcome_title">Приветственное сообщение</string> @@ -1057,7 +1036,6 @@ <string name="button_welcome_message">Приветственное сообщение</string> <string name="save_and_update_group_profile">Сохранить сообщение и обновить группу</string> <string name="muted_when_inactive">Без звука, когда не активный!</string> - <string name="cant_delete_user_profile">Нельзя удалить профиль пользователя!</string> <string name="enter_password_to_show">Введите пароль в поиске!</string> <string name="save_profile_password">Сохранить пароль профиля</string> <string name="v4_6_chinese_spanish_interface">Китайский и Испанский интерфейс</string> @@ -1076,7 +1054,7 @@ <string name="v4_6_reduced_battery_usage_descr">Дополнительные улучшения скоро!</string> <string name="v4_6_group_moderation_descr">Теперь админы могут: \n- удалять сообщения членов. -\n- приостанавливать членов (роль \"наблюдатель\")</string> +\n- приостанавливать членов (роль "наблюдатель")</string> <string name="v4_6_hidden_chat_profiles_descr">Защитите Ваши профили чата паролем!</string> <string name="user_unhide">Раскрыть</string> <string name="v4_6_audio_video_calls_descr">Поддержка bluetooth и другие улучшения.</string> @@ -1154,7 +1132,7 @@ <string name="decryption_error">Ошибка расшифровки</string> <string name="lock_not_enabled">Блокировка SimpleX не включена!</string> <string name="alert_title_msg_bad_hash">Ошибка хэш сообщения</string> - <string name="alert_text_msg_bad_hash">Хэш предыдущего сообщения отличается.</string> + <string name="alert_text_msg_bad_hash">Хэш предыдущего сообщения отличается\"</string> <string name="confirm_passcode">Подтвердить код</string> <string name="incorrect_passcode">Неправильный код</string> <string name="lock_after">Заблокировать через</string> @@ -1214,7 +1192,7 @@ <string name="set_passcode">Установить код доступа</string> <string name="edit_history">История</string> <string name="info_menu">Информация</string> - <string name="auth_open_chat_profiles">Открыть профили чата</string> + <string name="auth_open_chat_profiles">Изменить профили чата</string> <string name="received_message">Полученное сообщение</string> <string name="sent_message">Отправленное сообщение</string> <string name="disappearing_message">Исчезающее сообщение</string> @@ -1248,14 +1226,14 @@ <string name="allow_message_reactions">Разрешить реакции на сообщения.</string> <string name="allow_message_reactions_only_if">Разрешить реакции на сообщения, только если ваш контакт разрешает их.</string> <string name="allow_your_contacts_adding_message_reactions">Разрешить контактам добавлять реакции на сообщения.</string> - <string name="group_members_can_add_message_reactions">Члены группы могут добавлять реакции на сообщения.</string> + <string name="group_members_can_add_message_reactions">Члены могут добавлять реакции на сообщения.</string> <string name="message_reactions_prohibited_in_this_chat">Реакции на сообщения в этом чате запрещены.</string> - <string name="message_reactions_are_prohibited">Реакции на сообщения запрещены в этой группе.</string> + <string name="message_reactions_are_prohibited">Реакции на сообщения запрещены.</string> <string name="only_your_contact_can_add_message_reactions">Только Ваш контакт может добавлять реакции на сообщения.</string> <string name="prohibit_message_reactions">Запретить реакции на сообщения.</string> <string name="prohibit_message_reactions_group">Запретить реакции на сообщения.</string> <string name="custom_time_unit_seconds">секунд</string> - <string name="theme_colors_section_title">ЦВЕТА ТЕМЫ</string> + <string name="theme_colors_section_title">ЦВЕТА ИНТЕРФЕЙСА</string> <string name="share_address_with_contacts_question">Поделиться адресом с контактами\?</string> <string name="profile_update_will_be_sent_to_contacts">Обновлённый профиль будет отправлен Вашим контактам.</string> <string name="learn_more_about_address">Об адресе SimpleX</string> @@ -1333,7 +1311,7 @@ <string name="email_invite_subject">Давайте поговорим в SimpleX Chat</string> <string name="opening_database">Открытие базы данных…</string> <string name="info_row_updated_at">Запись обновлена</string> - <string name="non_fatal_errors_occured_during_import">Во время импорта произошли некоторые ошибки - для получения более подробной информации вы можете обратиться к консоли.</string> + <string name="non_fatal_errors_occured_during_import">Во время импорта произошли некоторые ошибки:</string> <string name="item_info_no_text">нет текста</string> <string name="search_verb">Поиск</string> <string name="la_mode_off">Отключено</string> @@ -1342,7 +1320,7 @@ <string name="snd_conn_event_ratchet_sync_ok">шифрование работает для %s</string> <string name="snd_conn_event_ratchet_sync_required">требуется новое соглашение о шифровании для %s</string> <string name="abort_switch_receiving_address_desc">Изменение адреса будет прекращено. Будет использоваться старый адрес.</string> - <string name="abort_switch_receiving_address">Прекратить изменение адреса</string> + <string name="abort_switch_receiving_address">Остановить изменение адреса</string> <string name="receipts_section_contacts">Контакты</string> <string name="receipts_contacts_disable_keep_overrides">Выключить (кроме исключений)</string> <string name="conn_event_ratchet_sync_started">шифрование согласовывается…</string> @@ -1364,13 +1342,13 @@ <string name="you_can_enable_delivery_receipts_later_alert">Вы можете включить их позже в настройках Конфиденциальности.</string> <string name="error_aborting_address_change">Ошибка при прекращении изменения адреса</string> <string name="abort_switch_receiving_address_confirm">Прекратить</string> - <string name="abort_switch_receiving_address_question">Прекратить изменение адреса\?</string> + <string name="abort_switch_receiving_address_question">Остановить изменение адреса?</string> <string name="unfavorite_chat">Не избранный</string> <string name="shutdown_alert_desc">Нотификации перестанут работать, пока вы не перезапустите приложение</string> <string name="network_option_protocol_timeout_per_kb">Таймаут протокола на KB</string> <string name="allow_to_send_files">Разрешить посылать файлы и медиа.</string> - <string name="group_members_can_send_files">Члены группы могут слать файлы и медиа.</string> - <string name="files_are_prohibited_in_group">Файлы и медиа запрещены в этой группе.</string> + <string name="group_members_can_send_files">Члены могут слать файлы и медиа.</string> + <string name="files_are_prohibited_in_group">Файлы и медиа запрещены.</string> <string name="files_and_media_prohibited">Файлы и медиа запрещены!</string> <string name="only_owners_can_enable_files_and_media">Только владельцы группы могут разрешить файлы и медиа.</string> <string name="files_and_media">Файлы и медиа</string> @@ -1518,7 +1496,7 @@ <string name="connect_plan_connect_via_link">Соединиться через ссылку?</string> <string name="v5_4_incognito_groups">Инкогнито группы</string> <string name="connect_plan_already_joining_the_group">Вступление в группу уже начато!</string> - <string name="moderated_items_description">%d сообщений модерировано членом %s</string> + <string name="moderated_items_description">%1$d сообщений модерировано членом %2$s</string> <string name="remote_host_was_disconnected_toast"><![CDATA[Мобильный <b>%s</b> был отключен]]></string> <string name="v5_4_better_groups_descr">Быстрое вступление и надежная доставка сообщений.</string> <string name="connect_plan_connect_to_yourself">Соединиться с самим собой?</string> @@ -1566,9 +1544,9 @@ <string name="block_member_desc">Все новые сообщения от %s будут скрыты!</string> <string name="desktop_app_version_is_incompatible">Версия настольного приложения %s несовместима с этим приложением.</string> <string name="blocked_item_description">заблокировано</string> - <string name="connect_plan_you_are_already_connecting_to_vName">Вы уже соединяетесь с %1$s.</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Вы уже соединяетесь с <b>%1$s</b>.]]></string> <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Вы уже вступаете в группу по этой ссылке.</string> - <string name="connect_plan_you_are_already_joining_the_group_vName">Вы уже вступаете в группу %1$s.</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Вы уже вступаете в группу <b>%1$s</b>.]]></string> <string name="connect_plan_this_is_your_own_one_time_link">Это ваша собственная одноразовая ссылка!</string> <string name="v5_4_link_mobile_desktop_descr">Через безопасный квантово-устойчивый протокол.</string> <string name="v5_4_block_group_members_descr">Чтобы скрыть нежелательные сообщения.</string> @@ -1578,9 +1556,9 @@ <string name="group_members_n">%s, %s и %d членов группы</string> <string name="this_device">Это устройство</string> <string name="unblock_member_button">Разблокировать члена группы</string> - <string name="contact_tap_to_connect">Нажмите чтобы соединиться</string> + <string name="contact_tap_to_connect">Нажмите, чтобы соединиться</string> <string name="this_device_name">Имя этого устройства</string> - <string name="connect_plan_you_are_already_in_group_vName">Вы уже состоите в группе %1$s.</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Вы уже состоите в группе <b>%1$s</b>.]]></string> <string name="connect_plan_this_is_your_own_simplex_address">Это ваш собственный адрес SimpleX!</string> <string name="unblock_member_question">Разблокировать члена группы?</string> <string name="settings_section_title_use_from_desktop">Использовать с компьютера</string> @@ -1590,7 +1568,7 @@ <string name="this_device_name_shared_with_mobile">Имя устройства будет доступно подключенному мобильному клиенту.</string> <string name="verify_code_on_mobile">Сверьте код на мобильном</string> <string name="non_content_uri_alert_text">Указан неверный путь к файлу. Сообщите о проблеме разработчикам приложения.</string> - <string name="connect_plan_this_is_your_link_for_group_vName">Это ваша ссылка на группу %1$s!</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Это ваша ссылка на группу <b>%1$s</b>!]]></string> <string name="verify_code_with_desktop">Сверьте код с компьютером</string> <string name="scan_qr_code_from_desktop">Сканировать QR код с компьютера</string> <string name="unblock_member_confirmation">Разблокировать</string> @@ -1605,4 +1583,888 @@ <string name="not_compatible">Несовместимая версия!</string> <string name="group_member_role_author">автор</string> <string name="multicast_discoverable_via_local_network">Найти через локальную сеть</string> -</resources> \ No newline at end of file + <string name="refresh_qr_code">Обновить</string> + <string name="random_port">Случайный</string> + <string name="open_port_in_firewall_desc">Чтобы разрешить мобильному приложению подключаться к компьютеру, откройте этот порт в брандмауэре, если он включен</string> + <string name="create_chat_profile">Создать профиль чата</string> + <string name="open_port_in_firewall_title">Открыть порт в брандмауэре</string> + <string name="disconnect_remote_hosts">Отключить мобильные</string> + <string name="no_connected_mobile">Нет подключённых мобильных</string> + <string name="add_contact_tab">Добавить контакт</string> + <string name="v5_5_private_notes">Личные заметки</string> + <string name="v5_5_join_group_conversation">Присоединяйтесь к разговорам в группах</string> + <string name="v5_5_simpler_connect_ui">Вставьте ссылку, чтобы соединиться!</string> + <string name="v5_5_simpler_connect_ui_descr">Поле поиска поддерживает ссылки-приглашения.</string> + <string name="v5_5_join_group_conversation_descr">История сообщений и улучшенный каталог групп.</string> + <string name="remote_host_error_inactive"><![CDATA[Мобильный <b>%s</b> неактивен]]></string> + <string name="remote_ctrl_error_timeout">Превышено максимальное время соединения с компьютером.</string> + <string name="remote_ctrl_error_disconnected">Компьютер отсоединён</string> + <string name="remote_ctrl_error_bad_invitation">Неверный код приглашения у компьютера</string> + <string name="loading_chats">Загрузка чатов…</string> + <string name="enable_camera_access">Включить доступ к камере</string> + <string name="tap_to_scan">Нажмите, чтобы сканировать</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Создать группу</b>: создать новую группу.]]></string> + <string name="disable_sending_recent_history">Не отправлять историю новым членам.</string> + <string name="enable_sending_recent_history">Отправить до 100 последних сообщений новым членам.</string> + <string name="clear_note_folder_warning">Все сообщения будут удалены - это нельзя отменить!</string> + <string name="camera_not_available">Камера недоступна</string> + <string name="la_app_passcode">Код доступа в приложение</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Добавить контакт</b>: создать новую ссылку-приглашение или подключиться через полученную ссылку.]]></string> + <string name="chat_is_stopped_you_should_transfer_database">Чат остановлен. Если вы уже использовали эту базу данных на другом устройстве, перенесите ее обратно до запуска чата.</string> + <string name="remote_host_was_disconnected_title">Соединение остановлено</string> + <string name="info_row_created_at">Создано</string> + <string name="keep_invitation_link">Оставить</string> + <string name="share_text_created_at">Создано: %s</string> + <string name="remote_host_error_missing"><![CDATA[Мобильный <b>%s</b> отсутствует]]></string> + <string name="or_show_this_qr_code">Или покажите этот код</string> + <string name="agent_internal_error_desc">Пожалуйста, сообщите об этом разработчикам: +\n%s</string> + <string name="developer_options_section">Опции разработчика</string> + <string name="remote_host_disconnected_from"><![CDATA[Отсоединён от мобильного <b>%s</b> по причине: %s]]></string> + <string name="error_creating_message">Ошибка создания сообщения</string> + <string name="error_deleting_note_folder">Ошибка удаления заметки</string> + <string name="v5_5_new_interface_languages">Венгерский и Турецкий интерфейс</string> + <string name="search_or_paste_simplex_link">Искать или вставьте ссылку SimpleX</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">Этот QR код не является SimpleX-ccылкой.</string> + <string name="v5_5_private_notes_descr">С зашифрованными файлами и медиа.</string> + <string name="v5_5_message_delivery_descr">С уменьшенным потреблением батареи.</string> + <string name="keep_unused_invitation_question">Оставить неиспользованное приглашение?</string> + <string name="v5_5_message_delivery">Улучшенная доставка сообщений</string> + <string name="remote_ctrl_was_disconnected_title">Соединение остановлено</string> + <string name="restart_chat_button">Перезапустить чат</string> + <string name="start_chat_question">Запустить чат?</string> + <string name="note_folder_local_display_name">Личные заметки</string> + <string name="recent_history">Доступ к истории</string> + <string name="recent_history_is_not_sent_to_new_members">История не отправляется новым членам.</string> + <string name="recent_history_is_sent_to_new_members">До 100 последних сообщений отправляются новым членам.</string> + <string name="show_internal_errors">Показывать внутренние ошибки</string> + <string name="remote_ctrl_error_bad_state">Ошибка соединения с компьютером</string> + <string name="remote_host_error_bad_state"><![CDATA[Ошибка соединения с мобильным <b>%s</b>]]></string> + <string name="remote_ctrl_error_busy">Компьютер занят</string> + <string name="remote_ctrl_error_inactive">Компьютер неактивен</string> + <string name="remote_host_error_bad_version"><![CDATA[Версия приложения на мобильном <b>%s</b> не поддерживается. Пожалуйста, установите одинаковую версию на оба устройства.]]></string> + <string name="remote_host_error_busy"><![CDATA[Мобильный <b>%s</b> занят]]></string> + <string name="remote_host_error_disconnected"><![CDATA[Мобильный <b>%s</b> отсоединён]]></string> + <string name="remote_host_error_timeout"><![CDATA[Превышено максимальное время соединения с мобильным <b>%s</b>]]></string> + <string name="agent_critical_error_title">Критическая ошибка</string> + <string name="agent_critical_error_desc">Пожалуйста, сообщите об этом разработчикам: +\n%s +\n +\nРекомендовано перезапустить приложение.</string> + <string name="failed_to_create_user_invalid_title">Ошибка имени!</string> + <string name="failed_to_create_user_invalid_desc">Ошибка имени профиля. Пожалуйста, выберите другое имя.</string> + <string name="the_text_you_pasted_is_not_a_link">Вставленный текст не является SimpleX-ссылкой.</string> + <string name="creating_link">Создаётся ссылка…</string> + <string name="tap_to_paste_link">Нажмите, чтобы вставить ссылку</string> + <string name="invalid_qr_code">Ошибка QR кода</string> + <string name="share_this_1_time_link">Поделиться одноразовой ссылкой-приглашением</string> + <string name="retry_verb">Повторить</string> + <string name="app_was_crashed">Ошибка приложения</string> + <string name="error_showing_message">ошибка отображения сообщения</string> + <string name="error_showing_content">ошибка отображения содержания</string> + <string name="remote_ctrl_disconnected_with_reason">Отсоединён по причине: %s</string> + <string name="possible_slow_function_desc">Выполнение задачи занимает долгое время: %1$d секунд: %2$s</string> + <string name="possible_slow_function_title">Медленный вызов</string> + <string name="profile_update_event_contact_name_changed">контакт %1$s изменён на %2$s</string> + <string name="profile_update_event_member_name_changed">член %1$s изменился на %2$s</string> + <string name="profile_update_event_removed_address">удалён адрес контакта</string> + <string name="profile_update_event_removed_picture">удалена картинка профиля</string> + <string name="profile_update_event_set_new_address">установлен новый адрес контакта</string> + <string name="profile_update_event_set_new_picture">установлена новая картинка профиля</string> + <string name="profile_update_event_updated_profile">профиль обновлён</string> + <string name="remote_ctrl_error_bad_version">Версия приложения на компьютере не поддерживается. Пожалуйста, установите одинаковую версию на оба устройства.</string> + <string name="agent_internal_error_title">Внутренняя ошибка</string> + <string name="clear_note_folder_question">Очистить личные заметки?</string> + <string name="new_chat">Новый чат</string> + <string name="new_chat">Новое сообщение</string> + <string name="or_scan_qr_code">Или отсканируйте QR код</string> + <string name="you_can_view_invitation_link_again">Вы можете увидеть ссылку-приглашение снова открыв соединение.</string> + <string name="show_slow_api_calls">Показывать медленные вызовы API</string> + <string name="past_member_vName">Бывший член %1$s</string> + <string name="saved_message_title">Сохраненное сообщение</string> + <string name="group_member_status_unknown_short">неизвестно</string> + <string name="group_member_status_unknown">неизвестный статус</string> + <string name="blocked_by_admin_items_description">%d сообщений заблокировано администратором</string> + <string name="rcv_group_event_member_blocked">%s заблокирован</string> + <string name="rcv_group_event_member_unblocked">%s разблокирован</string> + <string name="snd_group_event_member_unblocked">Вы разблокировали %s</string> + <string name="unblock_for_all">Разблокировать для всех</string> + <string name="block_for_all_question">Заблокировать члена для всех?</string> + <string name="member_info_member_blocked">заблокирован</string> + <string name="blocked_by_admin_item_description">заблокировано администратором</string> + <string name="member_blocked_by_admin">Заблокирован администратором</string> + <string name="block_for_all">Заблокировать для всех</string> + <string name="error_blocking_member_for_all">Ошибка при блокировании члена для всех</string> + <string name="unblock_for_all_question">Разблокировать члена для всех?</string> + <string name="snd_group_event_member_blocked">Вы заблокировали %s</string> + <string name="e2ee_info_no_pq"><![CDATA[Сообщения, файлы и звонки защищены <b>end-to-end шифрованием</b> с прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома.]]></string> + <string name="e2ee_info_no_pq_short">Чат защищен end-to-end шифрованием.</string> + <string name="e2ee_info_pq_short">Чат защищен квантово-устойчивым end-to-end шифрованием.</string> + <string name="auth_open_migration_to_another_device">Открыть экран миграции</string> + <string name="migrate_from_another_device">Миграция с другого устройства</string> + <string name="set_passphrase">Установить пароль</string> + <string name="conn_event_disabled_pq">стандартное end-to-end шифрование</string> + <string name="welcome_message_is_too_long">Приветственное сообщение слишком длинное</string> + <string name="message_too_large">Сообщение слишком большое</string> + <string name="migrate_to_device_repeat_download">Повторить загрузку</string> + <string name="migrate_to_device_try_again">Вы можете попробовать еще раз.</string> + <string name="migrate_from_device_uploading_archive">Загрузка архива</string> + <string name="migrate_from_device_archiving_database">Подготовка архива</string> + <string name="migrate_from_device_bytes_uploaded">%s загружено</string> + <string name="migrate_from_device_start_chat">Запустить чат</string> + <string name="v5_6_app_data_migration">Миграция данных</string> + <string name="v5_6_quantum_resistant_encryption_descr">Включите для контактов (BETA)!</string> + <string name="v5_6_safer_groups">Более безопасные группы</string> + <string name="v5_6_safer_groups_descr">Админы могут заблокировать члена группы.</string> + <string name="paste_archive_link">Вставьте ссылку архива</string> + <string name="invalid_file_link">Ошибка ссылки</string> + <string name="migrate_to_device_migrating">Миграция</string> + <string name="migrate_to_device_downloading_archive">Загрузка архива</string> + <string name="migrate_to_device_downloading_details">Загрузка ссылки архива</string> + <string name="migrate_to_device_database_init">Подготовка загрузки</string> + <string name="migrate_to_device_download_failed">Ошибка загрузки</string> + <string name="migrate_to_device_bytes_downloaded">%s загружено</string> + <string name="migrate_to_device_importing_archive">Импорт архива</string> + <string name="migrate_to_device_import_failed">Ошибка импорта</string> + <string name="migrate_to_device_chat_migrated">Чат мигрирован!</string> + <string name="migrate_to_device_enter_passphrase">Введите пароль</string> + <string name="migrate_to_device_error_downloading_archive">Ошибка загрузки архива</string> + <string name="migrate_to_device_repeat_import">Повторить импорт</string> + <string name="migrate_to_device_confirm_network_settings_footer">Пожалуйста, подтвердите, что настройки сети верны для этого устройства.</string> + <string name="migrate_to_device_apply_onion">Применить</string> + <string name="migrate_from_device_title">Мигрировать устройство</string> + <string name="migrate_from_device_error_saving_settings">Ошибка сохранения настроек</string> + <string name="migrate_from_device_error_exporting_archive">Ошибка при экспорте архива чата</string> + <string name="migrate_from_device_database_init">Подготовка загрузки</string> + <string name="migrate_from_device_error_uploading_archive">Ошибка загрузки архива</string> + <string name="migrate_from_device_error_deleting_database">Ошибка при удалении данных чата</string> + <string name="migrate_from_device_stopping_chat">Остановка чата</string> + <string name="migrate_from_device_archive_and_upload">Архивировать и загрузить</string> + <string name="migrate_from_device_confirm_upload">Подтвердить загрузку</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Все ваши контакты, разговоры и файлы будут надежно зашифрованы и загружены на выбранные XFTP серверы.</string> + <string name="migrate_from_device_upload_failed">Ошибка загрузки</string> + <string name="migrate_from_device_repeat_upload">Повторить загрузку</string> + <string name="migrate_from_device_try_again">Вы можете попробовать еще раз.</string> + <string name="migrate_from_device_cancel_migration">Отменить миграцию</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Выберите <i>Мигрировать с другого устройства</i> на новом устройстве и сосканируйте QR код.]]></string> + <string name="migrate_from_device_creating_archive_link">Создание ссылки на архив</string> + <string name="migrate_from_device_delete_database_from_device">Удалить базу данных с этого устройства</string> + <string name="migrate_from_device_finalize_migration">Завершить миграцию</string> + <string name="migrate_from_device_or_share_this_file_link">Или передайте эту ссылку</string> + <string name="migrate_from_device_migration_complete">Миграция завершена</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Внимание: запуск чата на нескольких устройствах не поддерживается и приведет к сбоям доставки сообщений.</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[Вы <b>не должны</b> использовать одну и ту же базу данных на двух устройствах.]]></string> + <string name="migrate_from_device_check_connection_and_try_again">Проверьте подключение к Интернету и повторите попытку</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Подтвердите, что Вы помните пароль базы данных для ее миграции.</string> + <string name="migrate_from_device_verify_database_passphrase">Проверка пароля базы данных</string> + <string name="migrate_from_device_verify_passphrase">Проверить пароль</string> + <string name="migrate_from_device_error_verifying_passphrase">Ошибка подтверждения пароля:</string> + <string name="call_service_notification_audio_call">Аудиозвонок</string> + <string name="call_service_notification_end_call">Завершить звонок</string> + <string name="call_service_notification_video_call">Видеозвонок</string> + <string name="migrate_from_device_chat_should_be_stopped">Чтобы продолжить, чат должен быть остановлен.</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Обратите внимание</b>: использование одной и той же базы данных на двух устройствах нарушит расшифровку сообщений от ваших контактов, как свойство защиты соединений.]]></string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Внимание</b>: архив будет удален.]]></string> + <string name="migrate_to_device_confirm_network_settings">Подтвердите настройки сети</string> + <string name="e2ee_info_pq"><![CDATA[Сообщения, файлы и звонки защищены <b>квантово-устойчивым end-to-end шифрованием</b> с идеальной прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома.]]></string> + <string name="migrate_to_device_title">Мигрировать сюда</string> + <string name="migrate_from_device_to_another_device">Мигрировать на другое устройство</string> + <string name="v5_6_app_data_migration_descr">Мигрируйте на другое устройство через QR код.</string> + <string name="or_paste_archive_link">Или вставьте ссылку архива</string> + <string name="v5_6_picture_in_picture_calls">Звонки с картинкой-в-картинке</string> + <string name="conn_event_enabled_pq">квантово-устойчивое e2e шифрование</string> + <string name="v5_6_quantum_resistant_encryption">Квантово-устойчивое шифрование</string> + <string name="v5_6_picture_in_picture_calls_descr">Используйте приложение во время звонка.</string> + <string name="migrate_from_device_exported_file_doesnt_exist">Экспортированный файл не существует</string> + <string name="migrate_to_device_file_delete_or_link_invalid">Файл удален или ошибка ссылки</string> + <string name="migrate_to_device_finalize_migration">Завершите миграцию на другом устройстве.</string> + <string name="database_migration_in_progress">Выполняется миграция базы данных. +\nЭто может занять несколько минут.</string> + <string name="unable_to_open_browser_title">Ошибка открытия браузера</string> + <string name="unable_to_open_browser_desc">Для звонков требуется веб-браузер по умолчанию. Пожалуйста, настройте браузер по умолчанию в системе и поделитесь дополнительной информацией с разработчиками.</string> + <string name="network_type_other">Другaя сеть</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="network_type_ethernet">Проводная сеть</string> + <string name="network_type_cellular">Мобильная сеть</string> + <string name="settings_section_title_network_connection">Интернет-соединение</string> + <string name="network_type_no_network_connection">Нет интернет-соединения</string> + <string name="error_showing_desktop_notification">Ошибка при показе уведомления, свяжитесь с разработчиками.</string> + <string name="permissions_camera">Камера</string> + <string name="permissions_camera_and_record_audio">Камера и микрофон</string> + <string name="permissions_find_in_settings_and_grant">Найдите это разрешение в настройках Android и добавьте его вручную.</string> + <string name="permissions_grant_in_settings">Разрешить в настройках</string> + <string name="permissions_required">Дать разрешение</string> + <string name="permissions_grant">Дать разрешение для совершения звонков</string> + <string name="permissions_record_audio">Микрофон</string> + <string name="permissions_open_settings">Открыть настройки</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="network_option_rcv_concurrency">Одновременный приём</string> + <string name="audio_device_earpiece">Верхний динамик</string> + <string name="audio_device_wired_headphones">Наушники</string> + <string name="audio_device_speaker">Громкоговоритель</string> + <string name="v5_7_call_sounds">Звуки во время звонков</string> + <string name="v5_7_network_descr">Более надежное соединение с сетью.</string> + <string name="v5_7_network">Статус сети</string> + <string name="saved_description">сохранено</string> + <string name="saved_from_description">сохранено из %s</string> + <string name="forwarded_chat_item_info_tab">Переслано</string> + <string name="forwarded_from_chat_item_info_title">Переслано из</string> + <string name="recipients_can_not_see_who_message_from">Получатели не видят от кого это сообщение.</string> + <string name="forwarded_description">переслано</string> + <string name="files_and_media_not_allowed">Файлы и медиа не разрешены</string> + <string name="simplex_links_not_allowed">Ссылки SimpleX не разрешены</string> + <string name="voice_messages_not_allowed">Голосовые сообщения не разрешены</string> + <string name="simplex_links">Ссылки SimpleX</string> + <string name="allow_to_send_simplex_links">Разрешить отправлять ссылки SimpleX.</string> + <string name="prohibit_sending_simplex_links">Запретить отправку ссылок SimpleX</string> + <string name="group_members_can_send_simplex_links">Члены могут отправлять ссылки SimpleX</string> + <string name="feature_roles_admins">админы</string> + <string name="feature_roles_all_members">все члены</string> + <string name="feature_roles_owners">владельцы</string> + <string name="download_file">Загрузить</string> + <string name="saved_chat_item_info_tab">Сохранено</string> + <string name="saved_from_chat_item_info_title">Сохранено из</string> + <string name="feature_enabled_for">Включено для</string> + <string name="forward_chat_item">Переслать</string> + <string name="v5_7_forward">Переслать и сохранить сообщение</string> + <string name="simplex_links_are_prohibited_in_group">Ссылки SimpleX запрещены.</string> + <string name="forward_message">Переслать сообщение…</string> + <string name="v5_7_new_interface_languages">Литовский интерфейс</string> + <string name="v5_7_forward_descr">Источник сообщения остаётся конфиденциальным.</string> + <string name="v5_7_call_sounds_descr">Во время соединения аудио и видео звонков.</string> + <string name="v5_7_shape_profile_images">Форма картинок профилей</string> + <string name="v5_7_shape_profile_images_descr">Квадрат, круг и все, что между ними.</string> + <string name="v5_7_quantum_resistant_encryption_descr">Будет включено в прямых разговорах!</string> + <string name="settings_section_title_files">ФАЙЛЫ</string> + <string name="v5_8_chat_themes">Новые темы чатов</string> + <string name="message_queue_info_none">нет</string> + <string name="color_mode_light">Светлая</string> + <string name="color_mode_system">Системная</string> + <string name="dark_mode_colors">Цвета тёмного режима</string> + <string name="v5_8_safe_files">Получайте файлы безопасно</string> + <string name="v5_8_private_routing">Конфиденциальная доставка 🚀</string> + <string name="v5_8_message_delivery">Улучшенная доставка сообщений</string> + <string name="v5_8_message_delivery_descr">Уменьшенный расход батареи.</string> + <string name="srv_error_version">Версия сервера несовместима с настройками сети.</string> + <string name="snd_error_auth">Неверный ключ или неизвестное соединение - скорее всего, это соединение удалено.</string> + <string name="snd_error_quota">Превышено количество сообщений - предыдущие сообщения не доставлены.</string> + <string name="snd_error_relay">Ошибка сервера получателя: %1$s</string> + <string name="ci_status_other_error">Ошибка: %1$s</string> + <string name="snd_error_proxy_relay">Пересылающий сервер: %1$s +\nОшибка сервера получателя: %2$s</string> + <string name="snd_error_proxy">Пересылающий сервер: %1$s +\nОшибка: %2$s</string> + <string name="message_delivery_warning_title">Предупреждение доставки сообщения</string> + <string name="snd_error_expired">Ошибка сети - сообщение не было отправлено после многократных попыток.</string> + <string name="srv_error_host">Адрес сервера несовместим с настройками сети.</string> + <string name="message_queue_info_server_info">информация сервера об очереди: %1$s +\n +\nпоследнее полученное сообщение: %2$s</string> + <string name="chat_list_always_visible">Показать список чатов в новом окне</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">Приложение будет запрашивать подтверждение загрузки с неизвестных серверов (за исключением .onion адресов или когда SOCKS-прокси включен).</string> + <string name="network_smp_proxy_mode_unprotected">Незащищённый</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Без Тора или ВПН, Ваш IP адрес будет доступен серверам файлов.</string> + <string name="network_smp_proxy_fallback_allow_protected_description">Отправлять сообщения напрямую, когда IP адрес защищен, и Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку.</string> + <string name="theme_black">Черная</string> + <string name="chat_theme_apply_to_dark_mode">Тёмный режим</string> + <string name="network_smp_proxy_fallback_prohibit_description">Не отправлять сообщения напрямую, даже если сервер получателя не поддерживает конфиденциальную доставку.</string> + <string name="color_mode">Режим цветов</string> + <string name="network_smp_proxy_fallback_allow_downgrade">Разрешить прямую доставку</string> + <string name="network_smp_proxy_mode_always">Всегда</string> + <string name="v5_8_safe_files_descr">Подтверждать файлы с неизвестных серверов.</string> + <string name="network_smp_proxy_mode_always_description">Всегда использовать конфиденциальную доставку.</string> + <string name="color_mode_dark">Тёмная</string> + <string name="info_row_debug_delivery">Отладка доставки</string> + <string name="error_initializing_web_view">Ошибка инициализации WebView. Обновите Вашу систему до новой версии. Свяжитесь с разработчиками. +\nОшибка: %s</string> + <string name="chat_theme_apply_to_light_mode">Светлый режим</string> + <string name="v5_8_chat_themes_descr">Сделайте ваши чаты разными!</string> + <string name="message_queue_info">Информация об очереди сообщений</string> + <string name="v5_8_persian_ui">Персидский интерфейс</string> + <string name="protect_ip_address">Защитить IP адрес</string> + <string name="v5_8_private_routing_descr">Защитите ваш IP адрес от серверов сообщений, выбранных Вашими контактами. \nВключите в настройках Сети и серверов.</string> + <string name="network_smp_proxy_fallback_allow_description">Отправьте сообщения напрямую, когда Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку.</string> + <string name="network_smp_proxy_mode_private_routing">Конфиденциальная доставка</string> + <string name="network_smp_proxy_mode_unknown_description">Использовать конфиденциальную доставку с неизвестными серверами.</string> + <string name="network_smp_proxy_mode_unprotected_description">Использовать конфиденциальную доставку с неизвестными серверами, когда IP адрес не защищен.</string> + <string name="network_smp_proxy_fallback_allow_protected">Когда IP защищен</string> + <string name="network_smp_proxy_fallback_allow">Да</string> + <string name="private_routing_explanation">Чтобы защитить ваш IP адрес, приложение использует Ваши SMP серверы для конфиденциальной доставки сообщений.</string> + <string name="settings_section_title_profile_images">Изображения профилей</string> + <string name="chat_theme_apply_to_all_modes">Все режимы</string> + <string name="theme_destination_app_theme">Тема приложения</string> + <string name="chat_theme_reset_to_app_theme">Сбросить на тему приложения</string> + <string name="chat_theme_reset_to_user_theme">Сбросить на тему пользователя</string> + <string name="file_not_approved_title">Неизвестные серверы!</string> + <string name="file_not_approved_descr">Без Тора или ВПН, Ваш IP адрес будет доступен этим серверам файлов: +\n%1$s.</string> + <string name="network_smp_proxy_mode_never_description">Не использовать конфиденциальную маршрутизацию.</string> + <string name="network_smp_proxy_mode_never">Никогда</string> + <string name="network_smp_proxy_mode_unknown">Неизвестные серверы</string> + <string name="network_smp_proxy_fallback_prohibit">Нет</string> + <string name="private_routing_show_message_status">Показать статус сообщения</string> + <string name="update_network_smp_proxy_fallback_question">Прямая доставка сообщений</string> + <string name="update_network_smp_proxy_mode_question">Режим доставки сообщений</string> + <string name="settings_section_title_private_message_routing">КОНФИДЕНЦИАЛЬНАЯ ДОСТАВКА СООБЩЕНИЙ</string> + <string name="settings_section_title_chat_colors">Цвета чата</string> + <string name="settings_section_title_chat_theme">Тема чата</string> + <string name="settings_section_title_user_theme">Тема профиля</string> + <string name="color_primary_variant2">Дополнительный акцент 2</string> + <string name="wallpaper_advanced_settings">Дополнительные настройки</string> + <string name="wallpaper_scale_fill">Обрезать</string> + <string name="wallpaper_scale_fit">Полностью</string> + <string name="wallpaper_preview_hello_alice">Добрый день!</string> + <string name="wallpaper_preview_hello_bob">Доброе утро!</string> + <string name="color_received_quote">Полученный ответ</string> + <string name="theme_remove_image">Удалить изображение</string> + <string name="wallpaper_scale_repeat">Повторить</string> + <string name="reset_single_color">Сбросить цвет</string> + <string name="wallpaper_scale">Масштаб</string> + <string name="color_sent_quote">Отправленный ответ</string> + <string name="chat_theme_set_default_theme">Установить тему по умолчанию</string> + <string name="color_wallpaper_tint">Рисунок обоев</string> + <string name="color_wallpaper_background">Фон обоев</string> + <string name="chat_theme_apply_to_mode">Применить к</string> + <string name="cannot_share_message_alert_title">Не удается отправить сообщение</string> + <string name="app_check_for_updates_beta">Бета</string> + <string name="servers_info_sessions_connected">Соединeно</string> + <string name="attempts_label">попытки</string> + <string name="completed">Готово</string> + <string name="confirm_delete_contact_question">Потвердить удаление контакта?</string> + <string name="privacy_media_blur_radius">Размытие изображений</string> + <string name="all_users">Все профили</string> + <string name="app_check_for_updates_notice_title">Проверка на наличие обновлений</string> + <string name="allow_calls_question">Разрешить звонки?</string> + <string name="calls_prohibited_alert_title">Звонки запрещены!</string> + <string name="cant_call_member_alert_title">Не удается позвонить члену группы</string> + <string name="app_check_for_updates_download_completed_title">Обновление скачано</string> + <string name="info_view_call_button">звонок</string> + <string name="cant_call_contact_alert_title">Не удается позвонить контакту</string> + <string name="cant_send_message_to_member_alert_title">Не удается написать члену группы</string> + <string name="app_check_for_updates">Проверять обновления</string> + <string name="info_view_connect_button">соединиться</string> + <string name="proxy_destination_error_broker_host">Адрес сервера назначения %1$s несовместим с настройками пересылающего сервера %2$s.</string> + <string name="smp_proxy_error_connecting">Ошибка подключения к пересылающему серверу %1$s. Попробуйте позже.</string> + <string name="proxy_destination_error_failed_to_connect">Пересылающий сервер %1$s не смог подключиться к серверу назначения %2$s. Попробуйте позже.</string> + <string name="smp_proxy_error_broker_version">Версия пересылающего сервера несовместима с настройками сети: %1$s.</string> + <string name="proxy_destination_error_broker_version">Версия сервера назначения %1$s несовместима с пересылающим сервером %2$s.</string> + <string name="file_error_auth">Неверный ключ или неизвестный адрес блока файла - скорее всего, файл удален.</string> + <string name="cannot_share_message_alert_text">Выбранные настройки чата запрещают это сообщение.</string> + <string name="file_error">Ошибка файла</string> + <string name="scan_paste_link">Сканировать / Вставить ссылку</string> + <string name="xftp_servers_other">Другие XFTP серверы</string> + <string name="xftp_servers_configured">Настроенные XFTP серверы</string> + <string name="app_check_for_updates_button_download">Загрузка %s (%s)</string> + <string name="app_check_for_updates_update_available">Доступно обновление: %s</string> + <string name="app_check_for_updates_notice_disable">Выключить</string> + <string name="share_text_message_status">Статус сообщения: %s</string> + <string name="appearance_font_size">Размер шрифта</string> + <string name="remote_ctrl_connection_stopped_desc">Пожалуйста, проверьте, что мобильный и компьютер находятся в одной и той же локальной сети, и что брандмауэр компьютера разрешает подключение. +\nПожалуйста, поделитесь любыми другими ошибками с разработчиками.</string> + <string name="servers_info_files_tab">Файлы</string> + <string name="servers_info_sessions_connecting">Соединяется</string> + <string name="servers_info_details">Подробности</string> + <string name="servers_info_subscriptions_total">Всего</string> + <string name="servers_info_subscriptions_connections_subscribed">Активные соединения</string> + <string name="servers_info_subscriptions_section_header">Прием сообщений</string> + <string name="servers_info_subscriptions_connections_pending">В ожидании</string> + <string name="servers_info_uploaded">Загружено</string> + <string name="servers_info_reset_stats_alert_message">Статистика серверов будет сброшена - это нельзя отменить!</string> + <string name="servers_info_detailed_statistics_sent_messages_total">Всего отправлено</string> + <string name="reconnect">Переподключить</string> + <string name="smp_server">SMP сервер</string> + <string name="servers_info_starting_from">Начиная с %s.</string> + <string name="xftp_server">XFTP сервер</string> + <string name="duplicates_label">дубликаты</string> + <string name="expired_label">истекло</string> + <string name="other_label">другое</string> + <string name="connections">Соединения</string> + <string name="deletion_errors">Ошибки удаления</string> + <string name="created">Создано</string> + <string name="deleted">Удалено</string> + <string name="secured">Защищено</string> + <string name="subscribed">Подписано</string> + <string name="chunks_uploaded">Блоков загружено</string> + <string name="upload_errors">Ошибки загрузки</string> + <string name="size">Размер</string> + <string name="download_errors">Ошибки приема</string> + <string name="downloaded_files">Принятые файлы</string> + <string name="server_address">Адрес сервера</string> + <string name="file_error_relay">Ошибка сервера файлов: %1$s</string> + <string name="info_row_file_status">Статус файла</string> + <string name="temporary_file_error">Временная ошибка файла</string> + <string name="privacy_media_blur_radius_strong">Сильное</string> + <string name="acknowledged">Подтверждено</string> + <string name="acknowledgement_errors">Ошибки подтверждения</string> + <string name="decryption_errors">ошибки расшифровки</string> + <string name="other_errors">другие ошибки</string> + <string name="proxied">Проксировано</string> + <string name="send_errors">Ошибки отправки</string> + <string name="chunks_deleted">Блоков удалено</string> + <string name="chunks_downloaded">Блоков принято</string> + <string name="subscription_results_ignored">Подписок игнорировано</string> + <string name="copy_error">Ошибка копирования</string> + <string name="info_view_video_button">видеозвонок</string> + <string name="delete_contact_cannot_undo_warning">Контакт будет удален — это нельзя отменить!</string> + <string name="keep_conversation">Оставить разговор</string> + <string name="only_delete_conversation">Удалить только разговор</string> + <string name="delete_without_notification">Удалить без уведомления</string> + <string name="you_can_still_send_messages_to_contact">Вы можете отправлять сообщения %1$s из Архивированных контактов.</string> + <string name="paste_link">Вставить ссылку</string> + <string name="no_filtered_contacts">Нет отфильтрованных контактов</string> + <string name="contact_list_header_title">Ваши контакты</string> + <string name="deleted_chats">Архивированные контакты</string> + <string name="smp_servers_configured">Настроенные SMP серверы</string> + <string name="subscription_percentage">Показать процент</string> + <string name="privacy_media_blur_radius_soft">Слабое</string> + <string name="privacy_media_blur_radius_medium">Среднее</string> + <string name="privacy_media_blur_radius_off">Выключено</string> + <string name="one_hand_ui">Доступная панель приложения</string> + <string name="current_user">Текущий профиль</string> + <string name="servers_info_missing">Нет информации, попробуйте перезагрузить</string> + <string name="servers_info">Информация о серверах</string> + <string name="servers_info_target">Информация по</string> + <string name="servers_info_connected_servers_section_header">Подключенные серверы</string> + <string name="servers_info_previously_connected_servers_section_header">Ранее подключенные серверы</string> + <string name="servers_info_proxied_servers_section_header">Проксированные серверы</string> + <string name="servers_info_private_data_disclaimer">Начиная с %s.\nВсе данные хранятся только на вашем устройстве.</string> + <string name="servers_info_reconnect_server_message">Переподключить сервер для устранения неполадок доставки сообщений. Это использует дополнительный трафик.</string> + <string name="servers_info_modal_error_title">Ошибка</string> + <string name="servers_info_reconnect_server_error">Ошибка переподключения к серверу</string> + <string name="servers_info_reconnect_servers_error">Ошибка переподключения к серверам</string> + <string name="servers_info_reset_stats_alert_confirm">Сбросить</string> + <string name="servers_info_detailed_statistics">Подробная статистика</string> + <string name="servers_info_downloaded">Принято</string> + <string name="servers_info_reset_stats_alert_error_title">Ошибка сброса статистики</string> + <string name="servers_info_reset_stats_alert_title">Сбросить всю статистику?</string> + <string name="please_try_later">Пожалуйста, попробуйте позже.</string> + <string name="private_routing_error">Ошибка конфиденциальной доставки</string> + <string name="network_error_broker_host_desc">Адрес сервера несовместим с сетевыми настройками: %1$s.</string> + <string name="network_error_broker_version_desc">Версия сервера несовместима с вашим приложением: %1$s.</string> + <string name="subscription_errors">Ошибки подписки</string> + <string name="uploaded_files">Отправленные файлы</string> + <string name="delete_members_messages__question">Удалить %d сообщений членов группы?</string> + <string name="delete_messages_mark_deleted_warning">Сообщения будут помечены на удаление. Получатель(и) смогут посмотреть эти сообщения.</string> + <string name="select_verb">Выбрать</string> + <string name="moderate_messages_will_be_deleted_warning">Сообщения будут удалены для всех членов группы.</string> + <string name="moderate_messages_will_be_marked_warning">Сообщения будут помечены как удаленные для всех членов группы.</string> + <string name="contact_deleted">Контакт удален!</string> + <string name="conversation_deleted">Разговор удален!</string> + <string name="member_inactive_title">Член неактивен</string> + <string name="message_forwarded_desc">Прямого соединения пока нет, сообщение переслано или будет переслано админом.</string> + <string name="selected_chat_items_nothing_selected">Ничего не выбрано</string> + <string name="info_view_open_button">открыть</string> + <string name="info_view_search_button">поиск</string> + <string name="selected_chat_items_selected_n">Выбрано %d</string> + <string name="toolbar_settings">Настройки</string> + <string name="you_can_still_view_conversation_with_contact">Вы по-прежнему можете просмотреть разговор с %1$s в списке чатов.</string> + <string name="smp_servers_other">Другие SMP серверы</string> + <string name="app_check_for_updates_disabled">Выключено</string> + <string name="app_check_for_updates_installed_successfully_title">Установлено успешно</string> + <string name="app_check_for_updates_button_install">Установить обновление</string> + <string name="app_check_for_updates_button_open">Открыть расположение файла</string> + <string name="app_check_for_updates_installed_successfully_desc">Пожалуйста, перезапустите приложение.</string> + <string name="app_check_for_updates_button_remind_later">Напомнить позже</string> + <string name="app_check_for_updates_stable">Стабильная версия</string> + <string name="app_check_for_updates_canceled">Загрузка обновления отменена</string> + <string name="share_text_file_status">Статус файла: %s</string> + <string name="chat_database_exported_title">Данные чата экспортированы</string> + <string name="chat_database_exported_continue">Продолжить</string> + <string name="media_and_file_servers">Серверы файлов и медиа</string> + <string name="message_servers">Серверы сообщений</string> + <string name="network_socks_proxy">SOCKS прокси</string> + <string name="chat_database_exported_not_all_files">Некоторые файл(ы) не были экспортированы</string> + <string name="chat_database_exported_migrate">Вы можете мигрировать экспортированную базу данных.</string> + <string name="chat_database_exported_save">Вы можете сохранить экспортированный архив.</string> + <string name="member_info_member_disabled">выключен</string> + <string name="member_info_member_inactive">неактивен</string> + <string name="action_button_add_members">Пригласить</string> + <string name="info_row_message_status">Статус сообщения</string> + <string name="cant_call_contact_connecting_wait_alert_text">Контакт соединяется, подождите или проверьте позже!</string> + <string name="cant_call_contact_deleted_alert_text">Контакт удален.</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">Попросите Вашего контакта разрешить звонки.</string> + <string name="network_options_save_and_reconnect">Сохранить и переподключиться</string> + <string name="cant_call_member_send_message_alert_text">Отправьте сообщение, чтобы включить звонки.</string> + <string name="network_option_tcp_connection">TCP-соединение</string> + <string name="you_need_to_allow_calls">Чтобы включить звонки, разрешите их Вашему контакту.</string> + <string name="appearance_zoom">Масштабирование</string> + <string name="remote_ctrl_connection_stopped_identity_desc">Эта ссылка была использована на другом мобильном, пожалуйста, создайте новую ссылку на компьютере.</string> + <string name="servers_info_sessions_errors">Ошибки</string> + <string name="servers_info_messages_received">Получено сообщений</string> + <string name="servers_info_messages_sent">Сообщений отправлено</string> + <string name="servers_info_reconnect_servers_message">Переподключить все подключенные серверы для устранения неполадок доставки сообщений. Это использует дополнительный трафик.</string> + <string name="servers_info_reconnect_all_servers_button">Переподключить все серверы</string> + <string name="servers_info_reconnect_server_title">Переподключить сервер?</string> + <string name="servers_info_reconnect_servers_title">Переподключить серверы?</string> + <string name="servers_info_reset_stats">Сбросить всю статистику</string> + <string name="servers_info_statistics_section_header">Статистика</string> + <string name="servers_info_proxied_servers_section_footer">Вы не подключены к этим серверам. Для доставки сообщений на них используется конфиденциальная доставка.</string> + <string name="v6_0_connect_faster_descr">Соединяйтесь с друзьями быстрее</string> + <string name="v6_0_connection_servers_status">Управляйте своей сетью</string> + <string name="v6_0_private_routing_descr">Защищает ваш IP адрес и соединения.</string> + <string name="open_server_settings_button">Открыть настройки серверов</string> + <string name="servers_info_detailed_statistics_received_messages_header">Полученные сообщения</string> + <string name="servers_info_detailed_statistics_receive_errors">Ошибки приема</string> + <string name="v6_0_your_contacts_descr">Архивируйте контакты чтобы продолжить переписку.</string> + <string name="sent_directly">Отправлено напрямую</string> + <string name="sent_via_proxy">Отправлено через прокси</string> + <string name="servers_info_transport_sessions_section_header">Транспортные сессии</string> + <string name="v6_0_connection_servers_status_descr">Состояние соединения и серверов.</string> + <string name="v6_0_delete_many_messages_descr">Удаляйте до 20 сообщений за раз.</string> + <string name="app_check_for_updates_download_started">Загрузка обновления, не закрывайте приложение.</string> + <string name="file_error_no_file">Файл не найден - скорее всего, файл был удален или отменен.</string> + <string name="smp_proxy_error_broker_host">Адрес пересылающего сервера несовместим с настройками сети: %1$s.</string> + <string name="info_view_message_button">написать</string> + <string name="compose_message_placeholder">Сообщение</string> + <string name="member_inactive_desc">Сообщение может быть доставлено позже, если член группы станет активным.</string> + <string name="message_forwarded_title">Сообщение переслано</string> + <string name="v6_0_reachable_chat_toolbar">Доступная панель чата</string> + <string name="servers_info_detailed_statistics_received_total">Всего получено</string> + <string name="app_check_for_updates_button_skip">Пропустить эту версию</string> + <string name="app_check_for_updates_notice_desc">Чтобы получать уведомления об обновлениях, включите периодическую проверку стабильных или бета-версий.</string> + <string name="v6_0_reachable_chat_toolbar_descr">Используйте приложение одной рукой.</string> + <string name="servers_info_detailed_statistics_sent_messages_header">Отправленные сообщения</string> + <string name="v6_0_privacy_blur">Размыть для конфиденциальности.</string> + <string name="create_address_button">Создать</string> + <string name="v6_0_new_media_options">Новые медиа-опции</string> + <string name="invite_friends_short">Пригласить</string> + <string name="new_message">Новое сообщение</string> + <string name="one_hand_ui_card_title">Переключите список чатов:</string> + <string name="v6_0_upgrade_app">Обновление приложения</string> + <string name="v6_0_upgrade_app_descr">Загружать новые версии из GitHub.</string> + <string name="v6_0_increase_font_size">Увеличить размер шрифтов.</string> + <string name="v6_0_new_chat_experience">Новый интерфейс 🎉</string> + <string name="v6_0_chat_list_media">Открыть из списка чатов.</string> + <string name="reset_all_hints">Сбросить все подсказки.</string> + <string name="one_hand_ui_change_instruction">Вы можете изменить это в настройках Интерфейса.</string> + <string name="compose_forward_messages_n">Пересылка %1$s сообщений</string> + <string name="compose_save_messages_n">Сохранение %1$s сообщений</string> + <string name="network_proxy_incorrect_config_desc">Убедитесь, что конфигурация прокси правильная.</string> + <string name="network_proxy_auth">Аутентификация прокси</string> + <string name="network_proxy_random_credentials">Использовать случайные учетные данные</string> + <string name="system_mode_toast">Режим системы</string> + <string name="error_forwarding_messages">Ошибка пересылки сообщений</string> + <string name="n_file_errors">%1$d ошибок файлов:\n%2$s</string> + <string name="n_other_file_errors">%1$d других ошибок файлов.</string> + <string name="forward_alert_title_messages_to_forward">Переслать %1$s сообщение(й)?</string> + <string name="forward_alert_forward_messages_without_files">Переслать сообщения без файлов?</string> + <string name="forward_files_messages_deleted_after_selection_desc">Сообщения были удалены после того, как вы их выбрали.</string> + <string name="forward_alert_title_nothing_to_forward">Нет сообщений, которые можно переслать!</string> + <string name="forward_files_in_progress_desc">%1$d файл(ов) загружаются.</string> + <string name="forward_files_failed_to_receive_desc">%1$d файл(ов) не удалось загрузить.</string> + <string name="forward_files_missing_desc">%1$d файлов было удалено.</string> + <string name="forward_files_not_accepted_desc">%1$d файлов не было загружено.</string> + <string name="forward_files_not_accepted_receive_files">Загрузить</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s сообщений не переслано</string> + <string name="forward_multiple">Переслать сообщения…</string> + <string name="error_parsing_uri_desc">Проверьте правильность ссылки SimpleX.</string> + <string name="error_parsing_uri_title">Неверная ссылка</string> + <string name="settings_section_title_chat_database">БАЗА ДАННЫХ</string> + <string name="error_initializing_web_view_wrong_arch">Ошибка инициализации WebView. Убедитесь, что у вас установлен WebView и его поддерживаемая архитектура – arm64.\nОшибка: %s</string> + <string name="icon_descr_sound_muted">Звук отключен</string> + <string name="delete_messages_cannot_be_undone_warning">Сообщения будут удалены — это нельзя отменить!</string> + <string name="switching_profile_error_title">Ошибка переключения профиля</string> + <string name="select_chat_profile">Выберите профиль чата</string> + <string name="new_chat_share_profile">Поделиться профилем</string> + <string name="switching_profile_error_message">Соединение было перемещено на %s, но при смене профиля произошла неожиданная ошибка.</string> + <string name="settings_message_shape_corner">Угол</string> + <string name="network_session_mode_session">Сессия приложения</string> + <string name="network_session_mode_session_description">Новые учетные данные SOCKS будут использоваться при каждом запуске приложения.</string> + <string name="network_session_mode_server_description">Новые учетные данные SOCKS будут использоваться для каждого сервера.</string> + <string name="network_session_mode_server">Сервер</string> + <string name="settings_section_title_message_shape">Форма сообщений</string> + <string name="settings_message_shape_tail">Хвост</string> + <string name="call_desktop_permission_denied_chrome">Нажмите кнопку информации рядом с адресной строкой, чтобы разрешить микрофон.</string> + <string name="call_desktop_permission_denied_safari">Откройте Настройки Safari / Веб-сайты / Микрофон, затем выберите Разрешить для localhost.</string> + <string name="v6_1_better_calls">Улучшенные звонки</string> + <string name="v6_1_message_dates_descr">Улучшенные даты сообщений.</string> + <string name="v6_1_better_security">Улучшенная безопасность ✅</string> + <string name="v6_1_better_user_experience">Улучшенный интерфейс</string> + <string name="v6_1_customizable_message_descr">Настраиваемая форма сообщений.</string> + <string name="v6_1_delete_many_messages_descr">Удаляйте или модерируйте до 200 сообщений.</string> + <string name="v6_1_forward_many_messages_descr">Пересылайте до 20 сообщений за раз.</string> + <string name="v6_1_better_calls_descr">Переключайте звук и видео во время звонка.</string> + <string name="v6_1_switch_chat_profile_descr">Переключайте профиль чата для одноразовых приглашений.</string> + <string name="v6_1_better_security_descr">Аудит SimpleX протоколов от Trail of Bits.</string> + <string name="call_desktop_permission_denied_title">Чтобы совершать звонки, разрешите использовать микрофон. Завершите вызов и попробуйте позвонить снова.</string> + <string name="network_proxy_auth_mode_no_auth">Не использовать учетные данные с прокси.</string> + <string name="network_proxy_incorrect_config_title">Ошибка сохранения прокси</string> + <string name="network_proxy_password">Пароль</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">Использовать разные учетные данные прокси для каждого соединения.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">Использовать разные учетные данные прокси для каждого профиля.</string> + <string name="network_proxy_username">Имя пользователя</string> + <string name="network_proxy_auth_mode_username_password">Ваши учетные данные могут быть отправлены в незашифрованном виде.</string> + <string name="migrate_from_device_remove_archive_question">Удалить архив?</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">Загруженный архив базы данных будет навсегда удален с серверов.</string> + <string name="operator_conditions_accepted">Принятые условия</string> + <string name="accept_conditions">Принять условия</string> + <string name="no_message_servers_configured">Нет серверов сообщений.</string> + <string name="no_message_servers_configured_for_receiving">Нет серверов для приема сообщений.</string> + <string name="errors_in_servers_configuration">Ошибки в настройках серверов.</string> + <string name="for_chat_profile">Для профиля %s:</string> + <string name="no_media_servers_configured">Нет серверов файлов и медиа.</string> + <string name="no_media_servers_configured_for_private_routing">Нет серверов для приема файлов.</string> + <string name="no_media_servers_configured_for_sending">Нет серверов для отправки файлов.</string> + <string name="connection_error_quota">Недоставленные сообщения</string> + <string name="address_creation_instruction">Нажмите Создать адрес SimpleX в меню, чтобы создать его позже.</string> + <string name="address_or_1_time_link">Адрес или одноразовая ссылка?</string> + <string name="connection_security">Безопасность соединения</string> + <string name="onboarding_choose_server_operators">Операторы серверов</string> + <string name="your_servers">Ваши серверы</string> + <string name="view_conditions">Посмотреть условия</string> + <string name="operator_review_conditions">Посмотреть условия</string> + <string name="operators_conditions_accepted_for"><![CDATA[Условия приняты для оператора(ов): <b>%s</b>.]]></string> + <string name="operator_conditions_accepted_for_enabled_operators_on">Условия будут автоматически приняты для включенных операторов: %s</string> + <string name="operator_conditions_accepted_on">Условия приняты: %s.</string> + <string name="operator_website">Вебсайт</string> + <string name="operator_conditions_accepted_for_some"><![CDATA[Условия уже приняты для следующих операторов: <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_applied"><![CDATA[Эти условия также будут применены к: <b>%s</b>.]]></string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[Чтобы использовать серверы оператора <b>%s</b>, примите условия использования.]]></string> + <string name="operator_use_for_sending">Для оправки</string> + <string name="operator_added_message_servers">Дополнительные серверы сообщений</string> + <string name="operator_use_for_files">Использовать для файлов</string> + <string name="operator_open_conditions">Открыть условия</string> + <string name="error_adding_server">Ошибка добавления сервера</string> + <string name="operator_server_alert_title">Сервер оператора</string> + <string name="server_added_to_operator__name">Сервер добавлен к оператору %s.</string> + <string name="appearance_app_toolbars">Тулбары приложения</string> + <string name="appearance_in_app_bars_alpha">Прозрачность</string> + <string name="v6_2_network_decentralization">Децентрализация сети</string> + <string name="v6_2_network_decentralization_descr">Второй оператор серверов в приложении!</string> + <string name="v6_2_network_decentralization_enable_flux">Включите Flux в настройках Сеть и серверы для лучшей конфиденциальности метаданных.</string> + <string name="v6_2_network_decentralization_enable_flux_reason">для лучшей конфиденциальности метаданных.</string> + <string name="v6_2_improved_chat_navigation">Улучшенная навигация в разговоре</string> + <string name="view_updated_conditions">Посмотреть измененные условия</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Устройства Xiaomi</b>: пожалуйста, включите опцию Autostart в системных настройках для работы нотификаций.]]></string> + <string name="message_deleted_or_not_received_error_title">Нет сообщения</string> + <string name="message_deleted_or_not_received_error_desc">Это сообщение было удалено или еще не получено.</string> + <string name="maximum_message_size_title">Сообщение слишком большое!</string> + <string name="maximum_message_size_reached_text">Пожалуйста, уменьшите размер сообщения и отправьте снова.</string> + <string name="maximum_message_size_reached_non_text">Пожалуйста, уменьшите размер сообщения или уберите медиа и отправьте снова.</string> + <string name="maximum_message_size_reached_forwarding">Чтобы отправить сообщение, скопируйте и уменьшите его размер.</string> + <string name="share_1_time_link_with_a_friend">Поделитесь одноразовой ссылкой с другом</string> + <string name="share_address_publicly">Поделитесь адресом</string> + <string name="share_simplex_address_on_social_media">Поделитесь SimpleX адресом в социальных сетях.</string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">Адрес SimpleX и одноразовые ссылки безопасно отправлять через любой мессенджер.</string> + <string name="you_can_set_connection_name_to_remember">Вы можете установить имя соединения, чтобы запомнить кому Вы отправили ссылку.</string> + <string name="smp_servers_new_server">Новый сервер</string> + <string name="create_1_time_link">Создать одноразовую ссылку</string> + <string name="for_social_media">Для социальных сетей</string> + <string name="or_to_share_privately">Или поделиться конфиденциально</string> + <string name="simplex_address_or_1_time_link">Адрес SimpleX или одноразовая ссылка?</string> + <string name="address_settings">Настройки адреса</string> + <string name="add_your_team_members_to_conversations">Добавьте сотрудников в разговор.</string> + <string name="business_address">Бизнес адрес</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[Все сообщения и файлы отправляются с <b>end-to-end шифрованием</b>, с пост-квантовой безопасностью в прямых разговорах.]]></string> + <string name="onboarding_notifications_mode_service_desc_short">Приложение всегда выполняется в фоне</string> + <string name="onboarding_notifications_mode_periodic_desc_short">Проверять сообщения каждые 10 минут</string> + <string name="onboarding_notifications_mode_off_desc_short">Без фонового сервиса</string> + <string name="onboarding_notifications_mode_battery">Нотификации и батарейка</string> + <string name="how_it_helps_privacy">Как это улучшает конфиденциальность</string> + <string name="onboarding_network_operators">Операторы серверов</string> + <string name="onboarding_select_network_operators_to_use">Выберите операторов сети.</string> + <string name="onboarding_network_operators_conditions_you_can_configure">Вы можете настроить операторов в настройках Сети и серверов.</string> + <string name="onboarding_network_operators_continue">Продолжить</string> + <string name="onboarding_network_operators_review_later">Посмотреть позже</string> + <string name="onboarding_network_operators_update">Обновить</string> + <string name="remote_hosts_section">Связанные мобильные устройства</string> + <string name="leave_chat_question">Покинуть разговор?</string> + <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Вы прекратите получать сообщения в этом разговоре. История будет сохранена.</string> + <string name="button_add_friends">Добавить друзей</string> + <string name="button_add_team_members">Добавить сотрудников</string> + <string name="button_delete_chat">Удалить разговор</string> + <string name="delete_chat_question">Удалить разговор?</string> + <string name="invite_to_chat_button">Пригласить в разговор</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">Разговор будет удален для всех участников - это действие нельзя отменить!</string> + <string name="operator">Оператор</string> + <string name="operator_servers_title">%s серверы</string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[Условия будут приняты для оператора(ов): <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_accepted_on">Условия будут приняты: %s</string> + <string name="operator_info_title">Оператор сети</string> + <string name="use_servers_of_operator_x">Использовать %s</string> + <string name="operator_use_operator_toggle_description">Использовать серверы</string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Условия будут приняты для оператора(ов): <b>%s</b>.]]></string> + <string name="operators_conditions_will_also_apply"><![CDATA[Эти условия также будут применены к: <b>%s</b>.]]></string> + <string name="chat_archive">Или импортировать файл архива</string> + <string name="chat_bottom_bar">Доступная панель чата</string> + <string name="delete_chat_for_self_cannot_undo_warning">Разговор будет удален для Вас - это действие нельзя отменить!</string> + <string name="button_leave_chat">Покинуть разговор</string> + <string name="only_chat_owners_can_change_prefs">Только владельцы разговора могут поменять предпочтения.</string> + <string name="operator_conditions_failed_to_load">Текст условий использования не может быть показан, вы можете посмотреть их через ссылку:</string> + <string name="info_row_chat">Разговор</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">Член будет удален из разговора - это действие нельзя отменить!</string> + <string name="network_preset_servers_title">Серверы по умолчанию</string> + <string name="member_role_will_be_changed_with_notification_chat">Роль будет изменена на %s. Все участники разговора получат уведомление.</string> + <string name="chat_main_profile_sent">Ваш профиль будет отправлен участникам разговора.</string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[Те же условия будут действовать для оператора <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Те же условия будут действовать для операторов: <b>%s</b>.]]></string> + <string name="operator_conditions_of_use">Условия использования</string> + <string name="operator_added_xftp_servers">Дополнительные серверы файлов и медиа</string> + <string name="error_updating_server_title">Ошибка сохранения сервера</string> + <string name="operator_use_for_messages_private_routing">Для доставки сообщений</string> + <string name="operator_open_changes">Открыть изменения</string> + <string name="error_server_operator_changed">Оператор сервера изменен.</string> + <string name="error_server_protocol_changed">Протокол сервера изменен.</string> + <string name="xftp_servers_per_user">Серверы для новых файлов Вашего текущего профиля</string> + <string name="operator_use_for_messages_receiving">Для получения</string> + <string name="operator_use_for_messages">Использовать для сообщений</string> + <string name="appearance_bars_blur_radius">Размыть</string> + <string name="direct_messages_are_prohibited">Прямые сообщения между членами запрещены.</string> + <string name="v6_2_business_chats">Бизнес разговоры</string> + <string name="v6_2_improved_chat_navigation_descr">- Открывает разговор на первом непрочитанном сообщении.\n- Перейти к цитируемому сообщению.</string> + <string name="v6_2_business_chats_descr">Конфиденциальность для ваших покупателей.</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Вы уже соединены с <b>%1$s</b>.]]></string> + <string name="connect_plan_chat_already_exists">Разговор уже существует!</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[Одноразовая ссылка может быть использована <i>только с одним контактом</i> - поделитесь при встрече или через любой мессенджер.]]></string> + <string name="no_message_servers_configured_for_private_routing">Нет серверов для доставки сообщений.</string> + <string name="onboarding_network_operators_configure_via_settings">Вы можете настроить серверы позже.</string> + <string name="onboarding_network_operators_simplex_flux_agreement">SimpleX Chat и Flux заключили соглашение добавить серверы под управлением Flux в приложение.</string> + <string name="onboarding_network_operators_app_will_use_different_operators">Приложение защищает вашу конфиденциальность, используя разные операторы в каждом разговоре.</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">Когда больше чем один оператор включен, ни один из них не видит метаданные, чтобы определить, кто соединен с кем.</string> + <string name="failed_to_save_servers">Ошибка сохранения серверов</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">Условия будут приняты для включенных операторов через 30 дней.</string> + <string name="error_accepting_operator_conditions">Ошибка приема условий</string> + <string name="connection_error_quota_desc">Соединение достигло предела недоставленных сообщений. Возможно, Ваш контакт не в сети.</string> + <string name="to_protect_against_your_link_replaced_compare_codes">Чтобы защитить Вашу ссылку от замены, Вы можете сравнить код безопасности.</string> + <string name="onboarding_network_operators_app_will_use_for_routing">Например, если ваш контакт получает сообщения через сервер SimpleX Chat, ваше приложение будет доставлять их через сервер Flux.</string> + <string name="direct_messages_are_prohibited_in_chat">Прямые сообщения между членами запрещены в этом разговоре.</string> + <string name="chat_list_groups">Группы</string> + <string name="delete_chat_list_menu_action">Удалить</string> + <string name="delete_chat_list_question">Удалить список?</string> + <string name="chat_list_all">Все</string> + <string name="change_order_chat_list_menu_action">Изменить порядок</string> + <string name="chat_list_favorites">Избранное</string> + <string name="display_name_requested_to_connect">запрошено соединение</string> + <string name="edit_chat_list_menu_action">Редактировать</string> + <string name="chat_list_businesses">Предприятия</string> + <string name="debug_logs">Включить журналы</string> + <string name="onboarding_network_about_operators">О операторах</string> + <string name="error_saving_database">Ошибка при сохранении базы данных</string> + <string name="connection_not_ready">Соединение не готово.</string> + <string name="error_updating_chat_tags">Ошибка обновления списка чата</string> + <string name="error_creating_chat_tags">Ошибка создания списка чатов</string> + <string name="list_menu">Список</string> + <string name="no_chats_in_list">Никаких чатов в списке %s.</string> + <string name="no_unread_chats">Без непрочитанных чатов</string> + <string name="no_chats">Никаких чатов</string> + <string name="no_chats_found">Чаты не найдены</string> + <string name="delete_chat_list_warning">Все чаты будут удалены из списка %s, а сам список удален</string> + <string name="chat_list_add_list">Добавить список</string> + <string name="chat_list_notes">Примечания</string> + <string name="open_with_app">Открыто с %s</string> + <string name="create_list">Создать список</string> + <string name="add_to_list">Добавить в список</string> + <string name="change_list">Изменить список</string> + <string name="save_list">Сохранить список</string> + <string name="list_name_field_placeholder">Имя списка...</string> + <string name="sync_connection_question">Исправить соединение?</string> + <string name="sync_connection_desc">Соединение требует повторного согласования шифрования.</string> + <string name="sync_connection_confirm">Исправление</string> + <string name="encryption_renegotiation_in_progress">Выполняется повторное согласование шифрования.</string> + <string name="display_name_accepted_invitation">принятое приглашение</string> + <string name="error_loading_chat_tags">Ошибка при загрузке списков чатов</string> + <string name="chat_list_contacts">Контакты</string> + <string name="duplicated_list_error">Название списка и эмодзи должны быть разными для всех списков.</string> + <string name="report_verb">Пожаловаться</string> + <string name="report_reason_spam">Спам</string> + <string name="report_compose_reason_header_spam">Пожаловаться на спам: увидят только модераторы группы.</string> + <string name="change_automatic_chat_deletion_message">Это действие не может быть отмененено - сообщения, отправленные и полученные в этом чате ранее чем выбранное, будут удалены</string> + <string name="v6_3_mentions_descr">Получайте уведомления от упоминаний.</string> + <string name="member_reports_are_prohibited">Сообщения о нарушениях запрещены в этой группе.</string> + <string name="report_compose_reason_header_community">Пожаловаться на нарушение: увидят только модераторы группы.</string> + <string name="text_field_set_chat_placeholder">Установить имя чата…</string> + <string name="v6_3_better_groups_performance">Улучшенная производительность групп</string> + <string name="v6_3_private_media_file_names">Приватные названия медиафайлов.</string> + <string name="blocking_reason_spam">Спам</string> + <string name="chat_list_group_reports">Сообщения о нарушениях</string> + <string name="unread_mentions">Непрочитанные упоминания</string> + <string name="privacy_chat_list_open_links_yes">Да</string> + <string name="v6_3_mentions">Упоминайте участников 👋</string> + <string name="v6_3_better_privacy_and_security">Улучшенная приватность и безопасность</string> + <string name="v6_3_faster_deletion_of_groups">Ускорено удаление групп.</string> + <string name="v6_3_faster_sending_messages">Ускорена отправка сообщений.</string> + <string name="v6_3_reports_descr">Помогайте администраторам модерировать их группы.</string> + <string name="v6_3_organize_chat_lists">Организуйте чаты в списки</string> + <string name="v6_3_reports">Вы можете сообщить о нарушениях</string> + <string name="v6_3_set_message_expiration_in_chats">Установите время исчезания сообщений в чатах.</string> + <string name="max_group_mentions_per_message_reached">Вы можете упомянуть до %1$s пользователей в одном сообщении!</string> + <string name="report_reason_alert_title">Причина сообщения?</string> + <string name="report_archive_alert_desc">Эта жалоба будет архивирована для вас.</string> + <string name="enable_sending_member_reports">Разрешить отправлять сообщения о нарушениях модераторам.</string> + <string name="blocking_reason_content">Содержание нарушает условия использования</string> + <string name="error_reading_passphrase">Ошибка чтения пароля базы данных</string> + <string name="report_item_archived_by">сообщение о нарушении заархивировано %s</string> + <string name="report_reason_community">Нарушение правил группы</string> + <string name="report_reason_illegal">Неприемлемое сообщение</string> + <string name="report_reason_other">Другая причина</string> + <string name="report_reason_profile">Неприемлемый профиль</string> + <string name="group_reports_active">%d сообщений о нарушениях</string> + <string name="error_creating_report">Ошибка создания сообщения о нарушении</string> + <string name="connection_error_blocked">Соединение заблокировано</string> + <string name="connection_error_blocked_desc">Соединение заблокировано сервером оператора:\n%1$s.</string> + <string name="privacy_chat_list_open_links_ask">Спросить</string> + <string name="disable_automatic_deletion_question">Отключить автоматическое удаление сообщений?</string> + <string name="chat_ttl_options_footer">Удалить сообщения с вашего устройства.</string> + <string name="disable_automatic_deletion">Отключить удаление сообщений</string> + <string name="chat_item_ttl_default">по умолчанию (%s)</string> + <string name="report_archive_alert_desc_all">Все сообщения о нарушениях будут заархивированы для вас.</string> + <string name="report_archive_alert_title_all">Архивировать все сообщения о нарушениях?</string> + <string name="report_archive_alert_title_nth">Архивировать %d сообщений о нарушениях?</string> + <string name="report_archive_for_me">Для меня</string> + <string name="archive_report">Архивировать сообщение о нарушении</string> + <string name="archive_reports">Архивировать сообщения о нарушениях</string> + <string name="delete_report">Удалить сообщение о нарушении</string> + <string name="file_error_blocked">Файл заблокирован оператором сервера:\n%1$s.</string> + <string name="report_archive_for_all_moderators">Для всех модераторов</string> + <string name="group_reports_active_one">1 сообщение о нарушении</string> + <string name="change_automatic_deletion_question">Измененить автоматическое удаление сообщений?</string> + <string name="chat_item_ttl_year">1 год</string> + <string name="v6_3_organize_chat_lists_descr">Не пропустите важные сообщения.</string> + <string name="prefs_error_saving_settings">Ошибка сохранения настроек</string> + <string name="report_item_archived">заархивированное сообщение о нарушении</string> + <string name="archive_verb">архивировать</string> + <string name="report_archive_alert_title">Архивировать сообщение о нарушении?</string> + <string name="restore_passphrase_can_not_be_read_desc">Пароль не может быть прочитан из Keystore. Это могло произойти после обновления системы, несовместимого с приложением. Если это не так, обратитесь к разработчикам.</string> + <string name="restore_passphrase_can_not_be_read_enter_manually_desc">Пароль не может быть прочитан из Keystore, пожалуйста, введите его. Это могло произойти после обновления системы, несовместимого с приложением. Если это не так, обратитесь к разработчикам.</string> + <string name="group_member_role_moderator">модератор</string> + <string name="group_member_status_pending_approval">ожидает утверждения</string> + <string name="group_member_status_pending_approval_short">ожидает</string> + <string name="operator_updated_conditions">Обновленные условия</string> + <string name="disable_sending_member_reports">Запретить жаловаться модераторам группы.</string> + <string name="group_members_can_send_reports">Члены группы могут пожаловаться модераторам.</string> + <string name="disable_automatic_deletion_message">Сообщения в этом чате никогда не будут удалены.</string> + <string name="privacy_chat_list_open_links">Открыть ссылку из списка чатов</string> + <string name="privacy_chat_list_open_web_link_question">Открыть веб-ссылку?</string> + <string name="report_compose_reason_header_profile">Пожаловаться на профиль: увидят только модераторы группы.</string> + <string name="group_reports_member_reports">Сообщения о нарушениях</string> + <string name="report_compose_reason_header_other">Пожаловаться: увидят только модераторы группы.</string> + <string name="mute_all_chat">Выключить уведомления для всех</string> + <string name="network_smp_web_port_footer">Использовать TCP-порт %1$s, когда порт не указан.</string> + <string name="network_smp_web_port_preset_footer">Использовать TCP-порт 443 только для серверов по умолчанию.</string> + <string name="network_smp_web_port_all">Все серверы</string> + <string name="network_smp_web_port_preset">Серверы по умолчанию</string> + <string name="network_smp_web_port_off">Нет</string> + <string name="network_smp_web_port_toggle">Использовать веб-порт</string> + <string name="privacy_chat_list_open_links_no">Нет</string> + <string name="report_compose_reason_header_illegal">Пожаловаться на сообщение: увидят только модераторы группы.</string> + <string name="group_preview_rejected">отклонён</string> + <string name="notification_group_report">Сообщение о нарушении: %s</string> + <string name="network_smp_web_port_section_title">TCP-порт для отправки сообщений</string> + <string name="privacy_chat_list_open_web_link">Открыть ссылку</string> + <string name="group_member_status_rejected">отклонён</string> + <string name="report_item_visibility_moderators">Только отправитель и модераторы видят это</string> + <string name="report_item_visibility_submitter">Только вы и модераторы видят это</string> + <string name="unblock_members_for_all_question">Разблокировать членов для всех?</string> + <string name="unblock_members_desc">Сообщения от этих членов группы будут показаны!</string> + <string name="block_members_desc">Все новые сообщения от этих членов группы будут скрыты!</string> + <string name="block_members_for_all_question">Заблокировать членов для всех?</string> + <string name="members_will_be_removed_from_group_cannot_be_undone">Члены группы будут удалены - это действие нельзя отменить!</string> + <string name="members_will_be_removed_from_chat_cannot_be_undone">Участники будут удалены из разговора - это действие нельзя отменить!</string> + <string name="feature_roles_moderators">модераторы</string> + <string name="button_remove_members_question">Удалить членов группы?</string> + <string name="onboarding_conditions_accept">Принять</string> + <string name="onboarding_conditions_by_using_you_agree">Используя SimpleX Chat, Вы согласны:\n- отправлять только законные сообщения в публичных группах.\n- уважать других пользователей – не отправлять спам.</string> + <string name="onboarding_conditions_private_chats_not_accessible">Частные разговоры, группы и Ваши контакты недоступны для операторов серверов.</string> + <string name="onboarding_conditions_configure_server_operators">Настроить операторов серверов</string> + <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Политика конфиденциальности и условия использования.</string> +</resources> 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 91330717c4..b4d854c3d1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml @@ -46,7 +46,7 @@ <string name="icon_descr_asked_to_receive">ขอรับภาพ</string> <string name="icon_descr_video_asked_to_receive">ขอรับวิดีโอ</string> <string name="clear_chat_warning">ข้อความทั้งหมดจะถูกลบ - การดำเนินการนี้ไม่สามารถยกเลิกได้! ข้อความจะถูกลบสำหรับคุณเท่านั้น</string> - <string name="smp_servers_add">เพิ่มเซิร์ฟเวอร์…</string> + <string name="smp_servers_add">เพิ่มเซิร์ฟเวอร์</string> <string name="app_version_title">เวอร์ชันแอป</string> <string name="app_version_name">เวอร์ชันแอป: v%s</string> <string name="all_your_contacts_will_remain_connected">ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่.</string> @@ -112,8 +112,6 @@ <string name="both_you_and_your_contact_can_add_message_reactions">ทั้งคุณและผู้ติดต่อของคุณสามารถเพิ่มปฏิกิริยาต่อข้อความได้</string> <string name="both_you_and_your_contacts_can_delete">ทั้งคุณและผู้ติดต่อของคุณสามารถลบข้อความที่ส่งแล้วอย่างถาวรได้</string> <string name="use_camera_button">กล้อง</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b> เพิ่มผู้ติดต่อใหม่ </b>: เพื่อสร้างรหัส QR แบบใช้ครั้งเดียวสําหรับผู้ติดต่อของคุณ]]></string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>สแกนรหัส QR</b>: เพื่อเชื่อมต่อกับผู้ติดต่อที่แสดงรหัส QR ให้คุณ]]></string> <string name="learn_more_about_address">เกี่ยวกับที่อยู่ SimpleX</string> <string name="bold_text">ตัวหนา</string> <string name="callstatus_calling">กำลังโทร…</string> @@ -144,7 +142,6 @@ <string name="change_member_role_question">เปลี่ยนบทบาทกลุ่ม\?</string> <string name="icon_descr_cancel_live_message">ยกเลิกข้อความสด</string> <string name="feature_cancelled_item">ยกเลิกเรียบร้อยแล้ว %s</string> - <string name="cant_delete_user_profile">ไม่สามารถลบโปรไฟล์ผู้ใช้ได้!</string> <string name="alert_title_cant_invite_contacts">ไม่สามารถเชิญผู้ติดต่อได้!</string> <string name="change_verb">เปลี่ยน</string> <string name="change_database_passphrase_question">เปลี่ยนรหัสผ่านฐานข้อมูล\?</string> @@ -163,7 +160,6 @@ <string name="connection_timeout">หมดเวลาการเชื่อมต่อ</string> <string name="connect_via_group_link">เชื่อมต่อผ่านลิงค์กลุ่ม\?</string> <string name="delete_contact_all_messages_deleted_cannot_undo_warning">ผู้ติดต่อและข้อความทั้งหมดจะถูกลบ - ไม่สามารถยกเลิกได้!</string> - <string name="archive_created_on_ts">สร้างเมื่อ %1$s</string> <string name="maximum_supported_file_size">ขนาดไฟล์สูงสุดที่รองรับในปัจจุบันคือ %1$s</string> <string name="v5_1_custom_themes">ธีมที่กำหนดเอง</string> <string name="developer_options">ID ฐานข้อมูลและตัวเลือกการแยกการส่งผ่าน</string> @@ -205,7 +201,6 @@ <string name="icon_descr_close_button">ปุ่มปิด</string> <string name="connect_button">เชื่อมต่อ</string> <string name="connect_via_link">เชื่อมต่อผ่านลิงก์</string> - <string name="create_one_time_link">สร้างลิงก์เชิญแบบใช้ครั้งเดียว</string> <string name="clear_verification">ล้างการยืนยัน</string> <string name="chat_console">คอนโซลแชท</string> <string name="smp_servers_check_address">ตรวจสอบที่อยู่เซิร์ฟเวอร์แล้วลองอีกครั้ง</string> @@ -243,8 +238,6 @@ <string name="database_error">ความผิดพลาดในฐานข้อมูล</string> <string name="confirm_database_upgrades">ยืนยันการอัพเกรดฐานข้อมูล</string> <string name="database_downgrade">ดาวน์เกรดฐานข้อมูล</string> - <string name="chat_archive_header">ที่เก็บแชทถาวร</string> - <string name="chat_archive_section">ที่เก็บแชทถาวร</string> <string name="chat_is_stopped_indication">การแชทหยุดทํางานแล้ว</string> <string name="rcv_group_event_member_connected">เชื่อมต่อสำเร็จ</string> <string name="rcv_conn_event_switch_queue_phase_changing">กำลังเปลี่ยนที่อยู่…</string> @@ -436,8 +429,6 @@ <string name="mtr_error_no_down_migration">เวอร์ชันฐานข้อมูลใหม่กว่าแอป แต่ไม่มีการย้ายข้อมูลลงสำหรับ: %s</string> <string name="mtr_error_different">การย้ายข้อมูลที่แตกต่างกันในแอป/ฐานข้อมูล: %s / %s</string> <string name="downgrade_and_open_chat">ปรับลดรุ่นและเปิดแชท</string> - <string name="delete_archive">ลบที่เก็บถาวร</string> - <string name="delete_chat_archive_question">ลบที่เก็บแชทถาวร\?</string> <string name="icon_descr_group_inactive">กลุ่มที่ไม่ได้ใช้งาน</string> <string name="alert_title_no_group">ไม่พบกลุ่ม!</string> <string name="group_invitation_expired">คำเชิญเข้าร่วมกลุ่มหมดอายุแล้ว</string> @@ -489,7 +480,7 @@ <string name="group_members_can_send_disappearing">สมาชิกกลุ่มสามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้</string> <string name="group_members_can_send_dms">สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้</string> <string name="group_members_can_send_voice">สมาชิกกลุ่มสามารถส่งข้อความเสียง</string> - <string name="direct_messages_are_prohibited_in_chat">ข้อความโดยตรงระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้</string> + <string name="direct_messages_are_prohibited_in_group">ข้อความโดยตรงระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้</string> <string name="disappearing_messages_are_prohibited">ข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) เป็นสิ่งต้องห้ามในกลุ่มนี้</string> <string name="group_members_can_add_message_reactions">สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้</string> <string name="delete_after">ลบหลังจาก</string> @@ -669,10 +660,7 @@ <string name="network_settings_title">การตั้งค่าเครือข่าย</string> <string name="network_use_onion_hosts_required_desc">จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ</string> <string name="network_use_onion_hosts_no">ไม่</string> - <string name="network_use_onion_hosts_required_desc_in_alert">จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">โฮสต์หัวหอมจะถูกใช้เมื่อมี</string> <string name="network_use_onion_hosts_no_desc">โฮสต์หัวหอมจะไม่ถูกใช้</string> - <string name="network_use_onion_hosts_no_desc_in_alert">โฮสต์หัวหอมจะไม่ถูกใช้</string> <string name="password_to_show">รหัสผ่านที่จะแสดง</string> <string name="callstatus_missed">สายที่ไม่ได้รับ</string> <string name="people_can_connect_only_via_links_you_share">ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น</string> @@ -749,7 +737,7 @@ <string name="v4_6_reduced_battery_usage_descr">การปรับปรุงเพิ่มเติมกำลังจะมาเร็ว ๆ นี้!</string> <string name="v4_6_group_moderation_descr">ขณะนี้ผู้ดูแลระบบสามารถ: \n- ลบข้อความของสมาชิก -\n- ปิดการใช้งานสมาชิก (บทบาท \"ผู้สังเกตการณ์\")</string> +\n- ปิดการใช้งานสมาชิก (บทบาท "ผู้สังเกตการณ์")</string> <string name="v5_1_message_reactions">ปฏิกิริยาต่อข้อความ</string> <string name="custom_time_unit_minutes">นาที</string> <string name="custom_time_unit_months">เดือน</string> @@ -758,7 +746,6 @@ <string name="message_delivery_error_desc">เป็นไปได้มากว่าผู้ติดต่อนี้ได้ลบการเชื่อมต่อกับคุณ</string> <string name="message_delivery_error_title">ข้อผิดพลาดในการส่งข้อความ</string> <string name="ensure_smp_server_address_are_correct_format_and_unique">ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ SMP อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[หลายคนถามว่า: <i>ถ้า SimpleX ไม่มีตัวระบุผู้ใช้ จะส่งข้อความได้อย่างไร\?</i>]]></string> <string name="network_use_onion_hosts_prefer_desc">โฮสต์หัวหอมจะถูกใช้เมื่อมี</string> <string name="only_your_contact_can_send_voice">ผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความเสียงได้</string> <string name="simplex_link_mode_browser_warning">การเปิดลิงก์ในเบราว์เซอร์อาจลดความเป็นส่วนตัวและความปลอดภัยของการเชื่อมต่อ ลิงก์ SimpleX ที่ไม่น่าเชื่อถือจะเป็นสีแดง</string> @@ -780,7 +767,6 @@ <string name="callstate_received_confirmation">ได้รับการยืนยัน…</string> <string name="privacy_redefined">นิยามความเป็นส่วนตัวใหม่</string> <string name="read_more_in_github_with_link"><![CDATA[อ่านเพิ่มเติมใน <font color="#0088ff"> GitHub repository </font> ของเรา]]></string> - <string name="read_more_in_github">อ่านเพิ่มเติมใน GitHub repository ของเรา</string> <string name="onboarding_notifications_mode_title">การแจ้งเตือนส่วนตัว</string> <string name="alert_text_fragment_please_report_to_developers">โปรดรายงานไปยังผู้พัฒนาแอป</string> <string name="privacy_and_security">ความเป็นส่วนตัวและความปลอดภัย</string> @@ -844,7 +830,6 @@ <string name="restore_database_alert_confirm">คืนค่า</string> <string name="restore_database_alert_title">คืนค่าฐานข้อมูลสำรองไหม\?</string> <string name="database_restore_error">กู้คืนข้อผิดพลาดของฐานข้อมูล</string> - <string name="save_archive">บันทึกไฟล์เก็บถาวร</string> <string name="rcv_group_event_member_deleted">ลบแล้ว %1$s</string> <string name="rcv_group_event_user_deleted">ลบคุณออกแล้ว</string> <string name="group_member_status_removed">ถูกลบแล้ว</string> @@ -856,10 +841,8 @@ <string name="save_and_update_group_profile">บันทึกและอัปเดตโปรไฟล์กลุ่ม</string> <string name="receiving_via">กำลังรับผ่าน</string> <string name="network_options_reset_to_defaults">รีเซ็ตเป็นค่าเริ่มต้น</string> - <string name="network_options_revert">เปลี่ยนกลับ</string> <string name="network_options_save">บันทึก</string> <string name="reset_color">รีเซ็ตสี</string> - <string name="save_color">บันทึกสี</string> <string name="feature_received_prohibited">ได้รับ, ห้าม</string> <string name="v4_4_live_messages_desc">ผู้รับจะเห็นการอัปเดตเมื่อคุณพิมพ์</string> <string name="v4_5_reduced_battery_usage">ลดการใช้แบตเตอรี่</string> @@ -954,7 +937,6 @@ <string name="star_on_github">ติดดาวบน GitHub</string> <string name="switch_verb">เปลี่ยน</string> <string name="callstate_starting">กำลังเริ่มต้น…</string> - <string name="stop_chat_to_enable_database_actions">หยุดการแชทเพื่อเปิดใช้งานการดำเนินการกับฐานข้อมูล</string> <string name="stop_chat_to_export_import_or_delete_chat_database">หยุดแชทเพื่อส่งออก นำเข้า หรือลบฐานข้อมูลแชท คุณจะไม่สามารถรับและส่งข้อความได้ในขณะที่การแชทหยุดลง</string> <string name="v4_6_audio_video_calls_descr">รองรับบลูทูธและการปรับปรุงอื่นๆ</string> <string name="stop_sharing_address">หยุดแชร์ที่อยู่ไหม\?</string> @@ -1029,7 +1011,7 @@ <string name="v5_0_polish_interface_descr">ขอบคุณผู้ใช้ – มีส่วนร่วมผ่าน Weblate!</string> <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">แพลตฟอร์มการส่งข้อความและแอปพลิเคชันที่ปกป้องความเป็นส่วนตัวและความปลอดภัยของคุณ</string> <string name="next_generation_of_private_messaging">การส่งข้อความส่วนตัวรุ่นต่อไป</string> - <string name="member_role_will_be_changed_with_notification">บทบาทจะถูกเปลี่ยนเป็น \"%s\" ทุกคนในกลุ่มจะได้รับแจ้ง</string> + <string name="member_role_will_be_changed_with_notification">บทบาทจะถูกเปลี่ยนเป็น "%s" ทุกคนในกลุ่มจะได้รับแจ้ง</string> <string name="delete_files_and_media_desc">การดำเนินการนี้ไม่สามารถยกเลิกได้ ไฟล์และสื่อที่ได้รับและส่งทั้งหมดจะถูกลบ รูปภาพความละเอียดต่ำจะยังคงอยู่</string> <string name="to_reveal_profile_enter_password">หากต้องการเปิดเผยโปรไฟล์ที่ซ่อนอยู่ของคุณ ให้ป้อนรหัสผ่านแบบเต็มในช่องค้นหาในหน้าโปรไฟล์แชทของคุณ</string> <string name="network_session_mode_transport_isolation">การแยกการขนส่ง</string> @@ -1152,7 +1134,6 @@ <string name="network_socks_toggle_use_socks_proxy">ใช้พร็อกซี SOCKS</string> <string name="network_disable_socks">ใช้การเชื่อมต่ออินเทอร์เน็ตโดยตรงหรือไม่\?</string> <string name="network_enable_socks">ใช้พร็อกซี SOCKS หรือไม่\?</string> - <string name="update_onion_hosts_settings_question">อัปเดตการตั้งค่าโฮสต์ .onion ไหม\?</string> <string name="network_use_onion_hosts">ใช้โฮสต์ .onion</string> <string name="network_use_onion_hosts_prefer">เมื่อพร้อมใช้งาน</string> <string name="update_network_session_mode_question">อัปเดตโหมดการแยกการขนส่งไหม\?</string> @@ -1165,7 +1146,6 @@ <string name="your_profile_is_stored_on_your_device">โปรไฟล์ รายชื่อผู้ติดต่อ และข้อความที่ส่งของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณ</string> <string name="you_can_use_markdown_to_format_messages__prompt">คุณสามารถใช้มาร์กดาวน์เพื่อจัดรูปแบบข้อความ:</string> <string name="callstate_waiting_for_answer">รอคำตอบ…</string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[คุณควบคุมผ่านเซิร์ฟเวอร์ที่จะ<b>รับ</b>ข้อความและผู้ติดต่อของคุณ – เซิร์ฟเวอร์ที่คุณใช้เพื่อส่งข้อความถึงพวกเขา]]></string> <string name="use_chat">ใช้แชท</string> <string name="icon_descr_video_call">การสนทนาทางวิดีโอ</string> <string name="your_calls">การโทรของคุณ</string> @@ -1208,7 +1188,7 @@ <string name="button_welcome_message">ข้อความต้อนรับ</string> <string name="you_can_share_group_link_anybody_will_be_able_to_connect">คุณสามารถแชร์ลิงก์หรือคิวอาร์โค้ดได้ ทุกคนจะสามารถเข้าร่วมกลุ่มได้ คุณจะไม่สูญเสียสมาชิกของกลุ่มหากคุณลบในภายหลัง</string> <string name="you_can_share_this_address_with_your_contacts">คุณสามารถแบ่งปันที่อยู่นี้กับผู้ติดต่อของคุณเพื่อให้พวกเขาเชื่อมต่อกับ %s</string> - <string name="member_role_will_be_changed_with_invitation">บทบาทจะถูกเปลี่ยนเป็น \"%s\" สมาชิกจะได้รับคำเชิญใหม่</string> + <string name="member_role_will_be_changed_with_invitation">บทบาทจะถูกเปลี่ยนเป็น "%s" สมาชิกจะได้รับคำเชิญใหม่</string> <string name="group_welcome_title">ข้อความต้อนรับ</string> <string name="group_main_profile_sent">โปรไฟล์การแชทของคุณจะถูกส่งไปยังสมาชิกในกลุ่ม</string> <string name="update_network_settings_confirmation">อัปเดต</string> @@ -1216,8 +1196,6 @@ <string name="updating_settings_will_reconnect_client_to_all_servers">การอัปเดตการตั้งค่าจะเชื่อมต่อไคลเอนต์กับเซิร์ฟเวอร์ทั้งหมดอีกครั้ง</string> <string name="user_unhide">ยกเลิกการซ่อน</string> <string name="user_unmute">เปิดเสียง</string> - <string name="should_be_at_least_one_profile">ควรมีโปรไฟล์ผู้ใช้อย่างน้อยหนึ่งโปรไฟล์</string> - <string name="should_be_at_least_one_visible_profile">ควรมีอย่างน้อยหนึ่งโปรไฟล์ผู้ใช้ที่มองเห็นได้</string> <string name="you_can_hide_or_mute_user_profile">คุณสามารถซ่อนหรือปิดเสียงโปรไฟล์ผู้ใช้ - กดค้างไว้เพื่อที่จะแสดงเมนู</string> <string name="unhide_profile">เลิกซ่อนโปรไฟล์</string> <string name="chat_preferences_you_allow">คุณอนุญาต</string> @@ -1350,4 +1328,4 @@ <string name="in_reply_to">ในการตอบกลับถึง</string> <string name="no_history">ไม่มีประวัติ</string> <string name="conn_event_ratchet_sync_ok">encryptionใช้ได้</string> -</resources> \ No newline at end of file +</resources> 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 ff9febd088..257daec596 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -3,8 +3,7 @@ <string name="notifications">Bildirimler</string> <string name="connect_via_link_or_qr">Bağlantı ya da karekod ile bağlan</string> <string name="create_group">Gizli grup oluştur</string> - <string name="create_one_time_link">Tek seferlik davet bağlantısı oluştur</string> - <string name="your_simplex_contact_address">SimpleX addresin</string> + <string name="your_simplex_contact_address">SimpleX adresin</string> <string name="callstatus_missed">cevapsız çağrı</string> <string name="incoming_video_call">Gelen görüntülü arama</string> <string name="accept">Onayla</string> @@ -14,32 +13,32 @@ <string name="settings_audio_video_calls">Sesli ve görüntülü aramalar</string> <string name="icon_descr_video_call">Görüntülü ara</string> <string name="privacy_and_security">Gizlilik ve güvenlik</string> - <string name="incognito">Gizli ol</string> + <string name="incognito">Gizli</string> <string name="your_chats">Konuşmalar</string> <string name="connect_via_link">Bağlantı ile bağlan</string> - <string name="your_chat_profiles">Konuşma profillerin</string> + <string name="your_chat_profiles">Sohbet profillerin</string> <string name="chat_preferences">Konuşma tercihleri</string> <string name="network_and_servers">Ağ ve sunucular</string> <string name="appearance_settings">Görünüm</string> - <string name="your_settings">Ayarların</string> + <string name="your_settings">Ayarlarınız</string> <string name="mute_chat">Sessize al</string> - <string name="unmute_chat">Sessizden çıkar</string> + <string name="unmute_chat">Susturmayı kaldır</string> <string name="abort_switch_receiving_address_confirm">İptal</string> <string name="abort_switch_receiving_address_question">Adres değişikliğini iptal et\?</string> <string name="send_disappearing_message_30_seconds">30 saniye</string> <string name="send_disappearing_message_5_minutes">5 dakika</string> <string name="accept_contact_button">Onayla</string> - <string name="accept_connection_request__question">Bağlantı isteğini kabul et\?</string> - <string name="learn_more_about_address">SimpleX Adresiyle İlgili</string> - <string name="about_simplex_chat">SimpleX Chat İle İlgili</string> - <string name="about_simplex">SimpleX İle İlgili</string> + <string name="accept_connection_request__question">Bağlantı isteği kabul edilsin mi?</string> + <string name="learn_more_about_address">SimpleX Adresi Hakkında</string> + <string name="about_simplex_chat">SimpleX Chat Hakkında</string> + <string name="about_simplex">SimpleX Hakkında</string> <string name="accept_call_on_lock_screen">Yanıtla</string> <string name="abort_switch_receiving_address">Adres değişikliğini iptal et</string> <string name="notifications_mode_service">Sürekli açık</string> <string name="abort_switch_receiving_address_desc">Adres değişikliği iptal edilecek. Eski alıcı adresi kullanılacaktır.</string> <string name="send_disappearing_message_1_minute">1 dakika</string> <string name="clear_chat_warning">Tüm mesajlar silinecektir. Bu, geri alınamaz! Mesajlar, YALNIZCA senin için silinecektir.</string> - <string name="smp_servers_add">Sunucu ekle…</string> + <string name="smp_servers_add">Sunucu ekle</string> <string name="database_passphrase_and_export">Veri tabanı ayarları</string> <string name="one_time_link_short">tek kullanımlık bağlantı</string> <string name="network_settings">Gelişmiş ağ ayarları</string> @@ -48,7 +47,7 @@ <string name="app_version_name">Uygulama sürümü: v%s</string> <string name="icon_descr_audio_call">sesli arama</string> <string name="audio_call_no_encryption">sesli arama (uçtan uca şifreli değil)</string> - <string name="answer_call">Aramaya cevap ver</string> + <string name="answer_call">Aramayı cevapla</string> <string name="full_backup">Uygulama veri yedekleme</string> <string name="all_app_data_will_be_cleared">Tüm uygulama verileri silinir.</string> <string name="settings_section_title_app">UYGULAMA</string> @@ -59,21 +58,20 @@ <string name="button_add_welcome_message">Karşılama mesajı ekleyin</string> <string name="users_delete_all_chats_deleted">Tüm konuşmalar ve mesajlar silinecektir. Bu, geri alınamaz!</string> <string name="color_primary">Ana renk</string> - <string name="chat_preferences_always">sürekli</string> + <string name="chat_preferences_always">her zaman</string> <string name="allow_message_reactions">Mesaj tepkilerine izin ver.</string> <string name="v4_3_improved_server_configuration_desc">Karekodu okutarak sunucular ekle.</string> <string name="v4_6_audio_video_calls">Sesli ve görüntülü aramalar</string> <string name="chat_item_ttl_day">1 gün</string> <string name="chat_item_ttl_month">1 ay</string> - <string name="add_address_to_your_profile">Profilinize adres ekleyin, böylece kişileriniz bunu diğer insanlarla paylaşabilir. Profil güncellemesi kişilerinize gönderilecektir.</string> + <string name="add_address_to_your_profile">Profilinize adres ekleyin, böylece kişileriniz adresinizi diğer insanlarla paylaşabilir. Profil güncellemesi kişilerinize gönderilecektir.</string> <string name="address_section_title">Adres</string> <string name="v4_2_group_links_desc">Yöneticiler, gruplara katılım bağlantısı oluşturabilirler.</string> <string name="all_group_members_will_remain_connected">Konuşma üyelerinin tümü bağlı kalacaktır.</string> <string name="allow_verb">İzin ver</string> <string name="v5_0_app_passcode">Uygulama erişim kodu</string> <string name="network_session_mode_user_description"><![CDATA[<b>Uygulamadaki her konuşma profliniz için</b> ayrı bir TCP bağlantısı (ve SOCKS kimliği) kullanılacaktır.]]></string> - <string name="network_session_mode_entity_description"><b>Konuştuğun kişilerin ve grup üyelerinin tamamı için</b> ayrı bir TCP bağlantısı (ve SOCKS kimliği) kullanılacaktır. -\n<b>Bilgin olsun</b>: Çok sayıda bağlantın varsa pilin ve veri kullanımın önemli ölçüde artabilir ve bazı bağlantılar başarısız olabilir.</string> + <string name="network_session_mode_entity_description"><![CDATA[<b>Konuştuğun kişilerin ve grup üyelerinin tamamı için</b> ayrı bir TCP bağlantısı (ve SOCKS kimliği) kullanılacaktır.\n<b>Bilgin olsun</b>: Çok sayıda bağlantın varsa pilin ve veri kullanımın önemli ölçüde artabilir ve bazı bağlantılar başarısız olabilir.]]></string> <string name="save_and_notify_group_members">Kaydet ve grup üyelerini bilgilendir</string> <string name="notifications_mode_off">Uygulama açıkken çalışır</string> <string name="search_verb">Ara</string> @@ -88,14 +86,14 @@ <string name="scan_code_from_contacts_app">Konuştuğunuz kişinin uygulamasından güvenlik kodunu okut.</string> <string name="ensure_ICE_server_address_are_correct_format_and_unique">WebRTC ICE sunucu adreslerinin doğru formatta olduğundan emin olun: Satırlara ayrılmış ve yinelenmemiş şekilde.</string> <string name="save_servers_button">Kaydet</string> - <string name="theme_colors_section_title">TEMA RENKLERİ</string> + <string name="theme_colors_section_title">ARAYÜZ RENKLERİ</string> <string name="save_auto_accept_settings">Otomatik-kabul ayarlarını kaydet</string> - <string name="save_settings_question">Ayarları kaydet\?</string> + <string name="save_settings_question">Ayarlar kaydedilsin mi?</string> <string name="save_and_notify_contacts">Kaydet ve konuştuğun kişilere bildir</string> <string name="save_preferences_question">Tercihleri kaydet\?</string> <string name="save_profile_password">Profil parolasını kaydet</string> <string name="profile_is_only_shared_with_your_contacts">Profil sadece konuştuğun kişilerle paylaşılır.</string> - <string name="next_generation_of_private_messaging">Gizli iletişimin gelecek kuşağı</string> + <string name="next_generation_of_private_messaging">Mesajlaşmanın geleceği</string> <string name="icon_descr_audio_off">Ses kapalı</string> <string name="authentication_cancelled">Doğrulama iptal edildi</string> <string name="settings_restart_app">Yeniden başlat</string> @@ -106,18 +104,15 @@ <string name="restore_database">Veri tabanı yedeğini geri yükle</string> <string name="restore_database_alert_title">Veri tabanı yedeğini geri yükle\?</string> <string name="database_restore_error">Veri tabanını geri yüklerken hata</string> - <string name="database_downgrade">Veri tabanının yapısal gerilemesi</string> - <string name="save_archive">Arşivi kaydet</string> + <string name="database_downgrade">Veritabanı sürüm düşürme</string> <string name="current_version_timestamp">%s (mevcut)</string> <string name="save_and_update_group_profile">Kaydet ve grup profilini güncelle</string> - <string name="save_welcome_message_question">Karşılama mesajını kaydet\?</string> + <string name="save_welcome_message_question">Karşılama mesajı kaydedilsin mi?</string> <string name="network_options_reset_to_defaults">Varsayılana sıfırla</string> <string name="save_group_profile">Grup profilini kaydet</string> <string name="network_option_seconds_label">sn</string> <string name="network_options_save">Kaydet</string> - <string name="should_be_at_least_one_visible_profile">There should be at least one visible user profile.</string> - <string name="should_be_at_least_one_profile">En az bir kullanıcı profili olmalıdır.</string> - <string name="incognito_info_protects">Gizlilik kipi, ana profil adının ve görselinin gizliliğini korur. Tüm yeni kişiler için rastgele yeni bir profil oluşturulur.</string> + <string name="incognito_info_protects">Gizli mod her farklı kişi için yeni rasgele profil kullanarak gizliliğini korur.</string> <string name="theme_system">Sistem</string> <string name="language_system">Sistem</string> <string name="theme_light">Açık</string> @@ -125,7 +120,6 @@ <string name="theme_simplex">SimpleX</string> <string name="dark_theme">Koyu tema</string> <string name="theme">Tema</string> - <string name="save_color">Rengi kaydet</string> <string name="import_theme">Temayı içe aktar</string> <string name="import_theme_error">Temayı içe aktarırken hata oluştu</string> <string name="import_theme_error_desc">Dosyanın doğru YAML sözdizimine sahip olduğundan emin olun. Tema dosyası yapısının bir örneğine sahip olmak için temayı dışa aktarın.</string> @@ -137,63 +131,63 @@ <string name="custom_time_unit_seconds">saniye</string> <string name="save_verb">Kaydet</string> <string name="saved_ICE_servers_will_be_removed">Kaydedilen WebRTC ICE sunucuları kaldırılacaktır.</string> - <string name="smp_save_servers_question">Sunucuları kaydet\?</string> + <string name="smp_save_servers_question">Sunucular kaydedilsin mi?</string> <string name="save_passphrase_and_open_chat">Parolayı kaydet ve konuşmayı aç</string> <string name="save_and_notify_contact">Kaydet ve kişiyi bilgilendir</string> <string name="save_passphrase_in_keychain">Parolayı, Keystore\'a kaydet.</string> <string name="icon_descr_audio_on">Ses açık</string> - <string name="member_role_will_be_changed_with_notification">Yetki, \"%s\" olarak değiştirelecek. Gruptaki herkes bilgilendirilecek.</string> + <string name="member_role_will_be_changed_with_notification">Yetki, "%s" olarak değiştirelecek. Gruptaki herkes bilgilendirilecek.</string> <string name="calls_prohibited_with_this_contact">Sesli/görüntülü aramalar yasaktır.</string> <string name="la_auth_failed">Kimlik doğrulama başarısız</string> - <string name="icon_descr_address">SimpleX adresi</string> + <string name="icon_descr_address">SimpleX Adresi</string> <string name="moderate_message_will_be_deleted_warning">Mesaj, tüm üyeler için silinecek.</string> <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Gizliliğinizi ve güvenliğinizi koruyan mesajlaşma ve uygulama platformu.</string> <string name="settings_section_title_incognito">Gizlilik kipi</string> - <string name="member_role_will_be_changed_with_invitation">Yetki, \"%s\" olarak değiştirilecek. Üye, yeni bir davet alacak.</string> + <string name="member_role_will_be_changed_with_invitation">Yetki, "%s" olarak değiştirilecek. Üye, yeni bir davet alacak.</string> <string name="role_in_group">Yetki</string> - <string name="allow_voice_messages_question">Sesli mesajlara izin ver\?</string> + <string name="allow_voice_messages_question">Sesli mesajlara izin verilsin mi?</string> <string name="users_add">Profil ekle</string> - <string name="allow_direct_messages">Üyelere direkt mesaj gönderilmesine izin ver.</string> + <string name="allow_direct_messages">Üyelere doğrudan mesaj gönderilmesine izin ver.</string> <string name="allow_to_send_disappearing">Kendiliğinden yok olan mesajlar göndermeye izin ver.</string> - <string name="allow_to_delete_messages">Gönderilen mesajların kalıcı olarak silinmesine izin ver.</string> + <string name="allow_to_delete_messages">Gönderilen mesajların kalıcı olarak silinmesine izin ver. (24 saat içinde)</string> <string name="allow_to_send_files">Dosya ve medya göndermeye izin ver.</string> <string name="allow_to_send_voice">Sesli mesaj göndermeye izin ver.</string> <string name="share_invitation_link">Tek kullanımlık bağlantıyı paylaş</string> - <string name="connect_via_invitation_link">Davet bağlantısı ile bağlan\?</string> - <string name="connect_via_group_link">Grup bağlantısı ile bağlan\?</string> - <string name="you_will_join_group">Bu bağlantının yönlendirdiği bir gruba katılacak ve üyeleriyle bağlantı kuracaksın.</string> + <string name="connect_via_invitation_link">Tek kullanımlık bağlantıyla bağlanılsın mı?</string> + <string name="connect_via_group_link">Gruba bağlan?</string> + <string name="you_will_join_group">Bütün grup üyelerine bağlanacaksın.</string> <string name="opening_database">Veri tabanı açılıyor…</string> <string name="server_connecting">bağlanılıyor</string> - <string name="moderated_item_description">%s yönetiyor</string> + <string name="moderated_item_description">%s tarafından yönetiliyor</string> <string name="sending_files_not_yet_supported">dosya gönderme henüz desteklenmiyor</string> <string name="unknown_message_format">bilinmeyen mesaj biçimi</string> <string name="invalid_message_format">geçersiz mesaj biçimi</string> <string name="live">CANLI</string> <string name="invalid_data">geçersiz veri</string> <string name="display_name_connection_established">bağlantı kuruldu</string> - <string name="display_name_invited_to_connect">bağlanmaya davet etti</string> + <string name="display_name_invited_to_connect">bağlanmaya davet edildi</string> <string name="display_name_connecting">bağlanılıyor…</string> <string name="description_you_shared_one_time_link">tek seferlik bağlantı paylaştınız</string> <string name="description_via_group_link">grup bağlantısı ile</string> - <string name="description_via_one_time_link">tek seferlik bağlantı ile</string> + <string name="description_via_one_time_link">tek seferlik bağlantı aracıığıyla</string> <string name="simplex_link_invitation">SimpleX tek kullanımlık bağlantı</string> <string name="simplex_link_group">SimpleX grup bağlantısı</string> <string name="simplex_link_mode">SimpleX bağlantıları</string> <string name="simplex_link_mode_description">Açıklama</string> - <string name="simplex_link_mode_full">Bütün olan bağlantı</string> + <string name="simplex_link_mode_full">Tam bağlantı</string> <string name="thousand_abbreviation">bin</string> <string name="profile_will_be_sent_to_contact_sending_link">Profiliniz, bu bağlantıyı aldığınız kişiye gönderilecek.</string> - <string name="connect_via_contact_link">Kişi bağlantısı üzerinden bağlan\?</string> + <string name="connect_via_contact_link">Adres bağlantısı üzerinden bağlan?</string> <string name="server_error">hata</string> <string name="connect_via_link_verb">Bağlan</string> <string name="server_connected">bağlanıldı</string> <string name="deleted_description">silindi</string> <string name="receiving_files_not_yet_supported">dosya alma henüz desteklenmiyor</string> <string name="sender_you_pronoun">sen</string> - <string name="invalid_chat">geçersiz konuşma</string> + <string name="invalid_chat">geçersi̇z sohbet</string> <string name="connection_local_display_name">bağlantı %1$d</string> <string name="simplex_link_mode_browser">Tarayıcı ile</string> - <string name="simplex_link_connection">%1$s ile</string> + <string name="simplex_link_connection">%1$s tarafından</string> <string name="connection_error">Bağlantı hatası</string> <string name="connection_timeout">Bağlantı süre aşımı</string> <string name="cannot_receive_file">Dosya alınamıyor</string> @@ -225,24 +219,24 @@ <string name="network_session_mode_entity">Bağlantı</string> <string name="network_session_mode_user">Konuşma profili</string> <string name="create_address">Adres oluştur</string> - <string name="callstatus_accepted">Yanıtlanmış arama</string> - <string name="callstatus_calling">aranıyor</string> + <string name="callstatus_accepted">kabul edilen arama</string> + <string name="callstatus_calling">aranıyor…</string> <string name="callstatus_in_progress">arama yapılıyor</string> <string name="colored_text">renklendirilmiş</string> <string name="confirm_password">Parolayı onayla</string> <string name="callstatus_rejected">geri çevrilmiş çağrı</string> <string name="create_profile_button">Oluştur</string> - <string name="callstatus_error">görüşme hatası</string> - <string name="callstate_connected">Bağlanıldı</string> + <string name="callstatus_error">arama hatası</string> + <string name="callstate_connected">bağlanıldı</string> <string name="callstate_connecting">bağlanılıyor…</string> <string name="call_already_ended">Görüşmesi çoktan bitti!</string> - <string name="your_calls">Görüşmelerin</string> + <string name="your_calls">Aramaların</string> <string name="open_verb">Aç</string> <string name="icon_descr_call_progress">Arama yapılıyor</string> <string name="icon_descr_call_rejected">Geri çevrilmiş çağrı</string> <string name="icon_descr_call_ended">Görüşme bitti.</string> <string name="alert_text_decryption_error_too_many_skipped">%1$d mesaj atlanıldı.</string> - <string name="chat_database_deleted">Konuşma veri tabanı silindi</string> + <string name="chat_database_deleted">Sohbet veritabanı silindi</string> <string name="error_with_info">Hata: %s</string> <string name="unknown_error">Bilinmeyen hata</string> <string name="rcv_group_event_member_connected">bağlanıldı</string> @@ -257,10 +251,10 @@ <string name="chat_preferences_yes">evet</string> <string name="accept_feature">Onayla</string> <string name="allow_disappearing_messages_only_if">Konuştuğun kişiler, kendiliğinden yok olan mesajlara izin veriyorsa sen de ver.</string> - <string name="allow_your_contacts_irreversibly_delete">Konuştuğun kişilerin gönderilen mesajları kalıcı olarak silmesine izin ver.</string> + <string name="allow_your_contacts_irreversibly_delete">Kişilerinin gönderilen mesajları kalıcı olarak silmesine izin ver. (24 saat içinde)</string> <string name="prohibit_sending_disappearing_messages">Kendiliğinden yok olan measj gönderimini engelle.</string> - <string name="allow_irreversible_message_deletion_only_if">Konuştuğun kişi, kalıcı olarak silinebilen mesajlara izin veriyorsa sen de ver.</string> - <string name="allow_message_reactions_only_if">Konuştuğun kişi, mesaj tepkilerine izin veriyorsa sen de ver.</string> + <string name="allow_irreversible_message_deletion_only_if">Konuştuğun kişi, kalıcı olarak silinebilen mesajlara izin veriyorsa sen de ver. (24 saat içinde)</string> + <string name="allow_message_reactions_only_if">Yalnızca kişin mesaj tepkilerine izin veriyorsa sen de ver.</string> <string name="only_you_can_send_disappearing">Sadece sen kendiliğinden yok olan mesaj gönderebilirsin.</string> <string name="only_your_contact_can_send_disappearing">Sadece konuştuğun kişi kendiliğinden yok olan mesaj gönderebilir.</string> <string name="both_you_and_your_contact_can_send_disappearing">Konuştuğun kişi de sen de kendiliğinden yok olan mesaj gönderebilirsiniz.</string> @@ -269,19 +263,19 @@ <string name="v4_4_disappearing_messages_desc">Gönderilen mesajlar, önceden belirlenmiş bir süre sonra silecektir.</string> <string name="v5_1_self_destruct_passcode_descr">Kullanıldığında bütün veriler silinir.</string> <string name="timed_messages">Kendiliğinden yok olan mesajlar</string> - <string name="allow_your_contacts_to_send_disappearing_messages">Konuştuğun kişilerin sana, kendiğinden yok olan mesajlar göndermesine izin ver.</string> - <string name="disappearing_messages_are_prohibited">Bu grupta kendiliğinden yok olan mesajlara izin verilmiyor.</string> - <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d mesaj deşifrelenemedi.</string> - <string name="group_info_section_title_num_members">%1$s ÜYE</string> - <string name="integrity_msg_skipped">%1$d atlanılmış mesaj</string> - <string name="allow_calls_only_if">Konuştuğun kişi, aramalara izin veriyorsa sen de ver.</string> + <string name="allow_your_contacts_to_send_disappearing_messages">Kişilerinin sana, kendiğinden yok olan mesajlar göndermesine izin ver.</string> + <string name="disappearing_messages_are_prohibited">Kendiliğinden yok olan mesajlara izin verilmiyor.</string> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d mesajlar deşifrelenemedi.</string> + <string name="group_info_section_title_num_members">%1$s ÜYELER</string> + <string name="integrity_msg_skipped">%1$d atlanılmış mesaj(lar)</string> + <string name="allow_calls_only_if">Yalnızca irtibat kişiniz izin veriyorsa aramalara izin verin.</string> <string name="allow_your_contacts_adding_message_reactions">Konuştuğun kişilerin mesajlarına tepki eklemesine izin ver.</string> - <string name="allow_your_contacts_to_call">Konuştuğun kişilerin seni aramasına izin ver.</string> - <string name="all_your_contacts_will_remain_connected">Konuştuğun kişilerin tümü bağlı kalacaktır.</string> + <string name="allow_your_contacts_to_call">Kişilerinin seni aramasına izin ver.</string> + <string name="all_your_contacts_will_remain_connected">Kişilerin tümü bağlı kalacaktır.</string> <string name="icon_descr_cancel_file_preview">Dosya ön izlemesini iptal et</string> - <string name="chat_database_imported">Konuşma veri tabanı içe aktarıldı</string> + <string name="chat_database_imported">Sohbet veritabanı içe aktarıldı</string> <string name="icon_descr_cancel_image_preview">Görsel ön izlemesini iptal et</string> - <string name="clear_chat_question">Konuşmayı temizle\?</string> + <string name="clear_chat_question">Konuşma temizlensin mi?</string> <string name="v4_6_chinese_spanish_interface">Çince ve İspanyolca arayüz</string> <string name="confirm_new_passphrase">Yeni parolayı onayla…</string> <string name="info_row_connection">Bağlantı</string> @@ -296,25 +290,25 @@ <string name="settings_section_title_calls">ARAMALAR</string> <string name="settings_section_title_chats">KONUŞMALAR</string> <string name="settings_section_title_you">SEN</string> - <string name="chat_database_section">KONUŞMA VERİ TABANI</string> + <string name="chat_database_section">SOHBET VERİTABANI</string> <string name="remove_passphrase">Kaldır</string> <string name="wrong_passphrase_title">Yanlış parola!</string> - <string name="confirm_database_upgrades">Veri tabanının yapısal ilerlemelerini onayla</string> + <string name="confirm_database_upgrades">Veritabanı yükseltmelerini onayla</string> <string name="rcv_group_event_member_deleted">%1$s çıkarıldı</string> <string name="rcv_group_event_user_deleted">seni çıkardı</string> <string name="group_member_status_connected">bağlanıldı</string> - <string name="group_member_status_announced">bağlanılıyor (duyruldu)</string> + <string name="group_member_status_announced">bağlanılıyor (duyuruldu)</string> <string name="group_info_member_you">sen: %1$s</string> - <string name="group_member_status_removed">çıkarıldı</string> + <string name="group_member_status_removed">kaldırıldı</string> <string name="member_info_section_title_member">ÜYE</string> <string name="group_members_can_send_disappearing">Grup üyeleri kendiliğinden yok olan mesajlar gönderebilir.</string> <string name="prohibit_sending_disappearing">Kendiliğinden yok olan mesaj gönderimini engelle.</string> - <string name="allow_voice_messages_only_if">Konuştuğun kişi, sesli mesajlara izin veriyorsa sen de ver.</string> - <string name="allow_your_contacts_to_send_voice_messages">Konuştuğun kişilerin sesli mesaj göndermesine izin ver.</string> + <string name="allow_voice_messages_only_if">Yalnızca kişiniz sesli mesaj göndermeye izin veriyorsa sen de ver.</string> + <string name="allow_your_contacts_to_send_voice_messages">Kişilerinin sesli mesaj göndermesine izin ver.</string> <string name="both_you_and_your_contact_can_add_message_reactions">Konuştuğun kişi de sen de mesajlara tepki ekleyebilirsinsiz.</string> <string name="both_you_and_your_contact_can_send_voice">Konuştuğun kişi de sen de sesli mesaj gönderebilirsiniz.</string> <string name="v4_4_disappearing_messages">Kendiliğinden yok olan mesajlar</string> - <string name="both_you_and_your_contacts_can_delete">Konuştuğun kişi de sen de mesajları kalıcı olarak silebilirsiniz.</string> + <string name="both_you_and_your_contacts_can_delete">Konuştuğun kişi ve sen mesajları kalıcı olarak silebilirsiniz. (24 saat içinde)</string> <string name="decryption_error">Deşifreleme hatası</string> <string name="description_via_contact_address_link">kişi adres bağlantısı ile</string> <string name="simplex_link_contact">SimpleX bağlantı adresi</string> @@ -342,9 +336,9 @@ <string name="error_creating_link_for_group">Toplu konuşma bağlantısı oluştururken hata oluştu</string> <string name="error_changing_role">Yetki değiştirirken hata oluştu</string> <string name="error_deleting_link_for_group">Toplu konuşma bağlantısı silinirken hata oluştu</string> - <string name="error_deleting_contact">Konuştuğun kişiyi silerken hata oluştu</string> + <string name="error_deleting_contact">Kişiyi silerken hata oluştu</string> <string name="error_deleting_contact_request">Konuşma isteği silinirken hata oluştu</string> - <string name="error_deleting_group">Toplu konuşma silinirken hata oluştu</string> + <string name="error_deleting_group">Grup silinirken hata oluştu</string> <string name="error_deleting_pending_contact_connection">Bekleyen konuşma bağlantısını silinirken hata oluştu</string> <string name="error_deleting_user">Kullanıcı profili silinirken hata oluştu</string> <string name="error_deleting_database">Konuşma veri tabanı silinirken hata oluştu</string> @@ -356,15 +350,15 @@ <string name="error_importing_database">Konuşma veri tabanını içe aktarılırken hata oluştu</string> <string name="database_error">Veri tabanı hatası</string> <string name="create_your_profile">Profilini oluştur</string> - <string name="custom_time_picker_custom">kişiselleştirilmiş</string> + <string name="custom_time_picker_custom">özel</string> <string name="database_passphrase_will_be_updated">Veri tabanının şifrelemesinde kullanılan parolası güncellenecek.</string> <string name="encrypted_with_random_passphrase">Veri tabanı, rastgele bir parola ile şifrelendi. İstersen parolayı değiştirebilirsin.</string> <string name="ttl_d">%dg</string> - <string name="delete_link_question">Bağlantıyı sil\?</string> + <string name="delete_link_question">Bağlantı silinsin mi?</string> <string name="ttl_hours">%d saat</string> <string name="v4_6_group_moderation_descr">Yeni yöneticiler artık: \n- üyelerin mesajlarını silebilir. -\n- üyeleri etkisizleştirebilir (\"gözlemci\" yetkisi)</string> +\n- üyeleri etkisizleştirebilir ("gözlemci" yetkisi verir)</string> <string name="receipts_section_contacts">Konuşmalar</string> <string name="create_group_link">Grup bağlantısı oluştur</string> <string name="error_accepting_contact_request">Konuşma isteğini onaylarken hata oluştu</string> @@ -378,8 +372,8 @@ <string name="la_current_app_passcode">Güncel Erişim Kodu</string> <string name="copy_verb">Kopyala</string> <string name="delete_verb">Sil</string> - <string name="delete_message__question">Mesajı sil\?</string> - <string name="delete_member_message__question">Üye mesajını sil\?</string> + <string name="delete_message__question">Mesaj silinsin mi?</string> + <string name="delete_member_message__question">Üyenin mesajı silinsin mi?</string> <string name="for_me_only">Benim için sil</string> <string name="icon_descr_context">İçerik simgesi</string> <string name="image_decoding_exception_title">Dekodlama hatası</string> @@ -407,10 +401,10 @@ <string name="delete_image">Görseli sil</string> <string name="you_control_your_chat">Konuşman, senin elinde!</string> <string name="create_profile">Profil oluştur</string> - <string name="decentralized">Özeksiz</string> + <string name="decentralized">Merkezi olmayan</string> <string name="confirm_passcode">Erişim kodunu onayla</string> <string name="incorrect_passcode">Yanlış erişim kodu</string> - <string name="new_passcode">Yeni Şifre</string> + <string name="new_passcode">Yeni erişim kodu</string> <string name="submit_passcode">Gönder</string> <string name="la_mode_passcode">Erişim kodu</string> <string name="passcode_changed">Erişim kodu değişti!</string> @@ -433,9 +427,6 @@ <string name="set_password_to_export_desc">Veri tabanı, rastgele bir parola ile şifrelendi. Dışa aktarmadan önce lütfen değiştir.</string> <string name="delete_files_and_media_question">Dosyaları ve medyayı sil\?</string> <string name="database_will_be_encrypted">Veri tabanı şifrelenecektir.</string> - <string name="archive_created_on_ts">%1$s tarihinde oluşturuldu</string> - <string name="delete_archive">Belgeliği sil</string> - <string name="delete_chat_archive_question">Konuşma belgeliğini sil\?</string> <string name="rcv_group_event_changed_member_role">%s üyesinin yetkisi %s olarak değiştirildi</string> <string name="rcv_group_event_group_deleted">silinmiş grup</string> <string name="snd_group_event_changed_role_for_yourself">kendi yetkini, %s olarak değiştirdin</string> @@ -443,7 +434,7 @@ <string name="group_member_status_creator">oluşturan</string> <string name="no_contacts_selected">Hiçbir konuşma seçilmedi</string> <string name="num_contacts_selected">%d tane konuşma seçildi</string> - <string name="share_text_database_id">Veri tabanı kimliği: %d</string> + <string name="share_text_database_id">Veritabanı kimliği: %d</string> <string name="info_row_deleted_at">Şu tarihte silindi</string> <string name="share_text_deleted_at">Şu tarihte silindi: %s</string> <string name="item_info_current">(güncel)</string> @@ -460,24 +451,24 @@ <string name="database_encrypted">Veri tabanı şifrelendi!</string> <string name="delete_messages">Mesajları sil</string> <string name="delete_messages_after">Şu süre sonra mesajları sil</string> - <string name="total_files_count_and_size">%d tane dosya, toplam boyutu %s</string> + <string name="total_files_count_and_size">%d dosya(lar),toplam boyut olarak %s</string> <string name="new_passphrase">Yeni parola…</string> <string name="database_will_be_encrypted_and_passphrase_stored">Veri tabanı şifrelenecek ve parola, Keystore\'a kaydedilecek.</string> <string name="database_encryption_will_be_updated">Veri tabanının şifrelemesinde kullanılan parola güncellenecek ve Keystore\'da barındıralacak.</string> <string name="passphrase_is_different">Veri tabanı parolası, Keystore\'da barındırılandan değişik.</string> <string name="database_passphrase_is_required">Konuşmayı açmak için veri tabanı parolası gerekli.</string> - <string name="database_upgrade">Veri tabanını yapısal olarak ilerlet</string> + <string name="database_upgrade">Veritabanı yükseltmesi</string> <string name="rcv_group_event_changed_your_role">yetkin, %s olarak değişti</string> <string name="snd_group_event_changed_member_role">%s üyesinin yetkisini, %s olarak değiştirdin</string> - <string name="group_member_role_owner">iye/sahip</string> + <string name="group_member_role_owner">sahip</string> <string name="icon_descr_expand_role">Yetki seçimini genişlet</string> <string name="initial_member_role">İlk olarak verilen yetki</string> <string name="new_member_role">Yeni üye yetkisi</string> <string name="button_delete_group">Grubu sil</string> - <string name="delete_group_question">Grubu sil\?</string> + <string name="delete_group_question">Grup silinsin mi?</string> <string name="button_create_group_link">Bağlantı oluştur</string> <string name="delete_link">Bağlantıyı sil</string> - <string name="info_row_database_id">Veri tabanı kimliği</string> + <string name="info_row_database_id">Veritabanı kimliği</string> <string name="change_role">Yetkiyi değiştir</string> <string name="create_secret_group_title">Gizli grup oluştur</string> <string name="chat_preferences_no">hayır</string> @@ -502,18 +493,18 @@ <string name="edit_image">Görseli düzenle</string> <string name="encrypted_audio_call">uçtan uca şifreli sesli arama</string> <string name="encrypted_video_call">uçtan uca şifreli görüntülü arama</string> - <string name="status_e2e_encrypted">uçtan uca şifreli</string> + <string name="status_e2e_encrypted">uçtan uca şifrelenmiş</string> <string name="allow_accepting_calls_from_lock_screen">Ayarlardan, kilitli ekrandan aramaları izin ver.</string> - <string name="integrity_msg_duplicate">Aynısı bulunan mesaj</string> + <string name="integrity_msg_duplicate">yinelenen mesaj</string> <string name="enable_lock">Kilidi etkinleştir</string> <string name="info_row_disappears_at">Kendiliğinden şu sürede yok olacak</string> <string name="share_text_disappears_at">Kendiliğinden şu sürede yok olacak: %s</string> - <string name="feature_enabled">etkinleşti</string> - <string name="direct_messages">Direkt mesaj</string> + <string name="feature_enabled">etkin</string> + <string name="direct_messages">Doğrudan mesajlar</string> <string name="no_call_on_lock_screen">Devre dışı bırak</string> <string name="display_name_cannot_contain_whitespace">Görünen ad, boşluk gibi aralıklama türleri içeremez.</string> - <string name="display_name">Görünen Ad</string> - <string name="display_name__field">Görünen ad:</string> + <string name="display_name">İsmini gir:</string> + <string name="display_name__field">Profil adı:</string> <string name="ttl_mth">%day</string> <string name="ttl_s">%dsn</string> <string name="dont_create_address">Adres oluşturma</string> @@ -524,18 +515,18 @@ <string name="receipts_contacts_disable_for_all">Herkes için devre dışı bırak</string> <string name="receipts_contacts_disable_keep_overrides">Devre dışı bırak (ayrıcalıklar kalsın)</string> <string name="receipts_contacts_enable_for_all">tüm kişiler için etkinleştir</string> - <string name="enable_automatic_deletion_question">Kendi kendine silinen mesajları etkinleştir\?</string> + <string name="enable_automatic_deletion_question">Kendi kendine silinen mesajlar etkinleştirilsin mi?</string> <string name="encrypt_database">Şifrele</string> <string name="encrypt_database_question">Veri tabanını şifrele\?</string> <string name="conn_event_ratchet_sync_ok">şifreleme etkin</string> <string name="button_edit_group_profile">Toplu konuşma profilini düzenle</string> <string name="conn_event_ratchet_sync_agreed">şifreleme kabul edildi</string> <string name="snd_conn_event_ratchet_sync_agreed">%s üyesi için şifreleme kabul edildi</string> - <string name="conn_level_desc_direct">direkt</string> + <string name="conn_level_desc_direct">doğrudan</string> <string name="dont_show_again">Yeniden gösterme</string> - <string name="direct_messages_are_prohibited_in_chat">Bu grupta üyeler arası direkt mesajlar yasaklıdır.</string> + <string name="direct_messages_are_prohibited_in_group">Bu grupta üyeler arası doğrudan mesajlaşma yasaklıdır.</string> <string name="feature_enabled_for_contact">konuşulan kişi için etkinleşti</string> - <string name="feature_enabled_for_you">senin için etkinleşti</string> + <string name="feature_enabled_for_you">senin için etkinleştirildi</string> <string name="ttl_sec">%d sn</string> <string name="ttl_m">%da</string> <string name="ttl_min">%d dk</string> @@ -585,21 +576,21 @@ <string name="incorrect_code">Yanlış güvenlik kodu!</string> <string name="how_to_use_your_servers">Sunucuların nasıl kullanılır</string> <string name="enter_one_ICE_server_per_line">ICE sunucuları (her satır başı bir tane)</string> - <string name="host_verb">Barındır</string> + <string name="host_verb">Ana bilgisayar</string> <string name="hide_dev_options">Gizle:</string> <string name="full_name__field">Ad ve soyad:</string> <string name="email_invite_body">Merhaba! -\nSimplex Chat kullanarak benle bağlantıya geç: %s</string> +\nSimplex Chat kullanarak benimle bağlantıya geç: %s</string> <string name="exit_without_saving">Kaydetmeden çık</string> <string name="hide_profile">Profili gizle</string> <string name="error_saving_user_password">Kullanıcı parolası kaydedilirken hata oluştu</string> <string name="hidden_profile_password">Gizli profil parolası</string> <string name="how_to_use_markdown">Markdown nasıl kullanılır</string> <string name="how_it_works">Nasıl çalışıyor</string> - <string name="immune_to_spam_and_abuse">Kötüye kullanmaya ve istenmeyen mesajlara duyarlı</string> + <string name="immune_to_spam_and_abuse">Spamdan etkilenmez</string> <string name="icon_descr_hang_up">Çağırıyı bitir.</string> <string name="icon_descr_flip_camera">Kameranın karşı yüzüne geç</string> - <string name="import_database_question">Konuşma veri tabanını içe aktar\?</string> + <string name="import_database_question">Konuşma veri tabanı içe aktarılsın mı?</string> <string name="alert_message_group_invitation_expired">Grup daveti artık geçerli değil. Gönderici tarafından kaldırıldı.</string> <string name="alert_title_no_group">Grup bulunamadı!</string> <string name="delete_group_for_all_members_cannot_undo_warning">Grup tüm üyeler için silinecektir - bu geri alınamaz!</string> @@ -614,7 +605,7 @@ <string name="v5_2_fix_encryption_descr">Yedekleri geri yükledikten sonra şifrelemeyi onar.</string> <string name="v4_4_french_interface">Fransız arayüzü</string> <string name="v4_6_reduced_battery_usage">Daha da azaltılmış pil kullanımı</string> - <string name="group_members_can_add_message_reactions">Grup üyeleri, mesajlara tepki ekleyebilir.</string> + <string name="group_members_can_add_message_reactions">Üyeler, mesajlara tepki ekleyebilir.</string> <string name="group_profile_is_stored_on_members_devices">Grup profili, üyelerinin aygıtlarında barındırılmaktadır, sunucularda değil.</string> <string name="hide_verb">Gizle</string> <string name="user_hide">Gizle</string> @@ -641,25 +632,25 @@ <string name="incompatible_database_version">Uyumsuz veri tabanı sürümü</string> <string name="icon_descr_group_inactive">Grup etkin değil</string> <string name="group_invitation_expired">Grup davetinin süresi doldu</string> - <string name="snd_group_event_group_profile_updated">Grup profili güncellendi</string> - <string name="group_member_status_group_deleted">toplu konuşma silindi</string> + <string name="snd_group_event_group_profile_updated">grup profili güncellendi</string> + <string name="group_member_status_group_deleted">grup silindi</string> <string name="error_updating_link_for_group">Toplu konuşma bağlantısı güncellenirken hata oluştu</string> <string name="section_title_for_console">UÇBİRİM İÇİN</string> <string name="group_link">Grup bağlantısı</string> - <string name="info_row_group">Toplu konuşma</string> + <string name="info_row_group">Grup</string> <string name="conn_level_desc_indirect">dolaylı (%1$s)</string> <string name="fix_connection">Bağlantıyı onar</string> - <string name="fix_connection_question">Bağlantıyı onar\?</string> + <string name="fix_connection_question">Bağlantı onarılsın mı?</string> <string name="fix_connection_confirm">Onar</string> <string name="fix_connection_not_supported_by_contact">Konuştuğunuz kişi, onarımı desteklemiyor.</string> <string name="fix_connection_not_supported_by_group_member">Toplu konuşma üyesi, onarımı desteklemiyor.</string> - <string name="group_display_name_field">Grup görünen adı:</string> + <string name="group_display_name_field">Grup adını gir:</string> <string name="group_full_name_field">Grup tam adı:</string> <string name="files_and_media">Dosya ve medya</string> - <string name="group_members_can_send_dms">Grup üyeleri direkt mesaj gönderebilir.</string> - <string name="group_members_can_delete">Gru üyeleri, gönderilen mesajları kalıcı olarak silebilir.</string> + <string name="group_members_can_send_dms">Grup üyeleri doğrudan mesaj gönderebilir.</string> + <string name="group_members_can_delete">Üyeler, gönderilen mesajları kalıcı olarak silebilir. (24 saat içinde)</string> <string name="group_members_can_send_voice">Grup üyeleri sesli mesaj gönderebilirler.</string> - <string name="files_are_prohibited_in_group">Bu toplu konuşmada, dosya ve medya yasaklanmıştır.</string> + <string name="files_are_prohibited_in_group">Dosya ve medya yasaklanmıştır.</string> <string name="group_members_can_send_files">Grup üyeleri dosya ve medya paylaşabilir.</string> <string name="v4_2_group_links">Grup bağlantıları</string> <string name="v5_2_disappear_one_message_descr">Konuşmada devre dışı bırakıldığında bile</string> @@ -670,7 +661,7 @@ <string name="v4_6_group_moderation">Grup yönetimi</string> <string name="v4_6_group_welcome_message">Grup karşılama mesajı</string> <string name="v4_6_hidden_chat_profiles">Gizli konuşma profilleri</string> - <string name="custom_time_unit_hours">Saat</string> + <string name="custom_time_unit_hours">saat</string> <string name="notifications_mode_off_desc">Sadece uygulama çalışırken bildirim alabileceksiniz, hiçbir arka plan hizmeti başlatılmayacaktır</string> <string name="notification_preview_new_message">yeni mesaj</string> <string name="welcome">Hoşgeldin!</string> @@ -694,12 +685,12 @@ <string name="empty_chat_profile_is_created">Verilen adla boş bir sohbet profili oluşturulur ve uygulama her zamanki gibi açılır.</string> <string name="voice_prohibited_in_this_chat">Bu sohbette sesli mesajlar yasaktır.</string> <string name="v4_2_auto_accept_contact_requests">Bağlanma isteklerini otomatik kabul et</string> - <string name="database_downgrade_warning">Uyarı: Bazı verileri kaybedebilirsiniz!</string> + <string name="database_downgrade_warning">Uyarı: Bazı verileri kaybedebilirsin!</string> <string name="all_your_contacts_will_remain_connected_update_sent">Tüm kişileriniz bağlı kalacaktır. Profil güncellemesi kişilerinize gönderilecektir.</string> <string name="keychain_is_storing_securely">Android Keystore parolayı güvenli bir şekilde saklamak için kullanılır - bildirim hizmetinin çalışmasını sağlar.</string> <string name="button_welcome_message">Karşılama mesajı</string> <string name="group_welcome_title">Karşılama mesajı</string> - <string name="voice_messages_are_prohibited">Bu grupta sesli mesajlar yasaktır.</string> + <string name="voice_messages_are_prohibited">Sesli mesajlar yasaktır.</string> <string name="whats_new">Neler yeni</string> <string name="new_in_version">%s sürümünde yeni</string> <string name="whats_new_read_more">Daha fazla bilgi edinin</string> @@ -713,7 +704,7 @@ <string name="icon_descr_instant_notifications">Anlık bildirimler</string> <string name="service_notifications">Anlık bildirimler</string> <string name="service_notifications_disabled">Anlık bildirimler devre dışı!</string> - <string name="turn_off_battery_optimization"><![CDATA[Bunu kullanmak için lütfen bir sonraki iletişim kutusunda SimpleX için <b>pil optimizasyonunu devre dışı bırakın</b>. Aksi takdirde, bildirimler devre dışı bırakılacaktır.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[Bildirimleri anında almak için bir sonraki iletişim kutusunda buna <b>izin verin</b>. ]]></string> <string name="notification_preview_mode_contact">Kişi ismi</string> <string name="auth_device_authentication_is_disabled_turning_off">Cihaz doğrulaması devre dışı. SimpleX Kilidi Kapatılıyor.</string> <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Cihaz doğrulaması etkin değil. Cihaz doğrulamasını etkinleştirdikten sonra SimpleX Kilidini Ayarlar üzerinden açabilirsiniz.</string> @@ -736,17 +727,14 @@ <string name="v4_3_irreversible_message_deletion">Geri alınamaz mesaj silme</string> <string name="v4_5_italian_interface">İtalyanca arayüz</string> <string name="choose_file_title">Dosya seç</string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>QR kodunu tara</b>: size QR kodunu gösteren kişiyle bağlantı kurmak için.]]></string> <string name="invite_friends">Arkadaşlarınızı davet edin</string> <string name="bold_text">kalın</string> - <string name="italic_text">İtalik</string> - <string name="onboarding_notifications_mode_subtitle">Daha sonra ayarlardan değiştirebilirsiniz.</string> - <string name="status_contact_has_e2e_encryption">Kişi uçtan uca şifrelemeye sahiptir</string> - <string name="status_contact_has_no_e2e_encryption">Kişi uçtan uca şifrelemeye sahip değildir</string> + <string name="italic_text">italik</string> + <string name="onboarding_notifications_mode_subtitle">Pili nasıl etkiler</string> + <string name="status_contact_has_e2e_encryption">kişi uçtan uca şifrelemeye sahiptir</string> + <string name="status_contact_has_no_e2e_encryption">kişi uçtan uca şifrelemeye sahip değildir</string> <string name="chat_is_stopped">Sohbet durduruldu</string> <string name="impossible_to_recover_passphrase"><![CDATA[<b>Aklınızda bulunsun</b>: kaybederseniz, parolayı kurtaramaz veya değiştiremezsiniz.]]></string> - <string name="chat_archive_header">Sohbet arşivi</string> - <string name="chat_archive_section">SOHBET ARŞİVİ</string> <string name="group_invitation_item_description">%1$s grubuna davet</string> <string name="join_group_question">Gruba katıl\?</string> <string name="rcv_group_event_member_added">%1$s davet edildi</string> @@ -755,15 +743,14 @@ <string name="invite_to_group_button">Gruba davet edin</string> <string name="invite_prohibited">Kişi davet edilemiyor!</string> <string name="button_add_members">Üyeleri davet edin</string> - <string name="cant_delete_user_profile">Kullanıcı profili silinemiyor!</string> <string name="color_background">Arka plan</string> <string name="message_deletion_prohibited">Bu sohbette geri alınamaz mesaj silme yasaktır.</string> <string name="delete_contact_question">Kişiyi sil\?</string> <string name="button_delete_contact">Kişiyi sil</string> - <string name="callstatus_connecting">Aramaya bağlanılıyor…</string> + <string name="callstatus_connecting">aramaya bağlanılıyor…</string> <string name="delivery_receipts_are_disabled">Gönderildi bilgisi kapalı!</string> <string name="v5_2_more_things">Birkaç şey daha</string> - <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Daha fazla pil kullanır</b>! Arka plan hizmeti her zaman çalışır - mesajlar gelir gelmez bildirim gönderilir.]]></string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Daha fazla pil kullanır</b>! Uygulama her zaman arka planda çalışır - bildirimler anında gösterilir.]]></string> <string name="in_developing_title">Çok yakında!</string> <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Kişi ve tüm mesajlar silinecektir - bu geri alınamaz!</string> <string name="alert_title_contact_connection_pending">Kişi henüz bağlanmadı!</string> @@ -771,13 +758,13 @@ <string name="v5_1_japanese_portuguese_interface">Japonca ve Portekizce kullanıcı arayüzü</string> <string name="alert_title_cant_invite_contacts">Kişiler davet edilemiyor!</string> <string name="alert_title_group_invitation_expired">Davetin süresi dolmuş!</string> - <string name="message_deletion_prohibited_in_chat">Geri alınamaz mesaj silme bu grupta yasaktır</string> + <string name="message_deletion_prohibited_in_chat">Geri alınamaz mesaj silme yasaktır</string> <string name="delivery_receipts_title">Mesaj gönderildi bilgisi!</string> <string name="joining_group">Gruba katılınıyor</string> <string name="description_you_shared_one_time_link_incognito">tek seferlik gizli bağlantı paylaştınız</string> - <string name="description_via_group_link_incognito">Grup bağlantısı ile gizli</string> - <string name="description_via_contact_address_link_incognito">Bağlantı linki ile gizli</string> - <string name="description_via_one_time_link_incognito">Tek seferlik bağlantı ile gizli</string> + <string name="description_via_group_link_incognito">grup bağlantısı ile gizli</string> + <string name="description_via_contact_address_link_incognito">kişi bağlantı linki ile gizli</string> + <string name="description_via_one_time_link_incognito">tek seferlik bağlantı aracılığıyla gizli</string> <string name="smp_server_test_compare_file">Dosyaları karşılaştır</string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>ayarlardan devre dışı bırakılabilir</b> - uygulama çalışıyorken bildirimler gösterilmeye devam edilecektir.]]></string> <string name="turning_off_service_and_periodic">Pil optimizasyonu etkin, arka plan hizmeti kapatılacak ve düzenli olarak yeni mesajlar kontrol edilmeyecek . Bunları ayarlardan yeniden etkinleştirebilirsiniz.</string> @@ -788,9 +775,8 @@ <string name="auth_unavailable">Doğrulama mevcut değil</string> <string name="message_delivery_error_title">Mesaj teslim hatası</string> <string name="delete_message_cannot_be_undone_warning">Mesajlar silinecek - bu geri alınamaz!</string> - <string name="switch_receiving_address_question">Alıcı adresini değiştir\?</string> + <string name="switch_receiving_address_question">Alıcı adresi değiştirilsin mi?</string> <string name="back">Geri</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Yeni kişi ekle</b>: Kişiniz için tek seferlik QR Kodunuzu oluşturmak için.]]></string> <string name="you_will_be_connected_when_your_connection_request_is_accepted">Bağlantı isteğiniz kabul edildiğinde bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!</string> <string name="you_will_be_connected_when_your_contacts_device_is_online">Kişinizin cihazı çevrimiçi olduğunda bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!</string> <string name="learn_more">Daha fazla bilgi edinin</string> @@ -812,7 +798,7 @@ \nv5.1\'de mevcut"</string> <string name="v4_5_transport_isolation_descr">Sohbet profili ile (varsayılan) veya bağlantı ile (BETA).</string> <string name="image_descr_link_preview">bağlantı önizleme resmi</string> - <string name="your_profile_is_stored_on_your_device">Profiliniz, kişileriniz ve mesajlar cihazınızda saklanır.</string> + <string name="your_profile_is_stored_on_your_device">Profiliniz, kişileriniz ve gönderilmiş mesajlar cihazınızda saklanır.</string> <string name="callstatus_ended">arama sona erdi %1$s</string> <string name="call_on_lock_screen">Kilit ekranında aramalar:</string> <string name="alert_title_msg_bad_id">Kötü mesaj kimliği</string> @@ -824,7 +810,7 @@ <string name="snd_conn_event_switch_queue_phase_changing">adres değiştiriliyor…</string> <string name="group_member_status_left">ayrıldı</string> <string name="button_leave_group">Gruptan ayrıl</string> - <string name="incognito_random_profile">Rastgele profiliniz</string> + <string name="incognito_random_profile">Rasgele profiliniz</string> <string name="message_reactions_prohibited_in_this_chat">Mesaj tepkileri bu sohbette yasaklıdır</string> <string name="v5_1_message_reactions">Mesaj tepkileri</string> <string name="v5_1_better_messages">Daha iyi mesajlar</string> @@ -832,7 +818,7 @@ <string name="mark_read">Okundu olarak işaretle</string> <string name="mark_unread">Okunmadı olarak işaretle</string> <string name="chat_console">Sohbet konsolu</string> - <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Pil için iyi</b>. Arka plan hizmeti mesajları 10 dakikada bir kontrol eder. Aramaları veya acil mesajları kaçırabilirsiniz.]]></string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Pil ömrü için iyi</b>. Uygulama mesajları 10 dakikada bir kontrol eder. Aramaları veya acil mesajları kaçırabilirsiniz.]]></string> <string name="v4_4_verify_connection_security_desc">Güvenlik kodlarını kişilerinizle karşılaştırın.</string> <string name="notifications_mode_service_desc">Arka plan hizmeti her zaman çalışır - mesajlar gelir gelmez bildirim gönderilir.</string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Pil için en iyisi</b>. Sadece uygulama çalışırken bildirim alırsınız (arka plan hizmeti YOK).]]></string> @@ -849,22 +835,21 @@ <string name="switch_receiving_address">Alıcı adresini değiştir</string> <string name="message_reactions">Mesaj tepkileri</string> <string name="your_preferences">Tercihleriniz</string> - <string name="message_reactions_are_prohibited">Mesaj tepkileri bu grupta yasaklıdır</string> + <string name="message_reactions_are_prohibited">Mesaj tepkileri yasaklıdır.</string> <string name="connected_to_server_to_receive_messages_from_contact">Bu kişiden mesaj almak için kullanılan sunucuya bağlısınız.</string> - <string name="you_are_already_connected_to_vName_via_this_link">Zaten şuna bağlısınız: %1$s</string> + <string name="you_are_already_connected_to_vName_via_this_link">Zaten %1$s e bağlısınız</string> <string name="la_could_not_be_verified">Doğrulanamadınız; lütfen tekrar deneyin.</string> <string name="you_can_turn_on_lock">SimpleX Kilidini Ayarlar üzerinden açabilirsiniz.</string> <string name="group_preview_you_are_invited">gruba davet edildiniz</string> <string name="you_have_no_chats">Hiç sohbetiniz yok</string> <string name="you_are_observer">Gözlemcisiniz</string> - <string name="observer_cant_send_message_title">Mesaj gönderemezsiniz</string> + <string name="observer_cant_send_message_title">Mesajlar gönderemezsiniz!</string> <string name="view_security_code">Güvenlik kodunu görüntüle</string> <string name="you_need_to_allow_to_send_voice">Sesli mesaj gönderebilmeniz için kişinizin de sesli mesaj göndermesine izin vermeniz gerekir.</string> <string name="xftp_servers">XFTP sunucuları</string> <string name="your_ICE_servers">ICE sunucularınız</string> <string name="how_to">Nasıl</string> <string name="your_current_profile">Mevcut profiliniz</string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Mesajların hangi sunucu(lar)dan <b>alınacağını</b> siz kontrol edersiniz, kişileriniz - onlara mesaj göndermek için kullandığınız sunucular.]]></string> <string name="video_call_no_encryption">video arama (uçtan uca şifreli değil)</string> <string name="your_ice_servers">ICE sunucularınız</string> <string name="icon_descr_video_off">Video kapalı</string> @@ -877,9 +862,9 @@ <string name="your_contacts_will_remain_connected">Kişileriniz bağlı kalacaktır.</string> <string name="you_can_create_it_later">Daha sonra oluşturabilirsiniz</string> <string name="you_can_use_markdown_to_format_messages__prompt">Mesajları biçimlendirmek için markdown kullanabilirsiniz:</string> - <string name="your_chat_database">Mesaj veri tabanınız</string> + <string name="your_chat_database">Sohbet veritabanınız</string> <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Mevcut sohbet veritabanınız SİLİNECEK ve içe aktarılan veritabanıyla DEĞİŞTİRİLECEKTİR. -\nBu işlem geri alınamaz - profiliniz, kişileriniz, mesajlarınız ve dosyalarınız geri alınamaz şekilde kaybolacaktır.</string> +\nBu eylem geri alınamaz - profiliniz, kişileriniz, mesajlarınız ve dosyalarınız geri alınamaz şekilde kaybolacaktır.</string> <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Bu gruba katıldınız. Davet eden grup üyesine bağlanılıyor.</string> <string name="you_are_invited_to_group_join_to_connect_with_group_members">Gruba davetlisiniz. Grup üyeleriyle bağlantı kurmak için katılın.</string> <string name="alert_title_cant_invite_contacts_descr">Bu grup için gizli bir profil kullanıyorsunuz - ana profilinizi paylaşmayı önlemek için kişileri davet etmeye izin verilmiyor</string> @@ -896,13 +881,13 @@ <string name="you_can_share_your_address">Adresinizi bir bağlantı veya QR kodu olarak paylaşabilirsiniz - herkes size bağlanabilir.</string> <string name="snd_conn_event_switch_queue_phase_completed">bağlantı değiştirdiniz</string> <string name="you_can_enable_delivery_receipts_later">Daha sonra Ayarlardan etkinleştirebilirsin</string> - <string name="you_can_enable_delivery_receipts_later_alert">Sonrasında uygulamanın Gizlilik ve Güvenlik ayarlarından etkinleştirebilirsiniz.</string> + <string name="you_can_enable_delivery_receipts_later_alert">Daha sonra uygulamanın Gizlilik ve Güvenlik ayarlarından etkinleştirebilirsiniz.</string> <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Bağlantının tamamlanması için kişinizin çevrimiçi olması gerekir. \nBu bağlantıyı iptal edebilir ve kişiyi kaldırabilirsiniz (ve daha sonra yeni bir bağlantıyla deneyebilirsiniz).</string> <string name="contact_sent_large_file">Kişiniz desteklenen maksimum boyuttan (%1$s) daha büyük bir dosya gönderdi.</string> <string name="video_will_be_received_when_contact_completes_uploading">Kişiniz yüklemeyi tamamladığında video alınacaktır.</string> <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Ayrıca bağlantıya tıklayarak da bağlanabilirsiniz. Eğer bağlantı tarayıcda açılırsa, <b>mobil uygulamada aç</b> seçeneğine tıklayın.]]></string> - <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Soru sormak ve güncellemeleri almak için <font color=#0088ff>SimpleX Chat geliştiricilerine bağlanabilirsiniz</font>.]]></string> + <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Soru sormak ve güncellemeleri almak için <font color="#0088ff">SimpleX Chat geliştiricilerine bağlanabilirsiniz</font>.]]></string> <string name="you_can_hide_or_mute_user_profile">Bir kullanıcının profilini gizleyebilir veya sessize alabilirsiniz - menü için basılı tutun.</string> <string name="you_must_use_the_most_recent_version_of_database">Sohbet veritabanınızın en son sürümünü SADECE bir cihazda kullanmalısınız, aksi takdirde bazı kişilerden daha fazla mesaj alamayabilirsiniz.</string> <string name="wrong_passphrase">Yanlış veritabanı parolası</string> @@ -921,7 +906,7 @@ <string name="ok">TAMAM</string> <string name="icon_descr_more_button">Daha fazla</string> <string name="one_time_link">Tek seferlik davet bağlantısı</string> - <string name="network_settings_title">Ağ ayarları</string> + <string name="network_settings_title">Gelişmiş ayarlar</string> <string name="shutdown_alert_desc">Siz uygulamayı yeniden başlatana kadar bildirimler çalışmayacaktır</string> <string name="la_mode_off">Kapalı</string> <string name="self_destruct_new_display_name">Yeni görünen ad:</string> @@ -939,14 +924,14 @@ <string name="v4_6_reduced_battery_usage_descr">Daha fazla gelişme yakında geliyor!</string> <string name="only_stored_on_members_devices">(sadece grup üyeleri tarafından saklanır)</string> <string name="markdown_help">markdown yardımı</string> - <string name="no_info_on_delivery">Gönderi hakkında bilgi yok</string> + <string name="no_info_on_delivery">Gönderi bilgisi yok</string> <string name="no_selected_chat">Seçili sohbet yok</string> <string name="add_contact">Tek seferlik davet bağlantısı</string> <string name="receipts_groups_enable_for_all">Tüm gruplar için etkinleştir</string> <string name="v5_2_more_things_descr">- daha stabil mesaj iletimi. \n- biraz daha iyi gruplar. \n- ve daha fazlası!</string> - <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Sadece istemci cihazlar <b>2 katmanlı uçtan uca şifreleme</b> ile kullanıcı profillerini, kişileri, grupları ve gönderilen mesajları depolar.]]></string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Sadece istemci cihazlar kullanıcı profillerini, kişileri, grupları ve gönderilen mesajları depolar.</string> <string name="only_group_owners_can_change_prefs">Grup tercihlerini sadece grup sahipleri değiştirebilir.</string> <string name="item_info_no_text">metin yok</string> <string name="network_status">Ağ durumu</string> @@ -955,7 +940,7 @@ <string name="change_lock_mode">Kilit modunu değiştir</string> <string name="trying_to_connect_to_server_to_receive_messages">Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor.</string> <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Lütfen doğru bağlantıyı kullandığınızı kontrol edin veya irtibat kişinizden size başka bir bağlantı göndermesini isteyin.</string> - <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Gizliliğinizi korumak için, anlık bildirimler yerine <b>SimpleX arka plan hizmeti</b> kullanılır - günde pilin yüzde birkaçını kullanır.]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Gizliliğinizi korumak için anlık bildirimler kullanmak yerine, <b>SimpleX arka planda çalışır</b>.]]></string> <string name="periodic_notifications">Periyodik bildirimler</string> <string name="periodic_notifications_disabled">Periyodik bildirimler devre dışı</string> <string name="enter_passphrase_notification_desc">Bildirimleri almak için lütfen veri tabanı parolasını girin</string> @@ -964,7 +949,7 @@ <string name="la_notice_turn_on">Aç</string> <string name="la_please_remember_to_store_password">Hatırlayın veya güvenli bir şekilde saklayın - kaybolan bir parolayı kurtarmanın bir yolu yoktur!</string> <string name="auth_open_chat_console">Sohbet konsolunu aç</string> - <string name="auth_open_chat_profiles">Sohbet profillerini aö</string> + <string name="auth_open_chat_profiles">Sohbet profillerini değiştir.</string> <string name="this_text_is_available_in_settings">Bu metin ayarlarda mevcut</string> <string name="no_filtered_chats">Filtrelenmiş sohbet yok</string> <string name="images_limit_title">Çok fazla görsel!</string> @@ -976,7 +961,7 @@ <string name="to_connect_via_link_title">Link ile bağlanmak için</string> <string name="this_link_is_not_a_valid_connection_link">Bu geçerli bir bağlantı linki değil</string> <string name="this_QR_code_is_not_a_link">Bu QR kodu bir bağlantı değil!</string> - <string name="read_more_in_user_guide_with_link"><![CDATA[Daha fazla bilgi için <font color=#0088ff>Kullanıcı Kılavuzu</font>.]]></string> + <string name="read_more_in_user_guide_with_link"><![CDATA[Daha fazla bilgi için <font color="#0088ff">Kullanıcı Kılavuzu</font>.]]></string> <string name="paste_button">Yapıştır</string> <string name="this_string_is_not_a_connection_link">Bu dize bir bağlantı linki değil!</string> <string name="rate_the_app">Uygulamaya puan verin</string> @@ -996,15 +981,14 @@ <string name="v4_5_private_filenames_descr">Zaman dilimi, görsel/ses korumak için UTC kullan.</string> <string name="v4_5_private_filenames">Özel dosya adları</string> <string name="to_start_a_new_chat_help_header">Yeni bir sohbet başlatmak için</string> - <string name="people_can_connect_only_via_links_you_share">İnsanlar size sadece paylaştığınız bağlantılar üzerinden ulaşabilir.</string> + <string name="people_can_connect_only_via_links_you_share">Kimin bağlanabileceğine siz karar verirsiniz.</string> <string name="privacy_redefined">Gizlilik yeniden tanımlanıyor</string> - <string name="read_more_in_github">GitHub repomuzda daha fazlasını okuyun.</string> <string name="onboarding_notifications_mode_periodic">Periyodik</string> <string name="onboarding_notifications_mode_title">Gizli bildirimler</string> - <string name="paste_the_link_you_received">Alınan linki yapıştır</string> + <string name="paste_the_link_you_received">Aldığın bağlantıyı yapıştır</string> <string name="unknown_database_error_with_info">Bilinmeyen veri tabanı hatası: %s</string> <string name="open_chat">Sohbeti aç</string> - <string name="restore_database_alert_desc">Veritabanı yedeğini geri yükledikten sonra önceki şifreyi girin. Bu işlem geri alınamaz.</string> + <string name="restore_database_alert_desc">Veritabanı yedeğini geri yükledikten sonra önceki şifreyi girin. Bu eylem geri alınamaz.</string> <string name="network_option_ping_interval">PING aralığı</string> <string name="network_option_protocol_timeout_per_kb">KB başına protokol zaman aşımı</string> <string name="unhide_chat_profile">Sohbet profilini gizlemeyi kaldır</string> @@ -1013,14 +997,14 @@ <string name="prohibit_message_reactions">Mesaj tepkilerini yasakla.</string> <string name="prohibit_sending_voice_messages">Sesli mesaj göndermeyi yasakla.</string> <string name="prohibit_message_deletion">Geri alınamaz mesaj silme işlemini yasakla.</string> - <string name="prohibit_direct_messages">Üyelere direkt mesaj göndermeyi yasakla.</string> + <string name="prohibit_direct_messages">Üyelere doğrudan mesaj göndermeyi yasakla.</string> <string name="prohibit_sending_files">Dosya ve medya göndermeyi yasakla.</string> <string name="v4_4_live_messages">Canlı mesajlar</string> <string name="v5_0_polish_interface">Arayüz geliştirildi</string> <string name="unfavorite_chat">Favorilerden çıkar</string> <string name="make_profile_private">Sohbeti gizli yap!</string> <string name="profile_update_will_be_sent_to_contacts">Profil güncellemesi kişilerinize gönderilecektir.</string> - <string name="read_more_in_github_with_link"><![CDATA[<font color=#0088ff>GitHub repomuzda</font> daha fazlasını okuyun.]]></string> + <string name="read_more_in_github_with_link"><![CDATA[<font color="#0088ff">GitHub repomuzda</font> daha fazlasını okuyun.]]></string> <string name="alert_text_fragment_please_report_to_developers">Lütfen geliştiricilere bildirin.</string> <string name="users_delete_with_connections">Profil ve sunucu bağlantıları</string> <string name="user_unhide">gizlemeyi kaldır</string> @@ -1038,7 +1022,7 @@ <string name="v4_5_message_draft">Mesaj taslağı</string> <string name="v4_6_hidden_chat_profiles_descr">Sohbet profillerini parola ile koru!</string> <string name="v4_5_reduced_battery_usage">Daha az pil kullanımı</string> - <string name="to_protect_privacy_simplex_has_ids_for_queues">Gizliliği korumak için, diğer tüm platformlar gibi kullanıcı kimliği kullanmak yerine, SimpleX mesaj kuyrukları için kişilerinizin her biri için ayrı tanımlayıcılara sahiptir.</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">Gizliliği korumak için, SimpleX her bir konuşma için farklı bir ID kullanır.</string> <string name="trying_to_connect_to_server_to_receive_messages_with_error">Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor (hata: %1$s).</string> <string name="v4_4_live_messages_desc">Alıcılar güncellemeleri siz yazdıkça görürler.</string> <string name="auth_log_in_using_credential">Bilgilerinizi kullanarak giriş yapın</string> @@ -1050,7 +1034,6 @@ <string name="stop_file__action">Dosyayı durdur</string> <string name="error_alert_title">Hata</string> <string name="create_another_profile_button">ProfilProfil oluştur</string> - <string name="stop_chat_to_enable_database_actions">Veri tabanı eylemlerini etkinleştirmek için sohbeti durdur.</string> <string name="stop_snd_file__title">Dosya göndermeyi durdur?</string> <string name="auth_stop_chat">Sohbeti durdur</string> <string name="connect_use_current_profile">Mevcut profili kullan</string> @@ -1063,10 +1046,1181 @@ <string name="tap_to_activate_profile">Profili etkinleştirmek için dokun.</string> <string name="stop_chat_confirmation">Durdur</string> <string name="stop_sharing_address">Adres paylaşmayı durdur?</string> - <string name="send_receipts_disabled">kapalı</string> + <string name="send_receipts_disabled">devre dışı</string> <string name="turn_off_battery_optimization_button">İzin ver</string> <string name="error">Hata</string> <string name="stop_sharing">Paylaşmayı durdur</string> <string name="stop_rcv_file__title">Dosya almayı durdur?</string> <string name="stop_chat_question">Sohbeti durdur?</string> -</resources> \ No newline at end of file + <string name="expand_verb">Genişlet</string> + <string name="connect_plan_repeat_connection_request">Bağlantı isteğini tekrarla?</string> + <string name="no_details">detay yok</string> + <string name="database_initialization_error_desc">Veri tabanı düzgün çalışmıyor. Daha fazla bilgi için dokunun</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Açık kaynaklı protokol ve kod - sunucuları herkes çalıştırabilir.</string> + <string name="rcv_group_event_open_chat">Aç</string> + <string name="settings_notification_preview_mode_title">Önizlemeyi göster</string> + <string name="v5_3_encrypt_local_files">Depolanan dosyaları ve medyayı şifrele</string> + <string name="only_you_can_send_voice">Sadece sen sesli mesaj gönderebilirsin.</string> + <string name="update_network_settings_confirmation">Güncelle</string> + <string name="database_backup_can_be_restored">Veritabanı parolasını değiştirme girişimi tamamlanmadı.</string> + <string name="update_database_passphrase">Veri tabanı parolasını güncelle</string> + <string name="turn_off_system_restriction_button">Uygulama ayarlarını aç</string> + <string name="enable_automatic_deletion_message">Bu eylem geri alınamaz - seçilenden daha önce gönderilen ve alınan mesajlar silinecektir. Birkaç dakika sürebilir.</string> + <string name="create_group_button">Grup oluştur</string> + <string name="custom_time_picker_select">Seç</string> + <string name="only_you_can_make_calls">Sadece sen arama yapabilirsin.</string> + <string name="new_mobile_device">Yeni mobil cihaz</string> + <string name="use_chat">Sohbeti kullan</string> + <string name="multicast_connect_automatically">Otomatik olarak bağlan</string> + <string name="share_verb">Paylaş</string> + <string name="rcv_group_event_n_members_connected">%s, %s ve %d diğer üye bağlandı</string> + <string name="only_your_contact_can_make_calls">Sadece senin kişin arama yapabilir.</string> + <string name="desktop_address">Masaüstü adresi</string> + <string name="smp_servers_test_some_failed">Bazı sunucular testi geçemedi:</string> + <string name="chat_with_the_founder">Sorularınızı ve fikirlerinizi gönderin</string> + <string name="delete_files_and_media_desc">Bu eylem geri alınamaz - tüm alınan ve gönderilen dosyalar ve medyalar silinecek. Düşük çözünürlükteki resimlar kalacaktır.</string> + <string name="encrypt_local_files">Yerel dosyaları şifrele</string> + <string name="ntf_channel_calls">SimpleX Chat aramaları</string> + <string name="v5_3_new_desktop_app">Yeni masaüstü uygulaması!</string> + <string name="v5_3_new_interface_languages">6 yeni arayüz dili</string> + <string name="notification_preview_mode_contact_desc">Sadece kişiyi göster</string> + <string name="connect_plan_group_already_exists">Grup zaten mevcut!</string> + <string name="icon_descr_speaker_on">Hoparlör açık</string> + <string name="only_your_contact_can_send_voice">Sadece senin kişin sesli mesaj gönderebilir.</string> + <string name="in_developing_desc">Bu özellik henüz desteklenmiyor. Bir sonraki sürümü deneyin.</string> + <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Mobile uygulamadan <i>Masaüstünden kullan</i> seçeneğini aç ve karekodu okut.]]></string> + <string name="thank_you_for_installing_simplex">SimpleX Chat\'i yüklediğiniz için teşekkürler!</string> + <string name="skip_inviting_button">Üyeleri davet etmeyi atla</string> + <string name="connect_plan_already_connecting">Zaten bağlanılıyor!</string> + <string name="icon_descr_send_message">Mesaj Gönder</string> + <string name="contact_you_shared_link_with_wont_be_able_to_connect">Bu bağlantıyı paylaştığınız kişi bağlanamayacak!</string> + <string name="desktop_incompatible_version">Uyumsuz sürüm</string> + <string name="new_desktop"><![CDATA[<i>(yeni)</i>]]></string> + <string name="notification_preview_mode_message_desc">Kişiyi ve mesajı göster</string> + <string name="v5_4_better_groups">Daha iyi gruplar</string> + <string name="video_decoding_exception_desc">Videonun kodu çözülemiyor. Lütfen farklı bir video deneyin veya geliştiricilerle iletişime geçin.</string> + <string name="non_fatal_errors_occured_during_import">İçe aktarma sırasında bazı önemli olmayan hatalar oluştu:</string> + <string name="show_developer_options">Geliştirici seçeneklerini göster</string> + <string name="rcv_group_event_1_member_connected">%s bağlandı</string> + <string name="network_disable_socks_info">Onaylarsanız, mesajlaşma sunucuları IP adresinizi ve sağlayıcınızı - hangi sunuculara bağlandığınızı - görebilecektir.</string> + <string name="share_with_contacts">Kişilerle paylaş</string> + <string name="chat_item_ttl_seconds">%s saniye (sn)</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="system_restricted_background_desc">SimpleX arka planda çalışamaz. Bildirimleri sadece uygulama çalışırken alırsınız.</string> + <string name="connect_plan_connect_via_link">Bağlantı ile bağlanılsın mı?</string> + <string name="database_encryption_will_be_updated_in_settings">Veri tabanı şifreleme parolası güncellenecek ve ayarlarda depolanacaktır.</string> + <string name="connect_plan_already_joining_the_group">Zaten gruba bağlanılıyor!</string> + <string name="group_members_n">%s, %s ve %d üye</string> + <string name="this_device">Bu cihaz</string> + <string name="share_address_with_contacts_question">Adresi kişilerle paylaş?</string> + <string name="system_restricted_background_in_call_desc">Uygulama arka planda 1 dakika kaldıktan sonra kapatılabilir.</string> + <string name="text_field_set_contact_placeholder">Kişi ismini ayarla…</string> + <string name="send_us_an_email">Bize e-posta gönder</string> + <string name="v4_2_security_assessment_desc">SimpleX Chat\'in güvenliği Trail of Bits tarafından denetlenmiştir.</string> + <string name="simplex_service_notification_title">SimpleX Chat hizmeti</string> + <string name="is_verified">%s onaylı</string> + <string name="password_to_show">Göserilecek parola</string> + <string name="incognito_info_allows">Tek bir sohbet profilinde, aralarında herhangi bir veri paylaşımı olmadan birden fazla anonim bağlantı kurmaya izin verir.</string> + <string name="select_contacts">Kişileri seç</string> + <string name="v4_6_chinese_spanish_interface_descr">Kullanıcılara teşekkürler - Weblate aracılığıyla katkıda bulunun!</string> + <string name="v5_3_simpler_incognito_mode">Basitleştirilmiş gizli mod</string> + <string name="update_network_settings_question">Ağ ayarları güncellensin mi?</string> + <string name="shutdown_alert_question">Kapat?</string> + <string name="you_invited_a_contact">Bir kişiyi davet ettin</string> + <string name="is_not_verified">%s onaylı değil</string> + <string name="this_device_name">Bu cihazın ismi</string> + <string name="smp_server_test_upload_file">Dosya yükle</string> + <string name="network_use_onion_hosts_required">Gerekli</string> + <string name="ntf_channel_messages">SimpleX Chat mesajlar</string> + <string name="share_link">Bağlantı paylaş</string> + <string name="icon_descr_simplex_team">SimpleX Ekibi</string> + <string name="rcv_group_event_3_members_connected">%s, %s ve %s bağlandı</string> + <string name="settings_section_title_socks">SOCKS VEKİLİ</string> + <string name="desktop_devices">Masaüstür cihazlar</string> + <string name="smp_servers">SMP sunucuları</string> + <string name="not_compatible">Uyumlu değil!</string> + <string name="v4_4_verify_connection_security">Bağlantı güvenliğini onayla</string> + <string name="connect_with_contact_name_question">%1$s ile bağlan?</string> + <string name="remove_member_button">Üyeyi çıkar</string> + <string name="open_database_folder">Veri tabanı dosyasını aç</string> + <string name="settings_section_title_use_from_desktop">Masaüstünden kullan</string> + <string name="network_proxy_port">port %d</string> + <string name="share_address">Adres paylaş</string> + <string name="connect_plan_repeat_join_request">Bağlantı talebini tekrarla?</string> + <string name="button_remove_member_question">Üyeyi çıkar?</string> + <string name="verify_security_code">Güvenlik kodunu onayla</string> + <string name="show_dev_options">Göster:</string> + <string name="receipts_section_groups">Küçük gruplar (maks 20)</string> + <string name="v5_3_new_interface_languages_descr">Arapça, Bulgarca, Fince, İbranice, Tayca ve Ukraynaca - kullanıcılar ve Weblate sayesinde.</string> + <string name="port_verb">Port</string> + <string name="connection_you_accepted_will_be_cancelled">Kabul ettiğiniz bağlantı iptal edilecek!</string> + <string name="connect_plan_open_group">Grubu aç</string> + <string name="privacy_show_last_messages">Son mesajları göster</string> + <string name="this_device_version"><![CDATA[<i>bu cihaz s%s</i>]]></string> + <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> + <string name="update_database">Güncelle</string> + <string name="this_device_name_shared_with_mobile">Cihaz ismi bağlı olduğu mobil istemci ile paylaşılacak.</string> + <string name="v5_3_new_desktop_app_descr">Masaüstü uygulamasında yeni bir profil oluştur. 💻</string> + <string name="set_contact_name">Kişi ismini ayarla</string> + <string name="verify_code_on_mobile">Kodu mobilde onayla</string> + <string name="share_image">Medya paylaş…</string> + <string name="passphrase_will_be_saved_in_settings">Parola, siz onu değiştirdikten veya uygulamayı yeniden başlattıktan sonra ayarlarda düz metin olarak depolanacak.</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Kişinizle bağlantı kurmak için aldığınız bağlantıyı yapıştırın…</string> + <string name="settings_shutdown">Kapat</string> + <string name="icon_descr_sent_msg_status_sent">gönderildi</string> + <string name="send_live_message">Canlı mesaj gönder</string> + <string name="enter_this_device_name">Bu cihazın adını girin…</string> + <string name="smp_servers_per_user">Mevcut sohbet profilinizin yeni bağlantıları için sunucular</string> + <string name="show_QR_code">QR kodu göster</string> + <string name="network_socks_proxy_settings">SOCKS proxy ayarları</string> + <string name="rcv_group_events_count">%d grup etkinlikleri</string> + <string name="invalid_name">Geçersiz isim!</string> + <string name="la_lock_mode">SimpleX Kilit modu</string> + <string name="share_file">Dosya paylaş…</string> + <string name="periodic_notifications_desc">Uygulama yeni mesajları periyodik olarak alır - günde pilin yüzde birkaçını kullanır. Uygulama anlık bildirimleri kullanmaz - cihazınızdan gelen veriler sunuculara gönderilmez.</string> + <string name="first_platform_without_user_ids">Herhangi bir kullanıcı tanımlayıcısı yok.</string> + <string name="icon_descr_speaker_off">Hoparlör kapalı</string> + <string name="sync_connection_force_desc">Şifreleme çalışıyor ve yeni bir şifreleme anlaşması gerekli değil. Yoksa bağlantı hataları ortaya çıkabilir!</string> + <string name="show_call_on_lock_screen">Göster</string> + <string name="send_verb">Gönder</string> + <string name="paste_desktop_address">Masaüstü adresini yapıştır</string> + <string name="verify_code_with_desktop">Kodu masaüstü ile onayla</string> + <string name="scan_qr_code_from_desktop">Masaüstünden karekodu tara</string> + <string name="image_descr_simplex_logo">SimpleX logo</string> + <string name="devices">Cihazlar</string> + <string name="delete_chat_profile_action_cannot_be_undone_warning">Bu eylem geri alınamaz - profiliniz, kişileriniz, mesajlarınız ve dosyalarınız geri döndürülemez şekilde kaybolacaktır.</string> + <string name="v5_4_more_things_descr">- isteğe bağlı olarak silinen kişileri bildirme. +\n- boşluklu profil adları. +\n- ve daha fazlası!</string> + <string name="la_notice_title_simplex_lock">SimpleX Kilit</string> + <string name="connect_via_member_address_alert_desc">Bu grup üyesine bağlantı isteği gönderilecek.</string> + <string name="only_your_contact_can_add_message_reactions">Sadece senin kişin mesaj tepkileri ekleyebilir.</string> + <string name="settings_is_storing_in_clear_text">Parola, ayarlarda düz metin olarak saklanır.</string> + <string name="auth_simplex_lock_turned_on">SimpleX Kilit aktif</string> + <string name="terminal_always_visible">Yeni pencerede konsolu göster</string> + <string name="scan_from_mobile">Mobilden tara</string> + <string name="verify_connections">Bağlantıları onayla</string> + <string name="share_message">Mesaj paylaş…</string> + <string name="alert_text_msg_bad_hash">Önceki mesajın hash\'i farklı.\"</string> + <string name="lock_not_enabled">SimpleX Kilit aktif değil!</string> + <string name="chat_lock">SimpleX Kilit</string> + <string name="connect_via_member_address_alert_title">Doğrudan bağlanılsın mı?</string> + <string name="receipts_section_description">Bu ayarlar mevcut profiliniz içindir</string> + <string name="smp_servers_test_failed">Sunucu testi başarısız!</string> + <string name="verify_connection">Bağlantıyı onayla</string> + <string name="add_contact_or_create_group">Yeni sohbet başlat</string> + <string name="connect__your_profile_will_be_shared">Profiliniz %1$s paylaşılacaktır.</string> + <string name="v5_4_incognito_groups_descr">Rastgele bir profil kullanarak grup oluştur.</string> + <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Gruba zaten bu bağlantı üzerinden katılıyorsunuz.</string> + <string name="your_chat_profile_will_be_sent_to_your_contact">Sohbet profiliniz kişinize +\ngönderilecek</string> + <string name="invite_prohibited_description">Gizli bir profil paylaştığınız kişiyi ana profilinizi kullandığınız gruba davet etmeye çalışıyorsunuz</string> + <string name="rcv_conn_event_verification_code_reset">güvenlik kodu değiştirildi</string> + <string name="v4_6_audio_video_calls_descr">Bluetooth desteği ve diğer iyileştirmeler.</string> + <string name="icon_descr_settings">Ayarlar</string> + <string name="settings_section_title_settings">AYARLAR</string> + <string name="compose_send_direct_message_to_connect">Bağlanmak için doğrudan mesaj gönderin</string> + <string name="security_code">Güvenlik kodu</string> + <string name="v5_4_better_groups_descr">Daha hızlı gruplara katılma ve daha güvenilir mesajlar.</string> + <string name="group_main_profile_sent">Sohbet profiliniz grup üyelerine gönderilecek</string> + <string name="connect_plan_connect_to_yourself">Kendine bağlan?</string> + <string name="notifications_mode_periodic">Periyodik olarak başlar</string> + <string name="connect_plan_this_is_your_own_simplex_address">Bu sizin kendi SimpleX adresiniz!</string> + <string name="v4_3_irreversible_message_deletion_desc">Kişileriniz tam mesaj silme işlemine izin verebilir.</string> + <string name="icon_descr_sent_msg_status_send_failed">gönderme başarısız</string> + <string name="sender_may_have_deleted_the_connection_request">Gönderen kişi bağlantı isteğini silmiş olabilir.</string> + <string name="create_chat_profile">Sohbet profili oluştur</string> + <string name="error_sending_message_contact_invitation">Davetiye gönderirken hata</string> + <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Sohbeti uygulama Ayarları/Veritabanı üzerinden veya uygulamayı yeniden başlatarak başlatabilirsiniz.</string> + <string name="connect_to_desktop">Masaüstüne bağlan</string> + <string name="button_send_direct_message">Doğrudan mesaj gönder</string> + <string name="connect_plan_you_have_already_requested_connection_via_this_address">Bu adres üzerinden zaten bağlantı talebinde bulundunuz!</string> + <string name="member_contact_send_direct_message">doğrudan mesaj gönder</string> + <string name="sender_cancelled_file_transfer">Gönderen kişi dosya aktarımını iptal etti.</string> + <string name="only_you_can_delete_messages">Mesajları yalnızca siz geri döndürülemez şekilde silebilirsiniz (kişiniz bunları silinmek üzere işaretleyebilir). (24 saat içinde)</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Zaten <b>%1$s</b>’ye bağlanıyorsunuz.]]></string> + <string name="whats_new_thanks_to_users_contribute_weblate">Kullanıcılara teşekkürler - Weblate aracılığıyla katkıda bulunun!</string> + <string name="lock_mode">Kilit modu</string> + <string name="only_one_device_can_work_at_the_same_time">Aynı anda yalnızca bir cihaz çalışabilir</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Zaten <b>%1$s</b> grubuna katılıyorsunuz.]]></string> + <string name="group_invitation_tap_to_join">Katılmak için dokun</string> + <string name="v5_0_polish_interface_descr">Kullanıcılara teşekkürler - Weblate aracılığıyla katkıda bulunun!</string> + <string name="v5_4_link_mobile_desktop_descr">Güvenli kuantum dirençli protokol ile.</string> + <string name="star_on_github">Bize GitHub\'da yıldız verin</string> + <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Arka planda 30 saniye kaldıktan sonra uygulamayı başlattığınızda veya devam ettirdiğinizde kimlik doğrulaması yapmanız gerekecektir.</string> + <string name="v4_5_italian_interface_descr">Kullanıcılara teşekkürler - Weblate aracılığıyla katkıda bulunun!</string> + <string name="waiting_for_desktop">Masaüstü bekleniyor…</string> + <string name="database_is_not_encrypted">Sohbet veritabanınız şifreli değildir - korumak için parola ayarlayın.</string> + <string name="you_will_still_receive_calls_and_ntfs">Aktif olduklarında sessize alınmış profillerden arama ve bildirim almaya devam edersiniz.</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Zaten<b> %1$s </b>grubundasınız.]]></string> + <string name="v4_4_french_interface_descr">Kullanıcılara teşekkürler - Weblate aracılığıyla katkıda bulunun!</string> + <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">Bu tek seferlik bağlantı üzerinden zaten bağlanıyorsunuz!</string> + <string name="non_content_uri_alert_text">Geçersiz bir dosya yolu paylaştınız. Sorunu uygulama geliştiricilerine bildirin.</string> + <string name="disable_notifications_button">Bildirimleri devre dışı bırak</string> + <string name="non_content_uri_alert_title">Geçersiz dosya yolu</string> + <string name="only_your_contact_can_delete">Yalnızca kişiniz mesajları geri alınamaz şekilde silebilir (silinmeleri için işaretleyebilirsiniz). (24 saat içinde)</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Veritabanı şifrelenecek ve parola ayarlarda depolanacak.</string> + <string name="v5_4_block_group_members">Grup üyelerini engelle</string> + <string name="feature_received_prohibited">alınmış, yasaklanmış</string> + <string name="error_xftp_test_server_auth">Sunucunun yükleme yapması için yetkilendirilmesi gerekli, şifreyi kontrol et</string> + <string name="encryption_renegotiation_error">Şifreleme yeniden aşma hatası</string> + <string name="smp_servers_preset_server">Ön ayarlı sunucu</string> + <string name="feature_cancelled_item">%s iptal edildi</string> + <string name="rcv_direct_event_contact_deleted">silinmiş kişi</string> + <string name="network_option_tcp_connection_timeout">TCP bağlantısı zaman aşımına uğradı</string> + <string name="mtr_error_different">uygulama/veritabanı içinde farklı değişim: %s / %s</string> + <string name="using_simplex_chat_servers">SimpleX Chat serverları kullanılıyor.</string> + <string name="smp_servers_preset_add">Ön ayarlı sunucular ekle</string> + <string name="secret_text">gizli</string> + <string name="receipts_groups_disable_for_all">Bütün gruplar için devre dışı bırak</string> + <string name="v4_6_group_welcome_message_descr">Yeni üyeler için gösterilecek mesajı seç!</string> + <string name="stop_snd_file__message">Dosya gönderimi durdurulacaktır.</string> + <string name="moderated_description">yönetildi</string> + <string name="error_creating_member_contact">Üye bağlantısı oluşturulurken hata</string> + <string name="sending_delivery_receipts_will_be_enabled">Tüm kişiler için iletim bilgisi gönderme özelliği etkinleştirilecek</string> + <string name="ensure_xftp_server_address_are_correct_format_and_unique">XFTP sunucu adreslerinin doğru formatta olduğundan, satırın ayrılmış ve kopyalanmamış olduğundan emin olun.</string> + <string name="refresh_qr_code">Yenile</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Lütfen unutmayın</b>: mesaj ve dosya yönlendiricileri SOCKS vekili tarafından bağlandı. Aramalar ve bağlantı ön gösterimleri doğrudan bağlantı kullanıyor.]]></string> + <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱mobil: <b>Telefon uygulamasında aç</b> seçeneğine tıkla, sonra uygulama içinden <b>Bağlan</b> seçeneğine tıkla.]]></string> + <string name="smp_server_test_secure_queue">Gizli sıra</string> + <string name="connected_desktop">Masaüstü bağlandı</string> + <string name="group_members_2">%s ve %s</string> + <string name="v5_2_fix_encryption">Bağlantılarını koru</string> + <string name="received_message">Alınmış mesaj</string> + <string name="you_can_make_address_visible_via_settings">Ayarlardan SimpleX kişilerinize görünür yapabilirsiniz.</string> + <string name="connect_plan_join_your_group">Grubuna bağlanılsın mı?</string> + <string name="v5_4_link_mobile_desktop">Telefon veya bilgisayar uygulamalarını bağla! 🔗</string> + <string name="connect__a_new_random_profile_will_be_shared">Yeni bir rasgele profil paylaşılacaktır.</string> + <string name="receipts_contacts_override_enabled">Alıcılar %d bağlantıları için etkinleştirilecektir</string> + <string name="moderate_message_will_be_marked_warning">Mesaj herkes için yönetilmiş olarak işaretlenecek.</string> + <string name="connect_plan_this_is_your_own_one_time_link">Bu senin kendi tek-kullanımlık bağlantın!</string> + <string name="marked_deleted_items_description">%d mesaj silinmiş olarak işaretlendi</string> + <string name="connect_use_new_incognito_profile">Yeni sahte profil kullan</string> + <string name="chat_is_stopped_you_should_transfer_database">Sohbet durduruldu. Eğer çoktan başka bir cihazda bu veritabanını kullandıysan,sohbeti başlatmadan önce veritabanını geri aktarmalısın.</string> + <string name="send_link_previews">Bağlantı ön izlemelerini gönder</string> + <string name="sending_via">Aracılığıyla gönderiliyor</string> + <string name="icon_descr_sent_msg_status_unauthorized_send">yetkisiz gönder</string> + <string name="v4_5_transport_isolation">Ulaşım izolasyonu</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Çoklu gönderim bütün görünebilir sohbet profillerinde etkinleştirilecektir.</string> + <string name="system_restricted_background_in_call_warn"><![CDATA[Arkaplanda arama yapmak için, lütfen uygulama ayarlarından <b>Uygulama pil kullanımı</b> / <b>Sınırsız</b> seçeneğini seç.]]></string> + <string name="callstate_received_confirmation">onaylama alındı…</string> + <string name="share_text_updated_at">Kayıt %s te güncellendi</string> + <string name="v5_3_encrypt_local_files_descr">Uygulama yeni yerel dosyaları şifreler (videolar dışında).</string> + <string name="sender_at_ts">%s , %s de</string> + <string name="receipts_contacts_title_disable">Alıcılar devre dışı bırakılsın mı?</string> + <string name="sync_connection_force_question">Bağlantı yeniden senkronizasyonunu şifrele?</string> + <string name="receipts_section_description_1">Grup ayarlarından ve kişilerden geçersiz kılınmış olabilirler</string> + <string name="recent_history_is_not_sent_to_new_members">Yeni üyelere geçmiş gönderilmedi.</string> + <string name="v4_2_security_assessment">Güvenlik değerlendirmesi</string> + <string name="retry_verb">Yeniden dene</string> + <string name="network_enable_socks_info">Sunuculara %d bağlantı noktasındaki vekil SOCKS aracılığıyla erişilsin mi? Vekil bu seçeneği etkinleştirmeden önce başlatılmak zorundadır.</string> + <string name="v5_4_block_group_members_descr">İstenmeyen mesajları gizlemek için.</string> + <string name="error_smp_test_failed_at_step">Test %s adımında hata yaşandı.</string> + <string name="disable_onion_hosts_when_not_supported"><![CDATA[<i> .onion ana bilgisayarlarını kullan</i> seçeneğini eğer SOCKS vekili destek vermiyorsa devre dışı bırak]]></string> + <string name="you_can_change_it_later">Rasgele şifreler düz metin olarak ayarlarda saklanacaktır. +\nBunu sonra değiştirebilirsin.</string> + <string name="ensure_smp_server_address_are_correct_format_and_unique">SMP sunucu adreslerinin doğru formatta olduğundan, satırın ayrılmış ve kopyalanmamış olduğundan emin olun.</string> + <string name="group_invitation_tap_to_join_incognito">Sahte İsimle katılmak için tıkla</string> + <string name="network_use_onion_hosts_no_desc">Onion ana bilgisayarları kullanılmayacaktır.</string> + <string name="set_password_to_export">Dışarıya çıkarmak için parola belirle</string> + <string name="snd_conn_event_ratchet_sync_started">%s için şifreleme kabul ediliyor…</string> + <string name="receipts_groups_override_disabled">Alıcılar %d grupları için devre dışı bırakılacak</string> + <string name="notification_new_contact_request">Yeni bağlantı isteği</string> + <string name="unlink_desktop_question">Bilgisayarla bağlantıyı kes?</string> + <string name="camera_not_available">Kamera hazır değil</string> + <string name="use_simplex_chat_servers__question">SimpleX Chat sunucularını kullan?</string> + <string name="linked_desktop_options">Bağlanmış bilgisayar ayarları</string> + <string name="developer_options">Veritabanı kimlikleri ve Taşıma izolasyonu seçeneği.</string> + <string name="network_option_ping_count">PING sayısı</string> + <string name="update_network_session_mode_question">Ulaşım izolasyonu modu güncellensin mi?</string> + <string name="random_port">Rasgele</string> + <string name="linked_desktops">Bağlanmış bilgisayarlar</string> + <string name="discover_on_network">Yerel ağ aracılığıyla keşfet</string> + <string name="muted_when_inactive">Aktif değilken susturuldu!</string> + <string name="rcv_group_and_other_events">ve %d diğer etkinlikler</string> + <string name="network_session_mode_transport_isolation">Ulaşım izolazyonu</string> + <string name="v5_3_discover_join_groups">Keşfet ve gruplara katıl</string> + <string name="v5_4_incognito_groups">Gizli gruplar</string> + <string name="user_unmute">Susturmayı kaldır</string> + <string name="share_text_sent_at">%s de gönderildi</string> + <string name="enable_sending_recent_history">Yeni katılımcılara 100e kadar son mesajlar gönder</string> + <string name="moderate_verb">Yönet</string> + <string name="moderated_items_description">%1$d mesaj %2$s tarafından yönetildi</string> + <string name="remove_passphrase_from_settings">Ayarlardaki parola silinsin mi?</string> + <string name="receipts_contacts_title_enable">Alıcılar etkinleştirilsin mi?</string> + <string name="tap_to_start_new_chat">Yeni bir sohbet başlatmak için tıkla</string> + <string name="unblock_member_button">Kişinin engelini kaldır</string> + <string name="relay_server_if_necessary">Yönlendirici sunucusu sadece lazım ise kullanılacak. Diğer taraf IP adresini görebilir.</string> + <string name="remote_host_was_disconnected_toast"><![CDATA[Telefon bağlantılı <b>%s</b> ın bağlantısı kesildi]]></string> + <string name="smp_servers_test_server">Sunucuyu test et</string> + <string name="conn_stats_section_title_servers">SUNUCULAR</string> + <string name="smp_servers_test_servers">Sunucuları test et</string> + <string name="privacy_message_draft">Mesaj taslağı</string> + <string name="v5_2_disappear_one_message">Bir mesajı yok edin</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Kişi ekle</b> yeni bir davet bağlantısı oluşturmak için, veya aldığın bir davet bağlantısından bağlan.]]></string> + <string name="disable_sending_recent_history">Yeni üyelere geçmişi gönderme.</string> + <string name="icon_descr_contact_checked">Kişi doğrulandı</string> + <string name="use_random_passphrase">Rasgele parola kullan</string> + <string name="v5_0_app_passcode_descr">Sistem yetkilendirilmesi yerine ayarla.</string> + <string name="run_chat_section">SOHBETİ ÇALIŞTIR</string> + <string name="network_disable_socks">Direkt internet bağlantısı kullan?</string> + <string name="rcv_group_event_updated_group_profile">grup profili güncellendi</string> + <string name="network_use_onion_hosts_required_desc">Onion ana bilgisayarları bağlantı için gerekli olacaktır. +\nLütfen unutmayın: artık .onion adresi olmayan sunuculara bağlanamayacaksınız.</string> + <string name="switch_receiving_address_desc">Alınan adres başka bir sunucuda değiştirilecektir. Adres değişimi gönderen çevrimiçi olunca tamamlanacaktır.</string> + <string name="error_smp_test_server_auth">Sunucunun sıralar oluşturması için yetkilendirilmesi gerekli, şifreyi kontrol et</string> + <string name="system_restricted_background_in_call_title">Arkaplan araması yok</string> + <string name="group_is_decentralized">Tamamiyle merkezi olmayan - sadece üyelere görünür.</string> + <string name="error_showing_content">içerik gösterilirken hata</string> + <string name="or_show_this_qr_code">Veya bu kodu göster</string> + <string name="above_then_preposition_continuation">üzerinde, ardından:</string> + <string name="linked_mobiles">Bağlanmış telefonlar</string> + <string name="recent_history_is_sent_to_new_members">Yeni üyelere 100e kadar en son mesajlar gönderildi.</string> + <string name="sent_message">Gönderilmiş mesaj</string> + <string name="save_passphrase_in_settings">Ayarlarda parolayı kaydet</string> + <string name="upgrade_and_open_chat">Yükselt ve sohbeti aç</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">Tarattığın kod SimpleX QR kodu bağlantısı değil.</string> + <string name="send_receipts_disabled_alert_msg">Bu grupta %1$d den fazla kişi var,çoklu gönderim yapılamıyor.</string> + <string name="connected_to_desktop">Masaüstüne bağlandı</string> + <string name="error_smp_test_certificate">Muhtemelen, sunucu adresindeki sertifika parmak izi doğru değil</string> + <string name="v5_2_message_delivery_receipts_descr">✅ özlediğimiz ikinci tik!</string> + <string name="clear_verification">Doğrulamayı temizle</string> + <string name="setup_database_passphrase">Veritabanı parolası ayarla</string> + <string name="the_text_you_pasted_is_not_a_link">Yapıştırdığın bağlantı bir SimpleX bağlantısı değil.</string> + <string name="color_sent_message">Gönderilmiş mesaj</string> + <string name="receipts_groups_title_disable">Gruplar için alıcılar devre dışı bırakılsın mı?</string> + <string name="relay_server_protects_ip">Yönlendirici sunucu IP adresinizi korur, ancak aramanın süresini gözlemleyebilir.</string> + <string name="loading_remote_file_title">Dosya yükleniyor</string> + <string name="connecting_to_desktop">Masaüstüne bağlanıyor</string> + <string name="no_contacts_to_add">Eklenecek kişi yok</string> + <string name="v4_5_message_draft_descr">Son mesaj taslağını saklayın, ekleriyle birlikte.</string> + <string name="found_desktop">Bilgisayar bulundu</string> + <string name="alert_text_encryption_renegotiation_failed">Şifreleme yeniden aşması başarısız oldu.</string> + <string name="smp_servers_add_to_another_device">Başka bir cihaza ekle</string> + <string name="feature_offered_item_with_param">%s: %2s teklif etti</string> + <string name="error_showing_message">mesaj gösterilirken hata</string> + <string name="accept_contact_incognito_button">Takma Ad ile kabul et</string> + <string name="receipts_groups_override_enabled">Alıcılar %d grupları için etkinleştirilecektir</string> + <string name="correct_name_to">İsmi %s olarak düzelt?</string> + <string name="sync_connection_force_confirm">Bağlantıyı yeniden senkronize etmek</string> + <string name="waiting_for_mobile_to_connect">Bağlanmak için telefon bekleniyor:</string> + <string name="enable_camera_access">Kamera erişimini etkinleştir</string> + <string name="delete_messages__question">%d mesajlarını sil?</string> + <string name="marked_deleted_description">silinmiş olarak işaretlenmiş</string> + <string name="you_can_view_invitation_link_again">Bağlantı detaylarından davet bağlantısını yeniden görüntüleyebilirsin.</string> + <string name="link_a_mobile">Bir telefon bağla</string> + <string name="keep_unused_invitation_question">Kullanılmamış daveti tut?</string> + <string name="start_chat_question">Sohbet başlatılsın mı?</string> + <string name="set_database_passphrase">Veritabanı parolası ayarla</string> + <string name="share_this_1_time_link">Bu tek-kullanımlık davet bağlantısını paylaş</string> + <string name="unblock_member_question">Kişinin engeli kaldırılsın mı?</string> + <string name="blocked_items_description">%d mesaj engellendi</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Grup oluştur</b>: yeni bir grup oluşturmak için.]]></string> + <string name="icon_descr_video_asked_to_receive">Videoyu almak için soruldu</string> + <string name="open_port_in_firewall_desc">Telefon uygulamanın bilgisayarına bağlanmasına izin vermek için, güvenlik duvarın etkin ise bu portu aç</string> + <string name="block_member_button">Üye engelle</string> + <string name="receipts_contacts_override_disabled">Alıcılar %d bağlantıları için devre dışı bırakılacak</string> + <string name="receipts_groups_enable_keep_overrides">Etkinleştir (grup geçersiz kılmalarını tut)</string> + <string name="system_restricted_background_warn"><![CDATA[Bildirimleri etkinleştirmek için, Lütfen uygulama ayarlarından <b>Uygulama pil kullanımı</b> / <b>Sınırsız</b> seçeneğini seç.]]></string> + <string name="send_receipts_disabled_alert_title">Alıcılar devre dışı bırakıldı.</string> + <string name="set_group_preferences">Grup tercihlerini ayarla</string> + <string name="invalid_migration_confirmation">Geçerli olmayan taşıma onayı</string> + <string name="recent_history">Görünür geçmiş</string> + <string name="connected_mobile">Telefondan bağlandı</string> + <string name="group_member_status_complete">tamamlandı</string> + <string name="session_code">Oturum kodu</string> + <string name="smp_server_test_create_queue">Sıra başlat</string> + <string name="app_was_crashed">Görünüm çöktü</string> + <string name="callstate_received_answer">alınan cevap…</string> + <string name="delete_and_notify_contact">Sil ve kişiye haber ver</string> + <string name="simplex_service_notification_text">Mesajlar alınıyor…</string> + <string name="la_app_passcode">Uygulama şifresi</string> + <string name="rcv_group_event_2_members_connected">%s ve %s bağlandı</string> + <string name="delete_after">Sonra sil</string> + <string name="info_row_sent_at">Gönderildi</string> + <string name="smp_servers_preset_address">Önceden ayarlanmış sunucu adresi</string> + <string name="desktop_connection_terminated">Bağlantı sonlandırıldı</string> + <string name="updating_settings_will_reconnect_client_to_all_servers">Ayarları güncellemek alıcıyı bütün sunuculara yeniden bağlandıracak.</string> + <string name="unlink_desktop">Bağlantıyı kes</string> + <string name="feature_offered_item">%s teklif edildi</string> + <string name="icon_descr_received_msg_status_unread">okunmamış</string> + <string name="new_chat">Yeni sohbet</string> + <string name="send_live_message_desc">Bir canlı mesaj gönder - bu yazdıklarını anlık olarak alıcıya(lara) güncelleyen bir mesajdır</string> + <string name="remove_passphrase_from_keychain">Şifre Yöneticisindeki parola silinsin mi?</string> + <string name="settings_section_title_delivery_receipts">LERE GÖNDER</string> + <string name="connect_via_link_incognito">Takma adla bağlan</string> + <string name="always_use_relay">Her zaman yönlendirici kullan.</string> + <string name="auth_unlock">Kilidini aç</string> + <string name="unblock_member_desc">%s ten gelen mesajlar gösterilecek!</string> + <string name="error_enabling_delivery_receipts">Teslim alan alıcılar etkinleştirilirken hata!</string> + <string name="chat_preferences_contact_allows">Kişi izin verir</string> + <string name="alert_title_skipped_messages">Atlanmış mesajlar</string> + <string name="alert_text_msg_bad_id">Bir sonraki mesajın kimliği yanlış (bir öncekinden az veya aynı). +\nBazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir.</string> + <string name="notification_preview_somebody">Kişi gizlendi:</string> + <string name="alert_text_skipped_messages_it_can_happen_when">Şu durumlarda gerçekleşebilir: +\n1. Mesajların süresi, gönderen istemcide 2 gün sonra veya sunucuda 30 gün sonra sona erdi. +\n2. Siz veya kişiniz eski veritabanı yedeğini kullandığınız için mesajın şifresini çözme işlemi başarısız oldu. +\n3. Bağlantı tehlikeye girdi.</string> + <string name="network_use_onion_hosts_prefer_desc">Onion ana bilgisayarları mümkün olduğu zaman kullanılacaktır.</string> + <string name="switch_verb">Değiştir</string> + <string name="open_port_in_firewall_title">Güvenlik duvarında port aç</string> + <string name="callstate_starting">başlatılıyor…</string> + <string name="network_socks_toggle_use_socks_proxy">SOCKS vekili kullan</string> + <string name="mtr_error_no_down_migration">veritabanı versiyonu uygulamadan daha yeni, ama %s için aşağı geçiş yok</string> + <string name="keychain_allows_to_receive_ntfs">Android Şifre Yöneticisi uygulamayı yeniden başlattıktan veya parolanı değiştirdikten sonra güvenle parolalarını tutmak için kullanılacaktır - bu bildirimleri almanı sağlayacaktır.</string> + <string name="send_receipts">Alıcılara gönder</string> + <string name="loading_chats">Sohbetler yükleniyor…</string> + <string name="chat_help_tap_button">Butona bas</string> + <string name="connection_error_auth_desc">Kişinin bağlantısını silmesi veya bağlantının çoktan kullanılması gibi bir durum yoksa, bu bir hata olabilir - lütfen bize bildirin. +\nBağlanmak için, lütfen kişiye başka bir bağlanma bağlantısı göndermesini isteyin ve stabil bir internet bağlantınız olduğunu kontrol edin.</string> + <string name="image_decoding_exception_desc">Bu fotoğraf deşifre edilemedi. Lütfen,başka bir fotoğraf deneyin veya geliştiricilerle iletişime geçin.</string> + <string name="disconnect_remote_hosts">Telefonların bağlantısını kes</string> + <string name="receiving_via">Aracılığıyla alınıyor</string> + <string name="share_text_moderated_at">%s de yönetildi</string> + <string name="disconnect_remote_host">Bağlantıyı kes</string> + <string name="renegotiate_encryption">Bağlantı yeniden senkronizasyonu şifrelemesi</string> + <string name="receipts_groups_disable_keep_overrides">Devre dışı bırak (grup geçersiz kılmalarını tut)</string> + <string name="smp_server_test_delete_queue">Silme sırası</string> + <string name="creating_link">Bağlantı oluşturuluyor…</string> + <string name="block_member_question">Üye engellensin mi?</string> + <string name="receipts_groups_title_enable">Gruplar için alıcılar etkinleştirilsin mi?</string> + <string name="group_member_role_author">yetkili</string> + <string name="v4_5_multiple_chat_profiles_descr">Farklı isimler, profil fotoğrafları ve ulaşım izolasyonu.</string> + <string name="network_use_onion_hosts">.onion ana bilgisayarlarını kullan</string> + <string name="revoke_file__action">Dosyayı geri çek</string> + <string name="info_row_moderated_at">de yönetildi</string> + <string name="call_connection_via_relay">yönlendirici aracılığıyla</string> + <string name="mark_code_verified">Doğrulanmış olarak işaretle</string> + <string name="connected_to_mobile">Telefona bağlandı</string> + <string name="v5_3_simpler_incognito_mode_descr">Bağlanırken takma ada geçiş yap.</string> + <string name="v5_2_message_delivery_receipts">Mesaj gönderildi!</string> + <string name="smp_servers_use_server_for_new_conn">Yeni bağlantılar için kullan</string> + <string name="rcv_conn_event_switch_queue_phase_completed">senin için adres değiştirildi</string> + <string name="network_enable_socks">SOCKS vekilini kullan?</string> + <string name="stop_rcv_file__message">Dosya alımı durdurulacak.</string> + <string name="bad_desktop_address">Kötü masaüstü adresi</string> + <string name="revoke_file__title">Dosya geri çekilsin mi?</string> + <string name="la_lock_mode_system">Sistem yetkilendirmesi</string> + <string name="or_scan_qr_code">Veya QR kod okut</string> + <string name="database_migrations">Taşınmalar: %s</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Bu senin grup için bağlantın! <b>%1$s</b>]]></string> + <string name="unblock_member_confirmation">Engeli kaldır</string> + <string name="icon_descr_asked_to_receive">Fotoğrafı almak için soruldu</string> + <string name="invalid_qr_code">Geçerli olmayan QR kodu</string> + <string name="cannot_access_keychain">Veritabanı şifresini kaydetmek için lazım olan Şifre Yöneticisine erişilemiyor.</string> + <string name="make_private_connection">Gizli bir bağlantı kur</string> + <string name="multicast_discoverable_via_local_network">Yerel ağ üzerinden keşfedilebilir</string> + <string name="info_row_updated_at">Kayıt güncellendi</string> + <string name="smp_servers_use_server">Sunucu kullan</string> + <string name="share_text_received_at">Alınma tarihi: %s</string> + <string name="add_contact_tab">Kişi Ekle</string> + <string name="v5_3_discover_join_groups_descr">- dizin hizmetine bağlan +\n(BETADA!) +\n- çoklu mesaj gönderme +\n(20 kişiye kadar). +\n- daha hızlı ve daha stabil.</string> + <string name="tap_to_scan">Taramak için tıkla</string> + <string name="keep_invitation_link">Tut</string> + <string name="lock_after">Sonra kilitle</string> + <string name="tap_to_paste_link">Bağlantı yapıştırmak için tıkla</string> + <string name="keychain_error">Anahtarlık hatası</string> + <string name="disconnect_desktop_question">Bilgisayarın bağlantısı kesilsin mi?</string> + <string name="loading_remote_file_desc">Bağlantılı telefondan dosya yüklenirken lütfen bekleyin.</string> + <string name="block_member_desc">%s ten gelen bütün yeni mesajlar gizli olacak!</string> + <string name="desktop_app_version_is_incompatible">Masaüstü uygulama versiyonu %s bu uygulama ile uyumlu değil.</string> + <string name="delivery">Gönderim</string> + <string name="no_connected_mobile">Telefona bağlı değil</string> + <string name="search_or_paste_simplex_link">Ara veya SimpleX bağlantısını yapıştır</string> + <string name="info_row_received_at">Şuradan alındı</string> + <string name="accept_feature_set_1_day">1 güne ayarla</string> + <string name="color_received_message">Alınmış mesaj</string> + <string name="rcv_group_event_member_created_contact">doğrudan bağlandı</string> + <string name="blocked_item_description">engellendi</string> + <string name="v5_5_private_notes">Gizli notlar</string> + <string name="v5_5_message_delivery_descr">Azaltılmış pil kullanımı ile birlikte.</string> + <string name="v5_5_message_delivery">İyileştirilmiş mesaj iletimi</string> + <string name="v5_5_new_interface_languages">Macarca ve Türkçe arayüz</string> + <string name="remote_host_error_timeout"><![CDATA[Telefona bağlanırken zaman aşımına uğranıldı <b>%s</b>]]></string> + <string name="remote_host_error_disconnected"><![CDATA[Telefon <b>%s</b> bağlantısı kesildi]]></string> + <string name="remote_ctrl_error_disconnected">Masaüstünün bağlantısı kesilmiş</string> + <string name="blocked_by_admin_item_description">yönetici tarafından engellendi</string> + <string name="blocked_by_admin_items_description">%d mesaj yönetici tarafından engellendi</string> + <string name="clear_note_folder_question">Gizli notlar temizlensin mi?</string> + <string name="clear_note_folder_warning">Tüm mesajlar silinecektir - bu geri alınamaz!</string> + <string name="developer_options_section">Geliştirici seçenekleri</string> + <string name="show_internal_errors">Dahili hataları göster</string> + <string name="show_slow_api_calls">Yavaş API aramalarını göster</string> + <string name="rcv_group_event_member_blocked">engellendi %s</string> + <string name="rcv_group_event_member_unblocked">engeli kaldırıldı %s</string> + <string name="group_member_status_unknown_short">bilinmeyen</string> + <string name="group_member_status_unknown">bilinmeyen durum</string> + <string name="snd_group_event_member_blocked">engelledin %s</string> + <string name="snd_group_event_member_unblocked">engeli kaldırdın %s</string> + <string name="past_member_vName">Geçmiş üye %1$s</string> + <string name="member_blocked_by_admin">Yönetici tarafından engellendi</string> + <string name="block_for_all">Herkes için engelle</string> + <string name="info_row_created_at">Şurada oluşturuldu</string> + <string name="share_text_created_at">Şurada oluşturuldu: %s</string> + <string name="error_blocking_member_for_all">Üye herkes için engellenirken hata oluştu</string> + <string name="unblock_for_all">Herkes için engelini kaldır</string> + <string name="saved_message_title">Kaydedilmiş mesaj</string> + <string name="remote_host_was_disconnected_title">Bağlantı durduruldu</string> + <string name="remote_host_error_bad_state"><![CDATA[Telefona bağlantı <b>%s</b> kötü durumda]]></string> + <string name="remote_host_disconnected_from"><![CDATA[Telefon bağlantısı <b>%s</b> şu nedenle kesildi: %s]]></string> + <string name="remote_ctrl_disconnected_with_reason">Nedeniyle birlikte bağlantı kesildi: %s</string> + <string name="v5_5_private_notes_descr">Şifrelenmiş dosyalar ve medya ile birlikte.</string> + <string name="v5_5_join_group_conversation">Grup sohbetlerine katıl</string> + <string name="v5_5_simpler_connect_ui">Bağlanmak için bağlantıyı yapıştır!</string> + <string name="remote_ctrl_error_timeout">Masaüstüne bağlanırken zaman aşımına ulaşıldı</string> + <string name="v5_5_join_group_conversation_descr">Yakın tarih ve geliştirilmiş dizin botu.</string> + <string name="v5_5_simpler_connect_ui_descr">Arama çubuğu davet bağlantılarını kabul eder.</string> + <string name="member_info_member_blocked">engellendi</string> + <string name="block_for_all_question">Üye herkes için engellensin mi?</string> + <string name="unblock_for_all_question">Üyenin engeli herkes için kaldırılsın mı?</string> + <string name="remote_host_error_bad_version"><![CDATA[Mobil <b>%s</b> desteklenmeyen bir sürüme sahip. Lütfen her iki cihazda da aynı sürümü kullandığınızdan emin olun]]></string> + <string name="remote_host_error_busy"><![CDATA[Telefon <b>%s</b> meşgul]]></string> + <string name="remote_host_error_inactive"><![CDATA[Telefon <b>%s</b> aktif değil]]></string> + <string name="remote_host_error_missing"><![CDATA[Telefon <b>%s</b> kayıp]]></string> + <string name="remote_ctrl_error_inactive">Masaüstü aktif değil</string> + <string name="remote_ctrl_error_bad_state">Masaüstüne bağlantı kötü durumda</string> + <string name="remote_ctrl_error_busy">Masaüstü meşgul</string> + <string name="remote_ctrl_error_bad_version">Masaüstü desteklenmeyen bir sürüme sahiptir. Lütfen her iki cihazda da aynı sürümü kullandığınızdan emin olun</string> + <string name="agent_critical_error_title">Kritik hata</string> + <string name="remote_ctrl_error_bad_invitation">Masaüstünün yanlış davetiye kodu var</string> + <string name="agent_internal_error_title">Dahili hata</string> + <string name="agent_internal_error_desc">Lütfen geliştiricilere bildir: +\n +\n%s</string> + <string name="agent_critical_error_desc">Lütfen geliştiricilere bildir: +\n +\n%s +\n +\nUygulamayı yeniden başlatmak önerilir.</string> + <string name="restart_chat_button">Sohbeti yeniden başlat</string> + <string name="failed_to_create_user_invalid_desc">Bu görünen ad geçersiz. +\nLütfen başka bir ad seç.</string> + <string name="failed_to_create_user_invalid_title">Geçersiz görünen ad!</string> + <string name="remote_ctrl_was_disconnected_title">Bağlantı durduruldu</string> + <string name="possible_slow_function_desc">İşlevin yürütülmesi çok uzun zaman alıyor: %1$d saniye: %2$s</string> + <string name="possible_slow_function_title">Yavaş fonksiyon</string> + <string name="profile_update_event_contact_name_changed">kişi %1$s olarak değişti %2$s</string> + <string name="profile_update_event_updated_profile">güncellenmiş profil</string> + <string name="profile_update_event_member_name_changed">üye %1$s olarak değişti %2$s</string> + <string name="profile_update_event_set_new_address">yeni kişi adresi ayarla</string> + <string name="profile_update_event_set_new_picture">yeni profil fotoğrafı ayarla</string> + <string name="profile_update_event_removed_address">kişi adresi silindi</string> + <string name="profile_update_event_removed_picture">profil fotoğrafı silindi</string> + <string name="error_creating_message">Mesaj oluşturulurken hata</string> + <string name="error_deleting_note_folder">Gizli notlar silinirken hata</string> + <string name="note_folder_local_display_name">Gizli notlar</string> + <string name="welcome_message_is_too_long">Hoşgeldin mesajı çok uzun</string> + <string name="message_too_large">Mesaj çok büyük</string> + <string name="database_migration_in_progress">Veritabanı geçişi devam ediyor. +\nBu birkaç dakika sürebilir.</string> + <string name="unable_to_open_browser_desc">Aramalar için varsayılan web tarayıcısı gereklidir. Lütfen sistemdeki varsayılan tarayıcıyı yapılandırın ve geliştiricilerle daha fazla bilgi paylaşın.</string> + <string name="call_service_notification_audio_call">Sesli arama</string> + <string name="call_service_notification_video_call">Görüntülü arama</string> + <string name="call_service_notification_end_call">Aramayı bitir</string> + <string name="unable_to_open_browser_title">Tarayıcıyı açarken hata</string> + <string name="e2ee_info_no_pq"><![CDATA[Mesajlar, dosyalar ve aramalar, mükemmel ileriye dönük gizlilik, reddetme ve izinsiz giriş kurtarması ile <b>uçtan uca şifreleme</b> ile korunmaktadır.]]></string> + <string name="e2ee_info_no_pq_short">Bu sohbet uçtan uca şifrelemeyle korunmaktadır.</string> + <string name="auth_open_migration_to_another_device">Taşıma ekranını aç</string> + <string name="migrate_from_another_device">Başka bir cihazdan taşı</string> + <string name="set_passphrase">Parolayı ayarla</string> + <string name="or_paste_archive_link">Veya arşiv bağlantısını yapıştırın</string> + <string name="migrate_to_device_chat_migrated">Sohbet taşındı!</string> + <string name="migrate_to_device_apply_onion">Uygula</string> + <string name="migrate_from_device_finalize_migration">Taşıma işlemini sonlandır</string> + <string name="v5_6_quantum_resistant_encryption_descr">Doğrudan sohbetlerde etkinleştir (BETA)!</string> + <string name="v5_6_quantum_resistant_encryption">Kuantuma dayanıklı şifreleme</string> + <string name="v5_6_safer_groups_descr">Yöneticiler bir üyeyi tamamen engelleyebilirler.</string> + <string name="v5_6_app_data_migration">Uygulama verisi taşıma</string> + <string name="v5_6_app_data_migration_descr">QR kodu aracılığıyla başka bir cihaza geçiş yapın.</string> + <string name="v5_6_picture_in_picture_calls">Resim içinde resim çağrıları</string> + <string name="v5_6_safer_groups">Daha güvenli gruplar</string> + <string name="v5_6_picture_in_picture_calls_descr">Arama sırasında uygulamayı kullanın.</string> + <string name="migrate_to_device_title">Buraya taşı</string> + <string name="paste_archive_link">Arşiv bağlantısını yapıştır</string> + <string name="migrate_to_device_database_init">İndirmeye hazırlanıyor</string> + <string name="migrate_to_device_download_failed">Yükleme başarısız</string> + <string name="migrate_to_device_downloading_archive">Arşiv indiriliyor</string> + <string name="migrate_to_device_downloading_details">Bağlantı detayları indiriliyor</string> + <string name="migrate_to_device_repeat_download">İndirmeyi tekrarla</string> + <string name="migrate_to_device_bytes_downloaded">%s indirildi</string> + <string name="migrate_to_device_import_failed">İçe aktarma başarısız oldu</string> + <string name="migrate_to_device_importing_archive">Arşiv içe aktarılıyor</string> + <string name="migrate_to_device_repeat_import">İçe aktarmayı tekrarla</string> + <string name="migrate_to_device_try_again">Bir kez daha deneyebilirsiniz.</string> + <string name="migrate_to_device_error_downloading_archive">Arşiv indirilirken hata oluştu</string> + <string name="migrate_to_device_finalize_migration">Taşıma işlemini başka bir cihazda sonlandırın.</string> + <string name="migrate_from_device_exported_file_doesnt_exist">Dışa aktarılan dosya mevcut değil</string> + <string name="migrate_from_device_title">Cihazı taşı</string> + <string name="migrate_from_device_to_another_device">Başka bir cihaza geçiş</string> + <string name="migrate_from_device_error_deleting_database">Veritabanı silinirken hata oluştu</string> + <string name="migrate_from_device_error_exporting_archive">Sohbet veritabanı dışa aktarılırken hata oluştu</string> + <string name="migrate_from_device_error_uploading_archive">Arşiv yüklenirken hata oluştu</string> + <string name="migrate_from_device_database_init">Yükleme hazırlanıyor</string> + <string name="migrate_from_device_confirm_upload">Yüklemeyi onayla</string> + <string name="migrate_from_device_chat_should_be_stopped">Devam etmek için sohbetin durdurulması gerekiyor.</string> + <string name="migrate_from_device_archiving_database">Veritabanını arşivleme</string> + <string name="migrate_from_device_repeat_upload">Yüklemeyi tekrarla</string> + <string name="migrate_from_device_bytes_uploaded">%s yüklendi</string> + <string name="migrate_from_device_upload_failed">Yükleme başarısız</string> + <string name="migrate_from_device_uploading_archive">Arşiv yükleniyor</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Lütfen dikkat</b>: aynı veritabanını iki cihazda kullanmak, bir güvenlik koruması olarak bağlantılarınızdaki mesajların şifresinin çözülmesini engelleyecektir.]]></string> + <string name="migrate_from_device_cancel_migration">Taşımayı iptal et</string> + <string name="migrate_from_device_creating_archive_link">Arşiv bağlantısı oluşturuluyor</string> + <string name="migrate_from_device_migration_complete">Taşıma işlemi tamamlandı</string> + <string name="migrate_from_device_or_share_this_file_link">Veya bu dosya bağlantısını güvenli bir şekilde paylaşın</string> + <string name="migrate_from_device_start_chat">Sohbeti başlat</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Uyarı: Birden fazla cihazda sohbet başlatmak desteklenmez ve mesaj iletimi başarısızlıklara neden olabilir.</string> + <string name="migrate_from_device_verify_database_passphrase">Veritabanı parolasını doğrulayın</string> + <string name="migrate_from_device_verify_passphrase">Parolayı doğrulayın</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Tüm kişileriniz, konuşmalarınız ve dosyalarınız güvenli bir şekilde şifrelenir ve yapılandırılmış XFTP yönlendiricilerine parçalar halinde yüklenir.</string> + <string name="migrate_from_device_archive_and_upload">Arşivle ve yükle</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Uyarı</b>: arşiv silinecektir.]]></string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Taşımak için veritabanı parolasını hatırladığınızı doğrulayın.</string> + <string name="migrate_from_device_check_connection_and_try_again">İnternet bağlantınızı kontrol edip tekrar deneyin</string> + <string name="migrate_to_device_confirm_network_settings">Ağ ayarlarını onaylayın</string> + <string name="migrate_from_device_delete_database_from_device">Veritabanını bu cihazdan sil</string> + <string name="migrate_to_device_enter_passphrase">Parolayı girin</string> + <string name="migrate_from_device_error_saving_settings">Ayarlar kaydedilirken hata oluştu</string> + <string name="migrate_from_device_error_verifying_passphrase">Parola doğrulanırken hata oluştu:</string> + <string name="migrate_to_device_file_delete_or_link_invalid">Dosya silindi veya bağlantı geçersiz</string> + <string name="invalid_file_link">Geçersiz link</string> + <string name="e2ee_info_pq"><![CDATA[Mesajlar, dosyalar ve aramalar, mükemmel iletme gizliliği, reddetme ve izinsiz giriş kurtarma özellikleriyle <b>kuantum dirençli e2e şifreleme</b> ile korunmaktadır.]]></string> + <string name="migrate_to_device_migrating">Taşınıyor</string> + <string name="migrate_to_device_confirm_network_settings_footer">Lütfen bu cihaz için ağ ayarlarının doğru olduğunu onaylayın.</string> + <string name="conn_event_enabled_pq">kuantum dirençli e2e şifreleme</string> + <string name="conn_event_disabled_pq">standart uçtan uca şifreleme</string> + <string name="migrate_from_device_stopping_chat">Sohbet durduruluyor</string> + <string name="e2ee_info_pq_short">Bu sohbet kuantum dirençli uçtan uca şifrelemeyle korunmaktadır.</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[Aynı veritabanını iki cihazda <b>kullanmamalısınız</b>.]]></string> + <string name="migrate_from_device_try_again">Bir kez daha deneyebilirsiniz.</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Yeni cihazda <i>Başka bir cihazdan taşı</i>\'yı seçin ve QR kodunu tarayın.]]></string> + <string name="permissions_find_in_settings_and_grant">Bu izni Android ayarlarında bulun ve manuel olarak izin verin.</string> + <string name="permissions_required">İzinleri verin</string> + <string name="permissions_grant">Arama yapmak için izin(ler)i verin</string> + <string name="permissions_record_audio">Mikrofon</string> + <string name="permissions_open_settings">Ayarları aç</string> + <string name="permissions_camera">Kamera</string> + <string name="permissions_camera_and_record_audio">Kamera ve mikrofon</string> + <string name="error_showing_desktop_notification">Bildirim gösterilirken hata oluştu, geliştiricilerle irtibata geçin.</string> + <string name="permissions_grant_in_settings">Ayarlarda izin verin</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="feature_roles_owners">sahipler</string> + <string name="prohibit_sending_simplex_links">SimpleX bağlantısı gönderimini yasakla</string> + <string name="network_option_rcv_concurrency">Eşzamanlılık alınıyor</string> + <string name="simplex_links">SimpleX bağlantıları</string> + <string name="simplex_links_are_prohibited_in_group">SimpleX bağlantıları yasaklandı</string> + <string name="audio_device_earpiece">Kulaklık</string> + <string name="audio_device_speaker">Hoparlör</string> + <string name="audio_device_wired_headphones">Kulaklıklar</string> + <string name="v5_7_quantum_resistant_encryption_descr">Doğrudan sohbetlerde etkinleştirilecektir!</string> + <string name="v5_7_network">Ağ yönetimi</string> + <string name="v5_7_new_interface_languages">Litvanya Kullanıcı Arayüzü</string> + <string name="network_type_other">Diğer</string> + <string name="saved_description">kaydedildi</string> + <string name="saved_from_description">%s tarafından kaydedildi</string> + <string name="forwarded_chat_item_info_tab">İletildi</string> + <string name="saved_chat_item_info_tab">Kaydedildi</string> + <string name="download_file">İndir</string> + <string name="forwarded_from_chat_item_info_title">Şuradan iletildi</string> + <string name="forward_message">Mesaj ilet…</string> + <string name="recipients_can_not_see_who_message_from">Alıcı(lar) bu mesajın kimden geldiğini göremez.</string> + <string name="simplex_links_not_allowed">SimpleX bağlantılarına izin verilmiyor</string> + <string name="files_and_media_not_allowed">Dosyalar ve medyaya izin verilmiyor</string> + <string name="voice_messages_not_allowed">Sesli mesajlara izin verilmiyor</string> + <string name="group_members_can_send_simplex_links">Grup üyeleri SimpleX bağlantıları gönderebilir.</string> + <string name="forwarded_description">iletildi</string> + <string name="settings_section_title_network_connection">Ağ bağlantısı</string> + <string name="allow_to_send_simplex_links">SimpleX bağlantıları göndermesine izin ver.</string> + <string name="network_type_no_network_connection">Ağ bağlantısı yok</string> + <string name="forward_chat_item">İlet</string> + <string name="v5_7_forward">Mesajları ilet ve kaydet</string> + <string name="feature_roles_admins">yöneticiler</string> + <string name="network_type_cellular">Hücresel Veri</string> + <string name="feature_enabled_for">Şunlar için etkinleştirildi</string> + <string name="feature_roles_all_members">bütün üyeler</string> + <string name="v5_7_call_sounds">Arama içi sesler</string> + <string name="saved_from_chat_item_info_title">Tarafından kaydedildi</string> + <string name="v5_7_forward_descr">Mesaj kaynağı gizli kalır.</string> + <string name="v5_7_network_descr">Daha güvenilir ağ bağlantısı.</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="v5_7_call_sounds_descr">Sesli ve görüntülü aramalara bağlanırken.</string> + <string name="network_type_ethernet">Kablolu ethernet</string> + <string name="settings_section_title_profile_images">Profil resimleri</string> + <string name="v5_7_shape_profile_images">Profil resimlerini şekillendir</string> + <string name="v5_7_shape_profile_images_descr">Kare,daire, veya aralarında herhangi bir şey.</string> + <string name="snd_error_quota">Kapasite aşıldı - alıcı önceden gönderilen mesajları almadı.</string> + <string name="snd_error_relay">Hedef sunucu hatası: %1$s</string> + <string name="ci_status_other_error">Hata: %1$s</string> + <string name="snd_error_proxy_relay">Yönlendirme sunucusu: %1$s +\nHedef sunucu hatası: %2$s</string> + <string name="snd_error_proxy">Yönlendirme sunucusu: %1$s +\nHata: %2$s</string> + <string name="message_delivery_warning_title">Mesaj iletimi uyarısı</string> + <string name="snd_error_expired">Ağ sorunları - birçok gönderme denemesinden sonra mesajın süresi doldu.</string> + <string name="srv_error_host">Sunucu adresi ağ ayarlarıyla uyumlu değil.</string> + <string name="srv_error_version">Sunucu sürümü ağ ayarlarıyla uyumlu değil.</string> + <string name="snd_error_auth">Yanlış anahtar veya bilinmeyen bağlantı - büyük olasılıkla bu bağlantı silinmiştir.</string> + <string name="network_smp_proxy_mode_private_routing">Gizli yönlendirme</string> + <string name="network_smp_proxy_mode_unknown">Bilinmeyen sunucular</string> + <string name="network_smp_proxy_mode_always_description">Her zaman gizli yönlendirmeyi kullan.</string> + <string name="network_smp_proxy_mode_never_description">Gizli yönlendirmeyi KULLANMA.</string> + <string name="update_network_smp_proxy_mode_question">Mesaj yönlendirme modu</string> + <string name="network_smp_proxy_mode_never">Hiçbir zaman</string> + <string name="network_smp_proxy_mode_unknown_description">Bilinmeyen sunucularla gizli yönlendirme kullan.</string> + <string name="network_smp_proxy_fallback_allow_downgrade">Sürüm düşürmeye izin ver</string> + <string name="network_smp_proxy_fallback_prohibit">Hayır</string> + <string name="update_network_smp_proxy_fallback_question">Mesaj yönlendirme yedeklemesi</string> + <string name="network_smp_proxy_mode_always">Her zaman</string> + <string name="network_smp_proxy_fallback_prohibit_description">Sizin veya hedef sunucunun özel yönlendirmeyi desteklememesi durumunda bile mesajları doğrudan GÖNDERMEYİN.</string> + <string name="network_smp_proxy_fallback_allow_protected_description">IP adresi korumalı olduğunda ve sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin.</string> + <string name="network_smp_proxy_fallback_allow_description">Sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin.</string> + <string name="settings_section_title_private_message_routing">GİZLİ MESAJ YÖNLENDİRME</string> + <string name="private_routing_show_message_status">Mesaj durumunu göster</string> + <string name="private_routing_explanation">IP adresinizi korumak için,gizli yönlendirme mesajları iletmek için SMP sunucularınızı kullanır.</string> + <string name="network_smp_proxy_mode_unprotected">Korumasız</string> + <string name="network_smp_proxy_mode_unprotected_description">IP adresi korunmadığında bilinmeyen sunucularla gizli yönlendirme kullan.</string> + <string name="network_smp_proxy_fallback_allow_protected">IP gizliyken</string> + <string name="network_smp_proxy_fallback_allow">Evet</string> + <string name="settings_section_title_chat_theme">Sohbet teması</string> + <string name="settings_section_title_user_theme">Profil teması</string> + <string name="theme_black">Siyah</string> + <string name="color_mode">Renk modu</string> + <string name="dark_mode_colors">Karanlık mod renkleri</string> + <string name="color_mode_light">Aydınlık</string> + <string name="color_mode_system">Sistem</string> + <string name="color_primary_variant2">Ek vurgu 2</string> + <string name="chat_theme_apply_to_all_modes">Bütün renk modları</string> + <string name="chat_theme_apply_to_mode">Şuna uygula</string> + <string name="chat_theme_apply_to_dark_mode">Karanlık mod</string> + <string name="wallpaper_scale_fill">Doldur</string> + <string name="wallpaper_scale">Ölçeklendir</string> + <string name="color_sent_quote">Gönderilen cevap</string> + <string name="chat_theme_set_default_theme">Varsayılan temaya ayarla</string> + <string name="wallpaper_advanced_settings">Gelişmiş ayarlar</string> + <string name="wallpaper_preview_hello_bob">Günaydın!</string> + <string name="color_mode_dark">Karanlık</string> + <string name="chat_theme_apply_to_light_mode">Aydınlık mod</string> + <string name="protect_ip_address">IP adresini koru</string> + <string name="settings_section_title_files">DOSYALAR</string> + <string name="settings_section_title_chat_colors">Sohbet renkleri</string> + <string name="wallpaper_scale_fit">Sığdır</string> + <string name="color_received_quote">Alınan cevap</string> + <string name="wallpaper_preview_hello_alice">İyi öğlenler!</string> + <string name="theme_remove_image">Resmi kaldır</string> + <string name="wallpaper_scale_repeat">Tekrarla</string> + <string name="reset_single_color">Rengi sıfırla</string> + <string name="chat_list_always_visible">Sohbet listesini yeni pencerede göster</string> + <string name="file_not_approved_title">Bilinmeyen sunucular!</string> + <string name="file_not_approved_descr">Tor veya VPN olmadan, IP adresiniz bu XFTP yönlendiricileri tarafından görülebilir: +\n%1$s.</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Tor veya VPN olmadan, IP adresiniz dosya sunucularına görülebilir.</string> + <string name="color_wallpaper_tint">Duvar kağıdı vurgusu</string> + <string name="color_wallpaper_background">Duvar kağıdı arkaplanı</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">Uygulama, bilinmeyen dosya sunucularından indirmeleri onaylamanızı isteyecektir (.onion veya SOCKS vekilleri etkin değilse).</string> + <string name="error_initializing_web_view">WebView başlatılırken hata oluştu. Sisteminizi yeni sürüme güncelleyin. Lütfen geliştiricilerle iletişime geçin. +\nHata: %s</string> + <string name="v5_8_chat_themes_descr">Sohbetlerinizin farklı görünmesini sağlayın!</string> + <string name="v5_8_persian_ui">Farsça Arayüz</string> + <string name="chat_theme_reset_to_user_theme">Kullanıcı temasına sıfırla</string> + <string name="v5_8_private_routing_descr">IP adresinizi kişileriniz tarafından seçilen mesajlaşma yönlendiricilerinden koruyun. +\n*Ağ ve sunucular* ayarlarında etkinleştirin.</string> + <string name="v5_8_private_routing">Gizli mesaj yönlendirme 🚀</string> + <string name="v5_8_safe_files_descr">Bilinmeyen sunuculardan gelen dosyaları onayla.</string> + <string name="v5_8_message_delivery">Geliştirilmiş mesaj iletimi</string> + <string name="v5_8_chat_themes">Yeni sohbet temaları</string> + <string name="v5_8_safe_files">Dosyaları güvenle alın</string> + <string name="v5_8_message_delivery_descr">Azaltılmış pil kullanımı ile.</string> + <string name="theme_destination_app_theme">Uygulama teması</string> + <string name="chat_theme_reset_to_app_theme">Uygulama temasına sıfırla</string> + <string name="info_row_debug_delivery">Hata ayıklama teslimatı</string> + <string name="message_queue_info">Mesaj kuyruğu bilgisi</string> + <string name="message_queue_info_none">hiçbiri</string> + <string name="message_queue_info_server_info">sunucu kuyruk bilgisi: %1$s +\n +\nson alınan msj: %2$s</string> + <string name="smp_proxy_error_connecting">Yönlendirme sunucusuna (%1$s) bağlantı sırasında hata oluştu. Lütfen daha sonra tekrar deneyin.</string> + <string name="file_error_relay">Dosya sunucusu hatası. %1$s</string> + <string name="scan_paste_link">Tara / Bağlantı yapıştır</string> + <string name="app_check_for_updates">Güncellemeleri kontrol et</string> + <string name="app_check_for_updates_disabled">Devre dışı</string> + <string name="app_check_for_updates_download_completed_title">Uygulama Güncellemesi indirildi</string> + <string name="app_check_for_updates_button_remind_later">Daha sonra hatırlat</string> + <string name="app_check_for_updates_notice_title">Güncellemeleri kontrol et</string> + <string name="servers_info_reconnect_server_message">Mesajı göndermeye zorlamak için sunucuya yeniden bağlan. Bu ekstra internet kullanır.</string> + <string name="servers_info_modal_error_title">Hata</string> + <string name="error_forwarding_messages">Mesajların yönlendirilmesi sırasında hata oluştu.</string> + <string name="file_error_no_file">Dosya bulunamadı - muhtemelen dosya silindi veya göderim iptal edildi.</string> + <string name="forward_alert_title_messages_to_forward">%1$s Mesaj yönlendirilsin mi ?</string> + <string name="selected_chat_items_nothing_selected">Hiçbir şey seçilmedi.</string> + <string name="forward_alert_title_nothing_to_forward">Yönlendirilecek bir şey yok!</string> + <string name="selected_chat_items_selected_n">%d seçildi.</string> + <string name="forward_alert_forward_messages_without_files">Mesajlar dosyalar olmadan iletilsin mi ?</string> + <string name="forward_files_messages_deleted_after_selection_desc">Mesajlar siz seçtikten sonra silindi.</string> + <string name="cannot_share_message_alert_title">Mesaj gönderilemedi</string> + <string name="forward_files_not_accepted_receive_files">İndir</string> + <string name="forward_multiple">Mesajları ilet..</string> + <string name="compose_forward_messages_n">%1$s mesaj iletiliyor</string> + <string name="compose_save_messages_n">%1$s Mesaj kayıt ediliyor.</string> + <string name="info_view_connect_button">bağlan</string> + <string name="info_view_call_button">Ara</string> + <string name="info_view_message_button">mesaj</string> + <string name="only_delete_conversation">Sadece sohbeti sil</string> + <string name="info_view_open_button">açık</string> + <string name="info_view_search_button">ara</string> + <string name="switching_profile_error_title">Profil değiştirme sırasında hata oluştu.</string> + <string name="select_chat_profile">Sohbet profili seç</string> + <string name="xftp_servers_configured">XFTP sunucuları yapılandırıldı</string> + <string name="media_and_file_servers">Medya ve dosya sunucuları</string> + <string name="network_proxy_auth_mode_no_auth">Kimlik bilgilerini proxy ile kullanmayın.</string> + <string name="network_proxy_incorrect_config_title">Proxy kayıt edilirken hata oluştu.</string> + <string name="network_proxy_incorrect_config_desc">Proxy konfigürasyonunun doğru olduğundan emin olun.</string> + <string name="network_proxy_password">Şifre</string> + <string name="app_check_for_updates_beta">Beta</string> + <string name="app_check_for_updates_download_started">Uygulama güncellemesi indiriliyor, uygulamayı kapatmayın</string> + <string name="app_check_for_updates_installed_successfully_title">Kurulum başarılı</string> + <string name="app_check_for_updates_button_install">Güncellemeyi Kur</string> + <string name="app_check_for_updates_button_open">Dosya konumunu aç</string> + <string name="app_check_for_updates_notice_disable">Devre dışı bırak</string> + <string name="app_check_for_updates_installed_successfully_desc">Lütfen uygulamayı yeniden başlatın.</string> + <string name="invite_friends_short">Davet</string> + <string name="create_address_button">Yarat</string> + <string name="privacy_media_blur_radius_off">Kapalı</string> + <string name="settings_section_title_chat_database">Mesajlaşma Veritabanı</string> + <string name="chat_database_exported_continue">Devam et</string> + <string name="share_text_message_status">Mesaj durumu: %s</string> + <string name="appearance_font_size">Yazı tipi boyutu</string> + <string name="v6_0_new_chat_experience">Yeni bir sohbet deneyimi 🎉</string> + <string name="v6_0_connection_servers_status">Ağınızı kontrol edin</string> + <string name="v6_0_connection_servers_status_descr">Bağlantı ve sunucuların durumu</string> + <string name="v6_0_increase_font_size">Yazı boyutunu arttır</string> + <string name="remote_ctrl_connection_stopped_desc">Lütfen telefonun ve bilgisayarın aynı lokal ağa bağlı olduğundan ve bilgisayar güvenlik duvarının bağlantıya izin verdiğinden emin olun. Lütfen diğer herhangi bir sorunu geliştiricilerle paylaşın.</string> + <string name="servers_info_reset_stats_alert_title">Tüm istatistikler sıfırlansın mı ?</string> + <string name="servers_info_reconnect_server_error">Hata, sunucuya yeniden bağlanılıyor</string> + <string name="servers_info_reconnect_all_servers_button">Tüm sunuculara yeniden bağlan</string> + <string name="duplicates_label">Kopyalar</string> + <string name="chunks_uploaded">Parçalar yüklendi</string> + <string name="downloaded_files">Dosyalar İndirildi</string> + <string name="download_errors">İndirme hataları</string> + <string name="info_row_message_status">Mesaj durumu</string> + <string name="error_parsing_uri_title">Geçersiz link</string> + <string name="error_parsing_uri_desc">Lütfen SimpleX bağlantısının doğru olup olmadığını kontrol edin.</string> + <string name="proxy_destination_error_broker_host">Varış sunucusu ardesi (%1$s) yönlendirme sunucusu (%2$s) ile uyumsuz.</string> + <string name="proxy_destination_error_broker_version">Varış sunucusu sürümü (%1$s) yönlendirme sunucusu (%2$s) ile uyumsuz.</string> + <string name="message_forwarded_title">Mesaj iletildi</string> + <string name="member_inactive_desc">Kullanıcı aktif olursa mesaj iletilebilir.</string> + <string name="compose_message_placeholder">Mesaj</string> + <string name="deleted_chats">Arşivli kişiler</string> + <string name="no_filtered_contacts">Filtrelenmiş kişiler yok</string> + <string name="paste_link">Bağlantıyı yapıştır</string> + <string name="action_button_add_members">Davet et</string> + <string name="member_info_member_disabled">Devre dışı</string> + <string name="member_info_member_inactive">inaktif</string> + <string name="allow_calls_question">Aramalara izin verilsin mi ?</string> + <string name="calls_prohibited_alert_title">Aramalara izin verilmiyor</string> + <string name="cant_call_contact_deleted_alert_text">Kişi silindi.</string> + <string name="cant_send_message_to_member_alert_title">Grup üyesine mesaj gönderilemiyor</string> + <string name="cant_call_member_send_message_alert_text">Çağrıları aktif etmek için mesaj gönder.</string> + <string name="v6_0_your_contacts_descr">Daha sonra görüşmek için kişileri arşivleyin</string> + <string name="v6_0_private_routing_descr">IP adresinizi ve bağlantılarınızı korur.</string> + <string name="v6_0_new_media_options">Yeni medya seçenekleri</string> + <string name="v6_0_privacy_blur">Daha iyi gizlilik için bulanıklaştır.</string> + <string name="v6_0_connect_faster_descr">Arkadaşlarınıza daha hızlı bağlanın</string> + <string name="v6_0_delete_many_messages_descr">Tek seferde en fazla 20 mesaj silin.</string> + <string name="v6_0_chat_list_media">Sohbet listesinden oynat.</string> + <string name="migrate_from_device_remove_archive_question">Arşiv kaldırılsın mı ?</string> + <string name="servers_info_sessions_connected">Bağlandı</string> + <string name="current_user">Aktif profil</string> + <string name="servers_info_files_tab">Dosyalar</string> + <string name="servers_info_missing">Bilgi yok, yenilemeyi deneyin</string> + <string name="servers_info_sessions_connecting">Bağlanıyor</string> + <string name="servers_info_sessions_errors">Hatalar</string> + <string name="servers_info_messages_received">Mesajlar alındı</string> + <string name="servers_info_messages_sent">Mesajlar gönderildi</string> + <string name="servers_info_reconnect_servers_message">Mesaj iletimine zorlamak için tüm sunuculara yeniden bağlan. Bu ekstra internet kullanılır.</string> + <string name="servers_info_reconnect_server_title">Sunucuya yeniden bağlansın mı ?</string> + <string name="servers_info_reconnect_servers_title">Sunuculara yeniden bağlanılsın mı ?</string> + <string name="servers_info_reconnect_servers_error">Hata sunuculara yeniden bağlanılıyor</string> + <string name="servers_info_reset_stats_alert_confirm">Sıfırla</string> + <string name="connections">Bağlantılar</string> + <string name="created">Yaratıldı</string> + <string name="decryption_errors">Şifre çözme hataları</string> + <string name="other_errors">diğer hatalar</string> + <string name="completed">Tamamlandı</string> + <string name="deleted">Silindi</string> + <string name="deletion_errors">Silme hatası</string> + <string name="open_server_settings_button">Sunucu ayarlarını aç</string> + <string name="share_text_file_status">Dosya durumu: %s</string> + <string name="new_message">Yeni mesaj</string> + <string name="smp_servers_other">Diğer SMP sunucuları</string> + <string name="reconnect">Yeniden bağlan</string> + <string name="sent_directly">Direkt gönderildi.</string> + <string name="all_users">Tüm Profiller</string> + <string name="cant_call_member_alert_title">Grup üyesi aranamıyor</string> + <string name="chat_database_exported_title">Veritabanı dışa aktarıldı</string> + <string name="chunks_deleted">Parçalar silindi</string> + <string name="chunks_downloaded">Parçalar indirildi</string> + <string name="servers_info_connected_servers_section_header">Bağlı sunucular</string> + <string name="cant_call_contact_connecting_wait_alert_text">Kişiye bağlanılıyor, lütfen bekleyin ya da daha sonra kontrol edin.</string> + <string name="copy_error">Kopyalama hatası</string> + <string name="delete_without_notification">Bildirim göndermeden sil</string> + <string name="v6_0_upgrade_app_descr">Yeni versiyonları GitHub\'dan indirin</string> + <string name="servers_info_reset_stats_alert_error_title">Hata istatistikler sıfırlanıyor</string> + <string name="smp_proxy_error_broker_host">Yönlendirme sunucu adresi (%1$s) ağ ayarlarıyla uyumsuz.</string> + <string name="info_row_file_status">Dosya durumu</string> + <string name="proxy_destination_error_failed_to_connect">Yönlendirme sunucusu (%1$s) varış sunucusuna (%2$s) bağlanamadı. Lütfen daha sonra tekrar deneyin.</string> + <string name="smp_proxy_error_broker_version">Yönlendirme suncusu sürümü ağ ayarlarıyla uyumsuz: %1$s</string> + <string name="servers_info_subscriptions_connections_pending">Bekliyor</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">Lütfen kişinizden çağrılara izin vermesini isteyin.</string> + <string name="servers_info_previously_connected_servers_section_header">Önceden bağlanılmış sunucular</string> + <string name="cant_call_contact_alert_title">Kişi aranamıyor</string> + <string name="network_options_save_and_reconnect">Kayıt et ve yeniden bağlan</string> + <string name="delete_messages_cannot_be_undone_warning">Mesajlar silinecek - bu geri alınamaz!</string> + <string name="delete_messages_mark_deleted_warning">Mesajlar silinmek üzere işaretlendi. Alıcı (lar) bu mesajları görebilecek.</string> + <string name="delete_members_messages__question">Üylerin %d mesajı silinsin mi ?</string> + <string name="member_inactive_title">Üye inaktif</string> + <string name="message_forwarded_desc">Henüz direkt bağlantı yok mesaj admin tarafından yönlendirildi.</string> + <string name="message_servers">Mesaj sunucuları</string> + <string name="smp_servers_configured">SMP sunucları yapılandırıldı</string> + <string name="xftp_servers_other">Diğer XFTP sunucuları</string> + <string name="privacy_media_blur_radius">Medyayı bulanıklaştır.</string> + <string name="delete_contact_cannot_undo_warning">Kişiler silinecek - bu geri alınamaz !</string> + <string name="keep_conversation">Sohbeti sakla.</string> + <string name="confirm_delete_contact_question">Kişiyi silmek istediğinizden emin misiniz ?</string> + <string name="conversation_deleted">Sohbet silindi!</string> + <string name="contact_deleted">Kişiler silindi!</string> + <string name="please_try_later">Lütfen sonra tekrar deneyin</string> + <string name="private_routing_error">Gizli yönlendirme hatası</string> + <string name="file_error">Dosya hatası</string> + <string name="servers_info_details">Detaylar</string> + <string name="expired_label">Süresi dolmuş</string> + <string name="send_errors">Gönderme hataları</string> + <string name="attempts_label">denemeler</string> + <string name="servers_info_detailed_statistics">Detaylı istatistikler</string> + <string name="servers_info_downloaded">İndirildi</string> + <string name="other_label">diğer</string> + <string name="servers_info_detailed_statistics_received_messages_header">Alınan mesajlar</string> + <string name="servers_info_detailed_statistics_received_total">Toplam alınan</string> + <string name="select_verb">Seç</string> + <string name="cannot_share_message_alert_text">Seçilen sohbet tercihleri bu mesajı yasakladı.</string> + <string name="servers_info_reset_stats">Tüm istatistikleri sıfırla</string> + <string name="reset_all_hints">Tüm ip uçlarını sıfırla</string> + <string name="n_file_errors">%1$d dosya hata(ları)\n%2$s</string> + <string name="forward_files_in_progress_desc">%1$d dosya(lar) hala indiriliyor.</string> + <string name="forward_files_failed_to_receive_desc">%1$d dosyası (ları) indirilemedi.</string> + <string name="forward_files_missing_desc">%1$d dosyası (ları) silindi.</string> + <string name="n_other_file_errors">%1$d diğer dosya hatası(ları).</string> + <string name="forward_files_not_accepted_desc">%1$d dosyası (ları) indirilemedi.</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s mesajları iletilemedi</string> + <string name="privacy_media_blur_radius_medium">Orta</string> + <string name="servers_info_subscriptions_section_header">Mesaj alındısı</string> + <string name="acknowledged">Onaylandı</string> + <string name="error_initializing_web_view_wrong_arch">WebView başlatılırken hata oluştu. WebView\'in yüklü olduğundan ve desteklenen mimarisinin arm64 olduğundan emin olun.\nHata: %s</string> + <string name="network_session_mode_session_description">Uygulamayı her başlattığınızda yeni SOCKS kimlik bilgileri kullanılacaktır.</string> + <string name="network_session_mode_server_description">Her sunucu için yeni SOCKS kimlik bilgileri kullanılacaktır.</string> + <string name="app_check_for_updates_button_download">İndir %s (%s)</string> + <string name="settings_section_title_message_shape">Mesaj şekli</string> + <string name="subscription_percentage">Yüzdeyi göster</string> + <string name="app_check_for_updates_canceled">Güncelleme indirme işlemi iptal edildi</string> + <string name="subscription_errors">Abone olurken hata</string> + <string name="moderate_messages_will_be_deleted_warning">Mesajlar tüm üyeler için silinecektir.</string> + <string name="moderate_messages_will_be_marked_warning">Mesajlar tüm üyeler için moderasyonlu olarak işaretlenecektir.</string> + <string name="file_error_auth">Yanlış anahtar veya bilinmeyen dosya yığın adresi - büyük olasılıkla dosya silinmiştir.</string> + <string name="toolbar_settings">Ayarlar</string> + <string name="new_chat_share_profile">Profil paylaş</string> + <string name="switching_profile_error_message">Bağlantınız %s\'ye taşındı ancak sizi profile yönlendirirken beklenmedik bir hata oluştu.</string> + <string name="network_proxy_auth">Proxy kimlik doğrulaması</string> + <string name="network_proxy_random_credentials">Rastgele kimlik bilgileri kullan</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">Her bağlantı için farklı proxy kimlik bilgileri kullan.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">Her profil için farklı proxy kimlik bilgileri kullan.</string> + <string name="network_proxy_auth_mode_username_password">Kimlik bilgileriniz şifrelenmeden gönderilebilir.</string> + <string name="network_proxy_username">Kullanıcı Adı</string> + <string name="app_check_for_updates_button_skip">Bu sürümü atlayın</string> + <string name="app_check_for_updates_notice_desc">Yeni sürümlerden haberdar olmak için Kararlı veya Beta sürümleri için periyodik kontrolü açın.</string> + <string name="privacy_media_blur_radius_soft">Yumuşak</string> + <string name="chat_database_exported_not_all_files">Bazı dosya(lar) dışa aktarılmadı</string> + <string name="appearance_zoom">Zoom</string> + <string name="v6_0_upgrade_app">Uygulamayı otomatik olarak yükselt</string> + <string name="uploaded_files">Yüklenen dosyalar</string> + <string name="size">Boyut</string> + <string name="subscribed">Abone olundu</string> + <string name="subscription_results_ignored">Abonelikler göz ardı edildi</string> + <string name="contact_list_header_title">Bağlantılarınız</string> + <string name="icon_descr_sound_muted">Ses kapatıldı</string> + <string name="servers_info_uploaded">Yüklendi</string> + <string name="servers_info_reset_stats_alert_message">Sunucu istatistikleri sıfırlanacaktır - bu geri alınamaz!</string> + <string name="one_hand_ui">Erişilebilir uygulama araç çubukları</string> + <string name="one_hand_ui_change_instruction">Görünüm ayarlarından değiştirebilirsiniz.</string> + <string name="one_hand_ui_card_title">Sohbet listesini değiştir:</string> + <string name="system_mode_toast">Sistem modu</string> + <string name="v6_0_reachable_chat_toolbar">Erişilebilir sohbet araç çubuğu</string> + <string name="servers_info_target">İçin bilgi gösteriliyor</string> + <string name="servers_info_statistics_section_header">İstatistikler</string> + <string name="servers_info_private_data_disclaimer">%s\'den başlayarak.\nTüm veriler cihazınızda gizli tutulur.</string> + <string name="sent_via_proxy">Bir proxy aracılığıyla gönderildi</string> + <string name="server_address">Sunucu adresi</string> + <string name="upload_errors">Yükleme hataları</string> + <string name="network_option_tcp_connection">TCP bağlantısı</string> + <string name="network_socks_proxy">SOCKS proxy</string> + <string name="servers_info_proxied_servers_section_header">Proxy sunucuları</string> + <string name="temporary_file_error">Geçici dosya hatası</string> + <string name="info_view_video_button">Video</string> + <string name="you_can_still_send_messages_to_contact">Arşivlenen kişilerden %1$s\'e mesaj gönderebilirsiniz.</string> + <string name="you_can_still_view_conversation_with_contact">Sohbetler listesinde %1$s ile yapılan konuşmayı hala görüntüleyebilirsiniz.</string> + <string name="xftp_server">XFTP sunucusu</string> + <string name="servers_info_detailed_statistics_receive_errors">Alım sırasında hata</string> + <string name="smp_server">SMP sunucusu</string> + <string name="network_error_broker_host_desc">Sunucu adresi ağ ayarlarıyla uyumsuz: %1$s.</string> + <string name="network_error_broker_version_desc">Sunucu sürümü uygulamanızla uyumlu değil: %1$s.</string> + <string name="you_need_to_allow_calls">Kendiniz arayabilmeniz için önce irtibat kişinizin sizi aramasına izin vermelisiniz.</string> + <string name="servers_info_starting_from">%s\'den başlayarak.</string> + <string name="acknowledgement_errors">Onay hataları</string> + <string name="secured">Güvenli</string> + <string name="network_session_mode_session">Uygulama oturumu</string> + <string name="network_session_mode_server">Sunucu</string> + <string name="app_check_for_updates_stable">Stabil</string> + <string name="app_check_for_updates_update_available">Güncelleme mevcut: %s</string> + <string name="remote_ctrl_connection_stopped_identity_desc">Bu bağlantı başka bir mobil cihazda kullanıldı, lütfen masaüstünde yeni bir bağlantı oluşturun.</string> + <string name="call_desktop_permission_denied_chrome">Mikrofon kullanımına izin vermek için adres alanının yanındaki bilgi düğmesine tıklayın.</string> + <string name="call_desktop_permission_denied_safari">Safari Ayarları / Web Siteleri / Mikrofon\'u açın, ardından localhost için İzin Ver\'i seçin.</string> + <string name="call_desktop_permission_denied_title">Arama yapmak için mikrofonunuzu kullanmanıza izin verin. Aramayı sonlandırın ve tekrar aramayı deneyin.</string> + <string name="settings_message_shape_tail">Konuşma balonu</string> + <string name="privacy_media_blur_radius_strong">Güçlü</string> + <string name="v6_0_reachable_chat_toolbar_descr">Uygulamayı tek elle kullan.</string> + <string name="servers_info">Sunucu bilgileri</string> + <string name="servers_info_subscriptions_total">Toplam</string> + <string name="servers_info_proxied_servers_section_footer">Bu sunuculara bağlı değilsiniz. Mesajları onlara iletmek için özel yönlendirme kullanılır.</string> + <string name="servers_info_subscriptions_connections_subscribed">Aktif bağlantılar</string> + <string name="settings_message_shape_corner">Köşeleri yuvarlama</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">Yüklenen veritabanı arşivi sunuculardan kalıcı olarak kaldırılacaktır.</string> + <string name="proxied">Proxyli</string> + <string name="servers_info_transport_sessions_section_header">Taşıma oturumları</string> + <string name="chat_database_exported_migrate">Dışa aktarılan veritabanını taşıyabilirsiniz.</string> + <string name="chat_database_exported_save">Dışa aktarılan arşivi kaydedebilirsiniz.</string> + <string name="servers_info_detailed_statistics_sent_messages_total">Gönderilen tüm mesajların toplamı</string> + <string name="servers_info_detailed_statistics_sent_messages_header">Gönderilen mesajlar</string> + <string name="v6_1_message_dates_descr">Daha iyi mesaj tarihleri.</string> + <string name="v6_1_customizable_message_descr">Özelleştirilebilir mesaj şekli.</string> + <string name="v6_1_forward_many_messages_descr">Aynı anda en fazla 20 mesaj iletin.</string> + <string name="v6_1_better_calls_descr">Görüşme sırasında ses ve görüntüyü değiştirin.</string> + <string name="v6_1_switch_chat_profile_descr">Sohbet profilini 1 kerelik davetler için değiştirin.</string> + <string name="v6_1_better_calls">Daha iyi aramalar</string> + <string name="v6_1_better_user_experience">Daha iyi kullanıcı deneyimi</string> + <string name="v6_1_delete_many_messages_descr">200\'e kadar mesajı silin veya düzenleyin.</string> + <string name="v6_1_better_security">Daha iyi güvenlik ✅</string> + <string name="v6_1_better_security_descr">SimpleX protokolleri Trail of Bits tarafından incelenmiştir.</string> + <string name="address_or_1_time_link">Adres mi yoksa tek seferlik bağlantı mı?</string> + <string name="operator_added_message_servers">Mesaj sunucuları eklendi</string> + <string name="accept_conditions">Koşulları kabul edin</string> + <string name="operator_conditions_accepted">Kabul edilen koşullar</string> + <string name="operator_added_xftp_servers">Medya ve dosya sunucuları eklendi</string> + <string name="appearance_app_toolbars">Uygulama araç çubukları</string> + <string name="address_settings">Adres ayarları</string> + <string name="appearance_bars_blur_radius">Bulanıklık</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[1 kerelik bağlantı <i>sadece bir kişiyle</i> kullanılabilir - yüz yüze veya herhangi bir mesajlaşma programı aracılığıyla paylaşın]]></string> + <string name="operator_conditions_accepted_for_some"><![CDATA[Aşağıdaki operatör(ler) için koşullar zaten kabul edilmiştir: <b>%s</b>.]]></string> + <string name="operators_conditions_accepted_for"><![CDATA[Şart(lar) operatör(ler) için kabul edilir: <b>%s</b>.]]></string> + <string name="button_add_friends">Arkadaş ekle</string> + <string name="v6_2_business_chats">İş konuşmaları</string> + <string name="button_add_team_members">Takım üyesi ekle</string> + <string name="display_name_accepted_invitation">kabul edilmiş davet</string> + <string name="business_address">İş adresi</string> + <string name="add_your_team_members_to_conversations">Takım üyelerini konuşmalara ekle</string> + <string name="onboarding_notifications_mode_service_desc_short">Uygulama her zaman arka planda çalışır</string> + <string name="onboarding_network_about_operators">Operatörler hakkında</string> + <string name="a_plus_b">a+b</string> + <string name="chat_list_add_list">Liste ekle</string> + <string name="chat_list_all">Hepsi</string> + <string name="add_to_list">Listeye ekle</string> + <string name="delete_chat_list_warning">%s listesindeki bütün sohbetler kaldırılacak ve liste silinecek</string> + <string name="chat_list_businesses">İşletmeler</string> + <string name="report_item_archived">arşivlenmiş rapor</string> + <string name="report_reason_other">Başka bir sebep</string> + <string name="archive_report">Raporu arşivle</string> + <string name="report_item_archived_by">%s tarafından arşivlenen rapor</string> + <string name="group_reports_active_one">1 rapor</string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[Operatör(ler) için koşullar kabul edilecektir: <b>%s</b>.]]></string> + <string name="privacy_chat_list_open_links_ask">Sor</string> + <string name="chat_item_ttl_year">1 yıl</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Xiaomi cihazları</b>: Bildirimlerin çalışması için lütfen sistem ayarlarında Otomatik Başlat\'ı etkinleştirin.]]></string> + <string name="report_archive_alert_desc_all">Tüm raporlar sizin için arşivlenecektir.</string> + <string name="report_archive_alert_title_all">Tüm raporlar arşivlensin mi?</string> + <string name="archive_reports">Raporları arşivle</string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Operatör(ler) için koşullar kabul edilecektir: <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[Aynı koşullar <b>%s</b> operatörü için de geçerli olacaktır.]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Aynı koşullar <b>%s</b> operatörü(leri) için de geçerli olacaktır.]]></string> + <string name="enable_sending_member_reports">Mesajların moderatörlere bildirilmesine izin verin.</string> + <string name="report_archive_alert_title_nth">%d raporu arşivleyelim mi?</string> + <string name="archive_verb">Arşiv</string> + <string name="report_archive_alert_title">Raporu arşivleyelim mi?</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[Tüm mesajlar ve dosyalar <b>uçtan uca şifreli</b> olarak gönderilir ve doğrudan mesajlarda kuantum sonrası güvenlik sağlanır.]]></string> + <string name="error_reading_passphrase">Veritabanı şifresini okurken hata oluştu</string> + <string name="button_delete_chat">Sohbeti sil</string> + <string name="delete_chat_question">Sohbet silinsin mi?</string> + <string name="connection_not_ready">Bağlantı hazır değil.</string> + <string name="operator_conditions_of_use">Kullanım şartları</string> + <string name="v6_2_network_decentralization_enable_flux_reason">daha iyi üstveri gizliliği için.</string> + <string name="failed_to_save_servers">Sunucuları kaydederken hata oluştu</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">Şartlar 30 gün sonra etkin operatörler için kabul edilecektir.</string> + <string name="delete_report">Raporu sil</string> + <string name="change_list">Listeyi değiştir</string> + <string name="operators_conditions_will_also_apply"><![CDATA[Bu koşullar aynı zamanda: <b>%s</b> için de geçerli olacaktır.]]></string> + <string name="block_members_for_all_question">Üyeler herkes için engellensin mi?</string> + <string name="report_reason_community">Topluluk kurallarının ihlali</string> + <string name="error_accepting_operator_conditions">Şartları kabul ederken hata oluştu</string> + <string name="delete_chat_list_menu_action">Sil</string> + <string name="delete_chat_list_question">Liste silinsin mi?</string> + <string name="connection_security">Bağlantı güvenliği</string> + <string name="blocking_reason_content">İçerik kullanım şartlarını ihlal ediyor</string> + <string name="error_creating_report">Rapor oluşturulurken hata oluştu</string> + <string name="connection_error_blocked">Bağlantı engellendi</string> + <string name="connection_error_blocked_desc">Bağlantı sunucu operatörü tarafından engellendi:\n%1$s.</string> + <string name="file_error_blocked">Dosya sunucu operatörü tarafından engellendi:\n%1$s.</string> + <string name="group_reports_active">%d rapor</string> + <string name="maximum_message_size_title">Mesaj çok büyük!</string> + <string name="change_automatic_deletion_question">Otomatik silinen mesajlar değiştirilsin mi?</string> + <string name="disable_automatic_deletion_question">Otomatik silinen mesajlar devre dışı bırakılsın mı?</string> + <string name="create_list">Liste oluştur</string> + <string name="change_order_chat_list_menu_action">Sırayı değiştir</string> + <string name="edit_chat_list_menu_action">Düzenle</string> + <string name="for_social_media">Sosyal medya için</string> + <string name="create_1_time_link">Tek kullanımlık bağlantı oluştur</string> + <string name="onboarding_network_operators_app_will_use_for_routing">Örneğin, eğer kişiniz SimpleX Sohbet sunucusundan mesajları alıyorsa, uygulamanız bu mesajları Flux sunucusundan iletecektir.</string> + <string name="chat_item_ttl_default">varsayılan (%s)</string> + <string name="block_members_desc">Bu üyelerden gelen yeni mesajların hepsi gizlenecektir.</string> + <string name="v6_3_better_groups_performance">İyileştirilmiş grup performansı</string> + <string name="connect_plan_chat_already_exists">Sohbet zaten var!</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Zaten <b>%1$s</b> ile bağlısınız.]]></string> + <string name="for_chat_profile">Konuşma profili %s için:</string> + <string name="direct_messages_are_prohibited">Üyeler arası doğrudan mesajlaşma yasaklıdır.</string> + <string name="operator_conditions_accepted_for_enabled_operators_on">Şartlar %s tarihinde etkin operatörler için otomatik olarak kabul edilecektir.</string> + <string name="chat_ttl_options_footer">Sohbet mesajlarını cihazınızdan silin.</string> + <string name="onboarding_notifications_mode_periodic_desc_short">Her 10 dakikada mesajları kontrol et</string> + <string name="onboarding_network_operators_continue">Devam et</string> + <string name="v6_3_better_privacy_and_security">İyileştirilmiş gizlilik ve güvenlik</string> + <string name="v6_3_faster_sending_messages">Daha hızlı mesaj gönderme.</string> + <string name="direct_messages_are_prohibited_in_chat">Bu sohbette üyeler arası doğrudan mesajlaşma yasaklıdır.</string> + <string name="error_loading_chat_tags">Sohbet listesi yüklenirken hata oluştu</string> + <string name="error_creating_chat_tags">Sohbet listesi oluşturulurken hata oluştu</string> + <string name="error_updating_chat_tags">Sohbet listesini güncellerken hata oluştu</string> + <string name="chat_list_favorites">Sık kullanılanlar</string> + <string name="chat_list_contacts">Konuşmalar</string> + <string name="error_saving_database">Veritabanını kaydederken hata oluştu</string> + <string name="delete_chat_for_self_cannot_undo_warning">Sohbet sizin için silinecek - bu geri alınamaz!</string> + <string name="operator_conditions_will_be_applied"><![CDATA[Bu koşullar aynı zamanda: <b>%s</b> için de geçerli olacaktır.]]></string> + <string name="report_archive_for_all_moderators">Bütün moderatörler için</string> + <string name="report_archive_for_me">Benim için</string> + <string name="prefs_error_saving_settings">Ayarlar kaydedilirken hata oluştu</string> + <string name="debug_logs">Günlükleri etkinleştir</string> + <string name="info_row_chat">Sohbet</string> + <string name="v6_3_faster_deletion_of_groups">Grupların daha hızlı silinmesi.</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">Sohbet bütün üyeler için silinecek - bu geri alınamaz!</string> + <string name="error_adding_server">Sunucu eklerken hata oluştu</string> + <string name="sync_connection_confirm">Onar</string> + <string name="sync_connection_question">Bağlantı onarılsın mı?</string> + <string name="error_updating_server_title">Sunucuyu güncellerken hata oluştu</string> +</resources> 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 d9d49aef7d..549cb01b63 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -2,8 +2,8 @@ <resources> <string name="send_disappearing_message_1_minute">1 хвилина</string> <string name="about_simplex">Про SimpleX</string> - <string name="accept_connection_request__question">Прийняти запит на підключення\?</string> - <string name="above_then_preposition_continuation">вище, тоді:</string> + <string name="accept_connection_request__question">Прийняти запит на з\'єднання?</string> + <string name="above_then_preposition_continuation">вище, а потім:</string> <string name="accept_contact_button">Прийняти</string> <string name="accept_call_on_lock_screen">Прийняти</string> <string name="color_primary">Акцент</string> @@ -14,564 +14,552 @@ <string name="accept_feature">Прийняти</string> <string name="send_disappearing_message_5_minutes">5 хвилин</string> <string name="chat_item_ttl_month">1 місяць</string> - <string name="learn_more_about_address">Про адресу SimpleX</string> + <string name="learn_more_about_address">Про SimpleX-адресу</string> <string name="one_time_link_short">1-разове посилання</string> <string name="about_simplex_chat">Про SimpleX Chat</string> - <string name="v4_3_improved_server_configuration_desc">Додайте сервери, відсканувавши QR-коди.</string> - <string name="users_delete_all_chats_deleted">Всі чати та повідомлення будуть видалені - це неможливо скасувати!</string> - <string name="allow_calls_only_if">Дозволяйте дзвінки, тільки якщо ваш контакт дозволяє їх.</string> - <string name="allow_irreversible_message_deletion_only_if">Дозволяйте безповоротне видалення повідомлень, тільки якщо контакт дозволяє вам це зробити.</string> + <string name="v4_3_improved_server_configuration_desc">Додавайте сервери, скануючи QR-коди.</string> + <string name="users_delete_all_chats_deleted">Усі чати та повідомлення будуть видалені - цю дію неможливо скасувати!</string> + <string name="allow_calls_only_if">Дозволити дзвінки тільки за умови, що ваш контакт дозволяє їх.</string> + <string name="allow_irreversible_message_deletion_only_if">Дозволити безповоротне видалення повідомлень, тільки якщо ваш контакт дозволяє вам. (24 години)</string> <string name="allow_voice_messages_question">Дозволити голосові повідомлення\?</string> - <string name="app_passcode_replaced_with_self_destruct">Код доступу до програми замінено на пароль самознищення.</string> - <string name="smp_servers_preset_add">Додавання попередньо встановлених серверів</string> - <string name="full_backup">Резервне копіювання даних програми</string> - <string name="smp_servers_add_to_another_device">Додати до іншого пристрою</string> - <string name="keychain_is_storing_securely">Android Keystore використовується для безпечного зберігання парольної фрази - це дозволяє сервісу сповіщень працювати.</string> - <string name="v4_2_group_links_desc">Адміни можуть створювати посилання для приєднання до груп.</string> - <string name="app_version_code">Збірка програми: %s</string> - <string name="allow_voice_messages_only_if">Дозволяйте голосові повідомлення, тільки якщо ваш контакт дозволяє їх.</string> - <string name="allow_to_send_disappearing">Дозволити відправляти зникаючі повідомлення.</string> - <string name="callstatus_accepted">прийнято виклик</string> - <string name="always_use_relay">Завжди використовуйте реле</string> + <string name="app_passcode_replaced_with_self_destruct">Пароль застосунку замінено паролем самознищення.</string> + <string name="smp_servers_preset_add">Додати попередньо встановлені сервери</string> + <string name="full_backup">Резервне копіювання даних застосунку</string> + <string name="smp_servers_add_to_another_device">Додати на інший пристрій</string> + <string name="keychain_is_storing_securely">Сховище ключів Android використовується для безпечного збереження ключової фрази - це дозволяє службі сповіщень працювати.</string> + <string name="v4_2_group_links_desc">Адміністратори можуть створювати посилання для приєднання до групи.</string> + <string name="app_version_code">Збірка додатку: %s</string> + <string name="allow_voice_messages_only_if">Дозволити голосові повідомлення тільки за умови, що ваш контакт дозволяє їх.</string> + <string name="allow_to_send_disappearing">Дозволити надсилати зникаючі повідомлення.</string> + <string name="callstatus_accepted">прийнятий виклик</string> + <string name="always_use_relay">Завжди використовувати реле</string> <string name="settings_section_title_app">ДОДАТОК</string> - <string name="allow_direct_messages">Дозволяє надсилати прямі повідомлення користувачам.</string> - <string name="allow_to_delete_messages">Дозволяє безповоротно видаляти надіслані повідомлення.</string> - <string name="allow_to_send_voice">Дозволити надсилати голосові повідомлення.</string> - <string name="allow_message_reactions">Дозволити реакцію на повідомлення.</string> - <string name="v5_1_self_destruct_passcode_descr">Всі дані стираються при введенні.</string> - <string name="v5_0_app_passcode">Пароль додатку</string> - <string name="settings_section_title_icon">ЗНАЧОК ДОДАТКУ</string> - <string name="allow_disappearing_messages_only_if">Дозволяйте зникати повідомленням, тільки якщо ваш контакт дозволяє це робити.</string> + <string name="allow_direct_messages">Дозволити надсилання приватних повідомлень учасникам.</string> + <string name="allow_to_delete_messages">Дозволити безповоротно видаляти надіслані повідомлення. (24 години)</string> + <string name="allow_to_send_voice">Дозволяйте надсилати голосові повідомлення.</string> + <string name="allow_message_reactions">Дозволити реакції на повідомлення.</string> + <string name="v5_1_self_destruct_passcode_descr">Вся інформація стирається при його введенні.</string> + <string name="v5_0_app_passcode">Пароль для додатка</string> + <string name="settings_section_title_icon">ІКОНКА ДОДАТКУ</string> + <string name="allow_disappearing_messages_only_if">Дозволити зникаючі повідомлення тільки за умови, що ваш контакт дозволяє їх.</string> <string name="allow_your_contacts_adding_message_reactions">Дозвольте вашим контактам додавати реакції на повідомлення.</string> - <string name="allow_message_reactions_only_if">Дозволяйте реакції на повідомлення, тільки якщо ваш контакт дозволяє їх.</string> - <string name="empty_chat_profile_is_created">Створюється порожній профіль чату з вказаним ім\'ям, і додаток відкривається у звичайному режимі.</string> + <string name="allow_message_reactions_only_if">Дозволити реакції на повідомлення тільки за умови, що ваш контакт дозволяє їх.</string> + <string name="empty_chat_profile_is_created">Створений порожній профіль чату з наданим ім\'ям, і застосунок відкривається, як завжди.</string> <string name="color_primary_variant">Додатковий акцент</string> - <string name="allow_your_contacts_irreversibly_delete">Дозвольте вашим контактам безповоротно видаляти надіслані повідомлення.</string> - <string name="allow_verb">Дозвольте</string> + <string name="allow_your_contacts_irreversibly_delete">Дозволити вашим контактам безповоротно видаляти надіслані повідомлення. (24 години)</string> + <string name="allow_verb">Дозволити</string> <string name="network_settings">Розширені налаштування мережі</string> - <string name="network_enable_socks_info">Доступ до серверів через SOCKS проксі на порту %d\? Перед увімкненням цієї опції проксі має бути запущено.</string> - <string name="all_your_contacts_will_remain_connected">Всі ваші контакти залишаться на зв\'язку.</string> - <string name="all_app_data_will_be_cleared">Всі дані програми видаляються.</string> - <string name="keychain_allows_to_receive_ntfs">Android Keystore буде використовуватися для безпечного зберігання пароля після перезапуску програми або зміни пароля - це дозволить отримувати сповіщення.</string> - <string name="allow_your_contacts_to_send_voice_messages">Дозвольте своїм контактам надсилати голосові повідомлення.</string> + <string name="network_enable_socks_info">Отримувати доступ до серверів через SOCKS-проксі на порті %d? Проксі має бути запущено до активації цієї опції.</string> + <string name="all_your_contacts_will_remain_connected">Усі ваші контакти залишаться підключеними.</string> + <string name="all_app_data_will_be_cleared">Усі дані застосунку буде видалено.</string> + <string name="keychain_allows_to_receive_ntfs">Після перезапуску додатка або зміни ключової фрази буде використано сховище ключів Android для безпечного збереження ключової фрази - це дозволить отримувати сповіщення.</string> + <string name="allow_your_contacts_to_send_voice_messages">Дозвольте вашим контактам надсилати голосові повідомлення.</string> <string name="accept_contact_incognito_button">Прийняти інкогніто</string> - <string name="smp_servers_add">Додати сервер…</string> - <string name="group_member_role_admin">адмін</string> - <string name="button_add_welcome_message">Додати вітальне повідомлення</string> - <string name="all_group_members_will_remain_connected">Всі учасники групи залишаться на зв\'язку.</string> - <string name="allow_your_contacts_to_send_disappearing_messages">Дозвольте своїм контактам надсилати зникаючі повідомлення.</string> - <string name="clear_chat_warning">Всі повідомлення будуть видалені - це неможливо скасувати! Повідомлення будуть видалені ТІЛЬКИ для вас.</string> - <string name="app_version_title">Версія програми</string> - <string name="add_address_to_your_profile">Додайте адресу до свого профілю, щоб ваші контакти могли поділитися нею з іншими людьми. Повідомлення про оновлення профілю буде надіслано вашим контактам.</string> - <string name="all_your_contacts_will_remain_connected_update_sent">Всі ваші контакти залишаться на зв\'язку. Повідомлення про оновлення профілю буде надіслано вашим контактам.</string> - <string name="answer_call">Відповісти на дзвінок</string> + <string name="smp_servers_add">Додати сервер</string> + <string name="group_member_role_admin">адміністратор</string> + <string name="button_add_welcome_message">Додати привітання</string> + <string name="all_group_members_will_remain_connected">Усі учасники групи залишаться підключеними.</string> + <string name="allow_your_contacts_to_send_disappearing_messages">Дозвольте вашим контактам надсилати повідомлення, які зникають.</string> + <string name="clear_chat_warning">Усі повідомлення будуть видалені - цю дію неможливо скасувати! Повідомлення будуть видалені ЛИШЕ для вас.</string> + <string name="app_version_title">Версія додатку</string> + <string name="add_address_to_your_profile">Додайте адресу до свого профілю, щоб ваші контакти могли поділитися нею з іншими людьми. Оновлення профілю буде відправлено вашим контактам.</string> + <string name="all_your_contacts_will_remain_connected_update_sent">Усі ваші контакти залишаться підключеними. Оновлення профілю буде відправлено вашим контактам.</string> + <string name="answer_call">Відповісти на виклик</string> <string name="address_section_title">Адреса</string> <string name="users_add">Додати профіль</string> - <string name="color_secondary_variant">Додатковий вторинний</string> - <string name="notifications_mode_service">Завжди увімкнено</string> - <string name="notifications_mode_off_desc">Додаток може отримувати сповіщення лише під час роботи, жодні фонові служби не запускаються</string> + <string name="color_secondary_variant">Додатковий другорядний</string> + <string name="notifications_mode_service">Завжди включено</string> + <string name="notifications_mode_off_desc">Додаток може отримувати сповіщення тільки тоді, коли він працює, фоновий сервіс не буде запущено</string> <string name="chat_preferences_always">завжди</string> - <string name="allow_your_contacts_to_call">Дозвольте вашим контактам телефонувати вам.</string> - <string name="network_session_mode_user_description"><![CDATA[<b>Для кожного профілю чату, який ви маєте в додатку</b>, буде використовуватися окреме TCP-з\'єднання (і SOCKS-обліковий запис).]]></string> - <string name="appearance_settings">Зовнішній вигляд</string> - <string name="app_version_name">Версія програми: v%s</string> - <string name="network_session_mode_entity_description"><b>Для кожного контакту і члена групи</b> буде використовуватися окреме TCP-з\'єднання (і SOCKS-обліковий запис). -\n<b>Зверніть увагу</b>: якщо у вас багато з\'єднань, споживання заряду акумулятора і трафіку може бути значно вищим, а деякі з\'єднання можуть обірватися.</string> - <string name="turning_off_service_and_periodic">Активна оптимізація заряду батареї, яка вимикає фоновий сервіс і періодичні запити на нові повідомлення. Ви можете знову увімкнути їх через налаштування.</string> + <string name="allow_your_contacts_to_call">Дозвольте вашим контактам дзвонити вам.</string> + <string name="network_session_mode_user_description"><![CDATA[Окреме TCP-підключення (і обліковий запис SOCKS) буде використовуватися <b>для кожного профілю чату, який у вас є в додатку</b>.]]></string> + <string name="appearance_settings">Вигляд</string> + <string name="app_version_name">Версія додатку: v%s</string> + <string name="network_session_mode_entity_description"><![CDATA[Окреме TCP-з’єднання (та облікові дані SOCKS) використовуватиметься <b>для кожного контакту та члена групи</b>.\n<b>Зверніть увагу</b>: якщо у вас багато з’єднань, заряд акумулятора та споживання трафіку можуть бути значно вищими, а деякі з’єднання можуть бути невдалими.]]></string> + <string name="turning_off_service_and_periodic">Активована оптимізація батареї, вимикається фоновий сервіс і періодичні запити нових повідомлень. Ви можете знову увімкнути їх у налаштуваннях.</string> <string name="back">Назад</string> <string name="bold_text">жирний</string> - <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Корисно для батареї</b>. Фонова служба перевіряє повідомлення кожні 10 хвилин. Ви можете пропустити дзвінки або термінові повідомлення.]]></string> - <string name="settings_audio_video_calls">Аудіо та відео виклики</string> - <string name="icon_descr_audio_off">Звук вимкнено</string> - <string name="auth_unavailable">Автентифікація недоступна</string> - <string name="icon_descr_video_asked_to_receive">Попросили отримати відео</string> - <string name="v4_2_auto_accept_contact_requests">Автоматичне прийняття запитів на контакт</string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Добре для акумулятора</b>. Додаток перевіряє повідомлення кожні 10 хвилин. Ви можете пропустити виклики чи важливі повідомлення.]]></string> + <string name="settings_audio_video_calls">Аудіо та відеовиклики</string> + <string name="icon_descr_audio_off">Аудіо вимкнено</string> + <string name="auth_unavailable">Аутентифікація недоступна</string> + <string name="icon_descr_video_asked_to_receive">Запит на отримання відео</string> + <string name="v4_2_auto_accept_contact_requests">Автоприйняття запитів на контакт</string> <string name="audio_call_no_encryption">аудіовиклик (не зашифрований e2e)</string> - <string name="both_you_and_your_contacts_can_delete">І ви, і ваш контакт можете безповоротно видалити надіслані повідомлення.</string> + <string name="both_you_and_your_contacts_can_delete">Як і ви, так і ваш контакт можете безповоротно видаляти надіслані повідомлення. (24 години)</string> <string name="alert_title_msg_bad_hash">Поганий хеш повідомлення</string> <string name="alert_title_msg_bad_id">Неправильний ідентифікатор повідомлення</string> - <string name="auto_accept_images">Автоматичне прийняття зображень</string> + <string name="auto_accept_images">Автоматично приймати зображення</string> <string name="v5_1_better_messages">Кращі повідомлення</string> - <string name="icon_descr_audio_on">Звук увімкнено</string> - <string name="both_you_and_your_contact_can_add_message_reactions">Реакції на повідомлення можете додавати як ви, так і ваш контакт.</string> + <string name="icon_descr_audio_on">Аудіо увімкнено</string> + <string name="both_you_and_your_contact_can_add_message_reactions">Як ви, так і ваш контакт можуть додавати реакції на повідомлення.</string> <string name="icon_descr_audio_call">аудіовиклик</string> - <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Найкраще для батареї</b>. Ви отримуватимете сповіщення лише тоді, коли додаток працює (БЕЗ фонового сервісу).]]></string> - <string name="v4_6_audio_video_calls">Аудіо та відеодзвінки</string> - <string name="la_authenticate">Автентифікація</string> - <string name="la_auth_failed">Не вдалося пройти автентифікацію</string> - <string name="authentication_cancelled">Аутентифікацію скасовано</string> - <string name="auto_accept_contact">Автоприйняття</string> + <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Найкраще для акумулятора</b>. Ви отримуєте сповіщення тільки тоді, коли додаток працює (НЕТЛЕ фоновий сервіс).]]></string> + <string name="v4_6_audio_video_calls">Аудіо та відеовиклики</string> + <string name="la_authenticate">Аутентифікація</string> + <string name="la_auth_failed">Помилка аутентифікації</string> + <string name="authentication_cancelled">Аутентифікація відмінена</string> + <string name="auto_accept_contact">Автоприйом</string> <string name="integrity_msg_bad_hash">поганий хеш повідомлення</string> - <string name="integrity_msg_bad_id">помилковий ідентифікатор повідомлення</string> + <string name="integrity_msg_bad_id">поганий ідентифікатор повідомлення</string> <string name="color_background">Фон</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Додайте новий контакт</b>: для створення одноразового QR-коду для вашого контакту.]]></string> - <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Його можна вимкнути в налаштуваннях</b> - сповіщення все одно будуть показуватися під час роботи програми.]]></string> - <string name="notifications_mode_service_desc">Фоновий сервіс працює постійно - сповіщення з\'являтимуться, щойно повідомлення стануть доступними.</string> - <string name="icon_descr_asked_to_receive">Попросили отримати зображення</string> + <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Це можна вимкнути в налаштуваннях</b> – сповіщення все одно відображатимуться під час роботи програми.]]></string> + <string name="notifications_mode_service_desc">Служба фонового режиму завжди активна – сповіщення відображатимуться, як тільки повідомлення будуть доступні.</string> + <string name="icon_descr_asked_to_receive">Запит на отримання зображення</string> <string name="attach">Прикріпити</string> <string name="audio_video_calls">Аудіо/відео дзвінки</string> <string name="available_in_v51">" -\nДоступно у v5.1"</string> +\nДоступно в версії 5.1"</string> <string name="calls_prohibited_with_this_contact">Аудіо/відео дзвінки заборонені.</string> <string name="app_name">SimpleX</string> - <string name="both_you_and_your_contact_can_make_calls">Дзвонити можете як ви, так і ваш контакт.</string> - <string name="thousand_abbreviation">k</string> - <string name="connect_via_contact_link">Підключитися через контактну адресу?</string> - <string name="connect_via_invitation_link">Підключитися за одноразовим посиланням?</string> + <string name="both_you_and_your_contact_can_make_calls">Як ви, так і ваш контакт можуть здійснювати дзвінки.</string> + <string name="thousand_abbreviation">тис.</string> + <string name="connect_via_contact_link">Підключитися через адресу контакту?</string> + <string name="connect_via_invitation_link">Підключитися через одноразове посилання?</string> <string name="connect_via_group_link">Приєднатися до групи?</string> - <string name="profile_will_be_sent_to_contact_sending_link">Ваш профіль буде надіслано контакту, від якого ви отримали це посилання.</string> - <string name="you_will_join_group">Ви з\'єднаєтеся з усіма учасниками групи.</string> - <string name="connect_via_link_verb">Підключіться</string> - <string name="server_connected">підключений</string> + <string name="profile_will_be_sent_to_contact_sending_link">Ваш профіль буде відправлено контакту, з якого ви отримали це посилання.</string> + <string name="you_will_join_group">Ви приєднаєтеся до всіх учасників групи.</string> + <string name="connect_via_link_verb">Підключити</string> + <string name="server_connected">підключено</string> <string name="server_error">помилка</string> <string name="server_connecting">підключення</string> - <string name="connected_to_server_to_receive_messages_from_contact">Ви підключені до сервера, який використовується для отримання повідомлень від цього контакту.</string> - <string name="trying_to_connect_to_server_to_receive_messages_with_error">Спроба з\'єднатися з сервером, який використовується для отримання повідомлень від цього контакту (помилка: %1$s ).</string> + <string name="connected_to_server_to_receive_messages_from_contact">Ви підключені до сервера для отримання повідомлень від цього контакту.</string> + <string name="trying_to_connect_to_server_to_receive_messages_with_error">Спроба підключитися до сервера для отримання повідомлень від цього контакту (помилка: %1$s).</string> <string name="deleted_description">видалено</string> - <string name="trying_to_connect_to_server_to_receive_messages">Спроба з\'єднатися з сервером, який використовується для отримання повідомлень від цього контакту.</string> - <string name="marked_deleted_description">з позначкою видалено</string> - <string name="moderated_item_description">модерується %s</string> + <string name="trying_to_connect_to_server_to_receive_messages">Спроба підключитися до сервера для отримання повідомлень від цього контакту.</string> + <string name="marked_deleted_description">відзначено як видалено</string> + <string name="moderated_item_description">модеровано %s</string> <string name="sending_files_not_yet_supported">надсилання файлів поки що не підтримується</string> - <string name="receiving_files_not_yet_supported">отримання файлів поки що не підтримується</string> + <string name="receiving_files_not_yet_supported">приймання файлів поки що не підтримується</string> <string name="sender_you_pronoun">ви</string> <string name="unknown_message_format">невідомий формат повідомлення</string> - <string name="invalid_message_format">невірний формат повідомлення</string> - <string name="live">НАЖИВО</string> - <string name="moderated_description">модерується</string> - <string name="invalid_chat">недійсний чат</string> - <string name="invalid_data">невірні дані</string> - <string name="display_name_connection_established">з\'єднання встановлено</string> - <string name="display_name_invited_to_connect">запрошено до підключення</string> + <string name="invalid_message_format">неправильний формат повідомлення</string> + <string name="live">LIVE</string> + <string name="moderated_description">модеровано</string> + <string name="invalid_chat">неправильний чат</string> + <string name="invalid_data">неправильні дані</string> + <string name="display_name_connection_established">підключення встановлено</string> + <string name="display_name_invited_to_connect">запрошення на підключення</string> <string name="display_name_connecting">підключення…</string> <string name="description_you_shared_one_time_link">ви поділилися одноразовим посиланням</string> - <string name="description_you_shared_one_time_link_incognito">ви поділилися одноразовим посиланням анонімно</string> - <string name="description_via_group_link">через групове посилання</string> - <string name="description_via_group_link_incognito">інкогніто через групове посилання</string> - <string name="description_via_contact_address_link">за посиланням на контактну адресу</string> - <string name="description_via_contact_address_link_incognito">інкогніто за посиланням на контактну адресу</string> - <string name="description_via_one_time_link">за одноразовим посиланням</string> - <string name="description_via_one_time_link_incognito">інкогніто за одноразовим посиланням</string> - <string name="simplex_link_contact">Контактна адреса SimpleX</string> - <string name="simplex_link_invitation">Одноразове запрошення SimpleX</string> - <string name="simplex_link_group">Посилання на групу SimpleX</string> + <string name="description_you_shared_one_time_link_incognito">ви поділилися одноразовим посиланням в інкогніто</string> + <string name="description_via_group_link">через посилання групи</string> + <string name="description_via_group_link_incognito">інкогніто через посилання групи</string> + <string name="description_via_contact_address_link">через посилання адреси контакту</string> + <string name="description_via_contact_address_link_incognito">інкогніто через посилання адреси контакту</string> + <string name="description_via_one_time_link">через одноразове посилання</string> + <string name="description_via_one_time_link_incognito">інкогніто через одноразове посилання</string> + <string name="simplex_link_contact">Адреса контакту у SimpleX</string> + <string name="simplex_link_invitation">Одноразове запрошення у SimpleX</string> + <string name="simplex_link_group">Посилання на групу у SimpleX</string> <string name="simplex_link_connection">через %1$s</string> <string name="simplex_link_mode">Посилання SimpleX</string> <string name="simplex_link_mode_description">Опис</string> <string name="simplex_link_mode_full">Повне посилання</string> <string name="simplex_link_mode_browser">Через браузер</string> - <string name="simplex_link_mode_browser_warning">Відкриття посилання в браузері може знизити конфіденційність і безпеку з\'єднання. Ненадійні посилання SimpleX будуть червоного кольору.</string> + <string name="simplex_link_mode_browser_warning">Відкриття посилання в браузері може зменшити конфіденційність та безпеку з\'єднання. Ненадійні посилання SimpleX будуть виділені червоним кольором.</string> <string name="failed_to_parse_chats_title">Не вдалося завантажити чати</string> - <string name="error_saving_smp_servers">Помилка збереження SMP-серверів</string> - <string name="ensure_smp_server_address_are_correct_format_and_unique">Переконайтеся, що адреси SMP-серверів мають правильний формат, розділені рядками і не дублюються.</string> + <string name="error_saving_smp_servers">Помилка збереження серверів SMP</string> + <string name="ensure_smp_server_address_are_correct_format_and_unique">Переконайтеся, що адреси серверів SMP вірного формату, розділені переносами рядків і не дублюються.</string> <string name="error_setting_network_config">Помилка оновлення конфігурації мережі</string> <string name="failed_to_parse_chat_title">Не вдалося завантажити чат</string> - <string name="contact_developers">Будь ласка, оновіть додаток та зв\'яжіться з розробниками.</string> - <string name="failed_to_create_user_title">Помилка при створенні профілю!</string> - <string name="failed_to_create_user_duplicate_desc">Ви вже маєте профіль у чаті з таким самим іменем. Будь ласка, виберіть інше ім\'я.</string> - <string name="failed_to_active_user_title">Помилка перемикання профілю!</string> - <string name="connection_timeout">Тайм-аут з\'єднання</string> + <string name="contact_developers">Будь ласка, оновіть додаток і зверніться до розробників.</string> + <string name="failed_to_create_user_title">Помилка створення профілю!</string> + <string name="failed_to_create_user_duplicate_desc">Ви вже маєте профіль чату з таким самим ім\'ям відображення. Будь ласка, виберіть інше ім\'я.</string> + <string name="failed_to_active_user_title">Помилка переключення профілю!</string> + <string name="connection_timeout">Тайм-аут підключення</string> <string name="connection_error">Помилка підключення</string> - <string name="network_error_desc">Будь ласка, перевірте своє мережеве з\'єднання за допомогою %1$s і спробуйте ще раз.</string> + <string name="network_error_desc">Будь ласка, перевірте ваше мережеве підключення з %1$s та спробуйте ще раз.</string> <string name="contact_already_exists">Контакт вже існує</string> <string name="connection_error_auth">Помилка підключення (AUTH)</string> - <string name="sender_may_have_deleted_the_connection_request">Можливо, відправник видалив запит на підключення.</string> + <string name="sender_may_have_deleted_the_connection_request">Відправник, можливо, видалив запит на з\'єднання.</string> <string name="error_deleting_contact_request">Помилка видалення запиту на контакт</string> <string name="error_changing_address">Помилка зміни адреси</string> - <string name="error_smp_test_failed_at_step">Помилка тесту на кроці %s.</string> + <string name="error_smp_test_failed_at_step">Тест не пройшов на кроці %s.</string> <string name="error_smp_test_server_auth">Сервер вимагає авторизації для створення черг, перевірте пароль</string> - <string name="error_smp_test_certificate">Можливо, в адресі сервера неправильно вказано відбиток сертифіката</string> + <string name="error_smp_test_certificate">Можливо, відбиток цифрового підпису сертифіката в адресі сервера невірний</string> <string name="smp_server_test_create_queue">Створити чергу</string> <string name="service_notifications">Миттєві сповіщення!</string> <string name="service_notifications_disabled">Миттєві сповіщення вимкнено!</string> <string name="simplex_service_notification_text">Отримання повідомлень…</string> - <string name="hide_notification">Приховати</string> - <string name="ntf_channel_messages">Повідомлення SimpleX Chat</string> - <string name="notification_preview_mode_contact_desc">Показати тільки контакт</string> + <string name="hide_notification">Сховати</string> + <string name="ntf_channel_messages">Повідомлення чату SimpleX</string> + <string name="notification_preview_mode_contact_desc">Показати лише контакт</string> <string name="notification_new_contact_request">Новий запит на контакт</string> <string name="notification_contact_connected">Підключено</string> - <string name="auth_simplex_lock_turned_on">SimpleX Lock увімкнено</string> + <string name="auth_simplex_lock_turned_on">Блокування SimpleX увімкнено</string> <string name="auth_unlock">Розблокувати</string> - <string name="auth_disable_simplex_lock">Вимкнути SimpleX Lock</string> + <string name="auth_disable_simplex_lock">Вимкнути блокування SimpleX</string> <string name="reply_verb">Відповісти</string> <string name="save_verb">Зберегти</string> <string name="delete_verb">Видалити</string> - <string name="delete_message_mark_deleted_warning">Повідомлення буде позначено для видалення. Отримувач(и) зможуть відкрити це повідомлення.</string> - <string name="delete_member_message__question">Видалити повідомлення користувача\?</string> - <string name="for_me_only">Видалити для мене</string> + <string name="delete_message_mark_deleted_warning">Повідомлення буде позначено для видалення. Однак одержувач(і) зможуть розкрити це повідомлення.</string> + <string name="delete_member_message__question">Видалити повідомлення учасника?</string> + <string name="for_me_only">Видалити лише для мене</string> <string name="for_everybody">Для всіх</string> - <string name="icon_descr_edited">відредаговано</string> - <string name="icon_descr_sent_msg_status_send_failed">не вдалося відправити</string> - <string name="icon_descr_received_msg_status_unread">не прочитано</string> + <string name="icon_descr_edited">редаговано</string> + <string name="icon_descr_sent_msg_status_send_failed">помилка відправки</string> + <string name="icon_descr_received_msg_status_unread">непрочитане</string> <string name="group_preview_join_as">приєднатися як %s</string> <string name="icon_descr_cancel_image_preview">Скасувати попередній перегляд зображення</string> <string name="icon_descr_cancel_file_preview">Скасувати попередній перегляд файлу</string> - <string name="icon_descr_waiting_for_image">Очікування зображення</string> - <string name="icon_descr_image_snd_complete">Зображення надіслано</string> - <string name="waiting_for_image">Очікування зображення</string> + <string name="icon_descr_waiting_for_image">Очікування на зображення</string> + <string name="icon_descr_image_snd_complete">Зображення відправлене</string> + <string name="waiting_for_image">Очікування на зображення</string> <string name="large_file">Великий файл!</string> <string name="file_saved">Файл збережено</string> <string name="voice_message">Голосове повідомлення</string> <string name="voice_message_send_text">Голосове повідомлення…</string> - <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Контакт і всі повідомлення будуть видалені - це неможливо скасувати!</string> - <string name="icon_descr_server_status_pending">В очікуванні</string> - <string name="switch_receiving_address_question">Змінити адресу отримання\?</string> + <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Контакт і всі повідомлення будуть видалені - цього не можна скасувати!</string> + <string name="icon_descr_server_status_pending">Очікує</string> + <string name="switch_receiving_address_question">Змінити адресу для отримання?</string> <string name="view_security_code">Переглянути код безпеки</string> - <string name="you_need_to_allow_to_send_voice">Щоб мати змогу надсилати голосові повідомлення, вам потрібно дозволити контакту надсилати їх.</string> + <string name="you_need_to_allow_to_send_voice">Ви повинні дозволити вашому контакту надсилати голосові повідомлення, щоб мати змогу надсилати їх.</string> <string name="cancel_verb">Скасувати</string> - <string name="ok">ГАРАЗД</string> + <string name="ok">OK</string> <string name="copied">Скопійовано в буфер обміну</string> - <string name="to_connect_via_link_title">Підключитися за посиланням</string> - <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 мобільний: торкніться <b>Відкрийте в мобільному додатку</b>, потім торкніться <b>Підключіть</b> в додатку.]]></string> - <string name="mute_chat">Вимкнути звук</string> - <string name="unmute_chat">Увімкнути звук</string> - <string name="you_invited_a_contact">Ви запросили свого контакта</string> - <string name="contact_you_shared_link_with_wont_be_able_to_connect">Контакт, якому ви надали це посилання, НЕ зможе підключитися!</string> - <string name="icon_descr_profile_image_placeholder">заповнювач зображення профілю</string> + <string name="to_connect_via_link_title">Для підключення через посилання</string> + <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 мобільний: натисніть <b>Відкрити у мобільному додатку</b>, а потім торкніться <b>Підключити</b> в додатку.]]></string> + <string name="mute_chat">Приглушити</string> + <string name="unmute_chat">Скасувати приглушення</string> + <string name="you_invited_a_contact">Ви запросили контакт</string> + <string name="contact_you_shared_link_with_wont_be_able_to_connect">Контакт, якому ви поділилися посиланням, НЕ зможе підключитися!</string> + <string name="icon_descr_profile_image_placeholder">аватар не встановлено</string> <string name="image_descr_qr_code">QR-код</string> - <string name="icon_descr_help">допомога</string> - <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Якщо ви не можете зустрітися особисто, <b>покажіть QR-код у відеодзвінку</b> або поділіться посиланням.]]></string> - <string name="your_chat_profile_will_be_sent_to_your_contact">Ваш профіль чату буде надіслано -\nдо вашого контакту</string> - <string name="one_time_link">Посилання на одноразове запрошення</string> - <string name="incorrect_code">Неправильний код безпеки!</string> - <string name="to_verify_compare">Щоб перевірити наскрізне шифрування з вашим контактом, порівняйте (або відскануйте) код на ваших пристроях.</string> - <string name="your_settings">Ваші налаштування</string> - <string name="your_simplex_contact_address">Ваша адреса SimpleX</string> - <string name="markdown_help">Допомога з уцінкою</string> - <string name="chat_lock">SimpleX Lock</string> + <string name="icon_descr_help">довідка</string> + <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Якщо ви не можете зустрітися особисто, <b>покажіть QR-код у відеовиклику</b>, або поділіться посиланням.]]></string> + <string name="your_chat_profile_will_be_sent_to_your_contact">Ваш профіль буде відправлено \nвашому контакту</string> + <string name="one_time_link">Одноразове запрошення</string> + <string name="incorrect_code">Невірний код безпеки!</string> + <string name="to_verify_compare">Для перевірки end-to-end шифрування порівняйте (або скануйте) код на своїх пристроях.</string> + <string name="your_settings">Налаштування</string> + <string name="your_simplex_contact_address">Ваша SimpleX-адреса</string> + <string name="markdown_help">Допомога з Markdown</string> + <string name="chat_lock">Блокування SimpleX</string> <string name="chat_console">Консоль чату</string> <string name="smp_servers">Сервери SMP</string> - <string name="smp_servers_scan_qr">Відскануйте QR-код сервера</string> - <string name="smp_servers_use_server_for_new_conn">Використовуйте для нових з\'єднань</string> + <string name="smp_servers_scan_qr">Сканувати QR-код сервера</string> + <string name="smp_servers_use_server_for_new_conn">Використовувати для нових підключень</string> <string name="smp_servers_delete_server">Видалити сервер</string> - <string name="star_on_github">Зірка на GitHub</string> - <string name="how_to_use_your_servers">Як користуватися вашими серверами</string> - <string name="saved_ICE_servers_will_be_removed">Збережені сервери WebRTC ICE буде видалено.</string> - <string name="configure_ICE_servers">Налаштування серверів ICE</string> + <string name="star_on_github">Оцінити на GitHub</string> + <string name="how_to_use_your_servers">Як використовувати власні сервери</string> + <string name="saved_ICE_servers_will_be_removed">Збережені сервери WebRTC ICE будуть видалені.</string> + <string name="configure_ICE_servers">Налаштувати сервери ICE</string> <string name="network_and_servers">Мережа та сервери</string> - <string name="network_settings_title">Налаштування мережі</string> - <string name="network_enable_socks">Використовувати SOCKS проксі\?</string> - <string name="network_disable_socks">Використовуєте пряме підключення до Інтернету\?</string> - <string name="update_onion_hosts_settings_question">Оновити налаштування хостів .onion\?</string> - <string name="network_use_onion_hosts">Використовуйте хости .onion</string> - <string name="network_use_onion_hosts_prefer">При наявності</string> + <string name="network_settings_title">Розширені налаштування</string> + <string name="network_enable_socks">Використовувати SOCKS-проксі?</string> + <string name="network_disable_socks">Використовувати прямий підключення до Інтернету?</string> + <string name="network_use_onion_hosts">Використовувати .onion-хости</string> + <string name="network_use_onion_hosts_prefer">Якщо доступно</string> <string name="network_use_onion_hosts_no">Ні</string> - <string name="network_use_onion_hosts_prefer_desc">Onion хости будуть використовуватися за наявності.</string> - <string name="update_network_session_mode_question">Оновити режим транспортної ізоляції\?</string> + <string name="network_use_onion_hosts_prefer_desc">.Onion-хости будуть використовуватися, якщо доступні.</string> + <string name="update_network_session_mode_question">Оновити режим ізоляції транспорту?</string> <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> <string name="create_address">Створити адресу</string> - <string name="share_link">Поділіться посиланням</string> + <string name="share_link">Поділитися посиланням</string> <string name="delete_address">Видалити адресу</string> <string name="full_name__field">Повне ім\'я:</string> <string name="you_control_your_chat">Ви керуєте своїм чатом!</string> - <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Платформа для обміну повідомленнями та додатків, що захищає вашу конфіденційність і безпеку.</string> + <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Платформа обміну повідомленнями і застосунок, які захищають вашу конфіденційність та безпеку.</string> <string name="display_name">Введіть своє ім\'я:</string> - <string name="callstatus_in_progress">виклик у процесі</string> - <string name="callstate_starting">починаючи…</string> - <string name="first_platform_without_user_ids">Перша платформа без жодних ідентифікаторів користувачів – приватна за дизайном.</string> - <string name="decentralized">Децентралізований</string> - <string name="use_chat">Використовуйте чат</string> - <string name="onboarding_notifications_mode_subtitle">Пізніше його можна змінити через налаштування.</string> + <string name="callstatus_in_progress">дзвінок в процесі</string> + <string name="callstate_starting">запуск…</string> + <string name="first_platform_without_user_ids">Ніяких ідентифікаторів користувачів.</string> + <string name="decentralized">Децентралізована</string> + <string name="use_chat">Використовувати чат</string> + <string name="onboarding_notifications_mode_subtitle">Як це впливає на батарею</string> <string name="onboarding_notifications_mode_service">Миттєво</string> - <string name="call_already_ended">Дзвінок вже закінчився!</string> - <string name="your_calls">Ваші дзвінки</string> + <string name="call_already_ended">Виклик вже завершено!</string> + <string name="your_calls">Виклики</string> <string name="your_ice_servers">Ваші сервери ICE</string> <string name="open_verb">Відкрити</string> <string name="call_connection_via_relay">через реле</string> <string name="icon_descr_speaker_on">Динамік увімкнено</string> - <string name="icon_descr_flip_camera">Перевернути камеру</string> - <string name="icon_descr_call_rejected">Відхилений дзвінок</string> - <string name="integrity_msg_skipped">%1$d пропущено повідомлення(і)</string> + <string name="icon_descr_flip_camera">Повернути камеру</string> + <string name="icon_descr_call_rejected">Відхилений виклик</string> + <string name="integrity_msg_skipped">%1$d пропущено повідомлень</string> <string name="settings_section_title_chats">ЧАТИ</string> - <string name="settings_section_title_socks">SOCKS PROXY</string> - <string name="error_starting_chat">Помилка запуску чату</string> + <string name="settings_section_title_socks">SOCKS-ПРОКСІ</string> + <string name="error_starting_chat">Помилка при запуску чату</string> <string name="stop_chat_confirmation">Зупинити</string> - <string name="import_database_confirmation">Імпорт</string> + <string name="import_database_confirmation">Імпортувати</string> <string name="files_and_media_section">Файли та медіа</string> <string name="messages_section_title">Повідомлення</string> - <string name="error_changing_message_deletion">Налаштування зміни помилки</string> + <string name="error_changing_message_deletion">Помилка зміни налаштувань</string> <string name="database_encrypted">База даних зашифрована!</string> - <string name="new_passphrase">Новий пароль…</string> - <string name="confirm_new_passphrase">Підтвердіть нову парольну фразу…</string> + <string name="new_passphrase">Нова ключова фраза…</string> + <string name="confirm_new_passphrase">Підтвердіть нову ключову фразу…</string> <string name="encrypt_database_question">Зашифрувати базу даних\?</string> - <string name="wrong_passphrase_title">Неправильна парольна фраза!</string> - <string name="enter_correct_passphrase">Введіть правильну парольну фразу.</string> - <string name="chat_archive_header">Архів чату</string> - <string name="chat_archive_section">АРХІВ ЧАТУ</string> - <string name="rcv_group_event_member_connected">підключений</string> - <string name="rcv_group_event_changed_your_role">змінив свою роль на %s</string> - <string name="snd_group_event_changed_role_for_yourself">ви змінили роль для себе на %s</string> + <string name="wrong_passphrase_title">Неправильна ключова фраза!</string> + <string name="enter_correct_passphrase">Введіть правильну ключову фразу.</string> + <string name="rcv_group_event_member_connected">підключив(лась)</string> + <string name="rcv_group_event_changed_your_role">змінив(ла) вашу роль на %s</string> + <string name="snd_group_event_changed_role_for_yourself">ви змінили свою роль на %s</string> <string name="snd_conn_event_switch_queue_phase_completed">ви змінили адресу</string> - <string name="no_contacts_selected">Не вибрано жодного контакту</string> + <string name="no_contacts_selected">Не вибрано контактів</string> <string name="delete_group_question">Видалити групу\?</string> - <string name="remove_member_confirmation">Видалити</string> + <string name="remove_member_confirmation">Вилучити</string> <string name="info_row_connection">Підключення</string> - <string name="conn_level_desc_direct">прямий</string> - <string name="update_network_settings_confirmation">Оновлення</string> + <string name="conn_level_desc_direct">пряме</string> + <string name="update_network_settings_confirmation">Оновити</string> <string name="chat_preferences_contact_allows">Контакт дозволяє</string> <string name="chat_preferences">Налаштування чату</string> - <string name="prohibit_sending_disappearing_messages">Заборонити надсилання зникаючих повідомлень.</string> - <string name="prohibit_sending_voice_messages">Заборонити надсилання голосових повідомлень.</string> + <string name="prohibit_sending_disappearing_messages">Заборонити надсилання повідомлень, які зникають.</string> + <string name="prohibit_sending_voice_messages">Забороняйте надсилання голосових повідомлень.</string> <string name="v4_4_french_interface">Французький інтерфейс</string> - <string name="error_saving_xftp_servers">Помилка збереження XFTP-серверів</string> - <string name="ensure_xftp_server_address_are_correct_format_and_unique">Переконайтеся, що адреси XFTP-серверів мають правильний формат, розділені рядками і не дублюються.</string> - <string name="error_loading_xftp_servers">Помилка завантаження XFTP-серверів</string> - <string name="error_adding_members">Помилка додавання користувача(ів)</string> + <string name="error_saving_xftp_servers">Помилка збереження серверів XFTP</string> + <string name="ensure_xftp_server_address_are_correct_format_and_unique">Переконайтеся, що адреси серверів XFTP вірного формату, розділені переносами рядків і не дублюються.</string> + <string name="error_loading_xftp_servers">Помилка завантаження серверів XFTP</string> + <string name="error_adding_members">Помилка додавання учасників</string> <string name="error_joining_group">Помилка приєднання до групи</string> - <string name="cannot_receive_file">Не вдається отримати файл</string> + <string name="cannot_receive_file">Неможливо отримати файл</string> <string name="smp_server_test_compare_file">Порівняти файл</string> <string name="smp_server_test_delete_file">Видалити файл</string> <string name="periodic_notifications_disabled">Періодичні сповіщення вимкнено!</string> - <string name="enter_passphrase_notification_title">Потрібна парольна фраза</string> + <string name="enter_passphrase_notification_title">Потрібен пароль</string> <string name="la_notice_turn_on">Увімкнути</string> - <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Автентифікація пристрою не ввімкнена. Ви можете увімкнути SimpleX Lock у Налаштуваннях, коли увімкнете автентифікацію пристрою.</string> - <string name="auth_device_authentication_is_disabled_turning_off">Автентифікацію пристрою вимкнено. Вимкнення SimpleX Lock.</string> + <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Аутентифікація пристрою не увімкнена. Ви можете увімкнути блокування SimpleX через налаштування, якщо увімкнете аутентифікацію пристрою.</string> + <string name="auth_device_authentication_is_disabled_turning_off">Аутентифікація пристрою вимкнена. Вимикається блокування SimpleX.</string> <string name="auth_stop_chat">Зупинити чат</string> - <string name="auth_open_chat_console">Відкрийте консоль чату</string> + <string name="auth_open_chat_console">Відкрити консоль чату</string> <string name="moderate_message_will_be_marked_warning">Повідомлення буде позначено як модероване для всіх учасників.</string> <string name="your_chats">Чати</string> <string name="contact_connection_pending">підключення…</string> <string name="group_connection_pending">підключення…</string> - <string name="tap_to_start_new_chat">Натисніть, щоб почати новий чат</string> - <string name="chat_with_developers">Чат з розробниками</string> + <string name="tap_to_start_new_chat">Торкніться, щоб розпочати новий чат</string> + <string name="chat_with_developers">Чат із розробниками</string> <string name="you_have_no_chats">У вас немає чатів</string> - <string name="observer_cant_send_message_title">Ви не можете надсилати повідомлення!</string> - <string name="observer_cant_send_message_desc">Будь ласка, зверніться до адміністратора групи.</string> - <string name="file_will_be_received_when_contact_is_online">Файл буде отримано, коли ваш контакт буде онлайн, будь ласка, зачекайте або перевірте пізніше!</string> - <string name="icon_descr_video_snd_complete">Відео надіслано</string> - <string name="waiting_for_video">В очікуванні відео</string> + <string name="observer_cant_send_message_title">Ви не можете відправляти повідомлення!</string> + <string name="observer_cant_send_message_desc">Будь ласка, зв\'яжіться з адміністратором групи.</string> + <string name="file_will_be_received_when_contact_is_online">Файл буде отримано, коли ваш контакт буде в мережі, будь ласка, зачекайте або перевірте пізніше!</string> + <string name="icon_descr_video_snd_complete">Відео відправлене</string> + <string name="waiting_for_video">Очікування на відео</string> <string name="icon_descr_file">Файл</string> <string name="icon_descr_server_status_connected">Підключено</string> <string name="icon_descr_server_status_disconnected">Відключено</string> <string name="live_message">Живе повідомлення!</string> - <string name="ask_your_contact_to_enable_voice">Будь ласка, попросіть вашого контакту увімкнути відправку голосових повідомлень.</string> - <string name="send_live_message">Надіслати повідомлення в прямому ефірі</string> - <string name="send_live_message_desc">Надішліть повідомлення в реальному часі - воно буде оновлюватися для одержувача (одержувачів), поки ви його вводите</string> + <string name="ask_your_contact_to_enable_voice">Будь ласка, попросіть вашого контакту увімкнути надсилання голосових повідомлень.</string> + <string name="send_live_message">Надіслати живе повідомлення</string> + <string name="send_live_message_desc">Надішліть живе повідомлення - воно буде оновлюватися для одержувача(-ів), коли ви його набираєте</string> <string name="send_verb">Надіслати</string> - <string name="to_start_a_new_chat_help_header">Щоб почати новий чат</string> + <string name="to_start_a_new_chat_help_header">Щоб розпочати новий чат</string> <string name="chat_help_tap_button">Натисніть кнопку</string> - <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Якщо ви вирішите відхилити, відправник НЕ отримає сповіщення.</string> + <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Якщо ви виберете відхилити, відправник НЕ буде повідомлений.</string> <string name="clear_chat_button">Очистити чат</string> - <string name="invalid_contact_link">Невірне посилання!</string> - <string name="this_link_is_not_a_valid_connection_link">Це посилання не є дійсним посиланням для підключення!</string> - <string name="connection_request_sent">Запит на підключення відправлено!</string> - <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Ви також можете підключитися за посиланням. Якщо воно відкриється в браузері, натисніть кнопку <b>Відкрити в мобільному додатку</b>.]]></string> - <string name="create_one_time_link">Створіть одноразове посилання-запрошення</string> + <string name="invalid_contact_link">Неправильне посилання!</string> + <string name="this_link_is_not_a_valid_connection_link">Це посилання не є дійсним з\'єднувальним посиланням!</string> + <string name="connection_request_sent">Запит на з\'єднання відправлено!</string> + <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Ви також можете підключитися, клацнувши посилання. Якщо воно відкривається у браузері, клацніть кнопку <b>Відкрити у мобільному додатку</b>.]]></string> <string name="scan_code">Сканувати код</string> - <string name="scan_code_from_contacts_app">Відскануйте код безпеки з додатку вашого контакту.</string> - <string name="smp_servers_invalid_address">Неправильна адреса сервера!</string> - <string name="smp_servers_check_address">Перевірте адресу сервера та спробуйте ще раз.</string> + <string name="scan_code_from_contacts_app">Скануйте код безпеки з додатка вашого контакту.</string> + <string name="smp_servers_invalid_address">Невірна адреса сервера!</string> + <string name="smp_servers_check_address">Перевірте адресу сервера і спробуйте ще раз.</string> <string name="xftp_servers">Сервери XFTP</string> - <string name="install_simplex_chat_for_terminal">Встановіть SimpleX Chat для терміналу</string> - <string name="contribute">Зробити внесок</string> - <string name="rate_the_app">Оцініть програму</string> + <string name="install_simplex_chat_for_terminal">Встановити SimpleX Chat для терміналу</string> + <string name="contribute">Внести вклад</string> + <string name="rate_the_app">Оцініть додаток</string> <string name="your_SMP_servers">Ваші сервери SMP</string> <string name="your_XFTP_servers">Ваші сервери XFTP</string> <string name="using_simplex_chat_servers">Використання серверів SimpleX Chat.</string> - <string name="how_to">Як зробити</string> - <string name="callstatus_calling">телефоную…</string> - <string name="callstatus_missed">пропущений дзвінок</string> + <string name="how_to">Як користуватися</string> + <string name="callstatus_calling">дзвінок…</string> + <string name="callstatus_missed">пропущений виклик</string> <string name="callstatus_rejected">відхилений виклик</string> - <string name="incoming_video_call">Вхідний відеодзвінок</string> + <string name="incoming_video_call">Вхідний відеовиклик</string> <string name="incoming_audio_call">Вхідний аудіовиклик</string> - <string name="video_call_no_encryption">відеодзвінок (без шифрування e2e)</string> - <string name="call_on_lock_screen">Дзвінки на екрані блокування:</string> - <string name="call_connection_peer_to_peer">одноранговий</string> - <string name="icon_descr_hang_up">Покласти слухавку</string> - <string name="archive_created_on_ts">Створено на %1$s</string> - <string name="icon_descr_expand_role">Розширити вибір ролей</string> - <string name="chat_preferences_yes">Так</string> - <string name="contact_preferences">Налаштування контактів</string> + <string name="video_call_no_encryption">відеовиклик (не зашифрований e2e)</string> + <string name="call_on_lock_screen">Виклики на екрані блокування:</string> + <string name="call_connection_peer_to_peer">від абонента до абонента</string> + <string name="icon_descr_hang_up">Завершити дзвінок</string> + <string name="icon_descr_expand_role">Розгорнути вибір ролі</string> + <string name="chat_preferences_yes">так</string> + <string name="contact_preferences">Налаштування контакту</string> <string name="v4_2_security_assessment_desc">Безпека SimpleX Chat була перевірена компанією Trail of Bits.</string> - <string name="v4_3_irreversible_message_deletion_desc">Ваші контакти можуть дозволити повне видалення повідомлень.</string> + <string name="v4_3_irreversible_message_deletion_desc">Ваші контакти можуть дозволити остаточне видалення повідомлень.</string> <string name="database_will_be_encrypted">База даних буде зашифрована.</string> - <string name="keychain_error">Помилка ланцюжка ключів</string> + <string name="keychain_error">Помилка сховища ключів</string> <string name="unknown_error">Невідома помилка</string> - <string name="database_restore_error">Відновлення помилки бази даних</string> - <string name="snd_group_event_user_left">ви залишили</string> + <string name="database_restore_error">Помилка відновлення бази даних</string> + <string name="snd_group_event_user_left">ви залишили групу</string> <string name="group_member_status_accepted">підключення (прийнято)</string> <string name="button_add_members">Запросити учасників</string> <string name="button_leave_group">Покинути групу</string> - <string name="change_verb">Зміна</string> - <string name="sending_via">Відправлення через</string> + <string name="change_verb">Змінити</string> + <string name="sending_via">Надсилання через</string> <string name="network_status">Стан мережі</string> <string name="update_network_settings_question">Оновити налаштування мережі\?</string> - <string name="users_delete_data_only">Тільки локальні дані профілю</string> - <string name="incognito_random_profile">Ваш випадковий профіль</string> - <string name="feature_enabled">увімкнено</string> - <string name="feature_enabled_for_you">увімкнено для вас</string> - <string name="prohibit_direct_messages">Заборонити надсилати прямі повідомлення учасникам.</string> + <string name="users_delete_data_only">Локальні дані профілю тільки</string> + <string name="incognito_random_profile">Випадковий профіль</string> + <string name="feature_enabled">ввімкнено</string> + <string name="feature_enabled_for_you">ввімкнено для вас</string> + <string name="prohibit_direct_messages">Забороняйте надсилання прямих повідомлень учасникам.</string> <string name="delete_after">Видалити після</string> <string name="v4_2_security_assessment">Оцінка безпеки</string> - <string name="v4_2_group_links">Посилання на групи</string> - <string name="v4_3_improved_privacy_and_security">Покращена конфіденційність та безпека</string> + <string name="v4_2_group_links">Посилання на групу</string> + <string name="v4_3_improved_privacy_and_security">Покращена конфіденційність і безпека</string> <string name="v4_4_live_messages">Живі повідомлення</string> - <string name="v4_4_verify_connection_security">Перевірте безпеку підключення</string> - <string name="v4_4_verify_connection_security_desc">Порівняйте коди безпеки зі своїми контактами.</string> - <string name="v4_5_multiple_chat_profiles">Кілька профілів чату</string> - <string name="core_version">Основна версія: v%s</string> + <string name="v4_4_verify_connection_security">Перевірка безпеки підключення</string> + <string name="v4_4_verify_connection_security_desc">Порівнюйте коди безпеки із своїми контактами.</string> + <string name="v4_5_multiple_chat_profiles">Декілька профілів чату</string> + <string name="core_version">Версія ядра: v%s</string> <string name="delete_address__question">Видалити адресу\?</string> <string name="display_name__field">Ім\'я профілю:</string> - <string name="callstate_waiting_for_confirmation">чекаємо на підтвердження…</string> - <string name="privacy_redefined">Переосмислення конфіденційності</string> - <string name="people_can_connect_only_via_links_you_share">Люди можуть підключатися до вас лише за посиланнями, якими ви ділитеся.</string> + <string name="callstate_waiting_for_confirmation">очікування підтвердження…</string> + <string name="privacy_redefined">Приватність перевизначена</string> + <string name="people_can_connect_only_via_links_you_share">Ви вирішуєте, хто може під\'єднатися.</string> <string name="how_simplex_works">Як працює SimpleX</string> - <string name="read_more_in_github">Детальніше читайте в нашому репозиторії GitHub.</string> - <string name="encrypted_audio_call">e2e зашифрований аудіодзвінок</string> - <string name="open_simplex_chat_to_accept_call">Відкрийте SimpleX Chat, щоб прийняти дзвінок</string> - <string name="status_e2e_encrypted">e2e зашифрований</string> + <string name="encrypted_audio_call">зашифрований e2e аудіовиклик</string> + <string name="open_simplex_chat_to_accept_call">Відкрийте SimpleX Chat для прийняття виклику</string> + <string name="status_e2e_encrypted">e2e зашифровано</string> <string name="icon_descr_speaker_off">Динамік вимкнено</string> - <string name="icon_descr_call_pending_sent">Дзвінок в очікуванні</string> - <string name="icon_descr_call_missed">Пропущений дзвінок</string> - <string name="icon_descr_call_connecting">З\'єднувальний дзвінок</string> + <string name="icon_descr_call_pending_sent">Очікування виклику</string> + <string name="icon_descr_call_missed">Пропущений виклик</string> + <string name="icon_descr_call_connecting">Підключення виклику</string> <string name="privacy_and_security">Конфіденційність і безпека</string> - <string name="your_privacy">Ваша конфіденційність</string> + <string name="your_privacy">Конфіденційність</string> <string name="settings_section_title_settings">НАЛАШТУВАННЯ</string> <string name="settings_section_title_help">ДОПОМОГА</string> - <string name="settings_section_title_support">ПІДТРИМКА SIMPLEX CHAT</string> + <string name="settings_section_title_support">ПІДТРИМАЙТЕ SIMPLEX CHAT</string> <string name="stop_chat_to_export_import_or_delete_chat_database">Зупиніть чат, щоб експортувати, імпортувати або видалити базу даних чату. Ви не зможете отримувати та надсилати повідомлення, поки чат зупинено.</string> <string name="error_deleting_database">Помилка видалення бази даних чату</string> - <string name="notifications_will_be_hidden">Сповіщення будуть надходити лише до моменту зупинки програми!</string> - <string name="remove_passphrase">Видалити</string> + <string name="notifications_will_be_hidden">Сповіщення будуть доставлятися лише до зупинки додатка!</string> + <string name="remove_passphrase">Вилучити</string> <string name="encrypt_database">Зашифрувати</string> - <string name="update_database">Оновлення</string> - <string name="current_passphrase">Поточна парольна фраза…</string> - <string name="database_will_be_encrypted_and_passphrase_stored">База даних буде зашифрована, а пароль зберігатиметься у сховищі ключів.</string> - <string name="database_encryption_will_be_updated">Ключову фразу шифрування бази даних буде оновлено і збережено у сховищі ключів.</string> - <string name="rcv_group_event_member_added">запрошені %1$s</string> - <string name="rcv_group_event_invited_via_your_group_link">запрошені за посиланням у вашій групі</string> + <string name="update_database">Оновити</string> + <string name="current_passphrase">Поточна ключова фраза…</string> + <string name="database_will_be_encrypted_and_passphrase_stored">База даних буде зашифрована, і ключова фраза буде збережена в сховищі ключів.</string> + <string name="database_encryption_will_be_updated">Ключова фраза шифрування бази даних буде оновлена і збережена в сховищі ключів.</string> + <string name="rcv_group_event_member_added">запросив(ла) %1$s</string> + <string name="rcv_group_event_invited_via_your_group_link">запросив(ла) через ваше посилання на групу</string> <string name="snd_group_event_changed_member_role">ви змінили роль %s на %s</string> <string name="snd_conn_event_switch_queue_phase_completed_for_member">ви змінили адресу для %s</string> - <string name="snd_conn_event_switch_queue_phase_changing_for_member">зміна адреси для %s…</string> - <string name="invite_prohibited">Не вдається запросити контакт!</string> - <string name="group_info_section_title_num_members">%1$s УЧАСНИКИ</string> + <string name="snd_conn_event_switch_queue_phase_changing_for_member">змінює адресу для %s…</string> + <string name="invite_prohibited">Неможливо запросити контакт!</string> + <string name="group_info_section_title_num_members">%1$s УЧАСНИКІВ</string> <string name="button_delete_group">Видалити групу</string> <string name="group_link">Посилання на групу</string> - <string name="button_edit_group_profile">Редагування профілю групи</string> - <string name="create_group_link">Створити групове посилання</string> + <string name="button_edit_group_profile">Редагувати профіль групи</string> + <string name="create_group_link">Створити посилання на групу</string> <string name="change_member_role_question">Змінити роль у групі\?</string> - <string name="error_removing_member">Помилка видалення учасника</string> - <string name="group_main_profile_sent">Ваш профіль у чаті буде надіслано учасникам групи</string> - <string name="save_color">Зберегти колір</string> - <string name="full_deletion">Видалити для всіх</string> + <string name="error_removing_member">Помилка при вилученні учасника</string> + <string name="group_main_profile_sent">Ваш профіль буде відправлений учасникам групи</string> + <string name="full_deletion">Видалення для всіх</string> <string name="voice_messages">Голосові повідомлення</string> - <string name="voice_prohibited_in_this_chat">Голосові повідомлення в цьому чаті заборонені.</string> - <string name="v4_3_voice_messages_desc">Максимум 40 секунд, отримується миттєво.</string> - <string name="v5_0_app_passcode_descr">Встановіть його замість системної автентифікації.</string> + <string name="voice_prohibited_in_this_chat">Голосові повідомлення заборонені в цьому чаті.</string> + <string name="v4_3_voice_messages_desc">Максимум 40 секунд, надходять миттєво.</string> + <string name="v5_0_app_passcode_descr">Встановіть його замість системної аутентифікації.</string> <string name="shutdown_alert_question">Вимкнути\?</string> - <string name="share_with_contacts">Поділіться з контактами</string> - <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ваш профіль зберігається на вашому пристрої та доступний лише вашим контактам. Сервери SimpleX не бачать ваш профіль.</string> - <string name="save_and_notify_contacts">Збереження та сповіщення контактів</string> - <string name="save_and_notify_group_members">Збереження та сповіщення учасників групи</string> - <string name="exit_without_saving">Вихід без збереження</string> - <string name="hide_profile">Приховати профіль</string> - <string name="password_to_show">Показати пароль</string> + <string name="share_with_contacts">Поділитися з контактами</string> + <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ваш профіль зберігається на вашому пристрої та ділиться лише з вашими контактами. Серверам SimpleX профіль недоступний.</string> + <string name="save_and_notify_contacts">Зберегти та сповістити контакти</string> + <string name="save_and_notify_group_members">Зберегти та сповістити учасників</string> + <string name="exit_without_saving">Вийти без збереження</string> + <string name="hide_profile">Сховати профіль</string> + <string name="password_to_show">Пароль для відображення</string> <string name="create_profile_button">Створити</string> - <string name="status_no_e2e_encryption">немає шифрування e2e</string> - <string name="status_contact_has_e2e_encryption">контакт має шифрування e2e</string> - <string name="alert_text_msg_bad_hash">Хеш попереднього повідомлення відрізняється.</string> - <string name="confirm_passcode">Підтвердьте пароль</string> + <string name="status_no_e2e_encryption">без зашифрування e2e</string> + <string name="status_contact_has_e2e_encryption">контакт має зашифрування e2e</string> + <string name="alert_text_msg_bad_hash">Хеш попереднього повідомлення інший.\"</string> + <string name="confirm_passcode">Підтвердити пароль</string> <string name="new_passcode">Новий пароль</string> - <string name="settings_restart_app">Перезавантажити</string> - <string name="your_chat_database">Ваша база даних чату</string> + <string name="settings_restart_app">Перезапустити</string> + <string name="your_chat_database">База даних чату</string> <string name="chat_is_stopped">Чат зупинено</string> <string name="chat_database_section">БАЗА ДАНИХ ЧАТУ</string> <string name="new_database_archive">Новий архів бази даних</string> <string name="stop_chat_question">Зупинити чат\?</string> - <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Вашу поточну базу даних чату буде ВИДАЛЕНО та ЗАМІНЕНО імпортованою. -\nЦю дію неможливо скасувати – ваш профіль, контакти, повідомлення та файли будуть безповоротно втрачені.</string> + <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Ваша поточна база даних чату буде ВИДАЛЕНА та ЗАМІНЕНА імпортованою. +\nЦя дія незворотня - ваш профіль, контакти, повідомлення та файли буде втрачено безповоротно.</string> <string name="chat_item_ttl_none">ніколи</string> <string name="chat_item_ttl_seconds">%s секунд(и)</string> - <string name="downgrade_and_open_chat">Пониження та відкритий чат</string> + <string name="downgrade_and_open_chat">Знизити версію та відкрити чат</string> <string name="new_member_role">Нова роль учасника</string> <string name="delete_link_question">Видалити посилання\?</string> <string name="delete_link">Видалити посилання</string> - <string name="switch_verb">Перемикач</string> - <string name="error_changing_role">Помилка, що змінює роль</string> + <string name="switch_verb">Перемкнути</string> + <string name="error_changing_role">Помилка при зміні ролі</string> <string name="group_display_name_field">Введіть назву групи:</string> <string name="group_full_name_field">Повна назва групи:</string> - <string name="error_saving_group_profile">Помилка збереження профілю групи</string> - <string name="network_options_reset_to_defaults">Скидання до налаштувань за замовчуванням</string> + <string name="error_saving_group_profile">Помилка при збереженні профілю групи</string> + <string name="network_options_reset_to_defaults">Скинути на замовчування</string> <string name="network_option_seconds_label">сек</string> <string name="network_option_protocol_timeout">Тайм-аут протоколу</string> <string name="network_options_save">Зберегти</string> - <string name="updating_settings_will_reconnect_client_to_all_servers">Оновлення налаштувань призведе до перепідключення клієнта до всіх серверів.</string> - <string name="user_mute">Вимкнути звук</string> - <string name="enter_password_to_show">Введіть пароль у пошуку</string> - <string name="both_you_and_your_contact_can_send_disappearing">Ви і ваш контакт можете надсилати зникаючі повідомлення.</string> - <string name="only_you_can_delete_messages">Тільки ви можете безповоротно видалити повідомлення (ваш контакт може позначити їх для видалення).</string> + <string name="updating_settings_will_reconnect_client_to_all_servers">Оновлення налаштувань призведе до повторного підключення клієнта до всіх серверів.</string> + <string name="user_mute">Приглушити</string> + <string name="enter_password_to_show">Введіть пароль для пошуку</string> + <string name="both_you_and_your_contact_can_send_disappearing">Як ви, так і ваш контакт можуть надсилати повідомлення, які зникають.</string> + <string name="only_you_can_delete_messages">Тільки ви можете безповоротно видаляти повідомлення (ваш контакт може позначати їх для видалення). (24 години)</string> <string name="only_your_contact_can_add_message_reactions">Тільки ваш контакт може додавати реакції на повідомлення.</string> - <string name="only_your_contact_can_delete">Тільки ваш контакт може безповоротно видалити повідомлення (ви можете позначити їх для видалення).</string> + <string name="only_your_contact_can_delete">Тільки ваш контакт може безповоротно видаляти повідомлення (ви можете позначати їх для видалення). (24 години)</string> <string name="only_your_contact_can_send_voice">Тільки ваш контакт може надсилати голосові повідомлення.</string> - <string name="prohibit_sending_disappearing">Заборонити надсилання зникаючих повідомлень.</string> - <string name="prohibit_message_deletion">Заборонити безповоротне видалення повідомлень.</string> - <string name="group_members_can_send_voice">Учасники групи можуть надсилати голосові повідомлення.</string> + <string name="prohibit_sending_disappearing">Забороняйте надсилання повідомлень, які зникають.</string> + <string name="prohibit_message_deletion">Забороняйте невідворотне видалення повідомлень.</string> + <string name="group_members_can_send_voice">Учасники можуть надсилати голосові повідомлення.</string> <string name="ttl_m">%dм</string> <string name="new_in_version">Нове в %s</string> - <string name="v5_1_self_destruct_passcode">Пароль самознищення</string> + <string name="v5_1_self_destruct_passcode">Самознищуючий пароль</string> <string name="v4_5_italian_interface">Італійський інтерфейс</string> - <string name="custom_time_unit_hours">годин</string> - <string name="custom_time_unit_days">днів</string> - <string name="custom_time_picker_select">Виберіть</string> - <string name="custom_time_picker_custom">нестандартний</string> - <string name="secret_text">таємниця</string> - <string name="join_group_button">Приєднуйтесь</string> + <string name="custom_time_unit_hours">години</string> + <string name="custom_time_unit_days">дні</string> + <string name="custom_time_picker_select">Вибрати</string> + <string name="custom_time_picker_custom">інше</string> + <string name="secret_text">прихований</string> + <string name="join_group_button">Приєднатися</string> <string name="role_in_group">Роль</string> - <string name="conn_level_desc_indirect">непрямі (%1$s)</string> - <string name="user_unmute">Увімкнути звук</string> - <string name="should_be_at_least_one_profile">Повинен бути принаймні один профіль користувача.</string> + <string name="conn_level_desc_indirect">непряме (%1$s)</string> + <string name="user_unmute">Відглушити</string> <string name="make_profile_private">Зробіть профіль приватним!</string> - <string name="feature_offered_item">запропонований %s</string> + <string name="feature_offered_item">запропоновано %s</string> <string name="v4_5_message_draft">Чернетка повідомлення</string> - <string name="v4_5_message_draft_descr">Зберегти чернетку останнього повідомлення з вкладеннями.</string> - <string name="disappearing_message">Повідомлення зникає</string> - <string name="send_disappearing_message">Надіслати зникаюче повідомлення</string> - <string name="image_descr_profile_image">зображення профілю</string> + <string name="v4_5_message_draft_descr">Зберігайте останню чернетку повідомлення із вкладеннями.</string> + <string name="disappearing_message">Зникне повідомлення</string> + <string name="send_disappearing_message">Надіслати зникне повідомлення</string> + <string name="image_descr_profile_image">аватар</string> <string name="icon_descr_more_button">Більше</string> <string name="create_profile">Створити профіль</string> - <string name="read_more_in_github_with_link"><![CDATA[Детальніше читайте в нашому репозиторії <font color="#0088ff">GitHub</font>.]]></string> - <string name="icon_descr_video_on">Відео включено</string> - <string name="alert_text_fragment_encryption_out_of_sync_old_database">Це може статися, коли ви або ваше підключення використовували стару резервну копію бази даних.</string> - <string name="restore_database">Відновлення резервної копії бази даних</string> - <string name="save_archive">Зберегти архів</string> + <string name="read_more_in_github_with_link"><![CDATA[Докладніше читайте в нашому репозиторії на <font color="#0088ff">GitHub</font>.]]></string> + <string name="icon_descr_video_on">Відео увімкнено</string> + <string name="alert_text_fragment_encryption_out_of_sync_old_database">Це може трапитися, якщо ви або ваше з\'єднання використовували застарілу резервну копію бази даних.</string> + <string name="restore_database">Відновити резервну копію бази даних</string> <string name="group_invitation_item_description">запрошення до групи %1$s</string> - <string name="you_are_invited_to_group_join_to_connect_with_group_members">Вас запрошено до групи. Приєднуйтесь, щоб спілкуватися з учасниками групи.</string> + <string name="you_are_invited_to_group_join_to_connect_with_group_members">Вас запрошено в групу. Приєднуйтесь, щоб спілкуватися з учасниками групи.</string> <string name="message_reactions">Реакції на повідомлення</string> - <string name="accept_feature_set_1_day">Встановлюється 1 день</string> - <string name="prohibit_message_reactions">Заборонити реакцію на повідомлення.</string> - <string name="message_reactions_prohibited_in_this_chat">Реакції на повідомлення в цьому чаті заборонені.</string> - <string name="ttl_s">%ds</string> + <string name="accept_feature_set_1_day">Встановити на 1 день</string> + <string name="prohibit_message_reactions">Забороняйте реакції на повідомлення.</string> + <string name="message_reactions_prohibited_in_this_chat">Реакції на повідомлення заборонені в цьому чаті.</string> + <string name="ttl_s">%dс</string> <string name="custom_time_unit_minutes">хвилини</string> - <string name="v4_6_chinese_spanish_interface">Інтерфейс китайською та іспанською мовами</string> + <string name="v4_6_chinese_spanish_interface">Китайський та іспанський інтерфейс</string> <string name="connection_local_display_name">підключення %1$d</string> <string name="error_loading_smp_servers">Помилка завантаження серверів SMP</string> - <string name="failed_to_create_user_duplicate_title">Дублююче ім\'я користувача!</string> - <string name="error_sending_message">Помилка надсилання повідомлення</string> + <string name="failed_to_create_user_duplicate_title">Дубль імені відображення!</string> + <string name="error_sending_message">Помилка відправлення повідомлення</string> <string name="sender_cancelled_file_transfer">Відправник скасував передачу файлу.</string> <string name="error_receiving_file">Помилка отримання файлу</string> <string name="error_creating_address">Помилка створення адреси</string> - <string name="invalid_connection_link">Неправильне посилання для підключення</string> - <string name="error_accepting_contact_request">Помилка при прийнятті запиту на контакт</string> + <string name="invalid_connection_link">Неправильне посилання на підключення</string> + <string name="error_accepting_contact_request">Помилка прийняття запиту на контакт</string> <string name="smp_server_test_create_file">Створити файл</string> - <string name="error_deleting_user">Помилка видалення профілю користувача</string> + <string name="error_deleting_user">Помилка видалення користувача</string> <string name="error_updating_user_privacy">Помилка оновлення конфіденційності користувача</string> - <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Щоб зберегти вашу конфіденційність, замість push-сповіщень програма має <b>SimpleX фонову службу</b> – вона використовує кілька відсотків заряду акумулятора на день.]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Для покращення конфіденційності, <b>SimpleX працює у фоновому режимі</b> замість використання пуш-повідомлень.]]></string> <string name="periodic_notifications">Періодичні сповіщення</string> - <string name="simplex_service_notification_title">Сервіс SimpleX Chat</string> + <string name="simplex_service_notification_title">Служба чату SimpleX</string> <string name="notifications_mode_periodic_desc">Перевіряє нові повідомлення кожні 10 хвилин протягом 1 хвилини</string> <string name="notification_preview_mode_hidden">Приховано</string> - <string name="notification_preview_mode_message_desc">Показати контакт та повідомлення</string> - <string name="notification_display_mode_hidden_desc">Приховати контакт і повідомлення</string> - <string name="la_notice_title_simplex_lock">SimpleX Lock</string> - <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Щоб захистити вашу інформацію, увімкніть SimpleX Lock. -\nПеред увімкненням цієї функції вам буде запропоновано пройти автентифікацію.</string> - <string name="auth_log_in_using_credential">Увійдіть, використовуючи свій обліковий запис</string> - <string name="auth_enable_simplex_lock">Увімкнути SimpleX Lock</string> - <string name="lock_not_enabled">SimpleX Lock не ввімкнено!</string> - <string name="share_verb">Поділіться</string> + <string name="notification_preview_mode_message_desc">Показати контакт і повідомлення</string> + <string name="notification_display_mode_hidden_desc">Сховати контакт і повідомлення</string> + <string name="la_notice_title_simplex_lock">Блокування SimpleX</string> + <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Щоб захистити вашу інформацію, увімкніть блокування SimpleX. +\nВам буде запропоновано завершити аутентифікацію перед увімкненням цієї функції.</string> + <string name="auth_log_in_using_credential">Пройдіть аутентифікацію</string> + <string name="auth_enable_simplex_lock">Увімкнути блокування SimpleX</string> + <string name="lock_not_enabled">Блокування SimpleX не увімкнено!</string> + <string name="share_verb">Поділитися</string> <string name="copy_verb">Копіювати</string> <string name="moderate_message_will_be_deleted_warning">Повідомлення буде видалено для всіх учасників.</string> - <string name="stop_snd_file__message">Надсилання файлу буде зупинено.</string> + <string name="stop_snd_file__message">Відправлення файлу буде зупинено.</string> <string name="stop_rcv_file__title">Зупинити отримання файлу\?</string> <string name="stop_rcv_file__message">Отримання файлу буде зупинено.</string> <string name="stop_file__confirm">Зупинити</string> @@ -580,299 +568,294 @@ <string name="revoke_file__message">Файл буде видалено з серверів.</string> <string name="revoke_file__confirm">Відкликати</string> <string name="icon_descr_sent_msg_status_unauthorized_send">несанкціонована відправка</string> - <string name="personal_welcome">Ласкаво просимо %1$s!</string> - <string name="welcome">Ласкаво просимо!</string> + <string name="personal_welcome">Вітаємо, %1$s!</string> + <string name="welcome">Вітаємо!</string> <string name="this_text_is_available_in_settings">Цей текст доступний у налаштуваннях</string> - <string name="group_preview_you_are_invited">вас запрошують до групи</string> + <string name="group_preview_you_are_invited">вас запрошено в групу</string> <string name="share_message">Поділитися повідомленням…</string> - <string name="share_image">Поділитися медіафайлами…</string> - <string name="share_file">Поділіться файлом…</string> - <string name="images_limit_desc">Одночасно можна надіслати лише 10 зображень</string> - <string name="videos_limit_desc">Одночасно можна надіслати лише 10 відео</string> - <string name="image_decoding_exception_title">Помилка декодування</string> - <string name="image_decoding_exception_desc">Зображення не може бути декодовано. Будь ласка, спробуйте інше зображення або зверніться до розробників.</string> - <string name="image_descr">Зображення</string> - <string name="image_will_be_received_when_contact_completes_uploading">Зображення буде отримано, коли ваш контакт завершить завантаження.</string> - <string name="image_will_be_received_when_contact_is_online">Зображення буде отримано, коли ваш контакт буде онлайн, будь ласка, зачекайте або перевірте пізніше!</string> - <string name="image_saved">Зображення збережено до Галереї</string> + <string name="share_image">Поділитися медіа…</string> + <string name="share_file">Поділитися файлом…</string> + <string name="images_limit_desc">Одночасно можна відправити лише 10 зображень</string> + <string name="videos_limit_desc">Одночасно можна відправити лише 10 відео</string> + <string name="image_decoding_exception_title">Помилка декодування зображення</string> + <string name="image_decoding_exception_desc">Неможливо декодувати зображення. Спробуйте інше зображення або зв\'яжіться з розробниками.</string> + <string name="image_descr">Фото</string> + <string name="image_will_be_received_when_contact_completes_uploading">Зображення буде отримано, коли ваш контакт завершить його вивантаження.</string> + <string name="image_will_be_received_when_contact_is_online">Зображення буде отримано, коли ваш контакт буде в мережі, будь ласка, зачекайте або перевірте пізніше!</string> + <string name="image_saved">Зображення збережено в галереї</string> <string name="video_descr">Відео</string> - <string name="contact_sent_large_file">Ваш контакт надіслав файл, розмір якого перевищує дозволений максимальний розмір (%1$s).</string> - <string name="maximum_supported_file_size">Наразі максимальний підтримуваний розмір файлу: %1$s .</string> - <string name="switch_receiving_address_desc">Ця функція є експериментальною! Вона працюватиме, тільки якщо на іншому клієнті встановлено версію 4.2. Після завершення зміни адреси ви побачите повідомлення в бесіді - будь ласка, перевірте, чи можете ви отримувати повідомлення від цього контакту (або члена групи).</string> - <string name="verify_security_code">Підтвердіть код безпеки</string> - <string name="icon_descr_send_message">Відправити повідомлення</string> + <string name="contact_sent_large_file">Ваш контакт відправив файл, розмір якого більший, ніж поточно підтримуваний максимальний розмір (%1$s).</string> + <string name="maximum_supported_file_size">Поточно максимально підтримуваний розмір файлу - %1$s.</string> + <string name="switch_receiving_address_desc">Адреса отримувача буде змінена на інший сервер. Зміна адреси завершиться після того, як відправник з\'явиться в мережі.</string> + <string name="verify_security_code">Перевірити код безпеки</string> + <string name="icon_descr_send_message">Надіслати повідомлення</string> <string name="icon_descr_record_voice_message">Записати голосове повідомлення</string> - <string name="only_stored_on_members_devices">(зберігається тільки учасниками групи)</string> - <string name="toast_permission_denied">У дозволі відмовлено!</string> + <string name="only_stored_on_members_devices">(зберігається лише на пристроях учасників групи)</string> + <string name="toast_permission_denied">Доступ відхилено!</string> <string name="use_camera_button">Камера</string> - <string name="thank_you_for_installing_simplex">Дякуємо, що встановили SimpleX Chat!</string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Відскануйте QR-код</b>: щоб з\'єднатися з вашим контактом, який покаже вам QR-код.]]></string> - <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Якщо ви отримали посилання на запрошення SimpleX Chat, ви можете відкрити його у своєму браузері:</string> - <string name="clear_chat_menu_action">Чисто</string> + <string name="thank_you_for_installing_simplex">Дякуємо за установку SimpleX Chat!</string> + <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Якщо ви отримали запрошення від SimpleX Chat, ви можете відкрити його у вашому браузері:</string> + <string name="clear_chat_menu_action">Очистити</string> <string name="delete_contact_menu_action">Видалити</string> <string name="delete_group_menu_action">Видалити</string> - <string name="mark_read">Позначити прочитано</string> - <string name="mark_unread">Позначити як непрочитане</string> + <string name="mark_read">Позначити прочитаним</string> + <string name="mark_unread">Позначити непрочитаним</string> <string name="set_contact_name">Встановити ім\'я контакту</string> - <string name="you_accepted_connection">Ви прийняли підключення</string> - <string name="delete_pending_connection__question">Видалити очікуване з\'єднання\?</string> - <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Ваш контакт має бути онлайн, щоб з’єднання завершилося. -\nВи можете скасувати це з’єднання та видалити контакт (і спробувати пізніше з новим посиланням).</string> - <string name="image_descr_simplex_logo">SimpleX Логотип</string> + <string name="you_accepted_connection">Ви прийняли з\'єднання</string> + <string name="delete_pending_connection__question">Видалити очікуюче з\'єднання?</string> + <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Ваш контакт повинен бути в мережі, щоб завершити з\'єднання. +\nВи можете скасувати це з\'єднання і видалити контакт (і спробувати пізніше за допомогою нового посилання).</string> + <string name="image_descr_simplex_logo">Логотип SimpleX</string> <string name="icon_descr_email">Електронна пошта</string> <string name="this_QR_code_is_not_a_link">Цей QR-код не є посиланням!</string> - <string name="you_will_be_connected_when_group_host_device_is_online">Вас буде підключено до групи, коли пристрій хоста групи буде онлайн, зачекайте або перевірте пізніше!</string> - <string name="you_will_be_connected_when_your_connection_request_is_accepted">Ви будете підключені, коли ваш запит на підключення буде прийнято, будь ласка, зачекайте або перевірте пізніше!</string> - <string name="share_invitation_link">Поділіться одноразовим посиланням</string> - <string name="learn_more">Детальніше</string> - <string name="scan_qr_to_connect_to_contact">Щоб підключитися, ваш контакт може відсканувати QR-код або скористатися посиланням у додатку.</string> - <string name="if_you_cant_meet_in_person">Якщо ви не можете зустрітися особисто, покажіть QR-код у відеодзвінку або поділіться посиланням.</string> - <string name="you_can_share_your_address">Ви можете поділитися своєю адресою у вигляді посилання або QR-коду - будь-хто зможе зв\'язатися з вами.</string> + <string name="you_will_be_connected_when_group_host_device_is_online">Ви будете підключені до групи, коли пристрій власник групи буде в мережі, зачекайте або перевірте пізніше!</string> + <string name="you_will_be_connected_when_your_connection_request_is_accepted">Підключення відбудеться, коли ваш запит на підключення буде прийнято. Будь ласка, зачекайте або спробуйте пізніше!</string> + <string name="share_invitation_link">Поділитися 1-разовим посиланням</string> + <string name="learn_more">Дізнатися більше</string> + <string name="scan_qr_to_connect_to_contact">Щоб підключитися, ваш контакт може сканувати QR-код або використовувати посилання у додатку.</string> + <string name="if_you_cant_meet_in_person">Якщо ви не можете зустрітися особисто, покажіть QR-код у відеовиклику або поділіться посиланням.</string> + <string name="you_can_share_your_address">Ви можете поділитися своєю адресою в якості посилання або QR-коду - кожен може підключитися до вас.</string> <string name="paste_button">Вставити</string> - <string name="this_string_is_not_a_connection_link">Цей рядок не є посиланням для з\'єднання!</string> + <string name="this_string_is_not_a_connection_link">Цей рядок не є з\'єднувальним посиланням!</string> <string name="security_code">Код безпеки</string> - <string name="mark_code_verified">Позначку перевірено</string> - <string name="your_chat_profiles">Ваші профілі чату</string> - <string name="database_passphrase_and_export">Ключова фраза бази даних та експорт</string> - <string name="markdown_in_messages">Націнка в повідомленнях</string> - <string name="chat_with_the_founder">Надсилайте запитання та ідеї</string> - <string name="smp_servers_enter_manually">Увійдіть на сервер вручну</string> + <string name="mark_code_verified">Позначити, що перевірено</string> + <string name="your_chat_profiles">Ваші профілі</string> + <string name="database_passphrase_and_export">Пароль бази даних та експорт</string> + <string name="markdown_in_messages">Markdown у повідомленнях</string> + <string name="chat_with_the_founder">Надсилайте питання та ідеї</string> + <string name="smp_servers_enter_manually">Ввести сервер вручну</string> <string name="smp_servers_preset_server">Попередньо встановлений сервер</string> - <string name="smp_servers_your_server_address">Ваша адреса сервера</string> - <string name="smp_servers_per_user">Сервери для нових підключень вашого поточного профілю чату</string> - <string name="use_simplex_chat_servers__question">Використовуєте сервери SimpleX Chat\?</string> - <string name="enter_one_ICE_server_per_line">Сервери ICE (по одному на лінію)</string> + <string name="smp_servers_your_server_address">Адреса вашого сервера</string> + <string name="smp_servers_per_user">Сервери для нових підключень до вашого поточного профілю</string> + <string name="use_simplex_chat_servers__question">Використовувати сервери SimpleX Chat?</string> + <string name="enter_one_ICE_server_per_line">Сервери ICE (один на рядок)</string> <string name="error_saving_ICE_servers">Помилка збереження серверів ICE</string> - <string name="network_use_onion_hosts_required_desc">Onion hosts will be required for connection. -\nPlease note: you will not be able to connect to the servers without .onion address.</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">Onion хости будуть використовуватися за наявності.</string> - <string name="network_use_onion_hosts_required_desc_in_alert">Для з\'єднання будуть потрібні хости onion.</string> - <string name="show_developer_options">Показати опції розробника</string> + <string name="network_use_onion_hosts_required_desc">.Onion-хости будуть обов\'язковими для підключення. +\nЗверніть увагу: ви не зможете підключитися до серверів без адреси .onion.</string> + <string name="show_developer_options">Показати параметри розробника</string> <string name="developer_options">Ідентифікатори бази даних та опція ізоляції транспорту.</string> <string name="shutdown_alert_desc">Сповіщення перестануть працювати, поки ви не перезапустите додаток</string> <string name="you_can_create_it_later">Ви можете створити його пізніше</string> <string name="your_current_profile">Ваш поточний профіль</string> <string name="delete_image">Видалити зображення</string> - <string name="save_preferences_question">Зберегти налаштування\?</string> - <string name="save_and_notify_contact">Зберегти та повідомити контакт</string> + <string name="save_preferences_question">Зберегти налаштування?</string> + <string name="save_and_notify_contact">Зберегти та сповістити контакт</string> <string name="save_profile_password">Зберегти пароль профілю</string> - <string name="hidden_profile_password">Прихований пароль профілю</string> - <string name="profile_is_only_shared_with_your_contacts">Профіль доступний лише вашим контактам.</string> - <string name="display_name_cannot_contain_whitespace">Ім\'я не може містити пробілів.</string> - <string name="how_to_use_markdown">Як використовувати націнку</string> - <string name="you_can_use_markdown_to_format_messages__prompt">Ви можете використовувати розмітку для форматування повідомлень:</string> + <string name="hidden_profile_password">Пароль схованого профілю</string> + <string name="profile_is_only_shared_with_your_contacts">Профіль обмінюється лише з вашими контактами.</string> + <string name="display_name_cannot_contain_whitespace">Ім\'я для відображення не може містити пробіли.</string> + <string name="how_to_use_markdown">Як використовувати markdown</string> + <string name="you_can_use_markdown_to_format_messages__prompt">Ви можете використовувати markdown для форматування повідомлень:</string> <string name="create_your_profile">Створіть свій профіль</string> - <string name="make_private_connection">Створіть приватне з\'єднання</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[Багато людей запитували: <i>якщо SimpleX не має ідентифікаторів користувачів, як він може доставляти повідомлення\?</i>]]></string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Ви контролюєте, через який(і) сервер(и) <b>отримувати</b> повідомлення, ваші контакти - сервери, які ви використовуєте для надсилання їм повідомлень.]]></string> - <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Лише клієнтські пристрої зберігають профілі користувачів, контакти, групи та повідомлення, надіслані за допомогою <b>дворівневого наскрізного шифрування</b>.]]></string> + <string name="make_private_connection">Створіть приватне підключення</string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Тільки клієнтські пристрої зберігають профілі, контакти, групи та повідомлення.</string> <string name="onboarding_notifications_mode_title">Приватні сповіщення</string> - <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Використовує більше заряду акумулятора</b>! Завжди працює фоновий сервіс - сповіщення показуються, як тільки з\'являються повідомлення.]]></string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Споживає більше акумулятора</b>! Додаток завжди працює у фоновому режимі – сповіщення відображаються миттєво.]]></string> <string name="paste_the_link_you_received">Вставте отримане посилання</string> <string name="icon_descr_video_off">Відео вимкнено</string> - <string name="icon_descr_call_ended">Дзвінок завершено</string> - <string name="alert_text_msg_bad_id">Ідентифікатор наступного повідомлення неправильний (менше або дорівнює попередньому). -\nЦе може статися через помилку або коли з\'єднання скомпрометовано.</string> - <string name="self_destruct_passcode_enabled">Пароль самознищення ввімкнено!</string> + <string name="icon_descr_call_ended">Завершено виклик</string> + <string name="alert_text_msg_bad_id">Ідентифікатор наступного повідомлення є неправильним (менший або рівний попередньому). +\nЦе може трапитися через якусь помилку або коли з\'єднання скомпрометоване.</string> + <string name="self_destruct_passcode_enabled">Пароль самознищення увімкнено!</string> <string name="self_destruct_passcode_changed">Пароль самознищення змінено!</string> <string name="your_profile_is_stored_on_your_device">Ваш профіль, контакти та доставлені повідомлення зберігаються на вашому пристрої.</string> <string name="settings_section_title_you">ВИ</string> <string name="settings_section_title_device">ПРИСТРІЙ</string> - <string name="settings_shutdown">Вимкнення</string> + <string name="settings_shutdown">Вимкнути</string> <string name="settings_section_title_themes">ТЕМИ</string> <string name="settings_section_title_messages">ПОВІДОМЛЕННЯ ТА ФАЙЛИ</string> - <string name="chat_is_running">Чат запущено</string> - <string name="import_database">Імпорт бази даних</string> + <string name="chat_is_running">Чат працює</string> + <string name="import_database">Імпортувати базу даних</string> <string name="old_database_archive">Старий архів бази даних</string> <string name="delete_database">Видалити базу даних</string> - <string name="set_password_to_export">Встановіть парольну фразу для експорту</string> - <string name="set_password_to_export_desc">База даних зашифрована за допомогою випадкової парольної фрази. Будь ласка, змініть його перед експортом.</string> + <string name="set_password_to_export">Встановити пароль для експорту</string> + <string name="set_password_to_export_desc">База даних зашифрована випадковим паролем. Змініть його перед експортом.</string> <string name="error_exporting_chat_database">Помилка експорту бази даних чату</string> <string name="import_database_question">Імпортувати базу даних чату\?</string> <string name="error_importing_database">Помилка імпорту бази даних чату</string> <string name="delete_chat_profile_question">Видалити профіль чату\?</string> - <string name="delete_files_and_media_desc">Цю дію неможливо скасувати - всі отримані та надіслані файли і медіа будуть видалені. Зображення з низькою роздільною здатністю залишаться.</string> - <string name="delete_messages">Видалення повідомлень</string> - <string name="update_database_passphrase">Оновити парольну фразу бази даних</string> - <string name="encrypted_with_random_passphrase">База даних зашифрована за допомогою випадкової парольної фрази, яку ви можете змінити.</string> - <string name="database_passphrase_will_be_updated">Парольну фразу шифрування бази даних буде оновлено.</string> - <string name="store_passphrase_securely">Будь ласка, зберігайте пароль надійно, ви НЕ зможете змінити його, якщо втратите.</string> - <string name="wrong_passphrase">Неправильна парольна фраза бази даних</string> - <string name="passphrase_is_different">Пароль до бази даних відрізняється від збереженого у сховищі ключів.</string> - <string name="database_backup_can_be_restored">Спроба змінити пароль бази даних не була завершена.</string> + <string name="delete_files_and_media_desc">Цю дію неможливо відмінити - всі отримані та надіслані файли та медіа будуть видалені. Зображення низької роздільної здатності залишаться.</string> + <string name="delete_messages">Видалити повідомлення</string> + <string name="update_database_passphrase">Оновити ключову фразу бази даних</string> + <string name="encrypted_with_random_passphrase">База даних зашифрована випадковою ключовою фразою, яку можна змінити.</string> + <string name="database_passphrase_will_be_updated">Ключова фраза шифрування бази даних буде оновлена.</string> + <string name="store_passphrase_securely">Будь ласка, зберігайте ключову фразу надійно, ви НЕ зможете її змінити, якщо втратите її.</string> + <string name="wrong_passphrase">Неправильна ключова фраза бази даних</string> + <string name="passphrase_is_different">Ключова фраза бази даних відрізняється від збереженої в сховищі ключів.</string> + <string name="database_backup_can_be_restored">Спроба змінити ключову фразу бази даних не була завершена.</string> <string name="restore_database_alert_title">Відновити резервну копію бази даних\?</string> - <string name="restore_database_alert_desc">Будь ласка, введіть попередній пароль після відновлення резервної копії бази даних. Ця дія не може бути скасована.</string> - <string name="join_group_incognito_button">Приєднуйтесь інкогніто</string> - <string name="rcv_group_event_member_left">ліворуч</string> - <string name="rcv_group_event_changed_member_role">змінили роль %s на %s</string> - <string name="rcv_group_event_group_deleted">видалена група</string> + <string name="restore_database_alert_desc">Будь ласка, введіть попередній пароль після відновлення резервної копії бази даних. Цю дію неможливо скасувати.</string> + <string name="join_group_incognito_button">Приєднатися анонімно</string> + <string name="rcv_group_event_member_left">вийшов(ла)</string> + <string name="rcv_group_event_changed_member_role">змінив(ла) роль %s на %s</string> + <string name="rcv_group_event_group_deleted">групу видалено</string> <string name="snd_group_event_group_profile_updated">оновлено профіль групи</string> - <string name="rcv_conn_event_switch_queue_phase_completed">змінили для вас адресу</string> + <string name="rcv_conn_event_switch_queue_phase_completed">змінив(ла) адресу для вас</string> <string name="rcv_conn_event_switch_queue_phase_changing">змінює адресу…</string> <string name="group_member_role_member">учасник</string> - <string name="group_member_status_left">ліворуч</string> + <string name="group_member_status_left">залишено</string> <string name="group_member_status_group_deleted">групу видалено</string> - <string name="group_member_status_invited">запрошені</string> + <string name="group_member_status_invited">запрошено</string> <string name="group_member_status_introduced">підключення (введено)</string> <string name="invite_to_group_button">Запросити до групи</string> <string name="skip_inviting_button">Пропустити запрошення учасників</string> - <string name="num_contacts_selected">%d вибрано контакт(и)</string> + <string name="num_contacts_selected">%d вибрано контактів</string> <string name="group_info_member_you">ви: %1$s</string> - <string name="delete_group_for_all_members_cannot_undo_warning">Група буде видалена для всіх учасників - це неможливо скасувати!</string> - <string name="delete_group_for_self_cannot_undo_warning">Група буде видалена для вас - це не може бути скасовано!</string> + <string name="delete_group_for_all_members_cannot_undo_warning">Група буде видалена для всіх учасників - цю дію неможливо скасувати!</string> + <string name="delete_group_for_self_cannot_undo_warning">Група буде видалена для вас - цю дію неможливо скасувати!</string> <string name="button_create_group_link">Створити посилання</string> - <string name="error_updating_link_for_group">Помилка оновлення посилання на групу</string> - <string name="error_deleting_link_for_group">Помилка видалення посилання на групу</string> - <string name="only_group_owners_can_change_prefs">Тільки власники груп можуть змінювати налаштування групи.</string> - <string name="info_row_updated_at">Запис оновлено за</string> - <string name="info_row_sent_at">Надіслано на</string> - <string name="info_row_moderated_at">Модерується на</string> - <string name="info_row_disappears_at">Зникає за</string> + <string name="error_updating_link_for_group">Помилка при оновленні посилання на групу</string> + <string name="error_deleting_link_for_group">Помилка при видаленні посилання на групу</string> + <string name="only_group_owners_can_change_prefs">Змінювати налаштування групи можуть лише її власники.</string> + <string name="info_row_updated_at">Запис оновлено</string> + <string name="info_row_sent_at">Надіслано</string> + <string name="info_row_moderated_at">Модеровано о</string> + <string name="info_row_disappears_at">Зникає о</string> <string name="share_text_database_id">Ідентифікатор бази даних: %d</string> - <string name="share_text_updated_at">Запис оновлено за: %s</string> - <string name="share_text_moderated_at">Модерується на: %s</string> - <string name="share_text_disappears_at">Зникає в: %s</string> - <string name="item_info_current">(поточний)</string> - <string name="button_remove_member">Видалити учасника</string> - <string name="member_role_will_be_changed_with_notification">Роль буде змінено на \"%s\". Всі учасники групи будуть повідомлені про це.</string> - <string name="member_role_will_be_changed_with_invitation">Роль буде змінено на \"%s\". Учасник отримає нове запрошення.</string> + <string name="share_text_updated_at">Запис оновлено о: %s</string> + <string name="share_text_moderated_at">Модеровано о: %s</string> + <string name="share_text_disappears_at">Зникає о: %s</string> + <string name="item_info_current">(поточне)</string> + <string name="button_remove_member">Вилучити учасника</string> + <string name="member_role_will_be_changed_with_notification">Роль буде змінено на %s. Усі учасники групи будуть сповіщені.</string> + <string name="member_role_will_be_changed_with_invitation">Роль буде змінено на "%s". Учасник отримає нове запрошення.</string> <string name="info_row_group">Група</string> - <string name="group_welcome_title">Вітальне повідомлення</string> + <string name="group_welcome_title">Привітальне повідомлення</string> <string name="group_profile_is_stored_on_members_devices">Профіль групи зберігається на пристроях учасників, а не на серверах.</string> <string name="save_group_profile">Зберегти профіль групи</string> - <string name="users_delete_with_connections">Підключення профілю та сервера</string> + <string name="users_delete_with_connections">Профіль і підключення до серверів</string> <string name="user_unhide">Показати</string> - <string name="you_can_hide_or_mute_user_profile">Ви можете приховати або вимкнути звук профілю користувача - утримуйте його для виклику меню.</string> + <string name="you_can_hide_or_mute_user_profile">Ви можете приховати або вимкнути звук профілю користувача - утримуйте його для меню.</string> <string name="unhide_profile">Показати профіль</string> <string name="unhide_chat_profile">Показати профіль чату</string> - <string name="incognito_info_share">Коли ви ділитеся з кимось своїм профілем інкогніто, цей профіль буде використовуватися для груп, до яких вони вас запрошують.</string> + <string name="incognito_info_share">Коли ви ділитесь анонімним профілем з кимось, цей профіль буде використовуватися для груп, до яких вас запрошують.</string> <string name="theme_light">Світлий</string> <string name="import_theme_error">Помилка імпорту теми</string> <string name="group_preferences">Налаштування групи</string> - <string name="timed_messages">Зникнення повідомлень</string> - <string name="feature_enabled_for_contact">увімкнено для контакту</string> + <string name="timed_messages">Зникаючі повідомлення</string> + <string name="feature_enabled_for_contact">ввімкнено для контакту</string> <string name="feature_off">вимкнено</string> <string name="feature_received_prohibited">отримано, заборонено</string> - <string name="contacts_can_mark_messages_for_deletion">Контакти можуть позначати повідомлення для видалення; ви зможете їх переглянути.</string> - <string name="prohibit_calls">Заборонити аудіо/відеодзвінки.</string> - <string name="only_you_can_send_disappearing">Тільки ви можете надсилати зникаючі повідомлення.</string> - <string name="disappearing_prohibited_in_this_chat">Зникаючі повідомлення в цьому чаті заборонені.</string> - <string name="message_deletion_prohibited">У цьому чаті заборонено безповоротне видалення повідомлень.</string> - <string name="both_you_and_your_contact_can_send_voice">Надсилати голосові повідомлення можете як ви, так і ваш контакт.</string> + <string name="contacts_can_mark_messages_for_deletion">Контакти можуть позначати повідомлення для видалення; ви зможете їх переглядати.</string> + <string name="prohibit_calls">Забороняйте аудіо/відео дзвінки.</string> + <string name="only_you_can_send_disappearing">Тільки ви можете надсилати повідомлення, які зникають.</string> + <string name="disappearing_prohibited_in_this_chat">Зникнення повідомлень заборонене в цьому чаті.</string> + <string name="message_deletion_prohibited">Невідворотне видалення повідомлень заборонено в цьому чаті.</string> + <string name="both_you_and_your_contact_can_send_voice">Як ви, так і ваш контакт можуть надсилати голосові повідомлення.</string> <string name="only_you_can_send_voice">Тільки ви можете надсилати голосові повідомлення.</string> <string name="only_you_can_add_message_reactions">Тільки ви можете додавати реакції на повідомлення.</string> <string name="prohibit_message_reactions_group">Заборонити реакції на повідомлення.</string> - <string name="disappearing_messages_are_prohibited">У цій групі заборонено зникаючі повідомлення.</string> - <string name="group_members_can_send_dms">Учасники групи можуть надсилати прямі повідомлення.</string> - <string name="direct_messages_are_prohibited_in_chat">У цій групі заборонені прямі повідомлення між учасниками.</string> - <string name="group_members_can_delete">Учасники групи можуть безповоротно видаляти надіслані повідомлення.</string> - <string name="message_deletion_prohibited_in_chat">У цій групі заборонено безповоротне видалення повідомлень.</string> - <string name="voice_messages_are_prohibited">Голосові повідомлення в цій групі заборонені.</string> - <string name="group_members_can_add_message_reactions">Учасники групи можуть додавати реакції на повідомлення.</string> - <string name="message_reactions_are_prohibited">Реакції на повідомлення в цій групі заборонені.</string> + <string name="disappearing_messages_are_prohibited">Повідомлення, що зникають, заборонені.</string> + <string name="group_members_can_send_dms">Учасники можуть надсилати прямі повідомлення.</string> + <string name="direct_messages_are_prohibited_in_group">Приватні повідомлення між учасниками заборонені в цій групі.</string> + <string name="group_members_can_delete">Учасники можуть необоротно видаляти надіслані повідомлення (протягом 24 годин).</string> + <string name="message_deletion_prohibited_in_chat">Заборонено необоротне видалення повідомлень.</string> + <string name="voice_messages_are_prohibited">Голосові повідомлення заборонені</string> + <string name="group_members_can_add_message_reactions">Учасники можуть додавати реакції на повідомлення.</string> + <string name="message_reactions_are_prohibited">Реакції на повідомлення заборонені.</string> <string name="ttl_hour">%d година</string> <string name="ttl_week">%d тиждень</string> - <string name="ttl_weeks">%d тижнів</string> - <string name="ttl_w">%dw</string> - <string name="feature_offered_item_with_param">запропонував %s: %2s</string> - <string name="v4_2_auto_accept_contact_requests_desc">З необов’язковим вітальним повідомленням.</string> - <string name="v4_3_improved_privacy_and_security_desc">Приховати екран програми в останніх програмах.</string> - <string name="v4_4_disappearing_messages">Зникнення повідомлень</string> + <string name="ttl_weeks">%d тижні</string> + <string name="ttl_w">%dтиж</string> + <string name="feature_offered_item_with_param">запропоновано %s: %2s</string> + <string name="v4_2_auto_accept_contact_requests_desc">З опційним вітанням.</string> + <string name="v4_3_improved_privacy_and_security_desc">Приховуйте екран додатка в останніх програмах.</string> + <string name="v4_4_disappearing_messages">Самознищувальні повідомлення</string> <string name="v4_4_live_messages_desc">Одержувачі бачать оновлення, коли ви їх вводите.</string> - <string name="v4_5_transport_isolation">Транспортна ізоляція</string> - <string name="v4_5_private_filenames_descr">Для захисту часового поясу у файлах зображень/голосу використовується UTC.</string> - <string name="v4_5_italian_interface_descr">Дякуємо користувачам - зробіть свій внесок через Weblate!</string> + <string name="v4_5_transport_isolation">Ізоляція транспорту</string> + <string name="v4_5_private_filenames_descr">Для захисту часового поясу файли зображень/голосу використовують UTC.</string> + <string name="v4_5_italian_interface_descr">Дякуємо користувачам – приєднуйтеся через Weblate!</string> <string name="v4_6_group_moderation_descr">Тепер адміністратори можуть: -\n- видалити повідомлення учасників. -\n- відключити учасників (роль \"спостерігач\")</string> - <string name="v4_6_group_welcome_message_descr">Налаштуйте повідомлення, яке показуватиметься новим користувачам!</string> - <string name="v4_6_reduced_battery_usage">Подальше зменшення використання акумулятора</string> - <string name="v4_6_reduced_battery_usage_descr">Незабаром буде ще більше покращень!</string> - <string name="v5_0_large_files_support">Відео та файли до 1 Гб</string> +\n- видаляти повідомлення учасників. +\n- вимикати учасників (роль спостерігача).</string> + <string name="v4_6_group_welcome_message_descr">Встановіть повідомлення, яке показується новим учасникам!</string> + <string name="v4_6_reduced_battery_usage">Додатково зменшено використання батареї</string> + <string name="v4_6_reduced_battery_usage_descr">Більше поліпшень незабаром!</string> + <string name="v5_0_large_files_support">Відео та файли до 1 ГБ</string> <string name="v5_0_polish_interface">Польський інтерфейс</string> - <string name="v5_0_polish_interface_descr">Дякуємо користувачам - зробіть свій внесок через Weblate!</string> + <string name="v5_0_polish_interface_descr">Дякуємо користувачам – приєднуйтеся через Weblate!</string> <string name="v5_1_message_reactions">Реакції на повідомлення</string> - <string name="v5_1_message_reactions_descr">Нарешті, вони у нас є! 🚀</string> + <string name="v5_1_message_reactions_descr">Нарешті, ми їх маємо! 🚀</string> <string name="v5_1_custom_themes_descr">Налаштовуйте та діліться кольоровими темами.</string> - <string name="whats_new_thanks_to_users_contribute_weblate">Дякуємо користувачам - зробіть свій внесок через Weblate!</string> - <string name="custom_time_unit_seconds">секунд</string> - <string name="custom_time_unit_weeks">тижнів</string> + <string name="whats_new_thanks_to_users_contribute_weblate">Дякуємо користувачам – приєднуйтеся через Weblate!</string> + <string name="custom_time_unit_seconds">секунди</string> + <string name="custom_time_unit_weeks">тижні</string> <string name="custom_time_unit_months">місяці</string> - <string name="you_are_already_connected_to_vName_via_this_link">Ви вже підключені до %1$s.</string> + <string name="you_are_already_connected_to_vName_via_this_link">Ви вже підключені до %1$s через це посилання.</string> <string name="settings_section_title_incognito">Режим інкогніто</string> <string name="conn_stats_section_title_servers">СЕРВЕРИ</string> - <string name="save_welcome_message_question">Зберегти привітальне повідомлення\?</string> + <string name="save_welcome_message_question">Зберегти вітальне повідомлення?</string> <string name="receiving_via">Отримання через</string> - <string name="muted_when_inactive">Вимкнено, коли неактивний!</string> + <string name="muted_when_inactive">Приглушено, коли неактивно!</string> <string name="delete_profile">Видалити профіль</string> <string name="incognito">Інкогніто</string> - <string name="incognito_info_allows">Це дозволяє мати багато анонімних з\'єднань без будь-яких спільних даних між ними в одному профілі чату.</string> + <string name="incognito_info_allows">Це дозволяє мати багато анонімних з\'єднань без будь-яких загальних даних між ними в одному чат-профілі.</string> <string name="theme_simplex">SimpleX</string> - <string name="only_your_contact_can_send_disappearing">Тільки ваш контакт може надсилати зникаючі повідомлення.</string> - <string name="chat_preferences_on">увімкнути</string> - <string name="ttl_hours">%d годин</string> - <string name="error_deleting_pending_contact_connection">Помилка видалення очікуваного з\'єднання контакту</string> + <string name="only_your_contact_can_send_disappearing">Тільки ваш контакт може надсилати повідомлення, які зникають.</string> + <string name="chat_preferences_on">увімк</string> + <string name="ttl_hours">%d години</string> + <string name="error_deleting_pending_contact_connection">Помилка видалення очікуючого з\'єднання з контактом</string> <string name="error_loading_details">Помилка завантаження деталей</string> - <string name="connection_error_auth_desc">Якщо ваш контакт не видалив з\'єднання або якщо це посилання вже використовувалося, це може бути помилкою - будь ласка, повідомте про це. -\nЩоб підключитися, попросіть вашого контакта створити інше посилання і перевірте, чи маєте ви стабільне з\'єднання з мережею.</string> + <string name="connection_error_auth_desc">Якщо ваш контакт не видалив з\'єднання або це посилання вже використано, це може бути помилкою - будь ласка, повідомте про це. +\nДля підключення попросіть вашого контакту створити інше посилання на з\'єднання та перевірте стабільність мережевого підключення.</string> <string name="error_deleting_contact">Помилка видалення контакту</string> <string name="error_deleting_group">Помилка видалення групи</string> - <string name="smp_server_test_disconnect">Від\'єднати</string> + <string name="smp_server_test_disconnect">Відключити</string> <string name="smp_server_test_secure_queue">Безпечна черга</string> <string name="smp_server_test_delete_queue">Видалити чергу</string> - <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Будь ласка, переконайтеся, що ви використали правильне посилання, або попросіть свого контакта надіслати вам інше.</string> - <string name="turn_off_battery_optimization"><![CDATA[Щоб використовувати його, <b>дозвольте SimpleX працювати у фоновому режимі</b> у наступному діалоговому вікні. \u0020В іншому випадку сповіщення буде вимкнено.]]></string> + <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Будь ласка, перевірте, що ви використали правильне посилання або попросіть вашого контакту вислати інше.</string> + <string name="turn_off_battery_optimization"><![CDATA[<b>Дозвольте це</b> в наступному діалозі, щоб отримувати сповіщення миттєво.]]></string> <string name="icon_descr_instant_notifications">Миттєві сповіщення</string> - <string name="notification_preview_somebody">Контакт приховано:</string> + <string name="notification_preview_somebody">Контакт прихований:</string> <string name="notification_preview_new_message">нове повідомлення</string> <string name="edit_verb">Редагувати</string> <string name="info_menu">Інформація</string> - <string name="sent_message">Надіслано повідомлення</string> - <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Вам потрібно буде пройти автентифікацію при запуску або відновленні програми після 30 секунд роботи у фоновому режимі.</string> - <string name="received_message">Отримано повідомлення</string> + <string name="sent_message">Відправлене повідомлення</string> + <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Вам буде потрібно пройти аутентифікацію при запуску або відновленні програми через 30 секунд у фоновому режимі.</string> + <string name="received_message">Отримане повідомлення</string> <string name="edit_history">Історія</string> <string name="reveal_verb">Розкрити</string> <string name="hide_verb">Приховати</string> - <string name="moderate_verb">Редагування</string> + <string name="moderate_verb">Модерувати</string> <string name="delete_message__question">Видалити повідомлення\?</string> - <string name="delete_message_cannot_be_undone_warning">Повідомлення буде видалено - це неможливо скасувати!</string> - <string name="icon_descr_sent_msg_status_sent">надісланий</string> - <string name="voice_messages_prohibited">Голосові повідомлення заборонені!</string> + <string name="delete_message_cannot_be_undone_warning">Повідомлення буде видалено - цю дію неможливо скасувати!</string> + <string name="icon_descr_sent_msg_status_sent">відправлено</string> + <string name="voice_messages_prohibited">Заборонено голосові повідомлення!</string> <string name="send_disappearing_message_send">Надіслати</string> <string name="confirm_verb">Підтвердити</string> - <string name="send_disappearing_message_custom_time">Індивідуальний час</string> - <string name="share_one_time_link">Створіть одноразове посилання-запрошення</string> - <string name="scan_QR_code">Відскануйте QR-код</string> - <string name="gallery_image_button">Зображення</string> + <string name="send_disappearing_message_custom_time">Інший час</string> + <string name="share_one_time_link">Створити одноразове запрошення</string> + <string name="scan_QR_code">Сканувати QR-код</string> + <string name="gallery_image_button">Фото</string> <string name="gallery_video_button">Відео</string> - <string name="connection_you_accepted_will_be_cancelled">Прийняте вами з\'єднання буде скасовано!</string> - <string name="alert_title_contact_connection_pending">Контакт ще не підключено!</string> - <string name="smp_servers_test_servers">Тестові сервери</string> + <string name="connection_you_accepted_will_be_cancelled">Прийняте вами з\'єднання буде скасоване!</string> + <string name="alert_title_contact_connection_pending">Контакт ще не підключений!</string> + <string name="smp_servers_test_servers">Тестувати сервери</string> <string name="smp_servers_save">Зберегти сервери</string> <string name="smp_servers_your_server">Ваш сервер</string> - <string name="smp_servers_test_failed">Тест сервера завершився невдало!</string> + <string name="smp_servers_test_failed">Тест сервера не вдався!</string> <string name="smp_servers_test_some_failed">Деякі сервери не пройшли тест:</string> <string name="smp_servers_use_server">Використовувати сервер</string> - <string name="ensure_ICE_server_address_are_correct_format_and_unique">Переконайтеся, що адреси серверів WebRTC ICE мають правильний формат, розділені рядками та не дублюються.</string> - <string name="network_disable_socks_info">Якщо ви підтвердите, сервери обміну повідомленнями зможуть бачити вашу IP-адресу, а ваш провайдер - до яких серверів ви підключаєтеся.</string> - <string name="network_use_onion_hosts_no_desc">Onion хости не використовуватимуться.</string> - <string name="network_session_mode_transport_isolation">Транспортна ізоляція</string> - <string name="customize_theme_title">Налаштувати тему</string> - <string name="share_address_with_contacts_question">Поділіться адресою з контактами\?</string> - <string name="callstatus_connecting">З\'єднувальний дзвінок…</string> - <string name="we_do_not_store_contacts_or_messages_on_servers">Ми не зберігаємо жодних ваших контактів чи повідомлень (після доставки) на серверах.</string> - <string name="callstate_waiting_for_answer">в очікуванні відповіді…</string> + <string name="ensure_ICE_server_address_are_correct_format_and_unique">Переконайтеся, що адреси серверів WebRTC ICE вказані в правильному форматі, розділені по рядках і не повторюються.</string> + <string name="network_disable_socks_info">Якщо ви підтвердите, сервери обміну повідомленнями матимуть можливість бачити ваш IP-адресу, а ваш постачальник - які саме сервери ви використовуєте для підключення.</string> + <string name="network_use_onion_hosts_no_desc">.Onion-хости не будуть використовуватися.</string> + <string name="network_session_mode_transport_isolation">Ізоляція транспорту</string> + <string name="customize_theme_title">Налаштування теми</string> + <string name="share_address_with_contacts_question">Поділитися адресою з контактами?</string> + <string name="callstatus_connecting">підключення дзвінка…</string> + <string name="we_do_not_store_contacts_or_messages_on_servers">Ми не зберігаємо жодні з ваших контактів чи повідомлень (після доставки) на серверах.</string> + <string name="callstate_waiting_for_answer">очікування відповіді…</string> <string name="how_it_works">Як це працює</string> - <string name="icon_descr_video_call">відеодзвінок</string> + <string name="icon_descr_video_call">відеовиклик</string> <string name="show_call_on_lock_screen">Показати</string> <string name="no_call_on_lock_screen">Вимкнути</string> - <string name="relay_server_if_necessary">Сервер ретрансляції використовується лише за необхідності. Інша сторона може спостерігати за вашою IP-адресою.</string> - <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d повідомлення не вдалося розшифрувати.</string> - <string name="alert_text_decryption_error_too_many_skipped">%1$d повідомлення пропущені.</string> - <string name="alert_text_fragment_please_report_to_developers">Будь ласка, повідомте про це розробникам.</string> - <string name="send_link_previews">Надіслати попередній перегляд за посиланням</string> - <string name="lock_after">Блокування після</string> - <string name="submit_passcode">Представити</string> + <string name="relay_server_if_necessary">Реле-сервер використовується лише у необхідних випадках. Інша сторона може спостерігати за вашою IP-адресою.</string> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d повідомлень не вдалося розшифрувати.</string> + <string name="alert_text_decryption_error_too_many_skipped">%1$d пропущено повідомлень.</string> + <string name="alert_text_fragment_please_report_to_developers">Будь ласка, повідомте розробникам про це.</string> + <string name="send_link_previews">Надсилати попередні перегляди посилань</string> + <string name="lock_after">Блокувати через</string> + <string name="submit_passcode">Підтвердити</string> <string name="enabled_self_destruct_passcode">Увімкнути пароль самознищення</string> <string name="change_self_destruct_mode">Змінити режим самознищення</string> <string name="incorrect_passcode">Неправильний пароль</string> @@ -880,581 +863,1529 @@ <string name="change_self_destruct_passcode">Змінити пароль самознищення</string> <string name="self_destruct_passcode">Пароль самознищення</string> <string name="enable_self_destruct">Увімкнути самознищення</string> - <string name="self_destruct_new_display_name">Нове ім\'я для відображення:</string> - <string name="if_you_enter_self_destruct_code">Якщо ви введете пароль самознищення під час відкриття програми:</string> - <string name="if_you_enter_passcode_data_removed">Якщо ви введете цей пароль при відкритті програми, всі дані програми будуть безповоротно видалені!</string> + <string name="self_destruct_new_display_name">Нове ім\'я профілю:</string> + <string name="if_you_enter_self_destruct_code">Якщо ви введете пароль самознищення при відкритті застосунку:</string> + <string name="if_you_enter_passcode_data_removed">Якщо ви введете цей пароль при відкритті застосунку, всі дані застосунку буде неможливо відновити!</string> <string name="set_passcode">Встановити пароль</string> - <string name="delete_chat_profile_action_cannot_be_undone_warning">Цю дію неможливо скасувати - ваш профіль, контакти, повідомлення та файли будуть безповоротно втрачені.</string> - <string name="chat_database_deleted">Видалено базу даних чату</string> - <string name="no_received_app_files">Немає отриманих або відправлених файлів</string> + <string name="delete_chat_profile_action_cannot_be_undone_warning">Цю дію неможливо відмінити - ваш профіль, контакти, повідомлення та файли буде втрачено безповоротно.</string> + <string name="chat_database_deleted">Базу даних чату видалено</string> + <string name="no_received_app_files">Немає отриманих або відісланих файлів</string> <string name="info_row_received_at">Отримано о</string> - <string name="info_row_deleted_at">Видалено за</string> - <string name="share_text_received_at">Отримано за: %s</string> - <string name="share_text_sent_at">Надіслано: %s</string> - <string name="share_text_deleted_at">Видалено за: %s</string> - <string name="current_version_timestamp">%s (поточний)</string> - <string name="ttl_h">%dh</string> + <string name="info_row_deleted_at">Видалено о</string> + <string name="share_text_received_at">Отримано о: %s</string> + <string name="share_text_sent_at">Надіслано о: %s</string> + <string name="share_text_deleted_at">Видалено о: %s</string> + <string name="current_version_timestamp">%s (поточне)</string> + <string name="ttl_h">%dч</string> <string name="ttl_day">%d день</string> <string name="ttl_days">%d днів</string> <string name="feature_cancelled_item">скасовано %s</string> - <string name="run_chat_section">ЗАПУСТИТИ ЧАТ</string> - <string name="database_passphrase">Ключова фраза бази даних</string> - <string name="export_database">Експорт бази даних</string> + <string name="run_chat_section">ЗАПУСК ЧАТУ</string> + <string name="database_passphrase">Пароль бази даних</string> + <string name="export_database">Експортувати базу даних</string> <string name="delete_files_and_media_all">Видалити всі файли</string> <string name="delete_messages_after">Видаляйте повідомлення після</string> <string name="enable_automatic_deletion_question">Увімкнути автоматичне видалення повідомлень\?</string> - <string name="should_be_at_least_one_visible_profile">Повинен бути принаймні один видимий профіль користувача.</string> <string name="v4_6_hidden_chat_profiles">Приховані профілі чату</string> - <string name="v4_6_group_welcome_message">Привітальне повідомлення групи</string> - <string name="v4_6_chinese_spanish_interface_descr">Дякуємо користувачам - зробіть свій внесок через Weblate!</string> - <string name="la_lock_mode">Режим SimpleX Lock</string> - <string name="la_lock_mode_system">Аутентифікація системи</string> - <string name="to_protect_privacy_simplex_has_ids_for_queues">Для захисту конфіденційності, замість ідентифікаторів користувачів, які використовуються на всіх інших платформах, SimpleX має ідентифікатори для черг повідомлень, окремі для кожного з ваших контактів.</string> - <string name="onboarding_notifications_mode_off">Коли додаток працює</string> - <string name="onboarding_notifications_mode_periodic">Періодичні</string> - <string name="status_contact_has_no_e2e_encryption">контакт не має шифрування e2e</string> + <string name="v4_6_group_welcome_message">Повідомлення вітання групи</string> + <string name="v4_6_chinese_spanish_interface_descr">Дякуємо користувачам – приєднуйтеся через Weblate!</string> + <string name="la_lock_mode">Режим блокування SimpleX</string> + <string name="la_lock_mode_system">Системна аутентифікація</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">Для захисту вашої конфіденційності SimpleX використовує окремі ID для кожного вашого контакту.</string> + <string name="onboarding_notifications_mode_off">Коли додаток запущено</string> + <string name="onboarding_notifications_mode_periodic">Періодично</string> + <string name="status_contact_has_no_e2e_encryption">контакт не має зашифрування e2e</string> <string name="enable_lock">Увімкнути блокування</string> <string name="passcode_not_changed">Пароль не змінено!</string> - <string name="change_lock_mode">Зміна режиму блокування</string> - <string name="stop_chat_to_enable_database_actions">Зупиніть чат, щоб увімкнути дії з базою даних.</string> + <string name="change_lock_mode">Змінити режим блокування</string> <string name="restart_the_app_to_create_a_new_chat_profile">Перезапустіть додаток, щоб створити новий профіль чату.</string> - <string name="delete_files_and_media_for_all_users">Видалення файлів для всіх профілів чату</string> - <string name="delete_files_and_media_question">Видаляти файли та медіа\?</string> - <string name="total_files_count_and_size">%d файл(и) загальним розміром %s</string> - <string name="database_is_not_encrypted">Ваша база даних чату не зашифрована - встановіть парольну фразу, щоб захистити її.</string> - <string name="impossible_to_recover_passphrase"><![CDATA[<b>Зверніть увагу</b>: ви НЕ зможете відновити або змінити парольну фразу, якщо її втратите.]]></string> + <string name="delete_files_and_media_for_all_users">Видалити файли для всіх профілів чату</string> + <string name="delete_files_and_media_question">Видалити файли та медіа?</string> + <string name="total_files_count_and_size">%d файл(ів) обсягом %s</string> + <string name="database_is_not_encrypted">Ваша база даних чату не зашифрована - встановіть ключову фразу для її захисту.</string> + <string name="impossible_to_recover_passphrase"><![CDATA[<b>Зверніть увагу</b>: ви НЕ зможете відновити або змінити ключову фразу, якщо ви її втратите.]]></string> <string name="theme_system">Система</string> <string name="incompatible_database_version">Несумісна версія бази даних</string> <string name="confirm_database_upgrades">Підтвердити оновлення бази даних</string> - <string name="invalid_migration_confirmation">Недійсне підтвердження перенесення</string> - <string name="mtr_error_no_down_migration">версія бази даних новіша, ніж додаток, але без міграції вниз: %s</string> - <string name="mtr_error_different">інша міграція в додатку/базі даних: %s / %s</string> + <string name="invalid_migration_confirmation">Недійсне підтвердження міграції</string> + <string name="mtr_error_no_down_migration">Версія бази даних новіша, ніж додаток, але немає можливості знизити до: %s</string> + <string name="mtr_error_different">Різна міграція в додатку/базі даних: %s / %s</string> <string name="database_migrations">Міграції: %s</string> - <string name="alert_title_group_invitation_expired">Термін дії запрошення закінчився!</string> - <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Ви більше не будете отримувати повідомлення від цієї групи. Історія чату буде збережена.</string> + <string name="alert_title_group_invitation_expired">Запрошення закінчилось!</string> + <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Ви перестанете отримувати повідомлення від цієї групи. Історія чату буде збережена.</string> <string name="icon_descr_add_members">Запросити учасників</string> <string name="icon_descr_group_inactive">Група неактивна</string> <string name="snd_group_event_member_deleted">ви видалили %1$s</string> - <string name="tap_to_activate_profile">Натисніть, щоб активувати профіль.</string> - <string name="cant_delete_user_profile">Не можу видалити профіль користувача!</string> - <string name="prohibit_sending_voice">Заборонити надсилання голосових повідомлень.</string> - <string name="group_members_can_send_disappearing">Учасники групи можуть надсилати зникаючі повідомлення.</string> + <string name="tap_to_activate_profile">Торкніться для активації профілю.</string> + <string name="prohibit_sending_voice">Забороняйте надсилання голосових повідомлень.</string> + <string name="group_members_can_send_disappearing">Учасники можуть надсилати повідомлення, що зникають.</string> <string name="ttl_min">%d хв</string> - <string name="v4_5_reduced_battery_usage">Зменшення використання акумулятора</string> + <string name="v4_5_reduced_battery_usage">Зменшене споживання енергії батареї</string> <string name="edit_image">Редагувати зображення</string> - <string name="integrity_msg_duplicate">дублююче повідомлення</string> - <string name="unknown_database_error_with_info">Невідома помилка в базі даних: %s</string> - <string name="enter_passphrase">Введіть пароль…</string> + <string name="integrity_msg_duplicate">дубльоване повідомлення</string> + <string name="unknown_database_error_with_info">Невідома помилка бази даних: %s</string> + <string name="enter_passphrase">Введіть ключову фразу…</string> <string name="open_chat">Відкрити чат</string> - <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Ви приєдналися до цієї групи. Підключення до запрошеного учасника групи.</string> - <string name="rcv_group_event_updated_group_profile">оновлений профіль групи</string> + <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Ви приєдналися до цієї групи. З\'єднання з учасником, який вас запрошував.</string> + <string name="rcv_group_event_updated_group_profile">оновлено профіль групи</string> <string name="snd_conn_event_switch_queue_phase_changing">змінює адресу…</string> <string name="leave_group_button">Залишити</string> <string name="group_member_role_observer">спостерігач</string> <string name="member_info_section_title_member">УЧАСНИК</string> <string name="incognito_info_protects">Режим інкогніто захищає вашу конфіденційність, використовуючи новий випадковий профіль для кожного контакту.</string> - <string name="v4_5_reduced_battery_usage_descr">Незабаром буде ще більше покращень!</string> - <string name="only_group_owners_can_enable_voice">Тільки власники груп можуть вмикати голосові повідомлення.</string> + <string name="v4_5_reduced_battery_usage_descr">Більше поліпшень незабаром!</string> + <string name="only_group_owners_can_enable_voice">Тільки власники груп можуть увімкнути голосові повідомлення.</string> <string name="reject_contact_button">Відхилити</string> <string name="clear_chat_question">Очистити чат\?</string> - <string name="image_descr_link_preview">посилання для попереднього перегляду зображення</string> + <string name="image_descr_link_preview">зображення прев’ю посилання</string> <string name="icon_descr_cancel_link_preview">скасувати попередній перегляд посилання</string> <string name="icon_descr_settings">Налаштування</string> - <string name="network_use_onion_hosts_no_desc_in_alert">Onion хости не використовуватимуться.</string> <string name="icon_descr_call_progress">Виклик у процесі</string> - <string name="database_error">Помилка в базі даних</string> + <string name="database_error">Помилка бази даних</string> <string name="restore_database_alert_confirm">Відновити</string> - <string name="group_member_status_connected">підключений</string> + <string name="group_member_status_connected">підключено</string> <string name="group_member_status_creator">творець</string> <string name="group_member_status_announced">підключення (оголошено)</string> <string name="ttl_month">%d місяць</string> - <string name="v4_5_multiple_chat_profiles_descr">Різні імена, аватарки та транспортна ізоляція.</string> + <string name="v4_5_multiple_chat_profiles_descr">Різні імена, аватари та ізоляція транспорту.</string> <string name="v4_5_private_filenames">Приватні імена файлів</string> <string name="v4_3_improved_server_configuration">Покращена конфігурація сервера</string> <string name="error_saving_user_password">Помилка збереження пароля користувача</string> - <string name="alert_title_cant_invite_contacts_descr">Ви використовуєте профіль анонімного перегляду для цієї групи – щоб запобігти спільному доступу до вашого основного профілю, запрошення контактів заборонено</string> + <string name="alert_title_cant_invite_contacts_descr">Ви використовуєте анонімний профіль для цієї групи - для запобігання розголошенню вашого основного профілю запрошення контактів не дозволено.</string> <string name="you_joined_this_group">Ви приєдналися до цієї групи</string> - <string name="la_could_not_be_verified">Вас не вдалося перевірити; будь ласка спробуйте ще раз.</string> - <string name="la_no_app_password">Немає пароля додатку</string> - <string name="la_enter_app_passcode">Введіть пароль</string> - <string name="la_current_app_passcode">Поточний пароль</string> - <string name="la_lock_mode_passcode">Введення пароля</string> - <string name="la_change_app_passcode">Змінити пароль</string> + <string name="la_could_not_be_verified">Вашу особу не вдалося підтвердити; спробуйте ще раз.</string> + <string name="la_no_app_password">Немає коду доступу до додатка</string> + <string name="la_enter_app_passcode">Введіть код доступу</string> + <string name="la_current_app_passcode">Поточний код доступу</string> + <string name="la_lock_mode_passcode">Введення коду доступу</string> + <string name="la_change_app_passcode">Змінити код доступу</string> <string name="la_immediately">Негайно</string> - <string name="la_seconds">%d секунд</string> - <string name="la_minutes">%d хвилин</string> + <string name="la_seconds">%d секунд(и)</string> + <string name="la_minutes">%d хвилин(и)</string> <string name="message_delivery_error_title">Помилка доставки повідомлення</string> <string name="network_session_mode_user">Профіль чату</string> <string name="reject">Відхилити</string> - <string name="relay_server_protects_ip">Сервер ретрансляції захищає вашу IP-адресу, але він може спостерігати за тривалістю дзвінка.</string> + <string name="relay_server_protects_ip">Реле-сервер захищає ваш IP-адресу, але може відслідковувати тривалість виклику.</string> <string name="lock_mode">Режим блокування</string> <string name="passcode_changed">Пароль змінено!</string> <string name="la_mode_system">Система</string> <string name="la_mode_passcode">Пароль</string> <string name="passcode_set">Пароль встановлено!</string> - <string name="upgrade_and_open_chat">Оновіть і відкрийте чат</string> - <string name="database_downgrade_warning">Попередження: ви можете втратити деякі дані!</string> - <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Ви можете розпочати чат через Налаштування програми / База даних або перезапустивши програму.</string> - <string name="you_sent_group_invitation">Ви надіслали запрошення до групи</string> - <string name="you_are_invited_to_group">Вас запросили до групи</string> - <string name="group_invitation_expired">Термін дії групового запрошення закінчився</string> - <string name="icon_descr_contact_checked">Контакт перевірено</string> - <string name="invite_prohibited_description">Ви намагаєтеся запросити контакт, з яким ви поділилися анонімним профілем, до групи, у якій ви використовуєте свій основний профіль</string> - <string name="error_creating_link_for_group">Помилка створення посилання на групу</string> + <string name="upgrade_and_open_chat">Оновити та відкрити чат</string> + <string name="database_downgrade_warning">Попередження: можливо, ви втратите деякі дані!</string> + <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Ви можете запустити чат через налаштування програми або перезапустивши додаток.</string> + <string name="you_sent_group_invitation">Ви відправили запрошення в групу</string> + <string name="you_are_invited_to_group">Вас запрошено в групу</string> + <string name="group_invitation_expired">Запрошення в групу закінчилось</string> + <string name="icon_descr_contact_checked">Контакт відмічено</string> + <string name="invite_prohibited_description">Ви намагаєтеся запросити контакт, з яким ви поділилися інкогніто-профілем, до групи, в якій ви використовуєте основний профіль</string> + <string name="error_creating_link_for_group">Помилка при створенні посилання на групу</string> <string name="section_title_for_console">ДЛЯ КОНСОЛІ</string> - <string name="member_will_be_removed_from_group_cannot_be_undone">Учасник буде видалений з групи - це неможливо скасувати!</string> + <string name="member_will_be_removed_from_group_cannot_be_undone">Учасника буде вилучено з групи - цю дію неможливо скасувати!</string> <string name="change_role">Змінити роль</string> - <string name="network_options_revert">Повернути</string> - <string name="you_will_still_receive_calls_and_ntfs">Ви все одно отримуватимете дзвінки та сповіщення від вимкнених профілів, якщо вони активні.</string> - <string name="ttl_months">%d місяців</string> - <string name="ttl_mth">%d міс</string> - <string name="v4_4_disappearing_messages_desc">Надіслані повідомлення будуть видалені через встановлений час.</string> + <string name="you_will_still_receive_calls_and_ntfs">Ви все ще отримуватимете дзвінки та сповіщення від приглушених профілів, коли вони активні.</string> + <string name="ttl_months">%d місяці</string> + <string name="ttl_mth">%dміс</string> + <string name="v4_4_disappearing_messages_desc">Надіслані повідомлення будуть видалені після встановленого часу.</string> <string name="opening_database">Відкриття бази даних…</string> - <string name="error_setting_address">Помилка налаштування адреси</string> - <string name="smp_server_test_connect">Підключіться</string> - <string name="la_please_remember_to_store_password">Будь ласка, запам\'ятайте або надійно збережіть його - втрачений пароль неможливо відновити!</string> - <string name="auth_open_chat_profiles">Відкрити профілі чату</string> - <string name="icon_descr_waiting_for_video">В очікуванні відео</string> - <string name="waiting_for_file">Очікування файлу</string> + <string name="error_setting_address">Помилка встановлення адреси</string> + <string name="smp_server_test_connect">Підключити</string> + <string name="la_please_remember_to_store_password">Будь ласка, запам\'ятайте або збережіть його надійно - немає можливості відновлення втраченого пароля!</string> + <string name="auth_open_chat_profiles">Змінити профілі чату</string> + <string name="icon_descr_waiting_for_video">Очікування на відео</string> + <string name="waiting_for_file">Очікування на файл</string> <string name="voice_message_with_duration">Голосове повідомлення (%1$s)</string> <string name="notifications">Сповіщення</string> <string name="delete_contact_question">Видалити контакт\?</string> - <string name="icon_descr_cancel_live_message">Скасувати повідомлення в прямому ефірі</string> + <string name="icon_descr_cancel_live_message">Скасувати живе повідомлення</string> <string name="reset_verb">Скинути</string> - <string name="no_details">без подробиць</string> + <string name="no_details">без деталей</string> <string name="connect_via_link_or_qr">Підключитися за посиланням / QR-кодом</string> - <string name="clear_verb">Чисто</string> + <string name="clear_verb">Очистити</string> <string name="invalid_QR_code">Неправильний QR-код</string> - <string name="you_will_be_connected_when_your_contacts_device_is_online">Ви будете з\'єднані, коли пристрій вашого контакту буде в мережі, будь ласка, зачекайте або перевірте пізніше!</string> - <string name="you_wont_lose_your_contacts_if_delete_address">Ви не втратите свої контакти, якщо згодом видалите свою адресу.</string> - <string name="you_can_accept_or_reject_connection">Коли люди звертаються із запитом на підключення, ви можете прийняти або відхилити його.</string> - <string name="read_more_in_user_guide_with_link"><![CDATA[Докладніше в <font color="#0088ff">Посібнику користувача</font>.]]></string> - <string name="simplex_address">Адреса SimpleX</string> - <string name="clear_verification">Очистити верифікацію</string> + <string name="you_will_be_connected_when_your_contacts_device_is_online">Підключення відбудеться, коли пристрій вашого контакту буде онлайн. Будь ласка, зачекайте або спробуйте пізніше!</string> + <string name="you_wont_lose_your_contacts_if_delete_address">Ви не втратите свої контакти, якщо ви пізніше видалите свою адресу.</string> + <string name="you_can_accept_or_reject_connection">Коли люди просять про з\'єднання, ви можете його прийняти чи відхилити.</string> + <string name="read_more_in_user_guide_with_link"><![CDATA[Докладніше читайте в <font color="#0088ff">Посібнику користувача</font>.]]></string> + <string name="simplex_address">SimpleX-адреса</string> + <string name="clear_verification">Скинути підтвердження</string> <string name="is_verified">%s перевірено</string> <string name="is_not_verified">%s не перевірено</string> - <string name="send_us_an_email">Надішліть нам електронний лист</string> + <string name="send_us_an_email">Написати нам ел. листа</string> <string name="smp_servers_test_server">Тестовий сервер</string> <string name="smp_save_servers_question">Зберегти сервери\?</string> <string name="your_ICE_servers">Ваші сервери ICE</string> <string name="save_servers_button">Зберегти</string> - <string name="network_socks_proxy_settings">Налаштування проксі SOCKS</string> - <string name="network_socks_toggle_use_socks_proxy">Використовуйте проксі SOCKS</string> + <string name="network_socks_proxy_settings">Налаштування SOCKS-проксі</string> + <string name="network_socks_toggle_use_socks_proxy">Використовувати SOCKS-проксі</string> <string name="network_proxy_port">порт %d</string> <string name="host_verb">Хост</string> <string name="port_verb">Порт</string> - <string name="network_use_onion_hosts_required">Потрібно</string> - <string name="theme_colors_section_title">КОЛЬОРИ ТЕМИ</string> - <string name="create_address_and_let_people_connect">Створіть адресу, щоб люди могли з вами зв\'язатися.</string> - <string name="your_contacts_will_remain_connected">Ваші контакти залишаться на зв’язку.</string> - <string name="create_simplex_address">Створіть адресу SimpleX</string> - <string name="profile_update_will_be_sent_to_contacts">Оновлення профілю буде надіслано вашим контактам.</string> - <string name="stop_sharing_address">Припинити ділитися адресою\?</string> - <string name="stop_sharing">Припиніть ділитися</string> - <string name="enter_welcome_message_optional">Введіть вітальне повідомлення... (необов\'язково)</string> + <string name="network_use_onion_hosts_required">Обов\'язково</string> + <string name="theme_colors_section_title">КОЛЬОРИ ІНТЕРФЕЙСУ</string> + <string name="create_address_and_let_people_connect">Створіть адресу, щоб дозволити людям підключатися до вас.</string> + <string name="your_contacts_will_remain_connected">Контакти залишатимуться підключеними.</string> + <string name="create_simplex_address">Створити SimpleX-адресу</string> + <string name="profile_update_will_be_sent_to_contacts">Оновлення профілю буде відправлено вашим контактам.</string> + <string name="stop_sharing_address">Зупинити поділ адреси?</string> + <string name="stop_sharing">Зупинити поділ</string> + <string name="enter_welcome_message_optional">Введіть текст привітання... (необов\'язково)</string> <string name="save_settings_question">Зберегти налаштування\?</string> - <string name="save_auto_accept_settings">Зберегти налаштування автоприйняття</string> + <string name="save_auto_accept_settings">Зберегти налаштування автоприйому</string> <string name="email_invite_body">Привіт! -\nЗв\'яжіться зі мною через SimpleX Chat: %s</string> +\nПриєднуйтесь до мене через SimpleX Chat: %s</string> <string name="invite_friends">Запросити друзів</string> - <string name="email_invite_subject">Давайте поговоримо в SimpleX Chat</string> + <string name="email_invite_subject">Давайте говорити в SimpleX Chat</string> <string name="continue_to_next_step">Продовжити</string> <string name="dont_create_address">Не створювати адресу</string> - <string name="callstate_received_answer">отримав відповідь…</string> - <string name="callstate_received_confirmation">отримав підтвердження…</string> + <string name="callstate_received_answer">отримано відповідь…</string> + <string name="callstate_received_confirmation">отримано підтвердження…</string> <string name="callstate_connecting">підключення…</string> - <string name="callstate_connected">підключений</string> - <string name="callstate_ended">закінчився</string> - <string name="immune_to_spam_and_abuse">Імунітет до спаму та зловживань</string> - <string name="contact_wants_to_connect_via_call">%1$s хоче зв\'язатися з вами через</string> - <string name="encrypted_video_call">e2e зашифрований відеодзвінок</string> + <string name="callstate_connected">підключено</string> + <string name="callstate_ended">завершено</string> + <string name="immune_to_spam_and_abuse">Стійкий до спаму</string> + <string name="contact_wants_to_connect_via_call">%1$s хоче підключитися до вас через</string> + <string name="encrypted_video_call">зашифрований e2e відеовиклик</string> <string name="ignore">Ігнорувати</string> - <string name="alert_text_skipped_messages_it_can_happen_when">Це може статися, коли: -\n1. Термін дії повідомлень закінчився в клієнті-відправнику через 2 дні або на сервері через 30 днів. -\n2. Не вдалося розшифрувати повідомлення, тому що ви або ваш контакт використовували стару резервну копію бази даних. -\n3. З\'єднання було скомпрометовано.</string> + <string name="alert_text_skipped_messages_it_can_happen_when">Це може трапитися, коли: +\n1. Повідомлення застаріли відправником через 2 дні або на сервері через 30 днів. +\n2. Помилка розшифровки повідомлення, оскільки ви або ваш контакт використовували застарілу резервну копію бази даних. +\n3. З\'єднання було компрометоване.</string> <string name="alert_title_skipped_messages">Пропущені повідомлення</string> - <string name="protect_app_screen">Захистіть екран програми</string> - <string name="error_stopping_chat">Помилка зупинки чату</string> - <string name="chat_database_imported">Імпорт бази даних чату</string> - <string name="restart_the_app_to_use_imported_chat_database">Перезапустіть програму, щоб використовувати імпортовану базу даних чату.</string> + <string name="protect_app_screen">Захист екрану застосунку</string> + <string name="error_stopping_chat">Помилка при зупинці чату</string> + <string name="chat_database_imported">База даних чату імпортована</string> + <string name="restart_the_app_to_use_imported_chat_database">Перезапустіть додаток, щоб використовувати імпортовану базу даних чату.</string> <string name="file_with_path">Файл: %s</string> - <string name="database_passphrase_is_required">Для відкриття чату потрібно ввести пароль до бази даних.</string> + <string name="database_passphrase_is_required">Для відкриття чату потрібна ключова фраза бази даних.</string> <string name="join_group_question">Приєднатися до групи\?</string> <string name="database_upgrade">Оновлення бази даних</string> - <string name="delete_archive">Видалити архів</string> - <string name="delete_chat_archive_question">Видалити архів чату\?</string> - <string name="leave_group_question">Покинути групу\?</string> + <string name="leave_group_question">Вийти з групи?</string> <string name="alert_title_no_group">Групу не знайдено!</string> - <string name="alert_message_no_group">Цієї групи більше не існує.</string> - <string name="alert_title_cant_invite_contacts">Неможливо запросити контакти!</string> + <string name="alert_message_no_group">Ця група більше не існує.</string> + <string name="alert_title_cant_invite_contacts">Неможливо запрошувати контакти!</string> <string name="group_member_status_complete">завершено</string> - <string name="button_welcome_message">Вітальне повідомлення</string> - <string name="select_contacts">Виберіть контакти</string> + <string name="button_welcome_message">Привітання</string> + <string name="select_contacts">Вибрати контакти</string> <string name="share_address">Поділитися адресою</string> - <string name="you_can_share_group_link_anybody_will_be_able_to_connect">Ви можете поділитися посиланням або QR-кодом - будь-хто зможе приєднатися до групи. Ви не втратите учасників групи, якщо пізніше видалите її.</string> - <string name="you_can_share_this_address_with_your_contacts">Ви можете поділитися цією адресою зі своїми контактами, щоб вони могли зв\'язатися з %s.</string> - <string name="info_row_local_name">Місцева назва</string> + <string name="you_can_share_group_link_anybody_will_be_able_to_connect">Ви можете поділитися посиланням або QR-кодом - будь-хто зможе приєднатися до групи. Ви не втратите учасників групи, якщо потім видалите її.</string> + <string name="you_can_share_this_address_with_your_contacts">Ви можете поділитися цією адресою зі своїми контактами, щоб вони могли підключитися до %s.</string> + <string name="info_row_local_name">Локальна назва</string> <string name="info_row_database_id">Ідентифікатор бази даних</string> <string name="group_welcome_preview">Попередній перегляд</string> - <string name="enter_welcome_message">Введіть вітальне повідомлення…</string> + <string name="enter_welcome_message">Введіть привітальне повідомлення…</string> <string name="switch_receiving_address">Змінити адресу отримання</string> <string name="create_secret_group_title">Створити секретну групу</string> - <string name="group_is_decentralized">Повністю децентралізована - видима лише для учасників.</string> - <string name="network_option_tcp_connection_timeout">Тайм-аут підключення TCP</string> - <string name="dont_show_again">Більше не показувати</string> - <string name="color_secondary">Вторинний</string> - <string name="color_surface">Меню та сповіщення</string> - <string name="color_title">Назва</string> - <string name="color_sent_message">Надіслано повідомлення</string> - <string name="color_received_message">Отримано повідомлення</string> + <string name="group_is_decentralized">Повністю децентралізовано - видимо тільки для учасників.</string> + <string name="network_option_tcp_connection_timeout">Тайм-аут з\'єднання TCP</string> + <string name="dont_show_again">Не показувати знову</string> + <string name="color_secondary">Другорядний</string> + <string name="color_surface">Меню та повідомлення</string> + <string name="color_title">Заголовок</string> + <string name="color_sent_message">Надіслане повідомлення</string> + <string name="color_received_message">Отримане повідомлення</string> <string name="chat_preferences_you_allow">Ви дозволяєте</string> <string name="chat_preferences_no">ні</string> - <string name="chat_preferences_off">вимкнено</string> - <string name="set_group_preferences">Встановіть налаштування групи</string> - <string name="your_preferences">Ваші уподобання</string> + <string name="chat_preferences_off">вимк</string> + <string name="set_group_preferences">Налаштування групи</string> + <string name="your_preferences">Налаштування</string> <string name="direct_messages">Прямі повідомлення</string> <string name="icon_descr_server_status_error">Помилка</string> - <string name="add_contact">Посилання на одноразове запрошення</string> - <string name="add_contact_or_create_group">Почніть новий чат</string> + <string name="add_contact">Одноразове запрошення</string> + <string name="add_contact_or_create_group">Створити новий чат</string> <string name="icon_descr_close_button">Кнопка закриття</string> <string name="smp_servers_preset_address">Попередньо встановлена адреса сервера</string> <string name="chat_is_stopped_indication">Чат зупинено</string> - <string name="save_and_update_group_profile">Збереження та оновлення профілю групи</string> + <string name="save_and_update_group_profile">Зберегти та оновити профіль групи</string> <string name="theme">Тема</string> - <string name="v4_6_audio_video_calls_descr">Підтримка Bluetooth та інші покращення.</string> - <string name="v4_6_group_moderation">Модерація груп</string> + <string name="v4_6_audio_video_calls_descr">Підтримка Bluetooth та інші поліпшення.</string> + <string name="v4_6_group_moderation">Модерація групи</string> <string name="profile_password">Пароль профілю</string> - <string name="video_will_be_received_when_contact_completes_uploading">Відео буде отримано, коли ваш контакт завершить завантаження.</string> - <string name="video_will_be_received_when_contact_is_online">Відео буде отримано, коли ваш контакт буде онлайн, будь ласка, зачекайте або перевірте пізніше!</string> - <string name="file_will_be_received_when_contact_completes_uploading">Файл буде отримано, коли ваш контакт завершить завантаження.</string> + <string name="video_will_be_received_when_contact_completes_uploading">Відео буде отримано, коли ваш контакт завершить його вивантаження.</string> + <string name="video_will_be_received_when_contact_is_online">Відео буде отримано, коли ваш контакт буде в мережі, будь ласка, зачекайте або перевірте пізніше!</string> + <string name="file_will_be_received_when_contact_completes_uploading">Файл буде отримано, коли ваш контакт завершить його вивантаження.</string> <string name="error_saving_file">Помилка збереження файлу</string> <string name="button_delete_contact">Видалити контакт</string> <string name="text_field_set_contact_placeholder">Встановити ім\'я контакту…</string> <string name="choose_file">Файл</string> - <string name="from_gallery_button">З галереї</string> - <string name="icon_descr_simplex_team">SimpleX Команда</string> - <string name="contact_wants_to_connect_with_you">хоче зв\'язатися з вами!</string> - <string name="settings_section_title_experimenta">ЕКСПЕРИМЕНТАЛЬНИЙ</string> - <string name="you_must_use_the_most_recent_version_of_database">Ви повинні використовувати найновішу версію вашої бази даних чату ЛИШЕ на одному пристрої, інакше ви можете перестати отримувати повідомлення від деяких контактів.</string> - <string name="messages_section_description">Це налаштування застосовується до повідомлень у вашому поточному профілі чату</string> + <string name="from_gallery_button">Галерея</string> + <string name="icon_descr_simplex_team">Команда SimpleX</string> + <string name="contact_wants_to_connect_with_you">хоче підключитися до вас!</string> + <string name="settings_section_title_experimenta">ЕКСПЕРИМЕНТАЛЬНІ ФУНКЦІЇ</string> + <string name="you_must_use_the_most_recent_version_of_database">Ви повинні використовувати найновішу версію бази даних чату лише на одному пристрої, інакше ви можете припинити отримання повідомлень від деяких контактів.</string> + <string name="messages_section_description">Цей параметр застосовується до повідомлень у вашому поточному профілі чату</string> <string name="encrypted_database">Зашифрована база даних</string> <string name="error_with_info">Помилка: %s</string> - <string name="database_downgrade">Пониження бази даних</string> - <string name="alert_message_group_invitation_expired">Групове запрошення більше не дійсне, воно було видалено відправником.</string> - <string name="rcv_group_event_member_deleted">видалено %1$s</string> - <string name="group_member_status_intro_invitation">підключення (вступне запрошення)</string> + <string name="database_downgrade">Зниження версії бази даних</string> + <string name="alert_message_group_invitation_expired">Запрошення в групу більше не дійсне, його видалив відправник.</string> + <string name="rcv_group_event_member_deleted">видалив(ла) %1$s</string> + <string name="group_member_status_intro_invitation">підключення (запрошення для введення)</string> <string name="group_member_status_connecting">підключення</string> <string name="no_contacts_to_add">Немає контактів для додавання</string> <string name="initial_member_role">Початкова роль</string> - <string name="clear_contacts_selection_button">Чисто</string> + <string name="clear_contacts_selection_button">Очистити</string> <string name="whats_new_read_more">Читати далі</string> - <string name="v4_3_irreversible_message_deletion">Безповоротне видалення повідомлення</string> - <string name="v4_4_french_interface_descr">Дякуємо користувачам - зробіть свій внесок через Weblate!</string> - <string name="enter_passphrase_notification_desc">Щоб отримувати повідомлення, будь ласка, введіть пароль до бази даних</string> + <string name="v4_3_irreversible_message_deletion">Незворотне видалення повідомлень</string> + <string name="v4_4_french_interface_descr">Дякуємо користувачам – приєднуйтеся через Weblate!</string> + <string name="enter_passphrase_notification_desc">Для отримання сповіщень, будь ласка, введіть пароль бази даних</string> <string name="file_not_found">Файл не знайдено</string> - <string name="connect_via_link">Підключіться за посиланням</string> - <string name="connect_button">Підключіться</string> - <string name="to_reveal_profile_enter_password">Щоб відкрити свій прихований профіль, введіть повний пароль у поле пошуку на сторінці \"Ваші профілі чату\".</string> - <string name="confirm_password">Підтвердьте пароль</string> - <string name="ttl_d">%dd</string> - <string name="v4_6_hidden_chat_profiles_descr">Захистіть свої профілі чату паролем!</string> - <string name="decryption_error">Помилка розшифровки</string> + <string name="connect_via_link">Підключитися через посилання</string> + <string name="connect_button">Підключитися</string> + <string name="to_reveal_profile_enter_password">Щоб показати ваш схований профіль, введіть повний пароль у поле пошуку на сторінці Ваші профілі.</string> + <string name="confirm_password">Підтвердити пароль</string> + <string name="ttl_d">%dд</string> + <string name="v4_6_hidden_chat_profiles_descr">Захистіть свої чат-профілі паролем!</string> + <string name="decryption_error">Помилка дешифрування</string> <string name="error_xftp_test_server_auth">Сервер вимагає авторизації для завантаження, перевірте пароль</string> <string name="smp_server_test_upload_file">Завантажити файл</string> <string name="smp_server_test_download_file">Завантажити файл</string> <string name="database_initialization_error_title">Не вдається ініціалізувати базу даних</string> <string name="database_initialization_error_desc">База даних працює некоректно. Натисніть, щоб дізнатися більше</string> - <string name="periodic_notifications_desc">Додаток періодично отримує нові повідомлення - він використовує кілька відсотків заряду акумулятора на день. Додаток не використовує push-сповіщення - дані з вашого пристрою не надсилаються на сервери.</string> - <string name="ntf_channel_calls">Дзвінки SimpleX Chat</string> + <string name="periodic_notifications_desc">Програма періодично отримує нові повідомлення — це використовує кілька відсотків батареї щодня. Програма не використовує торкання сповіщень — дані з вашого пристрою не відправляються на сервери.</string> + <string name="ntf_channel_calls">Дзвінки чату SimpleX</string> <string name="settings_notifications_mode_title">Служба сповіщень</string> <string name="settings_notification_preview_mode_title">Показати попередній перегляд</string> - <string name="settings_notification_preview_title">Попередній перегляд сповіщень</string> - <string name="notifications_mode_off">Запускається, коли програма відкрита</string> - <string name="notifications_mode_periodic">Періодично запускається</string> + <string name="settings_notification_preview_title">Перегляд сповіщень</string> + <string name="notifications_mode_off">Запускається, коли додаток відкритий</string> + <string name="notifications_mode_periodic">Запускається періодично</string> <string name="notification_preview_mode_message">Текст повідомлення</string> - <string name="notification_preview_mode_contact">Ім\'я контактної особи</string> - <string name="auth_confirm_credential">Підтвердіть свій обліковий запис</string> - <string name="you_can_turn_on_lock">Увімкнути SimpleX Lock можна в Налаштуваннях.</string> - <string name="message_delivery_error_desc">Швидше за все, цей контакт видалив зв\'язок з вами.</string> - <string name="icon_descr_context">Контекстна піктограма</string> + <string name="notification_preview_mode_contact">Ім\'я контакту</string> + <string name="auth_confirm_credential">Підтвердіть свої облікові дані</string> + <string name="you_can_turn_on_lock">Ви можете увімкнути блокування SimpleX через налаштування.</string> + <string name="message_delivery_error_desc">Ймовірно, цей контакт видалив з вами зв\'язок.</string> + <string name="icon_descr_context">Значок контексту</string> <string name="images_limit_title">Забагато зображень!</string> <string name="videos_limit_title">Забагато відео!</string> <string name="you_are_observer">ви спостерігач</string> - <string name="colored_text">кольорові</string> - <string name="callstatus_ended">дзвінок закінчився %1$s</string> - <string name="callstatus_error">помилка виклику</string> - <string name="next_generation_of_private_messaging">Наступне покоління приватних повідомлень</string> - <string name="opensource_protocol_and_code_anybody_can_run_servers">Протокол і код з відкритим вихідним кодом - будь-хто може запускати сервери.</string> - <string name="settings_developer_tools">Інструменти для розробників</string> - <string name="settings_experimental_features">Експериментальні особливості</string> + <string name="colored_text">кольоровий</string> + <string name="callstatus_ended">дзвінок завершено %1$s</string> + <string name="callstatus_error">помилка дзвінка</string> + <string name="next_generation_of_private_messaging">Майбутнє обміну повідомленнями</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Кожен може хостити сервери.</string> + <string name="settings_developer_tools">Інструменти розробника</string> + <string name="settings_experimental_features">Експериментальні функції</string> <string name="settings_section_title_calls">ДЗВІНКИ</string> - <string name="save_passphrase_in_keychain">Збережіть парольну фразу у сховищі ключів</string> + <string name="save_passphrase_in_keychain">Зберегти ключову фразу в сховищі ключів</string> <string name="error_encrypting_database">Помилка шифрування бази даних</string> - <string name="remove_passphrase_from_keychain">Видалити парольну фразу з Keystore\?</string> - <string name="enter_correct_current_passphrase">Будь ласка, введіть правильний поточний пароль.</string> - <string name="store_passphrase_securely_without_recover">Будь ласка, зберігайте пароль надійно, ви НЕ зможете отримати доступ до чату, якщо втратите його.</string> + <string name="remove_passphrase_from_keychain">Вилучити ключову фразу із сховища ключів?</string> + <string name="enter_correct_current_passphrase">Будь ласка, введіть правильну поточну ключову фразу.</string> + <string name="store_passphrase_securely_without_recover">Будь ласка, зберігайте ключову фразу надійно, ви НЕ зможете отримати доступ до чату, якщо втратите її.</string> <string name="users_delete_question">Видалити профіль чату\?</string> - <string name="stop_file__action">Зупинити файл</string> - <string name="stop_snd_file__title">Припинити надсилати файл\?</string> + <string name="stop_file__action">Зупинити відправлення файлу</string> + <string name="stop_snd_file__title">Зупинити відправлення файлу?</string> <string name="create_group">Створити секретну групу</string> <string name="to_share_with_your_contact">(щоб поділитися з вашим контактом)</string> - <string name="connect_via_link_or_qr_from_clipboard_or_in_person">(відсканувати або вставити з буфера обміну)</string> - <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Ви можете <font color="#0088ff"> зв\'язатися з розробниками SimpleX Chat, щоб задати будь-які питання та отримати оновлення</font>.]]></string> - <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 десктоп: відскануйте відображений QR-код з додатку, за допомогою <b>Відскануйте QR-код</b>.]]></string> - <string name="icon_descr_address">SimpleX Адреса</string> + <string name="connect_via_link_or_qr_from_clipboard_or_in_person">(сканувати або вставити з буферу обміну)</string> + <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Ви можете <font color="#0088ff">підключитися до розробників SimpleX Chat, щоб задати будь-які питання і отримувати оновлення</font>.]]></string> + <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 на комп\'ютері: скануйте відображений QR-код з додатка за допомогою <b>Сканувати QR-код</b>.]]></string> + <string name="icon_descr_address">Адреса SimpleX</string> <string name="show_QR_code">Показати QR-код</string> <string name="joining_group">Приєднання до групи</string> <string name="italic_text">курсив</string> - <string name="webrtc_ice_servers">Сервери WebRTC ICE</string> - <string name="allow_accepting_calls_from_lock_screen">Увімкніть дзвінки з екрана блокування через Налаштування.</string> - <string name="v5_0_large_files_support_descr">Швидко і без очікування, поки відправник буде онлайн!</string> + <string name="webrtc_ice_servers">WebRTC сервери ICE</string> + <string name="allow_accepting_calls_from_lock_screen">Увімкніть прийом викликів з екрану блокування через Налаштування.</string> + <string name="v5_0_large_files_support_descr">Швидко та без очікування, поки відправник у мережі!</string> <string name="item_info_no_text">немає тексту</string> - <string name="button_send_direct_message">Надішліть пряме повідомлення</string> + <string name="button_send_direct_message">Надіслати пряме повідомлення</string> <string name="network_option_ping_interval">Інтервал PING</string> <string name="network_option_ping_count">Кількість PING</string> <string name="network_option_enable_tcp_keep_alive">Увімкнути TCP keep-alive</string> <string name="users_delete_profile_for">Видалити профіль чату для</string> <string name="user_hide">Приховати</string> - <string name="theme_dark">Темний</string> - <string name="language_system">Система</string> + <string name="theme_dark">Темна</string> + <string name="language_system">Системна</string> <string name="dark_theme">Темна тема</string> - <string name="import_theme">Імпорт теми</string> - <string name="export_theme">Експорт теми</string> + <string name="import_theme">Імпортувати тему</string> + <string name="export_theme">Експортувати тему</string> <string name="import_theme_error_desc">Переконайтеся, що файл має правильний синтаксис YAML. Експортуйте тему, щоб мати приклад структури файлу теми.</string> <string name="reset_color">Скинути кольори</string> - <string name="chat_preferences_default">за замовчуванням (%s)</string> - <string name="only_you_can_make_calls">Дзвонити можете тільки ви.</string> + <string name="chat_preferences_default">типово (%s)</string> + <string name="only_you_can_make_calls">Тільки ви можете здійснювати дзвінки.</string> <string name="only_your_contact_can_make_calls">Тільки ваш контакт може здійснювати дзвінки.</string> <string name="whats_new">Що нового</string> <string name="v4_3_voice_messages">Голосові повідомлення</string> - <string name="v4_5_transport_isolation_descr">За профілем чату (за замовчуванням) або за з\'єднанням (BETA).</string> - <string name="v5_1_custom_themes">Користувацькі теми</string> - <string name="v5_1_better_messages_descr">- голосові повідомлення до 5 хвилин. -\n- користувальницький час зникнення. -\n- історія редагування.</string> + <string name="v4_5_transport_isolation_descr">За профілем чату (типово) або за підключенням (BETA).</string> + <string name="v5_1_custom_themes">Власні теми</string> + <string name="v5_1_better_messages_descr">- голосові повідомлення до 5 хвилин.\n- налаштування часу для зникнення повідомлень.\n- історія змін.</string> <string name="v5_1_japanese_portuguese_interface">Японський та португальський інтерфейс</string> <string name="group_invitation_tap_to_join">Натисніть, щоб приєднатися</string> - <string name="group_invitation_tap_to_join_incognito">Натисніть, щоб приєднатися інкогніто</string> - <string name="you_rejected_group_invitation">Ви відхилили запрошення до групи</string> - <string name="rcv_group_event_user_deleted">видалив вас</string> + <string name="group_invitation_tap_to_join_incognito">Натисніть, щоб приєднатися анонімно</string> + <string name="you_rejected_group_invitation">Ви відхилили запрошення в групу</string> + <string name="rcv_group_event_user_deleted">вас видалили</string> <string name="group_member_role_owner">власник</string> <string name="group_member_status_removed">видалено</string> - <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Якщо ви не можете зустрітися особисто, ви можете <b>відсканувати QR-код у відеодзвінку</b>, або ваш контакт може поділитися посиланням на запрошення.]]></string> - <string name="how_to_use_simplex_chat">Як ним користуватися</string> + <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Якщо ви не можете зустрітися особисто, ви можете <b>сканувати QR-код у відеовиклику</b>, або ваш контакт може поділитися посиланням на запрошення.]]></string> + <string name="how_to_use_simplex_chat">Як користуватися</string> <string name="network_session_mode_entity">Підключення</string> - <string name="disable_onion_hosts_when_not_supported"><![CDATA[Встановіть <i>Використовувати .onion хости</i> на Ні, якщо SOCKS проксі не підтримує їх.]]></string> - <string name="show_dev_options">Покажи:</string> - <string name="hide_dev_options">Приховати:</string> - <string name="non_fatal_errors_occured_during_import">Під час імпорту виникли деякі нефатальні помилки – ви можете переглянути консоль чату, щоб дізнатися більше.</string> - <string name="enable_automatic_deletion_message">Цю дію неможливо скасувати - повідомлення, надіслані та отримані раніше, ніж вибрані, будуть видалені. Це може зайняти кілька хвилин.</string> - <string name="you_have_to_enter_passphrase_every_time">Вам доведеться вводити парольну фразу щоразу при запуску програми - вона не зберігається на пристрої.</string> - <string name="change_database_passphrase_question">Змінити пароль до бази даних\?</string> - <string name="cannot_access_keychain">Не вдається отримати доступ до сховища ключів, щоб зберегти пароль до бази даних</string> - <string name="save_passphrase_and_open_chat">Збережіть пароль і відкрийте чат</string> - <string name="restore_passphrase_not_found_desc">Пароль не знайдено у сховищі ключів, будь ласка, введіть його вручну. Це могло статися, якщо ви відновили дані програми за допомогою інструменту резервного копіювання. Якщо це не так, зверніться до розробників.</string> + <string name="disable_onion_hosts_when_not_supported"><![CDATA[Встановіть <i>Використовувати .onion-хости</i> на Ні, якщо SOCKS-проксі їх не підтримує.]]></string> + <string name="show_dev_options">Показати:</string> + <string name="hide_dev_options">Сховати:</string> + <string name="non_fatal_errors_occured_during_import">Під час імпорту відбулися деякі непередбачувані помилки:</string> + <string name="enable_automatic_deletion_message">Цю дію неможливо відмінити - будуть видалені повідомлення, відправлені та отримані раніше вибраного часу. Це може зайняти декілька хвилин.</string> + <string name="you_have_to_enter_passphrase_every_time">Вам потрібно вводити ключову фразу кожен раз при запуску додатка - вона не зберігається на пристрої.</string> + <string name="change_database_passphrase_question">Змінити ключову фразу бази даних?</string> + <string name="cannot_access_keychain">Не вдається отримати доступ до сховища ключів для збереження пароля бази даних</string> + <string name="save_passphrase_and_open_chat">Зберегти ключову фразу і відкрити чат</string> + <string name="restore_passphrase_not_found_desc">Пароль не знайдено в сховищі ключів, введіть його вручну. Це могло статися, якщо ви відновили дані додатка за допомогою інструмента резервного копіювання. Якщо це не так, зверніться до розробників.</string> <string name="delete_chat_profile">Видалити профіль чату</string> <string name="ttl_sec">%d сек</string> <string name="search_verb">Пошук</string> - <string name="la_mode_off">Вимкнено</string> - <string name="receipts_contacts_override_disabled">Відправлення підтвердження доставлення вимкнено для контакту %d</string> - <string name="receipts_contacts_disable_keep_overrides">Вимкнути (зберегти перевизначення)</string> + <string name="la_mode_off">Вимк.</string> + <string name="receipts_contacts_override_disabled">Надсилання повідомлень про доставку вимкнено для %d контактів</string> + <string name="receipts_contacts_disable_keep_overrides">Вимкнути (зберегти заміни)</string> <string name="receipts_contacts_enable_for_all">Увімкнути для всіх</string> - <string name="conn_event_ratchet_sync_ok">шифрування ok</string> + <string name="conn_event_ratchet_sync_ok">шифрування в порядку</string> <string name="conn_event_ratchet_sync_started">узгодження шифрування…</string> <string name="snd_conn_event_ratchet_sync_started">узгодження шифрування для %s…</string> - <string name="sender_at_ts">%s в %s</string> - <string name="sending_delivery_receipts_will_be_enabled">Для всіх контактів буде ввімкнено відправку підтвердження доставки.</string> - <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Відправлення підтвердження доставлення буде ввімкнено для всіх контактів у всіх видимих профілях чату.</string> - <string name="you_can_enable_delivery_receipts_later">Ви можете увімкнути пізніше в Налаштуваннях</string> - <string name="dont_enable_receipts">Не вмикати</string> - <string name="error_enabling_delivery_receipts">Помилка активації підтвердження доставлення!</string> - <string name="error_aborting_address_change">Помилка переривання зміни адреси</string> - <string name="abort_switch_receiving_address">Скасувати зміну адреси</string> - <string name="allow_to_send_files">Дозволяє надсилати файли та медіа.</string> - <string name="files_are_prohibited_in_group">Файли та медіа в цій групі заборонені.</string> - <string name="connect_via_link_incognito">Підключайтеся інкогніто</string> + <string name="sender_at_ts">%s о %s</string> + <string name="sending_delivery_receipts_will_be_enabled">Відправка квитанцій про доставку буде увімкнена для всіх контактів.</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Відправка квитанцій про доставку буде увімкнена для всіх контактів у всіх видимих чат-профілях.</string> + <string name="you_can_enable_delivery_receipts_later">Ви можете увімкнути їх пізніше через налаштування</string> + <string name="dont_enable_receipts">Не увімкнювати</string> + <string name="error_enabling_delivery_receipts">Помилка увімкнення квитанцій про доставку!</string> + <string name="error_aborting_address_change">Помилка відміни зміни адреси</string> + <string name="abort_switch_receiving_address">Перервати зміну адреси</string> + <string name="allow_to_send_files">Дозволити надсилання файлів та медіафайлів.</string> + <string name="files_are_prohibited_in_group">Файли та медіа заборонені.</string> + <string name="connect_via_link_incognito">Підключити інкогніто</string> <string name="connect_use_current_profile">Використовувати поточний профіль</string> - <string name="turn_off_battery_optimization_button">Дозвольте</string> + <string name="turn_off_battery_optimization_button">Дозволити</string> <string name="disable_notifications_button">Вимкнути сповіщення</string> - <string name="turn_off_system_restriction_button">Відкрийте налаштування програми</string> - <string name="system_restricted_background_desc">SimpleX не може працювати у фоновому режимі. Ви будете отримувати сповіщення лише під час роботи програми.</string> - <string name="system_restricted_background_in_call_title">Ніяких фонових дзвінків</string> - <string name="system_restricted_background_in_call_desc">Додаток можна закрити після 1 хвилини роботи у фоновому режимі.</string> + <string name="turn_off_system_restriction_button">Відкрити налаштування додатка</string> + <string name="system_restricted_background_desc">SimpleX не може працювати в фоновому режимі. Ви отримуватимете сповіщення лише тоді, коли програма працює.</string> + <string name="system_restricted_background_in_call_title">Обмеженість фонових дзвінків</string> + <string name="system_restricted_background_in_call_desc">Додаток може бути закритий після 1 хвилини в фоновому режимі.</string> <string name="in_reply_to">У відповідь на</string> <string name="no_history">Немає історії</string> - <string name="files_and_media_prohibited">Файли та медіа заборонені!</string> - <string name="connect__your_profile_will_be_shared">Ваш профіль %1$s буде опублікований.</string> + <string name="files_and_media_prohibited">Заборонено файли та медіа!</string> + <string name="connect__your_profile_will_be_shared">Буде відправлено ваш профіль %1$s.</string> <string name="receipts_groups_disable_for_all">Вимкнути для всіх груп</string> - <string name="settings_section_title_delivery_receipts">НАДІШЛІТЬ ПІДТВЕРДЖЕННЯ ДОСТАВКИ НА АДРЕСУ</string> - <string name="connect_via_member_address_alert_title">Підключатися напряму\?</string> + <string name="settings_section_title_delivery_receipts">НАДСИЛАТИ ПОВІДОМЛЕННЯ ПРО ДОСТАВКУ</string> + <string name="connect_via_member_address_alert_title">Підключитися безпосередньо?</string> <string name="recipient_colon_delivery_status">%s: %s</string> - <string name="connect_via_member_address_alert_desc">Запит на підключення буде надіслано цьому учаснику групи.</string> - <string name="fix_connection_confirm">Виправлення</string> + <string name="connect_via_member_address_alert_desc">Запит на підключення буде відправлено учаснику групи.</string> + <string name="fix_connection_confirm">Виправити</string> <string name="files_and_media">Файли та медіа</string> <string name="v5_2_favourites_filter_descr">Фільтруйте непрочитані та улюблені чати.</string> <string name="v5_2_favourites_filter">Швидше знаходьте чати</string> - <string name="v5_2_disappear_one_message">Зробити так, щоб одне повідомлення зникло</string> - <string name="no_selected_chat">Немає вибраного чату</string> + <string name="v5_2_disappear_one_message">Зникайте одне повідомлення</string> + <string name="no_selected_chat">Не обрано жодного чату</string> <string name="abort_switch_receiving_address_confirm">Скасувати</string> <string name="choose_file_title">Виберіть файл</string> <string name="receipts_section_contacts">Контакти</string> <string name="receipts_section_description">Ці налаштування стосуються вашого поточного профілю</string> - <string name="receipts_contacts_title_disable">Вимкнути підтвердження\?</string> - <string name="receipts_contacts_title_enable">Активувати підтвердження\?</string> - <string name="conn_event_ratchet_sync_allowed">переузгодження шифрування дозволено</string> - <string name="snd_conn_event_ratchet_sync_agreed">узгоджене шифрування для %s</string> - <string name="snd_conn_event_ratchet_sync_allowed">переузгодження шифрування дозволено для %s</string> - <string name="snd_conn_event_ratchet_sync_required">потрібно переузгодження шифрування для %s</string> - <string name="rcv_conn_event_verification_code_reset">змінено код безпеки</string> + <string name="receipts_contacts_title_disable">Вимкнути повідомлення про доставку?</string> + <string name="receipts_contacts_title_enable">Увімкнути повідомлення про доставку?</string> + <string name="conn_event_ratchet_sync_allowed">можлива перезапис шифрування</string> + <string name="snd_conn_event_ratchet_sync_agreed">шифрування узгоджено для %s</string> + <string name="snd_conn_event_ratchet_sync_allowed">можлива перезапис шифрування для %s</string> + <string name="snd_conn_event_ratchet_sync_required">перезапис шифрування обов\'язковий для %s</string> + <string name="rcv_conn_event_verification_code_reset">код безпеки змінено</string> <string name="fix_connection_not_supported_by_contact">Виправлення не підтримується контактом</string> - <string name="send_receipts">Надсилати звіти про доставку</string> - <string name="prohibit_sending_files">Заборонити надсилання файлів і медіа.</string> - <string name="v5_2_fix_encryption_descr">Виправити шифрування після відновлення резервних копій.</string> - <string name="v5_2_fix_encryption">Зберігайте свої зв\'язки</string> - <string name="v5_2_more_things_descr">- стабільніше доставлення повідомлень. -\n- трохи кращі групи. -\n- і багато іншого!</string> - <string name="in_developing_title">Вже скоро!</string> + <string name="send_receipts">Надсилати звіти</string> + <string name="prohibit_sending_files">Заборонити надсилання файлів та медіафайлів.</string> + <string name="v5_2_fix_encryption_descr">Виправте шифрування після відновлення резервних копій.</string> + <string name="v5_2_fix_encryption">Зберігайте ваші підключення</string> + <string name="v5_2_more_things_descr">- стабільніша доставка повідомлень. +\n- трошки кращі групи. +\n- та багато іншого!</string> + <string name="in_developing_title">Скоро!</string> <string name="delivery">Доставка</string> - <string name="no_info_on_delivery">Немає інформації про доставлення</string> + <string name="no_info_on_delivery">Немає інформації про доставку</string> <string name="no_filtered_chats">Немає фільтрованих чатів</string> - <string name="sync_connection_force_confirm">Переузгодьте</string> - <string name="unfavorite_chat">Неулюблене</string> + <string name="sync_connection_force_confirm">Узгодити повторно</string> + <string name="unfavorite_chat">Забрати з улюблених</string> <string name="receipts_contacts_disable_for_all">Вимкнути для всіх</string> - <string name="receipts_contacts_enable_keep_overrides">Увімкнути (зберегти перевизначення)</string> - <string name="receipts_contacts_override_enabled">Підтвердження надсилання ввімкнено для контакту %d</string> - <string name="receipts_section_groups">Невеликі групи (максимум 20 осіб)</string> - <string name="receipts_groups_title_disable">Вимкнути підтвердження доставлення для груп\?</string> - <string name="receipts_groups_title_enable">Активувати підтвердження доставлення для груп\?</string> - <string name="receipts_groups_override_enabled">Надсилання підтвердження доставлення дозволено для груп %d</string> - <string name="receipts_groups_disable_keep_overrides">Вимкнути (зберегти групові перевизначення)</string> + <string name="receipts_contacts_enable_keep_overrides">Увімкнути (зберегти заміни)</string> + <string name="receipts_contacts_override_enabled">Надсилання повідомлень про доставку увімкнено для %d контактів</string> + <string name="receipts_section_groups">Невеликі групи (макс. 20)</string> + <string name="receipts_groups_title_disable">Вимкнути повідомлення про доставку для груп?</string> + <string name="receipts_groups_title_enable">Увімкнути повідомлення про доставку для груп?</string> + <string name="receipts_groups_override_enabled">Надсилання повідомлень про доставку увімкнено для %d груп</string> + <string name="receipts_groups_disable_keep_overrides">Вимкнути (зберегти заміни для груп)</string> <string name="receipts_groups_enable_for_all">Увімкнути для всіх груп</string> - <string name="receipts_groups_enable_keep_overrides">Увімкнути (зберегти перевизначення групи)</string> - <string name="conn_event_ratchet_sync_required">потрібне повторне узгодження шифрування</string> - <string name="send_receipts_disabled_alert_title">Підтвердження доставки вимкнено</string> + <string name="receipts_groups_enable_keep_overrides">Увімкнути (зберегти заміни для груп)</string> + <string name="conn_event_ratchet_sync_required">перезапис шифрування обов\'язковий</string> + <string name="send_receipts_disabled_alert_title">Звіти вимкнено</string> <string name="send_receipts_disabled">вимкнено</string> - <string name="send_receipts_disabled_alert_msg">У цій групі більше %1$d учасників, підтвердження доставлення не буде надіслано.</string> - <string name="in_developing_desc">Ця функція поки що не підтримується. Спробуйте наступну версію.</string> - <string name="fix_connection">Виправити з\'єднання</string> - <string name="fix_connection_question">Виправити з\'єднання\?</string> - <string name="renegotiate_encryption">Переузгодьте шифрування</string> - <string name="network_option_protocol_timeout_per_kb">Тайм-аут протоколу на КБ</string> - <string name="conn_event_ratchet_sync_agreed">узгоджено шифрування</string> - <string name="rcv_group_event_2_members_connected">%s і %s під\'єднано</string> + <string name="send_receipts_disabled_alert_msg">У цій групі понад %1$d учасників, звіти про доставку не відправляються.</string> + <string name="in_developing_desc">Ця функція ще не підтримується. Спробуйте наступний випуск.</string> + <string name="fix_connection">Виправити підключення</string> + <string name="fix_connection_question">Виправити підключення?</string> + <string name="renegotiate_encryption">Переговорити щодо шифрування</string> + <string name="network_option_protocol_timeout_per_kb">Тайм-аут протоколу за КБ</string> + <string name="conn_event_ratchet_sync_agreed">шифрування узгоджено</string> + <string name="rcv_group_event_2_members_connected">%s і %s підключилися</string> <string name="fix_connection_not_supported_by_group_member">Виправлення не підтримується учасником групи</string> - <string name="v5_2_message_delivery_receipts">Підтвердження доставлення повідомлення!</string> - <string name="v5_2_message_delivery_receipts_descr">Другу галочку ми пропустили! ✅</string> - <string name="favorite_chat">Улюблений</string> - <string name="v5_2_more_things">Ще кілька моментів</string> - <string name="connect__a_new_random_profile_will_be_shared">Буде створено новий випадковий профіль.</string> - <string name="delivery_receipts_title">Підтвердження доставлення!</string> - <string name="delivery_receipts_are_disabled">Підтвердження доставлення вимкнено!</string> + <string name="v5_2_message_delivery_receipts">Підтвердження доставки повідомлень!</string> + <string name="v5_2_message_delivery_receipts_descr">Друга галочка, яку ми пропустили! ✅</string> + <string name="favorite_chat">Улюблені</string> + <string name="v5_2_more_things">Ще кілька речей</string> + <string name="connect__a_new_random_profile_will_be_shared">Буде відправлено новий випадковий профіль.</string> + <string name="delivery_receipts_title">Квитанції про доставку!</string> + <string name="delivery_receipts_are_disabled">Квитанції про доставку вимкнено!</string> <string name="enable_receipts_all">Увімкнути</string> - <string name="snd_conn_event_ratchet_sync_ok">шифрування ok для %s</string> - <string name="v5_2_disappear_one_message_descr">Навіть коли вимкнений у розмові.</string> - <string name="privacy_message_draft">Чернетка повідомлення</string> - <string name="only_owners_can_enable_files_and_media">Тільки власники груп можуть вмикати файли та медіа.</string> - <string name="paste_the_link_you_received_to_connect_with_your_contact">Вставте отримане посилання для зв\'язку з вашим контактом…</string> - <string name="receipts_groups_override_disabled">Відправлення підтвердження доставки вимкнено для груп %d</string> - <string name="rcv_group_event_3_members_connected">%s, %s і %s підключено</string> - <string name="receipts_section_description_1">Їх можна перевизначити в налаштуваннях контактів і груп.</string> - <string name="system_restricted_background_warn"><![CDATA[Щоб увімкнути сповіщення, виберіть <b>Використання акумулятора програми</b> / <b>Необмежено</b> в налаштуваннях програми.]]></string> - <string name="system_restricted_background_in_call_warn"><![CDATA[Щоб здійснювати дзвінки у фоновому режимі, виберіть <b>Використання акумулятора програми</b> / <b>Без обмежень</b> у налаштуваннях програми.]]></string> - <string name="connect_use_new_incognito_profile">Використовуйте новий профіль інкогніто</string> - <string name="you_can_enable_delivery_receipts_later_alert">Ви можете увімкнути їх пізніше в налаштуваннях конфіденційності та безпеки програми.</string> - <string name="rcv_group_event_n_members_connected">%s, %s та %d інших учасників під\'єднано</string> - <string name="privacy_show_last_messages">Показати останні повідомлення</string> + <string name="snd_conn_event_ratchet_sync_ok">шифрування в порядку для %s</string> + <string name="v5_2_disappear_one_message_descr">Навіть коли вимкнено в розмові.</string> + <string name="privacy_message_draft">Чорновик повідомлення</string> + <string name="only_owners_can_enable_files_and_media">Тільки власники групи можуть включити файли та медіа.</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Вставте посилання, яке ви отримали, щоб підключитися до свого контакту…</string> + <string name="receipts_groups_override_disabled">Надсилання повідомлень про доставку вимкнено для %d груп</string> + <string name="rcv_group_event_3_members_connected">%s, %s і %s підключилися</string> + <string name="receipts_section_description_1">Їх можна замінити в налаштуваннях контактів і груп.</string> + <string name="system_restricted_background_warn"><![CDATA[Щоб увімкнути сповіщення, оберіть, будь ласка, <b>Використання батареї додатком</b> / <b>Без обмежень</b> у налаштуваннях додатка.]]></string> + <string name="system_restricted_background_in_call_warn"><![CDATA[Щоб здійснювати дзвінки в фоновому режимі, будь ласка, оберіть <b>Використання батареї додатком</b> / <b>Без обмежень</b> у налаштуваннях додатка.]]></string> + <string name="connect_use_new_incognito_profile">Використовувати новий інкогніто-профіль</string> + <string name="you_can_enable_delivery_receipts_later_alert">Ви зможете увімкнути їх пізніше через налаштування конфіденційності та безпеки додатка.</string> + <string name="rcv_group_event_n_members_connected">%s, %s і ще %d учасників підключилися</string> + <string name="privacy_show_last_messages">Показувати останні повідомлення</string> <string name="error_synchronizing_connection">Помилка синхронізації з\'єднання</string> - <string name="abort_switch_receiving_address_question">Скасувати зміну адреси\?</string> - <string name="abort_switch_receiving_address_desc">Зміна адреси буде скасована. Буде використано стару адресу отримання.</string> - <string name="sync_connection_force_question">Переузгодьте шифрування\?</string> - <string name="sync_connection_force_desc">Шифрування працює і нова угода про шифрування не потрібна. Це може призвести до помилок з\'єднання!</string> - <string name="group_members_can_send_files">Учасники групи можуть надсилати файли та медіа.</string> - <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">База даних буде зашифрована, а ключова фраза збережена в налаштуваннях.</string> + <string name="abort_switch_receiving_address_question">Скасувати зміну адреси для отримання?</string> + <string name="abort_switch_receiving_address_desc">Зміна адреси буде скасована. Буде використовуватися стара адреса для отримання.</string> + <string name="sync_connection_force_question">Повторно узгодити шифрування?</string> + <string name="sync_connection_force_desc">Шифрування працює і нова угода про шифрування не потрібна. Це може призвести до помилок підключення!</string> + <string name="group_members_can_send_files">Учасники можуть надсилати файли та медіа.</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">База даних буде зашифрована, і ключова фраза буде збережена в налаштуваннях.</string> <string name="expand_verb">Розгорнути</string> <string name="connect_plan_repeat_connection_request">Повторити запит на підключення?</string> - <string name="encryption_renegotiation_error">Помилка повторного узгодження шифрування</string> - <string name="rcv_direct_event_contact_deleted">видалений контакт</string> - <string name="connect_plan_you_are_already_connecting_to_vName">Ви вже з\'єднані з %1$s.</string> + <string name="encryption_renegotiation_error">Помилка переговорів щодо шифрування</string> + <string name="rcv_direct_event_contact_deleted">видалено контакт</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Ви вже підключаєтеся до <b>%1$s</b>.]]></string> <string name="rcv_group_event_open_chat">Відкрити</string> - <string name="v5_3_encrypt_local_files">Шифрування збережених файлів і носіїв</string> + <string name="v5_3_encrypt_local_files">Шифрування збережених файлів та медіа</string> <string name="error_alert_title">Помилка</string> - <string name="error_creating_member_contact">Помилка при створенні контакту користувача</string> - <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Ви вже приєдналися до групи за цим посиланням.</string> + <string name="error_creating_member_contact">Помилка при створенні контакту учасника</string> + <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Ви вже приєднуєтеся до групи за цим посиланням.</string> <string name="create_group_button">Створити групу</string> - <string name="socks_proxy_setting_limitations"><![CDATA[<b>Зверніть увагу</b>: ретранслятори повідомлень і файлів підключаються через проксі SOCKS. Дзвінки та надсилання попередніх переглядів посилань використовують пряме з’єднання.]]></string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Зверніть увагу</b>: ретрансляція повідомлень та файлів підключається через SOCKS-проксі. Дзвінки та відправлення переглядів посилань використовують пряме підключення.]]></string> <string name="create_another_profile_button">Створити профіль</string> - <string name="group_members_2">%s та %s</string> - <string name="connect_plan_join_your_group">Приєднатися до групи?</string> - <string name="connect_plan_you_are_already_joining_the_group_vName">Ви вже приєдналися до групи %1$s.</string> - <string name="encrypt_local_files">Шифрування локальних файлів</string> - <string name="connect_plan_this_is_your_own_one_time_link">Це ваше власне одноразове посилання!</string> - <string name="marked_deleted_items_description">%d повідомлень позначено як видалені</string> - <string name="v5_3_new_desktop_app">Новий десктопний застосунок!</string> + <string name="group_members_2">%s і %s</string> + <string name="connect_plan_join_your_group">Приєднатися до вашої групи?</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Ви вже приєднуєтеся до групи <b>%1$s</b>.]]></string> + <string name="encrypt_local_files">Шифрувати локальні файли</string> + <string name="connect_plan_this_is_your_own_one_time_link">Це ваш власний одноразовий посилання!</string> + <string name="marked_deleted_items_description">%d повідомлень відзначено як видалені</string> + <string name="v5_3_new_desktop_app">Новий додаток для комп\'ютера!</string> <string name="v5_3_new_interface_languages">6 нових мов інтерфейсу</string> <string name="connect_plan_group_already_exists">Група вже існує!</string> - <string name="v5_3_encrypt_local_files_descr">Застосунок шифрує нові локальні файли (крім відео).</string> - <string name="connect_plan_already_connecting">Вже під\'єднуємося!</string> - <string name="you_can_change_it_later">Випадкова фраза зберігається у налаштуваннях у вигляді відкритого тексту. + <string name="v5_3_encrypt_local_files_descr">Додаток шифрує нові локальні файли (крім відео).</string> + <string name="connect_plan_already_connecting">Вже підключено!</string> + <string name="you_can_change_it_later">Випадковий пароль зберігається в налаштуваннях у відкритому вигляді. \nВи можете змінити його пізніше.</string> - <string name="compose_send_direct_message_to_connect">Надішліть пряме повідомлення, щоб підключитися</string> - <string name="video_decoding_exception_desc">Відео не може бути декодовано. Будь ласка, спробуйте інше відео або зверніться до розробників.</string> - <string name="rcv_group_event_1_member_connected">%s підключено</string> - <string name="rcv_group_and_other_events">ще інших подій - %d</string> - <string name="v5_3_discover_join_groups">Знаходьте та приєднуйтесь до груп</string> - <string name="connect_plan_connect_via_link">Підключитися за посиланням?</string> - <string name="database_encryption_will_be_updated_in_settings">Пароль для шифрування бази даних буде оновлено і збережено в налаштуваннях.</string> - <string name="connect_plan_already_joining_the_group">Вже долучаємось до групи!</string> - <string name="group_members_n">члени %s, %s та %d</string> - <string name="moderated_items_description">%d повідомлень модерує %s</string> - <string name="remove_passphrase_from_settings">Видалити парольну фразу з налаштувань?</string> - <string name="unblock_member_button">Розблокувати</string> - <string name="use_random_passphrase">Використовуйте випадкову парольну фразу</string> + <string name="compose_send_direct_message_to_connect">Відправте приватне повідомлення для підключення</string> + <string name="video_decoding_exception_desc">Неможливо декодувати відео. Спробуйте інше відео або зв\'яжіться з розробниками.</string> + <string name="rcv_group_event_1_member_connected">%s підключився</string> + <string name="rcv_group_and_other_events">і ще %d подій</string> + <string name="v5_3_discover_join_groups">Відкривайте та приєднуйтесь до груп</string> + <string name="connect_plan_connect_via_link">Підключитися за допомогою посилання?</string> + <string name="database_encryption_will_be_updated_in_settings">Ключова фраза шифрування бази даних буде оновлена і збережена в налаштуваннях.</string> + <string name="connect_plan_already_joining_the_group">Вже приєднано до групи!</string> + <string name="group_members_n">%s, %s і %d учасників</string> + <string name="moderated_items_description">%1$d повідомлень модеровано %2$s</string> + <string name="remove_passphrase_from_settings">Вилучити ключову фразу із налаштувань?</string> + <string name="unblock_member_button">Розблокувати учасника</string> + <string name="use_random_passphrase">Використовувати випадковий пароль</string> <string name="connect_plan_connect_to_yourself">Підключитися до себе?</string> - <string name="save_passphrase_in_settings">Зберегти парольну фразу в налаштуваннях</string> + <string name="save_passphrase_in_settings">Зберегти ключову фразу в налаштуваннях</string> <string name="v5_3_simpler_incognito_mode">Спрощений режим інкогніто</string> - <string name="contact_tap_to_connect">Натисніть, щоб підключитися</string> - <string name="setup_database_passphrase">Ключова фраза для налаштування бази даних</string> - <string name="connect_plan_you_are_already_in_group_vName">Ви вже в групі %1$s.</string> - <string name="connect_plan_this_is_your_own_simplex_address">Це ваша власна SimpleX адреса!</string> - <string name="alert_text_encryption_renegotiation_failed">Повторне узгодження шифрування не вдалося.</string> + <string name="contact_tap_to_connect">Торкніться, щоб підключитися</string> + <string name="setup_database_passphrase">Налаштування паролю бази даних</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Ви вже є учасником групи <b>%1$s</b>.]]></string> + <string name="connect_plan_this_is_your_own_simplex_address">Це ваш власний адреса SimpleX!</string> + <string name="alert_text_encryption_renegotiation_failed">Не вдалося виконати переговори щодо шифрування.</string> <string name="correct_name_to">Виправити ім\'я на %s?</string> - <string name="delete_messages__question">Видалити %d повідомлень?</string> + <string name="delete_messages__question">Видалити %d повідомлення?</string> <string name="connect_with_contact_name_question">Підключитися до %1$s?</string> - <string name="remove_member_button">Видалити учасника</string> - <string name="set_database_passphrase">Встановити пароль до бази даних</string> + <string name="remove_member_button">Вилучити учасника</string> + <string name="set_database_passphrase">Встановити ключову фразу бази даних</string> <string name="block_member_confirmation">Заблокувати</string> <string name="unblock_member_question">Розблокувати учасника?</string> <string name="blocked_items_description">%d повідомлень заблоковано</string> <string name="block_member_button">Заблокувати учасника</string> - <string name="open_database_folder">Відкрийте теку з базою даних</string> + <string name="open_database_folder">Відкрити папку бази даних</string> <string name="connect_plan_repeat_join_request">Повторити запит на приєднання?</string> - <string name="button_remove_member_question">Видалити учасника?</string> - <string name="delete_and_notify_contact">Видалити та повідомити контакт</string> - <string name="v5_3_new_interface_languages_descr">Арабська, Болгарська, Фінська, Іврит, Тайська та Українська – завдяки користувачам і Weblate.</string> - <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">Ви вже підключаєтеся за цим одноразовим посиланням!</string> + <string name="button_remove_member_question">Вилучити учасника?</string> + <string name="delete_and_notify_contact">Видалити та сповістити контакт</string> + <string name="v5_3_new_interface_languages_descr">Арабська, болгарська, фінська, іврит, тайська та українська - завдяки користувачам і Weblate.</string> + <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">Ви вже підключаєтеся через це одноразове посилання!</string> <string name="connect_plan_open_group">Відкрити групу</string> - <string name="unblock_member_desc">Будуть показані повідомлення від %s!</string> - <string name="v5_3_new_desktop_app_descr">Створіть новий профіль у десктопному застосунку. 💻</string> - <string name="error_sending_message_contact_invitation">Помилка надсилання запрошення</string> - <string name="passphrase_will_be_saved_in_settings">Після зміни пароля або перезапуску програми він буде збережений у налаштуваннях у вигляді відкритого тексту.</string> - <string name="non_content_uri_alert_text">Ви надали невірний шлях до файлу. Повідомте про проблему розробникам програми.</string> + <string name="unblock_member_desc">Повідомлення від %s будуть відображені!</string> + <string name="v5_3_new_desktop_app_descr">Створіть новий профіль у додатку для комп\'ютера. 💻</string> + <string name="error_sending_message_contact_invitation">Помилка при відправці запрошення</string> + <string name="passphrase_will_be_saved_in_settings">Після зміни ключової фрази або перезапуску додатка ключова фраза буде збережена в налаштуваннях як звичайний текст.</string> + <string name="non_content_uri_alert_text">Ви поділилися неправильним шляхом до файлу. Повідомте про цю проблему розробникам додатку.</string> <string name="block_member_question">Заблокувати учасника?</string> - <string name="rcv_group_events_count">%d групових подій</string> - <string name="invalid_name">Неправильне ім\'я!</string> - <string name="v5_3_simpler_incognito_mode_descr">Увімкніть інкогніто при підключенні.</string> - <string name="connect_plan_this_is_your_link_for_group_vName">Це ваше посилання для групи %1$s!</string> + <string name="rcv_group_events_count">%d подій в групі</string> + <string name="invalid_name">Невірне ім\'я!</string> + <string name="v5_3_simpler_incognito_mode_descr">Перемикайте інкогніто під час підключення.</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Це ваше посилання для групи <b>%1$s</b>!]]></string> <string name="unblock_member_confirmation">Розблокувати</string> <string name="non_content_uri_alert_title">Неправильний шлях до файлу</string> - <string name="v5_3_discover_join_groups_descr">- підключитися до служби каталогів (БЕТА)! + <string name="v5_3_discover_join_groups_descr">- підключайтесь до служби каталогів (BETA)! \n- квитанції про доставку (до 20 учасників). -\n- швидше і стабільніше.</string> - <string name="settings_is_storing_in_clear_text">Пароль зберігається у налаштуваннях у вигляді відкритого тексту.</string> - <string name="connect_plan_you_have_already_requested_connection_via_this_address">Ви вже надсилали запит на підключення за цією адресою!</string> - <string name="member_contact_send_direct_message">надіслати пряме повідомлення</string> - <string name="terminal_always_visible">Показати консоль у новому вікні</string> - <string name="block_member_desc">Всі повідомлення від %s будуть приховані</string> - <string name="rcv_group_event_member_created_contact">підключений безпосередньо</string> +\n- швидше та надійніше.</string> + <string name="settings_is_storing_in_clear_text">Ключова фраза зберігається в налаштуваннях як звичайний текст.</string> + <string name="connect_plan_you_have_already_requested_connection_via_this_address">Ви вже подали запит на підключення за цією адресою!</string> + <string name="member_contact_send_direct_message">надіслати приватне повідомлення</string> + <string name="terminal_always_visible">Показувати консоль в новому вікні</string> + <string name="block_member_desc">Усі нові повідомлення від %s будуть приховані!</string> + <string name="rcv_group_event_member_created_contact">підключив(лась) безпосередньо</string> <string name="blocked_item_description">заблоковано</string> -</resources> \ No newline at end of file + <string name="v5_4_block_group_members">Блокувати учасників групи</string> + <string name="v5_4_incognito_groups_descr">Створіть групу, використовуючи випадковий профіль.</string> + <string name="connected_desktop">Підключений робочий стіл</string> + <string name="new_mobile_device">Новий мобільний пристрій</string> + <string name="multicast_connect_automatically">Підключати автоматично</string> + <string name="desktop_address">Адреса комп\'ютера</string> + <string name="only_one_device_can_work_at_the_same_time">Одночасно може працювати лише один пристрій</string> + <string name="v5_4_link_mobile_desktop">Підключіть мобільний і десктопний додатки! 🔗</string> + <string name="v5_4_link_mobile_desktop_descr">Через безпечний квантовостійкий протокол.</string> + <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Відкрийте <i>Використовувати з комп\'ютера</i> в мобільному додатку та відскануйте QR-код.]]></string> + <string name="v5_4_block_group_members_descr">Щоб приховати небажані повідомлення.</string> + <string name="desktop_incompatible_version">Несумісна версія</string> + <string name="new_desktop"><![CDATA[<i>(новий)</i>]]></string> + <string name="unlink_desktop_question">Відключити комп\'ютер?</string> + <string name="v5_4_better_groups">Кращі групи</string> + <string name="linked_desktop_options">Параметри пов\'язаних комп\'ютерів</string> + <string name="linked_desktops">Підключені комп\'ютери</string> + <string name="discover_on_network">Виявити через локальну мережу</string> + <string name="v5_4_incognito_groups">Інкогніто групи</string> + <string name="this_device">Цей пристрій</string> + <string name="remote_host_was_disconnected_toast"><![CDATA[Мобільний <b>%s</b> був відключений]]></string> + <string name="waiting_for_desktop">Очікування комп\'ютера…</string> + <string name="v5_4_better_groups_descr">Швидше приєднуйтесь та надійшовні повідомлення.</string> + <string name="linked_mobiles">Під’єднані мобільні</string> + <string name="desktop_device">Робочий стіл</string> + <string name="connected_to_desktop">Підключено до комп\'ютера</string> + <string name="this_device_name">Назва цього пристрою</string> + <string name="loading_remote_file_title">Завантаження файлу</string> + <string name="connecting_to_desktop">Підключення до комп\'ютера</string> + <string name="found_desktop">Знайдено робочий стіл</string> + <string name="desktop_devices">Комп\'ютери</string> + <string name="not_compatible">Не сумісно!</string> + <string name="link_a_mobile">Зв\'язати з мобільним</string> + <string name="settings_section_title_use_from_desktop">Використовувати з комп\'ютера</string> + <string name="connected_mobile">Підключений мобільний</string> + <string name="session_code">Код сеансу</string> + <string name="desktop_connection_terminated">Підключення завершено</string> + <string name="this_device_version"><![CDATA[<i>(цей пристрій v%s)</i>]]></string> + <string name="unlink_desktop">Відсунути відсилання</string> + <string name="this_device_name_shared_with_mobile">Назва пристрою буде надіслана підключеному мобільному клієнту.</string> + <string name="verify_code_on_mobile">Перевірте код на мобільному</string> + <string name="enter_this_device_name">Введіть назву цього пристрою…</string> + <string name="error">Помилка</string> + <string name="connect_to_desktop">Підключитися до комп\'ютера</string> + <string name="disconnect_remote_host">Відключити</string> + <string name="group_member_role_author">автор</string> + <string name="connected_to_mobile">Підключено до мобільного</string> + <string name="bad_desktop_address">Некоректна адреса комп\'ютера</string> + <string name="paste_desktop_address">Вставити адресу комп\'ютера</string> + <string name="verify_code_with_desktop">Перевірити код з комп\'ютером</string> + <string name="scan_qr_code_from_desktop">Сканувати QR-код з комп\'ютера</string> + <string name="devices">Пристрої</string> + <string name="multicast_discoverable_via_local_network">Виявлено через локальну мережу</string> + <string name="v5_4_more_things_descr">- за бажанням повідомляйте про видалених контактів. +\n- імена профілю з пробілами. +\n- та інше!</string> + <string name="scan_from_mobile">Сканувати з мобільного</string> + <string name="verify_connections">Перевірити підключення</string> + <string name="disconnect_desktop_question">Відключити робочий стіл?</string> + <string name="loading_remote_file_desc">Будь ласка, зачекайте, поки файл завантажується з підключеного мобільного</string> + <string name="desktop_app_version_is_incompatible">Версія комп\'ютерного додатка %s несумісна з цим додатком.</string> + <string name="verify_connection">Перевірити підключення</string> + <string name="chat_is_stopped_you_should_transfer_database">Чат зупинено. Якщо ви вже використовували цю базу даних на іншому пристрої, перенесіть її назад перед запуском чату.</string> + <string name="v5_5_private_notes_descr">З зашифрованими файлами та медіа.</string> + <string name="v5_5_message_delivery_descr">З меншим споживанням заряду акумулятора.</string> + <string name="agent_critical_error_title">Критична помилка</string> + <string name="v5_5_message_delivery">Покращено доставку повідомлень</string> + <string name="v5_5_join_group_conversation">Приєднатися до групових розмов</string> + <string name="v5_5_simpler_connect_ui">Вставте посилання, щоб приєднатися!</string> + <string name="note_folder_local_display_name">Приватні нотатки</string> + <string name="retry_verb">Повторити</string> + <string name="v5_5_private_notes">Приватні нотатки</string> + <string name="v5_5_simpler_connect_ui_descr">Рядок пошуку підтримує посилання-запрошення.</string> + <string name="search_or_paste_simplex_link">Пошук або вставка посилання SimpleX</string> + <string name="share_this_1_time_link">Поділитися цим одноразовим посиланням-запрошенням</string> + <string name="remote_host_error_timeout"><![CDATA[Перевищено максимальний час очікування з\'єднання з мобільним <b>%s</b>]]></string> + <string name="disconnect_remote_hosts">Відʼєднати мобільні</string> + <string name="add_contact_tab">Додати контакт</string> + <string name="new_chat">Новий чат</string> + <string name="or_show_this_qr_code">Або показати цей код</string> + <string name="invalid_qr_code">Помилка QR-коду</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">Відсканований код не є QR-кодом посилання SimpleX.</string> + <string name="recent_history">Видима історія повідомлень</string> + <string name="no_connected_mobile">Немає під\'єднаних мобільних</string> + <string name="blocked_by_admin_item_description">заблокований адміністратором</string> + <string name="blocked_by_admin_items_description">%d повідомлень заблоковано адміністратором</string> + <string name="error_creating_message">Помилка при створенні повідомлення</string> + <string name="failed_to_create_user_invalid_desc">Це ім\'я недійсне. Будь ласка, виберіть інше ім\'я.</string> + <string name="error_deleting_note_folder">Помилка при видаленні приватних нотаток</string> + <string name="loading_chats">Завантаження чатів…</string> + <string name="enable_camera_access">Надати доступ до камери</string> + <string name="camera_not_available">Камера недоступна</string> + <string name="tap_to_scan">Торкнутися, щоб відсканувати</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Додати контакт</b>: створити нове посилання-запрошення або підключитися за отриманим посиланням.]]></string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Створити групу</b>: створити нову групу.]]></string> + <string name="clear_note_folder_question">Очистити приватні нотатки?</string> + <string name="clear_note_folder_warning">Усі повідомлення будуть видалені - цю дію не можна скасувати!</string> + <string name="keep_invitation_link">Залишити</string> + <string name="keep_unused_invitation_question">Залишити невикористане запрошення?</string> + <string name="you_can_view_invitation_link_again">Ви можете знову побачити посилання-запрошення відкривши з\'єднання.</string> + <string name="create_chat_profile">Створити профіль чату</string> + <string name="creating_link">Створення посилання…</string> + <string name="developer_options_section">Налаштування для розробників</string> + <string name="show_slow_api_calls">Показувати повільні виклики API</string> + <string name="you_can_make_address_visible_via_settings">Ви можете зробити це видимим для ваших контактів у SimpleX через Налаштування.</string> + <string name="rcv_group_event_member_blocked">%s заблокований</string> + <string name="rcv_group_event_member_unblocked">%s розблокований</string> + <string name="snd_group_event_member_blocked">ви заблокували %s</string> + <string name="snd_group_event_member_unblocked">ви розблокували %s</string> + <string name="group_member_status_unknown_short">невідомо</string> + <string name="info_row_created_at">Створено</string> + <string name="member_info_member_blocked">заблокований</string> + <string name="member_blocked_by_admin">Заблокований адміністратором</string> + <string name="error_blocking_member_for_all">Помилка під час блокування учасника для всіх</string> + <string name="unblock_for_all">Розблокувати для всіх</string> + <string name="unblock_for_all_question">Розблокувати учасника для всіх?</string> + <string name="disable_sending_recent_history">Не надсилати історію новим користувачам.</string> + <string name="agent_critical_error_desc">Будь ласка, повідомте про це розробників: +\n%s +\n +\nРекомендується перезапустити додаток.</string> + <string name="enable_sending_recent_history">Надсилати до 100 останніх повідомлень новим користувачам.</string> + <string name="recent_history_is_sent_to_new_members">До 100 останніх повідомлень надсилаються новим учасникам.</string> + <string name="remote_host_was_disconnected_title">З\'єднання перервано</string> + <string name="share_text_created_at">Створено: %s</string> + <string name="show_internal_errors">Показати внутрішні помилки</string> + <string name="agent_internal_error_desc">Будь ласка, повідомте про це розробників: +\n%s</string> + <string name="remote_ctrl_disconnected_with_reason">Відʼєднаний з причини: %s</string> + <string name="remote_ctrl_error_bad_state">Помилка з\'єднання з комп\'ютером</string> + <string name="remote_ctrl_error_bad_invitation">Неправильний код запрошення в комп\'ютера</string> + <string name="remote_ctrl_error_busy">Компʼютер зайнятий</string> + <string name="remote_ctrl_error_inactive">Компʼютер неактивний</string> + <string name="remote_ctrl_error_disconnected">Компʼютер відʼєднаний</string> + <string name="remote_ctrl_error_timeout">Перевищено максимальний час очікування з\'єднання з комп\'ютером.</string> + <string name="remote_host_error_disconnected"><![CDATA[Мобільний <b>%s</b> було відʼєднано]]></string> + <string name="failed_to_create_user_invalid_title">Помилка імені!</string> + <string name="start_chat_question">Почати чат?</string> + <string name="remote_host_disconnected_from"><![CDATA[Від\'єднаний від мобільного <b>%s</b> з причини: %s]]></string> + <string name="remote_host_error_bad_version"><![CDATA[Версія програми на мобільному <b>%s</b> не підтримується. Будь ласка, встановіть однакову версію на обидва пристрої.]]></string> + <string name="agent_internal_error_title">Внутрішня помилка</string> + <string name="restart_chat_button">Оновити чат</string> + <string name="app_was_crashed">Помилка додатку</string> + <string name="refresh_qr_code">Оновити</string> + <string name="database_migration_in_progress">Триває міграція бази даних. +\nЦе може тривати кілька хвилин.</string> + <string name="tap_to_paste_link">Торкнутися, щоб вставити посилання</string> + <string name="the_text_you_pasted_is_not_a_link">Вставлений текст не є посиланням SimpleX.</string> + <string name="saved_message_title">Збережене повідомлення</string> + <string name="block_for_all">Заблокувати для всіх</string> + <string name="block_for_all_question">Заблокувати учасника для всіх?</string> + <string name="message_too_large">Повідомлення надто велике</string> + <string name="welcome_message_is_too_long">Привітання занадто довге</string> + <string name="recent_history_is_not_sent_to_new_members">Історія не надсилається новим учасникам.</string> + <string name="v5_5_new_interface_languages">Угорська та турецька мови інтерфейсу</string> + <string name="v5_5_join_group_conversation_descr">Нещодавня історія повідомлень та покращений бот каталогу.</string> + <string name="remote_ctrl_was_disconnected_title">З\'єднання перервано</string> + <string name="remote_ctrl_error_bad_version">Версія програми на комп\'ютері не підтримується. Будь ласка, встановіть однакову версію на обидва пристрої.</string> + <string name="random_port">Випадковий</string> + <string name="error_showing_message">помилка відображення повідомлення</string> + <string name="error_showing_content">помилка відображення вмісту</string> + <string name="waiting_for_mobile_to_connect">Очікується підключення мобільного:</string> + <string name="open_port_in_firewall_title">Відкрити порт у брандмауері</string> + <string name="open_port_in_firewall_desc">Щоб дозволити мобільному додатку підключатися до комп\'ютера, відкрийте цей порт у брандмауері, якщо він увімкнений</string> + <string name="remote_host_error_missing"><![CDATA[Мобільний <b>%s</b> відсутній]]></string> + <string name="remote_host_error_busy"><![CDATA[Мобільний <b>%s</b> зайнятий]]></string> + <string name="remote_host_error_bad_state"><![CDATA[Помилка з\'єднання з мобільним <b>%s</b>]]></string> + <string name="remote_host_error_inactive"><![CDATA[Мобільний <b>%s</b> неактивний]]></string> + <string name="la_app_passcode">Код для доступу в додаток</string> + <string name="group_member_status_unknown">невідомий статус</string> + <string name="possible_slow_function_desc">Виконання функції займає занадто багато часу: %1$d секунд: %2$s</string> + <string name="possible_slow_function_title">Уповільнене функціонування</string> + <string name="or_scan_qr_code">Або відсканувати QR-код</string> + <string name="profile_update_event_contact_name_changed">контакт %1$s змінено на %2$s</string> + <string name="profile_update_event_member_name_changed">учасник %1$s змінений на %2$s</string> + <string name="profile_update_event_removed_picture">вилучено зображення профілю</string> + <string name="profile_update_event_set_new_address">Установлено нову адресу контакту</string> + <string name="profile_update_event_set_new_picture">встановити новий аватар</string> + <string name="profile_update_event_updated_profile">оновлений профіль</string> + <string name="profile_update_event_removed_address">вилучено адресу контакту</string> + <string name="past_member_vName">Колишній учасник %1$s</string> + <string name="call_service_notification_end_call">Кінець дзвінка</string> + <string name="call_service_notification_video_call">Відеодзвінок</string> + <string name="call_service_notification_audio_call">Аудіодзвінок</string> + <string name="unable_to_open_browser_title">Помилка відкриття браузера</string> + <string name="unable_to_open_browser_desc">Для використання дзвінків потрібен браузер за замовчуванням. Будь ласка, налаштуйте браузер за замовчуванням в системі та надайте більше інформації розробникам.</string> + <string name="migrate_from_another_device">Перенести з іншого пристрою</string> + <string name="conn_event_enabled_pq">квантово-стійке шифрування e2e</string> + <string name="conn_event_disabled_pq">стандартне наскрізне шифрування</string> + <string name="e2ee_info_no_pq_short">Цей чат захищений наскрізним шифруванням.</string> + <string name="e2ee_info_pq_short">Цей чат захищений квантово-стійким наскрізним шифруванням.</string> + <string name="v5_6_safer_groups_descr">Адміністратори можуть заблокувати користувача для всіх.</string> + <string name="v5_6_app_data_migration">Міграція даних додатків</string> + <string name="v5_6_app_data_migration_descr">Перейдіть на інший пристрій за допомогою QR-коду.</string> + <string name="v5_6_picture_in_picture_calls">Дзвінки картинка в картинці</string> + <string name="v5_6_quantum_resistant_encryption">Квантово-стійке шифрування</string> + <string name="v5_6_safer_groups">Безпечніші групи</string> + <string name="invalid_file_link">Невірне посилання</string> + <string name="migrate_to_device_title">Мігруйте сюди</string> + <string name="migrate_to_device_migrating">Міграція</string> + <string name="or_paste_archive_link">Або вставте посилання на архів</string> + <string name="paste_archive_link">Вставити посилання на архів</string> + <string name="migrate_to_device_download_failed">Не вдалося завантажити</string> + <string name="migrate_to_device_downloading_details">Деталі посилання для завантаження</string> + <string name="migrate_to_device_database_init">Підготовка до завантаження</string> + <string name="migrate_to_device_repeat_download">Повторити завантаження</string> + <string name="migrate_to_device_bytes_downloaded">%s завантажено</string> + <string name="migrate_to_device_enter_passphrase">Введіть парольну фразу</string> + <string name="migrate_to_device_file_delete_or_link_invalid">Файл було видалено або посилання недійсне</string> + <string name="migrate_to_device_import_failed">Не вдалося імпортувати</string> + <string name="migrate_to_device_importing_archive">Імпорт архіву</string> + <string name="migrate_to_device_repeat_import">Повторний імпорт</string> + <string name="migrate_to_device_finalize_migration">Завершіть міграцію на іншому пристрої.</string> + <string name="migrate_to_device_apply_onion">Застосувати</string> + <string name="migrate_from_device_title">Міграція пристрою</string> + <string name="migrate_from_device_to_another_device">Перенести на інший пристрій</string> + <string name="migrate_from_device_error_exporting_archive">Помилка експорту бази даних чату</string> + <string name="migrate_from_device_error_saving_settings">Налаштування збереження помилок</string> + <string name="migrate_from_device_error_uploading_archive">Помилка завантаження архіву</string> + <string name="migrate_from_device_exported_file_doesnt_exist">Експортований файл не існує</string> + <string name="migrate_from_device_archive_and_upload">Архівування та завантаження</string> + <string name="migrate_from_device_archiving_database">Архівування бази даних</string> + <string name="migrate_from_device_confirm_upload">Підтвердити завантаження</string> + <string name="migrate_from_device_error_deleting_database">Помилка видалення бази даних</string> + <string name="migrate_from_device_chat_should_be_stopped">Для того, щоб продовжити, чат слід зупинити.</string> + <string name="migrate_from_device_stopping_chat">Зупинка чату</string> + <string name="migrate_from_device_bytes_uploaded">%s вивантажено</string> + <string name="migrate_from_device_uploading_archive">Завантаження архіву</string> + <string name="migrate_from_device_creating_archive_link">Створення архівного посилання</string> + <string name="migrate_from_device_delete_database_from_device">Видалити базу даних з цього пристрою</string> + <string name="migrate_from_device_finalize_migration">Завершити міграцію</string> + <string name="migrate_from_device_or_share_this_file_link">Або безпечно поділіться цим посиланням на файл</string> + <string name="migrate_from_device_repeat_upload">Повторити завантаження</string> + <string name="migrate_from_device_try_again">Ви можете спробувати ще раз.</string> + <string name="migrate_from_device_start_chat">Почати чат</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Попередження: запуск чату на декількох пристроях не підтримується і може призвести до збоїв у доставці повідомлень</string> + <string name="migrate_from_device_migration_complete">Міграція завершена</string> + <string name="migrate_from_device_verify_database_passphrase">Перевірте пароль до бази даних</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[Ви <b>не повинні</b> використовувати одну і ту ж базу даних на двох пристроях.]]></string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Warning</b>: архів буде видалено.]]></string> + <string name="migrate_from_device_check_connection_and_try_again">Перевірте підключення до Інтернету та спробуйте ще раз</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Переконайтеся, що ви пам\'ятаєте пароль до бази даних для її перенесення.</string> + <string name="migrate_from_device_error_verifying_passphrase">Помилка при перевірці парольної фрази:</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Усі ваші контакти, розмови та файли будуть надійно зашифровані та завантажені частинами на налаштовані XFTP-реле.</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Please note</b>: використання однієї і тієї ж бази даних на двох пристроях порушить розшифровку повідомлень з ваших з\'єднань, як захист безпеки.]]></string> + <string name="migrate_from_device_cancel_migration">Скасувати міграцію</string> + <string name="migrate_to_device_chat_migrated">Чат перемістився!</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Обирайте <i>Migrate from another device</i> на новому пристрої та відскануйте QR-код.]]></string> + <string name="migrate_to_device_confirm_network_settings">Підтвердьте налаштування мережі</string> + <string name="migrate_to_device_downloading_archive">Завантажити архів</string> + <string name="v5_6_quantum_resistant_encryption_descr">Увімкнути в прямих чатах (BETA)!</string> + <string name="migrate_to_device_error_downloading_archive">Помилка завантаження архіву</string> + <string name="e2ee_info_no_pq"><![CDATA[Повідомлення, файли та дзвінки захищені <b>наскрізним шифруванням</b> з ідеальною секретністю переадресації, відмовою та відновленням після злому.]]></string> + <string name="e2ee_info_pq"><![CDATA[Повідомлення, файли та дзвінки захищені <b>квантово-стійким шифруванням e2e</b> з ідеальною прямою секретністю, відмовою та відновленням після злому.]]></string> + <string name="auth_open_migration_to_another_device">Відкрийте екран міграції</string> + <string name="migrate_to_device_confirm_network_settings_footer">Переконайтеся, що налаштування мережі для цього пристрою є правильними.</string> + <string name="migrate_from_device_database_init">Підготовка до завантаження</string> + <string name="set_passphrase">Встановити парольну фразу</string> + <string name="migrate_from_device_upload_failed">Не вдалося завантажити</string> + <string name="v5_6_picture_in_picture_calls_descr">Використовуйте додаток під час розмови.</string> + <string name="migrate_from_device_verify_passphrase">Підтвердіть парольну фразу</string> + <string name="migrate_to_device_try_again">Ви можете спробувати ще раз.</string> + <string name="snd_error_quota">Перевищено ліміт - одержувач не отримав раніше надіслані повідомлення.</string> + <string name="snd_error_relay">Помилка сервера призначення: %1$s</string> + <string name="ci_status_other_error">Помилка: %1$s</string> + <string name="message_delivery_warning_title">Попередження про доставку повідомлення</string> + <string name="snd_error_expired">Проблеми з мережею - термін дії повідомлення закінчився після багатьох спроб надіслати його.</string> + <string name="snd_error_proxy_relay">Сервер переадресації: %1$s +\nПомилка сервера призначення: %2$s</string> + <string name="snd_error_proxy">Сервер переадресації: %1$s +\nПомилка: %2$s</string> + <string name="permissions_record_audio">Мікрофон</string> + <string name="v5_7_forward_descr">Джерело повідомлення залишається приватним.</string> + <string name="network_smp_proxy_mode_always">Завжди</string> + <string name="network_smp_proxy_mode_always_description">Завжди використовуйте приватну маршрутизацію.</string> + <string name="update_network_smp_proxy_mode_question">Режим маршрутизації повідомлень</string> + <string name="network_smp_proxy_mode_never_description">НЕ використовуйте приватну маршрутизацію.</string> + <string name="network_smp_proxy_fallback_prohibit">Ні</string> + <string name="network_smp_proxy_fallback_prohibit_description">НЕ надсилайте повідомлення напряму, навіть якщо ваш сервер або сервер призначення не підтримує приватну маршрутизацію.</string> + <string name="permissions_camera">Камера</string> + <string name="permissions_grant_in_settings">Надати в налаштуваннях</string> + <string name="audio_device_wired_headphones">Навушники</string> + <string name="network_type_cellular">Стільниковий</string> + <string name="feature_roles_owners">власники</string> + <string name="v5_7_network_descr">Більш надійне з\'єднання з мережею.</string> + <string name="v5_7_network">Керування мережею</string> + <string name="v5_7_forward">Пересилання та збереження повідомлень</string> + <string name="forward_message">Переадресувати повідомлення…</string> + <string name="network_smp_proxy_fallback_allow_downgrade">Дозволити зниження рейтингу</string> + <string name="theme_destination_app_theme">Тема програми</string> + <string name="chat_theme_apply_to_dark_mode">Темний режим</string> + <string name="download_file">Завантажити</string> + <string name="audio_device_earpiece">Навушник</string> + <string name="feature_enabled_for">Увімкнено для</string> + <string name="error_showing_desktop_notification">Повідомлення про помилку, зв\'яжіться з розробниками.</string> + <string name="files_and_media_not_allowed">Файли та медіафайли заборонені</string> + <string name="permissions_find_in_settings_and_grant">Знайдіть цей дозвіл у налаштуваннях Android і надайте його вручну.</string> + <string name="forward_chat_item">Переслати</string> + <string name="forwarded_chat_item_info_tab">Переслано</string> + <string name="forwarded_from_chat_item_info_title">Переслано з</string> + <string name="group_members_can_send_simplex_links">Учасники можуть надсилати посилання SimpleX.</string> + <string name="v5_7_call_sounds">Звуки вхідного дзвінка</string> + <string name="chat_theme_apply_to_light_mode">Світлий режим</string> + <string name="update_network_smp_proxy_fallback_question">Запасний варіант маршрутизації повідомлень</string> + <string name="settings_section_title_private_message_routing">МАРШРУТИЗАЦІЯ ПРИВАТНИХ ПОВІДОМЛЕНЬ</string> + <string name="forwarded_description">переслано</string> + <string name="network_type_other">Інше</string> + <string name="allow_to_send_simplex_links">Дозволити надсилати посилання SimpleX.</string> + <string name="prohibit_sending_simplex_links">Заборонити надсилання посилань SimpleX</string> + <string name="network_type_no_network_connection">Немає підключення до мережі</string> + <string name="network_smp_proxy_mode_never">Ніколи</string> + <string name="network_smp_proxy_mode_private_routing">Приватна маршрутизація</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="permissions_camera_and_record_audio">Камера та мікрофон</string> + <string name="permissions_grant">Надайте дозвіл(и) на здійснення дзвінків</string> + <string name="permissions_open_settings">Відкрити налаштування</string> + <string name="settings_section_title_files">ФАЙЛИ</string> + <string name="settings_section_title_profile_images">Зображення профілів</string> + <string name="settings_section_title_network_connection">Підключення до мережі</string> + <string name="feature_roles_admins">адміністратори</string> + <string name="feature_roles_all_members">всіх учасників</string> + <string name="v5_7_new_interface_languages">Литовський інтерфейс</string> + <string name="permissions_required">Надавати дозволи</string> + <string name="settings_section_title_chat_colors">Кольори чату</string> + <string name="settings_section_title_chat_theme">Тема чату</string> + <string name="settings_section_title_user_theme">Тема профілю</string> + <string name="theme_black">Темна</string> + <string name="color_primary_variant2">Додатковий акцент 2</string> + <string name="wallpaper_advanced_settings">Розширені налаштування</string> + <string name="chat_theme_apply_to_all_modes">Усі кольорові режими</string> + <string name="chat_theme_apply_to_mode">Застосувати до</string> + <string name="color_mode">Колірний режим</string> + <string name="color_mode_dark">Темна</string> + <string name="dark_mode_colors">Кольори темного режиму</string> + <string name="wallpaper_scale_fill">Заповнити</string> + <string name="wallpaper_scale_fit">Підходить</string> + <string name="wallpaper_preview_hello_alice">Доброго дня!</string> + <string name="wallpaper_preview_hello_bob">Доброго ранку!</string> + <string name="color_mode_light">Світлий</string> + <string name="theme_remove_image">Видалити зображення</string> + <string name="error_initializing_web_view">Помилка ініціалізації WebView. Оновіть систему до нової версії. Зверніться до розробників. +\nПомилка: %s</string> + <string name="v5_8_safe_files_descr">Підтвердити файли з невідомих серверів.</string> + <string name="v5_8_message_delivery">Покращена доставка повідомлень</string> + <string name="v5_8_persian_ui">Перський інтерфейс</string> + <string name="v5_8_private_routing">Маршрутизація приватних повідомлень 🚀</string> + <string name="protect_ip_address">Захист IP-адреси</string> + <string name="v5_8_private_routing_descr">Захистіть свою IP-адресу від ретрансляторів повідомлень, обраних вашими контактами. +\nУвімкніть у налаштуваннях *Мережа та сервери*.</string> + <string name="color_received_quote">Отримано відповідь</string> + <string name="saved_chat_item_info_tab">Збережено</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">Програма попросить підтвердити завантаження з невідомих файлових серверів (крім .onion або коли ввімкнено SOCKS-проксі).</string> + <string name="network_smp_proxy_fallback_allow_protected_description">Надсилайте повідомлення напряму, якщо IP-адреса захищена, а ваш сервер або сервер призначення не підтримує приватну маршрутизацію.</string> + <string name="network_smp_proxy_fallback_allow_description">Надсилайте повідомлення напряму, якщо ваш сервер або сервер призначення не підтримує приватну маршрутизацію.</string> + <string name="chat_theme_set_default_theme">Встановлення теми за замовчуванням</string> + <string name="color_sent_quote">Надіслано відповідь</string> + <string name="chat_list_always_visible">Показати список чату в новому вікні</string> + <string name="network_smp_proxy_mode_unknown_description">Використовуйте приватну маршрутизацію з невідомими серверами.</string> + <string name="network_smp_proxy_mode_unprotected_description">Використовуйте приватну маршрутизацію з невідомими серверами, якщо IP-адреса не захищена.</string> + <string name="color_wallpaper_background">Фон шпалер</string> + <string name="color_wallpaper_tint">Акцент на шпалерах</string> + <string name="wallpaper_scale_repeat">Повторити</string> + <string name="wallpaper_scale">Масштаб</string> + <string name="v5_8_chat_themes_descr">Нехай ваші чати виглядають інакше!</string> + <string name="v5_8_chat_themes">Нові теми чату</string> + <string name="v5_8_safe_files">Безпечне отримання файлів</string> + <string name="v5_8_message_delivery_descr">З меншим споживанням заряду акумулятора.</string> + <string name="snd_error_auth">Неправильний ключ або невідоме з\'єднання - швидше за все, це з\'єднання видалено.</string> + <string name="srv_error_host">Адреса сервера несумісна з налаштуваннями мережі.</string> + <string name="voice_messages_not_allowed">Голосові повідомлення заборонені</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="audio_device_speaker">Спікер</string> + <string name="chat_theme_reset_to_app_theme">Повернутися до теми програми</string> + <string name="chat_theme_reset_to_user_theme">Повернутися до теми користувача</string> + <string name="simplex_links">Посилання SimpleX</string> + <string name="v5_7_shape_profile_images_descr">Квадрат, коло або щось середнє між ними.</string> + <string name="v5_7_quantum_resistant_encryption_descr">Буде ввімкнено в прямих чатах!</string> + <string name="saved_description">збережено</string> + <string name="saved_from_description">збережено з %s</string> + <string name="network_type_ethernet">Дротова мережа Ethernet</string> + <string name="file_not_approved_title">Невідомі сервери!</string> + <string name="file_not_approved_descr">Без Tor або VPN ваша IP-адреса буде видимою для цих XFTP-ретрансляторів: +\n%1$s.</string> + <string name="recipients_can_not_see_who_message_from">Одержувач(и) не бачить, від кого це повідомлення.</string> + <string name="saved_from_chat_item_info_title">Збережено з</string> + <string name="srv_error_version">Серверна версія несумісна з мережевими налаштуваннями.</string> + <string name="simplex_links_not_allowed">Посилання SimpleX заборонені</string> + <string name="private_routing_show_message_status">Показати статус повідомлення</string> + <string name="private_routing_explanation">Щоб захистити вашу IP-адресу, приватна маршрутизація використовує ваші SMP-сервери для доставки повідомлень.</string> + <string name="network_smp_proxy_mode_unknown">Невідомі сервери</string> + <string name="network_smp_proxy_mode_unprotected">Незахищений</string> + <string name="network_smp_proxy_fallback_allow_protected">Коли IP приховано</string> + <string name="network_smp_proxy_fallback_allow">Так</string> + <string name="network_option_rcv_concurrency">Отримання паралелізму</string> + <string name="simplex_links_are_prohibited_in_group">Посилання SimpleX заборонені.</string> + <string name="v5_7_shape_profile_images">Форма зображень профілю</string> + <string name="v5_7_call_sounds_descr">При підключенні аудіо та відеодзвінків.</string> + <string name="reset_single_color">Скинути колір</string> + <string name="color_mode_system">Система</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Без Tor або VPN ваша IP-адреса буде видимою для файлових серверів.</string> + <string name="message_queue_info_none">немає</string> + <string name="info_row_debug_delivery">Доставка налагодження</string> + <string name="message_queue_info">Інформація про чергу повідомлень</string> + <string name="message_queue_info_server_info">інформація про чергу на сервері: %1$s +\n +\nостаннє отримане повідомлення: %2$s</string> + <string name="proxy_destination_error_broker_host">Адреса сервера призначення %1$s несумісна з налаштуваннями сервера переадресації %2$s.</string> + <string name="file_error_no_file">Файл не знайдено — ймовірно, файл був видалений або скасований.</string> + <string name="scan_paste_link">Сканувати / Вставити посилання</string> + <string name="xftp_servers_configured">Налаштовані XFTP сервери</string> + <string name="app_check_for_updates_beta">Бета</string> + <string name="info_row_file_status">Статус файлу</string> + <string name="servers_info_messages_sent">Надіслано повідомлень</string> + <string name="servers_info_statistics_section_header">Статистика</string> + <string name="servers_info_previously_connected_servers_section_header">Попередньо підключені сервери</string> + <string name="file_error_auth">Помилковий ключ або невідома адреса чанка файлу - найбільш імовірно, що файл було видалено.</string> + <string name="error_parsing_uri_title">Недійсне посилання</string> + <string name="error_parsing_uri_desc">Будь ласка, перевірте, чи правильне посилання SimpleX.</string> + <string name="network_error_broker_host_desc">Адреса сервера несумісна з налаштуваннями мережі: %1$s.</string> + <string name="smp_proxy_error_connecting">Помилка підключення до сервера переадресації %1$s. Будь ласка, спробуйте пізніше.</string> + <string name="smp_proxy_error_broker_host">Адреса сервера переадресації несумісна з налаштуваннями мережі: %1$s.</string> + <string name="smp_proxy_error_broker_version">Версія сервера переадресації несумісна з налаштуваннями мережі: %1$s.</string> + <string name="private_routing_error">Помилка приватного маршрутизації</string> + <string name="proxy_destination_error_broker_version">Версія сервера призначення %1$s несумісна з сервером переадресації %2$s.</string> + <string name="please_try_later">Будь ласка, спробуйте пізніше.</string> + <string name="file_error_relay">Помилка сервера файлів: %1$s</string> + <string name="member_inactive_title">Учасник неактивний</string> + <string name="message_forwarded_title">Повідомлення переслано</string> + <string name="message_forwarded_desc">Поки що немає прямого з\'єднання, повідомлення пересилається адміністратором.</string> + <string name="selected_chat_items_selected_n">Вибрано %d</string> + <string name="compose_message_placeholder">Повідомлення</string> + <string name="file_error">Помилка файлу</string> + <string name="temporary_file_error">Тимчасова помилка файлу</string> + <string name="delete_contact_cannot_undo_warning">Контакт буде видалено - це неможливо скасувати!</string> + <string name="only_delete_conversation">Видалити лише розмову</string> + <string name="deleted_chats">Архівовані контакти</string> + <string name="app_check_for_updates_button_remind_later">Нагадати пізніше</string> + <string name="app_check_for_updates_stable">Стабільна</string> + <string name="v6_0_new_chat_experience">Новий досвід чату 🎉</string> + <string name="server_address">Адреса сервера</string> + <string name="confirm_delete_contact_question">Підтвердити видалення контакту?</string> + <string name="info_view_connect_button">підключитися</string> + <string name="proxy_destination_error_failed_to_connect">Сервер переадресації %1$s не зміг з\'єднатися з цільовим сервером %2$s. Будь ласка, спробуйте пізніше.</string> + <string name="member_inactive_desc">Повідомлення може бути доставлено пізніше, якщо учасник стане активним.</string> + <string name="selected_chat_items_nothing_selected">Нічого не вибрано</string> + <string name="open_server_settings_button">Відкрити налаштування сервера</string> + <string name="network_error_broker_version_desc">Версія сервера несумісна з вашим додатком: %1$s.</string> + <string name="moderate_messages_will_be_marked_warning">Повідомлення будуть позначені як модеровані для всіх учасників.</string> + <string name="v6_0_upgrade_app">Оновлювати додаток автоматично</string> + <string name="cannot_share_message_alert_title">Не вдалося надіслати повідомлення</string> + <string name="cannot_share_message_alert_text">Вибрані налаштування чату забороняють це повідомлення.</string> + <string name="share_text_file_status">Статус файлу: %s</string> + <string name="share_text_message_status">Статус повідомлення: %s</string> + <string name="new_message">Нове повідомлення</string> + <string name="create_address_button">Створити</string> + <string name="invite_friends_short">Запросити</string> + <string name="one_hand_ui_card_title">Перемикнути список чатів:</string> + <string name="one_hand_ui_change_instruction">Ви можете змінити це в налаштуваннях зовнішнього вигляду.</string> + <string name="info_row_message_status">Статус повідомлення</string> + <string name="v6_0_your_contacts_descr">Архівувати контакти, щоб поговорити пізніше.</string> + <string name="v6_0_chat_list_media">Відтворити зі списку чатів.</string> + <string name="v6_0_new_media_options">Нові опції медіа</string> + <string name="v6_0_privacy_blur">Розмиття для кращої конфіденційності.</string> + <string name="v6_0_increase_font_size">Збільшити розмір шрифту.</string> + <string name="v6_0_private_routing_descr">Він захищає вашу IP-адресу та з\'єднання.</string> + <string name="app_check_for_updates_notice_title">Перевірити, чи є оновлення</string> + <string name="v6_0_upgrade_app_descr">Завантажуйте нові версії з GitHub.</string> + <string name="allow_calls_question">Дозволити дзвінки?</string> + <string name="cant_call_contact_deleted_alert_text">Контакт видалено.</string> + <string name="network_option_tcp_connection">TCP з\'єднання</string> + <string name="appearance_zoom">Масштабування</string> + <string name="appearance_font_size">Розмір шрифту</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">Будь ласка, попросіть вашого контакту увімкнути дзвінки.</string> + <string name="network_options_save_and_reconnect">Зберегти і перепідключитися</string> + <string name="v6_0_delete_many_messages_descr">Видалити до 20 повідомлень за один раз.</string> + <string name="v6_0_reachable_chat_toolbar">Доступна панель чату</string> + <string name="v6_0_reachable_chat_toolbar_descr">Користуватися застосунком однією рукою.</string> + <string name="v6_0_connect_faster_descr">З\'єднуйтеся з друзями швидше.</string> + <string name="v6_0_connection_servers_status">Керуйте своєю мережею</string> + <string name="servers_info_downloaded">Завантажено</string> + <string name="servers_info_reset_stats_alert_error_title">Помилка скидання статистики</string> + <string name="servers_info_reconnect_servers_title">Перепідключити сервери?</string> + <string name="servers_info_detailed_statistics_sent_messages_header">Надіслані повідомлення</string> + <string name="servers_info_reset_stats_alert_message">Статистика серверів буде скинута — це не можна буде відмінити!</string> + <string name="v6_0_connection_servers_status_descr">Статус з\'єднання та серверів.</string> + <string name="servers_info_detailed_statistics">Докладна статистика</string> + <string name="servers_info_detailed_statistics_receive_errors">Отримати помилки</string> + <string name="sent_directly">Надіслано безпосередньо</string> + <string name="servers_info_detailed_statistics_sent_messages_total">Надіслано загалом</string> + <string name="sent_via_proxy">Надіслано через проксі</string> + <string name="smp_server">Сервер SMP</string> + <string name="servers_info_starting_from">Починаючи з %s.</string> + <string name="servers_info_detailed_statistics_received_messages_header">Отримані повідомлення</string> + <string name="servers_info_detailed_statistics_received_total">Отримано загалом</string> + <string name="privacy_media_blur_radius">Розмити медіа</string> + <string name="privacy_media_blur_radius_medium">Середній</string> + <string name="privacy_media_blur_radius_off">Вимкнено</string> + <string name="privacy_media_blur_radius_soft">Слабке</string> + <string name="privacy_media_blur_radius_strong">Сильна</string> + <string name="member_info_member_disabled">вимкнено</string> + <string name="member_info_member_inactive">неактивний</string> + <string name="servers_info">Інформація про сервери</string> + <string name="servers_info_files_tab">Файли</string> + <string name="servers_info_missing">Ніякої інформації, спробуйте перезавантажити</string> + <string name="servers_info_subscriptions_connections_subscribed">Активні з\'єднання</string> + <string name="servers_info_sessions_connected">Підключено</string> + <string name="servers_info_connected_servers_section_header">Підключені сервери</string> + <string name="servers_info_sessions_connecting">Підключення</string> + <string name="servers_info_subscriptions_total">Всього</string> + <string name="servers_info_transport_sessions_section_header">Сесії передачі даних</string> + <string name="servers_info_proxied_servers_section_footer">Ви не підключені до цих серверів. Для доставки повідомлень до них використовується приватна маршрутизація.</string> + <string name="current_user">Поточний профіль</string> + <string name="servers_info_details">Деталі</string> + <string name="servers_info_sessions_errors">Помилки</string> + <string name="servers_info_subscriptions_section_header">Отримання повідомлень</string> + <string name="servers_info_messages_received">Отримані повідомлення</string> + <string name="servers_info_subscriptions_connections_pending">В очікуванні</string> + <string name="servers_info_proxied_servers_section_header">Проксіровані сервери</string> + <string name="servers_info_target">Показувати інформацію для</string> + <string name="servers_info_private_data_disclaimer">Починаючи з %s.\nУсі дані зберігаються приватно на вашому пристрої.</string> + <string name="servers_info_reconnect_server_message">Перепідключити сервер для примусової доставки повідомлень. Це використовує додатковий трафік.</string> + <string name="servers_info_reset_stats">Скинути всю статистику</string> + <string name="servers_info_reset_stats_alert_title">Скинути всю статистику?</string> + <string name="servers_info_modal_error_title">Помилка</string> + <string name="servers_info_reconnect_server_error">Помилка повторного підключення до сервера</string> + <string name="servers_info_reconnect_servers_error">Помилка повторного підключення до серверів</string> + <string name="servers_info_reconnect_servers_message">Перепідключити всі підключені сервери для примусової доставки повідомлень. Це використовує додатковий трафік.</string> + <string name="servers_info_reconnect_all_servers_button">Перепідключити всі сервери</string> + <string name="servers_info_reconnect_server_title">Перепідключити сервер?</string> + <string name="acknowledgement_errors">Помилки підтвердження</string> + <string name="duplicates_label">дублікати</string> + <string name="other_errors">інші помилки</string> + <string name="acknowledged">Підтверджено</string> + <string name="connections">Підключення</string> + <string name="decryption_errors">помилки розшифрування</string> + <string name="deleted">Видалено</string> + <string name="subscription_errors">Помилки підписки</string> + <string name="deletion_errors">Помилки видалення</string> + <string name="size">Розмір</string> + <string name="subscribed">Підписано</string> + <string name="subscription_results_ignored">Підписки проігноровані</string> + <string name="uploaded_files">Завантажені файли</string> + <string name="download_errors">Помилки завантаження</string> + <string name="downloaded_files">Завантажені файли</string> + <string name="upload_errors">Помилки завантаження</string> + <string name="chunks_deleted">Частини видалені</string> + <string name="chunks_downloaded">Частин завантажено</string> + <string name="chunks_uploaded">Частин вивантажено</string> + <string name="remote_ctrl_connection_stopped_identity_desc">Ця посилання було використано на іншому мобільному пристрої, створіть нове посилання на комп\'ютері.</string> + <string name="copy_error">Помилка копіювання</string> + <string name="remote_ctrl_connection_stopped_desc">Будь ласка, перевірте, що мобільний пристрій і комп\'ютер підключені до однієї локальної мережі, і що брандмауер комп\'ютера дозволяє з\'єднання. +\nБудь ласка, повідомте про будь-які інші проблеми розробникам.</string> + <string name="toolbar_settings">Налаштування</string> + <string name="info_view_video_button">відеодзвінок</string> + <string name="info_view_message_button">повідомлення</string> + <string name="info_view_call_button">дзвінок</string> + <string name="keep_conversation">Зберегти розмову</string> + <string name="info_view_open_button">відкрити</string> + <string name="info_view_search_button">пошук</string> + <string name="you_can_still_view_conversation_with_contact">Ви все ще можете переглядати розмову з %1$s у списку чатів.</string> + <string name="contact_deleted">Контакт видалено!</string> + <string name="conversation_deleted">Розмову видалено!</string> + <string name="delete_without_notification">Видалити без сповіщення</string> + <string name="you_can_still_send_messages_to_contact">Ви можете надсилати повідомлення %1$s з архівованих контактів.</string> + <string name="smp_servers_configured">Налаштовані SMP сервери</string> + <string name="no_filtered_contacts">Ніяких відфільтрованих контактів</string> + <string name="smp_servers_other">Інші SMP сервери</string> + <string name="contact_list_header_title">Ваші контакти</string> + <string name="xftp_servers_other">Інші XFTP сервери</string> + <string name="subscription_percentage">Показати відсоток</string> + <string name="app_check_for_updates">Перевірити оновлення</string> + <string name="app_check_for_updates_disabled">Вимкнено</string> + <string name="app_check_for_updates_download_started">Завантаження оновлення додатку, не закривайте додаток</string> + <string name="app_check_for_updates_button_download">Завантажити %s (%s)</string> + <string name="app_check_for_updates_button_open">Відкрити розташування файлу</string> + <string name="app_check_for_updates_button_skip">Пропустити цю версію</string> + <string name="one_hand_ui">Доступні панелі додатка</string> + <string name="cant_call_contact_alert_title">Не можна зателефонувати контакту</string> + <string name="cant_call_contact_connecting_wait_alert_text">Підключення до контакту, будь ласка, зачекайте або перевірте пізніше!</string> + <string name="calls_prohibited_alert_title">Дзвінки заборонені!</string> + <string name="you_need_to_allow_calls">Вам необхідно дозволити контакту викликати вас, щоб ви могли самі їм дзвонити.</string> + <string name="cant_call_member_send_message_alert_text">Надіслати повідомлення, щоб увімкнути дзвінки.</string> + <string name="cant_call_member_alert_title">Не можна зателефонувати учаснику групи</string> + <string name="cant_send_message_to_member_alert_title">Не можна надіслати повідомлення учаснику групи</string> + <string name="attempts_label">спроби</string> + <string name="xftp_server">XFTP сервер</string> + <string name="reconnect">Перепідключитися</string> + <string name="created">Створено</string> + <string name="expired_label">закінчився</string> + <string name="secured">Захищений</string> + <string name="other_label">інший</string> + <string name="proxied">Проксірований</string> + <string name="send_errors">Надіслати помилки</string> + <string name="completed">Завершено</string> + <string name="all_users">Усі профілі</string> + <string name="servers_info_reset_stats_alert_confirm">Скинути</string> + <string name="servers_info_uploaded">Вивантажено</string> + <string name="delete_members_messages__question">Видалити %d повідомлень учасників?</string> + <string name="delete_messages_mark_deleted_warning">Повідомлення будуть позначені для видалення. Одержувач(і) зможуть розкрити ці повідомлення.</string> + <string name="select_verb">Вибрати</string> + <string name="moderate_messages_will_be_deleted_warning">Повідомлення будуть видалені для всіх учасників.</string> + <string name="paste_link">Вставити посилання</string> + <string name="app_check_for_updates_notice_disable">Вимкнути</string> + <string name="app_check_for_updates_notice_desc">Щоб отримувати повідомлення про нові випуски, увімкніть періодичну перевірку стабільної або бета-версії.</string> + <string name="chat_database_exported_continue">Продовжити</string> + <string name="chat_database_exported_title">База даних чату експортована</string> + <string name="media_and_file_servers">Медіа та файлові сервери</string> + <string name="message_servers">Сервери повідомлень</string> + <string name="network_socks_proxy">SOCKS проксі</string> + <string name="chat_database_exported_not_all_files">Деякі файли не були експортовані</string> + <string name="chat_database_exported_migrate">Ви можете переїхати експортовану базу даних.</string> + <string name="chat_database_exported_save">Ви можете зберегти експортований архів.</string> + <string name="action_button_add_members">Запросити</string> + <string name="app_check_for_updates_download_completed_title">Оновлення додатку завантажено</string> + <string name="app_check_for_updates_installed_successfully_title">Встановлено успішно</string> + <string name="app_check_for_updates_button_install">Встановити оновлення</string> + <string name="app_check_for_updates_installed_successfully_desc">Будь ласка, перезапустіть додаток.</string> + <string name="reset_all_hints">Скинути всі підказки</string> + <string name="app_check_for_updates_update_available">Доступно оновлення: %s</string> + <string name="app_check_for_updates_canceled">Завантаження оновлення скасовано</string> + <string name="settings_section_title_chat_database">БАЗА ДАНИХ ЧАТУ</string> + <string name="select_chat_profile">Вибрати профіль чату</string> + <string name="switching_profile_error_title">Помилка при зміні профілю</string> + <string name="delete_messages_cannot_be_undone_warning">Повідомлення будуть видалені — це не можна скасувати!</string> + <string name="migrate_from_device_remove_archive_question">Видалити архів?</string> + <string name="new_chat_share_profile">Поділитися профілем</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">Завантажений архів бази даних буде остаточно видалено з серверів.</string> + <string name="switching_profile_error_message">Підключення було перенесено до %s, але під час перенаправлення на профіль сталася непередбачена помилка.</string> + <string name="system_mode_toast">Режим системи</string> + <string name="network_proxy_auth_mode_no_auth">Не використовуйте облікові дані з проксі.</string> + <string name="network_proxy_auth">Аутентифікація проксі</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">Використовуйте різні облікові дані проксі для кожного з\'єднання.</string> + <string name="network_proxy_random_credentials">Використовувати випадкові облікові дані</string> + <string name="network_proxy_auth_mode_username_password">Ваші облікові дані можуть бути надіслані в незашифрованому вигляді.</string> + <string name="network_proxy_incorrect_config_title">Помилка під час збереження проксі</string> + <string name="network_proxy_incorrect_config_desc">Переконайтеся, що конфігурація проксі правильна.</string> + <string name="network_proxy_password">Пароль</string> + <string name="network_proxy_username">Ім\'я користувача</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">Використовуйте різні облікові дані проксі для кожного профілю.</string> + <string name="n_other_file_errors">%1$d інша(і) помилка(и) файлу.</string> + <string name="forward_files_failed_to_receive_desc">%1$d файл(и) не вдалося завантажити.</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s повідомлень не було переслано.</string> + <string name="forward_files_not_accepted_receive_files">Завантажити</string> + <string name="forward_alert_title_messages_to_forward">Переслати %1$s повідомлень?</string> + <string name="forward_multiple">Пересилаю повідомлення…</string> + <string name="forward_alert_forward_messages_without_files">Переслати повідомлення без файлів?</string> + <string name="forward_alert_title_nothing_to_forward">Немає нічого для пересилання!</string> + <string name="compose_save_messages_n">Зберігаю %1$s повідомлень.</string> + <string name="n_file_errors">%1$d помилка(и) файлу: +\n%2$s</string> + <string name="forward_files_not_accepted_desc">%1$d файл(и) не були завантажені.</string> + <string name="forward_files_in_progress_desc">%1$d файл(и) ще завантажуються.</string> + <string name="forward_files_missing_desc">%1$d файл(и) були видалені.</string> + <string name="compose_forward_messages_n">Пересилаю %1$s повідомлень</string> + <string name="forward_files_messages_deleted_after_selection_desc">Повідомлення були видалені після того, як ви їх вибрали.</string> + <string name="error_forwarding_messages">Помилка при пересиланні повідомлень</string> + <string name="icon_descr_sound_muted">Звук вимкнено</string> + <string name="error_initializing_web_view_wrong_arch">Помилка ініціалізації WebView. Переконайтеся, що WebView встановлено, і його підтримувана архітектура — arm64. \nПомилка: %s</string> + <string name="settings_message_shape_tail">Хвіст</string> + <string name="settings_message_shape_corner">Кут</string> + <string name="settings_section_title_message_shape">Форма повідомлення</string> + <string name="network_session_mode_session">Сесія додатку</string> + <string name="network_session_mode_session_description">Нові облікові дані SOCKS будуть використовуватись щоразу, коли ви запускаєте додаток.</string> + <string name="network_session_mode_server_description">Нові облікові дані SOCKS будуть використовуватись для кожного сервера.</string> + <string name="network_session_mode_server">Сервер</string> + <string name="call_desktop_permission_denied_chrome">Натисніть кнопку інформації поруч із полем адреси, щоб дозволити використання мікрофона.</string> + <string name="call_desktop_permission_denied_safari">Відкрийте Налаштування Safari / Сайти / Мікрофон, а потім виберіть \"Дозволити для localhost\".</string> + <string name="call_desktop_permission_denied_title">Щоб здійснювати дзвінки, дозволіть використовувати ваш мікрофон. Завершіть дзвінок і спробуйте зателефонувати знову.</string> + <string name="v6_1_better_calls">Кращі дзвінки</string> + <string name="v6_1_better_security">Краща безпека ✅</string> + <string name="v6_1_customizable_message_descr">Налаштовувана форма повідомлень.</string> + <string name="v6_1_better_security_descr">Протоколи SimpleX перевірені компанією Trail of Bits.</string> + <string name="v6_1_better_calls_descr">Переключити аудіо та відео під час дзвінка.</string> + <string name="v6_1_message_dates_descr">Кращі дати повідомлень.</string> + <string name="v6_1_better_user_experience">Кращий користувацький досвід</string> + <string name="v6_1_delete_many_messages_descr">Видалити або модерувати до 200 повідомлень.</string> + <string name="v6_1_forward_many_messages_descr">Переслати до 20 повідомлень одночасно.</string> + <string name="v6_1_switch_chat_profile_descr">Переключити профіль чату для одноразових запрошень.</string> + <string name="error_accepting_operator_conditions">Помилка прийняття умов</string> + <string name="failed_to_save_servers">Сервери збереження помилок</string> + <string name="for_chat_profile">Для профілю чату %s:</string> + <string name="no_media_servers_configured">Ніяких медіа та файлових серверів.</string> + <string name="no_message_servers_configured">Немає серверів повідомлень.</string> + <string name="no_message_servers_configured_for_private_routing">Немає серверів для маршрутизації приватних повідомлень.</string> + <string name="connection_error_quota">Недоставлені повідомлення</string> + <string name="message_deleted_or_not_received_error_title">Немає повідомлення</string> + <string name="message_deleted_or_not_received_error_desc">Це повідомлення було видалено або ще не отримано.</string> + <string name="share_1_time_link_with_a_friend">Поділіться одноразовим посиланням з другом</string> + <string name="share_address_publicly">Поділіться адресою публічно</string> + <string name="you_can_set_connection_name_to_remember">Ви можете задати ім\'я з\'єднання, щоб запам\'ятати, з ким ви поділилися посиланням.</string> + <string name="connection_security">Безпека з\'єднання</string> + <string name="to_protect_against_your_link_replaced_compare_codes">Щоб захиститися від заміни вашого посилання, ви можете порівняти коди безпеки контактів.</string> + <string name="for_social_media">Для соціальних мереж</string> + <string name="or_to_share_privately">Або поділитися приватно</string> + <string name="onboarding_choose_server_operators">Оператори серверів</string> + <string name="onboarding_network_operators">Мережеві оператори</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">Умови будуть прийняті для ввімкнених операторів через 30 днів.</string> + <string name="onboarding_network_operators_app_will_use_for_routing">Наприклад, якщо ваш контакт отримує повідомлення через сервер SimpleX Chat, ваш додаток доставлятиме їх через сервер Flux.</string> + <string name="onboarding_select_network_operators_to_use">Виберіть мережевих операторів для використання.</string> + <string name="onboarding_network_operators_configure_via_settings">Ви можете налаштувати сервери за допомогою налаштувань.</string> + <string name="onboarding_network_operators_review_later">Перегляньте пізніше</string> + <string name="onboarding_network_operators_update">Оновлення</string> + <string name="operator_conditions_accepted">Прийняті умови</string> + <string name="operator_conditions_accepted_for_enabled_operators_on">Умови будуть автоматично прийняті для увімкнених операторів: %s.</string> + <string name="operator_info_title">Оператор мережі</string> + <string name="operator_servers_title">%s сервери</string> + <string name="operator_website">Вебсайт</string> + <string name="your_servers">Ваші сервери</string> + <string name="use_servers_of_operator_x">Використовувати %s</string> + <string name="operator_use_operator_toggle_description">Використовувати сервери</string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[Ті ж умови будуть застосовуватись до оператора <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_applied"><![CDATA[Ці умови також застосовуватимуться для: <b>%s</b>.]]></string> + <string name="accept_conditions">Прийняти умови</string> + <string name="view_conditions">Переглянути умови</string> + <string name="operator_added_message_servers">Додано сервери повідомлень</string> + <string name="operator_use_for_messages_private_routing">Для приватної маршрутизації</string> + <string name="operator_use_for_messages_receiving">Для отримання повідомлень</string> + <string name="operator_use_for_files">Використовувати для файлів</string> + <string name="operator_added_xftp_servers">Додано медіа та файлові сервери</string> + <string name="operator_open_changes">Відкрити зміни</string> + <string name="operator_open_conditions">Відкриті умови</string> + <string name="xftp_servers_per_user">Сервери для нових файлів вашого поточного профілю чату</string> + <string name="operator_use_for_sending">Для відправки</string> + <string name="error_adding_server">Помилка додавання сервера</string> + <string name="operator_server_alert_title">Сервер оператора</string> + <string name="server_added_to_operator__name">Сервер додано до оператора %s.</string> + <string name="error_server_operator_changed">Оператор сервера змінився.</string> + <string name="appearance_app_toolbars">Панелі інструментів додатків</string> + <string name="appearance_bars_blur_radius">Розмиття</string> + <string name="appearance_in_app_bars_alpha">Прозорість</string> + <string name="v6_2_improved_chat_navigation">Покращена навігація в чаті</string> + <string name="v6_2_improved_chat_navigation_descr">- Відкрити чат на першому непрочитаному повідомленні.\n- Перейти до цитованих повідомлень.</string> + <string name="v6_2_network_decentralization_descr">Другий попередньо встановлений оператор у застосунку!</string> + <string name="view_updated_conditions">Переглянути оновлені умови</string> + <string name="address_settings">Налаштування адреси</string> + <string name="operators_conditions_accepted_for"><![CDATA[Умови прийняті для оператора(ів): <b>%s</b>.]]></string> + <string name="address_or_1_time_link">Адреса або одноразове посилання?</string> + <string name="operator_conditions_of_use">Умови використання</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[одноразове посилання можна використовувати <i>лише з одним контактом</i> – поділіться особисто чи через будь-який месенджер.]]></string> + <string name="operator_conditions_accepted_for_some"><![CDATA[Умови вже прийняті для наступних операторів: <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Умови будуть прийняті для оператора(ів): <b>%s</b>.]]></string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[Умови будуть прийняті для оператора(ів): <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Ті ж умови будуть застосовуватись до оператора(ів): <b>%s</b>.]]></string> + <string name="operators_conditions_will_also_apply"><![CDATA[Ці умови також застосовуватимуться для: <b>%s</b>.]]></string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[Щоб використовувати сервери <b>%s</b>, прийміть умови використання.]]></string> + <string name="operator_conditions_failed_to_load">Текст поточних умов не вдалося завантажити, ви можете переглянути умови за цим посиланням:</string> + <string name="errors_in_servers_configuration">Помилки в конфігурації серверів.</string> + <string name="operator_conditions_accepted_on">Умови приймаються з: %s.</string> + <string name="v6_2_network_decentralization_enable_flux">Увімкніть Flux у налаштуваннях мережі та серверів для кращої конфіденційності метаданих</string> + <string name="operator_conditions_will_be_accepted_on">Умови приймаються до: %s.</string> + <string name="onboarding_network_operators_continue">Продовжити</string> + <string name="create_1_time_link">Створити одноразове посилання</string> + <string name="error_updating_server_title">Помилка оновлення сервера</string> + <string name="v6_2_network_decentralization_enable_flux_reason">для кращої конфіденційності метаданих.</string> + <string name="v6_2_network_decentralization">Децентралізація мережі</string> + <string name="operator">Оператор</string> + <string name="no_message_servers_configured_for_receiving">Немає серверів для отримання повідомлень.</string> + <string name="simplex_address_or_1_time_link">SimpleX адреса або одноразове посилання?</string> + <string name="smp_servers_new_server">Новий сервер</string> + <string name="no_media_servers_configured_for_private_routing">Немає серверів для отримання файлів.</string> + <string name="operator_review_conditions">Переглянути умови</string> + <string name="no_media_servers_configured_for_sending">Немає серверів для надсилання файлів.</string> + <string name="network_preset_servers_title">Попередньо встановлені сервери</string> + <string name="error_server_protocol_changed">Протокол сервера змінено.</string> + <string name="share_simplex_address_on_social_media">Поділіться адресою SimpleX у соціальних мережах.</string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">SimpleX-адреси та одноразові посилання можна безпечно ділитися через будь-який месенджер.</string> + <string name="connection_error_quota_desc">З\'єднання досягло ліміту недоставлених повідомлень, ваш контакт може бути офлайн.</string> + <string name="address_creation_instruction">Натисніть Створити адресу SimpleX у меню, щоб створити її пізніше.</string> + <string name="onboarding_network_operators_app_will_use_different_operators">Додаток захищає вашу конфіденційність, використовуючи різних операторів у кожній розмові.</string> + <string name="operator_use_for_messages">Використовувати для повідомлень</string> + <string name="onboarding_network_operators_conditions_you_can_configure">Ви можете налаштувати операторів у налаштуваннях Мережі та серверів.</string> + <string name="chat_archive">Або імпортуйте архівний файл</string> + <string name="remote_hosts_section">Віддалені мобільні</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Пристрої Xiaomi</b>: будь ласка, увімкніть Автозапуск у налаштуваннях системи, щоб сповіщення працювали.]]></string> + <string name="maximum_message_size_title">Повідомлення занадто велике!</string> + <string name="maximum_message_size_reached_non_text">Будь ласка, зменшіть розмір повідомлення або видаліть медіа та надішліть знову.</string> + <string name="add_your_team_members_to_conversations">Додайте учасників команди до розмови.</string> + <string name="business_address">Бізнес адреса</string> + <string name="onboarding_notifications_mode_periodic_desc_short">Перевіряти повідомлення кожні 10 хвилин.</string> + <string name="onboarding_notifications_mode_off_desc_short">Без фонової служби</string> + <string name="onboarding_notifications_mode_battery">Сповіщення та батарея</string> + <string name="onboarding_notifications_mode_service_desc_short">Додаток завжди працює у фоні.</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[Усі повідомлення та файли надсилаються <b>зашифрованими end-to-end</b>, з пост-квантовою безпекою в особистих повідомленнях.]]></string> + <string name="leave_chat_question">Покинути чат?</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">Учасник буде видалений з чату — це неможливо скасувати!</string> + <string name="v6_2_business_chats">Бізнес чати</string> + <string name="v6_2_business_chats_descr">Конфіденційність для ваших клієнтів.</string> + <string name="chat_bottom_bar">Доступна панель чату</string> + <string name="button_add_friends">Додати друзів</string> + <string name="button_add_team_members">Додати учасників команди</string> + <string name="invite_to_chat_button">Запросити до чату</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">Чат буде видалений для всіх учасників — це неможливо скасувати!</string> + <string name="button_delete_chat">Видалити чат</string> + <string name="delete_chat_question">Видалити чат?</string> + <string name="only_chat_owners_can_change_prefs">Тільки власники чату можуть змінювати налаштування.</string> + <string name="member_role_will_be_changed_with_notification_chat">Роль буде змінена на %s. Усі учасники чату отримають повідомлення.</string> + <string name="direct_messages_are_prohibited">Прямі повідомлення між учасниками заборонені.</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Ви вже підключені до <b>%1$s</b>.]]></string> + <string name="connect_plan_chat_already_exists">Чат вже існує!</string> + <string name="how_it_helps_privacy">Як це допомагає зберігати конфіденційність</string> + <string name="direct_messages_are_prohibited_in_chat">Прямі повідомлення між учасниками заборонені в цьому чаті.</string> + <string name="button_leave_chat">Покинути чат</string> + <string name="info_row_chat">Чат</string> + <string name="delete_chat_for_self_cannot_undo_warning">Чат буде видалений для вас — це неможливо скасувати!</string> + <string name="maximum_message_size_reached_text">Будь ласка, зменшіть розмір повідомлення та надішліть знову.</string> + <string name="maximum_message_size_reached_forwarding">Скопіюйте та зменшіть розмір повідомлення для відправки.</string> + <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Ви припините отримувати повідомлення з цього чату. Історія чату буде збережена.</string> + <string name="chat_main_profile_sent">Ваш профіль буде надіслано учасникам чату.</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">Коли увімкнено більше ніж одного оператора, жоден з них не має метаданих, щоб дізнатися, хто спілкується з ким.</string> + <string name="display_name_accepted_invitation">прийнято запрошення</string> + <string name="display_name_requested_to_connect">запит на підключення</string> + <string name="onboarding_network_about_operators">Про операторів</string> + <string name="onboarding_network_operators_simplex_flux_agreement">SimpleX Chat та Flux уклали угоду про включення серверів, що працюють на Flux, до додатку.</string> + <string name="sync_connection_desc">Підключення вимагає повторного узгодження шифрування.</string> + <string name="encryption_renegotiation_in_progress">Виконується повторне узгодження шифрування.</string> + <string name="sync_connection_confirm">Виправити</string> + <string name="sync_connection_question">Виправити підключення?</string> + <string name="debug_logs">Увімкнути журнали</string> + <string name="error_saving_database">Помилка збереження бази даних</string> + <string name="a_plus_b">a + b</string> + <string name="strikethrough_text">закреслити</string> + <string name="connection_not_ready">Підключення не готове.</string> + <string name="open_with_app">Відкрити за допомогою %s</string> + <string name="no_chats_in_list">Немає чатів у списку %s.</string> + <string name="no_unread_chats">Немає непрочитаних чатів</string> + <string name="chat_list_all">всі</string> + <string name="save_list">Зберегти список</string> + <string name="delete_chat_list_menu_action">Видалити</string> + <string name="delete_chat_list_question">Видалити список?</string> + <string name="list_name_field_placeholder">Назва списку...</string> + <string name="delete_chat_list_warning">Усі чати буде видалено зі списку %s, а сам список видалено</string> + <string name="chat_list_contacts">Контакти</string> + <string name="error_creating_chat_tags">Помилка створення списку чату</string> + <string name="no_chats_found">Чати не знайдено</string> + <string name="create_list">Створити список</string> + <string name="chat_list_favorites">Вибране</string> + <string name="no_chats">Без чатів</string> + <string name="edit_chat_list_menu_action">Редагувати</string> + <string name="error_loading_chat_tags">Помилка завантаження списків чату</string> + <string name="error_updating_chat_tags">Помилка оновлення списку чату</string> + <string name="chat_list_groups">Групи</string> + <string name="chat_list_businesses">Компанії</string> + <string name="add_to_list">Додати до списку</string> + <string name="chat_list_add_list">Додати список</string> + <string name="list_menu">Список</string> + <string name="duplicated_list_error">Назва списку та емодзі мають відрізнятися для всіх списків.</string> + <string name="chat_list_notes">Нотатки</string> + <string name="change_list">Список змін</string> + <string name="change_order_chat_list_menu_action">Змінити порядок</string> + <string name="prefs_error_saving_settings">Помилка збереження налаштувань</string> + <string name="error_creating_report">Помилка при створенні звіту</string> + <string name="report_item_visibility_submitter">Тільки ви та модератори бачать це</string> + <string name="report_compose_reason_header_spam">Повідомити про спам: тільки модератори групи побачать це.</string> + <string name="report_reason_community">Порушення правил спільноти</string> + <string name="group_member_role_moderator">модератор</string> + <string name="report_reason_alert_title">Причина повідомлення?</string> + <string name="report_item_visibility_moderators">Тільки відправник і модератори бачать це</string> + <string name="report_verb">Повідомити</string> + <string name="report_compose_reason_header_community">Повідомити про порушення: тільки модератори групи побачать це.</string> + <string name="report_archive_alert_desc">Повідомлення буде архівоване для вас.</string> + <string name="report_reason_other">Інша причина</string> + <string name="report_item_archived">архівование повідомлення</string> + <string name="archive_verb">Архів</string> + <string name="report_reason_illegal">Неприпустимий контент</string> + <string name="report_compose_reason_header_illegal">Повідомити про контент: тільки модератори групи побачать це.</string> + <string name="report_archive_alert_title">Архівувати повідомлення?</string> + <string name="report_compose_reason_header_profile">Повідомити про профіль учасника: тільки модератори групи побачать це.</string> + <string name="report_reason_profile">Неприпустимий профіль</string> + <string name="report_reason_spam">Спам</string> + <string name="report_compose_reason_header_other">Повідомити інше: тільки модератори групи побачать це.</string> + <string name="archive_report">Архівувати повідомлення</string> + <string name="report_item_archived_by">архівоване повідомлення від %s</string> + <string name="delete_report">Видалити повідомлення</string> + <string name="group_reports_active_one">1 повідомлення</string> + <string name="group_reports_active">%d повідомлень</string> + <string name="group_reports_member_reports">Повідомлення учасників</string> + <string name="chat_list_group_reports">Повідомлення</string> + <string name="blocking_reason_content">Вміст порушує умови використання</string> + <string name="blocking_reason_spam">Спам</string> + <string name="file_error_blocked">Файл заблоковано оператором сервера: \n%1$s.</string> + <string name="privacy_chat_list_open_links_yes">Так</string> + <string name="privacy_chat_list_open_web_link">Відкрити посилання</string> + <string name="privacy_chat_list_open_web_link_question">Відкрити вебпосилання?</string> + <string name="connection_error_blocked">Підключення заблоковано</string> + <string name="privacy_chat_list_open_links_ask">Запитати</string> + <string name="privacy_chat_list_open_links">Відкрити посилання зі списку чату</string> + <string name="connection_error_blocked_desc">Підключення заблоковано оператором сервера: \n%1$s.</string> + <string name="privacy_chat_list_open_links_no">Ні</string> + <string name="text_field_set_chat_placeholder">Назвати чат…</string> + <string name="disable_automatic_deletion_message">Повідомлення в цьому чаті ніколи не будуть видалені.</string> + <string name="chat_ttl_options_footer">Видалити повідомлення чату з вашого пристрою.</string> + <string name="disable_automatic_deletion_question">Вимкнути автоматичне видалення повідомлень?</string> + <string name="disable_automatic_deletion">Вимкнути видалення повідомлень</string> + <string name="chat_item_ttl_year">1 рік</string> + <string name="chat_item_ttl_default">типово (%s)</string> + <string name="change_automatic_deletion_question">Змінити автоматичне видалення повідомлень?</string> + <string name="change_automatic_chat_deletion_message">Цю дію не можна скасувати — повідомлення, надіслані та отримані в цьому чаті раніше за обраний час, будуть видалені.</string> + <string name="network_smp_web_port_section_title">TCP-порт для повідомлень</string> + <string name="network_smp_web_port_footer">Використовувати TCP-порт %1$s, якщо порт не вказано.</string> + <string name="network_smp_web_port_toggle">Використовувати веб-порт</string> + <string name="mute_all_chat">Вимкнути звук</string> + <string name="unread_mentions">Непрочитані згадки</string> + <string name="max_group_mentions_per_message_reached">Ви можете згадувати до %1$s учасників у кожному повідомленні!</string> + <string name="report_archive_alert_desc_all">Усі звіти будуть архівовані для вас.</string> + <string name="report_archive_alert_title_all">Архівувати всі звіти?</string> + <string name="report_archive_alert_title_nth">Архівувати %d звітів?</string> + <string name="archive_reports">Архівувати звіти</string> + <string name="report_archive_for_all_moderators">Для всіх модераторів</string> + <string name="report_archive_for_me">Для мене</string> + <string name="notification_group_report">Звіт: %s</string> + <string name="disable_sending_member_reports">Заборонити повідомлення модераторам.</string> + <string name="member_reports_are_prohibited">Повідомлення модераторам заборонено в цій групі.</string> + <string name="group_members_can_send_reports">Учасники можуть повідомляти повідомлення модераторам.</string> + <string name="enable_sending_member_reports">Дозволити повідомляти повідомлення модераторам.</string> + <string name="group_preview_rejected">відхилено</string> + <string name="group_member_status_rejected">відхилено</string> + <string name="v6_3_better_groups_performance">Краща продуктивність груп</string> + <string name="v6_3_organize_chat_lists_descr">Не пропускайте важливі повідомлення.</string> + <string name="v6_3_mentions_descr">Отримуйте сповіщення, коли вас згадують.</string> + <string name="v6_3_reports_descr">Допоможіть адміністраторам модерувати їхні групи.</string> + <string name="v6_3_organize_chat_lists">Організовуйте чати в списки</string> + <string name="v6_3_private_media_file_names">Приватні імена медіа-файлів.</string> + <string name="v6_3_reports">Надсилати приватні звіти</string> + <string name="v6_3_better_privacy_and_security">Краща конфіденційність та безпека</string> + <string name="v6_3_faster_sending_messages">Швидша відправка повідомлень.</string> + <string name="v6_3_mentions">Згадуйте учасників 👋</string> + <string name="v6_3_set_message_expiration_in_chats">Встановлюйте термін придатності повідомлень у чатах.</string> + <string name="v6_3_faster_deletion_of_groups">Швидше видалення груп.</string> + <string name="error_reading_passphrase">Помилка зчитування пароля бази даних</string> + <string name="restore_passphrase_can_not_be_read_desc">Пароль у сховищі ключів не можна зчитати. Це могло статися після оновлення системи, несумісного з додатком. Якщо це не так, зверніться до розробників.</string> + <string name="restore_passphrase_can_not_be_read_enter_manually_desc">Пароль у сховищі ключів не можна зчитати, введіть його вручну. Це могло статися після оновлення системи, несумісного з додатком. Якщо це не так, зверніться до розробників.</string> + <string name="group_member_status_pending_approval_short">очікує</string> + <string name="group_member_status_pending_approval">очікує на схвалення</string> + <string name="button_remove_members_question">Видалити учасників?</string> + <string name="block_members_desc">Усі нові повідомлення від цих учасників будуть приховані!</string> + <string name="block_members_for_all_question">Заблокувати учасників для всіх?</string> + <string name="members_will_be_removed_from_group_cannot_be_undone">Учасників буде видалено з групи – це неможливо скасувати!</string> + <string name="feature_roles_moderators">модератори</string> + <string name="unblock_members_desc">Повідомлення від цих учасників буде показано!</string> + <string name="members_will_be_removed_from_chat_cannot_be_undone">Учасників буде видалено з чату – це неможливо скасувати!</string> + <string name="unblock_members_for_all_question">Розблокувати учасників для всіх?</string> + <string name="operator_updated_conditions">Оновлені умови</string> + <string name="onboarding_conditions_private_chats_not_accessible">Приватні чати, групи та ваші контакти недоступні для операторів сервера.</string> + <string name="onboarding_conditions_accept">Прийняти</string> + <string name="onboarding_conditions_by_using_you_agree">Використовуючи SimpleX Chat, ви погоджуєтесь на:\n- надсилати тільки легальний контент у публічних групах.\n- поважати інших користувачів – без спаму.</string> + <string name="onboarding_conditions_configure_server_operators">Налаштувати операторів сервера</string> + <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Політика конфіденційності та умови використання</string> + <string name="privacy_short_links">Використовувати короткі посилання (BETA)</string> + <string name="link_requires_newer_app_version_please_upgrade">Це посилання вимагає новішої версії додатку. Будь ласка, оновіть додаток або попросіть вашого контакту надіслати сумісне посилання.</string> + <string name="full_link_button_text">Повне посилання</string> + <string name="short_link_button_text">Коротке посилання</string> + <string name="simplex_link_channel">Посилання на канал SimpleX</string> + <string name="unsupported_connection_link">Несумісне посилання для підключення</string> + <string name="network_smp_web_port_all">Усі сервери</string> + <string name="network_smp_web_port_off">Ні</string> + <string name="network_smp_web_port_preset">Типові сервери</string> + <string name="network_smp_web_port_preset_footer">Використовуйте TCP порт 443 лише для попередньо налаштованих серверів.</string> +</resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml new file mode 100644 index 0000000000..3cbc54f652 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml @@ -0,0 +1,2365 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d tin nhắn không thể giải mã.</string> + <string name="moderated_items_description">%1$d tin nhắn đã bị xóa bởi %2$s</string> + <string name="group_info_section_title_num_members">%1$s THÀNH VIÊN</string> + <string name="learn_more_about_address">Thông tin về địa chỉ SimpleX</string> + <string name="address_section_title">Địa chỉ</string> + <string name="abort_switch_receiving_address_confirm">Hủy bỏ</string> + <string name="accept_contact_button">Chấp nhận</string> + <string name="one_time_link_short">đường dẫn dùng 1 lần</string> + <string name="add_contact_tab">Thêm liên hệ</string> + <string name="about_simplex_chat">Thông tin về SimpleX Chat</string> + <string name="smp_servers_add">Thêm máy chủ</string> + <string name="about_simplex">Thông tin về SimpleX</string> + <string name="group_member_role_admin">quản trị viên</string> + <string name="button_add_welcome_message">Thêm lời chào</string> + <string name="contact_wants_to_connect_via_call">%1$s muốn kết nối với bạn qua</string> + <string name="accept">Chấp nhận</string> + <string name="alert_text_decryption_error_too_many_skipped">%1$d tin nhắn đã bị bỏ qua.</string> + <string name="users_add">Thêm hồ sơ</string> + <string name="integrity_msg_skipped">%1$d tin nhắn bị bỏ qua</string> + <string name="send_disappearing_message_1_minute">1 phút</string> + <string name="send_disappearing_message_5_minutes">5 phút</string> + <string name="accept_feature">Chấp nhận</string> + <string name="v4_3_improved_server_configuration_desc">Thêm máy chủ bằng cách quét mã QR.</string> + <string name="network_settings">Cài đặt mạng nâng cao</string> + <string name="users_delete_all_chats_deleted">Tất cả các cuộc trò chuyện và tin nhắn sẽ bị xóa - quá trình này không thể được hoàn tác!</string> + <string name="abort_switch_receiving_address_question">Hủy bỏ việc đổi địa chỉ?</string> + <string name="abort_switch_receiving_address_desc">Việc thay đổi địa chỉ sẽ bị hủy bỏ. Địa chỉ nhận cũ tiếp tục được sử dụng.</string> + <string name="send_disappearing_message_30_seconds">30 giây</string> + <string name="accept_connection_request__question">Chấp nhận yêu cầu kết nối?</string> + <string name="accept_contact_incognito_button">Chấp nhận bằng hồ sơ ẩn danh</string> + <string name="smp_servers_preset_add">Thêm các máy chủ được cài sẵn</string> + <string name="smp_servers_add_to_another_device">Thêm vào một thiết bị khác</string> + <string name="callstatus_accepted">cuộc gọi được chấp nhận</string> + <string name="conn_event_ratchet_sync_started">đồng ý mã hóa…</string> + <string name="snd_conn_event_ratchet_sync_started">đồng ý mã hóa cho %s</string> + <string name="v5_2_more_things">Một vài điều nữa</string> + <string name="accept_call_on_lock_screen">Chấp nhận</string> + <string name="all_app_data_will_be_cleared">Tất cả dữ liệu đã bị xóa.</string> + <string name="chat_item_ttl_month">1 tháng</string> + <string name="chat_item_ttl_week">1 tuần</string> + <string name="abort_switch_receiving_address">Hủy bỏ việc đổi địa chỉ</string> + <string name="v4_2_group_links_desc">Các quản trị viên có thể tạo đường dẫn để tham gia nhóm.</string> + <string name="chat_item_ttl_day">Một ngày</string> + <string name="color_secondary_variant">Biến thể của màu thứ cấp</string> + <string name="all_group_members_will_remain_connected">Tất cả các thành viên trong nhóm vẫn sẽ được giữ kết nối.</string> + <string name="clear_note_folder_warning">Tất cả tin nhắn sẽ bị xóa - Quá trình này không thể hoàn tác được!</string> + <string name="block_member_desc">Tất cả các tin nhắn mới từ %s sẽ bị ẩn!</string> + <string name="turn_off_battery_optimization_button">Cho phép</string> + <string name="color_primary">Màu sơ cấp</string> + <string name="network_enable_socks_info">Truy cập các máy chủ thông qua SOCKS proxy tại cổng %d? Proxy phải được khởi động trước khi bật cài đặt này.</string> + <string name="add_address_to_your_profile">Thêm địa chỉ vào hồ sơ để các liên hệ của bạn có thể dễ dàng chia sẻ với mọi người. Bản cập nhật hồ sơ cũng sẽ được gửi tới các liên hệ hiện thời.</string> + <string name="allow_disappearing_messages_only_if">Cho phép nhắn tin nhắn tự xóa chỉ khi liên hệ của bạn cũng cho phép</string> + <string name="allow_verb">Cho phép</string> + <string name="above_then_preposition_continuation">theo như ở trên, thì:</string> + <string name="clear_chat_warning">Tất cả tin nhắn sẽ bị xóa - Quá trình này không thể hoàn tác được! Tin nhắn sẽ CHỈ bị xóa ở phía bạn.</string> + <string name="color_primary_variant">Biến thể của màu sơ cấp</string> + <string name="color_secondary">Màu thứ cấp</string> + <string name="allow_calls_only_if">Cho phép gọi điện chỉ khi liên hệ của bạn cũng cho phép.</string> + <string name="v5_3_new_interface_languages">6 ngôn ngữ giao diện mới</string> + <string name="v5_1_self_destruct_passcode_descr">Tất cả dữ liệu sẽ bị xóa khi nó được nhập vào.</string> + <string name="allow_voice_messages_question">Cho phép tin nhắn thoại?</string> + <string name="allow_irreversible_message_deletion_only_if">Cho phép xóa tin nhắn theo cách không thể phục hồi chỉ khi liên hệ của bạn cũng cho phép điều đó. (24 giờ)</string> + <string name="allow_your_contacts_irreversibly_delete">Cho phép các liên hệ của bạn xóa tin nhắn đã gửi theo cách không thể phục hồi. (24 giờ)</string> + <string name="allow_your_contacts_to_send_disappearing_messages">Cho phép các liên hệ của bạn gửi tin nhắn tự xóa.</string> + <string name="allow_message_reactions_only_if">Cho phép thả biểu tượng cảm xúc chỉ khi liên hệ của bạn cũng cho phép.</string> + <string name="allow_voice_messages_only_if">Cho phép tin nhắn thoại chỉ khi liên hệ của bạn cũng cho phép.</string> + <string name="allow_your_contacts_adding_message_reactions">Cho phép liên hệ của bạn thả biểu tượng cảm xúc.</string> + <string name="allow_your_contacts_to_call">Cho phép các liên hệ của bạn gọi cho bạn.</string> + <string name="allow_your_contacts_to_send_voice_messages">Cho phép các liên hệ của bạn gửi tin nhắn thoại.</string> + <string name="allow_direct_messages">Cho phép gửi tin nhắn trực tiếp tới các thành viên.</string> + <string name="allow_to_delete_messages">Cho phép xóa tin nhắn đã gửi một cách không thể phục hồi. (24 giờ)</string> + <string name="allow_to_send_disappearing">Cho phép gửi tin nhắn tự xóa.</string> + <string name="allow_to_send_voice">Cho phép gửi tin nhắn thoại.</string> + <string name="allow_message_reactions">Cho phép thả biểu tượng cảm xúc.</string> + <string name="allow_to_send_files">Cho phép gửi các tệp và đa phương tiện.</string> + <string name="notifications_mode_service">Luôn luôn bật</string> + <string name="all_your_contacts_will_remain_connected">Tất cả liên hệ của bạn vẫn sẽ được giữ kết nối.</string> + <string name="all_your_contacts_will_remain_connected_update_sent">Tất cả liên hệ của bạn vẫn sẽ được giữ kết nối. Bản cập nhật hồ sơ sẽ được gửi tới các liên hệ của bạn.</string> + <string name="chat_preferences_always">luôn luôn</string> + <string name="connect_plan_already_connecting">Đã kết nối rồi!</string> + <string name="connect_plan_already_joining_the_group">Đã tham gia nhóm rồi!</string> + <string name="always_use_relay">Luôn sử dụng relay</string> + <string name="rcv_group_and_other_events">và %d sự kiện khác</string> + <string name="keychain_is_storing_securely">Android Keystore được sử dụng để lưu trữ passphrase - nó cho phép dịch vụ thông báo hoạt động.</string> + <string name="keychain_allows_to_receive_ntfs">Android Keystore sẽ được sử dụng để lưu trữ passphrase một cách an toàn sau khi bạn khởi động lại ứng dụng hoặc thay đổi passphrase - nó cho phép tiếp nhận thông báo.</string> + <string name="migrate_from_device_all_data_will_be_uploaded">Tất cả các liên hệ, cuộc hội thoại và tệp của bạn sẽ được mã hóa an toàn và tải lên từng phần tới các XFTP relay được chỉ định.</string> + <string name="v5_6_safer_groups_descr">Quản trị viên có thể chặn một thành viên khỏi tất cả.</string> + <string name="empty_chat_profile_is_created">Một hồ sơ trống với tên chỉ định đã được tạo, và ứng dụng sẽ mở ra như bình thường.</string> + <string name="answer_call">Trả lời cuộc gọi</string> + <string name="connect__a_new_random_profile_will_be_shared">Một hồ sơ ngẫu nhiên mới sẽ được chia sẻ.</string> + <string name="notifications_mode_off_desc">Ứng dụng chỉ có thể nhận thông báo khi nó đang chạy, không có dịch vụ nền nào được khởi động</string> + <string name="app_version_code">Bản dựng ứng dụng: %s</string> + <string name="appearance_settings">Giao diện</string> + <string name="settings_section_title_app">ỨNG DỤNG</string> + <string name="v5_6_app_data_migration">Di chuyển dữ liệu ứng dụng</string> + <string name="full_backup">Sao lưu dữ liệu ứng dụng</string> + <string name="app_passcode_replaced_with_self_destruct">Mã truy cập ứng dụng đã được thay thế bằng mã tự hủy.</string> + <string name="v5_3_encrypt_local_files_descr">Ứng dụng mã hóa các tệp cục bộ mới (trừ video).</string> + <string name="migrate_to_device_apply_onion">Áp dụng</string> + <string name="la_app_passcode">Mã truy cập ứng dụng</string> + <string name="settings_section_title_icon">BIỂU TƯỢNG ỨNG DỤNG</string> + <string name="v5_0_app_passcode">Mã truy cập</string> + <string name="app_version_name">Phiên bản ứng dụng: v%s</string> + <string name="app_version_title">Phiên bản ứng dụng</string> + <string name="v5_3_new_interface_languages_descr">Tiếng Ả Rập, tiếng Bungari, tiếng Phần Lan, tiếng Do Thái, tiếng Thái và tiếng Ukraina - trân thành gửi lời cảm ơn tới các tình nguyện viên dịch thuật và Weblate.</string> + <string name="migrate_from_device_archiving_database">Lưu trữ cơ sở dữ liệu</string> + <string name="migrate_from_device_archive_and_upload">Lưu trữ và tải lên</string> + <string name="icon_descr_audio_on">Mở âm</string> + <string name="icon_descr_video_asked_to_receive">Yêu cầu để nhận video</string> + <string name="attach">Gắn</string> + <string name="call_service_notification_audio_call">Cuộc gọi thoại</string> + <string name="audio_call_no_encryption">cuộc gọi thoại (không được mã hóa đầu cuối)</string> + <string name="icon_descr_audio_off">Tắt âm</string> + <string name="icon_descr_asked_to_receive">Yêu cầu để nhận hình ảnh</string> + <string name="network_session_mode_user_description"><![CDATA[Một kết nối TCP riêng biệt (và thông tin xác thực SOCKS) sẽ được sử dụng <b>cho mỗi hồ sơ trò chuyện bạn có trong ứng dụng</b>.]]></string> + <string name="network_session_mode_entity_description"><![CDATA[Một kết nối TCP riêng biệt (và thông tin xác thực SOCKS) sẽ được sử dụng <b>cho từng liên hệ và thành viên nhóm</b>. \n<b>Xin lưu ý</b>: nếu bạn có nhiều kết nối, mức tiêu thụ pin và lưu lượng truy cập của bạn có thể cao hơn đáng kể và một số kết nối có thể không thành công.]]></string> + <string name="icon_descr_audio_call">cuộc gọi thoại</string> + <string name="v4_6_audio_video_calls">Các cuộc gọi thoại và video</string> + <string name="settings_audio_video_calls">Các cuộc gọi thoại và video</string> + <string name="audio_video_calls">Các cuộc gọi thoại/video</string> + <string name="feature_roles_all_members">tất cả các thành viên</string> + <string name="calls_prohibited_with_this_contact">Các cuộc gọi thoại/video bị cấm.</string> + <string name="allow_to_send_simplex_links">Cho phép gửi đường dẫn SimpleX.</string> + <string name="feature_roles_admins">quản trị viên</string> + <string name="group_member_role_author">tác giả</string> + <string name="authentication_cancelled">Quá trình xác thực đã bị hủy bỏ</string> + <string name="la_authenticate">Xác thực</string> + <string name="auth_unavailable">Quá trình xác thực không có sẵn</string> + <string name="la_auth_failed">Quá trình xác thực đã thất bại</string> + <string name="color_background">Phông nền</string> + <string name="alert_title_msg_bad_hash">Hàm băm tin nhắn xấu</string> + <string name="integrity_msg_bad_id">ID tin nhắn xấu</string> + <string name="alert_title_msg_bad_id">ID tin nhắn xấu</string> + <string name="notifications_mode_service_desc">Dịch vụ nền luôn luôn chạy - thông báo sẽ được hiển thị ngay khi nhận được tin nhắn.</string> + <string name="v4_2_auto_accept_contact_requests">Tự động chấp nhận yêu cầu liên hệ</string> + <string name="bad_desktop_address">Địa chỉ máy tính xấu</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Thêm liên hệ</b>: để tạo đường dẫn mời mới, hoặc kết nối qua đường dẫn bạn nhận được.]]></string> + <string name="integrity_msg_bad_hash">hàm băm tin nhắn xấu</string> + <string name="available_in_v51">" +\nCó sẵn ở v5.1"</string> + <string name="auto_accept_contact">Tự động chấp nhận</string> + <string name="back">Quay về</string> + <string name="auto_accept_images">Tự động chấp nhận hình ảnh</string> + <string name="audio_device_bluetooth">Bluetooth</string> + <string name="v5_4_better_groups">Nhóm tốt hơn</string> + <string name="v5_4_block_group_members">Chặn thành viên nhóm</string> + <string name="rcv_group_event_member_blocked">đã chặn %s</string> + <string name="v5_1_better_messages">Tin nhắn tốt hơn</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Xin lưu ý</b>: việc sử dụng cùng một cơ sở dữ liệu trên hai thiết bị sẽ phá vỡ quá trình giải mã tin nhắn từ các kết nối của bạn, như một biện pháp bảo vệ.]]></string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>Tạo nhóm</b>: để tạo một nhóm mới.]]></string> + <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Tốt nhất cho pin</b>. Bạn sẽ chỉ nhận được thông báo khi ứng dụng đang chạy (KHÔNG có dịch vụ nền).]]></string> + <string name="impossible_to_recover_passphrase"><![CDATA[<b>Xin lưu ý</b>: bạn sẽ KHÔNG thể khôi phục hoặc thay đổi passphrase nếu bạn làm mất nó.]]></string> + <string name="member_blocked_by_admin">Bị chặn bởi quản trị viên</string> + <string name="block_member_button">Chặn thành viên</string> + <string name="member_info_member_blocked">đã chặn</string> + <string name="blocked_by_admin_item_description">bị chặn bởi quản trị viên</string> + <string name="blocked_item_description">đã chặn</string> + <string name="turning_off_service_and_periodic">Tính năng tối ưu hóa pin đang được kích hoạt, tắt dịch vụ nền và các yêu cầu định kỳ về tin nhắn mới. Bạn có thể kích hoạt lại chúng thông qua cài đặt.</string> + <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Có thể tắt thông qua cài đặt</b> - thông báo vẫn sẽ được hiển thị khi ứng dụng đang chạy.]]></string> + <string name="block_member_question">Chặn thành viên?</string> + <string name="both_you_and_your_contacts_can_delete">Cả bạn và liên hệ của bạn đều có thể xóa tin nhắn đã gửi mà không thể hoàn tác. (24 tiếng)</string> + <string name="block_for_all">Chặn tất cả</string> + <string name="block_for_all_question">Chặn thành viên cho tất cả?</string> + <string name="block_member_confirmation">Chặn</string> + <string name="both_you_and_your_contact_can_send_voice">Cả bạn và liên hệ của bạn đều có thể gửi tin nhắn thoại.</string> + <string name="both_you_and_your_contact_can_make_calls">Cả bạn và liên hệ của bạn đều có thể thực hiện cuộc gọi.</string> + <string name="bold_text">in đậm</string> + <string name="both_you_and_your_contact_can_send_disappearing">Cả bạn và liên hệ của bạn đều có thể gửi tin nhắn tự xóa.</string> + <string name="both_you_and_your_contact_can_add_message_reactions">Cả bạn và liên hệ của bạn đều có thể thả cảm xúc tin nhắn.</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Xin lưu ý</b>: relay tin nhắn và tệp được kết nối thông qua SOCKS proxy. Các cuộc gọi và bản xem trước đường dẫn sử dụng kết nối trực tiếp.]]></string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Tốt cho pin</b>. Ứng dụng kiểm tra tin nhắn 10 phút một lần. Bạn có thể bỏ lỡ các cuộc gọi hoặc tin nhắn khẩn cấp.]]></string> + <string name="network_smp_proxy_mode_always">Luôn luôn</string> + <string name="callstatus_calling">đang gọi…</string> + <string name="v4_5_transport_isolation_descr">Theo hồ sơ trò chuyện (mặc định) hoặc theo kết nối (BETA).</string> + <string name="callstatus_in_progress">cuộc gọi đang chờ</string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Sử dụng nhiều pin hơn</b>! Ứng dụng luôn luôn chạy - thông báo sẽ được hiển thị ngay lập tức.]]></string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Cảnh báo</b>: kho lưu trữ sẽ bị xóa.]]></string> + <string name="icon_descr_call_progress">Cuộc gọi đang chờ</string> + <string name="network_smp_proxy_fallback_allow_downgrade">Cho phép hạ cấp</string> + <string name="call_already_ended">Cuộc gọi đã kết thúc!</string> + <string name="network_smp_proxy_mode_always_description">Luôn luôn sử dụng định tuyến riêng tư.</string> + <string name="icon_descr_call_ended">Cuộc gọi kết thúc</string> + <string name="callstatus_ended">cuộc gọi kết thúc %1$s</string> + <string name="callstatus_error">lỗi cuộc gọi</string> + <string name="settings_section_title_calls">CUỘC GỌI</string> + <string name="icon_descr_cancel_image_preview">Hủy xem trước ảnh</string> + <string name="icon_descr_cancel_file_preview">Hủy xem trước tệp</string> + <string name="cancel_verb">Hủy</string> + <string name="use_camera_button">Camera</string> + <string name="camera_not_available">Camera hiện đang bận</string> + <string name="permissions_camera">Camera</string> + <string name="call_on_lock_screen">Cuộc gọi trên màn hình khóa:</string> + <string name="theme_black">Đen</string> + <string name="chat_theme_apply_to_mode">Áp dụng cho</string> + <string name="color_primary_variant2">Biến thể của màu sơ cấp 2</string> + <string name="wallpaper_advanced_settings">Cài đặt nâng cao</string> + <string name="chat_theme_apply_to_all_modes">Tất cả chế độ màu</string> + <string name="permissions_camera_and_record_audio">Camera và mic</string> + <string name="alert_title_cant_invite_contacts">Không thể mời liên hệ!</string> + <string name="cannot_access_keychain">Không thể truy cập Keystore để lưu mật khẩu cơ sở dữ liệu</string> + <string name="icon_descr_cancel_link_preview">hủy bỏ xem trước đường dẫn</string> + <string name="migrate_from_device_cancel_migration">Hủy bỏ di chuyển</string> + <string name="theme_destination_app_theme">Chủ đề ứng dụng</string> + <string name="cannot_receive_file">Không thể nhận tệp</string> + <string name="database_initialization_error_title">Không thể khởi tạo cơ sở dữ liệu</string> + <string name="feature_cancelled_item">đã hủy bỏ %s</string> + <string name="invite_prohibited">Không thể mời liên hệ!</string> + <string name="icon_descr_cancel_live_message">Hủy bỏ tin nhắn động</string> + <string name="rcv_group_event_changed_member_role">đã thay đổi chức vụ của %s thành %s</string> + <string name="snd_error_quota">Dung lượng lưu trữ vượt quá giới hạn - người nhận không thể nhận được tin nhắn vừa gửi trước đó.</string> + <string name="rcv_conn_event_switch_queue_phase_completed">đã thay đổi địa chỉ cho bạn</string> + <string name="change_database_passphrase_question">Thay đổi mật khẩu cơ sở dữ liệu?</string> + <string name="rcv_group_event_changed_your_role">đã thay đổi chức vụ của bạn thành %s</string> + <string name="change_lock_mode">Thay đổi chế độ khóa</string> + <string name="network_type_cellular">Di động</string> + <string name="la_change_app_passcode">Đổi mật khẩu</string> + <string name="change_member_role_question">Thay đổi chức vụ nhóm?</string> + <string name="change_verb">Thay đổi</string> + <string name="switch_receiving_address_question">Thay đổi địa chỉ nhận?</string> + <string name="switch_receiving_address">Thay đổi địa chỉ nhận</string> + <string name="change_self_destruct_mode">Thay đổi chế độ tự hủy</string> + <string name="change_self_destruct_passcode">Thay đổi mã tự hủy</string> + <string name="change_role">Thay đổi chức vụ</string> + <string name="snd_conn_event_switch_queue_phase_changing">đang thay đổi địa chỉ…</string> + <string name="rcv_conn_event_switch_queue_phase_changing">đang thay đổi địa chỉ…</string> + <string name="chat_console">Bảng điều khiển trò chuyện</string> + <string name="chat_is_running">Kết nối trò chuyện đang hoạt động</string> + <string name="chat_database_deleted">Cơ sở dữ liệu trò chuyện đã bị xóa</string> + <string name="chat_is_stopped_you_should_transfer_database">Kết nối trò chuyện đã được dừng lại. Nếu bạn đã sử dụng cơ sở dữ liệu này trên một thiết bị khác, bạn nên chuyển nó trở lại trước khi khởi động kết nối.</string> + <string name="chat_database_imported">Cơ sở dữ liệu trò chuyện đã được nhập</string> + <string name="chat_is_stopped_indication">Kết nối trò chuyện đã được dừng lại</string> + <string name="snd_conn_event_switch_queue_phase_changing_for_member">đang thay đổi địa chỉ cho %s…</string> + <string name="chat_preferences">Tùy chọn trò chuyện</string> + <string name="settings_section_title_chat_colors">Màu trò chuyện</string> + <string name="chat_database_section">CƠ SỞ DỮ LIỆU TRÒ CHUYỆN</string> + <string name="chat_is_stopped">Kết nối trò chuyện đã được dừng lại</string> + <string name="migrate_to_device_chat_migrated">Cơ sở dữ liệu đã được di chuyển!</string> + <string name="your_chats">Các cuộc trò chuyện</string> + <string name="settings_section_title_chats">CÁC CUỘC TRÒ CHUYỆN</string> + <string name="notifications_mode_periodic_desc">Kiểm tra tin nhắn mới mỗi 10 phút trong tối đa 1 phút</string> + <string name="v4_6_chinese_spanish_interface">Giao diện Trung Quốc và Tây Ban Nha</string> + <string name="chat_with_developers">Trò chuyện với nhà phát triển</string> + <string name="smp_servers_check_address">Kiểm tra lại địa chỉ server và thử lại.</string> + <string name="choose_file_title">Chọn một tệp</string> + <string name="network_session_mode_user">Hồ sơ trò chuyện</string> + <string name="migrate_from_device_check_connection_and_try_again">Kiểm tra kết nối internet của bạn và thử lại</string> + <string name="settings_section_title_chat_theme">Chủ đề trò chuyện</string> + <string name="color_mode">Chế độ màu</string> + <string name="clear_verification">Xác minh dọn dẹp</string> + <string name="clear_contacts_selection_button">Dọn dẹp</string> + <string name="icon_descr_close_button">Nút đóng</string> + <string name="clear_note_folder_question">Dọn dẹp ghi chú riêng tư?</string> + <string name="colored_text">có màu</string> + <string name="clear_chat_button">Dọn dẹp cuộc trò chuyện</string> + <string name="clear_chat_menu_action">Dọn dẹp</string> + <string name="in_developing_title">Sắp ra mắt!</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Chọn <i>Di chuyển từ một thiết bị khác</i>trên thiết bị mới và quét mã QR.]]></string> + <string name="clear_verb">Dọn dẹp</string> + <string name="clear_chat_question">Dọn dẹp cuộc trò chuyện?</string> + <string name="configure_ICE_servers">Cài đặt cấu hình cho các máy chủ ICE</string> + <string name="migrate_to_device_confirm_network_settings">Xác nhận các cài đặt mạng</string> + <string name="confirm_passcode">Xác nhận mã truy cập</string> + <string name="group_member_status_complete">hoàn thành</string> + <string name="confirm_password">Xác nhận mật khẩu</string> + <string name="confirm_database_upgrades">Xác nhận nâng cấp cơ sở dữ liệu</string> + <string name="v4_4_verify_connection_security_desc">So sánh mã bảo mật với liên hệ của bạn.</string> + <string name="confirm_verb">Xác nhận</string> + <string name="smp_server_test_compare_file">So sánh tệp</string> + <string name="v5_8_safe_files_descr">Xác nhận các tệp từ những máy chủ không xác định.</string> + <string name="confirm_new_passphrase">Xác nhận mật khẩu mới…</string> + <string name="migrate_from_device_confirm_upload">Xác nhận tải lên</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">Xác nhận rằng bạn nhớ mật khẩu cơ sở dữ liệu để chuyển nó đi.</string> + <string name="smp_server_test_connect">Kết nối</string> + <string name="group_member_status_connected">đã kết nối</string> + <string name="icon_descr_server_status_connected">Đã kết nối</string> + <string name="callstate_connected">đã kết nối</string> + <string name="connect_via_member_address_alert_title">Kết nối trực tiếp?</string> + <string name="connect_via_link_verb">Kết nối</string> + <string name="rcv_group_event_member_connected">đã kết nối</string> + <string name="multicast_connect_automatically">Tự động kết nối</string> + <string name="server_connected">đã kết nối</string> + <string name="notification_contact_connected">Đã kết nối</string> + <string name="auth_confirm_credential">Xác nhận thông tin đăng nhập của bạn</string> + <string name="connect_button">Kết nối</string> + <string name="connected_desktop">Máy tính đã được kết nối</string> + <string name="rcv_group_event_member_created_contact">đã kết nối trực tiếp</string> + <string name="group_member_status_accepted">đang kết nối (đã được chấp nhận)</string> + <string name="contact_connection_pending">đang kết nối…</string> + <string name="connected_to_mobile">Đã kết nối tới điện thoại</string> + <string name="connected_mobile">Điện thoại đã được kết nối</string> + <string name="group_member_status_connecting">đang kết nối</string> + <string name="group_connection_pending">đang kết nối…</string> + <string name="callstate_connecting">đang kết nối…</string> + <string name="connect_via_link_incognito">Kết nối ẩn danh</string> + <string name="server_connecting">đang kết nối</string> + <string name="connected_to_desktop">Đã kết nối tới máy tính</string> + <string name="group_member_status_announced">đang kết nối (đã được thông báo)</string> + <string name="display_name_connecting">đang kết nối…</string> + <string name="group_member_status_introduced">đang kết nối (đã được giới thiệu)</string> + <string name="group_member_status_intro_invitation">đang kết nối (lời mời giới thiệu)</string> + <string name="remote_ctrl_error_bad_state">Kết nối tới máy tính đang ở trong tình trạng không tốt</string> + <string name="remote_host_was_disconnected_title">Kết nối đã bị ngắt</string> + <string name="connect_via_link_or_qr">Kết nối qua đường dẫn / mã QR</string> + <string name="connect_plan_connect_to_yourself">Kết nối tới chính bạn?</string> + <string name="connection_request_sent">Yêu cầu kết nối đã được gửi!</string> + <string name="connect_via_link">Kết nối qua đường dẫn</string> + <string name="icon_descr_call_connecting">Đang kết nối cuộc gọi</string> + <string name="desktop_connection_terminated">Kết nối đã bị ngắt</string> + <string name="connecting_to_desktop">Đang kết nối tới máy tính</string> + <string name="v5_3_discover_join_groups_descr">- kết nối tới dịch vụ thư mục (BETA)! +\n- đánh dấu đã nhận (tối đa 20 thành viên). +\n- nhanh hơn và ổn định hơn.</string> + <string name="remote_ctrl_was_disconnected_title">Kết nối đã bị ngắt</string> + <string name="remote_host_error_bad_state"><![CDATA[Kết nối tới điện thoại <b>%s</b> đang ở trong tình trạng không tốt]]></string> + <string name="connect_to_desktop">Kết nối tới máy tính</string> + <string name="connection_timeout">Thời gian chờ kết nối</string> + <string name="connection_error">Lỗi kết nối</string> + <string name="info_row_connection">Kết nối</string> + <string name="connect_plan_connect_via_link">Kết nối qua đường dẫn?</string> + <string name="connection_local_display_name">kết nối %1$d</string> + <string name="display_name_connection_established">kết nối đã được tạo lập</string> + <string name="connect_with_contact_name_question">Kết nối với %1$s?</string> + <string name="connection_error_auth">Lỗi kết nối (AUTH)</string> + <string name="network_session_mode_entity">Kết nối</string> + <string name="callstatus_connecting">đang kết nối cuộc gọi…</string> + <string name="connect_via_contact_link">Kết nối qua địa chỉ liên lạc?</string> + <string name="connect_via_invitation_link">Kết nối qua đường dẫn dùng một lần?</string> + <string name="icon_descr_contact_checked">Liên hệ đã được kiểm tra</string> + <string name="receipts_section_contacts">Các liên hệ</string> + <string name="chat_preferences_contact_allows">Liên hệ cho phép</string> + <string name="contact_already_exists">Liên hệ đã tồn tại</string> + <string name="profile_update_event_contact_name_changed">liện hệ %1$s đã thay đổi thành %2$s</string> + <string name="alert_title_contact_connection_pending">Liên hệ này vẫn chưa được kết nối!</string> + <string name="notification_preview_somebody">Liên hệ ẩn:</string> + <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Liên hệ và tất cả tin nhắn sẽ bị xóa - quá trình này không thể hoàn tác được!</string> + <string name="notification_preview_mode_contact">Tên liên hệ</string> + <string name="status_contact_has_e2e_encryption">liên hệ có bảo mật đầu cuối</string> + <string name="status_contact_has_no_e2e_encryption">liên hệ không có bảo mật đầu cuối</string> + <string name="contact_preferences">Tùy chọn liên hệ</string> + <string name="copy_verb">Sao chép</string> + <string name="create_profile_button">Tạo</string> + <string name="contacts_can_mark_messages_for_deletion">Liên hệ có thể đánh dấu tin nhắn để xóa; bạn vẫn sẽ có thể xem được chúng.</string> + <string name="icon_descr_context">Biểu tượng ngữ cảnh</string> + <string name="continue_to_next_step">Tiếp tục</string> + <string name="contribute">Đóng góp</string> + <string name="copied">Đã sao chép vào bộ nhớ đệm</string> + <string name="core_version">Phiên bản lõi: v%s</string> + <string name="correct_name_to">Sửa tên thành %s?</string> + <string name="copy_error">Sao chép lỗi</string> + <string name="v5_4_incognito_groups_descr">Tạo nhóm bằng một hồ sơ ngẫu nhiên.</string> + <string name="v5_3_new_desktop_app_descr">Tạo hồ sơ mới trong ứng dụng trên máy tính. 💻</string> + <string name="cannot_share_message_alert_title">Không thể gửi tin nhắn</string> + <string name="smp_server_test_create_file">Tạo tệp</string> + <string name="create_address_and_let_people_connect">Tạo một địa chỉ để cho mọi người kết nối với bạn.</string> + <string name="create_group_link">Tạo đường dẫn nhóm</string> + <string name="button_create_group_link">Tạo đường dẫn</string> + <string name="info_row_created_at">Được tạo ra tại</string> + <string name="create_address">Tạo địa chỉ</string> + <string name="share_text_created_at">Được tạo ra tại: %s</string> + <string name="create_chat_profile">Tạo hồ sơ trò chuyện</string> + <string name="create_group_button">Tạo nhóm</string> + <string name="chat_theme_apply_to_dark_mode">Chủ đề tối</string> + <string name="current_passphrase">Mật khẩu hiện tại…</string> + <string name="database_encrypted">Cơ sở dữ liệu đã được mã hóa!</string> + <string name="database_encryption_will_be_updated">Mật khẩu mã hóa cơ sở dữ liệu sẽ đượ cập nhật và lưu trữ trong Keystore.</string> + <string name="item_info_current">(hiện tại)</string> + <string name="color_mode_dark">Tối</string> + <string name="dark_mode_colors">Các màu chế độ tối</string> + <string name="theme_dark">Tối</string> + <string name="agent_critical_error_title">Lỗi nghiêm trọng</string> + <string name="share_one_time_link">Tạo đường dẫn lời mời dùng một lần</string> + <string name="creating_link">Đang tạo đường dẫn…</string> + <string name="create_profile">Tạo hồ sơ</string> + <string name="v5_1_custom_themes_descr">Tùy chỉnh và chia sẻ các chủ đề màu sắc.</string> + <string name="v5_1_custom_themes">Các chủ đề tùy chỉnh</string> + <string name="maximum_supported_file_size">Kích cỡ tệp hiện đang được hỗ trợ tối đa là %1$s.</string> + <string name="create_your_profile">Tạo hồ sơ của bạn</string> + <string name="group_member_status_creator">Người sáng tạo</string> + <string name="create_secret_group_title">Tạo nhóm bí mật</string> + <string name="la_current_app_passcode">Mã truy cập hiện tại</string> + <string name="create_group">Tạo nhóm bí mật</string> + <string name="database_encryption_will_be_updated_in_settings">Mật khẩu mã hóa cơ sở dữ liệu sẽ được cập nhật và lưu trữ trong cài đặt.</string> + <string name="send_disappearing_message_custom_time">Thời lượng tùy chỉnh</string> + <string name="database_passphrase_will_be_updated">Mật khẩu mã hóa cơ sở dữ liệu sẽ được cập nhật.</string> + <string name="customize_theme_title">Tùy chỉnh chủ đề</string> + <string name="create_simplex_address">Tạo địa chỉ SimpleX</string> + <string name="create_another_profile_button">Tạo hồ sơ</string> + <string name="smp_server_test_create_queue">Tạo hàng đợi</string> + <string name="migrate_from_device_creating_archive_link">Tạo đường dẫn lưu trữ</string> + <string name="custom_time_picker_custom">tùy chỉnh</string> + <string name="dark_theme">Chủ đề tối</string> + <string name="database_downgrade">Hạ cấp cơ sở dữ liệu</string> + <string name="num_contacts_selected">%d liên hệ đã được chọn</string> + <string name="share_text_database_id">ID cơ sở dữ liệu: %d</string> + <string name="database_will_be_encrypted_and_passphrase_stored">Cơ sở dữ liệu sẽ được mã hóa và mật khẩu thì được lưu trữ trong Keystore.</string> + <string name="database_upgrade">Nâng cấp cơ sở dữ liệu</string> + <string name="mtr_error_no_down_migration">Phiên bản cơ sở dữ liệu mới hơn so với ứng dụng, nhưng không có hạ cấp cho: %s</string> + <string name="info_row_database_id">ID cơ sở dữ liệu</string> + <string name="custom_time_unit_days">ngày</string> + <string name="set_password_to_export_desc">Cơ sở dữ liệu được mã hóa bằng một mật khẩu ngẫu nhiên. Vui lòng đổi mật khẩu trước khi xuất dữ liệu.</string> + <string name="encrypted_with_random_passphrase">Cơ sở dữ liệu được mã hóa bằng một mật khẩu ngẫu nhiên, bạn có thể thay đổi nó.</string> + <string name="database_passphrase">Mật khẩu cơ sở dữ liệu</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Cơ sở dữ liệu sẽ được mã hóa và mật khẩu thì được lưu trữ trong phần cài đặt.</string> + <string name="ttl_days">%d ngày</string> + <string name="developer_options">Các ID cơ sở dữ liệu và tùy chọn cách ly truyền tải.</string> + <string name="ttl_d">%dd</string> + <string name="ttl_day">%d ngày</string> + <string name="database_will_be_encrypted">Cơ sở dữ liệu sẽ được mã hóa.</string> + <string name="database_migration_in_progress">Việc di chuyển cơ sở dữ liệu đang diễn ra. +\nQuá trình này có thể mất một vài phút.</string> + <string name="database_passphrase_and_export">Mật khẩu cơ sở dữ liệu và xuất dữ liệu</string> + <string name="passphrase_is_different">Mật khẩu cơ sở dữ liệu khác với mật khẩu được lưu trong Keystore.</string> + <string name="database_passphrase_is_required">Mật khẩu cơ sở dữ liệu là cần thiết để mở kết nối trò chuyện.</string> + <string name="database_error">Lỗi cơ sở dữ liệu</string> + <string name="delete_contact_menu_action">Xóa</string> + <string name="delete_address__question">Xóa địa chỉ?</string> + <string name="users_delete_question">Xóa hồ sơ trò chuyện?</string> + <string name="delete_messages__question">Xóa %d tin nhắn?</string> + <string name="delete_address">Xóa địa chỉ</string> + <string name="decentralized">Phi tập trung</string> + <string name="delete_verb">Xóa</string> + <string name="migrate_from_device_delete_database_from_device">Xóa cơ sở dữ liệu khỏi thiết bị này</string> + <string name="delete_after">Xóa sau</string> + <string name="rcv_direct_event_contact_deleted">đã xóa liên hệ</string> + <string name="info_row_debug_delivery">Gỡ lỗi truyền tải</string> + <string name="share_text_deleted_at">Đã xóa vào: %s</string> + <string name="image_decoding_exception_title">Lỗi chuyển đổi</string> + <string name="delete_and_notify_contact">Xóa và thông báo tới liên hệ</string> + <string name="button_delete_contact">Xóa liên hệ</string> + <string name="delete_group_menu_action">Xóa</string> + <string name="delete_database">Xóa cơ sở dữ liệu</string> + <string name="delete_files_and_media_all">Xóa tất cả các tệp</string> + <string name="delete_files_and_media_question">Xóa tệp và đa phương tiện?</string> + <string name="chat_preferences_default">mặc định (%s)</string> + <string name="rcv_group_event_group_deleted">đã xóa nhóm</string> + <string name="smp_server_test_delete_file">Xóa tệp</string> + <string name="delete_contact_question">Xóa liên hệ?</string> + <string name="info_row_deleted_at">Đã xóa vào</string> + <string name="decryption_error">Lỗi giải mã</string> + <string name="deleted_description">đã xóa</string> + <string name="delete_chat_profile">Xóa hồ sơ trò chuyện</string> + <string name="delete_chat_profile_question">Xóa hồ sơ trò chuyện?</string> + <string name="delete_member_message__question">Xóa tin nhắn thành viên?</string> + <string name="for_me_only">Xóa chỉ mình tôi</string> + <string name="button_delete_group">Xóa nhóm</string> + <string name="delete_pending_connection__question">Xóa kết nối đang chờ?</string> + <string name="delete_files_and_media_for_all_users">Xóa tệp cho tất cả các hồ sơ trò chuyện</string> + <string name="delete_link_question">Xóa đường dẫn?</string> + <string name="delete_profile">Xóa hồ sơ</string> + <string name="full_deletion">Xóa cho mọi người</string> + <string name="delete_link">Xóa đường dẫn</string> + <string name="delete_image">Xóa ảnh</string> + <string name="delete_messages">Xóa các tin nhắn</string> + <string name="delete_group_question">Xóa nhóm?</string> + <string name="delete_message__question">Xóa tin nhắn?</string> + <string name="desktop_app_version_is_incompatible">Phiên bản ứng dụng trên máy tính %s không tương thích với ứng dụng này.</string> + <string name="remote_ctrl_error_busy">Máy tính đang bận</string> + <string name="remote_ctrl_error_inactive">Máy tính đang không hoạt động</string> + <string name="remote_ctrl_error_disconnected">Máy tính đã bị ngắt kết nối</string> + <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 máy tính: quét mã QR được hiển thị ở trong ứng dụng, với <b>Quét mã QR</b>.]]></string> + <string name="smp_servers_delete_server">Xóa máy chủ</string> + <string name="smp_server_test_delete_queue">Xóa hàng đợi</string> + <string name="settings_developer_tools">Công cụ nhà phát triển</string> + <string name="settings_section_title_device">THIẾT BỊ</string> + <string name="developer_options_section">Tùy chọn cho nhà phát triển</string> + <string name="auth_device_authentication_is_disabled_turning_off">Xác thực thiết bị đã bị vô hiệu hóa. Tắt Khóa SimpleX.</string> + <string name="snd_error_relay">Lỗi máy chủ đích: %1$s</string> + <string name="delivery_receipts_title">Chỉ báo đã nhận!</string> + <string name="delivery_receipts_are_disabled">Chỉ báo đã nhận bị vô hiệu hóa!</string> + <string name="remote_ctrl_error_bad_invitation">Máy tính có mã mời sai</string> + <string name="simplex_link_mode_description">Mô tả</string> + <string name="delivery">Chuyển gửi</string> + <string name="desktop_devices">Các thiết bị máy tính</string> + <string name="desktop_device">Máy tính</string> + <string name="desktop_address">Địa chỉ máy tính</string> + <string name="remote_ctrl_error_bad_version">Máy tính có một phiên bản không được hỗ trợ. Vui lòng đảm bảo rằng bạn sử dụng cùng một phiên bản ở cả hai thiết bị.</string> + <string name="acknowledged">Đã xác nhận</string> + <string name="servers_info_subscriptions_connections_subscribed">Các kết nối đang hoạt động</string> + <string name="all_users">Tất cả hồ sơ</string> + <string name="app_check_for_updates_beta">Beta</string> + <string name="app_check_for_updates">Kiểm tra cập nhật</string> + <string name="servers_info_sessions_connecting">Đang kết nối</string> + <string name="smp_servers_configured">Các máy chủ SMP đã được cấu hình</string> + <string name="xftp_servers_configured">Các máy chủ XFTP đã được cấu hình</string> + <string name="app_check_for_updates_download_completed_title">Bản cập nhật ứng dụng đã được tải xuống</string> + <string name="app_check_for_updates_notice_title">Kiểm tra cập nhật</string> + <string name="servers_info_sessions_connected">Đã kết nối</string> + <string name="attempts_label">thử</string> + <string name="servers_info_connected_servers_section_header">Các máy chủ đã kết nối</string> + <string name="acknowledgement_errors">Lỗi xác nhận</string> + <string name="completed">Đã hoàn thành</string> + <string name="chunks_deleted">Các khối đã bị xóa</string> + <string name="chunks_downloaded">Các khối đã được tải xuống</string> + <string name="chunks_uploaded">Các khối đã được tải lên</string> + <string name="current_user">Hồ sơ hiện tại</string> + <string name="decryption_errors">lỗi giải mã</string> + <string name="servers_info_detailed_statistics">Thống kê chi tiết</string> + <string name="servers_info_details">Chi tiết</string> + <string name="connections">Các kết nối</string> + <string name="created">Đã tạo</string> + <string name="deleted">Đã xóa</string> + <string name="deletion_errors">Lỗi xóa</string> + <string name="rcv_group_events_count">%d sự kiện nhóm</string> + <string name="direct_messages_are_prohibited_in_group">Tin nhắn trực tiếp giữa các thành viên bị cấm trong nhóm này.</string> + <string name="total_files_count_and_size">%d tệp với tổng kích thước là %s</string> + <string name="mtr_error_different">phần di dời khác nhau trong ứng dụng/cơ sở dữ liệu: %s / %s</string> + <string name="direct_messages">Tin nhắn trực tiếp</string> + <string name="ttl_h">%dh</string> + <string name="ttl_hour">%d giờ</string> + <string name="ttl_hours">%d giờ</string> + <string name="v4_5_multiple_chat_profiles_descr">Tên, hình đại diện và cách ly truyền tải khác nhau.</string> + <string name="devices">Thiết bị</string> + <string name="conn_level_desc_direct">trực tiếp</string> + <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Xác thực thiết bị không được bật. Bạn có thể bật SimpleX Lock thông qua phần Cài đặt, sau khi bạn bật xác thực thiết bị.</string> + <string name="receipts_contacts_title_disable">Tắt chỉ báo đã nhận?</string> + <string name="timed_messages">Tin nhắn tự xóa</string> + <string name="disappearing_message">Tin nhắn tự xóa</string> + <string name="smp_server_test_disconnect">Ngắt kết nối</string> + <string name="receipts_contacts_disable_keep_overrides">Tắt (giữ thông tin ghi đè)</string> + <string name="share_text_disappears_at">Biến mất vào lúc: %s</string> + <string name="v4_4_disappearing_messages">Tin nhắn tự xóa</string> + <string name="receipts_contacts_disable_for_all">Tắt cho tất cả</string> + <string name="receipts_groups_disable_for_all">Tắt cho tất cả các nhóm</string> + <string name="info_row_disappears_at">Biến mất vào lúc</string> + <string name="disconnect_remote_host">Ngắt kết nối</string> + <string name="disable_notifications_button">Tắt thông báo</string> + <string name="auth_disable_simplex_lock">Tắt SimpleX Lock</string> + <string name="receipts_groups_disable_keep_overrides">Tắt (giữ thông tin ghi đè về nhóm)</string> + <string name="disappearing_prohibited_in_this_chat">Tin nhắn tự xóa bị cấm trong cuộc hội thoại này.</string> + <string name="no_call_on_lock_screen">Tắt</string> + <string name="receipts_groups_title_disable">Tắt chỉ báo đã nhận cho nhóm?</string> + <string name="send_receipts_disabled">đã bị tắt</string> + <string name="app_check_for_updates_disabled">Đã bị tắt</string> + <string name="app_check_for_updates_notice_disable">Tắt</string> + <string name="member_info_member_disabled">đã bị tắt</string> + <string name="disappearing_messages_are_prohibited">Tin nhắn tự xóa bị cấm.</string> + <string name="info_view_call_button">gọi</string> + <string name="info_view_connect_button">kết nối</string> + <string name="delete_contact_cannot_undo_warning">Liên hệ sẽ bị xóa - điều này không thể hoàn tác!</string> + <string name="conversation_deleted">Cuộc trò chuyện đã bị xóa!</string> + <string name="privacy_media_blur_radius">Làm mờ đa phương tiện</string> + <string name="cant_call_contact_alert_title">Không thể gọi liên hệ</string> + <string name="allow_calls_question">Cho phép thực hiện cuộc gọi?</string> + <string name="calls_prohibited_alert_title">Cuộc gọi bị cấm!</string> + <string name="cant_call_contact_connecting_wait_alert_text">Đang kết nối tới liên hệ, xin vui lòng đợi hoặc kiểm tra sau!</string> + <string name="cant_call_contact_deleted_alert_text">Liên hệ đã bị xóa.</string> + <string name="cant_send_message_to_member_alert_title">Không thể nhắn tin cho thành viên nhóm</string> + <string name="cant_call_member_alert_title">Không thể gọi thành viên nhóm</string> + <string name="confirm_delete_contact_question">Xác nhận xóa liên hệ?</string> + <string name="contact_deleted">Liên hệ đã bị xóa!</string> + <string name="deleted_chats">Các liên hệ được lưu trữ</string> + <string name="create_address_button">Tạo</string> + <string name="v6_0_privacy_blur">Mờ hình ảnh để riêng tư hơn.</string> + <string name="v6_0_connection_servers_status">Kiểm soát mạng của bạn</string> + <string name="v6_0_connection_servers_status_descr">Kết nối và trạng thái máy chủ.</string> + <string name="network_settings_title">Cài đặt nâng cao</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Bất kỳ ai cũng có thể tạo máy chủ.</string> + <string name="chat_database_exported_continue">Tiếp tục</string> + <string name="v6_0_connect_faster_descr">Kết nối nhanh hơn với bạn bè.</string> + <string name="chat_database_exported_title">Cơ sở dữ liệu trò chuyện đã được xuất</string> + <string name="v6_0_your_contacts_descr">Lưu trữ các liên hệ để trò chuyện sau.</string> + <string name="disconnect_desktop_question">Ngắt kết nối máy tính?</string> + <string name="v6_0_delete_many_messages_descr">Xóa tối đa 20 tin nhắn cùng một lúc.</string> + <string name="remote_ctrl_disconnected_with_reason">Đã ngắt kết nối với lý do:%s</string> + <string name="remote_host_disconnected_from"><![CDATA[Đã ngắt kết nối khỏi thiết bị di động <b>%s</b> với lý do: %s]]></string> + <string name="disconnect_remote_hosts">Ngắt kết nối các thiết bị di động</string> + <string name="icon_descr_server_status_disconnected">Đã ngắt kết nối</string> + <string name="proxy_destination_error_broker_host">Địa chỉ máy chủ đích của %1$s không tương thích với thiết lập máy chủ chuyển tiếp %2$s.</string> + <string name="proxy_destination_error_broker_version">Phiên bản máy chủ đích của %1$s không tương thích với máy chủ chuyển tiếp %2$s.</string> + <string name="delete_without_notification">Xóa mà không thông báo</string> + <string name="multicast_discoverable_via_local_network">Có thể tìm thấy qua mạng cục bộ</string> + <string name="delete_members_messages__question">Xóa %d tin nhắn của các thành viên?</string> + <string name="ttl_months">%d tháng</string> + <string name="ttl_m">%dm</string> + <string name="network_smp_proxy_mode_never_description">KHÔNG sử dụng định tuyến riêng tư.</string> + <string name="v5_3_discover_join_groups">Khám phá và tham gia nhóm</string> + <string name="ttl_mth">%dmth</string> + <string name="disable_sending_recent_history">Không gửi lịch sử đến các thành viên mới.</string> + <string name="la_minutes">%d phút</string> + <string name="network_smp_proxy_fallback_prohibit_description">KHÔNG gửi tin nhắn trực tiếp, kể cả khi máy chủ của bạn hoặc máy chủ đích không hỗ trợ định tuyến riêng tư.</string> + <string name="ttl_month">%d tháng</string> + <string name="ttl_min">%d phút</string> + <string name="blocked_items_description">%d tin nhắn bị chặn</string> + <string name="blocked_by_admin_items_description">%d tin nhắn bị chặn bởi quản trị viên</string> + <string name="marked_deleted_items_description">%d tin nhắn được đánh dấu là đã xóa</string> + <string name="display_name_cannot_contain_whitespace">Tên hiển thị không thể chứa khoảng trắng.</string> + <string name="discover_on_network">Khám phá qua mạng cục bộ</string> + <string name="migrate_to_device_download_failed">Tải về không thành công</string> + <string name="dont_create_address">Không tạo địa chỉ</string> + <string name="dont_show_again">Không hiển thị lại</string> + <string name="smp_server_test_download_file">Tải về tệp tin</string> + <string name="migrate_to_device_downloading_archive">Đang tải về kho lưu trữ</string> + <string name="downgrade_and_open_chat">Hạ cấp và mở kết nối trò chuyện</string> + <string name="download_file">Tải về</string> + <string name="dont_enable_receipts">Không bật</string> + <string name="servers_info_downloaded">Đã tải về</string> + <string name="app_check_for_updates_download_started">Đang tải về bản cập nhật ứng dụng, đừng đóng ứng dụng</string> + <string name="downloaded_files">Các tệp đã tải về</string> + <string name="download_errors">Lỗi tải về</string> + <string name="status_e2e_encrypted">mã hóa đầu cuối</string> + <string name="v6_0_upgrade_app_descr">Tải xuống các phiên bản mới từ GitHub.</string> + <string name="ttl_sec">%d giây</string> + <string name="ttl_s">%ds</string> + <string name="migrate_to_device_downloading_details">Đang tải xuống chi tiết đường dẫn</string> + <string name="failed_to_create_user_duplicate_title">Tên hiển thị trùng lặp!</string> + <string name="ttl_week">%d tuần</string> + <string name="ttl_weeks">%d tuần</string> + <string name="la_seconds">%d giây</string> + <string name="integrity_msg_duplicate">tin nhắn trùng lặp</string> + <string name="ttl_w">%dw</string> + <string name="encrypted_audio_call">cuộc gọi thoại mã hóa đầu cuối</string> + <string name="duplicates_label">các bản sao</string> + <string name="app_check_for_updates_button_download">Tải xuống %s (%s)</string> + <string name="feature_enabled_for_contact">đã bật cho liên hệ</string> + <string name="audio_device_earpiece">Tai nghe</string> + <string name="receipts_groups_enable_for_all">Bật cho tất cả các nhóm</string> + <string name="edit_image">Chỉnh sửa hình ảnh</string> + <string name="icon_descr_edited">đã chỉnh sửa</string> + <string name="receipts_contacts_enable_for_all">Bật cho tất cả</string> + <string name="feature_enabled">đã bật</string> + <string name="feature_enabled_for_you">đã bật cho bạn</string> + <string name="feature_enabled_for">Đã bật cho</string> + <string name="enable_camera_access">Cho phép truy cập camera</string> + <string name="encrypted_video_call">cuộc gọi video mã hóa đầu cuối</string> + <string name="allow_accepting_calls_from_lock_screen">Cho phép nhận cuộc gọi từ màn hình khóa thông qua Cài đặt.</string> + <string name="enable_receipts_all">Cho phép</string> + <string name="button_edit_group_profile">Chỉnh sửa hồ sơ nhóm</string> + <string name="enable_automatic_deletion_question">Cho phép xóa tin nhắn tự động?</string> + <string name="edit_verb">Chỉnh sửa</string> + <string name="icon_descr_email">Thư điện tử</string> + <string name="ci_status_other_error">Lỗi: %1$s</string> + <string name="error">Lỗi</string> + <string name="alert_text_encryption_renegotiation_failed">Tái đàm phán mã hóa thất bại.</string> + <string name="conn_event_ratchet_sync_ok">mã hóa ok</string> + <string name="snd_conn_event_ratchet_sync_allowed">cho phép tái đàm phán mã hóa với %s</string> + <string name="snd_conn_event_ratchet_sync_required">cần tái đàm phán mã hóa cho %s</string> + <string name="receipts_contacts_title_enable">Bật chỉ báo đã nhận?</string> + <string name="v5_3_encrypt_local_files">Mã hóa các tệp và đa phương tiện được lưu trữ</string> + <string name="error_alert_title">Lỗi</string> + <string name="call_service_notification_end_call">Kết thúc cuộc gọi</string> + <string name="v5_6_quantum_resistant_encryption_descr">Bật trong cuộc trò chuyện trực tiếp (BETA)!</string> + <string name="enabled_self_destruct_passcode">Bật mã truy cập tự hủy</string> + <string name="encrypt_database_question">Mã hóa cơ sở dữ liệu?</string> + <string name="encrypted_database">Cơ sở dữ liệu được mã hóa</string> + <string name="encryption_renegotiation_error">Lỗi tái đàm phán mã hóa</string> + <string name="migrate_to_device_enter_passphrase">Nhập mật khẩu</string> + <string name="receipts_contacts_enable_keep_overrides">Bật (giữ thông tin ghi đè)</string> + <string name="encrypt_database">Mã hóa</string> + <string name="conn_event_ratchet_sync_agreed">mã hóa đã đồng nhất</string> + <string name="conn_event_ratchet_sync_allowed">cho phép tái đàm phán mã hóa</string> + <string name="conn_event_ratchet_sync_required">cần tái đàm phán mã hóa</string> + <string name="network_option_enable_tcp_keep_alive">Bật TCP keep-alive</string> + <string name="enter_password_to_show">Nhập mật khẩu trong tìm kiếm</string> + <string name="enter_this_device_name">Nhập tên của thiết bị này…</string> + <string name="auth_enable_simplex_lock">Bật SimpleX Lock</string> + <string name="enter_welcome_message">Nhập lời chào…</string> + <string name="la_enter_app_passcode">Nhập mã truy cập</string> + <string name="icon_descr_server_status_error">Lỗi</string> + <string name="enter_welcome_message_optional">Nhập lời chào... (không bắt buộc)</string> + <string name="callstate_ended">đã kết thúc</string> + <string name="snd_conn_event_ratchet_sync_agreed">mã hóa đã đồng nhất cho %s</string> + <string name="snd_conn_event_ratchet_sync_ok">mã hóa ok cho %s</string> + <string name="server_error">lỗi</string> + <string name="smp_servers_enter_manually">Nhập máy chủ thủ công</string> + <string name="receipts_groups_enable_keep_overrides">Bật (giữ thông tin ghi đè nhóm)</string> + <string name="enter_correct_passphrase">Nhập đúng mật khẩu.</string> + <string name="enter_passphrase">Nhập mật khẩu…</string> + <string name="encrypt_local_files">Mã hóa các tệp cục bộ</string> + <string name="enable_self_destruct">Bật tự hủy</string> + <string name="receipts_groups_title_enable">Bật chỉ báo đã nhận cho các nhóm?</string> + <string name="group_display_name_field">Nhập tên nhóm:</string> + <string name="display_name">Nhập tên của bạn:</string> + <string name="enable_lock">Bật khóa</string> + <string name="servers_info_modal_error_title">Lỗi</string> + <string name="failed_to_create_user_title">Lỗi tạo hồ sơ!</string> + <string name="error_changing_message_deletion">Lỗi thay đổi cài đặt</string> + <string name="error_blocking_member_for_all">Lỗi chặn thành viên cho tất cả</string> + <string name="error_adding_members">Lỗi thêm thành viên</string> + <string name="error_creating_message">Lỗi tạo tin nhắn</string> + <string name="error_creating_address">Lỗi tạo địa chỉ</string> + <string name="error_accepting_contact_request">Lỗi chấp nhận yêu cầu liên hệ</string> + <string name="error_changing_address">Lỗi thay đổi địa chỉ</string> + <string name="error_deleting_database">Lỗi xóa cơ sở dữ liệu trò chuyện</string> + <string name="error_creating_member_contact">Lỗi tạo liên hệ thành viên</string> + <string name="error_aborting_address_change">Lỗi hủy bỏ thay đổi địa chỉ</string> + <string name="error_creating_link_for_group">Lỗi tạo đường dẫn nhóm</string> + <string name="error_changing_role">Lỗi thay đổi chức vụ</string> + <string name="smp_proxy_error_connecting">Lỗi kết nối đến máy chủ chuyển tiếp %1$s. Vui lòng thử lại sau.</string> + <string name="error_deleting_contact">Lỗi xóa liên hệ</string> + <string name="error_deleting_group">Lỗi xóa nhóm</string> + <string name="error_deleting_link_for_group">Lỗi xóa đường dẫn nhóm</string> + <string name="error_deleting_contact_request">Lỗi xóa yêu cầu liên hệ</string> + <string name="migrate_from_device_error_deleting_database">Lỗi xóa cơ sở dữ liệu</string> + <string name="error_deleting_pending_contact_connection">Lỗi xóa kết nối liên hệ đang chờ xử lý</string> + <string name="migrate_from_device_error_exporting_archive">Lỗi xuất cơ sở dữ liệu trò chuyện</string> + <string name="error_encrypting_database">Lỗi mã hóa cơ sở dữ liệu</string> + <string name="error_enabling_delivery_receipts">Lỗi bật chỉ báo đã nhận!</string> + <string name="error_deleting_note_folder">Lỗi xóa ghi chú riêng tư</string> + <string name="error_deleting_user">Lỗi xóa hồ sơ người dùng</string> + <string name="error_importing_database">Lỗi nhập cơ sở dữ liệu trò chuyện</string> + <string name="error_exporting_chat_database">Lỗi xuất cơ sở dữ liệu trò chuyện</string> + <string name="migrate_to_device_error_downloading_archive">Lỗi tải xuống kho lưu trữ</string> + <string name="error_initializing_web_view">Lỗi khởi tạo WebView. Cập nhật hệ điều hành của bạn lên phiên bản mới. Vui lòng liên hệ với nhà phát triển. +\nLỗi: %s</string> + <string name="error_removing_member">Lỗi xóa thành viên</string> + <string name="error_with_info">Lỗi: %s</string> + <string name="unable_to_open_browser_title">Lỗi mở trình duyệt</string> + <string name="error_joining_group">Lỗi tham gia nhóm</string> + <string name="error_loading_details">Lỗi tải thông tin chi tiết</string> + <string name="error_receiving_file">Lỗi nhận tệp</string> + <string name="error_saving_group_profile">Lỗi lưu hồ sơ nhóm</string> + <string name="error_saving_file">Lỗi lưu tệp</string> + <string name="error_loading_smp_servers">Lỗi tải máy chủ SMP</string> + <string name="error_loading_xftp_servers">Lỗi tải máy chủ XFTP</string> + <string name="servers_info_reconnect_server_error">Lỗi kết nối lại máy chủ</string> + <string name="servers_info_reconnect_servers_error">Lỗi kết nối lại máy chủ</string> + <string name="servers_info_sessions_errors">Lỗi</string> + <string name="servers_info_reset_stats_alert_error_title">Lỗi khôi phục thống kê</string> + <string name="error_updating_link_for_group">Lỗi cập nhật đường dẫn nhóm</string> + <string name="error_showing_message">lỗi hiển thị tin nhắn</string> + <string name="error_showing_content">lỗi hiển thị nội dung</string> + <string name="error_saving_ICE_servers">Lỗi lưu máy chủ ICE</string> + <string name="error_saving_xftp_servers">Lỗi lưu máy chủ XFTP</string> + <string name="error_saving_smp_servers">Lỗi lưu máy chủ SMP</string> + <string name="error_sending_message">Lỗi gửi tin nhắn</string> + <string name="error_starting_chat">Lỗi khởi động kết nối trò chuyện</string> + <string name="error_stopping_chat">Lỗi dừng kết nối trò chuyện</string> + <string name="error_showing_desktop_notification">Lỗi hiển thị thông báo, liên hệ với nhà phát triển.</string> + <string name="error_saving_user_password">Lỗi lưu mật khẩu người dùng</string> + <string name="migrate_from_device_error_saving_settings">Lỗi lưu cài đặt</string> + <string name="error_sending_message_contact_invitation">Lỗi gửi lời mời</string> + <string name="failed_to_active_user_title">Lỗi chuyển đổi hồ sơ!</string> + <string name="error_synchronizing_connection">Lỗi đồng bộ kết nối</string> + <string name="error_setting_address">Lỗi cài đặt địa chỉ</string> + <string name="v5_2_disappear_one_message_descr">Dù đã tắt trong cuộc trò chuyện.</string> + <string name="error_setting_network_config">Lỗi cập nhật cấu hình mạng</string> + <string name="error_updating_user_privacy">Lỗi cập nhật quyền riêng tư người dùng</string> + <string name="icon_descr_expand_role">Mở rộng chọn chức vụ</string> + <string name="settings_section_title_experimenta">THỬ NGHIỆM</string> + <string name="expand_verb">Mở rộng</string> + <string name="exit_without_saving">Thoát mà không lưu</string> + <string name="expired_label">đã hết hạn</string> + <string name="migrate_from_device_error_verifying_passphrase">Lỗi xác thực mật khẩu:</string> + <string name="possible_slow_function_desc">Quá trình thực hiện chức năng mất quá nhiều thời gian: %1$dgiây: %2$s</string> + <string name="settings_experimental_features">Tính năng thử nghiệm</string> + <string name="export_database">Xuất cơ sở dữ liệu</string> + <string name="migrate_from_device_error_uploading_archive">Lỗi tải lên kho lưu trữ</string> + <string name="migrate_from_device_exported_file_doesnt_exist">Tập tin đã xuất không tồn tại</string> + <string name="settings_section_title_files">TẬP TIN</string> + <string name="failed_to_parse_chats_title">Không thể tải các cuộc trò chuyện</string> + <string name="file_error_no_file">Không tìm thấy tệp - có thể tập tin đã bị xóa và hủy bỏ.</string> + <string name="file_error">Lỗi tệp</string> + <string name="export_theme">Xuất chủ đề</string> + <string name="failed_to_parse_chat_title">Không thể tải cuộc trò chuyện</string> + <string name="choose_file">Tập tin</string> + <string name="v5_0_large_files_support_descr">Nhanh chóng và không cần phải đợi người gửi hoạt động!</string> + <string name="file_not_found">Không tìm thấy tệp</string> + <string name="file_with_path">Tập tin: %s</string> + <string name="v5_4_better_groups_descr">Tham gia nhanh chóng hơn và xử lý tin nhắn ổn định hơn.</string> + <string name="servers_info_files_tab">Tập tin</string> + <string name="favorite_chat">Yêu thích</string> + <string name="icon_descr_file">Tập tin</string> + <string name="file_error_relay">Lỗi máy chủ tệp: %1$s</string> + <string name="info_row_file_status">Trạng thái tệp</string> + <string name="files_and_media_not_allowed">Tệp và đa phương tiện không được cho phép</string> + <string name="files_are_prohibited_in_group">Tệp và đa phương tiện bị cấm.</string> + <string name="revoke_file__message">Tệp sẽ bị xóa khỏi máy chủ.</string> + <string name="files_and_media_section">Tệp & đa phương tiện</string> + <string name="files_and_media_prohibited">Tệp và đa phương tiện bị cấm!</string> + <string name="file_will_be_received_when_contact_completes_uploading">Tệp sẽ được nhận khi liên hệ của bạn hoàn tất quá trình tải lên.</string> + <string name="files_and_media">Tệp và đa phương tiện</string> + <string name="migrate_to_device_file_delete_or_link_invalid">Tệp đã bị xóa hoặc đường dẫn không hợp lệ</string> + <string name="file_saved">Tệp đã được lưu</string> + <string name="file_will_be_received_when_contact_is_online">Tệp sẽ được nhận khi liên hệ của bạn hoạt động, vui lòng chờ hoặc kiểm tra lại sau!</string> + <string name="share_text_file_status">Trạng thái tệp: %s</string> + <string name="wallpaper_scale_fill">Lấp đầy</string> + <string name="settings_section_title_chat_database">CƠ SỞ DỮ LIỆU TRÒ CHUYỆN</string> + <string name="switching_profile_error_title">Lỗi chuyển đổi hồ sơ</string> + <string name="v5_2_favourites_filter_descr">Lọc các cuộc hội thoại chưa đọc và các cuộc hội thoại yêu thích.</string> + <string name="v5_1_message_reactions_descr">Cuối cùng, chúng ta đã có chúng! 🚀</string> + <string name="migrate_to_device_finalize_migration">Hoàn tất quá trình di chuyển ở thiết bị khác.</string> + <string name="migrate_from_device_finalize_migration">Hoàn tất quá trình di chuyển</string> + <string name="v5_2_favourites_filter">Tìm các cuộc trò chuyện nhanh hơn</string> + <string name="permissions_find_in_settings_and_grant">Tìm kiếm quyền này trong phần cài đặt Android và cấp quyền theo cách thủ công.</string> + <string name="fix_connection_confirm">Sửa</string> + <string name="wallpaper_scale_fit">Kích thước phù hợp</string> + <string name="forward_chat_item">Chuyển tiếp</string> + <string name="forwarded_from_chat_item_info_title">Đã được chuyển tiếp từ</string> + <string name="forwarded_description">đã được chuyển tiếp</string> + <string name="snd_error_proxy_relay">Máy chủ chuyển tiếp: %1$s +\nLỗi máy chủ đích: %2$s</string> + <string name="snd_error_proxy">Máy chủ chuyển tiếp: %1$s +\nLỗi: %2$s</string> + <string name="v5_7_forward">Chuyển tiếp và lưu tin nhắn</string> + <string name="fix_connection_not_supported_by_group_member">Chức năng sửa không hỗ trợ bởi thành viên nhóm</string> + <string name="for_everybody">Cho tất cả mọi người</string> + <string name="forwarded_chat_item_info_tab">Đã được chuyển tiếp</string> + <string name="network_proxy_auth_mode_no_auth">Không sử dụng thông tin đăng nhập với proxy.</string> + <string name="fix_connection">Sửa kết nối</string> + <string name="fix_connection_question">Sửa kết nối?</string> + <string name="fix_connection_not_supported_by_contact">Chức năng sửa không hỗ trợ bởi liên hệ</string> + <string name="v5_2_fix_encryption_descr">Sửa mã hóa sau khi hồi phục dữ liệu dự phòng.</string> + <string name="network_proxy_incorrect_config_title">Lỗi lưu proxy</string> + <string name="icon_descr_flip_camera">Đổi máy ảnh</string> + <string name="appearance_font_size">Kích thước font</string> + <string name="n_other_file_errors">%1$d lỗi tệp khác.</string> + <string name="error_forwarding_messages">Lỗi chuyển tiếp tin nhắn</string> + <string name="forward_files_failed_to_receive_desc">%1$d tệp tải không thành công.</string> + <string name="forward_files_missing_desc">%1$d tệp đã bị xóa.</string> + <string name="forward_files_not_accepted_desc">%1$d tệp đã không được tải xuống.</string> + <string name="forward_files_not_accepted_receive_files">Tải xuống</string> + <string name="forward_alert_title_messages_to_forward">Chuyển tiếp %1$s tin nhắn?</string> + <string name="forward_multiple">Chuyển tiếp tin nhắn…</string> + <string name="n_file_errors">%1$d lỗi tệp: +\n%2$s</string> + <string name="forward_files_in_progress_desc">%1$d tệp đang được tải xuống.</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s tin nhắn không được chuyển tiếp</string> + <string name="compose_forward_messages_n">Đang chuyển tiếp %1$s tin nhắn</string> + <string name="proxy_destination_error_failed_to_connect">Máy chủ chuyển tiếp %1$s không thể kết nối tới máy chủ đích %2$s. Vui lòng thử lại sau.</string> + <string name="smp_proxy_error_broker_host">Địa chỉ máy chủ chuyển tiếp không tương thích với cài đặt mạng: %1$s.</string> + <string name="smp_proxy_error_broker_version">Phiên bản máy chủ chuyển tiếp không tương thích với cài đặt mạng: %1$s.</string> + <string name="section_title_for_console">CHO CONSOLE</string> + <string name="forward_message">Chuyển tiếp tin nhắn…</string> + <string name="v4_6_reduced_battery_usage">Giảm thiểu sử dụng pin hơn nữa</string> + <string name="forward_alert_forward_messages_without_files">Chuyển tiếp tin nhắn mà không có tệp?</string> + <string name="wallpaper_preview_hello_alice">Chào buổi chiều!</string> + <string name="v4_4_french_interface">Giao diện tiếng Pháp</string> + <string name="found_desktop">Đã tìm thấy máy tính</string> + <string name="simplex_link_mode_full">Toàn bộ đường dẫn</string> + <string name="from_gallery_button">Từ Thư viện</string> + <string name="full_name__field">Tên đầy đủ:</string> + <string name="group_is_decentralized">Tuyệt đối phi tập trung - chỉ hiển thị cho thành viên.</string> + <string name="wallpaper_preview_hello_bob">Chào buổi sáng!</string> + <string name="group_member_status_group_deleted">nhóm đã bị xóa</string> + <string name="group_members_can_add_message_reactions">Các thành viên có thể thả cảm xúc tin nhắn.</string> + <string name="group_members_can_delete">Các thành viên có thể xóa các tin nhắn đã gửi mà không thể phục hồi lại. (24 giờ)</string> + <string name="group_members_can_send_voice">Các thành viên có thể gửi tin nhắn thoại.</string> + <string name="permissions_required">Cấp quyền</string> + <string name="group_link">Đường dẫn nhóm</string> + <string name="info_row_group">Nhóm</string> + <string name="group_full_name_field">Tên đầy đủ nhóm:</string> + <string name="group_members_can_send_disappearing">Các thành viên có thể gửi tin nhắn tự xóa.</string> + <string name="group_members_can_send_files">Các thành viên có thể gửi tệp và đa phương tiện.</string> + <string name="group_members_can_send_simplex_links">Các thành viên có thể gửi đường dẫn SimpleX.</string> + <string name="icon_descr_group_inactive">Nhóm không hoạt động</string> + <string name="connect_plan_group_already_exists">Nhóm đã tồn tại rồi!</string> + <string name="group_members_can_send_dms">Các thành viên có thể gửi tin nhắn trực tiếp.</string> + <string name="v4_2_group_links">Đường dẫn nhóm</string> + <string name="group_invitation_expired">Lời mời nhóm đã hết hạn</string> + <string name="alert_message_group_invitation_expired">Lời mới nhóm không còn có hiệu lực, nó đã bị xóa bởi người gửi.</string> + <string name="permissions_grant">Cho phép thực hiện cuộc gọi</string> + <string name="permissions_grant_in_settings">Cấp quyền trong cài đặt</string> + <string name="hide_notification">Ẩn</string> + <string name="icon_descr_hang_up">Ngắt kết nối</string> + <string name="notification_preview_mode_hidden">Ẩn</string> + <string name="hidden_profile_password">Mật khẩu hồ sơ ẩn</string> + <string name="group_profile_is_stored_on_members_devices">Hồ sơ nhóm được lưu trữ tại thiết bị của thành viên, không lưu trữ trên máy chủ.</string> + <string name="snd_group_event_group_profile_updated">hồ sơ nhóm đã được cập nhật</string> + <string name="icon_descr_help">trợ giúp</string> + <string name="hide_dev_options">Ẩn:</string> + <string name="email_invite_body">Xin chào! +\nKết nối với tôi qua SimpleX Chat: %s</string> + <string name="hide_profile">Ẩn hồ sơ</string> + <string name="settings_section_title_help">TRỢ GIÚP</string> + <string name="delete_group_for_all_members_cannot_undo_warning">Nhóm sẽ bị xóa cho tất cả các thành viên - điều này không thể hoàn tác!</string> + <string name="delete_group_for_self_cannot_undo_warning">Nhóm sẽ bị xóa cho bạn - điều này không thể hoàn tác!</string> + <string name="group_preferences">Tùy chọn nhóm</string> + <string name="recent_history_is_not_sent_to_new_members">Lịch sử không được gửi đến các thành viên mới.</string> + <string name="v4_3_improved_privacy_and_security_desc">Ẩn màn hình ứng dụng trong danh sách các ứng dụng gần đây.</string> + <string name="v4_6_group_moderation">Quản trị nhóm</string> + <string name="v4_6_group_welcome_message">Lời chào nhóm</string> + <string name="audio_device_wired_headphones">Tai nghe</string> + <string name="notification_display_mode_hidden_desc">Ẩn liên hệ và tin nhắn</string> + <string name="hide_verb">Ẩn</string> + <string name="user_hide">Ẩn</string> + <string name="edit_history">Lịch sử</string> + <string name="alert_title_no_group">Không tìm thấy nhóm!</string> + <string name="v4_6_hidden_chat_profiles">Hồ sơ trò chuyện ẩn</string> + <string name="how_to_use_simplex_chat">Cách sử dụng</string> + <string name="how_it_works">Cách thức hoạt động</string> + <string name="how_simplex_works">Cách thức SimpleX hoạt động</string> + <string name="how_to">Cách làm</string> + <string name="error_initializing_web_view_wrong_arch">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</string> + <string name="custom_time_unit_hours">giờ</string> + <string name="host_verb">Lưu trữ</string> + <string name="network_session_mode_session">Phiên làm việc trên ứng dụng</string> + <string name="how_to_use_markdown">Cách sử dụng markdown</string> + <string name="call_desktop_permission_denied_chrome">Nhấn nút thông tin gần trường địa chỉ để cho phép sử dụng microphone.</string> + <string name="v6_1_better_calls">Trải nghiệm cuộc gọi tốt hơn</string> + <string name="v6_1_better_security">Bảo mật hơn ✅</string> + <string name="v6_1_better_user_experience">Trải nghiệm người dùng tuyệt vời hơn</string> + <string name="v6_1_customizable_message_descr">Hình dạng tin nhắn có thể tùy chỉnh được.</string> + <string name="v6_1_message_dates_descr">Mô tả thời gian tin nhắn tốt hơn.</string> + <string name="v6_1_delete_many_messages_descr">Xóa hay kiểm duyệt tối đa 200 tin nhắn.</string> + <string name="settings_message_shape_corner">Góc</string> + <string name="v6_1_forward_many_messages_descr">Chuyển tiếp tối đa 20 tin nhắn cùng một lúc.</string> + <string name="how_to_use_your_servers">Cách sử dụng máy chủ của bạn</string> + <string name="v5_5_new_interface_languages">Giao diện Hungary và Thổ Nhĩ Kỳ</string> + <string name="immune_to_spam_and_abuse">Miễn nhiễm với tin nhắn rác</string> + <string name="import_database_question">Nhập cơ sở dữ liệu trò chuyện?</string> + <string name="if_you_enter_self_destruct_code">Nếu bạn nhập mã tự hủy của mình khi mở ứng dụng:</string> + <string name="enter_one_ICE_server_per_line">Máy chủ ICE (một dòng mỗi máy)</string> + <string name="if_you_enter_passcode_data_removed">Nếu bạn nhập mã truy cập này khi mở ứng dụng, tất cả dữ liệu ứng dụng sẽ bị xóa vĩnh viễn!</string> + <string name="image_will_be_received_when_contact_is_online">Hình ảnh sẽ được nhận khi liên hệ của bạn trực tuyến, xin vui lòng chờ hoặc kiểm tra lại sau!</string> + <string name="la_immediately">Ngay lập tức</string> + <string name="image_will_be_received_when_contact_completes_uploading">Hình ảnh sẽ được nhận khi liên hệ của bạn hoàn thành việc tải lên.</string> + <string name="gallery_image_button">Hình ảnh</string> + <string name="if_you_cant_meet_in_person">Nếu bạn không thể gặp mặt trực tiếp, cho liên hệ của bạn xem mã QR trong một cuộc gọi video, hoặc chia sẻ đường dẫn.</string> + <string name="icon_descr_image_snd_complete">Hình ảnh đã được gửi</string> + <string name="image_descr">Hình ảnh</string> + <string name="image_saved">Hình ảnh đã được lưu vào Thư viện</string> + <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Nếu bạn nhận được đường dẫn mời SimpleX Chat, bạn có thể mở nó trong trình duyệt của mình:</string> + <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Nếu bạn không thể gặp mặt trực tiếp, bạn có thể <b>quét mã QR trong cuộc gọi video</b>, hoặc liên hệ của bạn có thể chia sẻ một đường dẫn mời.]]></string> + <string name="network_disable_socks_info">Nếu bạn xác nhận, các máy chủ truyền tin nhắn sẽ có thể biết địa chỉ IP, và nhà cung cấp của bạn - máy chủ nào mà bạn đang kết nối.</string> + <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Nếu bạn chọn từ chối người gửi sẽ KHÔNG được thông báo.</string> + <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Nếu bạn không thể gặp mặt trực tiếp, <b>cho liên hệ của bạn xem mã QR trong cuộc gọi video</b>, hoặc chia sẻ đường dẫn.]]></string> + <string name="import_database_confirmation">Nhập</string> + <string name="ignore">Bỏ qua</string> + <string name="member_info_member_inactive">không hoạt động</string> + <string name="migrate_to_device_import_failed">Nhập dữ liệu không thành công</string> + <string name="v5_5_message_delivery">Đã cải thiện việc chuyển gửi tin nhắn</string> + <string name="import_theme_error">Lỗi nhập chủ đề</string> + <string name="settings_section_title_incognito">Chế độ ẩn danh</string> + <string name="incognito">Ẩn danh</string> + <string name="migrate_to_device_importing_archive">Đang nhập dữ liệu từ kho lưu trữ</string> + <string name="import_theme">Nhập chủ đề</string> + <string name="v4_3_improved_server_configuration">Đã cải thiện cấu hình máy chủ</string> + <string name="v5_4_incognito_groups">Nhóm ẩn danh</string> + <string name="incognito_info_protects">Chế độ ẩn danh bảo vệ sự riêng tư của bạn bằng cách sử dụng một hồ sơ ngẫu nhiên mới với mỗi liên hệ.</string> + <string name="v5_8_message_delivery">Đã cải thiện việc chuyển gửi tin nhắn</string> + <string name="import_database">Nhập cơ sở dữ liệu</string> + <string name="v5_7_call_sounds">Âm thanh trong cuộc gọi</string> + <string name="v4_3_improved_privacy_and_security">Nâng cao bảo mật và sự riêng tư</string> + <string name="description_via_one_time_link_incognito">ẩn danh qua đường dẫn dùng một lần</string> + <string name="info_menu">Thông tin</string> + <string name="conn_level_desc_indirect">gián tiếp (%1$s)</string> + <string name="migrate_from_device_chat_should_be_stopped">Để tiếp tục, hãy ngắt kết nối trò chuyện.</string> + <string name="install_simplex_chat_for_terminal">Cài đặt SimpleX Chat cho cửa sổ câu lệnh</string> + <string name="description_via_contact_address_link_incognito">ẩn danh qua đường dẫn địa chỉ liên lạc</string> + <string name="incoming_audio_call">Cuộc gọi thoại đến</string> + <string name="initial_member_role">Chức vụ ban đầu</string> + <string name="description_via_group_link_incognito">ẩn danh qua đường dẫn nhóm</string> + <string name="incorrect_code">Mã bảo mật không đúng!</string> + <string name="in_reply_to">Trả lời đến</string> + <string name="incompatible_database_version">Phiên bản cơ sở dữ liệu không tương thích</string> + <string name="onboarding_notifications_mode_service">Ngay lập tức</string> + <string name="incorrect_passcode">Mã truy cập không đúng</string> + <string name="v6_0_increase_font_size">Tăng cỡ chữ.</string> + <string name="new_desktop"><![CDATA[<i>(mới)</i>]]></string> + <string name="app_check_for_updates_installed_successfully_title">Đã cài đặt thành công</string> + <string name="app_check_for_updates_button_install">Cài đặt cập nhật</string> + <string name="incoming_video_call">Cuộc gọi video đến</string> + <string name="desktop_incompatible_version">Phiên bản không tương thích</string> + <string name="theme_colors_section_title">MÀU SẮC GIAO DIỆN</string> + <string name="group_member_status_invited">đã được mời</string> + <string name="error_parsing_uri_title">Đường dẫn không hợp lệ</string> + <string name="invalid_chat">cuộc trò chuyện không hợp lệ</string> + <string name="invalid_data">dữ liệu không hợp lệ</string> + <string name="invalid_message_format">định dạng tin nhắn không hợp lệ</string> + <string name="action_button_add_members">Mời</string> + <string name="alert_title_group_invitation_expired">Lời mời đã hết hạn!</string> + <string name="invalid_contact_link">Đường dẫn không hợp lệ!</string> + <string name="invalid_file_link">Đường dẫn không hợp lệ</string> + <string name="failed_to_create_user_invalid_title">Tên hiển thị không hợp lệ!</string> + <string name="invalid_connection_link">Đường dẫn kết nối không hợp lệ</string> + <string name="service_notifications">Thông báo tức thời!</string> + <string name="invalid_QR_code">Mã QR không hợp lệ</string> + <string name="icon_descr_instant_notifications">Thông báo tức thời</string> + <string name="service_notifications_disabled">Thông báo tức thời đã bị tắt!</string> + <string name="non_content_uri_alert_title">Đường dẫn tệp không hợp lệ</string> + <string name="agent_internal_error_title">Lỗi nội bộ</string> + <string name="invalid_qr_code">Mã QR không hợp lệ</string> + <string name="smp_servers_invalid_address">Địa chỉ máy chủ không hợp lệ!</string> + <string name="invalid_migration_confirmation">Xác nhận di dời không hợp lệ</string> + <string name="invalid_name">Tên không hợp lệ!</string> + <string name="invite_friends_short">Mời</string> + <string name="group_invitation_item_description">lời mời tham gia nhóm %1$s</string> + <string name="icon_descr_add_members">Mời thành viên</string> + <string name="info_row_local_name">Tên cục bộ</string> + <string name="message_delivery_warning_title">Cảnh báo chuyển gửi tin nhắn</string> + <string name="ensure_smp_server_address_are_correct_format_and_unique">Đảm bảo địa chỉ máy chủ SMP ở đúng định dạng, dòng được phân tách và không bị trùng lặp.</string> + <string name="ensure_xftp_server_address_are_correct_format_and_unique">Đảm bảo địa chỉ máy chủ XFTP ở đúng định dạng, dòng được phân tách và không bị trùng lặp.</string> + <string name="link_a_mobile">Liên kết với điện thoại</string> + <string name="member_inactive_title">Thành viên không hoạt động</string> + <string name="message_forwarded_title">Tin nhắn đã được chuyển tiếp</string> + <string name="member_inactive_desc">Tin nhắn có thể được gửi sau nếu thành viên hoạt động</string> + <string name="network_proxy_incorrect_config_desc">Đảm bảo cấu hình proxy là chính xác.</string> + <string name="joining_group">Đang tham gia nhóm</string> + <string name="theme_light">Sáng</string> + <string name="color_mode_light">Sáng</string> + <string name="linked_mobiles">Các thiết bị di động đã được liên kết</string> + <string name="connect_plan_join_your_group">Tham gia nhóm của bạn?</string> + <string name="v5_1_japanese_portuguese_interface">UI Nhật Bản và Bồ Đào Nha</string> + <string name="marked_deleted_description">được đánh dấu là đã xóa</string> + <string name="live">TRỰC TIẾP</string> + <string name="join_group_button">Tham gia</string> + <string name="join_group_incognito_button">Tham gia ẩn danh</string> + <string name="privacy_message_draft">Bản nháp tin nhắn</string> + <string name="leave_group_question">Rời nhóm?</string> + <string name="button_add_members">Mời thành viên</string> + <string name="invite_to_group_button">Mời vào nhóm</string> + <string name="button_leave_group">Rời nhóm</string> + <string name="member_info_section_title_member">THÀNH VIÊN</string> + <string name="message_queue_info">Thông tin hàng đợi tin nhắn</string> + <string name="users_delete_data_only">Chỉ dữ liệu hồ sơ cục bộ</string> + <string name="v5_2_fix_encryption">Giữ lại các kết nối của bạn</string> + <string name="v5_2_disappear_one_message">Làm cho một tin nhắn biến mất</string> + <string name="live_message">Tin nhắn động!</string> + <string name="keep_unused_invitation_question">Giữ lại lời mời chưa sử dụng?</string> + <string name="update_network_smp_proxy_fallback_question">Dự phòng định tuyến tin nhắn</string> + <string name="invite_friends">Mời bạn bè</string> + <string name="mark_code_verified">Đánh dấu đã xác thực</string> + <string name="alert_text_skipped_messages_it_can_happen_when">Điều này có thể xảy ra khi:\n1. Tin nhắn hết hạn sau 2 ngày trên máy gửi hoặc sau 30 ngày trên máy chủ.\n2. Quá trình giải mã tin nhắn thất bại do bạn hoặc liên hệ của bạn sử dụng bản sao lưu cơ sở dữ liệu cũ.\n3. Kết nối bị xâm phạm.</string> + <string name="join_group_question">Tham gia nhóm?</string> + <string name="v5_4_link_mobile_desktop">Liên kết ứng dụng trên điện thoại và máy tính! 🔗</string> + <string name="profile_update_event_member_name_changed">thành viên %1$s đã đổi thành %2$s</string> + <string name="make_private_connection">Tạo kết nối riêng tư</string> + <string name="make_profile_private">Tạo hồ sơ riêng tư!</string> + <string name="ensure_ICE_server_address_are_correct_format_and_unique">Đảm bảo địa chỉ máy chủ WebRTC ICE ở đúng định dạng, dòng được phân tách và không bị trùng lặp.</string> + <string name="alert_text_fragment_encryption_out_of_sync_old_database">Nó có thể xảy ra khi bạn hoặc liên hệ của bạn sử dụng bản sao lưu cơ sở dữ liệu cũ.</string> + <string name="lock_mode">Chế độ khóa</string> + <string name="v4_5_italian_interface">Giao diện tiếng Ý</string> + <string name="this_device_version"><![CDATA[<i>thiết bị này v%s)</i>]]></string> + <string name="message_reactions_are_prohibited">Thả biểu tượng cảm xúc tin nhắn bị cấm.</string> + <string name="display_name_invited_to_connect">đã được mời để kết nối</string> + <string name="learn_more">Tìm hiểu thêm</string> + <string name="keep_invitation_link">Giữ</string> + <string name="leave_group_button">Rời</string> + <string name="auth_log_in_using_credential">Đăng nhập bằng thông tin xác thực của bạn</string> + <string name="message_delivery_error_title">Lỗi chuyển gửi tin nhắn</string> + <string name="group_preview_join_as">tham gia với tư cách %s</string> + <string name="markdown_help">Trợ giúp markdown</string> + <string name="markdown_in_messages">Sử dụng markdown trong tin nhắn</string> + <string name="connect_via_group_link">Tham gia nhóm?</string> + <string name="thousand_abbreviation">k</string> + <string name="loading_remote_file_title">Đang tải tệp</string> + <string name="linked_desktops">Các máy tính đã được liên kết</string> + <string name="loading_chats">Đang tải các cuộc trò chuyện…</string> + <string name="info_view_message_button">tin nhắn</string> + <string name="mark_unread">Đánh dấu chưa đọc</string> + <string name="v4_3_voice_messages_desc">Tối đa 40 giây, được nhận ngay lập tức.</string> + <string name="v5_2_message_delivery_receipts">Chỉ báo đã nhận tin nhắn!</string> + <string name="large_file">Tệp lớn!</string> + <string name="mark_read">Đánh dấu đã đọc</string> + <string name="privacy_media_blur_radius_medium">Trung bình</string> + <string name="v4_4_live_messages">Tin nhắn động</string> + <string name="keep_conversation">Giữ lại cuộc trò chuyện</string> + <string name="image_descr_link_preview">Hình ảnh xem trước của đường dẫn</string> + <string name="member_will_be_removed_from_group_cannot_be_undone">Thành viên sẽ bị xóa khỏi nhóm - việc này không thể được hoàn tác!</string> + <string name="chat_theme_apply_to_light_mode">Chế độ sáng</string> + <string name="v5_7_new_interface_languages">UI tiếng Litva</string> + <string name="settings_section_title_messages">TIN NHẮN VÀ TỆP</string> + <string name="message_deletion_prohibited_in_chat">Việc xóa tin nhắn mà không thể phục hồi là bị cấm.</string> + <string name="v5_5_join_group_conversation">Tham gia vào các cuộc trò chuyện nhóm</string> + <string name="update_network_smp_proxy_mode_question">Chế độ định tuyến tin nhắn</string> + <string name="email_invite_subject">Hãy trò chuyện trên SimpleX Chat</string> + <string name="italic_text">in nghiêng</string> + <string name="onboarding_notifications_mode_subtitle">Nó ảnh hưởng đến pin như thế nào</string> + <string name="message_deletion_prohibited">Việc xóa tin nhắn mà không thể phục hồi bị cấm trong cuộc trò chuyện này.</string> + <string name="message_reactions_prohibited_in_this_chat">Biểu đạt cảm xúc tin nhắn bị cấm trong cuộc trò chuyện này.</string> + <string name="v4_5_message_draft">Bản nháp tin nhắn</string> + <string name="v6_0_private_routing_descr">Nó bảo vệ địa chỉ IP và các kết nối của bạn.</string> + <string name="lock_after">Khóa sau</string> + <string name="messages_section_title">Tin nhắn</string> + <string name="keychain_error">Lỗi keychain</string> + <string name="rcv_group_event_member_left">đã rời</string> + <string name="rcv_group_event_invited_via_your_group_link">đã được mời qua đường dẫn nhóm của bạn</string> + <string name="group_member_role_member">thành viên</string> + <string name="group_member_status_left">đã rời</string> + <string name="incognito_info_allows">Nó cho phép việc có các kết nối ẩn danh mà không có bất kỳ dữ liệu chung nào giữa chúng trong một hồ sơ trò chuyện</string> + <string name="import_theme_error_desc">Đảm bảo tệp có cú pháp YAML chính xác. Xuất chủ đề để có một ví dụ về cấu trúc tệp chủ đề.</string> + <string name="color_surface">Menu và cảnh báo</string> + <string name="message_reactions">Cảm xúc tin nhắn</string> + <string name="v5_1_message_reactions">Cảm xúc tin nhắn</string> + <string name="v5_8_chat_themes_descr">Làm cho các cuộc trò chuyện của bạn trở nên khác biệt!</string> + <string name="servers_info_subscriptions_section_header">Tiếp nhận tin nhắn</string> + <string name="linked_desktop_options">Cài đặt máy tính đã được liên kết</string> + <string name="rcv_group_event_member_added">đã được mời %1$s</string> + <string name="v4_3_irreversible_message_deletion">Xóa tin nhắn mà không thể phục hồi</string> + <string name="compose_message_placeholder">Tin nhắn</string> + <string name="message_servers">Máy chủ tin nhắn</string> + <string name="media_and_file_servers">Máy chủ tệp và đa phương tiện</string> + <string name="notification_preview_mode_message">Nội dung tin nhắn</string> + <string name="forward_files_messages_deleted_after_selection_desc">Tin nhắn đã bị xóa sau khi bạn chọn chúng.</string> + <string name="unblock_member_desc">Tin nhắn từ %s sẽ được hiển thị!</string> + <string name="info_row_message_status">Trạng thái tin nhắn</string> + <string name="servers_info_messages_received">Tin nhắn đã được nhận</string> + <string name="servers_info_messages_sent">Tin nhắn đã được gửi</string> + <string name="share_text_message_status">Trạng thái tin nhắn: %s</string> + <string name="message_too_large">Tin nhắn quá lớn</string> + <string name="e2ee_info_no_pq"><![CDATA[Tin nhắn, tệp, và cuộc gọi được bảo vệ bởi <b>mã hóa đầu cuối</b>với bí mật chuyển tiếp hoàn hảo, sự cự tuyệt và khôi phục xâm nhập.]]></string> + <string name="delete_message_cannot_be_undone_warning">Tin nhắn sẽ bị xóa - việc này không thể được hoàn tác!</string> + <string name="migrate_from_another_device">Di chuyển từ một thiết bị khác</string> + <string name="delete_messages_cannot_be_undone_warning">Tin nhắn sẽ bị xóa - việc này không thể được hoàn tác!</string> + <string name="delete_message_mark_deleted_warning">Tin nhắn sẽ được đánh dấu để xóa. Người nhận sẽ có thể xem lại những tin nhắn này.</string> + <string name="e2ee_info_pq"><![CDATA[Tin nhắn, tệp và cuộc gọi được bảo vệ bởi <b> mã hóa đầu cuối kháng lượng tử</b> với bí mật chuyển tiếp hoàn hảo, sự cự tuyệt và khôi phục xâm nhập.]]></string> + <string name="permissions_record_audio">Mic</string> + <string name="settings_section_title_message_shape">Hình dạng tin nhắn</string> + <string name="v5_7_forward_descr">Nguồn tin nhắn vẫn còn riêng tư.</string> + <string name="migrate_from_device_title">Di chuyển thiết bị</string> + <string name="delete_messages_mark_deleted_warning">Tin nhắn sẽ được đánh dấu để xóa. Người nhận sẽ có thể xem lại những tin nhắn này.</string> + <string name="migrate_to_device_title">Di chuyển tới đây</string> + <string name="custom_time_unit_months">tháng</string> + <string name="v4_5_multiple_chat_profiles">Nhiều hồ sơ trò chuyện</string> + <string name="new_mobile_device">Thiết bị di động mới</string> + <string name="network_smp_proxy_mode_never">Không bao giờ</string> + <string name="v6_0_new_chat_experience">Trải nghiệm trò chuyện mới 🎉</string> + <string name="v5_3_new_desktop_app">Ứng dụng máy tính mới!</string> + <string name="network_and_servers">Mạng & máy chủ</string> + <string name="callstatus_missed">cuộc gọi nhỡ</string> + <string name="new_passcode">Mã truy cập mới</string> + <string name="self_destruct_new_display_name">Tên hiển thị mới:</string> + <string name="settings_section_title_network_connection">Kết nối mạng</string> + <string name="network_status">Trạng thái mạng</string> + <string name="v5_8_chat_themes">Chủ đề trò chuyện mới</string> + <string name="moderated_item_description">đã được kiểm duyệt bởi %s</string> + <string name="moderated_description">đã được kiểm duyệt</string> + <string name="info_row_moderated_at">Đã được kiểm duyệt vào</string> + <string name="share_text_moderated_at">Đã được kiểm duyệt vào: %s</string> + <string name="remote_host_was_disconnected_toast"><![CDATA[Thiết bị di động <b>%s</b> đã bị ngắt kết nối]]></string> + <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 di động: nhấn <b>Mở trong ứng dụng di động</b>, sau đó nhấn <b>Kết nối</b> trong ứng dung.]]></string> + <string name="new_message">Tin nhắn mới</string> + <string name="muted_when_inactive">Đã tắt thông báo khi không hoạt động!</string> + <string name="new_in_version">Mới trong %s</string> + <string name="v5_2_more_things_descr">- chuyển gửi tin nhắn ổn định hơn.\n- các nhóm đã được cải thiện hơn một chút.\n- và hơn thế nữa!</string> + <string name="v5_7_network_descr">Kết nối mạng ổn định hơn.</string> + <string name="v5_7_network">Quản lý mạng</string> + <string name="remote_host_error_missing"><![CDATA[Thiết bị di dộng <b>%s</b> bị thiếu]]></string> + <string name="remote_host_error_busy"><![CDATA[Thiết bị di động <b>%s</b> đang bận]]></string> + <string name="migrate_from_device_to_another_device">Di chuyển sang một thiết bị khác</string> + <string name="migrate_to_device_migrating">Đang di chuyển</string> + <string name="icon_descr_more_button">Hơn nữa</string> + <string name="notification_preview_new_message">tin nhắn mới</string> + <string name="message_delivery_error_desc">Khả năng cao liên hệ này đã xóa kết nối với bạn.</string> + <string name="snd_error_expired">Sự cố mạng - tin nhắn đã hết hạn sau nhiều lần cố gắng gửi đi.</string> + <string name="database_migrations">Di chuyển: %s</string> + <string name="user_mute">Tắt thông báo</string> + <string name="notification_new_contact_request">Yêu cầu liên lạc mới</string> + <string name="moderate_verb">Kiểm duyệt</string> + <string name="chat_item_ttl_none">không bao giờ</string> + <string name="mute_chat">Tắt thông báo</string> + <string name="new_chat">Cuộc trò chuyện mới</string> + <string name="custom_time_unit_minutes">phút</string> + <string name="new_database_archive">Kho lưu trữ cơ sở dữ liệu mới</string> + <string name="new_member_role">Chức vụ thành viên mới</string> + <string name="v4_6_reduced_battery_usage_descr">Nhiều cải tiến hơn nữa sắp ra mắt!</string> + <string name="remote_host_error_bad_version"><![CDATA[Thiết bị di động <b>%s</b> có một phiên bản không được hỗ trợ. Xin vui lòng đảm bảo rằng bạn dùng cùng một phiên bản trên cả hai thiết bị.]]></string> + <string name="v6_0_new_media_options">Các tùy chọn đa phương tiện mới</string> + <string name="icon_descr_call_missed">Cuộc gọi nhỡ</string> + <string name="v4_5_reduced_battery_usage_descr">Nhiều cải tiến hơn nữa sắp ra mắt!</string> + <string name="v5_6_app_data_migration_descr">Di chuyển sang một thiết bị khác qua mã QR.</string> + <string name="migrate_from_device_migration_complete">Quá trình di chuyển hoàn tất</string> + <string name="remote_host_error_inactive"><![CDATA[Thiết bị di động <b>%s</b> đang không hoạt động]]></string> + <string name="remote_host_error_disconnected"><![CDATA[Thiết bị di động <b>%s</b> đã bị ngắt kết nối]]></string> + <string name="no_info_on_delivery">Không có thông tin chuyển gửi</string> + <string name="message_forwarded_desc">Chưa có kết nối trực tiếp, tin nhắn được chuyển tiếp bởi quản trị viên.</string> + <string name="no_contacts_to_add">Không có liên hệ để thêm</string> + <string name="no_connected_mobile">Không có thiết bị di động nào được kết nối</string> + <string name="chat_preferences_no">không</string> + <string name="no_details">không có thông tin</string> + <string name="new_passphrase">Mật khẩu mới…</string> + <string name="network_session_mode_session_description">Thông tin xác thực SOCKS mới sẽ được sử dụng mỗi khi bạn khởi động ứng dụng.</string> + <string name="network_smp_proxy_fallback_prohibit">Không</string> + <string name="no_contacts_selected">Không có liên hệ nào được chọn</string> + <string name="system_restricted_background_in_call_title">Không có cuộc gọi nền</string> + <string name="la_no_app_password">Không có mã truy cập ứng dụng</string> + <string name="no_filtered_chats">Không có cuộc trò chuyện nào được lọc</string> + <string name="no_history">Không có lịch sử</string> + <string name="no_filtered_contacts">Không có liên hệ nào được lọc</string> + <string name="message_queue_info_none">không</string> + <string name="network_use_onion_hosts_no">Không</string> + <string name="network_session_mode_server_description">Thông tin xác thực SOCKS mới sẽ được sử dụng cho mỗi máy chủ.</string> + <string name="status_no_e2e_encryption">không có mã hóa đầu cuối</string> + <string name="servers_info_missing">Không có thông tin, hãy thử tải lại</string> + <string name="chat_preferences_on">bật</string> + <string name="forward_alert_title_nothing_to_forward">Không có gì để chuyển tiếp!</string> + <string name="shutdown_alert_desc">Thông báo sẽ dừng hoạt động cho đến khi bạn khởi động lại ứng dụng</string> + <string name="first_platform_without_user_ids">Không có thông tin định danh người dùng.</string> + <string name="item_info_no_text">không có văn bản</string> + <string name="feature_off">tắt</string> + <string name="chat_preferences_off">tắt</string>` + <string name="only_you_can_make_calls">Chỉ bạn mới có thể thực hiện cuộc gọi.</string> + <string name="only_your_contact_can_add_message_reactions">Chỉ liên hệ của bạn mới có thể thả cảm xúc tin nhắn.</string> + <string name="videos_limit_desc">Chỉ có thể gửi 10 video cùng một lúc</string> + <string name="add_contact">Đường dẫn lời mời dùng một lần</string> + <string name="only_stored_on_members_devices">(chỉ được lưu trữ bởi thành viên nhóm)</string> + <string name="settings_notification_preview_title">Xem trước thông báo</string> + <string name="settings_notifications_mode_title">Dịch vụ thông báo</string> + <string name="privacy_media_blur_radius_off">Tắt</string> + <string name="no_received_app_files">Không có tệp nào được gửi hay được nhận</string> + <string name="group_member_role_observer">quan sát viên</string> + <string name="network_type_no_network_connection">Không có kết nối mạng</string> + <string name="only_owners_can_enable_files_and_media">Chỉ chủ nhóm mới có thể bật tính năng cho phép gửi tệp và đa phương tiện.</string> + <string name="images_limit_desc">Chỉ có thể gửi 10 hình ảnh cùng một lúc</string> + <string name="only_one_device_can_work_at_the_same_time">Chỉ một thiết bị mới có thể hoạt động cùng một lúc</string> + <string name="not_compatible">Không tương thích!</string> + <string name="notifications">Thông báo</string> + <string name="ok">OK</string> + <string name="only_delete_conversation">Chỉ xóa cuộc trò chuyện</string> + <string name="only_you_can_send_voice">Chỉ bạn mới có thể gửi tin nhắn thoại.</string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Chỉ thiết bị cuối mới lưu trữ các hồ sơ người dùng, liên hệ, nhóm, và tin nhắn</string> + <string name="only_group_owners_can_enable_voice">Chỉ chủ nhóm mới có thể bật tính năng tin nhắn thoại.</string> + <string name="only_you_can_send_disappearing">Chỉ bạn mới có thể gửi tin nhắn tự xóa.</string> + <string name="one_time_link">Đường dẫn lời mời dùng một lần</string> + <string name="only_you_can_add_message_reactions">Chỉ có bạn mới có thể thả cảm xúc tin nhắn.</string> + <string name="notifications_will_be_hidden">Thông báo sẽ chỉ được gửi cho đến khi ứng dụng dừng!</string> + <string name="only_you_can_delete_messages">Chỉ bạn mới có thể xóa tin nhắn mà không thể phục hồi (liên hệ của bạn có thể đánh dấu chúng để xóa). (24 giờ)</string> + <string name="old_database_archive">Bản lưu trữ cơ sở dữ liệu cũ</string> + <string name="feature_offered_item">được đề nghị %s</string> + <string name="feature_offered_item_with_param">được đề nghị %s: %2s</string> + <string name="la_mode_off">Tắt</string> + <string name="only_group_owners_can_change_prefs">Chỉ chủ nhóm mới có thể điều chỉnh các tùy chọn nhóm.</string> + <string name="v4_6_group_moderation_descr">Giờ thì quản trị viên có thể:\n- xóa tin nhắn của thành viên\n- vô hiệu hóa thành viên (chức vụ quan sát viên)</string> + <string name="no_selected_chat">Không có cuộc trò chuyện nào được chọn</string> + <string name="selected_chat_items_nothing_selected">Không có gì được chọn</string> + <string name="open_verb">Mở</string> + <string name="auth_open_chat_console">Mở bảng điều khiển trò chuyện</string> + <string name="auth_open_chat_profiles">Thay đổi hồ sơ trò chuyện</string> + <string name="network_use_onion_hosts_required_desc">Dịch vụ onion sẽ được yêu cầu để kết nối.\nXin lưu ý: bạn sẽ không thể kết nối tới các máy chủ mà không có địa chỉ .onion.</string> + <string name="rcv_group_event_open_chat">Mở</string> + <string name="open_database_folder">Mở thư mục cơ sở dữ liệu</string> + <string name="only_your_contact_can_send_disappearing">Chỉ liên hệ của bạn mới có thể gửi tin nhắn tự xóa.</string> + <string name="network_type_other">Khác</string> + <string name="or_scan_qr_code">Hoặc quét mã QR</string> + <string name="network_use_onion_hosts_prefer_desc">Dịch vụ onion sẽ được sử dụng khi có sẵn.</string> + <string name="only_your_contact_can_send_voice">Chỉ liên hệ của bạn mới có thể gửi tin nhắn thoại.</string> + <string name="group_member_role_owner">chủ sở hữu</string> + <string name="open_server_settings_button">Mở cài đặt máy chủ</string> + <string name="simplex_link_mode_browser_warning">Mở đường dẫn trong trình duyệt có thể làm giảm sự riêng tư và bảo mật của kết nối. Đường dẫn SimpleX không đáng tin cậy sẽ được đánh dấu màu đỏ.</string> + <string name="only_your_contact_can_delete">Chỉ liên hệ của bạn mới có thể xóa tin nhắn mà không thể phục hồi (bạn có thể đánh dấu chúng để xóa). (24 giờ)</string> + <string name="turn_off_system_restriction_button">Mở cài đặt ứng dụng</string> + <string name="la_lock_mode_passcode">Mục mã truy cập</string> + <string name="auth_open_migration_to_another_device">Mở màn hình di chuyển</string> + <string name="network_use_onion_hosts_no_desc">Dịch vụ onion sẽ không được sử dụng.</string> + <string name="opening_database">Đang mở cơ sở dữ liệu…</string> + <string name="or_show_this_qr_code">Hoặc hiển thị mã này</string> + <string name="only_your_contact_can_make_calls">Chỉ liên hệ của bạn mới có thể thực hiện cuộc gọi.</string> + <string name="info_view_open_button">mở</string> + <string name="v5_4_more_things_descr">- tùy chọn thông báo khi xóa liên hệ.\n- tên hồ sơ với dấu cách.\n- và hơn thế nữa!</string> + <string name="permissions_open_settings">Mở cài đặt</string> + <string name="migrate_from_device_or_share_this_file_link">Hoặc chia sẻ đường dẫn tệp này một cách an toàn.</string> + <string name="open_simplex_chat_to_accept_call">Mở SimpleX Chat để chấp nhận cuộc gọi</string> + <string name="call_desktop_permission_denied_safari">Mở Cài đặt Safari / Trang Web / Mic, rồi chọn Cho phép với localhost.</string> + <string name="la_mode_passcode">Mã truy cập</string> + <string name="open_chat">Mở cuộc trò chuyện</string> + <string name="app_check_for_updates_button_open">Mở vị trí tệp</string> + <string name="open_on_mobile_and_scan_qr_code"><![CDATA[Mở <i>Sử dụng từ máy tính</i> trong ứng dụng di động và quét mã QR.]]></string> + <string name="or_paste_archive_link">Hoặc dán đường dẫn lưu trữ</string> + <string name="feature_roles_owners">các chủ sở hữu</string> + <string name="open_port_in_firewall_title">Mở cổng trong tường lửa</string> + <string name="passcode_changed">Mã truy cập đã được đổi!</string> + <string name="connect_plan_open_group">Mở nhóm</string> + <string name="other_label">khác</string> + <string name="other_errors">các lỗi khác</string> + <string name="smp_servers_other">Các máy chủ SMP khác</string> + <string name="xftp_servers_other">Các máy chủ XFTP khác</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Dán đường dẫn mà bạn nhận được để kết nối với liên hệ của bạn…</string> + <string name="paste_link">Dán đường dẫn</string> + <string name="network_proxy_password">Mật khẩu</string> + <string name="onboarding_notifications_mode_periodic">Định kỳ</string> + <string name="servers_info_subscriptions_connections_pending">Đang chờ xử lý</string> + <string name="icon_descr_server_status_pending">Đang chờ xử lý</string> + <string name="restore_passphrase_not_found_desc">Không tìm thấy mật khẩu trong Keystore, vui lòng nhập thủ công. Điều này có thể xảy ra nếu bạn khôi phục dữ liệu ứng dụng bằng một công cụ sao lưu. Nếu không phải như vậy, xin vui lòng liên hệ với nhà phát triển.</string> + <string name="past_member_vName">Thành viên trước đây %1$s</string> + <string name="v5_5_simpler_connect_ui">Dán đường dẫn để kết nối!</string> + <string name="periodic_notifications">Thông báo định kỳ</string> + <string name="paste_the_link_you_received">Dán đường dẫn mà bạn nhận được</string> + <string name="paste_button">Dán</string> + <string name="call_connection_peer_to_peer">ngang hàng</string> + <string name="enter_passphrase_notification_title">Cần có mật khẩu</string> + <string name="paste_desktop_address">Dán địa chỉ máy tính</string> + <string name="paste_archive_link">Dán đường dẫn sao lưu</string> + <string name="password_to_show">Mật khẩu để hiển thị</string> + <string name="icon_descr_call_pending_sent">Cuộc gọi chờ</string> + <string name="passcode_not_changed">Mã truy cập không đổi!</string> + <string name="passcode_set">Mã truy cập đã được đặt!</string> + <string name="network_option_ping_count">bộ đếm PING</string> + <string name="v5_8_persian_ui">UI tiếng Ba Tư</string> + <string name="migrate_to_device_confirm_network_settings_footer">Xin vui lòng xác nhận rằng cài đặt mạng cho thiết bị này là chính xác.</string> + <string name="error_parsing_uri_desc">Xin vui lòng kiểm tra rằng đường dẫn SimpleX là chính xác.</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">Xin vui lòng yêu cầu liên hệ của bạn mở tính năng thực hiện cuộc gọi.</string> + <string name="network_error_desc">Xin vui lòng kiểm tra kết nối mạng của bạn với %1$s và thử lại.</string> + <string name="enter_correct_current_passphrase">Xin vui lòng nhập đúng mật khẩu hiện tại.</string> + <string name="v6_0_chat_list_media">Mở từ danh sách cuộc trò chuyện.</string> + <string name="v5_6_picture_in_picture_calls">Cuộc gọi hình trong hình</string> + <string name="agent_internal_error_desc">Xin vui lòng báo cáo với các nhà phát triển: \n%s</string> + <string name="periodic_notifications_disabled">Thông báo định kỳ đã bị tắt!</string> + <string name="remote_ctrl_connection_stopped_desc">Xin vui lòng kiểm tra rằng thiết bị di động và máy tính kết nối tới cùng một mạng cục bộ, và tường lửa của máy tính cho phép kết nối.\nHãy chia sẻ bất kỳ vấn đề nào khác với nhà phát triển.</string> + <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Xin vui lòng kiểm tra rằng bạn đã dùng đúng đường dẫn hoặc yêu cầu liên hệ của bạn gửi cho bạn một đường dẫn khác.</string> + <string name="la_please_remember_to_store_password">Xin vui lòng ghi nhớ hoặc lưu trữ nó một cách an toàn - không có cách nào để khôi phục một mật khẩu đã bị mất!</string> + <string name="toast_permission_denied">Quyền truy cập bị tự chối!</string> + <string name="ask_your_contact_to_enable_voice">Xin vui lòng yêu cầu liên hệ của bạn mở tính năng gửi tin nhắn thoại.</string> + <string name="network_option_ping_interval">khoảng PING</string> + <string name="restore_database_alert_desc">Xin vui lòng nhập mật khẩu trước đó sau khi khôi phục bản sao lưu cơ sở dữ liệu. Việc này không thể được hoàn tác.</string> + <string name="observer_cant_send_message_desc">Xin vui lòng liên lạc với quản trị viên nhóm.</string> + <string name="alert_text_fragment_please_report_to_developers">Xin vui lòng báo cáo với các nhà phát triển.</string> + <string name="agent_critical_error_desc">Xin vui lòng báo cáo với các nhà phát triển: \n%s\n\nGợi ý rằng bạn nên khởi động lại ứng dụng.</string> + <string name="error_smp_test_certificate">Có lẽ vân tay chứng chỉ trong địa chỉ máy chủ là không chính xác</string> + <string name="migrate_from_device_database_init">Đang chuẩn bị tải lên</string> + <string name="port_verb">Cổng</string> + <string name="network_proxy_port">cổng %d</string> + <string name="store_passphrase_securely_without_recover">Xin vui lòng lưu trữ mật khẩu một cách an toàn, bạn sẽ KHÔNG thể try cập kết nối trò chuyện nếu bạn làm mất nó.</string> + <string name="loading_remote_file_desc">Xin vui lòng chờ trong khi tệp đang được tải từ thiết bị được liên kết</string> + <string name="smp_servers_preset_address">Địa chỉ máy chủ cài sẵn</string> + <string name="v4_5_message_draft_descr">Lưu lại bản nháp tin nhắn cuối cùng, với các tệp đính kèm.</string> + <string name="migrate_to_device_database_init">Đang chuẩn bị tải xuống</string> + <string name="contact_developers">Xin vui lòng cập nhật ứng dụng và liên lạc với các nhà phát triển.</string> + <string name="smp_servers_preset_server">Máy chủ cài sẵn</string> + <string name="please_try_later">Xin vui lòng thử lại sau.</string> + <string name="group_welcome_preview">Xem trước</string> + <string name="store_passphrase_securely">Xin vui lòng lưu trữ mật khẩu một cách an toàn, bạn sẽ KHÔNG thể thay đổi nếu bạn làm mất nó.</string> + <string name="v5_0_polish_interface">Giao diện tiếng Ba Lan</string> + <string name="app_check_for_updates_installed_successfully_desc">Xin vui lòng khởi động lại ứng dụng.</string> + <string name="servers_info_previously_connected_servers_section_header">Các máy chủ đã kết nối trước đó</string> + <string name="privacy_redefined">Định hình lại sự riêng tư</string> + <string name="privacy_and_security">Quyền riêng tư & bảo mật</string> + <string name="profile_update_will_be_sent_to_contacts">Bản cập nhật hồ sơ sẽ được gửi đến các liên hệ của bạn.</string> + <string name="prohibit_message_reactions">Cấm thả cảm xúc tin nhắn.</string> + <string name="prohibit_calls">Cấm các cuộc gọi thoại/video.</string> + <string name="onboarding_notifications_mode_title">Thông báo riêng tư</string> + <string name="settings_section_title_profile_images">Các ảnh đại diện</string> + <string name="profile_password">Mật khẩu hồ sơ</string> + <string name="note_folder_local_display_name">Ghi chú riêng tư</string> + <string name="prohibit_message_deletion">Cấm xóa tin nhắn mà không thể phục hồi.</string> + <string name="settings_section_title_private_message_routing">ĐỊNH TUYẾN TIN NHẮN RIÊNG TƯ</string> + <string name="display_name__field">Tên hồ sơ:</string> + <string name="image_descr_profile_image">ảnh đại diện</string> + <string name="users_delete_with_connections">Hồ sơ và các kết nối máy chủ</string> + <string name="icon_descr_profile_image_placeholder">chỗ để ảnh đại diện</string> + <string name="v4_5_private_filenames">Tên tệp riêng tư</string> + <string name="v5_8_private_routing">Định tuyến tin nhắn riêng tư 🚀</string> + <string name="private_routing_error">Lỗi định tuyến riêng tư</string> + <string name="v5_5_private_notes">Ghi chú riêng tư</string> + <string name="network_smp_proxy_mode_private_routing">Định tuyến riêng tư</string> + <string name="settings_section_title_user_theme">Chủ đề hồ sơ</string> + <string name="prohibit_message_reactions_group">Cấm thả cảm xúc tin nhắn.</string> + <string name="network_option_protocol_timeout_per_kb">Thời gian chờ giao thức trên mỗi KB</string> + <string name="v4_6_hidden_chat_profiles_descr">Bảo vệ các hồ sơ trò chuyện của bạn bằng mật khẩu!</string> + <string name="v5_8_private_routing_descr">Bảo vệ địa chỉ IP của bạn khỏi các máy chủ tiếp tin được chọn bởi liên hệ của bạn.\nBật trong cài đặt *Mạng & các máy chủ*</string> + <string name="protect_ip_address">Bảo vệ địa chỉ IP</string> + <string name="prohibit_direct_messages">Cấm gửi tin nhắn trực tiếp tới các thành viên.</string> + <string name="prohibit_sending_voice_messages">Cấm gửi tin nhắn thoại.</string> + <string name="network_option_protocol_timeout">Thời gian chờ giao thức</string> + <string name="prohibit_sending_disappearing">Cấm gửi tin nhắn tự xóa.</string> + <string name="protect_app_screen">Bảo vệ màn hình ứng dụng</string> + <string name="prohibit_sending_files">Cấm gửi tệp và đa phương tiện.</string> + <string name="prohibit_sending_voice">Cấm gửi tin nhắn thoại.</string> + <string name="prohibit_sending_disappearing_messages">Cấm gửi tin nhắn tự xóa.</string> + <string name="prohibit_sending_simplex_links">Cấm gửi đường dẫn SimpleX</string> + <string name="proxied">Được proxy</string> + <string name="address_or_1_time_link">Địa chỉ hay đường dẫn dùng 1 lần?</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[đường dẫn dùng một lần có thể được dùng <i>với chỉ một liên hệ</i> - chia sẻ trực tiếp hoặc thông qua bất kỳ ứng dụng tin nhắn nào.]]></string> + <string name="disable_onion_hosts_when_not_supported"><![CDATA[Set <i>Cài đặt máy chủ .onion</i> thành Không nếu proxy SOCKS không hỗ trợ chúng.]]></string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[Các điều kiện sẽ được chấp nhận với (các) nhà cung cấp: <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[Các điều kiện sẽ được chấp nhận với (các) nhà cung cấp: <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_applied"><![CDATA[Các điều kiện này cũng sẽ áp dụng cho: <b>%s</b>.]]></string> + <string name="operator_added_message_servers">Đã thêm các máy chủ truyền tin nhắn</string> + <string name="appearance_app_toolbars">Thanh công cụ ứng dụng</string> + <string name="appearance_bars_blur_radius">Làm mờ</string> + <string name="accept_conditions">Chấp nhận điều kiện</string> + <string name="operator_added_xftp_servers">Đã thêm các máy chủ truyền tệp & đa phương tiện</string> + <string name="operator_conditions_accepted">Đã chấp nhận điều kiện</string> + <string name="address_settings">Cài đặt địa chỉ</string> + <string name="operators_conditions_accepted_for"><![CDATA[Các điều kiện được chấp nhận với (các) nhà cung cấp: <b>%s</b>.]]></string> + <string name="operator_conditions_accepted_for_some"><![CDATA[Các điều kiện đã được chấp nhận rồi đối với (các) nhà cung cấp sau: <b>%s</b>.]]></string> + <string name="read_more_in_github_with_link"><![CDATA[Đọc thêm tại <font color="#0088ff">Kho lưu trữ GitHub của chúng tôi</font>.]]></string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[Các điều kiện tương tự sẽ áp dụng cho nhà cung cấp <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Các điều kiện tương tự sẽ áp dụng cho (các) nhà cung cấp <b>%s</b>.]]></string> + <string name="operators_conditions_will_also_apply"><![CDATA[Các điều kiện này cũng sẽ áp dụng cho: <b>%s</b>.]]></string> + <string name="read_more_in_user_guide_with_link"><![CDATA[Đọc thêm tại <font color="#0088ff">Hướng dẫn người dùng</font>.]]></string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[Bạn đã kết nối tới <b>%1$s</b> rồi.]]></string> + <string name="system_restricted_background_in_call_warn"><![CDATA[Để thực hiện cuộc gọi trong nền, vui lòng chọn <b>Mức sử dụng pin ứng dụng</b> / <b>Không hạn chế</b> trong phần cài đặt ứng dụng.]]></string> + <string name="remote_host_error_timeout"><![CDATA[Đã hết thời gian chờ khi kết nối tới thiết bị di động <b>%s</b>]]></string> + <string name="turn_off_battery_optimization"><![CDATA[<b>Chọn cho phép</b> trong hộp thoại tiếp theo để nhận thông báo ngay lập tức.]]></string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[Để sử dụng các máy chủ của <b>%s</b>, vui lòng chấp nhận điều kiện sử dụng.]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Để tăng cường sự riêng tư, <b>SimpleX chạy trong nền</b> thay vì dùng thông báo đẩy.]]></string> + <string name="system_restricted_background_warn"><![CDATA[Để bật thông báo, vui lòng chọn <b>Mức sử dụng pin ứng dụng</b> / <b>Không hạn chế</b> trong phần cài đặt ứng dụng.]]></string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[Bạn đã ở trong nhóm <b>%1$s</b> rồi.]]></string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Đây là đường dẫn của bạn cho nhóm <b>%1$s</b>!]]></string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[Bạn đã tham gia nhóm <b>%1$s</b> rồi.]]></string> + <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Bạn cũng có thể kết nối bằng cách nhấp vào đường dẫn. Nếu nó mở trong trình duyệt, nhấp vào nút<b>Mở trong ứng dụng di động</b>.]]></string> + <string name="onboarding_choose_server_operators">Các bên vận hành máy chủ</string> + <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Bạn có thể <font color="#0088ff">kết nối với các nhà phát triển SimpleX Chat để hỏi bất kỳ câu hỏi nào và nhận thông tin cập nhật</font>.]]></string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[Bạn <b>không được</b> sử dụng cùng một cơ sở dữ liệu trên hai thiết .]]></string> + <string name="error_accepting_operator_conditions">Lỗi chấp nhận điều kiện</string> + <string name="failed_to_save_servers">Lỗi lưu máy chủ</string> + <string name="errors_in_servers_configuration">Lỗi trong cấu hình máy chủ.</string> + <string name="connection_security">Bảo mật kết nối</string> + <string name="create_1_time_link">Tạo đường dẫn dùng một lần</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">Các điều kiện sẽ được chấp nhận với các nhà cung cấp được cho phép sau 30 ngày.</string> + <string name="onboarding_network_operators_continue">Tiếp tục</string> + <string name="operator_conditions_failed_to_load">Không thể tải văn bản về các điều kiện hiện tại, bạn có thể xem xét các điều kiện thông qua đường dẫn này:</string> + <string name="operator_conditions_of_use">Các điều kiện sử dụng</string> + <string name="v6_2_network_decentralization_enable_flux">Sử dụng Flux trong cài đặt Mạng & máy chủ để bảo mật siêu dữ liệu tốt hơn.</string> + <string name="operator_conditions_will_be_accepted_on">Các điều kiện sẽ được chấp nhận vào: %s.</string> + <string name="error_adding_server">Lỗi thêm máy chủ</string> + <string name="error_updating_server_title">Lỗi cập nhật máy chủ</string> + <string name="operator_conditions_accepted_on">Các điều kiện đã được chấp nhận vào: %s.</string> + <string name="operator_conditions_accepted_for_enabled_operators_on">Các điều kiện sẽ được tự động chấp nhận với các nhà cung cấp được cho phép vào: %s.</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Thiết bị Xiaomi</b>: vui lòng bật Tự động khởi động trong phần cài đặt hệ thống để thông báo có thể hoạt động bình thường.]]></string> + <string name="onboarding_notifications_mode_service_desc_short">Ứng dụng luôn chạy dưới nền</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">Cuộc trò chuyện sẽ bị xóa cho tất cả các thành viên - việc này không thể được hoàn tác!</string> + <string name="delete_chat_for_self_cannot_undo_warning">Cuộc trò chuyện sẽ bị xóa cho bạn - việc này không thể được hoàn tác!</string> + <string name="v6_2_business_chats">Các cuộc trò chuyện công việc</string> + <string name="button_add_team_members">Thêm các thành viên nhóm</string> + <string name="info_row_chat">Cuộc trò chuyện</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[Bạn đã kết nối với <b>%1$s</b> rồi.]]></string> + <string name="connect_plan_chat_already_exists">Cuộc trò chuyện đã tồn tại!</string> + <string name="button_add_friends">Thêm bạn bè</string> + <string name="display_name_accepted_invitation">đã chấp nhận lời mời</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[Tất cả các tin nhắn và tệp gửi đi đều được <b>mã hóa đầu cuối</b>, với bảo mật sau ượng tử trong các tin nhắn trực tiếp.]]></string> + <string name="onboarding_network_about_operators">Giới thiệu về các nhà cung cấp</string> + <string name="add_your_team_members_to_conversations">Thêm các thành viên nhóm của bạn vào các cuộc trò chuyện.</string> + <string name="business_address">Địa chỉ doanh nghiệp</string> + <string name="error_saving_database">Lỗi lưu cơ sở dữ liệu</string> + <string name="direct_messages_are_prohibited_in_chat">Tin nhắn trực tiếp giữa các thành viên bị cấm trong cuộc trò chuyện này.</string> + <string name="for_chat_profile">Dành cho hồ sơ trò chuyện %s:</string> + <string name="onboarding_notifications_mode_periodic_desc_short">Kiểm tra tin nhắn mỗi 10 phút.</string> + <string name="onboarding_network_operators_app_will_use_for_routing">Ví dụ, nếu liên hệ của bạn nhận tin nhắn thông qua một máy chủ SimpleX Chat, ứng dụng của bạn sẽ gửi chúng qua một máy chủ Flux.</string> + <string name="how_it_helps_privacy">Cách nó cải thiện sự riêng tư</string> + <string name="invite_to_chat_button">Mời để trò chuyện</string> + <string name="button_leave_chat">Rời cuộc trò chuyện</string> + <string name="operator_use_for_messages_private_routing">Cho định tuyến riêng tư</string> + <string name="v6_2_network_decentralization_enable_flux_reason">để bảo mật siêu dữ liệu tốt hơn.</string> + <string name="v6_2_improved_chat_navigation">Đã cải thiện điều hướng trò chuyện</string> + <string name="button_delete_chat">Xóa cuộc trò chuyện</string> + <string name="for_social_media">Cho mạng xã hội</string> + <string name="leave_chat_question">Rời cuộc trò chuyện?</string> + <string name="sync_connection_desc">Kết nối yêu cầu thiết lập lại mã hóa.</string> + <string name="encryption_renegotiation_in_progress">Đang trong quá trình thiết lập lại mã hóa.</string> + <string name="sync_connection_confirm">Sửa</string> + <string name="sync_connection_question">Sửa kết nối?</string> + <string name="delete_chat_question">Xóa cuộc trò chuyện?</string> + <string name="direct_messages_are_prohibited">Tin nhắn trực tiếp giữa các thành viên bị cấm.</string> + <string name="debug_logs">Bật logs</string> + <string name="a_plus_b">a + b</string> + <string name="connection_not_ready">Kết nối chưa sẵn sàng.</string> + <string name="error_updating_chat_tags">Lỗi cập nhật danh sách trò chuyện</string> + <string name="error_creating_chat_tags">Lỗi tạo danh sách trò chuyện</string> + <string name="error_loading_chat_tags">Lỗi tải các danh sách trò chuyện</string> + <string name="chat_list_contacts">Các liên hệ</string> + <string name="chat_list_groups">Nhóm</string> + <string name="chat_list_add_list">Thêm danh sách</string> + <string name="chat_list_all">Tất cả</string> + <string name="delete_chat_list_menu_action">Xóa</string> + <string name="delete_chat_list_question">Xóa danh sách?</string> + <string name="edit_chat_list_menu_action">Chỉnh sửa</string> + <string name="list_name_field_placeholder">Tên danh sách...</string> + <string name="users_delete_profile_for">Xóa hồ sơ trò chuyện cho</string> + <string name="create_list">Tạo danh sách</string> + <string name="add_to_list">Thêm vào danh sách</string> + <string name="chat_list_businesses">Các doanh nghiệp</string> + <string name="delete_chat_list_warning">Tất cả các cuộc trò chuyện sẽ bị xóa khỏi danh sách %s, và danh sách cũng sẽ bị xóa</string> + <string name="chat_list_favorites">Ưa thích</string> + <string name="list_menu">Danh sách</string> + <string name="delete_messages_after">Xóa tin nhắn sau</string> + <string name="no_message_servers_configured">Không có máy chủ tin nhắn.</string> + <string name="no_message_servers_configured_for_private_routing">Không có máy chủ để định tuyến tin nhắn riêng tư.</string> + <string name="no_message_servers_configured_for_receiving">Không có máy chủ để nhận tin nhắn.</string> + <string name="maximum_message_size_title">Tin nhắn quá lớn!</string> + <string name="onboarding_network_operators">Các nhà cung cấp mạng</string> + <string name="onboarding_notifications_mode_battery">Thông báo và pin</string> + <string name="only_chat_owners_can_change_prefs">Chỉ có các chủ cuộc trò chuyện mới có thể thay đổi các tùy chọn.</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">Thành viên sẽ bị xóa khỏi cuộc trò chuyện - việc này không thể được hoàn tác!</string> + <string name="operator_info_title">Nhà cung cấp mạng</string> + <string name="v6_2_network_decentralization">Phi tập trung hóa mạng lưới</string> + <string name="message_deleted_or_not_received_error_title">Không có tin nhắn</string> + <string name="no_chats">Không có cuộc trò chuyện nào</string> + <string name="no_chats_found">Không tìm thấy cuộc trò chuyện nào</string> + <string name="no_chats_in_list">Không có cuộc trò chuyện nào trong dánh sách %s.</string> + <string name="no_unread_chats">Không có cuộc trò chuyện nào chưa được đọc</string> + <string name="duplicated_list_error">Tên danh sách và biểu tượng cảm xúc phải khác nhau đối với tất cả các danh sách.</string> + <string name="no_media_servers_configured_for_private_routing">Không có máy chủ để nhận tệp.</string> + <string name="no_media_servers_configured_for_sending">Không có máy chủ để gửi tệp.</string> + <string name="onboarding_notifications_mode_off_desc_short">Không có dịch vụ nền</string> + <string name="smp_servers_new_server">Máy chủ mới</string> + <string name="no_media_servers_configured">Không có máy chủ tệp và đa phương tiện.</string> + <string name="v6_0_reachable_chat_toolbar">Thanh công cụ trò chuyện trong tầm</string> + <string name="random_port">Ngẫu nhiên</string> + <string name="network_preset_servers_title">Các máy chủ cài sẵn</string> + <string name="operator_open_changes">Xem thay đổi</string> + <string name="operator_open_conditions">Xem điều kiện</string> + <string name="operator_server_alert_title">Bên vận hành máy chủ</string> + <string name="v6_2_improved_chat_navigation_descr">- Mở cuộc trò chuyện từ tin nhắn chưa đọc đầu tiên.\n- Nhảy tới tin nhắn được trích dẫn.</string> + <string name="chat_archive">Hoặc nhập tệp lưu trữ</string> + <string name="v5_6_quantum_resistant_encryption">Mã hóa kháng lượng tử</string> + <string name="v6_2_business_chats_descr">Sự riêng tư cho các khách hàng của bạn.</string> + <string name="maximum_message_size_reached_text">Xin vui lòng giảm kích thước tin nhắn và gửi lại.</string> + <string name="maximum_message_size_reached_non_text">Xin vui lòng giảm kích thước tin nhắn hoặc xóa đa phương tiện và gửi lại.</string> + <string name="network_proxy_auth">Xác thực proxy</string> + <string name="image_descr_qr_code">Mã QR</string> + <string name="open_with_app">Mở bằng %s</string> + <string name="chat_bottom_bar">Thanh công cụ trò chuyện trong tầm</string> + <string name="rate_the_app">Đánh giá ứng dụng</string> + <string name="you_can_change_it_later">Mật khẩu ngẫu nhiên được lưu trong cài đặt dưới dạng văn bản thuần túy.\nBạn có thể thay đổi nó sau.</string> + <string name="servers_info_proxied_servers_section_header">Các máy chủ được proxy</string> + <string name="whats_new_read_more">Đọc thêm</string> + <string name="one_hand_ui">Thanh công cụ ứng dụng trong tầm</string> + <string name="or_to_share_privately">Hoặc chia sẻ một cách riêng tư</string> + <string name="operator">Bên vận hành</string> + <string name="conn_event_enabled_pq">mã hóa đầu cuối kháng lượng tử</string> + <string name="callstate_received_answer">đã nhận lời đáp…</string> + <string name="color_received_message">Đã nhận tin nhắn</string> + <string name="receiving_via">Đang nhận qua</string> + <string name="feature_received_prohibited">đã nhận, bị cấm</string> + <string name="v5_5_join_group_conversation_descr">Lịch sử gần đây và đã cải thiện bot thư mục.</string> + <string name="receiving_files_not_yet_supported">Việc nhận tệp vẫn chưa được hỗ trợ</string> + <string name="color_received_quote">Đã nhận lời đáp</string> + <string name="switch_receiving_address_desc">Địa chỉ nhận sẽ được đổi sang một máy chủ khác. Việc thay đổi địa chỉ sẽ hoàn thành sau khi người gửi hoạt động.</string> + <string name="callstate_received_confirmation">đã nhận lời xác nhận…</string> + <string name="servers_info_reconnect_servers_message">Kết nối lại tất cả các máy chủ đã được kết nối để buộc gửi tin nhắn. Việc này tiêu tốn thêm lưu lượng.</string> + <string name="servers_info_reconnect_all_servers_button">Kết nối lại tất cả các máy chủ</string> + <string name="servers_info_detailed_statistics_received_total">Đã nhận tổng số</string> + <string name="servers_info_detailed_statistics_receive_errors">Nhận được lỗi</string> + <string name="v4_4_live_messages_desc">Người nhận có thể thấy các bản cập nhật tin nhắn khi bạn gõ chúng.</string> + <string name="servers_info_reconnect_server_message">Kết nối lại máy chủ để buộc gửi tin nhắn. Việc này tiêu tốn thêm lưu lượng.</string> + <string name="servers_info_detailed_statistics_received_messages_header">Đã nhận tin nhắn</string> + <string name="stop_rcv_file__message">Việc nhận tệp sẽ bị dừng lại</string> + <string name="simplex_service_notification_text">Đang nhận tin nhắn…</string> + <string name="received_message">Đã nhận tin nhắn</string> + <string name="servers_info_reconnect_server_title">Kết nối lại máy chủ?</string> + <string name="send_receipts_disabled_alert_title">Chỉ báo đã nhận bị tắt</string> + <string name="servers_info_reconnect_servers_title">Kết nối lại các máy chủ?</string> + <string name="recipients_can_not_see_who_message_from">Người nhận không thể xem tin nhắn này đến từ ai.</string> + <string name="reconnect">Kết nối lại</string> + <string name="network_option_rcv_concurrency">Đang nhận đồng thời</string> + <string name="info_row_received_at">Đã nhận vào</string> + <string name="share_text_received_at">Đã nhận vào: %s</string> + <string name="theme_remove_image">Xóa ảnh</string> + <string name="icon_descr_call_rejected">Đã từ chối cuộc gọi</string> + <string name="app_check_for_updates_button_remind_later">Nhắc lại sau</string> + <string name="refresh_qr_code">Làm mới</string> + <string name="v4_5_reduced_battery_usage">Đã giảm mức sử dụng pin</string> + <string name="reject_contact_button">Từ chối</string> + <string name="remote_hosts_section">Các thiết bị di động từ xa</string> + <string name="remove_passphrase">Xóa</string> + <string name="remove_member_confirmation">Xóa</string> + <string name="button_remove_member">Xóa thành viên</string> + <string name="remove_member_button">Xóa thành viên</string> + <string name="button_remove_member_question">Xóa thành viên?</string> + <string name="reject">Từ chối</string> + <string name="group_member_status_removed">đã xóa</string> + <string name="info_row_updated_at">Bản ghi được cập nhật vào</string> + <string name="icon_descr_record_voice_message">Ghi lại tin nhắn thoại</string> + <string name="remove_passphrase_from_settings">Xóa mật khẩu khỏi cài đặt?</string> + <string name="remove_passphrase_from_keychain">Xóa mật khẩu khỏi Keystore?</string> + <string name="share_text_updated_at">Bản ghi được cập nhật vào: %s</string> + <string name="migrate_from_device_remove_archive_question">Xóa kho lưu trữ?</string> + <string name="profile_update_event_removed_address">đã xóa địa chỉ liên lạc</string> + <string name="profile_update_event_removed_picture">đã xóa ảnh đại diện hồ sơ</string> + <string name="relay_server_if_necessary">Máy chủ relay chỉ được sử dụng khi cần thiết. Bên khác có thể quan sát địa chỉ IP của bạn.</string> + <string name="rcv_group_event_member_deleted">đã xóa %1$s</string> + <string name="rcv_group_event_user_deleted">đã xóa bạn</string> + <string name="callstatus_rejected">đã từ chối cuộc gọi</string> + <string name="relay_server_protects_ip">Máy chủ relay bảo vệ địa chỉ IP của bạn, nhưng nó có thể quan sát thời lượng của cuộc gọi.</string> + <string name="reset_color">Đặt lại các màu</string> + <string name="connect_plan_repeat_connection_request">Lặp lại yêu cầu kết nối?</string> + <string name="reset_single_color">Đặt lại màu</string> + <string name="network_options_reset_to_defaults">Đặt lại về mặc định</string> + <string name="servers_info_reset_stats_alert_confirm">Đặt lại</string> + <string name="wallpaper_scale_repeat">Lặp lại</string> + <string name="display_name_requested_to_connect">đã yêu cầu kết nối</string> + <string name="migrate_from_device_repeat_upload">Tải lên lại lần nữa</string> + <string name="sync_connection_force_question">Thiết lập lại mã hóa?</string> + <string name="migrate_to_device_repeat_import">Nhập lại</string> + <string name="network_use_onion_hosts_required">Bắt buộc</string> + <string name="migrate_to_device_repeat_download">Tải xuống lại lần nữa</string> + <string name="sync_connection_force_confirm">Thiết lập lại</string> + <string name="reset_verb">Đặt lại</string> + <string name="reply_verb">Trả lời</string> + <string name="chat_theme_reset_to_user_theme">Đặt lại về chủ đề người dùng</string> + <string name="renegotiate_encryption">Thiết lập lại mã hóa</string> + <string name="settings_restart_app">Khởi động lại</string> + <string name="restart_the_app_to_use_imported_chat_database">Khởi động lại ứng dụng để sử dụng cơ sở dữ liệu trò chuyện đã được nhập.</string> + <string name="connect_plan_repeat_join_request">Lặp lại yêu cầu tham gia?</string> + <string name="reset_all_hints">Đặt lại tất cả gợi ý</string> + <string name="chat_theme_reset_to_app_theme">Đặt lại trở về chủ đề ứng dụng</string> + <string name="restart_the_app_to_create_a_new_chat_profile">Khởi động lại ứng dụng để tạo một hồ sơ trò chuyện mới.</string> + <string name="servers_info_reset_stats">Đặt lại tất cả số liệu thống kê</string> + <string name="servers_info_reset_stats_alert_title">Đặt lại tất cả số liệu thống kê?</string> + <string name="save_passphrase_and_open_chat">Lưu mật khẩu và mở kết nối trò chuyện</string> + <string name="send_verb">Gửi</string> + <string name="run_chat_section">KHỞI CHẠY KẾT NỐI TRÒ CHUYỆN</string> + <string name="save_verb">Lưu</string> + <string name="scan_paste_link">Quét / Dán đường dẫn</string> + <string name="smp_servers_scan_qr">Quét mã QR máy chủ</string> + <string name="database_restore_error">Lỗi khôi phục cơ sở dữ liệu</string> + <string name="current_version_timestamp">%s (hiện tại)</string> + <string name="sender_at_ts">%s vào %s</string> + <string name="select_contacts">Chọn các liên hệ</string> + <string name="secret_text">bí mật</string> + <string name="sending_files_not_yet_supported">gửi tệp chưa được hỗ trợ</string> + <string name="send_disappearing_message">Gửi tin nhắn tự xóa</string> + <string name="cannot_share_message_alert_text">Các tùy chọn của cuộc trò chuyện được chọn không cho phép tin nhắn này.</string> + <string name="scan_QR_code">Quét mã QR</string> + <string name="saved_description">đã lưu</string> + <string name="saved_from_description">đã lưu từ %s</string> + <string name="saved_from_chat_item_info_title">Đã lưu từ</string> + <string name="scan_qr_code_from_desktop">Quét mã QR từ máy tính</string> + <string name="secured">Đã được bảo mật</string> + <string name="v5_5_simpler_connect_ui_descr">Thanh tìm kiếm chấp nhận đường dẫn lời mời.</string> + <string name="smp_server_test_secure_queue">Rào hàng đợi</string> + <string name="send_errors">Gửi lỗi</string> + <string name="save_servers_button">Lưu</string> + <string name="custom_time_picker_select">Chọn</string> + <string name="self_destruct_passcode">Mã truy cập tự hủy</string> + <string name="restore_database_alert_confirm">Khôi phục</string> + <string name="rcv_group_event_2_members_connected">%s và %s đã được kết nối</string> + <string name="migrate_to_device_bytes_downloaded">%s đã được tải xuống</string> + <string name="restore_database">Khôi phục bản sao lưu cơ sở dữ liệu</string> + <string name="notifications_mode_off">Chạy khi sử dụng ứng dụng</string> + <string name="revoke_file__action">Thu hồi tệp</string> + <string name="revoke_file__title">Thu hồi tệp?</string> + <string name="revoke_file__confirm">Thu hồi</string> + <string name="member_contact_send_direct_message">gửi tin nhắn trực tiếp</string> + <string name="selected_chat_items_selected_n">Đã chọn %d</string> + <string name="compose_send_direct_message_to_connect">Gửi tin nhắn trực tiếp để kết nối</string> + <string name="compose_save_messages_n">Đang lưu %1$s tin nhắn</string> + <string name="connect_via_link_or_qr_from_clipboard_or_in_person">(quét hoặc dán từ bảng nháp)</string> + <string name="onboarding_select_network_operators_to_use">Chọn các bên vận hành mạng lưới để sử dụng.</string> + <string name="button_send_direct_message">Gửi tin nhắn trực tiếp</string> + <string name="save_welcome_message_question">Lưu lời chào?</string> + <string name="icon_descr_sent_msg_status_send_failed">gửi thất bại</string> + <string name="scan_code_from_contacts_app">Quét mã bảo mật từ ứng dụng của liên hệ bạn.</string> + <string name="settings_section_title_delivery_receipts">GỬI CHỈ BÁO ĐÃ NHẬN TỚI</string> + <string name="search_verb">Tìm kiếm</string> + <string name="search_or_paste_simplex_link">Tìm kiếm hoặc dán đường dẫn SimpleX</string> + <string name="save_list">Lưu danh sách</string> + <string name="retry_verb">Thử lại</string> + <string name="save_and_notify_contact">Lưu và thông báo tới liên hệ</string> + <string name="onboarding_network_operators_review_later">Xem xét lại sau</string> + <string name="rcv_conn_event_verification_code_reset">mã bảo mật đã thay đổi</string> + <string name="save_group_profile">Lưu hồ sơ nhóm</string> + <string name="network_options_save_and_reconnect">Lưu và kết nối lại</string> + <string name="sending_delivery_receipts_will_be_enabled">Tính năng gửi chỉ báo đã nhận sẽ được bật cho tất cả liên hệ.</string> + <string name="operator_review_conditions">Xem xét lại các điều kiện</string> + <string name="wallpaper_scale">Tỷ lệ</string> + <string name="send_disappearing_message_send">Gửi</string> + <string name="select_chat_profile">Chọn hồ sơ trò chuyện</string> + <string name="scan_code">Quét mã</string> + <string name="security_code">Mã bảo mật</string> + <string name="smp_save_servers_question">Lưu các máy chủ?</string> + <string name="rcv_group_event_1_member_connected">%s đã được kết nối</string> + <string name="saved_message_title">Đã lưu tin nhắn</string> + <string name="network_option_seconds_label">giây</string> + <string name="send_live_message_desc">Gửi một tin nhắn động - nó sẽ cập nhật cho (các) người nhận ngay khi bạn gõ</string> + <string name="sender_cancelled_file_transfer">Người gửi đã hủy quá trình truyền tệp.</string> + <string name="v5_1_self_destruct_passcode">Mã truy cập tự hủy</string> + <string name="saved_ICE_servers_will_be_removed">Các máy chủ WebRTC ICE đã được lưu sẽ bị xóa.</string> + <string name="restart_chat_button">Khởi động lại kết nối trò chuyện</string> + <string name="network_options_save">Lưu</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Tính năng gửi chỉ báo đã nhận sẽ được bật cho tất cả các liên hệ trong tất cả các hồ sơ trò chuyện có thể thấy được.</string> + <string name="reveal_verb">Tiết lộ</string> + <string name="save_and_notify_contacts">Lưu và thông báo tới các liên hệ</string> + <string name="save_and_notify_group_members">Lưu vào thông báo tới các thành viên nhóm</string> + <string name="save_profile_password">Lưu mật khẩu hồ sơ</string> + <string name="v4_2_security_assessment">Thẩm định bảo mật</string> + <string name="v5_8_safe_files">Nhận tệp một cách an toàn</string> + <string name="save_and_update_group_profile">Lưu và cập nhật hồ sơ nhóm</string> + <string name="custom_time_unit_seconds">giây</string> + <string name="saved_chat_item_info_tab">Đã lưu</string> + <string name="self_destruct">Tự hủy</string> + <string name="select_verb">Chọn</string> + <string name="self_destruct_passcode_enabled">Mã truy cập tự hủy đã được kích hoạt!</string> + <string name="save_passphrase_in_keychain">Lưu mật khẩu trong Keystore</string> + <string name="group_members_2">%s và %s</string> + <string name="role_in_group">Chức vụ</string> + <string name="v5_6_safer_groups">Các nhóm trở nên an toàn hơn</string> + <string name="scan_from_mobile">Quét từ di động</string> + <string name="chat_list_notes">Ghi chú</string> + <string name="info_view_search_button">tìm kiếm</string> + <string name="change_list">Thay đổi danh sách</string> + <string name="change_order_chat_list_menu_action">Thay đổi thứ tự</string> + <string name="smp_servers_save">Lưu các máy chủ</string> + <string name="save_auto_accept_settings">Lưu cài đặt tự động chấp nhận</string> + <string name="save_preferences_question">Lưu tùy chọn?</string> + <string name="save_settings_question">Lưu cài đặt?</string> + <string name="restore_database_alert_title">Khôi phục bản sao lưu cơ sở dữ liệu?</string> + <string name="save_passphrase_in_settings">Lưu mật khẩu trong cài đặt</string> + <string name="self_destruct_passcode_changed">Mã truy cập tự hủy đã được đổi!</string> + <string name="sender_may_have_deleted_the_connection_request">Người gửi có thể đã xóa yêu cầu kết nối.</string> + <string name="network_error_broker_version_desc">Phiên bản máy chủ không tương thích với ứng dụng của bạn: %1$s.</string> + <string name="accept_feature_set_1_day">Đặt 1 ngày</string> + <string name="profile_update_event_set_new_picture">đặt ảnh đại diện mới</string> + <string name="v4_4_disappearing_messages_desc">Các tin nhắn đã gửi sẽ bị xóa sau thời gian đã cài.</string> + <string name="message_queue_info_server_info">thông tin hàng đợi máy chủ: %1$s\n\ntin nhắn được nhận cuối cùng: %2$s</string> + <string name="settings_section_title_settings">CÀI ĐẶT</string> + <string name="info_row_sent_at">Đã gửi vào</string> + <string name="server_address">Địa chỉ máy chủ</string> + <string name="session_code">Mã phiên</string> + <string name="color_sent_quote">Lời đáp đã gửi</string> + <string name="text_field_set_contact_placeholder">Đặt tên liên hệ…</string> + <string name="send_live_message">Gửi tin nhắn động</string> + <string name="set_contact_name">Đặt tên liên hệ</string> + <string name="error_server_operator_changed">Bên vận hành máy chủ đã được đổi.</string> + <string name="error_server_protocol_changed">Giao thức máy chủ đã được đổi.</string> + <string name="network_smp_proxy_fallback_allow_description">Gửi tin nhắn trực tiếp khi địa chỉ của bạn hoặc địa chỉ đích không hỗ trợ định tuyến riêng tư.</string> + <string name="send_link_previews">Gửi xem trước đường dẫn</string> + <string name="servers_info_detailed_statistics_sent_messages_header">Các tin nhắn đã gửi</string> + <string name="servers_info">Thông tin các máy chủ</string> + <string name="error_smp_test_server_auth">Máy chủ yêu cầu xác thực để tạo hàng đợi, kiểm tra mật khẩu</string> + <string name="stop_snd_file__message">Việc gửi tệp sẽ bị dừng lại.</string> + <string name="send_us_an_email">Gửi thư điện tử cho chúng tôi</string> + <string name="share_text_sent_at">Đã gửi vào: %s</string> + <string name="server_added_to_operator__name">Máy chủ đã được thêm cho bên vận hành %s.</string> + <string name="srv_error_host">Địa chỉ máy chủ không tương thích với cài đặt mạng.</string> + <string name="srv_error_version">Phiên bản máy chủ không tương thích với cài đặt mạng.</string> + <string name="icon_descr_sent_msg_status_sent">đã gửi</string> + <string name="icon_descr_settings">Cài đặt</string> + <string name="chat_with_the_founder">Gửi các câu hỏi và ý tưởng</string> + <string name="v5_0_app_passcode_descr">Đặt nó thay vì sử dụng xác thực hệ thống.</string> + <string name="sent_message">Đã gửi tin nhắn</string> + <string name="set_password_to_export">Đặt mật khẩu để xuất</string> + <string name="sending_via">Đang gửi qua</string> + <string name="set_passphrase">Đặt mật khẩu</string> + <string name="toolbar_settings">Cài đặt</string> + <string name="network_error_broker_host_desc">Địa chỉ máy chủ không tương thích với cài đặt mạng: %1$s.</string> + <string name="conn_stats_section_title_servers">CÁC MÁY CHỦ</string> + <string name="error_xftp_test_server_auth">Máy chủ yêu cầu xác thực để tải lên, kiểm tra mật khẩu</string> + <string name="network_session_mode_server">Máy chủ</string> + <string name="set_passcode">Đặt mã truy cập</string> + <string name="receipts_contacts_override_disabled">Gửi chỉ báo đã nhận bị tắt cho %d liên hệ</string> + <string name="set_group_preferences">Đặt tùy chọn nhóm</string> + <string name="servers_info_reset_stats_alert_message">Số liệu thống kê các máy chủ sẽ được đặt lại - việc này không thể được hoàn tác!</string> + <string name="cant_call_member_send_message_alert_text">Gửi tin nhắn để cho phép gọi điện.</string> + <string name="sent_via_proxy">Đã gửi qua proxy</string> + <string name="receipts_contacts_override_enabled">Gửi chỉ báo đã nhận được bật cho %d liên hệ</string> + <string name="set_database_passphrase">Đặt mật khẩu cơ sở dữ liệu</string> + <string name="send_receipts">Gửi chỉ báo đã nhận</string> + <string name="profile_update_event_set_new_address">đặt địa chỉ liên lạc mới</string> + <string name="color_sent_message">Tin nhắn đã gửi</string> + <string name="chat_theme_set_default_theme">Đặt chủ đề mặc định</string> + <string name="enable_sending_recent_history">Gửi tối đa 100 tin nhắn cuối cùng tới các thành viên mới.</string> + <string name="servers_info_detailed_statistics_sent_messages_total">Tổng số đã gửi</string> + <string name="icon_descr_send_message">Gửi Tin nhắn</string> + <string name="smp_servers_test_failed">Kiểm tra máy chủ thất bại!</string> + <string name="network_smp_proxy_fallback_allow_protected_description">Gửi tin nhắn trực tiếp khi địa chỉ IP được bảo vệ và máy chủ của bạn hoặc máy chủ đích không hõ trợ định tuyến riêng tư.</string> + <string name="receipts_groups_override_disabled">Gửi chỉ báo đã nhận bị tắt cho %d nhóm</string> + <string name="receipts_groups_override_enabled">Gửi chỉ báo đã nhận được bật cho %d nhóm</string> + <string name="sent_directly">Đã gửi trực tiếp</string> + <string name="v4_6_group_welcome_message_descr">Hiển thị thông điệp tới các thành viên mới!</string> + <string name="share_simplex_address_on_social_media">Chia sẻ địa chỉ SimpleX trên mạng xã hội.</string> + <string name="notification_preview_mode_message_desc">Hiển thị liên hệ và tin nhắn</string> + <string name="private_routing_show_message_status">Hiển thị trạng thái tin nhắn</string> + <string name="show_dev_options">Hiển thị:</string> + <string name="share_address_publicly">Chia sẻ địa chỉ một cách công khai</string> + <string name="show_call_on_lock_screen">Hiển thị</string> + <string name="privacy_show_last_messages">Hiển thị các tin nhắn cuối cùng</string> + <string name="share_verb">Chia sẻ</string> + <string name="terminal_always_visible">Hiển thị bảng điều khiển trong cửa sổ mới</string> + <string name="error_creating_report">Lỗi tạo báo cáo</string> + <string name="share_message">Chia sẻ tin nhắn…</string> + <string name="chat_list_always_visible">Hiển thị danh sách trò chuyện trong cửa sổ mới</string> + <string name="notification_preview_mode_contact_desc">Chỉ hiển thị liên hệ</string> + <string name="share_this_1_time_link">Chia sẻ đường dẫn dùng 1 lần này</string> + <string name="share_with_contacts">Chia sẻ với các liên hệ</string> + <string name="share_file">Chia sẻ tệp…</string> + <string name="share_address">Chia sẻ địa chỉ</string> + <string name="share_image">Chia sẻ đa phương tiện…</string> + <string name="new_chat_share_profile">Chia sẻ hồ sơ</string> + <string name="show_developer_options">Hiển thị tùy chọn cho nhà phát triển</string> + <string name="v5_7_shape_profile_images">Tạo khuôn ảnh đại diện</string> + <string name="setup_database_passphrase">Thiết lập mật khẩu cơ sở dữ liệu</string> + <string name="servers_info_target">Đang hiển thị thông tin cho</string> + <string name="share_invitation_link">Chia sẻ đường dẫn dùng 1 lần</string> + <string name="share_address_with_contacts_question">Chia sẻ địa chỉ với các liên hệ?</string> + <string name="subscription_percentage">Hiển thị tỷ lệ phần trăm</string> + <string name="show_internal_errors">Hiển thị lỗi nội bộ</string> + <string name="share_link">Chia sẻ đường dẫn</string> + <string name="share_1_time_link_with_a_friend">Chia sẻ đường dẫn dùng 1 lần với một người bạn</string> + <string name="prefs_error_saving_settings">Lỗi lưu cài đặt</string> + <string name="settings_notification_preview_mode_title">Hiển thị bản xem trước</string> + <string name="ntf_channel_calls">Các cuộc gọi SimpleX Chat</string> + <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> + <string name="simplex_links_not_allowed">Các đường dẫn SimpleX là không được phép</string> + <string name="chat_lock">Khóa SimpleX</string> + <string name="simplex_links">Các đường dẫn SimpleX</string> + <string name="ntf_channel_messages">Các tin nhắn SimpleX Chat</string> + <string name="icon_descr_address">Địa chỉ SimpleX</string> + <string name="system_restricted_background_desc">SimpleX không thể chạy trong nền. Bạn sẽ chỉ nhận được thông báo khi ứng dụng đang chạy.</string> + <string name="la_lock_mode">Chế độ Khóa SimpleX</string> + <string name="simplex_link_contact">Địa chỉ liên lạc SimpleX Chat</string> + <string name="simplex_link_group">Đường dẫn nhóm SimpleX</string> + <string name="simplex_link_mode">Các đường dẫn SimpleX</string> + <string name="simplex_link_invitation">Lời mời SimpleX dùng một lần</string> + <string name="la_notice_title_simplex_lock">Khóa SimpleX</string> + <string name="onboarding_network_operators_simplex_flux_agreement">SimpleX Chat và Flux đã đi đến một thỏa thuận để đưa các máy chủ do Flux vận hành vào ứng dụng.</string> + <string name="theme_simplex">SimpleX</string> + <string name="simplex_address">Địa chỉ SimpleX</string> + <string name="shutdown_alert_question">Tắt?</string> + <string name="settings_shutdown">Tắt</string> + <string name="v4_2_security_assessment_desc">Sự an toàn của SimpleX Chat đã được kiểm định bởi Trail of Bits.</string> + <string name="simplex_service_notification_title">Dịch vụ SimpleX Chat</string> + <string name="auth_simplex_lock_turned_on">Khóa SimpleX đã được bật</string> + <string name="show_QR_code">Hiển thị mã QR</string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">Địa chỉ SimpleX và các đường dẫn dùng một lần đều an toàn để chia sẻ thông qua bất kỳ ứng dụng nhắn tin nào.</string> + <string name="image_descr_simplex_logo">Logo SimpleX</string> + <string name="show_slow_api_calls">Hiển thị các lượt yêu cầu API chậm</string> + <string name="simplex_address_or_1_time_link">Địa chỉ SimpleX hay đường dẫn dùng 1 lần?</string> + <string name="lock_not_enabled">Khóa SimpleX không được bật!</string> + <string name="simplex_links_are_prohibited_in_group">Các đường dẫn SimpleX là bị cấm.</string> + <string name="report_item_visibility_moderators">Chỉ người gửi và các kiểm duyệt mới xem được</string> + <string name="report_archive_alert_title">Lưu trữ bản báo cáo?</string> + <string name="report_item_archived">đã lưu trữ bản báo cáo</string> + <string name="report_compose_reason_header_illegal">Báo cáo nội dụng: sẽ chỉ có các kiểm duyệt viên trong nhóm là xem được.</string> + <string name="archive_verb">Lưu trữ</string> + <string name="report_reason_other">Một lý do khác</string> + <string name="group_member_role_moderator">kiểm duyệt viên</string> + <string name="report_compose_reason_header_profile">Báo cáo hồ sơ thành viên: sẽ chỉ có các kiểm duyệt viên trong nhóm là xem được.</string> + <string name="icon_descr_simplex_team">Đội ngũ SimpleX</string> + <string name="v6_1_better_security_descr">Các giao thức SimpleX đã được xem xét bởi Trail of Bits.</string> + <string name="v5_3_simpler_incognito_mode">Đã đơn giản hóa chế độ ẩn danh</string> + <string name="report_reason_community">Vi phạm các nguyên tắc cộng động</string> + <string name="report_reason_illegal">Nội dung không phù hợp</string> + <string name="report_reason_profile">Hồ sơ không phù hợp</string> + <string name="report_item_visibility_submitter">Chỉ bạn và các kiểm duyệt viên mới xem được</string> + <string name="report_verb">Báo cáo</string> + <string name="report_reason_alert_title">Lý do báo cáo?</string> + <string name="report_compose_reason_header_other">Báo cáo khác: sẽ chỉ có các kiểm duyệt viên trong nhóm là xem được.</string> + <string name="report_compose_reason_header_spam">Báo cáo tin nhắn rác: chỉ có các kiểm duyệt viên trong nhóm là xem được.</string> + <string name="report_compose_reason_header_community">Báo cáo vi phạm: sẽ chỉ có các kiểm duyệt viên trong nhóm là xem được.</string> + <string name="size">Kích thước</string> + <string name="is_verified">%s đã được xác minh</string> + <string name="is_not_verified">%s chưa được xác minh</string> + <string name="alert_title_skipped_messages">Các tin nhắn bị bỏ qua</string> + <string name="skip_inviting_button">Bỏ qua việc mời các thành viên</string> + <string name="icon_descr_speaker_on">Loa ngoài bật</string> + <string name="icon_descr_sound_muted">Âm thanh đã bị tắt</string> + <string name="app_check_for_updates_stable">Ổn định</string> + <string name="settings_section_title_socks">PROXY SOCKS</string> + <string name="receipts_section_groups">Các nhóm nhỏ (tối đa 20 thành viên)</string> + <string name="non_fatal_errors_occured_during_import">Một vài lỗi không nghiêm trọng đã xảy ra trong lúc nhập:</string> + <string name="icon_descr_speaker_off">Loa ngoài tắt</string> + <string name="archive_report">Lưu trữ báo cáo</string> + <string name="delete_report">Xóa báo cáo</string> + <string name="operator_servers_title">Các máy chủ %s</string> + <string name="possible_slow_function_title">Hàm chạy chậm</string> + <string name="privacy_media_blur_radius_soft">Nhẹ</string> + <string name="chat_item_ttl_seconds">%s giây</string> + <string name="smp_servers_test_some_failed">Một vài máy chủ không vượt qua bài kiểm tra:</string> + <string name="star_on_github">Thả sao trên Github</string> + <string name="rcv_group_event_3_members_connected">%s, %s và %s đã được kết nối</string> + <string name="smp_server">Máy chủ SMP</string> + <string name="rcv_group_event_n_members_connected">%s, %s và %d thành viên khác đã được kết nối</string> + <string name="conn_event_disabled_pq">mã hóa đầu cuối tiêu chuẩn</string> + <string name="v5_7_shape_profile_images_descr">Hình vuông, hình tròn, hoặc bất kỳ hình thù gì ở giữa.</string> + <string name="network_socks_proxy">proxy SOCKS</string> + <string name="network_socks_proxy_settings">Cài đặt proxy SOCKS</string> + <string name="callstate_starting">đang bắt đầu…</string> + <string name="group_members_n">%s, %s và %d thành viên</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="app_check_for_updates_button_skip">Bỏ qua phiên bản này</string> + <string name="chat_database_exported_not_all_files">Một số tệp đã không được xuất</string> + <string name="audio_device_speaker">Loa ngoài</string> + <string name="start_chat_question">Bắt đầu kết nối trò chuyện?</string> + <string name="smp_servers">Các máy chủ SMP</string> + <string name="migrate_from_device_start_chat">Bắt đầu kết nối trò chuyện</string> + <string name="report_reason_spam">Tin nhắn rác</string> + <string name="privacy_media_blur_radius_strong">Mạnh</string> + <string name="servers_info_starting_from">Bắt đầu từ %s.</string> + <string name="stop_file__confirm">Dừng</string> + <string name="stop_chat_question">Dừng kết nối trò chuyện?</string> + <string name="migrate_from_device_bytes_uploaded">%s đã được tải lên</string> + <string name="subscribed">Đã đăng ký</string> + <string name="report_item_archived_by">đã lưu trữ báo cáo bởi %s</string> + <string name="stop_file__action">Dừng tệp</string> + <string name="group_reports_active_one">1 báo cáo</string> + <string name="group_reports_active">%d báo cáo</string> + <string name="group_reports_member_reports">Các báo cáo của thành viên</string> + <string name="subscription_errors">Lỗi đăng ký dài hạn</string> + <string name="chat_list_group_reports">Các báo cáo</string> + <string name="servers_info_private_data_disclaimer">Bắt đầu từ %s.\nTất cả dữ liệu được lưu trữ một cách riêng tư trên thiết bị của bạn.</string> + <string name="v4_6_audio_video_calls_descr">Hỗ trợ bluetooth và nhiều cải tiến khác.</string> + <string name="subscription_results_ignored">Các đăng ký dài hạn bị bỏ qua</string> + <string name="stop_rcv_file__title">Dừng nhận tệp?</string> + <string name="stop_snd_file__title">Dừng gửi tệp?</string> + <string name="add_contact_or_create_group">Tạo cuộc trò chuyện mới</string> + <string name="servers_info_statistics_section_header">Số liệu thống kê</string> + <string name="notifications_mode_periodic">Bắt đầu định kỳ</string> + <string name="stop_chat_confirmation">Dừng</string> + <string name="stop_sharing_address">Dừng chia sẻ địa chỉ?</string> + <string name="stop_chat_to_export_import_or_delete_chat_database">Dừng kết nối trò chuyện để xuất, nhập hoặc xóa cơ sở dữ liệu trò chuyện. Bạn sẽ không thể nhận hay gửi tin nhắn trong khi kết nối trò chuyện bị dừng lại.</string> + <string name="submit_passcode">Nhập</string> + <string name="migrate_from_device_stopping_chat">Đang dừng kết nối trò chuyện</string> + <string name="stop_sharing">Dừng chia sẻ</string> + <string name="strikethrough_text">gạch ngang</string> + <string name="auth_stop_chat">Dừng kết nối trò chuyện</string> + <string name="v4_5_italian_interface_descr">Xin gửi lời cảm ơn tới các người dùng đã góp công qua Weblate!</string> + <string name="color_mode_system">Hệ thống</string> + <string name="group_invitation_tap_to_join">Nhấn để tham gia</string> + <string name="network_option_tcp_connection_timeout">Thời gian chờ kết nối TCP</string> + <string name="tap_to_activate_profile">Nhấn để kích hoạt hồ sơ.</string> + <string name="language_system">Hệ thống</string> + <string name="v4_4_french_interface_descr">Xin gửi lời cảm ơn tới các người dùng đã góp công qua Weblate!</string> + <string name="v4_6_chinese_spanish_interface_descr">Xin gửi lời cảm ơn tới các người dùng đã góp công qua Weblate!</string> + <string name="v5_0_polish_interface_descr">Xin gửi lời cảm ơn tới các người dùng đã góp công qua Weblate!</string> + <string name="system_mode_toast">Chế độ hệ thống</string> + <string name="settings_section_title_support">HỖ TRỢ SIMPLEX CHAT</string> + <string name="temporary_file_error">Lỗi tệp tạm thời</string> + <string name="chat_help_tap_button">Nhấn nút</string> + <string name="network_option_tcp_connection">Kết nối TCP</string> + <string name="la_mode_system">Hệ thống</string> + <string name="whats_new_thanks_to_users_contribute_weblate">Xin gửi lời cảm ơn tới các người dùng đã góp công qua Weblate!</string> + <string name="contact_tap_to_connect">Nhấn để Kết nối</string> + <string name="address_creation_instruction">Nhấn Tạo địa chỉ SimpleX ở menu để tạo sau.</string> + <string name="group_invitation_tap_to_join_incognito">Nhấn để tham gia một cách ẩn danh</string> + <string name="theme_system">Hệ thống</string> + <string name="v6_1_better_calls_descr">Chuyển đổi âm thanh và video trong lúc gọi.</string> + <string name="v6_1_switch_chat_profile_descr">Chuyển đổi hồ sơ trò chuyện cho các lời mời dùng 1 lần.</string> + <string name="error_smp_test_failed_at_step">Kiểm tra thất bại tại bước %s.</string> + <string name="la_lock_mode_system">Xác thực hệ thống</string> + <string name="tap_to_scan">Nhấn để quét</string> + <string name="thank_you_for_installing_simplex">Cảm ơn bạn đã cài đặt SimpleX Chat!</string> + <string name="smp_servers_test_server">Kiểm tra máy chủ</string> + <string name="smp_servers_test_servers">Kiểm tra các máy chủ</string> + <string name="tap_to_start_new_chat">Nhấn để bắt đầu một cuộc trò chuyện mới</string> + <string name="tap_to_paste_link">Nhấn để dán đường dẫn</string> + <string name="settings_message_shape_tail">Đuôi</string> + <string name="switch_verb">Chuyển đổi</string> + <string name="connection_error_quota_desc">Kết nối đã chạm giới hạn của các tin nhắn chưa được gửi đi, liên hệ của bạn có thể đang ngoại tuyến.</string> + <string name="next_generation_of_private_messaging">Tương lai của nhắn tin</string> + <string name="image_decoding_exception_desc">Hình ảnh không thể được giải mã. Xin vui lòng thử lại với một hình ảnh khác hoặc liên lạc với các nhà phát triển.</string> + <string name="moderate_messages_will_be_deleted_warning">Các tin nhắn sẽ bị xóa cho tất cả các thành viên.</string> + <string name="sync_connection_force_desc">Mã hóa đang hoạt động và thỏa thuận mã hóa mới là không bắt buộc. Nó có thể dẫn đến các lỗi kết nối!</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">Ứng dụng sẽ yêu cầu bạn xác nhận các lượt tải xuống từ các máy chủ truyền tệp không xác định (ngoại trừ .onion hoặc khi proxy SOCKS được sử dụng).</string> + <string name="connection_error_blocked">Kết nối đã bị chặn</string> + <string name="connection_error_blocked_desc">Kết nối đã bị chặn bởi bên vận hành máy chủ:\n%1$s.</string> + <string name="blocking_reason_content">Nội dung vi phạm các điều kiện sử dụng</string> + <string name="blocking_reason_spam">Tin nhắn rác</string> + <string name="file_error_blocked">Tệp đã bị chặn bởi bên vận hành máy chủ:\n%1$s.</string> + <string name="database_initialization_error_desc">Cơ sở dữ liệu đang không hoạt động như bình thường. Nhấn để tìm hiểu thêm</string> + <string name="privacy_chat_list_open_links_no">Không</string> + <string name="privacy_chat_list_open_web_link">Mở đường dẫn</string> + <string name="privacy_chat_list_open_links_ask">Yêu cầu</string> + <string name="privacy_chat_list_open_web_link_question">Mở đường dẫn web?</string> + <string name="privacy_chat_list_open_links">Mở các đường dẫn từ danh sách trò chuyện</string> + <string name="connection_you_accepted_will_be_cancelled">Kết nối bạn đã chấp nhận sẽ bị hủy bỏ!</string> + <string name="contact_you_shared_link_with_wont_be_able_to_connect">Liên hệ mà bạn đã chia sẻ đường dẫn này sẽ không thể kết nối!</string> + <string name="unable_to_open_browser_desc">Trình duyệt web mặc định là cần thiết cho các cuộc gọi. Xin vui lòng thiết lập trình duyệt mặc định trong hệ thống, và chia sẻ thêm thông tin với các nhà phát triển.</string> + <string name="onboarding_network_operators_app_will_use_different_operators">Ứng dụng bảo vệ sự riêng tư của bạn bằng cách sử dụng các bên vận hành khác nhau trong mỗi cuộc trò chuyện.</string> + <string name="system_restricted_background_in_call_desc">Ứng dụng có thể bị đóng sau 1 phút chạy trong nền.</string> + <string name="periodic_notifications_desc">Ứng dụng tìm nhận tin nhắn mới một cách định kỳ - nó tiêu tốn một vài phần trăm pin mỗi ngày. Ứng dụng không sử dụng thông báo đẩy - dữ liệu ở thiết bị của bạn không được gửi đi tới máy chủ nào.</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">Mã mà bạn đã quét không phải là một mã QR dẫn SimpleX.</string> + <string name="theme">Chủ đề</string> + <string name="alert_text_msg_bad_hash">Mã băm của tin nhắn trước có sự khác biệt.\"</string> + <string name="alert_text_msg_bad_id">ID của tin nhắn tiếp theo là không chính xác (nhỏ hơn hoặc bằng với cái trước).\nViệc này có thể xảy ra do một vài lỗi hoặc khi kết nối bị xâm phạm.</string> + <string name="database_backup_can_be_restored">Nỗ lực đổi mật khẩu cơ sở dữ liệu đã không được hoàn thành.</string> + <string name="this_device_name_shared_with_mobile">Tên thiết bị sẽ được chia sẻ với thiết bị di động đã được kết nối.</string> + <string name="settings_section_title_themes">CÁC CHỦ ĐỀ</string> + <string name="failed_to_create_user_invalid_desc">Tên hiển thị này không hợp lệ. Xin vui lòng chọn một cái tên khác.</string> + <string name="profile_is_only_shared_with_your_contacts">Hồ sơ chỉ được chia sẻ với các liên hệ của bạn.</string> + <string name="e2ee_info_pq_short">Cuộc trò chuyện này được bảo vệ bằng mã hóa đầu cuối có kháng lượng tử.</string> + <string name="settings_is_storing_in_clear_text">Mật khẩu được lưu trữ trong cài đặt dưới dạng thuần văn bản.</string> + <string name="this_device">Thiết bị này</string> + <string name="video_decoding_exception_desc">Video không thể được giải mã. Xin vui lòng thử với một video khác hoặc liên lạc với các nhà phát triển.</string> + <string name="moderate_message_will_be_marked_warning">Tin nhắn sẽ bị đánh dấu là đã được kiểm duyệt cho tất cả các thành viên.</string> + <string name="moderate_message_will_be_deleted_warning">Tin nhắn sẽ bị xóa cho tất cả các thành viên.</string> + <string name="delete_files_and_media_desc">Việc này không thể được hoàn tác - tất cả các đa phương tiện và tệp đã được gửi và nhận sẽ bị xóa. Những hình ảnh chất lượng thấp sẽ được giữ lại.</string> + <string name="enable_automatic_deletion_message">Việc này không thể được hoàn tác - các tin nhắn đã được gửi và nhận sớm hơn so với thời gian được chọn sẽ bị xóa. Có thể mất vài phút để hoàn thành.</string> + <string name="alert_message_no_group">Nhóm này không còn tồn tại.</string> + <string name="send_receipts_disabled_alert_msg">Nhóm này có trên %1$d thành viên, chỉ báo đã nhận không được gửi.</string> + <string name="moderate_messages_will_be_marked_warning">Các tin nhắn sẽ bị đánh dấu là đã được kiểm duyệt cho tất cả thành viên.</string> + <string name="this_device_name">Tên thiết bị này</string> + <string name="receipts_section_description_1">Chúng có thể bị ghi đề trong cài đặt liên hệ và nhóm.</string> + <string name="e2ee_info_no_pq_short">Cuộc trò chuyện này được bởi vệ bằng mã hóa đầu cuối.</string> + <string name="in_developing_desc">Tính năng này chưa được hỗ trợ. Hãy thử bản phát hành tiếp theo.</string> + <string name="v5_2_message_delivery_receipts_descr">Dấu tick thứ hai mà chúng ta từng thiếu! ✅</string> + <string name="xftp_servers_per_user">Những máy chủ cho tệp mới của hồ sơ trò chuyện hiện tại của bạn</string> + <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Nền tảng ứng dụng và nhắn tin bảo vệ sự riêng tư và bảo mật của bạn.</string> + <string name="v6_2_network_decentralization_descr">Bên vận hành được cài sẵn thứ hai trong ứng dụng!</string> + <string name="member_role_will_be_changed_with_notification_chat">Chức vụ sẽ được đổi thành %s. Tất cả mọi người trong cuộc trò chuyện sẽ được thông báo.</string> + <string name="the_text_you_pasted_is_not_a_link">Văn bản bạn vừa dán không phải là một đường dẫn SimpleX.</string> + <string name="member_role_will_be_changed_with_invitation">Chức vụ sẽ được đổi thành %s. Thành viên sẽ nhận được một lời mời mới.</string> + <string name="passphrase_will_be_saved_in_settings">Mật khẩu sẽ được lưu trữ trong cài đặt dưới dạng thuần văn bản sau khi bản đổi nó hoặc khởi động lại ứng dụng.</string> + <string name="receipts_section_description">Các cài đặt này là cho hồ sơ trò chuyện hiện tại của bạn</string> + <string name="member_role_will_be_changed_with_notification">Chức vụ sẽ được đổi thành %s. Tất cả mọi người trong nhóm sẽ được thông báo.</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">Bản lưu trữ cơ sở dữ liệu đã được tải lên sẽ bị xóa vĩnh viễn khỏi các máy chủ.</string> + <string name="delete_chat_profile_action_cannot_be_undone_warning">Việc này không thể được hoàn tác - hồ sơ, các liên hệ, tin nhắn và tệp của bạn sẽ biến mất mà không thể khôi phục.</string> + <string name="smp_servers_per_user">Những máy chủ cho các kết nối mới của hồ sơ trò chuyện hiện tại của bạn</string> + <string name="report_archive_alert_desc">Báo cáo sẽ được lưu trữ cho bạn.</string> + <string name="message_deleted_or_not_received_error_desc">Tin nhắn này đã bị xóa hoặc vẫn chưa được nhận.</string> + <string name="this_QR_code_is_not_a_link">Mã QR này không phải là một đường dẫn!</string> + <string name="this_link_is_not_a_valid_connection_link">Đường dẫn này không phải là một đường dẫn kết nối hợp lệ!</string> + <string name="one_hand_ui_card_title">Chuyển đổi danh sách trò chuyện:</string> + <string name="remote_ctrl_error_timeout">Thời gian chờ đã hết trong khi kết nối tới máy tính</string> + <string name="open_port_in_firewall_desc">Để 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</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">Để 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ó.</string> + <string name="enter_passphrase_notification_desc">Để nhận thông báo, xin vui lòng nhập mật khẩu cơ sở dữ liệu</string> + <string name="images_limit_title">Quá nhiều ảnh!</string> + <string name="videos_limit_title">Quá nhiều video!</string> + <string name="operator_use_for_sending">Để gửi</string> + <string name="remote_ctrl_connection_stopped_identity_desc">Đường dẫn này đã được sử dụng với một thiết bị di động khác, xin vui lòng tạo một đường dẫn mới trên máy tính.</string> + <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Để bảo vệ thông tin của bạn, bật Khóa SimpleX.\nBạn sẽ được nhắc để hoàn thành xác thực trước khi tính năng này được bật.</string> + <string name="this_string_is_not_a_connection_link">Chuỗi ký tự này không phải là một đường dẫn kết nối!</string> + <string name="call_desktop_permission_denied_title">Để thực hiện các cuộc gọi, cho phép sử dụng mic của bạn. Kết thúc cuộc gọi và thử gọi lại.</string> + <string name="operator_use_for_messages_receiving">Để nhận</string> + <string name="scan_qr_to_connect_to_contact">Để kết nối, liên hệ của bạn có thể quét mã QR hoặc dùng đường dẫn trong ứng dụng.</string> + <string name="color_title">Tiêu đề</string> + <string name="v5_3_simpler_incognito_mode_descr">Chuyển đổi ẩn danh khi kết nối.</string> + <string name="v4_5_private_filenames_descr">Để bảo vệ múi giờ, các tệp hình ảnh/âm thanh sử dụng UTC.</string> + <string name="this_text_is_available_in_settings">Văn bản này có sẵn trong cài đặt</string> + <string name="app_check_for_updates_notice_desc">Để được thông báo về các bản phát hành mới, bật kiểm tra định kỳ cho các phiên bản Ổn định hoặc Beta.</string> + <string name="v5_4_block_group_members_descr">Để ẩn các tin nhắn không mong muốn.</string> + <string name="to_reveal_profile_enter_password">Để tiết lộ hồ sơ ẩn của bạn, nhập đầy đủ mật khẩu vào trường tìm kiếm trong trang Các hồ sơ trò chuyện của bạn.</string> + <string name="connect_plan_this_is_your_own_one_time_link">Đây là đường dẫn dùng một lần của riêng bạn!</string> + <string name="messages_section_description">Cài đặt này áp dụng cho các tin nhắn trong hồ sơ trò chuyện hiện tại của bạn</string> + <string name="connect_plan_this_is_your_own_simplex_address">Đây là địa chỉ SimpleX của riêng bạn!</string> + <string name="to_connect_via_link_title">Để kết nối qua đường dẫn</string> + <string name="to_protect_against_your_link_replaced_compare_codes">Để bảo vệ đường dẫn của bạn khỏi bị thay thế, bạn có thể so sánh các mã bảo mật liên lạc.</string> + <string name="private_routing_explanation">Để bảo vệ địa chỉ IP của bạn, định tuyến riêng tư sử dụng các máy chủ SMP của bạn để gửi tin nhắn.</string> + <string name="v4_5_transport_isolation">Cách ly truyền tải</string> + <string name="network_smp_proxy_mode_unknown">Các máy chủ không xác định</string> + <string name="group_member_status_unknown">trạng thái không xác định</string> + <string name="unknown_error">Lỗi không xác định</string> + <string name="connection_error_quota">Các tin nhắn không được gửi đi</string> + <string name="unfavorite_chat">Bỏ yêu thích</string> + <string name="unblock_for_all">Bỏ chặn cho tất cả</string> + <string name="unblock_for_all_question">Bỏ chặn thành viên cho tất cả?</string> + <string name="to_start_a_new_chat_help_header">Để bắt đầu một cuộc trò chuyện mới</string> + <string name="unhide_chat_profile">Bỏ ẩn hồ sơ trò chuyện</string> + <string name="unhide_profile">Bỏ ẩn hồ sơ</string> + <string name="rcv_group_event_member_unblocked">đã bỏ chặn %s</string> + <string name="unblock_member_confirmation">Bỏ chặn</string> + <string name="unblock_member_button">Bỏ chặn thành viên</string> + <string name="unblock_member_question">Bỏ chặn thành viên?</string> + <string name="appearance_in_app_bars_alpha">Độ trong suốt</string> + <string name="servers_info_transport_sessions_section_header">Các phiên truyền tải</string> + <string name="file_not_approved_title">Các máy chủ không xác định!</string> + <string name="icon_descr_sent_msg_status_unauthorized_send">gửi mà không được cho phép</string> + <string name="to_share_with_your_contact">(để chia sẻ với liên hệ của bạn)</string> + <string name="user_unhide">Bỏ ẩn</string> + <string name="la_notice_turn_on">Bật</string> + <string name="servers_info_subscriptions_total">Tổng</string> + <string name="group_member_status_unknown_short">không xác định</string> + <string name="trying_to_connect_to_server_to_receive_messages_with_error">Đang cố gắng kết nối tới máy chủ dùng để nhận tin nhắn từ liên hệ này (lỗi: %1$s).</string> + <string name="trying_to_connect_to_server_to_receive_messages">Đang cố gắng kết nối tới máy chủ dùng để nhận tin nhắn từ liên hệ này.</string> + <string name="to_verify_compare">Để xác minh mã hóa đầu cuối với liên hệ của bạn, so sánh (hoặc quét) mã trên các thiết bị của các bạn.</string> + <string name="network_session_mode_transport_isolation">Cách ly truyền tải</string> + <string name="unknown_message_format">định dạng tin nhắn không xác định</string> + <string name="unknown_database_error_with_info">Lỗi cơ sở dữ liệu không xác định: %s</string> + <string name="recent_history_is_sent_to_new_members">Tối đa tới 100 tin nhắn cuối cùng là được gửi tới các thành viên mới.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">Sử dụng các thông tin đăng nhập proxy khác cho mỗi kết nối.</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">Sử dụng các thông tin đăng nhập proxy khác nhau cho mỗi hồ sơ.</string> + <string name="update_network_session_mode_question">Cập nhật chế độ cách ly truyền tải?</string> + <string name="unlink_desktop_question">Hủy liên kết máy tính?</string> + <string name="smp_server_test_upload_file">Tải lên tệp</string> + <string name="app_check_for_updates_update_available">Có bản cập nhật: %s</string> + <string name="updating_settings_will_reconnect_client_to_all_servers">Việc cập nhật cài đặt sẽ kết nối lại thiết bị với tất cả các máy chủ.</string> + <string name="uploaded_files">Đã tải lên các tệp</string> + <string name="icon_descr_received_msg_status_unread">chưa đọc</string> + <string name="update_database">Cập nhật</string> + <string name="update_database_passphrase">Cập nhật mật khẩu cơ sở dữ liệu</string> + <string name="unlink_desktop">Hủy liên kết</string> + <string name="update_network_settings_question">Cập nhật cài đặt mạng?</string> + <string name="migrate_from_device_upload_failed">Tải lên đã thất bại</string> + <string name="migrate_from_device_uploading_archive">Đang tải lên bản lưu trữ</string> + <string name="app_check_for_updates_canceled">Tải xuống bản cập nhật đã bị hủy</string> + <string name="user_unmute">Bật thông báo</string> + <string name="upgrade_and_open_chat">Nâng cấp và mở kết nối trò chuyện</string> + <string name="unmute_chat">Bật thông báo</string> + <string name="rcv_group_event_updated_group_profile">đã cập nhật hồ sơ nhóm</string> + <string name="profile_update_event_updated_profile">đã cập nhật hồ sơ</string> + <string name="connect_use_current_profile">Sử dụng hồ sơ hiện tại</string> + <string name="network_disable_socks">Sử dụng kết nối Internet trực tiếp?</string> + <string name="network_smp_proxy_mode_unprotected">Không được bảo vệ</string> + <string name="connection_error_auth_desc">Trừ khi liên hệ của bạn đã xóa kết nối hoặc đường dẫn này đã được sử dụng, có thể đã có lỗi - xin vui lòng báo cáo.\nĐể kết nối, xin vui lòng yêu cầu liên hệ của bạn tạo một đường dẫn kết nối khác và chắc chắn rằng bạn có kết nối mạng ổn định.</string> + <string name="upload_errors">Lỗi tải lên</string> + <string name="v6_0_upgrade_app">Nâng cấp ứng dụng một cách tự động</string> + <string name="servers_info_uploaded">Đã tải lên</string> + <string name="update_network_settings_confirmation">Cập nhật</string> + <string name="onboarding_network_operators_update">Cập nhật</string> + <string name="auth_unlock">Mở khóa</string> + <string name="v4_4_verify_connection_security">Xác minh bảo mật kết nối</string> + <string name="operator_use_for_files">Sử dụng cho các tệp</string> + <string name="v6_0_reachable_chat_toolbar_descr">Dùng ứng dụng với một tay.</string> + <string name="verify_code_on_mobile">Xác minh mã trên di động</string> + <string name="migrate_from_device_verify_database_passphrase">Xác minh mật khẩu cơ sở dữ liệu</string> + <string name="network_smp_proxy_mode_unknown_description">Sử dụng định tuyến riêng tư với các máy chủ không xác định.</string> + <string name="use_servers_of_operator_x">Sử dụng %s</string> + <string name="operator_use_operator_toggle_description">Sử dụng các máy chủ</string> + <string name="v5_6_picture_in_picture_calls_descr">Dùng ứng dụng khi đang trong cuộc gọi.</string> + <string name="verify_connection">Xác minh kết nối</string> + <string name="using_simplex_chat_servers">Đang sử dụng các máy chủ SimpleX Chat.</string> + <string name="verify_connections">Xác minh các kết nối</string> + <string name="operator_use_for_messages">Sử dụng cho các tin nhắn</string> + <string name="network_proxy_username">Tên người dùng</string> + <string name="network_proxy_random_credentials">Sử dụng các thông tin đăng nhập ngẫu nhiên</string> + <string name="network_use_onion_hosts">Sử dụng các dịch vụ .onion</string> + <string name="verify_security_code">Xác minh mã bảo mật</string> + <string name="connect_use_new_incognito_profile">Sử dụng hồ sơ ẩn danh mới</string> + <string name="simplex_link_connection">qua %1$s</string> + <string name="network_socks_toggle_use_socks_proxy">Sử dụng proxy SOCKS</string> + <string name="use_chat">Sử dụng SimpleX Chat</string> + <string name="smp_servers_use_server_for_new_conn">Sử dụng cho các kết nối mới</string> + <string name="smp_servers_use_server">Sử dụng máy chủ</string> + <string name="network_enable_socks">Sử dụng proxy SOCKS?</string> + <string name="use_simplex_chat_servers__question">Sử dụng các máy chủ SimpleX Chat?</string> + <string name="migrate_from_device_verify_passphrase">Xác minh mật khẩu</string> + <string name="verify_code_with_desktop">Xác minh mã với máy tính</string> + <string name="use_random_passphrase">Sử dụng mật khẩu ngẫu nhiên</string> + <string name="settings_section_title_use_from_desktop">Sử dụng từ máy tính</string> + <string name="network_smp_proxy_mode_unprotected_description">Sử dụng định tuyến riêng tư với các máy chủ không xác định khi địa chỉ IP không được bảo vệ.</string> + <string name="view_conditions">Xem các điều kiện</string> + <string name="description_via_one_time_link">qua đường dẫn dùng một lần</string> + <string name="voice_messages_are_prohibited">Các tin nhắn thoại bị cấm.</string> + <string name="recent_history">Lịch sử hữu hình</string> + <string name="call_service_notification_video_call">Cuộc gọi video</string> + <string name="icon_descr_video_snd_complete">Video đã được gửi</string> + <string name="voice_message_send_text">Tin nhắn thoại…</string> + <string name="voice_messages_not_allowed">Các tin nhắn thoại là không được cho phép</string> + <string name="voice_message">Tin nhắn thoại</string> + <string name="gallery_video_button">Video</string> + <string name="voice_message_with_duration">Tin nhắn thoại (%1$s)</string> + <string name="icon_descr_video_on">Video bật</string> + <string name="voice_prohibited_in_this_chat">Các tin nhắn thoại bị cấm trong cuộc trò chuyện này.</string> + <string name="voice_messages">Tin nhắn thoại</string> + <string name="v5_4_link_mobile_desktop_descr">Qua giao thức kháng lượng tử an toàn.</string> + <string name="description_via_group_link">qua đường dẫn nhóm</string> + <string name="simplex_link_mode_browser">Qua trình duyệt</string> + <string name="info_view_video_button">video</string> + <string name="view_updated_conditions">Xem các điều kiện đã được cập nhật</string> + <string name="app_was_crashed">Xem sự cố</string> + <string name="icon_descr_video_off">Video tắt</string> + <string name="video_will_be_received_when_contact_completes_uploading">Video sẽ được nhận khi liên hệ của bạn hoàn thành việc tải nó lên.</string> + <string name="video_will_be_received_when_contact_is_online">Video sẽ được nhận khi liên hệ của bạn trực tuyến, xin vui lòng chờ hoặc kiểm tra lại sau!</string> + <string name="view_security_code">Xem mã bảo mật</string> + <string name="v5_0_large_files_support">Các tệp và video với kích thước tối đa lên tới 1gb</string> + <string name="icon_descr_video_call">cuộc gọi video</string> + <string name="call_connection_via_relay">thông qua relay</string> + <string name="v4_3_voice_messages">Tin nhắn thoại</string> + <string name="video_call_no_encryption">cuộc gọi video (không được mã hóa đầu cuối)</string> + <string name="description_via_contact_address_link">qua đường dẫn địa chỉ liên lạc</string> + <string name="video_descr">Video</string> + <string name="we_do_not_store_contacts_or_messages_on_servers">Chúng tôi không lưu bất kỳ liên hệ hay tin nhắn nào của bạn (một khi đã được gửi) trên các máy chủ.</string> + <string name="operator_website">Website</string> + <string name="personal_welcome">Xin chào %1$s!</string> + <string name="network_use_onion_hosts_prefer">Khi có sẵn</string> + <string name="voice_messages_prohibited">Các tin nhắn thoại bị cấm!</string> + <string name="group_welcome_title">Lời chào</string> + <string name="color_wallpaper_tint">Màu sơ cấp hình nền</string> + <string name="icon_descr_waiting_for_image">Đang chờ hình ảnh</string> + <string name="waiting_for_image">Đang chờ hình ảnh</string> + <string name="callstate_waiting_for_confirmation">đang chờ xác nhận…</string> + <string name="whats_new">Có gì mới</string> + <string name="custom_time_unit_weeks">tuần</string> + <string name="button_welcome_message">Lời chào</string> + <string name="color_wallpaper_background">Màu nền hình nền</string> + <string name="waiting_for_desktop">Đang chờ máy tính…</string> + <string name="waiting_for_mobile_to_connect">Đang chờ di động để kết nối:</string> + <string name="welcome">Xin chào!</string> + <string name="waiting_for_video">Đang chờ video</string> + <string name="contact_wants_to_connect_with_you">muốn kết nối với bạn!</string> + <string name="waiting_for_file">Đang chờ tệp</string> + <string name="callstate_waiting_for_answer">đang chờ trả lời…</string> + <string name="welcome_message_is_too_long">Lời chào quá dài</string> + <string name="v5_1_better_messages_descr">- tin nhắn thoại với thời lượng tối đa lên tới 5 phút.\n- tùy chỉnh thời gian để tự xóa.\n- lịch sử chỉnh sử.</string> + <string name="database_downgrade_warning">Cảnh báo: bạn có thể mất một số dữ liệu!</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Cảnh báo: khởi động kết nối trò chuyện trên nhiều thiết bị không được hỗ trợ và sẽ gây ra các lỗi gửi tin nhắn</string> + <string name="webrtc_ice_servers">Các máy chủ ICE WebRTC</string> + <string name="onboarding_notifications_mode_off">Khi ứng dụng đang chạy</string> + <string name="v5_7_call_sounds_descr">Khi kết nối các cuộc gọi video và âm thanh.</string> + <string name="network_smp_proxy_fallback_allow_protected">Khi IP bị ẩn</string> + <string name="icon_descr_waiting_for_video">Đang chờ video</string> + <string name="network_smp_proxy_fallback_allow">Có</string> + <string name="change_automatic_deletion_question">Thay đổi xóa tin nhắn tự động?</string> + <string name="disable_automatic_deletion_message">Các tin nhắn trong cuộc trò chuyện này sẽ không bao giờ bị xóa.</string> + <string name="xftp_server">Máy chủ XFTP</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Không có Tor hoặc VPN, địa chỉ IP của bạn sẽ bị lộ ra cho các máy chủ truyền tệp.</string> + <string name="v5_7_quantum_resistant_encryption_descr">Sẽ được kích hoạt trong các cuộc trò chuyện trực tiếp!</string> + <string name="network_type_ethernet">Mạng ethernet có dây</string> + <string name="snd_error_auth">Khóa sai hoặc kết nối không xác định - khả năng cao kết nối này đã bị xóa.</string> + <string name="wrong_passphrase_title">Mật khẩu sai rồi!</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="privacy_chat_list_open_links_yes">Có</string> + <string name="wrong_passphrase">Mật khẩu cơ sở dữ liệu sai</string> + <string name="incognito_info_share">Khi bạn chia sẻ một hồ sơ ẩn danh với ai đó, hồ sơ này sẽ được sử dụng cho các nhóm mà họ mời bạn tham gia.</string> + <string name="chat_item_ttl_default">mặc định (%s)</string> + <string name="chat_ttl_options_footer">Xóa tin nhắn trò chuyện khỏi thiết bị của bạn.</string> + <string name="disable_automatic_deletion_question">Tắt xóa tin nhắn tự động?</string> + <string name="disable_automatic_deletion">Tắt tính năng xóa tin nhắn</string> + <string name="text_field_set_chat_placeholder">Đặt tên cuộc trò chuyện…</string> + <string name="file_not_approved_descr">Không có Tor hoặc VPN, địa chỉ IP của bạn sẽ bị lộ ra cho các relay XFTP sau đây:\n%1$s.</string> + <string name="xftp_servers">Các máy chủ XFTP</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">Khi có nhiều hơn một bên vận hành được kích hoạt, không ai trong số họ có siêu dữ liệu để biết được ai trò chuyện với ai.</string> + <string name="v5_5_private_notes_descr">Với các tệp và đa phương tiện được mã hóa.</string> + <string name="v5_5_message_delivery_descr">Mức sử dụng pin đã được giảm xuống.</string> + <string name="v5_8_message_delivery_descr">Mức sử dụng pin đã được giảm xuống.</string> + <string name="v4_2_auto_accept_contact_requests_desc">Với lời chào tùy chọn.</string> + <string name="you_can_accept_or_reject_connection">Khi mọi người gửi yêu cầu kết nối, bạn có thể chấp nhận hoặc từ chối nó.</string> + <string name="file_error_auth">Khóa sai hoặc địa chỉ khối tệp không xác định - khả năng cao tệp đã bị xóa.</string> + <string name="chat_item_ttl_year">1 năm</string> + <string name="change_automatic_chat_deletion_message">Việc này không thể được hoàn tác - các tin nhắn đã được gửi và nhận trong cuộc trò chuyện này sớm hơn thời gian được chọn sẽ bị xóa.</string> + <string name="chat_preferences_yes">có</string> + <string name="you_can_make_address_visible_via_settings">Bạn có thể hiển thị nó cho các liên hệ SimpleX của mình thông qua Cài đặt.</string> + <string name="you_can_create_it_later">Bạn có thể tạo nó sau</string> + <string name="one_hand_ui_change_instruction">Bạn có thể thay đổi nói trong cài đặt Giao diện.</string> + <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Bạn đang tham gia nhóm thông qua đường dẫn này.</string> + <string name="you_can_enable_delivery_receipts_later">Bạn có thể bật vào lúc sau thông qua Cài đặt</string> + <string name="settings_section_title_you">BẠN</string> + <string name="you_can_share_group_link_anybody_will_be_able_to_connect">Bạn có thể chia sẻ một đường dẫn hoặc mã QR - bất kỳ ai cũng sẽ có thể tham gia nhóm. Bạn sẽ không mất các thành viên của nhóm nếu sau này bạn xóa nó đi.</string> + <string name="migrate_to_device_try_again">Bạn có thể thử một lần nữa.</string> + <string name="connected_to_server_to_receive_messages_from_contact">Bạn đã kết nối tới máy chủ dùng để nhận tin nhắn từ liên hệ này.</string> + <string name="failed_to_create_user_duplicate_desc">Bạn đã có một hồ sơ trò chuyện với cùng một tên hiển thị. Xin vui lòng chọn một cái tên khác.</string> + <string name="you_can_still_send_messages_to_contact">Bạn có thể gửi tin nhắn tới %1$s từ Các liên hệ được lưu trữ.</string> + <string name="you_are_already_connected_to_vName_via_this_link">Bạn đã kết nối với %1$s rồi.</string> + <string name="chat_preferences_you_allow">Bạn cho phép</string> + <string name="servers_info_proxied_servers_section_footer">Bạn không được kết nối với các máy chủ này. Định tuyến riêng tư được sử dụng để gửi tin nhắn tới chúng.</string> + <string name="onboarding_network_operators_conditions_you_can_configure">Bạn có thể định cấu hình các bên vận hành trong cài đặt Mạng & máy chủ.</string> + <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">Bạn đang kết nối thông qua đường dẫn dùng một lần này!</string> + <string name="migrate_from_device_try_again">Bạn có thể thử một lần nữa.</string> + <string name="you_are_invited_to_group_join_to_connect_with_group_members">Bạn được mời vào nhóm. Tham gia để kết nối với các thành viên nhóm.</string> + <string name="you_are_invited_to_group">Bạn được mời vào nhóm</string> + <string name="group_info_member_you">bạn: %1$s</string> + <string name="onboarding_network_operators_configure_via_settings">Bạn có thể tùy chỉnh các máy chủ thông qua cài đặt.</string> + <string name="you_can_set_connection_name_to_remember">Bạn có thể đặt tên kết nối, để nhớ xem đường dẫn đã được chia sẻ với ai.</string> + <string name="you_are_observer">bạn là quan sát viên</string> + <string name="maximum_message_size_reached_forwarding">Bạn có thể sao chép và giảm kích thước tin nhắn để gửi nó đi.</string> + <string name="you_can_enable_delivery_receipts_later_alert">Bạn có thể bật chúng vào lúc sau thông qua cài đặt Quyền riêng tư & Bảo mật của ứng dụng.</string> + <string name="you_can_hide_or_mute_user_profile">Bạn có thể ẩn hoặc tắt thông báo một hồ sơ người dùng - giữ nó trong phần menu.</string> + <string name="snd_group_event_member_blocked">bạn đã chặn %s</string> + <string name="you_accepted_connection">Bạn đã chấp nhận kết nối</string> + <string name="sender_you_pronoun">bạn</string> + <string name="group_preview_you_are_invited">bạn được mời vào nhóm</string> + <string name="your_calls">Các cuộc gọi của bạn</string> + <string name="you_can_turn_on_lock">Bạn có thể bật Khóa SimpleX thông qua Cài đặt.</string> + <string name="you_have_no_chats">Bạn không có cuộc trò chuyện nào</string> + <string name="you_can_still_view_conversation_with_contact">Bạn vẫn có thể xem cuộc hội thoại với %1$s trong danh sách các cuộc trò chuyện.</string> + <string name="you_joined_this_group">Bạn đã tham gia nhóm này</string> + <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Bạn đã tham gia nhóm này. Đang kết nối tới thành viên nhóm đi mời.</string> + <string name="la_could_not_be_verified">Bạn đã không thể được xác minh; xin vui lòng thử lại.</string> + <string name="snd_group_event_changed_role_for_yourself">bạn đã thay đổi chức vụ của mình thành %s</string> + <string name="chat_database_exported_save">Bạn có thể lưu bản lưu trữ đã được xuất.</string> + <string name="database_is_not_encrypted">Cơ sở dữ liệu trò chuyện của bạn không được mã hóa - đặt mật khẩu để bảo vệ nó.</string> + <string name="you_have_to_enter_passphrase_every_time">Bạn phải nhập mật khẩu mỗi lần ứng dụng khởi chạy - nó không được lưu trên thiết bị của bạn.</string> + <string name="snd_group_event_user_left">bạn đã rời</string> + <string name="you_need_to_allow_calls">Bạn cần cho phép liên hệ của mình thực hiện cuộc gọi để có thể gọi cho họ.</string> + <string name="snd_group_event_changed_member_role">bạn đã thay đổi chức vụ của %s thành %s</string> + <string name="snd_conn_event_switch_queue_phase_completed">bạn đã thay đổi địa chỉ</string> + <string name="connect_plan_you_have_already_requested_connection_via_this_address">Bạn đã yêu cầu kết nối thông qua địa chỉ này rồi!</string> + <string name="you_invited_a_contact">Bạn đã mời một liên hệ</string> + <string name="you_can_share_your_address">Bạn có thể chia sẻ địa chỉ của mình dưới dạng một đường dẫn hoặc mã QR - bất kỳ ai cũng có thể kết nối với bạn.</string> + <string name="you_can_view_invitation_link_again">Bạn có thể xem đường dẫn mời lần nữa trong chi tiết kết nối.</string> + <string name="you_control_your_chat">Bạn kiểm soát cuộc trò chuyện của mình!</string> + <string name="observer_cant_send_message_title">Bạn không thể gửi tin nhắn!</string> + <string name="you_can_share_this_address_with_your_contacts">Bạn có thể chia sẻ địa chỉ này với các liên hệ của mình để họ kết nối với %s.</string> + <string name="you_need_to_allow_to_send_voice">Bạn cần cho phép liên hệ của mình gửi tin nhắn thoại để có thể gửi cho họ.</string> + <string name="chat_database_exported_migrate">Bạn có thể di chuyển cơ sở dữ liệu đã được xuất.</string> + <string name="you_must_use_the_most_recent_version_of_database">Bạn CHỈ nên sử dụng phiên bản gần nhất của cơ sở dữ liệu trò chuyện của mình trên một thiết bị mà thôi, nếu không thì bạn có thể dừng nhận tin nhắn từ một vài liên hệ.</string> + <string name="snd_conn_event_switch_queue_phase_completed_for_member">bạn đã thay đổi địa chỉ cho %s</string> + <string name="you_can_use_markdown_to_format_messages__prompt">Bạn có thể sử dụng markdown để định dạng tin nhắn:</string> + <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Bạn có thể bắt đầu kết nối trò chuyện thông qua phần Cài đặt / Cơ sở dữ liệu ở trên ứng dụng hoặc bằng cách khởi động lại ứng dụng.</string> + <string name="people_can_connect_only_via_links_you_share">Bạn quyết định ai có thể kết nối tới.</string> + <string name="your_chat_database">Cơ sở dữ liệu trò chuyện của bạn</string> + <string name="your_privacy">Sự riêng tư của bạn</string> + <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Cơ sở dữ liệu trò chuyện hiện tại của bạn sẽ bị XÓA và THAY THẾ bằng cái được nhập vào.\nViệc này không thể được hoàn tác - hồ sơ, các liên hệ, tin nhắn và tệp của bạn sẽ biến mất mà không thể khôi phục.</string> + <string name="snd_group_event_member_deleted">bạn đã xóa %1$s</string> + <string name="your_contacts_will_remain_connected">Các liên hệ của bạn vẫn sẽ được kết nối.</string> + <string name="your_profile_is_stored_on_your_device">Hồ sơ, các liên hệ và những tin nhắn đã được gửi của bạn được lưu trữ trên thiết bị bạn dùng.</string> + <string name="v4_3_irreversible_message_deletion_desc">Các liên hệ của bạn có thể cho phép xóa tin nhắn hoàn toàn.</string> + <string name="appearance_zoom">Thu phóng</string> + <string name="your_SMP_servers">Các máy chủ SMP của bạn</string> + <string name="description_you_shared_one_time_link">Bạn đã chia sẻ đường dẫn dùng một lần</string> + <string name="group_main_profile_sent">Hồ sơ trò chuyện của bạn sẽ được gửi tới các thành viên nhóm</string> + <string name="your_current_profile">Hồ sơ hiện tại của bạn</string> + <string name="alert_title_cant_invite_contacts_descr">Bạn đang dùng một hồ sơ ẩn danh cho nhóm này - để tránh khỏi chia sẻ hồ sơ chính của mình với các liên hệ đi mời mà không được cho phép</string> + <string name="your_preferences">Các tùy chọn của bạn</string> + <string name="incognito_random_profile">Hồ sơ ngẫu nhiên của bạn</string> + <string name="you_sent_group_invitation">Bạn đã gửi lời mời nhóm</string> + <string name="you_rejected_group_invitation">Bạn đã từ chối lời mời nhóm</string> + <string name="snd_group_event_member_unblocked">bạn đã bỏ chặn %s</string> + <string name="your_chat_profile_will_be_sent_to_your_contact">Hồ sơ trò chuyện của bạn sẽ được gửi\ntới liên hệ của bạn</string> + <string name="description_you_shared_one_time_link_incognito">bạn đã chia sẻ đường dẫn ẩn danh dùng một lần</string> + <string name="your_simplex_contact_address">Địa chỉ SimpleX của bạn</string> + <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Liên hệ của bạn cần phải trực tuyến để cho kết nối hoàn thành.\nBạn có thể hủy kết nối này và xóa liên hệ (và thử lại sau với một đường dẫn mới).</string> + <string name="your_ICE_servers">Các máy chủ ICE của bạn</string> + <string name="invite_prohibited_description">Bạn đang cố mời liên hệ mà bạn đã chia sẻ một hồ sơ ẩn danh với để tham gia nhóm mà bạn đang dùng hồ sơ chính của mình</string> + <string name="chat_main_profile_sent">Hồ sơ trò chuyện của bạn sẽ được gửi tới các thành viên có liên lạc</string> + <string name="you_will_be_connected_when_your_connection_request_is_accepted">Bạn sẽ được kết nối khi yêu cầu kết nối của bạn được chấp nhận, xin vui lòng đợi hoặc kiểm tra sau!</string> + <string name="connect__your_profile_will_be_shared">Hồ sơ %1$s sẽ được chia sẻ.</string> + <string name="your_XFTP_servers">Các máy chủ XFTP của bạn</string> + <string name="profile_will_be_sent_to_contact_sending_link">Hồ sơ của bạn sẽ được gửi tới liên hệ mà bạn đã nhận từ người đó đường dẫn này.</string> + <string name="you_will_join_group">Bạn sẽ kết nối với tất cả các thành viên nhóm.</string> + <string name="non_content_uri_alert_text">Bạn đã chia sẻ một đường dẫn tệp không hợp lệ. Báo cáo vấn đề tới các nhà phát triển ứng dụng.</string> + <string name="your_servers">Các máy chủ của bạn</string> + <string name="you_will_still_receive_calls_and_ntfs">Bạn vẫn sẽ nhận các cuộc gọi và thông báo từ các hồ sơ đã bị tắt thông báo khi chúng hoạt động.</string> + <string name="connect_via_member_address_alert_desc">Yêu cầu kết nối sẽ được gửi tới thành viên nhóm này.</string> + <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Bạn sẽ dừng nhận tin nhắn từ cuộc hội thoại này. Lịch sử trò chuyện sẽ được giữ lại.</string> + <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Bạn sẽ dừng nhận tin nhắn từ nhóm này. Lịch sử trò chuyện sẽ được giữ lại.</string> + <string name="contact_sent_large_file">Liên hệ của bạn đã gửi một tệp có kích thước lớn hơn so với kích thước tối đa hiện đang được hỗ trợ (%1$s).</string> + <string name="you_wont_lose_your_contacts_if_delete_address">Bạn sẽ không mất các liên hệ của mình nếu bạn sau đó xóa địa chỉ của mình đi.</string> + <string name="contact_list_header_title">Các liên hệ của bạn</string> + <string name="your_chat_profiles">Các hồ sơ trò chuyện của bạn</string> + <string name="your_settings">Cài đặt của bạn</string> + <string name="smp_servers_your_server">Máy chủ của bạn</string> + <string name="smp_servers_your_server_address">Địa chỉ máy chủ của bạn</string> + <string name="your_ice_servers">Các máy chủ ICE của bạn</string> + <string name="network_proxy_auth_mode_username_password">Thông tin định danh của bạn có thể bị gửi mà không được mã hóa.</string> + <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Hồ sơ của bạn được lưu trên thiết bị bạn dùng và chỉ được chia sẻ với các liên hệ bạn có. Các máy chủ SimpleX không thể xem hồ sơ của bạn.</string> + <string name="switching_profile_error_message">Kết nối của bạn đã bị chuyển tới %s nhưng một lỗi không mong muốn đã xảy ra trong khi chuyển hướng bạn đến hồ sơ.</string> + <string name="you_will_be_connected_when_group_host_device_is_online">Bạn sẽ được kết nối tới nhóm khi thiết bị của chủ nhóm trực tuyến, xin vui lòng đợi hoặc kiểm tra sau!</string> + <string name="you_will_be_connected_when_your_contacts_device_is_online">Bạn sẽ được kết nối khi thiết bị của liên hệ bạn trực tuyến, xin vui lòng đợi hoặc kiểm tra sau!</string> + <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Bạn sẽ được yêu cầu xác thực khi bạn khởi động hoặc tiếp tục ứng dụng sau 30 giây trong nền.</string> + <string name="network_smp_web_port_section_title">Cổng TCP để nhắn tin</string> + <string name="network_smp_web_port_footer">Sử dụng cổng TCP %1$s khi không có cổng nào được chỉ định.</string> + <string name="network_smp_web_port_toggle">Sử dụng cổng web</string> + <string name="max_group_mentions_per_message_reached">Bạn có thể nhắc đến tối đa là %1$s thành viên trong một tin nhắn!</string> + <string name="mute_all_chat">Tắt thông báo tất cả</string> + <string name="unread_mentions">Những lời nhắc chưa đọc</string> + <string name="group_members_can_send_reports">Các thành viên có thể báo cáo tin nhắn tới các kiểm duyệt viên.</string> + <string name="report_archive_alert_desc_all">Tất cả các báo cáo sẽ được lưu trữ cho bạn.</string> + <string name="report_archive_alert_title_all">Lưu trữ tất cả báo cáo?</string> + <string name="report_archive_alert_title_nth">Lưu trữ %d báo cáo?</string> + <string name="archive_reports">Lưu trữ các báo cáo</string> + <string name="report_archive_for_all_moderators">Cho tất cả các kiểm duyệt viên</string> + <string name="report_archive_for_me">Cho tôi</string> + <string name="notification_group_report">Báo cáo: %s</string> + <string name="enable_sending_member_reports">Cho phép báo cáo tin nhắn tới các kiểm duyệt viên.</string> + <string name="disable_sending_member_reports">Cấm báo cáo tin nhắn tới các kiểm duyệt viên.</string> + <string name="member_reports_are_prohibited">Báo cáo tin nhắn bị cấm trong nhóm này.</string> + <string name="v6_3_mentions_descr">Nhận thông báo khi được nhắc đến.</string> + <string name="v6_3_reports_descr">Giúp các quản trị viên quản lý các nhóm của họ.</string> + <string name="v6_3_mentions">Nhắc đến các thành viên 👋</string> + <string name="v6_3_organize_chat_lists">Sắp xếp các cuộc trò chuyện thành danh sách</string> + <string name="v6_3_faster_deletion_of_groups">Xóa nhóm nhanh hơn.</string> + <string name="v6_3_faster_sending_messages">Gửi tin nhắn nhanh hơn.</string> + <string name="v6_3_organize_chat_lists_descr">Không còn bỏ lỡ các tin nhắn quan trọng nữa.</string> + <string name="v6_3_better_privacy_and_security">Bảo mật và riêng tư hơn</string> + <string name="v6_3_better_groups_performance">Nhóm hoạt động hiệu quả hơn</string> + <string name="v6_3_reports">Gửi các báo cáo riêng tư</string> + <string name="group_preview_rejected">đã từ chối</string> + <string name="group_member_status_rejected">đã từ chối</string> + <string name="v6_3_set_message_expiration_in_chats">Thiết lập giờ hết hạn cho tin nhắn trong các cuộc trò chuyện.</string> + <string name="v6_3_private_media_file_names">Tên các tệp tin đa phương tiện riêng tư.</string> + <string name="error_reading_passphrase">Lỗi đọc mật khẩu cơ sở dữ liệu</string> + <string name="block_members_desc">Tất cả các tin nhắn mới từ những thành viên này sẽ bị ẩn!</string> + <string name="block_members_for_all_question">Chặn các thành viên cho tất cả?</string> + <string name="feature_roles_moderators">các kiểm duyệt viên</string> + <string name="unblock_members_desc">Các tin nhắn từ những thành viên này sẽ được hiển thị!</string> + <string name="members_will_be_removed_from_group_cannot_be_undone">Các thành viên sẽ bị xóa khỏi nhóm - việc này không thể được hoàn tác!</string> + <string name="members_will_be_removed_from_chat_cannot_be_undone">Các thành viên sẽ bị xóa khỏi cuộc trò chuyện - việc này không thể được hoàn tác!</string> + <string name="restore_passphrase_can_not_be_read_desc">Không thể đọc mật khẩu trong Keystore. Điều này có thể xảy ra sau bản cập nhật hệ thống không tương thích với ứng dụng. Nếu không phải như vậy, xin vui lòng liên hệ với các nhà phát triển.</string> + <string name="group_member_status_pending_approval_short">đang chờ xử lý</string> + <string name="operator_updated_conditions">Các điều kiện đã được cập nhật</string> + <string name="restore_passphrase_can_not_be_read_enter_manually_desc">Không thể đọc mật khẩu trong Keystore, xin vui lòng nhập thủ công. Điều này có thể xảy ra sau khi bản cập nhật hệ thống không tương thích với ứng dụng. Nếu không phải như vậy, vui lòng liên hệ với các nhà phát triển.</string> + <string name="group_member_status_pending_approval">đang chờ phê duyệt</string> + <string name="unblock_members_for_all_question">Bỏ chặn các thành viên cho tất cả?</string> + <string name="button_remove_members_question">Xóa các thành viên?</string> + <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Chính sách quyền riêng tư và các điều kiện sử dụng.</string> + <string name="onboarding_conditions_by_using_you_agree">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.</string> + <string name="onboarding_conditions_private_chats_not_accessible">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ủ.</string> + <string name="onboarding_conditions_accept">Chấp nhận</string> + <string name="onboarding_conditions_configure_server_operators">Định cấu hình các bên vận hành máy chủ</string> + <string name="link_requires_newer_app_version_please_upgrade">Đườ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.</string> + <string name="simplex_link_channel">Đường dẫn kênh SimpleX</string> + <string name="unsupported_connection_link">Đường dẫn kết nối không được hỗ trợ</string> + <string name="privacy_short_links">Sử dụng đường dẫn ngắn (BETA)</string> + <string name="full_link_button_text">Toàn bộ đường dẫn</string> + <string name="short_link_button_text">Đường dẫn ngắn</string> + <string name="network_smp_web_port_off">Tắt</string> + <string name="network_smp_web_port_preset">Các máy chủ cài sẵn</string> + <string name="network_smp_web_port_preset_footer">Chỉ sử dụng cổng TCP 443 cho các máy chủ cài sẵn.</string> + <string name="network_smp_web_port_all">Tất cả máy chủ</string> +</resources> 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 c19a3960f7..28a5f6f50d 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 @@ -3,7 +3,7 @@ <string name="a_plus_b">a + b</string> <string name="chat_item_ttl_day">1天</string> <string name="about_simplex">关于 SimpleX</string> - <string name="all_group_members_will_remain_connected">所有群组成员将保持连接。</string> + <string name="all_group_members_will_remain_connected">所有群成员将保持连接。</string> <string name="about_simplex_chat">关于 SimpleX Chat</string> <string name="above_then_preposition_continuation">以上,然后:</string> <string name="accept_contact_button">接受</string> @@ -15,14 +15,14 @@ <string name="callstatus_accepted">已接受通话</string> <string name="accept">接受</string> <string name="network_enable_socks_info">通过在 %d 端口的 SOCKS 代理访问服务器?启用该选项前必须先启动代理。</string> - <string name="smp_servers_add">添加服务器……</string> + <string name="smp_servers_add">添加服务器</string> <string name="smp_servers_add_to_another_device">添加另一设备</string> <string name="group_member_role_admin">管理员</string> <string name="v4_3_improved_server_configuration_desc">扫描二维码来添加服务器。</string> <string name="network_settings">高级网络设置</string> <string name="accept_connection_request__question">接受连接请求?</string> <string name="accept_contact_incognito_button">接受隐身聊天</string> - <string name="v4_2_group_links_desc">管理员可以创建链接以加入群组。</string> + <string name="v4_2_group_links_desc">管理员可以创建链接以加入群。</string> <string name="smp_servers_preset_add">添加预设服务器</string> <string name="connect_via_link">通过链接连接</string> <string name="display_name_connection_established">已建立连接</string> @@ -39,7 +39,7 @@ <string name="settings_section_title_messages">消息和文件</string> <string name="users_add">添加个人资料</string> <string name="users_delete_all_chats_deleted">所有聊天记录和消息将被删除——这一行为无法撤销!</string> - <string name="clear_chat_warning">所有聊天记录和消息将被删除——这一行为无法撤销!只有您的消息会被删除。</string> + <string name="clear_chat_warning">所有聊天记录和消息将被删除——这一行为无法撤销!只有你的消息会被删除。</string> <string name="allow_to_send_voice">允许发送语音消息。</string> <string name="allow_voice_messages_question">允许语音消息?</string> <string name="delete_verb">删除</string> @@ -47,14 +47,13 @@ <string name="delete_group_menu_action">删除</string> <string name="delete_address__question">删除地址?</string> <string name="delete_after">在此后删除</string> - <string name="delete_archive">删除档案</string> <string name="deleted_description">已删除</string> <string name="delete_files_and_media_question">删除文件和媒体文件?</string> <string name="full_deletion">为所有人删除</string> <string name="for_me_only">为我删除</string> <string name="delete_files_and_media_for_all_users">为所有聊天资料删除文件</string> - <string name="button_delete_group">删除群组</string> - <string name="delete_group_question">删除群组?</string> + <string name="button_delete_group">删除群</string> + <string name="delete_group_question">删除群?</string> <string name="delete_link">删除链接</string> <string name="delete_link_question">删除链接?</string> <string name="network_session_mode_entity">连接</string> @@ -64,35 +63,34 @@ <string name="info_row_connection">连接</string> <string name="connect_via_invitation_link">通过一次性链接进行连接?</string> <string name="connect_via_contact_link">通过联系人地址进行连接?</string> - <string name="connect_via_group_link">加群吗?</string> - <string name="connect_via_link_or_qr">通过群组链接/二维码连接</string> + <string name="connect_via_group_link">加入群?</string> + <string name="connect_via_link_or_qr">通过群链接/二维码连接</string> <string name="always_use_relay">总是通过中继连接</string> - <string name="allow_your_contacts_irreversibly_delete">允许您的联系人不可撤回地删除已发送消息。</string> + <string name="allow_your_contacts_irreversibly_delete">允许你的联系人不不可逆地删除已发送消息。(24小时)</string> <string name="chat_preferences_contact_allows">联系人允许</string> - <string name="allow_voice_messages_only_if">仅有您的联系人许可后才允许语音消息。</string> - <string name="group_info_member_you">您: %1$s</string> - <string name="allow_your_contacts_to_send_voice_messages">允许您的联系人发送语音消息。</string> + <string name="allow_voice_messages_only_if">允许语音消息,前提是你的联系人允许这样的消息。</string> + <string name="group_info_member_you">你: %1$s</string> + <string name="allow_your_contacts_to_send_voice_messages">允许你的联系人发送语音消息。</string> <string name="chat_preferences_always">始终</string> <string name="notifications_mode_service">始终开启</string> - <string name="allow_your_contacts_to_send_disappearing_messages">允许您的联系人发送限时消息。</string> + <string name="allow_your_contacts_to_send_disappearing_messages">允许你的联系人发送限时消息。</string> <string name="app_version_code">应用程序构建:%s</string> <string name="all_your_contacts_will_remain_connected">所有联系人会保持连接。</string> <string name="allow_verb">允许</string> <string name="allow_direct_messages">允许向成员发送私信。</string> <string name="allow_to_send_disappearing">允许发送限时消息。</string> <string name="delete_address">删除地址</string> - <string name="delete_chat_archive_question">删除聊天档案?</string> <string name="users_delete_question">删除聊天资料?</string> <string name="button_delete_contact">删除联系人</string> <string name="delete_contact_question">删除联系人?</string> - <string name="rcv_group_event_group_deleted">已删除群组</string> + <string name="rcv_group_event_group_deleted">已删除群</string> <string name="delete_image">删除图片</string> - <string name="allow_disappearing_messages_only_if">仅有您的联系人许可后才允许限时消息。</string> - <string name="allow_irreversible_message_deletion_only_if">仅有您的联系人许可后才允许不可撤回消息移除。</string> - <string name="allow_to_delete_messages">允许不可撤回地删除已发送消息。</string> + <string name="allow_disappearing_messages_only_if">允许限时消息,前提是你的联系人允许这样的消息。</string> + <string name="allow_irreversible_message_deletion_only_if">允许不可逆的消息删除,前提是你的联系人允许这样做。(24小时)</string> + <string name="allow_to_delete_messages">允许不可逆地删除已发送消息。(24小时)</string> <string name="users_delete_profile_for">为此删除聊天资料</string> <string name="delete_database">删除数据库</string> - <string name="keychain_allows_to_receive_ntfs">在您重启应用程序或者更换密码后安卓密钥库系统用来安全地保存密码——来确保收到通知。</string> + <string name="keychain_allows_to_receive_ntfs">在你重启应用程序或者更换密码后安卓密钥库系统用来安全地保存密码——来确保收到通知。</string> <string name="keychain_is_storing_securely">安卓密钥库系统用来安全地保存密码——来确保通知服务运作。</string> <string name="appearance_settings">外观</string> <string name="app_version_title">应用程序版本</string> @@ -109,27 +107,24 @@ <string name="integrity_msg_bad_hash">消息散列值错误</string> <string name="integrity_msg_bad_id">错误消息 ID</string> <string name="settings_audio_video_calls">语音和视频通话</string> - <string name="turning_off_service_and_periodic">启用电池优化,关闭了后台服务和对新消息的定期请求。您可以在设置里重新启用它们。</string> + <string name="turning_off_service_and_periodic">启用电池优化,关闭了后台服务和对新消息的定期请求。你可以在设置里重新启用它们。</string> <string name="notifications_mode_service_desc">后台服务始终运行——一旦有消息,就会显示通知。</string> <string name="icon_descr_audio_off">关闭音频</string> <string name="icon_descr_audio_on">开启音频</string> <string name="icon_descr_asked_to_receive">已要求接收图片</string> - <string name="network_session_mode_user_description"><![CDATA[一个单独的TCP连接(和SOCKS凭证)将被用于<b>,用于您在应用程序中的每个聊天资料</b> 。]]></string> - <string name="network_session_mode_entity_description">每个联系人和群组成员 <b>将使用单独的 TCP 连接(和 SOCKS 凭证)</b>。 -\n<b>请注意</b>:如果您有很多连接,您的电池和流量消耗可能会大大增加,并且某些连接可能会失败。</string> + <string name="network_session_mode_user_description"><![CDATA[一个单独的TCP连接(和SOCKS凭证)将被用于<b>,用于你在应用程序中的每个聊天资料</b> 。]]></string> + <string name="network_session_mode_entity_description"><![CDATA[不同的 TCP 连接 (和 SOCKS 凭据) 会被用于 <b>每个联系人和群成员</b>。\n<b>请注意</b>:如果你有很多连接,你的电池和流量消耗可能会大大增加,并且某些连接可能会失败。]]></string> <string name="back">返回</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>添加新联系人</b>:为您的联系人创建一次性二维码。]]></string> - <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b> 最长续航 </b>。您只会在应用程序运行时收到通知(无后台服务)。]]></string> - <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b> 较长续航 </b>。后台服务每 10 分钟检查一次消息。您可能会错过来电或者紧急信息。]]></string> + <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b> 最长续航 </b>。你只会在应用程序运行时收到通知(无后台服务)。]]></string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b> 较长续航 </b>。应用每 10 分钟检查一次消息。你可能会错过来电或者紧急信息。]]></string> <string name="bold_text">加粗</string> - <string name="both_you_and_your_contacts_can_delete">您和您的联系人都可以永久删除已发送的消息。</string> - <string name="both_you_and_your_contact_can_send_disappearing">您和您的联系人都可以发送限时消息。</string> - <string name="both_you_and_your_contact_can_send_voice">您和您的联系人都可以发送语音消息。</string> + <string name="both_you_and_your_contacts_can_delete">你和你的联系人都可以不可逆地删除已发送的消息。(24小时)</string> + <string name="both_you_and_your_contact_can_send_disappearing">你和你的联系人都可以发送限时消息。</string> + <string name="both_you_and_your_contact_can_send_voice">你和你的联系人都可以发送语音消息。</string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b> 可以在设置里禁用它 </b> - 应用程序运行时仍会显示通知。]]></string> - <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b> 使用更多电量 </b>!后台服务始终运行——一旦收到消息,就会显示通知。]]></string> - <string name="impossible_to_recover_passphrase"><![CDATA[<b>请注意</b>:如果您丢失密码,您将无法恢复或者更改密码。]]></string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b> 使用更多电量 </b>!应用始终在后台运行——一即刻显示通知。]]></string> + <string name="impossible_to_recover_passphrase"><![CDATA[<b>请注意</b>:如果你丢失密码,你将无法恢复或者更改密码。]]></string> <string name="call_already_ended">通话已结束!</string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>扫描二维码</b> :与向您展示二维码的联系人联系。]]></string> <string name="alert_title_cant_invite_contacts">无法邀请联系人!</string> <string name="invite_prohibited">无法邀请联系人!</string> <string name="cancel_verb">取消</string> @@ -138,7 +133,7 @@ <string name="icon_descr_call_ended">通话结束</string> <string name="change_database_passphrase_question">更改数据库密码?</string> <string name="callstatus_error">通话错误</string> - <string name="rcv_conn_event_switch_queue_phase_completed">为您更改地址</string> + <string name="rcv_conn_event_switch_queue_phase_completed">为你更改地址</string> <string name="callstatus_in_progress">通话中</string> <string name="icon_descr_call_progress">通话进行中</string> <string name="callstatus_calling">呼叫中……</string> @@ -152,66 +147,64 @@ <string name="cannot_receive_file">无法接收文件</string> <string name="database_initialization_error_title">无法初始化数据库</string> <string name="rcv_group_event_changed_member_role">将 %s 的角色更改为 %s</string> - <string name="rcv_group_event_changed_your_role">将您的角色更改为 %s</string> + <string name="rcv_group_event_changed_your_role">将你的角色更改为 %s</string> <string name="change_role">改变角色</string> - <string name="change_member_role_question">更改群组角色?</string> + <string name="change_member_role_question">更改群角色?</string> <string name="icon_descr_cancel_link_preview">取消链接预览</string> <string name="snd_conn_event_switch_queue_phase_changing_for_member">正在为 %s 更改地址……</string> <string name="rcv_conn_event_switch_queue_phase_changing">更改地址中……</string> <string name="snd_conn_event_switch_queue_phase_changing">更改地址中……</string> - <string name="create_your_profile">创建您的资料</string> + <string name="create_your_profile">创建你的资料</string> <string name="chat_database_deleted">聊天数据库已删除</string> <string name="chat_database_imported">聊天数据库已导入</string> <string name="keychain_error">钥匙串错误</string> - <string name="chat_archive_section">聊天档案</string> - <string name="chat_archive_header">聊天档案</string> <string name="chat_console">聊天控制台</string> <string name="chat_database_section">聊天数据库</string> <string name="chat_is_stopped_indication">聊天已停止</string> <string name="chat_is_running">聊天运行中</string> <string name="chat_is_stopped">聊天已停止</string> <string name="contact_preferences">联系人偏好设置</string> - <string name="your_preferences">您的偏好设置</string> - <string name="group_preferences">群组偏好设置</string> - <string name="only_group_owners_can_change_prefs">只有群主可以改变群组偏好设置。</string> + <string name="your_preferences">你的偏好设置</string> + <string name="group_preferences">群偏好设置</string> + <string name="only_group_owners_can_change_prefs">只有群主可以改变群偏好设置。</string> <string name="save_preferences_question">保存偏好设置?</string> - <string name="set_group_preferences">设置群组偏好设置</string> + <string name="set_group_preferences">设置群偏好设置</string> <string name="privacy_redefined">重新定义隐私</string> <string name="v4_3_improved_privacy_and_security">改进的隐私和安全</string> <string name="incognito">隐身聊天</string> - <string name="joining_group">加入群组中</string> + <string name="joining_group">加入群中</string> <string name="join_group_incognito_button">加入隐身聊天</string> <string name="settings_section_title_incognito">隐身模式</string> <string name="tap_to_start_new_chat">点击开始一个新聊天</string> - <string name="incognito_random_profile">您的随机资料</string> + <string name="incognito_random_profile">你的随机资料</string> <string name="description_via_contact_address_link_incognito">通过联系地址链接隐身</string> - <string name="description_via_group_link_incognito">通过群组链接隐身</string> - <string name="description_you_shared_one_time_link_incognito">您分享了一次性链接隐身聊天</string> + <string name="description_via_group_link_incognito">通过群链接隐身</string> + <string name="description_you_shared_one_time_link_incognito">你分享了一次性链接隐身聊天</string> <string name="group_invitation_tap_to_join_incognito">点击以加入隐身聊天</string> - <string name="group_main_profile_sent">您的聊天资料将被发送给群组成员</string> - <string name="invite_prohibited_description">您正在尝试邀请与您共享隐身个人资料的联系人加入您使用主要个人资料的群组</string> - <string name="incognito_info_protects">隐身模式通过为每个联系人使用新的随机配置文件来保护您的隐私。</string> - <string name="alert_title_cant_invite_contacts_descr">您正在为该群组使用隐身个人资料——为防止共享您的主要个人资料,不允许邀请联系人</string> + <string name="group_main_profile_sent">你的聊天资料将被发送给群成员</string> + <string name="invite_prohibited_description">你正在尝试邀请与你共享隐身个人资料的联系人加入你使用主要个人资料的群</string> + <string name="incognito_info_protects">隐身模式通过为每个联系人使用新的随机配置文件来保护你的隐私。</string> + <string name="alert_title_cant_invite_contacts_descr">你正在为该群使用隐身个人资料——为防止共享你的主要个人资料,不允许邀请联系人</string> <string name="description_via_one_time_link_incognito">通过一次性链接隐身</string> <string name="only_group_owners_can_enable_voice">只有群主可以启用语音信息。</string> - <string name="your_privacy">您的隐私设置</string> + <string name="your_privacy">你的隐私设置</string> <string name="privacy_and_security">隐私和安全</string> <string name="smp_servers_save">保存服务器</string> <string name="incognito_info_allows">它允许在一个聊天资料中有多个匿名连接,而它们之间没有任何共享数据。</string> - <string name="incognito_info_share">当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。</string> + <string name="incognito_info_share">当你与某人共享隐身聊天资料时,该资料将用于他们邀请你加入的群。</string> <string name="v4_3_improved_server_configuration">改进的服务器配置</string> <string name="icon_descr_email">电邮</string> <string name="edit_image">编辑图片</string> - <string name="button_edit_group_profile">编辑群组资料</string> + <string name="button_edit_group_profile">编辑群资料</string> <string name="error_encrypting_database">加密数据库错误</string> <string name="error_exporting_chat_database">导出聊天数据库错误</string> <string name="error_importing_database">导入聊天数据库错误</string> - <string name="error_joining_group">加入群组错误</string> + <string name="error_joining_group">加入群错误</string> <string name="error_deleting_user">删除用户资料错误</string> <string name="passphrase_is_different">数据库密码不同于保存在密钥库中的密码。</string> <string name="database_encryption_will_be_updated">数据库加密密码将被更新并存储在密钥库中。</string> <string name="database_will_be_encrypted_and_passphrase_stored">数据库将被加密,密码存储在密钥库中。</string> - <string name="restore_passphrase_not_found_desc">在密匙库中没有找到密码,请手动输入。如果您使用备份工具恢复了应用程序的数据,可能会发生这种情况。如果不是这种情况,请联系开发者。</string> + <string name="restore_passphrase_not_found_desc">在密匙库中没有找到密码,请手动输入。如果你使用备份工具恢复了应用程序的数据,可能会发生这种情况。如果不是这种情况,请联系开发者。</string> <string name="remove_passphrase_from_keychain">从密钥库中删除密码?</string> <string name="save_passphrase_in_keychain">在密钥库中保存密码</string> <string name="simplex_service_notification_title">SimpleX Chat 服务</string> @@ -236,10 +229,10 @@ <string name="v4_5_transport_isolation_descr">按聊天资料(默认)或按连接(BETA)。</string> <string name="smp_servers_check_address">检查服务器地址并重试。</string> <string name="clear_verification">清除验证</string> - <string name="icon_descr_close_button">关闭按键</string> + <string name="icon_descr_close_button">关闭</string> <string name="configure_ICE_servers">配置 ICE 服务器</string> <string name="confirm_verb">确认</string> - <string name="auth_confirm_credential">确认您的证书</string> + <string name="auth_confirm_credential">确认你的证书</string> <string name="server_connected">已连接</string> <string name="icon_descr_server_status_connected">已连接</string> <string name="smp_server_test_connect">连接</string> @@ -248,14 +241,14 @@ <string name="group_member_status_connecting">连接中</string> <string name="notifications_mode_periodic_desc">每10分钟检查一次新消息,最长检查1分钟</string> <string name="rcv_group_event_member_connected">已连接</string> - <string name="v4_4_verify_connection_security_desc">与您的联系人比较安全码。</string> + <string name="v4_4_verify_connection_security_desc">与你的联系人比较安全码。</string> <string name="incoming_audio_call">语音通话来电</string> <string name="error_changing_message_deletion">更改设置错误</string> - <string name="alert_message_group_invitation_expired">群组邀请不再有效,已被发件人删除。</string> + <string name="alert_message_group_invitation_expired">群邀请不再有效,已被发件人删除。</string> <string name="failed_to_create_user_duplicate_title">重复的显示名!</string> <string name="failed_to_create_user_title">创建资料错误!</string> <string name="error_accepting_contact_request">接受联系人请求错误</string> - <string name="error_deleting_group">删除群组错误</string> + <string name="error_deleting_group">删除群错误</string> <string name="error_deleting_pending_contact_connection">删除待定的联系人连接错误</string> <string name="error_receiving_file">接收文件错误</string> <string name="failed_to_active_user_title">切换资料错误!</string> @@ -266,7 +259,7 @@ <string name="for_everybody">对于每个人</string> <string name="image_decoding_exception_title">解码错误</string> <string name="image_saved">图片保存到相册</string> - <string name="image_will_be_received_when_contact_is_online">图片将在您的联系人在线时收到,请稍等或稍后查看!</string> + <string name="image_will_be_received_when_contact_is_online">图片将在你的联系人在线时收到,请稍等或稍后查看!</string> <string name="error_saving_file">保存文件错误</string> <string name="icon_descr_file">文件</string> <string name="file_not_found">未找到文件</string> @@ -303,17 +296,17 @@ <string name="encrypt_database">加密</string> <string name="encrypted_database">加密数据库</string> <string name="enter_correct_passphrase">输入正确密码。</string> - <string name="icon_descr_group_inactive">不活跃群组</string> + <string name="icon_descr_group_inactive">不活跃群</string> <string name="group_member_status_creator">创建者</string> <string name="group_member_status_accepted">连接中(已接受)</string> <string name="group_member_status_announced">连接中(已宣布)</string> <string name="icon_descr_expand_role">扩展角色选择</string> - <string name="group_link">群组链接</string> - <string name="error_deleting_link_for_group">删除群组链接错误</string> + <string name="group_link">群链接</string> + <string name="error_deleting_link_for_group">删除群链接错误</string> <string name="info_row_database_id">数据库 ID</string> <string name="error_removing_member">删除成员错误</string> <string name="error_changing_role">更改角色错误</string> - <string name="info_row_group">群组</string> + <string name="info_row_group">群</string> <string name="timed_messages">限时消息</string> <string name="ttl_day">%d 天</string> <string name="group_connection_pending">连接中……</string> @@ -322,7 +315,7 @@ <string name="notification_preview_mode_contact">联系人姓名</string> <string name="group_member_status_intro_invitation">连接中(介绍邀请)</string> <string name="callstate_connecting">连接中……</string> - <string name="contacts_can_mark_messages_for_deletion">联系人可以将信息标记为删除;您将可以查看这些信息。</string> + <string name="contacts_can_mark_messages_for_deletion">联系人可以将信息标记为删除;你将可以查看这些信息。</string> <string name="contribute">贡献</string> <string name="icon_descr_contact_checked">已检查联系人</string> <string name="notification_preview_somebody">联系人已隐藏:</string> @@ -330,15 +323,14 @@ <string name="icon_descr_context">上下文图标</string> <string name="copied">已复制到剪贴板</string> <string name="contact_connection_pending">连接中……</string> - <string name="create_group_link">创建群组链接</string> - <string name="create_group">创建私密群组</string> + <string name="create_group_link">创建群链接</string> + <string name="create_group">创建私密群</string> <string name="button_create_group_link">创建链接</string> <string name="share_one_time_link">创建一次性邀请链接</string> <string name="smp_server_test_create_queue">创建队列</string> - <string name="create_secret_group_title">创建私密群组</string> + <string name="create_secret_group_title">创建私密群</string> <string name="v4_5_multiple_chat_profiles_descr">不同的名字、头像和传输隔离。</string> <string name="v4_4_french_interface">法语界面</string> - <string name="create_one_time_link">创建一次性邀请链接</string> <string name="how_to_use_simplex_chat">如何使用它</string> <string name="server_error">错误</string> <string name="display_name_connecting">连接中……</string> @@ -360,17 +352,17 @@ <string name="full_name__field">全名:</string> <string name="display_name">输入你的名字:</string> <string name="callstate_ended">已结束</string> - <string name="group_member_status_group_deleted">群组已删除</string> - <string name="delete_group_for_all_members_cannot_undo_warning">将为所有成员删除群组——此操作无法撤消!</string> + <string name="group_member_status_group_deleted">群已删除</string> + <string name="delete_group_for_all_members_cannot_undo_warning">将为所有成员删除群——此操作无法撤消!</string> <string name="conn_level_desc_direct">直接</string> <string name="direct_messages">私信</string> <string name="feature_enabled">已启用</string> - <string name="group_members_can_send_voice">群组成员可以发送语音消息。</string> - <string name="v4_2_group_links">群组链接</string> + <string name="group_members_can_send_voice">成员可以发送语音消息。</string> + <string name="v4_2_group_links">群链接</string> <string name="error_starting_chat">启动聊天错误</string> <string name="database_encrypted">数据库已加密!</string> <string name="encrypt_database_question">加密数据库?</string> - <string name="encrypted_with_random_passphrase">数据库使用随机密码进行加密,您可以更改它。</string> + <string name="encrypted_with_random_passphrase">数据库使用随机密码进行加密,你可以更改它。</string> <string name="database_passphrase_is_required">打开聊天需要数据库密码。</string> <string name="error_stopping_chat">停止聊天错误</string> <string name="import_database_question">导入聊天数据库?</string> @@ -379,16 +371,16 @@ <string name="enter_passphrase">输入密码……</string> <string name="database_passphrase_will_be_updated">数据库加密密码将被更新。</string> <string name="database_error">数据库错误</string> - <string name="snd_group_event_group_profile_updated">群组资料已更新</string> - <string name="group_full_name_field">群组全名:</string> + <string name="snd_group_event_group_profile_updated">群资料已更新</string> + <string name="group_full_name_field">群全名:</string> <string name="theme_dark">深色</string> <string name="feature_enabled_for_contact">已为联系人启用</string> - <string name="feature_enabled_for_you">为您启用</string> - <string name="group_members_can_send_disappearing">群组成员可以发送限时消息。</string> + <string name="feature_enabled_for_you">为你启用</string> + <string name="group_members_can_send_disappearing">成员可以发送限时消息。</string> <string name="create_profile">创建个人资料</string> <string name="how_it_works">工作原理</string> - <string name="alert_title_no_group">未找到群组!</string> - <string name="error_creating_link_for_group">创建群组链接错误</string> + <string name="alert_title_no_group">未找到群!</string> + <string name="error_creating_link_for_group">创建群链接错误</string> <string name="group_member_status_introduced">连接中(已介绍)</string> <string name="set_password_to_export_desc">数据库使用随机密码进行加密。请在导出前更改它。</string> <string name="database_passphrase_and_export">数据库密码和导出</string> @@ -405,25 +397,25 @@ <string name="error_deleting_contact">删除联系人错误</string> <string name="error_setting_network_config">更新网络配置错误</string> <string name="error_deleting_contact_request">删除联系人请求错误</string> - <string name="error_saving_group_profile">保存群组资料错误</string> + <string name="error_saving_group_profile">保存群资料错误</string> <string name="error_saving_smp_servers">保存 SMP 服务器错误</string> <string name="error_saving_ICE_servers">保存 ICE 服务器错误</string> <string name="error_sending_message">发送消息错误</string> <string name="simplex_link_mode_full">完整链接</string> - <string name="group_display_name_field">输入群组名:</string> - <string name="group_invitation_expired">群组邀请已过期</string> - <string name="delete_group_for_self_cannot_undo_warning">将为您删除群组——此操作无法撤消!</string> - <string name="group_profile_is_stored_on_members_devices">群组资料存储在成员的设备上,而不是服务器上。</string> - <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">如果您选择拒绝发件人,将不会收到通知。</string> - <string name="how_to_use_your_servers">如何使用您的服务器</string> - <string name="network_disable_socks_info">如果您确认,消息服务器将能够看到您的 IP 地址和您的提供商——以及您正在连接的服务器。</string> + <string name="group_display_name_field">输入群名:</string> + <string name="group_invitation_expired">群邀请已过期</string> + <string name="delete_group_for_self_cannot_undo_warning">将为你删除群——此操作无法撤消!</string> + <string name="group_profile_is_stored_on_members_devices">群资料存储在成员的设备上,而不是服务器上。</string> + <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">如果你选择拒绝发件人,将不会收到通知。</string> + <string name="how_to_use_your_servers">如何使用你的服务器</string> + <string name="network_disable_socks_info">如果你确认,消息服务器将能够看到你的 IP 地址和你的提供商——以及你正在连接的服务器。</string> <string name="image_descr">图片</string> <string name="icon_descr_image_snd_complete">图片已发送</string> <string name="auth_device_authentication_is_disabled_turning_off">设备验证被禁用。关闭 SimpleX 锁定。</string> - <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">没有启用设备验证。一旦启用设备验证,您可以通过设置打开 SimpleX 锁定。</string> + <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">没有启用设备验证。一旦启用设备验证,你可以通过设置打开 SimpleX 锁定。</string> <string name="auth_disable_simplex_lock">禁用 SimpleX 锁定</string> <string name="maximum_supported_file_size">目前支持的最大文件尺寸是 %1$s。</string> - <string name="file_will_be_received_when_contact_is_online">文件将在您的联系人在线时收到,请稍等或稍后再查看!</string> + <string name="file_will_be_received_when_contact_is_online">文件将在你的联系人在线时收到,请稍等或稍后再查看!</string> <string name="from_gallery_button">从图库</string> <string name="gallery_image_button">照片</string> <string name="gallery_video_button">视频</string> @@ -431,10 +423,10 @@ <string name="icon_descr_flip_camera">翻转相机</string> <string name="enable_automatic_deletion_question">启用自动删除消息?</string> <string name="section_title_for_console">用于控制台</string> - <string name="direct_messages_are_prohibited_in_chat">此群中禁止成员之间私信。</string> - <string name="disappearing_messages_are_prohibited">该组禁止限时消息。</string> - <string name="group_members_can_delete">群组成员可以不可撤回地删除已发送的消息。</string> - <string name="group_members_can_send_dms">群组成员可以私信。</string> + <string name="direct_messages_are_prohibited_in_group">此群中禁止成员之间私信。</string> + <string name="disappearing_messages_are_prohibited">限时消息被禁止。</string> + <string name="group_members_can_delete">成员可以不可逆地删除已发送的消息。(24小时)</string> + <string name="group_members_can_send_dms">成员可以发送私信。</string> <string name="v4_4_disappearing_messages">限时消息</string> <string name="v4_3_improved_privacy_and_security_desc">在最近的应用程序中隐藏应用程序屏幕。</string> <string name="leave_group_button">离开</string> @@ -442,13 +434,10 @@ <string name="live_message">实时消息!</string> <string name="image_descr_link_preview">链接预览图片</string> <string name="invalid_QR_code">无效的二维码</string> - <string name="onboarding_notifications_mode_subtitle">以后可以通过设置进行更改。</string> - <string name="alert_text_skipped_messages_it_can_happen_when">它可能在以下情况发生: -\n1. 消息在发送客户端 2 天后或在服务器上 30 天后过期。 -\n2. 消息解密失败,因为您或您的联系人使用了旧的数据库备份。 -\n3.连接被破坏。</string> - <string name="leave_group_question">离开群组?</string> - <string name="rcv_group_event_invited_via_your_group_link">通过您的群组链接邀请</string> + <string name="onboarding_notifications_mode_subtitle">它如何影响电量</string> + <string name="alert_text_skipped_messages_it_can_happen_when">它可能在以下情况发生: \n1. 消息在发送客户端 2 天后或在服务器上 30 天后过期。 \n2. 消息解密失败,因为你或你的联系人使用了旧的数据库备份。 \n3.连接被破坏。</string> + <string name="leave_group_question">离开群?</string> + <string name="rcv_group_event_invited_via_your_group_link">通过你的群链接邀请</string> <string name="info_row_local_name">本地名称</string> <string name="invalid_message_format">无效的消息格式</string> <string name="invalid_data">无效数据</string> @@ -457,38 +446,37 @@ <string name="invalid_connection_link">无效的连接链接</string> <string name="italic_text">斜体</string> <string name="group_member_status_invited">已邀请</string> - <string name="invite_to_group_button">邀请加入群组</string> + <string name="invite_to_group_button">邀请加入群</string> <string name="join_group_button">加入</string> - <string name="join_group_question">加入群组?</string> + <string name="join_group_question">加入群?</string> <string name="icon_descr_add_members">邀请成员</string> <string name="group_member_status_left">已离开</string> <string name="theme_light">浅色</string> - <string name="message_deletion_prohibited">此聊天中禁止不可撤回消息移除。</string> + <string name="message_deletion_prohibited">此聊天中禁止不可逆消息移除。</string> <string name="v4_5_italian_interface">意大利语界面</string> <string name="incorrect_code">安全码不正确!</string> <string name="service_notifications_disabled">即时通知被禁用!</string> <string name="invalid_chat">无效聊天</string> <string name="smp_servers_invalid_address">无效的服务器地址!</string> <string name="button_add_members">邀请成员</string> - <string name="button_leave_group">离开群组</string> + <string name="button_leave_group">离开群</string> <string name="users_delete_data_only">仅本地配置文件数据</string> <string name="icon_descr_instant_notifications">即时通知</string> <string name="service_notifications">即时通知!</string> - <string name="auth_log_in_using_credential">使用您的凭据登录</string> + <string name="auth_log_in_using_credential">使用你的凭据登录</string> <string name="large_file">大文件!</string> <string name="invalid_contact_link">链接无效!</string> <string name="rcv_group_event_member_left">已离开</string> - <string name="message_deletion_prohibited_in_chat">此群组中禁止不可撤回消息移除。</string> - <string name="v4_3_irreversible_message_deletion">不可撤回消息移除</string> + <string name="message_deletion_prohibited_in_chat">不可逆消息删除被禁止。</string> + <string name="v4_3_irreversible_message_deletion">不可逆消息移除</string> <string name="v4_4_live_messages">实时消息</string> <string name="notification_preview_mode_message">消息正文</string> <string name="callstate_waiting_for_confirmation">等待确认中……</string> <string name="onboarding_notifications_mode_service">即时</string> - <string name="only_your_contact_can_send_disappearing">只有您的联系人才可以发送限时消息。</string> + <string name="only_your_contact_can_send_disappearing">只有你的联系人才可以发送限时消息。</string> <string name="notification_preview_mode_message_desc">显示联系人和消息</string> <string name="notification_preview_mode_contact_desc">只显示联系人</string> - <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">为保护您的信息,请打开 SimpleX 锁定。 -\n在启用此功能之前,系统将提示您完成身份验证。</string> + <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">为保护你的信息,请打开 SimpleX 锁定。 \n在启用此功能之前,系统将提示你完成身份验证。</string> <string name="your_chats">聊天</string> <string name="share_file">分享文件……</string> <string name="share_image">分享媒体……</string> @@ -496,16 +484,16 @@ <string name="text_field_set_contact_placeholder">设置联系人姓名……</string> <string name="callstate_received_answer">已收到回复……</string> <string name="callstate_received_confirmation">已受到确认……</string> - <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[只有客户端设备存储用户配置文件、联系人、群组和使用 <b>双层端到端加密 </b> 发送的消息。]]></string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">仅客户端设备存储用户个人资料、联系人、群和消息。</string> <string name="video_call_no_encryption">视频通话(非端到端加密)</string> <string name="onboarding_notifications_mode_periodic">定期</string> <string name="onboarding_notifications_mode_title">私密通知</string> <string name="onboarding_notifications_mode_off">应用程序运行时</string> <string name="status_no_e2e_encryption">无端到端加密</string> <string name="show_call_on_lock_screen">显示</string> - <string name="you_must_use_the_most_recent_version_of_database">您只能在一台设备上使用最新版本的聊天数据库,否则您可能会停止接收来自某些联系人的消息。</string> + <string name="you_must_use_the_most_recent_version_of_database">你只能在一台设备上使用最新版本的聊天数据库,否则你可能会停止接收来自某些联系人的消息。</string> <string name="new_passphrase">新密码……</string> - <string name="member_role_will_be_changed_with_notification">该角色将更改为“%s”。群组中每个人都会收到通知。</string> + <string name="member_role_will_be_changed_with_notification">该角色将更改为 %s。群中每个人都会收到通知。</string> <string name="chat_lock">SimpleX 锁定</string> <string name="periodic_notifications">定期通知</string> <string name="notifications_mode_periodic">定期启动</string> @@ -515,12 +503,12 @@ <string name="network_use_onion_hosts_no">否</string> <string name="callstate_starting">启动中……</string> <string name="prohibit_sending_disappearing_messages">禁止发送限时消息。</string> - <string name="only_you_can_send_disappearing">只有您可以发送限时消息。</string> - <string name="only_your_contact_can_delete">只有您的联系人才能不可撤回地删除消息(您可以将它们标记为删除)。</string> + <string name="only_you_can_send_disappearing">只有你可以发送限时消息。</string> + <string name="only_your_contact_can_delete">只有你的联系人能不可逆地删除消息(你可以将它们标记为删除)。(24小时)</string> <string name="prohibit_sending_disappearing">禁止发送限时消息。</string> <string name="notifications_will_be_hidden">通知只会在应用程序停止之前发送!</string> <string name="chat_preferences_no">否</string> - <string name="prohibit_message_deletion">禁止不可撤回消息删除。</string> + <string name="prohibit_message_deletion">禁止不可逆消息删除。</string> <string name="notification_new_contact_request">新联系人请求</string> <string name="callstate_waiting_for_answer">等待答复中……</string> <string name="la_notice_title_simplex_lock">SimpleX 锁定</string> @@ -528,18 +516,18 @@ <string name="notifications">通知</string> <string name="simplex_service_notification_text">正在接收消息……</string> <string name="enter_passphrase_notification_desc">要接收通知,请输入数据库密码</string> - <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[为了保护您的隐私,该应用程序没有推送通知,而是具有 <b>SimpleX 后台服务 </b>——它每天使用百分之几的电池。]]></string> - <string name="your_settings">您的设置</string> - <string name="turn_off_battery_optimization"><![CDATA[要使用它,请在下一个对话框中<b>允许 SimpleX 在后台运行</b>。 否则,通知将被禁用。]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[To improve privacy, <b>SimpleX 在后台运行</b>而不是使用推送通知。]]></string> + <string name="your_settings">你的设置</string> + <string name="turn_off_battery_optimization"><![CDATA[在下个对话框中<b>允许它</b> 来立即接收通知。]]></string> <string name="settings_notification_preview_title">通知预览</string> <string name="enter_passphrase_notification_title">需要密码</string> <string name="periodic_notifications_disabled">定期通知被禁用!</string> <string name="notifications_mode_off">在应用程序打开时运行</string> <string name="settings_notification_preview_mode_title">显示预览</string> - <string name="periodic_notifications_desc">该应用程序会定期获取新消息——它每天会消耗百分之几的电量。该应用程序不使用推送通知——您设备中的数据不会发送到服务器。</string> - <string name="v4_3_irreversible_message_deletion_desc">您的联系人可以允许完全删除消息。</string> + <string name="periodic_notifications_desc">该应用程序会定期获取新消息——它每天会消耗百分之几的电量。该应用程序不使用推送通知——你设备中的数据不会发送到服务器。</string> + <string name="v4_3_irreversible_message_deletion_desc">你的联系人可以允许完全删除消息。</string> <string name="v4_4_disappearing_messages_desc">已发送的消息将在设定的时间后被删除。</string> - <string name="your_chat_database">您的聊天数据库</string> + <string name="your_chat_database">你的聊天数据库</string> <string name="wrong_passphrase_title">密码错误!</string> <string name="save_verb">保存</string> <string name="open_verb">打开</string> @@ -548,50 +536,48 @@ <string name="open_chat">打开聊天</string> <string name="database_backup_can_be_restored">更改数据库密码的尝试未完成。</string> <string name="remove_member_confirmation">移除</string> - <string name="sender_you_pronoun">您</string> + <string name="sender_you_pronoun">你</string> <string name="save_servers_button">保存</string> - <string name="settings_section_title_you">您</string> + <string name="settings_section_title_you">你</string> <string name="remove_passphrase">移除</string> - <string name="you_have_to_enter_passphrase_every_time">您必须在每次应用程序启动时输入密码——它不存储在设备上。</string> + <string name="you_have_to_enter_passphrase_every_time">你必须在每次应用程序启动时输入密码——它不存储在设备上。</string> <string name="set_password_to_export">设置密码来导出</string> <string name="enter_correct_current_passphrase">请输入正确的当前密码。</string> <string name="update_database_passphrase">更新数据库密码</string> - <string name="database_is_not_encrypted">您的聊天数据库未加密——设置密码来保护它。</string> - <string name="store_passphrase_securely_without_recover">请安全地保存密码,如果您丢失了密码,您将无法访问聊天。</string> - <string name="store_passphrase_securely">请安全地保存密码,如果您丢失了密码,您将无法更改它。</string> + <string name="database_is_not_encrypted">你的聊天数据库未加密——设置密码来保护它。</string> + <string name="store_passphrase_securely_without_recover">请安全地保存密码,如果你丢失了密码,你将无法访问聊天。</string> + <string name="store_passphrase_securely">请安全地保存密码,如果你丢失了密码,你将无法更改它。</string> <string name="wrong_passphrase">数据库密码错误</string> <string name="network_options_save">保存</string> <string name="update_network_settings_confirmation">更新</string> <string name="open_simplex_chat_to_accept_call">打开 SimpleX Chat 来接听电话</string> <string name="icon_descr_video_call">视频通话</string> - <string name="contact_wants_to_connect_via_call">%1$s 想通过以下方式与您联系</string> + <string name="contact_wants_to_connect_via_call">%1$s 想通过以下账户与你连接</string> <string name="icon_descr_call_rejected">拒接来电</string> <string name="call_connection_peer_to_peer">点对点</string> <string name="error_with_info">错误:%s</string> - <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[如果您不能亲自见面,您可以 <b>扫描视频通话中的二维码</b>,或者您的联系人可以分享邀请链接。]]></string> - <string name="your_calls">您的通话</string> + <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[如果你不能亲自见面,你可以 <b>扫描视频通话中的二维码</b>,或者你的联系人可以分享邀请链接。]]></string> + <string name="your_calls">你的通话</string> <string name="call_connection_via_relay">通过中继</string> <string name="callstatus_missed">未接来电</string> <string name="callstatus_rejected">拒接来电</string> <string name="v4_3_voice_messages">语音消息</string> <string name="voice_messages">语音消息</string> <string name="ntf_channel_calls">SimpleX Chat 通话</string> - <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[如果您不能亲自见面,<b>在视频通话</b>中出示二维码,或分享链接。]]></string> - <string name="your_chat_profiles">您的聊天资料</string> + <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[如果你不能亲自见面,<b>在视频通话</b>中出示二维码,或分享链接。]]></string> + <string name="your_chat_profiles">你的聊天资料</string> <string name="icon_descr_call_missed">未接来电</string> <string name="icon_descr_call_pending_sent">待定来电</string> - <string name="connection_error_auth_desc">除非您的联系人已删除此连接或此链接已被使用,否则它可能是一个错误——请报告。 -\n如果要连接,请让您的联系人创建另一个连接链接,并检查您的网络连接是否稳定。</string> - <string name="you_are_already_connected_to_vName_via_this_link">您已经连接到 %1$s。</string> - <string name="your_chat_profile_will_be_sent_to_your_contact">您的聊天资料将被发送 -\n给您的联系人</string> + <string name="connection_error_auth_desc">除非你的联系人已删除此连接或此链接已被使用,否则它可能是一个错误——请报告。 \n如果要连接,请让你的联系人创建另一个连接链接,并检查你的网络连接是否稳定。</string> + <string name="you_are_already_connected_to_vName_via_this_link">你已经连接到 %1$s。</string> + <string name="your_chat_profile_will_be_sent_to_your_contact">你的聊天资料将被发送 \n给你的联系人</string> <string name="users_delete_with_connections">资料和服务器连接</string> <string name="update_network_settings_question">更新网络设置?</string> - <string name="only_you_can_delete_messages">只有您可以不可撤回地删除消息(您的联系人可以将它们标记为删除)。</string> + <string name="only_you_can_delete_messages">只有你可以不可逆地删除消息(你的联系人可以将它们标记为删除)。(24小时)</string> <string name="restart_the_app_to_create_a_new_chat_profile">重新启动应用程序以创建新的聊天资料。</string> <string name="error_smp_test_server_auth">服务器需要授权才能创建队列,检查密码</string> <string name="error_smp_test_failed_at_step">测试在步骤 %s 失败。</string> - <string name="failed_to_create_user_duplicate_desc">您已经有一个显示名相同的聊天资料。请选择另一个名字。</string> + <string name="failed_to_create_user_duplicate_desc">你已经有一个显示名相同的聊天资料。请选择另一个名字。</string> <string name="icon_descr_sent_msg_status_sent">已发送</string> <string name="mute_chat">静音</string> <string name="image_descr_profile_image">资料图片</string> @@ -599,33 +585,32 @@ <string name="settings_section_title_settings">设置</string> <string name="unknown_error">未知错误</string> <string name="unknown_database_error_with_info">未知数据库错误:%s</string> - <string name="rcv_group_event_updated_group_profile">已更新的群组资料</string> + <string name="rcv_group_event_updated_group_profile">已更新的群资料</string> <string name="rcv_group_event_member_deleted">已删除 %1$s</string> - <string name="snd_group_event_member_deleted">您删除了 %1$s</string> - <string name="profile_will_be_sent_to_contact_sending_link">您的个人资料将发送给您收到此链接的联系人。</string> + <string name="snd_group_event_member_deleted">你删除了 %1$s</string> + <string name="profile_will_be_sent_to_contact_sending_link">你的个人资料将发送给你收到此链接的联系人。</string> <string name="trying_to_connect_to_server_to_receive_messages_with_error">正在尝试连接到用于从该联系人接收消息的服务器(错误:%1$s)。</string> - <string name="connected_to_server_to_receive_messages_from_contact">您已连接到用于接收该联系人消息的服务器。</string> - <string name="description_you_shared_one_time_link">您分享了一次性链接</string> - <string name="message_delivery_error_desc">很可能此联系人已经删除了与您的联系。</string> + <string name="connected_to_server_to_receive_messages_from_contact">你已连接到用于接收该联系人消息的服务器。</string> + <string name="description_you_shared_one_time_link">你分享了一次性链接</string> + <string name="message_delivery_error_desc">很可能此联系人已经删除了与你的联系。</string> <string name="icon_descr_profile_image_placeholder">资料图片占位符</string> - <string name="smp_servers_per_user">您当前聊天资料的新连接服务器</string> - <string name="your_current_profile">您当前的资料</string> - <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">您的资料存储在您的设备上并且仅与您的联系人共享。SimpleX 服务器无法看见您的资料。</string> - <string name="your_profile_is_stored_on_your_device">您的资料、联系人和发送的消息存储在您的设备上。</string> - <string name="profile_is_only_shared_with_your_contacts">该资料仅与您的联系人共享。</string> + <string name="smp_servers_per_user">你当前聊天资料的新连接服务器</string> + <string name="your_current_profile">你当前的资料</string> + <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">你的资料存储在你的设备上并且仅与你的联系人共享。SimpleX 服务器无法看见你的资料。</string> + <string name="your_profile_is_stored_on_your_device">你的资料、联系人和发送的消息存储在你的设备上。</string> + <string name="profile_is_only_shared_with_your_contacts">该资料仅与你的联系人共享。</string> <string name="chat_preferences_on">开启</string> - <string name="delete_chat_profile_action_cannot_be_undone_warning">此操作无法撤消——您的个人资料、联系人、消息和文件将不可撤回地丢失。</string> - <string name="messages_section_description">此设置适用于您当前聊天资料中的消息</string> + <string name="delete_chat_profile_action_cannot_be_undone_warning">此操作无法撤消——你的个人资料、联系人、消息和文件将不可逆地丢失。</string> + <string name="messages_section_description">此设置适用于你当前聊天资料中的消息</string> <string name="database_restore_error">恢复数据库错误</string> <string name="restore_database_alert_confirm">恢复</string> - <string name="you_sent_group_invitation">您发送了群组邀请</string> - <string name="you_rejected_group_invitation">您拒绝了群组邀请</string> - <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">您当前的聊天数据库将被删除并替换为导入的数据库。 -\n此操作无法撤消——您的个人资料、联系人、消息和文件将不可撤回地丢失。</string> + <string name="you_sent_group_invitation">你发送了群邀请</string> + <string name="you_rejected_group_invitation">你拒绝了群邀请</string> + <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">你当前的聊天数据库将被删除并替换为导入的数据库。 \n此操作无法撤消——你的个人资料、联系人、消息和文件将不可逆地丢失。</string> <string name="rcv_group_event_member_added">已邀请 %1$s</string> - <string name="save_group_profile">保存群组资料</string> + <string name="save_group_profile">保存群资料</string> <string name="error_smp_test_certificate">服务器地址中的证书指纹可能不正确</string> - <string name="network_error_desc">请使用 %1$s 检查您的网络连接,然后重试。</string> + <string name="network_error_desc">请使用 %1$s 检查你的网络连接,然后重试。</string> <string name="v4_5_multiple_chat_profiles">多个聊天资料</string> <string name="database_initialization_error_desc">数据库不能正常工作。点击了解更多</string> <string name="message_delivery_error_title">消息传递错误</string> @@ -635,30 +620,29 @@ <string name="ttl_sec">%d 秒</string> <string name="how_simplex_works">SimpleX 是如何工作的</string> <string name="ensure_ICE_server_address_are_correct_format_and_unique">确保 WebRTC ICE 服务器地址格式正确、每行分开且不重复。</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[许多人问:<i>如果SimpleX没有用户标识符,它是怎样传递信息的?</i>]]></string> <string name="ensure_smp_server_address_are_correct_format_and_unique">确保 SMP 服务器地址格式正确、每行分开且不重复。</string> <string name="markdown_help">Markdown 帮助</string> <string name="mark_code_verified">标记为已验证</string> <string name="make_private_connection">建立私密连接</string> <string name="group_preview_join_as">以 %s 身份加入</string> - <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">如果您收到 SimpleX Chat 邀请链接,您可以在浏览器中打开它:</string> + <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">如果你收到 SimpleX Chat 邀请链接,你可以在浏览器中打开它:</string> <string name="mark_read">标记为已读</string> <string name="mark_unread">标记为未读</string> <string name="markdown_in_messages">在消息中使用 Markdown</string> <string name="file_with_path">文件:%s</string> - <string name="member_will_be_removed_from_group_cannot_be_undone">成员将被移出群组——此操作无法撤消!</string> + <string name="member_will_be_removed_from_group_cannot_be_undone">成员将被移出群——此操作无法撤消!</string> <string name="v4_5_message_draft">消息草稿</string> <string name="thousand_abbreviation">k</string> <string name="marked_deleted_description">标记为已删除</string> <string name="ttl_week">%d 星期</string> - <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">您将停止接收来自该群组的消息。聊天记录将被保留。</string> + <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">你将停止接收来自该群的消息。聊天记录将被保留。</string> <string name="group_member_role_member">成员</string> <string name="member_info_section_title_member">成员</string> <string name="ttl_weeks">%d 星期</string> <string name="ttl_min">%d 分钟</string> - <string name="ttl_months">%d 月</string> + <string name="ttl_months">%d 个月</string> <string name="network_and_servers">网络和服务器</string> - <string name="network_settings_title">网络设置</string> + <string name="network_settings_title">高级设置</string> <string name="moderated_description">已被管理员移除</string> <string name="network_use_onion_hosts_prefer_desc">Onion 主机将在可用时使用。</string> <string name="network_use_onion_hosts_no_desc">将不会使用 Onion 主机。</string> @@ -668,9 +652,7 @@ <string name="no_contacts_selected">未选择联系人</string> <string name="one_time_link">一次性邀请链接</string> <string name="feature_off">关闭</string> - <string name="network_use_onion_hosts_required_desc">连接需要 Onion 主机。 -\n请注意:如果没有 .onion 地址,您将无法连接到服务器。</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">Onion 主机将在可用时使用。</string> + <string name="network_use_onion_hosts_required_desc">连接需要 Onion 主机。 \n请注意:如果没有 .onion 地址,你将无法连接到服务器。</string> <string name="chat_item_ttl_none">从不</string> <string name="feature_offered_item">已提供 %s</string> <string name="feature_offered_item_with_param">已提供 %s:%2s</string> @@ -678,8 +660,8 @@ <string name="add_contact">一次性邀请链接</string> <string name="ok">好的</string> <string name="no_details">没有细节</string> - <string name="only_stored_on_members_devices">(仅由群组成员存储)</string> - <string name="only_you_can_send_voice">只有您可以发送语音消息。</string> + <string name="only_stored_on_members_devices">(仅由群成员存储)</string> + <string name="only_you_can_send_voice">只有你可以发送语音消息。</string> <string name="delete_message_cannot_be_undone_warning">消息将被删除——此操作无法撤消!</string> <string name="images_limit_desc">一次只能发送10张图片</string> <string name="icon_descr_more_button">更多</string> @@ -689,8 +671,6 @@ <string name="no_contacts_to_add">没有联系人可添加</string> <string name="network_status">网络状态</string> <string name="chat_preferences_off">关闭</string> - <string name="network_use_onion_hosts_no_desc_in_alert">将不会使用 Onion 主机。</string> - <string name="network_use_onion_hosts_required_desc_in_alert">连接需要 Onion 主机。</string> <string name="no_received_app_files">没有收到或发送的文件</string> <string name="sender_cancelled_file_transfer">发送人已取消文件传输。</string> <string name="share_verb">分享</string> @@ -704,9 +684,8 @@ <string name="network_use_onion_hosts_required">必须</string> <string name="save_and_notify_contact">保存并通知联系人</string> <string name="save_and_notify_contacts">保存并通知联系人</string> - <string name="read_more_in_github">在我们的 GitHub 仓库中阅读更多内容。</string> <string name="reject">拒绝</string> - <string name="to_protect_privacy_simplex_has_ids_for_queues">为了保护隐私,而不是所有其他平台使用的用户 ID,SimpleX 具有消息队列的标识符,每个联系人都是分开的。</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">为了保护隐私,SimpleX 对你的每一个联系人使用不同的 ID。</string> <string name="network_option_tcp_connection_timeout">TCP 连接超时</string> <string name="feature_received_prohibited">收到,禁止</string> <string name="accept_feature_set_1_day">设定1天</string> @@ -714,40 +693,38 @@ <string name="simplex_link_mode_browser_warning">在浏览器中打开链接可能会降低连接的隐私和安全性。SimpleX 上不受信任的链接将显示为红色。</string> <string name="restore_database_alert_desc">恢复数据库备份后请输入之前的密码。 此操作无法撤消。</string> <string name="contact_developers">请更新应用程序并联系开发者。</string> - <string name="opensource_protocol_and_code_anybody_can_run_servers">开源协议和代码——任何人都可以运行服务器。</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">任何人都可以托管服务器。</string> <string name="paste_button">粘贴</string> <string name="network_option_ping_count">PING 次数</string> <string name="prohibit_sending_voice">禁止发送语音消息。</string> <string name="network_option_ping_interval">PING 间隔</string> - <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">请检查您使用的链接是否正确,或者让您的联系人给您发送另一个链接。</string> + <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">请检查你使用的链接是否正确,或者让你的联系人给你发送另一个链接。</string> <string name="network_option_protocol_timeout">协议超时</string> <string name="reject_contact_button">拒绝</string> <string name="reply_verb">回复</string> <string name="network_options_reset_to_defaults">重置为默认</string> <string name="run_chat_section">运行聊天程序</string> - <string name="save_archive">保存存档</string> <string name="scan_code">扫码</string> - <string name="scan_code_from_contacts_app">从您联系人的应用程序中扫描安全码。</string> + <string name="scan_code_from_contacts_app">从你联系人的应用程序中扫描安全码。</string> <string name="security_code">安全码</string> <string name="secret_text">秘密</string> <string name="v4_2_security_assessment">安全评估</string> <string name="ntf_channel_messages">SimpleX 消息</string> <string name="is_not_verified">%s 未验证</string> <string name="v4_5_italian_interface_descr">感谢用户——通过 Weblate 做出贡献!</string> - <string name="first_platform_without_user_ids">第一个没有任何用户标识符的平台——专为隐私保护设计。</string> + <string name="first_platform_without_user_ids">没有用户标识符。</string> <string name="group_is_decentralized">完全去中心化 - 仅对成员可见。</string> <string name="image_decoding_exception_desc">图像无法解码。 请尝试不同的图像或联系开发者。</string> <string name="theme">主题</string> <string name="delete_files_and_media_desc">此操作无法撤消——所有接收和发送的文件和媒体都将被删除。 低分辨率图片将保留。</string> - <string name="member_role_will_be_changed_with_invitation">角色将更改为“%s”。 该成员将收到新的邀请。</string> + <string name="member_role_will_be_changed_with_invitation">角色将更改为%s。 该成员将收到新的邀请。</string> <string name="enable_automatic_deletion_message">此操作无法撤消——早于所选的发送和接收的消息将被删除。 这可能需要几分钟时间。</string> <string name="this_QR_code_is_not_a_link">此二维码不是链接!</string> <string name="switch_receiving_address_desc">接收地址将变更到不同的服务器。地址更改将在发件人上线后完成。</string> <string name="this_link_is_not_a_valid_connection_link">此链接不是有效的连接链接!</string> <string name="to_start_a_new_chat_help_header">开始新的聊天</string> - <string name="to_verify_compare">要与您的联系人验证端到端加密,请比较(或扫描)您设备上的代码。</string> + <string name="to_verify_compare">要与你的联系人验证端到端加密,请比较(或扫描)你设备上的代码。</string> <string name="unmute_chat">取消静音</string> - <string name="update_onion_hosts_settings_question">更新 .onion 主机设置?</string> <string name="update_network_session_mode_question">更新传输隔离模式?</string> <string name="connect_via_link_or_qr_from_clipboard_or_in_person">(从剪贴板扫描或粘贴)</string> <string name="smp_server_test_secure_queue">保护队列</string> @@ -758,31 +735,31 @@ <string name="images_limit_title">太多图片!</string> <string name="icon_descr_server_status_pending">待办的</string> <string name="switch_receiving_address_question">更改接收地址?</string> - <string name="ask_your_contact_to_enable_voice">请让您的联系人启用发送语音消息。</string> + <string name="ask_your_contact_to_enable_voice">请让你的联系人启用发送语音消息。</string> <string name="icon_descr_record_voice_message">录制语音消息</string> <string name="icon_descr_send_message">发消息</string> <string name="reset_verb">重置</string> <string name="send_verb">发送</string> - <string name="send_live_message_desc">发送实时消息——它会在您键入时为收件人更新</string> + <string name="send_live_message_desc">发送实时消息——它会在你键入时为收件人更新</string> <string name="add_contact_or_create_group">开始新聊天</string> - <string name="to_share_with_your_contact">(与您的联系人分享)</string> + <string name="to_share_with_your_contact">(与你的联系人分享)</string> <string name="to_connect_via_link_title">通过链接连接</string> <string name="set_contact_name">设置联系人姓名</string> - <string name="connection_you_accepted_will_be_cancelled">您接受的连接将被取消!</string> - <string name="contact_you_shared_link_with_wont_be_able_to_connect">您与之共享此链接的联系人将无法连接!</string> + <string name="connection_you_accepted_will_be_cancelled">你接受的连接将被取消!</string> + <string name="contact_you_shared_link_with_wont_be_able_to_connect">你与之共享此链接的联系人将无法连接!</string> <string name="show_QR_code">显示二维码</string> <string name="chat_with_the_founder">发送问题和想法</string> - <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">保护您的隐私和安全的消息传递和应用程序平台。</string> + <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">保护你的隐私和安全的消息传递和应用程序平台。</string> <string name="strikethrough_text">删去</string> - <string name="people_can_connect_only_via_links_you_share">人们只能通过您共享的链接与您建立联系。</string> + <string name="people_can_connect_only_via_links_you_share">你决定谁可以连接。</string> <string name="next_generation_of_private_messaging">下一代私密通讯软件</string> - <string name="paste_the_link_you_received">粘贴收到的链接</string> + <string name="paste_the_link_you_received">粘贴你收到的链接</string> <string name="alert_title_skipped_messages">已跳过消息</string> <string name="settings_section_title_support">支持 SIMPLEX CHAT</string> <string name="send_link_previews">发送链接预览</string> <string name="settings_section_title_socks">SOCKS 代理</string> <string name="stop_chat_question">停止聊天程序?</string> - <string name="stop_chat_to_export_import_or_delete_chat_database">停止聊天以便导出、导入或删除聊天数据库。在聊天停止期间,您将无法收发消息。</string> + <string name="stop_chat_to_export_import_or_delete_chat_database">停止聊天以便导出、导入或删除聊天数据库。在聊天停止期间,你将无法收发消息。</string> <string name="restore_database">恢复数据库备份</string> <string name="restore_database_alert_title">恢复数据库备份?</string> <string name="button_remove_member">删除成员</string> @@ -807,7 +784,7 @@ <string name="smp_servers_test_servers">测试服务器</string> <string name="simplex_link_contact">SimpleX 联系地址</string> <string name="simplex_link_invitation">SimpleX 一次性邀请</string> - <string name="simplex_link_group">SimpleX 群组链接</string> + <string name="simplex_link_group">SimpleX 群链接</string> <string name="simplex_link_mode">SimpleX 链接</string> <string name="sender_may_have_deleted_the_connection_request">发送人可能已删除连接请求。</string> <string name="smp_servers_preset_server">预设服务器</string> @@ -818,13 +795,12 @@ <string name="skip_inviting_button">跳过邀请成员</string> <string name="switch_verb">转变</string> <string name="prohibit_sending_voice_messages">禁止发送语音消息。</string> - <string name="only_your_contact_can_send_voice">只有您的联系人可以发送语音消息。</string> + <string name="only_your_contact_can_send_voice">只有你的联系人可以发送语音消息。</string> <string name="prohibit_direct_messages">禁止向成员发送私信。</string> <string name="protect_app_screen">保护应用程序屏幕</string> <string name="settings_section_title_themes">主题</string> - <string name="stop_chat_to_enable_database_actions">停止聊天以启用数据库操作。</string> <string name="chat_item_ttl_seconds">%s 秒</string> - <string name="alert_message_no_group">该群组已不存在。</string> + <string name="alert_message_no_group">该群已不存在。</string> <string name="group_invitation_tap_to_join">点击加入</string> <string name="stop_chat_confirmation">停止</string> <string name="restart_the_app_to_use_imported_chat_database">重新启动应用程序以使用导入的聊天数据库。</string> @@ -832,9 +808,7 @@ <string name="group_member_status_removed">已删除</string> <string name="role_in_group">角色</string> <string name="network_option_seconds_label">秒</string> - <string name="network_options_revert">恢复</string> <string name="reset_color">重置颜色</string> - <string name="save_color">保存颜色</string> <string name="v4_5_reduced_battery_usage">减少电池使用量</string> <string name="v4_5_private_filenames_descr">为了保护时区,图像/语音文件使用 UTC。</string> <string name="use_chat">使用聊天</string> @@ -843,110 +817,107 @@ <string name="auth_stop_chat">停止聊天程序</string> <string name="toast_permission_denied">权限被拒绝!</string> <string name="chat_help_tap_button">点击按钮</string> - <string name="thank_you_for_installing_simplex">感谢您安装 SimpleX Chat!</string> + <string name="thank_you_for_installing_simplex">感谢你安装 SimpleX Chat!</string> <string name="use_camera_button">相机</string> <string name="smp_servers_preset_address">预设服务器地址</string> <string name="smp_servers_scan_qr">扫描服务器二维码</string> <string name="smp_servers_test_failed">服务器测试失败!</string> <string name="smp_servers_test_some_failed">一些服务器未通过测试:</string> <string name="star_on_github">在 GitHub 上加星</string> - <string name="save_and_notify_group_members">保存并通知群组成员</string> + <string name="save_and_notify_group_members">保存并通知群成员</string> <string name="icon_descr_speaker_off">扬声器关闭</string> <string name="icon_descr_speaker_on">扬声器开启</string> - <string name="rcv_group_event_user_deleted">已将您移除</string> + <string name="rcv_group_event_user_deleted">已将你移除</string> <string name="updating_settings_will_reconnect_client_to_all_servers">更新设置会将客户端重新连接到所有服务器。</string> <string name="theme_system">系统</string> - <string name="v4_4_live_messages_desc">对方会在您键入时看到更新。</string> + <string name="v4_4_live_messages_desc">对方会在你键入时看到更新。</string> <string name="view_security_code">查看安全码</string> <string name="voice_message_with_duration">语音消息 (%1$s)</string> <string name="waiting_for_image">等待图像中</string> <string name="welcome">欢迎!</string> <string name="personal_welcome">欢迎 %1$s!</string> - <string name="you_will_be_connected_when_your_contacts_device_is_online">当您的联系人设备在线时,您将可以连接,请稍等或稍后查看!</string> + <string name="you_will_be_connected_when_your_contacts_device_is_online">当你的联系人设备在线时,你将可以连接,请稍等或稍后查看!</string> <string name="rate_the_app">评价此应用程序</string> <string name="network_enable_socks">使用 SOCKS 代理?</string> <string name="total_files_count_and_size">%d 个文件,总大小为 %s</string> - <string name="you_joined_this_group">您已加入此群组</string> - <string name="you_are_invited_to_group">您被邀请加入群组</string> + <string name="you_joined_this_group">你已加入此群</string> + <string name="you_are_invited_to_group">你被邀请加入群</string> <string name="chat_preferences_default">默认(%s)</string> <string name="voice_prohibited_in_this_chat">此聊天中禁止语音消息。</string> - <string name="voice_messages_are_prohibited">语音信息在该群组中被禁用。</string> + <string name="voice_messages_are_prohibited">语音信息被禁止。</string> <string name="verify_security_code">验证安全码</string> <string name="using_simplex_chat_servers">使用 SimpleX Chat 服务器。</string> <string name="simplex_link_connection">通过 %1$s</string> - <string name="group_invitation_item_description">邀请至群组 %1$s</string> + <string name="group_invitation_item_description">邀请至群 %1$s</string> <string name="icon_descr_address">SimpleX 地址</string> <string name="icon_descr_simplex_team">SimpleX 团队</string> - <string name="group_info_section_title_num_members">%1$s 成员</string> + <string name="group_info_section_title_num_members">%1$s 名成员</string> <string name="chat_preferences_yes">是</string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[您可以控制通过哪些服务器<b>接收</b>消息,您的联系人 - 您用来向他们发送消息的服务器。]]></string> - <string name="you_will_be_connected_when_group_host_device_is_online">您将在组主设备上线时连接到该群组,请稍等或稍后再检查!</string> - <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">当您启动应用或在应用程序驻留后台超过30 秒后,您将需要进行身份验证。</string> - <string name="archive_created_on_ts">创建于 %1$s</string> - <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[您可以 <font color="#0088ff"> 连接到 SimpleX Chat 开发者提出任何问题并接收更新 </font>。]]></string> - <string name="you_accepted_connection">您已接受连接</string> - <string name="your_SMP_servers">您的 SMP 服务器</string> - <string name="integrity_msg_skipped">%1$d 已跳过消息</string> + <string name="you_will_be_connected_when_group_host_device_is_online">你将在组主设备上线时连接到该群,请稍等或稍后再检查!</string> + <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">当你启动应用或在应用程序驻留后台超过30 秒后,你将需要进行身份验证。</string> + <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[你可以 <font color="#0088ff"> 连接到 SimpleX Chat 开发者提出任何问题并接收更新 </font>。]]></string> + <string name="you_accepted_connection">你已接受连接</string> + <string name="your_SMP_servers">你的 SMP 服务器</string> + <string name="integrity_msg_skipped">%1$d 条已跳过消息</string> <string name="ttl_s">%ds</string> <string name="whats_new">更新内容</string> - <string name="group_preview_you_are_invited">您被邀请加入群组</string> - <string name="you_have_no_chats">您没有聊天记录</string> + <string name="group_preview_you_are_invited">你被邀请加入群</string> + <string name="you_have_no_chats">你没有聊天记录</string> <string name="icon_descr_waiting_for_image">等待图像中</string> <string name="voice_message">语音消息</string> <string name="voice_messages_prohibited">语音消息禁止发送!</string> - <string name="you_need_to_allow_to_send_voice">您需要允许您的联系人发送语音消息才能发送它们。</string> + <string name="you_need_to_allow_to_send_voice">你需要允许你的联系人发送语音消息才能发送它们。</string> <string name="scan_QR_code">扫描二维码</string> - <string name="you_invited_a_contact">您邀请了您的联系人</string> - <string name="contact_wants_to_connect_with_you">想要与您连接!</string> - <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">您的联系人需要在线才能完成连接。 -\n您可以取消此连接并删除联系人(稍后尝试使用新链接)。</string> + <string name="you_invited_a_contact">你邀请了一名联系人</string> + <string name="contact_wants_to_connect_with_you">想要与你连接!</string> + <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">你的联系人需要在线才能完成连接。 \n你可以取消此连接并删除联系人(稍后尝试使用新链接)。</string> <string name="image_descr_simplex_logo">SimpleX 标志</string> - <string name="your_simplex_contact_address">您的 SimpleX 地址</string> + <string name="your_simplex_contact_address">你的 SimpleX 地址</string> <string name="install_simplex_chat_for_terminal">为终端安装 SimpleX Chat</string> <string name="use_simplex_chat_servers__question">使用 SimpleX Chat 服务器?</string> - <string name="we_do_not_store_contacts_or_messages_on_servers">我们不会在服务器上存储您的任何联系人或消息(一旦发送)。</string> + <string name="we_do_not_store_contacts_or_messages_on_servers">我们不会在服务器上存储你的任何联系人或消息(一旦发送)。</string> <string name="webrtc_ice_servers">WebRTC ICE 服务器</string> - <string name="relay_server_protects_ip">中继服务器保护您的 IP 地址,但它可以观察通话的持续时间。</string> - <string name="relay_server_if_necessary">中继服务器仅在必要时使用。其他人可能会观察到您的IP地址。</string> - <string name="your_ice_servers">您的 ICE 服务器</string> + <string name="relay_server_protects_ip">中继服务器保护你的 IP 地址,但它可以观察通话的持续时间。</string> + <string name="relay_server_if_necessary">中继服务器仅在必要时使用。其他人可能会观察到你的IP地址。</string> + <string name="your_ice_servers">你的 ICE 服务器</string> <string name="icon_descr_video_off">视频关闭</string> - <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">您可以通过应用设置/数据库或重启应用开始聊天。</string> - <string name="snd_group_event_changed_member_role">您将 %s 的角色更改为 %s</string> - <string name="snd_group_event_changed_role_for_yourself">您将自己的角色更改为 %s</string> - <string name="snd_conn_event_switch_queue_phase_completed">您已更改地址</string> - <string name="you_can_share_group_link_anybody_will_be_able_to_connect">您可以共享链接或二维码——任何人都可以加入该群组。如果您稍后将其删除,您不会失去该组的成员。</string> + <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">你可以通过应用设置/数据库或重启应用开始聊天。</string> + <string name="snd_group_event_changed_member_role">你将 %s 的角色更改为 %s</string> + <string name="snd_group_event_changed_role_for_yourself">你将自己的角色更改为 %s</string> + <string name="snd_conn_event_switch_queue_phase_completed">你已更改地址</string> + <string name="you_can_share_group_link_anybody_will_be_able_to_connect">你可以共享链接或二维码——任何人都可以加入该群。如果你稍后将其删除,你不会失去该组的成员。</string> <string name="conn_level_desc_indirect">间接(%1$s)</string> - <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[您也可以通过点击链接进行连接。 如果它在浏览器中打开,请单击<b>在移动应用程序中打开</b>按钮。]]></string> + <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[你也可以通过点击链接进行连接。 如果它在浏览器中打开,请单击<b>在移动应用程序中打开</b>按钮。]]></string> <string name="app_name">SimpleX</string> <string name="you_will_join_group">你将连接到所有群成员。</string> - <string name="description_via_group_link">通过群组链接</string> + <string name="description_via_group_link">通过群链接</string> <string name="description_via_one_time_link">通过一次性链接</string> <string name="description_via_contact_address_link">通过联系地址链接</string> <string name="simplex_link_mode_browser">通过浏览器</string> - <string name="smp_servers_your_server">您的服务器</string> + <string name="smp_servers_your_server">你的服务器</string> <string name="network_use_onion_hosts_prefer">当可用时</string> <string name="network_use_onion_hosts">使用 .onion 主机</string> - <string name="your_ICE_servers">您的 ICE 服务器</string> + <string name="your_ICE_servers">你的 ICE 服务器</string> <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> - <string name="you_control_your_chat">您的聊天由您掌控!</string> - <string name="you_can_use_markdown_to_format_messages__prompt">您可以使用 markdown 来编排消息格式:</string> + <string name="you_control_your_chat">你的聊天由你掌控!</string> + <string name="you_can_use_markdown_to_format_messages__prompt">你可以使用 markdown 来编排消息格式:</string> <string name="ttl_h">%dh</string> <string name="ttl_days">%d 天</string> <string name="ttl_w">%dw</string> - <string name="you_are_invited_to_group_join_to_connect_with_group_members">您被邀请加入群组。 加入以与群组成员联系。</string> - <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">你加入了这个群组。连接到邀请组成员。</string> - <string name="snd_conn_event_switch_queue_phase_completed_for_member">您更改了 %s 的地址</string> - <string name="snd_group_event_user_left">您已离开</string> - <string name="num_contacts_selected">%d 已选择联系人</string> - <string name="chat_preferences_you_allow">您允许</string> + <string name="you_are_invited_to_group_join_to_connect_with_group_members">你被邀请加入群。 加入以与群成员联系。</string> + <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">你加入了这个群。连接到邀请组成员。</string> + <string name="snd_conn_event_switch_queue_phase_completed_for_member">你更改了 %s 的地址</string> + <string name="snd_group_event_user_left">你已离开</string> + <string name="num_contacts_selected">已选择 %d 名联系人</string> + <string name="chat_preferences_you_allow">你允许</string> <string name="v4_2_auto_accept_contact_requests_desc">带有可选的欢迎消息。</string> <string name="ttl_m">%dm</string> <string name="ttl_mth">%dmth</string> <string name="waiting_for_file">等待文件中</string> - <string name="contact_sent_large_file">您的联系人发送的文件大于当前支持的最大大小 (%1$s).</string> - <string name="you_will_be_connected_when_your_connection_request_is_accepted">当您的连接请求被接受后,您将可以连接,请稍等或稍后检查!</string> + <string name="contact_sent_large_file">你的联系人发送的文件大于当前支持的最大大小 (%1$s).</string> + <string name="you_will_be_connected_when_your_connection_request_is_accepted">当你的连接请求被接受后,你将可以连接,请稍等或稍后检查!</string> <string name="smp_servers_use_server">使用服务器</string> - <string name="smp_servers_your_server_address">您的服务器地址</string> + <string name="smp_servers_your_server_address">你的服务器地址</string> <string name="icon_descr_video_on">视频开启</string> <string name="v4_3_voice_messages_desc">最多 40 秒,立即收到。</string> <string name="v4_4_verify_connection_security">验证连接安全</string> @@ -955,12 +926,12 @@ <string name="moderate_message_will_be_deleted_warning">将为所有成员删除该消息。</string> <string name="moderate_message_will_be_marked_warning">该消息将对所有成员标记为已被管理员移除。</string> <string name="delete_member_message__question">删除成员消息?</string> - <string name="group_member_role_observer">观察者</string> - <string name="you_are_observer">您是观察者</string> - <string name="error_updating_link_for_group">更新群组链接错误</string> - <string name="observer_cant_send_message_title">您无法发送消息!</string> + <string name="group_member_role_observer">观察员</string> + <string name="you_are_observer">你是观察者</string> + <string name="error_updating_link_for_group">更新群链接错误</string> + <string name="observer_cant_send_message_title">你无法发送消息!</string> <string name="initial_member_role">初始角色</string> - <string name="observer_cant_send_message_desc">请联系群组管理员。</string> + <string name="observer_cant_send_message_desc">请联系群管理员。</string> <string name="language_system">系统</string> <string name="password_to_show">用于显示的密码</string> <string name="save_profile_password">保存个人资料密码</string> @@ -977,32 +948,29 @@ <string name="v4_6_reduced_battery_usage_descr">更多改进即将推出!</string> <string name="v4_6_group_moderation_descr">现在管理员可以: \n- 删除成员的消息。 -\n- 禁用成员(“观察员”角色)</string> - <string name="v4_6_hidden_chat_profiles_descr">使用密码保护您的聊天资料!</string> +\n- 禁用成员(观察员角色)</string> + <string name="v4_6_hidden_chat_profiles_descr">使用密码保护你的聊天资料!</string> <string name="confirm_password">确认密码</string> <string name="error_updating_user_privacy">更新用户隐私错误</string> - <string name="cant_delete_user_profile">无法删除用户资料!</string> <string name="error_saving_user_password">保存用户密码错误</string> <string name="enter_password_to_show">在搜索中输入密码</string> - <string name="v4_6_group_welcome_message">群组欢迎消息</string> - <string name="v4_6_group_moderation">群组管理员移除</string> + <string name="v4_6_group_welcome_message">群欢迎消息</string> + <string name="v4_6_group_moderation">群管理员移除</string> <string name="hidden_profile_password">隐藏的个人资料密码</string> <string name="v4_6_hidden_chat_profiles">隐藏的聊天资料</string> <string name="hide_profile">隐藏个人资料</string> <string name="smp_save_servers_question">保存服务器?</string> - <string name="to_reveal_profile_enter_password">要显示您的隐藏的个人资料,请在您的聊天个人资料页面的搜索字段中输入完整密码。</string> + <string name="to_reveal_profile_enter_password">要显示你的隐藏的个人资料,请在你的聊天个人资料页面的搜索字段中输入完整密码。</string> <string name="save_welcome_message_question">保存欢迎信息?</string> <string name="tap_to_activate_profile">点击以激活个人资料。</string> - <string name="should_be_at_least_one_profile">应该至少有一个用户资料。</string> <string name="user_unhide">取消隐藏</string> <string name="v4_6_group_welcome_message_descr">设置向新成员显示的消息!</string> <string name="v4_6_audio_video_calls_descr">支持蓝牙和其他改进。</string> <string name="v4_6_chinese_spanish_interface_descr">感谢用户——通过 Weblate 做出贡献!</string> - <string name="should_be_at_least_one_visible_profile">应该至少有一个可见的用户资料。</string> <string name="user_unmute">解除静音</string> <string name="button_welcome_message">欢迎消息</string> - <string name="you_will_still_receive_calls_and_ntfs">当静音配置文件处于活动状态时,您仍会收到来自静音配置文件的电话和通知。</string> - <string name="you_can_hide_or_mute_user_profile">您可以隐藏或静音用户配置文件——长按以显示菜单。</string> + <string name="you_will_still_receive_calls_and_ntfs">当静音配置文件处于活动状态时,你仍会收到来自静音配置文件的电话和通知。</string> + <string name="you_can_hide_or_mute_user_profile">你可以隐藏或静音用户配置文件——长按以显示菜单。</string> <string name="group_welcome_title">欢迎消息</string> <string name="confirm_database_upgrades">确认数据库升级</string> <string name="settings_section_title_experimenta">实验性</string> @@ -1013,13 +981,13 @@ <string name="mtr_error_no_down_migration">数据库版本比应用程序更新,但无法降级迁移:%s</string> <string name="downgrade_and_open_chat">降级并打开聊天</string> <string name="hide_dev_options">隐藏:</string> - <string name="file_will_be_received_when_contact_completes_uploading">文件将在您的联系人完成上传后收到。</string> + <string name="file_will_be_received_when_contact_completes_uploading">文件将在你的联系人完成上传后收到。</string> <string name="incompatible_database_version">数据库版本不兼容</string> <string name="database_migrations">迁移:%s</string> - <string name="image_will_be_received_when_contact_completes_uploading">图片将在您的联系人完成上传后收到。</string> + <string name="image_will_be_received_when_contact_completes_uploading">图片将在你的联系人完成上传后收到。</string> <string name="show_developer_options">显示开发者选项</string> <string name="upgrade_and_open_chat">升级并打开聊天</string> - <string name="database_downgrade_warning">警告:您可能会丢失部分数据!</string> + <string name="database_downgrade_warning">警告:你可能会丢失部分数据!</string> <string name="invalid_migration_confirmation">迁移确认无效</string> <string name="show_dev_options">显示:</string> <string name="delete_profile">删除个人资料</string> @@ -1031,15 +999,15 @@ <string name="videos_limit_title">过多视频!</string> <string name="video_descr">视频</string> <string name="icon_descr_waiting_for_video">等待视频中</string> - <string name="video_will_be_received_when_contact_is_online">视频将在您的联系人在线时收到,请稍等或稍后查看!</string> + <string name="video_will_be_received_when_contact_is_online">视频将在你的联系人在线时收到,请稍等或稍后查看!</string> <string name="waiting_for_video">等待视频中</string> <string name="icon_descr_video_snd_complete">视频已发送</string> <string name="icon_descr_video_asked_to_receive">要求接收视频</string> - <string name="video_will_be_received_when_contact_completes_uploading">视频将在您的联系人完成上传后收到。</string> + <string name="video_will_be_received_when_contact_completes_uploading">视频将在你的联系人完成上传后收到。</string> <string name="error_xftp_test_server_auth">服务器需要授权来上传,检查密码</string> <string name="smp_server_test_upload_file">上传文件</string> <string name="xftp_servers">XFTP 服务器</string> - <string name="your_XFTP_servers">您的 XFTP 服务器</string> + <string name="your_XFTP_servers">你的 XFTP 服务器</string> <string name="disable_onion_hosts_when_not_supported"><![CDATA[如果 SOCKS 代理不支持它们,请将 <i>Use .onion hosts</i> 设置为否。]]></string> <string name="network_socks_toggle_use_socks_proxy">使用 SOCKS 代理</string> <string name="port_verb">端口</string> @@ -1062,7 +1030,7 @@ <string name="la_no_app_password">没有应用程序密码</string> <string name="la_lock_mode_passcode">密码输入</string> <string name="la_please_remember_to_store_password">请牢记或妥善保管——丢失的密码将无法恢复!</string> - <string name="you_can_turn_on_lock">您可以通过设置开启 SimpleX 锁定。</string> + <string name="you_can_turn_on_lock">你可以通过设置开启 SimpleX 锁定。</string> <string name="la_authenticate">身份验证</string> <string name="la_auth_failed">身份验证失败</string> <string name="la_change_app_passcode">更改密码</string> @@ -1080,18 +1048,18 @@ <string name="passcode_set">密码已设置!</string> <string name="la_mode_system">系统</string> <string name="lock_not_enabled">未启用 SimpleX 锁定!</string> - <string name="la_could_not_be_verified">您的身份无法验证,请再试一次。</string> + <string name="la_could_not_be_verified">你的身份无法验证,请再试一次。</string> <string name="authentication_cancelled">身份验证已取消</string> <string name="la_current_app_passcode">当前密码</string> <string name="la_immediately">立即</string> <string name="alert_title_msg_bad_hash">错误消息散列</string> <string name="alert_title_msg_bad_id">错误消息 ID</string> - <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d 消息解密失败。</string> - <string name="alert_text_decryption_error_too_many_skipped">%1$d 已跳过消息。</string> - <string name="alert_text_fragment_encryption_out_of_sync_old_database">当您或您的连接使用旧数据库备份时,可能会发生这种情况。</string> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d 条消息解密失败。</string> + <string name="alert_text_decryption_error_too_many_skipped">跳过了 %1$d 条消息。</string> + <string name="alert_text_fragment_encryption_out_of_sync_old_database">当你或你的连接使用旧数据库备份时,可能会发生这种情况。</string> <string name="decryption_error">解密错误</string> <string name="alert_text_fragment_please_report_to_developers">请向开发者报告。</string> - <string name="alert_text_msg_bad_hash">上一条消息的散列不同。</string> + <string name="alert_text_msg_bad_hash">上条消息的哈希值不同。</string> <string name="alert_text_msg_bad_id">下一条消息的 ID 不正确(小于或等于上一条)。 \n它可能是由于某些错误或连接被破坏才发生。</string> <string name="stop_file__action">停止文件</string> @@ -1100,10 +1068,10 @@ <string name="stop_rcv_file__title">停止接收文件?</string> <string name="stop_rcv_file__message">即将停止接收文件。</string> <string name="stop_file__confirm">停止</string> - <string name="revoke_file__action">撤销文件</string> - <string name="revoke_file__title">撤销文件?</string> + <string name="revoke_file__action">吊销文件</string> + <string name="revoke_file__title">吊销文件?</string> <string name="revoke_file__message">文件将从服务器中删除。</string> - <string name="revoke_file__confirm">撤销</string> + <string name="revoke_file__confirm">吊销</string> <string name="audio_video_calls">音频/视频通话</string> <string name="available_in_v51">" \n在 v5.1 版本中可用"</string> @@ -1114,30 +1082,30 @@ <string name="v5_0_large_files_support">最大 1gb 的视频和文件</string> <string name="v5_0_large_files_support_descr">快速且无需等待发件人在线!</string> <string name="prohibit_calls">禁止音频/视频通话。</string> - <string name="both_you_and_your_contact_can_make_calls">您和您的联系人都可以拨打电话。</string> - <string name="only_you_can_make_calls">只有您可以拨打电话。</string> - <string name="only_your_contact_can_make_calls">只有您的联系人可以拨打电话。</string> - <string name="allow_your_contacts_to_call">允许您的联系人与您进行语音通话。</string> - <string name="allow_calls_only_if">仅当您的联系人允许时才允许呼叫。</string> + <string name="both_you_and_your_contact_can_make_calls">你和你的联系人都可以进行呼叫。</string> + <string name="only_you_can_make_calls">只有你可以进行呼叫。</string> + <string name="only_your_contact_can_make_calls">只有你的联系人可以进行呼叫。</string> + <string name="allow_your_contacts_to_call">允许联系人呼叫你。</string> + <string name="allow_calls_only_if">允许通话,前提是你的联系人允许它们。</string> <string name="calls_prohibited_with_this_contact">禁止音频/视频通话。</string> <string name="send_disappearing_message_1_minute">1分钟</string> <string name="one_time_link_short">一次性链接</string> - <string name="both_you_and_your_contact_can_add_message_reactions">您和您的联系人都可以添加消息回应。</string> + <string name="both_you_and_your_contact_can_add_message_reactions">你和你的联系人都可以添加消息回应。</string> <string name="allow_message_reactions">允许消息回应。</string> - <string name="allow_message_reactions_only_if">只有您的联系人允许时才允许消息回应。</string> + <string name="allow_message_reactions_only_if">允许消息回应,前提是你的联系人允许它们。</string> <string name="app_passcode_replaced_with_self_destruct">应用程序密码被替换为自毁密码。</string> <string name="change_self_destruct_mode">更改自毁模式</string> <string name="learn_more_about_address">关于 SimpleX 地址</string> <string name="continue_to_next_step">继续</string> - <string name="all_your_contacts_will_remain_connected_update_sent">您的所有联系人将保持连接。个人资料更新将发送给您的联系人。</string> + <string name="all_your_contacts_will_remain_connected_update_sent">你的所有联系人将保持连接。个人资料更新将发送给你的联系人。</string> <string name="auto_accept_contact">自动接受</string> <string name="color_secondary_variant">额外的次要</string> <string name="color_background">背景</string> <string name="send_disappearing_message_5_minutes">5分钟</string> <string name="send_disappearing_message_30_seconds">30秒</string> <string name="address_section_title">地址</string> - <string name="add_address_to_your_profile">将地址添加到您的个人资料,以便您的联系人可以与其他人共享。个人资料更新将发送给您的联系人。</string> - <string name="allow_your_contacts_adding_message_reactions">允许您的联系人添加消息回应。</string> + <string name="add_address_to_your_profile">将地址添加到你的个人资料,以便你的联系人可以与其他人共享。个人资料更新将发送给你的联系人。</string> + <string name="allow_your_contacts_adding_message_reactions">允许你的联系人添加消息回应。</string> <string name="color_primary_variant">额外的强调色</string> <string name="all_app_data_will_be_cleared">已删除所有应用程序数据。</string> <string name="empty_chat_profile_is_created">已创建一个包含所提供名字的空白聊天资料,应用程序照常打开。</string> @@ -1153,7 +1121,7 @@ <string name="info_row_disappears_at">消失于</string> <string name="error_setting_address">设置地址错误</string> <string name="customize_theme_title">自定义主题</string> - <string name="create_address_and_let_people_connect">创建一个地址,让人们与您联系。</string> + <string name="create_address_and_let_people_connect">创建一个地址,让人们与你联系。</string> <string name="create_simplex_address">创建 SimpleX 地址</string> <string name="enter_welcome_message_optional">输入欢迎消息……(可选)</string> <string name="dont_create_address">不创建地址</string> @@ -1171,9 +1139,9 @@ <string name="share_text_deleted_at">已删除于:%s</string> <string name="share_text_disappears_at">消失于:%s</string> <string name="prohibit_message_reactions">禁止消息回应。</string> - <string name="only_you_can_add_message_reactions">只有您可以添加消息回应。</string> - <string name="group_members_can_add_message_reactions">群组成员可以添加信息回应。</string> - <string name="message_reactions_are_prohibited">该群组禁用了消息回应。</string> + <string name="only_you_can_add_message_reactions">只有你可以添加消息回应。</string> + <string name="group_members_can_add_message_reactions">成员可以添加信息回应。</string> + <string name="message_reactions_are_prohibited">消息回应被禁止。</string> <string name="self_destruct_passcode_changed">自毁密码已更改!</string> <string name="self_destruct_passcode_enabled">自毁密码已启用!</string> <string name="set_passcode">设置密码</string> @@ -1181,7 +1149,7 @@ <string name="sent_message">已发信息</string> <string name="edit_history">历史记录</string> <string name="send_disappearing_message_send">发送</string> - <string name="if_you_enter_passcode_data_removed">如果您在打开应用时输入该密码,所有应用程序数据将被不可撤回地删除!</string> + <string name="if_you_enter_passcode_data_removed">如果你在打开应用时输入该密码,所有应用程序数据将被不可逆地删除!</string> <string name="self_destruct_new_display_name">新的显示名:</string> <string name="info_row_moderated_at">已被管理员移除于</string> <string name="info_row_sent_at">已发送于</string> @@ -1191,18 +1159,18 @@ <string name="current_version_timestamp">%s (当前)</string> <string name="share_text_sent_at">发送于 %s</string> <string name="color_received_message">收到的信息</string> - <string name="only_your_contact_can_add_message_reactions">只有您的联系人可以添加消息回应。</string> + <string name="only_your_contact_can_add_message_reactions">只有你的联系人可以添加消息回应。</string> <string name="opening_database">打开数据库中……</string> - <string name="auth_open_chat_profiles">打开聊天资料</string> - <string name="scan_qr_to_connect_to_contact">您的联系人可以扫描二维码或使用应用程序中的链接来建立连接。</string> - <string name="you_can_share_your_address">您可以将您的地址作为链接或二维码共享——任何人都可以连接到您。</string> - <string name="if_you_cant_meet_in_person">如果您不能亲自见面,可以在视频通话中展示二维码,或分享链接。</string> + <string name="auth_open_chat_profiles">更改聊天资料</string> + <string name="scan_qr_to_connect_to_contact">你的联系人可以扫描二维码或使用应用程序中的链接来建立连接。</string> + <string name="you_can_share_your_address">你可以将你的地址作为链接或二维码共享——任何人都可以连接到你。</string> + <string name="if_you_cant_meet_in_person">如果你不能亲自见面,可以在视频通话中展示二维码,或分享链接。</string> <string name="learn_more">了解更多</string> - <string name="you_can_accept_or_reject_connection">当人们请求连接时,您可以接受或拒绝它。</string> - <string name="you_wont_lose_your_contacts_if_delete_address">如果您以后删除您的地址,您不会丢失您的联系人。</string> + <string name="you_can_accept_or_reject_connection">当人们请求连接时,你可以接受或拒绝它。</string> + <string name="you_wont_lose_your_contacts_if_delete_address">如果你以后删除你的地址,你不会丢失你的联系人。</string> <string name="read_more_in_user_guide_with_link"><![CDATA[在<font color="#0088ff">用户指南</font>中阅读更多。]]></string> - <string name="theme_colors_section_title">主题颜色</string> - <string name="your_contacts_will_remain_connected">与您的联系人保持连接。</string> + <string name="theme_colors_section_title">界面颜色</string> + <string name="your_contacts_will_remain_connected">与你的联系人保持连接。</string> <string name="share_with_contacts">与联系人分享</string> <string name="invite_friends">邀请朋友</string> <string name="save_auto_accept_settings">保存自动接受设置</string> @@ -1211,9 +1179,9 @@ <string name="email_invite_body">你好! \n用 SimpleX Chat 与我联系:%s</string> <string name="email_invite_subject">让我们一起在 SimpleX Chat 里聊天</string> - <string name="you_can_create_it_later">您可以以后创建它</string> + <string name="you_can_create_it_later">你可以以后创建它</string> <string name="share_address">分享地址</string> - <string name="you_can_share_this_address_with_your_contacts">您可以与您的联系人分享该地址,让他们与 %s 联系。</string> + <string name="you_can_share_this_address_with_your_contacts">你可以与你的联系人分享该地址,让他们与 %s 联系。</string> <string name="group_welcome_preview">预览</string> <string name="import_theme">导入主题</string> <string name="theme_simplex">SimpleX</string> @@ -1238,8 +1206,8 @@ <string name="color_sent_message">已发信息</string> <string name="message_reactions">消息回应</string> <string name="message_reactions_prohibited_in_this_chat">该聊天禁用了消息回应。</string> - <string name="if_you_enter_self_destruct_code">如果您在打开应用程序时输入自毁密码:</string> - <string name="profile_update_will_be_sent_to_contacts">个人资料更新将被发送给您的联系人。</string> + <string name="if_you_enter_self_destruct_code">如果你在打开应用程序时输入自毁密码:</string> + <string name="profile_update_will_be_sent_to_contacts">个人资料更新将被发送给你的联系人。</string> <string name="info_row_updated_at">记录更新于</string> <string name="prohibit_message_reactions_group">禁止消息回应。</string> <string name="info_row_received_at">已收到于</string> @@ -1251,10 +1219,10 @@ <string name="v5_1_better_messages_descr">- 语音消息最长5分钟。 \n- 自定义限时消息。 \n- 编辑消息历史。</string> - <string name="non_fatal_errors_occured_during_import">导入过程中发生了一些非致命错误——您可以查看聊天控制台了解更多详细信息。</string> + <string name="non_fatal_errors_occured_during_import">导入过程中发生了一些非致命错误:</string> <string name="settings_section_title_app">应用程序</string> <string name="settings_restart_app">重启</string> - <string name="shutdown_alert_desc">通知将停止工作直到您重启应用程序</string> + <string name="shutdown_alert_desc">通知将停止工作直到你重启应用程序</string> <string name="shutdown_alert_question">关闭?</string> <string name="settings_shutdown">关闭</string> <string name="error_aborting_address_change">中止地址更改错误</string> @@ -1264,9 +1232,9 @@ <string name="abort_switch_receiving_address">中止地址更改</string> <string name="allow_to_send_files">允许发送文件和媒体。</string> <string name="files_and_media">文件和媒体</string> - <string name="only_owners_can_enable_files_and_media">只有组主可以启用文件和媒体。</string> - <string name="files_are_prohibited_in_group">此群组中禁止文件和媒体。</string> - <string name="group_members_can_send_files">群组成员可以发送文件和媒体。</string> + <string name="only_owners_can_enable_files_and_media">只有群主可以启用文件和媒体。</string> + <string name="files_are_prohibited_in_group">文件和媒体被禁止。</string> + <string name="group_members_can_send_files">成员可以发送文件和媒体。</string> <string name="prohibit_sending_files">禁止发送文件和媒体。</string> <string name="files_and_media_prohibited">禁止文件和媒体!</string> <string name="no_filtered_chats">无过滤聊天</string> @@ -1283,9 +1251,7 @@ <string name="choose_file_title">选择一个文件</string> <string name="receipts_section_contacts">联系人</string> <string name="conn_event_ratchet_sync_started">协调加密中…</string> - <string name="v5_2_more_things_descr">- 更稳定的消息送达. -\n- 更好的群组. -\n- 还有更多!</string> + <string name="v5_2_more_things_descr">- 更稳定的消息传送. \n- 更好的群. \n- 还有更多!</string> <string name="connect__a_new_random_profile_will_be_shared">一个新的随机个人档案将被分享。</string> <string name="snd_conn_event_ratchet_sync_started">与 %s 协调加密中…</string> <string name="in_developing_desc">该功能还没支持。请尝试下一个版本。</string> @@ -1296,10 +1262,10 @@ <string name="recipient_colon_delivery_status">%s: %s</string> <string name="in_developing_title">敬请期待!</string> <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">数据库将被加密,密码将存储在设置中。</string> - <string name="you_can_enable_delivery_receipts_later">您可以稍后在“设置”中启用它</string> - <string name="receipts_groups_disable_for_all">对所有群组关闭</string> + <string name="you_can_enable_delivery_receipts_later">你可以稍后在“设置”中启用它</string> + <string name="receipts_groups_disable_for_all">对所有群关闭</string> <string name="no_info_on_delivery">无送货信息</string> - <string name="connect__your_profile_will_be_shared">您的个人资料 %1$s 将被共享。</string> + <string name="connect__your_profile_will_be_shared">你的个人资料 %1$s 将被共享。</string> <string name="sending_delivery_receipts_will_be_enabled">将为所有联系人启用送达回执功能。</string> <string name="turn_off_system_restriction_button">打开应用程序设置</string> <string name="receipts_groups_enable_for_all">为所有组启用</string> @@ -1316,13 +1282,12 @@ <string name="sender_at_ts">%s 在 %s</string> <string name="receipts_contacts_title_disable">禁用回执?</string> <string name="sync_connection_force_question">重新协商加密?</string> - <string name="receipts_section_description_1">可以在联系人和群组设置中覆盖它们。</string> + <string name="receipts_section_description_1">可以在联系人和群设置中覆盖它们。</string> <string name="receipts_contacts_disable_for_all">对所有联系人关闭</string> - <string name="you_can_change_it_later">随机密码以明文形式存储在设置中。 -\n您可以稍后更改。</string> + <string name="you_can_change_it_later">随机密码以明文形式存储在设置中。 \n你可以稍后更改。</string> <string name="receipts_groups_override_disabled">已禁用 %d 组的送达回执功能</string> <string name="snd_conn_event_ratchet_sync_required">需要为 %s 重新协商加密</string> - <string name="system_restricted_background_desc">SimpleX 无法在后台运行。只有在应用程序运行时,您才会收到通知。</string> + <string name="system_restricted_background_desc">SimpleX 无法在后台运行。只有在应用程序运行时,你才会收到通知。</string> <string name="receipts_contacts_enable_keep_overrides">启用(保留覆盖)</string> <string name="database_encryption_will_be_updated_in_settings">即将更新数据库加密密码并将其存储在设置中。</string> <string name="connect_use_current_profile">使用当前配置文件</string> @@ -1334,16 +1299,16 @@ <string name="v5_2_disappear_one_message_descr">即使在对话中禁用。</string> <string name="use_random_passphrase">使用随机密码</string> <string name="system_restricted_background_in_call_title">无后台通话</string> - <string name="you_can_enable_delivery_receipts_later_alert">您可以稍后通过应用程序隐私和安全设置启用它们。</string> + <string name="you_can_enable_delivery_receipts_later_alert">你可以稍后通过应用程序隐私和安全设置启用它们。</string> <string name="save_passphrase_in_settings">在设置中保存密码</string> <string name="enable_receipts_all">启用</string> - <string name="send_receipts_disabled_alert_msg">该群组成员超过 %1$d ,未发送送达回执。</string> + <string name="send_receipts_disabled_alert_msg">该群成员超过 %1$d ,未发送送达回执。</string> <string name="fix_connection_question">修复连接?</string> - <string name="v5_2_message_delivery_receipts_descr">我们错过的第二个\"√\"!✅</string> + <string name="v5_2_message_delivery_receipts_descr">我们错过的第二个"√"!✅</string> <string name="setup_database_passphrase">设定数据库密码</string> - <string name="receipts_groups_title_disable">为群组禁用回执吗?</string> + <string name="receipts_groups_title_disable">为群禁用回执吗?</string> <string name="rcv_group_event_3_members_connected">%s、%s 和 %s 已连接</string> - <string name="fix_connection_not_supported_by_group_member">修复群组成员不支持的问题</string> + <string name="fix_connection_not_supported_by_group_member">修复群成员不支持的问题</string> <string name="receipts_groups_override_enabled">已为 %d 组启用送达回执功能</string> <string name="sync_connection_force_confirm">重新协商</string> <string name="receipts_contacts_disable_keep_overrides">禁用(保留覆盖)</string> @@ -1358,18 +1323,18 @@ <string name="fix_connection">修复连接</string> <string name="rcv_group_event_2_members_connected">%s 和 %s 已连接</string> <string name="send_receipts_disabled">关闭</string> - <string name="receipts_section_groups">小群组(最多 20 人)</string> + <string name="receipts_section_groups">小群(最多 20 人)</string> <string name="privacy_show_last_messages">显示最近的消息</string> <string name="settings_section_title_delivery_receipts">将送达回执发送给</string> <string name="error_enabling_delivery_receipts">启用已读回执时出错!</string> <string name="passphrase_will_be_saved_in_settings">更改密码或重启应用后,密码将以明文形式保存在设置中。</string> - <string name="paste_the_link_you_received_to_connect_with_your_contact">粘贴您收到的链接以与您的联系人联系…</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">粘贴你收到的链接以与你的联系人联系…</string> <string name="send_receipts">送达回执</string> - <string name="no_selected_chat">没有选择聊天</string> + <string name="no_selected_chat">没有选中的聊天</string> <string name="conn_event_ratchet_sync_ok">可以加密</string> <string name="renegotiate_encryption">重新协商加密</string> <string name="receipts_groups_disable_keep_overrides">禁用(保留组覆盖)</string> - <string name="receipts_groups_title_enable">为群组启用回执吗?</string> + <string name="receipts_groups_title_enable">为群启用回执吗?</string> <string name="fix_connection_not_supported_by_contact">修复联系人不支持的问题</string> <string name="snd_conn_event_ratchet_sync_ok">对 %s 加密正常</string> <string name="v5_2_fix_encryption_descr">修复还原备份后的加密问题。</string> @@ -1380,7 +1345,7 @@ <string name="connect_via_member_address_alert_desc">连接请求将发送给该组成员。</string> <string name="settings_is_storing_in_clear_text">密码以明文形式存储在设置中。</string> <string name="error_synchronizing_connection">同步连接时出错</string> - <string name="receipts_section_description">这些设置适用于您当前的配置文件</string> + <string name="receipts_section_description">这些设置适用于你当前的配置文件</string> <string name="snd_conn_event_ratchet_sync_allowed">允许为 %s 重新协商加密</string> <string name="receipts_contacts_enable_for_all">为所有人启用</string> <string name="conn_event_ratchet_sync_required">需要重新协商加密</string> @@ -1391,14 +1356,12 @@ <string name="v5_3_new_desktop_app">全新桌面应用!</string> <string name="v5_3_new_interface_languages">6种全新的界面语言</string> <string name="v5_3_encrypt_local_files_descr">应用程序为新的本地文件(视频除外)加密。</string> - <string name="v5_3_discover_join_groups">发现和加入群组</string> + <string name="v5_3_discover_join_groups">发现和加入群</string> <string name="v5_3_simpler_incognito_mode">简化的隐身模式</string> <string name="v5_3_new_interface_languages_descr">阿拉伯语、保加利亚语、芬兰语、希伯莱语、泰国语和乌克兰语——得益于用户和Weblate。</string> <string name="v5_3_new_desktop_app_descr">在桌面应用里创建新的账号。💻</string> <string name="v5_3_simpler_incognito_mode_descr">在连接时切换隐身模式。</string> - <string name="v5_3_discover_join_groups_descr">- 连接到目录服务(BETA)! -\n- 发送回执(至多20名成员)。 -\n- 更快,更稳定。</string> + <string name="v5_3_discover_join_groups_descr">- 连接到目录服务(BETA)! \n- 发送回执(至多20名成员)。 \n- 更快、更稳定。</string> <string name="rcv_group_event_open_chat">打开</string> <string name="error_creating_member_contact">创建成员联系人时出错</string> <string name="compose_send_direct_message_to_connect">发送私信来连接</string> @@ -1407,28 +1370,28 @@ <string name="expand_verb">展开</string> <string name="connect_plan_repeat_connection_request">重复连接请求吗?</string> <string name="rcv_direct_event_contact_deleted">已删除联系人</string> - <string name="connect_plan_you_are_already_connecting_to_vName">你已经在连接到 %1$s。</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[你已经在连接到 <b>%1$s</b>。]]></string> <string name="error_alert_title">错误</string> <string name="connect_plan_you_are_already_joining_the_group_via_this_link">你已经在通过此链接加入该群。</string> <string name="create_group_button">建群</string> <string name="create_another_profile_button">创建个人资料</string> <string name="group_members_2">%s 和 %s</string> <string name="connect_plan_join_your_group">加入你的群吗?</string> - <string name="connect_plan_you_are_already_joining_the_group_vName">你已经在加入 %1$s 群。</string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[你已经在加入 <b>%1$s</b> 群。]]></string> <string name="connect_plan_this_is_your_own_one_time_link">这是你自己的一次性链接!</string> - <string name="marked_deleted_items_description">%d 条消息被标记为删除</string> + <string name="marked_deleted_items_description">%d 条消息被标记为已删除</string> <string name="connect_plan_group_already_exists">群已存在!</string> <string name="connect_plan_already_connecting">已经在连接了!</string> <string name="video_decoding_exception_desc">无法解码该视频。请尝试不同视频或联络开发者。</string> <string name="rcv_group_event_1_member_connected">%s 已连接</string> <string name="rcv_group_and_other_events">及其他 %d 个事件</string> <string name="connect_plan_connect_via_link">通过链接进行连接吗?</string> - <string name="connect_plan_already_joining_the_group">已经加入了该群组!</string> + <string name="connect_plan_already_joining_the_group">已经加入了该群!</string> <string name="group_members_n">%s、 %s 和 %d 名成员</string> <string name="unblock_member_button">解封成员</string> <string name="connect_plan_connect_to_yourself">连接到你自己?</string> <string name="contact_tap_to_connect">轻按连接</string> - <string name="connect_plan_you_are_already_in_group_vName">你已经在%1$s 群内。</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[你已经在<b>%1$s</b> 群内。]]></string> <string name="connect_plan_this_is_your_own_simplex_address">这是你自己的 SimpleX 地址!</string> <string name="correct_name_to">更正名称为 %s?</string> <string name="delete_messages__question">删除 %d 条消息吗?</string> @@ -1449,7 +1412,7 @@ <string name="block_member_question">封禁成员吗?</string> <string name="rcv_group_events_count">%d 个群事件</string> <string name="invalid_name">无效名称!</string> - <string name="connect_plan_this_is_your_link_for_group_vName">这是给你的 %1$s 群链接!</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[这是给你的 <b>%1$s</b> 群链接!]]></string> <string name="unblock_member_confirmation">解封</string> <string name="non_content_uri_alert_title">无效的文件路径</string> <string name="connect_plan_you_have_already_requested_connection_via_this_address">你已经请求通过此地址进行连接!</string> @@ -1502,14 +1465,14 @@ <string name="loading_remote_file_desc">从已链接移动设备加载文件时请稍候片刻</string> <string name="desktop_app_version_is_incompatible">桌面应用版本 %s 不兼容此应用。</string> <string name="verify_connection">验证连接</string> - <string name="v5_4_block_group_members">屏蔽群组成员</string> - <string name="v5_4_incognito_groups_descr">使用随机身份创建群组</string> + <string name="v5_4_block_group_members">屏蔽群成员</string> + <string name="v5_4_incognito_groups_descr">使用随机身份创建群</string> <string name="v5_4_link_mobile_desktop">连接移动端和桌面端应用程序!🔗</string> <string name="v5_4_link_mobile_desktop_descr">通过安全的、抗量子计算机破解的协议。</string> <string name="v5_4_block_group_members_descr">隐藏不需要的信息。</string> - <string name="v5_4_better_groups">更佳的群组</string> - <string name="v5_4_incognito_groups">匿名群组</string> - <string name="remote_host_was_disconnected_toast"><![CDATA[远程主机 <b>%s</b> 已断开连接]]></string> + <string name="v5_4_better_groups">更佳的群</string> + <string name="v5_4_incognito_groups">匿名群</string> + <string name="remote_host_was_disconnected_toast"><![CDATA[移动主机 <b>%s</b> 连接断开]]></string> <string name="v5_4_better_groups_descr">加入速度更快、信息更可靠。</string> <string name="v5_4_more_things_descr">- 可选择通知已删除的联系人。 \n- 带空格的个人资料名称。 @@ -1521,4 +1484,895 @@ <string name="found_desktop">找到了桌面</string> <string name="not_compatible">不兼容!</string> <string name="multicast_discoverable_via_local_network">可通过本地网络发现</string> -</resources> \ No newline at end of file + <string name="refresh_qr_code">刷新</string> + <string name="create_chat_profile">创建聊天资料</string> + <string name="no_connected_mobile">没有已连接的移动设备</string> + <string name="disconnect_remote_hosts">断开移动设备连接</string> + <string name="random_port">随机</string> + <string name="open_port_in_firewall_desc">要允许移动应用连接到桌面,请在防火墙中打开此端口,如果你已启用了它</string> + <string name="open_port_in_firewall_title">在防火墙中打开端口</string> + <string name="app_was_crashed">查看崩溃</string> + <string name="moderated_items_description">%2$s 审核了 %1$d 条消息</string> + <string name="error_showing_content">显示内容出错</string> + <string name="error_showing_message">显示消息出错</string> + <string name="you_can_make_address_visible_via_settings">你可以通过设置让它对你的 SimpleX 联系人可见。</string> + <string name="recent_history_is_not_sent_to_new_members">未发送历史消息给新成员。</string> + <string name="retry_verb">重试</string> + <string name="camera_not_available">相机不可用</string> + <string name="enable_sending_recent_history">给新成员发送最多 100 条历史消息。</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>添加联系人</b>: 来创建新的邀请链接,或通过你收到的链接进行连接。]]></string> + <string name="disable_sending_recent_history">不给新成员发送历史消息。</string> + <string name="or_show_this_qr_code">或者显示此码</string> + <string name="recent_history_is_sent_to_new_members">给新成员发送了最多 100 条历史消息。</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">你扫描的码不是 SimpleX 链接的二维码。</string> + <string name="the_text_you_pasted_is_not_a_link">你粘贴的文本不是 SimpleX 链接。</string> + <string name="enable_camera_access">启用相机访问</string> + <string name="you_can_view_invitation_link_again">你可以在连接详情中再次查看邀请链接。</string> + <string name="keep_unused_invitation_question">保留未使用的邀请吗?</string> + <string name="share_this_1_time_link">分享此一次性邀请链接</string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>建群</b>: 来建立新群。]]></string> + <string name="recent_history">可见的历史</string> + <string name="la_app_passcode">应用程序密码</string> + <string name="new_chat">新聊天</string> + <string name="loading_chats">加载聊天中…</string> + <string name="creating_link">创建链接中…</string> + <string name="or_scan_qr_code">或者扫描二维码</string> + <string name="invalid_qr_code">无效的二维码</string> + <string name="add_contact_tab">添加联系人</string> + <string name="tap_to_scan">轻按扫描</string> + <string name="keep_invitation_link">保留</string> + <string name="tap_to_paste_link">轻按粘贴链接</string> + <string name="search_or_paste_simplex_link">搜索或粘贴 SimpleX 链接</string> + <string name="chat_is_stopped_you_should_transfer_database">聊天已停止。如果你已经在另一台设备商使用过此数据库,你应该在启动聊天前将数据库传输回来。</string> + <string name="start_chat_question">启动聊天吗?</string> + <string name="agent_internal_error_title">内部错误</string> + <string name="agent_internal_error_desc">请将它报告给开发者: +\n%s</string> + <string name="agent_critical_error_desc">请将它报告给开发者: +\n%s +\n +\n建议重启应用。</string> + <string name="agent_critical_error_title">严重错误</string> + <string name="restart_chat_button">重启聊天</string> + <string name="show_internal_errors">显示内部错误</string> + <string name="remote_host_disconnected_from"><![CDATA[和移动主机<b>%s</b>的连接断开,原因是:%s]]></string> + <string name="remote_ctrl_disconnected_with_reason">断连原因:%s</string> + <string name="remote_ctrl_error_timeout">连接到桌面时超时</string> + <string name="remote_ctrl_error_busy">桌面繁忙</string> + <string name="remote_ctrl_error_bad_invitation">桌面邀请码错误</string> + <string name="remote_ctrl_error_inactive">桌面不活跃</string> + <string name="remote_ctrl_error_bad_state">到桌面的连接处于糟糕状态</string> + <string name="remote_ctrl_error_bad_version">已安装的桌面版本不受支持。请确保两台设备所安装版本形同</string> + <string name="remote_ctrl_error_disconnected">桌面连接已断开</string> + <string name="failed_to_create_user_invalid_title">无效的显示名!</string> + <string name="remote_ctrl_was_disconnected_title">连接停止</string> + <string name="remote_host_error_bad_version"><![CDATA[移动主机 <b>%s</b> 有一个不受支持的版本。请确保两台设备安装的版本相同]]></string> + <string name="remote_host_error_busy"><![CDATA[移动主机 <b>%s</b> 繁忙]]></string> + <string name="remote_host_error_inactive"><![CDATA[移动主机 <b>%s</b>不活跃]]></string> + <string name="remote_host_error_missing"><![CDATA[移动主机 <b>%s</b> 缺失]]></string> + <string name="remote_host_error_disconnected"><![CDATA[移动主机<b>%s</b>连接断开]]></string> + <string name="remote_host_was_disconnected_title">连接停止</string> + <string name="remote_host_error_bad_state"><![CDATA[到移动主机 <b>%s</b>的连接状态不佳]]></string> + <string name="remote_host_error_timeout"><![CDATA[连接到移动主机<b>%s</b>时超时]]></string> + <string name="failed_to_create_user_invalid_desc">显示名无效。请另选一个名称。</string> + <string name="possible_slow_function_title">慢函数</string> + <string name="show_slow_api_calls">显示缓慢的 API 调用</string> + <string name="past_member_vName">过往成员 %1$s</string> + <string name="group_member_status_unknown_short">未知</string> + <string name="group_member_status_unknown">未知状态</string> + <string name="developer_options_section">开发者选项</string> + <string name="possible_slow_function_desc">函数执行花费的时间过久:%1$d 秒: %2$s</string> + <string name="v5_5_message_delivery">改进了消息传递</string> + <string name="v5_5_join_group_conversation">加入群对话</string> + <string name="v5_5_simpler_connect_ui">粘贴链接以连接!</string> + <string name="v5_5_simpler_connect_ui_descr">搜索栏接受邀请链接。</string> + <string name="v5_5_private_notes_descr">加密的文件和媒体。</string> + <string name="v5_5_message_delivery_descr">降低了电量使用。</string> + <string name="clear_note_folder_warning">所有消息都将被删除 - 这无法被撤销!</string> + <string name="info_row_created_at">创建于</string> + <string name="share_text_created_at">创建于:%s</string> + <string name="v5_5_new_interface_languages">匈牙利语和土耳其语用户界面</string> + <string name="v5_5_private_notes">私密笔记</string> + <string name="v5_5_join_group_conversation_descr">最近历史和改进的目录机器人。</string> + <string name="profile_update_event_contact_name_changed">联系人姓名从 %1$s 改为了 %2$s</string> + <string name="profile_update_event_member_name_changed">成员姓名从 %1$s 改为了 %2$s</string> + <string name="profile_update_event_removed_address">删除了联系地址</string> + <string name="profile_update_event_removed_picture">删除了资料图片</string> + <string name="profile_update_event_set_new_address">设置新的联系地址</string> + <string name="profile_update_event_set_new_picture">设置新的资料图片</string> + <string name="profile_update_event_updated_profile">更新了资料</string> + <string name="error_creating_message">创建消息出错</string> + <string name="error_deleting_note_folder">删除私密笔记出错</string> + <string name="note_folder_local_display_name">私密笔记</string> + <string name="clear_note_folder_question">清除私密笔记?</string> + <string name="saved_message_title">已保存的消息</string> + <string name="blocked_by_admin_items_description">管理员封禁了 %d 条消息</string> + <string name="rcv_group_event_member_unblocked">已解封 %s</string> + <string name="block_for_all">为所有人封禁</string> + <string name="unblock_for_all">为所有人解封</string> + <string name="unblock_for_all_question">为所有其他成员解封该成员?</string> + <string name="member_blocked_by_admin">由管理员封禁</string> + <string name="blocked_by_admin_item_description">由管理员封禁</string> + <string name="rcv_group_event_member_blocked">已封禁 %s</string> + <string name="block_for_all_question">为所有其他成员封禁该成员?</string> + <string name="error_blocking_member_for_all">执行为所有人封禁时出错</string> + <string name="member_info_member_blocked">已封禁</string> + <string name="snd_group_event_member_blocked">你封禁了 %s</string> + <string name="snd_group_event_member_unblocked">你解封了 %s</string> + <string name="welcome_message_is_too_long">欢迎消息太大了</string> + <string name="database_migration_in_progress">正在进行数据库迁移。 +\n可能需要几分钟时间。</string> + <string name="message_too_large">消息太大了</string> + <string name="call_service_notification_video_call">视频通话</string> + <string name="call_service_notification_audio_call">语音通话</string> + <string name="call_service_notification_end_call">结束通话</string> + <string name="unable_to_open_browser_title">打开浏览器出错</string> + <string name="unable_to_open_browser_desc">没有默认网络浏览器无法使用通话功能。请在系统中配置默认浏览器并和开发者分享更多信息。</string> + <string name="e2ee_info_pq_short">此聊天受抗量子的端到端加密保护。</string> + <string name="e2ee_info_no_pq_short">此聊天受端到端加密保护。</string> + <string name="auth_open_migration_to_another_device">打开迁移屏幕</string> + <string name="migrate_from_another_device">从另一台设备迁移</string> + <string name="set_passphrase">设置密码短语</string> + <string name="conn_event_disabled_pq">标准端到端加密</string> + <string name="migrate_from_device_archive_and_upload">存档和上传</string> + <string name="migrate_from_device_verify_database_passphrase">验证数据库密码短语</string> + <string name="v5_6_quantum_resistant_encryption">抗量子加密</string> + <string name="v5_6_app_data_migration">应用数据迁移</string> + <string name="v5_6_app_data_migration_descr">通过二维码迁移到另一部设备。</string> + <string name="v5_6_picture_in_picture_calls">画中画通话</string> + <string name="v5_6_safer_groups">更安全的群</string> + <string name="v5_6_picture_in_picture_calls_descr">通话时使用本应用</string> + <string name="migrate_to_device_title">迁移到此处</string> + <string name="or_paste_archive_link">或粘贴存档链接</string> + <string name="migrate_to_device_downloading_archive">正在下载存档</string> + <string name="invalid_file_link">无效链接</string> + <string name="migrate_to_device_migrating">迁移中</string> + <string name="paste_archive_link">粘贴存档链接</string> + <string name="migrate_to_device_database_init">正在准备下载</string> + <string name="migrate_to_device_download_failed">下载失败了</string> + <string name="migrate_to_device_repeat_download">重复下载</string> + <string name="migrate_to_device_bytes_downloaded">%s 已下载</string> + <string name="migrate_to_device_enter_passphrase">输入密码短语</string> + <string name="migrate_to_device_import_failed">导入失败了</string> + <string name="migrate_to_device_importing_archive">正在导入存档</string> + <string name="migrate_to_device_repeat_import">重复导入</string> + <string name="migrate_to_device_chat_migrated">已迁移聊天!</string> + <string name="migrate_to_device_error_downloading_archive">下载存档出错</string> + <string name="migrate_to_device_file_delete_or_link_invalid">文件被删除或链接无效</string> + <string name="migrate_to_device_finalize_migration">在另一部设备上完成迁移</string> + <string name="migrate_to_device_confirm_network_settings_footer">请确认网络设置对此这台设备正确无误。</string> + <string name="migrate_to_device_apply_onion">应用</string> + <string name="migrate_to_device_confirm_network_settings">确认网络设置</string> + <string name="migrate_from_device_error_exporting_archive">导出聊天数据库出错</string> + <string name="migrate_from_device_exported_file_doesnt_exist">导出的文件不存在</string> + <string name="migrate_from_device_to_another_device">迁移到另一部设备</string> + <string name="migrate_from_device_database_init">正在准备上传</string> + <string name="migrate_from_device_all_data_will_be_uploaded">你的所有联系人、对话和文件将被安全加密并分块上传到配置的 XFTP 中继。</string> + <string name="migrate_from_device_archiving_database">正在存档数据库</string> + <string name="migrate_from_device_confirm_upload">确认上传</string> + <string name="migrate_from_device_error_deleting_database">删除数据库出错</string> + <string name="migrate_from_device_chat_should_be_stopped">必须停止聊天才能继续。</string> + <string name="migrate_from_device_stopping_chat">正在停止聊天</string> + <string name="migrate_from_device_repeat_upload">重复上传</string> + <string name="migrate_from_device_bytes_uploaded">%s 已上传</string> + <string name="migrate_from_device_upload_failed">上传失败了</string> + <string name="migrate_from_device_uploading_archive">正在上传存档</string> + <string name="migrate_from_device_cancel_migration">取消迁移</string> + <string name="migrate_from_device_creating_archive_link">正在创建存档链接</string> + <string name="migrate_from_device_finalize_migration">完成迁移</string> + <string name="migrate_from_device_delete_database_from_device">从这部设备上删除数据库</string> + <string name="migrate_from_device_or_share_this_file_link">或安全地分享此文件链接</string> + <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">警告:不支持在多部设备上启动聊天,这么做会导致消息传送失败。</string> + <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[你 <b>不能</b> 在两部设备上使用同一数据库。]]></string> + <string name="migrate_from_device_migration_complete">迁移完毕</string> + <string name="migrate_from_device_start_chat">启动聊天</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">请在迁移前确认你记得数据库的密码短语。</string> + <string name="migrate_from_device_verify_passphrase">验证密码短语</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>警告</b>:该存档将被删除。]]></string> + <string name="migrate_from_device_check_connection_and_try_again">检查你的互联网连接并重试</string> + <string name="migrate_from_device_error_verifying_passphrase">验证密码短语出错:</string> + <string name="e2ee_info_no_pq"><![CDATA[消息、文件和通话受带完全前向保密、可否认性和试验性恢复的<b>端到端加密</b>保护。]]></string> + <string name="migrate_from_device_title">迁移设备</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[在新设备上选择 <i>从一部设备迁移</i> 并扫描二维码。]]></string> + <string name="e2ee_info_pq"><![CDATA[消息、文件和通话受带完全前向保密、可否认性和试验性恢复的<b>抗量子端到端加密</b>保护。]]></string> + <string name="v5_6_safer_groups_descr">管理员可以为所有人封禁一名成员。</string> + <string name="migrate_to_device_downloading_details">正在下载链接详情</string> + <string name="v5_6_quantum_resistant_encryption_descr">在私聊中开启(公测)!</string> + <string name="migrate_from_device_error_saving_settings">保存设置出错</string> + <string name="migrate_from_device_error_uploading_archive">上传存档出错</string> + <string name="conn_event_enabled_pq">抗量子端到端加密</string> + <string name="migrate_to_device_try_again">你可以再试一次。</string> + <string name="migrate_from_device_try_again">你可以再试一次。</string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>请注意</b>: 作为安全保护措施,在两部设备上使用同一数据库会破坏解密来自你联系人的消息。]]></string> + <string name="error_showing_desktop_notification">显示通知出错,请联系开发者。</string> + <string name="permissions_find_in_settings_and_grant">在 Android 系统设置中找到此权限并手动授予权限。</string> + <string name="permissions_grant_in_settings">在系统设置中授予</string> + <string name="permissions_grant">要进行通话请授予一项或多项权限</string> + <string name="permissions_camera">相机</string> + <string name="permissions_record_audio">麦克风</string> + <string name="permissions_camera_and_record_audio">相机和麦克风</string> + <string name="permissions_open_settings">打开设置</string> + <string name="permissions_required">授予权限</string> + <string name="settings_section_title_network_connection">网络连接</string> + <string name="network_type_network_wifi">WiFi</string> + <string name="network_type_ethernet">有线以太网</string> + <string name="network_type_cellular">移动网络</string> + <string name="network_type_no_network_connection">无网络连接</string> + <string name="network_type_other">其他</string> + <string name="forwarded_description">已转发</string> + <string name="simplex_links_not_allowed">不允许SimpleX 链接</string> + <string name="files_and_media_not_allowed">不允许文件和媒体</string> + <string name="voice_messages_not_allowed">不允许语音消息</string> + <string name="simplex_links">SimpleX 链接</string> + <string name="allow_to_send_simplex_links">允许发送 SimpleX 链接。</string> + <string name="group_members_can_send_simplex_links">成员可发送 SimpleX 链接。</string> + <string name="prohibit_sending_simplex_links">禁止发送 SimpleX 链接</string> + <string name="simplex_links_are_prohibited_in_group">SimpleX 链接被禁止。</string> + <string name="feature_roles_owners">所有者</string> + <string name="feature_enabled_for">启用对象</string> + <string name="feature_roles_admins">管理员</string> + <string name="feature_roles_all_members">所有成员</string> + <string name="download_file">下载</string> + <string name="forward_message">转发消息…</string> + <string name="forward_chat_item">转发</string> + <string name="recipients_can_not_see_who_message_from">收件人看不到这条消息来自何人。</string> + <string name="saved_description">已保存</string> + <string name="saved_chat_item_info_tab">已保存</string> + <string name="saved_from_chat_item_info_title">保存自</string> + <string name="saved_from_description">保存自%s</string> + <string name="forwarded_chat_item_info_tab">已转发</string> + <string name="forwarded_from_chat_item_info_title">转发自</string> + <string name="audio_device_bluetooth">蓝牙</string> + <string name="network_option_rcv_concurrency">并行接收</string> + <string name="audio_device_earpiece">耳机</string> + <string name="audio_device_wired_headphones">头戴式耳机</string> + <string name="audio_device_speaker">扬声器</string> + <string name="v5_7_call_sounds">通话声音</string> + <string name="v5_7_forward_descr">消息来源保持私密。</string> + <string name="v5_7_network_descr">更可靠的网络连接。</string> + <string name="v5_7_network">网络管理</string> + <string name="v5_7_forward">转发并保存消息</string> + <string name="v5_7_new_interface_languages">立陶宛语用户界面</string> + <string name="v5_7_call_sounds_descr">连接音频和视频通话时。</string> + <string name="v5_7_quantum_resistant_encryption_descr">将在私聊中启用!</string> + <string name="settings_section_title_profile_images">个人资料图</string> + <string name="v5_7_shape_profile_images">改变个人资料图形状</string> + <string name="v5_7_shape_profile_images_descr">方形、圆形、或两者之间的任意形状</string> + <string name="snd_error_quota">超出了额度 — 收信人没收到之前发送的消息。</string> + <string name="snd_error_relay">目标服务器错误:%1$s</string> + <string name="ci_status_other_error">错误:%1$s</string> + <string name="snd_error_proxy">转发服务器:%1$s +\n错误:%2$s</string> + <string name="message_delivery_warning_title">消息传输警告</string> + <string name="snd_error_expired">网络问题 — 许多发送消息的尝试后,消息过期了。</string> + <string name="snd_error_auth">密钥错误或连接未知 — 连接被删除的可能性最大。</string> + <string name="network_smp_proxy_mode_always">始终</string> + <string name="network_smp_proxy_mode_never">从不</string> + <string name="network_smp_proxy_mode_unknown">未知服务器</string> + <string name="network_smp_proxy_mode_always_description">始终使用私密路由。</string> + <string name="network_smp_proxy_mode_never_description">不使用私密路由。</string> + <string name="network_smp_proxy_mode_unknown_description">在未知服务器上使用私密路由。</string> + <string name="network_smp_proxy_mode_unprotected_description">当 IP 地址不受保护时,在未知服务器上使用私密路由。</string> + <string name="network_smp_proxy_fallback_allow_protected">当 IP 隐藏时</string> + <string name="network_smp_proxy_fallback_allow">是</string> + <string name="network_smp_proxy_fallback_prohibit">不</string> + <string name="network_smp_proxy_fallback_allow_description">当你的服务器或目标服务器不支持私密路由时直接发送消息。</string> + <string name="update_network_smp_proxy_fallback_question">备用消息路由</string> + <string name="private_routing_show_message_status">显示消息状态</string> + <string name="private_routing_explanation">为了保护你的 IP 地址,私密路由使用你的 SMP 服务器来传送消息。</string> + <string name="settings_section_title_private_message_routing">私密消息路由</string> + <string name="network_smp_proxy_mode_private_routing">私密路由</string> + <string name="network_smp_proxy_fallback_allow_protected_description">当 IP 地址受保护且你的服务器或目标服务器不支持私密路由时,直接发送消息。</string> + <string name="srv_error_host">服务器地址和网络设置不兼容。</string> + <string name="network_smp_proxy_fallback_allow_downgrade">允许降级</string> + <string name="srv_error_version">服务器版本和网络设置不兼容。</string> + <string name="network_smp_proxy_mode_unprotected">未受保护</string> + <string name="network_smp_proxy_fallback_prohibit_description">不直接发送消息,即便你的服务器或目标服务器不支持私密路由。</string> + <string name="snd_error_proxy_relay">转发服务器:%1$s +\n目标服务器错误:%2$s</string> + <string name="update_network_smp_proxy_mode_question">消息路由模式</string> + <string name="file_not_approved_title">未知服务器!</string> + <string name="file_not_approved_descr">没有 Tor 或 VPN,这些 XFTP 中继可以看到你的 IP 地址: +\n%1$s.</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">没有 Tor 或 VPN,文件服务器可以看到你的 IP 地址。</string> + <string name="protect_ip_address">保护 IP 地址</string> + <string name="settings_section_title_files">文件</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">应用将请求确认来自未知服务器的下载(.onion 或启用 SOCKS 代理时除外)。</string> + <string name="settings_section_title_user_theme">个人资料主题</string> + <string name="chat_list_always_visible">在新窗口中显示聊天列表</string> + <string name="chat_theme_apply_to_all_modes">所有颜色模式</string> + <string name="chat_theme_apply_to_mode">应用到</string> + <string name="theme_black">黑</string> + <string name="color_mode">颜色模式</string> + <string name="color_mode_dark">深色</string> + <string name="chat_theme_apply_to_dark_mode">深色模式</string> + <string name="dark_mode_colors">深色模式颜色</string> + <string name="wallpaper_scale_fill">填充</string> + <string name="wallpaper_scale_fit">适配</string> + <string name="wallpaper_preview_hello_alice">下午好!</string> + <string name="wallpaper_preview_hello_bob">早上好!</string> + <string name="color_mode_light">浅色</string> + <string name="chat_theme_apply_to_light_mode">浅色模式</string> + <string name="color_received_quote">收到的回复</string> + <string name="theme_remove_image">删除图片</string> + <string name="color_sent_quote">已发送回复</string> + <string name="chat_theme_set_default_theme">设置默认主题</string> + <string name="color_mode_system">系统</string> + <string name="color_wallpaper_tint">壁纸强调色</string> + <string name="color_wallpaper_background">壁纸背景色</string> + <string name="color_primary_variant2">额外的强调色2</string> + <string name="wallpaper_advanced_settings">高级设置</string> + <string name="settings_section_title_chat_colors">聊天颜色</string> + <string name="wallpaper_scale_repeat">重复</string> + <string name="settings_section_title_chat_theme">聊天主题</string> + <string name="reset_single_color">重置颜色</string> + <string name="wallpaper_scale">缩放</string> + <string name="error_initializing_web_view">Webview 初始化失败。更新你的系统到新版本。请联系开发者。 +\n错误:%s</string> + <string name="v5_8_private_routing_descr">保护你的真实 IP 地址。不让你的联系人选择的消息中继看到它。 \n在*网络&服务器*设置中开启。</string> + <string name="v5_8_safe_files_descr">确认来自未知服务器的文件。</string> + <string name="v5_8_safe_files">安全地接收文件</string> + <string name="v5_8_message_delivery">改进了消息传递</string> + <string name="v5_8_chat_themes_descr">让你的聊天看上去不同!</string> + <string name="v5_8_private_routing">私密消息路由🚀</string> + <string name="v5_8_chat_themes">新的聊天主题</string> + <string name="v5_8_persian_ui">波斯语用户界面</string> + <string name="v5_8_message_delivery_descr">降低电池用量</string> + <string name="theme_destination_app_theme">主题</string> + <string name="chat_theme_reset_to_app_theme">重置为应用主题</string> + <string name="chat_theme_reset_to_user_theme">重置为用户主题</string> + <string name="info_row_debug_delivery">发送调试</string> + <string name="message_queue_info">消息队列信息</string> + <string name="message_queue_info_server_info">消息队列信息:%1$s +\n +\n上一则收到的信息:%2$s</string> + <string name="message_queue_info_none">无</string> + <string name="file_error_no_file">未找到文件 - 最有可能的情况是文件被删或被取消了</string> + <string name="file_error_auth">错误的密钥或未知的文件块地址 - 最可能的情况是文件被删了。</string> + <string name="file_error">文件错误</string> + <string name="file_error_relay">文件服务器错误:%1$s</string> + <string name="info_row_file_status">文件状态</string> + <string name="info_row_message_status">消息状态</string> + <string name="temporary_file_error">临时性文件错误</string> + <string name="share_text_file_status">文件状态:%s</string> + <string name="share_text_message_status">消息状态:%s</string> + <string name="remote_ctrl_connection_stopped_desc">请检查移动设备和桌面设备连接到的是同一个本地网络,且桌面防火墙允许连接。 +\n请和开发者分享任何其他问题。</string> + <string name="remote_ctrl_connection_stopped_identity_desc">此链接用于另一台移动设备,请在桌面上创建新的链接。</string> + <string name="copy_error">复制错误</string> + <string name="cannot_share_message_alert_title">无法发送消息</string> + <string name="cannot_share_message_alert_text">选择的聊天首选项禁止此条消息。</string> + <string name="please_try_later">请稍后尝试。</string> + <string name="private_routing_error">私密路由出错</string> + <string name="message_forwarded_title">已转发的消息</string> + <string name="message_forwarded_desc">尚无直接连接,消息由管理员转发。</string> + <string name="smp_servers_other">其他 SMP 服务器</string> + <string name="xftp_servers_other">其他 XFTP 服务器</string> + <string name="scan_paste_link">扫描/粘贴链接</string> + <string name="subscription_percentage">显示百分比</string> + <string name="member_info_member_inactive">不活跃</string> + <string name="appearance_zoom">缩放</string> + <string name="all_users">所有配置文件</string> + <string name="servers_info_files_tab">文件</string> + <string name="servers_info_missing">没有信息,试试重新加载</string> + <string name="servers_info">服务器信息</string> + <string name="attempts_label">尝试</string> + <string name="servers_info_sessions_connected">已连接</string> + <string name="servers_info_connected_servers_section_header">已连接的服务器</string> + <string name="servers_info_sessions_connecting">连接中</string> + <string name="servers_info_subscriptions_connections_subscribed">活跃连接</string> + <string name="servers_info_detailed_statistics">详细统计数据</string> + <string name="servers_info_details">详情</string> + <string name="servers_info_downloaded">已下载</string> + <string name="servers_info_modal_error_title">错误</string> + <string name="servers_info_reconnect_server_error">重连服务器出错</string> + <string name="servers_info_reconnect_servers_error">重连服务器出错</string> + <string name="servers_info_reset_stats_alert_error_title">重设统计数据出错</string> + <string name="servers_info_sessions_errors">错误</string> + <string name="servers_info_messages_received">收到的消息</string> + <string name="servers_info_subscriptions_section_header">消息接收</string> + <string name="servers_info_subscriptions_connections_pending">待连接</string> + <string name="servers_info_previously_connected_servers_section_header">先前连接的服务器</string> + <string name="servers_info_proxied_servers_section_header">已代理的服务器</string> + <string name="servers_info_detailed_statistics_received_messages_header">接收到的消息</string> + <string name="servers_info_detailed_statistics_received_total">接收总计</string> + <string name="servers_info_detailed_statistics_receive_errors">接收错误</string> + <string name="reconnect">重连</string> + <string name="servers_info_reconnect_server_title">重连服务器?</string> + <string name="servers_info_reconnect_servers_title">重连服务器?</string> + <string name="servers_info_reconnect_server_message">重连服务器强制消息传输。这会使用额外流量。</string> + <string name="servers_info_reset_stats">重置所有统计数据</string> + <string name="servers_info_reset_stats_alert_title">重置所有统计数据吗?</string> + <string name="sent_directly">直接发送</string> + <string name="servers_info_detailed_statistics_sent_messages_header">已发送消息</string> + <string name="servers_info_detailed_statistics_sent_messages_total">发送总计</string> + <string name="sent_via_proxy">通过代理发送</string> + <string name="servers_info_reset_stats_alert_message">服务器统计数据将被重置。此操作无法撤销!</string> + <string name="xftp_server">XFTP 服务器</string> + <string name="acknowledgement_errors">认可出错</string> + <string name="chunks_deleted">块已删除</string> + <string name="chunks_downloaded">块已下载</string> + <string name="completed">已完毕</string> + <string name="connections">连接数</string> + <string name="created">已创建</string> + <string name="decryption_errors">解密出错</string> + <string name="deleted">已删除</string> + <string name="deletion_errors">删除错误</string> + <string name="downloaded_files">已下载的文件</string> + <string name="download_errors">下载出错</string> + <string name="duplicates_label">重复</string> + <string name="expired_label">已过期</string> + <string name="other_label">其他</string> + <string name="other_errors">其他错误</string> + <string name="proxied">已代理</string> + <string name="secured">已受保护</string> + <string name="send_errors">发送错误</string> + <string name="server_address">服务器地址</string> + <string name="size">大小</string> + <string name="uploaded_files">已上传的文件</string> + <string name="upload_errors">上传出错</string> + <string name="servers_info_reconnect_servers_message">重新连接所有已连接的服务器来强制消息传输。这会使用额外流量。</string> + <string name="servers_info_proxied_servers_section_footer">你没有连接到这些服务器。私密路由被用于向它们传输消息。</string> + <string name="servers_info_reconnect_all_servers_button">重连所有服务器</string> + <string name="servers_info_reset_stats_alert_confirm">重置</string> + <string name="network_error_broker_host_desc">服务器地址不兼容网络设置:%1$s。</string> + <string name="servers_info_starting_from">起始自 %s。</string> + <string name="servers_info_private_data_disclaimer">自 %s 起 \n所有数据均私密地保存在你的设备上.</string> + <string name="subscribed">已订阅</string> + <string name="acknowledged">已认可</string> + <string name="network_error_broker_version_desc">服务器版本不兼容你的应用:%1$s.</string> + <string name="servers_info_target">信息主体</string> + <string name="smp_server">SMP 服务器</string> + <string name="servers_info_statistics_section_header">统计数据</string> + <string name="subscription_errors">订阅错误</string> + <string name="servers_info_subscriptions_total">总计</string> + <string name="chunks_uploaded">块已上传</string> + <string name="subscription_results_ignored">订阅被忽略</string> + <string name="smp_servers_configured">已配置的 SMP 服务器</string> + <string name="xftp_servers_configured">已配置的 XFTP 服务器</string> + <string name="current_user">当前配置文件</string> + <string name="servers_info_transport_sessions_section_header">传输会话</string> + <string name="servers_info_uploaded">已上传</string> + <string name="member_info_member_disabled">已停用</string> + <string name="appearance_font_size">字体大小</string> + <string name="member_inactive_title">成员不活跃</string> + <string name="member_inactive_desc">如果成员变得活跃,可能会在之后传输消息。</string> + <string name="servers_info_messages_sent">发送的消息</string> + <string name="open_server_settings_button">打开服务器设置</string> + <string name="app_check_for_updates">检查更新</string> + <string name="app_check_for_updates_notice_title">检查更新</string> + <string name="app_check_for_updates_notice_disable">停用</string> + <string name="app_check_for_updates_disabled">已停用</string> + <string name="app_check_for_updates_download_started">正在下载应用更新,不要关闭应用</string> + <string name="app_check_for_updates_button_download">下载 %s(%s)</string> + <string name="app_check_for_updates_button_open">打开文件位置</string> + <string name="app_check_for_updates_installed_successfully_desc">请重启应用。</string> + <string name="app_check_for_updates_button_remind_later">稍后提醒</string> + <string name="app_check_for_updates_button_skip">跳过此版本</string> + <string name="app_check_for_updates_stable">稳定版</string> + <string name="app_check_for_updates_update_available">有更新可用:%s</string> + <string name="app_check_for_updates_canceled">取消了更新下载</string> + <string name="app_check_for_updates_notice_desc">要接收新版本通知,请打开“定期检查稳定或测试版本”。</string> + <string name="app_check_for_updates_download_completed_title">应用更新已下载</string> + <string name="app_check_for_updates_beta">测试版</string> + <string name="app_check_for_updates_installed_successfully_title">安装成功</string> + <string name="app_check_for_updates_button_install">安装更新</string> + <string name="proxy_destination_error_broker_version">%1$s 的目的地服务器版本不兼容转发服务器 %2$s.</string> + <string name="proxy_destination_error_failed_to_connect">转发服务器 %1$s 连接目的地服务器 %2$s 失败。请稍后尝试。</string> + <string name="smp_proxy_error_broker_host">转发服务器地址不兼容网络设置:%1$s。</string> + <string name="smp_proxy_error_broker_version">转发服务器版本不兼容网络设置:%1$s。</string> + <string name="proxy_destination_error_broker_host">%1$s 的目的地服务器地址不兼容转发服务器 %2$s 的设置。</string> + <string name="smp_proxy_error_connecting">连接转发服务器 %1$s 出错。请稍后尝试。</string> + <string name="privacy_media_blur_radius">模糊媒体文件</string> + <string name="privacy_media_blur_radius_medium">中度</string> + <string name="privacy_media_blur_radius_off">关闭</string> + <string name="privacy_media_blur_radius_soft">轻柔</string> + <string name="privacy_media_blur_radius_strong">强烈</string> + <string name="info_view_message_button">消息</string> + <string name="info_view_call_button">呼叫</string> + <string name="delete_contact_cannot_undo_warning">联系人将被删除 - 无法撤销此操作!</string> + <string name="keep_conversation">保留对话</string> + <string name="only_delete_conversation">只删除对话</string> + <string name="contact_deleted">删除了联系人!</string> + <string name="conversation_deleted">删除了对话!</string> + <string name="delete_without_notification">不通知删除</string> + <string name="you_can_still_view_conversation_with_contact">你仍可以在聊天列表中查看与 %1$s 的对话。</string> + <string name="paste_link">粘贴链接</string> + <string name="contact_list_header_title">联系人</string> + <string name="one_hand_ui">单手应用工具栏</string> + <string name="cant_call_contact_connecting_wait_alert_text">正在连接联系人,请等候或稍后检查!</string> + <string name="cant_call_contact_deleted_alert_text">联系人被删除了。</string> + <string name="you_need_to_allow_calls">要能够呼叫联系人,你需要先允许联系人进行呼叫。</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">请让你的联系人启用通话。</string> + <string name="cant_call_contact_alert_title">无法呼叫联系人</string> + <string name="cant_call_member_alert_title">无法呼叫群成员</string> + <string name="cant_send_message_to_member_alert_title">无法给群成员发消息</string> + <string name="confirm_delete_contact_question">确认删除联系人?</string> + <string name="info_view_connect_button">连接</string> + <string name="action_button_add_members">邀请</string> + <string name="no_filtered_contacts">没有过滤的联系人</string> + <string name="info_view_open_button">打开</string> + <string name="info_view_search_button">搜索</string> + <string name="cant_call_member_send_message_alert_text">发送消息来开启通话。</string> + <string name="you_can_still_send_messages_to_contact">你可以发消息给来自已存档联系人的 %1$s。</string> + <string name="deleted_chats">已存档的联系人</string> + <string name="allow_calls_question">允许通话?</string> + <string name="calls_prohibited_alert_title">通话被禁止!</string> + <string name="toolbar_settings">设置</string> + <string name="info_view_video_button">视频</string> + <string name="delete_messages_mark_deleted_warning">消息将被标记为删除。收信人将可以揭示这些消息。</string> + <string name="delete_members_messages__question">删除成员的 %d 条消息吗?</string> + <string name="select_verb">选择</string> + <string name="compose_message_placeholder">写消息</string> + <string name="selected_chat_items_nothing_selected">什么也没选中</string> + <string name="selected_chat_items_selected_n">已选中 %d</string> + <string name="moderate_messages_will_be_deleted_warning">将对所有成员删除这些消息。</string> + <string name="moderate_messages_will_be_marked_warning">这些消息将对所有成员标记为受管制。</string> + <string name="v6_0_connect_faster_descr">更快地连接到你的好友</string> + <string name="v6_0_connection_servers_status_descr">连接和服务器状态。</string> + <string name="v6_0_delete_many_messages_descr">最多同时删除 20 条消息</string> + <string name="v6_0_private_routing_descr">它保护你的 IP 地址和连接。</string> + <string name="one_hand_ui_card_title">切换聊天列表:</string> + <string name="one_hand_ui_change_instruction">你可以在“外观”设置中更改它。</string> + <string name="network_options_save_and_reconnect">保存并重新连接</string> + <string name="network_option_tcp_connection">TCP 连接</string> + <string name="v6_0_connection_servers_status">控制你的网络</string> + <string name="v6_0_reachable_chat_toolbar">单手聊天工具栏</string> + <string name="v6_0_your_contacts_descr">存档之后要聊天的联系人。</string> + <string name="v6_0_reachable_chat_toolbar_descr">用一只手使用本应用。</string> + <string name="chat_database_exported_title">已导出聊天数据库</string> + <string name="chat_database_exported_continue">继续</string> + <string name="media_and_file_servers">媒体和文件服务器</string> + <string name="message_servers">消息服务器</string> + <string name="network_socks_proxy">SOCKS 代理</string> + <string name="chat_database_exported_not_all_files">某些文件未导出</string> + <string name="chat_database_exported_migrate">你可以迁移导出的数据库。</string> + <string name="chat_database_exported_save">你可以保存导出的存档。</string> + <string name="reset_all_hints">重置所有提示</string> + <string name="v6_0_privacy_blur">模糊以增强隐私。</string> + <string name="v6_0_new_media_options">新的媒体选项</string> + <string name="v6_0_chat_list_media">从聊天列表播放。</string> + <string name="v6_0_upgrade_app">自动升级应用</string> + <string name="create_address_button">创建</string> + <string name="v6_0_upgrade_app_descr">从 GitHub 下载新版。</string> + <string name="v6_0_increase_font_size">增大字体尺寸。</string> + <string name="invite_friends_short">邀请</string> + <string name="v6_0_new_chat_experience">新的聊天体验 🎉</string> + <string name="new_message">新消息</string> + <string name="error_parsing_uri_desc">请检查 Simple X 链接是否正确。</string> + <string name="error_parsing_uri_title">无效链接</string> + <string name="settings_section_title_chat_database">聊天数据库</string> + <string name="system_mode_toast">系统模式</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">上传的数据库存档将永久性从服务器被删除。</string> + <string name="network_proxy_incorrect_config_desc">确保代理配置正确</string> + <string name="delete_messages_cannot_be_undone_warning">消息将被删除 - 此操作无法撤销!</string> + <string name="switching_profile_error_message">你的连接被移动到 %s,但在将你重定向到配置文件时发生了意料之外的错误。</string> + <string name="network_proxy_auth_mode_no_auth">代理不使用身份验证凭据</string> + <string name="switching_profile_error_title">切换配置文件出错</string> + <string name="network_proxy_auth">代理身份验证</string> + <string name="migrate_from_device_remove_archive_question">删除存档?</string> + <string name="select_chat_profile">选择聊天配置文件</string> + <string name="network_proxy_incorrect_config_title">保存代理出错</string> + <string name="network_proxy_password">密码</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">每个连接使用不同的代理身份验证凭据。</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">每个配置文件使用不同的代理身份验证。</string> + <string name="network_proxy_auth_mode_username_password">你的凭据可能以未经加密的方式被发送。</string> + <string name="network_proxy_random_credentials">使用随机凭据</string> + <string name="network_proxy_username">用户名</string> + <string name="new_chat_share_profile">分享配置文件</string> + <string name="error_forwarding_messages">转发消息出错</string> + <string name="forward_files_messages_deleted_after_selection_desc">在你选中消息后这些消息已被删除。</string> + <string name="n_file_errors">%1$d 个文件错误:\n%2$s</string> + <string name="n_other_file_errors">其他 %1$d 个文件错误。</string> + <string name="forward_files_not_accepted_desc">%1$d 个文件未被下载。</string> + <string name="forward_alert_title_messages_to_forward">转发 %1$s 条消息?</string> + <string name="forward_alert_forward_messages_without_files">仅转发消息不转发文件?</string> + <string name="forward_alert_title_nothing_to_forward">没什么可转发的!</string> + <string name="forward_files_in_progress_desc">仍有 %1$d 个文件在下载中。</string> + <string name="forward_files_failed_to_receive_desc">%1$d 个文件下载失败。</string> + <string name="forward_files_missing_desc">删除了 %1$d 个文件。</string> + <string name="forward_files_not_accepted_receive_files">下载</string> + <string name="forward_files_messages_deleted_after_selection_title">%1$s 条消息未被转发</string> + <string name="forward_multiple">转发消息…</string> + <string name="compose_forward_messages_n">转发 %1$s 条消息</string> + <string name="compose_save_messages_n">保存 %1$s 条消息</string> + <string name="icon_descr_sound_muted">已静音</string> + <string name="settings_section_title_message_shape">消息形状</string> + <string name="settings_message_shape_corner">拐角</string> + <string name="settings_message_shape_tail">尾部</string> + <string name="error_initializing_web_view_wrong_arch">初始化 WebView 出错。确保你安装了 WebView 且其支持的架构为 arm64。\n错误:%s</string> + <string name="network_session_mode_session">应用会话</string> + <string name="network_session_mode_session_description">每次启动应用都会使用新的 SOCKS 凭据。</string> + <string name="network_session_mode_server">服务器</string> + <string name="call_desktop_permission_denied_safari">打开 Safari 设置/网站/麦克风,接着在 localhost 选择“允许”。</string> + <string name="call_desktop_permission_denied_title">要进行通话,请允许使用设备麦克风。结束通话并尝试再次呼叫。</string> + <string name="call_desktop_permission_denied_chrome">单击地址栏附近的“信息”按钮允许使用麦克风。</string> + <string name="network_session_mode_server_description">每个服务器都会使用新的 SOCKS 凭据。</string> + <string name="v6_1_message_dates_descr">更好的消息日期。</string> + <string name="v6_1_better_security">更佳的安全性✅</string> + <string name="v6_1_better_user_experience">更佳的使用体验</string> + <string name="v6_1_customizable_message_descr">可自定义消息形状。</string> + <string name="v6_1_forward_many_messages_descr">一次转发最多20条消息。</string> + <string name="v6_1_better_security_descr">Trail of Bits 审核了 SimpleX 协议。</string> + <string name="v6_1_better_calls_descr">通话期间切换音频和视频。</string> + <string name="v6_1_switch_chat_profile_descr">对一次性邀请切换聊天配置文件。</string> + <string name="v6_1_better_calls">更佳的通话</string> + <string name="v6_1_delete_many_messages_descr">允许自行删除或管理员移除最多200条消息。</string> + <string name="failed_to_save_servers">保存服务器出错</string> + <string name="errors_in_servers_configuration">服务器配置有错误。</string> + <string name="for_chat_profile">用于聊天资料 %s:</string> + <string name="no_message_servers_configured">无消息服务器</string> + <string name="no_message_servers_configured_for_private_routing">无私密消息路由服务器。</string> + <string name="no_message_servers_configured_for_receiving">无消息接收服务器。</string> + <string name="no_media_servers_configured_for_sending">无文件发送服务器。</string> + <string name="connection_error_quota">未送达的消息</string> + <string name="message_deleted_or_not_received_error_title">无消息</string> + <string name="connection_security">连接安全性</string> + <string name="share_1_time_link_with_a_friend">和一位好友分享一次性链接</string> + <string name="share_address_publicly">公开分享地址</string> + <string name="share_simplex_address_on_social_media">在社媒上分享 SimpleX 地址。</string> + <string name="you_can_set_connection_name_to_remember">你可以设置连接名称,用来记住和谁分享了这个链接。</string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">可以通过任何消息应用安全分享 SimpleX 地址和一次性链接。</string> + <string name="create_1_time_link">创建一次性链接</string> + <string name="for_social_media">用于社交媒体</string> + <string name="or_to_share_privately">或者私下分享</string> + <string name="onboarding_choose_server_operators">服务器运营方</string> + <string name="onboarding_network_operators">网络运营方</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">30 天后将接受已启用的运营方的条款。</string> + <string name="onboarding_network_operators_continue">继续</string> + <string name="onboarding_network_operators_review_later">稍后审阅</string> + <string name="onboarding_select_network_operators_to_use">选择要使用的网络运营方。</string> + <string name="onboarding_network_operators_update">更新</string> + <string name="onboarding_network_operators_configure_via_settings">你可以通过设置配置服务器。</string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[将接受运营方的条款: <b>%s</b>.]]></string> + <string name="operator_conditions_accepted_for_enabled_operators_on">将于下列日期自动接受已启用的运营方的条款:%s。</string> + <string name="network_preset_servers_title">预设服务器</string> + <string name="your_servers">你的服务器</string> + <string name="operator_conditions_will_be_accepted_on">接受条款的将来日期为:%s。</string> + <string name="operator_info_title">网络运营方</string> + <string name="operator">运营方</string> + <string name="operator_servers_title">%s 台服务器</string> + <string name="operator_website">网站</string> + <string name="operator_conditions_failed_to_load">无法加载当前条款文本,你可以通过此链接审阅条款:</string> + <string name="use_servers_of_operator_x">使用 %s</string> + <string name="operator_use_operator_toggle_description">使用服务器</string> + <string name="operator_conditions_will_be_applied"><![CDATA[这些条款也将应用于: <b>%s</b>.]]></string> + <string name="operators_conditions_will_also_apply"><![CDATA[这些条款也将应用于: <b>%s</b>.]]></string> + <string name="view_conditions">查看条款</string> + <string name="operator_conditions_of_use">使用条款</string> + <string name="operator_use_for_messages_private_routing">用于私密路由</string> + <string name="operator_use_for_messages_receiving">消息接收</string> + <string name="operator_use_for_sending">发送</string> + <string name="operator_use_for_files">用于文件</string> + <string name="operator_use_for_messages">用于消息</string> + <string name="operator_open_changes">打开更改</string> + <string name="operator_open_conditions">打开条款</string> + <string name="operator_server_alert_title">运营方服务器</string> + <string name="server_added_to_operator__name">已添加服务器到运营方 %s</string> + <string name="error_server_operator_changed">服务器运营方已更改。</string> + <string name="error_server_protocol_changed">服务器协议已更改。</string> + <string name="appearance_in_app_bars_alpha">透明度</string> + <string name="v6_2_network_decentralization">网络去中心化</string> + <string name="v6_2_network_decentralization_descr">应用中的第二个预设运营方!</string> + <string name="v6_2_improved_chat_navigation">改进了聊天导航</string> + <string name="view_updated_conditions">查看更新后的条款</string> + <string name="onboarding_network_operators_app_will_use_for_routing">比如,如果你通过 SimpleX 服务器收到消息,应用会通过 Flux 服务器传送它们。</string> + <string name="onboarding_network_operators_app_will_use_different_operators">应用通过在每个对话中使用不同运营方保护你的隐私。</string> + <string name="accept_conditions">接受条款</string> + <string name="appearance_bars_blur_radius">模糊</string> + <string name="address_or_1_time_link">地址还是一次性链接?</string> + <string name="operator_added_message_servers">已添加消息服务器</string> + <string name="operator_added_xftp_servers">已添加媒体和文件服务器</string> + <string name="address_settings">地址设置</string> + <string name="operator_conditions_accepted">已接受的条款</string> + <string name="appearance_app_toolbars">应用工具栏</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[1-time link can be used <i>仅用于一名联系人</i> - 面对面或通过任何消息应用分享.]]></string> + <string name="operator_conditions_accepted_for_some"><![CDATA[已经接受了下列运营方的条款: <b>%s</b>.]]></string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[将接受运营方的条款:<b>%s</b>.]]></string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[相同条款将应用于运营方 <b>%s</b>.]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[相同条款将应用于运营方: <b>%s</b>.]]></string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[要使用 <b>%s</b>的服务器,请接受使用条款。]]></string> + <string name="operators_conditions_accepted_for"><![CDATA[接受了运营方的条款: <b>%s</b>.]]></string> + <string name="v6_2_network_decentralization_enable_flux">在“网络&服务器”设置中启用 Flux,更好地保护元数据隐私。</string> + <string name="error_accepting_operator_conditions">接受条款出错</string> + <string name="v6_2_network_decentralization_enable_flux_reason">为了更好的元数据隐私。</string> + <string name="error_adding_server">添加服务器出错</string> + <string name="no_media_servers_configured">无媒体和文件服务器。</string> + <string name="error_updating_server_title">更新服务器出错</string> + <string name="smp_servers_new_server">新服务器</string> + <string name="no_media_servers_configured_for_private_routing">无文件接收服务器。</string> + <string name="v6_2_improved_chat_navigation_descr">- 在第一条未读消息上打开聊天.\n- 跳转到引用的消息.</string> + <string name="operator_review_conditions">审阅条款</string> + <string name="simplex_address_or_1_time_link">SimpleX 地址或一次性链接?</string> + <string name="address_creation_instruction">要稍后创建 SimpleX 地址,请在菜单中轻按“创建 SimpleX 地址”</string> + <string name="xftp_servers_per_user">当前聊天资料的新文件服务器</string> + <string name="message_deleted_or_not_received_error_desc">此消息被删除或尚未收到。</string> + <string name="connection_error_quota_desc">连接达到了未送达消息上限,你的联系人可能处于离线状态。</string> + <string name="to_protect_against_your_link_replaced_compare_codes">为了防止链接被替换,你可以比较联系人安全代码。</string> + <string name="onboarding_network_operators_conditions_you_can_configure">你可以在“网络和服务器”设置中配置运营方。</string> + <string name="operator_conditions_accepted_on">接受运营方条款的日期:%s</string> + <string name="remote_hosts_section">远程移动设备</string> + <string name="chat_archive">或者导入存档文件</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>小米设备</b>:请在系统设置中开启“自动启动”让通知正常工作。]]></string> + <string name="maximum_message_size_title">消息太大!</string> + <string name="maximum_message_size_reached_forwarding">你可以复制并减小消息大小来发送它。</string> + <string name="maximum_message_size_reached_non_text">请减小消息大小或删除媒体并再次发送。</string> + <string name="add_your_team_members_to_conversations">将你的团队成员加入对话。</string> + <string name="business_address">企业地址</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[All messages and files are sent <b>端到端加密</b>,私信具备后量子密码安全性。]]></string> + <string name="onboarding_notifications_mode_off_desc_short">无后台服务</string> + <string name="onboarding_notifications_mode_periodic_desc_short">每 10 分钟检查消息</string> + <string name="how_it_helps_privacy">它如何帮助隐私</string> + <string name="onboarding_notifications_mode_service_desc_short">应用始终在后台运行</string> + <string name="onboarding_notifications_mode_battery">通知和电量</string> + <string name="leave_chat_question">离开聊天?</string> + <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">你将停止从这个聊天收到消息。聊天历史将被保留。</string> + <string name="invite_to_chat_button">邀请加入聊天</string> + <string name="delete_chat_for_self_cannot_undo_warning">将为你删除聊天 - 此操作无法撤销!</string> + <string name="button_delete_chat">删除聊天</string> + <string name="delete_chat_question">删除聊天?</string> + <string name="button_add_friends">添加好友</string> + <string name="button_add_team_members">添加团队成员</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">将为所有成员删除聊天 - 此操作无法撤销!</string> + <string name="only_chat_owners_can_change_prefs">仅聊天所有人可更改首选项。</string> + <string name="member_role_will_be_changed_with_notification_chat">角色将被更改为 %s。聊天中的每个人都会收到通知。</string> + <string name="direct_messages_are_prohibited">成员之间的私信被禁止。</string> + <string name="direct_messages_are_prohibited_in_chat">此聊天禁止成员之间的私信。</string> + <string name="v6_2_business_chats">企业聊天</string> + <string name="v6_2_business_chats_descr">客户隐私。</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[你已经和<b>%1$s</b>连接。]]></string> + <string name="connect_plan_chat_already_exists">聊天已存在!</string> + <string name="chat_bottom_bar">单手聊天工具栏</string> + <string name="button_leave_chat">离开聊天</string> + <string name="chat_main_profile_sent">你的聊天个人资料将被发送给聊天成员</string> + <string name="info_row_chat">聊天</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">将从聊天中删除成员 - 此操作无法撤销!</string> + <string name="maximum_message_size_reached_text">请减小消息尺寸并再次发送。</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">当启用了超过一个运营方时,没有一个运营方拥有了解谁和谁联络的元数据。</string> + <string name="display_name_accepted_invitation">已接受邀请</string> + <string name="display_name_requested_to_connect">被请求连接</string> + <string name="onboarding_network_about_operators">关于运营方</string> + <string name="onboarding_network_operators_simplex_flux_agreement">SimpleX Chat 和 Flux 达成协议将 Flux 运营的服务器包括在应用中。</string> + <string name="sync_connection_confirm">修复</string> + <string name="sync_connection_question">修复连接?</string> + <string name="encryption_renegotiation_in_progress">正进行加密重协商。</string> + <string name="sync_connection_desc">连接需要加密重协商。</string> + <string name="error_saving_database">保存数据库出错</string> + <string name="debug_logs">启用日志</string> + <string name="connection_not_ready">连接未就绪。</string> + <string name="error_creating_chat_tags">创建聊天列表出错</string> + <string name="error_updating_chat_tags">更新聊天列表出错</string> + <string name="error_loading_chat_tags">加载聊天列表出错</string> + <string name="list_menu">列表</string> + <string name="no_chats">无聊天</string> + <string name="no_chats_found">找不到聊天</string> + <string name="no_chats_in_list">%s列表中没有聊天。</string> + <string name="chat_list_favorites">收藏</string> + <string name="chat_list_groups">群</string> + <string name="list_name_field_placeholder">列表名…</string> + <string name="duplicated_list_error">所有列表的名称和表情符号都应不同。</string> + <string name="save_list">保存列表</string> + <string name="delete_chat_list_menu_action">删除</string> + <string name="open_with_app">用%s打开</string> + <string name="delete_chat_list_warning">将从%s列表删除所有聊天,列表本身也将被删除</string> + <string name="chat_list_businesses">企业</string> + <string name="edit_chat_list_menu_action">编辑</string> + <string name="add_to_list">添加到列表</string> + <string name="chat_list_contacts">联系人</string> + <string name="create_list">创建列表</string> + <string name="chat_list_add_list">添加列表</string> + <string name="chat_list_all">全部</string> + <string name="delete_chat_list_question">删除列表?</string> + <string name="no_unread_chats">没有未读聊天</string> + <string name="chat_list_notes">附注</string> + <string name="change_list">更改列表</string> + <string name="change_order_chat_list_menu_action">更改顺序</string> + <string name="error_creating_report">创建报告出错</string> + <string name="prefs_error_saving_settings">保存设置出错</string> + <string name="archive_verb">存档</string> + <string name="delete_report">删除举报</string> + <string name="report_verb">举报</string> + <string name="report_compose_reason_header_other">举报其他:仅moderators会看到。</string> + <string name="report_compose_reason_header_profile">举报成员个人资料:仅moderators会看到。</string> + <string name="report_compose_reason_header_community">举报违规:仅moderators会看到。</string> + <string name="archive_report">存档举报</string> + <string name="report_compose_reason_header_illegal">举报内容:仅moderators会看到。</string> + <string name="report_compose_reason_header_spam">举报垃圾信息:仅moderators会看到。</string> + <string name="group_member_role_moderator">moderators</string> + <string name="report_reason_other">另一个理由</string> + <string name="report_item_archived">已存档的举报</string> + <string name="report_reason_community">违反社区指导方针</string> + <string name="report_reason_illegal">不当内容</string> + <string name="report_reason_profile">不当个人资料</string> + <string name="report_item_visibility_moderators">仅发送人和moderators能看到</string> + <string name="report_item_visibility_submitter">只有你和moderators能看到</string> + <string name="report_reason_spam">垃圾信息</string> + <string name="report_archive_alert_title">存档举报?</string> + <string name="report_reason_alert_title">举报理由?</string> + <string name="report_archive_alert_desc">将为你存档该举报。</string> + <string name="report_item_archived_by">存档了 %s 的举报</string> + <string name="chat_list_group_reports">举报</string> + <string name="group_reports_active_one">1 个举报</string> + <string name="group_reports_member_reports">成员举报</string> + <string name="group_reports_active">%d 个举报</string> + <string name="blocking_reason_spam">垃圾信息</string> + <string name="connection_error_blocked">连接被阻止</string> + <string name="connection_error_blocked_desc">连接被服务器运营方阻止:\n%1$s.</string> + <string name="blocking_reason_content">内容违反使用条款</string> + <string name="file_error_blocked">文件被服务器运营方阻止:\n%1$s.</string> + <string name="privacy_chat_list_open_links_ask">询问</string> + <string name="privacy_chat_list_open_links_no">否</string> + <string name="privacy_chat_list_open_links_yes">是</string> + <string name="privacy_chat_list_open_web_link_question">打开 web 链接?</string> + <string name="privacy_chat_list_open_web_link">打开链接</string> + <string name="privacy_chat_list_open_links">打开来自聊天列表的链接</string> + <string name="text_field_set_chat_placeholder">设置聊天名称…</string> + <string name="disable_automatic_deletion_question">停用消息自动删除?</string> + <string name="disable_automatic_deletion">停用消息删除</string> + <string name="chat_item_ttl_year">1 年</string> + <string name="chat_item_ttl_default">默认(%s)</string> + <string name="chat_ttl_options_footer">从你的设备删除聊天消息。</string> + <string name="change_automatic_chat_deletion_message">此操作无法撤销 —— 比此聊天中所选消息更早发出并收到的消息将被删除。</string> + <string name="disable_automatic_deletion_message">此聊天中的消息永远不会被删除。</string> + <string name="change_automatic_deletion_question">更改消息自动删除设置?</string> + <string name="network_smp_web_port_section_title">用于消息收发的 TCP 端口</string> + <string name="network_smp_web_port_footer">未指定端口时,使用 TCP 端口 %1$s。</string> + <string name="network_smp_web_port_toggle">使用 web 端口</string> + <string name="mute_all_chat">全部静音</string> + <string name="unread_mentions">取消提及的已读状态</string> + <string name="max_group_mentions_per_message_reached">每条消息最多提及 %1$s 名成员!</string> + <string name="report_archive_alert_desc_all">将为你存档所有举报。</string> + <string name="report_archive_alert_title_all">存档所有举报?</string> + <string name="report_archive_alert_title_nth">存档 %d 份举报?</string> + <string name="archive_reports">存档举报</string> + <string name="report_archive_for_all_moderators">所有 moderators</string> + <string name="report_archive_for_me">仅自己</string> + <string name="notification_group_report">举报:%s</string> + <string name="disable_sending_member_reports">禁止向 moderators 举报消息。</string> + <string name="member_reports_are_prohibited">此群禁止消息举报。</string> + <string name="group_members_can_send_reports">成员可以向 moderators 举报消息。</string> + <string name="enable_sending_member_reports">允许向 moderators 举报消息。</string> + <string name="v6_3_mentions">提及成员👋</string> + <string name="v6_3_better_groups_performance">更好的群性能</string> + <string name="v6_3_better_privacy_and_security">更好的隐私和安全</string> + <string name="v6_3_organize_chat_lists_descr">不错过重要消息。</string> + <string name="v6_3_faster_deletion_of_groups">更快地删除群。</string> + <string name="v6_3_faster_sending_messages">更快发送消息。</string> + <string name="v6_3_mentions_descr">被提及时收到通知。</string> + <string name="v6_3_reports_descr">帮助管理员管理群组。</string> + <string name="v6_3_organize_chat_lists">将聊天组织到列表</string> + <string name="v6_3_private_media_file_names">私密媒体文件名。</string> + <string name="v6_3_reports">发送私下举报</string> + <string name="v6_3_set_message_expiration_in_chats">在聊天中设置消息过期时间。</string> + <string name="group_preview_rejected">被拒绝</string> + <string name="group_member_status_rejected">被拒绝</string> + <string name="error_reading_passphrase">数据库密码短语读取出错</string> + <string name="restore_passphrase_can_not_be_read_desc">无法读取 Keystore 中的密码短语,请手动输入它。这可能在不兼容本应用的系统更新后出现。如果不是这种情况,请联系开发者。</string> + <string name="restore_passphrase_can_not_be_read_enter_manually_desc">无法读取 Keystore 中的密码短语,请手动输入它。这可能在不兼容本应用的系统更新后出现。如果不是这种情况,请联系开发者。</string> + <string name="group_member_status_pending_approval_short">待批准</string> + <string name="group_member_status_pending_approval">待批准</string> + <string name="operator_updated_conditions">条款已更新</string> + <string name="block_members_desc">所有来自这些成员的新消息都将被隐藏!</string> + <string name="block_members_for_all_question">为所有其他成员封禁这些成员?</string> + <string name="members_will_be_removed_from_group_cannot_be_undone">将从群中移除这些成员 — 此操作无法撤销!</string> + <string name="unblock_members_desc">将显示来自这些成员的消息!</string> + <string name="button_remove_members_question">删除成员吗?</string> + <string name="unblock_members_for_all_question">为所有其他成员解封这些成员吗?</string> + <string name="feature_roles_moderators">moderators</string> + <string name="members_will_be_removed_from_chat_cannot_be_undone">将从聊天中移除这些成员 — 此操作无法撤销!</string> + <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">隐私政策和使用条款。</string> + <string name="onboarding_conditions_accept">接受</string> + <string name="onboarding_conditions_by_using_you_agree">使用 SimpleX Chat 代表您同意:\n- 在公开群中只发送合法内容\n- 尊重其他用户 – 没有垃圾信息。</string> + <string name="onboarding_conditions_private_chats_not_accessible">服务器运营方无法访问私密聊天、群组和你的联系人。</string> + <string name="onboarding_conditions_configure_server_operators">配置服务器运营方</string> + <string name="unsupported_connection_link">不支持的连接链接</string> + <string name="privacy_short_links">使用短链接(测试)</string> + <string name="simplex_link_channel">SimpleX 频道链接</string> + <string name="short_link_button_text">短链接</string> + <string name="link_requires_newer_app_version_please_upgrade">此链接需要更新的应用版本。请升级应用或请求你的联系人发送相容的链接。</string> + <string name="full_link_button_text">完整链接</string> + <string name="network_smp_web_port_all">全部服务器</string> + <string name="network_smp_web_port_off">关闭</string> + <string name="network_smp_web_port_preset">预设服务器</string> + <string name="network_smp_web_port_preset_footer">仅预设服务器使用 TCP 协议 443 端口。</string> +</resources> 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 7ab98ca323..8ae414de00 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 @@ -15,14 +15,14 @@ <string name="group_member_role_admin">管理員</string> <string name="above_then_preposition_continuation">然後,選按:</string> <string name="smp_servers_preset_add">新增預設伺服器</string> - <string name="smp_servers_add">新增伺服器…</string> + <string name="smp_servers_add">新增伺服器</string> <string name="accept">接受</string> <string name="auth_unavailable">認證無效</string> <string name="allow_verb">允許</string> - <string name="display_name__field">顯示名稱:</string> + <string name="display_name__field">個人資料名稱:</string> <string name="full_name__field">全名:</string> - <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>使用更多電量</b>!通知服務長期在背景中運行 – 有效的訊息就會即時顯示在通知內。]]></string> - <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>對電量友善</b>。通知服務於每十分鐘檢查一次訊息。你可能會錯過通話和迫切的訊息。]]></string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>使用更多電量</b>!程式始終在背景中運行 – 通知會立即顯示。]]></string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>對電量友善</b>。程式每10分鐘檢查一次訊息。你可能會錯過電話或警急訊息。]]></string> <string name="answer_call">回應通話請求</string> <string name="clear_contacts_selection_button">清除</string> <string name="allow_direct_messages">允許向群組內的成員傳送私訊。</string> @@ -47,8 +47,6 @@ <string name="allow_voice_messages_question">允許使用語音訊息?</string> <string name="cancel_verb">取消</string> <string name="icon_descr_cancel_live_message">取消實況訊息</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>新增新的聯絡人</b>:建立你的一次性二維碼給你的聯絡人。]]></string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>掃描二維碼</b>:連接到向你出示二維碼的聯絡人。]]></string> <string name="choose_file">選擇檔案</string> <string name="use_camera_button">相機</string> <string name="from_gallery_button">從圖片庫選擇圖片</string> @@ -71,7 +69,7 @@ <string name="your_current_profile">你目前的個人檔案</string> <string name="display_name_cannot_contain_whitespace">顯示的名稱中不能有空白。</string> <string name="save_preferences_question">儲存設定?</string> - <string name="display_name">顯示名稱</string> + <string name="display_name">輸入你的名稱:</string> <string name="callstatus_error">通話出錯</string> <string name="callstatus_calling">正在撥打…</string> <string name="callstatus_in_progress">通話中</string> @@ -121,7 +119,6 @@ <string name="chat_is_stopped">聊天室已停止運作</string> <string name="stop_chat_confirmation">停止</string> <string name="chat_database_deleted">已刪除數據庫的對話內容</string> - <string name="stop_chat_to_enable_database_actions">停止聊天室以啟用數據庫功能。</string> <string name="change_database_passphrase_question">修改數據庫密碼?</string> <string name="leave_group_question">確定要退出群組?</string> <string name="leave_group_button">退出</string> @@ -132,7 +129,7 @@ <string name="create_secret_group_title">建立私密群組</string> <string name="button_leave_group">退出群組</string> <string name="group_member_status_connected">已連接</string> - <string name="group_display_name_field">群組顯示名稱:</string> + <string name="group_display_name_field">輸入群組名稱:</string> <string name="group_full_name_field">群組全名:</string> <string name="chat_preferences">對話設定</string> <string name="chat_preferences_off">關閉</string> @@ -145,8 +142,7 @@ <string name="icon_descr_cancel_file_preview">取消檔案預覽</string> <string name="cannot_receive_file">無法接收檔案</string> <string name="failed_to_create_user_duplicate_title">重複的顯示名稱!</string> - <string name="network_session_mode_entity_description">一個單獨的 TCP 連接(和 SOCKS 憑證)將用於<b>每個聯絡人和群組內的成員</b>。 -\n<b>請注意</b>:如果你有很多連接,你的電話電量和數據流量的消耗率會大大增加,一些連接有機會會連接失敗。</string> + <string name="network_session_mode_entity_description"><![CDATA[一個單獨的 TCP 連接(和 SOCKS 憑證)將用於<b>每個聯絡人和群組內的成員</b>。 \n<b>請注意</b>:如果你有很多連接,你的電話電量和數據流量的消耗率會大大增加,一些連接有機會會連接失敗。]]></string> <string name="network_session_mode_user_description"><![CDATA[一個單獨的 TCP 連接(和 SOCKS 憑證)將用於 <b>每個聊天室的設定</b>。]]></string> <string name="back">返回</string> <string name="turning_off_service_and_periodic">省電模式運行中,關閉了背景通知服務和定期更新接收訊息。你可以在通知設定內重新啟用。</string> @@ -155,7 +151,7 @@ <string name="group_preferences">群組設定</string> <string name="contact_preferences">聯絡人設定</string> <string name="share_image">分享媒體…</string> - <string name="both_you_and_your_contacts_can_delete">你和你的聯絡人都可以不可逆地刪除已經傳送的訊息。</string> + <string name="both_you_and_your_contacts_can_delete">你和你的聯絡人都可以不可逆地刪除已傳送的訊息。</string> <string name="server_connected">已連接</string> <string name="simplex_link_mode_description">簡介</string> <string name="simplex_link_mode_full">完整連結</string> @@ -181,13 +177,13 @@ <string name="smp_servers_check_address">檢查輸入的伺服器地址,然後再試一次。</string> <string name="chat_console">終端機對話</string> <string name="star_on_github">於 Github 給個星星</string> - <string name="incognito_info_protects">匿名聊天模式會保護你的真實個人檔案名稱和頭像 — 當有新聯絡人的時候會自動建立一個隨機性的個人檔案。</string> + <string name="incognito_info_protects">隱身模式透過為每個聯絡人使用新的隨機設定檔來保護您的隱私。</string> <string name="incognito_info_allows">這樣是允許每一個對話中擁有不同的顯示名稱,並且沒有任何的個人資料可用於分享或有機會外洩。</string> <string name="allow_disappearing_messages_only_if">只有你的聯絡人允許的情況下,才允許自動銷毀訊息。</string> <string name="allow_your_contacts_to_send_disappearing_messages">允許你的聯絡人傳送自動銷毀的訊息。</string> - <string name="allow_irreversible_message_deletion_only_if">只有你的聯絡人允許的情況下,才允許不可逆地將訊息刪除。</string> - <string name="allow_your_contacts_irreversibly_delete">允許你的聯絡人可以不可逆地刪除已發送的訊息。</string> - <string name="allow_to_delete_messages">允許將不可撤銷的訊息刪除。</string> + <string name="allow_irreversible_message_deletion_only_if">只有你的聯絡人允許的情況下,才允許不可逆地將訊息刪除。(24小時)</string> + <string name="allow_your_contacts_irreversibly_delete">允許你的聯絡人不可逆地刪除已發送的訊息。(24小時)</string> + <string name="allow_to_delete_messages">允許不可逆地將已傳送的訊息刪除。(24小時)</string> <string name="allow_to_send_voice">允許傳送語音訊息。</string> <string name="delete_after">多久後刪除</string> <string name="all_group_members_will_remain_connected">群組內所有成員會保持連接。</string> @@ -233,9 +229,9 @@ <string name="v4_5_italian_interface_descr">感謝用戶 - 使用 Weblate 的翻譯貢獻!</string> <string name="app_name">SimpleX</string> <string name="thousand_abbreviation">k</string> - <string name="connect_via_contact_link">透過連結連接聯絡人?</string> - <string name="connect_via_invitation_link">透過邀請連結連接?</string> - <string name="connect_via_group_link">透過邀請連結連接群組?</string> + <string name="connect_via_contact_link">透過聯絡人地址連接?</string> + <string name="connect_via_invitation_link">透過一次性連結連接?</string> + <string name="connect_via_group_link">加入群組?</string> <string name="profile_will_be_sent_to_contact_sending_link">你的個人檔案將傳送給你接收此連結的聯絡人。</string> <string name="you_will_join_group">你將加入此連結內的群組並且連接到此群組成為群組內的成員。</string> <string name="connect_via_link_verb">連接</string> @@ -414,8 +410,6 @@ <string name="enter_one_ICE_server_per_line">ICE 伺服器(每行一個)</string> <string name="network_use_onion_hosts">使用 .onion 主機</string> <string name="network_use_onion_hosts_no_desc">Onion 主機不會啟用。</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">Onion 主機會在可用時啟用。</string> - <string name="network_use_onion_hosts_no_desc_in_alert">Onion 主機不會啟用。</string> <string name="network_session_mode_entity">連接</string> <string name="delete_address">刪除聯絡地址</string> <string name="colored_text">顏色</string> @@ -424,7 +418,6 @@ <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 桌面版:在應用程式內掃描一個已存在的二維碼,透過 <b>掃描二維碼</b>。]]></string> <string name="icon_descr_settings">設定</string> <string name="this_QR_code_is_not_a_link">這個二維碼不是一個連結!</string> - <string name="create_one_time_link">建立一次性邀請連結</string> <string name="network_use_onion_hosts_prefer">當可行的時候</string> <string name="core_version">核心版本:v%s</string> <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> @@ -440,7 +433,6 @@ <string name="ensure_ICE_server_address_are_correct_format_and_unique">請確保你的 WebRTC ICE 伺服器地址是正確的格式,每行也有分隔和沒有重複。</string> <string name="network_disable_socks">使用直接互聯網連接?</string> <string name="network_disable_socks_info">如果你確定,你的訊息伺服器能夠看到你的 IP 位置,和你的網路供應商 - 你正在連接到哪些伺服器。</string> - <string name="update_onion_hosts_settings_question">更新 .onion 主機設定?</string> <string name="delete_image">刪除圖片</string> <string name="callstate_starting">開始中 …</string> <string name="callstate_waiting_for_answer">等待對方回應…</string> @@ -499,7 +491,6 @@ <string name="network_use_onion_hosts_no">不</string> <string name="network_use_onion_hosts_required">需要</string> <string name="network_use_onion_hosts_prefer_desc">Onion 主機會在可用時啟用。</string> - <string name="network_use_onion_hosts_required_desc_in_alert">連接時將需要使用 Onion 主機。</string> <string name="delete_address__question">刪除聯絡地址?</string> <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">你的個人檔案只會儲存於你的裝置和只會分享給你的聯絡人。 SimpleX 伺服器並不會看到你的個人檔案。</string> <string name="save_and_notify_contact">儲存並通知你的聯絡人</string> @@ -516,7 +507,7 @@ <string name="callstate_ended">通話完結</string> <string name="connect_button">連接</string> <string name="network_and_servers">網路 & 伺服器</string> - <string name="network_settings_title">網路設定</string> + <string name="network_settings_title">進階設定</string> <string name="network_enable_socks">使用 SOCKS 代理伺服器</string> <string name="save_and_notify_group_members">儲存並通知群組內的聯絡人</string> <string name="exit_without_saving">退出並且不儲存記錄</string> @@ -554,13 +545,13 @@ <string name="smp_servers_test_some_failed">有一些伺服器測試失敗:</string> <string name="smp_servers_scan_qr">掃描伺服器的二維碼</string> <string name="smp_servers_your_server">你的伺服器</string> - <string name="network_use_onion_hosts_required_desc">連接時將需要使用 Onion 主機。</string> + <string name="network_use_onion_hosts_required_desc">連接時需要使用 Onion 主機。 +\n請注意:如果沒有 .onion 地址,您將無法連接到伺服器。</string> <string name="network_session_mode_user">對話檔案</string> <string name="description_via_group_link">透過群組連結</string> <string name="description_via_group_link_incognito">透過群組連結使用匿名聊天模式</string> <string name="description_via_contact_address_link_incognito">一個使用了匿名聊天模式的人透過連結加入了群組</string> <string name="description_via_one_time_link_incognito">透過使用一次性連結匿名聊天模式連接</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[有很多人問:<i>如果 SimpleX 沒有任何的用戶標識符,它如何傳送訊息?</i>]]></string> <string name="onboarding_notifications_mode_service">即時</string> <string name="onboarding_notifications_mode_periodic">定期的</string> <string name="no_call_on_lock_screen">關閉</string> @@ -605,8 +596,8 @@ <string name="button_create_group_link">建立連結</string> <string name="delete_link_question">刪除連結?</string> <string name="button_remove_member">移除成員</string> - <string name="member_role_will_be_changed_with_notification">成員的身份會修改為 \"%s\"。所有在群組內的成員都接收到通知。</string> - <string name="member_role_will_be_changed_with_invitation">成員的身份會修改為 \"%s\"。該成員將接收到新的邀請。</string> + <string name="member_role_will_be_changed_with_notification">成員的身份會修改為 "%s"。所有在群組內的成員都接收到通知。</string> + <string name="member_role_will_be_changed_with_invitation">成員的身份會修改為 "%s"。該成員將接收到新的邀請。</string> <string name="network_status">網路狀態</string> <string name="network_options_reset_to_defaults">重置為預設值</string> <string name="incognito_info_share">當你與某人分享已啟用匿名聊天模式的個人檔案時,此個人檔案將用於他們邀請你參加的群組。</string> @@ -628,7 +619,7 @@ <string name="no_contacts_selected">沒有聯絡人可以選擇</string> <string name="group_link">群組連結</string> <string name="delete_link">刪除連結</string> - <string name="group_is_decentralized">群組是完全去中心化的 - 只有群組內的成員能看到。</string> + <string name="group_is_decentralized">完全去中心化 - 只有成員能看到。</string> <string name="prohibit_sending_disappearing">禁止傳送自動銷毀的訊息。</string> <string name="ttl_hours">%d 個小時</string> <string name="whats_new">更新內容</string> @@ -638,7 +629,6 @@ <string name="chat_item_ttl_seconds">%s 秒(s)</string> <string name="error_encrypting_database">加密數據庫時出錯</string> <string name="save_passphrase_in_keychain">在金鑰庫儲存密碼</string> - <string name="archive_created_on_ts">建立於 %1$s</string> <string name="restore_database">還原數據庫的備份</string> <string name="icon_descr_group_inactive">群組為不活躍狀態</string> <string name="alert_title_group_invitation_expired">邀請連結過時!</string> @@ -650,7 +640,7 @@ <string name="save_group_profile">儲存群組檔案</string> <string name="voice_prohibited_in_this_chat">語音訊息於這個聊天室是禁用的。</string> <string name="v4_3_irreversible_message_deletion_desc">允許你的聯絡人可以完全刪除訊息。</string> - <string name="first_platform_without_user_ids">第一個沒有任何用戶識別符的通訊平台 – 以私隱為設計。</string> + <string name="first_platform_without_user_ids">沒有用戶識別符。</string> <string name="next_generation_of_private_messaging">新一代的私密訊息平台</string> <string name="decentralized">去中心化的</string> <string name="people_can_connect_only_via_links_you_share">人們只能在你分享了連結後,才能和你連接。</string> @@ -659,10 +649,10 @@ <string name="how_it_works">這是如何運作</string> <string name="onboarding_notifications_mode_subtitle">你可以之後透過設定修改。</string> <string name="make_private_connection">私下連接</string> - <string name="opensource_protocol_and_code_anybody_can_run_servers">開放源碼協議和程式碼 – 任何人也可以運行伺服器。</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">任何人都可以託管伺服器。</string> <string name="ignore">無視</string> <string name="incoming_audio_call">語音通話來電</string> - <string name="paste_the_link_you_received">貼上你接收到的連結</string> + <string name="paste_the_link_you_received">貼上你收到的連結</string> <string name="status_e2e_encrypted">端對端加密</string> <string name="status_no_e2e_encryption">沒有端對端加密</string> <string name="icon_descr_speaker_off">關閉喇叭</string> @@ -706,8 +696,6 @@ <string name="restore_database_alert_confirm">還原</string> <string name="restore_database_alert_title">還原數據庫的備份?</string> <string name="database_restore_error">還原數據庫時出錯</string> - <string name="save_archive">儲存存檔</string> - <string name="delete_archive">刪除存檔</string> <string name="join_group_button">加入</string> <string name="join_group_question">確定要加入群組?</string> <string name="join_group_incognito_button">加入匿名聊天模式</string> @@ -763,8 +751,8 @@ <string name="disappearing_prohibited_in_this_chat">自動銷毀訊息於這個聊天室內是禁用的。</string> <string name="message_deletion_prohibited">不可逆地刪除訊息於這個聊天室內是禁用的。</string> <string name="only_you_can_send_voice">只有你可以傳送語音訊息。</string> - <string name="direct_messages_are_prohibited_in_chat">私訊群組內的成員於這個群組內是禁用的。</string> - <string name="group_members_can_delete">群組內的成員可以不可逆地刪除訊息。</string> + <string name="direct_messages_are_prohibited_in_group">私訊群組內的成員於這個群組內是禁用的。</string> + <string name="group_members_can_delete">群組內的成員可以不可逆地刪除訊息。(24小時)</string> <string name="v4_3_voice_messages">語音訊息</string> <string name="v4_3_improved_server_configuration">改善伺服器配置</string> <string name="v4_3_improved_privacy_and_security_desc">當你切換至最近應用程式版面時,無法預覽程式畫面。</string> @@ -783,7 +771,6 @@ <string name="error_exporting_chat_database">匯出數據庫時出錯</string> <string name="error_importing_database">匯入數據庫時出錯</string> <string name="database_passphrase_will_be_updated">受加密的數據庫密碼會再次更新。</string> - <string name="delete_chat_archive_question">刪除封存對話?</string> <string name="encrypt_database_question">加密數據庫?</string> <string name="group_invitation_item_description">邀請至群組 %1$s</string> <string name="icon_descr_add_members">邀請成員</string> @@ -797,7 +784,6 @@ <string name="ttl_s">%ds</string> <string name="onboarding_notifications_mode_title">私人通知</string> <string name="read_more_in_github_with_link"><![CDATA[可在 <font color="#0088ff">GitHub</font>內查看更多。]]></string> - <string name="read_more_in_github">於 GitHub 儲存庫內查看更多。</string> <string name="incoming_video_call">視訊通話來電</string> <string name="icon_descr_hang_up">掛斷電話來電</string> <string name="call_connection_peer_to_peer">點對點</string> @@ -805,13 +791,11 @@ <string name="status_contact_has_no_e2e_encryption">對話沒有經過端對端加密</string> <string name="database_encrypted">數據庫已加密!</string> <string name="encrypted_database">已加密數據庫</string> - <string name="chat_archive_section">封存對話</string> <string name="snd_group_event_group_profile_updated">群組資料已經更新</string> <string name="group_member_role_member">成員</string> <string name="group_info_member_you">你:%1$s</string> <string name="button_delete_group">刪除群組</string> <string name="v4_4_live_messages">即時訊息</string> - <string name="chat_archive_header">封存對話</string> <string name="error_removing_member">移除成員時出錯</string> <string name="error_changing_role">修改身份時出錯</string> <string name="info_row_group">群組</string> @@ -828,17 +812,15 @@ <string name="disappearing_messages_are_prohibited">自動銷毀訊息於這個群組內是禁用的。</string> <string name="feature_offered_item">已提供 %s</string> <string name="error_saving_group_profile">儲存群組檔案時出錯</string> - <string name="network_options_revert">恢復</string> <string name="theme">主題</string> - <string name="save_color">儲存顏色</string> <string name="chat_preferences_you_allow">你允許</string> <string name="set_group_preferences">修改群組內的設定</string> <string name="direct_messages">私訊</string> <string name="feature_enabled">已啟用</string> <string name="feature_enabled_for_you">已為你啟用</string> <string name="feature_enabled_for_contact">已為聯絡人啟用</string> - <string name="only_you_can_delete_messages">只有你能不可逆地刪除訊息(你的聯絡人可以將它標記為刪除)。</string> - <string name="only_your_contact_can_delete">只有你的聊絡人可以不可逆的刪除訊息(你可以將它標記為刪除)。</string> + <string name="only_you_can_delete_messages">只有你能不可逆地刪除訊息(你的聯絡人可以將它標記為刪除)。(24小時)</string> + <string name="only_your_contact_can_delete">只有你的聊絡人可以不可逆的刪除訊息(你可以將它標記為刪除)。(24小時)</string> <string name="only_your_contact_can_send_voice">只有你的聯絡人可以傳送語音訊息。</string> <string name="prohibit_direct_messages">禁止私訊群組內的成員。</string> <string name="message_deletion_prohibited_in_chat">不可逆地刪除訊息於這個群組內是禁用的。</string> @@ -888,7 +870,6 @@ <string name="update_network_session_mode_question">更新傳輸隔離模式?</string> <string name="to_protect_privacy_simplex_has_ids_for_queues">為了保護隱私,而不像是其他平台般需要提取和存儲用戶的 IDs 資料, SimpleX 平台有自家佇列的標識符,這對於你的每個聯絡人也是獨一無二的。</string> <string name="onboarding_notifications_mode_off">當應用程式是運行中</string> - <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[你可以控制通過哪一個伺服器 <b>來接收</b> 你的聯絡人訊息 – 這些伺服器用來接收他們傳送給你的訊息。]]></string> <string name="allow_accepting_calls_from_lock_screen">透過設定啟用於上鎖畫面顯示來電通知。</string> <string name="delete_chat_profile_action_cannot_be_undone_warning">這操作不能還原 - 你目前的個人檔案,聯絡人,訊息和檔案將不可逆地遺失。</string> <string name="you_must_use_the_most_recent_version_of_database">你必須在裝置上使用最新版本的對話數據庫,否則你可能會停止接收某些聯絡人的訊息。</string> @@ -908,7 +889,7 @@ <string name="icon_descr_expand_role">添加更多身份選項</string> <string name="icon_descr_context">聯絡人頭像</string> <string name="icon_descr_profile_image_placeholder">個人檔案頭像占位符</string> - <string name="immune_to_spam_and_abuse">不受垃圾郵件和濫用行為影響</string> + <string name="immune_to_spam_and_abuse">不受垃圾和騷擾訊息影響</string> <string name="contact_wants_to_connect_via_call">%1$s 希望透過以下方式聯絡你</string> <string name="icon_descr_video_on">開啟視訊</string> <string name="icon_descr_flip_camera">翻轉相機</string> @@ -917,7 +898,7 @@ <string name="your_calls">你的通話</string> <string name="always_use_relay">經由分程傳遞連接</string> <string name="call_on_lock_screen">在上鎖畫面顯示來電通知:</string> - <string name="integrity_msg_skipped">%1$d 你錯過了多個訊息</string> + <string name="integrity_msg_skipped">%1$d 條訊息已跳過</string> <string name="integrity_msg_bad_hash">錯誤的訊息雜湊值</string> <string name="alert_title_skipped_messages">你錯過了多個訊息</string> <string name="settings_section_title_you">你</string> @@ -968,7 +949,6 @@ <string name="button_welcome_message">歡迎訊息</string> <string name="save_and_update_group_profile">儲存和更新群組配置檔案</string> <string name="save_welcome_message_question">儲存歡迎訊息?</string> - <string name="cant_delete_user_profile">無法刪除個人檔案!</string> <string name="user_hide">隱藏</string> <string name="make_profile_private">將個人資料設為私密!</string> <string name="v4_6_audio_video_calls">語音和視訊通話</string> @@ -994,10 +974,8 @@ <string name="v4_6_group_welcome_message_descr">設定向新成員顯示的訊息!</string> <string name="tap_to_activate_profile">點擊以激活配置檔案。</string> <string name="v4_6_audio_video_calls_descr">支援藍牙和其他改進。</string> - <string name="should_be_at_least_one_visible_profile">至少要有一個可見的個人檔案。</string> <string name="group_welcome_title">歡迎訊息</string> <string name="v4_6_chinese_spanish_interface_descr">感謝用戶-透過 Weblate 做出貢獻!</string> - <string name="should_be_at_least_one_profile">應該至少有一個個人檔案。</string> <string name="user_unmute">解除靜音</string> <string name="you_will_still_receive_calls_and_ntfs">當靜音配置檔案處於活動狀態時,你仍會接收來自靜音配置檔案的通話和通知。</string> <string name="user_unhide">取消隱藏</string> @@ -1115,7 +1093,7 @@ <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d 訊息解密失敗。</string> <string name="network_socks_toggle_use_socks_proxy">使用SOCKS 代理伺服器</string> <string name="your_XFTP_servers">你的 XFTP 伺服器</string> - <string name="alert_text_decryption_error_too_many_skipped">%1$d 錯過了多個訊息。</string> + <string name="alert_text_decryption_error_too_many_skipped">%1$d 條訊息已跳過。</string> <string name="v5_0_large_files_support">影片和檔案和最大上限為1gb</string> <string name="gallery_video_button">影片</string> <string name="submit_passcode">呈交</string> @@ -1123,7 +1101,7 @@ <string name="learn_more">查看更多</string> <string name="simplex_address">SimpleX 聯絡地址</string> <string name="one_time_link_short">一次性連結</string> - <string name="theme_colors_section_title">主題顏色</string> + <string name="theme_colors_section_title">介面顏色</string> <string name="create_simplex_address">建立 SimpleX 的聯絡地址</string> <string name="profile_update_will_be_sent_to_contacts">更新了的個人檔案將傳送給你的聯絡人。</string> <string name="share_address_with_contacts_question">與你的聯絡人分享聯絡地址?</string> @@ -1152,7 +1130,7 @@ <string name="color_sent_message">已傳送訊息</string> <string name="color_title">標題</string> <string name="learn_more_about_address">關於 SimpleX 的聯絡地址</string> - <string name="color_primary_variant">外加的顏色</string> + <string name="color_primary_variant">額外的強調色</string> <string name="color_secondary_variant">外加的輔助</string> <string name="address_section_title">聯絡地址</string> <string name="color_background">後台</string> @@ -1253,4 +1231,563 @@ \n- 編輯紀錄。</string> <string name="search_verb">搜尋</string> <string name="la_mode_off">已關閉</string> -</resources> \ No newline at end of file + <string name="v5_8_safe_files_descr">確認來自未知伺服器的檔案。</string> + <string name="snd_error_quota">超出額度 - 收件人未收到先前傳送的訊息</string> + <string name="v5_6_app_data_migration">應用程式資料轉移</string> + <string name="migrate_to_device_apply_onion">應用</string> + <string name="migrate_from_device_confirm_you_remember_passphrase">請在轉移之前確認你還記得數據庫密碼</string> + <string name="blocked_by_admin_item_description">被管理員封鎖</string> + <string name="wallpaper_advanced_settings">進階設定</string> + <string name="v5_4_block_group_members">封鎖群組成員</string> + <string name="servers_info_subscriptions_connections_subscribed">活躍連接</string> + <string name="abort_switch_receiving_address_confirm">中止</string> + <string name="rcv_group_and_other_events">和其他 %d 事件</string> + <string name="block_member_question">封鎖成員?</string> + <string name="v5_3_new_interface_languages">6種全新的介面語言</string> + <string name="audio_device_bluetooth">藍芽</string> + <string name="moderated_items_description">%2$s 審核了 %1$d 條訊息</string> + <string name="blocked_item_description">已封鎖</string> + <string name="abort_switch_receiving_address_desc">將停止地址更改。將使用舊聯絡地址。</string> + <string name="app_check_for_updates_beta">測試</string> + <string name="app_check_for_updates">檢查更新</string> + <string name="permissions_camera_and_record_audio">相機和麥克風</string> + <string name="block_member_confirmation">封鎖</string> + <string name="theme_destination_app_theme">應用程式主題</string> + <string name="feature_roles_admins">管理員</string> + <string name="v6_0_privacy_blur">模糊以增強隱私</string> + <string name="feature_roles_all_members">所有成員</string> + <string name="v5_6_safer_groups_descr">管理員可以為所有人封鎖一名成員</string> + <string name="cannot_share_message_alert_title">無法傳送訊息</string> + <string name="block_for_all_question">為所有成員封鎖此成員?</string> + <string name="theme_black">黑</string> + <string name="abort_switch_receiving_address_question">中止更改地址?</string> + <string name="cant_send_message_to_member_alert_title">無法傳送訊息給群組成員</string> + <string name="chat_theme_apply_to_all_modes">所有顏色模式</string> + <string name="chat_theme_apply_to_mode">應用到</string> + <string name="la_app_passcode">應用程式密碼</string> + <string name="settings_section_title_app">應用程式</string> + <string name="settings_section_title_chat_colors">聊天顏色</string> + <string name="chat_is_stopped_you_should_transfer_database">聊天已停止。如果你已經在另一台設備使用過此資料庫,你應該在啟動聊天前將數據庫傳輸回來。</string> + <string name="in_developing_title">即將推出!</string> + <string name="app_check_for_updates_download_completed_title">軟體更新以下載</string> + <string name="v6_0_your_contacts_descr">儲存聯絡人以便稍後聊天</string> + <string name="permissions_camera">相機</string> + <string name="choose_file_title">選擇一個檔案</string> + <string name="migrate_from_device_archive_and_upload">存檔並上傳</string> + <string name="migrate_from_device_all_data_will_be_uploaded">你的所有聯絡人、對話和檔案將被安全加密並切塊上傳到你設定的 XFTP 中繼</string> + <string name="migrate_from_device_archiving_database">正在儲存資料庫</string> + <string name="migrate_from_device_cancel_migration">取消遷移</string> + <string name="snd_conn_event_ratchet_sync_started">與 %s 協調加密中…</string> + <string name="turn_off_battery_optimization_button">允許</string> + <string name="call_service_notification_audio_call">語音通話</string> + <string name="camera_not_available">相機不可用</string> + <string name="clear_note_folder_warning">所有訊息都將被刪除 - 這無法復原</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>請注意</b>:訊息和檔案中繼通過 SOCKS 代理連接。通話和傳送連預覽使用直接連接。]]></string> + <string name="block_for_all">封鎖全部</string> + <string name="v5_4_better_groups">改進群組功能</string> + <string name="network_type_cellular">行動網路</string> + <string name="block_member_button">封鎖成員</string> + <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>警告</b>:此存檔將被刪除。]]></string> + <string name="clear_note_folder_question">清除私密筆記?</string> + <string name="add_contact_tab">添加聯絡人</string> + <string name="network_smp_proxy_mode_always">總是</string> + <string name="conn_event_ratchet_sync_started">協調加密中…</string> + <string name="allow_to_send_files">允許傳送檔案和媒體</string> + <string name="allow_to_send_simplex_links">允許傳送 SimpleX 連結</string> + <string name="v5_2_more_things">其他</string> + <string name="connect_plan_already_connecting">已連接!</string> + <string name="abort_switch_receiving_address">中止更改地址</string> + <string name="connect__a_new_random_profile_will_be_shared">將分享新的隨機個人檔案</string> + <string name="block_member_desc">所有來自 %s 的新訊息都將被隱藏!</string> + <string name="rcv_group_event_member_blocked">已封鎖 %s</string> + <string name="group_member_role_author">作者</string> + <string name="member_info_member_blocked">已封鎖</string> + <string name="member_blocked_by_admin">被管理員封鎖</string> + <string name="color_primary_variant2">額外的強調色2</string> + <string name="v5_3_new_interface_languages_descr">阿拉伯語、保加利亞語、芬蘭語、希伯來語、泰國語和烏克蘭語——感謝使用者們與Weblate</string> + <string name="connect_plan_already_joining_the_group">已加入群組!</string> + <string name="migrate_to_device_confirm_network_settings">確認網路設定</string> + <string name="attempts_label">嘗試</string> + <string name="acknowledged">已確認</string> + <string name="acknowledgement_errors">確認錯誤</string> + <string name="completed">完成</string> + <string name="chunks_deleted">區塊已刪除</string> + <string name="chunks_uploaded">區塊已上傳</string> + <string name="chunks_downloaded">區塊已下載</string> + <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>添加聯絡人</b>: 來創建新的邀請連結,或通過你收到的連結進行連接。]]></string> + <string name="create_group_button_to_create_new_group"><![CDATA[<b>建立群組</b>: 建立新的群組。]]></string> + <string name="bad_desktop_address">錯誤的桌面地址</string> + <string name="migrate_to_device_chat_migrated">已轉移聊天</string> + <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[在新的設備上選擇 <i>從另一部設備轉移</i> 並掃描QR code。]]></string> + <string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>請注意</b>: 作為安全保護措施,在兩部設備上使用同一數據庫會破壞解密來自你聯絡人的訊息。]]></string> + <string name="confirm_delete_contact_question">確定刪除聯絡人?</string> + <string name="app_check_for_updates_notice_title">檢查更新</string> + <string name="cant_call_contact_alert_title">無法與聯絡人通話</string> + <string name="calls_prohibited_alert_title">通話被禁止!</string> + <string name="cant_call_member_alert_title">無法與群組成員通話</string> + <string name="v5_3_encrypt_local_files_descr">應用程式將為新的本機檔案(影片除外)加密。</string> + <string name="migrate_from_device_check_connection_and_try_again">檢查你的網路連接並重試</string> + <string name="all_users">所有配置文件</string> + <string name="smp_servers_configured">已設定的 SMP 伺服器</string> + <string name="settings_section_title_chat_theme">聊天主題</string> + <string name="info_view_call_button">通話</string> + <string name="network_smp_proxy_fallback_allow_downgrade">允許降級</string> + <string name="network_smp_proxy_mode_always_description">始終使用私密路由。</string> + <string name="chat_database_exported_title">以導出聊天資料庫</string> + <string name="xftp_servers_configured">已設定的 XFTP 伺服器</string> + <string name="color_mode">色彩模式</string> + <string name="deleted_chats">已儲存的聯絡人</string> + <string name="privacy_media_blur_radius">模糊媒體</string> + <string name="allow_calls_question">允許通話?</string> + <string name="share_text_created_at">建立於:%s</string> + <string name="error_initializing_web_view">Webview 初始化失敗。更新你的系統到新版本。請聯繫開發者。 +\n錯誤:%s</string> + <string name="rcv_direct_event_contact_deleted">已刪除聯絡人</string> + <string name="rcv_group_events_count">%d 個群事件</string> + <string name="message_too_large">訊息太大</string> + <string name="message_delivery_warning_title">訊息傳送警告</string> + <string name="ci_status_other_error">錯誤:%1$s</string> + <string name="developer_options_section">開發者選項</string> + <string name="snd_conn_event_ratchet_sync_required">與 %s 的加密需要重協商</string> + <string name="remote_host_error_inactive"><![CDATA[行動裝置 <b>%s</b> 不活躍]]></string> + <string name="favorite_chat">最喜歡</string> + <string name="v5_2_message_delivery_receipts">訊息成功送達!</string> + <string name="files_and_media">檔案和媒體</string> + <string name="remote_ctrl_was_disconnected_title">連線停止</string> + <string name="remote_host_error_bad_state"><![CDATA[與行動裝置 \u0020<b>%s</b>的連接不穩定]]></string> + <string name="receipts_section_contacts">聯絡人</string> + <string name="wallpaper_scale_fit">適合</string> + <string name="group_members_can_send_files">群組成員可以傳送檔案和媒體。</string> + <string name="linked_mobiles">連結行動裝置</string> + <string name="files_are_prohibited_in_group">此群組禁止檔案和媒體</string> + <string name="call_service_notification_end_call">結束通話</string> + <string name="delete_messages__question">刪除 %d 條訊息嗎?</string> + <string name="privacy_message_draft">訊息草稿</string> + <string name="conn_event_ratchet_sync_allowed">允許重新協商加密</string> + <string name="info_row_created_at">建立於</string> + <string name="error_blocking_member_for_all">為所有人封鎖時出錯</string> + <string name="desktop_app_version_is_incompatible">桌面應用版本 %s 與此應用不相容</string> + <string name="file_error_no_file">未找到檔案 - 檔案可能被刪除或被取消了</string> + <string name="file_error_relay">檔案伺服器錯誤:%1$s</string> + <string name="expand_verb">展開</string> + <string name="receipts_groups_enable_keep_overrides">啟用(保留組覆蓋)</string> + <string name="color_mode_light">淺色</string> + <string name="chat_theme_apply_to_light_mode">淺色模式</string> + <string name="group_members_can_send_simplex_links">群組成員可傳送 SimpleX 連結。</string> + <string name="color_mode_dark">深色</string> + <string name="servers_info_details">詳情</string> + <string name="servers_info_subscriptions_section_header">訊息接收</string> + <string name="error_parsing_uri_title">無效連結</string> + <string name="marked_deleted_items_description">%d 條訊息被標記為刪除</string> + <string name="blocked_items_description">%d 條訊息已攔截</string> + <string name="smp_proxy_error_broker_host">轉發伺服器地址不相容網路設定:%1$s。</string> + <string name="smp_proxy_error_broker_version">轉發伺服器地址不相容網路設定:%1$s。</string> + <string name="proxy_destination_error_broker_host">%1$s 的目標伺服器地址不相容轉送伺服器 %2$s 的設定</string> + <string name="proxy_destination_error_broker_version">%1$s 的目地伺服器版本不相容於轉送伺服器 %2$s.</string> + <string name="disable_notifications_button">關閉通知</string> + <string name="snd_error_expired">網路問題 - 多次嘗試傳送訊息後,訊息已過期。</string> + <string name="snd_error_proxy_relay">轉發伺服器:%1$s +\n目標伺服器錯誤:%2$s</string> + <string name="member_inactive_desc">如果成員變得活躍,可能會在之後傳送訊息。</string> + <string name="contact_deleted">刪除了聯絡人!</string> + <string name="delete_contact_cannot_undo_warning">聯絡人將被刪除 - 無法復原此操作</string> + <string name="network_smp_proxy_fallback_prohibit">不</string> + <string name="app_check_for_updates_disabled">已停用</string> + <string name="app_check_for_updates_installed_successfully_title">安裝成功</string> + <string name="create_address_button">建立</string> + <string name="unable_to_open_browser_title">打開瀏覽器出錯</string> + <string name="receipts_contacts_enable_keep_overrides">啟用(保留覆蓋)</string> + <string name="receipts_groups_disable_keep_overrides">禁用(保留組覆蓋)</string> + <string name="receipts_groups_title_enable">為群組啟用回執?</string> + <string name="settings_section_title_network_connection">網路連接</string> + <string name="chat_database_exported_continue">繼續</string> + <string name="info_row_file_status">檔案狀態</string> + <string name="dark_mode_colors">深色模式顏色</string> + <string name="appearance_font_size">字體大小</string> + <string name="feature_enabled_for">啟用於</string> + <string name="v5_7_new_interface_languages">立陶宛語使用者介面</string> + <string name="v5_8_chat_themes">新的聊天主題</string> + <string name="v6_0_connection_servers_status_descr">連線和伺服器狀態</string> + <string name="v6_0_connection_servers_status">控制你的網路</string> + <string name="v6_0_upgrade_app_descr">從GitHub下載最新版本。</string> + <string name="enable_receipts_all">啟用</string> + <string name="v6_0_new_chat_experience">新的聊天體驗 🎉</string> + <string name="v6_0_new_media_options">新的媒體選項</string> + <string name="connected_mobile">以連接的行動裝置</string> + <string name="connected_desktop">連接桌面</string> + <string name="connected_to_desktop">連接到桌面</string> + <string name="desktop_connection_terminated">連線終止</string> + <string name="connect_to_desktop">連接到桌面</string> + <string name="migrate_from_device_error_uploading_archive">上傳存檔出錯</string> + <string name="migrate_to_device_file_delete_or_link_invalid">文件被刪除或鏈接無效</string> + <string name="migrate_to_device_import_failed">導入失敗</string> + <string name="audio_device_wired_headphones">頭戴式耳機</string> + <string name="audio_device_earpiece">耳機</string> + <string name="receipts_contacts_disable_for_all">對所有聯絡人關閉</string> + <string name="chat_theme_apply_to_dark_mode">深色模式</string> + <string name="error_enabling_delivery_receipts">啟用已讀回條時出錯!</string> + <string name="this_device_version"><![CDATA[<i>(此裝置 v%s)</i>]]></string> + <string name="desktop_incompatible_version">不相容的版本</string> + <string name="remote_ctrl_error_disconnected">PC版已斷線</string> + <string name="migrate_from_device_title">轉移裝置</string> + <string name="v5_7_call_sounds">通話鈴聲</string> + <string name="v5_7_forward">轉發並保存訊息</string> + <string name="v5_7_forward_descr">訊息來源保持私密</string> + <string name="no_connected_mobile">沒有已連接的行動裝置</string> + <string name="files_and_media_prohibited">禁止檔案和媒體!</string> + <string name="loading_remote_file_title">檔案載入中</string> + <string name="file_error">檔案錯誤</string> + <string name="update_network_smp_proxy_fallback_question">備用訊息路由</string> + <string name="network_smp_proxy_fallback_prohibit_description">如果你或你的目標伺服器不支持私密路由,將不直接傳送訊息。</string> + <string name="create_another_profile_button">建立個人資料</string> + <string name="migrate_from_another_device">從另一台裝置轉移</string> + <string name="profile_update_event_member_name_changed">成員姓名從 %1$s 改為了 %2$s</string> + <string name="conn_event_ratchet_sync_agreed">同意加密</string> + <string name="info_row_message_status">訊息狀態</string> + <string name="connect_via_member_address_alert_desc">連接請求將傳送給該組成員。</string> + <string name="v5_2_more_things_descr">- 更穩定的消息傳送。 +\n- 更好的群組。 +\n- 還有更多!</string> + <string name="v5_5_new_interface_languages">匈牙利語和土耳其語用戶界面</string> + <string name="migrate_from_device_migration_complete">轉移完成</string> + <string name="migrate_from_device_delete_database_from_device">從此裝置刪除數據庫</string> + <string name="delivery">傳送</string> + <string name="download_file">下載</string> + <string name="forward_chat_item">轉發</string> + <string name="forwarded_chat_item_info_tab">已轉發</string> + <string name="forwarded_from_chat_item_info_title">轉發自</string> + <string name="files_and_media_not_allowed">不允許檔案和媒體</string> + <string name="forward_message">轉發訊息…</string> + <string name="creating_link">建立連結中…</string> + <string name="keep_invitation_link">保留</string> + <string name="profile_update_event_contact_name_changed">聯絡人姓名從 %1$s 改為了 %2$s</string> + <string name="new_message">新訊息</string> + <string name="invite_friends_short">邀請</string> + <string name="receipts_contacts_disable_keep_overrides">停用(保留覆蓋)</string> + <string name="share_text_message_status">訊息狀態:%s</string> + <string name="message_queue_info">訊息隊列資訊</string> + <string name="servers_info_messages_received">收到的訊息</string> + <string name="encrypt_local_files">加密本機檔案</string> + <string name="settings_section_title_files">檔案</string> + <string name="receipts_groups_disable_for_all">對所有群組關閉</string> + <string name="wallpaper_scale_fill">填充</string> + <string name="v5_8_chat_themes_descr">讓你的聊天看上去不相同!</string> + <string name="v6_0_increase_font_size">增大字體大小。</string> + <string name="migrate_to_device_enter_passphrase">輸入密碼短語</string> + <string name="servers_info_messages_sent">傳送的訊息</string> + <string name="remote_ctrl_error_busy">PC版處理中</string> + <string name="connect_via_link_incognito">隱身模式連接</string> + <string name="conversation_deleted">已刪除對話!</string> + <string name="snd_error_relay">目標伺服器錯誤:%1$s</string> + <string name="loading_chats">聊天載入中…</string> + <string name="message_forwarded_title">已轉發的訊息</string> + <string name="blocked_by_admin_items_description">管理員封鎖了 %d 條訊息</string> + <string name="smp_proxy_error_connecting">連結轉發伺服器 %1$s 出錯。請稍候嘗試。</string> + <string name="migrate_from_device_error_deleting_database">刪除資料庫出錯</string> + <string name="snd_error_proxy">轉發伺服器:%1$s +\n錯誤:%2$s</string> + <string name="proxy_destination_error_failed_to_connect">轉發伺服器 %1$s 連結目標伺服器 %2$s 失敗。請稍後嘗試。</string> + <string name="migrate_to_device_importing_archive">導入存檔中</string> + <string name="v5_8_message_delivery">改進訊息傳送</string> + <string name="app_check_for_updates_button_install">安裝更新</string> + <string name="v6_0_private_routing_descr">它保護你的 IP 位址和連線。</string> + <string name="v5_5_join_group_conversation">加入群組對話</string> + <string name="remote_host_error_bad_version"><![CDATA[不支援行動裝置 <b>%s</b> 的版本。請檢察兩台裝置安裝的是否版本相同]]></string> + <string name="v5_7_network_descr">更可靠的網路連接</string> + <string name="v5_3_discover_join_groups">發現和加入群組</string> + <string name="devices">設備</string> + <string name="new_mobile_device">新行動裝置</string> + <string name="migrate_from_device_error_saving_settings">保存設定出錯</string> + <string name="migrate_from_device_exported_file_doesnt_exist">導出的檔案不存在</string> + <string name="migrate_from_device_error_exporting_archive">導出資料庫時出錯</string> + <string name="migrate_from_device_confirm_upload">確認上傳</string> + <string name="migrate_from_device_creating_archive_link">正在建立存檔連結</string> + <string name="conn_event_ratchet_sync_ok">加密OK</string> + <string name="unblock_member_desc">將顯示來自 %s 的訊息!</string> + <string name="delivery_receipts_title">送達回執!</string> + <string name="forwarded_description">已轉發</string> + <string name="e2ee_info_no_pq"><![CDATA[訊息、檔案和通話均受到<b>端對端加密</b>的保護,並具有完全的前向加密、不可否認性和入侵恢復。]]></string> + <string name="encryption_renegotiation_error">加密協商錯誤</string> + <string name="e2ee_info_pq"><![CDATA[訊息、檔案和通話受到完全前向加密、不可否認性和入侵恢復的<b>抗量子端對端加密</b>保護。]]></string> + <string name="alert_text_encryption_renegotiation_failed">加密重協商失敗</string> + <string name="database_encryption_will_be_updated_in_settings">將更新資料庫密碼並儲存在設定中。</string> + <string name="v5_4_incognito_groups_descr">使用隨機身分建立群組</string> + <string name="v5_4_better_groups_descr">加入速度更快、訊息更可靠。</string> + <string name="v5_4_incognito_groups">匿名群組</string> + <string name="link_a_mobile">連接行動裝置</string> + <string name="in_reply_to">回復</string> + <string name="connect_with_contact_name_question">和 %1$s 連接?</string> + <string name="v5_3_new_desktop_app_descr">在桌面應用裡建立新的帳號。💻</string> + <string name="enter_this_device_name">輸入此裝置名稱…</string> + <string name="connected_to_mobile">已連結到行動裝置</string> + <string name="multicast_discoverable_via_local_network">可通過局域網發現</string> + <string name="new_desktop"><![CDATA[<i>(新)</i>]]></string> + <string name="agent_critical_error_title">嚴重錯誤</string> + <string name="agent_internal_error_title">內部錯誤</string> + <string name="migrate_from_device_error_verifying_passphrase">驗證密碼短語出錯:</string> + <string name="failed_to_create_user_invalid_title">顯示名稱無效!</string> + <string name="non_content_uri_alert_title">無效的檔案路徑</string> + <string name="v5_2_favourites_filter_descr">過濾未讀和收藏的聊天記錄。</string> + <string name="disconnect_remote_host">斷開連結</string> + <string name="disconnect_desktop_question">斷開桌面連結?</string> + <string name="error_aborting_address_change">中止地址更改時出錯</string> + <string name="error_showing_desktop_notification">顯示通知出錯,請聯繫開發者。</string> + <string name="delete_and_notify_contact">刪除並通知聯絡人</string> + <string name="update_network_smp_proxy_mode_question">訊息路由模式</string> + <string name="network_smp_proxy_mode_never_description">不使用私密路由。</string> + <string name="permissions_grant">要進行通話請授予一項或多項權限</string> + <string name="permissions_find_in_settings_and_grant">在 Android 系統設定中找到此權限並手動授予權限。</string> + <string name="permissions_grant_in_settings">在系統設定中授予</string> + <string name="error_sending_message_contact_invitation">傳送邀請出錯</string> + <string name="error_creating_member_contact">建立聯絡人時出錯</string> + <string name="v5_2_disappear_one_message">移除一條訊息</string> + <string name="v5_2_fix_encryption">保持連接</string> + <string name="v5_3_discover_join_groups_descr">- 連接到目錄服務(BETA)! +\n- 發送回執(最多20名成員)。 +\n- 更快,更穩定。</string> + <string name="v5_4_link_mobile_desktop">連接行動端和桌面端應用程式! 🔗</string> + <string name="v5_5_message_delivery">改進訊息傳送</string> + <string name="delivery_receipts_are_disabled">已關閉送達回執!</string> + <string name="remote_host_was_disconnected_toast"><![CDATA[行動裝置 <b>%s</b> 斷開連接]]></string> + <string name="remote_host_error_missing"><![CDATA[行動裝置 <b>%s</b> 未找到]]></string> + <string name="migrate_to_device_downloading_archive">存檔下載中</string> + <string name="migrate_to_device_migrating">轉移中</string> + <string name="migrate_to_device_download_failed">下載失敗</string> + <string name="receipts_groups_enable_for_all">為所有組啟用</string> + <string name="conn_event_ratchet_sync_required">需要重協商加密</string> + <string name="send_receipts_disabled">關閉</string> + <string name="fix_connection">修復連結</string> + <string name="fix_connection_not_supported_by_contact">修復聯絡人不支援的問題</string> + <string name="fix_connection_confirm">修復</string> + <string name="fix_connection_question">修復連結?</string> + <string name="database_migration_in_progress">正在進行資料庫轉移。 +\n可能需要幾分鐘時間。</string> + <string name="error_creating_message">建立訊息出錯</string> + <string name="error_deleting_note_folder">刪除私密筆記錯誤</string> + <string name="enable_camera_access">啟用相機訪問</string> + <string name="new_chat">新聊天</string> + <string name="receipts_contacts_enable_for_all">為所有人啟用</string> + <string name="error_synchronizing_connection">同步連接時出錯</string> + <string name="error_showing_content">顯示內容出錯</string> + <string name="error_showing_message">顯示訊息出錯</string> + <string name="error_alert_title">錯誤</string> + <string name="system_restricted_background_in_call_title">無後台通話</string> + <string name="create_chat_profile">建立聊天資料</string> + <string name="snd_conn_event_ratchet_sync_agreed">已同意 %s 的加密</string> + <string name="snd_conn_event_ratchet_sync_allowed">允許重新協商與 %s 的加密</string> + <string name="snd_conn_event_ratchet_sync_ok">與 %s 的加密OK</string> + <string name="desktop_devices">桌面設備</string> + <string name="linked_desktop_options">連接桌面選項</string> + <string name="linked_desktops">連接桌面</string> + <string name="rcv_group_event_member_created_contact">直接連線中</string> + <string name="action_button_add_members">邀請</string> + <string name="create_group_button">建立群組</string> + <string name="fix_connection_not_supported_by_group_member">修復群組成員不支援的問題</string> + <string name="v6_0_connect_faster_descr">更快地連接到你的好友</string> + <string name="v6_0_delete_many_messages_descr">最多同時刪除20條訊息</string> + <string name="remote_ctrl_error_inactive">PC版非活躍</string> + <string name="servers_info_connected_servers_section_header">已連接的伺服器</string> + <string name="servers_info_detailed_statistics">詳細統計數據</string> + <string name="member_info_member_disabled">已停用</string> + <string name="member_info_member_inactive">不活躍</string> + <string name="connect_via_member_address_alert_title">直接連接?</string> + <string name="wallpaper_preview_hello_alice">午安!</string> + <string name="wallpaper_preview_hello_bob">早安!</string> + <string name="disable_sending_recent_history">不向新成員傳送歷史訊息</string> + <string name="recent_history_is_not_sent_to_new_members">未發送歷史訊息給新成員。</string> + <string name="v5_6_quantum_resistant_encryption_descr">在私聊中開啟(測試版)!</string> + <string name="v5_6_app_data_migration_descr">透過QR code轉移到另一部裝置。</string> + <string name="dont_enable_receipts">不啟用</string> + <string name="error">錯誤</string> + <string name="remote_host_was_disconnected_title">連線停止</string> + <string name="remote_host_disconnected_from"><![CDATA[和移動裝置<b>%s</b>的連結斷開,原因是:%s]]></string> + <string name="remote_ctrl_disconnected_with_reason">斷線原因:%s</string> + <string name="disconnect_remote_hosts">斷開行動裝置連接</string> + <string name="desktop_address">桌面地址</string> + <string name="discover_on_network">通過局域網發現</string> + <string name="remote_host_error_busy"><![CDATA[行動裝置 <b>%s</b> 處理中]]></string> + <string name="remote_host_error_disconnected"><![CDATA[行動裝置 <b>%s</b> 斷開連接]]></string> + <string name="migrate_to_device_downloading_details">下載連結詳情中</string> + <string name="connect_plan_group_already_exists">群組已存在!</string> + <string name="servers_info_modal_error_title">錯誤</string> + <string name="servers_info_downloaded">已下載</string> + <string name="servers_info_reset_stats_alert_error_title">重製統計數據出錯</string> + <string name="expired_label">已過期</string> + <string name="connections">連接數</string> + <string name="created">已建立</string> + <string name="deletion_errors">刪除錯誤</string> + <string name="decryption_errors">解密出錯</string> + <string name="deleted">已刪除</string> + <string name="downloaded_files">已下載的檔案</string> + <string name="download_errors">下載出錯</string> + <string name="possible_slow_function_desc">功能執行所花費的時間過長:%1$d 秒:%2$s</string> + <string name="invalid_name">無效名稱</string> + <string name="correct_name_to">正確名字為 %s?</string> + <string name="copy_error">複製錯誤</string> + <string name="connecting_to_desktop">正在連接到桌面</string> + <string name="found_desktop">找到桌面</string> + <string name="multicast_connect_automatically">自動連接</string> + <string name="remote_ctrl_error_bad_state">與PC版的連接不穩定</string> + <string name="desktop_device">桌上電腦</string> + <string name="remote_ctrl_error_bad_version">已安裝的PC版本不支援。請確認兩台裝置所安裝的版本相同</string> + <string name="remote_ctrl_error_bad_invitation">PC版邀請碼錯誤</string> + <string name="connect_plan_connect_via_link">通過連結連接?</string> + <string name="connect_plan_join_your_group">加入你的群組嗎?</string> + <string name="migrate_to_device_title">轉移到此處</string> + <string name="invalid_file_link">無效連結</string> + <string name="migrate_to_device_finalize_migration">在另一部設備上完成轉移</string> + <string name="migrate_to_device_error_downloading_archive">下載存檔錯誤</string> + <string name="migrate_from_device_to_another_device">轉移到另一部裝置</string> + <string name="migrate_from_device_chat_should_be_stopped">必須停止聊天才能繼續。</string> + <string name="keep_conversation">保留對話</string> + <string name="delete_without_notification">不通知刪除</string> + <string name="invalid_qr_code">無效的QR code</string> + <string name="keep_unused_invitation_question">保留未使用的邀請嗎?</string> + <string name="receipts_contacts_title_disable">停用回執?</string> + <string name="receipts_contacts_title_enable">啟用回執?</string> + <string name="cant_call_contact_connecting_wait_alert_text">正在連接聯絡人,請等待或稍後檢查!</string> + <string name="cant_call_contact_deleted_alert_text">聯絡人被刪除了。</string> + <string name="info_row_debug_delivery">傳送調試</string> + <string name="v5_3_encrypt_local_files">為儲存的檔案和媒體加密</string> + <string name="connect_plan_connect_to_yourself">連接到你自己?</string> + <string name="migrate_from_device_finalize_migration">完成轉移</string> + <string name="current_user">目前配置文件</string> + <string name="servers_info_files_tab">檔案</string> + <string name="servers_info_reconnect_server_error">重連伺服器出錯</string> + <string name="servers_info_reconnect_servers_error">重連伺服器出錯</string> + <string name="duplicates_label">重複</string> + <string name="share_text_file_status">檔案狀態:%s</string> + <string name="permissions_required">授予權限</string> + <string name="permissions_record_audio">麥克風</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">資料庫將被加密,密碼將儲存在設定中</string> + <string name="delete_members_messages__question">刪除成員的 %d 條訊息嗎?</string> + <string name="member_inactive_title">成員非活躍</string> + <string name="compose_message_placeholder">輸入訊息</string> + <string name="delete_messages_mark_deleted_warning">訊息將被標記為刪除。收信人可以揭示這些訊息。</string> + <string name="info_view_connect_button">連接</string> + <string name="info_view_message_button">訊息</string> + <string name="app_check_for_updates_notice_disable">停用</string> + <string name="app_check_for_updates_download_started">下載更新中,請不要關閉應用</string> + <string name="app_check_for_updates_button_download">下載 %s(%s)</string> + <string name="network_smp_proxy_mode_never">從不</string> + <string name="privacy_media_blur_radius_medium">中等</string> + <string name="media_and_file_servers">媒體和檔案伺服器</string> + <string name="message_servers">訊息伺服器</string> + <string name="v5_2_disappear_one_message_descr">即使在對話中禁用。</string> + <string name="v5_2_favourites_filter">更快的發起聊天</string> + <string name="v5_2_fix_encryption_descr">修復還原備份後的加密問題</string> + <string name="v5_7_network">網路管理</string> + <string name="v5_3_new_desktop_app">新的桌面應用!</string> + <string name="servers_info_sessions_connected">已連接</string> + <string name="servers_info_sessions_connecting">連接中</string> + <string name="servers_info_sessions_errors">錯誤</string> + <string name="receipts_groups_title_disable">為群組停用回執?</string> + <string name="past_member_vName">過往的成員 %1$s</string> + <string name="v5_8_private_routing">私密訊息路由 🚀</string> + <string name="paste_archive_link">貼上存檔連結</string> + <string name="open_on_mobile_and_scan_qr_code"><![CDATA[在行動應用中打開<i>從桌面使用</i>並掃描QR code。]]></string> + <string name="no_info_on_delivery">無傳送資訊</string> + <string name="or_show_this_qr_code">或者顯示此碼</string> + <string name="servers_info_subscriptions_connections_pending">待連接</string> + <string name="error_parsing_uri_desc">請檢查 Simple X 鏈接是否正確。</string> + <string name="note_folder_local_display_name">私密筆記</string> + <string name="please_try_later">請稍後再試。</string> + <string name="no_filtered_contacts">沒有過濾的聯絡人</string> + <string name="app_check_for_updates_button_open">打開檔案位置</string> + <string name="rcv_group_event_open_chat">打開</string> + <string name="v5_5_private_notes">私密筆記</string> + <string name="v5_8_persian_ui">波斯語用戶界面</string> + <string name="v6_0_chat_list_media">從聊天列表播放。</string> + <string name="migrate_from_device_database_init">正在準備上傳</string> + <string name="feature_roles_owners">擁有者</string> + <string name="not_compatible">不相容!</string> + <string name="agent_critical_error_desc">請將它報告給開發者: +\n%s +\n +\n建議重啟應用。</string> + <string name="loading_remote_file_desc">從已連接行動裝置加載檔案時請稍候片刻</string> + <string name="or_scan_qr_code">或者掃描QR code</string> + <string name="prohibit_sending_simplex_links">禁止傳送 SimpleX 連結</string> + <string name="no_selected_chat">沒有選擇聊天</string> + <string name="permissions_open_settings">打開設定</string> + <string name="settings_section_title_user_theme">這人資料主題</string> + <string name="settings_section_title_profile_images">個人資料圖片</string> + <string name="only_one_device_can_work_at_the_same_time">同一時刻只有一台裝置可工作</string> + <string name="protect_ip_address">保護 IP 地址</string> + <string name="network_type_other">其他</string> + <string name="network_type_no_network_connection">無網路連接</string> + <string name="no_history">無歷史記錄</string> + <string name="no_filtered_chats">無過濾聊天</string> + <string name="agent_internal_error_desc">請將它報告給開發者: +\n%s</string> + <string name="turn_off_system_restriction_button">打開應用程式設定</string> + <string name="auth_open_migration_to_another_device">開啟轉移畫面</string> + <string name="shutdown_alert_desc">通知將停止,直到您重啟應用程式</string> + <string name="prohibit_sending_files">禁止傳送檔案和媒體。</string> + <string name="connect_plan_open_group">打開群組</string> + <string name="only_owners_can_enable_files_and_media">只有群組所有者才能啟用檔案和媒體。</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">貼上你收到的連結以與你的聯絡人聯絡…</string> + <string name="servers_info_previously_connected_servers_section_header">先前連接的伺服器</string> + <string name="other_label">其他</string> + <string name="message_queue_info_none">無</string> + <string name="v5_4_more_things_descr">- 可選擇通知已刪除的聯絡人。 +\n- 帶空格的個人資料名稱。 +\n- 以及更多!</string> + <string name="v5_5_simpler_connect_ui">貼上連結以連接!</string> + <string name="paste_desktop_address">貼上桌面地址</string> + <string name="migrate_to_device_confirm_network_settings_footer">請確認此裝置的網路設定是否正確。</string> + <string name="other_errors">其他錯誤</string> + <string name="remote_ctrl_connection_stopped_desc">請檢查行動裝置和桌面設備連接到的是同一個本地網絡,且桌面防火牆允許連接。 +\n請和開發者分享任何其他問題。</string> + <string name="open_port_in_firewall_title">在防火牆中打開端口</string> + <string name="or_paste_archive_link">或貼上存檔連結</string> + <string name="migrate_to_device_database_init">正在準備下載</string> + <string name="only_delete_conversation">只刪除對話</string> + <string name="paste_link">貼上連結</string> + <string name="smp_servers_other">其他 SMP 伺服器</string> + <string name="xftp_servers_other">其他 XFTP 伺服器</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">請讓你的聯絡人啟用通話。</string> + <string name="v5_6_picture_in_picture_calls">畫中畫通話</string> + <string name="migrate_from_device_or_share_this_file_link">或安全分享此文件連結</string> + <string name="servers_info_missing">無資訊,試試重新加載</string> + <string name="open_server_settings_button">打開伺服器設定</string> + <string name="private_routing_error">私密路由出錯</string> + <string name="message_forwarded_desc">尚無直接連接,訊息由管理員轉發。</string> + <string name="selected_chat_items_nothing_selected">什麼也沒選中</string> + <string name="info_view_open_button">打開</string> + <string name="network_smp_proxy_mode_private_routing">私密路由</string> + <string name="open_database_folder">打開資料庫文件夾</string> + <string name="settings_section_title_private_message_routing">私密訊息路由</string> + <string name="app_check_for_updates_installed_successfully_desc">請重啟應用程式。</string> + <string name="privacy_media_blur_radius_off">關閉</string> + <string name="v5_6_quantum_resistant_encryption">抗量子加密</string> + <string name="profile_update_event_removed_address">刪除了聯繫地址</string> + <string name="servers_info_reconnect_server_title">重連伺服器?</string> + <string name="servers_info_detailed_statistics_received_messages_header">接收到的訊息</string> + <string name="servers_info_detailed_statistics_receive_errors">接收錯誤</string> + <string name="servers_info_reconnect_servers_message">重新連接所有已連接的伺服器來強制傳送訊息。這會使用額外流量。</string> + <string name="servers_info_reconnect_server_message">重連伺服器強制傳送訊息。這會使用額外流量。</string> + <string name="network_option_rcv_concurrency">並行接收</string> + <string name="recipients_can_not_see_who_message_from">收件人看不到這條訊息來自誰。</string> + <string name="profile_update_event_removed_picture">刪除了資料圖片</string> + <string name="one_hand_ui">可使用的聊天工具箱</string> + <string name="v6_0_reachable_chat_toolbar">可存取的聊天工具欄</string> + <string name="v5_5_join_group_conversation_descr">最近歷史和改進的目錄機器人。</string> + <string name="network_option_protocol_timeout_per_kb">每 KB 協議超時</string> + <string name="v5_8_private_routing_descr">保護您的真實 IP 地址。不讓你聯絡人選擇的訊息中繼看到它。 +\n在*網絡&伺服器*設定中開啓。</string> + <string name="you_can_change_it_later">隨機密碼以明文形式儲存在設定中。 +\n您可以稍後更改。</string> + <string name="send_receipts_disabled_alert_title">傳送回條已禁用</string> + <string name="servers_info_proxied_servers_section_header">代理伺服器</string> + <string name="servers_info_reconnect_servers_title">重連伺服器?</string> + <string name="conn_event_enabled_pq">抗量子端到端加密</string> + <string name="color_received_quote">收到的回覆</string> + <string name="servers_info_reconnect_all_servers_button">重連所有伺服器</string> + <string name="reconnect">重連</string> + <string name="proxied">代理</string> + <string name="random_port">隨機</string> + <string name="refresh_qr_code">更新</string> + <string name="servers_info_detailed_statistics_received_total">接收總計</string> + <string name="app_check_for_updates_button_remind_later">稍後提醒</string> +</resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/android/call.html b/apps/multiplatform/common/src/commonMain/resources/assets/www/android/call.html index 7b51a05151..51815e2995 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/android/call.html +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/android/call.html @@ -6,20 +6,41 @@ <script src="../lz-string.min.js"></script> </head> <body> + <video + id="remote-screen-video-stream" + class="inline" + playsinline + poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" + style="visibility: hidden" + onclick="javascript:toggleRemoteScreenVideoFitFill()" + ></video> + <video id="remote-video-stream" + class="inline" autoplay playsinline poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" onclick="javascript:toggleRemoteVideoFitFill()" ></video> + <video id="local-video-stream" + class="inline" muted autoplay playsinline poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" ></video> + + <video + id="local-screen-video-stream" + class="inline" + muted + playsinline + poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" + style="visibility: hidden" + ></video> </body> <footer> <script src="../call.js"></script> diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/android/style.css b/apps/multiplatform/common/src/commonMain/resources/assets/www/android/style.css index 3d2941c71e..377458c184 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/android/style.css +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/android/style.css @@ -5,24 +5,143 @@ body { background-color: black; } -#remote-video-stream { +#remote-video-stream.inline { position: absolute; width: 100%; height: 100%; object-fit: cover; } -#local-video-stream { +@media (orientation: portrait) { + #remote-video-stream.collapsed { + position: absolute; + width: 30%; + max-width: 30%; + height: 39.9vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + bottom: 80px; + right: 0; + } +} + +@media (orientation: landscape) { + #remote-video-stream.collapsed { + position: absolute; + width: 20%; + max-width: 20%; + height: 15.03vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + bottom: 80px; + right: 0; + } +} + +@media (orientation: portrait) { + #remote-video-stream.collapsed-pip { + position: absolute; + width: 50%; + max-width: 50%; + height: 66.5vw; + object-fit: cover; + margin: 8px; + border-radius: 8px; + bottom: 0; + right: 0; + } +} + +@media (orientation: landscape) { + #remote-video-stream.collapsed-pip { + position: absolute; + width: 50%; + max-width: 50%; + height: 37.59vw; + object-fit: cover; + margin: 8px; + border-radius: 8px; + bottom: 0; + right: 0; + } +} + +#remote-screen-video-stream.inline { + position: absolute; + width: 100%; + height: 100%; + object-fit: cover; +} + +@media (orientation: portrait) { + #local-video-stream.inline { + position: absolute; + width: 30%; + max-width: 30%; + height: 39.9vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + top: 0; + right: 0; + } +} + +@media (orientation: landscape) { + #local-video-stream.inline { + position: absolute; + width: 20%; + max-width: 20%; + height: 15.03vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + top: 0; + right: 0; + } +} + +#local-screen-video-stream.inline { position: absolute; width: 30%; max-width: 30%; object-fit: cover; margin: 16px; border-radius: 16px; - top: 0; + top: 30%; right: 0; } +#remote-video-stream.fullscreen { + position: absolute; + height: 100%; + width: 100%; + object-fit: cover; +} + +#remote-screen-video-stream.fullscreen { + position: absolute; + height: 100%; + width: 100%; + object-fit: cover; +} + +#local-video-stream.fullscreen { + position: absolute; + height: 100%; + width: 100%; + object-fit: cover; +} + +#local-screen-video-stream.fullscreen { + position: absolute; + height: 100%; + width: 100%; + object-fit: cover; +} + *::-webkit-media-controls { display: none !important; -webkit-appearance: none !important; diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js b/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js index 6092eb39e5..7ab8d6fdd6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js @@ -6,15 +6,32 @@ var CallMediaType; CallMediaType["Audio"] = "audio"; CallMediaType["Video"] = "video"; })(CallMediaType || (CallMediaType = {})); +var CallMediaSource; +(function (CallMediaSource) { + CallMediaSource["Mic"] = "mic"; + CallMediaSource["Camera"] = "camera"; + CallMediaSource["ScreenAudio"] = "screenAudio"; + CallMediaSource["ScreenVideo"] = "screenVideo"; + CallMediaSource["Unknown"] = "unknown"; +})(CallMediaSource || (CallMediaSource = {})); var VideoCamera; (function (VideoCamera) { VideoCamera["User"] = "user"; VideoCamera["Environment"] = "environment"; })(VideoCamera || (VideoCamera = {})); +var LayoutType; +(function (LayoutType) { + LayoutType["Default"] = "default"; + LayoutType["LocalVideo"] = "localVideo"; + LayoutType["RemoteVideo"] = "remoteVideo"; +})(LayoutType || (LayoutType = {})); // for debugging // var sendMessageToNative = ({resp}: WVApiMessage) => console.log(JSON.stringify({command: resp})) var sendMessageToNative = (msg) => console.log(JSON.stringify(msg)); var toggleScreenShare = async () => { }; +var localOrPeerMediaSourcesChanged = (_call) => { }; +var inactiveCallMediaSourcesChanged = (_inactiveCallMediaSources) => { }; +var failedToGetPermissions = (_title, _description) => { }; // Global object with cryptrographic/encoding functions const callCrypto = callCryptoFunction(); var TransformOperation; @@ -22,17 +39,37 @@ var TransformOperation; TransformOperation["Encrypt"] = "encrypt"; TransformOperation["Decrypt"] = "decrypt"; })(TransformOperation || (TransformOperation = {})); +function localMedia(call) { + return call.localMediaSources.camera || call.localMediaSources.screenVideo ? CallMediaType.Video : CallMediaType.Audio; +} +function peerMedia(call) { + return call.peerMediaSources.camera || call.peerMediaSources.screenVideo ? CallMediaType.Video : CallMediaType.Audio; +} +let inactiveCallMediaSources = { + mic: false, + camera: false, + screenAudio: false, + screenVideo: false, +}; let activeCall; +let notConnectedCall; let answerTimeout = 30000; var useWorker = false; var isDesktop = false; var localizedState = ""; var localizedDescription = ""; +// Passing true here will send audio in screen record stream +const allowSendScreenAudio = false; +// When one side of a call sends candidates tot fast (until local & remote descriptions are set), that candidates +// will be stored here and then set when the call will be ready to process them +let afterCallInitializedCandidates = []; +const stopTrackOnAndroid = false; const processCommand = (function () { const defaultIceServers = [ + { urls: ["stuns:stun.simplex.im:443"] }, { urls: ["stun:stun.simplex.im:443"] }, - { urls: ["turn:turn.simplex.im:443?transport=udp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" }, - { urls: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" }, + //{urls: ["turns:turn.simplex.im:443?transport=udp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj"}, + { urls: ["turns:turn.simplex.im:443?transport=tcp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj" }, ]; function getCallConfig(encodedInsertableStreams, iceServers, relay) { return { @@ -41,6 +78,8 @@ const processCommand = (function () { iceCandidatePoolSize: 10, encodedInsertableStreams, iceTransportPolicy: relay ? "relay" : "all", + // needed for Android WebView >= 69 && <= 72 where default was "plan-b" which is incompatible with transceivers + sdpSemantics: "unified-plan", }, iceCandidates: { delay: 750, @@ -104,31 +143,75 @@ const processCommand = (function () { }); } async function initializeCall(config, mediaType, aesKey) { - const pc = new RTCPeerConnection(config.peerConnectionConfig); - const remoteStream = new MediaStream(); - const localCamera = VideoCamera.User; - const localStream = await getLocalMediaStream(mediaType, localCamera); - if (isDesktop) { - localStream - .getTracks() - .filter((elem) => elem.kind == "video") - .forEach((elem) => (elem.enabled = false)); + var _a, _b, _c; + let pc; + try { + pc = new RTCPeerConnection(config.peerConnectionConfig); } + catch (e) { + console.log("Error while constructing RTCPeerConnection, will try without 'stuns' specified: " + e); + const withoutStuns = (_a = config.peerConnectionConfig.iceServers) === null || _a === void 0 ? void 0 : _a.filter((elem) => typeof elem.urls === "string" ? !elem.urls.startsWith("stuns:") : !elem.urls.some((url) => url.startsWith("stuns:"))); + config.peerConnectionConfig.iceServers = withoutStuns; + pc = new RTCPeerConnection(config.peerConnectionConfig); + } + const remoteStream = new MediaStream(); + const remoteScreenStream = new MediaStream(); + const localCamera = (_b = notConnectedCall === null || notConnectedCall === void 0 ? void 0 : notConnectedCall.localCamera) !== null && _b !== void 0 ? _b : VideoCamera.User; + let localStream; + try { + localStream = (notConnectedCall === null || notConnectedCall === void 0 ? void 0 : notConnectedCall.localStream) + ? notConnectedCall.localStream + : await getLocalMediaStream(inactiveCallMediaSources.mic, inactiveCallMediaSources.camera && (await browserHasCamera()), localCamera); + } + catch (e) { + console.log("Error while getting local media stream", e); + if (isDesktop) { + desktopShowPermissionsAlert(mediaType); + localStream = new MediaStream(); + } + else { + // On Android all streams should be present + throw e; + } + } + const localScreenStream = new MediaStream(); + // Will become video when any video tracks will be added const iceCandidates = getIceCandidates(pc, config); const call = { connection: pc, iceCandidates, - localMedia: mediaType, + localMediaSources: { + mic: localStream.getAudioTracks().length > 0, + camera: localStream.getVideoTracks().length > 0, + screenAudio: localScreenStream.getAudioTracks().length > 0, + screenVideo: localScreenStream.getVideoTracks().length > 0, + }, localCamera, localStream, + localScreenStream, remoteStream, + remoteTracks: new Map(), + remoteScreenStream, + peerMediaSources: { + mic: false, + camera: false, + screenAudio: false, + screenVideo: false, + }, aesKey, - screenShareEnabled: false, - cameraEnabled: true, + layout: (_c = notConnectedCall === null || notConnectedCall === void 0 ? void 0 : notConnectedCall.layout) !== null && _c !== void 0 ? _c : LayoutType.Default, + cameraTrackWasSetBefore: localStream.getVideoTracks().length > 0, + peerHasOldVersion: false, }; + localOrPeerMediaSourcesChanged(call); await setupMediaStreams(call); let connectionTimeout = setTimeout(connectionHandler, answerTimeout); - pc.addEventListener("connectionstatechange", connectionStateChange); + if (pc.connectionState) { + pc.addEventListener("connectionstatechange", connectionStateChange); + } + else { + pc.addEventListener("iceconnectionstatechange", connectionStateChange); + } return call; async function connectionStateChange() { // "failed" means the second party did not answer in time (15 sec timeout in Chrome WebView) @@ -137,26 +220,38 @@ const processCommand = (function () { connectionHandler(); } async function connectionHandler() { + var _a; sendMessageToNative({ resp: { type: "connection", state: { - connectionState: pc.connectionState, + connectionState: (_a = pc.connectionState) !== null && _a !== void 0 ? _a : (pc.iceConnectionState != "completed" && pc.iceConnectionState != "checking" + ? pc.iceConnectionState + : pc.iceConnectionState == "completed" + ? "connected" + : "connecting") /* webView 69-70 doesn't have connectionState yet */, iceConnectionState: pc.iceConnectionState, iceGatheringState: pc.iceGatheringState, signalingState: pc.signalingState, }, }, }); - if (pc.connectionState == "disconnected" || pc.connectionState == "failed") { + if (pc.connectionState == "disconnected" || + pc.connectionState == "failed" || + (!pc.connectionState && (pc.iceConnectionState == "disconnected" || pc.iceConnectionState == "failed"))) { clearConnectionTimeout(); - pc.removeEventListener("connectionstatechange", connectionStateChange); + if (pc.connectionState) { + pc.removeEventListener("connectionstatechange", connectionStateChange); + } + else { + pc.removeEventListener("iceconnectionstatechange", connectionStateChange); + } if (activeCall) { setTimeout(() => sendMessageToNative({ resp: { type: "ended" } }), 0); } endCall(); } - else if (pc.connectionState == "connected") { + else if (pc.connectionState == "connected" || (!pc.connectionState && pc.iceConnectionState == "connected")) { clearConnectionTimeout(); const stats = (await pc.getStats()); for (const stat of stats.values()) { @@ -191,6 +286,7 @@ const processCommand = (function () { return JSON.parse(LZString.decompressFromBase64(s)); } async function processCommand(body) { + var _a; const { corrId, command } = body; const pc = activeCall === null || activeCall === void 0 ? void 0 : activeCall.connection; let resp; @@ -200,9 +296,34 @@ const processCommand = (function () { console.log("starting outgoing call - capabilities"); if (activeCall) endCall(); - // This request for local media stream is made to prompt for camera/mic permissions on call start - if (command.media) - await getLocalMediaStream(command.media, VideoCamera.User); + let localStream = null; + try { + localStream = await getLocalMediaStream(true, command.media == CallMediaType.Video && (await browserHasCamera()), VideoCamera.User); + const videos = getVideoElements(); + if (videos) { + setupLocalVideoRatio(videos.local); + videos.local.srcObject = localStream; + videos.local.play().catch((e) => console.log(e)); + } + } + catch (e) { + console.log(e); + // Do not allow to continue the call without audio permission + resp = { type: "error", message: "capabilities: no permissions were granted for mic and/or camera" }; + break; + localStream = new MediaStream(); + // Will be shown on the next stage of call estabilishing, can work without any streams + //desktopShowPermissionsAlert(command.media) + } + // Specify defaults that can be changed via UI before call estabilished. It's only used before activeCall instance appears + inactiveCallMediaSources.mic = localStream != null && localStream.getAudioTracks().length > 0; + inactiveCallMediaSources.camera = localStream != null && localStream.getVideoTracks().length > 0; + inactiveCallMediaSourcesChanged(inactiveCallMediaSources); + notConnectedCall = { + localCamera: VideoCamera.User, + localStream: localStream, + layout: LayoutType.Default, + }; const encryption = supportsInsertableStreams(useWorker); resp = { type: "capabilities", capabilities: { encryption } }; break; @@ -210,13 +331,25 @@ const processCommand = (function () { console.log("starting incoming call - create webrtc session"); if (activeCall) endCall(); + // It can be already defined on Android when switching calls (if the previous call was outgoing) + notConnectedCall = undefined; + inactiveCallMediaSources.mic = true; + inactiveCallMediaSources.camera = command.media == CallMediaType.Video; + inactiveCallMediaSourcesChanged(inactiveCallMediaSources); + setupLocalVideoRatio(getVideoElements().local); const { media, iceServers, relay } = command; const encryption = supportsInsertableStreams(useWorker); const aesKey = encryption ? command.aesKey : undefined; activeCall = await initializeCall(getCallConfig(encryption && !!aesKey, iceServers, relay), media, aesKey); + await setupLocalStream(true, activeCall); + setupCodecPreferences(activeCall); const pc = activeCall.connection; const offer = await pc.createOffer(); await pc.setLocalDescription(offer); + // should be called after setLocalDescription in order to have transceiver.mid set + setupEncryptionForLocalStream(activeCall); + addIceCandidates(pc, afterCallInitializedCandidates); + afterCallInitializedCandidates = []; // for debugging, returning the command for callee to use // resp = { // type: "offer", @@ -251,10 +384,25 @@ const processCommand = (function () { activeCall = await initializeCall(getCallConfig(!!aesKey, iceServers, relay), media, aesKey); const pc = activeCall.connection; // console.log("offer remoteIceCandidates", JSON.stringify(remoteIceCandidates)) - await pc.setRemoteDescription(new RTCSessionDescription(offer)); - const answer = await pc.createAnswer(); + await pc.setRemoteDescription(new RTCSessionDescription(!webView69Or70() ? offer : adaptSdpToOldWebView(offer))); + // setting up local stream only after setRemoteDescription in order to have transceivers set + await setupLocalStream(false, activeCall); + setupEncryptionForLocalStream(activeCall); + setupCodecPreferences(activeCall); + // enable using the same transceivers for sending media too, so total number of transceivers will be: audio, camera, screen audio, screen video + pc.getTransceivers().forEach((elem) => (elem.direction = "sendrecv")); + // setting media streams after remote description in order to have all transceivers ready (so ordering will be preserved) + // console.log( + // "Transceivers", + // pc.getTransceivers(), + // pc.getTransceivers().map((elem) => "" + elem.mid + " " + elem.sender.track?.kind + " " + elem.sender.track?.label) + // ) + adaptToOldVersion(pc.getTransceivers().length <= 2, activeCall); + let answer = await pc.createAnswer(); await pc.setLocalDescription(answer); addIceCandidates(pc, remoteIceCandidates); + addIceCandidates(pc, afterCallInitializedCandidates); + afterCallInitializedCandidates = []; // same as command for caller to use resp = { type: "answer", @@ -278,40 +426,92 @@ const processCommand = (function () { const answer = parse(command.answer); const remoteIceCandidates = parse(command.iceCandidates); // console.log("answer remoteIceCandidates", JSON.stringify(remoteIceCandidates)) - await pc.setRemoteDescription(new RTCSessionDescription(answer)); + await pc.setRemoteDescription(new RTCSessionDescription(!webView69Or70() ? answer : adaptSdpToOldWebView(answer))); + adaptToOldVersion(pc.getTransceivers()[2].currentDirection == "sendonly", activeCall); addIceCandidates(pc, remoteIceCandidates); + addIceCandidates(pc, afterCallInitializedCandidates); + afterCallInitializedCandidates = []; resp = { type: "ok" }; } break; case "ice": + const remoteIceCandidates = parse(command.iceCandidates); if (pc) { - const remoteIceCandidates = parse(command.iceCandidates); addIceCandidates(pc, remoteIceCandidates); resp = { type: "ok" }; } else { - resp = { type: "error", message: "ice: call not started" }; + afterCallInitializedCandidates.push(...remoteIceCandidates); + resp = { type: "error", message: "ice: call not started yet, will add candidates later" }; } break; case "media": if (!activeCall) { - resp = { type: "error", message: "media: call not started" }; + if (!notConnectedCall) { + // call can have a slow startup and be in this place even before "capabilities" stage + resp = { type: "error", message: "media: call has not yet pass capabilities stage" }; + break; + } + switch (command.source) { + case CallMediaSource.Mic: + inactiveCallMediaSources.mic = command.enable; + break; + case CallMediaSource.Camera: + inactiveCallMediaSources.camera = command.enable; + break; + case CallMediaSource.ScreenAudio: + inactiveCallMediaSources.screenAudio = command.enable; + break; + case CallMediaSource.ScreenVideo: + inactiveCallMediaSources.screenVideo = command.enable; + break; + } + inactiveCallMediaSourcesChanged(inactiveCallMediaSources); + recreateLocalStreamWhileNotConnected((_a = notConnectedCall === null || notConnectedCall === void 0 ? void 0 : notConnectedCall.localCamera) !== null && _a !== void 0 ? _a : VideoCamera.User); + resp = { type: "ok" }; } - else if (activeCall.localMedia == CallMediaType.Audio && command.media == CallMediaType.Video) { - resp = { type: "error", message: "media: no video" }; + else if (activeCall.peerHasOldVersion && command.source == CallMediaSource.Camera && !activeCall.peerMediaSources.camera) { + resp = { type: "error", message: "media: cannot enable camera since the peer has an old version" }; + } + else if (!activeCall.cameraTrackWasSetBefore && command.source == CallMediaSource.Camera && command.enable) { + await startSendingCamera(activeCall, activeCall.localCamera); + resp = { type: "ok" }; + } + else if ((command.source == CallMediaSource.Mic && activeCall.localStream.getAudioTracks().length > 0) || + (command.source == CallMediaSource.Camera && activeCall.localStream.getVideoTracks().length > 0)) { + if (enableMedia(activeCall.localStream, command.source, command.enable)) { + resp = { type: "ok" }; + } + else { + resp = { type: "error", message: "media: cannot enable media source" }; + } } else { - enableMedia(activeCall.localStream, command.media, command.enable); - resp = { type: "ok" }; + if (await replaceMedia(activeCall, command.source, command.enable, activeCall.localCamera)) { + resp = { type: "ok" }; + } + else { + resp = { type: "error", message: "media: cannot replace media source" }; + } } break; case "camera": if (!activeCall || !pc) { - resp = { type: "error", message: "camera: call not started" }; + if (notConnectedCall) { + recreateLocalStreamWhileNotConnected(command.camera); + resp = { type: "ok" }; + } + else { + resp = { type: "error", message: "camera: call has not yet pass capabilities stage" }; + } } else { - await replaceMedia(activeCall, command.camera); - resp = { type: "ok" }; + if (await replaceMedia(activeCall, CallMediaSource.Camera, true, command.camera)) { + resp = { type: "ok" }; + } + else { + resp = { type: "error", message: "camera: cannot replace media source" }; + } } break; case "description": @@ -319,10 +519,24 @@ const processCommand = (function () { localizedDescription = command.description; resp = { type: "ok" }; break; + case "layout": + if (activeCall) { + activeCall.layout = command.layout; + } + else if (notConnectedCall) { + notConnectedCall.layout = command.layout; + } + changeLayout(command.layout); + resp = { type: "ok" }; + break; case "end": endCall(); resp = { type: "ok" }; break; + case "permission": + failedToGetPermissions(command.title, permissionDescription(command)); + resp = { type: "ok" }; + break; default: resp = { type: "error", message: "unknown command" }; break; @@ -337,13 +551,13 @@ const processCommand = (function () { } function endCall() { var _a; + shutdownCameraAndMic(); try { (_a = activeCall === null || activeCall === void 0 ? void 0 : activeCall.connection) === null || _a === void 0 ? void 0 : _a.close(); } catch (e) { console.log(e); } - shutdownCameraAndMic(); activeCall = undefined; resetVideoElements(); } @@ -358,13 +572,14 @@ const processCommand = (function () { if (!videos) throw Error("no video elements"); await setupEncryptionWorker(call); - setupLocalStream(call); setupRemoteStream(call); - setupCodecPreferences(call); - // setupVideoElement(videos.local) - // setupVideoElement(videos.remote) - videos.local.srcObject = call.localStream; + videos.localScreen.srcObject = call.localScreenStream; videos.remote.srcObject = call.remoteStream; + videos.remoteScreen.srcObject = call.remoteScreenStream; + // videos.localScreen.play() + // For example, exception can be: NotAllowedError: play() failed because the user didn't interact with the document first + videos.remote.play().catch((e) => console.log(e)); + videos.remoteScreen.play().catch((e) => console.log(e)); } async function setupEncryptionWorker(call) { if (call.aesKey) { @@ -378,19 +593,86 @@ const processCommand = (function () { } } } - function setupLocalStream(call) { + async function setupLocalStream(incomingCall, call) { + var _a, _b, _c, _d; const videos = getVideoElements(); if (!videos) throw Error("no video elements"); const pc = call.connection; let { localStream } = call; - for (const track of localStream.getTracks()) { - pc.addTrack(track, localStream); + const transceivers = call.connection.getTransceivers(); + const audioTracks = localStream.getAudioTracks(); + const videoTracks = localStream.getVideoTracks(); + if (incomingCall) { + // incoming call, no transceivers yet. But they should be added in order: mic, camera, screen audio, screen video + // mid = 0 + const audioTransceiver = pc.addTransceiver("audio", { streams: [localStream] }); + if (audioTracks.length != 0) { + audioTransceiver.sender.replaceTrack(audioTracks[0]); + } + // mid = 1 + const videoTransceiver = pc.addTransceiver("video", { streams: [localStream] }); + if (videoTracks.length != 0) { + videoTransceiver.sender.replaceTrack(videoTracks[0]); + } + if (call.localScreenStream.getAudioTracks().length == 0) { + // mid = 2 + pc.addTransceiver("audio", { streams: [call.localScreenStream] }); + } + if (call.localScreenStream.getVideoTracks().length == 0) { + // mid = 3 + pc.addTransceiver("video", { streams: [call.localScreenStream] }); + } } + else { + // new version + if (transceivers.length > 2) { + // Outgoing call. All transceivers are ready. Don't addTrack() because it will create new transceivers, replace existing (null) tracks + await ((_b = (_a = transceivers + .find((elem) => mediaSourceFromTransceiverMid(elem.mid) == CallMediaSource.Mic)) === null || _a === void 0 ? void 0 : _a.sender) === null || _b === void 0 ? void 0 : _b.replaceTrack(audioTracks[0])); + await ((_d = (_c = transceivers + .find((elem) => mediaSourceFromTransceiverMid(elem.mid) == CallMediaSource.Camera)) === null || _c === void 0 ? void 0 : _c.sender) === null || _d === void 0 ? void 0 : _d.replaceTrack(videoTracks[0])); + } + else { + // old version, only two transceivers + for (const track of localStream.getTracks()) { + pc.addTrack(track, localStream); + } + } + } + // src can be set to notConnectedCall.localStream which is the same as call.localStream + if (!videos.local.srcObject) { + videos.local.srcObject = call.localStream; + } + // Without doing it manually Firefox shows black screen but video can be played in Picture-in-Picture + videos.local.play().catch((e) => console.log(e)); + } + function setupLocalVideoRatio(local) { + local.addEventListener("loadedmetadata", function () { + console.log("Local video videoWidth: " + local.videoWidth + "px, videoHeight: " + local.videoHeight + "px"); + if (local.videoWidth == 0 || local.videoHeight == 0) + return; + const ratio = local.videoWidth > local.videoHeight ? 0.2 : 0.3; + local.style.height = `${(ratio / (local.videoWidth / local.videoHeight)) * 100}vw`; + }); + local.onresize = function () { + console.log("Local video size changed to " + local.videoWidth + "x" + local.videoHeight); + if (local.videoWidth == 0 || local.videoHeight == 0) + return; + const ratio = local.videoWidth > local.videoHeight ? 0.2 : 0.3; + local.style.height = `${(ratio / (local.videoWidth / local.videoHeight)) * 100}vw`; + }; + } + function setupEncryptionForLocalStream(call) { if (call.aesKey && call.key) { + const pc = call.connection; console.log("set up encryption for sending"); - for (const sender of pc.getSenders()) { - setupPeerTransform(TransformOperation.Encrypt, sender, call.worker, call.aesKey, call.key); + let mid = 0; + for (const transceiver of pc.getTransceivers()) { + const sender = transceiver.sender; + const source = mediaSourceFromTransceiverMid(mid.toString()); + setupPeerTransform(TransformOperation.Encrypt, sender, call.worker, call.aesKey, call.key, source == CallMediaSource.Camera || source == CallMediaSource.ScreenVideo ? CallMediaType.Video : CallMediaType.Audio); + mid++; } } } @@ -398,16 +680,16 @@ const processCommand = (function () { // Pull tracks from remote stream as they arrive add them to remoteStream video const pc = call.connection; pc.ontrack = (event) => { + const track = event.track; + //console.log("On track", event) try { if (call.aesKey && call.key) { console.log("set up decryption for receiving"); - setupPeerTransform(TransformOperation.Decrypt, event.receiver, call.worker, call.aesKey, call.key); - } - for (const stream of event.streams) { - for (const track of stream.getTracks()) { - call.remoteStream.addTrack(track); - } + setupPeerTransform(TransformOperation.Decrypt, event.receiver, call.worker, call.aesKey, call.key, event.receiver.track.kind == "video" ? CallMediaType.Video : CallMediaType.Audio); } + setupMuteUnmuteListener(event.transceiver, track); + const mediaSource = mediaSourceFromTransceiverMid(event.transceiver.mid); + call.remoteTracks.set(mediaSource, track); console.log(`ontrack success`); } catch (e) { @@ -428,72 +710,239 @@ const processCommand = (function () { // which is 10 bytes for key frames and 3 bytes for delta frames. // For opus (where encodedFrame.type is not set) this is the TOC byte from // https://tools.ietf.org/html/rfc6716#section-3.1 - var _a; - const capabilities = RTCRtpSender.getCapabilities("video"); + // Using RTCRtpReceiver instead of RTCRtpSender, see these lines: + // - if (!is_recv_codec && !is_send_codec) { + // + if (!is_recv_codec) { + // https://webrtc.googlesource.com/src.git/+/db2f52ba88cf9f98211df2dabb3f8aca9251c4a2%5E%21/ + const capabilities = RTCRtpReceiver.getCapabilities("video"); if (capabilities) { const { codecs } = capabilities; const selectedCodecIndex = codecs.findIndex((c) => c.mimeType === "video/VP8"); const selectedCodec = codecs[selectedCodecIndex]; codecs.splice(selectedCodecIndex, 1); codecs.unshift(selectedCodec); + // On this stage transceiver.mid may not be set so using a sequence starting from 0 to decide which track.kind is inside + let mid = 0; for (const t of call.connection.getTransceivers()) { - if (((_a = t.sender.track) === null || _a === void 0 ? void 0 : _a.kind) === "video") { - t.setCodecPreferences(codecs); + // Firefox doesn't have this function implemented: + // https://bugzilla.mozilla.org/show_bug.cgi?id=1396922 + const source = mediaSourceFromTransceiverMid(mid.toString()); + if ((source == CallMediaSource.Camera || source == CallMediaSource.ScreenVideo) && t.setCodecPreferences) { + try { + t.setCodecPreferences(codecs); + } + catch (error) { + // Shouldn't be here but in case something goes wrong, it will allow to make a call with auto-selected codecs + console.log("Failed to set codec preferences, trying without any preferences: " + error); + } } + mid++; } } } - async function replaceMedia(call, camera) { + async function startSendingCamera(call, camera) { + console.log("Starting sending video..."); const videos = getVideoElements(); if (!videos) throw Error("no video elements"); const pc = call.connection; - const oldAudioTracks = call.localStream.getAudioTracks(); - const audioWasEnabled = oldAudioTracks.some((elem) => elem.enabled); + // Taking the first video transceiver and use it for sending video from camera. Following tracks are for other purposes + const tc = pc.getTransceivers().find((tc) => tc.receiver.track.kind == "video" && tc.direction == "sendrecv"); + if (!tc) { + console.log("No camera transceiver. Probably, calling to an old version"); + return; + } + console.log(pc.getTransceivers().map((elem) => { var _a, _b; return "" + ((_a = elem.sender.track) === null || _a === void 0 ? void 0 : _a.kind) + " " + ((_b = elem.receiver.track) === null || _b === void 0 ? void 0 : _b.kind) + " " + elem.direction; })); let localStream; try { - localStream = call.screenShareEnabled ? await getLocalScreenCaptureStream() : await getLocalMediaStream(call.localMedia, camera); + localStream = await getLocalMediaStream(call.localMediaSources.mic, true, camera); + for (const t of localStream.getVideoTracks()) { + call.localStream.addTrack(t); + tc === null || tc === void 0 ? void 0 : tc.sender.replaceTrack(t); + localStream.removeTrack(t); + } + call.localMediaSources.camera = true; + call.cameraTrackWasSetBefore = true; + localOrPeerMediaSourcesChanged(call); + changeLayout(call.layout); } catch (e) { - if (call.screenShareEnabled) { - call.screenShareEnabled = false; + console.log("Start sending camera error", e); + desktopShowPermissionsAlert(CallMediaType.Video); + return; + } + // Without doing it manually Firefox shows black screen but video can be played in Picture-in-Picture + videos.local.play().catch((e) => console.log(e)); + } + toggleScreenShare = async function () { + const call = activeCall; + if (!call) + return; + const videos = getVideoElements(); + if (!videos) + throw Error("no video elements"); + const pc = call.connection; + if (!call.localMediaSources.screenVideo) { + let localScreenStream; + try { + localScreenStream = await getLocalScreenCaptureStream(); } - return; + catch (e) { + return; + } + for (const t of localScreenStream.getTracks()) + call.localScreenStream.addTrack(t); + for (const t of localScreenStream.getTracks()) + localScreenStream.removeTrack(t); + pc.getTransceivers().forEach((elem) => { + const source = mediaSourceFromTransceiverMid(elem.mid); + const screenAudioTrack = call.localScreenStream.getTracks().find((elem) => elem.kind == "audio"); + const screenVideoTrack = call.localScreenStream.getTracks().find((elem) => elem.kind == "video"); + if (source == CallMediaSource.ScreenAudio && screenAudioTrack) { + elem.sender.replaceTrack(screenAudioTrack); + console.log("Replaced audio screen track"); + } + else if (source == CallMediaSource.ScreenVideo && screenVideoTrack) { + elem.sender.replaceTrack(screenVideoTrack); + screenVideoTrack.onended = () => { + console.log("Ended screen video track"); + toggleScreenShare(); + }; + console.log("Replaced screen video track"); + } + }); + // videos.localScreen.pause() + // videos.localScreen.srcObject = call.localScreenStream + videos.localScreen.play().catch((e) => console.log(e)); + } + else { + pc.getTransceivers().forEach((elem) => { + const source = mediaSourceFromTransceiverMid(elem.mid); + if (source == CallMediaSource.ScreenAudio || source == CallMediaSource.ScreenVideo) { + elem.sender.replaceTrack(null); + } + }); + for (const t of call.localScreenStream.getTracks()) + t.stop(); + for (const t of call.localScreenStream.getTracks()) + call.localScreenStream.removeTrack(t); + } + if (allowSendScreenAudio) { + call.localMediaSources.screenAudio = !call.localMediaSources.screenAudio; + } + call.localMediaSources.screenVideo = !call.localMediaSources.screenVideo; + localOrPeerMediaSourcesChanged(call); + changeLayout(call.layout); + }; + async function replaceMedia(call, source, enable, camera) { + const videos = getVideoElements(); + if (!videos) + throw Error("no video elements"); + const pc = call.connection; + // disabling track first, then asking for a new one. + // doing it vice versa gives an error like "too many cameras were open" on some Android devices or webViews + // which means the second camera will never be opened + for (const t of source == CallMediaSource.Mic ? call.localStream.getAudioTracks() : call.localStream.getVideoTracks()) { + if (isDesktop || source != CallMediaSource.Mic || stopTrackOnAndroid) + t.stop(); + else + t.enabled = false; + call.localStream.removeTrack(t); + } + let localStream; + try { + localStream = await getLocalMediaStream(source == CallMediaSource.Mic ? enable : false, source == CallMediaSource.Camera ? enable : false, camera); + } + catch (e) { + console.log("Replace media error", e); + desktopShowPermissionsAlert(source == CallMediaSource.Mic ? CallMediaType.Audio : CallMediaType.Video); + return false; + } + for (const t of localStream.getTracks()) { + call.localStream.addTrack(t); + localStream.removeTrack(t); } - for (const t of call.localStream.getTracks()) - t.stop(); call.localCamera = camera; - const audioTracks = localStream.getAudioTracks(); - const videoTracks = localStream.getVideoTracks(); - if (!audioWasEnabled && oldAudioTracks.length > 0) { - audioTracks.forEach((elem) => (elem.enabled = false)); - } - if (!call.cameraEnabled && !call.screenShareEnabled) { - videoTracks.forEach((elem) => (elem.enabled = false)); - } - replaceTracks(pc, audioTracks); - replaceTracks(pc, videoTracks); - call.localStream = localStream; - videos.local.srcObject = localStream; + const audioTracks = call.localStream.getAudioTracks(); + const videoTracks = call.localStream.getVideoTracks(); + replaceTracks(pc, CallMediaSource.Mic, audioTracks); + replaceTracks(pc, CallMediaSource.Camera, videoTracks); + videos.local.play().catch((e) => console.log("replace media: local play", JSON.stringify(e))); + call.localMediaSources.mic = call.localStream.getAudioTracks().length > 0; + call.localMediaSources.camera = call.localStream.getVideoTracks().length > 0; + localOrPeerMediaSourcesChanged(call); + changeLayout(call.layout); + return true; } - function replaceTracks(pc, tracks) { - if (!tracks.length) + function replaceTracks(pc, source, tracks) { + var _a; + const sender = (_a = pc.getTransceivers().find((elem) => mediaSourceFromTransceiverMid(elem.mid) == source)) === null || _a === void 0 ? void 0 : _a.sender; + if (sender) { + if (tracks.length > 0) + for (const t of tracks) { + console.log("Replaced media track"); + sender.replaceTrack(t); + } + else { + console.log("Media track set to null"); + sender.replaceTrack(null); + } + } + } + async function recreateLocalStreamWhileNotConnected(newCamera) { + const videos = getVideoElements(); + const localStream = notConnectedCall === null || notConnectedCall === void 0 ? void 0 : notConnectedCall.localStream; + const oldCamera = notConnectedCall === null || notConnectedCall === void 0 ? void 0 : notConnectedCall.localCamera; + if (!localStream || !oldCamera || !videos) return; - const sender = pc.getSenders().find((s) => { var _a; return ((_a = s.track) === null || _a === void 0 ? void 0 : _a.kind) === tracks[0].kind; }); - if (sender) - for (const t of tracks) - sender.replaceTrack(t); + if (!inactiveCallMediaSources.mic) { + localStream.getAudioTracks().forEach((elem) => (isDesktop || stopTrackOnAndroid ? elem.stop() : (elem.enabled = false))); + localStream.getAudioTracks().forEach((elem) => localStream.removeTrack(elem)); + } + if (!inactiveCallMediaSources.camera || oldCamera != newCamera) { + localStream.getVideoTracks().forEach((elem) => elem.stop()); + localStream.getVideoTracks().forEach((elem) => localStream.removeTrack(elem)); + } + await getLocalMediaStream(inactiveCallMediaSources.mic && localStream.getAudioTracks().length == 0, inactiveCallMediaSources.camera && (localStream.getVideoTracks().length == 0 || oldCamera != newCamera) && (await browserHasCamera()), newCamera) + .then((stream) => { + stream.getTracks().forEach((elem) => { + localStream.addTrack(elem); + stream.removeTrack(elem); + }); + if (notConnectedCall && localStream.getVideoTracks().length > 0) { + notConnectedCall.localCamera = newCamera; + } + }) + .catch((e) => console.log("Error while enabling camera in not connected call", e)); + if (!videos.local.srcObject && localStream.getTracks().length > 0) { + videos.local.srcObject = localStream; + } + videos.local.play().catch((e) => console.log(e)); } - function setupPeerTransform(operation, peer, worker, aesKey, key) { + function mediaSourceFromTransceiverMid(mid) { + switch (mid) { + case "0": + return CallMediaSource.Mic; + case "1": + return CallMediaSource.Camera; + case "2": + return CallMediaSource.ScreenAudio; + case "3": + return CallMediaSource.ScreenVideo; + default: + return CallMediaSource.Unknown; + } + } + function setupPeerTransform(operation, peer, worker, aesKey, key, media) { if (worker && "RTCRtpScriptTransform" in window) { console.log(`${operation} with worker & RTCRtpScriptTransform`); - peer.transform = new RTCRtpScriptTransform(worker, { operation, aesKey }); + peer.transform = new RTCRtpScriptTransform(worker, { operation, aesKey, media }); } else if ("createEncodedStreams" in peer) { const { readable, writable } = peer.createEncodedStreams(); if (worker) { console.log(`${operation} with worker`); - worker.postMessage({ operation, readable, writable, aesKey }, [readable, writable]); + worker.postMessage({ operation, readable, writable, aesKey, media }, [readable, writable]); } else { console.log(`${operation} without worker`); @@ -505,9 +954,126 @@ const processCommand = (function () { console.log(`no ${operation}`); } } - function getLocalMediaStream(mediaType, facingMode) { - const constraints = callMediaConstraints(mediaType, facingMode); - return navigator.mediaDevices.getUserMedia(constraints); + function setupMuteUnmuteListener(transceiver, track) { + // console.log("Setting up mute/unmute listener in the call without encryption for mid = ", transceiver.mid) + let inboundStatsId = ""; + let lastBytesReceived = 0; + // muted initially + let mutedSeconds = 4; + let statsInterval = setInterval(async () => { + var _a; + const stats = await transceiver.receiver.getStats(); + if (!inboundStatsId) { + stats.forEach((elem) => { + if (elem.type == "inbound-rtp") { + inboundStatsId = elem.id; + } + }); + } + if (inboundStatsId) { + // even though MSDN site says `bytesReceived` is available in WebView 80+, in reality it's available even in 69 + const bytes = (_a = stats.get(inboundStatsId)) === null || _a === void 0 ? void 0 : _a.bytesReceived; + if (bytes <= lastBytesReceived) { + mutedSeconds++; + if (mutedSeconds == 3) { + onMediaMuteUnmute(transceiver.mid, true); + } + } + else { + if (mutedSeconds >= 3) { + onMediaMuteUnmute(transceiver.mid, false); + } + lastBytesReceived = bytes; + mutedSeconds = 0; + } + } + }, 1000); + track.onended = (_) => { + clearInterval(statsInterval); + }; + } + function onMediaMuteUnmute(transceiverMid, mute) { + const videos = getVideoElements(); + if (!videos) + throw Error("no video elements"); + if (!activeCall) + return; + const source = mediaSourceFromTransceiverMid(transceiverMid); + console.log(`Mute/unmute ${source} track = ${mute} with mid = ${transceiverMid}`); + const sources = activeCall.peerMediaSources; + if (source == CallMediaSource.Mic && activeCall.peerMediaSources.mic == mute) { + const resp = { + type: "peerMedia", + source: source, + enabled: !mute, + }; + sources.mic = !mute; + activeCall.peerMediaSources = sources; + sendMessageToNative({ resp: resp }); + if (!mute) + videos.remote.play().catch((e) => console.log(e)); + } + else if (source == CallMediaSource.Camera && activeCall.peerMediaSources.camera == mute) { + const resp = { + type: "peerMedia", + source: source, + enabled: !mute, + }; + sources.camera = !mute; + activeCall.peerMediaSources = sources; + sendMessageToNative({ resp: resp }); + if (!mute) + videos.remote.play().catch((e) => console.log(e)); + } + else if (source == CallMediaSource.ScreenAudio && activeCall.peerMediaSources.screenAudio == mute) { + const resp = { + type: "peerMedia", + source: source, + enabled: !mute, + }; + sources.screenAudio = !mute; + activeCall.peerMediaSources = sources; + sendMessageToNative({ resp: resp }); + if (!mute) + videos.remoteScreen.play().catch((e) => console.log(e)); + } + else if (source == CallMediaSource.ScreenVideo && activeCall.peerMediaSources.screenVideo == mute) { + const resp = { + type: "peerMedia", + source: source, + enabled: !mute, + }; + sources.screenVideo = !mute; + activeCall.peerMediaSources = sources; + sendMessageToNative({ resp: resp }); + if (!mute) + videos.remoteScreen.play().catch((e) => console.log(e)); + } + if (!mute) + addRemoteTracksWhenUnmuted(source, activeCall); + localOrPeerMediaSourcesChanged(activeCall); + // Make sure that remote camera and remote screen video in their places and shown/hidden based on layout type currently in use + changeLayout(activeCall.layout); + } + /* + When new remote tracks are coming, they don't get added to remote streams. They are stored in a map and once any of them "unmuted", + that track is added to the stream. Such workaround needed because Safari doesn't play one stream + if another one is not playing too, eg. no audio if only audio is playing while video track is present too but muted. + But we have possibility to have only one currently active track, even no active track at all. + */ + function addRemoteTracksWhenUnmuted(source, call) { + const track = call.remoteTracks.get(source); + if (track) { + const stream = source == CallMediaSource.Mic || source == CallMediaSource.Camera ? call.remoteStream : call.remoteScreenStream; + stream.addTrack(track); + call.remoteTracks.delete(source); + } + } + async function getLocalMediaStream(mic, camera, facingMode) { + if (!mic && !camera) + return new MediaStream(); + const constraints = callMediaConstraints(mic, camera, facingMode); + return await navigator.mediaDevices.getUserMedia(constraints); } function getLocalScreenCaptureStream() { const constraints /* DisplayMediaStreamConstraints */ = { @@ -520,37 +1086,49 @@ const processCommand = (function () { //}, //aspectRatio: 1.33, }, - audio: true, + audio: allowSendScreenAudio, + // This works with Chrome, Edge, Opera, but not with Firefox and Safari + // systemAudio: "include" }; return navigator.mediaDevices.getDisplayMedia(constraints); } - function callMediaConstraints(mediaType, facingMode) { - switch (mediaType) { - case CallMediaType.Audio: - return { audio: true, video: false }; - case CallMediaType.Video: - return { - audio: true, - video: { - frameRate: 24, - width: { - min: 480, - ideal: 720, - max: 1280, - }, - aspectRatio: 1.33, - facingMode, - }, - }; + async function browserHasCamera() { + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + const hasCamera = devices.some((elem) => elem.kind == "videoinput"); + console.log("Camera is available: " + hasCamera); + return hasCamera; } + catch (error) { + console.log("Error while enumerating devices: " + error, error); + return false; + } + } + function callMediaConstraints(mic, camera, facingMode) { + return { + audio: mic, + video: !camera + ? false + : { + frameRate: 24, + width: { + min: 480, + ideal: 720, + max: 1280, + }, + aspectRatio: 1.33, + facingMode, + }, + }; } function supportsInsertableStreams(useWorker) { return (("createEncodedStreams" in RTCRtpSender.prototype && "createEncodedStreams" in RTCRtpReceiver.prototype) || (!!useWorker && "RTCRtpScriptTransform" in window)); } function shutdownCameraAndMic() { - if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.localStream) { + if (activeCall) { activeCall.localStream.getTracks().forEach((track) => track.stop()); + activeCall.localScreenStream.getTracks().forEach((track) => track.stop()); } } function resetVideoElements() { @@ -558,14 +1136,9 @@ const processCommand = (function () { if (!videos) return; videos.local.srcObject = null; + videos.localScreen.srcObject = null; videos.remote.srcObject = null; - } - function getVideoElements() { - const local = document.getElementById("local-video-stream"); - const remote = document.getElementById("remote-video-stream"); - if (!(local && remote && local instanceof HTMLMediaElement && remote instanceof HTMLMediaElement)) - return; - return { local, remote }; + videos.remoteScreen.srcObject = null; } // function setupVideoElement(video: HTMLElement) { // // TODO use display: none @@ -574,39 +1147,200 @@ const processCommand = (function () { // video.style.opacity = "1" // } // } - function enableMedia(s, media, enable) { - const tracks = media == CallMediaType.Video ? s.getVideoTracks() : s.getAudioTracks(); - for (const t of tracks) - t.enabled = enable; - if (media == CallMediaType.Video && activeCall) { - activeCall.cameraEnabled = enable; + function enableMedia(s, source, enable) { + if (!activeCall) + return false; + const tracks = source == CallMediaSource.Camera ? s.getVideoTracks() : s.getAudioTracks(); + let changedSource = false; + for (const t of tracks) { + for (const transceiver of activeCall.connection.getTransceivers()) { + if ((t.kind == CallMediaType.Audio && mediaSourceFromTransceiverMid(transceiver.mid) == CallMediaSource.Mic) || + (t.kind == CallMediaType.Video && mediaSourceFromTransceiverMid(transceiver.mid) == CallMediaSource.Camera)) { + if (enable) { + transceiver.sender.replaceTrack(t); + } + else { + if (isDesktop || t.kind == CallMediaType.Video || stopTrackOnAndroid) + t.stop(); + else + t.enabled = false; + s.removeTrack(t); + transceiver.sender.replaceTrack(null); + } + if (source == CallMediaSource.Mic) { + activeCall.localMediaSources.mic = enable; + changedSource = true; + } + else if (source == CallMediaSource.Camera) { + activeCall.localMediaSources.camera = enable; + changedSource = true; + } + } + } + } + if (changedSource) { + localOrPeerMediaSourcesChanged(activeCall); + changeLayout(activeCall.layout); + return true; + } + else { + console.log("Enable media error"); + desktopShowPermissionsAlert(source == CallMediaSource.Mic ? CallMediaType.Audio : CallMediaType.Video); + return false; } } - toggleScreenShare = async function () { - const call = activeCall; - if (!call) - return; - call.screenShareEnabled = !call.screenShareEnabled; - await replaceMedia(call, call.localCamera); - }; + function adaptToOldVersion(peerHasOldVersion, activeCall) { + activeCall.peerHasOldVersion = peerHasOldVersion; + if (peerHasOldVersion) { + console.log("The peer has an old version.", "Tracks size:", activeCall.remoteStream.getAudioTracks().length, activeCall.remoteStream.getVideoTracks().length); + onMediaMuteUnmute("0", false); + if (activeCall.remoteStream.getVideoTracks().length > 0 || activeCall.remoteTracks.get(CallMediaSource.Camera)) { + onMediaMuteUnmute("1", false); + } + if (activeCall.localMediaSources.camera && !activeCall.peerMediaSources.camera) { + console.log("Stopping video track for the old version"); + activeCall.localStream.getVideoTracks().forEach((elem) => { + elem.stop(); + activeCall.localStream.removeTrack(elem); + }); + activeCall.localMediaSources.camera = false; + sendMessageToNative({ resp: { type: "ok" }, command: { type: "media", source: CallMediaSource.Camera, enable: false } }); + localOrPeerMediaSourcesChanged(activeCall); + changeLayout(activeCall.layout); + } + } + } + function webView69Or70() { + return !isDesktop && (navigator.userAgent.includes("Chrome/69.") || navigator.userAgent.includes("Chrome/70.")); + } + // Adding `a=extmap-allow-mixed` causes exception on old WebViews + // https://groups.google.com/a/chromium.org/g/blink-dev/c/7z3uvp0-ZAc/m/8Z7qpp71BgAJ + function adaptSdpToOldWebView(desc) { + var _a; + const res = []; + (_a = desc.sdp) === null || _a === void 0 ? void 0 : _a.split("\n").forEach((line) => { + // Chrome has a bug related to SDP parser in old web view versions + if (!line.includes("a=extmap-allow-mixed")) { + res.push(line); + } + }); + return { sdp: res.join("\n"), type: desc.type }; + } return processCommand; })(); function toggleRemoteVideoFitFill() { const remote = document.getElementById("remote-video-stream"); remote.style.objectFit = remote.style.objectFit != "contain" ? "contain" : "cover"; } -function toggleMedia(s, media) { +function toggleRemoteScreenVideoFitFill() { + const remoteScreen = document.getElementById("remote-screen-video-stream"); + remoteScreen.style.objectFit = remoteScreen.style.objectFit != "contain" ? "contain" : "cover"; +} +function togglePeerMedia(s, media) { + if (!activeCall) + return false; let res = false; const tracks = media == CallMediaType.Video ? s.getVideoTracks() : s.getAudioTracks(); for (const t of tracks) { t.enabled = !t.enabled; res = t.enabled; } - if (media == CallMediaType.Video && activeCall) { - activeCall.cameraEnabled = res; - } return res; } +function changeLayout(layout) { + var _a, _b; + const videos = getVideoElements(); + const localSources = (_a = activeCall === null || activeCall === void 0 ? void 0 : activeCall.localMediaSources) !== null && _a !== void 0 ? _a : inactiveCallMediaSources; + const peerSources = (_b = activeCall === null || activeCall === void 0 ? void 0 : activeCall.peerMediaSources) !== null && _b !== void 0 ? _b : { mic: false, camera: false, screenAudio: false, screenVideo: false }; + if (!videos || !localSources || !peerSources) + return; + switch (layout) { + case LayoutType.Default: + videos.local.className = "inline"; + videos.remote.className = peerSources.screenVideo ? "collapsed" : "inline"; + videos.local.style.visibility = "visible"; + videos.remote.style.visibility = peerSources.camera ? "visible" : "hidden"; + videos.remoteScreen.style.visibility = peerSources.screenVideo ? "visible" : "hidden"; + break; + case LayoutType.LocalVideo: + videos.local.className = "fullscreen"; + videos.local.style.visibility = "visible"; + videos.remote.style.visibility = "hidden"; + videos.remoteScreen.style.visibility = "hidden"; + break; + case LayoutType.RemoteVideo: + if (peerSources.screenVideo && peerSources.camera) { + videos.remoteScreen.className = "fullscreen"; + videos.remoteScreen.style.visibility = "visible"; + videos.remote.style.visibility = "visible"; + videos.remote.className = "collapsed-pip"; + } + else if (peerSources.screenVideo) { + videos.remoteScreen.className = "fullscreen"; + videos.remoteScreen.style.visibility = "visible"; + videos.remote.style.visibility = "hidden"; + videos.remote.className = "inline"; + } + else if (peerSources.camera) { + videos.remote.className = "fullscreen"; + videos.remote.style.visibility = "visible"; + videos.remoteScreen.style.visibility = "hidden"; + videos.remoteScreen.className = "inline"; + } + else { + videos.remote.style.visibility = "hidden"; + } + videos.local.style.visibility = "hidden"; + break; + } + videos.localScreen.style.visibility = localSources.screenVideo ? "visible" : "hidden"; + if (!isDesktop && !localSources.camera) { + resetLocalVideoElementHeight(videos.local); + } +} +function getVideoElements() { + const local = document.getElementById("local-video-stream"); + const localScreen = document.getElementById("local-screen-video-stream"); + const remote = document.getElementById("remote-video-stream"); + const remoteScreen = document.getElementById("remote-screen-video-stream"); + if (!(local && + localScreen && + remote && + remoteScreen && + local instanceof HTMLVideoElement && + localScreen instanceof HTMLMediaElement && + remote instanceof HTMLMediaElement && + remoteScreen instanceof HTMLMediaElement)) + return; + return { local, localScreen, remote, remoteScreen }; +} +// Allow CSS to figure out the size of view by itself on Android because rotating to different orientation +// without dropping override will cause the view to have not normal proportion while no video is present +function resetLocalVideoElementHeight(local) { + local.style.height = ""; +} +function desktopShowPermissionsAlert(mediaType) { + if (!isDesktop) + return; + if (mediaType == CallMediaType.Audio) { + window.alert("Permissions denied. Please, allow access to mic to make the call working and hit unmute button. Don't reload the page."); + } + else { + window.alert("Permissions denied. Please, allow access to mic and camera to make the call working and hit unmute/camera button. Don't reload the page."); + } +} +function permissionDescription(command) { + if (window.safari) { + return command.safari; + } + else if ((navigator.userAgent.includes("Chrome") && navigator.vendor.includes("Google Inc")) || + navigator.userAgent.includes("Firefox")) { + return command.chrome; + } + else { + return ""; + } +} // Cryptography function - it is loaded both in the main window and in worker context (if the worker is used) function callCryptoFunction() { const initialPlainTextRequired = { diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/call.html b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/call.html index dd1193babf..8ea76ed488 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/call.html +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/call.html @@ -7,42 +7,69 @@ <script src="/lz-string.min.js"></script> </head> <body> + <video + id="remote-screen-video-stream" + class="inline" + playsinline + poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" + style="visibility: hidden" + onclick="javascript:toggleRemoteScreenVideoFitFill()" + ></video> + <video id="remote-video-stream" - autoplay + class="inline" playsinline poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" onclick="javascript:toggleRemoteVideoFitFill()" ></video> + <video id="local-video-stream" + class="inline" muted - autoplay playsinline poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" ></video> + + <video + id="local-screen-video-stream" + class="inline" + muted + playsinline + poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" + style="visibility: hidden" + ></video> + <div id="progress"></div> <div id="info-block"> <p id="state"></p> <p id="description"></p> + <b id="media-sources" style="color: #fff"></b> </div> <div id="audio-call-icon"> <img src="/desktop/images/ic_phone_in_talk.svg" /> </div> + + <div id="permission-denied"> + <p id="permission-denied-title"></p> + <p id="permission-denied-desc"></p> + </div> + <p id="manage-call"> - <button id="toggle-screen" style="display: none" onclick="javascript:toggleScreenManually()"> + <button id="toggle-screen" onclick="javascript:toggleScreenManually()"> <img src="/desktop/images/ic_screen_share.svg" /> </button> - <button id="toggle-audio" style="display: none" onclick="javascript:toggleAudioManually()"> + <button id="toggle-mic" onclick="javascript:toggleMicManually()"> <img src="/desktop/images/ic_mic.svg" /> </button> - <button id="end-call" onclick="javascript:endCallManually()"> + <button id="end-call" style="background: red" onclick="javascript:endCallManually()"> <img src="/desktop/images/ic_call_end_filled.svg" /> </button> - <button id="toggle-speaker" style="display: none" onclick="javascript:toggleSpeakerManually()"> + <button id="toggle-speaker" onclick="javascript:toggleSpeakerManually()"> <img src="/desktop/images/ic_volume_up.svg" /> </button> - <button id="toggle-video" style="display: none" onclick="javascript:toggleVideoManually()"> + <button id="toggle-camera" onclick="javascript:toggleCameraManually()"> <img src="/desktop/images/ic_videocam_off.svg" /> </button> </p> diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/images/ic_call_end_filled.svg b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/images/ic_call_end_filled.svg index 34c409818a..5ce74d3cda 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/images/ic_call_end_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/images/ic_call_end_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="red" d="M480 418q125 0 238.75 50.25T914 613.5q8 9.5 8.25 21t-8.25 20L821 748q-8 8-22.5 8.75t-23-5.75l-113-84.5q-6-4.5-8.75-10.25T651 643.5v-139q-42-16-85.5-22.5t-85-6.5q-42 0-85.5 6.5t-85.5 22.5v139q0 6.5-2.75 12.5T298 666.5L184.5 751q-11.5 8.5-23.5 7.5T139.5 748L46 654.5q-8.5-8.5-8.25-20t8.25-21q81.5-95 195.25-145.25T480 418Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M480 418q125 0 238.75 50.25T914 613.5q8 9.5 8.25 21t-8.25 20L821 748q-8 8-22.5 8.75t-23-5.75l-113-84.5q-6-4.5-8.75-10.25T651 643.5v-139q-42-16-85.5-22.5t-85-6.5q-42 0-85.5 6.5t-85.5 22.5v139q0 6.5-2.75 12.5T298 666.5L184.5 751q-11.5 8.5-23.5 7.5T139.5 748L46 654.5q-8.5-8.5-8.25-20t8.25-21q81.5-95 195.25-145.25T480 418Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/images/ic_volume_off.svg b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/images/ic_volume_off.svg new file mode 100644 index 0000000000..2767d8d48d --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/images/ic_volume_off.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 -960 960 960" width="44" fill="white"><path d="m809.5-61.5-133-133q-27 19-58.25 33.25T553.5-139.5V-199q21.5-6.5 42.5-14.5t39.5-22L476-396v229L280-363H122.5v-234H274L54.5-816.5 96-858l755 754-41.5 42.5ZM770-291l-41.5-41.5q20-33 29.75-70.67Q768-440.85 768-481q0-100.82-58.75-180.41T553.5-763v-59.5q120 28 196 123.25t76 218.25q0 50.5-14 98.75T770-291ZM642.5-418.5l-89-89v-132q46.5 21.5 73.75 64.75T654.5-480q0 16-3 31.5t-9 30ZM476-585 372-689l104-104v208Zm-57.5 278v-145.5l-87-87H180v119h124.5l114 113.5ZM375-496Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/style.css b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/style.css index 24c31fa6f7..5110c7c7d6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/style.css +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/style.css @@ -3,26 +3,98 @@ body { padding: 0; margin: 0; background-color: black; + font-family: Arial, Helvetica, sans-serif; } -#remote-video-stream { +#remote-video-stream.inline { position: absolute; width: 100%; height: 100%; object-fit: cover; } -#local-video-stream { +#remote-video-stream.collapsed { + position: absolute; + width: 20%; + max-width: 20%; + height: 15.03vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + bottom: 80px; + right: 0; +} + +#remote-video-stream.collapsed-pip { + position: absolute; + max-width: 30%; + max-height: 30%; + object-fit: cover; + margin: 8px; + border-radius: 8px; + bottom: 0; + right: 0; +} + +#remote-screen-video-stream.inline { + position: absolute; + width: 100%; + height: 100%; + object-fit: cover; +} + +#local-video-stream.inline { + background-color: black; + position: absolute; + width: 20%; + max-width: 20%; + height: 15.03vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + top: 0; + right: 0; +} + +#local-screen-video-stream.inline { position: absolute; width: 20%; max-width: 20%; object-fit: cover; margin: 16px; border-radius: 16px; - top: 0; + top: 33%; right: 0; } +#remote-video-stream.fullscreen { + position: absolute; + height: 100%; + width: 100%; + object-fit: cover; +} + +#remote-screen-video-stream.fullscreen { + position: absolute; + height: 100%; + width: 100%; + object-fit: cover; +} + +#local-video-stream.fullscreen { + position: absolute; + height: 100%; + width: 100%; + object-fit: cover; +} + +#local-screen-video-stream.fullscreen { + position: absolute; + height: 100%; + width: 100%; + object-fit: cover; +} + *::-webkit-media-controls { display: none !important; -webkit-appearance: none !important; @@ -43,19 +115,32 @@ body { #manage-call { position: absolute; width: fit-content; - top: 90%; + bottom: 15px; left: 50%; transform: translate(-50%, 0); display: grid; grid-auto-flow: column; - grid-column-gap: 30px; + grid-column-gap: 38px; } #manage-call button { border: none; cursor: pointer; appearance: none; - background-color: inherit; + background-color: #ffffff33; + border-radius: 50%; + padding: 13px; + width: 61px; + height: 61px; +} + +#manage-call img { + width: 35px; + height: 35px; +} + +#manage-call button .video { + background: #00000033; } #progress { @@ -75,6 +160,23 @@ body { animation: spin 2s linear infinite; } +#permission-denied { + position: absolute; + width: 100%; + text-align: center; + height: 100%; + align-content: center; + color: white; +} + +#permission-denied-title { + font-weight: bold; +} + +#permission-denied-desc { + font-style: italic; +} + @-webkit-keyframes spin { 0% { -webkit-transform: rotate(0deg); @@ -96,10 +198,8 @@ body { #info-block { position: absolute; color: white; - line-height: 10px; opacity: 0.8; width: 200px; - font-family: Arial, Helvetica, sans-serif; } #info-block.audio { diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/ui.js b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/ui.js index 6cf711b790..7c0836960c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/ui.js +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/ui.js @@ -9,6 +9,7 @@ socket.addEventListener("open", (_event) => { sendMessageToNative = (msg) => { console.log("Message to server"); socket.send(JSON.stringify(msg)); + reactOnMessageToServer(msg); }; }); socket.addEventListener("message", (event) => { @@ -27,71 +28,166 @@ socket.addEventListener("close", (_event) => { function endCallManually() { sendMessageToNative({ resp: { type: "end" } }); } -function toggleAudioManually() { - if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.localMedia) { - document.getElementById("toggle-audio").innerHTML = toggleMedia(activeCall.localStream, CallMediaType.Audio) - ? '<img src="/desktop/images/ic_mic.svg" />' - : '<img src="/desktop/images/ic_mic_off.svg" />'; - } +function toggleMicManually() { + const enable = activeCall ? !activeCall.localMediaSources.mic : !inactiveCallMediaSources.mic; + const apiCall = { + command: { type: "media", source: CallMediaSource.Mic, enable: enable }, + }; + processCommand(apiCall); } function toggleSpeakerManually() { - if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.remoteStream) { - document.getElementById("toggle-speaker").innerHTML = toggleMedia(activeCall.remoteStream, CallMediaType.Audio) - ? '<img src="/desktop/images/ic_volume_up.svg" />' - : '<img src="/desktop/images/ic_volume_down.svg" />'; + if ((activeCall === null || activeCall === void 0 ? void 0 : activeCall.remoteStream) && activeCall.peerMediaSources.mic) { + enableSpeakerIcon(togglePeerMedia(activeCall.remoteStream, CallMediaType.Audio), !activeCall.peerMediaSources.mic); } } -function toggleVideoManually() { - if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.localMedia) { - let res; - if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.screenShareEnabled) { - activeCall.cameraEnabled = !activeCall.cameraEnabled; - res = activeCall.cameraEnabled; - } - else { - res = toggleMedia(activeCall.localStream, CallMediaType.Video); - } - document.getElementById("toggle-video").innerHTML = res - ? '<img src="/desktop/images/ic_videocam_filled.svg" />' - : '<img src="/desktop/images/ic_videocam_off.svg" />'; - } +function toggleCameraManually() { + const enable = activeCall ? !activeCall.localMediaSources.camera : !inactiveCallMediaSources.camera; + const apiCall = { + command: { type: "media", source: CallMediaSource.Camera, enable: enable }, + }; + processCommand(apiCall); } async function toggleScreenManually() { - const was = activeCall === null || activeCall === void 0 ? void 0 : activeCall.screenShareEnabled; await toggleScreenShare(); - if (was != (activeCall === null || activeCall === void 0 ? void 0 : activeCall.screenShareEnabled)) { - document.getElementById("toggle-screen").innerHTML = (activeCall === null || activeCall === void 0 ? void 0 : activeCall.screenShareEnabled) - ? '<img src="/desktop/images/ic_stop_screen_share.svg" />' - : '<img src="/desktop/images/ic_screen_share.svg" />'; +} +// override function in call.ts to adapt UI to enabled media sources +localOrPeerMediaSourcesChanged = (call) => { + enableMicIcon(call.localMediaSources.mic); + enableCameraIcon(call.localMediaSources.camera); + enableScreenIcon(call.localMediaSources.screenVideo); + const className = localMedia(call) == CallMediaType.Video || peerMedia(call) == CallMediaType.Video ? CallMediaType.Video : CallMediaType.Audio; + document.getElementById("info-block").className = className; + if (call.connection.iceConnectionState == "connected") { + document.getElementById("audio-call-icon").style.display = className == CallMediaType.Audio ? "block" : "none"; } + // document.getElementById("media-sources")!.innerText = mediaSourcesStatus(call) + document.getElementById("manage-call").className = localMedia(call) == CallMediaType.Video ? CallMediaType.Video : ""; +}; +// override function in call.ts to adapt UI to enabled media sources +inactiveCallMediaSourcesChanged = (inactiveCallMediaSources) => { + const mic = inactiveCallMediaSources.mic; + const camera = inactiveCallMediaSources.camera; + const screenVideo = inactiveCallMediaSources.screenVideo; + enableMicIcon(mic); + enableCameraIcon(camera); + enableScreenIcon(screenVideo); + const className = camera ? CallMediaType.Video : CallMediaType.Audio; + document.getElementById("info-block").className = className; + // document.getElementById("media-sources")!.innerText = inactiveCallMediaSourcesStatus(inactiveCallMediaSources) +}; +failedToGetPermissions = (title, description) => { + document.getElementById("info-block").style.visibility = "hidden"; + document.getElementById("progress").style.visibility = "hidden"; + document.getElementById("permission-denied-title").innerText = title; + document.getElementById("permission-denied-desc").innerText = description; + document.getElementById("toggle-mic").style.visibility = "hidden"; + document.getElementById("toggle-camera").style.visibility = "hidden"; + document.getElementById("toggle-screen").style.visibility = "hidden"; + document.getElementById("toggle-speaker").style.visibility = "hidden"; +}; +function enableMicIcon(enabled) { + document.getElementById("toggle-mic").innerHTML = enabled + ? '<img src="/desktop/images/ic_mic.svg" />' + : '<img src="/desktop/images/ic_mic_off.svg" />'; +} +function enableCameraIcon(enabled) { + document.getElementById("toggle-camera").innerHTML = enabled + ? '<img src="/desktop/images/ic_videocam_filled.svg" />' + : '<img src="/desktop/images/ic_videocam_off.svg" />'; +} +function enableScreenIcon(enabled) { + document.getElementById("toggle-screen").innerHTML = enabled + ? '<img src="/desktop/images/ic_stop_screen_share.svg" />' + : '<img src="/desktop/images/ic_screen_share.svg" />'; +} +function enableSpeakerIcon(enabled, muted) { + document.getElementById("toggle-speaker").innerHTML = muted + ? '<img src="/desktop/images/ic_volume_off.svg" />' + : enabled + ? '<img src="/desktop/images/ic_volume_up.svg" />' + : '<img src="/desktop/images/ic_volume_down.svg" />'; + document.getElementById("toggle-speaker").style.opacity = muted ? "0.7" : "1"; +} +function mediaSourcesStatus(call) { + let status = "local"; + if (call.localMediaSources.mic) + status += " mic"; + if (call.localMediaSources.camera) + status += " cam"; + if (call.localMediaSources.screenAudio) + status += " scrA"; + if (call.localMediaSources.screenVideo) + status += " scrV"; + status += " | peer"; + if (call.peerMediaSources.mic) + status += " mic"; + if (call.peerMediaSources.camera) + status += " cam"; + if (call.peerMediaSources.screenAudio) + status += " scrA"; + if (call.peerMediaSources.screenVideo) + status += " scrV"; + return status; +} +function inactiveCallMediaSourcesStatus(inactiveCallMediaSources) { + let status = "local"; + const mic = inactiveCallMediaSources.mic; + const camera = inactiveCallMediaSources.camera; + const screenAudio = inactiveCallMediaSources.screenAudio; + const screenVideo = inactiveCallMediaSources.screenVideo; + if (mic) + status += " mic"; + if (camera) + status += " cam"; + if (screenAudio) + status += " scrA"; + if (screenVideo) + status += " scrV"; + return status; } function reactOnMessageFromServer(msg) { - var _a; - switch ((_a = msg.command) === null || _a === void 0 ? void 0 : _a.type) { + var _a, _b, _c; + // screen is not allowed to be enabled before connection estabilished + if (((_a = msg.command) === null || _a === void 0 ? void 0 : _a.type) == "capabilities" || ((_b = msg.command) === null || _b === void 0 ? void 0 : _b.type) == "offer") { + document.getElementById("toggle-screen").style.opacity = "0.7"; + } + else if (activeCall) { + document.getElementById("toggle-screen").style.opacity = "1"; + } + switch ((_c = msg.command) === null || _c === void 0 ? void 0 : _c.type) { case "capabilities": - document.getElementById("info-block").className = msg.command.media; - break; case "offer": case "start": - document.getElementById("toggle-audio").style.display = "inline-block"; - document.getElementById("toggle-speaker").style.display = "inline-block"; - if (msg.command.media == CallMediaType.Video) { - document.getElementById("toggle-video").style.display = "inline-block"; - document.getElementById("toggle-screen").style.display = "inline-block"; - } document.getElementById("info-block").className = msg.command.media; + document.getElementById("toggle-mic").style.display = "inline-block"; + document.getElementById("toggle-speaker").style.display = "inline-block"; + document.getElementById("toggle-camera").style.display = "inline-block"; + document.getElementById("toggle-screen").style.display = "inline-block"; + enableSpeakerIcon(true, true); break; case "description": updateCallInfoView(msg.command.state, msg.command.description); if ((activeCall === null || activeCall === void 0 ? void 0 : activeCall.connection.connectionState) == "connected") { document.getElementById("progress").style.display = "none"; - if (document.getElementById("info-block").className == CallMediaType.Audio) { - document.getElementById("audio-call-icon").style.display = "block"; - } + document.getElementById("audio-call-icon").style.display = + document.getElementById("info-block").className == CallMediaType.Audio ? "block" : "none"; } break; } } +function reactOnMessageToServer(msg) { + var _a; + if (!activeCall) + return; + switch ((_a = msg.resp) === null || _a === void 0 ? void 0 : _a.type) { + case "peerMedia": + const className = localMedia(activeCall) == CallMediaType.Video || peerMedia(activeCall) == CallMediaType.Video ? "video" : "audio"; + document.getElementById("info-block").className = className; + document.getElementById("audio-call-icon").style.display = className == CallMediaType.Audio ? "block" : "none"; + enableSpeakerIcon(activeCall.remoteStream.getAudioTracks().every((elem) => elem.enabled), !activeCall.peerMediaSources.mic); + break; + } +} function updateCallInfoView(state, description) { document.getElementById("state").innerText = state; document.getElementById("description").innerText = description; diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/lz-string.min.js b/apps/multiplatform/common/src/commonMain/resources/assets/www/lz-string.min.js index 2d1900a0d3..f7c26ae2b4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/lz-string.min.js +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/lz-string.min.js @@ -1 +1 @@ -var LZString=function(){function o(o,r){if(!t[o]){t[o]={};for(var n=0;n<o.length;n++)t[o][o.charAt(n)]=n}return t[o][r]}var r=String.fromCharCode,n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",t={},i={compressToBase64:function(o){if(null==o)return"";var r=i._compress(o,6,function(o){return n.charAt(o)});switch(r.length%4){default:case 0:return r;case 1:return r+"===";case 2:return r+"==";case 3:return r+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(e){return o(n,r.charAt(e))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(o){return null==o?"":""==o?null:i._decompress(o.length,16384,function(r){return o.charCodeAt(r)-32})},compressToUint8Array:function(o){for(var r=i.compress(o),n=new Uint8Array(2*r.length),e=0,t=r.length;t>e;e++){var s=r.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null===o||void 0===o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;t>e;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(o){return null==o?"":i._compress(o,6,function(o){return e.charAt(o)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(n){return o(e,r.charAt(n))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(o,r,n){if(null==o)return"";var e,t,i,s={},p={},u="",c="",a="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<o.length;i+=1)if(u=o.charAt(i),Object.prototype.hasOwnProperty.call(s,u)||(s[u]=f++,p[u]=!0),c=a+u,Object.prototype.hasOwnProperty.call(s,c))a=c;else{if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++),s[c]=f++,a=String(u)}if(""!==a){if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==r-1){d.push(n(m));break}v++}return d.join("")},decompress:function(o){return null==o?"":""==o?null:i._decompress(o.length,32768,function(r){return o.charCodeAt(r)})},_decompress:function(o,n,e){var t,i,s,p,u,c,a,l,f=[],h=4,d=4,m=3,v="",w=[],A={val:e(0),position:n,index:1};for(i=0;3>i;i+=1)f[i]=i;for(p=0,c=Math.pow(2,2),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(t=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 2:return""}for(f[3]=l,s=l,w.push(l);;){if(A.index>o)return"";for(p=0,c=Math.pow(2,m),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(l=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 2:return w.join("")}if(0==h&&(h=Math.pow(2,m),m++),f[l])v=f[l];else{if(l!==d)return null;v=s+s.charAt(0)}w.push(v),f[d++]=s+v.charAt(0),h--,s=v,0==h&&(h=Math.pow(2,m),m++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module&&(module.exports=LZString); +var LZString=function(){var r=String.fromCharCode,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",e={};function t(r,o){if(!e[r]){e[r]={};for(var n=0;n<r.length;n++)e[r][r.charAt(n)]=n}return e[r][o]}var i={compressToBase64:function(r){if(null==r)return"";var n=i._compress(r,6,function(r){return o.charAt(r)});switch(n.length%4){default:case 0:return n;case 1:return n+"===";case 2:return n+"==";case 3:return n+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(n){return t(o,r.charAt(n))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(r){return null==r?"":""==r?null:i._decompress(r.length,16384,function(o){return r.charCodeAt(o)-32})},compressToUint8Array:function(r){for(var o=i.compress(r),n=new Uint8Array(2*o.length),e=0,t=o.length;e<t;e++){var s=o.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null==o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;e<t;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(r){return null==r?"":i._compress(r,6,function(r){return n.charAt(r)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(o){return t(n,r.charAt(o))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(r,o,n){if(null==r)return"";var e,t,i,s={},u={},a="",p="",c="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<r.length;i+=1)if(a=r.charAt(i),Object.prototype.hasOwnProperty.call(s,a)||(s[a]=f++,u[a]=!0),p=c+a,Object.prototype.hasOwnProperty.call(s,p))c=p;else{if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++),s[p]=f++,c=String(a)}if(""!==c){if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==o-1){d.push(n(m));break}v++}return d.join("")},decompress:function(r){return null==r?"":""==r?null:i._decompress(r.length,32768,function(o){return r.charCodeAt(o)})},_decompress:function(o,n,e){var t,i,s,u,a,p,c,l=[],f=4,h=4,d=3,m="",v=[],g={val:e(0),position:n,index:1};for(t=0;t<3;t+=1)l[t]=t;for(s=0,a=Math.pow(2,2),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 2:return""}for(l[3]=c,i=c,v.push(c);;){if(g.index>o)return"";for(s=0,a=Math.pow(2,d),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(c=s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 2:return v.join("")}if(0==f&&(f=Math.pow(2,d),d++),l[c])m=l[c];else{if(c!==h)return null;m=i+i.charAt(0)}v.push(m),l[h++]=i+m.charAt(0),i=m,0==--f&&(f=Math.pow(2,d),d++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module?module.exports=LZString:"undefined"!=typeof angular&&null!=angular&&angular.module("LZString",[]).factory("LZString",function(){return LZString}); \ No newline at end of file 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 new file mode 100644 index 0000000000..18b17b25a9 --- /dev/null +++ b/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ChatItemsMergerTest.kt @@ -0,0 +1,158 @@ +package chat.simplex.app + +import androidx.compose.runtime.mutableStateOf +import chat.simplex.common.model.* +import chat.simplex.common.views.chat.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.datetime.Clock +import kotlin.test.Test +import kotlin.test.assertEquals + +class ChatItemsMergerTest { + + @Test + fun testRecalculateSplitPositions() { + val oldItems = listOf(ChatItem.getSampleData(0), ChatItem.getSampleData(123L), ChatItem.getSampleData(124L), ChatItem.getSampleData(125L)) + + val splits1 = MutableStateFlow(listOf(123L)) + 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) + assertEquals(1, splits1.value.size) + assertEquals(124L, splits1.value.first()) + + val splits2 = MutableStateFlow(listOf(123L)) + 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) + assertEquals(1, splits2.value.size) + assertEquals(125L, splits2.value.first()) + + val splits3 = MutableStateFlow(listOf(123L)) + 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) + assertEquals(0, splits3.value.size) + + val splits4 = MutableStateFlow(listOf(123L)) + val chatState4 = ActiveChatState(splits = splits4) + val recalc4 = recalculateChatStatePositions(chatState4) + recalc4.cleared() + assertEquals(0, splits4.value.size) + } + + @Test + fun testItemsMerging() { + val items = listOf( + ChatItem(CIDirection.DirectRcv(), CIMeta.getSample(100L, Clock.System.now(), text = ""), CIContent.SndGroupFeature(GroupFeature.Voice, GroupPreference(GroupFeatureEnabled.ON), memberRole_ = null), reactions = emptyList()), + ChatItem(CIDirection.DirectRcv(), CIMeta.getSample(99L, Clock.System.now(), text = ""), CIContent.SndGroupFeature(GroupFeature.FullDelete, GroupPreference(GroupFeatureEnabled.ON), memberRole_ = null), reactions = emptyList()), + ChatItem(CIDirection.DirectRcv(), CIMeta.getSample(98L, Clock.System.now(), text = "", itemDeleted = CIDeleted.Deleted(null)), CIContent.RcvDeleted(CIDeleteMode.cidmBroadcast), reactions = emptyList()), + ChatItem(CIDirection.DirectRcv(), CIMeta.getSample(97L, Clock.System.now(), text = "", itemDeleted = CIDeleted.Deleted(null)), CIContent.RcvDeleted(CIDeleteMode.cidmBroadcast), reactions = emptyList()), + ChatItem(CIDirection.DirectRcv(), CIMeta.getSample(96L, Clock.System.now(), text = ""), CIContent.RcvMsgContent(MsgContent.MCText("")), reactions = emptyList()), + ChatItem(CIDirection.DirectRcv(), CIMeta.getSample(95L, Clock.System.now(), text = ""), CIContent.RcvMsgContent(MsgContent.MCText("")), reactions = emptyList()), + ChatItem(CIDirection.DirectRcv(), CIMeta.getSample(94L, Clock.System.now(), text = ""), CIContent.RcvMsgContent(MsgContent.MCText("")), reactions = emptyList()), + ) + + val unreadCount = mutableStateOf(0) + val chatState = ActiveChatState() + val merged1 = MergedItems.create(items, unreadCount, emptySet(), chatState) + assertEquals( + listOf( + listOf(0, false, + listOf( + listOf(0, 100, CIMergeCategory.ChatFeature), + listOf(1, 99, CIMergeCategory.ChatFeature) + ) + ), + listOf(2, false, + listOf( + listOf(0, 98, CIMergeCategory.RcvItemDeleted), + listOf(1, 97, CIMergeCategory.RcvItemDeleted) + ) + ), + listOf(4, true, + listOf( + listOf(0, 96, null), + ) + ), + listOf(5, true, + listOf( + listOf(0, 95, null), + ) + ), + listOf(6, true, + listOf( + listOf(0, 94, null) + ) + ) + ).toList().toString(), + merged1.items.map { + listOf( + it.startIndexInReversedItems, + if (it is MergedItem.Grouped) it.revealed else true, + when (it) { + is MergedItem.Grouped -> it.items.mapIndexed { index, listItem -> + listOf(index, listItem.item.id, listItem.item.mergeCategory) + } + is MergedItem.Single -> listOf(listOf(0, it.item.item.id, it.item.item.mergeCategory)) + } + ) + }.toString() + ) + + val merged2 = MergedItems.create(items, unreadCount, setOf(98L, 97L), chatState) + assertEquals( + listOf( + listOf(0, false, + listOf( + listOf(0, 100, CIMergeCategory.ChatFeature), + listOf(1, 99, CIMergeCategory.ChatFeature) + ) + ), + listOf(2, true, + listOf( + listOf(0, 98, CIMergeCategory.RcvItemDeleted), + ) + ), + listOf(3, true, + listOf( + listOf(0, 97, CIMergeCategory.RcvItemDeleted) + ) + ), + listOf(4, true, + listOf( + listOf(0, 96, null), + ) + ), + listOf(5, true, + listOf( + listOf(0, 95, null), + ) + ), + listOf(6, true, + listOf( + listOf(0, 94, null) + ) + ) + ).toList().toString(), + merged2.items.map { + listOf( + it.startIndexInReversedItems, + if (it is MergedItem.Grouped) it.revealed else true, + when (it) { + is MergedItem.Grouped -> it.items.mapIndexed { index, listItem -> + listOf(index, listItem.item.id, listItem.item.mergeCategory) + } + is MergedItem.Single -> listOf(listOf(0, it.item.item.id, it.item.item.mergeCategory)) + } + ) + }.toString() + ) + } +} diff --git a/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ThemesTest.kt b/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ThemesTest.kt new file mode 100644 index 0000000000..ae838dcff5 --- /dev/null +++ b/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ThemesTest.kt @@ -0,0 +1,38 @@ +package chat.simplex.app + +import chat.simplex.common.ui.theme.* +import kotlin.test.Test +import kotlin.test.assertEquals + +// use this command for testing: +// ./gradlew desktopTest +class ThemesTest { + @Test + fun testSkipDuplicates() { + val r = ArrayList<ThemeOverrides>() + r.add(ThemeOverrides("UUID", DefaultTheme.DARK)) + r.add(ThemeOverrides("UUID", DefaultTheme.DARK)) + r.add(ThemeOverrides("UUID", DefaultTheme.LIGHT)) + r.add(ThemeOverrides("UUID2", DefaultTheme.DARK)) + r.add(ThemeOverrides("UUID3", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper())) + r.add(ThemeOverrides("UUID4", DefaultTheme.LIGHT, wallpaper = null)) + r.add(ThemeOverrides("UUID5", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(preset = "something"))) + r.add(ThemeOverrides("UUID5", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(preset = "something2"))) + r.add(ThemeOverrides("UUID6", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(preset = "something2"))) + r.add(ThemeOverrides("UUID7", DefaultTheme.DARK, wallpaper = ThemeWallpaper(preset = "something2"))) + r.add(ThemeOverrides("UUID8", DefaultTheme.DARK, wallpaper = ThemeWallpaper(imageFile = "image"))) + r.add(ThemeOverrides("UUID9", DefaultTheme.DARK, wallpaper = ThemeWallpaper(imageFile = "image2"))) + r.add(ThemeOverrides("UUID10", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(imageFile = "image"))) + assertEquals( + r.skipDuplicates(), listOf( + ThemeOverrides("UUID", DefaultTheme.DARK), + ThemeOverrides("UUID3", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper()), + ThemeOverrides("UUID5", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(preset = "something")), + ThemeOverrides("UUID6", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(preset = "something2")), + ThemeOverrides("UUID7", DefaultTheme.DARK, wallpaper = ThemeWallpaper(preset = "something2")), + ThemeOverrides("UUID8", DefaultTheme.DARK, wallpaper = ThemeWallpaper(imageFile = "image")), + ThemeOverrides("UUID10", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(imageFile = "image")) + ) + ) + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index 57371e25a7..dfffb826f5 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -4,8 +4,7 @@ import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -14,14 +13,14 @@ import androidx.compose.ui.input.key.* import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.window.* -import chat.simplex.common.model.ChatController -import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.common.views.TerminalView import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import java.awt.event.WindowEvent @@ -40,7 +39,8 @@ fun showApp() { WindowExceptionHandler { e -> AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.app_was_crashed), - text = e.stackTraceToString() + text = e.stackTraceToString(), + shareText = true ) Log.e(TAG, "App crashed, thread name: " + Thread.currentThread().name + ", exception: " + e.stackTraceToString()) window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING)) @@ -55,8 +55,15 @@ fun showApp() { // Better to not close fullscreen since it can contain passcode } else { // The last possible cause that can be closed - chatModel.chatId.value = null - chatModel.chatItems.clear() + withApi { + withContext(Dispatchers.Main) { + chatModel.chatId.value = null + chatModel.chatsContext.chatItems.clearAndNotify() + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value = null + } + } } chatModel.activeCall.value?.let { withBGApi { @@ -84,32 +91,38 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState<Boolean>) { position = WindowPosition(state.x.dp, state.y.dp) ) + val storingJob: MutableState<Job> = remember { mutableStateOf(Job()) } LaunchedEffect( windowState.position.x.value, windowState.position.y.value, windowState.size.width.value, windowState.size.height.value ) { - storeWindowState( - WindowPositionSize( - x = windowState.position.x.value.toInt(), - y = windowState.position.y.value.toInt(), - width = windowState.size.width.value.toInt(), - height = windowState.size.height.value.toInt() + storingJob.value.cancel() + storingJob.value = launch { + delay(1000L) + storeWindowState( + WindowPositionSize( + x = windowState.position.x.value.toInt(), + y = windowState.position.y.value.toInt(), + width = windowState.size.width.value.toInt(), + height = windowState.size.height.value.toInt() + ) ) - ) + } } simplexWindowState.windowState = windowState // Reload all strings in all @Composable's after language change at runtime if (remember { ChatController.appPrefs.appLanguage.state }.value != "") { - Window(state = windowState, onCloseRequest = { closedByError.value = false; exitApplication() }, onKeyEvent = { + Window(state = windowState, icon = painterResource(MR.images.ic_simplex), onCloseRequest = { closedByError.value = false; exitApplication() }, onKeyEvent = { if (it.key == Key.Escape && it.type == KeyEventType.KeyUp) { simplexWindowState.backstack.lastOrNull()?.invoke() != null } else { false } }, title = "SimpleX") { +// val hardwareAccelerationDisabled = remember { listOf(GraphicsApi.SOFTWARE_FAST, GraphicsApi.SOFTWARE_COMPAT, GraphicsApi.UNKNOWN).contains(window.renderApi) } simplexWindowState.window = window AppScreen() if (simplexWindowState.openDialog.isAwaiting) { @@ -164,7 +177,7 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState<Boolean>) { var windowFocused by remember { simplexWindowState.windowFocused } LaunchedEffect(windowFocused) { val delay = ChatController.appPrefs.laLockDelay.get() - if (!windowFocused && ChatModel.performLA.value && delay > 0) { + if (!windowFocused && ChatModel.showAuthScreen.value && delay > 0) { delay(delay * 1000L) // Trigger auth state check when delay ends (and if it ends) AppLock.recheckAuthState() @@ -186,13 +199,25 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState<Boolean>) { } } // Reload all strings in all @Composable's after language change at runtime - if (remember { ChatController.appPrefs.terminalAlwaysVisible.state }.value && remember { ChatController.appPrefs.appLanguage.state }.value != "") { + if (remember { ChatController.appPrefs.developerTools.state }.value && remember { ChatController.appPrefs.terminalAlwaysVisible.state }.value && remember { ChatController.appPrefs.appLanguage.state }.value != "") { var hiddenUntilRestart by remember { mutableStateOf(false) } if (!hiddenUntilRestart) { - val cWindowState = rememberWindowState(placement = WindowPlacement.Floating, width = DEFAULT_START_MODAL_WIDTH, height = 768.dp) + val cWindowState = rememberWindowState(placement = WindowPlacement.Floating, width = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier, height = + 768.dp) Window(state = cWindowState, onCloseRequest = { hiddenUntilRestart = true }, title = stringResource(MR.strings.chat_console)) { + val data = remember { ModalData() } SimpleXTheme { - TerminalView(ChatModel) { hiddenUntilRestart = true } + CompositionLocalProvider(LocalAppBarHandler provides data.appBarHandler) { + ModalView({ hiddenUntilRestart = true }) { + TerminalView(true) + } + ModalManager.floatingTerminal.showInView() + DisposableEffect(Unit) { + onDispose { + ModalManager.floatingTerminal.closeModals() + } + } + } } } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt index cb34bdb3b0..ee415ae82b 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt @@ -8,16 +8,19 @@ import chat.simplex.common.views.call.RcvCallInvitation import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import com.sshtools.twoslices.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import java.awt.* import java.awt.TrayIcon.MessageType import java.io.File import javax.imageio.ImageIO object NtfManager { - private val prevNtfs = arrayListOf<Pair<ChatId, Slice>>() + private val prevNtfs = arrayListOf<Pair<Pair<Long, ChatId>, Slice>>() + private val prevNtfsMutex: Mutex = Mutex() - fun notifyCallInvitation(invitation: RcvCallInvitation) { - if (simplexWindowState.windowFocused.value) return + fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean { + if (simplexWindowState.windowFocused.value) return false val contactId = invitation.contact.id Log.d(TAG, "notifyCallInvitation $contactId") val image = invitation.contact.image @@ -42,29 +45,53 @@ object NtfManager { generalGetString(MR.strings.accept) to { ntfManager.acceptCallAction(invitation.contact.id) }, generalGetString(MR.strings.reject) to { ChatModel.callManager.endCall(invitation = invitation) } ) - displayNotificationViaLib(contactId, title, text, prepareIconPath(largeIcon), actions) { + displayNotificationViaLib(invitation.user.userId, contactId, title, text, prepareIconPath(largeIcon), actions) { ntfManager.openChatAction(invitation.user.userId, contactId) } + return true + } + + fun showMessage(title: String, text: String) { + displayNotificationViaLib(-1, "MESSAGE", title, text, null, emptyList()) {} } fun hasNotificationsForChat(chatId: ChatId) = false//prevNtfs.any { it.first == chatId } fun cancelNotificationsForChat(chatId: ChatId) { - val ntf = prevNtfs.firstOrNull { it.first == chatId } - if (ntf != null) { - prevNtfs.remove(ntf) - /*try { - ntf.second.close() - } catch (e: Exception) { - // Can be java.lang.UnsupportedOperationException, for example. May do nothing - println("Failed to close notification: ${e.stackTraceToString()}") - }*/ + withBGApi { + prevNtfsMutex.withLock { + val ntf = prevNtfs.firstOrNull { (userChat) -> userChat.second == chatId } + if (ntf != null) { + prevNtfs.remove(ntf) + /*try { + ntf.second.close() + } catch (e: Exception) { + // Can be java.lang.UnsupportedOperationException, for example. May do nothing + Log.e(TAG, "Failed to close notification: ${e.stackTraceToString()}") + }*/ + } + } + } + } + + fun cancelNotificationsForUser(userId: Long) { + withBGApi { + prevNtfsMutex.withLock { + prevNtfs.filter { (userChat) -> userChat.first == userId }.forEach { + prevNtfs.remove(it) + } + } } } fun cancelAllNotifications() { -// prevNtfs.forEach { try { it.second.close() } catch (e: Exception) { println("Failed to close notification: ${e.stackTraceToString()}") } } - prevNtfs.clear() +// prevNtfs.forEach { try { it.second.close() } catch (e: Exception) { Log.e(TAG, "Failed to close notification: ${e + // .stackTraceToString()}") } } + withBGApi { + prevNtfsMutex.withLock { + prevNtfs.clear() + } + } } fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String?, actions: List<Pair<NotificationAction, () -> Unit>>) { @@ -79,12 +106,13 @@ object NtfManager { else -> base64ToBitmap(image) } - displayNotificationViaLib(chatId, title, content, prepareIconPath(largeIcon), actions.map { it.first.name to it.second }) { + displayNotificationViaLib(user.userId, chatId, title, content, prepareIconPath(largeIcon), actions.map { it.first.name to it.second }) { ntfManager.openChatAction(user.userId, chatId) } } private fun displayNotificationViaLib( + userId: Long, chatId: String, title: String, text: String, @@ -105,9 +133,17 @@ object NtfManager { builder.action(it.first, it.second) } try { - prevNtfs.add(chatId to builder.toast()) - } catch (e: Exception) { + withBGApi { + prevNtfsMutex.withLock { + prevNtfs.add(Pair(userId, chatId) to builder.toast()) + } + } + } catch (e: Throwable) { Log.e(TAG, e.stackTraceToString()) + if (e !is Exception) { + val text = e.stackTraceToString().lines().getOrNull(0) ?: "" + showToast(generalGetString(MR.strings.error_showing_desktop_notification) + " " + text, 4_000) + } } } @@ -118,7 +154,7 @@ object NtfManager { ImageIO.write(icon.toAwtImage(), "PNG", newFile.outputStream()) newFile.absolutePath } catch (e: Exception) { - println("Failed to write an icon to tmpDir: ${e.stackTraceToString()}") + Log.e(TAG, "Failed to write an icon to tmpDir: ${e.stackTraceToString()}") null } } else null diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt index 92111f162a..d425d81875 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt @@ -1,41 +1,47 @@ package chat.simplex.common.platform import chat.simplex.common.model.* +import chat.simplex.common.simplexWindowState import chat.simplex.common.views.call.RcvCallInvitation -import chat.simplex.common.views.helpers.generalGetString -import chat.simplex.common.views.helpers.withBGApi +import chat.simplex.common.views.database.deleteOldChatArchive +import chat.simplex.common.views.helpers.* import java.util.* import chat.simplex.res.MR +import java.io.File actual val appPlatform = AppPlatform.DESKTOP actual val deviceName = generalGetString(MR.strings.desktop_device) +actual fun isAppVisibleAndFocused() = simplexWindowState.windowFocused.value + @Suppress("ConstantLocale") val defaultLocale: Locale = Locale.getDefault() fun initApp() { ntfManager = object : NtfManager() { - override fun notifyCallInvitation(invitation: RcvCallInvitation) = chat.simplex.common.model.NtfManager.notifyCallInvitation(invitation) + override fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean = chat.simplex.common.model.NtfManager.notifyCallInvitation(invitation) override fun hasNotificationsForChat(chatId: String): Boolean = chat.simplex.common.model.NtfManager.hasNotificationsForChat(chatId) override fun cancelNotificationsForChat(chatId: String) = chat.simplex.common.model.NtfManager.cancelNotificationsForChat(chatId) + override fun cancelNotificationsForUser(userId: Long) = chat.simplex.common.model.NtfManager.cancelNotificationsForUser(userId) override fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String?, actions: List<Pair<NotificationAction, () -> Unit>>) = chat.simplex.common.model.NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions) override fun androidCreateNtfChannelsMaybeShowAlert() {} override fun cancelCallNotification() {} override fun cancelAllNotifications() = chat.simplex.common.model.NtfManager.cancelAllNotifications() + override fun showMessage(title: String, text: String) = chat.simplex.common.model.NtfManager.showMessage(title, text) } applyAppLocale() - withBGApi { - initChatController() - runMigrations() + deleteOldChatArchive() + if (DatabaseUtils.ksSelfDestructPassword.get() == null) { + initChatControllerOnStart() } // LALAL //testCrypto() } -fun discoverVlcLibs(path: String) { - uk.co.caprica.vlcj.binding.LibC.INSTANCE.setenv("VLC_PLUGIN_PATH", path, 1) -} +//fun discoverVlcLibs(path: String) { +// uk.co.caprica.vlcj.binding.LibC.INSTANCE.setenv("VLC_PLUGIN_PATH", path, 1) +//} private fun applyAppLocale() { val lang = ChatController.appPrefs.appLanguage.get() diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt index 9b2368fcd3..f7a87e3ced 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt @@ -13,8 +13,12 @@ actual val dataDir: File = File(desktopPlatform.dataPath) actual val tmpDir: File = File(System.getProperty("java.io.tmpdir") + File.separator + "simplex").also { it.deleteOnExit() } actual val filesDir: File = File(dataDir.absolutePath + File.separator + "simplex_v1_files") actual val appFilesDir: File = filesDir +actual val wallpapersDir: File = File(dataDir.absolutePath + File.separator + "simplex_v1_assets" + File.separator + "wallpapers").also { it.mkdirs() } actual val coreTmpDir: File = File(dataDir.absolutePath + File.separator + "tmp") actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "simplex_v1" +actual val preferencesDir = File(desktopPlatform.configPath).also { it.parentFile.mkdirs() } +actual val preferencesTmpDir = File(desktopPlatform.configPath, "tmp") + .also { it.deleteRecursively() } actual val chatDatabaseFileName: String = "simplex_v1_chat.db" actual val agentDatabaseFileName: String = "simplex_v1_agent.db" @@ -24,9 +28,13 @@ actual val databaseExportDir: File = tmpDir actual val remoteHostsDir: File = File(dataDir.absolutePath + File.separator + "remote_hosts") actual fun desktopOpenDatabaseDir() { + desktopOpenDir(dataDir) +} + +actual fun desktopOpenDir(dir: File) { if (Desktop.isDesktopSupported()) { try { - Desktop.getDesktop().open(dataDir); + Desktop.getDesktop().open(dir); } catch (e: IOException) { Log.e(TAG, e.stackTraceToString()) AlertManager.shared.showAlertMsg( diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt index 0f7d140b0e..53f3301507 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt @@ -1,6 +1,8 @@ package chat.simplex.common.platform import androidx.compose.ui.graphics.* +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize import boofcv.io.image.ConvertBufferedImage import boofcv.struct.image.GrayU8 import chat.simplex.res.MR @@ -17,14 +19,14 @@ import javax.imageio.stream.MemoryCacheImageOutputStream import kotlin.math.sqrt private fun errorBitmap(): ImageBitmap = - ImageIO.read(ByteArrayInputStream(Base64.getDecoder().decode("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg=="))).toComposeImageBitmap() + ImageIO.read(ByteArrayInputStream(Base64.getMimeDecoder().decode("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg=="))).toComposeImageBitmap() actual fun base64ToBitmap(base64ImageString: String): ImageBitmap { val imageString = base64ImageString .removePrefix("data:image/png;base64,") .removePrefix("data:image/jpg;base64,") return try { - ImageIO.read(ByteArrayInputStream(Base64.getDecoder().decode(imageString))).toComposeImageBitmap() + ImageIO.read(ByteArrayInputStream(Base64.getMimeDecoder().decode(imageString))).toComposeImageBitmap() } catch (e: IOException) { Log.e(TAG, "base64ToBitmap error: $e") errorBitmap() @@ -67,8 +69,16 @@ actual fun cropToSquare(image: ImageBitmap): ImageBitmap { } else { yOffset = (image.height - side) / 2 } - // LALAL MAKE REAL CROP - return image + val croppedImage = ImageBitmap(side, side) + val canvas = Canvas(croppedImage) + canvas.drawImageRect( + image, + srcOffset = IntOffset(xOffset, yOffset), + srcSize = IntSize(side, side), + dstSize = IntSize(side, side), + paint = Paint() + ) + return croppedImage } actual fun compressImageStr(bitmap: ImageBitmap): String { @@ -77,7 +87,7 @@ actual fun compressImageStr(bitmap: ImageBitmap): String { return try { val encoded = Base64.getEncoder().encodeToString(compressImageData(bitmap, usePng).toByteArray()) "data:image/$ext;base64,$encoded" - } catch (e: IOException) { + } catch (e: Exception) { Log.e(TAG, "resizeImageToStrSize error: $e") throw e } @@ -206,3 +216,35 @@ fun BufferedImage.flip(vertically: Boolean, horizontally: Boolean): BufferedImag } return AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR).filter(this, null) } + +fun BufferedImage.saveInTmpFile(): File? { + val formats = arrayOf("jpg", "png") + for (format in formats) { + val tmpFile = File.createTempFile("image", ".$format", tmpDir) + try { + // May fail on JPG, using PNG as an alternative + val success = ImageIO.write(this, format, tmpFile) + if (success) { + tmpFile.deleteOnExit() + chatModel.filesToDelete.add(tmpFile) + return tmpFile + } else { + tmpFile.delete() + } + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + tmpFile.delete() + return null + } + } + return null +} + +fun BufferedImage.hasAlpha(): Boolean { + for (x in 0 until width) { + for (y in 0 until height) { + if (getRGB(x, y) == 0) return true + } + } + return false +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Log.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Log.desktop.kt index 395754c51b..2b75a41cd3 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Log.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Log.desktop.kt @@ -1,8 +1,10 @@ package chat.simplex.common.platform +import chat.simplex.common.model.ChatController.appPrefs + actual object Log { - actual fun d(tag: String, text: String) = println("D: $text") - actual fun e(tag: String, text: String) = println("E: $text") - actual fun i(tag: String, text: String) = println("I: $text") - actual fun w(tag: String, text: String) = println("W: $text") + actual fun d(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.DEBUG && appPrefs.developerTools.get()) println("D: $text") } + actual fun e(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.ERROR || !appPrefs.developerTools.get()) println("E: $text") } + actual fun i(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.INFO && appPrefs.developerTools.get()) println("I: $text") } + actual fun w(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.WARNING || !appPrefs.developerTools.get()) println("W: $text") } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt index 6f317acb92..b090e301d5 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt @@ -1,38 +1,87 @@ package chat.simplex.common.platform import androidx.compose.foundation.contextMenuOpenDetector +import androidx.compose.foundation.draganddrop.dragAndDropTarget import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.* -import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.draganddrop.* +import androidx.compose.ui.draganddrop.DragData +import androidx.compose.ui.input.pointer.* +import java.awt.Image +import java.awt.datatransfer.DataFlavor +import java.awt.datatransfer.Transferable +import java.awt.image.BufferedImage import java.io.File import java.net.URI -actual fun Modifier.navigationBarsWithImePadding(): Modifier = this - -@Composable -actual fun ProvideWindowInsets( - consumeWindowInsets: Boolean, - windowInsetsAnimationsEnabled: Boolean, - content: @Composable () -> Unit -) { - content() -} - @Composable actual fun Modifier.desktopOnExternalDrag( enabled: Boolean, onFiles: (List<File>) -> Unit, - onImage: (Painter) -> Unit, + onImage: (File) -> Unit, onText: (String) -> Unit -): Modifier = -onExternalDrag(enabled) { - when(val data = it.dragData) { - // data.readFiles() returns filePath in URI format (where spaces replaces with %20). But it's an error-prone idea to work later - // with such format when everywhere we use absolutePath in File() format - is DragData.FilesList -> onFiles(data.readFiles().map { URI.create(it).toFile() }) - is DragData.Image -> onImage(data.readImage()) - is DragData.Text -> onText(data.readText()) +): Modifier { + val callback = remember { + object : DragAndDropTarget { + override fun onDrop(event: DragAndDropEvent): Boolean { + when (val data = event.dragData()) { + // data.readFiles() returns filePath in URI format (where spaces replaces with %20). But it's an error-prone idea to work later + // with such format when everywhere we use absolutePath in File() format + is DragData.FilesList -> { + val files = data.readFiles() + // When dragging and dropping an image from browser, it comes to FilesList section but no files inside + if (files.isNotEmpty()) { + onFiles(files.map { URI.create(it).toFile() }) + } else { + try { + val transferable = event.awtTransferable + if (transferable.isDataFlavorSupported(DataFlavor.imageFlavor)) { + onImage(DragDataImageImpl(transferable).bufferedImage().saveInTmpFile() ?: return false) + } else { + return false + } + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + return false + } + } + } + is DragData.Image -> onImage(DragDataImageImpl(event.awtTransferable).bufferedImage().saveInTmpFile() ?: return false) + is DragData.Text -> onText(data.readText()) + } + return true + } + } + } + return dragAndDropTarget(shouldStartDragAndDrop = { true }, target = callback) +} + +// Copied from AwtDragData and modified +private class DragDataImageImpl(private val transferable: Transferable) { + fun bufferedImage(): BufferedImage = (transferable.getTransferData(DataFlavor.imageFlavor) as Image).bufferedImage() + private fun Image.bufferedImage(): BufferedImage { + if (this is BufferedImage && hasAlpha()) { + // Such image cannot be drawn as JPG, only PNG + return this + } + // Creating non-transparent image which can be drawn as JPG + val bufferedImage = BufferedImage(getWidth(null), getHeight(null), BufferedImage.TYPE_INT_RGB) + val g2 = bufferedImage.createGraphics() + try { + g2.drawImage(this, 0, 0, null) + } finally { + g2.dispose() + } + return bufferedImage } } actual fun Modifier.onRightClick(action: () -> Unit): Modifier = contextMenuOpenDetector { action() } + +actual fun Modifier.desktopPointerHoverIconHand(): Modifier = Modifier.pointerHoverIcon(PointerIcon.Hand) + +actual fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier = + this then Modifier + .onPointerEvent(PointerEventType.Enter) { action(true) } + .onPointerEvent(PointerEventType.Exit) { action(false) } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt index 9217551a8d..97de08b07e 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt @@ -8,12 +8,12 @@ private val unixConfigPath = (System.getenv("XDG_CONFIG_HOME") ?: "$home/.config private val unixDataPath = (System.getenv("XDG_DATA_HOME") ?: "$home/.local/share") + "/simplex" val desktopPlatform = detectDesktopPlatform() -enum class DesktopPlatform(val libExtension: String, val configPath: String, val dataPath: String) { - LINUX_X86_64("so", unixConfigPath, unixDataPath), - LINUX_AARCH64("so", unixConfigPath, unixDataPath), - WINDOWS_X86_64("dll", System.getenv("AppData") + File.separator + "SimpleX", System.getenv("AppData") + File.separator + "SimpleX"), - MAC_X86_64("dylib", unixConfigPath, unixDataPath), - MAC_AARCH64("dylib", unixConfigPath, unixDataPath); +enum class DesktopPlatform(val libExtension: String, val configPath: String, val dataPath: String, val githubAssetName: String) { + LINUX_X86_64("so", unixConfigPath, unixDataPath, "simplex-desktop-x86_64.AppImage"), + LINUX_AARCH64("so", unixConfigPath, unixDataPath, " simplex-desktop-aarch64.AppImage"), + WINDOWS_X86_64("dll", System.getenv("AppData") + File.separator + "SimpleX", System.getenv("AppData") + File.separator + "SimpleX", "simplex-desktop-windows-x86_64.msi"), + MAC_X86_64("dylib", unixConfigPath, unixDataPath, "simplex-desktop-macos-x86_64.dmg"), + MAC_AARCH64("dylib", unixConfigPath, unixDataPath, "simplex-desktop-macos-aarch64.dmg"); fun isLinux() = this == LINUX_X86_64 || this == LINUX_AARCH64 fun isWindows() = this == WINDOWS_X86_64 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 8016b18b12..41964b7d18 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 @@ -1,24 +1,23 @@ package chat.simplex.common.platform -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* +import androidx.compose.material.TextFieldDefaults.textFieldWithLabelPadding import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.* +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.input.key.* import androidx.compose.ui.platform.* import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.* import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import chat.simplex.common.views.chat.* @@ -45,24 +44,27 @@ import kotlin.text.substring actual fun PlatformTextField( composeState: MutableState<ComposeState>, sendMsgEnabled: Boolean, + disabledText: String?, sendMsgButtonDisabled: Boolean, textStyle: MutableState<TextStyle>, showDeleteTextButton: MutableState<Boolean>, - userIsObserver: Boolean, - onMessageChange: (String) -> Unit, + placeholder: String, + showVoiceButton: Boolean, + onMessageChange: (ComposeMessage) -> Unit, onUpArrow: () -> Unit, onFilesPasted: (List<URI>) -> Unit, + focusRequester: FocusRequester?, onDone: () -> Unit, ) { + val cs = composeState.value - val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current val keyboard = LocalSoftwareKeyboardController.current - val padding = PaddingValues(12.dp, 12.dp, 45.dp, 0.dp) + val focusReq = focusRequester ?: remember { FocusRequester() } LaunchedEffect(cs.contextItem) { if (cs.contextItem !is ComposeContextItem.QuotedItem) return@LaunchedEffect // In replying state - focusRequester.requestFocus() + focusReq.requestFocus() delay(50) keyboard?.show() } @@ -73,9 +75,22 @@ actual fun PlatformTextField( keyboard?.hide() } } - val isRtl = remember(cs.message) { isRtl(cs.message.subSequence(0, min(50, cs.message.length))) } - var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = cs.message)) } - val textFieldValue = textFieldValueState.copy(text = cs.message) + val lastTimeWasRtlByCharacters = remember { mutableStateOf(isRtl(cs.message.text.subSequence(0, min(50, cs.message.text.length)))) } + val isRtlByCharacters = remember(cs.message) { + if (cs.message.text.isNotEmpty()) isRtl(cs.message.text.subSequence(0, min(50, cs.message.text.length))) else lastTimeWasRtlByCharacters.value + } + LaunchedEffect(isRtlByCharacters) { + lastTimeWasRtlByCharacters.value = isRtlByCharacters + } + val isLtrGlobally = LocalLayoutDirection.current == LayoutDirection.Ltr + // 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 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) val clipboard = LocalClipboardManager.current BasicTextField( value = textFieldValue, @@ -91,18 +106,20 @@ actual fun PlatformTextField( } } textFieldValueState = it - onMessageChange(it.text) + onMessageChange(ComposeMessage(it.text, it.selection)) } }, textStyle = textStyle.value, maxLines = 16, keyboardOptions = KeyboardOptions.Default.copy( capitalization = KeyboardCapitalization.Sentences, - autoCorrect = true + autoCorrectEnabled = true ), modifier = Modifier - .padding(vertical = 4.dp) - .focusRequester(focusRequester) + .padding(start = startPadding, end = endPadding) + .offset(y = (-5).dp) + .fillMaxWidth() + .focusRequester(focusReq) .onPreviewKeyEvent { if ((it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyDown) { if (it.isShiftPressed) { @@ -113,12 +130,12 @@ actual fun PlatformTextField( text = newText, selection = TextRange(textFieldValue.selection.min + 1) ) - onMessageChange(newText) + onMessageChange(ComposeMessage(newText, textFieldValueState.selection)) } else if (!sendMsgButtonDisabled) { onDone() } true - } else if (it.key == Key.DirectionUp && it.type == KeyEventType.KeyDown && cs.message.isEmpty()) { + } else if (it.key == Key.DirectionUp && it.type == KeyEventType.KeyDown && cs.message.text.isEmpty()) { onUpArrow() true } else if (it.key == Key.V && @@ -150,7 +167,7 @@ actual fun PlatformTextField( chatModel.filesToDelete.add(tempFile) tempFile.writeBytes(bytes) - composeState.processPickedMedia(listOf(tempFile.toURI()), composeState.value.message) + composeState.processPickedMedia(listOf(tempFile.toURI()), composeState.value.message.text) } } catch (e: Exception) { Log.e(TAG, "Pasting image exception: ${e.stackTraceToString()}") @@ -165,40 +182,37 @@ actual fun PlatformTextField( }, cursorBrush = SolidColor(MaterialTheme.colors.secondary), decorationBox = { innerTextField -> - Surface( - shape = RoundedCornerShape(18.dp), - border = BorderStroke(1.dp, MaterialTheme.colors.secondary) - ) { - Row( - Modifier.background(MaterialTheme.colors.background), - verticalAlignment = Alignment.Bottom + CompositionLocalProvider( + LocalLayoutDirection provides if (isRtlByCharacters) LayoutDirection.Rtl else LocalLayoutDirection.current ) { - CompositionLocalProvider( - LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current - ) { - Column(Modifier.weight(1f).padding(start = 12.dp, end = 32.dp)) { - Spacer(Modifier.height(8.dp)) - innerTextField() - Spacer(Modifier.height(10.dp)) - } - } + TextFieldDefaults.TextFieldDecorationBox( + value = textFieldValue.text, + innerTextField = innerTextField, + placeholder = { Text(placeholder, style = textStyle.value.copy(color = MaterialTheme.colors.secondary)) }, + singleLine = false, + enabled = true, + isError = false, + trailingIcon = null, + interactionSource = remember { MutableInteractionSource() }, + contentPadding = textFieldWithLabelPadding(start = 0.dp, end = 0.dp), + visualTransformation = VisualTransformation.None, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified) + ) } - } - }, - + } ) - showDeleteTextButton.value = cs.message.split("\n").size >= 4 && !cs.inProgress + showDeleteTextButton.value = cs.message.text.split("\n").size >= 4 && !cs.inProgress if (composeState.value.preview is ComposePreview.VoicePreview) { - ComposeOverlay(MR.strings.voice_message_send_text, textStyle, padding) - } else if (userIsObserver) { - ComposeOverlay(MR.strings.you_are_observer, textStyle, padding) + ComposeOverlay(generalGetString(MR.strings.voice_message_send_text), textStyle, padding) + } else if (disabledText != null) { + ComposeOverlay(disabledText, textStyle, padding) } } @Composable -private fun ComposeOverlay(textId: StringResource, textStyle: MutableState<TextStyle>, padding: PaddingValues) { +private fun ComposeOverlay(text: String, textStyle: MutableState<TextStyle>, padding: PaddingValues) { Text( - generalGetString(textId), + text, Modifier.padding(padding), color = MaterialTheme.colors.secondary, style = textStyle.value.copy(fontStyle = FontStyle.Italic) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt index b6d18aaf8b..9f34891b37 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 @@ -25,18 +25,13 @@ actual class RecorderNative: RecorderInterface { } actual object AudioPlayer: AudioPlayerInterface { - val player by lazy { AudioPlayerComponent().mediaPlayer() } + private val player by lazy { AudioPlayerComponent().mediaPlayer() } - // Filepath: String, onProgressUpdate - private val currentlyPlaying: MutableState<Pair<CryptoFile, (position: Int?, state: TrackState) -> Unit>?> = mutableStateOf(null) + override val currentlyPlaying: MutableState<CurrentlyPlayingState?> = mutableStateOf(null) private var progressJob: Job? = null - enum class TrackState { - PLAYING, PAUSED, REPLACED - } - // Returns real duration of the track - private fun start(fileSource: CryptoFile, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? { + private fun start(fileSource: CryptoFile, smallView: Boolean, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? { val absoluteFilePath = if (fileSource.isAbsolutePath) fileSource.filePath else getAppFilePath(fileSource.filePath) if (!File(absoluteFilePath).exists()) { Log.e(TAG, "No such file: ${fileSource.filePath}") @@ -46,7 +41,7 @@ actual object AudioPlayer: AudioPlayerInterface { VideoPlayerHolder.stopAll() RecorderInterface.stopRecording?.invoke() val current = currentlyPlaying.value - if (current == null || current.first != fileSource || !player.status().isPlayable) { + if (current == null || current.fileSource != fileSource || !player.status().isPlayable || smallView != current.smallView) { stopListener() player.stop() runCatching { @@ -60,13 +55,13 @@ actual object AudioPlayer: AudioPlayerInterface { }.onFailure { Log.e(TAG, it.stackTraceToString()) fileSource.deleteTmpFile() - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.message) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.stackTraceToString()) return null } } if (seek != null) player.seekTo(seek) player.start() - currentlyPlaying.value = fileSource to onProgressUpdate + currentlyPlaying.value = CurrentlyPlayingState(fileSource, onProgressUpdate, smallView) progressJob = CoroutineScope(Dispatchers.Default).launch { onProgressUpdate(player.currentPosition, TrackState.PLAYING) while(isActive && (player.isPlaying || player.status().state() == State.OPENING)) { @@ -80,7 +75,11 @@ actual object AudioPlayer: AudioPlayerInterface { onProgressUpdate(player.currentPosition, TrackState.PLAYING) } onProgressUpdate(null, TrackState.PAUSED) - currentlyPlaying.value?.first?.deleteTmpFile() + currentlyPlaying.value?.fileSource?.deleteTmpFile() + // Since coroutine is still NOT canceled, means player ended (no stop/no pause). + if (smallView && isActive) { + stopListener() + } } return player.duration } @@ -103,7 +102,7 @@ actual object AudioPlayer: AudioPlayerInterface { // FileName or filePath are ok override fun stop(fileName: String?) { - if (fileName != null && currentlyPlaying.value?.first?.filePath?.endsWith(fileName) == true) { + if (fileName != null && currentlyPlaying.value?.fileSource?.filePath?.endsWith(fileName) == true) { stop() } } @@ -111,8 +110,8 @@ actual object AudioPlayer: AudioPlayerInterface { private fun stopListener() { val afterCoroutineCancel: CompletionHandler = { // Notify prev audio listener about stop - currentlyPlaying.value?.second?.invoke(null, TrackState.REPLACED) - currentlyPlaying.value?.first?.deleteTmpFile() + currentlyPlaying.value?.onProgressUpdate?.invoke(null, TrackState.REPLACED) + currentlyPlaying.value?.fileSource?.deleteTmpFile() currentlyPlaying.value = null } /** Preventing race by calling a code AFTER coroutine ends, so [TrackState] will be: @@ -133,11 +132,12 @@ actual object AudioPlayer: AudioPlayerInterface { progress: MutableState<Int>, duration: MutableState<Int>, resetOnEnd: Boolean, + smallView: Boolean, ) { if (progress.value == duration.value) { progress.value = 0 } - val realDuration = start(fileSource, progress.value) { pro, state -> + val realDuration = start(fileSource, smallView = smallView, progress.value) { pro, state -> if (pro != null) { progress.value = pro } @@ -162,7 +162,7 @@ actual object AudioPlayer: AudioPlayerInterface { override fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) { pro.value = ms - if (currentlyPlaying.value?.first?.filePath == filePath) { + if (currentlyPlaying.value?.fileSource?.filePath == filePath) { player.seekTo(ms) } } @@ -211,15 +211,13 @@ actual object SoundPlayer: SoundPlayerInterface { var playing = false override fun start(scope: CoroutineScope, sound: Boolean) { - withBGApi { - val tmpFile = File(tmpDir, UUID.randomUUID().toString()) - tmpFile.deleteOnExit() - SoundPlayer::class.java.getResource("/media/ring_once.mp3").openStream()!!.use { it.copyTo(tmpFile.outputStream()) } - playing = true - while (playing) { - if (sound) { - AudioPlayer.play(CryptoFile.plain(tmpFile.absolutePath), mutableStateOf(true), mutableStateOf(0), mutableStateOf(0), true) - } + val tmpFile = File(tmpDir, UUID.randomUUID().toString()) + tmpFile.deleteOnExit() + SoundPlayer::class.java.getResource("/media/ring_once.mp3")!!.openStream()!!.use { it.copyTo(tmpFile.outputStream()) } + playing = true + scope.launch { + while (playing && sound) { + AudioPlayer.play(CryptoFile.plain(tmpFile.absolutePath), mutableStateOf(true), mutableStateOf(0), mutableStateOf(0), resetOnEnd = true, smallView = false) delay(3500) } } @@ -230,3 +228,37 @@ actual object SoundPlayer: SoundPlayerInterface { AudioPlayer.stop() } } + +actual object CallSoundsPlayer: CallSoundsPlayerInterface { + private var playingJob: Job? = null + + private fun start(soundPath: String, delay: Long, scope: CoroutineScope) { + playingJob?.cancel() + val tmpFile = File(tmpDir, UUID.randomUUID().toString()) + tmpFile.deleteOnExit() + SoundPlayer::class.java.getResource(soundPath)!!.openStream()!!.use { it.copyTo(tmpFile.outputStream()) } + playingJob = scope.launch { + while (isActive) { + AudioPlayer.play(CryptoFile.plain(tmpFile.absolutePath), mutableStateOf(true), mutableStateOf(0), mutableStateOf(0), resetOnEnd = true, smallView = false) + delay(delay) + } + } + } + + override fun startConnectingCallSound(scope: CoroutineScope) { + // Taken from https://github.com/TelegramOrg/Telegram-Android + // https://github.com/TelegramOrg/Telegram-Android/blob/master/LICENSE + start("/media/connecting_call.mp3", 3000, scope) + } + + override fun startInCallSound(scope: CoroutineScope) { + start("/media/in_call.mp3", 5000, scope) + } + + override fun vibrate(times: Int) {} + + override fun stop() { + playingJob?.cancel() + AudioPlayer.stop() + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt index b758988227..4e7594f998 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt @@ -1,15 +1,23 @@ package chat.simplex.common.platform import androidx.compose.runtime.* +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.* import chat.simplex.common.simplexWindowState +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import com.jthemedetecor.OsThemeDetector import com.russhwolf.settings.* +import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.desc.desc import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption import java.util.* @Composable @@ -18,23 +26,69 @@ actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle) actual fun StringResource.localized(): String = desc().toString() -actual fun isInNightMode() = false +private val detector: OsThemeDetector = OsThemeDetector.getDetector() +actual fun isInNightMode() = try { + detector.isDark +} +catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + /* On Mac this code can produce exception */ + false +} private val settingsFile = File(desktopPlatform.configPath + File.separator + "settings.properties") - .also { it.parentFile.mkdirs() } private val settingsThemesFile = File(desktopPlatform.configPath + File.separator + "themes.properties") - .also { it.parentFile.mkdirs() } + private val settingsProps = Properties() - .also { try { it.load(settingsFile.reader()) } catch (e: Exception) { Properties() } } + .also { props -> + if (!settingsFile.exists()) return@also + + try { + settingsFile.reader().use { + // Force exception to happen + //it.close() + props.load(it) + } + } catch (e: Exception) { + Log.e(TAG, "Error reading settings file: ${e.stackTraceToString()}") + } + } private val settingsThemesProps = Properties() - .also { try { it.load(settingsThemesFile.reader()) } catch (e: Exception) { Properties() } } + .also { props -> try { settingsThemesFile.reader().use { props.load(it) } } catch (e: Exception) { /**/ } } -actual val settings: Settings = PropertiesSettings(settingsProps) { settingsProps.store(settingsFile.writer(), "") } -actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) { settingsThemesProps.store(settingsThemesFile.writer(), "") } +private const val lock = "settingsSaver" +actual val settings: Settings = PropertiesSettings(settingsProps) { + synchronized(lock) { + try { + createTmpFileAndDelete(preferencesTmpDir) { tmpFile -> + tmpFile.writer().use { settingsProps.store(it, "") } + settingsFile.parentFile.mkdirs() + Files.move(tmpFile.toPath(), settingsFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + } catch (e: Exception) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.prefs_error_saving_settings), e.stackTraceToString()) + throw e + } + } +} +actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) { + synchronized(lock) { + try { + createTmpFileAndDelete(preferencesTmpDir) { tmpFile -> + tmpFile.writer().use { settingsThemesProps.store(it, "") } + settingsThemesFile.parentFile.mkdirs() + Files.move(tmpFile.toPath(), settingsThemesFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + } catch (e: Exception) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.prefs_error_saving_settings), e.stackTraceToString()) + throw e + } + } +} actual fun windowOrientation(): WindowOrientation = if (simplexWindowState.windowState.size.width > simplexWindowState.windowState.size.height) { @@ -46,6 +100,9 @@ actual fun windowOrientation(): WindowOrientation = @Composable actual fun windowWidth(): Dp = simplexWindowState.windowState.size.width +@Composable +actual fun windowHeight(): Dp = simplexWindowState.windowState.size.height + actual fun desktopExpandWindowToWidth(width: Dp) { if (simplexWindowState.windowState.size.width >= width) return simplexWindowState.windowState.size = simplexWindowState.windowState.size.copy(width = width) @@ -58,3 +115,6 @@ actual fun isRtl(text: CharSequence): Boolean { dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT || dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC } } + +actual fun ImageResource.toComposeImageBitmap(): ImageBitmap? = + image.toComposeImageBitmap() diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt new file mode 100644 index 0000000000..7a2a1dff0a --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt @@ -0,0 +1,326 @@ +package chat.simplex.common.platform + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.interaction.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.input.pointer.* +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.OnboardingStage +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.filter +import kotlin.math.* + +@Composable +actual fun LazyColumnWithScrollBar( + modifier: Modifier, + state: LazyListState?, + contentPadding: PaddingValues, + reverseLayout: Boolean, + verticalArrangement: Arrangement.Vertical, + horizontalAlignment: Alignment.Horizontal, + flingBehavior: FlingBehavior, + userScrollEnabled: Boolean, + additionalBarOffset: State<Dp>?, + additionalTopBar: State<Boolean>, + chatBottomBar: State<Boolean>, + fillMaxSize: Boolean, + content: LazyListScope.() -> Unit +) { + val handler = LocalAppBarHandler.current + require(handler != null) { "Using LazyColumnWithScrollBar and without AppBarHandler is an error. Use LazyColumnWithScrollBarNoAppBar instead" } + + val scope = rememberCoroutineScope() + val scrollBarAlpha = remember { Animatable(0f) } + val scrollJob: MutableState<Job> = remember { mutableStateOf(Job()) } + val scrollModifier = remember { + Modifier + .pointerInput(Unit) { + detectCursorMove { + scope.launch { + scrollBarAlpha.animateTo(1f) + } + scrollJob.value.cancel() + scrollJob.value = scope.launch { + delay(1000L) + scrollBarAlpha.animateTo(0f) + } + } + } + } + val state = state ?: handler.listState + val connection = handler.connection + // When scroll bar is dragging, there is no scroll event in nested scroll modifier. So, listen for changes on lazy column state + // (only first visible row is useful because LazyColumn doesn't have absolute scroll position, only relative to row) + val scrollBarDraggingState = remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + if (reverseLayout) { + snapshotFlow { state.layoutInfo.visibleItemsInfo.lastOrNull()?.offset ?: 0 } + .collect { scrollPosition -> + connection.appBarOffset = if (state.layoutInfo.visibleItemsInfo.lastOrNull()?.index == state.layoutInfo.totalItemsCount - 1) { + state.layoutInfo.viewportEndOffset - scrollPosition.toFloat() - state.layoutInfo.afterContentPadding + } else { + // show always when last item is not visible + -1000f + } + //Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } + } else { + snapshotFlow { state.firstVisibleItemScrollOffset } + .filter { state.firstVisibleItemIndex == 0 } + .collect { scrollPosition -> + val offset = connection.appBarOffset + if ((offset + scrollPosition + state.layoutInfo.afterContentPadding).absoluteValue > 1 || scrollBarDraggingState.value) { + connection.appBarOffset = -scrollPosition.toFloat() + //Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } + } + } + } + val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier) else modifier + Box(Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).nestedScroll(connection)) { + LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) + ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset, additionalTopBar, chatBottomBar) + } +} + +@Composable +actual fun LazyColumnWithScrollBarNoAppBar( + modifier: Modifier, + state: LazyListState?, + contentPadding: PaddingValues, + reverseLayout: Boolean, + verticalArrangement: Arrangement.Vertical, + horizontalAlignment: Alignment.Horizontal, + flingBehavior: FlingBehavior, + userScrollEnabled: Boolean, + additionalBarOffset: State<Dp>?, + additionalTopBar: State<Boolean>, + chatBottomBar: State<Boolean>, + maxHeight: State<Dp>?, + containerAlignment: Alignment, + content: LazyListScope.() -> Unit, +) { + val scope = rememberCoroutineScope() + val scrollBarAlpha = remember { Animatable(0f) } + val scrollJob: MutableState<Job> = remember { mutableStateOf(Job()) } + val scrollModifier = remember { + Modifier + .pointerInput(Unit) { + detectCursorMove { + scope.launch { + scrollBarAlpha.animateTo(1f) + } + scrollJob.value.cancel() + scrollJob.value = scope.launch { + delay(1000L) + scrollBarAlpha.animateTo(0f) + } + } + } + } + val state = state ?: rememberLazyListState() + // When scroll bar is dragging, there is no scroll event in nested scroll modifier. So, listen for changes on lazy column state + // (only first visible row is useful because LazyColumn doesn't have absolute scroll position, only relative to row) + val scrollBarDraggingState = remember { mutableStateOf(false) } + Box(contentAlignment = containerAlignment) { + LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) + Box(if (maxHeight?.value != null) Modifier.height(maxHeight.value).fillMaxWidth() else Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { + DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout, scrollBarDraggingState) + } + } +} + +@Composable +private fun ScrollBar( + reverseLayout: Boolean, + state: LazyListState, + scrollBarAlpha: Animatable<Float, AnimationVector1D>, + scrollJob: MutableState<Job>, + scrollBarDraggingState: MutableState<Boolean>, + additionalBarHeight: State<Dp>?, + additionalTopBar: State<Boolean>, + chatBottomBar: State<Boolean>, +) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val topBarPadding = if (additionalTopBar.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp + val padding = if (additionalBarHeight != null) { + PaddingValues(top = topBarPadding + if (oneHandUI.value && chatBottomBar.value) 0.dp else AppBarHeight * fontSizeSqrtMultiplier, bottom = additionalBarHeight.value) + } else if (reverseLayout) { + PaddingValues(bottom = AppBarHeight * fontSizeSqrtMultiplier) + } else { + PaddingValues(top = if (oneHandUI.value && chatBottomBar.value) 0.dp else AppBarHeight * fontSizeSqrtMultiplier) + } + Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.CenterEnd) { + DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout, scrollBarDraggingState) + } +} + +@Composable +actual fun ColumnWithScrollBar( + modifier: Modifier, + verticalArrangement: Arrangement.Vertical, + horizontalAlignment: Alignment.Horizontal, + state: ScrollState?, + maxIntrinsicSize: Boolean, + fillMaxSize: Boolean, + content: @Composable() (ColumnScope.() -> Unit) +) { + val handler = LocalAppBarHandler.current + require(handler != null) { "Using ColumnWithScrollBar and without AppBarHandler is an error. Use ColumnWithScrollBarNoAppBar instead" } + + val scope = rememberCoroutineScope() + val scrollBarAlpha = remember { Animatable(0f) } + val scrollJob: MutableState<Job> = remember { mutableStateOf(Job()) } + val scrollModifier = remember { + Modifier + .pointerInput(Unit) { + detectCursorMove { + scope.launch { + scrollBarAlpha.animateTo(1f) + } + scrollJob.value.cancel() + scrollJob.value = scope.launch { + delay(1000L) + scrollBarAlpha.animateTo(0f) + } + } + } + } + val state = state ?: handler.scrollState + val connection = handler.connection + // When scroll bar is dragging, there is no scroll event in nested scroll modifier. So, listen for changes on column state + // (exact scroll position is available but in Int, not Float) + val scrollBarDraggingState = remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + snapshotFlow { state.value } + .collect { scrollPosition -> + val offset = connection.appBarOffset + if ((offset + scrollPosition).absoluteValue > 1 || scrollBarDraggingState.value) { + connection.appBarOffset = -scrollPosition.toFloat() +// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } + } + } + val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier) else modifier + Box(Modifier.nestedScroll(connection)) { + val oneHandUI = remember { derivedStateOf { if (appPrefs.onboardingStage.state.value == OnboardingStage.OnboardingComplete) appPrefs.oneHandUI.state.value else false } } + val padding = if (oneHandUI.value) PaddingValues(bottom = AppBarHeight * fontSizeSqrtMultiplier) else PaddingValues(top = AppBarHeight * fontSizeSqrtMultiplier) + Column( + if (maxIntrinsicSize) { + modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).verticalScroll(state).height(IntrinsicSize.Max).then(scrollModifier) + } else { + modifier.then(scrollModifier).copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).verticalScroll(state) + }, + verticalArrangement, horizontalAlignment + ) { + Spacer(if (oneHandUI.value) Modifier.padding(top = DEFAULT_PADDING + 5.dp) else Modifier.padding(padding)) + content() + if (oneHandUI.value) { + Spacer(Modifier.padding(padding)) + } + } + Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.CenterEnd) { + DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, false, scrollBarDraggingState) + } + } +} + +@Composable +actual fun ColumnWithScrollBarNoAppBar( + modifier: Modifier, + verticalArrangement: Arrangement.Vertical, + horizontalAlignment: Alignment.Horizontal, + state: ScrollState?, + maxIntrinsicSize: Boolean, + content: @Composable() (ColumnScope.() -> Unit) +) { + val scope = rememberCoroutineScope() + val scrollBarAlpha = remember { Animatable(0f) } + val scrollJob: MutableState<Job> = remember { mutableStateOf(Job()) } + val scrollModifier = remember { + Modifier + .pointerInput(Unit) { + detectCursorMove { + scope.launch { + scrollBarAlpha.animateTo(1f) + } + scrollJob.value.cancel() + scrollJob.value = scope.launch { + delay(1000L) + scrollBarAlpha.animateTo(0f) + } + } + } + } + val state = state ?: rememberScrollState() + // When scroll bar is dragging, there is no scroll event in nested scroll modifier. So, listen for changes on column state + // (exact scroll position is available but in Int, not Float) + val scrollBarDraggingState = remember { mutableStateOf(false) } + Box { + Column( + if (maxIntrinsicSize) { + modifier.verticalScroll(state).height(IntrinsicSize.Max).then(scrollModifier) + } else { + modifier.then(scrollModifier).verticalScroll(state) + }, + verticalArrangement, horizontalAlignment) { + content() + } + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { + DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, false, scrollBarDraggingState) + } + } +} + +@Composable +fun DesktopScrollBar(adapter: androidx.compose.foundation.v2.ScrollbarAdapter, modifier: Modifier, scrollBarAlpha: Animatable<Float, AnimationVector1D>, scrollJob: MutableState<Job>, reversed: Boolean, updateDraggingState: MutableState<Boolean> = remember { mutableStateOf(false) }) { + val scope = rememberCoroutineScope() + val interactionSource = remember { MutableInteractionSource() } + val isHovered by interactionSource.collectIsHoveredAsState() + val isDragged by interactionSource.collectIsDraggedAsState() + LaunchedEffect(isHovered, isDragged) { + scrollJob.value.cancel() + updateDraggingState.value = isDragged + if (isHovered || isDragged) { + scrollBarAlpha.animateTo(1f) + } else { + scrollJob.value = scope.launch { + delay(1000L) + scrollBarAlpha.animateTo(0f) + } + } + } + VerticalScrollbar( + modifier = modifier.graphicsLayer { alpha = scrollBarAlpha.value } + .onPointerEvent(PointerEventType.Enter) { + scrollJob.value.cancel() + scope.launch { + scrollBarAlpha.animateTo(1f) + } + }, + reverseLayout = reversed, + style = LocalScrollbarStyle.current.copy( + thickness = if (isHovered || isDragged) 10.dp else 6.dp, + shape = RoundedCornerShape(if (isHovered || isDragged) 5.dp else 3.dp), + unhoverColor = if (MaterialTheme.colors.isLight) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.7f) else MaterialTheme.colors.onBackground.mixWith(MaterialTheme.colors.background, 0.3f), + hoverColor = if (MaterialTheme.colors.isLight) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.6f) else MaterialTheme.colors.onBackground.mixWith(MaterialTheme.colors.background, 0.4f) + ), + adapter = adapter, + interactionSource = interactionSource + ) +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt index b40b892de4..a10f675085 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt @@ -4,12 +4,12 @@ import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.text.AnnotatedString import chat.simplex.common.model.* -import chat.simplex.common.views.helpers.generalGetString -import chat.simplex.common.views.helpers.withApi +import chat.simplex.common.views.helpers.* import java.io.File import java.net.URI import java.net.URLEncoder import chat.simplex.res.MR +import java.awt.Desktop actual fun UriHandler.sendEmail(subject: String, body: CharSequence) { val subjectEncoded = URLEncoder.encode(subject, "UTF-8").replace("+", "%20") @@ -23,7 +23,7 @@ actual fun ClipboardManager.shareText(text: String) { } actual fun shareFile(text: String, fileSource: CryptoFile) { - withApi { + withLongRunningApi { FileChooserLauncher(false) { to: URI? -> if (to != null) { val absolutePath = if (fileSource.isAbsolutePath) fileSource.filePath else getAppFilePath(fileSource.filePath) @@ -33,6 +33,7 @@ actual fun shareFile(text: String, fileSource: CryptoFile) { showToast(generalGetString(MR.strings.file_saved)) } catch (e: Exception) { Log.e(TAG, "Unable to decrypt crypto file: " + e.stackTraceToString()) + AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString()) } } else { copyFileToFile(File(absolutePath), to) {} @@ -41,3 +42,30 @@ actual fun shareFile(text: String, fileSource: CryptoFile) { }.launch(fileSource.filePath) } } + +actual fun openFile(fileSource: CryptoFile) { + try { + val filePath = filePathForShare(fileSource) ?: return + Desktop.getDesktop().open(File(filePath)) + } catch (e: Exception) { + Log.e(TAG, "Unable to open the file: " + e.stackTraceToString()) + AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString()) + } +} + +fun filePathForShare(fileSource: CryptoFile): String? { + return if (fileSource.cryptoArgs != null) { + val tmpFile = File(tmpDir, fileSource.filePath) + tmpFile.deleteOnExit() + try { + decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs ?: return null, tmpFile.absolutePath) + } catch (e: Exception) { + Log.e(TAG, "Unable to decrypt crypto file: " + e.stackTraceToString()) + AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString()) + return null + } + tmpFile.absolutePath + } else { + getAppFilePath(fileSource.filePath) + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt index b5a0e2e008..e49dc6f2b1 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt @@ -16,7 +16,7 @@ actual fun LocalMultiplatformView(): Any? = null @Composable actual fun getKeyboardState(): State<KeyboardState> = remember { mutableStateOf(KeyboardState.Opened) } -actual fun hideKeyboard(view: Any?) {} +actual fun hideKeyboard(view: Any?, clearFocus: Boolean) {} actual fun androidIsFinishingMainActivity(): Boolean = false 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 ec487bc884..50eeaee604 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 @@ -29,7 +29,7 @@ actual class VideoPlayer actual constructor( override val brokenVideo: MutableState<Boolean> = mutableStateOf(false) override val videoPlaying: MutableState<Boolean> = mutableStateOf(false) override val progress: MutableState<Long> = mutableStateOf(0L) - override val duration: MutableState<Long> = mutableStateOf(0L) + override val duration: MutableState<Long> = mutableStateOf(defaultDuration) override val preview: MutableState<ImageBitmap> = mutableStateOf(defaultPreview) val mediaPlayerComponent by lazy { runBlocking(playerThread.asCoroutineDispatcher()) { getOrCreatePlayer() } } @@ -70,7 +70,7 @@ actual class VideoPlayer actual constructor( } }.onFailure { Log.e(TAG, it.stackTraceToString()) - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.message) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.stackTraceToString()) brokenVideo.value = true return false } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt index 94268002a6..d7dc1ca859 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt @@ -1,5 +1,6 @@ package chat.simplex.common.ui.theme +import androidx.compose.runtime.Composable import chat.simplex.common.platform.Log import chat.simplex.common.platform.TAG import com.jthemedetecor.OsThemeDetector @@ -9,6 +10,11 @@ private val detector: OsThemeDetector = OsThemeDetector.getDetector() registerListener(::reactOnDarkThemeChanges) } +// TODO: explore possibility to use +//@Composable +//actual fun isSystemInDarkTheme(): Boolean = androidx.compose.foundation.isSystemInDarkTheme() + +@Composable actual fun isSystemInDarkTheme(): Boolean = try { detector.isDark } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt index 22d39409ed..e3b0642547 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt @@ -1,30 +1,15 @@ package chat.simplex.common.views.call -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.snapshots.SnapshotStateList -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.unit.dp import chat.simplex.common.model.* import chat.simplex.common.platform.* -import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.helpers.* import chat.simplex.res.MR -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.datetime.Clock -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import org.nanohttpd.protocols.http.IHTTPSession import org.nanohttpd.protocols.http.response.Response @@ -40,11 +25,7 @@ val connections = ArrayList<WebSocket>() @Composable actual fun ActiveCallView() { - val endCall = { - val call = chatModel.activeCall.value - if (call != null) withBGApi { chatModel.callManager.endCall(call) } - } - BackHandler(onBack = endCall) + val scope = rememberCoroutineScope() WebRTCController(chatModel.callCommand) { apiMsg -> Log.d(TAG, "received from WebRTCController: $apiMsg") val call = chatModel.activeCall.value @@ -53,17 +34,20 @@ actual fun ActiveCallView() { val callRh = call.remoteHostId when (val r = apiMsg.resp) { is WCallResponse.Capabilities -> withBGApi { - val callType = CallType(call.localMedia, r.capabilities) + val callType = CallType(call.initialCallType, r.capabilities) chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType) chatModel.activeCall.value = call.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) + CallSoundsPlayer.startConnectingCallSound(scope) + activeCallWaitDeliveryReceipt(scope) } is WCallResponse.Offer -> withBGApi { - chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities) + chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.initialCallType, r.capabilities) chatModel.activeCall.value = call.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities) } is WCallResponse.Answer -> withBGApi { chatModel.controller.apiSendCallAnswer(callRh, call.contact, r.answer, r.iceCandidates) chatModel.activeCall.value = call.copy(callState = CallState.Negotiated) + CallSoundsPlayer.stop() } is WCallResponse.Ice -> withBGApi { chatModel.controller.apiSendCallExtraInfo(callRh, call.contact, r.iceCandidates) @@ -75,12 +59,21 @@ actual fun ActiveCallView() { chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectedAt = Clock.System.now()) } withBGApi { chatModel.controller.apiCallStatus(callRh, call.contact, callStatus) } - } catch (e: Error) { + } catch (e: Throwable) { Log.d(TAG, "call status ${r.state.connectionState} not used") } is WCallResponse.Connected -> { chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo) } + is WCallResponse.PeerMedia -> { + val sources = call.peerMediaSources + chatModel.activeCall.value = when (r.source) { + CallMediaSource.Mic -> call.copy(peerMediaSources = sources.copy(mic = r.enabled)) + CallMediaSource.Camera -> call.copy(peerMediaSources = sources.copy(camera = r.enabled)) + CallMediaSource.ScreenAudio -> call.copy(peerMediaSources = sources.copy(screenAudio = r.enabled)) + CallMediaSource.ScreenVideo -> call.copy(peerMediaSources = sources.copy(screenVideo = r.enabled)) + } + } is WCallResponse.End -> { withBGApi { chatModel.callManager.endCall(call) } } @@ -93,15 +86,18 @@ actual fun ActiveCallView() { is WCallCommand.Answer -> chatModel.activeCall.value = call.copy(callState = CallState.Negotiated) is WCallCommand.Media -> { - when (cmd.media) { - CallMediaType.Video -> chatModel.activeCall.value = call.copy(videoEnabled = cmd.enable) - CallMediaType.Audio -> chatModel.activeCall.value = call.copy(audioEnabled = cmd.enable) + val sources = call.localMediaSources + when (cmd.source) { + CallMediaSource.Mic -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(mic = cmd.enable)) + CallMediaSource.Camera -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(camera = cmd.enable)) + CallMediaSource.ScreenAudio -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(screenAudio = cmd.enable)) + CallMediaSource.ScreenVideo -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(screenVideo = cmd.enable)) } } is WCallCommand.Camera -> { chatModel.activeCall.value = call.copy(localCamera = cmd.camera) - if (!call.audioEnabled) { - chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false)) + if (!call.localMediaSources.mic) { + chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = false)) } } is WCallCommand.End -> @@ -109,6 +105,14 @@ actual fun ActiveCallView() { else -> {} } is WCallResponse.Error -> { + when (apiMsg.command) { + is WCallCommand.Capabilities -> chatModel.callCommand.add(WCallCommand.Permission( + title = generalGetString(MR.strings.call_desktop_permission_denied_title), + chrome = generalGetString(MR.strings.call_desktop_permission_denied_chrome), + safari = generalGetString(MR.strings.call_desktop_permission_denied_safari) + )) + else -> {} + } Log.e(TAG, "ActiveCallView: command error ${r.message}") } } @@ -121,6 +125,7 @@ actual fun ActiveCallView() { // After the first call, End command gets added to the list which prevents making another calls chatModel.callCommand.removeAll { it is WCallCommand.End } onDispose { + CallSoundsPlayer.stop() chatModel.activeCallViewIsVisible.value = false chatModel.callCommand.clear() } @@ -146,8 +151,21 @@ private fun SendStateUpdates() { @Composable fun WebRTCController(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIMessage) -> Unit) { val uriHandler = LocalUriHandler.current + val endCall = { + val call = chatModel.activeCall.value + if (call != null) withBGApi { chatModel.callManager.endCall(call) } + } val server = remember { - uriHandler.openUri("http://${SERVER_HOST}:$SERVER_PORT/simplex/call/") + try { + uriHandler.openUri("http://${SERVER_HOST}:$SERVER_PORT/simplex/call/") + } catch (e: Exception) { + Log.e(TAG, "Unable to open browser: ${e.stackTraceToString()}") + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.unable_to_open_browser_title), + text = generalGetString(MR.strings.unable_to_open_browser_desc) + ) + endCall() + } startServer(onResponse) } fun processCommand(cmd: WCallCommand) { @@ -206,7 +224,7 @@ fun startServer(onResponse: (WVAPIMessage) -> Unit): NanoWSD { return when { session.headers["upgrade"] == "websocket" -> super.handle(session) session.uri.contains("/simplex/call/") -> resourcesToResponse("/desktop/call.html") - else -> resourcesToResponse(URI.create(session.uri).path) + else -> resourcesToResponse(uriCreateOrNull(session.uri)?.path ?: return newFixedLengthResponse("Error parsing URL")) } } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.desktop.kt new file mode 100644 index 0000000000..eceb7de9be --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.desktop.kt @@ -0,0 +1,18 @@ +package chat.simplex.common.views.chat.item + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import chat.simplex.common.model.CryptoFile +import java.net.URI + +@Composable +actual fun SaveOrOpenFileMenu( + showMenu: MutableState<Boolean>, + encrypted: Boolean, + ext: String?, + encryptedUri: URI, + fileSource: CryptoFile, + saveFile: () -> Unit +) { + +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt index 6da2078567..38054cb873 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt @@ -13,6 +13,7 @@ actual fun SimpleAndAnimatedImageView( imageBitmap: ImageBitmap, file: CIFile?, imageProvider: () -> ImageGalleryProvider, + smallView: Boolean, ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit ) { // LALAL make it animated too diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt index 8dac39199f..bdfbf6863f 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt @@ -5,8 +5,11 @@ import androidx.compose.foundation.layout.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.unit.Dp import chat.simplex.common.platform.* +import chat.simplex.common.simplexWindowState +import java.awt.Window @Composable actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) { @@ -23,14 +26,18 @@ actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLon } } +/* +* This function doesn't take into account multi-window environment. In case more windows will be used, modify the code +* */ @Composable -actual fun LocalWindowWidth(): Dp { - return with(LocalDensity.current) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() } - /*val density = LocalDensity.current - var width by remember { mutableStateOf(with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() }) } - SideEffect { - if (width != with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() }) - width = with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() } +actual fun LocalWindowWidth(): Dp = with(LocalDensity.current) { + val windows = java.awt.Window.getWindows() + if (windows.size == 1) { + (windows.getOrNull(0)?.width ?: 0).toDp() + } else { + simplexWindowState.windowState.size.width } - return width.also { println("LALAL $it") }*/ } + +@Composable +actual fun LocalWindowHeight(): Dp = with(LocalDensity.current) { LocalWindowInfo.current.containerSize.height.toDp() } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt index 905a6e3520..cd206c8e4e 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt @@ -35,14 +35,14 @@ actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserL ItemAction(stringResource(MR.strings.save_verb), painterResource(if (cItem.file?.fileSource?.cryptoArgs == null) MR.images.ic_download else MR.images.ic_lock_open_right), onClick = { val saveIfExists = { when (cItem.content.msgContent) { - is MsgContent.MCImage, is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") } + is MsgContent.MCImage, is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withLongRunningApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") } else -> {} } showMenu.value = false } var fileSource = getLoadedFileSource(cItem.file) if (chatModel.connectedToRemote() && fileSource == null) { - withBGApi { + withLongRunningApi(slow = 600_000) { cItem.file?.loadRemoteFile(true) fileSource = getLoadedFileSource(cItem.file) saveIfExists() @@ -51,7 +51,7 @@ actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserL }) } -actual fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) = withBGApi { +actual fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) = withLongRunningApi(slow = 600_000) { var fileSource = getLoadedFileSource(cItem.file) if (chatModel.connectedToRemote() && fileSource == null) { cItem.file?.loadRemoteFile(true) @@ -59,19 +59,7 @@ actual fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) = w } if (fileSource != null) { - val filePath: String = if (fileSource.cryptoArgs != null) { - val tmpFile = File(tmpDir, fileSource.filePath) - tmpFile.deleteOnExit() - try { - decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs ?: return@withBGApi, tmpFile.absolutePath) - } catch (e: Exception) { - Log.e(TAG, "Unable to decrypt crypto file: " + e.stackTraceToString()) - return@withBGApi - } - tmpFile.absolutePath - } else { - getAppFilePath(fileSource.filePath) - } + val filePath = filePathForShare(fileSource) ?: return@withLongRunningApi when { desktopPlatform.isWindows() -> clipboard.setText(AnnotatedString("\"${File(filePath).absolutePath}\"")) else -> clipboard.setText(AnnotatedString(filePath)) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt index 6c37c93ccc..a1f70213d0 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt @@ -1,29 +1,28 @@ package chat.simplex.common.views.chatlist +import SectionDivider import androidx.compose.foundation.* import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.material.Divider -import androidx.compose.material.MaterialTheme +import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.node.DelegatableNode +import androidx.compose.ui.node.DrawModifierNode import androidx.compose.ui.unit.dp import chat.simplex.common.platform.onRightClick import chat.simplex.common.views.helpers.* -object NoIndication : Indication { - private object NoIndicationInstance : IndicationInstance { - override fun ContentDrawScope.drawIndication() { - drawContent() - } - } - @Composable - override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance { - return NoIndicationInstance +object NoIndication : IndicationNodeFactory { + // Should be as a class, not an object. Otherwise, crash + private class NoIndicationInstance : Modifier.Node(), DrawModifierNode { + override fun ContentDrawScope.draw() { drawContent() } } + override fun create(interactionSource: InteractionSource): DelegatableNode = NoIndicationInstance() + override fun hashCode(): Int = -1 + override fun equals(other: Any?) = other === this } @Composable @@ -32,16 +31,16 @@ actual fun ChatListNavLinkLayout( click: () -> Unit, dropdownMenuItems: (@Composable () -> Unit)?, showMenu: MutableState<Boolean>, - stopped: Boolean, - selectedChat: State<Boolean> + disabled: Boolean, + selectedChat: State<Boolean>, + nextChatSelected: State<Boolean>, ) { var modifier = Modifier.fillMaxWidth() - if (!stopped) modifier = modifier - .background(color = if (selectedChat.value) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.95f) else Color.Unspecified) + if (!disabled) modifier = modifier .combinedClickable(onClick = click, onLongClick = { showMenu.value = true }) .onRightClick { showMenu.value = true } CompositionLocalProvider( - LocalIndication provides if (selectedChat.value && !stopped) NoIndication else LocalIndication.current + LocalIndication provides if (selectedChat.value && !disabled) NoIndication else LocalIndication.current ) { Box(modifier) { Row( @@ -52,10 +51,17 @@ actual fun ChatListNavLinkLayout( ) { chatLinkPreview() } + if (selectedChat.value) { + Box(Modifier.matchParentSize().background(MaterialTheme.colors.onBackground.copy(0.05f))) + } if (dropdownMenuItems != null) { DefaultDropdownMenu(showMenu, dropdownMenuItems = dropdownMenuItems) } } } - Divider() + if (selectedChat.value || nextChatSelected.value) { + Divider() + } else { + SectionDivider() + } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt index 61e0e0d3ce..9fd65ec995 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt @@ -1,75 +1,162 @@ package chat.simplex.common.views.chatlist import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme +import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.call.CallMediaType +import chat.simplex.common.views.call.Call import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import kotlinx.coroutines.flow.MutableStateFlow + +@OptIn(ExperimentalLayoutApi::class) +@Composable +actual fun TagsRow(content: @Composable() (() -> Unit)) { + FlowRow(modifier = Modifier.padding(horizontal = 14.dp)) { content() } +} @Composable -actual fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>) { - val call = remember { chatModel.activeCall}.value - // if (call?.callState == CallState.Connected && !newChatSheetState.collectAsState().value.isVisible()) { - if (call != null && !newChatSheetState.collectAsState().value.isVisible()) { - val showMenu = remember { mutableStateOf(false) } - val media = call.peerMedia ?: call.localMedia - CompositionLocalProvider( - LocalIndication provides NoIndication - ) { - Box( - Modifier - .fillMaxSize(), - contentAlignment = Alignment.BottomEnd - ) { - Box( - Modifier - .padding(end = 71.dp, bottom = 92.dp) - .size(67.dp) - .combinedClickable(onClick = { - val chat = chatModel.getChat(call.contact.id) - if (chat != null) { - withApi { - openChat(chat.remoteHostId, chat.chatInfo, chatModel) - } - } - }, - onLongClick = { showMenu.value = true }) - .onRightClick { showMenu.value = true }, - contentAlignment = Alignment.Center - ) { - Box(Modifier.background(MaterialTheme.colors.background, CircleShape)) { - ProfileImageForActiveCall(size = 56.dp, image = call.contact.profile.image) - } - Box(Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp).align(Alignment.TopEnd)) { - if (media == CallMediaType.Video) { - Icon(painterResource(MR.images.ic_videocam_filled), stringResource(MR.strings.icon_descr_video_call), Modifier.size(18.dp), tint = Color.White) - } else { - Icon(painterResource(MR.images.ic_call_filled), stringResource(MR.strings.icon_descr_audio_call), Modifier.size(18.dp), tint = Color.White) - } - } - DefaultDropdownMenu(showMenu) { - ItemAction(stringResource(MR.strings.icon_descr_hang_up), painterResource(MR.images.ic_call_end_filled), color = MaterialTheme.colors.error, onClick = { - withBGApi { chatModel.callManager.endCall(call) } - showMenu.value = false - }) - } +actual fun ActiveCallInteractiveArea(call: Call) { + val showMenu = remember { mutableStateOf(false) } + val oneHandUI = remember { appPrefs.oneHandUI.state } + if (oneHandUI.value) { + ActiveCallInteractiveAreaOneHand(call, showMenu) + } else { + CompositionLocalProvider( + LocalIndication provides NoIndication + ) { + ActiveCallInteractiveAreaNonOneHand(call, showMenu) + } + } +} + +@Composable +private fun ActiveCallInteractiveAreaOneHand(call: Call, showMenu: MutableState<Boolean>) { + Box( + Modifier + .minimumInteractiveComponentSize() + .combinedClickable(onClick = { + val chat = chatModel.getChat(call.contact.id) + if (chat != null) { + withBGApi { + openChat(secondaryChatsCtx = null, chat.remoteHostId, chat.chatInfo) } } + }, + onLongClick = { showMenu.value = true }, + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = remember { ripple(bounded = false, radius = 24.dp) } + ) + .onRightClick { showMenu.value = true }, + contentAlignment = Alignment.Center + ) { + ProfileImage( + image = call.contact.profile.image, + size = 37.dp * fontSizeSqrtMultiplier, + color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f) + ) + Box( + Modifier.offset(x = 1.dp, y = (-1).dp).background(SimplexGreen, CircleShape).padding(3.dp) + .align(Alignment.TopEnd) + ) { + if (call.hasVideo) { + Icon( + painterResource(MR.images.ic_videocam_filled), + stringResource(MR.strings.icon_descr_video_call), + Modifier.size(12.dp), + tint = Color.White + ) + } else { + Icon( + painterResource(MR.images.ic_call_filled), + stringResource(MR.strings.icon_descr_audio_call), + Modifier.size(12.dp), + tint = Color.White + ) } } + DefaultDropdownMenu(showMenu) { + ItemAction( + stringResource(MR.strings.icon_descr_hang_up), + painterResource(MR.images.ic_call_end_filled), + color = MaterialTheme.colors.error, + onClick = { + withBGApi { chatModel.callManager.endCall(call) } + showMenu.value = false + }) + } + } +} + +@Composable +private fun ActiveCallInteractiveAreaNonOneHand(call: Call, showMenu: MutableState<Boolean>) { + Box( + Modifier + .fillMaxSize(), + contentAlignment = Alignment.BottomEnd + ) { + Box( + Modifier + .padding(end = 15.dp, bottom = 92.dp) + .size(67.dp) + .combinedClickable(onClick = { + val chat = chatModel.getChat(call.contact.id) + if (chat != null) { + withBGApi { + openChat(secondaryChatsCtx = null, chat.remoteHostId, chat.chatInfo) + } + } + }, + onLongClick = { showMenu.value = true }) + .onRightClick { showMenu.value = true }, + contentAlignment = Alignment.Center + ) { + Box(Modifier.background(MaterialTheme.colors.background, CircleShape)) { + ProfileImageForActiveCall(size = 56.dp, image = call.contact.profile.image) + } + Box( + Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp) + .align(Alignment.TopEnd) + ) { + if (call.hasVideo) { + Icon( + painterResource(MR.images.ic_videocam_filled), + stringResource(MR.strings.icon_descr_video_call), + Modifier.size(18.dp), + tint = Color.White + ) + } else { + Icon( + painterResource(MR.images.ic_call_filled), + stringResource(MR.strings.icon_descr_audio_call), + Modifier.size(18.dp), + tint = Color.White + ) + } + } + DefaultDropdownMenu(showMenu) { + ItemAction( + stringResource(MR.strings.icon_descr_hang_up), + painterResource(MR.images.ic_call_end_filled), + color = MaterialTheme.colors.error, + onClick = { + withBGApi { chatModel.callManager.endCall(call) } + showMenu.value = false + }) + } + } + } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/TagListView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/TagListView.desktop.kt new file mode 100644 index 0000000000..75a76014a9 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/TagListView.desktop.kt @@ -0,0 +1,72 @@ +package chat.simplex.common.views.chatlist + +import SectionItemView +import androidx.compose.foundation.layout.* +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.graphics.Color +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.common.ui.theme.* +import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex +import chat.simplex.common.views.chat.item.isHeartEmoji +import chat.simplex.common.views.chat.item.isShortEmoji +import chat.simplex.common.views.helpers.toDp +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource + +@Composable +actual fun ChatTagInput(name: MutableState<String>, showError: State<Boolean>, emoji: MutableState<String?>) { + SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + SingleEmojiInput(emoji) + TagListNameTextField(name, showError = showError) + } +} + +@Composable +private fun SingleEmojiInput( + emoji: MutableState<String?> +) { + val state = remember { mutableStateOf(TextFieldValue(emoji.value ?: "")) } + val colors = TextFieldDefaults.textFieldColors( + textColor = if (isHeartEmoji(emoji.value ?: "")) Color(0xffD63C31) else MaterialTheme.colors.onPrimary, + backgroundColor = Color.Unspecified, + focusedIndicatorColor = MaterialTheme.colors.secondary.copy(alpha = 0.6f), + unfocusedIndicatorColor = CurrentColors.value.colors.secondary.copy(alpha = 0.3f), + cursorColor = MaterialTheme.colors.secondary, + ) + TextField( + value = state.value, + onValueChange = { newValue -> + if (newValue.text == emoji.value) { + state.value = newValue + return@TextField + } + val newValueClamped = newValue.text.replace(emoji.value ?: "", "") + val isEmoji = isShortEmoji(newValueClamped) + emoji.value = if (isEmoji) newValueClamped else null + state.value = if (isEmoji) newValue else TextFieldValue() + }, + singleLine = true, + modifier = Modifier + .padding(4.dp) + .size(width = TextFieldDefaults.MinHeight.value.sp.toDp(), height = TextFieldDefaults.MinHeight), + textStyle = LocalTextStyle.current.copy(fontFamily = EmojiFont, textAlign = TextAlign.Center), + placeholder = { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Icon( + painter = painterResource(MR.images.ic_add_reaction), + contentDescription = null, + tint = MaterialTheme.colors.secondary + ) + } + }, + colors = colors, + ) +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt new file mode 100644 index 0000000000..3855835ab6 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt @@ -0,0 +1,107 @@ +package chat.simplex.common.views.chatlist + +import androidx.compose.animation.* +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +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.dp +import androidx.compose.ui.unit.sp +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.User +import chat.simplex.common.model.UserInfo +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import kotlinx.coroutines.flow.MutableStateFlow + +@Composable +actual fun UserPickerUsersSection( + users: List<UserInfo>, + iconColor: Color, + stopped: Boolean, + onUserClicked: (user: User) -> Unit, +) { + if (users.isNotEmpty()) { + val userRows = users.chunked(5) + val rowsToDisplay = if (userRows.size > 2) 2 else userRows.size + val horizontalPadding = DEFAULT_PADDING_HALF + 8.dp + + Column(Modifier + .padding(horizontal = horizontalPadding) + .height((55.dp + 16.sp.toDp()) * rowsToDisplay + (if (rowsToDisplay > 1) DEFAULT_PADDING else 0.dp)) + ) { + ColumnWithScrollBarNoAppBar( + verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING) + ) { + val spaceBetween = (((DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) - (horizontalPadding)) - (65.dp * 5)) / 5 + + userRows.forEach { row -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(spaceBetween), + ) { + row.forEach { u -> + Column(modifier = Modifier + .clickable ( + onClick = { onUserClicked(u.user) }, + enabled = !stopped + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val user = u.user + Box { + ProfileImage(size = 55.dp, image = user.profile.image, color = iconColor) + + if (u.unreadCount > 0 && !user.activeUser) { + unreadBadge(u.unreadCount, user.showNtfs, true) + } + } + + Text( + user.displayName, + fontSize = 12.sp, + fontWeight = if (user.activeUser) FontWeight.Bold else FontWeight.Normal, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.width(65.dp), + textAlign = TextAlign.Center + ) + } + } + } + } + } + } + } +} + +@Composable +actual fun PlatformUserPicker(modifier: Modifier, pickerState: MutableStateFlow<AnimatedViewState>, content: @Composable () -> Unit) { + AnimatedVisibility( + visible = pickerState.value.isVisible(), + enter = fadeIn(), + exit = fadeOut() + ) { + Box( + Modifier + .fillMaxSize() + .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { pickerState.value = AnimatedViewState.HIDING }), + contentAlignment = Alignment.TopStart + ) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + ColumnWithScrollBarNoAppBar(modifier.align(if (oneHandUI.value) Alignment.BottomCenter else Alignment.TopCenter)) { + content() + } + } + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.desktop.kt index af2b269b58..eb93e7c510 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.desktop.kt @@ -2,6 +2,7 @@ package chat.simplex.common.views.database import SectionItemView import SectionTextFooter +import TextIconSpaced import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable @@ -22,8 +23,9 @@ actual fun SavePassphraseSetting( useKeychain: Boolean, initialRandomDBPassphrase: Boolean, storedKey: Boolean, - progressIndicator: Boolean, minHeight: Dp, + enabled: Boolean, + smallPadding: Boolean, onCheckedChange: (Boolean) -> Unit, ) { SectionItemView(minHeight = minHeight) { @@ -33,7 +35,11 @@ actual fun SavePassphraseSetting( stringResource(MR.strings.save_passphrase_in_settings), tint = if (storedKey) WarningOrange else MaterialTheme.colors.secondary ) - Spacer(Modifier.padding(horizontal = 4.dp)) + if (smallPadding) { + Spacer(Modifier.padding(horizontal = 4.dp)) + } else { + TextIconSpaced(false) + } Text( stringResource(MR.strings.save_passphrase_in_settings), Modifier.padding(end = 24.dp), @@ -43,7 +49,7 @@ actual fun SavePassphraseSetting( DefaultSwitch( checked = useKeychain, onCheckedChange = onCheckedChange, - enabled = !initialRandomDBPassphrase && !progressIndicator + enabled = enabled ) } } @@ -55,13 +61,14 @@ actual fun DatabaseEncryptionFooter( chatDbEncrypted: Boolean?, storedKey: MutableState<Boolean>, initialRandomDBPassphrase: MutableState<Boolean>, + migration: Boolean, ) { if (chatDbEncrypted == false) { SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted)) } else if (useKeychain.value) { if (storedKey.value) { SectionTextFooter(generalGetString(MR.strings.settings_is_storing_in_clear_text)) - if (initialRandomDBPassphrase.value) { + if (initialRandomDBPassphrase.value && !migration) { SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase)) } else { SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase)) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseView.desktop.kt new file mode 100644 index 0000000000..9ed7170a31 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseView.desktop.kt @@ -0,0 +1,23 @@ +package chat.simplex.common.views.database + +import androidx.compose.runtime.mutableStateOf +import chat.simplex.common.platform.chatModel +import chat.simplex.common.views.helpers.withBGApi +import kotlinx.coroutines.delay +import kotlinx.datetime.Instant + +actual fun restartChatOrApp() { + if (chatModel.chatRunning.value == false) { + chatModel.chatDbChanged.value = true + startChat(chatModel, mutableStateOf(Instant.DISTANT_PAST), chatModel.chatDbChanged, mutableStateOf(false)) + } else { + authStopChat(chatModel) { + withBGApi { + // adding delay in order to prevent locked database by previous initialization + delay(1000) + chatModel.chatDbChanged.value = true + startChat(chatModel, mutableStateOf(Instant.DISTANT_PAST), chatModel.chatDbChanged, mutableStateOf(false)) + } + } + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt new file mode 100644 index 0000000000..974578882d --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt @@ -0,0 +1,466 @@ +package chat.simplex.common.views.helpers + +import SectionItemView +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import chat.simplex.common.BuildConfigCommon +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ChatController.getNetCfg +import chat.simplex.common.model.json +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.WarningOrange +import chat.simplex.common.views.onboarding.ReadMoreButton +import chat.simplex.res.MR +import kotlinx.coroutines.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.Closeable +import java.io.File +import java.net.InetSocketAddress +import java.net.Proxy +import kotlin.math.min + +data class SemVer( + val major: Int, + val minor: Int, + val patch: Int, + val preRelease: String? = null, + val buildNumber: Int? = null, +): Comparable<SemVer?> { + + val isNotStable: Boolean = preRelease != null + + override fun compareTo(other: SemVer?): Int { + if (other == null) return 1 + return when { + major != other.major -> major.compareTo(other.major) + minor != other.minor -> minor.compareTo(other.minor) + patch != other.patch -> patch.compareTo(other.patch) + preRelease != null && other.preRelease != null -> { + val pr = preRelease.compareTo(other.preRelease, ignoreCase = true) + when { + pr != 0 -> pr + buildNumber != null && other.buildNumber != null -> buildNumber.compareTo(other.buildNumber) + buildNumber != null -> -1 + other.buildNumber != null -> 1 + else -> 0 + } + } + preRelease != null -> -1 + other.preRelease != null -> 1 + else -> 0 + } + } + + companion object { + private val regex = Regex("^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([A-Za-z]+)\\.(\\d+))?\$") + fun from(tagName: String): SemVer? { + val trimmed = tagName.trimStart { it == 'v' } + val redacted = when { + trimmed.contains('-') && trimmed.substringBefore('-').count { it == '.' } == 1 -> "${trimmed.substringBefore('-')}.0-${trimmed.substringAfter('-')}" + trimmed.substringBefore('-').count { it == '.' } == 1 -> "${trimmed}.0" + else -> trimmed + } + val group = regex.matchEntire(redacted)?.groups + return if (group != null) { + SemVer( + major = group[1]?.value?.toIntOrNull() ?: return null, + minor = group[2]?.value?.toIntOrNull() ?: return null, + patch = group[3]?.value?.toIntOrNull() ?: return null, + preRelease = group[4]?.value, + buildNumber = group[5]?.value?.toIntOrNull(), + ) + } else { + null + } + } + + fun fromCurrentVersionName(): SemVer? { + val currentVersionName = if (appPlatform.isAndroid) BuildConfigCommon.ANDROID_VERSION_NAME else BuildConfigCommon.DESKTOP_VERSION_NAME + return from(currentVersionName) + } + } +} + +@Serializable +data class GitHubRelease( + @SerialName("tag_name") + val tagName: String, + @SerialName("html_url") + val htmlUrl: String, + val name: String, + val draft: Boolean, + @SerialName("prerelease") + private val preRelease: Boolean, + val body: String, + @SerialName("published_at") + val publishedAt: String, + val assets: List<GitHubAsset> +) { + @Transient + val semVer: SemVer? = SemVer.from(tagName) + + val isConsideredBeta: Boolean = preRelease || semVer == null || semVer.isNotStable +} + +@Serializable +data class GitHubAsset( + @SerialName("browser_download_url") + val browserDownloadUrl: String, + val name: String, + val size: Long, + + val isAppImage: Boolean = name.lowercase().contains(".appimage") +) + +fun showAppUpdateNotice() { + AlertManager.shared.showAlertDialogButtonsColumn( + generalGetString(MR.strings.app_check_for_updates_notice_title), + text = generalGetString(MR.strings.app_check_for_updates_notice_desc), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + appPrefs.appUpdateChannel.set(AppUpdatesChannel.STABLE) + setupUpdateChecker() + }) { + Text(generalGetString(MR.strings.app_check_for_updates_stable), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + + SectionItemView({ + AlertManager.shared.hideAlert() + appPrefs.appUpdateChannel.set(AppUpdatesChannel.BETA) + setupUpdateChecker() + }) { + Text(generalGetString(MR.strings.app_check_for_updates_beta), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + + SectionItemView({ + AlertManager.shared.hideAlert() + appPrefs.appUpdateChannel.set(AppUpdatesChannel.DISABLED) + }) { + Text(generalGetString(MR.strings.app_check_for_updates_notice_disable), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) +} + +private var updateCheckerJob: Job = Job() +fun setupUpdateChecker() = withLongRunningApi { + updateCheckerJob.cancel() + if (appPrefs.appUpdateChannel.get() == AppUpdatesChannel.DISABLED) { + return@withLongRunningApi + } + checkForUpdate() + createUpdateJob() +} + +private fun createUpdateJob() { + updateCheckerJob = withLongRunningApi { + delay(24 * 60 * 60 * 1000) + checkForUpdate() + createUpdateJob() + } +} + + +fun checkForUpdate() { + Log.d(TAG, "Checking for update") + val currentSemVer = SemVer.fromCurrentVersionName() + if (currentSemVer == null) { + Log.e(TAG, "Current SemVer cannot be parsed") + return + } + val client = setupHttpClient() + try { + val request = Request.Builder().url("https://api.github.com/repos/simplex-chat/simplex-chat/releases").addHeader("User-agent", "curl").build() + client.newCall(request).execute().use { response -> + response.body?.use { + val body = it.string() + val releases = json.decodeFromString<List<GitHubRelease>>(body) + val release = when (appPrefs.appUpdateChannel.get()) { + AppUpdatesChannel.STABLE -> releases.firstOrNull { r -> !r.draft && !r.isConsideredBeta && currentSemVer < r.semVer } + AppUpdatesChannel.BETA -> releases.firstOrNull { r -> !r.draft && currentSemVer < r.semVer } + AppUpdatesChannel.DISABLED -> return + } + + if (release == null || release.tagName == appPrefs.appSkippedUpdate.get()) { + Log.d(TAG, "Skipping update because of the same version or skipped version") + return + } + val assets = chooseGitHubReleaseAssets(release) + // No need to show an alert if no suitable packages were found. But for Flatpak users it's useful to see release notes anyway + if (assets.isEmpty() && !isRunningFromFlatpak()) { + Log.d(TAG, "No assets to download for current system") + return + } + val lines = ArrayList<String>() + for (line in release.body.lines()) { + if (line == "Commits:") break + lines.add(line) + } + val text = lines.joinToString("\n") + AlertManager.shared.showAlertDialogButtonsColumn( + generalGetString(MR.strings.app_check_for_updates_update_available).format(release.name), + text = text, + textAlign = TextAlign.Start, + dismissible = false, + belowTextContent = { + ReadMoreButton(release.htmlUrl) + }, + buttons = { + Column { + for (asset in assets) { + SectionItemView({ + AlertManager.shared.hideAlert() + chatModel.updatingProgress.value = 0f + withLongRunningApi { + try { + downloadAsset(asset) + } finally { + chatModel.updatingProgress.value = null + } + } + }) { + Text( + generalGetString(MR.strings.app_check_for_updates_button_download).format( + if (asset.name.length > 34) "…" + asset.name.substringAfter("simplex-desktop-") else asset.name, + formatBytes(asset.size)), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary + ) + } + } + + SectionItemView({ + AlertManager.shared.hideAlert() + skipRelease(release) + }) { + Text(generalGetString(MR.strings.app_check_for_updates_button_skip), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = WarningOrange) + } + + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(generalGetString(MR.strings.app_check_for_updates_button_remind_later), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to get the latest release: ${e.stackTraceToString()}") + } +} + +private fun setupHttpClient(): OkHttpClient { + val netCfg = getNetCfg() + var proxy: Proxy? = null + if (netCfg.useSocksProxy && netCfg.socksProxy != null) { + val hostname = netCfg.socksProxy.substringBefore(":").ifEmpty { "localhost" } + val port = netCfg.socksProxy.substringAfter(":").toIntOrNull() + if (port != null) { + proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(hostname, port)) + } + } + return OkHttpClient.Builder().proxy(proxy).followRedirects(true).build() +} + +private fun skipRelease(release: GitHubRelease) { + appPrefs.appSkippedUpdate.set(release.tagName) +} + +private suspend fun downloadAsset(asset: GitHubAsset) { + withContext(Dispatchers.Main) { + showToast(generalGetString(MR.strings.app_check_for_updates_download_started)) + } + val progressListener = object: ProgressListener { + override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { + if (contentLength != -1L) { + chatModel.updatingProgress.value = if (done) 1f else bytesRead / contentLength.toFloat() + } + } + } + val client = setupHttpClient().newBuilder() + .addNetworkInterceptor { chain -> + val originalResponse = chain.proceed(chain.request()) + val body = originalResponse.body + if (body != null) { + originalResponse.newBuilder().body(ProgressResponseBody(body, progressListener)).build() + } else { + originalResponse + } + } + .build() + + try { + val request = Request.Builder().url(asset.browserDownloadUrl).addHeader("User-agent", "curl").build() + val call = client.newCall(request) + chatModel.updatingRequest = Closeable { + call.cancel() + withApi { + showToast(generalGetString(MR.strings.app_check_for_updates_canceled)) + } + } + call.execute().use { response -> + response.body?.use { body -> + body.byteStream().use { stream -> + createTmpFileAndDelete { file -> + // It's important to close output stream (with use{}), otherwise, Windows cannot rename the file + file.outputStream().use { output -> + stream.copyTo(output) + } + val newFile = File(file.parentFile, asset.name) + file.renameTo(newFile) + + AlertManager.shared.showAlertDialogButtonsColumn( + generalGetString(MR.strings.app_check_for_updates_download_completed_title), + dismissible = false, + buttons = { + Column { + // It's problematic to install .deb package because it requires either root or GUI package installer which is not available on + // Debian by default. Let the user install it manually only + if (!asset.name.lowercase().endsWith(".deb")) { + SectionItemView({ + AlertManager.shared.hideAlert() + chatModel.updatingProgress.value = -1f + withLongRunningApi { + try { + installAppUpdate(newFile) + } finally { + chatModel.updatingProgress.value = null + } + } + }) { + Text(generalGetString(MR.strings.app_check_for_updates_button_install), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + + SectionItemView({ + desktopOpenDir(newFile.parentFile) + }) { + Text(generalGetString(MR.strings.app_check_for_updates_button_open), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + + SectionItemView({ + AlertManager.shared.hideAlert() + newFile.delete() + }) { + Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + } + } + ) + } + } + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to download the asset from release: ${e.stackTraceToString()}") + } +} + +private fun isRunningFromAppImage(): Boolean = System.getenv("APPIMAGE") != null + +private fun isRunningFromFlatpak(): Boolean = System.getenv("container") == "flatpak" + +private fun chooseGitHubReleaseAssets(release: GitHubRelease): List<GitHubAsset> { + val res = if (isRunningFromFlatpak()) { + // No need to show download options for Flatpak users + emptyList() + } else if (!isRunningFromAppImage() && Runtime.getRuntime().exec("which dpkg").onExit().join().exitValue() == 0) { + // Show all available .deb packages and user will choose the one that works on his system (for Debian derivatives) + release.assets.filter { it.name.lowercase().endsWith(".deb") } + } else { + release.assets.filter { it.name == desktopPlatform.githubAssetName } + } + return res +} + +private suspend fun installAppUpdate(file: File) = withContext(Dispatchers.IO) { + when { + desktopPlatform.isLinux() -> { + val process = Runtime.getRuntime().exec("xdg-open ${file.absolutePath}").onExit().join() + val startedInstallation = process.exitValue() == 0 && process.children().count() > 0 + if (!startedInstallation) { + Log.e(TAG, "Error starting installation: ${process.inputReader().use { it.readLines().joinToString("\n") }}${process.errorStream.use { String(it.readAllBytes()) }}") + // Failed to start installation. show directory with the file for manual installation + desktopOpenDir(file.parentFile) + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.app_check_for_updates_installed_successfully_title), + text = generalGetString(MR.strings.app_check_for_updates_installed_successfully_desc) + ) + file.delete() + } + } + desktopPlatform.isWindows() -> { + val process = Runtime.getRuntime().exec("msiexec /i ${file.absolutePath}"/* /qb */).onExit().join() + val startedInstallation = process.exitValue() == 0 + if (!startedInstallation) { + Log.e(TAG, "Error starting installation: ${process.inputReader().use { it.readLines().joinToString("\n") }}${process.errorStream.use { String(it.readAllBytes()) }}") + // Failed to start installation. show directory with the file for manual installation + desktopOpenDir(file.parentFile) + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.app_check_for_updates_installed_successfully_title), + text = generalGetString(MR.strings.app_check_for_updates_installed_successfully_desc) + ) + file.delete() + } + } + desktopPlatform.isMac() -> { + // Default mount point if no other DMGs were mounted before + var volume = "/Volumes/SimpleX" + try { + val process = Runtime.getRuntime().exec("hdiutil mount ${file.absolutePath}").onExit().join() + val startedInstallation = process.exitValue() == 0 + val lines = process.inputReader().use { it.readLines() } + // This is needed for situations when mount point has non-default path. + // For example, when a user already had mounted SimpleX.dmg before and default mount point is not available. + // Mac will make volume like /Volumes/SimpleX 1 + val lastLine = lines.lastOrNull()?.substringAfterLast('\t') + if (!startedInstallation || lastLine == null || !lastLine.lowercase().contains("/volumes/")) { + Log.e(TAG, "Error starting installation: ${process.inputReader().use { it.readLines().joinToString("\n") }}${process.errorStream.use { String(it.readAllBytes()) }}") + // Failed to start installation. show directory with the file for manual installation + desktopOpenDir(file.parentFile) + return@withContext + } + volume = lastLine + File("/Applications/SimpleX.app").renameTo(File("/Applications/SimpleX-old.app")) + val process2 = Runtime.getRuntime().exec(arrayOf("cp", "-R", "${volume}/SimpleX.app", "/Applications")).onExit().join() + val copiedSuccessfully = process2.exitValue() == 0 + if (!copiedSuccessfully) { + Log.e(TAG, "Error copying the app: ${process2.inputReader().use { it.readLines().joinToString("\n") }}${process2.errorStream.use { String(it.readAllBytes()) }}") + // Failed to start installation. show directory with the file for manual installation + desktopOpenDir(file.parentFile) + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.app_check_for_updates_installed_successfully_title), + text = generalGetString(MR.strings.app_check_for_updates_installed_successfully_desc) + ) + file.delete() + } + } finally { + try { + Runtime.getRuntime().exec(arrayOf("hdiutil", "unmount", volume)).onExit().join() + } finally { + if (!File("/Applications/SimpleX.app").exists()) { + File("/Applications/SimpleX-old.app").renameTo(File("/Applications/SimpleX.app")) + } else { + Runtime.getRuntime().exec("rm -rf /Applications/SimpleX-old.app").onExit().join() + } + } + } + } + } + Unit +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.desktop.kt new file mode 100644 index 0000000000..03c8e51c55 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.desktop.kt @@ -0,0 +1,80 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import chat.simplex.common.model.CustomTimeUnit +import chat.simplex.common.ui.theme.DEFAULT_PADDING + +@Composable +actual fun CustomTimePicker( + selection: MutableState<Int>, + timeUnitsLimits: List<TimeUnitLimits> +) { + val unit = remember { + var res: CustomTimeUnit = CustomTimeUnit.Second + val found = timeUnitsLimits.asReversed().any { + if (selection.value >= it.minValue * it.timeUnit.toSeconds && selection.value <= it.maxValue * it.timeUnit.toSeconds) { + res = it.timeUnit + selection.value = (selection.value / it.timeUnit.toSeconds).coerceIn(it.minValue, it.maxValue) * it.timeUnit.toSeconds + true + } else { + false + } + } + if (!found) { + // If custom interval doesn't fit in any category, set it to 1 second interval + selection.value = 1 + } + mutableStateOf(res) + } + val values = remember(unit.value) { + val limit = timeUnitsLimits.first { it.timeUnit == unit.value } + val res = ArrayList<Pair<Int, String>>() + for (i in limit.minValue..limit.maxValue) { + val seconds = i * limit.timeUnit.toSeconds + val desc = i.toString() + res.add(seconds to desc) + } + if (res.none { it.first == selection.value }) { + // Doesn't fit into min..max, put it equal to the closest value + selection.value = selection.value.coerceIn(res.first().first, res.last().first) + //selection.value = res.last { it.first <= selection.value }.first + } + res + } + val units = remember { + val res = ArrayList<Pair<CustomTimeUnit, String>>() + for (unit in timeUnitsLimits) { + res.add(unit.timeUnit to unit.timeUnit.text) + } + res + } + + Row( + Modifier.padding(bottom = DEFAULT_PADDING), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly + ) { + ExposedDropDownSetting( + values, + selection, + textColor = MaterialTheme.colors.onBackground, + enabled = remember { mutableStateOf(true) }, + onSelected = { selection.value = it } + ) + Spacer(Modifier.width(DEFAULT_PADDING)) + ExposedDropDownSetting( + units, + unit, + textColor = MaterialTheme.colors.onBackground, + enabled = remember { mutableStateOf(true) }, + onSelected = { + selection.value = selection.value / unit.value.toSeconds * it.toSeconds + unit.value = it + } + ) + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt index 79fcda7a50..7341c6af23 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt @@ -2,8 +2,7 @@ package chat.simplex.common.views.helpers import androidx.compose.foundation.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface +import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.* @@ -39,7 +38,8 @@ actual fun DefaultDialog( ) { Surface( Modifier - .border(border = BorderStroke(1.dp, MaterialTheme.colors.secondary.copy(alpha = 0.3F)), shape = RoundedCornerShape(8)) + .border(border = BorderStroke(1.dp, MaterialTheme.colors.secondary.copy(alpha = 0.3F)), shape = RoundedCornerShape(8)), + contentColor = LocalContentColor.current ) { content() } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/GetImageView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/GetImageView.desktop.kt index 979857f044..59865dd66f 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/GetImageView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/GetImageView.desktop.kt @@ -45,7 +45,7 @@ actual fun GetImageBottomSheet( } val pickImageLauncher = rememberFileChooserLauncher(true, null, processPickedImage) ActionButton(null, stringResource(MR.strings.from_gallery_button), icon = painterResource(MR.images.ic_image)) { - withApi { pickImageLauncher.launch("image/*") } + withLongRunningApi { pickImageLauncher.launch("image/*") } } } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.desktop.kt index a251b7dc20..e245efae03 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.desktop.kt @@ -7,10 +7,11 @@ actual fun authenticate( promptSubtitle: String, selfDestruct: Boolean, usingLAMode: LAMode, + oneTime: Boolean, completed: (LAResult) -> Unit ) { when (usingLAMode) { - LAMode.PASSCODE -> authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, completed) + LAMode.PASSCODE -> authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, oneTime, completed) else -> {} } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/OkHttpProgressListener.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/OkHttpProgressListener.kt new file mode 100644 index 0000000000..24fa0b8ef1 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/OkHttpProgressListener.kt @@ -0,0 +1,46 @@ +package chat.simplex.common.views.helpers + +import okhttp3.MediaType +import okhttp3.ResponseBody +import okio.* + +// https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java +class ProgressResponseBody( + val responseBody: ResponseBody, + val progressListener: ProgressListener +): ResponseBody() { + private var bufferedSource: BufferedSource? = null + + override fun contentType(): MediaType? { + return responseBody.contentType() + } + + override fun contentLength(): Long { + return responseBody.contentLength() + } + + override fun source(): BufferedSource { + if (bufferedSource == null) { + bufferedSource = source(responseBody.source()).buffer() + } + return bufferedSource!! + } + + private fun source(source: Source): Source { + return object: ForwardingSource(source) { + var totalBytesRead = 0L + + override fun read(sink: Buffer, byteCount: Long): Long { + val bytesRead = super.read(sink, byteCount) + // read() returns the number of bytes read, or -1 if this source is exhausted. + totalBytesRead += if (bytesRead != -1L) bytesRead else 0L + progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L) + return bytesRead; + } + } + } +} + +interface ProgressListener { + fun update(bytesRead: Long, contentLength: Long, done: Boolean); +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt index 19c9fc0fd7..d541a5780e 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt @@ -1,15 +1,20 @@ package chat.simplex.common.views.helpers +import androidx.compose.runtime.* import androidx.compose.ui.graphics.* +import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.Density import chat.simplex.common.model.CIFile import chat.simplex.common.model.readCryptoFile import chat.simplex.common.platform.* import chat.simplex.common.simplexWindowState -import java.io.* +import kotlinx.coroutines.delay +import java.io.ByteArrayInputStream +import java.io.File import java.net.URI import javax.imageio.ImageIO import kotlin.io.encoding.Base64 @@ -17,6 +22,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi private val bStyle = SpanStyle(fontWeight = FontWeight.Bold) private val iStyle = SpanStyle(fontStyle = FontStyle.Italic) +private val uStyle = SpanStyle(textDecoration = TextDecoration.Underline) private fun fontStyle(color: String) = SpanStyle(color = Color(color.replace("#", "ff").toLongOrNull(16) ?: Color.White.toArgb().toLong())) @@ -54,6 +60,22 @@ actual fun escapedHtmlToAnnotatedString(text: String, density: Density): Annotat } break } + text.substringSafe(innerI, 2) == "u>" -> { + val textStart = innerI + 2 + for (insideTagI in textStart until text.length) { + if (text[insideTagI] == '<') { + withStyle(uStyle) { append(text.substring(textStart, insideTagI)) } + skipTil = insideTagI + 4 + break + } + } + break + } + text.substringSafe(innerI, 3) == "br>" -> { + val textStart = innerI + 3 + append("\n") + skipTil = textStart + } text.substringSafe(innerI, 4) == "font" -> { var textStart = innerI + 5 var color = "#000000" @@ -85,6 +107,18 @@ actual fun escapedHtmlToAnnotatedString(text: String, density: Density): Annotat AnnotatedString(text) } +@Composable +actual fun SetupClipboardListener() { + val clipboard = LocalClipboardManager.current + chatModel.clipboardHasText.value = clipboard.hasText() + LaunchedEffect(Unit) { + while (true) { + delay(1000) + chatModel.clipboardHasText.value = clipboard.hasText() + } + } +} + actual fun getAppFileUri(fileName: String): URI { val rh = chatModel.currentRemoteHost.value return if (rh == null) { @@ -122,7 +156,9 @@ actual fun getFileSize(uri: URI): Long? = uri.toFile().length() actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitmap? = try { - ImageIO.read(uri.inputStream()).toComposeImageBitmap() + uri.inputStream().use { + ImageIO.read(it).toComposeImageBitmap() + } } catch (e: Exception) { Log.e(TAG, "Error while decoding drawable: ${e.stackTraceToString()}") if (withAlertOnException) showImageDecodingException() diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/ConnectViaLinkView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/ConnectViaLinkView.desktop.kt deleted file mode 100644 index 72d9678154..0000000000 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/ConnectViaLinkView.desktop.kt +++ /dev/null @@ -1,11 +0,0 @@ -package chat.simplex.common.views.newchat - -import androidx.compose.runtime.* -import chat.simplex.common.model.ChatModel -import chat.simplex.common.model.RemoteHostInfo - -@Composable -actual fun ConnectViaLinkView(m: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) { - // TODO this should close if remote host changes in model - PasteToConnectView(m, rh, close) -} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.desktop.kt index 16d35b5b8d..6ae35e1a41 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.desktop.kt @@ -1,8 +1,13 @@ package chat.simplex.common.views.newchat +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.* @Composable -actual fun QRCodeScanner(onBarcode: (String) -> Unit) { +actual fun QRCodeScanner( + showQRCodeScanner: MutableState<Boolean>, + padding: PaddingValues, + onBarcode: suspend (String) -> Boolean +) { //LALAL } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.desktop.kt deleted file mode 100644 index 7579f09fa5..0000000000 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.desktop.kt +++ /dev/null @@ -1,15 +0,0 @@ -package chat.simplex.common.views.newchat - -import androidx.compose.runtime.Composable -import chat.simplex.common.model.ChatModel -import chat.simplex.common.model.RemoteHostInfo - -@Composable -actual fun ScanToConnectView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) { - ConnectContactLayout( - chatModel = chatModel, - rh = rh, - incognitoPref = chatModel.controller.appPrefs.incognito, - close = close - ) -} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt index 74c490445e..1b9582b7d2 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt @@ -1,8 +1,9 @@ package chat.simplex.common.views.onboarding -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.SharedPreference import chat.simplex.common.model.User @@ -15,10 +16,10 @@ import dev.icerock.moko.resources.compose.painterResource actual fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference<OnboardingStage>, onclick: (() -> Unit)?) { if (user == null) { Row(horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING * 2.5f)) { - OnboardingActionButton(MR.strings.link_a_mobile, onboarding = if (controller.appPrefs.initialRandomDBPassphrase.get() && !chatModel.desktopOnboardingRandomPassword.value) OnboardingStage.Step2_5_SetupDatabasePassphrase else OnboardingStage.LinkAMobile, true, icon = painterResource(MR.images.ic_smartphone_300), onclick = onclick) - OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, icon = painterResource(MR.images.ic_desktop), onclick = onclick) + OnboardingActionButton(labelId = MR.strings.link_a_mobile, onboarding = if (controller.appPrefs.initialRandomDBPassphrase.get() && !chatModel.desktopOnboardingRandomPassword.value) OnboardingStage.Step2_5_SetupDatabasePassphrase else OnboardingStage.LinkAMobile, icon = painterResource(MR.images.ic_smartphone_300), onclick = onclick) + OnboardingActionButton(labelId = MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, icon = painterResource(MR.images.ic_desktop), onclick = onclick) } } else { - OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, true, onclick = onclick) + OnboardingActionButton(Modifier.widthIn(min = 300.dp), labelId = MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onclick = onclick) } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt index 1a8ad010e3..c270bddb73 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt @@ -2,37 +2,36 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced +import SectionSpacer import SectionView +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf +import androidx.compose.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.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel import chat.simplex.common.model.SharedPreference -import chat.simplex.common.platform.defaultLocale -import chat.simplex.common.ui.theme.ThemeColor +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.usersettings.AppearanceScope.ColorEditor import chat.simplex.res.MR import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.delay import java.util.Locale @Composable -actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) { +actual fun AppearanceView(m: ChatModel) { AppearanceScope.AppearanceLayout( m.controller.appPrefs.appLanguage, m.controller.appPrefs.systemDarkTheme, - showSettingsModal = showSettingsModal, - editColor = { name, initialColor -> - ModalManager.start.showModalCloseable { close -> - ColorEditor(name, initialColor, close) - } - }, ) } @@ -40,14 +39,10 @@ actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatMod fun AppearanceScope.AppearanceLayout( languagePref: SharedPreference<String?>, systemDarkTheme: SharedPreference<String?>, - showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - editColor: (ThemeColor, Color) -> Unit, ) { - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.appearance_settings)) - SectionView(stringResource(MR.strings.settings_section_title_language), padding = PaddingValues()) { + SectionView(stringResource(MR.strings.settings_section_title_language), contentPadding = PaddingValues()) { val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") } LangSelector(state) { state.value = it @@ -62,9 +57,73 @@ fun AppearanceScope.AppearanceLayout( } } } + SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) + if (remember { appPrefs.oneHandUI.state }.value && !remember { appPrefs.chatBottomBar.state }.value) { + SettingsPreferenceItem(icon = null, stringResource(MR.strings.chat_bottom_bar), ChatModel.controller.appPrefs.chatBottomBar) + } } + SectionDividerSpaced() + ThemesSection(systemDarkTheme) + + SectionDividerSpaced() + AppToolbarsSection() + + SectionDividerSpaced() + MessageShapeSection() + + SectionDividerSpaced() + ProfileImageSection() + SectionDividerSpaced(maxTopPadding = true) - ThemesSection(systemDarkTheme, showSettingsModal, editColor) + FontScaleSection() + + SectionDividerSpaced(maxTopPadding = true) + DensityScaleSection() + SectionBottomSpacer() } } + +@Composable +fun DensityScaleSection() { + val localDensityScale = remember { mutableStateOf(appPrefs.densityScale.get()) } + SectionView(stringResource(MR.strings.appearance_zoom).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) { + Box(Modifier.size(50.dp) + .background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22)) + .clip(RoundedCornerShape(percent = 22)) + .clickable { + localDensityScale.value = 1f + appPrefs.densityScale.set(localDensityScale.value) + }, + contentAlignment = Alignment.Center) { + CompositionLocalProvider( + LocalDensity provides Density(LocalDensity.current.density * localDensityScale.value, LocalDensity.current.fontScale) + ) { + Text("${localDensityScale.value}", + color = if (localDensityScale.value == 1f) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground, + fontSize = 12.sp, + maxLines = 1 + ) + } + } + Spacer(Modifier.width(15.dp)) + Slider( + localDensityScale.value, + valueRange = 1f..2f, + steps = 11, + onValueChange = { + val diff = it % 0.1f + localDensityScale.value = String.format(Locale.US, "%.1f", it + (if (diff >= 0.05f) -diff + 0.1f else -diff)).toFloatOrNull() ?: 1f + }, + onValueChangeFinished = { + appPrefs.densityScale.set(localDensityScale.value) + }, + colors = SliderDefaults.colors( + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, + ) + ) + } + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.desktop.kt deleted file mode 100644 index 2d436dbbf0..0000000000 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.desktop.kt +++ /dev/null @@ -1,9 +0,0 @@ -package chat.simplex.common.views.usersettings - -import androidx.compose.runtime.Composable -import chat.simplex.common.model.ServerCfg - -@Composable -actual fun ScanProtocolServer(rhId: Long?, onNext: (ServerCfg) -> Unit) { - ScanProtocolServerLayout(rhId, onNext) -} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt index 95a079fe1b..5b4a044df3 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt @@ -1,8 +1,16 @@ package chat.simplex.common.views.usersettings import SectionView +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.AppUpdatesChannel +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -10,12 +18,17 @@ import dev.icerock.moko.resources.compose.stringResource @Composable actual fun SettingsSectionApp( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showVersion: () -> Unit, withAuth: (title: String, desc: String, block: () -> Unit) -> Unit ) { SectionView(stringResource(MR.strings.settings_section_title_app)) { - SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(withAuth) }) + val selectedChannel = remember { appPrefs.appUpdateChannel.state } + val values = AppUpdatesChannel.entries.map { it to it.text } + ExposedDropDownSettingRow(stringResource(MR.strings.app_check_for_updates), values, selectedChannel) { + appPrefs.appUpdateChannel.set(it) + setupUpdateChecker() + } AppVersionItem(showVersion) } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.desktop.kt new file mode 100644 index 0000000000..c9f62a5b79 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.desktop.kt @@ -0,0 +1,51 @@ +package chat.simplex.common.views.usersettings.networkAndServers + +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import chat.simplex.common.platform.DesktopScrollBar +import chat.simplex.common.views.helpers.detectCursorMove +import kotlinx.coroutines.* + +@Composable +actual fun ConditionsBox(modifier: Modifier, scrollState: ScrollState, content: @Composable() (BoxScope.() -> Unit)) { + val scope = rememberCoroutineScope() + val scrollBarAlpha = remember { Animatable(0f) } + val scrollJob: MutableState<Job> = remember { mutableStateOf(Job()) } + val scrollBarDraggingState = remember { mutableStateOf(false) } + val scrollModifier = remember { + Modifier + .pointerInput(Unit) { + detectCursorMove { + scope.launch { + scrollBarAlpha.animateTo(1f) + } + scrollJob.value.cancel() + scrollJob.value = scope.launch { + delay(1000L) + scrollBarAlpha.animateTo(0f) + } + } + } + } + + Box(modifier = modifier) { + Box( + Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .padding(8.dp) + .then(scrollModifier) + ) { + content() + } + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { + DesktopScrollBar(rememberScrollbarAdapter(scrollState), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, false, scrollBarDraggingState) + } + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.desktop.kt new file mode 100644 index 0000000000..7d6f305a83 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.desktop.kt @@ -0,0 +1,9 @@ +package chat.simplex.common.views.usersettings.networkAndServers + +import androidx.compose.runtime.Composable +import chat.simplex.common.model.UserServer + +@Composable +actual fun ScanProtocolServer(rhId: Long?, onNext: (UserServer) -> Unit) { + ScanProtocolServerLayout(rhId, onNext) +} diff --git a/apps/multiplatform/common/src/desktopMain/resources/media/connecting_call.mp3 b/apps/multiplatform/common/src/desktopMain/resources/media/connecting_call.mp3 new file mode 100644 index 0000000000..fc425bab97 Binary files /dev/null and b/apps/multiplatform/common/src/desktopMain/resources/media/connecting_call.mp3 differ diff --git a/apps/multiplatform/common/src/desktopMain/resources/media/in_call.mp3 b/apps/multiplatform/common/src/desktopMain/resources/media/in_call.mp3 new file mode 100644 index 0000000000..1049be4462 Binary files /dev/null and b/apps/multiplatform/common/src/desktopMain/resources/media/in_call.mp3 differ diff --git a/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/SemVerTest.kt b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/SemVerTest.kt new file mode 100644 index 0000000000..561911773f --- /dev/null +++ b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/SemVerTest.kt @@ -0,0 +1,63 @@ +package chat.simplex.app + +import chat.simplex.common.views.helpers.SemVer +import kotlin.test.Test +import kotlin.test.assertEquals + +// use this command for testing: +// ./gradlew desktopTest +class SemVerTest { + @Test + fun testValidSemVer() { + assertEquals(SemVer.from("1.0.0"), SemVer(1, 0, 0)) + assertEquals(SemVer.from("1.0"), SemVer(1, 0, 0)) + assertEquals(SemVer.from("v1.0"), SemVer(1, 0, 0)) + assertEquals(SemVer.from("v1.0-beta.1"), SemVer(1, 0, 0, "beta", 1)) + val r = listOf<Pair<String, SemVer>>( + "0.0.4" to SemVer(0, 0, 4), + "1.2.3" to SemVer(1, 2, 3), + "10.20.30" to SemVer(10, 20, 30), + "1.0.0-alpha.1" to SemVer(1, 0, 0, "alpha", buildNumber = 1), + "1.0.0" to SemVer(1, 0, 0), + "2.0.0" to SemVer(2, 0, 0), + "1.1.7" to SemVer(1, 1, 7), + "2.0.1-alpha.1227" to SemVer(2, 0, 1, "alpha", 1227), + ) + r.forEach { (value, correct) -> + assertEquals(SemVer.from(value), correct) + } + } + + @Test + fun testComparisonSemVer() { + assert(SemVer(0, 1, 0) == SemVer.from("0.1.0")) + assert(SemVer(1, 1, 0) == SemVer.from("v1.1.0")) + assert(SemVer(0, 1, 0) > SemVer(0, 0, 1)) + assert(SemVer(1, 0, 0) > SemVer(0, 100, 100)) + assert(SemVer(0, 200, 0) > SemVer(0, 100, 100)) + assert(SemVer(0, 1, 0, "beta") > SemVer(0, 1, 0, "alpha")) + assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "alpha")) + assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "beta")) + assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "beta.0")) + assert(SemVer(0, 1, 0, "beta", 1) > SemVer(0, 1, 0, "beta", 0)) + assert(SemVer(0, 1, 0, "beta", 11) > SemVer(0, 1, 0, "beta", 10)) + assert(SemVer(0, 1, 0, "beta", 11) > SemVer(0, 1, 0, "beta", 9)) + assert(SemVer(0, 1, 0, "beta.1") > SemVer(0, 1, 0, "alpha.2")) + assert(SemVer(1, 1, 0, "beta.1") > SemVer(0, 1, 0, "beta.1")) + assert(SemVer(1, 0, 0) > SemVer(1, 0, 0, "beta.1")) + assert(SemVer(1, 0, 0) > null) + assert(SemVer.from("v6.0.0")!! > SemVer.from("v6.0.0-beta.3")) + assert(SemVer.from("v6.0.0-beta.3")!! > SemVer.from("v6.0.0-beta.2")) + assert(SemVer.from("0.1.0") == SemVer.from("0.1.0")) + assert(SemVer.from("0.1.1")!! > SemVer.from("0.1.0")) + assert(SemVer.from("0.2.1")!! > SemVer.from("0.1.1")) + assert(SemVer.from("2.0.1")!! > SemVer.from("0.1.1")) + assert(SemVer.from("0.1.1-beta.0")!! > SemVer.from("0.1.0-beta.0")) + assert(SemVer.from("0.1.1-beta.0")!! == SemVer.from("0.1.1-beta.0")) + assert(SemVer.from("0.1.1-beta.1")!! > SemVer.from("0.1.1-beta.0")) + assert(SemVer.from("10.0.0-beta.12")!! > SemVer.from("1.1.1")) + assert(SemVer.from("1.1.1-beta.120")!! > SemVer.from("1.1.1-alpha.9")) + assert(SemVer.from("1.1.1-beta.120")!! > SemVer.from("1.1.1-alpha.120")) + assert(SemVer.from("2.0.1")!! > SemVer.from("0.1.1")) + } +} diff --git a/apps/multiplatform/desktop/build.gradle.kts b/apps/multiplatform/desktop/build.gradle.kts index a784d5c5fe..e39ba48a0b 100644 --- a/apps/multiplatform/desktop/build.gradle.kts +++ b/apps/multiplatform/desktop/build.gradle.kts @@ -12,16 +12,12 @@ version = extra["desktop.version_name"] as String kotlin { - jvm { - jvmToolchain(11) - withJava() - } + jvm() sourceSets { val jvmMain by getting { dependencies { implementation(project(":common")) implementation(compose.desktop.currentOs) - implementation("net.java.dev.jna:jna:5.13.0") } } val jvmTest by getting @@ -102,9 +98,8 @@ compose { // Packaging requires to have version like MAJOR.MINOR.PATCH var adjustedVersion = rootProject.extra["desktop.version_name"] as String adjustedVersion = adjustedVersion.replace(Regex("[^0-9.]"), "") - if (adjustedVersion.split(".").size != 3) { - adjustedVersion += ".0" - } + val split = adjustedVersion.split(".") + adjustedVersion = split[0] + "." + (split.getOrNull(1) ?: "0") + "." + (split.getOrNull(2) ?: "0") version = adjustedVersion } } @@ -152,7 +147,7 @@ cmake { tasks.named("clean") { dependsOn("cmakeClean") } -tasks.named("compileJava") { +tasks.named("compileKotlinJvm") { dependsOn("cmakeBuildAndCopy") } afterEvaluate { diff --git a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt index e32b0ae79a..0e8a452e08 100644 --- a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt +++ b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt @@ -1,17 +1,36 @@ package chat.simplex.desktop +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.foundation.* +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.size import chat.simplex.common.platform.* +import chat.simplex.common.platform.DesktopPlatform import chat.simplex.common.showApp +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.OnboardingStage +import kotlinx.coroutines.* import java.io.File fun main() { + // Disable hardware acceleration + //System.setProperty("skiko.renderApi", "SOFTWARE") initHaskell() + runMigrations() + setupUpdateChecker() initApp() tmpDir.deleteRecursively() tmpDir.mkdir() return showApp() } +@OptIn(ExperimentalComposeUiApi::class) @Suppress("UnsafeDynamicallyLoadedCode") private fun initHaskell() { val resourcesDir = File(System.getProperty("compose.application.resources.dir")) @@ -27,11 +46,38 @@ private fun initHaskell() { System.setProperty("jna.library.path", vlcDir.absolutePath) //discoverVlcLibs(File(File(vlcDir, "vlc"), "plugins").absolutePath) initHS() + + platform = object: PlatformInterface { + @Composable + override fun desktopShowAppUpdateNotice() { + fun showNoticeIfNeeded() { + if ( + !chatModel.controller.appPrefs.appUpdateNoticeShown.get() + && chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete + && chatModel.chats.size > 3 + && chatModel.activeCallInvitation.value == null + ) { + appPrefs.appUpdateNoticeShown.set(true) + showAppUpdateNotice() + } + } + // Will show notice if chats were loaded before that moment and number of chats > 3 + LaunchedEffect(Unit) { + showNoticeIfNeeded() + } + // Will show notice if chats were loaded later (a lot of chats/slow query) and number of chats > 3 + KeyChangeEffect(chatModel.chats.size) { oldSize -> + if (oldSize == 0) { + showNoticeIfNeeded() + } + } + } + } } private fun windowsLoadRequiredLibs(libsTmpDir: File, vlcDir: File) { val mainLibs = arrayOf( - "libcrypto-1_1-x64.dll", + "libcrypto-3-x64.dll", "libsimplex.dll", "libapp-lib.dll" ) diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 9a7b8ab811..a51b70e52b 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -21,16 +21,20 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true kotlin.mpp.androidSourceSetLayoutVersion=2 +kotlin.jvm.target=11 -android.version_name=5.4.2 -android.version_code=166 +android.version_name=6.3.7 +android.version_code=298 -desktop.version_name=5.4.2 -desktop.version_code=20 +android.bundle=false -kotlin.version=1.8.20 -gradle.plugin.version=7.4.2 -compose.version=1.5.10 +desktop.version_name=6.3.7 +desktop.version_code=108 + +kotlin.version=1.9.23 +gradle.plugin.version=8.2.0 +compose.version=1.7.0 + +# Choose sqlite or postgres backend +database.backend=sqlite diff --git a/apps/multiplatform/gradle/wrapper/gradle-wrapper.properties b/apps/multiplatform/gradle/wrapper/gradle-wrapper.properties index 00c09d815c..4e4a6a3f29 100644 --- a/apps/multiplatform/gradle/wrapper/gradle-wrapper.properties +++ b/apps/multiplatform/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Feb 14 14:23:51 GMT 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/apps/simplex-bot-advanced/Main.hs b/apps/simplex-bot-advanced/Main.hs index 50d7005e34..40e6686065 100644 --- a/apps/simplex-bot-advanced/Main.hs +++ b/apps/simplex-bot-advanced/Main.hs @@ -9,6 +9,7 @@ module Main where import Control.Concurrent.Async import Control.Concurrent.STM import Control.Monad +import Data.Text (Text) import qualified Data.Text as T import Simplex.Chat.Bot import Simplex.Chat.Controller @@ -18,6 +19,7 @@ import Simplex.Chat.Messages.CIContent import Simplex.Chat.Options import Simplex.Chat.Terminal (terminalChatConfig) import Simplex.Chat.Types +import Simplex.Messaging.Util (tshow) import System.Directory (getAppUserDataDirectory) import Text.Read @@ -29,28 +31,28 @@ main = do welcomeGetOpts :: IO ChatOpts welcomeGetOpts = do appDir <- getAppUserDataDirectory "simplex" - opts@ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix}} <- getChatOpts appDir "simplex_bot" + opts@ChatOpts {coreOptions} <- getChatOpts appDir "simplex_bot" putStrLn $ "SimpleX Chat Bot v" ++ versionNumber - putStrLn $ "db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db" + printDbOpts coreOptions pure opts -welcomeMessage :: String +welcomeMessage :: Text welcomeMessage = "Hello! I am a simple squaring bot.\nIf you send me a number, I will calculate its square" mySquaringBot :: User -> ChatController -> IO () mySquaringBot _user cc = do initializeBotAddress cc race_ (forever $ void getLine) . forever $ do - (_, _, resp) <- atomically . readTBQueue $ outputQ cc - case resp of - CRContactConnected _ contact _ -> do + (_, evt) <- atomically . readTBQueue $ outputQ cc + case evt of + Right (CEvtContactConnected _ contact _) -> do contactConnected contact sendMessage cc contact welcomeMessage - CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) -> do - let msg = T.unpack $ ciContentToText mc - number_ = readMaybe msg :: Maybe Integer + Right CEvtNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) : _} -> do + let msg = ciContentToText mc + number_ = readMaybe (T.unpack msg) :: Maybe Integer sendMessage cc contact $ case number_ of - Just n -> msg <> " * " <> msg <> " = " <> show (n * n) + Just n -> msg <> " * " <> msg <> " = " <> tshow (n * n) _ -> "\"" <> msg <> "\" is not a number" _ -> pure () where diff --git a/apps/simplex-bot/Main.hs b/apps/simplex-bot/Main.hs index c24f9c251f..290e6286b1 100644 --- a/apps/simplex-bot/Main.hs +++ b/apps/simplex-bot/Main.hs @@ -25,7 +25,7 @@ welcomeMessage = "Hello! I am a simple squaring bot.\nIf you send me a number, I welcomeGetOpts :: IO ChatOpts welcomeGetOpts = do appDir <- getAppUserDataDirectory "simplex" - opts@ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix}} <- getChatOpts appDir "simplex_bot" + opts@ChatOpts {coreOptions} <- getChatOpts appDir "simplex_bot" putStrLn $ "SimpleX Chat Bot v" ++ versionNumber - putStrLn $ "db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db" + printDbOpts coreOptions pure opts diff --git a/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs b/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs index 5fa3fff0a7..86f89f86e8 100644 --- a/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs +++ b/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs @@ -2,6 +2,7 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} module Broadcast.Bot where @@ -21,52 +22,46 @@ import Simplex.Chat.Messages.CIContent import Simplex.Chat.Options import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Types +import Simplex.Messaging.Util (tshow) import System.Directory (getAppUserDataDirectory) welcomeGetOpts :: IO BroadcastBotOpts welcomeGetOpts = do appDir <- getAppUserDataDirectory "simplex" - opts@BroadcastBotOpts {coreOptions = CoreChatOpts {dbFilePrefix}} <- getBroadcastBotOpts appDir "simplex_status_bot" + opts@BroadcastBotOpts {coreOptions} <- getBroadcastBotOpts appDir "simplex_status_bot" putStrLn $ "SimpleX Chat Bot v" ++ versionNumber - putStrLn $ "db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db" + printDbOpts coreOptions pure opts broadcastBot :: BroadcastBotOpts -> User -> ChatController -> IO () broadcastBot BroadcastBotOpts {publishers, welcomeMessage, prohibitedMessage} _user cc = do initializeBotAddress cc race_ (forever $ void getLine) . forever $ do - (_, _, resp) <- atomically . readTBQueue $ outputQ cc - case resp of - CRContactConnected _ ct _ -> do + (_, evt) <- atomically . readTBQueue $ outputQ cc + case evt of + Right (CEvtContactConnected _ ct _) -> do contactConnected ct sendMessage cc ct welcomeMessage - CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc}) - | publisher `elem` publishers -> - if allowContent mc - then do - sendChatCmd cc ListContacts >>= \case - CRContactsList _ cts -> void . forkIO $ do - let cts' = filter broadcastTo cts - forM_ cts' $ \ct' -> sendComposedMessage cc ct' Nothing mc - sendReply $ "Forwarded to " <> show (length cts') <> " contact(s)" - r -> putStrLn $ "Error getting contacts list: " <> show r - else sendReply "!1 Message is not supported!" - | otherwise -> do - sendReply prohibitedMessage - deleteMessage cc ct $ chatItemId' ci + Right CEvtNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc}) : _} + | sender `notElem` publishers -> do + sendReply prohibitedMessage + deleteMessage cc ct $ chatItemId' ci + | allowContent mc -> + void $ forkIO $ + sendChatCmd cc (SendMessageBroadcast mc) >>= \case + Right CRBroadcastSent {successes, failures} -> + sendReply $ "Forwarded to " <> tshow successes <> " contact(s), " <> tshow failures <> " errors" + r -> putStrLn $ "Error broadcasting message: " <> show r + | otherwise -> + sendReply "!1 Message is not supported!" where - sendReply = sendComposedMessage cc ct (Just $ chatItemId' ci) . textMsgContent - publisher = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct} + sendReply = sendComposedMessage cc ct (Just $ chatItemId' ci) . MCText + sender = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct} allowContent = \case MCText _ -> True MCLink {} -> True MCImage {} -> True _ -> False - broadcastTo Contact {activeConn = Nothing} = False - broadcastTo ct'@Contact {activeConn = Just conn@Connection {connStatus}} = - (connStatus == ConnSndReady || connStatus == ConnReady) - && not (connDisabled conn) - && contactId' ct' /= contactId' ct _ -> pure () where contactConnected ct = putStrLn $ T.unpack (localDisplayName' ct) <> " connected" diff --git a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs index 9a79af4b48..8107b664c4 100644 --- a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs +++ b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs @@ -7,27 +7,28 @@ module Broadcast.Options where import Data.Maybe (fromMaybe) +import Data.Text (Text) import Options.Applicative import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller (updateStr, versionNumber, versionString) -import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts, coreChatOptsP) +import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, coreChatOptsP) data BroadcastBotOpts = BroadcastBotOpts { coreOptions :: CoreChatOpts, publishers :: [KnownContact], - welcomeMessage :: String, - prohibitedMessage :: String + welcomeMessage :: Text, + prohibitedMessage :: Text } -defaultWelcomeMessage :: [KnownContact] -> String +defaultWelcomeMessage :: [KnownContact] -> Text defaultWelcomeMessage ps = "Hello! I am a broadcast bot.\nI broadcast messages to all connected users from " <> knownContactNames ps <> "." -defaultProhibitedMessage :: [KnownContact] -> String +defaultProhibitedMessage :: [KnownContact] -> Text defaultProhibitedMessage ps = "Sorry, only these users can broadcast messages: " <> knownContactNames ps <> ". Your message is deleted." broadcastBotOpts :: FilePath -> FilePath -> Parser BroadcastBotOpts -broadcastBotOpts appDir defaultDbFileName = do - coreOptions <- coreChatOptsP appDir defaultDbFileName +broadcastBotOpts appDir defaultDbName = do + coreOptions <- coreChatOptsP appDir defaultDbName publishers <- option parseKnownContacts @@ -60,10 +61,10 @@ broadcastBotOpts appDir defaultDbFileName = do } getBroadcastBotOpts :: FilePath -> FilePath -> IO BroadcastBotOpts -getBroadcastBotOpts appDir defaultDbFileName = +getBroadcastBotOpts appDir defaultDbName = execParser $ info - (helper <*> versionOption <*> broadcastBotOpts appDir defaultDbFileName) + (helper <*> versionOption <*> broadcastBotOpts appDir defaultDbName) (header versionStr <> fullDesc <> progDesc "Start chat bot with DB_FILE file and use SERVER as SMP server") where versionStr = versionString versionNumber @@ -74,11 +75,12 @@ mkChatOpts :: BroadcastBotOpts -> ChatOpts mkChatOpts BroadcastBotOpts {coreOptions} = ChatOpts { coreOptions, - deviceName = Nothing, chatCmd = "", chatCmdDelay = 3, + chatCmdLog = CCLNone, chatServerPort = Nothing, optFilesFolder = Nothing, + optTempDirectory = Nothing, showReactions = False, allowInstantFiles = True, autoAcceptFileSize = 0, diff --git a/apps/simplex-chat/Main.hs b/apps/simplex-chat/Main.hs index ccfc6a4848..41321edc68 100644 --- a/apps/simplex-chat/Main.hs +++ b/apps/simplex-chat/Main.hs @@ -1,51 +1,8 @@ -{-# LANGUAGE NamedFieldPuns #-} - module Main where -import Control.Concurrent (threadDelay) -import Control.Concurrent.STM.TVar (readTVarIO) -import Data.Time.Clock (getCurrentTime) -import Data.Time.LocalTime (getCurrentTimeZone) -import Server -import Simplex.Chat.Controller (currentRemoteHost, versionNumber, versionString) -import Simplex.Chat.Core -import Simplex.Chat.Options -import Simplex.Chat.Terminal -import Simplex.Chat.View (serializeChatResponse) -import Simplex.Messaging.Client (NetworkConfig (..)) -import System.Directory (getAppUserDataDirectory) -import System.Terminal (withTerminal) +import Server (simplexChatServer) +import Simplex.Chat.Terminal (terminalChatConfig) +import Simplex.Chat.Terminal.Main (simplexChatCLI) main :: IO () -main = do - appDir <- getAppUserDataDirectory "simplex" - opts@ChatOpts {chatCmd, chatServerPort} <- getChatOpts appDir "simplex_v1" - if null chatCmd - then case chatServerPort of - Just chatPort -> - simplexChatServer defaultChatServerConfig {chatPort} terminalChatConfig opts - _ -> do - welcome opts - t <- withTerminal pure - simplexChatTerminal terminalChatConfig opts t - else simplexChatCore terminalChatConfig opts $ \user cc -> do - rh <- readTVarIO $ currentRemoteHost cc - let cmdRH = rh -- response RemoteHost is the same as for the command itself - r <- sendChatCmdStr cc chatCmd - ts <- getCurrentTime - tz <- getCurrentTimeZone - putStrLn $ serializeChatResponse (rh, Just user) ts tz cmdRH r - threadDelay $ chatCmdDelay opts * 1000000 - -welcome :: ChatOpts -> IO () -welcome ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, networkConfig}} = - mapM_ - putStrLn - [ versionString versionNumber, - "db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db", - maybe - "direct network connection - use `/network` command or `-x` CLI option to connect via SOCKS5 at :9050" - (("using SOCKS5 proxy " <>) . show) - (socksProxy networkConfig), - "type \"/help\" or \"/h\" for usage info" - ] +main = simplexChatCLI terminalChatConfig (Just simplexChatServer) diff --git a/apps/simplex-chat/Server.hs b/apps/simplex-chat/Server.hs index 3f4484eac6..0906d14536 100644 --- a/apps/simplex-chat/Server.hs +++ b/apps/simplex-chat/Server.hs @@ -2,35 +2,74 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE UndecidableInstances #-} module Server where import Control.Monad import Control.Monad.Except import Control.Monad.Reader -import Data.Aeson (FromJSON, ToJSON) +import Data.Aeson (FromJSON, ToJSON (..)) import qualified Data.Aeson as J +import qualified Data.Aeson.TH as JQ +import Data.Bifunctor (first) import Data.Text (Text) import Data.Text.Encoding (encodeUtf8) import GHC.Generics (Generic) import Network.Socket import qualified Network.WebSockets as WS import Numeric.Natural (Natural) -import Simplex.Chat import Simplex.Chat.Controller import Simplex.Chat.Core +import Simplex.Chat.Library.Commands import Simplex.Chat.Options -import Simplex.Messaging.Transport.Server (runTCPServer) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, taggedObjectJSON) +import Simplex.Messaging.Transport.Server (runLocalTCPServer) import Simplex.Messaging.Util (raceAny_) import UnliftIO.Exception import UnliftIO.STM -simplexChatServer :: ChatServerConfig -> ChatConfig -> ChatOpts -> IO () -simplexChatServer srvCfg cfg opts = - simplexChatCore cfg opts . const $ runChatServer srvCfg +data ChatSrvRequest = ChatSrvRequest {corrId :: Text, cmd :: Text} + deriving (Generic, FromJSON) + +data ChatSrvResponse r = ChatSrvResponse {corrId :: Maybe Text, resp :: CSRBody r} + +data CSRBody r = CSRBody {csrBody :: Either ChatError r} + +-- backwards compatible encoding, to avoid breaking any chat bots +data ObjChatCmdError = ObjChatCmdError {chatError :: ChatError} + +data ObjChatError = ObjChatError {chatError :: ChatError} + +$(JQ.deriveToJSON (taggedObjectJSON $ dropPrefix "Obj") ''ObjChatCmdError) + +$(JQ.deriveToJSON (taggedObjectJSON $ dropPrefix "Obj") ''ObjChatError) + +instance ToJSON (CSRBody ChatResponse) where + toJSON = toJSON . first ObjChatCmdError . csrBody + toEncoding = toEncoding . first ObjChatCmdError . csrBody + +instance ToJSON (CSRBody ChatEvent) where + toJSON = toJSON . first ObjChatError . csrBody + toEncoding = toEncoding . first ObjChatError . csrBody + +data AChatSrvResponse = forall r. ToJSON (ChatSrvResponse r) => ACR (ChatSrvResponse r) + +$(pure []) + +instance ToJSON (CSRBody r) => ToJSON (ChatSrvResponse r) where + toEncoding = $(JQ.mkToEncoding defaultJSON ''ChatSrvResponse) + toJSON = $(JQ.mkToJSON defaultJSON ''ChatSrvResponse) + +simplexChatServer :: ServiceName -> ChatConfig -> ChatOpts -> IO () +simplexChatServer chatPort cfg opts = + simplexChatCore cfg opts . const $ runChatServer defaultChatServerConfig {chatPort} data ChatServerConfig = ChatServerConfig { chatPort :: ServiceName, @@ -44,19 +83,9 @@ defaultChatServerConfig = clientQSize = 1 } -data ChatSrvRequest = ChatSrvRequest {corrId :: Text, cmd :: Text} - deriving (Generic, FromJSON) - -data ChatSrvResponse = ChatSrvResponse {corrId :: Maybe Text, resp :: ChatResponse} - deriving (Generic) - -instance ToJSON ChatSrvResponse where - toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} - toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} - data ChatClient = ChatClient { rcvQ :: TBQueue (Text, ChatCommand), - sndQ :: TBQueue ChatSrvResponse + sndQ :: TBQueue AChatSrvResponse } newChatServerClient :: Natural -> STM ChatClient @@ -68,7 +97,7 @@ newChatServerClient qSize = do runChatServer :: ChatServerConfig -> ChatController -> IO () runChatServer ChatServerConfig {chatPort, clientQSize} cc = do started <- newEmptyTMVarIO - runTCPServer started chatPort $ \sock -> do + runLocalTCPServer started chatPort $ \sock -> do ws <- liftIO $ getConnection sock c <- atomically $ newChatServerClient clientQSize putStrLn "client connected" @@ -78,14 +107,14 @@ runChatServer ChatServerConfig {chatPort, clientQSize} cc = do getConnection sock = WS.makePendingConnection sock WS.defaultConnectionOptions >>= WS.acceptRequest send ws ChatClient {sndQ} = forever $ - atomically (readTBQueue sndQ) >>= WS.sendTextData ws . J.encode + atomically (readTBQueue sndQ) >>= \(ACR r) -> WS.sendTextData ws (J.encode r) client ChatClient {rcvQ, sndQ} = forever $ do atomically (readTBQueue rcvQ) >>= processCommand - >>= atomically . writeTBQueue sndQ + >>= atomically . writeTBQueue sndQ . ACR output ChatClient {sndQ} = forever $ do - (_, _, resp) <- atomically . readTBQueue $ outputQ cc - atomically $ writeTBQueue sndQ ChatSrvResponse {corrId = Nothing, resp} + (_, r) <- atomically . readTBQueue $ outputQ cc + atomically $ writeTBQueue sndQ $ ACR ChatSrvResponse {corrId = Nothing, resp = CSRBody r} receive ws ChatClient {rcvQ, sndQ} = forever $ do s <- WS.receiveData ws case J.decodeStrict' s of @@ -96,11 +125,9 @@ runChatServer ChatServerConfig {chatPort, clientQSize} cc = do Left e -> sendError (Just corrId) e Nothing -> sendError Nothing "invalid request" where - sendError corrId e = atomically $ writeTBQueue sndQ ChatSrvResponse {corrId, resp = chatCmdError Nothing e} + sendError corrId e = atomically $ writeTBQueue sndQ $ ACR ChatSrvResponse {corrId, resp = CSRBody $ chatCmdError e} processCommand (corrId, cmd) = - runReaderT (runExceptT $ processChatCommand cmd) cc >>= \case - Right resp -> response resp - Left e -> response $ CRChatCmdError Nothing e + response <$> runReaderT (runExceptT $ processChatCommand cmd) cc where - response resp = pure ChatSrvResponse {corrId = Just corrId, resp} + response r = ChatSrvResponse {corrId = Just corrId, resp = CSRBody r} clientDisconnected _ = pure () diff --git a/apps/simplex-directory-service/Main.hs b/apps/simplex-directory-service/Main.hs index af9c9dd252..2091ab444b 100644 --- a/apps/simplex-directory-service/Main.hs +++ b/apps/simplex-directory-service/Main.hs @@ -5,11 +5,17 @@ module Main where import Directory.Options import Directory.Service import Directory.Store +import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHooks) import Simplex.Chat.Core import Simplex.Chat.Terminal (terminalChatConfig) main :: IO () main = do - opts@DirectoryOpts {directoryLog} <- welcomeGetOpts + opts@DirectoryOpts {directoryLog, runCLI} <- welcomeGetOpts st <- restoreDirectoryStore directoryLog - simplexChatCore terminalChatConfig (mkChatOpts opts) $ directoryService st opts + if runCLI + then directoryServiceCLI st opts + else do + env <- newServiceState opts + let cfg = terminalChatConfig {chatHooks = defaultChatHooks {acceptMember = Just $ acceptMemberHook opts env}} + simplexChatCore cfg (mkChatOpts opts) $ directoryService st opts env diff --git a/apps/simplex-directory-service/src/Directory/BlockedWords.hs b/apps/simplex-directory-service/src/Directory/BlockedWords.hs new file mode 100644 index 0000000000..a29e2c99e0 --- /dev/null +++ b/apps/simplex-directory-service/src/Directory/BlockedWords.hs @@ -0,0 +1,77 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} + +module Directory.BlockedWords where + +import Data.Char (isMark, isPunctuation, isSpace) +import Data.List (isPrefixOf) +import Data.Maybe (fromMaybe) +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as M +import Data.Set (Set) +import qualified Data.Set as S +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.Normalize as TN + +data BlockedWordsConfig = BlockedWordsConfig + { blockedWords :: Set Text, + blockedFragments :: Set Text, + extensionRules :: [(String, [String])], + spelling :: Map Char [Char] + } + +hasBlockedFragments :: BlockedWordsConfig -> Text -> Bool +hasBlockedFragments BlockedWordsConfig {spelling, blockedFragments} s = + any (\w -> any (`T.isInfixOf` w) blockedFragments) ws + where + ws = S.fromList $ filter (not . T.null) $ normalizeText spelling s + +hasBlockedWords :: BlockedWordsConfig -> Text -> Bool +hasBlockedWords BlockedWordsConfig {spelling, blockedWords} s = + not $ ws1 `S.disjoint` blockedWords && (length ws <= 1 || ws2 `S.disjoint` blockedWords) + where + ws = T.words s + ws1 = normalizeWords ws + ws2 = normalizeWords $ T.splitOn " " s + normalizeWords = S.fromList . filter (not . T.null) . concatMap (normalizeText spelling) + +normalizeText :: Map Char [Char] -> Text -> [Text] +normalizeText spelling' = + map (T.pack . filter (\c -> not $ isSpace c || isPunctuation c || isMark c)) + . allSubstitutions spelling' + . removeTriples + . T.unpack + . T.toLower + . TN.normalize TN.NFKD + +-- replaces triple and larger occurences with doubles +removeTriples :: String -> String +removeTriples xs = go xs '\0' False + where + go [] _ _ = [] + go (c : cs) prev samePrev + | prev /= c = c : go cs c False + | samePrev = go cs c True + | otherwise = c : go cs c True + +-- Generate all possible strings by substituting each character +allSubstitutions :: Map Char [Char] -> String -> [String] +allSubstitutions spelling' = sequence . map substs + where + substs c = fromMaybe [c] $ M.lookup c spelling' + +wordVariants :: [(String, [String])] -> String -> [Text] +wordVariants [] s = [T.pack s] +wordVariants (sub : subs) s = concatMap (wordVariants subs) (replace sub) + where + replace (pat, tos) = go s + where + go [] = [""] + go s'@(c : rest) + | pat `isPrefixOf` s' = + let s'' = drop (length pat) s' + restVariants = go s'' + in map (pat <>) restVariants + <> concatMap (\to -> map (to <>) restVariants) tos + | otherwise = map (c :) (go rest) diff --git a/apps/simplex-directory-service/src/Directory/Captcha.hs b/apps/simplex-directory-service/src/Directory/Captcha.hs new file mode 100644 index 0000000000..d60b09df83 --- /dev/null +++ b/apps/simplex-directory-service/src/Directory/Captcha.hs @@ -0,0 +1,37 @@ +module Directory.Captcha (getCaptchaStr, matchCaptchaStr) where + +import qualified Data.Map.Strict as M +import Data.Maybe (fromMaybe) +import qualified Data.Text as T +import System.Random (randomRIO) + +getCaptchaStr :: Int -> String -> IO String +getCaptchaStr 0 s = pure s +getCaptchaStr n s = do + i <- randomRIO (0, length captchaChars - 1) + let c = captchaChars !! i + getCaptchaStr (n - 1) (c : s) + where + captchaChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +matchCaptchaStr :: T.Text -> T.Text -> Bool +matchCaptchaStr captcha guess = T.length captcha == T.length guess && matchChars (T.zip captcha guess) + where + matchChars [] = True + matchChars ((c, g) : cs) = matchChar c == matchChar g && matchChars cs + matchChar c = fromMaybe c $ M.lookup c captchaMatches + captchaMatches = + M.fromList + [ ('0', 'O'), + ('1', 'I'), + ('c', 'C'), + ('l', 'I'), + ('o', 'O'), + ('p', 'P'), + ('s', 'S'), + ('u', 'U'), + ('v', 'V'), + ('w', 'W'), + ('x', 'X'), + ('z', 'Z') + ] diff --git a/apps/simplex-directory-service/src/Directory/Events.hs b/apps/simplex-directory-service/src/Directory/Events.hs index a187ac3e82..faaccbd2bf 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -11,36 +11,44 @@ module Directory.Events ( DirectoryEvent (..), DirectoryCmd (..), ADirectoryCmd (..), + DirectoryHelpSection (..), DirectoryRole (..), SDirectoryRole (..), crDirectoryEvent, - viewName, + directoryCmdTag, ) where -import Control.Applicative ((<|>)) +import Control.Applicative (optional, (<|>)) import Data.Attoparsec.Text (Parser) import qualified Data.Attoparsec.Text as A +import Data.Char (isSpace) +import Data.Either (fromRight) import Data.Functor (($>)) +import Data.Maybe (fromMaybe) import Data.Text (Text) 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.Messages import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Types +import Simplex.Chat.Types.Shared +import Simplex.Messaging.Agent.Protocol (AgentErrorType (..)) import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Util ((<$?>)) -import Data.Char (isSpace) -import Data.Either (fromRight) +import Simplex.Messaging.Protocol (BrokerErrorType (..)) +import Simplex.Messaging.Util (tshow, (<$?>)) data DirectoryEvent = DEContactConnected Contact | DEGroupInvitation {contact :: Contact, groupInfo :: GroupInfo, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole} - | DEServiceJoinedGroup {contactId :: ContactId, groupInfo :: GroupInfo, hostMember :: GroupMember} - | DEGroupUpdated {contactId :: ContactId, fromGroup :: GroupInfo, toGroup :: GroupInfo} + | DEServiceJoinedGroup {contactId :: ContactId, groupInfo :: GroupInfo, hostMember :: GroupMember} + | DEGroupUpdated {member :: GroupMember, fromGroup :: GroupInfo, toGroup :: GroupInfo} + | DEPendingMember GroupInfo GroupMember + | DEPendingMemberMsg GroupInfo GroupMember ChatItemId Text | DEContactRoleChanged GroupInfo ContactId GroupMemberRole -- contactId here is the contact whose role changed | DEServiceRoleChanged GroupInfo GroupMemberRole | DEContactRemovedFromGroup ContactId GroupInfo @@ -51,36 +59,56 @@ data DirectoryEvent | DEItemEditIgnored Contact | DEItemDeleteIgnored Contact | DEContactCommand Contact ChatItemId ADirectoryCmd + | DELogChatResponse Text deriving (Show) -crDirectoryEvent :: ChatResponse -> Maybe DirectoryEvent +crDirectoryEvent :: Either ChatError ChatEvent -> Maybe DirectoryEvent crDirectoryEvent = \case - CRContactConnected {contact} -> Just $ DEContactConnected contact - CRReceivedGroupInvitation {contact, groupInfo, fromMemberRole, memberRole} -> Just $ DEGroupInvitation {contact, groupInfo, fromMemberRole, memberRole} - CRUserJoinedGroup {groupInfo, hostMember} -> (\contactId -> DEServiceJoinedGroup {contactId, groupInfo, hostMember}) <$> memberContactId hostMember - CRGroupUpdated {fromGroup, toGroup, member_} -> (\contactId -> DEGroupUpdated {contactId, fromGroup, toGroup}) <$> (memberContactId =<< member_) - CRMemberRole {groupInfo, member, toRole} + Right evt -> crDirectoryEvent_ evt + Left e -> case e of + ChatErrorAgent {agentError = BROKER _ NETWORK} -> Nothing + ChatErrorAgent {agentError = BROKER _ TIMEOUT} -> Nothing + _ -> Just $ DELogChatResponse $ "chat error: " <> tshow e + +crDirectoryEvent_ :: ChatEvent -> Maybe DirectoryEvent +crDirectoryEvent_ = \case + CEvtContactConnected {contact} -> Just $ DEContactConnected contact + CEvtReceivedGroupInvitation {contact, groupInfo, fromMemberRole, memberRole} -> Just $ DEGroupInvitation {contact, groupInfo, fromMemberRole, memberRole} + CEvtUserJoinedGroup {groupInfo, hostMember} -> (\contactId -> DEServiceJoinedGroup {contactId, groupInfo, hostMember}) <$> memberContactId hostMember + CEvtGroupUpdated {fromGroup, toGroup, member_} -> (\member -> DEGroupUpdated {member, fromGroup, toGroup}) <$> member_ + CEvtJoinedGroupMember {groupInfo, member = m} + | pending m -> Just $ DEPendingMember groupInfo m + | otherwise -> Nothing + CEvtNewChatItems {chatItems = AChatItem _ _ (GroupChat g) ci : _} -> case ci of + ChatItem {chatDir = CIGroupRcv m, content = CIRcvMsgContent (MCText t)} | pending m -> Just $ DEPendingMemberMsg g m (chatItemId' ci) t + _ -> Nothing + CEvtMemberRole {groupInfo, member, toRole} | groupMemberId' member == groupMemberId' (membership groupInfo) -> Just $ DEServiceRoleChanged groupInfo toRole | otherwise -> (\ctId -> DEContactRoleChanged groupInfo ctId toRole) <$> memberContactId member - CRDeletedMember {groupInfo, deletedMember} -> (`DEContactRemovedFromGroup` groupInfo) <$> memberContactId deletedMember - CRLeftMember {groupInfo, member} -> (`DEContactLeftGroup` groupInfo) <$> memberContactId member - CRDeletedMemberUser {groupInfo} -> Just $ DEServiceRemovedFromGroup groupInfo - CRGroupDeleted {groupInfo} -> Just $ DEGroupDeleted groupInfo - CRChatItemUpdated {chatItem = AChatItem _ SMDRcv (DirectChat ct) _} -> Just $ DEItemEditIgnored ct - CRChatItemDeleted {deletedChatItem = AChatItem _ SMDRcv (DirectChat ct) _, byUser = False} -> Just $ DEItemDeleteIgnored ct - CRNewChatItem {chatItem = AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc, meta = CIMeta {itemLive}}} -> + CEvtDeletedMember {groupInfo, deletedMember} -> (`DEContactRemovedFromGroup` groupInfo) <$> memberContactId deletedMember + CEvtLeftMember {groupInfo, member} -> (`DEContactLeftGroup` groupInfo) <$> memberContactId member + CEvtDeletedMemberUser {groupInfo} -> Just $ DEServiceRemovedFromGroup groupInfo + CEvtGroupDeleted {groupInfo} -> Just $ DEGroupDeleted groupInfo + 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}}) : _} -> Just $ case (mc, itemLive) of - (MCText t, Nothing) -> DEContactCommand ct ciId $ fromRight err $ A.parseOnly directoryCmdP $ T.dropWhileEnd isSpace t + (MCText t, Nothing) -> DEContactCommand ct ciId $ fromRight err $ A.parseOnly (directoryCmdP <* A.endOfInput) $ T.dropWhileEnd isSpace t _ -> DEUnsupportedMessage ct ciId where ciId = chatItemId' ci err = ADC SDRUser DCUnknownCommand + CEvtMessageError {severity, errorMessage} -> Just $ DELogChatResponse $ "message error: " <> severity <> ", " <> errorMessage + CEvtChatErrors {chatErrors} -> Just $ DELogChatResponse $ "chat errors: " <> T.intercalate ", " (map tshow chatErrors) _ -> Nothing + where + pending m = memberStatus m == GSMemPendingApproval -data DirectoryRole = DRUser | DRSuperUser +data DirectoryRole = DRUser | DRAdmin | DRSuperUser data SDirectoryRole (r :: DirectoryRole) where SDRUser :: SDirectoryRole 'DRUser + SDRAdmin :: SDirectoryRole 'DRAdmin SDRSuperUser :: SDirectoryRole 'DRSuperUser deriving instance Show (SDirectoryRole r) @@ -94,19 +122,30 @@ data DirectoryCmdTag (r :: DirectoryRole) where DCConfirmDuplicateGroup_ :: DirectoryCmdTag 'DRUser DCListUserGroups_ :: DirectoryCmdTag 'DRUser DCDeleteGroup_ :: DirectoryCmdTag 'DRUser - DCApproveGroup_ :: DirectoryCmdTag 'DRSuperUser - DCRejectGroup_ :: DirectoryCmdTag 'DRSuperUser - DCSuspendGroup_ :: DirectoryCmdTag 'DRSuperUser - DCResumeGroup_ :: DirectoryCmdTag 'DRSuperUser - DCListLastGroups_ :: DirectoryCmdTag 'DRSuperUser + DCMemberRole_ :: DirectoryCmdTag 'DRUser + DCGroupFilter_ :: DirectoryCmdTag 'DRUser + DCApproveGroup_ :: DirectoryCmdTag 'DRAdmin + DCRejectGroup_ :: DirectoryCmdTag 'DRAdmin + DCSuspendGroup_ :: DirectoryCmdTag 'DRAdmin + DCResumeGroup_ :: DirectoryCmdTag 'DRAdmin + DCListLastGroups_ :: DirectoryCmdTag 'DRAdmin + DCListPendingGroups_ :: DirectoryCmdTag 'DRAdmin + DCShowGroupLink_ :: DirectoryCmdTag 'DRAdmin + DCSendToGroupOwner_ :: DirectoryCmdTag 'DRAdmin + DCInviteOwnerToGroup_ :: DirectoryCmdTag 'DRAdmin + -- DCAddBlockedWord_ :: DirectoryCmdTag 'DRAdmin + -- DCRemoveBlockedWord_ :: DirectoryCmdTag 'DRAdmin DCExecuteCommand_ :: DirectoryCmdTag 'DRSuperUser deriving instance Show (DirectoryCmdTag r) data ADirectoryCmdTag = forall r. ADCT (SDirectoryRole r) (DirectoryCmdTag r) +data DirectoryHelpSection = DHSRegistration | DHSCommands + deriving (Show) + data DirectoryCmd (r :: DirectoryRole) where - DCHelp :: DirectoryCmd 'DRUser + DCHelp :: DirectoryHelpSection -> DirectoryCmd 'DRUser DCSearchGroup :: Text -> DirectoryCmd 'DRUser DCSearchNext :: DirectoryCmd 'DRUser DCAllGroups :: DirectoryCmd 'DRUser @@ -115,11 +154,19 @@ data DirectoryCmd (r :: DirectoryRole) where DCConfirmDuplicateGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser DCListUserGroups :: DirectoryCmd 'DRUser DCDeleteGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser - DCApproveGroup :: {groupId :: GroupId, displayName :: GroupName, groupApprovalId :: GroupApprovalId} -> DirectoryCmd 'DRSuperUser - DCRejectGroup :: GroupId -> GroupName -> DirectoryCmd 'DRSuperUser - DCSuspendGroup :: GroupId -> GroupName -> DirectoryCmd 'DRSuperUser - DCResumeGroup :: GroupId -> GroupName -> DirectoryCmd 'DRSuperUser - DCListLastGroups :: Int -> DirectoryCmd 'DRSuperUser + DCMemberRole :: UserGroupRegId -> Maybe GroupName -> Maybe GroupMemberRole -> DirectoryCmd 'DRUser + DCGroupFilter :: UserGroupRegId -> Maybe GroupName -> Maybe DirectoryMemberAcceptance -> DirectoryCmd 'DRUser + DCApproveGroup :: {groupId :: GroupId, displayName :: GroupName, groupApprovalId :: GroupApprovalId} -> DirectoryCmd 'DRAdmin + DCRejectGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin + DCSuspendGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin + DCResumeGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin + DCListLastGroups :: Int -> DirectoryCmd 'DRAdmin + DCListPendingGroups :: Int -> DirectoryCmd 'DRAdmin + DCShowGroupLink :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin + DCSendToGroupOwner :: GroupId -> GroupName -> Text -> DirectoryCmd 'DRAdmin + DCInviteOwnerToGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin + -- DCAddBlockedWord :: Text -> DirectoryCmd 'DRAdmin + -- DCRemoveBlockedWord :: Text -> DirectoryCmd 'DRAdmin DCExecuteCommand :: String -> DirectoryCmd 'DRSuperUser DCUnknownCommand :: DirectoryCmd 'DRUser DCCommandError :: DirectoryCmdTag r -> DirectoryCmd r @@ -139,55 +186,134 @@ directoryCmdP = cmdStrP = (tagP >>= \(ADCT u t) -> ADC u <$> (cmdP t <|> pure (DCCommandError t))) <|> pure (ADC SDRUser DCUnknownCommand) - tagP = A.takeTill (== ' ') >>= \case - "help" -> u DCHelp_ - "h" -> u DCHelp_ - "next" -> u DCSearchNext_ - "all" -> u DCAllGroups_ - "new" -> u DCRecentGroups_ - "submit" -> u DCSubmitGroup_ - "confirm" -> u DCConfirmDuplicateGroup_ - "list" -> u DCListUserGroups_ - "ls" -> u DCListUserGroups_ - "delete" -> u DCDeleteGroup_ - "approve" -> su DCApproveGroup_ - "reject" -> su DCRejectGroup_ - "suspend" -> su DCSuspendGroup_ - "resume" -> su DCResumeGroup_ - "last" -> su DCListLastGroups_ - "exec" -> su DCExecuteCommand_ - "x" -> su DCExecuteCommand_ - _ -> fail "bad command tag" + tagP = + A.takeTill isSpace >>= \case + "help" -> u DCHelp_ + "h" -> u DCHelp_ + "next" -> u DCSearchNext_ + "all" -> u DCAllGroups_ + "new" -> u DCRecentGroups_ + "submit" -> u DCSubmitGroup_ + "confirm" -> u DCConfirmDuplicateGroup_ + "list" -> u DCListUserGroups_ + "ls" -> u DCListUserGroups_ + "delete" -> u DCDeleteGroup_ + "role" -> u DCMemberRole_ + "filter" -> u DCGroupFilter_ + "approve" -> au DCApproveGroup_ + "reject" -> au DCRejectGroup_ + "suspend" -> au DCSuspendGroup_ + "resume" -> au DCResumeGroup_ + "last" -> au DCListLastGroups_ + "pending" -> au DCListPendingGroups_ + "link" -> au DCShowGroupLink_ + "owner" -> au DCSendToGroupOwner_ + "invite" -> au DCInviteOwnerToGroup_ + -- "block_word" -> au DCAddBlockedWord_ + -- "unblock_word" -> au DCRemoveBlockedWord_ + "exec" -> su DCExecuteCommand_ + "x" -> su DCExecuteCommand_ + _ -> fail "bad command tag" where u = pure . ADCT SDRUser + au = pure . ADCT SDRAdmin su = pure . ADCT SDRSuperUser cmdP :: DirectoryCmdTag r -> Parser (DirectoryCmd r) cmdP = \case - DCHelp_ -> pure DCHelp + DCHelp_ -> DCHelp . fromMaybe DHSRegistration <$> optional (A.takeWhile isSpace *> helpSectionP) + where + helpSectionP = + A.takeText >>= \case + "registration" -> pure DHSRegistration + "r" -> pure DHSRegistration + "commands" -> pure DHSCommands + "c" -> pure DHSCommands + _ -> fail "bad help section" DCSearchNext_ -> pure DCSearchNext DCAllGroups_ -> pure DCAllGroups DCRecentGroups_ -> pure DCRecentGroups - DCSubmitGroup_ -> fmap DCSubmitGroup . strDecode . encodeUtf8 <$?> (A.takeWhile1 isSpace *> A.takeText) + DCSubmitGroup_ -> fmap DCSubmitGroup . strDecode . encodeUtf8 <$?> (spacesP *> A.takeText) DCConfirmDuplicateGroup_ -> gc DCConfirmDuplicateGroup DCListUserGroups_ -> pure DCListUserGroups DCDeleteGroup_ -> gc DCDeleteGroup + DCMemberRole_ -> do + (groupId, displayName_) <- gc_ (,) + memberRole_ <- optional $ spacesP *> ("member" $> GRMember <|> "observer" $> GRObserver) + pure $ DCMemberRole groupId displayName_ memberRole_ + DCGroupFilter_ -> do + (groupId, displayName_) <- gc_ (,) + acceptance_ <- + (A.takeWhile isSpace >> A.endOfInput) $> Nothing + <|> Just <$> (acceptancePresetsP <|> acceptanceFiltersP) + pure $ DCGroupFilter groupId displayName_ acceptance_ + where + acceptancePresetsP = + spacesP + *> A.choice + [ "off" $> noJoinFilter, + "basic" $> basicJoinFilter, + ("moderate" <|> "mod") $> moderateJoinFilter, + "strong" $> strongJoinFilter + ] + acceptanceFiltersP = do + rejectNames <- filterP "name" + passCaptcha <- filterP "captcha" + makeObserver <- filterP "observer" + pure DirectoryMemberAcceptance {rejectNames, passCaptcha, makeObserver} + filterP :: Text -> Parser (Maybe ProfileCondition) + filterP s = Just <$> (spacesP *> A.string s *> conditionP) <|> pure Nothing + conditionP = + "=all" $> PCAll + <|> ("=noimage" <|> "=no_image" <|> "=no-image") $> PCNoImage + <|> pure PCAll DCApproveGroup_ -> do (groupId, displayName) <- gc (,) groupApprovalId <- A.space *> A.decimal - pure $ DCApproveGroup {groupId, displayName, groupApprovalId} + pure DCApproveGroup {groupId, displayName, groupApprovalId} DCRejectGroup_ -> gc DCRejectGroup DCSuspendGroup_ -> gc DCSuspendGroup DCResumeGroup_ -> gc DCResumeGroup DCListLastGroups_ -> DCListLastGroups <$> (A.space *> A.decimal <|> pure 10) - DCExecuteCommand_ -> DCExecuteCommand . T.unpack <$> (A.space *> A.takeText) + DCListPendingGroups_ -> DCListPendingGroups <$> (A.space *> A.decimal <|> pure 10) + DCShowGroupLink_ -> gc DCShowGroupLink + DCSendToGroupOwner_ -> do + (groupId, displayName) <- gc (,) + msg <- A.space *> A.takeText + pure $ DCSendToGroupOwner groupId displayName msg + DCInviteOwnerToGroup_ -> gc DCInviteOwnerToGroup + -- DCAddBlockedWord_ -> DCAddBlockedWord <$> wordP + -- DCRemoveBlockedWord_ -> DCRemoveBlockedWord <$> wordP + DCExecuteCommand_ -> DCExecuteCommand . T.unpack <$> (spacesP *> A.takeText) where - gc f = f <$> (A.space *> A.decimal <* A.char ':') <*> displayNameP - displayNameP = quoted '\'' <|> takeNameTill (== ' ') - takeNameTill p = - A.peekChar' >>= \c -> - if refChar c then A.takeTill p else fail "invalid first character in display name" - quoted c = A.char c *> takeNameTill (== c) <* A.char c - refChar c = c > ' ' && c /= '#' && c /= '@' + gc f = f <$> (spacesP *> A.decimal) <*> (A.char ':' *> displayNameTextP) + gc_ f = f <$> (spacesP *> A.decimal) <*> optional (A.char ':' *> displayNameTextP) + -- wordP = spacesP *> A.takeTill isSpace + spacesP = A.takeWhile1 isSpace -viewName :: String -> String -viewName n = if ' ' `elem` n then "'" <> n <> "'" else n +directoryCmdTag :: DirectoryCmd r -> Text +directoryCmdTag = \case + DCHelp _ -> "help" + DCSearchGroup _ -> "search" + DCSearchNext -> "next" + DCAllGroups -> "all" + DCRecentGroups -> "new" + DCSubmitGroup _ -> "submit" + DCConfirmDuplicateGroup {} -> "confirm" + DCListUserGroups -> "list" + DCDeleteGroup {} -> "delete" + DCApproveGroup {} -> "approve" + DCMemberRole {} -> "role" + DCGroupFilter {} -> "filter" + DCRejectGroup {} -> "reject" + DCSuspendGroup {} -> "suspend" + DCResumeGroup {} -> "resume" + DCListLastGroups _ -> "last" + DCListPendingGroups _ -> "pending" + DCShowGroupLink {} -> "link" + DCSendToGroupOwner {} -> "owner" + DCInviteOwnerToGroup {} -> "invite" + -- DCAddBlockedWord _ -> "block_word" + -- DCRemoveBlockedWord _ -> "unblock_word" + DCExecuteCommand _ -> "exec" + DCUnknownCommand -> "unknown" + DCCommandError _ -> "error" diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index 6d4e1296f2..2c26905e79 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -1,5 +1,6 @@ {-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -11,23 +12,41 @@ module Directory.Options ) where +import qualified Data.Text as T import Options.Applicative import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller (updateStr, versionNumber, versionString) -import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts, coreChatOptsP) +import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, coreChatOptsP) data DirectoryOpts = DirectoryOpts { coreOptions :: CoreChatOpts, + adminUsers :: [KnownContact], superUsers :: [KnownContact], + ownersGroup :: Maybe KnownGroup, + blockedWordsFile :: Maybe FilePath, + blockedFragmentsFile :: Maybe FilePath, + blockedExtensionRules :: Maybe FilePath, + nameSpellingFile :: Maybe FilePath, + profileNameLimit :: Int, + captchaGenerator :: Maybe FilePath, directoryLog :: Maybe FilePath, - serviceName :: String, + serviceName :: T.Text, + runCLI :: Bool, searchResults :: Int, testing :: Bool } directoryOpts :: FilePath -> FilePath -> Parser DirectoryOpts -directoryOpts appDir defaultDbFileName = do - coreOptions <- coreChatOptsP appDir defaultDbFileName +directoryOpts appDir defaultDbName = do + coreOptions <- coreChatOptsP appDir defaultDbName + adminUsers <- + option + parseKnownContacts + ( long "admin-users" + <> metavar "ADMIN_USERS" + <> value [] + <> help "Comma-separated list of admin-users in the format CONTACT_ID:DISPLAY_NAME who will be allowed to manage the directory" + ) superUsers <- option parseKnownContacts @@ -35,6 +54,57 @@ directoryOpts appDir defaultDbFileName = do <> metavar "SUPER_USERS" <> help "Comma-separated list of super-users in the format CONTACT_ID:DISPLAY_NAME who will be allowed to manage the directory" ) + ownersGroup <- + optional $ + option + parseKnownGroup + ( long "owners-group" + <> metavar "OWNERS_GROUP" + <> help "The group of group owners in the format GROUP_ID:DISPLAY_NAME - owners of listed groups will be invited automatically" + ) + blockedWordsFile <- + optional $ + strOption + ( long "blocked-words-file" + <> metavar "BLOCKED_WORDS_FILE" + <> help "File with the basic forms of words not allowed in profiles" + ) + blockedFragmentsFile <- + optional $ + strOption + ( long "blocked-fragments-file" + <> metavar "BLOCKED_WORDS_FILE" + <> help "File with the basic forms of word fragments not allowed in profiles" + ) + blockedExtensionRules <- + optional $ + strOption + ( long "blocked-extenstion-rules" + <> metavar "BLOCKED_EXTENSION_RULES" + <> help "Substitions to extend the list of blocked words" + ) + nameSpellingFile <- + optional $ + strOption + ( long "name-spelling-file" + <> metavar "NAME_SPELLING_FILE" + <> help "File with the character substitions to match in profile names" + ) + profileNameLimit <- + option + auto + ( long "profile-name-limit" + <> metavar "PROFILE_NAME_LIMIT" + <> help "Max length of profile name that will be allowed to connect and to join groups" + <> value maxBound + ) + captchaGenerator <- + optional $ + strOption + ( long "captcha-generator" + <> metavar "CAPTCHA_GENERATOR" + <> help "Executable to generate captcha files, must accept text as parameter and save file to stdout as base64 up to 12500 bytes" + ) directoryLog <- Just <$> strOption @@ -49,21 +119,35 @@ directoryOpts appDir defaultDbFileName = do <> help "The display name of the directory service bot, without *'s and spaces (SimpleX-Directory)" <> value "SimpleX-Directory" ) + runCLI <- + switch + ( long "run-cli" + <> help "Run directory service as CLI" + ) pure DirectoryOpts { coreOptions, + adminUsers, superUsers, + ownersGroup, + blockedWordsFile, + blockedFragmentsFile, + blockedExtensionRules, + nameSpellingFile, + profileNameLimit, + captchaGenerator, directoryLog, - serviceName, + serviceName = T.pack serviceName, + runCLI, searchResults = 10, testing = False } getDirectoryOpts :: FilePath -> FilePath -> IO DirectoryOpts -getDirectoryOpts appDir defaultDbFileName = +getDirectoryOpts appDir defaultDbName = execParser $ info - (helper <*> versionOption <*> directoryOpts appDir defaultDbFileName) + (helper <*> versionOption <*> directoryOpts appDir defaultDbName) (header versionStr <> fullDesc <> progDesc "Start SimpleX Directory Service with DB_FILE, DIRECTORY_FILE and SUPER_USERS options") where versionStr = versionString versionNumber @@ -74,11 +158,12 @@ mkChatOpts :: DirectoryOpts -> ChatOpts mkChatOpts DirectoryOpts {coreOptions} = ChatOpts { coreOptions, - deviceName = Nothing, chatCmd = "", chatCmdDelay = 3, + chatCmdLog = CCLNone, chatServerPort = Nothing, optFilesFolder = Nothing, + optTempDirectory = Nothing, showReactions = False, allowInstantFiles = True, autoAcceptFileSize = 0, diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index ea79dabb10..4517ee9c5b 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -1,29 +1,45 @@ +{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE MultiWayIf #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} module Directory.Service ( welcomeGetOpts, directoryService, + directoryServiceCLI, + newServiceState, + acceptMemberHook ) where import Control.Concurrent (forkIO) import Control.Concurrent.Async import Control.Concurrent.STM +import qualified Control.Exception as E +import Control.Logger.Simple import Control.Monad -import qualified Data.ByteString.Char8 as B -import Data.Maybe (fromMaybe, maybeToList) +import Control.Monad.Except +import Control.Monad.IO.Class +import Data.Int (Int64) +import Data.List (find, intercalate) +import Data.List.NonEmpty (NonEmpty (..)) +import qualified Data.Map.Strict as M +import Data.Maybe (fromMaybe, isJust, maybeToList) import Data.Set (Set) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T -import Data.Time.Clock (diffUTCTime, getCurrentTime) +import qualified Data.Text.IO as T +import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime, getCurrentTime) import Data.Time.LocalTime (getCurrentTimeZone) +import Directory.BlockedWords +import Directory.Captcha import Directory.Events import Directory.Options import Directory.Search @@ -32,16 +48,29 @@ import Simplex.Chat.Bot import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller import Simplex.Chat.Core +import Simplex.Chat.Markdown (FormattedText (..), Format (..), parseMaybeMarkdownList, viewName) import Simplex.Chat.Messages import Simplex.Chat.Options import Simplex.Chat.Protocol (MsgContent (..)) +import Simplex.Chat.Store.Direct (getContact) +import Simplex.Chat.Store.Groups (getGroupInfo, getGroupLink, getGroupSummary, setGroupCustomData) +import Simplex.Chat.Store.Profiles (GroupLinkInfo (..), getGroupLinkInfo) +import Simplex.Chat.Store.Shared (StoreError (..)) +import Simplex.Chat.Terminal (terminalChatConfig) +import Simplex.Chat.Terminal.Main (simplexChatCLI') import Simplex.Chat.Types -import Simplex.Chat.View (serializeChatResponse, simplexChatContact) +import Simplex.Chat.Types.Shared +import Simplex.Chat.View (serializeChatError, serializeChatResponse, simplexChatContact, viewContactName, viewGroupName) +import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnectionLink (..), CreatedConnLink (..)) +import Simplex.Messaging.Agent.Store.Common (withTransaction) +import Simplex.Messaging.Agent.Protocol (SConnectionMode (..), sameConnReqContact, sameShortLinkContact) +import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Encoding.String import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Util (safeDecodeUtf8, tshow, ($>>=), (<$$>)) import System.Directory (getAppUserDataDirectory) +import System.Process (readProcess) data GroupProfileUpdate = GPNoServiceLink | GPServiceLinkAdded | GPServiceLinkRemoved | GPHasServiceLink | GPServiceLinkError @@ -58,63 +87,161 @@ data GroupRolesStatus deriving (Eq) data ServiceState = ServiceState - { searchRequests :: TMap ContactId SearchRequest + { searchRequests :: TMap ContactId SearchRequest, + blockedWordsCfg :: BlockedWordsConfig, + pendingCaptchas :: TMap GroupMemberId PendingCaptcha } -newServiceState :: IO ServiceState -newServiceState = do - searchRequests <- atomically TM.empty - pure ServiceState {searchRequests} +data PendingCaptcha = PendingCaptcha + { captchaText :: Text, + sentAt :: UTCTime, + attempts :: Int + } + +captchaLength :: Int +captchaLength = 7 + +maxCaptchaAttempts :: Int +maxCaptchaAttempts = 5 + +captchaTTL :: NominalDiffTime +captchaTTL = 600 -- 10 minutes + +newServiceState :: DirectoryOpts -> IO ServiceState +newServiceState opts = do + searchRequests <- TM.emptyIO + blockedWordsCfg <- readBlockedWordsConfig opts + pendingCaptchas <- TM.emptyIO + pure ServiceState {searchRequests, blockedWordsCfg, pendingCaptchas} welcomeGetOpts :: IO DirectoryOpts welcomeGetOpts = do appDir <- getAppUserDataDirectory "simplex" - opts@DirectoryOpts {coreOptions = CoreChatOpts {dbFilePrefix}, testing} <- getDirectoryOpts appDir "simplex_directory_service" + opts@DirectoryOpts {coreOptions, testing, superUsers, adminUsers, ownersGroup} <- getDirectoryOpts appDir "simplex_directory_service" unless testing $ do putStrLn $ "SimpleX Directory Service Bot v" ++ versionNumber - putStrLn $ "db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db" + printDbOpts coreOptions + putStrLn $ knownContacts "superuser" superUsers + putStrLn $ knownContacts "admin user" adminUsers + putStrLn $ case ownersGroup of + Nothing -> "No owner's group" + Just KnownGroup {groupId, localDisplayName = n} -> "Owners' group: " <> knownName groupId n pure opts - -directoryService :: DirectoryStore -> DirectoryOpts -> User -> ChatController -> IO () -directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testing} user@User {userId} cc = do - initializeBotAddress' (not testing) cc - env <- newServiceState - race_ (forever $ void getLine) . forever $ do - (_, _, resp) <- atomically . readTBQueue $ outputQ cc - forM_ (crDirectoryEvent resp) $ \case - DEContactConnected ct -> deContactConnected ct - DEGroupInvitation {contact = ct, groupInfo = g, fromMemberRole, memberRole} -> deGroupInvitation ct g fromMemberRole memberRole - DEServiceJoinedGroup ctId g owner -> deServiceJoinedGroup ctId g owner - DEGroupUpdated {contactId, fromGroup, toGroup} -> deGroupUpdated contactId fromGroup toGroup - DEContactRoleChanged g ctId role -> deContactRoleChanged g ctId role - DEServiceRoleChanged g role -> deServiceRoleChanged g role - DEContactRemovedFromGroup ctId g -> deContactRemovedFromGroup ctId g - DEContactLeftGroup ctId g -> deContactLeftGroup ctId g - DEServiceRemovedFromGroup g -> deServiceRemovedFromGroup g - DEGroupDeleted _g -> pure () - DEUnsupportedMessage _ct _ciId -> pure () - DEItemEditIgnored _ct -> pure () - DEItemDeleteIgnored _ct -> pure () - DEContactCommand ct ciId aCmd -> case aCmd of - ADC SDRUser cmd -> deUserCommand env ct ciId cmd - ADC SDRSuperUser cmd -> deSuperUserCommand ct ciId cmd where + knownContacts userType = \case + [] -> "No " <> userType <> "s" + cts -> show (length cts) <> " " <> userType <> "(s): " <> intercalate ", " (map knownContact cts) + knownContact KnownContact {contactId, localDisplayName = n} = knownName contactId n + knownName i n = show i <> ":" <> T.unpack (viewName n) + +directoryServiceCLI :: DirectoryStore -> DirectoryOpts -> IO () +directoryServiceCLI st opts = do + env <- newServiceState opts + eventQ <- newTQueueIO + let eventHook cc resp = atomically $ resp <$ writeTQueue eventQ (cc, resp) + chatHooks = defaultChatHooks {eventHook = Just eventHook, acceptMember = Just $ acceptMemberHook opts env} + race_ + (simplexChatCLI' terminalChatConfig {chatHooks} (mkChatOpts opts) Nothing) + (processEvents eventQ env) + where + processEvents eventQ env = forever $ do + (cc, resp) <- atomically $ readTQueue eventQ + u_ <- readTVarIO (currentUser cc) + forM_ u_ $ \user -> directoryServiceEvent st opts env user cc resp + +directoryService :: DirectoryStore -> DirectoryOpts -> ServiceState -> User -> ChatController -> IO () +directoryService st opts@DirectoryOpts {testing} env user cc = do + initializeBotAddress' (not testing) cc + race_ (forever $ void getLine) . forever $ do + (_, resp) <- atomically . readTBQueue $ outputQ cc + directoryServiceEvent st opts env user cc resp + +acceptMemberHook :: DirectoryOpts -> ServiceState -> GroupInfo -> GroupLinkInfo -> Profile -> IO (Either GroupRejectionReason (GroupAcceptance, GroupMemberRole)) +acceptMemberHook + DirectoryOpts {profileNameLimit} + ServiceState {blockedWordsCfg} + g + GroupLinkInfo {memberRole} + Profile {displayName, image = img} = runExceptT $ do + let a = groupMemberAcceptance g + when (useMemberFilter img $ rejectNames a) checkName + pure $ + if + | useMemberFilter img (passCaptcha a) -> (GAPending, GRMember) + | useMemberFilter img (makeObserver a) -> (GAAccepted, GRObserver) + | otherwise -> (GAAccepted, memberRole) + where + checkName :: ExceptT GroupRejectionReason IO () + checkName + | T.length displayName > profileNameLimit = throwError GRRLongName + | otherwise = do + when (hasBlockedFragments blockedWordsCfg displayName) $ throwError GRRBlockedName + when (hasBlockedWords blockedWordsCfg displayName) $ throwError GRRBlockedName + +groupMemberAcceptance :: GroupInfo -> DirectoryMemberAcceptance +groupMemberAcceptance GroupInfo {customData} = memberAcceptance $ fromCustomData customData + +useMemberFilter :: Maybe ImageData -> Maybe ProfileCondition -> Bool +useMemberFilter img_ = \case + Just PCAll -> True + Just PCNoImage -> maybe True (\(ImageData i) -> i == "") img_ + Nothing -> False + +readBlockedWordsConfig :: DirectoryOpts -> IO BlockedWordsConfig +readBlockedWordsConfig DirectoryOpts {blockedFragmentsFile, blockedWordsFile, nameSpellingFile, blockedExtensionRules, testing} = do + extensionRules <- maybe (pure []) (fmap read . readFile) blockedExtensionRules + spelling <- maybe (pure M.empty) (fmap (M.fromList . read) . readFile) nameSpellingFile + blockedFragments <- S.fromList <$> maybe (pure []) (fmap T.lines . T.readFile) blockedFragmentsFile + bws <- maybe (pure []) (fmap lines . readFile) blockedWordsFile + let blockedWords = S.fromList $ concatMap (wordVariants extensionRules) bws + 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 :: DirectoryStore -> 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 + 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 + DEPendingMember g m -> dePendingMember g m + DEPendingMemberMsg g m ciId t -> dePendingMemberMsg g m ciId t + DEContactRoleChanged g ctId role -> deContactRoleChanged g ctId role + DEServiceRoleChanged g role -> deServiceRoleChanged g role + DEContactRemovedFromGroup ctId g -> deContactRemovedFromGroup ctId g + DEContactLeftGroup ctId g -> deContactLeftGroup ctId g + DEServiceRemovedFromGroup g -> deServiceRemovedFromGroup g + DEGroupDeleted g -> deGroupDeleted g + DEUnsupportedMessage _ct _ciId -> pure () + DEItemEditIgnored _ct -> pure () + DEItemDeleteIgnored _ct -> pure () + DEContactCommand ct ciId (ADC sUser cmd) -> do + logInfo $ "command received " <> directoryCmdTag cmd + case sUser of + SDRUser -> deUserCommand ct ciId cmd + SDRAdmin -> deAdminCommand ct ciId cmd + SDRSuperUser -> deSuperUserCommand ct ciId cmd + DELogChatResponse r -> logInfo r + where + withAdminUsers action = void . forkIO $ do + forM_ superUsers $ \KnownContact {contactId} -> action contactId + forM_ adminUsers $ \KnownContact {contactId} -> action contactId withSuperUsers action = void . forkIO $ forM_ superUsers $ \KnownContact {contactId} -> action contactId - notifySuperUsers s = withSuperUsers $ \contactId -> sendMessage' cc contactId s + notifyAdminUsers s = withAdminUsers $ \contactId -> sendMessage' cc contactId s notifyOwner GroupReg {dbContactId} = sendMessage' cc dbContactId ctId `isOwner` GroupReg {dbContactId} = ctId == dbContactId withGroupReg GroupInfo {groupId, localDisplayName} err action = do - atomically (getGroupReg st groupId) >>= \case + getGroupReg st groupId >>= \case Just gr -> action gr - Nothing -> putStrLn $ T.unpack $ "Error: " <> err <> ", group: " <> localDisplayName <> ", can't find group registration ID " <> tshow groupId + Nothing -> logError $ "Error: " <> err <> ", group: " <> localDisplayName <> ", can't find group registration ID " <> tshow groupId groupInfoText GroupProfile {displayName = n, fullName = fn, description = d} = n <> (if n == fn || T.null fn then "" else " (" <> fn <> ")") <> maybe "" ("\nWelcome message:\n" <>) d userGroupReference gr GroupInfo {groupProfile = GroupProfile {displayName}} = userGroupReference' gr displayName userGroupReference' GroupReg {userGroupRegId} displayName = groupReference' userGroupRegId displayName groupReference GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = groupReference' groupId displayName - groupReference' groupId displayName = "ID " <> show groupId <> " (" <> T.unpack displayName <> ")" + groupReference' groupId displayName = "ID " <> tshow groupId <> " (" <> displayName <> ")" groupAlreadyListed GroupInfo {groupProfile = GroupProfile {displayName, fullName}} = - T.unpack $ "The group " <> displayName <> " (" <> fullName <> ") is already listed in the directory, please choose another name." + "The group " <> displayName <> " (" <> fullName <> ") is already listed in the directory, please choose another name." getGroups :: Text -> IO (Maybe [(GroupInfo, GroupSummary)]) getGroups = getGroups_ . Just @@ -122,63 +249,73 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi getGroups_ :: Maybe Text -> IO (Maybe [(GroupInfo, GroupSummary)]) getGroups_ search_ = sendChatCmd cc (APIListGroups userId Nothing $ T.unpack <$> search_) >>= \case - CRGroupsList {groups} -> pure $ Just groups + Right CRGroupsList {groups} -> pure $ Just groups _ -> pure Nothing getDuplicateGroup :: GroupInfo -> IO (Maybe DuplicateGroup) getDuplicateGroup GroupInfo {groupId, groupProfile = GroupProfile {displayName, fullName}} = getGroups fullName >>= mapM duplicateGroup where - sameGroup (GroupInfo {groupId = gId, groupProfile = GroupProfile {displayName = n, fullName = fn}}, _) = - gId /= groupId && n == displayName && fn == fullName + sameGroupNotRemoved (g@GroupInfo {groupId = gId, groupProfile = GroupProfile {displayName = n, fullName = fn}}, _) = + gId /= groupId && n == displayName && fn == fullName && not (memberRemoved $ membership g) duplicateGroup [] = pure DGUnique duplicateGroup groups = do - let gs = filter sameGroup groups + let gs = filter sameGroupNotRemoved groups if null gs then pure DGUnique else do (lgs, rgs) <- atomically $ (,) <$> readTVar (listedGroups st) <*> readTVar (reservedGroups st) let reserved = any (\(GroupInfo {groupId = gId}, _) -> gId `S.member` lgs || gId `S.member` rgs) gs - pure $ if reserved then DGReserved else DGRegistered + if reserved + then pure DGReserved + else do + removed <- foldM (\r -> fmap (r &&) . isGroupRemoved) True gs + pure $ if removed then DGUnique else DGRegistered + isGroupRemoved (GroupInfo {groupId = gId}, _) = + getGroupReg st gId >>= \case + Just GroupReg {groupRegStatus} -> groupRemoved <$> readTVarIO groupRegStatus + Nothing -> pure True processInvitation :: Contact -> GroupInfo -> IO () processInvitation ct g@GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = do void $ addGroupReg st ct g GRSProposed - r <- sendChatCmd cc $ APIJoinGroup groupId - sendMessage cc ct $ T.unpack $ case r of - CRUserAcceptedGroupSent {} -> "Joining the group " <> displayName <> "…" + r <- sendChatCmd cc $ APIJoinGroup groupId MFNone + sendMessage cc ct $ case r of + Right CRUserAcceptedGroupSent {} -> "Joining the group " <> displayName <> "…" _ -> "Error joining group " <> displayName <> ", please re-send the invitation!" deContactConnected :: Contact -> IO () deContactConnected ct = when (contactDirect ct) $ do - unless testing $ putStrLn $ T.unpack (localDisplayName' ct) <> " connected" + logInfo $ (viewContactName ct) <> " connected" sendMessage cc ct $ - "Welcome to " <> serviceName <> " service!\n\ - \Send a search string to find groups or */help* to learn how to add groups to directory.\n\n\ - \For example, send _privacy_ to find groups about privacy.\n\ - \Or send */all* or */new* to list groups.\n\n\ - \Content and privacy policy: https://simplex.chat/docs/directory.html" + ("Welcome to " <> serviceName <> " service!\n") + <> "Send a search string to find groups or */help* to learn how to add groups to directory.\n\n\ + \For example, send _privacy_ to find groups about privacy.\n\ + \Or send */all* or */new* to list groups.\n\n\ + \Content and privacy policy: https://simplex.chat/docs/directory.html" deGroupInvitation :: Contact -> GroupInfo -> GroupMemberRole -> GroupMemberRole -> IO () deGroupInvitation ct g@GroupInfo {groupProfile = GroupProfile {displayName, fullName}} fromMemberRole memberRole = do + logInfo $ "invited to group " <> viewGroupName g <> " by " <> viewContactName ct case badRolesMsg $ groupRolesStatus fromMemberRole memberRole of Just msg -> sendMessage cc ct msg - Nothing -> getDuplicateGroup g >>= \case - Just DGUnique -> processInvitation ct g - Just DGRegistered -> askConfirmation - Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g - Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers." + Nothing -> + getDuplicateGroup g >>= \case + Just DGUnique -> processInvitation ct g + Just DGRegistered -> askConfirmation + Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g + Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers." where askConfirmation = do ugrId <- addGroupReg st ct g GRSPendingConfirmation - sendMessage cc ct $ T.unpack $ "The group " <> displayName <> " (" <> fullName <> ") is already submitted to the directory.\nTo confirm the registration, please send:" - sendMessage cc ct $ "/confirm " <> show ugrId <> ":" <> viewName (T.unpack displayName) + sendMessage cc ct $ "The group " <> displayName <> " (" <> fullName <> ") is already submitted to the directory.\nTo confirm the registration, please send:" + sendMessage cc ct $ "/confirm " <> tshow ugrId <> ":" <> viewName displayName - badRolesMsg :: GroupRolesStatus -> Maybe String + badRolesMsg :: GroupRolesStatus -> Maybe Text badRolesMsg = \case GRSOk -> Nothing - GRSServiceNotAdmin -> Just "You must have a group *owner* role to register the group" - GRSContactNotOwner -> Just "You must grant directory service *admin* role to register the group" + GRSServiceNotAdmin -> Just "You must grant directory service *admin* role to register the group" + GRSContactNotOwner -> Just "You must have a group *owner* role to register the group" GRSBadRoles -> Just "You must have a group *owner* role and you must grant directory service *admin* role to register the group" getGroupRolesStatus :: GroupInfo -> GroupReg -> IO (Maybe GroupRolesStatus) @@ -190,8 +327,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi groupRolesStatus :: GroupMemberRole -> GroupMemberRole -> GroupRolesStatus groupRolesStatus contactRole serviceRole = case (contactRole, serviceRole) of (GROwner, GRAdmin) -> GRSOk - (_, GRAdmin) -> GRSServiceNotAdmin - (GROwner, _) -> GRSContactNotOwner + (_, GRAdmin) -> GRSContactNotOwner + (GROwner, _) -> GRSServiceNotAdmin _ -> GRSBadRoles getGroupMember :: GroupReg -> IO (Maybe GroupMember) @@ -200,25 +337,27 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi $>>= \mId -> resp <$> sendChatCmd cc (APIGroupMemberInfo dbGroupId mId) where resp = \case - CRGroupMemberInfo {member} -> Just member + Right CRGroupMemberInfo {member} -> Just member _ -> Nothing deServiceJoinedGroup :: ContactId -> GroupInfo -> GroupMember -> IO () - deServiceJoinedGroup ctId g owner = + deServiceJoinedGroup ctId g owner = do + logInfo $ "service joined group " <> viewGroupName g withGroupReg g "joined group" $ \gr -> when (ctId `isOwner` gr) $ do setGroupRegOwner st gr owner let GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = g - notifyOwner gr $ T.unpack $ "Joined the group " <> displayName <> ", creating the link…" - sendChatCmd cc (APICreateGroupLink groupId GRMember) >>= \case - CRGroupLinkCreated {connReqContact} -> do + notifyOwner gr $ "Joined the group " <> displayName <> ", creating the link…" + sendChatCmd cc (APICreateGroupLink groupId GRMember False) >>= \case + Right CRGroupLinkCreated {connLinkContact = CCLink gLink _} -> do setGroupStatus st gr GRSPendingUpdate - notifyOwner gr + notifyOwner + gr "Created the public link to join the group via this directory service that is always online.\n\n\ \Please add it to the group welcome message.\n\ \For example, add:" - notifyOwner gr $ "Link to join the group " <> T.unpack displayName <> ": " <> B.unpack (strEncode $ simplexChatContact connReqContact) - CRChatCmdError _ (ChatError e) -> case e of + notifyOwner gr $ "Link to join the group " <> displayName <> ": " <> strEncodeTxt (simplexChatContact gLink) + Left (ChatError e) -> case e of CEGroupUserRole {} -> notifyOwner gr "Failed creating group link, as service is no longer an admin." CEGroupMemberUserRemoved -> notifyOwner gr "Failed creating group link, as service is removed from the group." CEGroupNotJoined _ -> notifyOwner gr $ unexpectedError "group not joined" @@ -226,77 +365,98 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi _ -> notifyOwner gr $ unexpectedError "can't create group link" _ -> notifyOwner gr $ unexpectedError "can't create group link" - deGroupUpdated :: ContactId -> GroupInfo -> GroupInfo -> IO () - deGroupUpdated ctId fromGroup toGroup = + deGroupUpdated :: GroupMember -> GroupInfo -> GroupInfo -> IO () + deGroupUpdated m@GroupMember {memberProfile = LocalProfile {displayName = mName}} fromGroup toGroup = do + logInfo $ "group updated " <> viewGroupName toGroup unless (sameProfile p p') $ do withGroupReg toGroup "group updated" $ \gr -> do let userGroupRef = userGroupReference gr toGroup + 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. readTVarIO (groupRegStatus gr) >>= \case GRSPendingConfirmation -> pure () GRSProposed -> pure () - GRSPendingUpdate -> groupProfileUpdate >>= \case - GPNoServiceLink -> - when (ctId `isOwner` gr) $ notifyOwner gr $ "The profile updated for " <> userGroupRef <> ", but the group link is not added to the welcome message." - GPServiceLinkAdded - | ctId `isOwner` gr -> groupLinkAdded gr - | otherwise -> notifyOwner gr "The group link is added by another group member, your registration will not be processed.\n\nPlease update the group profile yourself." - GPServiceLinkRemoved -> when (ctId `isOwner` gr) $ notifyOwner gr $ "The group link of " <> userGroupRef <> " is removed from the welcome message, please add it." - GPHasServiceLink -> when (ctId `isOwner` gr) $ groupLinkAdded gr - GPServiceLinkError -> do - when (ctId `isOwner` gr) $ notifyOwner gr $ "Error: " <> serviceName <> " has no group link for " <> userGroupRef <> ". Please report the error to the developers." - putStrLn $ "Error: no group link for " <> userGroupRef - GRSPendingApproval n -> processProfileChange gr $ n + 1 - GRSActive -> processProfileChange gr 1 - GRSSuspended -> processProfileChange gr 1 - GRSSuspendedBadRoles -> processProfileChange gr 1 + 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 $ n + 1 + GRSActive -> processProfileChange gr byMember 1 + GRSSuspended -> processProfileChange gr byMember 1 + GRSSuspendedBadRoles -> processProfileChange gr byMember 1 GRSRemoved -> pure () where - isInfix l d_ = l `T.isInfixOf` fromMaybe "" d_ GroupInfo {groupId, groupProfile = p} = fromGroup GroupInfo {groupProfile = p'} = toGroup sameProfile GroupProfile {displayName = n, fullName = fn, image = i, description = d} GroupProfile {displayName = n', fullName = fn', image = i', description = d'} = n == n' && fn == fn' && i == i' && d == d' - groupLinkAdded gr = do + groupLinkAdded gr byMember = do getDuplicateGroup toGroup >>= \case Nothing -> notifyOwner gr "Error: getDuplicateGroup. Please notify the developers." Just DGReserved -> notifyOwner gr $ groupAlreadyListed toGroup _ -> do let gaId = 1 setGroupStatus st gr $ GRSPendingApproval gaId - notifyOwner gr $ "Thank you! The group link for " <> userGroupReference gr toGroup <> " is added to the welcome message.\nYou will be notified once the group is added to the directory - it may take up to 24 hours." + notifyOwner gr $ + ("Thank you! The group link for " <> userGroupReference gr toGroup <> " is added to the welcome message" <> byMember) + <> ".\nYou will be notified once the group is added to the directory - it may take up to 48 hours." checkRolesSendToApprove gr gaId - processProfileChange gr n' = do + processProfileChange gr byMember n' = do setGroupStatus st gr GRSPendingUpdate let userGroupRef = userGroupReference gr toGroup groupRef = groupReference toGroup groupProfileUpdate >>= \case GPNoServiceLink -> do - notifyOwner gr $ "The group profile is updated " <> userGroupRef <> ", but no link is added to the welcome message.\n\nThe group will remain hidden from the directory until the group link is added and the group is re-approved." + notifyOwner gr $ + ("The group profile is updated for " <> userGroupRef <> byMember <> ", but no link is added to the welcome message.\n\n") + <> "The group will remain hidden from the directory until the group link is added and the group is re-approved." GPServiceLinkRemoved -> do - notifyOwner gr $ "The group link for " <> userGroupRef <> " is removed from the welcome message.\n\nThe group is hidden from the directory until the group link is added and the group is re-approved." - notifySuperUsers $ "The group link is removed from " <> groupRef <> ", de-listed." + notifyOwner gr $ + ("The group link for " <> userGroupRef <> " is removed from the welcome message" <> byMember) + <> ".\n\nThe group is hidden from the directory until the group link is added and the group is re-approved." + notifyAdminUsers $ "The group link is removed from " <> groupRef <> ", de-listed." GPServiceLinkAdded -> do setGroupStatus st gr $ GRSPendingApproval n' - notifyOwner gr $ "The group link is added to " <> userGroupRef <> "!\nIt is hidden from the directory until approved." - notifySuperUsers $ "The group link is added to " <> groupRef <> "." + notifyOwner gr $ + ("The group link is added to " <> userGroupRef <> byMember) + <> "!\nIt is hidden from the directory until approved." + notifyAdminUsers $ "The group link is added to " <> groupRef <> byMember <> "." checkRolesSendToApprove gr n' GPHasServiceLink -> do setGroupStatus st gr $ GRSPendingApproval n' - notifyOwner gr $ "The group " <> userGroupRef <> " is updated!\nIt is hidden from the directory until approved." - notifySuperUsers $ "The group " <> groupRef <> " is updated." + notifyOwner gr $ + ("The group " <> userGroupRef <> " is updated" <> byMember) + <> "!\nIt is hidden from the directory until approved." + notifyAdminUsers $ "The group " <> groupRef <> " is updated" <> byMember <> "." checkRolesSendToApprove gr n' - GPServiceLinkError -> putStrLn $ "Error: no group link for " <> groupRef <> " pending approval." + GPServiceLinkError -> logError $ "Error: no group link for " <> groupRef <> " pending approval." groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId) where profileUpdate = \case - CRGroupLink {connReqContact} -> - let groupLink1 = safeDecodeUtf8 $ strEncode connReqContact - groupLink2 = safeDecodeUtf8 $ strEncode $ simplexChatContact connReqContact - hadLinkBefore = groupLink1 `isInfix` description p || groupLink2 `isInfix` description p - hasLinkNow = groupLink1 `isInfix` description p' || groupLink2 `isInfix` description p' - in if + Right CRGroupLink {connLinkContact = CCLink cr sl_} -> + let hadLinkBefore = profileHasGroupLink fromGroup + hasLinkNow = profileHasGroupLink toGroup + profileHasGroupLink GroupInfo {groupProfile = gp} = + maybe False (any ftHasLink) $ parseMaybeMarkdownList =<< description gp + ftHasLink = \case + FormattedText (Just SimplexLink {simplexUri = ACL SCMContact cLink}) _ -> case cLink of + CLFull cr' -> sameConnReqContact cr' cr + CLShort sl' -> maybe False (sameShortLinkContact sl') sl_ + _ -> False + in if | hadLinkBefore && hasLinkNow -> GPHasServiceLink | hadLinkBefore -> GPServiceLinkRemoved | hasLinkNow -> GPServiceLinkAdded @@ -308,20 +468,97 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi Just (Just msg) -> notifyOwner gr msg Just Nothing -> sendToApprove toGroup gr gaId + dePendingMember :: GroupInfo -> GroupMember -> IO () + dePendingMember g@GroupInfo {groupProfile = GroupProfile {displayName}} m + | memberRequiresCaptcha a m = sendMemberCaptcha g m Nothing captchaNotice 0 + | otherwise = approvePendingMember a g m + where + a = groupMemberAcceptance g + captchaNotice = "Captcha is generated by SimpleX Directory service.\n\n*Send captcha text* to join the group " <> displayName <> "." + + sendMemberCaptcha :: GroupInfo -> GroupMember -> Maybe ChatItemId -> Text -> Int -> IO () + sendMemberCaptcha GroupInfo {groupId} m quotedId noticeText prevAttempts = do + s <- getCaptchaStr captchaLength "" + mc <- getCaptcha s + sentAt <- getCurrentTime + let captcha = PendingCaptcha {captchaText = T.pack s, sentAt, attempts = prevAttempts + 1} + atomically $ TM.insert gmId captcha $ pendingCaptchas env + sendCaptcha mc + where + getCaptcha s = case captchaGenerator opts of + Nothing -> pure textMsg + Just script -> content <$> readProcess script [s] "" + where + textMsg = MCText $ T.pack s + content r = case T.lines $ T.pack r of + [] -> textMsg + "" : _ -> textMsg + img : _ -> MCImage "" $ ImageData img + sendCaptcha mc = sendComposedMessages_ cc (SRGroup groupId $ Just gmId) [(quotedId, MCText noticeText), (Nothing, mc)] + gmId = groupMemberId' m + + approvePendingMember :: DirectoryMemberAcceptance -> GroupInfo -> GroupMember -> IO () + approvePendingMember a g@GroupInfo {groupId} m@GroupMember {memberProfile = LocalProfile {displayName, image}} = do + gli_ <- join <$> withDB' "getGroupLinkInfo" cc (\db -> getGroupLinkInfo db userId groupId) + let role = if useMemberFilter image (makeObserver a) then GRObserver else maybe GRMember (\GroupLinkInfo {memberRole} -> memberRole) gli_ + gmId = groupMemberId' m + sendChatCmd cc (APIAcceptMember groupId gmId role) >>= \case + Right CRJoinedGroupMember {} -> do + atomically $ TM.delete gmId $ pendingCaptchas env + logInfo $ "Member " <> viewName displayName <> " accepted, group " <> tshow groupId <> ":" <> viewGroupName g + r -> logError $ "unexpected accept member response: " <> tshow r + + dePendingMemberMsg :: GroupInfo -> GroupMember -> ChatItemId -> Text -> IO () + dePendingMemberMsg g@GroupInfo {groupId, groupProfile = GroupProfile {displayName = n}} m@GroupMember {memberProfile = LocalProfile {displayName}} ciId msgText + | memberRequiresCaptcha a m = do + ts <- getCurrentTime + atomically (TM.lookup (groupMemberId' m) $ pendingCaptchas env) >>= \case + Just PendingCaptcha {captchaText, sentAt, attempts} + | ts `diffUTCTime` sentAt > captchaTTL -> sendMemberCaptcha g m (Just ciId) captchaExpired $ attempts - 1 + | matchCaptchaStr captchaText msgText -> do + sendComposedMessages_ cc (SRGroup groupId $ Just $ groupMemberId' m) [(Just ciId, MCText $ "Correct, you joined the group " <> n)] + approvePendingMember a g m + | attempts >= maxCaptchaAttempts -> rejectPendingMember tooManyAttempts + | otherwise -> sendMemberCaptcha g m (Just ciId) (wrongCaptcha attempts) attempts + Nothing -> sendMemberCaptcha g m (Just ciId) noCaptcha 0 + | otherwise = approvePendingMember a g m + where + a = groupMemberAcceptance g + rejectPendingMember rjctNotice = do + let gmId = groupMemberId' m + sendComposedMessages cc (SRGroup groupId $ Just gmId) [MCText rjctNotice] + sendChatCmd cc (APIRemoveMembers groupId [gmId] False) >>= \case + Right (CRUserDeletedMembers _ _ (_ : _) _) -> do + atomically $ TM.delete gmId $ pendingCaptchas env + logInfo $ "Member " <> viewName displayName <> " rejected, group " <> tshow groupId <> ":" <> viewGroupName g + r -> logError $ "unexpected remove member response: " <> tshow r + captchaExpired = "Captcha expired, please try again." + wrongCaptcha attempts + | attempts == maxCaptchaAttempts - 1 = "Incorrect text, please try again - this is your last attempt." + | otherwise = "Incorrect text, please try again." + noCaptcha = "Unexpected message, please try again." + tooManyAttempts = "Too many failed attempts, you can't join group." + + memberRequiresCaptcha :: DirectoryMemberAcceptance -> GroupMember -> Bool + memberRequiresCaptcha a GroupMember {memberProfile = LocalProfile {image}} = + useMemberFilter image $ passCaptcha a + sendToApprove :: GroupInfo -> GroupReg -> GroupApprovalId -> IO () sendToApprove GroupInfo {groupProfile = p@GroupProfile {displayName, image = image'}} GroupReg {dbGroupId, dbContactId} gaId = do - ct_ <- getContact cc dbContactId - gr_ <- getGroupAndSummary cc dbGroupId + ct_ <- getContact' cc user dbContactId + gr_ <- getGroupAndSummary cc user dbGroupId let membersStr = maybe "" (\(_, s) -> "_" <> tshow (currentMembers s) <> " members_\n") gr_ - text = maybe ("The group ID " <> tshow dbGroupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the group ID " <> tshow dbGroupId <> ": ") ct_ - <> "\n" <> groupInfoText p <> "\n" <> membersStr <> "\nTo approve send:" + text = + maybe ("The group ID " <> tshow dbGroupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the group ID " <> tshow dbGroupId <> ": ") ct_ + <> ("\n" <> groupInfoText p <> "\n" <> membersStr <> "\nTo approve send:") msg = maybe (MCText text) (\image -> MCImage {text, image}) image' - withSuperUsers $ \cId -> do + withAdminUsers $ \cId -> do sendComposedMessage' cc cId Nothing msg - sendMessage' cc cId $ "/approve " <> show dbGroupId <> ":" <> viewName (T.unpack displayName) <> " " <> show gaId + sendMessage' cc cId $ "/approve " <> tshow dbGroupId <> ":" <> viewName displayName <> " " <> tshow gaId deContactRoleChanged :: GroupInfo -> ContactId -> GroupMemberRole -> IO () - deContactRoleChanged g@GroupInfo {membership = GroupMember {memberRole = serviceRole}} ctId contactRole = + deContactRoleChanged g@GroupInfo {membership = GroupMember {memberRole = serviceRole}} ctId contactRole = do + logInfo $ "contact ID " <> tshow ctId <> " role changed in group " <> viewGroupName g <> " to " <> tshow contactRole withGroupReg g "contact role changed" $ \gr -> do let userGroupRef = userGroupReference gr g uCtRole = "Your role in the group " <> userGroupRef <> " is changed to " <> ctRole @@ -330,23 +567,24 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi GRSSuspendedBadRoles -> when (rStatus == GRSOk) $ do setGroupStatus st gr GRSActive notifyOwner gr $ uCtRole <> ".\n\nThe group is listed in the directory again." - notifySuperUsers $ "The group " <> groupRef <> " is listed " <> suCtRole + notifyAdminUsers $ "The group " <> groupRef <> " is listed " <> suCtRole GRSPendingApproval gaId -> when (rStatus == GRSOk) $ do sendToApprove g gr gaId notifyOwner gr $ uCtRole <> ".\n\nThe group is submitted for approval." GRSActive -> when (rStatus /= GRSOk) $ do setGroupStatus st gr GRSSuspendedBadRoles notifyOwner gr $ uCtRole <> ".\n\nThe group is no longer listed in the directory." - notifySuperUsers $ "The group " <> groupRef <> " is de-listed " <> suCtRole + notifyAdminUsers $ "The group " <> groupRef <> " is de-listed " <> suCtRole _ -> pure () where rStatus = groupRolesStatus contactRole serviceRole groupRef = groupReference g - ctRole = "*" <> B.unpack (strEncode contactRole) <> "*" + ctRole = "*" <> strEncodeTxt contactRole <> "*" suCtRole = "(user role is set to " <> ctRole <> ")." deServiceRoleChanged :: GroupInfo -> GroupMemberRole -> IO () deServiceRoleChanged g serviceRole = do + logInfo $ "service role changed in group " <> viewGroupName g <> " to " <> tshow serviceRole withGroupReg g "service role changed" $ \gr -> do let userGroupRef = userGroupReference gr g uSrvRole = serviceName <> " role in the group " <> userGroupRef <> " is changed to " <> srvRole @@ -355,7 +593,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi whenContactIsOwner gr $ do setGroupStatus st gr GRSActive notifyOwner gr $ uSrvRole <> ".\n\nThe group is listed in the directory again." - notifySuperUsers $ "The group " <> groupRef <> " is listed " <> suSrvRole + notifyAdminUsers $ "The group " <> groupRef <> " is listed " <> suSrvRole GRSPendingApproval gaId -> when (serviceRole == GRAdmin) $ whenContactIsOwner gr $ do sendToApprove g gr gaId @@ -363,49 +601,73 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi GRSActive -> when (serviceRole /= GRAdmin) $ do setGroupStatus st gr GRSSuspendedBadRoles notifyOwner gr $ uSrvRole <> ".\n\nThe group is no longer listed in the directory." - notifySuperUsers $ "The group " <> groupRef <> " is de-listed " <> suSrvRole + notifyAdminUsers $ "The group " <> groupRef <> " is de-listed " <> suSrvRole _ -> pure () where groupRef = groupReference g - srvRole = "*" <> B.unpack (strEncode serviceRole) <> "*" + srvRole = "*" <> strEncodeTxt serviceRole <> "*" suSrvRole = "(" <> serviceName <> " role is changed to " <> srvRole <> ")." whenContactIsOwner gr action = - getGroupMember gr >>= - mapM_ (\cm@GroupMember {memberRole} -> when (memberRole == GROwner && memberActive cm) action) + getGroupMember gr + >>= mapM_ (\cm@GroupMember {memberRole} -> when (memberRole == GROwner && memberActive cm) action) deContactRemovedFromGroup :: ContactId -> GroupInfo -> IO () - deContactRemovedFromGroup ctId g = + deContactRemovedFromGroup ctId g = do + logInfo $ "contact ID " <> tshow ctId <> " removed from group " <> viewGroupName g withGroupReg g "contact removed" $ \gr -> do when (ctId `isOwner` gr) $ do setGroupStatus st gr GRSRemoved notifyOwner gr $ "You are removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." - notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (group owner is removed)." + notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group owner is removed)." deContactLeftGroup :: ContactId -> GroupInfo -> IO () - deContactLeftGroup ctId g = + deContactLeftGroup ctId g = do + logInfo $ "contact ID " <> tshow ctId <> " left group " <> viewGroupName g withGroupReg g "contact left" $ \gr -> do when (ctId `isOwner` gr) $ do setGroupStatus st gr GRSRemoved notifyOwner gr $ "You left the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." - notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (group owner left)." + notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group owner left)." deServiceRemovedFromGroup :: GroupInfo -> IO () - deServiceRemovedFromGroup g = + deServiceRemovedFromGroup g = do + logInfo $ "service removed from group " <> viewGroupName g withGroupReg g "service removed" $ \gr -> do setGroupStatus st gr GRSRemoved notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." - notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (directory service is removed)." + notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (directory service is removed)." - deUserCommand :: ServiceState -> Contact -> ChatItemId -> DirectoryCmd 'DRUser -> IO () - deUserCommand env@ServiceState {searchRequests} ct ciId = \case - DCHelp -> + deGroupDeleted :: GroupInfo -> IO () + deGroupDeleted g = do + logInfo $ "group removed " <> viewGroupName g + withGroupReg g "group removed" $ \gr -> do + setGroupStatus st gr GRSRemoved + 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)." + + deUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRUser -> IO () + deUserCommand ct ciId = \case + DCHelp DHSRegistration -> sendMessage cc ct $ "You must be the owner to add the group to the directory:\n\ - \1. Invite " <> serviceName <> " bot to your group as *admin* (you can send `/list` to see all groups you submitted).\n\ - \2. " <> serviceName <> " bot will create a public group link for the new members to join even when you are offline.\n\ - \3. You will then need to add this link to the group welcome message.\n\ - \4. Once the link is added, service admins will approve the group (it can take up to 24 hours), and everybody will be able to find it in directory.\n\n\ - \Start from inviting the bot to your group as admin - it will guide you through the process" + \1. Invite " + <> serviceName + <> " bot to your group as *admin* (you can send `/list` to see all groups you submitted).\n\ + \2. " + <> serviceName + <> " bot will create a public group link for the new members to join even when you are offline.\n\ + \3. You will then need to add this link to the group welcome message.\n\ + \4. Once the link is added, service admins will approve the group (it can take up to 48 hours), and everybody will be able to find it in directory.\n\n\ + \Start from inviting the bot to your group as admin - it will guide you through the process." + DCHelp DHSCommands -> + sendMessage cc ct $ + "*/help commands* - receive this help message.\n\ + \*/help* - how to register your group to be added to directory.\n\ + \*/list* - list the groups you registered.\n\ + \*/delete <ID>:<NAME>* - remove the group you submitted from directory, with _ID_ and _name_ as shown by */list* command.\n\ + \*/role <ID>* - view and set default member role for your group.\n\ + \*/filter <ID>* - view and set spam filter settings for group.\n\n\ + \To search for groups, send the search text." DCSearchGroup s -> withFoundListedGroups (Just s) $ sendSearchResults s DCSearchNext -> atomically (TM.lookup (contactId' ct) searchRequests) >>= \case @@ -421,58 +683,114 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi STRecent -> withFoundListedGroups Nothing $ sendNextSearchResults takeRecent search Nothing -> showAllGroups where - showAllGroups = deUserCommand env ct ciId DCAllGroups + showAllGroups = deUserCommand ct ciId DCAllGroups DCAllGroups -> withFoundListedGroups Nothing $ sendAllGroups takeTop "top" STAll DCRecentGroups -> withFoundListedGroups Nothing $ sendAllGroups takeRecent "the most recent" STRecent DCSubmitGroup _link -> pure () DCConfirmDuplicateGroup ugrId gName -> - atomically (getUserGroupReg st (contactId' ct) ugrId) >>= \case - Nothing -> sendReply $ "Group ID " <> show ugrId <> " not found" - Just GroupReg {dbGroupId, groupRegStatus} -> do - getGroup cc dbGroupId >>= \case - Nothing -> sendReply $ "Group ID " <> show ugrId <> " not found" - Just g@GroupInfo {groupProfile = GroupProfile {displayName}} - | displayName == gName -> - readTVarIO groupRegStatus >>= \case - GRSPendingConfirmation -> do - getDuplicateGroup g >>= \case - Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers." - Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g - _ -> processInvitation ct g - _ -> sendReply $ "Error: the group ID " <> show ugrId <> " (" <> T.unpack displayName <> ") is not pending confirmation." - | otherwise -> sendReply $ "Group ID " <> show ugrId <> " has the display name " <> T.unpack displayName + withUserGroupReg ugrId gName $ \g@GroupInfo {groupProfile = GroupProfile {displayName}} gr -> + readTVarIO (groupRegStatus gr) >>= \case + GRSPendingConfirmation -> + getDuplicateGroup g >>= \case + Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers." + Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g + _ -> processInvitation ct g + _ -> sendReply $ "Error: the group ID " <> tshow ugrId <> " (" <> displayName <> ") is not pending confirmation." DCListUserGroups -> - atomically (getUserGroupRegs st $ contactId' ct) >>= \grs -> do - sendReply $ show (length grs) <> " registered group(s)" + getUserGroupRegs st (contactId' ct) >>= \grs -> do + sendReply $ tshow (length grs) <> " registered group(s)" void . forkIO $ forM_ (reverse grs) $ \gr@GroupReg {userGroupRegId} -> sendGroupInfo ct gr userGroupRegId Nothing - DCDeleteGroup _ugrId _gName -> pure () + DCDeleteGroup gId gName -> + (if isAdmin then withGroupAndReg sendReply else withUserGroupReg) gId gName $ \GroupInfo {groupProfile = GroupProfile {displayName}} gr -> do + delGroupReg st gr + sendReply $ (if isAdmin then "The group " else "Your group ") <> displayName <> " is deleted from the directory" + DCMemberRole gId gName_ mRole_ -> + (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g _gr -> do + let GroupInfo {groupProfile = GroupProfile {displayName = n}} = g + case mRole_ of + Nothing -> + getGroupLinkRole cc user g >>= \case + Just (_, CCLink gLink _, mRole) -> do + let anotherRole = case mRole of GRObserver -> GRMember; _ -> GRObserver + sendReply $ + initialRole n mRole + <> ("Send */role " <> tshow gId <> " " <> strEncodeTxt anotherRole <> "* to change it.\n\n") + <> onlyViaLink gLink + Nothing -> sendReply $ "Error: failed reading the initial member role for the group " <> n + Just mRole -> do + setGroupLinkRole cc g mRole >>= \case + Just gLink -> sendReply $ initialRole n mRole <> "\n" <> onlyViaLink gLink + Nothing -> sendReply $ "Error: the initial member role for the group " <> n <> " was NOT upgated." + where + initialRole n mRole = "The initial member role for the group " <> n <> " is set to *" <> strEncodeTxt mRole <> "*\n" + onlyViaLink gLink = "*Please note*: it applies only to members joining via this link: " <> strEncodeTxt (simplexChatContact gLink) + DCGroupFilter gId gName_ acceptance_ -> + (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g _gr -> do + let GroupInfo {groupProfile = GroupProfile {displayName = n}} = g + a = groupMemberAcceptance g + case acceptance_ of + Just a' | a /= a' -> do + let d = toCustomData $ DirectoryGroupData a' + withDB' "setGroupCustomData" cc (\db -> setGroupCustomData db user g $ Just d) >>= \case + Just () -> sendSettigns n a' " set to" + Nothing -> sendReply $ "Error changing spam filter settings for group " <> n + _ -> sendSettigns n a "" + where + sendSettigns n a setTo = + sendReply $ + T.unlines + [ "Spam filter settings for group " <> n <> setTo <> ":", + "- reject long/inappropriate names: " <> showCondition (rejectNames a), + "- pass captcha to join: " <> showCondition (passCaptcha a), + -- "- make observer: " <> showCondition (makeObserver a) <> (if isJust (makeObserver a) then "" else " (use default set with /role command)"), + "", + -- "Use */filter " <> tshow gId <> " <level>* to change spam filter level: no (disable), basic, moderate, strong.", + -- "Or use */filter " <> tshow gId <> " [name[=noimage]] [captcha[=noimage]] [observer[=noimage]]* for advanced filter configuration." + "Use */filter " <> tshow gId <> " [name] [captcha]* to enable and */filter " <> tshow gId <> " off* to disable filter." + ] + showCondition = \case + Nothing -> "_disabled_" + Just PCAll -> "_enabled_" + Just PCNoImage -> "_enabled for profiles without image_" DCUnknownCommand -> sendReply "Unknown command" - DCCommandError tag -> sendReply $ "Command error: " <> show tag + DCCommandError tag -> sendReply $ "Command error: " <> tshow tag where - sendReply = sendComposedMessage cc ct (Just ciId) . textMsgContent + knownCt = knownContact ct + isAdmin = knownCt `elem` adminUsers || knownCt `elem` superUsers + withUserGroupReg ugrId = withUserGroupReg_ ugrId . Just + withUserGroupReg_ ugrId gName_ action = + getUserGroupReg st (contactId' ct) ugrId >>= \case + Nothing -> sendReply $ "Group ID " <> tshow ugrId <> " not found" + Just gr@GroupReg {dbGroupId} -> do + getGroup cc user dbGroupId >>= \case + Nothing -> sendReply $ "Group ID " <> tshow ugrId <> " not found" + Just g@GroupInfo {groupProfile = GroupProfile {displayName}} + | maybe True (displayName ==) gName_ -> action g gr + | otherwise -> sendReply $ "Group ID " <> tshow ugrId <> " has the display name " <> displayName + sendReply = mkSendReply ct ciId withFoundListedGroups s_ action = getGroups_ s_ >>= \case - Just groups -> atomically (filterListedGroups st groups) >>= action + Just groups -> filterListedGroups st groups >>= action Nothing -> sendReply "Error: getGroups. Please notify the developers." sendSearchResults s = \case [] -> sendReply "No groups found" gs -> do let gs' = takeTop searchResults gs moreGroups = length gs - length gs' - more = if moreGroups > 0 then ", sending top " <> show (length gs') else "" - sendReply $ "Found " <> show (length gs) <> " group(s)" <> more <> "." + more = if moreGroups > 0 then ", sending top " <> tshow (length gs') else "" + reply = "Found " <> tshow (length gs) <> " group(s)" <> more <> "." updateSearchRequest (STSearch s) $ groupIds gs' - sendFoundGroups gs' moreGroups + sendFoundGroups reply gs' moreGroups sendAllGroups takeFirst sortName searchType = \case [] -> sendReply "No groups listed" gs -> do let gs' = takeFirst searchResults gs moreGroups = length gs - length gs' - more = if moreGroups > 0 then ", sending " <> sortName <> " " <> show (length gs') else "" - sendReply $ show (length gs) <> " group(s) listed" <> more <> "." + more = if moreGroups > 0 then ", sending " <> sortName <> " " <> tshow (length gs') else "" + reply = tshow (length gs) <> " group(s) listed" <> more <> "." updateSearchRequest searchType $ groupIds gs' - sendFoundGroups gs' moreGroups + sendFoundGroups reply gs' moreGroups sendNextSearchResults takeFirst SearchRequest {searchType, sentGroups} = \case [] -> do sendReply "Sorry, no more groups" @@ -481,110 +799,217 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi let gs' = takeFirst searchResults $ filterNotSent sentGroups gs sentGroups' = sentGroups <> groupIds gs' moreGroups = length gs - S.size sentGroups' - sendReply $ "Sending " <> show (length gs') <> " more group(s)." + reply = "Sending " <> tshow (length gs') <> " more group(s)." updateSearchRequest searchType sentGroups' - sendFoundGroups gs' moreGroups + sendFoundGroups reply gs' moreGroups updateSearchRequest :: SearchType -> Set GroupId -> IO () updateSearchRequest searchType sentGroups = do searchTime <- getCurrentTime let search = SearchRequest {searchType, searchTime, sentGroups} atomically $ TM.insert (contactId' ct) search searchRequests - sendFoundGroups gs moreGroups = - void . forkIO $ do - forM_ gs $ - \(GroupInfo {groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do - let membersStr = "_" <> tshow currentMembers <> " members_" - text = groupInfoText p <> "\n" <> membersStr - msg = maybe (MCText text) (\image -> MCImage {text, image}) image_ - sendComposedMessage cc ct Nothing msg - when (moreGroups > 0) $ - sendComposedMessage cc ct Nothing $ MCText $ "Send */next* or just *.* for " <> tshow moreGroups <> " more result(s)." + sendFoundGroups reply gs moreGroups = + void . forkIO $ sendComposedMessages_ cc (SRDirect $ contactId' ct) msgs + where + msgs = replyMsg :| map foundGroup gs <> [moreMsg | moreGroups > 0] + replyMsg = (Just ciId, MCText reply) + foundGroup (GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) = + let membersStr = "_" <> tshow currentMembers <> " members_" + showId = if isAdmin then tshow groupId <> ". " else "" + text = showId <> groupInfoText p <> "\n" <> membersStr + in (Nothing, maybe (MCText text) (\image -> MCImage {text, image}) image_) + moreMsg = (Nothing, MCText $ "Send */next* or just *.* for " <> tshow moreGroups <> " more result(s).") - deSuperUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRSuperUser -> IO () - deSuperUserCommand ct ciId cmd - | superUser `elem` superUsers = case cmd of - DCApproveGroup {groupId, displayName = n, groupApprovalId} -> do - getGroupAndReg groupId n >>= \case - Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)." - Just (g, gr) -> + deAdminCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRAdmin -> IO () + deAdminCommand ct ciId cmd + | knownCt `elem` adminUsers || knownCt `elem` superUsers = case cmd of + DCApproveGroup {groupId, displayName = n, groupApprovalId} -> + withGroupAndReg sendReply groupId n $ \g gr@GroupReg {userGroupRegId = ugrId} -> readTVarIO (groupRegStatus gr) >>= \case GRSPendingApproval gaId - | gaId == groupApprovalId -> do - getDuplicateGroup g >>= \case - Nothing -> sendReply "Error: getDuplicateGroup. Please notify the developers." - Just DGReserved -> sendReply $ "The group " <> groupRef <> " is already listed in the directory." - _ -> do - getGroupRolesStatus g gr >>= \case - Just GRSOk -> do - setGroupStatus st gr GRSActive - sendReply "Group approved!" - notifyOwner gr $ "The group " <> userGroupReference' gr n <> " is approved and listed in directory!\nPlease note: if you change the group profile it will be hidden from directory until it is re-approved." - Just GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin - Just GRSContactNotOwner -> replyNotApproved "user is not an owner." - Just GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin - Nothing -> sendReply "Error: getGroupRolesStatus. Please notify the developers." - where - replyNotApproved reason = sendReply $ "Group is not approved: " <> reason - serviceNotAdmin = serviceName <> " is not an admin." + | gaId == groupApprovalId -> do + getDuplicateGroup g >>= \case + Nothing -> sendReply "Error: getDuplicateGroup. Please notify the developers." + Just DGReserved -> sendReply $ "The group " <> groupRef <> " is already listed in the directory." + _ -> do + getGroupRolesStatus g gr >>= \case + Just GRSOk -> do + setGroupStatus st gr GRSActive + 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") + <> "- */help commands* - other 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 $ "Group approved!" <> maybe "" ("\n" <>) invited + notifyOtherSuperUsers $ approved <> " by " <> viewName (localDisplayName' ct) <> fromMaybe "" invited + Just GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin + Just GRSContactNotOwner -> replyNotApproved "user is not an owner." + Just GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin + Nothing -> sendReply "Error: getGroupRolesStatus. Please notify the developers." + where + replyNotApproved reason = sendReply $ "Group is not approved: " <> reason + serviceNotAdmin = serviceName <> " is not an admin." | otherwise -> sendReply "Incorrect approval code" _ -> sendReply $ "Error: the group " <> groupRef <> " is not pending approval." - where - groupRef = groupReference' groupId n - DCRejectGroup _gaId _gName -> pure () - DCSuspendGroup groupId gName -> do - let groupRef = groupReference' groupId gName - getGroupAndReg groupId gName >>= \case - Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)." - Just (_, gr) -> + where + groupRef = groupReference' groupId n + DCRejectGroup _gaId _gName -> pure () + DCSuspendGroup groupId gName -> do + let groupRef = groupReference' groupId gName + withGroupAndReg sendReply groupId gName $ \_ gr -> readTVarIO (groupRegStatus gr) >>= \case GRSActive -> do setGroupStatus st gr GRSSuspended - notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is suspended and hidden from directory. Please contact the administrators." + let suspended = "The group " <> userGroupReference' gr gName <> " is suspended" + notifyOwner gr $ suspended <> " and hidden from directory. Please contact the administrators." sendReply "Group suspended!" + notifyOtherSuperUsers $ suspended <> " by " <> viewName (localDisplayName' ct) _ -> sendReply $ "The group " <> groupRef <> " is not active, can't be suspended." - DCResumeGroup groupId gName -> do - let groupRef = groupReference' groupId gName - getGroupAndReg groupId gName >>= \case - Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)." - Just (_, gr) -> + DCResumeGroup groupId gName -> do + let groupRef = groupReference' groupId gName + withGroupAndReg sendReply groupId gName $ \_ gr -> readTVarIO (groupRegStatus gr) >>= \case GRSSuspended -> do setGroupStatus st gr GRSActive - notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is listed in the directory again!" + let groupStr = "The group " <> userGroupReference' gr gName + notifyOwner gr $ groupStr <> " is listed in the directory again!" sendReply "Group listing resumed!" + notifyOtherSuperUsers $ groupStr <> " listing resumed by " <> viewName (localDisplayName' ct) _ -> sendReply $ "The group " <> groupRef <> " is not suspended, can't be resumed." - DCListLastGroups count -> - readTVarIO (groupRegs st) >>= \grs -> do - sendReply $ show (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> show count else "") - void . forkIO $ forM_ (reverse $ take count grs) $ \gr@GroupReg {dbGroupId, dbContactId} -> do - ct_ <- getContact cc dbContactId - let ownerStr = "Owner: " <> maybe "getContact error" localDisplayName' ct_ - sendGroupInfo ct gr dbGroupId $ Just ownerStr - DCExecuteCommand cmdStr -> - sendChatCmdStr cc cmdStr >>= \r -> do - ts <- getCurrentTime - tz <- getCurrentTimeZone - sendReply $ serializeChatResponse (Nothing, Just user) ts tz Nothing r - DCCommandError tag -> sendReply $ "Command error: " <> show tag + DCListLastGroups count -> listGroups count False + DCListPendingGroups count -> listGroups count True + DCShowGroupLink groupId gName -> do + let groupRef = groupReference' groupId gName + withGroupAndReg sendReply groupId gName $ \_ _ -> + sendChatCmd cc (APIGetGroupLink groupId) >>= \case + Right CRGroupLink {connLinkContact = CCLink cReq _, memberRole} -> + sendReply $ T.unlines + [ "The link to join the group " <> groupRef <> ":", + strEncodeTxt $ simplexChatContact cReq, + "New member role: " <> strEncodeTxt memberRole + ] + Left (ChatErrorStore (SEGroupLinkNotFound _)) -> + sendReply $ "The group " <> groupRef <> " has no public link." + Right r -> do + ts <- getCurrentTime + tz <- getCurrentTimeZone + let resp = T.pack $ serializeChatResponse (Nothing, Just user) (config cc) ts tz Nothing r + sendReply $ "Unexpected error:\n" <> resp + Left e -> do + let resp = T.pack $ serializeChatError True (config cc) e + sendReply $ "Unexpected error:\n" <> resp + DCSendToGroupOwner groupId gName msg -> do + let groupRef = groupReference' groupId gName + withGroupAndReg sendReply groupId gName $ \_ gr@GroupReg {dbContactId} -> do + notifyOwner gr msg + owner <- groupOwnerInfo groupRef dbContactId + sendReply $ "Forwarded to " <> owner + DCInviteOwnerToGroup groupId gName -> case ownersGroup of + Just og@KnownGroup {localDisplayName = ogName} -> + withGroupAndReg sendReply groupId gName $ \_ gr@GroupReg {dbContactId = ctId} -> do + inviteToOwnersGroup og gr $ \case + Right () -> do + let groupRef = groupReference' groupId gName + owner <- groupOwnerInfo groupRef ctId + let invited = " invited " <> owner <> " to owners' group " <> viewName ogName + notifyOtherSuperUsers $ viewName (localDisplayName' ct) <> invited + sendReply $ "you" <> invited + Left err -> sendReply err + Nothing -> sendReply "owners' group is not specified" + -- DCAddBlockedWord _word -> pure () + -- DCRemoveBlockedWord _word -> pure () + DCCommandError tag -> sendReply $ "Command error: " <> tshow tag | otherwise = sendReply "You are not allowed to use this command" where - superUser = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct} - sendReply = sendComposedMessage cc ct (Just ciId) . textMsgContent + knownCt = knownContact ct + sendReply = mkSendReply ct ciId + notifyOtherSuperUsers s = withSuperUsers $ \ctId -> unless (ctId == contactId' ct) $ sendMessage' cc ctId s + listGroups count pending = + readTVarIO (groupRegs st) >>= \groups -> do + grs <- + if pending + then filterM (fmap pendingApproval . readTVarIO . groupRegStatus) groups + else pure groups + sendReply $ tshow (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> tshow count else "") + void . forkIO $ forM_ (reverse $ take count grs) $ \gr@GroupReg {dbGroupId, dbContactId} -> do + ct_ <- getContact' cc user dbContactId + let ownerStr = "Owner: " <> maybe "getContact error" localDisplayName' ct_ + sendGroupInfo ct gr dbGroupId $ Just ownerStr + inviteToOwnersGroup :: KnownGroup -> GroupReg -> (Either Text () -> IO a) -> IO a + inviteToOwnersGroup KnownGroup {groupId = ogId} GroupReg {dbContactId = ctId} cont = + sendChatCmd cc (APIListMembers ogId) >>= \case + Right (CRGroupMembers _ (Group _ ms)) + | alreadyMember ms -> cont $ Left "Owner is already a member of owners' group" + | otherwise -> do + sendChatCmd cc (APIAddMember ogId ctId GRMember) >>= \case + Right CRSentGroupInvitation {} -> do + printLog cc CLLInfo $ "invited contact ID " <> show ctId <> " to owners' group" + cont $ Right () + r -> contErr r + r -> contErr r + where + alreadyMember = isJust . find ((Just ctId == ) . memberContactId) + contErr r = do + let err = "error inviting contact ID " <> tshow ctId <> " to owners' group: " <> tshow r + putStrLn $ T.unpack err + cont $ Left err + groupOwnerInfo groupRef dbContactId = do + owner_ <- getContact' cc user dbContactId + let ownerInfo = "the owner of the group " <> groupRef + ownerName ct' = "@" <> viewName (localDisplayName' ct') <> ", " + pure $ maybe "" ownerName owner_ <> ownerInfo - getGroupAndReg :: GroupId -> GroupName -> IO (Maybe (GroupInfo, GroupReg)) - getGroupAndReg gId gName = - getGroup cc gId - $>>= \g@GroupInfo {groupProfile = GroupProfile {displayName}} -> - if displayName == gName - then atomically (getGroupReg st gId) - $>>= \gr -> pure $ Just (g, gr) - else pure Nothing + deSuperUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRSuperUser -> IO () + deSuperUserCommand ct ciId cmd + | knownContact ct `elem` superUsers = case cmd of + DCExecuteCommand cmdStr -> + sendChatCmdStr cc cmdStr >>= \case + Right r -> do + ts <- getCurrentTime + tz <- getCurrentTimeZone + sendReply $ T.pack $ serializeChatResponse (Nothing, Just user) (config cc) ts tz Nothing r + Left e -> + sendReply $ T.pack $ serializeChatError True (config cc) e + DCCommandError tag -> sendReply $ "Command error: " <> tshow tag + | otherwise = sendReply "You are not allowed to use this command" + where + sendReply = mkSendReply ct ciId + + knownContact :: Contact -> KnownContact + knownContact ct = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct} + + mkSendReply :: Contact -> ChatItemId -> Text -> IO () + mkSendReply ct ciId = sendComposedMessage cc ct (Just ciId) . MCText + + withGroupAndReg :: (Text -> IO ()) -> GroupId -> GroupName -> (GroupInfo -> GroupReg -> IO ()) -> IO () + withGroupAndReg sendReply gId = withGroupAndReg_ sendReply gId . Just + + withGroupAndReg_ :: (Text -> IO ()) -> GroupId -> Maybe GroupName -> (GroupInfo -> GroupReg -> IO ()) -> IO () + withGroupAndReg_ sendReply gId gName_ action = + getGroup cc user gId >>= \case + Nothing -> sendReply $ "Group ID " <> tshow gId <> " not found (getGroup)" + Just g@GroupInfo {groupProfile = GroupProfile {displayName}} + | maybe False (displayName ==) gName_ -> + getGroupReg st gId >>= \case + Nothing -> sendReply $ "Registration for group ID " <> tshow gId <> " not found (getGroupReg)" + Just gr -> action g gr + | otherwise -> + sendReply $ "Group ID " <> tshow gId <> " has the display name " <> displayName sendGroupInfo :: Contact -> GroupReg -> GroupId -> Maybe Text -> IO () sendGroupInfo ct gr@GroupReg {dbGroupId} useGroupId ownerStr_ = do grStatus <- readTVarIO $ groupRegStatus gr let statusStr = "Status: " <> groupRegStatusText grStatus - getGroupAndSummary cc dbGroupId >>= \case + getGroupAndSummary cc user dbGroupId >>= \case Just (GroupInfo {groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do let membersStr = "_" <> tshow currentMembers <> " members_" text = T.unlines $ [tshow useGroupId <> ". " <> groupInfoText p] <> maybeToList ownerStr_ <> [membersStr, statusStr] @@ -594,28 +1019,43 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi let text = T.unlines $ [tshow useGroupId <> ". Error: getGroup. Please notify the developers."] <> maybeToList ownerStr_ <> [statusStr] sendComposedMessage cc ct Nothing $ MCText text -getContact :: ChatController -> ContactId -> IO (Maybe Contact) -getContact cc ctId = resp <$> sendChatCmd cc (APIGetChat (ChatRef CTDirect ctId) (CPLast 0) Nothing) - where - resp :: ChatResponse -> Maybe Contact - resp = \case - CRApiChat _ (AChat SCTDirect Chat {chatInfo = DirectChat ct}) -> Just ct - _ -> Nothing +getContact' :: ChatController -> User -> ContactId -> IO (Maybe Contact) +getContact' cc user ctId = withDB "getContact" cc $ \db -> getContact db (vr cc) user ctId -getGroup :: ChatController -> GroupId -> IO (Maybe GroupInfo) -getGroup cc gId = resp <$> sendChatCmd cc (APIGroupInfo gId) - where - resp :: ChatResponse -> Maybe GroupInfo - resp = \case - CRGroupInfo {groupInfo} -> Just groupInfo - _ -> Nothing +getGroup :: ChatController -> User -> GroupId -> IO (Maybe GroupInfo) +getGroup cc user gId = withDB "getGroupInfo" cc $ \db -> getGroupInfo db (vr cc) user gId -getGroupAndSummary :: ChatController -> GroupId -> IO (Maybe (GroupInfo, GroupSummary)) -getGroupAndSummary cc gId = resp <$> sendChatCmd cc (APIGroupInfo gId) +withDB' :: Text -> ChatController -> (DB.Connection -> IO a) -> IO (Maybe a) +withDB' cxt cc a = withDB cxt cc $ ExceptT . fmap Right . a + +withDB :: Text -> ChatController -> (DB.Connection -> ExceptT StoreError IO a) -> IO (Maybe a) +withDB cxt ChatController {chatStore} action = do + r_ :: Either ChatError a <- withTransaction chatStore (runExceptT . withExceptT ChatErrorStore . action) `E.catches` handleDBErrors + case r_ of + Right r -> pure $ Just r + Left e -> Nothing <$ logError ("Database error: " <> cxt <> " " <> tshow e) + +getGroupAndSummary :: ChatController -> User -> GroupId -> IO (Maybe (GroupInfo, GroupSummary)) +getGroupAndSummary cc user gId = + withDB "getGroupAndSummary" cc $ \db -> (,) <$> getGroupInfo db (vr cc) user gId <*> liftIO (getGroupSummary db user gId) + +vr :: ChatController -> VersionRangeChat +vr ChatController {config = ChatConfig {chatVRange}} = chatVRange +{-# INLINE vr #-} + +getGroupLinkRole :: ChatController -> User -> GroupInfo -> IO (Maybe (Int64, CreatedLinkContact, GroupMemberRole)) +getGroupLinkRole cc user gInfo = + withDB "getGroupLink" cc $ \db -> getGroupLink db user gInfo + +setGroupLinkRole :: ChatController -> GroupInfo -> GroupMemberRole -> IO (Maybe ConnReqContact) +setGroupLinkRole cc GroupInfo {groupId} mRole = resp <$> sendChatCmd cc (APIGroupLinkMemberRole groupId mRole) where resp = \case - CRGroupInfo {groupInfo, groupSummary} -> Just (groupInfo, groupSummary) + Right (CRGroupLink _ _ (CCLink gLink _) _) -> Just gLink _ -> Nothing -unexpectedError :: String -> String +unexpectedError :: Text -> Text unexpectedError err = "Unexpected error: " <> err <> ", please notify the developers." + +strEncodeTxt :: StrEncoding a => a -> Text +strEncodeTxt = safeDecodeUtf8 . strEncode diff --git a/apps/simplex-directory-service/src/Directory/Store.hs b/apps/simplex-directory-service/src/Directory/Store.hs index 5082cab2ce..628419ea1d 100644 --- a/apps/simplex-directory-service/src/Directory/Store.hs +++ b/apps/simplex-directory-service/src/Directory/Store.hs @@ -3,6 +3,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} module Directory.Store ( DirectoryStore (..), @@ -10,8 +11,12 @@ module Directory.Store GroupRegStatus (..), UserGroupRegId, GroupApprovalId, + DirectoryGroupData (..), + DirectoryMemberAcceptance (..), + ProfileCondition (..), restoreDirectoryStore, addGroupReg, + delGroupReg, setGroupStatus, setGroupRegOwner, getGroupReg, @@ -19,28 +24,40 @@ module Directory.Store getUserGroupRegs, filterListedGroups, groupRegStatusText, + pendingApproval, + groupRemoved, + fromCustomData, + toCustomData, + noJoinFilter, + basicJoinFilter, + moderateJoinFilter, + strongJoinFilter ) where import Control.Concurrent.STM import Control.Monad +import Data.Aeson ((.=), (.:)) +import qualified Data.Aeson.KeyMap as JM +import qualified Data.Aeson.TH as JQ +import qualified Data.Aeson.Types as JT import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B -import Data.Composition ((.:)) import Data.Int (Int64) import Data.List (find, foldl', sortOn) import Data.Map (Map) import qualified Data.Map.Strict as M -import Data.Maybe (isJust) +import Data.Maybe (fromMaybe, isJust) import Data.Set (Set) import qualified Data.Set as S import Data.Text (Text) import Simplex.Chat.Types import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) import Simplex.Messaging.Util (ifM) -import System.IO (Handle, IOMode (..), openFile, BufferMode (..), hSetBuffering) -import System.Directory (renameFile, doesFileExist) +import System.Directory (doesFileExist, renameFile) +import System.IO (BufferMode (..), Handle, IOMode (..), hSetBuffering, openFile) data DirectoryStore = DirectoryStore { groupRegs :: TVar [GroupReg], @@ -65,6 +82,51 @@ data GroupRegData = GroupRegData groupRegStatus_ :: GroupRegStatus } +data DirectoryGroupData = DirectoryGroupData + { memberAcceptance :: DirectoryMemberAcceptance + } + +-- these filters are applied in the order of fields, depending on ProfileCondition: +-- Nothing - do not apply +-- Just +-- PCAll - apply to all profiles +-- PCNoImage - apply to profiles without images +data DirectoryMemberAcceptance = DirectoryMemberAcceptance + { rejectNames :: Maybe ProfileCondition, -- reject long names and names with profanity + passCaptcha :: Maybe ProfileCondition, -- run captcha challenge with joining members + makeObserver :: Maybe ProfileCondition -- the role assigned in the end, after captcha challenge + } + deriving (Eq, Show) + +data ProfileCondition = PCAll | PCNoImage deriving (Eq, Show) + +noJoinFilter :: DirectoryMemberAcceptance +noJoinFilter = DirectoryMemberAcceptance Nothing Nothing Nothing + +basicJoinFilter :: DirectoryMemberAcceptance +basicJoinFilter = + DirectoryMemberAcceptance + { rejectNames = Just PCNoImage, + passCaptcha = Nothing, + makeObserver = Nothing + } + +moderateJoinFilter :: DirectoryMemberAcceptance +moderateJoinFilter = + DirectoryMemberAcceptance + { rejectNames = Just PCAll, + passCaptcha = Just PCNoImage, + makeObserver = Nothing + } + +strongJoinFilter :: DirectoryMemberAcceptance +strongJoinFilter = + DirectoryMemberAcceptance + { rejectNames = Just PCAll, + passCaptcha = Just PCAll, + makeObserver = Nothing + } + type UserGroupRegId = Int64 type GroupApprovalId = Int64 @@ -78,8 +140,19 @@ data GroupRegStatus | GRSSuspended | GRSSuspendedBadRoles | GRSRemoved + deriving (Show) -data DirectoryStatus = DSListed | DSReserved | DSRegistered +pendingApproval :: GroupRegStatus -> Bool +pendingApproval = \case + GRSPendingApproval _ -> True + _ -> False + +groupRemoved :: GroupRegStatus -> Bool +groupRemoved = \case + GRSRemoved -> True + _ -> False + +data DirectoryStatus = DSListed | DSReserved | DSRegistered | DSRemoved groupRegStatusText :: GroupRegStatus -> Text groupRegStatusText = \case @@ -97,27 +170,50 @@ grDirectoryStatus = \case GRSActive -> DSListed GRSSuspended -> DSReserved GRSSuspendedBadRoles -> DSReserved + GRSRemoved -> DSRemoved _ -> DSRegistered +$(JQ.deriveJSON (enumJSON $ dropPrefix "PC") ''ProfileCondition) + +$(JQ.deriveJSON defaultJSON ''DirectoryMemberAcceptance) + +$(JQ.deriveJSON defaultJSON ''DirectoryGroupData) + +fromCustomData :: Maybe CustomData -> DirectoryGroupData +fromCustomData cd_ = + let memberAcceptance = fromMaybe noJoinFilter $ cd_ >>= \(CustomData o) -> JT.parseMaybe (.: "memberAcceptance") o + in DirectoryGroupData {memberAcceptance} + +toCustomData :: DirectoryGroupData -> CustomData +toCustomData DirectoryGroupData {memberAcceptance} = + CustomData $ JM.fromList ["memberAcceptance" .= memberAcceptance] + addGroupReg :: DirectoryStore -> Contact -> GroupInfo -> GroupRegStatus -> IO UserGroupRegId addGroupReg st ct GroupInfo {groupId} grStatus = do - grData <- atomically addGroupReg_ + grData <- addGroupReg_ logGCreate st grData pure $ userGroupRegId_ grData where addGroupReg_ = do let grData = GroupRegData {dbGroupId_ = groupId, userGroupRegId_ = 1, dbContactId_ = ctId, dbOwnerMemberId_ = Nothing, groupRegStatus_ = grStatus} gr <- dataToGroupReg grData - stateTVar (groupRegs st) $ \grs -> + atomically $ stateTVar (groupRegs st) $ \grs -> let ugrId = 1 + foldl' maxUgrId 0 grs grData' = grData {userGroupRegId_ = ugrId} gr' = gr {userGroupRegId = ugrId} - in (grData', gr' : grs) + in (grData', gr' : grs) ctId = contactId' ct maxUgrId mx GroupReg {dbContactId, userGroupRegId} | dbContactId == ctId && userGroupRegId > mx = userGroupRegId | otherwise = mx +delGroupReg :: DirectoryStore -> GroupReg -> IO () +delGroupReg st GroupReg {dbGroupId = gId, groupRegStatus} = do + logGDelete st gId + atomically $ writeTVar groupRegStatus GRSRemoved + atomically $ unlistGroup st gId + atomically $ modifyTVar' (groupRegs st) $ filter ((gId /=) . dbGroupId) + setGroupStatus :: DirectoryStore -> GroupReg -> GroupRegStatus -> IO () setGroupStatus st gr grStatus = do logGUpdateStatus st (dbGroupId gr) grStatus @@ -129,6 +225,7 @@ setGroupStatus st gr grStatus = do DSListed -> listGroup DSReserved -> reserveGroup DSRegistered -> unlistGroup + DSRemoved -> unlistGroup setGroupRegOwner :: DirectoryStore -> GroupReg -> GroupMember -> IO () setGroupRegOwner st gr owner = do @@ -136,18 +233,18 @@ setGroupRegOwner st gr owner = do logGUpdateOwner st (dbGroupId gr) memberId atomically $ writeTVar (dbOwnerMemberId gr) (Just memberId) -getGroupReg :: DirectoryStore -> GroupId -> STM (Maybe GroupReg) -getGroupReg st gId = find ((gId ==) . dbGroupId) <$> readTVar (groupRegs st) +getGroupReg :: DirectoryStore -> GroupId -> IO (Maybe GroupReg) +getGroupReg st gId = find ((gId ==) . dbGroupId) <$> readTVarIO (groupRegs st) -getUserGroupReg :: DirectoryStore -> ContactId -> UserGroupRegId -> STM (Maybe GroupReg) -getUserGroupReg st ctId ugrId = find (\r -> ctId == dbContactId r && ugrId == userGroupRegId r) <$> readTVar (groupRegs st) +getUserGroupReg :: DirectoryStore -> ContactId -> UserGroupRegId -> IO (Maybe GroupReg) +getUserGroupReg st ctId ugrId = find (\r -> ctId == dbContactId r && ugrId == userGroupRegId r) <$> readTVarIO (groupRegs st) -getUserGroupRegs :: DirectoryStore -> ContactId -> STM [GroupReg] -getUserGroupRegs st ctId = filter ((ctId ==) . dbContactId) <$> readTVar (groupRegs st) +getUserGroupRegs :: DirectoryStore -> ContactId -> IO [GroupReg] +getUserGroupRegs st ctId = filter ((ctId ==) . dbContactId) <$> readTVarIO (groupRegs st) -filterListedGroups :: DirectoryStore -> [(GroupInfo, GroupSummary)] -> STM [(GroupInfo, GroupSummary)] +filterListedGroups :: DirectoryStore -> [(GroupInfo, GroupSummary)] -> IO [(GroupInfo, GroupSummary)] filterListedGroups st gs = do - lgs <- readTVar $ listedGroups st + lgs <- readTVarIO $ listedGroups st pure $ filter (\(GroupInfo {groupId}, _) -> groupId `S.member` lgs) gs listGroup :: DirectoryStore -> GroupId -> STM () @@ -167,10 +264,15 @@ unlistGroup st gId = do data DirectoryLogRecord = GRCreate GroupRegData + | GRDelete GroupId | GRUpdateStatus GroupId GroupRegStatus | GRUpdateOwner GroupId GroupMemberId -data DLRTag = GRCreate_ | GRUpdateStatus_ | GRUpdateOwner_ +data DLRTag + = GRCreate_ + | GRDelete_ + | GRUpdateStatus_ + | GRUpdateOwner_ logDLR :: DirectoryStore -> DirectoryLogRecord -> IO () logDLR st r = forM_ (directoryLogFile st) $ \h -> B.hPutStrLn h (strEncode r) @@ -178,20 +280,25 @@ logDLR st r = forM_ (directoryLogFile st) $ \h -> B.hPutStrLn h (strEncode r) logGCreate :: DirectoryStore -> GroupRegData -> IO () logGCreate st = logDLR st . GRCreate +logGDelete :: DirectoryStore -> GroupId -> IO () +logGDelete st = logDLR st . GRDelete + logGUpdateStatus :: DirectoryStore -> GroupId -> GroupRegStatus -> IO () -logGUpdateStatus st = logDLR st .: GRUpdateStatus +logGUpdateStatus st gId = logDLR st . GRUpdateStatus gId logGUpdateOwner :: DirectoryStore -> GroupId -> GroupMemberId -> IO () -logGUpdateOwner st = logDLR st .: GRUpdateOwner +logGUpdateOwner st gId = logDLR st . GRUpdateOwner gId instance StrEncoding DLRTag where strEncode = \case GRCreate_ -> "GCREATE" + GRDelete_ -> "GDELETE" GRUpdateStatus_ -> "GSTATUS" GRUpdateOwner_ -> "GOWNER" strP = A.takeTill (== ' ') >>= \case "GCREATE" -> pure GRCreate_ + "GDELETE" -> pure GRDelete_ "GSTATUS" -> pure GRUpdateStatus_ "GOWNER" -> pure GRUpdateOwner_ _ -> fail "invalid DLRTag" @@ -199,13 +306,15 @@ instance StrEncoding DLRTag where instance StrEncoding DirectoryLogRecord where strEncode = \case GRCreate gr -> strEncode (GRCreate_, gr) + GRDelete gId -> strEncode (GRDelete_, gId) GRUpdateStatus gId grStatus -> strEncode (GRUpdateStatus_, gId, grStatus) GRUpdateOwner gId grOwnerId -> strEncode (GRUpdateOwner_, gId, grOwnerId) strP = - strP >>= \case - GRCreate_ -> GRCreate <$> (A.space *> strP) - GRUpdateStatus_ -> GRUpdateStatus <$> (A.space *> A.decimal) <*> (A.space *> strP) - GRUpdateOwner_ -> GRUpdateOwner <$> (A.space *> A.decimal) <*> (A.space *> A.decimal) + strP_ >>= \case + GRCreate_ -> GRCreate <$> strP + GRDelete_ -> GRDelete <$> strP + GRUpdateStatus_ -> GRUpdateStatus <$> A.decimal <*> _strP + GRUpdateOwner_ -> GRUpdateOwner <$> A.decimal <* A.space <*> A.decimal instance StrEncoding GroupRegData where strEncode GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerMemberId_, groupRegStatus_} = @@ -246,10 +355,10 @@ instance StrEncoding GroupRegStatus where "removed" -> pure GRSRemoved _ -> fail "invalid GroupRegStatus" -dataToGroupReg :: GroupRegData -> STM GroupReg +dataToGroupReg :: GroupRegData -> IO GroupReg dataToGroupReg GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerMemberId_, groupRegStatus_} = do - dbOwnerMemberId <- newTVar dbOwnerMemberId_ - groupRegStatus <- newTVar groupRegStatus_ + dbOwnerMemberId <- newTVarIO dbOwnerMemberId_ + groupRegStatus <- newTVarIO groupRegStatus_ pure GroupReg { dbGroupId = dbGroupId_, @@ -261,10 +370,9 @@ dataToGroupReg GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerM restoreDirectoryStore :: Maybe FilePath -> IO DirectoryStore restoreDirectoryStore = \case - Just f -> ifM (doesFileExist f) (restore f) (newFile f >>= new . Just) - Nothing -> new Nothing + Just f -> ifM (doesFileExist f) (restore f) (newFile f >>= newDirectoryStore . Just) + Nothing -> newDirectoryStore Nothing where - new = atomically . newDirectoryStore newFile f = do h <- openFile f WriteMode hSetBuffering h LineBuffering @@ -273,15 +381,15 @@ restoreDirectoryStore = \case grs <- readDirectoryData f renameFile f (f <> ".bak") h <- writeDirectoryData f grs -- compact - atomically $ mkDirectoryStore h grs + mkDirectoryStore h grs emptyStoreData :: ([GroupReg], Set GroupId, Set GroupId) emptyStoreData = ([], S.empty, S.empty) -newDirectoryStore :: Maybe Handle -> STM DirectoryStore +newDirectoryStore :: Maybe Handle -> IO DirectoryStore newDirectoryStore = (`mkDirectoryStore_` emptyStoreData) -mkDirectoryStore :: Handle -> [GroupRegData] -> STM DirectoryStore +mkDirectoryStore :: Handle -> [GroupRegData] -> IO DirectoryStore mkDirectoryStore h groups = foldM addGroupRegData emptyStoreData groups >>= mkDirectoryStore_ (Just h) where @@ -292,12 +400,13 @@ mkDirectoryStore h groups = DSListed -> (grs', S.insert gId listed, reserved) DSReserved -> (grs', listed, S.insert gId reserved) DSRegistered -> (grs', listed, reserved) + DSRemoved -> (grs, listed, reserved) -mkDirectoryStore_ :: Maybe Handle -> ([GroupReg], Set GroupId, Set GroupId) -> STM DirectoryStore +mkDirectoryStore_ :: Maybe Handle -> ([GroupReg], Set GroupId, Set GroupId) -> IO DirectoryStore mkDirectoryStore_ h (grs, listed, reserved) = do - groupRegs <- newTVar grs - listedGroups <- newTVar listed - reservedGroups <- newTVar reserved + groupRegs <- newTVarIO grs + listedGroups <- newTVarIO listed + reservedGroups <- newTVarIO reserved pure DirectoryStore {groupRegs, listedGroups, reservedGroups, directoryLogFile = h} readDirectoryData :: FilePath -> IO [GroupRegData] @@ -311,14 +420,18 @@ readDirectoryData f = Right r -> case r of GRCreate gr@GroupRegData {dbGroupId_ = gId} -> do when (isJust $ M.lookup gId m) $ - putStrLn $ "Warning: duplicate group with ID " <> show gId <> ", group replaced." + putStrLn $ + "Warning: duplicate group with ID " <> show gId <> ", group replaced." pure $ M.insert gId gr m + GRDelete gId -> case M.lookup gId m of + Just _ -> pure $ M.delete gId m + Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <> ", deletion ignored.") GRUpdateStatus gId groupRegStatus_ -> case M.lookup gId m of Just gr -> pure $ M.insert gId gr {groupRegStatus_} m - Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <>", status update ignored.") + Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <> ", status update ignored.") GRUpdateOwner gId grOwnerId -> case M.lookup gId m of Just gr -> pure $ M.insert gId gr {dbOwnerMemberId_ = Just grOwnerId} m - Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <>", owner update ignored.") + Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <> ", owner update ignored.") writeDirectoryData :: FilePath -> [GroupRegData] -> IO Handle writeDirectoryData f grs = do diff --git a/blog/20210512-simplex-chat-terminal-ui.md b/blog/20210512-simplex-chat-terminal-ui.md index a6a5aea5a4..1357f6c9b3 100644 --- a/blog/20210512-simplex-chat-terminal-ui.md +++ b/blog/20210512-simplex-chat-terminal-ui.md @@ -10,7 +10,7 @@ permalink: "/blog/20210512-simplex-chat-terminal-ui.html" **Published:** May 12, 2021 -For the last six months [me](https://github.com/epoberezkin) and my son [Efim](https://github.com/efim-poberezkin) have been working to bring you a working prototype of SimpleX Chat. We're excited to announce SimpleX Chat terminal client is now available [here](https://github.com/simplex-chat/simplex-chat) on Linux, Windows and Mac (you can either build from source or download the binary for Linux, Windows or Mac from the latest release). +For the last six months [me](https://github.com/epoberezkin) and my son Efim have been working to bring you a working prototype of SimpleX Chat. We're excited to announce SimpleX Chat terminal client is now available [here](https://github.com/simplex-chat/simplex-chat) on Linux, Windows and Mac (you can either build from source or download the binary for Linux, Windows or Mac from the latest release). We’ve been using the terminal client between us and a few other people for a couple of months now, eating our own “dog food”, and have developed up to version 0.3.1, with most of the messaging protocol features we originally planned diff --git a/blog/20220404-simplex-chat-instant-notifications.md b/blog/20220404-simplex-chat-instant-notifications.md index 73bf68a10b..7d88a47fa7 100644 --- a/blog/20220404-simplex-chat-instant-notifications.md +++ b/blog/20220404-simplex-chat-instant-notifications.md @@ -66,9 +66,9 @@ This service continues running when the app is switched off, and it is restarted So, for Android we can now deliver instant message notifications without compromising users' privacy in any way. The app version 1.5 that includes private instant notifications is now available on [Play Store](https://play.google.com/store/apps/details?id=chat.simplex.app), in our [F-Droid repo](https://app.simplex.chat/) and via direct [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) downloads! -Please let us what needs to be improved - it's only the first version of instant notifications for Android! +Please let us know what needs to be improved - it's only the first version of instant notifications for Android! -## Our iOS approach has one trade-off +## iOS notifications require a server iOS is much more protective of what apps are allowed to run on the devices, and the solution that worked on Android is not viable on iOS. diff --git a/blog/20220928-simplex-chat-v4-encrypted-database.md b/blog/20220928-simplex-chat-v4-encrypted-database.md index bdf7e9790c..6f8064454f 100644 --- a/blog/20220928-simplex-chat-v4-encrypted-database.md +++ b/blog/20220928-simplex-chat-v4-encrypted-database.md @@ -78,7 +78,7 @@ You can run SimpleX Chat CLI as a local WebSockets server on any port, we use 52 simplex-chat -p 5225 ``` -Then you can create a JavaScript or TypeScript application that would connect to it and control it via a simple WebSocket API. TypeScript SDK defines all necessary types and convenience functions to use in your applications. See this [sample bot](https://github.com/simplex-chat/simplex-chat/blob/stable/packages/simplex-chat-client/typescript/examples/squaring-bot.js) and [README page](https://github.com/simplex-chat/simplex-chat/tree/ep/blog-v4/packages/simplex-chat-client/typescript). +Then you can create a JavaScript or TypeScript application that would connect to it and control it via a simple WebSocket API. TypeScript SDK defines all necessary types and convenience functions to use in your applications. See this [sample bot](https://github.com/simplex-chat/simplex-chat/blob/stable/packages/simplex-chat-client/typescript/examples/squaring-bot.js) and README page. SimpleX Chat API allows you to: diff --git a/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md b/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md index 515f06c66b..51dba8818c 100644 --- a/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md +++ b/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md @@ -27,7 +27,7 @@ In the course of the audit, Trail of Bits assessed the maturity of the [simplexm Explained below is our understanding of the issues, as well as fixes implemented by the SimpleX Chat team after project completion. The full security review is available via [Trail of Bits publications](https://github.com/trailofbits/publications/blob/master/reviews/SimpleXChat.pdf). -We are hugely thankful to Trail of Bits and their engineers for the work they did, helping us identify these issues and supporting the ongoing efforts to make Simple Chat more secure. +We are hugely thankful to Trail of Bits and their engineers for the work they did, helping us identify these issues and supporting the ongoing efforts to make SimpleX Chat more secure. ### Medium severity issues diff --git a/blog/20221206-simplex-chat-v4.3-voice-messages.md b/blog/20221206-simplex-chat-v4.3-voice-messages.md index 32bbe058e5..07a6e227f0 100644 --- a/blog/20221206-simplex-chat-v4.3-voice-messages.md +++ b/blog/20221206-simplex-chat-v4.3-voice-messages.md @@ -14,11 +14,11 @@ permalink: "/blog/20221206-simplex-chat-v4.3-voice-messages.html" ## SimpleX Chat reviews -Since we published [the security assessment of SimpleX Chat](https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html) completed by Trail of Bits in November, several sites published the reviews and included it in their recommendations: +Since we published [the security assessment of SimpleX Chat](./20221108-simplex-chat-v4.2-security-audit-new-website.md) completed by Trail of Bits in November, several sites published the reviews and included it in their recommendations: - Privacy Guides added SimpleX Chat to [the recommended private and secure messengers](https://www.privacyguides.org/real-time-communication/#simplex-chat). - Mike Kuketz – a well-known security expert – published [the review of SimpleX Chat](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/) and added it to [the messenger matrix](https://www.messenger-matrix.de). -- Supernova published [the review](https://supernova.tilde.team/detailed_reviews.html#simplex) and increased [SimpleX Chat recommendation ratings](https://supernova.tilde.team/messengers.html). +- Supernova published [the review](https://supernovas.space/detailed_reviews.html#simplex) and increased [SimpleX Chat recommendation ratings](https://supernovas.space/messengers.html). ## What's new in v4.3 diff --git a/blog/20230103-simplex-chat-v4.4-disappearing-messages.md b/blog/20230103-simplex-chat-v4.4-disappearing-messages.md index 7c00df3228..ab9010535f 100644 --- a/blog/20230103-simplex-chat-v4.4-disappearing-messages.md +++ b/blog/20230103-simplex-chat-v4.4-disappearing-messages.md @@ -71,7 +71,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20230204-simplex-chat-v4-5-user-chat-profiles.md b/blog/20230204-simplex-chat-v4-5-user-chat-profiles.md index 18817a18b6..0f20747ae8 100644 --- a/blog/20230204-simplex-chat-v4-5-user-chat-profiles.md +++ b/blog/20230204-simplex-chat-v4-5-user-chat-profiles.md @@ -97,7 +97,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20230301-simplex-file-transfer-protocol.md b/blog/20230301-simplex-file-transfer-protocol.md index 0008dd6b9b..9219b8122c 100644 --- a/blog/20230301-simplex-file-transfer-protocol.md +++ b/blog/20230301-simplex-file-transfer-protocol.md @@ -139,7 +139,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20230328-simplex-chat-v4-6-hidden-profiles.md b/blog/20230328-simplex-chat-v4-6-hidden-profiles.md index 4fe282b081..c369eb5792 100644 --- a/blog/20230328-simplex-chat-v4-6-hidden-profiles.md +++ b/blog/20230328-simplex-chat-v4-6-hidden-profiles.md @@ -104,7 +104,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](https://simplex.chat/#why-ids-bad-for-privacy). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md b/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md index 0a66124934..eb1288059a 100644 --- a/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md +++ b/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md @@ -40,9 +40,11 @@ Many large tech companies prioritizing value extraction over value creation earn ### How is it funded and what is the business model? -We started working full-time on the project in 2021 when [Portman Wills](https://www.linkedin.com/in/portmanwills/) and [Peter Briffett](https://www.linkedin.com/in/peterbriffett/) (the founders of [Wagestream](https://wagestream.com/en/) where I led the engineering team) supported the company very early on, and several other angel investors joined later. In July 2022 SimpleX Chat raised a pre-seed funding from the VC fund [Village Global](https://www.villageglobal.vc) - its co-founder [Ben Casnocha](https://casnocha.com) was very excited about our vision of privacy-first fully decentralized messaging and community platform, both for the individual users and for the companies, independent of any crypto-currencies, that might grow to replace large centralized platforms, such as WhatsApp, Telegram and Signal. +We started working full-time on the project in 2021 when [Portman Wills](https://www.linkedin.com/in/portmanwills/) and [Peter Briffett](https://www.linkedin.com/in/peterbriffett/) (the founders of [Wagestream](https://wagestream.com/en/) where I led the engineering team) supported the company very early on, and several other angel investors joined later. In July 2022 SimpleX Chat raised a pre-seed funding from the VC fund [Village Global](https://www.villageglobal.vc) - its co-founder [Ben Casnocha](https://www.villageglobal.vc/team/ben-casnocha) was very excited about our vision of privacy-first fully decentralized messaging and community platform, both for the individual users and for the companies, independent of any crypto-currencies, that might grow to replace large centralized platforms, such as WhatsApp, Telegram and Signal. -Overall we raised from our investors approximately $370,000 for a small share of the company to allow the project team working full time for almost two years, funding product design and development, infrastructure, and also [the security assessment by Trail of Bits](https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html). A large part of this money is not spent yet. +> Edit: please see the comment from Ben Casnocha about this investment in [our post from August 14, 2024](./20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md). + +Overall we raised from our investors approximately $370,000 for a small share of the company to allow the project team working full time for almost two years, funding product design and development, infrastructure, and also [the security assessment by Trail of Bits](./20221108-simplex-chat-v4.2-security-audit-new-website.md). A large part of this money is not spent yet. The project was hugely supported by the users as well - collectively, [you donated](https://github.com/simplex-chat/simplex-chat#help-us-with-donations) over $25,000. Without these donations the investment we raised would not be possible, because we believe that voluntary user donations can sustain the project in the long term – it already covers all infrastructure costs. There are only two ways an Internet service can exist - either users are paying for it, or the users data becomes the product for the real customers, as happened with many large Internet companies. In the latter case the users are losing much more money than they are saving by giving away their privacy and the rights to the content they create on the centralized platforms. @@ -106,7 +108,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](https://simplex.chat/#why-ids-bad-for-privacy). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md b/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md index 0cdbe2831f..0128a64b21 100644 --- a/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md +++ b/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md @@ -102,7 +102,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](https://simplex.chat/#why-ids-bad-for-privacy). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md b/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md index 759587821b..5818049858 100644 --- a/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md +++ b/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md @@ -160,7 +160,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](https://simplex.chat/#why-ids-bad-for-privacy). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md b/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md index fc924a8706..3c3fb7b515 100644 --- a/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md +++ b/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md @@ -50,7 +50,7 @@ Other limitations of the desktop app: - you cannot send voice messages. - there is no support for calls yet. -You can download the desktop app for Linux and Mac via [downloads page](https://simplex.chat/downloads). Windows version will be available soon. +You can download the desktop app for Linux and Mac via [downloads page](../docs/DOWNLOADS.md). Windows version will be available soon. ## Group directory service and other group improvements @@ -108,7 +108,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](https://simplex.chat/#why-ids-bad-for-privacy). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md b/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md index 4fbfc400ad..539d719af4 100644 --- a/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md +++ b/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md @@ -40,7 +40,7 @@ This is only possible when both devices are connected to the same local network. **On desktop** -If you don't have desktop app installed yet, [download it](https://simplex.chat/downloads/) and create any chat profile - you don't need to use it, and when you create it there are no server requests sent and no accounts are created. Think about it as about user profile on your computer. +If you don't have desktop app installed yet, [download it](../docs/DOWNLOADS.md) and create any chat profile - you don't need to use it, and when you create it there are no server requests sent and no accounts are created. Think about it as about user profile on your computer. Then in desktop app settings choose *Link a mobile* - it will show a QR code. @@ -133,7 +133,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md b/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md new file mode 100644 index 0000000000..f5539106b7 --- /dev/null +++ b/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md @@ -0,0 +1,119 @@ +--- +layout: layouts/article.html +title: "SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect." +date: 2024-01-24 +previewBody: blog_previews/20240124.html +image: images/20240124-connect1.png +permalink: "/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.html" +--- + +# SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect. + +**Published:** Jan 24, 2024 + +[SimpleX Chat infrastructure on Linode](#simplex-chat-infrastructure-on-linode): +- Free infrastructure. +- SimpleX servers in Linode Marketplace. +- High capacity messaging servers. + +What's new in v5.5: +- [private notes](#private-notes) +- [group history](#group-history) +- [simpler UX to connect to other users](#simpler-ux-to-connect-to-other-users) +- [message delivery stability and other improvements](#message-delivery-stability-and-other-improvements) + +Also, we added Hungarian (only Android) and Turkish interface languages, thanks to [our users and Weblate](https://github.com/simplex-chat/simplex-chat#help-translating-simplex-chat). + +SimpleX Chat Android app is now available in 20 languages! + +## SimpleX Chat infrastructure on Linode + +We chose Linode as our hosting provider as and they have been consistently reliable, cheaper than alternatives, with excellent support and great documentation. + +When Linode was acquired by Akamai, we were a bit nervous about how it may affect service quality. So far it's been working out quite well. + +As the usage of SimpleX network was growing, so did our hosting costs, and from being really small they started to become significant, particularly as we didn't yet manage to optimize the servers last year. + +Linode helped - we're really excited to announce that Akamai decided to support SimpleX Chat growth by accepting it into their [Linode Rise startup program](https://www.linode.com/linode-for-startups/). + +Thanks to this program: + +- we received free infrastructure for the first year up to $10,000 per month, no strings attached. It already saved us some money, and gave us enough time to optimize the servers - the latest version of the servers are much less costly to operate with the current traffic, and can support a much larger traffic within this limit. In the year 2 of the program we will receive 50% discount with unlimited traffic, and in year 3 - 25% discount. + +- Linode Marketplace now includes [SimpleX Chat messages and file servers](https://www.linode.com/marketplace/apps/simplex-chat/simplex-chat/) - you can get free $100 credits for the first 2 months and run your own servers in just a few clicks, and use them in SimpleX Chat apps. Anybody can submit their application to Linode marketplace, but dedicated support we have from Linode team via this program made it simpler. + +- Akamai solution engineers are helping us to design high capacity server solution, free of charge, so that a single host can provide horizontally scalable capacity for messaging, allowing for a much larger number of concurrent users on a single server address. Initially we considered using HAProxy, and the latest proof of concept uses OpenResty - a fork of Nginx with Lua script engine - to route requests from a single host to multiple SMP relays, reducing an overhead for the clients that would be configured with a smaller number of higher capacity servers. This project is still in progress, there will be more details as we roll it out. + +## What's new in v5.5 + +### Private notes + +<img src="./images/20240124-notes1.png" width="220" class="float-to-left"> <img src="./images/20240124-notes2.png" width="220" class="float-to-left"> + +*"Where do I put notes for myself?"* was a very common support question. There was a workaround - you could create an empty group, just with yourself, and use it to save notes, but it was not very convenient, and you could accidentally add members there. + +This version has a more convenient and private alternative - the Private notes. It looks like an ordinary conversation where you can put text messages, links with previews, and any media and files, but they are not sent anywhere - they are stored locally, only on your device, with encrypted files. + +You can access the Private notes created in mobile app from desktop app too, by linking a mobile and desktop apps - the feature [added in the previous version](./20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md). It allows to conveniently share files between the devices without sending them over the Internet. + +### Group history + +<img src="./images/20240124-history1.png" width="220" class="float-to-left"> <img src="./images/20240124-history2.png" width="220" class="float-to-left"> + +In the previous version, when users joined groups, they only saw an empty conversation, and the notifications of being connected to other members. This version allows group admins sending recent group history to the new members - this option is enabled by default for new groups, and can be enabled for the existing groups in the preferences. So now new members can join the conversation as soon as they join. + +This does not mean that these messages are stored on any servers - the admin member that adds a new member to the group sends these messages directly when a new member joins. Groups are still fully decentralized, do not have any identity on the network, and fully private - only their members know they exist. + +That is, unless a group owner decides to make it public. Groups can be registered in [SimpleX groups directory](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) to be discovered by the new members - group directory is also improved. + +### Simpler UX to connect to other users + +<img src="./images/20240124-connect1.png" width="220" class="float-to-left"> <img src="./images/20240124-connect2.png" width="220" class="float-to-left"> + +SimpleX platform has no user accounts or identities, and while it improves metadata privacy, it also makes it harder to understand how to connect to other people, particularly for the new users who are not invited by the existing users. + +This version simplifies this interface by allowing to connect via the received link just by pasting the address into the search bar, as is common in many wallet apps and some other decentralized messengers. We also improved the interface of creating invitation links. + +We will continue working on improving and simplifying user interface throughout the year. Please send us any feedback and suggestions to the team's address available in the app. + +### Message delivery stability and other improvements + +One of the long standing issues was that message reception could get stuck in some rare occasions, and only get resumed once the app is fully restarted. As Android app includes an always-on notification service that runs in background, full restart should be done via the app settings. + +This version fixed many issues with message delivery stability and also added some diagnostics to identify any other cases when message delivery may stop. These fixes should also reduce battery usage, particularly on slow internet connections. + +Other improvements in this version: +- you can now reveal secret messages by tapping. To send a secret message wrap in "#" characters, e.g. "\#password\#". +- you can delete the last user profile, simplifying account deletion. If you have [hidden user profiles](./20230328-simplex-chat-v4-6-hidden-profiles.md), they won't be deleted in this case, and will be accessible again once you create a new profile. + +## SimpleX platform + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). + +[How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). + +Please also see our [website](https://simplex.chat). + +## Help us with donations + +Huge thank you to everybody who donated to SimpleX Chat! + +We are prioritizing users privacy and security - it would be impossible without your support. + +Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds – any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/tree/master#help-us-with-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder diff --git a/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md b/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md new file mode 100644 index 0000000000..55158130f8 --- /dev/null +++ b/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md @@ -0,0 +1,262 @@ +--- +layout: layouts/article.html +title: "SimpleX Chat v5.6 (beta): adding quantum resistance to Signal double ratchet algorithm" +date: 2024-03-14 +previewBody: blog_previews/20240314.html +image: images/20240314-kem.jpg +imageWide: true +permalink: "/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.html" +--- + +# SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm + +**Published:** Mar 14, 2024 + +This is a major upgrade for SimpleX messaging protocols, we are really proud to present the results of the hard work of our whole team on the [Pi day](https://en.wikipedia.org/wiki/Pi_Day). + +This post also covers various aspects of end-to-end encryption, compares different messengers, and explains why and how quantum-resistant encryption is added to SimpleX Chat: + +- [Why do we need end-to-end encryption?](#why-do-we-need-end-to-end-encryption) +- [Why is encryption even allowed?](#why-is-encryption-even-allowed) +- [End-to-end encryption security: attacks and defense.](#end-to-end-encryption-security-attacks-and-defense) + 1. Compromised message size - mitigated by padding messages to a fixed block size. + 2. Compromised confidentiality - mitigated by repudiation (deniability). + 3. Compromised message keys - mitigated by forward secrecy. + 4. Compromised long-term or session - mitigated by break-in recovery. + 5. Man-in-the-middle attack - mitigated by two-factor key exchange. + 6. "Record now, decrypt later" attacks - mitigated by post-quantum cryptography. +- [How secure is encryption in different messengers?](#how-secure-is-end-to-end-encryption-in-different-messengers) +- [Adding quantum resistance to Signal double ratchet algorithm.](#adding-quantum-resistance-to-signal-double-ratchet-algorithm) +- [When can you start using quantum resistant chats?](#when-can-you-start-using-quantum-resistant-chats) +- [Next for post-quantum crypto - all direct chats, small groups and security audit.](#next-for-post-quantum-crypto---all-direct-chats-small-groups-and-security-audit) + +## Why do we need end-to-end encryption? + +The objective of end-to-end encryption is to make any potential attackers, such as traffic observers or communication providers who pass the messages between senders and recipients, unable to recover *any* message content or meaningful information about the messages, even if these attackers possess very advanced computing and mathematical capabilities. + +While human eyes are unable to see any difference between simply scrambled and encrypted messages, the difference between unreadable scrambling and unbreakable encryption can be as huge as just a few seconds to unscramble a message on an average laptop and more time than the Universe existed required to break the encryption on the most powerful computer in the world. + +Achieving the latter requires a lot of mathematical precision in both the cryptographic algorithms and in how they are used, and effectively makes encrypted messages indistinguishable from random noise, without any discoverable patterns or statistical irregularities that a computer could use to break the message encryption any faster than it it would take to try every possible combination of bits in the key. + +End-to-end encryption is an important component of our individual and business security, privacy and sovereignty. Having our private communications protected from any observers is both the natural condition and our inalienable human right. + +It's very sad to see the same people who keep their financial affairs private to protect from financial crimes, lock their doors to protect from thieves, and curtain their windows to protect from the occasional prying eyes, when it comes to protecting their personal lives from the data criminals say "we don't care about privacy, we have nothing to hide". Everybody's safety depends on keeping their affairs and relations private, not visible to a vast and ruthless data gathering machines, that abuse our data for commercial gain, without any regard to our interests or even [the safety of our families and children](https://nmdoj.gov/press-release/attorney-general-raul-torrez-files-lawsuit-against-meta-platforms-and-mark-zuckerberg-to-protect-children-from-sexual-abuse-and-human-trafficking/). + +## Why is encryption even allowed? + +<img src="./images/20240314-djb.jpg" class="float-to-right"> + +If encryption is such a powerful tool to protect our lives, it also can be used to conceal crimes, so why the governments don't consider it similar to arms, and don't heavily regulate its use? + +Prior to 1996 the cryptography was considered munition, and its export from the United States was controlled under this category, [alongside flamethrowers and B-1 bombers](https://cr.yp.to/export/1995/0303-eff.txt). When [Daniel J. Bernstein](https://en.wikipedia.org/wiki/Daniel_J._Bernstein) (DJB), then a student of Mathematics at University of California, Berkeley, wanted to publish the paper and the source code of his Snuffle encryption system, the Office of Defense Trade Controls of the Department of State (DOS) after more than a year of correspondence requested that DJB registers as the arms dealer. + +In 1995 DJB represented by the Electronic Frontier Foundation brought a case against the DOS to overturn cryptography restrictions. The ruling in the case declared that the export control over cryptographic software and related technical data constitute [an impermissible infringement on speech in violation of the First Amendment](https://cr.yp.to/export/1996/1206-order.txt). This decision resulted in regulatory changes, reducing controls on encryption exports, particularly for open-source algorithms. The case continued until 2003, when it was put on hold after the commitment from the US government not to enforce any remaining regulations. + +This case is very important for the whole industry, as to this day we can freely create and use open-source cryptography without export control restrictions. It also shows the importance of engaging with the system and challenging its views in an open dialogue, rather than either blindly complying or violating regulations. + +DJB role for cryptography and open-source goes beyond this case – many cryptographic algorithms that are considered to be the most advanced, and many of which we use in SimpleX Chat, were designed and developed by him: + +- Ed25519 cryptographic signature algorithm we use to authorize commands to the servers. +- NaCL library with cryptobox and secretbox constructions that combine X25519 Diffie-Hellman key agreement with Salsa20 encryption and Poly1305 authentication. We use cryptobox to encrypt messages in two of three encryption layers and secretbox to encrypt files. +- Streamlined NTRU Prime algorithm for quantum resistant key agreement that we used in the protocol for linking mobile app with desktop, and now added to Signal double ratchet algorithm, as explained below. + +Without DJB's work the world would have been in a much worse place privacy- and security-wise. + +Daniel, we are really grateful for the work you did and continue doing. Thank you, and congratulations on the International Mathematics Day! + +## End-to-end encryption security: attacks and defense + +End-to-end encryption is offered by many messaging apps and protocols, but the security of different implementations are not the same. While many users know about the importance of forward secrecy - the quality of end-to-end encryption that preserves security of the encryption of the past messages, even if the keys used to encrypt some of the messages were compromised - there are many other qualities that protect from different attacks. Below there is the overview of these attacks and the properties of end-to-end encryption schemes that mitigate these attacks. + +### 1. Compromised message size - mitigated by padding messages to a fixed block size + +While the content encryption is the most important, concealing the actual message size is almost as important for several reasons: + +- attacker able to observe even approximate message sizes can use these sizes as an additional signal for machine learning to de-anonymise the users and to categorize the relationships between the users. +- if a messenger conceals the routing of the messages to hide the transport identities (IP addresses) of senders and recipients, message sizes can be used by traffic observers to confirm the fact of communication with a much higher degree of certainty. + +The only effective mitigation to these attacks is to pad all messages to a fixed size. Using space-efficient schemes like Padme, or padding to encryption block size is ineffective for mitigating these attacks, as they still allow differentiating message sizes. + +To the best of our knowledge, the only messengers other than SimpleX Chat that pad all messages to a fixed packet size are Cwtch and no longer developed [Pond](https://github.com/agl/pond) - SimpleX design can be seen as an evolution of Pond design. + +### 2. Compromised confidential messages - mitigated by repudiation (deniability) + +Many users are very interested in having ability to irreversibly delete sent messages from the recipients devices. But not only would this ability violate data sovereignty of device owners, it is also completely ineffective, as the recipients could simply put the device offline or use a modified client app to ignore message deletion requests. While SimpleX Chat provides such features as [disappearing messages](./20230103-simplex-chat-v4.4-disappearing-messages.md#disappearing-messages) and the ability to [irreversibly delete sent messages](./20221206-simplex-chat-v4.3-voice-messages.md#irreversible-message-deletion) provided both parties agree to that, these are convenience features, and they cannot be considered security measures. + +The solution to that is well known to cryptographers - it is the quality of the encryption algorithms called "repudiation", sometimes also called "deniability". This is the ability of the senders to plausibly deny having sent any messages, because cryptographic algorithms used to encrypt allow recipients forging these messages on their devices, so while the encryption proves authenticity of the message to the recipient, it cannot be used as a proof to any third party. + +Putting it all in a simpler language - a sender can claim that the recipient forged messages on their device, and deny ever having sent them. The recipient will not be able to provide any cryptographic proof. This quality makes digital conversation having the same qualities as private off-the-record conversation - that's why the family of algorithms that provide these qualities are called off-the-record (OTR) encryption. + +Repudiation is still a rather new concept - the first off-the-record algorithms were proposed in 2004 and were only offered to a wide range of users in Signal messenger. This concept is still quite badly understood by users and society, and yet to have been used as the defense in any public court cases, as legal systems evolve much slower than technology. In high profile cases repudiation can be used as an effective evidence for the defense. + +Repudiation in messaging systems can be undermined by adding cryptographic signature to the protocol, and many messengers that use OTR encryption algorithms do exactly that, unfortunately. SimpleX Chat does not use signature in any part of client-client protocol, but the signature is currently used when authorizing sender's messages to the relays. v5.7 will improve deniability by enabling a different authorization scheme that will provide full-stack repudiation in all protocol layers. + +### 3. Compromised message keys - mitigated by forward secrecy + +The attacker who obtained or broke the keys used to encrypt individual messages, may try to use these keys to decrypt past or future messages. This attack is unlikely to succeed via message interception, and it is likely to require breaking into the device storage. But in any case, if the key was broken or obtained in some other way it's important that this key cannot be used to decrypt other messages - this is achieved by forward secrecy. + +This property is well understood by the users, and most messengers that focus on privacy and security, with the exception of Session, provide forward secrecy as part of their encryption schemes design. + +### 4. Compromised long-term or session - mitigated by break-in recovery + +This attack is much less understood by the users, and forward secrecy does not protect from it. Arguably, it's almost impossible to compromise individual message keys without compromising long-term or session keys. So the ability of the encryption to recover from break-in (attacker making a copy of the device data without retaining the ongoing access) is both very important and pragmatic - break-in attacks are simpler to execute on mobile devices during short-term device access than long-term ongoing compromise. + +Out of all encryption algorithms known to us only _Signal double ratchet algorithm_ (also referred to as _Signal algorithm_ or _double ratchet algorithm_, which is not the same as Signal messaging platform and protocols) provides the ability for the encryption security to recover after break-ins attacks. This recovery happens automatically and transparently to the users, without them doing anything special or even knowing about break-in, by simply sending messages. Every time one of the communication parties replies to another party message, new random keys are generated and previously stolen keys become useless. + +Double ratchet algorithm is used in Signal, Cwtch and SimpleX Chat. But Signal app by allowing to use the same profile on multiple devices compromises the break-in recovery function of Signal algorithm, as explained in [this paper](https://eprint.iacr.org/2021/626.pdf). Because of break-in recovery you cannot use SimpleX Chat profile on more than one device at the same time - the encryption scheme rotates the long term keys, randomly, and keys on another device become useless, as they would become useless for the attacker who stole them. Security always has some costs to the convenience. + +### 5. Man-in-the-middle attack - mitigated by two-factor key exchange + +Many people incorrectly believe that security of end-to-end encryption cannot be broken by communication provider. But end-to-end encryption is only as secure as key exchange. While any intermediary passing the keys between senders and recipients cannot recover the private keys from the public keys, they can simply replace the passed public keys with their own and then proxy all communication between the users having full access to the original messages. So instead of having an end-to-end encrypted channel, users would have two half-way encrypted channels - between users and their communication intermediary. + +Pictures below illustrate how this attack works for RSA encryption. + +#### 1) Alice sends the key to Bob (e.g. via p2p network or via the messaging server). + +![Public key is shared](./images/20240314-mitm1.jpg) + +#### 2) Now Bob can send encrypted messages to Alice - he believes they are secure! + +![Message is encrypted](./images/20240314-mitm2.jpg) + +#### 3) But the key could have been intercepted and substituted by Tom (the attacker, or a service provider). + +![Key is intercepted and replaced](./images/20240314-mitm3.jpg) + +#### 4) Now the attacker can read the messages without Alice and Bob knowing. + +![End-to-end encryption is compromised](./images/20240314-mitm4.jpg) + +The attack on Diffie-Hellman (or on quantum-resistant) key exchange, when both parties send their public keys (or public key and ciphertext), requires the attacker to intercept and replace both keys, but the outcome remains the same - if all communication is passed via a single channel, as it is usually the case with communication services, then any attacker that has inside access to the service can selectively compromise some of the conversations. Two years ago I wrote the post about this [vulnerability of end-to-end encryption to MITM attacks](https://www.poberezkin.com/posts/2022-12-07-why-privacy-needs-to-be-redefined.html#e2e-encryption-is-not-bulletproof). + +All known mitigations of this attack require using the secondary communication channel to ensure that the keys have not been substituted. The most secure approach is to make user's key (or key fingerprint) a part of the user's address or connection link, thus making two-factor key exchange non-optional. This approach is used in Session, Cwtch and SimpleX Chat. + +A less secure approach is to provide users an optional way to compare security codes - this is what is done by Signal, Element and many other messengers. The problem with this post-key-exchange verification is that it is optional, and is usually skipped by the majority of the users. Also, this security code can change because the user changed the device, or as a result of the attack via the service provider. When you see in the client app the notification that the security code changed, it's pointless to ask in the same messenger whether the device was changed, as if it were an attack, the attacker would simply confirm it. Instead, the security code needs to be re-validated again via another channel. A good security practice for the users would be to warn their communication partners about the intention to switch the device in advance, before the security code is changed. + +### 6. "Record now, decrypt later" attacks - mitigated by post-quantum cryptography. + +This is the idea based on the assumption that commercially viable quantum computers will become available during the next 10 years, and then they can use time-efficient [Shor's algorithm](https://en.wikipedia.org/wiki/Shor%27s_algorithm) developed in 1994 to break asymmetric encryption with quantum computer (symmetric encryption is not vulnerable to this algorithm). + +Post-quantum cryptography, or encryption algorithms that are resistant to quantum computers, has been the area of ongoing research for several decades, and there are some algorithms that _might_ protect from quantum computers. It's important to account for these limitations: + +- _none of the post-quantum algorithms are proven to be secure_ against quantum or conventional computers. They are usually referred to as "believed to be secure" by the researchers and security experts. There is continuous research to break post-quantum algorithms, and to prove their security, and many of these algorithms are broken every year, often by conventional computers. +- because of the lack of proofs or guarantees that post-quantum cryptography delivers on its promise, these algorithms can only be used in hybrid encryption schemes to augment conventional cryptography, and never to replace it, contrary to some expert recommendations, as DJB explains in this [blog post](https://blog.cr.yp.to/20240102-hybrid.html). +- they are much more computationally expensive and less space efficient, and the encryption schemes have to balance their usability and security. +- many of post-quantum algorithms have known patent claims, so any system deploying them accepts the risks of patent litigation. +- the silver lining to these limitations is that the risk of appearance of commercially viable quantum computers in the next decade may be exaggerated. + +So, to say it provocatively, post-quantum cryptography can be compared with a remedy against the illness that nobody has, without any guarantee that it will work. While there is a reasonable hope that it _might_ work, so it's not exactly a _snake oil_, these limitations and risks have to be much better communicated to the end users than they are. + +<img src="./images/20240314-datacenter.jpg" width="400" class="float-to-right"> + +Does it mean that post-quantum cryptography is useless and should be ignored? Absolutely not. The risks of "record now, decrypt later" attacks are real, particularly for high profile targets, including millions of people - journalists, whistle-blowers, freedom-fighters in oppressive regimes, and even some ordinary people who may become targets of information crimes. Large scale collection of encrypted communication data is ongoing, and this data may be used in the future. So having the solution that _may_ protect you (post-quantum cryptography), as long as it doesn't replace the solution that is _proven_ to protect you (conventional cryptography), is highly beneficial in any communication solution, and has already been deployed in many tools and in some messengers. + +## How secure is end-to-end encryption in different messengers? + +This comparison may be incorrect in some of the columns. We apologize if some of the points are incorrect, please let us know about any mistakes so we can amend them! + +The main objective here is to establish the framework for comparing the security of end-to-end encryption schemes, and to highlight any areas for improvement, not to criticize any implementations. + +![Messengers comparison](./images/20240314-comparison.jpg) + +<sup>1</sup> Repudiation in SimpleX Chat will include client-server protocol from v5.7 or v5.8. Currently it is implemented but not enabled yet, as its support requires releasing the relay protocol that breaks backward compatibility. + +<sup>2</sup> Post-quantum cryptography is available in beta version, as opt-in only for direct conversations. See below how it will be rolled-out further. + +Some columns are marked with a yellow checkmark: +- when messages are padded, but not to a fixed size (Briar pads messages to the size rounded up to 1024 bytes, Signal - to 160 bytes). +- when repudiation does not include client-server connection. In case of Cwtch it appears that the presence of cryptographic signatures compromises repudiation (deniability), but it needs to be clarified. +- when 2-factor key exchange is optional (via security code verification). +- when post-quantum cryptography is only added to the initial key agreement and does not protect break-in recovery. + +## Adding quantum resistance to Signal double ratchet algorithm + +We have been exploring post-quantum cryptography since early 2022, when SimpleX Chat was first released, and we did not want to be pioneers here - cryptography is critically important to make it right. + +We hoped to adopt the algorithm that will be standardized by NIST, but the standardization process turned out to be hugely disappointing, and the ML-KEM (Kyber) algorithm that was accepted as a standard was modified to remove an important hashing step (see the lines 304-314 in [the published spec](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.203.ipd.pdf))), that mitigates the attacks via a compromised random numbers generator, ignoring strong criticism from many expert cryptographers, including DJB (see [this discussion](https://groups.google.com/a/list.nist.gov/g/pqc-forum/c/WFRDl8DqYQ4) and [the comments NIST received](https://csrc.nist.gov/files/pubs/fips/203/ipd/docs/fips-203-initial-public-comments-2023.pdf)). To make it even worse, the calculation of security levels of Kyber appears to have been done incorrectly, and overall, the chosen Kyber seems worse than rejected NTRU according to [the analysis by DJB](https://blog.cr.yp.to/20231003-countcorrectly.html). + +We also analyzed the encryption schemes proposed in Tutanota in 2021, and another scheme adopted by Signal last year, and published the design of [quantum resistant double ratchet algorithm](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/rfcs/2023-09-30-pq-double-ratchet.md) that we believe provides better security than these schemes: + +- unlike Tutanota design, it augments rather than replaces conventional cryptography, and also avoids using signatures when the new keys are agreed (ratchet steps). +- unlike other messengers that adopted or plan to adopt ML-KEM, we used Streamlined NTRU Prime algorithm (specifically, strnup761) that has no problems of ML-KEM, no known patent claims, and seems less likely to be compromised than other algorithms - it is exactly the same algorithm that is used in SSH. You can review the comparison of [the risks of various post-quantum algorithms](https://ntruprime.cr.yp.to/warnings.html). +- unlike Signal design that only added quantum resistance to the initial key exchange by replacing X3DH key agreement scheme with post-quantum [PQXDH](https://signal.org/docs/specifications/pqxdh/), but did not improve Signal algorithm itself, our design added quantum-resistant key agreements inside double algorithm, making its break-in recovery property also quantum resistant. + +The we could make break-in recovery property of Signal algorithm quantum-resistant, and why, probably, Signal didn't, is because irrespective of the message size SimpleX Chat uses a fixed block size of 16kb to provide security and privacy against any traffic observers and against messaging relays. So we had an extra space to accommodate additional ~2.2kb worth of keys in each message without any additional traffic costs. + +In case when the message is larger than the remaining block size, e.g. when the message contains image or link preview, or a large text, we used [zstd compression](https://en.wikipedia.org/wiki/Zstd) to provide additional space for the required keys without reducing image preview quality or creating additional traffic - our previously inefficient JSON encoding of chat messages was helpful in this case. + +<image src="./images/20240314-kem.jpg" alt="Double KEM agreement" width="500" class="float-to-right"> + +The additional challenge in adding sntrup761 was that unlike Diffie-Hellman key exchange, which is symmetric (that is, the parties can share their public keys in any order and the shared secret can be computed from two public keys), sntrup761 is interactive key-encapsulation mechanism (KEM) that requires that one party shares its public key, and another party uses it to encapsulate (which is a fancy term for "encrypt" - that is why it has asterisks in the image) a random shared secret, and sends it back - making it somewhat similar to RSA cryptography. But this asymmetric design does not fit the symmetric operation of Signal double ratchet algorithm, where both sides need to generate random public keys and to compute new shared secrets every time messaging direction changes for them. So to achieve that symmetry we had to use two KEM key agreements running in parallel, in a lock-step fashion, as shown on the diagram. In this case both parties generate random public keys and also use the public key of another party to encapsulate the random shared secret. Effectively, this design adds a double quantum-resistant key agreement to double ratchet algorithm steps that provide break-in recovery. + +## When can you start using quantum resistant chats? + +<img src="./images/20240314-pq1.png" width="288"> <img src="./images/20240314-pq2.png" width="288"> <img src="./images/20240314-pq3.png" width="288"> + +Quantum resistant double ratchet algorithm is already available in v5.6 (beta) of SimpleX Chat as an optional feature that can be enabled for the new and, separately, for the existing direct conversations. + +The reason it is released as opt-in is because once the conversation is upgraded to be quantum resistant, it will no longer work in the previous version of the app, and we see this ability to downgrade the app if something is not working correctly as very important for the users who use the app for critical communications. + +**To enable quantum resistance for the new conversations**: +- open the app settings (tap user avatar in the top left corner). +- scroll down to _Developer tools_ and open them. +- enable _Show developer options_ toggle. +- now you will see _Post-quantum E2EE_ toggle - enable it as well. + +Now all new contacts you add to the app will use quantum resistant double ratchet algorithm. + +Once you have enabled it for the new contacts, you can also **enable it for some of the existing contacts**: +- open the chat with the contact you want to upgrade to be quantum resistant. +- tap contact name above the chat. +- tap Allow PQ encryption. +- exchange several messages back and forth with that contact - the quantum resistant double ratchet will kick in after 3-5 messages (depending on how many messages you send in each direction), and you will see the notice in the chat once it enables. + +## Next for post-quantum crypto - all direct chats, small groups and security audit + +We will be making quantum resistance default for all direct chats in v5.7, and we plan that all existing direct chats will be automatically upgraded when both contacts install v5.7. + +We will also be adding quantum resistance to small groups up to 10-20 members. Computing cryptographic keys is much slower, in comparison, and it would be very inefficient (and completely unnecessary) for large public groups. + +We have also arranged a 3rd party cryptographic review of our protocol and encryption schemes design for June/July 2024 - it will cover the additions to SimpleX protocols since [the previous security audit](./20221108-simplex-chat-v4.2-security-audit-new-website.md) in November 2022, including [XFTP protocol](./20230301-simplex-file-transfer-protocol.md) we use for file transfers and quantum resistant Signal double ratchet algorithm we just released in this beta version. + +In November 2024 we will be conducting further implementation audit, with double the scope of our 2022 audit. + +Security audits are very expensive, as they require employing exceptionally competent engineers and cryptographers, and it does stretch our budgets - so any donations to help us cover the costs would be hugely helpful. + +That's it for now! + +Thank you for helping us improve the app, and look forward to your feedback. + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). + +[How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). + +Please also see our [website](https://simplex.chat). + +## Help us with donations + +Huge thank you to everybody who donates to SimpleX Chat! + +As I wrote, we are planning a 3rd party security audit for the protocols and cryptography design, and also for an app implementation, and it would hugely help us if some part of this $50,000+ expense is covered with donations. + +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 network based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds – any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/tree/master#help-us-with-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder diff --git a/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md b/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md new file mode 100644 index 0000000000..0980eb8896 --- /dev/null +++ b/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md @@ -0,0 +1,159 @@ +--- +layout: layouts/article.html +title: "SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration" +date: 2024-03-23 +previewBody: blog_previews/20240323.html +image: images/20240323-post-preview.png +imageBottom: true +permalink: "/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.html" +--- + +# SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration + +**Published:** Mar 23, 2024 + +SimpleX network: deliver real privacy via a profitable business and non-profit protocol governance: +- [community and business interests are aligned](#community-and-business-interests-are-aligned). +- [the journey to the decentralized non-profit protocol governance](#the-journey-to-the-decentralized-non-profit-protocol-governance). +- [welcome, Esra’a](#welcome-esraa)! + +What's new in v5.6: +- [quantum resistant end-to-end encryption](#quantum-resistant-end-to-end-encryption-beta) (BETA) – enable it for the new contacts. +- [use the app during the audio and video calls](#use-the-app-during-the-audio-and-video-calls). +- [migrate all app data to another device via QR code](#migrate-all-app-data-to-another-device-via-qr-code). + +There are many other improvements and fixes in this release: +- group admins can block a member for all other members. +- filtering chats no longer includes muted chats with unread messages. +- and more - see the [release notes](https://github.com/simplex-chat/simplex-chat/releases/tag/v5.6.0). + +## SimpleX network: deliver real privacy via a profitable business and non-profit protocol governance + +### Community and business interests are aligned + +Some people in our users' community believe that there is a conflict between the business and the community interests. This view fails to see a bigger picture of how these interests are aligned. I wrote a blog post about it [here](https://www.poberezkin.com/posts/2023-10-31-why-privacy-impossible-without-venture-funding.html). Our goal is to grow the network and the ecosystem while enriching all stakeholders involved, both the community and the business shareholders. This strategy relies on the strengths and opportunities of both the business and the decentralized network we build, with its vibrant community of individuals and organizations. It positions the business not as a controller but as a supporter of the community growth, resilience and sustainability. + +By leveraging access to investment resources, its ability to provide stock-based incentives beyond salaries, operating as a business will satisfy users' and community needs more effectively, ensuring the long-term viability and growth of the network, while remaining true to the mission. A purely nonprofit model with no avenue to sustain this growth is simply not possible. Sustainability requires profits, that is simply spending less than earning, as every responsible individual and organization should aim for, and these profits must be based on users' payments, and not on some other sources of revenue or funds. And building the software product that customers are willing to pay for requires substantial business investments. We will accomplish it while maintaining transparency and the commitment to keep SimpleX network and its protocols open and free to use. + +### The journey to the decentralized non-profit protocol governance + +At the current early stage of network growth the protocols will benefit most from the rapid evolution and centralized design. Many protocols that decentralized their evolution too early failed to evolve and to adapt to the product requirements of the majority of users, causing them to stall in growth. Even the open web became possible only thanks to Netscape, a venture funded company, that developed browsers and web server software taking it upon themselves to rapidly evolve web specification to make it into a robust application platform as we now know it. It is very unlikely that it would have happened if it was managed by a nonprofit or community, in a decentralized way. + +Once SimpleX network protocols stabilize and reach the required maturity, we plan to transition its governance to several new nonprofit entities, ensuring its continued evolution aligns more closely with our vision of community-driven, independent and transparent governance. We and our investors believe that this is a win-win strategy. It both maximizes the business value, by creating a product built on protecting privacy and users' trust - a model that we see as the only viable path forward. It also maximizes the value created for the user community. + +### Welcome, Esra’a! + +To help us deliver these goals, Esra’a Al Shafei joined SimpleX Chat team - we are excited to welcome her! + +Esra'a is a longtime privacy and human rights advocate from Bahrain. She is the founder of [Majal.org](http://majal.org), a network of platforms that amplify under-reported and marginalized voices in the Middle East and North Africa. She is also the co-founder of the [Numun Fund](https://numun.fund/), the first dedicated fund for women-led tech in the Global South. Esra’a is currently the Vice Chair of the Board at the [Wikimedia Foundation](https://wikimediafoundation.org/), the nonprofit which hosts Wikipedia. She is also on the Board of the [Tor Project](https://www.torproject.org/), developers of one of the world’s strongest tools for privacy and freedom online. Previously, she served on the Board of [Access Now](https://www.accessnow.org/), an international nonprofit dedicated to an open and free Internet. + +In her own words: *"Privacy and security have been the center of my work as a human rights advocate for the last 20 years in one of the most surveilled countries in the world where staying safe remains a burden. Our privacy is our dignity. It is something that belongs to us and something we should treasure - as people, as companies, as governments. Despite messaging being the most intimate aspect of our digital lives, the leading protocols in this space are centralized and we deserve a web that operates on the core principles of genuine privacy and security in a fully decentralized framework. SimpleX network will play a critical role towards that vision."* + +Executing the plan towards decentralized nonprofit protocol governance will be Esra’a’s primary responsibility. Our primary objective with this plan is to ensure that no single entity can control or have unrestricted ownership of the protocols, so it can maintain its integrity. + +## What's new in v5.6 + +### Quantum resistant end-to-end encryption (BETA) + +Adding post-quantum resistance to the double ratchet end-to-end encryption algorithm in SimpleX Chat is a major upgrade to messaging protocols, and it creates the security against the future quantum computers. + +I wrote in detail in the previous post about various properties of end-to-end encryption and how exactly quantum resistance is implemented in SimpleX Chat - currently SimpleX Chat has [the most secure end-to-end encryption design](./20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md#how-secure-is-end-to-end-encryption-in-different-messengers) against the present and future attacks. + +We plan to perform a security audit of this design and implementation later this year. + +Post-quantum encryption is currently added as opt-in and has to be separately enabled to be active for the new contacts, and, additionally, can be enabled for the existing contacts. + +<img src="./images/20240314-pq1.png" width="288"> <img src="./images/20240314-pq2.png" width="288"> <img src="./images/20240314-pq3.png" width="288"> + +**To enable quantum resistance for the new conversations**: +- open the app settings (tap user avatar in the top left corner). +- scroll down to _Developer tools_ and open them. +- enable _Show developer options_ toggle. +- now you will see _Post-quantum E2EE_ toggle - enable it as well. + +Now all new contacts you add to the app will use quantum resistant Signal double ratchet algorithm. + +Once you have enabled it for the new contacts, you can also **enable it for some of the existing contacts**: +- open the chat with the contact you want to upgrade to be quantum resistant. +- tap contact name above the chat. +- tap Allow PQ encryption. +- exchange several messages back and forth with that contact - the quantum resistant double ratchet will kick in after 3-5 messages (depending on how many messages you send in each direction), and you will see the notice in the chat once it enables. + +We will be making quantum resistance default for all direct chats in v5.7, and we plan that all existing direct chats will be automatically upgraded when both contacts install v5.7. + +### Use the app during the audio and video calls + +<img src="./images/20240323-call1.png" width="220" class="float-to-left"> <img src="./images/20240323-call2.png" width="220" class="float-to-left"> + +Since we added the audio and video calls in 2022 a lot of work was done to improve the stability of calls. One of the usability downsides until this release was that it was impossible to use the app during the call. + +This version solved this problem - now you can return back to the app without interrupting the call and use any of the app functions - for example, you can send the messages or files to the person you are in the call with. If this is a video call, you will continue seeing your contact in a small picture-in-picture view. + +### Migrate all app data to another device via QR code + +We always believed that cross-platform data portability is very important for any software. Users own their data, and they should have a way to export it from any software they use. So from the version of SimpleX Chat [released in July 2022](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#database-export-and-import) it was possible to export the database as a self-contained file, including all contacts, messages and files, and import it on a new device. + +_"How can I migrate my data to another device?"_ was one of the most frequent user questions, but until this release this process was [quite complex](), requiring too many steps, and most non-technical users were not willing to try it, as even though it was reliable, it appeared risky. + +This release made the app data migration very simple, see the steps below. + +**Start the migration on the source device** + +<img src="./images/20240323-migrate-from1.png" width="170"> <img src="./images/arrow.png" width="24"> <img src="./images/20240323-migrate-from2.png" width="170"> <img src="./images/arrow.png" width="24"> <img src="./images/20240323-migrate-from3.png" width="170"> <img src="./images/arrow.png" width="24"> <img src="./images/20240323-migrate-from4.png" width="170"> <img src="./images/arrow.png" width="24"> <img src="./images/20240323-migrate-from5.png" width="170"> + +On the device you migrate your app data from, follow these steps: +- choose _Migrate to another device_, +- if necessary, set the database passphrase (initially, the database is encrypted with a random passphrase), or, if it's already set, verify it - for security and to ensure that the archive will be usable on the new device, +- tap _Archive and upload_ to and upload the app data in chunks to the XFTP servers configured in the app, +- the app will show the QR code that should be scanned from the new device. + +**Migrate data to the target device** + +<img src="./images/20240323-migrate-to1.png" width="170"> <img src="./images/arrow.png" width="24"> <img src="./images/20240323-migrate-to2.png" width="170"> <img src="./images/arrow.png" width="24"> <img src="./images/20240323-migrate-to3.png" width="170"> <img src="./images/arrow.png" width="24"> <img src="./images/20240323-migrate-to4.png" width="170"> <img src="./images/arrow.png" width="24"> <img src="./images/20240323-migrate-to5.png" width="170"> + +On the device you migrate your data to, follow these steps: +- on the first app page choose _Migrate from another device_, +- scan the QR code shown on the source device, +- once the archive downloads, enter the database passphrase and tap _Open chat_, + +**Finalize migration on the source device** + +<img src="./images/20240323-migrate-from6.png" width="170" class="float-to-left"> + +When the chat starts on the target device, make sure to tap _Finalize migration_ and then _Delete database_ on the source device. + +The database cannot be used on two devices at the same time, doing so would make it impossible to decrypt the received messages. This is a security measure - the break-in recovery property of double ratchet algorithm. + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). + +[How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). + +Please also see our [website](https://simplex.chat). + +## Help us with donations + +Huge thank you to everybody who donates to SimpleX Chat! + +As I wrote, we are planning a 3rd party security audit for the protocols and cryptography design, and also for an app implementation, and it would hugely help us if some part of this $50,000+ expense is covered with donations. + +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 network based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds – any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/tree/master#help-us-with-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder diff --git a/blog/20240404-why-i-joined-simplex-chat-esraa-al-shafei.md b/blog/20240404-why-i-joined-simplex-chat-esraa-al-shafei.md new file mode 100644 index 0000000000..e554db5102 --- /dev/null +++ b/blog/20240404-why-i-joined-simplex-chat-esraa-al-shafei.md @@ -0,0 +1,38 @@ +--- +layout: layouts/article.html +title: "Why I joined SimpleX Chat - by Esra'a al Shafei" +date: 2024-04-04 +previewBody: blog_previews/20240404.html +image: images/20240404-esraa.png +permalink: "/blog/20240404-why-i-joined-simplex-chat-esraa-al-shafei.html" +--- + +# Why I joined SimpleX Chat + +**Published:** Apr 4, 2024 + +_By [Esra'a al Shafei](https://mastodon.social/@alshafei)_ + +Transitioning from a lifelong career dedicated to nonprofits, including Board roles at organizations like the Wikimedia Foundation, Access Now and Tor, my decision to join SimpleX Chat may come as a surprise to some. But, as I step into this new chapter, I want to share the insights and convictions that have guided me here, shedding light on what I think sets SimpleX Chat apart and why this move feels like an essential learning opportunity. + +The nonprofit world has been my primary focus for decades. My team and I ran the platforms at Majal.org with an extremely limited budget. We had to navigate many complexities and challenges that shadow the nonprofit model. And because we worked primarily in creating applications and tools, a recurring theme has been financial sustainability. Being a Bahrain-based entity for most of these years meant that the many communities we served were not in a position to provide contributions and we were not eligible for most foundation grants. This drastically limited our growth and the reliability of our apps. When we failed to raise sufficient funds or meet our target budgets, we often had to shutter certain applications, sometimes after spending more than 10 years building them. + +With secure and private messaging, the stakes are even graver. Any failure to commit and resource/fund ongoing development, security patches, etc means lives can be at risk. I still believe in nonprofit models, and it’s why I continue to serve them through various volunteer roles. I do also believe that there is room for a mixture of models that, in the case of something as unique as SimpleX Chat, can serve as a fully open and transparent public interest technology while also having a profitable values-aligned company that can keep the lights on to continue developing, expanding, and improving the protocol, network and their reach. + +I’m no stranger to writing about some VC models being [corrupt](https://mastodon.social/@alshafei/112125959080515656). Frankly, I also hold the view that some tech VCs are amongst the [most complicit](https://responsiblestatecraft.org/defense-tech/) in egregious war crimes worldwide, or enabling the [intrusive surveillance](https://mastodon.social/@alshafei/112140566088322925) we’re fighting against. So being part of a VC-funded venture is not a decision I take lightly. However, I have been following SimpleX Chat’s growth since early 2022 when I first met Evgeny at the Mozilla Festival. I appreciated the drive and Evgeny’s firm refusal to settle for the current models of private messaging. We share the belief that messaging is something we need to keep improving and that we must continue pushing its boundaries to make it even more private, secure, usable for groups, and, most importantly - fully decentralized. This is a major undertaking, and it requires funding to achieve. Candidly, I did worry about funding and sustainability because, at the time, SimpleX was still primarily funded by user contributions. + +But even knowing this, I scrutinized SimpleX Chat for taking VC funding ($350K) from Village Global and questioned the individuals featured on its frontpage. I had to speak with Evgeny directly to learn who exactly from this fund was involved, how much power they wielded, if any, and if this changes the ethos of the company - all of which he is already making public. It was only after these discussions that I was comfortable to take a leap of faith and continue to use the app and vouch for its current and future offerings. It required me to question my own views on whether a VC-funded company can actually have major positive contributions to privacy as well as the open ecosystem. + +<img src="./images/20240404-messsaging-apps.png" class="float-to-right" width="50%"> + +The web has a long history of [trading privacy](https://www.engadget.com/from-its-start-gmail-conditioned-us-to-trade-privacy-for-free-services-120009741.html) for “free” services. Traditionally, these services have also been centralized, closed-source, non-transparent, and profit-oriented. The companies behind these apps and services became prolific because of their disregard of privacy rights, which normalized lucrative surveillance capitalism. There is such an extensive global monopoly that in Africa, only 1 of the 5 biggest messaging apps in Africa isn't owned by Meta, notoriously known for spying not just through its own apps but even through [its competitors](https://qz.com/project-ghostbusters-facebook-meta-wiretap-snapchat-1851366814), – relentless, massive data harvesting that stretches far beyond its own walled gardens: + +Some of the world’s top engineers often go to these companies because of the benefits and financial opportunities. We can question their ethics all day long, but we also need to question if the web would look significantly different if there were as many opportunities at privacy-first companies with purpose and strong, proven moral boundaries, set up in a way that can guarantee operational independence from any shareholders and VCs. + +SimpleX could have taken the route of other companies in the privacy space, whether it’s Skiff which rushed to take a large amount of [VC money](https://techcrunch.com/2022/03/30/skiff-series-a-encrypted-workspaces/) only to [shutter its doors](https://www.techradar.com/computing/cyber-security/skiff-gets-bought-by-notion-raising-privacy-concerns) after an acquisition, leaving its users hanging with many unanswered questions, or giving up control of the company, which would puts its future solely in the hands of VCs with majority ownership. SimpleX aims to prevent this, and in fact has left money on the table to ensure that it does not occur. Had it not been for this information, I would not have joined, and I would have remained a user of the product, albeit a very cautious one, constantly wondering whether it will be sold or corrupted. + +It’s worth noting that some private foundations operate on the VC model in supporting nonprofits, either by requiring Board seats or requesting that their funding be used towards very specific objectives not always in alignment with the organization’s values and mission. It’s also worth noting that [some nonprofits](https://www.engadget.com/2019-05-31-sex-lies-and-surveillance-fosta-privacy.html) actually operate on the models of surveillance and censorship. Therefore, whether an organization or company is VC-backed or a nonprofit should not be the sole factor in deciding whether or not it is trustworthy. Actions are important, with full transparency being one of the most critical factors, and being fully open source being another to attract valid criticisms and audits to ensure any product or protocol lives up to its privacy and security promise. SimpleX Chat prides itself on being both transparent and open, on top of also being fully decentralized. If you’re new to it and eager to know more, you can start with [this overview](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md). + +Another important consideration is that the SimpleX network does have a plan that would rely on users' payments for specific or tailored services, and not on some other sources of revenue or funds (ads, etc.). Building anything that users would be willing to pay for requires substantially more time and resources, hence the VC route to establish a business model that doesn’t translate to the user being the product. But any business services need to be separate from SimpleX as a public interest technology. As outlined in this [recent post](./20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md), I’ll be using my background in nonprofit governance structures to ensure that the SimpleX network protocols evolve under the stewardship of nonprofit entities in various jurisdictions, so that its continued evolution aligns more closely with the vision of community-driven, independent and decentralized governance. This would help create a necessary balance between different structures, in the same way many tech nonprofits also have for-profit subsidiaries to attract fee-for-service agreements to sustain their operations. + +In summary: My decision to join Simplex Chat, despite my deep-rooted beliefs and skepticism towards VC funding, reflects a broader realization: that the fight for privacy, security, and decentralization in today’s web is multifaceted and sometimes requires us to depart from our comfort zones to explore sustainable paths for continuous growth and impact so that open source privacy tools and protocols are no longer “niche”, but universally accessible standards. As long as nothing in this journey compromises our moral principles and integrity, this will remain a very worthwhile goal to pursue. diff --git a/blog/20240416-dangers-of-metadata-in-messengers.md b/blog/20240416-dangers-of-metadata-in-messengers.md new file mode 100644 index 0000000000..b0832af4f7 --- /dev/null +++ b/blog/20240416-dangers-of-metadata-in-messengers.md @@ -0,0 +1,52 @@ +--- +layout: layouts/article.html +title: "The dangers of metadata in messengers" +date: 2024-04-16 +previewBody: blog_previews/20240416.html +image: images/20240416-metadata.png +imageWide: true +permalink: "/blog/20240416-dangers-of-metadata-in-messengers.html" +--- + +# The dangers of metadata in messengers + +**Published:** Apr 16, 2024 + +_By [Esra'a al Shafei](https://mastodon.social/@alshafei)_ + +In many countries around the world, phone numbers are attached to biometrics data and personal IDs. Telecommunications companies are either government owned or are heavily regulated, privately owned monopolies who comply with most government requests for backdoors or user data. The idea that today, we still need to give out our phone numbers as primary identifiers to be able to use the leading messaging apps should be frowned upon and actively challenged. It’s necessary to advocate for private alternatives in messaging that do not rely on user IDs of any kind - and yes, it’s possible. + +Messaging is still not where it needs to be. Privacy is confused with security, when both are not synonymous, and there are major gaps in helping users understand the fundamental differences. + +<img src="./images/20240416-whatsapp.jpg" class="float-to-right" width="38%"> + +For example, while WhatsApp messages are [end-to-end encrypted](https://faq.whatsapp.com/820124435853543), let’s consider what you give up when you use it, per its own listings in app stores: + +- App activity (app interactions, in-app search history, and other user-generated content) +- Location +- Financial information (user payment info and payment history) +- Contacts and their phone numbers +- Groups you’re a member of +- When you use the app and how often you use it +- Device and other IDs +- Personal info (email address, user IDs, phone number) + +This is called [metadata](https://en.wikipedia.org/wiki/Metadata). It reveals a wealth of information about you and your connections, and in the hands of a centralized monopoly, this can and does get misused in incredibly dangerous ways. Once such metadata is logged, it can create very detailed profiles about who you are, everywhere you’ve been, and everyone you’ve ever spoken to. In settling for apps that normalize this while giving you the illusion of privacy in their marketing, we are doing ourselves a disservice by accepting this as the default. Collectively, we aren’t doing enough to protect ourselves and our social graph from this invasive overreach. + +When stored, aggregated and analyzed, this metadata provides ample information that could potentially incriminate someone or be submitted to authorities. When WhatsApp and Facebook Messenger enabled end-to-end encryption for messages, of course it was a welcome and widely celebrated change. But it’s important to remember that not all end-to-end encryption utilizes the same standards, [some implementations are more secure](./20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md#how-secure-is-end-to-end-encryption-in-different-messengers) than others, so it’s something that shouldn’t necessarily be accepted at face value. More importantly: collecting and storing an obscene amount of metadata should invite global scrutiny, considering this data is often combined with whatever other information companies like Meta harvest about your identity (which is [a lot](https://www.vox.com/recode/23172691/meta-tracking-privacy-hospitals).) + +<img src="./images/20240416-metadata.png" class="float-to-right" width="38%"> + +This is one of the many reasons why we need to resist giving out our phone numbers just to access an app, especially to do something as personal and intimate as private messaging. Even though users can sometimes mask their numbers with a username, their identity on the app is still fundamentally tied to their phone number. App operators have access to this, as well as user contacts. Additionally, with a simple modification to the app's source code, the contacts may also gain access in some cases. This should raise more concerns about privacy, and it makes the need for anonymity difficult to achieve. + +Everyone has a different threat model (and if you don’t yet, now is a good time to [create one](https://www.privacyguides.org/en/basics/threat-modeling/#creating-your-threat-model)). For many users today, WhatsApp and other apps may be sufficient for their specific needs, especially in connecting with families and friends who are already on the app and unlikely to migrate elsewhere. If that suits your life and needs, and if you’re aware and consciously accept the risks, great. + +But we also need to acknowledge that the world is becoming increasingly dangerous in the way AI is being used to [supercharge surveillance](https://www.forbes.com/sites/forbestechcouncil/2024/02/02/artificial-intelligence-the-new-eyes-of-surveillance/?sh=cd57bc214f27), and we need to be educated and aware of the risks this is already having on our lives and what it subjects others in your network to when you choose metadata-heavy apps as your primary form of communication. Having alternatives will always be important, even if it’s not what you default to for everyday messaging. Recognize who in your social circles might require the extra privacy, anonymity and security, so that you can play a role in protecting vulnerable individuals who need it most. The messaging app you choose implicates others as well, not just yourself, and while you personally may not require complete privacy, others might have their lives depend on it. + +End-to-end encryption is a solid start, but it's just the beginning of our pursuit for true privacy and security. True privacy means that even when legal demands come knocking, there's no useful metadata to hand over. It's not enough to just protect the content of messages; we need consistent innovation in protecting metadata too. + +Changing ingrained habits is tough, but your privacy is always worth the fight. Although giants like WhatsApp and Telegram may dominate global messaging for now, increasing concerns about data harvesting and AI-driven surveillance are fueling demand for alternatives. SimpleX Chat aims to be one of those strong alternatives, hence its radical focus on a decentralized framework with no user identifiers (in other words, nothing that uniquely identifies users on the protocol level to their contacts or to the relays) and extra optionality (self-hosting an [SMP server](../docs/SERVER.md) or [XFTP server](../docs/XFTP-SERVER.md), access via Tor, [chat profiles](../docs/guide/chat-profiles.md) with incognito mode, etc.) + +As of today, most messaging alternatives, including SimpleX, will have some limitations. But with the limited resources we have, we are committed to daily progress towards creating a truly private messenger that anyone can use while maintaining the features that users have come to know and love in messaging interfaces. We want to be the prime example of a messenger that achieves genuine privacy without compromising it for convenience. We need to be able to reliably move away from small and niche use cases to endorsing and enforcing global standards for privacy and making it accessible for all users regardless of their technical expertise. + +We’re grateful for the users and [donors](https://github.com/simplex-chat/simplex-chat#help-us-with-donations) who have been following along on this journey thus far and helping with feedback, anything from bug reports to identifying potential risks. Building in the open has always been a necessity for transparency and ongoing [auditability](./20221108-simplex-chat-v4.2-security-audit-new-website.md), because we don’t want anyone to just take our word for it. [See for yourself](https://github.com/simplex-chat) and engage in the discussions. We fully expect you to hold us accountable to our word. diff --git a/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md b/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md new file mode 100644 index 0000000000..225c2637d7 --- /dev/null +++ b/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md @@ -0,0 +1,117 @@ +--- +layout: layouts/article.html +title: "SimpleX network: legally binding transparency, v5.7 released with better calls and messages" +date: 2024-04-26 +previewBody: blog_previews/20240426.html +image: images/20240426-profile.png +imageBottom: true +permalink: "/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.html" +--- + +# SimpleX network: legally binding transparency, v5.7 released with better calls and messages + +**Published:** Apr 26, 2024 + +What's new in v5.7: +- [quantum resistant end-to-end encryption](#quantum-resistant-end-to-end-encryption) with all contacts. +- [forward and save messages](#forward-and-save-messages) without revealing the source. +- [in-call sounds and switching sound sources](#in-call-sounds-and-switching-sound-sources). +- [better network connection management](#network-management). +- [customizable profile images](#customizable-shape-of-profile-images) + +Also, we added Lithuanian interface language to the Android and desktop apps, thanks to [our users and Weblate](https://github.com/simplex-chat/simplex-chat#help-translating-simplex-chat). + +## Legally binding transparency + +We are committed to open-source, privacy and security. Here are the recent changes we made: + +- We now have a [Transparency Reports](../docs/TRANSPARENCY.md) page. +- We updated our [Privacy Policy](../PRIVACY.md) to remove undefined terms "impermissible" and "acceptable", which would allow us to remove anything we don't like, without any clarity on what that is. You can see the edits [here](https://github.com/simplex-chat/simplex-chat/pull/4076/files). +- We published a new page with [Frequently Asked Questions](../docs/FAQ.md), thanks to the guidance from users. +- We also have a new [Security Policy](../docs/SECURITY.md) – we welcome your feedback on it. + +What do we mean by “legally binding transparency?”. It includes these principles: +- Accountability: an empty promise or commitment to transparency that is not legally binding is just marketing, and can provide opportunities for the organizations to be misleading or not disclose important information that can affect their users privacy and security. +- Consistency: often, there's a disconnect between marketing claims and legally binding policies. Our approach is to ensure that promises made in marketing materials or any external communications align with our legally binding documents, so that users can rely on our promises, and know exactly what to expect from us. + +For example: +- we use open-source code, and we made a legally binding commitment to use the published code in all released apps and deployed preset servers. +- we use precise and technical language in the Privacy Policy defining what data and metadata can be accessed via the preset relays. + +If you see any inconsistency between technical parameters of SimpleX Network and what is promised in our Privacy Policy please raise it with us. + +## What's new in v5.7 + +This release focus is improving the app usability, and preparing the foundation for v5.8 that will provide an in-built protection of user IP addresses when connecting to unknown file and messaging servers, reducing the need to use Tor (which would still remain supported via SOCKS proxy, for additional privacy). + +### Quantum resistant end-to-end encryption + +<img src="./images/20240426-pq.png" width="288" class="float-right"> + +We [wrote before](./20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md) about how quantum resistant encryption was added to SimpleX Chat and also about other properties of end-to-end encryption, possible attacks on its security and known mitigations. + +Quantum resistant encryption will now be enabled by default in all direct chats. For the new conversations it will be enabled from the beginning, and for the existing conversations it will be agreed after you exchange several messages with your contacts - you will see a notice in the conversation when it happens. + +You can still safely downgrade the app to an earlier version if needed, as v5.6 already supports quantum resistant encryption. + +With the users who have an earlier version, the app will work using the conventional encryption, which is still very secure. It's important that we augmented the conventional encryption with post-quantum cryptographic algorithm rather than replaced it, using a hybrid construction as recommended by the cryptography experts. + +The groups currently do not support quantum resistant encryption yet - we plan to add it in the future to small groups. + +### Forward and save messages + +You can now save received messages to private notes and forward them to your contacts and groups. This is both more convenient than copy-pasting the messages, and also more private - you can forward files and preserve a disappearing file in your private notes without saving them outside of the app. + +You can see and navigate to the original source of the message via the message information, but the recipient of the message can only see that it was forwarded, but not from which conversation - in this way you can show that the message was quoted from another source without revealing the source ([Chatham House Rule](https://en.wikipedia.org/wiki/Chatham_House_rule)). + +<img src="./images/20240426-forward1.png" width="288"> <img src="./images/20240426-forward2.png" width="288"> <img src="./images/20240426-forward3.png" width="288"> + +### In-call sounds and switching sound sources + +This was the most frequent request of the users who use SimpleX Chat for audio and video calls - to add sound indication to the connection progress, as happens in all other apps. This release added these sounds and also vibration when call connects and disconnects. + +You can also switch between bluetooth headphones, speakerphone and earpiece via the new button in the call. + +### Network management + +To reduce traffic and battery usage, this release made connection timeouts dependent on which network your device uses - timeouts will be larger when you are on mobile connection and smaller when on WiFi. It also makes connection attempts very infrequent when the device is not connected to network. + +### Customizable shape of profile images + +To customizable the interface, you can now change the shapes of profile images to anything from squares with sharp corners to circles, as before, via the Appearance menu in the app Settings. The default shape of profile images is changed to squares with rounded corners. + +<img src="./images/20240426-profile1.png" width="288"> <img src="./images/20240426-profile2.png" width="288"> <img src="./images/20240426-profile3.png" width="288"> + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). + +[Frequently asked questions](../docs/FAQ.md). + +Please also see our [website](https://simplex.chat). + +## Help us with donations + +Huge thank you to everybody who donates to SimpleX Chat! + +We are planning a 3rd party security audit for the protocols and cryptography design in July 2024, and also the security audit for an implementation in December 2024/January 2025, and it would hugely help us if some part of this $50,000+ expense is covered with donations. + +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 network based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds – any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/tree/master#help-us-with-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder diff --git a/blog/20240516-simplex-redefining-privacy-hard-choices.md b/blog/20240516-simplex-redefining-privacy-hard-choices.md new file mode 100644 index 0000000000..30c8a89e14 --- /dev/null +++ b/blog/20240516-simplex-redefining-privacy-hard-choices.md @@ -0,0 +1,79 @@ +--- +layout: layouts/article.html +title: "SimpleX: Redefining Privacy by Making Hard Choices" +date: 2024-05-16 +previewBody: blog_previews/20240516.html +image: images/simplex-explained.png +imageWide: true +permalink: "/blog/20240516-simplex-redefining-privacy-hard-choices.html" +--- + +# SimpleX: Redefining Privacy by Making Hard Choices + +**Published:** May 16, 2024 + +When it comes to open source privacy tools, the status quo often dictates the limitations of existing protocols and structures. However, these norms need to be challenged to radically shift how we approach genuinely private communication. This requires doing some uncomfortable things, like making hard choices as it relates to funding, alternative decentralization models, doubling down on privacy over convenience, and more. + +There will always be questions on why the SimpleX Chat and network makes the choices it makes, and that’s good! It’s important to question us and to understand the reasoning behind each decision, whether it’s technical, structural, financial or any other. + +In this post we explain a bit more about why SimpleX operates and makes decisions the way it does. + +## No user accounts + +Within SimpleX network there are no user accounts, and more importantly, no user profile identifiers whatsoever at the protocol level, not even random numbers or cryptographic keys used to identify the users. This means there is absolutely nothing that uniquely links users to their contacts or to the network relays. While it's accurate to say, "You need an address to send something," it's crucial to understand that this "address" serves merely as a transient delivery destination, and not as a user profile identifier in any sense. + +You can read more about how SimpleX works [here](https://simplex.chat/#how-simplex-works). + +## Privacy over convenience + +One of the main considerations often ignored in security and privacy comparisons between messaging applications is multi-device access. For example, in Signal’s case, the Sesame protocol used to support multi-device access has the vulnerability that is [explained in detail here](https://eprint.iacr.org/2021/626.pdf): + +_"We present an attack on the post-compromise security of the Signal messenger that allows to stealthily register a new device via the Sesame protocol. [...] This new device can send and receive messages without raising any ‘Bad encrypted message’ errors. Our attack thus shows that the Signal messenger does not guarantee post-compromise security at all in the multi-device setting"_. + +<img src="./images/20231125-mobile2.png" width="170" class="float-right"> + +Solutions are possible, and even the quoted paper proposes improvements, but they are not implemented in any existing communication solutions. Unfortunately this results in most communication systems, even those in the privacy space, having compromised security in multi-device settings due to these limitations. That's the reason we are not rushing a full multi-device support, and currently only provide [the ability to use mobile app profiles via the desktop app](./20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md#link-mobile-and-desktop-apps-via-secure-quantum-resistant-protocol), while they are on the same network. + +Another choice that compromises privacy for convenience and usability is 3rd party push notifications. At SimpleX, we take a slow path of optimizing the network and battery consumption in the app, rather than simply hiding inefficiencies behind the quick fix solution of 3rd party push notifications that [increases vulnerability](https://www.wired.com/story/apple-google-push-notification-surveillance/), a path Signal and others chose. Like other choices, it has usability and optimization trade offs, but ultimately it’s the right thing to continue progressing towards a better solution as we explain [here](https://simplex.chat/blog/20220404-simplex-chat-instant-notifications.html). + +Whenever possible, we strive to achieve significantly higher levels of privacy and security. For example, unlike most, if not all, applications (including Signal), [we encrypt application files with per-file unique key](https://simplex.chat/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html#encrypted-local-files-and-media-with-forward-secrecy). Consequently, once a message is deleted, there's no means to open a file that someone may have stolen in hopes of acquiring the key later. Similarly, apps like Session have done away with forward secrecy, a decision which caused them [not to be recommended](https://www.privacyguides.org/en/real-time-communication/#additional-options) for "long-term or sensitive communications". And [misinformation](https://simplifiedprivacy.com/spain-has-banned-telegram-defending-session/) around this makes it dangerous and irresponsible to recommend without such necessary disclosures for people’s awareness. + +Session’s decision was based on [the incorrect statements](https://getsession.org/blog/session-protocol-explained) about double ratchet being impossible in decentralized networks, and underplayed importance of forward secrecy, break-in recovery and deniability - the absence of these crucial qualities makes Session a much weaker choice for private messaging. For transparency, this was something that was debated with their team [here](https://twitter.com/SimpleXChat/status/1755216356159414602). We also made [a separate post](./20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md#end-to-end-encryption-security-attacks-and-defense) about these qualities of end-to-end encryption and their presence in different messengers, to show that not all end-to-end encrypted apps offer the same level of protections. + +## Network decentralization + +<img src="./images/simplex-explained.svg" width="40%" class="float-right"> + +It's important to recognize that a model of decentralization where all servers are openly known and accessible to all clients, that some users ask for, actually results in a less decentralized network, and as the network grows it often requires an introduction of a central authority to protect from bad actors with malicious intent. Therefore, we've deliberately opted for a slower path towards achieving a higher degree of decentralization where there is no central server registry or network authority. For example, p2p designs may offer higher initial decentralization but often compromise on privacy and eventual decentralization. In essence, our approach prioritizes a balance between initial decentralization, privacy, and higher degree of decentralization down the line. + +Additionally, while it's true that we haven't yet established a model to incentivize other network operators, it's certainly on the roadmap. We see the decentralization of network operators offered within the app as a top priority.  + +Where it stands today, users have the freedom to select their preferred servers within the SimpleX network by configuring the app, with thousands of self-hosted servers in operation. Moreover, numerous third-party applications rely on our code for their in-app communications, operating independently of our servers, many of which we may not even be aware of. + +Decentralization is an ongoing journey, and we strive to proceed at a measured pace to ensure its proper implementation. While the immediate results may not always appear ideal, prioritizing a careful approach ensures that in the long run, the decisions made in this area align with our ultimate objectives of a private, efficient, reliable and fully decentralized network. + +## Funding and profitability + +We explain our rationale for funding [here](../docs/FAQ.md#funding-and-business-model). Funding sources is always one of the most difficult choices to make, and it’s important to underline that VC models don’t necessarily translate to a quest for control, interference of any kind, or overall influence on product roadmap and strategy. The vast majority of investors seek profitability. Irrespective of the organization type profitability is essential for a sustainable operation, and it can and should be done while adhering to the highest possible standards for privacy. For-profit vs. nonprofit is also not an accurate metric to measure a commitment towards privacy and open standards, which is further explained [here](./20240404-why-i-joined-simplex-chat-esraa-al-shafei.md).   + +To make a profit, satisfying customers is the key. Unlike the many companies that profit from selling customer data, we put user privacy first. Doing this at scale requires investments. If the investors don’t own or control a company, their participation becomes merely about profit for them, and not about how this profit is obtained. With the investors we have, we are completely aligned on this - they are betting on the future where privacy is the norm. They do not dictate on anything related to our model. We build SimpleX chat, protocols and network the way Internet should have been built if we as developers always put the privacy and empowerment of users first. + +## Company jurisdiction + +<img src="./images/20240516-parliament.jpg" width="40%" class="float-right"> + +With regards to jurisdictions, nowhere is perfect. For that reason we plan to establish the foundations for protocol governance in [various jurisdictions](https://simplex.chat/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.html#the-journey-to-the-decentralized-non-profit-protocol-governance). + +But we’d like to clarify some misconceptions about the UK, where SimpleX Chat Ltd. is registered, and the UK legislation. + +For example, the Online Safety Act (OSA). Some people believe that it applies only to UK companies. But the OSA applicability isn’t determined by the company’s jurisdiction - it applies based on the nature and characteristics of the business and its services, as well as the number of its users in the UK. In case of SimpleX network, the OSA doesn’t apply for both of these reasons. + +The UK’s position on communication encryption, and more specifically, on end-to-end encrypted messaging, remains the subject of political debates. But with the OSA, the legislative intent was to propose technical measures to block CSAM, and it was trying to explore ways to do this via client-side scanning, which of course would undermine the encryption. However, and thanks to the hard work of privacy experts, researchers, academics and rights organizations throughout the UK and the rest of the world, the Online Safety Bill did not prohibit end-to-end encrypted apps without such scanners. It is an open question whether such technology will ever be possible, and the UK government made a public commitment that client-side scanning won't be required until it is. + +For now, strong end-to-end encryption remains permissible and protected, and we hope to also add to the privacy advocacy and debates as a UK-based company to keep it legally protected. + +Overall, we view the UK as being better jurisdiction for privacy than many alternatives - there are some trade-offs everywhere. + +## Looking ahead  + +The future of the Internet should be based on decentralized infrastructure operated by commercially viable organizations. These operators need to possess minimal user data, so that users have genuine control over their identities, and free from lock-in by the operators, to support fair competition. This requires a drastic re-imagining of the current norms and newer, more privacy-minded protocols. All in all, private messaging is surrounded by very difficult challenges but it’s worth it to keep pushing the industry forward and not settle for the status quo and current trade offs, protocol limitations and vulnerabilities. The Internet deserves better standards, and so do users. diff --git a/blog/20240601-protecting-children-safety-requires-e2e-encryption.md b/blog/20240601-protecting-children-safety-requires-e2e-encryption.md new file mode 100644 index 0000000000..39a047f93f --- /dev/null +++ b/blog/20240601-protecting-children-safety-requires-e2e-encryption.md @@ -0,0 +1,32 @@ +--- +layout: layouts/article.html +title: "Protecting Children's Safety Requires End-to-End Encryption" +date: 2024-06-01 +previewBody: blog_previews/20240601.html +image: images/20240601-eu-privacy.png +permalink: "/blog/20240601-protecting-children-safety-requires-e2e-encryption.html" +--- + +# Protecting Children's Safety Requires End-to-End Encryption + +As lawmakers grapple with the serious issue of child exploitation online, some proposed solutions would fuel the very problem they aim to solve. Despite expert warnings, the Belgian Presidency persists in pushing for the implementation of client-side scanning on encrypted messaging services, rebranding the effort as "upload moderation". Their [latest proposal](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=COM%3A2022%3A209%3AFIN&qid=1652451192472) mandates that providers of private communication services obtain user consent for AI-based scanning of their private chats. If users do not consent, they will be prohibited from sharing images, videos, and URLs. + +Privacy critics have long pushed for measures like centralized scanning of private photos and messaging data, arguing it could detect illicit content. However, invasive monitoring of private communications would create detrimental risks that far outweigh any perceived benefits. + +## Why we’re taking action + +SimpleX Chat signed a [joint statement](https://www.globalencryption.org/2024/05/joint-statement-on-the-dangers-of-the-may-2024-council-of-the-eu-compromise-proposal-on-eu-csam/) about the dangers of the EU compromise proposal on EU CSAM because maintaining end-to-end encryption is crucial for protecting privacy and security for everyone, including and especially children.  + +We urge the Ministers in the Council of the EU to stand firm against any scanning proposals that undermine end-to-end encryption, which would enable mass surveillance and misuse by bad actors, whether framed as client-side scanning, upload moderation, or any other terminology. Compromising this basic principle opens the door to devastating privacy violations. We also urge any organizations or individuals reading this to write to their representatives and voice their concerns. European Digital Rights has [outlined these issues](https://edri.org/our-work/be-scanned-or-get-banned/) in greater detail for anyone seeking more information. + +## Why compromising privacy endangers children + +The core issue is that compromising encryption and privacy makes innocent people vulnerable to malicious hackers and criminals seeking to exploit users data. Centralized scanning systems become a tempting target, potentially exposing millions of private family photos when breached. This would easily open up avenues for blackmail, abuse, and victimization of children. A case in point is the recent [criminal charges](https://techcrunch.com/2024/01/17/unredacted-meta-documents-reveal-historical-reluctance-to-protect-children-new-mexico-lawsuit/) against Meta in New Mexico, which highlights how the tech giant's algorithms enabled child exploitation by encouraging connections between minors and sexual predators. Privacy-eroding initiatives like client-side scanning would play into the hands of malicious actors by making more sensitive information accessible and weaponized in the same way that it has been on Meta platforms. + +## What should be done + +Rather than undermining privacy, to achieve child safety online users should be empowered with high standards for encryption and data control. For example, adopting a model where children (and users in general) cannot be discovered or approached on networks unless they or their parents permit it, similar to the SimpleX network privacy model. Intelligent multi-device synchronization could enable this oversight without compromising end-to-end encryption overall. It’s always possible to protect children without opening everyone, especially children themselves, to greater vulnerabilities due to such proposals. + +However, some recent legislative efforts have bizarrely moved in the opposite direction by seeking to limit parental access. The chilling truth is that the least private platforms have been major enablers of child exploitation. Eroding privacy protections on other services will only aid criminals further, not protect children. Preserving strong encryption and user privacy must be the foundation for any credible effort to combat online child exploitation. Initiatives trading privacy for supposed safety are not just technically flawed, but would achieve the exact opposite of their stated intent. We must avoid being gaslighted by narratives that defy logic, and instead provide users with the highest possible standards for privacy protections as a core principle. + +Protecting end-to-end encryption without carving out backdoors or vulnerabilities should be non-negotiable for children's and everyone’s safety. It is critical to redirect the discourse to focus on taking genuine privacy further by protecting against [metadata hoarding](https://simplex.chat/blog/20240416-dangers-of-metadata-in-messengers.html) and other means by which people’s data can be abused or subjected to surveillance. diff --git a/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md b/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md new file mode 100644 index 0000000000..e06f7c2084 --- /dev/null +++ b/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md @@ -0,0 +1,171 @@ +--- +layout: layouts/article.html +title: "SimpleX network: private message routing, v5.8 released with IP address protection and chat themes" +date: 2024-06-04 +previewBody: blog_previews/20240604.html +image: images/20240604-routing.png +imageBottom: true +permalink: "/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html" +--- + +# SimpleX network: private message routing, v5.8 released with IP address protection and chat themes + +**Published:** June 4, 2024 + +What's new in v5.8: +- [private message routing](#private-message-routing). +- [server transparency](#server-transparency). +- [protect IP address when downloading files & media](#protect-ip-address-when-downloading-files--media). +- [chat themes](#chat-themes) for better conversation privacy - in Android and desktop apps. +- [group improvements](#group-improvements) - reduced traffic and additional preferences. +- improved networking, message and file delivery. + +Also, we added Persian interface language to the Android and desktop apps, thanks to [our users and Weblate](https://github.com/simplex-chat/simplex-chat#help-translating-simplex-chat). + +## Private message routing + +### What's the problem? + +<img src="./images/simplex-explained.svg" width="37%" class="float-right"> + +SimpleX network design has always been focussed on protecting user identity on the messaging protocol level - there is no user profile identifiers of any kind in the protocol design, not even random numbers or cryptographic keys. + +Until this release though, SimpleX network had no built-in protection of user transport identities - IP addresses. As previously the users could only choose which messaging relays to use to receive messages, these relays could observe the IP addresses of the senders, and if these relays were controlled by the recipients, the recipients themselves could observe them too - either by modifying server code or simply by tracking all connecting IP addresses. + +To work around this limitation, many users connected to SimpleX network relays via Tor or VPN - so that the recipients' relays could not observe IP addresses of the users when they send messages. Still, it was the most important and the most criticized limitation of SimpleX network for the users. + +### Why didn't we just embed Tor in the app? + +Tor is the best transport overlay network in existence, and it provides network anonymity for millions of Internet users. + +SimpleX Chat has many integration points with Tor: +- it allows [dual server addresses](./20220901-simplex-chat-v3.2-incognito-mode.md#using-onion-server-addresses-with-tor), when the same messaging relay can be reached both via Tor and via clearnet. +- it utilises Tor's SOCKS proxy "isolate-by-auth" feature to create a new Tor circuit for each user profile, and with an additional option - for each contact. Per-contact [transport isolation](./20230204-simplex-chat-v4-5-user-chat-profiles.md#transport-isolation) is still experimental, as it doesn't work if you connect to groups with many members, and it's only available if you enable developer tools. + +Many SimpleX network design ideas are borrowed from Tor network design: +- mitigation of [MITM attack](../docs/GLOSSARY.md#man-in-the-middle-attack) on client-server connection is done in the same way as Tor relays do it - the fingerprint of offline certificate is included in server address and validated by the client. +- the private routing itself uses the approach similar to onion routing, by adding encryption layers on each hop. +- we are also considering to implement Tor's [Proof-of-work DoS defence](https://blog.torproject.org/introducing-proof-of-work-defense-for-onion-services/) mechanism. + +So why didn't we just embed Tor into the messaging clients to provide IP address protection? + +We believe that Tor may be the wrong solution for some users for one of the reasons: +- much higher latency, error rate and resource usage. +- people who want to use Tor are better served by specialized apps, such as [Orbot](https://guardianproject.info/apps/org.torproject.android/). +- Tor usage is restricted in some networks, so it would require complex configuration in the app UI. +- some countries have legislative restrictions on Tor usage, so embedding Tor would require supporting multiple app versions, and it would leave the original problem unsolved in these countries. + +Also, while Tor solves the problem of IP address protection, it doesn't solve the problem of meta-data correlation by user's transport session. When the client connects to the messaging relays via Tor, the relays can still observe which messaging queues a user sends messages to via a single TCP connection. The client can mitigate it with per-contact transport isolation, but it uses too much traffic and battery for most users. + +So we believed we would create more value to the users of SimpleX network with private message routing. This new message routing protocol provides IP address and transport session protection out of the box, once released. It can also be extended to support delayed delivery and other functions, improving both usability and transport privacy in the future. + +At the same time, we plan to continue supporting Tor and other overlay networks. Any overlay network that supports SOCKS proxy with "isolate-by-auth" feature will work with SimpleX Chat app. + +### What is private message routing and how does it work? + +Private message routing is a major milestone for SimpleX network evolution. It is a new message routing protocol that protects both users' IP addresses and transport sessions from the messaging relays chosen by their contacts. Private message routing is, effectively, a 2-hop onion routing protocol inspired by Tor design, but with one important difference - the first (forwarding) relay is always chosen by message sender and the second (destination) - by the message recipient. In this way, neither side of the conversation can observe IP address or transport session of another. + +At the same time, the relays chosen by the sending clients to forward the messages cannot observe to which connections (messaging queues) the messages are sent, because of the additional layer of end-to-end encryption between the sender and the destination relay, similar to how onion routing works in Tor network, and also thanks to the protocol design that avoids any repeated or non-random identifiers associated with the messages, that would otherwise allow correlating the messages sent to different connections as sent by the same user. Each message forwarded to the destination relay is additionally encrypted with one-time ephemeral key, to be independent of messages sent to different connections. + +The routing protocol also prevents the possibility of MITM attack by the forwarding relay, which provides the certificate the session keys of the destination server to the sending client that are cryptographically signed by the same certificate that is included in destination server address, so the client can verify that the messages are sent to the intended destination, and not intercepted. + +The diagram below shows all the encryption layers used in private message routing: + +``` +----------------- ----------------- -- TLS -- ----------------- ----------------- +| | -- TLS -- | | -- f2d -- | | -- TLS -- | | +| | -- s2d -- | | -- s2d -- | | -- d2r -- | | +| Sending | -- e2e -- | sender's | -- e2e -- | recipient's | -- e2e -- | Receiving | +| client | message -> | Forwarding | message -> | Destination | message -> | client | +| | -- e2e -- | relay | -- e2e -- | relay | -- e2e -- | | +| | -- s2d -- | | -- s2d -- | | -- d2r -- | | +| | -- TLS -- | | -- f2d -- | | -- TLS -- | | +----------------- ----------------- -- TLS -- ----------------- ----------------- +``` + +**e2e** - two end-to-end encryption layers between **sending** and **receiving** clients, one of which uses double ratchet algorithm. These encryption layers are present in the previous version of message routing protocol too. + +**s2d** - encryption between the **sending** client and recipient's **destination** relay. This new encryption layer hides the message metadata (destination connection address and message notification flag) from the forwarding relay. + +**f2d** - additional new encryption layer between **forwarding** and **destination** relays, protecting from traffic correlation in case TLS is compromised - there are no identifiers or cyphertext in common between incoming and outgoing traffic of both relays inside TLS connection. + +**d2r** - additional encryption layer between destination relay and the recipient, also protecting from traffic correlation in case TLS is compromised. + +**TLS** - TLS 1.3 transport encryption. + +For private routing to work, both the forwardig and the destination relays should support the updated messaging protocol - it is supported from v5.8 of the messaging relays. It is already released to all relays preset in the app, and available as a self-hosted server. We updated [the guide](../docs/SERVER.md) about how to host your own messaging relays. + +Because many self-hosted relays did not upgrade yet, private routing is not enabled by default. To enable it, you can open *Network & servers* settings in the app and change the settings in *Private message routing* section. We recommend setting *Private routing* option to *Unprotected* (to use it only with unknown relays and when not connecting via Tor) and *Allow downgrade* to *Yes* (so messages can still be delivered to the messaging relays that didn't upgrade yet) or to *When IP hidden* (in which case the messages will fail to deliver to unknown relays that didn't upgrade yet unless you connect to them via Tor). +See [F.A.Q. section](../docs/FAQ.md#does-simplex-protect-my-ip-address) for answers about private message routing. + +Read more about the technical design of the private message routing in [this document](https://github.com/simplex-chat/simplexmq/blob/stable/rfcs/done/2023-09-12-second-relays.md) and in [the messaging protocol specification](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md#proxying-sender-commands). + +## Server transparency + +<img src="./images/20240604-server.png" width="40%" class="float-to-right"> + +Even with very limited information available to the messaging relays, there are [several things](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#simplex-messaging-protocol-server) that would reduce users' privacy that a compromised relay can do. + +We [wrote previously](https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2024-03-20-server-metadata.md) that it is important that server operators commit to running unmodified server code or disclose any code modifications, and also disclose server ownership and any other relevant information. + +While we cannot require the operators of self-hosted and private servers to disclose any information about them (apart from which server code they use - this is the requirement of the AGPLv3 license to share this information with users connecting to the server), as we add other server operators to the app, it is important for the users to have all important information about these operators and servers location. + +This server release adds server information page where all this information can be made available to the users. For example, this is <a href="https://smp8.simplex.im" target="_blank">the information</a> about one of the servers preset in the app. + +The updated server guide also includes [the instruction](../docs/SERVER.md#) about how to host this page for your server. It is generated as a static page when the server starts. We recommend using Caddy webserver to serve it. + +## More new things in v5.8 + +### Protect IP address when downloading files & media + +This version added the protection of your IP address when receiving files from unknown file servers without Tor. Images and voice messages won't automatically download from unknown servers too until you tap them, and confirm that you trust the file server where they were uploaded. + +### Chat themes + +<img src="./images/20240604-theme1.png" width="244" class="float-to-right"> <img src="./images/20240604-theme2.png" width="244" class="float-to-right"> + +In Android and desktop app you can now customize how the app looks by choosing wallpapers with one of the preset themes or choose your own image as a wallpaper. + +But this feature is not only about customization - it allows to set different colors and wallpaper for different user profiles and even specific conversations. You can also choose different themes for different chat profiles. + +In case you use different identities for different conversations, it helps avoiding mistakes. + +### Group improvements + +This version adds additional group configuration options to allow sending images, files and media, and also SimpleX links only to group administrators and owners. So with this release group owners can have more control over content shared in the groups. + +We also stopped unnecessary traffic caused by the members who became inactive without leaving the groups - it should substantially reduce traffic and battery consumption to the users who send messages in large groups. + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). + +[Frequently asked questions](../docs/FAQ.md). + +Please also see our [website](https://simplex.chat). + +## Help us with donations + +Huge thank you to everybody who donates to SimpleX Chat! + +We are planning a 3rd party security audit for the protocols and cryptography design in July 2024, and also the security audit for an implementation in December 2024/January 2025, and it would hugely help us if some part of this $50,000+ expense is covered with donations. + +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 network based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds – any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/tree/master#help-us-with-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder diff --git a/blog/20240704-future-of-privacy-enforcing-privacy-standards.md b/blog/20240704-future-of-privacy-enforcing-privacy-standards.md new file mode 100644 index 0000000000..681242e044 --- /dev/null +++ b/blog/20240704-future-of-privacy-enforcing-privacy-standards.md @@ -0,0 +1,51 @@ +--- +layout: layouts/article.html +title: "The Future of Privacy: Enforcing Privacy Standards" +date: 2024-07-04 +previewBody: blog_previews/20240704.html +image: images/20240704-privacy.jpg +imageWide: true +permalink: "/blog/20240704-future-of-privacy-enforcing-privacy-standards.html" +--- + +# The Future of Privacy: Enforcing Privacy Standards + +**Published:** Jul 4, 2024 + +Recent anti-privacy legislations and proposals in [Europe](https://www.theverge.com/2024/6/19/24181214/eu-chat-control-law-propose-scanning-encrypted-messages-csam), [the US](https://theconversation.com/section-702-foreign-surveillance-law-lives-on-but-privacy-fight-continues-229253) and [Australia](https://www.theguardian.com/technology/article/2024/jun/20/meredith-walker-signal-boss-government-encryption-laws) threaten to infringe our fundamental right to privacy and to create grave risks to the [safety](https://simplex.chat/blog/20240601-protecting-children-safety-requires-e2e-encryption.html) of children and vulnerable people. It's time we shift the focus: privacy should be a non-negotiable duty of technology providers, not just a right users must constantly fight to protect, and not something that users can be asked to consent away as a condition of access to a service. + +Tech giants are trying to normalize surveillance capitalism, often with little to no consequences globally. These companies are contributing to a growing ecosystem where opting out of invasive data hoarding practices is becoming increasingly challenging, if not outright impossible. We are being gaslit by the technology executives who try to justify profiteering from AI theft, from [Microsoft](https://www.computing.co.uk/news/4330395/microsoft-ai-chief-makes-questionable-claims-about-copyright-online-content) claiming all our content is fair game for their exploitation to unethical startups like [Perplexity](https://www.theverge.com/2024/6/27/24187405/perplexity-ai-twitter-lie-plagiarism) turning the word “[privacy](https://x.com/perplexity_ai/status/1789007907092066559)” into a marketable farce. + +## The AI Hype’s Impact on Privacy + +The exaggeration of AI’s actual capabilities and the continuous promotion of its “intelligence” is creating a rat race where tech companies and well-funded startups are evading accountability, as they eagerly collect and exploit more data than ever.  + +They're prioritizing AI development over user privacy and rights, setting a dangerous precedent for current and future online engagements. They've already normalized the use of AI to scan and analyze supposedly private communications - from emails to instant messages - repackaging this intrusion as "productivity tools”. Meanwhile, most consumers actually want [more data privacy](https://iapp.org/news/a/most-consumers-want-data-privacy-and-will-act-to-defend-it), not less, and are increasingly concerned by the lack of it. + +The legal push towards “client-side scanning”, attacks on end-to-end encryption and the support for pro-surveillance legislation gives credibility to these highly intrusive practices that literally endanger lives. And we know that moral obligations mean nothing to corporations benefiting from these exploitative models, so we have to ensure that our demands for privacy are legally enforceable and non-negotiable.  + +## Legal Action + +We are encouraged to see more legal pressure on companies that exploit user data on a daily basis. For example, the European Center for Digital Rights’ (Noyb) [complaints](https://noyb.eu/en/noyb-urges-11-dpas-immediately-stop-metas-abuse-personal-data-ai) against Meta’s abuse of personal data to train their AI and, and the demands from the Norwegian Consumer Council to data protection authority to ensure that applicable laws are enforced against Meta considering there is “[no way to remove personal data from AI models once Meta has begun the training](https://www.forbrukerradet.no/side/legal-complaint-against-metas-use-of-personal-content-for-ai-training/)”. + +Noyb is taking a strong stance against [other companies](https://noyb.eu/en/project/cases) with similar exploitative models, including facial recognition surveillance tools often misused by law enforcement agencies. Consider [supporting](https://noyb.eu/en/support) their ongoing efforts — we strongly believe legal action is one of the most effective means to hold these companies accountable for their persistent abuses, which are otherwise shielded by heavily funded self-serving lobby groups. + +## Privacy as a Legal Obligation + +We must shift from a defensive stance to a proactive one by proposing privacy legislation that puts users in direct control of their private data. + +This legislation should: +1. Establish non-negotiable provider duties for protecting user privacy, with hefty fines and consequences for service operators who do not comply. +2. Prevent providers from circumventing these duties through user consent clauses — it should be legally prohibited to ask for a consent to share user data or to use it for anything other than providing a service. +3. Prevent providers from asking for any more personal information from the users than technically necessary and legally required. For example, asking for a phone number as a condition of access to a service should be made illegal in most cases — it does not provide a sufficient security, exposes users' private information and allows simple aggregation of users' data across multiple services. +4. Create a strong legal framework that cannot be resisted or modified + +By codifying these principles into law, we can establish a strong technological framework that is built to create more value for end users, while protecting their privacy against data exploitation, criminal use and identity theft. We will continue the fight against illogical legislative proposals designed to normalize mass surveillance, but our efforts should equally gear towards creating and supporting new models and technological foundations that bring us far closer to the reality we urgently need. + +## Collective Action + +There is great work being done by advocacy organizations, and service providers need to contribute to this fight as well by shifting the narrative and reclaiming the term “privacy” from the tech giants who co-opted and corrupted it. We must play a bigger role in supporting users in setting stronger boundaries, making demands, and refusing anything less than genuine privacy and data ownership, while getting comfortable with holding providers accountable for any violations. + +Privacy should be seen as a fundamental obligation of technology providers, and legislators must actively enforce this expectation. The more consumers make this demand, the more pressure we put on anti-privacy lobbyists with rogue motives, the easier it will be to hold abusers accountable, and the more likely we can collectively ensure that a privacy-first web becomes a reality. + +You can support privacy today by signing [the petition](https://www.globalencryption.org/2024/05/joint-statement-on-the-dangers-of-the-may-2024-council-of-the-eu-compromise-proposal-on-eu-csam/) prepared by Global Encryption Coalition in support of communication privacy. You can also write to your elected representatives, explaining them how data privacy and encrypted communications protect children safety and reduce crime. diff --git a/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md b/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md new file mode 100644 index 0000000000..de9e33a87e --- /dev/null +++ b/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md @@ -0,0 +1,247 @@ +--- +layout: layouts/article.html +title: "SimpleX network: the investment from Jack Dorsey and Asymmetric, v6.0 released with the new user experience and private message routing." +date: 2024-08-14 +image: images/20240814-reachable.png +previewBody: blog_previews/20240814.html +permalink: "/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html" +--- + +# SimpleX network: the investment from Jack Dorsey and Asymmetric, v6.0 released with the new user experience and private message routing. + +**Published:** Aug 14, 2024 + +[SimpleX Chat: vision and funding 2.0](#simplex-chat-vision-and-funding-20): +- [The past](#the-past-investment-from-village-global): investment from Village Global. +- [The present](#the-present-announcing-the-investment-from-jack-dorsey-and-asymmetric): announcing the investment from Jack Dorsey and Asymmetric Capital Partners. +- [The future](#the-future-faster-development-and-transition-to-non-profit-governance): faster development and the path to non-profit governance. + +[What's new in v6.0](#whats-new-in-v60): +- Private message routing — now enabled by default. +- [New chat experience](#new-chat-experience): + - connect to your friends faster. + - [new reachable interface](#new-reachable-interface). + - archive contacts to chat later. + - new way to start chat. + - [moderate like a pro](#moderate-like-a-pro): delete many messages at once. + - new chat themes<sup>*</sup> + - increase font size<sup>**</sup>. +- [New media options](#new-media-options): + - play from the chat list. + - blur for better privacy. + - [share from other apps](#share-from-other-apps)<sup>*</sup>. +- [Improved networking and reduced battery usage](#improved-networking-and-reduced-battery-usage) + +\* New for iOS app. + +\*\* Android and desktop apps. + +## SimpleX Chat: vision and funding 2.0 + +### The past: investment from Village Global + +Last year [we announced](https://simplex.chat/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.html#how-is-it-funded-and-what-is-the-business-model) pre-seed funding from several angel investors and Village Global. Some of our users were very excited that we have funds to continue developing SimpleX network. But as some of Village Global LPs (Limited Partners) are [the founders of very large technology companies](https://www.villageglobal.vc), some of our users were worried about any negative influence this investment might have on the project. + +[Ben Casnocha](https://www.villageglobal.vc/team/ben-casnocha), the founder and general partner of Village Global, commented on their investment: + +> I believe in SimpleX Chat vision and team’s ability to execute it. The growing number of Internet users who demand privacy of their data and contacts will make SimpleX Chat profitable, which is critically important for any sustainable organization. +> +> We are fortunate to have LPs who founded many iconic Internet ventures. But they don’t have any influence on the 400+ companies we invested in. They are financial investors in our fund and exert no control or influence on any of the underlying portfolio companies. +> +> What's more, we believe that founders should lead their ventures, as it yields better results – our investment in SimpleX Chat has no control provisions. We are happy to help, but we don’t control any decisions nor have a board seat. Evgeny runs the company independently. + +Ben, thank you for believing in our vision – without it SimpleX Chat would simply not exist, as most other investors at the time did not believe that privacy could ever escape the niche of privacy enthusiasts – and we already see the first signs of it happening. + +### The present: announcing the investment from Jack Dorsey and Asymmetric + +The Android app recently hit [100,000 downloads on Google Play Store](https://play.google.com/store/apps/details?id=chat.simplex.app), and our users naturally ask for improved reliability, privacy, security, better user experience and design – all at the same time, and as soon as possible. This requires more funding. + +We are very happy to announce that we now have funds to move faster – we raised a $1.3 million pre-seed round led by [Jack Dorsey](https://en.wikipedia.org/wiki/Jack_Dorsey), with participation of [Asymmetric Capital Partners](https://www.acp.vc) (ACP) VC fund. + +When Jack discovered SimpleX Chat last year, he [posted on Twitter](https://x.com/jack/status/1661681076983529479): + +> Better than Signal? Looks promising. +> A few bugs and UX issues but great foundation. Love that it’s public domain. + +And [on Nostr](https://primal.net/e/note1txz9xmmc456kwcg7zrsrtqrhn7as29ptuz0qulu452k8n85hsshqq6uh6q): + +> A full day with @SimpleX Chat. Solid overall. TestFlight is not recommended. There are some scaling issues today. And not the most intuitive onboarding for everyone. Name still reminds everyone of herpes. All fixable. It’s fast and doesn’t require a phone number or email and I do believe people will eventually see the value of that. Finally, some competition for Signal, and in a permissionless way. And def a solid path so apps don’t have to build their own DM experiences. + +Jack, we are super lucky to have your support and investment – thank you for believing in our ability to build a better messaging network! It is a hard work, and we’ve made a lot of progress since your note was written, and a lot of work is ongoing! + +The ACP investment is strategically important – it is a fund that only invests in B2B startups, and SimpleX Chat currently is mostly used by individual users. Making a private communication network sustainable requires its adoption by businesses, and we already see a growing usage by the small teams. + +[Rob Biederman](https://www.acp.vc/team/rob-biederman) and [Sam Clayman](https://www.acp.vc/team/sam), the partners of ACP, commented: + +> We believe that SimpleX Chat network can grow into a de facto Internet standard for private and secure communications for both businesses and individual users, unifying instant and email-like messaging into a single product. +> +> Emails no longer provide privacy and security that businesses require, particularly given the emerging threat of AI-led phishing and social engineering attacks. We look forward to SimpleX network providing a secure alternative. + +I was lucky to have met Rob, Sam and the ACP team when I was presenting SimpleX Chat in London – thank you all for your support and believing that the future of communication requires a single product, both for businesses and individual users. + +### The future: faster development and the path to non-profit governance + +Jack Dorsey and ACP support enable us to make huge product improvements, thanks to a bigger team, and provide us with medium-term funding to get to the next stage of product and business evolution. Like with Village Global, this is a financial investment, without control or board seat provisions – so the users can be certain that SimpleX remains true to our vision of privacy first communication network. + +We already added two great engineers to the team and are about to hire a UX/UI designer. + +[Trail of Bits](https://www.trailofbits.com/about/) has just completed the protocols design security review and will be doing implementation security review in the end of the year. We will publish the first report soon. + +This year we will launch group improvements that we presented in the [live-stream last year](https://www.youtube.com/watch?v=7yjQFmhAftE). While the main problem explained in this video was solved with the current design, the issue of group scalability remains – to send a message to a group your client needs to send it to each member, creating substantial traffic. + +We will also launch long-form email-like messaging over SimpleX network this year, together with optional short public addresses that show profile you are connecting to before the connection – this is important for any public users and businesses. + +The last but not the least, we started the work with [Heather Meeker](https://www.techlawpartners.com/heather), a great legal expert on intellectual property matters and one of the earliest advocates of the open-source software development in businesses, to setup open-source governance model, to some extent similar to how Matrix did it. We believe, and our investors agree, that it would both increase the company value and also create more value for the users community. + +## What's new in v6.0 + +v6.0 is one of our biggest releases ever, with a lot of focus on UX and stability improvements, and the new features the users asked for. + +The private message routing [we announced before](./20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md) is now enabled for all users by default – it protects users IP addresses and sessions from the destination servers. + +### New chat experience + +#### Connect to your friends faster + +This version includes messaging protocol improvements that reduce twice the number of messages required for two users to connect. Not only it means connecting faster and using less traffic, this change allows to start sending messages sooner, so you would see "connecting" in the list of the chats for a much shorter time than before. + +It will be improved further in the next version: you will be able to send messages straight after using the invitation link, without waiting for your contact to be online. + +#### New reachable interface + +<img src="./images/20240814-reachable.png" width="288" class="float-to-right"> + +Like with the most innovative mobile browsers (e.g., Safari and Firefox), SimpleX Chat users now can use the app with one hand by moving the toolbar and search bar to the bottom of the screen, and ordering the chats with the most recent conversations in the bottom too, where they can be more easily reached on a mobile screen. + +This layout is enabled by default, and you can disable it right from the list of chats when you install the new version if you prefer to use conventional UI. + +Give it a try – our experience is that that after less than a day of using it, it starts feeling as the only right way. You can always toggle it in the Appearance settings. + +#### Archive contacts to chat later + +<img src="./images/20240814-delete-contact-2.png" width="288" class="float-to-right">   <img src="./images/20240814-delete-contact-1.png" width="288" class="float-to-right"> + +Now you have two new options when deleting a conversation: +- only delete conversation, and archive contact. We will add archiving conversation without clearing it in the next version, as some users of our beta version asked. +- delete contact but keep the conversation. + +Also, deleting a contact now requires double confirmation, so you are less likely to delete the contact accidentally. This deletion is irreversible, and the only way to re-connect would be using a new link. + +#### New way to start chat + +<img src="./images/20240814-new-message.png" width="288" class="float-to-right"> + +When you tap pencil button, you will see a large *New message* sheet, that adds new functions to the options you had before. + +Old options: +- *Add contact* to create a new 1-time invitation link, +- *Scan / paste link*: to use the link you received. It can be 1-time invitation, a public SimpleX address, or a link to join the group. +- *Create group* + +New options: +- Open archived chats. +- Accept pending contact requests. +- Connect to preset public addresses (we will add an option to add your own addresses here too). +- Search for your contacts. + +#### New chat themes + +We released the new themes [for Android and desktop apps](./20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md) in the previous version, and now they are available for iOS too. + +You can set different themes for different chat profiles you have, and for different conversations – it can help avoid mistakes about which conversation you are in. + +Also, these themes are compatible between platforms, so you can import the theme created on Android into iOS app and vice versa. + +#### Moderate like a pro + +<img src="./images/20240814-delete-messages.png" width="288" class="float-to-right"> + +As much as we disagree with the attacks on the freedom of speech on the society level – all people must be able to express their opinions – we also believe that the small community owners should have full control over which content is allowed and which is not. But as communities grow, bad actors begin to join in order to disrupt, subvert and troll the conversations. So, the moderation tools are critical for small public communities to thrive. + +SimpleX Chat already has several moderation tools available for community owners: +- Moderate individual messages. +- Set the default role of the new members to "observer" — they won't be able to send messages until you allow it. In addition to that, by enabling default messages for admins and owners only you can reach out to the new members and ask some questions before allowing to send messages. +- Block messages of a member for yourself only. +- Block a member for all other members — only admins and group owners can do that. + +With this version you can now select multiple messages at once and delete or moderate them, depending on your role in the community. The current version limits the number of messages that can be deleted to 20 — this limit will be increased to 200 messages in the next version. + +Also, this version makes profile images of the blocked members blurred, to prevent the abuse via inappropriate profile images. + +#### Increase font size + +Android and desktop apps now allow to increase font size inside the app, without changing the system settings. Desktop app also allows to zoom the whole screen — it can be helpful on some systems with a limited support of high density displays. + +These settings can be changed via Appearance settings. + +### New media options + +#### Play from the chat list + +<img src="./images/20240814-play.png" width="288" class="float-to-right"> + +Now you can interact with the media directly from the list of the chats. + +This is very convenient – when somebody sends you a voice message or a video, they can be played directly from the list of chats, without opening a conversation. Similarly, an image can be opened, a file can be saved, and the link with preview can be opened in the browser. + +And, in some circumstances, this is also more private, as you can interact with the media, without opening the whole conversation. + +We will add the option to return missed calls from the chat list in the next version. + +#### Blur for better privacy + +You can set all images and videos to blur in your app, and unblur them on tap (or on hover in desktop app). The blur level can be set in Privacy and security settings. + +#### Share from other apps + +<img src="./images/20240814-share.png" width="288" class="float-to-right"> + +Not much to brag about, as most iOS messaging apps allow it, and users expected it to be possible since the beginning. + +But iOS makes it much harder to develop the capability to share into the app than Android, so it's only in this version you can share images, videos, files and links into SimpleX Chat from other apps. + +### Improved networking and reduced battery usage + +This version includes the statistics of how your app communicates with all servers when sending and receiving messages and files. This information also includes the status of connection to all servers from which you receive messages — whether the connection is authorized to push messages from server to your device, and the share of these active connections. + +Please note, that when you send a message to a group, your app has to send it to each member separately, so sent message statistics account for that — it may seem to be quite a large number if you actively participate in some large groups. Also, message counts not only include visible messages you receive and send, but also any service messages, reactions, message updates, message deletions, etc. — this is the correct reflection of how much traffic your app uses. + +This information is only available to your device, we do NOT collect this information, even in the aggregate form. + +While the main reason we added this information is to reduce traffic and battery usage, to be able to identify any cases of high traffic, this version already reduced a lot battery and traffic usage, as reported by several beta-version users. + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). + +[Frequently asked questions](../docs/FAQ.md). + +Please also see our [website](https://simplex.chat). + +## Please support us with your donations + +Huge thank you to everybody who donated to SimpleX Chat! + +You might ask: *Why do you need donations if you've just raised the investment?* + +Prioritizing users privacy and security, and also raising the investment, would have been impossible without your support and donations. + +Also, funding the work to transition the protocols to non-profit governance model would not have been possible without the donations we received from the users. + +Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds — any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/tree/master#help-us-with-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder diff --git a/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md b/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md new file mode 100644 index 0000000000..1bede1cc97 --- /dev/null +++ b/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md @@ -0,0 +1,192 @@ +--- +layout: layouts/article.html +title: "SimpleX network: cryptographic design review by Trail of Bits, v6.1 released with better calls and user experience." +date: 2024-10-14 +image: images/20221108-trail-of-bits.jpg +previewBody: blog_previews/20241014.html +permalink: "/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html" + +--- + +# SimpleX network: security review of protocols design by Trail of Bits, v6.1 released with better calls and user experience. + +**Published:** Oct 14, 2024 + +[New security audit](#simplex-cryptographic-design-review-by-trail-of-bits): +- [review findings](#review-findings-our-comments-and-improvements), our comments and improvements. +- [next](#next-security-audit-in-2025): security audit in early 2025. + +[What's new in v6.1](#whats-new-in-v61): +- [better calls](#better-calls). +- [better iOS notifications](#better-ios-notifications). +- [better user experience](#better-user-experience). + +## SimpleX cryptographic design review by Trail of Bits + +<img src="./images/20221108-trail-of-bits.jpg" width=240 class="float-to-right"> + +It's been almost two years since Trail of Bits did the first security assessment of SimpleX Chat. + +Since then SimpleX Chat grew a lot, both in the number of users and in its functionality. We added XFTP — a protocol for sending files, — and XRCP — the protocol for using a mobile app profile from a desktop app. Messaging protocols also evolved a lot, adding private message routing and quantum resistant encryption. + +Trail of Bits reviewed the design of protocols used in SimpleX network and applications in July 2024. Even though there are no critical issues, we made some security improvements based on this report. + +[Trail of Bits](https://www.trailofbits.com/about) is a US based security and technology consultancy whose clients include big tech companies, governmental agencies and major blockchain projects. Its engineers reviewed the cryptographic design of the protocols used in SimpleX network and applications over a week: +- SimpleX Messaging Protocol (SMP), including a formal verification of currently used message queue negotiation protocol, +- the SMP agent protocol, +- the push notification system, +- the file transfer protocol (XFTP), +- the remote control protocol (XRCP), +- and the chat protocol. + +There are 3 medium and 1 low severity findings, all of which require a high difficulty attack to exploit — the attacker would need to have a privileged access to the system, may need to know complex technical details, or must discover other weaknesses to exploit them. Additionally, there are 3 informational findings. + +3 of these issues are improved in v6.1, and the remaining issues are accepted. Below we are commenting on these findings in detail, and also on the released improvements. + +The full cryptographic design review is available [here](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/SimpleX_Design_Review_2024_Summary_Report_12_08_2024.pdf). + +We are very thankful to Trail of Bits and their engineers for their work identifying these issues and helping us make SimpleX Chat more secure. + +### Review findings, our comments and improvements + +#### Protocols specifications (informational) + +The review finding #1 is that the protocols specification is informal. We addressed [reported](https://github.com/simplex-chat/simplexmq/commit/7b6c86c6c1093cdae5ad2ee566655828076bc25c) [inconsistencies](https://github.com/simplex-chat/simplex-chat/commit/1cb3c25478db0f2a42c943f7469f5f9f75752a27), and we accept that we need to improve specification beyond verbose descriptions and ABNF syntax specification, and add algebraic notations and sequence diagrams. Having said that, the current specification correctly describes the implemented protocol, without any contradictions. + +#### User-correlating attacks via introduced latency or via GET command of messaging protocol (medium and low severity) + +These two findings #7 and #2 of the report relate to the attacks confirming that two known users communicate via observing their internet traffic. + +The first attack is possible for a party that can introduce the latency in the network traffic. This attacker has to control some network node that passes the traffic of the sender — for example, it could be the sender's ISP, VPN provider, Tor entry node operator, the operator of the forwarding SMP server or a server hosting provider, etc. Such attacker can correlate delays in sender's traffic and the suspected recipient's traffic to confirm that they communicate. + +The second attack relates to GET command used by iOS clients receiving notifications — depending on whether the server has the message, there will be a different number of packets sent, allowing the observer to determine if there was the message. While this comment is correct, in practice iOS clients only send GET commands when they receive notification, which also happens only when there is a message on the server, so in absolute majority of cases the number of packets will be the same. + +These are not new findings — this type of attacks is covered in [threat model](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#a-passive-adversary-able-to-monitor-a-set-of-senders-and-recipients): _a passive adversary able to monitor a set of senders and recipients **can** perform traffic correlation attacks against senders and recipients and correlate senders and recipients within the monitored set, frustrated by the number of users on the servers_. + +As threat model states, this attack is more likely to be successful with the less busy servers, and also for the users with few connections. + +The recommendation of the review is to add optional randomized latency to message delivery that would reduce the opportunities for traffic correlation attacks — we consider adding it in the future. + +#### A compromised transport protocol allows more efficient correlation attacks (medium severity) + +The finding #3 is about the incorrect statement in threat model for SMP and XFTP protocols: _a passive adversary, able to monitor a set of senders and recipients, **cannot**, even in case of a compromised transport protocol perform traffic correlation attacks with any increase in efficiency over a non-compromised transport protocol_. + +For protocols prior to v6.1 it is only partially correct, as responses to the commands that create a messaging queue or a file chunk include the identifiers both for senders and for the recipients, so if any observers were to compromise transport protocol (TLS) and record these identifiers, then they were able to correlate message senders with the recipients (and file recipients with the file senders). + +The solution to make this correlation impossible even in case of compromised TLS is to encrypt these identifiers, as proposed in the review, or, better, encrypt the whole transmission inside TLS. + +However unlikely is TLS being compromised, we added additional [transport encryption layer in SMP protocol](https://github.com/simplex-chat/simplexmq/pull/1317/files), where it can be more important, and we are going to add the same layer of encryption in XFTP protocol later, where we [amended the threat model](https://github.com/simplex-chat/simplexmq/commit/7b6c86c6c1093cdae5ad2ee566655828076bc25c). + +#### XRCP protocol recommendations (informational) + +XRCP protocol is used for connecting desktop and mobile. There are two findings in the review: + +- SHA256 was used as a KDF in XRCP (#4). +- there was no forward secrecy or break-in recovery between sessions (#5). + +SHA256 is now [replaced with SHA3-256](https://github.com/simplex-chat/simplexmq/pull/1302/files), as was [recommended](https://www.ietf.org/archive/id/draft-josefsson-ntruprime-hybrid-01.html) by the internet draft about hybrid key agreement that XRCP uses. + +Even though XRCP sessions are short lived, and usually the connection happens over local network, we added forward secrecy to XRCP sessions [here](https://github.com/simplex-chat/simplexmq/pull/1328/files) and [here](https://github.com/simplex-chat/simplex-chat/pull/4926/files) — each request from desktop app to mobile app is now encrypted with a new key derived from chain ratchets. This improves security of this connection. + +We believe that it is unnecessary to have in-session break-in recovery in XRCP protocol, as there is break-in recovery between the sessions. + +#### Device compromise can be hidden in some scenarios (medium) + +The finding #6 in the report is about an attacker who was not only able to break into the device and get a copy of the database, which would be mitigated by break-in recovery in [double ratchet protocol](../docs/GLOSSARY.md#double-ratchet-algorithm), but also was able to modify the state of the app database and to substitute the addresses and cryptographic keys of the messaging queues used with some contact with other message queues that the attacker controls. + +Even though this is a very hard attack, if successful, it would allow the attacker intercepting all messages with this contact. + +Effectively, it is a [man-in-the-middle attack](../docs/GLOSSARY.md#man-in-the-middle-attack), where an intermediary is inserted via the app database modification. Such attack can be mitigated by periodic verification of security codes. Although, the attacker who was able to modify the state of the device, could have also modified the app itself, making it show the same security code as the compromised contact has, thus avoiding detection. + +We accept that such an attack is possible, and we don't believe there is any viable defense against the attacker who can modify the device state. We may consider adding the measures to validate the database integrity, but they may be ineffective in case the app and/or operating system are compromised. + +### Next: security audit in 2025 + +We are planning the implementation security assessment with Trail of Bits in the beginning of 2025. It will be a twice bigger assessment than we did in 2022 — it will cover both the core of the app and the handling of cryptographic secrets in the mobile applications. + +## What's new in v6.1 + +This release has many user experience and stability improvements. + +### Better calls + +<img src="./images/20241014-calls.png" width="288" class="float-to-right"> + +This release improves reliability and usability of the calls. Now you can enable the camera and share the screen from the desktop app even if the call started as a voice call. We've also fixed several issues that prevented calls from connecting. + +This is a substantial change, and some issues may have been introduced - please report them. + +We will be further improving the calls interface in the app in the next versions. + +### Better iOS notifications + +iOS notifications were added [more than 2 years ago](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md), based on this [system design](./20220404-simplex-chat-instant-notifications.md). Until recently we made almost no improvements to them. As the number of iOS users is growing, their reliability is insufficient. In addition to that, once we started the work on improving them, we have found several important issues, one of which was introduced recently, when we improved the speed of creating new connections. + +This release fixes many important issues with iOS notifications delivery in iOS app, improves app performance and reduces traffic required to manage notifications. + +We also fixed several notification server issues, made change that almost completely prevents losing notifications when notification servers are restarted, and added real-time monitoring to diagnose any issues with iOS notifications delivery. + +This work is not over – iOS notifications in a decentralized network are complex and require more work. We will be further improving both client apps and servers to make their delivery stable. + + +### Better user experience + +#### New conversation layout and customizable messages + +<img src="./images/20241014-messages.png" width="288" class="float-to-right"> + +Messages are now grouped when they are sent sequentially, with less than 60 seconds between them. We also made message shapes configurable, and separated the messages in different days. When you scroll conversation quickly, there will be a floating date indication, allowing to find messages faster. + +#### Improved switching between user profiles + +<img src="./images/20241014-profiles2.png" width="288" class="float-to-right"> <img src="./images/20241014-profiles1.png" width="288" class="float-to-right"> + +Another improvement relates to switching between chat profiles. Previously, when you added multiple chat profiles to the app, there were two problems: +- you had to tap twice to get to some important functions in the app, +- anybody who could see your screen, could also see all your chat profiles. + +We changed this design by making important functions available after tapping profile image once, and by only showing the previously used profile image to switch to it quickly, while switching to other profiles now requires scrolling to them or opening *Your chat profiles* screen. + +You also can switch chat profile when creating a one-time invitation link. + +#### Faster deletion, moderation and forwarding of messages + +<img src="./images/20241014-forward.png" width="288" class="float-to-right"> + +You now can forward multiple messages at once - up to 20. If you are forwarding messages with files or media, and they were not received, the app will offer you to download them, and it will also allow forwarding messages without files. These messages will be "packed" into the smallest number of sent messages as possible. If there are no images and messages are not too large, it will be just one sent message containing all forwarded messages. + +The previous version allowed deleting and moderating multiple messages. As most users now upgraded the app, we increased the maximum number of messages that can be deleted or moderated to 200 messages - in most cases all these deletions will be packed into one sent message. + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). + +[Frequently asked questions](../docs/FAQ.md). + +Please also see our [website](https://simplex.chat). + +## Please support us with your donations + +Huge *thank you* to everybody who donated to SimpleX Chat! + +Prioritizing users privacy and security, and also raising the investment, would have been impossible without your support and donations. + +Also, funding the work to transition the protocols to non-profit governance model would not have been possible without the donations we received from the users. + +Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds — any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/tree/master#help-us-with-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder diff --git a/blog/20241016-wired-attack-on-privacy.md b/blog/20241016-wired-attack-on-privacy.md new file mode 100644 index 0000000000..1c2961e98a --- /dev/null +++ b/blog/20241016-wired-attack-on-privacy.md @@ -0,0 +1,41 @@ +--- +layout: layouts/article.html +title: "Wired’s Attack on Privacy" +date: 2024-10-16 +previewBody: blog_previews/20241016.html +image: images/20241016-wired-privacy.jpg +imageWide: true +permalink: "/blog/20241016-wired-attack-on-privacy.html" +--- + +# Wired’s Attack on Privacy + +**Published:** Oct 16, 2024 + +<img src="./images/20241016-wired-privacy.jpg" width="330" class="float-to-right"> + +The [Wired article](https://www.wired.com/story/neo-nazis-flee-telegram-encrypted-app-simplex/) by David Gilbert focusing on neo-Nazis moving to SimpleX Chat following the Telegram's changes in privacy policy is biased and misleading. By cherry-picking information from [the report](https://www.isdglobal.org/digital_dispatches/neo-nazi-accelerationists-seek-new-digital-refuge-amid-looming-telegram-crackdown/) by the Institute for Strategic Dialogue (ISD), Wired fails to mention that SimpleX network design prioritizes privacy in order to protect human rights defenders, journalists, and everyday users who value their privacy — many people feel safer using SimpleX than non-private apps, being protected from strangers contacting them. + +Yes, privacy-focused SimpleX network offers encryption and anonymity — that’s the point. To paint this as problematic solely because of who may use such apps misses the broader, critical context. + +SimpleX’s true strength lies in protection of [users' metadata](./20240416-dangers-of-metadata-in-messengers.md), which can reveal sensitive information about who is communicating, when, and how often. SimpleX protocols are designed to minimize metadata collection. For countless people, especially vulnerable groups, these features can be life-saving. Wired article ignores these essential protections, and overlooks the positive aspects of having such a unique design, as noted in the publication which [they link to](https://www.maargentino.com/is-telegrams-privacy-shift-driving-extremists-toward-simplex/): + +> *“SimpleX also has a significant advantage when it comes to protecting metadata — the information that can reveal who you’re talking to, when, and how often. SimpleX is designed with privacy at its core, minimizing the amount of metadata collected and ensuring that any temporary data necessary for functionality is not retained or linked to identifiable users.”* + +Both publications referenced by Wired also explore how SimpleX design actually hinders extremist groups from spreading propaganda or building large networks. SimpleX design restricts message visibility and file retention, making it far from ideal for those looking to coordinate large networks. Yet these important qualities are ignored by Wired in favor of fear-mongering about encryption — an argument we've seen before when apps like Signal [faced similar treatment](https://foreignpolicy.com/2021/03/13/telegram-signal-apps-right-wing-extremism-islamic-state-terrorism-violence-europol-encrypted/). Ironically, Wired just a month earlier encouraged its readers to [adopt encrypted messaging apps](https://www.wired.com/story/gadget-lab-podcast-657/), making its current stance even more contradictory. + +The vilification of apps that offer critically important privacy, anonymity, and encryption must stop. That a small share of users may abuse these tools doesn’t justify broad criticism. Additionally, the lobbying for client-side scanning, which Wired’s article seems to indirectly endorse, is not only dangerous but goes against fundamental principles of free speech and personal security. We strongly oppose the use of private communications for any kind of monitoring, including AI training, which would undermine the very trust encryption is designed to build. + +It’s alarming to see Wired not only criticize SimpleX for its strong privacy protections but also subtly blame the European Court of Human Rights for [upholding basic human rights](https://www.theregister.com/2024/02/15/echr_backdoor_encryption/) by rejecting laws that would force encrypted apps to scan and hand over private messages before encryption. Wired writes: + +> *…European Court of Human Rights decision in February of this year ruled that forcing encrypted messaging apps to provide a backdoor to law enforcement was illegal. This decision undermined the EU’s controversial proposal that would potentially force encrypted messaging apps to scan all user content for identifiers of child sexual abuse material.* + +This commentary is both inappropriate and misguided — it plays into the hands of anti-privacy lobbyists attempting to criminalize access to private communications. Framing privacy and anonymity as tools for criminals ignores the reality that these protections are essential for millions of legitimate users, from activists to journalists, to ordinary citizens. Client-side scanning can't have any meaningful effect on reducing CSAM distribution, instead resulting in increase of crime and abuse when criminals get access to this data. + +We need to correct this narrative. The real danger lies not in protecting communication, but in failing to do so. Privacy apps like SimpleX are crucial, not just for those resisting mass surveillance, but for everyone who values the right to communicate without fear of their conversations being monitored or misused. This is a right we must defend and incorporate into law, [as we wrote before](./20240704-future-of-privacy-enforcing-privacy-standards.md). + +Wired could have stood on the right side of this battle and helped normalize the demand for privacy, genuinely protecting people from criminals and from the exploitation of the increasingly AI-enabled mass surveillance. Instead they chose the path of spreading fear and uncertainty of encrypted messaging and tools that enable privacy and anonymity. + +Spreading misinformation about privacy and security undermines trust in the tools that protect us, making it easier to justify more invasive surveillance measures that chip away at our civil liberties. + +Wired did not respond to our request for comment. diff --git a/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md b/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md new file mode 100644 index 0000000000..57c4f69981 --- /dev/null +++ b/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md @@ -0,0 +1,146 @@ +--- +layout: layouts/article.html +title: "Servers operated by Flux - true privacy and decentralization for all users" +date: 2024-11-25 +previewBody: blog_previews/20241125.html +image: images/simplexonflux.png +imageWide: true +permalink: "/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.html" +--- + +# Servers operated by Flux — true privacy and decentralization for all users + +**Published:** Nov 25, 2024 + +- [Welcome, Flux](#welcome-flux--the-new-servers-in-v62-beta1) — the new servers in v6.2-beta.1! +- [What's the problem](#whats-the-problem)? +- [Using two operators improves connection privacy](#using-two-operators-improves-connection-privacy). +- [SimpleX decentralization](#simplex-decentralization-compared-with-matrix-session-and-tor) compared with Matrix, Session and Tor. +- [What's next](#whats-next-for-simplex-network-decentralization) for SimpleX network decentralization? + +## Welcome, Flux – the new servers in v6.2-beta.1! + +<img src="./images/20241125-operators-1.png" width="288" class="float-to-right"> + +[Flux](https://runonflux.com) is a decentralized cloud infrastructure that consists of user-operated nodes [1]. With this beta release all SimpleX Chat users can use pre-configured Flux servers to improve metadata privacy and decentralization. + +We are very grateful to [Daniel Keller](https://x.com/dak_flux), CEO and co-founder of Flux, for supporting SimpleX network, and betting on our vision of extreme decentralization of communication. Flux investing their infrastructure in our vision is a game changer for us and our users. + +Download new mobile and desktop SimpleX apps from [TestFlight](https://testflight.apple.com/join/DWuT2LQu) (iOS), [Play Store](https://play.google.com/store/apps/details?id=chat.simplex.app), our [F-Droid repo](https://simplex.chat/fdroid/) or [GitHub](https://github.com/simplex-chat/simplex-chat/releases/tag/v6.2.0-beta.1). + +Read on to learn why it is important and how using several operators improves metadata privacy. + +## What's the problem? + +SimpleX network is fully decentralized, without any central component or bootstrap nodes — you could use your own servers from day one. While there is no full list of SimpleX network servers, we see many hundreds of servers in public groups. + +But a large number of SimpleX app users use the servers pre-configured in the app. Even though the app randomly chooses 4 servers in each connection to improve privacy and security, prior to v6.2 for these users the servers were operated by the same company — ourselves. + +Our open-source code that we are [legally bound to use](./20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md#legally-binding-transparency) doesn't provide any metadata that could be used to learn who connects to whom. But the privacy of users' connections still depends on us honouring our promises and [privacy policy](../PRIVACY.md). Flux servers in the app improve that. + +## Using two operators improves connection privacy + +<img src="./images/20241125-operators-2.png" width="288" class="float-to-right"> + +To ensure that the users' metadata from different servers cannot be combined to discover who talks to whom, the servers in each connection have to be operated by different independent organizations. + +Before this version the app was choosing servers randomly. Now, when both SimpleX Chat and Flux servers are enabled it will always choose servers of different operators in each connection to receive messages and for [private message routing](./20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md), increasing metadata privacy for all users. + +Flux servers are configured as opt-in, and the privacy policy and conditions of use that apply to Flux servers are the same as for SimpleX Chat servers, to make it simple for the users. + +To improve connection privacy by using Flux servers all you have to do is to enable Flux once the app offers it, or later, via Network & servers settings, and accept that the same conditions apply. + +<img src="./images/20241125-operators-3.png" width="330" class="float-to-left"> + +By default, if both Flux and SimpleX servers are enabled in this version, you will be using SimpleX Chat servers to receive messages, Flux servers to forward messages to SimpleX Chat servers, and the servers of both to forward messages to unknown servers. We will enable Flux to receive messages by default a bit later, or you can change it now via settings. + +Any additional servers you add to app configuration are treated as belonging to another operator, so they will also be used to improve connection privacy, together with pre-configured servers, unless you disable them. + +## SimpleX decentralization compared with Matrix, Session and Tor + +SimpleX network decentralization model is different from other decentralized networks in several important aspects. + +| Communication network | SimpleX | Matrix | Session | Tor-based | +|:-----------------------------|:-------:|:------:|:-------:|:---------:| +| Full decentralization | ✅ | - | - | - | +| No user profile identity | ✅ | - | - | - | +| Connection privacy | ✅ | - | ✅ | ✅ | +| Server operator transparency | ✅ | ✅ | - | - | + +**Full decentralization** + +Fully decentralized networks do not have a central component, bootstrap nodes or any global shared state, like in cryptocurrency/blockchain-based communication networks. The presence of any central component or shared state introduces an attack vector that undermines privacy and security of the network. + +**No user profile identity** + +User profile identities, even if it is only a random number or a long-term key, undermine privacy of users connections, because in some cases they may allow network operators, observers and users to find out who talks to whom. + +Most communication networks rely on fixed user profile identities. It includes Matrix and communication networks with onion routing. + +SimpleX network design avoids the need for profile identities or keys, while still allowing optional long-term addresses for users and groups for convenience. It protects users from being discovered and approached by malicious parties, and many family users chose to use SimpleX with children because of it. + +**Connection privacy** + +SimpleX network has [private message routing](./20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md) (2-hop onion routing) — it prevents server operators from discovering who connects to whom via network traffic metadata. Onion routing used in Tor-based messengers and in Session also hides it. But because neither Tor nor Session users have knowledge about who operates servers, in some cases the clients may connect via the servers controlled by one entity, that may learn the IP addresses of both parties. + +Statistically, if traffic metadata from 2% of onion network servers is available to an attacker, and the client chooses servers randomly, after about 1750 of such choices the probability of choosing attacker's servers as both entry and exit nodes, and connection privacy being compromised becomes over 50% [2]. + +Matrix network does not provide connection privacy, as not only user identity exists, it is tied to a specific server that knows all user connections and a part of user's contacts connections. What is worse, Element — the most widely used Matrix app — offers the servers of only one organization to create an account, resulting in some degree of network centralization. + +**Server operator transparency** + +Operator transparency means that network users know who operates the servers they use. + +You may argue that when the operators are known, the servers data can be requested by the authorities. But such requests, in particular when multiple operators are used by all users, will follow a due legal process, and will not result in compromising the privacy of all users. + +With Tor and Session networks such legal process becomes impossible, and some users may see it as advantage. But nothing prevents the attackers, both criminal and corporate- or state-funded, to compromise the privacy of Tor or Session users by running many servers, or by purchasing traffic metadata from the existing server owners — there are no legal conditions that prohibit server owners of these networks to share or sell traffic data. + +Because of that, we see operator transparency in SimpleX network as a better trade-off for privacy of most users than operator anonymity provided by Session and Tor. You can see privacy of network participants as a zero sum game — for the end users to have it, server operators should be known. + +## What's next for SimpleX network decentralization + +SimpleX network is designed for extreme decentralization — not only users are distributed across network operators, as happens with federated networks, but each conversation will be relying on servers of 4-6 independent operators, and these operators will be regularly and automatically changed in the near future. + +We believe that the only viable commercial model is freemium — a small share of paying users, who have better service quality and additional features, sponsors free users. This model doesn't have downsides of exploitative "provide service, sell data" approaches, that technology monopolies practice, and it also doesn't have problems of cryptocurrency blockchains, that have shared and immutable state, and also have regulatory problems. + +To provide this extreme decentralization with freemium model we will create the system of payments allowing server operators to receive money for infrastructure certificates that will be used with any other participating network operators without compromising privacy of the paying users. You can read about this model [here](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/rfcs/2024-04-26-commercial-model.md). We will be writing more about it as this development progresses. + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). + +[Frequently asked questions](../docs/FAQ.md). + +Please also see our [website](https://simplex.chat). + +## Please support us with your donations + +Huge *thank you* to everybody who donated to SimpleX Chat! + +Prioritizing users privacy and security, and also raising the investment, would have been impossible without your support and donations. + +Also, funding the work to transition the protocols to non-profit governance model would not have been possible without the donations we received from the users. + +Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds — any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/#please-support-us-with-your-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder + +[1] You can also to self-host your own SimpleX servers on [Flux decentralized cloud](https://home.runonflux.io/apps/marketplace?q=simplex). + +[2] The probability of connection being de-anonymized and the number of random server choices follow this equation: `(1 - s ^ 2) ^ n = 1 - p`, where `s` is the share of attacker-controlled servers in the network, `n` is the number of random choices of entry and exit nodes for the circuit, and `p` is the probability of both entry and exit nodes, and the connection privacy being compromised. Substituting `0.02` (2%) for `s`, `0.5` (50%) for `p`, and solving this equation for `n` we obtain that `1733` random circuits have 50% probability of privacy being compromised. + +Also see [this presentation about Tor](https://ritter.vg/p/tor-v1.6.pdf), specifically the approximate calculations on page 76, and also [Tor project post](https://blog.torproject.org/announcing-vanguards-add-onion-services/) about the changes that made attack on hidden service anonymity harder, but still viable in case the it is used for a long time. diff --git a/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.md b/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.md new file mode 100644 index 0000000000..339fab4d16 --- /dev/null +++ b/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.md @@ -0,0 +1,119 @@ +--- +layout: layouts/article.html +title: "SimpleX network: preset servers operated by Flux, business chats and more with v6.2 of the apps" +date: 2024-12-10 +previewBody: blog_previews/20241210.html +image: images/20241210-operators-1.png +permalink: "/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html" +--- + +# SimpleX network: preset servers operated by Flux, business chats and more with v6.2 of the apps + +**Published:** Dec 10, 2024 + +What's new in v6.2: + +- [SimpleX Chat and Flux](#simplex-chat-and-flux-improve-metadata-privacy-in-simplex-network) improve metadata privacy in SimpleX network. +- [Business chats](#business-chats) to provide support from your business to users of SimpleX network. +- [Better user experience](#better-user-experience): open on the first unread, jump to quoted messages, see who reacted. +- [Improving notifications in iOS app](#improving-notifications-in-ios-app). + +## What's new in v6.2 + +### SimpleX Chat and Flux improve metadata privacy in SimpleX network + +<img src="./images/20241210-operators-1.png" width=288 class="float-to-right"> <img src="./images/20241210-operators-2.png" width=288 class="float-to-right"> + +SimpleX Chat and [Flux](https://runonflux.com) (Influx Technology Limited) made an agreement to include messaging and file servers operated by Flux into the app. + +SimpleX network is decentralized by design, but in the users of the previous app versions had to find other servers online or host servers themselves to use any other servers than operated by us. + +Now all users can choose between servers of two companies, use both of them, and continue using any other servers they host or available online. + +To use Flux servers enable them when the app offers it, or at any point later via Network & servers settings in the app. + +When both SimpleX Chat and Flux servers are enabled, the app will use servers of both operators in each connection to receive messages and for [private message routing](./20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md), increasing metadata privacy for all users. + +Read more about why SimpleX network benefits from multiple operators in [our previous post](./20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md). + +You can also read about our plan [how network operators will make money](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/rfcs/2024-04-26-commercial-model.md), while continuing to protect users privacy, based on network design rather than on trust to operators, and without any cryptocurrency emission. + +### Business chats + +<img src="./images/20241210-business.png" width=288 class="float-to-right"> + +We use SimpleX Chat to provide support to SimpleX Chat users, and we also see some other companies offering SimpleX Chat as a support channel. + +One of the problem of providing support via general purpose messengers is that the customers don't see who they talk to, as they can in all dedicated support systems. + +It is not possible in most messengers, including SimpleX Chat prior to v6.2 - every new customer joins a one-to-one conversation, where the customers see that they talk to a company, not knowing who they talk to, and if it's a bot or a human. + +The new business chats in SimpleX Chat solve this problem: to use them enable the toggle under the contact address in your chat profile. It is safe to do, and you can always toggle it off, if needed - the address itself does not change. + +Once you do it, the app will be creating a new business chat with each connecting customer where multiple people can participate. Business chat is a hybrid of one-to-one and group conversation. In the list of chats you will see customer names and avatars, and the customer will see your business name and avatar, like with one-to-one conversations. But inside it works as a group, allowing customer to see who sent the message, and allowing you to add other participants from the business side, for delegation and escalation of customer questions. + +This can be done manually, or you can automate these conversations using bots that can answer some customer questions and then add a human to the conversation when appropriate or requested by the customer. We will be offering more bot-related features to the app and a simpler way to program bots very soon - watch our announcements. + +### Better user experience + +<img src="./images/20241210-reactions.png" width=288 class="float-to-right"> + +**Chat navigation** + +This has been a long-standing complaint from the users: *why does the app opens conversations on the last message, and not on the first unread message*? + +Android and desktop apps now open the chat on the first unread message. It will soon be done in the iOS app too. + +Also, the app can scroll to the replied message anywhere in the conversation (when you tap it), even if it was sent a very long time ago. + +**See who reacted!** + +This is a small but important change - you can now see who reacted to your messages! + +### Improving notifications in iOS app + +iOS notifications in a decentralized network is a complex problem. We [support iOS notifications](./20220404-simplex-chat-instant-notifications.md#ios-notifications-require-a-server) from early versions of the app, focussing on preserving privacy as much as possible. But the reliability of notifications was not good enough. + +We solved several problems of notification delivery in this release: +- messaging servers no longer lose notifications while notification servers are restarted. +- Apple can drop notifications while your device is offline - about 15-20% of notifications are dropped because of it. The servers and the new version of the app work around this problem by delivering several last notifications, to show notifications correctly even when Apple drops them. + +With these changes the iOS notifications remained as private and secure as before. The notifications only contain metadata, without the actual messages, and even the metadata is end-to-end encrypted between SimpleX notification servers and the client device, inaccessible to Apple push notification servers. + +There are two remaining problems we will solve soon: +- iOS only allows to use 25mb of device memory when processing notifications in the background. This limit didn't change for many years, and it is challenging for decentralized design. If the app uses more memory, iOS kills it and the notification is not shown – approximately 10% of notifications can be lost because of that. +- for notifications to work, the app communicates with the notification server. If the user puts the app in background too quickly, the app may fail to enable notification for the new contacts. We plan to change clients and servers to delegate this task to messaging servers, to remove the need for this additional communication entirely, without any impact on privacy and security. This will happen early next year. + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). + +[Frequently asked questions](../docs/FAQ.md). + +Please also see our [website](https://simplex.chat). + +## Please support us with your donations + +Huge *thank you* to everybody who donated to SimpleX Chat! + +Prioritizing users privacy and security, and also raising the investment, would have been impossible without your support and donations. + +Also, funding the work to transition the protocols to non-profit governance model would not have been possible without the donations we received from the users. + +Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds — any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/#please-support-us-with-your-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder diff --git a/blog/20241218-oppose-digital-ids-they-break-law-lead-to-mass-scale-surveillance.md b/blog/20241218-oppose-digital-ids-they-break-law-lead-to-mass-scale-surveillance.md new file mode 100644 index 0000000000..8a8fffffb4 --- /dev/null +++ b/blog/20241218-oppose-digital-ids-they-break-law-lead-to-mass-scale-surveillance.md @@ -0,0 +1,53 @@ +--- +layout: layouts/article.html +title: "Oppose digital IDs – they break the law and lead to mass scale surveillance" +date: 2024-12-18 +preview: Starting next year, the UK government plans to introduce digital ID cards for the young people to prove their age when visiting pubs. +image: images/20241218-pub.jpg +imageWide: true +permalink: "/blog/20241218-oppose-digital-ids-they-break-law-lead-to-mass-scale-surveillance.html" +--- + +# Oppose digital IDs – they break the law and lead to mass scale surveillance + +**Published:** Dec 18, 2024 + +<img src="./images/20241218-pub.jpg" width="330" class="float-to-right"> + +Starting next year, the UK government plans to introduce [digital ID cards](https://www.telegraph.co.uk/politics/2024/12/08/digital-id-to-be-introduced-for-pubs-and-clubs/) for the young people to prove their age when visiting pubs. While officials claim this system will remain optional, it's part of a broader government initiative to move more state functions online so that people can prove their identity for everything from paying taxes to opening a bank account using the government-backed app. This will be a step toward a society where every pub visit, purchase, and social interaction becomes a permanent digital record linked to a government-issued ID – a step to normalizing mass surveillance at scale. + +Digital IDs are promoted as a way to fight law violations, and some politicians support them as [a way to tackle illegal immigration](https://www.telegraph.co.uk/politics/2024/07/10/id-cards-inevitable-tackle-immigration-lord-blunkett-labour/). But digital IDs themselves break the law. Article 8 of the European Convention of Human Rights says: “Everyone has the right to respect for his private and family life”. It means that not only our right to privacy is enshrined in the law, but the right to have our privacy respected is also part of the law. Asking to present a digital ID when visiting a pub, even if it is optional, disrespects our privacy, and is therefore illegal. + +Digital IDs would not stop people who decide to break laws. Pubs already can refuse to serve alcohol to young people and require the ID in case the age is in doubt. And illegal immigration can also be reduced without any digital IDs. But introducing digital IDs and collecting our actions, names and locations in one government-controlled database will result in making this information easier to access for criminals, and being exploited for financial and identity crimes. + +What starts as a "convenient option" is likely to end as a mandatory requirement. The digital ID systems being pushed by governments and corporations aren't about making our lives easier. They're about tracking, monitoring, and controlling every move we make. And we can see [where this road leads in China](https://www.wired.com/story/china-social-credit-system-explained/), when IDs and social scores created for convenience are used to prevent access to basic services and bank accounts as a punishment for legal social media posts that the government disagrees with. What started as a convenience, is now trialed [to track the duration of public toilet visits](https://www.thesun.ie/news/13154812/china-installs-toilet-timers-to-broadcast-time-spent/). + +The United Kingdom is a democratic country, and the law protects our right to privacy and freedom of speech. If we accept digital IDs as something required for simple things, like buying a drink, it leaves the door wide open to a range of privacy violations. + +We call on everyone to oppose the digital ID systems. Do not use them. Do not install these systems in your pub, for as long as it is not legally required. Support local businesses that don’t use them. Protect your privacy and freedom by using software that respects them. Demand that your privacy is respected, as required by law. + +To make your voice heard, email your MP expressing your rejection of digital IDs as a violation of European Convention of Human Rights in three simple steps: + +1. **Copy the text below** or [click this link](mailto:?subject=Please%20oppose%20the%20plan%20for%20Digital%20IDs&body=Dear%20%E2%80%A6%2C%0A%0AI%20object%20to%20the%20introduction%20of%20digital%20IDs%20in%20pubs%20or%20any%20other%20public%20places%20for%20these%20reasons%3A%0A%0A1.%20It%20violates%20the%20European%20Convention%20of%20Human%20Rights%2C%20article%208%3A%20%E2%80%9CEveryone%20has%20the%20right%20to%20respect%20for%20his%20private%20and%20family%20life%E2%80%9D%20(https%3A%2F%2Ffra.europa.eu%2Fen%2Flaw-reference%2Feuropean-convention-human-rights-article-8-0).%0AAsking%20to%20present%20digital%20IDs%20when%20proof%20of%20identity%20is%20not%20legally%20required%2C%20even%20if%20it%20is%20optional%2C%20disrespects%20our%20privacy%2C%20and%20is%20therefore%20illegal.%0A%0A2.%20It%20will%20not%20be%20an%20effective%20measure%20in%20reducing%20the%20violations%20of%20the%20law.%20People%20who%20want%20to%20circumvent%20it%2C%20will%20find%20a%20way.%0A%0A3.%20It%20will%20increase%20crime%2C%20because%20combining%20a%20large%20amount%20of%20private%20information%20in%20a%20single%20system%20increases%20the%20risks%20of%20this%20information%20becoming%20available%20to%20criminals%2C%20who%20will%20exploit%20it%20for%20financial%20crimes%20and%20identity%20theft.%0A%0AI%20kindly%20ask%20you%20to%20oppose%20this%20plan%2C%20both%20publicly%20and%20during%20any%20discussions%20in%20the%20UK%20Parliament.%0A%0ASincerely%20yours%2C%0A%E2%80%A6) to copy it into email app: + +*Dear …,* + +*I object to the introduction of digital IDs in pubs or any other public places for these reasons:* + +1. *It violates the European Convention of Human Rights, article 8: “Everyone has the right to respect for his private and family life” (https://fra.europa.eu/en/law-reference/european-convention-human-rights-article-8-0).* +*Asking to present digital IDs when proof of identity is not legally required, even if it is optional, disrespects our privacy, and is therefore illegal.* +2. *It will not be an effective measure in reducing the violations of the law. People who want to circumvent it, will find a way.* +3. *It will increase crime, because combining a large amount of private information in a single system increases the risks of this information becoming available to criminals, who will exploit it for financial crimes and identity theft.* + +*I kindly ask you to oppose this plan, both publicly and during any discussions in the UK Parliament.* + +*Sincerely yours,* +*…* + +2. [**Find the email address of your MP**](https://members.parliament.uk/members/Commons) and copy it to the email. + +3. Fill in the blanks, edit the text if needed, and **send it**! + +Public opposition changed government decisions in many cases. + +It is your opportunity to tell the government which country you want to live in — please use it! diff --git a/blog/20250114-simplex-network-large-groups-privacy-preserving-content-moderation.md b/blog/20250114-simplex-network-large-groups-privacy-preserving-content-moderation.md new file mode 100644 index 0000000000..ffa322aafe --- /dev/null +++ b/blog/20250114-simplex-network-large-groups-privacy-preserving-content-moderation.md @@ -0,0 +1,142 @@ +--- +layout: layouts/article.html +title: "SimpleX network: large groups and privacy-preserving content moderation" +date: 2025-01-14 +preview: "This post explains how server operators can moderate end-to-end encrypted conversations without compromising user privacy or end-to-end encryption." +image: images/20250114-locked-books.jpg +permalink: "/blog/20250114-simplex-network-large-groups-privacy-preserving-content-moderation.html" +--- + +# SimpleX network: large groups and privacy-preserving content moderation + +**Published:** Jan 14, 2025 + +<img src="./images/20250114-locked-books.jpg" width="345" class="float-to-right"> + +Many people believe that it is impossible to moderate and prevent abuse in end-to-end encrypted conversations. This belief is incorrect — there is a way to prevent abuse and distribution of illegal content without any compromises to users' privacy and security of end-to-end encryption. + +Anti-privacy lobbyists use this incorrect belief to advocate for scanning of private communications, which not only would fail to prevent abuse, but would make it worse — because our private data will become available to criminals. + +So it's very important to understand how privacy preserving content moderation works, and educate the politicians who you voted for, and who is currently in the office, that we do not need to compromise privacy and security in any way to substantially reduce online crime and abuse. + +This post answers these questions: +- Why [large groups on SimpleX network](#large-groups-on-simplex-network) don't work well? +- How do we plan to [make them scale](#can-large-groups-scale)? +- How do [group owners prevent abuse](#preventing-abuse-with-anonymous-participation) when people participate anonymously? +- How do server operators [prevent abuse of their servers](#preventing-server-abuse-without-compromising-e2e-encryption) and [how these measures will evolve](#privacy-preserving-content-moderation) without any compromises to privacy and end-to-end encryption? +- Which [privacy and security improvements](#privacy-and-security-improvements-we-plan-this-year) we plan this year? + +## Large groups on SimpleX network + +When we designed groups, we expected them to be used primarily for small groups where people know each other, with not more than 100 or so members. + +But we learnt that people want to participate in public discussions remaining anonymous — it protects their freedom of speech. As an experiment, we are curating a small [directory of groups](../docs/DIRECTORY.md) that currently has almost 400 public groups, with the largest ones having thousands of members. You can connect to this experimental directory via [SimpleX chat address](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). + +## Can large groups scale? + +Currently the groups are fully decentralized, and every time you send the message to some group your client has to send it to each group member, which is very costly for traffic and battery in large groups. + +We are currently working on the new group architecture when dedicated group members that run their clients on the server or on desktop with good internet connection will re-broadcast messages to all members — these members are "super-peers". + +We will be offering pre-configured super-peers via the app, and you will be able to use your own super-peers, in case you are hosting a large private group, and to have a better control and ownership of the group — e.g., if we decide to remove our super peer from the group, it will continue to function thanks to your super-peer re-broadcasting messages. + +This new design improves both privacy of group participation and censorship resistance of the groups, and also makes abusing the group harder. + +## Preventing abuse with anonymous participation + +All public discussions are abused by spammers and trolls, whether anonymous or not. We have been evolving ability of group owners to moderate conversations by allowing to remove inappropriate and off-topic messages, to block members who send spam, and to make all new members who join their group unable to send messages until approved. + +As support for large groups improves, we expect that the attempts to abuse may increase too, unless we add better moderation capabilities in advance. + +v6.3 will add ability of the group members to send reports to the group owners and administrators — the beta version we just released adds ability to manage these reports, so group admins won't miss reports when members start sending them. + +Other features that we plan to add this year to improve both usability and safety of the groups: +- message comments — some groups may choose to allow only comments, when ability to send messages is restricted to group owners or admins. +- ability to limit the maximum number of messages the members can send per day. +- ability to pre-moderate messages before they can be seen by all members. +- "knocking" — approving new members before they can join the group. +- sub-groups — smaller conversations with the same members. + +## Preventing server abuse without compromising e2e encryption + +Some categories of content may be prohibited by servers operators. An extreme case would be child sexual abuse materials (CSAM). + +Many people believe that when conversation is end-to-end encrypted, the problem is unsolvable. This incorrect belief is used by unscrupulous lobbyists and politicians who attempt to mandate various types of content scanning under the guise of preventing CSAM distribution. + +We [wrote before](./20240601-protecting-children-safety-requires-e2e-encryption.md) about how such measures not only would fail to solve the problem, but would make it worse. If our private photos become available to service providers, they will eventually become available to criminals too, and will be used to abuse and exploit the users and their children. + +An absolute majority of CSAM distributed online is publicly accessible. Many large tech companies failed to act on it and to remove CSAM from their services before it became an epidemic. We see it as a very important objective to eliminate the possibility to distribute CSAM from publicly accessible groups, even if it hurts network growth. + +When we receive a user complaint about CSAM shared in any group, we remove the files and, in some cases, the links to join the group from our servers. Our approach to moderation preserves user privacy and security of end-to-end encryption. + +How does it work? Let's go over the process step by step. + +1. A user discovered the link to join the group that distributes CSAM and sent a complaint to our support email address or via the app to [SimpleX Chat team](simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D) contact. + +2. Once we received the link to join the group, we instruct our automated bot to join it. If the complaint is confirmed as valid, the bot sends the information about the files sent in this group to the servers that store these files. + +3. Once the servers receive the file identifiers from the bot, they block the files. + +File servers cannot look inside end-to-end encrypted files, and they don't even know file sizes — they are securely locked, and sent in chunks, across multiple servers. But if the bot that joined the group provides the address of the particular file, the server can delete this file. It doesn't allow the servers to access any other files. + +In this way, the moderation is possible without any content scanning, and it preserves privacy and security of end-to-end encryption. + +## Privacy-preserving content moderation + +Right now, when we act on user complaints, we delete uploaded files or the links to join the groups from our servers, and to the users it looks as if something stopped working. + +We are currently rolling out the change to the servers that would mark these files and group links as blocked, so that users who try to download them or to join blocked groups can see that they were blocked for violating server operator conditions of use. This will improve transparency of moderation and reliability of the network. + +Later this year we plan to do more than that — client-side restrictions on the clients that violated conditions of use by uploading prohibited content. + +How would it work? When the client discovers that the uploaded file was blocked, it may, optionally, depending on the information in the blocking record, disable further uploads from the app to the servers of the operator that blocked the file. Also, when the client that tried to receive the file sees that the file is blocked, it may also refuse to receive further files from the same group member via the same servers. + +In this way, the servers can restrict the future actions of the users who violate the conditions of use, while preserving privacy and security of the users and content – even of those users who violated the conditions. + +We discussed this plan with the users, and we really appreciate their feedback. The current plan is quite different from our initial ideas, the users had a real impact. Users asked the questions below. + +**Can't users modify their clients code to circumvent these restrictions?** + +Yes, they can, but for this to work both sender and recipient would have to modify their clients. It's technically complex, so most users won't do it, and it is also hard to coordinate between users who don't know and don't trust each other. + +So these measures would be effective, even though they can be in theory circumvented, as any restrictions can be. + +Other services that identify users reduce abuse by blocking the user account. It is even easier to circumvent than changing the client code, and yet these measures reduce abuse. + +**Can't users use other servers?** + +Yes, they can. But in the same way as web browser is not responsible for the content you can access, SimpleX app should not restrict your communications with other servers based on blocking action from just one server. + +That approach allows different server operators to have different content policies, depending on their jurisdiction and other factors. It also prevents the possibility of abuse by server operators. + +**Wouldn't these measures be abused?** + +With the proposed changes, server operators will only be able to prevent uploads to their own servers, which prevents any impact on other communications. + +In the future we plan to increase the resilience to any server malfunction or abuse by using multiple different servers with each contact. + +If servers were to apply any upload restrictions unreasonably, the users would simply stop using them. + +At the same time, server operators need to have technical means to protect their servers from users' abuse, and the proposed client-side restrictions achieve it. + +**What additional measures are considered?** + +We published other technical ideas that could be used to prevent distribution of illegal content in [this document](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/rfcs/2024-12-30-content-moderation.md). None of these measures compromise users' privacy or end-to-end encryption, and they can (and should) only be applied to publicly accessible content that other users complained about. + +We technically cannot, and we won't scan all content. We actively [campaign against any content-scanning proposals](./20240704-future-of-privacy-enforcing-privacy-standards.md), because it violates our right to privacy, and it would result in huge increase of online crime. + +The belief that it is impossible to moderate conversations when they are e2e encrypted is incorrect. It is possible when users themselves share conversation contents with server operators, in which case the operators can identify and, if necessary, remove this content. It is also possible to moderate conversations that users made publicly accessible. + +## Send us comments and questions + +Let us know any comments and feedback to the proposed measures. This is still an evolving design, and it won't be implemented until later this year. + +Your comments will help to find the right balance between users' and server operators' requirements. + +## Privacy and security improvements we plan this year + +To increase privacy and security we plan to add this year: +- quantum-resistant e2e encryption in small groups. +- receiving proxy for files, to protect users IP addresses and other transport metadata from file senders' servers. + +We see privacy and security as necessary for online safety, and prevention of abuse. If you don't already use SimpleX network, try it now, and let us know what you need to make it better. diff --git a/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.md b/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.md new file mode 100644 index 0000000000..f1aa3a243f --- /dev/null +++ b/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.md @@ -0,0 +1,188 @@ +--- +layout: layouts/article.html +title: "SimpleX Chat v6.3: new user experience and safety in public groups" +date: 2025-03-08 +previewBody: blog_previews/20250308.html +image: images/20250308-captcha.png +imageBottom: true +permalink: "/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html" +--- + +# SimpleX Chat v6.3: new user experience and safety in public groups + +**Published:** Mar 8, 2025 + +**What's new in v6.3**: +- [preventing spam and abuse in public groups](#preventing-spam-and-abuse-in-public-groups). +- [group improvements](#group-improvements): [mention other members](#mention-other-members-and-get-notified-when-mentioned), [improved performance](#better-group-performance). +- [better chat navigation](#better-chat-navigation): [organize chats into lists](#organize-chats-into-lists) and [jump to found and forwarded messages](#jump-to-found-and-forwarded-messages). +- [privacy and security improvements](#privacy-and-security-improvements): [chat retention period](#set-message-retention-period-in-chats) and [private media file names](#private-media-file-names). + +Also, we added Catalan interface language to Android and desktop apps, thanks to [our users and Weblate](https://github.com/simplex-chat/simplex-chat#help-translating-simplex-chat). + +The last but not the least - [server builds are now reproducible](#reproducible-server-builds). + +## What's new in v6.3 + +## Preventing spam and abuse in public groups + +[We wrote before](./20250114-simplex-network-large-groups-privacy-preserving-content-moderation.md): as the network grows, it becomes more attractive to attackers. This release adds several features that reduce the possibility of attacks and abuse. + +### Spam in groups that are listed in our group directory + +There is no built-in group discovery in SimpleX Chat apps. Instead, we offer an experimental chat bot that allows to submit and to discover public groups. Not so long ago, spammers started sending messages via bots attempting to disrupt these groups. + +We released several changes to the groups directory to protect from spam attacks. + +**Optional captcha verification** + +<img src="./images/20250308-captcha.png" width="288" class="float-to-right"> + +Group owners can enable the requirement to pass captcha challenge before joining the group. Captcha is generated in the directory bot itself, without any 3rd party servers, and is sent to the joining member. The new member must reply with the text in the image to be accepted to the group. While not a perfect protection, this basic measure complicates programming automatic bots to join public groups. It also provides a foundation to implement "knocking" - a conversation with dedicated group admins prior to joining the group. We plan to release support for knocking in March. + +**Profanity filter for member names** + +While group settings support giving all joining member an "observer" role - that is, without the right to send messages - the attackers tried spaming groups by joining and leaving. We added an optional filter for member names that group owners can enable for groups listed in directory - if a member name contains profanity, they will be rejected. Further improvements will be released in March as well. + +The current SimpleX directory chatbot is a hybrid of [future chat relays](./20250114-simplex-network-large-groups-privacy-preserving-content-moderation.md#can-large-groups-scale) (a.k.a. super-peers) we are now developing to support large groups, and of a directory service that will be embedded in the app UI later this year, allowing to search and to discover public groups. Anybody is able to run their own directory bots now, and there will be possibility to use third party directories via the app UI in the future too. + +Read more about [SimpleX group directory](../docs/DIRECTORY.md), how to submit your groups, and which groups we now accept. Currently we accept groups related to a limited list of topics that will be expanded once we have better moderation functionality for the groups. + +### More power to group owners and moderators + +This release includes two new features to help group moderators. + +<img src="./images/20250308-reports.png" width="288" class="float-to-right"> + +**Private reports** + +Group members can privately bring to group moderators attention specific messages and members, even if the group does not allow direct messages. The simply need to choose report in the message context menu and choose the report reason. This report will be visible to all group owners and moderators, but not to other members. + +Group moderators can see all member reports in a separate view, and quickly find the problematic messages, making moderation much easier in public groups. These reports are private to groups, they are not sent to server operators. + +Please note: in the groups listed in our directory, the directory bot acts as admin, so it will receive all reports as well. + +**Acting on multiple members at once** + +When attackers come, they often use multiple profiles. This version allows selecting multiple members at once and perform these actions on all selected members: +- switch members role between "observer" and "member". +- block and unblock members - this is a "shadow" block, so when you block multiple members who you believe are attackers, their messages will be blocked for all other members but not for them. +- remove members from the group. + +The next version will also allow to remove members together with all messages they sent - for example, if a spam bot joined and sent a lot of spam, but nothing of value. + +## Group improvements + +### Mention other members and get notified when mentioned + +<img src="./images/20250308-mentions.png" width="288" class="float-to-right"> + +This feature allows you to mention other members in the group in the usual way - type `@` character, and choose the member you want to mention from the menu. Even that there is no user accounts and persistent identities we made it work by referencing members by their random group ID that is also used for replies and all other interactions in the group. + +You can also now switch message notifications in the group to "mentions only" mode. You will be notified only when you are mentioned in a message, or when somebody replies to your message. Simply choose "Mute" in the context menu of the group in the list of chats to switch group notifications to "mentions only" mode. After that you can choose "Mute all" to disable all notifications, including mentions. + +### Better group performance + +**Send messages faster** + +We didn't reduce the required network traffic to send messages to large groups yet - your client still has to send message to each member individually. But we redesigned the process of sending a message, reducing temporary storage required to schedule the message for delivery by about 100x. This creates a significant storage saving - e.g, to send one message to a group of 1,000 members previously required ~20Mb, and now it is reduced to ~200kb. It also reduces the time and battery used to send a message. + +**Faster group deletion** + +When you leave the group, the app preserves a copy of all your communications in the group. You can choose to keep it or to delete it completely. This final group deletion was very slow prior to this release - depending on the number of groups on your device it could sometimes take several minutes. + +This release solved this problem – the time it takes to delete the group is reduced to seconds, and even in cases when the app is terminated half-way, it either rolls back or completes, but it cannot leave the group in a partially deleted state. It improves both user experience and privacy, as gives you better control over your data. + +## Better chat navigation + +### Organize chats into lists + +<img src="./images/20250308-lists.png" width="288" class="float-to-right"> + +It is a common feature in many messengers – it helps organizing your conversations. + +The lists also show a blue mark when any chat in the list has new messages. + +There are several preset lists: contacts, groups, private notes, business chats, favourite chats and also groups with member reports - the last list is automatically shown if members of any groups where you are the moderator or the owner sent private reports, until these reports are acted on or archived. + +### Jump to found and forwarded messages + +This version allows to quickly navigate from message in the search results to the point in the conversation when it was sent. + +You can also navigate from the forwarded message (or from the message saved to private notes) to the original message in the chat where it was forwarded or saved from. + +## Privacy and security improvements + +### Set message retention period in chats + +Before this version, you could enable message retention period for all chats in your profile. While helpful in some cases, many of us have conversations that we want to keep for a long time, and some other conversations that we want to remove quicker. + +This version allows it - you can set different retention periods in different conversations. It can be 1 day, 1 week, 1 month or 1 year. We may allow custom retention time in the future. + +### Private media file names + +Previously there were scenarios when original media file names were preserved - e.g., when sending a video file or when forwarding any media file. The latter problem was worse, as media file name is generated automatically, and includes timestamp. So the same name could have been used to correlate files between conversations, as one of our users pointed out. + +This version fixes this problem - media file name is now changed when forwarding it to match the time of forwarding, so no additional metadata is revealed. + +Please also note: +- the apps remove metadata from all static images, +- iOS app removes metadata from videos, but android and desktop apps do not do it yet, +- animated images are sent as is, +- other file types are sent as is, and their names are left unchanged - we believe that for ordinary files their name is part of their content. + +We plan further improvements to reduce metadata in files in the near future – please let us know what you believe is the most important to reduce first. + +## Reproducible server builds + +Starting from v6.3 server releases are reproducible! + +**Why it is important** + +With reproducible builds anybody can build servers from our code following the same process, and the build would produce identical binaries. + +This also allows us to sign releases, as we reproduce GitHub builds ourselves and by signing them we attest that our builds resulted in identical binaries. + +**How to reproduce builds** + +You can reproduce our builds on Linux with x86 CPU in docker container - please follow the instructions [here](../docs/SERVER.md#reproduce-builds). + +We are looking for support from open-source contributors or security researchers who would also reproduce and sign our releases. + +**How to verify release signature** + +Please see the instructions [here](../docs/SERVER.md#verifying-server-binaries). + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). + +[Frequently asked questions](../docs/FAQ.md). + +Please also see our [website](https://simplex.chat). + +## Please support us with your donations + +Huge *thank you* to everybody who donated to SimpleX Chat! + +Prioritizing users privacy and security, and also raising the investment, would have been impossible without your support and donations. + +Also, funding the work to transition the protocols to non-profit governance model would not have been possible without the donations we received from the users. + +Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds — any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/#please-support-us-with-your-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder diff --git a/blog/README.md b/blog/README.md index 815009139f..00a84eca6c 100644 --- a/blog/README.md +++ b/blog/README.md @@ -1,5 +1,187 @@ # Blog +Mar 3, 2025 [SimpleX Chat v6.3: new user experience and safety in public groups](./20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md) + +What's new in v6.3: +- preventing spam and abuse in public groups. +- group improvements: mention other members and improved performance. +- better chat navigation: organize chats into lists and jump to found and forwarded messages. +- privacy and security improvements: chat retention period and private media file names. + +Also, we added Catalan interface language to Android and desktop apps, thanks to our users and Weblate. + +The last but not the least - server builds are now reproducible! + +-- + +Jan 14, 2025 [SimpleX network: large groups and privacy-preserving content moderation](./20250114-simplex-network-large-groups-privacy-preserving-content-moderation.md) + +This post explains how server operators can moderate end-to-end encrypted conversations without compromising user privacy or end-to-end encryption. + +-- + +Dec 10, 2024 [SimpleX network: preset servers operated by Flux, business chats and more with v6.2 of the apps](./20241210-simplex-network-v6-2-servers-by-flux-business-chats.md) + +- SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app to improve metadata privacy in SimpleX network. +- Business chats for better privacy and support of your customers. +- Better user experience: open on the first unread, jump to quoted messages, see who reacted. +- Improving notifications in iOS app. + +-- + +Nov 25, 2024 [Servers operated by Flux - true privacy and decentralization for all users](./20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md) + +- Welcome, Flux - the new servers in v6.2-beta.1! +- What's the problem? +- Using two operators improves connection privacy. +- SimpleX decentralization compared with Matrix, Session and Tor. +- What is next for SimpleX decentralization? + +--- + +Oct 14, 2024 [SimpleX network: security review of protocols design by Trail of Bits, v6.1 released with better calls and user experience](./20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md) + +New security audit: Trail of Bits reviewed the cryptographic design of protocols used in SimpleX network and apps. + +What's new in v6.1: +- Better calls: switch audio and video during the call. +- Better iOS notifications: improved delivery, reduced traffic usage. +- Better user experience: switch chat profiles, customizable message shapes, forward up to 20 messages. + +--- + +Aug 14, 2024 [SimpleX network: the investment from Jack Dorsey and Asymmetric, v6.0 released with the new user experience and private message routing](./20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md) + +SimpleX Chat: vision and funding 2.0: past, present, future. + +Announcing the investment from Jack Dorsey and Asymmetric. + +What's new in v6.0: +- Private message routing - now enabled by default +- New chat experience: + - connect to your friends faster. + - new reachable interface. + - and much more! +- Improved networking and reduced battery usage + +--- + +Jul 4, 2024 [The Future of Privacy: Enforcing Privacy Standards](./20240704-future-of-privacy-enforcing-privacy-standards.md) + +It's time we shift the focus: privacy should be a non-negotiable duty of technology providers, not just a right users must fight to protect, and not something that users can be asked to consent away as a condition of access to a service. + +--- + +Jun 4, 2024 [SimpleX network: private message routing, v5.8 released with IP address protection and chat themes](./20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md) + +What's new in v5.8: +- private message routing. +- server transparency. +- protect IP address when downloading files & media. +- chat themes for better conversation privacy - in Android and desktop apps. +- group improvements - reduced traffic and additional preferences. +- improved networking, message and file delivery. + +Also, we added Persian interface language to the Android and desktop apps, thanks to our users and Weblate. + +--- + +Jun 1, 2024 [Children's Safety Requires End-to-End Encryption](./20240601-children-safety-requires-e2e-encryption.md) + +As lawmakers grapple with the serious issue of child exploitation online, some proposed solutions would fuel the very problem they aim to solve. + +--- + +May 16, 2024 [SimpleX: Redefining Privacy by Making Hard Choices](./20240516-simplex-redefining-privacy-hard-choices.md) + +When it comes to open source privacy tools, the status quo often dictates the limitations of existing protocols and structures. However, these norms need to be challenged to radically shift how we approach genuinely private communication. This requires doing some uncomfortable things, like making hard choices as it relates to funding, alternative decentralization models, doubling down on privacy over convenience, and more. + +In this post we explain a bit more about why SimpleX operates and makes decisions the way it does: + +- No user accounts. +- Privacy over convenience. +- Network decentralization. +- Funding and profitability. +- Company jurisdiction. + +--- + +Apr 26, 2024 [SimpleX network: legally binding transparency, v5.7 released with better calls and messages](./20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md) + +We published Transparency Reports, Security Policy, and Frequently Asked Questions, and updated Privacy Policy. + +We are committed to have full consistency between marketing promises and legally binding documents. + +What's new in v5.7: +- quantum resistant end-to-end encryption with all contacts. +- forward and save messages without revealing the source. +- in-call sounds and switching sound sources. +- better network connection management. +- customizable profile images + +--- + +Apr 16. 2024 [The dangers of metadata in messengers](./20240416-dangers-of-metadata-in-messengers.md) + +_By [Esra'a al Shafei](https://mastodon.social/@alshafei)_ + +It's important not to be complacent with the current standards of messaging, where metadata aggregation is still normalized in apps falsely and dangerously marketed as "private". This is a post exploring the fundamental differences between privacy and security. + +--- + +Apr 4. 2024 [Why I joined SimpleX Chat](./20240404-why-i-joined-simplex-chat-esraa-al-shafei.md) + +_By [Esra'a al Shafei](https://mastodon.social/@alshafei)_ + +Transitioning from a lifelong career dedicated to nonprofits, including Board roles at organizations like the Wikimedia Foundation, Access Now and Tor, my decision to join SimpleX Chat may come as a surprise to some. But, as I step into this new chapter, I want to share the insights and convictions that have guided me here, shedding light on what I think sets SimpleX Chat apart and why this move feels like an essential learning opportunity. + +--- + +Mar 23, 2024 [SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration](./20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md) + +SimpleX network: deliver real privacy via a profitable business and non-profit protocol governance: +- community and business interests are aligned. +- the journey to the decentralized non-profit protocol governance. +- welcome, Esra’a! + +What's new in v5.6: +- quantum resistant end-to-end encryption (BETA) – enable it for the new contacts. +- use the app during the audio and video calls. +- migrate all app data to another device via QR code. + +--- + +Mar 14, 2024 [SimpleX Chat v5.6 (beta): adding quantum resistance to Signal double ratchet algorithm](./20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md) + +This is a major upgrade for SimpleX Chat messaging protocol stack, I am really proud to present this work of the whole team. + +This post also covers various aspects of end-to-end encryption, compares different messengers, and explains how and why quantum-resistant encryption is added to SimpleX Chat: + +- Why do we need end-to-end encryption? +- Why encryption is even allowed? +- End-to-end encryption security: attacks and defense. +- How secure is encryption in different messengers? +- Adding quantum resistance to Signal double ratchet algorithm. +- When can you start using quantum resistant chats? +- Next for post-quantum crypto: all direct chats, small groups and security audit. + +--- + +Jan 24, 2024 [SimpleX Chat: free infrastructure from Linode, v5.5 released](./20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md) + +SimpleX Chat infrastructure on Linode: +- Free infrastructure. +- SimpleX servers in Linode Marketplace. +- High capacity messaging servers. + +What's new in v5.5: +- private notes. +- group history. +- simpler UX to connect to other users. +- message delivery stability and other improvements. + +--- + Nov 25, 2023 [SimpleX Chat v5.4 released](./20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md) - Link mobile and desktop apps via secure quantum-resistant protocol. 🔗 @@ -131,7 +313,7 @@ November reviews: - [Privacy Guides](https://www.privacyguides.org/real-time-communication/#simplex-chat) recommendations. - [Review by Mike Kuketz](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/). - [The messenger matrix](https://www.messenger-matrix.de). -- [Supernova review](https://supernova.tilde.team/detailed_reviews.html#simplex) and [messenger ratings](https://supernova.tilde.team/messengers.html). +- [Supernova review](https://supernovas.space/detailed_reviews.html#simplex) and [messenger ratings](https://supernovas.space/messengers.html). --- diff --git a/blog/images/20240124-connect1.png b/blog/images/20240124-connect1.png new file mode 100644 index 0000000000..9b5a3cd1ff Binary files /dev/null and b/blog/images/20240124-connect1.png differ diff --git a/blog/images/20240124-connect2.png b/blog/images/20240124-connect2.png new file mode 100644 index 0000000000..fda81efb20 Binary files /dev/null and b/blog/images/20240124-connect2.png differ diff --git a/blog/images/20240124-history1.png b/blog/images/20240124-history1.png new file mode 100644 index 0000000000..fbba77b5fa Binary files /dev/null and b/blog/images/20240124-history1.png differ diff --git a/blog/images/20240124-history2.png b/blog/images/20240124-history2.png new file mode 100644 index 0000000000..a54992bb48 Binary files /dev/null and b/blog/images/20240124-history2.png differ diff --git a/blog/images/20240124-notes1.png b/blog/images/20240124-notes1.png new file mode 100644 index 0000000000..4c253b4704 Binary files /dev/null and b/blog/images/20240124-notes1.png differ diff --git a/blog/images/20240124-notes2.png b/blog/images/20240124-notes2.png new file mode 100644 index 0000000000..82c6b18eb5 Binary files /dev/null and b/blog/images/20240124-notes2.png differ diff --git a/blog/images/20240314-comparison.jpg b/blog/images/20240314-comparison.jpg new file mode 100644 index 0000000000..815922aa7a Binary files /dev/null and b/blog/images/20240314-comparison.jpg differ diff --git a/blog/images/20240314-datacenter.jpg b/blog/images/20240314-datacenter.jpg new file mode 100644 index 0000000000..9bb55c47c2 Binary files /dev/null and b/blog/images/20240314-datacenter.jpg differ diff --git a/blog/images/20240314-djb.jpg b/blog/images/20240314-djb.jpg new file mode 100644 index 0000000000..33551ec365 Binary files /dev/null and b/blog/images/20240314-djb.jpg differ diff --git a/blog/images/20240314-kem.jpg b/blog/images/20240314-kem.jpg new file mode 100644 index 0000000000..97d6ffdb4f Binary files /dev/null and b/blog/images/20240314-kem.jpg differ diff --git a/blog/images/20240314-mitm1.jpg b/blog/images/20240314-mitm1.jpg new file mode 100644 index 0000000000..b82e6534bb Binary files /dev/null and b/blog/images/20240314-mitm1.jpg differ diff --git a/blog/images/20240314-mitm2.jpg b/blog/images/20240314-mitm2.jpg new file mode 100644 index 0000000000..24132f21ab Binary files /dev/null and b/blog/images/20240314-mitm2.jpg differ diff --git a/blog/images/20240314-mitm3.jpg b/blog/images/20240314-mitm3.jpg new file mode 100644 index 0000000000..a20b37765a Binary files /dev/null and b/blog/images/20240314-mitm3.jpg differ diff --git a/blog/images/20240314-mitm4.jpg b/blog/images/20240314-mitm4.jpg new file mode 100644 index 0000000000..44a3a322a1 Binary files /dev/null and b/blog/images/20240314-mitm4.jpg differ diff --git a/blog/images/20240314-pq1.png b/blog/images/20240314-pq1.png new file mode 100644 index 0000000000..f3b88f943e Binary files /dev/null and b/blog/images/20240314-pq1.png differ diff --git a/blog/images/20240314-pq2.png b/blog/images/20240314-pq2.png new file mode 100644 index 0000000000..73d3dbde42 Binary files /dev/null and b/blog/images/20240314-pq2.png differ diff --git a/blog/images/20240314-pq3.png b/blog/images/20240314-pq3.png new file mode 100644 index 0000000000..0384c09276 Binary files /dev/null and b/blog/images/20240314-pq3.png differ diff --git a/blog/images/20240323-call1.png b/blog/images/20240323-call1.png new file mode 100644 index 0000000000..0cf954a07e Binary files /dev/null and b/blog/images/20240323-call1.png differ diff --git a/blog/images/20240323-call2.png b/blog/images/20240323-call2.png new file mode 100644 index 0000000000..8a69d1891e Binary files /dev/null and b/blog/images/20240323-call2.png differ diff --git a/blog/images/20240323-migrate-from0.png b/blog/images/20240323-migrate-from0.png new file mode 100644 index 0000000000..033ead3fd9 Binary files /dev/null and b/blog/images/20240323-migrate-from0.png differ diff --git a/blog/images/20240323-migrate-from1.png b/blog/images/20240323-migrate-from1.png new file mode 100644 index 0000000000..be016ada2c Binary files /dev/null and b/blog/images/20240323-migrate-from1.png differ diff --git a/blog/images/20240323-migrate-from2.png b/blog/images/20240323-migrate-from2.png new file mode 100644 index 0000000000..2cee12e6f4 Binary files /dev/null and b/blog/images/20240323-migrate-from2.png differ diff --git a/blog/images/20240323-migrate-from3.png b/blog/images/20240323-migrate-from3.png new file mode 100644 index 0000000000..92d7bf19a6 Binary files /dev/null and b/blog/images/20240323-migrate-from3.png differ diff --git a/blog/images/20240323-migrate-from4.png b/blog/images/20240323-migrate-from4.png new file mode 100644 index 0000000000..9b96f9efd8 Binary files /dev/null and b/blog/images/20240323-migrate-from4.png differ diff --git a/blog/images/20240323-migrate-from5.png b/blog/images/20240323-migrate-from5.png new file mode 100644 index 0000000000..dfc22a3987 Binary files /dev/null and b/blog/images/20240323-migrate-from5.png differ diff --git a/blog/images/20240323-migrate-from6.png b/blog/images/20240323-migrate-from6.png new file mode 100644 index 0000000000..18d9c8b627 Binary files /dev/null and b/blog/images/20240323-migrate-from6.png differ diff --git a/blog/images/20240323-migrate-to1.png b/blog/images/20240323-migrate-to1.png new file mode 100644 index 0000000000..b3fb0e32f1 Binary files /dev/null and b/blog/images/20240323-migrate-to1.png differ diff --git a/blog/images/20240323-migrate-to2.png b/blog/images/20240323-migrate-to2.png new file mode 100644 index 0000000000..f64544cd9b Binary files /dev/null and b/blog/images/20240323-migrate-to2.png differ diff --git a/blog/images/20240323-migrate-to3.png b/blog/images/20240323-migrate-to3.png new file mode 100644 index 0000000000..626032737b Binary files /dev/null and b/blog/images/20240323-migrate-to3.png differ diff --git a/blog/images/20240323-migrate-to4.png b/blog/images/20240323-migrate-to4.png new file mode 100644 index 0000000000..59153a464c Binary files /dev/null and b/blog/images/20240323-migrate-to4.png differ diff --git a/blog/images/20240323-migrate-to5.png b/blog/images/20240323-migrate-to5.png new file mode 100644 index 0000000000..b86f42b292 Binary files /dev/null and b/blog/images/20240323-migrate-to5.png differ diff --git a/blog/images/20240323-post-preview.png b/blog/images/20240323-post-preview.png new file mode 100644 index 0000000000..69c9908a1e Binary files /dev/null and b/blog/images/20240323-post-preview.png differ diff --git a/blog/images/20240404-esraa.png b/blog/images/20240404-esraa.png new file mode 100644 index 0000000000..baee242023 Binary files /dev/null and b/blog/images/20240404-esraa.png differ diff --git a/blog/images/20240404-messsaging-apps.png b/blog/images/20240404-messsaging-apps.png new file mode 100644 index 0000000000..6081a468a1 Binary files /dev/null and b/blog/images/20240404-messsaging-apps.png differ diff --git a/blog/images/20240416-metadata.png b/blog/images/20240416-metadata.png new file mode 100644 index 0000000000..743930bf15 Binary files /dev/null and b/blog/images/20240416-metadata.png differ diff --git a/blog/images/20240416-whatsapp.jpg b/blog/images/20240416-whatsapp.jpg new file mode 100644 index 0000000000..399235347a Binary files /dev/null and b/blog/images/20240416-whatsapp.jpg differ diff --git a/blog/images/20240426-forward1.png b/blog/images/20240426-forward1.png new file mode 100644 index 0000000000..8fb99a4580 Binary files /dev/null and b/blog/images/20240426-forward1.png differ diff --git a/blog/images/20240426-forward2.png b/blog/images/20240426-forward2.png new file mode 100644 index 0000000000..8dd5043703 Binary files /dev/null and b/blog/images/20240426-forward2.png differ diff --git a/blog/images/20240426-forward3.png b/blog/images/20240426-forward3.png new file mode 100644 index 0000000000..9cf97e8ae1 Binary files /dev/null and b/blog/images/20240426-forward3.png differ diff --git a/blog/images/20240426-pq.png b/blog/images/20240426-pq.png new file mode 100644 index 0000000000..2a86d10a7c Binary files /dev/null and b/blog/images/20240426-pq.png differ diff --git a/blog/images/20240426-profile.png b/blog/images/20240426-profile.png new file mode 100644 index 0000000000..fc0b9536bb Binary files /dev/null and b/blog/images/20240426-profile.png differ diff --git a/blog/images/20240426-profile1.png b/blog/images/20240426-profile1.png new file mode 100644 index 0000000000..f638b3b7bc Binary files /dev/null and b/blog/images/20240426-profile1.png differ diff --git a/blog/images/20240426-profile2.png b/blog/images/20240426-profile2.png new file mode 100644 index 0000000000..6592eec886 Binary files /dev/null and b/blog/images/20240426-profile2.png differ diff --git a/blog/images/20240426-profile3.png b/blog/images/20240426-profile3.png new file mode 100644 index 0000000000..f6972f2013 Binary files /dev/null and b/blog/images/20240426-profile3.png differ diff --git a/blog/images/20240516-parliament.jpg b/blog/images/20240516-parliament.jpg new file mode 100644 index 0000000000..53e8013249 Binary files /dev/null and b/blog/images/20240516-parliament.jpg differ diff --git a/blog/images/20240601-eu-privacy.png b/blog/images/20240601-eu-privacy.png new file mode 100644 index 0000000000..4ae1a17e30 Binary files /dev/null and b/blog/images/20240601-eu-privacy.png differ diff --git a/blog/images/20240604-routing.png b/blog/images/20240604-routing.png new file mode 100644 index 0000000000..8fb0c821c4 Binary files /dev/null and b/blog/images/20240604-routing.png differ diff --git a/blog/images/20240604-server.png b/blog/images/20240604-server.png new file mode 100644 index 0000000000..4ab610f3b9 Binary files /dev/null and b/blog/images/20240604-server.png differ diff --git a/blog/images/20240604-theme1.png b/blog/images/20240604-theme1.png new file mode 100644 index 0000000000..e9a1422a71 Binary files /dev/null and b/blog/images/20240604-theme1.png differ diff --git a/blog/images/20240604-theme2.png b/blog/images/20240604-theme2.png new file mode 100644 index 0000000000..e7972f6e05 Binary files /dev/null and b/blog/images/20240604-theme2.png differ diff --git a/blog/images/20240704-privacy.jpg b/blog/images/20240704-privacy.jpg new file mode 100644 index 0000000000..bfcbf11d3f Binary files /dev/null and b/blog/images/20240704-privacy.jpg differ diff --git a/blog/images/20240814-delete-contact-1.png b/blog/images/20240814-delete-contact-1.png new file mode 100644 index 0000000000..7f35664502 Binary files /dev/null and b/blog/images/20240814-delete-contact-1.png differ diff --git a/blog/images/20240814-delete-contact-2.png b/blog/images/20240814-delete-contact-2.png new file mode 100644 index 0000000000..766ddd677b Binary files /dev/null and b/blog/images/20240814-delete-contact-2.png differ diff --git a/blog/images/20240814-delete-messages.png b/blog/images/20240814-delete-messages.png new file mode 100644 index 0000000000..b2c65f28ad Binary files /dev/null and b/blog/images/20240814-delete-messages.png differ diff --git a/blog/images/20240814-new-message.png b/blog/images/20240814-new-message.png new file mode 100644 index 0000000000..69930499e7 Binary files /dev/null and b/blog/images/20240814-new-message.png differ diff --git a/blog/images/20240814-play.png b/blog/images/20240814-play.png new file mode 100644 index 0000000000..3e719df642 Binary files /dev/null and b/blog/images/20240814-play.png differ diff --git a/blog/images/20240814-reachable.png b/blog/images/20240814-reachable.png new file mode 100644 index 0000000000..dde3c747f9 Binary files /dev/null and b/blog/images/20240814-reachable.png differ diff --git a/blog/images/20240814-share.png b/blog/images/20240814-share.png new file mode 100644 index 0000000000..b486fe2356 Binary files /dev/null and b/blog/images/20240814-share.png differ diff --git a/blog/images/20241014-calls.png b/blog/images/20241014-calls.png new file mode 100644 index 0000000000..c9d8bb18a7 Binary files /dev/null and b/blog/images/20241014-calls.png differ diff --git a/blog/images/20241014-forward.png b/blog/images/20241014-forward.png new file mode 100644 index 0000000000..c094bf80a9 Binary files /dev/null and b/blog/images/20241014-forward.png differ diff --git a/blog/images/20241014-messages.png b/blog/images/20241014-messages.png new file mode 100644 index 0000000000..afc7993899 Binary files /dev/null and b/blog/images/20241014-messages.png differ diff --git a/blog/images/20241014-profiles1.png b/blog/images/20241014-profiles1.png new file mode 100644 index 0000000000..5fb44e3086 Binary files /dev/null and b/blog/images/20241014-profiles1.png differ diff --git a/blog/images/20241014-profiles2.png b/blog/images/20241014-profiles2.png new file mode 100644 index 0000000000..91b18ca4af Binary files /dev/null and b/blog/images/20241014-profiles2.png differ diff --git a/blog/images/20241016-wired-privacy.jpg b/blog/images/20241016-wired-privacy.jpg new file mode 100644 index 0000000000..a97e9ba79c Binary files /dev/null and b/blog/images/20241016-wired-privacy.jpg differ diff --git a/blog/images/20241125-operators-1.png b/blog/images/20241125-operators-1.png new file mode 100644 index 0000000000..a749fa0ed1 Binary files /dev/null and b/blog/images/20241125-operators-1.png differ diff --git a/blog/images/20241125-operators-2.png b/blog/images/20241125-operators-2.png new file mode 100644 index 0000000000..4e5f4a9042 Binary files /dev/null and b/blog/images/20241125-operators-2.png differ diff --git a/blog/images/20241125-operators-3.png b/blog/images/20241125-operators-3.png new file mode 100644 index 0000000000..7ae26ef350 Binary files /dev/null and b/blog/images/20241125-operators-3.png differ diff --git a/blog/images/20241210-business.png b/blog/images/20241210-business.png new file mode 100644 index 0000000000..407ed66a94 Binary files /dev/null and b/blog/images/20241210-business.png differ diff --git a/blog/images/20241210-operators-1.png b/blog/images/20241210-operators-1.png new file mode 100644 index 0000000000..863bd00822 Binary files /dev/null and b/blog/images/20241210-operators-1.png differ diff --git a/blog/images/20241210-operators-2.png b/blog/images/20241210-operators-2.png new file mode 100644 index 0000000000..85e599c827 Binary files /dev/null and b/blog/images/20241210-operators-2.png differ diff --git a/blog/images/20241210-reactions.png b/blog/images/20241210-reactions.png new file mode 100644 index 0000000000..6de8ba8f07 Binary files /dev/null and b/blog/images/20241210-reactions.png differ diff --git a/blog/images/20241218-pub.jpg b/blog/images/20241218-pub.jpg new file mode 100644 index 0000000000..0acd5d7dc1 Binary files /dev/null and b/blog/images/20241218-pub.jpg differ diff --git a/blog/images/20250114-locked-books.jpg b/blog/images/20250114-locked-books.jpg new file mode 100644 index 0000000000..1963b52df5 Binary files /dev/null and b/blog/images/20250114-locked-books.jpg differ diff --git a/blog/images/20250308-captcha.png b/blog/images/20250308-captcha.png new file mode 100644 index 0000000000..7f43b47bd1 Binary files /dev/null and b/blog/images/20250308-captcha.png differ diff --git a/blog/images/20250308-lists.png b/blog/images/20250308-lists.png new file mode 100644 index 0000000000..cd15adbcbc Binary files /dev/null and b/blog/images/20250308-lists.png differ diff --git a/blog/images/20250308-mentions.png b/blog/images/20250308-mentions.png new file mode 100644 index 0000000000..658c7e0011 Binary files /dev/null and b/blog/images/20250308-mentions.png differ diff --git a/blog/images/20250308-reports.png b/blog/images/20250308-reports.png new file mode 100644 index 0000000000..def06bf83e Binary files /dev/null and b/blog/images/20250308-reports.png differ diff --git a/blog/images/simplex-explained.png b/blog/images/simplex-explained.png new file mode 100644 index 0000000000..bf5c738d85 Binary files /dev/null and b/blog/images/simplex-explained.png differ diff --git a/blog/images/simplex-explained.svg b/blog/images/simplex-explained.svg new file mode 100644 index 0000000000..ac57f491ef --- /dev/null +++ b/blog/images/simplex-explained.svg @@ -0,0 +1,82 @@ +<svg width="539" height="399" viewBox="0 0 539 399" fill="none" xmlns="http://www.w3.org/2000/svg"> +<circle cx="268.083" cy="80.9476" r="32.1985" fill="url(#paint0_linear_651_6627)"/> +<circle cx="268.082" cy="80.9513" r="39.3169" stroke="url(#paint1_linear_651_6627)" stroke-width="0.956242"/> +<circle cx="267.428" cy="73.3473" r="5.65297" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M256.025 90.0869H278.595C277.039 85.3429 272.575 81.917 267.31 81.917C262.046 81.917 257.582 85.3429 256.025 90.0869Z" fill="white"/> +<circle cx="40.5779" cy="218.884" r="32.1985" fill="url(#paint2_linear_651_6627)"/> +<circle cx="40.5777" cy="218.888" r="39.3169" stroke="url(#paint3_linear_651_6627)" stroke-width="0.956242"/> +<circle cx="39.9227" cy="211.284" r="5.65297" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M51.0904 228.023H28.5205C30.0767 223.279 34.541 219.854 39.8054 219.854C45.0699 219.854 49.5341 223.279 51.0904 228.023Z" fill="white"/> +<circle cx="268.083" cy="358.611" r="32.1985" fill="url(#paint4_linear_651_6627)"/> +<circle cx="268.082" cy="358.614" r="39.3169" stroke="url(#paint5_linear_651_6627)" stroke-width="0.956242"/> +<circle cx="267.428" cy="351.01" r="5.65297" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M256.025 367.75H278.595C277.039 363.006 272.575 359.58 267.31 359.58C262.046 359.58 257.582 363.006 256.025 367.75Z" fill="white"/> +<circle cx="499.17" cy="217.092" r="32.1985" fill="url(#paint6_linear_651_6627)"/> +<circle cx="499.17" cy="217.096" r="39.3169" stroke="url(#paint7_linear_651_6627)" stroke-width="0.956242"/> +<circle cx="498.515" cy="209.492" r="5.65297" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M509.683 226.231H487.113C488.669 221.487 493.133 218.062 498.398 218.062C503.662 218.062 508.126 221.487 509.683 226.231Z" fill="white"/> +<path d="M79.8735 192.248L224.526 103.265" stroke="#0197FF" stroke-width="2.99282" stroke-linecap="round" stroke-dasharray="5.99 8.98"/> +<path d="M455.16 192.248L310.507 103.265" stroke="#CD5CAE" stroke-width="2.99282" stroke-linecap="round" stroke-dasharray="5.99 8.98"/> +<path d="M222.747 338.695L78.0943 249.712" stroke="#9B90FB" stroke-width="2.99282" stroke-linecap="round" stroke-dasharray="5.99 8.98"/> +<path d="M268.016 301.529L268.016 137.652" stroke="#1BC3BF" stroke-width="2.99282" stroke-linecap="round" stroke-dasharray="5.99 8.98"/> +<rect x="87.3558" y="63.0347" width="42.9204" height="42.9204" rx="2.99282" stroke="#0197FF" stroke-width="1.99521"/> +<path d="M108.408 76.2031H111.281V92.9629H107.977V79.6987L104.817 80.5846L104.002 77.7594L108.408 76.2031Z" fill="#0197FF"/> +<rect x="167.228" y="198.131" width="42.9204" height="42.9204" rx="2.99282" stroke="#0197FF" stroke-width="1.99521"/> +<path d="M190.169 218.101C191.303 218.437 192.22 219.027 192.923 219.873C193.641 220.703 194 221.709 194 222.89C194 224.614 193.417 225.963 192.252 226.936C191.103 227.91 189.69 228.397 188.014 228.397C186.706 228.397 185.532 228.101 184.495 227.511C183.473 226.904 182.731 226.018 182.268 224.853L185.093 223.225C185.508 224.518 186.482 225.164 188.014 225.164C188.86 225.164 189.515 224.965 189.978 224.566C190.457 224.151 190.696 223.592 190.696 222.89C190.696 222.204 190.457 221.653 189.978 221.238C189.515 220.823 188.86 220.615 188.014 220.615H187.296L186.027 218.7L189.331 214.39H182.771V211.302H193.306V214.031L190.169 218.101Z" fill="#0197FF"/> +<rect x="86.9073" y="326.685" width="42.9204" height="42.9204" rx="2.99282" stroke="#0197FF" stroke-width="1.99521"/> +<path d="M108.065 345.96C109.677 345.96 111.034 346.447 112.136 347.42C113.237 348.378 113.788 349.719 113.788 351.443C113.788 353.167 113.205 354.515 112.04 355.489C110.891 356.463 109.478 356.949 107.802 356.949C106.445 356.949 105.248 356.646 104.211 356.04C103.173 355.433 102.423 354.539 101.96 353.358L104.785 351.706C105.2 353.047 106.206 353.717 107.802 353.717C108.648 353.717 109.302 353.518 109.765 353.119C110.244 352.704 110.484 352.145 110.484 351.443C110.484 350.756 110.252 350.206 109.789 349.791C109.326 349.376 108.696 349.168 107.898 349.168H102.822L103.492 339.854H112.997V342.943H106.557L106.341 345.96H108.065Z" fill="#0197FF"/> +<path d="M72.9777 331.097C72.8728 331.917 72.1232 332.497 71.3035 332.392L57.9447 330.682C57.125 330.577 56.5455 329.828 56.6504 329.008C56.7553 328.188 57.5049 327.609 58.3246 327.714L70.199 329.233L71.7186 317.359C71.8235 316.539 72.5731 315.959 73.3928 316.064C74.2126 316.169 74.7921 316.919 74.6872 317.739L72.9777 331.097ZM38.6697 271.837C44.3979 288.6 58.4045 311.53 72.6773 329.992L70.3095 331.823C55.9494 313.248 41.7191 290.016 35.8377 272.805L38.6697 271.837Z" fill="#9B90FB"/> +<path d="M215.639 371.664C216.25 372.22 216.295 373.167 215.738 373.778L206.672 383.737C206.115 384.348 205.169 384.392 204.558 383.836C203.947 383.279 203.902 382.333 204.459 381.722L212.518 372.87L203.666 364.811C203.054 364.254 203.01 363.308 203.566 362.697C204.123 362.085 205.069 362.041 205.68 362.597L215.639 371.664ZM147.42 365.75C164.435 370.679 191.252 372.369 214.562 371.276L214.702 374.265C191.249 375.365 164.057 373.686 146.587 368.625L147.42 365.75Z" fill="#9B90FB"/> +<path d="M186.13 264.557C186.388 263.772 187.233 263.344 188.018 263.601L200.815 267.8C201.6 268.057 202.028 268.903 201.77 269.688C201.513 270.473 200.667 270.901 199.882 270.644L188.507 266.912L184.775 278.286C184.518 279.072 183.672 279.499 182.887 279.242C182.102 278.984 181.674 278.139 181.932 277.353L186.13 264.557ZM228.079 319.816C211.627 305.232 196.047 285.13 186.217 265.699L188.887 264.348C198.544 283.436 213.89 303.238 230.065 317.576L228.079 319.816Z" fill="#9B90FB"/> +<path d="M101.379 249.32C100.828 248.704 100.881 247.758 101.497 247.208L111.539 238.233C112.155 237.682 113.101 237.736 113.652 238.352C114.203 238.968 114.149 239.914 113.533 240.465L104.607 248.442L112.584 257.368C113.135 257.984 113.082 258.93 112.466 259.481C111.85 260.032 110.904 259.978 110.353 259.362L101.379 249.32ZM162.973 242.498C143.252 249.825 124.069 251.033 102.41 249.817L102.578 246.829C124.02 248.033 142.745 246.821 161.931 239.692L162.973 242.498Z" fill="#9B90FB"/> +<rect x="325.884" y="198.131" width="42.9204" height="42.9204" rx="2.99282" stroke="#0197FF" stroke-width="1.99521"/> +<path d="M353.504 222.148V225.212H351.708V228.062H348.38V225.212H340.455V222.148L346.034 211.302H349.577L344.022 222.148H348.38V218.173H351.708V222.148H353.504Z" fill="#0197FF"/> +<path d="M317.746 191.65C318.511 191.964 319.385 191.599 319.699 190.835L324.815 178.376C325.129 177.612 324.764 176.738 323.999 176.424C323.235 176.11 322.36 176.475 322.046 177.24L317.499 188.313L306.425 183.766C305.661 183.452 304.786 183.817 304.472 184.582C304.158 185.346 304.524 186.221 305.288 186.535L317.746 191.65ZM283.185 132.538C294.926 145.802 307.938 169.311 316.934 190.843L319.696 189.689C310.645 168.026 297.481 144.173 285.425 130.554L283.185 132.538Z" fill="#1BC3BF"/> +<path d="M302.485 317.64C302.635 318.453 303.416 318.99 304.228 318.839L317.472 316.393C318.284 316.243 318.822 315.462 318.671 314.649C318.521 313.837 317.741 313.3 316.928 313.45L305.156 315.625L302.981 303.853C302.831 303.04 302.05 302.503 301.238 302.653C300.425 302.803 299.888 303.584 300.038 304.396L302.485 317.64ZM333.458 256.571C328.667 273.625 315.952 297.296 302.724 316.52L305.189 318.216C318.498 298.875 331.42 274.89 336.339 257.38L333.458 256.571Z" fill="#1BC3BF"/> +<path d="M214.41 250.543C213.646 250.228 212.771 250.592 212.456 251.356L207.324 263.807C207.009 264.571 207.373 265.446 208.137 265.761C208.902 266.076 209.776 265.712 210.091 264.948L214.653 253.88L225.721 258.442C226.485 258.757 227.36 258.393 227.675 257.629C227.99 256.864 227.626 255.99 226.862 255.675L214.41 250.543ZM248.894 309.7C237.17 296.421 224.189 272.895 215.221 251.351L212.458 252.501C221.481 274.177 234.613 298.046 246.651 311.681L248.894 309.7Z" fill="#1BC3BF"/> +<path d="M252.992 138.384C252.581 137.667 251.666 137.42 250.949 137.832L239.272 144.542C238.556 144.954 238.309 145.868 238.72 146.585C239.132 147.302 240.047 147.549 240.763 147.137L251.143 141.172L257.107 151.552C257.519 152.269 258.434 152.516 259.15 152.104C259.867 151.692 260.114 150.778 259.702 150.061L252.992 138.384ZM223.657 201.324C235.535 186.647 247.194 161.53 253.14 139.52L250.25 138.739C244.379 160.474 232.874 185.176 221.331 199.442L223.657 201.324Z" fill="#1BC3BF"/> +<rect x="398.022" y="63.0347" width="42.9204" height="42.9204" rx="2.99282" stroke="#0197FF" stroke-width="1.99521"/> +<path d="M413.321 92.9629V90.4968L419.044 84.607C420.321 83.2662 420.959 82.1489 420.959 81.255C420.959 80.6006 420.752 80.0738 420.337 79.6748C419.938 79.2757 419.419 79.0762 418.78 79.0762C417.519 79.0762 416.586 79.7307 415.979 81.0395L413.202 79.4114C413.728 78.2622 414.487 77.3843 415.476 76.7777C416.466 76.1712 417.551 75.8679 418.732 75.8679C420.249 75.8679 421.55 76.3468 422.635 77.3045C423.721 78.2462 424.263 79.5232 424.263 81.1353C424.263 82.8751 423.345 84.6628 421.51 86.4984L418.23 89.7785H424.527V92.9629H413.321Z" fill="#0197FF"/> +<path d="M77.1479 111.221C77.253 110.401 76.6737 109.651 75.854 109.546L62.4958 107.833C61.676 107.727 60.9263 108.307 60.8211 109.126C60.716 109.946 61.2953 110.696 62.115 110.801L73.989 112.324L72.4659 124.198C72.3607 125.018 72.94 125.768 73.7598 125.873C74.5795 125.978 75.3292 125.399 75.4344 124.579L77.1479 111.221ZM42.3631 168.218C46.7014 146.722 60.4086 124.708 76.5785 112.214L74.7487 109.846C57.9918 122.793 43.9057 145.446 39.4294 167.626L42.3631 168.218Z" fill="#0197FF"/> +<path d="M215.45 70.1934C216.205 69.8575 216.545 68.973 216.209 68.2179L210.734 55.9134C210.398 55.1584 209.513 54.8186 208.758 55.1546C208.003 55.4906 207.663 56.3751 207.999 57.1301L212.866 68.0675L201.929 72.9343C201.174 73.2703 200.834 74.1548 201.17 74.9098C201.506 75.6649 202.391 76.0046 203.146 75.6686L215.45 70.1934ZM148.677 70.0794C169.298 62.6197 195.23 62.8963 214.305 70.2232L215.378 67.4294C195.611 59.8365 168.936 59.5682 147.659 67.2651L148.677 70.0794Z" fill="#0197FF"/> +<path d="M203.562 181.745C203.705 182.559 204.482 183.102 205.295 182.959L218.558 180.62C219.372 180.476 219.916 179.7 219.772 178.886C219.629 178.072 218.852 177.529 218.039 177.672L206.249 179.752L204.17 167.962C204.027 167.148 203.25 166.605 202.436 166.749C201.623 166.892 201.079 167.668 201.223 168.482L203.562 181.745ZM235.03 120.93C230.101 137.944 217.194 161.511 203.81 180.627L206.261 182.343C219.727 163.111 232.843 139.232 237.905 121.762L235.03 120.93Z" fill="#0197FF"/> +<path d="M86.7735 223.138C86.1511 223.682 86.0872 224.627 86.6309 225.249L95.4903 235.393C96.034 236.015 96.9793 236.079 97.6018 235.535C98.2242 234.992 98.2881 234.046 97.7444 233.424L89.8693 224.408L98.8857 216.533C99.5081 215.989 99.572 215.044 99.0283 214.421C98.4846 213.799 97.5393 213.735 96.9169 214.279L86.7735 223.138ZM155.1 218.631C137.987 223.208 111.141 224.345 87.8588 222.772L87.657 225.758C111.082 227.341 138.303 226.222 155.873 221.522L155.1 218.631Z" fill="#0197FF"/> +<path d="M383.345 66.4144C384.119 66.1236 384.51 65.2608 384.219 64.4872L379.481 51.8806C379.19 51.107 378.327 50.7156 377.554 51.0064C376.78 51.2971 376.389 52.16 376.679 52.9336L380.891 64.1394L369.686 68.3513C368.912 68.6421 368.521 69.505 368.811 70.2786C369.102 71.0522 369.965 71.4436 370.739 71.1528L383.345 66.4144ZM316.695 62.3541C337.722 56.1262 363.592 57.9349 382.2 66.3764L383.437 63.6509C364.152 54.903 337.54 53.0586 315.845 59.4845L316.695 62.3541Z" fill="#CD5CAE"/> +<path d="M484.334 169.427C484.984 169.937 485.925 169.823 486.435 169.173L494.746 158.575C495.256 157.925 495.142 156.984 494.491 156.474C493.841 155.964 492.901 156.078 492.391 156.728L485.003 166.149L475.583 158.761C474.933 158.251 473.992 158.365 473.482 159.016C472.972 159.666 473.086 160.607 473.736 161.117L484.334 169.427ZM453.18 110.367C469.425 125.098 481.318 148.143 483.772 168.429L486.743 168.07C484.2 147.047 471.952 123.35 455.191 108.15L453.18 110.367Z" fill="#CD5CAE"/> +<path d="M381.01 219.006C380.358 219.514 380.241 220.455 380.749 221.107L389.024 231.732C389.532 232.384 390.472 232.501 391.124 231.993C391.776 231.485 391.893 230.545 391.386 229.893L384.03 220.448L393.475 213.093C394.127 212.585 394.244 211.645 393.736 210.993C393.228 210.341 392.288 210.224 391.636 210.731L381.01 219.006ZM449.482 218.349C432.139 221.957 405.272 221.582 382.115 218.702L381.745 221.672C405.044 224.57 432.285 224.983 450.091 221.279L449.482 218.349Z" fill="#CD5CAE"/> +<path d="M305.2 125.099C304.379 125.194 303.791 125.937 303.886 126.758L305.439 140.136C305.534 140.957 306.277 141.545 307.098 141.45C307.919 141.355 308.507 140.612 308.412 139.791L307.032 127.9L318.923 126.52C319.744 126.424 320.332 125.682 320.237 124.861C320.142 124.04 319.399 123.451 318.578 123.547L305.2 125.099ZM342.253 184.234C340.61 174.231 335.593 162.688 329.05 152.113C322.503 141.531 314.345 131.783 306.302 125.413L304.444 127.759C312.143 133.857 320.086 143.312 326.505 153.687C332.928 164.069 337.742 175.237 339.299 184.719L342.253 184.234Z" fill="#CD5CAE"/> +<defs> +<linearGradient id="paint0_linear_651_6627" x1="299.334" y1="48.749" x2="222.184" y2="75.0796" gradientUnits="userSpaceOnUse"> +<stop stop-color="#53C1FF"/> +<stop offset="1" stop-color="#0053D0"/> +</linearGradient> +<linearGradient id="paint1_linear_651_6627" x1="306.707" y1="41.1562" x2="211.356" y2="73.6989" gradientUnits="userSpaceOnUse"> +<stop stop-color="#53C1FF"/> +<stop offset="1" stop-color="#0053D0"/> +</linearGradient> +<linearGradient id="paint2_linear_651_6627" x1="71.8294" y1="186.686" x2="-5.32023" y2="213.016" gradientUnits="userSpaceOnUse"> +<stop stop-color="#53C1FF"/> +<stop offset="1" stop-color="#0053D0"/> +</linearGradient> +<linearGradient id="paint3_linear_651_6627" x1="79.2023" y1="179.093" x2="-16.149" y2="211.635" gradientUnits="userSpaceOnUse"> +<stop stop-color="#53C1FF"/> +<stop offset="1" stop-color="#0053D0"/> +</linearGradient> +<linearGradient id="paint4_linear_651_6627" x1="299.334" y1="326.412" x2="222.184" y2="352.743" gradientUnits="userSpaceOnUse"> +<stop stop-color="#53C1FF"/> +<stop offset="1" stop-color="#0053D0"/> +</linearGradient> +<linearGradient id="paint5_linear_651_6627" x1="306.707" y1="318.819" x2="211.356" y2="351.362" gradientUnits="userSpaceOnUse"> +<stop stop-color="#53C1FF"/> +<stop offset="1" stop-color="#0053D0"/> +</linearGradient> +<linearGradient id="paint6_linear_651_6627" x1="530.421" y1="184.894" x2="453.272" y2="211.224" gradientUnits="userSpaceOnUse"> +<stop stop-color="#53C1FF"/> +<stop offset="1" stop-color="#0053D0"/> +</linearGradient> +<linearGradient id="paint7_linear_651_6627" x1="537.794" y1="177.301" x2="442.443" y2="209.843" gradientUnits="userSpaceOnUse"> +<stop stop-color="#53C1FF"/> +<stop offset="1" stop-color="#0053D0"/> +</linearGradient> +</defs> +</svg> diff --git a/blog/images/simplexonflux.png b/blog/images/simplexonflux.png new file mode 100644 index 0000000000..dad3f480f6 Binary files /dev/null and b/blog/images/simplexonflux.png differ diff --git a/blog/lang/fr-fr/README_fr.md b/blog/lang/fr-fr/README_fr.md index d3deefe3f0..471f3ce046 100644 --- a/blog/lang/fr-fr/README_fr.md +++ b/blog/lang/fr-fr/README_fr.md @@ -1,6 +1,6 @@ # Blog -4 févr. 2023 [SimpleX Chat v4.5 publié](./20230103-simplex-chat-v4.4-disappearing-messages.md) +4 févr. 2023 [SimpleX Chat v4.5 publié](../../20230103-simplex-chat-v4.4-disappearing-messages.md) - profils de chat multiples. - brouillon de message. @@ -10,7 +10,7 @@ Nous avons également ajouté [l'interface en italien](#french-language-interface), grâce à nos utilisateurs et à Weblate ! -3 janv. 2023 [SimpleX Chat v4.4 publié](./20230103-simplex-chat-v4.4-disappearing-messages.md) +3 janv. 2023 [SimpleX Chat v4.4 publié](../../20230103-simplex-chat-v4.4-disappearing-messages.md) - messages éphèméres. - messages "en direct" (dynamique). @@ -19,14 +19,14 @@ Nous avons également ajouté [l'interface en italien](#french-language-interfac Nous avons également ajouté [l'interface en français](#french-language-interface), grâce à nos utilisateurs et à Weblate ! -6 déc. 2022 [SimpleX Chat : révision et sortie de la v4.3](./20221206-simplex-chat-v4.3-voice-messages.md) +6 déc. 2022 [SimpleX Chat : révision et sortie de la v4.3](../../20221206-simplex-chat-v4.3-voice-messages.md) Critiques de novembre : - Recommandations de [Privacy Guides](https://www.privacyguides.org/real-time-communication/#simplex-chat). - [Revue par Mike Kuketz](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/). - [La matrice des messageries](https://www.messenger-matrix.de). -- [Revue de Supernova](https://supernova.tilde.team/detailed_reviews.html#simplex) et [évaluations des messageries](https://supernova.tilde.team/messengers.html). +- [Revue de Supernova](https://supernovas.space/detailed_reviews.html#simplex) et [évaluations des messageries](https://supernovas.space/messengers.html). Sortie de la v4.3 : @@ -35,7 +35,7 @@ Sortie de la v4.3 : - amélioration de la configuration du serveur SMP et du support des mots de passe du serveur - améliorations de la confidentialité et de la sécurité : protection de l'écran de l'application, sécurité des liens SimpleX, etc. -8 nov. 2022 [Audit de sécurité par Trail of Bits, nouveau site web et sortie de la v4.2](./20221108-simplex-chat-v4.2-security-audit-new-website.md) +8 nov. 2022 [Audit de sécurité par Trail of Bits, nouveau site web et sortie de la v4.2](../../20221108-simplex-chat-v4.2-security-audit-new-website.md) _"Avez-vous été audité ou devons-nous simplement vous ignorer ?"_ @@ -51,7 +51,7 @@ Sortie de la v4.2 : - changer manuellement de contact ou de membre vers une autre adresse / serveur (BETA) - recevoir des fichiers plus rapidement (BETA) -28 sept. 2022 [v4 : chiffrement de la base de données locale](./20220928-simplex-chat-v4-encrypted-database.md) +28 sept. 2022 [v4 : chiffrement de la base de données locale](../../20220928-simplex-chat-v4-encrypted-database.md) - base de données locale de chat chiffrée - si vous utilisez déjà l'application, vous pouvez chiffrer la base de données dans les paramètres de l'application. - support pour les serveurs WebRTC ICE auto-hébergés @@ -61,7 +61,7 @@ Sortie de la v4.2 : - support des images animées dans l'application Android - Interface utilisateur en allemand pour les applications mobiles -1 sept. 2022 [v3.2 : Mode Incognito](./20220901-simplex-chat-v3.2-incognito-mode.md) +1 sept. 2022 [v3.2 : Mode Incognito](../../20220901-simplex-chat-v3.2-incognito-mode.md) - Mode Incognito - utiliser un nouveau nom de profil aléatoire pour chaque contact - utiliser des adresses de serveur .onion avec Tor @@ -71,7 +71,7 @@ Sortie de la v4.2 : L'audit d'implémentation est prévu pour Octobre ! -8 août 2022 [v3.1 : groupes de discussion](./20220808-simplex-chat-v3.1-chat-groups.md) +8 août 2022 [v3.1 : groupes de discussion](../../20220808-simplex-chat-v3.1-chat-groups.md) - enfin, des groupes de chat secrets - personne d'autre que les membres ne sait qu'ils existent ! - accès aux serveurs de messagerie via Tor sur toutes les plateformes @@ -79,37 +79,37 @@ L'audit d'implémentation est prévu pour Octobre ! - protocole de chat publié - nouvelles icônes d'application -23 juil. 2022 [v3.1-beta : accès aux serveurs via Tor](./20220723-simplex-chat-v3.1-tor-groups-efficiency.md) +23 juil. 2022 [v3.1-beta : accès aux serveurs via Tor](../../20220723-simplex-chat-v3.1-tor-groups-efficiency.md) - application terminale : accès aux serveurs de messagerie via un proxy SOCKS5 (par exemple, Tor). - applications mobiles : rejoindre et quitter des groupes de discussion. - utilisation optimisée de la batterie et du trafic - réduction jusqu'à 90x ! - deux configurations docker pour les serveurs SMP auto-hébergés. -11 juil. 2022 [v3 : notifications push instantanées pour iOS et appels audio/vidéo](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md) : +11 juil. 2022 [v3 : notifications push instantanées pour iOS et appels audio/vidéo](../../20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md) : - exportation et importation de la base de données de chat - appels audio/vidéo chiffrés de bout en bout - amélioration de la confidentialité du protocole et des performances -4 juin 2022 [v2.2 : nouveaux paramètres de confidentialité et de sécurité](./20220604-simplex-chat-new-privacy-security-settings.md) +4 juin 2022 [v2.2 : nouveaux paramètres de confidentialité et de sécurité](../../20220604-simplex-chat-new-privacy-security-settings.md) -24 mai 2022 [v2.1 : effacement des messages pour une meilleure confidentialité des conversations](./20220524-simplex-chat-better-privacy.md) +24 mai 2022 [v2.1 : effacement des messages pour une meilleure confidentialité des conversations](../../20220524-simplex-chat-better-privacy.md) -11 mai 2022 [Publication de la v2.0 - envoi d'images et de fichiers dans les applications mobiles](./20220511-simplex-chat-v2-images-files.md) +11 mai 2022 [Publication de la v2.0 - envoi d'images et de fichiers dans les applications mobiles](../../20220511-simplex-chat-v2-images-files.md) -04 avr. 2022 [Notifications instantanées pour les applications mobiles SimpleX Chat](./20220404-simplex-chat-instant-notifications.md) +04 avr. 2022 [Notifications instantanées pour les applications mobiles SimpleX Chat](../../20220404-simplex-chat-instant-notifications.md) -08 mars 2022 [Applications mobiles pour iOS et Android](./20220308-simplex-chat-mobile-apps.md) +08 mars 2022 [Applications mobiles pour iOS et Android](../../20220308-simplex-chat-mobile-apps.md) -14 févr. 2022. [SimpleX Chat : rejoignez notre version bêta publique pour iOS](./20220214-simplex-chat-ios-public-beta.md) +14 févr. 2022. [SimpleX Chat : rejoignez notre version bêta publique pour iOS](../../20220214-simplex-chat-ios-public-beta.md) -12 janv. 2022. [SimpleX Chat v1 : la plateforme de chat et d'application la plus privée et la plus sécurisée](./20220112-simplex-chat-v1-released.md) +12 janv. 2022. [SimpleX Chat v1 : la plateforme de chat et d'application la plus privée et la plus sécurisée](../../20220112-simplex-chat-v1-released.md) -08 déc. 2021. [Sortie de SimpleX Chat v0.5 : la première plateforme de chat 100% privée par définition - aucun accès à votre graphe de connexions](./20211208-simplex-chat-v0.5-released.md) +08 déc. 2021. [Sortie de SimpleX Chat v0.5 : la première plateforme de chat 100% privée par définition - aucun accès à votre graphe de connexions](../../20211208-simplex-chat-v0.5-released.md) -14 septembre 2021. [SimpleX Chat v0.4 publié : chat open-source qui utilise un protocole de routage de messages préservant la confidentialité](./20210914-simplex-chat-v0.4-released.md) +14 septembre 2021. [SimpleX Chat v0.4 publié : chat open-source qui utilise un protocole de routage de messages préservant la confidentialité](../../20210914-simplex-chat-v0.4-released.md) -12 mai 2021. [Prototype de chat SimpleX](./20210512-simplex-chat-terminal-ui.md) +12 mai 2021. [Prototype de chat SimpleX](../../20210512-simplex-chat-terminal-ui.md) -22 oct. 2020. [SimpleX Chat](./20201022-simplex-chat.md) +22 oct. 2020. [SimpleX Chat](../../20201022-simplex-chat.md) diff --git a/cabal.project b/cabal.project index baf7d5c4c1..f406b9820e 100644 --- a/cabal.project +++ b/cabal.project @@ -2,8 +2,6 @@ packages: . -- packages: . ../simplexmq -- packages: . ../simplexmq ../direct-sqlcipher ../sqlcipher-simple -with-compiler: ghc-9.6.3 - index-state: 2023-12-12T00:00:00Z package cryptostore @@ -14,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: d0588bd0ac23a459cbfc9a4789633014e91ffa19 + tag: 3d62a383d5dcae6529d6d866233857182bcb4d47 source-repository-package type: git @@ -51,3 +49,17 @@ source-repository-package type: git location: https://github.com/simplex-chat/zip.git tag: bd421c6b19cc4c465cd7af1f6f26169fb8ee1ebc + +-- waiting for published warp-tls-3.4.7 +source-repository-package + type: git + location: https://github.com/yesodweb/wai.git + tag: ec5e017d896a78e787a5acea62b37a4e677dec2e + subdir: warp-tls + +-- backported fork due http-5.0 +source-repository-package + type: git + location: https://github.com/simplex-chat/wai.git + tag: 2f6e5aa5f05ba9140ac99e195ee647b4f7d926b0 + subdir: warp diff --git a/docs/ANDROID.md b/docs/ANDROID.md index fa8921c827..61f81d1a40 100644 --- a/docs/ANDROID.md +++ b/docs/ANDROID.md @@ -3,7 +3,7 @@ title: Accessing files in Android app revision: 07.02.2023 --- -| 07.02.2023 | EN, [CZ](/docs/lang/cs/ANDROID.md), [FR](/docs/lang/fr/ANDROID.md) | +| 07.02.2023 | EN, [CZ](/docs/lang/cs/ANDROID.md), [FR](/docs/lang/fr/ANDROID.md), [PL](/docs/lang/pl/ANDROID.md) | # Accessing files in Android app diff --git a/docs/BUSINESS.md b/docs/BUSINESS.md new file mode 100755 index 0000000000..8fd5df5c36 --- /dev/null +++ b/docs/BUSINESS.md @@ -0,0 +1,127 @@ +--- +title: SimpleX for business +revision: 03.12.2024 +--- + +# Using SimpleX Chat in business + +SimpleX Chat (aka SimpleX) is a decentralized communication network that provides private and secure messaging. Its users are rapidly growing, and providing customer services via SimpleX can offer you a unique opportunity to engage people who are the most enthusiastic about trying out early stage technology products and services. + +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 + +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, +- to ask any questions, make suggestions and provide feedback, +- to discover more information about your business. + +Customers who value privacy and security, and want to engage with you without sharing any personal data and minimizing any metadata that is shared with you, will be really happy to use this communication channel. + +From v6.2 SimpleX Chat supports business addresses. Their design allows you to accept requests from multiple customers, with the app creating a new business chat with each of them. + +Business chats operate in a way similar to dedicated customer support systems by combining features of direct conversations and groups, and the only widely used messenger that provides such functionality is WeChat with Chinese business accounts. + +When a customer connects to your business via the business contact address, a new conversation is created. Similarly to how direct chats work, the customer will see the name and logo of your business, and you will see the name and avatar of your customer. + +But the business conversation works as a group - once the customer is connected, other people from the business can be added to the conversation, and the customers will see who are they talking with. This can be used to transfer business conversation to another person, or for escalation - in the same way as with the dedicated support systems. + +SimpleX Chat profile with the business address can be used in one of these ways: +- for small teams it can be managed by one person running the app on their desktop computer, who would respond to customer questions and manually add to the conversation other people in the business, as required. +- if you have multiple support agents, you can run business profile in CLI client running in cloud VM or on any machine with high speed Internet (see Technical advice below), and they can connect to this client from desktop client, in turns. This is how we use our business profile ourselves, even though it requires some configuration. You can manage 100s of thousands of connected customers in this way. +- For larger teams, it would be appropriate to have this profile managed by chat bot that can reply to some simple questions, and to add support agents, based on their availability and the questions asked. These scenarios would require programming a chat bot, and we are currently working to simplify it. + +In any case, it is important that the client application remains running and connected to the Internet for you to receive support requests. + +## Customer broadcasts + +While currently supported only via CLI clients (or via chat console in desktop and mobile clients), it can be used to broadcast important announcements to all connected customers. We will be adding this feature to desktop clients soon. We use it to broadcast release updates to a very large number of users who are connected to our own support profile. + +## Community groups and promotion in group directory + +In addition to providing support to clients individually, you can create a community group, and promote it via our experimental and growing [directory of public groups](./DIRECTORY.md). Community groups require ongoing moderation. + +## Limitations + +With all advantages in privacy and security of e2e encryption in SimpleX Chat, there are some important limitations: +- **protecting your data from loss is your responsibility**. This is the price of privacy - if you lose your device, or database passphrase, there is absolutely no way we would be able to support you to recover access. There are ways to work around these limitations. +- **you cannot access the same profile from multiple devices**. For all communication products it's a basic expectation, and yet there is not a single one that delivered it without some very serious privacy and security compromises. Better solutions are possible, and we will be implementing it, but reasonably secure approach is much more complex to implement than what is affordable at the current stage. You can access mobile or CLI profile from desktop, and the latter allows to use one profile by multiple people in turns, as we explain below. +- **your owner role in the groups cannot be restored if you lose the device**. The solution is to create owner profiles on multiple devices for all your important groups. This way if you lose device or data for one of profiles, you won't lose control of the group, and you can add a new one. Think about it as about keys to your cryptowallet. +- **current groups are highly experimental**. Message delivery can be delayed or fail in some cases, lists of members can be out of sync. There are approaches to make them more stable we use for our groups. + +## Technical advice + +### Running SimpleX Chat in the cloud + +To install SimpleX Chat CLI in the cloud, follow this: + +1. Create dedicated user for CLI: + + ```sh + useradd -m -s /bin/bash simplex-cli + ``` + +2. Create new tmux session + + ```sh + tmux new -s simplex-cli + ``` + +3. Login to dedicated user: + + ```sh + su - simplex-cli + ``` + +4. Install CLI: + + ```sh + curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/stable/install.sh | bash + ``` + +5. Run the CLI: + + ```sh + simplex-chat + ``` + +To deattach from running CLI simply press `Ctrl+B` and then `D`. + +To reattach back to CLI, run: `tmux attach -t simplex-cli`. + +### Using remote profiles via Desktop app + +To use CLI from Desktop app, follow this: + +1. Enable Developer tools in desktop app. + +2. In the Desktop app, click - `Linked mobile` -> `+ Link a mobile`, choose local address `127.0.0.1`, enter some fixed port (can be any free port, e.g. 12345), and copy the link. + +3. In the same machine where Desktop app is running, execute: + + Change `PORT` to port, chosen in the previous step in Desktop app and `SERVER_IP` to your server. + + ```sh + ssh -R PORT:127.0.0.1:PORT -N root@SERVER_IP + ``` + +4. In the CLI on the server: + + Change `LINK` to link, copied in the first step and enter the following: + + ```sh + /crc LINK + ``` + + CLI will print verification code: + + ```sh + Compare session code with controller and use: + /verify remote ctrl ... + ``` + + Simply copy the whole line starting with `/verify ...` from the terminal and paste it. Now you can control the CLI from your Desktop app. + +## Organizations using SimpleX Chat for customer service, support and sales + +Please let us know if you use SimpleX Chat to communicate with your customers and want to be included in this list. diff --git a/docs/CLI.md b/docs/CLI.md index baf79bb3bc..628fe2a4af 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -3,7 +3,7 @@ title: Terminal CLI revision: 31.01.2023 --- -| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/CLI.md), [CZ](/docs/lang/cs/CLI.md) | +| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/CLI.md), [CZ](/docs/lang/cs/CLI.md), [PL](/docs/lang/pl/CLI.md) | # SimpleX Chat terminal (console) app for Linux/MacOS/Windows @@ -98,7 +98,7 @@ git checkout stable DOCKER_BUILDKIT=1 docker build --output ~/.local/bin . ``` -> **Please note:** If you encounter `` version `GLIBC_2.28' not found `` error, rebuild it with `haskell:8.10.7-stretch` base image (change it in your local [Dockerfile](Dockerfile)). +> **Please note:** If you encounter `` version `GLIBC_2.28' not found `` error, rebuild it with `haskell:8.10.7-stretch` base image (change it in your local [Dockerfile](/Dockerfile)). #### In any OS @@ -120,7 +120,7 @@ git checkout stable # git checkout v5.3.0-beta.8 ``` -`master` is a development branch, it may containt unstable code. +`master` is a development branch, it may contain unstable code. 3. Prepare the system: @@ -134,7 +134,7 @@ cp scripts/cabal.project.local.linux cabal.project.local On Mac: ``` -brew install openssl@1.1 +brew install openssl@3.0 cp scripts/cabal.project.local.mac cabal.project.local ``` diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index aaf452af00..e7ce63ea54 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -3,7 +3,7 @@ title: Contributing guide revision: 31.01.2023 --- -| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/CONTRIBUTING.md), [CZ](/docs/lang/cs/CONTRIBUTING.md) | +| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/CONTRIBUTING.md), [CZ](/docs/lang/cs/CONTRIBUTING.md), [PL](/docs/lang/pl/CONTRIBUTING.md) | # Contributing guide @@ -21,9 +21,9 @@ cp scripts/cabal.project.local.mac cabal.project.local MacOS comes with LibreSSL as default, OpenSSL must be installed to compile SimpleX from source. -OpenSSL can be installed with `brew install openssl@1.1` +OpenSSL can be installed with `brew install openssl@3.0` -You will have to add `/opt/homebrew/opt/openssl@1.1/bin` to your PATH in order to have things working properly +You will have to add `/opt/homebrew/opt/openssl@3.0/bin` to your PATH in order to have things working properly ## Project branches @@ -34,48 +34,49 @@ You will have to add `/opt/homebrew/opt/openssl@1.1/bin` to your PATH in order t - `stable-android` - used to build stable Android core library with Nix (GHC 8.10.7) - only for Android armv7a. -- `stable-ios` - used to build stable iOS core library with Nix (GHC 8.10.7) – this branch should be the same as `stable-android` except Nix configuration files. Deprecated. - -- `master` - branch for beta version releases (GHC 9.6.3). - -- `master-ghc8107` - branch for beta version releases (GHC 8.10.7). Deprecated. +- `master` - branch for beta version releases (compatible with both GHC 9.6.3 and 8.10.7). - `master-android` - used to build beta Android core library with Nix (GHC 8.10.7) - only for Android armv7a. -- `master-ios` - used to build beta iOS core library with Nix (GHC 8.10.7). Deprecated. - -- `windows-ghc8107` - branch for windows core library build (GHC 8.10.7). Deprecated? - -`master-ios` and `windows-ghc8107` branches should be the same as `master-ghc8107` except Nix configuration files. - **In simplexmq repo** -- `master` - uses GHC 9.6.3 its commit should be used in `master` branch of simplex-chat repo. - -- `master-ghc8107` - its commit should be used in `master-android` (and `master-ios`) branch of simplex-chat repo. Deprecated. +- `master` - compatible with both GHC 9.6.3 and 8.10.7. ## Development & release process 1. Make PRs to `master` branch _only_ for both simplex-chat and simplexmq repos. -2. If simplexmq repo was changed, to build mobile core libraries you need to merge its `master` branch into `master-ghc8107` branch. - -3. To build core libraries for Android, iOS and windows: +2. To build core libraries for Android, iOS and windows: - merge `master` branch to `master-android` branch. -- update code to be compatible with GHC 8.10.7 (see below). - push to GitHub. -4. All libraries should be built from `master` branch, Android armv7a - from `master-android` branch. +3. All libraries should be built from `master` branch, Android armv7a - from `master-android` branch. -5. To build Desktop and CLI apps, make tag in `master` branch, APK files should be attached to the release. +4. To build Desktop and CLI apps, make tag in `master` branch, APK files should be attached to the release. -6. After the public release to App Store and Play Store, merge: +5. After the public release to App Store and Play Store, merge: - `master` to `stable` - `master` to `master-android` (and compile/update code) - `master-android` to `stable-android` -7. Independently, `master` branch of simplexmq repo should be merged to `stable` branch on stable releases. +6. Independently, `master` branch of simplexmq repo should be merged to `stable` branch on stable releases. +## Branches and PRs + +Use change scope (or comma separated scopes) as the first word in the PR names, followed by the colon. Commit name itself should be lowercase, in present tense. + +The PR names in simplex-chat repo are used in release notes, they should describe the solved problem and not the change. Possible PR scopes: +- ios +- android +- desktop +- core +- docs +- website +- ci + +We squash PRs, do not rewrite branch history after the review. + +For some complex features we create feature branches that will be merged once ready - do not make commits directly to them, make PRs to feature branches. ## Differences between GHC 8.10.7 and GHC 9.6.3 diff --git a/docs/DOWNLOADS.md b/docs/DOWNLOADS.md index 5362e4f2c3..df22ff64a2 100644 --- a/docs/DOWNLOADS.md +++ b/docs/DOWNLOADS.md @@ -1,14 +1,12 @@ --- title: Download SimpleX apps permalink: /downloads/index.html -revision: 25.11.2023 +revision: 09.09.2024 --- -| Updated 25.11.2023 | Languages: EN | +| Updated 09.09.2024 | Languages: EN | # Download SimpleX apps -The latest stable version is v5.4.1. - You can get the latest beta releases from [GitHub](https://github.com/simplex-chat/simplex-chat/releases). - [desktop](#desktop-app) @@ -19,26 +17,26 @@ You can get the latest beta releases from [GitHub](https://github.com/simplex-ch <img src="/docs/images/simplex-desktop-light.png" alt="desktop app" width=500> -Using the same profile as on mobile device is not yet supported – you need to create a separate profile to use desktop apps. +You can link your mobile device with desktop to use the same profile remotely, but this is only possible when both devices are connected to the same local network. -**Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-desktop-x86_64.AppImage) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-desktop-ubuntu-22_04-x86_64.deb). +**Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-x86_64.AppImage) or [Flatpak](https://flathub.org/apps/chat.simplex.simplex) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-22_04-x86_64.deb). -**Mac**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-desktop-macos-x86_64.dmg) (Intel), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-desktop-macos-aarch64.dmg) (Apple Silicon). +**Mac**: [aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-macos-aarch64.dmg) (Apple Silicon), [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-macos-x86_64.dmg) (Intel). -**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-desktop-windows-x86_64.msi). +**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-windows-x86_64.msi). ## Mobile apps **iOS**: [App store](https://apps.apple.com/us/app/simplex-chat/id1605771084), [TestFlight](https://testflight.apple.com/join/DWuT2LQu). -**Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-armv7a.apk). +**Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-armv7a.apk). ## Terminal (console) app See [Using terminal app](/docs/CLI.md). -**Linux**: [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-chat-ubuntu-20_04-x86-64), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-chat-ubuntu-22_04-x86-64). +**Linux**: [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-ubuntu-20_04-x86-64), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-ubuntu-22_04-x86-64). -**Mac** [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-chat-macos-x86-64), aarch64 - [compile from source](./CLI.md#). +**Mac** [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-macos-x86-64), aarch64 - [compile from source](/docs/CLI.md#). -**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-chat-windows-x86-64). +**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-windows-x86-64). diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 0000000000..0d0426d7c9 --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,340 @@ +--- +title: Frequently Asked Questions +permalink: /faq/index.html +revision: 23.04.2024 +--- + +# Frequently Asked Questions + +[How to use it](#how-to-use-it) +- [I have nobody to chat with! Where can I find any groups?](#i-have-nobody-to-chat-with-where-can-i-find-any-groups) +- [What is database? What can I do with it?](#what-is-database-what-can-i-do-with-it) +- [Can I send files over SimpleX? ](#can-i-send-files-over-simplex) +- [What’s incognito profile?](#whats-incognito-profile) +- [How do invitations work?](#how-do-invitations-work) +- [How to configure and delete groups?](#how-to-configure-and-delete-groups) +- [Are there any reactions to messages? Can I answer specific messages directly?](#are-there-any-reactions-to-messages-can-i-answer-specific-messages-directly) +- [What do checkmarks mean?](#what-do-checkmarks-mean) +- [I want to see when my contacts read my messages](#i-want-to-see-when-my-contacts-read-my-messages) +- [Can I use the same profile on desktop? Do messages sync cross-platform?](#can-i-use-the-same-profile-on-desktop-do-messages-sync-cross-platform) +- [Why cannot I delete messages I sent from my contact's device?](#why-cannot-i-delete-messages-i-sent-from-my-contacts-device) +- [Why invitation links use simplex.chat domain?](#why-invitation-links-use-simplex.chat-domain) + +[Troubleshooting](#troubleshooting) +- [I do not receive messages or message notifications](#i-do-not-receive-messages-or-message-notifications) +- [I do not see the second tick on the messages I sent](#i-do-not-see-the-second-tick-on-the-messages-i-sent) +- [I see image preview but cannot open the image](#i-see-image-preview-but-cannot-open-the-image) +- [I cannot play a voice message](#i-cannot-play-a-voice-message) +- [Audio or video calls do not connect](#audio-or-video-calls-do-not-connect) +- [Audio or video calls without e2e encryption](#audio-or-video-calls-without-e2e-encryption) +- [I clicked the link to connect, but could not connect](#i-clicked-the-link-to-connect-but-could-not-connect) +- [I do not know my database passphrase](#i-do-not-know-my-database-passphrase) + +[Privacy and security](#privacy-and-security) +- [Does SimpleX support post quantum cryptography?](#does-simplex-support-post-quantum-cryptography) +- [What user data can be provided on request?](#what-user-data-can-be-provided-on-request) +- [Does SimpleX protect my IP address?](#does-simplex-protect-my-ip-address) +- [Doesn't private message routing reinvent Tor?](#doesnt-private-message-routing-reinvent-tor) +- [Why don't you embed Tor in SimpleX Chat app?](#why-dont-you-embed-tor-in-simplex-chat-app) +- [Can I host my own relays?](#can-i-host-my-own-relays) + +[Funding and business model](#funding-and-business-model) +- [How are you funded?](#how-are-you-funded) +- [Why VCs?](#why-vcs) +- [What will be the business model?](#what-will-be-the-business-model) + +## How to use it + +### I have nobody to chat with! Where can I find any groups? + +Please check our [Groups Directory](./DIRECTORY.md) in the first place. You might find some interesting groups and meet even more interesting people. + +### What is database? What can I do with it? + +Database is essential for SimpleX Chat to function properly. In comparison to centralized messaging providers, it is _the user_ who is responsible for taking care of their data. On the other hand, user is sure that _nobody but them_ has access to it. Please read more about it: [Database](./guide/managing-data.md). + +### Can I send files over SimpleX? + +Of course! While doing so, you are using a _state-of-the-art_ protocol that greatly reduces metadata leaks. Please read more about it: [XFTP Protocol](../blog/20230301-simplex-file-transfer-protocol.md). + +### What’s incognito profile? + +This feature is unique to SimpleX Chat – it is independent from chat profiles. + +When "Incognito Mode” is turned on, your currently chosen profile name and image are hidden from your new contacts. It allows anonymous connections with other people without any shared data – when you make new connections or join groups via a link a new random profile name will be generated for each connection. + +### How do invitations work? + +It is quite a complex process, but fortunately all of this happens in the background, so it's simply to use. + +Whenever somebody connects to you via your address, they basically ask your client whether they want to establish connection. After that, you can either agree or disagree. +If interested, please read more: [Addresses and invitations](./guide/making-connections.md). + +### How to configure and delete groups? + +Please check: [Users guide](./guide/secret-groups.md). + +### Are there any reactions to messages? Can I answer specific messages directly? + +Yes! Currently, there are six emojis available. What's more, you can respond to specific message by holding it and selecting _Reply_. + +### What do checkmarks mean? + +It's quite simple: +- one checkmark - message is delivered to the relay (the server). +- two checkmarks - message is delivered to the recipient's device. +"sent" means accepted by the relay for delivery, "delivered" - stored on the recipient device. + +Also see: [I do not see the second tick on the messages I sent](#i-do-not-see-the-second-tick-on-the-messages-i-sent) + +### 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. + +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. +- what practical problems an automatic notification sent to your contacts when you read the message solves for you compared with you simply adding a reaction to a message or sending a quick reply? + +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. + +### Can I use the same profile on desktop? Do messages sync cross-platform? + +You can use your profile from mobile device on desktop. However, to do so you need to be on the same network, both on your mobile and desktop. More about it: [Release info](../blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md#link-mobile-and-desktop-apps-via-secure-quantum-resistant-protocol). + +### Why cannot I delete messages I sent from my contact's device? + +In SimpleX Chat, you and your contacts can delete the messages you send from recipients' devices if you both agree to that within 24 hours of sending it. To be able to do that you both have to enable "Delete for everyone" option in Contact preferences - tap on the contact's name above the conversation to get there. + +You can also revoke the files you send. If the recipients did not yet receive the file, they will not be able to receive it after the file is revoked. + +This is different from most other messengers that allow deleting messages from the recipients' devices without any agreement with the recipients. + +We believe that allowing deleting information from your device to your contacts is a very wrong design decision for several reasons: +1) it violates your data sovereignty as the device owner - once your are in possession of any information, you have the rights to retain it, and any deletion should be agreed with you. And security and privacy is not possible if users don't have sovereignty over their devices. +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. + +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. + +When "Delete for everyone" is not enabled, you can still mark the sent message as deleted within 24 hours of sending it. In this case the recipient will see it as "deleted message", and will be able to reveal the original message. + +### Why invitation links use simplex.chat domain? + +You can replace `https://simplex.chat/` with `simplex:/` or with any other domain - the app never connect with it, ignoring it completely. It is only used to make it easier to connect for the new users who did not install the app yet. + +The invitation links will soon move to servers' domains. The servers already can host the pages that will be used to show QR codes. + +The link itself and the key exchange are not hosted anywhere, and the server that hosts the page to show QR code does not observe the actual connection link, because it is in the hash part of the link. The part after hash character (`#`) is not sent over the internet - the server can only see `https://simplex.chat/contact/` and the rest is processed on user's device in the browser, if you open it as a page. + +## Troubleshooting + +### I do not receive messages or message notifications + +There may be several reasons messages are not delivered to you from your contact: + +**You or your contact cannot connect to the server that you use to receive messages from your contact.** + +You can check which server is used to receive messages by tapping the contact name above the conversation. + +You can also run tests for this server from the app Network settings. + +Please ask your contact if they have a single tick on the message to determine if the message failed to send or if you fail to receive it. + +**Message delivery got stuck because of some unresolved bug.** + +Fully restarting the app is the workaround to resume message delivery. + +To do it on iOS, simply close the app (swipe up from the opened apps) and open it again. + +To do it on Android - choose Restart from the app settings, simply closing and re-opening the app will not restart the messaging service. + +**Your Android operating system kills the app while it is in background.** + +Check battery settings for the app - it should be set to Unrestricted. + +For some devices, there may be additional options to prevent the app from being killed - e.g., on Xiaomi you need to enable Auto Start setting for the app. Please consult https://dontkillmyapp.com site for any additional settings for your device. + +**Why my notifications aren't working on iOS** + +Check the color of the bolt icon next to Notifications in app settings - it should be green. + +If it's not, please open notifications, disable them (choose Off / Local), and then enable again - you should do it when you have Internet connection. + +Check if your push server has been restarted at time of the issue (Notifications -> Push server) at https://status.simplex.chat if it has been restarted, you may not receive notifications from that time. + +If device was offline, you may need to open the app to start receiving notifications. + +If the above didn't help, the reason could be that iOS failed to issue notification token - we have seen this issue several times. In this case, restarting the whole device should help. + +In some cases notifications may still not work, iOS notifications are hard to do right in a decentralized app, we will be improving them soon to be more reliable. + +**Messaging server or notification server is under maintenance** + +Please check the current status of preset servers at [https://status.simplex.chat](https://status.simplex.chat). You can also connect to status bot via QR code on that page - it will send the updates when the server is offline for maintenance, and also when the new versions of the app are released. + +### I do not see the second tick on the messages I sent + +You may not have the second tick on your sent messages for these reasons: + +- your contact is not online, and did not receive your message. +- possibly, message delivery to your contact or to you is disrupted - see [I do not receive messages](#i-do-not-receive-messages-or-message-notifications) - please check with your contact via some other channel if they received your message. If the message was delivered, then it means your device could fail to receive the delivery notification. +- possibly, your contact disabled sending delivery receipts - it can be disabled for specific or for all contacts - please check with your contact. + +### 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 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. + +### I cannot play a voice message + +This can happen for similar reasons as for [images](#i-see-image-preview-but-cannot-open-the-image). + +Please check your network settings and make sure you use the latest version of the app. + +Please report such issues if you use v5.7 or newer. + +### Audio or video calls do not connect + +App uses WebRTC for calls. Please check that you can connect to the servers configured in the app: stun.simplex.im and turn.simplex.im - see [troubleshooting WebRTC](./WEBRTC.md#troubleshoot). + +If you can connect to the server, please report this issue to us privately, including the following information: + +- how you connect to the network: WiFi, mobile network, VPN provider - the more information you can provide the better. + +- app version and platform. For mobile apps, it would help if you can make a screen recording from both devices during unsuccessful calls and share with us. + +- if the issue is on desktop app, which browser is used for calls. In this case also please check browser console during the call and send us the log, ideally from both sides of the unsuccessful calls. + +Thank you for helping us debug and improve calls. + +### Audio or video calls without e2e encryption + +During the call, the app indicates whether or not the call has end-to-end encryption. + +If one of the call parties uses Android (or desktop) app, the call would use Android system webview (or browser). Some older systems do not support media stream encryption, in which case the call will connect without it. + +To determine whether it is the limitation of your, your contact's or both devices: +- if some of your calls have e2e encryption but some don't, then it's certainly the old webview version or browser of your contacts - please ask them to upgrade. +- if you are not sure, you can check at what point "no e2e encryption" appears: + - if it is shown when the call rings on your device, then your contact's device does not support call encryption. + - if it is shown on your screen as soon as you start the call, then your device does not support call encryption. + - if in the beginning of the call your device shows "e2e encryption" but when your contact accepts the call it changes to "no e2e encryption", then it is only your contact's device that does not support it. + +You need to upgrade webview (some Android systems allow it), Android system or the device to have support for e2e encryption in the calls - all modern WebViews (and browsers) support it. + +### I clicked the link to connect, but could not connect + +If you confirmed the connection in the app, pending connection will be shown in the list of chats - you can assign the name to it, so you know who it was when your contact is connected (e.g., if they choose some name you don't recognize). + +For connection to complete, your contact has to be online and have the app running - please ask them to open the app, and try to have the app open at the same time - it will help to complete the connection faster. + +Once the connection is established you don't need to be online at the same time to send messages. + +### I do not know my database passphrase + +If you are prompted to enter database passphrase and you do not know it, this could have happened due to: +- You may have forgotten the passphrase. (There is no other way to access your data). +- Migration of app data from one device to another while using unsupported migration process, e.g. via iCloud backup. Use SimpleX Chat's own migration process in the app Settings. + +In the previous desktop app versions it could also happen in case of error during SimpleX Chat installation. + +You can resolve it by deleting the app's database: (WARNING: this results in deletion of all profiles, contacts and messages) +- on Android/iOS, uninstall the app and install it again. +- on Windows, delete folder `C:\AppData\Roaming\SimpleX`, you can find it by pressing Windows key + R and entering `%appdata%`. +- on Linux/Mac, delete directories `~/.local/share/simplex` and `~/.config/simplex`, where `~` represents your home directory (/home/user) +- on Flatpak, delete directory `~/.var/app/chat.simplex.simplex`. + +## Privacy and security + +### 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). + +### What user data can be provided on request? + +Our objective is to consistently ensure that no user data and absolute minimum of the metadata required for the network to function is available for disclosure by any infrastructure operators, under any circumstances. + +Please see our [Privacy Policy](../PRIVACY.md) and [Transparency Reports](./TRANSPARENCY.md). + +### Does SimpleX protect my IP address? + +Yes! + +SimpleX Chat from version 6.0 uses *private message routing* whenever you send messages to unknown servers (all servers in app network settings, both enabled and not, are considered "known"). + +For private routing to work, the servers chosen by your contacts (and by the group members in your groups) must be upgraded to the recent versions. Messaging servers include support for private routing from v5.8, but we recommend using the latest versions. + +If the servers didn't upgrade, the messages would temporarily fail to deliver. You will see an orange warning icon on the message, and you can decide if you want to deliver them by connecting to these servers directly (it would require changing network settings). At the time of writing (August 2024), all preset servers and absolute majority of self-hosted servers we can see on the network support private message routing. + +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. + +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. + +You can optionally enable private message routing for all servers in Advanced network settings to complicate traffic correlation for known servers too. This will be default once the clients are improved to "know about" and to take into account network server operators. + +See [this post](../blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md#private-message-routing) for more details about how private message routing works. + +### Doesn't private message routing reinvent Tor? + +No, it does not! + +It provides better privacy for messaging than Tor, and it can be used with and without Tor or other means to additionally protect your traffic from known servers as well. + +Tor, VPN and other transport overlay networks route sockets, by creating long-lived TCP circuits between you and the destination server. While it protects your IP address, it does not protect your activity within this circuit. E.g., if you visit a website via Tor, it can still observe all pages you view within a session. Likewise, if you were connecting directly to a messaging server via Tor, this server would be able to list all message queues you send messages to. + +Private message routing routes packets (each message is one 16kb packet), not sockets. Unlike Tor and VPN, it does not create circuits between your client and destination servers. The forwarding server creates one shared session between itself and the destination, and forwards all messages from you and other clients to that destination server, mixing messages from many clients into a single TCP session. + +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. + +### Why don't you embed Tor in SimpleX Chat app? + +[Tor](https://www.torproject.org) is a fantastic transport overlay network - we believe it might be the best there is right now. If its [threat model](https://support.torproject.org/about/attacks-on-onion-routing/) works for you, you absolutely should use it - SimpleX Chat app supports Tor via SOCKS proxy [since v3.1](https://simplex.chat/blog/20220808-simplex-chat-v3.1-chat-groups.html#access-messaging-servers-via-tor), and SimpleX network servers can be available on both public and onion address at the same time [since v3.2](https://simplex.chat/blog/20220901-simplex-chat-v3.2-incognito-mode.html#using-onion-server-addresses-with-tor), improving anonymity of the users who use Tor. + +If you host your messaging server on the onion address only, the users who don't use Tor would still be able to message you via private message routing - all preset servers are configured to forward messages to onion-only servers. + +But there are many reasons not to embed Tor in the app: +- it increases response latency, error rate, and battery usage, and we believe that for most users enabling Tor by default would be a bad trade-off. +- it would require us regularly updating Tor library in the app, and your Tor integrity would depend on us – you would be "putting too many eggs in one basket". +- some networks restrict Tor traffic, so the app UI would have to support advanced Tor configuration, diverting our limited resources from the core app features that benefit all users. +- some countries have legislative restrictions on Tor usage, so we would have to support multiple app versions, also increasing our costs and slowing down the progress. + +The last, but not the least, it would create an unfair competitive advantage to Tor. We believe in competition, and we want our users to be able to choose which transport overlay network to use, based on what network threat model works best for them. + +If you want to use Tor or any other overlay network, such as i2p, [Nym network](https://nymtech.net), [Katzenpost](https://katzenpost.network), etc., you need to research their limitations, because none of them provides absolute anonymity against all possible attackers. + +And if after that research you decide to use Tor, it takes about 2 minutes to install and start [Orbot app](https://guardianproject.info/apps/org.torproject.android/). We believe that if it seems complex, then you *should not* be using Tor - it is an advanced technology that can only improve your privacy and anonymity if you understand its limitations and know how to configure it. + +### Can I host my own relays? + +Of course! Please check these tutorials: [SMP server](./SERVER.md) and [XFTP server](./XFTP-SERVER.md). + +## Funding and business model + +### How are you funded? + +SimpleX Chat Ltd is funded by private investors and venture capital. As an open-source project, it is also being generously supported by donations as well. Read the posts [from 2023](../blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md#how-is-it-funded-and-what-is-the-business-model) and [from 2024](../blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md) for more details. + +### Why VCs? + +Here are some reflections on VC funding being a necessity for a project at this scale, as well as sustainability and profitability for longtime operations: https://www.poberezkin.com/posts/2023-10-31-why-privacy-impossible-without-venture-funding.html + +And another perspective from a team member on the delicate balance of venture-backed and nonprofit structures, and the plans for the SimpleX network protocols to evolve under the stewardship of nonprofit entities in various jurisdictions, so that its continued evolution aligns more closely with the vision of community-driven, independent and transparent governance: +[https://simplex.chat/blog/20240404-why-i-joined-simplex-chat-esraa-al-shafei.html](../blog/20240404-why-i-joined-simplex-chat-esraa-al-shafei.md). + +### What will be the business model? + +We are focusing on product-market fit, and as such the business model is still a work in progress. However, the app will have a freemium model with extra features or capabilities for paid users (taking into consideration a potential formula like 5% paying $5/month is $3/user/year - ~90% gross profit margin). + +The other income stream would be via business services, for entities needing direct and customized support to integrate with the SimpleX protocol or related resources. There will also be a revenue-sharing model from customers to network operators, to provide an incentive for them to continue running nodes, which will increase decentralization and reliability of the network. + +Non-exploitative commercial models with fully open source code are not easy to achieve, and we’re committed to finding the best possible fit for our context. Everything will be fully communicated as this plan progresses. diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index e68508ccc3..0cb855d729 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -143,6 +143,12 @@ 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 + +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. + +[Wikipedia](https://en.wikipedia.org/wiki/Non-repudiation) + ## Pairwise pseudonymous identifier 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. @@ -185,6 +191,12 @@ Network topology of the communication system when peers communicate via proxies [Post-compromise security](#post-compromise-security). +## Repudiation + +The property of the cryptographic or communication system that allows the sender of the message to plausibly deny having sent the message, because while the recipient can verify that the message was sent by the sender, they cannot prove it to any third party - the recipient has a technical ability to forge the same encrypted message. This is an important quality of private communications, as it allows to have the conversation that can later be denied, similarly to having a private face-to-face conversation. + +See also [non-repudiation](#non-repudiation). + ## User identity In a communication system it refers to anything that uniquely identifies the users to the network. Depending on the communication network, it can be a phone number, email address, username, public key or a random opaque identifier. Most messaging networks rely on some form of user identity. SimpleX appears to be the only messaging network that does not rely on any kind of user identity - see [this comparison](https://en.wikipedia.org/wiki/Comparison_of_instant_messaging_protocols). diff --git a/docs/JOIN_TEAM.md b/docs/JOIN_TEAM.md index cf33df1ee7..c72a75cfec 100644 --- a/docs/JOIN_TEAM.md +++ b/docs/JOIN_TEAM.md @@ -8,36 +8,36 @@ layout: layouts/jobs.html SimpleX Chat Ltd is a seed stage startup with a lot of user growth in 2022-2023, and a lot of exciting technical and product problems to solve to grow faster. -We currently have 4 full-time people in the team - all engineers, including the founder. - -We want to add up to 3 people to the team. +We currently have 6 full-time people in the team. +We want to add 2 people to the team. ## Who we are looking for +### Web designer & developer for a website contract + +You will work with the founder and a product marketing expert to convert the stories we want to tell our current and prospective users into interactive experiences. + +You are an expert in creating interactive web experiences: +- 15+ years of web development and design experience. +- Passionate about communications, privacy and data ownership. +- Competent using PhotoShop, 3D modelling, etc. +- Competent in Web tech, including JavaScript, animations, etc. + +We will NOT consider agencies or groups – it must be one person working on the project. + ### Application Haskell engineer +You will work with the Haskell core of the client applications and with the network servers. + You are an expert in language models, databases and Haskell: - expert knowledge of SQL. -- exception handling, concurrency, STM. -- type systems - we use ad hoc dependent types a lot. -- experience integrating open-source language models. -- experience developing community-centric applications. -- interested to build the next generation of messaging network. - -You will be focussed mostly on our client applications, and will also contribute to the servers also written in Haskell. - -### iOS / Mac engineer - -You are an expert in Apple platforms, including: -- iOS and Mac platform architecture. -- Swift and Objective-C. -- SwiftUI and UIKit. -- extensions, including notification service extension and sharing extension. -- low level inter-process communication primitives for concurrency. -- interested about creating the next generation of UX for a communication/social network. - -Knowledge of Android and Kotlin Multiplatform would be a bonus - we use Kotlin Jetpack Compose for our Android and desktop apps. +- Haskell strictness, exceptions, [concurrency](https://simonmar.github.io/pages/pcph.html), STM, [type systems](https://thinkingwithtypes.com). +- 15y+ of software engineering experience in complex projects. +- deep understanding of the common programming principles: + - data structures, bits and bytes, text encoding. + - [functional software design](https://mitp-content-server.mit.edu/books/content/sectbyfn/books_pres_0/6515/sicp.zip/index.html) and algorithms. + - protocols and networking. ## About you @@ -45,6 +45,7 @@ Knowledge of Android and Kotlin Multiplatform would be a bonus - we use Kotlin J - already use SimpleX Chat to communicate with friends/family or participate in public SimpleX Chat groups. - passionate about privacy, security and communications. - interested to make contributions to SimpleX Chat open-source project in your free time before we hire you, as an extended test. + - you founded (and probably failed) at least one startup, or spent more time working for yourself than being employed. - **Exceptionally pragmatic, very fast and customer-focussed**: - care about the customers (aka users) and about the product we build much more than about the code quality, technology stack, etc. @@ -53,25 +54,17 @@ Knowledge of Android and Kotlin Multiplatform would be a bonus - we use Kotlin J - focus on solving only today's problems and resist engineering for the future (aka over-engineering) – see [The Duct Tape Programmer](https://www.joelonsoftware.com/2009/09/23/the-duct-tape-programmer/) and [Why I Hate Frameworks](https://medium.com/@johnfliu/why-i-hate-frameworks-6af8cbadba42). - do not suffer from "not invented here" syndrome, at the same time interested to design and implement protocols and systems from the ground up when appropriate. -- **Love software engineering**: - - have 5y+ of software engineering experience in complex projects, - - great understanding of the common principles: - - data structures, bits and byte manipulation - - text encoding and manipulation - - software design and algorithms - - concurrency - - networking - - **Want to join a very early stage startup**: - high pace and intensity, longer hours. - a substantial part of the compensation is stock options. - - full transparency – we believe that too much [autonomy](https://twitter.com/KentBeck/status/851459129830850561) hurts learning and slows down progress. - + - full transparency - we believe that too much [autonomy](https://twitter.com/KentBeck/status/851459129830850561) hurts learning and slows down progress. ## How to join the team -1. [Install the app](../README.md#install-the-app), try using it with the friends and [join some user groups](https://github.com/simplex-chat/simplex-chat#join-user-groups) – you will discover a lot of things that need improvements. +1. [Install the app](https://github.com/simplex-chat/simplex-chat#install-the-app), try using it with the friends and [join some user groups](https://github.com/simplex-chat/simplex-chat#join-user-groups) – you will discover a lot of things that need improvements. 2. Also look through [GitHub issues](https://github.com/simplex-chat/simplex-chat/issues) submitted by the users to see what would you want to contribute as a test. 3. [Connect to us](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FKBCmxJ3-lEjpWLPPkI6OWPk-YJneU5uY%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEAtixHJWDXvYWcoe-77vIfjvI6XWEuzUsapMS9nVHP_Go%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) via SimpleX Chat to chat about what you want to contribute and about joining the team. + +4. You can also email [jobs@simplex.chat](mailto:jobs@simplex.chat?subject=Join%20SimpleX%20Chat%20team) diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000000..72db650c35 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,81 @@ +--- +title: Security Policy +permalink: /security/index.html +revision: 23.04.2024 +--- + +# Security Policy + +While great care is taken to ensure the highest level of security and privacy in SimpleX network servers and clients, all software can have flaws, and we believe it is a critical part of an organization's social responsibility to minimize the impact of these flaws through continual vulnerability discovery efforts, defense in depth design, and prompt remediation and notification. + +The implementation security assessment of SimpleX cryptography and networking was done by Trail of Bits in [November 2022](../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). + +The cryptographic review of SimpleX protocols design was done by Trail of Bits in [July 2024](../blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md). + +We are planning implementation security assessment in early 2025. + +## Reporting security issues + +To report a security issue, please contact us directly via email [chat@simplex.chat](mailto:chat@simplex.chat). Please do NOT report security issues via GitHub issues or via any public channels. + +Please encrypt the email message using the key for this address from [keys.openpgp.org](https://keys.openpgp.org/search?q=chat%40simplex.chat) (its fingerprint is `FB44 AF81 A45B DE32 7319 797C 8510 7E35 7D4A 17FC`) and make your key available for a secure reply. + +While we encourage you to encrypt the message, if this poses a barrier to reporting, an unencrypted submission is better than no submission. + +## Issue triage + +Our team will investigate and prioritize the reported issue. We may work in private with individuals who are not our direct team members, as well as other organizations, where we believe this can help with the issue investigation, resolution, or testing. + +## Threat Model + +Please review threat model for SimpleX: https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#threat-model + +Certain threats are considered out of the scope of SimpleX security model. For example, we do not consider SimpleX secure against the following classes of attacks: + +- CPU/hardware flaws. +- physical observation side channels (e.g. power consumption, EM emissions, etc). + +Further, any user data stored on the device can be accessed with user's or root privileges, such as: +- user files in the app storage (encrypted or not, depending on the app settings). +- encrypted database. +- database encryption key in case it is stored on the device. + +Mitigations for security issues outside of our threat model may still be implemented, however they will be weighed against competing priorities, and we do not classify them as SimpleX vulnerabilities. + +## Issue severity + +We will determine the risk of each issue, taking into account our experience dealing with past issues, versions affected, common defaults, and use cases. We classify issues on two dimensions, based on their severity level and the difficulty required to exploit them. + +**Difficulty levels** + +- **Low**: The flaw is well known; public tools for its exploitation exist or can be scripted. +- **Medium**: An attacker must write an exploit or will need in-depth knowledge of the system. +- **High**: An attacker must have privileged access to the system, may need to know complex technical details, or must discover other weaknesses to exploit this issue. + +**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. +- **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. + +## Notification policy + +Security fixes of critical, high and medium severity MUST NOT be mentioned in the commit message. Security fixes of low severity MAY be mentioned in the commit messages. + +We will privately notify trusted partners about forthcoming security fixes on the day when the fix is publicly released and available for download via all supported channels, indicating the issue level, but not further details. + +7 days after the new software version is released and available for download via all supported channels, we will indicate that it fixes a security issue and its level, but not further details. This notification will be published in our release notes and broadcast channels we use. + +14 days later the details will be published in the release notes, describing the impact and the nature of vulnerability, but not necessarily providing detailed instruction for the exploit - it will be decided on a case by case basis. + +## Trusted partners + +You may be privately notified about the forthcoming releases containing fixes to the security issues of critical, high and medium severity. We will communicate only the level of the issue severity, and not the issue itself. + +To be included in this list, you should be one of the following: +- software or hardware vendor depending on our code. +- commercial or non-profit organization that uses our software in scenarios where security and privacy is critically important. +- we may also include other organizations that are not listed but would otherwise qualify for list membership. +- we may also include organizations with which we have a commercial relationship. +- we may withdraw notifying certain organizations if they leak issues before they are public or do not add value. diff --git a/docs/SERVER.md b/docs/SERVER.md index 00e3e0f6ee..f45403be8a 100644 --- a/docs/SERVER.md +++ b/docs/SERVER.md @@ -1,69 +1,327 @@ --- title: Hosting your own SMP Server -revision: 31.07.2023 +revision: 12.10.2024 --- -| Updated 05.06.2023 | Languages: EN, [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) | - # Hosting your own SMP Server +| Updated 12.10.2024 | Languages: EN, [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md), [PL](/docs/lang/pl/SERVER.md) | + +## Table of Contents + +- [Overview](#overview) +- [Quick start](#quick-start) with systemd service +- [Installation options](#installation-options) + - [systemd service](#systemd-service) with [installation script](#installation-script) or [manually](#manual-deployment) + - [docker container](#docker-container) + - [Linode marketplace](#linode-marketplace) +- [Verifying server binaries](#verifying-server-binaries) +- [Configuration](#configuration) + - [Interactively](#interactively) + - [Via command line options](#via-command-line-options) +- [Further configuration](#further-configuration) +- [Server security](#server-security) + - [Initialization](#initialization) + - [Private keys](#private-keys) + - [Online certificate rotation](#online-certificate-rotation) +- [Tor: installation and configuration](#tor-installation-and-configuration) + - [Installation for onion address](#installation-for-onion-address) + - [SOCKS port for SMP PROXY](#socks-port-for-smp-proxy) +- [Server information page](#server-information-page) +- [Documentation](#documentation) + - [SMP server address](#smp-server-address) + - [Systemd commands](#systemd-commands) + - [Control port](#control-port) + - [Daily statistics](#daily-statistics) +- [Reproduce builds](#reproduce-builds) +- [Updating your SMP server](#updating-your-smp-server) +- [Configuring the app to use the server](#configuring-the-app-to-use-the-server) + ## Overview SMP server is the relay server used to pass messages in SimpleX network. SimpleX Chat apps have preset servers (for mobile apps these are smp11, smp12 and smp14.simplex.im), but you can easily change app configuration to use other servers. SimpleX clients only determine which server is used to receive the messages, separately for each contact (or group connection with a group member), and these servers are only temporary, as the delivery address can change. -_Please note_: when you change the servers in the app configuration, it only affects which server will be used for the new contacts, the existing contacts will not automatically move to the new servers, but you can move them manually using ["Change receiving address"](../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md#change-your-delivery-address-beta) button in contact/member information pages – it will be automated soon. +To create SMP server, you'll need: -## Installation +1. VPS or any other server. +2. Your own domain, pointed at the server (`smp.example.com`) +3. A basic Linux knowledge. -0. First, install `smp-server`: +_Please note_: when you change the servers in the app configuration, it only affects which servers will be used for the new contacts, the existing contacts will not automatically move to the new servers, but you can move them manually using ["Change receiving address"](../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md#change-your-delivery-address-beta) button in contact/member information pages – it will be automated in the future. - - Manual deployment (see below) +## Quick start - - Semi-automatic deployment: - - [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script) - - [Docker container](https://github.com/simplex-chat/simplexmq#using-docker) - - [Linode StackScript](https://github.com/simplex-chat/simplexmq#deploy-smp-server-on-linode) +To create SMP server as a systemd service, you'll need: -Manual installation requires some preliminary actions: +- VPS or any other server. +- Your server domain, with A and AAAA records specifying server IPv4 and IPv6 addresses (`smp1.example.com`) +- A basic Linux knowledge. -0. Install binary: +*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. - - Using offical binaries: +1. Install server with [Installation script](https://github.com/simplex-chat/simplexmq#using-installation-script). + +2. Adjust firewall: + + ```sh + ufw allow 80/tcp &&\ + ufw allow 443/tcp &&\ + ufw allow 5223/tcp + ``` + +3. Init server: + + Replace `smp1.example.com` with your actual server domain. + + ```sh + su smp -c 'smp-server init --yes \ + --store-log \ + --no-password \ + --control-port \ + --socks-proxy \ + --source-code \ + --fqdn=smp1.example.com + ``` + +4. Install tor: + + ```sh + CODENAME="$(lsb_release -c | awk '{print $2}')" + + echo "deb [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main + deb-src [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main" > /etc/apt/sources.list.d/tor.list &&\ + curl --proto '=https' --tlsv1.2 -sSf https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | tee /usr/share/keyrings/tor-archive-keyring.gpg >/dev/null &&\ + apt update && apt install -y tor deb.torproject.org-keyring + ``` + +5. Configure tor: + + ```sh + tor-instance-create tor2 &&\ + mkdir /var/lib/tor/simplex-smp/ &&\ + chown debian-tor:debian-tor /var/lib/tor/simplex-smp/ &&\ + chmod 700 /var/lib/tor/simplex-smp/ + ``` + + ```sh + vim /etc/tor/torrc + ``` + + Paste the following: + + ```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. + SOCKSPort 0 + HiddenServiceNonAnonymousMode 1 + HiddenServiceSingleHopMode 1 + # smp-server hidden service host directory and port mappings + HiddenServiceDir /var/lib/tor/simplex-smp/ + HiddenServicePort 5223 localhost:5223 + HiddenServicePort 443 localhost:443 + ``` + + ```sh + vim /etc/tor/instances/tor2/torrc + ``` + + Paste the following: + + ```sh + # Log tor to systemd daemon + Log notice syslog + # Listen to local 9050 port for socks proxy + SocksPort 9050 + ``` + +6. Start tor: + + ```sh + systemctl enable tor &&\ + systemctl start tor &&\ + systemctl restart tor &&\ + systemctl enable --now tor@tor2 + ``` + +7. Install Caddy: + + ```sh + sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl &&\ + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg &&\ + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list &&\ + sudo apt update && sudo apt install caddy + ``` + +8. Configure Caddy: + + ```sh + vim /etc/caddy/Caddyfile + ``` + + Replace `smp1.example.com` with your actual server domain. Paste the following: + + ``` + http://smp1.example.com { + redir https://smp1.example.com{uri} permanent + } + + smp1.example.com:8443 { + tls { + key_type rsa4096 + } + } + ``` + + ```sh + vim /usr/local/bin/simplex-servers-certs + ``` + + Replace `smp1.example.com` with your actual server domain. Paste the following: + + ```sh + #!/usr/bin/env sh + set -eu + + user='smp' + group="$user" + + domain='smp1.example.com' + folder_in="/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/${domain}" + folder_out='/etc/opt/simplex' + key_name='web.key' + cert_name='web.crt' + + # Copy certifiacte 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 + cp "${folder_in}/${domain}.key" "${folder_out}/${key_name}" + # Assign correct permissions + chown "$user":"$group" "${folder_out}/${key_name}" + ``` + + ```sh + chmod +x /usr/local/bin/simplex-servers-certs + ``` + + ```sh + sudo crontab -e + ``` + + Paste the following: + + ```sh + # Every week on 00:20 sunday + 20 0 * * 0 /usr/local/bin/simplex-servers-certs + ``` + +9. Enable and start Caddy service: + + Wait until "good to go" has been printed. + + ```sh + systemctl enable --now caddy &&\ + sleep 10 &&\ + /usr/local/bin/simplex-servers-certs &&\ + echo 'good to go' + ``` + +10. Enable and start smp-server: + + ```sh + systemctl enable --now smp-server.service + ``` + +11. Print your address: + + ```sh + smp="$(journalctl --output cat -q _SYSTEMD_INVOCATION_ID="$(systemctl show -p InvocationID --value smp-server)" | grep -m1 'Server address:' | awk '{print $NF}' | sed 's/:443.*//')" + tor="$(cat /var/lib/tor/simplex-smp/hostname)" + + echo "$smp,$tor" + ``` + +## Installation options + +You can install SMP server in one of the following ways: + +- [systemd service](#systemd-service) + - using [installation script](#installation-script) - **recommended** + - or [manually](#manual-deployment) +- [Docker container](#docker-container) from DockerHub +- [Linode marketplace](#linode-marketplace) + +### systemd service + +#### Installation script + +This installation script will automatically install binaries, systemd services and additional scripts that will manage backups, updates and uninstallation. This is the recommended option due to its flexibility, easy updating, and being battle tested on our servers. + +**Please note** that currently only Ubuntu distribution is supported. + +Run the following script on the server: + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/install.sh -o simplex-server-install.sh &&\ +if echo '53fcdb4ceab324316e2c4cda7e84dbbb344f32550a65975a7895425e5a1be757 simplex-server-install.sh' | sha256sum -c; then + chmod +x ./simplex-server-install.sh + ./simplex-server-install.sh + rm ./simplex-server-install.sh +else + echo "SHA-256 checksum is incorrect!" + rm ./simplex-server-install.sh +fi +``` + +Type `1` and hit enter to install `smp-server`. + +#### Manual deployment + +Manual installation is the most advanced deployment that provides the most flexibility. Generally recommended only for advanced users. + +1. Install binary: + + - Using pre-compiled binaries: ```sh - curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/smp-server + curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/smp-server && chmod +x /usr/local/bin/smp-server ``` - Compiling from source: Please refer to [Build from source: Using your distribution](https://github.com/simplex-chat/simplexmq#using-your-distribution) -1. Create user and group for `smp-server`: +2. Create user and group for `smp-server`: ```sh sudo useradd -m smp ``` -2. Create necessary directories and assign permissions: +3. Create necessary directories and assign permissions: ```sh sudo mkdir -p /var/opt/simplex /etc/opt/simplex sudo chown smp:smp /var/opt/simplex /etc/opt/simplex ``` -3. Allow `smp-server` port in firewall: +4. Allow `smp-server` port in firewall: ```sh # For Ubuntu sudo ufw allow 5223/tcp + sudo ufw allow 443/tcp + sudo ufw allow 80/tcp # For Fedora - sudo firewall-cmd --permanent --add-port=5223/tcp && \ + sudo firewall-cmd --permanent --add-port=5223/tcp --add-port=443/tcp --add-port=80/tcp && \ sudo firewall-cmd --reload ``` -4. **Optional** — If you're using distribution with `systemd`, create `/etc/systemd/system/smp-server.service` file with the following content: +5. **Optional** — If you're using distribution with `systemd`, create `/etc/systemd/system/smp-server.service` file with the following content: ```sh [Unit] @@ -78,6 +336,7 @@ Manual installation requires some preliminary actions: LimitNOFILE=65535 KillSignal=SIGINT TimeoutStopSec=infinity + AmbientCapabilities=CAP_NET_BIND_SERVICE [Install] WantedBy=multi-user.target @@ -85,85 +344,200 @@ Manual installation requires some preliminary actions: And execute `sudo systemctl daemon-reload`. -## Tor installation +### Docker container -smp-server can also be deployed to serve from [tor](https://www.torproject.org) network. Run the following commands as `root` user. +You can deploy smp-server using Docker Compose. This is second recommended option due to its popularity and relatively easy deployment. -1. Install tor: +This deployment provides two Docker Compose files: the **automatic** one and **manual**. If you're not sure, choose **automatic**. - 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. +This will download images from [Docker Hub](https://hub.docker.com/r/simplexchat). - - Configure offical Tor PPA repository: +#### Docker: Automatic setup - ```sh - CODENAME="$(lsb_release -c | awk '{print $2}')" - echo "deb [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main - deb-src [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main" > /etc/apt/sources.list.d/tor.list - ``` +This configuration provides quick and easy way to setup your SMP server: Caddy will automatically manage Let's Encrypt certificates and redirect HTTP to HTTPS, while smp-server will serve both [server information page](#server-information-page) and SMP Protocol by 443 port. 5223 port is used as fallback. - - Import repository key: +**Please note** that you _must_ have `80` and `443` ports unallocated by other servers. - ```sh - curl --proto '=https' --tlsv1.2 -sSf https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | tee /usr/share/keyrings/tor-archive-keyring.gpg >/dev/null - ``` +1. Create `smp-server` directory and switch to it: - - Update repository index: + ```sh + mkdir smp-server && cd smp-server + ``` - ```sh - apt update - ``` +2. Create `docker-compose.yml` file with the following content: - - Install `tor` package: + You can also grab it from here - [docker-compose-smp-complete.yml](https://raw.githubusercontent.com/simplex-chat/simplexmq/refs/heads/stable/scripts/docker/docker-compose-smp-complete.yml). Don't forget to rename it to `docker-compose.yml`. - ```sh - apt install -y tor deb.torproject.org-keyring - ``` + ```yaml + name: SimpleX Chat - smp-server -2. Configure tor: + services: + oneshot: + image: ubuntu:latest + environment: + CADDYCONF: | + ${CADDY_OPTS:-} - - File configuration: - - Open tor configuration with your editor of choice (`nano`,`vim`,`emacs`,etc.): + http://{$$ADDR} { + redir https://{$$ADDR}{uri} permanent + } - ```sh - vim /etc/tor/torrc - ``` + {$$ADDR}:8443 { + tls { + key_type rsa4096 + } + } + command: sh -c 'if [ ! -f /etc/caddy/Caddyfile ]; then printf "$${CADDYCONF}" > /etc/caddy/Caddyfile; fi' + volumes: + - ./caddy_conf:/etc/caddy - And insert the following lines to the bottom of configuration. Please note lines starting with `#`: this is comments about each individual options. + caddy: + image: caddy:latest + depends_on: + oneshot: + condition: service_completed_successfully + cap_add: + - NET_ADMIN + environment: + ADDR: ${ADDR?"Please specify the domain."} + volumes: + - ./caddy_conf:/etc/caddy + - caddy_data:/data + - caddy_config:/config + ports: + - 80:80 + restart: unless-stopped + healthcheck: + test: "test -d /data/caddy/certificates/${CERT_PATH:-acme-v02.api.letsencrypt.org-directory}/${ADDR} || exit 1" + interval: 1s + retries: 60 - ```sh - # Enable log (otherwise, tor doesn't seemd to deploy onion address) - Log notice file /var/log/tor/notices.log - # Enable single hop routing (2 options below are dependencies of third). Will reduce latency in exchange of anonimity (since tor runs alongside smp-server and onion address will be displayed in clients, this is totally fine) - SOCKSPort 0 - HiddenServiceNonAnonymousMode 1 - HiddenServiceSingleHopMode 1 - # smp-server hidden service host directory and port mappings - HiddenServiceDir /var/lib/tor/simplex-smp/ - HiddenServicePort 5223 localhost:5223 - ``` + smp-server: + image: ${SIMPLEX_IMAGE:-simplexchat/smp-server:latest} + depends_on: + caddy: + condition: service_healthy + environment: + ADDR: ${ADDR?"Please specify the domain."} + PASS: ${PASS:-} + volumes: + - ./smp_configs:/etc/opt/simplex + - ./smp_state:/var/opt/simplex + - type: volume + source: caddy_data + target: /certificates + volume: + subpath: "caddy/certificates/${CERT_PATH:-acme-v02.api.letsencrypt.org-directory}/${ADDR}" + ports: + - 443:443 + - 5223:5223 + restart: unless-stopped - - Create directories: + volumes: + caddy_data: + caddy_config: + ``` - ```sh - mkdir /var/lib/tor/simplex-smp/ && chown debian-tor:debian-tor /var/lib/tor/simplex-smp/ && chmod 700 /var/lib/tor/simplex-smp/ - ``` +3. In the same directory, create `.env` file with the following content: -3. Start tor: + You can also grab it from here - [docker-compose-smp-complete.env](https://raw.githubusercontent.com/simplex-chat/simplexmq/refs/heads/stable/scripts/docker/docker-compose-smp-complete.env). Don't forget to rename it to `.env`. - 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. + Change variables according to your preferences. - ```sh - systemctl enable tor && systemctl start tor && systemctl restart tor - ``` + ```env + # Mandatory + ADDR=your_ip_or_addr -4. Display onion host: + # Optional + #PASS='123123' + ``` - Execute the following command to display your onion host address: +4. Start your containers: - ```sh - cat /var/lib/tor/simplex-smp/hostname - ``` + ```sh + docker compose up + ``` + +#### Docker: Manual setup + +If you know what you are doing, this configuration provides bare SMP server setup without automatically managed Let's Encrypt certificates by Caddy to serve [server information page](#server-information-page) with 5223 port set as primary. + +This configuration allows you to retain the ability to manage 80 and 443 ports yourself. As a downside, SMP server **can not* be served to 443 port. + +1. Create `smp-server` directory and switch to it: + + ```sh + mkdir smp-server && cd smp-server + ``` + +2. Create `docker-compose.yml` file with the following content: + + You can also grab it from here - [docker-compose-smp-manual.yml](https://raw.githubusercontent.com/simplex-chat/simplexmq/refs/heads/stable/scripts/docker/docker-compose-smp-manual.yml). Don't forget to rename it to `docker-compose.yml`. + + ```yaml + name: SimpleX Chat - smp-server + + services: + smp-server: + image: ${SIMPLEX_IMAGE:-simplexchat/smp-server:latest} + environment: + WEB_MANUAL: ${WEB_MANUAL:-1} + ADDR: ${ADDR?"Please specify the domain."} + PASS: ${PASS:-} + volumes: + - ./smp_configs:/etc/opt/simplex + - ./smp_state:/var/opt/simplex + ports: + - 5223:5223 + restart: unless-stopped + ``` + +3. In the same directory, create `.env` file with the following content: + + You can also grab it from here - [docker-compose-smp-manual.env](https://raw.githubusercontent.com/simplex-chat/simplexmq/refs/heads/stable/scripts/docker/docker-compose-smp-manual.env). Don't forget to rename it to `.env`. + + Change variables according to your preferences. + + ```env + # Mandatory + ADDR=your_ip_or_addr + + # Optional + #PASS='123123' + WEB_MANUAL=1 + ``` + +4. Start your containers: + + ```sh + docker compose up + ``` + +### Linode marketplace + +You can deploy smp-server upon creating new Linode VM. Please refer to: [Linode Marketplace](https://www.linode.com/marketplace/apps/simplex-chat/simplex-chat/) + +## Verifying server binaries + +Starting from v6.3 server builds are [reproducible](#reproduce-builds). + +That also allows us to sign server releases, confirming the integrity of GitHub builds. + +To verify server binaries after you downloaded them: + +1. Download `_sha256sums` (hashes of all server binaries) and `_sha256sums.asc` (signature). + +2. Download our key FB44AF81A45BDE327319797C85107E357D4A17FC from [openpgp.org](https://keys.openpgp.org/search?q=chat%40simplex.chat) + +3. Import the key with `gpg --import FB44AF81A45BDE327319797C85107E357D4A17FC`. Key filename should be the same as its fingerprint, but please change it if necessary. + +4. Run `gpg --verify _sha256sums.asc _sha256sums`. It should print: + +> Good signature from "SimpleX Chat <chat@simplex.chat>" + +5. Compute the hashes of the binaries you plan to use with `shu256sum <file>` or with `openssl sha256 <file>` 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. ## Configuration @@ -205,11 +579,11 @@ There are several options to consider: Enter `y` to enable logging statistics in CSV format, e.g. they can be used to show aggregate usage charts in `Grafana`. -These statistics include daily counts of created, secured and deleted queues, sent and received messages, and also daily, weekly, and monthly counts of active queues (that is, the queues that were used for any messages). We believe that this information does not include anything that would allow correlating different queues as belonging to the same users, but please let us know, confidentially, if you believe that this can be exploited in any way. +These statistics include daily counts of created, secured and deleted queues, sent and received messages, and also daily, weekly, and monthly counts of active queues (that is, the queues that were used for any messages). We believe that this information does not include anything that would allow correlating different queues as belonging to the same users, but please [let us know](./SECURITY.md), confidentially, if you believe that this can be exploited in any way. - `Require a password to create new messaging queues?` - Enter `r` or your arbitrary password to password-protect `smp-server`, or `n` to disable password protection. + Press `Enter` or enter your arbitrary password to password-protect `smp-server`, or `n` to disable password protection. - `Enter server FQDN or IP address for certificate (127.0.0.1):` @@ -277,7 +651,571 @@ Fingerprint: d5fcsc7hhtPpexYUbI2XPxDbyU2d3WsVmROimcL90ss= Server address: smp://d5fcsc7hhtPpexYUbI2XPxDbyU2d3WsVmROimcL90ss=:V8ONoJ6ICwnrZnTC_QuSHfCEYq53uLaJKQ_oIC6-ve8=@<hostnames> ``` -The server address above should be used in your client configuration and if you added server password it should only be shared with the other people when you want to allow them to use your server to receive the messages (all your contacts will be able to send messages, as it does not require a password). If you passed IP address or hostnames during the initialisation, they will be printed as part of server address, otherwise replace `<hostnames>` with the actual server addresses. +The server address above should be used in your client configuration, and if you added server password it should only be shared with the other people who you want to allow using your server to receive the messages (all your contacts will be able to send messages - it does not require a password). If you passed IP address or hostnames during the initialisation, they will be printed as part of server address, otherwise replace `<hostnames>` with the actual server hostnames. + +## Further configuration + +All generated configuration, along with a description for each parameter, is available inside configuration file in `/etc/opt/simplex/smp-server.ini` for further customization. Depending on the smp-server version, the configuration file looks something like this: + +```ini +[INFORMATION] +# AGPLv3 license requires that you make any source code modifications +# available to the end users of the server. +# LICENSE: https://github.com/simplex-chat/simplexmq/blob/stable/LICENSE +# Include correct source code URI in case the server source code is modified in any way. +# If any other information fields are present, source code property also MUST be present. + +source_code: https://github.com/simplex-chat/simplexmq + +# Declaring all below information is optional, any of these fields can be omitted. + +# Server usage conditions and amendments. +# It is recommended to use standard conditions with any amendments in a separate document. +# usage_conditions: https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md +# condition_amendments: link + +# Server location and operator. +# server_country: ISO-3166 2-letter code +# operator: entity (organization or person name) +# operator_country: ISO-3166 2-letter code +# website: + +# Administrative contacts. +# admin_simplex: SimpleX address +# admin_email: +# admin_pgp: +# admin_pgp_fingerprint: + +# Contacts for complaints and feedback. +# complaints_simplex: SimpleX address +# complaints_email: +# complaints_pgp: +# complaints_pgp_fingerprint: + +# Hosting provider. +# hosting: entity (organization or person name) +# hosting_country: ISO-3166 2-letter code + +[STORE_LOG] +# The server uses STM memory for persistence, +# that will be lost on restart (e.g., as with redis). +# This option enables saving memory to append only log, +# and restoring it when the server is started. +# Log is compacted on start (deleted objects are removed). +enable: on + +# Undelivered messages are optionally saved and restored when the server restarts, +# they are preserved in the .bak file until the next restart. +restore_messages: on +expire_messages_days: 21 +expire_ntfs_hours: 24 + +# Log daily server statistics to CSV file +log_stats: on + +[AUTH] +# Set new_queues option to off to completely prohibit creating new messaging queues. +# This can be useful when you want to decommission the server, but not all connections are switched yet. +new_queues: on + +# Use create_password option to enable basic auth to create new messaging queues. +# The password should be used as part of server address in client configuration: +# smp://fingerprint:password@host1,host2 +# The password will not be shared with the connecting contacts, you must share it only +# with the users who you want to allow creating messaging queues on your server. +# create_password: password to create new queues (any printable ASCII characters without whitespace, '@', ':' and '/') + +# control_port_admin_password: +# control_port_user_password: + +[TRANSPORT] +# Host is only used to print server address on start. +# You can specify multiple server ports. +host: <domain/ip> +port: 5223,443 +log_tls_errors: off + +# Use `websockets: 443` to run websockets server in addition to plain TLS. +websockets: off +# control_port: 5224 + +[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. +# host_mode: public +# required_host_mode: off + +# The domain suffixes of the relays you operate (space-separated) to count as separate proxy statistics. +# own_server_domains: + +# SOCKS proxy port for forwarding messages to destination servers. +# You may need a separate instance of SOCKS proxy for incoming single-hop requests. +# socks_proxy: localhost:9050 + +# `socks_mode` can be 'onion' for SOCKS proxy to be used for .onion destination hosts only (default) +# 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. +# client_concurrency: 32 + +[INACTIVE_CLIENTS] +# TTL and interval to check inactive clients +disconnect: off +# ttl: 21600 +# check_interval: 3600 + +[WEB] +# Set path to generate static mini-site for server information and qr codes/links +static_path: /var/opt/simplex/www + +# Run an embedded server on this port +# Onion sites can use any port and register it in the hidden service config. +# Running on a port 80 may require setting process capabilities. +#http: 8000 + +# You can run an embedded TLS web server too if you provide port and cert and key files. +# Not required for running relay on onion address. +https: 443 +cert: /etc/opt/simplex/web.crt +key: /etc/opt/simplex/web.key +``` + +## Server security + +### Initialization + +Although it's convenient to initialize smp-server configuration directly on the server, operators **ARE ADVISED** to initialize smp-server fully offline to protect your SMP server CA private key. + +Follow the steps to quickly initialize the server offline: + +1. Install Docker on your system. + +2. Deploy [smp-server](https://github.com/simplex-chat/simplexmq#using-docker) locally. + +3. Destroy the container. All relevant configuration files and keys will be available at `$HOME/simplex/smp/config`. + +4. Move your `CA` private key (`ca.key`) to the safe place. For further explanation, see the next section: [Server security: Private keys](#private-keys). + +5. Copy all other configuration files **except** the CA key to the server: + + ```sh + rsync -hzasP $HOME/simplex/smp/config/ <server_user>@<server_address>:/etc/opt/simplex/ + ``` + +### Private keys + +Connection to the smp server occurs via a TLS connection. During the TLS handshake, the client verifies smp-server CA and server certificates by comparing its fingerprint with the one included in server address. If server TLS credential is compromised, this key can be used to sign a new one, keeping the same server identity and established connections. In order to protect your smp-server from bad actors, operators **ARE ADVISED** to move CA private key to a safe place. That could be: + +- [Tails](https://tails.net/) live usb drive with [persistent and encrypted storage](https://tails.net/doc/persistent_storage/create/index.en.html). +- Offline Linux laptop. +- Bitwarden. +- Any other safe storage that satisfy your security requirements. + +Follow the steps to secure your CA keys: + +1. Login to your server via SSH. + +2. Copy the CA key to a safe place from this file: + + ```sh + /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)**: + + ```sh + rm /etc/opt/simplex/ca.key + ``` + +### Online certificate rotation + +Operators of smp servers **ARE ADVISED** to rotate online certificate regularly (e.g., every 3 months). In order to do this, follow the steps: + +1. Create relevant folders: + + ```sh + mkdir -p $HOME/simplex/smp/config + ``` + +1. Copy the configuration files from the server to the local machine (if not yet): + + ```sh + rsync -hzasP <server_user>@<server_address>:/etc/opt/simplex/ $HOME/simplex/smp/config/ + ``` + +2. **Copy** your CA private key from a safe place to the local machine and name it `ca.key`. + +3. Download latest `smp-server` binary [from Github releases](https://github.com/simplex-chat/simplexmq/releases): + + ```sh + curl -L 'https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64' -o smp-server + ``` + +4. Put the `smp-server` binary to your `$PATH` and make it executable: + + ```sh + sudo mv smp-server /usr/local/bin/ && chmod +x /usr/local/bin/smp-server + ``` + +5. Export a variable to configure your path to smp-server configuration: + + ```sh + export SMP_SERVER_CFG_PATH=$HOME/simplex/smp/config + ``` + +6. Execute the following command: + + ```sh + smp-server cert + ``` + + This command should print: + + ```sh + Certificate request self-signature ok + subject=CN = <your domain or IP> + Generated new server credentials + ---------- + You should store CA private key securely and delete it from the server. + If server TLS credential is compromised this key can be used to sign a new one, keeping the same server identity and established connections. + CA private key location: + $HOME/simplex/smp/config/ca.key + ---------- + ``` + +7. Remove the CA key from the config folder (make sure you have a backup!): + + ```sh + rm $HOME/simplex/smp/config/ca.key + ``` + +8. Upload new certificates to the server: + + ```sh + rsync -hzasP $HOME/simplex/smp/config/ <server_user>@<server_address>:/etc/opt/simplex/ + ``` + +9. Connect to the server via SSH and restart the service: + + ```sh + ssh <server_user>@<server_address> "systemctl restart smp-server" + ``` + +10. Done! + +## Tor: installation and configuration + +### Installation for onion address + +SMP-server can also be deployed to be available via [Tor](https://www.torproject.org) network. Run the following commands as `root` user. + +1. Install tor: + + We're assuming you're using Ubuntu/Debian based distributions. If not, please refer to [offical tor documentation](https://community.torproject.org/onion-services/setup/install/) or your distribution guide. + + - Configure offical Tor PPA repository: + + ```sh + CODENAME="$(lsb_release -c | awk '{print $2}')" + echo "deb [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main + deb-src [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main" > /etc/apt/sources.list.d/tor.list + ``` + + - Import repository key: + + ```sh + curl --proto '=https' --tlsv1.2 -sSf https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | tee /usr/share/keyrings/tor-archive-keyring.gpg >/dev/null + ``` + + - Update repository index: + + ```sh + apt update + ``` + + - Install `tor` package: + + ```sh + apt install -y tor deb.torproject.org-keyring + ``` + +2. Configure tor: + + - File configuration: + + Open tor configuration with your editor of choice (`nano`,`vim`,`emacs`,etc.): + + ```sh + vim /etc/tor/torrc + ``` + + And insert the following lines to the bottom of configuration. Please note lines starting with `#`: this is comments about each individual options. + + ```sh + # Enable log (otherwise, tor doesn't 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. + SOCKSPort 0 + HiddenServiceNonAnonymousMode 1 + HiddenServiceSingleHopMode 1 + # smp-server hidden service host directory and port mappings + HiddenServiceDir /var/lib/tor/simplex-smp/ + HiddenServicePort 5223 localhost:5223 + HiddenServicePort 443 localhost:443 + ``` + + - Create directories: + + ```sh + mkdir /var/lib/tor/simplex-smp/ && chown debian-tor:debian-tor /var/lib/tor/simplex-smp/ && chmod 700 /var/lib/tor/simplex-smp/ + ``` + +3. Start tor: + + Enable `systemd` service and start tor. Offical `tor` is a bit 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 + ``` + +4. Display onion host: + + Execute the following command to display your onion host address: + + ```sh + cat /var/lib/tor/simplex-smp/hostname + ``` + +### SOCKS port for SMP PROXY + +SMP-server versions starting from `v5.8.0-beta.0` can be configured to PROXY smp servers available exclusively through [Tor](https://www.torproject.org) network to be accessible to the clients that do not use Tor. Run the following commands as `root` user. + +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: + + ```sh + tor-instance-create tor2 + ``` + +3. Open the `tor2` configuration and replace its content with the following lines: + + ```sh + vim /etc/tor/instances/tor2/torrc + ``` + + ```sh + # Log tor to systemd daemon + Log notice syslog + # Listen to local 9050 port for socks proxy + SocksPort 9050 + ``` + +3. Enable service at startup and start the daemon: + + ```sh + systemctl enable --now tor@tor2 + ``` + + You can check `tor2` logs with the following command: + + ```sh + journalctl -u tor@tor2 + ``` + +4. After [server initialization](#configuration), configure the `PROXY` section like so: + + ```ini + ... + [PROXY] + socks_proxy: 127.0.0.1:9050 + own_server_domains: <your domain suffixes if using `log_stats: on`> + ... + ``` + +## Server information page + +SMP server **SHOULD** be configured to serve Web page with server information that can include admin info, server info, provider info, etc. It will also serve connection links, generated using the mobile/desktop apps. Run the following commands as `root` user. + +_Please note:_ this configuration is supported since `v6.1.0-beta.2`. + +1. Add the following to your smp-server configuration (please modify fields in [INFORMATION] section to include relevant information): + + ```sh + vim /etc/opt/simplex/smp-server.ini + ``` + + ```ini + [TRANSPORT] + # host is only used to print server address on start + host: <domain/ip> + port: 443,5223 + websockets: off + log_tls_errors: off + control_port: 5224 + + [WEB] + https: 443 + static_path: /var/opt/simplex/www + cert: /etc/opt/simplex/web.crt + key: /etc/opt/simplex/web.key + + [INFORMATION] + # AGPLv3 license requires that you make any source code modifications + # available to the end users of the server. + # LICENSE: https://github.com/simplex-chat/simplexmq/blob/stable/LICENSE + # Include correct source code URI in case the server source code is modified in any way. + # If any other information fields are present, source code property also MUST be present. + + source_code: https://github.com/simplex-chat/simplexmq + + # Declaring all below information is optional, any of these fields can be omitted. + + # Server usage conditions and amendments. + # It is recommended to use standard conditions with any amendments in a separate document. + # usage_conditions: https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md + # condition_amendments: link + + # Server location and operator. + server_country: <YOUR_SERVER_LOCATION> + operator: <YOUR_NAME> + operator_country: <YOUR_LOCATION> + website: <WEBSITE_IF_AVAILABLE> + + # Administrative contacts. + #admin_simplex: SimpleX address + admin_email: <EMAIL> + # admin_pgp: + # admin_pgp_fingerprint: + + # Contacts for complaints and feedback. + # complaints_simplex: SimpleX address + complaints_email: <COMPLAINTS_EMAIL> + # complaints_pgp: + # complaints_pgp_fingerprint: + + # Hosting provider. + hosting: <HOSTING_PROVIDER_NAME> + hosting_country: <HOSTING_PROVIDER_LOCATION> + ``` + +2. Install the webserver. For easy deployment we'll describe the installtion process of [Caddy](https://caddyserver.com) webserver on Ubuntu server: + + 1. Install the packages: + + ```sh + sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl + ``` + + 2. Install caddy gpg key for repository: + + ```sh + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg + ``` + + 3. Install Caddy repository: + + ```sh + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list + ``` + + 4. Install Caddy: + + ```sh + sudo apt update && sudo apt install caddy + ``` + + [Full Caddy instllation instructions](https://caddyserver.com/docs/install) + +3. Replace Caddy configuration with the following: + + Please replace `YOUR_DOMAIN` with your actual domain (smp.example.com). + + ```sh + vim /etc/caddy/Caddyfile + ``` + + ``` + http://YOUR_DOMAIN { + redir https://YOUR_DOMAIN{uri} permanent + } + + YOUR_DOMAIN:8443 { + tls { + key_type rsa4096 + } + } + ``` + +4. Enable and start Caddy service: + + ```sh + systemctl enable --now caddy + ``` + +5. Create script to copy certificates to your smp directory: + + Please replace `YOUR_DOMAIN` with your actual domain (smp.example.com). + + ```sh + vim /usr/local/bin/simplex-servers-certs + ``` + + ```sh + #!/usr/bin/env sh + set -eu + + user='smp' + group="$user" + + domain='HOST' + folder_in="/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/${domain}" + folder_out='/etc/opt/simplex' + key_name='web.key' + cert_name='web.crt' + + # Copy certifiacte 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 + cp "${folder_in}/${domain}.key" "${folder_out}/${key_name}" + # Assign correct permissions + chown "$user":"$group" "${folder_out}/${key_name}" + ``` + +6. Make the script executable and execute it: + + ```sh + chmod +x /usr/local/bin/simplex-servers-certs && /usr/local/bin/simplex-servers-certs + ``` + +7. Check if certificates were copied: + + ```sh + ls -haltr /etc/opt/simplex/web* + ``` + +8. Create cronjob to copy certificates to smp directory in timely manner: + + ```sh + sudo crontab -e + ``` + + ```sh + # Every week on 00:20 sunday + 20 0 * * 0 /usr/local/bin/simplex-servers-certs + ``` + +9. Then: + + - If you're running at least `v6.1.0-beta.2`, [restart the server](#systemd-commands). + - If you're running below `v6.1.0-beta.2`, [upgrade the server](#updating-your-smp-server). + +10. Access the webpage you've deployed from your browser (`https://smp.example.org`). You should see the smp-server information that you've provided in your ini file. ## Documentation @@ -364,33 +1302,180 @@ Nov 23 19:23:21 5588ab759e80 smp-server[30878]: not expiring inactive clients Nov 23 19:23:21 5588ab759e80 smp-server[30878]: creating new queues requires password ``` -### Monitoring +### Control port + +Enabling control port in the configuration allows administrator to see information about the smp-server in real-time. Additionally, it allows to delete queues for content moderation and see the debug info about the clients, sockets, etc. Enabling the control port requires setting the `admin` and `user` passwords. + +1. Generate two passwords for each user: + + ```sh + tr -dc A-Za-z0-9 </dev/urandom | head -c 20; echo + ``` + +2. Open the configuration file: + + ```sh + vim /etc/opt/simplex/smp-server.ini + ``` + +2. Configure the control port and replace the passwords: + + ```ini + [AUTH] + control_port_admin_password: <your_randomly_generated_admin_password> + control_port_user_password: <your_randomly_generated_user_password> + + [TRANSPORT] + control_port: 5224 + ``` + +3. Restart the server: + + ```sh + systemctl restart smp-server + ``` + +To access the control port, use: + +```sh +nc 127.0.0.1 5224 +``` + +or: + +```sh +telnet 127.0.0.1 5224 +``` + +Upon connecting, the control port should print: + +```sh +SMP server control port +'help' for supported commands +``` + +To authenticate, type the following and hit enter. Change the `my_generated_password` with the `user` or `admin` password from the configuration: + +```sh +auth my_generated_password +``` + +Here's the full list of commands, their descriptions and who can access them. + +| Command | Description | Requires `admin` role | +| ---------------- | ------------------------------------------------------------------------------- | -------------------------- | +| `stats` | Real-time statistics. Fields described in [Daily statistics](#daily-statistics) | - | +| `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 | +| `threads` | Threads information. Useful for debugging. | yes | +| `server-info` | Aggregated server infomation. | - | +| `delete` | Delete known queue. Useful for content moderation. | - | +| `save` | Save queues/messages from memory. | yes | +| `help` | Help menu. | - | +| `quit` | Exit the control port. | - | + +### Daily statistics You can enable `smp-server` statistics for `Grafana` dashboard by setting value `on` in `/etc/opt/simplex/smp-server.ini`, under `[STORE_LOG]` section in `log_stats:` field. Logs will be stored as `csv` file in `/var/opt/simplex/smp-server-stats.daily.log`. Fields for the `csv` file are: ```sh -fromTime,qCreated,qSecured,qDeleted,msgSent,msgRecv,dayMsgQueues,weekMsgQueues,monthMsgQueues +fromTime,qCreated,qSecured,qDeleted,msgSent,msgRecv,dayMsgQueues,weekMsgQueues,monthMsgQueues,msgSentNtf,msgRecvNtf,dayCountNtf,weekCountNtf,monthCountNtf,qCount,msgCount,msgExpired,qDeletedNew,qDeletedSecured,pRelays_pRequests,pRelays_pSuccesses,pRelays_pErrorsConnect,pRelays_pErrorsCompat,pRelays_pErrorsOther,pRelaysOwn_pRequests,pRelaysOwn_pSuccesses,pRelaysOwn_pErrorsConnect,pRelaysOwn_pErrorsCompat,pRelaysOwn_pErrorsOther,pMsgFwds_pRequests,pMsgFwds_pSuccesses,pMsgFwds_pErrorsConnect,pMsgFwds_pErrorsCompat,pMsgFwds_pErrorsOther,pMsgFwdsOwn_pRequests,pMsgFwdsOwn_pSuccesses,pMsgFwdsOwn_pErrorsConnect,pMsgFwdsOwn_pErrorsCompat,pMsgFwdsOwn_pErrorsOther,pMsgFwdsRecv,qSub,qSubAuth,qSubDuplicate,qSubProhibited,msgSentAuth,msgSentQuota,msgSentLarge,msgNtfs,msgNtfNoSub,msgNtfLost,qSubNoMsg,msgRecvGet,msgGet,msgGetNoMsg,msgGetAuth,msgGetDuplicate,msgGetProhibited,psSubDaily,psSubWeekly,psSubMonthly,qCount2,ntfCreated,ntfDeleted,ntfSub,ntfSubAuth,ntfSubDuplicate,ntfCount,qDeletedAllB,qSubAllB,qSubEnd,qSubEndB,ntfDeletedB,ntfSubB,msgNtfsB,msgNtfExpired ``` -- `fromTime` - timestamp; date and time of event +**Fields description** -- `qCreated` - int; created queues - -- `qSecured` - int; established queues - -- `qDeleted` - int; deleted queues - -- `msgSent` - int; sent messages - -- `msgRecv` - int; received messages - -- `dayMsgQueues` - int; active queues in a day - -- `weekMsgQueues` - int; active queues in a week - -- `monthMsgQueues` - int; active queues in a month +| Field number | Field name | Field Description | +| ------------- | ---------------------------- | -------------------------- | +| 1 | `fromTime` | Date of statistics | +| Messaging queue: | +| 2 | `qCreated` | Created | +| 3 | `qSecured` | Established | +| 4 | `qDeleted` | Deleted | +| Messages: | +| 5 | `msgSent` | Sent | +| 6 | `msgRecv` | Received | +| 7 | `dayMsgQueues` | Active queues in a day | +| 8 | `weekMsgQueues` | Active queues in a week | +| 9 | `monthMsgQueues` | Active queues in a month | +| Messages with "notification" flag | +| 10 | `msgSentNtf` | Sent | +| 11 | `msgRecvNtf` | Received | +| 12 | `dayCountNtf` | Active queues in a day | +| 13 | `weekCountNtf` | Active queues in a week | +| 14 | `monthCountNtf` | Active queues in a month | +| Additional statistics: | +| 15 | `qCount` | Stored queues | +| 16 | `msgCount` | Stored messages | +| 17 | `msgExpired` | Expired messages | +| 18 | `qDeletedNew` | New deleted queues | +| 19 | `qDeletedSecured` | Secured deleted queues | +| Requested sessions with all relays: | +| 20 | `pRelays_pRequests` | - requests | +| 21 | `pRelays_pSuccesses` | - successes | +| 22 | `pRelays_pErrorsConnect` | - connection errors | +| 23 | `pRelays_pErrorsCompat` | - compatability 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 | +| 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 | +| 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 | +| 39 | `pMsgFwdsOwn_pErrorsOther` | - other errors | +| Received message forwards: | +| 40 | `pMsgFwdsRecv` | | +| Message queue subscribtion errors: | +| 41 | `qSub` | All | +| 42 | `qSubAuth` | Authentication erorrs | +| 43 | `qSubDuplicate` | Duplicate SUB errors | +| 44 | `qSubProhibited` | Prohibited SUB errors | +| Message errors: | +| 45 | `msgSentAuth` | Authentication errors | +| 46 | `msgSentQuota` | Quota errors | +| 47 | `msgSentLarge` | Large message errors | +| 48 | `msgNtfs` | XXXXXXXXXXXXXXXXXXXX | +| 49 | `msgNtfNoSub` | XXXXXXXXXXXXXXXXXXXX | +| 50 | `msgNtfLost` | XXXXXXXXXXXXXXXXXXXX | +| 51 | `qSubNoMsg` | Removed, always 0 | +| 52 | `msgRecvGet` | XXXXXXXXXXXXXXXXX | +| 53 | `msgGet` | XXXXXXXXXXXXXXXXX | +| 54 | `msgGetNoMsg` | XXXXXXXXXXXXXXXXX | +| 55 | `msgGetAuth` | XXXXXXXXXXXXXXXXX | +| 56 | `msgGetDuplicate` | XXXXXXXXXXXXXXXXX | +| 57 | `msgGetProhibited` | XXXXXXXXXXXXXXXXX | +| 58 | `psSub_dayCount` | Removed, always 0 | +| 59 | `psSub_weekCount` | Removed, always 0 | +| 60 | `psSub_monthCount` | Removed, always 0 | +| 61 | `qCount` | XXXXXXXXXXXXXXXXX | +| 62 | `ntfCreated` | XXXXXXXXXXXXXXXXX | +| 63 | `ntfDeleted` | XXXXXXXXXXXXXXXXX | +| 64 | `ntfSub` | XXXXXXXXXXXXXXXXX | +| 65 | `ntfSubAuth` | XXXXXXXXXXXXXXXXX | +| 66 | `ntfSubDuplicate` | XXXXXXXXXXXXXXXXX | +| 67 | `ntfCount` | XXXXXXXXXXXXXXXXX | +| 68 | `qDeletedAllB` | XXXXXXXXXXXXXXXXX | +| 69 | `qSubAllB` | XXXXXXXXXXXXXXXXX | +| 70 | `qSubEnd` | XXXXXXXXXXXXXXXXX | +| 71 | `qSubEndB` | XXXXXXXXXXXXXXXXX | +| 72 | `ntfDeletedB` | XXXXXXXXXXXXXXXXX | +| 73 | `ntfSubB` | XXXXXXXXXXXXXXXXX | +| 74 | `msgNtfsB` | XXXXXXXXXXXXXXXXX | +| 75 | `msgNtfExpired` | XXXXXXXXXXXXXXXXX | To import `csv` to `Grafana` one should: @@ -417,7 +1502,143 @@ To import `csv` to `Grafana` one should: For further documentation, see: [CSV Data Source for Grafana - Documentation](https://grafana.github.io/grafana-csv-datasource/) -### Configuring the app to use the server +## Updating your SMP server + +To update your smp-server to latest version, choose your installation method and follow the steps: + + - Manual deployment + + 1. Stop the server: + + ```sh + sudo systemctl stop smp-server + ``` + + 2. Update the binary: + + ```sh + curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/smp-server && chmod +x /usr/local/bin/smp-server + ``` + + 3. Start the server: + + ```sh + sudo systemctl start smp-server + ``` + + - [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script) + + 1. Execute the followin command: + + ```sh + sudo simplex-servers-update + ``` + + To install specific version, run: + + ```sh + export VER=<version_from_github_releases> &&\ + sudo -E simplex-servers-update + ``` + + 2. Done! + + - [Docker container](https://github.com/simplex-chat/simplexmq#using-docker) + + 1. Stop and remove the container: + + ```sh + docker rm $(docker stop $(docker ps -a -q --filter ancestor=simplexchat/smp-server --format="\{\{.ID\}\}")) + ``` + + 2. Pull latest image: + + ```sh + docker pull simplexchat/smp-server:latest + ``` + + 3. Start new container: + + ```sh + docker run -d \ + -p 5223:5223 \ + -p 443:443 \ + -v $HOME/simplex/smp/config:/etc/opt/simplex:z \ + -v $HOME/simplex/smp/logs:/var/opt/simplex:z \ + simplexchat/smp-server:latest + ``` + + - [Linode Marketplace](https://www.linode.com/marketplace/apps/simplex-chat/simplex-chat/) + + 1. Pull latest images: + + ```sh + docker-compose --project-directory /etc/docker/compose/simplex pull + ``` + + 2. Restart the containers: + + ```sh + docker-compose --project-directory /etc/docker/compose/simplex up -d --remove-orphans + ``` + + 3. Remove obsolete images: + + ```sh + docker image prune + ``` + +## Reproduce builds + +You can locally reproduce server binaries, following these instructions. + +If you are a security expert or researcher, you can help SimpleX network and users community by signing the release checksums – we will [publish your signature](https://github.com/simplex-chat/simplexmq/releases/tag/v6.3.1). Please reach out to us! + +To reproduce the build you must have: + +- Linux machine +- `x86-64` architecture +- Installed `docker`, `curl` and `git` + +1. Download script: + + ```sh + curl -LO 'https://raw.githubusercontent.com/simplex-chat/simplexmq/refs/heads/master/scripts/simplexmq-reproduce-builds.sh' + ``` + +2. Make it executable: + + ```sh + chmod +x simplexmq-reproduce-builds.sh + ``` + +3. Execute the script with the required tag: + + ```sh + ./simplexmq-reproduce-builds.sh 'v6.3.1' + ``` + + The script executes these steps (please review the script to confirm): + + 1) builds all server 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 (e.g., `v6.3.1`) with two subfolders: + + ```sh + ls v6.3.1 + ``` + + ```sh + from-source prebuilt _sha256sums + ``` + + The file _sha256sums contains the hashes of all builds - you can compare it with the same file in GitHub release. + +## 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. diff --git a/docs/SIMPLEX.md b/docs/SIMPLEX.md index 7ed01efa3c..ec25afaf88 100644 --- a/docs/SIMPLEX.md +++ b/docs/SIMPLEX.md @@ -3,7 +3,7 @@ title: SimpleX platform revision: 07.02.2023 --- -| Updated 07.02.2023 | Languages: EN, [FR](/docs/lang/fr/SIMPLEX.md), [CZ](/docs/lang/cs/SIMPLEX.md) | +| Updated 07.02.2023 | Languages: EN, [FR](/docs/lang/fr/SIMPLEX.md), [CZ](/docs/lang/cs/SIMPLEX.md), [PL](/docs/lang/pl/SIMPLEX.md) | # SimpleX platform - motivation and comparison ## Problems diff --git a/docs/SimpleX_Design_Review_2024_Summary_Report_12_08_2024.pdf b/docs/SimpleX_Design_Review_2024_Summary_Report_12_08_2024.pdf new file mode 100644 index 0000000000..aec222046c Binary files /dev/null and b/docs/SimpleX_Design_Review_2024_Summary_Report_12_08_2024.pdf differ diff --git a/docs/TRANSLATIONS.md b/docs/TRANSLATIONS.md index 85b24b368e..d5c1cdef0b 100644 --- a/docs/TRANSLATIONS.md +++ b/docs/TRANSLATIONS.md @@ -3,7 +3,7 @@ title: Contributing translations to SimpleX Chat revision: 19.03.2023 --- -| 19.03.2023 | EN, [CZ](/docs/lang/cs/TRANSLATIONS.md), [FR](/docs/lang/fr/TRANSLATIONS.md) | +| 19.03.2023 | EN, [CZ](/docs/lang/cs/TRANSLATIONS.md), [FR](/docs/lang/fr/TRANSLATIONS.md), [PL](/docs/lang/pl/TRANSLATIONS.md) | # Contributing translations to SimpleX Chat diff --git a/docs/TRANSPARENCY.md b/docs/TRANSPARENCY.md new file mode 100644 index 0000000000..bd0dcabb53 --- /dev/null +++ b/docs/TRANSPARENCY.md @@ -0,0 +1,34 @@ +--- +title: Transparency Reports +permalink: /transparency/index.html +revision: 15.01.2025 +--- + +# Transparency Reports + +**Updated**: Jan 15, 2025 + +SimpleX Chat Ltd. is a company registered in the UK – it develops communication software enabling users to operate and communicate via SimpleX network, without user profile identifiers of any kind, and without having their data hosted by any network infrastructure operators. + +This page will include any and all reports on requests for user data. + +*To date, we received none*. + +In 2024 we received enquiries from several law enforcement agencies seeking information on our procedures for handling data requests. We responded by noting that we operate under the UK law and will consider such requests pursuant to UK law. + +Our objective is to consistently ensure that no user data and absolute minimum of the metadata required for the network to function is available for disclosure by any infrastructure operators, under any circumstances. + +**Helpful resources**: +- [Privacy policy](/PRIVACY.md) +- [Privacy and security: technical details and limitations](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#privacy-and-security-technical-details-and-limitations) +- Whitepaper: + - [Trust in servers](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#trust-in-servers) + - [Encryption Primitives Used](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#encryption-primitives-used) + - [Threat model](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#threat-model) +- Security assessments: + - Trail of Bits, SimpleX cryptography and networking, [October 2022](../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). + - Trail of Bits, the cryptographic review of SimpleX protocols design, [July 2024](../blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md). + +Have a more specific question? Reach out to us via [SimpleX 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) or via email [chat@simplex.chat](mailto:chat@simplex.chat). + +For any sensitive questions please use SimpleX Chat or encrypted email messages using the key for this address from [keys.openpgp.org](https://keys.openpgp.org/search?q=chat%40simplex.chat) (its fingerprint is `FB44 AF81 A45B DE32 7319 797C 8510 7E35 7D4A 17FC`) and make your key available for a secure reply. diff --git a/docs/WEBRTC.md b/docs/WEBRTC.md index 7978d21ec7..8ce31bf959 100644 --- a/docs/WEBRTC.md +++ b/docs/WEBRTC.md @@ -3,7 +3,7 @@ title: Using custom WebRTC ICE servers in SimpleX Chat revision: 31.01.2023 --- -| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/WEBRTC.md), [CZ](/docs/lang/cs/WEBRTC.md) | +| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/WEBRTC.md), [CZ](/docs/lang/cs/WEBRTC.md), [PL](/docs/lang/pl/WEBRTC.md) | # Using custom WebRTC ICE servers in SimpleX Chat diff --git a/docs/XFTP-SERVER.md b/docs/XFTP-SERVER.md index 2977ff15da..ba4770644e 100644 --- a/docs/XFTP-SERVER.md +++ b/docs/XFTP-SERVER.md @@ -4,6 +4,23 @@ revision: 31.07.2023 --- # Hosting your own XFTP Server +## Table of Contents + +- [Overview](#overview) +- [Installation options](#installation-options) + - [systemd service](#systemd-service) with [installation script](#installation-script) or [manually](#manual-deployment) + - [docker container](#docker-сontainer) + - [Linode marketplace](#linode-marketplace) +- [Tor installation](#tor-installation) +- [Configuration](#configuration) +- [Documentation](#documentation) + - [XFTP server address](#xftp-server-address) + - [Systemd commands](#systemd-commands) + - [Control port](#control-port) + - [Daily statistics](#daily-statistics) +- [Updating your XFTP server](#updating-your-xftp-server) +- [Configuring the app to use the server](#configuring-the-app-to-use-the-server) + ## Overview XFTP is a new file transfer protocol focussed on meta-data protection - it is based on the same principles as SimpleX Messaging Protocol used in SimpleX Chat messenger: @@ -15,24 +32,50 @@ XFTP is a new file transfer protocol focussed on meta-data protection - it is ba - no identifiers or ciphertext in common between sent and received relay traffic, same as for messages delivered by SMP relays. - protection of sender IP address from the recipients. -## Installation +## Installation options -0. First, install `xftp-server`: +You can install XFTP server in one of the following ways: - - Manual deployment (see below) +- [systemd service](#systemd-service) + - using [installation script](#installation-script) - **recommended** + - or [manually](#manual-deployment) +- [Docker container](#docker-container) from DockerHub +- [Linode marketplace](#linode-marketplace) - - Semi-automatic deployment: - - [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script) - - [Docker container](https://github.com/simplex-chat/simplexmq#using-docker) +### systemd service -Manual installation requires some preliminary actions: +#### Installation script -0. Install binary: +This installation script will automatically install binaries, systemd services and additional scripts that will manage backups, updates and uninstallation. This is the recommended option due to its flexibility, easy updating, and being battle tested on our servers. + +**Please note** that currently only Ubuntu distribution is supported. + +Run the following script on the server: + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/install.sh -o simplex-server-install.sh &&\ +if echo '53fcdb4ceab324316e2c4cda7e84dbbb344f32550a65975a7895425e5a1be757 simplex-server-install.sh' | sha256sum -c; then + chmod +x ./simplex-server-install.sh + ./simplex-server-install.sh + rm ./simplex-server-install.sh +else + echo "SHA-256 checksum is incorrect!" + rm ./simplex-server-install.sh +fi +``` + +Type `2` and hit enter to install `xftp-server`. + +#### Manual deployment + +Manual installation is the most advanced deployment that provides the most flexibility. Generally recommended only for advanced users. + +1. Install binary: - Using offical binaries: ```sh - curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/xftp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/xftp-server + 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 ``` - Compiling from source: @@ -40,20 +83,20 @@ Manual installation requires some preliminary actions: Please refer to [Build from source: Using your distribution](https://github.com/simplex-chat/simplexmq#using-your-distribution) -1. Create user and group for `xftp-server`: +2. Create user and group for `xftp-server`: ```sh sudo useradd -m xftp ``` -2. Create necessary directories and assign permissions: +3. Create necessary directories and assign permissions: ```sh sudo mkdir -p /var/opt/simplex-xftp /etc/opt/simplex-xftp /srv/xftp sudo chown xftp:xftp /var/opt/simplex-xftp /etc/opt/simplex-xftp /srv/xftp ``` -3. Allow xftp-server port in firewall: +4. Allow xftp-server port in firewall: ```sh # For Ubuntu @@ -63,7 +106,7 @@ Manual installation requires some preliminary actions: sudo firewall-cmd --reload ``` -4. **Optional** — If you're using distribution with `systemd`, create `/etc/systemd/system/xftp-server.service` file with the following content: +5. **Optional** — If you're using distribution with `systemd`, create `/etc/systemd/system/xftp-server.service` file with the following content: ```sh [Unit] @@ -74,7 +117,7 @@ Manual installation requires some preliminary actions: Group=xftp Type=simple ExecStart=/usr/local/bin/xftp-server start +RTS -N -RTS - ExecStopPost=/usr/bin/env sh -c '[ -e "/var/opt/simplex-xftp/file-server-store.log" ] && cp "/var/opt/simplex-xftp/file-server-store.log" "/var/opt/simplex-xftp/file-server-store.log.$(date +'%FT%T')"' + ExecStopPost=/usr/bin/env sh -c '[ -e "/var/opt/simplex-xftp/file-server-store.log" ] && cp "/var/opt/simplex-xftp/file-server-store.log" "/var/opt/simplex-xftp/file-server-store.log.$(date +'%%FT%%T')"' LimitNOFILE=65535 KillSignal=SIGINT TimeoutStopSec=infinity @@ -86,6 +129,68 @@ Manual installation requires some preliminary actions: And execute `sudo systemctl daemon-reload`. +### Docker сontainer + +You can deploy smp-server using Docker Compose. This is 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**. + +This will download images from [Docker Hub](https://hub.docker.com/r/simplexchat). + +1. Create `xftp-server` directory and switch to it: + + ```sh + mkdir xftp-server && cd xftp-server + ``` + +2. Create `docker-compose.yml` file with the following content: + + You can also grab it from here - [docker-compose-xftp.yml](https://raw.githubusercontent.com/simplex-chat/simplexmq/refs/heads/stable/scripts/docker/docker-compose-xftp.yml). Don't forget to rename it to `docker-compose.yml`. + + ```yaml + name: SimpleX Chat - xftp-server + + services: + xftp-server: + image: ${SIMPLEX_XFTP_IMAGE:-simplexchat/xftp-server:latest} + environment: + ADDR: ${ADDR?"Please specify the domain."} + QUOTA: ${QUOTA?"Please specify disk quota."} + PASS: ${PASS:-} + volumes: + - ./xftp_configs:/etc/opt/simplex-xftp + - ./xftp_state:/var/opt/simplex-xftp + - ./xftp_files:/srv/xftp + ports: + - 443:443 + restart: unless-stopped + ``` + +3. In the same directory, create `.env` file with the following content: + + You can also grab it from here - [docker-compose-xftp.env](https://raw.githubusercontent.com/simplex-chat/simplexmq/refs/heads/stable/scripts/docker/docker-compose-xftp.yml). Don't forget to rename it to `.env`. + + Change variables according to your preferences. + + ```env + # Mandatory + ADDR=your_ip_or_addr + QUOTA=120gb + + # Optional + #PASS='123123' + ``` + +4. Start your containers: + + ```sh + docker compose up + ``` + +### Linode marketplace + +You can deploy xftp-server upon creating new Linode VM. Please refer to: [Linode Marketplace](https://www.linode.com/marketplace/apps/simplex-chat/simplex-chat/) + ## Tor installation xftp-server can also be deployed to serve from [tor](https://www.torproject.org) network. Run the following commands as `root` user. @@ -358,9 +463,76 @@ Feb 27 19:21:11 localhost xftp-server[2350]: Store log: /var/opt/simplex-xftp/fi Feb 27 19:21:11 localhost xftp-server[2350]: Uploading new files allowed. Feb 27 19:21:11 localhost xftp-server[2350]: Listening on port 443... Feb 27 19:21:11 localhost xftp-server[2350]: [INFO 2023-02-27 19:21:11 +0000 src/Simplex/FileTransfer/Server/Env.hs:85] Total / available storage: 64424509440 / 64424509440 -```` +``` -### Monitoring +### Control port + +Enabling control port in the configuration allows administrator to see information about the xftp-server in real-time. Additionally, it allows to delete file chunks for content moderation and see the debug info about the clients, sockets, etc. Enabling the control port requires setting the `admin` and `user` passwords. + +1. Generate two passwords for each user: + + ```sh + tr -dc A-Za-z0-9 </dev/urandom | head -c 20; echo + ``` + +2. Open the configuration file: + + ```sh + vim /etc/opt/simplex-xftp/file-server.ini + ``` + +2. Configure the control port and replace the passwords: + + ```ini + [AUTH] + control_port_admin_password: <your_randomly_generated_admin_password> + control_port_user_password: <your_randomly_generated_user_password> + + [TRANSPORT] + control_port: 5224 + ``` + +3. Restart the server: + + ```sh + systemctl restart xftp-server + ``` + +To access the control port, use: + +```sh +nc 127.0.0.1 5224 +``` + +or: + +```sh +telnet 127.0.0.1 5224 +``` + +Upon connecting, the control port should print: + +```sh +XFTP server control port +'help' for supported commands +``` + +To authenticate, type the following and hit enter. Change the `my_generated_password` with the `user` or `admin` password from the configuration: + +```sh +auth my_generated_password +``` + +Here's the full list of commands, their descriptions and who can access them. + +| Command | Description | Requires `admin` role | +| ---------------- | ------------------------------------------------------------------------------- | -------------------------- | +| `stats-rts` | GHC/Haskell statistics. Can be enabled with `+RTS -T -RTS` option | - | +| `delete` | Delete known file chunk. Useful for content moderation. | - | +| `help` | Help menu. | - | +| `quit` | Exit the control port. | - | + +### Daily statistics You can enable `xftp-server` statistics for `Grafana` dashboard by setting value `on` in `/etc/opt/simplex-xftp/file-server.ini`, under `[STORE_LOG]` section in `log_stats:` field. @@ -418,6 +590,65 @@ To import `csv` to `Grafana` one should: For further documentation, see: [CSV Data Source for Grafana - Documentation](https://grafana.github.io/grafana-csv-datasource/) -### Configuring the app to use the server + +## Updating your XFTP server + +To update your XFTP server to latest version, choose your installation method and follow the steps: + + - Manual deployment + 1. Stop the server: + ```sh + sudo systemctl stop xftp-server + ``` + 2. Update the binary: + ```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 + ``` + 3. Start the server: + ```sh + sudo systemctl start xftp-server + ``` + + - [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script) + 1. Execute the followin command: + ```sh + sudo simplex-servers-update + ``` + 2. Done! + + - [Docker container](https://github.com/simplex-chat/simplexmq#using-docker) + 1. Stop and remove the container: + ```sh + docker rm $(docker stop $(docker ps -a -q --filter ancestor=simplexchat/xftp-server --format="\{\{.ID\}\}")) + ``` + 2. Pull latest image: + ```sh + docker pull simplexchat/xftp-server:latest + ``` + 3. Start new container: + ```sh + docker run -d \ + -p 443:443 \ + -v $HOME/simplex/xftp/config:/etc/opt/simplex-xftp:z \ + -v $HOME/simplex/xftp/logs:/var/opt/simplex-xftp:z \ + -v $HOME/simplex/xftp/files:/srv/xftp:z \ + simplexchat/xftp-server:latest + ``` + + - [Linode Marketplace](https://www.linode.com/marketplace/apps/simplex-chat/simplex-chat/) + 1. Pull latest images: + ```sh + docker-compose --project-directory /etc/docker/compose/simplex pull + ``` + 2. Restart the containers: + ```sh + docker-compose --project-directory /etc/docker/compose/simplex up -d --remove-orphans + ``` + 3. Remove obsolete images: + ```sh + docker image prune + ``` + +## Configuring the app to use the server Please see: [SMP Server: Configuring the app to use the server](./SERVER.md#configuring-the-app-to-use-the-server). diff --git a/docs/dependencies/HASKELL.md b/docs/dependencies/HASKELL.md new file mode 100644 index 0000000000..e6b180e33b --- /dev/null +++ b/docs/dependencies/HASKELL.md @@ -0,0 +1,177 @@ +# Dependency License Report + +Bold-faced **`package-name`**s denote standard libraries bundled with `ghc-9.6.3`. + +## Direct dependencies of `simplex-chat:exe:simplex-chat` + +| Name | Version | [SPDX](https://spdx.org/licenses/) License Id | Description | Also depended upon by | +| --- | --- | --- | --- | --- | +| **`base`** | [`4.18.1.0`](http://hackage.haskell.org/package/base-4.18.1.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/base-4.18.1.0/src/LICENSE) | Basic libraries | *(core library)* | +| `base64-bytestring` | [`1.2.1.0`](http://hackage.haskell.org/package/base64-bytestring-1.2.1.0) | [`BSD-3-Clause`](./licences/haskell/base64-bytestring-1.2.1.0/LICENSE) | Fast base64 encoding and decoding for ByteStrings | `simplexmq`, `websockets` | +| **`bytestring`** | [`0.11.5.2`](http://hackage.haskell.org/package/bytestring-0.11.5.2) | [`BSD-3-Clause`](http://hackage.haskell.org/package/bytestring-0.11.5.2/src/LICENSE) | Fast, compact, strict and lazy byte strings with a list interface | `SHA`, `aeson`, `appar`, `asn1-encoding`, `asn1-parse`, `asn1-types`, `attoparsec`, `base64-bytestring`, `binary`, `bitvec`, `blaze-builder`, `blaze-textual`, `bytestring-builder`, `case-insensitive`, `cereal`, `conduit`, `conduit-extra`, `cryptohash-md5`, `cryptohash-sha1`, `crypton`, `crypton-x509`, `crypton-x509-store`, `crypton-x509-validation`, `cryptostore`, `digest`, `direct-sqlcipher`, `email-validate`, `entropy`, `fast-logger`, `file-embed`, `filepath`, `hashable`, `http-types`, `http2`, `integer-conversion`, `iproute`, `libyaml`, `memory`, `monad-logger`, `mono-traversable`, `network`, `network-byte-order`, `network-transport`, `network-udp`, `parsec`, `pem`, `random`, `scientific`, `simplexmq`, `socks`, `sqlcipher-simple`, `streaming-commons`, `strict`, `terminal`, `text`, `text-short`, `tls`, `typed-process`, `unix`, `unix-time`, `unliftio`, `uuid`, `uuid-types`, `vector-algorithms`, `websockets`, `yaml`, `zip`, `zlib`, `zstd` | +| `composition` | [`1.0.2.2`](http://hackage.haskell.org/package/composition-1.0.2.2) | [`BSD-3-Clause`](./licences/haskell/composition-1.0.2.2/LICENSE) | Combinators for unorthodox function composition | `simplexmq` | +| `constraints` | [`0.14`](http://hackage.haskell.org/package/constraints-0.14) | [`BSD-2-Clause`](./licences/haskell/constraints-0.14/LICENSE) | Constraint manipulation | `simplexmq` | +| **`containers`** | [`0.6.7`](http://hackage.haskell.org/package/containers-0.6.7) | [`BSD-3-Clause`](http://hackage.haskell.org/package/containers-0.6.7/src/LICENSE) | Assorted concrete container types | `QuickCheck`, `aeson`, `attoparsec`, `bifunctors`, `binary`, `cereal`, `comonad`, `crypton-x509`, `crypton-x509-store`, `crypton-x509-validation`, `data-default-instances-containers`, `hashable`, `http2`, `indexed-traversable`, `iproute`, `mono-traversable`, `network-udp`, `resourcet`, `scientific`, `semialign`, `semigroupoids`, `simplexmq`, `sqlcipher-simple`, `th-abstraction`, `websockets`, `witherable`, `yaml`, `zip` | +| `crypton` | [`0.34`](http://hackage.haskell.org/package/crypton-0.34) | [`BSD-3-Clause`](./licences/haskell/crypton-0.34/LICENSE) | Cryptography Primitives sink | `crypton-x509`, `crypton-x509-store`, `crypton-x509-validation`, `cryptostore`, `simplexmq`, `tls` | +| `uuid` | [`1.3.15`](http://hackage.haskell.org/package/uuid-1.3.15) | [`BSD-3-Clause`](./licences/haskell/uuid-1.3.15/LICENSE) | For creating, comparing, parsing and printing Universally Unique Identifiers | | +| **`directory`** | [`1.3.8.1`](http://hackage.haskell.org/package/directory-1.3.8.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/directory-1.3.8.1/src/LICENSE) | Platform-agnostic library for filesystem operations | `conduit`, `conduit-extra`, `crypton-x509-store`, `easy-file`, `fast-logger`, `file-embed`, `network`, `process`, `simplexmq`, `streaming-commons`, `temporary`, `unliftio`, `yaml`, `zip` | +| `direct-sqlcipher` | [`2.3.28`](http://hackage.haskell.org/package/direct-sqlcipher-2.3.28) | *MISSING* | *MISSING* | `simplexmq`, `sqlcipher-simple` | +| `data-default` | [`0.7.1.1`](http://hackage.haskell.org/package/data-default-0.7.1.1) | [`BSD-3-Clause`](./licences/haskell/data-default-0.7.1.1/LICENSE) | A class for types with a default value | `simplexmq` | +| **`exceptions`** | [`0.10.7`](http://hackage.haskell.org/package/exceptions-0.10.7) | [`BSD-3-Clause`](http://hackage.haskell.org/package/exceptions-0.10.7/src/LICENSE) | Extensible optionally-pure exceptions | `aeson`, `conduit`, `filepath`, `monad-logger`, `resourcet`, `safe-exceptions`, `sqlcipher-simple`, `temporary`, `terminal`, `zip` | +| **`filepath`** | [`1.4.100.4`](http://hackage.haskell.org/package/filepath-1.4.100.4) | [`BSD-3-Clause`](http://hackage.haskell.org/package/filepath-1.4.100.4/src/LICENSE) | Library for manipulating FilePaths in a cross platform way. | `conduit`, `conduit-extra`, `crypton-x509-store`, `directory`, `easy-file`, `fast-logger`, `file-embed`, `hashable`, `process`, `simplexmq`, `temporary`, `unix`, `unliftio`, `yaml`, `zip` | +| `file-embed` | [`0.0.15.0`](http://hackage.haskell.org/package/file-embed-0.0.15.0) | [`BSD-2-Clause`](./licences/haskell/file-embed-0.0.15.0/LICENSE) | Use Template Haskell to embed file contents directly. | | +| `http-types` | [`0.12.4`](http://hackage.haskell.org/package/http-types-0.12.4) | [`BSD-3-Clause`](./licences/haskell/http-types-0.12.4/LICENSE) | Generic HTTP types for Haskell (for both client and server code). | `http2`, `simplexmq` | +| `http2` | [`5.0.0`](http://hackage.haskell.org/package/http2-5.0.0) | [`BSD-3-Clause`](./licences/haskell/http2-5.0.0/LICENSE) | HTTP/2 library | `simplexmq` | +| `email-validate` | [`2.3.2.19`](http://hackage.haskell.org/package/email-validate-2.3.2.19) | [`BSD-3-Clause`](./licences/haskell/email-validate-2.3.2.19/LICENSE) | Email address validation | | +| `memory` | [`0.18.0`](http://hackage.haskell.org/package/memory-0.18.0) | [`BSD-3-Clause`](./licences/haskell/memory-0.18.0/LICENSE) | memory and related abstraction stuff | `asn1-types`, `crypton`, `crypton-x509`, `crypton-x509-validation`, `cryptostore`, `pem`, `simplexmq`, `tls` | +| **`mtl`** | [`2.3.1`](http://hackage.haskell.org/package/mtl-2.3.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/mtl-2.3.1/src/LICENSE) | Monad classes for transformers, using functional dependencies | `conduit`, `constraints`, `crypton-x509-store`, `crypton-x509-validation`, `exceptions`, `monad-logger`, `parsec`, `random`, `resourcet`, `simple-logger`, `simplexmq`, `tls`, `yaml`, `zip` | +| `unliftio` | [`0.2.25.0`](http://hackage.haskell.org/package/unliftio-0.2.25.0) | [`MIT`](./licences/haskell/unliftio-0.2.25.0/LICENSE) | The MonadUnliftIO typeclass for unlifting monads to IO (batteries included) | `http2`, `simplexmq`, `time-manager` | +| `unliftio-core` | [`0.2.1.0`](http://hackage.haskell.org/package/unliftio-core-0.2.1.0) | [`MIT`](./licences/haskell/unliftio-core-0.2.1.0/LICENSE) | The MonadUnliftIO typeclass for unlifting monads to IO | `conduit`, `conduit-extra`, `monad-logger`, `resourcet`, `simplexmq`, `typed-process`, `unliftio` | +| `ansi-terminal` | [`1.0`](http://hackage.haskell.org/package/ansi-terminal-1.0) | [`BSD-3-Clause`](./licences/haskell/ansi-terminal-1.0/LICENSE) | Simple ANSI terminal support | `prettyprinter-ansi-terminal` | +| `network` | [`3.1.4.0`](http://hackage.haskell.org/package/network-3.1.4.0) | [`BSD-3-Clause`](./licences/haskell/network-3.1.4.0/LICENSE) | Low-level networking interface | `conduit-extra`, `http2`, `iproute`, `network-udp`, `simplexmq`, `socks`, `streaming-commons`, `tls`, `websockets` | +| `network-transport` | [`0.5.6`](http://hackage.haskell.org/package/network-transport-0.5.6) | [`BSD-3-Clause`](./licences/haskell/network-transport-0.5.6/LICENSE) | Network abstraction layer | `simplexmq` | +| **`process`** | [`1.6.17.0`](http://hackage.haskell.org/package/process-1.6.17.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/process-1.6.17.0/src/LICENSE) | Process libraries | `conduit-extra`, `optparse-applicative`, `simplexmq`, `streaming-commons`, `typed-process`, `unliftio` | +| `optparse-applicative` | [`0.18.1.0`](http://hackage.haskell.org/package/optparse-applicative-0.18.1.0) | [`BSD-3-Clause`](./licences/haskell/optparse-applicative-0.18.1.0/LICENSE) | Utilities and combinators for parsing command line options | `simplexmq` | +| `record-hasfield` | [`1.0`](http://hackage.haskell.org/package/record-hasfield-1.0) | [`BSD-3-Clause`](./licences/haskell/record-hasfield-1.0/LICENSE) | A version of GHC.Records as available in future GHCs. | | +| `random` | [`1.2.1.1`](http://hackage.haskell.org/package/random-1.2.1.1) | [`BSD-3-Clause`](./licences/haskell/random-1.2.1.1/LICENSE) | Pseudo-random number generation | `QuickCheck`, `simplexmq`, `streaming-commons`, `temporary`, `uuid`, `uuid-types`, `websockets` | +| `socks` | [`0.6.1`](http://hackage.haskell.org/package/socks-0.6.1) | [`BSD-3-Clause`](./licences/haskell/socks-0.6.1/LICENSE) | Socks proxy (ver 5) | `simplexmq` | +| `scientific` | [`0.3.7.0`](http://hackage.haskell.org/package/scientific-0.3.7.0) | [`BSD-3-Clause`](./licences/haskell/scientific-0.3.7.0/LICENSE) | Numbers represented using scientific notation | `aeson`, `attoparsec`, `yaml` | +| `simple-logger` | [`0.1.1`](http://hackage.haskell.org/package/simple-logger-0.1.1) | [`MIT`](./licences/haskell/simple-logger-0.1.1/LICENSE) | A very simple but efficient logging framework | `simplexmq` | +| `simplexmq` | [`6.4.0.1`](http://hackage.haskell.org/package/simplexmq-6.4.0.1) | *MISSING* | *MISSING* | | +| `aeson` | [`2.2.1.0`](http://hackage.haskell.org/package/aeson-2.2.1.0) | [`BSD-3-Clause`](./licences/haskell/aeson-2.2.1.0/LICENSE) | Fast JSON parsing and encoding | `simplexmq`, `yaml` | +| `sqlcipher-simple` | [`0.4.18.1`](http://hackage.haskell.org/package/sqlcipher-simple-0.4.18.1) | *MISSING* | *MISSING* | `simplexmq` | +| **`stm`** | [`2.5.1.0`](http://hackage.haskell.org/package/stm-2.5.1.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/stm-2.5.1.0/src/LICENSE) | Software Transactional Memory | `StateVar`, `async`, `conduit-extra`, `exceptions`, `fast-logger`, `http2`, `monad-control`, `monad-logger`, `simplexmq`, `stm-chans`, `streaming-commons`, `terminal`, `transformers-base`, `typed-process`, `unliftio` | +| `async` | [`2.2.5`](http://hackage.haskell.org/package/async-2.2.5) | [`BSD-3-Clause`](./licences/haskell/async-2.2.5/LICENSE) | Run IO operations asynchronously and wait for their results | `conduit-extra`, `http2`, `simplexmq`, `streaming-commons`, `terminal`, `tls`, `typed-process`, `unliftio`, `websockets` | +| **`template-haskell`** | [`2.20.0.0`](http://hackage.haskell.org/package/template-haskell-2.20.0.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/template-haskell-2.20.0.0/src/LICENSE) | Support library for Template Haskell | `OneTuple`, `QuickCheck`, `aeson`, `bifunctors`, `bytestring`, `containers`, `email-validate`, `exceptions`, `file-embed`, `filepath`, `monad-logger`, `network-uri`, `primitive`, `scientific`, `semigroupoids`, `sqlcipher-simple`, `tagged`, `text`, `text-short`, `th-abstraction`, `th-compat`, `unordered-containers`, `uuid-types`, `yaml` | +| **`text`** | [`2.0.2`](http://hackage.haskell.org/package/text-2.0.2) | [`BSD-2-Clause`](http://hackage.haskell.org/package/text-2.0.2/src/LICENSE) | An efficient packed Unicode text type. | `aeson`, `attoparsec`, `blaze-builder`, `blaze-textual`, `case-insensitive`, `conduit`, `conduit-extra`, `direct-sqlcipher`, `fast-logger`, `hashable`, `http-types`, `ini`, `integer-conversion`, `monad-logger`, `mono-traversable`, `optparse-applicative`, `parsec`, `prettyprinter`, `prettyprinter-ansi-terminal`, `scientific`, `simple-logger`, `simplexmq`, `sqlcipher-simple`, `streaming-commons`, `strict`, `terminal`, `text-iso8601`, `text-short`, `uuid`, `uuid-types`, `websockets`, `yaml`, `zip` | +| **`time`** | [`1.12.2`](http://hackage.haskell.org/package/time-1.12.2) | [`BSD-2-Clause`](http://hackage.haskell.org/package/time-1.12.2/src/LICENSE) | A time library | `aeson`, `directory`, `easy-file`, `iso8601-time`, `simplexmq`, `sqlcipher-simple`, `text-iso8601`, `time-compat`, `unix`, `unliftio`, `uuid`, `zip` | +| `tls` | [`1.9.0`](http://hackage.haskell.org/package/tls-1.9.0) | [`BSD-3-Clause`](./licences/haskell/tls-1.9.0/LICENSE) | TLS/SSL protocol native implementation (Server and Client) | `simplexmq` | +| `terminal` | [`0.2.0.0`](http://hackage.haskell.org/package/terminal-0.2.0.0) | [`BSD-3-Clause`](./licences/haskell/terminal-0.2.0.0/LICENSE) | Portable terminal interaction library | | +| `attoparsec` | [`0.14.4`](http://hackage.haskell.org/package/attoparsec-0.14.4) | [`BSD-3-Clause`](./licences/haskell/attoparsec-0.14.4/LICENSE) | Fast combinator parsing for bytestrings and text | `conduit-extra`, `email-validate`, `ini`, `simplexmq`, `sqlcipher-simple`, `websockets`, `yaml` | +| `websockets` | [`0.12.7.3`](http://hackage.haskell.org/package/websockets-0.12.7.3) | [`BSD-3-Clause`](./licences/haskell/websockets-0.12.7.3/LICENCE) | A sensible and clean way to write WebSocket-capable servers in Haskell. | `simplexmq` | +| `zip` | [`2.0.0`](http://hackage.haskell.org/package/zip-2.0.0) | [`BSD-3-Clause`](./licences/haskell/zip-2.0.0/LICENSE.md) | Operations on zip archives | | + +## Indirect transitive dependencies + +| Name | Version | [SPDX](https://spdx.org/licenses/) License Id | Description | Depended upon by | +| --- | --- | --- | --- | --- | +| `OneTuple` | [`0.4.1.1`](http://hackage.haskell.org/package/OneTuple-0.4.1.1) | [`BSD-3-Clause`](./licences/haskell/OneTuple-0.4.1.1/LICENSE) | Singleton Tuple | `aeson`, `indexed-traversable-instances` | +| `Only` | [`0.1`](http://hackage.haskell.org/package/Only-0.1) | [`BSD-3-Clause`](./licences/haskell/Only-0.1/LICENSE) | The 1-tuple type or single-value "collection" | `sqlcipher-simple` | +| `QuickCheck` | [`2.14.3`](http://hackage.haskell.org/package/QuickCheck-2.14.3) | [`BSD-3-Clause`](./licences/haskell/QuickCheck-2.14.3/LICENSE) | Automatic testing of Haskell programs | `aeson` | +| `SHA` | [`1.6.4.4`](http://hackage.haskell.org/package/SHA-1.6.4.4) | [`BSD-3-Clause`](./licences/haskell/SHA-1.6.4.4/LICENSE) | Implementations of the SHA suite of message digest functions | `websockets` | +| `StateVar` | [`1.2.2`](http://hackage.haskell.org/package/StateVar-1.2.2) | [`BSD-3-Clause`](./licences/haskell/StateVar-1.2.2/LICENSE) | State variables | `contravariant` | +| **`array`** | [`0.5.5.0`](http://hackage.haskell.org/package/array-0.5.5.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/array-0.5.5.0/src/LICENSE) | Mutable and immutable arrays | `SHA`, `attoparsec`, `binary`, `cereal`, `containers`, `deepseq`, `fast-logger`, `http-types`, `http2`, `indexed-traversable`, `integer-logarithms`, `stm`, `streaming-commons`, `text` | +| `bifunctors` | [`5.6.1`](http://hackage.haskell.org/package/bifunctors-5.6.1) | [`BSD-3-Clause`](./licences/haskell/bifunctors-5.6.1/LICENSE) | Bifunctors | `semigroupoids` | +| **`binary`** | [`0.8.9.1`](http://hackage.haskell.org/package/binary-0.8.9.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/binary-0.8.9.1/src/LICENSE) | Binary serialisation for Haskell values using lazy ByteStrings | `SHA`, `constraints`, `network-transport`, `scientific`, `strict`, `text`, `text-short`, `these`, `unix-time`, `uuid`, `uuid-types`, `websockets` | +| `blaze-builder` | [`0.4.2.3`](http://hackage.haskell.org/package/blaze-builder-0.4.2.3) | [`BSD-3-Clause`](./licences/haskell/blaze-builder-0.4.2.3/LICENSE) | Efficient buffered output. | `blaze-textual`, `sqlcipher-simple` | +| `blaze-textual` | [`0.2.3.1`](http://hackage.haskell.org/package/blaze-textual-0.2.3.1) | [`BSD-3-Clause`](./licences/haskell/blaze-textual-0.2.3.1/LICENSE) | Fast rendering of common datatypes | `sqlcipher-simple` | +| `boring` | [`0.2.1`](http://hackage.haskell.org/package/boring-0.2.1) | [`BSD-3-Clause`](./licences/haskell/boring-0.2.1/LICENSE) | Boring and Absurd types | `constraints` | +| `base-orphans` | [`0.9.1`](http://hackage.haskell.org/package/base-orphans-0.9.1) | [`MIT`](./licences/haskell/base-orphans-0.9.1/LICENSE) | Backwards-compatible orphan instances for base | `distributive`, `semigroupoids`, `time-compat`, `transformers-base`, `witherable` | +| `basement` | [`0.0.16`](http://hackage.haskell.org/package/basement-0.0.16) | [`BSD-3-Clause`](./licences/haskell/basement-0.0.16/LICENSE) | Foundation scrap box of array & string | `crypton`, `cryptostore`, `memory`, `pem`, `socks` | +| `bitvec` | [`1.1.5.0`](http://hackage.haskell.org/package/bitvec-1.1.5.0) | [`BSD-3-Clause`](./licences/haskell/bitvec-1.1.5.0/LICENSE) | Space-efficient bit vectors | `vector-algorithms` | +| `byteorder` | [`1.0.4`](http://hackage.haskell.org/package/byteorder-1.0.4) | [`BSD-3-Clause`](./licences/haskell/byteorder-1.0.4/LICENSE) | Exposes the native endianness or byte ordering of the system. | `iproute` | +| `bytestring-builder` | [`0.10.8.2.0`](http://hackage.haskell.org/package/bytestring-builder-0.10.8.2.0) | [`BSD-3-Clause`](./licences/haskell/bytestring-builder-0.10.8.2.0/LICENSE) | The new bytestring builder, packaged outside of GHC | `websockets` | +| `clock` | [`0.8.4`](http://hackage.haskell.org/package/clock-0.8.4) | [`BSD-3-Clause`](./licences/haskell/clock-0.8.4/LICENSE) | High-resolution clock functions: monotonic, realtime, cputime. | `websockets` | +| `colour` | [`2.3.6`](http://hackage.haskell.org/package/colour-2.3.6) | [`MIT`](./licences/haskell/colour-2.3.6/LICENSE) | A model for human colour/color perception | `ansi-terminal`, `ansi-terminal-types` | +| `comonad` | [`5.0.8`](http://hackage.haskell.org/package/comonad-5.0.8) | [`BSD-3-Clause`](./licences/haskell/comonad-5.0.8/LICENSE) | Comonads | `bifunctors`, `semigroupoids` | +| `conduit` | [`1.3.5`](http://hackage.haskell.org/package/conduit-1.3.5) | [`MIT`](./licences/haskell/conduit-1.3.5/LICENSE) | Streaming data processing library. | `conduit-extra`, `libyaml`, `monad-logger`, `yaml`, `zip` | +| `conduit-extra` | [`1.3.6`](http://hackage.haskell.org/package/conduit-extra-1.3.6) | [`MIT`](./licences/haskell/conduit-extra-1.3.6/LICENSE) | Batteries included conduit: adapters for common libraries. | `monad-logger`, `zip` | +| `contravariant` | [`1.5.5`](http://hackage.haskell.org/package/contravariant-1.5.5) | [`BSD-3-Clause`](./licences/haskell/contravariant-1.5.5/LICENSE) | Contravariant functors | `semigroupoids` | +| `cereal` | [`0.5.8.3`](http://hackage.haskell.org/package/cereal-0.5.8.3) | [`BSD-3-Clause`](./licences/haskell/cereal-0.5.8.3/LICENSE) | A binary serialization library | `socks`, `tls`, `zip` | +| `cryptohash-md5` | [`0.11.101.0`](http://hackage.haskell.org/package/cryptohash-md5-0.11.101.0) | [`BSD-3-Clause`](./licences/haskell/cryptohash-md5-0.11.101.0/LICENSE) | Fast, pure and practical MD5 implementation | `uuid` | +| `cryptohash-sha1` | [`0.11.101.0`](http://hackage.haskell.org/package/cryptohash-sha1-0.11.101.0) | [`BSD-3-Clause`](./licences/haskell/cryptohash-sha1-0.11.101.0/LICENSE) | Fast, pure and practical SHA-1 implementation | `uuid` | +| `crypton-x509` | [`1.7.6`](http://hackage.haskell.org/package/crypton-x509-1.7.6) | [`BSD-3-Clause`](./licences/haskell/crypton-x509-1.7.6/LICENSE) | X509 reader and writer | `crypton-x509-store`, `crypton-x509-validation`, `cryptostore`, `simplexmq`, `tls` | +| `crypton-x509-store` | [`1.6.9`](http://hackage.haskell.org/package/crypton-x509-store-1.6.9) | [`BSD-3-Clause`](./licences/haskell/crypton-x509-store-1.6.9/LICENSE) | X.509 collection accessing and storing methods | `crypton-x509-validation`, `simplexmq`, `tls` | +| `crypton-x509-validation` | [`1.6.12`](http://hackage.haskell.org/package/crypton-x509-validation-1.6.12) | [`BSD-3-Clause`](./licences/haskell/crypton-x509-validation-1.6.12/LICENSE) | X.509 Certificate and CRL validation | `cryptostore`, `simplexmq`, `tls` | +| `cryptostore` | [`0.3.0.1`](http://hackage.haskell.org/package/cryptostore-0.3.0.1) | [`BSD-3-Clause`](./licences/haskell/cryptostore-0.3.0.1/LICENSE) | Serialization of cryptographic data types | `simplexmq` | +| `case-insensitive` | [`1.2.1.0`](http://hackage.haskell.org/package/case-insensitive-1.2.1.0) | [`BSD-3-Clause`](./licences/haskell/case-insensitive-1.2.1.0/LICENSE) | Case insensitive string comparison | `http-types`, `http2`, `simplexmq`, `websockets`, `zip` | +| `uuid-types` | [`1.0.5.1`](http://hackage.haskell.org/package/uuid-types-1.0.5.1) | [`BSD-3-Clause`](./licences/haskell/uuid-types-1.0.5.1/LICENSE) | Type definitions for Universally Unique Identifiers | `aeson`, `uuid` | +| **`deepseq`** | [`1.4.8.1`](http://hackage.haskell.org/package/deepseq-1.4.8.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/deepseq-1.4.8.1/src/LICENSE) | Deep evaluation of data structures | `Only`, `QuickCheck`, `aeson`, `attoparsec`, `bitvec`, `blaze-builder`, `bytestring`, `bytestring-builder`, `case-insensitive`, `constraints`, `containers`, `crypton`, `data-fix`, `dlist`, `filepath`, `hashable`, `hourglass`, `memory`, `network`, `network-transport`, `network-uri`, `pretty`, `primitive`, `process`, `psqueues`, `random`, `safe-exceptions`, `scientific`, `splitmix`, `strict`, `tagged`, `text`, `text-short`, `these`, `time`, `time-compat`, `unliftio`, `unordered-containers`, `uuid-types`, `vector`, `zstd` | +| `digest` | [`0.0.1.7`](http://hackage.haskell.org/package/digest-0.0.1.7) | [`BSD-2-Clause`](./licences/haskell/digest-0.0.1.7/LICENSE) | Various hashes for bytestrings; CRC32 and Adler32 for now. | `zip` | +| `dlist` | [`1.0`](http://hackage.haskell.org/package/dlist-1.0) | [`BSD-3-Clause`](./licences/haskell/dlist-1.0/license.md) | Difference lists | `aeson`, `data-default-instances-dlist`, `zip` | +| `distributive` | [`0.6.2.1`](http://hackage.haskell.org/package/distributive-0.6.2.1) | [`BSD-3-Clause`](./licences/haskell/distributive-0.6.2.1/LICENSE) | Distributive functors -- Dual to Traversable | `comonad`, `semigroupoids` | +| `data-default-class` | [`0.1.2.0`](http://hackage.haskell.org/package/data-default-class-0.1.2.0) | [`BSD-3-Clause`](./licences/haskell/data-default-class-0.1.2.0/LICENSE) | A class for types with a default value | `crypton-x509-validation`, `data-default`, `data-default-instances-containers`, `data-default-instances-dlist`, `data-default-instances-old-locale`, `tls` | +| `data-default-instances-containers` | [`0.0.1`](http://hackage.haskell.org/package/data-default-instances-containers-0.0.1) | [`BSD-3-Clause`](./licences/haskell/data-default-instances-containers-0.0.1/LICENSE) | Default instances for types in containers | `data-default` | +| `data-default-instances-dlist` | [`0.0.1`](http://hackage.haskell.org/package/data-default-instances-dlist-0.0.1) | [`BSD-3-Clause`](./licences/haskell/data-default-instances-dlist-0.0.1/LICENSE) | Default instances for types in dlist | `data-default` | +| `data-default-instances-old-locale` | [`0.0.1`](http://hackage.haskell.org/package/data-default-instances-old-locale-0.0.1) | [`BSD-3-Clause`](./licences/haskell/data-default-instances-old-locale-0.0.1/LICENSE) | Default instances for types in old-locale | `data-default` | +| `data-fix` | [`0.3.2`](http://hackage.haskell.org/package/data-fix-0.3.2) | [`BSD-3-Clause`](./licences/haskell/data-fix-0.3.2/LICENSE) | Fixpoint data types | `aeson` | +| `fast-logger` | [`3.2.2`](http://hackage.haskell.org/package/fast-logger-3.2.2) | [`BSD-3-Clause`](./licences/haskell/fast-logger-3.2.2/LICENSE) | A fast logging system | `monad-logger`, `simple-logger` | +| **`ghc-bignum`** | [`1.3`](http://hackage.haskell.org/package/ghc-bignum-1.3) | [`BSD-3-Clause`](http://hackage.haskell.org/package/ghc-bignum-1.3/src/LICENSE) | GHC BigNum library | `base`, `bitvec`, `hashable`, `integer-gmp`, `integer-logarithms` | +| **`ghc-boot-th`** | [`9.6.3`](http://hackage.haskell.org/package/ghc-boot-th-9.6.3) | [`BSD-3-Clause`](http://hackage.haskell.org/package/ghc-boot-th-9.6.3/src/LICENSE) | Shared functionality between GHC and the @template-haskell@ library | `template-haskell` | +| **`ghc-prim`** | [`0.10.0`](http://hackage.haskell.org/package/ghc-prim-0.10.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/ghc-prim-0.10.0/src/LICENSE) | GHC primitives | *(core library)* | +| `generically` | [`0.1.1`](http://hackage.haskell.org/package/generically-0.1.1) | [`BSD-3-Clause`](./licences/haskell/generically-0.1.1/LICENSE) | Generically newtype to use with DerivingVia | `aeson` | +| `hourglass` | [`0.2.12`](http://hackage.haskell.org/package/hourglass-0.2.12) | [`BSD-3-Clause`](./licences/haskell/hourglass-0.2.12/LICENSE) | simple performant time related library | `asn1-encoding`, `asn1-types`, `crypton-x509`, `crypton-x509-validation`, `cryptostore`, `simplexmq` | +| `hashable` | [`1.4.3.0`](http://hackage.haskell.org/package/hashable-1.4.3.0) | [`BSD-3-Clause`](./licences/haskell/hashable-1.4.3.0/LICENSE) | A class for types that can be converted to a hash value | `aeson`, `async`, `case-insensitive`, `constraints`, `data-fix`, `mono-traversable`, `network-transport`, `psqueues`, `scientific`, `semialign`, `semigroupoids`, `simplexmq`, `strict`, `text-short`, `these`, `time-compat`, `unordered-containers`, `uuid-types`, `witherable` | +| **`integer-gmp`** | [`1.1`](http://hackage.haskell.org/package/integer-gmp-1.1) | [`BSD-3-Clause`](http://hackage.haskell.org/package/integer-gmp-1.1/src/LICENSE) | Integer library based on GMP | *(core library)* | +| `libyaml` | [`0.1.2`](http://hackage.haskell.org/package/libyaml-0.1.2) | [`BSD-3-Clause`](./licences/haskell/libyaml-0.1.2/LICENSE) | Low-level, streaming YAML interface. | `yaml` | +| `old-locale` | [`1.0.0.7`](http://hackage.haskell.org/package/old-locale-1.0.0.7) | [`BSD-3-Clause`](./licences/haskell/old-locale-1.0.0.7/LICENSE) | locale library | `blaze-textual`, `data-default-instances-old-locale`, `old-time` | +| `old-time` | [`1.1.0.4`](http://hackage.haskell.org/package/old-time-1.1.0.4) | [`BSD-3-Clause`](./licences/haskell/old-time-1.1.0.4/LICENSE) | Time library | `unix-time` | +| `lifted-base` | [`0.2.3.12`](http://hackage.haskell.org/package/lifted-base-0.2.3.12) | [`BSD-3-Clause`](./licences/haskell/lifted-base-0.2.3.12/LICENSE) | lifted IO operations from the base library | `monad-logger` | +| `mono-traversable` | [`1.0.15.3`](http://hackage.haskell.org/package/mono-traversable-1.0.15.3) | [`MIT`](./licences/haskell/mono-traversable-1.0.15.3/LICENSE) | Type classes for mapping, folding, and traversing monomorphic containers | `conduit` | +| `monad-control` | [`1.0.3.1`](http://hackage.haskell.org/package/monad-control-1.0.3.1) | [`BSD-3-Clause`](./licences/haskell/monad-control-1.0.3.1/LICENSE) | Lift control operations, like exception catching, through monad transformers | `lifted-base`, `monad-logger`, `zip` | +| `monad-logger` | [`0.3.40`](http://hackage.haskell.org/package/monad-logger-0.3.40) | [`MIT`](./licences/haskell/monad-logger-0.3.40/LICENSE) | A class of monads which can log messages. | `simple-logger` | +| `monad-loops` | [`0.4.3`](http://hackage.haskell.org/package/monad-loops-0.4.3) | [`LicenseRef-PublicDomain`](http://hackage.haskell.org/package/monad-loops-0.4.3) | Monadic loops | `monad-logger` | +| `ini` | [`0.4.2`](http://hackage.haskell.org/package/ini-0.4.2) | [`BSD-3-Clause`](./licences/haskell/ini-0.4.2/LICENSE) | Configuration files in the INI format. | `simplexmq` | +| `indexed-traversable` | [`0.1.3`](http://hackage.haskell.org/package/indexed-traversable-0.1.3) | [`BSD-2-Clause`](./licences/haskell/indexed-traversable-0.1.3/LICENSE) | FunctorWithIndex, FoldableWithIndex, TraversableWithIndex | `aeson`, `comonad`, `indexed-traversable-instances`, `semialign`, `witherable` | +| `indexed-traversable-instances` | [`0.1.1.2`](http://hackage.haskell.org/package/indexed-traversable-instances-0.1.1.2) | [`BSD-2-Clause`](./licences/haskell/indexed-traversable-instances-0.1.1.2/LICENSE) | More instances of FunctorWithIndex, FoldableWithIndex, TraversableWithIndex | `semialign`, `witherable` | +| `unordered-containers` | [`0.2.19.1`](http://hackage.haskell.org/package/unordered-containers-0.2.19.1) | [`BSD-3-Clause`](./licences/haskell/unordered-containers-0.2.19.1/LICENSE) | Efficient hashing-based container types | `aeson`, `indexed-traversable-instances`, `ini`, `mono-traversable`, `semialign`, `semigroupoids`, `witherable`, `yaml` | +| `ansi-terminal-types` | [`0.11.5`](http://hackage.haskell.org/package/ansi-terminal-types-0.11.5) | [`BSD-3-Clause`](./licences/haskell/ansi-terminal-types-0.11.5/LICENSE) | Types and functions used to represent SGR aspects | `ansi-terminal` | +| `integer-conversion` | [`0.1.0.1`](http://hackage.haskell.org/package/integer-conversion-0.1.0.1) | [`BSD-3-Clause`](./licences/haskell/integer-conversion-0.1.0.1/LICENSE) | Conversion from strings to Integer | `aeson`, `text-iso8601` | +| `integer-logarithms` | [`1.0.3.1`](http://hackage.haskell.org/package/integer-logarithms-1.0.3.1) | [`MIT`](./licences/haskell/integer-logarithms-1.0.3.1/LICENSE) | Integer logarithms. | `aeson`, `scientific` | +| `entropy` | [`0.4.1.10`](http://hackage.haskell.org/package/entropy-0.4.1.10) | [`BSD-3-Clause`](./licences/haskell/entropy-0.4.1.10/LICENSE) | A platform independent entropy source | `uuid`, `websockets` | +| `network-byte-order` | [`0.1.7`](http://hackage.haskell.org/package/network-byte-order-0.1.7) | [`BSD-3-Clause`](./licences/haskell/network-byte-order-0.1.7/LICENSE) | Network byte order utilities | `http2` | +| `network-control` | [`0.0.2`](http://hackage.haskell.org/package/network-control-0.0.2) | [`BSD-3-Clause`](./licences/haskell/network-control-0.0.2/LICENSE) | Library to control network protocols | `http2` | +| `network-udp` | [`0.0.0`](http://hackage.haskell.org/package/network-udp-0.0.0) | [`BSD-3-Clause`](./licences/haskell/network-udp-0.0.0/LICENSE) | UDP library | `simplexmq` | +| `network-info` | [`0.2.1`](http://hackage.haskell.org/package/network-info-0.2.1) | [`BSD-3-Clause`](./licences/haskell/network-info-0.2.1/LICENSE) | Access the local computer's basic network configuration | `simplexmq`, `uuid` | +| `network-uri` | [`2.6.4.2`](http://hackage.haskell.org/package/network-uri-2.6.4.2) | [`BSD-3-Clause`](./licences/haskell/network-uri-2.6.4.2/LICENSE) | URI manipulation | `aeson` | +| `unix-compat` | [`0.7.1`](http://hackage.haskell.org/package/unix-compat-0.7.1) | [`BSD-3-Clause`](./licences/haskell/unix-compat-0.7.1/LICENSE) | Portable POSIX-compatibility layer. | `fast-logger` | +| `unix-time` | [`0.4.11`](http://hackage.haskell.org/package/unix-time-0.4.11) | [`BSD-3-Clause`](./licences/haskell/unix-time-0.4.11/LICENSE) | Unix time parser/formatter and utilities | `fast-logger`, `http2`, `network-control`, `tls` | +| **`parsec`** | [`3.1.16.1`](http://hackage.haskell.org/package/parsec-3.1.16.1) | [`BSD-2-Clause`](http://hackage.haskell.org/package/parsec-3.1.16.1/src/LICENSE) | Monadic parser combinators | `network-uri` | +| `pem` | [`0.2.4`](http://hackage.haskell.org/package/pem-0.2.4) | [`BSD-3-Clause`](./licences/haskell/pem-0.2.4/LICENSE) | Privacy Enhanced Mail (PEM) format reader and writer. | `crypton-x509`, `crypton-x509-store`, `crypton-x509-validation`, `cryptostore` | +| `appar` | [`0.1.8`](http://hackage.haskell.org/package/appar-0.1.8) | [`BSD-3-Clause`](./licences/haskell/appar-0.1.8/LICENSE) | A simple applicative parser | `iproute` | +| **`pretty`** | [`1.1.3.6`](http://hackage.haskell.org/package/pretty-1.1.3.6) | [`BSD-3-Clause`](http://hackage.haskell.org/package/pretty-1.1.3.6/src/LICENSE) | Pretty-printing library | `template-haskell` | +| `primitive` | [`0.9.0.0`](http://hackage.haskell.org/package/primitive-0.9.0.0) | [`BSD-3-Clause`](./licences/haskell/primitive-0.9.0.0/LICENSE) | Primitive memory-related operations | `aeson`, `bitvec`, `conduit`, `conduit-extra`, `integer-conversion`, `resourcet`, `scientific`, `vector`, `vector-algorithms` | +| `iproute` | [`1.7.12`](http://hackage.haskell.org/package/iproute-1.7.12) | [`BSD-3-Clause`](./licences/haskell/iproute-1.7.12/LICENSE) | IP Routing Table | `network-udp`, `simplexmq` | +| `prettyprinter` | [`1.7.1`](http://hackage.haskell.org/package/prettyprinter-1.7.1) | [`BSD-2-Clause`](./licences/haskell/prettyprinter-1.7.1/LICENSE.md) | A modern, easy to use, well-documented, extensible pretty-printer. | `optparse-applicative`, `prettyprinter-ansi-terminal`, `terminal` | +| `prettyprinter-ansi-terminal` | [`1.1.3`](http://hackage.haskell.org/package/prettyprinter-ansi-terminal-1.1.3) | [`BSD-2-Clause`](./licences/haskell/prettyprinter-ansi-terminal-1.1.3/LICENSE.md) | ANSI terminal backend for the »prettyprinter« package. | `optparse-applicative` | +| `psqueues` | [`0.2.8.0`](http://hackage.haskell.org/package/psqueues-0.2.8.0) | [`BSD-3-Clause`](./licences/haskell/psqueues-0.2.8.0/LICENSE) | Pure priority search queues | `network-control` | +| `resourcet` | [`1.3.0`](http://hackage.haskell.org/package/resourcet-1.3.0) | [`BSD-3-Clause`](./licences/haskell/resourcet-1.3.0/LICENSE) | Deterministic allocation and freeing of scarce resources. | `conduit`, `conduit-extra`, `libyaml`, `monad-logger`, `yaml`, `zip` | +| `iso8601-time` | [`0.1.5`](http://hackage.haskell.org/package/iso8601-time-0.1.5) | [`MIT`](http://hackage.haskell.org/package/iso8601-time-0.1.5) | Convert to/from the ISO 8601 time format | `simplexmq` | +| `safe-exceptions` | [`0.1.7.4`](http://hackage.haskell.org/package/safe-exceptions-0.1.7.4) | [`MIT`](./licences/haskell/safe-exceptions-0.1.7.4/LICENSE) | Safe, consistent, and easy exception handling | `unliftio` | +| `semigroupoids` | [`6.0.0.1`](http://hackage.haskell.org/package/semigroupoids-6.0.0.1) | [`BSD-2-Clause`](./licences/haskell/semigroupoids-6.0.0.1/LICENSE) | Semigroupoids: Category sans id | `semialign` | +| `semialign` | [`1.3`](http://hackage.haskell.org/package/semialign-1.3) | [`BSD-3-Clause`](./licences/haskell/semialign-1.3/LICENSE) | Align and Zip type-classes from the common Semialign ancestor. | `aeson` | +| `asn1-encoding` | [`0.9.6`](http://hackage.haskell.org/package/asn1-encoding-0.9.6) | [`BSD-3-Clause`](./licences/haskell/asn1-encoding-0.9.6/LICENSE) | ASN1 data reader and writer in RAW, BER and DER forms | `asn1-parse`, `crypton-x509`, `crypton-x509-store`, `crypton-x509-validation`, `cryptostore`, `simplexmq`, `tls` | +| `asn1-parse` | [`0.9.5`](http://hackage.haskell.org/package/asn1-parse-0.9.5) | [`BSD-3-Clause`](./licences/haskell/asn1-parse-0.9.5/LICENSE) | Simple monadic parser for ASN1 stream types. | `crypton-x509` | +| `asn1-types` | [`0.3.4`](http://hackage.haskell.org/package/asn1-types-0.3.4) | [`BSD-3-Clause`](./licences/haskell/asn1-types-0.3.4/LICENSE) | ASN.1 types | `asn1-encoding`, `asn1-parse`, `crypton-x509`, `crypton-x509-store`, `crypton-x509-validation`, `cryptostore`, `simplexmq`, `tls` | +| `split` | [`0.2.4`](http://hackage.haskell.org/package/split-0.2.4) | [`BSD-3-Clause`](./licences/haskell/split-0.2.4/LICENSE) | Combinator library for splitting lists. | `mono-traversable` | +| `splitmix` | [`0.1.0.5`](http://hackage.haskell.org/package/splitmix-0.1.0.5) | [`BSD-3-Clause`](./licences/haskell/splitmix-0.1.0.5/LICENSE) | Fast Splittable PRNG | `QuickCheck`, `random` | +| `assoc` | [`1.1`](http://hackage.haskell.org/package/assoc-1.1) | [`BSD-3-Clause`](./licences/haskell/assoc-1.1/LICENSE) | swap and assoc: Symmetric and Semigroupy Bifunctors | `bifunctors`, `strict`, `these` | +| `stm-chans` | [`3.0.0.9`](http://hackage.haskell.org/package/stm-chans-3.0.0.9) | [`BSD-3-Clause`](./licences/haskell/stm-chans-3.0.0.9/LICENSE) | Additional types of channels for STM. | `monad-logger` | +| `strict` | [`0.5`](http://hackage.haskell.org/package/strict-0.5) | [`BSD-3-Clause`](./licences/haskell/strict-0.5/LICENSE) | Strict data types and String IO. | `aeson` | +| `streaming-commons` | [`0.2.2.6`](http://hackage.haskell.org/package/streaming-commons-0.2.2.6) | [`MIT`](./licences/haskell/streaming-commons-0.2.2.6/LICENSE) | Common lower-level functions needed by various streaming data libraries | `conduit-extra`, `websockets` | +| `easy-file` | [`0.2.5`](http://hackage.haskell.org/package/easy-file-0.2.5) | [`BSD-3-Clause`](./licences/haskell/easy-file-0.2.5/LICENSE) | Cross-platform File handling | `fast-logger` | +| `auto-update` | [`0.1.6`](http://hackage.haskell.org/package/auto-update-0.1.6) | [`MIT`](./licences/haskell/auto-update-0.1.6/LICENSE) | Efficiently run periodic, on-demand actions | `fast-logger`, `time-manager` | +| `tagged` | [`0.8.8`](http://hackage.haskell.org/package/tagged-0.8.8) | [`BSD-3-Clause`](./licences/haskell/tagged-0.8.8/LICENSE) | Haskell 98 phantom types to avoid unsafely passing dummy arguments | `aeson`, `assoc`, `bifunctors`, `boring`, `comonad`, `distributive`, `indexed-traversable-instances`, `semialign`, `semigroupoids` | +| `th-abstraction` | [`0.6.0.0`](http://hackage.haskell.org/package/th-abstraction-0.6.0.0) | [`ISC`](./licences/haskell/th-abstraction-0.6.0.0/LICENSE) | Nicer interface for reified information about data types | `aeson`, `bifunctors` | +| `th-compat` | [`0.1.4`](http://hackage.haskell.org/package/th-compat-0.1.4) | [`BSD-3-Clause`](./licences/haskell/th-compat-0.1.4/LICENSE) | Backward- (and forward-)compatible Quote and Code types | `network-uri` | +| `these` | [`1.2`](http://hackage.haskell.org/package/these-1.2) | [`BSD-3-Clause`](./licences/haskell/these-1.2/LICENSE) | An either-or-both data type. | `aeson`, `semialign`, `strict` | +| `time-compat` | [`1.9.6.1`](http://hackage.haskell.org/package/time-compat-1.9.6.1) | [`BSD-3-Clause`](./licences/haskell/time-compat-1.9.6.1/LICENSE) | Compatibility package for time | `aeson`, `text-iso8601` | +| `time-manager` | [`0.0.1`](http://hackage.haskell.org/package/time-manager-0.0.1) | [`MIT`](./licences/haskell/time-manager-0.0.1/LICENSE) | Scalable timer | `http2`, `simplexmq` | +| `temporary` | [`1.3`](http://hackage.haskell.org/package/temporary-1.3) | [`BSD-3-Clause`](./licences/haskell/temporary-1.3/LICENSE) | Portable temporary file and directory support | `simplexmq` | +| **`transformers`** | [`0.6.1.0`](http://hackage.haskell.org/package/transformers-0.6.1.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/transformers-0.6.1.0/src/LICENSE) | Concrete functor and monad transformers | `QuickCheck`, `StateVar`, `attoparsec`, `bifunctors`, `boring`, `comonad`, `conduit`, `conduit-extra`, `constraints`, `contravariant`, `crypton-x509`, `distributive`, `exceptions`, `indexed-traversable`, `monad-control`, `monad-logger`, `mono-traversable`, `mtl`, `network-transport`, `optparse-applicative`, `primitive`, `resourcet`, `safe-exceptions`, `semialign`, `semigroupoids`, `simplexmq`, `sqlcipher-simple`, `streaming-commons`, `strict`, `tagged`, `temporary`, `terminal`, `tls`, `transformers-base`, `transformers-compat`, `typed-process`, `unliftio`, `unliftio-core`, `witherable`, `yaml`, `zip` | +| `transformers-base` | [`0.4.6`](http://hackage.haskell.org/package/transformers-base-0.4.6) | [`BSD-3-Clause`](./licences/haskell/transformers-base-0.4.6/LICENSE) | Lift computations from the bottom of a transformer stack | `lifted-base`, `monad-control`, `monad-logger`, `zip` | +| `transformers-compat` | [`0.7.2`](http://hackage.haskell.org/package/transformers-compat-0.7.2) | [`BSD-3-Clause`](./licences/haskell/transformers-compat-0.7.2/LICENSE) | A small compatibility shim for the transformers library | `comonad`, `monad-control`, `monad-logger`, `optparse-applicative`, `semigroupoids`, `transformers-base` | +| `attoparsec` | [`0.14.4`](http://hackage.haskell.org/package/attoparsec-0.14.4) | [`BSD-3-Clause`](./licences/haskell/attoparsec-0.14.4/LICENSE) | Fast combinator parsing for bytestrings and text | `attoparsec` | +| `text-iso8601` | [`0.1`](http://hackage.haskell.org/package/text-iso8601-0.1) | [`BSD-3-Clause`](./licences/haskell/text-iso8601-0.1/LICENSE) | Converting time to and from ISO 8601 text. | `aeson` | +| `text-short` | [`0.1.5`](http://hackage.haskell.org/package/text-short-0.1.5) | [`BSD-3-Clause`](./licences/haskell/text-short-0.1.5/LICENSE) | Memory-efficient representation of Unicode text strings | `aeson` | +| `type-equality` | [`1`](http://hackage.haskell.org/package/type-equality-1) | [`BSD-3-Clause`](./licences/haskell/type-equality-1/LICENSE) | Data.Type.Equality compat package | `constraints` | +| `typed-process` | [`0.2.11.1`](http://hackage.haskell.org/package/typed-process-0.2.11.1) | [`MIT`](./licences/haskell/typed-process-0.2.11.1/LICENSE) | Run external processes, with strong typing of streams | `conduit-extra` | +| **`unix`** | [`2.8.1.0`](http://hackage.haskell.org/package/unix-2.8.1.0) | [`BSD-3-Clause`](http://hackage.haskell.org/package/unix-2.8.1.0/src/LICENSE) | POSIX functionality | `conduit`, `directory`, `easy-file`, `entropy`, `process`, `streaming-commons`, `temporary`, `unix-compat`, `unliftio`, `zip` | +| `vector` | [`0.13.1.0`](http://hackage.haskell.org/package/vector-0.13.1.0) | [`BSD-3-Clause`](./licences/haskell/vector-0.13.1.0/LICENSE) | Efficient Arrays | `aeson`, `bitvec`, `blaze-textual`, `conduit`, `indexed-traversable-instances`, `mono-traversable`, `semialign`, `vector-algorithms`, `witherable`, `yaml` | +| `vector-algorithms` | [`0.9.0.1`](http://hackage.haskell.org/package/vector-algorithms-0.9.0.1) | [`BSD-3-Clause`](./licences/haskell/vector-algorithms-0.9.0.1/LICENSE) | Efficient algorithms for vector arrays | `mono-traversable` | +| `vector-stream` | [`0.1.0.0`](http://hackage.haskell.org/package/vector-stream-0.1.0.0) | [`BSD-3-Clause`](./licences/haskell/vector-stream-0.1.0.0/LICENSE) | Efficient Streams | `vector` | +| `witherable` | [`0.4.2`](http://hackage.haskell.org/package/witherable-0.4.2) | [`BSD-3-Clause`](./licences/haskell/witherable-0.4.2/LICENSE) | filterable traversable | `aeson` | +| `yaml` | [`0.11.11.2`](http://hackage.haskell.org/package/yaml-0.11.11.2) | [`BSD-3-Clause`](./licences/haskell/yaml-0.11.11.2/LICENSE) | Support for parsing and rendering YAML documents. | `simplexmq` | +| `zlib` | [`0.6.3.0`](http://hackage.haskell.org/package/zlib-0.6.3.0) | [`BSD-3-Clause`](./licences/haskell/zlib-0.6.3.0/LICENSE) | Compression and decompression in the gzip and zlib formats | `streaming-commons` | +| `zstd` | [`0.1.3.0`](http://hackage.haskell.org/package/zstd-0.1.3.0) | [`BSD-3-Clause`](./licences/haskell/zstd-0.1.3.0/LICENSE) | Haskell bindings to the Zstandard compression algorithm | `simplexmq` | + diff --git a/docs/dependencies/README.md b/docs/dependencies/README.md new file mode 100644 index 0000000000..bec369c126 --- /dev/null +++ b/docs/dependencies/README.md @@ -0,0 +1,9 @@ +# SimpleX Chat and SimpleX servers dependencies + +[SQLCipher](https://github.com/sqlcipher/sqlcipher): Extension of [SQLite](https://sqlite.org) with encryption ([BSD-style](./licences/apps/sqlcipher/LICENSE.md)) + +[vlc](https://github.com/videolan/vlc): VLC media player library ([LGPLv2](./licences/apps/vlc/COPYING.LIB)) + +[WebRTC](https://webrtc.googlesource.com/src/): RTC for calls ([BSD-3-clause](./licences/apps/webrtc/LICENSE)) + +[Haskell dependencies](./HASKELL.md). diff --git a/docs/dependencies/licences/apps/sqlcipher/LICENSE.md b/docs/dependencies/licences/apps/sqlcipher/LICENSE.md new file mode 100644 index 0000000000..3f71443161 --- /dev/null +++ b/docs/dependencies/licences/apps/sqlcipher/LICENSE.md @@ -0,0 +1,24 @@ +Copyright (c) 2025, ZETETIC LLC +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the ZETETIC LLC nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY ZETETIC LLC ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ZETETIC LLC BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/apps/vlc/COPYING.LIB b/docs/dependencies/licences/apps/vlc/COPYING.LIB new file mode 100644 index 0000000000..4362b49151 --- /dev/null +++ b/docs/dependencies/licences/apps/vlc/COPYING.LIB @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/docs/dependencies/licences/apps/webrtc/LICENSE b/docs/dependencies/licences/apps/webrtc/LICENSE new file mode 100644 index 0000000000..8868dcdf45 --- /dev/null +++ b/docs/dependencies/licences/apps/webrtc/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2011, The WebRTC project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/OneTuple-0.4.1.1/LICENSE b/docs/dependencies/licences/haskell/OneTuple-0.4.1.1/LICENSE new file mode 100644 index 0000000000..405f9161e9 --- /dev/null +++ b/docs/dependencies/licences/haskell/OneTuple-0.4.1.1/LICENSE @@ -0,0 +1,34 @@ + +Copyright (c) 2008, John A. Dorsey. +All rights reserved. + +Redistribution and use of this software in source and binary forms, +with or without modification, are permitted provided that the +following conditions are met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +* Neither the name of John Dorsey nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/docs/dependencies/licences/haskell/Only-0.1/LICENSE b/docs/dependencies/licences/haskell/Only-0.1/LICENSE new file mode 100644 index 0000000000..98d4b1ad78 --- /dev/null +++ b/docs/dependencies/licences/haskell/Only-0.1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2017, Herbert Valerio Riedel + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Herbert Valerio Riedel nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/QuickCheck-2.14.3/LICENSE b/docs/dependencies/licences/haskell/QuickCheck-2.14.3/LICENSE new file mode 100644 index 0000000000..8b0fdbfe4e --- /dev/null +++ b/docs/dependencies/licences/haskell/QuickCheck-2.14.3/LICENSE @@ -0,0 +1,29 @@ +(The following is the 3-clause BSD license.) + +Copyright (c) 2000-2019, Koen Claessen +Copyright (c) 2006-2008, Björn Bringert +Copyright (c) 2009-2019, Nick Smallbone + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +- Neither the names of the copyright owners nor the names of the + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/SHA-1.6.4.4/LICENSE b/docs/dependencies/licences/haskell/SHA-1.6.4.4/LICENSE new file mode 100644 index 0000000000..8e49100207 --- /dev/null +++ b/docs/dependencies/licences/haskell/SHA-1.6.4.4/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2008, Galois, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of the Galois, Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/StateVar-1.2.2/LICENSE b/docs/dependencies/licences/haskell/StateVar-1.2.2/LICENSE new file mode 100644 index 0000000000..e71299ba0e --- /dev/null +++ b/docs/dependencies/licences/haskell/StateVar-1.2.2/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2014-2015, Edward Kmett +Copyright (c) 2009-2021, Sven Panne +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the author nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/aeson-2.2.1.0/LICENSE b/docs/dependencies/licences/haskell/aeson-2.2.1.0/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/ansi-terminal-1.0/LICENSE b/docs/dependencies/licences/haskell/ansi-terminal-1.0/LICENSE new file mode 100644 index 0000000000..814a83d4fc --- /dev/null +++ b/docs/dependencies/licences/haskell/ansi-terminal-1.0/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2008, Maximilian Bolingbroke +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted +provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + * Neither the name of Maximilian Bolingbroke nor the names of other contributors may be used to + endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/ansi-terminal-types-0.11.5/LICENSE b/docs/dependencies/licences/haskell/ansi-terminal-types-0.11.5/LICENSE new file mode 100644 index 0000000000..413047009d --- /dev/null +++ b/docs/dependencies/licences/haskell/ansi-terminal-types-0.11.5/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2008, Maximilian Bolingbroke +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of Maximilian Bolingbroke nor the names of other contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/appar-0.1.8/LICENSE b/docs/dependencies/licences/haskell/appar-0.1.8/LICENSE new file mode 100644 index 0000000000..5422193088 --- /dev/null +++ b/docs/dependencies/licences/haskell/appar-0.1.8/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2009, IIJ Innovation Institute Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/asn1-encoding-0.9.6/LICENSE b/docs/dependencies/licences/haskell/asn1-encoding-0.9.6/LICENSE new file mode 100644 index 0000000000..e68cc61f14 --- /dev/null +++ b/docs/dependencies/licences/haskell/asn1-encoding-0.9.6/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010-2013 Vincent Hanquez <vincent@snarc.org> + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/asn1-parse-0.9.5/LICENSE b/docs/dependencies/licences/haskell/asn1-parse-0.9.5/LICENSE new file mode 100644 index 0000000000..e68cc61f14 --- /dev/null +++ b/docs/dependencies/licences/haskell/asn1-parse-0.9.5/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010-2013 Vincent Hanquez <vincent@snarc.org> + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/asn1-types-0.3.4/LICENSE b/docs/dependencies/licences/haskell/asn1-types-0.3.4/LICENSE new file mode 100644 index 0000000000..e68cc61f14 --- /dev/null +++ b/docs/dependencies/licences/haskell/asn1-types-0.3.4/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010-2013 Vincent Hanquez <vincent@snarc.org> + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/assoc-1.1/LICENSE b/docs/dependencies/licences/haskell/assoc-1.1/LICENSE new file mode 100644 index 0000000000..96445e5484 --- /dev/null +++ b/docs/dependencies/licences/haskell/assoc-1.1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2017, Oleg Grenrus + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Oleg Grenrus nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/async-2.2.5/LICENSE b/docs/dependencies/licences/haskell/async-2.2.5/LICENSE new file mode 100644 index 0000000000..0acbb121f4 --- /dev/null +++ b/docs/dependencies/licences/haskell/async-2.2.5/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2012, Simon Marlow + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Simon Marlow nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/attoparsec-0.14.4/LICENSE b/docs/dependencies/licences/haskell/attoparsec-0.14.4/LICENSE new file mode 100644 index 0000000000..97392a6264 --- /dev/null +++ b/docs/dependencies/licences/haskell/attoparsec-0.14.4/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) Lennart Kolmodin + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/auto-update-0.1.6/LICENSE b/docs/dependencies/licences/haskell/auto-update-0.1.6/LICENSE new file mode 100644 index 0000000000..d2a7d05797 --- /dev/null +++ b/docs/dependencies/licences/haskell/auto-update-0.1.6/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2014 Michael Snoyman + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/dependencies/licences/haskell/base-orphans-0.9.1/LICENSE b/docs/dependencies/licences/haskell/base-orphans-0.9.1/LICENSE new file mode 100644 index 0000000000..4751ca3f63 --- /dev/null +++ b/docs/dependencies/licences/haskell/base-orphans-0.9.1/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2015-2017 Simon Hengel <sol@typeful.net>, João Cristóvão <jmacristovao@gmail.com>, Ryan Scott <ryan.gl.scott@gmail.com> + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/dependencies/licences/haskell/base64-bytestring-1.2.1.0/LICENSE b/docs/dependencies/licences/haskell/base64-bytestring-1.2.1.0/LICENSE new file mode 100644 index 0000000000..89d2a1b35f --- /dev/null +++ b/docs/dependencies/licences/haskell/base64-bytestring-1.2.1.0/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2010 Bryan O'Sullivan <bos@serpentine.com> + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/basement-0.0.16/LICENSE b/docs/dependencies/licences/haskell/basement-0.0.16/LICENSE new file mode 100644 index 0000000000..75e0288c26 --- /dev/null +++ b/docs/dependencies/licences/haskell/basement-0.0.16/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2015-2017 Vincent Hanquez <vincent@snarc.org> +Copyright (c) 2017-2019 Foundation Maintainers + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/bifunctors-5.6.1/LICENSE b/docs/dependencies/licences/haskell/bifunctors-5.6.1/LICENSE new file mode 100644 index 0000000000..531684e87f --- /dev/null +++ b/docs/dependencies/licences/haskell/bifunctors-5.6.1/LICENSE @@ -0,0 +1,26 @@ +Copyright 2008-2016 Edward Kmett + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/bitvec-1.1.5.0/LICENSE b/docs/dependencies/licences/haskell/bitvec-1.1.5.0/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/blaze-builder-0.4.2.3/LICENSE b/docs/dependencies/licences/haskell/blaze-builder-0.4.2.3/LICENSE new file mode 100644 index 0000000000..a8bcabfd42 --- /dev/null +++ b/docs/dependencies/licences/haskell/blaze-builder-0.4.2.3/LICENSE @@ -0,0 +1,30 @@ +Copyright Jasper Van der Jeugt 2010, Simon Meier 2010 & 2011 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Jasper Van der Jeugt nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/blaze-textual-0.2.3.1/LICENSE b/docs/dependencies/licences/haskell/blaze-textual-0.2.3.1/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/boring-0.2.1/LICENSE b/docs/dependencies/licences/haskell/boring-0.2.1/LICENSE new file mode 100644 index 0000000000..96445e5484 --- /dev/null +++ b/docs/dependencies/licences/haskell/boring-0.2.1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2017, Oleg Grenrus + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Oleg Grenrus nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/byteorder-1.0.4/LICENSE b/docs/dependencies/licences/haskell/byteorder-1.0.4/LICENSE new file mode 100644 index 0000000000..81b771a3b8 --- /dev/null +++ b/docs/dependencies/licences/haskell/byteorder-1.0.4/LICENSE @@ -0,0 +1,30 @@ +Copyright 2009, Antoine Latter + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the author nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/bytestring-builder-0.10.8.2.0/LICENSE b/docs/dependencies/licences/haskell/bytestring-builder-0.10.8.2.0/LICENSE new file mode 100644 index 0000000000..5106f1309a --- /dev/null +++ b/docs/dependencies/licences/haskell/bytestring-builder-0.10.8.2.0/LICENSE @@ -0,0 +1,30 @@ +Copyright Jasper Van der Jeugt 2010, Simon Meier 2010-2013 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Jasper Van der Jeugt nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/case-insensitive-1.2.1.0/LICENSE b/docs/dependencies/licences/haskell/case-insensitive-1.2.1.0/LICENSE new file mode 100644 index 0000000000..c81fb8bf9d --- /dev/null +++ b/docs/dependencies/licences/haskell/case-insensitive-1.2.1.0/LICENSE @@ -0,0 +1,31 @@ +Copyright (c) 2011-2013 Bas van Dijk + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * The name of Bas van Dijk and the names of contributors may NOT + be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/cereal-0.5.8.3/LICENSE b/docs/dependencies/licences/haskell/cereal-0.5.8.3/LICENSE new file mode 100644 index 0000000000..6dfb133efb --- /dev/null +++ b/docs/dependencies/licences/haskell/cereal-0.5.8.3/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) Lennart Kolmodin, Galois, Inc. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/clock-0.8.4/LICENSE b/docs/dependencies/licences/haskell/clock-0.8.4/LICENSE new file mode 100644 index 0000000000..c2451c8c0a --- /dev/null +++ b/docs/dependencies/licences/haskell/clock-0.8.4/LICENSE @@ -0,0 +1,31 @@ +Copyright (c) 2009-2022, Clock Contributors + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * The names of contributors may not be used to endorse or promote + products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/colour-2.3.6/LICENSE b/docs/dependencies/licences/haskell/colour-2.3.6/LICENSE new file mode 100644 index 0000000000..61aede888a --- /dev/null +++ b/docs/dependencies/licences/haskell/colour-2.3.6/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2008, 2009 +Russell O'Connor + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/dependencies/licences/haskell/comonad-5.0.8/LICENSE b/docs/dependencies/licences/haskell/comonad-5.0.8/LICENSE new file mode 100644 index 0000000000..f8e92e04df --- /dev/null +++ b/docs/dependencies/licences/haskell/comonad-5.0.8/LICENSE @@ -0,0 +1,27 @@ +Copyright 2008-2014 Edward Kmett +Copyright 2004-2008 Dave Menendez + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/composition-1.0.2.2/LICENSE b/docs/dependencies/licences/haskell/composition-1.0.2.2/LICENSE new file mode 100644 index 0000000000..e191e7ab64 --- /dev/null +++ b/docs/dependencies/licences/haskell/composition-1.0.2.2/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2011-2012, Dan Burton + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Dan Burton nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/conduit-1.3.5/LICENSE b/docs/dependencies/licences/haskell/conduit-1.3.5/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/conduit-extra-1.3.6/LICENSE b/docs/dependencies/licences/haskell/conduit-extra-1.3.6/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/constraints-0.14/LICENSE b/docs/dependencies/licences/haskell/constraints-0.14/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/contravariant-1.5.5/LICENSE b/docs/dependencies/licences/haskell/contravariant-1.5.5/LICENSE new file mode 100644 index 0000000000..f357c7c429 --- /dev/null +++ b/docs/dependencies/licences/haskell/contravariant-1.5.5/LICENSE @@ -0,0 +1,30 @@ +Copyright 2007-2015 Edward Kmett + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/cryptohash-md5-0.11.101.0/LICENSE b/docs/dependencies/licences/haskell/cryptohash-md5-0.11.101.0/LICENSE new file mode 100644 index 0000000000..467bf18f27 --- /dev/null +++ b/docs/dependencies/licences/haskell/cryptohash-md5-0.11.101.0/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2010-2014 Vincent Hanquez <vincent@snarc.org> + 2016 Herbert Valerio Riedel <hvr@gnu.org> + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/cryptohash-sha1-0.11.101.0/LICENSE b/docs/dependencies/licences/haskell/cryptohash-sha1-0.11.101.0/LICENSE new file mode 100644 index 0000000000..467bf18f27 --- /dev/null +++ b/docs/dependencies/licences/haskell/cryptohash-sha1-0.11.101.0/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2010-2014 Vincent Hanquez <vincent@snarc.org> + 2016 Herbert Valerio Riedel <hvr@gnu.org> + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/crypton-0.34/LICENSE b/docs/dependencies/licences/haskell/crypton-0.34/LICENSE new file mode 100644 index 0000000000..a61c84945b --- /dev/null +++ b/docs/dependencies/licences/haskell/crypton-0.34/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2006-2015 Vincent Hanquez <vincent@snarc.org> + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/crypton-x509-1.7.6/LICENSE b/docs/dependencies/licences/haskell/crypton-x509-1.7.6/LICENSE new file mode 100644 index 0000000000..e68cc61f14 --- /dev/null +++ b/docs/dependencies/licences/haskell/crypton-x509-1.7.6/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010-2013 Vincent Hanquez <vincent@snarc.org> + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/crypton-x509-store-1.6.9/LICENSE b/docs/dependencies/licences/haskell/crypton-x509-store-1.6.9/LICENSE new file mode 100644 index 0000000000..e68cc61f14 --- /dev/null +++ b/docs/dependencies/licences/haskell/crypton-x509-store-1.6.9/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010-2013 Vincent Hanquez <vincent@snarc.org> + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/crypton-x509-validation-1.6.12/LICENSE b/docs/dependencies/licences/haskell/crypton-x509-validation-1.6.12/LICENSE new file mode 100644 index 0000000000..e68cc61f14 --- /dev/null +++ b/docs/dependencies/licences/haskell/crypton-x509-validation-1.6.12/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010-2013 Vincent Hanquez <vincent@snarc.org> + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/cryptostore-0.3.0.1/LICENSE b/docs/dependencies/licences/haskell/cryptostore-0.3.0.1/LICENSE new file mode 100644 index 0000000000..b85926a265 --- /dev/null +++ b/docs/dependencies/licences/haskell/cryptostore-0.3.0.1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2018-2023, Olivier Chéron + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Olivier Chéron nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/data-default-0.7.1.1/LICENSE b/docs/dependencies/licences/haskell/data-default-0.7.1.1/LICENSE new file mode 100644 index 0000000000..cd44640be9 --- /dev/null +++ b/docs/dependencies/licences/haskell/data-default-0.7.1.1/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2013 Lukas Mai + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/data-default-class-0.1.2.0/LICENSE b/docs/dependencies/licences/haskell/data-default-class-0.1.2.0/LICENSE new file mode 100644 index 0000000000..cd44640be9 --- /dev/null +++ b/docs/dependencies/licences/haskell/data-default-class-0.1.2.0/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2013 Lukas Mai + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/data-default-instances-containers-0.0.1/LICENSE b/docs/dependencies/licences/haskell/data-default-instances-containers-0.0.1/LICENSE new file mode 100644 index 0000000000..4f9075d76b --- /dev/null +++ b/docs/dependencies/licences/haskell/data-default-instances-containers-0.0.1/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2013 Lukas Mai + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY LUKAS MAI AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/data-default-instances-dlist-0.0.1/LICENSE b/docs/dependencies/licences/haskell/data-default-instances-dlist-0.0.1/LICENSE new file mode 100644 index 0000000000..4f9075d76b --- /dev/null +++ b/docs/dependencies/licences/haskell/data-default-instances-dlist-0.0.1/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2013 Lukas Mai + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY LUKAS MAI AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/data-default-instances-old-locale-0.0.1/LICENSE b/docs/dependencies/licences/haskell/data-default-instances-old-locale-0.0.1/LICENSE new file mode 100644 index 0000000000..4f9075d76b --- /dev/null +++ b/docs/dependencies/licences/haskell/data-default-instances-old-locale-0.0.1/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2013 Lukas Mai + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY LUKAS MAI AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/data-fix-0.3.2/LICENSE b/docs/dependencies/licences/haskell/data-fix-0.3.2/LICENSE new file mode 100644 index 0000000000..0576170924 --- /dev/null +++ b/docs/dependencies/licences/haskell/data-fix-0.3.2/LICENSE @@ -0,0 +1,30 @@ +Copyright Anton Kholomiov 2010 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Anton Kholomiov nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/digest-0.0.1.7/LICENSE b/docs/dependencies/licences/haskell/digest-0.0.1.7/LICENSE new file mode 100644 index 0000000000..48326b0815 --- /dev/null +++ b/docs/dependencies/licences/haskell/digest-0.0.1.7/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2008-2009, Eugene Kirpichov +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/distributive-0.6.2.1/LICENSE b/docs/dependencies/licences/haskell/distributive-0.6.2.1/LICENSE new file mode 100644 index 0000000000..d6c9cbe4a4 --- /dev/null +++ b/docs/dependencies/licences/haskell/distributive-0.6.2.1/LICENSE @@ -0,0 +1,26 @@ +Copyright 2011-2016 Edward Kmett + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/dlist-1.0/license.md b/docs/dependencies/licences/haskell/dlist-1.0/license.md new file mode 100644 index 0000000000..32ac978e90 --- /dev/null +++ b/docs/dependencies/licences/haskell/dlist-1.0/license.md @@ -0,0 +1,28 @@ +Copyright © 2006-2009 Don Stewart, 2013-2020 Sean Leather, contributors + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holders nor the names of other contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/easy-file-0.2.5/LICENSE b/docs/dependencies/licences/haskell/easy-file-0.2.5/LICENSE new file mode 100644 index 0000000000..5422193088 --- /dev/null +++ b/docs/dependencies/licences/haskell/easy-file-0.2.5/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2009, IIJ Innovation Institute Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/email-validate-2.3.2.19/LICENSE b/docs/dependencies/licences/haskell/email-validate-2.3.2.19/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/entropy-0.4.1.10/LICENSE b/docs/dependencies/licences/haskell/entropy-0.4.1.10/LICENSE new file mode 100644 index 0000000000..6d24e89d0b --- /dev/null +++ b/docs/dependencies/licences/haskell/entropy-0.4.1.10/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) Thomas DuBuisson + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/fast-logger-3.2.2/LICENSE b/docs/dependencies/licences/haskell/fast-logger-3.2.2/LICENSE new file mode 100644 index 0000000000..5422193088 --- /dev/null +++ b/docs/dependencies/licences/haskell/fast-logger-3.2.2/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2009, IIJ Innovation Institute Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/file-embed-0.0.15.0/LICENSE b/docs/dependencies/licences/haskell/file-embed-0.0.15.0/LICENSE new file mode 100644 index 0000000000..11dc17a16c --- /dev/null +++ b/docs/dependencies/licences/haskell/file-embed-0.0.15.0/LICENSE @@ -0,0 +1,25 @@ +The following license covers this documentation, and the source code, except +where otherwise indicated. + +Copyright 2008, Michael Snoyman. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/generically-0.1.1/LICENSE b/docs/dependencies/licences/haskell/generically-0.1.1/LICENSE new file mode 100644 index 0000000000..58c573a1e2 --- /dev/null +++ b/docs/dependencies/licences/haskell/generically-0.1.1/LICENSE @@ -0,0 +1,28 @@ +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Herbert Valerio Riedel nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/hashable-1.4.3.0/LICENSE b/docs/dependencies/licences/haskell/hashable-1.4.3.0/LICENSE new file mode 100644 index 0000000000..7130957b4d --- /dev/null +++ b/docs/dependencies/licences/haskell/hashable-1.4.3.0/LICENSE @@ -0,0 +1,30 @@ +Copyright Milan Straka 2010 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Milan Straka nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/hourglass-0.2.12/LICENSE b/docs/dependencies/licences/haskell/hourglass-0.2.12/LICENSE new file mode 100644 index 0000000000..36c942d4a8 --- /dev/null +++ b/docs/dependencies/licences/haskell/hourglass-0.2.12/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2014 Vincent Hanquez <vincent@snarc.org> + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/http-types-0.12.4/LICENSE b/docs/dependencies/licences/haskell/http-types-0.12.4/LICENSE new file mode 100644 index 0000000000..f77a7b24dd --- /dev/null +++ b/docs/dependencies/licences/haskell/http-types-0.12.4/LICENSE @@ -0,0 +1,31 @@ +Copyright (c) 2011, Aristid Breitkreuz +Copyright (c) 2011, Michael Snoyman + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Aristid Breitkreuz nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/http2-5.0.0/LICENSE b/docs/dependencies/licences/haskell/http2-5.0.0/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/indexed-traversable-0.1.3/LICENSE b/docs/dependencies/licences/haskell/indexed-traversable-0.1.3/LICENSE new file mode 100644 index 0000000000..476166addc --- /dev/null +++ b/docs/dependencies/licences/haskell/indexed-traversable-0.1.3/LICENSE @@ -0,0 +1,26 @@ +Copyright 2012-2016 Edward Kmett + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/indexed-traversable-instances-0.1.1.2/LICENSE b/docs/dependencies/licences/haskell/indexed-traversable-instances-0.1.1.2/LICENSE new file mode 100644 index 0000000000..476166addc --- /dev/null +++ b/docs/dependencies/licences/haskell/indexed-traversable-instances-0.1.1.2/LICENSE @@ -0,0 +1,26 @@ +Copyright 2012-2016 Edward Kmett + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/ini-0.4.2/LICENSE b/docs/dependencies/licences/haskell/ini-0.4.2/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/integer-conversion-0.1.0.1/LICENSE b/docs/dependencies/licences/haskell/integer-conversion-0.1.0.1/LICENSE new file mode 100644 index 0000000000..3f74492595 --- /dev/null +++ b/docs/dependencies/licences/haskell/integer-conversion-0.1.0.1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2023, Oleg Grenrus + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Oleg Grenrus nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/integer-logarithms-1.0.3.1/LICENSE b/docs/dependencies/licences/haskell/integer-logarithms-1.0.3.1/LICENSE new file mode 100644 index 0000000000..9430f484d0 --- /dev/null +++ b/docs/dependencies/licences/haskell/integer-logarithms-1.0.3.1/LICENSE @@ -0,0 +1,16 @@ +Copyright (c) 2011 Daniel Fischer, 2017 Oleg Grenrus + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + associated documentation files (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, publish, distribute, + sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/dependencies/licences/haskell/iproute-1.7.12/LICENSE b/docs/dependencies/licences/haskell/iproute-1.7.12/LICENSE new file mode 100644 index 0000000000..5422193088 --- /dev/null +++ b/docs/dependencies/licences/haskell/iproute-1.7.12/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2009, IIJ Innovation Institute Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/libyaml-0.1.2/LICENSE b/docs/dependencies/licences/haskell/libyaml-0.1.2/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/lifted-base-0.2.3.12/LICENSE b/docs/dependencies/licences/haskell/lifted-base-0.2.3.12/LICENSE new file mode 100644 index 0000000000..b8b5ed95c0 --- /dev/null +++ b/docs/dependencies/licences/haskell/lifted-base-0.2.3.12/LICENSE @@ -0,0 +1,29 @@ +Copyright © 2010-2012, Bas van Dijk, Anders Kaseorg +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +• Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +• Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +• Neither the name of the author nor the names of other contributors + may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +“AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/memory-0.18.0/LICENSE b/docs/dependencies/licences/haskell/memory-0.18.0/LICENSE new file mode 100644 index 0000000000..8eafd52555 --- /dev/null +++ b/docs/dependencies/licences/haskell/memory-0.18.0/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2015-2018 Vincent Hanquez <vincent@snarc.org> +Copyright (c) 2017-2018 Nicolas Di Prima <nicolas@primetype.co.uk> + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/monad-control-1.0.3.1/LICENSE b/docs/dependencies/licences/haskell/monad-control-1.0.3.1/LICENSE new file mode 100644 index 0000000000..f4e1bfab44 --- /dev/null +++ b/docs/dependencies/licences/haskell/monad-control-1.0.3.1/LICENSE @@ -0,0 +1,29 @@ +Copyright © 2010, Bas van Dijk, Anders Kaseorg +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +• Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +• Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +• Neither the name of the author nor the names of other contributors + may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +“AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/monad-logger-0.3.40/LICENSE b/docs/dependencies/licences/haskell/monad-logger-0.3.40/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/mono-traversable-1.0.15.3/LICENSE b/docs/dependencies/licences/haskell/mono-traversable-1.0.15.3/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/network-3.1.4.0/LICENSE b/docs/dependencies/licences/haskell/network-3.1.4.0/LICENSE new file mode 100644 index 0000000000..40bba626f8 --- /dev/null +++ b/docs/dependencies/licences/haskell/network-3.1.4.0/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2002-2010, The University Court of the University of Glasgow. +Copyright (c) 2007-2010, Johan Tibell + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/docs/dependencies/licences/haskell/network-byte-order-0.1.7/LICENSE b/docs/dependencies/licences/haskell/network-byte-order-0.1.7/LICENSE new file mode 100644 index 0000000000..ed342e54a0 --- /dev/null +++ b/docs/dependencies/licences/haskell/network-byte-order-0.1.7/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2017, Kazu Yamamoto + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Kazu Yamamoto nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/network-control-0.0.2/LICENSE b/docs/dependencies/licences/haskell/network-control-0.0.2/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/network-info-0.2.1/LICENSE b/docs/dependencies/licences/haskell/network-info-0.2.1/LICENSE new file mode 100644 index 0000000000..be005bd412 --- /dev/null +++ b/docs/dependencies/licences/haskell/network-info-0.2.1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2010, Jacob Stanley + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Jacob Stanley nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/network-transport-0.5.6/LICENSE b/docs/dependencies/licences/haskell/network-transport-0.5.6/LICENSE new file mode 100644 index 0000000000..f3459e449d --- /dev/null +++ b/docs/dependencies/licences/haskell/network-transport-0.5.6/LICENSE @@ -0,0 +1,31 @@ +Copyright Well-Typed LLP, 2011-2012 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the owner nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/docs/dependencies/licences/haskell/network-udp-0.0.0/LICENSE b/docs/dependencies/licences/haskell/network-udp-0.0.0/LICENSE new file mode 100644 index 0000000000..73957a7a32 --- /dev/null +++ b/docs/dependencies/licences/haskell/network-udp-0.0.0/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2022, Internet Initiative Japan Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/network-uri-2.6.4.2/LICENSE b/docs/dependencies/licences/haskell/network-uri-2.6.4.2/LICENSE new file mode 100644 index 0000000000..40bba626f8 --- /dev/null +++ b/docs/dependencies/licences/haskell/network-uri-2.6.4.2/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2002-2010, The University Court of the University of Glasgow. +Copyright (c) 2007-2010, Johan Tibell + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/docs/dependencies/licences/haskell/old-locale-1.0.0.7/LICENSE b/docs/dependencies/licences/haskell/old-locale-1.0.0.7/LICENSE new file mode 100644 index 0000000000..06bb641487 --- /dev/null +++ b/docs/dependencies/licences/haskell/old-locale-1.0.0.7/LICENSE @@ -0,0 +1,63 @@ +This library (libraries/base) is derived from code from two +sources: + + * Code from the GHC project which is largely (c) The University of + Glasgow, and distributable under a BSD-style license (see below), + + * Code from the Haskell 98 Report which is (c) Simon Peyton Jones + and freely redistributable (but see the full license for + restrictions). + +The full text of these licenses is reproduced below. Both of the +licenses are BSD-style or compatible. + +----------------------------------------------------------------------------- + +The Glasgow Haskell Compiler License + +Copyright 2004, The University Court of the University of Glasgow. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +----------------------------------------------------------------------------- + +Code derived from the document "Report on the Programming Language +Haskell 98", is distributed under the following license: + + Copyright (c) 2002 Simon Peyton Jones + + The authors intend this Report to belong to the entire Haskell + community, and so we grant permission to copy and distribute it for + any purpose, provided that it is reproduced in its entirety, + including this Notice. Modified versions of this Report may also be + copied and distributed for any purpose, provided that the modified + version is clearly presented as such, and that it does not claim to + be a definition of the Haskell 98 Language. + +----------------------------------------------------------------------------- diff --git a/docs/dependencies/licences/haskell/old-time-1.1.0.4/LICENSE b/docs/dependencies/licences/haskell/old-time-1.1.0.4/LICENSE new file mode 100644 index 0000000000..06bb641487 --- /dev/null +++ b/docs/dependencies/licences/haskell/old-time-1.1.0.4/LICENSE @@ -0,0 +1,63 @@ +This library (libraries/base) is derived from code from two +sources: + + * Code from the GHC project which is largely (c) The University of + Glasgow, and distributable under a BSD-style license (see below), + + * Code from the Haskell 98 Report which is (c) Simon Peyton Jones + and freely redistributable (but see the full license for + restrictions). + +The full text of these licenses is reproduced below. Both of the +licenses are BSD-style or compatible. + +----------------------------------------------------------------------------- + +The Glasgow Haskell Compiler License + +Copyright 2004, The University Court of the University of Glasgow. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +----------------------------------------------------------------------------- + +Code derived from the document "Report on the Programming Language +Haskell 98", is distributed under the following license: + + Copyright (c) 2002 Simon Peyton Jones + + The authors intend this Report to belong to the entire Haskell + community, and so we grant permission to copy and distribute it for + any purpose, provided that it is reproduced in its entirety, + including this Notice. Modified versions of this Report may also be + copied and distributed for any purpose, provided that the modified + version is clearly presented as such, and that it does not claim to + be a definition of the Haskell 98 Language. + +----------------------------------------------------------------------------- diff --git a/docs/dependencies/licences/haskell/optparse-applicative-0.18.1.0/LICENSE b/docs/dependencies/licences/haskell/optparse-applicative-0.18.1.0/LICENSE new file mode 100644 index 0000000000..0507cc2a81 --- /dev/null +++ b/docs/dependencies/licences/haskell/optparse-applicative-0.18.1.0/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2012, Paolo Capriotti + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Paolo Capriotti nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/pem-0.2.4/LICENSE b/docs/dependencies/licences/haskell/pem-0.2.4/LICENSE new file mode 100644 index 0000000000..15c41788af --- /dev/null +++ b/docs/dependencies/licences/haskell/pem-0.2.4/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010-2018 Vincent Hanquez <vincent@snarc.org> + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/prettyprinter-1.7.1/LICENSE.md b/docs/dependencies/licences/haskell/prettyprinter-1.7.1/LICENSE.md new file mode 100644 index 0000000000..39592102dc --- /dev/null +++ b/docs/dependencies/licences/haskell/prettyprinter-1.7.1/LICENSE.md @@ -0,0 +1,23 @@ +Copyright 2008, Daan Leijen and Max Bolingbroke, 2016 David Luposchainsky. All +rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +This software is provided by the copyright holders "as is" and any express or +implied warranties, including, but not limited to, the implied warranties of +merchantability and fitness for a particular purpose are disclaimed. In no event +shall the copyright holders be liable for any direct, indirect, incidental, +special, exemplary, or consequential damages (including, but not limited to, +procurement of substitute goods or services; loss of use, data, or profits; or +business interruption) however caused and on any theory of liability, whether in +contract, strict liability, or tort (including negligence or otherwise) arising +in any way out of the use of this software, even if advised of the possibility +of such damage. diff --git a/docs/dependencies/licences/haskell/prettyprinter-ansi-terminal-1.1.3/LICENSE.md b/docs/dependencies/licences/haskell/prettyprinter-ansi-terminal-1.1.3/LICENSE.md new file mode 100644 index 0000000000..39592102dc --- /dev/null +++ b/docs/dependencies/licences/haskell/prettyprinter-ansi-terminal-1.1.3/LICENSE.md @@ -0,0 +1,23 @@ +Copyright 2008, Daan Leijen and Max Bolingbroke, 2016 David Luposchainsky. All +rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +This software is provided by the copyright holders "as is" and any express or +implied warranties, including, but not limited to, the implied warranties of +merchantability and fitness for a particular purpose are disclaimed. In no event +shall the copyright holders be liable for any direct, indirect, incidental, +special, exemplary, or consequential damages (including, but not limited to, +procurement of substitute goods or services; loss of use, data, or profits; or +business interruption) however caused and on any theory of liability, whether in +contract, strict liability, or tort (including negligence or otherwise) arising +in any way out of the use of this software, even if advised of the possibility +of such damage. diff --git a/docs/dependencies/licences/haskell/primitive-0.9.0.0/LICENSE b/docs/dependencies/licences/haskell/primitive-0.9.0.0/LICENSE new file mode 100644 index 0000000000..fc213a6ffb --- /dev/null +++ b/docs/dependencies/licences/haskell/primitive-0.9.0.0/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2008-2009, Roman Leshchinskiy +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + diff --git a/docs/dependencies/licences/haskell/psqueues-0.2.8.0/LICENSE b/docs/dependencies/licences/haskell/psqueues-0.2.8.0/LICENSE new file mode 100644 index 0000000000..92337b951e --- /dev/null +++ b/docs/dependencies/licences/haskell/psqueues-0.2.8.0/LICENSE @@ -0,0 +1,31 @@ +The Glasgow Haskell Compiler License + +Copyright 2004, The University Court of the University of Glasgow. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/docs/dependencies/licences/haskell/random-1.2.1.1/LICENSE b/docs/dependencies/licences/haskell/random-1.2.1.1/LICENSE new file mode 100644 index 0000000000..06bb641487 --- /dev/null +++ b/docs/dependencies/licences/haskell/random-1.2.1.1/LICENSE @@ -0,0 +1,63 @@ +This library (libraries/base) is derived from code from two +sources: + + * Code from the GHC project which is largely (c) The University of + Glasgow, and distributable under a BSD-style license (see below), + + * Code from the Haskell 98 Report which is (c) Simon Peyton Jones + and freely redistributable (but see the full license for + restrictions). + +The full text of these licenses is reproduced below. Both of the +licenses are BSD-style or compatible. + +----------------------------------------------------------------------------- + +The Glasgow Haskell Compiler License + +Copyright 2004, The University Court of the University of Glasgow. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +----------------------------------------------------------------------------- + +Code derived from the document "Report on the Programming Language +Haskell 98", is distributed under the following license: + + Copyright (c) 2002 Simon Peyton Jones + + The authors intend this Report to belong to the entire Haskell + community, and so we grant permission to copy and distribute it for + any purpose, provided that it is reproduced in its entirety, + including this Notice. Modified versions of this Report may also be + copied and distributed for any purpose, provided that the modified + version is clearly presented as such, and that it does not claim to + be a definition of the Haskell 98 Language. + +----------------------------------------------------------------------------- diff --git a/docs/dependencies/licences/haskell/record-hasfield-1.0/LICENSE b/docs/dependencies/licences/haskell/record-hasfield-1.0/LICENSE new file mode 100644 index 0000000000..f6a9dc60f7 --- /dev/null +++ b/docs/dependencies/licences/haskell/record-hasfield-1.0/LICENSE @@ -0,0 +1,30 @@ +Copyright Adam Gundry and Neil Mitchell 2018-2019. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Neil Mitchell nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/resourcet-1.3.0/LICENSE b/docs/dependencies/licences/haskell/resourcet-1.3.0/LICENSE new file mode 100644 index 0000000000..fe6d4e9b3f --- /dev/null +++ b/docs/dependencies/licences/haskell/resourcet-1.3.0/LICENSE @@ -0,0 +1,30 @@ +Copyright (c)2011, Michael Snoyman + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Michael Snoyman nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/safe-exceptions-0.1.7.4/LICENSE b/docs/dependencies/licences/haskell/safe-exceptions-0.1.7.4/LICENSE new file mode 100644 index 0000000000..9a69c0901b --- /dev/null +++ b/docs/dependencies/licences/haskell/safe-exceptions-0.1.7.4/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2016 FP Complete + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/dependencies/licences/haskell/scientific-0.3.7.0/LICENSE b/docs/dependencies/licences/haskell/scientific-0.3.7.0/LICENSE new file mode 100644 index 0000000000..c1015843aa --- /dev/null +++ b/docs/dependencies/licences/haskell/scientific-0.3.7.0/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2013, Bas van Dijk + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Bas van Dijk nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/semialign-1.3/LICENSE b/docs/dependencies/licences/haskell/semialign-1.3/LICENSE new file mode 100644 index 0000000000..9d6d650165 --- /dev/null +++ b/docs/dependencies/licences/haskell/semialign-1.3/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2012, C. McCann, 2015-2019 Oleg Grenrus + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of C. McCann nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/semigroupoids-6.0.0.1/LICENSE b/docs/dependencies/licences/haskell/semigroupoids-6.0.0.1/LICENSE new file mode 100644 index 0000000000..1812ac961c --- /dev/null +++ b/docs/dependencies/licences/haskell/semigroupoids-6.0.0.1/LICENSE @@ -0,0 +1,26 @@ +Copyright 2011-2015 Edward Kmett + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/simple-logger-0.1.1/LICENSE b/docs/dependencies/licences/haskell/simple-logger-0.1.1/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/socks-0.6.1/LICENSE b/docs/dependencies/licences/haskell/socks-0.6.1/LICENSE new file mode 100644 index 0000000000..ed6f548a98 --- /dev/null +++ b/docs/dependencies/licences/haskell/socks-0.6.1/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010-2019 Vincent Hanquez <vincent@snarc.org> + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/split-0.2.4/LICENSE b/docs/dependencies/licences/haskell/split-0.2.4/LICENSE new file mode 100644 index 0000000000..648167ba61 --- /dev/null +++ b/docs/dependencies/licences/haskell/split-0.2.4/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2008 Brent Yorgey, Louis Wasserman + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of other contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/splitmix-0.1.0.5/LICENSE b/docs/dependencies/licences/haskell/splitmix-0.1.0.5/LICENSE new file mode 100644 index 0000000000..96445e5484 --- /dev/null +++ b/docs/dependencies/licences/haskell/splitmix-0.1.0.5/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2017, Oleg Grenrus + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Oleg Grenrus nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/stm-chans-3.0.0.9/LICENSE b/docs/dependencies/licences/haskell/stm-chans-3.0.0.9/LICENSE new file mode 100644 index 0000000000..55157e8352 --- /dev/null +++ b/docs/dependencies/licences/haskell/stm-chans-3.0.0.9/LICENSE @@ -0,0 +1,35 @@ +=== stm-chans license === + +Copyright (c) 2011--2013, wren gayle romano. +ALL RIGHTS RESERVED. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the copyright holders nor the names of + other contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + diff --git a/docs/dependencies/licences/haskell/streaming-commons-0.2.2.6/LICENSE b/docs/dependencies/licences/haskell/streaming-commons-0.2.2.6/LICENSE new file mode 100644 index 0000000000..7198785aaf --- /dev/null +++ b/docs/dependencies/licences/haskell/streaming-commons-0.2.2.6/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 FP Complete + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docs/dependencies/licences/haskell/strict-0.5/LICENSE b/docs/dependencies/licences/haskell/strict-0.5/LICENSE new file mode 100644 index 0000000000..4782550806 --- /dev/null +++ b/docs/dependencies/licences/haskell/strict-0.5/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) Roman Leshchinskiy 2006-2007 + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/tagged-0.8.8/LICENSE b/docs/dependencies/licences/haskell/tagged-0.8.8/LICENSE new file mode 100644 index 0000000000..db046152dc --- /dev/null +++ b/docs/dependencies/licences/haskell/tagged-0.8.8/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2009-2015 Edward Kmett +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Edward Kmett nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/temporary-1.3/LICENSE b/docs/dependencies/licences/haskell/temporary-1.3/LICENSE new file mode 100644 index 0000000000..db55f34db3 --- /dev/null +++ b/docs/dependencies/licences/haskell/temporary-1.3/LICENSE @@ -0,0 +1,27 @@ +Copyright + (c) 2003-2006, Isaac Jones + (c) 2005-2009, Duncan Coutts + (c) 2008, Maximilian Bolingbroke + ... and other contributors + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted +provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + * Neither the name of Maximilian Bolingbroke nor the names of other contributors may be used to + endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/terminal-0.2.0.0/LICENSE b/docs/dependencies/licences/haskell/terminal-0.2.0.0/LICENSE new file mode 100644 index 0000000000..d45361ccf6 --- /dev/null +++ b/docs/dependencies/licences/haskell/terminal-0.2.0.0/LICENSE @@ -0,0 +1,30 @@ +Copyright Lars Petersen (c) 2018 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Lars Petersen nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/text-iso8601-0.1/LICENSE b/docs/dependencies/licences/haskell/text-iso8601-0.1/LICENSE new file mode 100644 index 0000000000..fc90c466c7 --- /dev/null +++ b/docs/dependencies/licences/haskell/text-iso8601-0.1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2023 Oleg Grenrus + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/text-short-0.1.5/LICENSE b/docs/dependencies/licences/haskell/text-short-0.1.5/LICENSE new file mode 100644 index 0000000000..98d4b1ad78 --- /dev/null +++ b/docs/dependencies/licences/haskell/text-short-0.1.5/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2017, Herbert Valerio Riedel + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Herbert Valerio Riedel nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/th-abstraction-0.6.0.0/LICENSE b/docs/dependencies/licences/haskell/th-abstraction-0.6.0.0/LICENSE new file mode 100644 index 0000000000..70f6077c10 --- /dev/null +++ b/docs/dependencies/licences/haskell/th-abstraction-0.6.0.0/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2017-2020 Eric Mertens + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/docs/dependencies/licences/haskell/th-compat-0.1.4/LICENSE b/docs/dependencies/licences/haskell/th-compat-0.1.4/LICENSE new file mode 100644 index 0000000000..879f215afc --- /dev/null +++ b/docs/dependencies/licences/haskell/th-compat-0.1.4/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2020, Ryan Scott + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Ryan Scott nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/these-1.2/LICENSE b/docs/dependencies/licences/haskell/these-1.2/LICENSE new file mode 100644 index 0000000000..9d6d650165 --- /dev/null +++ b/docs/dependencies/licences/haskell/these-1.2/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2012, C. McCann, 2015-2019 Oleg Grenrus + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of C. McCann nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/time-compat-1.9.6.1/LICENSE b/docs/dependencies/licences/haskell/time-compat-1.9.6.1/LICENSE new file mode 100644 index 0000000000..910ef51bb4 --- /dev/null +++ b/docs/dependencies/licences/haskell/time-compat-1.9.6.1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2019 time contibutors, Oleg Grenrus + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Oleg Grenrus nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/time-manager-0.0.1/LICENSE b/docs/dependencies/licences/haskell/time-manager-0.0.1/LICENSE new file mode 100644 index 0000000000..d9f041796e --- /dev/null +++ b/docs/dependencies/licences/haskell/time-manager-0.0.1/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2012 Michael Snoyman, http://www.yesodweb.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/dependencies/licences/haskell/tls-1.9.0/LICENSE b/docs/dependencies/licences/haskell/tls-1.9.0/LICENSE new file mode 100644 index 0000000000..96ec822eb9 --- /dev/null +++ b/docs/dependencies/licences/haskell/tls-1.9.0/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010-2015 Vincent Hanquez <vincent@snarc.org> + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/transformers-base-0.4.6/LICENSE b/docs/dependencies/licences/haskell/transformers-base-0.4.6/LICENSE new file mode 100644 index 0000000000..9d51261b52 --- /dev/null +++ b/docs/dependencies/licences/haskell/transformers-base-0.4.6/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2011, Mikhail Vorozhtsov, Bas van Dijk +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +- Neither the names of the copyright owners nor the names of the + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/docs/dependencies/licences/haskell/transformers-compat-0.7.2/LICENSE b/docs/dependencies/licences/haskell/transformers-compat-0.7.2/LICENSE new file mode 100644 index 0000000000..50586d197c --- /dev/null +++ b/docs/dependencies/licences/haskell/transformers-compat-0.7.2/LICENSE @@ -0,0 +1,30 @@ +Copyright 2012-2015 Edward Kmett + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the author nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/type-equality-1/LICENSE b/docs/dependencies/licences/haskell/type-equality-1/LICENSE new file mode 100644 index 0000000000..c30f3e7d31 --- /dev/null +++ b/docs/dependencies/licences/haskell/type-equality-1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2009 Erik Hesselink, 2019 Oleg Grenrus, Ryan Scott + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of authors nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/typed-process-0.2.11.1/LICENSE b/docs/dependencies/licences/haskell/typed-process-0.2.11.1/LICENSE new file mode 100644 index 0000000000..50bb8ea0cf --- /dev/null +++ b/docs/dependencies/licences/haskell/typed-process-0.2.11.1/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2016 FP Complete, https://www.fpcomplete.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/dependencies/licences/haskell/unix-compat-0.7.1/LICENSE b/docs/dependencies/licences/haskell/unix-compat-0.7.1/LICENSE new file mode 100644 index 0000000000..cc3be3e947 --- /dev/null +++ b/docs/dependencies/licences/haskell/unix-compat-0.7.1/LICENSE @@ -0,0 +1,31 @@ +BSD 3-Clause License + +Copyright (c) 2007-2008, Björn Bringert +Copyright (c) 2007-2009, Duncan Coutts +Copyright (c) 2010-2011, Jacob Stanley +Copyright (c) 2011, Bryan O'Sullivan +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +- Neither the names of the copyright owners nor the names of the + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/unix-time-0.4.11/LICENSE b/docs/dependencies/licences/haskell/unix-time-0.4.11/LICENSE new file mode 100644 index 0000000000..5422193088 --- /dev/null +++ b/docs/dependencies/licences/haskell/unix-time-0.4.11/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2009, IIJ Innovation Institute Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/unliftio-0.2.25.0/LICENSE b/docs/dependencies/licences/haskell/unliftio-0.2.25.0/LICENSE new file mode 100644 index 0000000000..1349134e01 --- /dev/null +++ b/docs/dependencies/licences/haskell/unliftio-0.2.25.0/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2017 FP Complete + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/dependencies/licences/haskell/unliftio-core-0.2.1.0/LICENSE b/docs/dependencies/licences/haskell/unliftio-core-0.2.1.0/LICENSE new file mode 100644 index 0000000000..1349134e01 --- /dev/null +++ b/docs/dependencies/licences/haskell/unliftio-core-0.2.1.0/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2017 FP Complete + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/dependencies/licences/haskell/unordered-containers-0.2.19.1/LICENSE b/docs/dependencies/licences/haskell/unordered-containers-0.2.19.1/LICENSE new file mode 100644 index 0000000000..5eb7e1bdda --- /dev/null +++ b/docs/dependencies/licences/haskell/unordered-containers-0.2.19.1/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2010, Johan Tibell + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Johan Tibell nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/uuid-1.3.15/LICENSE b/docs/dependencies/licences/haskell/uuid-1.3.15/LICENSE new file mode 100644 index 0000000000..dc46bca24c --- /dev/null +++ b/docs/dependencies/licences/haskell/uuid-1.3.15/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2008, Antoine Latter + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + * The names of the authors may not be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/uuid-types-1.0.5.1/LICENSE b/docs/dependencies/licences/haskell/uuid-types-1.0.5.1/LICENSE new file mode 100644 index 0000000000..dc46bca24c --- /dev/null +++ b/docs/dependencies/licences/haskell/uuid-types-1.0.5.1/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2008, Antoine Latter + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + * The names of the authors may not be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/vector-0.13.1.0/LICENSE b/docs/dependencies/licences/haskell/vector-0.13.1.0/LICENSE new file mode 100644 index 0000000000..ef2a5268d6 --- /dev/null +++ b/docs/dependencies/licences/haskell/vector-0.13.1.0/LICENSE @@ -0,0 +1,32 @@ +Copyright (c) 2008-2012, Roman Leshchinskiy + 2020-2022, Alexey Kuleshevich + 2020-2022, Aleksey Khudyakov + 2020-2022, Andrew Lelechenko +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/docs/dependencies/licences/haskell/vector-algorithms-0.9.0.1/LICENSE b/docs/dependencies/licences/haskell/vector-algorithms-0.9.0.1/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/vector-stream-0.1.0.0/LICENSE b/docs/dependencies/licences/haskell/vector-stream-0.1.0.0/LICENSE new file mode 100644 index 0000000000..ef2a5268d6 --- /dev/null +++ b/docs/dependencies/licences/haskell/vector-stream-0.1.0.0/LICENSE @@ -0,0 +1,32 @@ +Copyright (c) 2008-2012, Roman Leshchinskiy + 2020-2022, Alexey Kuleshevich + 2020-2022, Aleksey Khudyakov + 2020-2022, Andrew Lelechenko +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +- Neither name of the University nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY COURT OF THE UNIVERSITY OF +GLASGOW AND THE CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +UNIVERSITY COURT OF THE UNIVERSITY OF GLASGOW OR THE CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/docs/dependencies/licences/haskell/websockets-0.12.7.3/LICENCE b/docs/dependencies/licences/haskell/websockets-0.12.7.3/LICENCE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/witherable-0.4.2/LICENSE b/docs/dependencies/licences/haskell/witherable-0.4.2/LICENSE new file mode 100644 index 0000000000..1994a4c649 --- /dev/null +++ b/docs/dependencies/licences/haskell/witherable-0.4.2/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2014, Fumiaki Kinoshita + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Fumiaki Kinoshita nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/yaml-0.11.11.2/LICENSE b/docs/dependencies/licences/haskell/yaml-0.11.11.2/LICENSE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/zip-2.0.0/LICENSE.md b/docs/dependencies/licences/haskell/zip-2.0.0/LICENSE.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/dependencies/licences/haskell/zlib-0.6.3.0/LICENSE b/docs/dependencies/licences/haskell/zlib-0.6.3.0/LICENSE new file mode 100644 index 0000000000..e4d25e0e0a --- /dev/null +++ b/docs/dependencies/licences/haskell/zlib-0.6.3.0/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2006-2016, Duncan Coutts +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. This clause is intentionally left blank. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/dependencies/licences/haskell/zstd-0.1.3.0/LICENSE b/docs/dependencies/licences/haskell/zstd-0.1.3.0/LICENSE new file mode 100644 index 0000000000..a793a80289 --- /dev/null +++ b/docs/dependencies/licences/haskell/zstd-0.1.3.0/LICENSE @@ -0,0 +1,30 @@ +BSD License + +For Zstandard software + +Copyright (c) 2016-present, Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/lang/cs/ANDROID.md b/docs/lang/cs/ANDROID.md index 4edfc3018c..3c401f1d1b 100644 --- a/docs/lang/cs/ANDROID.md +++ b/docs/lang/cs/ANDROID.md @@ -2,7 +2,7 @@ title: Přístup k souborům v aplikaci Android revision: 07.02.2023 --- -| Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/ANDROID.md) | +| Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/ANDROID.md), [PL](/docs/lang/pl/ANDROID.md) | # Přístup k souborům v aplikaci Android diff --git a/docs/lang/cs/CLI.md b/docs/lang/cs/CLI.md index aa5a2ba281..2477f6ea2f 100644 --- a/docs/lang/cs/CLI.md +++ b/docs/lang/cs/CLI.md @@ -2,7 +2,7 @@ title: SimpleX Chat terminálová revision: 31.01.2023 --- -| Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/CLI.md), [FR](/docs/lang/fr/CLI.md) | +| Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/CLI.md), [FR](/docs/lang/fr/CLI.md), [PL](/docs/lang/pl/CLI.md) | # SimpleX Chat terminálová (konzolová) aplikace pro Linux/MacOS/Windows @@ -97,7 +97,7 @@ git checkout stable DOCKER_BUILDKIT=1 docker build --output ~/.local/bin . ``` -> **Upozornění:** Pokud narazíte na chybu `` verze `GLIBC_2.28' nenalezena ``, obnovte jej pomocí základního obrazu `haskell:8.10.7-stretch` (změňte jej ve svém lokálním [Dockerfile](Dockerfile)). +> **Upozornění:** Pokud narazíte na chybu `` verze `GLIBC_2.28' nenalezena ``, obnovte jej pomocí základního obrazu `haskell:8.10.7-stretch` (změňte jej ve svém lokálním [Dockerfile](/Dockerfile)). #### V libovolném operačním systému @@ -117,7 +117,7 @@ git checkout stable apt-get update && apt-get install -y build-essential libgmp3-dev zlib1g-dev cp scripts/cabal.project.local.linux cabal.project.local # nebo na MacOS: -# brew install openssl@1.1 +# brew install openssl@3.0 # cp scripts/cabal.project.local.mac cabal.project.local # možná budete muset změnit cabal.project.local tak, aby ukazoval na skutečné umístění openssl cabal update diff --git a/docs/lang/cs/CONTRIBUTING.md b/docs/lang/cs/CONTRIBUTING.md index 26c746e7d2..226b4d9343 100644 --- a/docs/lang/cs/CONTRIBUTING.md +++ b/docs/lang/cs/CONTRIBUTING.md @@ -2,7 +2,7 @@ title: Průvodce přispíváním revision: 31.01.2023 --- -| Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/CONTRIBUTING.md), [FR](/docs/lang/fr/CONTRIBUTING.md) | +| Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/CONTRIBUTING.md), [FR](/docs/lang/fr/CONTRIBUTING.md), [PL](/docs/lang/pl/CONTRIBUTING.md) | # Průvodce přispíváním @@ -20,6 +20,6 @@ cp scripts/cabal.project.local.mac cabal.project.local Systém MacOS je standardně dodáván s LibreSSL, pro kompilaci SimpleX ze zdrojových kódů je nutné nainstalovat OpenSSL. -OpenSSL lze nainstalovat pomocí `brew install openssl@1.1`. +OpenSSL lze nainstalovat pomocí `brew install openssl@3.0`. -Aby vše fungovalo správně, musíte do své cesty PATH přidat `/opt/homebrew/opt/openssl@1.1/bin`. +Aby vše fungovalo správně, musíte do své cesty PATH přidat `/opt/homebrew/opt/openssl@3.0/bin`. diff --git a/docs/lang/cs/README.md b/docs/lang/cs/README.md index 9423cc96b8..7eab61395e 100644 --- a/docs/lang/cs/README.md +++ b/docs/lang/cs/README.md @@ -1,4 +1,4 @@ -| Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/README.md), [FR](/docs/lang/fr/README.md) | +| Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/README.md), [FR](/docs/lang/fr/README.md), [PL](/docs/lang/pl/README.md) | <img src="images/simplex-chat-logo.svg" alt="SimpleX logo" width="100%"> @@ -26,7 +26,7 @@ - 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) s novými funkcemi o 1-2 týdny dříve - **omezeno na 10 000 uživatelů**! - 🖥 K dispozici jako terminálová (konzolová) [aplikace / CLI](#zap-quick-installation-of-a-terminal-app) v systémech Linux, MacOS, Windows. -**NOVINKA**: Bezpečnostní audit od [Trail of Bits](https://www.trailofbits.com/about), [nové webové stránky](https://simplex.chat) a vydána verze 4.2! [Viz oznámení](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). +**NOVINKA**: Bezpečnostní audit od [Trail of Bits](https://www.trailofbits.com/about), [nové webové stránky](https://simplex.chat) a vydána verze 4.2! [Viz oznámení](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). ## Obsah @@ -62,23 +62,23 @@ Nestačí používat end-to-end šifrovaný messenger, všichni bychom měli pou ### Úplné soukromí vaší identity, profilu, kontaktů a metadat. -**Na rozdíl od všech ostatních existujících platforem pro zasílání zpráv nemá SimpleX přiřazeny žádné identifikátory uživatelů** - dokonce ani náhodná čísla. To chrání soukromí toho, s kým komunikujete, a skrývá to před servery platformy SimpleX i před jakýmikoli pozorovateli. [Více informací](./docs/SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata). +**Na rozdíl od všech ostatních existujících platforem pro zasílání zpráv nemá SimpleX přiřazeny žádné identifikátory uživatelů** - dokonce ani náhodná čísla. To chrání soukromí toho, s kým komunikujete, a skrývá to před servery platformy SimpleX i před jakýmikoli pozorovateli. [Více informací](./SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata). ### Nejlepší ochrana proti spamu a zneužití -Protože na platformě SimpleX nemáte žádný identifikátor, nelze vás kontaktovat, pokud nesdílíte odkaz na jednorázovou pozvánku nebo volitelnou dočasnou uživatelskou adresu. [Více informací](./docs/SIMPLEX.md#nejlepší-ochrana-před-spamem-a-zneužitím). +Protože na platformě SimpleX nemáte žádný identifikátor, nelze vás kontaktovat, pokud nesdílíte odkaz na jednorázovou pozvánku nebo volitelnou dočasnou uživatelskou adresu. [Více informací](./SIMPLEX.md#nejlepší-ochrana-proti-spamu-a-zneužití). ### Úplné vlastnictví, kontrola a zabezpečení vašich dat -SimpleX ukládá všechna uživatelská data na klientských zařízeních, zprávy jsou pouze dočasně uchovávány na relay serverech SimpleX, dokud nejsou přijaty. [Více informací](./docs/SIMPLEX.md#complete-ownership-control-and-security-of-your-data). +SimpleX ukládá všechna uživatelská data na klientských zařízeních, zprávy jsou pouze dočasně uchovávány na relay serverech SimpleX, dokud nejsou přijaty. [Více informací](./SIMPLEX.md#complete-ownership-control-and-security-of-your-data). ### Uživatelé vlastní síť SimpleX -Můžete používat SimpleX s vlastními servery a přitom komunikovat s lidmi, kteří používají servery předkonfigurované v aplikacích nebo jakékoli jiné servery SimpleX. [Více informací](./docs/SIMPLEX.md#users-own-simplex-network). +Můžete používat SimpleX s vlastními servery a přitom komunikovat s lidmi, kteří používají servery předkonfigurované v aplikacích nebo jakékoli jiné servery SimpleX. [Více informací](./SIMPLEX.md#users-own-simplex-network). ## Často kladené otázky -1. _Jak může SimpleX doručovat zprávy bez identifikátorů uživatelů?_ Viz [oznámení o vydání v2](./blog/20220511-simplex-chat-v2-images-files.md#prvni-platforma-zasilani-zpráv-bez-identifikátoru-uživatele), kde je vysvětleno, jak SimpleX funguje. +1. _Jak může SimpleX doručovat zprávy bez identifikátorů uživatelů?_ Viz [oznámení o vydání v2](../../../blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers), kde je vysvětleno, jak SimpleX funguje. 2. _Proč bych neměl používat jen Signal?_ Signal je centralizovaná platforma, která k identifikaci svých uživatelů a jejich kontaktů používá telefonní čísla. To znamená, že zatímco obsah vašich zpráv na službě Signal je chráněn robustním šifrováním end-to-end, pro službu Signal je viditelné velké množství metadat - s kým a kdy hovoříte. @@ -88,17 +88,17 @@ Můžete používat SimpleX s vlastními servery a přitom komunikovat s lidmi, Poslední aktualizace: V současné době je k dispozici několik nových aplikací, např: -[Vydání verze 4.5 - s více uživatelskými profily, návrhem zpráv, izolací transportu a italským rozhraním](./blog/20230204-simplex-chat-v4-5-user-chat-profiles.md). +[Vydání verze 4.5 - s více uživatelskými profily, návrhem zpráv, izolací transportu a italským rozhraním](../../../blog/20230204-simplex-chat-v4-5-user-chat-profiles.md). -[03. 01. 2023. v4.4 vydána - s mizejícími zprávami, "živými" zprávami, bezpečnostním ověřováním spojení, GIFy a nálepkami a s francouzským jazykem rozhraní](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md). +[03. 01. 2023. v4.4 vydána - s mizejícími zprávami, "živými" zprávami, bezpečnostním ověřováním spojení, GIFy a nálepkami a s francouzským jazykem rozhraní](../../../blog/20230103-simplex-chat-v4.4-disappearing-messages.md). -[prosinec 06, 2022. Listopadové recenze a vydána verze 4.3 - s okamžitými hlasovými zprávami, nevratným mazáním odeslaných zpráv a vylepšenou konfigurací serveru](./blog/20221206-simplex-chat-v4.3-hlasove-zpravy.md). +[prosinec 06, 2022. Listopadové recenze a vydána verze 4.3 - s okamžitými hlasovými zprávami, nevratným mazáním odeslaných zpráv a vylepšenou konfigurací serveru](../../../blog/20221206-simplex-chat-v4.3-voice-messages.md). -[Nov 08, 2022. Bezpečnostní audit Trail of Bits, vydány nové webové stránky a verze 4.2](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). +[Nov 08, 2022. Bezpečnostní audit Trail of Bits, vydány nové webové stránky a verze 4.2](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). -[28. 9. 2022. v4.0: šifrovaná lokální databáze chatu a mnoho dalších změn](./blog/20220928-simplex-chat-v4-encrypted-database.md). +[28. 9. 2022. v4.0: šifrovaná lokální databáze chatu a mnoho dalších změn](../../../blog/20220928-simplex-chat-v4-encrypted-database.md). -[Všechny aktualizace](./blog) +[Všechny aktualizace](../../../blog) ## Vytvoření soukromého připojení @@ -118,13 +118,13 @@ Po instalaci chatovacího klienta jednoduše spusťte `simplex-chat` z terminál ![simplex-chat](./images/connection.gif) -Více informací o [instalaci a používání terminálové aplikace](./docs/CLI.md). +Více informací o [instalaci a používání terminálové aplikace](./CLI.md). ## Návrh platformy SimpleX SimpleX je síť klient-server s unikátní topologií sítě, která využívá redundantní, jednorázové uzly pro předávání zpráv (relay nodes) k asynchronnímu předávání zpráv prostřednictvím jednosměrných (simplexních) front zpráv, což zajišťuje anonymitu příjemce i odesílatele. -Na rozdíl od sítí P2P jsou všechny zprávy předávány přes jeden nebo několik serverových uzlů, které ani nemusí mít perzistenci. Současná implementace [SMP serveru](https://github.com/simplex-chat/simplexmq#smp-server) ve skutečnosti používá ukládání zpráv v paměti a uchovává pouze záznamy o frontách. SimpleX poskytuje lepší ochranu metadat než návrhy P2P, protože k doručování zpráv se nepoužívají globální identifikátory účastníků, a vyhýbá se [problémům sítí P2P](./docs/SIMPLEX.md#comparison-with-p2p-messaging-protocols). +Na rozdíl od sítí P2P jsou všechny zprávy předávány přes jeden nebo několik serverových uzlů, které ani nemusí mít perzistenci. Současná implementace [SMP serveru](https://github.com/simplex-chat/simplexmq#smp-server) ve skutečnosti používá ukládání zpráv v paměti a uchovává pouze záznamy o frontách. SimpleX poskytuje lepší ochranu metadat než návrhy P2P, protože k doručování zpráv se nepoužívají globální identifikátory účastníků, a vyhýbá se [problémům sítí P2P](./SIMPLEX.md#comparison-with-p2p-messaging-protocols). Na rozdíl od federativních sítí nemají uzly serveru **záznamy o uživatelích**, **nekomunikují mezi sebou** a **neukládají zprávy** po jejich doručení příjemcům. Neexistuje způsob, jak zjistit úplný seznam serverů účastnících se sítě SimpleX. Tato konstrukce se vyhýbá problému viditelnosti metadat, který mají všechny federované sítě, a lépe chrání před útoky na celou síť. @@ -132,7 +132,7 @@ Informace o uživatelích, jejich kontaktech a skupinách mají pouze klientská Další informace o cílech a technickém návrhu platformy naleznete v dokumentu [SimpleX whitepaper](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md). -Formát zpráv zasílaných mezi klienty chatu prostřednictvím [SimpleX Messaging Protocol](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md) viz [SimpleX Chat Protocol](./docs/protocol/simplex-chat.md). +Formát zpráv zasílaných mezi klienty chatu prostřednictvím [SimpleX Messaging Protocol](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md) viz [SimpleX Chat Protocol](../../protocol/simplex-chat.md). ## Soukromí: technické podrobnosti a omezení @@ -149,7 +149,7 @@ Co je již implementováno: 6. Počínaje verzí v2 protokolu SMP (současná verze je v4) jsou všechna metadata zprávy včetně času, kdy byla zpráva přijata serverem (zaokrouhleno na sekundy), odesílána příjemcům uvnitř šifrované obálky, takže ani v případě kompromitace TLS je nelze pozorovat. 7. Pro spojení klient-server je povoleno pouze TLS 1.2/1.3, omezené na kryptografické algoritmy: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448. 8. Na ochranu proti útokům typu replay vyžadují servery SimpleX [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) jako ID relace v každém klientském příkazu podepsaném efemérním klíčem per-queue. -9. Pro ochranu vaší IP adresy podporují všichni klienti SimpleX Chat přístup k serverům pro zasílání zpráv přes Tor - více informací najdete v [oznámení o vydání v3.1](./blog/20220808-simplex-chat-v3.1-chat-groups.md). +9. Pro ochranu vaší IP adresy podporují všichni klienti SimpleX Chat přístup k serverům pro zasílání zpráv přes Tor - více informací najdete v [oznámení o vydání v3.1](../../../blog/20220808-simplex-chat-v3.1-chat-groups.md). 10. Šifrování místní databáze s přístupovou frází - kontakty, skupiny a všechny odeslané a přijaté zprávy jsou uloženy šifrovaně. Pokud jste používali SimpleX Chat před verzí 4.0, musíte šifrování povolit prostřednictvím nastavení aplikace. 11. Izolace transportu - pro provoz různých uživatelských profilů se používají různá spojení TCP a okruhy Tor, volitelně - pro různá spojení kontaktů a členů skupin. @@ -166,7 +166,7 @@ Můžete: - použít knihovnu SimpleX Chat k integraci funkcí chatu do svých mobilních aplikací. - vytvářet chatovací boty a služby v jazyce Haskell - viz [simple](./apps/simplex-bot/) a více [advanced chat bot example](./apps/simplex-bot-advanced/). - vytvářet chatovací boty a služby v libovolném jazyce se spuštěným terminálem SimpleX Chat CLI jako lokálním serverem WebSocket. Viz [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) a [JavaScript chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js). -- spustit [simplex-chat terminal CLI](./docs/CLI.md) pro provádění jednotlivých příkazů chatu, např. pro odesílání zpráv v rámci provádění shellových skriptů. +- spustit [simplex-chat terminal CLI](./CLI.md) pro provádění jednotlivých příkazů chatu, např. pro odesílání zpráv v rámci provádění shellových skriptů. Pokud uvažujete o vývoji s platformou SimpleX, obraťte se na nás pro případné rady a podporu. @@ -228,7 +228,7 @@ Pokud uvažujete o vývoji s platformou SimpleX, obraťte se na nás pro přípa ## Připojte se ke skupině uživatelů -Pokud se chcete na něco zeptat, můžete se připojit k anglicky mluvící skupině: [#SimpleX-Group-2](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FQP8zaGjjmlXV-ix_Er4JgJ0lNPYGS1KX%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEApAgBkRZ3x12ayZ7sHrjHQWNMvqzZpWUgM_fFCUdLXwo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xWpPXEZZsQp_F7vwAcAYDw%3D%3D%22%7D) +Pokud se chcete na něco zeptat, můžete se připojit k anglicky mluvící skupině: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D) Existuje také několik skupin v jiných jazycích než v angličtině, do kterých jsme nechali přeložit rozhraní aplikací. Tyto skupiny slouží k testování a pokládání otázek ostatním uživatelům SimpleX Chat. Ne vždy v nich odpovídáme na dotazy, proto je prosím pokládejte v některé z anglicky mluvících skupin. @@ -253,7 +253,7 @@ Aktuální jazyky rozhraní: - Italština: [@unbranched](https://github.com/unbranched) - Ruština: projektový tým -Jazyky ve vývoji: Čínština, hindština, čeština, japonština, holandština a [mnoho dalších](https://hosted.weblate.org/projects/simplex-chat/#languages). Další jazyky budeme přidávat, jakmile budou některé z již přidaných jazyků dokončeny - navrhněte prosím nové jazyky, projděte si [průvodce překladem](./docs/TRANSLATIONS.md) a kontaktujte nás! +Jazyky ve vývoji: Čínština, hindština, čeština, japonština, holandština a [mnoho dalších](https://hosted.weblate.org/projects/simplex-chat/#languages). Další jazyky budeme přidávat, jakmile budou některé z již přidaných jazyků dokončeny - navrhněte prosím nové jazyky, projděte si [průvodce překladem](./TRANSLATIONS.md) a kontaktujte nás! ## Přispívejte @@ -294,7 +294,7 @@ Zakladatel SimpleX Chat Protokoly a bezpečnostní model [SimpleX](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) byly revidovány a ve verzi 1.0.0 došlo k mnoha zlomovým změnám a vylepšením. -Bezpečnostní audit provedla v říjnu 2022 společnost [Trail of Bits](https://www.trailofbits.com/about) a většina oprav byla vydána ve verzi 4.2.0 - viz [oznámení](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). +Bezpečnostní audit provedla v říjnu 2022 společnost [Trail of Bits](https://www.trailofbits.com/about) a většina oprav byla vydána ve verzi 4.2.0 - viz [oznámení](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). SimpleX Chat je stále relativně ranou fází platformy (mobilní aplikace byly vydány v březnu 2022), takže můžete objevit některé chyby a chybějící funkce. Velmi oceníme, pokud nám dáte vědět o všem, co je třeba opravit nebo vylepšit. diff --git a/docs/lang/cs/SERVER.md b/docs/lang/cs/SERVER.md index f8258909bc..f75adeb8cf 100644 --- a/docs/lang/cs/SERVER.md +++ b/docs/lang/cs/SERVER.md @@ -2,7 +2,7 @@ title: Hostování vlastního serveru SMP revision: 05.06.2023 --- -| Aktualizováno 05.06.2023 | Jazyky: CZ, [EN](/docs/SERVER.md), [FR](/docs/lang/fr/SERVER.md) | +| Aktualizováno 05.06.2023 | Jazyky: CZ, [EN](/docs/SERVER.md), [FR](/docs/lang/fr/SERVER.md), [PL](/docs/lang/pl/SERVER.md) | # Hostování vlastního serveru SMP @@ -12,7 +12,7 @@ SMP server je relay server používaný k předávání zpráv v síti SimpleX. Klienti SimpleX pouze určují, který server bude použit pro příjem zpráv, a to pro každý kontakt (nebo spojení skupiny s členem skupiny) zvlášť, přičemž tyto servery jsou pouze dočasné, protože adresa pro doručování se může změnit. -_Upozornění_: když změníte servery v konfiguraci aplikace, ovlivní to pouze to, který server bude použit pro nové kontakty, stávající kontakty se na nové servery automaticky nepřesunou, ale můžete je přesunout ručně pomocí tlačítka ["Změnit adresu příjmu"](../blog/20221108-simplex-chat-v4.2-bezpecnostni-audit-novy-website.md#zmeny-dorucovani-adresy-beta) na stránkách s informacemi o kontaktech/členech - brzy bude automatizováno. +_Upozornění_: když změníte servery v konfiguraci aplikace, ovlivní to pouze to, který server bude použit pro nové kontakty, stávající kontakty se na nové servery automaticky nepřesunou, ale můžete je přesunout ručně pomocí tlačítka ["Změnit adresu příjmu"](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md#change-your-delivery-address-beta) na stránkách s informacemi o kontaktech/členech - brzy bude automatizováno. ## Instalace diff --git a/docs/lang/cs/SIMPLEX.md b/docs/lang/cs/SIMPLEX.md index 33d45cec65..0d32fd06d9 100644 --- a/docs/lang/cs/SIMPLEX.md +++ b/docs/lang/cs/SIMPLEX.md @@ -2,7 +2,7 @@ title: Platforma SimpleX revision: 07.02.2023 --- -| Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/SIMPLEX.md), [FR](/docs/lang/fr/SIMPLEX.md) | +| Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/SIMPLEX.md), [FR](/docs/lang/fr/SIMPLEX.md), [PL](/docs/lang/pl/SIMPLEX.md) | # Platforma SimpleX - motivace a srovnání diff --git a/docs/lang/cs/TRANSLATIONS.md b/docs/lang/cs/TRANSLATIONS.md index b260bd12a6..c22979bdfc 100644 --- a/docs/lang/cs/TRANSLATIONS.md +++ b/docs/lang/cs/TRANSLATIONS.md @@ -2,7 +2,7 @@ title: Přispívání překladů do SimpleX Chat revision: 07.02.2023 --- -| Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/TRANSLATIONS.md) | +| Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/TRANSLATIONS.md), [PL](/docs/lang/pl/TRANSLATIONS.md) | # Přispívání překladů do SimpleX Chat diff --git a/docs/lang/cs/WEBRTC.md b/docs/lang/cs/WEBRTC.md index 77df920255..df63c3205e 100644 --- a/docs/lang/cs/WEBRTC.md +++ b/docs/lang/cs/WEBRTC.md @@ -2,7 +2,7 @@ title: Použití vlastních serverů WebRTC ICE v SimpleX Chat revision: 31.01.2023 --- -| Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/WEBRTC.md), [FR](/docs/lang/fr/WEBRTC.md) | +| Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/WEBRTC.md), [FR](/docs/lang/fr/WEBRTC.md), [PL](/docs/lang/pl/WEBRTC.md) | # Použití vlastních serverů WebRTC ICE v SimpleX Chat diff --git a/docs/lang/fr/ANDROID.md b/docs/lang/fr/ANDROID.md index 0f710542a8..1d100c5c04 100644 --- a/docs/lang/fr/ANDROID.md +++ b/docs/lang/fr/ANDROID.md @@ -2,7 +2,7 @@ title: Accès aux fichiers dans l'application Android revision: 07.02.2023 --- -| 07.02.2023 | FR, [EN](/docs/ANDROID.md), [CZ](/docs/lang/cs/ANDROID.md) | +| 07.02.2023 | FR, [EN](/docs/ANDROID.md), [CZ](/docs/lang/cs/ANDROID.md), [PL](/docs/lang/pl/ANDROID.md) | # Accès aux fichiers dans l'application Android diff --git a/docs/lang/fr/CLI.md b/docs/lang/fr/CLI.md index 6fe9c86d75..58b84a0919 100644 --- a/docs/lang/fr/CLI.md +++ b/docs/lang/fr/CLI.md @@ -2,7 +2,7 @@ title: Application de terminal revision: 31.01.2023 --- -| 31.01.2023 | FR, [EN](/docs/CLI.md), [CZ](/docs/lang/cs/CLI.md) | +| 31.01.2023 | FR, [EN](/docs/CLI.md), [CZ](/docs/lang/cs/CLI.md), [PL](/docs/lang/pl/CLI.md) | # Application de terminal (console) SimpleX Chat pour Linux/MacOS/Windows @@ -97,7 +97,7 @@ git checkout stable DOCKER_BUILDKIT=1 docker build --output ~/.local/bin . ``` -> **Veuillez noter** : Si vous rencontrez l'erreur ``version `GLIBC_2.28' non trouvée``, reconstruisez-le avec l'image de base `haskell:8.10.7-stretch`(changez-la dans votre [Dockerfile](Dockerfile) local). +> **Veuillez noter** : Si vous rencontrez l'erreur ``version `GLIBC_2.28' non trouvée``, reconstruisez-le avec l'image de base `haskell:8.10.7-stretch`(changez-la dans votre [Dockerfile](/Dockerfile) local). #### Utiliser Haskell stack @@ -119,7 +119,7 @@ git checkout stable apt-get update && apt-get install -y build-essential libgmp3-dev zlib1g-dev cp scripts/cabal.project.local.linux cabal.project.local # ou sur MacOS: -# brew install openssl@1.1 +# brew install openssl@3.0 # cp scripts/cabal.project.local.mac cabal.project.local # vous devrez peut-être modifier cabal.project.local pour indiquer l'emplacement réel d'openssl cabal update diff --git a/docs/lang/fr/CONTRIBUTING.md b/docs/lang/fr/CONTRIBUTING.md index 81515b09b2..1b83fc24ce 100644 --- a/docs/lang/fr/CONTRIBUTING.md +++ b/docs/lang/fr/CONTRIBUTING.md @@ -2,7 +2,7 @@ title: Guide pour contribuer revision: 31.01.2023 --- -| 31.01.2023 | FR, [EN](/docs/CONTRIBUTING.md), [CZ](/docs/lang/cs/CONTRIBUTING.md) | +| 31.01.2023 | FR, [EN](/docs/CONTRIBUTING.md), [CZ](/docs/lang/cs/CONTRIBUTING.md), [PL](/docs/lang/pl/CONTRIBUTING.md) | # Guide pour contribuer @@ -20,6 +20,6 @@ cp scripts/cabal.project.local.mac cabal.project.local LibreSSL est fourni par défaut sur MacOS, OpenSSL doit être installé pour compiler SimpleX à partir de la source. -OpenSSL peut être installé avec `brew install openssl@1.1` +OpenSSL peut être installé avec `brew install openssl@3.0` -Vous devez ajouter `/opt/homebrew/opt/openssl@1.1/bin` à votre PATH pour que tout fonctionne correctement. +Vous devez ajouter `/opt/homebrew/opt/openssl@3.0/bin` à votre PATH pour que tout fonctionne correctement. diff --git a/docs/lang/fr/README.md b/docs/lang/fr/README.md index 2f11fe9539..69401da5a2 100644 --- a/docs/lang/fr/README.md +++ b/docs/lang/fr/README.md @@ -4,7 +4,7 @@ [![Join on Reddit](https://img.shields.io/reddit/subreddit-subscribers/SimpleXChat?style=social)](https://www.reddit.com/r/SimpleXChat) [![Follow on Mastodon](https://img.shields.io/mastodon/follow/108619463746856738?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@simplex) -| 30/03/2023 | FR, [EN](/README.md), [CZ](/docs/lang/cs/README.md) | +| 30/03/2023 | FR, [EN](/README.md), [CZ](/docs/lang/cs/README.md), [PL](/docs/lang/pl/README.md) | <img src="images/simplex-chat-logo.svg" alt="SimpleX logo" width="100%"> @@ -48,7 +48,7 @@ ## Rejoindre des groupes d'utilisateurs -Vous pouvez rejoindre un groupe anglophone pour poser des questions : [#SimpleX-Group-2](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FQP8zaGjjmlXV-ix_Er4JgJ0lNPYGS1KX%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEApAgBkRZ3x12ayZ7sHrjHQWNMvqzZpWUgM_fFCUdLXwo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xWpPXEZZsQp_F7vwAcAYDw%3D%3D%22%7D) +Vous pouvez rejoindre un groupe anglophone pour poser des questions : [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D) Il existe des groupes dans d'autres langues, dans lesquelles l'interface de l'application a été traduite. Ces groupes servent à tester et à poser des questions aux autres utilisateurs de SimpleX Chat : diff --git a/docs/lang/fr/SERVER.md b/docs/lang/fr/SERVER.md index ac9cec7eb9..7bbe315273 100644 --- a/docs/lang/fr/SERVER.md +++ b/docs/lang/fr/SERVER.md @@ -2,7 +2,7 @@ title: Héberger votre propre serveur SMP revision: 05.06.2023 --- -| 05.06.2023 | FR, [EN](/docs/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) | +| 05.06.2023 | FR, [EN](/docs/SERVER.md), [CZ](/docs/lang/cs/SERVER.md), [PL](/docs/lang/pl/SERVER.md) | # Héberger votre propre serveur SMP diff --git a/docs/lang/fr/SIMPLEX.md b/docs/lang/fr/SIMPLEX.md index a7134205d0..aacc645054 100644 --- a/docs/lang/fr/SIMPLEX.md +++ b/docs/lang/fr/SIMPLEX.md @@ -2,7 +2,7 @@ title: Plateforme SimpleX revision: 07.02.2023 --- -| 07.02.2023 | FR, [EN](/docs/SIMPLEX.md), [CZ](/docs/lang/cs/SIMPLEX.md) | +| 07.02.2023 | FR, [EN](/docs/SIMPLEX.md), [CZ](/docs/lang/cs/SIMPLEX.md), [PL](/docs/lang/pl/SIMPLEX.md) | # Plateforme SimpleX - motivation et comparaison diff --git a/docs/lang/fr/TRANSLATIONS.md b/docs/lang/fr/TRANSLATIONS.md index e85a4a8513..1e216900e7 100644 --- a/docs/lang/fr/TRANSLATIONS.md +++ b/docs/lang/fr/TRANSLATIONS.md @@ -2,7 +2,7 @@ title: Contribuer aux traductions de SimpleX Chat revision: 19.03.2023 --- -| 19.03.2023 | FR, [EN](/docs/TRANSLATIONS.md), [CZ](/docs/lang/cs/TRANSLATIONS.md) | +| 19.03.2023 | FR, [EN](/docs/TRANSLATIONS.md), [CZ](/docs/lang/cs/TRANSLATIONS.md), [PL](/docs/lang/pl/TRANSLATIONS.md) | # Contribuer aux traductions de SimpleX Chat diff --git a/docs/lang/fr/WEBRTC.md b/docs/lang/fr/WEBRTC.md index 47296274b3..381677b3f6 100644 --- a/docs/lang/fr/WEBRTC.md +++ b/docs/lang/fr/WEBRTC.md @@ -2,7 +2,7 @@ title: Utilisation de serveurs WebRTC ICE personnalisés dans SimpleX Chat revision: 31.01.2023 --- -| 31.01.2023 | FR, [EN](/docs/WEBRTC.md), [CZ](/docs/lang/cs/WEBRTC.md) | +| 31.01.2023 | FR, [EN](/docs/WEBRTC.md), [CZ](/docs/lang/cs/WEBRTC.md), [PL](/docs/lang/pl/WEBRTC.md) | # Utilisation de serveurs WebRTC ICE personnalisés dans SimpleX Chat diff --git a/docs/lang/pl/ANDROID.md b/docs/lang/pl/ANDROID.md new file mode 100644 index 0000000000..ce422ab3b5 --- /dev/null +++ b/docs/lang/pl/ANDROID.md @@ -0,0 +1,58 @@ +--- +title: Dostęp do plików w aplikacji Androidowej +revision: 07.02.2023 +--- + +| 07.02.2023 | PL, [EN](/docs/ANDROID.md), [CZ](/docs/lang/cs/ANDROID.md), [FR](/docs/lang/fr/ANDROID.md) | + +# Dostęp do plików w aplikacji Androidowej + +SimpleX wykorzystuje bazy danych i przechowuje ustawienia w prywatnym katalogu w systemie Android. Katalog ten zawiera: + +- bazy danych +- wysłane i odebrane pliki +- pliki tymczasowe, które zostaną usunięte, gdy nie są już potrzebne +- ustawienia użytkownika. + +Jeśli chcesz zobaczyć, co jest przechowywane w katalogu SimpleX, musisz mieć: + +- System operacyjny oparty na systemie Unix (lub [MinGW](https://www.mingw-w64.org/downloads/) na Windowsie) +- narzędzie ADB (Android Debug Bridge) zainstalowane na komputerze ([pobierz je tutaj](https://developer.android.com/studio/releases/platform-tools) i zainstaluj) +- urządzenie podłączone przez USB lub Wi-Fi do komputera. + +## Proces: + +- otwórz SimpleX, przejdź do `Hasło do bazy danych i eksport`, włącz `Kopia zapasowa danych aplikacji`. To sprawi, że następne kroki będą działać. +- _opcjonalnie_: jeśli chcesz wyświetlić zawartość bazy danych, zmień hasło bazy danych z losowego na swoje. Aby to zrobić, zatrzymaj czat na ekranie `Hasło do bazy danych i eksport`, otwórz `Hasło do bazy danych`, wprowadź nowe hasło i potwierdź je, a następnie zatwierdź. Nie zapomnij go, w przeciwnym razie utracisz wszystkie dane w przypadku, gdy zostaniesz ponownie poproszony o hasło. +- otwórz emulator terminala (Windows CMD/Powershell nie zadziała) i zmień katalog na ten, którego chcesz użyć do przechowywania kopii zapasowej: + +```bash +cd /tmp # to tylko przykład +``` +Następne uruchom: +```bash +adb -d backup -f chat.ab -noapk chat.simplex.app && +tail -n +5 chat.ab > chat.dat && +printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" | cat - chat.dat > chat.gz && +tar -xvzf chat.gz +``` + +Teraz odblokuj urządzenie i potwierdź operację tworzenia kopii zapasowej bez użycia hasła do szyfrowania, w przeciwnym razie polecenia nie będą działać. + +Po tym kopia zapasowa powinna zostać zrobiona. Jeśli pojawi się błąd `tar: Error is not recoverable: exiting now`, ale wcześniej pojawiło się kilka nazw plików, nie martw się, wszystko jest w porządku. + +Teraz zapisane pliki będą w `./apps/chat.simplex.app/`. + +Pamiętaj, że jeśli korzystasz z nowej wersji SimpleX, bazy danych będą zaszyfrowane i nie będziesz w stanie przeglądać ich zawartości bez użycia aplikacji `sqlcipher` oraz gdy nie znasz hasła deszyfrującego (musisz najpierw zmienić je na swoje z losowo wygenerowanego w aplikacji). + +## Odszyfrowywanie baz danych + +Aby wyświetlić dane bazy danych, należy je najpierw odszyfrować. Zainstaluj `sqlcipher` używając ulubionego menedżera pakietów i uruchom następujące polecenia w katalogu z bazami danych: +```bash +sqlcipher files_chat.db +pragma key="youDecryptionPassphrase"; +# Upewnij się, że to działa +select * from users; +``` + +Jeśli zobaczysz `Parse error: no such table: users`, upewnij się, że wprowadzono prawidłowe hasło i zostało ono zmienione z losowego w aplikacji na Androida (jeśli oczywiście pobrano tę bazę danych z urządzenia z Androidem). diff --git a/docs/lang/pl/CLI.md b/docs/lang/pl/CLI.md new file mode 100644 index 0000000000..bc64b04415 --- /dev/null +++ b/docs/lang/pl/CLI.md @@ -0,0 +1,244 @@ +--- +title: Aplikacja konsolowa +revision: 31.01.2023 +--- + +| Updated 31.01.2023 | Języki: PL, [EN](/docs/CLI.md), [FR](/docs/lang/fr/CLI.md), [CZ](/docs/lang/cs/CLI.md) | + +# Terminalowa (konsolowa) aplikacja SimpleX Chat dla systemów Linux/MacOS/Windows + +## Spis treści + +- [Funkcje czatu w terminalu](#funkcje-czatu-w-terminalu) +- [Instalacja](#🚀-instalacja) + - [Pobieranie klienta czatu](#pobieranie-klienta-czatu) + - [Linux i MacOS](#linux-i-macos) + - [Windows](#windows) + - [Budowanie z kodu źródłowego](#budowanie-z-kodu-źródłowego) + - [Używając dockera](#using-docker) + - [Używając Haskella na dowolnym systemie operacyjnym](#używając-haskella-na-dowolnym-systemie-operacyjnym) +- [Używanie](#używanie) + - [Używanie klienta czatu](#używanie-klienta-czatu) + - [Dostęp do serwerów wiadomości przez Tor](#dostęp-do-serwerów-wiadomości-przez-tor) + - [Jak używać czatu SimpleX](#jak-używać-czatu-simplex) + - [Grupy](#grupy) + - [Wysyłanie plików](#wysyłanie-plików) + - [Adresy kontaktowe użytkowników](#adresy-kontaktowe-użytkowników) + +## Funkcje czatu w terminalu + +- Konwersacje 1 na 1 z wieloma osobami w tym samym oknie terminala. +- Wiadomości grupowe. +- Wysyłanie plików do kontaktów i grup. +- Adresy kontaktowe użytkowników - nawiązywanie połączeń za pomocą linków kontaktowych wielokrotnego użytku. +- Wiadomości przechowywane w lokalnej bazie danych SQLite. +- Automatycznie wypełniana nazwa odbiorcy - po nawiązaniu połączenia wystarczy po prostu napisać wiadomość, aby odpowiedzieć nadawcy. +- Dostępne wstępnie skonfigurowane przykładowe serwery SMP - można też użyć [własnego serwera](https://github.com/simplex-chat/simplexmq#using-smp-server-and-smp-agent). +- Żadna globalna tożsamość ani nazwy użytkowników nie są widoczne dla serwera (serwerów), co zapewnia pełną prywatność kontaktów i rozmów. +- Dwie warstwy szyfrowania E2E (double-ratchet dla połączeń dwukierunkowych, przy użyciu negocjacji klucza X3DH z efemerycznymi kluczami Curve448 i NaCl crypto_box dla kolejek SMP, przy użyciu kluczy Curve25519) oraz przekazywanie kluczy odbiorców za pomocą komunikacji out-of-band (zobacz [Jak używać czatu SimpleX](#how-to-use-simplex-chat)). +- Weryfikacja integralności wiadomości (poprzez uwzględnienie hashu poprzedniej wiadomości). +- Uwierzytelnianie każdego polecenia/wiadomości przez serwery SMP za pomocą automatycznie generowanych kluczy Ed448. +- Szyfrowanie transmisji przy użyciu TLS 1.3. +- Dodatkowe szyfrowanie wiadomości z serwera SMP do odbiorcy aby utrudnić możliwość korelacji ruchu. + +Klucze publiczne biorące udział podczas wymiany kluczy nie są używane jako tożsamość, są one generowane losowo dla każdego kontaktu. + +Aby uzyskać szczegółowe informacje techniczne zobacz [używane metody szyfrowania](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md#encryption-primitives-used). + +<a name="🚀-installation"></a> + +## 🚀 Instalacja + +### Pobieranie klienta czatu + +#### Linux i MacOS + +By **zainstalować** lub **zaktualizować** `simplex-chat`, należy uruchomić skrypt instalacyjny. Aby to zrobić, użyj następującego polecenia cURL lub Wget: + +```sh +curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/stable/install.sh | bash +``` + +```sh +wget -qO- https://raw.githubusercontent.com/simplex-chat/simplex-chat/stable/install.sh | bash +``` + +Po pobraniu klienta czatu można go uruchomić za pomocą polecenia `simplex-chat`. + +Możesz również ręcznie pobrać plik binarny czatu dla swojego systemu z [najnowszej stabilnej wersji](https://github.com/simplex-chat/simplex-chat/releases) i uczynić go uruchamialnym w sposób pokazany poniżej. + +```sh +chmod +x <binary> +mv <binary> ~/.local/bin/simplex-chat +``` + +(lub użyj innej preferowanej lokalizacji w `PATH`). + +Na MacOS musisz również [zezwolić Gatekeeperowi, by go uruchomić](https://support.apple.com/en-us/HT202491). + +#### Windows + +```sh +move <binary> %APPDATA%/local/bin/simplex-chat.exe +``` + +### Budowanie z kodu źródłowego + +> **Uwaga:** aby zbudować aplikację użyj [wersji stabilnej](https://github.com/simplex-chat/simplex-chat/tree/stable). + +#### Używając Dockera + +Na Linuxie, aby zbudować plik wykonywalny możesz użyć [docker build z customowym outputem](https://docs.docker.com/engine/reference/commandline/build/#custom-build-outputs): + +```shell +git clone git@github.com:simplex-chat/simplex-chat.git +cd simplex-chat +git checkout stable +DOCKER_BUILDKIT=1 docker build --output ~/.local/bin . +``` + +> **Uwaga:** Jeśli napotkasz błąd `` version `GLIBC_2.28' not found ``, przebuduj go z obrazem bazowym `haskell:8.10.7-stretch` (zmień go w Twoim lokalnym pliku [Dockerfile](/Dockerfile)). + +#### Używając Haskella na dowolnym systemie operacyjnym + +1. Zainstaluj [Haskell GHCup](https://www.haskell.org/ghcup/), GHC 9.6.3 i cabal 3.10.1.0: + +```shell +curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh +``` + +Możesz użyć polecenia `ghcup tui`, aby sprawdzić lub dodać wersje GHC i cabal. + +2. Sklonuj kod źródłowy: + +```shell +git clone git@github.com:simplex-chat/simplex-chat.git +cd simplex-chat +git checkout stable +# lub aby zbudować konkretną wersję: +# git checkout v5.3.0-beta.8 +``` + +`master` to branch deweloperski, może on zawierać niestabilny kod. + +3. Przygotowywanie systemu: + +Na Linuxie: + +```shell +apt-get update && apt-get install -y build-essential libgmp3-dev zlib1g-dev +cp scripts/cabal.project.local.linux cabal.project.local +``` + +Na Macu: + +``` +brew install openssl@3.0 +cp scripts/cabal.project.local.mac cabal.project.local +``` + +Może być konieczna zmiana cabal.project.local, aby wskazać poprawną lokalizację openssl + +4. Budowanie aplikacji: + +```shell +cabal update +cabal install simplex-chat +``` + +## Używanie + +### Używanie klienta czatu + +Aby uruchomić klienta, uruchom w terminalu polecenie `simplex-chat`. + +Domyślnie katalog z danymi aplikacji jest tworzony w katalogu domowym (`~/.simplex`, lub `%APPDATA%/simplex` na Windowsie), a dwa pliki danych SQLite `simplex_v1_chat.db` i `simplex_v1_agent.db` są w nim zainicjowane. + +Aby wskazać inny prefiks ścieżki dla plików bazy danych, należy użyć polecenia `-d`: + +```shell +$ simplex-chat -d alice +``` + +Uruchomienie powyższego przykładu spowoduje utworzenie plików baz danych `alice_v1_chat.db` i `alice_v1_agent.db` w bieżącym katalogu. + +Trzy domyślne serwery SMP są hostowane na Linode - są one [wstępnie skonfigurowane w aplikacji](https://github.com/simplex-chat/simplex-chat/blob/stable/src/Simplex/Chat/Options.hs#L42). + +Jeśli posiadasz własny serwer(y) SMP, możesz skonfigurować klienta poprzez opcję `-s`: + +```shell +$ simplex-chat -s smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@smp.example.com +``` + +Ciąg zakodowany w Base64url poprzedzający adres serwera to odcisk palca certyfikatu offline serwera, który jest weryfikowany przez klienta podczas handshake'a TLS. + +Po konfiguracji innego serwera w swoim kliencie nadal możesz rozmawiać z ludźmi używającymi domyślnego lub dowolnego innego serwera - ustawienie to wpływa tylko na lokalizację kolejki wiadomości podczas nawiązywania połączenia (a kolejka odpowiedzi może znajdować się na zupełnie innym serwerze, zgodnie z ustawieniami klienta rozmówcy). + +Polecenie `simplex-chat -h` pokazuje wszystkie dostępne opcje. + +### Dostęp do serwerów wiadomości przez Tor + +Zainstaluj Tor i uruchom go jako proxy SOCKS5 na porcie 9050, przykład dla MacOS: + +``` +brew install tor +brew services start tor +``` + +Użyj opcji `-x`, aby uzyskać dostęp do serwerów przez Tor: + +``` +simplex-chat -x +``` + +Możesz także użyć opcji `--socks-proxy=ipv4:port` lub `--socks-proxy=:port`, aby skonfigurować adres i port serwera proxy SOCKS5, przykładowo jeśli uruchamiasz go na innym hoście lub porcie. + +### Jak używać czatu SimpleX + +Po uruchomieniu czatu zostaniesz poproszony o podanie swojej "nazwy wyświetlanej" oraz opcjonalnej "pełnej nazwy" w celu utworzenia lokalnego profilu czatu. Nazwa wyświetlana jest aliasem, za pomocą którego kontakty mogą się do ciebie odnosić - nie jest ona unikalna i nie służy jako globalna tożsamość. Jeśli kilka kontaktów wybrało tę samą nazwę wyświetlaną, klient czatu dodaje numeryczną końcówkę (sufiks) do ich lokalnej nazwy wyświetlanej. + +Poniższy schemat przedstawia sposób łączenia się z kontaktem i wysyłania do niego wiadomości: + +<div align="center"> + <img align="center" src="/images/how-to-use-simplex.svg"> +</div> + +Gdy już skonfigurujesz swój profil lokalny, wpisz `/c` (oznaczające `/connect`), aby utworzyć nowe połączenie i wygenerować zaproszenie. Wyślij to zaproszenie do swojego kontaktu za pośrednictwem dowolnego innego kanału komunikacji. + +Możesz utworzyć wiele zaproszeń, kilkukrotnie wpisując `/connect` i wysłać te zaproszenia do kontaktów, z którymi chcesz się połączyć. + +Zaproszenie może być użyte tylko jeden raz i nawet jeśli zostanie ono przechwycone, atakujący nie będzie mógł go użyć do wysłania do Ciebie wiadomości za pośrednictwem tej kolejki, gdy Twój kontakt potwierdzi, że połączenie zostało nawiązane. Zobacz omówienie protokołu agenta dla [formatu zaproszeń](https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md#connection-request). + +Kontakt, który otrzymał zaproszenie powinien wpisać `/c <zaproszenie>`, aby zaakceptować połączenie. Spowoduje to nawiązanie połączenia, a obie strony zostaną o tym powiadomione. + +Następnie można użyć komendy `@<nazwa_kontaktu> <wiadomość>` do wysłania wiadomości. Możesz także po prostu zacząć pisać wiadomość, aby wysłać ją do kontaktu, który był ostatni. + +Użyj `/help` na czacie, by uzyskać listę pozostałych dostępnych komend. + +### Grupy + +Aby utworzyć grupę, użyj `/g <nazwa_grupy>`, a następnie dodaj do niej kontakty za pomocą `/a <nazwa_grupy> <nazwa_kontaktu>`. Możesz wysyłać wiadomości do grupy wpisując `#<nazwa_grupy> <wiadomość>`. Użyj `/help groups`, by uzyskać listę pozostałych dostępnych komend. + +![simplex-chat](/images/groups.gif) + +> **Uwaga**: informacje o grupach nie są przechowywane na żadnym serwerze, są one zapisywane jako lista członków w bazie danych aplikacji klientów, do których będą wysyłane wiadomości. + +### Wysyłanie plików + +Możesz wysłać plik do kontaktu za pomocą `/f @<nazwa_kontaktu> <ścieżka_do_pliku>` - odbiorca będzie musiał go zaakceptować przed rozpoczęciem wysyłania. Użyj `/help files`, by uzyskać listę pozostałych dostępnych komend. + +![simplex-chat](/images/files.gif) + +Możesz wysyłać pliki do grupy za pomocą `/f #<nazwa_grupy> <ścieżka_do_pliku>`. + +### Adresy kontaktowe użytkowników + +Alternatywą dla jednorazowych linków zapraszających są adresy długoterminowe. Możesz je utworzyć za pomocą `/ad` (oznaczające `/address`). Utworzony adres może być następnie udostępniony za pośrednictwem dowolnego innego kanału komunikacji i użyty przez innych użytkowników jako link do prośby o kontakt używając `/c <adres_kontaktowy_użytkownika>`. + +Prośby o kontakt możesz przyjąć za pomocą komendy `/ac <nazwa>` oraz odrzucić za pomocą `/rc <nazwa>`. + +"Długoterminowy" adres użytkownika jest długoterminowy w tym sensie, że jest to link wielokrotnego użytku - może być używany do momentu usunięcia go przez użytkownika. Po usunięciu wszystkie nawiązane połączenia pozostaną aktywne (w przeciwieństwie do tego, jak działa to w przypadku poczty e-mail, gdy zmiana adresu powoduje, że ludzie nie mogą już wysyłać do siebie wiadomości). + +Użyj `/help address`, by uzyskać listę pozostałych dostępnych komend. + +![simplex-chat](/images/user-addresses.gif) diff --git a/docs/lang/pl/CONTRIBUTING.md b/docs/lang/pl/CONTRIBUTING.md new file mode 100644 index 0000000000..5205e3c5a6 --- /dev/null +++ b/docs/lang/pl/CONTRIBUTING.md @@ -0,0 +1,121 @@ +--- +title: Poradnik wspierania projektu +revision: 31.01.2023 +--- + +| Updated 31.01.2023 | Języki: PL, [EN](/docs/CONTRIBUTING.md) [FR](/docs/lang/fr/CONTRIBUTING.md), [CZ](/docs/lang/cs/CONTRIBUTING.md) | + +# Poradnik wspierania projektu + +## Kompilacja z włączonym szyfrowaniem SQLCipher + +Dodaj `cabal.project.local` do katalogu głównego projektu z lokalizacją nagłówków i bibliotek OpenSSL oraz flagą ustawiającą tryb szyfrowania: + +``` +cp scripts/cabal.project.local.mac cabal.project.local +# lub +# cp scripts/cabal.project.local.linux cabal.project.local +``` + +## OpenSSL na MacOS + +MacOS ma domyślnie zainstalowany LibreSSL, OpenSSL musi być zainstalowany, aby skompilować SimpleX z kodu źródłowego. + +OpenSSL można zainstalować za pomocą `brew install openssl@3.0` + +Będziesz musiał dodać `/opt/homebrew/opt/openssl@3.0/bin` do swojego PATH, aby wszystko działało poprawnie + + +## Branche projektu + +**W repo simplex-chat** + + +- `stable` - stabilne wydanie aplikacji, może być używane do aktualizacji poprzedniego stabilnego wydania (GHC 9.6.3). + +- `stable-android` - używane do budowania stabilnej biblioteki rdzenia Androida z Nix (GHC 8.10.7) - tylko dla Androida armv7a. + +- `stable-ios` - używane do budowania stabilnej biblioteki rdzenia iOS z Nix (GHC 8.10.7) - ten branch powinien być taki sam jak `stable-android` z wyjątkiem plików konfiguracyjnych Nix. Przestarzałe. + +- `master` - branch dla wydań wersji beta (GHC 9.6.3). + +- `master-ghc8107` - branch dla wydań wersji beta (GHC 8.10.7). Przestarzałe. + +- `master-android` - używane do budowania biblioteki rdzenia Androida w wersji beta z Nix (GHC 8.10.7) - tylko dla Androida armv7a. + +- `master-ios` - służy do budowania biblioteki rdzenia beta iOS z Nix (GHC 8.10.7). Przestarzałe. + +- `windows-ghc8107` - branch do kompilacji głównej biblioteki Windows (GHC 8.10.7). Przestarzałe? + +Branche `master-ios` i `windows-ghc8107` powinny być takie same jak `master-ghc8107` z wyjątkiem plików konfiguracyjnych Nix. + +**W repo simplexmq** + +- `master` - używa GHC 9.6.3, jego commit powinien być użyty w branchu `master` repo simplex-chat. + +- `master-ghc8107` - jego commit powinien być użyty w branchu `master-android` (i `master-ios`) repo simplex-chat. Przestarzałe. + +## Development i proces wydawania + +1. Tworzenie PR-ów do brancha `master` _tylko_ dla repozytoriów simplex-chat i simplexmq. + +2. Jeśli repozytorium simplexmq zostało zmienione, aby skompilować mobilne biblioteki rdzenia należy połączyć jego branch `master` z branchem `master-ghc8107`. + +3. Aby skompilować podstawowe biblioteki dla Androida, iOS i Windows: + +- scal branch `master` z branchem `master-android`. + +- Zaktualizuj kod, aby był kompatybilny z GHC 8.10.7 (patrz niżej). + +- push do GitHuba. + +4. Wszystkie biblioteki powinny być budowane z brancha `master`, Android armv7a - z brancha `master-android`. + +5. Aby zbudować aplikacje Desktop i CLI, należy utworzyć tag w branchu `master`, pliki APK powinny być dołączone do wydania. + +6. Po publicznym wydaniu w App Store i Play Store, scal: + +- `master` do `stable` + +- `master` do `master-android` (i skompiluj/zaktualizuj kod) + +- `master-android` do `stable-android`. + +7. Branch `master` repo simplexmq powinien zostać niezależnie scalony z branchem `stable` w wydaniach stabilnych. + + +## Różnice pomiędzy GHC 8.10.7 i GHC 9.6.3 + +1. Główna różnica związana jest z rozszerzeniem `DuplicateRecordFields`. + +W GHC 9.6.3 nie jest już możliwe określenie typu podczas korzystania z selektorów, zamiast tego używane jest rozszerzenie OverloadedRecordDot i składnia, które muszą zostać usunięte w GHC 8.10.7: + +```haskell +{-# LANGUAGE DuplicateRecordFields #-} +-- Użyj tego w GHC 9.6.3, gdy jest to potrzebne +{-# LANGUAGE OverloadedRecordDot #-} + +-- syntax GHC 9.6.3 +let x = record.field + +-- syntax GHC 8.10.7 usunięty w GHC 9.6.3 +let x = field (record :: Record) +``` + +Nadal możliwe jest określenie typu podczas korzystania ze składni aktualizacji rekordu, użyj tej reguły, aby wyłączyć ostrzeżenie kompilatora: + +```haskell +-- Użyj tego w GHC 9.6.3, gdy jest to potrzebne +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} + +let r' = (record :: Record) {field = value} +``` + +2. Większość funkcji monad musi być teraz importowana z `Control.Monad`, a nie z konkretnych modułów monad (np. `Control.Monad.Except`). + +```haskell +-- Użyj tego w GHC 9.6.3, gdy jest to potrzebne +import Control.Monad +``` + +[Ten PR](https://github.com/simplex-chat/simplex-chat/pull/2975/files) opisuje wszystkie różnice. diff --git a/docs/lang/pl/README.md b/docs/lang/pl/README.md new file mode 100644 index 0000000000..23ca00c3e6 --- /dev/null +++ b/docs/lang/pl/README.md @@ -0,0 +1,435 @@ +[![build](https://github.com/simplex-chat/simplex-chat/actions/workflows/build.yml/badge.svg?branch=stable)](https://github.com/simplex-chat/simplex-chat/actions/workflows/build.yml) +[![Pobieranie z GitHuba](https://img.shields.io/github/downloads/simplex-chat/simplex-chat/total)](https://github.com/simplex-chat/simplex-chat/releases) +[![Wydanie na Githubie](https://img.shields.io/github/v/release/simplex-chat/simplex-chat)](https://github.com/simplex-chat/simplex-chat/releases) +[![Dołącz na Reddicie](https://img.shields.io/reddit/subreddit-subscribers/SimpleXChat?style=social)](https://www.reddit.com/r/SimpleXChat) +<a rel="me" href="https://mastodon.social/@simplex">![Śledź na Mastodonie](https://img.shields.io/mastodon/follow/108619463746856738?domain=https%3A%2F%2Fmastodon.social&style=social)</a> + +| 30/03/2023 | PL, [EN](/README.md), [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md) | + +<img src="images/simplex-chat-logo.svg" alt="SimpleX logo" width="100%"> + +# SimpleX - pierwszy komunikator bez jakichkolwiek identyfikatorów użytkowników - w 100% prywatny z założenia! + +[<img src="./images/trail-of-bits.jpg" height="100">](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html)     [<img src="./images/privacy-guides.jpg" height="80">](https://www.privacyguides.org/en/real-time-communication/#simplex-chat)     [<img src="./images/kuketz-blog.jpg" height="80">](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/) + +## Witamy w SimpleX Chat! + +1. 📲 [Zainstaluj aplikację](#zainstaluj-aplikację). +2. ↔️ [Połącz się z naszym zespołem](#połącz-się-z-naszym-zespołem), [dołącz do grup użytkowników](#dołącz-do-grup-użytkowników) oraz [śledź nasze aktualizacje](#śledź-nasze-aktualizacje). +3. 🤝 [Wykonaj prywatne połączenie](#wykonaj-prywatne-połączenie) ze znajomym. +4. 🔤 [Pomóż w tłumaczeniu SimpleX Chat](#pomóż-nam-przetłumaczyć-simplex-chat). +5. ⚡️ [Kontrybuuj](#kontrybuuj) i [wesprzyj nas dotacjami](#wesprzyj-nas-dotacjami). + +[Dowiedz się więcej na temat SimpleX Chat](#informacje). + +## Zainstaluj aplikację + +[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apple_store.svg" alt="Aplikacja iOS" height="42">](https://apps.apple.com/us/app/simplex-chat/id1605771084) +  +[![Android app](https://github.com/simplex-chat/.github/blob/master/profile/images/google_play.svg)](https://play.google.com/store/apps/details?id=chat.simplex.app) +  +[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/f_droid.svg" alt="F-Droid" height="41">](https://app.simplex.chat) +  +[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu) +  +[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) + +- 🖲 Chroni Twoje wiadomości i metadane - z kim rozmawiasz i kiedy. +- 🔐 Szyfrowanie end-to-end double ratchet, z dodatkową warstwą szyfrowania. +- 📱 Aplikacje mobilne dla Androida ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)) oraz [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084). +- 🚀 [TestFlight dla iOS](https://testflight.apple.com/join/DWuT2LQu) z nowymi funkcjami na tydzień-dwa wcześniej - **limitowane do 10,000 użytkowników**! +- 🖥 Dostępny jako terminalowa (konsolowa) [aplikacja / CLI](#zap-quick-installation-of-a-terminal-app) na Linuxa, MacOSa, Windowsa. + +## Połącz się z naszym zespołem + +Możesz połączyć się z naszym zespołem za pośrednictwem aplikacji, korzystając z przycisku "czat z deweloperami" który dostępny jest w przypadku gdy nie masz konwersacji na swoim profilu, opcji "wysyłaj pytania i pomysły" w ustawieniach aplikacji lub za pośrednictwem naszego [adresu SimpleX](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). Please connect to: + +- by zadać dowolne pytania +- by zasugerować dowolne ulepszenia +- by udostępnić nam wszystko co istotne + +Odpowiadamy na pytania manualnie, więc nie jest to natychmiastowe - może to potrwać do 24 godzin. + +Jeśli jesteś zainteresowany pomocą w integracji otwartoźródłowych modeli językowych i [dołączeniem do naszego zespołu](../../JOIN_TEAM.md), skontaktuj się z nami. + +## Dołącz do grup użytkowników + +Możesz dołączyć do grup utworzonych przez innych użytkowników za pośrednictwem nowej [usługi katalogowej](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). Nie jesteśmy odpowiedzialni za treści udostępniane w tych grupach. + +**Uwaga**: Poniższe grupy zostały utworzone, aby użytkownicy mogli zadawać pytania, zgłaszać sugestie i zadawać pytania dotyczące wyłącznie SimpleX Chat. + +Możesz również: +- krytykować aplikację i dokonywać porównań z innymi komunikatorami. +- udostępniać nowe komunikatory, które Twoim zdaniem mogą być interesujące z punktu widzenia prywatności, o ile nie spamujesz. +- udostępniać niektóre publikacje związane z prywatnością, raczej dość rzadko. +- po wstępnym zatwierdzeniu przez administratora w prywatnej wiadomości, udostępnić link do utworzonej grupy, ale tylko raz. Gdy grupa ma więcej niż 10 członków, może zostać przesłana do [SimpleX Directory Service](../../DIRECTORY.md), gdzie nowi użytkownicy będą mogli ją odkryć. + +Musisz: +- być uprzejmym wobec innych użytkowników. +- unikać spamu (zbyt częstych wiadomości, nawet jeśli są istotne). +- unikać ataków osobistych lub wrogiego nastawienia. +- unikać dzielenia się treściami, które nie są związane z powyższymi kwestiami (co obejmuje między innymi dyskusje na temat polityki lub innych aspektów życia społecznego niż prywatność, bezpieczeństwo, technologia i komunikacja, dzielenie się treściami, które mogą zostać uznane za obraźliwe przez innych użytkowników itp.). + +Wiadomości nieprzestrzegające tych zasad będą usuwane, prawo do wysyłania wiadomości może zostać odebrane ich autorom, a dostęp nowych członków do grupy może zostać tymczasowo ograniczony, aby zapobiec ponownemu dołączeniu pod inną nazwą - nasza niedoskonała moderacja grupy nie ma obecnie lepszego rozwiązania. + +Jeśli chcesz zadać jakieś pytania, możesz dołączyć do anglojęzycznej grupy użytkowników: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D) + +Istnieje również [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) dla deweloperów, którzy na bazie platformy SimpleX tworzą: + +- czatboty i automatyzacje +- integracje z innymi aplikacjami +- aplikacje społecznościowe i serwisy +- itp. + +Istnieją grupy w innych językach, na które przetłumaczyliśmy interfejs aplikacji. Grupy te służą do testowania i zadawania pytań innym użytkownikom SimpleX Chat: + +[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking), [\#SimpleX-ES](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FaJ8O1O8A8GbeoaHTo_V8dcefaCl7ouPb%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA034qWTA3sWcTsi6aWhNf9BA34vKVCFaEBdP2R66z6Ao%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22wiZ1v_wNjLPlT-nCSB-bRA%3D%3D%22%7D) (Spanish-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking). + +Możesz do nich dołączyć otwierając te linki w aplikacji lub otwierając je w przeglądarce na komputerze i skanując kod QR. + +## Śledź nasze aktualizacje + +Nasze aktualizacje i wydania publikujemy za pośrednictwem: + +- [Reddita](https://www.reddit.com/r/SimpleXChat/), [Twittera](https://twitter.com/SimpleXChat), [Lemmy](https://lemmy.ml/c/simplex), [Mastodona](https://mastodon.social/@simplex) oraz [Nostr](https://snort.social/p/npub1exv22uulqnmlluszc4yk92jhs2e5ajcs6mu3t00a6avzjcalj9csm7d828). +- [profilu zespołu](#connect-to-the-team) w aplikacji SimpleX. +- [bloga](https://simplex.chat/blog/) oraz [feedu RSS](https://simplex.chat/feed.rss). +- [listy mailingowej](https://simplex.chat/#join-simplex), bardzo rzadko. + +## Wykonaj prywatne połączenie + +Aby nawiązać połączenie i rozpocząć wysyłanie wiadomości, należy udostępnić znajomemu łącze lub zeskanować kod QR z jego telefonu, osobiście lub podczas połączenia wideo. + +Kanał, za pośrednictwem którego udostępniasz link, nie musi być bezpieczny - wystarczy, że możesz potwierdzić, kto wysłał Ci wiadomość i że połączenie SimpleX zostało nawiązane. + +<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app1.png" alt="Wykonaj prywatne połączenie" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app2.png" alt="Conversation" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app3.png" alt="Połączenie wideo" height="360"> + +Po wykonaniu połączenia możesz [zweryfikować kod bezpieczeństwa połączenia](../../../blog/20230103-simplex-chat-v4.4-disappearing-messages.md#connection-security-verification). + +## Poradnik dla użytkownika (NOWE) + +Przeczytaj o funkcjach i ustawieniach aplikacji w nowym [Przewodniku użytkownika](../../guide/README.md). + +## Pomóż nam przetłumaczyć SimpleX Chat + +Dzięki naszym użytkownikom i [Weblate](https://hosted.weblate.org/engage/simplex-chat/), aplikacje SimpleX Chat, strona internetowa i dokumenty są tłumaczone na wiele innych języków. + +Dołącz do naszych tłumaczy, aby pomóc SimpleX w rozwoju! + +|region|język |kontrybutor|[Android](https://play.google.com/store/apps/details?id=chat.simplex.app) i [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084)|[strona](https://simplex.chat)|dokumenty na GitHubie| +|:----:|:-------:|:---------:|:---------:|:---------:|:---------:| +|🇬🇧 en|English | |✓|✓|✓|✓| +|ar|العربية |[jermanuts](https://github.com/jermanuts)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/ar/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ar/)<br>-|[![website](https://hosted.weblate.org/widgets/simplex-chat/ar/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/ar/)|| +|🇧🇬 bg|Български | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/bg/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/bg/)<br>[![ios app](https://hosted.weblate.org/widget/simplex-chat/ios/bg/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/bg/)||| +|🇨🇿 cs|Čeština |[zen0bit](https://github.com/zen0bit)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/cs/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/cs/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/cs/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/cs/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/cs/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/cs/)|[✓](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/cs)| +|🇩🇪 de|Deutsch |[mlanp](https://github.com/mlanp)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/de/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/de/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/de/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/de/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/de/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/de/)|| +|🇪🇸 es|Español |[Mateyhv](https://github.com/Mateyhv)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/es/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/es/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/es/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/es/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/es/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/es/)|| +|🇫🇮 fi|Suomi | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/fi/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fi/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fi/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fi/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fi/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fi/)|| +|🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/fr/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fr/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fr/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fr/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fr/)|[✓](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/fr)| +|🇮🇱 he|עִברִית | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/he/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/he/)<br>-||| +|🇭🇺 hu|Magyar | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/hu/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/hu/)<br>-||| +|🇮🇹 it|Italiano |[unbranched](https://github.com/unbranched)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/it/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/it/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/it/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/it/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/it/)|| +|🇯🇵 ja|日本語 | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/ja/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ja/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ja/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ja/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/ja/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/ja/)|| +|🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/nl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/nl/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/nl/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/nl/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/nl/)|| +|🇵🇱 pl|Polski |[BxOxSxS](https://github.com/BxOxSxS)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/pl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/pl/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/pl/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/pl/)||| +|🇧🇷 pt-BR|Português||[![android app](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/)<br>-|[![website](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/pt_BR/)|| +|🇷🇺 ru|Русский ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/ru/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ru/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)||| +|🇹🇭 th|ภาษาไทย |[titapa-punpun](https://github.com/titapa-punpun)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/th/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/th/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/th/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/th/)||| +|🇹🇷 tr|Türkçe | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/tr/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/tr/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/tr/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/tr/)||| +|🇺🇦 uk|Українська| |[![android app](https://hosted.weblate.org/widgets/simplex-chat/uk/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/uk/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/uk/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/uk/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/uk/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/uk/)|| +|🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)<br><br>[Float-hu](https://github.com/Float-hu)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)<br> |<br><br>[![website](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)|| + +Trwają prace nad wersjami językowymi: Arabski, japoński, koreański, portugalski i [inne](https://hosted.weblate.org/projects/simplex-chat/#languages). Będziemy dodawać kolejne języki, gdy niektóre z już dodanych zostaną ukończone - zasugeruj nowe języki, przejrzyj [przewodnik po tłumaczeniach](./TRANSLATIONS.md) i skontaktuj się z nami! + +## Kontrybuuj + +Chcielibyśmy, abyś przyczynił się do naszego rozwoju! Możesz nam pomóc: + +- [dzieląc się motywem kolorystycznym](../../THEMES.md), którego używasz w aplikacji na Androida! +- pisząc samouczki lub poradniki, które dotyczą hostowania serwerów, automatyzacji czatbotów itp. +- współtworząc bazy wiedzy SimpleX Chat. +- rozwijając funkcje - skontaktuj się z nami za pośrednictwem czatu, abyśmy mogli pomóc Ci zacząć. + +## Wesprzyj nas dotacjami + +Ogromne podziękowania dla wszystkich, którzy wsparli projekt SimpleX Chat! + +Na pierwszym miejscu stawiamy prywatność i bezpieczeństwo użytkowników - byłoby to niemożliwe bez waszego wsparcia. + +Naszą obietnicą wobec użytkowników jest to, że protokoły SimpleX są i pozostaną otwarte i w domenie publicznej - tak więc każdy może stworzyć przyszłe implementacje klientów i serwerów. Budujemy platformę SimpleX opartą na tych samych zasadach, co poczta e-mail i Internet, ale znacznie bardziej prywatną i bezpieczną. + +Twoje darowizny pomogą nam zebrać więcej funduszy - każda kwota, nawet koszt filiżanki kawy, będzie dla nas ogromną pomocą. + +Możesz nas wesprzeć za pomocą: + +- [GitHuba](https://github.com/sponsors/simplex-chat) - jest to dla nas wolne od prowizji. +- [OpenCollective](https://opencollective.com/simplex-chat) - pobiera prowizję, a także przyjmuje darowizny w kryptowalutach. +- Monero: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt +- Bitcoin: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG +- BCH: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG +- USDT: + - BNB Smart Chain: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2 + - Tron: TNnTrKLBmdy2Wn3cAQR98dAVvWhLskQGfW +- Ethereum: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2 +- Solana: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L + +Dziękuję, + +Evgeny + +Twórca SimpleX Chat. + +## Informacje + +- [Dlaczego prywatność ma znaczenie](#dlaczego-prywatność-ma-znaczenie) +- [Podejście SimpleXa do problemu prywatności i bezpieczeństwa](#podejście-simplexa-do-problemu-prywatności-i-bezpieczeństwa) + - [Kompletna prywatność Twojej tożsamości, profilu, kontaktów i metadanych.](#kompletna-prywatność-twojej-tożsamości-profilu-kontaktów-i-metadanych) + - [Najlepsza ochrona przed spamem i nadużyciami](#najlepsza-ochrona-przed-spamem-i-nadużyciami) + - [Pełna kontrola i bezpieczeństwo Twoich danych](#pełna-kontrola-i-bezpieczeństwo-twoich-danych) + - [Użytkownicy są właścicielami sieci SimpleX](#użytkownicy-są-właścicielami-sieci-simplex) +- [Często zadawane pytania](#często-zadawane-pytania) +- [Newsy i aktualizacje](#newsy-i-aktualizacje) +- [Szybka instalacja terminalowej wersji aplikacji](#zap-szybka-instalacja-terminalowej-wersji-aplikacji) +- [Budowa Platformy SimpleX](#budowa-platformy-simplex) +- [Prywatność i bezpieczeństwo: szczegóły techniczne i ograniczenia](#prywatność-i-bezpieczeństwo-szczegóły-techniczne-i-ograniczenia) +- [Dla deweloperów](#dla-deweloperów) +- [Roadmapa](#roadmapa) +- [Ostrzeżenia, Kontakt w sprawie bezpieczeństwa, Licencja](#ostrzeżenia) + +## Dlaczego prywatność ma znaczenie + +Każdy powinien dbać o prywatność i bezpieczeństwo swojej komunikacji - nieszkodliwe rozmowy mogą narazić Cię na niebezpieczeństwo, nawet jeśli nie masz nic do ukrycia. + +Jedną z najbardziej wstrząsających historii jest doświadczenie [Mohamedou Ould Salahi](https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi). opisane w jego pamiętniku i pokazane w filmie Mauretańczyk (2021). Został on umieszczony w obozie Guantanamo, bez procesu, i był tam torturowany przez 15 lat po telefonie do swojego krewnego w Afganistanie, pod zarzutem udziału w atakach 9/11, mimo że przez poprzednie 10 lat mieszkał w Niemczech. + +Używanie szyfrowanego komunikatora end-to-end nie jest wystarczające. Powinniśmy używać komunikatorów, które zapewniają prywatność naszym powiązaniom, czyli tym z kim jesteśmy jakkolwiek połączeni. + +## Podejście SimpleXa do problemu prywatności i bezpieczeństwa + +### Kompletna prywatność Twojej tożsamości, profilu, kontaktów i metadanych. + +**W przeciwieństwie do innych komunikatorów, SimpleX nie posiada żadnych identyfikatorów przypisanych do użytkowników**. Nie posiada nawet numerów generowanych losowo. Zapewnia to prywatność tego, z kim się komunikujesz, ukrywając jego tożsamość oraz fakt komunikacji przed serwerami platformy SimpleX i wszelkimi obserwatorami [Czytaj więcej](./SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata). + +### Najlepsza ochrona przed spamem i nadużyciami + +Ponieważ na platformie SimpleX nie masz identyfikatora ani stałego adresu, nikt nie może się z Tobą skontaktować, chyba że udostępnisz jednorazowy lub tymczasowy adres użytkownika, w postaci kodu QR lub linku. [Czytaj więcej](./SIMPLEX.md#the-best-protection-against-spam-and-abuse). + +### Pełna kontrola i bezpieczeństwo Twoich danych + +SimpleX przechowuje wszystkie dane użytkownika na urządzeniach klienckich, wiadomości są przechowywane tymczasowo na serwerach przekaźnikowych SimpleX do momentu ich odebrania, po czym są trwale usuwane. [Czytaj więcej](./SIMPLEX.md#complete-ownership-control-and-security-of-your-data). + +### Użytkownicy są właścicielami sieci SimpleX + +Możesz używać SimpleX na własnych serwerach i nadal komunikować się z ludźmi za pomocą serwerów, które są wstępnie skonfigurowane w aplikacjach lub z dowolnymi innymi serwerami SimpleX. [Czytaj więcej](./SIMPLEX.md#users-own-simplex-network). + +## Często zadawane pytania + +1. _W jaki sposób SimpleX może dostarczać wiadomości bez jakichkolwiek identyfikatorów użytkownika?_ Zobacz [ogłoszenie wydania v2](../../../blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers) wyjaśniające jak SimpleX działa. + +2. _Dlaczego po prostu nie mogę używać Signal?_ Signal to scentralizowana platforma, która wykorzystuje numery telefonów do identyfikacji użytkowników i ich kontaktów. Oznacza to, że podczas gdy treść wiadomości w Signal jest chroniona solidnym szyfrowaniem end-to-end, istnieje duża ilość metadanych widocznych dla Signal - to, z kim rozmawiasz i kiedy. + +3. _Czym to się różni od Matrix, Session, Ricochet, Cwtch itp., które również nie wymagają tożsamości użytkownika?_ Mimo że te platformy nie wymagają _prawdziwej tożsamości_, to polegają na anonimowych tożsamościach użytkowników w celu dostarczania wiadomości - może to być na przykład klucz tożsamości lub liczba losowa. Korzystanie z trwałej tożsamości użytkownika, nawet anonimowej, stwarza ryzyko, że informacje na temat powiązań użytkownika staną się znane obserwatorom i/lub dostawcom usług, co może prowadzić do deanonimizacji poszczególnych użytkowników. Jeśli ten sam profil użytkownika jest używany do łączenia się z dwiema różnymi osobami za pośrednictwem dowolnego komunikatora innego niż SimpleX, wspomniane dwie osoby mogą stwierdzić, czy są rozmawiają z tą samą osobą - w wiadomościach używają tego samego identyfikatora użytkownika. W SimpleX nie ma metadanych wspólnych dla rozmów z różnymi kontaktami - jest to cecha, której nie ma żaden inny komunikator. + +## Newsy i aktualizacje + +Najnowsze i ważne wiadomości: + +[Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](../../../blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md) + +[Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](../../../blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md) + +[Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](../../../blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md) + +[Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](../../../blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md). + +[Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](../../../blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md). + +[Jul 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](../../../blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md). + +[May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](../../../blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md). + +[Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](../../../blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md). + +[Mar 1, 2023. SimpleX File Transfer Protocol – send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](../../../blog/20230301-simplex-file-transfer-protocol.md). + +[Nov 8, 2022. Security audit by Trail of Bits, the new website and v4.2 released](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). + +[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](../../../blog/20220928-simplex-chat-v4-encrypted-database.md). + +[All updates](../../../blog) + +## :zap: Szybka instalacja terminalowej wersji aplikacji + +```sh +curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/stable/install.sh | bash +``` + +Po pobraniu klienta czatu można go uruchomić za pomocą polecenia `simplex-chat`. + +![simplex-chat](./images/connection.gif) + +Przeczytaj więcej o [instalowaniu i używaniu terminalowej wersji czatu](./CLI.md). + +## Budowa Platformy SimpleX + +SimpleX to sieć typu klient-serwer z unikatową topologią sieciową, która wykorzystuje redundantne, jednorazowe węzły przekazywania wiadomości do asynchronicznego przekazywania wiadomości za pośrednictwem jednokierunkowych (simpleksowych) kolejek wiadomości, zapewniając anonimowość odbiorcy i nadawcy. + +W przeciwieństwie do sieci P2P, wszystkie wiadomości są przekazywane przez jeden lub kilka węzłów serwera, które nawet nie muszą być trwałe. Obecna implementacja [serwera SMP](https://github.com/simplex-chat/simplexmq#smp-server) wykorzystuje przechowywanie wiadomości w pamięci, utrzymując jedynie rejestr kolejki. SimpleX zapewnia lepszą ochronę metadanych niż projekty P2P, ponieważ żadne globalne identyfikatory uczestników nie są używane do dostarczania wiadomości i pozwala to uniknąć [różnych problemów związanych z sieciami P2P](./SIMPLEX.md#comparison-with-p2p-messaging-protocols). + +W przeciwieństwie do sieci sfederowanych, węzły serwera **nie posiadają danych użytkowników**, **nie komunikują się ze sobą** i **nie przechowują wiadomości** po ich dostarczeniu do odbiorców. Nie ma możliwości na odkrycie pełnej listy serwerów działających w sieci SimpleX. Taka konstrukcja pozwala uniknąć problemu związanego z widocznością metadanych, z którym borykają się wszystkie sieci sfederowane i pozwala ona na lepszą ochronę przed atakami obejmującymi całą sieć. + +Informacje o użytkownikach, ich kontaktach i grupach znajdują się wyłącznie na urządzeniach klienckich. + +Przeczytaj [whitepaper SimpleX](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md) po więcej informacji o zadaniach platformy oraz by dowiedzieć się jak wygląda koncepcja techniczna modelu. + +Zobacz [Protokół Czatu SimpleX](../../protocol/simplex-chat.md) by dowiedzieć się o formacie wiadomości wysyłanych między klientem czatu za pośrednictwem [Protokołu Wiadomości SimpleX](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md). + +## Prywatność i bezpieczeństwo: szczegóły techniczne i ograniczenia + +Prace nad SimpleX Chat wciąż trwają - udostępniamy nowe ulepszenia, gdy tylko będą gotowe. To Ty musisz zdecydować, czy obecny stan jest wystarczająco dobry dla Twojego przypadku zastosowania. + +Stworzyliśmy [słownik pojęć](../../GLOSSARY.md) używany do opisu systemów komunikacyjnych, aby pomóc zrozumieć niektóre z poniższych pojęć oraz aby pomóc Ci w porównaniu zalet i wad różnych systemów komunikacyjnych. + +Co zostało już wprowadzone: + +1. Zamiast identyfikatorów użytkownika używanych przez wszystkie inne platformy, nawet te najbardziej prywatne, SimpleX używa [pairwise per-queue identifiers](../../GLOSSARY.md#pairwise-pseudonymous-identifier) (2 adresy dla każdej jednokierunkowej kolejki wiadomości, z opcjonalnym trzecim adresem dla powiadomień push na iOS, 2 kolejki w każdym połączeniu między użytkownikami). Sprawia to, że trudniej jest w ten sposób obserwować przebieg połączeń sieciowych na poziomie aplikacji, ponieważ dla `n` użytkowników może istnieć do `n * (n-1)` kolejek wiadomości. +2. [Szyfrowanie end-to-end](../../GLOSSARY.md#end-to-end-encryption) w każdej kolejce wiadomości używając [cryptoboxa NaCl](https://nacl.cr.yp.to/box.html). Zostało to dodane, aby umożliwić redundancję w przyszłości (przekazywanie każdej wiadomości przez kilka serwerów), aby uniknąć posiadania tego samego ciphertext w różnych kolejkach (które byłyby widoczne tylko dla atakującego, w przypadku przejęcia TLS). Klucze szyfrujące używane do tego szyfrowania nie są rotowane, zamiast tego planujemy rotować kolejki. Do negocjacji kluczy używane są klucze Curve25519. +3. Szyfrowanie end-to-end [double ratchet](../../GLOSSARY.md#double-ratchet-algorithm) w każdej rozmowie między dwoma użytkownikami (lub członkami grupy). Jest to ten sam algorytm, który jest używany w Signal i wielu innych komunikatorach; zapewnia on komunikację OTR z [forward secrecy](../../GLOSSARY.md#forward-secrecy) (każda wiadomość jest szyfrowana własnym kluczem efemerycznym) i [break-in recovery](../../GLOSSARY.md#post-compromise-security) (klucze są często renegocjowane w ramach wymiany wiadomości). Dwie pary kluczy Curve448 są używane do początkowego [key agreement](../../GLOSSARY.md#key-agreement-protocol), strona inicjująca przekazuje te klucze przez link połączenia, a strona akceptująca - w nagłówku wiadomości potwierdzającej. +4. Dodatkowa warstwa szyfrowania przy użyciu NaCL cryptobox dla wiadomości dostarczanych z serwera do odbiorcy. Warstwa ta pozwala uniknąć wspólnego szyfrogramu między wysyłanym i odbieranym ruchem serwera wewnątrz TLS (i nie ma też wspólnych identyfikatorów). +5. Kilka poziomów [content padding](../../GLOSSARY.md#message-padding) w celu utrudnienia ataków na rozmiar wiadomości. +6. Wszystkie metadane wiadomości, w tym czas odebrania wiadomości przez serwer (zaokrąglony do sekundy), są wysyłane do odbiorców w zaszyfrowanej postaci, więc nawet jeśli TLS zostanie przejęty, nie można ich zobaczyć. +7. Dozwolone są tylko TLS 1.2/1.3 dla połączeń klient-serwer, z ograniczeniem do algorytmów kryptograficznych: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448. +8. Aby zapobiec atakom typu replay, serwery SimpleX wymagają [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) jako identyfikatora sesji w każdym poleceniu klienta podpisanym kluczem efemerycznym dla każdej kolejki. +9. Aby ochronić swój adres IP, wszystkie klienty SimpleX Chat obsługują dostęp do serwerów komunikacyjnych za pośrednictwem Tora - zobacz [v3.1 release announcement](../../../blog/20220808-simplex-chat-v3.1-chat-groups.md) po więcej szczegółów. +10. Lokalne szyfrowanie bazy danych z hasłem - kontakty, grupy oraz wszystkie wysłane i odebrane wiadomości są przechowywane w postaci zaszyfrowanej. Jeśli korzystałeś z SimpleX Chat przed wersją v4.0, musisz włączyć szyfrowanie w ustawieniach aplikacji. +11. Izolacja transportu - różne połączenia TCP i obwody Tor używane są dla ruchu różnych profili użytkowników, opcjonalnie - dla różnych kontaktów i połączeń członków grupy. +12. Ręczne obracanie kolejki wiadomości w celu przeniesienia konwersacji do innego przekaźnika SMP. +13. Wysyłanie zaszyfrowanych plików end-to-end przy użyciu [protokołu XFTP](../../../blog/20230301-simplex-file-transfer-protocol.md). +14. Szyfrowanie plików lokalnych. + +Planujemy dodać: + +1. Przekaźniki SMP nadawców i przekaźniki XFTP odbiorców w celu zmniejszenia ruchu i w celu ukrycia adresów IP przed przekaźnikami wybranymi i potencjalnie kontrolowanymi przez drugą stronę. +2. Post-kwantowa wymiana kluczy w protokole Double Ratchet. +3. Automatyczna rotacja kolejek wiadomości i redundancja. Obecnie kolejki utworzone między dwoma użytkownikami są używane, dopóki kolejka nie zostanie ręcznie zmieniona przez użytkownika lub dopóki kontakt nie zostanie usunięty. Planujemy dodać automatyczną rotację kolejek, aby te identyfikatory były tymczasowe i rotowały w oparciu o pewien harmonogram TBC (np. co X wiadomości lub co X godzin/dni). +4. "Mieszanie" wiadomości - dodanie opóźnienia do dostarczania wiadomości, w celu ochrony przed korelacją ruchu według czasu wiadomości. +5. Reprodukowalne kompilacje - ograniczeniem jest tu stos deweloperski, ale będziemy starali się rozwiązać ten problem. Użytkownicy nadal mogą tworzyć wszystkie aplikacje i usługi z kodu źródłowego. + +## Dla deweloperów + +Możesz: + +- korzystać z biblioteki SimpleX Chat w celu zintegrowania funkcji czatu z aplikacjami mobilnymi. +- tworzyć boty i usługi czatu w języku Haskell - zobacz [prosty](./apps/simplex-bot/) i bardziej [zaawansowany przykład bota czatu](./apps/simplex-bot-advanced/). +- tworzenie chat botów i usług w dowolnym języku z wykorzystaniem terminala CLI SimpleX Chat jako lokalnego serwera WebSocket. Zobacz [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) i [JavaScript chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js). +- uruchomić [simplex-chat w terminal ](./CLI.md), aby wykonywać poszczególne polecenia czatu, np. wysyłać wiadomości w ramach wykonywania skryptu powłoki. + +Jeśli chcesz rozwijać platformę SimpleX, skontaktuj się z nami, aby uzyskać porady i wsparcie. + +Dołącz również do grupy [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F6eHqy7uAbZPOcA6qBtrQgQquVlt4Ll91%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAqV_pg3FF00L98aCXp4D3bOs4Sxv_UmSd-gb0juVoQVs%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22XonlixcHBIb2ijCehbZoiw%3D%3D%22%7D), aby zadawać pytania i dzielić się swoimi sukcesami. + +## Roadmapa + +- ✅ Łatwy do utworzenia serwer SimpleX z przechowywaniem wiadomości w pamięci, bez żadnych dependency. +- ✅ Terminalowa (konsolowa) wersja klienta z obsługą grup oraz plików. +- ✅ Utworzenie serwera SimpleX na Linode zaledwie jednym kliknięciem. +- ✅ Szyfrowanie end-to-end używając protokołu double-ratchet z dodatkową warstwą szyfrowania. +- ✅ Pierwsza wersja aplikacji na Androida i iOS. +- ✅ Zachowujące prywatność natychmiastowe powiadomienia na Androida przy użyciu usługi działającej w tle. +- ✅ Przykłady czatbota w Haskellu. +- ✅ v2.0 - wsparcie dla zdjęć oraz plików w aplikacjach mobilnych. +- ✅ Manualne usuwanie historii czatu. +- ✅ Szyfrowane end-to-end encrypted audio oraz wideo rozmowy przy użyciu WebRTC w aplikacjach mobilnych. +- ✅ Zachowujące prywatność natychmiastowe powiadomienia na iOS przy użyciu usługi Apple Push Notification. +- ✅ Eksportowanie oraz importowanie bazy danych czatu. +- ✅ Konwersacje grupowe w aplikacjach mmobilnych. +- ✅ Łączenie się z serwerami wiadomości przez Tor. +- ✅ Podwójne adresy serwerów w celu uzyskania dostępu do serwerów wiadomości jako ukrytych usług v3. +- ✅ Serwer czatu i SDK klienta w języku TypeScript do tworzenia interfejsów czatu, integracji i botów czatu (gotowe do ogłoszenia). +- ✅ Tryb incognito do udostępniania nowej losowej nazwy każdemu kontaktowi. +- ✅ Szyfrowanie bazy danych czatu. +- ✅ Automatyczne usuwanie historii czatu. +- ✅ Linki umożliwiające dołączanie do grup i poprawienie ich stabilności. +- ✅ Wiadomości głosowe (z opcją rezygnacji odbiorcy dla każdego kontaktu). +- ✅ Podstawowe uwierzytelnianie dla serwerów SMP (w celu autoryzacji tworzenia nowych kolejek). +- ✅ Wyświetlanie usuniętych wiadomości, pełne usuwanie wiadomości przez nadawcę (z wyrażeniem zgody przez odbiorcę dla każdego kontaktu). +- ✅ Blokowanie zrzutów ekranu i wyświetlania zawartości aplikacji w "ostatnich aplikacjach". +- ✅ Zaawansowana konfiguracja serwera. +- ✅ Znikające wiadomości (z możliwością wyboru przez odbiorcę dla każdego kontaktu). +- ✅ Wiadomości "na żywo". +- ✅ Weryfikacja kontaktu za pośrednictwem oddzielnego kanału out-of-band. +- ✅ Wiele profili użytkowników w tej samej bazie danych czatu. +- ✅ Opcjonalnie unikanie ponownego użycia tej samej sesji TCP dla wielu połączeń. +- ✅ Zachowywanie wersji roboczych wiadomości. +- ✅ Serwer plików do optymalizacji wydajnego i prywatnego wysyłania dużych plików. +- ✅ Ulepszone połączenia audio i wideo. +- ✅ Obsługa starszego systemu operacyjnego Android i 32-bitowych procesorów. +- ✅ Ukryte profile czatu. +- ✅ Wysyłanie i odbieranie dużych plików przez [protokół XFTP](../../../blog/20230301-simplex-file-transfer-protocol.md). +- ✅ Wiadomości wideo. +- ✅ Kod dostępu do aplikacji. +- ✅ Ulepszenie interfejsu Androidowej aplikacji. +- ✅ Opcjonalne alternatywne hasło dostępu. +- ✅ Reakcje na wiadomości +- ✅ Historia edytowania wiadomości +- ✅ Zmniejszenie zużycia baterii i transferu danych w dużych grupach. +- ✅ Potwierdzenie dostarczenia wiadomości (z opcją rezygnacji nadawcy dla każdego kontaktu). +- ✅ Klient desktopowy. +- ✅ Szyfrowanie plików lokalnych przechowywanych w aplikacji. +- ✅ Korzystanie z profili mobilnych z poziomu aplikacji komputerowej. +- ✅ Prywatne notatki. +- ✅ Usprawnienie wysyłania filmów (w tym szyfrowanie lokalnie przechowywanych filmów). +- ✅ Post-kwantowa wymiana kluczy w protokole double ratchet. +- 🏗 Poprawienie stabilności i zmniejszenie zużycia baterii. +- 🏗 Poprawienie odczuć dla nowych użytkowników. +- 🏗 Duże grupy, społeczności i kanały publiczne. +- 🏗 Przekaźnik dostarczania wiadomości dla nadawców (w celu ukrycia adresu IP przed serwerami odbiorców i zmniejszenia ruchu). +- Suwak prywatności i bezpieczeństwa - prosty sposób na ustawienie wszystkich ustawień za jednym zamachem. +- Redundancja i rotacja kolejek SMP (obsługiwana ręcznie). +- Dołączanie opcjonalnej wiadomości do żądania połączenia wysyłanego za pośrednictwem adresu kontaktowego. +- Ulepszona nawigacja i wyszukiwanie w konwersacji (rozwijanie i przewijanie do cytowanej wiadomości, przewijanie do wyników wyszukiwania itp.) +- Kanały/transmisje. +- Efemeryczne/znikające/jednorazowe konwersacje z istniejącymi kontaktami. +- Prywatne udostępnianie swojej lokalizacji. +- Widżety internetowe dla niestandardowej interaktywności w czatach. +- Programowalne automatyzacje czatów / reguły (automatyczne odpowiedzi / przekazywanie / usuwanie / wysyłanie, przypomnienia itp.) +- Chroniący prywatność serwer tożsamości dla opcjonalnych adresów kontaktów/grup opartych na DNS w celu uproszczenia połączenia i odnajdywania, ale nieużywany do dostarczania wiadomości: + - zachowanie wszystkich kontaktów i grup nawet w przypadku utraty domeny. + - Serwer nie posiada informacji o kontaktach i grupach użytkownika. +- Wielowęzłowe przekaźniki SMP o dużej pojemności. + +## Ostrzeżenia + +[Protokoły i model bezpieczeństwa SimpleX](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) zostały poddane przeglądowi i zawierały wiele istotnych zmian i ulepszeń w wersji v1.0.0. + +Audyt bezpieczeństwa został przeprowadzony w październiku 2022 r. przez [Trail of Bits](https://www.trailofbits.com/about), a większość poprawek została wydana w wersji 4.2.0 - zobacz [ogłoszenie](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). + +SimpleX Chat jest nadal na stosunkowo wczesnym etapie rozwoju (aplikacje mobilne zostały wydane w marcu 2022 r.), więc możesz odkryć pewne błędy i brakujące funkcje. Będziemy bardzo wdzięczni za poinformowanie nas o wszystkim, co wymaga naprawy lub ulepszenia. + +Domyślne serwery skonfigurowane w aplikacji są dostarczane na zasadzie najlepszych starań. Obecnie nie gwarantujemy żadnych umów SLA, chociaż historycznie nasze serwery miały ponad 99,9% czasu pracy. + +Nigdy nie udostępnialiśmy ani nie byliśmy proszeni o dostęp do naszych serwerów lub jakichkolwiek informacji z naszych serwerów przez osoby trzecie. Jeśli kiedykolwiek zostaniemy poproszeni o zapewnienie takiego dostępu lub informacji, będziemy postępować zgodnie z odpowiednim procesem prawnym. + +Nie rejestrujemy adresów IP użytkowników i nie przeprowadzamy żadnej korelacji ruchu na naszych serwerach. Jeśli bezpieczeństwo na poziomie transportu jest krytyczne, musisz użyć Tor lub innej podobnej sieci, aby uzyskać dostęp do serwerów wiadomości. Będziemy ulepszać aplikacje klienckie, aby zmniejszyć możliwości korelacji ruchu. + +Więcej informacji można znaleźć w [Warunkach i polityce prywatności](./PRIVACY.md). + +## Kontakt w sprawie bezpieczeństwa + +Aby zgłosić podatność wyślij nam wiadomość e-mail na adres chat@simplex.chat. Będziemy wspólnie pracować nad poprawką i ujawnieniem szczegółów. NIE zgłaszaj luk w zabezpieczeniach za pośrednictwem zgłoszeń GitHub. + +Prosimy o traktowanie wszelkich ustaleń dotyczących możliwych ataków korelacji ruchu umożliwiających skorelowanie dwóch różnych konwersacji z tym samym użytkownikiem, innych niż objęte [modelem zagrożeń](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#threat-model), jako luk w zabezpieczeniach i postępowanie zgodnie z tym procesem ujawniania. + +## Licencja + +[AGPL v3](./LICENSE) + +[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apple_store.svg" alt="iOS app" height="42">](https://apps.apple.com/us/app/simplex-chat/id1605771084) +  +[![Android app](https://github.com/simplex-chat/.github/blob/master/profile/images/google_play.svg)](https://play.google.com/store/apps/details?id=chat.simplex.app) +  +[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/f_droid.svg" alt="F-Droid" height="41">](https://app.simplex.chat) +  +[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu) +  +[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) diff --git a/docs/lang/pl/SERVER.md b/docs/lang/pl/SERVER.md new file mode 100644 index 0000000000..f4bffc1bcc --- /dev/null +++ b/docs/lang/pl/SERVER.md @@ -0,0 +1,485 @@ +--- +title: Hostowanie własnego serwera SMP +revision: 31.07.2023 +--- + +| Updated 05.06.2023 | Języki: PL, [EN](/docs/SERVER.md), [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) | + +# Hostowanie własnego serwera SMP + +## Informacje ogólne + +Serwer SMP to serwer przekaźnikowy używany do przekazywania wiadomości w sieci SimpleX. Aplikacje SimpleX Chat mają wstępnie ustawione serwery (dla aplikacji mobilnych są to smp11, smp12 i smp14.simplex.im), ale można łatwo zmienić konfigurację aplikacji, aby korzystać z innych serwerów. + +Klienty SimpleX określają tylko, który serwer jest używany do odbierania wiadomości, oddzielnie dla każdego kontaktu (lub połączenia grupowego z członkiem grupy), a serwery te są tylko tymczasowe, ponieważ adres dostawy może ulec zmianie. + +_Uwaga_: gdy zmienisz serwery w ustawieniach aplikacji, wpłynie to tylko na to, który serwer będzie używany dla nowych kontaktów, istniejące kontakty nie zostaną automatycznie przeniesione na nowe serwery, ale możesz przenieść je ręcznie za pomocą przycisku ["Zmień adres odbiorczy"](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md#change-your-delivery-address-beta) na stronie z informacjami kontaktu/członka - wkrótce zostanie to zautomatyzowane. + +## Instalacja + +1. Najpierw zainstaluj `smp-server`: + + - Manualna instalacja (patrz niżej) + + - Półautomatyczna instalacja: + - [Oficjalny skrypt instalacyjny](https://github.com/simplex-chat/simplexmq#using-installation-script) + - [Kontener Dockera](https://github.com/simplex-chat/simplexmq#using-docker) + - [Linode Marketplace](https://www.linode.com/marketplace/apps/simplex-chat/simplex-chat/) + +Instalacja ręczna wymaga kilku kroków wstępnych: + +1. Zainstaluj binarkę: + + - Używając oficjalnych binarek: + + ```sh + curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/smp-server && chmod +x /usr/local/bin/smp-server + ``` + + - Budowanie z kodu źródłowego: + + Zobacz [Build from source: Using your distribution](https://github.com/simplex-chat/simplexmq#using-your-distribution) + +2. Utwórz użytkownika i grupę dla `smp-server`: + + ```sh + sudo useradd -m smp + ``` + +3. Utwórz niezbędne katalogi i przypisz uprawnienia: + + ```sh + sudo mkdir -p /var/opt/simplex /etc/opt/simplex + sudo chown smp:smp /var/opt/simplex /etc/opt/simplex + ``` + +4. Zezwól na port `smp-server` w firewallu: + + ```sh + # Dla Ubuntu + sudo ufw allow 5223/tcp + # Dla Fedory + sudo firewall-cmd --permanent --add-port=5223/tcp && \ + sudo firewall-cmd --reload + ``` + +5. **Opcjonalnie** - Jeśli używasz dystrybucji z `systemd`, utwórz plik `/etc/systemd/system/smp-server.service` z następującą zawartością: + + ```sh + [Unit] + Description=SMP server systemd service + + [Service] + User=smp + Group=smp + Type=simple + ExecStart=/usr/local/bin/smp-server start +RTS -N -RTS + ExecStopPost=/usr/bin/env sh -c '[ -e "/var/opt/simplex/smp-server-store.log" ] && cp "/var/opt/simplex/smp-server-store.log" "/var/opt/simplex/smp-server-store.log.bak"' + LimitNOFILE=65535 + KillSignal=SIGINT + TimeoutStopSec=infinity + + [Install] + WantedBy=multi-user.target + ``` + + I uruchom `sudo systemctl daemon-reload`. + +## Instalacja Tora + +smp-server można również zainstalować jako serwer działający w sieci [tor](https://www.torproject.org). Uruchom następujące polecenia jako użytkownik `root`. + +1. Zainstaluj Tor: + + Zakładamy, że używasz dystrybucji opartych na Ubuntu/Debian. Jeśli nie, zapoznaj się z [oficjalną dokumentacją tor](https://community.torproject.org/onion-services/setup/install/) lub poradnikiem dla Twojej dystrybucji. + + - Skonfiguruj oficjalne repozytorium Tor PPA: + + ```sh + CODENAME="$(lsb_release -c | awk '{print $2}')" + echo "deb [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main + deb-src [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main" > /etc/apt/sources.list.d/tor.list + ``` + + - Zimportuj klucz repozytorium: + + ```sh + curl --proto '=https' --tlsv1.2 -sSf https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | tee /usr/share/keyrings/tor-archive-keyring.gpg >/dev/null + ``` + + - Zaktualizuj indeks repozytorium: + + ```sh + apt update + ``` + + - Zainstaluj paczkę `tor`: + + ```sh + apt install -y tor deb.torproject.org-keyring + ``` + +2. Skonfiguruj tor: + + - Konfiguracja pliku: + + Otwórz konfigurację tora w wybranym edytorze (`nano`,`vim`,`emacs`, itp.): + + ```sh + vim /etc/tor/torrc + ``` + + I umieść następujące linie na dole konfiguracji. Zwróć uwagę na linie zaczynające się od `#`: są to komentarze dotyczące poszczególnych opcji. + + ```sh + # Włącz logowanie (w przeciwnym razie tor nie wyda adresu onion). + Log notice file /var/log/tor/notices.log + # Włącz routowanie single hop (2 opcje poniżej są zależne od trzeciej). Zmniejszy to opóźnienie w zamian za anonimowość (jako że tor działa równolegle z serwerem smp i adres onion będzie wyświetlany w klientach, jest to całkowicie w porządku). + SOCKSPort 0 + HiddenServiceNonAnonymousMode 1 + HiddenServiceSingleHopMode 1 + # Katalog hostów i mapowanie portów usługi ukrytej smp-server + HiddenServiceDir /var/lib/tor/simplex-smp/ + HiddenServicePort 5223 localhost:5223 + ``` + + - Utwórz katalogi: + + ```sh + mkdir /var/lib/tor/simplex-smp/ && chown debian-tor:debian-tor /var/lib/tor/simplex-smp/ && chmod 700 /var/lib/tor/simplex-smp/ + ``` + +3. Uruchom tor: + + Włącz usługę `systemd` oraz uruchom tor. Oficjalny `tor` jest nieco kłopotliwy przy pierwszym uruchomieniu i może nie utworzyć adresu hosta cebuli, więc na wszelki wypadek uruchamiamy go ponownie. + + ```sh + systemctl enable tor && systemctl start tor && systemctl restart tor + ``` + +4. Wyświetla onionowego hosta: + + Wykonaj następujące polecenie, aby wyświetlić adres onionowego hosta: + + ```sh + cat /var/lib/tor/simplex-smp/hostname + ``` + +## Configuration + +Aby zobaczyć, jakie opcje są dostępne, wykonaj `smp-server` bez flag: + +```sh +sudo su smp -c smp-server + +... +Available commands: + init Initialize server - creates /etc/opt/simplex and + /var/opt/simplex directories and configuration files + start Start server (configuration: + /etc/opt/simplex/smp-server.ini) + delete Delete configuration and log files +``` + +Możesz uzyskać dalszą pomoc, wykonując polecenie `sudo su smp -c "smp-server <command> -h"` + +Następnie musimy skonfigurować `smp-server`: + +### Interaktywnie + +Wykonaj poniższe polecenie: + +```sh +sudo su smp -c "smp-server init" +``` + +Istnieje kilka opcji, które należy rozważyć: + +- `Enable store log to restore queues and messages on server restart (Yn):` + + Wpisz `y`, aby włączyć zapisywanie i przywracanie połączeń i wiadomości po ponownym uruchomieniu serwera. + + _Uwaga_: ważne jest, aby użyć SIGINT do ponownego uruchomienia serwera, ponieważ w przeciwnym razie niedostarczone wiadomości nie zostaną przywrócone. Połączenia zostaną przywrócone niezależnie od sposobu ponownego uruchomienia serwera, ponieważ w przeciwieństwie do wiadomości są one dodawane do dziennika append-only. + +- `Enable logging daily statistics (yN):` + + Wpisz `y`, aby włączyć logowanie statystyk w formacie CSV, mogą one być przykładowo użyte do pokazania wykresów użycia w `Grafana`. + +Statystyki te obejmują dzienną liczbę utworzonych, zabezpieczonych i usuniętych kolejek, wysłanych i odebranych wiadomości, a także dzienną, tygodniową i miesięczną liczbę aktywnych kolejek (tj. kolejek, które były używane do wysyłania wiadomości). Uważamy, że informacje te nie zawierają niczego, co pozwoliłoby na skorelowanie różnych kolejek jako należących bezpośrednio do użytkowników, ale prosimy o poufne poinformowanie nas, jeśli uważasz, że można to w jakikolwiek sposób nadużyć. + +- `Require a password to create new messaging queues?` + + Wpisz `r` lub dowolne hasło, aby zabezpieczyć hasłem `smp-server`, lub `n`, aby wyłączyć ochronę hasłem. + +- `Enter server FQDN or IP address for certificate (127.0.0.1):` + + Wprowadź domenę lub adres IP, na którym działa Twój smp-server - zostanie on zamieszczony w certyfikatach serwera, a także wyświetlony jako część adresu serwera. + +### Za pomocą opcji wiersza poleceń + +Wykonaj poniższe polecenie: + +```sh +sudo su smp -c "smp-server init -h" + +... +Available options: + -l,--store-log Enable store log for persistence + -s,--daily-stats Enable logging daily server statistics + -a,--sign-algorithm ALG Signature algorithm used for TLS certificates: + ED25519, ED448 (default: ED448) + --ip IP Server IP address, used as Common Name for TLS online + certificate if FQDN is not supplied + (default: "127.0.0.1") + -n,--fqdn FQDN Server FQDN used as Common Name for TLS online + certificate + --no-password Allow creating new queues without password + --password PASSWORD Set password to create new messaging queues + -y,--yes Non-interactive initialization using command-line + options + -h,--help Show this help text +``` + +Powinieneś określić, które flagi są potrzebne dla Twojego zastosowania, a następnie wykonać `smp-server init` z flagą `-y` dla nieinteraktywnej inicjalizacji: + +```sh +sudo su smp -c "smp-server init -y -<your flag> <your option>" +``` + +Przykładowo, uruchom: + +```sh +sudo su smp -c "smp-server init -y -l --ip 192.168.1.5 --password test" +``` + +aby zainicjować konfigurację `smp-server` z: + +- przywracaniem połączeń i wiadomości po ponownym uruchomieniu serwera (flaga `-l`), +- adresem IP `192.168.1.5`, +- zabezpieczeniem `smp-server` hasłem `test`. + +--- + +Po tym instalacja jest ukończona i powinieneś zobaczyć coś takiego: + +```sh +Certificate request self-signature ok +subject=CN = 127.0.0.1 +Server initialized, you can modify configuration in /etc/opt/simplex/smp-server.ini. +Run `smp-server start` to start server. +---------- +You should store CA private key securely and delete it from the server. +If server TLS credential is compromised this key can be used to sign a new one, keeping the same server identity and established connections. +CA private key location: /etc/opt/simplex/ca.key +---------- +SMP server v3.4.0 +Fingerprint: d5fcsc7hhtPpexYUbI2XPxDbyU2d3WsVmROimcL90ss= +Server address: smp://d5fcsc7hhtPpexYUbI2XPxDbyU2d3WsVmROimcL90ss=:V8ONoJ6ICwnrZnTC_QuSHfCEYq53uLaJKQ_oIC6-ve8=@<hostnames> +``` + +Powyższy adres serwera powinien być użyty w konfiguracji klienta, a jeśli dodałeś hasło serwera, powinno ono być udostępnione innym osobom tylko wtedy, gdy chcesz zezwolić im na korzystanie z Twojego serwera do odbierania wiadomości (wszystkie Twoje kontakty będą mogły wysyłać wiadomości, ponieważ nie wymaga to hasła). Jeśli podałeś adres IP lub nazwę hosta podczas instalacji, zostanie to wyświetlone jako część adresu serwera, w przeciwnym razie zastąp `<hostnames>` rzeczywistymi adresami serwerów. + +## Dokumentacja + +Wszystkie niezbędne pliki dla `smp-server` znajdują się w folderze `/etc/opt/simplex/`. + +Przechowywane wiadomości, połączenia, statystyki i dziennik serwera znajdują się w folderze `/var/opt/simplex/`. + +### Adres serwera SMP + +Adres serwera SMP ma następujący format: + +``` +smp://<odcisk_palca>[:<hasło>]@<publiczna_nazwa_hosta>[,<onionowa_nazwa_hosta>] +``` + +- `<odcisk_palca>` + + To odcisk palca certyfikatu Twojego `smp-server`. Odcisk palca certyfikatu możesz sprawdzić w `/etc/opt/simplex/fingerprint`. + +- **opcjonalnie** `<hasło>` + + To ustawione przez Ciebie hasło Twojego `smp-server`. Możesz sprawdzić to hasło w pliku `/etc/opt/simplex/smp-server.ini`, w sekcji `[AUTH]` w polu `create_password:`. + +- `<publiczna_nazwa_hosta>`, **optional** `<onionowa_nazwa_hosta>` + + To skonfigurowane przez Ciebie nazwy hosta Twojego `smp-server`. Nazwy hostów możesz sprawdzić w pliku `/etc/opt/simplex/smp-server.ini`, w sekcji `[TRANSPORT]` w polu `host:`. + +### Komendy systemd + +Aby uruchomić `smp-server` przy starcie hosta, uruchom: + +```sh +sudo systemctl enable smp-server.service + +Created symlink /etc/systemd/system/multi-user.target.wants/smp-server.service → /etc/systemd/system/smp-server.service. +``` + +Aby uruchomić `smp-server`, uruchom: + +```sh +sudo systemctl start smp-server.service +``` + +Aby sprawdzić status `smp-server`, uruchom: + +```sh +sudo systemctl status smp-server.service + +● smp-server.service - SMP server + Loaded: loaded (/etc/systemd/system/smp-server.service; enabled; vendor preset: enabled) + Active: active (running) since Sat 2022-11-23 19:23:21 UTC; 1min 48s ago + Main PID: 30878 (smp-server) + CGroup: /docker/5588ab759e80546b4296a7c50ffebbb1fb7b55b8401300e9201313b720989aa8/system.slice/smp-server.service + └─30878 smp-server start + +Nov 23 19:23:21 5588ab759e80 systemd[1]: Started SMP server. +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: SMP server v3.4.0 +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: Fingerprint: d5fcsc7hhtPpexYUbI2XPxDbyU2d3WsVmROimcL90ss= +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: Server address: smp://d5fcsc7hhtPpexYUbI2XPxDbyU2d3WsVmROimcL90ss=:V8ONoJ6ICwnrZnTC_QuSHfCEYq53uLaJKQ_oIC6-ve8=@<hostnames> +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: Store log: /var/opt/simplex/smp-server-store.log +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: Listening on port 5223 (TLS)... +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: not expiring inactive clients +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: creating new queues requires password +``` + +Aby zatrzymać `smp-server`, uruchom: + +```sh +sudo systemctl stop smp-server.service +``` + +Aby sprawdzić zawartość dziennika `smp-server`, uruchom: + +```sh +sudo journalctl -fu smp-server.service + +Nov 23 19:23:21 5588ab759e80 systemd[1]: Started SMP server. +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: SMP server v3.4.0 +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: Fingerprint: d5fcsc7hhtPpexYUbI2XPxDbyU2d3WsVmROimcL90ss= +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: Server address: smp://d5fcsc7hhtPpexYUbI2XPxDbyU2d3WsVmROimcL90ss=:V8ONoJ6ICwnrZnTC_QuSHfCEYq53uLaJKQ_oIC6-ve8=@<hostnames> +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: Store log: /var/opt/simplex/smp-server-store.log +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: Listening on port 5223 (TLS)... +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: not expiring inactive clients +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: creating new queues requires password +``` + +### Monitorowanie + +Możesz włączyć statystyki `smp-server` dla dashboardu `Grafana` ustawiając wartość `on` w `/etc/opt/simplex/smp-server.ini`, w sekcji `[STORE_LOG]` w polu `log_stats:`. + +Logi będą przechowywane jako plik `csv` w `/var/opt/simplex/smp-server-stats.daily.log`. Pola dla pliku `csv` to: + +```sh +fromTime,qCreated,qSecured,qDeleted,msgSent,msgRecv,dayMsgQueues,weekMsgQueues,monthMsgQueues +``` + +- `fromTime` - timestamp; data i godzina zdarzenia + +- `qCreated` - int; utworzone kolejki + +- `qSecured` - int; ustanowione kolejki + +- `qDeleted` - int; usunięte queues + +- `msgSent` - int; wysłane wiadomości + +- `msgRecv` - int; odebrane wiadomości + +- `dayMsgQueues` - int; aktywnych kolejek podczas dnia + +- `weekMsgQueues` - int; aktywnych kolejek w tygodniu + +- `monthMsgQueues` - int; aktywnych kolejek w miesiącu + +Aby zaimportować `csv` do `Grafana` należy: + +1. Zainstalować wtyczkę Grafana: [Grafana - CSV datasource](https://grafana.com/grafana/plugins/marcusolsson-csv-datasource/) + +2. Zezwolić na tryb lokalny, dołączając następujące elementy: + + ```sh + [plugin.marcusolsson-csv-datasource] + allow_local_mode = true + ``` + + ... do `/etc/grafana/grafana.ini` + +3. Dodaj źródło danych CSV: + + - W menu bocznym kliknij zakładkę Configuration (ikona koła zębatego) + - Kliknij Add data source (Dodaj źródło danych) w prawym górnym rogu zakładki Data Sources (Źródła danych). + - Wpisz "CSV" w polu wyszukiwania, aby znaleźć źródło danych CSV. + - Kliknij wynik wyszukiwania z napisem "CSV". + - W polu URL wprowadź plik wskazujący na zawartość CSV. + +4. Gotowe! Teraz możesz utworzyć własny pulpit nawigacyjny ze statystykami. + +Dalsza dokumentacja znajduje się na stronie: [CSV Data Source for Grafana - Documentation](https://grafana.github.io/grafana-csv-datasource/). + +# Aktualizowanie twojego serwera SMP + +Aby zaktualizować smp-server do najnowszej wersji, wybierz metodę instalacji i postępuj zgodnie z instrukcjami: + + - Manualnie + 1. Zatrzymaj serwer: + ```sh + sudo systemctl stop smp-server + ``` + 2. Zaktualizuj binarkę: + ```sh + curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/smp-server && chmod +x /usr/local/bin/smp-server + ``` + 3. Uruchom serwer: + ```sh + sudo systemctl start smp-server + ``` + + - Używając [oficjalnego skryptu instalacyjnego](https://github.com/simplex-chat/simplexmq#using-installation-script) + 1. Uruchom: + ```sh + sudo simplex-servers-update + ``` + 2. Gotowe! + + - Używając [kontenera Dockera](https://github.com/simplex-chat/simplexmq#using-docker) + 1. Zatrzymaj i usuń kontener: + ```sh + docker rm $(docker stop $(docker ps -a -q --filter ancestor=simplexchat/smp-server --format="\{\{.ID\}\}")) + ``` + 2. Pobierz najnowszą wersję kontenera: + ```sh + docker pull simplexchat/smp-server:latest + ``` + 3. Uruchom nowy kontener: + ```sh + docker run -d \ + -p 5223:5223 \ + -v $HOME/simplex/smp/config:/etc/opt/simplex:z \ + -v $HOME/simplex/smp/logs:/var/opt/simplex:z \ + simplexchat/smp-server:latest + ``` + + - [Linode Marketplace](https://www.linode.com/marketplace/apps/simplex-chat/simplex-chat/) + 1. Pobierz najnowsze obrazy: + ```sh + docker-compose --project-directory /etc/docker/compose/simplex pull + ``` + 2. Zrestartuj kontenery: + ```sh + docker-compose --project-directory /etc/docker/compose/simplex up -d --remove-orphans + ``` + 3. Usuń niepotrzebne obrazy: + ```sh + docker image prune + ``` + +### Konfigurowanie aplikacji do korzystania z serwera + +Aby skonfigurować aplikację do korzystania z serwera wiadomości, skopiuj jego pełny adres, w tym hasło, i dodaj go do aplikacji. Możesz używać swojego serwera razem z predefiniowanymi serwerami lub bez nich - możesz je usunąć lub wyłączyć. + +Możliwe jest również udostępnienie adresu swojego serwera znajomym, pozwalając im zeskanować kod QR z ustawień serwera - będzie on zawierał hasło serwera, dzięki czemu będą mogli również otrzymywać wiadomości za pośrednictwem twojego serwera. + +_Uwaga_: Do obsługi haseł wymagany jest serwer SMP w wersji 4.0. Jeśli już posiadasz serwer, możesz dodać hasło do niego poprzez wpisanie hasła do pliku INI serwera. + +<img src="./server_config_1.png" width="288">    <img src="./server_config_2.png" width="288">    <img src="./server_config_3.png" width="288"> diff --git a/docs/lang/pl/SIMPLEX.md b/docs/lang/pl/SIMPLEX.md new file mode 100644 index 0000000000..a1ceb8c5e4 --- /dev/null +++ b/docs/lang/pl/SIMPLEX.md @@ -0,0 +1,102 @@ +--- +title: Platfoma SimpleX +revision: 07.02.2023 +--- + +| Updated 07.02.2023 | Języki: PL, [EN](/docs/SIMPLEX.md), [FR](/docs/lang/fr/SIMPLEX.md), [CZ](/docs/lang/cs/SIMPLEX.md) | +# Platfoma SimpleX - motywacja i porównanie + +## Problemy + +Istniejące komunikatory oraz protokoły borykają się ze wszystkimi lub kilkoma podanymi problemami: + +- Brak zachowania prywatności profilu i kontaktów użytkownika (zachowanie poufności metadanych). +- Brak ochrony (lub jedynie opcjonalna ochrona) przed atakami MITM przez dostawcę usług przy użyciu szyfrowania [end to end][1] +- Niechciane wiadomości (spam i nadużycia). +- Brak własności danych i ich ochrony. +- Dla nietechnicznych użytkowników używanie niescentralizowanych protokołów jest skomplikowane. + +Koncentracja komunikacji na niewielkiej liczbie scentralizowanych platform sprawia, że rozwiązanie tych problemów jest dość trudne. + +## Proponowane rozwiązanie + +Proponowany zestaw protokołów pozwala rozwiązać te problemy poprzez przechowywanie zarówno wiadomości, jak i kontaktów wyłącznie na urządzeniach klienckich, redukując rolę serwerów do zwykłych przekaźników wiadomości. Wymagają one jedynie autoryzacji wiadomości wysyłanych do kolejek, ale NIE wymagają uwierzytelniania użytkowników - dzięki temu chronione są nie tylko wiadomości, ale także metadane, ponieważ użytkownicy nie mają przypisanych do siebie żadnych identyfikatorów - w przeciwieństwie do innych platform. + +Zobacz [whitepaper](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) po więcej informacji o zadaniach platformy oraz by dowiedzieć się jak wygląda koncepcja techniczna modelu. + +## Dlaczego SimpleX + +## SimpleX podchodzi do problemu prywatności i bezpieczeństwa w unikalny sposób + +Każdy powinien zwracać uwagę na prywatność i bezpieczeństwo swojej komunikacji - nawet zwykłe rozmowy mogą narazić Cię na niebezpieczeństwo. + +### Pełna prywatność Twojej tożsamości, profilu, kontaktu i metadanych + +**W przeciwieństwie do innych komunikatorów, SimpleX nie posiada żadnych identyfikatorów przypisanych do użytkowników** - nie wymaga użycia numeru telefonu (jak Signal czy Whatsapp), adresu opartego o domenę (jak email, XMPP czy Matrix), nazw użytkownika (jak Telegram), kluczy publicznych czy nawet losowych numerów (jak pozostałe komunikatory) do identyfikowania użytkowników - nie wiemy nawet ile osób używa SimpleX. + +Do dostarczania wiadomości zamiast identyfikatorów użytkowników, których używają wszystkie inne platformy, SimpleX wykorzystuje adresy jednokierunkowych (simpleksowych) kolejek wiadomości. Korzystanie z SimpleX jest jak posiadanie innego adresu e-mail lub numeru telefonu dla każdego kontaktu, ale bez kłopotów z zarządzaniem tymi wszystkimi adresami. W niedalekiej przyszłości aplikacje SimpleX będą również automatycznie zmieniać kolejki wiadomości, przenosząc konwersacje z jednego serwera na drugi, aby zapewnić użytkownikom jeszcze lepszą prywatność. + +Takie podejście chroni prywatność tego, z kim się komunikujesz, ukrywając jego tożsamość oraz fakt komunikacji przed serwerami platformy SimpleX i wszelkimi obserwatorami. Prywatność komunikacji można dodatkowo zwiększyć, konfigurując dostęp do sieci w taki sposób, by łączyć się z serwerami SimpleX za pośrednictwem sieci transportowej typu overlay, np. sieci Tor. + +### Najlepsza ochrona przed spamem i nadużyciami + +Ponieważ nie masz żadnego identyfikatora na platformie SimpleX, nie można się z Tobą skontaktować, chyba że udostępnisz jednorazowy link z zaproszeniem lub opcjonalny tymczasowy adres użytkownika. Nawet przy użyciu opcjonalnych adresów użytkownika, które mogą być wykorzystywane do wysyłania spamu z prośbami o kontakt, można je zmienić lub całkowicie usunąć bez utraty jakichkolwiek połączeń (kontaktów). + +### Pełna kontrola i bezpieczeństwo Twoich danych + +SimpleX przechowuje wszystkie dane użytkownika na urządzeniach klienckich, wiadomości są przetrzymywane tylko tymczasowo na serwerach przekaźnikowych SimpleX do momentu ich odebrania, po czym są trwale usuwane. + +Używamy przenośnego formatu bazy danych, który może być używany na wszystkich obsługiwanych urządzeniach - wkrótce dodamy możliwość eksportu bazy danych czatu z aplikacji mobilnej, aby można było jej używać na innym urządzeniu. + +W przeciwieństwie do serwerów sieci federowanych (e-mail, XMPP lub Matrix), serwery SimpleX nie przechowują kont użytkowników, a jedynie przekazują wiadomości do odbiorców, chroniąc prywatność obu stron. Nie ma żadnych identyfikatorów ani zaszyfrowanych wiadomości występujących wspólnie z wysłanym i odbieranym ruchem serwera, dzięki dodatkowej warstwie szyfrowania dostarczanych wiadomości. Jeśli więc ktoś obserwuje ruch na serwerze, nie może łatwo określić, kto komunikuje się z kim (sprawdź [SimpleX whitepaper](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) by dowiedzieć się o znanych atakach korelacji ruchu). + +### Użytkownicy są właścicielami sieci SimpleX + +Możesz używać SimpleX na własnych serwerach i nadal komunikować się z ludźmi za pomocą serwerów, które są wstępnie skonfigurowane w aplikacjach lub z dowolnymi innymi serwerami SimpleX. + +Platforma SimpleX korzysta z otwartego protokołu i zapewnia zestaw SDK do tworzenia czatbotów, umożliwiając implementację usług, z którymi użytkownicy mogą wchodzić w interakcje za pośrednictwem aplikacji SimpleX Chat - naprawdę nie możemy się doczekać, aby zobaczyć, jakie usługi oparte o SimpleX można stworzyć. + +Jeśli rozważasz stworzenie czegoś w oparciu o platformę SimpleX, niezależnie od tego, czy chodzi o usługi czatbotów dla użytkowników aplikacji SimpleX, czy też integrację biblioteki SimpleX Chat z aplikacjami mobilnymi, skontaktuj się z nami, aby uzyskać porady i wsparcie. + +## Porównanie z innymi protokołami + +| | SimpleX Chat | Signal, duże platformy | XMPP, Matrix | Protokoły P2P | +| :---------------------------------------------------------- | :----------------------: | :--------------------: | :-------------: | :-------------: | +| Wymaga identyfikatorów użytkownika | Nie = prywatny | Tak<sup>1</sup> | Tak<sup>2</sup> | Tak<sup>3</sup> | +| Możliwość ataku MITM | Nie = bezpieczny | Tak<sup>4</sup> | Tak | Tak | +| Polega na DNS | Nie = odporny na cenzurę | Tak | Tak | Nie | +| Pojedynczy operator lub sieć | Nie = zdecentralizowany | Tak | Nie | Tak<sup>5</sup> | +| Scentralizowanie lub możliwość ataku obejmującego całą sieć | Nie = odporny na cenzurę | Tak | Tak<sup>2</sup> | Tak<sup>6</sup> | + +1. Zwykle opiera się na numerze telefonu, w niektórych przypadkach na nazwie użytkownika. +2. Bazuje na DNS. +3. Klucz publiczny lub inny globalnie unikalny identyfikator. +4. Jeśli serwery operatora zostaną przejęte. +5. Mimo że sieci P2P i sieci oparte na kryptowalutach są rozproszone, nie są w pełni zdecentralizowane - działają jako pojedyncza sieć, z pojedynczą przestrzenią nazw adresów użytkowników. +6. Sieci P2P albo mają jakiś centralny serwer, albo cała sieć może zostać przejęta - patrz następna sekcja. + +## Porównanie z komunikatorami [P2P][9] + +Istnieje kilka protokołów czatu/wiadomości P2P i implementacji, które mają na celu rozwiązanie problemu prywatności i centralizacji, ale mają one swój własny szereg problemów, które sprawiają, że są mniej niezawodne niż proponowany projekt, są bardziej skomplikowane w implementacji i analizie oraz są bardziej podatne na ataki. + +1. Sieci [P2P][9] korzystają z jakiegoś rodzaju [DHT][10] do routowania wiadomości/zapytań po sieci. Implementacje DHT mają złożone konstrukcje, muszą równoważyć niezawodność, gwarancję dostawy i czas oczekiwania. Proponowany model zapewnia zarówno większą gwarancję dostarczalności, jak i mniejsze opóźnienia (wiadomość jest przekazywana wiele razy równolegle, za każdym razem przez jeden węzeł, przy użyciu serwerów wybranych przez odbiorcę, podczas gdy w sieciach P2P wiadomość jest przekazywana przez `O(log N)` węzłów sekwencyjnie, przy użyciu węzłów wybranych przez algorytm). + +2. Proponowany model, w przeciwieństwie do większości sieci P2P, nie posiada żadnych globalnych identyfikatorów użytkowników, nawet tymczasowych. + +3. P2P samo w sobie nie rozwiązuje problemu [ataku MITM][2], a większość istniejących rozwiązań nie wykorzystuje komunikacji out-of-band do początkowej wymiany kluczy. Proponowany projekt wykorzystuje wiadomości out-of-band lub (w niektórych przypadkach) istniejące wcześniej bezpieczne i zaufane połączenia do początkowej wymiany kluczy. + +4. Implementacje P2P mogą być blokowane przez niektórych dostawców Internetu (tak jak [BitTorrent][11]). Proponowany model jest niezależny od rodzaju transmisji - może działać na standardowych protokołach sieciowych, a serwery mogą działać na tych samych domenach, co strony internetowe. + +5. Wszystkie znane sieci P2P mogą być podatne na [atak typu Sybil][12], ponieważ każdy węzeł jest wykrywalny, a sieć działa jako całość. Znane środki mające na celu zmniejszenie prawdopodobieństwa ataku typu Sybil wymagają zastosowania scentralizowanego komponentu lub kosztownego [proof of work][13]. Proponowany model, przeciwnie, nie ma możliwości wykrycia serwera - serwery nie są połączone, nie są znane sobie nawzajem i wszystkim klientom. Sieć SimpleX jest pofragmentowana i działa jako wiele odizolowanych połączeń. Uniemożliwia to ataki na całą sieć SimpleX - nawet jeśli niektóre serwery są zagrożone, inne części sieci mogą działać normalnie, a dotknięci atakiem użytkownicy mogą przełączyć się na inne serwery bez utraty kontaktów lub wiadomości. + +6. Sieci P2P są prawdopodobnie [podatne][14] na [atak DRDoS][15]. W proponowanym modelu klienci przekazują tylko ruch ze znanych zaufanych połączeń i nie mogą być wykorzystywani do odbijania i wzmacniania ruchu w całej sieci. + +[1]: https://pl.wikipedia.org/wiki/Szyfrowanie_od_ko%C5%84ca_do_ko%C5%84ca +[2]: https://pl.wikipedia.org/wiki/Atak_man_in_the_middle +[9]: https://pl.wikipedia.org/wiki/Peer-to-peer +[10]: https://pl.wikipedia.org/wiki/Rozproszona_tablica_mieszaj%C4%85ca +[11]: https://pl.wikipedia.org/wiki/BitTorrent +[12]: https://en.wikipedia.org/wiki/Sybil_attack +[13]: https://pl.wikipedia.org/wiki/Proof_of_Work +[14]: https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent +[15]: https://pl.wikipedia.org/wiki/DRDoS diff --git a/docs/lang/pl/TRANSLATIONS.md b/docs/lang/pl/TRANSLATIONS.md new file mode 100644 index 0000000000..36daa5a148 --- /dev/null +++ b/docs/lang/pl/TRANSLATIONS.md @@ -0,0 +1,104 @@ +--- +title: Współtworzenie tłumaczenia SimpleX Chat +revision: 19.03.2023 +--- + +| 19.03.2023 | PL, [EN](/docs/TRANSLATIONS.md), [CZ](/docs/lang/cs/TRANSLATIONS.md), [FR](/docs/lang/fr/TRANSLATIONS.md)| + +# Współtworzenie tłumaczenia SimpleX Chat + +Dziękujemy za zainteresowanie się tłumaczeniem SimpleX Chat - to bardzo pomaga w uczynieniu go dostępnym dla szerszego grona użytkowników i naprawdę doceniamy Twoją pomoc. + +Wymaga to znacznej inwestycji czasu - większość ludzi tego początkowo nie docenia - oraz stałej opieki w miarę rozwoju aplikacji. + +Ten dokument został stworzony, po to by przyspieszyć ten proces i podzielić się kilkoma ważnymi "gafami", które odkryliśmy podczas pracy z Weblate - platformą, której używamy do tłumaczeń interfejsu. + +## Zanim rozpoczniesz tłumaczenie + +1. Utwórz konto w Weblate, używając tego samego adresu e-mail, którego używasz na platformie GitHub - dzięki temu Twój wkład będzie powiązany z kontem GitHub, co może okazać się dla Ciebie przydatne w niektórych przypadkach. Gdy tłumaczenie zostanie udostępnione użytkownikom, dodamy nazwę twojego konta do [listy tłumaczy] (https://github.com/simplex-chat/simplex-chat#translate-the-apps), chyba że poprosisz nas, abyśmy tego nie robili. + +2. Przed rozpoczęciem tłumaczenia należy podpisać prostą umowę licencyjną za pośrednictwem Weblate - ma to na celu uniknięcie konfliktów związanych z prawami własności intelektualnej. Kopia tej umowy jest również [dostępna tutaj](https://github.com/simplex-chat/cla/blob/master/CLA.md). + +3. Możemy również dodać Cię do grupy tłumaczy w przypadku jakichkolwiek pytań i aktualizacji - skontaktuj się z programistami za pośrednictwem czatu (po zainstalowaniu aplikacji lub później, poprzez "Wyślij pytania i pomysły" w ustawieniach aplikacji). + +## Proces tłumaczenia + +Najłatwiej jest najpierw przetłumaczyć aplikację na Androida, a dopiero później aplikację na iOS, ponieważ przetłumaczone ciągi Androidowej aplikacji są skonfigurowane jako słownik dla iOS. + +Kroki są następujące: + +1. [Tłumaczysz aplikację na Androida](#translating-android-app) w Weblate. +2. [Sprawdzamy i publikujemy tłumaczenia aplikacji na Androida](#releasing-android-app-translations). +3. Sprawdzasz tłumaczenia w aplikacji i poprawiasz ewentualne błędy. +4. [Tłumaczysz aplikację iOS w Weblate](#translating-ios-app). +5. Sprawdzamy i publikujemy tłumaczenia aplikacji iOS. + +### Tłumaczenie aplikacji na Androida + +1. Zacznij od [aplikacji na Androida](https://hosted.weblate.org/projects/simplex-chat/android/), zarówno podczas wykonywania najbardziej czasochłonnego tłumaczenia wstępnego, jak i dodawania ciągów później. Ze względu na to, że po pierwsze, ciągi w systemie iOS mogą pojawiać się w Weblate z pewnym opóźnieniem, ponieważ wymagają ręcznego zatwierdzenia z naszej strony, zanim będą widoczne, a po drugie, aplikacja na Androida jest skonfigurowana jako słownik dla aplikacji na iOS. 2/3 wszystkich ciągów wymaga tylko kliknięcia, aby przenieść je z Androida na iOS (nadal zajmuje to trochę czasu, Weblate niestety tego nie automatyzuje). + +2. Niektóre ciągi nie wymagają tłumaczenia, ale nadal trzeba je skopiować - w interfejsie użytkownika weblate znajduje się odpowiedni przycisk: + +<img src="./images/weblate_1.png" alt="weblate: copy source to translation" width="100%"> + +3. Weblate posiada również automatyczne sugestie, które mogą przyspieszyć ten proces. Czasami mogą być używane w niezmienionej formie, a czasami wymagają edycji - kliknij, aby użyć ich w tłumaczeniach. + +4. Zwróć również uwagę na Klucz ciągu (znajduje się po prawej stronie ekranu) - może on dać ci podpowiedź, co ten ciąg oznacza, gdy jego znaczenie jest niejasne. Przykładowo, klucz dla " Dodatkowy akcent" ( nie wiadomo) to "color_primary_variant" (nieco bardziej jasne, że odnosi się do koloru używanego w aplikacji). + +5. Gdy wszystkie ciągi w aplikacji na Androida zostaną przetłumaczone, przejrzyj je, aby zapewnić spójny styl i język, tak aby te same słowa były konsekwentnie używane do podobnych działań użytkownika, tak samo jak w języku angielskim. Czasami będziesz musiał użyć różnych słów w przypadkach, gdy angielski ma tylko jedno, spróbuj użyć tych wyborów spójnie w podobnych kontekstach, aby uprościć obsługę użytkownikom końcowym. + +Prosimy również o sprawdzenie tłumaczeń przy użyciu przeglądarki Chrome i funkcji *Tłumacz na angielski* w trybie _Przeglądaj_ w weblate - tak będziemy sprawdzać tłumaczenia przed ich opublikowaniem. Popraw wszelkie błędy i dodaj komentarze w przypadkach, gdy uzasadnione jest użycie różnych tłumaczeń - znacznie przyspieszy to weryfikację. + +### Udostępnianie tłumaczeń dla aplikacji na Androida + +Gdy aplikacja na Androida zostanie przetłumaczona, poinformuj nas o tym. + +My wtedy: + - przejrzymy wszystkie tłumaczenia i zasugerujemy ewentualne poprawki - to również zajmie trochę czasu :) + - scalimy je z kodem źródłowym - w tym czasie weblate będzie ustawiony na blokadę zmian. + - stworzymy wersje beta aplikacji na iOS i Androida - możemy również dodać Cię do wewnętrznych grup testerów, abyś mógł zainstalować aplikacje przed innymi. + - udostępnimy ją naszym użytkownikom korzystającym z wersji beta - już ponad tysiąc osób korzysta z wersji beta. + - wydamy aplikację i uwzględnimy nowy język w ogłoszeniu. + +### Tłumaczenie aplikacji iOS + +1. Podczas tłumaczenia [aplikacji iOS](https://hosted.weblate.org/projects/simplex-chat/ios/) duża część ciągów jest dokładnie taka sama - można je skopiować jednym kliknięciem w sekcji słowniczka. Wskazówką jest podświetlenie całego ciągu źródłowego na żółto. Wiele innych ciągów jest bardzo do siebie podobnych, różnią się jedynie składnią lub sposobem pogrubienia czcionki - wymagają one minimalnej edycji. Istnieją jednak pewne ciągi które są unikalne dla platformy iOS - należy je przetłumaczyć osobno + +2. Przejrzyj tłumaczenia na iOS w taki sam sposób jak na Androida i daj nam znać, kiedy będą gotowe do sprawdzenia - powtórzymy ten sam proces dla aplikacji na iOS. + +Serdecznie dziękujemy! To ogromny wysiłek i wielka pomoc dla rozwoju sieci SimpleX. + +<img src="./images/weblate_2.png" alt="weblate: automatic suggestions" width="100%"> + +## Częste błędy w tłumaczeniu + +1. Słowo "chat" jest używane w kilku znaczeniach, w zależności od kontekstu. Może ono oznaczać "aplikację SimpleX Chat" (np. w opcji Rozpocznij/zatrzymaj czat) lub "pojedynczą rozmowę". Jeśli nie jest to jasne, zapytaj się nas, a my dodamy więcej uwag dotyczących tłumaczenia. + +2. Prosimy o używanie liczby mnogiej i pojedynczej tak jak w oryginalnych ciągach, w przeciwnym razie może to zmienić ich znaczenie. Przykładowo, niektóre ustawienia mają zastosowanie do wszystkich kontaktów, a niektóre tylko do jednego kontaktu, będzie to mylące dla użytkownika, jeśli użyjesz liczby mnogiej w obu przypadkach. + +3. Aplikacja używa "Passcode" do zapewnienia dostępu, a nie "hasła" ("password") - w wielu językach jest to tłumaczone jako "kod dostępu". Baza danych używa "Passphrase" - w wielu językach jest to tłumaczone jako "hasło". Prosimy o spójne używanie tych słów. + +4. "Rola" użytkownika. To słowo odnosi się do zestawu uprawnień posiadanych przez użytkownika, może to być "właściciel", "administrator", "członek" lub "obserwator" (najniższe uprawnienie, które pozwala tylko na czytanie wiadomości i dodawanie reakcji na wiadomości). Tłumaczenie tego jako "tożsamość" lub "funkcja" może być nieprawidłowe. + +5. "Moderate" / "moderated" ("moderować" / "zmoderowany"). Te słowa oznaczają odpowiednio "usunięcie wiadomości innego użytkownika" i "usunięcie przez administratora". Ta funkcja jest używana, gdy członek wysyła wiadomość, która nie jest odpowiednia dla grupy. Wiele języków ma podobne słowa. + +## Jak sprawdzamy tłumaczenia + +Aby zweryfikować poprawność tłumaczeń, sprawdzamy tłumaczenia poprzez przeglądanie stron Weblate w przeglądarce Google Chrome w trybie "Tłumacz na angielski". Na przykład, aby sprawdzić niemieckie tłumaczenia interfejsu Androida, ktoś z naszego zespołu przewinął [te 68 stron] (https://hosted.weblate.org/browse/simplex-chat/android/de/). + +Nie oczekujemy, że odwrócone tłumaczenie będzie dokładnie takie samo jak oryginał, rzadko się to zdarza, ale że będzie ogólnie poprawne. + +Znacznie ułatwiłoby to recenzję, gdybyś mógł wcześniej sprawdzić to w ten sam sposób i skomentować wszystkie przypadki, w których odwrócone tłumaczenia są zupełnie inne (mogą istnieć uzasadnione przypadki). + +## Co dalej + +1. W miarę aktualizowania aplikacji będziemy publikować aktualizacje w grupie tłumaczy. Nie masz absolutnie żadnego obowiązku tłumaczenia tych dodatkowych ciągów. Niemniej jednak bardzo docenimy, jeśli to zrobisz, ponieważ sprawia to, że użytkownicy mają o wiele lepsze wrażenia, gdy polegają na Twoich tłumaczeniach, niż gdyby jakaś nowa część aplikacji nie została przetłumaczona. + +2. Możesz jeszcze bardziej pomóc w popularyzacji SimpleX w swoim kraju / grupie językowej, tłumacząc [naszą stronę internetową](https://simplex.chat) (również [przez weblate](https://hosted.weblate.org/projects/simplex-chat/website/)) i/lub [dokumenty GitHub](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang) (jest to możliwe tylko przez git)! + +3. Ponadto, jeśli chcesz być moderatorem / administratorem grupy użytkowników w swoim języku, po przetłumaczeniu aplikacji możemy hostować taką grupę - przygotowujemy wytyczne dla społeczności i dodajemy kilka narzędzi moderacyjnych do aplikacji, która zostanie wydana w wersji 4.6 w marcu. + + +Jeszcze raz bardzo dziękujemy za pomoc w rozwoju SimpleX Chat! + +Evgeny, założyciel SimpleX Chat. diff --git a/docs/lang/pl/WEBRTC.md b/docs/lang/pl/WEBRTC.md new file mode 100644 index 0000000000..d279491fb8 --- /dev/null +++ b/docs/lang/pl/WEBRTC.md @@ -0,0 +1,158 @@ +--- +title: Korzystanie z niestandardowych serwerów WebRTC ICE w SimpleX Chat +revision: 31.01.2023 +--- + +| Updated 31.01.2023 | Języki: PL, [EN](/docs/WEBRTC.md), [FR](/docs/lang/fr/WEBRTC.md), [CZ](/docs/lang/cs/WEBRTC.md) | + +# Korzystanie z niestandardowych serwerów WebRTC ICE w SimpleX Chat + +## Instalacja serwera STUN/TURN + +W tym poradniku będziemy używać najbardziej funkcjonalnej i przetestowanej w boju implementacji serwera STUN/TURN - [`coturn`](https://github.com/coturn/coturn) i [Ubuntu 20.04 LTS`](https://ubuntu.com/download/server) dystrybucji Linuksa. + +0. Uzyskaj certyfikaty `stun.$TWOJA_DOMENA` i `turn.$TWOJA_DOMENA`. + + Używamy [Let's Encrypt](https://letsencrypt.org/getting-started/). + +1. Zainstaluj pakiet `coturn` z głównego repozytorium. + +```sh +apt update && apt install coturn` +``` + +2. Odkomentuj `TURNSERVER_ENABLED=1` z `/etc/default/coturn`: + +```sh +sed -i '/TURN/s/^#//g' /etc/default/coturn +``` + +3. Skonfiguruj `coturn` w `/etc/turnserver.conf`: + + Zobacz również komentarze dotyczące poszczególnych opcji. + +```sh +# Nasłuchuj również na porcie 443 dla tls +alt-tls-listening-port=443 +# Używaj odcisków palców w komunikatach TURN +fingerprint +# Użyj mechanizmu poświadczeń długoterminowych +lt-cred-mech +# Twoje poświadczenia +user=$YOUR_LOGIN:$YOUR_PASSWORD +# Domena Twojego serwera +server-name=$YOUR_DOMAIN +# Domyślny obszar, który ma być używany dla użytkowników, gdy nie znaleziono wyraźnej relacji pochodzenie/obszar +realm=$YOUR_DOMAIN +# Ścieżka do Twoich certyfikatów. Upewnij się, że są one czytelne dla użytkownika/grupy procesu cotun. +cert=/var/lib/turn/cert.pem +pkey=/var/lib/turn/key.pem +# Użyj predefiniowanego klucza DH TLS o długości 2066 bitów +dh2066 +# Logowanie do journalctl +syslog +# Użytkownik/grupa, która będzie uruchamiać usługę coturn +proc-user=turnserver +proc-group=turnserver +# Wyłącz słabe szyfrowanie +no-tlsv1 +no-tlsv1_1 +no-tlsv1_2 +``` + +4. Uruchom i włącz serwis `coturn`: + +```sh +systemctl enable coturn && systemctl start coturn +``` + +5. Opcjonalnie, jeśli używasz firewalla `ufw`, otwórz odpowiednie porty: + +- **3478** – "czysty" TURN/STUN; +- **5349** – TURN/STUN over TLS; +- **443** – TURN/STUN over TLS, który może omijać firewalle; +- **49152:65535** – zakres portów, który Coturn będzie domyślnie wykorzystywał dla przekaźnika TURN. + +```sh +# Dla Ubuntu +sudo ufw allow 3478 && \ +sudo ufw allow 443 && \ +sudo ufw allow 5349 && \ +sudo ufw allow 49152:65535/tcp && \ +sudo ufw allow 49152:65535/udp + +# Dla Fedory +sudo firewall-cmd --permanent --add-port=443/tcp && \ +sudo firewall-cmd --permanent --add-port=443/udp && \ +sudo firewall-cmd --permanent --add-port=5349/tcp && \ +sudo firewall-cmd --permanent --add-port=5349/udp && \ +sudo firewall-cmd --permanent --add-port=49152:65535/tcp && \ +sudo firewall-cmd --permanent --add-port=49152:65535/udp && \ +sudo firewall-cmd --reload +``` + +## Konfiguracja aplikacji mobilnych + +Aby skonfigurować aplikację mobilną do korzystania z serwera: + +1. Otwórz `Ustawienia / Sieć i serwery / Serwery WebRTC ICE` i przełącz przełącznik `Konfiguruj serwery ICE`. + +2. Wprowadź wszystkie adresy serwerów w polu, po jednym na linię, na przykład jeśli serwery znajdują się na porcie 5349: + +``` +stun:stun.example.com:5349 +turn:username:password@turn.example.com:5349 +``` + +To tyle - teraz możesz wykonywać połączenia audio i wideo za pośrednictwem własnego serwera, bez udostępniania jakichkolwiek danych naszym serwerom (poza wymianą kluczy z kontaktem w szyfrowanych wiadomościach E2E). + +## Rozwiązywanie problemów + +- **Określ czy Twój serwer jest dostępny**: + + Uruchom to polecenie w terminalu: + + ```sh + ping <twoje_ip_lub_domena> + ``` + + Jeśli pakiety są transmitowane, serwer działa! + +- **Określ czy porty są otwarte**: + + Uruchom to polecenie w terminalu: + + ```sh + nc -zvw10 <twoje_ip_lub_domena> 443 5349 + ``` + + Powinno się pojawić: + + ``` + Connection to <twoje_ip_lub_domena> 443 port [tcp/https] succeeded! + Connection to <twoje_ip_lub_domena> 5349 port [tcp/*] succeeded! + ``` + +- **Test połączenia STUN/TURN**: + + 1. Wejdź na [IceTest](https://icetest.info/). + + 2. W sekcji **Build up ICE Server List** dodaj: + + <img src="./stun_1.png"> + + - `STUN: stun:<twoje_ip_lub_domena>:<port>` kliknij `Add STUN` + - `TURN: turn:<twoje_ip_lub_domena>:<port>`, `Username: <twój_login>`, `Credential: <twoje_hasło>` kliknij `Add TURN` + + Gdzie `<port>` to 443 lub 5349. + + 3. Powinieneś zobaczyć swoje serwery w sekcji **ICE server list**. Jeśli wszystko jest skonfigurowane poprawnie, naciśnij `Start test`: + + <img src="./stun_2.png"> + + 4. W sekcji **Results** powinieneś zobaczyć coś takiego: + + <img src="./stun_3.png"> + + Jeśli wyniki pokazują `srflx` i `relay`, wszystko jest skonfigurowane poprawnie! + diff --git a/docs/protocol/diagrams/group.mmd b/docs/protocol/diagrams/group.mmd index 18d392caa5..6a9bd0c786 100644 --- a/docs/protocol/diagrams/group.mmd +++ b/docs/protocol/diagrams/group.mmd @@ -4,9 +4,16 @@ sequenceDiagram participant B as Bob participant C as Existing<br>contact - note over A, B: 1. send and accept group invitation - A ->> B: x.grp.inv<br>invite Bob to group<br>(via contact connection) - B ->> A: x.grp.acpt<br>accept invitation<br>(via member connection)<br>establish group member connection + alt invite contact + note over A, B: 1a. send and accept group invitation + A ->> B: x.grp.inv<br>invite Bob to group<br>(via contact connection) + B ->> A: x.grp.acpt<br>accept invitation<br>(via member connection)<br>establish group member connection + else join via group link + note over A, B: 1b. join via group link and accept request + B ->> A: join via group link<br>SimpleX contact address + A ->> B: x.grp.link.inv in SMP confirmation<br>accept joining member request,<br>sending group profile, etc.<br>establish group member connection + A ->> B: x.grp.link.mem<br>send inviting member profile + end note over M, B: 2. introduce new member Bob to all existing members A ->> M: x.grp.mem.new<br>"announce" Bob<br>to existing members<br>(via member connections) @@ -20,14 +27,25 @@ sequenceDiagram end A ->> M: x.grp.mem.fwd<br>forward "invitations" and<br>Bob's chat protocol version<br>to all members<br>(via member connections) + note over M, B: group message forwarding<br>(while connections between members are being established) + M -->> B: messages between members and Bob are forwarded by Alice + B -->> M: + note over M, B: 3. establish direct and group member connections M ->> B: establish group member connection opt chat protocol compatible version < 2 M ->> B: establish direct connection - note over M, C: 4. deduplicate new contact + note over M, C: 3*. deduplicate new contact B ->> M: x.info.probe<br>"probe" is sent to all new members B ->> C: x.info.probe.check<br>"probe" hash,<br>in case contact and<br>member profiles match C ->> B: x.info.probe.ok<br> original "probe",<br> in case contact and member<br>are the same user note over B: merge existing and new contacts if received and sent probe hashes match end + + note over M, B: 4. notify inviting member that connection is established + M ->> A: x.grp.mem.con + B ->> A: x.grp.mem.con + note over A: stops forwarding messages + M -->> B: messages are sent via group connection without forwarding + B -->> M: diff --git a/docs/protocol/diagrams/group.svg b/docs/protocol/diagrams/group.svg index 8c1b65dee2..f3c9aa8a26 100644 --- a/docs/protocol/diagrams/group.svg +++ b/docs/protocol/diagrams/group.svg @@ -1 +1,3 @@ -<svg aria-roledescription="sequence" role="graphics-document document" viewBox="-50 -10 1210 1726" style="max-width: 100%;" xmlns="http://www.w3.org/2000/svg" width="100%" id="graph-div" height="100%" xmlns:xlink="http://www.w3.org/1999/xlink"><style>@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css");'</style><g><rect class="actor" ry="3" rx="3" height="65" width="150" stroke="#666" fill="#eaeaea" y="1640" x="960"></rect><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="1672.5" x="1035"><tspan dy="-8" x="1035">Existing</tspan></text><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="1672.5" x="1035"><tspan dy="8" x="1035">contact</tspan></text></g><g><rect class="actor" ry="3" rx="3" height="65" width="150" stroke="#666" fill="#eaeaea" y="1640" x="620"></rect><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="1672.5" x="695"><tspan dy="0" x="695">Bob</tspan></text></g><g><rect class="actor" ry="3" rx="3" height="65" width="150" stroke="#666" fill="#eaeaea" y="1640" x="278"></rect><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="1672.5" x="353"><tspan dy="0" x="353">Alice</tspan></text></g><g><rect class="actor" ry="3" rx="3" height="65" width="150" stroke="#666" fill="#eaeaea" y="1640" x="0"></rect><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="1672.5" x="75"><tspan dy="-8" x="75">N existing</tspan></text><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="1672.5" x="75"><tspan dy="8" x="75">members</tspan></text></g><g><line stroke="#999" stroke-width="0.5px" class="200" y2="1640" x2="1035" y1="5" x1="1035" id="actor35"></line><g id="root-35"><rect class="actor" ry="3" rx="3" height="65" width="150" stroke="#666" fill="#eaeaea" y="0" x="960"></rect><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="32.5" x="1035"><tspan dy="-8" x="1035">Existing</tspan></text><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="32.5" x="1035"><tspan dy="8" x="1035">contact</tspan></text></g></g><g><line stroke="#999" stroke-width="0.5px" class="200" y2="1640" x2="695" y1="5" x1="695" id="actor34"></line><g id="root-34"><rect class="actor" ry="3" rx="3" height="65" width="150" stroke="#666" fill="#eaeaea" y="0" x="620"></rect><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="32.5" x="695"><tspan dy="0" x="695">Bob</tspan></text></g></g><g><line stroke="#999" stroke-width="0.5px" class="200" y2="1640" x2="353" y1="5" x1="353" id="actor33"></line><g id="root-33"><rect class="actor" ry="3" rx="3" height="65" width="150" stroke="#666" fill="#eaeaea" y="0" x="278"></rect><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="32.5" x="353"><tspan dy="0" x="353">Alice</tspan></text></g></g><g><line stroke="#999" stroke-width="0.5px" class="200" y2="1640" x2="75" y1="5" x1="75" id="actor32"></line><g id="root-32"><rect class="actor" ry="3" rx="3" height="65" width="150" stroke="#666" fill="#eaeaea" y="0" x="0"></rect><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="32.5" x="75"><tspan dy="-8" x="75">N existing</tspan></text><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="32.5" x="75"><tspan dy="8" x="75">members</tspan></text></g></g><style>#graph-div{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#graph-div .error-icon{fill:#552222;}#graph-div .error-text{fill:#552222;stroke:#552222;}#graph-div .edge-thickness-normal{stroke-width:2px;}#graph-div .edge-thickness-thick{stroke-width:3.5px;}#graph-div .edge-pattern-solid{stroke-dasharray:0;}#graph-div .edge-pattern-dashed{stroke-dasharray:3;}#graph-div .edge-pattern-dotted{stroke-dasharray:2;}#graph-div .marker{fill:#333333;stroke:#333333;}#graph-div .marker.cross{stroke:#333333;}#graph-div svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#graph-div .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#graph-div text.actor>tspan{fill:black;stroke:none;}#graph-div .actor-line{stroke:grey;}#graph-div .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#graph-div .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#graph-div #arrowhead path{fill:#333;stroke:#333;}#graph-div .sequenceNumber{fill:white;}#graph-div #sequencenumber{fill:#333;}#graph-div #crosshead path{fill:#333;stroke:#333;}#graph-div .messageText{fill:#333;stroke:none;}#graph-div .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#graph-div .labelText,#graph-div .labelText>tspan{fill:black;stroke:none;}#graph-div .loopText,#graph-div .loopText>tspan{fill:black;stroke:none;}#graph-div .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#graph-div .note{stroke:#aaaa33;fill:#fff5ad;}#graph-div .noteText,#graph-div .noteText>tspan{fill:black;stroke:none;}#graph-div .activation0{fill:#f4f4f4;stroke:#666;}#graph-div .activation1{fill:#f4f4f4;stroke:#666;}#graph-div .activation2{fill:#f4f4f4;stroke:#666;}#graph-div .actorPopupMenu{position:absolute;}#graph-div .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#graph-div .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#graph-div .actor-man circle,#graph-div line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#graph-div :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g></g><defs><symbol height="24" width="24" id="computer"><path d="M2 2v13h20v-13h-20zm18 11h-16v-9h16v9zm-10.228 6l.466-1h3.524l.467 1h-4.457zm14.228 3h-24l2-6h2.104l-1.33 4h18.45l-1.297-4h2.073l2 6zm-5-10h-14v-7h14v7z" transform="scale(.5)"></path></symbol></defs><defs><symbol clip-rule="evenodd" fill-rule="evenodd" id="database"><path d="M12.258.001l.256.004.255.005.253.008.251.01.249.012.247.015.246.016.242.019.241.02.239.023.236.024.233.027.231.028.229.031.225.032.223.034.22.036.217.038.214.04.211.041.208.043.205.045.201.046.198.048.194.05.191.051.187.053.183.054.18.056.175.057.172.059.168.06.163.061.16.063.155.064.15.066.074.033.073.033.071.034.07.034.069.035.068.035.067.035.066.035.064.036.064.036.062.036.06.036.06.037.058.037.058.037.055.038.055.038.053.038.052.038.051.039.05.039.048.039.047.039.045.04.044.04.043.04.041.04.04.041.039.041.037.041.036.041.034.041.033.042.032.042.03.042.029.042.027.042.026.043.024.043.023.043.021.043.02.043.018.044.017.043.015.044.013.044.012.044.011.045.009.044.007.045.006.045.004.045.002.045.001.045v17l-.001.045-.002.045-.004.045-.006.045-.007.045-.009.044-.011.045-.012.044-.013.044-.015.044-.017.043-.018.044-.02.043-.021.043-.023.043-.024.043-.026.043-.027.042-.029.042-.03.042-.032.042-.033.042-.034.041-.036.041-.037.041-.039.041-.04.041-.041.04-.043.04-.044.04-.045.04-.047.039-.048.039-.05.039-.051.039-.052.038-.053.038-.055.038-.055.038-.058.037-.058.037-.06.037-.06.036-.062.036-.064.036-.064.036-.066.035-.067.035-.068.035-.069.035-.07.034-.071.034-.073.033-.074.033-.15.066-.155.064-.16.063-.163.061-.168.06-.172.059-.175.057-.18.056-.183.054-.187.053-.191.051-.194.05-.198.048-.201.046-.205.045-.208.043-.211.041-.214.04-.217.038-.22.036-.223.034-.225.032-.229.031-.231.028-.233.027-.236.024-.239.023-.241.02-.242.019-.246.016-.247.015-.249.012-.251.01-.253.008-.255.005-.256.004-.258.001-.258-.001-.256-.004-.255-.005-.253-.008-.251-.01-.249-.012-.247-.015-.245-.016-.243-.019-.241-.02-.238-.023-.236-.024-.234-.027-.231-.028-.228-.031-.226-.032-.223-.034-.22-.036-.217-.038-.214-.04-.211-.041-.208-.043-.204-.045-.201-.046-.198-.048-.195-.05-.19-.051-.187-.053-.184-.054-.179-.056-.176-.057-.172-.059-.167-.06-.164-.061-.159-.063-.155-.064-.151-.066-.074-.033-.072-.033-.072-.034-.07-.034-.069-.035-.068-.035-.067-.035-.066-.035-.064-.036-.063-.036-.062-.036-.061-.036-.06-.037-.058-.037-.057-.037-.056-.038-.055-.038-.053-.038-.052-.038-.051-.039-.049-.039-.049-.039-.046-.039-.046-.04-.044-.04-.043-.04-.041-.04-.04-.041-.039-.041-.037-.041-.036-.041-.034-.041-.033-.042-.032-.042-.03-.042-.029-.042-.027-.042-.026-.043-.024-.043-.023-.043-.021-.043-.02-.043-.018-.044-.017-.043-.015-.044-.013-.044-.012-.044-.011-.045-.009-.044-.007-.045-.006-.045-.004-.045-.002-.045-.001-.045v-17l.001-.045.002-.045.004-.045.006-.045.007-.045.009-.044.011-.045.012-.044.013-.044.015-.044.017-.043.018-.044.02-.043.021-.043.023-.043.024-.043.026-.043.027-.042.029-.042.03-.042.032-.042.033-.042.034-.041.036-.041.037-.041.039-.041.04-.041.041-.04.043-.04.044-.04.046-.04.046-.039.049-.039.049-.039.051-.039.052-.038.053-.038.055-.038.056-.038.057-.037.058-.037.06-.037.061-.036.062-.036.063-.036.064-.036.066-.035.067-.035.068-.035.069-.035.07-.034.072-.034.072-.033.074-.033.151-.066.155-.064.159-.063.164-.061.167-.06.172-.059.176-.057.179-.056.184-.054.187-.053.19-.051.195-.05.198-.048.201-.046.204-.045.208-.043.211-.041.214-.04.217-.038.22-.036.223-.034.226-.032.228-.031.231-.028.234-.027.236-.024.238-.023.241-.02.243-.019.245-.016.247-.015.249-.012.251-.01.253-.008.255-.005.256-.004.258-.001.258.001zm-9.258 20.499v.01l.001.021.003.021.004.022.005.021.006.022.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.023.018.024.019.024.021.024.022.025.023.024.024.025.052.049.056.05.061.051.066.051.07.051.075.051.079.052.084.052.088.052.092.052.097.052.102.051.105.052.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.048.144.049.147.047.152.047.155.047.16.045.163.045.167.043.171.043.176.041.178.041.183.039.187.039.19.037.194.035.197.035.202.033.204.031.209.03.212.029.216.027.219.025.222.024.226.021.23.02.233.018.236.016.24.015.243.012.246.01.249.008.253.005.256.004.259.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.021.224-.024.22-.026.216-.027.212-.028.21-.031.205-.031.202-.034.198-.034.194-.036.191-.037.187-.039.183-.04.179-.04.175-.042.172-.043.168-.044.163-.045.16-.046.155-.046.152-.047.148-.048.143-.049.139-.049.136-.05.131-.05.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.053.083-.051.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.05.023-.024.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.023.01-.022.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.127l-.077.055-.08.053-.083.054-.085.053-.087.052-.09.052-.093.051-.095.05-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.045-.118.044-.12.043-.122.042-.124.042-.126.041-.128.04-.13.04-.132.038-.134.038-.135.037-.138.037-.139.035-.142.035-.143.034-.144.033-.147.032-.148.031-.15.03-.151.03-.153.029-.154.027-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.01-.179.008-.179.008-.181.006-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.006-.179-.008-.179-.008-.178-.01-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.027-.153-.029-.151-.03-.15-.03-.148-.031-.146-.032-.145-.033-.143-.034-.141-.035-.14-.035-.137-.037-.136-.037-.134-.038-.132-.038-.13-.04-.128-.04-.126-.041-.124-.042-.122-.042-.12-.044-.117-.043-.116-.045-.113-.045-.112-.046-.109-.047-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.05-.093-.052-.09-.051-.087-.052-.085-.053-.083-.054-.08-.054-.077-.054v4.127zm0-5.654v.011l.001.021.003.021.004.021.005.022.006.022.007.022.009.022.01.022.011.023.012.023.013.023.015.024.016.023.017.024.018.024.019.024.021.024.022.024.023.025.024.024.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.052.11.051.114.051.119.052.123.05.127.051.131.05.135.049.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.044.171.042.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.022.23.02.233.018.236.016.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.012.241-.015.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.048.139-.05.136-.049.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.051.051-.049.023-.025.023-.024.021-.025.02-.024.019-.024.018-.024.017-.024.015-.023.014-.023.013-.024.012-.022.01-.023.01-.023.008-.022.006-.022.006-.022.004-.021.004-.022.001-.021.001-.021v-4.139l-.077.054-.08.054-.083.054-.085.052-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.044-.118.044-.12.044-.122.042-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.035-.143.033-.144.033-.147.033-.148.031-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.009-.179.009-.179.007-.181.007-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.007-.179-.007-.179-.009-.178-.009-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.031-.146-.033-.145-.033-.143-.033-.141-.035-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.04-.126-.041-.124-.042-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.051-.093-.051-.09-.051-.087-.053-.085-.052-.083-.054-.08-.054-.077-.054v4.139zm0-5.666v.011l.001.02.003.022.004.021.005.022.006.021.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.024.018.023.019.024.021.025.022.024.023.024.024.025.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.051.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.043.171.043.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.021.23.02.233.018.236.017.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.013.241-.014.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.049.139-.049.136-.049.131-.051.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.049.023-.025.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.022.01-.023.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.153l-.077.054-.08.054-.083.053-.085.053-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.048-.105.048-.106.048-.109.046-.111.046-.114.046-.115.044-.118.044-.12.043-.122.043-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.034-.143.034-.144.033-.147.032-.148.032-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.024-.161.024-.162.023-.163.023-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.01-.178.01-.179.009-.179.007-.181.006-.182.006-.182.004-.184.003-.184.001-.185.001-.185-.001-.184-.001-.184-.003-.182-.004-.182-.006-.181-.006-.179-.007-.179-.009-.178-.01-.176-.01-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.023-.162-.023-.161-.024-.159-.024-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.032-.146-.032-.145-.033-.143-.034-.141-.034-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.041-.126-.041-.124-.041-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.048-.105-.048-.102-.048-.1-.05-.097-.049-.095-.051-.093-.051-.09-.052-.087-.052-.085-.053-.083-.053-.08-.054-.077-.054v4.153zm8.74-8.179l-.257.004-.254.005-.25.008-.247.011-.244.012-.241.014-.237.016-.233.018-.231.021-.226.022-.224.023-.22.026-.216.027-.212.028-.21.031-.205.032-.202.033-.198.034-.194.036-.191.038-.187.038-.183.04-.179.041-.175.042-.172.043-.168.043-.163.045-.16.046-.155.046-.152.048-.148.048-.143.048-.139.049-.136.05-.131.05-.126.051-.123.051-.118.051-.114.052-.11.052-.106.052-.101.052-.096.052-.092.052-.088.052-.083.052-.079.052-.074.051-.07.052-.065.051-.06.05-.056.05-.051.05-.023.025-.023.024-.021.024-.02.025-.019.024-.018.024-.017.023-.015.024-.014.023-.013.023-.012.023-.01.023-.01.022-.008.022-.006.023-.006.021-.004.022-.004.021-.001.021-.001.021.001.021.001.021.004.021.004.022.006.021.006.023.008.022.01.022.01.023.012.023.013.023.014.023.015.024.017.023.018.024.019.024.02.025.021.024.023.024.023.025.051.05.056.05.06.05.065.051.07.052.074.051.079.052.083.052.088.052.092.052.096.052.101.052.106.052.11.052.114.052.118.051.123.051.126.051.131.05.136.05.139.049.143.048.148.048.152.048.155.046.16.046.163.045.168.043.172.043.175.042.179.041.183.04.187.038.191.038.194.036.198.034.202.033.205.032.21.031.212.028.216.027.22.026.224.023.226.022.231.021.233.018.237.016.241.014.244.012.247.011.25.008.254.005.257.004.26.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.022.224-.023.22-.026.216-.027.212-.028.21-.031.205-.032.202-.033.198-.034.194-.036.191-.038.187-.038.183-.04.179-.041.175-.042.172-.043.168-.043.163-.045.16-.046.155-.046.152-.048.148-.048.143-.048.139-.049.136-.05.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.05.051-.05.023-.025.023-.024.021-.024.02-.025.019-.024.018-.024.017-.023.015-.024.014-.023.013-.023.012-.023.01-.023.01-.022.008-.022.006-.023.006-.021.004-.022.004-.021.001-.021.001-.021-.001-.021-.001-.021-.004-.021-.004-.022-.006-.021-.006-.023-.008-.022-.01-.022-.01-.023-.012-.023-.013-.023-.014-.023-.015-.024-.017-.023-.018-.024-.019-.024-.02-.025-.021-.024-.023-.024-.023-.025-.051-.05-.056-.05-.06-.05-.065-.051-.07-.052-.074-.051-.079-.052-.083-.052-.088-.052-.092-.052-.096-.052-.101-.052-.106-.052-.11-.052-.114-.052-.118-.051-.123-.051-.126-.051-.131-.05-.136-.05-.139-.049-.143-.048-.148-.048-.152-.048-.155-.046-.16-.046-.163-.045-.168-.043-.172-.043-.175-.042-.179-.041-.183-.04-.187-.038-.191-.038-.194-.036-.198-.034-.202-.033-.205-.032-.21-.031-.212-.028-.216-.027-.22-.026-.224-.023-.226-.022-.231-.021-.233-.018-.237-.016-.241-.014-.244-.012-.247-.011-.25-.008-.254-.005-.257-.004-.26-.001-.26.001z" transform="scale(.5)"></path></symbol></defs><defs><symbol height="24" width="24" id="clock"><path d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm5.848 12.459c.202.038.202.333.001.372-1.907.361-6.045 1.111-6.547 1.111-.719 0-1.301-.582-1.301-1.301 0-.512.77-5.447 1.125-7.445.034-.192.312-.181.343.014l.985 6.238 5.394 1.011z" transform="scale(.5)"></path></symbol></defs><defs><marker orient="auto" markerHeight="12" markerWidth="12" markerUnits="userSpaceOnUse" refY="5" refX="9" id="arrowhead"><path d="M 0 0 L 10 5 L 0 10 z"></path></marker></defs><defs><marker refY="5" refX="4" orient="auto" markerHeight="8" markerWidth="15" id="crosshead"><path d="M 1,2 L 6,7 M 6,2 L 1,7" stroke-width="1pt" style="stroke-dasharray: 0px, 0px;" stroke="#000000" fill="none"></path></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="18" id="filled-head"><path d="M 18,7 L9,13 L14,7 L9,1 Z"></path></marker></defs><defs><marker orient="auto" markerHeight="40" markerWidth="60" refY="15" refX="15" id="sequencenumber"><circle r="6" cy="15" cx="15"></circle></marker></defs><g><rect class="note" ry="0" rx="0" height="40" width="392" stroke="#666" fill="#EDF2AE" y="75" x="328"></rect><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="80" x="524"><tspan x="524">1. send and accept group invitation</tspan></text></g><g><rect class="note" ry="0" rx="0" height="40" width="670" stroke="#666" fill="#EDF2AE" y="316" x="50"></rect><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="321" x="385"><tspan x="385">2. introduce new member Bob to all existing members</tspan></text></g><g><rect class="note" ry="0" rx="0" height="40" width="291" stroke="#666" fill="#EDF2AE" y="621" x="549.5"></rect><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="626" x="695"><tspan x="695">prepare group member connections</tspan></text></g><g><rect class="note" ry="0" rx="0" height="40" width="224" stroke="#666" fill="#EDF2AE" y="753" x="583"></rect><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="758" x="695"><tspan x="695">prepare direct connections</tspan></text></g><g><line class="loopLine" y2="671" x2="817" y1="671" x1="573"></line><line class="loopLine" y2="803" x2="817" y1="671" x1="817"></line><line class="loopLine" y2="803" x2="817" y1="803" x1="573"></line><line class="loopLine" y2="803" x2="573" y1="671" x1="573"></line><polygon class="labelBox" points="573,671 623,671 623,684 614.6,691 573,691"></polygon><text class="labelText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="684" x="598">opt</text><text class="loopText" style="font-size: 16px; font-weight: 400;" text-anchor="middle" y="689" x="720"><tspan x="720">[chat protocol</tspan></text><text class="loopText" style="font-size: 16px; font-weight: 400;" text-anchor="middle" y="708" x="720"><tspan x="720">compatible version</tspan></text><text class="loopText" style="font-size: 16px; font-weight: 400;" text-anchor="middle" y="728" x="720"><tspan x="720">< 2]</tspan></text></g><g><line class="loopLine" y2="471" x2="850.5" y1="471" x1="343"></line><line class="loopLine" y2="918" x2="850.5" y1="471" x1="850.5"></line><line class="loopLine" y2="918" x2="850.5" y1="918" x1="343"></line><line class="loopLine" y2="918" x2="343" y1="471" x1="343"></line><polygon class="labelBox" points="343,471 393,471 393,484 384.6,491 343,491"></polygon><text class="labelText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="484" x="368">loop</text><text class="loopText" style="font-size: 16px; font-weight: 400;" text-anchor="middle" y="489" x="621.75"><tspan x="621.75">[batched]</tspan></text></g><g><rect class="note" ry="0" rx="0" height="40" width="670" stroke="#666" fill="#EDF2AE" y="1052" x="50"></rect><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1057" x="385"><tspan x="385">3. establish direct and group member connections</tspan></text></g><g><rect class="note" ry="0" rx="0" height="40" width="1010" stroke="#666" fill="#EDF2AE" y="1243" x="50"></rect><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1248" x="555"><tspan x="555">4. deduplicate new contact</tspan></text></g><g><rect class="note" ry="0" rx="0" height="40" width="580" stroke="#666" fill="#EDF2AE" y="1570" x="405"></rect><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1575" x="695"><tspan x="695">merge existing and new contacts if received and sent probe hashes match</tspan></text></g><g><line class="loopLine" y2="1150" x2="1070" y1="1150" x1="40"></line><line class="loopLine" y2="1620" x2="1070" y1="1150" x1="1070"></line><line class="loopLine" y2="1620" x2="1070" y1="1620" x1="40"></line><line class="loopLine" y2="1620" x2="40" y1="1150" x1="40"></line><polygon class="labelBox" points="40,1150 90,1150 90,1163 81.6,1170 40,1170"></polygon><text class="labelText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1163" x="65">opt</text><text class="loopText" style="font-size: 16px; font-weight: 400;" text-anchor="middle" y="1168" x="580"><tspan x="580">[chat protocol compatible version < 2]</tspan></text></g><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="130" x="524">x.grp.inv</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="150" x="524">invite Bob to group</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="169" x="524">(via contact connection)</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="201" x2="695" y1="201" x1="353"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="216" x="524">x.grp.acpt</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="236" x="524">accept invitation</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="255" x="524">(via member connection)</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="275" x="524">establish group member connection</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="306" x2="353" y1="306" x1="695"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="371" x="214">x.grp.mem.new</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="391" x="214">"announce" Bob</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="410" x="214">to existing members</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="430" x="214">(via member connections)</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="461" x2="75" y1="461" x1="353"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="521" x="524">x.grp.mem.intro * N</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="541" x="524">"introduce" members and</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="560" x="524">their chat protocol versions</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="580" x="524">(via member connection)</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="611" x2="695" y1="611" x1="353"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="818" x="524">x.grp.mem.inv * N</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="838" x="524">"invitations" to connect</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="857" x="524">for all members</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="877" x="524">(via member connection)</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="908" x2="353" y1="908" x1="695"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="933" x="214">x.grp.mem.fwd</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="953" x="214">forward "invitations" and</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="972" x="214">Bob's chat protocol version</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="992" x="214">to all members</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1011" x="214">(via member connections)</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="1042" x2="75" y1="1042" x1="353"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1107" x="385">establish group member connection</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="1140" x2="695" y1="1140" x1="75"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1200" x="385">establish direct connection</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="1233" x2="695" y1="1233" x1="75"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1298" x="385">x.info.probe</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1318" x="385">"probe" is sent to all new members</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="1350" x2="75" y1="1350" x1="695"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1365" x="865">x.info.probe.check</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1385" x="865">"probe" hash,</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1404" x="865">in case contact and</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1424" x="865">member profiles match</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="1455" x2="1035" y1="1455" x1="695"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1470" x="865">x.info.probe.ok</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1490" x="865"> original "probe",</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1509" x="865"> in case contact and member</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1529" x="865">are the same user</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="1560" x2="695" y1="1560" x1="1035"></line></svg> \ No newline at end of file +<?xml version="1.0" encoding="UTF-8"?> +<?xml-stylesheet href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" type="text/css"?> +<svg aria-roledescription="sequence" role="graphics-document document" viewBox="-50 -10 1197 2496" style="max-width: 100%;" xmlns="http://www.w3.org/2000/svg" width="100%" id="graph-div" height="100%" xmlns:xlink="http://www.w3.org/1999/xlink"><g><rect class="actor actor-bottom" ry="3" rx="3" name="C" height="65" width="150" stroke="#666" fill="#eaeaea" y="2410" x="947"></rect><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="2442.5" x="1022"><tspan dy="-8" x="1022">Existing</tspan></text><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="2442.5" x="1022"><tspan dy="8" x="1022">contact</tspan></text></g><g><rect class="actor actor-bottom" ry="3" rx="3" name="B" height="65" width="150" stroke="#666" fill="#eaeaea" y="2410" x="616"></rect><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="2442.5" x="691"><tspan dy="0" x="691">Bob</tspan></text></g><g><rect class="actor actor-bottom" ry="3" rx="3" name="A" height="65" width="150" stroke="#666" fill="#eaeaea" y="2410" x="282"></rect><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="2442.5" x="357"><tspan dy="0" x="357">Alice</tspan></text></g><g><rect class="actor actor-bottom" ry="3" rx="3" name="M" height="65" width="150" stroke="#666" fill="#eaeaea" y="2410" x="0"></rect><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="2442.5" x="75"><tspan dy="-8" x="75">N existing</tspan></text><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="2442.5" x="75"><tspan dy="8" x="75">members</tspan></text></g><g><line stroke="#999" stroke-width="0.5px" class="200" y2="2410" x2="1022" y1="5" x1="1022" id="actor171"></line><g id="root-171"><rect class="actor actor-top" ry="3" rx="3" name="C" height="65" width="150" stroke="#666" fill="#eaeaea" y="0" x="947"></rect><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="32.5" x="1022"><tspan dy="-8" x="1022">Existing</tspan></text><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="32.5" x="1022"><tspan dy="8" x="1022">contact</tspan></text></g></g><g><line stroke="#999" stroke-width="0.5px" class="200" y2="2410" x2="691" y1="5" x1="691" id="actor170"></line><g id="root-170"><rect class="actor actor-top" ry="3" rx="3" name="B" height="65" width="150" stroke="#666" fill="#eaeaea" y="0" x="616"></rect><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="32.5" x="691"><tspan dy="0" x="691">Bob</tspan></text></g></g><g><line stroke="#999" stroke-width="0.5px" class="200" y2="2410" x2="357" y1="5" x1="357" id="actor169"></line><g id="root-169"><rect class="actor actor-top" ry="3" rx="3" name="A" height="65" width="150" stroke="#666" fill="#eaeaea" y="0" x="282"></rect><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="32.5" x="357"><tspan dy="0" x="357">Alice</tspan></text></g></g><g><line stroke="#999" stroke-width="0.5px" class="200" y2="2410" x2="75" y1="5" x1="75" id="actor168"></line><g id="root-168"><rect class="actor actor-top" ry="3" rx="3" name="M" height="65" width="150" stroke="#666" fill="#eaeaea" y="0" x="0"></rect><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="32.5" x="75"><tspan dy="-8" x="75">N existing</tspan></text><text class="actor" alignment-baseline="central" dominant-baseline="central" style="text-anchor: middle; font-size: 16px; font-weight: 400;" y="32.5" x="75"><tspan dy="8" x="75">members</tspan></text></g></g><style>#graph-div{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#graph-div .error-icon{fill:#552222;}#graph-div .error-text{fill:#552222;stroke:#552222;}#graph-div .edge-thickness-normal{stroke-width:2px;}#graph-div .edge-thickness-thick{stroke-width:3.5px;}#graph-div .edge-pattern-solid{stroke-dasharray:0;}#graph-div .edge-pattern-dashed{stroke-dasharray:3;}#graph-div .edge-pattern-dotted{stroke-dasharray:2;}#graph-div .marker{fill:#333333;stroke:#333333;}#graph-div .marker.cross{stroke:#333333;}#graph-div svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#graph-div .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#graph-div text.actor>tspan{fill:black;stroke:none;}#graph-div .actor-line{stroke:grey;}#graph-div .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#graph-div .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#graph-div #arrowhead path{fill:#333;stroke:#333;}#graph-div .sequenceNumber{fill:white;}#graph-div #sequencenumber{fill:#333;}#graph-div #crosshead path{fill:#333;stroke:#333;}#graph-div .messageText{fill:#333;stroke:none;}#graph-div .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#graph-div .labelText,#graph-div .labelText>tspan{fill:black;stroke:none;}#graph-div .loopText,#graph-div .loopText>tspan{fill:black;stroke:none;}#graph-div .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#graph-div .note{stroke:#aaaa33;fill:#fff5ad;}#graph-div .noteText,#graph-div .noteText>tspan{fill:black;stroke:none;}#graph-div .activation0{fill:#f4f4f4;stroke:#666;}#graph-div .activation1{fill:#f4f4f4;stroke:#666;}#graph-div .activation2{fill:#f4f4f4;stroke:#666;}#graph-div .actorPopupMenu{position:absolute;}#graph-div .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#graph-div .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#graph-div .actor-man circle,#graph-div line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#graph-div :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g></g><defs><symbol height="24" width="24" id="computer"><path d="M2 2v13h20v-13h-20zm18 11h-16v-9h16v9zm-10.228 6l.466-1h3.524l.467 1h-4.457zm14.228 3h-24l2-6h2.104l-1.33 4h18.45l-1.297-4h2.073l2 6zm-5-10h-14v-7h14v7z" transform="scale(.5)"></path></symbol></defs><defs><symbol clip-rule="evenodd" fill-rule="evenodd" id="database"><path d="M12.258.001l.256.004.255.005.253.008.251.01.249.012.247.015.246.016.242.019.241.02.239.023.236.024.233.027.231.028.229.031.225.032.223.034.22.036.217.038.214.04.211.041.208.043.205.045.201.046.198.048.194.05.191.051.187.053.183.054.18.056.175.057.172.059.168.06.163.061.16.063.155.064.15.066.074.033.073.033.071.034.07.034.069.035.068.035.067.035.066.035.064.036.064.036.062.036.06.036.06.037.058.037.058.037.055.038.055.038.053.038.052.038.051.039.05.039.048.039.047.039.045.04.044.04.043.04.041.04.04.041.039.041.037.041.036.041.034.041.033.042.032.042.03.042.029.042.027.042.026.043.024.043.023.043.021.043.02.043.018.044.017.043.015.044.013.044.012.044.011.045.009.044.007.045.006.045.004.045.002.045.001.045v17l-.001.045-.002.045-.004.045-.006.045-.007.045-.009.044-.011.045-.012.044-.013.044-.015.044-.017.043-.018.044-.02.043-.021.043-.023.043-.024.043-.026.043-.027.042-.029.042-.03.042-.032.042-.033.042-.034.041-.036.041-.037.041-.039.041-.04.041-.041.04-.043.04-.044.04-.045.04-.047.039-.048.039-.05.039-.051.039-.052.038-.053.038-.055.038-.055.038-.058.037-.058.037-.06.037-.06.036-.062.036-.064.036-.064.036-.066.035-.067.035-.068.035-.069.035-.07.034-.071.034-.073.033-.074.033-.15.066-.155.064-.16.063-.163.061-.168.06-.172.059-.175.057-.18.056-.183.054-.187.053-.191.051-.194.05-.198.048-.201.046-.205.045-.208.043-.211.041-.214.04-.217.038-.22.036-.223.034-.225.032-.229.031-.231.028-.233.027-.236.024-.239.023-.241.02-.242.019-.246.016-.247.015-.249.012-.251.01-.253.008-.255.005-.256.004-.258.001-.258-.001-.256-.004-.255-.005-.253-.008-.251-.01-.249-.012-.247-.015-.245-.016-.243-.019-.241-.02-.238-.023-.236-.024-.234-.027-.231-.028-.228-.031-.226-.032-.223-.034-.22-.036-.217-.038-.214-.04-.211-.041-.208-.043-.204-.045-.201-.046-.198-.048-.195-.05-.19-.051-.187-.053-.184-.054-.179-.056-.176-.057-.172-.059-.167-.06-.164-.061-.159-.063-.155-.064-.151-.066-.074-.033-.072-.033-.072-.034-.07-.034-.069-.035-.068-.035-.067-.035-.066-.035-.064-.036-.063-.036-.062-.036-.061-.036-.06-.037-.058-.037-.057-.037-.056-.038-.055-.038-.053-.038-.052-.038-.051-.039-.049-.039-.049-.039-.046-.039-.046-.04-.044-.04-.043-.04-.041-.04-.04-.041-.039-.041-.037-.041-.036-.041-.034-.041-.033-.042-.032-.042-.03-.042-.029-.042-.027-.042-.026-.043-.024-.043-.023-.043-.021-.043-.02-.043-.018-.044-.017-.043-.015-.044-.013-.044-.012-.044-.011-.045-.009-.044-.007-.045-.006-.045-.004-.045-.002-.045-.001-.045v-17l.001-.045.002-.045.004-.045.006-.045.007-.045.009-.044.011-.045.012-.044.013-.044.015-.044.017-.043.018-.044.02-.043.021-.043.023-.043.024-.043.026-.043.027-.042.029-.042.03-.042.032-.042.033-.042.034-.041.036-.041.037-.041.039-.041.04-.041.041-.04.043-.04.044-.04.046-.04.046-.039.049-.039.049-.039.051-.039.052-.038.053-.038.055-.038.056-.038.057-.037.058-.037.06-.037.061-.036.062-.036.063-.036.064-.036.066-.035.067-.035.068-.035.069-.035.07-.034.072-.034.072-.033.074-.033.151-.066.155-.064.159-.063.164-.061.167-.06.172-.059.176-.057.179-.056.184-.054.187-.053.19-.051.195-.05.198-.048.201-.046.204-.045.208-.043.211-.041.214-.04.217-.038.22-.036.223-.034.226-.032.228-.031.231-.028.234-.027.236-.024.238-.023.241-.02.243-.019.245-.016.247-.015.249-.012.251-.01.253-.008.255-.005.256-.004.258-.001.258.001zm-9.258 20.499v.01l.001.021.003.021.004.022.005.021.006.022.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.023.018.024.019.024.021.024.022.025.023.024.024.025.052.049.056.05.061.051.066.051.07.051.075.051.079.052.084.052.088.052.092.052.097.052.102.051.105.052.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.048.144.049.147.047.152.047.155.047.16.045.163.045.167.043.171.043.176.041.178.041.183.039.187.039.19.037.194.035.197.035.202.033.204.031.209.03.212.029.216.027.219.025.222.024.226.021.23.02.233.018.236.016.24.015.243.012.246.01.249.008.253.005.256.004.259.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.021.224-.024.22-.026.216-.027.212-.028.21-.031.205-.031.202-.034.198-.034.194-.036.191-.037.187-.039.183-.04.179-.04.175-.042.172-.043.168-.044.163-.045.16-.046.155-.046.152-.047.148-.048.143-.049.139-.049.136-.05.131-.05.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.053.083-.051.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.05.023-.024.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.023.01-.022.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.127l-.077.055-.08.053-.083.054-.085.053-.087.052-.09.052-.093.051-.095.05-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.045-.118.044-.12.043-.122.042-.124.042-.126.041-.128.04-.13.04-.132.038-.134.038-.135.037-.138.037-.139.035-.142.035-.143.034-.144.033-.147.032-.148.031-.15.03-.151.03-.153.029-.154.027-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.01-.179.008-.179.008-.181.006-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.006-.179-.008-.179-.008-.178-.01-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.027-.153-.029-.151-.03-.15-.03-.148-.031-.146-.032-.145-.033-.143-.034-.141-.035-.14-.035-.137-.037-.136-.037-.134-.038-.132-.038-.13-.04-.128-.04-.126-.041-.124-.042-.122-.042-.12-.044-.117-.043-.116-.045-.113-.045-.112-.046-.109-.047-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.05-.093-.052-.09-.051-.087-.052-.085-.053-.083-.054-.08-.054-.077-.054v4.127zm0-5.654v.011l.001.021.003.021.004.021.005.022.006.022.007.022.009.022.01.022.011.023.012.023.013.023.015.024.016.023.017.024.018.024.019.024.021.024.022.024.023.025.024.024.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.052.11.051.114.051.119.052.123.05.127.051.131.05.135.049.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.044.171.042.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.022.23.02.233.018.236.016.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.012.241-.015.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.048.139-.05.136-.049.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.051.051-.049.023-.025.023-.024.021-.025.02-.024.019-.024.018-.024.017-.024.015-.023.014-.023.013-.024.012-.022.01-.023.01-.023.008-.022.006-.022.006-.022.004-.021.004-.022.001-.021.001-.021v-4.139l-.077.054-.08.054-.083.054-.085.052-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.044-.118.044-.12.044-.122.042-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.035-.143.033-.144.033-.147.033-.148.031-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.009-.179.009-.179.007-.181.007-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.007-.179-.007-.179-.009-.178-.009-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.031-.146-.033-.145-.033-.143-.033-.141-.035-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.04-.126-.041-.124-.042-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.051-.093-.051-.09-.051-.087-.053-.085-.052-.083-.054-.08-.054-.077-.054v4.139zm0-5.666v.011l.001.02.003.022.004.021.005.022.006.021.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.024.018.023.019.024.021.025.022.024.023.024.024.025.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.051.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.043.171.043.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.021.23.02.233.018.236.017.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.013.241-.014.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.049.139-.049.136-.049.131-.051.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.049.023-.025.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.022.01-.023.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.153l-.077.054-.08.054-.083.053-.085.053-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.048-.105.048-.106.048-.109.046-.111.046-.114.046-.115.044-.118.044-.12.043-.122.043-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.034-.143.034-.144.033-.147.032-.148.032-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.024-.161.024-.162.023-.163.023-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.01-.178.01-.179.009-.179.007-.181.006-.182.006-.182.004-.184.003-.184.001-.185.001-.185-.001-.184-.001-.184-.003-.182-.004-.182-.006-.181-.006-.179-.007-.179-.009-.178-.01-.176-.01-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.023-.162-.023-.161-.024-.159-.024-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.032-.146-.032-.145-.033-.143-.034-.141-.034-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.041-.126-.041-.124-.041-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.048-.105-.048-.102-.048-.1-.05-.097-.049-.095-.051-.093-.051-.09-.052-.087-.052-.085-.053-.083-.053-.08-.054-.077-.054v4.153zm8.74-8.179l-.257.004-.254.005-.25.008-.247.011-.244.012-.241.014-.237.016-.233.018-.231.021-.226.022-.224.023-.22.026-.216.027-.212.028-.21.031-.205.032-.202.033-.198.034-.194.036-.191.038-.187.038-.183.04-.179.041-.175.042-.172.043-.168.043-.163.045-.16.046-.155.046-.152.048-.148.048-.143.048-.139.049-.136.05-.131.05-.126.051-.123.051-.118.051-.114.052-.11.052-.106.052-.101.052-.096.052-.092.052-.088.052-.083.052-.079.052-.074.051-.07.052-.065.051-.06.05-.056.05-.051.05-.023.025-.023.024-.021.024-.02.025-.019.024-.018.024-.017.023-.015.024-.014.023-.013.023-.012.023-.01.023-.01.022-.008.022-.006.023-.006.021-.004.022-.004.021-.001.021-.001.021.001.021.001.021.004.021.004.022.006.021.006.023.008.022.01.022.01.023.012.023.013.023.014.023.015.024.017.023.018.024.019.024.02.025.021.024.023.024.023.025.051.05.056.05.06.05.065.051.07.052.074.051.079.052.083.052.088.052.092.052.096.052.101.052.106.052.11.052.114.052.118.051.123.051.126.051.131.05.136.05.139.049.143.048.148.048.152.048.155.046.16.046.163.045.168.043.172.043.175.042.179.041.183.04.187.038.191.038.194.036.198.034.202.033.205.032.21.031.212.028.216.027.22.026.224.023.226.022.231.021.233.018.237.016.241.014.244.012.247.011.25.008.254.005.257.004.26.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.022.224-.023.22-.026.216-.027.212-.028.21-.031.205-.032.202-.033.198-.034.194-.036.191-.038.187-.038.183-.04.179-.041.175-.042.172-.043.168-.043.163-.045.16-.046.155-.046.152-.048.148-.048.143-.048.139-.049.136-.05.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.05.051-.05.023-.025.023-.024.021-.024.02-.025.019-.024.018-.024.017-.023.015-.024.014-.023.013-.023.012-.023.01-.023.01-.022.008-.022.006-.023.006-.021.004-.022.004-.021.001-.021.001-.021-.001-.021-.001-.021-.004-.021-.004-.022-.006-.021-.006-.023-.008-.022-.01-.022-.01-.023-.012-.023-.013-.023-.014-.023-.015-.024-.017-.023-.018-.024-.019-.024-.02-.025-.021-.024-.023-.024-.023-.025-.051-.05-.056-.05-.06-.05-.065-.051-.07-.052-.074-.051-.079-.052-.083-.052-.088-.052-.092-.052-.096-.052-.101-.052-.106-.052-.11-.052-.114-.052-.118-.051-.123-.051-.126-.051-.131-.05-.136-.05-.139-.049-.143-.048-.148-.048-.152-.048-.155-.046-.16-.046-.163-.045-.168-.043-.172-.043-.175-.042-.179-.041-.183-.04-.187-.038-.191-.038-.194-.036-.198-.034-.202-.033-.205-.032-.21-.031-.212-.028-.216-.027-.22-.026-.224-.023-.226-.022-.231-.021-.233-.018-.237-.016-.241-.014-.244-.012-.247-.011-.25-.008-.254-.005-.257-.004-.26-.001-.26.001z" transform="scale(.5)"></path></symbol></defs><defs><symbol height="24" width="24" id="clock"><path d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm5.848 12.459c.202.038.202.333.001.372-1.907.361-6.045 1.111-6.547 1.111-.719 0-1.301-.582-1.301-1.301 0-.512.77-5.447 1.125-7.445.034-.192.312-.181.343.014l.985 6.238 5.394 1.011z" transform="scale(.5)"></path></symbol></defs><defs><marker orient="auto" markerHeight="12" markerWidth="12" markerUnits="userSpaceOnUse" refY="5" refX="7.9" id="arrowhead"><path d="M 0 0 L 10 5 L 0 10 z"></path></marker></defs><defs><marker refY="4.5" refX="4" orient="auto" markerHeight="8" markerWidth="15" id="crosshead"><path d="M 1,2 L 6,7 M 6,2 L 1,7" stroke-width="1pt" style="stroke-dasharray: 0px, 0px;" stroke="#000000" fill="none"></path></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="15.5" id="filled-head"><path d="M 18,7 L9,13 L14,7 L9,1 Z"></path></marker></defs><defs><marker orient="auto" markerHeight="40" markerWidth="60" refY="15" refX="15" id="sequencenumber"><circle r="6" cy="15" cx="15"></circle></marker></defs><g><rect class="note" ry="0" rx="0" height="40" width="384" stroke="#666" fill="#EDF2AE" y="120" x="332"></rect><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="125" x="524"><tspan x="524">1a. send and accept group invitation</tspan></text></g><g><rect class="note" ry="0" rx="0" height="40" width="384" stroke="#666" fill="#EDF2AE" y="406" x="332"></rect><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="411" x="524"><tspan x="524">1b. join via group link and accept request</tspan></text></g><g><line class="loopLine" y2="75" x2="726" y1="75" x1="322"></line><line class="loopLine" y2="695" x2="726" y1="75" x1="726"></line><line class="loopLine" y2="695" x2="726" y1="695" x1="322"></line><line class="loopLine" y2="695" x2="322" y1="75" x1="322"></line><line style="stroke-dasharray: 3px, 3px;" class="loopLine" y2="366" x2="726" y1="366" x1="322"></line><polygon class="labelBox" points="322,75 372,75 372,88 363.6,95 322,95"></polygon><text class="labelText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="88" x="347">alt</text><text class="loopText" style="font-size: 16px; font-weight: 400;" text-anchor="middle" y="93" x="549"><tspan x="549">[invite contact]</tspan></text><text class="loopText" style="font-size: 16px; font-weight: 400;" text-anchor="middle" y="384" x="524">[join via group link]</text></g><g><rect class="note" ry="0" rx="0" height="40" width="666" stroke="#666" fill="#EDF2AE" y="705" x="50"></rect><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="710" x="383"><tspan x="383">2. introduce new member Bob to all existing members</tspan></text></g><g><rect class="note" ry="0" rx="0" height="40" width="283" stroke="#666" fill="#EDF2AE" y="1010" x="549.5"></rect><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1015" x="691"><tspan x="691">prepare group member connections</tspan></text></g><g><rect class="note" ry="0" rx="0" height="40" width="218" stroke="#666" fill="#EDF2AE" y="1142" x="582"></rect><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1147" x="691"><tspan x="691">prepare direct connections</tspan></text></g><g><line class="loopLine" y2="1060" x2="810" y1="1060" x1="572"></line><line class="loopLine" y2="1192" x2="810" y1="1060" x1="810"></line><line class="loopLine" y2="1192" x2="810" y1="1192" x1="572"></line><line class="loopLine" y2="1192" x2="572" y1="1060" x1="572"></line><polygon class="labelBox" points="572,1060 622,1060 622,1073 613.6,1080 572,1080"></polygon><text class="labelText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1073" x="597">opt</text><text class="loopText" style="font-size: 16px; font-weight: 400;" text-anchor="middle" y="1078" x="716"><tspan x="716">[chat protocol</tspan></text><text class="loopText" style="font-size: 16px; font-weight: 400;" text-anchor="middle" y="1097" x="716"><tspan x="716">compatible version</tspan></text><text class="loopText" style="font-size: 16px; font-weight: 400;" text-anchor="middle" y="1117" x="716"><tspan x="716">< 2]</tspan></text></g><g><line class="loopLine" y2="860" x2="842.5" y1="860" x1="346"></line><line class="loopLine" y2="1307" x2="842.5" y1="860" x1="842.5"></line><line class="loopLine" y2="1307" x2="842.5" y1="1307" x1="346"></line><line class="loopLine" y2="1307" x2="346" y1="860" x1="346"></line><polygon class="labelBox" points="346,860 396,860 396,873 387.6,880 346,880"></polygon><text class="labelText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="873" x="371">loop</text><text class="loopText" style="font-size: 16px; font-weight: 400;" text-anchor="middle" y="878" x="619.25"><tspan x="619.25">[batched]</tspan></text></g><g><rect class="note" ry="0" rx="0" height="59" width="666" stroke="#666" fill="#EDF2AE" y="1441" x="50"></rect><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1446" x="383"><tspan x="383">group message forwarding</tspan></text><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1466" x="383"><tspan x="383">(while connections between members are being established)</tspan></text></g><g><rect class="note" ry="0" rx="0" height="40" width="666" stroke="#666" fill="#EDF2AE" y="1568" x="50"></rect><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1573" x="383"><tspan x="383">3. establish direct and group member connections</tspan></text></g><g><rect class="note" ry="0" rx="0" height="40" width="997" stroke="#666" fill="#EDF2AE" y="1759" x="50"></rect><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1764" x="549"><tspan x="549">3*. deduplicate new contact</tspan></text></g><g><rect class="note" ry="0" rx="0" height="40" width="562" stroke="#666" fill="#EDF2AE" y="2086" x="410"></rect><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="2091" x="691"><tspan x="691">merge existing and new contacts if received and sent probe hashes match</tspan></text></g><g><line class="loopLine" y2="1666" x2="1057" y1="1666" x1="40"></line><line class="loopLine" y2="2136" x2="1057" y1="1666" x1="1057"></line><line class="loopLine" y2="2136" x2="1057" y1="2136" x1="40"></line><line class="loopLine" y2="2136" x2="40" y1="1666" x1="40"></line><polygon class="labelBox" points="40,1666 90,1666 90,1679 81.6,1686 40,1686"></polygon><text class="labelText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1679" x="65">opt</text><text class="loopText" style="font-size: 16px; font-weight: 400;" text-anchor="middle" y="1684" x="573.5"><tspan x="573.5">[chat protocol compatible version < 2]</tspan></text></g><g><rect class="note" ry="0" rx="0" height="40" width="666" stroke="#666" fill="#EDF2AE" y="2146" x="50"></rect><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="2151" x="383"><tspan x="383">4. notify inviting member that connection is established</tspan></text></g><g><rect class="note" ry="0" rx="0" height="40" width="222" stroke="#666" fill="#EDF2AE" y="2292" x="246"></rect><text dy="1em" class="noteText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="2297" x="357"><tspan x="357">stops forwarding messages</tspan></text></g><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="175" x="523">x.grp.inv</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="195" x="523">invite Bob to group</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="214" x="523">(via contact connection)</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="246" x2="687" y1="246" x1="358"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="261" x="526">x.grp.acpt</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="281" x="526">accept invitation</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="300" x="526">(via member connection)</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="320" x="526">establish group member connection</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="351" x2="361" y1="351" x1="690"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="461" x="526">join via group link</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="481" x="526">SimpleX contact address</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="513" x2="361" y1="513" x1="690"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="528" x="523">x.grp.link.inv in SMP confirmation</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="548" x="523">accept joining member request,</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="567" x="523">sending group profile, etc.</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="587" x="523">establish group member connection</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="618" x2="687" y1="618" x1="358"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="633" x="523">x.grp.link.mem</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="653" x="523">send inviting member profile</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="685" x2="687" y1="685" x1="358"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="760" x="218">x.grp.mem.new</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="780" x="218">"announce" Bob</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="799" x="218">to existing members</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="819" x="218">(via member connections)</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="850" x2="79" y1="850" x1="356"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="910" x="523">x.grp.mem.intro * N</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="930" x="523">"introduce" members and</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="949" x="523">their chat protocol versions</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="969" x="523">(via member connection)</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="1000" x2="687" y1="1000" x1="358"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1207" x="526">x.grp.mem.inv * N</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1227" x="526">"invitations" to connect</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1246" x="526">for all members</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1266" x="526">(via member connection)</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="1297" x2="361" y1="1297" x1="690"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1322" x="218">x.grp.mem.fwd</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1342" x="218">forward "invitations" and</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1361" x="218">Bob's chat protocol version</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1381" x="218">to all members</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1400" x="218">(via member connections)</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="1431" x2="79" y1="1431" x1="356"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1515" x="382">messages between members and Bob are forwarded by Alice</text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" class="messageLine1" style="stroke-dasharray: 3px, 3px; fill: none;" y2="1548" x2="687" y1="1548" x1="76"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1563" x="385">​</text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" class="messageLine1" style="stroke-dasharray: 3px, 3px; fill: none;" y2="1558" x2="79" y1="1558" x1="690"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1623" x="382">establish group member connection</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="1656" x2="687" y1="1656" x1="76"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1716" x="382">establish direct connection</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="1749" x2="687" y1="1749" x1="76"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1814" x="385">x.info.probe</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1834" x="385">"probe" is sent to all new members</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="1866" x2="79" y1="1866" x1="690"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1881" x="855">x.info.probe.check</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1901" x="855">"probe" hash,</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1920" x="855">in case contact and</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1940" x="855">member profiles match</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="1971" x2="1018" y1="1971" x1="692"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="1986" x="858">x.info.probe.ok</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="2006" x="858"> original "probe",</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="2025" x="858"> in case contact and member</text><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="2045" x="858">are the same user</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="2076" x2="695" y1="2076" x1="1021"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="2201" x="215">x.grp.mem.con</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="2234" x2="353" y1="2234" x1="76"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="2249" x="526">x.grp.mem.con</text><line marker-end="url(#arrowhead)" style="fill: none;" stroke="none" stroke-width="2" class="messageLine0" y2="2282" x2="361" y1="2282" x1="690"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="2347" x="382">messages are sent via group connection without forwarding</text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" class="messageLine1" style="stroke-dasharray: 3px, 3px; fill: none;" y2="2380" x2="687" y1="2380" x1="76"></line><text dy="1em" class="messageText" style="font-size: 16px; font-weight: 400;" alignment-baseline="middle" dominant-baseline="middle" text-anchor="middle" y="2395" x="385">​</text><line marker-end="url(#arrowhead)" stroke="none" stroke-width="2" class="messageLine1" style="stroke-dasharray: 3px, 3px; fill: none;" y2="2390" x2="79" y1="2390" x1="690"></line></svg> \ No newline at end of file diff --git a/docs/protocol/simplex-chat.md b/docs/protocol/simplex-chat.md index 71d5efcef7..c74465f6fe 100644 --- a/docs/protocol/simplex-chat.md +++ b/docs/protocol/simplex-chat.md @@ -2,7 +2,7 @@ title: SimpleX Chat Protocol revision: 08.08.2022 --- -DRAFT Revision 0.1, 2022-08-08 +Revision 2, 2024-06-24 Evgeny Poberezkin @@ -10,25 +10,30 @@ Evgeny Poberezkin ## Abstract -SimpleX Chat Protocol is a protocol used to by SimpleX Chat clients to exchange messages. This protocol relies on lower level SimpleX protocols - SimpleX Messaging Protocol (SMP) and SimpleX Messaging Agent protocol. SimpleX Chat Protocol describes the format of messages and the client operations that should be performed when receiving such messages. +SimpleX Chat Protocol is a protocol used by SimpleX Chat clients to exchange messages. This protocol relies on lower level SimpleX protocols - SimpleX Messaging Protocol (SMP) and SimpleX Messaging Agent protocol. SimpleX Chat Protocol describes the format of messages and the client operations that should be performed when receiving such messages. ## Scope The scope of SimpleX Chat Protocol is application level messages, both for chat functionality, related to the conversations between the clients, and extensible for any other application functions. Currently supported chat functions: - direct and group messages, -- message replies (quoting), forwarded messages and message deletions, -- message attachments: images and files, +- message replies (quoting), message editing, forwarded messages and message deletions, +- message attachments: images, videos, voice messages and files, - creating and managing chat groups, - invitation and signalling for audio/video WebRTC calls. ## General message format -SimpleX Chat protocol supports two message formats: +SimpleX Chat protocol supports these message formats: - JSON-based format for chat and application messages. +- compressed format for adapting larger messages to reduced size of message envelope, caused by addition of PQ encryption keys to SMP agent message envelope. - binary format for sending files or any other binary data. +JSON-based message format supports batching inside a single container message, by encoding list of messages as JSON array. + +Current implementation of chat protocol in SimpleX Chat uses SimpleX File Transfer Protocol (XFTP) for file transfer, with passing file description as chat protocol messages, instead passing files in binary format via SMP connections. + ### JSON format for chat and application messages This document uses JTD schemas [RFC 8927](https://www.rfc-editor.org/rfc/rfc8927.html) to define the properties of chat messages, with some additional restrictions on message properties included in metadata member of JTD schemas. In case of any contradiction between JSON examples and JTD schema the latter MUST be considered correct. @@ -77,8 +82,22 @@ For example, this message defines a simple text message `"hello!"`: `params` property includes message data, depending on `event`, as defined below and in [JTD schema](./simplex-chat.schema.json). +### Compressed format + +The syntax of compressed message is defined by the following ABNF notation: + +```abnf +compressedMessage = %s"X" 1*15780 OCTET; compressed message data +``` + +Compressed message is required to fit into 13388 bytes, accounting for agent overhead (see Protocol's maxCompressedMsgLength). + +The actual JSON message is required to fit into 15610 bytes, accounting for group message forwarding (x.grp.msg.forward) overhead (see Protocol's maxEncodedMsgLength). + ### Binary format for sending files +> Note: Planned to be deprecated. No longer used for file transfer in SimpleX Chat implementation of chat protocol. + SimpleX Chat clients use separate connections to send files using a binary format. File chunk size send in each message MUST NOT be bigger than 15,780 bytes to fit into 16kb (16384 bytes) transport block. The syntax of each message used to send files is defined by the following ABNF notation: @@ -117,7 +136,9 @@ SimpleX Chat Protocol supports the following message types passed in `event` pro - `x.contact` - contact profile and additional data sent as part of contact request to a long-term contact address. - `x.info*` - messages to send, update and de-duplicate contact profiles. - `x.msg.*` - messages to create, update and delete content chat items. +- `x.msg.file.descr` - message to transfer XFTP file description. - `x.file.*` - messages to accept and cancel sending files (see files sub-protocol). +- `x.direct.del` - message to notify about contact deletion. - `x.grp.*` - messages used to manage groups and group members (see group sub-protocol). - `x.call.*` - messages to invite to WebRTC calls and send signalling messages. - `x.ok` - message sent during connection handshake. @@ -136,7 +157,7 @@ This message is sent by both sides of the connection during the connection hands ### Probing for duplicate contacts -As there are no globally unique user identitifiers, when the contact a user is already connected to is added to the group by some other group member, this contact will be added to user's list of contacts as a new contact. To allow merging such contacts, "a probe" (random base64url-encoded 32 bytes) SHOULD be sent to all new members as part of `x.info.probe` message and, in case there is a contact with the same profile, the hash of the probe MAY be sent to it as part of `x.info.probe.check` message. In case both the new member and the existing contact are the same user (they would receive both the probe and its hash), the contact would send back the original probe as part of `x.info.probe.ok` message via the previously existing contact connection – proving to the sender that this new member and the existing contact are the same user, in which case the sender SHOULD merge these two contacts. +As there are no globally unique user identifiers, when the contact a user is already connected to is added to the group by some other group member, this contact will be added to user's list of contacts as a new contact. To allow merging such contacts, "a probe" (random base64url-encoded 32 bytes) SHOULD be sent to all new members as part of `x.info.probe` message and, in case there is a contact with the same profile, the hash of the probe MAY be sent to it as part of `x.info.probe.check` message. In case both the new member and the existing contact are the same user (they would receive both the probe and its hash), the contact would send back the original probe as part of `x.info.probe.ok` message via the previously existing contact connection – proving to the sender that this new member and the existing contact are the same user, in which case the sender SHOULD merge these two contacts. Sending clients MAY disable this functionality, and receiving clients MAY ignore probe messages. @@ -155,6 +176,8 @@ Message content can be one of four types: - `text` - no file attachment is expected for this format, `text` property MUST be non-empty. - `file` - attached file is required, `text` property MAY be empty. - `image` - attached file is required, `text` property MAY be empty. +- `video` - attached file is required, `text` property MAY be empty. +- `voice` - attached file is required, `text` property MAY be empty. - `link` - no file attachment is expected, `text` property MUST be non-empty. `preview` property contains information about link preview. See `/definition/msgContent` in [JTD schema](./simplex-chat.schema.json) for message container format. @@ -181,25 +204,29 @@ File attachment can optionally include connection address to receive the file - `x.file.cancel` message is sent to notify the recipient that sending of the file was cancelled. It is sent in response to accepting the file with `x.file.acpt.inv` message. It is sent in the same connection where the file was offered. +`x.msg.file.descr` message is used to send XFTP file description. File descriptions that don't fit into a single chat protocol message are sent in parts, with messages including part number (`fileDescrPartNo`) and description completion marker (`fileDescrComplete`). Recipient client accumulates description parts and starts file download upon completing file description. + ## Sub-protocol for chat groups ### Decentralized design for chat groups -SimpleX Chat groups are fully decentralized and do not have any globally unique group identifiers - they are only defined on client devices as a group profile and a set of bi-directional SimpleX connections with other group members. When a new member accepts group invitation, the inviting member introduces a new member to all existing members and forwards the connection addresses so that they can establish direct and group member connections. +SimpleX Chat groups are fully decentralized and do not have any globally unique group identifiers - they are only defined on client devices as a group profile and a set of bi-directional SimpleX connections with other group members. When a new member accepts group invitation or joins via group link, the inviting member introduces a new member to all existing members and forwards the connection addresses so that they can establish direct and group member connections. There is a possibility of the attack here: as the introducing member forwards the addresses, they can substitute them with other addresses, performing MITM attack on the communication between existing and introduced members - this is similar to the communication operator being able to perform MITM on any connection between the users. To mitigate this attack this group sub-protocol will be extended to allow validating security of the connection by sending connection verification out-of-band. -Clients are RECOMMENDED to indicate in the UI whether the connection to a group member or contact was made directly or via annother user. +Clients are RECOMMENDED to indicate in the UI whether the connection to a group member or contact was made directly or via another user. Each member in the group is identified by a group-wide unique identifier used by all members in the group. This is to allow referencing members in the messages and to allow group message integrity validation. The diagram below shows the sequence of messages sent between the users' clients to add the new member to the group. +While introduced members establish connection inside group, inviting member forwards messages between them by sending `x.grp.msg.forward` messages. When introduced members finalize connection, they notify inviting member to stop forwarding via `x.grp.mem.con` message. + ![Adding member to the group](./diagrams/group.svg) ### Member roles -Currently members can have one of three roles - `owner`, `admin` and `member`. The user that created the group is self-assigned owner role, the new members are assigned role by the member who adds them - only `owner` and `admin` members can add new members; only `owner` members can add members with `owner` role. +Currently members can have one of four roles - `owner`, `admin`, `member` and `observer`. The user that created the group is self-assigned owner role, the new members are assigned role by the member who adds them - only `owner` and `admin` members can add new members; only `owner` members can add members with `owner` role. `Observer` members only receive messages and aren't allowed to send messages. ### Messages to manage groups and add members @@ -207,6 +234,10 @@ Currently members can have one of three roles - `owner`, `admin` and `member`. T `x.grp.acpt` message is sent as part of group member connection handshake, only to the inviting user. +`x.grp.link.inv` message is sent as part of connection handshake to member joining via group link, and contains group profile and initial information about inviting and joining member. + +`x.grp.link.mem` message is sent as part of connection handshake to member joining via group link, and contains remaining information about inviting member. + `x.grp.mem.new` message is sent by the inviting user to all connected members (and scheduled as pending to all announced but not yet connected members) to announce a new member to the existing members. This message MUST only be sent by members with `admin` or `owner` role. Receiving clients MUST ignore this message if it is received from member with `member` role. `x.grp.mem.intro` messages are sent by the inviting user to the invited member, via their group member connection, one message for each existing member. When this message is sent by any other member than the one who invited the recipient it MUST be ignored. @@ -219,6 +250,10 @@ Currently members can have one of three roles - `owner`, `admin` and `member`. T `x.grp.mem.role` message is sent to update group member role - it is sent to all members by the member who updated the role of the member referenced in this message. This message MUST only be sent by members with `admin` or `owner` role. Receiving clients MUST ignore this message if it is received from member with role less than `admin`. +`x.grp.mem.restrict` message is sent to group members to communicate group member restrictions, such as member being blocked for sending messages. + +`x.grp.mem.con` message is sent by members connecting inside group to inviting member, to notify the inviting member they have completed the connection and no longer require forwarding messages between them. + `x.grp.mem.del` message is sent to delete a member - it is sent to all members by the member who deletes the member referenced in this message. This message MUST only be sent by members with `admin` or `owner` role. Receiving clients MUST ignore this message if it is received from member with `member` role. `x.grp.leave` message is sent to all members by the member leaving the group. If the only group `owner` leaves the group, it will not be possible to delete it with `x.grp.del` message - but all members can still leave the group with `x.grp.leave` message and then delete a local copy of the group. @@ -227,6 +262,10 @@ Currently members can have one of three roles - `owner`, `admin` and `member`. T `x.grp.info` message is sent to all members by the member who updated group profile. Only group owners can update group profiles. Clients MAY implement some conflict resolution strategy - it is currently not implemented by SimpleX Chat client. This message MUST only be sent by members with `owner` role. Receiving clients MUST ignore this message if it is received from member other than with `owner` role. +`x.grp.direct.inv` message is sent to a group member to propose establishing a direct connection between members, thus creating a contact with another member. + +`x.grp.msg.forward` message is sent by inviting member to forward messages between introduced members, while they are connecting. + ## 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. @@ -240,3 +279,66 @@ These message are used for WebRTC calls: 3. `x.call.answer`: to continue with call connection the initiating clients must reply with `x.call.answer` message. This message contains WebRTC answer and collected ICE candidates. Additional ICE candidates can be sent in `x.call.extra` message. 4. `x.call.end` message is sent to notify the other party that the call is terminated. + +## Threat model + +This threat model compliments SMP, XFTP, push notifications and XRCP protocols threat models: + +- [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). + +#### A user's contact + +*can:* + +- send messages prohibited by user's preferences or otherwise act non-compliantly with user's preferences (for example, if message with updated preferences was lost or failed to be processed, or with modified client), in which case user client should treat such messages and actions as prohibited. + +- by exchanging special messages with user's client, match user's contact with existing group members and/or contacts that have identical user profile (see [Probing for duplicate contacts](#probing-for-duplicate-contacts)). + +- identify that and when a user is using SimpleX, in case user has delivery receipts enabled, or based on other automated client responses. + +*cannot:* + +- match user's contact with existing group members and/or contacts with different or with incognito profiles. + +- match user's contact without communicating with the user's client. + +#### A group member + +*can:* + +- send messages prohibited by group's preferences and member restrictions or otherwise act non-compliantly with preferences and restrictions (for example, if decentralized group state diverged, or with modified client), in which case user client should treat such messages and actions as prohibited. + +- create a direct contact with a user if group permissions allow it. + +- by exchanging special messages with user's client, match user's group member record with the existing group members and/or contacts that have identical user profile. + +- undetectably send different messages to different group members, or selectively send messages to some members and not send to others. + +- identify that and when a user is using SimpleX, in case user has delivery receipts enabled, or based on other automated client responses. + +- join the same group several times, from the same or from different user profile, and pretend to be different members. + +*cannot:* + +- match user's contact with existing group members and/or contacts with different or with incognito profiles. + +- match user's group member record with existing group members and/or contacts without communication of user's client. + +- determine whether two group members with different or with incognito profiles are the same user. + +#### A group admin + +*can:* + +- carry out MITM attack between user and other group member(s) when forwarding invitations for group connections (user can detect such attack by verifying connection security codes out-of-band). + +- undetectably forward different messages to different group members, selectively adding, modifying, and dropping forwarded messages. + +- disrupt decentralized group state by sending different messages that change group state (such as adding or removing members, member role changes, etc.) to different group members, or sending such messages selectively. + +*cannot:* + +- prove that two group members with incognito profiles is the same user. diff --git a/docs/protocol/simplex-chat.schema.json b/docs/protocol/simplex-chat.schema.json index a9738190bd..50d41265f7 100644 --- a/docs/protocol/simplex-chat.schema.json +++ b/docs/protocol/simplex-chat.schema.json @@ -8,7 +8,7 @@ "displayName": { "type": "string", "metadata": { - "format": "non-empty string without spaces, the first character must not be # or @" + "format": "non-empty string, the first character must not be # or @" } }, "fullName": {"type": "string"} @@ -19,6 +19,39 @@ "metadata": { "format": "data URI format for base64 encoded image" } + }, + "contactLink": {"ref": "connReqUri"}, + "preferences": { + "type": "string", + "metadata": { + "format": "JSON encoded user preferences" + } + } + }, + "additionalProperties": true + }, + "groupProfile": { + "properties": { + "displayName": { + "type": "string", + "metadata": { + "format": "non-empty string, the first character must not be # or @" + } + }, + "fullName": {"type": "string"} + }, + "optionalProperties": { + "image": { + "type": "string", + "metadata": { + "format": "data URI format for base64 encoded image" + } + }, + "groupPreferences": { + "type": "string", + "metadata": { + "format": "JSON encoded user preferences" + } } }, "additionalProperties": true @@ -29,6 +62,8 @@ }, "optionalProperties": { "file": {"ref": "fileInvitation"}, + "ttl": {"type": "integer"}, + "live": {"type": "boolean"}, "quote": { "properties": { "msgRef": {"ref": "msgRef"}, @@ -56,17 +91,53 @@ } }, "image": { - "text": {"type": "string", "metadata": {"comment": "can be empty"}}, - "image": {"ref": "base64url"} + "properties": { + "text": {"type": "string", "metadata": {"comment": "can be empty"}}, + "image": {"ref": "base64url"} + } + }, + "video": { + "properties": { + "text": {"type": "string", "metadata": {"comment": "can be empty"}}, + "image": {"ref": "base64url"}, + "duration": {"type": "integer"} + } + }, + "voice": { + "properties": { + "text": {"type": "string", "metadata": {"comment": "can be empty"}}, + "duration": {"type": "integer"} + } }, "file": { - "text": {"type": "string", "metadata": {"comment": "can be empty"}} + "properties": { + "text": {"type": "string", "metadata": {"comment": "can be empty"}} + } + }, + "report": { + "properties": { + "text": {"type": "string", "metadata": {"comment": "can be empty, includes report reason for old clients"}}, + "reason": {"enum": ["spam", "illegal", "community", "other"]} + } } }, "metadata": { "comment": "it is RECOMMENDED that the clients support other values in `type` properties showing them as text messages in case `text` property is present" } }, + "msgReaction" : { + "discriminator": "type", + "mapping": { + "emoji": { + "properties": { + "emoji": { + "type": "string", + "metadata": {"comment": "emoji character"} + } + } + } + } + }, "msgRef": { "properties": { "msgId": {"ref": "base64url"}, @@ -91,7 +162,31 @@ "fileSize": {"type": "uint32"} }, "optionalProperties": { - "fileConnReq": {"ref": "connReqUri"} + "fileDigest": {"ref": "base64url"}, + "fileConnReq": {"ref": "connReqUri"}, + "fileDescr": {"ref": "fileDescription"} + } + }, + "fileDescription": { + "properties": { + "fileDescrText": { + "type": "string", + "metadata": { + "format": "XFTP file description part text" + } + }, + "fileDescrPartNo": { + "type": "integer", + "metadata": { + "format": "XFTP file description part number" + } + }, + "fileDescrComplete": { + "type": "boolean", + "metadata": { + "format": "XFTP file description completion marker" + } + } } }, "linkPreview": { @@ -100,6 +195,21 @@ "title": {"type": "string"}, "description": {"type": "string"}, "image": {"ref": "base64url"} + }, + "optionalProperties": { + "content": {"ref": "linkContent"} + } + }, + "linkContent": { + "discriminator": "type", + "mapping": { + "page": {}, + "image": {}, + "video": { + "optionalProperties": { + "duration": {"type": "integer"} + } + } } }, "groupInvitation": { @@ -107,15 +217,27 @@ "fromMember": {"ref": "memberIdRole"}, "invitedMember": {"ref": "memberIdRole"}, "connRequest": {"ref": "connReqUri"}, - "groupProfile": {"ref": "profile"} + "groupProfile": {"ref": "groupProfile"} }, "optionalProperties": { "groupLinkId": {"ref": "base64url"}, + "groupSize": {"type": "integer"}, "metadata": { - "comment": "used to identify invitation via group link" + "comment": "groupLinkId is used to identify invitation via group link" } } }, + "groupLinkInvitation": { + "properties": { + "fromMember": {"ref": "memberIdRole"}, + "fromMemberName": {"type": "string"}, + "invitedMember": {"ref": "memberIdRole"}, + "groupProfile": {"ref": "groupProfile"} + }, + "optionalProperties": { + "groupSize": {"type": "integer"} + } + }, "memberIdRole": { "properties": { "memberId": {"ref": "base64url"}, @@ -127,16 +249,35 @@ "memberId": {"ref": "base64url"}, "memberRole": {"ref": "groupMemberRole"}, "profile": {"ref": "profile"} + }, + "optionalProperties": { + "v": {"ref": "chatVersionRange"} + } + }, + "memberRestrictions": { + "properties": { + "restriction": {"ref": "memberRestrictionStatus"} + } + }, + "memberRestrictionStatus": { + "enum": ["blocked", "unrestricted"] + }, + "chatVersionRange": { + "type": "string", + "metadata": { + "format": "chat version range string encoded as `<min>-<max>`, or as `<number>` if min = max" } }, "introInvitation": { "properties": { - "groupConnReq": {"ref": "connReqUri"}, + "groupConnReq": {"ref": "connReqUri"} + }, + "optionalProperties": { "directConnReq": {"ref": "connReqUri"} } }, "groupMemberRole": { - "enum": ["author", "member", "admin", "owner"] + "enum": ["observer", "author", "member", "admin", "owner"] }, "callInvitation": { "properties": { @@ -257,6 +398,17 @@ "params": {"ref": "msgContainer"} } }, + "x.msg.file.descr": { + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "msgId": {"ref": "base64url"}, + "fileDescr": {"ref": "fileDescription"} + } + } + } + }, "x.msg.update": { "properties": { "msgId": {"ref": "base64url"}, @@ -264,6 +416,10 @@ "properties": { "msgId": {"ref": "base64url"}, "content": {"ref": "msgContent"} + }, + "optionalProperties": { + "ttl": {"type": "integer"}, + "live": {"type": "boolean"} } } } @@ -274,6 +430,24 @@ "params": { "properties": { "msgId": {"ref": "base64url"} + }, + "optionalProperties": { + "memberId": {"ref": "base64url"} + } + } + } + }, + "x.msg.react": { + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "msgId": {"ref": "base64url"}, + "reaction": {"ref": "msgReaction"}, + "add": {"type": "boolean"} + }, + "optionalProperties": { + "memberId": {"ref": "base64url"} } } } @@ -294,8 +468,10 @@ "params": { "properties": { "msgId": {"ref": "base64url"}, - "fileConnReq": {"ref": "connReqUri"}, "fileName": {"type": "string"} + }, + "optionalProperties": { + "fileConnReq": {"ref": "connReqUri"} } } } @@ -310,6 +486,14 @@ } } }, + "x.direct.del": { + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": {} + } + } + }, "x.grp.inv": { "properties": { "msgId": {"ref": "base64url"}, @@ -330,6 +514,26 @@ } } }, + "x.grp.link.inv": { + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "groupLinkInvitation": {"ref": "groupLinkInvitation"} + } + } + } + }, + "x.grp.link.mem": { + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "profile": {"ref": "profile"} + } + } + } + }, "x.grp.mem.new": { "properties": { "msgId": {"ref": "base64url"}, @@ -346,6 +550,9 @@ "params": { "properties": { "memberInfo": {"ref": "memberInfo"} + }, + "optionalProperties": { + "memberRestrictions": {"ref": "memberRestrictions"} } } } @@ -394,6 +601,27 @@ } } }, + "x.grp.mem.restrict": { + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "memberId": {"ref": "base64url"}, + "memberRestrictions": {"ref": "memberRestrictions"} + } + } + } + }, + "x.grp.mem.con": { + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "memberId": {"ref": "base64url"} + } + } + } + }, "x.grp.mem.del": { "properties": { "msgId": {"ref": "base64url"}, @@ -425,7 +653,42 @@ "msgId": {"ref": "base64url"}, "params": { "properties": { - "groupProfile": {"ref": "profile"} + "groupProfile": {"ref": "groupProfile"} + } + } + } + }, + "x.grp.direct.inv": { + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "connReq": {"ref": "connReqUri"} + }, + "optionalProperties": { + "content": {"ref": "msgContent"} + } + } + } + }, + "x.grp.msg.forward": { + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "memberId": {"ref": "base64url"}, + "msg": { + "type": "string", + "metadata": { + "format": "JSON encoded chat message" + } + }, + "msgTs": { + "type": "string", + "metadata": { + "format": "ISO8601 UTC time of the message" + } + } } } } @@ -436,7 +699,7 @@ "params": { "properties": { "callId": {"ref": "base64url"}, - "invitation": {} + "invitation": {"ref": "callInvitation"} } } } diff --git a/docs/rfcs/2023-09-30-pq-double-ratchet.md b/docs/rfcs/2023-09-30-pq-double-ratchet.md index 255051320d..95e7aa3d1d 100644 --- a/docs/rfcs/2023-09-30-pq-double-ratchet.md +++ b/docs/rfcs/2023-09-30-pq-double-ratchet.md @@ -82,7 +82,7 @@ def RatchetInitAlicePQ2HE(state, SK, bob_dh_public_key, shared_hka, shared_nhkb, state.PQRs = GENERATE_PQKEM() state.PQRr = bob_pq_kem_encapsulation_key state.PQRss = random // shared secret for KEM - state.PQRenc_ss = PQKEM-ENC(state.PQRr.encaps, state.PQRss) // encapsulated additional shared secret + state.PQRct = PQKEM-ENC(state.PQRr, state.PQRss) // encapsulated additional shared secret // above added for KEM // below augments DH key agreement with PQ shared secret state.RK, state.CKs, state.NHKs = KDF_RK_HE(SK, DH(state.DHRs, state.DHRr) || state.PQRss) @@ -103,7 +103,7 @@ def RatchetInitBobPQ2HE(state, SK, bob_dh_key_pair, shared_hka, shared_nhkb, bob state.PQRs = bob_pq_kem_key_pair state.PQRr = None state.PQRss = None - state.PQRenc_ss = None + state.PQRct = None // above added for KEM state.RK = SK state.CKs = None @@ -132,10 +132,10 @@ def RatchetEncryptPQ2HE(state, plaintext, AD): // encapsulation key from PQRs and encapsulated shared secret is added to header header = HEADER_PQ2( dh = state.DHRs.public, + kem = state.PQRs.public, // added for KEM #2 + ct = state.PQRct // added for KEM #1 pn = state.PN, n = state.Ns, - encaps = state.PQRs.encaps, // added for KEM #1 - enc_ss = state.PQRenc_ss // added for KEM #2 ) enc_header = HENCRYPT(state.HKs, header) state.Ns += 1 @@ -162,6 +162,16 @@ def RatchetDecryptPQ2HE(state, enc_header, ciphertext, AD): state.Nr += 1 return DECRYPT(mk, ciphertext, CONCAT(AD, enc_header)) +// DecryptHeader is the same as in double ratchet specification +def DecryptHeader(state, enc_header): + header = HDECRYPT(state.HKr, enc_header) + if header != None: + return header, False + header = HDECRYPT(state.NHKr, enc_header) + if header != None: + return header, True + raise Error() + def DHRatchetPQ2HE(state, header): state.PN = state.Ns state.Ns = 0 @@ -170,16 +180,16 @@ def DHRatchetPQ2HE(state, header): state.HKr = state.NHKr state.DHRr = header.dh // save new encapsulation key from header - state.PQRr = header.encaps + state.PQRr = header.kem // decapsulate shared secret from header - KEM #2 - ss = PQKEM-DEC(state.PQRs.decaps, header.enc_ss) + ss = PQKEM-DEC(state.PQRs.private, header.ct) // use decapsulated shared secret with receiving ratchet state.RK, state.CKr, state.NHKr = KDF_RK_HE(state.RK, DH(state.DHRs, state.DHRr) || ss) state.DHRs = GENERATE_DH() // below is added for KEM state.PQRs = GENERATE_PQKEM() // generate new PQ key pair state.PQRss = random // shared secret for KEM - state.PQRenc_ss = PQKEM-ENC(state.PQRr.encaps, state.PQRss) // encapsulated additional shared secret KEM #1 + state.PQRct = PQKEM-ENC(state.PQRr, state.PQRss) // encapsulated additional shared secret KEM #1 // above is added for KEM // use new shared secret with sending ratchet state.RK, state.CKs, state.NHKs = KDF_RK_HE(state.RK, DH(state.DHRs, state.DHRr) || state.PQRss) @@ -201,7 +211,7 @@ The main downside is the absense of performance-efficient implementation for aar ## Implementation considerations for SimpleX Chat -As SimpleX Chat pads messages to a fixed size, using 16kb transport blocks, the size increase introduced by this scheme will not cause additional traffic in most cases. For large texts it may require additional messages to be sent. Similarly, for media previews it may require either reducing the preview size (and quality) or sending additional messages. +As SimpleX Chat pads messages to a fixed size, using 16kb transport blocks, the size increase introduced by this scheme will not cause additional traffic in most cases. For large texts it may require additional messages to be sent. Similarly, for media previews it may require either reducing the preview size (and quality), or sending additional messages, or compressing the current JSON encoding, e.g. with zstd algorithm. That might be the primary reason why this scheme was not adopted by Signal, as it would have resulted in substantial traffic growth – to the best of our knowledge, Signal messages are not padded to a fixed size. @@ -209,6 +219,8 @@ Sharing the initial keys in case of SimpleX Chat it is equivalent to sharing the It is possible to postpone sharing the encapsulation key until the first message from Alice (confirmation message in SMP protocol), the party sending connection request. The upside here is that the invitation link size would not increase. The downside is that the user profile shared in this confirmation will not be encrypted with PQ-resistant algorithm. To mitigate it, the hadnshake protocol can be modified to postpone sending the user profile until the second message from Alice (HELLO message in SMP protocol). +Another consideration is pairwise ratchets in groups. Key generation in sntrup761 is quite slow - on slow devices it can probably be as slow as 10 keys per second, so using this primitive in groups larger than 10 members would result in slow performance. An option could be not to use ratchets in groups at all, but that would result in the lack of protection in small groups that simply combine multiple devices of 1-3 people. So a better option would be to support dynamically adding and removing sntrup761 keys for pairwise ratchets in groups, which means that when sending each message a boolean flag needs to be passed whether to use PQ KEM or not. + ## Summary If chosen PQ KEM proves secure against quantum computer attacks, then the proposed augmented double ratchet will also be secure against quantum computer attack, including break-in recovery property, while keeping deniability and forward secrecy, because the [same proof](https://eprint.iacr.org/2016/1013.pdf) as for double ratchet algorithm would hold here, provided KEM is secure. diff --git a/docs/rfcs/2023-11-21-inactive-group-members.md b/docs/rfcs/2023-11-21-inactive-group-members.md index 66e58848af..12333855a8 100644 --- a/docs/rfcs/2023-11-21-inactive-group-members.md +++ b/docs/rfcs/2023-11-21-inactive-group-members.md @@ -108,3 +108,33 @@ Sending member builds messages history starting starting from requested/remember \*** Same XGrpMsgHistory protocol event could be sent by host to new members, after sending introductions. + +--- + +Update 2024-02-12: + +### Group "pings" + +Alternatively to tracking unanswered messages counts per member, which is complex and in some cases as discussed above ineffective, group members could periodically send group wide pings indicating their active presence. + +```haskell +XGrpPing :: ChatMsgEvent 'Json +``` + +Members track: + +- inactive flag (as above - set on QUOTA errors as well) +- last_snd_ts on group +- last_rcv_ts on group member + +Clients run a worker process for checking last_snd_ts in each of their groups, and send pings to groups on a periodic basis. + +- part of cleanup manager or separate process? +- on each worker step, for each group matching criteria to send ping, send ping with a random delay to reduce correlation between groups (spawn a separate thread with a random delay for each group) +- criteria for sending ping: last_snd_ts earlier than group_ping_interval ago +- configure group_ping_interval to, for example, 23 hours (so that if user opens app each day at same time client will match criteria to send pings daily) + +Clients receiving pings: + +- update last_rcv_ts +- when sending a message to group, check only for timestamp difference (no unanswered snd msg count logic as above) diff --git a/docs/rfcs/2024-01-04-members-profile-update.md b/docs/rfcs/2024-01-04-members-profile-update.md new file mode 100644 index 0000000000..c91cbcabab --- /dev/null +++ b/docs/rfcs/2024-01-04-members-profile-update.md @@ -0,0 +1,50 @@ +# Sending profile update to group members + +## Problem + +Profile updates are only sent to direct contacts, as sending them to all group member connections is prohibitively expensive. This results in group members not receiving profile updates. Previously the issue was less acute as all group members were created with two sets of connections, one being used as direct connection for their respective contacts (though the traffic issue was more pronounced due to that); also contacts were merged across group members. Since client started to support deletion of group member contact records, and later stopped creating direct connections for group members altogether, it became less likely for group members to receive profile updates. Still even in the latest versions group members can receive profile updates after creating direct contacts via "Send direct message" button, or connecting out-of-band and merging contact and member records. + +## Solution + +Keep track of which members received latest profile updates. Send profile updates when user is active in group. + +### How to track + +- users.user_member_profile_updated_at +- group_members.user_member_profile_sent_at +- when user updates profile, remember new user_member_profile_updated_at, later to be compared against group_members.user_member_profile_sent_at + +### What to track + +- not all profile fields make sense to send in profile update to group members +- changes to displayName, fullName, image should be sent +- changes to preferences aren't necessary to send as they only apply to user contacts +- changes to contactLink may be sent, but can also be excluded for purposes of privacy + - some users don't expect that sharing address (contactLink) shares it not only with contacts, but also group members + - this is a broader issue, as the user's contact link may also be sent in user's profile by admin when introducing members - it makes sense to either ignore this for the purposes of this feature, of change it in group handshake as well +- it then makes sense to remember new timestamp on user record only if name or image is changed + +### When/To whom to send + +- when user is active in group (i.e. broadcasts message via sendGroupMessage), compare group_members.user_member_profile_sent_at against users.user_member_profile_updated_at to determine whether latest profile update wasn't yet sent +- don't send to members in groups where user is incognito +- don't send to members with whom user has direct contact (as it would overwrite full profile update sent to contact)? + - alternatively it may be better to send the same pruned profile to such members, and for them to ignore this update (or only apply name and image updates, in case sender has silently deleted them as contact without notifying?): + - this would ensure that they do receive it in case they silently deleted contact without notifying user + - it simplifies processing, as then the same message is sent to all group members + - may remember "profile update hashes" on receiving side to not apply profile updates received via member connection to contact profile, if they arrive after previously processed updates received via contact connection (e.g. update that was received late would overwrite more up-to-date updates received via contact connection, until following messages arrive) +- it seems unnecessary to send profile updates on service messages to individual members: + - it would otherwise lead to members having different profiles of user at different points in time + - not all of these messages create chat items anyway (forward, intro messages), so user name/image wouldn't matter + - most if not all of these messages are sent by admins, who are likely to send either some content messages, group updates, or announce new members (x.grp.mem.new, which is also broadcasted) + - it simplifies processing, as then profile update is sent to all current members +- considering above points, perhaps we can simplify to track user_member_profile_sent_at on groups instead of group_members + - group_members.user_member_profile_sent_at -> groups.user_member_profile_sent_at + +### How to send + +Two options: +- send as a separate message, don't special case +- send batched with the main message (using chat protocol batching mechanism), it would avoid broadcasting additional message for users without profile images, and likely in some cases (when main message is short) even with them + - conflicts with forwarding as forwarding of batched messages is not supported + - simply implementing forwarding of batched messages is not enough, because currently there is no way to differentiate between history and other batched messages (and received history shouldn't be forwarded) diff --git a/docs/rfcs/2024-02-12-database-migration.md b/docs/rfcs/2024-02-12-database-migration.md new file mode 100644 index 0000000000..7d4dcc8d01 --- /dev/null +++ b/docs/rfcs/2024-02-12-database-migration.md @@ -0,0 +1,130 @@ +# Database migration and other operations + +## Problem + +Migrating database to another device is very complex for most people - it is multi-step and error-prone. + +In addition to that, any database operation is confusing as it requires stopping chat. + +## Solution + +Let users migrate database to another device by scanning QR code. + +Simplify other database operations by removing the need to compose multiple actions, stop chat, etc. + +To support it, we already added the way to represent the file as link/QR code (by uploading file description to XFTP, and supporting "recursive" descriptions). + +There will be these actions in the Database settings (no stop/start chat toggle): + +- Export database. +- Import database. +- Migrate from another device. +- Set passphrase (or Change passphrase if it was set). +- Remove passphrase from device / Store passphrase on the device. + +Stop chat toggle will be moved to dev tools. + +Migrate to another device will be available in the top part of the settings, + + +### Database export + +Currently, it requires these steps: + +1. Open Database settings. +2. Stop chat (many users don't understand it). +3. Tap "Export database" in settings. +4. Look at the alert that says "set passphrase". +5. Tap Ok. +6. Tap Set passphrase. +7. Enter passphrase and confirm. +8. Exit back to Database settings. +9. Tap "Export database" again. +10. Choose file location and save. +11. Tap "New archive". +12. Remove exported archive. + +These steps are all very confusing, and if they were to stay as composable steps, they belong to dev tools. + +Instead we can offer these simple steps: + +1. Open Database settings. +2. Tap "Export database". +3. Alert will appear saying: "The chat will stop, and you will need to set (or verify) database passphrase. Continue?". +4. Tap "Ok". +5. Enter passphrase and confirm in the window that appears (or verify if it was already set, possibly allowing to skip this step). +7. Choose whether to save file or upload to XFTP and generate link. +8. File: choose file location and save. + Link: show upload progress and then show link to copy. +9. Alert will appear saying: "Database exported!", exported archive will be automatically removed. + +So instead of asking users to understand the required sequence of steps, we will guide them through the required process. + +### Database import + +1. Open Database settings. +2. Tap "Import database". +3. Alert will appear saying: "The chat will stop, you will import?". +4. File: choose file location and tap "Import". + Link: paste link (or scan QR code) and tap "Import". +5. Confirm to replace database. +6. Start chat automatically once imported. + +### Set or change passphrase + +1. Open Database settings. +2. Tap "Set passphrase" or "Change passphrase" (if it was set). +3. Choose - store passphrase on the device or enter it every time the app starts. + +### Remove / store passphrase from the device + +To remove: + +1. Open Database settings. +2. Tap "Remove passphrase". +3. Confirm to remove passphrase in alert. +4. Button is replaced with Store. + +To store: + +1. Open Database settings. +2. Tap "Store passphrase". +3. Enter current passphrase - it is verified. +4. Button is replaced with Remove. + +### Migrate database to / from another device + +#### User experience + +This function is the most important, and it should be available from the main section in settings, under "Use from desktop" (or under "Link from mobile" on desktop). + +On the receiving device it will be available via Database settings and also on the Onboarding screen, so users don't need to create a profile. + +The steps are: + +On the source device: +1. Tap "Migrate to another device". +2. The chat will stop showing "Stopping chat" to the user. +3. If passphrase was: + - not set: make user set it in a separate screen. + - set: make user verify it. +5. Show the screen to confirm the upload. +6. Upload progress (full screen circular progress showing the share, with the %s and total/uploaded size) will be shown. +7. Once upload is completed, show QR code (with option to copy link), instruct to tap "Migrate from another device" on the receiving device. + +On the receiving device: +2. Tap "Migrate from another device". +2. The chat will stop (if not from Onboarding) showing "Stopping chat" to the user. +4. Scan QR code (with option to paste link on desktop only). +5. Show similar download progress, but probably in reversed direction - design TBC. +6. Once download is completed, show "Replace the current database" (if not from Onboarding). +7. Once imported, start chat automatically, and once chat started show "Tap remove database on source device". + +On the source device: +1. Tap "Remove database" on the showing screen (this should also remove uploaded file). + +#### Implementation considerations + +The latest updates allow uploading and downloading XFTP files without messages. + +So to perform the above, the second instance of the chat controller will be required, that probably requires supporting additional/optional chat controller parameter in the APIs that are required for that process. diff --git a/docs/rfcs/2024-02-13-inactive-group-members-2.md b/docs/rfcs/2024-02-13-inactive-group-members-2.md new file mode 100644 index 0000000000..6f7fc2f377 --- /dev/null +++ b/docs/rfcs/2024-02-13-inactive-group-members-2.md @@ -0,0 +1,38 @@ +# Inactive group members (simplified) + +[Original doc](./2023-11-21-inactive-group-members.md) + +## Problem + +Groups traffic is higher than necessary due to sending messages to inactive group members. + +## Solution + +### Improve connection deletion + +- When leaving or deleting group, batch db operations to optimize performance. +- In agent - fix race where connection can be deleted while it has remaining pending messages. + - Current agent logic is to immediately delete connection if it has no rcv queues left. + - Simplest should be to make a smart version of `deleteConn` for this improvement, checking `snd_messages` table for remaining messages, and keep connection around in case there are. + - While this may improve delivery of group leave and delete messages, it may as well have undesirable side effects for other use cases, as any pending messages will be sent prior to deleting connection. For example, user sends several messages on bad network, decides to delete contact, messages are still delivered when user is on good network before deletion, even though this contradicts user's intent and messages hadn't left user's device at the time of deletion. Considering this race when it happens is identical to simply leaving groups by deleting app, or deleting user profile only locally, it may be a bad idea to affect regular contact deletion for this use case. + +### Track member inactivity + +- Mark members as inactive on QUOTA errors, reset as active on QCONT + - track `group_members.inactive` flag per group member + - on SMP.QUOTA error agent to notify client with ERR CONN QUOTA (new ConnectionErrorType QUOTA) + - on receiving QCONT agent to notify client (new event) + - apart from QCONT, reset on any message or receipt +- Don't send to member if inactive + - don't send only content messages (x.msg.new, etc.) and always send messages altering group state? + - or don't send any messages? +- Track number of skipped messages per member and first skipped message + - count `group_members.skipped_msg_cnt` + - only count messages of same types/criteria that are included into history + - track `group_members.skipped_first_shared_msg_id` (only content or including service messages?) +- Send XGrpMsgSkipped before next message + - check `skipped_msg_cnt` > 0 and `skipped_first_shared_msg_id` is not null to only send once, reset after sending + +```haskell +XGrpMsgSkipped :: SharedMsgId -> Int64 -> ChatMsgEvent 'Json -- from, count +``` diff --git a/docs/rfcs/2024-02-19-settings.md b/docs/rfcs/2024-02-19-settings.md new file mode 100644 index 0000000000..002e381ce2 --- /dev/null +++ b/docs/rfcs/2024-02-19-settings.md @@ -0,0 +1,60 @@ +# Migrating app settings to another device + +## Problem + +This is related to simplified database migration UX in the [previous RFC](./2024-02-12-database-migration.md). + +Currently, when database is imported after the onboarding is complete, users can configure the app prior to the import. + +Some of the settings are particularly important for privacy and security: +- SOCKS proxy settings +- Automatic image etc. downloads +- Link previews + +With the new UX, the chat will start automatically, without giving users a chance to configure the app. That means that we have to migrate settings to a new device as well, as part of the archive. + +## Solution + +There are several possible approaches: +- put settings to the database via the API +- save settings as some file with cross-platform format (e.g. JSON or YAML or properties used on desktop). + +The second approach seems much simpler than maintaining the settings in the database. + +If we save a file, then there are two options: +- native apps maintain cross-platform schemas for this file, support any JSON and parse it in a safe way (so that even invalid or incorrect JSON - e.g., array instead of object - or invalid types in some properties do not cause the failure of properties that are correct). +- this schema and type will be maintained in the core library, that will be responsible for storing and reading the settings and passing to native UI as correct record of a given type. + +The downside of the second approach is that addition of any property that needs to be migrated will have to be done on any change in either of the platforms. The downside of the first approach is that neither app platform will be self-sufficient any more, and not only iOS/Android would have to take into account code, but also each other code. + +If we go with the second approach, there will be these types: + +```haskell +data AppSettings = AppSettings + { networkConfig :: NetworkConfig, -- existing type in Haskell and all UIs + privacyConfig :: PrivacyConfig -- new type, etc. + -- ... additional properties after the initial release should be added as Maybe, as all extensions + } + +data ArchiveConfig = ArchiveConfig + { -- existing properties + archivePath :: FilePath, + disableCompression :: Maybe Bool, + parentTempDirectory :: Maybe FilePath, + -- new property + appSettings :: AppSettings + -- for export, these settings will contain the settings passed from the UI and will be saved to JSON file as simplex_v1_settings.json in the archive + -- for import, these settings will contain the defaults that will be used if some property or subproperty is missing in JSON + } + +-- importArchive :: ChatMonad m => ArchiveConfig -> m [ArchiveError] -- current type +importArchive :: ChatMonad m => ArchiveConfig -> m ArchiveImportResult -- new type + +-- | CRArchiveImported {archiveErrors :: [ArchiveError]} -- current type + | CRArchiveImported {importResult :: ArchiveImportResult} -- new type + +data ArchiveImportResult = ArchiveImportResult + { archiveErrors :: [ArchiveError], + appSettings :: Maybe AppSettings + } +``` \ No newline at end of file diff --git a/docs/rfcs/2024-02-28-pq-integration.md b/docs/rfcs/2024-02-28-pq-integration.md new file mode 100644 index 0000000000..010f8105b7 --- /dev/null +++ b/docs/rfcs/2024-02-28-pq-integration.md @@ -0,0 +1,98 @@ +# PQ integration in chat + +## Problem + +- Group size not known when joining +- Communicate intent and current state of each conversation + +## Solution + +### Group size not known when joining + +- Add to XGrpInv GroupInvitation + - pros: easy + - cons: size can change before joining, but can ignore as it's still a good estimate + +or + +- Send before introductions + - new protocol message + - XGrpIntro :: GrpIntro -> ChatMsgEvent 'Json -- (GrpIntro is a box type with Int, for possible extension) + - or put into XGrpInfo + - XGrpInfo :: GroupProfile -> GroupStats -> ChatMsgEvent 'Json -- GroupData? + - can update profile between invitation if it happened before joining + - can later add logic to "verify" stats? + - may be over-complicated until since there "supposed" use cases are out-of-scope / not planned / not known + +- What should be default if it's not known? (e.g. admin has older version) + - On -> then off when member count reaches 20? + +### Communicate intent and current state of each conversation + +- Current state items + - RCEPQEnabled (see #3845) both for direct conversation and per member (regular event items, merged in UI) + - created when PQ changes for contact/member (e.g. received from agent on MsgMeta / SENT) + - experimental toggle is planned: it doesn't affect contacts/members with already enabled PQ + - contact enabled PQ always overrides toggle (can't downgrade) + - member enabled PQ also overrides, but can downgrade if group size increases past 20 + +- New items communicating state of e2e encryption in conversation + - should be well pronounced in UI, not merged + - should always say that conversation is e2e encrypted + - in direct chats: + - reflect actual state of PQ at the time of creation + - created during connection handshake when receiving first info about PQ in MsgMeta / some other event (TBC agent api) + - will not update if state changes (e.g. upgrades), as toggle is planned to be removed, PQ can't be downgraded, all will support soon + - flag in contacts table "e2e_info_created" to only create it once? + - should create for legacy contacts or not? + - in groups: + - reflect intent (should say "PQ will be used for members who support") based on number of members (see above) + toggle + - created at the same time as feature items? race with history may be possible, but we don't observe it? need to double check or ignore + - if based on XGrpInv GroupInvitation (first option above), can create item even before joining + - also will not update (as conversation progresses and it will scroll far up anyway) even if group size changes and it's disabled + - flag in groups table "e2e_info_created" to only create it once? and state is only reflected by RCEPQEnabled items? + - or create new such item if group size increases and PQ is off / decreases and PQ is on? + - "large group" thresholds have to different for group size increasing (e.g. 20) and decreases (e.g. 15), to avoid constant switching on the border. + +- Example texts for "e2e encryption info" chat items: + - for direct conversations: + - with PQ (and also forward a couple releases when more clients have upgraded): + ``` + Messages in this conversation are end-to-end encrypted. + Post-quantum encryption is enabled. + ``` + - no PQ (experimental toggle disabled): + ``` + -//- (e2ee) + Post-quantum encryption is not enabled. [Also possibly:] Enabling post-quantum encryption in experimental settings will enable it in this conversation if your contact supports it. + ``` + - no PQ (experimental toggle enabled): + ``` + -//- + Post-quantum encryption will be enabled when your contact upgrades. + ``` + "upgrades" / "supports it" / "starts to support it" + - can be of different color, but seems unnecessary + - created once at the start of conversation + - created once for old contacts when PQ is enabled? + - for groups: + - with PQ (small group; toggle enabled or later, as above): + ``` + -//- + Post-quantum encryption will be enabled for members who support it. + ``` + can remove qualification later when most clients have upgraded + - no PQ (large group): + ``` + -//- + Post-quantum encryption is not enabled (group is too large). + ``` + - created each time group changes between small/large, or once? + - created for old groups when experimental toggle is first turned on, and first message is received? + + +- Save PQ encryption on chat items (messages)? + - in meta for direct + group rcv + - in group_snd_item_statuses for group snd? + - display in chat item details (info) + - may be overkill if aggressive upgrade strategy is planned diff --git a/docs/rfcs/2024-03-14-super-peers.md b/docs/rfcs/2024-03-14-super-peers.md new file mode 100644 index 0000000000..9996c02490 --- /dev/null +++ b/docs/rfcs/2024-03-14-super-peers.md @@ -0,0 +1,72 @@ +# Large public grups / channels + +## Background + +SimpleX Chat users participate in public groups that were created for small, fully connected p2p groups - working groups, teams, etc. The ability to join the groups via the links was added as an afterthought, without forward looking design, simply to accomodate the interest from the users to use SimpleX platform for public groups and communities. Overall, it's correct to say that the emergence of public groups was unexpected in the context of private messaging, and it shows that protecting participants and publishers identity, and having per-group identity is important for many people. + +## Problems of the current p2p design + +### It doesn't scale to large size + +Current design assumes that each peer is connected to each peer and sends messages to all. It creates non-trivial cost of establishing the connections as the group grows, some abandoned connections when some members remain in "connecting" state and also linearly growing traffic to send each message. + +Historically, there were p2p designs when peers connected not to all but some members, but they were mostly used by desktop clients with more persistent network connections, did not provide any asynchronous delivery and were trading lower traffic for latency and availability. Such designs were viable for file sharing across desktop devices, but it probably would not work well for dynamic real-time communities with active participation from a small share of members and the design of other members to observe the conversation as it happens. + +### It doesn't account for participation asymmetry + +Most members of large public groups do not send messages, so connecting them to all other members directly appears unnecessary and costly, and it also requires tracking when members became inactive to stop sending messages to them. + +## Objectives for the new design + +### Transcript integrity + +This issue is covered in detail in [Group integrity](./2023-10-20-group-integrity.md). + +### Asynchronous delivery to group + +Asynchronous delivery is important to protect participants privacy from traffic observation. In addition to that, sending scheduled posts is quite often a convenient feature to schedule multiple updates for a longer period of time - for example, schedule daily updates for a week, doing it once a week. + +### Ability to conceal members list + +This is a rather common request for the current groups, and while it could be possible of course to hide it on the client level, this still makes data available via the database, and puts less technical users in unfair position, while not protecting users privacy from technically competent members. + +### Support pre-moderation + +As the group size grows, so does the activity of the bad actors. Some groups will benefit from switching all, some, most, or new members to pre-moderation - when each post needs to be approved by admin before it becomes visible to all members. It would slow down the conversations, but it would allow a better content quality and owners' control of the content. + +## Channels based on super-peers + +The proposal is to model the new UX design from Telegram channels, with optional subchannels, and granular participation rights for the members / followers. + +Another consideration is to create democratically governed communities when creators don't own the community but only appoint the initial administrators, but as the community grows it can elect the new admins or moderators from the existing members, where voting power is somehow determined by the community score (which is necessary to compensate for anonymous participants who could subvert the vote if plain vote count was made). This is probably out of scope for the initial implementation, but this idea is very appealing and it doesn't exist in any other decentralised platforms. + +Technologically, the channel or group would determine which super-peers would host the group or channel, with group content being a merkle tree with ability to remove some content creating holes - which seems to be very important quality, both to remove undesirable content and to protect participants privacy. + +Super-peer would manage this merkle-tree state based on the messages from owners, admins and members, with the ability to make some destructive actions confirmed by more than one command. E.g., group/channel deletion may require at least 2 or 3 votes (respectively, for 3 and 5 owners), thus protecting both from accidental deletions and from attacks via owner - one of the owners being compromised won't result in group deletion if 2 votes are required. As the group size grows, owners can also modify rules (which in itself can also require m of n votes). + +## Joining group + +The current model when the link to join the group is, effectively, an address of one of the admins, is not censorship-resistant, reliable or convenient - the admin can be offline, be removed, etc. So we want to somehow include addresses of multiple super-peers to join the group. Without identity-layer, the addresses are quite large already, and including multiple addresses in one link, while possible, would make the qr code very hard to scan. Practically, without creating identity layer, we can use up to 2-3 super-peer addresses for the group, and increase it later. 2 addresses is likely to be satisfactory, as one of them could be super-peer hosted by SimpleX Chat, and another - by group owners. + +The client then will be connecting to all super-peers in the address. Once connected, these super-peers could send the addresses of the additional super-peers, but this is probably unnecessary for the initial release. + +## MVP + +The challenge is to decide what should be in scope for the initial release, to make it a valuable upgrade and a viable starting point, without overloading it with the functions that can be added later. + +MVP scope: + +- Core functioning of the group - creation/deletion/choosing and changing super-peers. While a large scope, it appears essential. +- Message delivery via super-peers. +- Super-peer protocol extension. Most likely super-peer would receive ordinary chat messages, but some operations should be added and require additional protocol messages - adding/removing super-peers to groups. +- Protocol extensions for owner actions with approvals - we already had several accidental deletions or lost owner accounts. Possibly, it is out of MVP scope. +- Search and history navigation. Current decision to send 100 messages both creates unnecessary traffic spikes, and also doesn't provide access to older history and search functions. But, possibly, it should also be in follow up improvements, and only should be included as the initial protocol design. +- New format of the group address to include more than one super-peer. +- Granular permissions and management model. While the user interface can evolve, the protocol and the scenarios, and also rules models seems better to be added from the beginning. + +Follow-up / improvements: +- More scalable client - we already observe scalability issues with directory service, so replacing SQLite with Postgres, if the group participation starts growing seems very important. + +Out of scope: +- additional super-peers. +- smart-contacts. While very tempting to generalise permissions and management model via smart contracts, that would radically increase complexity and delivery time. diff --git a/docs/rfcs/2024-03-22-communicating-reject.md b/docs/rfcs/2024-03-22-communicating-reject.md new file mode 100644 index 0000000000..37bb4eab26 --- /dev/null +++ b/docs/rfcs/2024-03-22-communicating-reject.md @@ -0,0 +1,95 @@ +# Communicating rejection + +## Problem + +Many interactions support either approval, or silent rejection. From privacy perspective, rejection being silent is a correct default. However, for improved usability we could add optional communication of rejection, as many users don't mind sending such signals to their contacts. + +Features currently not supporting communicating rejection: +- Rejecting contact request +- Rejecting group join request +- Rejecting call +- Rejecting group invitation +- TBC Other? + +## Solution + +## Rejection of contact / group join requests + +- Contact and group join requests are very similar between each other and different from other features as they both use mechanism of "contact connections". + +- Rejection can be made to the address proposed by requester in AgentInvitation message, where currently AgentConfirmation is sent in case of acceptance. + + ``` haskell + data AgentMsgEnvelope + = ... + | AgentRejection + { e2eEncryption :: RcvE2ERatchetParams 'C.X448, + encRejectionInfo :: ByteString + } + ``` + +- Unlike other AgentMsgEnvelope constructors, AgentRejection doesn't require agentVersion since connection will be deleted after sending this message. + + - We may be able to re-use recently added mechanism of marking connection for deletion with deleted_at_wait_delivery field without much additional work. + +- e2eEncryption will be used to encrypt first message, same as for AgentConfirmation. (?) + +- Both sync and async versions of agent functions are required, as contact rejection will be user action, while group join rejection will be automated (in case, for example, if link host is no longer admin). + + - Group requests non-automatic approval is a separate matter and requires UI consideration, but if it was added it would use sync function. + + - For sync function either new API can be added, or rejectContact can be parameterized. + +- Chat protocol requires adding new messages to be sent in encRejectionInfo, to be processed on requester side based on connections' semantics. + + ```haskell + -- / contact request rejection + XReject :: ChatMsgEvent 'Json + -- or + -- (contact can send reason for rejection; we don't even have welcome messages in XContact though) + XReject :: Maybe Text -> ChatMsgEvent 'Json + + -- / group join request rejection + XGrpReject :: Maybe GrpRejectReason -> ChatMsgEvent 'Json + + data GrpRejectReason + = GRRNone -- manual reject with no reason? use Nothing in GRRText instead? + | GRRCantInvite -- e.g., no longer admin + | GRRText {text :: Text} -- manual reject once supported? Maybe Text? + + -- add similar type for contact request rejection? + -- minimal is to always send XReject without reasons + ``` + + - As a side note, it may have been a design mistake to mix both "connInfo" messages as well as regular chat messages in a single type for protocol messages, so it may be best to keep these as separate constructors. + +- Versioning considerations: + - Increase chat version. + - We already save peer chat version on contact_requests on initial REQ message. It can be used to differentiate UI whether contact supports rejection messages and not offer option to reject with notification. + - Increase agent version? Agent can prohibit sending AgentRejection based on version in AgentInvitation, though it shouldn't be reachable as chat should also prohibit it. + +## Rejecting calls + +- New chat protocol message is enough + - Based on user action in reply to XCallInv instead of XCallOffer. + - Send in APIRejectCall. + + ```haskell + XCallReject :: CallId -> ChatMsgEvent 'Json + ``` + +- Same chat versioning considerations as above. + +## Rejecting group invitation + +- Same, new protocol message. + - Based on user action in reply to XGrpInv instead of XGrpAcpt. + - Can't be sent as simple chat message since there's no group ID in invitation? (only optional groupLinkId) + - So, have to send as "conn info" via join, same as for XGrpAcpt. + - APIDeleteChat is already used for deleting group invitations, can re-use. In this case `notify` parameter in APIDeleteChat can be used to send rejection. + + ```haskell + XGrpReject :: CallId -> ChatMsgEvent 'Json + ``` + +- Same chat versioning considerations as above. diff --git a/docs/rfcs/2024-04-01-super-peers-2.md b/docs/rfcs/2024-04-01-super-peers-2.md new file mode 100644 index 0000000000..1ebd72bc41 --- /dev/null +++ b/docs/rfcs/2024-04-01-super-peers-2.md @@ -0,0 +1,262 @@ +# Large public grups / channels + +This document describes specific design elements for the MVP of [the groups based on super-peers](./2024-03-14-super-peers.md). + +## Super-peer members + +There are two possible design approaches for super-peer members: + +1. Separate non-participating members that can only be added as a super-peers and do not have their own roles in the group, can't send their own messages or administer the group. + +2. Super-peer being a function of any member, irrespective of their role. + +While approach 1 can be simpler, it has its downsides: +- more complex migration of the existing groups - e.g., directory service cannot become a super-peer while remaining group admin (or moderator, if we add a new role). +- less usable clients - users can have desktop clients with good internet connectivity and sufficient computing resources to effectively host several medium size groups, even with SQLite database. +- it makes super-peers more like servers, being both unusable as clients and also requiring larger concurrency, and therefore increasing centralization. + +Therefore, the approach 2 looks more attractive, when super-peers must have some role in the group, and the super peers that cannot send messages will have observer role. + +As a side note, it also implies that it is beneficial to see the permission to moderate messages not as a role somewhere between Member and Admin, but as a separate privilege that admins and owners have by default for the messages from the members up to their role, but in general it's a separate member profile setting that allows to moderate messages up to a certain role. That approach would help automatic moderation as well, when moderators may be allowed to moderate messages of admins, without being able to remove members. + +Proposed protocol modifications: + +```haskell +-- this type is used in XGrpMemNew, XGrpMemIntro and XGrpMemFwd for member introductions +data MemberInfo = MemberInfo + { memberId :: MemberId, + memberRole :: GroupMemberRole, + rank :: Maybe Word8, -- new field, 0 for usual members, 1 for super-peers, allows to build additional distribution hierarhies if needed. + perms :: Maybe MemberPermissions, -- new field + v :: Maybe ChatVersionRange, + profile :: Profile + } + +-- this type is used in XGrpInv (in GroupInvitation) and XGrpLinkInv (in GroupLinkInvitation) +-- to invite members to the group +data MemberIdRole = MemberIdRole + { memberId :: MemberId, + memberRole :: GroupMemberRole, + rank :: Maybe Word8, -- new field + perms :: Maybe MemberPermissions -- new field + } + +-- new type +data MemberPermissions = MemberPermissions + { moderate :: MemberPermissionTarget -- could be extended to array in the future + } + +-- new type +data MemberPermissionTarget = MemberPermissionTarget + { maxRole :: Maybe GroupMemberRole -- if absent, can moderate all messages + } +``` + +For backwards compatibility, `admin` role implies `{moderate: {maxRole: admin}}` permission and `owner` - `{moderate: {maxRole: owner}}`, which probably can be overridden with `{moderate: {maxRole: observer}}`. + +It is also proposed to migrate `memberId` and `memberRole` to `id` and `role` in the parser (in a forward/backward compatible way), without changing serializers. + +## Group routing mode + +Irrespective of the presense of super peers in the group, the group itself has to be switched to super-peers routing at some point via a group profile update. + +```haskell +data GroupProfile = GroupProfile + { displayName :: GroupName, + fullName :: Text, + description :: Maybe Text, + image :: Maybe ImageData, + groupPreferences :: Maybe GroupPreferences, + rank :: Maybe Int, -- new field, 0 for flat groups, 1 for groups with super peers + redundancy :: Maybe GroupRedundancy, -- new field, only used when rank > 0 + consensus :: Maybe GroupConsensus -- new field, see below in + } + +-- each field defines an average number of super-peers that will deliver messages (events) related to a specific scope, +-- by default all super peers will deliver the events in that scope. +data GroupRedundancy = GroupRedundancy + { messages :: Maybe Double, -- messages and message changes, including reactions and comments + members :: Maybe Double, -- member additions and permission changes + group :: Maybe Double -- group profile and other changes + } +``` + +## Decisions about message or another event delivery + +In groups with rank 0 (current groups) all members aim to establish connections with all other members, making it hard to scale. Admins who added the members play a temporary role of forwarding messages, but only until members are connected. + +In groups with rank 1 super-peers connect to all members, but each message or event may be delivered by some rather than by all super-peers, to avoid substantial traffic increase. E.g., in groups with 2 super-peers it would be desirable to deliver each message 1.33 (4/3) times on average, while for groups with 3 super peers it can be 2 or 1.667 (5/3) times. + +The decision whether a given super peer should deliver the message would depend on these factors: +- deterministic (see below) message hash, `h`. +- receiving member ID, `r`. +- super-peer member ID, `s`. +- target message redundancy `d` (the desired number of super-peers to deliver the message). +- number of super-peers connected to member `r` (and known as connected to super-peer making the decision), `n`. +- 0-based index of the current super-peer in the sorted array of known super peer IDs, `i`. It does not require sorting all peer IDs, it's enough to count how many peers have smaller or larger IDs. + +The assumption here is that member ID is unique within the group, and that message hashes are also unique. Also, message hashes rather than sending member IDs are used to ensure that the decision is made differently for the same sender/recipient pairs, allowing recipients to identify integrity violations (e.g., if some of the super-peers decides to change the messages or fail to deliver it). + +For simplicity, all parameters are normalized to 0..1 range from their respective binary ranges. + +The algorithm to decide whether the message should be delivered by a given super-peer: + +``` +if (d >= n || n == 1) deliver; +else + prob = d/n ; delivery probability based on target delivery redundancy + point = (h + r)/2 ; as member ID and message hash are uniformly random in 0..1 range, `point` will also be uniformly random in 0..1 range + start = (1 - prob) * i / (n - 1) ; `start` for the peer with `i == 0` will be `0` and with `i == n - 1` will be `1 - prob` + end = start + prob ; `end` for peer 0 will be `prob` and for peer `n - 1` will be 1 + ; for all peers the range `start..end` will have width `prob` + if (point >= start && point < end) deliver + else skip; +``` + +This algorithm can be proven to result in target average delivery redundancy. + +It also requires all super peers to inform other super peers about: +- establishing or losing the connection with other members (e.g., when AUTH is received). +- informing about their decision to stop being super peers in advance. +- most likely using delivery receipts in communications between super-peers that would include message hashes in RCVD info. + +## Authorising administrative changes + +As members no longer send messages directly to other members, in addition to the risk of the initial MITM by admin (which is now mitigated by having multiple super-peers) there is a risk of super-peers being compromised at a later stage, and in case of administrative changes (member or group level changes) we have these options: + +1. have all members postpone these changes until they are communicated by a sufficient number of super-peers (that would require configuring consensus on the group level). +2. have admins and owners sign administrative changes with the public key included in their profile during member introduction. + +The downside of approach 1 is that administrative actions will be delayed and have to be executed not at the time the message is delivered, but at the time message is confirmed by other super-peers. That is separate and in addition to member consensus for some changes (see below). That also means that groups with one super-peer will have no defence mechanism against super-peer being compromised. That also means that groups with two super-peers will either also have no such defence (in case required consensus level is 1) or will require that both super-peers are available, and no administrative actions will be executed unless both super-peers are available. + +The downside of approach 2 is the lack of repudiation, in fact, there is a non-repudiation quality of such administrative changes as role changes, member additions and deletions, and group changes. + +Overall, while repudiation of sent messages appears as important, it seems much less important for administrative changes, and signing member and group changes while relying on Merkle DAG for message history integrity appears to be an optimal tradeoff. + +To support this functionality the `admin` and `owner` members have to add public keys to their profiles and communicate profile updates to all members (before or after group is switched to super-peers, but probably before is better). + +The middle ground here is moderation, and trade-off here is more nuanced. Practically, whether message is fully removed or marked as removed is the group policy decision, and it can be made based on whether it's more important to preserve content or to remove undesirable content without trace, and also given that super-peers are likely to be give automatic moderation capabilities anyway, preserving deniability for moderation events and relying on Merkle DAG to identify integrity violation seems a better alternative than signing moderation messages. Although it can also be a part of group policy whether to require signing moderation events. + +The change to member profile will be: + +```haskell +data Profile = Profile + { displayName :: ContactName, + fullName :: Text, + image :: Maybe ImageData, + contactLink :: Maybe ConnReqContact, + preferences :: Maybe Preferences, + authKey :: Maybe AuthKey -- new field + } + +-- this is rather ad hoc and tries to allow two things: +-- - verify that the member has the private key. +-- - allow key rotations on profile changes, without signing the whole profile change. +-- A better option could be to use certificates, although they are much large in size, +-- and also to simply sign profile changes that include key change (then prevKeySignature won't be needed) +data AuthKey = AuthKey + { key :: PublicKeyEd25519, + signature :: SignatureEd25519, -- signature of the key itself + prevKeySignature :: Maybe SignatureEd25519 + } +``` + +## Role, rank and moderation permission changes of members and group profile changes + +There are two problems with the current approach to changing permission, when a single member makes this decision: +- member whose role changes may disagree. E.g., the member may not be willing to have owner or admin role in some groups. Even more so, the member may be unable to perform super-peer functions (have rank 1). +- other owners or admins may disagree with the change or it may have been made by mistake. + +With the move to super-peers it is additionally complicated by the fact that super-peers will be forwarding these messages, and they could be compromised, thus disrupting group functioning - this risk currently exists with the existing group directory that plays admin role and in case it is compromised it can remove all members from the group. + +Part of this problem is addressed in the previous point by requiring to sign administrative changes. + +Another part is about the members agreeing to changes that affect them when additional privileges are granted, and also by requiring the consensus between group owners and admins for any privilege changes. + +There are 2 options for proposals/acceptance/approvals flow design: +1. introduce additional protocol messages for each stage, and for each type of change to complement existing messages. +2. manage these stages in orthogonal way, by adding these stages on the top level of the protocol. + +The option 2 is likely to result in a more concise protocol as it will separate approval from from the events, also allowing to include authorizations in a standardized way. This also fits well in `XGrpMsgForward` event that includes that message inside, so all authorizations will be forwarded. + +```haskell +-- fields are the number of member approvals required for actions with that role. +-- As target member acceptance is required only for privilege increases it won't be among approvals required for consensus. +-- For example, for group with consensus = {admin: 3, owners: 2}, a new member can be made admin with the decision of 3 other admins or with the decision of single owner (as owner role is higher), or the group can be removed, a new admin is added or group consensus changed with the decision of 2 admins. +-- Owners leaving without changing the consensus can result in consensus becoming unreachable - this is not different from losing a single member, and it only prevents accidental or malicious destructive actions, but does not prevent losing access. +data GroupConsensus = GroupConsensus + { admin :: Maybe Word16, -- possibly, this field is not needed + owner :: Maybe Word16 -- 1 by default + } + +-- alternatively, the type could be more flexible than that, but it can be extended if needed. + +data ChatMessage e = ChatMessage + { chatVRange :: VersionRangeChat, + msgId :: Maybe SharedMsgId, + chatMsgEvent :: ChatMsgEvent e, + stage :: Maybe MessageStage, -- new field, Nothing for broadcasted messages + bcast :: Maybe MessageBroadcast, -- new field instructing super-peers how to broadcast the message + auth :: Maybe (Either MemberApproval [MemberApproval]) -- approvals for broadcasted messages from members, Either won't be in encoding + } + +data MessageStage = MSProposed | MSApproved + +-- possibly, this could include approval stage but it is implied by the context, message needs to be approved by: +-- - the sender with the sufficient permissions +-- - the target of the change when privileges are granted +-- - other admins or owners as required by `consensus` property in group profile. +data MemberApproval = MemberApproval + { memberId :: MemberId, -- can be encoded as id? + auth :: SignatureEd25519 + } + +data MessageBroadcast = MessageBroadcast + { from :: Maybe MessageFrom, -- MFSender by default + auth :: Maybe Bool, -- whether to keep auth if present, True by default, False means to validate and remove auth + schedule :: Maybe MessageSchedule + } + +data MessageFrom = MFSender | MFApprovers + +data MessageSchedule = MessageSchedule + { deliverAt :: Maybe UTCTime, -- when received by super-peer by default + minDelay :: Maybe Int, -- seconds, added to start, 0 by default + maxDelay :: Maybe Int -- seconds, added to start, 0 by default + } + +-- e.g., to deliver at least 2 hours after received by super-peer, the MessageSchedule would be {minDelay :: 7200} +-- or, to deliver all messages 20-40 seconds after it is received, to complicate traffic correlation it would be {minDelay: 20, maxDelay: 40} +-- or, to deliver at a scheduled time {deliverAt: "2024-04-01T00:00:00Z"} +``` + +The signature is computed over deterministic message hash excluding `auth` property. This structure would also allow asking super-peers to forward the message as originating from multiple owners or admins without showing who originated the message, as long as necessary approvals are present. + +That will require extending `XGrpMsgForward` to support array of MemberId and allow UI to show multiple senders of the message: + +```haskell + | XGrpMsgForward :: [MemberId] -> ChatMessage 'Json -> UTCTime -> ChatMsgEvent 'Json +``` + +The messages send from multiple owners can have the benefit of avoiding targeted attacks on the member who originated it - it will only be known to other members sending the message, but not to other members, creating mutual responsibility for some changes and/or announcements. + +## Choosing MVP scope + +With all these ideas for improvements, the challenge is to choose some valuable MVP shippable in the shortest time. + +From the UX point of view, the most lacking parts are: + +- broadcasting messages via super-peers, including: + - protocol to accept and approve role changes is necessary, as it should not be possible to unilaterally appoint some member to be a super-peer. + - computing deterministic message hashes and signatures (and only member with the key in profile can be made super-peer). + - protocol to communicate connections with members between super-peers, yet TBD. + - algorithm to make decision whether to deliver message. +- an equivalent of Telegram channels - groups where members can only send comments and set reactions, but cannot send messages. Protocol extensions yet TBD. + +Optionally, we could include: +- Merkle tree integrity validation. +- Owners/admins consensus. +- Messages from multiple members (don't have to be owners, can be any members, irrespective of the need for consensus). +- Delayed messages. + +None of this optional list is required to launch. diff --git a/docs/rfcs/2024-04-16-ip-address-protection.md b/docs/rfcs/2024-04-16-ip-address-protection.md new file mode 100644 index 0000000000..0019ad87ca --- /dev/null +++ b/docs/rfcs/2024-04-16-ip-address-protection.md @@ -0,0 +1,81 @@ +# IP address protection, support for SMP sending proxies + +## Problem + +IP addresses of senders being visible to recipients' chosen servers, which in case of self-hosting makes it visible to recipients themselves. In case of XFTP files the issue is reversed, with IP addresses of recipients being visible to senders' chosen servers. + +## Solution + +### SMP + +- Agent to support sending proxies +- New network settings configuration "Use SMP proxies" + - Can be set to "always", "never", "for unknown servers" + - Currently configured servers to be considered as "known", including those disabled for new connections + - Initial default is "never" to allow opt-in for testing, to be changed to "for unknown servers" later + - Support in UI +- No alerts about unknown servers are required in UI +- Tor setting to not affect agent decision to use SMP proxy for each given server + +``` haskell +data UseSMPProxies + = SMPPAlways + | SMPPNever + | SMPPUnknown + deriving (Eq, Show) + +data NetworkConfig = NetworkConfig + { ... + useSMPProxies :: UseSMPProxies, + ... + } +``` + +### XFTP + +Some considerations: + +- XFTP proxying is not planned to be implemented initially + - In future it could be done via open socket and with no persistance, and would be required only for FGET command +- Currently XFTP files are automatically set for reception in some cases: + - Images and voice messages + - In background (via "set to receive") +- Agent resumes file reception (download) after app re-start, in case they weren't fully downloaded +- User may change tor setting between and during app sessions, so a file can be "accepted" when app is connected via tor, but resumed when it's no longer + +Solution: + +- Client would make a decision whether to automatically accept file, or alert user about unknown servers +- Add only_via_tor flag to agent rcv_files + pass through APIs: + - add Bool to `ReceiveFile` (with False meaning user hasn't indicated any intent regarding unknown servers) + - can either be automatically accepted, or require alerting user and asking for confirmation (see below) + - add `onlyViaTor` to `xftpReceiveFile` +- Possible scenarios: + 1. XFTP file chunks are fully available on known servers + - Do not show alert, accept automatically (images, voice messages) or without approval (other files) + - `onlyViaTor` is False + 2. Some file chunks are on unknown servers, tor is not enabled + - User accepts manually, show alert to user + - User confirms, indicating it's Ok to proceed even though IP would be visible to unknown servers + - `onlyViaTor` is False + - \* Additional user preference to never ask? -> behavior same as now + 3. Some file chunks are on unknown servers, tor is enabled + - Do not show alert, accept automatically (images, voice messages) or without approval (other files) + - `onlyViaTor` is True +- On file download: + - `onlyViaTor` equal False is ignored + - If `onlyViaTor` is True, and tor is not enabled, throw new "permanent" error + - Permanent for simplicity as this is an edge case + - File record to be removed from agent + - Could analyze whether it's remaining (not yet downloaded) chunks are on unknown servers, and only abort in this case (and instead proceed if remaining chunks are on known servers) + - This requires changing download to load data for all chunks instead of loading only current chunk + - This is another edge case of edge case, as it seems more likely that sender either used only self-hosted servers, or only preset servers + - So it seems as unnecessary complication, and it should be Ok to abort simply based on only_via_tor flag + tor setting + - Error is RFERR XFTP UNKNOWN_NO_PROXY (new constructor) +- Chat to differentiate RFERR, and make file available for re-download on UNKNOWN_NO_PROXY error, similar to when file is cancelled + - New CIFileStatus - CIFSRcvCancelledNoProxy, to differentiate in UI + - Different icon for retry, alert explaining why file was aborted + +### Trusted servers + +We also considered an idea of trusted servers, but it proved to have unnecessarily complex UX, and in case of XFTP had issues on file download continuation after restart (e.g. server "trusted" flag changing, automatically accepting with tor enabled). It requires additional consideration and may be better suited to concept of "server providers". diff --git a/docs/rfcs/2024-04-26-commercial-model.md b/docs/rfcs/2024-04-26-commercial-model.md new file mode 100644 index 0000000000..841d800715 --- /dev/null +++ b/docs/rfcs/2024-04-26-commercial-model.md @@ -0,0 +1,68 @@ +# Commercial model for SimpleX communication network + +## Problem + +SimpleX two-tier network design provides a _potential_ for a much higher degree of decentralization and privacy than a p2p network can achieve even theoretically. This is a very strong statement, and its formal proof is out of scope of this document, please see the old comparison of SimpleX network and p2p network designs [here](../SIMPLEX.md#comparison-with-p2p-messaging-protocols). + +The main downside of most, if not all, p2p networks is the lack of (or very limited) asynchronous message delivery that is critically important both for the network usability and for privacy and protection against traffic correlation attacks. + +But while two-tier network design has a _potential_ for higher privacy, this potential is hard to realize as it does not have an in-built mechanism for network operator incentives. All SimpleX network relays preset in the app are operated by SimpleX Chat Ltd., and while there are probably over 1000 self-hosted and community-hosted messaging relays - not having a single register of such relays is important for true decentralization, so we cannot know the exact number - it is probably a valid argument that a substantial part of SimpleX network traffic is provided by preset relays. Again, while we don't know the exact share, it is probably not much less than 40-50% and probably not much more than 65-75% - which is a very large share in any case for a single entity. + +For SimpleX network to achieve the level of decentralization that is designed, the applications have to be able to offer multiple network operators via the app who have commercial incentives to operate these relays. + +Traditional commercial models for consumer Internet products rely on some of these ideas: +1. Offer service for free, sell users' data. We obviously had enough of such Internet and can't wait to see it implode, lose all users, and become illegal. +2. Create a cryptocurrency or issue cryptocurrency tokens, sell them for real money, and use this money to fund service operation. While many people believe that this is the future of the Web, calling it Web3, we see it as a technological dead end, that although it provides a great platform for speculation and a playground to test such ideas as smart contracts and consensus algorithms, all of which can be used outside of the context of cryptocurrency blockchains, is not a sustainable technological foundation for a general purpose communication or information management system due to its inherent regulatory risks and distorted commercial incentives. Moxie Marlinspike wrote [a good critique of Web3](https://moxie.org/2022/01/07/web3-first-impressions.html). +3. Freemium models where some users pay for the service and, effectively, sponsor the users of the free tier. This model is free from the downsides of the previous two options, and there are many commercial services operated on this model, but this model is suboptimal for privacy in the worst case, and in the best case it still does not achieve decentralization, as whoever charges the money (the app provider) should also provide the infrastructure. + +In the mentioned critique of Web3 Moxie wrote: _We should accept the premise that people will not run their own servers by designing systems that can distribute trust without having to distribute infrastructure_. + +This statement is interesting, as it contains the correct premise - that most people do not want to and won't run their own servers - but it reaches an incorrect and limited conclusion, that the only way to provide value is by figuring out how to decentralize the trust without decentralizing infrastructure. I completely disagree that this conclusion is the only one possible, and the offered solution actually offers a way for extreme infrastructure decentralization - when not just users are distributed across providers, as happens with federated designs, but each conversation between two users is supported by infrastructure of 4-6 different independent operators - and also provides commercial incentives for these operators. + +## Solution design requirements + +So, we want to find a solution that will: +1. Offer extreme decentralization, without any kind of central authority (unlike Tor that has central authority, and unlike p2p networks that have a single addressing space) or any kind of centralized state (unlike cryptocurrency blockchains that have a centralized single state that the whole network should reach consensus about). +2. Offer low barrier for entry for infrastructure operators. +3. Offer extreme data and infrastructure portability, when infrastructure operators offer standardized primitives used by client applications - such as messaging queues and file chunks in SimpleX network. Migrating from one operator to another should not be just possible or simple, it should happen continuously and automatically, all the time, without any user action, when files and conversations move from one set of operators to another, similarly to how data moves around on SSD drives - to balance the load on the whole network, to provide reliability and redundancy, and to ensure that infrastructure operators have zero control of users and their data, and have very limited knowledge of users activity. This limited knowledge both achieves users' privacy and reduces the legal responsibility of infrastructure operators to the level of network operators. +4. Offer commercially profitable model for infrastructure operators. + +If these requirements are satisfied it would achieve a radical shift of control from infrastructure operators of today (that is achieved via SaaS model, when infrastructure operators are also software vendors, and software is provided as a service, which seems to be the root cause of corruption of Internet services - the process that Cory Doctorow refers to as [enshittification](https://www.youtube.com/watch?v=q118B_QdP2k)) to the software users, who use client software to directly consume low level infrastructure primitives of commoditized infrastructure operators that have zero control of how these primitives are used, other than pricing, that can be determined in a competitive process (e.g., via the real-time reverse auction). + +## Solution concept + +There are three types of participants in the network: +- software vendors (e.g., SimpleX Chat ltd.). +- infrastructure operators - commercial entities that have agreement with software vendors - they run server software provided by software vendors. +- users - they run client software. + +These roles can obviously overlap, but strategically it is better if they don't, e.g. we would benefit from not operating infrastructure, and users would also achieve better metadata privacy by not running the servers. + +To use SimpleX network client software needs to provision simple infrastructure resources - such as, create a messaging queue to receive the messages, create or use a session from a sending proxy to a particular destination relay, or to upload a file chunk that will be stored for a defined number of days. These resources are very cheap to provide, their price could be a very tiny fraction of a cent. + +For the users' client software to be able to provision these resources, the software vendor will issue infrastructure certificates to the users. Some number of them can be provided for free, a larger number can be sold for a monetary payment (can be in-app purchase, or cryptocurrency payment, or any other process). Software vendor will keep a private record of issued certificates. Technically, certificates can be usual cryptographic certificates that sign a public key presented by the client (while the client holds the private key). + +These certificates cannot be used as a cryptocurrency, as there is no public record of all issued certificates, and they can only be "spent" once with the infrastructure operator that has a commercial agreement with a software vendor. + +When the client wants to provision infrastructure resource it signs the request using the private key (a public counterpart of which was signed by the vendor's root certificate) and present this signed request together with the client's certificate to infrastructure operator. Operator validates the signature and certificate using vendor's root certificate and immediately provisions the resource. + +Operator then within a limited time presents its own signed request for payment to the software vendor - it would include the original request, without specifying which resource was provisioned, but only confirming that it was to this vendor. Software vendor can either confirm the acceptance and void the certificate or inform the vendor that this certificate was previously used (double spend), in which case the operator will stop provisioning the resource. + +Pros: +- this design decouples payments from resource allocation, providing better privacy, and decentralizes the infrastructure. +- this design achieves extreme provider portability and lack of control of user and user data. + +Cons: +- this design creates a strong dependence on software vendor's payment infrastructure availability - even though there can be many software vendors using this approach in a compatible way, it still creates a strong dependence of client software functioning on a single vendor. +- as described, this design allows software vendor to correlate payments of a given software user to specific infrastructure operators. + +Problem 2 can be solved by using some sort of [zero-knowledge proofs](https://en.wikipedia.org/wiki/Non-interactive_zero-knowledge_proof), where infrastructure operator can prove to software vendor that 1) they received a valid request 2) software vendor can also prove that the same certificate was not presented before, but cannot determine which certificate it was. + +Problem 1 can possibly be solved by delegating the right to issue a limited number of certificate to infrastructure operators, and making all records in the private blockchain accessible and writable by the operators using the agreements with the vendor that would also be visible on this chain, so that the operators cannot issue more certificates than agreed. In this case the payments will be made not by the software vendor to the operators but by the operators to the vendor, by compensating the buy/sell price difference. + +This is a concept of design, rather than the actual design, and the details of cryptographic primitives and consensus algorithms for this chain are out of scope. It is important that this design limits the number of operations with each certificate to three: +1. certificate issued to the user, based on the rules agreed with the software vendor. +2. certificate is used as a micro-payment for infrastructure resource (it cannot be transferred to any other user without risks of double spend). +3. certificate can only be voided by software vendor (or delegate who issued it), so it cannot be used directly as a payment. + +If we see cryptocurrency as similar to money, with similar regulations, this cryptographic primitive is close to gift cards, that have zero monetary value and can be only exchanged to a specific resource/service, thus avoiding the usual regulatory risks, and also avoiding speculative hype that could decouple the value of the certificates from the price of the infrastructure. diff --git a/docs/rfcs/2024-05-17-flexible-user-records.md b/docs/rfcs/2024-05-17-flexible-user-records.md new file mode 100644 index 0000000000..da56fe75b6 --- /dev/null +++ b/docs/rfcs/2024-05-17-flexible-user-records.md @@ -0,0 +1,158 @@ +# Flexible user records + +## Problem + +Currently user records work as rigid containers for conversations. New conversations can be created only for an active user. Users want to be able to select a user record for a new conversation right at the connection screen (i.e. when scanning QR code or pasting a link). A similar problem was previously solved regarding Incognito mode - initially it required changing a global setting, then it was moved as a toggle to the connection screen, then it was reworked to be offered after scanning the link. + +## Solution + +## UI + +Connection UI would offer to join connections as other users. + +Current options when joining: + +``` +- "Use current profile" +- "Use new incognito profile" +``` + +Will change to: + +If there're only 2 users: + +``` +- "Use current profile" +- "Use new incognito profile" +- "Use <user 2 name> profile" +``` + +If there's more than 2 users: + +``` +- "Use current profile" +- "Use new incognito profile" +- "Use other profile" (opens sheet with list of users) +``` + +Things to consider: + +- hidden users should be excluded from this selection +- choosing different user should make it active and open chat list for this user, then create pending connection there +- should connection plan api take into account all users? + +## Other ideas + +### Incognito chats in a separate user "profile" + +Having incognito conversations interleaved with "main profile" conversations is another point of confusion, as incognito profile is offered as an alternative to main profile, but conversations are still "attached" to it and inherit some of its settings (e.g. servers). We could unite all incognito chats under a new dummy "incognito" user profile. It would have a special representation in UI, not as a regular user profile, but as an incognito mode. It would allow customizing a specific theme, servers, preferences and other settings for all incognito chats. + +``` haskell +-- Types + +data User = User + { ... + incognitoUser :: Bool, + ... + } + +-- Controller + +APIConnect UserId IncognitoEnabled (Maybe AConnectionRequestUri) +-- -> changed to -> +APIConnect UserId (Maybe AConnectionRequestUri) +-- since conversation being incognito would be defined by user +``` + +Considerations / problems: + +- migration of existing incognito conversations is non trivial, as it requires migrating both agent and chat connection records to a new user record, in addition to migrating other chat entities. + - some programmatic one-time migration on chat start would be required, e.g.: + - create new user in agent; + - create new user in chat with incognito_user set to true; + - for each (non hidden, see below) user read chat list, update user_id for all chat entities: + - contacts, + - groups, + - group_members, + - contact_profiles, + - chat_items, etc. + - this new user servers would include all servers from users that had incognito conversations (?). + - this seems quite complex error-prone. +- it may be more pragmatic to not migrate old conversations to new user record, but instead filter them out in their respective user chat lists, and filter them in incognito user profile. + - in this case "legacy" incognito conversations would be marked by their user record (avatar/name inside chat list; note inside chat view saying that such and such settings are inherited from user x). + - we could still make a hack to apply same incognito user theme for "legacy" incognito conversations. +- on the other hand the second approach requires loading all chats for the incognito user (this may be related to "All chats view", see below). +- when creating an incognito conversation for a hidden user, it should still be attached to that user. + - or we could create an "incognito hidden user". +- considering complexities, this all seems quite a rabbit hole and may be not worth it.. +- MVP may be to do nothing for legacy incognito contacts and just explain it in app. A-la "New incognito conversations will appear here, previously created incognito conversations will stay attached to user profiles they were created in". + +### Forward messages between users + +Should be somewhat easy in backend: + +``` haskell +APIForwardChatItem {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemId :: ChatItemId, ttl :: Maybe Int} +-- -> changed to -> +APIForwardChatItem {toUserId :: UserId, toChatRef :: ChatRef, fromUserId :: UserId, fromChatRef :: ChatRef, chatItemId :: ChatItemId, ttl :: Maybe Int} +-- or include UserId into ChatRef +``` + +More complex in UI - requires "knowing" conversations for other / all users: +- either have all conversations for all users in model. +- or have other users expand in forward list, and request their chat lists at that point. + +### Per user network settings + +Requires changes in agent and in backend. + +In agent requires storing network settings in UserId to settings maps, similar to servers: + +``` haskell +data AgentClient = AgentClient + { ... + useNetworkConfig :: TVar (NetworkConfig, NetworkConfig), + -- -> changed to -> + useNetworkConfig :: TMap UserId (NetworkConfig, NetworkConfig), -- slow/fast per user + } +``` + +Chat APIs: + +``` haskell +APISetNetworkConfig NetworkConfig +APIGetNetworkConfig +-- -> changed to -> +APISetNetworkConfig UserId NetworkConfig +APIGetNetworkConfig UserId +``` + +### All chats in united list + +We could add a view where chats for all users could be viewed in a single list / filtered by users. + +We could: +- either always load all chats for all users (see Incognito user, Forward between users above) and have a single api, then filter conversations by user in UI +- or modify/duplicate APIGetChats api and queries. +- in any case may require some rework of pagination queries, as indexes might become inefficient. + +``` haskell +APIGetChats {userId :: UserId, pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery} +-- -> changed to -> +APIGetChats {pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery} +-- or +APIGetChats {userId :: Maybe UserId, pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery} +-- with Nothing meaning all +-- or +APIGetChats {userIds :: [UserId], pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery} +-- to filter for multiple users? or only filter in UI? +``` + +### Move chats between user profiles + +This would further deepen the illusion of user record being a conversation tag rather than a rigid container for conversations. + +There are some of the same issues as described in migration of incognito conversation settings. + +"Moved" conversation would still be using servers that were configured for the previous user. +Perhaps it makes more sense to implement after automated queue rotation. diff --git a/docs/rfcs/2024-06-17-agent-stats-persistence.md b/docs/rfcs/2024-06-17-agent-stats-persistence.md new file mode 100644 index 0000000000..2f5d641769 --- /dev/null +++ b/docs/rfcs/2024-06-17-agent-stats-persistence.md @@ -0,0 +1,87 @@ +# Agent stats persistence + +## Problem + +State/state tracked in agent are lost on app restart, which makes it difficult to debug user bugs. + +## Solution + +Persist stats between sessions. + +App terminal signals may vary per platform / be absent (?) -> persist stats periodically. + +Stats would have `<userId, server>` key, so we don't want to store them in a plaintext file to not leak used servers locally -> persist in encrypted db. + +There's couple of orthogonal design decision to be made: +- persist in chat or in agent db + - pros for chat: + - possibly less contention for db than agent + - pros for agent: + - no unnecessary back and forth, especially if agent starts accumulating from past sessions and has to be parameterized with past stats (see below) +- agent to start accumulating from past sessions stats, or keep past separately and only accumulate for current session from zeros + - pros for accumulating from past sessions: + - easier to maintain stats - e.g. user deletion has to remove keys, which is more convoluted if past stats are not stored in memory + - simpler UI - overall stats, no differentiation for past/current session (or less logic in backend preparing presentation data) + - pros for accumulating from zeros: + - simpler start logic - no need to restore stats from agent db / pass initial stats from chat db + - can differentiate between past sessions and current session stats in UI + +### Option 1 - Persist in chat db, agent to track only current session + +- Chat stores stats in such table: + +```sql +CREATE TABLE agent_stats( + agent_stats_id INTEGER PRIMARY KEY, -- dummy id, there will only be one record + past_stats TEXT, -- accumulated from previous sessions + session_stats TEXT, -- current session + past_started_at TEXT NOT NULL DEFAULT(datetime('now')), -- starting point of tracking stats, reset on stats reset + session_started_at TEXT NOT NULL DEFAULT(datetime('now')), -- starting point of current session + session_updated_at TEXT NOT NULL DEFAULT(datetime('now')) -- last update of current session stats (periodic, frequent updates) +); +``` + +- Chat periodically calls getAgentServersStats api and updates `session_stats`. + - interval? should be short to not lose too much data, 5-30 seconds? +- On start `session_stats` are accumulated into `past_stats` and set to null. +- On user deletion, agent updates current session stats in memory (removes keys), chat has to do same for both stats fields in db. + - other cases where stats have to be manipulated in similar way? + +### Option 2 - Persist in chat db, agent to accumulate stats from past sessions + +- Table is only used for persistence of overall stats: + +```sql +CREATE TABLE agent_stats( + agent_stats_id INTEGER PRIMARY KEY, -- dummy id, there will only be one record + agent_stats TEXT, -- overall stats - past and session + started_tracking_at TEXT NOT NULL DEFAULT(datetime('now')), -- starting point of tracking stats, reset on stats reset + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +); +``` + +- Chat to parameterize creation of agent client with initial stats. + +### Option 3 - Persist in agent db, agent to differentiate past stats and session stats + +- Table in agent db similar to option 1. +- Agent is responsible for periodic updates in session, as well as accumulating into "past" and resetting session stats on start. +- Agent only communicates stats to chat on request. +- On user deletion agent is fully responsible for maintaining both in-memory session stats, and updating db records. + +### Option 4 - Persist in agent db, agent to accumulate stats from past sessions + +- Table in agent db similar to option 2. +- On start agent restores initial stats into memory by itself. +- Since all stats are in memory, on user deletion it's enough to update in memory without updating db. + - there is a race possible where agent crashes after updating stats (removing user keys) in memory before database stats have been overwritten by a periodic update, so it may be better to immediately overwrite and not wait for periodic update. + - still at least there's at least no additional logic to update past stats. + +### Other considerations + +Why is it important to timely remove user keys from past stats? +- stats not being saved for past users: + - important both privacy-wise and to not cause confusion when showing "All" stats (e.g. user summing up across users stats would have smaller total than total stats). + - to avoid accidentally mixing up with newer users. + - though we do have an AUTOINCREMENT user_id in agent so probably it wouldn't be a problem. +- on the other hand maybe we don't want to "forget" stats on user deletion so that stats would reflect networking more accurately? diff --git a/docs/rfcs/2024-07-09-group-snd-status.md b/docs/rfcs/2024-07-09-group-snd-status.md new file mode 100644 index 0000000000..43f491880d --- /dev/null +++ b/docs/rfcs/2024-07-09-group-snd-status.md @@ -0,0 +1,47 @@ +# Group messages sending status + +## Problem + +Currently in UI chat item info: +- There's no differentiation between sent messages and pending messages. +- There's no differentiation between pending messages reasons (establishing connection or member inactivity). + - Since the former is usually not a case due to group forwarding, this can be ignored. +- Messages to be forwarded by admin are not accounted. + +## Solution + +Differentiate new statuses for group sending in chat item info: +- forwarded +- inactive / pending + +Option 1 is to add statuses to CIStatus / ACIStatus types. + +Pros: +- simple. + +Cons: +- further muddies type of statuses for chat item with impossible states / different dimension, as it's not applicable directly to chat item but a technicality of group sending process. + +Option 2 is to create a new type, GroupSndStatus. + +```haskell +data GroupSndStatus + = GSSNew + | GSSForwarded + | GSSInactive + | GSSSent + | GSSRcvd {msgRcptStatus :: MsgReceiptStatus} + | GSSError {agentError :: SndError} + | GSSWarning {agentError :: SndError} + | GSSInvalid {text :: Text} +``` + +Most statuses repeat CIStatus sending statuses, with addition of forwarded and inactive for group sending process. + +Pros: +- separates concerns of chat item presentation from group sending process. +- allows future extension without further muddying CIStatus types. + +Cons: +- more work. +- requires backwards compatible decoding with ACIStatus to read previous data from db. diff --git a/docs/rfcs/2024-10-27-server-operators.md b/docs/rfcs/2024-10-27-server-operators.md new file mode 100644 index 0000000000..5456d28f08 --- /dev/null +++ b/docs/rfcs/2024-10-27-server-operators.md @@ -0,0 +1,24 @@ +# Server operators + +## Problem + +All preconfigured servers operated by a single company create a risk that user connections can be analysed by aggregating transport information from these servers. + +The solution is to have more than one operator servers pre-configured in the app. + +For operators to be protected from any violations of rights of other users or third parties by the users who use servers of these operators, the users have to explicitely accept conditions of use with the operator, in the same way they accept conditions of use with SimpleX Chat Ltd by downloading the app. + +## Solution + +Allow to assign operators to servers, both with preconfigured operators and servers, and with user-defined operators. Agent added support for server roles, chat app could: +- allow assigning server roles only on the operator level. +- only on server level. +- on both, with server roles overriding operator roles (that would require a different type for server for chat app). + +For simplicity of both UX and logic it is probably better to allow assigning roles only on operators' level, and servers without set operators can be used for both roles. + +For agreements, it is sufficient to record the signatures of these agreements on users' devices, together with the copy of signed agreement (or its hash and version) in a separate table. The terms themselves could be: +- included in the app - either in code or in migration. +- referenced with a stable link to a particular commit. + +The first solution seems better, as it avoids any third party dependency, and the agreement size is relatively small (~31kb), to reduce size we can store it compressed. diff --git a/docs/rfcs/2024-11-28-business-address.md b/docs/rfcs/2024-11-28-business-address.md new file mode 100644 index 0000000000..e41c904457 --- /dev/null +++ b/docs/rfcs/2024-11-28-business-address.md @@ -0,0 +1,29 @@ +# Business address + +## Problem + +When business uses a communication system for support and other business scenarios, it's important for the customer: +- to be able to talk to multiple people in the business, and know who they are. +- potentially, add friends or relatives to the conversation if this is about a group purchase. + +It's important for the business: +- to have bot accept incoming requests. +- to be able to add other people to the coversation, as transfer and as escalation. + +This is how all messaging support system works, and how WeChat business accounts work, but no messenger provides it. + +## Solution + +Make current contact addresses to support business mode. We already have all the elements for that. + +- connection requests will be accepted automatically (non-optionally), and auto-reply will be sent (if provided). +- the request sender will be made member, can be made admin later manually. +- the new group with the customer will be created on each request instead of direct conversation. + +Group will function differently from a normal group: +- Show business name and avatar to customer, customer name and avatar to business. +- Use different icon for customer and for the business if the avatar is not provided. +- Possibly, a sub-icon on business avatar for customers. +- Members added by business are marked as business, by customer as customer (not MVP). + +This functionality allows to develop support bots that automatically reply, potentially answer some questions, and add support agents as required, who can escalate further. diff --git a/docs/rfcs/2024-12-28-reports.md b/docs/rfcs/2024-12-28-reports.md new file mode 100644 index 0000000000..729ad47f19 --- /dev/null +++ b/docs/rfcs/2024-12-28-reports.md @@ -0,0 +1,84 @@ +# Content complaints / reports + +## Problem + +Group moderation is a hard work, particularly when members can join anonymously. + +As groups count and size grows, and as we are moving to working large groups, so will the abuse, so we need report function for active groups that would forward the message that members may find offensive or inappropriate or off-topic or violating any rules that community wants to have. + +It doesn't mean that the moderators must censor everything that is reported, and even less so, that it should be centralized (although in our directory our directory bot would also receive these complaints, and would allow us supporting group owners). + +While we have necessary basic features to remove content and block members, we need to simplify identifying the content both to the group owners and to ourselves, when it comes to the groups listed in directory, or for the groups and files hosted on our servers. + +Having simpler way to report content would also improve the perceived safety of the network for the majority of the users. + +## Solution proposal + +"Report" feature on the messages that would highlight this message to all group admins and moderators. + +Group directory service is also an admin (and will be reduced to moderator in the future), so reported content will be visible to us, so that we can both help group owners to moderate their groups and also to remove the group from directory if necessary. + +To the user who have the new version the reports will be sent as a special event, similar to reaction (or it can be simply an extended reaction?) the usual forwarded messages in the same group, but only to moderators (including admins and owners), with additional flag indicating that this is the report. + +In the clients with the new version the reports could be shown as a flag, possibly with the counter, on group messages that were reported, in the same line where we show emojis. + +If we do that these flags will be seen only by moderators and by the user who submitted the report. When the moderator taps the flag, s/he would see the list of user who reported it, together with the reason. + +The downside of the above UX is that it: +- does not solve the problem of highlighting the problem to admins, particularly if them manage many groups. +- creates confusion about who can see the reports. +- further increases data model complexity, as it requires additional table or self-references (as with quotes), as reports can be received prior to the reported content. +- does not allow admins to see the reported content before it is received by them (would be less important with super-peers). + +Alternatively, and it is probably a better option, all reports, both sent by the users and received by moderators across all groups can be shown in the special subview Reports in each group. The report should be shown as the reported message with the header showing the report reason and the reporter. The report should allow these actions: +- moderate the original message, +- navigate to the original message (requires infinite scrolling, so initially will be only supported on Android and desktop), +- connect to the user who sent the report - it should be possible even if the group prohibits direct messages. There are two options how this communication can be handled - either by creating a new connection, and shown as normal contacts, or as comments to the report, and sent in the same group connection. The latter approach has the advantage that the interface would not be clutter the interace. The former is much simpler, so should probably be offered as MVP. + +This additional chat is necessary, as without it it would be very hard to notice the reports, particularly for the people who moderate multiple groups, and even more so - in our group directory and future super peers. + +## Protocol + +**Option 1** + +The special message `x.msg.report` will be sent in the group with this schema: + +```json +{ + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "msgId": {"ref": "base64url"}, + "reason": {"enum": ["spam", "illegal", "community", "other"]} + }, + "optionalProperties": { + "memberId": {"ref": "base64url"}, + "comment": {"type": "string"} + } + } + } +} +``` + +The downside is that it does not include the original message, so that the admin cannot act on it before the message is received. + +**Option 2** + +Message quote with the new content type. + +Pro - backwards compatible (quote would include text repeating the reason). + +Con - allows reporting non-existing messages, or even mis-reporting, but it is the same consideration that applies to all quotes. In this case though the admin might moderate the message they did not see yet, and it can be abused to remove appropriate content, so the UI should show warning "do you trust the reporter, as you did not receive the message yet". Moderation via reports may have additional information to ensure that exactly the reported message is moderated - e.g., the receiving client would check that the hash of the message in moderation event matches the hash of one of the messages in history. Possibly this is unnecessary with the view of migration of groups to super-peers. + +The report itself would be a new message content type where the report reason would be repeated as text, for backward compatibility. + +The option 2 seems to be simpler to implement, backward compatible and also more naturally fitting the protocol design - the report is simply a message with the new type that the old clients would be able to show correctly as the usual quote. + +The new clients would have a special presentation of these messages and also merging them into one - e.g. they can be shown as group events on in a more prominent way, but less prominent than the actual messages, and also merge subsequent reports about the same message. + +Given that the old clients would not be able to differentiate the reports and normal replies, and can inadvertently reply to all, we probably should warn the members submitting the report that some of the moderators are running the old version, and give them a choice - send to all or send only to moderators with the new version (or don't send, in case all admins run the old version). + +Having the conversation with the member about their report probably fits with the future comment feature that we should start adding to the backend and to the UI as well, as there is no reasonable backward compatibility for it, and members with the old clients simply won't see the comments, so we will have to release it in two stages and simply not send comments to the members with the old version. + +The model for the comments is a new subtype of MsgContainer, that references the original message and member, but does not include the full message. diff --git a/docs/rfcs/2024-12-30-content-moderation.md b/docs/rfcs/2024-12-30-content-moderation.md new file mode 100644 index 0000000000..e4f21a2d21 --- /dev/null +++ b/docs/rfcs/2024-12-30-content-moderation.md @@ -0,0 +1,136 @@ +# Evolving content moderation + +## Problem + +As the users and groups grow, and particularly given that we are planning to make large (10-100k members) groups work, the abuse will inevitably grow as well. + +Our current approach to content moderation is the following: +- receive a user complaints about the group that violates content guidelines (e.g., most users who send complaints, send them about relatively rare cases of CSAM distribution). This complaint contains the link to join the group, so it is a public group that anybody can join, and there is no expectation of privacy of communications in this group. +- we forward this complaint to our automatic bot joins this group and validates the complaint. +- if the complaint is valid, and the link is hosted on one of the pre-configured servers, then we can disable the link to join the group. +- in addition to that, the bot automatically deletes all files sent to the group, in case they are uploaded to our servers, via secure SSH connection directly to server control port (we don't expose shell access in this way, only to a limited set of server control port commands). + +The problem of CSAM is small at the moment, compared with the network size, but without moderation it would grow, and we need to be ahead of this problem, so this solution was in place since early 2024 - we wrote about it on social media. + +The limitation of this approach is that nothing prevents users who created such group to create a new one, and communicate the link to the new group to the existing members so they can migrate there. While this whack-a-mole game has been working so far, it will not be sustainable once we add support for large groups, so we need to be ahead of this problem again, and implement more efficient solutions. + +At the same time, the advantage of both this solution and of the proposed one is that it achieves removal of CSAM without compromising privacy in any way. Most CSAM distribution in all communication networks happens in publicly accessible channels, and it's the same for SimpleX network. So while as server operators we cannot access any content, as users, anybody can access it, and we, acting as users can use available information to remove this content without any compromise to privacy in security. + +This is covered in our [Privacy Policy](https://simplex.chat/privacy/). + +## Solution + +The solution to prevent further CSAM distribution by the users who did it requires restricting their activity on the client side, and also preventing migration of blocked group to another group. + +Traditionally, communication networks have some form of identification on the server side, and that identification is used to block offending users. + +Innovative SimpleX network design removed the need for persistent user identification of users, and many users see it as an unsolvable dilemma - if we cannot identify the users, then we cannot restrict their actions. + +But it is not true. In the same way we already impose restriction on the sent file size, limiting it to 1gb only on the client-side, we can restrict any user actions on the client side, without having any form of user identification, and without knowing how many users were blocked - we would only know how many blocking actions we applied, but we would not have any information about whether they were applied to one or to many users, in the same way as we don't know whether multiple messaging queues are controlled by one or by multiple users. + +The usual counter-argument is that this can be easily circumvented, because the code is open-source, and the users can modify it, so this approach won't work. While this argument premise is correct, the conclusion that this solution won't be effective is incorrect for two reasons: +- most users are either unable or unwilling to invest time into modifying code. This fact alone makes this solution effective in absolute majority of cases. +- any restriction on communication can be applied both on sending and on receiving client, without the need to identify either of these clients. We already do it with 1gb file restriction - e.g., even if file sender modifies their client to allow sending larger files, most of the recipients won't be able to receive this file anyway, as their clients also restrict the size of file that can be received to 1gb. + +For the group that is blocked to continue functioning, not only message senders have to modify their clients, but also message recipients, which won't happen in the absence of ability to communicate in disabled group. Such groups will only be able to function in an isolated segment of the network, when all users use modified clients and with self-hosted servers, which is outside of our zone of any moral and any potential legal responsibility (while we do not have any responsibility for user-generated content under the existing laws, there are requirements we have to comply with that exist outside of law, e.g. requirements of application stores). + +## Potential changes + +This section is the brain-dump of technically possible changes for the future. They will not be implemented all at once, and this list is neither exhaustive, as we or our users can come up with better ideas, nor committed - some of the ideas below may never be implemented. So these ideas are only listed as technical possibilities. + +Our priority is to continue being able to prevent CSAM distribution as network and groups grow, while doing what is reasonable and minimally possible, to save our costs, to avoid any disruption to the users, and to avoid the reduction in privacy and security - on the opposite, we are planning multiple privacy and security improvements in 2025. + +### Mark files and group links as blocked on the server, with the relevant client action + +Add additional protocol command `BLOCK` that would contain the blocking reason that will be presented to the users who try to connect to the link or to download the file. This would differentiate between "not working" scenarios, when file simply fails to download, and "blocked" scenario, and this simple measure would already reduce any prohibited usage of our servers. This change is likely to be implemented in the near future, to make users aware that we are actively moderating illegal content on the network, to educate users about how we do it without any compromise to their privacy and security, and to increase trust in network reliability, as currently our moderation actions are perceived as "something is broken" by affected users. + +### Extend blocking records on files to include client-side restrictions, and apply them to the client who received this blocking record. + +E.g., the client of the user who uploaded the file would periodically check who this file was received by (this functionality currently does not exist), and during this check the client may find out that the file was blocked. When client finds it out it may do any of the following: +- show a warning that the file violated allowed usage conditions that user agreed to. +- apply restrictions, whether temporary or permanent, to upload further files to servers of this operator only (it would be inappropriate to apply wider restrictions - so we appreciate this comment made by one of the users during the consultation). In case we decide that permanent restrictions should be applied, we could also program the ability to appeal this decision to support team and lift it via unblock code - without the need to have any user identification. + +The downside of this approach is that the client would have to check the file after it is uploaded, which may create additional traffic. But at the same time it would provide file delivery receipts, so overall it could be a valuable, although substantial, change. + +To continue with the file, the clients of the users who attempt to receive the file after it was blocked could do one of the following, depending on the blocking record: +- see the warning that the file is blocked. If CSAM was sent in a group that is not distributing CSAM, this adds comfort and the feeling of safety. +- block image preview, in the same way we block avatars of blocked members. +- users can configure automatic deletion of messages with blocked files. +- refuse, temporarily or permanently, to receive future files and/or messages from this group member. Permanent restriction may be automatically lifted once the member's client presents the proof of being unblocked by server operator. + +Applying the restrictions on the receiving side is technically simpler, and requires only minimal protocol changes mentioned above. + +While file senders can circumvent client side restrictions applied by server operators, these measures can be effective, because the recipients would also have to circumvent them, which is much less likely to happen in a coordinated way. + +The upside of this approach is that it does not compromise users' privacy in any way, and it does not interfere with users rights too. A user voluntarily accepted the Conditions of Use that prohibit upload of illegal content to our servers, so it is in line with the agreement for us to enforce these conditions and restrict functionality in case of conditions being violated. At the same time it would be inappropriate for us to restrict the ability to upload files to the servers of 3rd party operators that are not pre-configured in the app - only these operators should be able to restrict uploads to their servers. + +It also avoids the need for any scanning of content, whether client- or server-side, that would also be an infringement on the users right to privacy under European Convention of Human Rights, article 8. It also makes it unnecessary to identify users, contrary to common belief that to restrict users one needs to identify them. + +In the same way the network design allows delivering user messages without any form of user identification on the network protocol level, which is the innovation that does not exist in any other network, we can apply client-side restrictions on user activities without the need to identify a user. So if the block we apply to a specific piece of content results in client-side upload/download restrictions, all we would know is how many times this restriction was applied, but not to how many users - multiple blocked files could have been all uploaded by one user or by multiple users, but this is not the knowledge that is required to restrict further abuse of our servers and violation of condition of use. Again, this is an innovative approach to moderation that is not present in any of the networks, that allows us both to remain in compliance with the contractual obligations (e.g., with application store owners) and any potential legal obligation (even though the legal advice we have is that we do not have obligation to moderate content, as we are not providing communication services), once it becomes a bigger issue. + +### Extend blocking records on links to include client-side restrictions, and apply them to the clients who received this blocking record. + +Similarly to files, once the link to join the group is blocked, both the owner's client and all members' clients can impose (technically) any of the following restrictions. + +For the owner: +- restrict, temporarily or permanently, ability to create public groups on the servers of the operator (or group of operators, in case of pre-configured operators) who applied this blocking record. +- restrict, temporarily or permanently, ability to upload files to operator's servers. +- restrict, temporarily or permanently, sending any messages to operator's servers, not only in the blocked group. + +For all group members: +- restrict, temporarily or permanently, ability to send and receive messages in the blocked group. + +For the same reason as with files, this measure will be an effective deterrence, even though the code is open-source. + +While full blocking may be seen as draconian, for the people who repeatedly violate the conditions of use, ignoring temporary or limited restrictions, it may be appropriate. The tracking of repeat violations of conditions also does not require any user identification and can be done fully on the client side, with sufficient efficiency. + +### Implement ability to submit reports to group owners and moderators + +This is covered under a [separate RFC](./2024-12-28-reports.md) and is currently in progress. This would improve the ability of group owners to moderate their groups, and would also improve our ability to moderate all listed groups, both manually and automatically, as Directory Service has moderation rights. + +### Implement ability to submit reports to 3rd party server operators + +While users already can send reports to ourselves directly via the app, sending them to other server operators requires additional steps from the users. + +This function would allow sending reports to any server operator directly via the app, to the address sent by the server during the initial connection. + +Server operators may be then offered efficient interfaces in the clients to manage these complaints and to apply client-side restrictions to the users who violate the conditions. + +### Blacklist servers who refuse to remove CSAM from receiving any traffic from our servers + +We cannot and should not enforce that 3rd party server operators remove CSAM from their servers. We will only be recommending it and providing tools to simplify it. + +But we can, technically, implement block-lists of servers so that the users who need to send messages to these servers would not be able to do that via our servers. + +We also can require mandatory server identification to requests to proxy messages via client certificates of the server that could be validated via a reverse connection, and also block incoming traffic from these servers. + +While both these measures are undesirable and would result in network fragmentation, they are technically possible. Similar restrictions already happen in fediverse networks, and they are effective. + +## Actual planned changes + +To summarize, the changes that are planned in the near future: + +- client-side notifications that files or group links were blocked (as opposed to show error, creating an impression that something is not working). +- [content reports](./2024-12-28-reports.md) to group owners and moderators. +- additional short notice about conditions of use that apply to file uploads prior to the first upload. + +Additional simple changes that are considered: + +- applying client-side restriction to create new public groups on operator's servers on admins of blocked groups (do not confuse that with the groups that we decided not to list in our directory, or decided to remove from our directory - this is not blocking that is being discussed here). +- if the group link was registered via directory service, we can prevent further registration of public groups in directory service for this user by, communicating that this link is blocked to directory service. +- preventing any communication in blocked groups. + +To clarify, all these restrictions are considered only for the groups that were created primarily to distribute or to promote CSAM content, they won't apply in cases some group members maliciously posted illegal content in a public group - in which case they will only be applied to this member, helping group owners to moderate. + +We will continue moderating the content as we do now, and as long as CSAM distribution is prevented, we may not need additional measures listed here. + +At the same time, we are committed to make it impossible to distribute CSAM in the part of SimpleX network that we or any other pre-configured operators operate. + +We are also committed to achieve this goal without any reduction in privacy and security even for the affected users. E.g., unless there is an enforceable order, we will not be recording any information identifying the user, such as IP address, because it may inadvertently affect the users whose content was flagged by mistake. + +Our ultimate commitment, and our business is to provide private and secure communication to the users who comply with conditions of use, and to prevent mass-scale surveillance of non-suspects (which is a direct violation of European Convention of Human Rights). + +Privacy and security of the network will further improve in 2025, as we plan: +- adding post-quantum encryption to small groups. +- adding proxying during file reception from unknown (or all) servers. +- adding scheduled and delayed re-broadcasts in large groups, to frustrate timing attacks that could otherwise allow identifying users who send messages to groups. diff --git a/docs/rfcs/2025-01-20-member-mentions.md b/docs/rfcs/2025-01-20-member-mentions.md new file mode 100644 index 0000000000..fdb35c608b --- /dev/null +++ b/docs/rfcs/2025-01-20-member-mentions.md @@ -0,0 +1,60 @@ +# Member mentions + +## Problem + +Mention members in the messages. + +There are several UX objectives that mentions must deliver: +- to notify the user about the messages that mention the user or to reply to user's messages - groups must have this notification mode (already present in the API), +- to allow the user to navigate to unread mentions and replies, +- to highlight mentions and allow all users to open mentioned member profile - this is the least important objective. + +## Solution + +Message text should include the reference to the shared member display name: + +``` +Hello @name +``` + +or + +``` +Hello @'member name' +``` + +This is the same format that people already use and that is currently supported in the API. The name in the message should use the display name at the time of mention, both for backwards compatibility and for better view in compose field, and the message should additionally include the mapping from used display names to shared group member IDs, and the UI would show the current display name (at the time of loading the message to the view). + +For this mapping the message JSON will include the array of mentions, as objects with properties `displayName` and `memberId`. This is to ensure the intent and that the fragments of text are treated as mentions. + +Using an immutable `memberId` would prevent any race conditions and duplicate display names. The receiving client would show a local view name (display name or an alias), and might open a correct member card when mention is tapped. + +As display names are not unique in the group, we should convert them to locally-unique names (per message), by appending _1, _2, as necessary, and the same locally unique names should be used in the mapping to member IDs. These locally unique names must NOT use local user aliases, and must NOT use localDisplayName, as otherwise it may leak information that is known only to the user's client. + +There should be a reasonable limit on the number of mentions per message, e.g. 3. This is to prevent abuse, expensive processing both in the client and in super-peers that would have to forward member profiles if they were not forwarded before. This limit has to be enforced both on sending and receiving ends. + +## UX for sending mentions + +When a member types '@' character in the entry field, the app would show the paginated list of most recently active members, with search. This requires a separate API, and the same API can be used to show a paginated member list - loading the full list is already quite expensive with groups over 1-2k members. + +## UX for navigating to mentions + +The current circles with unread messages should indicate the number of unread mentions (including replies) above and below the view. Tapping the circle should navigate to the next unread mention, and not to the bottom/top of the conversation. Long-pressing the circle should offer the option to navigate to the top/bottom. In the absense of mentions, tapping circles would navigate to top/bottom. + +## Message UI + +Item text will include markdown elements for mentioned members. This will be used when rendering to show member display names or local aliases. + +Chat items data will include the list of members used in the chat item, including view names and member IDs. + +## Forwarding and saving to local items + +When forwarding to another conversation or saving to notes a message with mentions the app should use: +- current display names instead of display names used in the message. +- remove mentions mapping from the message data. + +## Schema + +Two new columns for chat_items table: +- user_mention - 0 or 1 to indicate whether a message is a reply to user's message or mentions user. +- member_mentions - the object mapping display names to member IDs, either as JSON, or in a more economical comma-separated list of "ID:name" strings (or "ID:'member name'). This field can be processed to load mention information, with the limit of 3 mentions per message it's sufficient. diff --git a/docs/rfcs/2025-02-17-member-send-limits.md b/docs/rfcs/2025-02-17-member-send-limits.md new file mode 100644 index 0000000000..cc98d63e46 --- /dev/null +++ b/docs/rfcs/2025-02-17-member-send-limits.md @@ -0,0 +1,97 @@ +# Member sending limits + +## Problem + +Rate limiting member sending to prevent abuse in groups. + +## Solution + +Each member record to have `rateLimit :: Maybe MemberRateLimit`, Nothing by default. Nothing means default rate limit for regular members (`MRLWindow`), and no limit for owners/admins/moderators (`MRLNone`). Default rate limit is defined in configuration, e.g. limit of 15 messages in 60 second window, or could be defined in group profile. + +Rate limit can be overridden per member by sending `XGrpMemRestrict` with updated `rateLimit` by member of same role or higher, similar to changing roles. `APIRateLimitMember` allows to enable or disable rate limiting for member, we could also provide more granular control in it (pass `WindowLimit`), but it seems unnecessary complex for UI. + +```haskell +data MemberRateLimit + = MRLNone -- default for owners, admins, moderators + | MRLWindow WindowLimit + deriving (Eq, Show) + +data WindowLimit = WindowLimit + { window :: Int, -- seconds + limit :: Int + } + deriving (Eq, Show) + +-- sent in XGrpMemRestrict +data MemberRestrictions = MemberRestrictions + { restriction :: MemberRestrictionStatus, + rateLimit :: Maybe MemberRateLimit -- Nothing means use default + } + deriving (Eq, Show) + +-- new api in ChatCommand +| APIRateLimitMember GroupId GroupMemberId + +-- new response in ChatResponse +| CRMemberRateLimit {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + +-- new field in GroupMember +data GroupMember = GroupMember { + ... + rateLimit :: Maybe MemberRateLimit, + ... +} +``` + +Rate limit overrides to be persisted on group member records. + +```sql +ALTER TABLE group_members ADD COLUMN rate_limit TEXT; -- MemberRateLimit JSON encoded +``` + +Limits can be tracked inside fixed windows both for receiving and sending. + +```haskell +data ChatController = ChatController { + ... + memberLimits :: TMap (GroupId, MemberId) (TVar MemberRateLimitWindow), + ownLimits :: TMap GroupId (TVar MemberRateLimitWindow), + ... +} + +data MemberRateLimitWindow = MemberRateLimitWindow { + startedAt :: UTCTime, + windowLimit :: WindowLimit, + messages :: Int +} +``` + +Client to track limit for each writing member in state - `memberLimits`. If current window's interval has passed, checked against `startedAt` of `MemberRateLimitWindow`, reset `messages` counter. + +Track own limits per group - `ownLimits`. When limit in group is reached, `CRGroupSendingLimited blocked = True` event is sent to UI to block sending in group. Unblock group sending in UI by scheduling background process to send `CRGroupSendingLimited blocked = False` after interval? + +```haskell +-- new event in ChatResponse +| CRGroupSendingLimited {user :: User, groupInfo :: GroupInfo, blocked :: Bool} +``` + +### Receiving messages from limited members + +Receiving message from member that exceeded their limit would fail it as prohibited. We can limit content messages, updates, reactions, etc. Practically only regular members would be limited, so there's no need for limiting service messages. Should we limit deletes? + +Problems: + +- Inconsistencies in window tracking on sending and receiving sides -> track based on item_ts?; +- Subscription produces message surges; +- Server downtime (or network outage) leads to accumulation of scheduled messages on sending side -> item_ts tracking wouldn't help. Issue is similar to subscription, where many messages can be received in short span legitimately. +- This approach doesn't reduce load of retrieving message and of all machinery passing from agent to chat. + +Subscription issue could be solved by not tracking limits during subscription (client "knows" when it has subscribed) and for some time after it. For how long - 30 seconds / 1 minute? - arbitrary, longer absence periods lead to more not yet retrieved messages, freed quotas resulting in more sent pending messages. + +Better solution would be to not drop (prohibit) messages at all, but stop reception per connection for periods of time. Possible approaches: + +- Don't send (delay) ACK - bad idea as it would lead to repeated processing on client restart, and other possible failures in delivery. +- ACK with parameter `wait` for server - server would wait before sending next message. +- Unsubscribe (kill client?)/resubscribe after some time - more expensive. +- Signal agent to hold on next message - similar to delayed ACK but at least we don't process message. (for example, also via ACK but parameter to wait is for agent) +- Rework chat-to-agent communication to communicate via per connection queue (currently it's single `subQ`) - complex - but we get less expensive "unsubscribe"? Essentially agent still holds on to message like in previous approach. diff --git a/flake.lock b/flake.lock index a11e01683e..eac7357cd8 100644 --- a/flake.lock +++ b/flake.lock @@ -156,11 +156,11 @@ "ghc98X": { "flake": false, "locked": { - "lastModified": 1696643148, - "narHash": "sha256-E02DfgISH7EvvNAu0BHiPvl1E5FGMDi0pWdNZtIBC9I=", + "lastModified": 1715066704, + "narHash": "sha256-F0EVR8x/fcpj1st+hz96Wdsz5uwVIOziGKAwRxLOYJw=", "ref": "ghc-9.8", - "rev": "443e870d977b1ab6fc05f47a9a17bc49296adbd6", - "revCount": 61642, + "rev": "78a253543d466ac511a1664a3e6aff032ca684d5", + "revCount": 61757, "submodules": true, "type": "git", "url": "https://gitlab.haskell.org/ghc/ghc" @@ -175,11 +175,11 @@ "ghc99": { "flake": false, "locked": { - "lastModified": 1697054644, - "narHash": "sha256-kKarOuXUaAH3QWv7ASx+gGFMHaHKe0pK5Zu37ky2AL4=", + "lastModified": 1726585445, + "narHash": "sha256-IdwQBex4boY6s0Plj5+ixf36rfYSUyMdTWrztKvZH30=", "ref": "refs/heads/master", - "rev": "f383a242c76f90bcca8a4d7ee001dcb49c172a9a", - "revCount": 62040, + "rev": "7fd9e5e29ab54eb406880077463e8552e2ddd39a", + "revCount": 67238, "submodules": true, "type": "git", "url": "https://gitlab.haskell.org/ghc/ghc" @@ -225,6 +225,8 @@ "hls-2.2": "hls-2.2", "hls-2.3": "hls-2.3", "hls-2.4": "hls-2.4", + "hls-2.5": "hls-2.5", + "hls-2.6": "hls-2.6", "hpc-coveralls": "hpc-coveralls", "hydra": "hydra", "iserv-proxy": "iserv-proxy", @@ -238,16 +240,17 @@ "nixpkgs-2205": "nixpkgs-2205", "nixpkgs-2211": "nixpkgs-2211", "nixpkgs-2305": "nixpkgs-2305", + "nixpkgs-2311": "nixpkgs-2311", "nixpkgs-unstable": "nixpkgs-unstable", "old-ghc-nix": "old-ghc-nix", "stackage": "stackage" }, "locked": { - "lastModified": 1701163700, - "narHash": "sha256-sOrewUS3LnzV09nGr7+3R6Q6zsgU4smJc61QsHq+4DE=", + "lastModified": 1705833500, + "narHash": "sha256-rUIr6JNbCedt1g4gVYVvE9t0oFU6FUspCA0DS5cA8Bg=", "owner": "input-output-hk", "repo": "haskell.nix", - "rev": "2808bfe3e62e9eb4ee8974cd623a00e1611f302b", + "rev": "d0c35e75cbbc6858770af42ac32b0b85495fbd71", "type": "github" }, "original": { @@ -328,16 +331,50 @@ "hls-2.4": { "flake": false, "locked": { - "lastModified": 1696939266, - "narHash": "sha256-VOMf5+kyOeOmfXTHlv4LNFJuDGa7G3pDnOxtzYR40IU=", + "lastModified": 1699862708, + "narHash": "sha256-YHXSkdz53zd0fYGIYOgLt6HrA0eaRJi9mXVqDgmvrjk=", "owner": "haskell", "repo": "haskell-language-server", - "rev": "362fdd1293efb4b82410b676ab1273479f6d17ee", + "rev": "54507ef7e85fa8e9d0eb9a669832a3287ffccd57", "type": "github" }, "original": { "owner": "haskell", - "ref": "2.4.0.0", + "ref": "2.4.0.1", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.5": { + "flake": false, + "locked": { + "lastModified": 1701080174, + "narHash": "sha256-fyiR9TaHGJIIR0UmcCb73Xv9TJq3ht2ioxQ2mT7kVdc=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "27f8c3d3892e38edaef5bea3870161815c4d014c", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.5.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.6": { + "flake": false, + "locked": { + "lastModified": 1705325287, + "narHash": "sha256-+P87oLdlPyMw8Mgoul7HMWdEvWP/fNlo8jyNtwME8E8=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "6e0b342fa0327e628610f2711f8c3e4eaaa08b1e", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.6.0.0", "repo": "haskell-language-server", "type": "github" } @@ -384,11 +421,11 @@ "iserv-proxy": { "flake": false, "locked": { - "lastModified": 1691634696, - "narHash": "sha256-MZH2NznKC/gbgBu8NgIibtSUZeJ00HTLJ0PlWKCBHb0=", + "lastModified": 1707968597, + "narHash": "sha256-C53NqToxl+n9s1pQ0iLtiH6P5vX3rM+NW/mFt4Ykpsk=", "ref": "hkm/remote-iserv", - "rev": "43a979272d9addc29fbffc2e8542c5d96e993d73", - "revCount": 14, + "rev": "1b7f8aeb37bbc7c00f04e44d9379aa15a4409e8b", + "revCount": 18, "type": "git", "url": "https://gitlab.haskell.org/hamishmack/iserv-proxy.git" }, @@ -552,11 +589,11 @@ }, "nixpkgs-2305": { "locked": { - "lastModified": 1695416179, - "narHash": "sha256-610o1+pwbSu+QuF3GE0NU5xQdTHM3t9wyYhB9l94Cd8=", + "lastModified": 1705033721, + "narHash": "sha256-K5eJHmL1/kev6WuqyqqbS1cdNnSidIZ3jeqJ7GbrYnQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "715d72e967ec1dd5ecc71290ee072bcaf5181ed6", + "rev": "a1982c92d8980a0114372973cbdfe0a307f1bdea", "type": "github" }, "original": { @@ -566,6 +603,22 @@ "type": "github" } }, + "nixpkgs-2311": { + "locked": { + "lastModified": 1719957072, + "narHash": "sha256-gvFhEf5nszouwLAkT9nWsDzocUTqLWHuL++dvNjMp9I=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "7144d6241f02d171d25fba3edeaf15e0f2592105", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-23.11-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgs-lib": { "locked": { "dir": "lib", @@ -602,17 +655,17 @@ }, "nixpkgs-unstable": { "locked": { - "lastModified": 1695318763, - "narHash": "sha256-FHVPDRP2AfvsxAdc+AsgFJevMz5VBmnZglFUMlxBkcY=", + "lastModified": 1694822471, + "narHash": "sha256-6fSDCj++lZVMZlyqOe9SIOL8tYSBz1bI8acwovRwoX8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e12483116b3b51a185a33a272bf351e357ba9a99", + "rev": "47585496bcb13fb72e4a90daeea2f434e2501998", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixpkgs-unstable", "repo": "nixpkgs", + "rev": "47585496bcb13fb72e4a90daeea2f434e2501998", "type": "github" } }, @@ -664,11 +717,11 @@ "stackage": { "flake": false, "locked": { - "lastModified": 1699834215, - "narHash": "sha256-g/JKy0BCvJaxPuYDl3QVc4OY8cFEomgG+hW/eEV470M=", + "lastModified": 1726532152, + "narHash": "sha256-LRXbVY3M2S8uQWdwd2zZrsnVPEvt2GxaHGoy8EFFdJA=", "owner": "input-output-hk", "repo": "stackage.nix", - "rev": "47aacd04abcce6bad57f43cbbbd133538380248e", + "rev": "c77b3530cebad603812cb111c6f64968c2d2337d", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 6fabe7d657..9ee0c52020 100644 --- a/flake.nix +++ b/flake.nix @@ -198,6 +198,8 @@ packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [ pkgs.pkgsCross.mingwW64.openssl ]; + packages.simplex-chat.flags.client_library = true; + packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ pkgs.pkgsCross.mingwW64.openssl ]; @@ -335,6 +337,8 @@ packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-android-log.patch ]; + packages.simplex-chat.flags.client_library = true; + packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ (android32Pkgs.openssl.override { static = true; enableKTLS = false; }) ]; @@ -385,6 +389,7 @@ "chat_send_cmd" "chat_send_remote_cmd" "chat_valid_name" + "chat_json_length" "chat_write_file" ]; postInstall = '' @@ -442,6 +447,8 @@ packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-android-log.patch ]; + packages.simplex-chat.flags.client_library = true; + packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ (androidPkgs.openssl.override { static = true; }) ]; @@ -487,6 +494,7 @@ "chat_send_cmd" "chat_send_remote_cmd" "chat_valid_name" + "chat_json_length" "chat_write_file" ]; postInstall = '' @@ -545,6 +553,8 @@ packages.simplexmq.flags.swift = true; packages.direct-sqlcipher.flags.commoncrypto = true; packages.entropy.flags.DoNotGetEntropy = true; + packages.simplex-chat.flags.client_library = true; + packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ # TODO: have a cross override for iOS, that sets this. ((pkgs.openssl.override { static = true; }).overrideDerivation (old: { CFLAGS = "-mcpu=apple-a7 -march=armv8-a+norcpc" ;})) @@ -559,8 +569,10 @@ extra-modules = [{ packages.direct-sqlcipher.flags.commoncrypto = true; packages.entropy.flags.DoNotGetEntropy = true; + packages.simplex-chat.flags.client_library = true; + packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ - (pkgs.openssl.override { static = true; }) + ((pkgs.openssl.override { static = true; }).overrideDerivation (old: { CFLAGS = "-mcpu=apple-a7 -march=armv8-a+norcpc" ;})) ]; }]; }).simplex-chat.components.library.override ( @@ -576,6 +588,8 @@ packages.simplexmq.flags.swift = true; packages.direct-sqlcipher.flags.commoncrypto = true; packages.entropy.flags.DoNotGetEntropy = true; + packages.simplex-chat.flags.client_library = true; + packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ (pkgs.openssl.override { static = true; }) ]; @@ -589,6 +603,8 @@ extra-modules = [{ packages.direct-sqlcipher.flags.commoncrypto = true; packages.entropy.flags.DoNotGetEntropy = true; + packages.simplex-chat.flags.client_library = true; + packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ (pkgs.openssl.override { static = true; }) ]; diff --git a/images/privacy-guides.jpg b/images/privacy-guides.jpg index f15a8862f7..5876d10c02 100644 Binary files a/images/privacy-guides.jpg and b/images/privacy-guides.jpg differ diff --git a/images/whonix-logo.jpg b/images/whonix-logo.jpg new file mode 100644 index 0000000000..c6fc4729b2 Binary files /dev/null and b/images/whonix-logo.jpg differ diff --git a/libsimplex.dll.def b/libsimplex.dll.def index 4255f4409c..592e6db4f2 100644 --- a/libsimplex.dll.def +++ b/libsimplex.dll.def @@ -3,6 +3,7 @@ EXPORTS hs_init hs_init_with_rtsopts chat_migrate_init + chat_close_store chat_send_cmd chat_send_remote_cmd chat_recv_msg @@ -11,6 +12,7 @@ EXPORTS chat_parse_server chat_password_hash chat_valid_name + chat_json_length chat_encrypt_media chat_decrypt_media chat_write_file diff --git a/media-logos/README.md b/media-logos/README.md new file mode 100644 index 0000000000..3780029331 --- /dev/null +++ b/media-logos/README.md @@ -0,0 +1,5 @@ +# SimpleX logos + +Please use these logos in your publications about SimpleX Chat. + +You can also use app screenshots and diagrams from our [blog posts](../blog/README.md) – please reference them. diff --git a/media-logos/simplex-app-icon-dark.png b/media-logos/simplex-app-icon-dark.png new file mode 100644 index 0000000000..aba97be8ae Binary files /dev/null and b/media-logos/simplex-app-icon-dark.png differ diff --git a/media-logos/simplex-app-icon-light.png b/media-logos/simplex-app-icon-light.png new file mode 100644 index 0000000000..9a44d45b1f Binary files /dev/null and b/media-logos/simplex-app-icon-light.png differ diff --git a/media-logos/simplex-logo-dark.png b/media-logos/simplex-logo-dark.png new file mode 100644 index 0000000000..98c1f500e9 Binary files /dev/null and b/media-logos/simplex-logo-dark.png differ diff --git a/media-logos/simplex-logo-light.png b/media-logos/simplex-logo-light.png new file mode 100644 index 0000000000..6edecb0dd9 Binary files /dev/null and b/media-logos/simplex-logo-light.png differ diff --git a/media-logos/simplex-symbol-dark.png b/media-logos/simplex-symbol-dark.png new file mode 100644 index 0000000000..c561422bdb Binary files /dev/null and b/media-logos/simplex-symbol-dark.png differ diff --git a/media-logos/simplex-symbol-dark.svg b/media-logos/simplex-symbol-dark.svg new file mode 100644 index 0000000000..fa598acf3d --- /dev/null +++ b/media-logos/simplex-symbol-dark.svg @@ -0,0 +1,10 @@ +<svg width="34" height="35" viewBox="0 0 34 35" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M3.02958 8.60922L8.622 14.2013L14.3705 8.45375L17.1669 11.2498L11.4183 16.9972L17.0114 22.5895L14.1373 25.4633L8.54422 19.871L2.79636 25.6187L0 22.8227L5.74794 17.075L0.155484 11.483L3.02958 8.60922Z" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M14.0923 25.5156L16.944 22.6642L16.9429 22.6634L22.6467 16.9612L17.0513 11.3675L17.0523 11.367L14.2548 8.56979L8.65972 2.97535L11.5114 0.123963L17.1061 5.71849L22.8099 0.015625L25.6074 2.81285L19.9035 8.51562L25.4984 14.1099L31.2025 8.40729L34 11.2045L28.2958 16.907L33.8917 22.5017L31.0399 25.3531L25.4442 19.7584L19.7409 25.4611L25.3365 31.0559L22.4848 33.9073L16.8892 28.3124L11.1864 34.0156L8.38885 31.2184L14.0923 25.5156Z" fill="url(#paint0_linear_656_10815)"/> +<defs> +<linearGradient id="paint0_linear_656_10815" x1="12.8381" y1="-0.678252" x2="9.54355" y2="31.4493" gradientUnits="userSpaceOnUse"> +<stop stop-color="#01F1FF"/> +<stop offset="1" stop-color="#0197FF"/> +</linearGradient> +</defs> +</svg> diff --git a/media-logos/simplex-symbol-light.png b/media-logos/simplex-symbol-light.png new file mode 100644 index 0000000000..865f37bfc8 Binary files /dev/null and b/media-logos/simplex-symbol-light.png differ diff --git a/media-logos/simplex-symbol-light.svg b/media-logos/simplex-symbol-light.svg new file mode 100644 index 0000000000..d8b5951a0b --- /dev/null +++ b/media-logos/simplex-symbol-light.svg @@ -0,0 +1,15 @@ +<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_14_10)"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M3.02972 8.59396L8.62219 14.186L14.3703 8.43848L17.1668 11.2346L11.4182 16.982L17.0112 22.5742L14.1371 25.448L8.5441 19.8557L2.79651 25.6035L0 22.8074L5.74813 17.0597L0.155656 11.4678L3.02972 8.59396Z" fill="#023789"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M14.0922 25.5L16.9434 22.6486L16.9423 22.6478L22.6464 16.9456L17.0512 11.3519L17.0518 11.3514L14.2542 8.55418L8.65961 2.95973L11.5114 0.108337L17.106 5.70288L22.8095 0L25.607 2.79722L19.903 8.5L25.4981 14.0943L31.2022 8.39169L33.9997 11.1889L28.2957 16.8914L33.8914 22.4861L31.0396 25.3375L25.4439 19.7428L19.7404 25.4454L25.3361 31.0403L22.4843 33.8917L16.8887 28.2968L11.1862 34L8.38867 31.2028L14.0922 25.5Z" fill="url(#paint0_linear_14_10)"/> +</g> +<defs> +<linearGradient id="paint0_linear_14_10" x1="12.8379" y1="-0.693875" x2="9.54344" y2="31.4337" gradientUnits="userSpaceOnUse"> +<stop stop-color="#01F1FF"/> +<stop offset="1" stop-color="#0197FF"/> +</linearGradient> +<clipPath id="clip0_14_10"> +<rect width="34" height="34" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/package.yaml b/package.yaml deleted file mode 100644 index 4a7a2550fd..0000000000 --- a/package.yaml +++ /dev/null @@ -1,149 +0,0 @@ -name: simplex-chat -version: 5.4.2.1 -#synopsis: -#description: -homepage: https://github.com/simplex-chat/simplex-chat#readme -license: AGPL-3 -author: simplex.chat -maintainer: chat@simplex.chat -copyright: 2020-22 simplex.chat -category: Web, System, Services, Cryptography -extra-source-files: - - README.md - - cabal.project - -dependencies: - - aeson == 2.2.* - - ansi-terminal >= 0.10 && < 0.12 - - async == 2.2.* - - attoparsec == 0.14.* - - base >= 4.7 && < 5 - - base64-bytestring >= 1.0 && < 1.3 - - composition == 1.0.* - - constraints >= 0.12 && < 0.14 - - containers == 0.6.* - - crypton == 0.34.* - - data-default >= 0.7 && < 0.8 - - directory == 1.3.* - - direct-sqlcipher == 2.3.* - - email-validate == 2.3.* - - exceptions == 0.10.* - - filepath == 1.4.* - - http-types == 0.12.* - - http2 >= 4.2.2 && < 4.3 - - memory == 0.18.* - - mtl >= 2.3.1 && < 3.0 - - network >= 3.1.2.7 && < 3.2 - - network-transport == 0.5.6 - - optparse-applicative >= 0.15 && < 0.17 - - process == 1.6.* - - random >= 1.1 && < 1.3 - - record-hasfield == 1.0.* - - simple-logger == 0.1.* - - simplexmq >= 5.0 - - socks == 0.6.* - - sqlcipher-simple == 0.4.* - - stm == 2.5.* - - terminal == 0.2.* - - time == 1.12.* - - tls >= 1.7.0 && < 1.8 - - unliftio == 0.2.* - - unliftio-core == 0.2.* - - zip == 2.0.* - -flags: - swift: - description: Enable swift JSON format - manual: True - default: False - -when: - - condition: flag(swift) - cpp-options: - - -DswiftJSON - - condition: impl(ghc >= 9.6.2) - dependencies: - - bytestring == 0.11.* - - template-haskell == 2.20.* - - text >= 2.0.1 && < 2.2 - - condition: impl(ghc < 9.6.2) - dependencies: - - bytestring == 0.10.* - - template-haskell == 2.16.* - - text >= 1.2.3.0 && < 1.3 - -library: - source-dirs: src - -executables: - simplex-chat: - source-dirs: apps/simplex-chat - main: Main.hs - dependencies: - - simplex-chat - - network == 3.1.* - - websockets == 0.12.* - ghc-options: - - -threaded - - simplex-bot: - source-dirs: apps/simplex-bot - main: Main.hs - dependencies: - - simplex-chat - ghc-options: - - -threaded - - simplex-bot-advanced: - source-dirs: apps/simplex-bot-advanced - main: Main.hs - dependencies: - - simplex-chat - ghc-options: - - -threaded - - simplex-broadcast-bot: - source-dirs: apps/simplex-broadcast-bot/src - main: ../Main.hs - dependencies: - - simplex-chat - ghc-options: - - -threaded - - simplex-directory-service: - source-dirs: apps/simplex-directory-service/src - main: ../Main.hs - dependencies: - - simplex-chat - ghc-options: - - -threaded - -tests: - simplex-chat-test: - source-dirs: - - tests - - apps/simplex-broadcast-bot/src - - apps/simplex-directory-service/src - main: Test.hs - dependencies: - - QuickCheck == 2.14.* - - simplex-chat - - async == 2.2.* - - deepseq == 1.4.* - - generic-random == 1.5.* - - hspec == 2.11.* - - network == 3.1.* - - silently == 1.2.* - - stm == 2.5.* - ghc-options: - - -threaded - -ghc-options: - # - -haddock - - -Wall - - -Wcompat - - -Werror=incomplete-patterns - - -Wredundant-constraints - - -Wincomplete-record-updates - - -Wincomplete-uni-patterns - - -Wunused-type-patterns diff --git a/packages/simplex-chat-client/typescript/examples/squaring-bot.js b/packages/simplex-chat-client/typescript/examples/squaring-bot.js index 5a96dfb205..9651436ffc 100644 --- a/packages/simplex-chat-client/typescript/examples/squaring-bot.js +++ b/packages/simplex-chat-client/typescript/examples/squaring-bot.js @@ -35,15 +35,16 @@ async function run() { ) continue } - case "newChatItem": { + case "newChatItems": { // calculates the square of the number and sends the reply - const {chatInfo} = resp.chatItem - if (chatInfo.type !== ChatInfoType.Direct) continue - const msg = ciContentText(resp.chatItem.chatItem.content) - if (msg) { - const n = +msg - const reply = typeof n === "number" && !isNaN(n) ? `${n} * ${n} = ${n * n}` : `this is not a number` - await chat.apiSendTextMessage(ChatType.Direct, chatInfo.contact.contactId, reply) + for (const {chatInfo, chatItem} of resp.chatItems) { + if (chatInfo.type !== ChatInfoType.Direct) continue + const msg = ciContentText(chatItem.content) + if (msg) { + const n = +msg + const reply = typeof n === "number" && !isNaN(n) ? `${n} * ${n} = ${n * n}` : `this is not a number` + await chat.apiSendTextMessage(ChatType.Direct, chatInfo.contact.contactId, reply) + } } } } diff --git a/packages/simplex-chat-client/typescript/package.json b/packages/simplex-chat-client/typescript/package.json index c8aa6b4f1e..bb2fdda702 100644 --- a/packages/simplex-chat-client/typescript/package.json +++ b/packages/simplex-chat-client/typescript/package.json @@ -1,6 +1,6 @@ { "name": "simplex-chat", - "version": "0.2.0", + "version": "0.2.1", "description": "SimpleX Chat client", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/simplex-chat-client/typescript/src/client.ts b/packages/simplex-chat-client/typescript/src/client.ts index eb3e39f00a..3600147ba7 100644 --- a/packages/simplex-chat-client/typescript/src/client.ts +++ b/packages/simplex-chat-client/typescript/src/client.ts @@ -160,14 +160,14 @@ export class ChatClient { throw new ChatCommandError("error loading chat", r) } - async apiSendMessage(chatType: ChatType, chatId: number, message: CC.ComposedMessage): Promise<CR.AChatItem> { - const r = await this.sendChatCommand({type: "apiSendMessage", chatType, chatId, message}) - if (r.type === "newChatItem") return r.chatItem + async apiSendMessages(chatType: ChatType, chatId: number, messages: CC.ComposedMessage[]): Promise<CR.AChatItem[]> { + const r = await this.sendChatCommand({type: "apiSendMessage", chatType, chatId, messages}) + if (r.type === "newChatItems") return r.chatItems throw new ChatCommandError("unexpected response", r) } - apiSendTextMessage(chatType: ChatType, chatId: number, text: string): Promise<CR.AChatItem> { - return this.apiSendMessage(chatType, chatId, {msgContent: {type: "text", text}}) + async apiSendTextMessage(chatType: ChatType, chatId: number, text: string): Promise<CR.AChatItem[]> { + return this.apiSendMessages(chatType, chatId, [{msgContent: {type: "text", text}}]) } async apiUpdateChatItem(chatType: ChatType, chatId: number, chatItemId: CC.ChatItemId, msgContent: CC.MsgContent): Promise<CR.ChatItem> { diff --git a/packages/simplex-chat-client/typescript/src/command.ts b/packages/simplex-chat-client/typescript/src/command.ts index b49a3605b6..135dc41ba7 100644 --- a/packages/simplex-chat-client/typescript/src/command.ts +++ b/packages/simplex-chat-client/typescript/src/command.ts @@ -12,7 +12,6 @@ export type ChatCommand = | APIStopChat | SetTempFolder | SetFilesFolder - | APISetXFTPConfig | SetIncognito | APIExportArchive | APIImportArchive @@ -30,7 +29,6 @@ export type ChatCommand = | APIRejectContact | APIUpdateProfile | APISetContactAlias - | APIParseMarkdown | NewGroup | APIAddMember | APIJoinGroup @@ -112,7 +110,6 @@ type ChatCommandTag = | "apiStopChat" | "setTempFolder" | "setFilesFolder" - | "apiSetXFTPConfig" | "setIncognito" | "apiExportArchive" | "apiImportArchive" @@ -130,7 +127,6 @@ type ChatCommandTag = | "apiRejectContact" | "apiUpdateProfile" | "apiSetContactAlias" - | "apiParseMarkdown" | "newGroup" | "apiAddMember" | "apiJoinGroup" @@ -242,15 +238,6 @@ export interface SetFilesFolder extends IChatCommand { filePath: string } -export interface APISetXFTPConfig extends IChatCommand { - type: "apiSetXFTPConfig" - config?: XFTPFileConfig -} - -export interface XFTPFileConfig { - minFileSize: number -} - export interface SetIncognito extends IChatCommand { type: "setIncognito" incognito: boolean @@ -288,7 +275,7 @@ export interface APISendMessage extends IChatCommand { type: "apiSendMessage" chatType: ChatType chatId: number - message: ComposedMessage + messages: ComposedMessage[] } export interface ComposedMessage { @@ -366,11 +353,6 @@ export interface APISetContactAlias extends IChatCommand { localAlias: string } -export interface APIParseMarkdown extends IChatCommand { - type: "apiParseMarkdown" - text: string -} - export interface NewGroup extends IChatCommand { type: "newGroup" groupProfile: GroupProfile @@ -707,8 +689,6 @@ export function cmdString(cmd: ChatCommand): string { return `/_temp_folder ${cmd.tempFolder}` case "setFilesFolder": return `/_files_folder ${cmd.filePath}` - case "apiSetXFTPConfig": - return `/_xftp ${onOff(cmd.config)}${maybeJSON(cmd.config)}` case "setIncognito": return `/incognito ${onOff(cmd.incognito)}` case "apiExportArchive": @@ -722,7 +702,7 @@ export function cmdString(cmd: ChatCommand): string { case "apiGetChat": return `/_get chat ${cmd.chatType}${cmd.chatId}${paginationStr(cmd.pagination)}` case "apiSendMessage": - return `/_send ${cmd.chatType}${cmd.chatId} json ${JSON.stringify(cmd.message)}` + return `/_send ${cmd.chatType}${cmd.chatId} json ${JSON.stringify(cmd.messages)}` case "apiUpdateChatItem": return `/_update item ${cmd.chatType}${cmd.chatId} ${cmd.chatItemId} json ${JSON.stringify(cmd.msgContent)}` case "apiDeleteChatItem": @@ -745,8 +725,6 @@ export function cmdString(cmd: ChatCommand): string { return `/_profile ${cmd.userId} ${JSON.stringify(cmd.profile)}` case "apiSetContactAlias": return `/_set alias @${cmd.contactId} ${cmd.localAlias.trim()}` - case "apiParseMarkdown": - return `/_parse ${cmd.text}` case "newGroup": return `/_group ${JSON.stringify(cmd.groupProfile)}` case "apiAddMember": diff --git a/packages/simplex-chat-client/typescript/src/response.ts b/packages/simplex-chat-client/typescript/src/response.ts index b50b2e2943..5f91baa7db 100644 --- a/packages/simplex-chat-client/typescript/src/response.ts +++ b/packages/simplex-chat-client/typescript/src/response.ts @@ -12,7 +12,7 @@ export type ChatResponse = | CRUserProtoServers | CRContactInfo | CRGroupMemberInfo - | CRNewChatItem + | CRNewChatItems | CRChatItemStatusUpdated | CRChatItemUpdated | CRChatItemDeleted @@ -84,7 +84,6 @@ export type ChatResponse = | CRGroupRemoved | CRGroupDeleted | CRGroupUpdated - | CRUserContactLinkSubscribed | CRUserContactLinkSubError | CRContactConnectionDeleted | CRMessageError @@ -109,7 +108,7 @@ type ChatResponseTag = | "userProtoServers" | "contactInfo" | "groupMemberInfo" - | "newChatItem" + | "newChatItems" | "chatItemStatusUpdated" | "chatItemUpdated" | "chatItemDeleted" @@ -182,7 +181,6 @@ type ChatResponseTag = | "groupRemoved" | "groupDeleted" | "groupUpdated" - | "userContactLinkSubscribed" | "userContactLinkSubError" | "newContactConnection" | "contactConnectionDeleted" @@ -255,10 +253,10 @@ export interface CRGroupMemberInfo extends CR { connectionStats_?: ConnectionStats } -export interface CRNewChatItem extends CR { - type: "newChatItem" +export interface CRNewChatItems extends CR { + type: "newChatItems" user: User - chatItem: AChatItem + chatItems: AChatItem[] } export interface CRChatItemStatusUpdated extends CR { @@ -721,10 +719,6 @@ export interface CRGroupUpdated extends CR { member_?: GroupMember } -export interface CRUserContactLinkSubscribed extends CR { - type: "userContactLinkSubscribed" -} - export interface CRUserContactLinkSubError extends CR { type: "userContactLinkSubError" chatError: ChatError diff --git a/packages/simplex-chat-client/typescript/tests/client.test.ts b/packages/simplex-chat-client/typescript/tests/client.test.ts index 2c44406c3d..eef57558f2 100644 --- a/packages/simplex-chat-client/typescript/tests/client.test.ts +++ b/packages/simplex-chat-client/typescript/tests/client.test.ts @@ -26,8 +26,8 @@ describe.skip("ChatClient (expects SimpleX Chat server with a user, without cont assert(r2.type === "contactConnected") const contact1 = (r1 as CR.CRContactConnected).contact // const contact2 = (r2 as C.CRContactConnected).contact - const r3 = await c.apiSendTextMessage(CC.ChatType.CTDirect, contact1.contactId, "hello") - assert(r3.chatItem.content.type === "sndMsgContent" && r3.chatItem.content.msgContent.text === "hello") + const r3 = await c.apiSendTextMessage(CC.ChatType.Direct, contact1.contactId, "hello") + assert(r3[0].chatItem.content.type === "sndMsgContent" && r3[0].chatItem.content.msgContent.text === "hello") const r4 = await c.msgQ.dequeue() assert(isItemSent(r4) || isNewRcvItem(r4)) await c.disconnect() @@ -38,9 +38,9 @@ describe.skip("ChatClient (expects SimpleX Chat server with a user, without cont function isNewRcvItem(r: CR.ChatResponse): boolean { return ( - r.type === "newChatItem" && - r.chatItem.chatItem.content.type === "rcvMsgContent" && - r.chatItem.chatItem.content.msgContent.text === "hello" + r.type === "newChatItems" && + r.chatItems[0].chatItem.content.type === "rcvMsgContent" && + r.chatItems[0].chatItem.content.msgContent.text === "hello" ) } }, 20000) diff --git a/packages/simplex-chat-webrtc/package.json b/packages/simplex-chat-webrtc/package.json index f11ea36343..a6598b6ce7 100644 --- a/packages/simplex-chat-webrtc/package.json +++ b/packages/simplex-chat-webrtc/package.json @@ -27,7 +27,7 @@ "author": "SimpleX Chat", "license": "AGPL-3.0-or-later", "devDependencies": { - "@types/lz-string": "^1.3.34", + "@types/lz-string": "1.3.34", "husky": "^7.0.4", "isomorphic-webcrypto": "^2.3.8", "lint-staged": "^12.4.1", @@ -38,6 +38,6 @@ "**/*": "prettier --write --ignore-unknown" }, "dependencies": { - "lz-string": "^1.4.4" + "lz-string": "1.5.0" } } diff --git a/packages/simplex-chat-webrtc/src/android/call.html b/packages/simplex-chat-webrtc/src/android/call.html index 7b51a05151..51815e2995 100644 --- a/packages/simplex-chat-webrtc/src/android/call.html +++ b/packages/simplex-chat-webrtc/src/android/call.html @@ -6,20 +6,41 @@ <script src="../lz-string.min.js"></script> </head> <body> + <video + id="remote-screen-video-stream" + class="inline" + playsinline + poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" + style="visibility: hidden" + onclick="javascript:toggleRemoteScreenVideoFitFill()" + ></video> + <video id="remote-video-stream" + class="inline" autoplay playsinline poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" onclick="javascript:toggleRemoteVideoFitFill()" ></video> + <video id="local-video-stream" + class="inline" muted autoplay playsinline poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" ></video> + + <video + id="local-screen-video-stream" + class="inline" + muted + playsinline + poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" + style="visibility: hidden" + ></video> </body> <footer> <script src="../call.js"></script> diff --git a/packages/simplex-chat-webrtc/src/android/style.css b/packages/simplex-chat-webrtc/src/android/style.css index 3d2941c71e..377458c184 100644 --- a/packages/simplex-chat-webrtc/src/android/style.css +++ b/packages/simplex-chat-webrtc/src/android/style.css @@ -5,24 +5,143 @@ body { background-color: black; } -#remote-video-stream { +#remote-video-stream.inline { position: absolute; width: 100%; height: 100%; object-fit: cover; } -#local-video-stream { +@media (orientation: portrait) { + #remote-video-stream.collapsed { + position: absolute; + width: 30%; + max-width: 30%; + height: 39.9vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + bottom: 80px; + right: 0; + } +} + +@media (orientation: landscape) { + #remote-video-stream.collapsed { + position: absolute; + width: 20%; + max-width: 20%; + height: 15.03vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + bottom: 80px; + right: 0; + } +} + +@media (orientation: portrait) { + #remote-video-stream.collapsed-pip { + position: absolute; + width: 50%; + max-width: 50%; + height: 66.5vw; + object-fit: cover; + margin: 8px; + border-radius: 8px; + bottom: 0; + right: 0; + } +} + +@media (orientation: landscape) { + #remote-video-stream.collapsed-pip { + position: absolute; + width: 50%; + max-width: 50%; + height: 37.59vw; + object-fit: cover; + margin: 8px; + border-radius: 8px; + bottom: 0; + right: 0; + } +} + +#remote-screen-video-stream.inline { + position: absolute; + width: 100%; + height: 100%; + object-fit: cover; +} + +@media (orientation: portrait) { + #local-video-stream.inline { + position: absolute; + width: 30%; + max-width: 30%; + height: 39.9vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + top: 0; + right: 0; + } +} + +@media (orientation: landscape) { + #local-video-stream.inline { + position: absolute; + width: 20%; + max-width: 20%; + height: 15.03vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + top: 0; + right: 0; + } +} + +#local-screen-video-stream.inline { position: absolute; width: 30%; max-width: 30%; object-fit: cover; margin: 16px; border-radius: 16px; - top: 0; + top: 30%; right: 0; } +#remote-video-stream.fullscreen { + position: absolute; + height: 100%; + width: 100%; + object-fit: cover; +} + +#remote-screen-video-stream.fullscreen { + position: absolute; + height: 100%; + width: 100%; + object-fit: cover; +} + +#local-video-stream.fullscreen { + position: absolute; + height: 100%; + width: 100%; + object-fit: cover; +} + +#local-screen-video-stream.fullscreen { + position: absolute; + height: 100%; + width: 100%; + object-fit: cover; +} + *::-webkit-media-controls { display: none !important; -webkit-appearance: none !important; diff --git a/packages/simplex-chat-webrtc/src/call.ts b/packages/simplex-chat-webrtc/src/call.ts index 7afdbe1003..5f3d2bf332 100644 --- a/packages/simplex-chat-webrtc/src/call.ts +++ b/packages/simplex-chat-webrtc/src/call.ts @@ -9,6 +9,7 @@ interface WVApiMessage { type WCallCommand = | WCCapabilities + | WCPermission | WCStartCall | WCAcceptOffer | WCallAnswer @@ -16,6 +17,7 @@ type WCallCommand = | WCEnableMedia | WCToggleCamera | WCDescription + | WCLayout | WCEndCall type WCallResponse = @@ -25,26 +27,70 @@ type WCallResponse = | WCallIceCandidates | WRConnection | WRCallConnected + | WRPeerMedia | WRCallEnd | WRCallEnded | WROk | WRError | WCAcceptOffer -type WCallCommandTag = "capabilities" | "start" | "offer" | "answer" | "ice" | "media" | "camera" | "description" | "end" +type WCallCommandTag = + | "capabilities" + | "permission" + | "start" + | "offer" + | "answer" + | "ice" + | "media" + | "camera" + | "description" + | "layout" + | "end" -type WCallResponseTag = "capabilities" | "offer" | "answer" | "ice" | "connection" | "connected" | "end" | "ended" | "ok" | "error" +type WCallResponseTag = + | "capabilities" + | "offer" + | "answer" + | "ice" + | "connection" + | "connected" + | "peerMedia" + | "end" + | "ended" + | "ok" + | "error" enum CallMediaType { Audio = "audio", Video = "video", } +enum CallMediaSource { + Mic = "mic", + Camera = "camera", + ScreenAudio = "screenAudio", + ScreenVideo = "screenVideo", + Unknown = "unknown", +} + enum VideoCamera { User = "user", Environment = "environment", } +enum LayoutType { + Default = "default", + LocalVideo = "localVideo", + RemoteVideo = "remoteVideo", +} + +interface CallMediaSources { + mic: boolean + camera: boolean + screenAudio: boolean + screenVideo: boolean +} + interface IWCallCommand { type: WCallCommandTag } @@ -100,7 +146,7 @@ interface WCallIceCandidates extends IWCallCommand, IWCallResponse { interface WCEnableMedia extends IWCallCommand { type: "media" - media: CallMediaType + source: CallMediaSource enable: boolean } @@ -115,6 +161,18 @@ interface WCDescription extends IWCallCommand { description: string } +interface WCLayout extends IWCallCommand { + type: "layout" + layout: LayoutType +} + +interface WCPermission extends IWCallCommand { + type: "permission" + title: string + chrome: string + safari: string +} + interface WRCapabilities extends IWCallResponse { type: "capabilities" capabilities: CallCapabilities @@ -139,6 +197,12 @@ interface WRCallConnected extends IWCallResponse { connectionInfo: ConnectionInfo } +interface WRPeerMedia extends IWCallResponse { + type: "peerMedia" + source: CallMediaSource + enabled: boolean +} + interface WRCallEnd extends IWCallResponse { type: "end" } @@ -162,17 +226,27 @@ interface ConnectionInfo { remoteCandidate?: RTCIceCandidate } +interface VideoElements { + local: HTMLVideoElement + localScreen: HTMLMediaElement + remote: HTMLMediaElement + remoteScreen: HTMLMediaElement +} + // for debugging // var sendMessageToNative = ({resp}: WVApiMessage) => console.log(JSON.stringify({command: resp})) var sendMessageToNative = (msg: WVApiMessage) => console.log(JSON.stringify(msg)) var toggleScreenShare = async () => {} +var localOrPeerMediaSourcesChanged = (_call: Call) => {} +var inactiveCallMediaSourcesChanged = (_inactiveCallMediaSources: CallMediaSources) => {} +var failedToGetPermissions = (_title: string, _description: string) => {} // Global object with cryptrographic/encoding functions const callCrypto = callCryptoFunction() declare var RTCRtpScriptTransform: { prototype: RTCRtpScriptTransform - new (worker: Worker, options?: any): RTCRtpScriptTransform + new (worker: Worker, options?: any, transfer?: any[] | undefined): RTCRtpScriptTransform } enum TransformOperation { @@ -190,23 +264,59 @@ interface WVAPICall { interface Call { connection: RTCPeerConnection iceCandidates: Promise<string> // JSON strings for RTCIceCandidate - localMedia: CallMediaType + localMediaSources: CallMediaSources localCamera: VideoCamera localStream: MediaStream + localScreenStream: MediaStream + // has no tracks in the beggining, see addRemoteTracksWhenUnmuted remoteStream: MediaStream - screenShareEnabled: boolean - cameraEnabled: boolean + remoteTracks: Map<CallMediaSource, MediaStreamTrack> + // has no tracks in the beggining too + remoteScreenStream: MediaStream + peerMediaSources: CallMediaSources aesKey?: string worker?: Worker key?: CryptoKey + layout: LayoutType + // controls whether transceiver's track was replaced already or set initially (in video call) + cameraTrackWasSetBefore: boolean + peerHasOldVersion: boolean } +interface NotConnectedCall { + localCamera: VideoCamera + localStream: MediaStream + layout: LayoutType +} + +function localMedia(call: Call): CallMediaType { + return call.localMediaSources.camera || call.localMediaSources.screenVideo ? CallMediaType.Video : CallMediaType.Audio +} + +function peerMedia(call: Call): CallMediaType { + return call.peerMediaSources.camera || call.peerMediaSources.screenVideo ? CallMediaType.Video : CallMediaType.Audio +} + +let inactiveCallMediaSources: CallMediaSources = { + mic: false, + camera: false, + screenAudio: false, + screenVideo: false, +} let activeCall: Call | undefined +let notConnectedCall: NotConnectedCall | undefined let answerTimeout = 30_000 var useWorker = false var isDesktop = false var localizedState = "" var localizedDescription = "" +// Passing true here will send audio in screen record stream +const allowSendScreenAudio = false +// When one side of a call sends candidates tot fast (until local & remote descriptions are set), that candidates +// will be stored here and then set when the call will be ready to process them +let afterCallInitializedCandidates: RTCIceCandidateInit[] = [] + +const stopTrackOnAndroid = false const processCommand = (function () { type RTCRtpSenderWithEncryption = RTCRtpSender & { @@ -223,8 +333,12 @@ const processCommand = (function () { encodedInsertableStreams: boolean } + type RTCConfigurationWithSdpSemantics = RTCConfiguration & { + sdpSemantics: string + } + interface CallConfig { - peerConnectionConfig: RTCConfigurationWithEncryption + peerConnectionConfig: RTCConfigurationWithEncryption & RTCConfigurationWithSdpSemantics iceCandidates: { delay: number extrasInterval: number @@ -233,9 +347,10 @@ const processCommand = (function () { } const defaultIceServers: RTCIceServer[] = [ + {urls: ["stuns:stun.simplex.im:443"]}, {urls: ["stun:stun.simplex.im:443"]}, - {urls: ["turn:turn.simplex.im:443?transport=udp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"}, - {urls: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"}, + //{urls: ["turns:turn.simplex.im:443?transport=udp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj"}, + {urls: ["turns:turn.simplex.im:443?transport=tcp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj"}, ] function getCallConfig(encodedInsertableStreams: boolean, iceServers?: RTCIceServer[], relay?: boolean): CallConfig { @@ -245,6 +360,8 @@ const processCommand = (function () { iceCandidatePoolSize: 10, encodedInsertableStreams, iceTransportPolicy: relay ? "relay" : "all", + // needed for Android WebView >= 69 && <= 72 where default was "plan-b" which is incompatible with transceivers + sdpSemantics: "unified-plan", }, iceCandidates: { delay: 750, @@ -308,31 +425,76 @@ const processCommand = (function () { } async function initializeCall(config: CallConfig, mediaType: CallMediaType, aesKey?: string): Promise<Call> { - const pc = new RTCPeerConnection(config.peerConnectionConfig) - const remoteStream = new MediaStream() - const localCamera = VideoCamera.User - const localStream = await getLocalMediaStream(mediaType, localCamera) - if (isDesktop) { - localStream - .getTracks() - .filter((elem) => elem.kind == "video") - .forEach((elem) => (elem.enabled = false)) + let pc: RTCPeerConnection + try { + pc = new RTCPeerConnection(config.peerConnectionConfig) + } catch (e) { + console.log("Error while constructing RTCPeerConnection, will try without 'stuns' specified: " + e) + const withoutStuns = config.peerConnectionConfig.iceServers?.filter((elem) => + typeof elem.urls === "string" ? !elem.urls.startsWith("stuns:") : !elem.urls.some((url) => url.startsWith("stuns:")) + ) + config.peerConnectionConfig.iceServers = withoutStuns + pc = new RTCPeerConnection(config.peerConnectionConfig) } + const remoteStream = new MediaStream() + const remoteScreenStream = new MediaStream() + const localCamera = notConnectedCall?.localCamera ?? VideoCamera.User + let localStream: MediaStream + try { + localStream = notConnectedCall?.localStream + ? notConnectedCall.localStream + : await getLocalMediaStream( + inactiveCallMediaSources.mic, + inactiveCallMediaSources.camera && (await browserHasCamera()), + localCamera + ) + } catch (e) { + console.log("Error while getting local media stream", e) + if (isDesktop) { + desktopShowPermissionsAlert(mediaType) + localStream = new MediaStream() + } else { + // On Android all streams should be present + throw e + } + } + const localScreenStream = new MediaStream() + // Will become video when any video tracks will be added const iceCandidates = getIceCandidates(pc, config) - const call = { + const call: Call = { connection: pc, iceCandidates, - localMedia: mediaType, + localMediaSources: { + mic: localStream.getAudioTracks().length > 0, + camera: localStream.getVideoTracks().length > 0, + screenAudio: localScreenStream.getAudioTracks().length > 0, + screenVideo: localScreenStream.getVideoTracks().length > 0, + }, localCamera, localStream, + localScreenStream, remoteStream, + remoteTracks: new Map(), + remoteScreenStream, + peerMediaSources: { + mic: false, + camera: false, + screenAudio: false, + screenVideo: false, + }, aesKey, - screenShareEnabled: false, - cameraEnabled: true, + layout: notConnectedCall?.layout ?? LayoutType.Default, + cameraTrackWasSetBefore: localStream.getVideoTracks().length > 0, + peerHasOldVersion: false, } + localOrPeerMediaSourcesChanged(call) await setupMediaStreams(call) let connectionTimeout: number | undefined = setTimeout(connectionHandler, answerTimeout) - pc.addEventListener("connectionstatechange", connectionStateChange) + if (pc.connectionState) { + pc.addEventListener("connectionstatechange", connectionStateChange) + } else { + pc.addEventListener("iceconnectionstatechange", connectionStateChange) + } return call async function connectionStateChange() { @@ -346,21 +508,35 @@ const processCommand = (function () { resp: { type: "connection", state: { - connectionState: pc.connectionState, + connectionState: + pc.connectionState ?? + (pc.iceConnectionState != "completed" && pc.iceConnectionState != "checking" + ? pc.iceConnectionState + : pc.iceConnectionState == "completed" + ? "connected" + : "connecting") /* webView 69-70 doesn't have connectionState yet */, iceConnectionState: pc.iceConnectionState, iceGatheringState: pc.iceGatheringState, signalingState: pc.signalingState, }, }, }) - if (pc.connectionState == "disconnected" || pc.connectionState == "failed") { + if ( + pc.connectionState == "disconnected" || + pc.connectionState == "failed" || + (!pc.connectionState && (pc.iceConnectionState == "disconnected" || pc.iceConnectionState == "failed")) + ) { clearConnectionTimeout() - pc.removeEventListener("connectionstatechange", connectionStateChange) + if (pc.connectionState) { + pc.removeEventListener("connectionstatechange", connectionStateChange) + } else { + pc.removeEventListener("iceconnectionstatechange", connectionStateChange) + } if (activeCall) { setTimeout(() => sendMessageToNative({resp: {type: "ended"}}), 0) } endCall() - } else if (pc.connectionState == "connected") { + } else if (pc.connectionState == "connected" || (!pc.connectionState && pc.iceConnectionState == "connected")) { clearConnectionTimeout() const stats = (await pc.getStats()) as Map<string, any> for (const stat of stats.values()) { @@ -407,21 +583,66 @@ const processCommand = (function () { case "capabilities": console.log("starting outgoing call - capabilities") if (activeCall) endCall() - // This request for local media stream is made to prompt for camera/mic permissions on call start - if (command.media) await getLocalMediaStream(command.media, VideoCamera.User) + + let localStream: MediaStream | null = null + try { + localStream = await getLocalMediaStream( + true, + command.media == CallMediaType.Video && (await browserHasCamera()), + VideoCamera.User + ) + const videos = getVideoElements() + if (videos) { + setupLocalVideoRatio(videos.local) + videos.local.srcObject = localStream + videos.local.play().catch((e) => console.log(e)) + } + } catch (e) { + console.log(e) + // Do not allow to continue the call without audio permission + resp = {type: "error", message: "capabilities: no permissions were granted for mic and/or camera"} + break + localStream = new MediaStream() + // Will be shown on the next stage of call estabilishing, can work without any streams + //desktopShowPermissionsAlert(command.media) + } + // Specify defaults that can be changed via UI before call estabilished. It's only used before activeCall instance appears + inactiveCallMediaSources.mic = localStream != null && localStream.getAudioTracks().length > 0 + inactiveCallMediaSources.camera = localStream != null && localStream.getVideoTracks().length > 0 + inactiveCallMediaSourcesChanged(inactiveCallMediaSources) + notConnectedCall = { + localCamera: VideoCamera.User, + localStream: localStream, + layout: LayoutType.Default, + } const encryption = supportsInsertableStreams(useWorker) resp = {type: "capabilities", capabilities: {encryption}} break case "start": { console.log("starting incoming call - create webrtc session") if (activeCall) endCall() + + // It can be already defined on Android when switching calls (if the previous call was outgoing) + notConnectedCall = undefined + inactiveCallMediaSources.mic = true + inactiveCallMediaSources.camera = command.media == CallMediaType.Video + inactiveCallMediaSourcesChanged(inactiveCallMediaSources) + setupLocalVideoRatio(getVideoElements()!.local) + const {media, iceServers, relay} = command const encryption = supportsInsertableStreams(useWorker) const aesKey = encryption ? command.aesKey : undefined activeCall = await initializeCall(getCallConfig(encryption && !!aesKey, iceServers, relay), media, aesKey) + await setupLocalStream(true, activeCall) + setupCodecPreferences(activeCall) + const pc = activeCall.connection const offer = await pc.createOffer() await pc.setLocalDescription(offer) + // should be called after setLocalDescription in order to have transceiver.mid set + setupEncryptionForLocalStream(activeCall) + addIceCandidates(pc, afterCallInitializedCandidates) + afterCallInitializedCandidates = [] // for debugging, returning the command for callee to use // resp = { // type: "offer", @@ -454,10 +675,26 @@ const processCommand = (function () { activeCall = await initializeCall(getCallConfig(!!aesKey, iceServers, relay), media, aesKey) const pc = activeCall.connection // console.log("offer remoteIceCandidates", JSON.stringify(remoteIceCandidates)) - await pc.setRemoteDescription(new RTCSessionDescription(offer)) - const answer = await pc.createAnswer() + await pc.setRemoteDescription(new RTCSessionDescription(!webView69Or70() ? offer : adaptSdpToOldWebView(offer))) + // setting up local stream only after setRemoteDescription in order to have transceivers set + await setupLocalStream(false, activeCall) + setupEncryptionForLocalStream(activeCall) + setupCodecPreferences(activeCall) + + // enable using the same transceivers for sending media too, so total number of transceivers will be: audio, camera, screen audio, screen video + pc.getTransceivers().forEach((elem) => (elem.direction = "sendrecv")) + // setting media streams after remote description in order to have all transceivers ready (so ordering will be preserved) + // console.log( + // "Transceivers", + // pc.getTransceivers(), + // pc.getTransceivers().map((elem) => "" + elem.mid + " " + elem.sender.track?.kind + " " + elem.sender.track?.label) + // ) + adaptToOldVersion(pc.getTransceivers().length <= 2, activeCall) + let answer = await pc.createAnswer() await pc.setLocalDescription(answer) addIceCandidates(pc, remoteIceCandidates) + addIceCandidates(pc, afterCallInitializedCandidates) + afterCallInitializedCandidates = [] // same as command for caller to use resp = { type: "answer", @@ -478,36 +715,85 @@ const processCommand = (function () { const answer: RTCSessionDescriptionInit = parse(command.answer) const remoteIceCandidates: RTCIceCandidateInit[] = parse(command.iceCandidates) // console.log("answer remoteIceCandidates", JSON.stringify(remoteIceCandidates)) - await pc.setRemoteDescription(new RTCSessionDescription(answer)) + + await pc.setRemoteDescription(new RTCSessionDescription(!webView69Or70() ? answer : adaptSdpToOldWebView(answer))) + adaptToOldVersion(pc.getTransceivers()[2].currentDirection == "sendonly", activeCall!) addIceCandidates(pc, remoteIceCandidates) + addIceCandidates(pc, afterCallInitializedCandidates) + afterCallInitializedCandidates = [] resp = {type: "ok"} } break case "ice": + const remoteIceCandidates: RTCIceCandidateInit[] = parse(command.iceCandidates) if (pc) { - const remoteIceCandidates: RTCIceCandidateInit[] = parse(command.iceCandidates) addIceCandidates(pc, remoteIceCandidates) resp = {type: "ok"} } else { - resp = {type: "error", message: "ice: call not started"} + afterCallInitializedCandidates.push(...remoteIceCandidates) + resp = {type: "error", message: "ice: call not started yet, will add candidates later"} } break case "media": if (!activeCall) { - resp = {type: "error", message: "media: call not started"} - } else if (activeCall.localMedia == CallMediaType.Audio && command.media == CallMediaType.Video) { - resp = {type: "error", message: "media: no video"} - } else { - enableMedia(activeCall.localStream, command.media, command.enable) + if (!notConnectedCall) { + // call can have a slow startup and be in this place even before "capabilities" stage + resp = {type: "error", message: "media: call has not yet pass capabilities stage"} + break + } + switch (command.source) { + case CallMediaSource.Mic: + inactiveCallMediaSources.mic = command.enable + break + case CallMediaSource.Camera: + inactiveCallMediaSources.camera = command.enable + break + case CallMediaSource.ScreenAudio: + inactiveCallMediaSources.screenAudio = command.enable + break + case CallMediaSource.ScreenVideo: + inactiveCallMediaSources.screenVideo = command.enable + break + } + inactiveCallMediaSourcesChanged(inactiveCallMediaSources) + recreateLocalStreamWhileNotConnected(notConnectedCall?.localCamera ?? VideoCamera.User) resp = {type: "ok"} + } else if (activeCall.peerHasOldVersion && command.source == CallMediaSource.Camera && !activeCall.peerMediaSources.camera) { + resp = {type: "error", message: "media: cannot enable camera since the peer has an old version"} + } else if (!activeCall.cameraTrackWasSetBefore && command.source == CallMediaSource.Camera && command.enable) { + await startSendingCamera(activeCall, activeCall.localCamera) + resp = {type: "ok"} + } else if ( + (command.source == CallMediaSource.Mic && activeCall.localStream.getAudioTracks().length > 0) || + (command.source == CallMediaSource.Camera && activeCall.localStream.getVideoTracks().length > 0) + ) { + if (enableMedia(activeCall.localStream, command.source, command.enable)) { + resp = {type: "ok"} + } else { + resp = {type: "error", message: "media: cannot enable media source"} + } + } else { + if (await replaceMedia(activeCall, command.source, command.enable, activeCall.localCamera)) { + resp = {type: "ok"} + } else { + resp = {type: "error", message: "media: cannot replace media source"} + } } break case "camera": if (!activeCall || !pc) { - resp = {type: "error", message: "camera: call not started"} + if (notConnectedCall) { + recreateLocalStreamWhileNotConnected(command.camera) + resp = {type: "ok"} + } else { + resp = {type: "error", message: "camera: call has not yet pass capabilities stage"} + } } else { - await replaceMedia(activeCall, command.camera) - resp = {type: "ok"} + if (await replaceMedia(activeCall, CallMediaSource.Camera, true, command.camera)) { + resp = {type: "ok"} + } else { + resp = {type: "error", message: "camera: cannot replace media source"} + } } break case "description": @@ -515,10 +801,23 @@ const processCommand = (function () { localizedDescription = command.description resp = {type: "ok"} break + case "layout": + if (activeCall) { + activeCall.layout = command.layout + } else if (notConnectedCall) { + notConnectedCall.layout = command.layout + } + changeLayout(command.layout) + resp = {type: "ok"} + break case "end": endCall() resp = {type: "ok"} break + case "permission": + failedToGetPermissions(command.title, permissionDescription(command)) + resp = {type: "ok"} + break default: resp = {type: "error", message: "unknown command"} break @@ -532,12 +831,12 @@ const processCommand = (function () { } function endCall() { + shutdownCameraAndMic() try { activeCall?.connection?.close() } catch (e) { console.log(e) } - shutdownCameraAndMic() activeCall = undefined resetVideoElements() } @@ -553,13 +852,15 @@ const processCommand = (function () { const videos = getVideoElements() if (!videos) throw Error("no video elements") await setupEncryptionWorker(call) - setupLocalStream(call) setupRemoteStream(call) - setupCodecPreferences(call) - // setupVideoElement(videos.local) - // setupVideoElement(videos.remote) - videos.local.srcObject = call.localStream + + videos.localScreen.srcObject = call.localScreenStream videos.remote.srcObject = call.remoteStream + videos.remoteScreen.srcObject = call.remoteScreenStream + // videos.localScreen.play() + // For example, exception can be: NotAllowedError: play() failed because the user didn't interact with the document first + videos.remote.play().catch((e) => console.log(e)) + videos.remoteScreen.play().catch((e) => console.log(e)) } async function setupEncryptionWorker(call: Call) { @@ -574,20 +875,95 @@ const processCommand = (function () { } } - function setupLocalStream(call: Call) { + async function setupLocalStream(incomingCall: boolean, call: Call) { const videos = getVideoElements() if (!videos) throw Error("no video elements") const pc = call.connection let {localStream} = call - for (const track of localStream.getTracks()) { - pc.addTrack(track, localStream) + const transceivers = call.connection.getTransceivers() + const audioTracks = localStream.getAudioTracks() + const videoTracks = localStream.getVideoTracks() + + if (incomingCall) { + // incoming call, no transceivers yet. But they should be added in order: mic, camera, screen audio, screen video + // mid = 0 + const audioTransceiver = pc.addTransceiver("audio", {streams: [localStream]}) + if (audioTracks.length != 0) { + audioTransceiver.sender.replaceTrack(audioTracks[0]) + } + // mid = 1 + const videoTransceiver = pc.addTransceiver("video", {streams: [localStream]}) + if (videoTracks.length != 0) { + videoTransceiver.sender.replaceTrack(videoTracks[0]) + } + if (call.localScreenStream.getAudioTracks().length == 0) { + // mid = 2 + pc.addTransceiver("audio", {streams: [call.localScreenStream]}) + } + if (call.localScreenStream.getVideoTracks().length == 0) { + // mid = 3 + pc.addTransceiver("video", {streams: [call.localScreenStream]}) + } + } else { + // new version + if (transceivers.length > 2) { + // Outgoing call. All transceivers are ready. Don't addTrack() because it will create new transceivers, replace existing (null) tracks + await transceivers + .find((elem) => mediaSourceFromTransceiverMid(elem.mid) == CallMediaSource.Mic) + ?.sender?.replaceTrack(audioTracks[0]) + await transceivers + .find((elem) => mediaSourceFromTransceiverMid(elem.mid) == CallMediaSource.Camera) + ?.sender?.replaceTrack(videoTracks[0]) + } else { + // old version, only two transceivers + for (const track of localStream.getTracks()) { + pc.addTrack(track, localStream) + } + } } + // src can be set to notConnectedCall.localStream which is the same as call.localStream + if (!videos.local.srcObject) { + videos.local.srcObject = call.localStream + } + // Without doing it manually Firefox shows black screen but video can be played in Picture-in-Picture + videos.local.play().catch((e) => console.log(e)) + } + + function setupLocalVideoRatio(local: HTMLVideoElement) { + local.addEventListener("loadedmetadata", function () { + console.log("Local video videoWidth: " + local.videoWidth + "px, videoHeight: " + local.videoHeight + "px") + if (local.videoWidth == 0 || local.videoHeight == 0) return + const ratio = local.videoWidth > local.videoHeight ? 0.2 : 0.3 + local.style.height = `${(ratio / (local.videoWidth / local.videoHeight)) * 100}vw` + }) + + local.onresize = function () { + console.log("Local video size changed to " + local.videoWidth + "x" + local.videoHeight) + if (local.videoWidth == 0 || local.videoHeight == 0) return + const ratio = local.videoWidth > local.videoHeight ? 0.2 : 0.3 + local.style.height = `${(ratio / (local.videoWidth / local.videoHeight)) * 100}vw` + } + } + + function setupEncryptionForLocalStream(call: Call) { if (call.aesKey && call.key) { + const pc = call.connection console.log("set up encryption for sending") - for (const sender of pc.getSenders() as RTCRtpSenderWithEncryption[]) { - setupPeerTransform(TransformOperation.Encrypt, sender, call.worker, call.aesKey, call.key) + let mid = 0 + for (const transceiver of pc.getTransceivers()) { + const sender = transceiver.sender as RTCRtpSenderWithEncryption + const source = mediaSourceFromTransceiverMid(mid.toString()) + setupPeerTransform( + TransformOperation.Encrypt, + sender, + call.worker, + call.aesKey, + call.key, + source == CallMediaSource.Camera || source == CallMediaSource.ScreenVideo ? CallMediaType.Video : CallMediaType.Audio + ) + mid++ } } } @@ -596,16 +972,24 @@ const processCommand = (function () { // Pull tracks from remote stream as they arrive add them to remoteStream video const pc = call.connection pc.ontrack = (event) => { + const track = event.track + //console.log("On track", event) try { if (call.aesKey && call.key) { console.log("set up decryption for receiving") - setupPeerTransform(TransformOperation.Decrypt, event.receiver as RTCRtpReceiverWithEncryption, call.worker, call.aesKey, call.key) - } - for (const stream of event.streams) { - for (const track of stream.getTracks()) { - call.remoteStream.addTrack(track) - } + setupPeerTransform( + TransformOperation.Decrypt, + event.receiver as RTCRtpReceiverWithEncryption, + call.worker, + call.aesKey, + call.key, + event.receiver.track.kind == "video" ? CallMediaType.Video : CallMediaType.Audio + ) } + setupMuteUnmuteListener(event.transceiver, track) + + const mediaSource = mediaSourceFromTransceiverMid(event.transceiver.mid) + call.remoteTracks.set(mediaSource, track) console.log(`ontrack success`) } catch (e) { console.log(`ontrack error: ${(e as Error).message}`) @@ -627,58 +1011,236 @@ const processCommand = (function () { // For opus (where encodedFrame.type is not set) this is the TOC byte from // https://tools.ietf.org/html/rfc6716#section-3.1 - const capabilities = RTCRtpSender.getCapabilities("video") + // Using RTCRtpReceiver instead of RTCRtpSender, see these lines: + // - if (!is_recv_codec && !is_send_codec) { + // + if (!is_recv_codec) { + // https://webrtc.googlesource.com/src.git/+/db2f52ba88cf9f98211df2dabb3f8aca9251c4a2%5E%21/ + const capabilities = RTCRtpReceiver.getCapabilities("video") if (capabilities) { const {codecs} = capabilities const selectedCodecIndex = codecs.findIndex((c) => c.mimeType === "video/VP8") const selectedCodec = codecs[selectedCodecIndex] codecs.splice(selectedCodecIndex, 1) codecs.unshift(selectedCodec) + // On this stage transceiver.mid may not be set so using a sequence starting from 0 to decide which track.kind is inside + let mid = 0 for (const t of call.connection.getTransceivers()) { - if (t.sender.track?.kind === "video") { - t.setCodecPreferences(codecs) + // Firefox doesn't have this function implemented: + // https://bugzilla.mozilla.org/show_bug.cgi?id=1396922 + const source = mediaSourceFromTransceiverMid(mid.toString()) + if ((source == CallMediaSource.Camera || source == CallMediaSource.ScreenVideo) && t.setCodecPreferences) { + try { + t.setCodecPreferences(codecs) + } catch (error) { + // Shouldn't be here but in case something goes wrong, it will allow to make a call with auto-selected codecs + console.log("Failed to set codec preferences, trying without any preferences: " + error) + } } + mid++ } } } - async function replaceMedia(call: Call, camera: VideoCamera): Promise<void> { + async function startSendingCamera(call: Call, camera: VideoCamera): Promise<void> { + console.log("Starting sending video...") const videos = getVideoElements() if (!videos) throw Error("no video elements") const pc = call.connection - const oldAudioTracks = call.localStream.getAudioTracks() - const audioWasEnabled = oldAudioTracks.some((elem) => elem.enabled) - let localStream: MediaStream - try { - localStream = call.screenShareEnabled ? await getLocalScreenCaptureStream() : await getLocalMediaStream(call.localMedia, camera) - } catch (e: any) { - if (call.screenShareEnabled) { - call.screenShareEnabled = false - } + // Taking the first video transceiver and use it for sending video from camera. Following tracks are for other purposes + const tc = pc.getTransceivers().find((tc) => tc.receiver.track.kind == "video" && tc.direction == "sendrecv") + if (!tc) { + console.log("No camera transceiver. Probably, calling to an old version") return } - for (const t of call.localStream.getTracks()) t.stop() - call.localCamera = camera - - const audioTracks = localStream.getAudioTracks() - const videoTracks = localStream.getVideoTracks() - if (!audioWasEnabled && oldAudioTracks.length > 0) { - audioTracks.forEach((elem) => (elem.enabled = false)) - } - if (!call.cameraEnabled && !call.screenShareEnabled) { - videoTracks.forEach((elem) => (elem.enabled = false)) + console.log(pc.getTransceivers().map((elem) => "" + elem.sender.track?.kind + " " + elem.receiver.track?.kind + " " + elem.direction)) + let localStream: MediaStream + try { + localStream = await getLocalMediaStream(call.localMediaSources.mic, true, camera) + for (const t of localStream.getVideoTracks()) { + call.localStream.addTrack(t) + tc?.sender.replaceTrack(t) + localStream.removeTrack(t) + } + call.localMediaSources.camera = true + call.cameraTrackWasSetBefore = true + localOrPeerMediaSourcesChanged(call) + changeLayout(call.layout) + } catch (e: any) { + console.log("Start sending camera error", e) + desktopShowPermissionsAlert(CallMediaType.Video) + return } - replaceTracks(pc, audioTracks) - replaceTracks(pc, videoTracks) - call.localStream = localStream - videos.local.srcObject = localStream + // Without doing it manually Firefox shows black screen but video can be played in Picture-in-Picture + videos.local.play().catch((e) => console.log(e)) } - function replaceTracks(pc: RTCPeerConnection, tracks: MediaStreamTrack[]) { - if (!tracks.length) return - const sender = pc.getSenders().find((s) => s.track?.kind === tracks[0].kind) - if (sender) for (const t of tracks) sender.replaceTrack(t) + toggleScreenShare = async function () { + const call = activeCall + if (!call) return + + const videos = getVideoElements() + if (!videos) throw Error("no video elements") + const pc = call.connection + + if (!call.localMediaSources.screenVideo) { + let localScreenStream: MediaStream + try { + localScreenStream = await getLocalScreenCaptureStream() + } catch (e: any) { + return + } + + for (const t of localScreenStream.getTracks()) call.localScreenStream.addTrack(t) + for (const t of localScreenStream.getTracks()) localScreenStream.removeTrack(t) + + pc.getTransceivers().forEach((elem) => { + const source = mediaSourceFromTransceiverMid(elem.mid) + const screenAudioTrack = call.localScreenStream.getTracks().find((elem) => elem.kind == "audio") + const screenVideoTrack = call.localScreenStream.getTracks().find((elem) => elem.kind == "video") + if (source == CallMediaSource.ScreenAudio && screenAudioTrack) { + elem.sender.replaceTrack(screenAudioTrack) + console.log("Replaced audio screen track") + } else if (source == CallMediaSource.ScreenVideo && screenVideoTrack) { + elem.sender.replaceTrack(screenVideoTrack) + screenVideoTrack.onended = () => { + console.log("Ended screen video track") + toggleScreenShare() + } + console.log("Replaced screen video track") + } + }) + // videos.localScreen.pause() + // videos.localScreen.srcObject = call.localScreenStream + videos.localScreen.play().catch((e) => console.log(e)) + } else { + pc.getTransceivers().forEach((elem) => { + const source = mediaSourceFromTransceiverMid(elem.mid) + if (source == CallMediaSource.ScreenAudio || source == CallMediaSource.ScreenVideo) { + elem.sender.replaceTrack(null) + } + }) + for (const t of call.localScreenStream.getTracks()) t.stop() + for (const t of call.localScreenStream.getTracks()) call.localScreenStream.removeTrack(t) + } + + if (allowSendScreenAudio) { + call.localMediaSources.screenAudio = !call.localMediaSources.screenAudio + } + call.localMediaSources.screenVideo = !call.localMediaSources.screenVideo + localOrPeerMediaSourcesChanged(call) + changeLayout(call.layout) + } + + async function replaceMedia(call: Call, source: CallMediaSource, enable: boolean, camera: VideoCamera): Promise<boolean> { + const videos = getVideoElements() + if (!videos) throw Error("no video elements") + const pc = call.connection + // disabling track first, then asking for a new one. + // doing it vice versa gives an error like "too many cameras were open" on some Android devices or webViews + // which means the second camera will never be opened + for (const t of source == CallMediaSource.Mic ? call.localStream.getAudioTracks() : call.localStream.getVideoTracks()) { + if (isDesktop || source != CallMediaSource.Mic || stopTrackOnAndroid) t.stop() + else t.enabled = false + call.localStream.removeTrack(t) + } + let localStream: MediaStream + try { + localStream = await getLocalMediaStream( + source == CallMediaSource.Mic ? enable : false, + source == CallMediaSource.Camera ? enable : false, + camera + ) + } catch (e: any) { + console.log("Replace media error", e) + desktopShowPermissionsAlert(source == CallMediaSource.Mic ? CallMediaType.Audio : CallMediaType.Video) + return false + } + for (const t of localStream.getTracks()) { + call.localStream.addTrack(t) + localStream.removeTrack(t) + } + call.localCamera = camera + + const audioTracks = call.localStream.getAudioTracks() + const videoTracks = call.localStream.getVideoTracks() + + replaceTracks(pc, CallMediaSource.Mic, audioTracks) + replaceTracks(pc, CallMediaSource.Camera, videoTracks) + + videos.local.play().catch((e) => console.log("replace media: local play", JSON.stringify(e))) + + call.localMediaSources.mic = call.localStream.getAudioTracks().length > 0 + call.localMediaSources.camera = call.localStream.getVideoTracks().length > 0 + localOrPeerMediaSourcesChanged(call) + changeLayout(call.layout) + + return true + } + + function replaceTracks(pc: RTCPeerConnection, source: CallMediaSource, tracks: MediaStreamTrack[]) { + const sender = pc.getTransceivers().find((elem) => mediaSourceFromTransceiverMid(elem.mid) == source)?.sender + if (sender) { + if (tracks.length > 0) + for (const t of tracks) { + console.log("Replaced media track") + sender.replaceTrack(t) + } + else { + console.log("Media track set to null") + sender.replaceTrack(null) + } + } + } + + async function recreateLocalStreamWhileNotConnected(newCamera: VideoCamera) { + const videos = getVideoElements() + const localStream = notConnectedCall?.localStream + const oldCamera = notConnectedCall?.localCamera + if (!localStream || !oldCamera || !videos) return + + if (!inactiveCallMediaSources.mic) { + localStream.getAudioTracks().forEach((elem) => (isDesktop || stopTrackOnAndroid ? elem.stop() : (elem.enabled = false))) + localStream.getAudioTracks().forEach((elem) => localStream.removeTrack(elem)) + } + if (!inactiveCallMediaSources.camera || oldCamera != newCamera) { + localStream.getVideoTracks().forEach((elem) => elem.stop()) + localStream.getVideoTracks().forEach((elem) => localStream.removeTrack(elem)) + } + await getLocalMediaStream( + inactiveCallMediaSources.mic && localStream.getAudioTracks().length == 0, + inactiveCallMediaSources.camera && (localStream.getVideoTracks().length == 0 || oldCamera != newCamera) && (await browserHasCamera()), + newCamera + ) + .then((stream) => { + stream.getTracks().forEach((elem) => { + localStream.addTrack(elem) + stream.removeTrack(elem) + }) + if (notConnectedCall && localStream.getVideoTracks().length > 0) { + notConnectedCall.localCamera = newCamera + } + }) + .catch((e) => console.log("Error while enabling camera in not connected call", e)) + if (!videos.local.srcObject && localStream.getTracks().length > 0) { + videos.local.srcObject = localStream + } + videos.local.play().catch((e) => console.log(e)) + } + + function mediaSourceFromTransceiverMid(mid: string | null) { + switch (mid) { + case "0": + return CallMediaSource.Mic + case "1": + return CallMediaSource.Camera + case "2": + return CallMediaSource.ScreenAudio + case "3": + return CallMediaSource.ScreenVideo + default: + return CallMediaSource.Unknown + } } function setupPeerTransform( @@ -686,16 +1248,17 @@ const processCommand = (function () { peer: RTCRtpReceiverWithEncryption | RTCRtpSenderWithEncryption, worker: Worker | undefined, aesKey: string, - key: CryptoKey + key: CryptoKey, + media: CallMediaType ) { if (worker && "RTCRtpScriptTransform" in window) { console.log(`${operation} with worker & RTCRtpScriptTransform`) - peer.transform = new RTCRtpScriptTransform(worker, {operation, aesKey}) + peer.transform = new RTCRtpScriptTransform(worker, {operation, aesKey, media}) } else if ("createEncodedStreams" in peer) { const {readable, writable} = peer.createEncodedStreams() if (worker) { console.log(`${operation} with worker`) - worker.postMessage({operation, readable, writable, aesKey}, [readable, writable] as unknown as Transferable[]) + worker.postMessage({operation, readable, writable, aesKey, media}, [readable, writable] as unknown as Transferable[]) } else { console.log(`${operation} without worker`) const transform = callCrypto.transformFrame[operation](key) @@ -706,9 +1269,117 @@ const processCommand = (function () { } } - function getLocalMediaStream(mediaType: CallMediaType, facingMode: VideoCamera): Promise<MediaStream> { - const constraints = callMediaConstraints(mediaType, facingMode) - return navigator.mediaDevices.getUserMedia(constraints) + function setupMuteUnmuteListener(transceiver: RTCRtpTransceiver, track: MediaStreamTrack) { + // console.log("Setting up mute/unmute listener in the call without encryption for mid = ", transceiver.mid) + let inboundStatsId = "" + let lastBytesReceived = 0 + // muted initially + let mutedSeconds = 4 + let statsInterval = setInterval(async () => { + const stats: RTCStatsReport = await transceiver.receiver.getStats() + if (!inboundStatsId) { + stats.forEach((elem) => { + if (elem.type == "inbound-rtp") { + inboundStatsId = elem.id + } + }) + } + if (inboundStatsId) { + // even though MSDN site says `bytesReceived` is available in WebView 80+, in reality it's available even in 69 + const bytes = (stats as any).get(inboundStatsId)?.bytesReceived + if (bytes <= lastBytesReceived) { + mutedSeconds++ + if (mutedSeconds == 3) { + onMediaMuteUnmute(transceiver.mid, true) + } + } else { + if (mutedSeconds >= 3) { + onMediaMuteUnmute(transceiver.mid, false) + } + lastBytesReceived = bytes + mutedSeconds = 0 + } + } + }, 1000) + track.onended = (_) => { + clearInterval(statsInterval) + } + } + + function onMediaMuteUnmute(transceiverMid: string | null, mute: boolean) { + const videos = getVideoElements() + if (!videos) throw Error("no video elements") + if (!activeCall) return + + const source = mediaSourceFromTransceiverMid(transceiverMid) + console.log(`Mute/unmute ${source} track = ${mute} with mid = ${transceiverMid}`) + const sources = activeCall.peerMediaSources + if (source == CallMediaSource.Mic && activeCall.peerMediaSources.mic == mute) { + const resp: WRPeerMedia = { + type: "peerMedia", + source: source, + enabled: !mute, + } + sources.mic = !mute + activeCall.peerMediaSources = sources + sendMessageToNative({resp: resp}) + if (!mute) videos.remote.play().catch((e) => console.log(e)) + } else if (source == CallMediaSource.Camera && activeCall.peerMediaSources.camera == mute) { + const resp: WRPeerMedia = { + type: "peerMedia", + source: source, + enabled: !mute, + } + sources.camera = !mute + activeCall.peerMediaSources = sources + sendMessageToNative({resp: resp}) + if (!mute) videos.remote.play().catch((e) => console.log(e)) + } else if (source == CallMediaSource.ScreenAudio && activeCall.peerMediaSources.screenAudio == mute) { + const resp: WRPeerMedia = { + type: "peerMedia", + source: source, + enabled: !mute, + } + sources.screenAudio = !mute + activeCall.peerMediaSources = sources + sendMessageToNative({resp: resp}) + if (!mute) videos.remoteScreen.play().catch((e) => console.log(e)) + } else if (source == CallMediaSource.ScreenVideo && activeCall.peerMediaSources.screenVideo == mute) { + const resp: WRPeerMedia = { + type: "peerMedia", + source: source, + enabled: !mute, + } + sources.screenVideo = !mute + activeCall.peerMediaSources = sources + sendMessageToNative({resp: resp}) + if (!mute) videos.remoteScreen.play().catch((e) => console.log(e)) + } + if (!mute) addRemoteTracksWhenUnmuted(source, activeCall) + localOrPeerMediaSourcesChanged(activeCall) + // Make sure that remote camera and remote screen video in their places and shown/hidden based on layout type currently in use + changeLayout(activeCall.layout) + } + + /* + When new remote tracks are coming, they don't get added to remote streams. They are stored in a map and once any of them "unmuted", + that track is added to the stream. Such workaround needed because Safari doesn't play one stream + if another one is not playing too, eg. no audio if only audio is playing while video track is present too but muted. + But we have possibility to have only one currently active track, even no active track at all. + */ + function addRemoteTracksWhenUnmuted(source: CallMediaSource, call: Call) { + const track = call.remoteTracks.get(source) + if (track) { + const stream = source == CallMediaSource.Mic || source == CallMediaSource.Camera ? call.remoteStream : call.remoteScreenStream + stream.addTrack(track) + call.remoteTracks.delete(source) + } + } + + async function getLocalMediaStream(mic: boolean, camera: boolean, facingMode: VideoCamera): Promise<MediaStream> { + if (!mic && !camera) return new MediaStream() + const constraints = callMediaConstraints(mic, camera, facingMode) + return await navigator.mediaDevices.getUserMedia(constraints) } function getLocalScreenCaptureStream(): Promise<MediaStream> { @@ -722,19 +1393,31 @@ const processCommand = (function () { //}, //aspectRatio: 1.33, }, - audio: true, + audio: allowSendScreenAudio, + // This works with Chrome, Edge, Opera, but not with Firefox and Safari + // systemAudio: "include" } return navigator.mediaDevices.getDisplayMedia(constraints) } - function callMediaConstraints(mediaType: CallMediaType, facingMode: VideoCamera): MediaStreamConstraints { - switch (mediaType) { - case CallMediaType.Audio: - return {audio: true, video: false} - case CallMediaType.Video: - return { - audio: true, - video: { + async function browserHasCamera(): Promise<boolean> { + try { + const devices = await navigator.mediaDevices.enumerateDevices() + const hasCamera = devices.some((elem) => elem.kind == "videoinput") + console.log("Camera is available: " + hasCamera) + return hasCamera + } catch (error) { + console.log("Error while enumerating devices: " + error, error) + return false + } + } + + function callMediaConstraints(mic: boolean, camera: boolean, facingMode: VideoCamera): MediaStreamConstraints { + return { + audio: mic, + video: !camera + ? false + : { frameRate: 24, width: { min: 480, @@ -744,7 +1427,6 @@ const processCommand = (function () { aspectRatio: 1.33, facingMode, }, - } } } @@ -755,14 +1437,10 @@ const processCommand = (function () { ) } - interface VideoElements { - local: HTMLMediaElement - remote: HTMLMediaElement - } - function shutdownCameraAndMic() { - if (activeCall?.localStream) { + if (activeCall) { activeCall.localStream.getTracks().forEach((track) => track.stop()) + activeCall.localScreenStream.getTracks().forEach((track) => track.stop()) } } @@ -770,14 +1448,9 @@ const processCommand = (function () { const videos = getVideoElements() if (!videos) return videos.local.srcObject = null + videos.localScreen.srcObject = null videos.remote.srcObject = null - } - - function getVideoElements(): VideoElements | undefined { - const local = document.getElementById("local-video-stream") - const remote = document.getElementById("remote-video-stream") - if (!(local && remote && local instanceof HTMLMediaElement && remote instanceof HTMLMediaElement)) return - return {local, remote} + videos.remoteScreen.srcObject = null } // function setupVideoElement(video: HTMLElement) { @@ -788,19 +1461,89 @@ const processCommand = (function () { // } // } - function enableMedia(s: MediaStream, media: CallMediaType, enable: boolean) { - const tracks = media == CallMediaType.Video ? s.getVideoTracks() : s.getAudioTracks() - for (const t of tracks) t.enabled = enable - if (media == CallMediaType.Video && activeCall) { - activeCall.cameraEnabled = enable + function enableMedia(s: MediaStream, source: CallMediaSource, enable: boolean): boolean { + if (!activeCall) return false + + const tracks = source == CallMediaSource.Camera ? s.getVideoTracks() : s.getAudioTracks() + let changedSource = false + for (const t of tracks) { + for (const transceiver of activeCall.connection.getTransceivers()) { + if ( + (t.kind == CallMediaType.Audio && mediaSourceFromTransceiverMid(transceiver.mid) == CallMediaSource.Mic) || + (t.kind == CallMediaType.Video && mediaSourceFromTransceiverMid(transceiver.mid) == CallMediaSource.Camera) + ) { + if (enable) { + transceiver.sender.replaceTrack(t) + } else { + if (isDesktop || t.kind == CallMediaType.Video || stopTrackOnAndroid) t.stop() + else t.enabled = false + + s.removeTrack(t) + transceiver.sender.replaceTrack(null) + } + if (source == CallMediaSource.Mic) { + activeCall.localMediaSources.mic = enable + changedSource = true + } else if (source == CallMediaSource.Camera) { + activeCall.localMediaSources.camera = enable + changedSource = true + } + } + } + } + if (changedSource) { + localOrPeerMediaSourcesChanged(activeCall) + changeLayout(activeCall.layout) + return true + } else { + console.log("Enable media error") + desktopShowPermissionsAlert(source == CallMediaSource.Mic ? CallMediaType.Audio : CallMediaType.Video) + return false } } - toggleScreenShare = async function () { - const call = activeCall - if (!call) return - call.screenShareEnabled = !call.screenShareEnabled - await replaceMedia(call, call.localCamera) + function adaptToOldVersion(peerHasOldVersion: boolean, activeCall: Call) { + activeCall.peerHasOldVersion = peerHasOldVersion + if (peerHasOldVersion) { + console.log( + "The peer has an old version.", + "Tracks size:", + activeCall.remoteStream.getAudioTracks().length, + activeCall.remoteStream.getVideoTracks().length + ) + onMediaMuteUnmute("0", false) + if (activeCall.remoteStream.getVideoTracks().length > 0 || activeCall.remoteTracks.get(CallMediaSource.Camera)) { + onMediaMuteUnmute("1", false) + } + if (activeCall.localMediaSources.camera && !activeCall.peerMediaSources.camera) { + console.log("Stopping video track for the old version") + activeCall.localStream.getVideoTracks().forEach((elem) => { + elem.stop() + activeCall.localStream.removeTrack(elem) + }) + activeCall.localMediaSources.camera = false + sendMessageToNative({resp: {type: "ok"}, command: {type: "media", source: CallMediaSource.Camera, enable: false}}) + localOrPeerMediaSourcesChanged(activeCall) + changeLayout(activeCall.layout) + } + } + } + + function webView69Or70(): boolean { + return !isDesktop && (navigator.userAgent.includes("Chrome/69.") || navigator.userAgent.includes("Chrome/70.")) + } + + // Adding `a=extmap-allow-mixed` causes exception on old WebViews + // https://groups.google.com/a/chromium.org/g/blink-dev/c/7z3uvp0-ZAc/m/8Z7qpp71BgAJ + function adaptSdpToOldWebView(desc: RTCSessionDescriptionInit): RTCSessionDescriptionInit { + const res: string[] = [] + desc.sdp?.split("\n").forEach((line) => { + // Chrome has a bug related to SDP parser in old web view versions + if (!line.includes("a=extmap-allow-mixed")) { + res.push(line) + } + }) + return {sdp: res.join("\n"), type: desc.type} } return processCommand @@ -811,19 +1554,121 @@ function toggleRemoteVideoFitFill() { remote.style.objectFit = remote.style.objectFit != "contain" ? "contain" : "cover" } -function toggleMedia(s: MediaStream, media: CallMediaType): boolean { +function toggleRemoteScreenVideoFitFill() { + const remoteScreen = document.getElementById("remote-screen-video-stream")! + remoteScreen.style.objectFit = remoteScreen.style.objectFit != "contain" ? "contain" : "cover" +} + +function togglePeerMedia(s: MediaStream, media: CallMediaType): boolean { + if (!activeCall) return false let res = false const tracks = media == CallMediaType.Video ? s.getVideoTracks() : s.getAudioTracks() for (const t of tracks) { t.enabled = !t.enabled res = t.enabled } - if (media == CallMediaType.Video && activeCall) { - activeCall.cameraEnabled = res - } return res } +function changeLayout(layout: LayoutType) { + const videos = getVideoElements() + const localSources = activeCall?.localMediaSources ?? inactiveCallMediaSources + const peerSources = activeCall?.peerMediaSources ?? {mic: false, camera: false, screenAudio: false, screenVideo: false} + if (!videos || !localSources || !peerSources) return + switch (layout) { + case LayoutType.Default: + videos.local.className = "inline" + videos.remote.className = peerSources.screenVideo ? "collapsed" : "inline" + videos.local.style.visibility = "visible" + videos.remote.style.visibility = peerSources.camera ? "visible" : "hidden" + videos.remoteScreen.style.visibility = peerSources.screenVideo ? "visible" : "hidden" + break + case LayoutType.LocalVideo: + videos.local.className = "fullscreen" + videos.local.style.visibility = "visible" + videos.remote.style.visibility = "hidden" + videos.remoteScreen.style.visibility = "hidden" + break + case LayoutType.RemoteVideo: + if (peerSources.screenVideo && peerSources.camera) { + videos.remoteScreen.className = "fullscreen" + videos.remoteScreen.style.visibility = "visible" + videos.remote.style.visibility = "visible" + videos.remote.className = "collapsed-pip" + } else if (peerSources.screenVideo) { + videos.remoteScreen.className = "fullscreen" + videos.remoteScreen.style.visibility = "visible" + videos.remote.style.visibility = "hidden" + videos.remote.className = "inline" + } else if (peerSources.camera) { + videos.remote.className = "fullscreen" + videos.remote.style.visibility = "visible" + videos.remoteScreen.style.visibility = "hidden" + videos.remoteScreen.className = "inline" + } else { + videos.remote.style.visibility = "hidden" + } + videos.local.style.visibility = "hidden" + break + } + videos.localScreen.style.visibility = localSources.screenVideo ? "visible" : "hidden" + if (!isDesktop && !localSources.camera) { + resetLocalVideoElementHeight(videos.local) + } +} + +function getVideoElements(): VideoElements | undefined { + const local = document.getElementById("local-video-stream") + const localScreen = document.getElementById("local-screen-video-stream") + const remote = document.getElementById("remote-video-stream") + const remoteScreen = document.getElementById("remote-screen-video-stream") + if ( + !( + local && + localScreen && + remote && + remoteScreen && + local instanceof HTMLVideoElement && + localScreen instanceof HTMLMediaElement && + remote instanceof HTMLMediaElement && + remoteScreen instanceof HTMLMediaElement + ) + ) + return + return {local, localScreen, remote, remoteScreen} +} + +// Allow CSS to figure out the size of view by itself on Android because rotating to different orientation +// without dropping override will cause the view to have not normal proportion while no video is present +function resetLocalVideoElementHeight(local: HTMLVideoElement) { + local.style.height = "" +} + +function desktopShowPermissionsAlert(mediaType: CallMediaType) { + if (!isDesktop) return + + if (mediaType == CallMediaType.Audio) { + window.alert("Permissions denied. Please, allow access to mic to make the call working and hit unmute button. Don't reload the page.") + } else { + window.alert( + "Permissions denied. Please, allow access to mic and camera to make the call working and hit unmute/camera button. Don't reload the page." + ) + } +} + +function permissionDescription(command: WCPermission): string { + if ((window as any).safari) { + return command.safari + } else if ( + (navigator.userAgent.includes("Chrome") && navigator.vendor.includes("Google Inc")) || + navigator.userAgent.includes("Firefox") + ) { + return command.chrome + } else { + return "" + } +} + type TransformFrameFunc = (key: CryptoKey) => (frame: RTCEncodedVideoFrame, controller: TransformStreamDefaultController) => Promise<void> interface CallCrypto { diff --git a/packages/simplex-chat-webrtc/src/desktop/call.html b/packages/simplex-chat-webrtc/src/desktop/call.html index dd1193babf..8ea76ed488 100644 --- a/packages/simplex-chat-webrtc/src/desktop/call.html +++ b/packages/simplex-chat-webrtc/src/desktop/call.html @@ -7,42 +7,69 @@ <script src="/lz-string.min.js"></script> </head> <body> + <video + id="remote-screen-video-stream" + class="inline" + playsinline + poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" + style="visibility: hidden" + onclick="javascript:toggleRemoteScreenVideoFitFill()" + ></video> + <video id="remote-video-stream" - autoplay + class="inline" playsinline poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" onclick="javascript:toggleRemoteVideoFitFill()" ></video> + <video id="local-video-stream" + class="inline" muted - autoplay playsinline poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" ></video> + + <video + id="local-screen-video-stream" + class="inline" + muted + playsinline + poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" + style="visibility: hidden" + ></video> + <div id="progress"></div> <div id="info-block"> <p id="state"></p> <p id="description"></p> + <b id="media-sources" style="color: #fff"></b> </div> <div id="audio-call-icon"> <img src="/desktop/images/ic_phone_in_talk.svg" /> </div> + + <div id="permission-denied"> + <p id="permission-denied-title"></p> + <p id="permission-denied-desc"></p> + </div> + <p id="manage-call"> - <button id="toggle-screen" style="display: none" onclick="javascript:toggleScreenManually()"> + <button id="toggle-screen" onclick="javascript:toggleScreenManually()"> <img src="/desktop/images/ic_screen_share.svg" /> </button> - <button id="toggle-audio" style="display: none" onclick="javascript:toggleAudioManually()"> + <button id="toggle-mic" onclick="javascript:toggleMicManually()"> <img src="/desktop/images/ic_mic.svg" /> </button> - <button id="end-call" onclick="javascript:endCallManually()"> + <button id="end-call" style="background: red" onclick="javascript:endCallManually()"> <img src="/desktop/images/ic_call_end_filled.svg" /> </button> - <button id="toggle-speaker" style="display: none" onclick="javascript:toggleSpeakerManually()"> + <button id="toggle-speaker" onclick="javascript:toggleSpeakerManually()"> <img src="/desktop/images/ic_volume_up.svg" /> </button> - <button id="toggle-video" style="display: none" onclick="javascript:toggleVideoManually()"> + <button id="toggle-camera" onclick="javascript:toggleCameraManually()"> <img src="/desktop/images/ic_videocam_off.svg" /> </button> </p> diff --git a/packages/simplex-chat-webrtc/src/desktop/images/ic_call_end_filled.svg b/packages/simplex-chat-webrtc/src/desktop/images/ic_call_end_filled.svg index 34c409818a..5ce74d3cda 100644 --- a/packages/simplex-chat-webrtc/src/desktop/images/ic_call_end_filled.svg +++ b/packages/simplex-chat-webrtc/src/desktop/images/ic_call_end_filled.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="red" d="M480 418q125 0 238.75 50.25T914 613.5q8 9.5 8.25 21t-8.25 20L821 748q-8 8-22.5 8.75t-23-5.75l-113-84.5q-6-4.5-8.75-10.25T651 643.5v-139q-42-16-85.5-22.5t-85-6.5q-42 0-85.5 6.5t-85.5 22.5v139q0 6.5-2.75 12.5T298 666.5L184.5 751q-11.5 8.5-23.5 7.5T139.5 748L46 654.5q-8.5-8.5-8.25-20t8.25-21q81.5-95 195.25-145.25T480 418Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M480 418q125 0 238.75 50.25T914 613.5q8 9.5 8.25 21t-8.25 20L821 748q-8 8-22.5 8.75t-23-5.75l-113-84.5q-6-4.5-8.75-10.25T651 643.5v-139q-42-16-85.5-22.5t-85-6.5q-42 0-85.5 6.5t-85.5 22.5v139q0 6.5-2.75 12.5T298 666.5L184.5 751q-11.5 8.5-23.5 7.5T139.5 748L46 654.5q-8.5-8.5-8.25-20t8.25-21q81.5-95 195.25-145.25T480 418Z"/></svg> \ No newline at end of file diff --git a/packages/simplex-chat-webrtc/src/desktop/images/ic_volume_off.svg b/packages/simplex-chat-webrtc/src/desktop/images/ic_volume_off.svg new file mode 100644 index 0000000000..2767d8d48d --- /dev/null +++ b/packages/simplex-chat-webrtc/src/desktop/images/ic_volume_off.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 -960 960 960" width="44" fill="white"><path d="m809.5-61.5-133-133q-27 19-58.25 33.25T553.5-139.5V-199q21.5-6.5 42.5-14.5t39.5-22L476-396v229L280-363H122.5v-234H274L54.5-816.5 96-858l755 754-41.5 42.5ZM770-291l-41.5-41.5q20-33 29.75-70.67Q768-440.85 768-481q0-100.82-58.75-180.41T553.5-763v-59.5q120 28 196 123.25t76 218.25q0 50.5-14 98.75T770-291ZM642.5-418.5l-89-89v-132q46.5 21.5 73.75 64.75T654.5-480q0 16-3 31.5t-9 30ZM476-585 372-689l104-104v208Zm-57.5 278v-145.5l-87-87H180v119h124.5l114 113.5ZM375-496Z"/></svg> \ No newline at end of file diff --git a/packages/simplex-chat-webrtc/src/desktop/style.css b/packages/simplex-chat-webrtc/src/desktop/style.css index 24c31fa6f7..5110c7c7d6 100644 --- a/packages/simplex-chat-webrtc/src/desktop/style.css +++ b/packages/simplex-chat-webrtc/src/desktop/style.css @@ -3,26 +3,98 @@ body { padding: 0; margin: 0; background-color: black; + font-family: Arial, Helvetica, sans-serif; } -#remote-video-stream { +#remote-video-stream.inline { position: absolute; width: 100%; height: 100%; object-fit: cover; } -#local-video-stream { +#remote-video-stream.collapsed { + position: absolute; + width: 20%; + max-width: 20%; + height: 15.03vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + bottom: 80px; + right: 0; +} + +#remote-video-stream.collapsed-pip { + position: absolute; + max-width: 30%; + max-height: 30%; + object-fit: cover; + margin: 8px; + border-radius: 8px; + bottom: 0; + right: 0; +} + +#remote-screen-video-stream.inline { + position: absolute; + width: 100%; + height: 100%; + object-fit: cover; +} + +#local-video-stream.inline { + background-color: black; + position: absolute; + width: 20%; + max-width: 20%; + height: 15.03vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + top: 0; + right: 0; +} + +#local-screen-video-stream.inline { position: absolute; width: 20%; max-width: 20%; object-fit: cover; margin: 16px; border-radius: 16px; - top: 0; + top: 33%; right: 0; } +#remote-video-stream.fullscreen { + position: absolute; + height: 100%; + width: 100%; + object-fit: cover; +} + +#remote-screen-video-stream.fullscreen { + position: absolute; + height: 100%; + width: 100%; + object-fit: cover; +} + +#local-video-stream.fullscreen { + position: absolute; + height: 100%; + width: 100%; + object-fit: cover; +} + +#local-screen-video-stream.fullscreen { + position: absolute; + height: 100%; + width: 100%; + object-fit: cover; +} + *::-webkit-media-controls { display: none !important; -webkit-appearance: none !important; @@ -43,19 +115,32 @@ body { #manage-call { position: absolute; width: fit-content; - top: 90%; + bottom: 15px; left: 50%; transform: translate(-50%, 0); display: grid; grid-auto-flow: column; - grid-column-gap: 30px; + grid-column-gap: 38px; } #manage-call button { border: none; cursor: pointer; appearance: none; - background-color: inherit; + background-color: #ffffff33; + border-radius: 50%; + padding: 13px; + width: 61px; + height: 61px; +} + +#manage-call img { + width: 35px; + height: 35px; +} + +#manage-call button .video { + background: #00000033; } #progress { @@ -75,6 +160,23 @@ body { animation: spin 2s linear infinite; } +#permission-denied { + position: absolute; + width: 100%; + text-align: center; + height: 100%; + align-content: center; + color: white; +} + +#permission-denied-title { + font-weight: bold; +} + +#permission-denied-desc { + font-style: italic; +} + @-webkit-keyframes spin { 0% { -webkit-transform: rotate(0deg); @@ -96,10 +198,8 @@ body { #info-block { position: absolute; color: white; - line-height: 10px; opacity: 0.8; width: 200px; - font-family: Arial, Helvetica, sans-serif; } #info-block.audio { diff --git a/packages/simplex-chat-webrtc/src/desktop/ui.ts b/packages/simplex-chat-webrtc/src/desktop/ui.ts index 4f336a17b1..eac659a17a 100644 --- a/packages/simplex-chat-webrtc/src/desktop/ui.ts +++ b/packages/simplex-chat-webrtc/src/desktop/ui.ts @@ -10,6 +10,7 @@ socket.addEventListener("open", (_event) => { sendMessageToNative = (msg: WVApiMessage) => { console.log("Message to server") socket.send(JSON.stringify(msg)) + reactOnMessageToServer(msg) } }) @@ -32,74 +33,178 @@ function endCallManually() { sendMessageToNative({resp: {type: "end"}}) } -function toggleAudioManually() { - if (activeCall?.localMedia) { - document.getElementById("toggle-audio")!!.innerHTML = toggleMedia(activeCall.localStream, CallMediaType.Audio) - ? '<img src="/desktop/images/ic_mic.svg" />' - : '<img src="/desktop/images/ic_mic_off.svg" />' +function toggleMicManually() { + const enable = activeCall ? !activeCall.localMediaSources.mic : !inactiveCallMediaSources.mic + const apiCall: WVAPICall = { + command: {type: "media", source: CallMediaSource.Mic, enable: enable}, } + processCommand(apiCall) } function toggleSpeakerManually() { - if (activeCall?.remoteStream) { - document.getElementById("toggle-speaker")!!.innerHTML = toggleMedia(activeCall.remoteStream, CallMediaType.Audio) - ? '<img src="/desktop/images/ic_volume_up.svg" />' - : '<img src="/desktop/images/ic_volume_down.svg" />' + if (activeCall?.remoteStream && activeCall.peerMediaSources.mic) { + enableSpeakerIcon(togglePeerMedia(activeCall.remoteStream, CallMediaType.Audio), !activeCall.peerMediaSources.mic) } } -function toggleVideoManually() { - if (activeCall?.localMedia) { - let res: boolean - if (activeCall?.screenShareEnabled) { - activeCall.cameraEnabled = !activeCall.cameraEnabled - res = activeCall.cameraEnabled - } else { - res = toggleMedia(activeCall.localStream, CallMediaType.Video) - } - document.getElementById("toggle-video")!!.innerHTML = res - ? '<img src="/desktop/images/ic_videocam_filled.svg" />' - : '<img src="/desktop/images/ic_videocam_off.svg" />' +function toggleCameraManually() { + const enable = activeCall ? !activeCall.localMediaSources.camera : !inactiveCallMediaSources.camera + const apiCall: WVAPICall = { + command: {type: "media", source: CallMediaSource.Camera, enable: enable}, } + processCommand(apiCall) } async function toggleScreenManually() { - const was = activeCall?.screenShareEnabled await toggleScreenShare() - if (was != activeCall?.screenShareEnabled) { - document.getElementById("toggle-screen")!!.innerHTML = activeCall?.screenShareEnabled - ? '<img src="/desktop/images/ic_stop_screen_share.svg" />' - : '<img src="/desktop/images/ic_screen_share.svg" />' +} + +// override function in call.ts to adapt UI to enabled media sources +localOrPeerMediaSourcesChanged = (call: Call) => { + enableMicIcon(call.localMediaSources.mic) + enableCameraIcon(call.localMediaSources.camera) + enableScreenIcon(call.localMediaSources.screenVideo) + + const className = + localMedia(call) == CallMediaType.Video || peerMedia(call) == CallMediaType.Video ? CallMediaType.Video : CallMediaType.Audio + document.getElementById("info-block")!.className = className + + if (call.connection.iceConnectionState == "connected") { + document.getElementById("audio-call-icon")!.style.display = className == CallMediaType.Audio ? "block" : "none" } + + // document.getElementById("media-sources")!.innerText = mediaSourcesStatus(call) + document.getElementById("manage-call")!.className = localMedia(call) == CallMediaType.Video ? CallMediaType.Video : "" +} + +// override function in call.ts to adapt UI to enabled media sources +inactiveCallMediaSourcesChanged = (inactiveCallMediaSources: CallMediaSources) => { + const mic = inactiveCallMediaSources.mic + const camera = inactiveCallMediaSources.camera + const screenVideo = inactiveCallMediaSources.screenVideo + enableMicIcon(mic) + enableCameraIcon(camera) + enableScreenIcon(screenVideo) + + const className = camera ? CallMediaType.Video : CallMediaType.Audio + document.getElementById("info-block")!.className = className + // document.getElementById("media-sources")!.innerText = inactiveCallMediaSourcesStatus(inactiveCallMediaSources) +} + +failedToGetPermissions = (title: string, description: string) => { + document.getElementById("info-block")!.style.visibility = "hidden" + document.getElementById("progress")!.style.visibility = "hidden" + + document.getElementById("permission-denied-title")!.innerText = title + document.getElementById("permission-denied-desc")!.innerText = description + + document.getElementById("toggle-mic")!.style.visibility = "hidden" + document.getElementById("toggle-camera")!.style.visibility = "hidden" + document.getElementById("toggle-screen")!.style.visibility = "hidden" + document.getElementById("toggle-speaker")!.style.visibility = "hidden" +} + +function enableMicIcon(enabled: boolean) { + document.getElementById("toggle-mic")!.innerHTML = enabled + ? '<img src="/desktop/images/ic_mic.svg" />' + : '<img src="/desktop/images/ic_mic_off.svg" />' +} + +function enableCameraIcon(enabled: boolean) { + document.getElementById("toggle-camera")!.innerHTML = enabled + ? '<img src="/desktop/images/ic_videocam_filled.svg" />' + : '<img src="/desktop/images/ic_videocam_off.svg" />' +} + +function enableScreenIcon(enabled: boolean) { + document.getElementById("toggle-screen")!.innerHTML = enabled + ? '<img src="/desktop/images/ic_stop_screen_share.svg" />' + : '<img src="/desktop/images/ic_screen_share.svg" />' +} + +function enableSpeakerIcon(enabled: boolean, muted: boolean) { + document.getElementById("toggle-speaker")!!.innerHTML = muted + ? '<img src="/desktop/images/ic_volume_off.svg" />' + : enabled + ? '<img src="/desktop/images/ic_volume_up.svg" />' + : '<img src="/desktop/images/ic_volume_down.svg" />' + document.getElementById("toggle-speaker")!!.style.opacity = muted ? "0.7" : "1" +} + +function mediaSourcesStatus(call: Call): string { + let status = "local" + if (call.localMediaSources.mic) status += " mic" + if (call.localMediaSources.camera) status += " cam" + if (call.localMediaSources.screenAudio) status += " scrA" + if (call.localMediaSources.screenVideo) status += " scrV" + + status += " | peer" + + if (call.peerMediaSources.mic) status += " mic" + if (call.peerMediaSources.camera) status += " cam" + if (call.peerMediaSources.screenAudio) status += " scrA" + if (call.peerMediaSources.screenVideo) status += " scrV" + return status +} + +function inactiveCallMediaSourcesStatus(inactiveCallMediaSources: CallMediaSources): string { + let status = "local" + const mic = inactiveCallMediaSources.mic + const camera = inactiveCallMediaSources.camera + const screenAudio = inactiveCallMediaSources.screenAudio + const screenVideo = inactiveCallMediaSources.screenVideo + if (mic) status += " mic" + if (camera) status += " cam" + if (screenAudio) status += " scrA" + if (screenVideo) status += " scrV" + return status } function reactOnMessageFromServer(msg: WVApiMessage) { + // screen is not allowed to be enabled before connection estabilished + if (msg.command?.type == "capabilities" || msg.command?.type == "offer") { + document.getElementById("toggle-screen")!!.style.opacity = "0.7" + } else if (activeCall) { + document.getElementById("toggle-screen")!!.style.opacity = "1" + } switch (msg.command?.type) { case "capabilities": - document.getElementById("info-block")!!.className = msg.command.media - break case "offer": case "start": - document.getElementById("toggle-audio")!!.style.display = "inline-block" - document.getElementById("toggle-speaker")!!.style.display = "inline-block" - if (msg.command.media == CallMediaType.Video) { - document.getElementById("toggle-video")!!.style.display = "inline-block" - document.getElementById("toggle-screen")!!.style.display = "inline-block" - } document.getElementById("info-block")!!.className = msg.command.media + document.getElementById("toggle-mic")!!.style.display = "inline-block" + document.getElementById("toggle-speaker")!!.style.display = "inline-block" + document.getElementById("toggle-camera")!!.style.display = "inline-block" + document.getElementById("toggle-screen")!!.style.display = "inline-block" + enableSpeakerIcon(true, true) break case "description": updateCallInfoView(msg.command.state, msg.command.description) if (activeCall?.connection.connectionState == "connected") { document.getElementById("progress")!.style.display = "none" - if (document.getElementById("info-block")!!.className == CallMediaType.Audio) { - document.getElementById("audio-call-icon")!.style.display = "block" - } + document.getElementById("audio-call-icon")!.style.display = + document.getElementById("info-block")!!.className == CallMediaType.Audio ? "block" : "none" } break } } +function reactOnMessageToServer(msg: WVApiMessage) { + if (!activeCall) return + + switch (msg.resp?.type) { + case "peerMedia": + const className = localMedia(activeCall) == CallMediaType.Video || peerMedia(activeCall) == CallMediaType.Video ? "video" : "audio" + document.getElementById("info-block")!!.className = className + document.getElementById("audio-call-icon")!.style.display = className == CallMediaType.Audio ? "block" : "none" + enableSpeakerIcon( + activeCall.remoteStream.getAudioTracks().every((elem) => elem.enabled), + !activeCall.peerMediaSources.mic + ) + break + } +} + function updateCallInfoView(state: string, description: string) { document.getElementById("state")!!.innerText = state document.getElementById("description")!!.innerText = description diff --git a/scripts/android/build-android.sh b/scripts/android/build-android.sh index cd29a736e8..97190ec103 100755 --- a/scripts/android/build-android.sh +++ b/scripts/android/build-android.sh @@ -8,9 +8,9 @@ u="$USER" tmp="$(mktemp -d -t)" folder="$tmp/simplex-chat" -nix_ver="nix-2.15.1" +nix_ver="nix-2.22.0" nix_url="https://releases.nixos.org/nix/$nix_ver/install" -nix_hash="67aa37f0115195d8ddf32b5d6f471f1e60ecca0fdb3e98bcf54bc147c3078640" +nix_hash="4fed7db867186c01ce2a2077da4a6950ed16232efbf78d0cd19700cff80559f9" nix_config="sandbox = true max-jobs = auto experimental-features = nix-command flakes" @@ -43,12 +43,13 @@ nix_setup() { } git_setup() { - [ "$folder" != "." ] && { + if [ "$folder" != "." ]; then git clone "$repo" "$folder" - } + fi - # Switch to nix-android branch - git -C "$folder" checkout "$commit" + if [ -z ${git_skip+x} ]; then + git -C "$folder" checkout "$commit" + fi } checks() { @@ -100,10 +101,17 @@ build() { sed -i.bak 's/${extract_native_libs}/true/' "$folder/apps/multiplatform/android/src/main/AndroidManifest.xml" sed -i.bak 's/jniLibs.useLegacyPackaging =.*/jniLibs.useLegacyPackaging = true/' "$folder/apps/multiplatform/android/build.gradle.kts" sed -i.bak '/android {/a lint {abortOnError = false}' "$folder/apps/multiplatform/android/build.gradle.kts" + sed -i.bak '/tasks/Q' "$folder/apps/multiplatform/android/build.gradle.kts" for arch in $arches; do - android_simplex_lib="${folder}#hydraJobs.${arch}-android:lib:simplex-chat.x86_64-linux" - android_support_lib="${folder}#hydraJobs.${arch}-android:lib:support.x86_64-linux" + if [ "$arch" = "armv7a" ]; then + android_simplex_lib="${folder}#hydraJobs.${arch}-android:lib:simplex-chat.x86_64-linux" + android_support_lib="${folder}#hydraJobs.${arch}-android:lib:support.x86_64-linux" + else + android_simplex_lib="${folder}#hydraJobs.x86_64-linux.${arch}-android:lib:simplex-chat" + android_support_lib="${folder}#hydraJobs.x86_64-linux.${arch}-android:lib:support" + fi + android_simplex_lib_output="${PWD}/result/pkg-${arch}-android-libsimplex.zip" android_support_lib_output="${PWD}/result/pkg-${arch}-android-libsupport.zip" @@ -146,15 +154,22 @@ final() { printf 'Simplex-chat was successfully compiled: %s/simplex-chat-*.apk\nDelete nix and gradle caches with "rm -rf /nix && rm $HOME/.nix* && $HOME/.gradle/caches" in case if no longer needed.\n' "$PWD" } -main() { - while getopts ":s" opt; do +pre() { + while getopts ":sg" opt; do case $opt in s) folder="." ;; + g) git_skip=1 ;; *) printf "Flag '-%s' doesn't exist.\n" "$OPTARG"; exit 1 ;; esac done + shift $(( $OPTIND - 1 )) - commit="$1"; shift 1 + + commit="${1:-HEAD}" +} + +main() { + pre "$@" git_setup checks build diff --git a/scripts/android/compress-and-sign-apk.sh b/scripts/android/compress-and-sign-apk.sh index e46b8a54f1..74d59203c4 100755 --- a/scripts/android/compress-and-sign-apk.sh +++ b/scripts/android/compress-and-sign-apk.sh @@ -47,7 +47,7 @@ for ORIG_NAME in "${ORIG_NAMES[@]}"; do #(cd apk && 7z a -r -mx=0 -tzip ../$ORIG_NAME resources.arsc) ALL_TOOLS=("$sdk_dir"/build-tools/*/) - BIN_DIR="${ALL_TOOLS[1]}" + BIN_DIR="${ALL_TOOLS[${#ALL_TOOLS[@]}-1]}" "$BIN_DIR"/zipalign -p -f 4 "$ORIG_NAME" "$ORIG_NAME"-2 diff --git a/scripts/android/download-libs.sh b/scripts/android/download-libs.sh index 4702f03600..1691838d94 100755 --- a/scripts/android/download-libs.sh +++ b/scripts/android/download-libs.sh @@ -37,12 +37,12 @@ for ((i = 0 ; i < ${#arches[@]}; i++)); do mkdir -p "$output_dir" 2> /dev/null - curl --location -o libsupport.zip $job_repo/$arch-android:lib:support.x86_64-linux/latest/download/1 && \ + curl --tlsv1.2 --location -o libsupport.zip $job_repo/x86_64-linux."$arch"-android:lib:support/latest/download/1 && \ unzip -o libsupport.zip && \ mv libsupport.so "$output_dir" && \ rm libsupport.zip - curl --location -o libsimplex.zip "$job_repo"/"$arch"-android:lib:simplex-chat.x86_64-linux/latest/download/1 && \ + curl --tlsv1.2 --location -o libsimplex.zip "$job_repo"/x86_64-linux."$arch"-android:lib:simplex-chat/latest/download/1 && \ unzip -o libsimplex.zip && \ mv libsimplex.so "$output_dir" && \ rm libsimplex.zip diff --git a/scripts/android/prepare.sh b/scripts/android/prepare.sh index 683d9cbf87..88f62af34a 100755 --- a/scripts/android/prepare.sh +++ b/scripts/android/prepare.sh @@ -3,7 +3,7 @@ # libsimplex.so and libsupport.so binaries should be in ~/Downloads folder in their directories based on archive name mkdir -p ./apps/multiplatform/common/src/commonMain/cpp/android/libs/arm64-v8a/ rm ./apps/multiplatform/common/src/commonMain/cpp/android/libs/arm64-v8a/* -unzip -o ~/Downloads/pkg-aarch64-android-libsupport.zip -d ./apps/multiplatform/common/src/commonMain/cpp/android/libs/arm64-v8a +unzip -o ~/Downloads/pkg-aarch64-android-libsupport.zip -d ./apps/multiplatform/common/src/commonMain/cpp/android/libs/arm64-v8a/ unzip -o ~/Downloads/pkg-aarch64-android-libsimplex.zip -d ./apps/multiplatform/common/src/commonMain/cpp/android/libs/arm64-v8a/ mkdir -p ./apps/multiplatform/common/src/commonMain/cpp/android/libs/armeabi-v7a/ diff --git a/scripts/cabal.project.local.mac b/scripts/cabal.project.local.mac index dd62f1a391..6b4ff718b6 100644 --- a/scripts/cabal.project.local.mac +++ b/scripts/cabal.project.local.mac @@ -3,12 +3,12 @@ ignore-project: False -- amend to point to the actual openssl location package simplexmq - extra-include-dirs: /opt/homebrew/opt/openssl@1.1/include - extra-lib-dirs: /opt/homebrew/opt/openssl@1.1/lib + extra-include-dirs: /opt/homebrew/opt/openssl@3.0/include + extra-lib-dirs: /opt/homebrew/opt/openssl@3.0/lib package direct-sqlcipher - extra-include-dirs: /opt/homebrew/opt/openssl@1.1/include - extra-lib-dirs: /opt/homebrew/opt/openssl@1.1/lib + extra-include-dirs: /opt/homebrew/opt/openssl@3.0/include + extra-lib-dirs: /opt/homebrew/opt/openssl@3.0/lib flags: +openssl test-show-details: direct diff --git a/scripts/ci/build-desktop-mac.sh b/scripts/ci/build-desktop-mac.sh index 259b946228..9adea013b4 100755 --- a/scripts/ci/build-desktop-mac.sh +++ b/scripts/ci/build-desktop-mac.sh @@ -8,7 +8,7 @@ echo "desktop.mac.signing.keychain=/tmp/simplex.keychain" >> apps/multiplatform/ echo "desktop.mac.notarization.apple_id=$APPLE_SIMPLEX_NOTARIZATION_APPLE_ID" >> apps/multiplatform/local.properties echo "desktop.mac.notarization.password=$APPLE_SIMPLEX_NOTARIZATION_PASSWORD" >> apps/multiplatform/local.properties echo "desktop.mac.notarization.team_id=5NN7GUYB6T" >> apps/multiplatform/local.properties -echo "$APPLE_SIMPLEX_SIGNING_KEYCHAIN" | base64 --decode - > /tmp/simplex.keychain +echo "$APPLE_SIMPLEX_SIGNING_KEYCHAIN" | base64 --decode -o /tmp/simplex.keychain security unlock-keychain -p "" /tmp/simplex.keychain # Adding keychain to the list of keychains. diff --git a/scripts/ci/linux_util_free_space.sh b/scripts/ci/linux_util_free_space.sh new file mode 100755 index 0000000000..ef00eb886e --- /dev/null +++ b/scripts/ci/linux_util_free_space.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +# Taken from: https://github.com/apache/arrow/blob/main/ci/scripts/util_free_space.sh + +set -eux + +df -h +echo "::group::/usr/local/*" +du -hsc /usr/local/* +echo "::endgroup::" +# ~1GB +sudo rm -rf \ + /usr/local/aws-sam-cil \ + /usr/local/julia* || : +echo "::group::/usr/local/bin/*" +du -hsc /usr/local/bin/* +echo "::endgroup::" +# ~1GB (From 1.2GB to 214MB) +sudo rm -rf \ + /usr/local/bin/aliyun \ + /usr/local/bin/azcopy \ + /usr/local/bin/bicep \ + /usr/local/bin/cmake-gui \ + /usr/local/bin/cpack \ + /usr/local/bin/helm \ + /usr/local/bin/hub \ + /usr/local/bin/kubectl \ + /usr/local/bin/minikube \ + /usr/local/bin/node \ + /usr/local/bin/packer \ + /usr/local/bin/pulumi* \ + /usr/local/bin/sam \ + /usr/local/bin/stack \ + /usr/local/bin/terraform || : +# 142M +sudo rm -rf /usr/local/bin/oc || : \ +echo "::group::/usr/local/share/*" +du -hsc /usr/local/share/* +echo "::endgroup::" +# 506MB +sudo rm -rf /usr/local/share/chromium || : +# 1.3GB +sudo rm -rf /usr/local/share/powershell || : +echo "::group::/usr/local/lib/*" +du -hsc /usr/local/lib/* +echo "::endgroup::" +# 15GB +sudo rm -rf /usr/local/lib/android || : +# 341MB +sudo rm -rf /usr/local/lib/heroku || : +# 1.2GB +sudo rm -rf /usr/local/lib/node_modules || : +echo "::group::/opt/*" +du -hsc /opt/* +echo "::endgroup::" +# 679MB +sudo rm -rf /opt/az || : +echo "::group::/opt/microsoft/*" +du -hsc /opt/microsoft/* +echo "::endgroup::" +# 197MB +sudo rm -rf /opt/microsoft/powershell || : +echo "::group::/opt/hostedtoolcache/*" +du -hsc /opt/hostedtoolcache/* +echo "::endgroup::" +# 5.3GB +sudo rm -rf /opt/hostedtoolcache/CodeQL || : +# 1.4GB +sudo rm -rf /opt/hostedtoolcache/go || : +# 489MB +sudo rm -rf /opt/hostedtoolcache/PyPy || : +# 376MB +sudo rm -rf /opt/hostedtoolcache/node || : +# Remove Web browser packages +sudo apt purge -y \ + firefox \ + google-chrome-stable \ + microsoft-edge-stable +df -h diff --git a/scripts/db/README.md b/scripts/db/README.md new file mode 100644 index 0000000000..68d4f74ac7 --- /dev/null +++ b/scripts/db/README.md @@ -0,0 +1,105 @@ +# Transfer data from SQLite to Postgres database + +1. \* Decrypt SQLite database if it is encrypted. + + ```sh + sqlcipher encrypted_simplex_v1_agent.db + ``` + + ```sql + PRAGMA key = 'password'; + ATTACH DATABASE 'simplex_v1_agent.db' AS plaintext KEY ''; + SELECT sqlcipher_export('plaintext'); + DETACH DATABASE plaintext; + ``` + + Repeat for `simplex_v1_chat.db`. + +2. Prepare Postgres database. + + - Create Postgres database. In shell: + + ```sh + createdb -O simplex simplex_v1 + ``` + + Or via query. + + - Build `simplex-chat` executable with `client_postgres` flag and run it to initialize new chat database. + + This should create `simplex_v1_agent_schema` and `simplex_v1_chat_schema` schemas in `simplex_v1` database, with `migrations` tables populated. Some tables would have initialization data - it will be truncated via pgloader command in next step. + +3. Load data from decrypted SQLite databases to Postgres database via pgloader. + + Install pgloader and add it to PATH. Run in shell (substitute paths): + + ```sh + SQLITE_DBPATH='simplex_v1_agent.db' POSTGRES_CONN='postgres://simplex@/simplex_v1' POSTGRES_SCHEMA='simplex_v1_agent_schema' pgloader --on-error-stop sqlite.load + + SQLITE_DBPATH='simplex_v1_chat.db' POSTGRES_CONN='postgres://simplex@/simplex_v1' POSTGRES_SCHEMA='simplex_v1_chat_schema' pgloader --on-error-stop sqlite.load + ``` + +4. Update sequences for Postgres tables. + + ```sql + DO $$ + DECLARE + rec RECORD; + BEGIN + EXECUTE 'SET SEARCH_PATH TO simplex_v1_agent_schema'; + + FOR rec IN + SELECT + table_name, + column_name, + pg_get_serial_sequence(table_name, column_name) AS seq_name + FROM + information_schema.columns + WHERE + table_schema = 'simplex_v1_agent_schema' + AND identity_generation = 'ALWAYS' + LOOP + EXECUTE format( + 'SELECT setval(%L, (SELECT MAX(%I) FROM %I))', + rec.seq_name, rec.column_name, rec.table_name + ); + END LOOP; + END $$; + ``` + + Repeat for `simplex_v1_chat_schema`. + +5. \* Compare number of rows between Postgres and SQLite tables. + + To check number of rows for all tables in Postgres database schema run: + + ```sql + WITH tbl AS ( + SELECT table_schema, table_name + FROM information_schema.Tables + WHERE table_name NOT LIKE 'pg_%' + AND table_schema IN ('simplex_v1_agent_schema') + ) + SELECT + table_schema AS schema_name, + table_name, + (xpath('/row/c/text()', query_to_xml( + format('SELECT count(*) AS c FROM %I.%I', table_schema, table_name), false, true, '' + )))[1]::text::int AS records_count + FROM tbl + ORDER BY records_count DESC; + ``` + + Repeat for `simplex_v1_chat_schema`. + +6. Build and run desktop app with Postgres backend. + + Run in shell (paths are from project root): + + ```sh + ./scripts/desktop/build-lib-mac.sh arm64 postgres + + ./gradlew runDistributable -Pdatabase.backend=postgres + # or + ./gradlew packageDmg -Pdatabase.backend=postgres + ``` diff --git a/scripts/db/sqlite.load b/scripts/db/sqlite.load new file mode 100644 index 0000000000..105e8c39d2 --- /dev/null +++ b/scripts/db/sqlite.load @@ -0,0 +1,20 @@ +LOAD DATABASE + FROM {{SQLITE_DBPATH}} + INTO {{POSTGRES_CONN}} + + WITH include no drop, + truncate, + disable triggers, + create no tables, + create no indexes, + -- pgloader implementation doesn't find "GENERATED ALWAYS AS IDENTITY" sequences, + -- instead we reset sequences manually via custom query after load + reset no sequences, + data only + + EXCLUDING TABLE NAMES LIKE 'migrations', 'sqlite_sequence' + + SET work_mem to '16MB', + maintenance_work_mem to '512 MB', + search_path to '{{POSTGRES_SCHEMA}}' +; diff --git a/scripts/desktop/build-lib-linux.sh b/scripts/desktop/build-lib-linux.sh index fa1f892a03..1db2755926 100755 --- a/scripts/desktop/build-lib-linux.sh +++ b/scripts/desktop/build-lib-linux.sh @@ -20,11 +20,20 @@ root_dir="$(dirname "$(dirname "$(readlink "$0")")")" cd $root_dir BUILD_DIR=dist-newstyle/build/$ARCH-$OS/ghc-${GHC_VERSION}/simplex-chat-* +exports=( $(sed 's/foreign export ccall "chat_migrate_init_key"//' src/Simplex/Chat/Mobile.hs | sed 's/foreign export ccall "chat_reopen_store"//' |grep "foreign export ccall" | cut -d '"' -f2) ) +for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc -l); if [ $count -ne 1 ]; then echo Wrong exports in libsimplex.dll.def. Add \"$elem\" to that file; exit 1; fi ; done +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 -flink-rts -threaded' +cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts -threaded' --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library' cd $BUILD_DIR/build #patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so #patchelf --add-rpath '$ORIGIN' libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so + +# GitHub's Ubuntu 20.04 runner started to set libffi.so.7 as a dependency while Ubuntu 20.04 on user's devices may not have it +# but libffi.so.8 is shipped as an external library with other libs +patchelf --replace-needed "libffi.so.7" "libffi.so.8" libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so + mkdir deps 2> /dev/null || true ldd libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so | grep "ghc" | cut -d' ' -f 3 | xargs -I {} cp {} ./deps/ diff --git a/scripts/desktop/build-lib-mac.sh b/scripts/desktop/build-lib-mac.sh index 1a4deced4a..934d9b8d7d 100755 --- a/scripts/desktop/build-lib-mac.sh +++ b/scripts/desktop/build-lib-mac.sh @@ -6,6 +6,7 @@ OS=mac ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}" COMPOSE_ARCH=$ARCH GHC_VERSION=9.6.3 +DATABASE_BACKEND="${2:-sqlite}" if [ "$ARCH" == "arm64" ]; then ARCH=aarch64 @@ -19,8 +20,19 @@ GHC_LIBS_DIR=$(ghc --print-libdir) BUILD_DIR=dist-newstyle/build/$ARCH-*/ghc-*/simplex-chat-* +exports=( $(sed 's/foreign export ccall "chat_migrate_init_key"//' src/Simplex/Chat/Mobile.hs | sed 's/foreign export ccall "chat_reopen_store"//' |grep "foreign export ccall" | cut -d '"' -f2) ) +for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc -l); if [ $count -ne 1 ]; then echo Wrong exports in libsimplex.dll.def. Add \"$elem\" to that file; exit 1; fi ; done +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 lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" + +if [[ "$DATABASE_BACKEND" == "postgres" ]]; then + echo "Building with postgres backend..." + cabal build -f client_postgres lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library' +else + echo "Building with sqlite backend..." + cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library' +fi cd $BUILD_DIR/build mkdir deps 2> /dev/null || true @@ -32,7 +44,7 @@ mkdir deps 2> /dev/null || true cp /tmp/libffi-3.4.4/*-apple-darwin*/.libs/libffi.dylib $BUILD/deps || \ ( \ cd /tmp && \ - curl "https://gitlab.haskell.org/ghc/libffi-tarballs/-/raw/libffi-3.4.4/libffi-3.4.4.tar.gz?inline=false" -o libffi.tar.gz && \ + curl --tlsv1.2 "https://gitlab.haskell.org/ghc/libffi-tarballs/-/raw/libffi-3.4.4/libffi-3.4.4.tar.gz?inline=false" -o libffi.tar.gz && \ tar -xzvf libffi.tar.gz && \ cd "libffi-3.4.4" && \ ./configure && \ @@ -95,26 +107,30 @@ cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT apps/multiplatform cd apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ -LIBCRYPTO_PATH=$(otool -l libHSdrct-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) -install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT libHSdrct-*.$LIB_EXT -cp $LIBCRYPTO_PATH libcrypto.1.1.$LIB_EXT -chmod 755 libcrypto.1.1.$LIB_EXT -install_name_tool -id "libcrypto.1.1.$LIB_EXT" libcrypto.1.1.$LIB_EXT +LIBCRYPTO_PATH=$(otool -l libHSsmplxmq-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) +install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.3.0.$LIB_EXT libHSsmplxmq-*.$LIB_EXT +cp $LIBCRYPTO_PATH libcrypto.3.0.$LIB_EXT +chmod 755 libcrypto.3.0.$LIB_EXT +install_name_tool -id "libcrypto.3.0.$LIB_EXT" libcrypto.3.0.$LIB_EXT install_name_tool -id "libffi.8.$LIB_EXT" libffi.$LIB_EXT LIBCRYPTO_PATH=$(otool -l $LIB | grep libcrypto | cut -d' ' -f11) if [ -n "$LIBCRYPTO_PATH" ]; then - install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT $LIB + install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.3.0.$LIB_EXT $LIB fi -LIBCRYPTO_PATH=$(otool -l libHSsmplxmq*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) -if [ -n "$LIBCRYPTO_PATH" ]; then - install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT libHSsmplxmq*.$LIB_EXT -fi +# We could change libpq and libHSpstgrsql for postgres (?), remove sqlite condition for exit below. +# Unnecessary for now as app with postgres backend is not for distribution. +if [[ "$DATABASE_BACKEND" == "sqlite" ]]; then + LIBCRYPTO_PATH=$(otool -l libHSdrct-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) + if [ -n "$LIBCRYPTO_PATH" ]; then + install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.3.0.$LIB_EXT libHSdrct-*.$LIB_EXT + fi -LIBCRYPTO_PATH=$(otool -l libHSsqlcphr-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) -if [ -n "$LIBCRYPTO_PATH" ]; then - install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT libHSsqlcphr-*.$LIB_EXT + LIBCRYPTO_PATH=$(otool -l libHSsqlcphr-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) + if [ -n "$LIBCRYPTO_PATH" ]; then + install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.3.0.$LIB_EXT libHSsqlcphr-*.$LIB_EXT + fi fi for lib in $(find . -type f -name "*.$LIB_EXT"); do @@ -128,7 +144,9 @@ LOCAL_DIRS=`for lib in $(find . -type f -name "*.$LIB_EXT"); do otool -l $lib | if [ -n "$LOCAL_DIRS" ]; then echo These libs still point to local directories: echo $LOCAL_DIRS - exit 1 + if [[ "$DATABASE_BACKEND" == "sqlite" ]]; then + exit 1 + fi fi cd - diff --git a/scripts/desktop/build-lib-windows.sh b/scripts/desktop/build-lib-windows.sh index bd2cdc1c23..af408d4054 100755 --- a/scripts/desktop/build-lib-windows.sh +++ b/scripts/desktop/build-lib-windows.sh @@ -17,6 +17,10 @@ fi BUILD_DIR=dist-newstyle/build/$ARCH-$OS/ghc-*/simplex-chat-* +exports=( $(sed 's/foreign export ccall "chat_migrate_init_key"//' src/Simplex/Chat/Mobile.hs | sed 's/foreign export ccall "chat_reopen_store"//' |grep "foreign export ccall" | cut -d '"' -f2) ) +for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc -l); if [ $count -ne 1 ]; then echo Wrong exports in libsimplex.dll.def. Add \"$elem\" to that file; exit 1; fi ; done +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 + # IMPORTANT: in order to get a working build you should use x86_64 MinGW with make, cmake, gcc. # 100% working MinGW is https://github.com/brechtsanders/winlibs_mingw/releases/download/13.1.0-16.0.5-11.0.0-ucrt-r5/winlibs-x86_64-posix-seh-gcc-13.1.0-mingw-w64ucrt-11.0.0-r5.zip # Many other distributions I tested don't work in some cases or don't have required tools. @@ -32,7 +36,7 @@ mkdir dist-newstyle 2>/dev/null || true scripts/desktop/prepare-openssl-windows.sh -openssl_windows_style_path=$(echo `pwd`/dist-newstyle/openssl-1.1.1w | sed 's#/\([a-zA-Z]\)#\1:#' | sed 's#/#\\#g') +openssl_windows_style_path=$(echo `pwd`/dist-newstyle/openssl-3.0.15 | sed 's#/\([a-zA-Z]\)#\1:#' | sed 's#/#\\#g') rm -rf $BUILD_DIR 2>/dev/null || true # Existence of this directory produces build error: cabal's bug rm -rf dist-newstyle/src/direct-sq* 2>/dev/null || true @@ -43,17 +47,17 @@ echo " flags: +openssl" >> cabal.project.local echo " extra-include-dirs: $openssl_windows_style_path\include" >> cabal.project.local echo " extra-lib-dirs: $openssl_windows_style_path" >> cabal.project.local echo "package simplex-chat" >> cabal.project.local -echo " ghc-options: -shared -threaded -optl-L$openssl_windows_style_path -optl-lcrypto-1_1-x64 -o libsimplex.dll libsimplex.dll.def" >> cabal.project.local +echo " ghc-options: -shared -threaded -optl-L$openssl_windows_style_path -optl-lcrypto-3-x64 -o libsimplex.dll libsimplex.dll.def" >> cabal.project.local # Very important! Without it the build fails on linking step since the linker can't find exported symbols. # It looks like GHC bug because with such random path the build ends successfully sed -i "s/ld.lld.exe/abracadabra.exe/" `ghc --print-libdir`/settings -cabal build lib:simplex-chat +cabal build lib:simplex-chat --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library' rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ rm -rf apps/multiplatform/desktop/build/cmake mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ -cp dist-newstyle/openssl-1.1.1w/libcrypto-1_1-x64.dll apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +cp dist-newstyle/openssl-3.0.15/libcrypto-3-x64.dll apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cp libsimplex.dll apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ scripts/desktop/prepare-vlc-windows.sh diff --git a/scripts/desktop/make-appimage-linux.sh b/scripts/desktop/make-appimage-linux.sh index 9cd6f525f5..6cc7aac011 100755 --- a/scripts/desktop/make-appimage-linux.sh +++ b/scripts/desktop/make-appimage-linux.sh @@ -37,9 +37,13 @@ cp *imple*.desktop usr/share/applications/ cp $multiplatform_dir/desktop/src/jvmMain/resources/distribute/*.appdata.xml usr/share/metainfo if [ ! -f ../appimagetool-x86_64.AppImage ]; then - wget https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O ../appimagetool-x86_64.AppImage + wget --secure-protocol=TLSv1_3 https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O ../appimagetool-x86_64.AppImage chmod +x ../appimagetool-x86_64.AppImage fi -../appimagetool-x86_64.AppImage . +if [ ! -f ../runtime-x86_64 ]; then + wget --secure-protocol=TLSv1_3 https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-x86_64 -O ../runtime-x86_64 + chmod +x ../runtime-x86_64 +fi +../appimagetool-x86_64.AppImage --runtime-file ../runtime-x86_64 . mv *imple*.AppImage ../../ diff --git a/scripts/desktop/prepare-openssl-windows.sh b/scripts/desktop/prepare-openssl-windows.sh index 79822d3ff5..ce50ee0a74 100644 --- a/scripts/desktop/prepare-openssl-windows.sh +++ b/scripts/desktop/prepare-openssl-windows.sh @@ -9,12 +9,12 @@ root_dir="$(dirname "$(dirname "$(readlink "$0")")")" cd $root_dir -if [ ! -f dist-newstyle/openssl-1.1.1w/libcrypto-1_1-x64.dll ]; then +if [ ! -f dist-newstyle/openssl-3.0.15/libcrypto-3-x64.dll ]; then mkdir dist-newstyle 2>/dev/null || true cd dist-newstyle - curl https://www.openssl.org/source/openssl-1.1.1w.tar.gz -o openssl.tar.gz + curl --tlsv1.2 https://www.openssl.org/source/openssl-3.0.15.tar.gz -L -o openssl.tar.gz $WINDIR\\System32\\tar.exe -xvzf openssl.tar.gz - cd openssl-1.1.1w + cd openssl-3.0.15 ./Configure mingw64 make cd ../../ diff --git a/scripts/desktop/prepare-vlc-linux.sh b/scripts/desktop/prepare-vlc-linux.sh index a76486150c..dae1c9255d 100755 --- a/scripts/desktop/prepare-vlc-linux.sh +++ b/scripts/desktop/prepare-vlc-linux.sh @@ -14,7 +14,7 @@ mkdir $vlc_dir || exit 0 cd /tmp mkdir tmp 2>/dev/null || true cd tmp -curl https://github.com/cmatomic/VLCplayer-AppImage/releases/download/3.0.11.1/VLC_media_player-3.0.11.1-x86_64.AppImage -L -o appimage +curl --tlsv1.2 https://github.com/cmatomic/VLCplayer-AppImage/releases/download/3.0.11.1/VLC_media_player-3.0.11.1-x86_64.AppImage -L -o appimage chmod +x appimage ./appimage --appimage-extract cp -r squashfs-root/usr/lib/* $vlc_dir @@ -28,7 +28,7 @@ cd /tmp ( mkdir tmp cd tmp -curl http://archive.ubuntu.com/ubuntu/pool/universe/v/vlc/libvlc5_3.0.9.2-1_amd64.deb -o libvlc +curl --tlsv1.2 https://archive.ubuntu.com/ubuntu/pool/universe/v/vlc/libvlc5_3.0.9.2-1_amd64.deb -o libvlc ar p libvlc data.tar.xz > data.tar.xz tar -xvf data.tar.xz mv usr/lib/x86_64-linux-gnu/libvlc.so{.5,} @@ -40,7 +40,7 @@ rm -rf tmp ( mkdir tmp cd tmp -curl http://archive.ubuntu.com/ubuntu/pool/universe/v/vlc/libvlccore9_3.0.9.2-1_amd64.deb -o libvlccore +curl --tlsv1.2 https://archive.ubuntu.com/ubuntu/pool/universe/v/vlc/libvlccore9_3.0.9.2-1_amd64.deb -o libvlccore ar p libvlccore data.tar.xz > data.tar.xz tar -xvf data.tar.xz cp usr/lib/x86_64-linux-gnu/libvlccore.so* $vlc_dir @@ -51,7 +51,7 @@ rm -rf tmp ( mkdir tmp cd tmp -curl http://mirrors.edge.kernel.org/ubuntu/pool/universe/v/vlc/vlc-plugin-base_3.0.9.2-1_amd64.deb -o plugins +curl --tlsv1.2 https://mirrors.edge.kernel.org/ubuntu/pool/universe/v/vlc/vlc-plugin-base_3.0.9.2-1_amd64.deb -o plugins ar p plugins data.tar.xz > data.tar.xz tar -xvf data.tar.xz find usr/lib/x86_64-linux-gnu/vlc/plugins/ -name "lib*.so*" -exec patchelf --set-rpath '$ORIGIN/../../' {} \; @@ -63,7 +63,7 @@ rm -rf tmp ( mkdir tmp cd tmp -curl http://archive.ubuntu.com/ubuntu/pool/main/libi/libidn/libidn11_1.33-2.2ubuntu2_amd64.deb -o idn +curl --tlsv1.2 https://archive.ubuntu.com/ubuntu/pool/main/libi/libidn/libidn11_1.33-2.2ubuntu2_amd64.deb -o idn ar p idn data.tar.xz > data.tar.xz tar -xvf data.tar.xz cp lib/x86_64-linux-gnu/lib* $vlc_dir diff --git a/scripts/desktop/prepare-vlc-mac.sh b/scripts/desktop/prepare-vlc-mac.sh index 25ec1365fc..288b5a1a7d 100755 --- a/scripts/desktop/prepare-vlc-mac.sh +++ b/scripts/desktop/prepare-vlc-mac.sh @@ -23,7 +23,7 @@ mkdir -p $vlc_dir/vlc || exit 0 cd /tmp mkdir tmp 2>/dev/null || true cd tmp -curl https://github.com/simplex-chat/vlc/releases/download/v$vlc_version/vlc-macos-$ARCH.zip -L -o vlc +curl --tlsv1.2 https://github.com/simplex-chat/vlc/releases/download/v$vlc_version/vlc-macos-$ARCH.zip -L -o vlc unzip -oqq vlc install_name_tool -add_rpath "@loader_path/VLC.app/Contents/MacOS/lib" vlc-cache-gen cd VLC.app/Contents/MacOS/lib diff --git a/scripts/desktop/prepare-vlc-windows.sh b/scripts/desktop/prepare-vlc-windows.sh index 680fa7b803..7d2a71a952 100644 --- a/scripts/desktop/prepare-vlc-windows.sh +++ b/scripts/desktop/prepare-vlc-windows.sh @@ -13,7 +13,7 @@ mkdir -p $vlc_dir/vlc || exit 0 cd /tmp mkdir tmp 2>/dev/null || true cd tmp -curl https://irltoolkit.mm.fcix.net/videolan-ftp/vlc/3.0.18/win64/vlc-3.0.18-win64.zip -L -o vlc +curl --tlsv1.2 https://irltoolkit.mm.fcix.net/videolan-ftp/vlc/3.0.18/win64/vlc-3.0.18-win64.zip -L -o vlc $WINDIR\\System32\\tar.exe -xf vlc cd vlc-* # Setting the same date as the date that will be on the file after extraction from JAR to make VLC cache checker happy diff --git a/scripts/flatpak/chat.simplex.simplex.desktop b/scripts/flatpak/chat.simplex.simplex.desktop new file mode 100644 index 0000000000..4f4cd4eece --- /dev/null +++ b/scripts/flatpak/chat.simplex.simplex.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Terminal=false +Name=SimpleX Chat +Comment=A private & encrypted open-source messenger without any user IDs (not even random ones)! +Keywords=chat;message;private;secure;simplex; +Categories=Utility;Chat;InstantMessaging; +Exec=simplex %U +Icon=chat.simplex.simplex +StartupWMClass=simplex-chat diff --git a/scripts/flatpak/chat.simplex.simplex.metainfo.xml b/scripts/flatpak/chat.simplex.simplex.metainfo.xml new file mode 100644 index 0000000000..b15e382207 --- /dev/null +++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml @@ -0,0 +1,453 @@ +<?xml version="1.0" encoding="UTF-8"?> +<component type="desktop-application"> + <id>chat.simplex.simplex</id> + + <name>SimpleX Chat</name> + <summary>A private & encrypted open-source messenger without any user IDs (not even random)!</summary> + + <developer id="chat.simplex"> + <name>SimpleX Chat</name> + </developer> + + <metadata_license>MIT</metadata_license> + <project_license>AGPL-3.0-or-later</project_license> + + <description> + <p>SimpleX - the first messaging platform that has no user identifiers, not even random numbers!</p> + <p>Security assessment was done by Trail of Bits in November 2022.</p> + <p>SimpleX Chat features:</p> + <ol> + <li>end-to-end encrypted messages, with editing, replies and deletion of messages.</li> + <li>sending end-to-end encrypted images and files.</li> + <li>single-use and long-term user addresses.</li> + <li>secret chat groups - only group members know it exists and who is the member.</li> + <li>end-to-end encrypted audio and video calls.</li> + <li>private instant notifications.</li> + <li>portable chat profile - you can transfer your chat contacts and history to another device (terminal or mobile).</li> + </ol> + <p>SimpleX Chat advantages:</p> + <ol> + <li><em>Full privacy of your identity, profile, contacts and metadata:</em> unlike any other existing messaging platform, SimpleX uses no phone numbers or any other identifiers assigned to the users - not even random numbers. This protects the privacy of who you are communicating with, hiding it from SimpleX platform servers and from any observers.</li> + <li><em>Complete protection against spam and abuse:</em> as you have no identifier on SimpleX platform, you cannot be contacted unless you share a one-time invitation link or an optional temporary user address.</li> + <li><em>Full ownership, control and security of your data:</em> SimpleX stores all user data on client devices, the messages are only held temporarily on SimpleX relay servers until they are received.</li> + <li><em>Decentralized network:</em> you can use SimpleX with your own servers and still communicate with people using the servers that are pre-configured in the apps or any other SimpleX servers.</li> + </ol> + <p>You can connect to anybody you know via link or scan QR code (in the video call or in person) and start sending messages instantly - no emails, phone numbers or passwords needed.</p> + <p>Your profile and contacts are only stored in the app on your device - our servers do not have access to this information.</p> + <p>All messages are end-to-end encrypted using open-source double-ratchet protocol; the messages are routed via our servers using open-source SimpleX Messaging Protocol.</p> + </description> + + <releases> + <release version="6.3.4" date="2025-05-12"> + <url type="details">https://simplex.chat/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html</url> + <description> + <p>New in v6.3.1-4:</p> + <ul> + <li>fixes mentions with trailing punctuation (e.g., hello @name!).</li> + <li>recognizes domain names as links (e.g., simplex.chat).</li> + <li>forward compatibility with "knocking" (a feature for group admins to review and to chat with the new members prior to admitting them to groups, it will be released in 6.4)</li> + <li>support for connecting via short connection links.</li> + <li>fix related to backward/forward compatibility of the app in some rare cases.</li> + <li>scrolling/navigation improvements.</li> + <li>faster onboarding (conditions and operators are combined to one screen).</li> + </ul> + <p>New in v6.3.0:</p> + <ul> + <li>Mention members and get notified when mentioned.</li> + <li>Send private reports to moderators.</li> + <li>Delete, block and change role for multiple members at once</li> + <li>Faster sending messages and faster deletion.</li> + <li>Organize chats into lists to keep track of what's important.</li> + <li>Jump to found and forwarded messages.</li> + <li>Private media file names.</li> + <li>Message expiration in chats.</li> + </ul> + </description> + </release> + <release version="6.3.3" date="2025-04-24"> + <url type="details">https://simplex.chat/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html</url> + <description> + <p>New in v6.3.1-3:</p> + <ul> + <li>support for connecting via short connection links.</li> + <li>fix related to backward/forward compatibility of the app in some rare cases.</li> + <li>scrolling/navigation improvements.</li> + <li>faster onboarding (conditions and operators are combined to one screen).</li> + </ul> + <p>New in v6.3.0:</p> + <ul> + <li>Mention members and get notified when mentioned.</li> + <li>Send private reports to moderators.</li> + <li>Delete, block and change role for multiple members at once</li> + <li>Faster sending messages and faster deletion.</li> + <li>Organize chats into lists to keep track of what's important.</li> + <li>Jump to found and forwarded messages.</li> + <li>Private media file names.</li> + <li>Message expiration in chats.</li> + </ul> + </description> + </release> + <release version="6.3.2" date="2025-04-13"> + <url type="details">https://simplex.chat/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html</url> + <description> + <p>New in v6.3.1-2:</p> + <ul> + <li>fix related to backward/forward compatibility of the app in some rare cases.</li> + <li>scrolling/navigation improvements.</li> + <li>faster onboarding (conditions and operators are combined to one screen).</li> + </ul> + <p>New in v6.3.0:</p> + <ul> + <li>Mention members and get notified when mentioned.</li> + <li>Send private reports to moderators.</li> + <li>Delete, block and change role for multiple members at once</li> + <li>Faster sending messages and faster deletion.</li> + <li>Organize chats into lists to keep track of what's important.</li> + <li>Jump to found and forwarded messages.</li> + <li>Private media file names.</li> + <li>Message expiration in chats.</li> + </ul> + </description> + </release> + <release version="6.3.1" date="2025-03-31"> + <url type="details">https://simplex.chat/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html</url> + <description> + <p>New in v6.3.1:</p> + <ul> + <li>scrolling/navigation improvements.</li> + <li>faster onboarding (conditions and operators are combined to one screen).</li> + </ul> + <p>New in v6.3.0:</p> + <ul> + <li>Mention members and get notified when mentioned.</li> + <li>Send private reports to moderators.</li> + <li>Delete, block and change role for multiple members at once</li> + <li>Faster sending messages and faster deletion.</li> + <li>Organize chats into lists to keep track of what's important.</li> + <li>Jump to found and forwarded messages.</li> + <li>Private media file names.</li> + <li>Message expiration in chats.</li> + </ul> + </description> + </release> + <release version="6.3.0" date="2025-03-08"> + <url type="details">https://simplex.chat/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html</url> + <description> + <p>New in v6.3.0:</p> + <ul> + <li>Mention members and get notified when mentioned.</li> + <li>Send private reports to moderators.</li> + <li>Delete, block and change role for multiple members at once</li> + <li>Faster sending messages and faster deletion.</li> + <li>Organize chats into lists to keep track of what's important.</li> + <li>Jump to found and forwarded messages.</li> + <li>Private media file names.</li> + <li>Message expiration in chats.</li> + </ul> + </description> + </release> + <release version="6.2.5" date="2025-02-16"> + <url type="details">https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html</url> + <description> + <p>New in v6.2.1-5:</p> + <ul> + <li>change media filenames when forwarding.</li> + <li>fully delete wallpapers when deleting user or chat.</li> + <li>important fixes</li> + <li>offer to "fix" encryption when calling or making direct connection with member.</li> + <li>broken layout.</li> + <li>option to enable debug logs (disabled by default).</li> + <li>show who reacted in direct chats.</li> + </ul> + <p>New in v6.2:</p> + <ul> + <li>SimpleX Chat and Flux made an agreement to include servers operated by Flux into the app – to improve metadata privacy.</li> + <li>Business chats – your customers privacy.</li> + <li>Improved user experience in chats: open on the first unread, jump to quoted messages, see who reacted.</li> + </ul> + </description> + </release> + <release version="6.2.4" date="2025-01-14"> + <url type="details">https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html</url> + <description> + <p>New in v6.2.1-4:</p> + <ul> + <li>important fixes</li> + <li>offer to "fix" encryption when calling or making direct connection with member.</li> + <li>broken layout.</li> + <li>option to enable debug logs (disabled by default).</li> + <li>show who reacted in direct chats.</li> + </ul> + <p>New in v6.2:</p> + <ul> + <li>SimpleX Chat and Flux made an agreement to include servers operated by Flux into the app – to improve metadata privacy.</li> + <li>Business chats – your customers privacy.</li> + <li>Improved user experience in chats: open on the first unread, jump to quoted messages, see who reacted.</li> + </ul> + </description> + </release> + <release version="6.2.3" date="2024-12-26"> + <url type="details">https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html</url> + <description> + <p>New in v6.2.1-3:</p> + <ul> + <li>important fixes</li> + <li>offer to "fix" encryption when calling or making direct connection with member.</li> + <li>broken layout.</li> + <li>option to enable debug logs (disabled by default).</li> + <li>show who reacted in direct chats.</li> + </ul> + <p>New in v6.2:</p> + <ul> + <li>SimpleX Chat and Flux made an agreement to include servers operated by Flux into the app – to improve metadata privacy.</li> + <li>Business chats – your customers privacy.</li> + <li>Improved user experience in chats: open on the first unread, jump to quoted messages, see who reacted.</li> + </ul> + </description> + </release> + <release version="6.2.2" date="2024-12-25"> + <url type="details">https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html</url> + <description> + <p>New in v6.2.1-2:</p> + <ul> + <li>important fixes</li> + <li>offer to "fix" encryption when calling or making direct connection with member.</li> + <li>broken layout.</li> + <li>option to enable debug logs (disabled by default).</li> + <li>show who reacted in direct chats.</li> + </ul> + <p>New in v6.2:</p> + <ul> + <li>SimpleX Chat and Flux made an agreement to include servers operated by Flux into the app – to improve metadata privacy.</li> + <li>Business chats – your customers privacy.</li> + <li>Improved user experience in chats: open on the first unread, jump to quoted messages, see who reacted.</li> + </ul> + </description> + </release> + <release version="6.2.1" date="2024-12-12"> + <url type="details">https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html</url> + <description> + <p>New in v6.2.1:</p> + <ul> + <li>fixes</li> + <li>offer to "fix" encryption when calling or making direct connection with member.</li> + <li>broken layout.</li> + <li>option to enable debug logs (disabled by default).</li> + <li>show who reacted in direct chats.</li> + </ul> + <p>New in v6.2:</p> + <ul> + <li>SimpleX Chat and Flux made an agreement to include servers operated by Flux into the app – to improve metadata privacy.</li> + <li>Business chats – your customers privacy.</li> + <li>Improved user experience in chats: open on the first unread, jump to quoted messages, see who reacted.</li> + </ul> + </description> + </release> + <release version="6.2.0" date="2024-12-08"> + <url type="details">https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html</url> + <description> + <p>New in v6.2:</p> + <ul> + <li>SimpleX Chat and Flux made an agreement to include servers operated by Flux into the app – to improve metadata privacy.</li> + <li>Business chats – your customers privacy.</li> + <li>Improved user experience in chats: open on the first unread, jump to quoted messages, see who reacted.</li> + </ul> + </description> + </release> + <release version="6.1.1" date="2024-10-18"> + <url type="details">https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html</url> + <description> + <p>New in v6.1 - v6.1.1:</p> + <ul> + <li>Misc fixes</li> + </ul> + <p>New in v6.1:</p> + <p>Better security:</p> + <ul> + <li>SimpleX protocols reviewed by Trail of Bits.</li> + <li>security improvements (don't worry, there is nothing critical there).</li> + </ul> + <p>Better calls:</p> + <ul> + <li>you can switch audio and video during the call.</li> + <li>share the screen from desktop app.</li> + </ul> + <p>Better user experience:</p> + <ul> + <li>switch chat profile for 1-time invitations.</li> + <li>customizable message shape.</li> + <li>better message dates.</li> + <li>forward up to 20 messages at once.</li> + <li>delete or moderate up to 200 messages.</li> + </ul> + </description> + </release> + <release version="6.1.0" date="2024-10-14"> + <url type="details">https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html</url> + <description> + <p>New in v6.1:</p> + <p>Better security:</p> + <ul> + <li>SimpleX protocols reviewed by Trail of Bits.</li> + <li>security improvements (don't worry, there is nothing critical there).</li> + </ul> + <p>Better calls:</p> + <ul> + <li>you can switch audio and video during the call.</li> + <li>share the screen from desktop app.</li> + </ul> + <p>Better user experience:</p> + <ul> + <li>switch chat profile for 1-time invitations.</li> + <li>customizable message shape.</li> + <li>better message dates.</li> + <li>forward up to 20 messages at once.</li> + <li>delete or moderate up to 200 messages.</li> + </ul> + </description> + </release> + <release version="6.0.4" date="2024-08-31"> + <url type="details">https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html</url> + <description> + <p>New in v6.0.1-4:</p> + <ul> + <li>reduce traffic and battery usage on unstable networks.</li> + <li>only offer the next versions for update, based on SerVer ordering.</li> + <li>UI fixes.</li> + <li>reduce app memory usage and start time.</li> + <li>faster sending of files to groups.</li> + <li>fix rare delivery bug.</li> + </ul> + <p>New in v6.0:</p> + <p>New chat experience:</p> + <ul> + <li>connect to your friends faster.</li> + <li>archive contacts to chat later.</li> + <li>delete up to 20 messages at once.</li> + <li>increase font size.</li> + </ul> + <p>New media options:</p> + <ul> + <li>play from the chat list.</li> + <li>blur for better privacy.</li> + </ul> + <p>Private routing:</p> + <ul> + <li>it protects your IP address and connections and is now enabled by default.</li> + </ul> + <p>Connection and servers information:</p> + <ul> + <li>to control your network status and usage.</li> + </ul> + </description> + </release> + <release version="6.0.3" date="2024-08-24"> + <url type="details">https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html</url> + <description> + <p>New in v6.0.1-3:</p> + <ul> + <li>reduce app memory usage and start time.</li> + <li>faster sending files to groups.</li> + <li>fix rare delivery bug.</li> + </ul> + <p>New in v6.0:</p> + <p>New chat experience:</p> + <ul> + <li>connect to your friends faster.</li> + <li>archive contacts to chat later.</li> + <li>delete up to 20 messages at once.</li> + <li>increase font size.</li> + </ul> + <p>New media options:</p> + <ul> + <li>play from the chat list.</li> + <li>blur for better privacy.</li> + </ul> + <p>Private routing:</p> + <ul> + <li>it protects your IP address and connections and is now enabled by default.</li> + </ul> + <p>Connection and servers information:</p> + <ul> + <li>to control your network status and usage.</li> + </ul> + </description> + </release> + <release version="6.0.0" date="2024-08-12"> + <url type="details">https://github.com/simplex-chat/simplex-chat/releases/tag/v6.0.0</url> + <description> + <p>New chat experience:</p> + <ol> + <li>connect to your friends faster.</li> + <li>archive contacts to chat later.</li> + <li>delete up to 20 messages at once.</li> + <li>increase font size.</li> + </ol> + <p>New media options:</p> + <ol> + <li>play from the chat list.</li> + <li>blur for better privacy.</li> + </ol> + <p>Private routing:</p> + <ol> + <li>it protects your IP address and connections and is now enabled by default.</li> + </ol> + <p>Connection and servers information:</p> + <ol> + <li>to control your network status and usage.</li> + </ol> + </description> + </release> + <release version="5.8.2" date="2024-07-02"> + <url type="details">https://github.com/simplex-chat/simplex-chat/releases/tag/v5.8.2</url> + <description> + <p>General:</p> + <ol> + <li>missed call notification.</li> + <li>remove notifications of hidden/removed user profiles.</li> + <li>support for faster connection with the new contacts (disabled in this version).</li> + <li>general fixes.</li> + </ol> + </description> + </release> + <release version="5.8.1" date="2024-06-19"> + <description> + <p>General:</p> + <ol> + <li>fixes in sending/receiving files.</li> + <li>better error reporting when connecting to desktop app.</li> + <li>prevent forwarding to conversations where conversation preferences do not allow message.</li> + </ol> + <p>Android and desktop apps:</p> + <ol> + <li>support transparent theme colors for chat message bubbles.</li> + <li>do not reset changed network settings when switching SOCKS proxy on/off</li> + <li>fix swipe to reply when animation is disabled.</li> + <li>fix bug when duplicate group shown in the UI.</li> + </ol> + </description> + </release> + </releases> + + <content_rating type="oars-1.1" /> + + <url type="homepage">https://simplex.chat/</url> + <url type="bugtracker">https://github.com/simplex-chat/simplex-chat/issues</url> + <url type="donation">https://opencollective.com/simplex-chat</url> + <url type="translate">https://simplex.chat/docs/translations</url> + <url type="faq">https://simplex.chat/faq</url> + <url type="vcs-browser">https://github.com/simplex-chat/simplex-chat</url> + + <launchable type="desktop-id">chat.simplex.simplex.desktop</launchable> + + <branding> + <color type="primary" scheme_preference="light">#a5f0ff</color> + <color type="primary" scheme_preference="dark">#110e26</color> + </branding> + + <screenshots> + <screenshot type="default"> + <image>https://simplex.chat/blog/images/simplex-desktop-light.png</image> + </screenshot> + </screenshots> +</component> diff --git a/scripts/ios/download-libs.sh b/scripts/ios/download-libs.sh index 9d7e388870..6fb6576e94 100755 --- a/scripts/ios/download-libs.sh +++ b/scripts/ios/download-libs.sh @@ -35,7 +35,7 @@ for ((i = 0 ; i < ${#arches[@]}; i++)); do output_arch="${output_arches[$i]}" output_dir="$HOME/Downloads" - curl --location -o "$output_dir"/pkg-ios-"$arch"-swift-json.zip "$job_repo"/"$arch"-darwin-ios:lib:simplex-chat."$arch"-darwin/latest/download/1 && \ + curl --tlsv1.2 --location -o "$output_dir"/pkg-ios-"$arch"-swift-json.zip "$job_repo"/"$arch"-darwin."$arch"-darwin-ios:lib:simplex-chat/latest/download/1 && \ unzip -o "$output_dir"/pkg-ios-"$output_arch"-swift-json.zip -d ~/Downloads/pkg-ios-"$output_arch"-swift-json done -sh "$root_dir"/scripts/ios/prepare-x86_64.sh +sh "$root_dir"/scripts/ios/prepare-x86_64.sh \ No newline at end of file diff --git a/scripts/ios/export-localizations.sh b/scripts/ios/export-localizations.sh index cc6eed25a9..88555ba70b 100755 --- a/scripts/ios/export-localizations.sh +++ b/scripts/ios/export-localizations.sh @@ -2,7 +2,7 @@ set -e -langs=( en bg cs de es fi fr it ja nl pl ru uk zh-Hans ) +langs=( en bg cs de es fi fr hu it ja nl pl ru th tr uk zh-Hans ) for lang in "${langs[@]}"; do echo "***" diff --git a/scripts/ios/import-localizations.sh b/scripts/ios/import-localizations.sh index c699966d79..0f04cd0cde 100755 --- a/scripts/ios/import-localizations.sh +++ b/scripts/ios/import-localizations.sh @@ -2,7 +2,7 @@ set -e -langs=( en bg cs de es fi fr it ja nl pl ru th uk zh-Hans ) +langs=( en bg cs de es fi fr hu it ja nl pl ru th tr uk zh-Hans ) for lang in "${langs[@]}"; do echo "***" @@ -12,7 +12,6 @@ for lang in "${langs[@]}"; do xcodebuild -importLocalizations \ -project ./apps/ios/SimpleX.xcodeproj \ -localizationPath ./apps/ios/SimpleX\ Localizations/$lang.xcloc \ - -disableAutomaticPackageResolution \ -skipPackageUpdates sleep 10 done diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index b22cef750e..84f9d0db34 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."d0588bd0ac23a459cbfc9a4789633014e91ffa19" = "0b17qy74capb0jyli8f3pg1xi4aawhcgpmaz2ykl9g3605png1na"; + "https://github.com/simplex-chat/simplexmq.git"."3d62a383d5dcae6529d6d866233857182bcb4d47" = "133xm8jkim7agd6drwm3lbx1z7v8nf4l3asrm46ag3n2q201yfxc"; "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"; @@ -7,4 +7,6 @@ "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/simplex-chat/android-support.git"."9aa09f148089d6752ce563b14c2df1895718d806" = "0pbf2pf13v2kjzi397nr13f1h3jv0imvsq8rpiyy2qyx5vd50pqn"; "https://github.com/simplex-chat/zip.git"."bd421c6b19cc4c465cd7af1f6f26169fb8ee1ebc" = "1csqfjhvc8wb5h4kxxndmb6iw7b4ib9ff2n81hrizsmnf45a6gg0"; + "https://github.com/yesodweb/wai.git"."ec5e017d896a78e787a5acea62b37a4e677dec2e" = "1ckcpmpjfy9jiqrb52q20lj7ln4hmq9v2jk6kpkf3m68c1m9c2bx"; + "https://github.com/simplex-chat/wai.git"."2f6e5aa5f05ba9140ac99e195ee647b4f7d926b0" = "199g4rjdf1zp1fcw8nqdsyr1h36hmg424qqx03071jk7j00z7ay4"; } diff --git a/scripts/simplex-chat-reproduce-builds.sh b/scripts/simplex-chat-reproduce-builds.sh new file mode 100755 index 0000000000..408d1d4a53 --- /dev/null +++ b/scripts/simplex-chat-reproduce-builds.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env sh +set -eu + +TAG="$1" + +tempdir="$(mktemp -d)" +init_dir="$PWD" + +repo_name="simplex-chat" +repo="https://github.com/simplex-chat/${repo_name}" + +cabal_local='ignore-project: False +package direct-sqlcipher + flags: +openssl' + +export DOCKER_BUILDKIT=1 + +cleanup() { + docker exec -t builder sh -c 'rm -rf ./dist-newstyle' 2>/dev/null || : + rm -rf -- "$tempdir" + docker rm --force builder 2>/dev/null || : + docker image rm local 2>/dev/null || : + cd "$init_dir" +} +trap 'cleanup' EXIT INT + +mkdir -p "$init_dir/$TAG-$repo_name/from-source" "$init_dir/$TAG-$repo_name/prebuilt" + +git -C "$tempdir" clone "$repo.git" &&\ + cd "$tempdir/${repo_name}" &&\ + git checkout "$TAG" + +for os in 22.04 24.04; do + os_url="$(printf '%s' "$os" | tr '.' '_')" + + # Build image + docker build \ + --no-cache \ + --build-arg TAG=${os} \ + --build-arg GHC=9.6.3 \ + -f "$tempdir/${repo_name}/Dockerfile.build" \ + -t local \ + . + + printf '%s' "$cabal_local" > "$tempdir/${repo_name}/cabal.project.local" + + # Run container in background + docker run -t -d \ + --name builder \ + -v "$tempdir/${repo_name}:/project" \ + local + + docker exec \ + -t \ + builder \ + sh -c 'cabal clean && cabal update && cabal build -j --enable-tests && mkdir -p /out && for i in simplex-chat; do bin=$(find /project/dist-newstyle -name "$i" -type f -executable) && chmod +x "$bin" && mv "$bin" /out/; done && strip /out/simplex-chat' + + docker cp \ + builder:/out/simplex-chat \ + "$init_dir/$TAG-$repo_name/from-source/simplex-chat-ubuntu-${os_url}-x86-64" + + # Download prebuilt postgresql binary + curl -L \ + --output-dir "$init_dir/$TAG-$repo_name/prebuilt/" \ + -O \ + "$repo/releases/download/${TAG}/simplex-chat-ubuntu-${os_url}-x86-64" + + # Important! Remove dist-newstyle for the next interation + docker exec \ + -t \ + builder \ + sh -c 'rm -rf ./dist-newstyle' + + # Also restore git to previous state + git reset --hard && git clean -dfx + + # Stop containers, delete images + docker stop builder + docker rm --force builder + docker image rm local +done + +# Cleanup +rm -rf -- "$tempdir" +cd "$init_dir" + +# Final stage: compare hashes + +# Path to binaries +path_bin="$init_dir/$TAG-$repo_name" + +# Assume everything is okay for now +bad=0 + +# Check hashes for all binaries +for file in "$path_bin"/from-source/*; do + # Extract binary name + app="$(basename $file)" + + # Compute hash for compiled binary + compiled=$(sha256sum "$path_bin/from-source/$app" | awk '{print $1}') + # Compute hash for prebuilt binary + prebuilt=$(sha256sum "$path_bin/prebuilt/$app" | awk '{print $1}') + + # Compare + if [ "$compiled" != "$prebuilt" ]; then + # If hashes doesn't match, set bad... + bad=1 + + # ... and print affected binary + printf "%s - sha256sum hash doesn't match\n" "$app" + fi +done + +# If everything is still okay, compute checksums file +if [ "$bad" = 0 ]; then + sha256sum "$path_bin"/from-source/* | sed -e "s|$PWD/||g" -e 's|from-source/||g' -e "s|-$repo_name||g" > "$path_bin/_sha256sums" + + printf 'Checksums computed - %s\n' "$path_bin/_sha256sums" +fi diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 114c22f360..5e2ab32c21 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: 5.4.2.1 +version: 6.3.7.0 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat @@ -16,6 +16,7 @@ license-file: LICENSE build-type: Simple extra-source-files: README.md + PRIVACY.md cabal.project flag swift @@ -23,117 +24,40 @@ flag swift manual: True default: False +flag client_library + description: Don't build server- and CLI-related code. + manual: True + default: False + +flag client_postgres + description: Build with PostgreSQL instead of SQLite. + manual: True + default: False + library exposed-modules: Simplex.Chat - Simplex.Chat.Archive - Simplex.Chat.Bot - Simplex.Chat.Bot.KnownContacts + Simplex.Chat.AppSettings Simplex.Chat.Call Simplex.Chat.Controller - Simplex.Chat.Core Simplex.Chat.Files - Simplex.Chat.Help + Simplex.Chat.Library.Commands + Simplex.Chat.Library.Internal + Simplex.Chat.Library.Subscriber Simplex.Chat.Markdown Simplex.Chat.Messages Simplex.Chat.Messages.Batch Simplex.Chat.Messages.CIContent Simplex.Chat.Messages.CIContent.Events - Simplex.Chat.Migrations.M20220101_initial - Simplex.Chat.Migrations.M20220122_v1_1 - Simplex.Chat.Migrations.M20220205_chat_item_status - Simplex.Chat.Migrations.M20220210_deduplicate_contact_requests - Simplex.Chat.Migrations.M20220224_messages_fks - Simplex.Chat.Migrations.M20220301_smp_servers - Simplex.Chat.Migrations.M20220302_profile_images - Simplex.Chat.Migrations.M20220304_msg_quotes - Simplex.Chat.Migrations.M20220321_chat_item_edited - Simplex.Chat.Migrations.M20220404_files_status_fields - Simplex.Chat.Migrations.M20220514_profiles_user_id - Simplex.Chat.Migrations.M20220626_auto_reply - Simplex.Chat.Migrations.M20220702_calls - Simplex.Chat.Migrations.M20220715_groups_chat_item_id - Simplex.Chat.Migrations.M20220811_chat_items_indices - Simplex.Chat.Migrations.M20220812_incognito_profiles - Simplex.Chat.Migrations.M20220818_chat_notifications - Simplex.Chat.Migrations.M20220822_groups_host_conn_custom_user_profile_id - Simplex.Chat.Migrations.M20220823_delete_broken_group_event_chat_items - Simplex.Chat.Migrations.M20220824_profiles_local_alias - Simplex.Chat.Migrations.M20220909_commands - Simplex.Chat.Migrations.M20220926_connection_alias - Simplex.Chat.Migrations.M20220928_settings - Simplex.Chat.Migrations.M20221001_shared_msg_id_indices - Simplex.Chat.Migrations.M20221003_delete_broken_integrity_error_chat_items - Simplex.Chat.Migrations.M20221004_idx_msg_deliveries_message_id - Simplex.Chat.Migrations.M20221011_user_contact_links_group_id - Simplex.Chat.Migrations.M20221012_inline_files - Simplex.Chat.Migrations.M20221019_unread_chat - Simplex.Chat.Migrations.M20221021_auto_accept__group_links - Simplex.Chat.Migrations.M20221024_contact_used - Simplex.Chat.Migrations.M20221025_chat_settings - Simplex.Chat.Migrations.M20221029_group_link_id - Simplex.Chat.Migrations.M20221112_server_password - Simplex.Chat.Migrations.M20221115_server_cfg - Simplex.Chat.Migrations.M20221129_delete_group_feature_items - Simplex.Chat.Migrations.M20221130_delete_item_deleted - Simplex.Chat.Migrations.M20221209_verified_connection - Simplex.Chat.Migrations.M20221210_idxs - Simplex.Chat.Migrations.M20221211_group_description - Simplex.Chat.Migrations.M20221212_chat_items_timed - Simplex.Chat.Migrations.M20221214_live_message - Simplex.Chat.Migrations.M20221222_chat_ts - Simplex.Chat.Migrations.M20221223_idx_chat_items_item_status - Simplex.Chat.Migrations.M20221230_idxs - Simplex.Chat.Migrations.M20230107_connections_auth_err_counter - Simplex.Chat.Migrations.M20230111_users_agent_user_id - Simplex.Chat.Migrations.M20230117_fkey_indexes - Simplex.Chat.Migrations.M20230118_recreate_smp_servers - Simplex.Chat.Migrations.M20230129_drop_chat_items_group_idx - Simplex.Chat.Migrations.M20230206_item_deleted_by_group_member_id - Simplex.Chat.Migrations.M20230303_group_link_role - Simplex.Chat.Migrations.M20230317_hidden_profiles - Simplex.Chat.Migrations.M20230318_file_description - Simplex.Chat.Migrations.M20230321_agent_file_deleted - Simplex.Chat.Migrations.M20230328_files_protocol - Simplex.Chat.Migrations.M20230402_protocol_servers - Simplex.Chat.Migrations.M20230411_extra_xftp_file_descriptions - Simplex.Chat.Migrations.M20230420_rcv_files_to_receive - Simplex.Chat.Migrations.M20230422_profile_contact_links - Simplex.Chat.Migrations.M20230504_recreate_msg_delivery_events_cleanup_messages - Simplex.Chat.Migrations.M20230505_chat_item_versions - Simplex.Chat.Migrations.M20230511_reactions - Simplex.Chat.Migrations.M20230519_item_deleted_ts - Simplex.Chat.Migrations.M20230526_indexes - Simplex.Chat.Migrations.M20230529_indexes - Simplex.Chat.Migrations.M20230608_deleted_contacts - Simplex.Chat.Migrations.M20230618_favorite_chats - Simplex.Chat.Migrations.M20230621_chat_item_moderations - Simplex.Chat.Migrations.M20230705_delivery_receipts - Simplex.Chat.Migrations.M20230721_group_snd_item_statuses - Simplex.Chat.Migrations.M20230814_indexes - Simplex.Chat.Migrations.M20230827_file_encryption - Simplex.Chat.Migrations.M20230829_connections_chat_vrange - Simplex.Chat.Migrations.M20230903_connections_to_subscribe - Simplex.Chat.Migrations.M20230913_member_contacts - Simplex.Chat.Migrations.M20230914_member_probes - Simplex.Chat.Migrations.M20230926_contact_status - Simplex.Chat.Migrations.M20231002_conn_initiated - Simplex.Chat.Migrations.M20231009_via_group_link_uri_hash - Simplex.Chat.Migrations.M20231010_member_settings - Simplex.Chat.Migrations.M20231019_indexes - Simplex.Chat.Migrations.M20231030_xgrplinkmem_received - Simplex.Chat.Migrations.M20231107_indexes - Simplex.Chat.Migrations.M20231113_group_forward - Simplex.Chat.Migrations.M20231114_remote_control - Simplex.Chat.Migrations.M20231126_remote_ctrl_address - Simplex.Chat.Migrations.M20231207_chat_list_pagination - Simplex.Chat.Migrations.M20231214_item_content_tag - Simplex.Chat.Migrations.M20231215_recreate_msg_deliveries Simplex.Chat.Mobile Simplex.Chat.Mobile.File Simplex.Chat.Mobile.Shared Simplex.Chat.Mobile.WebRTC + Simplex.Chat.Operators + Simplex.Chat.Operators.Conditions + Simplex.Chat.Operators.Presets Simplex.Chat.Options + Simplex.Chat.Options.DB Simplex.Chat.ProfileGenerator Simplex.Chat.Protocol Simplex.Chat.Remote @@ -143,31 +67,182 @@ library Simplex.Chat.Remote.RevHTTP Simplex.Chat.Remote.Transport Simplex.Chat.Remote.Types + Simplex.Chat.Stats Simplex.Chat.Store + Simplex.Chat.Store.AppSettings Simplex.Chat.Store.Connections Simplex.Chat.Store.Direct Simplex.Chat.Store.Files Simplex.Chat.Store.Groups Simplex.Chat.Store.Messages - Simplex.Chat.Store.Migrations + Simplex.Chat.Store.NoteFolders Simplex.Chat.Store.Profiles Simplex.Chat.Store.Remote Simplex.Chat.Store.Shared Simplex.Chat.Styled - Simplex.Chat.Terminal - Simplex.Chat.Terminal.Input - Simplex.Chat.Terminal.Notification - Simplex.Chat.Terminal.Output Simplex.Chat.Types Simplex.Chat.Types.Preferences + Simplex.Chat.Types.Shared + Simplex.Chat.Types.UITheme Simplex.Chat.Types.Util Simplex.Chat.Util - Simplex.Chat.View + if !flag(client_library) + exposed-modules: + Simplex.Chat.Bot + Simplex.Chat.Bot.KnownContacts + Simplex.Chat.Core + Simplex.Chat.Help + Simplex.Chat.Terminal + Simplex.Chat.Terminal.Input + Simplex.Chat.Terminal.Main + Simplex.Chat.Terminal.Notification + Simplex.Chat.Terminal.Output + Simplex.Chat.View + if flag(client_postgres) + exposed-modules: + Simplex.Chat.Options.Postgres + Simplex.Chat.Store.Postgres.Migrations + Simplex.Chat.Store.Postgres.Migrations.M20241220_initial + Simplex.Chat.Store.Postgres.Migrations.M20250402_short_links + Simplex.Chat.Store.Postgres.Migrations.M20250512_member_admission + else + exposed-modules: + Simplex.Chat.Archive + Simplex.Chat.Options.SQLite + Simplex.Chat.Store.SQLite.Migrations + Simplex.Chat.Store.SQLite.Migrations.M20220101_initial + Simplex.Chat.Store.SQLite.Migrations.M20220122_v1_1 + Simplex.Chat.Store.SQLite.Migrations.M20220205_chat_item_status + Simplex.Chat.Store.SQLite.Migrations.M20220210_deduplicate_contact_requests + Simplex.Chat.Store.SQLite.Migrations.M20220224_messages_fks + Simplex.Chat.Store.SQLite.Migrations.M20220301_smp_servers + Simplex.Chat.Store.SQLite.Migrations.M20220302_profile_images + Simplex.Chat.Store.SQLite.Migrations.M20220304_msg_quotes + Simplex.Chat.Store.SQLite.Migrations.M20220321_chat_item_edited + Simplex.Chat.Store.SQLite.Migrations.M20220404_files_status_fields + Simplex.Chat.Store.SQLite.Migrations.M20220514_profiles_user_id + Simplex.Chat.Store.SQLite.Migrations.M20220626_auto_reply + Simplex.Chat.Store.SQLite.Migrations.M20220702_calls + Simplex.Chat.Store.SQLite.Migrations.M20220715_groups_chat_item_id + Simplex.Chat.Store.SQLite.Migrations.M20220811_chat_items_indices + Simplex.Chat.Store.SQLite.Migrations.M20220812_incognito_profiles + Simplex.Chat.Store.SQLite.Migrations.M20220818_chat_notifications + Simplex.Chat.Store.SQLite.Migrations.M20220822_groups_host_conn_custom_user_profile_id + Simplex.Chat.Store.SQLite.Migrations.M20220823_delete_broken_group_event_chat_items + Simplex.Chat.Store.SQLite.Migrations.M20220824_profiles_local_alias + Simplex.Chat.Store.SQLite.Migrations.M20220909_commands + Simplex.Chat.Store.SQLite.Migrations.M20220926_connection_alias + Simplex.Chat.Store.SQLite.Migrations.M20220928_settings + Simplex.Chat.Store.SQLite.Migrations.M20221001_shared_msg_id_indices + Simplex.Chat.Store.SQLite.Migrations.M20221003_delete_broken_integrity_error_chat_items + Simplex.Chat.Store.SQLite.Migrations.M20221004_idx_msg_deliveries_message_id + Simplex.Chat.Store.SQLite.Migrations.M20221011_user_contact_links_group_id + Simplex.Chat.Store.SQLite.Migrations.M20221012_inline_files + Simplex.Chat.Store.SQLite.Migrations.M20221019_unread_chat + Simplex.Chat.Store.SQLite.Migrations.M20221021_auto_accept__group_links + Simplex.Chat.Store.SQLite.Migrations.M20221024_contact_used + Simplex.Chat.Store.SQLite.Migrations.M20221025_chat_settings + Simplex.Chat.Store.SQLite.Migrations.M20221029_group_link_id + Simplex.Chat.Store.SQLite.Migrations.M20221112_server_password + Simplex.Chat.Store.SQLite.Migrations.M20221115_server_cfg + Simplex.Chat.Store.SQLite.Migrations.M20221129_delete_group_feature_items + Simplex.Chat.Store.SQLite.Migrations.M20221130_delete_item_deleted + Simplex.Chat.Store.SQLite.Migrations.M20221209_verified_connection + Simplex.Chat.Store.SQLite.Migrations.M20221210_idxs + Simplex.Chat.Store.SQLite.Migrations.M20221211_group_description + Simplex.Chat.Store.SQLite.Migrations.M20221212_chat_items_timed + Simplex.Chat.Store.SQLite.Migrations.M20221214_live_message + Simplex.Chat.Store.SQLite.Migrations.M20221222_chat_ts + Simplex.Chat.Store.SQLite.Migrations.M20221223_idx_chat_items_item_status + Simplex.Chat.Store.SQLite.Migrations.M20221230_idxs + Simplex.Chat.Store.SQLite.Migrations.M20230107_connections_auth_err_counter + Simplex.Chat.Store.SQLite.Migrations.M20230111_users_agent_user_id + Simplex.Chat.Store.SQLite.Migrations.M20230117_fkey_indexes + Simplex.Chat.Store.SQLite.Migrations.M20230118_recreate_smp_servers + Simplex.Chat.Store.SQLite.Migrations.M20230129_drop_chat_items_group_idx + Simplex.Chat.Store.SQLite.Migrations.M20230206_item_deleted_by_group_member_id + Simplex.Chat.Store.SQLite.Migrations.M20230303_group_link_role + Simplex.Chat.Store.SQLite.Migrations.M20230317_hidden_profiles + Simplex.Chat.Store.SQLite.Migrations.M20230318_file_description + Simplex.Chat.Store.SQLite.Migrations.M20230321_agent_file_deleted + Simplex.Chat.Store.SQLite.Migrations.M20230328_files_protocol + Simplex.Chat.Store.SQLite.Migrations.M20230402_protocol_servers + Simplex.Chat.Store.SQLite.Migrations.M20230411_extra_xftp_file_descriptions + Simplex.Chat.Store.SQLite.Migrations.M20230420_rcv_files_to_receive + Simplex.Chat.Store.SQLite.Migrations.M20230422_profile_contact_links + Simplex.Chat.Store.SQLite.Migrations.M20230504_recreate_msg_delivery_events_cleanup_messages + Simplex.Chat.Store.SQLite.Migrations.M20230505_chat_item_versions + Simplex.Chat.Store.SQLite.Migrations.M20230511_reactions + Simplex.Chat.Store.SQLite.Migrations.M20230519_item_deleted_ts + Simplex.Chat.Store.SQLite.Migrations.M20230526_indexes + Simplex.Chat.Store.SQLite.Migrations.M20230529_indexes + Simplex.Chat.Store.SQLite.Migrations.M20230608_deleted_contacts + Simplex.Chat.Store.SQLite.Migrations.M20230618_favorite_chats + Simplex.Chat.Store.SQLite.Migrations.M20230621_chat_item_moderations + Simplex.Chat.Store.SQLite.Migrations.M20230705_delivery_receipts + Simplex.Chat.Store.SQLite.Migrations.M20230721_group_snd_item_statuses + Simplex.Chat.Store.SQLite.Migrations.M20230814_indexes + Simplex.Chat.Store.SQLite.Migrations.M20230827_file_encryption + Simplex.Chat.Store.SQLite.Migrations.M20230829_connections_chat_vrange + Simplex.Chat.Store.SQLite.Migrations.M20230903_connections_to_subscribe + Simplex.Chat.Store.SQLite.Migrations.M20230913_member_contacts + Simplex.Chat.Store.SQLite.Migrations.M20230914_member_probes + Simplex.Chat.Store.SQLite.Migrations.M20230926_contact_status + Simplex.Chat.Store.SQLite.Migrations.M20231002_conn_initiated + Simplex.Chat.Store.SQLite.Migrations.M20231009_via_group_link_uri_hash + Simplex.Chat.Store.SQLite.Migrations.M20231010_member_settings + Simplex.Chat.Store.SQLite.Migrations.M20231019_indexes + Simplex.Chat.Store.SQLite.Migrations.M20231030_xgrplinkmem_received + Simplex.Chat.Store.SQLite.Migrations.M20231107_indexes + Simplex.Chat.Store.SQLite.Migrations.M20231113_group_forward + Simplex.Chat.Store.SQLite.Migrations.M20231114_remote_control + Simplex.Chat.Store.SQLite.Migrations.M20231126_remote_ctrl_address + Simplex.Chat.Store.SQLite.Migrations.M20231207_chat_list_pagination + Simplex.Chat.Store.SQLite.Migrations.M20231214_item_content_tag + Simplex.Chat.Store.SQLite.Migrations.M20231215_recreate_msg_deliveries + Simplex.Chat.Store.SQLite.Migrations.M20240102_note_folders + Simplex.Chat.Store.SQLite.Migrations.M20240104_members_profile_update + Simplex.Chat.Store.SQLite.Migrations.M20240115_block_member_for_all + Simplex.Chat.Store.SQLite.Migrations.M20240122_indexes + Simplex.Chat.Store.SQLite.Migrations.M20240214_redirect_file_id + Simplex.Chat.Store.SQLite.Migrations.M20240222_app_settings + Simplex.Chat.Store.SQLite.Migrations.M20240226_users_restrict + Simplex.Chat.Store.SQLite.Migrations.M20240228_pq + Simplex.Chat.Store.SQLite.Migrations.M20240313_drop_agent_ack_cmd_id + Simplex.Chat.Store.SQLite.Migrations.M20240324_custom_data + Simplex.Chat.Store.SQLite.Migrations.M20240402_item_forwarded + Simplex.Chat.Store.SQLite.Migrations.M20240430_ui_theme + Simplex.Chat.Store.SQLite.Migrations.M20240501_chat_deleted + Simplex.Chat.Store.SQLite.Migrations.M20240510_chat_items_via_proxy + Simplex.Chat.Store.SQLite.Migrations.M20240515_rcv_files_user_approved_relays + Simplex.Chat.Store.SQLite.Migrations.M20240528_quota_err_counter + Simplex.Chat.Store.SQLite.Migrations.M20240827_calls_uuid + Simplex.Chat.Store.SQLite.Migrations.M20240920_user_order + Simplex.Chat.Store.SQLite.Migrations.M20241008_indexes + Simplex.Chat.Store.SQLite.Migrations.M20241010_contact_requests_contact_id + Simplex.Chat.Store.SQLite.Migrations.M20241023_chat_item_autoincrement_id + Simplex.Chat.Store.SQLite.Migrations.M20241027_server_operators + Simplex.Chat.Store.SQLite.Migrations.M20241125_indexes + Simplex.Chat.Store.SQLite.Migrations.M20241128_business_chats + Simplex.Chat.Store.SQLite.Migrations.M20241205_business_chat_members + Simplex.Chat.Store.SQLite.Migrations.M20241222_operator_conditions + Simplex.Chat.Store.SQLite.Migrations.M20241223_chat_tags + Simplex.Chat.Store.SQLite.Migrations.M20241230_reports + Simplex.Chat.Store.SQLite.Migrations.M20250105_indexes + Simplex.Chat.Store.SQLite.Migrations.M20250115_chat_ttl + Simplex.Chat.Store.SQLite.Migrations.M20250122_chat_items_include_in_history + Simplex.Chat.Store.SQLite.Migrations.M20250126_mentions + Simplex.Chat.Store.SQLite.Migrations.M20250129_delete_unused_contacts + Simplex.Chat.Store.SQLite.Migrations.M20250130_indexes + Simplex.Chat.Store.SQLite.Migrations.M20250402_short_links + Simplex.Chat.Store.SQLite.Migrations.M20250512_member_admission other-modules: Paths_simplex_chat hs-source-dirs: src - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns + default-extensions: + StrictData + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -180,10 +255,10 @@ library , containers ==0.6.* , crypton ==0.34.* , data-default ==0.7.* - , direct-sqlcipher ==2.3.* , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* + , file-embed ==0.0.15.* , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 @@ -192,344 +267,212 @@ library , network >=3.1.2.7 && <3.2 , network-transport ==0.5.6 , optparse-applicative >=0.15 && <0.17 - , process ==1.6.* , random >=1.1 && <1.3 , record-hasfield ==1.0.* + , scientific ==0.3.7.* , simple-logger ==0.1.* - , simplexmq >=5.0 + , simplexmq >=6.3 , socks ==0.6.* - , sqlcipher-simple ==0.4.* , stm ==2.5.* , terminal ==0.2.* , time ==1.12.* - , tls >=1.7.0 && <1.8 + , tls >=1.9.0 && <1.10 , unliftio ==0.2.* , unliftio-core ==0.2.* + , uuid ==1.3.* , zip ==2.0.* default-language: Haskell2010 if flag(swift) cpp-options: -DswiftJSON + if flag(client_postgres) + build-depends: + postgresql-libpq >=0.10.0.0 + , postgresql-simple ==0.7.* + , raw-strings-qq ==1.1.* + cpp-options: -DdbPostgres + else + build-depends: + direct-sqlcipher ==2.3.* + , sqlcipher-simple ==0.4.* if impl(ghc >= 9.6.2) build-depends: bytestring ==0.11.* + , process ==1.6.* , template-haskell ==2.20.* , text >=2.0.1 && <2.2 if impl(ghc < 9.6.2) build-depends: bytestring ==0.10.* + , process >=1.6 && <1.6.18 , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + , text >=1.2.4.0 && <1.3 executable simplex-bot + if flag(client_library) + buildable: False main-is: Main.hs other-modules: Paths_simplex_chat hs-source-dirs: apps/simplex-bot - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded + default-extensions: + StrictData + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: - aeson ==2.2.* - , ansi-terminal >=0.10 && <0.12 - , async ==2.2.* - , attoparsec ==0.14.* - , base >=4.7 && <5 - , base64-bytestring >=1.0 && <1.3 - , composition ==1.0.* - , constraints >=0.12 && <0.14 - , containers ==0.6.* - , crypton ==0.34.* - , data-default ==0.7.* - , direct-sqlcipher ==2.3.* + base >=4.7 && <5 , directory ==1.3.* - , email-validate ==2.3.* - , exceptions ==0.10.* - , filepath ==1.4.* - , http-types ==0.12.* - , http2 >=4.2.2 && <4.3 - , memory ==0.18.* - , mtl >=2.3.1 && <3.0 - , network >=3.1.2.7 && <3.2 - , network-transport ==0.5.6 - , optparse-applicative >=0.15 && <0.17 - , process ==1.6.* - , random >=1.1 && <1.3 - , record-hasfield ==1.0.* - , simple-logger ==0.1.* , simplex-chat - , simplexmq >=5.0 - , socks ==0.6.* - , sqlcipher-simple ==0.4.* - , stm ==2.5.* - , terminal ==0.2.* - , time ==1.12.* - , tls >=1.7.0 && <1.8 - , unliftio ==0.2.* - , unliftio-core ==0.2.* - , zip ==2.0.* default-language: Haskell2010 - if flag(swift) - cpp-options: -DswiftJSON - if impl(ghc >= 9.6.2) - build-depends: - bytestring ==0.11.* - , template-haskell ==2.20.* - , text >=2.0.1 && <2.2 - if impl(ghc < 9.6.2) - build-depends: - bytestring ==0.10.* - , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + if flag(client_postgres) + cpp-options: -DdbPostgres executable simplex-bot-advanced + if flag(client_library) + buildable: False main-is: Main.hs other-modules: Paths_simplex_chat hs-source-dirs: apps/simplex-bot-advanced - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded + default-extensions: + StrictData + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: - aeson ==2.2.* - , ansi-terminal >=0.10 && <0.12 - , async ==2.2.* - , attoparsec ==0.14.* + async ==2.2.* , base >=4.7 && <5 - , base64-bytestring >=1.0 && <1.3 - , composition ==1.0.* - , constraints >=0.12 && <0.14 - , containers ==0.6.* - , crypton ==0.34.* - , data-default ==0.7.* - , direct-sqlcipher ==2.3.* , directory ==1.3.* - , email-validate ==2.3.* - , exceptions ==0.10.* - , filepath ==1.4.* - , http-types ==0.12.* - , http2 >=4.2.2 && <4.3 - , memory ==0.18.* - , mtl >=2.3.1 && <3.0 - , network >=3.1.2.7 && <3.2 - , network-transport ==0.5.6 - , optparse-applicative >=0.15 && <0.17 - , process ==1.6.* - , random >=1.1 && <1.3 - , record-hasfield ==1.0.* - , simple-logger ==0.1.* , simplex-chat - , simplexmq >=5.0 - , socks ==0.6.* - , sqlcipher-simple ==0.4.* + , simplexmq >=6.3 , stm ==2.5.* - , terminal ==0.2.* - , time ==1.12.* - , tls >=1.7.0 && <1.8 - , unliftio ==0.2.* - , unliftio-core ==0.2.* - , zip ==2.0.* default-language: Haskell2010 - if flag(swift) - cpp-options: -DswiftJSON + if flag(client_postgres) + cpp-options: -DdbPostgres if impl(ghc >= 9.6.2) build-depends: - bytestring ==0.11.* - , template-haskell ==2.20.* - , text >=2.0.1 && <2.2 + text >=2.0.1 && <2.2 if impl(ghc < 9.6.2) build-depends: - bytestring ==0.10.* - , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + text >=1.2.4.0 && <1.3 executable simplex-broadcast-bot - main-is: ../Main.hs + if flag(client_library) + buildable: False + main-is: Main.hs + hs-source-dirs: + apps/simplex-broadcast-bot + apps/simplex-broadcast-bot/src + default-extensions: + StrictData other-modules: Broadcast.Bot Broadcast.Options Paths_simplex_chat - hs-source-dirs: - apps/simplex-broadcast-bot/src - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: - aeson ==2.2.* - , ansi-terminal >=0.10 && <0.12 - , async ==2.2.* - , attoparsec ==0.14.* + async ==2.2.* , base >=4.7 && <5 - , base64-bytestring >=1.0 && <1.3 - , composition ==1.0.* - , constraints >=0.12 && <0.14 - , containers ==0.6.* - , crypton ==0.34.* - , data-default ==0.7.* - , direct-sqlcipher ==2.3.* , directory ==1.3.* - , email-validate ==2.3.* - , exceptions ==0.10.* - , filepath ==1.4.* - , http-types ==0.12.* - , http2 >=4.2.2 && <4.3 - , memory ==0.18.* - , mtl >=2.3.1 && <3.0 - , network >=3.1.2.7 && <3.2 - , network-transport ==0.5.6 , optparse-applicative >=0.15 && <0.17 - , process ==1.6.* - , random >=1.1 && <1.3 - , record-hasfield ==1.0.* - , simple-logger ==0.1.* , simplex-chat - , simplexmq >=5.0 - , socks ==0.6.* - , sqlcipher-simple ==0.4.* + , simplexmq >=6.3 , stm ==2.5.* - , terminal ==0.2.* - , time ==1.12.* - , tls >=1.7.0 && <1.8 - , unliftio ==0.2.* - , unliftio-core ==0.2.* - , zip ==2.0.* default-language: Haskell2010 - if flag(swift) - cpp-options: -DswiftJSON + if flag(client_postgres) + cpp-options: -DdbPostgres if impl(ghc >= 9.6.2) build-depends: - bytestring ==0.11.* - , template-haskell ==2.20.* - , text >=2.0.1 && <2.2 + text >=2.0.1 && <2.2 if impl(ghc < 9.6.2) build-depends: - bytestring ==0.10.* - , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + text >=1.2.4.0 && <1.3 executable simplex-chat + if flag(client_library) + buildable: False main-is: Main.hs other-modules: Server Paths_simplex_chat hs-source-dirs: apps/simplex-chat - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded + default-extensions: + StrictData + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: aeson ==2.2.* - , ansi-terminal >=0.10 && <0.12 - , async ==2.2.* - , attoparsec ==0.14.* , base >=4.7 && <5 - , base64-bytestring >=1.0 && <1.3 - , composition ==1.0.* - , constraints >=0.12 && <0.14 - , containers ==0.6.* - , crypton ==0.34.* - , data-default ==0.7.* - , direct-sqlcipher ==2.3.* , directory ==1.3.* - , email-validate ==2.3.* - , exceptions ==0.10.* - , filepath ==1.4.* - , http-types ==0.12.* - , http2 >=4.2.2 && <4.3 - , memory ==0.18.* , mtl >=2.3.1 && <3.0 , network ==3.1.* - , network-transport ==0.5.6 - , optparse-applicative >=0.15 && <0.17 - , process ==1.6.* - , random >=1.1 && <1.3 - , record-hasfield ==1.0.* - , simple-logger ==0.1.* , simplex-chat - , simplexmq >=5.0 - , socks ==0.6.* - , sqlcipher-simple ==0.4.* + , simplexmq >=6.3 , stm ==2.5.* - , terminal ==0.2.* - , time ==1.12.* - , tls >=1.7.0 && <1.8 , unliftio ==0.2.* - , unliftio-core ==0.2.* , websockets ==0.12.* - , zip ==2.0.* default-language: Haskell2010 - if flag(swift) - cpp-options: -DswiftJSON + if flag(client_postgres) + cpp-options: -DdbPostgres if impl(ghc >= 9.6.2) build-depends: - bytestring ==0.11.* - , template-haskell ==2.20.* - , text >=2.0.1 && <2.2 + text >=2.0.1 && <2.2 if impl(ghc < 9.6.2) build-depends: - bytestring ==0.10.* - , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + text >=1.2.4.0 && <1.3 executable simplex-directory-service - main-is: ../Main.hs + if flag(client_library) + buildable: False + main-is: Main.hs + hs-source-dirs: + apps/simplex-directory-service + apps/simplex-directory-service/src + default-extensions: + StrictData other-modules: + Directory.BlockedWords + Directory.Captcha Directory.Events Directory.Options Directory.Search Directory.Service Directory.Store Paths_simplex_chat - hs-source-dirs: - apps/simplex-directory-service/src - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: aeson ==2.2.* - , ansi-terminal >=0.10 && <0.12 , async ==2.2.* , attoparsec ==0.14.* , base >=4.7 && <5 - , base64-bytestring >=1.0 && <1.3 , composition ==1.0.* - , constraints >=0.12 && <0.14 , containers ==0.6.* - , crypton ==0.34.* - , data-default ==0.7.* - , direct-sqlcipher ==2.3.* , directory ==1.3.* - , email-validate ==2.3.* - , exceptions ==0.10.* - , filepath ==1.4.* - , http-types ==0.12.* - , http2 >=4.2.2 && <4.3 - , memory ==0.18.* , mtl >=2.3.1 && <3.0 - , network >=3.1.2.7 && <3.2 - , network-transport ==0.5.6 , optparse-applicative >=0.15 && <0.17 - , process ==1.6.* + , process >=1.6 && <1.6.18 , random >=1.1 && <1.3 - , record-hasfield ==1.0.* , simple-logger ==0.1.* , simplex-chat - , simplexmq >=5.0 - , socks ==0.6.* - , sqlcipher-simple ==0.4.* + , simplexmq >=6.3 , stm ==2.5.* - , terminal ==0.2.* , time ==1.12.* - , tls >=1.7.0 && <1.8 - , unliftio ==0.2.* - , unliftio-core ==0.2.* - , zip ==2.0.* + , unicode-transforms ==0.4.* default-language: Haskell2010 - if flag(swift) - cpp-options: -DswiftJSON + if flag(client_postgres) + cpp-options: -DdbPostgres if impl(ghc >= 9.6.2) build-depends: bytestring ==0.11.* - , template-haskell ==2.20.* , text >=2.0.1 && <2.2 if impl(ghc < 9.6.2) build-depends: bytestring ==0.10.* - , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + , text >=1.2.4.0 && <1.3 test-suite simplex-chat-test + if flag(swift) + cpp-options: -DswiftJSON + if flag(client_library) + buildable: False type: exitcode-stdio-1.0 main-is: Test.hs other-modules: @@ -539,33 +482,49 @@ test-suite simplex-chat-test ChatTests ChatTests.ChatList ChatTests.Direct + ChatTests.DBUtils ChatTests.Files + ChatTests.Forward ChatTests.Groups + ChatTests.Local ChatTests.Profiles ChatTests.Utils + JSONFixtures JSONTests MarkdownTests MessageBatching - MobileTests + OperatorTests ProtocolTests + RandomServers RemoteTests - SchemaDump ValidNames ViewTests - WebRTCTests Broadcast.Bot Broadcast.Options + Directory.BlockedWords + Directory.Captcha Directory.Events Directory.Options Directory.Search Directory.Service Directory.Store Paths_simplex_chat + if flag(client_postgres) + other-modules: + ChatTests.DBUtils.Postgres + else + other-modules: + ChatTests.DBUtils.SQLite + MobileTests + SchemaDump + WebRTCTests hs-source-dirs: tests apps/simplex-broadcast-bot/src apps/simplex-directory-service/src - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded + default-extensions: + StrictData + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: QuickCheck ==2.14.* , aeson ==2.2.* @@ -575,51 +534,44 @@ test-suite simplex-chat-test , base >=4.7 && <5 , base64-bytestring >=1.0 && <1.3 , composition ==1.0.* - , constraints >=0.12 && <0.14 , containers ==0.6.* , crypton ==0.34.* - , data-default ==0.7.* , deepseq ==1.4.* - , direct-sqlcipher ==2.3.* , directory ==1.3.* - , email-validate ==2.3.* - , exceptions ==0.10.* , filepath ==1.4.* , generic-random ==1.5.* - , hspec ==2.11.* , http-types ==0.12.* - , http2 >=4.2.2 && <4.3 , memory ==0.18.* , mtl >=2.3.1 && <3.0 , network ==3.1.* - , network-transport ==0.5.6 , optparse-applicative >=0.15 && <0.17 - , process ==1.6.* , random >=1.1 && <1.3 - , record-hasfield ==1.0.* , silently ==1.2.* , simple-logger ==0.1.* , simplex-chat - , simplexmq >=5.0 - , socks ==0.6.* - , sqlcipher-simple ==0.4.* + , simplexmq >=6.3 , stm ==2.5.* , terminal ==0.2.* , time ==1.12.* - , tls >=1.7.0 && <1.8 + , unicode-transforms ==0.4.* , unliftio ==0.2.* - , unliftio-core ==0.2.* - , zip ==2.0.* default-language: Haskell2010 - if flag(swift) - cpp-options: -DswiftJSON + if flag(client_postgres) + build-depends: + postgresql-simple ==0.7.* + cpp-options: -DdbPostgres + else + build-depends: + sqlcipher-simple ==0.4.* if impl(ghc >= 9.6.2) build-depends: bytestring ==0.11.* - , template-haskell ==2.20.* + , hspec ==2.11.* + , process ==1.6.* , text >=2.0.1 && <2.2 if impl(ghc < 9.6.2) build-depends: bytestring ==0.10.* - , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + , hspec ==2.7.* + , process >=1.6 && <1.6.18 + , text >=1.2.4.0 && <1.3 diff --git a/src/.hspec b/src/.hspec new file mode 100644 index 0000000000..28f079001c --- /dev/null +++ b/src/.hspec @@ -0,0 +1 @@ +--times diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index eb16a492ee..6b554d29c4 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -2,121 +2,54 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} +{-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} -{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} -{-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module Simplex.Chat where -import Control.Applicative (optional, (<|>)) -import Control.Concurrent.STM (retry) import Control.Logger.Simple import Control.Monad import Control.Monad.Except import Control.Monad.IO.Unlift -import Control.Monad.Reader -import qualified Data.Aeson as J -import Data.Attoparsec.ByteString.Char8 (Parser) -import qualified Data.Attoparsec.ByteString.Char8 as A -import Data.Bifunctor (bimap, first) -import Data.ByteArray (ScrubbedBytes) -import qualified Data.ByteArray as BA -import qualified Data.ByteString.Base64 as B64 -import Data.ByteString.Builder (toLazyByteString) -import Data.ByteString.Char8 (ByteString) -import qualified Data.ByteString.Char8 as B -import qualified Data.ByteString.Lazy.Char8 as LB -import Data.Char -import Data.Constraint (Dict (..)) -import Data.Either (fromRight, lefts, partitionEithers, rights) -import Data.Fixed (div') -import Data.Functor (($>)) -import Data.Int (Int64) -import Data.List (find, foldl', isSuffixOf, partition, sortOn) -import Data.List.NonEmpty (NonEmpty (..), nonEmpty, toList, (<|)) +import Data.Bifunctor (bimap, second) +import Data.List (partition, sortOn) +import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L -import Data.Map.Strict (Map) import qualified Data.Map.Strict as M -import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList) +import Data.Maybe (fromMaybe, mapMaybe) import Data.Text (Text) -import qualified Data.Text as T -import Data.Text.Encoding (decodeLatin1, encodeUtf8) -import Data.Time (NominalDiffTime, addUTCTime, defaultTimeLocale, formatTime) -import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDay, nominalDiffTimeToSeconds) -import Data.Time.Clock.System (systemToUTCTime) -import Data.Word (Word32) -import qualified Database.SQLite.Simple as SQL -import Simplex.Chat.Archive -import Simplex.Chat.Call +import Data.Time.Clock (getCurrentTime) import Simplex.Chat.Controller -import Simplex.Chat.Files -import Simplex.Chat.Markdown -import Simplex.Chat.Messages -import Simplex.Chat.Messages.Batch (MsgBatch (..), batchMessages) -import Simplex.Chat.Messages.CIContent -import Simplex.Chat.Messages.CIContent.Events +import Simplex.Chat.Library.Commands +import Simplex.Chat.Operators +import Simplex.Chat.Operators.Presets import Simplex.Chat.Options -import Simplex.Chat.ProfileGenerator (generateRandomProfile) +import Simplex.Chat.Options.DB import Simplex.Chat.Protocol -import Simplex.Chat.Remote -import Simplex.Chat.Remote.Types import Simplex.Chat.Store -import Simplex.Chat.Store.Connections -import Simplex.Chat.Store.Direct -import Simplex.Chat.Store.Files -import Simplex.Chat.Store.Groups -import Simplex.Chat.Store.Messages import Simplex.Chat.Store.Profiles -import Simplex.Chat.Store.Shared import Simplex.Chat.Types -import Simplex.Chat.Types.Preferences -import Simplex.Chat.Types.Util -import Simplex.Chat.Util (encryptFile, shuffle) -import Simplex.FileTransfer.Client.Main (maxFileSize) +import Simplex.Chat.Util (shuffle) import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) -import Simplex.FileTransfer.Description (ValidFileDescription, gb, kb, mb) -import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI) import Simplex.Messaging.Agent as Agent -import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, temporaryAgentError) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig) -import Simplex.Messaging.Agent.Lock +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), ServerCfg (..), allRoles, createAgentStore, defaultAgentConfig, presetServerCfg) import Simplex.Messaging.Agent.Protocol -import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration, withConnection) -import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) -import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB -import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations +import Simplex.Messaging.Agent.Store.Common (DBStore (dbNew)) +import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..), MigrationError) import Simplex.Messaging.Client (defaultNetworkConfig) import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) -import qualified Simplex.Messaging.Crypto.File as CF -import Simplex.Messaging.Encoding -import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (base64P) -import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI, SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol) -import qualified Simplex.Messaging.Protocol as SMP +import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), ProtocolType (..), SProtocolType (..), SubscriptionMode (..), UserProtocol) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport.Client (defaultSocksProxy) -import Simplex.Messaging.Util -import Simplex.Messaging.Version -import Simplex.RemoteControl.Invitation (RCInvitation (..), RCSignedInvitation (..)) -import Simplex.RemoteControl.Types (RCCtrlAddress (..)) -import System.Exit (ExitCode, exitFailure, exitSuccess) -import System.FilePath (takeFileName, (</>)) -import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, stdout) -import System.Random (randomRIO) -import Text.Read (readMaybe) -import UnliftIO.Async -import UnliftIO.Concurrent (forkFinally, forkIO, mkWeakThreadId, threadDelay) -import UnliftIO.Directory import qualified UnliftIO.Exception as E -import UnliftIO.IO (hClose, hSeek, hTell, openFile) import UnliftIO.STM defaultChatConfig :: ChatConfig @@ -124,25 +57,44 @@ defaultChatConfig = ChatConfig { agentConfig = defaultAgentConfig - { tcpPort = undefined, -- agent does not listen to TCP + { tcpPort = Nothing, -- agent does not listen to TCP tbqSize = 1024 }, chatVRange = supportedChatVRange, confirmMigrations = MCConsole, - defaultServers = - DefaultAgentServers - { smp = _defaultSMPServers, + -- this property should NOT use operator = Nothing + -- non-operator servers can be passed via options + presetServers = + PresetServers + { operators = + [ PresetOperator + { operator = Just operatorSimpleXChat, + smp = simplexChatSMPServers, + useSMP = 4, + xftp = map (presetServer True) $ L.toList defaultXFTPServers, + useXFTP = 3 + }, + PresetOperator + { operator = Just operatorFlux, + smp = fluxSMPServers, + useSMP = 3, + xftp = fluxXFTPServers, + useXFTP = 3 + } + ], ntf = _defaultNtfServers, - xftp = defaultXFTPServers, netCfg = defaultNetworkConfig }, + -- please note: if these servers are changed, this option needs to be split to two, + -- to have a different set of servers on the receiving end and on the sending end. + -- To preserve backward compatibility receiving end should update before the sending. + shortLinkPresetServers = allPresetServers, + presetDomains = [".simplex.im", ".simplexonflux.com"], tbqSize = 1024, fileChunkSize = 15780, -- do not change xftpDescrPartSize = 14000, inlineFiles = defaultInlineFilesConfig, autoAcceptFileSize = 0, - xftpFileConfig = Just defaultXFTPFileConfig, - tempDir = Nothing, showReactions = False, showReceipts = False, logLevel = CLLImportant, @@ -155,105 +107,90 @@ defaultChatConfig = ciExpirationInterval = 30 * 60 * 1000000, -- 30 minutes coreApi = False, highlyAvailable = False, - deviceNameForRemote = "" + deviceNameForRemote = "", + chatHooks = defaultChatHooks } -_defaultSMPServers :: NonEmpty SMPServerWithAuth -_defaultSMPServers = - L.fromList - [ "smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion", - "smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion", - "smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion", - "smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion", - "smp://hejn2gVIqNU6xjtGM3OwQeuk8ZEbDXVJXAlnSBJBWUA=@smp16.simplex.im,p3ktngodzi6qrf7w64mmde3syuzrv57y55hxabqcq3l5p6oi7yzze6qd.onion", - "smp://ZKe4uxF4Z_aLJJOEsC-Y6hSkXgQS5-oc442JQGkyP8M=@smp17.simplex.im,ogtwfxyi3h2h5weftjjpjmxclhb5ugufa5rcyrmg7j4xlch7qsr5nuqd.onion", - "smp://PtsqghzQKU83kYTlQ1VKg996dW4Cw4x_bvpKmiv8uns=@smp18.simplex.im,lyqpnwbs2zqfr45jqkncwpywpbtq7jrhxnib5qddtr6npjyezuwd3nqd.onion", - "smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion" - ] - -_defaultNtfServers :: [NtfServer] -_defaultNtfServers = ["ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,ntg7jdjy2i3qbib3sykiho3enekwiaqg3icctliqhtqcg6jmoh6cxiad.onion"] - -maxImageSize :: Integer -maxImageSize = 261120 * 2 -- auto-receive on mobiles - -imageExtensions :: [String] -imageExtensions = [".jpg", ".jpeg", ".png", ".gif"] - -maxMsgReactions :: Int -maxMsgReactions = 3 - -fixedImagePreview :: ImageData -fixedImagePreview = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg==" - -smallGroupsRcptsMemLimit :: Int -smallGroupsRcptsMemLimit = 20 - logCfg :: LogConfig logCfg = LogConfig {lc_file = Nothing, lc_stderr = True} -createChatDatabase :: FilePath -> ScrubbedBytes -> Bool -> MigrationConfirmation -> IO (Either MigrationError ChatDatabase) -createChatDatabase filePrefix key keepKey confirmMigrations = runExceptT $ do - chatStore <- ExceptT $ createChatStore (chatStoreFile filePrefix) key keepKey confirmMigrations - agentStore <- ExceptT $ createAgentStore (agentStoreFile filePrefix) key keepKey confirmMigrations +createChatDatabase :: ChatDbOpts -> MigrationConfirmation -> IO (Either MigrationError ChatDatabase) +createChatDatabase chatDbOpts confirmMigrations = runExceptT $ do + chatStore <- ExceptT $ createChatStore (toDBOpts chatDbOpts chatSuffix False) confirmMigrations + agentStore <- ExceptT $ createAgentStore (toDBOpts chatDbOpts agentSuffix False) confirmMigrations pure ChatDatabase {chatStore, agentStore} newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Bool -> IO ChatController newChatController ChatDatabase {chatStore, agentStore} user - cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, tempDir, deviceNameForRemote} - ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, deviceName, optFilesFolder, showReactions, allowInstantFiles, autoAcceptFileSize} + cfg@ChatConfig {agentConfig = aCfg, presetServers, inlineFiles, deviceNameForRemote, confirmMigrations} + ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, deviceName, highlyAvailable, yesToUpMigrations}, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} backgroundMode = do let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False} - config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable} + confirmMigrations' = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations + config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, presetServers = presetServers', inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable, confirmMigrations = confirmMigrations'} firstTime = dbNew chatStore currentUser <- newTVarIO user + randomPresetServers <- chooseRandomServers presetServers' + let rndSrvs = L.toList randomPresetServers + operatorWithId (i, op) = (\o -> o {operatorId = DBEntityId i}) <$> pOperator op + opDomains = operatorDomains $ mapMaybe operatorWithId $ zip [1 ..] rndSrvs + agentSMP <- randomServerCfgs "agent SMP servers" SPSMP opDomains rndSrvs + agentXFTP <- randomServerCfgs "agent XFTP servers" SPXFTP opDomains rndSrvs + let randomAgentServers = RandomAgentServers {smpServers = agentSMP, xftpServers = agentXFTP} currentRemoteHost <- newTVarIO Nothing - servers <- agentServers config + servers <- withTransaction chatStore $ \db -> agentServers db config randomPresetServers randomAgentServers smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore backgroundMode agentAsync <- newTVarIO Nothing random <- liftIO C.newRandom + eventSeq <- newTVarIO 0 inputQ <- newTBQueueIO tbqSize outputQ <- newTBQueueIO tbqSize - connNetworkStatuses <- atomically TM.empty + connNetworkStatuses <- TM.emptyIO subscriptionMode <- newTVarIO SMSubscribe chatLock <- newEmptyTMVarIO + entityLocks <- TM.emptyIO sndFiles <- newTVarIO M.empty rcvFiles <- newTVarIO M.empty - currentCalls <- atomically TM.empty + currentCalls <- TM.emptyIO localDeviceName <- newTVarIO $ fromMaybe deviceNameForRemote deviceName multicastSubscribers <- newTMVarIO 0 remoteSessionSeq <- newTVarIO 0 - remoteHostSessions <- atomically TM.empty + remoteHostSessions <- TM.emptyIO remoteHostsFolder <- newTVarIO Nothing remoteCtrlSession <- newTVarIO Nothing filesFolder <- newTVarIO optFilesFolder chatStoreChanged <- newTVarIO False - expireCIThreads <- newTVarIO M.empty - expireCIFlags <- newTVarIO M.empty + expireCIThreads <- TM.emptyIO + expireCIFlags <- TM.emptyIO cleanupManagerAsync <- newTVarIO Nothing - timedItemThreads <- atomically TM.empty + timedItemThreads <- TM.emptyIO + chatActivated <- newTVarIO True showLiveItems <- newTVarIO False encryptLocalFiles <- newTVarIO False - userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg - tempDirectory <- newTVarIO tempDir + tempDirectory <- newTVarIO optTempDirectory + assetsDirectory <- newTVarIO Nothing contactMergeEnabled <- newTVarIO True pure ChatController { firstTime, currentUser, + randomPresetServers, + randomAgentServers, currentRemoteHost, smpAgent, agentAsync, chatStore, chatStoreChanged, random, + eventSeq, inputQ, outputQ, connNetworkStatuses, subscriptionMode, chatLock, + entityLocks, sndFiles, rcvFiles, currentCalls, @@ -269,6322 +206,85 @@ newChatController expireCIFlags, cleanupManagerAsync, timedItemThreads, + chatActivated, showLiveItems, encryptLocalFiles, - userXFTPFileConfig, tempDirectory, + assetsDirectory, logFilePath = logFile, contactMergeEnabled } where - configServers :: DefaultAgentServers - configServers = - let smp' = fromMaybe (defaultServers.smp) (nonEmpty smpServers) - xftp' = fromMaybe (defaultServers.xftp) (nonEmpty xftpServers) - in defaultServers {smp = smp', xftp = xftp', netCfg = networkConfig} - agentServers :: ChatConfig -> IO InitialAgentServers - agentServers config@ChatConfig {defaultServers = defServers@DefaultAgentServers {ntf, netCfg}} = do - users <- withTransaction chatStore getUsers - smp' <- getUserServers users SPSMP - xftp' <- getUserServers users SPXFTP - pure InitialAgentServers {smp = smp', xftp = xftp', ntf, netCfg} + presetServers' :: PresetServers + presetServers' = presetServers {operators = operators', netCfg = netCfg'} where - getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => [User] -> SProtocolType p -> IO (Map UserId (NonEmpty (ProtoServerWithAuth p))) - getUserServers users protocol = case users of - [] -> pure $ M.fromList [(1, cfgServers protocol defServers)] - _ -> M.fromList <$> initialServers - where - initialServers :: IO [(UserId, NonEmpty (ProtoServerWithAuth p))] - initialServers = mapM (\u -> (aUserId u,) <$> userServers u) users - userServers :: User -> IO (NonEmpty (ProtoServerWithAuth p)) - userServers user' = activeAgentServers config protocol <$> withTransaction chatStore (`getProtocolServers` user') - -activeAgentServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [ServerCfg p] -> NonEmpty (ProtoServerWithAuth p) -activeAgentServers ChatConfig {defaultServers} p = - fromMaybe (cfgServers p defaultServers) - . nonEmpty - . map (\ServerCfg {server} -> server) - . filter (\ServerCfg {enabled} -> enabled) - -cfgServers :: UserProtocol p => SProtocolType p -> (DefaultAgentServers -> NonEmpty (ProtoServerWithAuth p)) -cfgServers p s = case p of - SPSMP -> s.smp - SPXFTP -> s.xftp - -startChatController :: forall m. ChatMonad' m => Bool -> Bool -> Bool -> m (Async ()) -startChatController subConns enableExpireCIs startXFTPWorkers = do - asks smpAgent >>= resumeAgentClient - unless subConns $ - chatWriteVar subscriptionMode SMOnlyCreate - users <- fromRight [] <$> runExceptT (withStoreCtx' (Just "startChatController, getUsers") getUsers) - restoreCalls - s <- asks agentAsync - readTVarIO s >>= maybe (start s users) (pure . fst) - where - start s users = do - a1 <- async agentSubscriber - a2 <- - if subConns - then Just <$> async (subscribeUsers False users) - else pure Nothing - atomically . writeTVar s $ Just (a1, a2) - when startXFTPWorkers $ do - startXFTP - void $ forkIO $ startFilesToReceive users - startCleanupManager - when enableExpireCIs $ startExpireCIs users - pure a1 - startXFTP = do - tmp <- readTVarIO =<< asks tempDirectory - runExceptT (withAgent $ \a -> xftpStartWorkers a tmp) >>= \case - Left e -> liftIO $ print $ "Error starting XFTP workers: " <> show e - Right _ -> pure () - startCleanupManager = do - cleanupAsync <- asks cleanupManagerAsync - readTVarIO cleanupAsync >>= \case - Nothing -> do - a <- Just <$> async (void $ runExceptT cleanupManager) - atomically $ writeTVar cleanupAsync a - _ -> pure () - startExpireCIs users = - forM_ users $ \user -> do - ttl <- fromRight Nothing <$> runExceptT (withStoreCtx' (Just "startExpireCIs, getChatItemTTL") (`getChatItemTTL` user)) - forM_ ttl $ \_ -> do - startExpireCIThread user - setExpireCIFlag user True - -subscribeUsers :: forall m. ChatMonad' m => Bool -> [User] -> m () -subscribeUsers onlyNeeded users = do - let (us, us') = partition activeUser users - vr <- chatVersionRange - subscribe vr us - subscribe vr us' - where - subscribe :: VersionRange -> [User] -> m () - subscribe vr = mapM_ $ runExceptT . subscribeUserConnections vr onlyNeeded Agent.subscribeConnections - -startFilesToReceive :: forall m. ChatMonad' m => [User] -> m () -startFilesToReceive users = do - let (us, us') = partition activeUser users - startReceive us - startReceive us' - where - startReceive :: [User] -> m () - startReceive = mapM_ $ runExceptT . startReceiveUserFiles - -startReceiveUserFiles :: ChatMonad m => User -> m () -startReceiveUserFiles user = do - filesToReceive <- withStoreCtx' (Just "startReceiveUserFiles, getRcvFilesToReceive") (`getRcvFilesToReceive` user) - forM_ filesToReceive $ \ft -> - flip catchChatError (toView . CRChatError (Just user)) $ - toView =<< receiveFile' user ft Nothing Nothing - -restoreCalls :: ChatMonad' m => m () -restoreCalls = do - savedCalls <- fromRight [] <$> runExceptT (withStoreCtx' (Just "restoreCalls, getCalls") $ \db -> getCalls db) - let callsMap = M.fromList $ map (\call@Call {contactId} -> (contactId, call)) savedCalls - calls <- asks currentCalls - atomically $ writeTVar calls callsMap - -stopChatController :: forall m. MonadUnliftIO m => ChatController -> m () -stopChatController ChatController {smpAgent, agentAsync = s, sndFiles, rcvFiles, expireCIFlags, remoteHostSessions, remoteCtrlSession} = do - readTVarIO remoteHostSessions >>= mapM_ (liftIO . cancelRemoteHost False . snd) - atomically (stateTVar remoteCtrlSession (,Nothing)) >>= mapM_ (liftIO . cancelRemoteCtrl False . snd) - disconnectAgentClient smpAgent - readTVarIO s >>= mapM_ (\(a1, a2) -> uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2) - closeFiles sndFiles - closeFiles rcvFiles - atomically $ do - keys <- M.keys <$> readTVar expireCIFlags - forM_ keys $ \k -> TM.insert k False expireCIFlags - writeTVar s Nothing - where - closeFiles :: TVar (Map Int64 Handle) -> m () - closeFiles files = do - fs <- readTVarIO files - mapM_ hClose fs - atomically $ writeTVar files M.empty - -execChatCommand :: ChatMonad' m => Maybe RemoteHostId -> ByteString -> m ChatResponse -execChatCommand rh s = do - u <- readTVarIO =<< asks currentUser - case parseChatCommand s of - Left e -> pure $ chatCmdError u e - Right cmd -> case rh of - Just rhId - | allowRemoteCommand cmd -> execRemoteCommand u rhId cmd s - | otherwise -> pure $ CRChatCmdError u $ ChatErrorRemoteHost (RHId rhId) $ RHELocalCommand - _ -> execChatCommand_ u cmd - -execChatCommand' :: ChatMonad' m => ChatCommand -> m ChatResponse -execChatCommand' cmd = asks currentUser >>= readTVarIO >>= (`execChatCommand_` cmd) - -execChatCommand_ :: ChatMonad' m => Maybe User -> ChatCommand -> m ChatResponse -execChatCommand_ u cmd = handleCommandError u $ processChatCommand cmd - -execRemoteCommand :: ChatMonad' m => Maybe User -> RemoteHostId -> ChatCommand -> ByteString -> m ChatResponse -execRemoteCommand u rhId cmd s = handleCommandError u $ getRemoteHostClient rhId >>= \rh -> processRemoteCommand rhId rh cmd s - -handleCommandError :: ChatMonad' m => Maybe User -> ExceptT ChatError m ChatResponse -> m ChatResponse -handleCommandError u a = either (CRChatCmdError u) id <$> (runExceptT a `E.catches` ioErrors) - where - ioErrors = - [ E.Handler $ \(e :: ExitCode) -> E.throwIO e, - E.Handler $ pure . Left . mkChatError - ] - -parseChatCommand :: ByteString -> Either String ChatCommand -parseChatCommand = A.parseOnly chatCommandP . B.dropWhileEnd isSpace - --- | Chat API commands interpreted in context of a local zone -processChatCommand :: forall m. ChatMonad m => ChatCommand -> m ChatResponse -processChatCommand cmd = chatVersionRange >>= (`processChatCommand'` cmd) -{-# INLINE processChatCommand #-} - -processChatCommand' :: forall m. ChatMonad m => VersionRange -> ChatCommand -> m ChatResponse -processChatCommand' vr = \case - ShowActiveUser -> withUser' $ pure . CRActiveUser - CreateActiveUser NewUser {profile, sameServers, pastTimestamp} -> do - forM_ profile $ \Profile {displayName} -> checkValidName displayName - p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile - u <- asks currentUser - (smp, smpServers) <- chooseServers SPSMP - (xftp, xftpServers) <- chooseServers SPXFTP - auId <- - withStore' getUsers >>= \case - [] -> pure 1 - users -> do - when (any (\User {localDisplayName = n} -> n == displayName) users) $ - throwChatError (CEUserExists displayName) - withAgent (\a -> createUser a smp xftp) - ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure - user <- withStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts - when (auId == 1) $ withStore (\db -> createContact db user simplexContactProfile) `catchChatError` \_ -> pure () - storeServers user smpServers - storeServers user xftpServers - atomically . writeTVar u $ Just user - pure $ CRActiveUser user - where - chooseServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> m (NonEmpty (ProtoServerWithAuth p), [ServerCfg p]) - chooseServers protocol - | sameServers = - asks currentUser >>= readTVarIO >>= \case - Nothing -> throwChatError CENoActiveUser - Just user -> do - servers <- withStore' (`getProtocolServers` user) - cfg <- asks config - pure (activeAgentServers cfg protocol servers, servers) - | otherwise = do - defServers <- asks $ defaultServers . config - pure (cfgServers protocol defServers, []) - storeServers user servers = - unless (null servers) . withStore $ - \db -> overwriteProtocolServers db user servers - coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day) - day = 86400 - ListUsers -> CRUsersList <$> withStoreCtx' (Just "ListUsers, getUsersInfo") getUsersInfo - APISetActiveUser userId' viewPwd_ -> do - unlessM chatStarted $ throwChatError CEChatNotStarted - user_ <- chatReadVar currentUser - user' <- privateGetUser userId' - validateUserPassword_ user_ user' viewPwd_ - withStoreCtx' (Just "APISetActiveUser, setActiveUser") $ \db -> setActiveUser db userId' - let user'' = user' {activeUser = True} - chatWriteVar currentUser $ Just user'' - pure $ CRActiveUser user'' - SetActiveUser uName viewPwd_ -> do - tryChatError (withStore (`getUserIdByName` uName)) >>= \case - Left _ -> throwChatError CEUserUnknown - Right userId -> processChatCommand $ APISetActiveUser userId viewPwd_ - SetAllContactReceipts onOff -> withUser $ \_ -> withStore' (`updateAllContactReceipts` onOff) >> ok_ - APISetUserContactReceipts userId' settings -> withUser $ \user -> do - user' <- privateGetUser userId' - validateUserPassword user user' Nothing - withStore' $ \db -> updateUserContactReceipts db user' settings - ok user - SetUserContactReceipts settings -> withUser $ \User {userId} -> processChatCommand $ APISetUserContactReceipts userId settings - APISetUserGroupReceipts userId' settings -> withUser $ \user -> do - user' <- privateGetUser userId' - validateUserPassword user user' Nothing - withStore' $ \db -> updateUserGroupReceipts db user' settings - ok user - SetUserGroupReceipts settings -> withUser $ \User {userId} -> processChatCommand $ APISetUserGroupReceipts userId settings - APIHideUser userId' (UserPwd viewPwd) -> withUser $ \user -> do - user' <- privateGetUser userId' - case viewPwdHash user' of - Just _ -> throwChatError $ CEUserAlreadyHidden userId' - _ -> do - when (T.null viewPwd) $ throwChatError $ CEEmptyUserPassword userId' - users <- withStore' getUsers - unless (length (filter (isNothing . viewPwdHash) users) > 1) $ throwChatError $ CECantHideLastUser userId' - viewPwdHash' <- hashPassword - setUserPrivacy user user' {viewPwdHash = viewPwdHash', showNtfs = False} + PresetServers {operators, netCfg} = presetServers + netCfg' = updateNetworkConfig netCfg simpleNetCfg + operators' = case (smpServers, xftpServers) of + ([], []) -> operators + (smpSrvs, []) -> L.map disableSMP operators <> [custom smpSrvs []] + ([], xftpSrvs) -> L.map disableXFTP operators <> [custom [] xftpSrvs] + (smpSrvs, xftpSrvs) -> [custom smpSrvs xftpSrvs] + disableSMP op@PresetOperator {smp} = (op :: PresetOperator) {smp = map disableSrv smp} + disableXFTP op@PresetOperator {xftp} = (op :: PresetOperator) {xftp = map disableSrv xftp} + disableSrv :: forall p. NewUserServer p -> NewUserServer p + disableSrv srv = (srv :: NewUserServer p) {enabled = False} + custom smpSrvs xftpSrvs = + PresetOperator + { operator = Nothing, + smp = map newUserServer smpSrvs, + useSMP = 0, + xftp = map newUserServer xftpSrvs, + useXFTP = 0 + } + randomServerCfgs :: UserProtocol p => String -> SProtocolType p -> [(Text, ServerOperator)] -> [PresetOperator] -> IO (NonEmpty (ServerCfg p)) + randomServerCfgs name p opDomains rndSrvs = + toJustOrError name $ L.nonEmpty $ agentServerCfgs p opDomains $ concatMap (pServers p) rndSrvs + agentServers :: DB.Connection -> ChatConfig -> NonEmpty PresetOperator -> RandomAgentServers -> IO InitialAgentServers + agentServers db ChatConfig {presetServers = PresetServers {ntf, netCfg}, presetDomains} presetOps as = do + users <- getUsers db + ops <- getUpdateServerOperators db presetOps (null users) + let opDomains = operatorDomains $ mapMaybe snd ops + (smp', xftp') <- unzip <$> mapM (getServers ops opDomains) users + pure InitialAgentServers {smp = M.fromList (optServers smp' smpServers), xftp = M.fromList (optServers xftp' xftpServers), ntf, netCfg, presetDomains} where - hashPassword = do - salt <- drgRandomBytes 16 - let hash = B64UrlByteString $ C.sha512Hash $ encodeUtf8 viewPwd <> salt - pure $ Just UserPwdHash {hash, salt = B64UrlByteString salt} - APIUnhideUser userId' viewPwd@(UserPwd pwd) -> withUser $ \user -> do - user' <- privateGetUser userId' - case viewPwdHash user' of - Nothing -> throwChatError $ CEUserNotHidden userId' - _ -> do - when (T.null pwd) $ throwChatError $ CEEmptyUserPassword userId' - validateUserPassword user user' $ Just viewPwd - setUserPrivacy user user' {viewPwdHash = Nothing, showNtfs = True} - APIMuteUser userId' -> setUserNotifications userId' False - APIUnmuteUser userId' -> setUserNotifications userId' True - HideUser viewPwd -> withUser $ \User {userId} -> processChatCommand $ APIHideUser userId viewPwd - UnhideUser viewPwd -> withUser $ \User {userId} -> processChatCommand $ APIUnhideUser userId viewPwd - MuteUser -> withUser $ \User {userId} -> processChatCommand $ APIMuteUser userId - UnmuteUser -> withUser $ \User {userId} -> processChatCommand $ APIUnmuteUser userId - APIDeleteUser userId' delSMPQueues viewPwd_ -> withUser $ \user -> do - user' <- privateGetUser userId' - validateUserPassword user user' viewPwd_ - checkDeleteChatUser user' - withChatLock "deleteUser" . procCmd $ deleteChatUser user' delSMPQueues - DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \userId -> APIDeleteUser userId delSMPQueues viewPwd_ - StartChat subConns enableExpireCIs startXFTPWorkers -> withUser' $ \_ -> - asks agentAsync >>= readTVarIO >>= \case - Just _ -> pure CRChatRunning - _ -> checkStoreNotChanged $ startChatController subConns enableExpireCIs startXFTPWorkers $> CRChatStarted - APIStopChat -> do - ask >>= stopChatController - pure CRChatStopped - APIActivateChat restoreChat -> withUser $ \_ -> do - when restoreChat restoreCalls - withAgent foregroundAgent - when restoreChat $ do - users <- withStoreCtx' (Just "APIActivateChat, getUsers") getUsers - void . forkIO $ subscribeUsers True users - void . forkIO $ startFilesToReceive users - setAllExpireCIFlags True - ok_ - APISuspendChat t -> do - setAllExpireCIFlags False - stopRemoteCtrl - withAgent (`suspendAgent` t) - ok_ - ResubscribeAllConnections -> withStoreCtx' (Just "ResubscribeAllConnections, getUsers") getUsers >>= subscribeUsers False >> ok_ - -- has to be called before StartChat - SetTempFolder tf -> do - createDirectoryIfMissing True tf - asks tempDirectory >>= atomically . (`writeTVar` Just tf) - ok_ - SetFilesFolder ff -> do - createDirectoryIfMissing True ff - asks filesFolder >>= atomically . (`writeTVar` Just ff) - ok_ - SetRemoteHostsFolder rf -> do - createDirectoryIfMissing True rf - chatWriteVar remoteHostsFolder $ Just rf - ok_ - APISetXFTPConfig cfg -> do - asks userXFTPFileConfig >>= atomically . (`writeTVar` cfg) - ok_ - APISetEncryptLocalFiles on -> chatWriteVar encryptLocalFiles on >> ok_ - SetContactMergeEnabled onOff -> do - asks contactMergeEnabled >>= atomically . (`writeTVar` onOff) - ok_ - APIExportArchive cfg -> checkChatStopped $ exportArchive cfg >> ok_ - ExportArchive -> do - ts <- liftIO getCurrentTime - let filePath = "simplex-chat." <> formatTime defaultTimeLocale "%FT%H%M%SZ" ts <> ".zip" - processChatCommand $ APIExportArchive $ ArchiveConfig filePath Nothing Nothing - APIImportArchive cfg -> checkChatStopped $ do - fileErrs <- importArchive cfg - setStoreChanged - pure $ CRArchiveImported fileErrs - APIDeleteStorage -> withStoreChanged deleteStorage - APIStorageEncryption cfg -> withStoreChanged $ sqlCipherExport cfg - ExecChatStoreSQL query -> CRSQLResult <$> withStore' (`execSQL` query) - ExecAgentStoreSQL query -> CRSQLResult <$> withAgent (`execAgentStoreSQL` query) - SlowSQLQueries -> do - ChatController {chatStore, smpAgent} <- ask - chatQueries <- slowQueries chatStore - agentQueries <- slowQueries $ agentClientStore smpAgent - pure CRSlowSQLQueries {chatQueries, agentQueries} - where - slowQueries st = - liftIO $ - map (uncurry SlowSQLQuery . first SQL.fromQuery) - . sortOn (timeAvg . snd) - . M.assocs - <$> withConnection st (readTVarIO . DB.slow) - APIGetChats {userId, pendingConnections, pagination, query} -> withUserId' userId $ \user -> do - (errs, previews) <- partitionEithers <$> withStore' (\db -> getChatPreviews db vr user pendingConnections pagination query) - unless (null errs) $ toView $ CRChatErrors (Just user) (map ChatErrorStore errs) - pure $ CRApiChats user previews - APIGetChat (ChatRef cType cId) pagination search -> withUser $ \user -> case cType of - -- TODO optimize queries calculating ChatStats, currently they're disabled - CTDirect -> do - directChat <- withStore (\db -> getDirectChat db user cId pagination search) - pure $ CRApiChat user (AChat SCTDirect directChat) - CTGroup -> do - groupChat <- withStore (\db -> getGroupChat db vr user cId pagination search) - pure $ CRApiChat user (AChat SCTGroup groupChat) - CTContactRequest -> pure $ chatCmdError (Just user) "not implemented" - CTContactConnection -> pure $ chatCmdError (Just user) "not supported" - APIGetChatItems pagination search -> withUser $ \user -> do - chatItems <- withStore $ \db -> getAllChatItems db vr user pagination search - pure $ CRChatItems user Nothing chatItems - APIGetChatItemInfo chatRef itemId -> withUser $ \user -> do - (aci@(AChatItem cType dir _ ci), versions) <- withStore $ \db -> - (,) <$> getAChatItem db vr user chatRef itemId <*> liftIO (getChatItemVersions db itemId) - let itemVersions = if null versions then maybeToList $ mkItemVersion ci else versions - memberDeliveryStatuses <- case (cType, dir) of - (SCTGroup, SMDSnd) -> do - withStore' (`getGroupSndStatuses` itemId) >>= \case - [] -> pure Nothing - memStatuses -> pure $ Just $ map (uncurry MemberDeliveryStatus) memStatuses - _ -> pure Nothing - pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses} - APISendMessage (ChatRef cType chatId) live itemTTL (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user@User {userId} -> withChatLock "sendMessage" $ case cType of - CTDirect -> do - ct@Contact {contactId, contactUsed} <- withStore $ \db -> getContact db user chatId - assertDirectAllowed user MDSnd ct XMsgNew_ - unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct - if isVoice mc && not (featureAllowed SCFVoice forUser ct) - then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (chatFeatureNameText CFVoice)) + optServers :: [(UserId, NonEmpty (ServerCfg p))] -> [ProtoServerWithAuth p] -> [(UserId, NonEmpty (ServerCfg p))] + optServers srvs overrides_ = case L.nonEmpty overrides_ of + Just overrides -> map (second $ const $ L.map (presetServerCfg True allRoles Nothing) overrides) srvs + Nothing -> srvs + getServers :: [(Maybe PresetOperator, Maybe ServerOperator)] -> [(Text, ServerOperator)] -> User -> IO ((UserId, NonEmpty (ServerCfg 'PSMP)), (UserId, NonEmpty (ServerCfg 'PXFTP))) + getServers ops opDomains user' = do + smpSrvs <- getProtocolServers db SPSMP user' + xftpSrvs <- getProtocolServers db SPXFTP user' + uss <- groupByOperator' (ops, smpSrvs, xftpSrvs) + ts <- getCurrentTime + uss' <- mapM (setUserServers' db user' ts . updatedUserServers) uss + let auId = aUserId user' + pure $ bimap (auId,) (auId,) $ useServers as opDomains uss' + +chooseRandomServers :: PresetServers -> IO (NonEmpty PresetOperator) +chooseRandomServers PresetServers {operators} = + forM operators $ \op -> do + smp' <- opSrvs SPSMP op + xftp' <- opSrvs SPXFTP op + pure (op :: PresetOperator) {smp = smp', xftp = xftp'} + where + opSrvs :: forall p. UserProtocol p => SProtocolType p -> PresetOperator -> IO [NewUserServer p] + opSrvs p op = do + let srvs = pServers p op + toUse = operatorServersToUse p op + (enbldSrvs, dsbldSrvs) = partition (\UserServer {enabled} -> enabled) srvs + if toUse <= 0 || toUse >= length enbldSrvs + then pure srvs else do - (fInv_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer ct - timed_ <- sndContactCITimed live ct itemTTL - (msgContainer, quotedItem_) <- prepareMsg fInv_ timed_ - (msg@SndMessage {sharedMsgId}, _) <- sendDirectContactMessage ct (XMsgNew msgContainer) - ci <- saveSndChatItem' user (CDDirectSnd ct) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live - case ft_ of - Just ft@FileTransferMeta {fileInline = Just IFMSent} -> - sendDirectFileInline ct ft sharedMsgId - _ -> pure () - forM_ (timed_ >>= timedDeleteAt') $ - startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) - pure $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) - where - setupSndFileTransfer :: Contact -> m (Maybe (FileInvitation, CIFile 'MDSnd, FileTransferMeta)) - setupSndFileTransfer ct = forM file_ $ \file -> do - (fileSize, fileMode) <- checkSndFile mc file 1 - case fileMode of - SendFileSMP fileInline -> smpSndFileTransfer file fileSize fileInline - SendFileXFTP -> xftpSndFileTransfer user file fileSize 1 $ CGContact ct - where - smpSndFileTransfer :: CryptoFile -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta) - smpSndFileTransfer (CryptoFile _ (Just _)) _ _ = throwChatError $ CEFileInternal "locally encrypted files can't be sent via SMP" -- can only happen if XFTP is disabled - smpSndFileTransfer (CryptoFile file Nothing) fileSize fileInline = do - subMode <- chatReadVar subscriptionMode - (agentConnId_, fileConnReq) <- - if isJust fileInline - then pure (Nothing, Nothing) - else bimap Just Just <$> withAgent (\a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode) - let fileName = takeFileName file - fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing} - chSize <- asks $ fileChunkSize . config - withStore $ \db -> do - ft@FileTransferMeta {fileId} <- liftIO $ createSndDirectFileTransfer db userId ct file fileInvitation agentConnId_ chSize subMode - fileStatus <- case fileInline of - Just IFMSent -> createSndDirectInlineFT db ct ft $> CIFSSndTransfer 0 1 - _ -> pure CIFSSndStored - let fileSource = Just $ CF.plain file - ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol = FPSMP} - pure (fileInvitation, ciFile, ft) - prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> m (MsgContainer, Maybe (CIQuote 'CTDirect)) - prepareMsg fInv_ timed_ = case quotedItemId_ of - Nothing -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) - Just quotedItemId -> do - CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <- - withStore $ \db -> getDirectChatItem db user chatId quotedItemId - (origQmc, qd, sent) <- quoteData qci - let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Nothing} - qmc = quoteContent mc origQmc file - quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText} - pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem) - where - quoteData :: ChatItem c d -> m (MsgContent, CIQDirection 'CTDirect, Bool) - quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} = throwChatError CEInvalidQuote - quoteData ChatItem {content = CISndMsgContent qmc} = pure (qmc, CIQDirectSnd, True) - quoteData ChatItem {content = CIRcvMsgContent qmc} = pure (qmc, CIQDirectRcv, False) - quoteData _ = throwChatError CEInvalidQuote - CTGroup -> do - g@(Group gInfo _) <- withStore $ \db -> getGroup db vr user chatId - assertUserGroupRole gInfo GRAuthor - send g - where - send g@(Group gInfo@GroupInfo {groupId} ms) - | isVoice mc && not (groupFeatureAllowed SGFVoice gInfo) = notAllowedError GFVoice - | not (isVoice mc) && isJust file_ && not (groupFeatureAllowed SGFFiles gInfo) = notAllowedError GFFiles - | otherwise = do - (fInv_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer g (length $ filter memberCurrent ms) - timed_ <- sndGroupCITimed live gInfo itemTTL - (msgContainer, quotedItem_) <- prepareGroupMsg user gInfo mc quotedItemId_ fInv_ timed_ live - (msg@SndMessage {sharedMsgId}, sentToMembers) <- sendGroupMessage user gInfo ms (XMsgNew msgContainer) - ci <- saveSndChatItem' user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live - withStore' $ \db -> - forM_ sentToMembers $ \GroupMember {groupMemberId} -> - createGroupSndStatus db (chatItemId' ci) groupMemberId CISSndNew - mapM_ (sendGroupFileInline ms sharedMsgId) ft_ - forM_ (timed_ >>= timedDeleteAt') $ - startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci) - pure $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) - notAllowedError f = pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText f)) - setupSndFileTransfer :: Group -> Int -> m (Maybe (FileInvitation, CIFile 'MDSnd, FileTransferMeta)) - setupSndFileTransfer g@(Group gInfo _) n = forM file_ $ \file -> do - (fileSize, fileMode) <- checkSndFile mc file $ fromIntegral n - case fileMode of - SendFileSMP fileInline -> smpSndFileTransfer file fileSize fileInline - SendFileXFTP -> xftpSndFileTransfer user file fileSize n $ CGGroup g - where - smpSndFileTransfer :: CryptoFile -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta) - smpSndFileTransfer (CryptoFile _ (Just _)) _ _ = throwChatError $ CEFileInternal "locally encrypted files can't be sent via SMP" -- can only happen if XFTP is disabled - smpSndFileTransfer (CryptoFile file Nothing) fileSize fileInline = do - let fileName = takeFileName file - fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq = Nothing, fileInline, fileDescr = Nothing} - fileStatus = if fileInline == Just IFMSent then CIFSSndTransfer 0 1 else CIFSSndStored - chSize <- asks $ fileChunkSize . config - withStore' $ \db -> do - ft@FileTransferMeta {fileId} <- createSndGroupFileTransfer db userId gInfo file fileInvitation chSize - let fileSource = Just $ CF.plain file - ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol = FPSMP} - pure (fileInvitation, ciFile, ft) - sendGroupFileInline :: [GroupMember] -> SharedMsgId -> FileTransferMeta -> m () - sendGroupFileInline ms sharedMsgId ft@FileTransferMeta {fileInline} = - when (fileInline == Just IFMSent) . forM_ ms $ \m -> - processMember m `catchChatError` (toView . CRChatError (Just user)) - where - processMember m@GroupMember {activeConn = Just conn@Connection {connStatus}} = - when (connStatus == ConnReady || connStatus == ConnSndReady) $ do - void . withStore' $ \db -> createSndGroupInlineFT db m conn ft - sendMemberFileInline m conn ft sharedMsgId - processMember _ = pure () - CTContactRequest -> pure $ chatCmdError (Just user) "not supported" - CTContactConnection -> pure $ chatCmdError (Just user) "not supported" - where - xftpSndFileTransfer :: User -> CryptoFile -> Integer -> Int -> ContactOrGroup -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta) - xftpSndFileTransfer user file@(CryptoFile filePath cfArgs) fileSize n contactOrGroup = do - let fileName = takeFileName filePath - fileDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False} - fInv = xftpFileInvitation fileName fileSize fileDescr - fsFilePath <- toFSFilePath filePath - let srcFile = CryptoFile fsFilePath cfArgs - aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) srcFile (roundedFDCount n) - -- TODO CRSndFileStart event for XFTP - chSize <- asks $ fileChunkSize . config - ft@FileTransferMeta {fileId} <- withStore' $ \db -> createSndFileTransferXFTP db user contactOrGroup file fInv (AgentSndFileId aFileId) chSize - let fileSource = Just $ CryptoFile filePath cfArgs - ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus = CIFSSndStored, fileProtocol = FPXFTP} - case contactOrGroup of - CGContact Contact {activeConn} -> forM_ activeConn $ \conn -> - withStore' $ \db -> createSndFTDescrXFTP db user Nothing conn ft fileDescr - CGGroup (Group _ ms) -> forM_ ms $ \m -> saveMemberFD m `catchChatError` (toView . CRChatError (Just user)) - where - -- we are not sending files to pending members, same as with inline files - saveMemberFD m@GroupMember {activeConn = Just conn@Connection {connStatus}} = - when ((connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn)) $ - withStore' $ - \db -> createSndFTDescrXFTP db user (Just m) conn ft fileDescr - saveMemberFD _ = pure () - pure (fInv, ciFile, ft) - unzipMaybe3 :: Maybe (a, b, c) -> (Maybe a, Maybe b, Maybe c) - unzipMaybe3 (Just (a, b, c)) = (Just a, Just b, Just c) - unzipMaybe3 _ = (Nothing, Nothing, Nothing) - APIUpdateChatItem (ChatRef cType chatId) itemId live mc -> withUser $ \user -> withChatLock "updateChatItem" $ case cType of - CTDirect -> do - ct@Contact {contactId} <- withStore $ \db -> getContact db user chatId - assertDirectAllowed user MDSnd ct XMsgUpdate_ - cci <- withStore $ \db -> getDirectCIWithReactions db user ct itemId - case cci of - CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do - case (ciContent, itemSharedMsgId, editable) of - (CISndMsgContent oldMC, Just itemSharedMId, True) -> do - let changed = mc /= oldMC - if changed || fromMaybe False itemLive - then do - (SndMessage {msgId}, _) <- sendDirectContactMessage ct (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) - ci' <- withStore' $ \db -> do - currentTs <- liftIO getCurrentTime - when changed $ - addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) - updateDirectChatItem' db user contactId ci (CISndMsgContent mc) live $ Just msgId - startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' - pure $ CRChatItemUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci') - else pure $ CRChatItemNotChanged user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) - _ -> throwChatError CEInvalidChatItemUpdate - CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate - CTGroup -> do - Group gInfo@GroupInfo {groupId} ms <- withStore $ \db -> getGroup db vr user chatId - assertUserGroupRole gInfo GRAuthor - cci <- withStore $ \db -> getGroupCIWithReactions db user gInfo itemId - case cci of - CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do - case (ciContent, itemSharedMsgId, editable) of - (CISndMsgContent oldMC, Just itemSharedMId, True) -> do - let changed = mc /= oldMC - if changed || fromMaybe False itemLive - then do - (SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) - ci' <- withStore' $ \db -> do - currentTs <- liftIO getCurrentTime - when changed $ - addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) - updateGroupChatItem db user groupId ci (CISndMsgContent mc) live $ Just msgId - startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' - pure $ CRChatItemUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci') - else pure $ CRChatItemNotChanged user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) - _ -> throwChatError CEInvalidChatItemUpdate - CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate - CTContactRequest -> pure $ chatCmdError (Just user) "not supported" - CTContactConnection -> pure $ chatCmdError (Just user) "not supported" - APIDeleteChatItem (ChatRef cType chatId) itemId mode -> withUser $ \user -> withChatLock "deleteChatItem" $ case cType of - CTDirect -> do - (ct, CChatItem msgDir ci@ChatItem {meta = CIMeta {itemSharedMsgId, editable}}) <- withStore $ \db -> (,) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId - case (mode, msgDir, itemSharedMsgId, editable) of - (CIDMInternal, _, _, _) -> deleteDirectCI user ct ci True False - (CIDMBroadcast, SMDSnd, Just itemSharedMId, True) -> do - assertDirectAllowed user MDSnd ct XMsgDel_ - (SndMessage {msgId}, _) <- sendDirectContactMessage ct (XMsgDel itemSharedMId Nothing) - if featureAllowed SCFFullDelete forUser ct - then deleteDirectCI user ct ci True False - else markDirectCIDeleted user ct ci msgId True =<< liftIO getCurrentTime - (CIDMBroadcast, _, _, _) -> throwChatError CEInvalidChatItemDelete - CTGroup -> do - Group gInfo ms <- withStore $ \db -> getGroup db vr user chatId - CChatItem msgDir ci@ChatItem {meta = CIMeta {itemSharedMsgId, editable}} <- withStore $ \db -> getGroupChatItem db user chatId itemId - case (mode, msgDir, itemSharedMsgId, editable) of - (CIDMInternal, _, _, _) -> deleteGroupCI user gInfo ci True False Nothing =<< liftIO getCurrentTime - (CIDMBroadcast, SMDSnd, Just itemSharedMId, True) -> do - assertUserGroupRole gInfo GRObserver -- can still delete messages sent earlier - (SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms $ XMsgDel itemSharedMId Nothing - delGroupChatItem user gInfo ci msgId Nothing - (CIDMBroadcast, _, _, _) -> throwChatError CEInvalidChatItemDelete - CTContactRequest -> pure $ chatCmdError (Just user) "not supported" - CTContactConnection -> pure $ chatCmdError (Just user) "not supported" - APIDeleteMemberChatItem gId mId itemId -> withUser $ \user -> withChatLock "deleteChatItem" $ do - Group gInfo@GroupInfo {membership} ms <- withStore $ \db -> getGroup db vr user gId - CChatItem _ ci@ChatItem {chatDir, meta = CIMeta {itemSharedMsgId}} <- withStore $ \db -> getGroupChatItem db user gId itemId - case (chatDir, itemSharedMsgId) of - (CIGroupRcv GroupMember {groupMemberId, memberRole, memberId}, Just itemSharedMId) -> do - when (groupMemberId /= mId) $ throwChatError CEInvalidChatItemDelete - assertUserGroupRole gInfo $ max GRAdmin memberRole - (SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms $ XMsgDel itemSharedMId $ Just memberId - delGroupChatItem user gInfo ci msgId (Just membership) - (_, _) -> throwChatError CEInvalidChatItemDelete - APIChatItemReaction (ChatRef cType chatId) itemId add reaction -> withUser $ \user -> withChatLock "chatItemReaction" $ case cType of - CTDirect -> - withStore (\db -> (,) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId) >>= \case - (ct, CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}}) -> do - unless (featureAllowed SCFReactions forUser ct) $ - throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions)) - unless (ciReactionAllowed ci) $ - throwChatError (CECommandError "reaction not allowed - chat item has no content") - rs <- withStore' $ \db -> getDirectReactions db ct itemSharedMId True - checkReactionAllowed rs - (SndMessage {msgId}, _) <- sendDirectContactMessage ct $ XMsgReact itemSharedMId Nothing reaction add - createdAt <- liftIO getCurrentTime - reactions <- withStore' $ \db -> do - setDirectReaction db ct itemSharedMId True reaction add msgId createdAt - liftIO $ getDirectCIReactions db ct itemSharedMId - let ci' = CChatItem md ci {reactions} - r = ACIReaction SCTDirect SMDSnd (DirectChat ct) $ CIReaction CIDirectSnd ci' createdAt reaction - pure $ CRChatItemReaction user add r - _ -> throwChatError $ CECommandError "reaction not possible - no shared item ID" - CTGroup -> - withStore (\db -> (,) <$> getGroup db vr user chatId <*> getGroupChatItem db user chatId itemId) >>= \case - (Group g@GroupInfo {membership} ms, CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}}) -> do - unless (groupFeatureAllowed SGFReactions g) $ - throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions)) - unless (ciReactionAllowed ci) $ - throwChatError (CECommandError "reaction not allowed - chat item has no content") - let GroupMember {memberId = itemMemberId} = chatItemMember g ci - rs <- withStore' $ \db -> getGroupReactions db g membership itemMemberId itemSharedMId True - checkReactionAllowed rs - (SndMessage {msgId}, _) <- sendGroupMessage user g ms (XMsgReact itemSharedMId (Just itemMemberId) reaction add) - createdAt <- liftIO getCurrentTime - reactions <- withStore' $ \db -> do - setGroupReaction db g membership itemMemberId itemSharedMId True reaction add msgId createdAt - liftIO $ getGroupCIReactions db g itemMemberId itemSharedMId - let ci' = CChatItem md ci {reactions} - r = ACIReaction SCTGroup SMDSnd (GroupChat g) $ CIReaction CIGroupSnd ci' createdAt reaction - pure $ CRChatItemReaction user add r - _ -> throwChatError $ CECommandError "reaction not possible - no shared item ID" - CTContactRequest -> pure $ chatCmdError (Just user) "not supported" - CTContactConnection -> pure $ chatCmdError (Just user) "not supported" - where - checkReactionAllowed rs = do - when ((reaction `elem` rs) == add) $ - throwChatError (CECommandError $ "reaction already " <> if add then "added" else "removed") - when (add && length rs >= maxMsgReactions) $ - throwChatError (CECommandError "too many reactions") - APIUserRead userId -> withUserId userId $ \user -> withStore' (`setUserChatsRead` user) >> ok user - UserRead -> withUser $ \User {userId} -> processChatCommand $ APIUserRead userId - APIChatRead (ChatRef cType chatId) fromToIds -> withUser $ \_ -> case cType of - CTDirect -> do - user <- withStore $ \db -> getUserByContactId db chatId - timedItems <- withStore' $ \db -> getDirectUnreadTimedItems db user chatId fromToIds - ts <- liftIO getCurrentTime - forM_ timedItems $ \(itemId, ttl) -> do - let deleteAt = addUTCTime (realToFrac ttl) ts - withStore' $ \db -> setDirectChatItemDeleteAt db user chatId itemId deleteAt - startProximateTimedItemThread user (ChatRef CTDirect chatId, itemId) deleteAt - withStore' $ \db -> updateDirectChatItemsRead db user chatId fromToIds - ok user - CTGroup -> do - user@User {userId} <- withStore $ \db -> getUserByGroupId db chatId - timedItems <- withStore' $ \db -> getGroupUnreadTimedItems db user chatId fromToIds - ts <- liftIO getCurrentTime - forM_ timedItems $ \(itemId, ttl) -> do - let deleteAt = addUTCTime (realToFrac ttl) ts - withStore' $ \db -> setGroupChatItemDeleteAt db user chatId itemId deleteAt - startProximateTimedItemThread user (ChatRef CTGroup chatId, itemId) deleteAt - withStore' $ \db -> updateGroupChatItemsRead db userId chatId fromToIds - ok user - CTContactRequest -> pure $ chatCmdError Nothing "not supported" - CTContactConnection -> pure $ chatCmdError Nothing "not supported" - APIChatUnread (ChatRef cType chatId) unreadChat -> withUser $ \user -> case cType of - CTDirect -> do - withStore $ \db -> do - ct <- getContact db user chatId - liftIO $ updateContactUnreadChat db user ct unreadChat - ok user - CTGroup -> do - withStore $ \db -> do - Group {groupInfo} <- getGroup db vr user chatId - liftIO $ updateGroupUnreadChat db user groupInfo unreadChat - ok user - _ -> pure $ chatCmdError (Just user) "not supported" - APIDeleteChat (ChatRef cType chatId) notify -> withUser $ \user@User {userId} -> case cType of - CTDirect -> do - ct <- withStore $ \db -> getContact db user chatId - filesInfo <- withStore' $ \db -> getContactFileInfo db user ct - withChatLock "deleteChat direct" . procCmd $ do - deleteFilesAndConns user filesInfo - when (contactReady ct && contactActive ct && notify) $ - void (sendDirectContactMessage ct XDirectDel) `catchChatError` const (pure ()) - contactConnIds <- map aConnId <$> withStore' (\db -> getContactConnections db userId ct) - deleteAgentConnectionsAsync user contactConnIds - -- functions below are called in separate transactions to prevent crashes on android - -- (possibly, race condition on integrity check?) - withStore' $ \db -> deleteContactConnectionsAndFiles db userId ct - withStore' $ \db -> deleteContact db user ct - pure $ CRContactDeleted user ct - CTContactConnection -> withChatLock "deleteChat contactConnection" . procCmd $ do - conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withStore $ \db -> getPendingContactConnection db userId chatId - deleteAgentConnectionAsync user acId - withStore' $ \db -> deletePendingContactConnection db userId chatId - pure $ CRContactConnectionDeleted user conn - CTGroup -> do - Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db vr user chatId - let isOwner = membership.memberRole == GROwner - canDelete = isOwner || not (memberCurrent membership) - unless canDelete $ throwChatError $ CEGroupUserRole gInfo GROwner - filesInfo <- withStore' $ \db -> getGroupFileInfo db user gInfo - withChatLock "deleteChat group" . procCmd $ do - deleteFilesAndConns user filesInfo - when (memberActive membership && isOwner) . void $ sendGroupMessage user gInfo members XGrpDel - deleteGroupLinkIfExists user gInfo - deleteMembersConnections user members - -- functions below are called in separate transactions to prevent crashes on android - -- (possibly, race condition on integrity check?) - withStore' $ \db -> deleteGroupConnectionsAndFiles db user gInfo members - withStore' $ \db -> deleteGroupItemsAndMembers db user gInfo members - withStore' $ \db -> deleteGroup db user gInfo - let contactIds = mapMaybe memberContactId members - deleteAgentConnectionsAsync user . concat =<< mapM deleteUnusedContact contactIds - pure $ CRGroupDeletedUser user gInfo - where - deleteUnusedContact :: ContactId -> m [ConnId] - deleteUnusedContact contactId = - (withStore (\db -> getContact db user contactId) >>= delete) - `catchChatError` (\e -> toView (CRChatError (Just user) e) $> []) - where - delete ct - | directOrUsed ct = pure [] - | otherwise = - withStore' (\db -> checkContactHasGroups db user ct) >>= \case - Just _ -> pure [] - Nothing -> do - conns <- withStore' $ \db -> getContactConnections db userId ct - withStore' (\db -> setContactDeleted db user ct) - `catchChatError` (toView . CRChatError (Just user)) - pure $ map aConnId conns - CTContactRequest -> pure $ chatCmdError (Just user) "not supported" - APIClearChat (ChatRef cType chatId) -> withUser $ \user -> case cType of - CTDirect -> do - ct <- withStore $ \db -> getContact db user chatId - filesInfo <- withStore' $ \db -> getContactFileInfo db user ct - deleteFilesAndConns user filesInfo - withStore' $ \db -> deleteContactCIs db user ct - pure $ CRChatCleared user (AChatInfo SCTDirect $ DirectChat ct) - CTGroup -> do - gInfo <- withStore $ \db -> getGroupInfo db vr user chatId - filesInfo <- withStore' $ \db -> getGroupFileInfo db user gInfo - deleteFilesAndConns user filesInfo - withStore' $ \db -> deleteGroupCIs db user gInfo - membersToDelete <- withStore' $ \db -> getGroupMembersForExpiration db user gInfo - forM_ membersToDelete $ \m -> withStore' $ \db -> deleteGroupMember db user m - pure $ CRChatCleared user (AChatInfo SCTGroup $ GroupChat gInfo) - CTContactConnection -> pure $ chatCmdError (Just user) "not supported" - CTContactRequest -> pure $ chatCmdError (Just user) "not supported" - APIAcceptContact incognito connReqId -> withUser $ \_ -> withChatLock "acceptContact" $ do - (user@User {userId}, cReq@UserContactRequest {userContactLinkId}) <- withStore $ \db -> getContactRequest' db connReqId - ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId - let contactUsed = (\(_, groupId_, _) -> isNothing groupId_) ucl - -- [incognito] generate profile to send, create connection with incognito profile - incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing - ct <- acceptContactRequest user cReq incognitoProfile contactUsed - pure $ CRAcceptingContactRequest user ct - APIRejectContact connReqId -> withUser $ \user -> withChatLock "rejectContact" $ do - cReq@UserContactRequest {agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId} <- - withStore $ \db -> - getContactRequest db user connReqId - `storeFinally` liftIO (deleteContactRequest db user connReqId) - withAgent $ \a -> rejectContact a connId invId - pure $ CRContactRequestRejected user cReq - APISendCallInvitation contactId callType -> withUser $ \user -> do - -- party initiating call - ct <- withStore $ \db -> getContact db user contactId - assertDirectAllowed user MDSnd ct XCallInv_ - if featureAllowed SCFCalls forUser ct - then do - calls <- asks currentCalls - withChatLock "sendCallInvitation" $ do - g <- asks random - callId <- atomically $ CallId <$> C.randomBytes 16 g - dhKeyPair <- atomically $ if encryptedCall callType then Just <$> C.generateKeyPair g else pure Nothing - let invitation = CallInvitation {callType, callDhPubKey = fst <$> dhKeyPair} - callState = CallInvitationSent {localCallType = callType, localDhPrivKey = snd <$> dhKeyPair} - (msg, _) <- sendDirectContactMessage ct (XCallInv callId invitation) - ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndCall CISCallPending 0) - let call' = Call {contactId, callId, chatItemId = chatItemId' ci, callState, callTs = chatItemTs' ci} - call_ <- atomically $ TM.lookupInsert contactId call' calls - forM_ call_ $ \call -> updateCallItemStatus user ct call WCSDisconnected Nothing - toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) - ok user - else pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (chatFeatureNameText CFCalls)) - SendCallInvitation cName callType -> withUser $ \user -> do - contactId <- withStore $ \db -> getContactIdByName db user cName - processChatCommand $ APISendCallInvitation contactId callType - APIRejectCall contactId -> - -- party accepting call - withCurrentCall contactId $ \user ct Call {chatItemId, callState} -> case callState of - CallInvitationReceived {} -> do - let aciContent = ACIContent SMDRcv $ CIRcvCall CISCallRejected 0 - withStore' $ \db -> updateDirectChatItemsRead db user contactId $ Just (chatItemId, chatItemId) - updateDirectChatItemView user ct chatItemId aciContent False Nothing $> Nothing - _ -> throwChatError . CECallState $ callStateTag callState - APISendCallOffer contactId WebRTCCallOffer {callType, rtcSession} -> - -- party accepting call - withCurrentCall contactId $ \user ct call@Call {callId, chatItemId, callState} -> case callState of - CallInvitationReceived {peerCallType, localDhPubKey, sharedKey} -> do - let callDhPubKey = if encryptedCall callType then localDhPubKey else Nothing - offer = CallOffer {callType, rtcSession, callDhPubKey} - callState' = CallOfferSent {localCallType = callType, peerCallType, localCallSession = rtcSession, sharedKey} - aciContent = ACIContent SMDRcv $ CIRcvCall CISCallAccepted 0 - (SndMessage {msgId}, _) <- sendDirectContactMessage ct (XCallOffer callId offer) - withStore' $ \db -> updateDirectChatItemsRead db user contactId $ Just (chatItemId, chatItemId) - updateDirectChatItemView user ct chatItemId aciContent False $ Just msgId - pure $ Just call {callState = callState'} - _ -> throwChatError . CECallState $ callStateTag callState - APISendCallAnswer contactId rtcSession -> - -- party initiating call - withCurrentCall contactId $ \user ct call@Call {callId, chatItemId, callState} -> case callState of - CallOfferReceived {localCallType, peerCallType, peerCallSession, sharedKey} -> do - let callState' = CallNegotiated {localCallType, peerCallType, localCallSession = rtcSession, peerCallSession, sharedKey} - aciContent = ACIContent SMDSnd $ CISndCall CISCallNegotiated 0 - (SndMessage {msgId}, _) <- sendDirectContactMessage ct (XCallAnswer callId CallAnswer {rtcSession}) - updateDirectChatItemView user ct chatItemId aciContent False $ Just msgId - pure $ Just call {callState = callState'} - _ -> throwChatError . CECallState $ callStateTag callState - APISendCallExtraInfo contactId rtcExtraInfo -> - -- any call party - withCurrentCall contactId $ \_ ct call@Call {callId, callState} -> case callState of - CallOfferSent {localCallType, peerCallType, localCallSession, sharedKey} -> do - -- TODO update the list of ice servers in localCallSession - void . sendDirectContactMessage ct $ XCallExtra callId CallExtraInfo {rtcExtraInfo} - let callState' = CallOfferSent {localCallType, peerCallType, localCallSession, sharedKey} - pure $ Just call {callState = callState'} - CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession, sharedKey} -> do - -- TODO update the list of ice servers in localCallSession - void . sendDirectContactMessage ct $ XCallExtra callId CallExtraInfo {rtcExtraInfo} - let callState' = CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession, sharedKey} - pure $ Just call {callState = callState'} - _ -> throwChatError . CECallState $ callStateTag callState - APIEndCall contactId -> - -- any call party - withCurrentCall contactId $ \user ct call@Call {callId} -> do - (SndMessage {msgId}, _) <- sendDirectContactMessage ct (XCallEnd callId) - updateCallItemStatus user ct call WCSDisconnected $ Just msgId - pure Nothing - APIGetCallInvitations -> withUser $ \_ -> do - calls <- asks currentCalls >>= readTVarIO - let invs = mapMaybe callInvitation $ M.elems calls - rcvCallInvitations <- rights <$> mapM rcvCallInvitation invs - pure $ CRCallInvitations rcvCallInvitations - where - callInvitation Call {contactId, callState, callTs} = case callState of - CallInvitationReceived {peerCallType, sharedKey} -> Just (contactId, callTs, peerCallType, sharedKey) - _ -> Nothing - rcvCallInvitation (contactId, callTs, peerCallType, sharedKey) = runExceptT . withStore $ \db -> do - user <- getUserByContactId db contactId - contact <- getContact db user contactId - pure RcvCallInvitation {user, contact, callType = peerCallType, sharedKey, callTs} - APIGetNetworkStatuses -> withUser $ \_ -> - CRNetworkStatuses Nothing . map (uncurry ConnNetworkStatus) . M.toList <$> chatReadVar connNetworkStatuses - APICallStatus contactId receivedStatus -> - withCurrentCall contactId $ \user ct call -> - updateCallItemStatus user ct call receivedStatus Nothing $> Just call - APIUpdateProfile userId profile -> withUserId userId (`updateProfile` profile) - APISetContactPrefs contactId prefs' -> withUser $ \user -> do - ct <- withStore $ \db -> getContact db user contactId - updateContactPrefs user ct prefs' - APISetContactAlias contactId localAlias -> withUser $ \user@User {userId} -> do - ct' <- withStore $ \db -> do - ct <- getContact db user contactId - liftIO $ updateContactAlias db userId ct localAlias - pure $ CRContactAliasUpdated user ct' - APISetConnectionAlias connId localAlias -> withUser $ \user@User {userId} -> do - conn' <- withStore $ \db -> do - conn <- getPendingContactConnection db userId connId - liftIO $ updateContactConnectionAlias db userId conn localAlias - pure $ CRConnectionAliasUpdated user conn' - APIParseMarkdown text -> pure . CRApiParsedMarkdown $ parseMaybeMarkdownList text - APIGetNtfToken -> withUser $ \_ -> crNtfToken <$> withAgent getNtfToken - APIRegisterToken token mode -> withUser $ \_ -> - CRNtfTokenStatus <$> withAgent (\a -> registerNtfToken a token mode) - APIVerifyToken token nonce code -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a token nonce code) >> ok_ - APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) >> ok_ - APIGetNtfMessage nonce encNtfInfo -> withUser $ \_ -> do - (NotificationInfo {ntfConnId, ntfMsgMeta}, msgs) <- withAgent $ \a -> getNotificationMessage a nonce encNtfInfo - let msgTs' = systemToUTCTime . (\SMP.NMsgMeta {msgTs} -> msgTs) <$> ntfMsgMeta - agentConnId = AgentConnId ntfConnId - user_ <- withStore' (`getUserByAConnId` agentConnId) - connEntity_ <- - pure user_ $>>= \user -> - withStore (\db -> Just <$> getConnectionEntity db vr user agentConnId) `catchChatError` (\e -> toView (CRChatError (Just user) e) $> Nothing) - pure CRNtfMessages {user_, connEntity_, msgTs = msgTs', ntfMessages = map ntfMsgInfo msgs} - APIGetUserProtoServers userId (AProtocolType p) -> withUserId userId $ \user -> withServerProtocol p $ do - ChatConfig {defaultServers} <- asks config - servers <- withStore' (`getProtocolServers` user) - let defServers = cfgServers p defaultServers - servers' = fromMaybe (L.map toServerCfg defServers) $ nonEmpty servers - pure $ CRUserProtoServers user $ AUPS $ UserProtoServers p servers' defServers - where - toServerCfg server = ServerCfg {server, preset = True, tested = Nothing, enabled = True} - GetUserProtoServers aProtocol -> withUser $ \User {userId} -> - processChatCommand $ APIGetUserProtoServers userId aProtocol - APISetUserProtoServers userId (APSC p (ProtoServersConfig servers)) -> withUserId userId $ \user -> withServerProtocol p $ - withChatLock "setUserSMPServers" $ do - withStore $ \db -> overwriteProtocolServers db user servers - cfg <- asks config - withAgent $ \a -> setProtocolServers a (aUserId user) $ activeAgentServers cfg p servers - ok user - SetUserProtoServers serversConfig -> withUser $ \User {userId} -> - processChatCommand $ APISetUserProtoServers userId serversConfig - APITestProtoServer userId srv@(AProtoServerWithAuth p server) -> withUserId userId $ \user -> - withServerProtocol p $ - CRServerTestResult user srv <$> withAgent (\a -> testProtocolServer a (aUserId user) server) - TestProtoServer srv -> withUser $ \User {userId} -> - processChatCommand $ APITestProtoServer userId srv - APISetChatItemTTL userId newTTL_ -> withUserId userId $ \user -> - checkStoreNotChanged $ - withChatLock "setChatItemTTL" $ do - case newTTL_ of - Nothing -> do - withStore' $ \db -> setChatItemTTL db user newTTL_ - setExpireCIFlag user False - Just newTTL -> do - oldTTL <- withStore' (`getChatItemTTL` user) - when (maybe True (newTTL <) oldTTL) $ do - setExpireCIFlag user False - expireChatItems user newTTL True - withStore' $ \db -> setChatItemTTL db user newTTL_ - startExpireCIThread user - whenM chatStarted $ setExpireCIFlag user True - ok user - SetChatItemTTL newTTL_ -> withUser' $ \User {userId} -> do - processChatCommand $ APISetChatItemTTL userId newTTL_ - APIGetChatItemTTL userId -> withUserId' userId $ \user -> do - ttl <- withStoreCtx' (Just "APIGetChatItemTTL, getChatItemTTL") (`getChatItemTTL` user) - pure $ CRChatItemTTL user ttl - GetChatItemTTL -> withUser' $ \User {userId} -> do - processChatCommand $ APIGetChatItemTTL userId - APISetNetworkConfig cfg -> withUser' $ \_ -> withAgent (`setNetworkConfig` cfg) >> ok_ - APIGetNetworkConfig -> withUser' $ \_ -> - CRNetworkConfig <$> withAgent getNetworkConfig - ReconnectAllServers -> withUser' $ \_ -> withAgent reconnectAllServers >> ok_ - APISetChatSettings (ChatRef cType chatId) chatSettings -> withUser $ \user -> case cType of - CTDirect -> do - ct <- withStore $ \db -> do - ct <- getContact db user chatId - liftIO $ updateContactSettings db user chatId chatSettings - pure ct - forM_ (contactConnId ct) $ \connId -> - withAgent $ \a -> toggleConnectionNtfs a connId (chatHasNtfs chatSettings) - ok user - CTGroup -> do - ms <- withStore $ \db -> do - Group _ ms <- getGroup db vr user chatId - liftIO $ updateGroupSettings db user chatId chatSettings - pure ms - forM_ (filter memberActive ms) $ \m -> forM_ (memberConnId m) $ \connId -> - withAgent (\a -> toggleConnectionNtfs a connId $ chatHasNtfs chatSettings) `catchChatError` (toView . CRChatError (Just user)) - ok user - _ -> pure $ chatCmdError (Just user) "not supported" - APISetMemberSettings gId gMemberId settings -> withUser $ \user -> do - m <- withStore $ \db -> do - liftIO $ updateGroupMemberSettings db user gId gMemberId settings - getGroupMember db user gId gMemberId - when (memberActive m) $ forM_ (memberConnId m) $ \connId -> do - let ntfOn = showMessages $ memberSettings m - withAgent (\a -> toggleConnectionNtfs a connId ntfOn) `catchChatError` (toView . CRChatError (Just user)) - ok user - APIContactInfo contactId -> withUser $ \user@User {userId} -> do - -- [incognito] print user's incognito profile for this contact - ct@Contact {activeConn} <- withStore $ \db -> getContact db user contactId - incognitoProfile <- case activeConn of - Nothing -> pure Nothing - Just Connection {customUserProfileId} -> - forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) - connectionStats <- mapM (withAgent . flip getConnectionServers) (contactConnId ct) - pure $ CRContactInfo user ct connectionStats (fmap fromLocalProfile incognitoProfile) - APIGroupInfo gId -> withUser $ \user -> do - (g, s) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> liftIO (getGroupSummary db user gId) - pure $ CRGroupInfo user g s - APIGroupMemberInfo gId gMemberId -> withUser $ \user -> do - (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db user gId gMemberId - connectionStats <- mapM (withAgent . flip getConnectionServers) (memberConnId m) - pure $ CRGroupMemberInfo user g m connectionStats - APISwitchContact contactId -> withUser $ \user -> do - ct <- withStore $ \db -> getContact db user contactId - case contactConnId ct of - Just connId -> do - connectionStats <- withAgent $ \a -> switchConnectionAsync a "" connId - pure $ CRContactSwitchStarted user ct connectionStats - Nothing -> throwChatError $ CEContactNotActive ct - APISwitchGroupMember gId gMemberId -> withUser $ \user -> do - (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db user gId gMemberId - case memberConnId m of - Just connId -> do - connectionStats <- withAgent (\a -> switchConnectionAsync a "" connId) - pure $ CRGroupMemberSwitchStarted user g m connectionStats - _ -> throwChatError CEGroupMemberNotActive - APIAbortSwitchContact contactId -> withUser $ \user -> do - ct <- withStore $ \db -> getContact db user contactId - case contactConnId ct of - Just connId -> do - connectionStats <- withAgent $ \a -> abortConnectionSwitch a connId - pure $ CRContactSwitchAborted user ct connectionStats - Nothing -> throwChatError $ CEContactNotActive ct - APIAbortSwitchGroupMember gId gMemberId -> withUser $ \user -> do - (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db user gId gMemberId - case memberConnId m of - Just connId -> do - connectionStats <- withAgent $ \a -> abortConnectionSwitch a connId - pure $ CRGroupMemberSwitchAborted user g m connectionStats - _ -> throwChatError CEGroupMemberNotActive - APISyncContactRatchet contactId force -> withUser $ \user -> withChatLock "syncContactRatchet" $ do - ct <- withStore $ \db -> getContact db user contactId - case contactConnId ct of - Just connId -> do - cStats@ConnectionStats {ratchetSyncState = rss} <- withAgent $ \a -> synchronizeRatchet a connId force - createInternalChatItem user (CDDirectSnd ct) (CISndConnEvent $ SCERatchetSync rss Nothing) Nothing - pure $ CRContactRatchetSyncStarted user ct cStats - Nothing -> throwChatError $ CEContactNotActive ct - APISyncGroupMemberRatchet gId gMemberId force -> withUser $ \user -> withChatLock "syncGroupMemberRatchet" $ do - (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db user gId gMemberId - case memberConnId m of - Just connId -> do - cStats@ConnectionStats {ratchetSyncState = rss} <- withAgent $ \a -> synchronizeRatchet a connId force - createInternalChatItem user (CDGroupSnd g) (CISndConnEvent . SCERatchetSync rss . Just $ groupMemberRef m) Nothing - pure $ CRGroupMemberRatchetSyncStarted user g m cStats - _ -> throwChatError CEGroupMemberNotActive - APIGetContactCode contactId -> withUser $ \user -> do - ct@Contact {activeConn} <- withStore $ \db -> getContact db user contactId - case activeConn of - Just conn@Connection {connId} -> do - code <- getConnectionCode $ aConnId conn - ct' <- case contactSecurityCode ct of - Just SecurityCode {securityCode} - | sameVerificationCode code securityCode -> pure ct - | otherwise -> do - withStore' $ \db -> setConnectionVerified db user connId Nothing - pure (ct :: Contact) {activeConn = Just $ (conn :: Connection) {connectionCode = Nothing}} - _ -> pure ct - pure $ CRContactCode user ct' code - Nothing -> throwChatError $ CEContactNotActive ct - APIGetGroupMemberCode gId gMemberId -> withUser $ \user -> do - (g, m@GroupMember {activeConn}) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db user gId gMemberId - case activeConn of - Just conn@Connection {connId} -> do - code <- getConnectionCode $ aConnId conn - m' <- case memberSecurityCode m of - Just SecurityCode {securityCode} - | sameVerificationCode code securityCode -> pure m - | otherwise -> do - withStore' $ \db -> setConnectionVerified db user connId Nothing - pure (m :: GroupMember) {activeConn = Just $ (conn :: Connection) {connectionCode = Nothing}} - _ -> pure m - pure $ CRGroupMemberCode user g m' code - _ -> throwChatError CEGroupMemberNotActive - APIVerifyContact contactId code -> withUser $ \user -> do - ct@Contact {activeConn} <- withStore $ \db -> getContact db user contactId - case activeConn of - Just conn -> verifyConnectionCode user conn code - Nothing -> throwChatError $ CEContactNotActive ct - APIVerifyGroupMember gId gMemberId code -> withUser $ \user -> do - GroupMember {activeConn} <- withStore $ \db -> getGroupMember db user gId gMemberId - case activeConn of - Just conn -> verifyConnectionCode user conn code - _ -> throwChatError CEGroupMemberNotActive - APIEnableContact contactId -> withUser $ \user -> do - ct@Contact {activeConn} <- withStore $ \db -> getContact db user contactId - case activeConn of - Just conn -> do - withStore' $ \db -> setConnectionAuthErrCounter db user conn 0 - ok user - Nothing -> throwChatError $ CEContactNotActive ct - APIEnableGroupMember gId gMemberId -> withUser $ \user -> do - GroupMember {activeConn} <- withStore $ \db -> getGroupMember db user gId gMemberId - case activeConn of - Just conn -> do - withStore' $ \db -> setConnectionAuthErrCounter db user conn 0 - ok user - _ -> throwChatError CEGroupMemberNotActive - SetShowMessages cName ntfOn -> updateChatSettings cName (\cs -> cs {enableNtfs = ntfOn}) - SetSendReceipts cName rcptsOn_ -> updateChatSettings cName (\cs -> cs {sendRcpts = rcptsOn_}) - SetShowMemberMessages gName mName showMessages -> withUser $ \user -> do - (gId, mId) <- getGroupAndMemberId user gName mName - m <- withStore $ \db -> getGroupMember db user gId mId - let settings = (memberSettings m) {showMessages} - processChatCommand $ APISetMemberSettings gId mId settings - ContactInfo cName -> withContactName cName APIContactInfo - ShowGroupInfo gName -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIGroupInfo groupId - GroupMemberInfo gName mName -> withMemberName gName mName APIGroupMemberInfo - SwitchContact cName -> withContactName cName APISwitchContact - SwitchGroupMember gName mName -> withMemberName gName mName APISwitchGroupMember - AbortSwitchContact cName -> withContactName cName APIAbortSwitchContact - AbortSwitchGroupMember gName mName -> withMemberName gName mName APIAbortSwitchGroupMember - SyncContactRatchet cName force -> withContactName cName $ \ctId -> APISyncContactRatchet ctId force - SyncGroupMemberRatchet gName mName force -> withMemberName gName mName $ \gId mId -> APISyncGroupMemberRatchet gId mId force - GetContactCode cName -> withContactName cName APIGetContactCode - GetGroupMemberCode gName mName -> withMemberName gName mName APIGetGroupMemberCode - VerifyContact cName code -> withContactName cName (`APIVerifyContact` code) - VerifyGroupMember gName mName code -> withMemberName gName mName $ \gId mId -> APIVerifyGroupMember gId mId code - EnableContact cName -> withContactName cName APIEnableContact - EnableGroupMember gName mName -> withMemberName gName mName $ \gId mId -> APIEnableGroupMember gId mId - ChatHelp section -> pure $ CRChatHelp section - Welcome -> withUser $ pure . CRWelcome - APIAddContact userId incognito -> withUserId userId $ \user -> withChatLock "addContact" . procCmd $ do - -- [incognito] generate profile for connection - incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - subMode <- chatReadVar subscriptionMode - (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode - conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnNew incognitoProfile subMode - pure $ CRInvitation user cReq conn - AddContact incognito -> withUser $ \User {userId} -> - processChatCommand $ APIAddContact userId incognito - APISetConnectionIncognito connId incognito -> withUser $ \user@User {userId} -> do - conn'_ <- withStore $ \db -> do - conn@PendingContactConnection {pccConnStatus, customUserProfileId} <- getPendingContactConnection db userId connId - case (pccConnStatus, customUserProfileId, incognito) of - (ConnNew, Nothing, True) -> liftIO $ do - incognitoProfile <- generateRandomProfile - pId <- createIncognitoProfile db user incognitoProfile - Just <$> updatePCCIncognito db user conn (Just pId) - (ConnNew, Just pId, False) -> liftIO $ do - deletePCCIncognitoProfile db user pId - Just <$> updatePCCIncognito db user conn Nothing - _ -> pure Nothing - case conn'_ of - Just conn' -> pure $ CRConnectionIncognitoUpdated user conn' - Nothing -> throwChatError CEConnectionIncognitoChangeProhibited - APIConnectPlan userId cReqUri -> withUserId userId $ \user -> - withChatLock "connectPlan" . procCmd $ - CRConnectionPlan user <$> connectPlan user cReqUri - APIConnect userId incognito (Just (ACR SCMInvitation cReq)) -> withUserId userId $ \user -> withChatLock "connect" . procCmd $ do - subMode <- chatReadVar subscriptionMode - -- [incognito] generate profile to send - incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - let profileToSend = userProfileToSend user incognitoProfile Nothing - dm <- directMessage $ XInfo profileToSend - connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm subMode - conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined (incognitoProfile $> profileToSend) subMode - pure $ CRSentConfirmation user conn - APIConnect userId incognito (Just (ACR SCMContact cReq)) -> withUserId userId $ \user -> connectViaContact user incognito cReq - APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq - Connect incognito aCReqUri@(Just cReqUri) -> withUser $ \user@User {userId} -> do - plan <- connectPlan user cReqUri `catchChatError` const (pure $ CPInvitationLink ILPOk) - unless (connectionPlanProceed plan) $ throwChatError (CEConnectionPlan plan) - case plan of - CPContactAddress (CAPContactViaAddress Contact {contactId}) -> - processChatCommand $ APIConnectContactViaAddress userId incognito contactId - _ -> processChatCommand $ APIConnect userId incognito aCReqUri - Connect _ Nothing -> throwChatError CEInvalidConnReq - APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do - ct@Contact {activeConn, profile = LocalProfile {contactLink}} <- withStore $ \db -> getContact db user contactId - when (isJust activeConn) $ throwChatError (CECommandError "contact already has connection") - case contactLink of - Just cReq -> connectContactViaAddress user incognito ct cReq - Nothing -> throwChatError (CECommandError "no address in contact profile") - ConnectSimplex incognito -> withUser $ \user@User {userId} -> do - let cReqUri = ACR SCMContact adminContactReq - plan <- connectPlan user cReqUri `catchChatError` const (pure $ CPInvitationLink ILPOk) - unless (connectionPlanProceed plan) $ throwChatError (CEConnectionPlan plan) - case plan of - CPContactAddress (CAPContactViaAddress Contact {contactId}) -> - processChatCommand $ APIConnectContactViaAddress userId incognito contactId - _ -> processChatCommand $ APIConnect userId incognito (Just cReqUri) - DeleteContact cName -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId) True - ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect - APIListContacts userId -> withUserId userId $ \user -> - CRContactsList user <$> withStore' (`getUserContacts` user) - ListContacts -> withUser $ \User {userId} -> - processChatCommand $ APIListContacts userId - APICreateMyAddress userId -> withUserId userId $ \user -> withChatLock "createMyAddress" . procCmd $ do - subMode <- chatReadVar subscriptionMode - (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact Nothing subMode - withStore $ \db -> createUserContactLink db user connId cReq subMode - pure $ CRUserContactLinkCreated user cReq - CreateMyAddress -> withUser $ \User {userId} -> - processChatCommand $ APICreateMyAddress userId - APIDeleteMyAddress userId -> withUserId userId $ \user@User {profile = p} -> do - conns <- withStore (`getUserAddressConnections` user) - withChatLock "deleteMyAddress" $ do - deleteAgentConnectionsAsync user $ map aConnId conns - withStore' (`deleteUserAddress` user) - let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing} - r <- updateProfile_ user p' $ withStore' $ \db -> setUserProfileContactLink db user Nothing - let user' = case r of - CRUserProfileUpdated u' _ _ _ -> u' - _ -> user - pure $ CRUserContactLinkDeleted user' - DeleteMyAddress -> withUser $ \User {userId} -> - processChatCommand $ APIDeleteMyAddress userId - APIShowMyAddress userId -> withUserId' userId $ \user -> - CRUserContactLink user <$> withStoreCtx (Just "APIShowMyAddress, getUserAddress") (`getUserAddress` user) - ShowMyAddress -> withUser' $ \User {userId} -> - processChatCommand $ APIShowMyAddress userId - APISetProfileAddress userId False -> withUserId userId $ \user@User {profile = p} -> do - let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing} - updateProfile_ user p' $ withStore' $ \db -> setUserProfileContactLink db user Nothing - APISetProfileAddress userId True -> withUserId userId $ \user@User {profile = p} -> do - ucl@UserContactLink {connReqContact} <- withStore (`getUserAddress` user) - let p' = (fromLocalProfile p :: Profile) {contactLink = Just connReqContact} - updateProfile_ user p' $ withStore' $ \db -> setUserProfileContactLink db user $ Just ucl - SetProfileAddress onOff -> withUser $ \User {userId} -> - processChatCommand $ APISetProfileAddress userId onOff - APIAddressAutoAccept userId autoAccept_ -> withUserId userId $ \user -> do - contactLink <- withStore (\db -> updateUserAddressAutoAccept db user autoAccept_) - pure $ CRUserContactLinkUpdated user contactLink - AddressAutoAccept autoAccept_ -> withUser $ \User {userId} -> - processChatCommand $ APIAddressAutoAccept userId autoAccept_ - AcceptContact incognito cName -> withUser $ \User {userId} -> do - connReqId <- withStore $ \db -> getContactRequestIdByName db userId cName - processChatCommand $ APIAcceptContact incognito connReqId - RejectContact cName -> withUser $ \User {userId} -> do - connReqId <- withStore $ \db -> getContactRequestIdByName db userId cName - processChatCommand $ APIRejectContact connReqId - SendMessage (ChatName cType name) msg -> withUser $ \user -> do - let mc = MCText msg - case cType of - CTDirect -> - withStore' (\db -> runExceptT $ getContactIdByName db user name) >>= \case - Right ctId -> do - let chatRef = ChatRef CTDirect ctId - processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage Nothing Nothing mc - Left _ -> - withStore' (\db -> runExceptT $ getActiveMembersByName db vr user name) >>= \case - Right [(gInfo, member)] -> do - let GroupInfo {localDisplayName = gName} = gInfo - GroupMember {localDisplayName = mName} = member - processChatCommand $ SendMemberContactMessage gName mName msg - Right (suspectedMember : _) -> - throwChatError $ CEContactNotFound name (Just suspectedMember) - _ -> - throwChatError $ CEContactNotFound name Nothing - CTGroup -> do - gId <- withStore $ \db -> getGroupIdByName db user name - let chatRef = ChatRef CTGroup gId - processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage Nothing Nothing mc - _ -> throwChatError $ CECommandError "not supported" - SendMemberContactMessage gName mName msg -> withUser $ \user -> do - (gId, mId) <- getGroupAndMemberId user gName mName - m <- withStore $ \db -> getGroupMember db user gId mId - let mc = MCText msg - case memberContactId m of - Nothing -> do - gInfo <- withStore $ \db -> getGroupInfo db vr user gId - toView $ CRNoMemberContactCreating user gInfo m - processChatCommand (APICreateMemberContact gId mId) >>= \case - cr@(CRNewMemberContact _ Contact {contactId} _ _) -> do - toView cr - processChatCommand $ APISendMemberContactInvitation contactId (Just mc) - cr -> pure cr - Just ctId -> do - let chatRef = ChatRef CTDirect ctId - processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage Nothing Nothing mc - SendLiveMessage chatName msg -> withUser $ \user -> do - chatRef <- getChatRef user chatName - let mc = MCText msg - processChatCommand . APISendMessage chatRef True Nothing $ ComposedMessage Nothing Nothing mc - SendMessageBroadcast msg -> withUser $ \user -> do - contacts <- withStore' (`getUserContacts` user) - let cts = filter (\ct -> contactReady ct && contactActive ct && directOrUsed ct) contacts - ChatConfig {logLevel} <- asks config - withChatLock "sendMessageBroadcast" . procCmd $ do - (successes, failures) <- foldM (sendAndCount user logLevel) (0, 0) cts - timestamp <- liftIO getCurrentTime - pure CRBroadcastSent {user, msgContent = mc, successes, failures, timestamp} - where - mc = MCText msg - sendAndCount user ll (s, f) ct = - (sendToContact user ct $> (s + 1, f)) `catchChatError` \e -> when (ll <= CLLInfo) (toView $ CRChatError (Just user) e) $> (s, f + 1) - sendToContact user ct = do - (sndMsg, _) <- sendDirectContactMessage ct (XMsgNew $ MCSimple (extMsgContent mc Nothing)) - void $ saveSndChatItem user (CDDirectSnd ct) sndMsg (CISndMsgContent mc) - SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \user@User {userId} -> do - contactId <- withStore $ \db -> getContactIdByName db user cName - quotedItemId <- withStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg - let mc = MCText msg - processChatCommand . APISendMessage (ChatRef CTDirect contactId) False Nothing $ ComposedMessage Nothing (Just quotedItemId) mc - DeleteMessage chatName deletedMsg -> withUser $ \user -> do - chatRef <- getChatRef user chatName - deletedItemId <- getSentChatItemIdByText user chatRef deletedMsg - processChatCommand $ APIDeleteChatItem chatRef deletedItemId CIDMBroadcast - DeleteMemberMessage gName mName deletedMsg -> withUser $ \user -> do - (gId, mId) <- getGroupAndMemberId user gName mName - deletedItemId <- withStore $ \db -> getGroupChatItemIdByText db user gId (Just mName) deletedMsg - processChatCommand $ APIDeleteMemberChatItem gId mId deletedItemId - EditMessage chatName editedMsg msg -> withUser $ \user -> do - chatRef <- getChatRef user chatName - editedItemId <- getSentChatItemIdByText user chatRef editedMsg - let mc = MCText msg - processChatCommand $ APIUpdateChatItem chatRef editedItemId False mc - UpdateLiveMessage chatName chatItemId live msg -> withUser $ \user -> do - chatRef <- getChatRef user chatName - let mc = MCText msg - processChatCommand $ APIUpdateChatItem chatRef chatItemId live mc - ReactToMessage add reaction chatName msg -> withUser $ \user -> do - chatRef <- getChatRef user chatName - chatItemId <- getChatItemIdByText user chatRef msg - processChatCommand $ APIChatItemReaction chatRef chatItemId add reaction - APINewGroup userId incognito gProfile@GroupProfile {displayName} -> withUserId userId $ \user -> do - checkValidName displayName - gVar <- asks random - -- [incognito] generate incognito profile for group membership - incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - groupInfo <- withStore $ \db -> createNewGroup db vr gVar user gProfile incognitoProfile - pure $ CRGroupCreated user groupInfo - NewGroup incognito gProfile -> withUser $ \User {userId} -> - processChatCommand $ APINewGroup userId incognito gProfile - APIAddMember groupId contactId memRole -> withUser $ \user -> withChatLock "addMember" $ do - -- TODO for large groups: no need to load all members to determine if contact is a member - (group, contact) <- withStore $ \db -> (,) <$> getGroup db vr user groupId <*> getContact db user contactId - assertDirectAllowed user MDSnd contact XGrpInv_ - let Group gInfo members = group - Contact {localDisplayName = cName} = contact - assertUserGroupRole gInfo $ max GRAdmin memRole - -- [incognito] forbid to invite contact to whom user is connected incognito - when (contactConnIncognito contact) $ throwChatError CEContactIncognitoCantInvite - -- [incognito] forbid to invite contacts if user joined the group using an incognito profile - when (incognitoMembership gInfo) $ throwChatError CEGroupIncognitoCantInvite - let sendInvitation = sendGrpInvitation user contact gInfo - case contactMember contact members of - Nothing -> do - gVar <- asks random - subMode <- chatReadVar subscriptionMode - (agentConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode - member <- withStore $ \db -> createNewContactMember db gVar user gInfo contact memRole agentConnId cReq subMode - sendInvitation member cReq - pure $ CRSentGroupInvitation user gInfo contact member - Just member@GroupMember {groupMemberId, memberStatus, memberRole = mRole} - | memberStatus == GSMemInvited -> do - unless (mRole == memRole) $ withStore' $ \db -> updateGroupMemberRole db user member memRole - withStore' (\db -> getMemberInvitation db user groupMemberId) >>= \case - Just cReq -> do - sendInvitation member {memberRole = memRole} cReq - pure $ CRSentGroupInvitation user gInfo contact member {memberRole = memRole} - Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName - | otherwise -> throwChatError $ CEGroupDuplicateMember cName - APIJoinGroup groupId -> withUser $ \user@User {userId} -> do - withChatLock "joinGroup" . procCmd $ do - (invitation, ct) <- withStore $ \db -> do - inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db vr user groupId - (inv,) <$> getContactViaMember db user fromMember - let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} = invitation - Contact {activeConn} = ct - case activeConn of - Just Connection {peerChatVRange} -> do - subMode <- chatReadVar subscriptionMode - dm <- directMessage $ XGrpAcpt membership.memberId - agentConnId <- withAgent $ \a -> joinConnection a (aUserId user) True connRequest dm subMode - withStore' $ \db -> do - createMemberConnection db userId fromMember agentConnId (fromJVersionRange peerChatVRange) subMode - updateGroupMemberStatus db userId fromMember GSMemAccepted - updateGroupMemberStatus db userId membership GSMemAccepted - updateCIGroupInvitationStatus user - pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing - Nothing -> throwChatError $ CEContactNotActive ct - where - updateCIGroupInvitationStatus user = do - AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withStore $ \db -> getChatItemByGroupId db vr user groupId - case (cInfo, content) of - (DirectChat ct, CIRcvGroupInvitation ciGroupInv memRole) -> do - let aciContent = ACIContent SMDRcv $ CIRcvGroupInvitation ciGroupInv {status = CIGISAccepted} memRole - updateDirectChatItemView user ct itemId aciContent False Nothing - _ -> pure () -- prohibited - APIMemberRole groupId memberId memRole -> withUser $ \user -> do - Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db vr user groupId - if memberId == groupMemberId' membership - then changeMemberRole user gInfo members membership $ SGEUserRole memRole - else case find ((== memberId) . groupMemberId') members of - Just m -> changeMemberRole user gInfo members m $ SGEMemberRole memberId (fromLocalProfile $ memberProfile m) memRole - _ -> throwChatError CEGroupMemberNotFound - where - changeMemberRole user gInfo members m gEvent = do - let GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus, memberContactId, localDisplayName = cName} = m - assertUserGroupRole gInfo $ maximum [GRAdmin, mRole, memRole] - withChatLock "memberRole" . procCmd $ do - unless (mRole == memRole) $ do - withStore' $ \db -> updateGroupMemberRole db user m memRole - case mStatus of - GSMemInvited -> do - withStore (\db -> (,) <$> mapM (getContact db user) memberContactId <*> liftIO (getMemberInvitation db user $ groupMemberId' m)) >>= \case - (Just ct, Just cReq) -> sendGrpInvitation user ct gInfo (m :: GroupMember) {memberRole = memRole} cReq - _ -> throwChatError $ CEGroupCantResendInvitation gInfo cName - _ -> do - (msg, _) <- sendGroupMessage user gInfo members $ XGrpMemRole mId memRole - ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent gEvent) - toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) - pure CRMemberRoleUser {user, groupInfo = gInfo, member = m {memberRole = memRole}, fromRole = mRole, toRole = memRole} - APIRemoveMember groupId memberId -> withUser $ \user -> do - Group gInfo members <- withStore $ \db -> getGroup db vr user groupId - case find ((== memberId) . groupMemberId') members of - Nothing -> throwChatError CEGroupMemberNotFound - Just m@GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus, memberProfile} -> do - assertUserGroupRole gInfo $ max GRAdmin mRole - withChatLock "removeMember" . procCmd $ do - case mStatus of - GSMemInvited -> do - deleteMemberConnection user m - withStore' $ \db -> deleteGroupMember db user m - _ -> do - (msg, _) <- sendGroupMessage user gInfo members $ XGrpMemDel mId - ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent $ SGEMemberDeleted memberId (fromLocalProfile memberProfile)) - toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) - deleteMemberConnection user m - -- undeleted "member connected" chat item will prevent deletion of member record - deleteOrUpdateMemberRecord user m - pure $ CRUserDeletedMember user gInfo m {memberStatus = GSMemRemoved} - APILeaveGroup groupId -> withUser $ \user@User {userId} -> do - Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db vr user groupId - withChatLock "leaveGroup" . procCmd $ do - (msg, _) <- sendGroupMessage user gInfo members XGrpLeave - ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent SGEUserLeft) - toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) - -- TODO delete direct connections that were unused - deleteGroupLinkIfExists user gInfo - -- member records are not deleted to keep history - deleteMembersConnections user members - withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemLeft - pure $ CRLeftMemberUser user gInfo {membership = membership {memberStatus = GSMemLeft}} - APIListMembers groupId -> withUser $ \user -> - CRGroupMembers user <$> withStore (\db -> getGroup db vr user groupId) - AddMember gName cName memRole -> withUser $ \user -> do - (groupId, contactId) <- withStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db user cName - processChatCommand $ APIAddMember groupId contactId memRole - JoinGroup gName -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIJoinGroup groupId - MemberRole gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIMemberRole gId gMemberId memRole - RemoveMember gName gMemberName -> withMemberName gName gMemberName APIRemoveMember - LeaveGroup gName -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APILeaveGroup groupId - DeleteGroup gName -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIDeleteChat (ChatRef CTGroup groupId) True - ClearGroup gName -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIClearChat (ChatRef CTGroup groupId) - ListMembers gName -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIListMembers groupId - APIListGroups userId contactId_ search_ -> withUserId userId $ \user -> - CRGroupsList user <$> withStore' (\db -> getUserGroupsWithSummary db vr user contactId_ search_) - ListGroups cName_ search_ -> withUser $ \user@User {userId} -> do - ct_ <- forM cName_ $ \cName -> withStore $ \db -> getContactByName db user cName - processChatCommand $ APIListGroups userId (contactId' <$> ct_) search_ - APIUpdateGroupProfile groupId p' -> withUser $ \user -> do - g <- withStore $ \db -> getGroup db vr user groupId - runUpdateGroupProfile user g p' - UpdateGroupNames gName GroupProfile {displayName, fullName} -> - updateGroupProfileByName gName $ \p -> p {displayName, fullName} - ShowGroupProfile gName -> withUser $ \user -> - CRGroupProfile user <$> withStore (\db -> getGroupInfoByName db vr user gName) - UpdateGroupDescription gName description -> - updateGroupProfileByName gName $ \p -> p {description} - ShowGroupDescription gName -> withUser $ \user -> - CRGroupDescription user <$> withStore (\db -> getGroupInfoByName db vr user gName) - APICreateGroupLink groupId mRole -> withUser $ \user -> withChatLock "createGroupLink" $ do - gInfo <- withStore $ \db -> getGroupInfo db vr user groupId - assertUserGroupRole gInfo GRAdmin - when (mRole > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole - groupLinkId <- GroupLinkId <$> drgRandomBytes 16 - subMode <- chatReadVar subscriptionMode - let crClientData = encodeJSON $ CRDataGroup groupLinkId - (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact (Just crClientData) subMode - withStore $ \db -> createGroupLink db user gInfo connId cReq groupLinkId mRole subMode - pure $ CRGroupLinkCreated user gInfo cReq mRole - APIGroupLinkMemberRole groupId mRole' -> withUser $ \user -> withChatLock "groupLinkMemberRole " $ do - gInfo <- withStore $ \db -> getGroupInfo db vr user groupId - (groupLinkId, groupLink, mRole) <- withStore $ \db -> getGroupLink db user gInfo - assertUserGroupRole gInfo GRAdmin - when (mRole' > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole' - when (mRole' /= mRole) $ withStore' $ \db -> setGroupLinkMemberRole db user groupLinkId mRole' - pure $ CRGroupLink user gInfo groupLink mRole' - APIDeleteGroupLink groupId -> withUser $ \user -> withChatLock "deleteGroupLink" $ do - gInfo <- withStore $ \db -> getGroupInfo db vr user groupId - deleteGroupLink' user gInfo - pure $ CRGroupLinkDeleted user gInfo - APIGetGroupLink groupId -> withUser $ \user -> do - gInfo <- withStore $ \db -> getGroupInfo db vr user groupId - (_, groupLink, mRole) <- withStore $ \db -> getGroupLink db user gInfo - pure $ CRGroupLink user gInfo groupLink mRole - APICreateMemberContact gId gMemberId -> withUser $ \user -> do - (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db user gId gMemberId - assertUserGroupRole g GRAuthor - unless (groupFeatureAllowed SGFDirectMessages g) $ throwChatError $ CECommandError "direct messages not allowed" - case memberConn m of - Just mConn@Connection {peerChatVRange} -> do - unless (isCompatibleRange (fromJVersionRange peerChatVRange) xGrpDirectInvVRange) $ throwChatError CEPeerChatVRangeIncompatible - when (isJust $ memberContactId m) $ throwChatError $ CECommandError "member contact already exists" - subMode <- chatReadVar subscriptionMode - (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode - -- [incognito] reuse membership incognito profile - ct <- withStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode - -- TODO not sure it is correct to set connections status here? - setContactNetworkStatus ct NSConnected - pure $ CRNewMemberContact user ct g m - _ -> throwChatError CEGroupMemberNotActive - APISendMemberContactInvitation contactId msgContent_ -> withUser $ \user -> do - (g, m, ct, cReq) <- withStore $ \db -> getMemberContact db vr user contactId - when (contactGrpInvSent ct) $ throwChatError $ CECommandError "x.grp.direct.inv already sent" - case memberConn m of - Just mConn -> do - let msg = XGrpDirectInv cReq msgContent_ - (sndMsg, _) <- sendDirectMessage mConn msg (GroupId $ g.groupId) - withStore' $ \db -> setContactGrpInvSent db ct True - let ct' = ct {contactGrpInvSent = True} - forM_ msgContent_ $ \mc -> do - ci <- saveSndChatItem user (CDDirectSnd ct') sndMsg (CISndMsgContent mc) - toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct') ci) - pure $ CRNewMemberContactSentInv user ct' g m - _ -> throwChatError CEGroupMemberNotActive - CreateGroupLink gName mRole -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APICreateGroupLink groupId mRole - GroupLinkMemberRole gName mRole -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIGroupLinkMemberRole groupId mRole - DeleteGroupLink gName -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIDeleteGroupLink groupId - ShowGroupLink gName -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIGetGroupLink groupId - SendGroupMessageQuote gName cName quotedMsg msg -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName - quotedItemId <- withStore $ \db -> getGroupChatItemIdByText db user groupId cName quotedMsg - let mc = MCText msg - processChatCommand . APISendMessage (ChatRef CTGroup groupId) False Nothing $ ComposedMessage Nothing (Just quotedItemId) mc - LastChats count_ -> withUser' $ \user -> do - let count = fromMaybe 5000 count_ - (errs, previews) <- partitionEithers <$> withStore' (\db -> getChatPreviews db vr user False (PTLast count) clqNoFilters) - unless (null errs) $ toView $ CRChatErrors (Just user) (map ChatErrorStore errs) - pure $ CRChats previews - LastMessages (Just chatName) count search -> withUser $ \user -> do - chatRef <- getChatRef user chatName - chatResp <- processChatCommand $ APIGetChat chatRef (CPLast count) search - pure $ CRChatItems user (Just chatName) (aChatItems . chat $ chatResp) - LastMessages Nothing count search -> withUser $ \user -> do - chatItems <- withStore $ \db -> getAllChatItems db vr user (CPLast count) search - pure $ CRChatItems user Nothing chatItems - LastChatItemId (Just chatName) index -> withUser $ \user -> do - chatRef <- getChatRef user chatName - chatResp <- processChatCommand (APIGetChat chatRef (CPLast $ index + 1) Nothing) - pure $ CRChatItemId user (fmap aChatItemId . listToMaybe . aChatItems . chat $ chatResp) - LastChatItemId Nothing index -> withUser $ \user -> do - chatItems <- withStore $ \db -> getAllChatItems db vr user (CPLast $ index + 1) Nothing - pure $ CRChatItemId user (fmap aChatItemId . listToMaybe $ chatItems) - ShowChatItem (Just itemId) -> withUser $ \user -> do - chatItem <- withStore $ \db -> do - chatRef <- getChatRefViaItemId db user itemId - getAChatItem db vr user chatRef itemId - pure $ CRChatItems user Nothing ((: []) chatItem) - ShowChatItem Nothing -> withUser $ \user -> do - chatItems <- withStore $ \db -> getAllChatItems db vr user (CPLast 1) Nothing - pure $ CRChatItems user Nothing chatItems - ShowChatItemInfo chatName msg -> withUser $ \user -> do - chatRef <- getChatRef user chatName - itemId <- getChatItemIdByText user chatRef msg - processChatCommand $ APIGetChatItemInfo chatRef itemId - ShowLiveItems on -> withUser $ \_ -> - asks showLiveItems >>= atomically . (`writeTVar` on) >> ok_ - SendFile chatName f -> withUser $ \user -> do - chatRef <- getChatRef user chatName - processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage (Just f) Nothing (MCFile "") - SendImage chatName f@(CryptoFile fPath _) -> withUser $ \user -> do - chatRef <- getChatRef user chatName - filePath <- toFSFilePath fPath - unless (any (`isSuffixOf` map toLower fPath) imageExtensions) $ throwChatError CEFileImageType {filePath} - fileSize <- getFileSize filePath - unless (fileSize <= maxImageSize) $ throwChatError CEFileImageSize {filePath} - -- TODO include file description for preview - processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage (Just f) Nothing (MCImage "" fixedImagePreview) - ForwardFile chatName fileId -> forwardFile chatName fileId SendFile - ForwardImage chatName fileId -> forwardFile chatName fileId SendImage - SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO" - ReceiveFile fileId encrypted_ rcvInline_ filePath_ -> withUser $ \_ -> - withChatLock "receiveFile" . procCmd $ do - (user, ft) <- withStore (`getRcvFileTransferById` fileId) - encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles - ft' <- (if encrypt then setFileToEncrypt else pure) ft - receiveFile' user ft' rcvInline_ filePath_ - SetFileToReceive fileId encrypted_ -> withUser $ \_ -> do - withChatLock "setFileToReceive" . procCmd $ do - encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles - cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing - withStore' $ \db -> setRcvFileToReceive db fileId cfArgs - ok_ - CancelFile fileId -> withUser $ \user@User {userId} -> - withChatLock "cancelFile" . procCmd $ - withStore (\db -> getFileTransfer db user fileId) >>= \case - FTSnd ftm@FileTransferMeta {xftpSndFile, cancelled} fts - | cancelled -> throwChatError $ CEFileCancel fileId "file already cancelled" - | not (null fts) && all fileCancelledOrCompleteSMP fts -> - throwChatError $ CEFileCancel fileId "file transfer is complete" - | otherwise -> do - fileAgentConnIds <- cancelSndFile user ftm fts True - deleteAgentConnectionsAsync user fileAgentConnIds - sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId - withStore (\db -> getChatRefByFileId db user fileId) >>= \case - ChatRef CTDirect contactId -> do - contact <- withStore $ \db -> getContact db user contactId - void . sendDirectContactMessage contact $ XFileCancel sharedMsgId - ChatRef CTGroup groupId -> do - Group gInfo ms <- withStore $ \db -> getGroup db vr user groupId - void . sendGroupMessage user gInfo ms $ XFileCancel sharedMsgId - _ -> throwChatError $ CEFileInternal "invalid chat ref for file transfer" - ci <- withStore $ \db -> getChatItemByFileId db vr user fileId - pure $ CRSndFileCancelled user ci ftm fts - where - fileCancelledOrCompleteSMP SndFileTransfer {fileStatus = s} = - s == FSCancelled || (s == FSComplete && isNothing xftpSndFile) - FTRcv ftr@RcvFileTransfer {cancelled, fileStatus, xftpRcvFile} - | cancelled -> throwChatError $ CEFileCancel fileId "file already cancelled" - | rcvFileComplete fileStatus -> throwChatError $ CEFileCancel fileId "file transfer is complete" - | otherwise -> case xftpRcvFile of - Nothing -> do - cancelRcvFileTransfer user ftr >>= mapM_ (deleteAgentConnectionAsync user) - ci <- withStore $ \db -> getChatItemByFileId db vr user fileId - pure $ CRRcvFileCancelled user ci ftr - Just XFTPRcvFile {agentRcvFileId} -> do - forM_ (liveRcvFileTransferPath ftr) $ \filePath -> do - fsFilePath <- toFSFilePath filePath - liftIO $ removeFile fsFilePath `catchAll_` pure () - forM_ agentRcvFileId $ \(AgentRcvFileId aFileId) -> - withAgent (`xftpDeleteRcvFile` aFileId) - ci <- withStore $ \db -> do - liftIO $ do - updateCIFileStatus db user fileId CIFSRcvInvitation - updateRcvFileStatus db fileId FSNew - updateRcvFileAgentId db fileId Nothing - getChatItemByFileId db vr user fileId - pure $ CRRcvFileCancelled user ci ftr - FileStatus fileId -> withUser $ \user -> do - ci@(AChatItem _ _ _ ChatItem {file}) <- withStore $ \db -> getChatItemByFileId db vr user fileId - case file of - Just CIFile {fileProtocol = FPXFTP} -> - pure $ CRFileTransferStatusXFTP user ci - _ -> do - fileStatus <- withStore $ \db -> getFileTransferProgress db user fileId - pure $ CRFileTransferStatus user fileStatus - ShowProfile -> withUser $ \user@User {profile} -> pure $ CRUserProfile user (fromLocalProfile profile) - UpdateProfile displayName fullName -> withUser $ \user@User {profile} -> do - let p = (fromLocalProfile profile :: Profile) {displayName = displayName, fullName = fullName} - updateProfile user p - UpdateProfileImage image -> withUser $ \user@User {profile} -> do - let p = (fromLocalProfile profile :: Profile) {image} - updateProfile user p - ShowProfileImage -> withUser $ \user@User {profile} -> pure $ CRUserProfileImage user $ fromLocalProfile profile - SetUserFeature (ACF f) allowed -> withUser $ \user@User {profile} -> do - let p = (fromLocalProfile profile :: Profile) {preferences = Just . setPreference f (Just allowed) $ preferences' user} - updateProfile user p - SetContactFeature (ACF f) cName allowed_ -> withUser $ \user -> do - ct@Contact {userPreferences} <- withStore $ \db -> getContactByName db user cName - let prefs' = setPreference f allowed_ $ Just userPreferences - updateContactPrefs user ct prefs' - SetGroupFeature (AGF f) gName enabled -> - updateGroupProfileByName gName $ \p -> - p {groupPreferences = Just . setGroupPreference f enabled $ groupPreferences p} - SetUserTimedMessages onOff -> withUser $ \user@User {profile} -> do - let allowed = if onOff then FAYes else FANo - pref = TimedMessagesPreference allowed Nothing - p = (fromLocalProfile profile :: Profile) {preferences = Just . setPreference' SCFTimedMessages (Just pref) $ preferences' user} - updateProfile user p - SetContactTimedMessages cName timedMessagesEnabled_ -> withUser $ \user -> do - ct@Contact {userPreferences = userPreferences@Preferences {timedMessages}} <- withStore $ \db -> getContactByName db user cName - let currentTTL = timedMessages >>= \TimedMessagesPreference {ttl} -> ttl - pref_ = tmeToPref currentTTL <$> timedMessagesEnabled_ - prefs' = setPreference' SCFTimedMessages pref_ $ Just userPreferences - updateContactPrefs user ct prefs' - SetGroupTimedMessages gName ttl_ -> do - let pref = uncurry TimedMessagesGroupPreference $ maybe (FEOff, Just 86400) (\ttl -> (FEOn, Just ttl)) ttl_ - updateGroupProfileByName gName $ \p -> - p {groupPreferences = Just . setGroupPreference' SGFTimedMessages pref $ groupPreferences p} - SetLocalDeviceName name -> chatWriteVar localDeviceName name >> ok_ - ListRemoteHosts -> CRRemoteHostList <$> listRemoteHosts - SwitchRemoteHost rh_ -> CRCurrentRemoteHost <$> switchRemoteHost rh_ - StartRemoteHost rh_ ca_ bp_ -> do - (localAddrs, remoteHost_, inv@RCSignedInvitation {invitation = RCInvitation {port}}) <- startRemoteHost rh_ ca_ bp_ - pure CRRemoteHostStarted {remoteHost_, invitation = decodeLatin1 $ strEncode inv, ctrlPort = show port, localAddrs} - StopRemoteHost rh_ -> closeRemoteHost rh_ >> ok_ - DeleteRemoteHost rh -> deleteRemoteHost rh >> ok_ - StoreRemoteFile rh encrypted_ localPath -> CRRemoteFileStored rh <$> storeRemoteFile rh encrypted_ localPath - GetRemoteFile rh rf -> getRemoteFile rh rf >> ok_ - ConnectRemoteCtrl inv -> withUser_ $ do - (remoteCtrl_, ctrlAppInfo) <- connectRemoteCtrlURI inv - pure CRRemoteCtrlConnecting {remoteCtrl_, ctrlAppInfo, appVersion = currentAppVersion} - FindKnownRemoteCtrl -> withUser_ $ findKnownRemoteCtrl >> ok_ - ConfirmRemoteCtrl rcId -> withUser_ $ do - (rc, ctrlAppInfo) <- confirmRemoteCtrl rcId - pure CRRemoteCtrlConnecting {remoteCtrl_ = Just rc, ctrlAppInfo, appVersion = currentAppVersion} - VerifyRemoteCtrlSession sessId -> withUser_ $ CRRemoteCtrlConnected <$> verifyRemoteCtrlSession (execChatCommand Nothing) sessId - StopRemoteCtrl -> withUser_ $ stopRemoteCtrl >> ok_ - ListRemoteCtrls -> withUser_ $ CRRemoteCtrlList <$> listRemoteCtrls - DeleteRemoteCtrl rc -> withUser_ $ deleteRemoteCtrl rc >> ok_ - QuitChat -> liftIO exitSuccess - ShowVersion -> do - -- simplexmqCommitQ makes iOS builds crash m( - let versionInfo = coreVersionInfo "" - chatMigrations <- map upMigration <$> withStore' (Migrations.getCurrent . DB.conn) - agentMigrations <- withAgent getAgentMigrations - pure $ CRVersionInfo {versionInfo, chatMigrations, agentMigrations} - DebugLocks -> do - chatLockName <- atomically . tryReadTMVar =<< asks chatLock - agentLocks <- withAgent debugAgentLocks - pure CRDebugLocks {chatLockName, agentLocks} - GetAgentStats -> CRAgentStats . map stat <$> withAgent getAgentStats - where - stat (AgentStatsKey {host, clientTs, cmd, res}, count) = - map B.unpack [host, clientTs, cmd, res, bshow count] - ResetAgentStats -> withAgent resetAgentStats >> ok_ - GetAgentSubs -> summary <$> withAgent getAgentSubscriptions - where - summary SubscriptionsInfo {activeSubscriptions, pendingSubscriptions, removedSubscriptions} = - CRAgentSubs - { activeSubs = foldl' countSubs M.empty activeSubscriptions, - pendingSubs = foldl' countSubs M.empty pendingSubscriptions, - removedSubs = foldl' accSubErrors M.empty removedSubscriptions - } - where - countSubs m SubInfo {server} = M.alter (Just . maybe 1 (+ 1)) server m - accSubErrors m = \case - SubInfo {server, subError = Just e} -> M.alter (Just . maybe [e] (e :)) server m - _ -> m - GetAgentSubsDetails -> CRAgentSubsDetails <$> withAgent getAgentSubscriptions - where - withChatLock name action = asks chatLock >>= \l -> withLock l name action - -- below code would make command responses asynchronous where they can be slow - -- in View.hs `r'` should be defined as `id` in this case - -- procCmd :: m ChatResponse -> m ChatResponse - -- procCmd action = do - -- ChatController {chatLock = l, smpAgent = a, outputQ = q, random = gVar} <- ask - -- corrId <- liftIO $ SMP.CorrId <$> randomBytes gVar 8 - -- void . forkIO $ - -- withAgentLock a . withLock l name $ - -- (atomically . writeTBQueue q) . (Just corrId,) =<< (action `catchChatError` (pure . CRChatError)) - -- pure $ CRCmdAccepted corrId - -- use function below to make commands "synchronous" - procCmd :: m ChatResponse -> m ChatResponse - procCmd = id - ok_ = pure $ CRCmdOk Nothing - ok = pure . CRCmdOk . Just - getChatRef :: User -> ChatName -> m ChatRef - getChatRef user (ChatName cType name) = - ChatRef cType <$> case cType of - CTDirect -> withStore $ \db -> getContactIdByName db user name - CTGroup -> withStore $ \db -> getGroupIdByName db user name - _ -> throwChatError $ CECommandError "not supported" - checkChatStopped :: m ChatResponse -> m ChatResponse - checkChatStopped a = asks agentAsync >>= readTVarIO >>= maybe a (const $ throwChatError CEChatNotStopped) - setStoreChanged :: m () - setStoreChanged = asks chatStoreChanged >>= atomically . (`writeTVar` True) - withStoreChanged :: m () -> m ChatResponse - withStoreChanged a = checkChatStopped $ a >> setStoreChanged >> ok_ - checkStoreNotChanged :: m ChatResponse -> m ChatResponse - checkStoreNotChanged = ifM (asks chatStoreChanged >>= readTVarIO) (throwChatError CEChatStoreChanged) - withUserName :: UserName -> (UserId -> ChatCommand) -> m ChatResponse - withUserName uName cmd = withStore (`getUserIdByName` uName) >>= processChatCommand . cmd - withContactName :: ContactName -> (ContactId -> ChatCommand) -> m ChatResponse - withContactName cName cmd = withUser $ \user -> - withStore (\db -> getContactIdByName db user cName) >>= processChatCommand . cmd - withMemberName :: GroupName -> ContactName -> (GroupId -> GroupMemberId -> ChatCommand) -> m ChatResponse - withMemberName gName mName cmd = withUser $ \user -> - getGroupAndMemberId user gName mName >>= processChatCommand . uncurry cmd - getConnectionCode :: ConnId -> m Text - getConnectionCode connId = verificationCode <$> withAgent (`getConnectionRatchetAdHash` connId) - verifyConnectionCode :: User -> Connection -> Maybe Text -> m ChatResponse - verifyConnectionCode user conn@Connection {connId} (Just code) = do - code' <- getConnectionCode $ aConnId conn - let verified = sameVerificationCode code code' - when verified . withStore' $ \db -> setConnectionVerified db user connId $ Just code' - pure $ CRConnectionVerified user verified code' - verifyConnectionCode user conn@Connection {connId} _ = do - code' <- getConnectionCode $ aConnId conn - withStore' $ \db -> setConnectionVerified db user connId Nothing - pure $ CRConnectionVerified user False code' - getSentChatItemIdByText :: User -> ChatRef -> Text -> m Int64 - getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId) msg = case cType of - CTDirect -> withStore $ \db -> getDirectChatItemIdByText db userId cId SMDSnd msg - CTGroup -> withStore $ \db -> getGroupChatItemIdByText db user cId (Just localDisplayName) msg - _ -> throwChatError $ CECommandError "not supported" - getChatItemIdByText :: User -> ChatRef -> Text -> m Int64 - getChatItemIdByText user (ChatRef cType cId) msg = case cType of - CTDirect -> withStore $ \db -> getDirectChatItemIdByText' db user cId msg - CTGroup -> withStore $ \db -> getGroupChatItemIdByText' db user cId msg - _ -> throwChatError $ CECommandError "not supported" - connectViaContact :: User -> IncognitoEnabled -> ConnectionRequestUri 'CMContact -> m ChatResponse - connectViaContact user@User {userId} incognito cReq@(CRContactUri ConnReqUriData {crClientData}) = withChatLock "connectViaContact" $ do - let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli - cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq - case groupLinkId of - -- contact address - Nothing -> - withStore' (\db -> getConnReqContactXContactId db user cReqHash) >>= \case - (Just contact, _) -> pure $ CRContactAlreadyExists user contact - (_, xContactId_) -> procCmd $ do - let randomXContactId = XContactId <$> drgRandomBytes 16 - xContactId <- maybe randomXContactId pure xContactId_ - connect' Nothing cReqHash xContactId - -- group link - Just gLinkId -> - withStore' (\db -> getConnReqContactXContactId db user cReqHash) >>= \case - (Just _contact, _) -> procCmd $ do - -- allow repeat contact request - newXContactId <- XContactId <$> drgRandomBytes 16 - connect' (Just gLinkId) cReqHash newXContactId - (_, xContactId_) -> procCmd $ do - let randomXContactId = XContactId <$> drgRandomBytes 16 - xContactId <- maybe randomXContactId pure xContactId_ - connect' (Just gLinkId) cReqHash xContactId - where - connect' groupLinkId cReqHash xContactId = do - (connId, incognitoProfile, subMode) <- requestContact user incognito cReq xContactId - conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode - pure $ CRSentInvitation user conn incognitoProfile - connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> ConnectionRequestUri 'CMContact -> m ChatResponse - connectContactViaAddress user incognito ct cReq = - withChatLock "connectViaContact" $ do - newXContactId <- XContactId <$> drgRandomBytes 16 - (connId, incognitoProfile, subMode) <- requestContact user incognito cReq newXContactId - let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq - ct' <- withStore $ \db -> createAddressContactConnection db user ct connId cReqHash newXContactId incognitoProfile subMode - pure $ CRSentInvitationToContact user ct' incognitoProfile - requestContact :: User -> IncognitoEnabled -> ConnectionRequestUri 'CMContact -> XContactId -> m (ConnId, Maybe Profile, SubscriptionMode) - requestContact user incognito cReq xContactId = do - -- [incognito] generate profile to send - incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - let profileToSend = userProfileToSend user incognitoProfile Nothing - dm <- directMessage (XContact profileToSend $ Just xContactId) - subMode <- chatReadVar subscriptionMode - connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm subMode - pure (connId, incognitoProfile, subMode) - contactMember :: Contact -> [GroupMember] -> Maybe GroupMember - contactMember Contact {contactId} = - find $ \GroupMember {memberContactId = cId, memberStatus = s} -> - cId == Just contactId && s /= GSMemRemoved && s /= GSMemLeft - checkSndFile :: MsgContent -> CryptoFile -> Integer -> m (Integer, SendFileMode) - checkSndFile mc (CryptoFile f cfArgs) n = do - fsFilePath <- toFSFilePath f - unlessM (doesFileExist fsFilePath) . throwChatError $ CEFileNotFound f - ChatConfig {fileChunkSize, inlineFiles} <- asks config - xftpCfg <- readTVarIO =<< asks userXFTPFileConfig - fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cfArgs - when (fromInteger fileSize > maxFileSize) $ throwChatError $ CEFileSize f - let chunks = -((-fileSize) `div` fileChunkSize) - fileInline = inlineFileMode mc inlineFiles chunks n - fileMode = case xftpCfg of - Just cfg - | isJust cfArgs -> SendFileXFTP - | fileInline == Just IFMSent || fileSize < minFileSize cfg || n <= 0 -> SendFileSMP fileInline - | otherwise -> SendFileXFTP - _ -> SendFileSMP fileInline - pure (fileSize, fileMode) - inlineFileMode mc InlineFilesConfig {offerChunks, sendChunks, totalSendChunks} chunks n - | chunks > offerChunks = Nothing - | chunks <= sendChunks && chunks * n <= totalSendChunks && isVoice mc = Just IFMSent - | otherwise = Just IFMOffer - updateProfile :: User -> Profile -> m ChatResponse - updateProfile user p' = updateProfile_ user p' $ withStore $ \db -> updateUserProfile db user p' - updateProfile_ :: User -> Profile -> m User -> m ChatResponse - updateProfile_ user@User {profile = p@LocalProfile {displayName = n}} p'@Profile {displayName = n'} updateUser - | p' == fromLocalProfile p = pure $ CRUserProfileNoChange user - | otherwise = do - when (n /= n') $ checkValidName n' - -- read contacts before user update to correctly merge preferences - -- [incognito] filter out contacts with whom user has incognito connections - contacts <- - filter (\ct -> contactReady ct && contactActive ct && not (contactConnIncognito ct)) - <$> withStore' (`getUserContacts` user) - user' <- updateUser - asks currentUser >>= atomically . (`writeTVar` Just user') - withChatLock "updateProfile" . procCmd $ do - ChatConfig {logLevel} <- asks config - summary <- foldM (processAndCount user' logLevel) (UserProfileUpdateSummary 0 0 0 []) contacts - pure $ CRUserProfileUpdated user' (fromLocalProfile p) p' summary - where - processAndCount user' ll s@UserProfileUpdateSummary {notChanged, updateSuccesses, updateFailures, changedContacts = cts} ct = do - let mergedProfile = userProfileToSend user Nothing $ Just ct - ct' = updateMergedPreferences user' ct - mergedProfile' = userProfileToSend user' Nothing $ Just ct' - if mergedProfile' == mergedProfile - then pure s {notChanged = notChanged + 1} - else - let cts' = if mergedPreferences ct == mergedPreferences ct' then cts else ct' : cts - in (notifyContact mergedProfile' ct' $> s {updateSuccesses = updateSuccesses + 1, changedContacts = cts'}) - `catchChatError` \e -> when (ll <= CLLInfo) (toView $ CRChatError (Just user) e) $> s {updateFailures = updateFailures + 1, changedContacts = cts'} - where - notifyContact mergedProfile' ct' = do - void $ sendDirectContactMessage ct' (XInfo mergedProfile') - when (directOrUsed ct') $ createSndFeatureItems user' ct ct' - updateContactPrefs :: User -> Contact -> Preferences -> m ChatResponse - updateContactPrefs _ ct@Contact {activeConn = Nothing} _ = throwChatError $ CEContactNotActive ct - updateContactPrefs user@User {userId} ct@Contact {activeConn = Just Connection {customUserProfileId}, userPreferences = contactUserPrefs} contactUserPrefs' - | contactUserPrefs == contactUserPrefs' = pure $ CRContactPrefsUpdated user ct ct - | otherwise = do - assertDirectAllowed user MDSnd ct XInfo_ - ct' <- withStore' $ \db -> updateContactUserPreferences db user ct contactUserPrefs' - incognitoProfile <- forM customUserProfileId $ \profileId -> withStore $ \db -> getProfileById db userId profileId - let mergedProfile = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct) - mergedProfile' = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct') - when (mergedProfile' /= mergedProfile) $ - withChatLock "updateProfile" $ do - void (sendDirectContactMessage ct' $ XInfo mergedProfile') `catchChatError` (toView . CRChatError (Just user)) - when (directOrUsed ct') $ createSndFeatureItems user ct ct' - pure $ CRContactPrefsUpdated user ct ct' - runUpdateGroupProfile :: User -> Group -> GroupProfile -> m ChatResponse - runUpdateGroupProfile user (Group g@GroupInfo {groupProfile = p@GroupProfile {displayName = n}} ms) p'@GroupProfile {displayName = n'} = do - assertUserGroupRole g GROwner - when (n /= n') $ checkValidName n' - g' <- withStore $ \db -> updateGroupProfile db user g p' - (msg, _) <- sendGroupMessage user g' ms (XGrpInfo p') - let cd = CDGroupSnd g' - unless (sameGroupProfileInfo p p') $ do - ci <- saveSndChatItem user cd msg (CISndGroupEvent $ SGEGroupUpdated p') - toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat g') ci) - createGroupFeatureChangedItems user cd CISndGroupFeature g g' - pure $ CRGroupUpdated user g g' Nothing - checkValidName :: GroupName -> m () - checkValidName displayName = do - let validName = T.pack $ mkValidName $ T.unpack displayName - when (displayName /= validName) $ throwChatError CEInvalidDisplayName {displayName, validName} - assertUserGroupRole :: GroupInfo -> GroupMemberRole -> m () - assertUserGroupRole g@GroupInfo {membership} requiredRole = do - when (membership.memberRole < requiredRole) $ throwChatError $ CEGroupUserRole g requiredRole - when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined g) - when (memberRemoved membership) $ throwChatError CEGroupMemberUserRemoved - unless (memberActive membership) $ throwChatError CEGroupMemberNotActive - delGroupChatItem :: MsgDirectionI d => User -> GroupInfo -> ChatItem 'CTGroup d -> MessageId -> Maybe GroupMember -> m ChatResponse - delGroupChatItem user gInfo ci msgId byGroupMember = do - deletedTs <- liftIO getCurrentTime - if groupFeatureAllowed SGFFullDelete gInfo - then deleteGroupCI user gInfo ci True False byGroupMember deletedTs - else markGroupCIDeleted user gInfo ci msgId True byGroupMember deletedTs - updateGroupProfileByName :: GroupName -> (GroupProfile -> GroupProfile) -> m ChatResponse - updateGroupProfileByName gName update = withUser $ \user -> do - g@(Group GroupInfo {groupProfile = p} _) <- withStore $ \db -> - getGroupIdByName db user gName >>= getGroup db vr user - runUpdateGroupProfile user g $ update p - withCurrentCall :: ContactId -> (User -> Contact -> Call -> m (Maybe Call)) -> m ChatResponse - withCurrentCall ctId action = do - (user, ct) <- withStore $ \db -> do - user <- getUserByContactId db ctId - (user,) <$> getContact db user ctId - calls <- asks currentCalls - withChatLock "currentCall" $ - atomically (TM.lookup ctId calls) >>= \case - Nothing -> throwChatError CENoCurrentCall - Just call@Call {contactId} - | ctId == contactId -> do - call_ <- action user ct call - case call_ of - Just call' -> do - unless (isRcvInvitation call') $ withStore' $ \db -> deleteCalls db user ctId - atomically $ TM.insert ctId call' calls - _ -> do - withStore' $ \db -> deleteCalls db user ctId - atomically $ TM.delete ctId calls - ok user - | otherwise -> throwChatError $ CECallContact contactId - withServerProtocol :: ProtocolTypeI p => SProtocolType p -> (UserProtocol p => m a) -> m a - withServerProtocol p action = case userProtocol p of - Just Dict -> action - _ -> throwChatError $ CEServerProtocol $ AProtocolType p - forwardFile :: ChatName -> FileTransferId -> (ChatName -> CryptoFile -> ChatCommand) -> m ChatResponse - forwardFile chatName fileId sendCommand = withUser $ \user -> do - withStore (\db -> getFileTransfer db user fileId) >>= \case - FTRcv RcvFileTransfer {fileStatus = RFSComplete RcvFileInfo {filePath}, cryptoArgs} -> forward filePath cryptoArgs - FTSnd {fileTransferMeta = FileTransferMeta {filePath, xftpSndFile}} -> forward filePath $ xftpSndFile >>= \f -> f.cryptoArgs - _ -> throwChatError CEFileNotReceived {fileId} - where - forward path cfArgs = processChatCommand . sendCommand chatName $ CryptoFile path cfArgs - getGroupAndMemberId :: User -> GroupName -> ContactName -> m (GroupId, GroupMemberId) - getGroupAndMemberId user gName groupMemberName = - withStore $ \db -> do - groupId <- getGroupIdByName db user gName - groupMemberId <- getGroupMemberIdByName db user groupId groupMemberName - pure (groupId, groupMemberId) - sendGrpInvitation :: User -> Contact -> GroupInfo -> GroupMember -> ConnReqInvitation -> m () - sendGrpInvitation user ct@Contact {localDisplayName} GroupInfo {groupId, groupProfile, membership} GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do - let GroupMember {memberRole = userRole, memberId = userMemberId} = membership - groupInv = GroupInvitation (MemberIdRole userMemberId userRole) (MemberIdRole memberId memRole) cReq groupProfile Nothing - (msg, _) <- sendDirectContactMessage ct $ XGrpInv groupInv - let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole - ci <- saveSndChatItem user (CDDirectSnd ct) msg content - toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) - sndContactCITimed :: Bool -> Contact -> Maybe Int -> m (Maybe CITimed) - sndContactCITimed live = sndCITimed_ live . contactTimedTTL - sndGroupCITimed :: Bool -> GroupInfo -> Maybe Int -> m (Maybe CITimed) - sndGroupCITimed live = sndCITimed_ live . groupTimedTTL - sndCITimed_ :: Bool -> Maybe (Maybe Int) -> Maybe Int -> m (Maybe CITimed) - sndCITimed_ live chatTTL itemTTL = - forM (chatTTL >>= (itemTTL <|>)) $ \ttl -> - CITimed ttl - <$> if live - then pure Nothing - else Just . addUTCTime (realToFrac ttl) <$> liftIO getCurrentTime - drgRandomBytes :: Int -> m ByteString - drgRandomBytes n = asks random >>= atomically . C.randomBytes n - privateGetUser :: UserId -> m User - privateGetUser userId = - tryChatError (withStore (`getUser` userId)) >>= \case - Left _ -> throwChatError CEUserUnknown - Right user -> pure user - validateUserPassword :: User -> User -> Maybe UserPwd -> m () - validateUserPassword = validateUserPassword_ . Just - validateUserPassword_ :: Maybe User -> User -> Maybe UserPwd -> m () - validateUserPassword_ user_ User {userId = userId', viewPwdHash} viewPwd_ = - forM_ viewPwdHash $ \pwdHash -> - let userId_ = (\User {userId} -> userId) <$> user_ - pwdOk = case viewPwd_ of - Nothing -> userId_ == Just userId' - Just (UserPwd viewPwd) -> validPassword viewPwd pwdHash - in unless pwdOk $ throwChatError CEUserUnknown - validPassword :: Text -> UserPwdHash -> Bool - validPassword pwd UserPwdHash {hash = B64UrlByteString hash, salt = B64UrlByteString salt} = - hash == C.sha512Hash (encodeUtf8 pwd <> salt) - setUserNotifications :: UserId -> Bool -> m ChatResponse - setUserNotifications userId' showNtfs = withUser $ \user -> do - user' <- privateGetUser userId' - case viewPwdHash user' of - Just _ -> throwChatError $ CEHiddenUserAlwaysMuted userId' - _ -> setUserPrivacy user user' {showNtfs} - setUserPrivacy :: User -> User -> m ChatResponse - setUserPrivacy user@User {userId} user'@User {userId = userId'} - | userId == userId' = do - asks currentUser >>= atomically . (`writeTVar` Just user') - withStore' (`updateUserPrivacy` user') - pure $ CRUserPrivacy {user = user', updatedUser = user'} - | otherwise = do - withStore' (`updateUserPrivacy` user') - pure $ CRUserPrivacy {user, updatedUser = user'} - checkDeleteChatUser :: User -> m () - checkDeleteChatUser user@User {userId} = do - users <- withStore' getUsers - let otherVisible = filter (\User {userId = userId', viewPwdHash} -> userId /= userId' && isNothing viewPwdHash) users - when (activeUser user && length otherVisible > 0) $ throwChatError (CECantDeleteActiveUser userId) - deleteChatUser :: User -> Bool -> m ChatResponse - deleteChatUser user delSMPQueues = do - filesInfo <- withStore' (`getUserFileInfo` user) - forM_ filesInfo $ \fileInfo -> deleteFile user fileInfo - withAgent $ \a -> deleteUser a (aUserId user) delSMPQueues - withStore' (`deleteUserRecord` user) - when (activeUser user) $ chatWriteVar currentUser Nothing - ok_ - updateChatSettings :: ChatName -> (ChatSettings -> ChatSettings) -> m ChatResponse - updateChatSettings (ChatName cType name) updateSettings = withUser $ \user -> do - (chatId, chatSettings) <- case cType of - CTDirect -> withStore $ \db -> do - ctId <- getContactIdByName db user name - Contact {chatSettings} <- getContact db user ctId - pure (ctId, chatSettings) - CTGroup -> - withStore $ \db -> do - gId <- getGroupIdByName db user name - GroupInfo {chatSettings} <- getGroupInfo db vr user gId - pure (gId, chatSettings) - _ -> throwChatError $ CECommandError "not supported" - processChatCommand $ APISetChatSettings (ChatRef cType chatId) $ updateSettings chatSettings - connectPlan :: User -> AConnectionRequestUri -> m ConnectionPlan - connectPlan user (ACR SCMInvitation cReq) = do - withStore' (\db -> getConnectionEntityByConnReq db vr user cReqSchemas) >>= \case - Nothing -> pure $ CPInvitationLink ILPOk - Just (RcvDirectMsgConnection conn ct_) -> do - let Connection {connStatus, contactConnInitiated} = conn - if - | connStatus == ConnNew && contactConnInitiated -> - pure $ CPInvitationLink ILPOwnLink - | not (connReady conn) -> - pure $ CPInvitationLink (ILPConnecting ct_) - | otherwise -> case ct_ of - Just ct -> pure $ CPInvitationLink (ILPKnown ct) - Nothing -> throwChatError $ CEInternalError "ready RcvDirectMsgConnection connection should have associated contact" - Just _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" - where - cReqSchemas :: (ConnReqInvitation, ConnReqInvitation) - cReqSchemas = case cReq of - (CRInvitationUri crData e2e) -> - ( CRInvitationUri crData {crScheme = CRSSimplex} e2e, - CRInvitationUri crData {crScheme = simplexChat} e2e - ) - connectPlan user (ACR SCMContact cReq) = do - let CRContactUri ConnReqUriData {crClientData} = cReq - groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli - case groupLinkId of - -- contact address - Nothing -> - withStore' (\db -> getUserContactLinkByConnReq db user cReqSchemas) >>= \case - Just _ -> pure $ CPContactAddress CAPOwnLink - Nothing -> - withStore' (\db -> getContactConnEntityByConnReqHash db vr user cReqHashes) >>= \case - Nothing -> - withStore' (\db -> getContactWithoutConnViaAddress db user cReqSchemas) >>= \case - Nothing -> pure $ CPContactAddress CAPOk - Just ct -> pure $ CPContactAddress (CAPContactViaAddress ct) - Just (RcvDirectMsgConnection _conn Nothing) -> pure $ CPContactAddress CAPConnectingConfirmReconnect - Just (RcvDirectMsgConnection _ (Just ct)) - | not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnectingProhibit ct) - | contactDeleted ct -> pure $ CPContactAddress CAPOk - | otherwise -> pure $ CPContactAddress (CAPKnown ct) - Just _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" - -- group link - Just _ -> - withStore' (\db -> getGroupInfoByUserContactLinkConnReq db vr user cReqSchemas) >>= \case - Just g -> pure $ CPGroupLink (GLPOwnLink g) - Nothing -> do - connEnt_ <- withStore' $ \db -> getContactConnEntityByConnReqHash db vr user cReqHashes - gInfo_ <- withStore' $ \db -> getGroupInfoByGroupLinkHash db vr user cReqHashes - case (gInfo_, connEnt_) of - (Nothing, Nothing) -> pure $ CPGroupLink GLPOk - (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 - (Nothing, Just _) -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" - (Just gInfo@GroupInfo {membership}, _) - | not (memberActive membership) && not (memberRemoved membership) -> - pure $ CPGroupLink (GLPConnectingProhibit gInfo_) - | memberActive membership -> pure $ CPGroupLink (GLPKnown gInfo) - | otherwise -> pure $ CPGroupLink GLPOk - where - cReqSchemas :: (ConnReqContact, ConnReqContact) - cReqSchemas = case cReq of - (CRContactUri crData) -> - ( CRContactUri crData {crScheme = CRSSimplex}, - CRContactUri crData {crScheme = simplexChat} - ) - cReqHashes :: (ConnReqUriHash, ConnReqUriHash) - cReqHashes = bimap hash hash cReqSchemas - hash = ConnReqUriHash . C.sha256Hash . strEncode - -prepareGroupMsg :: forall m. ChatMonad m => User -> GroupInfo -> MsgContent -> Maybe ChatItemId -> Maybe FileInvitation -> Maybe CITimed -> Bool -> m (MsgContainer, Maybe (CIQuote 'CTGroup)) -prepareGroupMsg user GroupInfo {groupId, membership} mc quotedItemId_ fInv_ timed_ live = case quotedItemId_ of - Nothing -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) - Just quotedItemId -> do - CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <- - withStore $ \db -> getGroupChatItem db user groupId quotedItemId - (origQmc, qd, sent, GroupMember {memberId}) <- quoteData qci membership - let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Just memberId} - qmc = quoteContent mc origQmc file - quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText} - pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem) - where - quoteData :: ChatItem c d -> GroupMember -> m (MsgContent, CIQDirection 'CTGroup, Bool, GroupMember) - quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} _ = throwChatError CEInvalidQuote - quoteData ChatItem {chatDir = CIGroupSnd, content = CISndMsgContent qmc} membership' = pure (qmc, CIQGroupSnd, True, membership') - quoteData ChatItem {chatDir = CIGroupRcv m, content = CIRcvMsgContent qmc} _ = pure (qmc, CIQGroupRcv $ Just m, False, m) - quoteData _ _ = throwChatError CEInvalidQuote - -quoteContent :: forall d. MsgContent -> MsgContent -> Maybe (CIFile d) -> MsgContent -quoteContent mc qmc ciFile_ - | replaceContent = MCText qTextOrFile - | otherwise = case qmc of - MCImage _ image -> MCImage qTextOrFile image - MCFile _ -> MCFile qTextOrFile - -- consider same for voice messages - -- MCVoice _ voice -> MCVoice qTextOrFile voice - _ -> qmc - where - -- if the message we're quoting with is one of the "large" MsgContents - -- we replace the quote's content with MCText - replaceContent = case mc of - MCText _ -> False - MCFile _ -> False - MCLink {} -> True - MCImage {} -> True - MCVideo {} -> True - MCVoice {} -> False - MCUnknown {} -> True - qText = msgContentText qmc - getFileName :: CIFile d -> String - getFileName CIFile {fileName} = fileName - qFileName = maybe qText (T.pack . getFileName) ciFile_ - qTextOrFile = if T.null qText then qFileName else qText - -assertDirectAllowed :: ChatMonad m => User -> MsgDirection -> Contact -> CMEventTag e -> m () -assertDirectAllowed user dir ct event = - unless (allowedChatEvent || anyDirectOrUsed ct) . unlessM directMessagesAllowed $ - throwChatError (CEDirectMessagesProhibited dir ct) - where - directMessagesAllowed = any (groupFeatureAllowed' SGFDirectMessages) <$> withStore' (\db -> getContactGroupPreferences db user ct) - allowedChatEvent = case event of - XMsgNew_ -> False - XMsgUpdate_ -> False - XMsgDel_ -> False - XFile_ -> False - XGrpInv_ -> False - XCallInv_ -> False - _ -> True - -roundedFDCount :: Int -> Int -roundedFDCount n - | n <= 0 = 4 - | otherwise = max 4 $ fromIntegral $ (2 :: Integer) ^ (ceiling (logBase 2 (fromIntegral n) :: Double) :: Integer) - -startExpireCIThread :: forall m. ChatMonad' m => User -> m () -startExpireCIThread user@User {userId} = do - expireThreads <- asks expireCIThreads - atomically (TM.lookup userId expireThreads) >>= \case - Nothing -> do - a <- Just <$> async (void $ runExceptT runExpireCIs) - atomically $ TM.insert userId a expireThreads - _ -> pure () - where - runExpireCIs = do - delay <- asks (initialCleanupManagerDelay . config) - liftIO $ threadDelay' delay - interval <- asks $ ciExpirationInterval . config - forever $ do - flip catchChatError (toView . CRChatError (Just user)) $ do - expireFlags <- asks expireCIFlags - atomically $ TM.lookup userId expireFlags >>= \b -> unless (b == Just True) retry - ttl <- withStoreCtx' (Just "startExpireCIThread, getChatItemTTL") (`getChatItemTTL` user) - forM_ ttl $ \t -> expireChatItems user t False - liftIO $ threadDelay' interval - -setExpireCIFlag :: ChatMonad' m => User -> Bool -> m () -setExpireCIFlag User {userId} b = do - expireFlags <- asks expireCIFlags - atomically $ TM.insert userId b expireFlags - -setAllExpireCIFlags :: ChatMonad' m => Bool -> m () -setAllExpireCIFlags b = do - expireFlags <- asks expireCIFlags - atomically $ do - keys <- M.keys <$> readTVar expireFlags - forM_ keys $ \k -> TM.insert k b expireFlags - -deleteFilesAndConns :: ChatMonad m => User -> [CIFileInfo] -> m () -deleteFilesAndConns user filesInfo = do - connIds <- mapM (deleteFile user) filesInfo - deleteAgentConnectionsAsync user $ concat connIds - -deleteFile :: ChatMonad m => User -> CIFileInfo -> m [ConnId] -deleteFile user fileInfo = deleteFile' user fileInfo False - -deleteFile' :: forall m. ChatMonad m => User -> CIFileInfo -> Bool -> m [ConnId] -deleteFile' user ciFileInfo@CIFileInfo {filePath} sendCancel = do - aConnIds <- cancelFile' user ciFileInfo sendCancel - delete `catchChatError` (toView . CRChatError (Just user)) - pure aConnIds - where - delete :: m () - delete = withFilesFolder $ \filesFolder -> - liftIO . forM_ filePath $ \fPath -> do - let fsFilePath = filesFolder </> fPath - removeFile fsFilePath `catchAll` \_ -> - removePathForcibly fsFilePath `catchAll_` pure () - -- perform an action only if filesFolder is set (i.e. on mobile devices) - withFilesFolder :: (FilePath -> m ()) -> m () - withFilesFolder action = asks filesFolder >>= readTVarIO >>= mapM_ action - -cancelFile' :: forall m. ChatMonad m => User -> CIFileInfo -> Bool -> m [ConnId] -cancelFile' user CIFileInfo {fileId, fileStatus} sendCancel = - case fileStatus of - Just fStatus -> cancel' fStatus `catchChatError` (\e -> toView (CRChatError (Just user) e) $> []) - Nothing -> pure [] - where - cancel' :: ACIFileStatus -> m [ConnId] - cancel' (AFS dir status) = - if ciFileEnded status - then pure [] - else case dir of - SMDSnd -> do - (ftm@FileTransferMeta {cancelled}, fts) <- withStore (\db -> getSndFileTransfer db user fileId) - if cancelled then pure [] else cancelSndFile user ftm fts sendCancel - SMDRcv -> do - ft@RcvFileTransfer {cancelled} <- withStore (\db -> getRcvFileTransfer db user fileId) - if cancelled then pure [] else maybeToList <$> cancelRcvFileTransfer user ft - -updateCallItemStatus :: ChatMonad m => User -> Contact -> Call -> WebRTCCallStatus -> Maybe MessageId -> m () -updateCallItemStatus user ct Call {chatItemId} receivedStatus msgId_ = do - aciContent_ <- callStatusItemContent user ct chatItemId receivedStatus - forM_ aciContent_ $ \aciContent -> updateDirectChatItemView user ct chatItemId aciContent False msgId_ - -updateDirectChatItemView :: ChatMonad m => User -> Contact -> ChatItemId -> ACIContent -> Bool -> Maybe MessageId -> m () -updateDirectChatItemView user ct chatItemId (ACIContent msgDir ciContent) live msgId_ = do - ci' <- withStore $ \db -> updateDirectChatItem db user ct chatItemId ciContent live msgId_ - toView $ CRChatItemUpdated user (AChatItem SCTDirect msgDir (DirectChat ct) ci') - -callStatusItemContent :: ChatMonad m => User -> Contact -> ChatItemId -> WebRTCCallStatus -> m (Maybe ACIContent) -callStatusItemContent user Contact {contactId} chatItemId receivedStatus = do - CChatItem msgDir ChatItem {meta = CIMeta {updatedAt}, content} <- - withStore $ \db -> getDirectChatItem db user contactId chatItemId - ts <- liftIO getCurrentTime - let callDuration :: Int = nominalDiffTimeToSeconds (ts `diffUTCTime` updatedAt) `div'` 1 - callStatus = case content of - CISndCall st _ -> Just st - CIRcvCall st _ -> Just st - _ -> Nothing - newState_ = case (callStatus, receivedStatus) of - (Just CISCallProgress, WCSConnected) -> Nothing -- if call in-progress received connected -> no change - (Just CISCallProgress, WCSDisconnected) -> Just (CISCallEnded, callDuration) -- calculate in-progress duration - (Just CISCallProgress, WCSFailed) -> Just (CISCallEnded, callDuration) -- whether call disconnected or failed - (Just CISCallPending, WCSDisconnected) -> Just (CISCallMissed, 0) - (Just CISCallEnded, _) -> Nothing -- if call already ended or failed -> no change - (Just CISCallError, _) -> Nothing - (Just _, WCSConnecting) -> Just (CISCallNegotiated, 0) - (Just _, WCSConnected) -> Just (CISCallProgress, 0) -- if call ended that was never connected, duration = 0 - (Just _, WCSDisconnected) -> Just (CISCallEnded, 0) - (Just _, WCSFailed) -> Just (CISCallError, 0) - (Nothing, _) -> Nothing -- some other content - we should never get here, but no exception is thrown - pure $ aciContent msgDir <$> newState_ - where - aciContent :: forall d. SMsgDirection d -> (CICallStatus, Int) -> ACIContent - aciContent msgDir (callStatus', duration) = case msgDir of - SMDSnd -> ACIContent SMDSnd $ CISndCall callStatus' duration - SMDRcv -> ACIContent SMDRcv $ CIRcvCall callStatus' duration - --- mobile clients use file paths relative to app directory (e.g. for the reason ios app directory changes on updates), --- so we have to differentiate between the file path stored in db and communicated with frontend, and the file path --- used during file transfer for actual operations with file system -toFSFilePath :: ChatMonad' m => FilePath -> m FilePath -toFSFilePath f = - maybe f (</> f) <$> (readTVarIO =<< asks filesFolder) - -setFileToEncrypt :: ChatMonad m => RcvFileTransfer -> m RcvFileTransfer -setFileToEncrypt ft@RcvFileTransfer {fileId} = do - cfArgs <- atomically . CF.randomArgs =<< asks random - withStore' $ \db -> setFileCryptoArgs db fileId cfArgs - pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs} - -receiveFile' :: ChatMonad m => User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> m ChatResponse -receiveFile' user ft rcvInline_ filePath_ = do - (CRRcvFileAccepted user <$> acceptFileReceive user ft rcvInline_ filePath_) `catchChatError` processError - where - processError = \case - -- TODO AChatItem in Cancelled events - ChatErrorAgent (SMP SMP.AUTH) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft - ChatErrorAgent (CONN DUPLICATE) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft - e -> throwError e - -acceptFileReceive :: forall m. ChatMonad m => User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> m AChatItem -acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId, cryptoArgs} rcvInline_ filePath_ = do - unless (fileStatus == RFSNew) $ case fileStatus of - RFSCancelled _ -> throwChatError $ CEFileCancelled fName - _ -> throwChatError $ CEFileAlreadyReceiving fName - vr <- chatVersionRange - case (xftpRcvFile, fileConnReq) of - -- direct file protocol - (Nothing, Just connReq) -> do - subMode <- chatReadVar subscriptionMode - dm <- directMessage $ XFileAcpt fName - connIds <- joinAgentConnectionAsync user True connReq dm subMode - filePath <- getRcvFilePath fileId filePath_ fName True - withStoreCtx (Just "acceptFileReceive, acceptRcvFileTransfer") $ \db -> acceptRcvFileTransfer db vr user fileId connIds ConnJoined filePath subMode - -- XFTP - (Just XFTPRcvFile {}, _) -> do - filePath <- getRcvFilePath fileId filePath_ fName False - (ci, rfd) <- withStoreCtx (Just "acceptFileReceive, xftpAcceptRcvFT ...") $ \db -> do - -- marking file as accepted and reading description in the same transaction - -- to prevent race condition with appending description - ci <- xftpAcceptRcvFT db vr user fileId filePath - rfd <- getRcvFileDescrByRcvFileId db fileId - pure (ci, rfd) - receiveViaCompleteFD user fileId rfd cryptoArgs - pure ci - -- group & direct file protocol - _ -> do - chatRef <- withStoreCtx (Just "acceptFileReceive, getChatRefByFileId") $ \db -> getChatRefByFileId db user fileId - case (chatRef, grpMemberId) of - (ChatRef CTDirect contactId, Nothing) -> do - ct <- withStoreCtx (Just "acceptFileReceive, getContact") $ \db -> getContact db user contactId - acceptFile CFCreateConnFileInvDirect $ \msg -> void $ sendDirectContactMessage ct msg - (ChatRef CTGroup groupId, Just memId) -> do - GroupMember {activeConn} <- withStoreCtx (Just "acceptFileReceive, getGroupMember") $ \db -> getGroupMember db user groupId memId - case activeConn of - Just conn -> do - acceptFile CFCreateConnFileInvGroup $ \msg -> void $ sendDirectMessage conn msg $ GroupId groupId - _ -> throwChatError $ CEFileInternal "member connection not active" - _ -> throwChatError $ CEFileInternal "invalid chat ref for file transfer" - where - acceptFile :: CommandFunction -> (ChatMsgEvent 'Json -> m ()) -> m AChatItem - acceptFile cmdFunction send = do - filePath <- getRcvFilePath fileId filePath_ fName True - inline <- receiveInline - vr <- chatVersionRange - if - | inline -> do - -- accepting inline - ci <- withStoreCtx (Just "acceptFile, acceptRcvInlineFT") $ \db -> acceptRcvInlineFT db vr user fileId filePath - sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId - send $ XFileAcptInv sharedMsgId Nothing fName - pure ci - | fileInline == Just IFMSent -> throwChatError $ CEFileAlreadyReceiving fName - | otherwise -> do - -- accepting via a new connection - subMode <- chatReadVar subscriptionMode - connIds <- createAgentConnectionAsync user cmdFunction True SCMInvitation subMode - withStoreCtx (Just "acceptFile, acceptRcvFileTransfer") $ \db -> acceptRcvFileTransfer db vr user fileId connIds ConnNew filePath subMode - receiveInline :: m Bool - receiveInline = do - ChatConfig {fileChunkSize, inlineFiles = InlineFilesConfig {receiveChunks, offerChunks}} <- asks config - pure $ - rcvInline_ /= Just False - && fileInline == Just IFMOffer - && ( fileSize <= fileChunkSize * receiveChunks - || (rcvInline_ == Just True && fileSize <= fileChunkSize * offerChunks) - ) - -receiveViaCompleteFD :: ChatMonad m => User -> FileTransferId -> RcvFileDescr -> Maybe CryptoFileArgs -> m () -receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} cfArgs = - when fileDescrComplete $ do - rd <- parseFileDescription fileDescrText - aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd cfArgs - startReceivingFile user fileId - withStoreCtx' (Just "receiveViaCompleteFD, updateRcvFileAgentId") $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId) - -startReceivingFile :: ChatMonad m => User -> FileTransferId -> m () -startReceivingFile user fileId = do - vr <- chatVersionRange - ci <- withStoreCtx (Just "startReceivingFile, updateRcvFileStatus ...") $ \db -> do - liftIO $ updateRcvFileStatus db fileId FSConnected - liftIO $ updateCIFileStatus db user fileId $ CIFSRcvTransfer 0 1 - getChatItemByFileId db vr user fileId - toView $ CRRcvFileStart user ci - -getRcvFilePath :: forall m. ChatMonad m => FileTransferId -> Maybe FilePath -> String -> Bool -> m FilePath -getRcvFilePath fileId fPath_ fn keepHandle = case fPath_ of - Nothing -> - chatReadVar filesFolder >>= \case - Nothing -> - getDefaultFilesFolder - >>= (`uniqueCombine` fn) - >>= createEmptyFile - Just filesFolder -> - filesFolder `uniqueCombine` fn - >>= createEmptyFile - >>= pure <$> takeFileName - Just fPath -> - ifM - (doesDirectoryExist fPath) - (fPath `uniqueCombine` fn >>= createEmptyFile) - $ ifM - (doesFileExist fPath) - (throwChatError $ CEFileAlreadyExists fPath) - (createEmptyFile fPath) - where - createEmptyFile :: FilePath -> m FilePath - createEmptyFile fPath = emptyFile fPath `catchThrow` (ChatError . CEFileWrite fPath . show) - emptyFile :: FilePath -> m FilePath - emptyFile fPath = do - h <- - if keepHandle - then getFileHandle fileId fPath rcvFiles AppendMode - else getTmpHandle fPath - liftIO $ B.hPut h "" >> hFlush h - pure fPath - getTmpHandle :: FilePath -> m Handle - getTmpHandle fPath = openFile fPath AppendMode `catchThrow` (ChatError . CEFileInternal . show) - -acceptContactRequest :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> Bool -> m Contact -acceptContactRequest user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = cp, userContactLinkId, xContactId} incognitoProfile contactUsed = do - subMode <- chatReadVar subscriptionMode - let profileToSend = profileToSendOnAccept user incognitoProfile - dm <- directMessage $ XInfo profileToSend - acId <- withAgent $ \a -> acceptContact a True invId dm subMode - withStore' $ \db -> createAcceptedContact db user acId (fromJVersionRange cReqChatVRange) cName profileId cp userContactLinkId xContactId incognitoProfile subMode contactUsed - -acceptContactRequestAsync :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> Bool -> m Contact -acceptContactRequestAsync user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile contactUsed = do - subMode <- chatReadVar subscriptionMode - let profileToSend = profileToSendOnAccept user incognitoProfile - (cmdId, acId) <- agentAcceptContactAsync user True invId (XInfo profileToSend) subMode - withStore' $ \db -> do - ct@Contact {activeConn} <- createAcceptedContact db user acId (fromJVersionRange cReqChatVRange) cName profileId p userContactLinkId xContactId incognitoProfile subMode contactUsed - forM_ activeConn $ \Connection {connId} -> setCommandConnId db user cmdId connId - pure ct - -acceptGroupJoinRequestAsync :: ChatMonad m => User -> GroupInfo -> UserContactRequest -> GroupMemberRole -> Maybe IncognitoProfile -> m GroupMember -acceptGroupJoinRequestAsync - user - gInfo@GroupInfo {groupProfile, membership} - ucr@UserContactRequest {agentInvitationId = AgentInvId invId} - gLinkMemRole - incognitoProfile = do - gVar <- asks random - (groupMemberId, memberId) <- withStore $ \db -> createAcceptedMember db gVar user gInfo ucr gLinkMemRole - let Profile {displayName} = profileToSendOnAccept user incognitoProfile - GroupMember {memberRole = userRole, memberId = userMemberId} = membership - msg = XGrpLinkInv $ GroupLinkInvitation (MemberIdRole userMemberId userRole) displayName (MemberIdRole memberId gLinkMemRole) groupProfile - subMode <- chatReadVar subscriptionMode - connIds <- agentAcceptContactAsync user True invId msg subMode - withStore $ \db -> do - liftIO $ createAcceptedMemberConnection db user connIds ucr groupMemberId subMode - getGroupMemberById db user groupMemberId - -profileToSendOnAccept :: User -> Maybe IncognitoProfile -> Profile -profileToSendOnAccept user ip = userProfileToSend user (getIncognitoProfile <$> ip) Nothing - where - getIncognitoProfile = \case - NewIncognito p -> p - ExistingIncognito lp -> fromLocalProfile lp - -deleteGroupLink' :: ChatMonad m => User -> GroupInfo -> m () -deleteGroupLink' user gInfo = do - conn <- withStore $ \db -> getGroupLinkConnection db user gInfo - deleteGroupLink_ user gInfo conn - -deleteGroupLinkIfExists :: ChatMonad m => User -> GroupInfo -> m () -deleteGroupLinkIfExists user gInfo = do - conn_ <- eitherToMaybe <$> withStore' (\db -> runExceptT $ getGroupLinkConnection db user gInfo) - mapM_ (deleteGroupLink_ user gInfo) conn_ - -deleteGroupLink_ :: ChatMonad m => User -> GroupInfo -> Connection -> m () -deleteGroupLink_ user gInfo conn = do - deleteAgentConnectionAsync user $ aConnId conn - withStore' $ \db -> deleteGroupLink db user gInfo - -agentSubscriber :: forall m. (MonadUnliftIO m, MonadReader ChatController m) => m () -agentSubscriber = do - q <- asks $ subQ . smpAgent - l <- asks chatLock - forever $ atomically (readTBQueue q) >>= void . process l - where - process :: Lock -> (ACorrId, EntityId, APartyCmd 'Agent) -> m (Either ChatError ()) - process l (corrId, entId, APC e msg) = run $ case e of - SAENone -> processAgentMessageNoConn msg - SAEConn -> processAgentMessage corrId entId msg - SAERcvFile -> processAgentMsgRcvFile corrId entId msg - SAESndFile -> processAgentMsgSndFile corrId entId msg - where - run action = do - let name = "agentSubscriber entity=" <> show e <> " entId=" <> str entId <> " msg=" <> str (aCommandTag msg) - withLock l name $ runExceptT $ action `catchChatError` (toView . CRChatError Nothing) - str :: StrEncoding a => a -> String - str = B.unpack . strEncode - -type AgentBatchSubscribe m = AgentClient -> [ConnId] -> ExceptT AgentErrorType m (Map ConnId (Either AgentErrorType ())) - -subscribeUserConnections :: forall m. ChatMonad m => VersionRange -> Bool -> AgentBatchSubscribe m -> User -> m () -subscribeUserConnections vr onlyNeeded agentBatchSubscribe user@User {userId} = do - -- get user connections - ce <- asks $ subscriptionEvents . config - (conns, cts, ucs, gs, ms, sfts, rfts, pcs) <- - if onlyNeeded - then do - (conns, entities) <- withStore' (`getConnectionsToSubscribe` vr) - let (cts, ucs, ms, sfts, rfts, pcs) = foldl' addEntity (M.empty, M.empty, M.empty, M.empty, M.empty, M.empty) entities - pure (conns, cts, ucs, [], ms, sfts, rfts, pcs) - else do - withStore' unsetConnectionToSubscribe - (ctConns, cts) <- getContactConns - (ucConns, ucs) <- getUserContactLinkConns - (gs, mConns, ms) <- getGroupMemberConns - (sftConns, sfts) <- getSndFileTransferConns - (rftConns, rfts) <- getRcvFileTransferConns - (pcConns, pcs) <- getPendingContactConns - let conns = concat [ctConns, ucConns, mConns, sftConns, rftConns, pcConns] - pure (conns, cts, ucs, gs, ms, sfts, rfts, pcs) - -- subscribe using batched commands - rs <- withAgent $ \a -> agentBatchSubscribe a conns - -- send connection events to view - contactSubsToView rs cts ce - -- TODO possibly, we could either disable these events or replace with less noisy for API - contactLinkSubsToView rs ucs - groupSubsToView rs gs ms ce - sndFileSubsToView rs sfts - rcvFileSubsToView rs rfts - pendingConnSubsToView rs pcs - where - addEntity (cts, ucs, ms, sfts, rfts, pcs) = \case - RcvDirectMsgConnection c (Just ct) -> let cts' = addConn c ct cts in (cts', ucs, ms, sfts, rfts, pcs) - RcvDirectMsgConnection c Nothing -> let pcs' = addConn c (toPCC c) pcs in (cts, ucs, ms, sfts, rfts, pcs') - RcvGroupMsgConnection c _g m -> let ms' = addConn c m ms in (cts, ucs, ms', sfts, rfts, pcs) - SndFileConnection c sft -> let sfts' = addConn c sft sfts in (cts, ucs, ms, sfts', rfts, pcs) - RcvFileConnection c rft -> let rfts' = addConn c rft rfts in (cts, ucs, ms, sfts, rfts', pcs) - UserContactConnection c uc -> let ucs' = addConn c uc ucs in (cts, ucs', ms, sfts, rfts, pcs) - addConn :: Connection -> a -> Map ConnId a -> Map ConnId a - addConn = M.insert . aConnId - toPCC Connection {connId, agentConnId, connStatus, viaUserContactLink, groupLinkId, customUserProfileId, localAlias, createdAt} = - PendingContactConnection - { pccConnId = connId, - pccAgentConnId = agentConnId, - pccConnStatus = connStatus, - viaContactUri = False, - viaUserContactLink, - groupLinkId, - customUserProfileId, - connReqInv = Nothing, - localAlias, - createdAt, - updatedAt = createdAt - } - getContactConns :: m ([ConnId], Map ConnId Contact) - getContactConns = do - cts <- withStore_ ("subscribeUserConnections " <> show userId <> ", getUserContacts") getUserContacts - let cts' = mapMaybe (\ct -> (,ct) <$> contactConnId ct) $ filter contactActive cts - pure (map fst cts', M.fromList cts') - getUserContactLinkConns :: m ([ConnId], Map ConnId UserContact) - getUserContactLinkConns = do - (cs, ucs) <- unzip <$> withStore_ ("subscribeUserConnections " <> show userId <> ", getUserContactLinks") getUserContactLinks - let connIds = map aConnId cs - pure (connIds, M.fromList $ zip connIds ucs) - getGroupMemberConns :: m ([Group], [ConnId], Map ConnId GroupMember) - getGroupMemberConns = do - gs <- withStore_ ("subscribeUserConnections " <> show userId <> ", getUserGroups") (`getUserGroups` vr) - let mPairs = concatMap (\(Group _ ms) -> mapMaybe (\m -> (,m) <$> memberConnId m) (filter (not . memberRemoved) ms)) gs - pure (gs, map fst mPairs, M.fromList mPairs) - getSndFileTransferConns :: m ([ConnId], Map ConnId SndFileTransfer) - getSndFileTransferConns = do - sfts <- withStore_ ("subscribeUserConnections " <> show userId <> ", getLiveSndFileTransfers") getLiveSndFileTransfers - let connIds = map sndFileTransferConnId sfts - pure (connIds, M.fromList $ zip connIds sfts) - getRcvFileTransferConns :: m ([ConnId], Map ConnId RcvFileTransfer) - getRcvFileTransferConns = do - rfts <- withStore_ ("subscribeUserConnections " <> show userId <> ", getLiveRcvFileTransfers") getLiveRcvFileTransfers - let rftPairs = mapMaybe (\ft -> (,ft) <$> liveRcvFileTransferConnId ft) rfts - pure (map fst rftPairs, M.fromList rftPairs) - getPendingContactConns :: m ([ConnId], Map ConnId PendingContactConnection) - getPendingContactConns = do - pcs <- withStore_ ("subscribeUserConnections " <> show userId <> ", getPendingContactConnections") getPendingContactConnections - let connIds = map aConnId' pcs - pure (connIds, M.fromList $ zip connIds pcs) - contactSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId Contact -> Bool -> m () - contactSubsToView rs cts ce = do - chatModifyVar connNetworkStatuses $ M.union (M.fromList statuses) - ifM (asks $ coreApi . config) (notifyAPI statuses) notifyCLI - where - notifyCLI = do - let cRs = resultsFor rs cts - cErrors = sortOn (\(Contact {localDisplayName = n}, _) -> n) $ filterErrors cRs - toView . CRContactSubSummary user $ map (uncurry ContactSubStatus) cRs - when ce $ mapM_ (toView . uncurry (CRContactSubError user)) cErrors - notifyAPI = toView . CRNetworkStatuses (Just user) . map (uncurry ConnNetworkStatus) - statuses = M.foldrWithKey' addStatus [] cts - where - addStatus :: ConnId -> Contact -> [(AgentConnId, NetworkStatus)] -> [(AgentConnId, NetworkStatus)] - addStatus _ Contact {activeConn = Nothing} nss = nss - addStatus connId Contact {activeConn = Just Connection {agentConnId}} nss = - let ns = (agentConnId, netStatus $ resultErr connId rs) - in ns : nss - netStatus :: Maybe ChatError -> NetworkStatus - netStatus = maybe NSConnected $ NSError . errorNetworkStatus - errorNetworkStatus :: ChatError -> String - errorNetworkStatus = \case - ChatErrorAgent (BROKER _ NETWORK) _ -> "network" - ChatErrorAgent (SMP SMP.AUTH) _ -> "contact deleted" - e -> show e - -- TODO possibly below could be replaced with less noisy events for API - contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> m () - contactLinkSubsToView rs = toView . CRUserContactSubSummary user . map (uncurry UserContactSubStatus) . resultsFor rs - groupSubsToView :: Map ConnId (Either AgentErrorType ()) -> [Group] -> Map ConnId GroupMember -> Bool -> m () - groupSubsToView rs gs ms ce = do - mapM_ groupSub $ - sortOn (\(Group GroupInfo {localDisplayName = g} _) -> g) gs - toView . CRMemberSubSummary user $ map (uncurry MemberSubStatus) mRs - where - mRs = resultsFor rs ms - groupSub :: Group -> m () - groupSub (Group g@GroupInfo {membership, groupId = gId} members) = do - when ce $ mapM_ (toView . uncurry (CRMemberSubError user g)) mErrors - toView groupEvent - where - mErrors :: [(GroupMember, ChatError)] - mErrors = - sortOn (\(GroupMember {localDisplayName = n}, _) -> n) - . filterErrors - $ filter (\(GroupMember {groupId}, _) -> groupId == gId) mRs - groupEvent :: ChatResponse - groupEvent - | memberStatus membership == GSMemInvited = CRGroupInvitation user g - | all (\GroupMember {activeConn} -> isNothing activeConn) members = - if memberActive membership - then CRGroupEmpty user g - else CRGroupRemoved user g - | otherwise = CRGroupSubscribed user g - sndFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId SndFileTransfer -> m () - sndFileSubsToView rs sfts = do - let sftRs = resultsFor rs sfts - forM_ sftRs $ \(ft@SndFileTransfer {fileId, fileStatus}, err_) -> do - forM_ err_ $ toView . CRSndFileSubError user ft - void . forkIO $ do - threadDelay 1000000 - l <- asks chatLock - when (fileStatus == FSConnected) . unlessM (isFileActive fileId sndFiles) . withLock l "subscribe sendFileChunk" $ - sendFileChunk user ft - rcvFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId RcvFileTransfer -> m () - rcvFileSubsToView rs = mapM_ (toView . uncurry (CRRcvFileSubError user)) . filterErrors . resultsFor rs - pendingConnSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId PendingContactConnection -> m () - pendingConnSubsToView rs = toView . CRPendingSubSummary user . map (uncurry PendingSubStatus) . resultsFor rs - withStore_ :: String -> (DB.Connection -> User -> IO [a]) -> m [a] - withStore_ ctx a = withStoreCtx' (Just ctx) (`a` user) `catchChatError` \e -> toView (CRChatError (Just user) e) $> [] - filterErrors :: [(a, Maybe ChatError)] -> [(a, ChatError)] - filterErrors = mapMaybe (\(a, e_) -> (a,) <$> e_) - resultsFor :: Map ConnId (Either AgentErrorType ()) -> Map ConnId a -> [(a, Maybe ChatError)] - resultsFor rs = M.foldrWithKey' addResult [] - where - addResult :: ConnId -> a -> [(a, Maybe ChatError)] -> [(a, Maybe ChatError)] - addResult connId = (:) . (,resultErr connId rs) - resultErr :: ConnId -> Map ConnId (Either AgentErrorType ()) -> Maybe ChatError - resultErr connId rs = case M.lookup connId rs of - Just (Left e) -> Just $ ChatErrorAgent e Nothing - Just _ -> Nothing - _ -> Just . ChatError . CEAgentNoSubResult $ AgentConnId connId - -cleanupManager :: forall m. ChatMonad m => m () -cleanupManager = do - interval <- asks (cleanupManagerInterval . config) - runWithoutInitialDelay interval - initialDelay <- asks (initialCleanupManagerDelay . config) - liftIO $ threadDelay' initialDelay - stepDelay <- asks (cleanupManagerStepDelay . config) - forever $ do - flip catchChatError (toView . CRChatError Nothing) $ do - waitChatStarted - users <- withStoreCtx' (Just "cleanupManager, getUsers 1") getUsers - let (us, us') = partition activeUser users - forM_ us $ cleanupUser interval stepDelay - forM_ us' $ cleanupUser interval stepDelay - cleanupMessages `catchChatError` (toView . CRChatError Nothing) - cleanupProbes `catchChatError` (toView . CRChatError Nothing) - liftIO $ threadDelay' $ diffToMicroseconds interval - where - runWithoutInitialDelay cleanupInterval = flip catchChatError (toView . CRChatError Nothing) $ do - waitChatStarted - users <- withStoreCtx' (Just "cleanupManager, getUsers 2") getUsers - let (us, us') = partition activeUser users - forM_ us $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` (toView . CRChatError (Just u)) - forM_ us' $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` (toView . CRChatError (Just u)) - cleanupUser cleanupInterval stepDelay user = do - cleanupTimedItems cleanupInterval user `catchChatError` (toView . CRChatError (Just user)) - liftIO $ threadDelay' stepDelay - cleanupDeletedContacts user `catchChatError` (toView . CRChatError (Just user)) - liftIO $ threadDelay' stepDelay - cleanupTimedItems cleanupInterval user = do - ts <- liftIO getCurrentTime - let startTimedThreadCutoff = addUTCTime cleanupInterval ts - timedItems <- withStoreCtx' (Just "cleanupManager, getTimedItems") $ \db -> getTimedItems db user startTimedThreadCutoff - forM_ timedItems $ \(itemRef, deleteAt) -> startTimedItemThread user itemRef deleteAt `catchChatError` const (pure ()) - cleanupDeletedContacts user = do - contacts <- withStore' (`getDeletedContacts` user) - forM_ contacts $ \ct -> - withStore' (\db -> deleteContactWithoutGroups db user ct) - `catchChatError` (toView . CRChatError (Just user)) - cleanupMessages = do - ts <- liftIO getCurrentTime - let cutoffTs = addUTCTime (-(30 * nominalDay)) ts - withStoreCtx' (Just "cleanupManager, deleteOldMessages") (`deleteOldMessages` cutoffTs) - cleanupProbes = do - ts <- liftIO getCurrentTime - let cutoffTs = addUTCTime (-(14 * nominalDay)) ts - withStore' (`deleteOldProbes` cutoffTs) - -startProximateTimedItemThread :: ChatMonad m => User -> (ChatRef, ChatItemId) -> UTCTime -> m () -startProximateTimedItemThread user itemRef deleteAt = do - interval <- asks (cleanupManagerInterval . config) - ts <- liftIO getCurrentTime - when (diffUTCTime deleteAt ts <= interval) $ - startTimedItemThread user itemRef deleteAt - -startTimedItemThread :: ChatMonad m => User -> (ChatRef, ChatItemId) -> UTCTime -> m () -startTimedItemThread user itemRef deleteAt = do - itemThreads <- asks timedItemThreads - threadTVar_ <- atomically $ do - exists <- TM.member itemRef itemThreads - if not exists - then do - threadTVar <- newTVar Nothing - TM.insert itemRef threadTVar itemThreads - pure $ Just threadTVar - else pure Nothing - forM_ threadTVar_ $ \threadTVar -> do - tId <- mkWeakThreadId =<< deleteTimedItem user itemRef deleteAt `forkFinally` const (atomically $ TM.delete itemRef itemThreads) - atomically $ writeTVar threadTVar (Just tId) - -deleteTimedItem :: ChatMonad m => User -> (ChatRef, ChatItemId) -> UTCTime -> m () -deleteTimedItem user (ChatRef cType chatId, itemId) deleteAt = do - ts <- liftIO getCurrentTime - liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime deleteAt ts - waitChatStarted - vr <- chatVersionRange - case cType of - CTDirect -> do - (ct, CChatItem _ ci) <- withStore $ \db -> (,) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId - deleteDirectCI user ct ci True True >>= toView - CTGroup -> do - (gInfo, CChatItem _ ci) <- withStore $ \db -> (,) <$> getGroupInfo db vr user chatId <*> getGroupChatItem db user chatId itemId - deletedTs <- liftIO getCurrentTime - deleteGroupCI user gInfo ci True True Nothing deletedTs >>= toView - _ -> toView . CRChatError (Just user) . ChatError $ CEInternalError "bad deleteTimedItem cType" - -startUpdatedTimedItemThread :: ChatMonad m => User -> ChatRef -> ChatItem c d -> ChatItem c d -> m () -startUpdatedTimedItemThread user chatRef ci ci' = - case (chatItemTimed ci >>= timedDeleteAt', chatItemTimed ci' >>= timedDeleteAt') of - (Nothing, Just deleteAt') -> - startProximateTimedItemThread user (chatRef, chatItemId' ci') deleteAt' - _ -> pure () - -expireChatItems :: forall m. ChatMonad m => User -> Int64 -> Bool -> m () -expireChatItems user@User {userId} ttl sync = do - currentTs <- liftIO getCurrentTime - vr <- chatVersionRange - let expirationDate = addUTCTime (-1 * fromIntegral ttl) currentTs - -- this is to keep group messages created during last 12 hours even if they're expired according to item_ts - createdAtCutoff = addUTCTime (-43200 :: NominalDiffTime) currentTs - contacts <- withStoreCtx' (Just "expireChatItems, getUserContacts") (`getUserContacts` user) - loop contacts $ processContact expirationDate - groups <- withStoreCtx' (Just "expireChatItems, getUserGroupDetails") (\db -> getUserGroupDetails db vr user Nothing Nothing) - loop groups $ processGroup expirationDate createdAtCutoff - where - loop :: [a] -> (a -> m ()) -> m () - loop [] _ = pure () - loop (a : as) process = continue $ do - process a `catchChatError` (toView . CRChatError (Just user)) - loop as process - continue :: m () -> m () - continue a = - if sync - then a - else do - expireFlags <- asks expireCIFlags - expire <- atomically $ TM.lookup userId expireFlags - when (expire == Just True) $ threadDelay 100000 >> a - processContact :: UTCTime -> Contact -> m () - processContact expirationDate ct = do - filesInfo <- withStoreCtx' (Just "processContact, getContactExpiredFileInfo") $ \db -> getContactExpiredFileInfo db user ct expirationDate - deleteFilesAndConns user filesInfo - withStoreCtx' (Just "processContact, deleteContactExpiredCIs") $ \db -> deleteContactExpiredCIs db user ct expirationDate - processGroup :: UTCTime -> UTCTime -> GroupInfo -> m () - processGroup expirationDate createdAtCutoff gInfo = do - filesInfo <- withStoreCtx' (Just "processGroup, getGroupExpiredFileInfo") $ \db -> getGroupExpiredFileInfo db user gInfo expirationDate createdAtCutoff - deleteFilesAndConns user filesInfo - withStoreCtx' (Just "processGroup, deleteGroupExpiredCIs") $ \db -> deleteGroupExpiredCIs db user gInfo expirationDate createdAtCutoff - membersToDelete <- withStoreCtx' (Just "processGroup, getGroupMembersForExpiration") $ \db -> getGroupMembersForExpiration db user gInfo - forM_ membersToDelete $ \m -> withStoreCtx' (Just "processGroup, deleteGroupMember") $ \db -> deleteGroupMember db user m - -processAgentMessage :: forall m. ChatMonad m => ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> m () -processAgentMessage _ connId (DEL_RCVQ srv qId err_) = - toView $ CRAgentRcvQueueDeleted (AgentConnId connId) srv (AgentQueueId qId) err_ -processAgentMessage _ connId DEL_CONN = - toView $ CRAgentConnDeleted (AgentConnId connId) -processAgentMessage corrId connId msg = do - vr <- chatVersionRange - withStore' (`getUserByAConnId` AgentConnId connId) >>= \case - Just user -> processAgentMessageConn vr user corrId connId msg `catchChatError` (toView . CRChatError (Just user)) - _ -> throwChatError $ CENoConnectionUser (AgentConnId connId) - -processAgentMessageNoConn :: forall m. ChatMonad m => ACommand 'Agent 'AENone -> m () -processAgentMessageNoConn = \case - CONNECT p h -> hostEvent $ CRHostConnected p h - DISCONNECT p h -> hostEvent $ CRHostDisconnected p h - DOWN srv conns -> serverEvent srv conns NSDisconnected CRContactsDisconnected - UP srv conns -> serverEvent srv conns NSConnected CRContactsSubscribed - SUSPENDED -> toView CRChatSuspended - DEL_USER agentUserId -> toView $ CRAgentUserDeleted agentUserId - where - hostEvent :: ChatResponse -> m () - hostEvent = whenM (asks $ hostEvents . config) . toView - serverEvent srv conns nsStatus event = do - chatModifyVar connNetworkStatuses $ \m -> foldl' (\m' cId -> M.insert cId nsStatus m') m connIds - ifM (asks $ coreApi . config) (notifyAPI connIds) notifyCLI - where - connIds = map AgentConnId conns - notifyAPI = toView . CRNetworkStatus nsStatus - notifyCLI = do - cs <- withStore' (`getConnectionsContacts` conns) - toView $ event srv cs - -processAgentMsgSndFile :: forall m. ChatMonad m => ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> m () -processAgentMsgSndFile _corrId aFileId msg = - withStore' (`getUserByASndFileId` AgentSndFileId aFileId) >>= \case - Just user -> process user `catchChatError` (toView . CRChatError (Just user)) - _ -> do - withAgent (`xftpDeleteSndFileInternal` aFileId) - throwChatError $ CENoSndFileUser $ AgentSndFileId aFileId - where - process :: User -> m () - process user = do - (ft@FileTransferMeta {fileId, cancelled}, sfts) <- withStore $ \db -> do - fileId <- getXFTPSndFileDBId db user $ AgentSndFileId aFileId - getSndFileTransfer db user fileId - vr <- chatVersionRange - unless cancelled $ case msg of - SFPROG sndProgress sndTotal -> do - let status = CIFSSndTransfer {sndProgress, sndTotal} - ci <- withStore $ \db -> do - liftIO $ updateCIFileStatus db user fileId status - getChatItemByFileId db vr user fileId - toView $ CRSndFileProgressXFTP user ci ft sndProgress sndTotal - SFDONE sndDescr rfds -> do - withStore' $ \db -> setSndFTPrivateSndDescr db user fileId (fileDescrText sndDescr) - ci@(AChatItem _ d cInfo _ci@ChatItem {meta = CIMeta {itemSharedMsgId = msgId_, itemDeleted}}) <- - withStore $ \db -> getChatItemByFileId db vr user fileId - case (msgId_, itemDeleted) of - (Just sharedMsgId, Nothing) -> do - when (length rfds < length sfts) $ throwChatError $ CEInternalError "not enough XFTP file descriptions to send" - -- TODO either update database status or move to SFPROG - toView $ CRSndFileProgressXFTP user ci ft 1 1 - case (rfds, sfts, d, cInfo) of - (rfd : extraRFDs, sft : _, SMDSnd, DirectChat ct) -> do - withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText extraRFDs) - msgDeliveryId <- sendFileDescription sft rfd sharedMsgId $ sendDirectContactMessage ct - withStore' $ \db -> updateSndFTDeliveryXFTP db sft msgDeliveryId - withAgent (`xftpDeleteSndFileInternal` aFileId) - (_, _, SMDSnd, GroupChat g@GroupInfo {groupId}) -> do - ms <- withStore' $ \db -> getGroupMembers db user g - let rfdsMemberFTs = zip rfds $ memberFTs ms - extraRFDs = drop (length rfdsMemberFTs) rfds - withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText extraRFDs) - forM_ rfdsMemberFTs $ \mt -> sendToMember mt `catchChatError` (toView . CRChatError (Just user)) - ci' <- withStore $ \db -> do - liftIO $ updateCIFileStatus db user fileId CIFSSndComplete - getChatItemByFileId db vr user fileId - withAgent (`xftpDeleteSndFileInternal` aFileId) - toView $ CRSndFileCompleteXFTP user ci' ft - where - memberFTs :: [GroupMember] -> [(Connection, SndFileTransfer)] - memberFTs ms = M.elems $ M.intersectionWith (,) (M.fromList mConns') (M.fromList sfts') - where - mConns' = mapMaybe useMember ms - sfts' = mapMaybe (\sft@SndFileTransfer {groupMemberId} -> (,sft) <$> groupMemberId) sfts - useMember GroupMember {groupMemberId, activeConn = Just conn@Connection {connStatus}} - | (connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn) = Just (groupMemberId, conn) - | otherwise = Nothing - useMember _ = Nothing - sendToMember :: (ValidFileDescription 'FRecipient, (Connection, SndFileTransfer)) -> m () - sendToMember (rfd, (conn, sft)) = - void $ sendFileDescription sft rfd sharedMsgId $ \msg' -> sendDirectMessage conn msg' $ GroupId groupId - _ -> pure () - _ -> pure () -- TODO error? - SFERR e - | temporaryAgentError e -> - throwChatError $ CEXFTPSndFile fileId (AgentSndFileId aFileId) e - | otherwise -> do - ci <- withStore $ \db -> do - liftIO $ updateFileCancelled db user fileId CIFSSndError - getChatItemByFileId db vr user fileId - withAgent (`xftpDeleteSndFileInternal` aFileId) - toView $ CRSndFileError user ci - where - fileDescrText :: FilePartyI p => ValidFileDescription p -> T.Text - fileDescrText = safeDecodeUtf8 . strEncode - sendFileDescription :: SndFileTransfer -> ValidFileDescription 'FRecipient -> SharedMsgId -> (ChatMsgEvent 'Json -> m (SndMessage, Int64)) -> m Int64 - sendFileDescription sft rfd msgId sendMsg = do - let rfdText = fileDescrText rfd - withStore' $ \db -> updateSndFTDescrXFTP db user sft rfdText - parts <- splitFileDescr rfdText - loopSend parts - where - -- returns msgDeliveryId of the last file description message - loopSend :: NonEmpty FileDescr -> m Int64 - loopSend (fileDescr :| fds) = do - (_, msgDeliveryId) <- sendMsg $ XMsgFileDescr {msgId, fileDescr} - case L.nonEmpty fds of - Just fds' -> loopSend fds' - Nothing -> pure msgDeliveryId - -splitFileDescr :: ChatMonad m => RcvFileDescrText -> m (NonEmpty FileDescr) -splitFileDescr rfdText = do - partSize <- asks $ xftpDescrPartSize . config - pure $ splitParts 1 partSize rfdText - where - splitParts partNo partSize remText = - let (part, rest) = T.splitAt partSize remText - complete = T.null rest - fileDescr = FileDescr {fileDescrText = part, fileDescrPartNo = partNo, fileDescrComplete = complete} - in if complete - then fileDescr :| [] - else fileDescr <| splitParts (partNo + 1) partSize rest - -processAgentMsgRcvFile :: forall m. ChatMonad m => ACorrId -> RcvFileId -> ACommand 'Agent 'AERcvFile -> m () -processAgentMsgRcvFile _corrId aFileId msg = - withStore' (`getUserByARcvFileId` AgentRcvFileId aFileId) >>= \case - Just user -> process user `catchChatError` (toView . CRChatError (Just user)) - _ -> do - withAgent (`xftpDeleteRcvFile` aFileId) - throwChatError $ CENoRcvFileUser $ AgentRcvFileId aFileId - where - process :: User -> m () - process user = do - ft@RcvFileTransfer {fileId} <- withStore $ \db -> do - fileId <- getXFTPRcvFileDBId db $ AgentRcvFileId aFileId - getRcvFileTransfer db user fileId - vr <- chatVersionRange - unless (rcvFileCompleteOrCancelled ft) $ case msg of - RFPROG rcvProgress rcvTotal -> do - let status = CIFSRcvTransfer {rcvProgress, rcvTotal} - ci <- withStore $ \db -> do - liftIO $ updateCIFileStatus db user fileId status - getChatItemByFileId db vr user fileId - toView $ CRRcvFileProgressXFTP user ci rcvProgress rcvTotal - RFDONE xftpPath -> - case liveRcvFileTransferPath ft of - Nothing -> throwChatError $ CEInternalError "no target path for received XFTP file" - Just targetPath -> do - fsTargetPath <- toFSFilePath targetPath - renameFile xftpPath fsTargetPath - ci <- withStore $ \db -> do - liftIO $ do - updateRcvFileStatus db fileId FSComplete - updateCIFileStatus db user fileId CIFSRcvComplete - getChatItemByFileId db vr user fileId - agentXFTPDeleteRcvFile aFileId fileId - toView $ CRRcvFileComplete user ci - RFERR e - | temporaryAgentError e -> - throwChatError $ CEXFTPRcvFile fileId (AgentRcvFileId aFileId) e - | otherwise -> do - ci <- withStore $ \db -> do - liftIO $ updateFileCancelled db user fileId CIFSRcvError - getChatItemByFileId db vr user fileId - agentXFTPDeleteRcvFile aFileId fileId - toView $ CRRcvFileError user ci e - -processAgentMessageConn :: forall m. ChatMonad m => VersionRange -> User -> ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> m () -processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = do - entity <- withStore (\db -> getConnectionEntity db vr user $ AgentConnId agentConnId) >>= updateConnStatus - case agentMessage of - END -> case entity of - RcvDirectMsgConnection _ (Just ct) -> toView $ CRContactAnotherClient user ct - _ -> toView $ CRSubscriptionEnd user entity - MSGNTF smpMsgInfo -> toView $ CRNtfMessage user entity $ ntfMsgInfo smpMsgInfo - _ -> case entity of - RcvDirectMsgConnection conn contact_ -> - processDirectMessage agentMessage entity conn contact_ - RcvGroupMsgConnection conn gInfo m -> - processGroupMessage agentMessage entity conn gInfo m - RcvFileConnection conn ft -> - processRcvFileConn agentMessage entity conn ft - SndFileConnection conn ft -> - processSndFileConn agentMessage entity conn ft - UserContactConnection conn uc -> - processUserContactRequest agentMessage entity conn uc - where - updateConnStatus :: ConnectionEntity -> m ConnectionEntity - updateConnStatus acEntity = case agentMsgConnStatus agentMessage of - Just connStatus -> do - let conn = (entityConnection acEntity) {connStatus} - withStore' $ \db -> updateConnectionStatus db conn connStatus - pure $ updateEntityConnStatus acEntity connStatus - Nothing -> pure acEntity - - agentMsgConnStatus :: ACommand 'Agent e -> Maybe ConnStatus - agentMsgConnStatus = \case - CONF {} -> Just ConnRequested - INFO _ -> Just ConnSndReady - CON -> Just ConnReady - _ -> Nothing - - processDirectMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> Maybe Contact -> m () - processDirectMessage agentMsg connEntity conn@Connection {connId, peerChatVRange, viaUserContactLink, customUserProfileId, connectionCode} = \case - Nothing -> case agentMsg of - CONF confId _ connInfo -> do - -- [incognito] send saved profile - incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) - let profileToSend = userProfileToSend user (fromLocalProfile <$> incognitoProfile) Nothing - conn' <- saveConnInfo conn connInfo - -- [async agent commands] no continuation needed, but command should be asynchronous for stability - allowAgentConnectionAsync user conn' confId $ XInfo profileToSend - INFO connInfo -> do - _conn' <- saveConnInfo conn connInfo - pure () - MSG meta _msgFlags msgBody -> do - cmdId <- createAckCmd conn - -- TODO only acknowledge without saving message? - -- probably this branch is never executed, so there should be no reason - -- to save message if contact hasn't been created yet - chat item isn't created anyway - withAckMessage agentConnId cmdId meta $ do - (_conn', _) <- saveDirectRcvMSG conn meta cmdId msgBody - pure False - SENT msgId -> - sentMsgDeliveryEvent conn msgId - OK -> - -- [async agent commands] continuation on receiving OK - withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} -> - when (cmdFunction == CFAckMessage) $ ackMsgDeliveryEvent conn cmdId - MERR _ err -> do - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - incAuthErrCounter connEntity conn err - ERR err -> do - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - -- TODO add debugging output - _ -> pure () - Just ct@Contact {contactId} -> case agentMsg of - INV (ACR _ cReq) -> - -- [async agent commands] XGrpMemIntro continuation on receiving INV - withCompletedCommand conn agentMsg $ \_ -> - case cReq of - directConnReq@(CRInvitationUri _ _) -> do - contData <- withStore' $ \db -> do - setConnConnReqInv db user connId cReq - getXGrpMemIntroContDirect db user ct - forM_ contData $ \(hostConnId, xGrpMemIntroCont) -> - sendXGrpMemInv hostConnId (Just directConnReq) xGrpMemIntroCont - CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" - MSG msgMeta _msgFlags msgBody -> do - cmdId <- createAckCmd conn - withAckMessage agentConnId cmdId msgMeta $ do - (conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn msgMeta cmdId msgBody - let ct' = ct {activeConn = Just conn'} :: Contact - assertDirectAllowed user MDRcv ct' $ toCMEventTag event - updateChatLock "directMessage" event - case event of - XMsgNew mc -> newContentMessage ct' mc msg msgMeta - XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct' sharedMsgId fileDescr msgMeta - XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct' sharedMsgId mContent msg msgMeta ttl live - XMsgDel sharedMsgId _ -> messageDelete ct' sharedMsgId msg msgMeta - XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct' sharedMsgId reaction add msg msgMeta - -- TODO discontinue XFile - XFile fInv -> processFileInvitation' ct' fInv msg msgMeta - XFileCancel sharedMsgId -> xFileCancel ct' sharedMsgId msgMeta - XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct' sharedMsgId fileConnReq_ fName msgMeta - XInfo p -> xInfo ct' p - XDirectDel -> xDirectDel ct' msg msgMeta - XGrpInv gInv -> processGroupInvitation ct' gInv msg msgMeta - XInfoProbe probe -> xInfoProbe (COMContact ct') probe - XInfoProbeCheck probeHash -> xInfoProbeCheck (COMContact ct') probeHash - XInfoProbeOk probe -> xInfoProbeOk (COMContact ct') probe - XCallInv callId invitation -> xCallInv ct' callId invitation msg msgMeta - XCallOffer callId offer -> xCallOffer ct' callId offer msg msgMeta - XCallAnswer callId answer -> xCallAnswer ct' callId answer msg msgMeta - XCallExtra callId extraInfo -> xCallExtra ct' callId extraInfo msg msgMeta - XCallEnd callId -> xCallEnd ct' callId msg msgMeta - BFileChunk sharedMsgId chunk -> bFileChunk ct' sharedMsgId chunk msgMeta - _ -> messageError $ "unsupported message: " <> T.pack (show event) - let Contact {chatSettings = ChatSettings {sendRcpts}} = ct' - pure $ fromMaybe (sendRcptsContacts user) sendRcpts && hasDeliveryReceipt (toCMEventTag event) - RCVD msgMeta msgRcpt -> - withAckMessage' agentConnId conn msgMeta $ - directMsgReceived ct conn msgMeta msgRcpt - CONF confId _ connInfo -> do - ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo - conn' <- updatePeerChatVRange conn chatVRange - case chatMsgEvent of - -- confirming direct connection with a member - XGrpMemInfo _memId _memProfile -> do - -- TODO check member ID - -- TODO update member profile - -- [async agent commands] no continuation needed, but command should be asynchronous for stability - allowAgentConnectionAsync user conn' confId XOk - XInfo profile -> do - ct' <- processContactProfileUpdate ct profile False `catchChatError` const (pure ct) - -- [incognito] send incognito profile - incognitoProfile <- forM customUserProfileId $ \profileId -> withStore $ \db -> getProfileById db userId profileId - let p = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct') - allowAgentConnectionAsync user conn' confId $ XInfo p - void $ withStore' $ \db -> resetMemberContactFields db ct' - _ -> messageError "CONF for existing contact must have x.grp.mem.info or x.info" - INFO connInfo -> do - ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo - _conn' <- updatePeerChatVRange conn chatVRange - case chatMsgEvent of - XGrpMemInfo _memId _memProfile -> do - -- TODO check member ID - -- TODO update member profile - pure () - XInfo profile -> - void $ processContactProfileUpdate ct profile False - XOk -> pure () - _ -> messageError "INFO for existing contact must have x.grp.mem.info, x.info or x.ok" - CON -> - withStore' (\db -> getViaGroupMember db vr user ct) >>= \case - Nothing -> do - -- [incognito] print incognito profile used for this contact - incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) - setContactNetworkStatus ct NSConnected - toView $ CRContactConnected user ct (fmap fromLocalProfile incognitoProfile) - when (directOrUsed ct) $ createFeatureEnabledItems ct - when (contactConnInitiated conn) $ do - let Connection {groupLinkId} = conn - doProbeContacts = isJust groupLinkId - probeMatchingContactsAndMembers ct (contactConnIncognito ct) doProbeContacts - withStore' $ \db -> resetContactConnInitiated db user conn - forM_ viaUserContactLink $ \userContactLinkId -> do - ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId - let (UserContactLink {autoAccept}, groupId_, gLinkMemRole) = ucl - forM_ autoAccept $ \(AutoAccept {autoReply = mc_}) -> - forM_ mc_ $ \mc -> do - (msg, _) <- sendDirectContactMessage ct (XMsgNew $ MCSimple (extMsgContent mc Nothing)) - ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) - toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) - forM_ groupId_ $ \groupId -> do - groupInfo <- withStore $ \db -> getGroupInfo db vr user groupId - subMode <- chatReadVar subscriptionMode - groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation subMode - gVar <- asks random - withStore $ \db -> createNewContactMemberAsync db gVar user groupInfo ct gLinkMemRole groupConnIds (fromJVersionRange peerChatVRange) subMode - Just (gInfo, m@GroupMember {activeConn}) -> - when (maybe False ((== ConnReady) . connStatus) activeConn) $ do - notifyMemberConnected gInfo m $ Just ct - let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo - when (memberCategory m == GCPreMember) $ probeMatchingContactsAndMembers ct connectedIncognito True - SENT msgId -> do - sentMsgDeliveryEvent conn msgId - checkSndInlineFTComplete conn msgId - updateDirectItemStatus ct conn msgId $ CISSndSent SSPComplete - SWITCH qd phase cStats -> do - toView $ CRContactSwitch user ct (SwitchProgress qd phase cStats) - when (phase `elem` [SPStarted, SPCompleted]) $ case qd of - QDRcv -> createInternalChatItem user (CDDirectSnd ct) (CISndConnEvent $ SCESwitchQueue phase Nothing) Nothing - QDSnd -> createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing - RSYNC rss cryptoErr_ cStats -> - case (rss, connectionCode, cryptoErr_) of - (RSRequired, _, Just cryptoErr) -> processErr cryptoErr - (RSAllowed, _, Just cryptoErr) -> processErr cryptoErr - (RSAgreed, Just _, _) -> do - withStore' $ \db -> setConnectionVerified db user connId Nothing - let ct' = ct {activeConn = Just $ (conn :: Connection) {connectionCode = Nothing}} :: Contact - ratchetSyncEventItem ct' - securityCodeChanged ct' - _ -> ratchetSyncEventItem ct - where - processErr cryptoErr = do - let e@(mde, n) = agentMsgDecryptError cryptoErr - ci_ <- withStore $ \db -> - getDirectChatItemsLast db user contactId 1 "" - >>= liftIO - . mapM (\(ci, content') -> updateDirectChatItem' db user contactId ci content' False Nothing) - . (mdeUpdatedCI e <=< headMaybe) - case ci_ of - Just ci -> toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) - _ -> do - toView $ CRContactRatchetSync user ct (RatchetSyncProgress rss cStats) - createInternalChatItem user (CDDirectRcv ct) (CIRcvDecryptionError mde n) Nothing - headMaybe = \case - x : _ -> Just x - _ -> Nothing - ratchetSyncEventItem ct' = do - toView $ CRContactRatchetSync user ct' (RatchetSyncProgress rss cStats) - createInternalChatItem user (CDDirectRcv ct') (CIRcvConnEvent $ RCERatchetSync rss) Nothing - OK -> - -- [async agent commands] continuation on receiving OK - withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} -> - when (cmdFunction == CFAckMessage) $ ackMsgDeliveryEvent conn cmdId - MERR msgId err -> do - updateDirectItemStatus ct conn msgId $ agentErrToItemStatus err - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - incAuthErrCounter connEntity conn err - ERR err -> do - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - -- TODO add debugging output - _ -> pure () - - processGroupMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> m () - processGroupMessage agentMsg connEntity conn@Connection {connId, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of - INV (ACR _ cReq) -> - withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} -> - case cReq of - groupConnReq@(CRInvitationUri _ _) -> case cmdFunction of - -- [async agent commands] XGrpMemIntro continuation on receiving INV - CFCreateConnGrpMemInv - | isCompatibleRange (fromJVersionRange $ peerChatVRange conn) groupNoDirectVRange -> sendWithoutDirectCReq - | otherwise -> sendWithDirectCReq - where - sendWithoutDirectCReq = do - let GroupMember {groupMemberId, memberId} = m - hostConnId <- withStore $ \db -> do - liftIO $ setConnConnReqInv db user connId cReq - getHostConnId db user groupId - sendXGrpMemInv hostConnId Nothing XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} - sendWithDirectCReq = do - let GroupMember {groupMemberId, memberId} = m - contData <- withStore' $ \db -> do - setConnConnReqInv db user connId cReq - getXGrpMemIntroContGroup db user m - forM_ contData $ \(hostConnId, directConnReq) -> - sendXGrpMemInv hostConnId (Just directConnReq) XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} - -- [async agent commands] group link auto-accept continuation on receiving INV - CFCreateConnGrpInv -> do - ct <- withStore $ \db -> getContactViaMember db user m - withStore' $ \db -> setNewContactMemberConnRequest db user m cReq - groupLinkId <- withStore' $ \db -> getGroupLinkId db user gInfo - sendGrpInvitation ct m groupLinkId - toView $ CRSentGroupInvitation user gInfo ct m - where - sendGrpInvitation :: Contact -> GroupMember -> Maybe GroupLinkId -> m () - sendGrpInvitation ct GroupMember {memberId, memberRole = memRole} groupLinkId = do - let GroupMember {memberRole = userRole, memberId = userMemberId} = membership - groupInv = GroupInvitation (MemberIdRole userMemberId userRole) (MemberIdRole memberId memRole) cReq groupProfile groupLinkId - (_msg, _) <- sendDirectContactMessage ct $ XGrpInv groupInv - -- we could link chat item with sent group invitation message (_msg) - createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing - _ -> throwChatError $ CECommandError "unexpected cmdFunction" - CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" - CONF confId _ connInfo -> do - ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo - conn' <- updatePeerChatVRange conn chatVRange - case memberCategory m of - GCInviteeMember -> - case chatMsgEvent of - XGrpAcpt memId - | sameMemberId memId m -> do - withStore $ \db -> liftIO $ updateGroupMemberStatus db userId m GSMemAccepted - -- [async agent commands] no continuation needed, but command should be asynchronous for stability - allowAgentConnectionAsync user conn' confId XOk - | otherwise -> messageError "x.grp.acpt: memberId is different from expected" - _ -> messageError "CONF from invited member must have x.grp.acpt" - _ -> - case chatMsgEvent of - XGrpMemInfo memId _memProfile - | sameMemberId memId m -> do - -- TODO update member profile - -- [async agent commands] no continuation needed, but command should be asynchronous for stability - allowAgentConnectionAsync user conn' confId $ XGrpMemInfo membership.memberId (fromLocalProfile $ memberProfile membership) - | otherwise -> messageError "x.grp.mem.info: memberId is different from expected" - _ -> messageError "CONF from member must have x.grp.mem.info" - INFO connInfo -> do - ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo - _conn' <- updatePeerChatVRange conn chatVRange - case chatMsgEvent of - XGrpMemInfo memId _memProfile - | sameMemberId memId m -> do - -- TODO update member profile - pure () - | otherwise -> messageError "x.grp.mem.info: memberId is different from expected" - XInfo _ -> pure () -- sent when connecting via group link - XOk -> pure () - _ -> messageError "INFO from member must have x.grp.mem.info, x.info or x.ok" - pure () - CON -> do - withStore' $ \db -> do - updateGroupMemberStatus db userId m GSMemConnected - unless (memberActive membership) $ - updateGroupMemberStatus db userId membership GSMemConnected - -- possible improvement: check for each pending message, requires keeping track of connection state - unless (connDisabled conn) $ sendPendingGroupMessages user m conn - withAgent $ \a -> toggleConnectionNtfs a (aConnId conn) $ chatHasNtfs chatSettings - case memberCategory m of - GCHostMember -> do - toView $ CRUserJoinedGroup user gInfo {membership = membership {memberStatus = GSMemConnected}} m {memberStatus = GSMemConnected} - createGroupFeatureItems gInfo m - let GroupInfo {groupProfile = GroupProfile {description}} = gInfo - memberConnectedChatItem gInfo m - forM_ description $ groupDescriptionChatItem gInfo m - GCInviteeMember -> do - memberConnectedChatItem gInfo m - toView $ CRJoinedGroupMember user gInfo m {memberStatus = GSMemConnected} - let Connection {viaUserContactLink} = conn - when (isJust viaUserContactLink && isNothing (memberContactId m)) sendXGrpLinkMem - members <- withStore' $ \db -> getGroupMembers db user gInfo - void . sendGroupMessage user gInfo members . XGrpMemNew $ memberInfo m - sendIntroductions members - when (groupFeatureAllowed SGFHistory gInfo) sendHistory - where - sendXGrpLinkMem = do - let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo - profileToSend = profileToSendOnAccept user profileMode - void $ sendDirectMessage conn (XGrpLinkMem profileToSend) (GroupId groupId) - sendIntroductions members = do - intros <- withStore' $ \db -> createIntroductions db (maxVersion vr) members m - shuffledIntros <- liftIO $ shuffleIntros intros - if isCompatibleRange (memberChatVRange' m) batchSendVRange - then do - let events = map (XGrpMemIntro . memberInfo . reMember) shuffledIntros - forM_ (L.nonEmpty events) $ \events' -> - sendGroupMemberMessages user conn events' groupId - else forM_ shuffledIntros $ \intro -> - processIntro intro `catchChatError` (toView . CRChatError (Just user)) - shuffleIntros :: [GroupMemberIntro] -> IO [GroupMemberIntro] - shuffleIntros intros = do - let (admins, others) = partition isAdmin intros - (admPics, admNoPics) = partition hasPicture admins - (othPics, othNoPics) = partition hasPicture others - mconcat <$> mapM shuffle [admPics, admNoPics, othPics, othNoPics] - where - isAdmin GroupMemberIntro {reMember = GroupMember {memberRole}} = memberRole >= GRAdmin - hasPicture GroupMemberIntro {reMember = GroupMember {memberProfile = LocalProfile {image}}} = isJust image - processIntro intro@GroupMemberIntro {introId} = do - void $ sendDirectMessage conn (XGrpMemIntro $ memberInfo (reMember intro)) (GroupId groupId) - withStore' $ \db -> updateIntroStatus db introId GMIntroSent - sendHistory = - when (isCompatibleRange (memberChatVRange' m) batchSendVRange) $ do - (errs, items) <- partitionEithers <$> withStore' (\db -> getGroupHistoryItems db user gInfo 100) - (errs', events) <- partitionEithers <$> mapM (tryChatError . itemForwardEvents) items - let errors = map ChatErrorStore errs <> errs' - unless (null errors) $ toView $ CRChatErrors (Just user) errors - forM_ (L.nonEmpty $ concat events) $ \events' -> - sendGroupMemberMessages user conn events' groupId - itemForwardEvents :: CChatItem 'CTGroup -> m [ChatMsgEvent 'Json] - itemForwardEvents cci = case cci of - (CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv sender, content = CIRcvMsgContent mc, file}) -> do - fInvDescr_ <- join <$> forM file getRcvFileInvDescr - processContentItem sender ci mc fInvDescr_ - (CChatItem SMDSnd ci@ChatItem {content = CISndMsgContent mc, file}) -> do - fInvDescr_ <- join <$> forM file getSndFileInvDescr - processContentItem membership ci mc fInvDescr_ - _ -> pure [] - where - getRcvFileInvDescr :: CIFile 'MDRcv -> m (Maybe (FileInvitation, RcvFileDescrText)) - getRcvFileInvDescr ciFile@CIFile {fileId, fileProtocol, fileStatus} = do - expired <- fileExpired - if fileProtocol /= FPXFTP || fileStatus == CIFSRcvCancelled || expired - then pure Nothing - else do - rfd <- withStore $ \db -> getRcvFileDescrByRcvFileId db fileId - pure $ invCompleteDescr ciFile rfd - getSndFileInvDescr :: CIFile 'MDSnd -> m (Maybe (FileInvitation, RcvFileDescrText)) - getSndFileInvDescr ciFile@CIFile {fileId, fileProtocol, fileStatus} = do - expired <- fileExpired - if fileProtocol /= FPXFTP || fileStatus == CIFSSndCancelled || expired - then pure Nothing - else do - -- can also lookup in extra_xftp_file_descriptions, though it can be empty; - -- would be best if snd file had a single rcv description for all members saved in files table - rfd <- withStore $ \db -> getRcvFileDescrBySndFileId db fileId - pure $ invCompleteDescr ciFile rfd - fileExpired :: m Bool - fileExpired = do - ttl <- asks $ rcvFilesTTL . agentConfig . config - cutoffTs <- addUTCTime (-ttl) <$> liftIO getCurrentTime - pure $ chatItemTs cci < cutoffTs - invCompleteDescr :: CIFile d -> RcvFileDescr -> Maybe (FileInvitation, RcvFileDescrText) - invCompleteDescr CIFile {fileName, fileSize} RcvFileDescr {fileDescrText, fileDescrComplete} - | fileDescrComplete = - let fInvDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False} - fInv = xftpFileInvitation fileName fileSize fInvDescr - in Just (fInv, fileDescrText) - | otherwise = Nothing - processContentItem :: GroupMember -> ChatItem 'CTGroup d -> MsgContent -> Maybe (FileInvitation, RcvFileDescrText) -> m [ChatMsgEvent Json] - processContentItem sender ChatItem {meta, quotedItem} mc fInvDescr_ = - if isNothing fInvDescr_ && not (msgContentHasText mc) - then pure [] - else do - let CIMeta {itemTs, itemSharedMsgId, itemTimed} = meta - quotedItemId_ = quoteItemId =<< quotedItem - fInv_ = fst <$> fInvDescr_ - (msgContainer, _) <- prepareGroupMsg user gInfo mc quotedItemId_ fInv_ itemTimed False - let senderVRange = memberChatVRange' sender - xMsgNewChatMsg = ChatMessage {chatVRange = senderVRange, msgId = itemSharedMsgId, chatMsgEvent = XMsgNew msgContainer} - fileDescrEvents <- case (snd <$> fInvDescr_, itemSharedMsgId) of - (Just fileDescrText, Just msgId) -> do - parts <- splitFileDescr fileDescrText - pure . toList $ L.map (XMsgFileDescr msgId) parts - _ -> pure [] - let fileDescrChatMsgs = map (ChatMessage senderVRange Nothing) fileDescrEvents - GroupMember {memberId} = sender - msgForwardEvents = map (\cm -> XGrpMsgForward memberId cm itemTs) (xMsgNewChatMsg : fileDescrChatMsgs) - pure msgForwardEvents - _ -> do - let memCategory = memberCategory m - withStore' (\db -> getViaGroupContact db user m) >>= \case - Nothing -> do - notifyMemberConnected gInfo m Nothing - let connectedIncognito = memberIncognito membership - when (memCategory == GCPreMember) $ probeMatchingMemberContact m connectedIncognito - Just ct@Contact {activeConn} -> - forM_ activeConn $ \Connection {connStatus} -> - when (connStatus == ConnReady) $ do - notifyMemberConnected gInfo m $ Just ct - let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo - when (memCategory == GCPreMember) $ probeMatchingContactsAndMembers ct connectedIncognito True - sendXGrpMemCon memCategory - where - sendXGrpMemCon = \case - GCPreMember -> - forM_ (invitedByGroupMemberId membership) $ \hostId -> do - host <- withStore $ \db -> getGroupMember db user groupId hostId - forM_ (memberConn host) $ \hostConn -> - void $ sendDirectMessage hostConn (XGrpMemCon m.memberId) (GroupId groupId) - GCPostMember -> - forM_ (invitedByGroupMemberId m) $ \invitingMemberId -> do - im <- withStore $ \db -> getGroupMember db user groupId invitingMemberId - forM_ (memberConn im) $ \imConn -> - void $ sendDirectMessage imConn (XGrpMemCon m.memberId) (GroupId groupId) - _ -> messageWarning "sendXGrpMemCon: member category GCPreMember or GCPostMember is expected" - MSG msgMeta _msgFlags msgBody -> do - checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure () - cmdId <- createAckCmd conn - let aChatMsgs = parseChatMessages msgBody - withAckMessage agentConnId cmdId msgMeta $ do - forM_ aChatMsgs $ \case - Right (ACMsg _ chatMsg) -> - processEvent cmdId chatMsg `catchChatError` \e -> toView $ CRChatError (Just user) e - Left e -> toView $ CRChatError (Just user) (ChatError . CEException $ "error parsing chat message: " <> e) - checkSendRcpt $ rights aChatMsgs - -- currently only a single message is forwarded - when (membership.memberRole >= GRAdmin) $ case aChatMsgs of - [Right (ACMsg _ chatMsg)] -> forwardMsg_ chatMsg - _ -> pure () - where - brokerTs = metaBrokerTs msgMeta - processEvent :: MsgEncodingI e => CommandId -> ChatMessage e -> m () - processEvent cmdId chatMsg = do - (m', conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m conn msgMeta cmdId msgBody chatMsg - updateChatLock "groupMessage" event - case event of - XMsgNew mc -> memberCanSend m' $ newGroupContentMessage gInfo m' mc msg brokerTs False - XMsgFileDescr sharedMsgId fileDescr -> memberCanSend m' $ groupMessageFileDescription gInfo m' sharedMsgId fileDescr - XMsgUpdate sharedMsgId mContent ttl live -> memberCanSend m' $ groupMessageUpdate gInfo m' sharedMsgId mContent msg brokerTs ttl live - XMsgDel sharedMsgId memberId -> groupMessageDelete gInfo m' sharedMsgId memberId msg brokerTs - XMsgReact sharedMsgId (Just memberId) reaction add -> groupMsgReaction gInfo m' sharedMsgId memberId reaction add msg brokerTs - -- TODO discontinue XFile - XFile fInv -> processGroupFileInvitation' gInfo m' fInv msg brokerTs - XFileCancel sharedMsgId -> xFileCancelGroup gInfo m' sharedMsgId - XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInvGroup gInfo m' sharedMsgId fileConnReq_ fName - XInfo p -> xInfoMember gInfo m' p - XGrpLinkMem p -> xGrpLinkMem gInfo m' conn' p - XGrpMemNew memInfo -> xGrpMemNew gInfo m' memInfo msg brokerTs - XGrpMemIntro memInfo -> xGrpMemIntro gInfo m' memInfo - XGrpMemInv memId introInv -> xGrpMemInv gInfo m' memId introInv - XGrpMemFwd memInfo introInv -> xGrpMemFwd gInfo m' memInfo introInv - XGrpMemRole memId memRole -> xGrpMemRole gInfo m' memId memRole msg brokerTs - XGrpMemCon memId -> xGrpMemCon gInfo m' memId - XGrpMemDel memId -> xGrpMemDel gInfo m' memId msg brokerTs - XGrpLeave -> xGrpLeave gInfo m' msg brokerTs - XGrpDel -> xGrpDel gInfo m' msg brokerTs - XGrpInfo p' -> xGrpInfo gInfo m' p' msg brokerTs - XGrpDirectInv connReq mContent_ -> memberCanSend m' $ xGrpDirectInv gInfo m' conn' connReq mContent_ msg brokerTs - XGrpMsgForward memberId msg' msgTs -> xGrpMsgForward gInfo m' memberId msg' msgTs - XInfoProbe probe -> xInfoProbe (COMGroupMember m') probe - XInfoProbeCheck probeHash -> xInfoProbeCheck (COMGroupMember m') probeHash - XInfoProbeOk probe -> xInfoProbeOk (COMGroupMember m') probe - BFileChunk sharedMsgId chunk -> bFileChunkGroup gInfo sharedMsgId chunk msgMeta - _ -> messageError $ "unsupported message: " <> T.pack (show event) - checkSendRcpt :: [AChatMessage] -> m Bool - checkSendRcpt aChatMsgs = do - currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo - let GroupInfo {chatSettings = ChatSettings {sendRcpts}} = gInfo - pure $ - fromMaybe (sendRcptsSmallGroups user) sendRcpts - && any aChatMsgHasReceipt aChatMsgs - && currentMemCount <= smallGroupsRcptsMemLimit - where - aChatMsgHasReceipt (ACMsg _ ChatMessage {chatMsgEvent}) = - hasDeliveryReceipt (toCMEventTag chatMsgEvent) - forwardMsg_ :: MsgEncodingI e => ChatMessage e -> m () - forwardMsg_ chatMsg = - forM_ (forwardedGroupMsg chatMsg) $ \chatMsg' -> do - ChatConfig {highlyAvailable} <- asks config - -- members introduced to this invited member - introducedMembers <- - if memberCategory m == GCInviteeMember - then withStore' $ \db -> getForwardIntroducedMembers db user m highlyAvailable - else pure [] - -- invited members to which this member was introduced - invitedMembers <- withStore' $ \db -> getForwardInvitedMembers db user m highlyAvailable - let ms = introducedMembers <> invitedMembers - msg = XGrpMsgForward m.memberId chatMsg' brokerTs - unless (null ms) . void $ - sendGroupMessage user gInfo ms msg - RCVD msgMeta msgRcpt -> - withAckMessage' agentConnId conn msgMeta $ - groupMsgReceived gInfo m conn msgMeta msgRcpt - SENT msgId -> do - sentMsgDeliveryEvent conn msgId - checkSndInlineFTComplete conn msgId - updateGroupItemStatus gInfo m conn msgId $ CISSndSent SSPComplete - SWITCH qd phase cStats -> do - toView $ CRGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats) - when (phase `elem` [SPStarted, SPCompleted]) $ case qd of - QDRcv -> createInternalChatItem user (CDGroupSnd gInfo) (CISndConnEvent . SCESwitchQueue phase . Just $ groupMemberRef m) Nothing - QDSnd -> createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing - RSYNC rss cryptoErr_ cStats -> - case (rss, connectionCode, cryptoErr_) of - (RSRequired, _, Just cryptoErr) -> processErr cryptoErr - (RSAllowed, _, Just cryptoErr) -> processErr cryptoErr - (RSAgreed, Just _, _) -> do - withStore' $ \db -> setConnectionVerified db user connId Nothing - let m' = m {activeConn = Just (conn {connectionCode = Nothing} :: Connection)} :: GroupMember - ratchetSyncEventItem m' - toView $ CRGroupMemberVerificationReset user gInfo m' - createInternalChatItem user (CDGroupRcv gInfo m') (CIRcvConnEvent RCEVerificationCodeReset) Nothing - _ -> ratchetSyncEventItem m - where - processErr cryptoErr = do - let e@(mde, n) = agentMsgDecryptError cryptoErr - ci_ <- withStore $ \db -> - getGroupMemberChatItemLast db user groupId (groupMemberId' m) - >>= liftIO - . mapM (\(ci, content') -> updateGroupChatItem db user groupId ci content' False Nothing) - . mdeUpdatedCI e - case ci_ of - Just ci -> toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci) - _ -> do - toView $ CRGroupMemberRatchetSync user gInfo m (RatchetSyncProgress rss cStats) - createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvDecryptionError mde n) Nothing - ratchetSyncEventItem m' = do - toView $ CRGroupMemberRatchetSync user gInfo m' (RatchetSyncProgress rss cStats) - createInternalChatItem user (CDGroupRcv gInfo m') (CIRcvConnEvent $ RCERatchetSync rss) Nothing - OK -> - -- [async agent commands] continuation on receiving OK - withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} -> - when (cmdFunction == CFAckMessage) $ ackMsgDeliveryEvent conn cmdId - MERR msgId err -> do - chatItemId_ <- withStore' $ \db -> getChatItemIdByAgentMsgId db connId msgId - forM_ chatItemId_ $ \itemId -> do - let GroupMember {groupMemberId} = m - updateGroupMemSndStatus itemId groupMemberId $ agentErrToItemStatus err - -- group errors are silenced to reduce load on UI event log - -- toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - incAuthErrCounter connEntity conn err - ERR err -> do - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - -- TODO add debugging output - _ -> pure () - - agentMsgDecryptError :: AgentCryptoError -> (MsgDecryptError, Word32) - agentMsgDecryptError = \case - DECRYPT_AES -> (MDEOther, 1) - DECRYPT_CB -> (MDEOther, 1) - RATCHET_HEADER -> (MDERatchetHeader, 1) - RATCHET_EARLIER _ -> (MDERatchetEarlier, 1) - RATCHET_SKIPPED n -> (MDETooManySkipped, n) - RATCHET_SYNC -> (MDERatchetSync, 0) - - mdeUpdatedCI :: (MsgDecryptError, Word32) -> CChatItem c -> Maybe (ChatItem c 'MDRcv, CIContent 'MDRcv) - mdeUpdatedCI (mde', n') (CChatItem _ ci@ChatItem {content = CIRcvDecryptionError mde n}) - | mde == mde' = case mde of - MDERatchetHeader -> r (n + n') - MDETooManySkipped -> r n' -- the numbers are not added as sequential MDETooManySkipped will have it incremented by 1 - MDERatchetEarlier -> r (n + n') - MDEOther -> r (n + n') - MDERatchetSync -> r 0 - | otherwise = Nothing - where - r n'' = Just (ci, CIRcvDecryptionError mde n'') - mdeUpdatedCI _ _ = Nothing - - processSndFileConn :: ACommand 'Agent e -> ConnectionEntity -> Connection -> SndFileTransfer -> m () - processSndFileConn agentMsg connEntity conn ft@SndFileTransfer {fileId, fileName, fileStatus} = - case agentMsg of - -- SMP CONF for SndFileConnection happens for direct file protocol - -- when recipient of the file "joins" connection created by the sender - CONF confId _ connInfo -> do - ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo - conn' <- updatePeerChatVRange conn chatVRange - case chatMsgEvent of - -- TODO save XFileAcpt message - XFileAcpt name - | name == fileName -> do - withStore' $ \db -> updateSndFileStatus db ft FSAccepted - -- [async agent commands] no continuation needed, but command should be asynchronous for stability - allowAgentConnectionAsync user conn' confId XOk - | otherwise -> messageError "x.file.acpt: fileName is different from expected" - _ -> messageError "CONF from file connection must have x.file.acpt" - CON -> do - ci <- withStore $ \db -> do - liftIO $ updateSndFileStatus db ft FSConnected - updateDirectCIFileStatus db vr user fileId $ CIFSSndTransfer 0 1 - toView $ CRSndFileStart user ci ft - sendFileChunk user ft - SENT msgId -> do - withStore' $ \db -> updateSndFileChunkSent db ft msgId - unless (fileStatus == FSCancelled) $ sendFileChunk user ft - MERR _ err -> do - cancelSndFileTransfer user ft True >>= mapM_ (deleteAgentConnectionAsync user) - case err of - SMP SMP.AUTH -> unless (fileStatus == FSCancelled) $ do - ci <- withStore $ \db -> do - getChatRefByFileId db user fileId >>= \case - ChatRef CTDirect _ -> liftIO $ updateFileCancelled db user fileId CIFSSndCancelled - _ -> pure () - getChatItemByFileId db vr user fileId - toView $ CRSndFileRcvCancelled user ci ft - _ -> throwChatError $ CEFileSend fileId err - MSG meta _ _ -> withAckMessage' agentConnId conn meta $ pure () - OK -> - -- [async agent commands] continuation on receiving OK - withCompletedCommand conn agentMsg $ \_cmdData -> pure () - ERR err -> do - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - -- TODO add debugging output - _ -> pure () - - processRcvFileConn :: ACommand 'Agent e -> ConnectionEntity -> Connection -> RcvFileTransfer -> m () - processRcvFileConn agentMsg connEntity conn ft@RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}, grpMemberId} = - case agentMsg of - INV (ACR _ cReq) -> - withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} -> - case cReq of - fileInvConnReq@(CRInvitationUri _ _) -> case cmdFunction of - -- [async agent commands] direct XFileAcptInv continuation on receiving INV - CFCreateConnFileInvDirect -> do - ct <- withStore $ \db -> getContactByFileId db user fileId - sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId - void $ sendDirectContactMessage ct (XFileAcptInv sharedMsgId (Just fileInvConnReq) fileName) - -- [async agent commands] group XFileAcptInv continuation on receiving INV - CFCreateConnFileInvGroup -> case grpMemberId of - Just gMemberId -> do - GroupMember {groupId, activeConn} <- withStore $ \db -> getGroupMemberById db user gMemberId - case activeConn of - Just gMemberConn -> do - sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId - void $ sendDirectMessage gMemberConn (XFileAcptInv sharedMsgId (Just fileInvConnReq) fileName) $ GroupId groupId - _ -> throwChatError $ CECommandError "no GroupMember activeConn" - _ -> throwChatError $ CECommandError "no grpMemberId" - _ -> throwChatError $ CECommandError "unexpected cmdFunction" - CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" - -- SMP CONF for RcvFileConnection happens for group file protocol - -- when sender of the file "joins" connection created by the recipient - -- (sender doesn't create connections for all group members) - CONF confId _ connInfo -> do - ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo - conn' <- updatePeerChatVRange conn chatVRange - case chatMsgEvent of - XOk -> allowAgentConnectionAsync user conn' confId XOk -- [async agent commands] no continuation needed, but command should be asynchronous for stability - _ -> pure () - CON -> startReceivingFile user fileId - MSG meta _ msgBody -> do - parseFileChunk msgBody >>= receiveFileChunk ft (Just conn) meta - OK -> - -- [async agent commands] continuation on receiving OK - withCompletedCommand conn agentMsg $ \_cmdData -> pure () - MERR _ err -> do - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - incAuthErrCounter connEntity conn err - ERR err -> do - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - -- TODO add debugging output - _ -> pure () - - receiveFileChunk :: RcvFileTransfer -> Maybe Connection -> MsgMeta -> FileChunk -> m () - receiveFileChunk ft@RcvFileTransfer {fileId, chunkSize} conn_ meta@MsgMeta {recipient = (msgId, _), integrity} = \case - FileChunkCancel -> - unless (rcvFileCompleteOrCancelled ft) $ do - cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user) - ci <- withStore $ \db -> getChatItemByFileId db vr user fileId - toView $ CRRcvFileSndCancelled user ci ft - FileChunk {chunkNo, chunkBytes = chunk} -> do - case integrity of - MsgOk -> pure () - MsgError MsgDuplicate -> pure () -- TODO remove once agent removes duplicates - MsgError e -> - badRcvFileChunk ft $ "invalid file chunk number " <> show chunkNo <> ": " <> show e - withStore' (\db -> createRcvFileChunk db ft chunkNo msgId) >>= \case - RcvChunkOk -> - if B.length chunk /= fromInteger chunkSize - then badRcvFileChunk ft "incorrect chunk size" - else ack $ appendFileChunk ft chunkNo chunk False - RcvChunkFinal -> - if B.length chunk > fromInteger chunkSize - then badRcvFileChunk ft "incorrect chunk size" - else do - appendFileChunk ft chunkNo chunk True - ci <- withStore $ \db -> do - liftIO $ do - updateRcvFileStatus db fileId FSComplete - updateCIFileStatus db user fileId CIFSRcvComplete - deleteRcvFileChunks db ft - getChatItemByFileId db vr user fileId - toView $ CRRcvFileComplete user ci - forM_ conn_ $ \conn -> deleteAgentConnectionAsync user (aConnId conn) - RcvChunkDuplicate -> ack $ pure () - RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo - where - ack a = case conn_ of - Just conn -> withAckMessage' agentConnId conn meta a - Nothing -> a - - processUserContactRequest :: ACommand 'Agent e -> ConnectionEntity -> Connection -> UserContact -> m () - processUserContactRequest agentMsg connEntity conn UserContact {userContactLinkId} = case agentMsg of - REQ invId _ connInfo -> do - ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo - case chatMsgEvent of - XContact p xContactId_ -> profileContactRequest invId chatVRange p xContactId_ - XInfo p -> profileContactRequest invId chatVRange p Nothing - -- TODO show/log error, other events in contact request - _ -> pure () - MERR _ err -> do - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - incAuthErrCounter connEntity conn err - ERR err -> do - toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - -- TODO add debugging output - _ -> pure () - where - profileContactRequest :: InvitationId -> VersionRange -> Profile -> Maybe XContactId -> m () - profileContactRequest invId chatVRange p xContactId_ = do - withStore (\db -> createOrUpdateContactRequest db user userContactLinkId invId chatVRange p xContactId_) >>= \case - CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact - CORRequest cReq -> do - ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId - let (UserContactLink {autoAccept}, groupId_, gLinkMemRole) = ucl - case autoAccept of - Just AutoAccept {acceptIncognito} -> case groupId_ of - Nothing -> do - -- [incognito] generate profile to send, create connection with incognito profile - incognitoProfile <- if acceptIncognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing - ct <- acceptContactRequestAsync user cReq incognitoProfile True - toView $ CRAcceptingContactRequest user ct - Just groupId -> do - gInfo <- withStore $ \db -> getGroupInfo db vr user groupId - let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo - if isCompatibleRange chatVRange groupLinkNoContactVRange - then do - mem <- acceptGroupJoinRequestAsync user gInfo cReq gLinkMemRole profileMode - createInternalChatItem user (CDGroupRcv gInfo mem) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing - toView $ CRAcceptingGroupJoinRequestMember user gInfo mem - else do - ct <- acceptContactRequestAsync user cReq profileMode False - toView $ CRAcceptingGroupJoinRequest user gInfo ct - _ -> toView $ CRReceivedContactRequest user cReq - - memberCanSend :: GroupMember -> m () -> m () - memberCanSend mem a - | mem.memberRole <= GRObserver = messageError "member is not allowed to send messages" - | otherwise = a - - incAuthErrCounter :: ConnectionEntity -> Connection -> AgentErrorType -> m () - incAuthErrCounter connEntity conn err = do - case err of - SMP SMP.AUTH -> do - authErrCounter' <- withStore' $ \db -> incConnectionAuthErrCounter db user conn - when (authErrCounter' >= authErrDisableCount) $ do - toView $ CRConnectionDisabled connEntity - _ -> pure () - - updateChatLock :: MsgEncodingI enc => String -> ChatMsgEvent enc -> m () - updateChatLock name event = do - l <- asks chatLock - atomically $ tryReadTMVar l >>= mapM_ (swapTMVar l . (<> s)) - where - s = " " <> name <> "=" <> B.unpack (strEncode $ toCMEventTag event) - - withCompletedCommand :: forall e. AEntityI e => Connection -> ACommand 'Agent e -> (CommandData -> m ()) -> m () - withCompletedCommand Connection {connId} agentMsg action = do - let agentMsgTag = APCT (sAEntity @e) $ aCommandTag agentMsg - cmdData_ <- withStore' $ \db -> getCommandDataByCorrId db user corrId - case cmdData_ of - Just cmdData@CommandData {cmdId, cmdConnId = Just cmdConnId', cmdFunction} - | connId == cmdConnId' && (agentMsgTag == commandExpectedResponse cmdFunction || agentMsgTag == APCT SAEConn ERR_) -> do - withStore' $ \db -> deleteCommand db user cmdId - action cmdData - | otherwise -> err cmdId $ "not matching connection id or unexpected response, corrId = " <> show corrId - Just CommandData {cmdId, cmdConnId = Nothing} -> err cmdId $ "no command connection id, corrId = " <> show corrId - Nothing -> throwChatError . CEAgentCommandError $ "command not found, corrId = " <> show corrId - where - err cmdId msg = do - withStore' $ \db -> updateCommandStatus db user cmdId CSError - throwChatError . CEAgentCommandError $ msg - - createAckCmd :: Connection -> m CommandId - createAckCmd Connection {connId} = do - withStore' $ \db -> createCommand db user (Just connId) CFAckMessage - - withAckMessage' :: ConnId -> Connection -> MsgMeta -> m () -> m () - withAckMessage' cId conn msgMeta action = do - cmdId <- createAckCmd conn - withAckMessage cId cmdId msgMeta $ action $> False - - withAckMessage :: ConnId -> CommandId -> MsgMeta -> m Bool -> m () - withAckMessage cId cmdId msgMeta action = do - -- [async agent commands] command should be asynchronous, continuation is ackMsgDeliveryEvent - -- TODO catching error and sending ACK after an error, particularly if it is a database error, will result in the message not processed (and no notification to the user). - -- Possible solutions are: - -- 1) retry processing several times - -- 2) stabilize database - -- 3) show screen of death to the user asking to restart - tryChatError action >>= \case - Right withRcpt -> ackMsg cId cmdId msgMeta $ if withRcpt then Just "" else Nothing - Left e -> ackMsg cId cmdId msgMeta Nothing >> throwError e - - ackMsg :: ConnId -> CommandId -> MsgMeta -> Maybe MsgReceiptInfo -> m () - ackMsg cId cmdId MsgMeta {recipient = (msgId, _)} rcpt = withAgent $ \a -> ackMessageAsync a (aCorrId cmdId) cId msgId rcpt - - ackMsgDeliveryEvent :: Connection -> CommandId -> m () - ackMsgDeliveryEvent Connection {connId} ackCmdId = - withStore' $ \db -> updateRcvMsgDeliveryStatus db connId ackCmdId MDSRcvAcknowledged - - sentMsgDeliveryEvent :: Connection -> AgentMsgId -> m () - sentMsgDeliveryEvent Connection {connId} msgId = - withStore' $ \db -> updateSndMsgDeliveryStatus db connId msgId MDSSndSent - - agentErrToItemStatus :: AgentErrorType -> CIStatus 'MDSnd - agentErrToItemStatus (SMP AUTH) = CISSndErrorAuth - agentErrToItemStatus err = CISSndError . T.unpack . safeDecodeUtf8 $ strEncode err - - badRcvFileChunk :: RcvFileTransfer -> String -> m () - badRcvFileChunk ft err = - unless (rcvFileCompleteOrCancelled ft) $ do - cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user) - throwChatError $ CEFileRcvChunk err - - memberConnectedChatItem :: GroupInfo -> GroupMember -> m () - memberConnectedChatItem gInfo m = - -- ts should be broker ts but we don't have it for CON - createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvGroupEvent RGEMemberConnected) Nothing - - groupDescriptionChatItem :: GroupInfo -> GroupMember -> Text -> m () - groupDescriptionChatItem gInfo m descr = - createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvMsgContent $ MCText descr) Nothing - - notifyMemberConnected :: GroupInfo -> GroupMember -> Maybe Contact -> m () - notifyMemberConnected gInfo m ct_ = do - memberConnectedChatItem gInfo m - mapM_ (`setContactNetworkStatus` NSConnected) ct_ - toView $ CRConnectedToGroupMember user gInfo m ct_ - - probeMatchingContactsAndMembers :: Contact -> IncognitoEnabled -> Bool -> m () - probeMatchingContactsAndMembers ct connectedIncognito doProbeContacts = do - gVar <- asks random - contactMerge <- readTVarIO =<< asks contactMergeEnabled - if contactMerge && not connectedIncognito - then do - (probe, probeId) <- withStore $ \db -> createSentProbe db gVar userId (COMContact ct) - -- ! when making changes to probe-and-merge mechanism, - -- ! test scenario in which recipient receives probe after probe hashes (not covered in tests): - -- sendProbe -> sendProbeHashes (currently) - -- sendProbeHashes -> sendProbe (reversed - change order in code, may add delay) - sendProbe probe - cs <- - if doProbeContacts - then map COMContact <$> withStore' (\db -> getMatchingContacts db user ct) - else pure [] - ms <- map COMGroupMember <$> withStore' (\db -> getMatchingMembers db user ct) - sendProbeHashes (cs <> ms) probe probeId - else sendProbe . Probe =<< liftIO (encodedRandomBytes gVar 32) - where - sendProbe :: Probe -> m () - sendProbe probe = void . sendDirectContactMessage ct $ XInfoProbe probe - - probeMatchingMemberContact :: GroupMember -> IncognitoEnabled -> m () - probeMatchingMemberContact GroupMember {activeConn = Nothing} _ = pure () - probeMatchingMemberContact m@GroupMember {groupId, activeConn = Just conn} connectedIncognito = do - gVar <- asks random - contactMerge <- readTVarIO =<< asks contactMergeEnabled - if contactMerge && not connectedIncognito - then do - (probe, probeId) <- withStore $ \db -> createSentProbe db gVar userId $ COMGroupMember m - sendProbe probe - cs <- map COMContact <$> withStore' (\db -> getMatchingMemberContacts db user m) - sendProbeHashes cs probe probeId - else sendProbe . Probe =<< liftIO (encodedRandomBytes gVar 32) - where - sendProbe :: Probe -> m () - sendProbe probe = void $ sendDirectMessage conn (XInfoProbe probe) (GroupId groupId) - - sendProbeHashes :: [ContactOrMember] -> Probe -> Int64 -> m () - sendProbeHashes cgms probe probeId = - forM_ cgms $ \cgm -> sendProbeHash cgm `catchChatError` \_ -> pure () - where - probeHash = ProbeHash $ C.sha256Hash (unProbe probe) - sendProbeHash :: ContactOrMember -> m () - sendProbeHash cgm@(COMContact c) = do - void . sendDirectContactMessage c $ XInfoProbeCheck probeHash - withStore' $ \db -> createSentProbeHash db userId probeId cgm - sendProbeHash (COMGroupMember GroupMember {activeConn = Nothing}) = pure () - sendProbeHash cgm@(COMGroupMember m@GroupMember {groupId, activeConn = Just conn}) = - when (memberCurrent m) $ do - void $ sendDirectMessage conn (XInfoProbeCheck probeHash) (GroupId groupId) - withStore' $ \db -> createSentProbeHash db userId probeId cgm - - messageWarning :: Text -> m () - messageWarning = toView . CRMessageError user "warning" - - messageError :: Text -> m () - messageError = toView . CRMessageError user "error" - - newContentMessage :: Contact -> MsgContainer -> RcvMessage -> MsgMeta -> m () - newContentMessage ct@Contact {contactUsed} mc msg@RcvMessage {sharedMsgId_} msgMeta = do - unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct - checkIntegrityCreateItem (CDDirectRcv ct) msgMeta - let ExtMsgContent content fInv_ _ _ = mcExtMsgContent mc - -- Uncomment to test stuck delivery on errors - see test testDirectMessageDelete - -- case content of - -- MCText "hello 111" -> - -- UE.throwIO $ userError "#####################" - -- -- throwChatError $ CECommandError "#####################" - -- _ -> pure () - if isVoice content && not (featureAllowed SCFVoice forContact ct) - then do - void $ newChatItem (CIRcvChatFeatureRejected CFVoice) Nothing Nothing False - else do - let ExtMsgContent _ _ itemTTL live_ = mcExtMsgContent mc - timed_ = rcvContactCITimed ct itemTTL - live = fromMaybe False live_ - file_ <- processFileInvitation fInv_ content $ \db -> createRcvFileTransfer db userId ct - newChatItem (CIRcvMsgContent content) (snd <$> file_) timed_ live - autoAcceptFile file_ - where - brokerTs = metaBrokerTs msgMeta - newChatItem ciContent ciFile_ timed_ live = do - ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ brokerTs ciContent ciFile_ timed_ live - reactions <- maybe (pure []) (\sharedMsgId -> withStore' $ \db -> getDirectCIReactions db ct sharedMsgId) sharedMsgId_ - toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci {reactions}) - - autoAcceptFile :: Maybe (RcvFileTransfer, CIFile 'MDRcv) -> m () - autoAcceptFile = mapM_ $ \(ft, CIFile {fileSize}) -> do - ChatConfig {autoAcceptFileSize = sz} <- asks config - when (sz > fileSize) $ receiveFile' user ft Nothing Nothing >>= toView - - messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> MsgMeta -> m () - messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr msgMeta = do - checkIntegrityCreateItem (CDDirectRcv ct) msgMeta - fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId - processFDMessage fileId fileDescr - - groupMessageFileDescription :: GroupInfo -> GroupMember -> SharedMsgId -> FileDescr -> m () - groupMessageFileDescription GroupInfo {groupId} _m sharedMsgId fileDescr = do - fileId <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId - processFDMessage fileId fileDescr - - processFDMessage :: FileTransferId -> FileDescr -> m () - processFDMessage fileId fileDescr = do - ft <- withStore $ \db -> getRcvFileTransfer db user fileId - unless (rcvFileCompleteOrCancelled ft) $ do - (rfd, RcvFileTransfer {fileStatus, xftpRcvFile, cryptoArgs}) <- withStore $ \db -> do - rfd <- appendRcvFD db userId fileId fileDescr - -- reading second time in the same transaction as appending description - -- to prevent race condition with accept - ft' <- getRcvFileTransfer db user fileId - pure (rfd, ft') - case (fileStatus, xftpRcvFile) of - (RFSAccepted _, Just XFTPRcvFile {}) -> receiveViaCompleteFD user fileId rfd cryptoArgs - _ -> pure () - - processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer) -> m (Maybe (RcvFileTransfer, CIFile 'MDRcv)) - processFileInvitation fInv_ mc createRcvFT = forM fInv_ $ \fInv@FileInvitation {fileName, fileSize} -> do - ChatConfig {fileChunkSize} <- asks config - inline <- receiveInlineMode fInv (Just mc) fileChunkSize - ft@RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFT db fInv inline fileChunkSize - let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP - (filePath, fileStatus, ft') <- case inline of - Just IFMSent -> do - encrypt <- chatReadVar encryptLocalFiles - ft' <- (if encrypt then setFileToEncrypt else pure) ft - fPath <- getRcvFilePath fileId Nothing fileName True - withStore' $ \db -> startRcvInlineFT db user ft' fPath inline - pure (Just fPath, CIFSRcvAccepted, ft') - _ -> pure (Nothing, CIFSRcvInvitation, ft) - let RcvFileTransfer {cryptoArgs} = ft' - fileSource = (`CryptoFile` cryptoArgs) <$> filePath - pure (ft', CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol}) - - messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> m () - messageUpdate ct@Contact {contactId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do - checkIntegrityCreateItem (CDDirectRcv ct) msgMeta - updateRcvChatItem `catchCINotFound` \_ -> do - -- 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 - ci <- saveRcvChatItem' user (CDDirectRcv ct) msg (Just sharedMsgId) brokerTs content Nothing timed_ live - ci' <- withStore' $ \db -> do - createChatItemVersion db (chatItemId' ci) brokerTs mc - updateDirectChatItem' db user contactId ci content live Nothing - toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') - where - brokerTs = metaBrokerTs msgMeta - content = CIRcvMsgContent mc - live = fromMaybe False live_ - updateRcvChatItem = do - cci <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId - case cci of - CChatItem SMDRcv ci@ChatItem {meta = CIMeta {itemLive}, content = CIRcvMsgContent oldMC} -> do - let changed = mc /= oldMC - if changed || fromMaybe False itemLive - then do - ci' <- withStore' $ \db -> do - when changed $ - addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc) - reactions <- getDirectCIReactions db ct sharedMsgId - updateDirectChatItem' db user contactId ci {reactions} content live $ Just msgId - toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') - startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' - else toView $ CRChatItemNotChanged user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) - _ -> messageError "x.msg.update: contact attempted invalid message update" - - messageDelete :: Contact -> SharedMsgId -> RcvMessage -> MsgMeta -> m () - messageDelete ct@Contact {contactId} sharedMsgId RcvMessage {msgId} msgMeta@MsgMeta {broker = (_, brokerTs)} = do - checkIntegrityCreateItem (CDDirectRcv ct) msgMeta - deleteRcvChatItem `catchCINotFound` (toView . CRChatItemDeletedNotFound user ct) - where - deleteRcvChatItem = do - CChatItem msgDir ci <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId - case msgDir of - SMDRcv -> - if featureAllowed SCFFullDelete forContact ct - then deleteDirectCI user ct ci False False >>= toView - else markDirectCIDeleted user ct ci msgId False brokerTs >>= toView - SMDSnd -> messageError "x.msg.del: contact attempted invalid message delete" - - directMsgReaction :: Contact -> SharedMsgId -> MsgReaction -> Bool -> RcvMessage -> MsgMeta -> m () - directMsgReaction ct sharedMsgId reaction add RcvMessage {msgId} MsgMeta {broker = (_, brokerTs)} = do - when (featureAllowed SCFReactions forContact ct) $ do - rs <- withStore' $ \db -> getDirectReactions db ct sharedMsgId False - when (reactionAllowed add reaction rs) $ do - updateChatItemReaction `catchCINotFound` \_ -> - withStore' $ \db -> setDirectReaction db ct sharedMsgId False reaction add msgId brokerTs - where - updateChatItemReaction = do - cr_ <- withStore $ \db -> do - CChatItem md ci <- getDirectChatItemBySharedMsgId db user (contactId' ct) sharedMsgId - if ciReactionAllowed ci - then liftIO $ do - setDirectReaction db ct sharedMsgId False reaction add msgId brokerTs - reactions <- getDirectCIReactions db ct sharedMsgId - let ci' = CChatItem md ci {reactions} - r = ACIReaction SCTDirect SMDRcv (DirectChat ct) $ CIReaction CIDirectRcv ci' brokerTs reaction - pure $ Just $ CRChatItemReaction user add r - else pure Nothing - mapM_ toView cr_ - - groupMsgReaction :: GroupInfo -> GroupMember -> SharedMsgId -> MemberId -> MsgReaction -> Bool -> RcvMessage -> UTCTime -> m () - groupMsgReaction g@GroupInfo {groupId} m sharedMsgId itemMemberId reaction add RcvMessage {msgId} brokerTs = do - when (groupFeatureAllowed SGFReactions g) $ do - rs <- withStore' $ \db -> getGroupReactions db g m itemMemberId sharedMsgId False - when (reactionAllowed add reaction rs) $ do - updateChatItemReaction `catchCINotFound` \_ -> - withStore' $ \db -> setGroupReaction db g m itemMemberId sharedMsgId False reaction add msgId brokerTs - where - updateChatItemReaction = do - cr_ <- withStore $ \db -> do - CChatItem md ci <- getGroupMemberCIBySharedMsgId db user groupId itemMemberId sharedMsgId - if ciReactionAllowed ci - then liftIO $ do - setGroupReaction db g m itemMemberId sharedMsgId False reaction add msgId brokerTs - reactions <- getGroupCIReactions db g itemMemberId sharedMsgId - let ci' = CChatItem md ci {reactions} - r = ACIReaction SCTGroup SMDRcv (GroupChat g) $ CIReaction (CIGroupRcv m) ci' brokerTs reaction - pure $ Just $ CRChatItemReaction user add r - else pure Nothing - mapM_ toView cr_ - - reactionAllowed :: Bool -> MsgReaction -> [MsgReaction] -> Bool - reactionAllowed add reaction rs = (reaction `elem` rs) /= add && not (add && length rs >= maxMsgReactions) - - catchCINotFound :: m a -> (SharedMsgId -> m a) -> m a - catchCINotFound f handle = - f `catchChatError` \case - ChatErrorStore (SEChatItemSharedMsgIdNotFound sharedMsgId) -> handle sharedMsgId - e -> throwError e - - newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContainer -> RcvMessage -> UTCTime -> Bool -> m () - newGroupContentMessage gInfo m@GroupMember {memberId, memberRole} mc msg@RcvMessage {sharedMsgId_} brokerTs forwarded - | isVoice content && not (groupFeatureAllowed SGFVoice gInfo) = rejected GFVoice - | not (isVoice content) && isJust fInv_ && not (groupFeatureAllowed SGFFiles gInfo) = rejected GFFiles - | otherwise = do - let timed_ = - if forwarded - then rcvCITimed_ (Just Nothing) itemTTL - else rcvGroupCITimed gInfo itemTTL - live = fromMaybe False live_ - withStore' (\db -> getCIModeration db user gInfo memberId sharedMsgId_) >>= \case - Just ciModeration -> do - applyModeration timed_ live ciModeration - withStore' $ \db -> deleteCIModeration db gInfo memberId sharedMsgId_ - Nothing -> createItem timed_ live - where - rejected f = void $ newChatItem (CIRcvGroupFeatureRejected f) Nothing Nothing False - ExtMsgContent content fInv_ itemTTL live_ = mcExtMsgContent mc - applyModeration timed_ live CIModeration {moderatorMember = moderator@GroupMember {memberRole = moderatorRole}, createdByMsgId, moderatedAt} - | moderatorRole < GRAdmin || moderatorRole < memberRole = - createItem timed_ live - | groupFeatureAllowed SGFFullDelete gInfo = do - ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ brokerTs CIRcvModerated Nothing timed_ False - ci' <- withStore' $ \db -> updateGroupChatItemModerated db user gInfo ci moderator moderatedAt - toView $ CRNewChatItem user $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci' - | otherwise = do - file_ <- processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId m - ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ brokerTs (CIRcvMsgContent content) (snd <$> file_) timed_ False - toView =<< markGroupCIDeleted user gInfo ci createdByMsgId False (Just moderator) moderatedAt - createItem timed_ live = do - file_ <- processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId m - newChatItem (CIRcvMsgContent content) (snd <$> file_) timed_ live - when (showMessages $ memberSettings m) $ autoAcceptFile file_ - newChatItem ciContent ciFile_ timed_ live = do - ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ brokerTs ciContent ciFile_ timed_ live - ci' <- blockedMember m ci $ withStore' $ \db -> markGroupChatItemBlocked db user gInfo ci - reactions <- maybe (pure []) (\sharedMsgId -> withStore' $ \db -> getGroupCIReactions db gInfo memberId sharedMsgId) sharedMsgId_ - groupMsgToView gInfo ci' {reactions} - - groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> RcvMessage -> UTCTime -> Maybe Int -> Maybe Bool -> m () - groupMessageUpdate gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId, memberId} sharedMsgId mc msg@RcvMessage {msgId} brokerTs ttl_ live_ = - updateRcvChatItem `catchCINotFound` \_ -> do - -- 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_ = rcvGroupCITimed gInfo ttl_ - ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg (Just sharedMsgId) brokerTs content Nothing timed_ live - ci' <- withStore' $ \db -> do - createChatItemVersion db (chatItemId' ci) brokerTs mc - ci' <- updateGroupChatItem db user groupId ci content live Nothing - blockedMember m ci' $ markGroupChatItemBlocked db user gInfo ci' - toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') - where - content = CIRcvMsgContent mc - live = fromMaybe False live_ - updateRcvChatItem = do - cci <- withStore $ \db -> getGroupChatItemBySharedMsgId db user groupId groupMemberId sharedMsgId - case cci of - CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv m', meta = CIMeta {itemLive}, content = CIRcvMsgContent oldMC} -> - if sameMemberId memberId m' - then do - let changed = mc /= oldMC - if changed || fromMaybe False itemLive - then do - ci' <- withStore' $ \db -> do - when changed $ - addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc) - reactions <- getGroupCIReactions db gInfo memberId sharedMsgId - updateGroupChatItem db user groupId ci {reactions} content live $ Just msgId - toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') - startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' - else toView $ CRChatItemNotChanged user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci) - else messageError "x.msg.update: group member attempted to update a message of another member" - _ -> messageError "x.msg.update: group member attempted invalid message update" - - groupMessageDelete :: GroupInfo -> GroupMember -> SharedMsgId -> Maybe MemberId -> RcvMessage -> UTCTime -> m () - groupMessageDelete gInfo@GroupInfo {groupId, membership} m@GroupMember {memberId, memberRole = senderRole} sharedMsgId sndMemberId_ RcvMessage {msgId} brokerTs = do - let msgMemberId = fromMaybe memberId sndMemberId_ - withStore' (\db -> runExceptT $ getGroupMemberCIBySharedMsgId db user groupId msgMemberId sharedMsgId) >>= \case - Right (CChatItem _ ci@ChatItem {chatDir}) -> case chatDir of - CIGroupRcv mem - | sameMemberId memberId mem && msgMemberId == memberId -> delete ci Nothing >>= toView - | otherwise -> deleteMsg mem ci - CIGroupSnd -> deleteMsg membership ci - Left e - | msgMemberId == memberId -> messageError $ "x.msg.del: message not found, " <> tshow e - | senderRole < GRAdmin -> messageError $ "x.msg.del: message not found, message of another member with insufficient member permissions, " <> tshow e - | otherwise -> withStore' $ \db -> createCIModeration db gInfo m msgMemberId sharedMsgId msgId brokerTs - where - deleteMsg :: MsgDirectionI d => GroupMember -> ChatItem 'CTGroup d -> m () - deleteMsg mem ci = case sndMemberId_ of - Just sndMemberId - | sameMemberId sndMemberId mem -> checkRole mem $ delete ci (Just m) >>= toView - | otherwise -> messageError "x.msg.del: message of another member with incorrect memberId" - _ -> messageError "x.msg.del: message of another member without memberId" - checkRole GroupMember {memberRole} a - | senderRole < GRAdmin || senderRole < memberRole = - messageError "x.msg.del: message of another member with insufficient member permissions" - | otherwise = a - delete :: MsgDirectionI d => ChatItem 'CTGroup d -> Maybe GroupMember -> m ChatResponse - delete ci byGroupMember - | groupFeatureAllowed SGFFullDelete gInfo = deleteGroupCI user gInfo ci False False byGroupMember brokerTs - | otherwise = markGroupCIDeleted user gInfo ci msgId False byGroupMember brokerTs - - -- TODO remove once XFile is discontinued - processFileInvitation' :: Contact -> FileInvitation -> RcvMessage -> MsgMeta -> m () - processFileInvitation' ct fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} msgMeta = do - checkIntegrityCreateItem (CDDirectRcv ct) msgMeta - ChatConfig {fileChunkSize} <- asks config - inline <- receiveInlineMode fInv Nothing fileChunkSize - RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize - let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP - ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} - ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ brokerTs (CIRcvMsgContent $ MCFile "") ciFile Nothing False - toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) - where - brokerTs = metaBrokerTs msgMeta - - -- TODO remove once XFile is discontinued - processGroupFileInvitation' :: GroupInfo -> GroupMember -> FileInvitation -> RcvMessage -> UTCTime -> m () - processGroupFileInvitation' gInfo m fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} brokerTs = do - ChatConfig {fileChunkSize} <- asks config - inline <- receiveInlineMode fInv Nothing fileChunkSize - RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvGroupFileTransfer db userId m fInv inline fileChunkSize - let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP - ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} - ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ brokerTs (CIRcvMsgContent $ MCFile "") ciFile Nothing False - ci' <- blockedMember m ci $ withStore' $ \db -> markGroupChatItemBlocked db user gInfo ci - groupMsgToView gInfo ci' - - blockedMember :: Monad m' => GroupMember -> ChatItem c d -> m' (ChatItem c d) -> m' (ChatItem c d) - blockedMember m ci blockedCI - | showMessages (memberSettings m) = pure ci - | otherwise = blockedCI - - receiveInlineMode :: FileInvitation -> Maybe MsgContent -> Integer -> m (Maybe InlineFileMode) - receiveInlineMode FileInvitation {fileSize, fileInline, fileDescr} mc_ chSize = case (fileInline, fileDescr) of - (Just mode, Nothing) -> do - InlineFilesConfig {receiveChunks, receiveInstant} <- asks $ inlineFiles . config - pure $ if fileSize <= receiveChunks * chSize then inline' receiveInstant else Nothing - where - inline' receiveInstant = if mode == IFMOffer || (receiveInstant && maybe False isVoice mc_) then fileInline else Nothing - _ -> pure Nothing - - xFileCancel :: Contact -> SharedMsgId -> MsgMeta -> m () - xFileCancel ct@Contact {contactId} sharedMsgId msgMeta = do - checkIntegrityCreateItem (CDDirectRcv ct) msgMeta - fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId - ft <- withStore (\db -> getRcvFileTransfer db user fileId) - unless (rcvFileCompleteOrCancelled ft) $ do - cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user) - ci <- withStore $ \db -> getChatItemByFileId db vr user fileId - toView $ CRRcvFileSndCancelled user ci ft - - xFileAcptInv :: Contact -> SharedMsgId -> Maybe ConnReqInvitation -> String -> MsgMeta -> m () - xFileAcptInv ct sharedMsgId fileConnReq_ fName msgMeta = do - checkIntegrityCreateItem (CDDirectRcv ct) msgMeta - fileId <- withStore $ \db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId - (AChatItem _ _ _ ci) <- withStore $ \db -> getChatItemByFileId db vr user fileId - assertSMPAcceptNotProhibited ci - ft@FileTransferMeta {fileName, fileSize, fileInline, cancelled} <- withStore (\db -> getFileTransferMeta db user fileId) - -- [async agent commands] no continuation needed, but command should be asynchronous for stability - if fName == fileName - then unless cancelled $ case fileConnReq_ of - -- receiving via a separate connection - Just fileConnReq -> do - subMode <- chatReadVar subscriptionMode - dm <- directMessage XOk - connIds <- joinAgentConnectionAsync user True fileConnReq dm subMode - withStore' $ \db -> createSndDirectFTConnection db user fileId connIds subMode - -- receiving inline - _ -> do - event <- withStore $ \db -> do - ci' <- updateDirectCIFileStatus db vr user fileId $ CIFSSndTransfer 0 1 - sft <- createSndDirectInlineFT db ct ft - pure $ CRSndFileStart user ci' sft - toView event - ifM - (allowSendInline fileSize fileInline) - (sendDirectFileInline ct ft sharedMsgId) - (messageError "x.file.acpt.inv: fileSize is bigger than allowed to send inline") - else messageError "x.file.acpt.inv: fileName is different from expected" - - assertSMPAcceptNotProhibited :: ChatItem c d -> m () - assertSMPAcceptNotProhibited ChatItem {file = Just CIFile {fileId, fileProtocol}, content} - | fileProtocol == FPXFTP && not (imageOrVoice content) = throwChatError $ CEFallbackToSMPProhibited fileId - | otherwise = pure () - where - imageOrVoice :: CIContent d -> Bool - imageOrVoice (CISndMsgContent (MCImage _ _)) = True - imageOrVoice (CISndMsgContent (MCVoice _ _)) = True - imageOrVoice _ = False - assertSMPAcceptNotProhibited _ = pure () - - checkSndInlineFTComplete :: Connection -> AgentMsgId -> m () - checkSndInlineFTComplete conn agentMsgId = do - sft_ <- withStore' $ \db -> getSndFTViaMsgDelivery db user conn agentMsgId - forM_ sft_ $ \sft@SndFileTransfer {fileId} -> do - ci@(AChatItem _ _ _ ChatItem {file}) <- withStore $ \db -> do - liftIO $ updateSndFileStatus db sft FSComplete - liftIO $ deleteSndFileChunks db sft - updateDirectCIFileStatus db vr user fileId CIFSSndComplete - case file of - Just CIFile {fileProtocol = FPXFTP} -> do - ft <- withStore $ \db -> getFileTransferMeta db user fileId - toView $ CRSndFileCompleteXFTP user ci ft - _ -> toView $ CRSndFileComplete user ci sft - - allowSendInline :: Integer -> Maybe InlineFileMode -> m Bool - allowSendInline fileSize = \case - Just IFMOffer -> do - ChatConfig {fileChunkSize, inlineFiles} <- asks config - pure $ fileSize <= fileChunkSize * offerChunks inlineFiles - _ -> pure False - - bFileChunk :: Contact -> SharedMsgId -> FileChunk -> MsgMeta -> m () - bFileChunk ct sharedMsgId chunk meta = do - ft <- withStore $ \db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId >>= getRcvFileTransfer db user - receiveInlineChunk ft chunk meta - - bFileChunkGroup :: GroupInfo -> SharedMsgId -> FileChunk -> MsgMeta -> m () - bFileChunkGroup GroupInfo {groupId} sharedMsgId chunk meta = do - ft <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId >>= getRcvFileTransfer db user - receiveInlineChunk ft chunk meta - - receiveInlineChunk :: RcvFileTransfer -> FileChunk -> MsgMeta -> m () - receiveInlineChunk RcvFileTransfer {fileId, fileStatus = RFSNew} FileChunk {chunkNo} _ - | chunkNo == 1 = throwChatError $ CEInlineFileProhibited fileId - | otherwise = pure () - receiveInlineChunk ft@RcvFileTransfer {fileId} chunk meta = do - case chunk of - FileChunk {chunkNo} -> when (chunkNo == 1) $ startReceivingFile user fileId - _ -> pure () - receiveFileChunk ft Nothing meta chunk - - xFileCancelGroup :: GroupInfo -> GroupMember -> SharedMsgId -> m () - xFileCancelGroup GroupInfo {groupId} GroupMember {groupMemberId, memberId} sharedMsgId = do - fileId <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId - CChatItem msgDir ChatItem {chatDir} <- withStore $ \db -> getGroupChatItemBySharedMsgId db user groupId groupMemberId sharedMsgId - case (msgDir, chatDir) of - (SMDRcv, CIGroupRcv m) -> do - if sameMemberId memberId m - then do - ft <- withStore (\db -> getRcvFileTransfer db user fileId) - unless (rcvFileCompleteOrCancelled ft) $ do - cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user) - ci <- withStore $ \db -> getChatItemByFileId db vr user fileId - toView $ CRRcvFileSndCancelled user ci ft - else messageError "x.file.cancel: group member attempted to cancel file of another member" -- shouldn't happen now that query includes group member id - (SMDSnd, _) -> messageError "x.file.cancel: group member attempted invalid file cancel" - - xFileAcptInvGroup :: GroupInfo -> GroupMember -> SharedMsgId -> Maybe ConnReqInvitation -> String -> m () - xFileAcptInvGroup GroupInfo {groupId} m@GroupMember {activeConn} sharedMsgId fileConnReq_ fName = do - fileId <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId - (AChatItem _ _ _ ci) <- withStore $ \db -> getChatItemByFileId db vr user fileId - assertSMPAcceptNotProhibited ci - -- TODO check that it's not already accepted - ft@FileTransferMeta {fileName, fileSize, fileInline, cancelled} <- withStore (\db -> getFileTransferMeta db user fileId) - if fName == fileName - then unless cancelled $ case (fileConnReq_, activeConn) of - (Just fileConnReq, _) -> do - subMode <- chatReadVar subscriptionMode - -- receiving via a separate connection - -- [async agent commands] no continuation needed, but command should be asynchronous for stability - dm <- directMessage XOk - connIds <- joinAgentConnectionAsync user True fileConnReq dm subMode - withStore' $ \db -> createSndGroupFileTransferConnection db user fileId connIds m subMode - (_, Just conn) -> do - -- receiving inline - event <- withStore $ \db -> do - ci' <- updateDirectCIFileStatus db vr user fileId $ CIFSSndTransfer 0 1 - sft <- liftIO $ createSndGroupInlineFT db m conn ft - pure $ CRSndFileStart user ci' sft - toView event - ifM - (allowSendInline fileSize fileInline) - (sendMemberFileInline m conn ft sharedMsgId) - (messageError "x.file.acpt.inv: fileSize is bigger than allowed to send inline") - _ -> messageError "x.file.acpt.inv: member connection is not active" - else messageError "x.file.acpt.inv: fileName is different from expected" - - groupMsgToView :: GroupInfo -> ChatItem 'CTGroup 'MDRcv -> m () - groupMsgToView gInfo ci = - toView $ CRNewChatItem user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci) - - processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> m () - processGroupInvitation ct inv msg msgMeta = do - let Contact {localDisplayName = c, activeConn} = ct - GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} = inv - forM_ activeConn $ \Connection {connId, peerChatVRange, customUserProfileId, groupLinkId = groupLinkId'} -> do - checkIntegrityCreateItem (CDDirectRcv ct) msgMeta - when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c) - when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId - -- [incognito] if direct connection with host is incognito, create membership using the same incognito profile - (gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership = membership@GroupMember {groupMemberId, memberId}}, hostId) <- - withStore $ \db -> createGroupInvitation db vr user ct inv customUserProfileId - if sameGroupLinkId groupLinkId groupLinkId' - then do - subMode <- chatReadVar subscriptionMode - dm <- directMessage $ XGrpAcpt memberId - connIds <- joinAgentConnectionAsync user True connRequest dm subMode - withStore' $ \db -> do - setViaGroupLinkHash db groupId connId - createMemberConnectionAsync db user hostId connIds (fromJVersionRange peerChatVRange) subMode - updateGroupMemberStatusById db userId hostId GSMemAccepted - updateGroupMemberStatus db userId membership GSMemAccepted - toView $ CRUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct) - else do - let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole - ci <- saveRcvChatItem user (CDDirectRcv ct) msg brokerTs content - withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci) - toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) - toView $ CRReceivedGroupInvitation {user, groupInfo = gInfo, contact = ct, fromMemberRole = fromRole, memberRole = memRole} - where - brokerTs = metaBrokerTs msgMeta - sameGroupLinkId :: Maybe GroupLinkId -> Maybe GroupLinkId -> Bool - sameGroupLinkId (Just gli) (Just gli') = gli == gli' - sameGroupLinkId _ _ = False - - checkIntegrityCreateItem :: forall c. ChatTypeI c => ChatDirection c 'MDRcv -> MsgMeta -> m () - checkIntegrityCreateItem cd MsgMeta {integrity, broker = (_, brokerTs)} = case integrity of - MsgOk -> pure () - MsgError e -> createInternalChatItem user cd (CIRcvIntegrityError e) (Just brokerTs) - - xInfo :: Contact -> Profile -> m () - xInfo c p' = void $ processContactProfileUpdate c p' True - - xDirectDel :: Contact -> RcvMessage -> MsgMeta -> m () - xDirectDel c msg msgMeta = - if directOrUsed c - then do - checkIntegrityCreateItem (CDDirectRcv c) msgMeta - ct' <- withStore' $ \db -> updateContactStatus db user c CSDeleted - contactConns <- withStore' $ \db -> getContactConnections db userId ct' - deleteAgentConnectionsAsync user $ map aConnId contactConns - forM_ contactConns $ \conn -> withStore' $ \db -> updateConnectionStatus db conn ConnDeleted - activeConn' <- forM (contactConn ct') $ \conn -> pure conn {connStatus = ConnDeleted} - let ct'' = ct' {activeConn = activeConn'} :: Contact - ci <- saveRcvChatItem user (CDDirectRcv ct'') msg brokerTs (CIRcvDirectEvent RDEContactDeleted) - toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct'') ci) - toView $ CRContactDeletedByContact user ct'' - else do - contactConns <- withStore' $ \db -> getContactConnections db userId c - deleteAgentConnectionsAsync user $ map aConnId contactConns - withStore' $ \db -> deleteContact db user c - where - brokerTs = metaBrokerTs msgMeta - - processContactProfileUpdate :: Contact -> Profile -> Bool -> m Contact - processContactProfileUpdate c@Contact {profile = p} p' createItems - | fromLocalProfile p /= p' = do - c' <- withStore $ \db -> - if userTTL == rcvTTL - then updateContactProfile db user c p' - else do - c' <- liftIO $ updateContactUserPreferences db user c ctUserPrefs' - updateContactProfile db user c' p' - when (directOrUsed c' && createItems) $ createRcvFeatureItems user c c' - toView $ CRContactUpdated user c c' - pure c' - | otherwise = - pure c - where - Contact {userPreferences = ctUserPrefs@Preferences {timedMessages = ctUserTMPref}} = c - userTTL = prefParam $ getPreference SCFTimedMessages ctUserPrefs - Profile {preferences = rcvPrefs_} = p' - rcvTTL = prefParam $ getPreference SCFTimedMessages rcvPrefs_ - ctUserPrefs' = - let userDefault = getPreference SCFTimedMessages (fullPreferences user) - userDefaultTTL = prefParam userDefault - ctUserTMPref' = case ctUserTMPref of - Just userTM -> Just (userTM :: TimedMessagesPreference) {ttl = rcvTTL} - _ - | rcvTTL /= userDefaultTTL -> Just (userDefault :: TimedMessagesPreference) {ttl = rcvTTL} - | otherwise -> Nothing - in setPreference_ SCFTimedMessages ctUserTMPref' ctUserPrefs - - xInfoMember :: GroupInfo -> GroupMember -> Profile -> m () - xInfoMember gInfo m p' = void $ processMemberProfileUpdate gInfo m p' - - xGrpLinkMem :: GroupInfo -> GroupMember -> Connection -> Profile -> m () - xGrpLinkMem gInfo@GroupInfo {membership} m@GroupMember {groupMemberId, memberCategory} Connection {viaGroupLink} p' = do - xGrpLinkMemReceived <- withStore $ \db -> getXGrpLinkMemReceived db groupMemberId - if viaGroupLink && isNothing (memberContactId m) && memberCategory == GCHostMember && not xGrpLinkMemReceived - then do - m' <- processMemberProfileUpdate gInfo m p' - withStore' $ \db -> setXGrpLinkMemReceived db groupMemberId True - let connectedIncognito = memberIncognito membership - probeMatchingMemberContact m' connectedIncognito - else messageError "x.grp.link.mem error: invalid group link host profile update" - - processMemberProfileUpdate :: GroupInfo -> GroupMember -> Profile -> m GroupMember - processMemberProfileUpdate gInfo m@GroupMember {memberContactId} p' = - case memberContactId of - Nothing -> do - m' <- withStore $ \db -> updateMemberProfile db user m p' - toView $ CRGroupMemberUpdated user gInfo m m' - pure m' - Just mContactId -> do - mCt <- withStore $ \db -> getContact db user mContactId - Contact {profile} <- processContactProfileUpdate mCt p' True - pure m {memberProfile = profile} - - createFeatureEnabledItems :: Contact -> m () - createFeatureEnabledItems ct@Contact {mergedPreferences} = - forM_ allChatFeatures $ \(ACF f) -> do - let state = featureState $ getContactUserPreference f mergedPreferences - createInternalChatItem user (CDDirectRcv ct) (uncurry (CIRcvChatFeature $ chatFeature f) state) Nothing - - createGroupFeatureItems :: GroupInfo -> GroupMember -> m () - createGroupFeatureItems g@GroupInfo {fullGroupPreferences} m = - forM_ allGroupFeatureItems $ \(AGF f) -> do - let p = getGroupPreference f fullGroupPreferences - (_, param) = groupFeatureState p - createInternalChatItem user (CDGroupRcv g m) (CIRcvGroupFeature (toGroupFeature f) (toGroupPreference p) param) Nothing - - xInfoProbe :: ContactOrMember -> Probe -> m () - xInfoProbe cgm2 probe = do - contactMerge <- readTVarIO =<< asks contactMergeEnabled - -- [incognito] unless connected incognito - when (contactMerge && not (contactOrMemberIncognito cgm2)) $ do - cgm1s <- withStore' $ \db -> matchReceivedProbe db user cgm2 probe - let cgm1s' = filter (not . contactOrMemberIncognito) cgm1s - probeMatches cgm1s' cgm2 - where - probeMatches :: [ContactOrMember] -> ContactOrMember -> m () - probeMatches [] _ = pure () - probeMatches (cgm1' : cgm1s') cgm2' = do - cgm2''_ <- probeMatch cgm1' cgm2' probe `catchChatError` \_ -> pure (Just cgm2') - let cgm2'' = fromMaybe cgm2' cgm2''_ - probeMatches cgm1s' cgm2'' - - xInfoProbeCheck :: ContactOrMember -> ProbeHash -> m () - xInfoProbeCheck cgm1 probeHash = do - contactMerge <- readTVarIO =<< asks contactMergeEnabled - -- [incognito] unless connected incognito - when (contactMerge && not (contactOrMemberIncognito cgm1)) $ do - cgm2Probe_ <- withStore' $ \db -> matchReceivedProbeHash db user cgm1 probeHash - forM_ cgm2Probe_ $ \(cgm2, probe) -> - unless (contactOrMemberIncognito cgm2) . void $ - probeMatch cgm1 cgm2 probe - - probeMatch :: ContactOrMember -> ContactOrMember -> Probe -> m (Maybe ContactOrMember) - probeMatch cgm1 cgm2 probe = - case cgm1 of - COMContact c1@Contact {contactId = cId1, profile = p1} -> - case cgm2 of - COMContact c2@Contact {contactId = cId2, profile = p2} - | cId1 /= cId2 && profilesMatch p1 p2 -> do - void . sendDirectContactMessage c1 $ XInfoProbeOk probe - COMContact <$$> mergeContacts c1 c2 - | otherwise -> messageWarning "probeMatch ignored: profiles don't match or same contact id" >> pure Nothing - COMGroupMember m2@GroupMember {memberProfile = p2, memberContactId} - | isNothing memberContactId && profilesMatch p1 p2 -> do - void . sendDirectContactMessage c1 $ XInfoProbeOk probe - COMContact <$$> associateMemberAndContact c1 m2 - | otherwise -> messageWarning "probeMatch ignored: profiles don't match or member already has contact" >> pure Nothing - COMGroupMember GroupMember {activeConn = Nothing} -> pure Nothing - COMGroupMember m1@GroupMember {groupId, memberProfile = p1, memberContactId, activeConn = Just conn} -> - case cgm2 of - COMContact c2@Contact {profile = p2} - | memberCurrent m1 && isNothing memberContactId && profilesMatch p1 p2 -> do - void $ sendDirectMessage conn (XInfoProbeOk probe) (GroupId groupId) - COMContact <$$> associateMemberAndContact c2 m1 - | otherwise -> messageWarning "probeMatch ignored: profiles don't match or member already has contact or member not current" >> pure Nothing - COMGroupMember _ -> messageWarning "probeMatch ignored: members are not matched with members" >> pure Nothing - - xInfoProbeOk :: ContactOrMember -> Probe -> m () - xInfoProbeOk cgm1 probe = do - cgm2 <- withStore' $ \db -> matchSentProbe db user cgm1 probe - case cgm1 of - COMContact c1@Contact {contactId = cId1} -> - case cgm2 of - Just (COMContact c2@Contact {contactId = cId2}) - | cId1 /= cId2 -> void $ mergeContacts c1 c2 - | otherwise -> messageWarning "xInfoProbeOk ignored: same contact id" - Just (COMGroupMember m2@GroupMember {memberContactId}) - | isNothing memberContactId -> void $ associateMemberAndContact c1 m2 - | otherwise -> messageWarning "xInfoProbeOk ignored: member already has contact" - _ -> pure () - COMGroupMember m1@GroupMember {memberContactId} -> - case cgm2 of - Just (COMContact c2) - | isNothing memberContactId -> void $ associateMemberAndContact c2 m1 - | otherwise -> messageWarning "xInfoProbeOk ignored: member already has contact" - Just (COMGroupMember _) -> messageWarning "xInfoProbeOk ignored: members are not matched with members" - _ -> pure () - - -- to party accepting call - xCallInv :: Contact -> CallId -> CallInvitation -> RcvMessage -> MsgMeta -> m () - xCallInv ct@Contact {contactId} callId CallInvitation {callType, callDhPubKey} msg@RcvMessage {sharedMsgId_} msgMeta = do - checkIntegrityCreateItem (CDDirectRcv ct) msgMeta - if featureAllowed SCFCalls forContact ct - then do - g <- asks random - dhKeyPair <- atomically $ if encryptedCall callType then Just <$> C.generateKeyPair g else pure Nothing - ci <- saveCallItem CISCallPending - let sharedKey = C.Key . C.dhBytes' <$> (C.dh' <$> callDhPubKey <*> (snd <$> dhKeyPair)) - callState = CallInvitationReceived {peerCallType = callType, localDhPubKey = fst <$> dhKeyPair, sharedKey} - call' = Call {contactId, callId, chatItemId = chatItemId' ci, callState, callTs = chatItemTs' ci} - calls <- asks currentCalls - -- theoretically, the new call invitation for the current contact can mark the in-progress call as ended - -- (and replace it in ChatController) - -- practically, this should not happen - withStore' $ \db -> createCall db user call' $ chatItemTs' ci - call_ <- atomically (TM.lookupInsert contactId call' calls) - forM_ call_ $ \call -> updateCallItemStatus user ct call WCSDisconnected Nothing - toView $ CRCallInvitation RcvCallInvitation {user, contact = ct, callType, sharedKey, callTs = chatItemTs' ci} - toView $ CRNewChatItem user $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci - else featureRejected CFCalls - where - brokerTs = metaBrokerTs msgMeta - saveCallItem status = saveRcvChatItem user (CDDirectRcv ct) msg brokerTs (CIRcvCall status 0) - featureRejected f = do - ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ brokerTs (CIRcvChatFeatureRejected f) Nothing Nothing False - toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) - - -- to party initiating call - xCallOffer :: Contact -> CallId -> CallOffer -> RcvMessage -> MsgMeta -> m () - xCallOffer ct callId CallOffer {callType, rtcSession, callDhPubKey} msg msgMeta = do - msgCurrentCall ct callId "x.call.offer" msg msgMeta $ - \call -> case callState call of - CallInvitationSent {localCallType, localDhPrivKey} -> do - let sharedKey = C.Key . C.dhBytes' <$> (C.dh' <$> callDhPubKey <*> localDhPrivKey) - callState' = CallOfferReceived {localCallType, peerCallType = callType, peerCallSession = rtcSession, sharedKey} - askConfirmation = encryptedCall localCallType && not (encryptedCall callType) - toView CRCallOffer {user, contact = ct, callType, offer = rtcSession, sharedKey, askConfirmation} - pure (Just call {callState = callState'}, Just . ACIContent SMDSnd $ CISndCall CISCallAccepted 0) - _ -> do - msgCallStateError "x.call.offer" call - pure (Just call, Nothing) - - -- to party accepting call - xCallAnswer :: Contact -> CallId -> CallAnswer -> RcvMessage -> MsgMeta -> m () - xCallAnswer ct callId CallAnswer {rtcSession} msg msgMeta = do - msgCurrentCall ct callId "x.call.answer" msg msgMeta $ - \call -> case callState call of - CallOfferSent {localCallType, peerCallType, localCallSession, sharedKey} -> do - let callState' = CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession = rtcSession, sharedKey} - toView $ CRCallAnswer user ct rtcSession - pure (Just call {callState = callState'}, Just . ACIContent SMDRcv $ CIRcvCall CISCallNegotiated 0) - _ -> do - msgCallStateError "x.call.answer" call - pure (Just call, Nothing) - - -- to any call party - xCallExtra :: Contact -> CallId -> CallExtraInfo -> RcvMessage -> MsgMeta -> m () - xCallExtra ct callId CallExtraInfo {rtcExtraInfo} msg msgMeta = do - msgCurrentCall ct callId "x.call.extra" msg msgMeta $ - \call -> case callState call of - CallOfferReceived {localCallType, peerCallType, peerCallSession, sharedKey} -> do - -- TODO update the list of ice servers in peerCallSession - let callState' = CallOfferReceived {localCallType, peerCallType, peerCallSession, sharedKey} - toView $ CRCallExtraInfo user ct rtcExtraInfo - pure (Just call {callState = callState'}, Nothing) - CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession, sharedKey} -> do - -- TODO update the list of ice servers in peerCallSession - let callState' = CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession, sharedKey} - toView $ CRCallExtraInfo user ct rtcExtraInfo - pure (Just call {callState = callState'}, Nothing) - _ -> do - msgCallStateError "x.call.extra" call - pure (Just call, Nothing) - - -- to any call party - xCallEnd :: Contact -> CallId -> RcvMessage -> MsgMeta -> m () - xCallEnd ct callId msg msgMeta = - msgCurrentCall ct callId "x.call.end" msg msgMeta $ \Call {chatItemId} -> do - toView $ CRCallEnded user ct - (Nothing,) <$> callStatusItemContent user ct chatItemId WCSDisconnected - - msgCurrentCall :: Contact -> CallId -> Text -> RcvMessage -> MsgMeta -> (Call -> m (Maybe Call, Maybe ACIContent)) -> m () - msgCurrentCall ct@Contact {contactId = ctId'} callId' eventName RcvMessage {msgId} msgMeta action = do - checkIntegrityCreateItem (CDDirectRcv ct) msgMeta - calls <- asks currentCalls - atomically (TM.lookup ctId' calls) >>= \case - Nothing -> messageError $ eventName <> ": no current call" - Just call@Call {contactId, callId, chatItemId} - | contactId /= ctId' || callId /= callId' -> messageError $ eventName <> ": wrong contact or callId" - | otherwise -> do - (call_, aciContent_) <- action call - case call_ of - Just call' -> do - unless (isRcvInvitation call') $ withStore' $ \db -> deleteCalls db user ctId' - atomically $ TM.insert ctId' call' calls - _ -> do - withStore' $ \db -> deleteCalls db user ctId' - atomically $ TM.delete ctId' calls - forM_ aciContent_ $ \aciContent -> - updateDirectChatItemView user ct chatItemId aciContent False $ Just msgId - - msgCallStateError :: Text -> Call -> m () - msgCallStateError eventName Call {callState} = - messageError $ eventName <> ": wrong call state " <> T.pack (show $ callStateTag callState) - - mergeContacts :: Contact -> Contact -> m (Maybe Contact) - mergeContacts c1 c2 = do - let Contact {localDisplayName = cLDN1, profile = LocalProfile {displayName}} = c1 - Contact {localDisplayName = cLDN2} = c2 - case (suffixOrd displayName cLDN1, suffixOrd displayName cLDN2) of - (Just cOrd1, Just cOrd2) - | cOrd1 < cOrd2 -> merge c1 c2 - | cOrd2 < cOrd1 -> merge c2 c1 - | otherwise -> pure Nothing - _ -> pure Nothing - where - merge c1' c2' = do - c2'' <- withStore $ \db -> mergeContactRecords db user c1' c2' - toView $ CRContactsMerged user c1' c2' c2'' - when (directOrUsed c2'') $ showSecurityCodeChanged c2'' - pure $ Just c2'' - where - showSecurityCodeChanged mergedCt = do - let sc1_ = contactSecurityCode c1' - sc2_ = contactSecurityCode c2' - scMerged_ = contactSecurityCode mergedCt - case (sc1_, sc2_) of - (Just sc1, Nothing) - | scMerged_ /= Just sc1 -> securityCodeChanged mergedCt - | otherwise -> pure () - (Nothing, Just sc2) - | scMerged_ /= Just sc2 -> securityCodeChanged mergedCt - | otherwise -> pure () - _ -> pure () - - associateMemberAndContact :: Contact -> GroupMember -> m (Maybe Contact) - associateMemberAndContact c m = do - let Contact {localDisplayName = cLDN, profile = LocalProfile {displayName}} = c - GroupMember {localDisplayName = mLDN} = m - case (suffixOrd displayName cLDN, suffixOrd displayName mLDN) of - (Just cOrd, Just mOrd) - | cOrd < mOrd -> Just <$> associateMemberWithContact c m - | mOrd < cOrd -> Just <$> associateContactWithMember m c - | otherwise -> pure Nothing - _ -> pure Nothing - - suffixOrd :: ContactName -> ContactName -> Maybe Int - suffixOrd displayName localDisplayName - | localDisplayName == displayName = Just 0 - | otherwise = case T.stripPrefix (displayName <> "_") localDisplayName of - Just suffix -> readMaybe $ T.unpack suffix - Nothing -> Nothing - - associateMemberWithContact :: Contact -> GroupMember -> m Contact - associateMemberWithContact c1 m2@GroupMember {groupId} = do - withStore' $ \db -> associateMemberWithContactRecord db user c1 m2 - g <- withStore $ \db -> getGroupInfo db vr user groupId - toView $ CRContactAndMemberAssociated user c1 g m2 c1 - pure c1 - - associateContactWithMember :: GroupMember -> Contact -> m Contact - associateContactWithMember m1@GroupMember {groupId} c2 = do - c2' <- withStore $ \db -> associateContactWithMemberRecord db user m1 c2 - g <- withStore $ \db -> getGroupInfo db vr user groupId - toView $ CRContactAndMemberAssociated user c2 g m1 c2' - pure c2' - - saveConnInfo :: Connection -> ConnInfo -> m Connection - saveConnInfo activeConn connInfo = do - ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage activeConn connInfo - conn' <- updatePeerChatVRange activeConn chatVRange - case chatMsgEvent of - XInfo p -> do - let contactUsed = connDirect activeConn - ct <- withStore $ \db -> createDirectContact db user conn' p contactUsed - toView $ CRContactConnecting user ct - pure conn' - XGrpLinkInv glInv -> do - (gInfo, host) <- withStore $ \db -> createGroupInvitedViaLink db vr user conn' glInv - toView $ CRGroupLinkConnecting user gInfo host - pure conn' - -- TODO show/log error, other events in SMP confirmation - _ -> pure conn' - - xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> RcvMessage -> UTCTime -> m () - xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ memberProfile) msg brokerTs = do - checkHostRole m memRole - unless (sameMemberId memId $ membership gInfo) $ - withStore' (\db -> runExceptT $ getGroupMemberByMemberId db user gInfo memId) >>= \case - Right _ -> messageError "x.grp.mem.new error: member already exists" - Left _ -> do - newMember@GroupMember {groupMemberId} <- withStore $ \db -> createNewGroupMember db user gInfo m memInfo GCPostMember GSMemAnnounced - ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent $ RGEMemberAdded groupMemberId memberProfile) - groupMsgToView gInfo ci - toView $ CRJoinedGroupMemberConnecting user gInfo m newMember - - xGrpMemIntro :: GroupInfo -> GroupMember -> MemberInfo -> m () - xGrpMemIntro gInfo@GroupInfo {chatSettings} m@GroupMember {memberRole, localDisplayName = c} memInfo@(MemberInfo memId _ memChatVRange _) = do - case memberCategory m of - GCHostMember -> - withStore' (\db -> runExceptT $ getGroupMemberByMemberId db user gInfo memId) >>= \case - Right _ -> messageError "x.grp.mem.intro ignored: member already exists" - Left _ -> do - when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole c) - subMode <- chatReadVar subscriptionMode - -- [async agent commands] commands should be asynchronous, continuation is to send XGrpMemInv - have to remember one has completed and process on second - groupConnIds <- createConn subMode - directConnIds <- case memChatVRange of - Nothing -> Just <$> createConn subMode - Just mcvr - | isCompatibleRange (fromChatVRange mcvr) groupNoDirectVRange -> pure Nothing - | otherwise -> Just <$> createConn subMode - let customUserProfileId = localProfileId <$> incognitoMembershipProfile gInfo - void $ withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnIds directConnIds customUserProfileId subMode - _ -> messageError "x.grp.mem.intro can be only sent by host member" - where - createConn subMode = createAgentConnectionAsync user CFCreateConnGrpMemInv (chatHasNtfs chatSettings) SCMInvitation subMode - - sendXGrpMemInv :: Int64 -> Maybe ConnReqInvitation -> XGrpMemIntroCont -> m () - sendXGrpMemInv hostConnId directConnReq XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} = do - hostConn <- withStore $ \db -> getConnectionById db user hostConnId - let msg = XGrpMemInv memberId IntroInvitation {groupConnReq, directConnReq} - void $ sendDirectMessage hostConn msg (GroupId groupId) - withStore' $ \db -> updateGroupMemberStatusById db userId groupMemberId GSMemIntroInvited - - xGrpMemInv :: GroupInfo -> GroupMember -> MemberId -> IntroInvitation -> m () - xGrpMemInv gInfo@GroupInfo {groupId} m memId introInv = do - case memberCategory m of - GCInviteeMember -> - withStore' (\db -> runExceptT $ getGroupMemberByMemberId db user gInfo memId) >>= \case - Left _ -> messageError "x.grp.mem.inv error: referenced member does not exist" - Right reMember -> do - GroupMemberIntro {introId} <- withStore $ \db -> saveIntroInvitation db reMember m introInv - sendGroupMemberMessage user reMember (XGrpMemFwd (memberInfo m) introInv) groupId (Just introId) $ - withStore' $ - \db -> updateIntroStatus db introId GMIntroInvForwarded - _ -> messageError "x.grp.mem.inv can be only sent by invitee member" - - xGrpMemFwd :: GroupInfo -> GroupMember -> MemberInfo -> IntroInvitation -> m () - xGrpMemFwd gInfo@GroupInfo {membership, chatSettings} m memInfo@(MemberInfo memId memRole memChatVRange _) introInv@IntroInvitation {groupConnReq, directConnReq} = do - checkHostRole m memRole - toMember <- - withStore' (\db -> runExceptT $ getGroupMemberByMemberId db user gInfo memId) >>= \case - -- TODO if the missed messages are correctly sent as soon as there is connection before anything else is sent - -- the situation when member does not exist is an error - -- member receiving x.grp.mem.fwd should have also received x.grp.mem.new prior to that. - -- For now, this branch compensates for the lack of delayed message delivery. - Left _ -> withStore $ \db -> createNewGroupMember db user gInfo m memInfo GCPostMember GSMemAnnounced - Right m' -> pure m' - withStore' $ \db -> saveMemberInvitation db toMember introInv - subMode <- chatReadVar subscriptionMode - -- [incognito] send membership incognito profile, create direct connection as incognito - dm <- directMessage $ XGrpMemInfo membership.memberId (fromLocalProfile $ memberProfile membership) - -- [async agent commands] no continuation needed, but commands should be asynchronous for stability - groupConnIds <- joinAgentConnectionAsync user (chatHasNtfs chatSettings) groupConnReq dm subMode - directConnIds <- forM directConnReq $ \dcr -> joinAgentConnectionAsync user True dcr dm subMode - let customUserProfileId = localProfileId <$> incognitoMembershipProfile gInfo - mcvr = maybe chatInitialVRange fromChatVRange memChatVRange - withStore' $ \db -> createIntroToMemberContact db user m toMember mcvr groupConnIds directConnIds customUserProfileId subMode - - xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> RcvMessage -> UTCTime -> m () - xGrpMemRole gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId memRole msg brokerTs - | membership.memberId == memId = - let gInfo' = gInfo {membership = membership {memberRole = memRole}} - in changeMemberRole gInfo' membership $ RGEUserRole memRole - | otherwise = - withStore' (\db -> runExceptT $ getGroupMemberByMemberId db user gInfo memId) >>= \case - Right member -> changeMemberRole gInfo member $ RGEMemberRole (groupMemberId' member) (fromLocalProfile $ memberProfile member) memRole - Left _ -> messageError "x.grp.mem.role with unknown member ID" - where - changeMemberRole gInfo' member@GroupMember {memberRole = fromRole} gEvent - | senderRole < GRAdmin || senderRole < fromRole = messageError "x.grp.mem.role with insufficient member permissions" - | otherwise = do - withStore' $ \db -> updateGroupMemberRole db user member memRole - ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent gEvent) - groupMsgToView gInfo ci - toView CRMemberRole {user, groupInfo = gInfo', byMember = m, member = member {memberRole = memRole}, fromRole, toRole = memRole} - - checkHostRole :: GroupMember -> GroupMemberRole -> m () - checkHostRole GroupMember {memberRole, localDisplayName} memRole = - when (memberRole < GRAdmin || memberRole < memRole) $ throwChatError (CEGroupContactRole localDisplayName) - - xGrpMemCon :: GroupInfo -> GroupMember -> MemberId -> m () - xGrpMemCon gInfo sendingMember memId = do - refMember <- withStore $ \db -> getGroupMemberByMemberId db user gInfo memId - case (memberCategory sendingMember, memberCategory refMember) of - (GCInviteeMember, GCInviteeMember) -> - withStore' (\db -> runExceptT $ getIntroduction db refMember sendingMember) >>= \case - Right intro -> inviteeXGrpMemCon intro - Left _ -> - withStore' (\db -> runExceptT $ getIntroduction db sendingMember refMember) >>= \case - Right intro -> forwardMemberXGrpMemCon intro - Left _ -> messageWarning "x.grp.mem.con: no introduction" - (GCInviteeMember, _) -> - withStore' (\db -> runExceptT $ getIntroduction db refMember sendingMember) >>= \case - Right intro -> inviteeXGrpMemCon intro - Left _ -> messageWarning "x.grp.mem.con: no introduction" - (_, GCInviteeMember) -> - withStore' (\db -> runExceptT $ getIntroduction db sendingMember refMember) >>= \case - Right intro -> forwardMemberXGrpMemCon intro - Left _ -> messageWarning "x.grp.mem.con: no introductiosupportn" - -- Note: we can allow XGrpMemCon to all member categories if we decide to support broader group forwarding, - -- deduplication (see saveGroupRcvMsg, saveGroupFwdRcvMsg) already supports sending XGrpMemCon - -- to any forwarding member, not only host/inviting member; - -- database would track all members connections then - -- (currently it's done via group_member_intros for introduced connections only) - _ -> - messageWarning "x.grp.mem.con: neither member is invitee" - where - inviteeXGrpMemCon :: GroupMemberIntro -> m () - inviteeXGrpMemCon GroupMemberIntro {introId, introStatus} - | introStatus == GMIntroReConnected = updateStatus introId GMIntroConnected - | introStatus `elem` [GMIntroToConnected, GMIntroConnected] = pure () - | otherwise = updateStatus introId GMIntroToConnected - forwardMemberXGrpMemCon :: GroupMemberIntro -> m () - forwardMemberXGrpMemCon GroupMemberIntro {introId, introStatus} - | introStatus == GMIntroToConnected = updateStatus introId GMIntroConnected - | introStatus `elem` [GMIntroReConnected, GMIntroConnected] = pure () - | otherwise = updateStatus introId GMIntroReConnected - updateStatus introId status = withStore' $ \db -> updateIntroStatus db introId status - - xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> RcvMessage -> UTCTime -> m () - xGrpMemDel gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId msg brokerTs = do - if membership.memberId == memId - then checkRole membership $ do - deleteGroupLinkIfExists user gInfo - -- member records are not deleted to keep history - members <- withStore' $ \db -> getGroupMembers db user gInfo - deleteMembersConnections user members - withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemRemoved - deleteMemberItem RGEUserDeleted - toView $ CRDeletedMemberUser user gInfo {membership = membership {memberStatus = GSMemRemoved}} m - else - withStore' (\db -> runExceptT $ getGroupMemberByMemberId db user gInfo memId) >>= \case - Left _ -> messageError "x.grp.mem.del with unknown member ID" - Right member@GroupMember {groupMemberId, memberProfile} -> - checkRole member $ do - -- ? prohibit deleting member if it's the sender - sender should use x.grp.leave - deleteMemberConnection user member - -- undeleted "member connected" chat item will prevent deletion of member record - deleteOrUpdateMemberRecord user member - deleteMemberItem $ RGEMemberDeleted groupMemberId (fromLocalProfile memberProfile) - toView $ CRDeletedMember user gInfo m member {memberStatus = GSMemRemoved} - where - checkRole GroupMember {memberRole} a - | senderRole < GRAdmin || senderRole < memberRole = - messageError "x.grp.mem.del with insufficient member permissions" - | otherwise = a - deleteMemberItem gEvent = do - ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent gEvent) - groupMsgToView gInfo ci - - xGrpLeave :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> m () - xGrpLeave gInfo m msg brokerTs = do - deleteMemberConnection user m - -- member record is not deleted to allow creation of "member left" chat item - withStore' $ \db -> updateGroupMemberStatus db userId m GSMemLeft - ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent RGEMemberLeft) - groupMsgToView gInfo ci - toView $ CRLeftMember user gInfo m {memberStatus = GSMemLeft} - - xGrpDel :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> m () - xGrpDel gInfo@GroupInfo {membership} m@GroupMember {memberRole} msg brokerTs = do - when (memberRole /= GROwner) $ throwChatError $ CEGroupUserRole gInfo GROwner - ms <- withStore' $ \db -> do - members <- getGroupMembers db user gInfo - updateGroupMemberStatus db userId membership GSMemGroupDeleted - pure members - -- member records are not deleted to keep history - deleteMembersConnections user ms - ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent RGEGroupDeleted) - groupMsgToView gInfo ci - toView $ CRGroupDeleted user gInfo {membership = membership {memberStatus = GSMemGroupDeleted}} m - - xGrpInfo :: GroupInfo -> GroupMember -> GroupProfile -> RcvMessage -> UTCTime -> m () - xGrpInfo g@GroupInfo {groupProfile = p} m@GroupMember {memberRole} p' msg brokerTs - | memberRole < GROwner = messageError "x.grp.info with insufficient member permissions" - | otherwise = unless (p == p') $ do - g' <- withStore $ \db -> updateGroupProfile db user g p' - toView $ CRGroupUpdated user g g' (Just m) - let cd = CDGroupRcv g' m - unless (sameGroupProfileInfo p p') $ do - ci <- saveRcvChatItem user cd msg brokerTs (CIRcvGroupEvent $ RGEGroupUpdated p') - groupMsgToView g' ci - createGroupFeatureChangedItems user cd CIRcvGroupFeature g g' - - xGrpDirectInv :: GroupInfo -> GroupMember -> Connection -> ConnReqInvitation -> Maybe MsgContent -> RcvMessage -> UTCTime -> m () - xGrpDirectInv g m mConn connReq mContent_ msg brokerTs = do - unless (groupFeatureAllowed SGFDirectMessages g) $ messageError "x.grp.direct.inv: direct messages not allowed" - let GroupMember {memberContactId} = m - subMode <- chatReadVar subscriptionMode - case memberContactId of - Nothing -> createNewContact subMode - Just mContactId -> do - mCt <- withStore $ \db -> getContact db user mContactId - let Contact {activeConn, contactGrpInvSent} = mCt - forM_ activeConn $ \Connection {connId} -> - if contactGrpInvSent - then do - ownConnReq <- withStore $ \db -> getConnReqInv db connId - -- in case both members sent x.grp.direct.inv before receiving other's for processing, - -- only the one who received greater connReq joins, the other creates items and waits for confirmation - if strEncode connReq > strEncode ownConnReq - then joinExistingContact subMode mCt - else createItems mCt m - else joinExistingContact subMode mCt - where - joinExistingContact subMode mCt = do - connIds <- joinConn subMode - mCt' <- withStore $ \db -> updateMemberContactInvited db user connIds g mConn mCt subMode - createItems mCt' m - securityCodeChanged mCt' - createNewContact subMode = do - connIds <- joinConn subMode - -- [incognito] reuse membership incognito profile - (mCt', m') <- withStore' $ \db -> createMemberContactInvited db user connIds g m mConn subMode - createItems mCt' m' - joinConn subMode = do - -- [incognito] send membership incognito profile - let p = userProfileToSend user (fromLocalProfile <$> incognitoMembershipProfile g) Nothing - dm <- directMessage $ XInfo p - joinAgentConnectionAsync user True connReq dm subMode - createItems mCt' m' = do - createInternalChatItem user (CDGroupRcv g m') (CIRcvGroupEvent RGEMemberCreatedContact) Nothing - toView $ CRNewMemberContactReceivedInv user mCt' g m' - forM_ mContent_ $ \mc -> do - ci <- saveRcvChatItem user (CDDirectRcv mCt') msg brokerTs (CIRcvMsgContent mc) - toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat mCt') ci) - - securityCodeChanged :: Contact -> m () - securityCodeChanged ct = do - toView $ CRContactVerificationReset user ct - createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent RCEVerificationCodeReset) Nothing - - xGrpMsgForward :: GroupInfo -> GroupMember -> MemberId -> ChatMessage 'Json -> UTCTime -> m () - xGrpMsgForward gInfo@GroupInfo {groupId} m memberId msg msgTs = do - when (m.memberRole < GRAdmin) $ throwChatError (CEGroupContactRole m.localDisplayName) - author <- withStore $ \db -> getGroupMemberByMemberId db user gInfo memberId - processForwardedMsg author msg - where - -- Note: forwarded group events (see forwardedGroupMsg) should include msgId to be deduplicated - processForwardedMsg :: GroupMember -> ChatMessage 'Json -> m () - processForwardedMsg author chatMsg = do - let body = LB.toStrict $ J.encode msg - rcvMsg@RcvMessage {chatMsgEvent = ACME _ event} <- saveGroupFwdRcvMsg user groupId m author body chatMsg - case event of - XMsgNew mc -> memberCanSend author $ newGroupContentMessage gInfo author mc rcvMsg msgTs True - XMsgFileDescr sharedMsgId fileDescr -> memberCanSend author $ groupMessageFileDescription gInfo author sharedMsgId fileDescr - XMsgUpdate sharedMsgId mContent ttl live -> memberCanSend author $ groupMessageUpdate gInfo author sharedMsgId mContent rcvMsg msgTs ttl live - XMsgDel sharedMsgId memId -> groupMessageDelete gInfo author sharedMsgId memId rcvMsg msgTs - XMsgReact sharedMsgId (Just memId) reaction add -> groupMsgReaction gInfo author sharedMsgId memId reaction add rcvMsg msgTs - XFileCancel sharedMsgId -> xFileCancelGroup gInfo author sharedMsgId - XInfo p -> xInfoMember gInfo author p - XGrpMemNew memInfo -> xGrpMemNew gInfo author memInfo rcvMsg msgTs - XGrpMemRole memId memRole -> xGrpMemRole gInfo author memId memRole rcvMsg msgTs - XGrpMemDel memId -> xGrpMemDel gInfo author memId rcvMsg msgTs - XGrpLeave -> xGrpLeave gInfo author rcvMsg msgTs - XGrpDel -> xGrpDel gInfo author rcvMsg msgTs - XGrpInfo p' -> xGrpInfo gInfo author p' rcvMsg msgTs - _ -> messageError $ "x.grp.msg.forward: unsupported forwarded event " <> T.pack (show $ toCMEventTag event) - - directMsgReceived :: Contact -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> m () - directMsgReceived ct conn@Connection {connId} msgMeta msgRcpts = do - checkIntegrityCreateItem (CDDirectRcv ct) msgMeta - forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do - withStore' $ \db -> updateSndMsgDeliveryStatus db connId agentMsgId $ MDSSndRcvd msgRcptStatus - updateDirectItemStatus ct conn agentMsgId $ CISSndRcvd msgRcptStatus SSPComplete - - -- TODO [batch send] update status of all messages in batch - -- - this is for when we implement identifying inactive connections - -- - regular messages sent in batch would all be marked as delivered by a single receipt - -- - repeat for directMsgReceived if same logic is applied to direct messages - -- - getChatItemIdByAgentMsgId to return [ChatItemId] - groupMsgReceived :: GroupInfo -> GroupMember -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> m () - groupMsgReceived gInfo m conn@Connection {connId} msgMeta msgRcpts = do - checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta - forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do - withStore' $ \db -> updateSndMsgDeliveryStatus db connId agentMsgId $ MDSSndRcvd msgRcptStatus - updateGroupItemStatus gInfo m conn agentMsgId $ CISSndRcvd msgRcptStatus SSPComplete - - updateDirectItemStatus :: Contact -> Connection -> AgentMsgId -> CIStatus 'MDSnd -> m () - updateDirectItemStatus ct@Contact {contactId} Connection {connId} msgId newStatus = - withStore' (\db -> getDirectChatItemByAgentMsgId db user contactId connId msgId) >>= \case - Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemStatus = CISSndRcvd _ _}}) -> pure () - Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemId, itemStatus}}) - | itemStatus == newStatus -> pure () - | otherwise -> do - chatItem <- withStore $ \db -> updateDirectChatItemStatus db user ct itemId newStatus - toView $ CRChatItemStatusUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem) - _ -> pure () - - updateGroupMemSndStatus :: ChatItemId -> GroupMemberId -> CIStatus 'MDSnd -> m Bool - updateGroupMemSndStatus itemId groupMemberId newStatus = - runExceptT (withStore $ \db -> getGroupSndStatus db itemId groupMemberId) >>= \case - Right (CISSndRcvd _ _) -> pure False - Right memStatus - | memStatus == newStatus -> pure False - | otherwise -> withStore' (\db -> updateGroupSndStatus db itemId groupMemberId newStatus) $> True - _ -> pure False - - updateGroupItemStatus :: GroupInfo -> GroupMember -> Connection -> AgentMsgId -> CIStatus 'MDSnd -> m () - updateGroupItemStatus gInfo@GroupInfo {groupId} GroupMember {groupMemberId} Connection {connId} msgId newMemStatus = - withStore' (\db -> getGroupChatItemByAgentMsgId db user groupId connId msgId) >>= \case - Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemStatus = CISSndRcvd _ SSPComplete}}) -> pure () - Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemId, itemStatus}}) -> do - memStatusChanged <- updateGroupMemSndStatus itemId groupMemberId newMemStatus - when memStatusChanged $ do - memStatusCounts <- withStore' (`getGroupSndStatusCounts` itemId) - let newStatus = membersGroupItemStatus memStatusCounts - when (newStatus /= itemStatus) $ do - chatItem <- withStore $ \db -> updateGroupChatItemStatus db user gInfo itemId newStatus - toView $ CRChatItemStatusUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) chatItem) - _ -> pure () - -metaBrokerTs :: MsgMeta -> UTCTime -metaBrokerTs MsgMeta {broker = (_, brokerTs)} = brokerTs - -sameMemberId :: MemberId -> GroupMember -> Bool -sameMemberId memId GroupMember {memberId} = memId == memberId - -updatePeerChatVRange :: ChatMonad m => Connection -> VersionRange -> m Connection -updatePeerChatVRange conn@Connection {connId, peerChatVRange} msgChatVRange = do - let jMsgChatVRange = JVersionRange msgChatVRange - if jMsgChatVRange /= peerChatVRange - then do - withStore' $ \db -> setPeerChatVRange db connId msgChatVRange - pure conn {peerChatVRange = jMsgChatVRange} - else pure conn - -updateMemberChatVRange :: ChatMonad m => GroupMember -> Connection -> VersionRange -> m (GroupMember, Connection) -updateMemberChatVRange mem@GroupMember {groupMemberId} conn@Connection {connId, peerChatVRange} msgChatVRange = do - let jMsgChatVRange = JVersionRange msgChatVRange - if jMsgChatVRange /= peerChatVRange - then do - withStore' $ \db -> do - setPeerChatVRange db connId msgChatVRange - setMemberChatVRange db groupMemberId msgChatVRange - let conn' = conn {peerChatVRange = jMsgChatVRange} - pure (mem {memberChatVRange = jMsgChatVRange, activeConn = Just conn'}, conn') - else pure (mem, conn) - -parseFileDescription :: (ChatMonad m, FilePartyI p) => Text -> m (ValidFileDescription p) -parseFileDescription = - liftEither . first (ChatError . CEInvalidFileDescription) . (strDecode . encodeUtf8) - -sendDirectFileInline :: ChatMonad m => Contact -> FileTransferMeta -> SharedMsgId -> m () -sendDirectFileInline ct ft sharedMsgId = do - msgDeliveryId <- sendFileInline_ ft sharedMsgId $ sendDirectContactMessage ct - withStore $ \db -> updateSndDirectFTDelivery db ct ft msgDeliveryId - -sendMemberFileInline :: ChatMonad m => GroupMember -> Connection -> FileTransferMeta -> SharedMsgId -> m () -sendMemberFileInline m@GroupMember {groupId} conn ft sharedMsgId = do - msgDeliveryId <- sendFileInline_ ft sharedMsgId $ \msg -> sendDirectMessage conn msg $ GroupId groupId - withStore' $ \db -> updateSndGroupFTDelivery db m conn ft msgDeliveryId - -sendFileInline_ :: ChatMonad m => FileTransferMeta -> SharedMsgId -> (ChatMsgEvent 'Binary -> m (SndMessage, Int64)) -> m Int64 -sendFileInline_ FileTransferMeta {filePath, chunkSize} sharedMsgId sendMsg = - sendChunks 1 =<< liftIO . B.readFile =<< toFSFilePath filePath - where - sendChunks chunkNo bytes = do - let (chunk, rest) = B.splitAt chSize bytes - (_, msgDeliveryId) <- sendMsg $ BFileChunk sharedMsgId $ FileChunk chunkNo chunk - if B.null rest - then pure msgDeliveryId - else sendChunks (chunkNo + 1) rest - chSize = fromIntegral chunkSize - -parseChatMessage :: ChatMonad m => Connection -> ByteString -> m (ChatMessage 'Json) -parseChatMessage conn s = do - case parseChatMessages s of - [msg] -> liftEither . first (ChatError . errType) $ (\(ACMsg _ m) -> checkEncoding m) =<< msg - _ -> throwChatError $ CEException "parseChatMessage: single message is expected" - where - errType = CEInvalidChatMessage conn Nothing (safeDecodeUtf8 s) -{-# INLINE parseChatMessage #-} - -sendFileChunk :: ChatMonad m => User -> SndFileTransfer -> m () -sendFileChunk user ft@SndFileTransfer {fileId, fileStatus, agentConnId = AgentConnId acId} = - unless (fileStatus == FSComplete || fileStatus == FSCancelled) $ do - vr <- chatVersionRange - withStore' (`createSndFileChunk` ft) >>= \case - Just chunkNo -> sendFileChunkNo ft chunkNo - Nothing -> do - ci <- withStore $ \db -> do - liftIO $ updateSndFileStatus db ft FSComplete - liftIO $ deleteSndFileChunks db ft - updateDirectCIFileStatus db vr user fileId CIFSSndComplete - toView $ CRSndFileComplete user ci ft - closeFileHandle fileId sndFiles - deleteAgentConnectionAsync user acId - -sendFileChunkNo :: ChatMonad m => SndFileTransfer -> Integer -> m () -sendFileChunkNo ft@SndFileTransfer {agentConnId = AgentConnId acId} chunkNo = do - chunkBytes <- readFileChunk ft chunkNo - msgId <- withAgent $ \a -> sendMessage a acId SMP.noMsgFlags $ smpEncode FileChunk {chunkNo, chunkBytes} - withStore' $ \db -> updateSndFileChunkMsg db ft chunkNo msgId - -readFileChunk :: ChatMonad m => SndFileTransfer -> Integer -> m ByteString -readFileChunk SndFileTransfer {fileId, filePath, chunkSize} chunkNo = do - fsFilePath <- toFSFilePath filePath - read_ fsFilePath `catchThrow` (ChatError . CEFileRead filePath . show) - where - read_ fsFilePath = do - h <- getFileHandle fileId fsFilePath sndFiles ReadMode - pos <- hTell h - let pos' = (chunkNo - 1) * chunkSize - when (pos /= pos') $ hSeek h AbsoluteSeek pos' - liftIO . B.hGet h $ fromInteger chunkSize - -parseFileChunk :: ChatMonad m => ByteString -> m FileChunk -parseFileChunk = liftEither . first (ChatError . CEFileRcvChunk) . smpDecode - -appendFileChunk :: forall m. ChatMonad m => RcvFileTransfer -> Integer -> ByteString -> Bool -> m () -appendFileChunk ft@RcvFileTransfer {fileId, fileStatus, cryptoArgs} chunkNo chunk final = - case fileStatus of - RFSConnected RcvFileInfo {filePath} -> append_ filePath - -- sometimes update of file transfer status to FSConnected - -- doesn't complete in time before MSG with first file chunk - RFSAccepted RcvFileInfo {filePath} -> append_ filePath - RFSCancelled _ -> pure () - _ -> throwChatError $ CEFileInternal "receiving file transfer not in progress" - where - append_ :: FilePath -> m () - append_ filePath = do - fsFilePath <- toFSFilePath filePath - h <- getFileHandle fileId fsFilePath rcvFiles AppendMode - liftIO (B.hPut h chunk >> hFlush h) `catchThrow` (fileErr . show) - withStore' $ \db -> updatedRcvFileChunkStored db ft chunkNo - when final $ do - closeFileHandle fileId rcvFiles - forM_ cryptoArgs $ \cfArgs -> do - tmpFile <- getChatTempDirectory >>= (`uniqueCombine` ft.fileInvitation.fileName) - tryChatError (liftError encryptErr $ encryptFile fsFilePath tmpFile cfArgs) >>= \case - Right () -> do - removeFile fsFilePath `catchChatError` \_ -> pure () - renameFile tmpFile fsFilePath - Left e -> do - toView $ CRChatError Nothing e - removeFile tmpFile `catchChatError` \_ -> pure () - withStore' (`removeFileCryptoArgs` fileId) - where - encryptErr e = fileErr $ e <> ", received file not encrypted" - fileErr = ChatError . CEFileWrite filePath - -getFileHandle :: ChatMonad m => Int64 -> FilePath -> (ChatController -> TVar (Map Int64 Handle)) -> IOMode -> m Handle -getFileHandle fileId filePath files ioMode = do - fs <- asks files - h_ <- M.lookup fileId <$> readTVarIO fs - maybe (newHandle fs) pure h_ - where - newHandle fs = do - h <- openFile filePath ioMode `catchThrow` (ChatError . CEFileInternal . show) - atomically . modifyTVar fs $ M.insert fileId h - pure h - -isFileActive :: ChatMonad m => Int64 -> (ChatController -> TVar (Map Int64 Handle)) -> m Bool -isFileActive fileId files = do - fs <- asks files - isJust . M.lookup fileId <$> readTVarIO fs - -cancelRcvFileTransfer :: ChatMonad m => User -> RcvFileTransfer -> m (Maybe ConnId) -cancelRcvFileTransfer user ft@RcvFileTransfer {fileId, xftpRcvFile, rcvFileInline} = - cancel' `catchChatError` (\e -> toView (CRChatError (Just user) e) $> fileConnId) - where - cancel' = do - closeFileHandle fileId rcvFiles - withStore' $ \db -> do - updateFileCancelled db user fileId CIFSRcvCancelled - updateRcvFileStatus db fileId FSCancelled - deleteRcvFileChunks db ft - case xftpRcvFile of - Just XFTPRcvFile {agentRcvFileId = Just (AgentRcvFileId aFileId), agentRcvFileDeleted} -> - unless agentRcvFileDeleted $ agentXFTPDeleteRcvFile aFileId fileId - _ -> pure () - pure fileConnId - fileConnId = if isNothing xftpRcvFile && isNothing rcvFileInline then liveRcvFileTransferConnId ft else Nothing - -cancelSndFile :: ChatMonad m => User -> FileTransferMeta -> [SndFileTransfer] -> Bool -> m [ConnId] -cancelSndFile user FileTransferMeta {fileId, xftpSndFile} fts sendCancel = do - withStore' (\db -> updateFileCancelled db user fileId CIFSSndCancelled) - `catchChatError` (toView . CRChatError (Just user)) - case xftpSndFile of - Nothing -> - catMaybes <$> forM fts (\ft -> cancelSndFileTransfer user ft sendCancel) - Just xsf -> do - forM_ fts (\ft -> cancelSndFileTransfer user ft False) - agentXFTPDeleteSndFileRemote user xsf fileId `catchChatError` (toView . CRChatError (Just user)) - pure [] - -cancelSndFileTransfer :: ChatMonad m => User -> SndFileTransfer -> Bool -> m (Maybe ConnId) -cancelSndFileTransfer user@User {userId} ft@SndFileTransfer {fileId, connId, agentConnId = AgentConnId acId, fileStatus, fileInline} sendCancel = - if fileStatus == FSCancelled || fileStatus == FSComplete - then pure Nothing - else cancel' `catchChatError` (\e -> toView (CRChatError (Just user) e) $> fileConnId) - where - cancel' = do - withStore' $ \db -> do - updateSndFileStatus db ft FSCancelled - deleteSndFileChunks db ft - when sendCancel $ case fileInline of - Just _ -> do - (sharedMsgId, conn) <- withStore $ \db -> (,) <$> getSharedMsgIdByFileId db userId fileId <*> getConnectionById db user connId - void . sendDirectMessage conn (BFileChunk sharedMsgId FileChunkCancel) $ ConnectionId connId - _ -> withAgent $ \a -> void . sendMessage a acId SMP.noMsgFlags $ smpEncode FileChunkCancel - pure fileConnId - fileConnId = if isNothing fileInline then Just acId else Nothing - -closeFileHandle :: ChatMonad m => Int64 -> (ChatController -> TVar (Map Int64 Handle)) -> m () -closeFileHandle fileId files = do - fs <- asks files - h_ <- atomically . stateTVar fs $ \m -> (M.lookup fileId m, M.delete fileId m) - liftIO $ mapM_ hClose h_ `catchAll_` pure () - -deleteMembersConnections :: ChatMonad m => User -> [GroupMember] -> m () -deleteMembersConnections user members = do - let memberConns = - filter (\Connection {connStatus} -> connStatus /= ConnDeleted) $ - mapMaybe (\GroupMember {activeConn} -> activeConn) members - deleteAgentConnectionsAsync user $ map aConnId memberConns - forM_ memberConns $ \conn -> withStore' $ \db -> updateConnectionStatus db conn ConnDeleted - -deleteMemberConnection :: ChatMonad m => User -> GroupMember -> m () -deleteMemberConnection user GroupMember {activeConn} = do - forM_ activeConn $ \conn -> do - deleteAgentConnectionAsync user $ aConnId conn - withStore' $ \db -> updateConnectionStatus db conn ConnDeleted - -deleteOrUpdateMemberRecord :: ChatMonad m => User -> GroupMember -> m () -deleteOrUpdateMemberRecord user@User {userId} member = - withStore' $ \db -> - checkGroupMemberHasItems db user member >>= \case - Just _ -> updateGroupMemberStatus db userId member GSMemRemoved - Nothing -> deleteGroupMember db user member - -sendDirectContactMessage :: (MsgEncodingI e, ChatMonad m) => Contact -> ChatMsgEvent e -> m (SndMessage, Int64) -sendDirectContactMessage ct@Contact {activeConn = Nothing} _ = throwChatError $ CEContactNotReady ct -sendDirectContactMessage ct@Contact {activeConn = Just conn@Connection {connId, connStatus}, contactStatus} chatMsgEvent - | connStatus /= ConnReady && connStatus /= ConnSndReady = throwChatError $ CEContactNotReady ct - | contactStatus /= CSActive = throwChatError $ CEContactNotActive ct - | connDisabled conn = throwChatError $ CEContactDisabled ct - | otherwise = sendDirectMessage conn chatMsgEvent (ConnectionId connId) - -sendDirectMessage :: (MsgEncodingI e, ChatMonad m) => Connection -> ChatMsgEvent e -> ConnOrGroupId -> m (SndMessage, Int64) -sendDirectMessage conn chatMsgEvent connOrGroupId = do - when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn) - msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent connOrGroupId - (msg,) <$> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId - -createSndMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> ConnOrGroupId -> m SndMessage -createSndMessage chatMsgEvent connOrGroupId = do - gVar <- asks random - vr <- chatVersionRange - withStore $ \db -> createNewSndMessage db gVar connOrGroupId chatMsgEvent (encodeMessage vr) - where - encodeMessage chatVRange sharedMsgId = - encodeChatMessage ChatMessage {chatVRange, msgId = Just sharedMsgId, chatMsgEvent} - -sendGroupMemberMessages :: forall e m. (MsgEncodingI e, ChatMonad m) => User -> Connection -> NonEmpty (ChatMsgEvent e) -> GroupId -> m () -sendGroupMemberMessages user conn@Connection {connId} events groupId = do - when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn) - (errs, msgs) <- partitionEithers <$> createSndMessages - unless (null errs) $ toView $ CRChatErrors (Just user) errs - unless (null msgs) $ do - let (errs', msgBatches) = partitionEithers $ batchMessages maxChatMsgSize msgs - -- shouldn't happen, as large messages would have caused createNewSndMessage to throw SELargeMsg - unless (null errs') $ toView $ CRChatErrors (Just user) errs' - forM_ msgBatches $ \batch -> - processBatch batch `catchChatError` (toView . CRChatError (Just user)) - where - processBatch :: MsgBatch -> m () - processBatch (MsgBatch builder sndMsgs) = do - let batchBody = LB.toStrict $ toLazyByteString builder - agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) MsgFlags {notification = True} batchBody - let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId} - void . withStoreBatch' $ \db -> map (\SndMessage {msgId} -> createSndMsgDelivery db sndMsgDelivery msgId) sndMsgs - createSndMessages :: m [Either ChatError SndMessage] - createSndMessages = do - gVar <- asks random - vr <- chatVersionRange - withStoreBatch $ \db -> map (createMsg db gVar vr) (toList events) - createMsg db gVar chatVRange evnt = do - r <- runExceptT $ createNewSndMessage db gVar (GroupId groupId) evnt (encodeMessage chatVRange evnt) - pure $ first ChatErrorStore r - encodeMessage chatVRange evnt sharedMsgId = - encodeChatMessage ChatMessage {chatVRange, msgId = Just sharedMsgId, chatMsgEvent = evnt} - -directMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> m ByteString -directMessage chatMsgEvent = do - chatVRange <- chatVersionRange - let r = encodeChatMessage ChatMessage {chatVRange, msgId = Nothing, chatMsgEvent} - case r of - ECMEncoded encodedBody -> pure . LB.toStrict $ encodedBody - ECMLarge -> throwChatError $ CEException "large message" - -deliverMessage :: ChatMonad m => Connection -> CMEventTag e -> LazyMsgBody -> MessageId -> m Int64 -deliverMessage conn cmEventTag msgBody msgId = do - let msgFlags = MsgFlags {notification = hasNotification cmEventTag} - deliverMessage' conn msgFlags msgBody msgId - -deliverMessage' :: ChatMonad m => Connection -> MsgFlags -> LazyMsgBody -> MessageId -> m Int64 -deliverMessage' conn msgFlags msgBody msgId = - deliverMessages [(conn, msgFlags, msgBody, msgId)] >>= \case - [r] -> liftEither r - rs -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 result, got " <> show (length rs) - -deliverMessages :: ChatMonad' m => [(Connection, MsgFlags, LazyMsgBody, MessageId)] -> m [Either ChatError Int64] -deliverMessages msgReqs = do - sent <- zipWith prepareBatch msgReqs <$> withAgent' (`sendMessages` aReqs) - withStoreBatch $ \db -> map (bindRight $ createDelivery db) sent - where - aReqs = map (\(conn, msgFlags, msgBody, _msgId) -> (aConnId conn, msgFlags, LB.toStrict msgBody)) msgReqs - prepareBatch req = bimap (`ChatErrorAgent` Nothing) (req,) - createDelivery :: DB.Connection -> ((Connection, MsgFlags, LazyMsgBody, MessageId), AgentMsgId) -> IO (Either ChatError Int64) - createDelivery db ((Connection {connId}, _, _, msgId), agentMsgId) = - Right <$> createSndMsgDelivery db (SndMsgDelivery {connId, agentMsgId}) msgId - -sendGroupMessage :: (MsgEncodingI e, ChatMonad m) => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> m (SndMessage, [GroupMember]) -sendGroupMessage user GroupInfo {groupId} members chatMsgEvent = do - msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent (GroupId groupId) - recipientMembers <- liftIO $ shuffleMembers (filter memberCurrent members) - let msgFlags = MsgFlags {notification = hasNotification $ toCMEventTag chatMsgEvent} - (toSend, pending) = foldr addMember ([], []) recipientMembers - msgReqs = map (\(_, conn) -> (conn, msgFlags, msgBody, msgId)) toSend - delivered <- deliverMessages msgReqs - let errors = lefts delivered - unless (null errors) $ toView $ CRChatErrors (Just user) errors - stored <- withStoreBatch' $ \db -> map (\m -> createPendingGroupMessage db (groupMemberId' m) msgId Nothing) pending - let sentToMembers = filterSent delivered toSend fst <> filterSent stored pending id - pure (msg, sentToMembers) - where - shuffleMembers :: [GroupMember] -> IO [GroupMember] - shuffleMembers ms = do - let (adminMs, otherMs) = partition isAdmin ms - liftM2 (<>) (shuffle adminMs) (shuffle otherMs) - where - isAdmin GroupMember {memberRole} = memberRole >= GRAdmin - addMember m (toSend, pending) = case memberSendAction chatMsgEvent members m of - Just (MSASend conn) -> ((m, conn) : toSend, pending) - Just MSAPending -> (toSend, m : pending) - Nothing -> (toSend, pending) - filterSent :: [Either ChatError a] -> [mem] -> (mem -> GroupMember) -> [GroupMember] - filterSent rs ms mem = [mem m | (Right _, m) <- zip rs ms] - -data MemberSendAction = MSASend Connection | MSAPending - -memberSendAction :: ChatMsgEvent e -> [GroupMember] -> GroupMember -> Maybe MemberSendAction -memberSendAction chatMsgEvent members m = case memberConn m of - Nothing -> pendingOrForwarded - Just conn@Connection {connStatus} - | connDisabled conn || connStatus == ConnDeleted -> Nothing - | connStatus == ConnSndReady || connStatus == ConnReady -> Just (MSASend conn) - | otherwise -> pendingOrForwarded - where - pendingOrForwarded - | forwardSupported && isForwardedGroupMsg chatMsgEvent = Nothing - | isXGrpMsgForward chatMsgEvent = Nothing - | otherwise = Just MSAPending - where - forwardSupported = - let mcvr = memberChatVRange' m - in isCompatibleRange mcvr groupForwardVRange && invitingMemberSupportsForward - invitingMemberSupportsForward = case m.invitedByGroupMemberId of - Just invMemberId -> - -- can be optimized for large groups by replacing [GroupMember] with Map GroupMemberId GroupMember - case find (\m' -> groupMemberId' m' == invMemberId) members of - Just invitingMember -> do - let mcvr = memberChatVRange' invitingMember - isCompatibleRange mcvr groupForwardVRange - Nothing -> False - Nothing -> False - isXGrpMsgForward ev = case ev of - XGrpMsgForward {} -> True - _ -> False - -sendGroupMemberMessage :: forall e m. (MsgEncodingI e, ChatMonad m) => User -> GroupMember -> ChatMsgEvent e -> Int64 -> Maybe Int64 -> m () -> m () -sendGroupMemberMessage user m@GroupMember {groupMemberId} chatMsgEvent groupId introId_ postDeliver = do - msg <- createSndMessage chatMsgEvent (GroupId groupId) - messageMember msg `catchChatError` (\e -> toView (CRChatError (Just user) e)) - where - messageMember :: SndMessage -> m () - messageMember SndMessage {msgId, msgBody} = forM_ (memberSendAction chatMsgEvent [m] m) $ \case - MSASend conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver - MSAPending -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_ - -sendPendingGroupMessages :: ChatMonad m => User -> GroupMember -> Connection -> m () -sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn = do - pendingMessages <- withStore' $ \db -> getPendingGroupMessages db groupMemberId - -- TODO ensure order - pending messages interleave with user input messages - forM_ pendingMessages $ \pgm -> - processPendingMessage pgm `catchChatError` (toView . CRChatError (Just user)) - where - processPendingMessage PendingGroupMessage {msgId, cmEventTag = ACMEventTag _ tag, msgBody, introId_} = do - void $ deliverMessage conn tag msgBody msgId - withStore' $ \db -> deletePendingGroupMessage db groupMemberId msgId - case tag of - XGrpMemFwd_ -> case introId_ of - Just introId -> withStore' $ \db -> updateIntroStatus db introId GMIntroInvForwarded - _ -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName - _ -> pure () - --- TODO [batch send] refactor direct message processing same as groups (e.g. checkIntegrity before processing) -saveDirectRcvMSG :: ChatMonad m => Connection -> MsgMeta -> CommandId -> MsgBody -> m (Connection, RcvMessage) -saveDirectRcvMSG conn@Connection {connId} agentMsgMeta agentAckCmdId msgBody = - case parseChatMessages msgBody of - [Right (ACMsg _ ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent})] -> do - conn' <- updatePeerChatVRange conn chatVRange - let agentMsgId = fst $ recipient agentMsgMeta - newMsg = NewRcvMessage {chatMsgEvent, msgBody} - rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta, agentAckCmdId} - msg <- withStore $ \db -> createNewMessageAndRcvMsgDelivery db (ConnectionId connId) newMsg sharedMsgId_ rcvMsgDelivery Nothing - pure (conn', msg) - [Left e] -> error $ "saveDirectRcvMSG: error parsing chat message: " <> e - _ -> error "saveDirectRcvMSG: batching not supported" - -saveGroupRcvMsg :: (MsgEncodingI e, ChatMonad m) => User -> GroupId -> GroupMember -> Connection -> MsgMeta -> CommandId -> MsgBody -> ChatMessage e -> m (GroupMember, Connection, RcvMessage) -saveGroupRcvMsg user groupId authorMember conn@Connection {connId} agentMsgMeta agentAckCmdId msgBody ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} = do - (am', conn') <- updateMemberChatVRange authorMember conn chatVRange - let agentMsgId = fst $ recipient agentMsgMeta - newMsg = NewRcvMessage {chatMsgEvent, msgBody} - rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta, agentAckCmdId} - amId = Just am'.groupMemberId - msg <- - withStore (\db -> createNewMessageAndRcvMsgDelivery db (GroupId groupId) newMsg sharedMsgId_ rcvMsgDelivery amId) - `catchChatError` \e -> case e of - ChatErrorStore (SEDuplicateGroupMessage _ _ _ (Just forwardedByGroupMemberId)) -> do - fm <- withStore $ \db -> getGroupMember db user groupId forwardedByGroupMemberId - forM_ (memberConn fm) $ \fmConn -> - void $ sendDirectMessage fmConn (XGrpMemCon am'.memberId) (GroupId groupId) - throwError e - _ -> throwError e - pure (am', conn', msg) - -saveGroupFwdRcvMsg :: (MsgEncodingI e, ChatMonad m) => User -> GroupId -> GroupMember -> GroupMember -> MsgBody -> ChatMessage e -> m RcvMessage -saveGroupFwdRcvMsg user groupId forwardingMember refAuthorMember msgBody ChatMessage {msgId = sharedMsgId_, chatMsgEvent} = do - let newMsg = NewRcvMessage {chatMsgEvent, msgBody} - fwdMemberId = Just $ groupMemberId' forwardingMember - refAuthorId = Just $ groupMemberId' refAuthorMember - withStore (\db -> createNewRcvMessage db (GroupId groupId) newMsg sharedMsgId_ refAuthorId fwdMemberId) - `catchChatError` \e -> case e of - ChatErrorStore (SEDuplicateGroupMessage _ _ (Just authorGroupMemberId) Nothing) -> do - am <- withStore $ \db -> getGroupMember db user groupId authorGroupMemberId - if sameMemberId refAuthorMember.memberId am - then forM_ (memberConn forwardingMember) $ \fmConn -> - void $ sendDirectMessage fmConn (XGrpMemCon am.memberId) (GroupId groupId) - else toView $ CRMessageError user "error" "saveGroupFwdRcvMsg: referenced author member id doesn't match message member id" - throwError e - _ -> throwError e - -saveSndChatItem :: ChatMonad m => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> m (ChatItem c 'MDSnd) -saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothing Nothing Nothing False - -saveSndChatItem' :: ChatMonad m => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIFile 'MDSnd) -> Maybe (CIQuote c) -> Maybe CITimed -> Bool -> m (ChatItem c 'MDSnd) -saveSndChatItem' user cd msg@SndMessage {sharedMsgId} content ciFile quotedItem itemTimed live = do - createdAt <- liftIO getCurrentTime - ciId <- withStore' $ \db -> do - when (ciRequiresAttention content) $ updateChatTs db user cd createdAt - ciId <- createNewSndChatItem db user cd msg content quotedItem itemTimed live createdAt - forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt - pure ciId - liftIO $ mkChatItem cd ciId content ciFile quotedItem (Just sharedMsgId) itemTimed live createdAt Nothing createdAt - -saveRcvChatItem :: ChatMonad m => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> CIContent 'MDRcv -> m (ChatItem c 'MDRcv) -saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} brokerTs content = - saveRcvChatItem' user cd msg sharedMsgId_ brokerTs content Nothing Nothing False - -saveRcvChatItem' :: ChatMonad m => User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> UTCTime -> CIContent 'MDRcv -> Maybe (CIFile 'MDRcv) -> Maybe CITimed -> Bool -> m (ChatItem c 'MDRcv) -saveRcvChatItem' user cd msg@RcvMessage {forwardedByMember} sharedMsgId_ brokerTs content ciFile itemTimed live = do - createdAt <- liftIO getCurrentTime - (ciId, quotedItem) <- withStore' $ \db -> do - when (ciRequiresAttention content) $ updateChatTs db user cd createdAt - (ciId, quotedItem) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live brokerTs createdAt - forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt - pure (ciId, quotedItem) - liftIO $ mkChatItem cd ciId content ciFile quotedItem sharedMsgId_ itemTimed live brokerTs forwardedByMember createdAt - -mkChatItem :: forall c d. MsgDirectionI d => ChatDirection c d -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CITimed -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> IO (ChatItem c d) -mkChatItem cd ciId content file quotedItem sharedMsgId itemTimed live itemTs forwardedByMember currentTs = do - let itemText = ciContentToText content - itemStatus = ciCreateStatus content - meta = mkCIMeta ciId content itemText itemStatus sharedMsgId Nothing False itemTimed (justTrue live) currentTs itemTs forwardedByMember currentTs currentTs - pure ChatItem {chatDir = toCIDirection cd, meta, content, formattedText = parseMaybeMarkdownList itemText, quotedItem, reactions = [], file} - -deleteDirectCI :: (ChatMonad m, MsgDirectionI d) => User -> Contact -> ChatItem 'CTDirect d -> Bool -> Bool -> m ChatResponse -deleteDirectCI user ct ci@ChatItem {file} byUser timed = do - deleteCIFile user file - withStoreCtx' (Just "deleteDirectCI, deleteDirectChatItem") $ \db -> deleteDirectChatItem db user ct ci - pure $ CRChatItemDeleted user (AChatItem SCTDirect msgDirection (DirectChat ct) ci) Nothing byUser timed - -deleteGroupCI :: (ChatMonad m, MsgDirectionI d) => User -> GroupInfo -> ChatItem 'CTGroup d -> Bool -> Bool -> Maybe GroupMember -> UTCTime -> m ChatResponse -deleteGroupCI user gInfo ci@ChatItem {file} byUser timed byGroupMember_ deletedTs = do - deleteCIFile user file - toCi <- withStoreCtx' (Just "deleteGroupCI, deleteGroupChatItem ...") $ \db -> - case byGroupMember_ of - Nothing -> deleteGroupChatItem db user gInfo ci $> Nothing - Just m -> Just <$> updateGroupChatItemModerated db user gInfo ci m deletedTs - pure $ CRChatItemDeleted user (gItem ci) (gItem <$> toCi) byUser timed - where - gItem = AChatItem SCTGroup msgDirection (GroupChat gInfo) - -deleteCIFile :: (ChatMonad m, MsgDirectionI d) => User -> Maybe (CIFile d) -> m () -deleteCIFile user file_ = - forM_ file_ $ \file -> do - fileAgentConnIds <- deleteFile' user (mkCIFileInfo file) True - deleteAgentConnectionsAsync user fileAgentConnIds - -markDirectCIDeleted :: (ChatMonad m, MsgDirectionI d) => User -> Contact -> ChatItem 'CTDirect d -> MessageId -> Bool -> UTCTime -> m ChatResponse -markDirectCIDeleted user ct ci@ChatItem {file} msgId byUser deletedTs = do - cancelCIFile user file - ci' <- withStore' $ \db -> markDirectChatItemDeleted db user ct ci msgId deletedTs - pure $ CRChatItemDeleted user (ctItem ci) (Just $ ctItem ci') byUser False - where - ctItem = AChatItem SCTDirect msgDirection (DirectChat ct) - -markGroupCIDeleted :: (ChatMonad m, MsgDirectionI d) => User -> GroupInfo -> ChatItem 'CTGroup d -> MessageId -> Bool -> Maybe GroupMember -> UTCTime -> m ChatResponse -markGroupCIDeleted user gInfo ci@ChatItem {file} msgId byUser byGroupMember_ deletedTs = do - cancelCIFile user file - ci' <- withStore' $ \db -> markGroupChatItemDeleted db user gInfo ci msgId byGroupMember_ deletedTs - pure $ CRChatItemDeleted user (gItem ci) (Just $ gItem ci') byUser False - where - gItem = AChatItem SCTGroup msgDirection (GroupChat gInfo) - -cancelCIFile :: (ChatMonad m, MsgDirectionI d) => User -> Maybe (CIFile d) -> m () -cancelCIFile user file_ = - forM_ file_ $ \file -> do - fileAgentConnIds <- cancelFile' user (mkCIFileInfo file) True - deleteAgentConnectionsAsync user fileAgentConnIds - -createAgentConnectionAsync :: forall m c. (ChatMonad m, ConnectionModeI c) => User -> CommandFunction -> Bool -> SConnectionMode c -> SubscriptionMode -> m (CommandId, ConnId) -createAgentConnectionAsync user cmdFunction enableNtfs cMode subMode = do - cmdId <- withStore' $ \db -> createCommand db user Nothing cmdFunction - connId <- withAgent $ \a -> createConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cMode subMode - pure (cmdId, connId) - -joinAgentConnectionAsync :: ChatMonad m => User -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> m (CommandId, ConnId) -joinAgentConnectionAsync user enableNtfs cReqUri cInfo subMode = do - cmdId <- withStore' $ \db -> createCommand db user Nothing CFJoinConn - connId <- withAgent $ \a -> joinConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cReqUri cInfo subMode - pure (cmdId, connId) - -allowAgentConnectionAsync :: (MsgEncodingI e, ChatMonad m) => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> m () -allowAgentConnectionAsync user conn@Connection {connId} confId msg = do - cmdId <- withStore' $ \db -> createCommand db user (Just connId) CFAllowConn - dm <- directMessage msg - withAgent $ \a -> allowConnectionAsync a (aCorrId cmdId) (aConnId conn) confId dm - withStore' $ \db -> updateConnectionStatus db conn ConnAccepted - -agentAcceptContactAsync :: (MsgEncodingI e, ChatMonad m) => User -> Bool -> InvitationId -> ChatMsgEvent e -> SubscriptionMode -> m (CommandId, ConnId) -agentAcceptContactAsync user enableNtfs invId msg subMode = do - cmdId <- withStore' $ \db -> createCommand db user Nothing CFAcceptContact - dm <- directMessage msg - connId <- withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) enableNtfs invId dm subMode - pure (cmdId, connId) - -deleteAgentConnectionAsync :: ChatMonad m => User -> ConnId -> m () -deleteAgentConnectionAsync user acId = - withAgent (`deleteConnectionAsync` acId) `catchChatError` (toView . CRChatError (Just user)) - -deleteAgentConnectionsAsync :: ChatMonad m => User -> [ConnId] -> m () -deleteAgentConnectionsAsync _ [] = pure () -deleteAgentConnectionsAsync user acIds = - withAgent (`deleteConnectionsAsync` acIds) `catchChatError` (toView . CRChatError (Just user)) - -agentXFTPDeleteRcvFile :: ChatMonad m => RcvFileId -> FileTransferId -> m () -agentXFTPDeleteRcvFile aFileId fileId = do - withAgent (`xftpDeleteRcvFile` aFileId) - withStore' $ \db -> setRcvFTAgentDeleted db fileId - -agentXFTPDeleteSndFileRemote :: ChatMonad m => User -> XFTPSndFile -> FileTransferId -> m () -agentXFTPDeleteSndFileRemote user XFTPSndFile {agentSndFileId = AgentSndFileId aFileId, privateSndFileDescr, agentSndFileDeleted} fileId = - unless agentSndFileDeleted $ - forM_ privateSndFileDescr $ \sfdText -> do - sd <- parseFileDescription sfdText - withAgent $ \a -> xftpDeleteSndFileRemote a (aUserId user) aFileId sd - withStore' $ \db -> setSndFTAgentDeleted db user fileId - -userProfileToSend :: User -> Maybe Profile -> Maybe Contact -> Profile -userProfileToSend user@User {profile = p} incognitoProfile ct = - let p' = fromMaybe (fromLocalProfile p) incognitoProfile - userPrefs = maybe (preferences' user) (const Nothing) incognitoProfile - in (p' :: Profile) {preferences = Just . toChatPrefs $ mergePreferences (userPreferences <$> ct) userPrefs} - -createRcvFeatureItems :: forall m. ChatMonad m => User -> Contact -> Contact -> m () -createRcvFeatureItems user ct ct' = - createFeatureItems user ct ct' CDDirectRcv CIRcvChatFeature CIRcvChatPreference contactPreference - -createSndFeatureItems :: forall m. ChatMonad m => User -> Contact -> Contact -> m () -createSndFeatureItems user ct ct' = - createFeatureItems user ct ct' CDDirectSnd CISndChatFeature CISndChatPreference getPref - where - getPref u = (userPreference u).preference - -type FeatureContent a d = ChatFeature -> a -> Maybe Int -> CIContent d - -createFeatureItems :: - forall d m. - (MsgDirectionI d, ChatMonad m) => - User -> - Contact -> - Contact -> - (Contact -> ChatDirection 'CTDirect d) -> - FeatureContent PrefEnabled d -> - FeatureContent FeatureAllowed d -> - (forall f. ContactUserPreference (FeaturePreference f) -> FeaturePreference f) -> - m () -createFeatureItems user Contact {mergedPreferences = cups} ct'@Contact {mergedPreferences = cups'} chatDir ciFeature ciOffer getPref = - forM_ allChatFeatures $ \(ACF f) -> createItem f - where - createItem :: forall f. FeatureI f => SChatFeature f -> m () - createItem f - | state /= state' = create ciFeature state' - | prefState /= prefState' = create ciOffer prefState' - | otherwise = pure () - where - create :: FeatureContent a d -> (a, Maybe Int) -> m () - create ci (s, param) = createInternalChatItem user (chatDir ct') (ci f' s param) Nothing - f' = chatFeature f - state = featureState cup - state' = featureState cup' - prefState = preferenceState $ getPref cup - prefState' = preferenceState $ getPref cup' - cup = getContactUserPreference f cups - cup' = getContactUserPreference f cups' - -createGroupFeatureChangedItems :: (MsgDirectionI d, ChatMonad m) => User -> ChatDirection 'CTGroup d -> (GroupFeature -> GroupPreference -> Maybe Int -> CIContent d) -> GroupInfo -> GroupInfo -> m () -createGroupFeatureChangedItems user cd ciContent GroupInfo {fullGroupPreferences = gps} GroupInfo {fullGroupPreferences = gps'} = - forM_ allGroupFeatureItems $ \(AGF f) -> do - let state = groupFeatureState $ getGroupPreference f gps - pref' = getGroupPreference f gps' - state'@(_, int') = groupFeatureState pref' - when (state /= state') $ - createInternalChatItem user cd (ciContent (toGroupFeature f) (toGroupPreference pref') int') Nothing - -sameGroupProfileInfo :: GroupProfile -> GroupProfile -> Bool -sameGroupProfileInfo p p' = p {groupPreferences = Nothing} == p' {groupPreferences = Nothing} - -createInternalChatItem :: forall c d m. (ChatTypeI c, MsgDirectionI d, ChatMonad m) => User -> ChatDirection c d -> CIContent d -> Maybe UTCTime -> m () -createInternalChatItem user cd content itemTs_ = do - createdAt <- liftIO getCurrentTime - let itemTs = fromMaybe createdAt itemTs_ - ciId <- withStore' $ \db -> do - when (ciRequiresAttention content) $ updateChatTs db user cd createdAt - createNewChatItemNoMsg db user cd content itemTs createdAt - ci <- liftIO $ mkChatItem cd ciId content Nothing Nothing Nothing Nothing False itemTs Nothing createdAt - toView $ CRNewChatItem user (AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci) - -getCreateActiveUser :: SQLiteStore -> Bool -> IO User -getCreateActiveUser st testView = do - user <- - withTransaction st getUsers >>= \case - [] -> newUser - users -> maybe (selectUser users) pure (find activeUser users) - unless testView $ putStrLn $ "Current user: " <> userStr user - pure user - where - newUser :: IO User - newUser = do - putStrLn - "No user profiles found, it will be created now.\n\ - \Please choose your display name and your full name.\n\ - \They will be sent to your contacts when you connect.\n\ - \They are only stored on your device and you can change them later." - loop - where - loop = do - displayName <- getContactName - withTransaction st (\db -> runExceptT $ createUserRecord db (AgentUserId 1) (profileFromName displayName) True) >>= \case - Left SEDuplicateName -> do - putStrLn "chosen display name is already used by another profile on this device, choose another one" - loop - Left e -> putStrLn ("database error " <> show e) >> exitFailure - Right user -> pure user - selectUser :: [User] -> IO User - selectUser [user] = do - withTransaction st (`setActiveUser` user.userId) - pure user - selectUser users = do - putStrLn "Select user profile:" - forM_ (zip [1 ..] users) $ \(n :: Int, user) -> putStrLn $ show n <> " - " <> userStr user - loop - where - loop = do - nStr <- getWithPrompt $ "user profile number (1 .. " <> show (length users) <> ")" - case readMaybe nStr :: Maybe Int of - Nothing -> putStrLn "invalid user number" >> loop - Just n - | n <= 0 || n > length users -> putStrLn "invalid user number" >> loop - | otherwise -> do - let user = users !! (n - 1) - withTransaction st (`setActiveUser` user.userId) - pure user - userStr :: User -> String - userStr User {localDisplayName, profile = LocalProfile {fullName}} = - T.unpack $ localDisplayName <> if T.null fullName || localDisplayName == fullName then "" else " (" <> fullName <> ")" - getContactName :: IO ContactName - getContactName = do - displayName <- getWithPrompt "display name" - let validName = mkValidName displayName - if - | null displayName -> putStrLn "display name can't be empty" >> getContactName - | null validName -> putStrLn "display name is invalid, please choose another" >> getContactName - | displayName /= validName -> putStrLn ("display name is invalid, you could use this one: " <> validName) >> getContactName - | otherwise -> pure $ T.pack displayName - getWithPrompt :: String -> IO String - getWithPrompt s = putStr (s <> ": ") >> hFlush stdout >> getLine - -withUser' :: ChatMonad m => (User -> m ChatResponse) -> m ChatResponse -withUser' action = - asks currentUser - >>= readTVarIO - >>= maybe (throwChatError CENoActiveUser) run - where - run u = action u `catchChatError` (pure . CRChatCmdError (Just u)) - -withUser :: ChatMonad m => (User -> m ChatResponse) -> m ChatResponse -withUser action = withUser' $ \user -> - ifM chatStarted (action user) (throwChatError CEChatNotStarted) - -withUser_ :: ChatMonad m => m ChatResponse -> m ChatResponse -withUser_ = withUser . const - -withUserId' :: ChatMonad m => UserId -> (User -> m ChatResponse) -> m ChatResponse -withUserId' userId action = withUser' $ \user -> do - checkSameUser userId user - action user - -withUserId :: ChatMonad m => UserId -> (User -> m ChatResponse) -> m ChatResponse -withUserId userId action = withUser $ \user -> do - checkSameUser userId user - action user - -checkSameUser :: ChatMonad m => UserId -> User -> m () -checkSameUser userId User {userId = activeUserId} = when (userId /= activeUserId) $ throwChatError (CEDifferentActiveUser userId activeUserId) - -chatStarted :: ChatMonad m => m Bool -chatStarted = fmap isJust . readTVarIO =<< asks agentAsync - -waitChatStarted :: ChatMonad m => m () -waitChatStarted = do - agentStarted <- asks agentAsync - atomically $ readTVar agentStarted >>= \a -> unless (isJust a) retry - -chatVersionRange :: ChatMonad' m => m VersionRange -chatVersionRange = do - ChatConfig {chatVRange} <- asks config - pure chatVRange - -chatCommandP :: Parser ChatCommand -chatCommandP = - choice - [ "/mute " *> ((`SetShowMessages` MFNone) <$> chatNameP), - "/unmute " *> ((`SetShowMessages` MFAll) <$> chatNameP), - "/unmute mentions " *> ((`SetShowMessages` MFMentions) <$> chatNameP), - "/receipts " *> (SetSendReceipts <$> chatNameP <* " " <*> ((Just <$> onOffP) <|> ("default" $> Nothing))), - "/block #" *> (SetShowMemberMessages <$> displayName <* A.space <*> (char_ '@' *> displayName) <*> pure False), - "/unblock #" *> (SetShowMemberMessages <$> displayName <* A.space <*> (char_ '@' *> displayName) <*> pure True), - "/_create user " *> (CreateActiveUser <$> jsonP), - "/create user " *> (CreateActiveUser <$> newUserP), - "/users" $> ListUsers, - "/_user " *> (APISetActiveUser <$> A.decimal <*> optional (A.space *> jsonP)), - ("/user " <|> "/u ") *> (SetActiveUser <$> displayName <*> optional (A.space *> pwdP)), - "/set receipts all " *> (SetAllContactReceipts <$> onOffP), - "/_set receipts contacts " *> (APISetUserContactReceipts <$> A.decimal <* A.space <*> receiptSettings), - "/set receipts contacts " *> (SetUserContactReceipts <$> receiptSettings), - "/_set receipts groups " *> (APISetUserGroupReceipts <$> A.decimal <* A.space <*> receiptSettings), - "/set receipts groups " *> (SetUserGroupReceipts <$> receiptSettings), - "/_hide user " *> (APIHideUser <$> A.decimal <* A.space <*> jsonP), - "/_unhide user " *> (APIUnhideUser <$> A.decimal <* A.space <*> jsonP), - "/_mute user " *> (APIMuteUser <$> A.decimal), - "/_unmute user " *> (APIUnmuteUser <$> A.decimal), - "/hide user " *> (HideUser <$> pwdP), - "/unhide user " *> (UnhideUser <$> pwdP), - "/mute user" $> MuteUser, - "/unmute user" $> UnmuteUser, - "/_delete user " *> (APIDeleteUser <$> A.decimal <* " del_smp=" <*> onOffP <*> optional (A.space *> jsonP)), - "/delete user " *> (DeleteUser <$> displayName <*> pure True <*> optional (A.space *> pwdP)), - ("/user" <|> "/u") $> ShowActiveUser, - "/_start subscribe=" *> (StartChat <$> onOffP <* " expire=" <*> onOffP <* " xftp=" <*> onOffP), - "/_start" $> StartChat True True True, - "/_stop" $> APIStopChat, - "/_app activate restore=" *> (APIActivateChat <$> onOffP), - "/_app activate" $> APIActivateChat True, - "/_app suspend " *> (APISuspendChat <$> A.decimal), - "/_resubscribe all" $> ResubscribeAllConnections, - "/_temp_folder " *> (SetTempFolder <$> filePath), - ("/_files_folder " <|> "/files_folder ") *> (SetFilesFolder <$> filePath), - "/remote_hosts_folder " *> (SetRemoteHostsFolder <$> filePath), - "/_xftp " *> (APISetXFTPConfig <$> ("on " *> (Just <$> jsonP) <|> ("off" $> Nothing))), - "/xftp " *> (APISetXFTPConfig <$> ("on" *> (Just <$> xftpCfgP) <|> ("off" $> Nothing))), - "/_files_encrypt " *> (APISetEncryptLocalFiles <$> onOffP), - "/contact_merge " *> (SetContactMergeEnabled <$> onOffP), - "/_db export " *> (APIExportArchive <$> jsonP), - "/db export" $> ExportArchive, - "/_db import " *> (APIImportArchive <$> jsonP), - "/_db delete" $> APIDeleteStorage, - "/_db encryption " *> (APIStorageEncryption <$> jsonP), - "/db encrypt " *> (APIStorageEncryption . dbEncryptionConfig "" <$> dbKeyP), - "/db key " *> (APIStorageEncryption <$> (dbEncryptionConfig <$> dbKeyP <* A.space <*> dbKeyP)), - "/db decrypt " *> (APIStorageEncryption . (`dbEncryptionConfig` "") <$> dbKeyP), - "/sql chat " *> (ExecChatStoreSQL <$> textP), - "/sql agent " *> (ExecAgentStoreSQL <$> textP), - "/sql slow" $> SlowSQLQueries, - "/_get chats " - *> ( APIGetChats - <$> A.decimal - <*> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False) - <*> (A.space *> paginationByTimeP <|> pure (PTLast 5000)) - <*> (A.space *> jsonP <|> pure clqNoFilters) - ), - "/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP <*> optional (" search=" *> stringP)), - "/_get items " *> (APIGetChatItems <$> chatPaginationP <*> optional (" search=" *> stringP)), - "/_get item info " *> (APIGetChatItemInfo <$> chatRefP <* A.space <*> A.decimal), - "/_send " *> (APISendMessage <$> chatRefP <*> liveMessageP <*> sendMessageTTLP <*> (" json " *> jsonP <|> " text " *> (ComposedMessage Nothing Nothing <$> mcTextP))), - "/_update item " *> (APIUpdateChatItem <$> chatRefP <* A.space <*> A.decimal <*> liveMessageP <* A.space <*> msgContentP), - "/_delete item " *> (APIDeleteChatItem <$> chatRefP <* A.space <*> A.decimal <* A.space <*> ciDeleteMode), - "/_delete member item #" *> (APIDeleteMemberChatItem <$> A.decimal <* A.space <*> A.decimal <* A.space <*> A.decimal), - "/_reaction " *> (APIChatItemReaction <$> chatRefP <* A.space <*> A.decimal <* A.space <*> onOffP <* A.space <*> jsonP), - "/_read user " *> (APIUserRead <$> A.decimal), - "/read user" $> UserRead, - "/_read chat " *> (APIChatRead <$> chatRefP <*> optional (A.space *> ((,) <$> ("from=" *> A.decimal) <* A.space <*> ("to=" *> A.decimal)))), - "/_unread chat " *> (APIChatUnread <$> chatRefP <* A.space <*> onOffP), - "/_delete " *> (APIDeleteChat <$> chatRefP <*> (A.space *> "notify=" *> onOffP <|> pure True)), - "/_clear chat " *> (APIClearChat <$> chatRefP), - "/_accept" *> (APIAcceptContact <$> incognitoOnOffP <* A.space <*> A.decimal), - "/_reject " *> (APIRejectContact <$> A.decimal), - "/_call invite @" *> (APISendCallInvitation <$> A.decimal <* A.space <*> jsonP), - "/call " *> char_ '@' *> (SendCallInvitation <$> displayName <*> pure defaultCallType), - "/_call reject @" *> (APIRejectCall <$> A.decimal), - "/_call offer @" *> (APISendCallOffer <$> A.decimal <* A.space <*> jsonP), - "/_call answer @" *> (APISendCallAnswer <$> A.decimal <* A.space <*> jsonP), - "/_call extra @" *> (APISendCallExtraInfo <$> A.decimal <* A.space <*> jsonP), - "/_call end @" *> (APIEndCall <$> A.decimal), - "/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP), - "/_call get" $> APIGetCallInvitations, - "/_network_statuses" $> APIGetNetworkStatuses, - "/_profile " *> (APIUpdateProfile <$> A.decimal <* A.space <*> jsonP), - "/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), - "/_set alias :" *> (APISetConnectionAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), - "/_set prefs @" *> (APISetContactPrefs <$> A.decimal <* A.space <*> jsonP), - "/_parse " *> (APIParseMarkdown . safeDecodeUtf8 <$> A.takeByteString), - "/_ntf get" $> APIGetNtfToken, - "/_ntf register " *> (APIRegisterToken <$> strP_ <*> strP), - "/_ntf verify " *> (APIVerifyToken <$> strP <* A.space <*> strP <* A.space <*> strP), - "/_ntf delete " *> (APIDeleteToken <$> strP), - "/_ntf message " *> (APIGetNtfMessage <$> strP <* A.space <*> strP), - "/_add #" *> (APIAddMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole), - "/_join #" *> (APIJoinGroup <$> A.decimal), - "/_member role #" *> (APIMemberRole <$> A.decimal <* A.space <*> A.decimal <*> memberRole), - "/_remove #" *> (APIRemoveMember <$> A.decimal <* A.space <*> A.decimal), - "/_leave #" *> (APILeaveGroup <$> A.decimal), - "/_members #" *> (APIListMembers <$> A.decimal), - "/_server test " *> (APITestProtoServer <$> A.decimal <* A.space <*> strP), - "/smp test " *> (TestProtoServer . AProtoServerWithAuth SPSMP <$> strP), - "/xftp test " *> (TestProtoServer . AProtoServerWithAuth SPXFTP <$> strP), - "/_servers " *> (APISetUserProtoServers <$> A.decimal <* A.space <*> srvCfgP), - "/smp " *> (SetUserProtoServers . APSC SPSMP . ProtoServersConfig . map toServerCfg <$> protocolServersP), - "/smp default" $> SetUserProtoServers (APSC SPSMP $ ProtoServersConfig []), - "/xftp " *> (SetUserProtoServers . APSC SPXFTP . ProtoServersConfig . map toServerCfg <$> protocolServersP), - "/xftp default" $> SetUserProtoServers (APSC SPXFTP $ ProtoServersConfig []), - "/_servers " *> (APIGetUserProtoServers <$> A.decimal <* A.space <*> strP), - "/smp" $> GetUserProtoServers (AProtocolType SPSMP), - "/xftp" $> GetUserProtoServers (AProtocolType SPXFTP), - "/_ttl " *> (APISetChatItemTTL <$> A.decimal <* A.space <*> ciTTLDecimal), - "/ttl " *> (SetChatItemTTL <$> ciTTL), - "/_ttl " *> (APIGetChatItemTTL <$> A.decimal), - "/ttl" $> GetChatItemTTL, - "/_network " *> (APISetNetworkConfig <$> jsonP), - ("/network " <|> "/net ") *> (APISetNetworkConfig <$> netCfgP), - ("/network" <|> "/net") $> APIGetNetworkConfig, - "/reconnect" $> ReconnectAllServers, - "/_settings " *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP), - "/_member settings #" *> (APISetMemberSettings <$> A.decimal <* A.space <*> A.decimal <* A.space <*> jsonP), - "/_info #" *> (APIGroupMemberInfo <$> A.decimal <* A.space <*> A.decimal), - "/_info #" *> (APIGroupInfo <$> A.decimal), - "/_info @" *> (APIContactInfo <$> A.decimal), - ("/info #" <|> "/i #") *> (GroupMemberInfo <$> displayName <* A.space <* char_ '@' <*> displayName), - ("/info #" <|> "/i #") *> (ShowGroupInfo <$> displayName), - ("/info " <|> "/i ") *> char_ '@' *> (ContactInfo <$> displayName), - "/_switch #" *> (APISwitchGroupMember <$> A.decimal <* A.space <*> A.decimal), - "/_switch @" *> (APISwitchContact <$> A.decimal), - "/_abort switch #" *> (APIAbortSwitchGroupMember <$> A.decimal <* A.space <*> A.decimal), - "/_abort switch @" *> (APIAbortSwitchContact <$> A.decimal), - "/_sync #" *> (APISyncGroupMemberRatchet <$> A.decimal <* A.space <*> A.decimal <*> (" force=on" $> True <|> pure False)), - "/_sync @" *> (APISyncContactRatchet <$> A.decimal <*> (" force=on" $> True <|> pure False)), - "/switch #" *> (SwitchGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName), - "/switch " *> char_ '@' *> (SwitchContact <$> displayName), - "/abort switch #" *> (AbortSwitchGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName), - "/abort switch " *> char_ '@' *> (AbortSwitchContact <$> displayName), - "/sync #" *> (SyncGroupMemberRatchet <$> displayName <* A.space <* char_ '@' <*> displayName <*> (" force=on" $> True <|> pure False)), - "/sync " *> char_ '@' *> (SyncContactRatchet <$> displayName <*> (" force=on" $> True <|> pure False)), - "/_get code @" *> (APIGetContactCode <$> A.decimal), - "/_get code #" *> (APIGetGroupMemberCode <$> A.decimal <* A.space <*> A.decimal), - "/_verify code @" *> (APIVerifyContact <$> A.decimal <*> optional (A.space *> verifyCodeP)), - "/_verify code #" *> (APIVerifyGroupMember <$> A.decimal <* A.space <*> A.decimal <*> optional (A.space *> verifyCodeP)), - "/_enable @" *> (APIEnableContact <$> A.decimal), - "/_enable #" *> (APIEnableGroupMember <$> A.decimal <* A.space <*> A.decimal), - "/code " *> char_ '@' *> (GetContactCode <$> displayName), - "/code #" *> (GetGroupMemberCode <$> displayName <* A.space <* char_ '@' <*> displayName), - "/verify " *> char_ '@' *> (VerifyContact <$> displayName <*> optional (A.space *> verifyCodeP)), - "/verify #" *> (VerifyGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> optional (A.space *> verifyCodeP)), - "/enable " *> char_ '@' *> (EnableContact <$> displayName), - "/enable #" *> (EnableGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName), - ("/help files" <|> "/help file" <|> "/hf") $> ChatHelp HSFiles, - ("/help groups" <|> "/help group" <|> "/hg") $> ChatHelp HSGroups, - ("/help contacts" <|> "/help contact" <|> "/hc") $> ChatHelp HSContacts, - ("/help address" <|> "/ha") $> ChatHelp HSMyAddress, - ("/help incognito" <|> "/hi") $> ChatHelp HSIncognito, - ("/help messages" <|> "/hm") $> ChatHelp HSMessages, - ("/help remote" <|> "/hr") $> ChatHelp HSRemote, - ("/help settings" <|> "/hs") $> ChatHelp HSSettings, - ("/help db" <|> "/hd") $> ChatHelp HSDatabase, - ("/help" <|> "/h") $> ChatHelp HSMain, - ("/group" <|> "/g") *> (NewGroup <$> incognitoP <* A.space <* char_ '#' <*> groupProfile), - "/_group " *> (APINewGroup <$> A.decimal <*> incognitoOnOffP <* A.space <*> jsonP), - ("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> (memberRole <|> pure GRMember)), - ("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayName), - ("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayName <* A.space <* char_ '@' <*> displayName <*> memberRole), - ("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMember <$> displayName <* A.space <* char_ '@' <*> displayName), - ("/leave " <|> "/l ") *> char_ '#' *> (LeaveGroup <$> displayName), - ("/delete #" <|> "/d #") *> (DeleteGroup <$> displayName), - ("/delete " <|> "/d ") *> char_ '@' *> (DeleteContact <$> displayName), - "/clear #" *> (ClearGroup <$> displayName), - "/clear " *> char_ '@' *> (ClearContact <$> displayName), - ("/members " <|> "/ms ") *> char_ '#' *> (ListMembers <$> displayName), - "/_groups" *> (APIListGroups <$> A.decimal <*> optional (" @" *> A.decimal) <*> optional (A.space *> stringP)), - ("/groups" <|> "/gs") *> (ListGroups <$> optional (" @" *> displayName) <*> optional (A.space *> stringP)), - "/_group_profile #" *> (APIUpdateGroupProfile <$> A.decimal <* A.space <*> jsonP), - ("/group_profile " <|> "/gp ") *> char_ '#' *> (UpdateGroupNames <$> displayName <* A.space <*> groupProfile), - ("/group_profile " <|> "/gp ") *> char_ '#' *> (ShowGroupProfile <$> displayName), - "/group_descr " *> char_ '#' *> (UpdateGroupDescription <$> displayName <*> optional (A.space *> msgTextP)), - "/set welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayName <* A.space <*> (Just <$> msgTextP)), - "/delete welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayName <*> pure Nothing), - "/show welcome " *> char_ '#' *> (ShowGroupDescription <$> displayName), - "/_create link #" *> (APICreateGroupLink <$> A.decimal <*> (memberRole <|> pure GRMember)), - "/_set link role #" *> (APIGroupLinkMemberRole <$> A.decimal <*> memberRole), - "/_delete link #" *> (APIDeleteGroupLink <$> A.decimal), - "/_get link #" *> (APIGetGroupLink <$> A.decimal), - "/create link #" *> (CreateGroupLink <$> displayName <*> (memberRole <|> pure GRMember)), - "/set link role #" *> (GroupLinkMemberRole <$> displayName <*> memberRole), - "/delete link #" *> (DeleteGroupLink <$> displayName), - "/show link #" *> (ShowGroupLink <$> displayName), - "/_create member contact #" *> (APICreateMemberContact <$> A.decimal <* A.space <*> A.decimal), - "/_invite member contact @" *> (APISendMemberContactInvitation <$> A.decimal <*> optional (A.space *> msgContentP)), - (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <*> pure Nothing <*> quotedMsg <*> msgTextP), - (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <* char_ '@' <*> (Just <$> displayName) <* A.space <*> quotedMsg <*> msgTextP), - "/_contacts " *> (APIListContacts <$> A.decimal), - "/contacts" $> ListContacts, - "/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> strP), - "/_connect " *> (APIConnect <$> A.decimal <*> incognitoOnOffP <* A.space <*> ((Just <$> strP) <|> A.takeByteString $> Nothing)), - "/_connect " *> (APIAddContact <$> A.decimal <*> incognitoOnOffP), - "/_set incognito :" *> (APISetConnectionIncognito <$> A.decimal <* A.space <*> onOffP), - ("/connect" <|> "/c") *> (Connect <$> incognitoP <* A.space <*> ((Just <$> strP) <|> A.takeTill isSpace $> Nothing)), - ("/connect" <|> "/c") *> (AddContact <$> incognitoP), - SendMessage <$> chatNameP <* A.space <*> msgTextP, - "@#" *> (SendMemberContactMessage <$> displayName <* A.space <* char_ '@' <*> displayName <* A.space <*> msgTextP), - "/live " *> (SendLiveMessage <$> chatNameP <*> (A.space *> msgTextP <|> pure "")), - (">@" <|> "> @") *> sendMsgQuote (AMsgDirection SMDRcv), - (">>@" <|> ">> @") *> sendMsgQuote (AMsgDirection SMDSnd), - ("\\ " <|> "\\") *> (DeleteMessage <$> chatNameP <* A.space <*> textP), - ("\\\\ #" <|> "\\\\#") *> (DeleteMemberMessage <$> displayName <* A.space <* char_ '@' <*> displayName <* A.space <*> textP), - ("! " <|> "!") *> (EditMessage <$> chatNameP <* A.space <*> (quotedMsg <|> pure "") <*> msgTextP), - ReactToMessage <$> (("+" $> True) <|> ("-" $> False)) <*> reactionP <* A.space <*> chatNameP' <* A.space <*> textP, - "/feed " *> (SendMessageBroadcast <$> msgTextP), - ("/chats" <|> "/cs") *> (LastChats <$> (" all" $> Nothing <|> Just <$> (A.space *> A.decimal <|> pure 20))), - ("/tail" <|> "/t") *> (LastMessages <$> optional (A.space *> chatNameP) <*> msgCountP <*> pure Nothing), - ("/search" <|> "/?") *> (LastMessages <$> optional (A.space *> chatNameP) <*> msgCountP <*> (Just <$> (A.space *> stringP))), - "/last_item_id" *> (LastChatItemId <$> optional (A.space *> chatNameP) <*> (A.space *> A.decimal <|> pure 0)), - "/show" *> (ShowLiveItems <$> (A.space *> onOffP <|> pure True)), - "/show " *> (ShowChatItem . Just <$> A.decimal), - "/item info " *> (ShowChatItemInfo <$> chatNameP <* A.space <*> msgTextP), - ("/file " <|> "/f ") *> (SendFile <$> chatNameP' <* A.space <*> cryptoFileP), - ("/image " <|> "/img ") *> (SendImage <$> chatNameP' <* A.space <*> cryptoFileP), - ("/fforward " <|> "/ff ") *> (ForwardFile <$> chatNameP' <* A.space <*> A.decimal), - ("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal), - ("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* A.space <*> filePath), - ("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> optional (" encrypt=" *> onOffP) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)), - "/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> optional (" encrypt=" *> onOffP)), - ("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal), - ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal), - "/_connect contact " *> (APIConnectContactViaAddress <$> A.decimal <*> incognitoOnOffP <* A.space <*> A.decimal), - "/simplex" *> (ConnectSimplex <$> incognitoP), - "/_address " *> (APICreateMyAddress <$> A.decimal), - ("/address" <|> "/ad") $> CreateMyAddress, - "/_delete_address " *> (APIDeleteMyAddress <$> A.decimal), - ("/delete_address" <|> "/da") $> DeleteMyAddress, - "/_show_address " *> (APIShowMyAddress <$> A.decimal), - ("/show_address" <|> "/sa") $> ShowMyAddress, - "/_profile_address " *> (APISetProfileAddress <$> A.decimal <* A.space <*> onOffP), - ("/profile_address " <|> "/pa ") *> (SetProfileAddress <$> onOffP), - "/_auto_accept " *> (APIAddressAutoAccept <$> A.decimal <* A.space <*> autoAcceptP), - "/auto_accept " *> (AddressAutoAccept <$> autoAcceptP), - ("/accept" <|> "/ac") *> (AcceptContact <$> incognitoP <* A.space <* char_ '@' <*> displayName), - ("/reject " <|> "/rc ") *> char_ '@' *> (RejectContact <$> displayName), - ("/markdown" <|> "/m") $> ChatHelp HSMarkdown, - ("/welcome" <|> "/w") $> Welcome, - "/set profile image " *> (UpdateProfileImage . Just . ImageData <$> imageP), - "/delete profile image" $> UpdateProfileImage Nothing, - "/show profile image" $> ShowProfileImage, - ("/profile " <|> "/p ") *> (uncurry UpdateProfile <$> profileNames), - ("/profile" <|> "/p") $> ShowProfile, - "/set voice #" *> (SetGroupFeature (AGF SGFVoice) <$> displayName <*> (A.space *> strP)), - "/set voice @" *> (SetContactFeature (ACF SCFVoice) <$> displayName <*> optional (A.space *> strP)), - "/set voice " *> (SetUserFeature (ACF SCFVoice) <$> strP), - "/set files #" *> (SetGroupFeature (AGF SGFFiles) <$> displayName <*> (A.space *> strP)), - "/set history #" *> (SetGroupFeature (AGF SGFHistory) <$> displayName <*> (A.space *> strP)), - "/set calls @" *> (SetContactFeature (ACF SCFCalls) <$> displayName <*> optional (A.space *> strP)), - "/set calls " *> (SetUserFeature (ACF SCFCalls) <$> strP), - "/set delete #" *> (SetGroupFeature (AGF SGFFullDelete) <$> displayName <*> (A.space *> strP)), - "/set delete @" *> (SetContactFeature (ACF SCFFullDelete) <$> displayName <*> optional (A.space *> strP)), - "/set delete " *> (SetUserFeature (ACF SCFFullDelete) <$> strP), - "/set direct #" *> (SetGroupFeature (AGF SGFDirectMessages) <$> displayName <*> (A.space *> strP)), - "/set disappear #" *> (SetGroupTimedMessages <$> displayName <*> (A.space *> timedTTLOnOffP)), - "/set disappear @" *> (SetContactTimedMessages <$> displayName <*> optional (A.space *> timedMessagesEnabledP)), - "/set disappear " *> (SetUserTimedMessages <$> (("yes" $> True) <|> ("no" $> False))), - ("/incognito" <* optional (A.space *> onOffP)) $> ChatHelp HSIncognito, - "/set device name " *> (SetLocalDeviceName <$> textP), - "/list remote hosts" $> ListRemoteHosts, - "/switch remote host " *> (SwitchRemoteHost <$> ("local" $> Nothing <|> (Just <$> A.decimal))), - "/start remote host " *> (StartRemoteHost <$> ("new" $> Nothing <|> (Just <$> ((,) <$> A.decimal <*> (" multicast=" *> onOffP <|> pure False)))) <*> optional (A.space *> rcCtrlAddressP) <*> optional (" port=" *> A.decimal)), - "/stop remote host " *> (StopRemoteHost <$> ("new" $> RHNew <|> RHId <$> A.decimal)), - "/delete remote host " *> (DeleteRemoteHost <$> A.decimal), - "/store remote file " *> (StoreRemoteFile <$> A.decimal <*> optional (" encrypt=" *> onOffP) <* A.space <*> filePath), - "/get remote file " *> (GetRemoteFile <$> A.decimal <* A.space <*> jsonP), - "/connect remote ctrl " *> (ConnectRemoteCtrl <$> strP), - "/find remote ctrl" $> FindKnownRemoteCtrl, - "/confirm remote ctrl " *> (ConfirmRemoteCtrl <$> A.decimal), - "/verify remote ctrl " *> (VerifyRemoteCtrlSession <$> textP), - "/list remote ctrls" $> ListRemoteCtrls, - "/stop remote ctrl" $> StopRemoteCtrl, - "/delete remote ctrl " *> (DeleteRemoteCtrl <$> A.decimal), - ("/quit" <|> "/q" <|> "/exit") $> QuitChat, - ("/version" <|> "/v") $> ShowVersion, - "/debug locks" $> DebugLocks, - "/get stats" $> GetAgentStats, - "/reset stats" $> ResetAgentStats, - "/get subs" $> GetAgentSubs, - "/get subs details" $> GetAgentSubsDetails - ] - where - choice = A.choice . map (\p -> p <* A.takeWhile (== ' ') <* A.endOfInput) - incognitoP = (A.space *> ("incognito" <|> "i")) $> True <|> pure False - incognitoOnOffP = (A.space *> "incognito=" *> onOffP) <|> pure False - imagePrefix = (<>) <$> "data:" <*> ("image/png;base64," <|> "image/jpg;base64,") - imageP = safeDecodeUtf8 <$> ((<>) <$> imagePrefix <*> (B64.encode <$> base64P)) - chatTypeP = A.char '@' $> CTDirect <|> A.char '#' $> CTGroup <|> A.char ':' $> CTContactConnection - chatPaginationP = - (CPLast <$ "count=" <*> A.decimal) - <|> (CPAfter <$ "after=" <*> A.decimal <* A.space <* "count=" <*> A.decimal) - <|> (CPBefore <$ "before=" <*> A.decimal <* A.space <* "count=" <*> A.decimal) - paginationByTimeP = - (PTLast <$ "count=" <*> A.decimal) - <|> (PTAfter <$ "after=" <*> strP <* A.space <* "count=" <*> A.decimal) - <|> (PTBefore <$ "before=" <*> strP <* A.space <* "count=" <*> A.decimal) - mcTextP = MCText . safeDecodeUtf8 <$> A.takeByteString - msgContentP = "text " *> mcTextP <|> "json " *> jsonP - ciDeleteMode = "broadcast" $> CIDMBroadcast <|> "internal" $> CIDMInternal - displayName = safeDecodeUtf8 <$> (quoted "'" <|> takeNameTill isSpace) - where - takeNameTill p = - A.peekChar' >>= \c -> - if refChar c then A.takeTill p else fail "invalid first character in display name" - quoted cs = A.choice [A.char c *> takeNameTill (== c) <* A.char c | c <- cs] - refChar c = c > ' ' && c /= '#' && c /= '@' - sendMsgQuote msgDir = SendMessageQuote <$> displayName <* A.space <*> pure msgDir <*> quotedMsg <*> msgTextP - quotedMsg = safeDecodeUtf8 <$> (A.char '(' *> A.takeTill (== ')') <* A.char ')') <* optional A.space - reactionP = MREmoji <$> (mrEmojiChar <$?> (toEmoji <$> A.anyChar)) - toEmoji = \case - '1' -> '👍' - '+' -> '👍' - '-' -> '👎' - ')' -> '😀' - ',' -> '😢' - '*' -> head "❤️" - '^' -> '🚀' - c -> c - liveMessageP = " live=" *> onOffP <|> pure False - sendMessageTTLP = " ttl=" *> ((Just <$> A.decimal) <|> ("default" $> Nothing)) <|> pure Nothing - receiptSettings = do - enable <- onOffP - clearOverrides <- (" clear_overrides=" *> onOffP) <|> pure False - pure UserMsgReceiptSettings {enable, clearOverrides} - onOffP = ("on" $> True) <|> ("off" $> False) - profileNames = (,) <$> displayName <*> fullNameP - newUserP = do - sameServers <- "same_servers=" *> onOffP <* A.space <|> pure False - (cName, fullName) <- profileNames - let profile = Just Profile {displayName = cName, fullName, image = Nothing, contactLink = Nothing, preferences = Nothing} - pure NewUser {profile, sameServers, pastTimestamp = False} - jsonP :: J.FromJSON a => Parser a - jsonP = J.eitherDecodeStrict' <$?> A.takeByteString - groupProfile = do - (gName, fullName) <- profileNames - let groupPreferences = - Just - (emptyGroupPrefs :: GroupPreferences) - { directMessages = Just DirectMessagesGroupPreference {enable = FEOn}, - history = Just HistoryGroupPreference {enable = FEOn} - } - pure GroupProfile {displayName = gName, fullName, description = Nothing, image = Nothing, groupPreferences} - fullNameP = A.space *> textP <|> pure "" - textP = safeDecodeUtf8 <$> A.takeByteString - pwdP = jsonP <|> (UserPwd . safeDecodeUtf8 <$> A.takeTill (== ' ')) - verifyCodeP = safeDecodeUtf8 <$> A.takeWhile (\c -> isDigit c || c == ' ') - msgTextP = jsonP <|> textP - stringP = T.unpack . safeDecodeUtf8 <$> A.takeByteString - filePath = stringP - cryptoFileP = do - cfArgs <- optional $ CFArgs <$> (" key=" *> strP <* A.space) <*> (" nonce=" *> strP) - path <- filePath - pure $ CryptoFile path cfArgs - memberRole = - A.choice - [ " owner" $> GROwner, - " admin" $> GRAdmin, - " member" $> GRMember, - " observer" $> GRObserver - ] - chatNameP = ChatName <$> chatTypeP <*> displayName - chatNameP' = ChatName <$> (chatTypeP <|> pure CTDirect) <*> displayName - chatRefP = ChatRef <$> chatTypeP <*> A.decimal - msgCountP = A.space *> A.decimal <|> pure 10 - ciTTLDecimal = ("none" $> Nothing) <|> (Just <$> A.decimal) - ciTTL = - ("day" $> Just 86400) - <|> ("week" $> Just (7 * 86400)) - <|> ("month" $> Just (30 * 86400)) - <|> ("none" $> Nothing) - timedTTLP = - ("30s" $> 30) - <|> ("5min" $> 300) - <|> ("1h" $> 3600) - <|> ("8h" $> (8 * 3600)) - <|> ("day" $> 86400) - <|> ("week" $> (7 * 86400)) - <|> ("month" $> (30 * 86400)) - <|> A.decimal - timedTTLOnOffP = - optional ("on" *> A.space) *> (Just <$> timedTTLP) - <|> ("off" $> Nothing) - timedMessagesEnabledP = - optional ("yes" *> A.space) *> (TMEEnableSetTTL <$> timedTTLP) - <|> ("yes" $> TMEEnableKeepTTL) - <|> ("no" $> TMEDisableKeepTTL) - netCfgP = do - socksProxy <- "socks=" *> ("off" $> Nothing <|> "on" $> Just defaultSocksProxy <|> Just <$> strP) - t_ <- optional $ " timeout=" *> A.decimal - logErrors <- " log=" *> onOffP <|> pure False - let tcpTimeout = 1000000 * fromMaybe (maybe 5 (const 10) socksProxy) t_ - pure $ fullNetworkConfig socksProxy tcpTimeout logErrors - xftpCfgP = XFTPFileConfig <$> (" size=" *> fileSizeP <|> pure 0) - fileSizeP = - A.choice - [ gb <$> A.decimal <* "gb", - mb <$> A.decimal <* "mb", - kb <$> A.decimal <* "kb", - A.decimal - ] - dbKeyP = nonEmptyKey <$?> strP - nonEmptyKey k@(DBEncryptionKey s) = if BA.null s then Left "empty key" else Right k - dbEncryptionConfig currentKey newKey = DBEncryptionConfig {currentKey, newKey, keepKey = Just False} - autoAcceptP = - ifM - onOffP - (Just <$> (AutoAccept <$> (" incognito=" *> onOffP <|> pure False) <*> optional (A.space *> msgContentP))) - (pure Nothing) - srvCfgP = strP >>= \case AProtocolType p -> APSC p <$> (A.space *> jsonP) - toServerCfg server = ServerCfg {server, preset = False, tested = Nothing, enabled = True} - rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> text1P) - text1P = safeDecodeUtf8 <$> A.takeTill (== ' ') - char_ = optional . A.char - -adminContactReq :: ConnReqContact -adminContactReq = - either error id $ strDecode "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D" - -simplexContactProfile :: Profile -simplexContactProfile = - Profile - { displayName = "SimpleX Chat team", - fullName = "", - image = Just (ImageData "data:image/jpg;base64,/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8KCwkMEQ8SEhEPERATFhwXExQaFRARGCEYGhwdHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAETARMDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD7LooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiivP/iF4yFvv0rSpAZek0yn7v+yPeunC4WpiqihBf8A8rOc5w2UYZ4jEPTourfZDvH3jL7MW03SpR53SWUfw+w96veA/F0erRLY3zKl6owD2k/8Ar15EWLEljknqadDK8MqyxMUdTlWB5Br66WS0Hh/ZLfv1ufiNLj7Mo5m8ZJ3g9OTpy+Xn5/pofRdFcd4B8XR6tEthfMEvVHyk9JB/jXY18fiMPUw9R06i1P3PK80w2aYaOIw8rxf3p9n5hRRRWB6AUUVDe3UFlavc3MixxIMsxppNuyJnOMIuUnZIL26gsrV7m5kWOJBlmNeU+I/Gd9e6sk1hI8FvA2Y1z973NVPGnimfXLoxRFo7JD8if3vc1zefevr8syiNKPtKyvJ9Ox+F8Ycb1cdU+rYCTjTi/iWjk1+nbue3eEPEdtrtoMER3SD95Hn9R7Vu18+6bf3On3kd1aSmOVDkEd/Y17J4P8SW2vWY6R3aD97F/Ue1eVmmVPDP2lP4fyPtODeMoZrBYXFO1Zf+Tf8AB7r5o3qKKK8Q/QgooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAqavbTXmmz20Fw1vJIhVZB1FeDa3p15pWoSWl6hWQHr2YeoNfQlY3izw9Z6/YGGZQky8xSgcqf8K9jKcyWEnyzXuv8D4njLhZ51RVSi7VYLRdGu3k+z+88HzRuq1rWmXmkX8lnexFHU8Hsw9RVLNfcxlGcVKLumfgFahUozdOorSWjT6E0M0kMqyxOyOpyrKcEGvXPAPjCPVolsb9wl6owGPAkH+NeO5p8M0kMqyxOyOpyrA4INcWPy+njKfLLfoz2+HuIMTkmI9pT1i/ij0a/wA+zPpGiuM+H/jCPV4lsL91S+QfKTwJR/jXW3t1BZWslzcyLHFGMsxNfB4jC1aFX2U1r+fof0Rl2bYXMMKsVRl7vXy7p9rBfXVvZWr3NzKscSDLMTXjnjbxVPrtyYoiY7JD8if3vc0zxv4ruNeujFEWjsoz8if3vc1zOa+synKFh0qtVe9+X/BPxvjLjKWZSeEwjtSW7/m/4H5kmaM1HmlB54r3bH51YkzXo3wz8MXMc0es3ZeED/VR5wW9z7VB8O/BpnMerarEREDuhhb+L3Pt7V6cAAAAAAOgFfL5xmqs6FH5v9D9a4H4MlzQzHGq1tYR/KT/AEXzCiiivlj9hCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAxfFvh208QWBhmASdRmKUdVP+FeH63pl5pGoSWV5EUdTwezD1HtX0VWL4t8O2fiHTzBONk6g+TKByp/wr28pzZ4WXs6msH+B8NxdwhTzeDxGHVqy/8m8n59n954FmjNW9b0y80fUHsr2MpIp4PZh6iqWfevuYyjOKlF3TPwetQnRm6dRWktGmSwzSQyrLE7I6nKsDgg1teIPFOqa3a29vdy4jiUAheN7f3jWBmjNROhTnJTkrtbGtLF4ijSnRpzajPddHbuP3e9Lmo80ua0scth+a9E+HXgw3Hl6tqsZEX3oYmH3vc+1J8OPBZnKavq0eIhzDCw+9/tH29q9SAAAAGAOgr5bOM35b0KD16v8ARH6twXwXz8uPx0dN4xfXzf6IFAUAAAAdBRRRXyZ+wBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFB4GTXyj+1p+0ONJjufA3ga6DX7qU1DUY24gB4McZH8Xqe38tqFCdefLETaSufQ3h/4geEde8Uah4a0rWra51Ow/wBfCrD8ceuO+OldRX5I+GfEWseG/ENvr2j30ttqFvJ5iSqxyT3z6g96/RH9nD41aT8U9AWGcx2fiK1QC7tC33/+mieqn07V14zL3QXNHVEQnc9dooorzjQKKKKACiis7xHrel+HdGudY1m8is7K2QvLLI2AAP600m3ZAYfxUg8Pr4VutT1+7isYbSMuLp/4Pb3z6V8++HNd0zxDpq6hpVys8DHGRwVPoR2NeIftJ/G7VPifrbWVk8lp4btZD9mtwcGU/wDPR/c9h2rgfh34z1LwdrAurV2ktZCBcW5PyyD/AB9DX2WTyqYWny1Ho+nY+C4t4Wp5tF16CtVX/k3k/Ps/vPr/ADRmsjwx4g07xFpMWpaZOJInHI/iQ9wR61qbq+mVmro/D6tCdGbp1FZrdEma6/4XafpWoa7jUpV3oA0MLdJD/ntXG5p8E0kMqyxOyOhyrKcEGsMTRlWpShGVm+p1ZbiYYPFQr1IKai72fU+nFAUAKAAOABRXEfDnxpFrMK6fqDhL9BhSeko9frXb1+a4rDVMNUdOotT+k8szLD5lh44jDu8X968n5hRRRXOegFFFFABUGoXlvYWkl1dSrHFGMliaL+7t7C0kuruVYoYxlmNeI+OvFtx4huzHFuisYz+7jz97/aNenluW1MbU00it2fM8S8SUMkoXetR/DH9X5fmeteF/E+m+IFkFoxSWMnMb9cev0rbr5t0vULrTb6K8s5TFNGcgj+R9q9w8E+KbXxDYjlY7xB+9i/qPaurNsneE/eUtYfkeTwlxjHNV9XxVo1V90vTz8vmjoqKKK8I+8CiiigAooooAKKKKACiiigD5V/a8+P0mgvdeAvCUskepFdl9eDjyQR9xPfHeviiR3lkaSR2d2OWZjkk+tfoj+058CtP+Jektq2jxRWnie2T91KMKLlR/yzf+h7V+fOuaVqGiarcaXqtpLaXls5jlikXDKRX0mWSpOlaG/U56l76lKtPwtr+reGNetdb0S8ls761cPHJG2D9D6g9MVmUV6TSasyD9Jf2cfjXpPxR0MW9w0dp4gtkAubYnHmf7aeo/lXr1fkh4W1/V/DGuW2taHey2d9bOHjkjP6H1HtX6Jfs5fGvR/inoQgmeOz8RWqD7XaE439vMT1U+navnMfgHRfPD4fyN4Tvoz12iis7xJremeHdEutZ1i7jtLK1jLyyucAAf1rzUm3ZGgeJNb0vw7otzrOs3kVpZWyF5ZZDgAD+Z9q/PL9pP436r8UNZaxs2ks/Dlq5+z24ODMf77+p9B2o/aU+N2p/FDXDZ2LS2fhy1ci3t84Mx/wCej+/oO1eNV9DgMAqS55/F+RhOd9EFFFABJwBkmvUMzqPh34y1Lwjq63FszSWshAntyeHHt719Z2EstzpVlqD2txbR3kCzxLPGUbawyODXK/slfs8nUpbXx144tGFkhElhp8q4849pHB/h9B3r608X+GLDxBpX2WRFiljX9xIowUPYfT2rGnnkMPWVJ6x6vt/XU+P4o4SjmtN4igrVV/5N5Pz7P7z56zRmrmvaVe6LqMljexMkiHg9mHqKoZr6uEozipRd0z8Rq0J0ZunUVmtGmTwTSQTJNC7JIhyrKcEGvZvhz41j1mJdP1GRUv0GFY8CX/69eJZqSCaWCVZYXZHU5VlOCDXDmGXU8bT5ZaPo+x7WQZ9iMlxHtKesX8UejX+fZn1FRXDfDbxtHrUKadqDqmoIuAx4EoHf613NfnWKwtTC1HTqKzR/QGW5lh8yw8cRh3eL+9Ps/MKr6heW1hZyXd3KsUUYyzGjUby20+zku7yZYoY13MzGvDPHvi+48RXpjiZorCM/u4/73+0feuvLMsqY6pZaRW7/AK6nlcScR0MloXetR/DH9X5D/Hni648Q3nlxlo7GM/u48/e9zXL7qZmjNfodDDwoU1TpqyR+AY7G18dXlXryvJ/19w/dVvSdRutMvo7yzlaOVDkY7+xqkDmvTPhn4HMxj1jV4v3Y+aCFh97/AGjWGPxNHDUXKrt27+R15JlWLzHFxp4XSS1v/L53PQ/C+oXGqaJb3t1bNbyyLkoe/v8AQ1p0AAAAAADoBRX5nUkpSbirLsf0lh6c6dKMJy5mkrvv5hRRRUGwUUUUAFFFFABRRRQAV4d+038CdO+JWkyavo8cdp4mtkzHIBhbkD+B/f0Ne40VpSqypSUovUTV9GfkTruk6joer3Ok6taS2d7ayGOaGVdrKRVKv0T/AGnfgXp/xK0h9Y0iOO18TWqZikAwLkD+B/6Gvz51zStQ0TVbjS9UtZbW8tnKSxSLgqRX1GExccRG636o55RcSlWp4V1/VvDGvWut6JeSWl9bOGjkQ4/A+oPpWXRXU0mrMk/RP4LftDeFvF3ge41HxDfW+lappkG+/idsBwP40HfJ7V8o/tJ/G/VPifrbWVk8tn4btn/0e2zgykfxv6n0HavGwSM4JGeuO9JXFRwFKlUc18vIpzbVgoooAJIAGSa7SQr6x/ZM/Z4k1J7Xxz44tClkMSWFhIuDL3Ejg/w+g70fsmfs8NqMtt448c2eLJCJLCwlX/WnqHcH+H0HevtFFVECIoVVGAAMACvFx+PtenTfqzWEOrEjRI41jjUIigBVAwAPSnUUV4ZsYXjLwzZeJNOaCcBLhQfJmA5U/wCFeBa/pV7ompSWF9GUkToccMOxHtX01WF4z8M2XiXTTBOAk6AmGYDlD/hXvZPnEsHL2dTWD/A+K4r4UhmsHXoK1Zf+TeT8+z+8+c80Zq5r2k3ui6jJY30ZSRTwezD1FUM1+gQlGcVKLumfiFWjOjN06is1umTwTSQTJNE7JIh3KynBBr2PwL8QrO701odbnSC5t0yZCcCUD+teK5pd1cWPy2ljoctTdbPqetkme4rJ6rqUHdPdPZ/8Mdb4/wDGFz4ivDFGxisIz+7j/ve5rls1HuozXTQw1PD01TpqyR5+OxlfHV5V68ryf9fcSZozTAa9P+GHgQzmPWdZhIjHzQQMPvf7R9qxxuMpYOk6lR/8E6MpyfEZriFQoL1fRLux/wAMvApmMesazFiP70EDfxf7R9vavWFAUAAAAcACgAAAAAAdBRX5xjsdVxtXnn8l2P3/ACXJcNlGHVGivV9W/wCugUUUVxHrhRRRQAUUUUAFFFFABRRRQAUUUUAFeH/tOfArT/iXpUmsaSsVp4mto/3UuMLcgDhH/oe1e4Vn+I9a0zw7otzrGsXkVpZWyF5ZZGwAB/WtaNSdOalDcTSa1PyZ1zStQ0TVrnStVtZLS8tnMcsUgwVIqlXp/wC0l8S7T4nePn1aw0q3srO3XyYJBGBNOoPDSHv7DtXmFfXU5SlBOSszlYUUUVYAAScDk19Zfsmfs7vqLW3jjx1ZFLMESafYSjmXuJHHZfQd6+VtLvJtO1K2v7cRtLbyrKgkQOpKnIyp4I46Gv0b/Zv+NOjfFDw+lrIIrDX7RAtzZ8AMMffj9V9u1efmVSrCn7m3Vl00m9T16NEjjWONVRFGFUDAA9KWiivmToCiiigAooooAwfGnhiy8S6cYJwEuEH7mYDlT/hXz7r+k32h6lJYahFskQ8Hsw9QfSvpjUr2106ykvLyZYYYxlmY18+/EXxa/ijU1aOMRWkGRCCBuPuT/Svr+GK2KcnTSvT/ACfl/kfmPiBhMvUI1m7Vn0XVefp0fy9Oa3UbqZmjNfa2PynlJM+9AOajzTo5GjkV0YqynIPoaVg5T1P4XeA/P8vWdaiIj+9BAw+9/tH29q9dAAAAAAHQVwPwx8dQ63Ammai6R6hGuFJ4Ew9vf2rvq/Ms5qYmeJaxGjWy6W8j+gOFcPl9LAReBd0931b8+3oFFFFeSfSBRRRQAUUUUAFFFFABRRRQAUUUUAFFFZ3iTW9L8OaJdazrN5HaWNqheWWQ4AH+NNJt2QB4l1vTPDmiXWs6xdx2llaxl5ZHOAAO3ufavzx/aT+N2qfFDWzZWbSWfhy2ci3tg2DKf77+p9B2pf2lfjdqfxQ1trGxeW08N2z/AOj2+cGYj/lo/v6DtXjVfQ4DAKkuefxfkYTnfRBRRQAScAZNeoZhRXv3w2/Zh8V+Lfh7deJprgadcvHv02zlT5rgdcsf4Qe1eHa5pWoaJq1zpWq2ktpeW0hjlikXDKwrOFanUk4xd2htNFKtTwrr+reGNdtta0S8ltL22cPHIhx07H1HtWXRWjSasxH6S/s4/GrSfijoYtp3jtfENqg+1WpON4/vp6j27V69X5IeFfEGr+F9etdc0O9ks7+1cPHKh/QjuD3Ffoj+zl8bNI+KWhLbztFZ+IraMfa7TON+Osieqn07V85j8A6L54fD+RvCd9GevUUUV5hoFVtTvrXTbGW9vJligiXczNRqd9aabYy3t7MsMEQyzMa+ffiN42uvE96YoS0OmxH91F3b/ab3r1spympmFSy0it3+i8z57iDiCjlFG71qPZfq/Id8RPGl14lvTFEzRafGf3cf97/aNclmmZozX6Xh8NTw1NU6askfheNxdbG1pV68ryY/NGTTM16R4J+GVxrGkSX+pSSWfmJ/oq45J7MR6Vni8ZRwkOes7I1y7K8TmNX2WHjd7/0zzvJozV3xDpF7oepyWF/EUkQ8HHDD1FZ+feuiEozipRd0zjq0Z0puE1ZrdE0E8sEyTQu0ciHKspwQa9z+GHjuLXIU0zUpFTUEXCseBKB/WvBs1JBPLBMk0LmORCGVlOCDXn5lllLH0uWWjWz7HsZFnlfJ6/tKesXuu6/z7M+tKK4D4X+PItdhTTNSdY9SQYVicCYDuPf2rv6/M8XhKuEqulVVmj92y7MaGYUFXoO6f4Ps/MKKKK5juCiiigAooooAKKKKACiig9KAM7xLrmleG9EudZ1q8jtLG2QvLK5wAPQep9q/PH9pP43ap8T9beyspJbTw3bSH7NbZx5pH8b+p9u1bH7YPxL8XeJPG114V1G0udH0jT5SIrNuDOR0kbs2e3pXgdfRZfgVTSqT3/IwnO+iCiigAkgAZJr1DMK+s/2TP2d31Brbxz46tNtmMSafp8i8y9/MkB6L0wO9J+yb+zwdSe28b+ObLFmpEljYSr/rT1DuP7voO9faCKqIERQqqMAAYAFeLj8fa9Om/VmsIdWEaJGixooVFGFUDAA9K8Q/ac+BWnfErSZNY0mOO08T2yZilAwtyAPuP/Q9q9worx6VWVKSlF6mrSasfkTrmlahomrXOlaray2l7bSGOaKRcMrCqVfon+098C7D4l6U+s6Skdr4mtY/3UmMC5UdI29/Q1+fOt6XqGi6rcaVqlrJa3ls5SWKQYKkV9RhMXHERut+qOeUeUpVqeFfEGreGNdttb0W7ktb22cNG6HH4H1FZdFdTSasyT9Jf2cPjVpXxR0Fbe4eK18Q2qD7Va7sbx/z0T1H8q9V1O+tdNsZb29mWGCJdzMxr8ovAOoeIdK8W2GoeF5podVhlDQtEefcH2PevsbxP4417xTp1jDq3lQGKFPOigJ2NLj5m59849K4KHD0sTX9x2h18vJHj55xDSyqhd61Hsv1fkaXxG8bXXie9MURaLTo2/dR5+9/tH3rkM1HmjNffYfC08NTVOmrJH4ljMXWxtaVau7yZJmgHmmAmvWfhN8PTceVrmuQkRDDW9uw+9/tN7Vjj8dSwNJ1ar9F3OjK8pr5nXVGivV9Eu7H/Cf4emcx63rkJEfDW9u4+9/tMPT2r2RQFAVQABwAKAAAAAAB0Aor8uzDMKuOq+0qfJdj9zyjKMPlVBUaK9X1bOf8b+FbHxRppt7gCO4UfuZwOUP9R7V86+IdHv8AQtTk0/UIikqHg9mHqD6V9VVz3jnwrY+KNMNvcKEuEBME2OUP+FenkmdywUvZVdab/A8PijheGZw9vQVqq/8AJvJ+fZnzLuo3Ve8Q6Pf6FqclhqERjkQ8Hsw9Qazs1+jwlGpFSi7pn4xVozpTcJqzW6J7eeSCZJoZGjkQhlZTgg17t8LvHsWuQppmpOseooMKxPEw/wAa8DzV3Q7fULvVIIdLWQ3ZcGMx8EH1z2rzs1y2jjaLVTRrZ9v+AezkGcYnK8SpUVzKWjj3/wCD2PrCiqOgx38Oj20eqTJNeLGBK6jAJq9X5VOPLJq9z98pyc4KTVr9H0CiiipLCiiigAooooAKKKKAPK/2hfg3o/xT8PFdsVprlupNnebec/3W9VNfnR4y8Naz4R8RXWg69ZvaXts5V1YcEdmB7g9jX6115V+0P8GtF+Knh05SO0161UmzvQuD/uP6qf0r08DjnRfJP4fyM5wvqj80RycCvrP9kz9ndtRNr458dWTLaAiTT9PlXBl9JJB/d7gd+tXv2bv2Y7yz19vEHxFs1VbKYi1sCQwlZTw7f7PcDvX2CiLGioihVUYAAwAK6cfmGns6T9WTCHVhGiRoqRqFRRgKBgAUtFFeGbBRRRQAV4h+038CtP8AiZpTatpCQ2fia2jPlS4wtyo52P8A0Pavb6K0pVZUpKUXqJq+jPyJ1zStQ0TVrnStVtJbS9tnMcsUgwVIqPS7C61O+isrKFpZ5W2qor9AP2r/AIM6J448OzeJLV7fTtesoyRO3yrcqP4H9/Q14F8OvBlp4XsvMkCTajKP3suM7f8AZX0H86+1yiDzFcy0S3Pms+zqllNLXWb2X6vyH/DnwZaeF7EPIEm1CUDzZcfd/wBke1dfmo80ua+0pUY0oqMVofjWLxNXF1XWrO8mSZozUea9N+B/hTTdau5NUv5opvsrjbak8k9mYelc+OxcMHQlWqbI1y3LqmYYmOHpbvuafwj+HhnMWva5DiMENb27D73ozD09q9oAAAAAAHQCkUBVCqAAOABS1+U5jmNXH1XUqfJdj9yyjKKGV0FRor1fVsKKKK4D1AooooA57xz4UsPFOmG3uFEdwgJgnA5Q/wBR7V84eI9Gv9A1SXT9RhMcqHg/wuOxB7ivrCud8d+E7DxTpZt51CXKDMEwHKn/AAr6LI88lgpeyq603+Hmv1Pj+J+GIZnB16KtVX/k3k/Psz5p0uxu9Tv4rGxheaeVtqIoyTX0T8OPBNp4XsRJKFm1GQfvZf7v+yvtR8OfBFn4UtDIxW41CUfvJsdB/dX0FdfWue568W3RoP3Pz/4BhwvwtHL0sTiVeq9l/L/wQooor5g+3CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKrarf2ml2E19fTpBbwrud2OAKTVdQtNLsJb6+mWGCJcszGvm34nePLzxXfmGEtDpkTfuos/f/wBpvevZyfJ6uZVbLSC3f6LzPBz3PaOVUbvWb2X6vyH/ABM8d3fiq/MULPDpsR/dRdN3+03vXF5pm6jdX6phsLTw1JUqSskfjGLxVbGVnWrO8mSZ96M0wGnSq8UhjkRkdeCrDBFb2OXlFzWn4b1y/wBA1SPUNPmMciHkdmHoR6Vk7hS596ipTjUi4zV0y6c50pqcHZrZn1X4C8W2HizShc27BLmMATwZ5Q/4V0dfIfhvXL/w/qseo6dMY5U6js47gj0r6Y8BeLtP8WaUtzbER3KAefATyh/qPevzPPshlgJe1pa03+Hk/wBGfr/DfEkcygqNbSqv/JvNefdHSUUUV80fWhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFVtVv7TS7CW+vp1ht4l3O7HpSatqNnpWny319OsMES7mZjXzP8UfH154tv8AyYWeDS4WPlQ5xvP95vU/yr2smyarmVWy0gt3+i8zws8zylldK71m9l+r8h/xP8eXfiy/MUJaHTIm/cxZ5b/ab3ris0zNGa/V8NhaWFpKlSVkj8bxeKrYuq61Z3kx+aX2pmTXsnwc+GrXBh8Qa/CViB3W9sw5b0Zh6e1YZhj6OAourVfourfY3y3LK+Y11Ror1fRLux3wc+GxuPK1/X4SIgQ1tbuPvf7TD09BXT/Fv4dQ6/bPqukxpFqca5KgYE4Hb6+9ekKAqhVAAHAApa/L62fYupi1ilKzWy6W7f5n63R4bwVPBPBuN0931v3/AMj4wuIZred4J42jlQlWVhgg0zNfRHxc+HUXiCB9W0mNI9TRcso4EwH9a+eLiKW2neCeNo5UO1kYYIPpX6TlOa0cypc8NJLddv8AgH5XnOS1srrck9YvZ9/+CJmtPw1rl/4f1WLUdPmMcqHkZ4Yeh9qys0Zr0qlONSLhNXTPKpznSmpwdmtmfWHgDxfp/i3SVubZhHcoAJ4CfmQ/1HvXSV8feGdd1Dw9q0WpabMY5UPIz8rr3UjuK+nPAHjDT/FulLcW7CO6QYngJ5Q/1FfmGfZBLAS9rS1pv8PJ/oz9c4c4jjmMFRraVV/5N5rz7o6WiiivmT6wKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAOY+JXhRfFvh5rAXDwTod8LA/KW9GHcV8s65pV/oupzadqNu0FxC2GVu/uPUV9m1x/xM8DWHi/TD8qw6jEP3E4HP+6fUV9Tw7n7wEvY1v4b/AAf+Xc+S4k4eWYR9vR/iL8V29ex8q5o+gq9ruk32i6nLp2oQNFPG2CCOvuPUV6v8Gvhk1w0PiDxDBiH71tbOPvejMPT2r9Cx2Z4fB4f283o9rdfQ/OMBlWIxuI+rwjZre/T1F+DPw0NwYfEPiCDEQ+a2tnH3vRmHp6Cvc1AVQqgADgAUKoVQqgAAYAHalr8lzPMq2Y1nVqv0XRI/YsryuhltBUqS9X1bCiiivOPSCvNfi98OYvEVu+raTEseqRrllHAnHoff3r0qiuvBY2tgqyq0nZr8fJnHjsDRx1F0ayun+Hmj4ruIZbad4J42ilQlWRhgg1Hmvoz4vfDiLxDA+raRGseqRjLIOBOP8a8AsdI1K91hdIgtJDetJ5ZiK4Knvn0xX6zleb0Mwoe1Ts1uu3/A8z8dzbJK+XYj2TV0/hff/g+Q3SbC81XUIbCwgee4mYKiKOpr6a+F3ga28IaaWkYTajOo8+Tsv+yvtTPhd4DtPCWnCWULNqcq/vZcfd/2V9q7avh+IeIHjG6FB/u1u+//AAD73hrhuOBSxGIV6j2X8v8AwQooor5M+xCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAxdd8LaHrd/a32pWKTT2rbo2Pf2PqK2VAVQqgAAYAHalorSVWc4qMm2lt5GcKNOEnKMUm9/MKKKKzNAooooAKKKKACs+HRdLh1iXV4rKFb6VQrzBfmIrQoqozlG/K7XJlCMrOSvYKKKKkoKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//2Q=="), - contactLink = Just adminContactReq, - preferences = Nothing - } - -timeItToView :: ChatMonad' m => String -> m a -> m a -timeItToView s action = do - t1 <- liftIO getCurrentTime - a <- action - t2 <- liftIO getCurrentTime - let diff = diffToMilliseconds $ diffUTCTime t2 t1 - toView $ CRTimedAction s diff - pure a - -mkValidName :: String -> String -mkValidName = reverse . dropWhile isSpace . fst3 . foldl' addChar ("", '\NUL', 0 :: Int) - where - fst3 (x, _, _) = x - addChar (r, prev, punct) c = if validChar then (c' : r, c', punct') else (r, prev, punct) - where - c' = if isSpace c then ' ' else c - punct' - | isPunctuation c = punct + 1 - | isSpace c = punct - | otherwise = 0 - validChar - | c == '\'' = False - | prev == '\NUL' = c > ' ' && c /= '#' && c /= '@' && validFirstChar - | isSpace prev = validFirstChar || (punct == 0 && isPunctuation c) - | isPunctuation prev = validFirstChar || isSpace c || (punct < 3 && isPunctuation c) - | otherwise = validFirstChar || isSpace c || isMark c || isPunctuation c - validFirstChar = isLetter c || isNumber c || isSymbol c + (enbldSrvs', srvsToDisable) <- splitAt toUse <$> shuffle enbldSrvs + let dsbldSrvs' = map (\srv -> (srv :: NewUserServer p) {enabled = False}) srvsToDisable + pure $ sortOn server' $ enbldSrvs' <> dsbldSrvs' <> dsbldSrvs + server' UserServer {server = ProtoServerWithAuth srv _} = srv + +toJustOrError :: String -> Maybe a -> IO a +toJustOrError name = \case + Just a -> pure a + Nothing -> do + putStrLn $ name <> ": expected Just, exiting" + E.throwIO $ userError name diff --git a/src/Simplex/Chat/AppSettings.hs b/src/Simplex/Chat/AppSettings.hs new file mode 100644 index 0000000000..1efa69fad4 --- /dev/null +++ b/src/Simplex/Chat/AppSettings.hs @@ -0,0 +1,280 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE StrictData #-} +{-# LANGUAGE TemplateHaskell #-} + +module Simplex.Chat.AppSettings where + +import Control.Applicative ((<|>)) +import Data.Aeson (FromJSON (..), (.:?)) +import qualified Data.Aeson as J +import qualified Data.Aeson.TH as JQ +import Data.Map.Strict (Map) +import Data.Maybe (fromMaybe) +import Data.Text (Text) +import Simplex.Chat.Types.UITheme +import Simplex.Messaging.Client (NetworkConfig, defaultNetworkConfig) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) +import Simplex.Messaging.Util (catchAll_) + +data AppPlatform = APIOS | APAndroid | APDesktop deriving (Show) + +data NotificationMode = NMOff | NMPeriodic | NMInstant deriving (Show) + +data NotificationPreviewMode = NPMHidden | NPMContact | NPMMessage deriving (Show) + +data LockScreenCalls = LSCDisable | LSCShow | LSCAccept deriving (Show) + +data AppSettings = AppSettings + { appPlatform :: Maybe AppPlatform, + networkConfig :: Maybe NetworkConfig, + networkProxy :: Maybe NetworkProxy, + privacyEncryptLocalFiles :: Maybe Bool, + privacyAskToApproveRelays :: Maybe Bool, + privacyAcceptImages :: Maybe Bool, + privacyLinkPreviews :: Maybe Bool, + privacyShowChatPreviews :: Maybe Bool, + privacySaveLastDraft :: Maybe Bool, + privacyProtectScreen :: Maybe Bool, + privacyMediaBlurRadius :: Maybe Int, + notificationMode :: Maybe NotificationMode, + notificationPreviewMode :: Maybe NotificationPreviewMode, + webrtcPolicyRelay :: Maybe Bool, + webrtcICEServers :: Maybe [Text], + confirmRemoteSessions :: Maybe Bool, + connectRemoteViaMulticast :: Maybe Bool, + connectRemoteViaMulticastAuto :: Maybe Bool, + developerTools :: Maybe Bool, + confirmDBUpgrades :: Maybe Bool, + androidCallOnLockScreen :: Maybe LockScreenCalls, + iosCallKitEnabled :: Maybe Bool, + iosCallKitCallsInRecents :: Maybe Bool, + uiProfileImageCornerRadius :: Maybe Double, + uiChatItemRoundness :: Maybe Double, + uiChatItemTail :: Maybe Bool, + uiColorScheme :: Maybe UIColorScheme, + uiDarkColorScheme :: Maybe DarkColorScheme, + uiCurrentThemeIds :: Maybe (Map ThemeColorScheme Text), + uiThemes :: Maybe [UITheme], + oneHandUI :: Maybe Bool, + chatBottomBar :: Maybe Bool + } + deriving (Show) + +data NetworkProxy = NetworkProxy + { host :: Text, + port :: Int, + auth :: NetworkProxyAuth, + username :: Text, + password :: Text + } + deriving (Show) + +data NetworkProxyAuth = NPAUsername | NPAIsolate + deriving (Show) + +defaultAppSettings :: AppSettings +defaultAppSettings = + AppSettings + { appPlatform = Nothing, + networkConfig = Just defaultNetworkConfig, + networkProxy = Nothing, + privacyEncryptLocalFiles = Just True, + privacyAskToApproveRelays = Just True, + privacyAcceptImages = Just True, + privacyLinkPreviews = Just True, + privacyShowChatPreviews = Just True, + privacySaveLastDraft = Just True, + privacyProtectScreen = Just False, + privacyMediaBlurRadius = Just 0, + notificationMode = Just NMInstant, + notificationPreviewMode = Just NPMMessage, + webrtcPolicyRelay = Just True, + webrtcICEServers = Just [], + confirmRemoteSessions = Just False, + connectRemoteViaMulticast = Just True, + connectRemoteViaMulticastAuto = Just True, + developerTools = Just False, + confirmDBUpgrades = Just False, + androidCallOnLockScreen = Just LSCShow, + iosCallKitEnabled = Just True, + iosCallKitCallsInRecents = Just False, + uiProfileImageCornerRadius = Just 22.5, + uiChatItemRoundness = Just 0.75, + uiChatItemTail = Just True, + uiColorScheme = Just UCSSystem, + uiDarkColorScheme = Just DCSSimplex, + uiCurrentThemeIds = Nothing, + uiThemes = Nothing, + oneHandUI = Just True, + chatBottomBar = Just True + } + +defaultParseAppSettings :: AppSettings +defaultParseAppSettings = + AppSettings + { appPlatform = Nothing, + networkConfig = Nothing, + networkProxy = Nothing, + privacyEncryptLocalFiles = Nothing, + privacyAskToApproveRelays = Nothing, + privacyAcceptImages = Nothing, + privacyLinkPreviews = Nothing, + privacyShowChatPreviews = Nothing, + privacySaveLastDraft = Nothing, + privacyProtectScreen = Nothing, + privacyMediaBlurRadius = Nothing, + notificationMode = Nothing, + notificationPreviewMode = Nothing, + webrtcPolicyRelay = Nothing, + webrtcICEServers = Nothing, + confirmRemoteSessions = Nothing, + connectRemoteViaMulticast = Nothing, + connectRemoteViaMulticastAuto = Nothing, + developerTools = Nothing, + confirmDBUpgrades = Nothing, + androidCallOnLockScreen = Nothing, + iosCallKitEnabled = Nothing, + iosCallKitCallsInRecents = Nothing, + uiProfileImageCornerRadius = Nothing, + uiChatItemRoundness = Nothing, + uiChatItemTail = Nothing, + uiColorScheme = Nothing, + uiDarkColorScheme = Nothing, + uiCurrentThemeIds = Nothing, + uiThemes = Nothing, + oneHandUI = Nothing, + chatBottomBar = Nothing + } + +combineAppSettings :: AppSettings -> AppSettings -> AppSettings +combineAppSettings platformDefaults storedSettings = + AppSettings + { appPlatform = p appPlatform, + networkConfig = p networkConfig, + networkProxy = p networkProxy, + privacyEncryptLocalFiles = p privacyEncryptLocalFiles, + privacyAskToApproveRelays = p privacyAskToApproveRelays, + privacyAcceptImages = p privacyAcceptImages, + privacyLinkPreviews = p privacyLinkPreviews, + privacyShowChatPreviews = p privacyShowChatPreviews, + privacySaveLastDraft = p privacySaveLastDraft, + privacyProtectScreen = p privacyProtectScreen, + privacyMediaBlurRadius = p privacyMediaBlurRadius, + notificationMode = p notificationMode, + notificationPreviewMode = p notificationPreviewMode, + webrtcPolicyRelay = p webrtcPolicyRelay, + webrtcICEServers = p webrtcICEServers, + confirmRemoteSessions = p confirmRemoteSessions, + connectRemoteViaMulticast = p connectRemoteViaMulticast, + connectRemoteViaMulticastAuto = p connectRemoteViaMulticastAuto, + developerTools = p developerTools, + confirmDBUpgrades = p confirmDBUpgrades, + iosCallKitEnabled = p iosCallKitEnabled, + iosCallKitCallsInRecents = p iosCallKitCallsInRecents, + androidCallOnLockScreen = p androidCallOnLockScreen, + uiProfileImageCornerRadius = p uiProfileImageCornerRadius, + uiChatItemRoundness = p uiChatItemRoundness, + uiChatItemTail = p uiChatItemTail, + uiColorScheme = p uiColorScheme, + uiDarkColorScheme = p uiDarkColorScheme, + uiCurrentThemeIds = p uiCurrentThemeIds, + uiThemes = p uiThemes, + oneHandUI = p oneHandUI, + chatBottomBar = p chatBottomBar + } + where + p :: (AppSettings -> Maybe a) -> Maybe a + p sel = sel storedSettings <|> sel platformDefaults <|> sel defaultAppSettings + +$(JQ.deriveJSON (enumJSON $ dropPrefix "AP") ''AppPlatform) + +$(JQ.deriveJSON (enumJSON $ dropPrefix "NM") ''NotificationMode) + +$(JQ.deriveJSON (enumJSON $ dropPrefix "NPM") ''NotificationPreviewMode) + +$(JQ.deriveJSON (enumJSON $ dropPrefix "LSC") ''LockScreenCalls) + +$(JQ.deriveJSON (enumJSON $ dropPrefix "NPA") ''NetworkProxyAuth) + +$(JQ.deriveJSON defaultJSON ''NetworkProxy) + +$(JQ.deriveToJSON defaultJSON ''AppSettings) + +instance FromJSON AppSettings where + parseJSON (J.Object v) = do + appPlatform <- p "appPlatform" + networkConfig <- p "networkConfig" + networkProxy <- p "networkProxy" + privacyEncryptLocalFiles <- p "privacyEncryptLocalFiles" + privacyAskToApproveRelays <- p "privacyAskToApproveRelays" + privacyAcceptImages <- p "privacyAcceptImages" + privacyLinkPreviews <- p "privacyLinkPreviews" + privacyShowChatPreviews <- p "privacyShowChatPreviews" + privacySaveLastDraft <- p "privacySaveLastDraft" + privacyProtectScreen <- p "privacyProtectScreen" + privacyMediaBlurRadius <- p "privacyMediaBlurRadius" + notificationMode <- p "notificationMode" + notificationPreviewMode <- p "notificationPreviewMode" + webrtcPolicyRelay <- p "webrtcPolicyRelay" + webrtcICEServers <- p "webrtcICEServers" + confirmRemoteSessions <- p "confirmRemoteSessions" + connectRemoteViaMulticast <- p "connectRemoteViaMulticast" + connectRemoteViaMulticastAuto <- p "connectRemoteViaMulticastAuto" + developerTools <- p "developerTools" + confirmDBUpgrades <- p "confirmDBUpgrades" + iosCallKitEnabled <- p "iosCallKitEnabled" + iosCallKitCallsInRecents <- p "iosCallKitCallsInRecents" + androidCallOnLockScreen <- p "androidCallOnLockScreen" + uiProfileImageCornerRadius <- p "uiProfileImageCornerRadius" + uiChatItemRoundness <- p "uiChatItemRoundness" + uiChatItemTail <- p "uiChatItemTail" + uiColorScheme <- p "uiColorScheme" + uiDarkColorScheme <- p "uiDarkColorScheme" + uiCurrentThemeIds <- p "uiCurrentThemeIds" + uiThemes <- p "uiThemes" + oneHandUI <- p "oneHandUI" + chatBottomBar <- p "chatBottomBar" + pure + AppSettings + { appPlatform, + networkConfig, + networkProxy, + privacyEncryptLocalFiles, + privacyAskToApproveRelays, + privacyAcceptImages, + privacyLinkPreviews, + privacyShowChatPreviews, + privacySaveLastDraft, + privacyProtectScreen, + privacyMediaBlurRadius, + notificationMode, + notificationPreviewMode, + webrtcPolicyRelay, + webrtcICEServers, + confirmRemoteSessions, + connectRemoteViaMulticast, + connectRemoteViaMulticastAuto, + developerTools, + confirmDBUpgrades, + iosCallKitEnabled, + iosCallKitCallsInRecents, + androidCallOnLockScreen, + uiProfileImageCornerRadius, + uiChatItemRoundness, + uiChatItemTail, + uiColorScheme, + uiDarkColorScheme, + uiCurrentThemeIds, + uiThemes, + oneHandUI, + chatBottomBar + } + where + p key = v .:? key <|> pure Nothing + parseJSON _ = pure defaultParseAppSettings + +readAppSettings :: FilePath -> Maybe AppSettings -> IO AppSettings +readAppSettings f platformDefaults = + combineAppSettings (fromMaybe defaultAppSettings platformDefaults) . fromMaybe defaultParseAppSettings + <$> (J.decodeFileStrict f `catchAll_` pure Nothing) diff --git a/src/Simplex/Chat/Archive.hs b/src/Simplex/Chat/Archive.hs index d386b48d40..2cbc941b44 100644 --- a/src/Simplex/Chat/Archive.hs +++ b/src/Simplex/Chat/Archive.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -9,7 +10,7 @@ module Simplex.Chat.Archive importArchive, deleteStorage, sqlCipherExport, - archiveFilesFolder, + sqlCipherTestKey, ) where @@ -20,11 +21,14 @@ import Control.Monad.Reader import qualified Data.ByteArray as BA import Data.Functor (($>)) import Data.Maybe (fromMaybe) +import Data.Text (Text) import qualified Data.Text as T import qualified Database.SQLite3 as SQL import Simplex.Chat.Controller +import Simplex.Chat.Util () import Simplex.Messaging.Agent.Client (agentClientStore) -import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), closeSQLiteStore, keyString, sqlString, storeKey) +import Simplex.Messaging.Agent.Store.SQLite (closeDBStore, keyString, sqlString, storeKey) +import Simplex.Messaging.Agent.Store.SQLite.Common (DBStore (..)) import Simplex.Messaging.Util import System.FilePath import UnliftIO.Directory @@ -42,124 +46,136 @@ archiveChatDbFile = "simplex_v1_chat.db" archiveFilesFolder :: String archiveFilesFolder = "simplex_v1_files" -exportArchive :: ChatMonad m => ArchiveConfig -> m () +archiveAssetsFolder :: String +archiveAssetsFolder = "simplex_v1_assets" + +wallpapersFolder :: String +wallpapersFolder = "wallpapers" + +exportArchive :: ArchiveConfig -> CM' [ArchiveError] exportArchive cfg@ArchiveConfig {archivePath, disableCompression} = withTempDir cfg "simplex-chat." $ \dir -> do - StorageFiles {chatStore, agentStore, filesPath} <- storageFiles + StorageFiles {chatStore, agentStore, filesPath, assetsPath} <- storageFiles copyFile (dbFilePath chatStore) $ dir </> archiveChatDbFile copyFile (dbFilePath agentStore) $ dir </> archiveAgentDbFile - forM_ filesPath $ \fp -> - copyDirectoryFiles fp $ dir </> archiveFilesFolder + errs <- + forM filesPath $ \fp -> + copyValidDirectoryFiles entrySelectorError fp $ dir </> archiveFilesFolder + forM_ assetsPath $ \fp -> + copyDirectoryFiles (fp </> wallpapersFolder) $ dir </> archiveAssetsFolder </> wallpapersFolder let method = if disableCompression == Just True then Z.Store else Z.Deflate Z.createArchive archivePath $ Z.packDirRecur method Z.mkEntrySelector dir + pure $ fromMaybe [] errs + where + entrySelectorError f = (Z.mkEntrySelector f $> Nothing) `E.catchAny` (pure . Just . show) -importArchive :: ChatMonad m => ArchiveConfig -> m [ArchiveError] +importArchive :: ArchiveConfig -> CM' [ArchiveError] importArchive cfg@ArchiveConfig {archivePath} = withTempDir cfg "simplex-chat." $ \dir -> do Z.withArchive archivePath $ Z.unpackInto dir - fs@StorageFiles {chatStore, agentStore, filesPath} <- storageFiles - liftIO $ closeSQLiteStore `withStores` fs + fs@StorageFiles {chatStore, agentStore, filesPath, assetsPath} <- storageFiles + liftIO $ closeDBStore `withStores` fs backup `withDBs` fs copyFile (dir </> archiveChatDbFile) $ dbFilePath chatStore copyFile (dir </> archiveAgentDbFile) $ dbFilePath agentStore - copyFiles dir filesPath - `E.catch` \(e :: E.SomeException) -> pure [AEImport . ChatError . CEException $ show e] + errs <- copyFiles (dir </> archiveFilesFolder) filesPath + errs' <- copyFiles (dir </> archiveAssetsFolder </> wallpapersFolder) ((</> wallpapersFolder) <$> assetsPath) + pure $ errs <> errs' where backup f = whenM (doesFileExist f) $ copyFile f $ f <> ".bak" - copyFiles dir filesPath = do - let filesDir = dir </> archiveFilesFolder - case filesPath of - Just fp -> - ifM - (doesDirectoryExist filesDir) - (copyDirectoryFiles filesDir fp) - (pure []) - _ -> pure [] + copyFiles fromDir = \case + Just fp -> + ifM + (doesDirectoryExist fromDir) + (copyDirectoryFiles fromDir fp) + (pure []) + `E.catch` \(e :: E.SomeException) -> pure [AEImport $ show e] + _ -> pure [] -withTempDir :: ChatMonad m => ArchiveConfig -> (String -> (FilePath -> m a) -> m a) +withTempDir :: ArchiveConfig -> (String -> (FilePath -> CM' a) -> CM' a) withTempDir cfg = case parentTempDirectory (cfg :: ArchiveConfig) of Just tmpDir -> withTempDirectory tmpDir _ -> withSystemTempDirectory -copyDirectoryFiles :: ChatMonad m => FilePath -> FilePath -> m [ArchiveError] -copyDirectoryFiles fromDir toDir = do - createDirectoryIfMissing False toDir +copyDirectoryFiles :: FilePath -> FilePath -> CM' [ArchiveError] +copyDirectoryFiles fromDir toDir = copyValidDirectoryFiles (\_ -> pure Nothing) fromDir toDir + +copyValidDirectoryFiles :: (FilePath -> IO (Maybe String)) -> FilePath -> FilePath -> CM' [ArchiveError] +copyValidDirectoryFiles isFileError fromDir toDir = do + createDirectoryIfMissing True toDir fs <- listDirectory fromDir foldM copyFileCatchError [] fs where copyFileCatchError fileErrs f = - (copyDirectoryFile f $> fileErrs) - `E.catch` \(e :: E.SomeException) -> pure (AEImportFile f (ChatError . CEException $ show e) : fileErrs) + liftIO (isFileError f) >>= \case + Nothing -> + (copyDirectoryFile f $> fileErrs) + `E.catch` \(e :: E.SomeException) -> addErr $ show e + Just e -> addErr e + where + addErr e = pure $ AEFileError f e : fileErrs copyDirectoryFile f = do let fn = takeFileName f f' = fromDir </> fn whenM (doesFileExist f') $ copyFile f' $ toDir </> fn -deleteStorage :: ChatMonad m => m () +deleteStorage :: CM () deleteStorage = do - fs <- storageFiles - liftIO $ closeSQLiteStore `withStores` fs + fs <- lift storageFiles + liftIO $ closeDBStore `withStores` fs remove `withDBs` fs mapM_ removeDir $ filesPath fs + mapM_ removeDir $ assetsPath fs mapM_ removeDir =<< chatReadVar tempDirectory where remove f = whenM (doesFileExist f) $ removeFile f removeDir d = whenM (doesDirectoryExist d) $ removePathForcibly d data StorageFiles = StorageFiles - { chatStore :: SQLiteStore, - agentStore :: SQLiteStore, - filesPath :: Maybe FilePath + { chatStore :: DBStore, + agentStore :: DBStore, + filesPath :: Maybe FilePath, + assetsPath :: Maybe FilePath } -storageFiles :: ChatMonad m => m StorageFiles +storageFiles :: CM' StorageFiles storageFiles = do - ChatController {chatStore, filesFolder, smpAgent} <- ask + ChatController {chatStore, filesFolder, assetsDirectory, smpAgent} <- ask let agentStore = agentClientStore smpAgent filesPath <- readTVarIO filesFolder - pure StorageFiles {chatStore, agentStore, filesPath} + assetsPath <- readTVarIO assetsDirectory + pure StorageFiles {chatStore, agentStore, filesPath, assetsPath} -sqlCipherExport :: forall m. ChatMonad m => DBEncryptionConfig -> m () +sqlCipherExport :: DBEncryptionConfig -> CM () sqlCipherExport DBEncryptionConfig {currentKey = DBEncryptionKey key, newKey = DBEncryptionKey key', keepKey} = when (key /= key') $ do - fs <- storageFiles + fs <- lift storageFiles checkFile `withDBs` fs backup `withDBs` fs checkEncryption `withStores` fs removeExported `withDBs` fs export `withDBs` fs -- closing after encryption prevents closing in case wrong encryption key was passed - liftIO $ closeSQLiteStore `withStores` fs + liftIO $ closeDBStore `withStores` fs (moveExported `withStores` fs) `catchChatError` \e -> (restore `withDBs` fs) >> throwError e where backup f = copyFile f (f <> ".bak") restore f = copyFile (f <> ".bak") f checkFile f = unlessM (doesFileExist f) $ throwDBError $ DBErrorNoFile f - checkEncryption SQLiteStore {dbKey} = do + checkEncryption DBStore {dbKey} = do enc <- maybe True (not . BA.null) <$> readTVarIO dbKey when (enc && BA.null key) $ throwDBError DBErrorEncrypted when (not enc && not (BA.null key)) $ throwDBError DBErrorPlaintext exported = (<> ".exported") removeExported f = whenM (doesFileExist $ exported f) $ removeFile (exported f) - moveExported SQLiteStore {dbFilePath = f, dbKey} = do + moveExported DBStore {dbFilePath = f, dbKey} = do renameFile (exported f) f atomically $ writeTVar dbKey $ storeKey key' (fromMaybe False keepKey) export f = do withDB f (`SQL.exec` exportSQL) DBErrorExport - withDB (exported f) (`SQL.exec` testSQL) DBErrorOpen + withDB (exported f) (`SQL.exec` testSQL key') DBErrorOpen where - withDB f' a err = - liftIO (bracket (SQL.open $ T.pack f') SQL.close a $> Nothing) - `catch` checkSQLError - `catch` (\(e :: SomeException) -> sqliteError' e) - >>= mapM_ (throwDBError . err) - where - checkSQLError e = case SQL.sqlError e of - SQL.ErrorNotADatabase -> pure $ Just SQLiteErrorNotADatabase - _ -> sqliteError' e - sqliteError' :: Show e => e -> m (Maybe SQLiteError) - sqliteError' = pure . Just . SQLiteError . show exportSQL = T.unlines $ keySQL key @@ -167,17 +183,41 @@ sqlCipherExport DBEncryptionConfig {currentKey = DBEncryptionKey key, newKey = D "SELECT sqlcipher_export('exported');", "DETACH DATABASE exported;" ] - testSQL = - T.unlines $ - keySQL key' - <> [ "PRAGMA foreign_keys = ON;", - "PRAGMA secure_delete = ON;", - "SELECT count(*) FROM sqlite_master;" - ] - keySQL k = ["PRAGMA key = " <> keyString k <> ";" | not (BA.null k)] + +withDB :: FilePath -> (SQL.Database -> IO a) -> (SQLiteError -> DatabaseError) -> CM () +withDB f' a err = + liftIO (bracket (SQL.open $ T.pack f') SQL.close a $> Nothing) + `catch` checkSQLError + `catch` (\(e :: SomeException) -> sqliteError' e) + >>= mapM_ (throwDBError . err) + where + checkSQLError e = case SQL.sqlError e of + SQL.ErrorNotADatabase -> pure $ Just SQLiteErrorNotADatabase + _ -> sqliteError' e + sqliteError' :: Show e => e -> CM (Maybe SQLiteError) + sqliteError' = pure . Just . SQLiteError . show + +testSQL :: BA.ScrubbedBytes -> Text +testSQL k = + T.unlines $ + keySQL k + <> [ "PRAGMA foreign_keys = ON;", + "PRAGMA secure_delete = ON;", + "SELECT count(*) FROM sqlite_master;" + ] + +keySQL :: BA.ScrubbedBytes -> [Text] +keySQL k = ["PRAGMA key = " <> keyString k <> ";" | not (BA.null k)] + +sqlCipherTestKey :: DBEncryptionKey -> CM () +sqlCipherTestKey (DBEncryptionKey key) = do + fs <- lift storageFiles + testKey `withDBs` fs + where + testKey f = withDB f (`SQL.exec` testSQL key) DBErrorOpen withDBs :: Monad m => (FilePath -> m b) -> StorageFiles -> m b action `withDBs` StorageFiles {chatStore, agentStore} = action (dbFilePath chatStore) >> action (dbFilePath agentStore) -withStores :: Monad m => (SQLiteStore -> m b) -> StorageFiles -> m b +withStores :: Monad m => (DBStore -> m b) -> StorageFiles -> m b action `withStores` StorageFiles {chatStore, agentStore} = action chatStore >> action agentStore diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index c5c5ff7eed..73a2970c61 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -3,6 +3,8 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE TupleSections #-} module Simplex.Chat.Bot where @@ -10,6 +12,11 @@ import Control.Concurrent.Async import Control.Concurrent.STM import Control.Monad import qualified Data.ByteString.Char8 as B +import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as L +import qualified Data.Map.Strict as M +import Data.Maybe (isJust) +import Data.Text (Text) import qualified Data.Text as T import Simplex.Chat.Controller import Simplex.Chat.Core @@ -18,6 +25,7 @@ import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Store import Simplex.Chat.Types (Contact (..), ContactId, IsContact (..), User (..)) +import Simplex.Messaging.Agent.Protocol (CreatedConnLink (..)) import Simplex.Messaging.Encoding.String (strEncode) import System.Exit (exitFailure) @@ -25,14 +33,14 @@ chatBotRepl :: String -> (Contact -> String -> IO String) -> User -> ChatControl chatBotRepl welcome answer _user cc = do initializeBotAddress cc race_ (forever $ void getLine) . forever $ do - (_, _, resp) <- atomically . readTBQueue $ outputQ cc - case resp of - CRContactConnected _ contact _ -> do + (_, event) <- atomically . readTBQueue $ outputQ cc + case event of + Right (CEvtContactConnected _ contact _) -> do contactConnected contact - void $ sendMessage cc contact welcome - CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) -> do + void $ sendMessage cc contact $ T.pack welcome + Right CEvtNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) : _} -> do let msg = T.unpack $ ciContentToText mc - void $ sendMessage cc contact =<< answer contact msg + void $ sendMessage cc contact . T.pack =<< answer contact msg _ -> pure () where contactConnected Contact {localDisplayName} = putStrLn $ T.unpack localDisplayName <> " connected" @@ -43,47 +51,52 @@ initializeBotAddress = initializeBotAddress' True initializeBotAddress' :: Bool -> ChatController -> IO () initializeBotAddress' logAddress cc = do sendChatCmd cc ShowMyAddress >>= \case - CRUserContactLink _ UserContactLink {connReqContact} -> showBotAddress connReqContact - CRChatCmdError _ (ChatErrorStore SEUserContactLinkNotFound) -> do + Right (CRUserContactLink _ UserContactLink {connLinkContact}) -> showBotAddress connLinkContact + Left (ChatErrorStore SEUserContactLinkNotFound) -> do when logAddress $ putStrLn "No bot address, creating..." - sendChatCmd cc CreateMyAddress >>= \case - CRUserContactLinkCreated _ uri -> showBotAddress uri + -- TODO [short links] create short link by default + sendChatCmd cc (CreateMyAddress False) >>= \case + Right (CRUserContactLinkCreated _ ccLink) -> showBotAddress ccLink _ -> putStrLn "can't create bot address" >> exitFailure _ -> putStrLn "unexpected response" >> exitFailure where - showBotAddress uri = do - when logAddress $ putStrLn $ "Bot's contact address is: " <> B.unpack (strEncode uri) - void $ sendChatCmd cc $ AddressAutoAccept $ Just AutoAccept {acceptIncognito = False, autoReply = Nothing} + showBotAddress (CCLink uri shortUri) = do + when logAddress $ putStrLn $ "Bot's contact address is: " <> B.unpack (maybe (strEncode uri) strEncode shortUri) + when (isJust shortUri) $ putStrLn $ "Full contact address for old clients: " <> B.unpack (strEncode uri) + void $ sendChatCmd cc $ AddressAutoAccept $ Just AutoAccept {businessAddress = False, acceptIncognito = False, autoReply = Nothing} -sendMessage :: ChatController -> Contact -> String -> IO () -sendMessage cc ct = sendComposedMessage cc ct Nothing . textMsgContent +sendMessage :: ChatController -> Contact -> Text -> IO () +sendMessage cc ct = sendComposedMessage cc ct Nothing . MCText -sendMessage' :: ChatController -> ContactId -> String -> IO () -sendMessage' cc ctId = sendComposedMessage' cc ctId Nothing . textMsgContent +sendMessage' :: ChatController -> ContactId -> Text -> IO () +sendMessage' cc ctId = sendComposedMessage' cc ctId Nothing . MCText sendComposedMessage :: ChatController -> Contact -> Maybe ChatItemId -> MsgContent -> IO () sendComposedMessage cc = sendComposedMessage' cc . contactId' sendComposedMessage' :: ChatController -> ContactId -> Maybe ChatItemId -> MsgContent -> IO () -sendComposedMessage' cc ctId quotedItemId msgContent = do - let cm = ComposedMessage {fileSource = Nothing, quotedItemId, msgContent} - sendChatCmd cc (APISendMessage (ChatRef CTDirect ctId) False Nothing cm) >>= \case - CRNewChatItem {} -> printLog cc CLLInfo $ "sent message to contact ID " <> show ctId +sendComposedMessage' cc ctId qiId mc = sendComposedMessages_ cc (SRDirect ctId) [(qiId, mc)] + +sendComposedMessages :: ChatController -> SendRef -> NonEmpty MsgContent -> IO () +sendComposedMessages cc sendRef = sendComposedMessages_ cc sendRef . L.map (Nothing,) + +sendComposedMessages_ :: ChatController -> SendRef -> NonEmpty (Maybe ChatItemId, MsgContent) -> IO () +sendComposedMessages_ cc sendRef qmcs = do + let cms = L.map (\(qiId, mc) -> ComposedMessage {fileSource = Nothing, quotedItemId = qiId, msgContent = mc, mentions = M.empty}) qmcs + sendChatCmd cc (APISendMessages sendRef False Nothing cms) >>= \case + Right (CRNewChatItems {}) -> printLog cc CLLInfo $ "sent " <> show (length cms) <> " messages to " <> show sendRef r -> putStrLn $ "unexpected send message response: " <> show r deleteMessage :: ChatController -> Contact -> ChatItemId -> IO () deleteMessage cc ct chatItemId = do - let cmd = APIDeleteChatItem (contactRef ct) chatItemId CIDMInternal + let cmd = APIDeleteChatItem (contactRef ct) [chatItemId] CIDMInternal sendChatCmd cc cmd >>= \case - CRChatItemDeleted {} -> printLog cc CLLInfo $ "deleted message from " <> contactInfo ct + Right (CRChatItemsDeleted {}) -> printLog cc CLLInfo $ "deleted message(s) from " <> contactInfo ct r -> putStrLn $ "unexpected delete message response: " <> show r contactRef :: Contact -> ChatRef contactRef = ChatRef CTDirect . contactId' -textMsgContent :: String -> MsgContent -textMsgContent = MCText . T.pack - printLog :: ChatController -> ChatLogLevel -> String -> IO () printLog cc level s | logLevel (config cc) <= level = putStrLn s diff --git a/src/Simplex/Chat/Bot/KnownContacts.hs b/src/Simplex/Chat/Bot/KnownContacts.hs index 1ea44d49be..0c902d8566 100644 --- a/src/Simplex/Chat/Bot/KnownContacts.hs +++ b/src/Simplex/Chat/Bot/KnownContacts.hs @@ -1,5 +1,7 @@ +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeApplications #-} module Simplex.Chat.Bot.KnownContacts where @@ -9,8 +11,8 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Options.Applicative +import Simplex.Chat.Library.Commands (displayNameP) import Simplex.Messaging.Parsers (parseAll) -import Simplex.Messaging.Util (safeDecodeUtf8) data KnownContact = KnownContact { contactId :: Int64, @@ -18,8 +20,13 @@ data KnownContact = KnownContact } deriving (Eq) -knownContactNames :: [KnownContact] -> String -knownContactNames = T.unpack . T.intercalate ", " . map (("@" <>) . localDisplayName) +data KnownGroup = KnownGroup + { groupId :: Int64, + localDisplayName :: Text + } + +knownContactNames :: [KnownContact] -> Text +knownContactNames = T.intercalate ", " . map (("@" <>) . (\KnownContact {localDisplayName = n} -> n)) parseKnownContacts :: ReadM [KnownContact] parseKnownContacts = eitherReader $ parseAll knownContactsP . encodeUtf8 . T.pack @@ -29,5 +36,14 @@ knownContactsP = contactP `A.sepBy1` A.char ',' where contactP = do contactId <- A.decimal <* A.char ':' - localDisplayName <- safeDecodeUtf8 <$> A.takeTill (A.inClass ", ") + localDisplayName <- displayNameP pure KnownContact {contactId, localDisplayName} + +parseKnownGroup :: ReadM KnownGroup +parseKnownGroup = eitherReader $ parseAll knownGroupP . encodeUtf8 . T.pack + +knownGroupP :: A.Parser KnownGroup +knownGroupP = do + groupId <- A.decimal <* A.char ':' + localDisplayName <- displayNameP + pure KnownGroup {groupId, localDisplayName} diff --git a/src/Simplex/Chat/Call.hs b/src/Simplex/Chat/Call.hs index 115cd839e4..18398ecb3e 100644 --- a/src/Simplex/Chat/Call.hs +++ b/src/Simplex/Chat/Call.hs @@ -1,9 +1,12 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} @@ -18,17 +21,18 @@ import Data.ByteString.Char8 (ByteString) import Data.Int (Int64) import Data.Text (Text) import Data.Time.Clock (UTCTime) -import Database.SQLite.Simple.FromField (FromField (..)) -import Database.SQLite.Simple.ToField (ToField (..)) +import Simplex.Chat.Options.DB (FromField (..), ToField (..)) import Simplex.Chat.Types (Contact, ContactId, User) -import Simplex.Chat.Types.Util (decodeJSON, encodeJSON) +import Simplex.Messaging.Agent.Store.DB (Binary (..), fromTextField_) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, fstToLower, singleFieldJSON) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fstToLower, singleFieldJSON) +import Simplex.Messaging.Util (decodeJSON, encodeJSON) data Call = Call { contactId :: ContactId, callId :: CallId, + callUUID :: Text, chatItemId :: Int64, callState :: CallState, callTs :: UTCTime @@ -89,6 +93,9 @@ data CallState newtype CallId = CallId ByteString deriving (Eq, Show) + deriving newtype (FromField) + +instance ToField CallId where toField (CallId m) = toField $ Binary m instance StrEncoding CallId where strEncode (CallId m) = strEncode m @@ -102,15 +109,12 @@ instance ToJSON CallId where toJSON = strToJSON toEncoding = strToJEncoding -instance FromField CallId where fromField f = CallId <$> fromField f - -instance ToField CallId where toField (CallId m) = toField m - data RcvCallInvitation = RcvCallInvitation { user :: User, contact :: Contact, callType :: CallType, sharedKey :: Maybe C.Key, + callUUID :: Text, callTs :: UTCTime } deriving (Show) diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index b198cccbf7..02e2e121b3 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -13,12 +13,14 @@ {-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -fno-warn-implicit-lift #-} module Simplex.Chat.Controller where import Control.Concurrent (ThreadId) import Control.Concurrent.Async (Async) -import Control.Exception +import Control.Exception (Exception, SomeException) +import qualified Control.Exception as E import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader @@ -34,58 +36,71 @@ import qualified Data.ByteArray as BA import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Char (ord) -import Data.Constraint (Dict (..)) import Data.Int (Int64) import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M +import Data.Maybe (fromMaybe) +import Data.Set (Set) import Data.String import Data.Text (Text) import Data.Text.Encoding (decodeLatin1) import Data.Time (NominalDiffTime, UTCTime) -import Data.Time.Clock.System (systemToUTCTime) +import Data.Time.Clock.System (SystemTime (..), systemToUTCTime) import Data.Version (showVersion) import Data.Word (Word16) import Language.Haskell.TH (Exp, Q, runIO) +import Network.Socket (HostName) import Numeric.Natural import qualified Paths_simplex_chat as SC +import Simplex.Chat.AppSettings import Simplex.Chat.Call -import Simplex.Chat.Markdown (MarkdownList) import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent +import Simplex.Chat.Operators import Simplex.Chat.Protocol import Simplex.Chat.Remote.AppVersion import Simplex.Chat.Remote.Types -import Simplex.Chat.Store (AutoAccept, StoreError (..), UserContactLink, UserMsgReceiptSettings) +import Simplex.Chat.Stats (PresentedServersSummary) +import Simplex.Chat.Store (AutoAccept, ChatLockEntity, StoreError (..), UserContactLink, GroupLinkInfo, UserMsgReceiptSettings) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared +import Simplex.Chat.Types.UITheme +import Simplex.Chat.Util (liftIOEither) +import Simplex.FileTransfer.Description (FileDescriptionURI) import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo) -import Simplex.Messaging.Agent.Client (AgentLocks, ProtocolTestFailure) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig) +import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, SMPServerSubs, ServerQueueInfo, UserNetworkInfo) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerCfg) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol -import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction) -import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) -import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction, withTransactionPriority) +import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation, UpMigration) +import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Client (HostMode (..), SMPProxyFallback (..), SMPProxyMode (..), SMPWebPortServers (..), SocksMode (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..)) import qualified Simplex.Messaging.Crypto.File as CF +import Simplex.Messaging.Crypto.Ratchet (PQEncryption) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServerWithAuth, userProtocol) +import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), MsgId, NMsgMeta (..), NtfServer, ProtocolType (..), QueueId, SMPMsgMeta (..), SubscriptionMode (..), XFTPServer) import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (TLS, simplexMQVersion) -import Simplex.Messaging.Transport.Client (TransportHost) -import Simplex.Messaging.Util (allFinally, catchAllErrors, liftEitherError, tryAllErrors, (<$$>)) -import Simplex.Messaging.Version +import Simplex.Messaging.Transport.Client (SocksProxyWithAuth, TransportHost) +import Simplex.Messaging.Util (allFinally, catchAllErrors, catchAllErrors', tryAllErrors, tryAllErrors', (<$$>)) import Simplex.RemoteControl.Client import Simplex.RemoteControl.Invitation (RCSignedInvitation, RCVerifiedInvitation) import Simplex.RemoteControl.Types import System.IO (Handle) import System.Mem.Weak (Weak) -import qualified UnliftIO.Exception as E import UnliftIO.STM +#if !defined(dbPostgres) +import Database.SQLite.Simple (SQLError) +import qualified Database.SQLite.Simple as SQL +import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) +#endif versionNumber :: String versionNumber = showVersion SC.version @@ -120,16 +135,16 @@ coreVersionInfo simplexmqCommit = data ChatConfig = ChatConfig { agentConfig :: AgentConfig, - chatVRange :: VersionRange, + chatVRange :: VersionRangeChat, confirmMigrations :: MigrationConfirmation, - defaultServers :: DefaultAgentServers, + presetServers :: PresetServers, + shortLinkPresetServers :: NonEmpty SMPServer, + presetDomains :: [HostName], tbqSize :: Natural, fileChunkSize :: Integer, xftpDescrPartSize :: Int, inlineFiles :: InlineFilesConfig, autoAcceptFileSize :: Integer, - xftpFileConfig :: Maybe XFTPFileConfig, -- Nothing - XFTP is disabled - tempDir :: Maybe FilePath, showReactions :: Bool, showReceipts :: Bool, subscriptionEvents :: Bool, @@ -142,15 +157,38 @@ data ChatConfig = ChatConfig ciExpirationInterval :: Int64, -- microseconds coreApi :: Bool, highlyAvailable :: Bool, - deviceNameForRemote :: Text + deviceNameForRemote :: Text, + chatHooks :: ChatHooks } -data DefaultAgentServers = DefaultAgentServers - { smp :: NonEmpty SMPServerWithAuth, +data RandomAgentServers = RandomAgentServers + { smpServers :: NonEmpty (ServerCfg 'PSMP), + xftpServers :: NonEmpty (ServerCfg 'PXFTP) + } + deriving (Show) + +-- The hooks can be used to extend or customize chat core in mobile or CLI clients. +data ChatHooks = ChatHooks + { -- preCmdHook can be used to process or modify the commands before they are processed. + -- This hook should be used to process CustomChatCommand. + -- if this hook returns ChatResponse, the command processing will be skipped. + preCmdHook :: Maybe (ChatController -> ChatCommand -> IO (Either (Either ChatError ChatResponse) ChatCommand)), + -- eventHook can be used to additionally process or modify events, + -- it is called before the event is sent to the user (or to the UI). + eventHook :: Maybe (ChatController -> Either ChatError ChatEvent -> IO (Either ChatError ChatEvent)), + -- acceptMember hook can be used to accept or reject member connecting via group link without API calls + acceptMember :: Maybe (GroupInfo -> GroupLinkInfo -> Profile -> IO (Either GroupRejectionReason (GroupAcceptance, GroupMemberRole))) + } + +defaultChatHooks :: ChatHooks +defaultChatHooks = ChatHooks Nothing Nothing Nothing + +data PresetServers = PresetServers + { operators :: NonEmpty PresetOperator, ntf :: [NtfServer], - xftp :: NonEmpty XFTPServerWithAuth, netCfg :: NetworkConfig } + deriving (Show) data InlineFilesConfig = InlineFilesConfig { offerChunks :: Integer, @@ -170,22 +208,26 @@ defaultInlineFilesConfig = receiveInstant = True -- allow receiving instant files, within receiveChunks limit } -data ChatDatabase = ChatDatabase {chatStore :: SQLiteStore, agentStore :: SQLiteStore} +data ChatDatabase = ChatDatabase {chatStore :: DBStore, agentStore :: DBStore} data ChatController = ChatController { currentUser :: TVar (Maybe User), + randomPresetServers :: NonEmpty PresetOperator, + randomAgentServers :: RandomAgentServers, currentRemoteHost :: TVar (Maybe RemoteHostId), firstTime :: Bool, smpAgent :: AgentClient, agentAsync :: TVar (Maybe (Async (), Maybe (Async ()))), - chatStore :: SQLiteStore, + chatStore :: DBStore, chatStoreChanged :: TVar Bool, -- if True, chat should be fully restarted random :: TVar ChaChaDRG, + eventSeq :: TVar Int, inputQ :: TBQueue String, - outputQ :: TBQueue (Maybe CorrId, Maybe RemoteHostId, ChatResponse), + outputQ :: TBQueue (Maybe RemoteHostId, Either ChatError ChatEvent), connNetworkStatuses :: TMap AgentConnId NetworkStatus, subscriptionMode :: TVar SubscriptionMode, chatLock :: Lock, + entityLocks :: TMap ChatLockEntity Lock, sndFiles :: TVar (Map Int64 Handle), rcvFiles :: TVar (Map Int64 Handle), currentCalls :: TMap ContactId Call, @@ -200,11 +242,12 @@ data ChatController = ChatController expireCIThreads :: TMap UserId (Maybe (Async ())), expireCIFlags :: TMap UserId Bool, cleanupManagerAsync :: TVar (Maybe (Async ())), + chatActivated :: TVar Bool, timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))), showLiveItems :: TVar Bool, encryptLocalFiles :: TVar Bool, - userXFTPFileConfig :: TVar (Maybe XFTPFileConfig), tempDirectory :: TVar (Maybe FilePath), + assetsDirectory :: TVar (Maybe FilePath), logFilePath :: Maybe FilePath, contactMergeEnabled :: TVar Bool } @@ -233,7 +276,8 @@ data ChatCommand | UnmuteUser | APIDeleteUser UserId Bool (Maybe UserPwd) | DeleteUser UserName Bool (Maybe UserPwd) - | StartChat {subscribeConnections :: Bool, enableExpireChatItems :: Bool, startXFTPWorkers :: Bool} + | StartChat {mainApp :: Bool, enableSndFiles :: Bool} -- enableSndFiles has no effect when mainApp is True + | CheckChatRunning | APIStopChat | APIActivateChat {restoreChat :: Bool} | APISuspendChat {suspendTimeout :: Int} @@ -241,31 +285,51 @@ data ChatCommand | SetTempFolder FilePath | SetFilesFolder FilePath | SetRemoteHostsFolder FilePath - | APISetXFTPConfig (Maybe XFTPFileConfig) + | APISetAppFilePaths AppFilePathsConfig | APISetEncryptLocalFiles Bool | SetContactMergeEnabled Bool +#if !defined(dbPostgres) | APIExportArchive ArchiveConfig | ExportArchive | APIImportArchive ArchiveConfig | APIDeleteStorage | APIStorageEncryption DBEncryptionConfig + | TestStorageEncryption DBEncryptionKey + | SlowSQLQueries +#endif | ExecChatStoreSQL Text | ExecAgentStoreSQL Text - | SlowSQLQueries + | APISaveAppSettings AppSettings + | APIGetAppSettings (Maybe AppSettings) + | APIGetChatTags UserId | APIGetChats {userId :: UserId, pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery} - | APIGetChat ChatRef ChatPagination (Maybe String) + | APIGetChat ChatRef (Maybe MsgContentTag) ChatPagination (Maybe String) | APIGetChatItems ChatPagination (Maybe String) | APIGetChatItemInfo ChatRef ChatItemId - | APISendMessage {chatRef :: ChatRef, liveMessage :: Bool, ttl :: Maybe Int, composedMessage :: ComposedMessage} - | APIUpdateChatItem {chatRef :: ChatRef, chatItemId :: ChatItemId, liveMessage :: Bool, msgContent :: MsgContent} - | APIDeleteChatItem ChatRef ChatItemId CIDeleteMode - | APIDeleteMemberChatItem GroupId GroupMemberId ChatItemId + | APISendMessages {sendRef :: SendRef, liveMessage :: Bool, ttl :: Maybe Int, composedMessages :: NonEmpty ComposedMessage} + | APICreateChatTag ChatTagData + | APISetChatTags ChatRef (Maybe (NonEmpty ChatTagId)) + | APIDeleteChatTag ChatTagId + | APIUpdateChatTag ChatTagId ChatTagData + | APIReorderChatTags (NonEmpty ChatTagId) + | APICreateChatItems {noteFolderId :: NoteFolderId, composedMessages :: NonEmpty ComposedMessage} + | APIReportMessage {groupId :: GroupId, chatItemId :: ChatItemId, reportReason :: ReportReason, reportText :: Text} + | ReportMessage {groupName :: GroupName, contactName_ :: Maybe ContactName, reportReason :: ReportReason, reportedMessage :: Text} + | APIUpdateChatItem {chatRef :: ChatRef, chatItemId :: ChatItemId, liveMessage :: Bool, updatedMessage :: UpdatedMessage} + | APIDeleteChatItem ChatRef (NonEmpty ChatItemId) CIDeleteMode + | APIDeleteMemberChatItem GroupId (NonEmpty ChatItemId) + | APIArchiveReceivedReports GroupId + | APIDeleteReceivedReports GroupId (NonEmpty ChatItemId) CIDeleteMode | APIChatItemReaction {chatRef :: ChatRef, chatItemId :: ChatItemId, add :: Bool, reaction :: MsgReaction} + | APIGetReactionMembers UserId GroupId ChatItemId MsgReaction + | APIPlanForwardChatItems {fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId} + | APIForwardChatItems {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId, ttl :: Maybe Int} | APIUserRead UserId | UserRead - | APIChatRead ChatRef (Maybe (ChatItemId, ChatItemId)) + | APIChatRead ChatRef + | APIChatItemsRead ChatRef (NonEmpty ChatItemId) | APIChatUnread ChatRef Bool - | APIDeleteChat ChatRef Bool -- `notify` flag is only applied to direct chats + | APIDeleteChat ChatRef ChatDeleteMode -- currently delete mode settings are only applied to direct chats | APIClearChat ChatRef | APIAcceptContact IncognitoEnabled Int64 | APIRejectContact Int64 @@ -282,44 +346,65 @@ data ChatCommand | APIUpdateProfile UserId Profile | APISetContactPrefs ContactId Preferences | APISetContactAlias ContactId LocalAlias + | APISetGroupAlias GroupId LocalAlias | APISetConnectionAlias Int64 LocalAlias - | APIParseMarkdown Text + | APISetUserUIThemes UserId (Maybe UIThemeEntityOverrides) + | APISetChatUIThemes ChatRef (Maybe UIThemeEntityOverrides) | APIGetNtfToken | APIRegisterToken DeviceToken NotificationsMode | APIVerifyToken DeviceToken C.CbNonce ByteString + | APICheckToken DeviceToken | APIDeleteToken DeviceToken - | APIGetNtfMessage {nonce :: C.CbNonce, encNtfInfo :: ByteString} + | APIGetNtfConns {nonce :: C.CbNonce, encNtfInfo :: ByteString} + | APIGetConnNtfMessages (NonEmpty ConnMsgReq) | APIAddMember GroupId ContactId GroupMemberRole - | APIJoinGroup GroupId - | APIMemberRole GroupId GroupMemberId GroupMemberRole - | APIRemoveMember GroupId GroupMemberId + | APIJoinGroup {groupId :: GroupId, enableNtfs :: MsgFilter} + | APIAcceptMember GroupId GroupMemberId GroupMemberRole + | APIMembersRole GroupId (NonEmpty GroupMemberId) GroupMemberRole + | APIBlockMembersForAll GroupId (NonEmpty GroupMemberId) Bool + | APIRemoveMembers {groupId :: GroupId, groupMemberIds :: Set GroupMemberId, withMessages :: Bool} | APILeaveGroup GroupId | APIListMembers GroupId | APIUpdateGroupProfile GroupId GroupProfile - | APICreateGroupLink GroupId GroupMemberRole + | APICreateGroupLink GroupId GroupMemberRole CreateShortLink | APIGroupLinkMemberRole GroupId GroupMemberRole | APIDeleteGroupLink GroupId | APIGetGroupLink GroupId | APICreateMemberContact GroupId GroupMemberId | APISendMemberContactInvitation {contactId :: ContactId, msgContent_ :: Maybe MsgContent} - | APIGetUserProtoServers UserId AProtocolType | GetUserProtoServers AProtocolType - | APISetUserProtoServers UserId AProtoServersConfig - | SetUserProtoServers AProtoServersConfig + | SetUserProtoServers AProtocolType [AProtoServerWithAuth] | APITestProtoServer UserId AProtoServerWithAuth | TestProtoServer AProtoServerWithAuth - | APISetChatItemTTL UserId (Maybe Int64) - | SetChatItemTTL (Maybe Int64) + | APIGetServerOperators + | APISetServerOperators (NonEmpty ServerOperator) + | SetServerOperators (NonEmpty ServerOperatorRoles) + | APIGetUserServers UserId + | APISetUserServers UserId (NonEmpty UpdatedUserOperatorServers) + | APIValidateServers UserId [UpdatedUserOperatorServers] -- response is CRUserServersValidation + | APIGetUsageConditions + | APISetConditionsNotified Int64 + | APIAcceptConditions Int64 (NonEmpty Int64) + | APISetChatItemTTL UserId Int64 + | SetChatItemTTL Int64 | APIGetChatItemTTL UserId | GetChatItemTTL + | APISetChatTTL UserId ChatRef (Maybe Int64) + | SetChatTTL ChatName (Maybe Int64) + | GetChatTTL ChatName | APISetNetworkConfig NetworkConfig | APIGetNetworkConfig + | SetNetworkConfig SimpleNetCfg + | APISetNetworkInfo UserNetworkInfo | ReconnectAllServers + | ReconnectServer UserId SMPServer | APISetChatSettings ChatRef ChatSettings | APISetMemberSettings GroupId GroupMemberId GroupMemberSettings | APIContactInfo ContactId | APIGroupInfo GroupId | APIGroupMemberInfo GroupId GroupMemberId + | APIContactQueueInfo ContactId + | APIGroupMemberQueueInfo GroupId GroupMemberId | APISwitchContact ContactId | APISwitchGroupMember GroupId GroupMemberId | APIAbortSwitchContact ContactId @@ -338,6 +423,8 @@ data ChatCommand | ContactInfo ContactName | ShowGroupInfo GroupName | GroupMemberInfo GroupName ContactName + | ContactQueueInfo ContactName + | GroupMemberQueueInfo GroupName ContactName | SwitchContact ContactName | SwitchGroupMember GroupName ContactName | AbortSwitchContact ContactName @@ -352,20 +439,21 @@ data ChatCommand | EnableGroupMember GroupName ContactName | ChatHelp HelpSection | Welcome - | APIAddContact UserId IncognitoEnabled - | AddContact IncognitoEnabled + | APIAddContact UserId CreateShortLink IncognitoEnabled + | AddContact CreateShortLink IncognitoEnabled | APISetConnectionIncognito Int64 IncognitoEnabled - | APIConnectPlan UserId AConnectionRequestUri - | APIConnect UserId IncognitoEnabled (Maybe AConnectionRequestUri) - | Connect IncognitoEnabled (Maybe AConnectionRequestUri) + | APIChangeConnectionUser Int64 UserId -- new user id to switch connection to + | APIConnectPlan UserId AConnectionLink + | APIConnect UserId IncognitoEnabled (Maybe ACreatedConnLink) + | Connect IncognitoEnabled (Maybe AConnectionLink) | APIConnectContactViaAddress UserId IncognitoEnabled ContactId | ConnectSimplex IncognitoEnabled -- UserId (not used in UI) - | DeleteContact ContactName + | DeleteContact ContactName ChatDeleteMode | ClearContact ContactName | APIListContacts UserId | ListContacts - | APICreateMyAddress UserId - | CreateMyAddress + | APICreateMyAddress UserId CreateShortLink + | CreateMyAddress CreateShortLink | APIDeleteMyAddress UserId | DeleteMyAddress | APIShowMyAddress UserId @@ -376,11 +464,14 @@ data ChatCommand | AddressAutoAccept (Maybe AutoAccept) | AcceptContact IncognitoEnabled ContactName | RejectContact ContactName + | ForwardMessage {toChatName :: ChatName, fromContactName :: ContactName, forwardedMsg :: Text} + | ForwardGroupMessage {toChatName :: ChatName, fromGroupName :: GroupName, fromMemberName_ :: Maybe ContactName, forwardedMsg :: Text} + | ForwardLocalMessage {toChatName :: ChatName, forwardedMsg :: Text} | SendMessage ChatName Text | SendMemberContactMessage GroupName ContactName Text | SendLiveMessage ChatName Text | SendMessageQuote {contactName :: ContactName, msgDir :: AMsgDirection, quotedMsg :: Text, message :: Text} - | SendMessageBroadcast Text -- UserId (not used in UI) + | SendMessageBroadcast MsgContent -- UserId (not used in UI) | DeleteMessage ChatName Text | DeleteMemberMessage GroupName ContactName Text | EditMessage {chatName :: ChatName, editedMsg :: Text, message :: Text} @@ -389,9 +480,10 @@ data ChatCommand | APINewGroup UserId IncognitoEnabled GroupProfile | NewGroup IncognitoEnabled GroupProfile | AddMember GroupName ContactName GroupMemberRole - | JoinGroup GroupName + | JoinGroup {groupName :: GroupName, enableNtfs :: MsgFilter} | MemberRole GroupName ContactName GroupMemberRole - | RemoveMember GroupName ContactName + | BlockForAll GroupName ContactName Bool + | RemoveMembers {groupName :: GroupName, members :: Set ContactName, withMessages :: Bool} | LeaveGroup GroupName | DeleteGroup GroupName | ClearGroup GroupName @@ -402,11 +494,12 @@ data ChatCommand | ShowGroupProfile GroupName | UpdateGroupDescription GroupName (Maybe Text) | ShowGroupDescription GroupName - | CreateGroupLink GroupName GroupMemberRole + | CreateGroupLink GroupName GroupMemberRole CreateShortLink | GroupLinkMemberRole GroupName GroupMemberRole | DeleteGroupLink GroupName | ShowGroupLink GroupName | SendGroupMessageQuote {groupName :: GroupName, contactName_ :: Maybe ContactName, quotedMsg :: Text, message :: Text} + | ClearNoteFolder | LastChats (Maybe Int) -- UserId (not used in UI) | LastMessages (Maybe ChatName) Int (Maybe String) -- UserId (not used in UI) | LastChatItemId (Maybe ChatName) Int -- UserId (not used in UI) @@ -418,8 +511,8 @@ data ChatCommand | ForwardFile ChatName FileTransferId | ForwardImage ChatName FileTransferId | SendFileDescription ChatName FilePath - | ReceiveFile {fileId :: FileTransferId, storeEncrypted :: Maybe Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath} - | SetFileToReceive {fileId :: FileTransferId, storeEncrypted :: Maybe Bool} + | ReceiveFile {fileId :: FileTransferId, userApprovedRelays :: Bool, storeEncrypted :: Maybe Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath} + | SetFileToReceive {fileId :: FileTransferId, userApprovedRelays :: Bool, storeEncrypted :: Maybe Bool} | CancelFile FileTransferId | FileStatus FileTransferId | ShowProfile -- UserId (not used in UI) @@ -428,7 +521,8 @@ data ChatCommand | ShowProfileImage | SetUserFeature AChatFeature FeatureAllowed -- UserId (not used in UI) | SetContactFeature AChatFeature ContactName (Maybe FeatureAllowed) - | SetGroupFeature AGroupFeature GroupName GroupFeatureEnabled + | SetGroupFeature AGroupFeatureNoRole GroupName GroupFeatureEnabled + | SetGroupFeatureRole AGroupFeatureRole GroupName GroupFeatureEnabled (Maybe GroupMemberRole) | SetUserTimedMessages Bool -- UserId (not used in UI) | SetContactTimedMessages ContactName (Maybe TimedMessagesEnabled) | SetGroupTimedMessages GroupName (Maybe Int) @@ -447,13 +541,24 @@ data ChatCommand | ListRemoteCtrls | StopRemoteCtrl -- Stop listening for announcements or terminate an active session | DeleteRemoteCtrl RemoteCtrlId -- Remove all local data associated with a remote controller session + | APIUploadStandaloneFile UserId CryptoFile + | APIDownloadStandaloneFile UserId FileDescriptionURI CryptoFile + | APIStandaloneFileInfo FileDescriptionURI | QuitChat | ShowVersion | DebugLocks - | GetAgentStats - | ResetAgentStats + | DebugEvent ChatEvent + | GetAgentSubsTotal UserId + | GetAgentServersSummary UserId + | ResetAgentServersStats | GetAgentSubs | GetAgentSubsDetails + | GetAgentWorkers + | GetAgentWorkersDetails + | GetAgentQueuesInfo + | -- The parser will return this command for strings that start from "//". + -- This command should be processed in preCmdHook + CustomChatCommand ByteString deriving (Show) allowRemoteCommand :: ChatCommand -> Bool -- XXX: consider using Relay/Block/ForceLocal @@ -466,13 +571,15 @@ allowRemoteCommand = \case SetTempFolder _ -> False SetFilesFolder _ -> False SetRemoteHostsFolder _ -> False - APISetXFTPConfig _ -> False APISetEncryptLocalFiles _ -> False +#if !defined(dbPostgres) APIExportArchive _ -> False APIImportArchive _ -> False ExportArchive -> False APIDeleteStorage -> False APIStorageEncryption _ -> False + SlowSQLQueries -> False +#endif APISetNetworkConfig _ -> False APIGetNetworkConfig -> False SetLocalDeviceName _ -> False @@ -492,7 +599,6 @@ allowRemoteCommand = \case DeleteRemoteCtrl _ -> False ExecChatStoreSQL _ -> False ExecAgentStoreSQL _ -> False - SlowSQLQueries -> False _ -> True data ChatResponse @@ -501,46 +607,42 @@ data ChatResponse | CRChatStarted | CRChatRunning | CRChatStopped - | CRChatSuspended | CRApiChats {user :: User, chats :: [AChat]} | CRChats {chats :: [AChat]} - | CRApiChat {user :: User, chat :: AChat} + | CRApiChat {user :: User, chat :: AChat, navInfo :: Maybe NavigationInfo} + | CRChatTags {user :: User, userTags :: [ChatTag]} | CRChatItems {user :: User, chatName_ :: Maybe ChatName, chatItems :: [AChatItem]} | CRChatItemInfo {user :: User, chatItem :: AChatItem, chatItemInfo :: ChatItemInfo} | CRChatItemId User (Maybe ChatItemId) - | CRApiParsedMarkdown {formattedText :: Maybe MarkdownList} - | CRUserProtoServers {user :: User, servers :: AUserProtoServers} | CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure} + | CRServerOperatorConditions {conditions :: ServerOperatorConditions} + | CRUserServers {user :: User, userServers :: [UserOperatorServers]} + | CRUserServersValidation {user :: User, serverErrors :: [UserServersError]} + | CRUsageConditions {usageConditions :: UsageConditions, conditionsText :: Text, acceptedConditions :: Maybe UsageConditions} | CRChatItemTTL {user :: User, chatItemTTL :: Maybe Int64} | CRNetworkConfig {networkConfig :: NetworkConfig} | CRContactInfo {user :: User, contact :: Contact, connectionStats_ :: Maybe ConnectionStats, customUserProfile :: Maybe Profile} | CRGroupInfo {user :: User, groupInfo :: GroupInfo, groupSummary :: GroupSummary} | CRGroupMemberInfo {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats} + | CRQueueInfo {user :: User, rcvMsgInfo :: Maybe RcvMsgInfo, queueInfo :: ServerQueueInfo} | CRContactSwitchStarted {user :: User, contact :: Contact, connectionStats :: ConnectionStats} | CRGroupMemberSwitchStarted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats} | CRContactSwitchAborted {user :: User, contact :: Contact, connectionStats :: ConnectionStats} | CRGroupMemberSwitchAborted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats} - | CRContactSwitch {user :: User, contact :: Contact, switchProgress :: SwitchProgress} - | CRGroupMemberSwitch {user :: User, groupInfo :: GroupInfo, member :: GroupMember, switchProgress :: SwitchProgress} | CRContactRatchetSyncStarted {user :: User, contact :: Contact, connectionStats :: ConnectionStats} | CRGroupMemberRatchetSyncStarted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats} - | CRContactRatchetSync {user :: User, contact :: Contact, ratchetSyncProgress :: RatchetSyncProgress} - | CRGroupMemberRatchetSync {user :: User, groupInfo :: GroupInfo, member :: GroupMember, ratchetSyncProgress :: RatchetSyncProgress} - | CRContactVerificationReset {user :: User, contact :: Contact} - | CRGroupMemberVerificationReset {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRContactCode {user :: User, contact :: Contact, connectionCode :: Text} | CRGroupMemberCode {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionCode :: Text} | CRConnectionVerified {user :: User, verified :: Bool, expectedCode :: Text} - | CRNewChatItem {user :: User, chatItem :: AChatItem} - | CRChatItemStatusUpdated {user :: User, chatItem :: AChatItem} + | CRTagsUpdated {user :: User, userTags :: [ChatTag], chatTags :: [ChatTagId]} + | CRNewChatItems {user :: User, chatItems :: [AChatItem]} | CRChatItemUpdated {user :: User, chatItem :: AChatItem} | CRChatItemNotChanged {user :: User, chatItem :: AChatItem} | CRChatItemReaction {user :: User, added :: Bool, reaction :: ACIReaction} - | CRChatItemDeleted {user :: User, deletedChatItem :: AChatItem, toChatItem :: Maybe AChatItem, byUser :: Bool, timed :: Bool} - | CRChatItemDeletedNotFound {user :: User, contact :: Contact, sharedMsgId :: SharedMsgId} + | CRReactionMembers {user :: User, memberReactions :: [MemberReaction]} + | CRChatItemsDeleted {user :: User, chatItemDeletions :: [ChatItemDeletion], byUser :: Bool, timed :: Bool} + | CRGroupChatItemsDeleted {user :: User, groupInfo :: GroupInfo, chatItemIDs :: [ChatItemId], byUser :: Bool, member_ :: Maybe GroupMember} | CRBroadcastSent {user :: User, msgContent :: MsgContent, successes :: Int, failures :: Int, timestamp :: UTCTime} - | CRMsgIntegrityError {user :: User, msgError :: MsgErrorType} - | CRCmdAccepted {corr :: CorrId} | CRCmdOk {user_ :: Maybe User} | CRChatHelp {helpSection :: HelpSection} | CRWelcome {user :: User} @@ -551,8 +653,7 @@ data ChatResponse | CRUserContactLinkUpdated {user :: User, contactLink :: UserContactLink} | CRContactRequestRejected {user :: User, contactRequest :: UserContactRequest} | CRUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact} - | CRGroupLinkConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember} - | CRUserDeletedMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | CRUserDeletedMembers {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], withMessages :: Bool} | CRGroupsList {user :: User, groups :: [(GroupInfo, GroupSummary)]} | CRSentGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember} | CRFileTransferStatus User (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus @@ -561,189 +662,260 @@ data ChatResponse | CRUserProfileNoChange {user :: User} | CRUserPrivacy {user :: User, updatedUser :: User} | CRVersionInfo {versionInfo :: CoreVersionInfo, chatMigrations :: [UpMigration], agentMigrations :: [UpMigration]} - | CRInvitation {user :: User, connReqInvitation :: ConnReqInvitation, connection :: PendingContactConnection} + | CRInvitation {user :: User, connLinkInvitation :: CreatedLinkInvitation, connection :: PendingContactConnection} | CRConnectionIncognitoUpdated {user :: User, toConnection :: PendingContactConnection} - | CRConnectionPlan {user :: User, connectionPlan :: ConnectionPlan} + | CRConnectionUserChanged {user :: User, fromConnection :: PendingContactConnection, toConnection :: PendingContactConnection, newUser :: User} + | CRConnectionPlan {user :: User, connLink :: ACreatedConnLink, connectionPlan :: ConnectionPlan} | CRSentConfirmation {user :: User, connection :: PendingContactConnection} | CRSentInvitation {user :: User, connection :: PendingContactConnection, customUserProfile :: Maybe Profile} | CRSentInvitationToContact {user :: User, contact :: Contact, customUserProfile :: Maybe Profile} - | CRContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact} - | CRGroupMemberUpdated {user :: User, groupInfo :: GroupInfo, fromMember :: GroupMember, toMember :: GroupMember} - | CRContactsMerged {user :: User, intoContact :: Contact, mergedContact :: Contact, updatedContact :: Contact} | CRContactDeleted {user :: User, contact :: Contact} - | CRContactDeletedByContact {user :: User, contact :: Contact} | CRChatCleared {user :: User, chatInfo :: AChatInfo} - | CRUserContactLinkCreated {user :: User, connReqContact :: ConnReqContact} + | CRUserContactLinkCreated {user :: User, connLinkContact :: CreatedLinkContact} | CRUserContactLinkDeleted {user :: User} - | CRReceivedContactRequest {user :: User, contactRequest :: UserContactRequest} | CRAcceptingContactRequest {user :: User, contact :: Contact} | CRContactAlreadyExists {user :: User, contact :: Contact} - | CRContactRequestAlreadyAccepted {user :: User, contact :: Contact} | CRLeftMemberUser {user :: User, groupInfo :: GroupInfo} | CRGroupDeletedUser {user :: User, groupInfo :: GroupInfo} - | CRRcvFileDescrReady {user :: User, chatItem :: AChatItem} + | CRForwardPlan {user :: User, itemsCount :: Int, chatItemIds :: [ChatItemId], forwardConfirmation :: Maybe ForwardConfirmation} | CRRcvFileAccepted {user :: User, chatItem :: AChatItem} + -- TODO add chatItem :: AChatItem | CRRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer} - | CRRcvFileDescrNotReady {user :: User, chatItem :: AChatItem} - | CRRcvFileStart {user :: User, chatItem :: AChatItem} - | CRRcvFileProgressXFTP {user :: User, chatItem :: AChatItem, receivedSize :: Int64, totalSize :: Int64} - | CRRcvFileComplete {user :: User, chatItem :: AChatItem} - | CRRcvFileCancelled {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer} - | CRRcvFileSndCancelled {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer} - | CRRcvFileError {user :: User, chatItem :: AChatItem, agentError :: AgentErrorType} - | CRSndFileStart {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} - | CRSndFileComplete {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} - | CRSndFileRcvCancelled {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} - | CRSndFileCancelled {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sndFileTransfers :: [SndFileTransfer]} - | CRSndFileStartXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta} - | CRSndFileProgressXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sentSize :: Int64, totalSize :: Int64} - | CRSndFileCompleteXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta} - | CRSndFileCancelledXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta} - | CRSndFileError {user :: User, chatItem :: AChatItem} + | CRStandaloneFileInfo {fileMeta :: Maybe J.Value} + | CRRcvStandaloneFileCreated {user :: User, rcvFileTransfer :: RcvFileTransfer} -- returned by _download + | CRRcvFileCancelled {user :: User, chatItem_ :: Maybe AChatItem, rcvFileTransfer :: RcvFileTransfer} + | CRSndFileCancelled {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, sndFileTransfers :: [SndFileTransfer]} + | CRSndStandaloneFileCreated {user :: User, fileTransferMeta :: FileTransferMeta} -- returned by _upload | CRUserProfileUpdated {user :: User, fromProfile :: Profile, toProfile :: Profile, updateSummary :: UserProfileUpdateSummary} | CRUserProfileImage {user :: User, profile :: Profile} | CRContactAliasUpdated {user :: User, toContact :: Contact} + | CRGroupAliasUpdated {user :: User, toGroup :: GroupInfo} | CRConnectionAliasUpdated {user :: User, toConnection :: PendingContactConnection} | CRContactPrefsUpdated {user :: User, fromContact :: Contact, toContact :: Contact} - | CRContactConnecting {user :: User, contact :: Contact} - | CRContactConnected {user :: User, contact :: Contact, userCustomProfile :: Maybe Profile} - | CRContactAnotherClient {user :: User, contact :: Contact} - | CRSubscriptionEnd {user :: User, connectionEntity :: ConnectionEntity} - | CRContactsDisconnected {server :: SMPServer, contactRefs :: [ContactRef]} - | CRContactsSubscribed {server :: SMPServer, contactRefs :: [ContactRef]} - | CRContactSubError {user :: User, contact :: Contact, chatError :: ChatError} - | CRContactSubSummary {user :: User, contactSubscriptions :: [ContactSubStatus]} - | CRUserContactSubSummary {user :: User, userContactSubscriptions :: [UserContactSubStatus]} - | CRNetworkStatus {networkStatus :: NetworkStatus, connections :: [AgentConnId]} | CRNetworkStatuses {user_ :: Maybe User, networkStatuses :: [ConnNetworkStatus]} - | CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost} - | CRHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost} - | CRGroupInvitation {user :: User, groupInfo :: GroupInfo} - | CRReceivedGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole} - | CRUserJoinedGroup {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember} | CRJoinedGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} - | CRJoinedGroupMemberConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, member :: GroupMember} - | CRMemberRole {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole} - | CRMemberRoleUser {user :: User, groupInfo :: GroupInfo, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole} - | CRConnectedToGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember, memberContact :: Maybe Contact} - | CRDeletedMember {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, deletedMember :: GroupMember} - | CRDeletedMemberUser {user :: User, groupInfo :: GroupInfo, member :: GroupMember} - | CRLeftMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} - | CRGroupEmpty {user :: User, groupInfo :: GroupInfo} - | CRGroupRemoved {user :: User, groupInfo :: GroupInfo} - | CRGroupDeleted {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | CRMembersRoleUser {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], toRole :: GroupMemberRole} + | CRMembersBlockedForAllUser {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], blocked :: Bool} | CRGroupUpdated {user :: User, fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember} | CRGroupProfile {user :: User, groupInfo :: GroupInfo} | CRGroupDescription {user :: User, groupInfo :: GroupInfo} -- only used in CLI - | CRGroupLinkCreated {user :: User, groupInfo :: GroupInfo, connReqContact :: ConnReqContact, memberRole :: GroupMemberRole} - | CRGroupLink {user :: User, groupInfo :: GroupInfo, connReqContact :: ConnReqContact, memberRole :: GroupMemberRole} + | CRGroupLinkCreated {user :: User, groupInfo :: GroupInfo, connLinkContact :: CreatedLinkContact, memberRole :: GroupMemberRole} + | CRGroupLink {user :: User, groupInfo :: GroupInfo, connLinkContact :: CreatedLinkContact, memberRole :: GroupMemberRole} | CRGroupLinkDeleted {user :: User, groupInfo :: GroupInfo} - | CRAcceptingGroupJoinRequest {user :: User, groupInfo :: GroupInfo, contact :: Contact} - | CRAcceptingGroupJoinRequestMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} - | CRNoMemberContactCreating {user :: User, groupInfo :: GroupInfo, member :: GroupMember} -- only used in CLI | CRNewMemberContact {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} | CRNewMemberContactSentInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} - | CRNewMemberContactReceivedInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} - | CRContactAndMemberAssociated {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember, updatedContact :: Contact} - | CRMemberSubError {user :: User, groupInfo :: GroupInfo, member :: GroupMember, chatError :: ChatError} - | CRMemberSubSummary {user :: User, memberSubscriptions :: [MemberSubStatus]} - | CRGroupSubscribed {user :: User, groupInfo :: GroupInfo} - | CRPendingSubSummary {user :: User, pendingSubscriptions :: [PendingSubStatus]} - | CRSndFileSubError {user :: User, sndFileTransfer :: SndFileTransfer, chatError :: ChatError} - | CRRcvFileSubError {user :: User, rcvFileTransfer :: RcvFileTransfer, chatError :: ChatError} - | CRCallInvitation {callInvitation :: RcvCallInvitation} - | CRCallOffer {user :: User, contact :: Contact, callType :: CallType, offer :: WebRTCSession, sharedKey :: Maybe C.Key, askConfirmation :: Bool} - | CRCallAnswer {user :: User, contact :: Contact, answer :: WebRTCSession} - | CRCallExtraInfo {user :: User, contact :: Contact, extraInfo :: WebRTCExtraInfo} - | CRCallEnded {user :: User, contact :: Contact} | CRCallInvitations {callInvitations :: [RcvCallInvitation]} - | CRUserContactLinkSubscribed -- TODO delete - | CRUserContactLinkSubError {chatError :: ChatError} -- TODO delete | CRNtfTokenStatus {status :: NtfTknStatus} - | CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode} - | CRNtfMessages {user_ :: Maybe User, connEntity_ :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessages :: [NtfMsgInfo]} - | CRNtfMessage {user :: User, connEntity :: ConnectionEntity, ntfMessage :: NtfMsgInfo} + | CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode, ntfServer :: NtfServer} + | CRNtfConns {ntfConns :: [NtfConn]} + | CRConnNtfMessages {receivedMsgs :: NonEmpty RcvNtfMsgInfo} | CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection} | CRRemoteHostList {remoteHosts :: [RemoteHostInfo]} | CRCurrentRemoteHost {remoteHost_ :: Maybe RemoteHostInfo} | CRRemoteHostStarted {remoteHost_ :: Maybe RemoteHostInfo, invitation :: Text, ctrlPort :: String, localAddrs :: NonEmpty RCCtrlAddress} - | CRRemoteHostSessionCode {remoteHost_ :: Maybe RemoteHostInfo, sessionCode :: Text} - | CRNewRemoteHost {remoteHost :: RemoteHostInfo} - | CRRemoteHostConnected {remoteHost :: RemoteHostInfo} - | CRRemoteHostStopped {remoteHostId_ :: Maybe RemoteHostId, rhsState :: RemoteHostSessionState, rhStopReason :: RemoteHostStopReason} | CRRemoteFileStored {remoteHostId :: RemoteHostId, remoteFileSource :: CryptoFile} | CRRemoteCtrlList {remoteCtrls :: [RemoteCtrlInfo]} - | CRRemoteCtrlFound {remoteCtrl :: RemoteCtrlInfo, ctrlAppInfo_ :: Maybe CtrlAppInfo, appVersion :: AppVersion, compatible :: Bool} | CRRemoteCtrlConnecting {remoteCtrl_ :: Maybe RemoteCtrlInfo, ctrlAppInfo :: CtrlAppInfo, appVersion :: AppVersion} - | CRRemoteCtrlSessionCode {remoteCtrl_ :: Maybe RemoteCtrlInfo, sessionCode :: Text} | CRRemoteCtrlConnected {remoteCtrl :: RemoteCtrlInfo} - | CRRemoteCtrlStopped {rcsState :: RemoteCtrlSessionState, rcStopReason :: RemoteCtrlStopReason} | CRSQLResult {rows :: [Text]} +#if !defined(dbPostgres) + | CRArchiveExported {archiveErrors :: [ArchiveError]} + | CRArchiveImported {archiveErrors :: [ArchiveError]} | CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]} - | CRDebugLocks {chatLockName :: Maybe String, agentLocks :: AgentLocks} - | CRAgentStats {agentStats :: [[String]]} +#endif + | CRDebugLocks {chatLockName :: Maybe String, chatEntityLocks :: Map String String, agentLocks :: AgentLocks} + | CRAgentSubsTotal {user :: User, subsTotal :: SMPServerSubs, hasSession :: Bool} + | CRAgentServersSummary {user :: User, serversSummary :: PresentedServersSummary} + | CRAgentWorkersDetails {agentWorkersDetails :: AgentWorkersDetails} + | CRAgentWorkersSummary {agentWorkersSummary :: AgentWorkersSummary} | CRAgentSubs {activeSubs :: Map Text Int, pendingSubs :: Map Text Int, removedSubs :: Map Text [String]} | CRAgentSubsDetails {agentSubs :: SubscriptionsInfo} - | CRConnectionDisabled {connectionEntity :: ConnectionEntity} - | CRAgentRcvQueueDeleted {agentConnId :: AgentConnId, server :: SMPServer, agentQueueId :: AgentQueueId, agentError_ :: Maybe AgentErrorType} - | CRAgentConnDeleted {agentConnId :: AgentConnId} - | CRAgentUserDeleted {agentUserId :: Int64} - | CRMessageError {user :: User, severity :: Text, errorMessage :: Text} - | CRChatCmdError {user_ :: Maybe User, chatError :: ChatError} - | CRChatError {user_ :: Maybe User, chatError :: ChatError} - | CRChatErrors {user_ :: Maybe User, chatErrors :: [ChatError]} - | CRArchiveImported {archiveErrors :: [ArchiveError]} - | CRTimedAction {action :: String, durationMilliseconds :: Int64} + | CRAgentQueuesInfo {agentQueuesInfo :: AgentQueuesInfo} + | CRAppSettings {appSettings :: AppSettings} + | CRCustomChatResponse {user_ :: Maybe User, response :: Text} deriving (Show) --- some of these can only be used as command responses -allowRemoteEvent :: ChatResponse -> Bool +data ChatEvent + = CEvtChatSuspended + | CEvtContactSwitch {user :: User, contact :: Contact, switchProgress :: SwitchProgress} + | CEvtGroupMemberSwitch {user :: User, groupInfo :: GroupInfo, member :: GroupMember, switchProgress :: SwitchProgress} + | CEvtContactRatchetSync {user :: User, contact :: Contact, ratchetSyncProgress :: RatchetSyncProgress} + | CEvtGroupMemberRatchetSync {user :: User, groupInfo :: GroupInfo, member :: GroupMember, ratchetSyncProgress :: RatchetSyncProgress} + | CEvtNewChatItems {user :: User, chatItems :: [AChatItem]} -- there is the same command response + | CEvtChatItemsStatusesUpdated {user :: User, chatItems :: [AChatItem]} + | CEvtChatItemUpdated {user :: User, chatItem :: AChatItem} -- there is the same command response + | CEvtChatItemNotChanged {user :: User, chatItem :: AChatItem} -- there is the same command response + | CEvtChatItemReaction {user :: User, added :: Bool, reaction :: ACIReaction} -- there is the same command response + | CEvtGroupChatItemsDeleted {user :: User, groupInfo :: GroupInfo, chatItemIDs :: [ChatItemId], byUser :: Bool, member_ :: Maybe GroupMember} -- there is the same command response + | CEvtChatItemsDeleted {user :: User, chatItemDeletions :: [ChatItemDeletion], byUser :: Bool, timed :: Bool} -- there is the same command response + | CEvtChatItemDeletedNotFound {user :: User, contact :: Contact, sharedMsgId :: SharedMsgId} + | CEvtUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact} -- there is the same command response + | CEvtGroupLinkConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember} + | CEvtBusinessLinkConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, fromContact :: Contact} + | CEvtSentGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember} -- there is the same command response + | CEvtContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact} + | CEvtGroupMemberUpdated {user :: User, groupInfo :: GroupInfo, fromMember :: GroupMember, toMember :: GroupMember} + | CEvtContactsMerged {user :: User, intoContact :: Contact, mergedContact :: Contact, updatedContact :: Contact} + | CEvtContactDeletedByContact {user :: User, contact :: Contact} + | CEvtReceivedContactRequest {user :: User, contactRequest :: UserContactRequest} + | CEvtAcceptingContactRequest {user :: User, contact :: Contact} -- there is the same command response + | CEvtAcceptingBusinessRequest {user :: User, groupInfo :: GroupInfo} + | CEvtContactRequestAlreadyAccepted {user :: User, contact :: Contact} + | CEvtBusinessRequestAlreadyAccepted {user :: User, groupInfo :: GroupInfo} + | CEvtRcvFileDescrReady {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer, rcvFileDescr :: RcvFileDescr} + | CEvtRcvFileAccepted {user :: User, chatItem :: AChatItem} -- there is the same command response + -- TODO add chatItem :: AChatItem + | CEvtRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer} -- there is the same command response + | CEvtRcvFileStart {user :: User, chatItem :: AChatItem} -- sent by chats + | CEvtRcvFileProgressXFTP {user :: User, chatItem_ :: Maybe AChatItem, receivedSize :: Int64, totalSize :: Int64, rcvFileTransfer :: RcvFileTransfer} + | CEvtRcvFileComplete {user :: User, chatItem :: AChatItem} + | CEvtRcvStandaloneFileComplete {user :: User, targetPath :: FilePath, rcvFileTransfer :: RcvFileTransfer} + | CEvtRcvFileSndCancelled {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer} + | CEvtRcvFileError {user :: User, chatItem_ :: Maybe AChatItem, agentError :: AgentErrorType, rcvFileTransfer :: RcvFileTransfer} + | CEvtRcvFileWarning {user :: User, chatItem_ :: Maybe AChatItem, agentError :: AgentErrorType, rcvFileTransfer :: RcvFileTransfer} + | CEvtSndFileStart {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} + | CEvtSndFileComplete {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} + | CEvtSndFileRcvCancelled {user :: User, chatItem_ :: Maybe AChatItem, sndFileTransfer :: SndFileTransfer} + | CEvtSndFileStartXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta} -- not used + | CEvtSndFileProgressXFTP {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, sentSize :: Int64, totalSize :: Int64} + | CEvtSndFileRedirectStartXFTP {user :: User, fileTransferMeta :: FileTransferMeta, redirectMeta :: FileTransferMeta} + | CEvtSndFileCompleteXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta} + | CEvtSndStandaloneFileComplete {user :: User, fileTransferMeta :: FileTransferMeta, rcvURIs :: [Text]} + | CEvtSndFileCancelledXFTP {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta} + | CEvtSndFileError {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, errorMessage :: Text} + | CEvtSndFileWarning {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, errorMessage :: Text} + | CEvtContactConnecting {user :: User, contact :: Contact} + | CEvtContactConnected {user :: User, contact :: Contact, userCustomProfile :: Maybe Profile} + | CEvtContactSndReady {user :: User, contact :: Contact} + | CEvtContactAnotherClient {user :: User, contact :: Contact} + | CEvtSubscriptionEnd {user :: User, connectionEntity :: ConnectionEntity} + | CEvtContactsDisconnected {server :: SMPServer, contactRefs :: [ContactRef]} + | CEvtContactsSubscribed {server :: SMPServer, contactRefs :: [ContactRef]} + | CEvtContactSubError {user :: User, contact :: Contact, chatError :: ChatError} + | CEvtContactSubSummary {user :: User, contactSubscriptions :: [ContactSubStatus]} + | CEvtUserContactSubSummary {user :: User, userContactSubscriptions :: [UserContactSubStatus]} + | CEvtNetworkStatus {networkStatus :: NetworkStatus, connections :: [AgentConnId]} + | CEvtNetworkStatuses {user_ :: Maybe User, networkStatuses :: [ConnNetworkStatus]} -- there is the same command response + | CEvtHostConnected {protocol :: AProtocolType, transportHost :: TransportHost} + | CEvtHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost} + | CEvtReceivedGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole} + | CEvtUserJoinedGroup {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember} + | CEvtJoinedGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} -- there is the same command response + | CEvtJoinedGroupMemberConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, member :: GroupMember} + | CEvtMemberRole {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole} + | CEvtMemberBlockedForAll {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, member :: GroupMember, blocked :: Bool} + | CEvtConnectedToGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember, memberContact :: Maybe Contact} + | CEvtDeletedMember {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, deletedMember :: GroupMember, withMessages :: Bool} + | CEvtDeletedMemberUser {user :: User, groupInfo :: GroupInfo, member :: GroupMember, withMessages :: Bool} + | CEvtLeftMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | CEvtUnknownMemberCreated {user :: User, groupInfo :: GroupInfo, forwardedByMember :: GroupMember, member :: GroupMember} + | CEvtUnknownMemberBlocked {user :: User, groupInfo :: GroupInfo, blockedByMember :: GroupMember, member :: GroupMember} + | CEvtUnknownMemberAnnounced {user :: User, groupInfo :: GroupInfo, announcingMember :: GroupMember, unknownMember :: GroupMember, announcedMember :: GroupMember} + | CEvtGroupDeleted {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | CEvtGroupUpdated {user :: User, fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember} -- there is the same command response + | CEvtAcceptingGroupJoinRequestMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | CEvtNoMemberContactCreating {user :: User, groupInfo :: GroupInfo, member :: GroupMember} -- only used in CLI + | CEvtNewMemberContactReceivedInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} + | CEvtContactAndMemberAssociated {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember, updatedContact :: Contact} + | CEvtCallInvitation {callInvitation :: RcvCallInvitation} + | CEvtCallOffer {user :: User, contact :: Contact, callType :: CallType, offer :: WebRTCSession, sharedKey :: Maybe C.Key, askConfirmation :: Bool} + | CEvtCallAnswer {user :: User, contact :: Contact, answer :: WebRTCSession} + | CEvtCallExtraInfo {user :: User, contact :: Contact, extraInfo :: WebRTCExtraInfo} + | CEvtCallEnded {user :: User, contact :: Contact} + | CEvtNtfMessage {user :: User, connEntity :: ConnectionEntity, ntfMessage :: NtfMsgAckInfo} + | CEvtRemoteHostSessionCode {remoteHost_ :: Maybe RemoteHostInfo, sessionCode :: Text} + | CEvtNewRemoteHost {remoteHost :: RemoteHostInfo} + | CEvtRemoteHostConnected {remoteHost :: RemoteHostInfo} + | CEvtRemoteHostStopped {remoteHostId_ :: Maybe RemoteHostId, rhsState :: RemoteHostSessionState, rhStopReason :: RemoteHostStopReason} + | CEvtRemoteCtrlFound {remoteCtrl :: RemoteCtrlInfo, ctrlAppInfo_ :: Maybe CtrlAppInfo, appVersion :: AppVersion, compatible :: Bool} + | CEvtRemoteCtrlSessionCode {remoteCtrl_ :: Maybe RemoteCtrlInfo, sessionCode :: Text} + | CEvtRemoteCtrlStopped {rcsState :: RemoteCtrlSessionState, rcStopReason :: RemoteCtrlStopReason} + | CEvtContactPQEnabled {user :: User, contact :: Contact, pqEnabled :: PQEncryption} + | CEvtContactDisabled {user :: User, contact :: Contact} + | CEvtConnectionDisabled {connectionEntity :: ConnectionEntity} + | CEvtConnectionInactive {connectionEntity :: ConnectionEntity, inactive :: Bool} + | CEvtAgentRcvQueuesDeleted {deletedRcvQueues :: NonEmpty DeletedRcvQueue} + | CEvtAgentConnsDeleted {agentConnIds :: NonEmpty AgentConnId} + | CEvtAgentUserDeleted {agentUserId :: Int64} + | CEvtMessageError {user :: User, severity :: Text, errorMessage :: Text} + | CEvtChatErrors {chatErrors :: [ChatError]} + | CEvtTimedAction {action :: String, durationMilliseconds :: Int64} + | CEvtTerminalEvent TerminalEvent + deriving (Show) + +data TerminalEvent + = TEGroupLinkRejected {user :: User, groupInfo :: GroupInfo, groupRejectionReason :: GroupRejectionReason} + | TERejectingGroupJoinRequestMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember, groupRejectionReason :: GroupRejectionReason} + | TENewMemberContact {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} + | TEContactVerificationReset {user :: User, contact :: Contact} + | TEGroupMemberVerificationReset {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | TEGroupEmpty {user :: User, shortGroupInfo :: ShortGroupInfo} + | TEGroupSubscribed {user :: User, shortGroupInfo :: ShortGroupInfo} + | TEGroupInvitation {user :: User, shortGroupInfo :: ShortGroupInfo} + | TEMemberSubError {user :: User, shortGroupInfo :: ShortGroupInfo, memberToSubscribe :: ShortGroupMember, chatError :: ChatError} + | TEMemberSubSummary {user :: User, memberSubscriptions :: [MemberSubStatus]} + | TEPendingSubSummary {user :: User, pendingSubscriptions :: [PendingSubStatus]} + | TESndFileSubError {user :: User, sndFileTransfer :: SndFileTransfer, chatError :: ChatError} + | TERcvFileSubError {user :: User, rcvFileTransfer :: RcvFileTransfer, chatError :: ChatError} + deriving (Show) + +data DeletedRcvQueue = DeletedRcvQueue + { agentConnId :: AgentConnId, + server :: SMPServer, + agentQueueId :: AgentQueueId, + agentError_ :: Maybe AgentErrorType + } + deriving (Show) + +allowRemoteEvent :: ChatEvent -> Bool allowRemoteEvent = \case - CRChatStarted -> False - CRChatRunning -> False - CRChatStopped -> False - CRChatSuspended -> False - CRRemoteHostList _ -> False - CRCurrentRemoteHost _ -> False - CRRemoteHostStarted {} -> False - CRRemoteHostSessionCode {} -> False - CRNewRemoteHost _ -> False - CRRemoteHostConnected _ -> False - CRRemoteHostStopped {} -> False - CRRemoteFileStored {} -> False - CRRemoteCtrlList _ -> False - CRRemoteCtrlFound {} -> False - CRRemoteCtrlConnecting {} -> False - CRRemoteCtrlSessionCode {} -> False - CRRemoteCtrlConnected _ -> False - CRRemoteCtrlStopped {} -> False - CRSQLResult _ -> False - CRSlowSQLQueries {} -> False + CEvtChatSuspended -> False + CEvtRemoteHostSessionCode {} -> False + CEvtNewRemoteHost _ -> False + CEvtRemoteHostConnected _ -> False + CEvtRemoteHostStopped {} -> False + CEvtRemoteCtrlFound {} -> False + CEvtRemoteCtrlSessionCode {} -> False + CEvtRemoteCtrlStopped {} -> False _ -> True -logResponseToFile :: ChatResponse -> Bool -logResponseToFile = \case - CRContactsDisconnected {} -> True - CRContactsSubscribed {} -> True - CRContactSubError {} -> True - CRMemberSubError {} -> True - CRSndFileSubError {} -> True - CRRcvFileSubError {} -> True - CRHostConnected {} -> True - CRHostDisconnected {} -> True - CRConnectionDisabled {} -> True - CRAgentRcvQueueDeleted {} -> True - CRAgentConnDeleted {} -> True - CRAgentUserDeleted {} -> True - CRChatCmdError {} -> True - CRChatError {} -> True - CRMessageError {} -> True +logEventToFile :: ChatEvent -> Bool +logEventToFile = \case + CEvtContactsDisconnected {} -> True + CEvtContactsSubscribed {} -> True + CEvtContactSubError {} -> True + CEvtHostConnected {} -> True + CEvtHostDisconnected {} -> True + CEvtConnectionDisabled {} -> True + CEvtAgentRcvQueuesDeleted {} -> True + CEvtAgentConnsDeleted {} -> True + CEvtAgentUserDeleted {} -> True + -- CRChatCmdError {} -> True -- TODO this should be separately logged to file as command error + CEvtMessageError {} -> True + CEvtTerminalEvent te -> case te of + TEMemberSubError {} -> True + TESndFileSubError {} -> True + TERcvFileSubError {} -> True + _ -> False _ -> False +-- (Maybe GroupMemberId) can later be changed to GroupSndScope = GSSAll | GSSAdmins | GSSMember GroupMemberId +data SendRef + = SRDirect ContactId + | SRGroup GroupId (Maybe GroupMemberId) + deriving (Eq, Show) + +sendToChatRef :: SendRef -> ChatRef +sendToChatRef = \case + SRDirect cId -> ChatRef CTDirect cId + SRGroup gId _ -> ChatRef CTGroup gId + data ChatPagination = CPLast Int | CPAfter ChatItemId Int | CPBefore ChatItemId Int + | CPAround ChatItemId Int + | CPInitial Int deriving (Show) data PaginationByTime @@ -760,10 +932,17 @@ data ChatListQuery clqNoFilters :: ChatListQuery clqNoFilters = CLQFilters {favorite = False, unread = False} +data ChatDeleteMode + = CDMFull {notify :: Bool} -- delete both contact and conversation + | CDMEntity {notify :: Bool} -- delete contact (connection), keep conversation + | CDMMessages -- delete conversation, keep contact - can be re-opened from Contacts view + deriving (Show) + data ConnectionPlan = CPInvitationLink {invitationLinkPlan :: InvitationLinkPlan} | CPContactAddress {contactAddressPlan :: ContactAddressPlan} | CPGroupLink {groupLinkPlan :: GroupLinkPlan} + | CPError {chatError :: ChatError} deriving (Show) data InvitationLinkPlan @@ -807,6 +986,14 @@ connectionPlanProceed = \case GLPOwnLink _ -> True GLPConnectingConfirmReconnect -> True _ -> False + CPError _ -> True + +data ForwardConfirmation + = FCFilesNotAccepted {fileIds :: [FileTransferId]} + | FCFilesInProgress {filesCount :: Int} + | FCFilesMissing {filesCount :: Int} + | FCFilesFailed {filesCount :: Int} + deriving (Show) newtype UserPwd = UserPwd {unUserPwd :: Text} deriving (Eq, Show) @@ -833,24 +1020,6 @@ instance ToJSON AgentQueueId where toJSON = strToJSON toEncoding = strToJEncoding -data ProtoServersConfig p = ProtoServersConfig {servers :: [ServerCfg p]} - deriving (Show) - -data AProtoServersConfig = forall p. ProtocolTypeI p => APSC (SProtocolType p) (ProtoServersConfig p) - -deriving instance Show AProtoServersConfig - -data UserProtoServers p = UserProtoServers - { serverProtocol :: SProtocolType p, - protoServers :: NonEmpty (ServerCfg p), - presetServers :: NonEmpty (ProtoServerWithAuth p) - } - deriving (Show) - -data AUserProtoServers = forall p. (ProtocolTypeI p, UserProtocol p) => AUPS (UserProtoServers p) - -deriving instance Show AUserProtoServers - data ArchiveConfig = ArchiveConfig {archivePath :: FilePath, disableCompression :: Maybe Bool, parentTempDirectory :: Maybe FilePath} deriving (Show) @@ -869,6 +1038,41 @@ instance StrEncoding DBEncryptionKey where instance FromJSON DBEncryptionKey where parseJSON = strParseJSON "DBEncryptionKey" +data AppFilePathsConfig = AppFilePathsConfig + { appFilesFolder :: FilePath, + appTempFolder :: FilePath, + appAssetsFolder :: FilePath, + appRemoteHostsFolder :: Maybe FilePath + } + deriving (Show) + +data SimpleNetCfg = SimpleNetCfg + { socksProxy :: Maybe SocksProxyWithAuth, + socksMode :: SocksMode, + hostMode :: HostMode, + requiredHostMode :: Bool, + smpProxyMode_ :: Maybe SMPProxyMode, + smpProxyFallback_ :: Maybe SMPProxyFallback, + smpWebPortServers :: SMPWebPortServers, + tcpTimeout_ :: Maybe Int, + logTLSErrors :: Bool + } + deriving (Show) + +defaultSimpleNetCfg :: SimpleNetCfg +defaultSimpleNetCfg = + SimpleNetCfg + { socksProxy = Nothing, + socksMode = SMAlways, + hostMode = HMOnionViaSocks, + requiredHostMode = False, + smpProxyMode_ = Nothing, + smpProxyFallback_ = Nothing, + smpWebPortServers = SWPPreset, + tcpTimeout_ = Nothing, + logTLSErrors = False + } + data ContactSubStatus = ContactSubStatus { contact :: Contact, contactError :: Maybe ChatError @@ -876,7 +1080,7 @@ data ContactSubStatus = ContactSubStatus deriving (Show) data MemberSubStatus = MemberSubStatus - { member :: GroupMember, + { member :: ShortGroupMember, memberError :: Maybe ChatError } deriving (Show) @@ -894,8 +1098,7 @@ data PendingSubStatus = PendingSubStatus deriving (Show) data UserProfileUpdateSummary = UserProfileUpdateSummary - { notChanged :: Int, - updateSuccesses :: Int, + { updateSuccesses :: Int, updateFailures :: Int, changedContacts :: [Contact] } @@ -904,39 +1107,68 @@ data UserProfileUpdateSummary = UserProfileUpdateSummary data ComposedMessage = ComposedMessage { fileSource :: Maybe CryptoFile, quotedItemId :: Maybe ChatItemId, - msgContent :: MsgContent + msgContent :: MsgContent, + mentions :: Map MemberName GroupMemberId } deriving (Show) --- This instance is needed for backward compatibility, can be removed in v6.0 -instance FromJSON ComposedMessage where - parseJSON (J.Object v) = do - fileSource <- - (v .:? "fileSource") >>= \case - Nothing -> CF.plain <$$> (v .:? "filePath") - f -> pure f - quotedItemId <- v .:? "quotedItemId" - msgContent <- v .: "msgContent" - pure ComposedMessage {fileSource, quotedItemId, msgContent} - parseJSON invalid = - JT.prependFailure "bad ComposedMessage, " (JT.typeMismatch "Object" invalid) - -data XFTPFileConfig = XFTPFileConfig - { minFileSize :: Integer +data UpdatedMessage = UpdatedMessage + { msgContent :: MsgContent, + mentions :: Map MemberName GroupMemberId } deriving (Show) -defaultXFTPFileConfig :: XFTPFileConfig -defaultXFTPFileConfig = XFTPFileConfig {minFileSize = 0} +data ChatTagData = ChatTagData + { emoji :: Maybe Text, + text :: Text + } + deriving (Show) +instance FromJSON ChatTagData where + parseJSON (J.Object v) = ChatTagData <$> v .:? "emoji" <*> v .: "text" + parseJSON invalid = JT.prependFailure "bad ChatTagData, " (JT.typeMismatch "Object" invalid) + +data NtfConn = NtfConn + { user :: User, + agentConnId :: AgentConnId, + agentDbQueueId :: Int64, + connEntity :: ConnectionEntity, + -- Decrypted ntf meta of the expected message (the one notification was sent for). + -- Nothing means it failed to decrypt or to decode, we can still show event for entity + expectedMsg_ :: Maybe NtfMsgInfo + } + deriving (Show) + +-- msgTs is broker message timestamp, it is used in ConnMsgReq / APIGetConnNtfMessages +-- to set it as last connection message in case queue is empty data NtfMsgInfo = NtfMsgInfo {msgId :: Text, msgTs :: UTCTime} deriving (Show) -ntfMsgInfo :: SMPMsgMeta -> NtfMsgInfo -ntfMsgInfo SMPMsgMeta {msgId, msgTs} = NtfMsgInfo {msgId = decodeLatin1 $ strEncode msgId, msgTs = systemToUTCTime msgTs} +data RcvNtfMsgInfo + = RNMInfo {ntfMsgInfo :: Maybe NtfMsgInfo} + | RNMError {ntfMsgError :: AgentErrorType} + deriving (Show) -crNtfToken :: (DeviceToken, NtfTknStatus, NotificationsMode) -> ChatResponse -crNtfToken (token, status, ntfMode) = CRNtfToken {token, status, ntfMode} +receivedMsgInfo :: Either AgentErrorType (Maybe SMPMsgMeta) -> RcvNtfMsgInfo +receivedMsgInfo = \case + Right msgMeta_ -> RNMInfo $ (\SMPMsgMeta {msgId, msgTs} -> ntfMsgInfo_ msgId msgTs) <$> msgMeta_ + Left e -> RNMError e + +expectedMsgInfo :: NMsgMeta -> NtfMsgInfo +expectedMsgInfo NMsgMeta {msgId, msgTs} = ntfMsgInfo_ msgId msgTs + +ntfMsgInfo_ :: MsgId -> SystemTime -> NtfMsgInfo +ntfMsgInfo_ msgId msgTs = NtfMsgInfo {msgId = decodeLatin1 $ strEncode msgId, msgTs = systemToUTCTime msgTs} + +-- Acknowledged message info - used to correlate with expected message +data NtfMsgAckInfo = NtfMsgAckInfo {msgId :: Text, msgTs_ :: Maybe UTCTime} + deriving (Show) + +ntfMsgAckInfo :: MsgId -> Maybe UTCTime -> NtfMsgAckInfo +ntfMsgAckInfo msgId msgTs_ = NtfMsgAckInfo {msgId = decodeLatin1 $ strEncode msgId, msgTs_} + +crNtfToken :: (DeviceToken, NtfTknStatus, NotificationsMode, NtfServer) -> ChatResponse +crNtfToken (token, status, ntfMode, ntfServer) = CRNtfToken {token, status, ntfMode, ntfServer} data SwitchProgress = SwitchProgress { queueDirection :: QueueDirection, @@ -978,6 +1210,12 @@ tmeToPref currentTTL tme = uncurry TimedMessagesPreference $ case tme of TMEEnableKeepTTL -> (FAYes, currentTTL) TMEDisableKeepTTL -> (FANo, currentTTL) +data ChatItemDeletion = ChatItemDeletion + { deletedChatItem :: AChatItem, + toChatItem :: Maybe AChatItem + } + deriving (Show) + data ChatLogLevel = CLLDebug | CLLInfo | CLLWarning | CLLError | CLLImportant deriving (Eq, Ord, Show) @@ -988,16 +1226,13 @@ data CoreVersionInfo = CoreVersionInfo } deriving (Show) -data SendFileMode - = SendFileSMP (Maybe InlineFileMode) - | SendFileXFTP - deriving (Show) - +#if !defined(dbPostgres) data SlowSQLQuery = SlowSQLQuery { query :: Text, queryStats :: SlowQueryStats } deriving (Show) +#endif data ChatError = ChatError {errorType :: ChatErrorType} @@ -1028,8 +1263,8 @@ data ChatErrorType | CEChatNotStarted | CEChatNotStopped | CEChatStoreChanged - | CEConnectionPlan {connectionPlan :: ConnectionPlan} | CEInvalidConnReq + | CEUnsupportedConnReq | CEInvalidChatMessage {connection :: Connection, msgMeta :: Maybe MsgMetaJSON, messageData :: Text, message :: String} | CEContactNotFound {contactName :: ContactName, suspectedMember :: Maybe (GroupInfo, GroupMember)} | CEContactNotReady {contact :: Contact} @@ -1045,9 +1280,9 @@ data ChatErrorType | CEGroupDuplicateMemberId | CEGroupNotJoined {groupInfo :: GroupInfo} | CEGroupMemberNotActive + | CECantBlockMemberForSelf {groupInfo :: GroupInfo, member :: GroupMember, setShowMessages :: Bool} | CEGroupMemberUserRemoved | CEGroupMemberNotFound - | CEGroupMemberIntroNotFound {contactName :: ContactName} | CEGroupCantResendInvitation {groupInfo :: GroupInfo, contactName :: ContactName} | CEGroupInternal {message :: String} | CEFileNotFound {message :: String} @@ -1064,11 +1299,10 @@ data ChatErrorType | CEFileImageType {filePath :: FilePath} | CEFileImageSize {filePath :: FilePath} | CEFileNotReceived {fileId :: FileTransferId} - | CEXFTPRcvFile {fileId :: FileTransferId, agentRcvFileId :: AgentRcvFileId, agentError :: AgentErrorType} - | CEXFTPSndFile {fileId :: FileTransferId, agentSndFileId :: AgentSndFileId, agentError :: AgentErrorType} + | CEFileNotApproved {fileId :: FileTransferId, unknownServers :: [XFTPServer]} | CEFallbackToSMPProhibited {fileId :: FileTransferId} | CEInlineFileProhibited {fileId :: FileTransferId} - | CEInvalidQuote + | CEInvalidForward | CEInvalidChatItemUpdate | CEInvalidChatItemDelete | CEHasCurrentCall @@ -1083,6 +1317,7 @@ data ChatErrorType | CEAgentCommandError {message :: String} | CEInvalidFileDescription {message :: String} | CEConnectionIncognitoChangeProhibited + | CEConnectionUserChangeProhibited | CEPeerChatVRangeIncompatible | CEInternalError {message :: String} | CEException {message :: String} @@ -1096,10 +1331,10 @@ data DatabaseError | DBErrorOpen {sqliteError :: SQLiteError} deriving (Show, Exception) -data SQLiteError = SQLiteErrorNotADatabase | SQLiteError String +data SQLiteError = SQLiteErrorNotADatabase | SQLiteError {dbError :: String} deriving (Show, Exception) -throwDBError :: ChatMonad m => DatabaseError -> m () +throwDBError :: DatabaseError -> CM () throwDBError = throwError . ChatErrorDatabase -- TODO review errors, some of it can be covered by HTTP2 errors @@ -1145,8 +1380,8 @@ data RemoteCtrlStopReason deriving (Show, Exception) data ArchiveError - = AEImport {chatError :: ChatError} - | AEImportFile {file :: String, chatError :: ChatError} + = AEImport {importError :: String} + | AEFileError {file :: String, fileError :: String} deriving (Show, Exception) -- | Host (mobile) side of transport to process remote commands and forward notifications @@ -1176,7 +1411,7 @@ data RemoteCtrlSession tls :: TLS, rcsSession :: RCCtrlSession, http2Server :: Async (), - remoteOutputQ :: TBQueue ChatResponse + remoteOutputQ :: TBQueue (Either ChatError ChatEvent) } data RemoteCtrlSessionState @@ -1203,39 +1438,59 @@ data RemoteCtrlInfo = RemoteCtrlInfo } deriving (Show) -type ChatMonad' m = (MonadUnliftIO m, MonadReader ChatController m) +type CM' a = ReaderT ChatController IO a -type ChatMonad m = (ChatMonad' m, MonadError ChatError m) +type CM a = ExceptT ChatError (ReaderT ChatController IO) a -chatReadVar :: ChatMonad' m => (ChatController -> TVar a) -> m a -chatReadVar f = asks f >>= readTVarIO +chatReadVar :: (ChatController -> TVar a) -> CM a +chatReadVar = lift . chatReadVar' {-# INLINE chatReadVar #-} -chatWriteVar :: ChatMonad' m => (ChatController -> TVar a) -> a -> m () -chatWriteVar f value = asks f >>= atomically . (`writeTVar` value) +chatReadVar' :: (ChatController -> TVar a) -> CM' a +chatReadVar' f = asks f >>= readTVarIO +{-# INLINE chatReadVar' #-} + +chatWriteVar :: (ChatController -> TVar a) -> a -> CM () +chatWriteVar f = lift . chatWriteVar' f {-# INLINE chatWriteVar #-} -chatModifyVar :: ChatMonad' m => (ChatController -> TVar a) -> (a -> a) -> m () -chatModifyVar f newValue = asks f >>= atomically . (`modifyTVar'` newValue) +chatWriteVar' :: (ChatController -> TVar a) -> a -> CM' () +chatWriteVar' f value = asks f >>= atomically . (`writeTVar` value) +{-# INLINE chatWriteVar' #-} + +chatModifyVar :: (ChatController -> TVar a) -> (a -> a) -> CM () +chatModifyVar f = lift . chatModifyVar' f {-# INLINE chatModifyVar #-} -setContactNetworkStatus :: ChatMonad' m => Contact -> NetworkStatus -> m () -setContactNetworkStatus Contact {activeConn = Nothing} _ = pure () -setContactNetworkStatus Contact {activeConn = Just Connection {agentConnId}} status = chatModifyVar connNetworkStatuses $ M.insert agentConnId status +chatModifyVar' :: (ChatController -> TVar a) -> (a -> a) -> CM' () +chatModifyVar' f newValue = asks f >>= atomically . (`modifyTVar'` newValue) +{-# INLINE chatModifyVar' #-} -tryChatError :: ChatMonad m => m a -> m (Either ChatError a) +setContactNetworkStatus :: Contact -> NetworkStatus -> CM' () +setContactNetworkStatus Contact {activeConn = Nothing} _ = pure () +setContactNetworkStatus Contact {activeConn = Just Connection {agentConnId}} status = chatModifyVar' connNetworkStatuses $ M.insert agentConnId status + +tryChatError :: CM a -> CM (Either ChatError a) tryChatError = tryAllErrors mkChatError {-# INLINE tryChatError #-} -catchChatError :: ChatMonad m => m a -> (ChatError -> m a) -> m a +tryChatError' :: CM a -> CM' (Either ChatError a) +tryChatError' = tryAllErrors' mkChatError +{-# INLINE tryChatError' #-} + +catchChatError :: CM a -> (ChatError -> CM a) -> CM a catchChatError = catchAllErrors mkChatError {-# INLINE catchChatError #-} -chatFinally :: ChatMonad m => m a -> m b -> m a +catchChatError' :: CM a -> (ChatError -> CM' a) -> CM' a +catchChatError' = catchAllErrors' mkChatError +{-# INLINE catchChatError' #-} + +chatFinally :: CM a -> CM b -> CM a chatFinally = allFinally mkChatError {-# INLINE chatFinally #-} -onChatError :: ChatMonad m => m a -> m b -> m a +onChatError :: CM a -> CM b -> CM a a `onChatError` onErr = a `catchChatError` \e -> onErr >> throwError e {-# INLINE onChatError #-} @@ -1243,70 +1498,114 @@ mkChatError :: SomeException -> ChatError mkChatError = ChatError . CEException . show {-# INLINE mkChatError #-} -chatCmdError :: Maybe User -> String -> ChatResponse -chatCmdError user = CRChatCmdError user . ChatError . CECommandError +catchStoreError :: ExceptT StoreError IO a -> (StoreError -> ExceptT StoreError IO a) -> ExceptT StoreError IO a +catchStoreError = catchAllErrors mkStoreError +{-# INLINE catchStoreError #-} -throwChatError :: ChatMonad m => ChatErrorType -> m a +tryStoreError' :: ExceptT StoreError IO a -> IO (Either StoreError a) +tryStoreError' = tryAllErrors' mkStoreError +{-# INLINE tryStoreError' #-} + +mkStoreError :: SomeException -> StoreError +mkStoreError = SEInternalError . show +{-# INLINE mkStoreError #-} + +throwCmdError :: String -> CM a +throwCmdError = throwError . ChatError . CECommandError +{-# INLINE throwCmdError #-} + +chatCmdError :: String -> Either ChatError ChatResponse +chatCmdError = Left . ChatError . CECommandError +{-# INLINE chatCmdError #-} + +throwChatError :: ChatErrorType -> CM a throwChatError = throwError . ChatError +{-# INLINE throwChatError #-} + +toViewTE :: TerminalEvent -> CM () +toViewTE = toView . CEvtTerminalEvent +{-# INLINE toViewTE #-} -- | Emit local events. -toView :: ChatMonad' m => ChatResponse -> m () -toView event = do - localQ <- asks outputQ - session <- asks remoteCtrlSession +toView :: ChatEvent -> CM () +toView = lift . toView' +{-# INLINE toView #-} + +toView' :: ChatEvent -> CM' () +toView' = toView_ . Right +{-# INLINE toView' #-} + +eToView :: ChatError -> CM () +eToView = lift . eToView' +{-# INLINE eToView #-} + +eToView' :: ChatError -> CM' () +eToView' = toView_ . Left +{-# INLINE eToView' #-} + +toView_ :: Either ChatError ChatEvent -> CM' () +toView_ ev = do + cc@ChatController {outputQ = localQ, remoteCtrlSession = session, config = ChatConfig {chatHooks}} <- ask + event <- case eventHook chatHooks of + Just hook -> liftIO $ hook cc ev + Nothing -> pure ev atomically $ readTVar session >>= \case Just (_, RCSessionConnected {remoteOutputQ}) - | allowRemoteEvent event -> writeTBQueue remoteOutputQ event + | either (const True) allowRemoteEvent event -> writeTBQueue remoteOutputQ event -- TODO potentially, it should hold some events while connecting - _ -> writeTBQueue localQ (Nothing, Nothing, event) + _ -> writeTBQueue localQ (Nothing, event) -withStore' :: ChatMonad m => (DB.Connection -> IO a) -> m a +withStore' :: (DB.Connection -> IO a) -> CM a withStore' action = withStore $ liftIO . action +{-# INLINE withStore' #-} -withStore :: ChatMonad m => (DB.Connection -> ExceptT StoreError IO a) -> m a -withStore = withStoreCtx Nothing +withFastStore' :: (DB.Connection -> IO a) -> CM a +withFastStore' action = withFastStore $ liftIO . action +{-# INLINE withFastStore' #-} -withStoreCtx' :: ChatMonad m => Maybe String -> (DB.Connection -> IO a) -> m a -withStoreCtx' ctx_ action = withStoreCtx ctx_ $ liftIO . action +withStore :: (DB.Connection -> ExceptT StoreError IO a) -> CM a +withStore = withStorePriority False +{-# INLINE withStore #-} -withStoreCtx :: ChatMonad m => Maybe String -> (DB.Connection -> ExceptT StoreError IO a) -> m a -withStoreCtx ctx_ action = do +withFastStore :: (DB.Connection -> ExceptT StoreError IO a) -> CM a +withFastStore = withStorePriority True +{-# INLINE withFastStore #-} + +withStorePriority :: Bool -> (DB.Connection -> ExceptT StoreError IO a) -> CM a +withStorePriority priority action = do ChatController {chatStore} <- ask - liftEitherError ChatErrorStore $ case ctx_ of - Nothing -> withTransaction chatStore (runExceptT . action) `catch` handleInternal "" - -- uncomment to debug store performance - -- Just ctx -> do - -- t1 <- liftIO getCurrentTime - -- putStrLn $ "withStoreCtx start :: " <> show t1 <> " :: " <> ctx - -- r <- withTransactionCtx ctx_ chatStore (runExceptT . action) `E.catch` handleInternal (" (" <> ctx <> ")") - -- t2 <- liftIO getCurrentTime - -- putStrLn $ "withStoreCtx end :: " <> show t2 <> " :: " <> ctx <> " :: duration=" <> show (diffToMilliseconds $ diffUTCTime t2 t1) - -- pure r - Just _ -> withTransaction chatStore (runExceptT . action) `catch` handleInternal "" - where - handleInternal :: String -> SomeException -> IO (Either StoreError a) - handleInternal ctxStr e = pure . Left . SEInternalError $ show e <> ctxStr + liftIOEither $ withTransactionPriority chatStore priority (runExceptT . withExceptT ChatErrorStore . action) `E.catches` handleDBErrors -withStoreBatch :: (ChatMonad' m, Traversable t) => (DB.Connection -> t (IO (Either ChatError a))) -> m (t (Either ChatError a)) +withStoreBatch :: Traversable t => (DB.Connection -> t (IO (Either ChatError a))) -> CM' (t (Either ChatError a)) withStoreBatch actions = do ChatController {chatStore} <- ask - liftIO $ withTransaction chatStore $ mapM (`E.catch` handleInternal) . actions - where - handleInternal :: E.SomeException -> IO (Either ChatError a) - handleInternal = pure . Left . ChatError . CEInternalError . show + liftIO $ withTransaction chatStore $ mapM (`E.catches` handleDBErrors) . actions -withStoreBatch' :: (ChatMonad' m, Traversable t) => (DB.Connection -> t (IO a)) -> m (t (Either ChatError a)) +-- TODO [postgres] postgres specific error handling +handleDBErrors :: [E.Handler (Either ChatError a)] +handleDBErrors = +#if !defined(dbPostgres) + ( E.Handler $ \(e :: SQLError) -> + let se = SQL.sqlError e + busy = se == SQL.ErrorBusy || se == SQL.ErrorLocked + in pure . Left . ChatErrorStore $ if busy then SEDBBusyError $ show se else SEDBException $ show e + ) : +#endif + [ E.Handler $ \(E.SomeException e) -> pure . Left . ChatErrorStore . SEDBException $ show e + ] + +withStoreBatch' :: Traversable t => (DB.Connection -> t (IO a)) -> CM' (t (Either ChatError a)) withStoreBatch' actions = withStoreBatch $ fmap (fmap Right) . actions -withAgent :: ChatMonad m => (AgentClient -> ExceptT AgentErrorType m a) -> m a +withAgent :: (AgentClient -> ExceptT AgentErrorType IO a) -> CM a withAgent action = asks smpAgent - >>= runExceptT . action + >>= liftIO . runExceptT . action >>= liftEither . first (`ChatErrorAgent` Nothing) -withAgent' :: ChatMonad' m => (AgentClient -> m a) -> m a -withAgent' action = asks smpAgent >>= action +withAgent' :: (AgentClient -> IO a) -> CM' a +withAgent' action = asks smpAgent >>= liftIO . action $(JQ.deriveJSON (enumJSON $ dropPrefix "HS") ''HelpSection) @@ -1318,7 +1617,7 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CAP") ''ContactAddressPlan) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GLP") ''GroupLinkPlan) -$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CP") ''ConnectionPlan) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FC") ''ForwardConfirmation) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CE") ''ChatErrorType) @@ -1332,6 +1631,10 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "DB") ''DatabaseError) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "Chat") ''ChatError) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CP") ''ConnectionPlan) + +$(JQ.deriveJSON defaultJSON ''AppFilePathsConfig) + $(JQ.deriveJSON defaultJSON ''ContactSubStatus) $(JQ.deriveJSON defaultJSON ''MemberSubStatus) @@ -1346,40 +1649,29 @@ $(JQ.deriveJSON defaultJSON ''UserProfileUpdateSummary) $(JQ.deriveJSON defaultJSON ''NtfMsgInfo) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RNM") ''RcvNtfMsgInfo) + +$(JQ.deriveJSON defaultJSON ''NtfConn) + +$(JQ.deriveJSON defaultJSON ''NtfMsgAckInfo) + $(JQ.deriveJSON defaultJSON ''SwitchProgress) $(JQ.deriveJSON defaultJSON ''RatchetSyncProgress) +$(JQ.deriveJSON defaultJSON ''DeletedRcvQueue) + $(JQ.deriveJSON defaultJSON ''ServerAddress) $(JQ.deriveJSON defaultJSON ''ParsedServerAddress) +$(JQ.deriveJSON defaultJSON ''ChatItemDeletion) + $(JQ.deriveJSON defaultJSON ''CoreVersionInfo) +#if !defined(dbPostgres) $(JQ.deriveJSON defaultJSON ''SlowSQLQuery) - -instance ProtocolTypeI p => FromJSON (ProtoServersConfig p) where - parseJSON = $(JQ.mkParseJSON defaultJSON ''ProtoServersConfig) - -instance ProtocolTypeI p => FromJSON (UserProtoServers p) where - parseJSON = $(JQ.mkParseJSON defaultJSON ''UserProtoServers) - -instance ProtocolTypeI p => ToJSON (UserProtoServers p) where - toJSON = $(JQ.mkToJSON defaultJSON ''UserProtoServers) - toEncoding = $(JQ.mkToEncoding defaultJSON ''UserProtoServers) - -instance FromJSON AUserProtoServers where - parseJSON v = J.withObject "AUserProtoServers" parse v - where - parse o = do - AProtocolType (p :: SProtocolType p) <- o .: "serverProtocol" - case userProtocol p of - Just Dict -> AUPS <$> J.parseJSON @(UserProtoServers p) v - Nothing -> fail $ "AUserProtoServers: unsupported protocol " <> show p - -instance ToJSON AUserProtoServers where - toJSON (AUPS s) = $(JQ.mkToJSON defaultJSON ''UserProtoServers) s - toEncoding (AUPS s) = $(JQ.mkToEncoding defaultJSON ''UserProtoServers) s +#endif $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RCS") ''RemoteCtrlSessionState) @@ -1389,12 +1681,31 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RCSR") ''RemoteCtrlStopReason) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RHSR") ''RemoteHostStopReason) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "TE") ''TerminalEvent) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CR") ''ChatResponse) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CEvt") ''ChatEvent) + $(JQ.deriveFromJSON defaultJSON ''ArchiveConfig) $(JQ.deriveFromJSON defaultJSON ''DBEncryptionConfig) -$(JQ.deriveJSON defaultJSON ''XFTPFileConfig) - $(JQ.deriveToJSON defaultJSON ''ComposedMessage) + +instance FromJSON ComposedMessage where + parseJSON (J.Object v) = do + fileSource <- + (v .:? "fileSource") >>= \case + Nothing -> CF.plain <$$> (v .:? "filePath") + f -> pure f + quotedItemId <- v .:? "quotedItemId" + msgContent <- v .: "msgContent" + mentions <- fromMaybe M.empty <$> v .:? "mentions" + pure ComposedMessage {fileSource, quotedItemId, msgContent, mentions} + parseJSON invalid = + JT.prependFailure "bad ComposedMessage, " (JT.typeMismatch "Object" invalid) + +$(JQ.deriveJSON defaultJSON ''UpdatedMessage) + +$(JQ.deriveToJSON defaultJSON ''ChatTagData) diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index 1d870bf381..865eb6a760 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -1,46 +1,129 @@ {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} -module Simplex.Chat.Core where +module Simplex.Chat.Core + ( simplexChatCore, + runSimplexChat, + sendChatCmdStr, + sendChatCmd, + printResponseEvent, + ) +where import Control.Logger.Simple +import Control.Monad import Control.Monad.Reader +import Data.List (find) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) +import Data.Time.Clock (getCurrentTime) +import Data.Time.LocalTime (getCurrentTimeZone) import Simplex.Chat import Simplex.Chat.Controller +import Simplex.Chat.Library.Commands import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..)) +import Simplex.Chat.Remote.Types (RemoteHostId) +import Simplex.Chat.Store.Profiles import Simplex.Chat.Types +import Simplex.Chat.View (ChatResponseEvent, serializeChatError, serializeChatResponse) +import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..)) +import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction) import System.Exit (exitFailure) +import System.IO (hFlush, stdout) +import Text.Read (readMaybe) import UnliftIO.Async simplexChatCore :: ChatConfig -> ChatOpts -> (User -> ChatController -> IO ()) -> IO () -simplexChatCore cfg@ChatConfig {confirmMigrations, testView} opts@ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, dbKey, logAgent}} chat = +simplexChatCore cfg@ChatConfig {confirmMigrations, testView} opts@ChatOpts {coreOptions = CoreChatOpts {dbOptions, logAgent, yesToUpMigrations}, maintenance} chat = case logAgent of Just level -> do setLogLevel level withGlobalLogging logCfg initRun _ -> initRun where - initRun = createChatDatabase dbFilePrefix dbKey False confirmMigrations >>= either exit run + initRun = createChatDatabase dbOptions confirm' >>= either exit run + confirm' = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations exit e = do putStrLn $ "Error opening database: " <> show e exitFailure run db@ChatDatabase {chatStore} = do - u <- getCreateActiveUser chatStore testView - cc <- newChatController db (Just u) cfg opts False + u_ <- getSelectActiveUser chatStore + let backgroundMode = not maintenance + cc <- newChatController db u_ cfg opts backgroundMode + u <- maybe (createActiveUser cc) pure u_ + unless testView $ putStrLn $ "Current user: " <> userStr u runSimplexChat opts u cc chat runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController -> IO ()) -> IO () runSimplexChat ChatOpts {maintenance} u cc chat | maintenance = wait =<< async (chat u cc) | otherwise = do - a1 <- runReaderT (startChatController True True True) cc + a1 <- runReaderT (startChatController True True) cc a2 <- async $ chat u cc waitEither_ a1 a2 -sendChatCmdStr :: ChatController -> String -> IO ChatResponse +sendChatCmdStr :: ChatController -> String -> IO (Either ChatError ChatResponse) sendChatCmdStr cc s = runReaderT (execChatCommand Nothing . encodeUtf8 $ T.pack s) cc -sendChatCmd :: ChatController -> ChatCommand -> IO ChatResponse +sendChatCmd :: ChatController -> ChatCommand -> IO (Either ChatError ChatResponse) sendChatCmd cc cmd = runReaderT (execChatCommand' cmd) cc + +getSelectActiveUser :: DBStore -> IO (Maybe User) +getSelectActiveUser st = do + users <- withTransaction st getUsers + case find activeUser users of + Just u -> pure $ Just u + Nothing -> selectUser users + where + selectUser :: [User] -> IO (Maybe User) + selectUser = \case + [] -> pure Nothing + [user] -> Just <$> withTransaction st (`setActiveUser` user) + users -> do + putStrLn "Select user profile:" + forM_ (zip [1 :: Int ..] users) $ \(n, user) -> putStrLn $ show n <> ": " <> userStr user + loop + where + loop = do + nStr <- getWithPrompt $ "user number (1 .. " <> show (length users) <> ")" + case readMaybe nStr :: Maybe Int of + Nothing -> putStrLn "not a number" >> loop + Just n + | n <= 0 || n > length users -> putStrLn "invalid user number" >> loop + | otherwise -> + let user = users !! (n - 1) + in Just <$> withTransaction st (`setActiveUser` user) + +createActiveUser :: ChatController -> IO User +createActiveUser cc = do + putStrLn + "No user profiles found, it will be created now.\n\ + \Please choose your display name.\n\ + \It will be sent to your contacts when you connect.\n\ + \It is only stored on your device and you can change it later." + loop + where + loop = do + displayName <- T.pack <$> getWithPrompt "display name" + let profile = Just Profile {displayName, fullName = "", image = Nothing, contactLink = Nothing, preferences = Nothing} + execChatCommand' (CreateActiveUser NewUser {profile, pastTimestamp = False}) `runReaderT` cc >>= \case + Right (CRActiveUser user) -> pure user + r -> printResponseEvent (Nothing, Nothing) (config cc) r >> loop + +printResponseEvent :: ChatResponseEvent r => (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Either ChatError r -> IO () +printResponseEvent hu cfg = \case + Right r -> do + ts <- getCurrentTime + tz <- getCurrentTimeZone + putStrLn $ serializeChatResponse hu cfg ts tz (fst hu) r + Left e -> do + putStrLn $ serializeChatError True cfg e + +getWithPrompt :: String -> IO String +getWithPrompt s = putStr (s <> ": ") >> hFlush stdout >> getLine + +userStr :: User -> String +userStr User {localDisplayName, profile = LocalProfile {fullName}} = + T.unpack $ localDisplayName <> if T.null fullName || localDisplayName == fullName then "" else " (" <> fullName <> ")" diff --git a/src/Simplex/Chat/Files.hs b/src/Simplex/Chat/Files.hs index 9c6d731dd7..0c04b22e28 100644 --- a/src/Simplex/Chat/Files.hs +++ b/src/Simplex/Chat/Files.hs @@ -3,13 +3,12 @@ module Simplex.Chat.Files where -import Control.Monad.IO.Class import Simplex.Chat.Controller import Simplex.Messaging.Util (ifM) import System.FilePath (combine, splitExtensions) import UnliftIO.Directory (doesDirectoryExist, doesFileExist, getHomeDirectory, getTemporaryDirectory) -uniqueCombine :: MonadIO m => FilePath -> String -> m FilePath +uniqueCombine :: FilePath -> String -> IO FilePath uniqueCombine fPath fName = tryCombine (0 :: Int) where tryCombine n = @@ -18,10 +17,10 @@ uniqueCombine fPath fName = tryCombine (0 :: Int) f = fPath `combine` (name <> suffix <> ext) in ifM (doesFileExist f) (tryCombine $ n + 1) (pure f) -getChatTempDirectory :: ChatMonad m => m FilePath -getChatTempDirectory = chatReadVar tempDirectory >>= maybe getTemporaryDirectory pure +getChatTempDirectory :: CM' FilePath +getChatTempDirectory = chatReadVar' tempDirectory >>= maybe getTemporaryDirectory pure -getDefaultFilesFolder :: ChatMonad m => m FilePath +getDefaultFilesFolder :: CM' FilePath getDefaultFilesFolder = do dir <- (`combine` "Downloads") <$> getHomeDirectory ifM (doesDirectoryExist dir) (pure dir) getChatTempDirectory diff --git a/src/Simplex/Chat/Help.hs b/src/Simplex/Chat/Help.hs index ac93e05533..adb77b9557 100644 --- a/src/Simplex/Chat/Help.hs +++ b/src/Simplex/Chat/Help.hs @@ -185,6 +185,8 @@ contactsHelpInfo = indent <> highlight "/verify @<name> " <> " - clear security code verification", indent <> highlight "/info @<name> " <> " - info about contact connection", indent <> highlight "/switch @<name> " <> " - switch receiving messages to another SMP relay", + indent <> highlight "/pq @<name> on/off " <> " - [BETA] toggle quantum resistant / standard e2e encryption for a contact", + indent <> " " <> " (both have to enable for quantum resistance)", "", green "Contact chat preferences:", indent <> highlight "/set voice @<name> yes/no/always " <> " - allow/prohibit voice messages with the contact", @@ -320,6 +322,7 @@ settingsInfo = map styleMarkdown [ green "Chat settings:", + indent <> highlight "/pq on/off " <> " - [BETA] toggle quantum resistant / standard e2e encryption for the new contacts", indent <> highlight "/network " <> " - show / set network access options", indent <> highlight "/smp " <> " - show / set configured SMP servers", indent <> highlight "/xftp " <> " - show / set configured XFTP servers", diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs new file mode 100644 index 0000000000..8c475b111b --- /dev/null +++ b/src/Simplex/Chat/Library/Commands.hs @@ -0,0 +1,4403 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} + +module Simplex.Chat.Library.Commands where + +import Control.Applicative (optional, (<|>)) +import Control.Concurrent.STM (retry) +import Control.Logger.Simple +import Control.Monad +import Control.Monad.Except +import Control.Monad.IO.Unlift +import Control.Monad.Reader +import qualified Data.Aeson as J +import Data.Attoparsec.ByteString.Char8 (Parser) +import qualified Data.Attoparsec.ByteString.Char8 as A +import qualified Data.ByteString.Base64 as B64 +import Data.ByteString.Char8 (ByteString) +import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Lazy.Char8 as LB +import Data.Char +import Data.Constraint (Dict (..)) +import Data.Either (fromRight, partitionEithers, rights) +import Data.Foldable (foldr') +import Data.Functor (($>)) +import Data.Int (Int64) +import Data.List (dropWhileEnd, find, foldl', isSuffixOf, partition, sortOn, zipWith4) +import Data.List.NonEmpty (NonEmpty (..)) +import qualified Data.List.NonEmpty as L +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as M +import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList) +import qualified Data.Set as S +import Data.Text (Text) +import qualified Data.Text as T +import Data.Text.Encoding (decodeLatin1, encodeUtf8) +import Data.Time (NominalDiffTime, addUTCTime, defaultTimeLocale, formatTime) +import Data.Time.Clock (UTCTime, getCurrentTime, nominalDay) +import Data.Type.Equality +import qualified Data.UUID as UUID +import qualified Data.UUID.V4 as V4 +import Simplex.Chat.Library.Subscriber +import Simplex.Chat.Call +import Simplex.Chat.Controller +import Simplex.Chat.Files +import Simplex.Chat.Markdown +import Simplex.Chat.Messages +import Simplex.Chat.Messages.CIContent +import Simplex.Chat.Messages.CIContent.Events +import Simplex.Chat.Operators +import Simplex.Chat.Options +import Simplex.Chat.ProfileGenerator (generateRandomProfile) +import Simplex.Chat.Protocol +import Simplex.Chat.Remote +import Simplex.Chat.Remote.Types +import Simplex.Chat.Library.Internal +import Simplex.Chat.Stats +import Simplex.Chat.Store +import Simplex.Chat.Store.AppSettings +import Simplex.Chat.Store.Connections +import Simplex.Chat.Store.Direct +import Simplex.Chat.Store.Files +import Simplex.Chat.Store.Groups +import Simplex.Chat.Store.Messages +import Simplex.Chat.Store.NoteFolders +import Simplex.Chat.Store.Profiles +import Simplex.Chat.Store.Shared +import Simplex.Chat.Types +import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared +import Simplex.Chat.Util (liftIOEither, zipWith3') +import qualified Simplex.Chat.Util as U +import Simplex.FileTransfer.Description (FileDescriptionURI (..), maxFileSize, maxFileSizeHard) +import Simplex.Messaging.Agent as Agent +import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles) +import Simplex.Messaging.Agent.Protocol +import Simplex.Messaging.Agent.Store.Interface (execSQL) +import Simplex.Messaging.Agent.Store.Shared (upMigration) +import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Agent.Store.Interface (getCurrentMigrations) +import Simplex.Messaging.Client (NetworkConfig (..), SMPWebPortServers (..), SocksMode (SMAlways), textToHostMode) +import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) +import qualified Simplex.Messaging.Crypto.File as CF +import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOff, pattern PQSupportOff, pattern PQSupportOn) +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) +import qualified Simplex.Messaging.Protocol as SMP +import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) +import qualified Simplex.Messaging.TMap as TM +import Simplex.Messaging.Transport.Client (defaultSocksProxyWithAuth) +import Simplex.Messaging.Util +import Simplex.Messaging.Version +import Simplex.RemoteControl.Invitation (RCInvitation (..), RCSignedInvitation (..)) +import Simplex.RemoteControl.Types (RCCtrlAddress (..)) +import System.Exit (ExitCode, exitSuccess) +import System.FilePath (takeExtension, takeFileName, (</>)) +import System.IO (Handle, IOMode (..)) +import System.Random (randomRIO) +import UnliftIO.Async +import UnliftIO.Concurrent (forkIO, threadDelay) +import UnliftIO.Directory +import qualified UnliftIO.Exception as E +import UnliftIO.IO (hClose) +import UnliftIO.STM +#if defined(dbPostgres) +import Data.Bifunctor (bimap, second) +import Simplex.Messaging.Agent.Client (SubInfo (..), getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary) +#else +import Data.Bifunctor (bimap, first, second) +import qualified Data.ByteArray as BA +import qualified Database.SQLite.Simple as SQL +import Simplex.Chat.Archive +import Simplex.Messaging.Agent.Client (SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary) +import Simplex.Messaging.Agent.Store.Common (withConnection) +import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) +#endif + +_defaultNtfServers :: [NtfServer] +_defaultNtfServers = + [ -- "ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,5ex3mupcazy3zlky64ab27phjhijpemsiby33qzq3pliejipbtx5xgad.onion" + "ntf://KmpZNNXiVZJx_G2T7jRUmDFxWXM3OAnunz3uLT0tqAA=@ntf3.simplex.im,pxculznuryunjdvtvh6s6szmanyadumpbmvevgdpe4wk5c65unyt4yid.onion", + "ntf://CJ5o7X6fCxj2FFYRU2KuCo70y4jSqz7td2HYhLnXWbU=@ntf4.simplex.im,wtvuhdj26jwprmomnyfu5wfuq2hjkzfcc72u44vi6gdhrwxldt6xauad.onion" + ] + +maxImageSize :: Integer +maxImageSize = 261120 * 2 -- auto-receive on mobiles + +imageExtensions :: [String] +imageExtensions = [".jpg", ".jpeg", ".png", ".gif"] + +fixedImagePreview :: ImageData +fixedImagePreview = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg==" + +imageFilePrefix :: String +imageFilePrefix = "IMG_" + +voiceFilePrefix :: String +voiceFilePrefix = "voice_" + +videoFilePrefix :: String +videoFilePrefix = "video_" + +-- enableSndFiles has no effect when mainApp is True +startChatController :: Bool -> Bool -> CM' (Async ()) +startChatController mainApp enableSndFiles = do + asks smpAgent >>= liftIO . resumeAgentClient + unless mainApp $ chatWriteVar' subscriptionMode SMOnlyCreate + users <- fromRight [] <$> runExceptT (withFastStore' getUsers) + restoreCalls + s <- asks agentAsync + readTVarIO s >>= maybe (start s users) (pure . fst) + where + start s users = do + a1 <- async agentSubscriber + a2 <- + if mainApp + then Just <$> async (subscribeUsers False users) + else pure Nothing + atomically . writeTVar s $ Just (a1, a2) + if mainApp + then do + startXFTP xftpStartWorkers + void $ forkIO $ startFilesToReceive users + startCleanupManager + void $ forkIO $ mapM_ startExpireCIs users + else when enableSndFiles $ startXFTP xftpStartSndWorkers + pure a1 + startXFTP startWorkers = do + tmp <- readTVarIO =<< asks tempDirectory + runExceptT (withAgent $ \a -> startWorkers a tmp) >>= \case + Left e -> liftIO $ putStrLn $ "Error starting XFTP workers: " <> show e + Right _ -> pure () + startCleanupManager = do + cleanupAsync <- asks cleanupManagerAsync + readTVarIO cleanupAsync >>= \case + Nothing -> do + a <- Just <$> async (void $ runExceptT cleanupManager) + atomically $ writeTVar cleanupAsync a + _ -> pure () + startExpireCIs user = whenM shouldExpireChats $ do + startExpireCIThread user + setExpireCIFlag user True + where + shouldExpireChats = + fmap (fromRight False) $ runExceptT $ withStore' $ \db -> do + ttl <- getChatItemTTL db user + ttlCount <- getChatTTLCount db user + pure $ ttl > 0 || ttlCount > 0 + +subscribeUsers :: Bool -> [User] -> CM' () +subscribeUsers onlyNeeded users = do + let (us, us') = partition activeUser users + vr <- chatVersionRange' + subscribe vr us + subscribe vr us' + where + subscribe :: VersionRangeChat -> [User] -> CM' () + subscribe vr = mapM_ $ runExceptT . subscribeUserConnections vr onlyNeeded Agent.subscribeConnections + +startFilesToReceive :: [User] -> CM' () +startFilesToReceive users = do + let (us, us') = partition activeUser users + startReceive us + startReceive us' + where + startReceive :: [User] -> CM' () + startReceive = mapM_ $ runExceptT . startReceiveUserFiles + +startReceiveUserFiles :: User -> CM () +startReceiveUserFiles user = do + filesToReceive <- withStore' (`getRcvFilesToReceive` user) + forM_ filesToReceive $ \ft -> + flip catchChatError eToView $ + toView =<< receiveFileEvt' user ft False Nothing Nothing + +restoreCalls :: CM' () +restoreCalls = do + savedCalls <- fromRight [] <$> runExceptT (withFastStore' getCalls) + let callsMap = M.fromList $ map (\call@Call {contactId} -> (contactId, call)) savedCalls + calls <- asks currentCalls + atomically $ writeTVar calls callsMap + +stopChatController :: ChatController -> IO () +stopChatController ChatController {smpAgent, agentAsync = s, sndFiles, rcvFiles, expireCIFlags, remoteHostSessions, remoteCtrlSession} = do + readTVarIO remoteHostSessions >>= mapM_ (cancelRemoteHost False . snd) + atomically (stateTVar remoteCtrlSession (,Nothing)) >>= mapM_ (cancelRemoteCtrl False . snd) + disconnectAgentClient smpAgent + readTVarIO s >>= mapM_ (\(a1, a2) -> uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2) + closeFiles sndFiles + closeFiles rcvFiles + atomically $ do + keys <- M.keys <$> readTVar expireCIFlags + forM_ keys $ \k -> TM.insert k False expireCIFlags + writeTVar s Nothing + where + closeFiles :: TVar (Map Int64 Handle) -> IO () + closeFiles files = do + fs <- readTVarIO files + mapM_ hClose fs + atomically $ writeTVar files M.empty + +updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig +updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPortServers, tcpTimeout_, logTLSErrors} = + let cfg1 = maybe cfg (\smpProxyMode -> cfg {smpProxyMode}) smpProxyMode_ + cfg2 = maybe cfg1 (\smpProxyFallback -> cfg1 {smpProxyFallback}) smpProxyFallback_ + cfg3 = maybe cfg2 (\tcpTimeout -> cfg2 {tcpTimeout, tcpConnectTimeout = (tcpTimeout * 3) `div` 2}) tcpTimeout_ + in cfg3 {socksProxy, socksMode, hostMode, requiredHostMode, smpWebPortServers, logTLSErrors} + +useServers :: Foldable f => RandomAgentServers -> [(Text, ServerOperator)] -> f UserOperatorServers -> (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP)) +useServers as opDomains uss = + let smp' = useServerCfgs SPSMP as opDomains $ concatMap (servers' SPSMP) uss + xftp' = useServerCfgs SPXFTP as opDomains $ concatMap (servers' SPXFTP) uss + in (smp', xftp') + +execChatCommand :: Maybe RemoteHostId -> ByteString -> CM' (Either ChatError ChatResponse) +execChatCommand rh s = + case parseChatCommand s of + Left e -> pure $ chatCmdError e + Right cmd -> case rh of + Just rhId + | allowRemoteCommand cmd -> execRemoteCommand rhId cmd s + | otherwise -> pure $ Left $ ChatErrorRemoteHost (RHId rhId) $ RHELocalCommand + _ -> do + cc@ChatController {config = ChatConfig {chatHooks}} <- ask + case preCmdHook chatHooks of + Just hook -> liftIO (hook cc cmd) >>= either pure execChatCommand' + Nothing -> execChatCommand' cmd + +execChatCommand' :: ChatCommand -> CM' (Either ChatError ChatResponse) +execChatCommand' cmd = handleCommandError $ processChatCommand cmd + +execRemoteCommand :: RemoteHostId -> ChatCommand -> ByteString -> CM' (Either ChatError ChatResponse) +execRemoteCommand rhId cmd s = handleCommandError $ getRemoteHostClient rhId >>= \rh -> processRemoteCommand rhId rh cmd s + +handleCommandError :: CM ChatResponse -> CM' (Either ChatError ChatResponse) +handleCommandError a = runExceptT a `E.catches` ioErrors + where + ioErrors = + [ E.Handler $ \(e :: ExitCode) -> E.throwIO e, + E.Handler $ pure . Left . mkChatError + ] + +parseChatCommand :: ByteString -> Either String ChatCommand +parseChatCommand = A.parseOnly chatCommandP . B.dropWhileEnd isSpace + +-- | Chat API commands interpreted in context of a local zone +processChatCommand :: ChatCommand -> CM ChatResponse +processChatCommand cmd = + chatVersionRange >>= (`processChatCommand'` cmd) +{-# INLINE processChatCommand #-} + +processChatCommand' :: VersionRangeChat -> ChatCommand -> CM ChatResponse +processChatCommand' vr = \case + ShowActiveUser -> withUser' $ pure . CRActiveUser + CreateActiveUser NewUser {profile, pastTimestamp} -> do + forM_ profile $ \Profile {displayName} -> checkValidName displayName + p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile + u <- asks currentUser + users <- withFastStore' getUsers + forM_ users $ \User {localDisplayName = n, activeUser, viewPwdHash} -> + when (n == displayName) . throwChatError $ + if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""} + (uss, (smp', xftp')) <- chooseServers =<< readTVarIO u + auId <- withAgent $ \a -> createUser a smp' xftp' + ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure + user <- withFastStore $ \db -> do + user <- createUserRecordAt db (AgentUserId auId) p True ts + mapM_ (setUserServers db user ts) uss + createPresetContactCards db user `catchStoreError` \_ -> pure () + createNoteFolder db user + pure user + atomically . writeTVar u $ Just user + pure $ CRActiveUser user + where + createPresetContactCards :: DB.Connection -> User -> ExceptT StoreError IO () + createPresetContactCards db user = do + createContact db user simplexStatusContactProfile + createContact db user simplexTeamContactProfile + chooseServers :: Maybe User -> CM ([UpdatedUserOperatorServers], (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP))) + chooseServers user_ = do + as <- asks randomAgentServers + mapM (withFastStore . flip getUserServers >=> liftIO . groupByOperator) user_ >>= \case + Just uss -> do + let opDomains = operatorDomains $ mapMaybe operator' uss + uss' = map copyServers uss + pure $ (uss',) $ useServers as opDomains uss + Nothing -> do + ps <- asks randomPresetServers + uss <- presetUserServers <$> withFastStore' (\db -> getUpdateServerOperators db ps True) + let RandomAgentServers {smpServers = smp', xftpServers = xftp'} = as + pure (uss, (smp', xftp')) + copyServers :: UserOperatorServers -> UpdatedUserOperatorServers + copyServers UserOperatorServers {operator, smpServers, xftpServers} = + let new srv = AUS SDBNew srv {serverId = DBNewEntity} + in UpdatedUserOperatorServers {operator, smpServers = map new smpServers, xftpServers = map new xftpServers} + coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day) + day = 86400 + ListUsers -> CRUsersList <$> withFastStore' getUsersInfo + APISetActiveUser userId' viewPwd_ -> do + unlessM (lift chatStarted) $ throwChatError CEChatNotStarted + user_ <- chatReadVar currentUser + user' <- privateGetUser userId' + validateUserPassword_ user_ user' viewPwd_ + user'' <- withFastStore' (`setActiveUser` user') + chatWriteVar currentUser $ Just user'' + pure $ CRActiveUser user'' + SetActiveUser uName viewPwd_ -> do + tryChatError (withFastStore (`getUserIdByName` uName)) >>= \case + Left _ -> throwChatError CEUserUnknown + Right userId -> processChatCommand $ APISetActiveUser userId viewPwd_ + SetAllContactReceipts onOff -> withUser $ \_ -> withFastStore' (`updateAllContactReceipts` onOff) >> ok_ + APISetUserContactReceipts userId' settings -> withUser $ \user -> do + user' <- privateGetUser userId' + validateUserPassword user user' Nothing + withFastStore' $ \db -> updateUserContactReceipts db user' settings + ok user + SetUserContactReceipts settings -> withUser $ \User {userId} -> processChatCommand $ APISetUserContactReceipts userId settings + APISetUserGroupReceipts userId' settings -> withUser $ \user -> do + user' <- privateGetUser userId' + validateUserPassword user user' Nothing + withFastStore' $ \db -> updateUserGroupReceipts db user' settings + ok user + SetUserGroupReceipts settings -> withUser $ \User {userId} -> processChatCommand $ APISetUserGroupReceipts userId settings + APIHideUser userId' (UserPwd viewPwd) -> withUser $ \user -> do + user' <- privateGetUser userId' + case viewPwdHash user' of + Just _ -> throwChatError $ CEUserAlreadyHidden userId' + _ -> do + when (T.null viewPwd) $ throwChatError $ CEEmptyUserPassword userId' + users <- withFastStore' getUsers + unless (length (filter (isNothing . viewPwdHash) users) > 1) $ throwChatError $ CECantHideLastUser userId' + viewPwdHash' <- hashPassword + setUserPrivacy user user' {viewPwdHash = viewPwdHash', showNtfs = False} + where + hashPassword = do + salt <- drgRandomBytes 16 + let hash = B64UrlByteString $ C.sha512Hash $ encodeUtf8 viewPwd <> salt + pure $ Just UserPwdHash {hash, salt = B64UrlByteString salt} + APIUnhideUser userId' viewPwd@(UserPwd pwd) -> withUser $ \user -> do + user' <- privateGetUser userId' + case viewPwdHash user' of + Nothing -> throwChatError $ CEUserNotHidden userId' + _ -> do + when (T.null pwd) $ throwChatError $ CEEmptyUserPassword userId' + validateUserPassword user user' $ Just viewPwd + setUserPrivacy user user' {viewPwdHash = Nothing, showNtfs = True} + APIMuteUser userId' -> setUserNotifications userId' False + APIUnmuteUser userId' -> setUserNotifications userId' True + HideUser viewPwd -> withUser $ \User {userId} -> processChatCommand $ APIHideUser userId viewPwd + UnhideUser viewPwd -> withUser $ \User {userId} -> processChatCommand $ APIUnhideUser userId viewPwd + MuteUser -> withUser $ \User {userId} -> processChatCommand $ APIMuteUser userId + UnmuteUser -> withUser $ \User {userId} -> processChatCommand $ APIUnmuteUser userId + APIDeleteUser userId' delSMPQueues viewPwd_ -> withUser $ \user -> do + user' <- privateGetUser userId' + validateUserPassword user user' viewPwd_ + checkDeleteChatUser user' + withChatLock "deleteUser" . procCmd $ deleteChatUser user' delSMPQueues + DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \userId -> APIDeleteUser userId delSMPQueues viewPwd_ + StartChat {mainApp, enableSndFiles} -> withUser' $ \_ -> + asks agentAsync >>= readTVarIO >>= \case + Just _ -> pure CRChatRunning + _ -> checkStoreNotChanged . lift $ startChatController mainApp enableSndFiles $> CRChatStarted + CheckChatRunning -> maybe CRChatStopped (const CRChatRunning) <$> chatReadVar agentAsync + APIStopChat -> do + ask >>= liftIO . stopChatController + pure CRChatStopped + APIActivateChat restoreChat -> withUser $ \_ -> do + lift $ when restoreChat restoreCalls + lift $ withAgent' foregroundAgent + chatWriteVar chatActivated True + when restoreChat $ do + users <- withFastStore' getUsers + lift $ do + void . forkIO $ subscribeUsers True users + void . forkIO $ startFilesToReceive users + setAllExpireCIFlags True + ok_ + APISuspendChat t -> do + chatWriteVar chatActivated False + lift $ setAllExpireCIFlags False + stopRemoteCtrl + lift $ withAgent' (`suspendAgent` t) + ok_ + ResubscribeAllConnections -> withStore' getUsers >>= lift . subscribeUsers False >> ok_ + -- has to be called before StartChat + SetTempFolder tf -> do + createDirectoryIfMissing True tf + asks tempDirectory >>= atomically . (`writeTVar` Just tf) + ok_ + SetFilesFolder ff -> do + createDirectoryIfMissing True ff + asks filesFolder >>= atomically . (`writeTVar` Just ff) + ok_ + SetRemoteHostsFolder rf -> do + createDirectoryIfMissing True rf + chatWriteVar remoteHostsFolder $ Just rf + ok_ + -- has to be called before StartChat + APISetAppFilePaths cfg -> do + setFolder filesFolder $ appFilesFolder cfg + setFolder tempDirectory $ appTempFolder cfg + setFolder assetsDirectory $ appAssetsFolder cfg + mapM_ (setFolder remoteHostsFolder) $ appRemoteHostsFolder cfg + ok_ + where + setFolder sel f = do + createDirectoryIfMissing True f + chatWriteVar sel $ Just f + APISetEncryptLocalFiles on -> chatWriteVar encryptLocalFiles on >> ok_ + SetContactMergeEnabled onOff -> chatWriteVar contactMergeEnabled onOff >> ok_ +#if !defined(dbPostgres) + APIExportArchive cfg -> checkChatStopped $ CRArchiveExported <$> lift (exportArchive cfg) + ExportArchive -> do + ts <- liftIO getCurrentTime + let filePath = "simplex-chat." <> formatTime defaultTimeLocale "%FT%H%M%SZ" ts <> ".zip" + processChatCommand $ APIExportArchive $ ArchiveConfig filePath Nothing Nothing + APIImportArchive cfg -> checkChatStopped $ do + fileErrs <- lift $ importArchive cfg + setStoreChanged + pure $ CRArchiveImported fileErrs + APIDeleteStorage -> withStoreChanged deleteStorage + APIStorageEncryption cfg -> withStoreChanged $ sqlCipherExport cfg + TestStorageEncryption key -> sqlCipherTestKey key >> ok_ + SlowSQLQueries -> do + ChatController {chatStore, smpAgent} <- ask + chatQueries <- slowQueries chatStore + agentQueries <- slowQueries $ agentClientStore smpAgent + pure CRSlowSQLQueries {chatQueries, agentQueries} + where + slowQueries st = + liftIO $ + map (uncurry SlowSQLQuery . first SQL.fromQuery) + . sortOn (timeAvg . snd) + . M.assocs + <$> withConnection st (readTVarIO . DB.slow) +#endif + ExecChatStoreSQL query -> CRSQLResult <$> withStore' (`execSQL` query) + ExecAgentStoreSQL query -> CRSQLResult <$> withAgent (`execAgentStoreSQL` query) + APISaveAppSettings as -> withFastStore' (`saveAppSettings` as) >> ok_ + APIGetAppSettings platformDefaults -> CRAppSettings <$> withFastStore' (`getAppSettings` platformDefaults) + APIGetChatTags userId -> withUserId' userId $ \user -> do + tags <- withFastStore' (`getUserChatTags` user) + pure $ CRChatTags user tags + APIGetChats {userId, pendingConnections, pagination, query} -> withUserId' userId $ \user -> do + (errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db vr user pendingConnections pagination query) + unless (null errs) $ toView $ CEvtChatErrors (map ChatErrorStore errs) + pure $ CRApiChats user previews + APIGetChat (ChatRef cType cId) contentFilter pagination search -> withUser $ \user -> case cType of + -- TODO optimize queries calculating ChatStats, currently they're disabled + CTDirect -> do + when (isJust contentFilter) $ throwChatError $ CECommandError "content filter not supported" + (directChat, navInfo) <- withFastStore (\db -> getDirectChat db vr user cId pagination search) + pure $ CRApiChat user (AChat SCTDirect directChat) navInfo + CTGroup -> do + (groupChat, navInfo) <- withFastStore (\db -> getGroupChat db vr user cId contentFilter pagination search) + pure $ CRApiChat user (AChat SCTGroup groupChat) navInfo + CTLocal -> do + when (isJust contentFilter) $ throwChatError $ CECommandError "content filter not supported" + (localChat, navInfo) <- withFastStore (\db -> getLocalChat db user cId pagination search) + pure $ CRApiChat user (AChat SCTLocal localChat) navInfo + CTContactRequest -> throwCmdError "not implemented" + CTContactConnection -> throwCmdError "not supported" + APIGetChatItems pagination search -> withUser $ \user -> do + chatItems <- withFastStore $ \db -> getAllChatItems db vr user pagination search + pure $ CRChatItems user Nothing chatItems + APIGetChatItemInfo chatRef itemId -> withUser $ \user -> do + (aci@(AChatItem cType dir _ ci), versions) <- withFastStore $ \db -> + (,) <$> getAChatItem db vr user chatRef itemId <*> liftIO (getChatItemVersions db itemId) + let itemVersions = if null versions then maybeToList $ mkItemVersion ci else versions + memberDeliveryStatuses <- case (cType, dir) of + (SCTGroup, SMDSnd) -> L.nonEmpty <$> withFastStore' (`getGroupSndStatuses` itemId) + _ -> pure Nothing + forwardedFromChatItem <- getForwardedFromItem user ci + pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses, forwardedFromChatItem} + where + getForwardedFromItem :: User -> ChatItem c d -> CM (Maybe AChatItem) + getForwardedFromItem user ChatItem {meta = CIMeta {itemForwarded}} = case itemForwarded of + Just (CIFFContact _ _ (Just ctId) (Just fwdItemId)) -> + Just <$> withFastStore (\db -> getAChatItem db vr user (ChatRef CTDirect ctId) fwdItemId) + Just (CIFFGroup _ _ (Just gId) (Just fwdItemId)) -> + Just <$> withFastStore (\db -> getAChatItem db vr user (ChatRef CTGroup gId) fwdItemId) + _ -> pure Nothing + APISendMessages sendRef live itemTTL cms -> withUser $ \user -> mapM_ assertAllowedContent' cms >> case sendRef of + SRDirect chatId -> do + mapM_ assertNoMentions cms + withContactLock "sendMessage" chatId $ + sendContactContentMessages user chatId live itemTTL (L.map composedMessageReq cms) + SRGroup chatId directMemId_ -> + withGroupLock "sendMessage" chatId $ do + (gInfo, cmrs) <- withFastStore $ \db -> do + g <- getGroupInfo db vr user chatId + (g,) <$> mapM (composedMessageReqMentions db user g) cms + sendGroupContentMessages user gInfo directMemId_ live itemTTL cmrs + APICreateChatTag (ChatTagData emoji text) -> withUser $ \user -> withFastStore' $ \db -> do + _ <- createChatTag db user emoji text + CRChatTags user <$> getUserChatTags db user + APISetChatTags (ChatRef cType chatId) tagIds -> withUser $ \user -> case cType of + CTDirect -> withFastStore' $ \db -> do + updateDirectChatTags db chatId (maybe [] L.toList tagIds) + CRTagsUpdated user <$> getUserChatTags db user <*> getDirectChatTags db chatId + CTGroup -> withFastStore' $ \db -> do + updateGroupChatTags db chatId (maybe [] L.toList tagIds) + CRTagsUpdated user <$> getUserChatTags db user <*> getGroupChatTags db chatId + _ -> throwCmdError "not supported" + APIDeleteChatTag tagId -> withUser $ \user -> do + withFastStore' $ \db -> deleteChatTag db user tagId + ok user + APIUpdateChatTag tagId (ChatTagData emoji text) -> withUser $ \user -> do + withFastStore' $ \db -> updateChatTag db user tagId emoji text + ok user + APIReorderChatTags tagIds -> withUser $ \user -> do + withFastStore' $ \db -> reorderChatTags db user $ L.toList tagIds + ok user + APICreateChatItems folderId cms -> withUser $ \user -> do + forM_ cms $ \cm -> assertAllowedContent' cm >> assertNoMentions cm + createNoteFolderContentItems user folderId (L.map composedMessageReq cms) + APIReportMessage gId reportedItemId reportReason reportText -> withUser $ \user -> + withGroupLock "reportMessage" gId $ do + (gInfo, ms) <- + withFastStore $ \db -> do + gInfo <- getGroupInfo db vr user gId + (gInfo,) <$> liftIO (getGroupModerators db vr user gInfo) + let ms' = filter compatibleModerator ms + mc = MCReport reportText reportReason + cm = ComposedMessage {fileSource = Nothing, quotedItemId = Just reportedItemId, msgContent = mc, mentions = M.empty} + when (null ms') $ throwChatError $ CECommandError "no moderators support receiving reports" + let numFileInvs = length $ filter memberCurrent ms' + sendGroupContentMessages_ user gInfo Nothing ms' numFileInvs False Nothing [composedMessageReq cm] + where + compatibleModerator GroupMember {activeConn, memberChatVRange} = + maxVersion (maybe memberChatVRange peerChatVRange activeConn) >= contentReportsVersion + ReportMessage {groupName, contactName_, reportReason, reportedMessage} -> withUser $ \user -> do + gId <- withFastStore $ \db -> getGroupIdByName db user groupName + reportedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user gId contactName_ reportedMessage + processChatCommand $ APIReportMessage gId reportedItemId reportReason "" + APIUpdateChatItem (ChatRef cType chatId) itemId live (UpdatedMessage mc mentions) -> withUser $ \user -> assertAllowedContent mc >> case cType of + CTDirect -> withContactLock "updateChatItem" chatId $ do + unless (null mentions) $ throwChatError $ CECommandError "mentions are not supported in this chat" + ct@Contact {contactId} <- withFastStore $ \db -> getContact db vr user chatId + assertDirectAllowed user MDSnd ct XMsgUpdate_ + cci <- withFastStore $ \db -> getDirectCIWithReactions db user ct itemId + case cci of + CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do + case (ciContent, itemSharedMsgId, editable) of + (CISndMsgContent oldMC, Just itemSharedMId, True) -> do + let changed = mc /= oldMC + if changed || fromMaybe False itemLive + then do + (SndMessage {msgId}, _) <- sendDirectContactMessage user ct (XMsgUpdate itemSharedMId mc M.empty (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) + ci' <- withFastStore' $ \db -> do + currentTs <- liftIO getCurrentTime + when changed $ + addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) + let edited = itemLive /= Just True + updateDirectChatItem' db user contactId ci (CISndMsgContent mc) edited live Nothing $ Just msgId + startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' + pure $ CRChatItemUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci') + else pure $ CRChatItemNotChanged user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) + _ -> throwChatError CEInvalidChatItemUpdate + CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate + CTGroup -> withGroupLock "updateChatItem" chatId $ do + Group gInfo@GroupInfo {groupId, membership} ms <- withFastStore $ \db -> getGroup db vr user chatId + assertUserGroupRole gInfo GRAuthor + let (_, ft_) = msgContentTexts mc + if prohibitedSimplexLinks gInfo membership ft_ + then throwCmdError ("feature not allowed " <> T.unpack (groupFeatureNameText GFSimplexLinks)) + else do + cci <- withFastStore $ \db -> getGroupCIWithReactions db user gInfo itemId + case cci of + CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do + case (ciContent, itemSharedMsgId, editable) of + (CISndMsgContent oldMC, Just itemSharedMId, True) -> do + let changed = mc /= oldMC + if changed || fromMaybe False itemLive + then do + ciMentions <- withFastStore $ \db -> getCIMentions db user gInfo ft_ mentions + let mentions' = M.map (\CIMention {memberId} -> MsgMention {memberId}) ciMentions + -- TODO [knocking] send separately to pending approval member + SndMessage {msgId} <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc mentions' (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) + ci' <- withFastStore' $ \db -> do + currentTs <- liftIO getCurrentTime + when changed $ + addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) + let edited = itemLive /= Just True + ci' <- updateGroupChatItem db user groupId ci (CISndMsgContent mc) edited live $ Just msgId + updateGroupCIMentions db gInfo ci' ciMentions + startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' + pure $ CRChatItemUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci') + else pure $ CRChatItemNotChanged user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) + _ -> throwChatError CEInvalidChatItemUpdate + CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate + CTLocal -> do + unless (null mentions) $ throwChatError $ CECommandError "mentions are not supported in this chat" + (nf@NoteFolder {noteFolderId}, cci) <- withFastStore $ \db -> (,) <$> getNoteFolder db user chatId <*> getLocalChatItem db user chatId itemId + case cci of + CChatItem SMDSnd ci@ChatItem {content = CISndMsgContent oldMC} + | mc == oldMC -> pure $ CRChatItemNotChanged user (AChatItem SCTLocal SMDSnd (LocalChat nf) ci) + | otherwise -> withFastStore' $ \db -> do + currentTs <- getCurrentTime + addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) + ci' <- updateLocalChatItem' db user noteFolderId ci (CISndMsgContent mc) True + pure $ CRChatItemUpdated user (AChatItem SCTLocal SMDSnd (LocalChat nf) ci') + _ -> throwChatError CEInvalidChatItemUpdate + CTContactRequest -> throwCmdError "not supported" + CTContactConnection -> throwCmdError "not supported" + APIDeleteChatItem (ChatRef cType chatId) itemIds mode -> withUser $ \user -> case cType of + CTDirect -> withContactLock "deleteChatItem" chatId $ do + (ct, items) <- getCommandDirectChatItems user chatId itemIds + deletions <- case mode of + CIDMInternal -> deleteDirectCIs user ct items + CIDMInternalMark -> markDirectCIsDeleted user ct items =<< liftIO getCurrentTime + CIDMBroadcast -> do + assertDeletable items + assertDirectAllowed user MDSnd ct XMsgDel_ + let msgIds = itemsMsgIds items + events = map (\msgId -> XMsgDel msgId Nothing) msgIds + forM_ (L.nonEmpty events) $ \events' -> + sendDirectContactMessages user ct events' + if featureAllowed SCFFullDelete forUser ct + then deleteDirectCIs user ct items + else markDirectCIsDeleted user ct items =<< liftIO getCurrentTime + pure $ CRChatItemsDeleted user deletions True False + CTGroup -> withGroupLock "deleteChatItem" chatId $ do + (gInfo, items) <- getCommandGroupChatItems user chatId itemIds + deletions <- case mode of + CIDMInternal -> deleteGroupCIs user gInfo items Nothing =<< liftIO getCurrentTime + CIDMInternalMark -> markGroupCIsDeleted user gInfo items Nothing =<< liftIO getCurrentTime + CIDMBroadcast -> do + ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo + assertDeletable items + assertUserGroupRole gInfo GRObserver -- can still delete messages sent earlier + let msgIds = itemsMsgIds items + events = L.nonEmpty $ map (`XMsgDel` Nothing) msgIds + -- TODO [knocking] validate: only current members or only single pending approval member + mapM_ (sendGroupMessages user gInfo ms) events + delGroupChatItems user gInfo items False + pure $ CRChatItemsDeleted user deletions True False + CTLocal -> do + (nf, items) <- getCommandLocalChatItems user chatId itemIds + deleteLocalCIs user nf items True False + CTContactRequest -> throwCmdError "not supported" + CTContactConnection -> throwCmdError "not supported" + where + assertDeletable :: forall c. ChatTypeI c => [CChatItem c] -> CM () + assertDeletable items = do + currentTs <- liftIO getCurrentTime + unless (all (itemDeletable currentTs) items) $ throwChatError CEInvalidChatItemDelete + where + itemDeletable :: UTCTime -> CChatItem c -> Bool + itemDeletable currentTs (CChatItem msgDir ChatItem {meta = CIMeta {itemSharedMsgId, itemTs, itemDeleted}, content}) = + case msgDir of + -- We check with a 6 hour margin compared to CIMeta deletable to account for deletion on the border + SMDSnd -> isJust itemSharedMsgId && deletable' content itemDeleted itemTs (nominalDay + 6 * 3600) currentTs + SMDRcv -> False + itemsMsgIds :: [CChatItem c] -> [SharedMsgId] + itemsMsgIds = mapMaybe (\(CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId}}) -> itemSharedMsgId) + APIDeleteMemberChatItem gId itemIds -> withUser $ \user -> withGroupLock "deleteChatItem" gId $ do + (gInfo, items) <- getCommandGroupChatItems user gId itemIds + ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo + deletions <- delGroupChatItemsForMembers user gInfo ms items + pure $ CRChatItemsDeleted user deletions True False + APIArchiveReceivedReports gId -> withUser $ \user -> withFastStore $ \db -> do + g <- getGroupInfo db vr user gId + deleteTs <- liftIO getCurrentTime + ciIds <- liftIO $ markReceivedGroupReportsDeleted db user g deleteTs + pure $ CRGroupChatItemsDeleted user g ciIds True (Just $ membership g) + APIDeleteReceivedReports gId itemIds mode -> withUser $ \user -> withGroupLock "deleteReports" gId $ do + (gInfo, items) <- getCommandGroupChatItems user gId itemIds + unless (all isRcvReport items) $ throwChatError $ CECommandError "some items are not received reports" + deletions <- case mode of + CIDMInternal -> deleteGroupCIs user gInfo items Nothing =<< liftIO getCurrentTime + CIDMInternalMark -> markGroupCIsDeleted user gInfo items Nothing =<< liftIO getCurrentTime + CIDMBroadcast -> do + ms <- withFastStore' $ \db -> getGroupModerators db vr user gInfo + delGroupChatItemsForMembers user gInfo ms items + pure $ CRChatItemsDeleted user deletions True False + where + isRcvReport = \case + CChatItem _ ChatItem {content = CIRcvMsgContent (MCReport {})} -> True + _ -> False + APIChatItemReaction (ChatRef cType chatId) itemId add reaction -> withUser $ \user -> case cType of + CTDirect -> + withContactLock "chatItemReaction" chatId $ + withFastStore (\db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId) >>= \case + (ct, CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}}) -> do + unless (featureAllowed SCFReactions forUser ct) $ + throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions)) + unless (ciReactionAllowed ci) $ + throwChatError (CECommandError "reaction not allowed - chat item has no content") + rs <- withFastStore' $ \db -> getDirectReactions db ct itemSharedMId True + checkReactionAllowed rs + (SndMessage {msgId}, _) <- sendDirectContactMessage user ct $ XMsgReact itemSharedMId Nothing reaction add + createdAt <- liftIO getCurrentTime + reactions <- withFastStore' $ \db -> do + setDirectReaction db ct itemSharedMId True reaction add msgId createdAt + liftIO $ getDirectCIReactions db ct itemSharedMId + let ci' = CChatItem md ci {reactions} + r = ACIReaction SCTDirect SMDSnd (DirectChat ct) $ CIReaction CIDirectSnd ci' createdAt reaction + pure $ CRChatItemReaction user add r + _ -> throwChatError $ CECommandError "reaction not possible - no shared item ID" + CTGroup -> + withGroupLock "chatItemReaction" chatId $ do + (Group g@GroupInfo {membership} ms, CChatItem md ci) <- withFastStore $ \db -> do + gr@(Group g _) <- getGroup db vr user chatId + (gr,) <$> getGroupCIWithReactions db user g itemId + case ci of + ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}} -> do + unless (groupFeatureAllowed SGFReactions g) $ + throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions)) + unless (ciReactionAllowed ci) $ + throwChatError (CECommandError "reaction not allowed - chat item has no content") + let GroupMember {memberId = itemMemberId} = chatItemMember g ci + rs <- withFastStore' $ \db -> getGroupReactions db g membership itemMemberId itemSharedMId True + checkReactionAllowed rs + -- TODO [knocking] send separately to pending approval member + SndMessage {msgId} <- sendGroupMessage user g ms (XMsgReact itemSharedMId (Just itemMemberId) reaction add) + createdAt <- liftIO getCurrentTime + reactions <- withFastStore' $ \db -> do + setGroupReaction db g membership itemMemberId itemSharedMId True reaction add msgId createdAt + liftIO $ getGroupCIReactions db g itemMemberId itemSharedMId + let ci' = CChatItem md ci {reactions} + r = ACIReaction SCTGroup SMDSnd (GroupChat g) $ CIReaction CIGroupSnd ci' createdAt reaction + pure $ CRChatItemReaction user add r + _ -> throwChatError $ CECommandError "reaction not possible - no shared item ID" + CTLocal -> throwCmdError "not supported" + CTContactRequest -> throwCmdError "not supported" + CTContactConnection -> throwCmdError "not supported" + where + checkReactionAllowed rs = do + when ((reaction `elem` rs) == add) $ + throwChatError (CECommandError $ "reaction already " <> if add then "added" else "removed") + when (add && length rs >= maxMsgReactions) $ + throwChatError (CECommandError "too many reactions") + APIGetReactionMembers userId groupId itemId reaction -> withUserId userId $ \user -> do + memberReactions <- withStore $ \db -> do + CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}} <- getGroupChatItem db user groupId itemId + liftIO $ getReactionMembers db vr user groupId itemSharedMId reaction + pure $ CRReactionMembers user memberReactions + APIPlanForwardChatItems (ChatRef fromCType fromChatId) itemIds -> withUser $ \user -> case fromCType of + CTDirect -> planForward user . snd =<< getCommandDirectChatItems user fromChatId itemIds + CTGroup -> planForward user . snd =<< getCommandGroupChatItems user fromChatId itemIds + CTLocal -> planForward user . snd =<< getCommandLocalChatItems user fromChatId itemIds + CTContactRequest -> throwCmdError "not supported" + CTContactConnection -> throwCmdError "not supported" + where + planForward :: User -> [CChatItem c] -> CM ChatResponse + planForward user items = do + (itemIds', forwardErrors) <- unzip <$> mapM planItemForward items + let forwardConfirmation = case catMaybes forwardErrors of + [] -> Nothing + errs -> Just $ case mainErr of + FFENotAccepted _ -> FCFilesNotAccepted fileIds + FFEInProgress -> FCFilesInProgress filesCount + FFEMissing -> FCFilesMissing filesCount + FFEFailed -> FCFilesFailed filesCount + where + mainErr = minimum errs + fileIds = catMaybes $ map (\case FFENotAccepted ftId -> Just ftId; _ -> Nothing) errs + filesCount = length $ filter (mainErr ==) errs + pure CRForwardPlan {user, itemsCount = length itemIds, chatItemIds = catMaybes itemIds', forwardConfirmation} + where + planItemForward :: CChatItem c -> CM (Maybe ChatItemId, Maybe ForwardFileError) + planItemForward (CChatItem _ ci) = forwardMsgContent ci >>= maybe (pure (Nothing, Nothing)) (forwardContentPlan ci) + forwardContentPlan :: ChatItem c d -> MsgContent -> CM (Maybe ChatItemId, Maybe ForwardFileError) + forwardContentPlan ChatItem {file, meta = CIMeta {itemId}} mc = case file of + Nothing -> pure (Just itemId, Nothing) + Just CIFile {fileId, fileStatus, fileSource} -> case ciFileForwardError fileId fileStatus of + Just err -> pure $ itemIdWithoutFile err + Nothing -> case fileSource of + Just CryptoFile {filePath} -> do + exists <- doesFileExist =<< lift (toFSFilePath filePath) + pure $ if exists then (Just itemId, Nothing) else itemIdWithoutFile FFEMissing + Nothing -> pure $ itemIdWithoutFile FFEMissing + where + itemIdWithoutFile err = (if hasContent then Just itemId else Nothing, Just err) + hasContent = case mc of + MCText _ -> True + MCLink {} -> True + MCImage {} -> True + MCVideo {text} -> text /= "" + MCVoice {text} -> text /= "" + MCFile t -> t /= "" + MCReport {} -> True + MCUnknown {} -> True + APIForwardChatItems toChat@(ChatRef toCType toChatId) fromChat@(ChatRef fromCType fromChatId) itemIds itemTTL -> withUser $ \user -> case toCType of + CTDirect -> do + cmrs <- prepareForward user + case L.nonEmpty cmrs of + Just cmrs' -> + withContactLock "forwardChatItem, to contact" toChatId $ + sendContactContentMessages user toChatId False itemTTL cmrs' + Nothing -> pure $ CRNewChatItems user [] + CTGroup -> do + cmrs <- prepareForward user + case L.nonEmpty cmrs of + Just cmrs' -> + withGroupLock "forwardChatItem, to group" toChatId $ do + gInfo <- withFastStore $ \db -> getGroupInfo db vr user toChatId + sendGroupContentMessages user gInfo Nothing False itemTTL cmrs' + Nothing -> pure $ CRNewChatItems user [] + CTLocal -> do + cmrs <- prepareForward user + case L.nonEmpty cmrs of + Just cmrs' -> + createNoteFolderContentItems user toChatId cmrs' + Nothing -> pure $ CRNewChatItems user [] + CTContactRequest -> throwCmdError "not supported" + CTContactConnection -> throwCmdError "not supported" + where + prepareForward :: User -> CM [ComposedMessageReq] + prepareForward user = case fromCType of + CTDirect -> withContactLock "forwardChatItem, from contact" fromChatId $ do + (ct, items) <- getCommandDirectChatItems user fromChatId itemIds + catMaybes <$> mapM (\ci -> ciComposeMsgReq ct ci <$$> prepareMsgReq ci) items + where + ciComposeMsgReq :: Contact -> CChatItem 'CTDirect -> (MsgContent, Maybe CryptoFile) -> ComposedMessageReq + ciComposeMsgReq ct (CChatItem md ci) (mc', file) = + let itemId = chatItemId' ci + ciff = forwardCIFF ci $ Just (CIFFContact (forwardName ct) (toMsgDirection md) (Just fromChatId) (Just itemId)) + in (composedMessage file mc', ciff, msgContentTexts mc', M.empty) + where + forwardName :: Contact -> ContactName + forwardName Contact {profile = LocalProfile {displayName, localAlias}} + | localAlias /= "" = localAlias + | otherwise = displayName + CTGroup -> withGroupLock "forwardChatItem, from group" fromChatId $ do + (gInfo, items) <- getCommandGroupChatItems user fromChatId itemIds + catMaybes <$> mapM (\ci -> ciComposeMsgReq gInfo ci <$$> prepareMsgReq ci) items + where + ciComposeMsgReq :: GroupInfo -> CChatItem 'CTGroup -> (MsgContent, Maybe CryptoFile) -> ComposedMessageReq + ciComposeMsgReq gInfo (CChatItem md ci@ChatItem {mentions, formattedText}) (mc, file) = do + let itemId = chatItemId' ci + ciff = forwardCIFF ci $ Just (CIFFGroup (forwardName gInfo) (toMsgDirection md) (Just fromChatId) (Just itemId)) + -- updates text to reflect current mentioned member names + (mc', _, mentions') = updatedMentionNames mc formattedText mentions + -- only includes mentions when forwarding to the same group + ciMentions = if toChat == fromChat then mentions' else M.empty + -- no need to have mentions in ComposedMessage, they are in ciMentions + in (ComposedMessage file Nothing mc' M.empty, ciff, msgContentTexts mc', ciMentions) + where + forwardName :: GroupInfo -> ContactName + forwardName GroupInfo {groupProfile = GroupProfile {displayName}} = displayName + CTLocal -> do + (_, items) <- getCommandLocalChatItems user fromChatId itemIds + catMaybes <$> mapM (\ci -> ciComposeMsgReq ci <$$> prepareMsgReq ci) items + where + ciComposeMsgReq :: CChatItem 'CTLocal -> (MsgContent, Maybe CryptoFile) -> ComposedMessageReq + ciComposeMsgReq (CChatItem _ ci) (mc', file) = + let ciff = forwardCIFF ci Nothing + in (composedMessage file mc', ciff, msgContentTexts mc', M.empty) + CTContactRequest -> throwChatError $ CECommandError "not supported" + CTContactConnection -> throwChatError $ CECommandError "not supported" + where + prepareMsgReq :: CChatItem c -> CM (Maybe (MsgContent, Maybe CryptoFile)) + prepareMsgReq (CChatItem _ ci) = forwardMsgContent ci $>>= forwardContent ci + forwardCIFF :: ChatItem c d -> Maybe CIForwardedFrom -> Maybe CIForwardedFrom + forwardCIFF ChatItem {meta = CIMeta {itemForwarded}} ciff = case itemForwarded of + Nothing -> ciff + Just CIFFUnknown -> ciff + Just prevCIFF -> Just prevCIFF + forwardContent :: ChatItem c d -> MsgContent -> CM (Maybe (MsgContent, Maybe CryptoFile)) + forwardContent ChatItem {file} mc = case file of + Nothing -> pure $ Just (mc, Nothing) + Just CIFile {fileName, fileStatus, fileSource = Just fromCF@CryptoFile {filePath}} + | ciFileLoaded fileStatus -> + chatReadVar filesFolder >>= \case + Nothing -> + ifM (doesFileExist filePath) (pure $ Just (mc, Just fromCF)) (pure contentWithoutFile) + Just filesFolder -> do + let fsFromPath = filesFolder </> filePath + ifM + (doesFileExist fsFromPath) + ( do + newFileName <- liftIO $ maybe (pure fileName) (generateNewFileName fileName) $ mediaFilePrefix mc + fsNewPath <- liftIO $ filesFolder `uniqueCombine` newFileName + liftIO $ B.writeFile fsNewPath "" -- create empty file + encrypt <- chatReadVar encryptLocalFiles + cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing + let toCF = CryptoFile fsNewPath cfArgs + -- to keep forwarded file in case original is deleted + liftIOEither $ runExceptT $ withExceptT (ChatError . CEInternalError . show) $ copyCryptoFile (fromCF {filePath = fsFromPath} :: CryptoFile) toCF + pure $ Just (mc, Just (toCF {filePath = takeFileName fsNewPath} :: CryptoFile)) + ) + (pure contentWithoutFile) + _ -> pure contentWithoutFile + where + contentWithoutFile = case mc of + MCImage {} -> Just (mc, Nothing) + MCLink {} -> Just (mc, Nothing) + _ | contentText /= "" -> Just (MCText contentText, Nothing) + _ -> Nothing + contentText = msgContentText mc + copyCryptoFile :: CryptoFile -> CryptoFile -> ExceptT CF.FTCryptoError IO () + copyCryptoFile fromCF@CryptoFile {filePath = fsFromPath, cryptoArgs = fromArgs} toCF@CryptoFile {cryptoArgs = toArgs} = do + fromSizeFull <- getFileSize fsFromPath + let fromSize = fromSizeFull - maybe 0 (const $ toInteger C.authTagSize) fromArgs + CF.withFile fromCF ReadMode $ \fromH -> + CF.withFile toCF WriteMode $ \toH -> do + copyChunks fromH toH fromSize + forM_ fromArgs $ \_ -> CF.hGetTag fromH + forM_ toArgs $ \_ -> liftIO $ CF.hPutTag toH + where + copyChunks :: CF.CryptoFileHandle -> CF.CryptoFileHandle -> Integer -> ExceptT CF.FTCryptoError IO () + copyChunks r w size = do + let chSize = min size U.chunkSize + chSize' = fromIntegral chSize + size' = size - chSize + ch <- liftIO $ CF.hGet r chSize' + when (B.length ch /= chSize') $ throwError $ CF.FTCEFileIOError "encrypting file: unexpected EOF" + liftIO . CF.hPut w $ LB.fromStrict ch + when (size' > 0) $ copyChunks r w size' + mediaFilePrefix :: MsgContent -> Maybe FilePath + mediaFilePrefix = \case + MCImage {} -> Just imageFilePrefix + MCVoice {} -> Just voiceFilePrefix + MCVideo {} -> Just videoFilePrefix + _ -> Nothing + generateNewFileName fileName prefix = do + currentDate <- liftIO getCurrentTime + let formattedDate = formatTime defaultTimeLocale "%Y%m%d_%H%M%S" currentDate + let ext = takeExtension fileName + pure $ prefix <> formattedDate <> ext + APIUserRead userId -> withUserId userId $ \user -> withFastStore' (`setUserChatsRead` user) >> ok user + UserRead -> withUser $ \User {userId} -> processChatCommand $ APIUserRead userId + APIChatRead chatRef@(ChatRef cType chatId) -> withUser $ \_ -> case cType of + CTDirect -> do + user <- withFastStore $ \db -> getUserByContactId db chatId + ts <- liftIO getCurrentTime + timedItems <- withFastStore' $ \db -> do + timedItems <- getDirectUnreadTimedItems db user chatId + updateDirectChatItemsRead db user chatId + setDirectChatItemsDeleteAt db user chatId timedItems ts + forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt + ok user + CTGroup -> do + user <- withFastStore $ \db -> getUserByGroupId db chatId + ts <- liftIO getCurrentTime + timedItems <- withFastStore' $ \db -> do + timedItems <- getGroupUnreadTimedItems db user chatId + updateGroupChatItemsRead db user chatId + setGroupChatItemsDeleteAt db user chatId timedItems ts + forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt + ok user + CTLocal -> do + user <- withFastStore $ \db -> getUserByNoteFolderId db chatId + withFastStore' $ \db -> updateLocalChatItemsRead db user chatId + ok user + CTContactRequest -> throwCmdError "not supported" + CTContactConnection -> throwCmdError "not supported" + APIChatItemsRead chatRef@(ChatRef cType chatId) itemIds -> withUser $ \_ -> case cType of + CTDirect -> do + user <- withFastStore $ \db -> getUserByContactId db chatId + timedItems <- withFastStore' $ \db -> do + timedItems <- updateDirectChatItemsReadList db user chatId itemIds + setDirectChatItemsDeleteAt db user chatId timedItems =<< getCurrentTime + forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt + ok user + CTGroup -> do + user <- withFastStore $ \db -> getUserByGroupId db chatId + timedItems <- withFastStore' $ \db -> do + timedItems <- updateGroupChatItemsReadList db user chatId itemIds + setGroupChatItemsDeleteAt db user chatId timedItems =<< getCurrentTime + forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt + ok user + CTLocal -> throwCmdError "not supported" + CTContactRequest -> throwCmdError "not supported" + CTContactConnection -> throwCmdError "not supported" + APIChatUnread (ChatRef cType chatId) unreadChat -> withUser $ \user -> case cType of + CTDirect -> do + withFastStore $ \db -> do + ct <- getContact db vr user chatId + liftIO $ updateContactUnreadChat db user ct unreadChat + ok user + CTGroup -> do + withFastStore $ \db -> do + Group {groupInfo} <- getGroup db vr user chatId + liftIO $ updateGroupUnreadChat db user groupInfo unreadChat + ok user + CTLocal -> do + withFastStore $ \db -> do + nf <- getNoteFolder db user chatId + liftIO $ updateNoteFolderUnreadChat db user nf unreadChat + ok user + _ -> throwCmdError "not supported" + APIDeleteChat cRef@(ChatRef cType chatId) cdm -> withUser $ \user@User {userId} -> case cType of + CTDirect -> do + ct <- withFastStore $ \db -> getContact db vr user chatId + filesInfo <- withFastStore' $ \db -> getContactFileInfo db user ct + withContactLock "deleteChat direct" chatId . procCmd $ + case cdm of + CDMFull notify -> do + deleteCIFiles user filesInfo + sendDelDeleteConns ct notify + -- functions below are called in separate transactions to prevent crashes on android + -- (possibly, race condition on integrity check?) + withFastStore' $ \db -> do + deleteContactConnections db user ct + deleteContactFiles db user ct + withFastStore $ \db -> deleteContact db user ct + pure $ CRContactDeleted user ct + CDMEntity notify -> do + cancelFilesInProgress user filesInfo + sendDelDeleteConns ct notify + ct' <- withFastStore $ \db -> do + liftIO $ deleteContactConnections db user ct + liftIO $ void $ updateContactStatus db user ct CSDeletedByUser + getContact db vr user chatId + pure $ CRContactDeleted user ct' + CDMMessages -> do + void $ processChatCommand $ APIClearChat cRef + withFastStore' $ \db -> setContactChatDeleted db user ct True + pure $ CRContactDeleted user ct {chatDeleted = True} + where + sendDelDeleteConns ct notify = do + let doSendDel = contactReady ct && contactActive ct && notify + when doSendDel $ void (sendDirectContactMessage user ct XDirectDel) `catchChatError` const (pure ()) + contactConnIds <- map aConnId <$> withFastStore' (\db -> getContactConnections db vr userId ct) + deleteAgentConnectionsAsync' contactConnIds doSendDel + CTContactConnection -> withConnectionLock "deleteChat contactConnection" chatId . procCmd $ do + conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withFastStore $ \db -> getPendingContactConnection db userId chatId + deleteAgentConnectionAsync acId + withFastStore' $ \db -> deletePendingContactConnection db userId chatId + pure $ CRContactConnectionDeleted user conn + CTGroup -> do + Group gInfo@GroupInfo {membership} members <- withFastStore $ \db -> getGroup db vr user chatId + let GroupMember {memberRole = membershipMemRole} = membership + let isOwner = membershipMemRole == GROwner + canDelete = isOwner || not (memberCurrent membership) + unless canDelete $ throwChatError $ CEGroupUserRole gInfo GROwner + filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo + withGroupLock "deleteChat group" chatId . procCmd $ do + deleteCIFiles user filesInfo + let doSendDel = memberActive membership && isOwner + -- TODO [knocking] send to pending approval members (move `memberCurrent` filter from sendGroupMessages_ to call sites) + when doSendDel . void $ sendGroupMessage' user gInfo members XGrpDel + deleteGroupLinkIfExists user gInfo + deleteMembersConnections' user members doSendDel + updateCIGroupInvitationStatus user gInfo CIGISRejected `catchChatError` \_ -> pure () + withFastStore' $ \db -> deleteGroupChatItems db user gInfo + withFastStore' $ \db -> cleanupHostGroupLinkConn db user gInfo + withFastStore' $ \db -> deleteGroupMembers db user gInfo + withFastStore' $ \db -> deleteGroup db user gInfo + pure $ CRGroupDeletedUser user gInfo + CTLocal -> throwCmdError "not supported" + CTContactRequest -> throwCmdError "not supported" + APIClearChat (ChatRef cType chatId) -> withUser $ \user@User {userId} -> case cType of + CTDirect -> do + ct <- withFastStore $ \db -> getContact db vr user chatId + filesInfo <- withFastStore' $ \db -> getContactFileInfo db user ct + deleteCIFiles user filesInfo + withFastStore' $ \db -> deleteContactCIs db user ct + pure $ CRChatCleared user (AChatInfo SCTDirect $ DirectChat ct) + CTGroup -> do + gInfo <- withFastStore $ \db -> getGroupInfo db vr user chatId + filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo + deleteCIFiles user filesInfo + withFastStore' $ \db -> deleteGroupChatItemsMessages db user gInfo + membersToDelete <- withFastStore' $ \db -> getGroupMembersForExpiration db vr user gInfo + forM_ membersToDelete $ \m -> withFastStore' $ \db -> deleteGroupMember db user m + pure $ CRChatCleared user (AChatInfo SCTGroup $ GroupChat gInfo) + CTLocal -> do + nf <- withFastStore $ \db -> getNoteFolder db user chatId + filesInfo <- withFastStore' $ \db -> getNoteFolderFileInfo db user nf + deleteFilesLocally filesInfo + withFastStore' $ \db -> deleteNoteFolderFiles db userId nf + withFastStore' $ \db -> deleteNoteFolderCIs db user nf + pure $ CRChatCleared user (AChatInfo SCTLocal $ LocalChat nf) + CTContactConnection -> throwCmdError "not supported" + CTContactRequest -> throwCmdError "not supported" + APIAcceptContact incognito connReqId -> withUser $ \_ -> do + userContactLinkId <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId + withUserContactLock "acceptContact" userContactLinkId $ do + (user@User {userId}, cReq) <- withFastStore $ \db -> getContactRequest' db connReqId + (ct, conn@Connection {connId}, sqSecured) <- acceptContactRequest user cReq incognito + ucl <- withFastStore $ \db -> getUserContactLinkById db userId userContactLinkId + let contactUsed = (\(_, gLinkInfo_) -> isNothing gLinkInfo_) ucl + ct' <- withStore' $ \db -> do + deleteContactRequestRec db user cReq + updateContactAccepted db user ct contactUsed + conn' <- + if sqSecured + then conn {connStatus = ConnSndReady} <$ updateConnectionStatusFromTo db connId ConnNew ConnSndReady + else pure conn + pure ct {contactUsed, activeConn = Just conn'} + pure $ CRAcceptingContactRequest user ct' + APIRejectContact connReqId -> withUser $ \user -> do + userContactLinkId <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId + withUserContactLock "rejectContact" userContactLinkId $ do + cReq@UserContactRequest {agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId} <- + withFastStore $ \db -> + getContactRequest db user connReqId + `storeFinally` liftIO (deleteContactRequest db user connReqId) + withAgent $ \a -> rejectContact a connId invId + pure $ CRContactRequestRejected user cReq + APISendCallInvitation contactId callType -> withUser $ \user -> do + -- party initiating call + ct <- withFastStore $ \db -> getContact db vr user contactId + assertDirectAllowed user MDSnd ct XCallInv_ + if featureAllowed SCFCalls forUser ct + then do + calls <- asks currentCalls + withContactLock "sendCallInvitation" contactId $ do + g <- asks random + callId <- atomically $ CallId <$> C.randomBytes 16 g + callUUID <- UUID.toText <$> liftIO V4.nextRandom + dhKeyPair <- atomically $ if encryptedCall callType then Just <$> C.generateKeyPair g else pure Nothing + let invitation = CallInvitation {callType, callDhPubKey = fst <$> dhKeyPair} + callState = CallInvitationSent {localCallType = callType, localDhPrivKey = snd <$> dhKeyPair} + (msg, _) <- sendDirectContactMessage user ct (XCallInv callId invitation) + ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndCall CISCallPending 0) + let call' = Call {contactId, callId, callUUID, chatItemId = chatItemId' ci, callState, callTs = chatItemTs' ci} + call_ <- atomically $ TM.lookupInsert contactId call' calls + forM_ call_ $ \call -> updateCallItemStatus user ct call WCSDisconnected Nothing + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] + ok user + else throwCmdError ("feature not allowed " <> T.unpack (chatFeatureNameText CFCalls)) + SendCallInvitation cName callType -> withUser $ \user -> do + contactId <- withFastStore $ \db -> getContactIdByName db user cName + processChatCommand $ APISendCallInvitation contactId callType + APIRejectCall contactId -> + -- party accepting call + withCurrentCall contactId $ \user ct Call {chatItemId, callState} -> case callState of + CallInvitationReceived {} -> do + let aciContent = ACIContent SMDRcv $ CIRcvCall CISCallRejected 0 + withFastStore' $ \db -> setDirectChatItemRead db user contactId chatItemId + timed_ <- contactCITimed ct + updateDirectChatItemView user ct chatItemId aciContent False False timed_ Nothing + forM_ (timed_ >>= timedDeleteAt') $ + startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId) + pure Nothing + _ -> throwChatError . CECallState $ callStateTag callState + APISendCallOffer contactId WebRTCCallOffer {callType, rtcSession} -> + -- party accepting call + withCurrentCall contactId $ \user ct call@Call {callId, chatItemId, callState} -> case callState of + CallInvitationReceived {peerCallType, localDhPubKey, sharedKey} -> do + let callDhPubKey = if encryptedCall callType then localDhPubKey else Nothing + offer = CallOffer {callType, rtcSession, callDhPubKey} + callState' = CallOfferSent {localCallType = callType, peerCallType, localCallSession = rtcSession, sharedKey} + aciContent = ACIContent SMDRcv $ CIRcvCall CISCallAccepted 0 + (SndMessage {msgId}, _) <- sendDirectContactMessage user ct (XCallOffer callId offer) + withFastStore' $ \db -> setDirectChatItemRead db user contactId chatItemId + updateDirectChatItemView user ct chatItemId aciContent False False Nothing $ Just msgId + pure $ Just call {callState = callState'} + _ -> throwChatError . CECallState $ callStateTag callState + APISendCallAnswer contactId rtcSession -> + -- party initiating call + withCurrentCall contactId $ \user ct call@Call {callId, chatItemId, callState} -> case callState of + CallOfferReceived {localCallType, peerCallType, peerCallSession, sharedKey} -> do + let callState' = CallNegotiated {localCallType, peerCallType, localCallSession = rtcSession, peerCallSession, sharedKey} + aciContent = ACIContent SMDSnd $ CISndCall CISCallNegotiated 0 + (SndMessage {msgId}, _) <- sendDirectContactMessage user ct (XCallAnswer callId CallAnswer {rtcSession}) + updateDirectChatItemView user ct chatItemId aciContent False False Nothing $ Just msgId + pure $ Just call {callState = callState'} + _ -> throwChatError . CECallState $ callStateTag callState + APISendCallExtraInfo contactId rtcExtraInfo -> + -- any call party + withCurrentCall contactId $ \user ct call@Call {callId, callState} -> case callState of + CallOfferSent {localCallType, peerCallType, localCallSession, sharedKey} -> do + -- TODO update the list of ice servers in localCallSession + void . sendDirectContactMessage user ct $ XCallExtra callId CallExtraInfo {rtcExtraInfo} + let callState' = CallOfferSent {localCallType, peerCallType, localCallSession, sharedKey} + pure $ Just call {callState = callState'} + CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession, sharedKey} -> do + -- TODO update the list of ice servers in localCallSession + void . sendDirectContactMessage user ct $ XCallExtra callId CallExtraInfo {rtcExtraInfo} + let callState' = CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession, sharedKey} + pure $ Just call {callState = callState'} + _ -> throwChatError . CECallState $ callStateTag callState + APIEndCall contactId -> + -- any call party + withCurrentCall contactId $ \user ct call@Call {callId} -> do + (SndMessage {msgId}, _) <- sendDirectContactMessage user ct (XCallEnd callId) + updateCallItemStatus user ct call WCSDisconnected $ Just msgId + pure Nothing + APIGetCallInvitations -> withUser' $ \_ -> lift $ do + calls <- asks currentCalls >>= readTVarIO + let invs = mapMaybe callInvitation $ M.elems calls + rcvCallInvitations <- rights <$> mapM rcvCallInvitation invs + pure $ CRCallInvitations rcvCallInvitations + where + callInvitation Call {contactId, callUUID, callState, callTs} = case callState of + CallInvitationReceived {peerCallType, sharedKey} -> Just (contactId, callUUID, callTs, peerCallType, sharedKey) + _ -> Nothing + rcvCallInvitation (contactId, callUUID, callTs, peerCallType, sharedKey) = runExceptT . withFastStore $ \db -> do + user <- getUserByContactId db contactId + contact <- getContact db vr user contactId + pure RcvCallInvitation {user, contact, callType = peerCallType, sharedKey, callUUID, callTs} + APIGetNetworkStatuses -> withUser $ \_ -> + CRNetworkStatuses Nothing . map (uncurry ConnNetworkStatus) . M.toList <$> chatReadVar connNetworkStatuses + APICallStatus contactId receivedStatus -> + withCurrentCall contactId $ \user ct call -> + updateCallItemStatus user ct call receivedStatus Nothing $> Just call + APIUpdateProfile userId profile -> withUserId userId (`updateProfile` profile) + APISetContactPrefs contactId prefs' -> withUser $ \user -> do + ct <- withFastStore $ \db -> getContact db vr user contactId + updateContactPrefs user ct prefs' + APISetContactAlias contactId localAlias -> withUser $ \user@User {userId} -> do + ct' <- withFastStore $ \db -> do + ct <- getContact db vr user contactId + liftIO $ updateContactAlias db userId ct localAlias + pure $ CRContactAliasUpdated user ct' + APISetGroupAlias gId localAlias -> withUser $ \user@User {userId} -> do + gInfo' <- withFastStore $ \db -> do + gInfo <- getGroupInfo db vr user gId + liftIO $ updateGroupAlias db userId gInfo localAlias + pure $ CRGroupAliasUpdated user gInfo' + APISetConnectionAlias connId localAlias -> withUser $ \user@User {userId} -> do + conn' <- withFastStore $ \db -> do + conn <- getPendingContactConnection db userId connId + liftIO $ updateContactConnectionAlias db userId conn localAlias + pure $ CRConnectionAliasUpdated user conn' + APISetUserUIThemes uId uiThemes -> withUser $ \user@User {userId} -> do + user'@User {userId = uId'} <- withFastStore $ \db -> do + user' <- getUser db uId + liftIO $ setUserUIThemes db user uiThemes + pure user' + when (userId == uId') $ chatWriteVar currentUser $ Just (user :: User) {uiThemes} + ok user' + APISetChatUIThemes (ChatRef cType chatId) uiThemes -> withUser $ \user -> case cType of + CTDirect -> do + withFastStore $ \db -> do + ct <- getContact db vr user chatId + liftIO $ setContactUIThemes db user ct uiThemes + ok user + CTGroup -> do + withFastStore $ \db -> do + g <- getGroupInfo db vr user chatId + liftIO $ setGroupUIThemes db user g uiThemes + ok user + _ -> throwCmdError "not supported" + APIGetNtfToken -> withUser' $ \_ -> crNtfToken <$> withAgent getNtfToken + APIRegisterToken token mode -> withUser $ \_ -> + CRNtfTokenStatus <$> withAgent (\a -> registerNtfToken a token mode) + APIVerifyToken token nonce code -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a token nonce code) >> ok_ + APICheckToken token -> withUser $ \_ -> + CRNtfTokenStatus <$> withAgent (`checkNtfToken` token) + APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) >> ok_ + APIGetNtfConns nonce encNtfInfo -> withUser $ \_ -> do + ntfInfos <- withAgent $ \a -> getNotificationConns a nonce encNtfInfo + (errs, ntfMsgs) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (getMsgConn db) (L.toList ntfInfos)) + unless (null errs) $ toView $ CEvtChatErrors errs + pure $ CRNtfConns $ catMaybes ntfMsgs + where + getMsgConn :: DB.Connection -> NotificationInfo -> IO (Maybe NtfConn) + getMsgConn db NotificationInfo {ntfConnId, ntfDbQueueId, ntfMsgMeta = nMsgMeta} = do + let agentConnId = AgentConnId ntfConnId + mkNtfConn user connEntity = NtfConn {user, agentConnId, agentDbQueueId = ntfDbQueueId, connEntity, expectedMsg_ = expectedMsgInfo <$> nMsgMeta} + getUserByAConnId db agentConnId + $>>= \user -> fmap (mkNtfConn user) . eitherToMaybe <$> runExceptT (getConnectionEntity db vr user agentConnId) + APIGetConnNtfMessages connMsgs -> withUser $ \_ -> do + msgs <- lift $ withAgent' (`getConnectionMessages` connMsgs) + let ntfMsgs = L.map receivedMsgInfo msgs + pure $ CRConnNtfMessages ntfMsgs + GetUserProtoServers (AProtocolType p) -> withUser $ \user -> withServerProtocol p $ do + srvs <- withFastStore (`getUserServers` user) + liftIO $ CRUserServers user <$> groupByOperator (protocolServers p srvs) + SetUserProtoServers (AProtocolType (p :: SProtocolType p)) srvs -> withUser $ \user@User {userId} -> withServerProtocol p $ do + userServers_ <- liftIO . groupByOperator =<< withFastStore (`getUserServers` user) + case L.nonEmpty userServers_ of + Nothing -> throwChatError $ CECommandError "no servers" + Just userServers -> case srvs of + [] -> throwChatError $ CECommandError "no servers" + _ -> do + srvs' <- mapM aUserServer srvs + processChatCommand $ APISetUserServers userId $ L.map (updatedServers p srvs') userServers + where + aUserServer :: AProtoServerWithAuth -> CM (AUserServer p) + aUserServer (AProtoServerWithAuth p' srv) = case testEquality p p' of + Just Refl -> pure $ AUS SDBNew $ newUserServer srv + Nothing -> throwChatError $ CECommandError $ "incorrect server protocol: " <> B.unpack (strEncode srv) + APITestProtoServer userId srv@(AProtoServerWithAuth _ server) -> withUserId userId $ \user -> + lift $ CRServerTestResult user srv <$> withAgent' (\a -> testProtocolServer a (aUserId user) server) + TestProtoServer srv -> withUser $ \User {userId} -> + processChatCommand $ APITestProtoServer userId srv + APIGetServerOperators -> CRServerOperatorConditions <$> withFastStore getServerOperators + APISetServerOperators operators -> do + as <- asks randomAgentServers + (opsConds, srvs) <- withFastStore $ \db -> do + liftIO $ setServerOperators db operators + opsConds <- getServerOperators db + let ops = serverOperators opsConds + ops' = map Just ops <> [Nothing] + opDomains = operatorDomains ops + liftIO $ fmap (opsConds,) . mapM (getServers db as ops' opDomains) =<< getUsers db + lift $ withAgent' $ \a -> forM_ srvs $ \(auId, (smp', xftp')) -> do + setProtocolServers a auId smp' + setProtocolServers a auId xftp' + pure $ CRServerOperatorConditions opsConds + where + getServers :: DB.Connection -> RandomAgentServers -> [Maybe ServerOperator] -> [(Text, ServerOperator)] -> User -> IO (UserId, (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP))) + getServers db as ops opDomains user = do + smpSrvs <- getProtocolServers db SPSMP user + xftpSrvs <- getProtocolServers db SPXFTP user + uss <- groupByOperator (ops, smpSrvs, xftpSrvs) + pure $ (aUserId user,) $ useServers as opDomains uss + SetServerOperators operatorsRoles -> do + ops <- serverOperators <$> withFastStore getServerOperators + ops' <- mapM (updateOp ops) operatorsRoles + processChatCommand $ APISetServerOperators ops' + where + updateOp :: [ServerOperator] -> ServerOperatorRoles -> CM ServerOperator + updateOp ops r = + case find (\ServerOperator {operatorId = DBEntityId opId} -> operatorId' r == opId) ops of + Just op -> pure op {enabled = enabled' r, smpRoles = smpRoles' r, xftpRoles = xftpRoles' r} + Nothing -> throwError $ ChatErrorStore $ SEOperatorNotFound $ operatorId' r + APIGetUserServers userId -> withUserId userId $ \user -> withFastStore $ \db -> do + CRUserServers user <$> (liftIO . groupByOperator =<< getUserServers db user) + APISetUserServers userId userServers -> withUserId userId $ \user -> do + errors <- validateAllUsersServers userId $ L.toList userServers + unless (null errors) $ throwChatError (CECommandError $ "user servers validation error(s): " <> show errors) + uss <- withFastStore $ \db -> do + ts <- liftIO getCurrentTime + mapM (setUserServers db user ts) userServers + as <- asks randomAgentServers + lift $ withAgent' $ \a -> do + let auId = aUserId user + opDomains = operatorDomains $ mapMaybe operator' $ L.toList uss + (smp', xftp') = useServers as opDomains uss + setProtocolServers a auId smp' + setProtocolServers a auId xftp' + ok_ + APIValidateServers userId userServers -> withUserId userId $ \user -> + CRUserServersValidation user <$> validateAllUsersServers userId userServers + APIGetUsageConditions -> do + (usageConditions, acceptedConditions) <- withFastStore $ \db -> do + usageConditions <- getCurrentUsageConditions db + acceptedConditions <- liftIO $ getLatestAcceptedConditions db + pure (usageConditions, acceptedConditions) + -- TODO if db commit is different from source commit, conditionsText should be nothing in response + pure + CRUsageConditions + { usageConditions, + conditionsText = usageConditionsText, + acceptedConditions + } + APISetConditionsNotified condId -> do + currentTs <- liftIO getCurrentTime + withFastStore' $ \db -> setConditionsNotified db condId currentTs + ok_ + APIAcceptConditions condId opIds -> withFastStore $ \db -> do + currentTs <- liftIO getCurrentTime + acceptConditions db condId opIds currentTs + CRServerOperatorConditions <$> getServerOperators db + APISetChatTTL userId (ChatRef cType chatId) newTTL_ -> + withUserId userId $ \user -> checkStoreNotChanged $ withChatLock "setChatTTL" $ do + (oldTTL_, globalTTL, ttlCount) <- withStore' $ \db -> + (,,) <$> getSetChatTTL db <*> getChatItemTTL db user <*> getChatTTLCount db user + let newTTL = fromMaybe globalTTL newTTL_ + oldTTL = fromMaybe globalTTL oldTTL_ + when (newTTL > 0 && (newTTL < oldTTL || oldTTL == 0)) $ do + lift $ setExpireCIFlag user False + expireChat user globalTTL `catchChatError` eToView + lift $ setChatItemsExpiration user globalTTL ttlCount + ok user + where + getSetChatTTL db = case cType of + CTDirect -> getDirectChatTTL db chatId <* setDirectChatTTL db chatId newTTL_ + CTGroup -> getGroupChatTTL db chatId <* setGroupChatTTL db chatId newTTL_ + _ -> pure Nothing + expireChat user globalTTL = do + currentTs <- liftIO getCurrentTime + case cType of + CTDirect -> expireContactChatItems user vr globalTTL chatId + CTGroup -> + let createdAtCutoff = addUTCTime (-43200 :: NominalDiffTime) currentTs + in expireGroupChatItems user vr globalTTL createdAtCutoff chatId + _ -> throwChatError $ CECommandError "not supported" + SetChatTTL chatName newTTL -> withUser' $ \user@User {userId} -> do + chatRef <- getChatRef user chatName + processChatCommand $ APISetChatTTL userId chatRef newTTL + GetChatTTL chatName -> withUser' $ \user -> do + ChatRef cType chatId <- getChatRef user chatName + ttl <- case cType of + CTDirect -> withFastStore' (`getDirectChatTTL` chatId) + CTGroup -> withFastStore' (`getGroupChatTTL` chatId) + _ -> throwChatError $ CECommandError "not supported" + pure $ CRChatItemTTL user ttl + APISetChatItemTTL userId newTTL -> withUserId userId $ \user -> + checkStoreNotChanged $ + withChatLock "setChatItemTTL" $ do + (oldTTL, ttlCount) <- withFastStore' $ \db -> + (,) <$> getChatItemTTL db user <* setChatItemTTL db user newTTL <*> getChatTTLCount db user + when (newTTL > 0 && (newTTL < oldTTL || oldTTL == 0)) $ do + lift $ setExpireCIFlag user False + expireChatItems user newTTL True + lift $ setChatItemsExpiration user newTTL ttlCount + ok user + SetChatItemTTL newTTL_ -> withUser' $ \User {userId} -> do + processChatCommand $ APISetChatItemTTL userId newTTL_ + APIGetChatItemTTL userId -> withUserId' userId $ \user -> do + ttl <- withFastStore' (`getChatItemTTL` user) + pure $ CRChatItemTTL user (Just ttl) + GetChatItemTTL -> withUser' $ \User {userId} -> do + processChatCommand $ APIGetChatItemTTL userId + APISetNetworkConfig cfg -> withUser' $ \_ -> lift (withAgent' (`setNetworkConfig` cfg)) >> ok_ + APIGetNetworkConfig -> withUser' $ \_ -> + CRNetworkConfig <$> lift getNetworkConfig + SetNetworkConfig simpleNetCfg -> do + cfg <- (`updateNetworkConfig` simpleNetCfg) <$> lift getNetworkConfig + void . processChatCommand $ APISetNetworkConfig cfg + pure $ CRNetworkConfig cfg + APISetNetworkInfo info -> lift (withAgent' (`setUserNetworkInfo` info)) >> ok_ + ReconnectAllServers -> withUser' $ \_ -> lift (withAgent' reconnectAllServers) >> ok_ + ReconnectServer userId srv -> withUserId userId $ \user -> do + lift (withAgent' $ \a -> reconnectSMPServer a (aUserId user) srv) + ok_ + APISetChatSettings (ChatRef cType chatId) chatSettings -> withUser $ \user -> case cType of + CTDirect -> do + ct <- withFastStore $ \db -> do + ct <- getContact db vr user chatId + liftIO $ updateContactSettings db user chatId chatSettings + pure ct + forM_ (contactConnId ct) $ \connId -> + withAgent $ \a -> toggleConnectionNtfs a connId (chatHasNtfs chatSettings) + ok user + CTGroup -> do + ms <- withFastStore $ \db -> do + Group _ ms <- getGroup db vr user chatId + liftIO $ updateGroupSettings db user chatId chatSettings + pure ms + forM_ (filter memberActive ms) $ \m -> forM_ (memberConnId m) $ \connId -> + withAgent (\a -> toggleConnectionNtfs a connId $ chatHasNtfs chatSettings) `catchChatError` eToView + ok user + _ -> throwCmdError "not supported" + APISetMemberSettings gId gMemberId settings -> withUser $ \user -> do + m <- withFastStore $ \db -> do + liftIO $ updateGroupMemberSettings db user gId gMemberId settings + getGroupMember db vr user gId gMemberId + let ntfOn = showMessages $ memberSettings m + toggleNtf m ntfOn + ok user + APIContactInfo contactId -> withUser $ \user@User {userId} -> do + -- [incognito] print user's incognito profile for this contact + ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId + incognitoProfile <- case activeConn of + Nothing -> pure Nothing + Just Connection {customUserProfileId} -> + forM customUserProfileId $ \profileId -> withFastStore (\db -> getProfileById db userId profileId) + connectionStats <- mapM (withAgent . flip getConnectionServers) (contactConnId ct) + pure $ CRContactInfo user ct connectionStats (fmap fromLocalProfile incognitoProfile) + APIContactQueueInfo contactId -> withUser $ \user -> do + ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId + case activeConn of + Just conn -> getConnQueueInfo user conn + Nothing -> throwChatError $ CEContactNotActive ct + APIGroupInfo gId -> withUser $ \user -> do + (g, s) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> liftIO (getGroupSummary db user gId) + pure $ CRGroupInfo user g s + APIGroupMemberInfo gId gMemberId -> withUser $ \user -> do + (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId + connectionStats <- mapM (withAgent . flip getConnectionServers) (memberConnId m) + pure $ CRGroupMemberInfo user g m connectionStats + APIGroupMemberQueueInfo gId gMemberId -> withUser $ \user -> do + GroupMember {activeConn} <- withFastStore $ \db -> getGroupMember db vr user gId gMemberId + case activeConn of + Just conn -> getConnQueueInfo user conn + Nothing -> throwChatError CEGroupMemberNotActive + APISwitchContact contactId -> withUser $ \user -> do + ct <- withFastStore $ \db -> getContact db vr user contactId + case contactConnId ct of + Just connId -> do + connectionStats <- withAgent $ \a -> switchConnectionAsync a "" connId + pure $ CRContactSwitchStarted user ct connectionStats + Nothing -> throwChatError $ CEContactNotActive ct + APISwitchGroupMember gId gMemberId -> withUser $ \user -> do + (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId + case memberConnId m of + Just connId -> do + connectionStats <- withAgent (\a -> switchConnectionAsync a "" connId) + pure $ CRGroupMemberSwitchStarted user g m connectionStats + _ -> throwChatError CEGroupMemberNotActive + APIAbortSwitchContact contactId -> withUser $ \user -> do + ct <- withFastStore $ \db -> getContact db vr user contactId + case contactConnId ct of + Just connId -> do + connectionStats <- withAgent $ \a -> abortConnectionSwitch a connId + pure $ CRContactSwitchAborted user ct connectionStats + Nothing -> throwChatError $ CEContactNotActive ct + APIAbortSwitchGroupMember gId gMemberId -> withUser $ \user -> do + (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId + case memberConnId m of + Just connId -> do + connectionStats <- withAgent $ \a -> abortConnectionSwitch a connId + pure $ CRGroupMemberSwitchAborted user g m connectionStats + _ -> throwChatError CEGroupMemberNotActive + APISyncContactRatchet contactId force -> withUser $ \user -> withContactLock "syncContactRatchet" contactId $ do + ct <- withFastStore $ \db -> getContact db vr user contactId + case contactConn ct of + Just conn@Connection {pqSupport} -> do + cStats@ConnectionStats {ratchetSyncState = rss} <- withAgent $ \a -> synchronizeRatchet a (aConnId conn) pqSupport force + createInternalChatItem user (CDDirectSnd ct) (CISndConnEvent $ SCERatchetSync rss Nothing) Nothing + pure $ CRContactRatchetSyncStarted user ct cStats + Nothing -> throwChatError $ CEContactNotActive ct + APISyncGroupMemberRatchet gId gMemberId force -> withUser $ \user -> withGroupLock "syncGroupMemberRatchet" gId $ do + (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId + case memberConnId m of + Just connId -> do + cStats@ConnectionStats {ratchetSyncState = rss} <- withAgent $ \a -> synchronizeRatchet a connId PQSupportOff force + createInternalChatItem user (CDGroupSnd g) (CISndConnEvent . SCERatchetSync rss . Just $ groupMemberRef m) Nothing + pure $ CRGroupMemberRatchetSyncStarted user g m cStats + _ -> throwChatError CEGroupMemberNotActive + APIGetContactCode contactId -> withUser $ \user -> do + ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId + case activeConn of + Just conn@Connection {connId} -> do + code <- getConnectionCode $ aConnId conn + ct' <- case contactSecurityCode ct of + Just SecurityCode {securityCode} + | sameVerificationCode code securityCode -> pure ct + | otherwise -> do + withFastStore' $ \db -> setConnectionVerified db user connId Nothing + pure (ct :: Contact) {activeConn = Just $ (conn :: Connection) {connectionCode = Nothing}} + _ -> pure ct + pure $ CRContactCode user ct' code + Nothing -> throwChatError $ CEContactNotActive ct + APIGetGroupMemberCode gId gMemberId -> withUser $ \user -> do + (g, m@GroupMember {activeConn}) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId + case activeConn of + Just conn@Connection {connId} -> do + code <- getConnectionCode $ aConnId conn + m' <- case memberSecurityCode m of + Just SecurityCode {securityCode} + | sameVerificationCode code securityCode -> pure m + | otherwise -> do + withFastStore' $ \db -> setConnectionVerified db user connId Nothing + pure (m :: GroupMember) {activeConn = Just $ (conn :: Connection) {connectionCode = Nothing}} + _ -> pure m + pure $ CRGroupMemberCode user g m' code + _ -> throwChatError CEGroupMemberNotActive + APIVerifyContact contactId code -> withUser $ \user -> do + ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId + case activeConn of + Just conn -> verifyConnectionCode user conn code + Nothing -> throwChatError $ CEContactNotActive ct + APIVerifyGroupMember gId gMemberId code -> withUser $ \user -> do + GroupMember {activeConn} <- withFastStore $ \db -> getGroupMember db vr user gId gMemberId + case activeConn of + Just conn -> verifyConnectionCode user conn code + _ -> throwChatError CEGroupMemberNotActive + APIEnableContact contactId -> withUser $ \user -> do + ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId + case activeConn of + Just conn -> do + withFastStore' $ \db -> setAuthErrCounter db user conn 0 + ok user + Nothing -> throwChatError $ CEContactNotActive ct + APIEnableGroupMember gId gMemberId -> withUser $ \user -> do + GroupMember {activeConn} <- withFastStore $ \db -> getGroupMember db vr user gId gMemberId + case activeConn of + Just conn -> do + withFastStore' $ \db -> setAuthErrCounter db user conn 0 + ok user + _ -> throwChatError CEGroupMemberNotActive + SetShowMessages cName ntfOn -> updateChatSettings cName (\cs -> cs {enableNtfs = ntfOn}) + SetSendReceipts cName rcptsOn_ -> updateChatSettings cName (\cs -> cs {sendRcpts = rcptsOn_}) + SetShowMemberMessages gName mName showMessages -> withUser $ \user -> do + (gId, mId) <- getGroupAndMemberId user gName mName + gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId + m <- withFastStore $ \db -> getGroupMember db vr user gId mId + let GroupInfo {membership = GroupMember {memberRole = membershipRole}} = gInfo + -- TODO GRModerator when most users migrate + when (membershipRole >= GRAdmin) $ throwChatError $ CECantBlockMemberForSelf gInfo m showMessages + let settings = (memberSettings m) {showMessages} + processChatCommand $ APISetMemberSettings gId mId settings + ContactInfo cName -> withContactName cName APIContactInfo + ShowGroupInfo gName -> withUser $ \user -> do + groupId <- withFastStore $ \db -> getGroupIdByName db user gName + processChatCommand $ APIGroupInfo groupId + GroupMemberInfo gName mName -> withMemberName gName mName APIGroupMemberInfo + ContactQueueInfo cName -> withContactName cName APIContactQueueInfo + GroupMemberQueueInfo gName mName -> withMemberName gName mName APIGroupMemberQueueInfo + SwitchContact cName -> withContactName cName APISwitchContact + SwitchGroupMember gName mName -> withMemberName gName mName APISwitchGroupMember + AbortSwitchContact cName -> withContactName cName APIAbortSwitchContact + AbortSwitchGroupMember gName mName -> withMemberName gName mName APIAbortSwitchGroupMember + SyncContactRatchet cName force -> withContactName cName $ \ctId -> APISyncContactRatchet ctId force + SyncGroupMemberRatchet gName mName force -> withMemberName gName mName $ \gId mId -> APISyncGroupMemberRatchet gId mId force + GetContactCode cName -> withContactName cName APIGetContactCode + GetGroupMemberCode gName mName -> withMemberName gName mName APIGetGroupMemberCode + VerifyContact cName code -> withContactName cName (`APIVerifyContact` code) + VerifyGroupMember gName mName code -> withMemberName gName mName $ \gId mId -> APIVerifyGroupMember gId mId code + EnableContact cName -> withContactName cName APIEnableContact + EnableGroupMember gName mName -> withMemberName gName mName $ \gId mId -> APIEnableGroupMember gId mId + ChatHelp section -> pure $ CRChatHelp section + Welcome -> withUser $ pure . CRWelcome + APIAddContact userId short incognito -> withUserId userId $ \user -> procCmd $ do + -- [incognito] generate profile for connection + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + subMode <- chatReadVar subscriptionMode + let userData = shortLinkUserData short + (connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation userData Nothing IKPQOn subMode + ccLink' <- shortenCreatedLink ccLink + -- TODO PQ pass minVersion from the current range + conn <- withFastStore' $ \db -> createDirectConnection db user connId ccLink' ConnNew incognitoProfile subMode initialChatVersion PQSupportOn + pure $ CRInvitation user ccLink' conn + AddContact short incognito -> withUser $ \User {userId} -> + processChatCommand $ APIAddContact userId short incognito + APISetConnectionIncognito connId incognito -> withUser $ \user@User {userId} -> do + conn'_ <- withFastStore $ \db -> do + conn@PendingContactConnection {pccConnStatus, customUserProfileId} <- getPendingContactConnection db userId connId + case (pccConnStatus, customUserProfileId, incognito) of + (ConnNew, Nothing, True) -> liftIO $ do + incognitoProfile <- generateRandomProfile + pId <- createIncognitoProfile db user incognitoProfile + Just <$> updatePCCIncognito db user conn (Just pId) + (ConnNew, Just pId, False) -> liftIO $ do + deletePCCIncognitoProfile db user pId + Just <$> updatePCCIncognito db user conn Nothing + _ -> pure Nothing + case conn'_ of + Just conn' -> pure $ CRConnectionIncognitoUpdated user conn' + Nothing -> throwChatError CEConnectionIncognitoChangeProhibited + APIChangeConnectionUser connId newUserId -> withUser $ \user@User {userId} -> do + conn <- withFastStore $ \db -> getPendingContactConnection db userId connId + let PendingContactConnection {pccConnStatus, connLinkInv} = conn + case (pccConnStatus, connLinkInv) of + (ConnNew, Just (CCLink cReqInv _)) -> do + newUser <- privateGetUser newUserId + conn' <- recreateConn user conn newUser + pure $ CRConnectionUserChanged user conn conn' newUser + _ -> throwChatError CEConnectionUserChangeProhibited + where + recreateConn user conn@PendingContactConnection {customUserProfileId, connLinkInv} newUser = do + subMode <- chatReadVar subscriptionMode + let userData = shortLinkUserData $ isJust $ connShortLink =<< connLinkInv + (agConnId, ccLink) <- withAgent $ \a -> createConnection a (aUserId newUser) True SCMInvitation userData Nothing IKPQOn subMode + ccLink' <- shortenCreatedLink ccLink + conn' <- withFastStore' $ \db -> do + deleteConnectionRecord db user connId + forM_ customUserProfileId $ \profileId -> + deletePCCIncognitoProfile db user profileId + createDirectConnection db newUser agConnId ccLink' ConnNew Nothing subMode initialChatVersion PQSupportOn + deleteAgentConnectionAsync (aConnId' conn) + pure conn' + APIConnectPlan userId cLink -> withUserId userId $ \user -> + uncurry (CRConnectionPlan user) <$> connectPlan user cLink + APIConnect userId incognito (Just (ACCL SCMInvitation (CCLink cReq@(CRInvitationUri crData e2e) sLnk_))) -> withUserId userId $ \user -> withInvitationLock "connect" (strEncode cReq) . procCmd $ do + subMode <- chatReadVar subscriptionMode + -- [incognito] generate profile to send + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + let profileToSend = userProfileToSend user incognitoProfile Nothing False + lift (withAgent' $ \a -> connRequestPQSupport a PQSupportOn cReq) >>= \case + Nothing -> throwChatError CEInvalidConnReq + -- TODO PQ the error above should be CEIncompatibleConnReqVersion, also the same API should be called in Plan + Just (agentV, pqSup') -> do + let chatV = agentToChatVersion agentV + dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend + withFastStore' (\db -> getConnectionEntityByConnReq db vr user cReqs) >>= \case + Nothing -> joinNewConn chatV dm + Just (RcvDirectMsgConnection conn@Connection {connId, connStatus, contactConnInitiated} Nothing) + | connStatus == ConnNew && contactConnInitiated -> joinNewConn chatV dm -- own connection link + | connStatus == ConnPrepared -> do + -- retrying join after error + pcc <- withFastStore $ \db -> getPendingContactConnection db userId connId + joinPreparedConn (aConnId conn) pcc dm + Just ent -> throwChatError $ CECommandError $ "connection exists: " <> show (connEntityInfo ent) + where + joinNewConn chatV dm = do + connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup' + let ccLink = CCLink cReq $ serverShortLink <$> sLnk_ + pcc <- withFastStore' $ \db -> createDirectConnection db user connId ccLink ConnPrepared (incognitoProfile $> profileToSend) subMode chatV pqSup' + joinPreparedConn connId pcc dm + joinPreparedConn connId pcc@PendingContactConnection {pccConnId} dm = do + void $ withAgent $ \a -> joinConnection a (aUserId user) connId True cReq dm pqSup' subMode + withFastStore' $ \db -> updateConnectionStatusFromTo db pccConnId ConnPrepared ConnJoined + pure $ CRSentConfirmation user pcc {pccConnStatus = ConnJoined} + cReqs = + ( CRInvitationUri crData {crScheme = SSSimplex} e2e, + CRInvitationUri crData {crScheme = simplexChat} e2e + ) + APIConnect userId incognito (Just (ACCL SCMContact ccLink)) -> withUserId userId $ \user -> connectViaContact user incognito ccLink + APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq + Connect incognito (Just cLink@(ACL m cLink')) -> withUser $ \user -> do + (ccLink, plan) <- connectPlan user cLink `catchChatError` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink ILPOk); _ -> throwError e + connectWithPlan user incognito ccLink plan + Connect _ Nothing -> throwChatError CEInvalidConnReq + APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do + ct@Contact {activeConn, profile = LocalProfile {contactLink}} <- withFastStore $ \db -> getContact db vr user contactId + when (isJust activeConn) $ throwChatError (CECommandError "contact already has connection") + ccLink <- case contactLink of + Just (CLFull cReq) -> pure $ CCLink cReq Nothing + Just (CLShort sLnk) -> do + cReq <- getShortLinkConnReq user sLnk + pure $ CCLink cReq $ Just sLnk + Nothing -> throwChatError (CECommandError "no address in contact profile") + connectContactViaAddress user incognito ct ccLink + ConnectSimplex incognito -> withUser $ \user -> do + plan <- contactRequestPlan user adminContactReq `catchChatError` const (pure $ CPContactAddress CAPOk) + connectWithPlan user incognito (ACCL SCMContact (CCLink adminContactReq Nothing)) plan + DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId) cdm + ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect + APIListContacts userId -> withUserId userId $ \user -> + CRContactsList user <$> withFastStore' (\db -> getUserContacts db vr user) + ListContacts -> withUser $ \User {userId} -> + processChatCommand $ APIListContacts userId + APICreateMyAddress userId short -> withUserId userId $ \user -> procCmd $ do + subMode <- chatReadVar subscriptionMode + let userData = shortLinkUserData short + (connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact userData Nothing IKPQOn subMode + ccLink' <- shortenCreatedLink ccLink + withFastStore $ \db -> createUserContactLink db user connId ccLink' subMode + pure $ CRUserContactLinkCreated user ccLink' + CreateMyAddress short -> withUser $ \User {userId} -> + processChatCommand $ APICreateMyAddress userId short + APIDeleteMyAddress userId -> withUserId userId $ \user@User {profile = p} -> do + conns <- withFastStore $ \db -> getUserAddressConnections db vr user + withChatLock "deleteMyAddress" $ do + deleteAgentConnectionsAsync $ map aConnId conns + withFastStore' (`deleteUserAddress` user) + let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing} + r <- updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user Nothing + let user' = case r of + CRUserProfileUpdated u' _ _ _ -> u' + _ -> user + pure $ CRUserContactLinkDeleted user' + DeleteMyAddress -> withUser $ \User {userId} -> + processChatCommand $ APIDeleteMyAddress userId + APIShowMyAddress userId -> withUserId' userId $ \user -> + CRUserContactLink user <$> withFastStore (`getUserAddress` user) + ShowMyAddress -> withUser' $ \User {userId} -> + processChatCommand $ APIShowMyAddress userId + APISetProfileAddress userId False -> withUserId userId $ \user@User {profile = p} -> do + let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing} + updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user Nothing + APISetProfileAddress userId True -> withUserId userId $ \user@User {profile = p} -> do + ucl@UserContactLink {connLinkContact = CCLink cReq _} <- withFastStore (`getUserAddress` user) + -- TODO [short links] replace with short links + let p' = (fromLocalProfile p :: Profile) {contactLink = Just $ CLFull cReq} + updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user $ Just ucl + SetProfileAddress onOff -> withUser $ \User {userId} -> + processChatCommand $ APISetProfileAddress userId onOff + APIAddressAutoAccept userId autoAccept_ -> withUserId userId $ \user -> do + forM_ autoAccept_ $ \AutoAccept {businessAddress, acceptIncognito} -> + when (businessAddress && acceptIncognito) $ throwChatError $ CECommandError "requests to business address cannot be accepted incognito" + contactLink <- withFastStore (\db -> updateUserAddressAutoAccept db user autoAccept_) + pure $ CRUserContactLinkUpdated user contactLink + AddressAutoAccept autoAccept_ -> withUser $ \User {userId} -> + processChatCommand $ APIAddressAutoAccept userId autoAccept_ + AcceptContact incognito cName -> withUser $ \User {userId} -> do + connReqId <- withFastStore $ \db -> getContactRequestIdByName db userId cName + processChatCommand $ APIAcceptContact incognito connReqId + RejectContact cName -> withUser $ \User {userId} -> do + connReqId <- withFastStore $ \db -> getContactRequestIdByName db userId cName + processChatCommand $ APIRejectContact connReqId + ForwardMessage toChatName fromContactName forwardedMsg -> withUser $ \user -> do + contactId <- withFastStore $ \db -> getContactIdByName db user fromContactName + forwardedItemId <- withFastStore $ \db -> getDirectChatItemIdByText' db user contactId forwardedMsg + toChatRef <- getChatRef user toChatName + processChatCommand $ APIForwardChatItems toChatRef (ChatRef CTDirect contactId) (forwardedItemId :| []) Nothing + ForwardGroupMessage toChatName fromGroupName fromMemberName_ forwardedMsg -> withUser $ \user -> do + groupId <- withFastStore $ \db -> getGroupIdByName db user fromGroupName + forwardedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user groupId fromMemberName_ forwardedMsg + toChatRef <- getChatRef user toChatName + processChatCommand $ APIForwardChatItems toChatRef (ChatRef CTGroup groupId) (forwardedItemId :| []) Nothing + ForwardLocalMessage toChatName forwardedMsg -> withUser $ \user -> do + folderId <- withFastStore (`getUserNoteFolderId` user) + forwardedItemId <- withFastStore $ \db -> getLocalChatItemIdByText' db user folderId forwardedMsg + toChatRef <- getChatRef user toChatName + processChatCommand $ APIForwardChatItems toChatRef (ChatRef CTLocal folderId) (forwardedItemId :| []) Nothing + SendMessage (ChatName cType name) msg -> withUser $ \user -> do + let mc = MCText msg + case cType of + CTDirect -> + withFastStore' (\db -> runExceptT $ getContactIdByName db user name) >>= \case + Right ctId -> do + let sendRef = SRDirect ctId + processChatCommand $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] + Left _ -> + withFastStore' (\db -> runExceptT $ getActiveMembersByName db vr user name) >>= \case + Right [(gInfo, member)] -> do + let GroupInfo {localDisplayName = gName} = gInfo + GroupMember {localDisplayName = mName} = member + processChatCommand $ SendMemberContactMessage gName mName msg + Right (suspectedMember : _) -> + throwChatError $ CEContactNotFound name (Just suspectedMember) + _ -> + throwChatError $ CEContactNotFound name Nothing + CTGroup -> do + (gId, mentions) <- withFastStore $ \db -> do + gId <- getGroupIdByName db user name + (gId,) <$> liftIO (getMessageMentions db user gId msg) + let sendRef = SRGroup gId Nothing + processChatCommand $ APISendMessages sendRef False Nothing [ComposedMessage Nothing Nothing mc mentions] + CTLocal + | name == "" -> do + folderId <- withFastStore (`getUserNoteFolderId` user) + processChatCommand $ APICreateChatItems folderId [composedMessage Nothing mc] + | otherwise -> throwChatError $ CECommandError "not supported" + _ -> throwChatError $ CECommandError "not supported" + SendMemberContactMessage gName mName msg -> withUser $ \user -> do + (gId, mId) <- getGroupAndMemberId user gName mName + m <- withFastStore $ \db -> getGroupMember db vr user gId mId + let mc = MCText msg + case memberContactId m of + Nothing -> do + g <- withFastStore $ \db -> getGroupInfo db vr user gId + unless (groupFeatureMemberAllowed SGFDirectMessages (membership g) g) $ throwChatError $ CECommandError "direct messages not allowed" + toView $ CEvtNoMemberContactCreating user g m + processChatCommand (APICreateMemberContact gId mId) >>= \case + CRNewMemberContact _ ct@Contact {contactId} _ _ -> do + toViewTE $ TENewMemberContact user ct g m + processChatCommand $ APISendMemberContactInvitation contactId (Just mc) + cr -> pure cr + Just ctId -> do + let sendRef = SRDirect ctId + processChatCommand $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] + SendLiveMessage chatName msg -> withUser $ \user -> do + (chatRef, mentions) <- getChatRefAndMentions user chatName msg + withSendRef chatRef $ \sendRef -> do + let mc = MCText msg + processChatCommand $ APISendMessages sendRef True Nothing [ComposedMessage Nothing Nothing mc mentions] + SendMessageBroadcast mc -> withUser $ \user -> do + contacts <- withFastStore' $ \db -> getUserContacts db vr user + withChatLock "sendMessageBroadcast" . procCmd $ do + let ctConns_ = L.nonEmpty $ foldr addContactConn [] contacts + case ctConns_ of + Nothing -> do + timestamp <- liftIO getCurrentTime + pure CRBroadcastSent {user, msgContent = mc, successes = 0, failures = 0, timestamp} + Just (ctConns :: NonEmpty (Contact, Connection)) -> do + let idsEvts = L.map ctSndEvent ctConns + -- TODO Broadcast rework + -- In createNewSndMessage and encodeChatMessage we could use Nothing for sharedMsgId, + -- then we could reuse message body across broadcast. + -- Encoding different sharedMsgId and reusing body is meaningless as referencing will not work anyway. + -- As an improvement, single message record with its sharedMsgId could be created for new "broadcast" entity. + -- Then all recipients could refer to broadcast message using same sharedMsgId. + sndMsgs <- lift $ createSndMessages idsEvts + let msgReqs_ :: NonEmpty (Either ChatError ChatMsgReq) = L.zipWith (fmap . ctMsgReq) ctConns sndMsgs + (errs, ctSndMsgs :: [(Contact, SndMessage)]) <- + partitionEithers . L.toList . zipWith3' combineResults ctConns sndMsgs <$> deliverMessagesB msgReqs_ + timestamp <- liftIO getCurrentTime + lift . void $ withStoreBatch' $ \db -> map (createCI db user timestamp) ctSndMsgs + pure CRBroadcastSent {user, msgContent = mc, successes = length ctSndMsgs, failures = length errs, timestamp} + where + addContactConn :: Contact -> [(Contact, Connection)] -> [(Contact, Connection)] + addContactConn ct ctConns = case contactSendConn_ ct of + Right conn | directOrUsed ct -> (ct, conn) : ctConns + _ -> ctConns + ctSndEvent :: (Contact, Connection) -> (ConnOrGroupId, ChatMsgEvent 'Json) + ctSndEvent (_, Connection {connId}) = (ConnectionId connId, XMsgNew $ MCSimple (extMsgContent mc Nothing)) + 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) + combineResults (ct, _) (Right msg') (Right _) = Right (ct, msg') + combineResults _ (Left e) _ = Left e + combineResults _ _ (Left e) = Left e + createCI :: DB.Connection -> User -> UTCTime -> (Contact, SndMessage) -> IO () + createCI db user createdAt (ct, sndMsg) = + void $ createNewSndChatItem db user (CDDirectSnd ct) Nothing sndMsg (CISndMsgContent mc) Nothing Nothing Nothing False createdAt + SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \user@User {userId} -> do + contactId <- withFastStore $ \db -> getContactIdByName db user cName + quotedItemId <- withFastStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg + let mc = MCText msg + processChatCommand $ APISendMessages (SRDirect contactId) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc M.empty] + DeleteMessage chatName deletedMsg -> withUser $ \user -> do + chatRef <- getChatRef user chatName + deletedItemId <- getSentChatItemIdByText user chatRef deletedMsg + processChatCommand $ APIDeleteChatItem chatRef (deletedItemId :| []) CIDMBroadcast + DeleteMemberMessage gName mName deletedMsg -> withUser $ \user -> do + gId <- withFastStore $ \db -> getGroupIdByName db user gName + deletedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user gId (Just mName) deletedMsg + processChatCommand $ APIDeleteMemberChatItem gId (deletedItemId :| []) + EditMessage chatName editedMsg msg -> withUser $ \user -> do + (chatRef, mentions) <- getChatRefAndMentions user chatName msg + editedItemId <- getSentChatItemIdByText user chatRef editedMsg + let mc = MCText msg + processChatCommand $ APIUpdateChatItem chatRef editedItemId False $ UpdatedMessage mc mentions + UpdateLiveMessage chatName chatItemId live msg -> withUser $ \user -> do + (chatRef, mentions) <- getChatRefAndMentions user chatName msg + let mc = MCText msg + processChatCommand $ APIUpdateChatItem chatRef chatItemId live $ UpdatedMessage mc mentions + ReactToMessage add reaction chatName msg -> withUser $ \user -> do + chatRef <- getChatRef user chatName + chatItemId <- getChatItemIdByText user chatRef msg + processChatCommand $ APIChatItemReaction chatRef chatItemId add reaction + APINewGroup userId incognito gProfile@GroupProfile {displayName} -> withUserId userId $ \user -> do + checkValidName displayName + gVar <- asks random + -- [incognito] generate incognito profile for group membership + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + gInfo <- withFastStore $ \db -> createNewGroup db vr gVar user gProfile incognitoProfile + let cd = CDGroupSnd gInfo + createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing + createGroupFeatureItems user cd CISndGroupFeature gInfo + pure $ CRGroupCreated user gInfo + NewGroup incognito gProfile -> withUser $ \User {userId} -> + processChatCommand $ APINewGroup userId incognito gProfile + 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 + assertDirectAllowed user MDSnd contact XGrpInv_ + let Group gInfo members = group + Contact {localDisplayName = cName} = contact + assertUserGroupRole gInfo $ max GRAdmin memRole + -- [incognito] forbid to invite contact to whom user is connected incognito + when (contactConnIncognito contact) $ throwChatError CEContactIncognitoCantInvite + -- [incognito] forbid to invite contacts if user joined the group using an incognito profile + when (incognitoMembership gInfo) $ throwChatError CEGroupIncognitoCantInvite + let sendInvitation = sendGrpInvitation user contact gInfo + case contactMember contact members of + Nothing -> do + gVar <- asks random + subMode <- chatReadVar subscriptionMode + (agentConnId, CCLink cReq _) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode + member <- withFastStore $ \db -> createNewContactMember db gVar user gInfo contact memRole agentConnId cReq subMode + sendInvitation member cReq + pure $ CRSentGroupInvitation user gInfo contact member + Just member@GroupMember {groupMemberId, memberStatus, memberRole = mRole} + | memberStatus == GSMemInvited -> do + unless (mRole == memRole) $ withFastStore' $ \db -> updateGroupMemberRole db user member memRole + withFastStore' (\db -> getMemberInvitation db user groupMemberId) >>= \case + Just cReq -> do + sendInvitation member {memberRole = memRole} cReq + pure $ CRSentGroupInvitation user gInfo contact member {memberRole = memRole} + Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName + | otherwise -> throwChatError $ CEGroupDuplicateMember cName + APIJoinGroup groupId enableNtfs -> withUser $ \user@User {userId} -> do + withGroupLock "joinGroup" groupId . procCmd $ do + (invitation, ct) <- withFastStore $ \db -> do + inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db vr user groupId + (inv,) <$> getContactViaMember db vr user fromMember + let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership, chatSettings}} = invitation + GroupMember {memberId = membershipMemId} = membership + Contact {activeConn} = ct + case activeConn of + Just Connection {peerChatVRange} -> do + subMode <- chatReadVar subscriptionMode + dm <- encodeConnInfo $ XGrpAcpt membershipMemId + agentConnId <- case memberConn fromMember of + Nothing -> do + agentConnId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connRequest PQSupportOff + let chatV = vr `peerConnChatVersion` peerChatVRange + void $ withFastStore' $ \db -> createMemberConnection db userId fromMember agentConnId chatV peerChatVRange subMode + pure agentConnId + Just conn -> pure $ aConnId conn + withFastStore' $ \db -> do + updateGroupMemberStatus db userId fromMember GSMemAccepted + updateGroupMemberStatus db userId membership GSMemAccepted + -- MFAll is default for new groups + unless (enableNtfs == MFAll) $ updateGroupSettings db user groupId chatSettings {enableNtfs} + void (withAgent $ \a -> joinConnection a (aUserId user) agentConnId (enableNtfs /= MFNone) connRequest dm PQSupportOff subMode) + `catchChatError` \e -> do + withFastStore' $ \db -> do + updateGroupMemberStatus db userId fromMember GSMemInvited + updateGroupMemberStatus db userId membership GSMemInvited + throwError e + updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` eToView + pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing + Nothing -> throwChatError $ CEContactNotActive ct + APIAcceptMember groupId gmId role -> withUser $ \user -> do + (gInfo, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user gmId + assertUserGroupRole gInfo GRAdmin + when (memberStatus m /= GSMemPendingApproval) $ throwChatError $ CECommandError "member is not pending approval" + case memberConn m of + Just mConn -> do + let msg = XGrpLinkAcpt role + void $ sendDirectMemberMessage mConn msg groupId + m' <- withFastStore' $ \db -> updateGroupMemberAccepted db user m role + introduceToGroup vr user gInfo m' + pure $ CRJoinedGroupMember user gInfo m' + _ -> throwChatError CEGroupMemberNotActive + APIMembersRole groupId memberIds newRole -> withUser $ \user -> + withGroupLock "memberRole" groupId . procCmd $ do + g@(Group gInfo members) <- withFastStore $ \db -> getGroup db vr user groupId + when (selfSelected gInfo) $ throwChatError $ CECommandError "can't change role for self" + let (invitedMems, currentMems, unchangedMems, maxRole, anyAdmin, anyPending) = selectMembers members + when (length invitedMems + length currentMems + length unchangedMems /= length memberIds) $ throwChatError CEGroupMemberNotFound + when (length memberIds > 1 && (anyAdmin || newRole >= GRAdmin)) $ + throwChatError $ CECommandError "can't change role of multiple members when admins selected, or new role is admin" + when anyPending $ throwChatError $ CECommandError "can't change role of members pending approval" + assertUserGroupRole gInfo $ maximum ([GRAdmin, maxRole, newRole] :: [GroupMemberRole]) + (errs1, changed1) <- changeRoleInvitedMems user gInfo invitedMems + (errs2, changed2, acis) <- changeRoleCurrentMems user g currentMems + unless (null acis) $ toView $ CEvtNewChatItems user acis + let errs = errs1 <> errs2 + unless (null errs) $ toView $ CEvtChatErrors errs + pure $ CRMembersRoleUser {user, groupInfo = gInfo, members = changed1 <> changed2, toRole = newRole} -- same order is not guaranteed + where + selfSelected GroupInfo {membership} = elem (groupMemberId' membership) memberIds + selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool, Bool) + selectMembers = foldr' addMember ([], [], [], GRObserver, False, False) + where + addMember m@GroupMember {groupMemberId, memberStatus, memberRole} (invited, current, unchanged, maxRole, anyAdmin, anyPending) + | groupMemberId `elem` memberIds = + let maxRole' = max maxRole memberRole + anyAdmin' = anyAdmin || memberRole >= GRAdmin + anyPending' = anyPending || memberStatus == GSMemPendingApproval + in + if + | memberRole == newRole -> (invited, current, m : unchanged, maxRole', anyAdmin', anyPending') + | memberStatus == GSMemInvited -> (m : invited, current, unchanged, maxRole', anyAdmin', anyPending') + | otherwise -> (invited, m : current, unchanged, maxRole', anyAdmin', anyPending') + | otherwise = (invited, current, unchanged, maxRole, anyAdmin, anyPending) + changeRoleInvitedMems :: User -> GroupInfo -> [GroupMember] -> CM ([ChatError], [GroupMember]) + changeRoleInvitedMems user gInfo memsToChange = do + -- not batched, as we need to send different invitations to different connections anyway + mems_ <- forM memsToChange $ \m -> (Right <$> changeRole m) `catchChatError` (pure . Left) + pure $ partitionEithers mems_ + where + changeRole :: GroupMember -> CM GroupMember + changeRole m@GroupMember {groupMemberId, memberContactId, localDisplayName = cName} = do + withFastStore (\db -> (,) <$> mapM (getContact db vr user) memberContactId <*> liftIO (getMemberInvitation db user groupMemberId)) >>= \case + (Just ct, Just cReq) -> do + sendGrpInvitation user ct gInfo (m :: GroupMember) {memberRole = newRole} cReq + withFastStore' $ \db -> updateGroupMemberRole db user m newRole + pure (m :: GroupMember) {memberRole = newRole} + _ -> throwChatError $ CEGroupCantResendInvitation gInfo cName + changeRoleCurrentMems :: User -> Group -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem]) + changeRoleCurrentMems user (Group gInfo members) memsToChange = case L.nonEmpty memsToChange of + Nothing -> pure ([], [], []) + Just memsToChange' -> do + let events = L.map (\GroupMember {memberId} -> XGrpMemRole memberId newRole) memsToChange' + (msgs_, _gsr) <- sendGroupMessages user gInfo members events + let itemsData = zipWith (fmap . sndItemData) memsToChange (L.toList msgs_) + cis_ <- saveSndChatItems user (CDGroupSnd gInfo) Nothing itemsData Nothing False + when (length cis_ /= length memsToChange) $ logError "changeRoleCurrentMems: memsToChange and cis_ length mismatch" + (errs, changed) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (updMember db) memsToChange) + let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo)) $ rights cis_ + pure (errs, changed, acis) + where + sndItemData :: GroupMember -> SndMessage -> NewSndChatItemData c + sndItemData GroupMember {groupMemberId, memberProfile} msg = + let content = CISndGroupEvent $ SGEMemberRole groupMemberId (fromLocalProfile memberProfile) newRole + ts = ciContentTexts content + in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing + updMember db m = do + updateGroupMemberRole db user m newRole + pure (m :: GroupMember) {memberRole = newRole} + APIBlockMembersForAll groupId memberIds blockFlag -> withUser $ \user -> + withGroupLock "blockForAll" groupId . procCmd $ do + Group gInfo members <- withFastStore $ \db -> getGroup db vr user groupId + when (selfSelected gInfo) $ throwChatError $ CECommandError "can't block/unblock self" + let (blockMems, remainingMems, maxRole, anyAdmin, anyPending) = selectMembers members + when (length blockMems /= length memberIds) $ throwChatError CEGroupMemberNotFound + when (length memberIds > 1 && anyAdmin) $ throwChatError $ CECommandError "can't block/unblock multiple members when admins selected" + when anyPending $ throwChatError $ CECommandError "can't block/unblock members pending approval" + assertUserGroupRole gInfo $ max GRModerator maxRole + blockMembers user gInfo blockMems remainingMems + where + selfSelected GroupInfo {membership} = elem (groupMemberId' membership) memberIds + selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], GroupMemberRole, Bool, Bool) + selectMembers = foldr' addMember ([], [], GRObserver, False, False) + where + addMember m@GroupMember {groupMemberId, memberRole, memberStatus} (block, remaining, maxRole, anyAdmin, anyPending) + | groupMemberId `elem` memberIds = + let maxRole' = max maxRole memberRole + anyAdmin' = anyAdmin || memberRole >= GRAdmin + anyPending' = anyPending || memberStatus == GSMemPendingApproval + in (m : block, remaining, maxRole', anyAdmin', anyPending') + | otherwise = (block, m : remaining, maxRole, anyAdmin, anyPending) + blockMembers :: User -> GroupInfo -> [GroupMember] -> [GroupMember] -> CM ChatResponse + blockMembers user gInfo blockMems remainingMems = case L.nonEmpty blockMems of + Nothing -> throwChatError $ CECommandError "no members to block/unblock" + Just blockMems' -> do + let mrs = if blockFlag then MRSBlocked else MRSUnrestricted + events = L.map (\GroupMember {memberId} -> XGrpMemRestrict memberId MemberRestrictions {restriction = mrs}) blockMems' + (msgs_, _gsr) <- sendGroupMessages user gInfo remainingMems events + let itemsData = zipWith (fmap . sndItemData) blockMems (L.toList msgs_) + cis_ <- saveSndChatItems user (CDGroupSnd gInfo) Nothing itemsData Nothing False + when (length cis_ /= length blockMems) $ logError "blockMembers: blockMems and cis_ length mismatch" + let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo)) $ rights cis_ + unless (null acis) $ toView $ CEvtNewChatItems user acis + (errs, blocked) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (updateGroupMemberBlocked db user gInfo mrs) blockMems) + unless (null errs) $ toView $ CEvtChatErrors errs + -- TODO not batched - requires agent batch api + forM_ blocked $ \m -> toggleNtf m (not blockFlag) + pure CRMembersBlockedForAllUser {user, groupInfo = gInfo, members = blocked, blocked = blockFlag} + where + sndItemData :: GroupMember -> SndMessage -> NewSndChatItemData c + sndItemData GroupMember {groupMemberId, memberProfile} msg = + let content = CISndGroupEvent $ SGEMemberBlocked groupMemberId (fromLocalProfile memberProfile) blockFlag + ts = ciContentTexts content + in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing + APIRemoveMembers {groupId, groupMemberIds, withMessages} -> withUser $ \user -> + withGroupLock "removeMembers" groupId . procCmd $ do + Group gInfo members <- withFastStore $ \db -> getGroup db vr user groupId + let (count, invitedMems, pendingMems, currentMems, maxRole, anyAdmin) = selectMembers members + memCount = S.size groupMemberIds + when (count /= memCount) $ throwChatError CEGroupMemberNotFound + when (memCount > 1 && anyAdmin) $ throwChatError $ CECommandError "can't remove multiple members when admins selected" + assertUserGroupRole gInfo $ max GRAdmin maxRole + (errs1, deleted1) <- deleteInvitedMems user invitedMems + (errs2, deleted2, acis2) <- deleteMemsSend user gInfo members currentMems + rs <- forM pendingMems $ \m -> deleteMemsSend user gInfo [m] [m] -- TODO [knocking] + let (errs3, deleted3, acis3) = concatTuples rs + acis = acis2 <> acis3 + errs = errs1 <> errs2 <> errs3 + unless (null acis) $ toView $ CEvtNewChatItems user acis + unless (null errs) $ toView $ CEvtChatErrors errs + when withMessages $ deleteMessages user gInfo $ currentMems <> pendingMems + pure $ CRUserDeletedMembers user gInfo (deleted1 <> deleted2 <> deleted3) withMessages -- same order is not guaranteed + where + selectMembers :: [GroupMember] -> (Int, [GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool) + selectMembers = foldl' addMember (0, [], [], [], GRObserver, False) + where + addMember acc@(n, invited, pending, current, maxRole, anyAdmin) m@GroupMember {groupMemberId, memberStatus, memberRole} + | groupMemberId `S.member` groupMemberIds = + let maxRole' = max maxRole memberRole + anyAdmin' = anyAdmin || memberRole >= GRAdmin + n' = n + 1 + in case memberStatus of + GSMemInvited -> (n', m : invited, pending, current, maxRole', anyAdmin') + GSMemPendingApproval -> (n', invited, m : pending, current, maxRole', anyAdmin') + _ -> (n', invited, pending, m : current, maxRole', anyAdmin') + | otherwise = acc + deleteInvitedMems :: User -> [GroupMember] -> CM ([ChatError], [GroupMember]) + deleteInvitedMems user memsToDelete = do + deleteMembersConnections user memsToDelete + lift $ partitionEithers <$> withStoreBatch' (\db -> map (delMember db) memsToDelete) + where + delMember db m = do + deleteGroupMember db user m + pure m {memberStatus = GSMemRemoved} + deleteMemsSend :: User -> GroupInfo -> [GroupMember] -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem]) + deleteMemsSend user gInfo sendToMems memsToDelete = case L.nonEmpty memsToDelete of + Nothing -> pure ([], [], []) + Just memsToDelete' -> do + let events = L.map (\GroupMember {memberId} -> XGrpMemDel memberId withMessages) memsToDelete' + (msgs_, _gsr) <- sendGroupMessages user gInfo sendToMems events + let itemsData = zipWith (fmap . sndItemData) memsToDelete (L.toList msgs_) + cis_ <- saveSndChatItems user (CDGroupSnd gInfo) Nothing itemsData Nothing False + when (length cis_ /= length memsToDelete) $ logError "deleteCurrentMems: memsToDelete and cis_ length mismatch" + deleteMembersConnections' user memsToDelete True + (errs, deleted) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (delMember db) memsToDelete) + let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo)) $ rights cis_ + pure (errs, deleted, acis) + where + sndItemData :: GroupMember -> SndMessage -> NewSndChatItemData c + sndItemData GroupMember {groupMemberId, memberProfile} msg = + let content = CISndGroupEvent $ SGEMemberDeleted groupMemberId (fromLocalProfile memberProfile) + ts = ciContentTexts content + in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing + delMember db m = do + deleteOrUpdateMemberRecordIO db user m + pure m {memberStatus = GSMemRemoved} + deleteMessages user gInfo@GroupInfo {membership} ms + | groupFeatureMemberAllowed SGFFullDelete membership gInfo = deleteGroupMembersCIs user gInfo ms membership + | otherwise = markGroupMembersCIsDeleted user gInfo ms membership + concatTuples :: [([a], [b], [c])] -> ([a], [b], [c]) + concatTuples xs = (concat as, concat bs, concat cs) + where (as, bs, cs) = unzip3 xs + APILeaveGroup groupId -> withUser $ \user@User {userId} -> do + Group gInfo@GroupInfo {membership} members <- withFastStore $ \db -> getGroup db vr user groupId + filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo + withGroupLock "leaveGroup" groupId . procCmd $ do + cancelFilesInProgress user filesInfo + -- TODO [knocking] send to pending approval members (move `memberCurrent` filter from sendGroupMessages_ to call sites) + msg <- sendGroupMessage' user gInfo members XGrpLeave + ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent SGEUserLeft) + toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) 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}} + APIListMembers groupId -> withUser $ \user -> + CRGroupMembers user <$> withFastStore (\db -> getGroup db vr user groupId) + AddMember gName cName memRole -> withUser $ \user -> do + (groupId, contactId) <- withFastStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db user cName + processChatCommand $ APIAddMember groupId contactId memRole + JoinGroup gName enableNtfs -> withUser $ \user -> do + groupId <- withFastStore $ \db -> getGroupIdByName db user gName + processChatCommand $ APIJoinGroup groupId enableNtfs + MemberRole gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIMembersRole gId [gMemberId] memRole + BlockForAll gName gMemberName blocked -> withMemberName gName gMemberName $ \gId gMemberId -> APIBlockMembersForAll gId [gMemberId] blocked + RemoveMembers gName gMemberNames withMessages -> withUser $ \user -> do + (gId, gMemberIds) <- withStore $ \db -> do + gId <- getGroupIdByName db user gName + gMemberIds <- S.fromList <$> mapM (getGroupMemberIdByName db user gId) (S.toList gMemberNames) + pure (gId, gMemberIds) + processChatCommand $ APIRemoveMembers gId gMemberIds withMessages + LeaveGroup gName -> withUser $ \user -> do + groupId <- withFastStore $ \db -> getGroupIdByName db user gName + processChatCommand $ APILeaveGroup groupId + DeleteGroup gName -> withUser $ \user -> do + groupId <- withFastStore $ \db -> getGroupIdByName db user gName + processChatCommand $ APIDeleteChat (ChatRef CTGroup groupId) (CDMFull True) + ClearGroup gName -> withUser $ \user -> do + groupId <- withFastStore $ \db -> getGroupIdByName db user gName + processChatCommand $ APIClearChat (ChatRef CTGroup groupId) + ListMembers gName -> withUser $ \user -> do + groupId <- withFastStore $ \db -> getGroupIdByName db user gName + processChatCommand $ APIListMembers groupId + APIListGroups userId contactId_ search_ -> withUserId userId $ \user -> + CRGroupsList user <$> withFastStore' (\db -> getUserGroupsWithSummary db vr user contactId_ search_) + ListGroups cName_ search_ -> withUser $ \user@User {userId} -> do + ct_ <- forM cName_ $ \cName -> withFastStore $ \db -> getContactByName db vr user cName + processChatCommand $ APIListGroups userId (contactId' <$> ct_) search_ + APIUpdateGroupProfile groupId p' -> withUser $ \user -> do + g <- withFastStore $ \db -> getGroup db vr user groupId + runUpdateGroupProfile user g p' + UpdateGroupNames gName GroupProfile {displayName, fullName} -> + updateGroupProfileByName gName $ \p -> p {displayName, fullName} + ShowGroupProfile gName -> withUser $ \user -> + CRGroupProfile user <$> withFastStore (\db -> getGroupInfoByName db vr user gName) + UpdateGroupDescription gName description -> + updateGroupProfileByName gName $ \p -> p {description} + ShowGroupDescription gName -> withUser $ \user -> + CRGroupDescription user <$> withFastStore (\db -> getGroupInfoByName db vr user gName) + APICreateGroupLink groupId mRole short -> withUser $ \user -> withGroupLock "createGroupLink" groupId $ do + gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + assertUserGroupRole gInfo GRAdmin + when (mRole > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole + groupLinkId <- GroupLinkId <$> drgRandomBytes 16 + subMode <- chatReadVar subscriptionMode + let crClientData = encodeJSON $ CRDataGroup groupLinkId + userData = shortLinkUserData short + (connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact userData (Just crClientData) IKPQOff subMode + ccLink' <- createdGroupLink <$> shortenCreatedLink ccLink + withFastStore $ \db -> createGroupLink db user gInfo connId ccLink' groupLinkId mRole subMode + pure $ CRGroupLinkCreated user gInfo ccLink' mRole + APIGroupLinkMemberRole groupId mRole' -> withUser $ \user -> withGroupLock "groupLinkMemberRole" groupId $ do + gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + (groupLinkId, groupLink, mRole) <- withFastStore $ \db -> getGroupLink db user gInfo + assertUserGroupRole gInfo GRAdmin + when (mRole' > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole' + when (mRole' /= mRole) $ withFastStore' $ \db -> setGroupLinkMemberRole db user groupLinkId mRole' + pure $ CRGroupLink user gInfo groupLink mRole' + APIDeleteGroupLink groupId -> withUser $ \user -> withGroupLock "deleteGroupLink" groupId $ do + gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + deleteGroupLink' user gInfo + pure $ CRGroupLinkDeleted user gInfo + APIGetGroupLink groupId -> withUser $ \user -> do + gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + (_, groupLink, mRole) <- withFastStore $ \db -> getGroupLink db user gInfo + pure $ CRGroupLink user gInfo groupLink mRole + APICreateMemberContact gId gMemberId -> withUser $ \user -> do + (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId + assertUserGroupRole g GRAuthor + unless (groupFeatureMemberAllowed SGFDirectMessages (membership g) g) $ throwChatError $ CECommandError "direct messages not allowed" + case memberConn m of + Just mConn@Connection {peerChatVRange} -> do + unless (maxVersion peerChatVRange >= groupDirectInvVersion) $ throwChatError CEPeerChatVRangeIncompatible + when (isJust $ memberContactId m) $ throwChatError $ CECommandError "member contact already exists" + subMode <- chatReadVar subscriptionMode + -- TODO PQ should negotitate contact connection with PQSupportOn? + (connId, CCLink cReq _) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode + -- [incognito] reuse membership incognito profile + ct <- withFastStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode + -- TODO not sure it is correct to set connections status here? + lift $ setContactNetworkStatus ct NSConnected + pure $ CRNewMemberContact user ct g m + _ -> throwChatError CEGroupMemberNotActive + APISendMemberContactInvitation contactId msgContent_ -> withUser $ \user -> do + (g@GroupInfo {groupId}, m, ct, cReq) <- withFastStore $ \db -> getMemberContact db vr user contactId + when (contactGrpInvSent ct) $ throwChatError $ CECommandError "x.grp.direct.inv already sent" + case memberConn m of + Just mConn -> do + let msg = XGrpDirectInv cReq msgContent_ + (sndMsg, _, _) <- sendDirectMemberMessage mConn msg groupId + withFastStore' $ \db -> setContactGrpInvSent db ct True + let ct' = ct {contactGrpInvSent = True} + forM_ msgContent_ $ \mc -> do + ci <- saveSndChatItem user (CDDirectSnd ct') sndMsg (CISndMsgContent mc) + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci] + pure $ CRNewMemberContactSentInv user ct' g m + _ -> throwChatError CEGroupMemberNotActive + CreateGroupLink gName mRole short -> withUser $ \user -> do + groupId <- withFastStore $ \db -> getGroupIdByName db user gName + processChatCommand $ APICreateGroupLink groupId mRole short + GroupLinkMemberRole gName mRole -> withUser $ \user -> do + groupId <- withFastStore $ \db -> getGroupIdByName db user gName + processChatCommand $ APIGroupLinkMemberRole groupId mRole + DeleteGroupLink gName -> withUser $ \user -> do + groupId <- withFastStore $ \db -> getGroupIdByName db user gName + processChatCommand $ APIDeleteGroupLink groupId + ShowGroupLink gName -> withUser $ \user -> do + groupId <- withFastStore $ \db -> getGroupIdByName db user gName + processChatCommand $ APIGetGroupLink groupId + SendGroupMessageQuote gName cName quotedMsg msg -> withUser $ \user -> do + (groupId, quotedItemId, mentions) <- + withFastStore $ \db -> do + gId <- getGroupIdByName db user gName + qiId <- getGroupChatItemIdByText db user gId cName quotedMsg + (gId, qiId,) <$> liftIO (getMessageMentions db user gId msg) + let mc = MCText msg + processChatCommand $ APISendMessages (SRGroup groupId Nothing) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc mentions] + ClearNoteFolder -> withUser $ \user -> do + folderId <- withFastStore (`getUserNoteFolderId` user) + processChatCommand $ APIClearChat (ChatRef CTLocal folderId) + LastChats count_ -> withUser' $ \user -> do + let count = fromMaybe 5000 count_ + (errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db vr user False (PTLast count) clqNoFilters) + unless (null errs) $ toView $ CEvtChatErrors (map ChatErrorStore errs) + pure $ CRChats previews + LastMessages (Just chatName) count search -> withUser $ \user -> do + chatRef <- getChatRef user chatName + chatResp <- processChatCommand $ APIGetChat chatRef Nothing (CPLast count) search + pure $ CRChatItems user (Just chatName) (aChatItems . chat $ chatResp) + LastMessages Nothing count search -> withUser $ \user -> do + chatItems <- withFastStore $ \db -> getAllChatItems db vr user (CPLast count) search + pure $ CRChatItems user Nothing chatItems + LastChatItemId (Just chatName) index -> withUser $ \user -> do + chatRef <- getChatRef user chatName + chatResp <- processChatCommand (APIGetChat chatRef Nothing (CPLast $ index + 1) Nothing) + pure $ CRChatItemId user (fmap aChatItemId . listToMaybe . aChatItems . chat $ chatResp) + LastChatItemId Nothing index -> withUser $ \user -> do + chatItems <- withFastStore $ \db -> getAllChatItems db vr user (CPLast $ index + 1) Nothing + pure $ CRChatItemId user (fmap aChatItemId . listToMaybe $ chatItems) + ShowChatItem (Just itemId) -> withUser $ \user -> do + chatItem <- withFastStore $ \db -> do + chatRef <- getChatRefViaItemId db user itemId + getAChatItem db vr user chatRef itemId + pure $ CRChatItems user Nothing ((: []) chatItem) + ShowChatItem Nothing -> withUser $ \user -> do + chatItems <- withFastStore $ \db -> getAllChatItems db vr user (CPLast 1) Nothing + pure $ CRChatItems user Nothing chatItems + ShowChatItemInfo chatName msg -> withUser $ \user -> do + chatRef <- getChatRef user chatName + itemId <- getChatItemIdByText user chatRef msg + processChatCommand $ APIGetChatItemInfo chatRef itemId + ShowLiveItems on -> withUser $ \_ -> + asks showLiveItems >>= atomically . (`writeTVar` on) >> ok_ + SendFile chatName f -> withUser $ \user -> do + chatRef <- getChatRef user chatName + case chatRef of + ChatRef CTLocal folderId -> processChatCommand $ APICreateChatItems folderId [composedMessage (Just f) (MCFile "")] + _ -> withSendRef chatRef $ \sendRef -> processChatCommand $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCFile "")] + SendImage chatName f@(CryptoFile fPath _) -> withUser $ \user -> do + chatRef <- getChatRef user chatName + withSendRef chatRef $ \sendRef -> do + filePath <- lift $ toFSFilePath fPath + unless (any (`isSuffixOf` map toLower fPath) imageExtensions) $ throwChatError CEFileImageType {filePath} + fileSize <- getFileSize filePath + unless (fileSize <= maxImageSize) $ throwChatError CEFileImageSize {filePath} + -- TODO include file description for preview + processChatCommand $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCImage "" fixedImagePreview)] + ForwardFile chatName fileId -> forwardFile chatName fileId SendFile + ForwardImage chatName fileId -> forwardFile chatName fileId SendImage + SendFileDescription _chatName _f -> throwCmdError "TODO" + -- TODO to use priority transactions we need a parameter that differentiates manual and automatic acceptance + ReceiveFile fileId userApprovedRelays encrypted_ rcvInline_ filePath_ -> withUser $ \_ -> + withFileLock "receiveFile" fileId . procCmd $ do + (user, ft@RcvFileTransfer {fileStatus}) <- withStore (`getRcvFileTransferById` fileId) + encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles + ft' <- (if encrypt && fileStatus == RFSNew then setFileToEncrypt else pure) ft + receiveFile' user ft' userApprovedRelays rcvInline_ filePath_ + SetFileToReceive fileId userApprovedRelays encrypted_ -> withUser $ \_ -> do + withFileLock "setFileToReceive" fileId . procCmd $ do + encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles + cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing + withStore' $ \db -> setRcvFileToReceive db fileId userApprovedRelays cfArgs + ok_ + CancelFile fileId -> withUser $ \user@User {userId} -> + withFileLock "cancelFile" fileId . procCmd $ + withFastStore (\db -> getFileTransfer db user fileId) >>= \case + FTSnd ftm@FileTransferMeta {xftpSndFile, cancelled} fts + | cancelled -> throwChatError $ CEFileCancel fileId "file already cancelled" + | not (null fts) && all fileCancelledOrCompleteSMP fts -> + throwChatError $ CEFileCancel fileId "file transfer is complete" + | otherwise -> do + fileAgentConnIds <- cancelSndFile user ftm fts True + deleteAgentConnectionsAsync fileAgentConnIds + withFastStore (\db -> liftIO $ lookupChatRefByFileId db user fileId) >>= \case + Nothing -> pure () + Just (ChatRef CTDirect contactId) -> do + (contact, sharedMsgId) <- withFastStore $ \db -> (,) <$> getContact db vr user contactId <*> getSharedMsgIdByFileId db userId fileId + void . sendDirectContactMessage user contact $ XFileCancel sharedMsgId + Just (ChatRef CTGroup groupId) -> do + (Group gInfo ms, sharedMsgId) <- withFastStore $ \db -> (,) <$> getGroup db vr user groupId <*> getSharedMsgIdByFileId db userId fileId + -- TODO [knocking] send separately to pending approval member + void . sendGroupMessage user gInfo ms $ XFileCancel sharedMsgId + Just _ -> throwChatError $ CEFileInternal "invalid chat ref for file transfer" + ci <- withFastStore $ \db -> lookupChatItemByFileId db vr user fileId + pure $ CRSndFileCancelled user ci ftm fts + where + fileCancelledOrCompleteSMP SndFileTransfer {fileStatus = s} = + s == FSCancelled || (s == FSComplete && isNothing xftpSndFile) + FTRcv ftr@RcvFileTransfer {cancelled, fileStatus, xftpRcvFile} + | cancelled -> throwChatError $ CEFileCancel fileId "file already cancelled" + | rcvFileComplete fileStatus -> throwChatError $ CEFileCancel fileId "file transfer is complete" + | otherwise -> case xftpRcvFile of + Nothing -> do + cancelRcvFileTransfer user ftr >>= mapM_ deleteAgentConnectionAsync + ci <- withFastStore $ \db -> lookupChatItemByFileId db vr user fileId + pure $ CRRcvFileCancelled user ci ftr + Just XFTPRcvFile {agentRcvFileId} -> do + forM_ (liveRcvFileTransferPath ftr) $ \filePath -> do + fsFilePath <- lift $ toFSFilePath filePath + liftIO $ removeFile fsFilePath `catchAll_` pure () + lift . forM_ agentRcvFileId $ \(AgentRcvFileId aFileId) -> + withAgent' (`xftpDeleteRcvFile` aFileId) + aci_ <- resetRcvCIFileStatus user fileId CIFSRcvInvitation + pure $ CRRcvFileCancelled user aci_ ftr + FileStatus fileId -> withUser $ \user -> do + withFastStore (\db -> lookupChatItemByFileId db vr user fileId) >>= \case + Nothing -> do + fileStatus <- withFastStore $ \db -> getFileTransferProgress db user fileId + pure $ CRFileTransferStatus user fileStatus + Just ci@(AChatItem _ _ _ ChatItem {file}) -> case file of + Just CIFile {fileProtocol = FPLocal} -> + throwChatError $ CECommandError "not supported for local files" + Just CIFile {fileProtocol = FPXFTP} -> + pure $ CRFileTransferStatusXFTP user ci + _ -> do + fileStatus <- withFastStore $ \db -> getFileTransferProgress db user fileId + pure $ CRFileTransferStatus user fileStatus + ShowProfile -> withUser $ \user@User {profile} -> pure $ CRUserProfile user (fromLocalProfile profile) + UpdateProfile displayName fullName -> withUser $ \user@User {profile} -> do + let p = (fromLocalProfile profile :: Profile) {displayName = displayName, fullName = fullName} + updateProfile user p + UpdateProfileImage image -> withUser $ \user@User {profile} -> do + let p = (fromLocalProfile profile :: Profile) {image} + updateProfile user p + ShowProfileImage -> withUser $ \user@User {profile} -> pure $ CRUserProfileImage user $ fromLocalProfile profile + SetUserFeature (ACF f) allowed -> withUser $ \user@User {profile} -> do + let p = (fromLocalProfile profile :: Profile) {preferences = Just . setPreference f (Just allowed) $ preferences' user} + updateProfile user p + SetContactFeature (ACF f) cName allowed_ -> withUser $ \user -> do + ct@Contact {userPreferences} <- withFastStore $ \db -> getContactByName db vr user cName + let prefs' = setPreference f allowed_ $ Just userPreferences + updateContactPrefs user ct prefs' + SetGroupFeature (AGFNR f) gName enabled -> + updateGroupProfileByName gName $ \p -> + p {groupPreferences = Just . setGroupPreference f enabled $ groupPreferences p} + SetGroupFeatureRole (AGFR f) gName enabled role -> + updateGroupProfileByName gName $ \p -> + p {groupPreferences = Just . setGroupPreferenceRole f enabled role $ groupPreferences p} + SetUserTimedMessages onOff -> withUser $ \user@User {profile} -> do + let allowed = if onOff then FAYes else FANo + pref = TimedMessagesPreference allowed Nothing + p = (fromLocalProfile profile :: Profile) {preferences = Just . setPreference' SCFTimedMessages (Just pref) $ preferences' user} + updateProfile user p + SetContactTimedMessages cName timedMessagesEnabled_ -> withUser $ \user -> do + ct@Contact {userPreferences = userPreferences@Preferences {timedMessages}} <- withFastStore $ \db -> getContactByName db vr user cName + let currentTTL = timedMessages >>= \TimedMessagesPreference {ttl} -> ttl + pref_ = tmeToPref currentTTL <$> timedMessagesEnabled_ + prefs' = setPreference' SCFTimedMessages pref_ $ Just userPreferences + updateContactPrefs user ct prefs' + SetGroupTimedMessages gName ttl_ -> do + let pref = uncurry TimedMessagesGroupPreference $ maybe (FEOff, Just 86400) (\ttl -> (FEOn, Just ttl)) ttl_ + updateGroupProfileByName gName $ \p -> + p {groupPreferences = Just . setGroupPreference' SGFTimedMessages pref $ groupPreferences p} + SetLocalDeviceName name -> chatWriteVar localDeviceName name >> ok_ + ListRemoteHosts -> CRRemoteHostList <$> listRemoteHosts + SwitchRemoteHost rh_ -> CRCurrentRemoteHost <$> switchRemoteHost rh_ + StartRemoteHost rh_ ca_ bp_ -> do + (localAddrs, remoteHost_, inv@RCSignedInvitation {invitation = RCInvitation {port}}) <- startRemoteHost rh_ ca_ bp_ + pure CRRemoteHostStarted {remoteHost_, invitation = decodeLatin1 $ strEncode inv, ctrlPort = show port, localAddrs} + StopRemoteHost rh_ -> closeRemoteHost rh_ >> ok_ + DeleteRemoteHost rh -> deleteRemoteHost rh >> ok_ + StoreRemoteFile rh encrypted_ localPath -> CRRemoteFileStored rh <$> storeRemoteFile rh encrypted_ localPath + GetRemoteFile rh rf -> getRemoteFile rh rf >> ok_ + ConnectRemoteCtrl inv -> withUser_ $ do + (remoteCtrl_, ctrlAppInfo) <- connectRemoteCtrlURI inv + pure CRRemoteCtrlConnecting {remoteCtrl_, ctrlAppInfo, appVersion = currentAppVersion} + FindKnownRemoteCtrl -> withUser_ $ findKnownRemoteCtrl >> ok_ + ConfirmRemoteCtrl rcId -> withUser_ $ do + (rc, ctrlAppInfo) <- confirmRemoteCtrl rcId + pure CRRemoteCtrlConnecting {remoteCtrl_ = Just rc, ctrlAppInfo, appVersion = currentAppVersion} + VerifyRemoteCtrlSession sessId -> withUser_ $ CRRemoteCtrlConnected <$> verifyRemoteCtrlSession (execChatCommand Nothing) sessId + StopRemoteCtrl -> withUser_ $ stopRemoteCtrl >> ok_ + ListRemoteCtrls -> withUser_ $ CRRemoteCtrlList <$> listRemoteCtrls + DeleteRemoteCtrl rc -> withUser_ $ deleteRemoteCtrl rc >> ok_ + APIUploadStandaloneFile userId file@CryptoFile {filePath} -> withUserId userId $ \user -> do + fsFilePath <- lift $ toFSFilePath filePath + fileSize <- liftIO $ CF.getFileContentsSize file {filePath = fsFilePath} + when (fileSize > toInteger maxFileSizeHard) $ throwChatError $ CEFileSize filePath + (_, _, fileTransferMeta) <- xftpSndFileTransfer_ user file fileSize 1 Nothing + pure CRSndStandaloneFileCreated {user, fileTransferMeta} + APIStandaloneFileInfo FileDescriptionURI {clientData} -> pure . CRStandaloneFileInfo $ clientData >>= J.decodeStrict . encodeUtf8 + APIDownloadStandaloneFile userId uri file -> withUserId userId $ \user -> do + ft <- receiveViaURI user uri file + pure $ CRRcvStandaloneFileCreated user ft + QuitChat -> liftIO exitSuccess + ShowVersion -> do + -- simplexmqCommitQ makes iOS builds crash m( + let versionInfo = coreVersionInfo "" + chatMigrations <- map upMigration <$> withFastStore' getCurrentMigrations + agentMigrations <- withAgent getAgentMigrations + pure $ CRVersionInfo {versionInfo, chatMigrations, agentMigrations} + DebugLocks -> lift $ do + chatLockName <- atomically . tryReadTMVar =<< asks chatLock + chatEntityLocks <- getLocks =<< asks entityLocks + agentLocks <- withAgent' debugAgentLocks + pure CRDebugLocks {chatLockName, chatEntityLocks, agentLocks} + where + getLocks ls = atomically $ M.mapKeys enityLockString . M.mapMaybe id <$> (mapM tryReadTMVar =<< readTVar ls) + enityLockString cle = case cle of + CLInvitation bs -> "Invitation " <> B.unpack bs + CLConnection connId -> "Connection " <> show connId + CLContact ctId -> "Contact " <> show ctId + CLGroup gId -> "Group " <> show gId + CLUserContact ucId -> "UserContact " <> show ucId + CLFile fId -> "File " <> show fId + DebugEvent event -> toView event >> ok_ + GetAgentSubsTotal userId -> withUserId userId $ \user -> do + users <- withStore' $ \db -> getUsers db + let userIds = map aUserId $ filter (\u -> isNothing (viewPwdHash u) || aUserId u == aUserId user) users + (subsTotal, hasSession) <- lift $ withAgent' $ \a -> getAgentSubsTotal a userIds + pure $ CRAgentSubsTotal user subsTotal hasSession + GetAgentServersSummary userId -> withUserId userId $ \user -> do + agentServersSummary <- lift $ withAgent' getAgentServersSummary + withStore' $ \db -> do + users <- getUsers db + smpServers <- getServers db user SPSMP + xftpServers <- getServers db user SPXFTP + let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers _defaultNtfServers + pure $ CRAgentServersSummary user presentedServersSummary + where + getServers :: ProtocolTypeI p => DB.Connection -> User -> SProtocolType p -> IO [ProtocolServer p] + getServers db user p = map (\UserServer {server} -> protoServer server) <$> getProtocolServers db p user + ResetAgentServersStats -> withAgent resetAgentServersStats >> ok_ + GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary + GetAgentWorkersDetails -> lift $ CRAgentWorkersDetails <$> withAgent' getAgentWorkersDetails + GetAgentSubs -> lift $ summary <$> withAgent' getAgentSubscriptions + where + summary SubscriptionsInfo {activeSubscriptions, pendingSubscriptions, removedSubscriptions} = + CRAgentSubs + { activeSubs = foldl' countSubs M.empty activeSubscriptions, + pendingSubs = foldl' countSubs M.empty pendingSubscriptions, + removedSubs = foldl' accSubErrors M.empty removedSubscriptions + } + where + countSubs m SubInfo {server} = M.alter (Just . maybe 1 (+ 1)) server m + accSubErrors m = \case + SubInfo {server, subError = Just e} -> M.alter (Just . maybe [e] (e :)) server m + _ -> m + GetAgentSubsDetails -> lift $ CRAgentSubsDetails <$> withAgent' getAgentSubscriptions + GetAgentQueuesInfo -> lift $ CRAgentQueuesInfo <$> withAgent' getAgentQueuesInfo + -- CustomChatCommand is unsupported, it can be processed in preCmdHook + -- in a modified CLI app or core - the hook should return Either (Either ChatError ChatResponse) ChatCommand, + -- where Left means command result, and Right – some other command to be processed by this function. + CustomChatCommand _cmd -> withUser $ \_ -> throwCmdError "not supported" + where + procCmd :: CM ChatResponse -> CM ChatResponse + procCmd = id + {-# INLINE procCmd #-} + ok_ = pure $ CRCmdOk Nothing + ok = pure . CRCmdOk . Just + getChatRef :: User -> ChatName -> CM ChatRef + getChatRef user (ChatName cType name) = + ChatRef cType <$> case cType of + CTDirect -> withFastStore $ \db -> getContactIdByName db user name + CTGroup -> withFastStore $ \db -> getGroupIdByName db user name + CTLocal + | name == "" -> withFastStore (`getUserNoteFolderId` user) + | otherwise -> throwChatError $ CECommandError "not supported" + _ -> throwChatError $ CECommandError "not supported" + getChatRefAndMentions :: User -> ChatName -> Text -> CM (ChatRef, Map MemberName GroupMemberId) + getChatRefAndMentions user cName msg = do + chatRef@(ChatRef cType chatId) <- getChatRef user cName + (chatRef,) <$> case cType of + CTGroup -> withFastStore' $ \db -> getMessageMentions db user chatId msg + _ -> pure [] +#if !defined(dbPostgres) + checkChatStopped :: CM ChatResponse -> CM ChatResponse + checkChatStopped a = asks agentAsync >>= readTVarIO >>= maybe a (const $ throwChatError CEChatNotStopped) + setStoreChanged :: CM () + setStoreChanged = asks chatStoreChanged >>= atomically . (`writeTVar` True) + withStoreChanged :: CM () -> CM ChatResponse + withStoreChanged a = checkChatStopped $ a >> setStoreChanged >> ok_ +#endif + checkStoreNotChanged :: CM ChatResponse -> CM ChatResponse + checkStoreNotChanged = ifM (asks chatStoreChanged >>= readTVarIO) (throwChatError CEChatStoreChanged) + withUserName :: UserName -> (UserId -> ChatCommand) -> CM ChatResponse + withUserName uName cmd = withFastStore (`getUserIdByName` uName) >>= processChatCommand . cmd + withContactName :: ContactName -> (ContactId -> ChatCommand) -> CM ChatResponse + withContactName cName cmd = withUser $ \user -> + withFastStore (\db -> getContactIdByName db user cName) >>= processChatCommand . cmd + withMemberName :: GroupName -> ContactName -> (GroupId -> GroupMemberId -> ChatCommand) -> CM ChatResponse + withMemberName gName mName cmd = withUser $ \user -> + getGroupAndMemberId user gName mName >>= processChatCommand . uncurry cmd + getConnectionCode :: ConnId -> CM Text + getConnectionCode connId = verificationCode <$> withAgent (`getConnectionRatchetAdHash` connId) + verifyConnectionCode :: User -> Connection -> Maybe Text -> CM ChatResponse + verifyConnectionCode user conn@Connection {connId} (Just code) = do + code' <- getConnectionCode $ aConnId conn + let verified = sameVerificationCode code code' + when verified . withFastStore' $ \db -> setConnectionVerified db user connId $ Just code' + pure $ CRConnectionVerified user verified code' + verifyConnectionCode user conn@Connection {connId} _ = do + code' <- getConnectionCode $ aConnId conn + withFastStore' $ \db -> setConnectionVerified db user connId Nothing + pure $ CRConnectionVerified user False code' + getSentChatItemIdByText :: User -> ChatRef -> Text -> CM Int64 + getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId) msg = case cType of + CTDirect -> withFastStore $ \db -> getDirectChatItemIdByText db userId cId SMDSnd msg + CTGroup -> withFastStore $ \db -> getGroupChatItemIdByText db user cId (Just localDisplayName) msg + CTLocal -> withFastStore $ \db -> getLocalChatItemIdByText db user cId SMDSnd msg + _ -> throwChatError $ CECommandError "not supported" + getChatItemIdByText :: User -> ChatRef -> Text -> CM Int64 + getChatItemIdByText user (ChatRef cType cId) msg = case cType of + CTDirect -> withFastStore $ \db -> getDirectChatItemIdByText' db user cId msg + CTGroup -> withFastStore $ \db -> getGroupChatItemIdByText' db user cId msg + CTLocal -> withFastStore $ \db -> getLocalChatItemIdByText' db user cId msg + _ -> throwChatError $ CECommandError "not supported" + connectViaContact :: User -> IncognitoEnabled -> CreatedLinkContact -> CM ChatResponse + connectViaContact user@User {userId} incognito (CCLink cReq@(CRContactUri ConnReqUriData {crClientData}) sLnk) = withInvitationLock "connectViaContact" (strEncode cReq) $ do + let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli + cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq + case groupLinkId of + -- contact address + Nothing -> + withFastStore' (\db -> getConnReqContactXContactId db vr user cReqHash) >>= \case + (Just contact, _) -> pure $ CRContactAlreadyExists user contact + (_, xContactId_) -> procCmd $ do + let randomXContactId = XContactId <$> drgRandomBytes 16 + xContactId <- maybe randomXContactId pure xContactId_ + connect' Nothing cReqHash xContactId False + -- group link + Just gLinkId -> + withFastStore' (\db -> getConnReqContactXContactId db vr user cReqHash) >>= \case + (Just _contact, _) -> procCmd $ do + -- allow repeat contact request + newXContactId <- XContactId <$> drgRandomBytes 16 + connect' (Just gLinkId) cReqHash newXContactId True + (_, xContactId_) -> procCmd $ do + let randomXContactId = XContactId <$> drgRandomBytes 16 + xContactId <- maybe randomXContactId pure xContactId_ + connect' (Just gLinkId) cReqHash xContactId True + where + connect' groupLinkId cReqHash xContactId inGroup = do + let pqSup = if inGroup then PQSupportOff else PQSupportOn + (connId, chatV) <- prepareContact user cReq pqSup + -- [incognito] generate profile to send + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + subMode <- chatReadVar subscriptionMode + let sLnk' = serverShortLink <$> sLnk + conn@PendingContactConnection {pccConnId} <- withFastStore' $ \db -> createConnReqConnection db userId connId cReqHash sLnk' xContactId incognitoProfile groupLinkId subMode chatV pqSup + joinContact user pccConnId connId cReq incognitoProfile xContactId inGroup pqSup chatV + pure $ CRSentInvitation user conn incognitoProfile + connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> CreatedLinkContact -> CM ChatResponse + connectContactViaAddress user incognito ct (CCLink cReq shortLink) = + withInvitationLock "connectContactViaAddress" (strEncode cReq) $ do + newXContactId <- XContactId <$> drgRandomBytes 16 + let pqSup = PQSupportOn + (connId, chatV) <- prepareContact user cReq pqSup + let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq + -- [incognito] generate profile to send + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + subMode <- chatReadVar subscriptionMode + (pccConnId, ct') <- withFastStore $ \db -> createAddressContactConnection db vr user ct connId cReqHash shortLink newXContactId incognitoProfile subMode chatV pqSup + joinContact user pccConnId connId cReq incognitoProfile newXContactId False pqSup chatV + pure $ CRSentInvitationToContact user ct' incognitoProfile + prepareContact :: User -> ConnReqContact -> PQSupport -> CM (ConnId, VersionChat) + prepareContact user cReq pqSup = do + -- 0) toggle disabled - PQSupportOff + -- 1) toggle enabled, address supports PQ (connRequestPQSupport returns Just True) - PQSupportOn, enable support with compression + -- 2) toggle enabled, address doesn't support PQ - PQSupportOn but without compression, with version range indicating support + lift (withAgent' $ \a -> connRequestPQSupport a pqSup cReq) >>= \case + Nothing -> throwChatError CEInvalidConnReq + Just (agentV, _) -> do + let chatV = agentToChatVersion agentV + connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup + pure (connId, chatV) + joinContact :: User -> Int64 -> ConnId -> ConnReqContact -> Maybe Profile -> XContactId -> Bool -> PQSupport -> VersionChat -> CM () + joinContact user pccConnId connId cReq incognitoProfile xContactId inGroup pqSup chatV = do + let profileToSend = userProfileToSend user incognitoProfile Nothing inGroup + dm <- encodeConnInfoPQ pqSup chatV (XContact profileToSend $ Just xContactId) + subMode <- chatReadVar subscriptionMode + joinPreparedAgentConnection user pccConnId connId cReq dm pqSup subMode + joinPreparedAgentConnection :: User -> Int64 -> ConnId -> ConnectionRequestUri m -> ByteString -> PQSupport -> SubscriptionMode -> CM () + joinPreparedAgentConnection user pccConnId connId cReq connInfo pqSup subMode = do + void (withAgent $ \a -> joinConnection a (aUserId user) connId True cReq connInfo pqSup subMode) + `catchChatError` \e -> do + withFastStore' $ \db -> deleteConnectionRecord db user pccConnId + withAgent $ \a -> deleteConnectionAsync a False connId + throwError e + contactMember :: Contact -> [GroupMember] -> Maybe GroupMember + contactMember Contact {contactId} = + find $ \GroupMember {memberContactId = cId, memberStatus = s} -> + cId == Just contactId && s /= GSMemRejected && s /= GSMemRemoved && s /= GSMemLeft + checkSndFile :: CryptoFile -> CM Integer + checkSndFile (CryptoFile f cfArgs) = do + fsFilePath <- lift $ toFSFilePath f + unlessM (doesFileExist fsFilePath) . throwChatError $ CEFileNotFound f + fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cfArgs + when (fromInteger fileSize > maxFileSize) $ throwChatError $ CEFileSize f + pure fileSize + updateProfile :: User -> Profile -> CM ChatResponse + updateProfile user p' = updateProfile_ user p' $ withFastStore $ \db -> updateUserProfile db user p' + updateProfile_ :: User -> Profile -> CM User -> CM ChatResponse + updateProfile_ user@User {profile = p@LocalProfile {displayName = n}} p'@Profile {displayName = n'} updateUser + | p' == fromLocalProfile p = pure $ CRUserProfileNoChange user + | otherwise = do + when (n /= n') $ checkValidName n' + -- read contacts before user update to correctly merge preferences + contacts <- withFastStore' $ \db -> getUserContacts db vr user + user' <- updateUser + asks currentUser >>= atomically . (`writeTVar` Just user') + withChatLock "updateProfile" . procCmd $ do + let changedCts_ = L.nonEmpty $ foldr (addChangedProfileContact user') [] contacts + summary <- case changedCts_ of + Nothing -> pure $ UserProfileUpdateSummary 0 0 [] + Just changedCts -> do + let idsEvts = L.map ctSndEvent changedCts + msgReqs_ <- lift $ L.zipWith ctMsgReq changedCts <$> createSndMessages idsEvts + (errs, cts) <- partitionEithers . L.toList . L.zipWith (second . const) changedCts <$> deliverMessagesB msgReqs_ + unless (null errs) $ toView $ CEvtChatErrors errs + let changedCts' = filter (\ChangedProfileContact {ct, ct'} -> directOrUsed ct' && mergedPreferences ct' /= mergedPreferences ct) cts + lift $ createContactsSndFeatureItems user' changedCts' + pure + UserProfileUpdateSummary + { updateSuccesses = length cts, + updateFailures = length errs, + changedContacts = map (\ChangedProfileContact {ct'} -> ct') changedCts' + } + pure $ CRUserProfileUpdated user' (fromLocalProfile p) p' summary + where + -- [incognito] filter out contacts with whom user has incognito connections + addChangedProfileContact :: User -> Contact -> [ChangedProfileContact] -> [ChangedProfileContact] + addChangedProfileContact user' ct changedCts = case contactSendConn_ ct' of + Right conn + | not (connIncognito conn) && mergedProfile' /= mergedProfile -> + ChangedProfileContact ct ct' mergedProfile' conn : changedCts + _ -> changedCts + where + mergedProfile = userProfileToSend user Nothing (Just ct) False + ct' = updateMergedPreferences user' ct + mergedProfile' = userProfileToSend user' Nothing (Just ct') False + ctSndEvent :: ChangedProfileContact -> (ConnOrGroupId, ChatMsgEvent 'Json) + ctSndEvent ChangedProfileContact {mergedProfile', conn = Connection {connId}} = (ConnectionId connId, XInfo mergedProfile') + ctMsgReq :: ChangedProfileContact -> Either ChatError SndMessage -> Either ChatError ChatMsgReq + ctMsgReq ChangedProfileContact {conn} = + fmap $ \SndMessage {msgId, msgBody} -> + (conn, MsgFlags {notification = hasNotification XInfo_}, (vrValue msgBody, [msgId])) + updateContactPrefs :: User -> Contact -> Preferences -> CM ChatResponse + updateContactPrefs _ ct@Contact {activeConn = Nothing} _ = throwChatError $ CEContactNotActive ct + updateContactPrefs user@User {userId} ct@Contact {activeConn = Just Connection {customUserProfileId}, userPreferences = contactUserPrefs} contactUserPrefs' + | contactUserPrefs == contactUserPrefs' = pure $ CRContactPrefsUpdated user ct ct + | otherwise = do + assertDirectAllowed user MDSnd ct XInfo_ + ct' <- withStore' $ \db -> updateContactUserPreferences db user ct contactUserPrefs' + incognitoProfile <- forM customUserProfileId $ \profileId -> withStore $ \db -> getProfileById db userId profileId + let mergedProfile = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct) False + mergedProfile' = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct') False + when (mergedProfile' /= mergedProfile) $ + withContactLock "updateProfile" (contactId' ct) $ do + void (sendDirectContactMessage user ct' $ XInfo mergedProfile') `catchChatError` eToView + lift . when (directOrUsed ct') $ createSndFeatureItems user ct ct' + pure $ CRContactPrefsUpdated user ct ct' + runUpdateGroupProfile :: User -> Group -> GroupProfile -> CM ChatResponse + runUpdateGroupProfile user (Group g@GroupInfo {businessChat, groupProfile = p@GroupProfile {displayName = n}} ms) p'@GroupProfile {displayName = n'} = do + assertUserGroupRole g GROwner + when (n /= n') $ checkValidName n' + g' <- withStore $ \db -> updateGroupProfile db user g p' + msg <- case businessChat of + Just BusinessChatInfo {businessId} -> do + let (newMs, oldMs) = partition (\m -> maxVersion (memberChatVRange m) >= businessChatPrefsVersion) ms + -- this is a fallback to send the members with the old version correct profile of the business when preferences change + unless (null oldMs) $ do + GroupMember {memberProfile = LocalProfile {displayName, fullName, image}} <- + withStore $ \db -> getGroupMemberByMemberId db vr user g businessId + let p'' = p' {displayName, fullName, image} :: GroupProfile + -- TODO [knocking] send to pending approval members (move `memberCurrent` filter from sendGroupMessages_ to call sites) + void $ sendGroupMessage user g' oldMs (XGrpInfo p'') + let ps' = fromMaybe defaultBusinessGroupPrefs $ groupPreferences p' + sendGroupMessage user g' newMs $ XGrpPrefs ps' + Nothing -> sendGroupMessage user g' ms (XGrpInfo p') + let cd = CDGroupSnd g' + unless (sameGroupProfileInfo p p') $ do + ci <- saveSndChatItem user cd msg (CISndGroupEvent $ SGEGroupUpdated p') + toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat g') ci] + createGroupFeatureChangedItems user cd CISndGroupFeature g g' + pure $ CRGroupUpdated user g g' Nothing + checkValidName :: GroupName -> CM () + checkValidName displayName = do + when (T.null displayName) $ throwChatError CEInvalidDisplayName {displayName, validName = ""} + let validName = T.pack $ mkValidName $ T.unpack displayName + when (displayName /= validName) $ throwChatError CEInvalidDisplayName {displayName, validName} + assertUserGroupRole :: GroupInfo -> GroupMemberRole -> CM () + assertUserGroupRole g@GroupInfo {membership} requiredRole = do + let GroupMember {memberRole = membershipMemRole} = membership + when (membershipMemRole < requiredRole) $ throwChatError $ CEGroupUserRole g requiredRole + when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined g) + when (memberRemoved membership) $ throwChatError CEGroupMemberUserRemoved + unless (memberActive membership) $ throwChatError CEGroupMemberNotActive + delGroupChatItemsForMembers :: User -> GroupInfo -> [GroupMember] -> [CChatItem 'CTGroup] -> CM [ChatItemDeletion] + delGroupChatItemsForMembers user gInfo ms items = do + assertDeletable gInfo items + assertUserGroupRole gInfo GRAdmin -- TODO GRModerator when most users migrate + let msgMemIds = itemsMsgMemIds gInfo items + events = L.nonEmpty $ map (\(msgId, memId) -> XMsgDel msgId (Just memId)) msgMemIds + -- TODO [knocking] validate: only current members or only single pending approval member, + -- TODO or prohibit pending approval members (only moderation and reports use this) + mapM_ (sendGroupMessages user gInfo ms) events + delGroupChatItems user gInfo items True + where + assertDeletable :: GroupInfo -> [CChatItem 'CTGroup] -> CM () + assertDeletable GroupInfo {membership = GroupMember {memberRole = membershipMemRole}} items' = + unless (all itemDeletable items') $ throwChatError CEInvalidChatItemDelete + where + itemDeletable :: CChatItem 'CTGroup -> Bool + itemDeletable (CChatItem _ ChatItem {chatDir, meta = CIMeta {itemSharedMsgId}}) = + case chatDir of + CIGroupRcv GroupMember {memberRole} -> membershipMemRole >= memberRole && isJust itemSharedMsgId + CIGroupSnd -> isJust itemSharedMsgId + itemsMsgMemIds :: GroupInfo -> [CChatItem 'CTGroup] -> [(SharedMsgId, MemberId)] + itemsMsgMemIds GroupInfo {membership = GroupMember {memberId = membershipMemId}} = mapMaybe itemMsgMemIds + where + itemMsgMemIds :: CChatItem 'CTGroup -> Maybe (SharedMsgId, MemberId) + itemMsgMemIds (CChatItem _ ChatItem {chatDir, meta = CIMeta {itemSharedMsgId}}) = + join <$> forM itemSharedMsgId $ \msgId -> Just $ case chatDir of + CIGroupRcv GroupMember {memberId} -> (msgId, memberId) + CIGroupSnd -> (msgId, membershipMemId) + + delGroupChatItems :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Bool -> CM [ChatItemDeletion] + delGroupChatItems user gInfo@GroupInfo {membership} items moderation = do + deletedTs <- liftIO getCurrentTime + when moderation $ do + ciIds <- concat <$> withStore' (\db -> forM items $ \(CChatItem _ ci) -> markMessageReportsDeleted db user gInfo ci membership deletedTs) + unless (null ciIds) $ toView $ CEvtGroupChatItemsDeleted user gInfo ciIds True (Just membership) + let m = if moderation then Just membership else Nothing + if groupFeatureMemberAllowed SGFFullDelete membership gInfo + then deleteGroupCIs user gInfo items m deletedTs + else markGroupCIsDeleted user gInfo items m deletedTs + updateGroupProfileByName :: GroupName -> (GroupProfile -> GroupProfile) -> CM ChatResponse + updateGroupProfileByName gName update = withUser $ \user -> do + g@(Group GroupInfo {groupProfile = p} _) <- withStore $ \db -> + getGroupIdByName db user gName >>= getGroup db vr user + runUpdateGroupProfile user g $ update p + withCurrentCall :: ContactId -> (User -> Contact -> Call -> CM (Maybe Call)) -> CM ChatResponse + withCurrentCall ctId action = do + (user, ct) <- withStore $ \db -> do + user <- getUserByContactId db ctId + (user,) <$> getContact db vr user ctId + calls <- asks currentCalls + withContactLock "currentCall" ctId $ + atomically (TM.lookup ctId calls) >>= \case + Nothing -> throwChatError CENoCurrentCall + Just call@Call {contactId} + | ctId == contactId -> do + call_ <- action user ct call + case call_ of + Just call' -> do + unless (isRcvInvitation call') $ withStore' $ \db -> deleteCalls db user ctId + atomically $ TM.insert ctId call' calls + _ -> do + withStore' $ \db -> deleteCalls db user ctId + atomically $ TM.delete ctId calls + ok user + | otherwise -> throwChatError $ CECallContact contactId + withServerProtocol :: ProtocolTypeI p => SProtocolType p -> (UserProtocol p => CM a) -> CM a + withServerProtocol p action = case userProtocol p of + Just Dict -> action + _ -> throwChatError $ CEServerProtocol $ AProtocolType p + validateAllUsersServers :: UserServersClass u => Int64 -> [u] -> CM [UserServersError] + validateAllUsersServers currUserId userServers = withFastStore $ \db -> do + users' <- filter (\User {userId} -> userId /= currUserId) <$> liftIO (getUsers db) + others <- mapM (getUserOperatorServers db) users' + pure $ validateUserServers userServers others + where + getUserOperatorServers :: DB.Connection -> User -> ExceptT StoreError IO (User, [UserOperatorServers]) + getUserOperatorServers db user = do + uss <- liftIO . groupByOperator =<< getUserServers db user + pure (user, map updatedUserSrvs uss) + updatedUserSrvs uss = uss {operator = updatedOp <$> operator' uss} :: UserOperatorServers + updatedOp op = fromMaybe op $ find matchingOp $ mapMaybe operator' userServers + where + matchingOp op' = operatorId op' == operatorId op + forwardFile :: ChatName -> FileTransferId -> (ChatName -> CryptoFile -> ChatCommand) -> CM ChatResponse + forwardFile chatName fileId sendCommand = withUser $ \user -> do + withStore (\db -> getFileTransfer db user fileId) >>= \case + FTRcv RcvFileTransfer {fileStatus = RFSComplete RcvFileInfo {filePath}, cryptoArgs} -> forward filePath cryptoArgs + FTSnd {fileTransferMeta = FileTransferMeta {filePath, xftpSndFile}} -> forward filePath $ xftpSndFile >>= \XFTPSndFile {cryptoArgs} -> cryptoArgs + _ -> throwChatError CEFileNotReceived {fileId} + where + forward path cfArgs = processChatCommand . sendCommand chatName $ CryptoFile path cfArgs + getGroupAndMemberId :: User -> GroupName -> ContactName -> CM (GroupId, GroupMemberId) + getGroupAndMemberId user gName groupMemberName = + withStore $ \db -> do + groupId <- getGroupIdByName db user gName + groupMemberId <- getGroupMemberIdByName db user groupId groupMemberName + pure (groupId, groupMemberId) + 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 + currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo + let GroupMember {memberRole = userRole, memberId = userMemberId} = membership + groupInv = + GroupInvitation + { fromMember = MemberIdRole userMemberId userRole, + invitedMember = MemberIdRole memberId memRole, + connRequest = cReq, + groupProfile, + business = businessChat, + groupLinkId = Nothing, + groupSize = Just currentMemCount + } + (msg, _) <- sendDirectContactMessage user ct $ XGrpInv groupInv + let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole + timed_ <- contactCITimed ct + ci <- saveSndChatItem' user (CDDirectSnd ct) msg content Nothing Nothing Nothing timed_ False + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] + forM_ (timed_ >>= timedDeleteAt') $ + startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) + drgRandomBytes :: Int -> CM ByteString + drgRandomBytes n = asks random >>= atomically . C.randomBytes n + privateGetUser :: UserId -> CM User + privateGetUser userId = + tryChatError (withStore (`getUser` userId)) >>= \case + Left _ -> throwChatError CEUserUnknown + Right user -> pure user + validateUserPassword :: User -> User -> Maybe UserPwd -> CM () + validateUserPassword = validateUserPassword_ . Just + validateUserPassword_ :: Maybe User -> User -> Maybe UserPwd -> CM () + validateUserPassword_ user_ User {userId = userId', viewPwdHash} viewPwd_ = + forM_ viewPwdHash $ \pwdHash -> + let userId_ = (\User {userId} -> userId) <$> user_ + pwdOk = case viewPwd_ of + Nothing -> userId_ == Just userId' + Just (UserPwd viewPwd) -> validPassword viewPwd pwdHash + in unless pwdOk $ throwChatError CEUserUnknown + validPassword :: Text -> UserPwdHash -> Bool + validPassword pwd UserPwdHash {hash = B64UrlByteString hash, salt = B64UrlByteString salt} = + hash == C.sha512Hash (encodeUtf8 pwd <> salt) + setUserNotifications :: UserId -> Bool -> CM ChatResponse + setUserNotifications userId' showNtfs = withUser $ \user -> do + user' <- privateGetUser userId' + case viewPwdHash user' of + Just _ -> throwChatError $ CEHiddenUserAlwaysMuted userId' + _ -> setUserPrivacy user user' {showNtfs} + setUserPrivacy :: User -> User -> CM ChatResponse + setUserPrivacy user@User {userId} user'@User {userId = userId'} + | userId == userId' = do + asks currentUser >>= atomically . (`writeTVar` Just user') + withFastStore' (`updateUserPrivacy` user') + pure $ CRUserPrivacy {user = user', updatedUser = user'} + | otherwise = do + withFastStore' (`updateUserPrivacy` user') + pure $ CRUserPrivacy {user, updatedUser = user'} + checkDeleteChatUser :: User -> CM () + checkDeleteChatUser user@User {userId} = do + users <- withFastStore' getUsers + let otherVisible = filter (\User {userId = userId', viewPwdHash} -> userId /= userId' && isNothing viewPwdHash) users + when (activeUser user && length otherVisible > 0) $ throwChatError (CECantDeleteActiveUser userId) + deleteChatUser :: User -> Bool -> CM ChatResponse + deleteChatUser user delSMPQueues = do + filesInfo <- withFastStore' (`getUserFileInfo` user) + deleteCIFiles user filesInfo + withAgent (\a -> deleteUser a (aUserId user) delSMPQueues) + `catchChatError` \case + e@(ChatErrorAgent NO_USER _) -> eToView e + e -> throwError e + withFastStore' (`deleteUserRecord` user) + when (activeUser user) $ chatWriteVar currentUser Nothing + ok_ + updateChatSettings :: ChatName -> (ChatSettings -> ChatSettings) -> CM ChatResponse + updateChatSettings (ChatName cType name) updateSettings = withUser $ \user -> do + (chatId, chatSettings) <- case cType of + CTDirect -> withFastStore $ \db -> do + ctId <- getContactIdByName db user name + Contact {chatSettings} <- getContact db vr user ctId + pure (ctId, chatSettings) + CTGroup -> + withFastStore $ \db -> do + gId <- getGroupIdByName db user name + GroupInfo {chatSettings} <- getGroupInfo db vr user gId + pure (gId, chatSettings) + _ -> throwChatError $ CECommandError "not supported" + processChatCommand $ APISetChatSettings (ChatRef cType chatId) $ updateSettings chatSettings + connectPlan :: User -> AConnectionLink -> CM (ACreatedConnLink, ConnectionPlan) + connectPlan user (ACL SCMInvitation cLink) = case cLink of + CLFull cReq -> invitationReqAndPlan cReq Nothing + CLShort l -> do + let l' = serverShortLink l + withFastStore' (\db -> getConnectionEntityViaShortLink db vr user l') >>= \case + Just (cReq, ent) -> + (ACCL SCMInvitation (CCLink cReq (Just l')),) <$> (invitationEntityPlan ent `catchChatError` (pure . CPError)) + Nothing -> getShortLinkConnReq user l' >>= (`invitationReqAndPlan` Just l') + where + invitationReqAndPlan cReq sLnk_ = do + plan <- inviationRequestPlan user cReq `catchChatError` (pure . CPError) + pure (ACCL SCMInvitation (CCLink cReq sLnk_), plan) + connectPlan user (ACL SCMContact cLink) = case cLink of + CLFull cReq -> contactReqAndPlan cReq Nothing + CLShort l@(CSLContact _ ct _ _) -> do + let l' = serverShortLink l + case ct of + CCTContact -> + withFastStore' (\db -> getUserContactLinkViaShortLink db user l') >>= \case + Just (UserContactLink (CCLink cReq _) _) -> pure (ACCL SCMContact $ CCLink cReq (Just l'), CPContactAddress CAPOwnLink) + Nothing -> getShortLinkConnReq user l' >>= (`contactReqAndPlan` Just l') + CCTGroup -> + withFastStore' (\db -> getGroupInfoViaUserShortLink db vr user l') >>= \case + Just (cReq, g) -> pure (ACCL SCMContact $ CCLink cReq (Just l'), CPGroupLink (GLPOwnLink g)) + Nothing -> getShortLinkConnReq user l' >>= (`contactReqAndPlan` Just l') + CCTChannel -> throwChatError $ CECommandError "channel links are not supported in this version" + where + contactReqAndPlan cReq sLnk_ = do + plan <- contactRequestPlan user cReq `catchChatError` (pure . CPError) + pure (ACCL SCMContact $ CCLink cReq sLnk_, plan) + connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse + connectWithPlan user@User {userId} incognito ccLink plan + | connectionPlanProceed plan = do + case plan of CPError e -> eToView e; _ -> pure () + case plan of + CPContactAddress (CAPContactViaAddress Contact {contactId}) -> + processChatCommand $ APIConnectContactViaAddress userId incognito contactId + _ -> processChatCommand $ APIConnect userId incognito (Just ccLink) + | otherwise = pure $ CRConnectionPlan user ccLink plan + inviationRequestPlan :: User -> ConnReqInvitation -> CM ConnectionPlan + inviationRequestPlan user cReq = do + withFastStore' (\db -> getConnectionEntityByConnReq db vr user $ cReqSchemas cReq) >>= \case + Nothing -> pure $ CPInvitationLink ILPOk + Just ent -> invitationEntityPlan ent + where + cReqSchemas :: ConnReqInvitation -> (ConnReqInvitation, ConnReqInvitation) + cReqSchemas (CRInvitationUri crData e2e) = + ( CRInvitationUri crData {crScheme = SSSimplex} e2e, + CRInvitationUri crData {crScheme = simplexChat} e2e + ) + invitationEntityPlan :: ConnectionEntity -> CM ConnectionPlan + invitationEntityPlan = \case + RcvDirectMsgConnection Connection {connStatus = ConnPrepared} Nothing -> + pure $ CPInvitationLink ILPOk + RcvDirectMsgConnection conn ct_ -> do + let Connection {connStatus, contactConnInitiated} = conn + if + | connStatus == ConnNew && contactConnInitiated -> + pure $ CPInvitationLink ILPOwnLink + | not (connReady conn) -> + pure $ CPInvitationLink (ILPConnecting ct_) + | otherwise -> case ct_ of + Just ct -> pure $ CPInvitationLink (ILPKnown ct) + Nothing -> throwChatError $ CEInternalError "ready RcvDirectMsgConnection connection should have associated contact" + _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" + contactRequestPlan :: User -> ConnReqContact -> CM ConnectionPlan + contactRequestPlan user (CRContactUri crData) = do + let ConnReqUriData {crClientData} = crData + groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli + cReqHashes = bimap hash hash cReqSchemas + case groupLinkId of + -- contact address + Nothing -> + withFastStore' (\db -> getUserContactLinkByConnReq db user cReqSchemas) >>= \case + Just _ -> pure $ CPContactAddress CAPOwnLink + Nothing -> + withFastStore' (\db -> getContactConnEntityByConnReqHash db vr user cReqHashes) >>= \case + Nothing -> + withFastStore' (\db -> getContactWithoutConnViaAddress db vr user cReqSchemas) >>= \case + Nothing -> pure $ CPContactAddress CAPOk + Just ct -> pure $ CPContactAddress (CAPContactViaAddress ct) + Just (RcvDirectMsgConnection _conn Nothing) -> pure $ CPContactAddress CAPConnectingConfirmReconnect + Just (RcvDirectMsgConnection _ (Just ct)) + | not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnectingProhibit ct) + | contactDeleted ct -> pure $ CPContactAddress CAPOk + | otherwise -> pure $ CPContactAddress (CAPKnown ct) + Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo + Just _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection or RcvGroupMsgConnection" + -- group link + Just _ -> + withFastStore' (\db -> getGroupInfoByUserContactLinkConnReq db vr user cReqSchemas) >>= \case + Just g -> pure $ CPGroupLink (GLPOwnLink g) + Nothing -> do + 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 + (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 + (Nothing, Just _) -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" + (Just gInfo, _) -> groupPlan gInfo + where + groupPlan gInfo@GroupInfo {membership} + | memberStatus membership == GSMemRejected = pure $ CPGroupLink (GLPKnown gInfo) + | not (memberActive membership) && not (memberRemoved membership) = + pure $ CPGroupLink (GLPConnectingProhibit $ Just gInfo) + | memberActive membership = pure $ CPGroupLink (GLPKnown gInfo) + | otherwise = pure $ CPGroupLink GLPOk + cReqSchemas :: (ConnReqContact, ConnReqContact) + cReqSchemas = + ( CRContactUri crData {crScheme = SSSimplex}, + CRContactUri crData {crScheme = simplexChat} + ) + hash :: ConnReqContact -> ConnReqUriHash + hash = ConnReqUriHash . C.sha256Hash . strEncode + getShortLinkConnReq :: User -> ConnShortLink m -> CM (ConnectionRequestUri m) + getShortLinkConnReq user l = do + l' <- restoreShortLink' l + (cReq, cData) <- withAgent (\a -> getConnShortLink a (aUserId user) l') + case cData of + ContactLinkData {direct} | not direct -> throwChatError CEUnsupportedConnReq + _ -> pure () + pure cReq + -- This function is needed, as UI uses simplex:/ schema in message view, so that the links can be handled without browser, + -- and short links are stored with server hostname schema, so they wouldn't match without it. + serverShortLink :: ConnShortLink m -> ConnShortLink m + serverShortLink = \case + CSLInvitation _ srv lnkId linkKey -> CSLInvitation SLSServer srv lnkId linkKey + CSLContact _ ct srv linkKey -> CSLContact SLSServer ct srv linkKey + restoreShortLink' l = (`restoreShortLink` l) <$> asks (shortLinkPresetServers . config) + shortLinkUserData short = if short then Just "" else Nothing + shortenCreatedLink :: CreatedConnLink m -> CM (CreatedConnLink m) + shortenCreatedLink (CCLink cReq sLnk) = CCLink cReq <$> mapM (\l -> (`shortenShortLink` l) <$> asks (shortLinkPresetServers . config)) sLnk + createdGroupLink :: CreatedLinkContact -> CreatedLinkContact + createdGroupLink (CCLink cReq shortLink) = CCLink cReq (toGroupLink <$> shortLink) + where + toGroupLink :: ShortLinkContact -> ShortLinkContact + toGroupLink (CSLContact sch _ srv k) = CSLContact sch CCTGroup srv k + updateCIGroupInvitationStatus :: User -> GroupInfo -> CIGroupInvitationStatus -> CM () + updateCIGroupInvitationStatus user GroupInfo {groupId} newStatus = do + AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withFastStore $ \db -> getChatItemByGroupId db vr user groupId + case (cInfo, content) of + (DirectChat ct@Contact {contactId}, CIRcvGroupInvitation ciGroupInv@CIGroupInvitation {status} memRole) + | status == CIGISPending -> do + let aciContent = ACIContent SMDRcv $ CIRcvGroupInvitation (ciGroupInv {status = newStatus} :: CIGroupInvitation) memRole + timed_ <- contactCITimed ct + updateDirectChatItemView user ct itemId aciContent False False timed_ Nothing + forM_ (timed_ >>= timedDeleteAt') $ + startProximateTimedItemThread user (ChatRef CTDirect contactId, itemId) + _ -> pure () -- prohibited + assertAllowedContent :: MsgContent -> CM () + assertAllowedContent = \case + MCReport {} -> throwChatError $ CECommandError "sending reports via this API is not supported" + _ -> pure () + assertAllowedContent' :: ComposedMessage -> CM () + assertAllowedContent' ComposedMessage {msgContent} = assertAllowedContent msgContent + assertNoMentions :: ComposedMessage -> CM () + assertNoMentions ComposedMessage {mentions} + | null mentions = pure () + | otherwise = throwChatError $ CECommandError "mentions are not supported in this chat" + sendContactContentMessages :: User -> ContactId -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse + sendContactContentMessages user contactId live itemTTL cmrs = do + assertMultiSendable live cmrs + ct <- withFastStore $ \db -> getContact db vr user contactId + assertDirectAllowed user MDSnd ct XMsgNew_ + assertVoiceAllowed ct + processComposedMessages ct + where + assertVoiceAllowed :: Contact -> CM () + assertVoiceAllowed ct = + when (not (featureAllowed SCFVoice forUser ct) && any (\(ComposedMessage {msgContent}, _, _, _) -> isVoice msgContent) cmrs) $ + throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFVoice)) + processComposedMessages :: Contact -> CM ChatResponse + processComposedMessages ct = do + (fInvs_, ciFiles_) <- L.unzip <$> setupSndFileTransfers + timed_ <- sndContactCITimed live ct itemTTL + (msgContainers, quotedItems_) <- L.unzip <$> prepareMsgs (L.zip cmrs fInvs_) timed_ + msgs_ <- sendDirectContactMessages user ct $ L.map XMsgNew msgContainers + let itemsData = prepareSndItemsData (L.toList cmrs) (L.toList ciFiles_) (L.toList quotedItems_) msgs_ + when (length itemsData /= length cmrs) $ logError "sendContactContentMessages: cmrs and itemsData length mismatch" + r@(_, cis) <- partitionEithers <$> saveSndChatItems user (CDDirectSnd ct) Nothing itemsData timed_ live + processSendErrs r + forM_ (timed_ >>= timedDeleteAt') $ \deleteAt -> + forM_ cis $ \ci -> + startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) deleteAt + pure $ CRNewChatItems user (map (AChatItem SCTDirect SMDSnd (DirectChat ct)) cis) + where + setupSndFileTransfers :: CM (NonEmpty (Maybe FileInvitation, Maybe (CIFile 'MDSnd))) + setupSndFileTransfers = + forM cmrs $ \(ComposedMessage {fileSource = file_}, _, _, _) -> case file_ of + Just file -> do + fileSize <- checkSndFile file + (fInv, ciFile) <- xftpSndFileTransfer user file fileSize 1 $ CGContact ct + pure (Just fInv, Just ciFile) + Nothing -> pure (Nothing, Nothing) + 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, Just _) -> pure (MCForward (ExtMsgContent mc M.empty fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) + (Just qiId, Nothing) -> do + CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <- + getDirectChatItem db user contactId qiId + (origQmc, qd, sent) <- quoteData qci + 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)), Just quotedItem) + (Just _, Just _) -> throwError SEInvalidQuote + where + quoteData :: ChatItem c d -> ExceptT StoreError IO (MsgContent, CIQDirection 'CTDirect, Bool) + quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} = throwError SEInvalidQuote + quoteData ChatItem {content = CISndMsgContent qmc} = pure (qmc, CIQDirectSnd, True) + quoteData ChatItem {content = CIRcvMsgContent qmc} = pure (qmc, CIQDirectRcv, False) + quoteData _ = throwError SEInvalidQuote + sendGroupContentMessages :: User -> GroupInfo -> Maybe GroupMemberId -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse + sendGroupContentMessages user gInfo@GroupInfo {membership} directMemId_ live itemTTL cmrs = do + assertMultiSendable live cmrs + (ms, numFileInvs, notInHistory_) <- case directMemId_ of + Nothing -> do + ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo + pure (ms, length $ filter memberCurrent ms, Nothing) + Just dmId -> do + when (dmId == groupMemberId' membership) $ throwChatError $ CECommandError "cannot send to self" + dm <- withFastStore $ \db -> getGroupMemberById db vr user dmId + unless (memberStatus dm == GSMemPendingApproval) $ throwChatError $ CECommandError "cannot send directly to member not pending approval" + pure ([dm], 1, Just NotInHistory) + sendGroupContentMessages_ user gInfo notInHistory_ ms numFileInvs live itemTTL cmrs + sendGroupContentMessages_ :: User -> GroupInfo -> Maybe NotInHistory -> [GroupMember] -> Int -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse + sendGroupContentMessages_ user gInfo@GroupInfo {groupId, membership} notInHistory_ ms numFileInvs live itemTTL cmrs = do + -- TODO [knocking] pass GroupSndScope? + let allowedRole = case ms of + [m] | memberCategory m == GCHostMember && memberStatus membership == GSMemPendingApproval -> Nothing + _ -> Just GRAuthor + forM_ allowedRole $ assertUserGroupRole gInfo + assertGroupContentAllowed + processComposedMessages + where + assertGroupContentAllowed :: CM () + assertGroupContentAllowed = + case findProhibited (L.toList cmrs) of + Just f -> throwChatError (CECommandError $ "feature not allowed " <> T.unpack (groupFeatureNameText f)) + Nothing -> pure () + where + findProhibited :: [ComposedMessageReq] -> Maybe GroupFeature + findProhibited = + foldr' + (\(ComposedMessage {fileSource, msgContent = mc}, _, (_, ft), _) acc -> prohibitedGroupContent gInfo membership mc ft fileSource True <|> acc) + Nothing + processComposedMessages :: CM ChatResponse + processComposedMessages = do + (fInvs_, ciFiles_) <- L.unzip <$> setupSndFileTransfers numFileInvs + timed_ <- sndGroupCITimed live gInfo itemTTL + (chatMsgEvents, quotedItems_) <- L.unzip <$> prepareMsgs (L.zip cmrs fInvs_) timed_ + (msgs_, gsr) <- sendGroupMessages user gInfo ms chatMsgEvents + let itemsData = prepareSndItemsData (L.toList cmrs) (L.toList ciFiles_) (L.toList quotedItems_) (L.toList msgs_) + cis_ <- saveSndChatItems user (CDGroupSnd gInfo) notInHistory_ itemsData timed_ live + when (length cis_ /= length cmrs) $ logError "sendGroupContentMessages: cmrs and cis_ length mismatch" + createMemberSndStatuses cis_ msgs_ gsr + let r@(_, cis) = partitionEithers cis_ + processSendErrs r + forM_ (timed_ >>= timedDeleteAt') $ \deleteAt -> + forM_ cis $ \ci -> + startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci) deleteAt + pure $ CRNewChatItems user (map (AChatItem SCTGroup SMDSnd (GroupChat gInfo)) cis) + where + setupSndFileTransfers :: Int -> CM (NonEmpty (Maybe FileInvitation, Maybe (CIFile 'MDSnd))) + setupSndFileTransfers n = + forM cmrs $ \(ComposedMessage {fileSource = file_}, _, _, _) -> case file_ of + Just file -> do + fileSize <- checkSndFile file + (fInv, ciFile) <- xftpSndFileTransfer user file fileSize n $ CGGroup gInfo ms + pure (Just fInv, Just ciFile) + Nothing -> pure (Nothing, Nothing) + prepareMsgs :: NonEmpty (ComposedMessageReq, Maybe FileInvitation) -> Maybe CITimed -> CM (NonEmpty (ChatMsgEvent 'Json, Maybe (CIQuote 'CTGroup))) + prepareMsgs cmsFileInvs timed_ = withFastStore $ \db -> + forM cmsFileInvs $ \((ComposedMessage {quotedItemId, msgContent = mc}, itemForwarded, _, ciMentions), fInv_) -> + let mentions = M.map (\CIMention {memberId} -> MsgMention {memberId}) ciMentions + in prepareGroupMsg db user gInfo mc mentions quotedItemId itemForwarded fInv_ timed_ live + createMemberSndStatuses :: + [Either ChatError (ChatItem 'CTGroup 'MDSnd)] -> + NonEmpty (Either ChatError SndMessage) -> + GroupSndResult -> + CM () + createMemberSndStatuses cis_ msgs_ GroupSndResult {sentTo, pending, forwarded} = do + let msgToItem = mapMsgToItem + withFastStore' $ \db -> do + forM_ sentTo (processSentTo db msgToItem) + forM_ forwarded (processForwarded db) + forM_ pending (processPending db msgToItem) + where + mapMsgToItem :: Map MessageId ChatItemId + mapMsgToItem = foldr' addItem M.empty (zip (L.toList msgs_) cis_) + where + addItem (Right SndMessage {msgId}, Right ci) m = M.insert msgId (chatItemId' ci) m + addItem _ m = m + processSentTo :: DB.Connection -> Map MessageId ChatItemId -> (GroupMemberId, Either ChatError [MessageId], Either ChatError ([Int64], PQEncryption)) -> IO () + processSentTo db msgToItem (mId, msgIds_, deliveryResult) = forM_ msgIds_ $ \msgIds -> do + let ciIds = mapMaybe (`M.lookup` msgToItem) msgIds + status = case deliveryResult of + Right _ -> GSSNew + Left e -> GSSError $ SndErrOther $ tshow e + forM_ ciIds $ \ciId -> createGroupSndStatus db ciId mId status + processForwarded :: DB.Connection -> GroupMember -> IO () + processForwarded db GroupMember {groupMemberId} = + forM_ cis_ $ \ci_ -> + forM_ ci_ $ \ci -> createGroupSndStatus db (chatItemId' ci) groupMemberId GSSForwarded + processPending :: DB.Connection -> Map MessageId ChatItemId -> (GroupMemberId, Either ChatError MessageId, Either ChatError ()) -> IO () + processPending db msgToItem (mId, msgId_, pendingResult) = forM_ msgId_ $ \msgId -> do + let ciId_ = M.lookup msgId msgToItem + status = case pendingResult of + Right _ -> GSSInactive + Left e -> GSSError $ SndErrOther $ tshow e + forM_ ciId_ $ \ciId -> createGroupSndStatus db ciId mId status + assertMultiSendable :: Bool -> NonEmpty ComposedMessageReq -> CM () + assertMultiSendable live cmrs + | length cmrs == 1 = pure () + | otherwise = + -- When sending multiple messages only single quote is allowed. + -- This is to support case of sending multiple attachments while also quoting another message. + -- UI doesn't allow composing with multiple quotes, so api prohibits it as well, and doesn't bother + -- batching retrieval of quoted messages (prepareMsgs). + when (live || length (L.filter (\(ComposedMessage {quotedItemId}, _, _, _) -> isJust quotedItemId) cmrs) > 1) $ + throwChatError (CECommandError "invalid multi send: live and more than one quote not supported") + xftpSndFileTransfer :: User -> CryptoFile -> Integer -> Int -> ContactOrGroup -> CM (FileInvitation, CIFile 'MDSnd) + xftpSndFileTransfer user file fileSize n contactOrGroup = do + (fInv, ciFile, ft) <- xftpSndFileTransfer_ user file fileSize n $ Just contactOrGroup + case contactOrGroup of + CGContact Contact {activeConn} -> forM_ activeConn $ \conn -> + withFastStore' $ \db -> createSndFTDescrXFTP db user Nothing conn ft dummyFileDescr + CGGroup _ ms -> forM_ ms $ \m -> saveMemberFD m `catchChatError` eToView + where + -- we are not sending files to pending members, same as with inline files + saveMemberFD m@GroupMember {activeConn = Just conn@Connection {connStatus}} = + when ((connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn)) $ + withFastStore' $ + \db -> createSndFTDescrXFTP db user (Just m) conn ft dummyFileDescr + saveMemberFD _ = pure () + pure (fInv, ciFile) + prepareSndItemsData :: + [ComposedMessageReq] -> + [Maybe (CIFile 'MDSnd)] -> + [Maybe (CIQuote c)] -> + [Either ChatError SndMessage] -> + [Either ChatError (NewSndChatItemData c)] + prepareSndItemsData = + zipWith4 $ \(ComposedMessage {msgContent}, itemForwarded, ts, mm) f q -> \case + Right msg -> Right $ NewSndChatItemData msg (CISndMsgContent msgContent) ts mm f q itemForwarded + Left e -> Left e -- step over original error + processSendErrs :: ([ChatError], [ChatItem c d]) -> CM () + processSendErrs = \case + -- no errors + ([], _) -> pure () + -- at least one item is successfully created + (errs, _ci : _) -> toView $ CEvtChatErrors errs + -- single error + ([err], []) -> throwError err + -- multiple errors + (errs@(err : _), []) -> do + toView $ CEvtChatErrors errs + throwError err + getCommandDirectChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (Contact, [CChatItem 'CTDirect]) + getCommandDirectChatItems user ctId itemIds = do + ct <- withFastStore $ \db -> getContact db vr user ctId + (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getDirectCI db) (L.toList itemIds)) + unless (null errs) $ toView $ CEvtChatErrors errs + pure (ct, items) + where + getDirectCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTDirect)) + getDirectCI db itemId = runExceptT . withExceptT ChatErrorStore $ getDirectChatItem db user ctId itemId + getCommandGroupChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (GroupInfo, [CChatItem 'CTGroup]) + getCommandGroupChatItems user gId itemIds = do + gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId + (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getGroupCI db gInfo) (L.toList itemIds)) + unless (null errs) $ toView $ CEvtChatErrors errs + pure (gInfo, items) + where + getGroupCI :: DB.Connection -> GroupInfo -> ChatItemId -> IO (Either ChatError (CChatItem 'CTGroup)) + getGroupCI db gInfo itemId = runExceptT . withExceptT ChatErrorStore $ getGroupCIWithReactions db user gInfo itemId + getCommandLocalChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (NoteFolder, [CChatItem 'CTLocal]) + getCommandLocalChatItems user nfId itemIds = do + nf <- withStore $ \db -> getNoteFolder db user nfId + (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getLocalCI db) (L.toList itemIds)) + unless (null errs) $ toView $ CEvtChatErrors errs + pure (nf, items) + where + getLocalCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTLocal)) + getLocalCI db itemId = runExceptT . withExceptT ChatErrorStore $ getLocalChatItem db user nfId itemId + forwardMsgContent :: ChatItem c d -> CM (Maybe MsgContent) + forwardMsgContent ChatItem {meta = CIMeta {itemDeleted = Just _}} = pure Nothing -- this can be deleted after selection + forwardMsgContent ChatItem {content = CISndMsgContent fmc} = pure $ Just fmc + forwardMsgContent ChatItem {content = CIRcvMsgContent fmc} = pure $ Just fmc + forwardMsgContent _ = throwChatError CEInvalidForward + createNoteFolderContentItems :: User -> NoteFolderId -> NonEmpty ComposedMessageReq -> CM ChatResponse + createNoteFolderContentItems user folderId cmrs = do + assertNoQuotes + nf <- withFastStore $ \db -> getNoteFolder db user folderId + createdAt <- liftIO getCurrentTime + ciFiles_ <- createLocalFiles nf createdAt + let itemsData = prepareLocalItemsData cmrs ciFiles_ + cis <- createLocalChatItems user (CDLocalSnd nf) itemsData createdAt + pure $ CRNewChatItems user (map (AChatItem SCTLocal SMDSnd (LocalChat nf)) cis) + where + assertNoQuotes :: CM () + assertNoQuotes = + when (any (\(ComposedMessage {quotedItemId}, _, _, _) -> isJust quotedItemId) cmrs) $ + throwChatError (CECommandError "createNoteFolderContentItems: quotes not supported") + createLocalFiles :: NoteFolder -> UTCTime -> CM (NonEmpty (Maybe (CIFile 'MDSnd))) + createLocalFiles nf createdAt = + forM cmrs $ \(ComposedMessage {fileSource = file_}, _, _, _) -> + forM file_ $ \cf@CryptoFile {filePath, cryptoArgs} -> do + fsFilePath <- lift $ toFSFilePath filePath + fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cryptoArgs + chunkSize <- asks $ fileChunkSize . config + withFastStore' $ \db -> do + fileId <- createLocalFile CIFSSndStored db user nf createdAt cf fileSize chunkSize + pure CIFile {fileId, fileName = takeFileName filePath, fileSize, fileSource = Just cf, fileStatus = CIFSSndStored, fileProtocol = FPLocal} + prepareLocalItemsData :: + NonEmpty ComposedMessageReq -> + NonEmpty (Maybe (CIFile 'MDSnd)) -> + NonEmpty (CIContent 'MDSnd, Maybe (CIFile 'MDSnd), Maybe CIForwardedFrom, (Text, Maybe MarkdownList)) + prepareLocalItemsData = + L.zipWith $ \(ComposedMessage {msgContent = mc}, itemForwarded, ts, _) f -> + (CISndMsgContent mc, f, itemForwarded, ts) + getConnQueueInfo user Connection {connId, agentConnId = AgentConnId acId} = do + msgInfo <- withFastStore' (`getLastRcvMsgInfo` connId) + CRQueueInfo user msgInfo <$> withAgent (`getConnectionQueueInfo` acId) + withSendRef :: ChatRef -> (SendRef -> CM ChatResponse) -> CM ChatResponse + withSendRef chatRef a = case chatRef of + ChatRef CTDirect cId -> a $ SRDirect cId + ChatRef CTGroup gId -> a $ SRGroup gId Nothing + _ -> throwChatError $ CECommandError "not supported" + +protocolServers :: UserProtocol p => SProtocolType p -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) +protocolServers p (operators, smpServers, xftpServers) = case p of + SPSMP -> (operators, smpServers, []) + SPXFTP -> (operators, [], xftpServers) + +-- disable preset and replace custom servers (groupByOperator always adds custom) +updatedServers :: forall p. UserProtocol p => SProtocolType p -> [AUserServer p] -> UserOperatorServers -> UpdatedUserOperatorServers +updatedServers p' srvs UserOperatorServers {operator, smpServers, xftpServers} = case p' of + SPSMP -> u (updateSrvs smpServers, map (AUS SDBStored) xftpServers) + SPXFTP -> u (map (AUS SDBStored) smpServers, updateSrvs xftpServers) + where + u = uncurry $ UpdatedUserOperatorServers operator + updateSrvs :: [UserServer p] -> [AUserServer p] + updateSrvs pSrvs = map disableSrv pSrvs <> maybe srvs (const []) operator + disableSrv srv@UserServer {preset} = + AUS SDBStored $ if preset then srv {enabled = False} else srv {deleted = True} + +type ComposedMessageReq = (ComposedMessage, Maybe CIForwardedFrom, (Text, Maybe MarkdownList), Map MemberName CIMention) + +composedMessage :: Maybe CryptoFile -> MsgContent -> ComposedMessage +composedMessage f mc = ComposedMessage {fileSource = f, quotedItemId = Nothing, msgContent = mc, mentions = M.empty} + +composedMessageReq :: ComposedMessage -> ComposedMessageReq +composedMessageReq cm@ComposedMessage {msgContent = mc} = (cm, Nothing, msgContentTexts mc, M.empty) + +composedMessageReqMentions :: DB.Connection -> User -> GroupInfo -> ComposedMessage -> ExceptT StoreError IO ComposedMessageReq +composedMessageReqMentions db user g cm@ComposedMessage {msgContent = mc, mentions} = do + let ts@(_, ft_) = msgContentTexts mc + (cm,Nothing,ts,) <$> getCIMentions db user g ft_ mentions + +data ChangedProfileContact = ChangedProfileContact + { ct :: Contact, + ct' :: Contact, + mergedProfile' :: Profile, + conn :: Connection + } + +createContactsSndFeatureItems :: User -> [ChangedProfileContact] -> CM' () +createContactsSndFeatureItems user cts = + createContactsFeatureItems user cts' CDDirectSnd CISndChatFeature CISndChatPreference getPref + where + cts' = map (\ChangedProfileContact {ct, ct'} -> (ct, ct')) cts + getPref ContactUserPreference {userPreference} = case userPreference of + CUPContact {preference} -> preference + CUPUser {preference} -> preference + +assertDirectAllowed :: User -> MsgDirection -> Contact -> CMEventTag e -> CM () +assertDirectAllowed user dir ct event = + unless (allowedChatEvent || anyDirectOrUsed ct) . unlessM directMessagesAllowed $ + throwChatError (CEDirectMessagesProhibited dir ct) + where + directMessagesAllowed = any (uncurry $ groupFeatureMemberAllowed' SGFDirectMessages) <$> withStore' (\db -> getContactGroupPreferences db user ct) + allowedChatEvent = case event of + XMsgNew_ -> False + XMsgUpdate_ -> False + XMsgDel_ -> False + XFile_ -> False + XGrpInv_ -> False + XCallInv_ -> False + _ -> True + +startExpireCIThread :: User -> CM' () +startExpireCIThread user@User {userId} = do + expireThreads <- asks expireCIThreads + atomically (TM.lookup userId expireThreads) >>= \case + Nothing -> do + a <- Just <$> async runExpireCIs + atomically $ TM.insert userId a expireThreads + _ -> pure () + where + runExpireCIs = do + delay <- asks (initialCleanupManagerDelay . config) + liftIO $ threadDelay' delay + interval <- asks $ ciExpirationInterval . config + forever $ do + flip catchChatError' (eToView') $ do + expireFlags <- asks expireCIFlags + atomically $ TM.lookup userId expireFlags >>= \b -> unless (b == Just True) retry + lift waitChatStartedAndActivated + ttl <- withStore' (`getChatItemTTL` user) + expireChatItems user ttl False + liftIO $ threadDelay' interval + +setChatItemsExpiration :: User -> Int64 -> Int -> CM' () +setChatItemsExpiration user newTTL ttlCount + | newTTL > 0 || ttlCount > 0 = do + startExpireCIThread user + whenM chatStarted $ setExpireCIFlag user True + | otherwise = setExpireCIFlag user False + +setExpireCIFlag :: User -> Bool -> CM' () +setExpireCIFlag User {userId} b = do + expireFlags <- asks expireCIFlags + atomically $ TM.insert userId b expireFlags + +setAllExpireCIFlags :: Bool -> CM' () +setAllExpireCIFlags b = do + expireFlags <- asks expireCIFlags + atomically $ do + keys <- M.keys <$> readTVar expireFlags + forM_ keys $ \k -> TM.insert k b expireFlags + +agentSubscriber :: CM' () +agentSubscriber = do + q <- asks $ subQ . smpAgent + forever (atomically (readTBQueue q) >>= process) + `E.catchAny` \e -> do + eToView' $ ChatErrorAgent (CRITICAL True $ "Message reception stopped: " <> show e) Nothing + E.throwIO e + where + process :: (ACorrId, AEntityId, AEvt) -> CM' () + process (corrId, entId, AEvt e msg) = run $ case e of + SAENone -> processAgentMessageNoConn msg + SAEConn -> processAgentMessage corrId entId msg + SAERcvFile -> processAgentMsgRcvFile corrId entId msg + SAESndFile -> processAgentMsgSndFile corrId entId msg + where + run action = action `catchChatError'` (eToView') + +type AgentBatchSubscribe = AgentClient -> [ConnId] -> ExceptT AgentErrorType IO (Map ConnId (Either AgentErrorType ())) + +subscribeUserConnections :: VersionRangeChat -> Bool -> AgentBatchSubscribe -> User -> CM () +subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do + -- get user connections + ce <- asks $ subscriptionEvents . config + (conns, cts, ucs, gs, ms, sfts, rfts, pcs) <- + if onlyNeeded + then do + (conns, entities) <- withStore' (`getConnectionsToSubscribe` vr) + let (cts, ucs, ms, sfts, rfts, pcs) = foldl' addEntity (M.empty, M.empty, M.empty, M.empty, M.empty, M.empty) entities + pure (conns, cts, ucs, [], ms, sfts, rfts, pcs) + else do + withStore' unsetConnectionToSubscribe + (ctConns, cts) <- getContactConns + (ucConns, ucs) <- getUserContactLinkConns + (gs, mConns, ms) <- getGroupMemberConns + (sftConns, sfts) <- getSndFileTransferConns + (rftConns, rfts) <- getRcvFileTransferConns + (pcConns, pcs) <- getPendingContactConns + let conns = concat ([ctConns, ucConns, mConns, sftConns, rftConns, pcConns] :: [[ConnId]]) + pure (conns, cts, ucs, gs, ms, sfts, rfts, pcs) + -- subscribe using batched commands + rs <- withAgent $ \a -> agentBatchSubscribe a conns + -- send connection events to view + contactSubsToView rs cts ce + unlessM (asks $ coreApi . config) $ do + contactLinkSubsToView rs ucs + groupSubsToView rs gs ms ce + sndFileSubsToView rs sfts + rcvFileSubsToView rs rfts + pendingConnSubsToView rs pcs + where + addEntity (cts, ucs, ms, sfts, rfts, pcs) = \case + RcvDirectMsgConnection c (Just ct) -> let cts' = addConn c ct cts in (cts', ucs, ms, sfts, rfts, pcs) + RcvDirectMsgConnection c Nothing -> let pcs' = addConn c (toPCC c) pcs in (cts, ucs, ms, sfts, rfts, pcs') + RcvGroupMsgConnection c _g m -> let ms' = addConn c (toShortMember m c) ms in (cts, ucs, ms', sfts, rfts, pcs) + SndFileConnection c sft -> let sfts' = addConn c sft sfts in (cts, ucs, ms, sfts', rfts, pcs) + RcvFileConnection c rft -> let rfts' = addConn c rft rfts in (cts, ucs, ms, sfts, rfts', pcs) + UserContactConnection c uc -> let ucs' = addConn c uc ucs in (cts, ucs', ms, sfts, rfts, pcs) + addConn :: Connection -> a -> Map ConnId a -> Map ConnId a + addConn = M.insert . aConnId + toPCC Connection {connId, agentConnId, connStatus, viaUserContactLink, groupLinkId, customUserProfileId, localAlias, createdAt} = + PendingContactConnection + { pccConnId = connId, + pccAgentConnId = agentConnId, + pccConnStatus = connStatus, + viaContactUri = False, + viaUserContactLink, + groupLinkId, + customUserProfileId, + connLinkInv = Nothing, + localAlias, + createdAt, + updatedAt = createdAt + } + toShortMember GroupMember {groupMemberId, groupId, localDisplayName} Connection {agentConnId} = + ShortGroupMember + { groupMemberId, + groupId, + memberName = localDisplayName, + connId = agentConnId + } + getContactConns :: CM ([ConnId], Map ConnId Contact) + getContactConns = do + cts <- withStore_ (`getUserContacts` vr) + let cts' = mapMaybe (\ct -> (,ct) <$> contactConnId ct) $ filter contactActive cts + pure (map fst cts', M.fromList cts') + getUserContactLinkConns :: CM ([ConnId], Map ConnId UserContact) + getUserContactLinkConns = do + (cs, ucs) <- unzip <$> withStore_ (`getUserContactLinks` vr) + let connIds = map aConnId cs + pure (connIds, M.fromList $ zip connIds ucs) + getGroupMemberConns :: CM ([ShortGroup], [ConnId], Map ConnId ShortGroupMember) + getGroupMemberConns = do + gs <- withStore_ getUserGroupsToSubscribe + let mPairs = concatMap (\(ShortGroup _ ms) -> map (\m -> (shortMemConnId m, m)) ms) gs + pure (gs, map fst mPairs, M.fromList mPairs) + where + shortMemConnId ShortGroupMember{connId = AgentConnId acId} = acId + getSndFileTransferConns :: CM ([ConnId], Map ConnId SndFileTransfer) + getSndFileTransferConns = do + sfts <- withStore_ getLiveSndFileTransfers + let connIds = map sndFileTransferConnId sfts + pure (connIds, M.fromList $ zip connIds sfts) + getRcvFileTransferConns :: CM ([ConnId], Map ConnId RcvFileTransfer) + getRcvFileTransferConns = do + rfts <- withStore_ getLiveRcvFileTransfers + let rftPairs = mapMaybe (\ft -> (,ft) <$> liveRcvFileTransferConnId ft) rfts + pure (map fst rftPairs, M.fromList rftPairs) + getPendingContactConns :: CM ([ConnId], Map ConnId PendingContactConnection) + getPendingContactConns = do + pcs <- withStore_ getPendingContactConnections + let connIds = map aConnId' pcs + pure (connIds, M.fromList $ zip connIds pcs) + contactSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId Contact -> Bool -> CM () + contactSubsToView rs cts ce = do + chatModifyVar connNetworkStatuses $ M.union (M.fromList statuses) + ifM (asks $ coreApi . config) (notifyAPI statuses) notifyCLI + where + notifyCLI = do + let cRs = resultsFor rs cts + cErrors = sortOn (\(Contact {localDisplayName = n}, _) -> n) $ filterErrors cRs + toView . CEvtContactSubSummary user $ map (uncurry ContactSubStatus) cRs + when ce $ mapM_ (toView . uncurry (CEvtContactSubError user)) cErrors + notifyAPI = toView . CEvtNetworkStatuses (Just user) . map (uncurry ConnNetworkStatus) + statuses = M.foldrWithKey' addStatus [] cts + where + addStatus :: ConnId -> Contact -> [(AgentConnId, NetworkStatus)] -> [(AgentConnId, NetworkStatus)] + addStatus _ Contact {activeConn = Nothing} nss = nss + addStatus connId Contact {activeConn = Just Connection {agentConnId}} nss = + let ns = (agentConnId, netStatus $ resultErr connId rs) + in ns : nss + netStatus :: Maybe ChatError -> NetworkStatus + netStatus = maybe NSConnected $ NSError . errorNetworkStatus + errorNetworkStatus :: ChatError -> String + errorNetworkStatus = \case + ChatErrorAgent (BROKER _ NETWORK) _ -> "network" + ChatErrorAgent (SMP _ SMP.AUTH) _ -> "contact deleted" + e -> show e + -- TODO possibly below could be replaced with less noisy events for API + contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> CM () + contactLinkSubsToView rs = toView . CEvtUserContactSubSummary user . map (uncurry UserContactSubStatus) . resultsFor rs + groupSubsToView :: Map ConnId (Either AgentErrorType ()) -> [ShortGroup] -> Map ConnId ShortGroupMember -> Bool -> CM () + groupSubsToView rs gs ms ce = do + mapM_ groupSub $ + sortOn (\(ShortGroup ShortGroupInfo {groupName = g} _) -> g) gs + toViewTE . TEMemberSubSummary user $ map (uncurry MemberSubStatus) mRs + where + mRs = resultsFor rs ms + groupSub :: ShortGroup -> CM () + groupSub (ShortGroup g@ShortGroupInfo {groupId = gId, membershipStatus} members) = do + when ce $ mapM_ (toViewTE . uncurry (TEMemberSubError user g)) mErrors + toViewTE groupEvent + where + mErrors :: [(ShortGroupMember, ChatError)] + mErrors = + sortOn (\(ShortGroupMember {memberName = n}, _) -> n) + . filterErrors + $ filter (\(ShortGroupMember {groupId}, _) -> groupId == gId) mRs + groupEvent :: TerminalEvent + groupEvent + | membershipStatus == GSMemInvited = TEGroupInvitation user g + | null members = TEGroupEmpty user g + | otherwise = TEGroupSubscribed user g + sndFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId SndFileTransfer -> CM () + sndFileSubsToView rs sfts = do + let sftRs = resultsFor rs sfts + forM_ sftRs $ \(ft@SndFileTransfer {fileId, fileStatus}, err_) -> do + forM_ err_ $ toViewTE . TESndFileSubError user ft + void . forkIO $ do + threadDelay 1000000 + when (fileStatus == FSConnected) . unlessM (isFileActive fileId sndFiles) . withChatLock "subscribe sendFileChunk" $ + sendFileChunk user ft + rcvFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId RcvFileTransfer -> CM () + rcvFileSubsToView rs = mapM_ (toViewTE . uncurry (TERcvFileSubError user)) . filterErrors . resultsFor rs + pendingConnSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId PendingContactConnection -> CM () + pendingConnSubsToView rs = toViewTE . TEPendingSubSummary user . map (uncurry PendingSubStatus) . resultsFor rs + withStore_ :: (DB.Connection -> User -> IO [a]) -> CM [a] + withStore_ a = withStore' (`a` user) `catchChatError` \e -> eToView e $> [] + filterErrors :: [(a, Maybe ChatError)] -> [(a, ChatError)] + filterErrors = mapMaybe (\(a, e_) -> (a,) <$> e_) + resultsFor :: Map ConnId (Either AgentErrorType ()) -> Map ConnId a -> [(a, Maybe ChatError)] + resultsFor rs = M.foldrWithKey' addResult [] + where + addResult :: ConnId -> a -> [(a, Maybe ChatError)] -> [(a, Maybe ChatError)] + addResult connId = (:) . (,resultErr connId rs) + resultErr :: ConnId -> Map ConnId (Either AgentErrorType ()) -> Maybe ChatError + resultErr connId rs = case M.lookup connId rs of + Just (Left e) -> Just $ ChatErrorAgent e Nothing + Just _ -> Nothing + _ -> Just . ChatError . CEAgentNoSubResult $ AgentConnId connId + +cleanupManager :: CM () +cleanupManager = do + interval <- asks (cleanupManagerInterval . config) + runWithoutInitialDelay interval + initialDelay <- asks (initialCleanupManagerDelay . config) + liftIO $ threadDelay' initialDelay + stepDelay <- asks (cleanupManagerStepDelay . config) + forever $ do + flip catchChatError eToView $ do + lift waitChatStartedAndActivated + users <- withStore' getUsers + let (us, us') = partition activeUser users + forM_ us $ cleanupUser interval stepDelay + forM_ us' $ cleanupUser interval stepDelay + cleanupMessages `catchChatError` eToView + -- TODO possibly, also cleanup async commands + cleanupProbes `catchChatError` eToView + liftIO $ threadDelay' $ diffToMicroseconds interval + where + runWithoutInitialDelay cleanupInterval = flip catchChatError eToView $ do + lift waitChatStartedAndActivated + users <- withStore' getUsers + let (us, us') = partition activeUser users + forM_ us $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` eToView + forM_ us' $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` eToView + cleanupUser cleanupInterval stepDelay user = do + cleanupTimedItems cleanupInterval user `catchChatError` eToView + liftIO $ threadDelay' stepDelay + -- TODO remove in future versions: legacy step - contacts are no longer marked as deleted + cleanupDeletedContacts user `catchChatError` eToView + liftIO $ threadDelay' stepDelay + cleanupTimedItems cleanupInterval user = do + ts <- liftIO getCurrentTime + let startTimedThreadCutoff = addUTCTime cleanupInterval ts + timedItems <- withStore' $ \db -> getTimedItems db user startTimedThreadCutoff + forM_ timedItems $ \(itemRef, deleteAt) -> startTimedItemThread user itemRef deleteAt `catchChatError` const (pure ()) + cleanupDeletedContacts user = do + vr <- chatVersionRange + contacts <- withStore' $ \db -> getDeletedContacts db vr user + forM_ contacts $ \ct -> + withStore (\db -> deleteContactWithoutGroups db user ct) + `catchChatError` eToView + cleanupMessages = do + ts <- liftIO getCurrentTime + let cutoffTs = addUTCTime (-(30 * nominalDay)) ts + withStore' (`deleteOldMessages` cutoffTs) + cleanupProbes = do + ts <- liftIO getCurrentTime + let cutoffTs = addUTCTime (-(14 * nominalDay)) ts + withStore' (`deleteOldProbes` cutoffTs) + +expireChatItems :: User -> Int64 -> Bool -> CM () +expireChatItems user@User {userId} globalTTL sync = do + currentTs <- liftIO getCurrentTime + vr <- chatVersionRange + -- this is to keep group messages created during last 12 hours even if they're expired according to item_ts + let createdAtCutoff = addUTCTime (-43200 :: NominalDiffTime) currentTs + lift waitChatStartedAndActivated + contactIds <- withStore' $ \db -> getUserContactsToExpire db user globalTTL + loop contactIds $ expireContactChatItems user vr globalTTL + lift waitChatStartedAndActivated + groupIds <- withStore' $ \db -> getUserGroupsToExpire db user globalTTL + loop groupIds $ expireGroupChatItems user vr globalTTL createdAtCutoff + where + loop :: [Int64] -> (Int64 -> CM ()) -> CM () + loop [] _ = pure () + loop (a : as) process = continue $ do + process a `catchChatError` eToView + loop as process + continue :: CM () -> CM () + continue a = + if sync + then a + else do + expireFlags <- asks expireCIFlags + expire <- atomically $ TM.lookup userId expireFlags + when (expire == Just True) $ threadDelay 100000 >> a + +expireContactChatItems :: User -> VersionRangeChat -> Int64 -> ContactId -> CM () +expireContactChatItems user vr globalTTL ctId = + -- reading contacts and groups inside the loop, + -- to allow ttl changing while processing and to reduce memory usage + tryChatError (withStore $ \db -> getContact db vr user ctId) >>= mapM_ process + where + process ct@Contact {chatItemTTL} = + withExpirationDate globalTTL chatItemTTL $ \expirationDate -> do + lift waitChatStartedAndActivated + filesInfo <- withStore' $ \db -> getContactExpiredFileInfo db user ct expirationDate + deleteCIFiles user filesInfo + withStore' $ \db -> deleteContactExpiredCIs db user ct expirationDate + +expireGroupChatItems :: User -> VersionRangeChat -> Int64 -> UTCTime -> GroupId -> CM () +expireGroupChatItems user vr globalTTL createdAtCutoff groupId = + tryChatError (withStore $ \db -> getGroupInfo db vr user groupId) >>= mapM_ process + where + process gInfo@GroupInfo {chatItemTTL} = + withExpirationDate globalTTL chatItemTTL $ \expirationDate -> do + lift waitChatStartedAndActivated + filesInfo <- withStore' $ \db -> getGroupExpiredFileInfo db user gInfo expirationDate createdAtCutoff + deleteCIFiles user filesInfo + withStore' $ \db -> deleteGroupExpiredCIs db user gInfo expirationDate createdAtCutoff + membersToDelete <- withStore' $ \db -> getGroupMembersForExpiration db vr user gInfo + forM_ membersToDelete $ \m -> withStore' $ \db -> deleteGroupMember db user m + +withExpirationDate :: Int64 -> Maybe Int64 -> (UTCTime -> CM ()) -> CM () +withExpirationDate globalTTL chatItemTTL action = do + currentTs <- liftIO getCurrentTime + let ttl = fromMaybe globalTTL chatItemTTL + when (ttl > 0) $ action $ addUTCTime (-1 * fromIntegral ttl) currentTs + +chatCommandP :: Parser ChatCommand +chatCommandP = + choice + [ "/mute " *> ((`SetShowMessages` MFNone) <$> chatNameP), + "/unmute " *> ((`SetShowMessages` MFAll) <$> chatNameP), + "/unmute mentions " *> ((`SetShowMessages` MFMentions) <$> chatNameP), + "/receipts " *> (SetSendReceipts <$> chatNameP <* " " <*> ((Just <$> onOffP) <|> ("default" $> Nothing))), + "/block #" *> (SetShowMemberMessages <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure False), + "/unblock #" *> (SetShowMemberMessages <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure True), + "/_create user " *> (CreateActiveUser <$> jsonP), + "/create user " *> (CreateActiveUser <$> newUserP), + "/users" $> ListUsers, + "/_user " *> (APISetActiveUser <$> A.decimal <*> optional (A.space *> jsonP)), + ("/user " <|> "/u ") *> (SetActiveUser <$> displayNameP <*> optional (A.space *> pwdP)), + "/set receipts all " *> (SetAllContactReceipts <$> onOffP), + "/_set receipts contacts " *> (APISetUserContactReceipts <$> A.decimal <* A.space <*> receiptSettings), + "/set receipts contacts " *> (SetUserContactReceipts <$> receiptSettings), + "/_set receipts groups " *> (APISetUserGroupReceipts <$> A.decimal <* A.space <*> receiptSettings), + "/set receipts groups " *> (SetUserGroupReceipts <$> receiptSettings), + "/_hide user " *> (APIHideUser <$> A.decimal <* A.space <*> jsonP), + "/_unhide user " *> (APIUnhideUser <$> A.decimal <* A.space <*> jsonP), + "/_mute user " *> (APIMuteUser <$> A.decimal), + "/_unmute user " *> (APIUnmuteUser <$> A.decimal), + "/hide user " *> (HideUser <$> pwdP), + "/unhide user " *> (UnhideUser <$> pwdP), + "/mute user" $> MuteUser, + "/unmute user" $> UnmuteUser, + "/_delete user " *> (APIDeleteUser <$> A.decimal <* " del_smp=" <*> onOffP <*> optional (A.space *> jsonP)), + "/delete user " *> (DeleteUser <$> displayNameP <*> pure True <*> optional (A.space *> pwdP)), + ("/user" <|> "/u") $> ShowActiveUser, + "/_start " *> do + mainApp <- "main=" *> onOffP + enableSndFiles <- " snd_files=" *> onOffP <|> pure mainApp + pure StartChat {mainApp, enableSndFiles}, + "/_start" $> StartChat True True, + "/_check running" $> CheckChatRunning, + "/_stop" $> APIStopChat, + "/_app activate restore=" *> (APIActivateChat <$> onOffP), + "/_app activate" $> APIActivateChat True, + "/_app suspend " *> (APISuspendChat <$> A.decimal), + "/_resubscribe all" $> ResubscribeAllConnections, + -- deprecated, use /set file paths + "/_temp_folder " *> (SetTempFolder <$> filePath), + -- /_files_folder deprecated, use /set file paths + ("/_files_folder " <|> "/files_folder ") *> (SetFilesFolder <$> filePath), + -- deprecated, use /set file paths + "/remote_hosts_folder " *> (SetRemoteHostsFolder <$> filePath), + "/set file paths " *> (APISetAppFilePaths <$> jsonP), + "/_files_encrypt " *> (APISetEncryptLocalFiles <$> onOffP), + "/contact_merge " *> (SetContactMergeEnabled <$> onOffP), +#if !defined(dbPostgres) + "/_db export " *> (APIExportArchive <$> jsonP), + "/db export" $> ExportArchive, + "/_db import " *> (APIImportArchive <$> jsonP), + "/_db delete" $> APIDeleteStorage, + "/_db encryption " *> (APIStorageEncryption <$> jsonP), + "/db encrypt " *> (APIStorageEncryption . dbEncryptionConfig "" <$> dbKeyP), + "/db key " *> (APIStorageEncryption <$> (dbEncryptionConfig <$> dbKeyP <* A.space <*> dbKeyP)), + "/db decrypt " *> (APIStorageEncryption . (`dbEncryptionConfig` "") <$> dbKeyP), + "/db test key " *> (TestStorageEncryption <$> dbKeyP), + "/sql slow" $> SlowSQLQueries, +#endif + "/_save app settings" *> (APISaveAppSettings <$> jsonP), + "/_get app settings" *> (APIGetAppSettings <$> optional (A.space *> jsonP)), + "/sql chat " *> (ExecChatStoreSQL <$> textP), + "/sql agent " *> (ExecAgentStoreSQL <$> textP), + "/_get tags " *> (APIGetChatTags <$> A.decimal), + "/_get chats " + *> ( APIGetChats + <$> A.decimal + <*> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False) + <*> (A.space *> paginationByTimeP <|> pure (PTLast 5000)) + <*> (A.space *> jsonP <|> pure clqNoFilters) + ), + "/_get chat " *> (APIGetChat <$> chatRefP <*> optional (" content=" *> strP) <* A.space <*> chatPaginationP <*> optional (" search=" *> stringP)), + "/_get items " *> (APIGetChatItems <$> chatPaginationP <*> optional (" search=" *> stringP)), + "/_get item info " *> (APIGetChatItemInfo <$> chatRefP <* A.space <*> A.decimal), + "/_send " *> (APISendMessages <$> sendRefP <*> liveMessageP <*> sendMessageTTLP <*> (" json " *> jsonP <|> " text " *> composedMessagesTextP)), + "/_create tag " *> (APICreateChatTag <$> jsonP), + "/_tags " *> (APISetChatTags <$> chatRefP <*> optional _strP), + "/_delete tag " *> (APIDeleteChatTag <$> A.decimal), + "/_update tag " *> (APIUpdateChatTag <$> A.decimal <* A.space <*> jsonP), + "/_reorder tags " *> (APIReorderChatTags <$> strP), + "/_create *" *> (APICreateChatItems <$> A.decimal <*> (" json " *> jsonP <|> " text " *> composedMessagesTextP)), + "/_report #" *> (APIReportMessage <$> A.decimal <* A.space <*> A.decimal <*> (" reason=" *> strP) <*> (A.space *> textP <|> pure "")), + "/report #" *> (ReportMessage <$> displayNameP <*> optional (" @" *> displayNameP) <*> _strP <* A.space <*> msgTextP), + "/_update item " *> (APIUpdateChatItem <$> chatRefP <* A.space <*> A.decimal <*> liveMessageP <*> (" json" *> jsonP <|> " text " *> updatedMessagesTextP)), + "/_delete item " *> (APIDeleteChatItem <$> chatRefP <*> _strP <*> _strP), + "/_delete member item #" *> (APIDeleteMemberChatItem <$> A.decimal <*> _strP), + "/_archive reports #" *> (APIArchiveReceivedReports <$> A.decimal), + "/_delete reports #" *> (APIDeleteReceivedReports <$> A.decimal <*> _strP <*> _strP), + "/_reaction " *> (APIChatItemReaction <$> chatRefP <* A.space <*> A.decimal <* A.space <*> onOffP <* A.space <*> (knownReaction <$?> jsonP)), + "/_reaction members " *> (APIGetReactionMembers <$> A.decimal <* " #" <*> A.decimal <* A.space <*> A.decimal <* A.space <*> (knownReaction <$?> jsonP)), + "/_forward plan " *> (APIPlanForwardChatItems <$> chatRefP <*> _strP), + "/_forward " *> (APIForwardChatItems <$> chatRefP <* A.space <*> chatRefP <*> _strP <*> sendMessageTTLP), + "/_read user " *> (APIUserRead <$> A.decimal), + "/read user" $> UserRead, + "/_read chat " *> (APIChatRead <$> chatRefP), + "/_read chat items " *> (APIChatItemsRead <$> chatRefP <*> _strP), + "/_unread chat " *> (APIChatUnread <$> chatRefP <* A.space <*> onOffP), + "/_delete " *> (APIDeleteChat <$> chatRefP <*> chatDeleteMode), + "/_clear chat " *> (APIClearChat <$> chatRefP), + "/_accept" *> (APIAcceptContact <$> incognitoOnOffP <* A.space <*> A.decimal), + "/_reject " *> (APIRejectContact <$> A.decimal), + "/_call invite @" *> (APISendCallInvitation <$> A.decimal <* A.space <*> jsonP), + "/call " *> char_ '@' *> (SendCallInvitation <$> displayNameP <*> pure defaultCallType), + "/_call reject @" *> (APIRejectCall <$> A.decimal), + "/_call offer @" *> (APISendCallOffer <$> A.decimal <* A.space <*> jsonP), + "/_call answer @" *> (APISendCallAnswer <$> A.decimal <* A.space <*> jsonP), + "/_call extra @" *> (APISendCallExtraInfo <$> A.decimal <* A.space <*> jsonP), + "/_call end @" *> (APIEndCall <$> A.decimal), + "/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP), + "/_call get" $> APIGetCallInvitations, + "/_network_statuses" $> APIGetNetworkStatuses, + "/_profile " *> (APIUpdateProfile <$> A.decimal <* A.space <*> jsonP), + "/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), + "/_set alias #" *> (APISetGroupAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), + "/_set alias :" *> (APISetConnectionAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), + "/_set prefs @" *> (APISetContactPrefs <$> A.decimal <* A.space <*> jsonP), + "/_set theme user " *> (APISetUserUIThemes <$> A.decimal <*> optional (A.space *> jsonP)), + "/_set theme " *> (APISetChatUIThemes <$> chatRefP <*> optional (A.space *> jsonP)), + "/_ntf get" $> APIGetNtfToken, + "/_ntf register " *> (APIRegisterToken <$> strP_ <*> strP), + "/_ntf verify " *> (APIVerifyToken <$> strP <* A.space <*> strP <* A.space <*> strP), + "/_ntf check " *> (APICheckToken <$> strP), + "/_ntf delete " *> (APIDeleteToken <$> strP), + "/_ntf conns " *> (APIGetNtfConns <$> strP <* A.space <*> strP), + "/_ntf conn messages " *> (APIGetConnNtfMessages <$> connMsgsP), + "/_add #" *> (APIAddMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole), + "/_join #" *> (APIJoinGroup <$> A.decimal <*> pure MFAll), -- needs to be changed to support in UI + "/_accept member #" *> (APIAcceptMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole), + "/_member role #" *> (APIMembersRole <$> A.decimal <*> _strP <*> memberRole), + "/_block #" *> (APIBlockMembersForAll <$> A.decimal <*> _strP <* " blocked=" <*> onOffP), + "/_remove #" *> (APIRemoveMembers <$> A.decimal <*> _strP <*> (" messages=" *> onOffP <|> pure False)), + "/_leave #" *> (APILeaveGroup <$> A.decimal), + "/_members #" *> (APIListMembers <$> A.decimal), + "/_server test " *> (APITestProtoServer <$> A.decimal <* A.space <*> strP), + "/smp test " *> (TestProtoServer . AProtoServerWithAuth SPSMP <$> strP), + "/xftp test " *> (TestProtoServer . AProtoServerWithAuth SPXFTP <$> strP), + "/ntf test " *> (TestProtoServer . AProtoServerWithAuth SPNTF <$> strP), + "/smp " *> (SetUserProtoServers (AProtocolType SPSMP) . map (AProtoServerWithAuth SPSMP) <$> protocolServersP), + "/xftp " *> (SetUserProtoServers (AProtocolType SPXFTP) . map (AProtoServerWithAuth SPXFTP) <$> protocolServersP), + "/smp" $> GetUserProtoServers (AProtocolType SPSMP), + "/xftp" $> GetUserProtoServers (AProtocolType SPXFTP), + "/_operators" $> APIGetServerOperators, + "/_operators " *> (APISetServerOperators <$> jsonP), + "/operators " *> (SetServerOperators . L.fromList <$> operatorRolesP `A.sepBy1` A.char ','), + "/_servers " *> (APIGetUserServers <$> A.decimal), + "/_servers " *> (APISetUserServers <$> A.decimal <* A.space <*> jsonP), + "/_validate_servers " *> (APIValidateServers <$> A.decimal <* A.space <*> jsonP), + "/_conditions" $> APIGetUsageConditions, + "/_conditions_notified " *> (APISetConditionsNotified <$> A.decimal), + "/_accept_conditions " *> (APIAcceptConditions <$> A.decimal <*> _strP), + "/_ttl " *> (APISetChatItemTTL <$> A.decimal <* A.space <*> A.decimal), + "/_ttl " *> (APISetChatTTL <$> A.decimal <* A.space <*> chatRefP <* A.space <*> ciTTLDecimal), + "/_ttl " *> (APIGetChatItemTTL <$> A.decimal), + "/ttl " *> (SetChatItemTTL <$> ciTTL), + "/ttl" $> GetChatItemTTL, + "/ttl " *> (SetChatTTL <$> chatNameP <* A.space <*> (("default" $> Nothing) <|> (Just <$> ciTTL))), + "/ttl " *> (GetChatTTL <$> chatNameP), + "/_network info " *> (APISetNetworkInfo <$> jsonP), + "/_network " *> (APISetNetworkConfig <$> jsonP), + ("/network " <|> "/net ") *> (SetNetworkConfig <$> netCfgP), + ("/network" <|> "/net") $> APIGetNetworkConfig, + "/reconnect " *> (ReconnectServer <$> A.decimal <* A.space <*> strP), + "/reconnect" $> ReconnectAllServers, + "/_settings " *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP), + "/_member settings #" *> (APISetMemberSettings <$> A.decimal <* A.space <*> A.decimal <* A.space <*> jsonP), + "/_info #" *> (APIGroupMemberInfo <$> A.decimal <* A.space <*> A.decimal), + "/_info #" *> (APIGroupInfo <$> A.decimal), + "/_info @" *> (APIContactInfo <$> A.decimal), + ("/info #" <|> "/i #") *> (GroupMemberInfo <$> displayNameP <* A.space <* char_ '@' <*> displayNameP), + ("/info #" <|> "/i #") *> (ShowGroupInfo <$> displayNameP), + ("/info " <|> "/i ") *> char_ '@' *> (ContactInfo <$> displayNameP), + "/_queue info #" *> (APIGroupMemberQueueInfo <$> A.decimal <* A.space <*> A.decimal), + "/_queue info @" *> (APIContactQueueInfo <$> A.decimal), + ("/queue info #" <|> "/qi #") *> (GroupMemberQueueInfo <$> displayNameP <* A.space <* char_ '@' <*> displayNameP), + ("/queue info " <|> "/qi ") *> char_ '@' *> (ContactQueueInfo <$> displayNameP), + "/_switch #" *> (APISwitchGroupMember <$> A.decimal <* A.space <*> A.decimal), + "/_switch @" *> (APISwitchContact <$> A.decimal), + "/_abort switch #" *> (APIAbortSwitchGroupMember <$> A.decimal <* A.space <*> A.decimal), + "/_abort switch @" *> (APIAbortSwitchContact <$> A.decimal), + "/_sync #" *> (APISyncGroupMemberRatchet <$> A.decimal <* A.space <*> A.decimal <*> (" force=on" $> True <|> pure False)), + "/_sync @" *> (APISyncContactRatchet <$> A.decimal <*> (" force=on" $> True <|> pure False)), + "/switch #" *> (SwitchGroupMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP), + "/switch " *> char_ '@' *> (SwitchContact <$> displayNameP), + "/abort switch #" *> (AbortSwitchGroupMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP), + "/abort switch " *> char_ '@' *> (AbortSwitchContact <$> displayNameP), + "/sync #" *> (SyncGroupMemberRatchet <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> (" force=on" $> True <|> pure False)), + "/sync " *> char_ '@' *> (SyncContactRatchet <$> displayNameP <*> (" force=on" $> True <|> pure False)), + "/_get code @" *> (APIGetContactCode <$> A.decimal), + "/_get code #" *> (APIGetGroupMemberCode <$> A.decimal <* A.space <*> A.decimal), + "/_verify code @" *> (APIVerifyContact <$> A.decimal <*> optional (A.space *> verifyCodeP)), + "/_verify code #" *> (APIVerifyGroupMember <$> A.decimal <* A.space <*> A.decimal <*> optional (A.space *> verifyCodeP)), + "/_enable @" *> (APIEnableContact <$> A.decimal), + "/_enable #" *> (APIEnableGroupMember <$> A.decimal <* A.space <*> A.decimal), + "/code " *> char_ '@' *> (GetContactCode <$> displayNameP), + "/code #" *> (GetGroupMemberCode <$> displayNameP <* A.space <* char_ '@' <*> displayNameP), + "/verify " *> char_ '@' *> (VerifyContact <$> displayNameP <*> optional (A.space *> verifyCodeP)), + "/verify #" *> (VerifyGroupMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> optional (A.space *> verifyCodeP)), + "/enable " *> char_ '@' *> (EnableContact <$> displayNameP), + "/enable #" *> (EnableGroupMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP), + ("/help files" <|> "/help file" <|> "/hf") $> ChatHelp HSFiles, + ("/help groups" <|> "/help group" <|> "/hg") $> ChatHelp HSGroups, + ("/help contacts" <|> "/help contact" <|> "/hc") $> ChatHelp HSContacts, + ("/help address" <|> "/ha") $> ChatHelp HSMyAddress, + ("/help incognito" <|> "/hi") $> ChatHelp HSIncognito, + ("/help messages" <|> "/hm") $> ChatHelp HSMessages, + ("/help remote" <|> "/hr") $> ChatHelp HSRemote, + ("/help settings" <|> "/hs") $> ChatHelp HSSettings, + ("/help db" <|> "/hd") $> ChatHelp HSDatabase, + ("/help" <|> "/h") $> ChatHelp HSMain, + ("/group" <|> "/g") *> (NewGroup <$> incognitoP <* A.space <* char_ '#' <*> groupProfile), + "/_group " *> (APINewGroup <$> A.decimal <*> incognitoOnOffP <* A.space <*> jsonP), + ("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> (memberRole <|> pure GRMember)), + ("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayNameP <*> (" mute" $> MFNone <|> pure MFAll)), + ("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> memberRole), + "/block for all #" *> (BlockForAll <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure True), + "/unblock for all #" *> (BlockForAll <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure False), + ("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMembers <$> displayNameP <* A.space <*> (S.fromList <$> (char_ '@' *> displayNameP) `A.sepBy1'` A.char ',') <*> (" messages=" *> onOffP <|> pure False)), + ("/leave " <|> "/l ") *> char_ '#' *> (LeaveGroup <$> displayNameP), + ("/delete #" <|> "/d #") *> (DeleteGroup <$> displayNameP), + ("/delete " <|> "/d ") *> char_ '@' *> (DeleteContact <$> displayNameP <*> chatDeleteMode), + "/clear *" $> ClearNoteFolder, + "/clear #" *> (ClearGroup <$> displayNameP), + "/clear " *> char_ '@' *> (ClearContact <$> displayNameP), + ("/members " <|> "/ms ") *> char_ '#' *> (ListMembers <$> displayNameP), + "/_groups" *> (APIListGroups <$> A.decimal <*> optional (" @" *> A.decimal) <*> optional (A.space *> stringP)), + ("/groups" <|> "/gs") *> (ListGroups <$> optional (" @" *> displayNameP) <*> optional (A.space *> stringP)), + "/_group_profile #" *> (APIUpdateGroupProfile <$> A.decimal <* A.space <*> jsonP), + ("/group_profile " <|> "/gp ") *> char_ '#' *> (UpdateGroupNames <$> displayNameP <* A.space <*> groupProfile), + ("/group_profile " <|> "/gp ") *> char_ '#' *> (ShowGroupProfile <$> displayNameP), + "/group_descr " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <*> optional (A.space *> msgTextP)), + "/set welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <* A.space <*> (Just <$> msgTextP)), + "/delete welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <*> pure Nothing), + "/show welcome " *> char_ '#' *> (ShowGroupDescription <$> displayNameP), + "/_create link #" *> (APICreateGroupLink <$> A.decimal <*> (memberRole <|> pure GRMember) <*> shortOnOffP), + "/_set link role #" *> (APIGroupLinkMemberRole <$> A.decimal <*> memberRole), + "/_delete link #" *> (APIDeleteGroupLink <$> A.decimal), + "/_get link #" *> (APIGetGroupLink <$> A.decimal), + "/create link #" *> (CreateGroupLink <$> displayNameP <*> (memberRole <|> pure GRMember) <*> shortP), + "/set link role #" *> (GroupLinkMemberRole <$> displayNameP <*> memberRole), + "/delete link #" *> (DeleteGroupLink <$> displayNameP), + "/show link #" *> (ShowGroupLink <$> displayNameP), + "/_create member contact #" *> (APICreateMemberContact <$> A.decimal <* A.space <*> A.decimal), + "/_invite member contact @" *> (APISendMemberContactInvitation <$> A.decimal <*> optional (A.space *> msgContentP)), + (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayNameP <* A.space <*> pure Nothing <*> quotedMsg <*> msgTextP), + (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayNameP <* A.space <* char_ '@' <*> (Just <$> displayNameP) <* A.space <*> quotedMsg <*> msgTextP), + "/_contacts " *> (APIListContacts <$> A.decimal), + "/contacts" $> ListContacts, + "/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> strP), + "/_connect " *> (APIAddContact <$> A.decimal <*> shortOnOffP <*> incognitoOnOffP), + "/_connect " *> (APIConnect <$> A.decimal <*> incognitoOnOffP <* A.space <*> connLinkP), + "/_set incognito :" *> (APISetConnectionIncognito <$> A.decimal <* A.space <*> onOffP), + "/_set conn user :" *> (APIChangeConnectionUser <$> A.decimal <* A.space <*> A.decimal), + ("/connect" <|> "/c") *> (AddContact <$> shortP <*> incognitoP), + ("/connect" <|> "/c") *> (Connect <$> incognitoP <* A.space <*> ((Just <$> strP) <|> A.takeTill isSpace $> Nothing)), + ForwardMessage <$> chatNameP <* " <- @" <*> displayNameP <* A.space <*> msgTextP, + ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <* A.space <* A.char '@' <*> (Just <$> displayNameP) <* A.space <*> msgTextP, + ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <*> pure Nothing <* A.space <*> msgTextP, + ForwardLocalMessage <$> chatNameP <* " <- * " <*> msgTextP, + SendMessage <$> chatNameP <* A.space <*> msgTextP, + "/* " *> (SendMessage (ChatName CTLocal "") <$> msgTextP), + "@#" *> (SendMemberContactMessage <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <* A.space <*> msgTextP), + "/live " *> (SendLiveMessage <$> chatNameP <*> (A.space *> msgTextP <|> pure "")), + (">@" <|> "> @") *> sendMsgQuote (AMsgDirection SMDRcv), + (">>@" <|> ">> @") *> sendMsgQuote (AMsgDirection SMDSnd), + ("\\ " <|> "\\") *> (DeleteMessage <$> chatNameP <* A.space <*> textP), + ("\\\\ #" <|> "\\\\#") *> (DeleteMemberMessage <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <* A.space <*> textP), + ("! " <|> "!") *> (EditMessage <$> chatNameP <* A.space <*> (quotedMsg <|> pure "") <*> msgTextP), + ReactToMessage <$> (("+" $> True) <|> ("-" $> False)) <*> reactionP <* A.space <*> chatNameP' <* A.space <*> textP, + "/feed " *> (SendMessageBroadcast . MCText <$> msgTextP), + ("/chats" <|> "/cs") *> (LastChats <$> (" all" $> Nothing <|> Just <$> (A.space *> A.decimal <|> pure 20))), + ("/tail" <|> "/t") *> (LastMessages <$> optional (A.space *> chatNameP) <*> msgCountP <*> pure Nothing), + ("/search" <|> "/?") *> (LastMessages <$> optional (A.space *> chatNameP) <*> msgCountP <*> (Just <$> (A.space *> stringP))), + "/last_item_id" *> (LastChatItemId <$> optional (A.space *> chatNameP) <*> (A.space *> A.decimal <|> pure 0)), + "/show" *> (ShowLiveItems <$> (A.space *> onOffP <|> pure True)), + "/show " *> (ShowChatItem . Just <$> A.decimal), + "/item info " *> (ShowChatItemInfo <$> chatNameP <* A.space <*> msgTextP), + ("/file " <|> "/f ") *> (SendFile <$> chatNameP' <* A.space <*> cryptoFileP), + ("/image " <|> "/img ") *> (SendImage <$> chatNameP' <* A.space <*> cryptoFileP), + ("/fforward " <|> "/ff ") *> (ForwardFile <$> chatNameP' <* A.space <*> A.decimal), + ("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal), + ("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* A.space <*> filePath), + ("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> (" approved_relays=" *> onOffP <|> pure False) <*> optional (" encrypt=" *> onOffP) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)), + "/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> (" approved_relays=" *> onOffP <|> pure False) <*> optional (" encrypt=" *> onOffP)), + ("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal), + ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal), + "/_connect contact " *> (APIConnectContactViaAddress <$> A.decimal <*> incognitoOnOffP <* A.space <*> A.decimal), + "/simplex" *> (ConnectSimplex <$> incognitoP), + "/_address " *> (APICreateMyAddress <$> A.decimal <*> shortOnOffP), + ("/address" <|> "/ad") *> (CreateMyAddress <$> shortP), + "/_delete_address " *> (APIDeleteMyAddress <$> A.decimal), + ("/delete_address" <|> "/da") $> DeleteMyAddress, + "/_show_address " *> (APIShowMyAddress <$> A.decimal), + ("/show_address" <|> "/sa") $> ShowMyAddress, + "/_profile_address " *> (APISetProfileAddress <$> A.decimal <* A.space <*> onOffP), + ("/profile_address " <|> "/pa ") *> (SetProfileAddress <$> onOffP), + "/_auto_accept " *> (APIAddressAutoAccept <$> A.decimal <* A.space <*> autoAcceptP), + "/auto_accept " *> (AddressAutoAccept <$> autoAcceptP), + ("/accept" <|> "/ac") *> (AcceptContact <$> incognitoP <* A.space <* char_ '@' <*> displayNameP), + ("/reject " <|> "/rc ") *> char_ '@' *> (RejectContact <$> displayNameP), + ("/markdown" <|> "/m") $> ChatHelp HSMarkdown, + ("/welcome" <|> "/w") $> Welcome, + "/set profile image " *> (UpdateProfileImage . Just . ImageData <$> imageP), + "/delete profile image" $> UpdateProfileImage Nothing, + "/show profile image" $> ShowProfileImage, + ("/profile " <|> "/p ") *> (uncurry UpdateProfile <$> profileNames), + ("/profile" <|> "/p") $> ShowProfile, + "/set voice #" *> (SetGroupFeatureRole (AGFR SGFVoice) <$> displayNameP <*> _strP <*> optional memberRole), + "/set voice @" *> (SetContactFeature (ACF SCFVoice) <$> displayNameP <*> optional (A.space *> strP)), + "/set voice " *> (SetUserFeature (ACF SCFVoice) <$> strP), + "/set files #" *> (SetGroupFeatureRole (AGFR SGFFiles) <$> displayNameP <*> _strP <*> optional memberRole), + "/set history #" *> (SetGroupFeature (AGFNR SGFHistory) <$> displayNameP <*> (A.space *> strP)), + "/set reactions #" *> (SetGroupFeature (AGFNR SGFReactions) <$> displayNameP <*> (A.space *> strP)), + "/set calls @" *> (SetContactFeature (ACF SCFCalls) <$> displayNameP <*> optional (A.space *> strP)), + "/set calls " *> (SetUserFeature (ACF SCFCalls) <$> strP), + "/set delete #" *> (SetGroupFeatureRole (AGFR SGFFullDelete) <$> displayNameP <*> _strP <*> optional memberRole), + "/set delete @" *> (SetContactFeature (ACF SCFFullDelete) <$> displayNameP <*> optional (A.space *> strP)), + "/set delete " *> (SetUserFeature (ACF SCFFullDelete) <$> strP), + "/set direct #" *> (SetGroupFeatureRole (AGFR SGFDirectMessages) <$> displayNameP <*> _strP <*> optional memberRole), + "/set disappear #" *> (SetGroupTimedMessages <$> displayNameP <*> (A.space *> timedTTLOnOffP)), + "/set disappear @" *> (SetContactTimedMessages <$> displayNameP <*> optional (A.space *> timedMessagesEnabledP)), + "/set disappear " *> (SetUserTimedMessages <$> (("yes" $> True) <|> ("no" $> False))), + "/set reports #" *> (SetGroupFeature (AGFNR SGFReports) <$> displayNameP <*> _strP), + "/set links #" *> (SetGroupFeatureRole (AGFR SGFSimplexLinks) <$> displayNameP <*> _strP <*> optional memberRole), + ("/incognito" <* optional (A.space *> onOffP)) $> ChatHelp HSIncognito, + "/set device name " *> (SetLocalDeviceName <$> textP), + "/list remote hosts" $> ListRemoteHosts, + "/switch remote host " *> (SwitchRemoteHost <$> ("local" $> Nothing <|> (Just <$> A.decimal))), + "/start remote host " *> (StartRemoteHost <$> ("new" $> Nothing <|> (Just <$> ((,) <$> A.decimal <*> (" multicast=" *> onOffP <|> pure False)))) <*> optional (A.space *> rcCtrlAddressP) <*> optional (" port=" *> A.decimal)), + "/stop remote host " *> (StopRemoteHost <$> ("new" $> RHNew <|> RHId <$> A.decimal)), + "/delete remote host " *> (DeleteRemoteHost <$> A.decimal), + "/store remote file " *> (StoreRemoteFile <$> A.decimal <*> optional (" encrypt=" *> onOffP) <* A.space <*> filePath), + "/get remote file " *> (GetRemoteFile <$> A.decimal <* A.space <*> jsonP), + ("/connect remote ctrl " <|> "/crc ") *> (ConnectRemoteCtrl <$> strP), + "/find remote ctrl" $> FindKnownRemoteCtrl, + "/confirm remote ctrl " *> (ConfirmRemoteCtrl <$> A.decimal), + "/verify remote ctrl " *> (VerifyRemoteCtrlSession <$> textP), + "/list remote ctrls" $> ListRemoteCtrls, + "/stop remote ctrl" $> StopRemoteCtrl, + "/delete remote ctrl " *> (DeleteRemoteCtrl <$> A.decimal), + "/_upload " *> (APIUploadStandaloneFile <$> A.decimal <* A.space <*> cryptoFileP), + "/_download info " *> (APIStandaloneFileInfo <$> strP), + "/_download " *> (APIDownloadStandaloneFile <$> A.decimal <* A.space <*> strP_ <*> cryptoFileP), + ("/quit" <|> "/q" <|> "/exit") $> QuitChat, + ("/version" <|> "/v") $> ShowVersion, + "/debug locks" $> DebugLocks, + "/debug event " *> (DebugEvent <$> jsonP), + "/get subs total " *> (GetAgentSubsTotal <$> A.decimal), + "/get servers summary " *> (GetAgentServersSummary <$> A.decimal), + "/reset servers stats" $> ResetAgentServersStats, + "/get subs" $> GetAgentSubs, + "/get subs details" $> GetAgentSubsDetails, + "/get workers" $> GetAgentWorkers, + "/get workers details" $> GetAgentWorkersDetails, + "/get queues" $> GetAgentQueuesInfo, + "//" *> (CustomChatCommand <$> A.takeByteString) + ] + where + choice = A.choice . map (\p -> p <* A.takeWhile (== ' ') <* A.endOfInput) + connLinkP = do + ((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing) + >>= mapM (\(ACR m cReq) -> ACCL m . CCLink cReq <$> optional (A.space *> strP)) + shortP = (A.space *> ("short" <|> "s")) $> True <|> pure False + incognitoP = (A.space *> ("incognito" <|> "i")) $> True <|> pure False + shortOnOffP = (A.space *> "short=" *> onOffP) <|> pure False + incognitoOnOffP = (A.space *> "incognito=" *> onOffP) <|> pure False + imagePrefix = (<>) <$> "data:" <*> ("image/png;base64," <|> "image/jpg;base64,") + imageP = safeDecodeUtf8 <$> ((<>) <$> imagePrefix <*> (B64.encode <$> base64P)) + chatTypeP = A.char '@' $> CTDirect <|> A.char '#' $> CTGroup <|> A.char '*' $> CTLocal <|> A.char ':' $> CTContactConnection + chatPaginationP = + (CPLast <$ "count=" <*> A.decimal) + <|> (CPAfter <$ "after=" <*> A.decimal <* A.space <* "count=" <*> A.decimal) + <|> (CPBefore <$ "before=" <*> A.decimal <* A.space <* "count=" <*> A.decimal) + <|> (CPAround <$ "around=" <*> A.decimal <* A.space <* "count=" <*> A.decimal) + <|> (CPInitial <$ "initial=" <*> A.decimal) + paginationByTimeP = + (PTLast <$ "count=" <*> A.decimal) + <|> (PTAfter <$ "after=" <*> strP <* A.space <* "count=" <*> A.decimal) + <|> (PTBefore <$ "before=" <*> strP <* A.space <* "count=" <*> A.decimal) + mcTextP = MCText . safeDecodeUtf8 <$> A.takeByteString + msgContentP = "text " *> mcTextP <|> "json " *> jsonP + chatDeleteMode = + A.choice + [ " full" *> (CDMFull <$> notifyP), + " entity" *> (CDMEntity <$> notifyP), + " messages" $> CDMMessages, + CDMFull <$> notifyP -- backwards compatible + ] + where + notifyP = " notify=" *> onOffP <|> pure True + sendMsgQuote msgDir = SendMessageQuote <$> displayNameP <* A.space <*> pure msgDir <*> quotedMsg <*> msgTextP + quotedMsg = safeDecodeUtf8 <$> (A.char '(' *> A.takeTill (== ')') <* A.char ')') <* optional A.space + reactionP = MREmoji <$> (mrEmojiChar <$?> (toEmoji <$> A.anyChar)) + toEmoji = \case + '1' -> '👍' + '+' -> '👍' + '-' -> '👎' + ')' -> '😀' + ',' -> '😢' + '*' -> head "❤️" + '^' -> '🚀' + c -> c + composedMessagesTextP = do + text <- mcTextP + pure [composedMessage Nothing text] + updatedMessagesTextP = (`UpdatedMessage` []) <$> mcTextP + liveMessageP = " live=" *> onOffP <|> pure False + sendMessageTTLP = " ttl=" *> ((Just <$> A.decimal) <|> ("default" $> Nothing)) <|> pure Nothing + receiptSettings = do + enable <- onOffP + clearOverrides <- (" clear_overrides=" *> onOffP) <|> pure False + pure UserMsgReceiptSettings {enable, clearOverrides} + onOffP = ("on" $> True) <|> ("off" $> False) + profileNames = (,) <$> displayNameP <*> fullNameP + newUserP = do + (cName, fullName) <- profileNames + let profile = Just Profile {displayName = cName, fullName, image = Nothing, contactLink = Nothing, preferences = Nothing} + pure NewUser {profile, pastTimestamp = False} + jsonP :: J.FromJSON a => Parser a + jsonP = J.eitherDecodeStrict' <$?> A.takeByteString + groupProfile = do + (gName, fullName) <- profileNames + let groupPreferences = + Just + (emptyGroupPrefs :: GroupPreferences) + { directMessages = Just DirectMessagesGroupPreference {enable = FEOn, role = Nothing}, + history = Just HistoryGroupPreference {enable = FEOn} + } + pure GroupProfile {displayName = gName, fullName, description = Nothing, image = Nothing, groupPreferences, memberAdmission = Nothing} + fullNameP = A.space *> textP <|> pure "" + textP = safeDecodeUtf8 <$> A.takeByteString + pwdP = jsonP <|> (UserPwd . safeDecodeUtf8 <$> A.takeTill (== ' ')) + verifyCodeP = safeDecodeUtf8 <$> A.takeWhile (\c -> isDigit c || c == ' ') + msgTextP = jsonP <|> textP + stringP = T.unpack . safeDecodeUtf8 <$> A.takeByteString + filePath = stringP + cryptoFileP = do + cfArgs <- optional $ CFArgs <$> (" key=" *> strP <* A.space) <*> (" nonce=" *> strP) + path <- filePath + pure $ CryptoFile path cfArgs + connMsgsP = L.fromList <$> connMsgP `A.sepBy1'` A.char ',' + connMsgP = do + AgentConnId msgConnId <- strP <* A.char ':' + msgDbQueueId <- strP <* A.char ':' + ts <- strP + pure ConnMsgReq {msgConnId, msgDbQueueId, msgTs = Just ts} + memberRole = + A.choice + [ " owner" $> GROwner, + " admin" $> GRAdmin, + " moderator" $> GRModerator, + " member" $> GRMember, + " observer" $> GRObserver + ] + chatNameP = + chatTypeP >>= \case + CTLocal -> pure $ ChatName CTLocal "" + ct -> ChatName ct <$> displayNameP + chatNameP' = ChatName <$> (chatTypeP <|> pure CTDirect) <*> displayNameP + chatRefP = ChatRef <$> chatTypeP <*> A.decimal + sendRefP = + (A.char '@' $> SRDirect <*> A.decimal) + <|> (A.char '#' $> SRGroup <*> A.decimal <*> optional (" @" *> A.decimal)) + msgCountP = A.space *> A.decimal <|> pure 10 + ciTTLDecimal = ("default" $> Nothing) <|> (Just <$> A.decimal) + ciTTL = + ("day" $> 86400) + <|> ("week" $> (7 * 86400)) + <|> ("month" $> (30 * 86400)) + <|> ("year" $> (365 * 86400)) + <|> ("none" $> 0) + timedTTLP = + ("30s" $> 30) + <|> ("5min" $> 300) + <|> ("1h" $> 3600) + <|> ("8h" $> (8 * 3600)) + <|> ("day" $> 86400) + <|> ("week" $> (7 * 86400)) + <|> ("month" $> (30 * 86400)) + <|> A.decimal + timedTTLOnOffP = + optional ("on" *> A.space) *> (Just <$> timedTTLP) + <|> ("off" $> Nothing) + timedMessagesEnabledP = + optional ("yes" *> A.space) *> (TMEEnableSetTTL <$> timedTTLP) + <|> ("yes" $> TMEEnableKeepTTL) + <|> ("no" $> TMEDisableKeepTTL) + operatorRolesP = do + operatorId' <- A.decimal + enabled' <- A.char ':' *> onOffP + smpRoles' <- (":smp=" *> srvRolesP) <|> pure allRoles + xftpRoles' <- (":xftp=" *> srvRolesP) <|> pure allRoles + pure ServerOperatorRoles {operatorId', enabled', smpRoles', xftpRoles'} + srvRolesP = srvRoles <$?> A.takeTill (\c -> c == ':' || c == ',') + where + srvRoles = \case + "off" -> Right $ ServerRoles False False + "proxy" -> Right ServerRoles {storage = False, proxy = True} + "storage" -> Right ServerRoles {storage = True, proxy = False} + "on" -> Right allRoles + _ -> Left "bad ServerRoles" + netCfgP = do + socksProxy <- "socks=" *> ("off" $> Nothing <|> "on" $> Just defaultSocksProxyWithAuth <|> Just <$> strP) + socksMode <- " socks-mode=" *> strP <|> pure SMAlways + hostMode <- " host-mode=" *> (textToHostMode . safeDecodeUtf8 <$?> A.takeTill (== ' ')) <|> pure (defaultHostMode socksProxy) + requiredHostMode <- (" required-host-mode" $> True) <|> pure False + smpProxyMode_ <- optional $ " smp-proxy=" *> strP + smpProxyFallback_ <- optional $ " smp-proxy-fallback=" *> strP + smpWebPortServers <- (" smp-web-port-servers=" *> strP) <|> (" smp-web-port" $> SWPAll) <|> pure SWPPreset + t_ <- optional $ " timeout=" *> A.decimal + logTLSErrors <- " log=" *> onOffP <|> pure False + let tcpTimeout_ = (1000000 *) <$> t_ + pure $ SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPortServers, tcpTimeout_, logTLSErrors} +#if !defined(dbPostgres) + dbKeyP = nonEmptyKey <$?> strP + nonEmptyKey k@(DBEncryptionKey s) = if BA.null s then Left "empty key" else Right k + dbEncryptionConfig currentKey newKey = DBEncryptionConfig {currentKey, newKey, keepKey = Just False} +#endif + autoAcceptP = ifM onOffP (Just <$> (businessAA <|> addressAA)) (pure Nothing) + where + addressAA = AutoAccept False <$> (" incognito=" *> onOffP <|> pure False) <*> autoReply + businessAA = AutoAccept True <$> (" business" *> pure False) <*> autoReply + autoReply = optional (A.space *> msgContentP) + rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (jsonP <|> text1P)) + text1P = safeDecodeUtf8 <$> A.takeTill (== ' ') + char_ = optional . A.char + +displayNameP :: Parser Text +displayNameP = safeDecodeUtf8 <$> (quoted '\'' <|> takeNameTill (\c -> isSpace c || c == ',')) + where + takeNameTill p = + A.peekChar' >>= \c -> + if refChar c then A.takeTill p else fail "invalid first character in display name" + quoted c = A.char c *> takeNameTill (== c) <* A.char c + refChar c = c > ' ' && c /= '#' && c /= '@' && c /= '\'' + +mkValidName :: String -> String +mkValidName = dropWhileEnd isSpace . take 50 . reverse . fst3 . foldl' addChar ("", '\NUL', 0 :: Int) + where + fst3 (x, _, _) = x + addChar (r, prev, punct) c = if validChar then (c' : r, c', punct') else (r, prev, punct) + where + c' = if isSpace c then ' ' else c + punct' + | isPunctuation c = punct + 1 + | isSpace c = punct + | otherwise = 0 + validChar + | c == '\'' = False + | prev == '\NUL' = c > ' ' && c /= '#' && c /= '@' && validFirstChar + | isSpace prev = validFirstChar || (punct == 0 && isPunctuation c) + | isPunctuation prev = validFirstChar || isSpace c || (punct < 3 && isPunctuation c) + | otherwise = validFirstChar || isSpace c || isMark c || isPunctuation c + validFirstChar = isLetter c || isNumber c || isSymbol c diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs new file mode 100644 index 0000000000..bbefbcfde0 --- /dev/null +++ b/src/Simplex/Chat/Library/Internal.hs @@ -0,0 +1,2275 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} + +module Simplex.Chat.Library.Internal where + +import Control.Applicative ((<|>)) +import Control.Concurrent.STM (retry) +import Control.Logger.Simple +import Control.Monad +import Control.Monad.Except +import Control.Monad.IO.Unlift +import Control.Monad.Reader +import Crypto.Random (ChaChaDRG) +import Data.Bifunctor (first) +import Data.ByteString.Char8 (ByteString) +import qualified Data.ByteString.Char8 as B +import Data.Char (isDigit) +import Data.Containers.ListUtils (nubOrd) +import Data.Either (partitionEithers, rights) +import Data.Fixed (div') +import Data.Foldable (foldr') +import Data.Functor (($>)) +import Data.Functor.Identity +import Data.Int (Int64) +import Data.List (find, mapAccumL, partition) +import Data.List.NonEmpty (NonEmpty (..), (<|)) +import qualified Data.List.NonEmpty as L +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as M +import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, mapMaybe) +import qualified Data.Set as S +import Data.Text (Text) +import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) +import Data.Time (addUTCTime) +import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds) +import Simplex.Chat.Call +import Simplex.Chat.Controller +import Simplex.Chat.Files +import Simplex.Chat.Markdown +import Simplex.Chat.Messages +import Simplex.Chat.Messages.Batch (MsgBatch (..), batchMessages) +import Simplex.Chat.Messages.CIContent +import Simplex.Chat.Messages.CIContent.Events +import Simplex.Chat.Operators +import Simplex.Chat.ProfileGenerator (generateRandomProfile) +import Simplex.Chat.Protocol +import Simplex.Chat.Store +import Simplex.Chat.Store.Connections +import Simplex.Chat.Store.Direct +import Simplex.Chat.Store.Files +import Simplex.Chat.Store.Groups +import Simplex.Chat.Store.Messages +import Simplex.Chat.Store.Profiles +import Simplex.Chat.Store.Shared +import Simplex.Chat.Types +import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared +import Simplex.Chat.Util (encryptFile, shuffle) +import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription) +import qualified Simplex.FileTransfer.Description as FD +import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI) +import Simplex.FileTransfer.Types (RcvFileId, SndFileId) +import Simplex.Messaging.Agent as Agent +import Simplex.Messaging.Agent.Client (getFastNetworkConfig, ipAddressProtected, withLockMap) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), ServerCfg (..)) +import Simplex.Messaging.Agent.Lock (withLock) +import Simplex.Messaging.Agent.Protocol +import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) +import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Client (NetworkConfig (..)) +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 PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) +import qualified Simplex.Messaging.Crypto.Ratchet as CR +import Simplex.Messaging.Encoding +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Protocol (MsgBody, MsgFlags (..), ProtoServerWithAuth (..), ProtocolServer, ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, XFTPServer) +import qualified Simplex.Messaging.Protocol as SMP +import qualified Simplex.Messaging.TMap as TM +import Simplex.Messaging.Util +import Simplex.Messaging.Version +import System.FilePath (takeFileName, (</>)) +import System.IO (Handle, IOMode (..), SeekMode (..), hFlush) +import UnliftIO.Concurrent (forkFinally, mkWeakThreadId) +import UnliftIO.Directory +import UnliftIO.IO (hClose, hSeek, hTell, openFile) +import UnliftIO.STM + +maxMsgReactions :: Int +maxMsgReactions = 3 + +maxRcvMentions :: Int +maxRcvMentions = 5 + +maxSndMentions :: Int +maxSndMentions = 3 + +withChatLock :: String -> CM a -> CM a +withChatLock name action = asks chatLock >>= \l -> withLock l name action + +withEntityLock :: String -> ChatLockEntity -> CM a -> CM a +withEntityLock name entity action = do + chatLock <- asks chatLock + ls <- asks entityLocks + atomically $ unlessM (isEmptyTMVar chatLock) retry + withLockMap ls entity name action + +withInvitationLock :: String -> ByteString -> CM a -> CM a +withInvitationLock name = withEntityLock name . CLInvitation +{-# INLINE withInvitationLock #-} + +withConnectionLock :: String -> Int64 -> CM a -> CM a +withConnectionLock name = withEntityLock name . CLConnection +{-# INLINE withConnectionLock #-} + +withContactLock :: String -> ContactId -> CM a -> CM a +withContactLock name = withEntityLock name . CLContact +{-# INLINE withContactLock #-} + +withGroupLock :: String -> GroupId -> CM a -> CM a +withGroupLock name = withEntityLock name . CLGroup +{-# INLINE withGroupLock #-} + +withUserContactLock :: String -> Int64 -> CM a -> CM a +withUserContactLock name = withEntityLock name . CLUserContact +{-# INLINE withUserContactLock #-} + +withFileLock :: String -> Int64 -> CM a -> CM a +withFileLock name = withEntityLock name . CLFile +{-# INLINE withFileLock #-} + +useServerCfgs :: forall p. UserProtocol p => SProtocolType p -> RandomAgentServers -> [(Text, ServerOperator)] -> [UserServer p] -> NonEmpty (ServerCfg p) +useServerCfgs p RandomAgentServers {smpServers, xftpServers} opDomains = + fromMaybe (rndAgentServers p) . L.nonEmpty . agentServerCfgs p opDomains + where + rndAgentServers :: SProtocolType p -> NonEmpty (ServerCfg p) + rndAgentServers = \case + SPSMP -> smpServers + SPXFTP -> xftpServers + +contactCITimed :: Contact -> CM (Maybe CITimed) +contactCITimed ct = sndContactCITimed False ct Nothing + +sndContactCITimed :: Bool -> Contact -> Maybe Int -> CM (Maybe CITimed) +sndContactCITimed live = sndCITimed_ live . contactTimedTTL + +sndGroupCITimed :: Bool -> GroupInfo -> Maybe Int -> CM (Maybe CITimed) +sndGroupCITimed live = sndCITimed_ live . groupTimedTTL + +sndCITimed_ :: Bool -> Maybe (Maybe Int) -> Maybe Int -> CM (Maybe CITimed) +sndCITimed_ live chatTTL itemTTL = + forM (chatTTL >>= (itemTTL <|>)) $ \ttl -> + CITimed ttl + <$> if live + then pure Nothing + else Just . addUTCTime (realToFrac ttl) <$> liftIO getCurrentTime + +callTimed :: Contact -> ACIContent -> CM (Maybe CITimed) +callTimed ct aciContent = + case aciContentCallStatus aciContent of + Just callStatus + | callComplete callStatus -> do + contactCITimed ct + _ -> pure Nothing + where + aciContentCallStatus :: ACIContent -> Maybe CICallStatus + aciContentCallStatus (ACIContent _ (CISndCall st _)) = Just st + aciContentCallStatus (ACIContent _ (CIRcvCall st _)) = Just st + aciContentCallStatus _ = Nothing + +toggleNtf :: GroupMember -> Bool -> CM () +toggleNtf m ntfOn = + when (memberActive m) $ + forM_ (memberConnId m) $ \connId -> + withAgent (\a -> toggleConnectionNtfs a connId ntfOn) `catchChatError` eToView + +prepareGroupMsg :: DB.Connection -> User -> GroupInfo -> 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} mc mentions quotedItemId_ itemForwarded fInv_ timed_ live = case (quotedItemId_, itemForwarded) of + (Nothing, Nothing) -> + let mc' = MCSimple $ ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live) + in pure (XMsgNew mc', Nothing) + (Nothing, Just _) -> + let mc' = MCForward $ ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live) + 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, GroupMember {memberId}) <- quoteData qci membership + let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Just memberId} + 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)) + pure (XMsgNew mc', Just quotedItem) + (Just _, Just _) -> throwError SEInvalidQuote + where + quoteData :: ChatItem c d -> GroupMember -> ExceptT StoreError IO (MsgContent, CIQDirection 'CTGroup, Bool, GroupMember) + quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} _ = throwError SEInvalidQuote + quoteData ChatItem {chatDir = CIGroupSnd, content = CISndMsgContent qmc} membership' = pure (qmc, CIQGroupSnd, True, membership') + quoteData ChatItem {chatDir = CIGroupRcv m, content = CIRcvMsgContent qmc} _ = pure (qmc, CIQGroupRcv $ Just m, False, m) + quoteData _ _ = throwError SEInvalidQuote + +updatedMentionNames :: MsgContent -> Maybe MarkdownList -> Map MemberName CIMention -> (MsgContent, Maybe MarkdownList, Map MemberName CIMention) +updatedMentionNames mc ft_ mentions = case ft_ of + Just ft + | not (null ft) && not (null mentions) && not (all sameName $ M.assocs mentions) -> + let (mentions', ft') = mapAccumL update M.empty ft + text = T.concat $ map markdownText ft' + in (mc {text} :: MsgContent, Just ft', mentions') + _ -> (mc, ft_, mentions) + where + sameName (name, CIMention {memberRef}) = case memberRef of + Just CIMentionMember {displayName} -> case T.stripPrefix displayName name of + Just rest + | T.null rest -> True + | otherwise -> case T.uncons rest of + Just ('_', suffix) -> T.all isDigit suffix + _ -> False + Nothing -> False + Nothing -> True + update mentions' ft@(FormattedText f _) = case f of + Just (Mention name) -> case M.lookup name mentions of + Just mm@CIMention {memberRef} -> + let name' = uniqueMentionName 0 $ case memberRef of + Just CIMentionMember {displayName} -> displayName + Nothing -> name + in (M.insert name' mm mentions', FormattedText (Just $ Mention name') ('@' `T.cons` viewName name')) + Nothing -> (mentions', ft) + _ -> (mentions', ft) + where + uniqueMentionName :: Int -> Text -> Text + uniqueMentionName pfx name = + let prefixed = if pfx == 0 then name else (name `T.snoc` '_') <> tshow pfx + in if prefixed `M.member` mentions' then uniqueMentionName (pfx + 1) name else prefixed + +getCIMentions :: DB.Connection -> User -> GroupInfo -> Maybe MarkdownList -> Map MemberName GroupMemberId -> ExceptT StoreError IO (Map MemberName CIMention) +getCIMentions db user GroupInfo {groupId} ft_ mentions = case ft_ of + Just ft | not (null ft) && not (null mentions) -> do + let msgMentions = S.fromList $ mentionedNames ft + n = M.size mentions + -- prevent "invisible" and repeated-with-different-name mentions (when the same member is mentioned via another name) + unless (n <= maxSndMentions && all (`S.member` msgMentions) (M.keys mentions) && S.size (S.fromList $ M.elems mentions) == n) $ + throwError SEInvalidMention + mapM (getMentionedGroupMember db user groupId) mentions + _ -> pure M.empty + +getRcvCIMentions :: DB.Connection -> User -> GroupInfo -> Maybe MarkdownList -> Map MemberName MsgMention -> IO (Map MemberName CIMention) +getRcvCIMentions db user GroupInfo {groupId} ft_ mentions = case ft_ of + Just ft + | not (null ft) && not (null mentions) -> + let mentions' = uniqueMsgMentions maxRcvMentions mentions $ mentionedNames ft + in mapM (getMentionedMemberByMemberId db user groupId) mentions' + _ -> pure M.empty + +-- prevent "invisible" and repeated-with-different-name mentions +uniqueMsgMentions :: Int -> Map MemberName MsgMention -> [ContactName] -> Map MemberName MsgMention +uniqueMsgMentions maxMentions mentions = go M.empty S.empty 0 + where + go acc _ _ [] = acc + go acc seen n (name : rest) + | n >= maxMentions = acc + | otherwise = case M.lookup name mentions of + Just mm@MsgMention {memberId} + | S.notMember memberId seen -> + go (M.insert name mm acc) (S.insert memberId seen) (n + 1) rest + _ -> go acc seen n rest + +getMessageMentions :: DB.Connection -> User -> GroupId -> Text -> IO (Map MemberName GroupMemberId) +getMessageMentions db user gId msg = case parseMaybeMarkdownList msg of + Just ft | not (null ft) -> M.fromList . catMaybes <$> mapM get (nubOrd $ mentionedNames ft) + _ -> pure M.empty + where + get name = + fmap (name,) . eitherToMaybe + <$> runExceptT (getGroupMemberIdByName db user gId name) + +msgContentTexts :: MsgContent -> (Text, Maybe MarkdownList) +msgContentTexts mc = let t = msgContentText mc in (t, parseMaybeMarkdownList t) + +ciContentTexts :: CIContent d -> (Text, Maybe MarkdownList) +ciContentTexts content = let t = ciContentToText content in (t, parseMaybeMarkdownList t) + +quoteContent :: forall d. MsgContent -> MsgContent -> Maybe (CIFile d) -> MsgContent +quoteContent mc qmc ciFile_ + | replaceContent = MCText qTextOrFile + | otherwise = case qmc of + MCImage _ image -> MCImage qTextOrFile image + MCFile _ -> MCFile qTextOrFile + -- consider same for voice messages + -- MCVoice _ voice -> MCVoice qTextOrFile voice + _ -> qmc + where + -- if the message we're quoting with is one of the "large" MsgContents + -- we replace the quote's content with MCText + replaceContent = case mc of + MCText _ -> False + MCFile _ -> False + MCLink {} -> True + MCImage {} -> True + MCVideo {} -> True + MCVoice {} -> False + MCReport {} -> False + MCUnknown {} -> True + qText = msgContentText qmc + getFileName :: CIFile d -> String + getFileName CIFile {fileName} = fileName + qFileName = maybe qText (T.pack . getFileName) ciFile_ + qTextOrFile = if T.null qText then qFileName else qText + +prohibitedGroupContent :: GroupInfo -> GroupMember -> MsgContent -> Maybe MarkdownList -> Maybe f -> Bool -> Maybe GroupFeature +prohibitedGroupContent gInfo@GroupInfo {membership = GroupMember {memberRole = userRole}} m mc ft file_ sent + | isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) = Just GFVoice + | not (isVoice mc) && isJust file_ && not (groupFeatureMemberAllowed SGFFiles m gInfo) = Just GFFiles + | isReport mc && (badReportUser || not (groupFeatureAllowed SGFReports gInfo)) = Just GFReports + | prohibitedSimplexLinks gInfo m ft = Just GFSimplexLinks + | otherwise = Nothing + where + -- admins cannot send reports, non-admins cannot receive reports + badReportUser + | sent = userRole >= GRModerator + | otherwise = userRole < GRModerator + +prohibitedSimplexLinks :: GroupInfo -> GroupMember -> Maybe MarkdownList -> Bool +prohibitedSimplexLinks gInfo m ft = + not (groupFeatureMemberAllowed SGFSimplexLinks m gInfo) + && maybe False (any ftIsSimplexLink) ft + where + ftIsSimplexLink :: FormattedText -> Bool + ftIsSimplexLink FormattedText {format} = maybe False isSimplexLink format + +roundedFDCount :: Int -> Int +roundedFDCount n + | n <= 0 = 4 + | otherwise = max 4 $ fromIntegral $ (2 :: Integer) ^ (ceiling (logBase 2 (fromIntegral n) :: Double) :: Integer) + +xftpSndFileTransfer_ :: User -> CryptoFile -> Integer -> Int -> Maybe ContactOrGroup -> CM (FileInvitation, CIFile 'MDSnd, FileTransferMeta) +xftpSndFileTransfer_ user file@(CryptoFile filePath cfArgs) fileSize n contactOrGroup_ = do + let fileName = takeFileName filePath + fInv = xftpFileInvitation fileName fileSize dummyFileDescr + fsFilePath <- lift $ toFSFilePath filePath + let srcFile = CryptoFile fsFilePath cfArgs + aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) srcFile (roundedFDCount n) + -- TODO CRSndFileStart event for XFTP + chSize <- asks $ fileChunkSize . config + ft@FileTransferMeta {fileId} <- withStore' $ \db -> createSndFileTransferXFTP db user contactOrGroup_ file fInv (AgentSndFileId aFileId) Nothing chSize + let fileSource = Just $ CryptoFile filePath cfArgs + ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus = CIFSSndStored, fileProtocol = FPXFTP} + pure (fInv, ciFile, ft) + +xftpSndFileRedirect :: User -> FileTransferId -> ValidFileDescription 'FRecipient -> CM FileTransferMeta +xftpSndFileRedirect user ftId vfd = do + let fileName = "redirect.yaml" + file = CryptoFile fileName Nothing + fInv = xftpFileInvitation fileName (fromIntegral $ B.length $ strEncode vfd) dummyFileDescr + aFileId <- withAgent $ \a -> xftpSendDescription a (aUserId user) vfd (roundedFDCount 1) + chSize <- asks $ fileChunkSize . config + withStore' $ \db -> createSndFileTransferXFTP db user Nothing file fInv (AgentSndFileId aFileId) (Just ftId) chSize + +dummyFileDescr :: FileDescr +dummyFileDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False} + +cancelFilesInProgress :: User -> [CIFileInfo] -> CM () +cancelFilesInProgress user filesInfo = do + let filesInfo' = filter (not . fileEnded) filesInfo + (sfs, rfs) <- lift $ splitFTTypes <$> withStoreBatch (\db -> map (getFT db) filesInfo') + forM_ rfs $ \RcvFileTransfer {fileId} -> lift (closeFileHandle fileId rcvFiles) `catchChatError` \_ -> pure () + lift . void . withStoreBatch' $ \db -> map (updateSndFileCancelled db) sfs + lift . void . withStoreBatch' $ \db -> map (updateRcvFileCancelled db) rfs + let xsfIds = mapMaybe (\(FileTransferMeta {fileId, xftpSndFile}, _) -> (,fileId) <$> xftpSndFile) sfs + xrfIds = mapMaybe (\RcvFileTransfer {fileId, xftpRcvFile} -> (,fileId) <$> xftpRcvFile) rfs + lift $ agentXFTPDeleteSndFilesRemote user xsfIds + lift $ agentXFTPDeleteRcvFiles xrfIds + let smpSFConnIds = concatMap (\(ft, sfts) -> mapMaybe (smpSndFileConnId ft) sfts) sfs + smpRFConnIds = mapMaybe smpRcvFileConnId rfs + deleteAgentConnectionsAsync smpSFConnIds + deleteAgentConnectionsAsync smpRFConnIds + where + fileEnded CIFileInfo {fileStatus} = case fileStatus of + Just (AFS _ status) -> ciFileEnded status + Nothing -> True + getFT :: DB.Connection -> CIFileInfo -> IO (Either ChatError FileTransfer) + getFT db CIFileInfo {fileId} = runExceptT . withExceptT ChatErrorStore $ getFileTransfer db user fileId + updateSndFileCancelled :: DB.Connection -> (FileTransferMeta, [SndFileTransfer]) -> IO () + updateSndFileCancelled db (FileTransferMeta {fileId}, sfts) = do + updateFileCancelled db user fileId CIFSSndCancelled + forM_ sfts updateSndFTCancelled + where + updateSndFTCancelled :: SndFileTransfer -> IO () + updateSndFTCancelled ft = unless (sndFTEnded ft) $ do + updateSndFileStatus db ft FSCancelled + deleteSndFileChunks db ft + updateRcvFileCancelled :: DB.Connection -> RcvFileTransfer -> IO () + updateRcvFileCancelled db ft@RcvFileTransfer {fileId} = do + updateFileCancelled db user fileId CIFSRcvCancelled + updateRcvFileStatus db fileId FSCancelled + deleteRcvFileChunks db ft + splitFTTypes :: [Either ChatError FileTransfer] -> ([(FileTransferMeta, [SndFileTransfer])], [RcvFileTransfer]) + splitFTTypes = foldr addFT ([], []) . rights + where + addFT f (sfs, rfs) = case f of + FTSnd ft@FileTransferMeta {cancelled} sfts | not cancelled -> ((ft, sfts) : sfs, rfs) + FTRcv ft@RcvFileTransfer {cancelled} | not cancelled -> (sfs, ft : rfs) + _ -> (sfs, rfs) + smpSndFileConnId :: FileTransferMeta -> SndFileTransfer -> Maybe ConnId + smpSndFileConnId FileTransferMeta {xftpSndFile} sft@SndFileTransfer {agentConnId = AgentConnId acId, fileInline} + | isNothing xftpSndFile && isNothing fileInline && not (sndFTEnded sft) = Just acId + | otherwise = Nothing + smpRcvFileConnId :: RcvFileTransfer -> Maybe ConnId + smpRcvFileConnId ft@RcvFileTransfer {xftpRcvFile, rcvFileInline} + | isNothing xftpRcvFile && isNothing rcvFileInline = liveRcvFileTransferConnId ft + | otherwise = Nothing + sndFTEnded SndFileTransfer {fileStatus} = fileStatus == FSCancelled || fileStatus == FSComplete + +deleteFilesLocally :: [CIFileInfo] -> CM () +deleteFilesLocally files = + withFilesFolder $ \filesFolder -> + liftIO . forM_ files $ \CIFileInfo {filePath} -> + mapM_ (delete . (filesFolder </>)) filePath + where + delete :: FilePath -> IO () + delete fPath = + removeFile fPath `catchAll` \_ -> + removePathForcibly fPath `catchAll_` pure () + -- perform an action only if filesFolder is set (i.e. on mobile devices) + withFilesFolder :: (FilePath -> CM ()) -> CM () + withFilesFolder action = asks filesFolder >>= readTVarIO >>= mapM_ action + +deleteDirectCIs :: User -> Contact -> [CChatItem 'CTDirect] -> CM [ChatItemDeletion] +deleteDirectCIs user ct items = do + let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items + deleteCIFiles user ciFilesInfo + (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items) + unless (null errs) $ toView $ CEvtChatErrors errs + pure deletions + where + deleteItem db (CChatItem md ci) = do + deleteDirectChatItem db user ct ci + pure $ contactDeletion md ct ci Nothing + +deleteGroupCIs :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Maybe GroupMember -> UTCTime -> CM [ChatItemDeletion] +deleteGroupCIs user gInfo items byGroupMember_ deletedTs = do + let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items + deleteCIFiles user ciFilesInfo + (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items) + unless (null errs) $ toView $ CEvtChatErrors errs + pure deletions + where + deleteItem :: DB.Connection -> CChatItem 'CTGroup -> IO ChatItemDeletion + deleteItem db (CChatItem md ci) = do + ci' <- case byGroupMember_ of + Just m -> Just <$> updateGroupChatItemModerated db user gInfo ci m deletedTs + Nothing -> Nothing <$ deleteGroupChatItem db user gInfo ci + pure $ groupDeletion md gInfo ci ci' + +deleteGroupMemberCIs :: MsgDirectionI d => User -> GroupInfo -> GroupMember -> GroupMember -> SMsgDirection d -> CM () +deleteGroupMemberCIs user gInfo member byGroupMember msgDir = do + deletedTs <- liftIO getCurrentTime + filesInfo <- withStore' $ \db -> deleteGroupMemberCIs_ db user gInfo member byGroupMember msgDir deletedTs + deleteCIFiles user filesInfo + +deleteGroupMembersCIs :: User -> GroupInfo -> [GroupMember] -> GroupMember -> CM () +deleteGroupMembersCIs user gInfo members byGroupMember = do + deletedTs <- liftIO getCurrentTime + filesInfo <- withStore' $ \db -> fmap concat $ forM members $ \m -> deleteGroupMemberCIs_ db user gInfo m byGroupMember SMDRcv deletedTs + deleteCIFiles user filesInfo + +deleteGroupMemberCIs_ :: MsgDirectionI d => DB.Connection -> User -> GroupInfo -> GroupMember -> GroupMember -> SMsgDirection d -> UTCTime -> IO [CIFileInfo] +deleteGroupMemberCIs_ db user gInfo member byGroupMember msgDir deletedTs = do + fs <- getGroupMemberFileInfo db user gInfo member + updateMemberCIsModerated db user gInfo member byGroupMember msgDir deletedTs + pure fs + +deleteLocalCIs :: User -> NoteFolder -> [CChatItem 'CTLocal] -> Bool -> Bool -> CM ChatResponse +deleteLocalCIs user nf items byUser timed = do + let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items + deleteFilesLocally ciFilesInfo + (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items) + unless (null errs) $ toView $ CEvtChatErrors errs + pure $ CRChatItemsDeleted user deletions byUser timed + where + deleteItem db (CChatItem md ci) = do + deleteLocalChatItem db user nf ci + pure $ ChatItemDeletion (nfItem md ci) Nothing + nfItem :: MsgDirectionI d => SMsgDirection d -> ChatItem 'CTLocal d -> AChatItem + nfItem md = AChatItem SCTLocal md (LocalChat nf) + +deleteCIFiles :: User -> [CIFileInfo] -> CM () +deleteCIFiles user filesInfo = do + cancelFilesInProgress user filesInfo + deleteFilesLocally filesInfo + +markDirectCIsDeleted :: User -> Contact -> [CChatItem 'CTDirect] -> UTCTime -> CM [ChatItemDeletion] +markDirectCIsDeleted user ct items deletedTs = do + let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items + cancelFilesInProgress user ciFilesInfo + (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (markDeleted db) items) + unless (null errs) $ toView $ CEvtChatErrors errs + pure deletions + where + markDeleted db (CChatItem md ci) = do + ci' <- markDirectChatItemDeleted db user ct ci deletedTs + pure $ contactDeletion md ct ci (Just ci') + +markGroupCIsDeleted :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Maybe GroupMember -> UTCTime -> CM [ChatItemDeletion] +markGroupCIsDeleted user gInfo items byGroupMember_ deletedTs = do + let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items + cancelFilesInProgress user ciFilesInfo + (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (markDeleted db) items) + unless (null errs) $ toView $ CEvtChatErrors errs + pure deletions + -- pure $ CRChatItemsDeleted user deletions byUser False + where + markDeleted db (CChatItem md ci) = do + ci' <- markGroupChatItemDeleted db user gInfo ci byGroupMember_ deletedTs + pure $ groupDeletion md gInfo ci (Just ci') + +markGroupMemberCIsDeleted :: User -> GroupInfo -> GroupMember -> GroupMember -> CM () +markGroupMemberCIsDeleted user gInfo member byGroupMember = do + deletedTs <- liftIO getCurrentTime + filesInfo <- withStore' $ \db -> markGroupMemberCIsDeleted_ db user gInfo member byGroupMember deletedTs + cancelFilesInProgress user filesInfo + +markGroupMembersCIsDeleted :: User -> GroupInfo -> [GroupMember] -> GroupMember -> CM () +markGroupMembersCIsDeleted user gInfo members byGroupMember = do + deletedTs <- liftIO getCurrentTime + filesInfo <- withStore' $ \db -> fmap concat $ forM members $ \m -> markGroupMemberCIsDeleted_ db user gInfo m byGroupMember deletedTs + cancelFilesInProgress user filesInfo + +markGroupMemberCIsDeleted_ :: DB.Connection -> User -> GroupInfo -> GroupMember -> GroupMember -> UTCTime -> IO [CIFileInfo] +markGroupMemberCIsDeleted_ db user gInfo member byGroupMember deletedTs = do + fs <- getGroupMemberFileInfo db user gInfo member + markMemberCIsDeleted db user gInfo member byGroupMember deletedTs + pure fs + +groupDeletion :: MsgDirectionI d => SMsgDirection d -> GroupInfo -> ChatItem 'CTGroup d -> Maybe (ChatItem 'CTGroup d) -> ChatItemDeletion +groupDeletion md g ci ci' = ChatItemDeletion (gItem ci) (gItem <$> ci') + where + gItem = AChatItem SCTGroup md (GroupChat g) + +contactDeletion :: MsgDirectionI d => SMsgDirection d -> Contact -> ChatItem 'CTDirect d -> Maybe (ChatItem 'CTDirect d) -> ChatItemDeletion +contactDeletion md ct ci ci' = ChatItemDeletion (ctItem ci) (ctItem <$> ci') + where + ctItem = AChatItem SCTDirect md (DirectChat ct) + +updateCallItemStatus :: User -> Contact -> Call -> WebRTCCallStatus -> Maybe MessageId -> CM () +updateCallItemStatus user ct@Contact {contactId} Call {chatItemId} receivedStatus msgId_ = do + aciContent_ <- callStatusItemContent user ct chatItemId receivedStatus + forM_ aciContent_ $ \aciContent -> do + timed_ <- callTimed ct aciContent + updateDirectChatItemView user ct chatItemId aciContent False False timed_ msgId_ + forM_ (timed_ >>= timedDeleteAt') $ + startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId) + +updateDirectChatItemView :: User -> Contact -> ChatItemId -> ACIContent -> Bool -> Bool -> Maybe CITimed -> Maybe MessageId -> CM () +updateDirectChatItemView user ct chatItemId (ACIContent msgDir ciContent) edited live timed_ msgId_ = do + ci' <- withStore $ \db -> updateDirectChatItem db user ct chatItemId ciContent edited live timed_ msgId_ + toView $ CEvtChatItemUpdated user (AChatItem SCTDirect msgDir (DirectChat ct) ci') + +callStatusItemContent :: User -> Contact -> ChatItemId -> WebRTCCallStatus -> CM (Maybe ACIContent) +callStatusItemContent user Contact {contactId} chatItemId receivedStatus = do + CChatItem msgDir ChatItem {meta = CIMeta {updatedAt}, content} <- + withStore $ \db -> getDirectChatItem db user contactId chatItemId + ts <- liftIO getCurrentTime + let callDuration :: Int = nominalDiffTimeToSeconds (ts `diffUTCTime` updatedAt) `div'` 1 + callStatus = case content of + CISndCall st _ -> Just st + CIRcvCall st _ -> Just st + _ -> Nothing + newState_ = case (callStatus, receivedStatus) of + (Just CISCallProgress, WCSConnected) -> Nothing -- if call in-progress received connected -> no change + (Just CISCallProgress, WCSDisconnected) -> Just (CISCallEnded, callDuration) -- calculate in-progress duration + (Just CISCallProgress, WCSFailed) -> Just (CISCallEnded, callDuration) -- whether call disconnected or failed + (Just CISCallPending, WCSDisconnected) -> Just (CISCallMissed, 0) + (Just CISCallEnded, _) -> Nothing -- if call already ended or failed -> no change + (Just CISCallError, _) -> Nothing + (Just _, WCSConnecting) -> Just (CISCallNegotiated, 0) + (Just _, WCSConnected) -> Just (CISCallProgress, 0) -- if call ended that was never connected, duration = 0 + (Just _, WCSDisconnected) -> Just (CISCallEnded, 0) + (Just _, WCSFailed) -> Just (CISCallError, 0) + (Nothing, _) -> Nothing -- some other content - we should never get here, but no exception is thrown + pure $ aciContent msgDir <$> newState_ + where + aciContent :: forall d. SMsgDirection d -> (CICallStatus, Int) -> ACIContent + aciContent msgDir (callStatus', duration) = case msgDir of + SMDSnd -> ACIContent SMDSnd $ CISndCall callStatus' duration + SMDRcv -> ACIContent SMDRcv $ CIRcvCall callStatus' duration + +-- mobile clients use file paths relative to app directory (e.g. for the reason ios app directory changes on updates), +-- so we have to differentiate between the file path stored in db and communicated with frontend, and the file path +-- used during file transfer for actual operations with file system +toFSFilePath :: FilePath -> CM' FilePath +toFSFilePath f = + maybe f (</> f) <$> (chatReadVar' filesFolder) + +setFileToEncrypt :: RcvFileTransfer -> CM RcvFileTransfer +setFileToEncrypt ft@RcvFileTransfer {fileId} = do + cfArgs <- atomically . CF.randomArgs =<< asks random + withStore' $ \db -> setFileCryptoArgs db fileId cfArgs + pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs} + +receiveFile' :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM ChatResponse +receiveFile' user ft userApprovedRelays rcvInline_ filePath_ = do + (CRRcvFileAccepted user <$> acceptFileReceive user ft userApprovedRelays rcvInline_ filePath_) `catchChatError` processError + where + -- TODO AChatItem in Cancelled events + processError e + | rctFileCancelled e = pure $ CRRcvFileAcceptedSndCancelled user ft + | otherwise = throwError e + +receiveFileEvt' :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM ChatEvent +receiveFileEvt' user ft userApprovedRelays rcvInline_ filePath_ = do + (CEvtRcvFileAccepted user <$> acceptFileReceive user ft userApprovedRelays rcvInline_ filePath_) `catchChatError` processError + where + -- TODO AChatItem in Cancelled events + processError e + | rctFileCancelled e = pure $ CEvtRcvFileAcceptedSndCancelled user ft + | otherwise = throwError e + +rctFileCancelled :: ChatError -> Bool +rctFileCancelled = \case + ChatErrorAgent (SMP _ SMP.AUTH) _ -> True + ChatErrorAgent (CONN DUPLICATE) _ -> True + _ -> False + +acceptFileReceive :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM AChatItem +acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId, cryptoArgs} userApprovedRelays rcvInline_ filePath_ = do + unless (fileStatus == RFSNew) $ case fileStatus of + RFSCancelled _ -> throwChatError $ CEFileCancelled fName + _ -> throwChatError $ CEFileAlreadyReceiving fName + vr <- chatVersionRange + case (xftpRcvFile, fileConnReq) of + -- direct file protocol + (Nothing, Just connReq) -> do + subMode <- chatReadVar subscriptionMode + dm <- encodeConnInfo $ XFileAcpt fName + connIds <- joinAgentConnectionAsync user True connReq dm subMode + filePath <- getRcvFilePath fileId filePath_ fName True + withStore $ \db -> acceptRcvFileTransfer db vr user fileId connIds ConnJoined filePath subMode + -- XFTP + (Just XFTPRcvFile {userApprovedRelays = approvedBeforeReady}, _) -> do + let userApproved = approvedBeforeReady || userApprovedRelays + filePath <- getRcvFilePath fileId filePath_ fName False + (ci, rfd) <- withStore $ \db -> do + -- marking file as accepted and reading description in the same transaction + -- to prevent race condition with appending description + ci <- xftpAcceptRcvFT db vr user fileId filePath userApproved + rfd <- getRcvFileDescrByRcvFileId db fileId + pure (ci, rfd) + receiveViaCompleteFD user fileId rfd userApproved cryptoArgs + pure ci + -- group & direct file protocol + _ -> do + chatRef <- withStore $ \db -> getChatRefByFileId db user fileId + case (chatRef, grpMemberId) of + (ChatRef CTDirect contactId, Nothing) -> do + ct <- withStore $ \db -> getContact db vr user contactId + acceptFile CFCreateConnFileInvDirect $ \msg -> void $ sendDirectContactMessage user ct msg + (ChatRef CTGroup groupId, Just memId) -> do + GroupMember {activeConn} <- withStore $ \db -> getGroupMember db vr user groupId memId + case activeConn of + Just conn -> do + acceptFile CFCreateConnFileInvGroup $ \msg -> void $ sendDirectMemberMessage conn msg groupId + _ -> throwChatError $ CEFileInternal "member connection not active" + _ -> throwChatError $ CEFileInternal "invalid chat ref for file transfer" + where + acceptFile :: CommandFunction -> (ChatMsgEvent 'Json -> CM ()) -> CM AChatItem + acceptFile cmdFunction send = do + filePath <- getRcvFilePath fileId filePath_ fName True + inline <- receiveInline + vr <- chatVersionRange + if + | inline -> do + -- accepting inline + ci <- withStore $ \db -> acceptRcvInlineFT db vr user fileId filePath + sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId + send $ XFileAcptInv sharedMsgId Nothing fName + pure ci + | fileInline == Just IFMSent -> throwChatError $ CEFileAlreadyReceiving fName + | otherwise -> do + -- accepting via a new connection + subMode <- chatReadVar subscriptionMode + connIds <- createAgentConnectionAsync user cmdFunction True SCMInvitation subMode + withStore $ \db -> acceptRcvFileTransfer db vr user fileId connIds ConnNew filePath subMode + receiveInline :: CM Bool + receiveInline = do + ChatConfig {fileChunkSize, inlineFiles = InlineFilesConfig {receiveChunks, offerChunks}} <- asks config + pure $ + rcvInline_ /= Just False + && fileInline == Just IFMOffer + && ( fileSize <= fileChunkSize * receiveChunks + || (rcvInline_ == Just True && fileSize <= fileChunkSize * offerChunks) + ) + +receiveViaCompleteFD :: User -> FileTransferId -> RcvFileDescr -> Bool -> Maybe CryptoFileArgs -> CM () +receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} userApprovedRelays cfArgs = + when fileDescrComplete $ do + rd <- parseFileDescription fileDescrText + if userApprovedRelays + then receive' rd True + else do + let srvs = fileServers rd + unknownSrvs <- getUnknownSrvs srvs + let approved = null unknownSrvs + ifM + ((approved ||) <$> ipProtectedForSrvs srvs) + (receive' rd approved) + (relaysNotApproved unknownSrvs) + where + receive' :: ValidFileDescription 'FRecipient -> Bool -> CM () + receive' rd approved = do + aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd cfArgs approved + startReceivingFile user fileId + withStore' $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId) + fileServers :: ValidFileDescription 'FRecipient -> [XFTPServer] + fileServers (FD.ValidFileDescription FD.FileDescription {chunks}) = + S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks + getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer] + getUnknownSrvs srvs = do + knownSrvs <- L.map protoServer' <$> getKnownAgentServers SPXFTP user + pure $ filter (`notElem` knownSrvs) srvs + ipProtectedForSrvs :: [XFTPServer] -> CM Bool + ipProtectedForSrvs srvs = do + netCfg <- lift getNetworkConfig + pure $ all (ipAddressProtected netCfg) srvs + relaysNotApproved :: [XFTPServer] -> CM () + relaysNotApproved unknownSrvs = do + aci_ <- resetRcvCIFileStatus user fileId CIFSRcvInvitation + forM_ aci_ $ \aci -> do + cleanupACIFile aci + toView $ CEvtChatItemUpdated user aci + throwChatError $ CEFileNotApproved fileId unknownSrvs + +cleanupACIFile :: AChatItem -> CM () +cleanupACIFile (AChatItem _ _ _ ChatItem {file = Just CIFile {fileSource = Just CryptoFile {filePath}}}) = do + fsFilePath <- lift $ toFSFilePath filePath + removeFile fsFilePath `catchChatError` \_ -> pure () +cleanupACIFile _ = pure () + +getKnownAgentServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> User -> CM (NonEmpty (ServerCfg p)) +getKnownAgentServers p user = do + as <- asks randomAgentServers + withStore $ \db -> do + opDomains <- operatorDomains . serverOperators <$> getServerOperators db + srvs <- liftIO $ getProtocolServers db p user + pure $ useServerCfgs p as opDomains srvs + +protoServer' :: ServerCfg p -> ProtocolServer p +protoServer' ServerCfg {server} = protoServer server + +getNetworkConfig :: CM' NetworkConfig +getNetworkConfig = withAgent' $ liftIO . getFastNetworkConfig + +resetRcvCIFileStatus :: User -> FileTransferId -> CIFileStatus 'MDRcv -> CM (Maybe AChatItem) +resetRcvCIFileStatus user fileId ciFileStatus = do + vr <- chatVersionRange + withStore $ \db -> do + liftIO $ do + updateCIFileStatus db user fileId ciFileStatus + updateRcvFileStatus db fileId FSNew + updateRcvFileAgentId db fileId Nothing + lookupChatItemByFileId db vr user fileId + +receiveViaURI :: User -> FileDescriptionURI -> CryptoFile -> CM RcvFileTransfer +receiveViaURI user@User {userId} FileDescriptionURI {description} cf@CryptoFile {cryptoArgs} = do + fileId <- withStore $ \db -> createRcvStandaloneFileTransfer db userId cf fileSize chunkSize + -- currently the only use case is user migrating via their configured servers, so we pass approvedRelays = True + aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) description cryptoArgs True + withStore $ \db -> do + liftIO $ do + updateRcvFileStatus db fileId FSConnected + updateCIFileStatus db user fileId $ CIFSRcvTransfer 0 1 + updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId) + getRcvFileTransfer db user fileId + where + FD.ValidFileDescription FD.FileDescription {size = FD.FileSize fileSize, chunkSize = FD.FileSize chunkSize} = description + +startReceivingFile :: User -> FileTransferId -> CM () +startReceivingFile user fileId = do + vr <- chatVersionRange + ci <- withStore $ \db -> do + liftIO $ updateRcvFileStatus db fileId FSConnected + liftIO $ updateCIFileStatus db user fileId $ CIFSRcvTransfer 0 1 + getChatItemByFileId db vr user fileId + toView $ CEvtRcvFileStart user ci + +getRcvFilePath :: FileTransferId -> Maybe FilePath -> String -> Bool -> CM FilePath +getRcvFilePath fileId fPath_ fn keepHandle = case fPath_ of + Nothing -> + chatReadVar filesFolder >>= \case + Nothing -> do + defaultFolder <- lift getDefaultFilesFolder + fPath <- liftIO $ defaultFolder `uniqueCombine` fn + createEmptyFile fPath $> fPath + Just filesFolder -> do + fPath <- liftIO $ filesFolder `uniqueCombine` fn + createEmptyFile fPath + pure $ takeFileName fPath + Just fPath -> + ifM + (doesDirectoryExist fPath) + (createInPassedDirectory fPath) + $ ifM + (doesFileExist fPath) + (throwChatError $ CEFileAlreadyExists fPath) + (createEmptyFile fPath $> fPath) + where + createInPassedDirectory :: FilePath -> CM FilePath + createInPassedDirectory fPathDir = do + fPath <- liftIO $ fPathDir `uniqueCombine` fn + createEmptyFile fPath $> fPath + createEmptyFile :: FilePath -> CM () + createEmptyFile fPath = emptyFile `catchThrow` (ChatError . CEFileWrite fPath . show) + where + emptyFile :: CM () + emptyFile + | keepHandle = do + h <- getFileHandle fileId fPath rcvFiles AppendMode + liftIO $ B.hPut h "" >> hFlush h + | otherwise = liftIO $ B.writeFile fPath "" + +acceptContactRequest :: User -> UserContactRequest -> IncognitoEnabled -> CM (Contact, Connection, SndQueueSecured) +acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId = AgentInvId invId, contactId_, cReqChatVRange, localDisplayName = cName, profileId, profile = cp, userContactLinkId, xContactId, pqSupport} incognito = do + subMode <- chatReadVar subscriptionMode + let pqSup = PQSupportOn + pqSup' = pqSup `CR.pqSupportAnd` pqSupport + vr <- chatVersionRange + let chatV = vr `peerConnChatVersion` cReqChatVRange + (ct, conn, incognitoProfile) <- case contactId_ of + Nothing -> do + incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing + connId <- withAgent $ \a -> prepareConnectionToAccept a True invId pqSup' + (ct, conn) <- withStore' $ \db -> createAcceptedContact db user connId chatV cReqChatVRange cName profileId cp userContactLinkId xContactId incognitoProfile subMode pqSup' False + pure (ct, conn, incognitoProfile) + Just contactId -> do + ct <- withFastStore $ \db -> getContact db vr user contactId + case contactConn ct of + Nothing -> throwChatError $ CECommandError "contact has no connection" + Just conn@Connection {customUserProfileId} -> do + incognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId + pure (ct, conn, ExistingIncognito <$> incognitoProfile) + let profileToSend = profileToSendOnAccept user incognitoProfile False + dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend + (ct,conn,) <$> withAgent (\a -> acceptContact a (aConnId conn) True invId dm pqSup' subMode) + +acceptContactRequestAsync :: User -> UserContactRequest -> Maybe IncognitoProfile -> PQSupport -> CM Contact +acceptContactRequestAsync user cReq@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile pqSup = do + subMode <- chatReadVar subscriptionMode + let profileToSend = profileToSendOnAccept user incognitoProfile False + vr <- chatVersionRange + let chatV = vr `peerConnChatVersion` cReqChatVRange + (cmdId, acId) <- agentAcceptContactAsync user True invId (XInfo profileToSend) subMode pqSup chatV + withStore' $ \db -> do + (ct, Connection {connId}) <- createAcceptedContact db user acId chatV cReqChatVRange cName profileId p userContactLinkId xContactId incognitoProfile subMode pqSup True + deleteContactRequestRec db user cReq + setCommandConnId db user cmdId connId + pure ct + +acceptGroupJoinRequestAsync :: User -> GroupInfo -> UserContactRequest -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> CM GroupMember +acceptGroupJoinRequestAsync + user + gInfo@GroupInfo {groupProfile, membership, businessChat} + ucr@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange} + gAccepted + gLinkMemRole + incognitoProfile = do + gVar <- asks random + let initialStatus = acceptanceToStatus gAccepted + (groupMemberId, memberId) <- withStore $ \db -> do + liftIO $ deleteContactRequestRec db user ucr + createJoiningMember db gVar user gInfo ucr gLinkMemRole initialStatus + currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo + let Profile {displayName} = profileToSendOnAccept user incognitoProfile True + GroupMember {memberRole = userRole, memberId = userMemberId} = membership + msg = + XGrpLinkInv $ + GroupLinkInvitation + { fromMember = MemberIdRole userMemberId userRole, + fromMemberName = displayName, + invitedMember = MemberIdRole memberId gLinkMemRole, + groupProfile, + accepted = Just gAccepted, + business = businessChat, + groupSize = Just currentMemCount + } + subMode <- chatReadVar subscriptionMode + vr <- chatVersionRange + let chatV = vr `peerConnChatVersion` cReqChatVRange + connIds <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV + withStore $ \db -> do + liftIO $ createJoiningMemberConnection db user connIds chatV ucr groupMemberId subMode + getGroupMemberById db vr user groupMemberId + +acceptGroupJoinSendRejectAsync :: User -> GroupInfo -> UserContactRequest -> GroupRejectionReason -> CM GroupMember +acceptGroupJoinSendRejectAsync + user + gInfo@GroupInfo {groupProfile, membership} + ucr@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange} + rejectionReason = do + gVar <- asks random + (groupMemberId, memberId) <- withStore $ \db -> do + liftIO $ deleteContactRequestRec db user ucr + createJoiningMember db gVar user gInfo ucr GRObserver GSMemRejected + let GroupMember {memberRole = userRole, memberId = userMemberId} = membership + msg = + XGrpLinkReject $ + GroupLinkRejection + { fromMember = MemberIdRole userMemberId userRole, + invitedMember = MemberIdRole memberId GRObserver, + groupProfile, + rejectionReason + } + subMode <- chatReadVar subscriptionMode + vr <- chatVersionRange + let chatV = vr `peerConnChatVersion` cReqChatVRange + connIds <- agentAcceptContactAsync user False invId msg subMode PQSupportOff chatV + withStore $ \db -> do + liftIO $ createJoiningMemberConnection db user connIds chatV ucr groupMemberId subMode + getGroupMemberById db vr user groupMemberId + +acceptBusinessJoinRequestAsync :: User -> UserContactRequest -> CM GroupInfo +acceptBusinessJoinRequestAsync + user + ucr@UserContactRequest {contactRequestId, agentInvitationId = AgentInvId invId, cReqChatVRange} = do + vr <- chatVersionRange + gVar <- asks random + let userProfile@Profile {displayName, preferences} = profileToSendOnAccept user Nothing True + groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs preferences + (gInfo, clientMember) <- withStore $ \db -> do + liftIO $ deleteContactRequest db user contactRequestId + createBusinessRequestGroup db vr gVar user ucr groupPreferences + let GroupInfo {membership} = gInfo + GroupMember {memberRole = userRole, memberId = userMemberId} = membership + GroupMember {groupMemberId, memberId} = clientMember + msg = + XGrpLinkInv $ + GroupLinkInvitation + { fromMember = MemberIdRole userMemberId userRole, + fromMemberName = displayName, + invitedMember = MemberIdRole memberId GRMember, + groupProfile = businessGroupProfile userProfile groupPreferences, + accepted = Just GAAccepted, + -- This refers to the "title member" that defines the group name and profile. + -- This coincides with fromMember to be current user when accepting the connecting user, + -- but it will be different when inviting somebody else. + business = Just $ BusinessChatInfo {chatType = BCBusiness, businessId = userMemberId, customerId = memberId}, + groupSize = Just 1 + } + subMode <- chatReadVar subscriptionMode + let chatV = vr `peerConnChatVersion` cReqChatVRange + connIds <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV + withStore' $ \db -> createJoiningMemberConnection db user connIds chatV ucr groupMemberId subMode + let cd = CDGroupSnd gInfo + createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing + createGroupFeatureItems user cd CISndGroupFeature gInfo + pure gInfo + where + businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile + businessGroupProfile Profile {displayName, fullName, image} groupPreferences = + GroupProfile {displayName, fullName, description = Nothing, image, groupPreferences = Just groupPreferences, memberAdmission = Nothing} + +profileToSendOnAccept :: User -> Maybe IncognitoProfile -> Bool -> Profile +profileToSendOnAccept user ip = userProfileToSend user (getIncognitoProfile <$> ip) Nothing + where + getIncognitoProfile = \case + NewIncognito p -> p + ExistingIncognito lp -> fromLocalProfile lp + +introduceToGroup :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () +introduceToGroup _ _ _ GroupMember {activeConn = Nothing} = throwChatError $ CEInternalError "member connection not active" +introduceToGroup vr user gInfo@GroupInfo {groupId, membership} m@GroupMember {activeConn = Just conn} = do + members <- withStore' $ \db -> getGroupMembers db vr user gInfo + void . sendGroupMessage user gInfo members . XGrpMemNew $ memberInfo m + sendIntroductions members + when (groupFeatureAllowed SGFHistory gInfo) sendHistory + where + sendIntroductions members = do + intros <- withStore' $ \db -> createIntroductions db (maxVersion vr) members m + shuffledIntros <- liftIO $ shuffleIntros intros + if m `supportsVersion` batchSendVersion + then do + let events = map (memberIntro . reMember) shuffledIntros + forM_ (L.nonEmpty events) $ \events' -> + sendGroupMemberMessages user conn events' groupId + else forM_ shuffledIntros $ \intro -> + processIntro intro `catchChatError` eToView + memberIntro :: GroupMember -> ChatMsgEvent 'Json + memberIntro reMember = + let mInfo = memberInfo reMember + mRestrictions = memberRestrictions reMember + in XGrpMemIntro mInfo mRestrictions + shuffleIntros :: [GroupMemberIntro] -> IO [GroupMemberIntro] + shuffleIntros intros = do + let (admins, others) = partition isAdmin intros + (admPics, admNoPics) = partition hasPicture admins + (othPics, othNoPics) = partition hasPicture others + mconcat <$> mapM shuffle [admPics, admNoPics, othPics, othNoPics] + where + isAdmin GroupMemberIntro {reMember = GroupMember {memberRole}} = memberRole >= GRAdmin + hasPicture GroupMemberIntro {reMember = GroupMember {memberProfile = LocalProfile {image}}} = isJust image + processIntro intro@GroupMemberIntro {introId} = do + void $ sendDirectMemberMessage conn (memberIntro $ reMember intro) groupId + withStore' $ \db -> updateIntroStatus db introId GMIntroSent + sendHistory = + when (m `supportsVersion` batchSendVersion) $ do + (errs, items) <- partitionEithers <$> withStore' (\db -> getGroupHistoryItems db user gInfo m 100) + (errs', events) <- partitionEithers <$> mapM (tryChatError . itemForwardEvents) items + let errors = map ChatErrorStore errs <> errs' + unless (null errors) $ toView $ CEvtChatErrors errors + let events' = maybe (concat events) (\x -> concat events <> [x]) descrEvent_ + forM_ (L.nonEmpty events') $ \events'' -> + sendGroupMemberMessages user conn events'' groupId + descrEvent_ :: Maybe (ChatMsgEvent 'Json) + descrEvent_ + | m `supportsVersion` groupHistoryIncludeWelcomeVersion = do + let GroupInfo {groupProfile = GroupProfile {description}} = gInfo + fmap (\descr -> XMsgNew $ MCSimple $ extMsgContent (MCText descr) Nothing) description + | otherwise = Nothing + itemForwardEvents :: CChatItem 'CTGroup -> CM [ChatMsgEvent 'Json] + itemForwardEvents cci = case cci of + (CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv sender, content = CIRcvMsgContent mc, file}) + | not (blockedByAdmin sender) -> do + fInvDescr_ <- join <$> forM file getRcvFileInvDescr + processContentItem sender ci mc fInvDescr_ + (CChatItem SMDSnd ci@ChatItem {content = CISndMsgContent mc, file}) -> do + fInvDescr_ <- join <$> forM file getSndFileInvDescr + processContentItem membership ci mc fInvDescr_ + _ -> pure [] + where + getRcvFileInvDescr :: CIFile 'MDRcv -> CM (Maybe (FileInvitation, RcvFileDescrText)) + getRcvFileInvDescr ciFile@CIFile {fileId, fileProtocol, fileStatus} = do + expired <- fileExpired + if fileProtocol /= FPXFTP || fileStatus == CIFSRcvCancelled || expired + then pure Nothing + else do + rfd <- withStore $ \db -> getRcvFileDescrByRcvFileId db fileId + pure $ invCompleteDescr ciFile rfd + getSndFileInvDescr :: CIFile 'MDSnd -> CM (Maybe (FileInvitation, RcvFileDescrText)) + getSndFileInvDescr ciFile@CIFile {fileId, fileProtocol, fileStatus} = do + expired <- fileExpired + if fileProtocol /= FPXFTP || fileStatus == CIFSSndCancelled || expired + then pure Nothing + else do + -- can also lookup in extra_xftp_file_descriptions, though it can be empty; + -- would be best if snd file had a single rcv description for all members saved in files table + rfd <- withStore $ \db -> getRcvFileDescrBySndFileId db fileId + pure $ invCompleteDescr ciFile rfd + fileExpired :: CM Bool + fileExpired = do + ttl <- asks $ rcvFilesTTL . agentConfig . config + cutoffTs <- addUTCTime (-ttl) <$> liftIO getCurrentTime + pure $ chatItemTs cci < cutoffTs + invCompleteDescr :: CIFile d -> RcvFileDescr -> Maybe (FileInvitation, RcvFileDescrText) + invCompleteDescr CIFile {fileName, fileSize} RcvFileDescr {fileDescrText, fileDescrComplete} + | fileDescrComplete = + let fInvDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False} + fInv = xftpFileInvitation fileName fileSize fInvDescr + in Just (fInv, fileDescrText) + | otherwise = Nothing + processContentItem :: GroupMember -> ChatItem 'CTGroup d -> MsgContent -> Maybe (FileInvitation, RcvFileDescrText) -> CM [ChatMsgEvent 'Json] + processContentItem sender ChatItem {formattedText, meta, quotedItem, mentions} mc fInvDescr_ = + if isNothing fInvDescr_ && not (msgContentHasText mc) + then pure [] + else do + let CIMeta {itemTs, itemSharedMsgId, itemTimed} = meta + quotedItemId_ = quoteItemId =<< quotedItem + fInv_ = fst <$> fInvDescr_ + (mc', _, mentions') = updatedMentionNames mc formattedText mentions + mentions'' = M.map (\CIMention {memberId} -> MsgMention {memberId}) mentions' + (chatMsgEvent, _) <- withStore $ \db -> prepareGroupMsg db user gInfo mc' mentions'' quotedItemId_ Nothing fInv_ itemTimed False + let senderVRange = memberChatVRange' sender + xMsgNewChatMsg = ChatMessage {chatVRange = senderVRange, msgId = itemSharedMsgId, chatMsgEvent} + fileDescrEvents <- case (snd <$> fInvDescr_, itemSharedMsgId) of + (Just fileDescrText, Just msgId) -> do + partSize <- asks $ xftpDescrPartSize . config + let parts = splitFileDescr partSize fileDescrText + pure . L.toList $ L.map (XMsgFileDescr msgId) parts + _ -> pure [] + let fileDescrChatMsgs = map (ChatMessage senderVRange Nothing) fileDescrEvents + GroupMember {memberId} = sender + msgForwardEvents = map (\cm -> XGrpMsgForward memberId cm itemTs) (xMsgNewChatMsg : fileDescrChatMsgs) + pure msgForwardEvents + +splitFileDescr :: Int -> RcvFileDescrText -> NonEmpty FileDescr +splitFileDescr partSize rfdText = splitParts 1 rfdText + where + splitParts partNo remText = + let (part, rest) = T.splitAt partSize remText + complete = T.null rest + fileDescr = FileDescr {fileDescrText = part, fileDescrPartNo = partNo, fileDescrComplete = complete} + in if complete + then fileDescr :| [] + else fileDescr <| splitParts (partNo + 1) rest + +deleteGroupLink' :: User -> GroupInfo -> CM () +deleteGroupLink' user gInfo = do + vr <- chatVersionRange + conn <- withStore $ \db -> getGroupLinkConnection db vr user gInfo + deleteGroupLink_ user gInfo conn + +deleteGroupLinkIfExists :: User -> GroupInfo -> CM () +deleteGroupLinkIfExists user gInfo = do + vr <- chatVersionRange + conn_ <- eitherToMaybe <$> withStore' (\db -> runExceptT $ getGroupLinkConnection db vr user gInfo) + mapM_ (deleteGroupLink_ user gInfo) conn_ + +deleteGroupLink_ :: User -> GroupInfo -> Connection -> CM () +deleteGroupLink_ user gInfo conn = do + deleteAgentConnectionAsync $ aConnId conn + withStore' $ \db -> deleteGroupLink db user gInfo + +startProximateTimedItemThread :: User -> (ChatRef, ChatItemId) -> UTCTime -> CM () +startProximateTimedItemThread user itemRef deleteAt = do + interval <- asks (cleanupManagerInterval . config) + ts <- liftIO getCurrentTime + when (diffUTCTime deleteAt ts <= interval) $ + startTimedItemThread user itemRef deleteAt + +startTimedItemThread :: User -> (ChatRef, ChatItemId) -> UTCTime -> CM () +startTimedItemThread user itemRef deleteAt = do + itemThreads <- asks timedItemThreads + threadTVar_ <- atomically $ do + exists <- TM.member itemRef itemThreads + if not exists + then do + threadTVar <- newTVar Nothing + TM.insert itemRef threadTVar itemThreads + pure $ Just threadTVar + else pure Nothing + forM_ threadTVar_ $ \threadTVar -> do + tId <- mkWeakThreadId =<< deleteTimedItem user itemRef deleteAt `forkFinally` const (atomically $ TM.delete itemRef itemThreads) + atomically $ writeTVar threadTVar (Just tId) + +deleteTimedItem :: User -> (ChatRef, ChatItemId) -> UTCTime -> CM () +deleteTimedItem user (ChatRef cType chatId, itemId) deleteAt = do + ts <- liftIO getCurrentTime + liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime deleteAt ts + lift waitChatStartedAndActivated + vr <- chatVersionRange + case cType of + CTDirect -> do + (ct, ci) <- withStore $ \db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId + deletions <- deleteDirectCIs user ct [ci] + toView $ CEvtChatItemsDeleted user deletions True True + CTGroup -> do + (gInfo, ci) <- withStore $ \db -> (,) <$> getGroupInfo db vr user chatId <*> getGroupChatItem db user chatId itemId + deletedTs <- liftIO getCurrentTime + deletions <- deleteGroupCIs user gInfo [ci] Nothing deletedTs + toView $ CEvtChatItemsDeleted user deletions True True + _ -> eToView $ ChatError $ CEInternalError "bad deleteTimedItem cType" + +startUpdatedTimedItemThread :: User -> ChatRef -> ChatItem c d -> ChatItem c d -> CM () +startUpdatedTimedItemThread user chatRef ci ci' = + case (chatItemTimed ci >>= timedDeleteAt', chatItemTimed ci' >>= timedDeleteAt') of + (Nothing, Just deleteAt') -> + startProximateTimedItemThread user (chatRef, chatItemId' ci') deleteAt' + _ -> pure () + +metaBrokerTs :: MsgMeta -> UTCTime +metaBrokerTs MsgMeta {broker = (_, brokerTs)} = brokerTs + +createContactPQSndItem :: User -> Contact -> Connection -> PQEncryption -> CM (Contact, Connection) +createContactPQSndItem user ct conn@Connection {pqSndEnabled} pqSndEnabled' = + flip catchChatError (const $ pure (ct, conn)) $ case (pqSndEnabled, pqSndEnabled') of + (Just b, b') | b' /= b -> createPQItem $ CISndConnEvent (SCEPqEnabled pqSndEnabled') + (Nothing, PQEncOn) -> createPQItem $ CISndDirectE2EEInfo (E2EInfo pqSndEnabled') + _ -> pure (ct, conn) + where + createPQItem ciContent = do + let conn' = conn {pqSndEnabled = Just pqSndEnabled'} :: Connection + ct' = ct {activeConn = Just conn'} :: Contact + when (contactPQEnabled ct /= contactPQEnabled ct') $ do + createInternalChatItem user (CDDirectSnd ct') ciContent Nothing + toView $ CEvtContactPQEnabled user ct' pqSndEnabled' + pure (ct', conn') + +updateContactPQRcv :: User -> Contact -> Connection -> PQEncryption -> CM (Contact, Connection) +updateContactPQRcv user ct conn@Connection {connId, pqRcvEnabled} pqRcvEnabled' = + flip catchChatError (const $ pure (ct, conn)) $ case (pqRcvEnabled, pqRcvEnabled') of + (Just b, b') | b' /= b -> updatePQ $ CIRcvConnEvent (RCEPqEnabled pqRcvEnabled') + (Nothing, PQEncOn) -> updatePQ $ CIRcvDirectE2EEInfo (E2EInfo pqRcvEnabled') + _ -> pure (ct, conn) + where + updatePQ ciContent = do + withStore' $ \db -> updateConnPQRcvEnabled db connId pqRcvEnabled' + let conn' = conn {pqRcvEnabled = Just pqRcvEnabled'} :: Connection + ct' = ct {activeConn = Just conn'} :: Contact + when (contactPQEnabled ct /= contactPQEnabled ct') $ do + createInternalChatItem user (CDDirectRcv ct') ciContent Nothing + toView $ CEvtContactPQEnabled user ct' pqRcvEnabled' + pure (ct', conn') + +updatePeerChatVRange :: Connection -> VersionRangeChat -> CM Connection +updatePeerChatVRange conn@Connection {connId, connChatVersion = v, peerChatVRange, connType, pqSupport, pqEncryption} msgVRange = do + v' <- lift $ upgradedConnVersion v msgVRange + conn' <- + if msgVRange /= peerChatVRange || v' /= v + then do + withStore' $ \db -> setPeerChatVRange db connId v' msgVRange + pure conn {connChatVersion = v', peerChatVRange = msgVRange} + else pure conn + -- TODO v6.0 remove/review: for contacts only version upgrade should trigger enabling PQ support/encryption + if connType == ConnContact && v' >= pqEncryptionCompressionVersion && (pqSupport /= PQSupportOn || pqEncryption /= PQEncOn) + then do + withStore' $ \db -> updateConnSupportPQ db connId PQSupportOn PQEncOn + pure conn' {pqSupport = PQSupportOn, pqEncryption = PQEncOn} + else pure conn' + +updateMemberChatVRange :: GroupMember -> Connection -> VersionRangeChat -> CM (GroupMember, Connection) +updateMemberChatVRange mem@GroupMember {groupMemberId} conn@Connection {connId, connChatVersion = v, peerChatVRange} msgVRange = do + v' <- lift $ upgradedConnVersion v msgVRange + if msgVRange /= peerChatVRange || v' /= v + then do + withStore' $ \db -> do + setPeerChatVRange db connId v' msgVRange + setMemberChatVRange db groupMemberId msgVRange + let conn' = conn {connChatVersion = v', peerChatVRange = msgVRange} + pure (mem {memberChatVRange = msgVRange, activeConn = Just conn'}, conn') + else pure (mem, conn) + +upgradedConnVersion :: VersionChat -> VersionRangeChat -> CM' VersionChat +upgradedConnVersion v peerVR = do + vr <- chatVersionRange' + -- don't allow reducing agreed connection version + pure $ maybe v (\(Compatible v') -> max v v') $ vr `compatibleVersion` peerVR + +parseFileDescription :: FilePartyI p => Text -> CM (ValidFileDescription p) +parseFileDescription = + liftEither . first (ChatError . CEInvalidFileDescription) . (strDecode . encodeUtf8) + +sendDirectFileInline :: User -> Contact -> FileTransferMeta -> SharedMsgId -> CM () +sendDirectFileInline user ct ft sharedMsgId = do + msgDeliveryId <- sendFileInline_ ft sharedMsgId $ sendDirectContactMessage user ct + withStore $ \db -> updateSndDirectFTDelivery db ct ft msgDeliveryId + +sendMemberFileInline :: GroupMember -> Connection -> FileTransferMeta -> SharedMsgId -> CM () +sendMemberFileInline m@GroupMember {groupId} conn ft sharedMsgId = do + msgDeliveryId <- sendFileInline_ ft sharedMsgId $ \msg -> do + (sndMsg, msgDeliveryId, _) <- sendDirectMemberMessage conn msg groupId + pure (sndMsg, msgDeliveryId) + withStore' $ \db -> updateSndGroupFTDelivery db m conn ft msgDeliveryId + +sendFileInline_ :: FileTransferMeta -> SharedMsgId -> (ChatMsgEvent 'Binary -> CM (SndMessage, Int64)) -> CM Int64 +sendFileInline_ FileTransferMeta {filePath, chunkSize} sharedMsgId sendMsg = + sendChunks 1 =<< liftIO . B.readFile =<< lift (toFSFilePath filePath) + where + sendChunks chunkNo bytes = do + let (chunk, rest) = B.splitAt chSize bytes + (_, msgDeliveryId) <- sendMsg $ BFileChunk sharedMsgId $ FileChunk chunkNo chunk + if B.null rest + then pure msgDeliveryId + else sendChunks (chunkNo + 1) rest + chSize = fromIntegral chunkSize + +parseChatMessage :: Connection -> ByteString -> CM (ChatMessage 'Json) +parseChatMessage conn s = do + case parseChatMessages s of + [msg] -> liftEither . first (ChatError . errType) $ (\(ACMsg _ m) -> checkEncoding m) =<< msg + _ -> throwChatError $ CEException "parseChatMessage: single message is expected" + where + errType = CEInvalidChatMessage conn Nothing (safeDecodeUtf8 s) +{-# INLINE parseChatMessage #-} + +sendFileChunk :: User -> SndFileTransfer -> CM () +sendFileChunk user ft@SndFileTransfer {fileId, fileStatus, agentConnId = AgentConnId acId} = + unless (fileStatus == FSComplete || fileStatus == FSCancelled) $ do + vr <- chatVersionRange + withStore' (`createSndFileChunk` ft) >>= \case + Just chunkNo -> sendFileChunkNo ft chunkNo + Nothing -> do + ci <- withStore $ \db -> do + liftIO $ updateSndFileStatus db ft FSComplete + liftIO $ deleteSndFileChunks db ft + updateDirectCIFileStatus db vr user fileId CIFSSndComplete + toView $ CEvtSndFileComplete user ci ft + lift $ closeFileHandle fileId sndFiles + deleteAgentConnectionAsync acId + +sendFileChunkNo :: SndFileTransfer -> Integer -> CM () +sendFileChunkNo ft@SndFileTransfer {agentConnId = AgentConnId acId} chunkNo = do + chunkBytes <- readFileChunk ft chunkNo + (msgId, _) <- withAgent $ \a -> sendMessage a acId PQEncOff SMP.noMsgFlags $ smpEncode FileChunk {chunkNo, chunkBytes} + withStore' $ \db -> updateSndFileChunkMsg db ft chunkNo msgId + +readFileChunk :: SndFileTransfer -> Integer -> CM ByteString +readFileChunk SndFileTransfer {fileId, filePath, chunkSize} chunkNo = do + fsFilePath <- lift $ toFSFilePath filePath + read_ fsFilePath `catchThrow` (ChatError . CEFileRead filePath . show) + where + read_ fsFilePath = do + h <- getFileHandle fileId fsFilePath sndFiles ReadMode + pos <- hTell h + let pos' = (chunkNo - 1) * chunkSize + when (pos /= pos') $ hSeek h AbsoluteSeek pos' + liftIO . B.hGet h $ fromInteger chunkSize + +parseFileChunk :: ByteString -> CM FileChunk +parseFileChunk = liftEither . first (ChatError . CEFileRcvChunk) . smpDecode + +appendFileChunk :: RcvFileTransfer -> Integer -> ByteString -> Bool -> CM () +appendFileChunk ft@RcvFileTransfer {fileId, fileStatus, cryptoArgs, fileInvitation = FileInvitation {fileName}} chunkNo chunk final = + case fileStatus of + RFSConnected RcvFileInfo {filePath} -> append_ filePath + -- sometimes update of file transfer status to FSConnected + -- doesn't complete in time before MSG with first file chunk + RFSAccepted RcvFileInfo {filePath} -> append_ filePath + RFSCancelled _ -> pure () + _ -> throwChatError $ CEFileInternal "receiving file transfer not in progress" + where + append_ :: FilePath -> CM () + append_ filePath = do + fsFilePath <- lift $ toFSFilePath filePath + h <- getFileHandle fileId fsFilePath rcvFiles AppendMode + liftIO (B.hPut h chunk >> hFlush h) `catchThrow` (fileErr . show) + withStore' $ \db -> updatedRcvFileChunkStored db ft chunkNo + when final $ do + lift $ closeFileHandle fileId rcvFiles + forM_ cryptoArgs $ \cfArgs -> do + tmpFile <- lift getChatTempDirectory >>= liftIO . (`uniqueCombine` fileName) + tryChatError (liftError encryptErr $ encryptFile fsFilePath tmpFile cfArgs) >>= \case + Right () -> do + removeFile fsFilePath `catchChatError` \_ -> pure () + renameFile tmpFile fsFilePath + Left e -> do + eToView e + removeFile tmpFile `catchChatError` \_ -> pure () + withStore' (`removeFileCryptoArgs` fileId) + where + encryptErr e = fileErr $ e <> ", received file not encrypted" + fileErr = ChatError . CEFileWrite filePath + +getFileHandle :: Int64 -> FilePath -> (ChatController -> TVar (Map Int64 Handle)) -> IOMode -> CM Handle +getFileHandle fileId filePath files ioMode = do + fs <- asks files + h_ <- M.lookup fileId <$> readTVarIO fs + maybe (newHandle fs) pure h_ + where + newHandle fs = do + h <- openFile filePath ioMode `catchThrow` (ChatError . CEFileInternal . show) + atomically . modifyTVar fs $ M.insert fileId h + pure h + +isFileActive :: Int64 -> (ChatController -> TVar (Map Int64 Handle)) -> CM Bool +isFileActive fileId files = do + fs <- asks files + isJust . M.lookup fileId <$> readTVarIO fs + +cancelRcvFileTransfer :: User -> RcvFileTransfer -> CM (Maybe ConnId) +cancelRcvFileTransfer user ft@RcvFileTransfer {fileId, xftpRcvFile, rcvFileInline} = + cancel' `catchChatError` (\e -> eToView e $> fileConnId) + where + cancel' = do + lift $ closeFileHandle fileId rcvFiles + withStore' $ \db -> do + updateFileCancelled db user fileId CIFSRcvCancelled + updateRcvFileStatus db fileId FSCancelled + deleteRcvFileChunks db ft + case xftpRcvFile of + Just XFTPRcvFile {agentRcvFileId = Just (AgentRcvFileId aFileId), agentRcvFileDeleted} -> + unless agentRcvFileDeleted $ agentXFTPDeleteRcvFile aFileId fileId + _ -> pure () + pure fileConnId + fileConnId = if isNothing xftpRcvFile && isNothing rcvFileInline then liveRcvFileTransferConnId ft else Nothing + +cancelSndFile :: User -> FileTransferMeta -> [SndFileTransfer] -> Bool -> CM [ConnId] +cancelSndFile user FileTransferMeta {fileId, xftpSndFile} fts sendCancel = do + withStore' (\db -> updateFileCancelled db user fileId CIFSSndCancelled) + `catchChatError` eToView + case xftpSndFile of + Nothing -> + catMaybes <$> forM fts (\ft -> cancelSndFileTransfer user ft sendCancel) + Just xsf -> do + forM_ fts (\ft -> cancelSndFileTransfer user ft False) + lift (agentXFTPDeleteSndFileRemote user xsf fileId) `catchChatError` eToView + pure [] + +-- TODO v6.0 remove +cancelSndFileTransfer :: User -> SndFileTransfer -> Bool -> CM (Maybe ConnId) +cancelSndFileTransfer user@User {userId} ft@SndFileTransfer {fileId, connId, agentConnId = AgentConnId acId, fileStatus, fileInline} sendCancel = + if fileStatus == FSCancelled || fileStatus == FSComplete + then pure Nothing + else cancel' `catchChatError` (\e -> eToView e $> fileConnId) + where + cancel' = do + withStore' $ \db -> do + updateSndFileStatus db ft FSCancelled + deleteSndFileChunks db ft + when sendCancel $ case fileInline of + Just _ -> do + vr <- chatVersionRange + (sharedMsgId, conn) <- withStore $ \db -> (,) <$> getSharedMsgIdByFileId db userId fileId <*> getConnectionById db vr user connId + void $ sendDirectMessage_ conn (BFileChunk sharedMsgId FileChunkCancel) (ConnectionId connId) + _ -> withAgent $ \a -> void . sendMessage a acId PQEncOff SMP.noMsgFlags $ smpEncode FileChunkCancel + pure fileConnId + fileConnId = if isNothing fileInline then Just acId else Nothing + +closeFileHandle :: Int64 -> (ChatController -> TVar (Map Int64 Handle)) -> CM' () +closeFileHandle fileId files = do + fs <- asks files + h_ <- atomically . stateTVar fs $ \m -> (M.lookup fileId m, M.delete fileId m) + liftIO $ mapM_ hClose h_ `catchAll_` pure () + +deleteMembersConnections :: User -> [GroupMember] -> CM () +deleteMembersConnections user members = deleteMembersConnections' user members False + +deleteMembersConnections' :: User -> [GroupMember] -> Bool -> CM () +deleteMembersConnections' user members waitDelivery = do + let memberConns = mapMaybe (\GroupMember {activeConn} -> activeConn) members + deleteAgentConnectionsAsync' (map aConnId memberConns) waitDelivery + lift . void . withStoreBatch' $ \db -> map (\Connection {connId} -> deleteConnectionRecord db user connId) memberConns + +deleteMemberConnection :: GroupMember -> CM () +deleteMemberConnection mem = deleteMemberConnection' mem False + +deleteMemberConnection' :: GroupMember -> Bool -> CM () +deleteMemberConnection' GroupMember {activeConn} waitDelivery = do + forM_ activeConn $ \conn -> do + deleteAgentConnectionAsync' (aConnId conn) waitDelivery + withStore' $ \db -> updateConnectionStatus db conn ConnDeleted + +deleteOrUpdateMemberRecord :: User -> GroupMember -> CM () +deleteOrUpdateMemberRecord user member = + withStore' $ \db -> deleteOrUpdateMemberRecordIO db user member + +deleteOrUpdateMemberRecordIO :: DB.Connection -> User -> GroupMember -> IO () +deleteOrUpdateMemberRecordIO db user@User {userId} member = + checkGroupMemberHasItems db user member >>= \case + Just _ -> updateGroupMemberStatus db userId member GSMemRemoved + Nothing -> deleteGroupMember db user member + +sendDirectContactMessages :: MsgEncodingI e => User -> Contact -> NonEmpty (ChatMsgEvent e) -> CM [Either ChatError SndMessage] +sendDirectContactMessages user ct events = do + Connection {connChatVersion = v} <- liftEither $ contactSendConn_ ct + if v >= batchSend2Version + then sendDirectContactMessages' user ct events + else forM (L.toList events) $ \evt -> + (Right . fst <$> sendDirectContactMessage user ct evt) `catchChatError` \e -> pure (Left e) + +sendDirectContactMessages' :: MsgEncodingI e => User -> Contact -> NonEmpty (ChatMsgEvent e) -> CM [Either ChatError SndMessage] +sendDirectContactMessages' user ct events = do + conn@Connection {connId} <- liftEither $ contactSendConn_ ct + let idsEvts = L.map (ConnectionId connId,) events + msgFlags = MsgFlags {notification = any (hasNotification . toCMEventTag) events} + sndMsgs_ <- lift $ createSndMessages idsEvts + (sndMsgs', pqEnc_) <- batchSendConnMessagesB user conn msgFlags sndMsgs_ + forM_ pqEnc_ $ \pqEnc' -> void $ createContactPQSndItem user ct conn pqEnc' + pure sndMsgs' + +sendDirectContactMessage :: MsgEncodingI e => User -> Contact -> ChatMsgEvent e -> CM (SndMessage, Int64) +sendDirectContactMessage user ct chatMsgEvent = do + conn@Connection {connId} <- liftEither $ contactSendConn_ ct + r <- sendDirectMessage_ conn chatMsgEvent (ConnectionId connId) + let (sndMessage, msgDeliveryId, pqEnc') = r + void $ createContactPQSndItem user ct conn pqEnc' + pure (sndMessage, msgDeliveryId) + +contactSendConn_ :: Contact -> Either ChatError Connection +contactSendConn_ ct@Contact {activeConn} = case activeConn of + Nothing -> err $ CEContactNotReady ct + Just conn + | not (connReady conn) -> err $ CEContactNotReady ct + | not (contactActive ct) -> err $ CEContactNotActive ct + | connDisabled conn -> err $ CEContactDisabled ct + | otherwise -> Right conn + where + err = Left . ChatError + +-- unlike sendGroupMemberMessage, this function will not store message as pending +-- TODO v5.8 we could remove pending messages once all clients support forwarding +sendDirectMemberMessage :: MsgEncodingI e => Connection -> ChatMsgEvent e -> GroupId -> CM (SndMessage, Int64, PQEncryption) +sendDirectMemberMessage conn chatMsgEvent groupId = sendDirectMessage_ conn chatMsgEvent (GroupId groupId) + +sendDirectMessage_ :: MsgEncodingI e => Connection -> ChatMsgEvent e -> ConnOrGroupId -> CM (SndMessage, Int64, PQEncryption) +sendDirectMessage_ conn chatMsgEvent connOrGroupId = do + when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn) + msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent connOrGroupId + -- TODO move compressed body to SndMessage and compress in createSndMessage + (msgDeliveryId, pqEnc') <- deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId + pure (msg, msgDeliveryId, pqEnc') + +createSndMessage :: MsgEncodingI e => ChatMsgEvent e -> ConnOrGroupId -> CM SndMessage +createSndMessage chatMsgEvent connOrGroupId = + liftEither . runIdentity =<< lift (createSndMessages $ Identity (connOrGroupId, chatMsgEvent)) + +createSndMessages :: forall e t. (MsgEncodingI e, Traversable t) => t (ConnOrGroupId, ChatMsgEvent e) -> CM' (t (Either ChatError SndMessage)) +createSndMessages idsEvents = do + g <- asks random + vr <- chatVersionRange' + withStoreBatch $ \db -> fmap (createMsg db g vr) idsEvents + where + createMsg :: DB.Connection -> TVar ChaChaDRG -> VersionRangeChat -> (ConnOrGroupId, ChatMsgEvent e) -> IO (Either ChatError SndMessage) + createMsg db g vr (connOrGroupId, evnt) = runExceptT $ do + withExceptT ChatErrorStore $ createNewSndMessage db g connOrGroupId evnt encodeMessage + where + encodeMessage sharedMsgId = + encodeChatMessage maxEncodedMsgLength ChatMessage {chatVRange = vr, msgId = Just sharedMsgId, chatMsgEvent = evnt} + +sendGroupMemberMessages :: forall e. MsgEncodingI e => User -> Connection -> NonEmpty (ChatMsgEvent e) -> GroupId -> CM () +sendGroupMemberMessages user conn events groupId = do + when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn) + let idsEvts = L.map (GroupId groupId,) events + (errs, msgs) <- lift $ partitionEithers . L.toList <$> createSndMessages idsEvts + unless (null errs) $ toView $ CEvtChatErrors errs + forM_ (L.nonEmpty msgs) $ \msgs' -> + batchSendConnMessages user conn MsgFlags {notification = True} msgs' + +batchSendConnMessages :: User -> Connection -> MsgFlags -> NonEmpty SndMessage -> CM ([Either ChatError SndMessage], Maybe PQEncryption) +batchSendConnMessages user conn msgFlags msgs = + batchSendConnMessagesB user conn msgFlags $ L.map Right msgs + +batchSendConnMessagesB :: User -> Connection -> MsgFlags -> NonEmpty (Either ChatError SndMessage) -> CM ([Either ChatError SndMessage], Maybe PQEncryption) +batchSendConnMessagesB _user conn msgFlags msgs_ = do + let batched_ = batchSndMessagesJSON msgs_ + case L.nonEmpty batched_ of + Just batched' -> do + let msgReqs = L.map (fmap msgBatchReq_) batched' + delivered <- deliverMessagesB msgReqs + let msgs' = concat $ L.zipWith flattenMsgs batched' delivered + pqEnc = findLastPQEnc delivered + when (length msgs' /= length msgs_) $ logError "batchSendConnMessagesB: msgs_ and msgs' length mismatch" + pure (msgs', pqEnc) + Nothing -> pure ([], Nothing) + where + msgBatchReq_ :: MsgBatch -> ChatMsgReq + msgBatchReq_ (MsgBatch batchBody sndMsgs) = + (conn, msgFlags, (vrValue batchBody, map (\SndMessage {msgId} -> msgId) sndMsgs)) + flattenMsgs :: Either ChatError MsgBatch -> Either ChatError ([Int64], PQEncryption) -> [Either ChatError SndMessage] + flattenMsgs (Right (MsgBatch _ sndMsgs)) (Right _) = map Right sndMsgs + flattenMsgs (Right (MsgBatch _ sndMsgs)) (Left ce) = replicate (length sndMsgs) (Left ce) + flattenMsgs (Left ce) _ = [Left ce] -- restore original ChatError + findLastPQEnc :: NonEmpty (Either ChatError ([Int64], PQEncryption)) -> Maybe PQEncryption + findLastPQEnc = foldr' (\x acc -> case x of Right (_, pqEnc) -> Just pqEnc; Left _ -> acc) Nothing + +batchSndMessagesJSON :: NonEmpty (Either ChatError SndMessage) -> [Either ChatError MsgBatch] +batchSndMessagesJSON = batchMessages maxEncodedMsgLength . L.toList + +encodeConnInfo :: MsgEncodingI e => ChatMsgEvent e -> CM ByteString +encodeConnInfo chatMsgEvent = do + vr <- chatVersionRange + encodeConnInfoPQ PQSupportOff (maxVersion vr) chatMsgEvent + +encodeConnInfoPQ :: MsgEncodingI e => PQSupport -> VersionChat -> ChatMsgEvent e -> CM ByteString +encodeConnInfoPQ pqSup v chatMsgEvent = do + vr <- chatVersionRange + let info = ChatMessage {chatVRange = vr, msgId = Nothing, chatMsgEvent} + case encodeChatMessage maxEncodedInfoLength info of + ECMEncoded connInfo -> case pqSup of + PQSupportOn | v >= pqEncryptionCompressionVersion && B.length connInfo > maxCompressedInfoLength -> do + let connInfo' = compressedBatchMsgBody_ connInfo + when (B.length connInfo' > maxCompressedInfoLength) $ throwChatError $ CEException "large compressed info" + pure connInfo' + _ -> pure connInfo + ECMLarge -> throwChatError $ CEException "large info" + +deliverMessage :: Connection -> CMEventTag e -> MsgBody -> MessageId -> CM (Int64, PQEncryption) +deliverMessage conn cmEventTag msgBody msgId = do + let msgFlags = MsgFlags {notification = hasNotification cmEventTag} + deliverMessage' conn msgFlags msgBody msgId + +deliverMessage' :: Connection -> MsgFlags -> MsgBody -> MessageId -> CM (Int64, PQEncryption) +deliverMessage' conn msgFlags msgBody msgId = + deliverMessages ((conn, msgFlags, (vrValue msgBody, [msgId])) :| []) >>= \case + r :| [] -> case r of + Right ([deliveryId], pqEnc) -> pure (deliveryId, pqEnc) + Right (deliveryIds, _) -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 delivery id, got " <> show (length deliveryIds) + Left e -> throwError e + rs -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 result, got " <> show (length rs) + +-- [MessageId] - SndMessage ids inside MsgBatch, or single message id +type ChatMsgReq = (Connection, MsgFlags, (ValueOrRef MsgBody, [MessageId])) + +deliverMessages :: NonEmpty ChatMsgReq -> CM (NonEmpty (Either ChatError ([Int64], PQEncryption))) +deliverMessages msgs = deliverMessagesB $ L.map Right msgs + +deliverMessagesB :: NonEmpty (Either ChatError ChatMsgReq) -> CM (NonEmpty (Either ChatError ([Int64], PQEncryption))) +deliverMessagesB msgReqs = do + msgReqs' <- if any connSupportsPQ msgReqs then liftIO compressBodies else pure msgReqs + sent <- L.zipWith prepareBatch msgReqs' <$> withAgent (`sendMessagesB` snd (mapAccumL toAgent Nothing msgReqs')) + lift . void $ withStoreBatch' $ \db -> map (updatePQSndEnabled db) (rights . L.toList $ sent) + lift . withStoreBatch $ \db -> L.map (bindRight $ createDelivery db) sent + where + connSupportsPQ = \case + Right (Connection {pqSupport = PQSupportOn, connChatVersion = v}, _, _) -> v >= pqEncryptionCompressionVersion + _ -> False + compressBodies = + forME msgReqs $ \(conn, msgFlags, (mbr, msgIds)) -> runExceptT $ do + mbr' <- case mbr of + VRValue i msgBody | B.length msgBody > maxCompressedMsgLength -> do + let msgBody' = compressedBatchMsgBody_ msgBody + when (B.length msgBody' > maxCompressedMsgLength) $ throwError $ ChatError $ CEException "large compressed message" + pure $ VRValue i msgBody' + v -> pure v + pure (conn, msgFlags, (mbr', msgIds)) + toAgent prev = \case + Right (conn@Connection {connId, pqEncryption}, msgFlags, (mbr, _msgIds)) -> + let cId = case prev of + Just prevId | prevId == connId -> "" + _ -> aConnId conn + in (Just connId, Right (cId, pqEncryption, msgFlags, mbr)) + Left _ce -> (prev, Left (AP.INTERNAL "ChatError, skip")) -- as long as it is Left, the agent batchers should just step over it + prepareBatch (Right req) (Right ar) = Right (req, ar) + prepareBatch (Left ce) _ = Left ce -- restore original ChatError + prepareBatch _ (Left ae) = Left $ ChatErrorAgent ae Nothing + createDelivery :: DB.Connection -> (ChatMsgReq, (AgentMsgId, PQEncryption)) -> IO (Either ChatError ([Int64], PQEncryption)) + createDelivery db ((Connection {connId}, _, (_, msgIds)), (agentMsgId, pqEnc')) = do + Right . (,pqEnc') <$> mapM (createSndMsgDelivery db (SndMsgDelivery {connId, agentMsgId})) msgIds + updatePQSndEnabled :: DB.Connection -> (ChatMsgReq, (AgentMsgId, PQEncryption)) -> IO () + updatePQSndEnabled db ((Connection {connId, pqSndEnabled}, _, _), (_, pqSndEnabled')) = + case (pqSndEnabled, pqSndEnabled') of + (Just b, b') | b' /= b -> updatePQ + (Nothing, PQEncOn) -> updatePQ + _ -> pure () + where + updatePQ = updateConnPQSndEnabled db connId pqSndEnabled' + +sendGroupMessage :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM SndMessage +sendGroupMessage user gInfo members chatMsgEvent = do + sendGroupMessages user gInfo members (chatMsgEvent :| []) >>= \case + ((Right msg) :| [], _) -> pure msg + _ -> throwChatError $ CEInternalError "sendGroupMessage: expected 1 message" + +sendGroupMessage' :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM SndMessage +sendGroupMessage' user gInfo members chatMsgEvent = + sendGroupMessages_ user gInfo members (chatMsgEvent :| []) >>= \case + ((Right msg) :| [], _) -> pure msg + _ -> throwChatError $ CEInternalError "sendGroupMessage': expected 1 message" + +sendGroupMessages :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult) +sendGroupMessages user gInfo members events = do + -- TODO [knocking] when sending to all, send profile update to pending approval members too, then filter for next step? + when shouldSendProfileUpdate $ + sendProfileUpdate `catchChatError` eToView + sendGroupMessages_ user gInfo members events + where + User {profile = p, userMemberProfileUpdatedAt} = user + GroupInfo {userMemberProfileSentAt} = gInfo + shouldSendProfileUpdate + | incognitoMembership gInfo = False + | otherwise = + case (userMemberProfileSentAt, userMemberProfileUpdatedAt) of + (Just lastSentTs, Just lastUpdateTs) -> lastSentTs < lastUpdateTs + (Nothing, Just _) -> True + _ -> False + sendProfileUpdate = do + let members' = filter (`supportsVersion` memberProfileUpdateVersion) members + profileUpdateEvent = XInfo $ redactedMemberProfile $ fromLocalProfile p + void $ sendGroupMessage' user gInfo members' profileUpdateEvent + currentTs <- liftIO getCurrentTime + withStore' $ \db -> updateUserMemberProfileSentAt db user gInfo currentTs + +data GroupSndResult = GroupSndResult + { sentTo :: [(GroupMemberId, Either ChatError [MessageId], Either ChatError ([Int64], PQEncryption))], + pending :: [(GroupMemberId, Either ChatError MessageId, Either ChatError ())], + forwarded :: [GroupMember] + } + +sendGroupMessages_ :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult) +sendGroupMessages_ _user gInfo@GroupInfo {groupId} members events = do + let idsEvts = L.map (GroupId groupId,) events + sndMsgs_ <- lift $ createSndMessages idsEvts + -- TODO [knocking] Possibly we need to pass GroupSndScope through all functions to here to avoid ad-hoc filtering. + recipientMembers <- case members of + [m] | memberStatus m == GSMemPendingApproval -> pure [m] + _ -> liftIO $ shuffleMembers (filter memberCurrent members) + let msgFlags = MsgFlags {notification = any (hasNotification . toCMEventTag) events} + (toSendSeparate, toSendBatched, toPending, forwarded, _, dups) = + foldr' addMember ([], [], [], [], S.empty, 0 :: Int) recipientMembers + when (dups /= 0) $ logError $ "sendGroupMessages_: " <> tshow dups <> " duplicate members" + -- TODO PQ either somehow ensure that group members connections cannot have pqSupport/pqEncryption or pass Off's here + -- Deliver to toSend members + let (sendToMemIds, msgReqs) = prepareMsgReqs msgFlags sndMsgs_ toSendSeparate toSendBatched + delivered <- maybe (pure []) (fmap L.toList . deliverMessagesB) $ L.nonEmpty msgReqs + when (length delivered /= length sendToMemIds) $ logError "sendGroupMessages_: sendToMemIds and delivered length mismatch" + -- Save as pending for toPending members + let (pendingMemIds, pendingReqs) = preparePending sndMsgs_ toPending + stored <- lift $ withStoreBatch (\db -> map (bindRight $ createPendingMsg db) pendingReqs) + when (length stored /= length pendingMemIds) $ logError "sendGroupMessages_: pendingMemIds and stored length mismatch" + -- Zip for easier access to results + let sentTo = zipWith3 (\mId mReq r -> (mId, fmap (\(_, _, (_, msgIds)) -> msgIds) mReq, r)) sendToMemIds msgReqs delivered + pending = zipWith3 (\mId pReq r -> (mId, fmap snd pReq, r)) pendingMemIds pendingReqs stored + pure (sndMsgs_, GroupSndResult {sentTo, pending, forwarded}) + where + shuffleMembers :: [GroupMember] -> IO [GroupMember] + shuffleMembers ms = do + let (adminMs, otherMs) = partition isAdmin ms + liftM2 (<>) (shuffle adminMs) (shuffle otherMs) + where + isAdmin GroupMember {memberRole} = memberRole >= GRAdmin + addMember m acc@(toSendSeparate, toSendBatched, pending, forwarded, !mIds, !dups) = + case memberSendAction gInfo events members m of + Just a + | mId `S.member` mIds -> (toSendSeparate, toSendBatched, pending, forwarded, mIds, dups + 1) + | otherwise -> case a of + MSASend conn -> ((m, conn) : toSendSeparate, toSendBatched, pending, forwarded, mIds', dups) + MSASendBatched conn -> (toSendSeparate, (m, conn) : toSendBatched, pending, forwarded, mIds', dups) + MSAPending -> (toSendSeparate, toSendBatched, m : pending, forwarded, mIds', dups) + MSAForwarded -> (toSendSeparate, toSendBatched, pending, m : forwarded, mIds', dups) + Nothing -> acc + where + mId = groupMemberId' m + mIds' = S.insert mId mIds + prepareMsgReqs :: MsgFlags -> NonEmpty (Either ChatError SndMessage) -> [(GroupMember, Connection)] -> [(GroupMember, Connection)] -> ([GroupMemberId], [Either ChatError ChatMsgReq]) + prepareMsgReqs msgFlags msgs toSendSeparate toSendBatched = do + let batched_ = batchSndMessagesJSON msgs + case L.nonEmpty batched_ of + Just batched' -> do + let lenMsgs = length msgs + (memsSep, mreqsSep) = foldMembers lenMsgs sndMessageMBR msgs toSendSeparate + (memsBtch, mreqsBtch) = foldMembers (length batched' + lenMsgs) msgBatchMBR batched' toSendBatched + (memsSep <> memsBtch, mreqsSep <> mreqsBtch) + Nothing -> ([], []) + where + foldMembers :: forall a. Int -> (Maybe Int -> Int -> a -> (ValueOrRef MsgBody, [MessageId])) -> NonEmpty (Either ChatError a) -> [(GroupMember, Connection)] -> ([GroupMemberId], [Either ChatError ChatMsgReq]) + foldMembers lastRef mkMb mbs mems = snd $ foldr' foldMsgBodies (lastMemIdx_, ([], [])) mems + where + lastMemIdx_ = let len = length mems in if len > 1 then Just len else Nothing + foldMsgBodies :: (GroupMember, Connection) -> (Maybe Int, ([GroupMemberId], [Either ChatError ChatMsgReq])) -> (Maybe Int, ([GroupMemberId], [Either ChatError ChatMsgReq])) + foldMsgBodies (GroupMember {groupMemberId}, conn) (memIdx_, memIdsReqs) = + (subtract 1 <$> memIdx_,) $ snd $ foldr' addBody (lastRef, memIdsReqs) mbs + where + addBody :: Either ChatError a -> (Int, ([GroupMemberId], [Either ChatError ChatMsgReq])) -> (Int, ([GroupMemberId], [Either ChatError ChatMsgReq])) + addBody mb (i, (memIds, reqs)) = + let req = (conn,msgFlags,) . mkMb memIdx_ i <$> mb + in (i - 1, (groupMemberId : memIds, req : reqs)) + sndMessageMBR :: Maybe Int -> Int -> SndMessage -> (ValueOrRef MsgBody, [MessageId]) + sndMessageMBR memIdx_ i SndMessage {msgId, msgBody} = (vrValue_ memIdx_ i msgBody, [msgId]) + msgBatchMBR :: Maybe Int -> Int -> MsgBatch -> (ValueOrRef MsgBody, [MessageId]) + msgBatchMBR memIdx_ i (MsgBatch batchBody sndMsgs) = (vrValue_ memIdx_ i batchBody, map (\SndMessage {msgId} -> msgId) sndMsgs) + vrValue_ memIdx_ i v = case memIdx_ of + Nothing -> VRValue Nothing v -- sending to one member, do not reference bodies + Just 1 -> VRValue (Just i) v + Just _ -> VRRef i + preparePending :: NonEmpty (Either ChatError SndMessage) -> [GroupMember] -> ([GroupMemberId], [Either ChatError (GroupMemberId, MessageId)]) + preparePending msgs_ = + foldr' foldMsgs ([], []) + where + foldMsgs :: GroupMember -> ([GroupMemberId], [Either ChatError (GroupMemberId, MessageId)]) -> ([GroupMemberId], [Either ChatError (GroupMemberId, MessageId)]) + foldMsgs GroupMember {groupMemberId} memIdsReqs = + foldr' (\msg_ (memIds, reqs) -> (groupMemberId : memIds, fmap pendingReq msg_ : reqs)) memIdsReqs msgs_ + where + pendingReq :: SndMessage -> (GroupMemberId, MessageId) + pendingReq SndMessage {msgId} = (groupMemberId, msgId) + createPendingMsg :: DB.Connection -> (GroupMemberId, MessageId) -> IO (Either ChatError ()) + createPendingMsg db (groupMemberId, msgId) = + createPendingGroupMessage db groupMemberId msgId Nothing $> Right () + +data MemberSendAction = MSASend Connection | MSASendBatched Connection | MSAPending | MSAForwarded + +memberSendAction :: GroupInfo -> NonEmpty (ChatMsgEvent e) -> [GroupMember] -> GroupMember -> Maybe MemberSendAction +memberSendAction gInfo events members m@GroupMember {memberRole, memberStatus} = case memberConn m of + Nothing -> pendingOrForwarded + Just conn@Connection {connStatus} + | connDisabled conn || connStatus == ConnDeleted || memberStatus == GSMemRejected -> Nothing + | connInactive conn -> Just MSAPending + | connStatus == ConnSndReady || connStatus == ConnReady -> sendBatchedOrSeparate conn + | otherwise -> pendingOrForwarded + where + sendBatchedOrSeparate conn + -- admin doesn't support batch forwarding - send messages separately so that admin can forward one by one + | memberRole >= GRAdmin && not (m `supportsVersion` batchSend2Version) = Just (MSASend conn) + -- either member is not admin, or admin supports batched forwarding + | otherwise = Just (MSASendBatched conn) + pendingOrForwarded = case memberCategory m of + GCUserMember -> Nothing -- shouldn't happen + GCInviteeMember -> Just MSAPending + GCHostMember -> Just MSAPending + GCPreMember -> forwardSupportedOrPending (invitedByGroupMemberId $ membership gInfo) + GCPostMember -> forwardSupportedOrPending (invitedByGroupMemberId m) + where + forwardSupportedOrPending invitingMemberId_ + | membersSupport && all isForwardedGroupMsg events = Just MSAForwarded + | any isXGrpMsgForward events = Nothing + | otherwise = Just MSAPending + where + membersSupport = + m `supportsVersion` groupForwardVersion && invitingMemberSupportsForward + invitingMemberSupportsForward = case invitingMemberId_ of + Just invMemberId -> + -- can be optimized for large groups by replacing [GroupMember] with Map GroupMemberId GroupMember + case find (\m' -> groupMemberId' m' == invMemberId) members of + Just invitingMember -> invitingMember `supportsVersion` groupForwardVersion + Nothing -> False + Nothing -> False + isXGrpMsgForward event = case event of + XGrpMsgForward {} -> True + _ -> False + +sendGroupMemberMessage :: MsgEncodingI e => GroupInfo -> GroupMember -> ChatMsgEvent e -> Maybe Int64 -> CM () -> CM () +sendGroupMemberMessage gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId} chatMsgEvent introId_ postDeliver = do + msg <- createSndMessage chatMsgEvent (GroupId groupId) + messageMember msg `catchChatError` eToView + where + messageMember :: SndMessage -> CM () + messageMember SndMessage {msgId, msgBody} = forM_ (memberSendAction gInfo (chatMsgEvent :| []) [m] m) $ \case + MSASend conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver + MSASendBatched conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver + MSAPending -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_ + MSAForwarded -> pure () + +-- TODO ensure order - pending messages interleave with user input messages +sendPendingGroupMessages :: User -> GroupMember -> Connection -> CM () +sendPendingGroupMessages user GroupMember {groupMemberId} conn = do + pgms <- withStore' $ \db -> getPendingGroupMessages db groupMemberId + forM_ (L.nonEmpty pgms) $ \pgms' -> do + let msgs = L.map (\(sndMsg, _, _) -> sndMsg) pgms' + void $ batchSendConnMessages user conn MsgFlags {notification = True} msgs + lift . void . withStoreBatch' $ \db -> L.map (\SndMessage {msgId} -> deletePendingGroupMessage db groupMemberId msgId) msgs + lift . void . withStoreBatch' $ \db -> L.map (\(_, tag, introId_) -> updateIntro_ db tag introId_) pgms' + where + updateIntro_ :: DB.Connection -> ACMEventTag -> Maybe Int64 -> IO () + updateIntro_ db tag introId_ = case (tag, introId_) of + (ACMEventTag _ XGrpMemFwd_, Just introId) -> updateIntroStatus db introId GMIntroInvForwarded + _ -> pure () + +saveDirectRcvMSG :: MsgEncodingI e => Connection -> MsgMeta -> MsgBody -> ChatMessage e -> CM (Connection, RcvMessage) +saveDirectRcvMSG conn@Connection {connId} agentMsgMeta msgBody ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} = do + conn' <- updatePeerChatVRange conn chatVRange + let agentMsgId = fst $ recipient agentMsgMeta + newMsg = NewRcvMessage {chatMsgEvent, msgBody} + rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta} + msg <- withStore $ \db -> createNewMessageAndRcvMsgDelivery db (ConnectionId connId) newMsg sharedMsgId_ rcvMsgDelivery Nothing + pure (conn', msg) + +saveGroupRcvMsg :: MsgEncodingI e => User -> GroupId -> GroupMember -> Connection -> MsgMeta -> MsgBody -> ChatMessage e -> CM (GroupMember, Connection, RcvMessage) +saveGroupRcvMsg user groupId authorMember conn@Connection {connId} agentMsgMeta msgBody ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} = do + (am'@GroupMember {memberId = amMemId, groupMemberId = amGroupMemId}, conn') <- updateMemberChatVRange authorMember conn chatVRange + let agentMsgId = fst $ recipient agentMsgMeta + newMsg = NewRcvMessage {chatMsgEvent, msgBody} + rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta} + msg <- + withStore (\db -> createNewMessageAndRcvMsgDelivery db (GroupId groupId) newMsg sharedMsgId_ rcvMsgDelivery $ Just amGroupMemId) + `catchChatError` \e -> case e of + ChatErrorStore (SEDuplicateGroupMessage _ _ _ (Just forwardedByGroupMemberId)) -> do + vr <- chatVersionRange + fm <- withStore $ \db -> getGroupMember db vr user groupId forwardedByGroupMemberId + forM_ (memberConn fm) $ \fmConn -> + void $ sendDirectMemberMessage fmConn (XGrpMemCon amMemId) groupId + throwError e + _ -> throwError e + pure (am', conn', msg) + +saveGroupFwdRcvMsg :: MsgEncodingI e => User -> GroupId -> GroupMember -> GroupMember -> MsgBody -> ChatMessage e -> CM RcvMessage +saveGroupFwdRcvMsg user groupId forwardingMember refAuthorMember@GroupMember {memberId = refMemberId} msgBody ChatMessage {msgId = sharedMsgId_, chatMsgEvent} = do + let newMsg = NewRcvMessage {chatMsgEvent, msgBody} + fwdMemberId = Just $ groupMemberId' forwardingMember + refAuthorId = Just $ groupMemberId' refAuthorMember + withStore (\db -> createNewRcvMessage db (GroupId groupId) newMsg sharedMsgId_ refAuthorId fwdMemberId) + `catchChatError` \e -> case e of + ChatErrorStore (SEDuplicateGroupMessage _ _ (Just authorGroupMemberId) Nothing) -> do + vr <- chatVersionRange + am@GroupMember {memberId = amMemberId} <- withStore $ \db -> getGroupMember db vr user groupId authorGroupMemberId + if sameMemberId refMemberId am + then forM_ (memberConn forwardingMember) $ \fmConn -> + void $ sendDirectMemberMessage fmConn (XGrpMemCon amMemberId) groupId + else toView $ CEvtMessageError user "error" "saveGroupFwdRcvMsg: referenced author member id doesn't match message member id" + throwError e + _ -> throwError e + +saveSndChatItem :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> CM (ChatItem c 'MDSnd) +saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothing Nothing Nothing Nothing False + +-- TODO [mentions] optimize by avoiding unnecesary parsing of control messages +saveSndChatItem' :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIFile 'MDSnd) -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> CM (ChatItem c 'MDSnd) +saveSndChatItem' user cd msg content ciFile quotedItem itemForwarded itemTimed live = do + let itemTexts = ciContentTexts content + saveSndChatItems user cd Nothing [Right NewSndChatItemData {msg, content, itemTexts, itemMentions = M.empty, ciFile, quotedItem, itemForwarded}] itemTimed live >>= \case + [Right ci] -> pure ci + _ -> throwChatError $ CEInternalError "saveSndChatItem': expected 1 item" + +data NewSndChatItemData c = NewSndChatItemData + { msg :: SndMessage, + content :: CIContent 'MDSnd, + itemTexts :: (Text, Maybe MarkdownList), + itemMentions :: Map MemberName CIMention, + ciFile :: Maybe (CIFile 'MDSnd), + quotedItem :: Maybe (CIQuote c), + itemForwarded :: Maybe CIForwardedFrom + } + +saveSndChatItems :: + forall c. + ChatTypeI c => + User -> + ChatDirection c 'MDSnd -> + Maybe NotInHistory -> + [Either ChatError (NewSndChatItemData c)] -> + Maybe CITimed -> + Bool -> + CM [Either ChatError (ChatItem c 'MDSnd)] +saveSndChatItems user cd notInHistory_ itemsData itemTimed live = do + createdAt <- liftIO getCurrentTime + when (contactChatDeleted cd || any (\NewSndChatItemData {content} -> ciRequiresAttention content) (rights itemsData)) $ + withStore' (\db -> updateChatTs db user cd createdAt) + lift $ withStoreBatch (\db -> map (bindRight $ createItem db createdAt) itemsData) + where + createItem :: DB.Connection -> UTCTime -> NewSndChatItemData c -> IO (Either ChatError (ChatItem c 'MDSnd)) + createItem db createdAt NewSndChatItemData {msg = msg@SndMessage {sharedMsgId}, content, itemTexts, itemMentions, ciFile, quotedItem, itemForwarded} = do + ciId <- createNewSndChatItem db user cd notInHistory_ msg content quotedItem itemForwarded itemTimed live createdAt + forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt + let ci = mkChatItem_ cd ciId content itemTexts ciFile quotedItem (Just sharedMsgId) itemForwarded itemTimed live False createdAt Nothing createdAt + Right <$> case cd of + CDGroupSnd g | not (null itemMentions) -> createGroupCIMentions db g ci itemMentions + _ -> pure ci + +saveRcvChatItemNoParse :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> CIContent 'MDRcv -> CM (ChatItem c 'MDRcv) +saveRcvChatItemNoParse user cd msg brokerTs = saveRcvChatItem user cd msg brokerTs . ciContentNoParse + +saveRcvChatItem :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) -> CM (ChatItem c 'MDRcv) +saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} brokerTs content = + saveRcvChatItem' user cd Nothing msg sharedMsgId_ brokerTs content Nothing Nothing False M.empty + +ciContentNoParse :: CIContent 'MDRcv -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) +ciContentNoParse content = (content, (ciContentToText content, Nothing)) + +saveRcvChatItem' :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> Maybe NotInHistory -> RcvMessage -> Maybe SharedMsgId -> UTCTime -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) -> Maybe (CIFile 'MDRcv) -> Maybe CITimed -> Bool -> Map MemberName MsgMention -> CM (ChatItem c 'MDRcv) +saveRcvChatItem' user cd notInHistory_ msg@RcvMessage {chatMsgEvent, forwardedByMember} sharedMsgId_ brokerTs (content, (t, ft_)) ciFile itemTimed live mentions = do + createdAt <- liftIO getCurrentTime + withStore' $ \db -> do + when (ciRequiresAttention content || contactChatDeleted cd) $ updateChatTs db user cd createdAt + (mentions' :: Map MemberName CIMention, userMention) <- case cd of + CDGroupRcv g@GroupInfo {membership} _ -> do + mentions' <- getRcvCIMentions db user g ft_ mentions + let userReply = case cmToQuotedMsg chatMsgEvent of + Just QuotedMsg {msgRef = MsgRef {memberId = Just mId}} -> sameMemberId mId membership + _ -> False + userMention' = userReply || any (\CIMention {memberId} -> sameMemberId memberId membership) mentions' + in pure (mentions', userMention') + CDDirectRcv _ -> pure (M.empty, False) + (ciId, quotedItem, itemForwarded) <- createNewRcvChatItem db user cd notInHistory_ msg sharedMsgId_ content itemTimed live userMention brokerTs createdAt + forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt + let ci = mkChatItem_ cd ciId content (t, ft_) ciFile quotedItem sharedMsgId_ itemForwarded itemTimed live userMention brokerTs forwardedByMember createdAt + case cd of + CDGroupRcv g _ | not (null mentions') -> createGroupCIMentions db g ci mentions' + _ -> pure ci + +-- TODO [mentions] optimize by avoiding unnecessary parsing +mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d +mkChatItem cd ciId content file quotedItem sharedMsgId itemForwarded itemTimed live userMention itemTs forwardedByMember currentTs = + let ts = ciContentTexts content + in mkChatItem_ cd ciId content ts file quotedItem sharedMsgId itemForwarded itemTimed live userMention itemTs forwardedByMember currentTs + +mkChatItem_ :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ChatItemId -> CIContent d -> (Text, Maybe MarkdownList) -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d +mkChatItem_ cd ciId content (itemText, formattedText) file quotedItem sharedMsgId itemForwarded itemTimed live userMention itemTs forwardedByMember currentTs = + let itemStatus = ciCreateStatus content + meta = mkCIMeta ciId content itemText itemStatus Nothing sharedMsgId itemForwarded Nothing False itemTimed (justTrue live) userMention currentTs itemTs forwardedByMember currentTs currentTs + in ChatItem {chatDir = toCIDirection cd, meta, content, mentions = M.empty, formattedText, quotedItem, reactions = [], file} + +createAgentConnectionAsync :: ConnectionModeI c => User -> CommandFunction -> Bool -> SConnectionMode c -> SubscriptionMode -> CM (CommandId, ConnId) +createAgentConnectionAsync user cmdFunction enableNtfs cMode subMode = do + cmdId <- withStore' $ \db -> createCommand db user Nothing cmdFunction + connId <- withAgent $ \a -> createConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cMode IKPQOff subMode + pure (cmdId, connId) + +joinAgentConnectionAsync :: User -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> CM (CommandId, ConnId) +joinAgentConnectionAsync user enableNtfs cReqUri cInfo subMode = do + cmdId <- withStore' $ \db -> createCommand db user Nothing CFJoinConn + connId <- withAgent $ \a -> joinConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cReqUri cInfo PQSupportOff subMode + pure (cmdId, connId) + +allowAgentConnectionAsync :: MsgEncodingI e => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> CM () +allowAgentConnectionAsync user conn@Connection {connId, pqSupport, connChatVersion} confId msg = do + cmdId <- withStore' $ \db -> createCommand db user (Just connId) CFAllowConn + dm <- encodeConnInfoPQ pqSupport connChatVersion msg + withAgent $ \a -> allowConnectionAsync a (aCorrId cmdId) (aConnId conn) confId dm + withStore' $ \db -> updateConnectionStatus db conn ConnAccepted + +agentAcceptContactAsync :: MsgEncodingI e => User -> Bool -> InvitationId -> ChatMsgEvent e -> SubscriptionMode -> PQSupport -> VersionChat -> CM (CommandId, ConnId) +agentAcceptContactAsync user enableNtfs invId msg subMode pqSup chatV = do + cmdId <- withStore' $ \db -> createCommand db user Nothing CFAcceptContact + dm <- encodeConnInfoPQ pqSup chatV msg + connId <- withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) enableNtfs invId dm pqSup subMode + pure (cmdId, connId) + +deleteAgentConnectionAsync :: ConnId -> CM () +deleteAgentConnectionAsync acId = deleteAgentConnectionAsync' acId False +{-# INLINE deleteAgentConnectionAsync #-} + +deleteAgentConnectionAsync' :: ConnId -> Bool -> CM () +deleteAgentConnectionAsync' acId waitDelivery = do + withAgent (\a -> deleteConnectionAsync a waitDelivery acId) `catchChatError` eToView + +deleteAgentConnectionsAsync :: [ConnId] -> CM () +deleteAgentConnectionsAsync acIds = deleteAgentConnectionsAsync' acIds False +{-# INLINE deleteAgentConnectionsAsync #-} + +deleteAgentConnectionsAsync' :: [ConnId] -> Bool -> CM () +deleteAgentConnectionsAsync' [] _ = pure () +deleteAgentConnectionsAsync' acIds waitDelivery = do + withAgent (\a -> deleteConnectionsAsync a waitDelivery acIds) `catchChatError` eToView + +agentXFTPDeleteRcvFile :: RcvFileId -> FileTransferId -> CM () +agentXFTPDeleteRcvFile aFileId fileId = do + lift $ withAgent' (`xftpDeleteRcvFile` aFileId) + withStore' $ \db -> setRcvFTAgentDeleted db fileId + +agentXFTPDeleteRcvFiles :: [(XFTPRcvFile, FileTransferId)] -> CM' () +agentXFTPDeleteRcvFiles rcvFiles = do + let rcvFiles' = filter (not . agentRcvFileDeleted . fst) rcvFiles + rfIds = mapMaybe fileIds rcvFiles' + withAgent' $ \a -> xftpDeleteRcvFiles a (map fst rfIds) + void . withStoreBatch' $ \db -> map (setRcvFTAgentDeleted db . snd) rfIds + where + fileIds :: (XFTPRcvFile, FileTransferId) -> Maybe (RcvFileId, FileTransferId) + fileIds (XFTPRcvFile {agentRcvFileId = Just (AgentRcvFileId aFileId)}, fileId) = Just (aFileId, fileId) + fileIds _ = Nothing + +agentXFTPDeleteSndFileRemote :: User -> XFTPSndFile -> FileTransferId -> CM' () +agentXFTPDeleteSndFileRemote user xsf fileId = + agentXFTPDeleteSndFilesRemote user [(xsf, fileId)] + +agentXFTPDeleteSndFilesRemote :: User -> [(XFTPSndFile, FileTransferId)] -> CM' () +agentXFTPDeleteSndFilesRemote user sndFiles = do + (_errs, redirects) <- partitionEithers <$> withStoreBatch' (\db -> map (lookupFileTransferRedirectMeta db user . snd) sndFiles) + let redirects' = mapMaybe mapRedirectMeta $ concat redirects + sndFilesAll = redirects' <> sndFiles + sndFilesAll' = filter (not . agentSndFileDeleted . fst) sndFilesAll + -- while file is being prepared and uploaded, it would not have description available; + -- this partitions files into those with and without descriptions - + -- files with description are deleted remotely, files without description are deleted internally + (sfsNoDescr, sfsWithDescr) <- partitionSndDescr sndFilesAll' [] [] + withAgent' $ \a -> xftpDeleteSndFilesInternal a sfsNoDescr + withAgent' $ \a -> xftpDeleteSndFilesRemote a (aUserId user) sfsWithDescr + void . withStoreBatch' $ \db -> map (setSndFTAgentDeleted db user . snd) sndFilesAll' + where + mapRedirectMeta :: FileTransferMeta -> Maybe (XFTPSndFile, FileTransferId) + mapRedirectMeta FileTransferMeta {fileId = fileId, xftpSndFile = Just sndFileRedirect} = Just (sndFileRedirect, fileId) + mapRedirectMeta _ = Nothing + partitionSndDescr :: + [(XFTPSndFile, FileTransferId)] -> + [SndFileId] -> + [(SndFileId, ValidFileDescription 'FSender)] -> + CM' ([SndFileId], [(SndFileId, ValidFileDescription 'FSender)]) + partitionSndDescr [] filesWithoutDescr filesWithDescr = pure (filesWithoutDescr, filesWithDescr) + partitionSndDescr ((XFTPSndFile {agentSndFileId = AgentSndFileId aFileId, privateSndFileDescr}, _) : xsfs) filesWithoutDescr filesWithDescr = + case privateSndFileDescr of + Nothing -> partitionSndDescr xsfs (aFileId : filesWithoutDescr) filesWithDescr + Just sfdText -> + tryChatError' (parseFileDescription sfdText) >>= \case + Left _ -> partitionSndDescr xsfs (aFileId : filesWithoutDescr) filesWithDescr + Right sfd -> partitionSndDescr xsfs filesWithoutDescr ((aFileId, sfd) : filesWithDescr) + +userProfileToSend :: User -> Maybe Profile -> Maybe Contact -> Bool -> Profile +userProfileToSend user@User {profile = p} incognitoProfile ct inGroup = do + let p' = fromMaybe (fromLocalProfile p) incognitoProfile + if inGroup + then redactedMemberProfile p' + else + let userPrefs = maybe (preferences' user) (const Nothing) incognitoProfile + in (p' :: Profile) {preferences = Just . toChatPrefs $ mergePreferences (userPreferences <$> ct) userPrefs} + +createRcvFeatureItems :: User -> Contact -> Contact -> CM' () +createRcvFeatureItems user ct ct' = + createFeatureItems user ct ct' CDDirectRcv CIRcvChatFeature CIRcvChatPreference contactPreference + +createSndFeatureItems :: User -> Contact -> Contact -> CM' () +createSndFeatureItems user ct ct' = + createFeatureItems user ct ct' CDDirectSnd CISndChatFeature CISndChatPreference getPref + where + getPref ContactUserPreference {userPreference} = case userPreference of + CUPContact {preference} -> preference + CUPUser {preference} -> preference + +type FeatureContent a d = ChatFeature -> a -> Maybe Int -> CIContent d + +createFeatureItems :: + MsgDirectionI d => + User -> + Contact -> + Contact -> + (Contact -> ChatDirection 'CTDirect d) -> + FeatureContent PrefEnabled d -> + FeatureContent FeatureAllowed d -> + (forall f. ContactUserPreference (FeaturePreference f) -> FeaturePreference f) -> + CM' () +createFeatureItems user ct ct' = createContactsFeatureItems user [(ct, ct')] + +createContactsFeatureItems :: + forall d. + MsgDirectionI d => + User -> + [(Contact, Contact)] -> + (Contact -> ChatDirection 'CTDirect d) -> + FeatureContent PrefEnabled d -> + FeatureContent FeatureAllowed d -> + (forall f. ContactUserPreference (FeaturePreference f) -> FeaturePreference f) -> + CM' () +createContactsFeatureItems user cts chatDir ciFeature ciOffer getPref = do + let dirsCIContents = map contactChangedFeatures cts + (errs, acis) <- partitionEithers <$> createInternalItemsForChats user Nothing dirsCIContents + unless (null errs) $ toView' $ CEvtChatErrors errs + toView' $ CEvtNewChatItems user acis + where + contactChangedFeatures :: (Contact, Contact) -> (ChatDirection 'CTDirect d, [CIContent d]) + contactChangedFeatures (Contact {mergedPreferences = cups}, ct'@Contact {mergedPreferences = cups'}) = do + let contents = mapMaybe (\(ACF f) -> featureCIContent_ f) allChatFeatures + (chatDir ct', contents) + where + featureCIContent_ :: forall f. FeatureI f => SChatFeature f -> Maybe (CIContent d) + featureCIContent_ f + | state /= state' = Just $ fContent ciFeature state' + | prefState /= prefState' = Just $ fContent ciOffer prefState' + | otherwise = Nothing + where + fContent :: FeatureContent a d -> (a, Maybe Int) -> CIContent d + fContent ci (s, param) = ci f' s param + f' = chatFeature f + state = featureState cup + state' = featureState cup' + prefState = preferenceState $ getPref cup + prefState' = preferenceState $ getPref cup' + cup = getContactUserPreference f cups + cup' = getContactUserPreference f cups' + +createGroupFeatureChangedItems :: MsgDirectionI d => User -> ChatDirection 'CTGroup d -> (GroupFeature -> GroupPreference -> Maybe Int -> Maybe GroupMemberRole -> CIContent d) -> GroupInfo -> GroupInfo -> CM () +createGroupFeatureChangedItems user cd ciContent GroupInfo {fullGroupPreferences = gps} GroupInfo {fullGroupPreferences = gps'} = + forM_ allGroupFeatures $ \(AGF f) -> do + let state = groupFeatureState $ getGroupPreference f gps + pref' = getGroupPreference f gps' + state'@(_, param', role') = groupFeatureState pref' + when (state /= state') $ + createInternalChatItem user cd (ciContent (toGroupFeature f) (toGroupPreference pref') param' role') Nothing + +sameGroupProfileInfo :: GroupProfile -> GroupProfile -> Bool +sameGroupProfileInfo p p' = p {groupPreferences = Nothing} == p' {groupPreferences = Nothing} + +createGroupFeatureItems :: MsgDirectionI d => User -> ChatDirection 'CTGroup d -> (GroupFeature -> GroupPreference -> Maybe Int -> Maybe GroupMemberRole -> CIContent d) -> GroupInfo -> CM () +createGroupFeatureItems user cd ciContent GroupInfo {fullGroupPreferences} = + forM_ allGroupFeatures $ \(AGF f) -> do + let p = getGroupPreference f fullGroupPreferences + (_, param, role) = groupFeatureState p + createInternalChatItem user cd (ciContent (toGroupFeature f) (toGroupPreference p) param role) Nothing + +createInternalChatItem :: (ChatTypeI c, MsgDirectionI d) => User -> ChatDirection c d -> CIContent d -> Maybe UTCTime -> CM () +createInternalChatItem user cd content itemTs_ = + lift (createInternalItemsForChats user itemTs_ [(cd, [content])]) >>= \case + [Right aci] -> toView $ CEvtNewChatItems user [aci] + [Left e] -> throwError e + rs -> throwChatError $ CEInternalError $ "createInternalChatItem: expected 1 result, got " <> show (length rs) + +createInternalItemsForChats :: + forall c d. + (ChatTypeI c, MsgDirectionI d) => + User -> + Maybe UTCTime -> + [(ChatDirection c d, [CIContent d])] -> + CM' [Either ChatError AChatItem] +createInternalItemsForChats user itemTs_ dirsCIContents = do + createdAt <- liftIO getCurrentTime + let itemTs = fromMaybe createdAt itemTs_ + void . withStoreBatch' $ \db -> map (uncurry $ updateChat db createdAt) dirsCIContents + withStoreBatch' $ \db -> concatMap (uncurry $ createACIs db itemTs createdAt) dirsCIContents + where + updateChat :: DB.Connection -> UTCTime -> ChatDirection c d -> [CIContent d] -> IO () + updateChat db createdAt cd contents + | any ciRequiresAttention contents || contactChatDeleted cd = updateChatTs db user cd createdAt + | otherwise = pure () + createACIs :: DB.Connection -> UTCTime -> UTCTime -> ChatDirection c d -> [CIContent d] -> [IO AChatItem] + createACIs db itemTs createdAt cd = map $ \content -> do + ciId <- createNewChatItemNoMsg db user cd content itemTs createdAt + let ci = mkChatItem cd ciId content Nothing Nothing Nothing Nothing Nothing False False itemTs Nothing createdAt + pure $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci + +createLocalChatItems :: + User -> + ChatDirection 'CTLocal 'MDSnd -> + NonEmpty (CIContent 'MDSnd, Maybe (CIFile 'MDSnd), Maybe CIForwardedFrom, (Text, Maybe MarkdownList)) -> + UTCTime -> + CM [ChatItem 'CTLocal 'MDSnd] +createLocalChatItems user cd itemsData createdAt = do + withStore' $ \db -> updateChatTs db user cd createdAt + (errs, items) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (createItem db) $ L.toList itemsData) + unless (null errs) $ toView $ CEvtChatErrors errs + pure items + where + createItem :: DB.Connection -> (CIContent 'MDSnd, Maybe (CIFile 'MDSnd), Maybe CIForwardedFrom, (Text, Maybe MarkdownList)) -> IO (ChatItem 'CTLocal 'MDSnd) + createItem db (content, ciFile, itemForwarded, ts) = do + ciId <- createNewChatItem_ db user cd Nothing Nothing Nothing content (Nothing, Nothing, Nothing, Nothing, Nothing) itemForwarded Nothing False False createdAt Nothing createdAt + forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt + pure $ mkChatItem_ cd ciId content ts ciFile Nothing Nothing itemForwarded Nothing False False createdAt Nothing createdAt + +withUser' :: (User -> CM ChatResponse) -> CM ChatResponse +withUser' action = + asks currentUser + >>= readTVarIO + >>= maybe (throwChatError CENoActiveUser) action + +withUser :: (User -> CM ChatResponse) -> CM ChatResponse +withUser action = withUser' $ \user -> + ifM (lift chatStarted) (action user) (throwChatError CEChatNotStarted) + +withUser_ :: CM ChatResponse -> CM ChatResponse +withUser_ = withUser . const + +withUserId' :: UserId -> (User -> CM ChatResponse) -> CM ChatResponse +withUserId' userId action = withUser' $ \user -> do + checkSameUser userId user + action user + +withUserId :: UserId -> (User -> CM ChatResponse) -> CM ChatResponse +withUserId userId action = withUser $ \user -> do + checkSameUser userId user + action user + +checkSameUser :: UserId -> User -> CM () +checkSameUser userId User {userId = activeUserId} = when (userId /= activeUserId) $ throwChatError (CEDifferentActiveUser userId activeUserId) + +chatStarted :: CM' Bool +chatStarted = fmap isJust . readTVarIO =<< asks agentAsync + +waitChatStartedAndActivated :: CM' () +waitChatStartedAndActivated = do + agentStarted <- asks agentAsync + chatActivated <- asks chatActivated + atomically $ do + started <- readTVar agentStarted + activated <- readTVar chatActivated + unless (isJust started && activated) retry + +chatVersionRange :: CM VersionRangeChat +chatVersionRange = lift chatVersionRange' +{-# INLINE chatVersionRange #-} + +chatVersionRange' :: CM' VersionRangeChat +chatVersionRange' = do + ChatConfig {chatVRange} <- asks config + pure chatVRange +{-# INLINE chatVersionRange' #-} + +adminContactReq :: ConnReqContact +adminContactReq = + either error id $ strDecode "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D" + +simplexTeamContactProfile :: Profile +simplexTeamContactProfile = + Profile + { displayName = "SimpleX Chat team", + fullName = "", + image = Just (ImageData "data:image/jpg;base64,/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8KCwkMEQ8SEhEPERATFhwXExQaFRARGCEYGhwdHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAETARMDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD7LooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiivP/iF4yFvv0rSpAZek0yn7v+yPeunC4WpiqihBf8A8rOc5w2UYZ4jEPTourfZDvH3jL7MW03SpR53SWUfw+w96veA/F0erRLY3zKl6owD2k/8Ar15EWLEljknqadDK8MqyxMUdTlWB5Br66WS0Hh/ZLfv1ufiNLj7Mo5m8ZJ3g9OTpy+Xn5/pofRdFcd4B8XR6tEthfMEvVHyk9JB/jXY18fiMPUw9R06i1P3PK80w2aYaOIw8rxf3p9n5hRRRWB6AUUVDe3UFlavc3MixxIMsxppNuyJnOMIuUnZIL26gsrV7m5kWOJBlmNeU+I/Gd9e6sk1hI8FvA2Y1z973NVPGnimfXLoxRFo7JD8if3vc1zefevr8syiNKPtKyvJ9Ox+F8Ycb1cdU+rYCTjTi/iWjk1+nbue3eEPEdtrtoMER3SD95Hn9R7Vu18+6bf3On3kd1aSmOVDkEd/Y17J4P8SW2vWY6R3aD97F/Ue1eVmmVPDP2lP4fyPtODeMoZrBYXFO1Zf+Tf8AB7r5o3qKKK8Q/QgooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAqavbTXmmz20Fw1vJIhVZB1FeDa3p15pWoSWl6hWQHr2YeoNfQlY3izw9Z6/YGGZQky8xSgcqf8K9jKcyWEnyzXuv8D4njLhZ51RVSi7VYLRdGu3k+z+88HzRuq1rWmXmkX8lnexFHU8Hsw9RVLNfcxlGcVKLumfgFahUozdOorSWjT6E0M0kMqyxOyOpyrKcEGvXPAPjCPVolsb9wl6owGPAkH+NeO5p8M0kMqyxOyOpyrA4INcWPy+njKfLLfoz2+HuIMTkmI9pT1i/ij0a/wA+zPpGiuM+H/jCPV4lsL91S+QfKTwJR/jXW3t1BZWslzcyLHFGMsxNfB4jC1aFX2U1r+fof0Rl2bYXMMKsVRl7vXy7p9rBfXVvZWr3NzKscSDLMTXjnjbxVPrtyYoiY7JD8if3vc0zxv4ruNeujFEWjsoz8if3vc1zOa+synKFh0qtVe9+X/BPxvjLjKWZSeEwjtSW7/m/4H5kmaM1HmlB54r3bH51YkzXo3wz8MXMc0es3ZeED/VR5wW9z7VB8O/BpnMerarEREDuhhb+L3Pt7V6cAAAAAAOgFfL5xmqs6FH5v9D9a4H4MlzQzHGq1tYR/KT/AEXzCiiivlj9hCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAxfFvh208QWBhmASdRmKUdVP+FeH63pl5pGoSWV5EUdTwezD1HtX0VWL4t8O2fiHTzBONk6g+TKByp/wr28pzZ4WXs6msH+B8NxdwhTzeDxGHVqy/8m8n59n954FmjNW9b0y80fUHsr2MpIp4PZh6iqWfevuYyjOKlF3TPwetQnRm6dRWktGmSwzSQyrLE7I6nKsDgg1teIPFOqa3a29vdy4jiUAheN7f3jWBmjNROhTnJTkrtbGtLF4ijSnRpzajPddHbuP3e9Lmo80ua0scth+a9E+HXgw3Hl6tqsZEX3oYmH3vc+1J8OPBZnKavq0eIhzDCw+9/tH29q9SAAAAGAOgr5bOM35b0KD16v8ARH6twXwXz8uPx0dN4xfXzf6IFAUAAAAdBRRRXyZ+wBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFB4GTXyj+1p+0ONJjufA3ga6DX7qU1DUY24gB4McZH8Xqe38tqFCdefLETaSufQ3h/4geEde8Uah4a0rWra51Ow/wBfCrD8ceuO+OldRX5I+GfEWseG/ENvr2j30ttqFvJ5iSqxyT3z6g96/RH9nD41aT8U9AWGcx2fiK1QC7tC33/+mieqn07V14zL3QXNHVEQnc9dooorzjQKKKKACiis7xHrel+HdGudY1m8is7K2QvLLI2AAP600m3ZAYfxUg8Pr4VutT1+7isYbSMuLp/4Pb3z6V8++HNd0zxDpq6hpVys8DHGRwVPoR2NeIftJ/G7VPifrbWVk8lp4btZD9mtwcGU/wDPR/c9h2rgfh34z1LwdrAurV2ktZCBcW5PyyD/AB9DX2WTyqYWny1Ho+nY+C4t4Wp5tF16CtVX/k3k/Ps/vPr/ADRmsjwx4g07xFpMWpaZOJInHI/iQ9wR61qbq+mVmro/D6tCdGbp1FZrdEma6/4XafpWoa7jUpV3oA0MLdJD/ntXG5p8E0kMqyxOyOhyrKcEGsMTRlWpShGVm+p1ZbiYYPFQr1IKai72fU+nFAUAKAAOABRXEfDnxpFrMK6fqDhL9BhSeko9frXb1+a4rDVMNUdOotT+k8szLD5lh44jDu8X968n5hRRRXOegFFFFABUGoXlvYWkl1dSrHFGMliaL+7t7C0kuruVYoYxlmNeI+OvFtx4huzHFuisYz+7jz97/aNenluW1MbU00it2fM8S8SUMkoXetR/DH9X5fmeteF/E+m+IFkFoxSWMnMb9cev0rbr5t0vULrTb6K8s5TFNGcgj+R9q9w8E+KbXxDYjlY7xB+9i/qPaurNsneE/eUtYfkeTwlxjHNV9XxVo1V90vTz8vmjoqKKK8I+8CiiigAooooAKKKKACiiigD5V/a8+P0mgvdeAvCUskepFdl9eDjyQR9xPfHeviiR3lkaSR2d2OWZjkk+tfoj+058CtP+Jektq2jxRWnie2T91KMKLlR/yzf+h7V+fOuaVqGiarcaXqtpLaXls5jlikXDKRX0mWSpOlaG/U56l76lKtPwtr+reGNetdb0S8ls761cPHJG2D9D6g9MVmUV6TSasyD9Jf2cfjXpPxR0MW9w0dp4gtkAubYnHmf7aeo/lXr1fkh4W1/V/DGuW2taHey2d9bOHjkjP6H1HtX6Jfs5fGvR/inoQgmeOz8RWqD7XaE439vMT1U+navnMfgHRfPD4fyN4Tvoz12iis7xJremeHdEutZ1i7jtLK1jLyyucAAf1rzUm3ZGgeJNb0vw7otzrOs3kVpZWyF5ZZDgAD+Z9q/PL9pP436r8UNZaxs2ks/Dlq5+z24ODMf77+p9B2o/aU+N2p/FDXDZ2LS2fhy1ci3t84Mx/wCej+/oO1eNV9DgMAqS55/F+RhOd9EFFFABJwBkmvUMzqPh34y1Lwjq63FszSWshAntyeHHt719Z2EstzpVlqD2txbR3kCzxLPGUbawyODXK/slfs8nUpbXx144tGFkhElhp8q4849pHB/h9B3r608X+GLDxBpX2WRFiljX9xIowUPYfT2rGnnkMPWVJ6x6vt/XU+P4o4SjmtN4igrVV/5N5Pz7P7z56zRmrmvaVe6LqMljexMkiHg9mHqKoZr6uEozipRd0z8Rq0J0ZunUVmtGmTwTSQTJNC7JIhyrKcEGvZvhz41j1mJdP1GRUv0GFY8CX/69eJZqSCaWCVZYXZHU5VlOCDXDmGXU8bT5ZaPo+x7WQZ9iMlxHtKesX8UejX+fZn1FRXDfDbxtHrUKadqDqmoIuAx4EoHf613NfnWKwtTC1HTqKzR/QGW5lh8yw8cRh3eL+9Ps/MKr6heW1hZyXd3KsUUYyzGjUby20+zku7yZYoY13MzGvDPHvi+48RXpjiZorCM/u4/73+0feuvLMsqY6pZaRW7/AK6nlcScR0MloXetR/DH9X5D/Hni648Q3nlxlo7GM/u48/e9zXL7qZmjNfodDDwoU1TpqyR+AY7G18dXlXryvJ/19w/dVvSdRutMvo7yzlaOVDkY7+xqkDmvTPhn4HMxj1jV4v3Y+aCFh97/AGjWGPxNHDUXKrt27+R15JlWLzHFxp4XSS1v/L53PQ/C+oXGqaJb3t1bNbyyLkoe/v8AQ1p0AAAAAADoBRX5nUkpSbirLsf0lh6c6dKMJy5mkrvv5hRRRUGwUUUUAFFFFABRRRQAV4d+038CdO+JWkyavo8cdp4mtkzHIBhbkD+B/f0Ne40VpSqypSUovUTV9GfkTruk6joer3Ok6taS2d7ayGOaGVdrKRVKv0T/AGnfgXp/xK0h9Y0iOO18TWqZikAwLkD+B/6Gvz51zStQ0TVbjS9UtZbW8tnKSxSLgqRX1GExccRG636o55RcSlWp4V1/VvDGvWut6JeSWl9bOGjkQ4/A+oPpWXRXU0mrMk/RP4LftDeFvF3ge41HxDfW+lappkG+/idsBwP40HfJ7V8o/tJ/G/VPifrbWVk8tn4btn/0e2zgykfxv6n0HavGwSM4JGeuO9JXFRwFKlUc18vIpzbVgoooAJIAGSa7SQr6x/ZM/Z4k1J7Xxz44tClkMSWFhIuDL3Ejg/w+g70fsmfs8NqMtt448c2eLJCJLCwlX/WnqHcH+H0HevtFFVECIoVVGAAMACvFx+PtenTfqzWEOrEjRI41jjUIigBVAwAPSnUUV4ZsYXjLwzZeJNOaCcBLhQfJmA5U/wCFeBa/pV7ompSWF9GUkToccMOxHtX01WF4z8M2XiXTTBOAk6AmGYDlD/hXvZPnEsHL2dTWD/A+K4r4UhmsHXoK1Zf+TeT8+z+8+c80Zq5r2k3ui6jJY30ZSRTwezD1FUM1+gQlGcVKLumfiFWjOjN06is1umTwTSQTJNE7JIh3KynBBr2PwL8QrO701odbnSC5t0yZCcCUD+teK5pd1cWPy2ljoctTdbPqetkme4rJ6rqUHdPdPZ/8Mdb4/wDGFz4ivDFGxisIz+7j/ve5rls1HuozXTQw1PD01TpqyR5+OxlfHV5V68ryf9fcSZozTAa9P+GHgQzmPWdZhIjHzQQMPvf7R9qxxuMpYOk6lR/8E6MpyfEZriFQoL1fRLux/wAMvApmMesazFiP70EDfxf7R9vavWFAUAAAAcACgAAAAAAdBRX5xjsdVxtXnn8l2P3/ACXJcNlGHVGivV9W/wCugUUUVxHrhRRRQAUUUUAFFFFABRRRQAUUUUAFeH/tOfArT/iXpUmsaSsVp4mto/3UuMLcgDhH/oe1e4Vn+I9a0zw7otzrGsXkVpZWyF5ZZGwAB/WtaNSdOalDcTSa1PyZ1zStQ0TVrnStVtZLS8tnMcsUgwVIqlXp/wC0l8S7T4nePn1aw0q3srO3XyYJBGBNOoPDSHv7DtXmFfXU5SlBOSszlYUUUVYAAScDk19Zfsmfs7vqLW3jjx1ZFLMESafYSjmXuJHHZfQd6+VtLvJtO1K2v7cRtLbyrKgkQOpKnIyp4I46Gv0b/Zv+NOjfFDw+lrIIrDX7RAtzZ8AMMffj9V9u1efmVSrCn7m3Vl00m9T16NEjjWONVRFGFUDAA9KWiivmToCiiigAooooAwfGnhiy8S6cYJwEuEH7mYDlT/hXz7r+k32h6lJYahFskQ8Hsw9QfSvpjUr2106ykvLyZYYYxlmY18+/EXxa/ijU1aOMRWkGRCCBuPuT/Svr+GK2KcnTSvT/ACfl/kfmPiBhMvUI1m7Vn0XVefp0fy9Oa3UbqZmjNfa2PynlJM+9AOajzTo5GjkV0YqynIPoaVg5T1P4XeA/P8vWdaiIj+9BAw+9/tH29q9dAAAAAAHQVwPwx8dQ63Ammai6R6hGuFJ4Ew9vf2rvq/Ms5qYmeJaxGjWy6W8j+gOFcPl9LAReBd0931b8+3oFFFFeSfSBRRRQAUUUUAFFFFABRRRQAUUUUAFFFZ3iTW9L8OaJdazrN5HaWNqheWWQ4AH+NNJt2QB4l1vTPDmiXWs6xdx2llaxl5ZHOAAO3ufavzx/aT+N2qfFDWzZWbSWfhy2ci3tg2DKf77+p9B2pf2lfjdqfxQ1trGxeW08N2z/AOj2+cGYj/lo/v6DtXjVfQ4DAKkuefxfkYTnfRBRRQAScAZNeoZhRXv3w2/Zh8V+Lfh7deJprgadcvHv02zlT5rgdcsf4Qe1eHa5pWoaJq1zpWq2ktpeW0hjlikXDKwrOFanUk4xd2htNFKtTwrr+reGNdtta0S8ltL22cPHIhx07H1HtWXRWjSasxH6S/s4/GrSfijoYtp3jtfENqg+1WpON4/vp6j27V69X5IeFfEGr+F9etdc0O9ks7+1cPHKh/QjuD3Ffoj+zl8bNI+KWhLbztFZ+IraMfa7TON+Osieqn07V85j8A6L54fD+RvCd9GevUUUV5hoFVtTvrXTbGW9vJligiXczNRqd9aabYy3t7MsMEQyzMa+ffiN42uvE96YoS0OmxH91F3b/ab3r1spympmFSy0it3+i8z57iDiCjlFG71qPZfq/Id8RPGl14lvTFEzRafGf3cf97/aNclmmZozX6Xh8NTw1NU6askfheNxdbG1pV68ryY/NGTTM16R4J+GVxrGkSX+pSSWfmJ/oq45J7MR6Vni8ZRwkOes7I1y7K8TmNX2WHjd7/0zzvJozV3xDpF7oepyWF/EUkQ8HHDD1FZ+feuiEozipRd0zjq0Z0puE1ZrdE0E8sEyTQu0ciHKspwQa9z+GHjuLXIU0zUpFTUEXCseBKB/WvBs1JBPLBMk0LmORCGVlOCDXn5lllLH0uWWjWz7HsZFnlfJ6/tKesXuu6/z7M+tKK4D4X+PItdhTTNSdY9SQYVicCYDuPf2rv6/M8XhKuEqulVVmj92y7MaGYUFXoO6f4Ps/MKKKK5juCiiigAooooAKKKKACiig9KAM7xLrmleG9EudZ1q8jtLG2QvLK5wAPQep9q/PH9pP43ap8T9beyspJbTw3bSH7NbZx5pH8b+p9u1bH7YPxL8XeJPG114V1G0udH0jT5SIrNuDOR0kbs2e3pXgdfRZfgVTSqT3/IwnO+iCiigAkgAZJr1DMK+s/2TP2d31Brbxz46tNtmMSafp8i8y9/MkB6L0wO9J+yb+zwdSe28b+ObLFmpEljYSr/rT1DuP7voO9faCKqIERQqqMAAYAFeLj8fa9Om/VmsIdWEaJGixooVFGFUDAA9K8Q/ac+BWnfErSZNY0mOO08T2yZilAwtyAPuP/Q9q9worx6VWVKSlF6mrSasfkTrmlahomrXOlaray2l7bSGOaKRcMrCqVfon+098C7D4l6U+s6Skdr4mtY/3UmMC5UdI29/Q1+fOt6XqGi6rcaVqlrJa3ls5SWKQYKkV9RhMXHERut+qOeUeUpVqeFfEGreGNdttb0W7ktb22cNG6HH4H1FZdFdTSasyT9Jf2cPjVpXxR0Fbe4eK18Q2qD7Va7sbx/z0T1H8q9V1O+tdNsZb29mWGCJdzMxr8ovAOoeIdK8W2GoeF5podVhlDQtEefcH2PevsbxP4417xTp1jDq3lQGKFPOigJ2NLj5m59849K4KHD0sTX9x2h18vJHj55xDSyqhd61Hsv1fkaXxG8bXXie9MURaLTo2/dR5+9/tH3rkM1HmjNffYfC08NTVOmrJH4ljMXWxtaVau7yZJmgHmmAmvWfhN8PTceVrmuQkRDDW9uw+9/tN7Vjj8dSwNJ1ar9F3OjK8pr5nXVGivV9Eu7H/Cf4emcx63rkJEfDW9u4+9/tMPT2r2RQFAVQABwAKAAAAAAB0Aor8uzDMKuOq+0qfJdj9zyjKMPlVBUaK9X1bOf8b+FbHxRppt7gCO4UfuZwOUP9R7V86+IdHv8AQtTk0/UIikqHg9mHqD6V9VVz3jnwrY+KNMNvcKEuEBME2OUP+FenkmdywUvZVdab/A8PijheGZw9vQVqq/8AJvJ+fZnzLuo3Ve8Q6Pf6FqclhqERjkQ8Hsw9Qazs1+jwlGpFSi7pn4xVozpTcJqzW6J7eeSCZJoZGjkQhlZTgg17t8LvHsWuQppmpOseooMKxPEw/wAa8DzV3Q7fULvVIIdLWQ3ZcGMx8EH1z2rzs1y2jjaLVTRrZ9v+AezkGcYnK8SpUVzKWjj3/wCD2PrCiqOgx38Oj20eqTJNeLGBK6jAJq9X5VOPLJq9z98pyc4KTVr9H0CiiipLCiiigAooooAKKKKAPK/2hfg3o/xT8PFdsVprlupNnebec/3W9VNfnR4y8Naz4R8RXWg69ZvaXts5V1YcEdmB7g9jX6115V+0P8GtF+Knh05SO0161UmzvQuD/uP6qf0r08DjnRfJP4fyM5wvqj80RycCvrP9kz9ndtRNr458dWTLaAiTT9PlXBl9JJB/d7gd+tXv2bv2Y7yz19vEHxFs1VbKYi1sCQwlZTw7f7PcDvX2CiLGioihVUYAAwAK6cfmGns6T9WTCHVhGiRoqRqFRRgKBgAUtFFeGbBRRRQAV4h+038CtP8AiZpTatpCQ2fia2jPlS4wtyo52P8A0Pavb6K0pVZUpKUXqJq+jPyJ1zStQ0TVrnStVtJbS9tnMcsUgwVIqPS7C61O+isrKFpZ5W2qor9AP2r/AIM6J448OzeJLV7fTtesoyRO3yrcqP4H9/Q14F8OvBlp4XsvMkCTajKP3suM7f8AZX0H86+1yiDzFcy0S3Pms+zqllNLXWb2X6vyH/DnwZaeF7EPIEm1CUDzZcfd/wBke1dfmo80ua+0pUY0oqMVofjWLxNXF1XWrO8mSZozUea9N+B/hTTdau5NUv5opvsrjbak8k9mYelc+OxcMHQlWqbI1y3LqmYYmOHpbvuafwj+HhnMWva5DiMENb27D73ozD09q9oAAAAAAHQCkUBVCqAAOABS1+U5jmNXH1XUqfJdj9yyjKKGV0FRor1fVsKKKK4D1AooooA57xz4UsPFOmG3uFEdwgJgnA5Q/wBR7V84eI9Gv9A1SXT9RhMcqHg/wuOxB7ivrCud8d+E7DxTpZt51CXKDMEwHKn/AAr6LI88lgpeyq603+Hmv1Pj+J+GIZnB16KtVX/k3k/Psz5p0uxu9Tv4rGxheaeVtqIoyTX0T8OPBNp4XsRJKFm1GQfvZf7v+yvtR8OfBFn4UtDIxW41CUfvJsdB/dX0FdfWue568W3RoP3Pz/4BhwvwtHL0sTiVeq9l/L/wQooor5g+3CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKrarf2ml2E19fTpBbwrud2OAKTVdQtNLsJb6+mWGCJcszGvm34nePLzxXfmGEtDpkTfuos/f/wBpvevZyfJ6uZVbLSC3f6LzPBz3PaOVUbvWb2X6vyH/ABM8d3fiq/MULPDpsR/dRdN3+03vXF5pm6jdX6phsLTw1JUqSskfjGLxVbGVnWrO8mSZ96M0wGnSq8UhjkRkdeCrDBFb2OXlFzWn4b1y/wBA1SPUNPmMciHkdmHoR6Vk7hS596ipTjUi4zV0y6c50pqcHZrZn1X4C8W2HizShc27BLmMATwZ5Q/4V0dfIfhvXL/w/qseo6dMY5U6js47gj0r6Y8BeLtP8WaUtzbER3KAefATyh/qPevzPPshlgJe1pa03+Hk/wBGfr/DfEkcygqNbSqv/JvNefdHSUUUV80fWhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFVtVv7TS7CW+vp1ht4l3O7HpSatqNnpWny319OsMES7mZjXzP8UfH154tv8AyYWeDS4WPlQ5xvP95vU/yr2smyarmVWy0gt3+i8zws8zylldK71m9l+r8h/xP8eXfiy/MUJaHTIm/cxZ5b/ab3ris0zNGa/V8NhaWFpKlSVkj8bxeKrYuq61Z3kx+aX2pmTXsnwc+GrXBh8Qa/CViB3W9sw5b0Zh6e1YZhj6OAourVfourfY3y3LK+Y11Ror1fRLux3wc+GxuPK1/X4SIgQ1tbuPvf7TD09BXT/Fv4dQ6/bPqukxpFqca5KgYE4Hb6+9ekKAqhVAAHAApa/L62fYupi1ilKzWy6W7f5n63R4bwVPBPBuN0931v3/AMj4wuIZred4J42jlQlWVhgg0zNfRHxc+HUXiCB9W0mNI9TRcso4EwH9a+eLiKW2neCeNo5UO1kYYIPpX6TlOa0cypc8NJLddv8AgH5XnOS1srrck9YvZ9/+CJmtPw1rl/4f1WLUdPmMcqHkZ4Yeh9qys0Zr0qlONSLhNXTPKpznSmpwdmtmfWHgDxfp/i3SVubZhHcoAJ4CfmQ/1HvXSV8feGdd1Dw9q0WpabMY5UPIz8rr3UjuK+nPAHjDT/FulLcW7CO6QYngJ5Q/1FfmGfZBLAS9rS1pv8PJ/oz9c4c4jjmMFRraVV/5N5rz7o6WiiivmT6wKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAOY+JXhRfFvh5rAXDwTod8LA/KW9GHcV8s65pV/oupzadqNu0FxC2GVu/uPUV9m1x/xM8DWHi/TD8qw6jEP3E4HP+6fUV9Tw7n7wEvY1v4b/AAf+Xc+S4k4eWYR9vR/iL8V29ex8q5o+gq9ruk32i6nLp2oQNFPG2CCOvuPUV6v8Gvhk1w0PiDxDBiH71tbOPvejMPT2r9Cx2Z4fB4f283o9rdfQ/OMBlWIxuI+rwjZre/T1F+DPw0NwYfEPiCDEQ+a2tnH3vRmHp6Cvc1AVQqgADgAUKoVQqgAAYAHalr8lzPMq2Y1nVqv0XRI/YsryuhltBUqS9X1bCiiivOPSCvNfi98OYvEVu+raTEseqRrllHAnHoff3r0qiuvBY2tgqyq0nZr8fJnHjsDRx1F0ayun+Hmj4ruIZbad4J42ilQlWRhgg1Hmvoz4vfDiLxDA+raRGseqRjLIOBOP8a8AsdI1K91hdIgtJDetJ5ZiK4Knvn0xX6zleb0Mwoe1Ts1uu3/A8z8dzbJK+XYj2TV0/hff/g+Q3SbC81XUIbCwgee4mYKiKOpr6a+F3ga28IaaWkYTajOo8+Tsv+yvtTPhd4DtPCWnCWULNqcq/vZcfd/2V9q7avh+IeIHjG6FB/u1u+//AAD73hrhuOBSxGIV6j2X8v8AwQooor5M+xCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAxdd8LaHrd/a32pWKTT2rbo2Pf2PqK2VAVQqgAAYAHalorSVWc4qMm2lt5GcKNOEnKMUm9/MKKKKzNAooooAKKKKACs+HRdLh1iXV4rKFb6VQrzBfmIrQoqozlG/K7XJlCMrOSvYKKKKkoKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//2Q=="), + contactLink = Just $ CLFull adminContactReq, + preferences = Nothing + } + +simplexStatusContactProfile :: Profile +simplexStatusContactProfile = + Profile + { displayName = "SimpleX-Status", + fullName = "", + image = Just (ImageData "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAr6ADAAQAAAABAAAArwAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgArwCvAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQAC//aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Q/v4ooooAKKKKACiiigAoorE8R+ItF8J6Jc+IvEVwlrZ2iGSWWQ4CgVUISlJRirtmdatTo05VaslGMU223ZJLVtvokbdFfl3of/BRbS734rtpup2Ig8LSsIYrjnzkOcea3bafTqBX6cafqFjq1jFqemSrPbzqHjkQ5VlPIINetm2Q43LXD65T5eZXX+XquqPiuC/Efh/itYh5HiVUdGTjJWaflJJ6uEvsy2fqXKKKK8c+5Ciq17e2mnWkl/fyLDDCpd3c4VVHJJJr8c/2kf8Ago34q8M3mpTfByG3fT7CGSJZrlC3nStwJF5GFU8gd69LA5VicXTrVaMfdpxcpPokk397toj4LjvxKyLhGjRqZxValVkowhFc05O9m0tPdjfV7dN2kfq346+J3w9+GWlPrXxA1m00i1QZL3Uqxj8Mnn8K/Mj4tf8ABYD4DeEJ5dM+Gmn3niq4TIE0YEFtn/ffBI+imv51vHfxA8b/ABR1+bxT8RNUuNXvp3LtJcOWCk84VeigdgBXI18LXzupLSkrL72fzrxH9IXNsTKVPKKMaMOkpe/P8fdXpaXqfqvrf/BYH9p6+1w3+iafo1jZA8WrRPKSPeTcpz9BX1l8J/8Ags34PvxDp/xn8M3OmSnAe709hcQfUoSHA/A1/PtSE4/GuKGZ4mLvz39T4TL/ABe4swlZ1ljpTvvGaUo/dbT/ALdsf2rfCX9pT4HfHGzF18M/EdnqTYBaFXCzJn+9G2GH5V7nX8IOm6hqGkX8eraLcy2d3EcpPbuY5FPsykGv6gf+CWf7QPxB+OPwX1Ky+JF22pX3h69+yJdyf62WJlDrvPdlzjPevdwGae3l7OcbP8D+i/DTxm/1ixkcqx2H5K7TalF3jLlV2rPWLtqtWvM/T2iiivYP3c//0f7+KKKKACiiigAooooAK/Fv/goX8Qvi2fFcXgfWrRtP8NDEls0bZS7YfxORxlT0Xt1r9pK8u+L/AMI/Cfxp8F3HgvxbFujlGYpgB5kMg6Op9R+tfR8K5vQy3MYYnE01KK0843+0vNf8NZn5f4wcFZhxTwziMpy3FOjVeqSdo1Lf8u5u11GXk97Xuro/mBFyDX3t+yL+2Be/CW+h8B+OHafw7cyALIxJa0Ldx6p6jt1FfMvx/wDgR4w/Z+8YN4d8RoZrSbLWd4owk6D+TDuK8KF0K/pLFYHA51geWVp0pq6a/Brs1/wH2P8ALvJsz4h4D4h9tR5qGLoS5ZRls11jJbSjJferSi9mf1uafqFlqtlFqWmyrPBOoeORDlWU8gg069vrPTbSS/v5FhghUu7ucKqjqSa/CH9j79sm++EuoQ/D/wAeSNceHbmRVjlZstZk9x6p6jt2q3+15+2fffFS8n8AfD2V7bw9CxWWZThrwj+Se3evxB+G2Zf2n9TX8Lf2nTl/+S/u/PbU/v2P0nuGv9Vf7cf+9/D9Xv73tLd/+ffXn7afF7pqftbfth3nxUu5vAXgGR7fw/A5WWUHDXZX19E9B361+Z/xKm3eCL9R3UfzFbQul6Cn+I/A3ivxR8LPEXivSbVn07RoVkurg8Iu5gAue7HPSv1HOsrwmVcN4uhRSjBUp6vq3Fq7fVt/5I/gTNeI884x4kjmeYOVWtKSdop2hCPvWjFbQjFNv5ybbuz4Toqa0ge9uoLOIhWnkSNSxwAXIUEnsBnmv0+/aK/4Jg+O/gj8Hoviz4b1n/hJFt40l1G2ig2NDG4yZEIJ3KvfgHHNfxVTw9SpGUoK6W5+xZVw1mWZYfEYrA0XOFBKU2raJ31te72b0T0R+XRIAyegr+gr/glx+yZoHhjwBc/tKfFywiafUY2OmpeIGS3sVGWmIbgF+TkjhR71+YP7DX7Lt9+1H8ZLfR75WTw5pBS61ScDKsoIKwg+snf0Ffqd/wAFSv2o4Phf4Ltv2WvhmVtrjUbRBfvA2Ps1kOFhAHQyAc9ML9a9HL6UacHi6q0W3mz9Q8M8owuV4KvxpnEL0aN40Yv/AJeVXpp5LZPo7v7J+M/7U/jX4e/EL4/+JfFXwrsI9P0Ke5K26RKESTZw0oUcAOeQBX7J/wDBFU5+HPjYf9RWH/0SK/nqACgKOgr+hT/giouPh143b11SH/0SKWVzc8YpPrf8jHwexk8XxzSxVRJSn7WTSVknKMnoui7H7a0UUV9cf3Mf/9L+/iiiigAoorzX4wfGD4afAP4bav8AF74v6xbaD4d0K3e6vb26cJHHGgyevUnoAOSeBTjFyajFXYHpVFf55Xxt/wCDu34nj9vzS/G3wX0Qz/ArQ2ksLnSp1CXurQyMA15uPMTqBmJD2+914/uU/Y//AGxfgH+3P8ENL+P37OutxazoWpoNwHyzW02PmhmjPKSKeCD9RxXqY/JcXg4QqV4WUvw8n2ZnCrGTaTPqGiiivKNDy/4u/CLwd8afBtx4N8ZW4kilBMUoH7yGTs6HsR+tfzjftA/AXxl+z54yfw34jQzWkuXs7xF/dzR/0YdxX9OPiDxBofhPQ7vxN4mu4rDT7CF57m4ncJHFFGMszMcAAAZJNf53n/Bav/g5W1H4ufGjTvg5+xB5F14E8JX4l1HVriIE6xNE2GjhLDKQdRuGC55HHX9L8Os+x2ExP1eKcsO/iX8vmvPy6/ifg3jZ4NYDjDBPFUEqeYU17k/50vsT8n0lvF+V0fq0LhTUgnA4r4y/ZG/bJ+FX7YXw9HjDwBP5N/ahV1LTZeJrSUjoR3U/wsOK+sRdL/n/APXX9G0nCrBTpu6Z/mVmuSYvLcXUwOPpOnWg7SjJWaf9ap7NarQ+pf2dP2evGH7Q3i4aLogNvp1uQ15esMpEnoPVj2Ffrd+1V8GvDnw5/YU8X+APh/Z7IrewEjYGXlZGUs7nqSQM18C/sO/ti6b8F7o/Dnx6qpoN9LvS6RRvglbjL45ZT69vpX7wX1poHjjwxNYzbL3TdUt2jbaQySRSrg4PoQa/nnxXxGaTxLwmIjy4e3uW2lpu33Xbp87v+7Po58I8L4nhfFVMuqKeY1oTp1nJe9S5k0oxWtoPfmXxve1uVfwqKA0YHYiv6Ev+CZ37bVv490eP9mb4zXAn1GKJo9Murg5F3bgYMLk9XUcD+8tflR+1/wDsn+Nv2XfiNdadqFs8vh28md9Mv1GY3iJyEY9nXoQa+UrC/v8ASr+DVdJnktbq2dZYZomKvG6nIZSOhFfztQrVMJW1Xqu5+Z8PZ5mvBWeSc4NSg+WrTeinHqv1jL56ptP+s7xHZ/A//gnR8EfE/jTwra+RHqF5JdxWpbLTXcwwkSnrsGPwXNfyrfEDx54l+J/jXU/iB4wna51LVZ3nmdj3Y8KPQKOAPQV2vxX/AGhvjT8corC3+K2vz6vFpq7beNgERT3YqvBY92NeNVeOxirNRpq0Fsju8RePKWfTo4TLqPscFRXuU9F7z+KTSuvJK7srvqwr+ir/AIIuaVd2/wAH/FesSIRDd6uFjb+8Y41Dfka/BX4YfCzx78ZfGVr4C+G+nyajqV22Aqj5I17u7dFUdya/r+/ZV+Aenfs2fBLSPhbZyC4ntVaW7nAx5tzKd0jfTJwPYV1ZLQk63tbaI+w8AOHcXiM8ebcjVClGS5ujlJWUV3sm27baX3R9FUUUV9Uf2gf/0/7+KKKKACv4If8Ag8QT9vN9W8IsVk/4Z+WJedOL7f7Xyd32/HGNu3yc/LnPev73q84+Lnwj+G/x3+HGr/CT4uaRba74d123e1vbK6QPHJG4weD0I6gjkHkV6WUY9YLFQxDgpJdP8vMipDmi0f4W1frt/wAEhP8Agrt8af8AglD8b38V+Fo21zwPr7xp4i0B3KpcRoeJoTyEnjBO04+boeK+m/8AguZ/wQz+I3/BMD4kyfEn4Ww3fiD4Oa5KzWWolC76XKx4tbphwOuI3PDAc81/PdX7LCeFzHC3VpU5f18mjympU5eZ/t9fsk/tb/Av9tv4G6N+0F+z3rUWs6BrEQYFCPNt5cfPDMnVJEPDKf5V794h8Q6F4T0O78TeJ7uGw06wiae4uZ3EcUUaDLMzHAAA6k1/j9f8EiP+Cunxv/4JTfHAeKPCZfWfAuuyRx+IvD8jkRTxg486Lsk8YJ2n+Loa/V7/AILy/wDBxZd/t2eHl/Zc/Y6mu9I+Gl1DDNrWoSBoLvUpGAY2+OqQoeH/AL5GOlfneI4OxCxio0taT+12Xn59u53xxMeW73ND/g4M/wCDgzVP2yNV1H9jz9j3UZrD4ZWE7waxrEDlH110ONiEYItgQe/7z6V/I6AAMDgCgAKNo6Cv0j/4Jkf8Ex/j/wD8FOvj/Y/Cj4UWE9voFvNGdf18xk2um2pPzEt0MhGdiZyTX6FhsNhctwvLH3YR1bfXzfn/AEjhlKVSR77/AMEMf2Rf2v8A9qr9tPRrb9mNpdL0fSp438UaxKjNYW+nk/PHKOA7uoIjTrnniv7Lfj98CvG37PPjiXwj4uiLxNl7S7UYjuIuzD39R1Ffvt+wn+wd+z5/wTy+A+n/AAF/Z70pbKyt1V728cA3V/c4w0079WYnoOijgV7V8cPgb4G+Pngqfwb41twwYEwXCgebBJ2ZT/MdDXi5N4mTwmYWqRvhXpb7S/vL9V28z8c8YfBXC8XYL61hbQx9Ne7LpNfyT8v5ZfZfkfyXi5r9Lf2Jv24bn4S3UHwz+JkzT+HZ5AsNy5LNZlu3vHn8q+KPj38CPHf7PPjabwn4yt2ELMxtLsD91cRg8Mp6Z9R2rxAXAPANfuePyzL89y/2c7TpTV1JdOzT6Nf8Bn8C5FnGfcEZ79Yw96OJpPlnCS0a6xkusX/k4u9mf2IeK/B/w++Mngt9C8U2ltrWi6lEGCuA6OrDhlPY+hHNfztftw/8E4tN+AGlTfE34ba3HJo0koVdMvGC3CFv4Ym/5aAenBArvf2PP2+9R+CGmv4B+JSy6joEUbtaOp3TQOBkRj1Rjx7V8uftEftH+Nf2i/G7+KPEzmG0hyllZqT5cEef1Y9zX4LT8GMTisynhsY7UI6qot5J7Jefe+i87o/prxI8YuEM/wCF6WM+rc2ZSXKo6qVJrdykvih/Ktebsmnb4DkilicxyqVYdQRzXUaN4R1HVMSzjyIf7zDk/QV6dIlpJIJ5Y1Z16MRk1+qf7DX7Ed58ULmH4p/Fe2kt/D8Dq9paSDabwjncf+mf/oX0rKXg3lOR+0zDPMW6lCL92EVyufZN3vfyjbvdI/AeFsJnHFOPp5TktD97L4pP4YLrJu2iXnq3ok20es/8Erv2f/G/gf8AtD4ozj7Bo2pwiFIpY/3t2VOQ4J5VFzx659q/aKq9paWthax2VlGsUMShERBtVVHAAA6AVYr4LNcdTxWIdSjRjSpqyjGKslFber7t6tn+k3APB1LhjJaOUUqsqjjdylJ/FKTvJpfZV9orbzd2yiiivNPsj//U/v4ooooAKKKKAPO/iz8Jvh18c/h1q/wm+LGk2+ueHtdt3tb2yukDxyxuMEEHoR1B6g81/lm/8Fy/+CFfxG/4Jh/ENvid8J4bzxF8Htdmke1vliaRtHctxbXTAEBecRyHAbGDzX+q54j8R6B4Q0C88U+KbyHT9N0+F7i5ubhxHFFFGMszMcAADqa/zM/+Dhb/AIL06p+3f4rvP2Tf2Xr6S0+Eui3DR397GcHXriM8N7W6EfIP4jz6V9fwfPGLFctD+H9q+3/D9jmxKjy+9ufyq0UAY4or9ZPMP0v/AOCX3/BLf9oT/gqP8d4Phf8ACa0lsvDtjLG3iDxDJGTa6bbse56NKwB8uPOSfav9ZX9hD9hT4Df8E8v2fdK/Z7+AenLbWNkoe8vHUfab+6I+eeZhyWY9B0UcCv8AKC/4JUf8FV/j1/wSu+PCfEf4aSHUvC+rPHH4i0CViIL63U43D+7MgJKN+B4r/Wd/Yy/bM+BH7eHwH0j9oL9n7Vo9S0fU4182LI8+0nx88MydVdTxz16ivzbjZ43nipfwOlu/n59uh6GE5Labn1ZRRRXwB2Hi3x3+BPgj9oHwJceCPGcIIYFre4UfvYJezKf5jvX8vH7QvwB8d/s4eOZfB/jKEtDIS9neKP3VxFngqfX1Hav6gvj58e/An7PHgK48ceN7gLtBW2twf3txL2RR/M9hX8rX7Qn7Rnjz9o3x5L418ZyhUXKWlqh/dW8WeFUevqe5r988G4Zu3Ut/ueu/839z/wBu6fM/jj6UdPhlwo8y/wCFTS3Lb+H/ANPf/bPtf9unlQuAec077SPWueFznrTxc1+/eyP4udE/XX9g79h24+K8tv8AF74qQvD4fgkDWdo64N4V53H/AKZg/wDfX0r+ge0tLWwtY7KyjWKGJQiIgwqqOAAOwFfzc/sIft2XnwO1KH4ZfEeVp/Ct5L8k7Es9k7YHH/TMnkjt1r+kDTNT07WtOg1fSJ0ubW5QSRSxncjowyCCOoNfyr4q0s3jmreYfwtfZW+Hl/8Akv5r6/Kx/or9HSXDX+rqhkqtidPb81vac/d/3P5Lab/auXqKKK/Lz+gwooooA//V/v4ooooAKxfEniTQPB2gXnirxVew6dpunQvcXV1cOI4oYoxlndjgAADJJrar/PV/4Ozf+CiX7Xlr8Yrf9hCx0u98GfDaS0iv5L1GZT4iZs5HmKceTERgx9d3LcYr08py2eOxMaEXbu/L9SKk1CN2fIX/AAcD/wDBfrXv27vFF1+yx+ylqFzpnwl0id476+icxSa/MhwGOMEWykHYv8fU9hX8qoAAwOAKUAAYFfqj/wAEnf8AglH8cv8Agqp8ek+Hvw/R9M8I6NJFJ4k19lzHZW7k/ImeGmcAhF/E8V+xUKGFyzC2Xuwju/1fds8tuVSXmM/4JQ/8Epfjr/wVU+Pcfw5+HiPpXhPSXjl8ReIZEJhsoGP3E7PO4B2J+J4r7o/4Li/8EC/H3/BL/UYPjH8Hp7vxV8JNQMcL3sy7rnTLkgDbcFRjZI3KPwATg9q/0rP2MP2MPgL+wZ8BdI/Z5/Z60hNM0bS4x5kpANxeTn7887gAvI55JPToOK9y+J/ww8AfGfwBqvwu+KOlW+t6Brdu9re2V0gkilicYIIP6HqDXwVbjSu8YqlNfulpy9139e3Y7VhY8tnuf4VdfqD/AMErP+Cpvx1/4Jb/ALQNn8S/h7cS6j4VvpUj8QeH2kIt723zgsB0WVRyjetffn/BeH/ghJ4x/wCCZvjlvjP8EYbvXPg5rk7GKcqZJdGmc5FvOwH+rOcRyH0wea/nCr9ApVcNmOGuvehL+vk0cLUqcvM/24v2Mf20PgH+3l8CdK/aA/Z61iPVNI1FF86LI+0Wc+PnhnTqjqeOevUcV3nx/wD2gfh/+zp4CuPHHjq5CBQVtrZT+9uJeyIP5noBX+Ud/wAEL/25f2t/2NP2u7A/s7xPrPhzW5Yk8T6LOzCyls1PzTE9I5UXJRupPHIr+p39o79pXx/+0v8AEGbxv42l2RrlLO0QnyreLPCqPX1PUmvM4b8KauYZg5VJWwkdW/tP+6vPu+i8z8r8VvF3D8L4P6vhbTx017sekF/PL/21fafkjV/aF/aN8e/tHePZ/GvjOc+XuK2lopPlW8WeFUevqe9eFfasDmsL7UB1r9kv+Cen/BPuX4mPa/Gv41Wrw6HE4k0/T5FwbsjkO4PPl56D+L6V/QWbZjlnDmW+1q2hSgrRit2+kYrq/wDh2fw9kXDmdcZ526NK9SvUfNOctorrKT6JdF6JIh/Yq/4JyXXxq8MSfEn4wtPpukXkLLp1vH8s0hYcTHPRR1Ud6+KP2nP2bvHX7MXj+Twl4pUz2U+Xsb5QRHcRZ/Rh/Etf2D2trbWNtHZ2caxRRKEREGFVRwAAOgFeSfHL4G+Af2gvAVz4A8f2wmt5huimUDzYJB0dD2I/Wv5/yrxgx0c3niMcr4abtyL7C6OPdrr/ADeWlv604g+jdlFTh6ngsrfLjaauqj/5eS6xn2i/s2+Hz1v/ABi+d3r9O/2DP28r/wCBGpRfDT4lSvdeFL2UBJmYs9izcZX1j7kduor48/ah/Zr8bfsu/EWTwZ4pHn2c4MtheqMJcQ5IB9mHRhXzd9oAFf0Djsuy3iHLeSdqlGorpr8Gn0a/4DW6P5DyrMc74Mzz2tG9LE0XaUXs11jJdYv/ACaezP7pdK1bTNd02DWdGnS6tLlBJFLEwZHRuQQR1FaFfix/wSG1n47X3hPVLHXUL+BoT/oEtxneLjPzLD6pjr2B6d6/aev424nyP+yMyrZf7RT5Huvv17NdV0Z/pTwPxP8A6w5Lh82dGVJ1FrGXdaNp9YveL6oKKKK8A+sP/9b+/iiiigAr4E/4KI/8E4f2b/8AgpZ8DLr4M/H7SklljV5NJ1aJQLzTblhxLC/Uc43L0YcGvvuitKNadKaqU3aS2Ymk1Zn+Vt8Nf+DZH9vDxJ/wUEn/AGQfGti+m+DdMkF5eeNlTNjLpRb5Xgz964cfL5XVWyTx1/0lv2L/ANif9nv9gn4H6b8Bv2dNDh0jSrFF8+YKDcXs4GGmuJOskjHPJ6dBxX1lgZz3pa9bNc+xWPjGFV2iui6vu/60M6dKMNgooorxTU4T4m/DHwB8ZfAeqfDH4paRba7oGtQPbXtjeRiWGaJxghlII/wr/M//AOCw/wDwbq/En9kb9o7Ttc/ZhQ6h8KvGl4VgknkUyaJIxy0UmTueMDmNgCexr/SN/aA/aA+Hf7N3w6u/iL8RbtYYIFIggBHm3Ev8Mca9yfyA5NfyB/tTftZfEX9qv4gSeL/GEv2exgLJYWEZPlW8WeOO7H+Ju9fsXhRwnmOZYl4hNwwi+Jv7T/lj5930Xnofj3iv4nYThrCPD0bTxs17kekV/PPy7L7T8rn58fs1fs1/Df8AZg8Dp4U8CwB7qYK19fuAZrmQDkseyjsvQV9GfaWrAWcjvUnnt6mv62w+Cp0KapUo2itkfwFmOLxWPxNTGYyo51Zu8pN6t/1stktEftx/wTa/YHsfi6sHx2+L8aT6BFJnT7DcGFy6dWlAzhQf4T171/SBaWltY20dlZRrFDEoREQYVVHAAA6AV/Hv+xJ+3N4y/ZO8Wi0ui+oeE9QkX7dYk5KdjLFzw49Ohr+tj4c/Efwb8WPB1l498A30eoaZqEYkiljOevVWHZh0IPIr+TPGXLs6p5p9Zxz5sO9KbXwxX8rXSXd/a3Wmi/t76P8AmHD08l+qZZDkxUdaydueT/mT0vDsl8Oz1d33FFFFfjR/QB4x8dPgN8O/2hvA1x4F+Idms8MgJhmAxLbydnjbqCP1r8RPg3/wSV8Z/wDC9r7T/izMreDNIlEkM8TYfUVPKpgcoAPv+/Ar+iKivrsh43zbKMLWweCq2hUXXXlf80eza0/HdJnwPFHhpkHEGOw+YZlQ5qlJ7rTnXSM/5op6/hs2jD8NeGdA8HaHbeGvC9nFYWFmgjhghUIiKOwArcoor5Oc5Tk5Sd292fd06cacVCCtFaJLZLsgoooqSz//1/7+KKKKACiiigAooooAK8J/aK/aG+H37M/wzvPiX8QrgRwwDbb26kebczH7saDuSep7DmvdW3bTt69s1/Hj/wAFS9c/acu/2hbiw+Psf2fTYWf+w47bd9ha2zw0ZPWQj7+eQfav0Dw44PpcRZssLXqqFOK5pK9pSS6RXfu+i1PzvxN4zrcN5PLF4ei51JPli7XjFv7U327Lq9Dwr9qv9rn4lftZ+Pv+Ev8AG8i29na7ksNPiJ8m2iJ7Ak5Y/wATHrXy/wDacDJNYfn45PFftR/wTX/4Ju6j8aryz+OXxttpLXwtbSrJY2Mi7W1Bl53MD0hB/wC+vpX9jZpmGU8LZT7WolTo01aMVu30jFdW/wDNvqz+HcryTOeLs4dODdSvUd5Tlsl1lJ9Eui9Elsix/wAE8/8Agmpc/Hq3HxZ+OcFxY+F8f6Daj93Jen++eMiMdum76V88ft4fsM+LP2RvGH9p6MJtS8G6gxNnfMMmFj/yxmIAAYfwnuPev7DbGxs9Ms4tP0+JYIIFCRxoNqqq8AADoBXL+P8AwB4R+KHhG+8C+OrGPUNM1CMxTQyjIIPcehHUEdDX8x4PxqzWOdvH11fDS0dJbKPRp/zrdvrtta39V47wCyWeQRy7D6YqOqrPeUuqkv5Hsl9ndXd7/wACwuGHevvT9iL9u7x1+yP4n+wMDqXhPUJVN/YMTlOxlh/uuB+BqH9vD9hXxl+yD4v/ALS03zNT8HajIfsV8VyYSf8AljNjgMOx/iHvX59C6bHav6fjDKeJsqurVcPVX9ecZRfzTP5LdLOeE850vRxNJ/15SjJfJo/v3+GnxJ8HfF3wRp/xC8BXiX2l6lEJYZEPr1Vh2YdCDyDXd1/PD/wRa8KftJW8moeKfPNp8N7kMBBdKT9ouR/Hbgn5QP4m6Gv6Hq/iHjXh6lkmb1svoVlUjF6Nbq/2ZdOZdbfhsf6AcC8SVs9yahmWIoOlOS1T2dvtR68r3V/x3ZRRRXyh9eFFFFABRRRQB//Q/v4ooooAKKKKACiiigAr5u/aj/Zg+HX7VvwyuPh14+i2N/rLO8jA861mHR0Pp2YdCOK+kaK6sDjq+DxEMVhZuFSDumt00cmOwOHxuHnhcVBTpzVpJ7NM/nF/ZW/4I2eINL+MV9rH7Rk0Vz4d0G5H2GCA8anjlXfuiDjcvJJ46V/RfY2FlpdlFpumxJBbwII444wFVEUYAAHAAFW6K9/injHM+Ia8a+Y1L8qsorSK7tLu3q3+iSPn+E+C8q4dw86GW07czvJvWT7Jvstkv1bYUUUV8sfVnEfEb4c+Dvix4Mv/AAB49sY9Q0vUYjFNDIMjB7j0YdQRyDX4HeH/APgiNJB+0LKNe1vzvhzARcxBeLyUEn/R27ADu46jtmv6KKK+r4d42zjI6Vajl1ZxjUVmt7P+aN9pW0uv0R8lxJwNk2e1aFfMqCnKk7p7XX8srbxvrZ/qzn/CnhXw/wCCPDll4R8K2sdlp2nQrBbwRDCoiDAAFdBRRXy05ynJzm7t6tvqfVwhGEVCCsloktkgoooqSgooooAKKKKAP//R/v4ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Z"), + contactLink = Just (either error CLFull $ strDecode "simplex:/contact/#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FShQuD-rPokbDvkyotKx5NwM8P3oUXHxA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA6fSx1k9zrOmF0BJpCaTarZvnZpMTAVQhd3RkDQ35KT0%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"), + preferences = Nothing + } + +timeItToView :: String -> CM' a -> CM' a +timeItToView s action = do + t1 <- liftIO getCurrentTime + a <- action + t2 <- liftIO getCurrentTime + let diff = diffToMilliseconds $ diffUTCTime t2 t1 + toView' $ CEvtTimedAction s diff + pure a diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs new file mode 100644 index 0000000000..d60faef639 --- /dev/null +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -0,0 +1,2825 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} + +module Simplex.Chat.Library.Subscriber where + +import Control.Logger.Simple +import Control.Monad +import Control.Monad.Except +import Control.Monad.IO.Unlift +import Control.Monad.Reader +import qualified Data.Aeson as J +import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Lazy.Char8 as LB +import Data.Either (lefts, partitionEithers, rights) +import Data.Functor (($>)) +import Data.Int (Int64) +import Data.List (foldl') +import Data.List.NonEmpty (NonEmpty (..)) +import qualified Data.List.NonEmpty as L +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as M +import Data.Maybe (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) +import qualified Data.UUID as UUID +import qualified Data.UUID.V4 as V4 +import Data.Word (Word32) +import Simplex.Chat.Call +import Simplex.Chat.Controller +import Simplex.Chat.Library.Internal +import Simplex.Chat.Messages +import Simplex.Chat.Messages.CIContent +import Simplex.Chat.Messages.CIContent.Events +import Simplex.Chat.ProfileGenerator (generateRandomProfile) +import Simplex.Chat.Protocol +import Simplex.Chat.Store +import Simplex.Chat.Store.Connections +import Simplex.Chat.Store.Direct +import Simplex.Chat.Store.Files +import Simplex.Chat.Store.Groups +import Simplex.Chat.Store.Messages +import Simplex.Chat.Store.Profiles +import Simplex.Chat.Store.Shared +import Simplex.Chat.Types +import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared +import Simplex.FileTransfer.Description (ValidFileDescription) +import qualified Simplex.FileTransfer.Description as FD +import Simplex.FileTransfer.Protocol (FilePartyI) +import qualified Simplex.FileTransfer.Transport as XFTP +import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId) +import Simplex.Messaging.Agent as Agent +import Simplex.Messaging.Agent.Protocol +import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) +import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Client (ProxyClientError (..)) +import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.File (CryptoFile (..)) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) +import qualified Simplex.Messaging.Crypto.Ratchet as CR +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Protocol (ErrorType (..), MsgFlags (..)) +import qualified Simplex.Messaging.Protocol as SMP +import qualified Simplex.Messaging.TMap as TM +import Simplex.Messaging.Transport (TransportError (..)) +import Simplex.Messaging.Util +import Simplex.Messaging.Version +import qualified System.FilePath as FP +import Text.Read (readMaybe) +import UnliftIO.Directory +import UnliftIO.STM + +smallGroupsRcptsMemLimit :: Int +smallGroupsRcptsMemLimit = 20 + +processAgentMessage :: ACorrId -> ConnId -> AEvent 'AEConn -> CM () +processAgentMessage _ _ (DEL_RCVQS delQs) = + toView $ CEvtAgentRcvQueuesDeleted $ L.map rcvQ delQs + where + rcvQ (connId, server, rcvId, err_) = DeletedRcvQueue (AgentConnId connId) server (AgentQueueId rcvId) err_ +processAgentMessage _ _ (DEL_CONNS connIds) = + toView $ CEvtAgentConnsDeleted $ L.map AgentConnId connIds +processAgentMessage _ "" (ERR e) = + eToView $ ChatErrorAgent e Nothing +processAgentMessage corrId connId msg = do + lockEntity <- critical (withStore (`getChatLockEntity` AgentConnId connId)) + withEntityLock "processAgentMessage" lockEntity $ do + vr <- chatVersionRange + -- getUserByAConnId never throws logical errors, only SEDBBusyError can be thrown here + critical (withStore' (`getUserByAConnId` AgentConnId connId)) >>= \case + Just user -> processAgentMessageConn vr user corrId connId msg `catchChatError` eToView + _ -> throwChatError $ CENoConnectionUser (AgentConnId connId) + +-- CRITICAL error will be shown to the user as alert with restart button in Android/desktop apps. +-- SEDBBusyError will only be thrown on IO exceptions or SQLError during DB queries, +-- e.g. when database is locked or busy for longer than 3s. +-- In this case there is no better mitigation than showing alert: +-- - without ACK the message delivery will be stuck, +-- - with ACK message will be lost, as it failed to be saved. +-- Full app restart is likely to resolve database condition and the message will be received and processed again. +critical :: CM a -> CM a +critical a = + a `catchChatError` \case + ChatErrorStore SEDBBusyError {message} -> throwError $ ChatErrorAgent (CRITICAL True message) Nothing + e -> throwError e + +processAgentMessageNoConn :: AEvent 'AENone -> CM () +processAgentMessageNoConn = \case + CONNECT p h -> hostEvent $ CEvtHostConnected p h + DISCONNECT p h -> hostEvent $ CEvtHostDisconnected p h + DOWN srv conns -> serverEvent srv conns NSDisconnected CEvtContactsDisconnected + UP srv conns -> serverEvent srv conns NSConnected CEvtContactsSubscribed + SUSPENDED -> toView CEvtChatSuspended + DEL_USER agentUserId -> toView $ CEvtAgentUserDeleted agentUserId + ERRS cErrs -> errsEvent cErrs + where + hostEvent :: ChatEvent -> CM () + hostEvent = whenM (asks $ hostEvents . config) . toView + serverEvent srv conns nsStatus event = do + chatModifyVar connNetworkStatuses $ \m -> foldl' (\m' cId -> M.insert cId nsStatus m') m connIds + ifM (asks $ coreApi . config) (notifyAPI connIds) notifyCLI + where + connIds = map AgentConnId conns + notifyAPI = toView . CEvtNetworkStatus nsStatus + notifyCLI = do + cs <- withStore' (`getConnectionsContacts` conns) + toView $ event srv cs + errsEvent :: [(ConnId, AgentErrorType)] -> CM () + errsEvent cErrs = do + vr <- chatVersionRange + errs <- lift $ rights <$> withStoreBatch' (\db -> map (getChatErr vr db) cErrs) + toView $ CEvtChatErrors errs + where + getChatErr :: VersionRangeChat -> DB.Connection -> (ConnId, AgentErrorType) -> IO ChatError + getChatErr vr db (connId, err) = + let acId = AgentConnId connId + in ChatErrorAgent err <$> (getUserByAConnId db acId $>>= \user -> eitherToMaybe <$> runExceptT (getConnectionEntity db vr user acId)) + +processAgentMsgSndFile :: ACorrId -> SndFileId -> AEvent 'AESndFile -> CM () +processAgentMsgSndFile _corrId aFileId msg = do + (cRef_, fileId) <- withStore (`getXFTPSndFileDBIds` AgentSndFileId aFileId) + withEntityLock_ cRef_ . withFileLock "processAgentMsgSndFile" fileId $ + withStore' (`getUserByASndFileId` AgentSndFileId aFileId) >>= \case + Just user -> process user fileId `catchChatError` eToView + _ -> do + lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) + throwChatError $ CENoSndFileUser $ AgentSndFileId aFileId + where + withEntityLock_ :: Maybe ChatRef -> CM a -> CM a + withEntityLock_ cRef_ = case cRef_ of + Just (ChatRef CTDirect contactId) -> withContactLock "processAgentMsgSndFile" contactId + Just (ChatRef CTGroup groupId) -> withGroupLock "processAgentMsgSndFile" groupId + _ -> id + process :: User -> FileTransferId -> CM () + process user fileId = do + (ft@FileTransferMeta {xftpRedirectFor, cancelled}, sfts) <- withStore $ \db -> getSndFileTransfer db user fileId + vr <- chatVersionRange + unless cancelled $ case msg of + SFPROG sndProgress sndTotal -> do + let status = CIFSSndTransfer {sndProgress, sndTotal} + ci <- withStore $ \db -> do + liftIO $ updateCIFileStatus db user fileId status + lookupChatItemByFileId db vr user fileId + toView $ CEvtSndFileProgressXFTP user ci ft sndProgress sndTotal + SFDONE sndDescr rfds -> do + withStore' $ \db -> setSndFTPrivateSndDescr db user fileId (fileDescrText sndDescr) + ci <- withStore $ \db -> lookupChatItemByFileId db vr user fileId + case ci of + Nothing -> do + lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) + withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText rfds) + case rfds of + [] -> sendFileError (FileErrOther "no receiver descriptions") "no receiver descriptions" vr ft + rfd : _ -> case [fd | fd@(FD.ValidFileDescription FD.FileDescription {chunks = [_]}) <- rfds] of + [] -> case xftpRedirectFor of + Nothing -> xftpSndFileRedirect user fileId rfd >>= toView . CEvtSndFileRedirectStartXFTP user ft + Just _ -> sendFileError (FileErrOther "chaining redirects") "Prohibit chaining redirects" vr ft + rfds' -> do + -- we have 1 chunk - use it as URI whether it is redirect or not + ft' <- maybe (pure ft) (\fId -> withStore $ \db -> getFileTransferMeta db user fId) xftpRedirectFor + toView $ CEvtSndStandaloneFileComplete user ft' $ map (decodeLatin1 . strEncode . FD.fileDescriptionURI) rfds' + Just (AChatItem _ d cInfo _ci@ChatItem {meta = CIMeta {itemSharedMsgId = msgId_, itemDeleted}}) -> + case (msgId_, itemDeleted) of + (Just sharedMsgId, Nothing) -> do + when (length rfds < length sfts) $ throwChatError $ CEInternalError "not enough XFTP file descriptions to send" + -- TODO either update database status or move to SFPROG + toView $ CEvtSndFileProgressXFTP user ci ft 1 1 + case (rfds, sfts, d, cInfo) of + (rfd : extraRFDs, sft : _, SMDSnd, DirectChat ct) -> do + withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText extraRFDs) + conn@Connection {connId} <- liftEither $ contactSendConn_ ct + sendFileDescriptions (ConnectionId connId) ((conn, sft, fileDescrText rfd) :| []) sharedMsgId >>= \case + Just rs -> case L.last rs of + Right ([msgDeliveryId], _) -> + withStore' $ \db -> updateSndFTDeliveryXFTP db sft msgDeliveryId + Right (deliveryIds, _) -> eToView $ ChatError $ CEInternalError $ "SFDONE, sendFileDescriptions: expected 1 delivery id, got " <> show (length deliveryIds) + Left e -> eToView e + Nothing -> eToView $ ChatError $ CEInternalError "SFDONE, sendFileDescriptions: expected at least 1 result" + lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) + (_, _, SMDSnd, GroupChat g@GroupInfo {groupId}) -> do + ms <- withStore' $ \db -> getGroupMembers db vr user g + let rfdsMemberFTs = zipWith (\rfd (conn, sft) -> (conn, sft, fileDescrText rfd)) rfds (memberFTs ms) + extraRFDs = drop (length rfdsMemberFTs) rfds + withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText extraRFDs) + forM_ (L.nonEmpty rfdsMemberFTs) $ \rfdsMemberFTs' -> + sendFileDescriptions (GroupId groupId) rfdsMemberFTs' sharedMsgId + ci' <- withStore $ \db -> do + liftIO $ updateCIFileStatus db user fileId CIFSSndComplete + getChatItemByFileId db vr user fileId + lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) + toView $ CEvtSndFileCompleteXFTP user ci' ft + where + memberFTs :: [GroupMember] -> [(Connection, SndFileTransfer)] + memberFTs ms = M.elems $ M.intersectionWith (,) (M.fromList mConns') (M.fromList sfts') + where + mConns' = mapMaybe useMember ms + sfts' = mapMaybe (\sft@SndFileTransfer {groupMemberId} -> (,sft) <$> groupMemberId) sfts + -- Should match memberSendAction logic + useMember GroupMember {groupMemberId, activeConn = Just conn@Connection {connStatus}} + | (connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn) && not (connInactive conn) = + Just (groupMemberId, conn) + | otherwise = Nothing + useMember _ = Nothing + _ -> pure () + _ -> pure () -- TODO error? + SFWARN e -> do + let err = tshow e + logWarn $ "Sent file warning: " <> err + ci <- withStore $ \db -> do + liftIO $ updateCIFileStatus db user fileId (CIFSSndWarning $ agentFileError e) + lookupChatItemByFileId db vr user fileId + toView $ CEvtSndFileWarning user ci ft err + SFERR e -> + sendFileError (agentFileError e) (tshow e) vr ft + where + fileDescrText :: FilePartyI p => ValidFileDescription p -> T.Text + fileDescrText = safeDecodeUtf8 . strEncode + sendFileDescriptions :: ConnOrGroupId -> NonEmpty (Connection, SndFileTransfer, RcvFileDescrText) -> SharedMsgId -> CM (Maybe (NonEmpty (Either ChatError ([Int64], PQEncryption)))) + sendFileDescriptions connOrGroupId connsTransfersDescrs sharedMsgId = do + lift . void . withStoreBatch' $ \db -> L.map (\(_, sft, rfdText) -> updateSndFTDescrXFTP db user sft rfdText) connsTransfersDescrs + partSize <- asks $ xftpDescrPartSize . config + let connsIdsEvts = connDescrEvents partSize + sndMsgs_ <- lift $ createSndMessages $ L.map snd connsIdsEvts + let (errs, msgReqs) = partitionEithers . L.toList $ L.zipWith (fmap . toMsgReq) connsIdsEvts sndMsgs_ + delivered <- mapM deliverMessages (L.nonEmpty msgReqs) + let errs' = errs <> maybe [] (lefts . L.toList) delivered + unless (null errs') $ toView $ CEvtChatErrors errs' + pure delivered + where + connDescrEvents :: Int -> NonEmpty (Connection, (ConnOrGroupId, ChatMsgEvent 'Json)) + connDescrEvents partSize = L.fromList $ concatMap splitText (L.toList connsTransfersDescrs) + where + splitText :: (Connection, SndFileTransfer, RcvFileDescrText) -> [(Connection, (ConnOrGroupId, ChatMsgEvent 'Json))] + splitText (conn, _, rfdText) = + map (\fileDescr -> (conn, (connOrGroupId, XMsgFileDescr {msgId = sharedMsgId, fileDescr}))) (L.toList $ splitFileDescr partSize rfdText) + toMsgReq :: (Connection, (ConnOrGroupId, ChatMsgEvent 'Json)) -> SndMessage -> ChatMsgReq + toMsgReq (conn, _) SndMessage {msgId, msgBody} = + (conn, MsgFlags {notification = hasNotification XMsgFileDescr_}, (vrValue msgBody, [msgId])) + sendFileError :: FileError -> Text -> VersionRangeChat -> FileTransferMeta -> CM () + sendFileError ferr err vr ft = do + logError $ "Sent file error: " <> err + ci <- withStore $ \db -> do + liftIO $ updateFileCancelled db user fileId (CIFSSndError ferr) + lookupChatItemByFileId db vr user fileId + lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) + toView $ CEvtSndFileError user ci ft err + +agentFileError :: AgentErrorType -> FileError +agentFileError = \case + XFTP _ XFTP.AUTH -> FileErrAuth + XFTP srv (XFTP.BLOCKED info) -> FileErrBlocked srv info + FILE NO_FILE -> FileErrNoFile + BROKER _ e -> brokerError FileErrRelay e + e -> FileErrOther $ tshow e + where + brokerError srvErr = \case + HOST -> srvErr SrvErrHost + SMP.TRANSPORT TEVersion -> srvErr SrvErrVersion + e -> srvErr . SrvErrOther $ tshow e + +processAgentMsgRcvFile :: ACorrId -> RcvFileId -> AEvent 'AERcvFile -> CM () +processAgentMsgRcvFile _corrId aFileId msg = do + (cRef_, fileId) <- withStore (`getXFTPRcvFileDBIds` AgentRcvFileId aFileId) + withEntityLock_ cRef_ . withFileLock "processAgentMsgRcvFile" fileId $ + withStore' (`getUserByARcvFileId` AgentRcvFileId aFileId) >>= \case + Just user -> process user fileId `catchChatError` eToView + _ -> do + lift $ withAgent' (`xftpDeleteRcvFile` aFileId) + throwChatError $ CENoRcvFileUser $ AgentRcvFileId aFileId + where + withEntityLock_ :: Maybe ChatRef -> CM a -> CM a + withEntityLock_ cRef_ = case cRef_ of + Just (ChatRef CTDirect contactId) -> withContactLock "processAgentMsgRcvFile" contactId + Just (ChatRef CTGroup groupId) -> withGroupLock "processAgentMsgRcvFile" groupId + _ -> id + process :: User -> FileTransferId -> CM () + process user fileId = do + ft <- withStore $ \db -> getRcvFileTransfer db user fileId + vr <- chatVersionRange + unless (rcvFileCompleteOrCancelled ft) $ case msg of + RFPROG rcvProgress rcvTotal -> do + let status = CIFSRcvTransfer {rcvProgress, rcvTotal} + ci <- withStore $ \db -> do + liftIO $ updateCIFileStatus db user fileId status + lookupChatItemByFileId db vr user fileId + toView $ CEvtRcvFileProgressXFTP user ci rcvProgress rcvTotal ft + RFDONE xftpPath -> + case liveRcvFileTransferPath ft of + Nothing -> throwChatError $ CEInternalError "no target path for received XFTP file" + Just targetPath -> do + fsTargetPath <- lift $ toFSFilePath targetPath + renameFile xftpPath fsTargetPath + ci_ <- withStore $ \db -> do + liftIO $ do + updateRcvFileStatus db fileId FSComplete + updateCIFileStatus db user fileId CIFSRcvComplete + lookupChatItemByFileId db vr user fileId + agentXFTPDeleteRcvFile aFileId fileId + toView $ maybe (CEvtRcvStandaloneFileComplete user fsTargetPath ft) (CEvtRcvFileComplete user) ci_ + RFWARN e -> do + ci <- withStore $ \db -> do + liftIO $ updateCIFileStatus db user fileId (CIFSRcvWarning $ agentFileError e) + lookupChatItemByFileId db vr user fileId + toView $ CEvtRcvFileWarning user ci e ft + RFERR e + | e == FILE NOT_APPROVED -> do + aci_ <- resetRcvCIFileStatus user fileId CIFSRcvAborted + forM_ aci_ cleanupACIFile + agentXFTPDeleteRcvFile aFileId fileId + forM_ aci_ $ \aci -> toView $ CEvtChatItemUpdated user aci + | otherwise -> do + aci_ <- withStore $ \db -> do + liftIO $ updateFileCancelled db user fileId (CIFSRcvError $ agentFileError e) + lookupChatItemByFileId db vr user fileId + forM_ aci_ cleanupACIFile + agentXFTPDeleteRcvFile aFileId fileId + toView $ CEvtRcvFileError user aci_ e ft + +processAgentMessageConn :: VersionRangeChat -> User -> ACorrId -> ConnId -> AEvent 'AEConn -> CM () +processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = do + -- Missing connection/entity errors here will be sent to the view but not shown as CRITICAL alert, + -- as in this case no need to ACK message - we can't process messages for this connection anyway. + -- SEDBException will be re-trown as CRITICAL as it is likely to indicate a temporary database condition + -- that will be resolved with app restart. + entity <- critical $ withStore (\db -> getConnectionEntity db vr user $ AgentConnId agentConnId) >>= updateConnStatus + case agentMessage of + END -> case entity of + RcvDirectMsgConnection _ (Just ct) -> toView $ CEvtContactAnotherClient user ct + _ -> toView $ CEvtSubscriptionEnd user entity + MSGNTF msgId msgTs_ -> toView $ CEvtNtfMessage user entity $ ntfMsgAckInfo msgId msgTs_ + _ -> case entity of + RcvDirectMsgConnection conn contact_ -> + processDirectMessage agentMessage entity conn contact_ + RcvGroupMsgConnection conn gInfo m -> + processGroupMessage agentMessage entity conn gInfo m + RcvFileConnection conn ft -> + processRcvFileConn agentMessage entity conn ft + SndFileConnection conn ft -> + processSndFileConn agentMessage entity conn ft + UserContactConnection conn uc -> + processUserContactRequest agentMessage entity conn uc + where + updateConnStatus :: ConnectionEntity -> CM ConnectionEntity + updateConnStatus acEntity = case agentMsgConnStatus agentMessage of + Just connStatus -> do + let conn = (entityConnection acEntity) {connStatus} + withStore' $ \db -> updateConnectionStatus db conn connStatus + pure $ updateEntityConnStatus acEntity connStatus + Nothing -> pure acEntity + + agentMsgConnStatus :: AEvent e -> Maybe ConnStatus + agentMsgConnStatus = \case + JOINED True -> Just ConnSndReady + CONF {} -> Just ConnRequested + INFO {} -> Just ConnSndReady + CON _ -> Just ConnReady + _ -> Nothing + + processCONFpqSupport :: Connection -> PQSupport -> CM Connection + processCONFpqSupport conn@Connection {connId, pqSupport = pq} pq' + | pq == PQSupportOn && pq' == PQSupportOff = do + let pqEnc' = CR.pqSupportToEnc pq' + withStore' $ \db -> updateConnSupportPQ db connId pq' pqEnc' + pure (conn {pqSupport = pq', pqEncryption = pqEnc'} :: Connection) + | pq /= pq' = do + messageWarning "processCONFpqSupport: unexpected pqSupport change" + pure conn + | otherwise = pure conn + + processINFOpqSupport :: Connection -> PQSupport -> CM () + processINFOpqSupport Connection {pqSupport = pq} pq' = + when (pq /= pq') $ messageWarning "processINFOpqSupport: unexpected pqSupport change" + + processDirectMessage :: AEvent e -> ConnectionEntity -> Connection -> Maybe Contact -> CM () + processDirectMessage agentMsg connEntity conn@Connection {connId, connChatVersion, peerChatVRange, viaUserContactLink, customUserProfileId, connectionCode} = \case + Nothing -> case agentMsg of + CONF confId pqSupport _ connInfo -> do + conn' <- processCONFpqSupport conn pqSupport + -- [incognito] send saved profile + (conn'', inGroup) <- saveConnInfo conn' connInfo + incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) + let profileToSend = userProfileToSend user (fromLocalProfile <$> incognitoProfile) Nothing inGroup + -- [async agent commands] no continuation needed, but command should be asynchronous for stability + allowAgentConnectionAsync user conn'' confId $ XInfo profileToSend + INFO pqSupport connInfo -> do + processINFOpqSupport conn pqSupport + void $ saveConnInfo conn connInfo + MSG meta _msgFlags _msgBody -> + -- We are not saving message (saveDirectRcvMSG) as contact hasn't been created yet, + -- chat item is also not created here + withAckMessage' "new contact msg" agentConnId meta $ pure () + SENT msgId _proxy -> do + void $ continueSending connEntity conn + sentMsgDeliveryEvent conn msgId + OK -> + -- [async agent commands] continuation on receiving OK + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + JOINED _ -> + -- [async agent commands] continuation on receiving JOINED + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + QCONT -> + void $ continueSending connEntity conn + MWARN _ err -> + processConnMWARN connEntity conn err + MERR _ err -> do + eToView (ChatErrorAgent err $ Just connEntity) + processConnMERR connEntity conn err + MERRS _ err -> do + -- error cannot be AUTH error here + eToView (ChatErrorAgent err $ Just connEntity) + ERR err -> do + eToView (ChatErrorAgent err $ Just connEntity) + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + -- TODO add debugging output + _ -> pure () + Just ct@Contact {contactId} -> case agentMsg of + INV (ACR _ cReq) -> + -- [async agent commands] XGrpMemIntro continuation on receiving INV + withCompletedCommand conn agentMsg $ \_ -> + case cReq of + directConnReq@(CRInvitationUri _ _) -> do + contData <- withStore' $ \db -> do + setConnConnReqInv db user connId cReq + getXGrpMemIntroContDirect db user ct + forM_ contData $ \(hostConnId, xGrpMemIntroCont) -> + sendXGrpMemInv hostConnId (Just directConnReq) xGrpMemIntroCont + CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" + MSG msgMeta _msgFlags msgBody -> do + tags <- newTVarIO [] + withAckMessage "contact msg" agentConnId msgMeta True (Just tags) $ \eInfo -> do + let MsgMeta {pqEncryption} = msgMeta + (ct', conn') <- updateContactPQRcv user ct conn pqEncryption + checkIntegrityCreateItem (CDDirectRcv ct') msgMeta `catchChatError` \_ -> pure () + forM_ aChatMsgs $ \case + Right (ACMsg _ chatMsg) -> + processEvent ct' conn' tags eInfo chatMsg `catchChatError` \e -> eToView e + Left e -> do + atomically $ modifyTVar' tags ("error" :) + logInfo $ "contact msg=error " <> eInfo <> " " <> tshow e + eToView (ChatError . CEException $ "error parsing chat message: " <> e) + checkSendRcpt ct' $ rights aChatMsgs -- not crucial to use ct'' from processEvent + where + aChatMsgs = parseChatMessages msgBody + processEvent :: Contact -> Connection -> TVar [Text] -> Text -> MsgEncodingI e => ChatMessage e -> CM () + processEvent ct' conn' tags eInfo chatMsg@ChatMessage {chatMsgEvent} = do + let tag = toCMEventTag chatMsgEvent + atomically $ modifyTVar' tags (tshow tag :) + logInfo $ "contact msg=" <> tshow tag <> " " <> eInfo + (conn'', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn' msgMeta msgBody chatMsg + let ct'' = ct' {activeConn = Just conn''} :: Contact + case event of + XMsgNew mc -> newContentMessage ct'' mc msg msgMeta + XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct'' sharedMsgId fileDescr + XMsgUpdate sharedMsgId mContent _ ttl live -> messageUpdate ct'' sharedMsgId mContent msg msgMeta ttl live + XMsgDel sharedMsgId _ -> messageDelete ct'' sharedMsgId msg msgMeta + XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct'' sharedMsgId reaction add msg msgMeta + -- TODO discontinue XFile + XFile fInv -> processFileInvitation' ct'' fInv msg msgMeta + XFileCancel sharedMsgId -> xFileCancel ct'' sharedMsgId + XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct'' sharedMsgId fileConnReq_ fName + XInfo p -> xInfo ct'' p + XDirectDel -> xDirectDel ct'' msg msgMeta + XGrpInv gInv -> processGroupInvitation ct'' gInv msg msgMeta + XInfoProbe probe -> xInfoProbe (COMContact ct'') probe + XInfoProbeCheck probeHash -> xInfoProbeCheck (COMContact ct'') probeHash + XInfoProbeOk probe -> xInfoProbeOk (COMContact ct'') probe + XCallInv callId invitation -> xCallInv ct'' callId invitation msg msgMeta + XCallOffer callId offer -> xCallOffer ct'' callId offer msg + XCallAnswer callId answer -> xCallAnswer ct'' callId answer msg + XCallExtra callId extraInfo -> xCallExtra ct'' callId extraInfo msg + XCallEnd callId -> xCallEnd ct'' callId msg + BFileChunk sharedMsgId chunk -> bFileChunk ct'' sharedMsgId chunk msgMeta + _ -> messageError $ "unsupported message: " <> T.pack (show event) + checkSendRcpt :: Contact -> [AChatMessage] -> CM Bool + checkSendRcpt ct' aMsgs = do + let Contact {chatSettings = ChatSettings {sendRcpts}} = ct' + pure $ fromMaybe (sendRcptsContacts user) sendRcpts && any aChatMsgHasReceipt aMsgs + where + aChatMsgHasReceipt (ACMsg _ ChatMessage {chatMsgEvent}) = + hasDeliveryReceipt (toCMEventTag chatMsgEvent) + RCVD msgMeta msgRcpt -> + withAckMessage' "contact rcvd" agentConnId msgMeta $ + directMsgReceived ct conn msgMeta msgRcpt + CONF confId pqSupport _ connInfo -> do + conn' <- processCONFpqSupport conn pqSupport + ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn' connInfo + conn'' <- updatePeerChatVRange conn' chatVRange + case chatMsgEvent of + -- confirming direct connection with a member + XGrpMemInfo _memId _memProfile -> do + -- TODO check member ID + -- TODO update member profile + -- [async agent commands] no continuation needed, but command should be asynchronous for stability + allowAgentConnectionAsync user conn'' confId XOk + XInfo profile -> do + ct' <- processContactProfileUpdate ct profile False `catchChatError` const (pure ct) + -- [incognito] send incognito profile + incognitoProfile <- forM customUserProfileId $ \profileId -> withStore $ \db -> getProfileById db userId profileId + let p = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct') False + allowAgentConnectionAsync user conn'' confId $ XInfo p + void $ withStore' $ \db -> resetMemberContactFields db ct' + XGrpLinkInv glInv -> do + -- XGrpLinkInv here means we are connecting via business contact card, so we replace contact with group + (gInfo, host) <- withStore $ \db -> do + liftIO $ deleteContactCardKeepConn db connId ct + createGroupInvitedViaLink db vr user conn'' glInv + -- [incognito] send saved profile + incognitoProfile <- forM customUserProfileId $ \pId -> withStore (\db -> getProfileById db userId pId) + let profileToSend = userProfileToSend user (fromLocalProfile <$> incognitoProfile) Nothing True + allowAgentConnectionAsync user conn'' confId $ XInfo profileToSend + toView $ CEvtBusinessLinkConnecting user gInfo host ct + _ -> messageError "CONF for existing contact must have x.grp.mem.info or x.info" + INFO pqSupport connInfo -> do + processINFOpqSupport conn pqSupport + ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo + _conn' <- updatePeerChatVRange conn chatVRange + case chatMsgEvent of + XGrpMemInfo _memId _memProfile -> do + -- TODO check member ID + -- TODO update member profile + pure () + XInfo profile -> + void $ processContactProfileUpdate ct profile False + XOk -> pure () + _ -> messageError "INFO for existing contact must have x.grp.mem.info, x.info or x.ok" + CON pqEnc -> + withStore' (\db -> getViaGroupMember db vr user ct) >>= \case + Nothing -> do + when (pqEnc == PQEncOn) $ withStore' $ \db -> updateConnPQEnabledCON db connId pqEnc + let conn' = conn {pqSndEnabled = Just pqEnc, pqRcvEnabled = Just pqEnc} :: Connection + ct' = ct {activeConn = Just conn'} :: Contact + -- [incognito] print incognito profile used for this contact + incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) + lift $ setContactNetworkStatus ct' NSConnected + toView $ CEvtContactConnected user ct' (fmap fromLocalProfile incognitoProfile) + when (directOrUsed ct') $ do + createInternalChatItem user (CDDirectRcv ct') (CIRcvDirectE2EEInfo $ E2EInfo pqEnc) Nothing + createFeatureEnabledItems ct' + when (contactConnInitiated conn') $ do + let Connection {groupLinkId} = conn' + doProbeContacts = isJust groupLinkId + probeMatchingContactsAndMembers ct' (contactConnIncognito ct') doProbeContacts + withStore' $ \db -> resetContactConnInitiated db user conn' + forM_ viaUserContactLink $ \userContactLinkId -> do + ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId + let (UserContactLink {autoAccept}, gli_) = ucl + when (connChatVersion < batchSend2Version) $ sendAutoReply ct' autoAccept + -- TODO REMOVE LEGACY vvv + forM_ gli_ $ \GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do + groupInfo <- withStore $ \db -> getGroupInfo db vr user groupId + subMode <- chatReadVar subscriptionMode + groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation subMode + gVar <- asks random + withStore $ \db -> createNewContactMemberAsync db gVar user groupInfo ct' gLinkMemRole groupConnIds connChatVersion peerChatVRange subMode + -- TODO REMOVE LEGACY ^^^ + Just (gInfo, m@GroupMember {activeConn}) -> + when (maybe False ((== ConnReady) . connStatus) activeConn) $ do + notifyMemberConnected gInfo m $ Just ct + let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo + when (memberCategory m == GCPreMember) $ probeMatchingContactsAndMembers ct connectedIncognito True + SENT msgId proxy -> do + void $ continueSending connEntity conn + sentMsgDeliveryEvent conn msgId + checkSndInlineFTComplete conn msgId + cis <- withStore $ \db -> do + cis <- updateDirectItemsStatus' db ct conn msgId (CISSndSent SSPComplete) + liftIO $ forM cis $ \ci -> setDirectSndChatItemViaProxy db user ct ci (isJust proxy) + let acis = map ctItem cis + unless (null acis) $ toView $ CEvtChatItemsStatusesUpdated user acis + where + ctItem = AChatItem SCTDirect SMDSnd (DirectChat ct) + SWITCH qd phase cStats -> do + toView $ CEvtContactSwitch user ct (SwitchProgress qd phase cStats) + when (phase == SPStarted || phase == SPCompleted) $ case qd of + QDRcv -> createInternalChatItem user (CDDirectSnd ct) (CISndConnEvent $ SCESwitchQueue phase Nothing) Nothing + QDSnd -> createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing + RSYNC rss cryptoErr_ cStats -> + case (rss, connectionCode, cryptoErr_) of + (RSRequired, _, Just cryptoErr) -> processErr cryptoErr + (RSAllowed, _, Just cryptoErr) -> processErr cryptoErr + (RSAgreed, Just _, _) -> do + withStore' $ \db -> setConnectionVerified db user connId Nothing + let ct' = ct {activeConn = Just $ (conn :: Connection) {connectionCode = Nothing}} :: Contact + ratchetSyncEventItem ct' + securityCodeChanged ct' + _ -> ratchetSyncEventItem ct + where + processErr cryptoErr = do + let e@(mde, n) = agentMsgDecryptError cryptoErr + ci_ <- withStore $ \db -> + getDirectChatItemLast db user contactId + >>= liftIO + . mapM (\(ci, content') -> updateDirectChatItem' db user contactId ci content' False False Nothing Nothing) + . mdeUpdatedCI e + case ci_ of + Just ci -> toView $ CEvtChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) + _ -> do + toView $ CEvtContactRatchetSync user ct (RatchetSyncProgress rss cStats) + createInternalChatItem user (CDDirectRcv ct) (CIRcvDecryptionError mde n) Nothing + ratchetSyncEventItem ct' = do + toView $ CEvtContactRatchetSync user ct' (RatchetSyncProgress rss cStats) + createInternalChatItem user (CDDirectRcv ct') (CIRcvConnEvent $ RCERatchetSync rss) Nothing + OK -> + -- [async agent commands] continuation on receiving OK + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + JOINED sqSecured -> + -- [async agent commands] continuation on receiving JOINED + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> + when (directOrUsed ct && sqSecured) $ do + lift $ setContactNetworkStatus ct NSConnected + toView $ CEvtContactSndReady user ct + forM_ viaUserContactLink $ \userContactLinkId -> do + ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId + let (UserContactLink {autoAccept}, _) = ucl + when (connChatVersion >= batchSend2Version) $ sendAutoReply ct autoAccept + QCONT -> + void $ continueSending connEntity conn + MWARN msgId err -> do + updateDirectItemStatus ct conn msgId (CISSndWarning $ agentSndError err) + processConnMWARN connEntity conn err + MERR msgId err -> do + updateDirectItemStatus ct conn msgId (CISSndError $ agentSndError err) + eToView (ChatErrorAgent err $ Just connEntity) + processConnMERR connEntity conn err + MERRS msgIds err -> do + -- error cannot be AUTH error here + updateDirectItemsStatusMsgs ct conn (L.toList msgIds) (CISSndError $ agentSndError err) + eToView (ChatErrorAgent err $ Just connEntity) + ERR err -> do + eToView (ChatErrorAgent err $ Just connEntity) + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + -- TODO add debugging output + _ -> pure () + where + sendAutoReply ct = \case + Just AutoAccept {autoReply = Just mc} -> do + (msg, _) <- sendDirectContactMessage user ct (XMsgNew $ MCSimple (extMsgContent mc Nothing)) + ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] + _ -> pure () + + processGroupMessage :: AEvent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM () + processGroupMessage agentMsg connEntity conn@Connection {connId, connChatVersion, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of + INV (ACR _ cReq) -> + withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} -> + case cReq of + groupConnReq@(CRInvitationUri _ _) -> case cmdFunction of + -- [async agent commands] XGrpMemIntro continuation on receiving INV + CFCreateConnGrpMemInv + | maxVersion (peerChatVRange conn) >= groupDirectInvVersion -> sendWithoutDirectCReq + | otherwise -> messageError "processGroupMessage INV: member chat version range incompatible" + where + sendWithoutDirectCReq = do + let GroupMember {groupMemberId, memberId} = m + hostConnId <- withStore $ \db -> do + liftIO $ setConnConnReqInv db user connId cReq + getHostConnId db user groupId + sendXGrpMemInv hostConnId Nothing XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} + -- TODO REMOVE LEGACY vvv + -- [async agent commands] group link auto-accept continuation on receiving INV + CFCreateConnGrpInv -> do + ct <- withStore $ \db -> getContactViaMember db vr user m + withStore' $ \db -> setNewContactMemberConnRequest db user m cReq + groupLinkId <- withStore' $ \db -> getGroupLinkId db user gInfo + sendGrpInvitation ct m groupLinkId + toView $ CEvtSentGroupInvitation user gInfo ct m + where + sendGrpInvitation :: Contact -> GroupMember -> Maybe GroupLinkId -> CM () + sendGrpInvitation ct GroupMember {memberId, memberRole = memRole} groupLinkId = do + currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo + let GroupMember {memberRole = userRole, memberId = userMemberId} = membership + groupInv = + GroupInvitation + { fromMember = MemberIdRole userMemberId userRole, + invitedMember = MemberIdRole memberId memRole, + connRequest = cReq, + groupProfile, + business = Nothing, + groupLinkId = groupLinkId, + groupSize = Just currentMemCount + } + (_msg, _) <- sendDirectContactMessage user ct $ XGrpInv groupInv + -- we could link chat item with sent group invitation message (_msg) + createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing + -- TODO REMOVE LEGACY ^^^ + _ -> throwChatError $ CECommandError "unexpected cmdFunction" + CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" + CONF confId _pqSupport _ connInfo -> do + ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo + conn' <- updatePeerChatVRange conn chatVRange + case memberCategory m of + GCInviteeMember -> + case chatMsgEvent of + XGrpAcpt memId + | sameMemberId memId m -> do + withStore $ \db -> liftIO $ updateGroupMemberStatus db userId m GSMemAccepted + -- [async agent commands] no continuation needed, but command should be asynchronous for stability + allowAgentConnectionAsync user conn' confId XOk + | otherwise -> messageError "x.grp.acpt: memberId is different from expected" + _ -> messageError "CONF from invited member must have x.grp.acpt" + _ -> + case chatMsgEvent of + XGrpMemInfo memId _memProfile + | sameMemberId memId m -> do + let GroupMember {memberId = membershipMemId} = membership + membershipProfile = redactedMemberProfile $ fromLocalProfile $ memberProfile membership + -- TODO update member profile + -- [async agent commands] no continuation needed, but command should be asynchronous for stability + allowAgentConnectionAsync user conn' confId $ XGrpMemInfo membershipMemId membershipProfile + | otherwise -> messageError "x.grp.mem.info: memberId is different from expected" + _ -> messageError "CONF from member must have x.grp.mem.info" + INFO _pqSupport connInfo -> do + ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo + _conn' <- updatePeerChatVRange conn chatVRange + case chatMsgEvent of + XGrpMemInfo memId _memProfile + | sameMemberId memId m -> do + -- TODO update member profile + pure () + | otherwise -> messageError "x.grp.mem.info: memberId is different from expected" + -- sent when connecting via group link + XInfo _ -> + -- TODO Keep rejected member to allow them to appeal against rejection. + when (memberStatus m == GSMemRejected) $ do + deleteMemberConnection' m True + withStore' $ \db -> deleteGroupMember db user m + XOk -> pure () + _ -> messageError "INFO from member must have x.grp.mem.info, x.info or x.ok" + pure () + CON _pqEnc -> unless (memberStatus m == GSMemRejected) $ do + status' <- case memberStatus m of + GSMemPendingApproval -> pure GSMemPendingApproval + _ -> do + withStore' $ \db -> do + updateGroupMemberStatus db userId m GSMemConnected + unless (memberActive membership) $ + updateGroupMemberStatus db userId membership GSMemConnected + -- possible improvement: check for each pending message, requires keeping track of connection state + unless (connDisabled conn) $ sendPendingGroupMessages user m conn + pure GSMemConnected + withAgent $ \a -> toggleConnectionNtfs a (aConnId conn) $ chatHasNtfs chatSettings + case memberCategory m of + GCHostMember -> do + toView $ CEvtUserJoinedGroup user gInfo {membership = membership {memberStatus = status'}} m {memberStatus = status'} + let cd = CDGroupRcv gInfo m + createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing + createGroupFeatureItems user cd CIRcvGroupFeature gInfo + let GroupInfo {groupProfile = GroupProfile {description}} = gInfo + memberConnectedChatItem gInfo m + unless expectHistory $ forM_ description $ groupDescriptionChatItem gInfo m + where + expectHistory = groupFeatureAllowed SGFHistory gInfo && m `supportsVersion` groupHistoryIncludeWelcomeVersion + GCInviteeMember -> do + memberConnectedChatItem gInfo m + toView $ CEvtJoinedGroupMember user gInfo m {memberStatus = status'} + let Connection {viaUserContactLink} = conn + when (isJust viaUserContactLink && isNothing (memberContactId m)) sendXGrpLinkMem + when (connChatVersion < batchSend2Version) sendGroupAutoReply + unless (status' == GSMemPendingApproval) $ introduceToGroup vr user gInfo m + where + sendXGrpLinkMem = do + let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo + profileToSend = profileToSendOnAccept user profileMode True + void $ sendDirectMemberMessage conn (XGrpLinkMem profileToSend) groupId + _ -> do + let memCategory = memberCategory m + withStore' (\db -> getViaGroupContact db vr user m) >>= \case + Nothing -> do + notifyMemberConnected gInfo m Nothing + let connectedIncognito = memberIncognito membership + when (memCategory == GCPreMember) $ probeMatchingMemberContact m connectedIncognito + Just ct@Contact {activeConn} -> + forM_ activeConn $ \Connection {connStatus} -> + when (connStatus == ConnReady) $ do + notifyMemberConnected gInfo m $ Just ct + let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo + when (memCategory == GCPreMember) $ probeMatchingContactsAndMembers ct connectedIncognito True + sendXGrpMemCon memCategory + where + GroupMember {memberId} = m + sendXGrpMemCon = \case + GCPreMember -> + forM_ (invitedByGroupMemberId membership) $ \hostId -> do + host <- withStore $ \db -> getGroupMember db vr user groupId hostId + forM_ (memberConn host) $ \hostConn -> + void $ sendDirectMemberMessage hostConn (XGrpMemCon memberId) groupId + GCPostMember -> + forM_ (invitedByGroupMemberId m) $ \invitingMemberId -> do + im <- withStore $ \db -> getGroupMember db vr user groupId invitingMemberId + forM_ (memberConn im) $ \imConn -> + void $ sendDirectMemberMessage imConn (XGrpMemCon memberId) groupId + _ -> messageWarning "sendXGrpMemCon: member category GCPreMember or GCPostMember is expected" + MSG msgMeta _msgFlags msgBody -> do + tags <- newTVarIO [] + withAckMessage "group msg" agentConnId msgMeta True (Just tags) $ \eInfo -> do + checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure () + forM_ aChatMsgs $ \case + Right (ACMsg _ chatMsg) -> + processEvent tags eInfo chatMsg `catchChatError` \e -> eToView e + Left e -> do + atomically $ modifyTVar' tags ("error" :) + logInfo $ "group msg=error " <> eInfo <> " " <> tshow e + eToView (ChatError . CEException $ "error parsing chat message: " <> e) + forwardMsgs (rights aChatMsgs) `catchChatError` eToView + checkSendRcpt $ rights aChatMsgs + where + aChatMsgs = parseChatMessages msgBody + brokerTs = metaBrokerTs msgMeta + processEvent :: TVar [Text] -> Text -> MsgEncodingI e => ChatMessage e -> CM () + processEvent tags eInfo chatMsg@ChatMessage {chatMsgEvent} = do + let tag = toCMEventTag chatMsgEvent + atomically $ modifyTVar' tags (tshow tag :) + logInfo $ "group msg=" <> tshow tag <> " " <> eInfo + (m', conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m conn msgMeta msgBody chatMsg + case event of + XMsgNew mc -> memberCanSend m' $ newGroupContentMessage gInfo m' mc msg brokerTs False + XMsgFileDescr sharedMsgId fileDescr -> memberCanSend m' $ groupMessageFileDescription gInfo m' sharedMsgId fileDescr + XMsgUpdate sharedMsgId mContent mentions ttl live -> memberCanSend m' $ groupMessageUpdate gInfo m' sharedMsgId mContent mentions msg brokerTs ttl live + XMsgDel sharedMsgId memberId -> groupMessageDelete gInfo m' sharedMsgId memberId msg brokerTs + XMsgReact sharedMsgId (Just memberId) reaction add -> groupMsgReaction gInfo m' sharedMsgId memberId reaction add msg brokerTs + -- TODO discontinue XFile + XFile fInv -> processGroupFileInvitation' gInfo m' fInv msg brokerTs + XFileCancel sharedMsgId -> xFileCancelGroup gInfo m' sharedMsgId + XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInvGroup gInfo m' sharedMsgId fileConnReq_ fName + XInfo p -> xInfoMember gInfo m' p brokerTs + XGrpLinkMem p -> xGrpLinkMem gInfo m' conn' p + XGrpLinkAcpt role -> xGrpLinkAcpt gInfo m' role + XGrpMemNew memInfo -> xGrpMemNew gInfo m' memInfo msg brokerTs + XGrpMemIntro memInfo memRestrictions_ -> xGrpMemIntro gInfo m' memInfo memRestrictions_ + XGrpMemInv memId introInv -> xGrpMemInv gInfo m' memId introInv + XGrpMemFwd memInfo introInv -> xGrpMemFwd gInfo m' memInfo introInv + XGrpMemRole memId memRole -> xGrpMemRole gInfo m' memId memRole msg brokerTs + XGrpMemRestrict memId memRestrictions -> xGrpMemRestrict gInfo m' memId memRestrictions msg brokerTs + XGrpMemCon memId -> xGrpMemCon gInfo m' memId + XGrpMemDel memId withMessages -> xGrpMemDel gInfo m' memId withMessages msg brokerTs + XGrpLeave -> xGrpLeave gInfo m' msg brokerTs + XGrpDel -> xGrpDel gInfo m' msg brokerTs + XGrpInfo p' -> xGrpInfo gInfo m' p' msg brokerTs + XGrpPrefs ps' -> xGrpPrefs gInfo m' ps' + XGrpDirectInv connReq mContent_ -> memberCanSend m' $ xGrpDirectInv gInfo m' conn' connReq mContent_ msg brokerTs + XGrpMsgForward memberId msg' msgTs -> xGrpMsgForward gInfo m' memberId msg' msgTs + XInfoProbe probe -> xInfoProbe (COMGroupMember m') probe + XInfoProbeCheck probeHash -> xInfoProbeCheck (COMGroupMember m') probeHash + XInfoProbeOk probe -> xInfoProbeOk (COMGroupMember m') probe + BFileChunk sharedMsgId chunk -> bFileChunkGroup gInfo sharedMsgId chunk msgMeta + _ -> messageError $ "unsupported message: " <> tshow event + checkSendRcpt :: [AChatMessage] -> CM Bool + checkSendRcpt aMsgs = do + currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo + let GroupInfo {chatSettings = ChatSettings {sendRcpts}} = gInfo + pure $ + fromMaybe (sendRcptsSmallGroups user) sendRcpts + && any aChatMsgHasReceipt aMsgs + && currentMemCount <= smallGroupsRcptsMemLimit + where + aChatMsgHasReceipt (ACMsg _ ChatMessage {chatMsgEvent}) = + hasDeliveryReceipt (toCMEventTag chatMsgEvent) + forwardMsgs :: [AChatMessage] -> CM () + forwardMsgs aMsgs = do + let GroupMember {memberRole = membershipMemRole} = membership + when (membershipMemRole >= GRAdmin && not (blockedByAdmin m)) $ do + let forwardedMsgs = mapMaybe (\(ACMsg _ chatMsg) -> forwardedGroupMsg chatMsg) aMsgs + forM_ (L.nonEmpty forwardedMsgs) $ \forwardedMsgs' -> do + ChatConfig {highlyAvailable} <- asks config + -- members introduced to this invited member + introducedMembers <- + if memberCategory m == GCInviteeMember + then withStore' $ \db -> getForwardIntroducedMembers db vr user m highlyAvailable + else pure [] + -- invited members to which this member was introduced + invitedMembers <- withStore' $ \db -> getForwardInvitedMembers db vr user m highlyAvailable + let GroupMember {memberId} = m + ms = forwardedToGroupMembers (introducedMembers <> invitedMembers) forwardedMsgs' + events = L.map (\cm -> XGrpMsgForward memberId cm brokerTs) forwardedMsgs' + unless (null ms) $ void $ sendGroupMessages user gInfo ms events + RCVD msgMeta msgRcpt -> + withAckMessage' "group rcvd" agentConnId msgMeta $ + groupMsgReceived gInfo m conn msgMeta msgRcpt + SENT msgId proxy -> do + continued <- continueSending connEntity conn + sentMsgDeliveryEvent conn msgId + checkSndInlineFTComplete conn msgId + updateGroupItemsStatus gInfo m conn msgId GSSSent (Just $ isJust proxy) + when continued $ sendPendingGroupMessages user m conn + SWITCH qd phase cStats -> do + toView $ CEvtGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats) + when (phase == SPStarted || phase == SPCompleted) $ case qd of + QDRcv -> createInternalChatItem user (CDGroupSnd gInfo) (CISndConnEvent . SCESwitchQueue phase . Just $ groupMemberRef m) Nothing + QDSnd -> createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing + RSYNC rss cryptoErr_ cStats -> + case (rss, connectionCode, cryptoErr_) of + (RSRequired, _, Just cryptoErr) -> processErr cryptoErr + (RSAllowed, _, Just cryptoErr) -> processErr cryptoErr + (RSAgreed, Just _, _) -> do + withStore' $ \db -> setConnectionVerified db user connId Nothing + let m' = m {activeConn = Just (conn {connectionCode = Nothing} :: Connection)} :: GroupMember + ratchetSyncEventItem m' + toViewTE $ TEGroupMemberVerificationReset user gInfo m' + createInternalChatItem user (CDGroupRcv gInfo m') (CIRcvConnEvent RCEVerificationCodeReset) Nothing + _ -> ratchetSyncEventItem m + where + processErr cryptoErr = do + let e@(mde, n) = agentMsgDecryptError cryptoErr + ci_ <- withStore $ \db -> + getGroupMemberChatItemLast db user groupId (groupMemberId' m) + >>= liftIO + . mapM (\(ci, content') -> updateGroupChatItem db user groupId ci content' False False Nothing) + . mdeUpdatedCI e + case ci_ of + Just ci -> toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci) + _ -> do + toView $ CEvtGroupMemberRatchetSync user gInfo m (RatchetSyncProgress rss cStats) + createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvDecryptionError mde n) Nothing + ratchetSyncEventItem m' = do + toView $ CEvtGroupMemberRatchetSync user gInfo m' (RatchetSyncProgress rss cStats) + createInternalChatItem user (CDGroupRcv gInfo m') (CIRcvConnEvent $ RCERatchetSync rss) Nothing + OK -> + -- [async agent commands] continuation on receiving OK + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + JOINED sqSecured -> + -- [async agent commands] continuation on receiving JOINED + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> + when (sqSecured && connChatVersion >= batchSend2Version) sendGroupAutoReply + QCONT -> do + continued <- continueSending connEntity conn + when continued $ sendPendingGroupMessages user m conn + MWARN msgId err -> do + withStore' $ \db -> updateGroupItemsErrorStatus db msgId (groupMemberId' m) (GSSWarning $ agentSndError err) + processConnMWARN connEntity conn err + MERR msgId err -> do + withStore' $ \db -> updateGroupItemsErrorStatus db msgId (groupMemberId' m) (GSSError $ agentSndError err) + -- group errors are silenced to reduce load on UI event log + -- eToView (ChatErrorAgent err $ Just connEntity) + processConnMERR connEntity conn err + MERRS msgIds err -> do + let newStatus = GSSError $ agentSndError err + -- error cannot be AUTH error here + withStore' $ \db -> forM_ msgIds $ \msgId -> + updateGroupItemsErrorStatus db msgId (groupMemberId' m) newStatus `catchAll_` pure () + eToView (ChatErrorAgent err $ Just connEntity) + ERR err -> do + eToView (ChatErrorAgent err $ Just connEntity) + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + -- TODO add debugging output + _ -> pure () + where + updateGroupItemsErrorStatus :: DB.Connection -> AgentMsgId -> GroupMemberId -> GroupSndStatus -> IO () + updateGroupItemsErrorStatus db msgId groupMemberId newStatus = do + itemIds <- getChatItemIdsByAgentMsgId db connId msgId + forM_ itemIds $ \itemId -> updateGroupMemSndStatus' db itemId groupMemberId newStatus + sendGroupAutoReply = autoReplyMC >>= mapM_ send + where + autoReplyMC = do + let GroupInfo {businessChat} = gInfo + GroupMember {memberId = joiningMemberId} = m + case businessChat of + Just BusinessChatInfo {customerId, chatType = BCCustomer} + | joiningMemberId == customerId -> useReply <$> withStore (`getUserAddress` user) + where + useReply UserContactLink {autoAccept} = case autoAccept of + Just AutoAccept {businessAddress, autoReply} | businessAddress -> autoReply + _ -> Nothing + _ -> pure Nothing + send mc = do + msg <- sendGroupMessage' user gInfo [m] (XMsgNew $ MCSimple (extMsgContent mc Nothing)) + ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndMsgContent mc) + withStore' $ \db -> createGroupSndStatus db (chatItemId' ci) (groupMemberId' m) GSSNew + toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci] + + agentMsgDecryptError :: AgentCryptoError -> (MsgDecryptError, Word32) + agentMsgDecryptError = \case + DECRYPT_AES -> (MDEOther, 1) + DECRYPT_CB -> (MDEOther, 1) + RATCHET_HEADER -> (MDERatchetHeader, 1) + RATCHET_EARLIER _ -> (MDERatchetEarlier, 1) + RATCHET_SKIPPED n -> (MDETooManySkipped, n) + RATCHET_SYNC -> (MDERatchetSync, 0) + + mdeUpdatedCI :: (MsgDecryptError, Word32) -> CChatItem c -> Maybe (ChatItem c 'MDRcv, CIContent 'MDRcv) + mdeUpdatedCI (mde', n') (CChatItem _ ci@ChatItem {content = CIRcvDecryptionError mde n}) + | mde == mde' = case mde of + MDERatchetHeader -> r (n + n') + MDETooManySkipped -> r n' -- the numbers are not added as sequential MDETooManySkipped will have it incremented by 1 + MDERatchetEarlier -> r (n + n') + MDEOther -> r (n + n') + MDERatchetSync -> r 0 + | otherwise = Nothing + where + r n'' = Just (ci, CIRcvDecryptionError mde n'') + mdeUpdatedCI _ _ = Nothing + + processSndFileConn :: AEvent e -> ConnectionEntity -> Connection -> SndFileTransfer -> CM () + processSndFileConn agentMsg connEntity conn ft@SndFileTransfer {fileId, fileName, fileStatus} = + case agentMsg of + -- SMP CONF for SndFileConnection happens for direct file protocol + -- when recipient of the file "joins" connection created by the sender + CONF confId _pqSupport _ connInfo -> do + ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo + conn' <- updatePeerChatVRange conn chatVRange + case chatMsgEvent of + -- TODO save XFileAcpt message + XFileAcpt name + | name == fileName -> do + withStore' $ \db -> updateSndFileStatus db ft FSAccepted + -- [async agent commands] no continuation needed, but command should be asynchronous for stability + allowAgentConnectionAsync user conn' confId XOk + | otherwise -> messageError "x.file.acpt: fileName is different from expected" + _ -> messageError "CONF from file connection must have x.file.acpt" + CON _ -> do + ci <- withStore $ \db -> do + liftIO $ updateSndFileStatus db ft FSConnected + updateDirectCIFileStatus db vr user fileId $ CIFSSndTransfer 0 1 + toView $ CEvtSndFileStart user ci ft + sendFileChunk user ft + SENT msgId _proxy -> do + withStore' $ \db -> updateSndFileChunkSent db ft msgId + unless (fileStatus == FSCancelled) $ sendFileChunk user ft + MERR _ err -> do + cancelSndFileTransfer user ft True >>= mapM_ deleteAgentConnectionAsync + case err of + SMP _ SMP.AUTH -> unless (fileStatus == FSCancelled) $ do + ci <- withStore $ \db -> do + liftIO (lookupChatRefByFileId db user fileId) >>= \case + Just (ChatRef CTDirect _) -> liftIO $ updateFileCancelled db user fileId CIFSSndCancelled + _ -> pure () + lookupChatItemByFileId db vr user fileId + toView $ CEvtSndFileRcvCancelled user ci ft + _ -> throwChatError $ CEFileSend fileId err + MSG meta _ _ -> + withAckMessage' "file msg" agentConnId meta $ pure () + OK -> + -- [async agent commands] continuation on receiving OK + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + JOINED _ -> + -- [async agent commands] continuation on receiving JOINED + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + ERR err -> do + eToView (ChatErrorAgent err $ Just connEntity) + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + -- TODO add debugging output + _ -> pure () + + processRcvFileConn :: AEvent e -> ConnectionEntity -> Connection -> RcvFileTransfer -> CM () + processRcvFileConn agentMsg connEntity conn ft@RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}, grpMemberId} = + case agentMsg of + INV (ACR _ cReq) -> + withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} -> + case cReq of + fileInvConnReq@(CRInvitationUri _ _) -> case cmdFunction of + -- [async agent commands] direct XFileAcptInv continuation on receiving INV + CFCreateConnFileInvDirect -> do + ct <- withStore $ \db -> getContactByFileId db vr user fileId + sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId + void $ sendDirectContactMessage user ct (XFileAcptInv sharedMsgId (Just fileInvConnReq) fileName) + -- [async agent commands] group XFileAcptInv continuation on receiving INV + CFCreateConnFileInvGroup -> case grpMemberId of + Just gMemberId -> do + GroupMember {groupId, activeConn} <- withStore $ \db -> getGroupMemberById db vr user gMemberId + case activeConn of + Just gMemberConn -> do + sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId + void $ sendDirectMemberMessage gMemberConn (XFileAcptInv sharedMsgId (Just fileInvConnReq) fileName) groupId + _ -> throwChatError $ CECommandError "no GroupMember activeConn" + _ -> throwChatError $ CECommandError "no grpMemberId" + _ -> throwChatError $ CECommandError "unexpected cmdFunction" + CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" + -- SMP CONF for RcvFileConnection happens for group file protocol + -- when sender of the file "joins" connection created by the recipient + -- (sender doesn't create connections for all group members) + CONF confId _pqSupport _ connInfo -> do + ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo + conn' <- updatePeerChatVRange conn chatVRange + case chatMsgEvent of + XOk -> allowAgentConnectionAsync user conn' confId XOk -- [async agent commands] no continuation needed, but command should be asynchronous for stability + _ -> pure () + CON _ -> startReceivingFile user fileId + MSG meta _ msgBody -> do + -- XXX: not all branches do ACK + parseFileChunk msgBody >>= receiveFileChunk ft (Just conn) meta + OK -> + -- [async agent commands] continuation on receiving OK + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + JOINED _ -> + -- [async agent commands] continuation on receiving JOINED + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + MERR _ err -> do + eToView (ChatErrorAgent err $ Just connEntity) + processConnMERR connEntity conn err + ERR err -> do + eToView (ChatErrorAgent err $ Just connEntity) + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + -- TODO add debugging output + _ -> pure () + + receiveFileChunk :: RcvFileTransfer -> Maybe Connection -> MsgMeta -> FileChunk -> CM () + receiveFileChunk ft@RcvFileTransfer {fileId, chunkSize} conn_ meta@MsgMeta {recipient = (msgId, _), integrity} = \case + FileChunkCancel -> + unless (rcvFileCompleteOrCancelled ft) $ do + cancelRcvFileTransfer user ft >>= mapM_ deleteAgentConnectionAsync + ci <- withStore $ \db -> getChatItemByFileId db vr user fileId + toView $ CEvtRcvFileSndCancelled user ci ft + FileChunk {chunkNo, chunkBytes = chunk} -> do + case integrity of + MsgOk -> pure () + MsgError MsgDuplicate -> pure () -- TODO remove once agent removes duplicates + MsgError e -> + badRcvFileChunk ft $ "invalid file chunk number " <> show chunkNo <> ": " <> show e + withStore' (\db -> createRcvFileChunk db ft chunkNo msgId) >>= \case + RcvChunkOk -> + if B.length chunk /= fromInteger chunkSize + then badRcvFileChunk ft "incorrect chunk size" + else withAckMessage' "file msg" agentConnId meta $ appendFileChunk ft chunkNo chunk False + RcvChunkFinal -> + if B.length chunk > fromInteger chunkSize + then badRcvFileChunk ft "incorrect chunk size" + else do + appendFileChunk ft chunkNo chunk True + ci <- withStore $ \db -> do + liftIO $ do + updateRcvFileStatus db fileId FSComplete + updateCIFileStatus db user fileId CIFSRcvComplete + deleteRcvFileChunks db ft + getChatItemByFileId db vr user fileId + toView $ CEvtRcvFileComplete user ci + mapM_ (deleteAgentConnectionAsync . aConnId) conn_ + RcvChunkDuplicate -> withAckMessage' "file msg" agentConnId meta $ pure () + RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo + + processUserContactRequest :: AEvent e -> ConnectionEntity -> Connection -> UserContact -> CM () + processUserContactRequest agentMsg connEntity conn UserContact {userContactLinkId} = case agentMsg of + REQ invId pqSupport _ connInfo -> do + ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo + case chatMsgEvent of + XContact p xContactId_ -> profileContactRequest invId chatVRange p xContactId_ pqSupport + XInfo p -> profileContactRequest invId chatVRange p Nothing pqSupport + -- TODO show/log error, other events in contact request + _ -> pure () + MERR _ err -> do + eToView (ChatErrorAgent err $ Just connEntity) + processConnMERR connEntity conn err + ERR err -> do + eToView (ChatErrorAgent err $ Just connEntity) + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + -- TODO add debugging output + _ -> pure () + where + profileContactRequest :: InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> PQSupport -> CM () + profileContactRequest invId chatVRange p@Profile {displayName} xContactId_ reqPQSup = do + withStore (\db -> createOrUpdateContactRequest db vr user userContactLinkId invId chatVRange p xContactId_ reqPQSup) >>= \case + CORContact contact -> toView $ CEvtContactRequestAlreadyAccepted user contact + CORGroup gInfo -> toView $ CEvtBusinessRequestAlreadyAccepted user gInfo + CORRequest cReq -> do + ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId + let (UserContactLink {connLinkContact = CCLink connReq _, autoAccept}, gLinkInfo_) = ucl + isSimplexTeam = sameConnReqContact connReq adminContactReq + v = maxVersion chatVRange + case autoAccept of + Just AutoAccept {acceptIncognito, businessAddress} + | businessAddress -> + if isSimplexTeam && v < businessChatsVersion + then do + ct <- acceptContactRequestAsync user cReq Nothing reqPQSup + toView $ CEvtAcceptingContactRequest user ct + else do + gInfo <- acceptBusinessJoinRequestAsync user cReq + toView $ CEvtAcceptingBusinessRequest user gInfo + | otherwise -> case gLinkInfo_ of + Nothing -> do + -- [incognito] generate profile to send, create connection with incognito profile + incognitoProfile <- if acceptIncognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing + ct <- acceptContactRequestAsync user cReq incognitoProfile reqPQSup + toView $ CEvtAcceptingContactRequest user ct + Just gli@GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do + gInfo <- withStore $ \db -> getGroupInfo db vr user groupId + acceptMember_ <- asks $ acceptMember . chatHooks . config + maybe (pure $ Right (GAAccepted, gLinkMemRole)) (\am -> liftIO $ am gInfo gli p) acceptMember_ >>= \case + Right (acceptance, useRole) + | v < groupFastLinkJoinVersion -> + messageError "processUserContactRequest: chat version range incompatible for accepting group join request" + | otherwise -> do + let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo + mem <- acceptGroupJoinRequestAsync user gInfo cReq acceptance useRole profileMode + createInternalChatItem user (CDGroupRcv gInfo mem) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing + toView $ CEvtAcceptingGroupJoinRequestMember user gInfo mem + Left rjctReason + | v < groupJoinRejectVersion -> + messageWarning $ "processUserContactRequest (group " <> groupName' gInfo <> "): joining of " <> displayName <> " is blocked" + | otherwise -> do + mem <- acceptGroupJoinSendRejectAsync user gInfo cReq rjctReason + toViewTE $ TERejectingGroupJoinRequestMember user gInfo mem rjctReason + _ -> toView $ CEvtReceivedContactRequest user cReq + + -- TODO [knocking] review + memberCanSend :: GroupMember -> CM () -> CM () + memberCanSend GroupMember {memberRole, memberStatus} a + | memberRole > GRObserver || memberStatus == GSMemPendingApproval = a + | otherwise = messageError "member is not allowed to send messages" + + processConnMERR :: ConnectionEntity -> Connection -> AgentErrorType -> CM () + processConnMERR connEntity conn err = do + case err of + SMP _ SMP.AUTH -> do + authErrCounter' <- withStore' $ \db -> incAuthErrCounter db user conn + when (authErrCounter' >= authErrDisableCount) $ case connEntity of + RcvDirectMsgConnection ctConn (Just ct) -> do + toView $ CEvtContactDisabled user ct {activeConn = Just ctConn {authErrCounter = authErrCounter'}} + _ -> toView $ CEvtConnectionDisabled connEntity + SMP _ SMP.QUOTA -> + unless (connInactive conn) $ do + withStore' $ \db -> setQuotaErrCounter db user conn quotaErrSetOnMERR + toView $ CEvtConnectionInactive connEntity True + _ -> pure () + + processConnMWARN :: ConnectionEntity -> Connection -> AgentErrorType -> CM () + processConnMWARN connEntity conn err = do + case err of + SMP _ SMP.QUOTA -> + unless (connInactive conn) $ do + quotaErrCounter' <- withStore' $ \db -> incQuotaErrCounter db user conn + when (quotaErrCounter' >= quotaErrInactiveCount) $ + toView $ CEvtConnectionInactive connEntity True + _ -> pure () + + continueSending :: ConnectionEntity -> Connection -> CM Bool + continueSending connEntity conn = + if connInactive conn + then do + withStore' $ \db -> setQuotaErrCounter db user conn 0 + toView $ CEvtConnectionInactive connEntity False + pure True + else pure False + + -- TODO v5.7 / v6.0 - together with deprecating old group protocol establishing direct connections? + -- we could save command records only for agent APIs we process continuations for (INV) + withCompletedCommand :: forall e. AEntityI e => Connection -> AEvent e -> (CommandData -> CM ()) -> CM () + withCompletedCommand Connection {connId} agentMsg action = do + let agentMsgTag = AEvtTag (sAEntity @e) $ aEventTag agentMsg + cmdData_ <- withStore' $ \db -> getCommandDataByCorrId db user corrId + case cmdData_ of + Just cmdData@CommandData {cmdId, cmdConnId = Just cmdConnId', cmdFunction} + | connId == cmdConnId' && (agentMsgTag == commandExpectedResponse cmdFunction || agentMsgTag == AEvtTag SAEConn ERR_) -> do + withStore' $ \db -> deleteCommand db user cmdId + action cmdData + | otherwise -> err cmdId $ "not matching connection id or unexpected response, corrId = " <> show corrId + Just CommandData {cmdId, cmdConnId = Nothing} -> err cmdId $ "no command connection id, corrId = " <> show corrId + Nothing -> throwChatError . CEAgentCommandError $ "command not found, corrId = " <> show corrId + where + err cmdId msg = do + withStore' $ \db -> updateCommandStatus db user cmdId CSError + throwChatError . CEAgentCommandError $ msg + + withAckMessage' :: Text -> ConnId -> MsgMeta -> CM () -> CM () + withAckMessage' label cId msgMeta action = do + withAckMessage label cId msgMeta False Nothing $ \_ -> action $> False + + withAckMessage :: Text -> ConnId -> MsgMeta -> Bool -> Maybe (TVar [Text]) -> (Text -> CM Bool) -> CM () + withAckMessage label cId msgMeta showCritical tags action = do + -- [async agent commands] command should be asynchronous + -- TODO catching error and sending ACK after an error, particularly if it is a database error, will result in the message not processed (and no notification to the user). + -- Possible solutions are: + -- 1) retry processing several times + -- 2) stabilize database + -- 3) show screen of death to the user asking to restart + eInfo <- eventInfo + logInfo $ label <> ": " <> eInfo + tryChatError (action eInfo) >>= \case + Right withRcpt -> + withLog (eInfo <> " ok") $ ackMsg msgMeta $ if withRcpt then Just "" else Nothing + -- If showCritical is True, then these errors don't result in ACK and show user visible alert + -- This prevents losing the message that failed to be processed. + Left (ChatErrorStore SEDBBusyError {message}) | showCritical -> throwError $ ChatErrorAgent (CRITICAL True message) Nothing + Left e -> do + withLog (eInfo <> " error: " <> tshow e) $ ackMsg msgMeta Nothing + throwError e + where + eventInfo = do + v <- asks eventSeq + eId <- atomically $ stateTVar v $ \i -> (i + 1, i + 1) + pure $ "conn_id=" <> tshow cId <> " event_id=" <> tshow eId + withLog eInfo' ack = do + ts <- showTags + logInfo $ T.unwords [label, "ack:", ts, eInfo'] + ack + logInfo $ T.unwords [label, "ack=success:", ts, eInfo'] + showTags = do + ts <- maybe (pure []) readTVarIO tags + pure $ case ts of + [] -> "no_chat_messages" + [t] -> "chat_message=" <> t + _ -> "chat_message_batch=" <> T.intercalate "," (reverse ts) + ackMsg :: MsgMeta -> Maybe MsgReceiptInfo -> CM () + ackMsg MsgMeta {recipient = (msgId, _)} rcpt = withAgent $ \a -> ackMessageAsync a "" cId msgId rcpt + + sentMsgDeliveryEvent :: Connection -> AgentMsgId -> CM () + sentMsgDeliveryEvent Connection {connId} msgId = + withStore' $ \db -> updateSndMsgDeliveryStatus db connId msgId MDSSndSent + + agentSndError :: AgentErrorType -> SndError + agentSndError = \case + SMP _ AUTH -> SndErrAuth + SMP _ QUOTA -> SndErrQuota + BROKER _ e -> brokerError SndErrRelay e + SMP proxySrv (SMP.PROXY (SMP.BROKER e)) -> brokerError (SndErrProxy proxySrv) e + AP.PROXY proxySrv _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> brokerError (SndErrProxyRelay proxySrv) e + e -> SndErrOther $ tshow e + where + brokerError srvErr = \case + NETWORK -> SndErrExpired + TIMEOUT -> SndErrExpired + HOST -> srvErr SrvErrHost + SMP.TRANSPORT TEVersion -> srvErr SrvErrVersion + e -> srvErr . SrvErrOther $ tshow e + + badRcvFileChunk :: RcvFileTransfer -> String -> CM () + badRcvFileChunk ft err = + unless (rcvFileCompleteOrCancelled ft) $ do + cancelRcvFileTransfer user ft >>= mapM_ deleteAgentConnectionAsync + throwChatError $ CEFileRcvChunk err + + memberConnectedChatItem :: GroupInfo -> GroupMember -> CM () + memberConnectedChatItem gInfo m = + -- ts should be broker ts but we don't have it for CON + createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvGroupEvent RGEMemberConnected) Nothing + + groupDescriptionChatItem :: GroupInfo -> GroupMember -> Text -> CM () + groupDescriptionChatItem gInfo m descr = + createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvMsgContent $ MCText descr) Nothing + + notifyMemberConnected :: GroupInfo -> GroupMember -> Maybe Contact -> CM () + notifyMemberConnected gInfo m ct_ = do + memberConnectedChatItem gInfo m + lift $ mapM_ (`setContactNetworkStatus` NSConnected) ct_ + toView $ CEvtConnectedToGroupMember user gInfo m ct_ + + probeMatchingContactsAndMembers :: Contact -> IncognitoEnabled -> Bool -> CM () + probeMatchingContactsAndMembers ct connectedIncognito doProbeContacts = do + gVar <- asks random + contactMerge <- readTVarIO =<< asks contactMergeEnabled + if contactMerge && not connectedIncognito + then do + (probe, probeId) <- withStore $ \db -> createSentProbe db gVar userId (COMContact ct) + -- ! when making changes to probe-and-merge mechanism, + -- ! test scenario in which recipient receives probe after probe hashes (not covered in tests): + -- sendProbe -> sendProbeHashes (currently) + -- sendProbeHashes -> sendProbe (reversed - change order in code, may add delay) + sendProbe probe + cs <- + if doProbeContacts + then map COMContact <$> withStore' (\db -> getMatchingContacts db vr user ct) + else pure [] + ms <- map COMGroupMember <$> withStore' (\db -> getMatchingMembers db vr user ct) + sendProbeHashes (cs <> ms) probe probeId + else sendProbe . Probe =<< liftIO (encodedRandomBytes gVar 32) + where + sendProbe :: Probe -> CM () + sendProbe probe = void . sendDirectContactMessage user ct $ XInfoProbe probe + + probeMatchingMemberContact :: GroupMember -> IncognitoEnabled -> CM () + probeMatchingMemberContact GroupMember {activeConn = Nothing} _ = pure () + probeMatchingMemberContact m@GroupMember {groupId, activeConn = Just conn} connectedIncognito = do + gVar <- asks random + contactMerge <- readTVarIO =<< asks contactMergeEnabled + if contactMerge && not connectedIncognito + then do + (probe, probeId) <- withStore $ \db -> createSentProbe db gVar userId $ COMGroupMember m + sendProbe probe + cs <- map COMContact <$> withStore' (\db -> getMatchingMemberContacts db vr user m) + sendProbeHashes cs probe probeId + else sendProbe . Probe =<< liftIO (encodedRandomBytes gVar 32) + where + sendProbe :: Probe -> CM () + sendProbe probe = void $ sendDirectMemberMessage conn (XInfoProbe probe) groupId + + sendProbeHashes :: [ContactOrMember] -> Probe -> Int64 -> CM () + sendProbeHashes cgms probe probeId = + forM_ cgms $ \cgm -> sendProbeHash cgm `catchChatError` \_ -> pure () + where + probeHash = ProbeHash $ C.sha256Hash (unProbe probe) + sendProbeHash :: ContactOrMember -> CM () + sendProbeHash cgm@(COMContact c) = do + void . sendDirectContactMessage user c $ XInfoProbeCheck probeHash + withStore' $ \db -> createSentProbeHash db userId probeId cgm + sendProbeHash (COMGroupMember GroupMember {activeConn = Nothing}) = pure () + sendProbeHash cgm@(COMGroupMember m@GroupMember {groupId, activeConn = Just conn}) = + when (memberCurrent m) $ do + void $ sendDirectMemberMessage conn (XInfoProbeCheck probeHash) groupId + withStore' $ \db -> createSentProbeHash db userId probeId cgm + + messageWarning :: Text -> CM () + messageWarning = toView . CEvtMessageError user "warning" + + messageError :: Text -> CM () + messageError = toView . CEvtMessageError user "error" + + newContentMessage :: Contact -> MsgContainer -> RcvMessage -> MsgMeta -> CM () + newContentMessage ct mc msg@RcvMessage {sharedMsgId_} msgMeta = do + let ExtMsgContent content _ fInv_ _ _ = mcExtMsgContent mc + -- Uncomment to test stuck delivery on errors - see test testDirectMessageDelete + -- case content of + -- MCText "hello 111" -> + -- UE.throwIO $ userError "#####################" + -- -- throwChatError $ CECommandError "#####################" + -- _ -> pure () + if isVoice content && not (featureAllowed SCFVoice forContact ct) + then do + void $ newChatItem (ciContentNoParse $ CIRcvChatFeatureRejected CFVoice) Nothing Nothing False + else do + let ExtMsgContent _ _ _ itemTTL live_ = mcExtMsgContent mc + timed_ = rcvContactCITimed ct itemTTL + live = fromMaybe False live_ + file_ <- processFileInvitation fInv_ content $ \db -> createRcvFileTransfer db userId ct + newChatItem (CIRcvMsgContent content, msgContentTexts content) (snd <$> file_) timed_ live + autoAcceptFile file_ + where + brokerTs = metaBrokerTs msgMeta + newChatItem content ciFile_ timed_ live = do + ci <- saveRcvChatItem' user (CDDirectRcv ct) Nothing msg sharedMsgId_ brokerTs content ciFile_ timed_ live M.empty + reactions <- maybe (pure []) (\sharedMsgId -> withStore' $ \db -> getDirectCIReactions db ct sharedMsgId) sharedMsgId_ + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci {reactions}] + + autoAcceptFile :: Maybe (RcvFileTransfer, CIFile 'MDRcv) -> CM () + autoAcceptFile = mapM_ $ \(ft, CIFile {fileSize}) -> do + -- ! autoAcceptFileSize is only used in tests + ChatConfig {autoAcceptFileSize = sz} <- asks config + when (sz > fileSize) $ receiveFileEvt' user ft False Nothing Nothing >>= toView + + messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> CM () + messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr = do + fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId + processFDMessage (CDDirectRcv ct) sharedMsgId fileId fileDescr + + groupMessageFileDescription :: GroupInfo -> GroupMember -> SharedMsgId -> FileDescr -> CM () + groupMessageFileDescription g@GroupInfo {groupId} m sharedMsgId fileDescr = do + fileId <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId + processFDMessage (CDGroupRcv g m) sharedMsgId fileId fileDescr + + processFDMessage :: ChatTypeQuotable c => ChatDirection c 'MDRcv -> SharedMsgId -> FileTransferId -> FileDescr -> CM () + processFDMessage cd sharedMsgId fileId fileDescr = do + ft <- withStore $ \db -> getRcvFileTransfer db user fileId + unless (rcvFileCompleteOrCancelled ft) $ do + (rfd@RcvFileDescr {fileDescrComplete}, ft'@RcvFileTransfer {fileStatus, xftpRcvFile, cryptoArgs}) <- withStore $ \db -> do + rfd <- appendRcvFD db userId fileId fileDescr + -- reading second time in the same transaction as appending description + -- to prevent race condition with accept + ft' <- getRcvFileTransfer db user fileId + pure (rfd, ft') + when fileDescrComplete $ do + ci <- withStore $ \db -> getAChatItemBySharedMsgId db user cd sharedMsgId + toView $ CEvtRcvFileDescrReady user ci ft' rfd + case (fileStatus, xftpRcvFile) of + (RFSAccepted _, Just XFTPRcvFile {userApprovedRelays}) -> receiveViaCompleteFD user fileId rfd userApprovedRelays cryptoArgs + _ -> pure () + + processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer) -> CM (Maybe (RcvFileTransfer, CIFile 'MDRcv)) + processFileInvitation fInv_ mc createRcvFT = forM fInv_ $ \fInv' -> do + ChatConfig {fileChunkSize} <- asks config + let fInv@FileInvitation {fileName, fileSize} = mkValidFileInvitation fInv' + inline <- receiveInlineMode fInv (Just mc) fileChunkSize + ft@RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFT db fInv inline fileChunkSize + let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP + (filePath, fileStatus, ft') <- case inline of + Just IFMSent -> do + encrypt <- chatReadVar encryptLocalFiles + ft' <- (if encrypt then setFileToEncrypt else pure) ft + fPath <- getRcvFilePath fileId Nothing fileName True + withStore' $ \db -> startRcvInlineFT db user ft' fPath inline + pure (Just fPath, CIFSRcvAccepted, ft') + _ -> pure (Nothing, CIFSRcvInvitation, ft) + let RcvFileTransfer {cryptoArgs} = ft' + fileSource = (`CryptoFile` cryptoArgs) <$> filePath + pure (ft', CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol}) + + mkValidFileInvitation :: FileInvitation -> FileInvitation + mkValidFileInvitation fInv@FileInvitation {fileName} = fInv {fileName = FP.makeValid $ FP.takeFileName fileName} + + messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> CM () + messageUpdate ct@Contact {contactId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do + updateRcvChatItem `catchCINotFound` \_ -> do + -- 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 <- saveRcvChatItem' user (CDDirectRcv ct) Nothing 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 (DirectChat ct) ci') + where + brokerTs = metaBrokerTs msgMeta + content = CIRcvMsgContent mc + live = fromMaybe False live_ + updateRcvChatItem = do + cci <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId + case cci of + CChatItem SMDRcv ci@ChatItem {meta = CIMeta {itemForwarded, itemLive}, content = CIRcvMsgContent oldMC} + | isNothing itemForwarded -> do + let changed = mc /= oldMC + if changed || fromMaybe False itemLive + then do + ci' <- withStore' $ \db -> do + when changed $ + addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc) + reactions <- getDirectCIReactions db ct sharedMsgId + let edited = itemLive /= Just True + updateDirectChatItem' db user contactId ci {reactions} content edited live Nothing $ Just msgId + toView $ CEvtChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') + startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' + else toView $ CEvtChatItemNotChanged user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) + _ -> messageError "x.msg.update: contact attempted invalid message update" + + messageDelete :: Contact -> SharedMsgId -> RcvMessage -> MsgMeta -> CM () + messageDelete ct@Contact {contactId} sharedMsgId _rcvMessage msgMeta = do + deleteRcvChatItem `catchCINotFound` (toView . CEvtChatItemDeletedNotFound user ct) + where + brokerTs = metaBrokerTs msgMeta + deleteRcvChatItem = do + cci@(CChatItem msgDir ci) <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId + case msgDir of + SMDRcv + | rcvItemDeletable ci brokerTs -> do + deletions <- if featureAllowed SCFFullDelete forContact ct + then deleteDirectCIs user ct [cci] + else markDirectCIsDeleted user ct [cci] brokerTs + toView $ CEvtChatItemsDeleted user deletions False False + | otherwise -> messageError "x.msg.del: contact attempted invalid message delete" + SMDSnd -> messageError "x.msg.del: contact attempted invalid message delete" + + rcvItemDeletable :: ChatItem c d -> UTCTime -> Bool + rcvItemDeletable ChatItem {meta = CIMeta {itemTs, itemDeleted}} brokerTs = + -- 78 hours margin to account for possible sending delay + diffUTCTime brokerTs itemTs < (78 * 3600) && isNothing itemDeleted + + directMsgReaction :: Contact -> SharedMsgId -> MsgReaction -> Bool -> RcvMessage -> MsgMeta -> CM () + directMsgReaction ct sharedMsgId reaction add RcvMessage {msgId} MsgMeta {broker = (_, brokerTs)} = do + when (featureAllowed SCFReactions forContact ct) $ do + rs <- withStore' $ \db -> getDirectReactions db ct sharedMsgId False + when (reactionAllowed add reaction rs) $ do + updateChatItemReaction `catchCINotFound` \_ -> + withStore' $ \db -> setDirectReaction db ct sharedMsgId False reaction add msgId brokerTs + where + updateChatItemReaction = do + cEvt_ <- withStore $ \db -> do + CChatItem md ci <- getDirectChatItemBySharedMsgId db user (contactId' ct) sharedMsgId + if ciReactionAllowed ci + then liftIO $ do + setDirectReaction db ct sharedMsgId False reaction add msgId brokerTs + reactions <- getDirectCIReactions db ct sharedMsgId + let ci' = CChatItem md ci {reactions} + r = ACIReaction SCTDirect SMDRcv (DirectChat ct) $ CIReaction CIDirectRcv ci' brokerTs reaction + pure $ Just $ CEvtChatItemReaction user add r + else pure Nothing + mapM_ toView cEvt_ + + groupMsgReaction :: GroupInfo -> GroupMember -> SharedMsgId -> MemberId -> MsgReaction -> Bool -> RcvMessage -> UTCTime -> CM () + groupMsgReaction g m sharedMsgId itemMemberId reaction add RcvMessage {msgId} brokerTs = do + when (groupFeatureAllowed SGFReactions g) $ do + rs <- withStore' $ \db -> getGroupReactions db g m itemMemberId sharedMsgId False + when (reactionAllowed add reaction rs) $ do + updateChatItemReaction `catchCINotFound` \_ -> + withStore' $ \db -> setGroupReaction db g m itemMemberId sharedMsgId False reaction add msgId brokerTs + where + updateChatItemReaction = do + cEvt_ <- withStore $ \db -> do + CChatItem md ci <- getGroupMemberCIBySharedMsgId db user g itemMemberId sharedMsgId + if ciReactionAllowed ci + then liftIO $ do + setGroupReaction db g m itemMemberId sharedMsgId False reaction add msgId brokerTs + reactions <- getGroupCIReactions db g itemMemberId sharedMsgId + let ci' = CChatItem md ci {reactions} + r = ACIReaction SCTGroup SMDRcv (GroupChat g) $ CIReaction (CIGroupRcv m) ci' brokerTs reaction + pure $ Just $ CEvtChatItemReaction user add r + else pure Nothing + mapM_ toView cEvt_ + + reactionAllowed :: Bool -> MsgReaction -> [MsgReaction] -> Bool + reactionAllowed add reaction rs = (reaction `elem` rs) /= add && not (add && length rs >= maxMsgReactions) + + catchCINotFound :: CM a -> (SharedMsgId -> CM a) -> CM a + catchCINotFound f handle = + f `catchChatError` \case + ChatErrorStore (SEChatItemSharedMsgIdNotFound sharedMsgId) -> handle sharedMsgId + e -> throwError e + + newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContainer -> RcvMessage -> UTCTime -> Bool -> CM () + newGroupContentMessage gInfo m@GroupMember {memberId, memberRole} mc msg@RcvMessage {sharedMsgId_} brokerTs forwarded + | blockedByAdmin m = createBlockedByAdmin + | otherwise = case prohibitedGroupContent gInfo m content ft_ fInv_ False of + Just f -> rejected f + Nothing -> + withStore' (\db -> getCIModeration db vr user gInfo memberId sharedMsgId_) >>= \case + Just ciModeration -> do + applyModeration ciModeration + withStore' $ \db -> deleteCIModeration db gInfo memberId sharedMsgId_ + Nothing -> createContentItem + where + rejected f = newChatItem (ciContentNoParse $ CIRcvGroupFeatureRejected f) Nothing Nothing False + timed' = if forwarded then rcvCITimed_ (Just Nothing) itemTTL else rcvGroupCITimed gInfo itemTTL + live' = fromMaybe False live_ + ExtMsgContent content mentions fInv_ itemTTL live_ = mcExtMsgContent mc + ts@(_, ft_) = msgContentTexts content + saveRcvCI = saveRcvChatItem' user (CDGroupRcv gInfo m) (memberNotInHistory m) msg sharedMsgId_ brokerTs + createBlockedByAdmin + | groupFeatureAllowed SGFFullDelete gInfo = do + -- ignores member role when blocked by admin + ci <- saveRcvCI (ciContentNoParse CIRcvBlocked) Nothing timed' False M.empty + ci' <- withStore' $ \db -> updateGroupCIBlockedByAdmin db user gInfo ci brokerTs + groupMsgToView gInfo ci' + | otherwise = do + file_ <- processFileInv + ci <- createNonLive file_ + ci' <- withStore' $ \db -> markGroupCIBlockedByAdmin db user gInfo ci + groupMsgToView gInfo ci' + applyModeration CIModeration {moderatorMember = moderator@GroupMember {memberRole = moderatorRole}, moderatedAt} + | moderatorRole < GRModerator || moderatorRole < memberRole = + createContentItem + | groupFeatureMemberAllowed SGFFullDelete moderator gInfo = do + ci <- saveRcvCI (ciContentNoParse CIRcvModerated) Nothing timed' False M.empty + ci' <- withStore' $ \db -> updateGroupChatItemModerated db user gInfo ci moderator moderatedAt + groupMsgToView gInfo ci' + | otherwise = do + file_ <- processFileInv + ci <- createNonLive file_ + deletions <- markGroupCIsDeleted user gInfo [CChatItem SMDRcv ci] (Just moderator) moderatedAt + toView $ CEvtChatItemsDeleted user deletions False False + createNonLive file_ = + saveRcvCI (CIRcvMsgContent content, ts) (snd <$> file_) timed' False mentions + createContentItem = do + file_ <- processFileInv + newChatItem (CIRcvMsgContent content, ts) (snd <$> file_) timed' live' + when (showMessages $ memberSettings m) $ autoAcceptFile file_ + processFileInv = + processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId m + newChatItem ciContent ciFile_ timed_ live = do + let mentions' = if showMessages (memberSettings m) then mentions else [] + ci <- saveRcvCI ciContent ciFile_ timed_ live mentions' + ci' <- blockedMember m ci $ withStore' $ \db -> markGroupChatItemBlocked db user gInfo ci + reactions <- maybe (pure []) (\sharedMsgId -> withStore' $ \db -> getGroupCIReactions db gInfo memberId sharedMsgId) sharedMsgId_ + groupMsgToView gInfo ci' {reactions} + + groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> Map MemberName MsgMention -> RcvMessage -> UTCTime -> Maybe Int -> Maybe Bool -> CM () + groupMessageUpdate gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId, memberId} sharedMsgId mc mentions msg@RcvMessage {msgId} brokerTs ttl_ live_ + | prohibitedSimplexLinks gInfo m ft_ = + messageWarning $ "x.msg.update ignored: feature not allowed " <> groupFeatureNameText GFSimplexLinks + | otherwise = do + updateRcvChatItem `catchCINotFound` \_ -> do + -- 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_ = rcvGroupCITimed gInfo ttl_ + mentions' = if showMessages (memberSettings m) then mentions else [] + ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) (memberNotInHistory m) msg (Just sharedMsgId) brokerTs (content, ts) Nothing timed_ live mentions' + ci' <- withStore' $ \db -> do + createChatItemVersion db (chatItemId' ci) brokerTs mc + ci' <- updateGroupChatItem db user groupId ci content True live Nothing + blockedMember m ci' $ markGroupChatItemBlocked db user gInfo ci' + toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') + where + content = CIRcvMsgContent mc + ts@(_, ft_) = msgContentTexts mc + live = fromMaybe False live_ + updateRcvChatItem = do + cci <- withStore $ \db -> getGroupChatItemBySharedMsgId db user gInfo groupMemberId sharedMsgId + case cci of + CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv m', meta = CIMeta {itemLive}, content = CIRcvMsgContent oldMC} -> + if sameMemberId memberId m' + then do + let changed = mc /= oldMC + if changed || fromMaybe False itemLive + then do + ci' <- withStore' $ \db -> do + when changed $ + addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc) + reactions <- getGroupCIReactions db gInfo memberId sharedMsgId + let edited = itemLive /= Just True + ciMentions <- getRcvCIMentions db user gInfo ft_ mentions + ci' <- updateGroupChatItem db user groupId ci {reactions} content edited live $ Just msgId + updateGroupCIMentions db gInfo ci' ciMentions + toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') + startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' + else toView $ CEvtChatItemNotChanged user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci) + else messageError "x.msg.update: group member attempted to update a message of another member" + _ -> messageError "x.msg.update: group member attempted invalid message update" + + memberNotInHistory :: GroupMember -> Maybe NotInHistory + memberNotInHistory = \case + GroupMember {memberStatus = GSMemPendingApproval} -> Just NotInHistory + _ -> Nothing + + groupMessageDelete :: GroupInfo -> GroupMember -> SharedMsgId -> Maybe MemberId -> RcvMessage -> UTCTime -> CM () + groupMessageDelete gInfo@GroupInfo {membership} m@GroupMember {memberId, memberRole = senderRole} sharedMsgId sndMemberId_ RcvMessage {msgId} brokerTs = do + let msgMemberId = fromMaybe memberId sndMemberId_ + withStore' (\db -> runExceptT $ getGroupMemberCIBySharedMsgId db user gInfo msgMemberId sharedMsgId) >>= \case + Right cci@(CChatItem _ ci@ChatItem {chatDir}) -> case chatDir of + CIGroupRcv mem -> case sndMemberId_ of + -- regular deletion + Nothing + | sameMemberId memberId mem && msgMemberId == memberId && rcvItemDeletable ci brokerTs -> + delete cci Nothing + | otherwise -> + messageError "x.msg.del: member attempted invalid message delete" + -- moderation (not limited by time) + Just _ + | sameMemberId memberId mem && msgMemberId == memberId -> + delete cci (Just m) + | otherwise -> + moderate mem cci + CIGroupSnd -> moderate membership cci + Left e + | msgMemberId == memberId -> messageError $ "x.msg.del: message not found, " <> tshow e + | senderRole < GRModerator -> messageError $ "x.msg.del: message not found, message of another member with insufficient member permissions, " <> tshow e + | otherwise -> withStore' $ \db -> createCIModeration db gInfo m msgMemberId sharedMsgId msgId brokerTs + where + moderate :: GroupMember -> CChatItem 'CTGroup -> CM () + moderate mem cci = case sndMemberId_ of + Just sndMemberId + | sameMemberId sndMemberId mem -> checkRole mem $ do + delete cci (Just m) + archiveMessageReports cci m + | otherwise -> messageError "x.msg.del: message of another member with incorrect memberId" + _ -> messageError "x.msg.del: message of another member without memberId" + checkRole GroupMember {memberRole} a + | senderRole < GRModerator || senderRole < memberRole = + messageError "x.msg.del: message of another member with insufficient member permissions" + | otherwise = a + delete :: CChatItem 'CTGroup -> Maybe GroupMember -> CM () + delete cci byGroupMember = do + deletions <- if groupFeatureMemberAllowed SGFFullDelete m gInfo + then deleteGroupCIs user gInfo [cci] byGroupMember brokerTs + else markGroupCIsDeleted user gInfo [cci] byGroupMember brokerTs + toView $ CEvtChatItemsDeleted user deletions False False + archiveMessageReports :: CChatItem 'CTGroup -> GroupMember -> CM () + archiveMessageReports (CChatItem _ ci) byMember = do + ciIds <- withStore' $ \db -> markMessageReportsDeleted db user gInfo ci byMember brokerTs + unless (null ciIds) $ toView $ CEvtGroupChatItemsDeleted user gInfo ciIds False (Just byMember) + + -- TODO remove once XFile is discontinued + processFileInvitation' :: Contact -> FileInvitation -> RcvMessage -> MsgMeta -> CM () + processFileInvitation' ct fInv' msg@RcvMessage {sharedMsgId_} msgMeta = do + ChatConfig {fileChunkSize} <- asks config + let fInv@FileInvitation {fileName, fileSize} = mkValidFileInvitation fInv' + inline <- receiveInlineMode fInv Nothing fileChunkSize + RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize + let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP + ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} + content = ciContentNoParse $ CIRcvMsgContent $ MCFile "" + ci <- saveRcvChatItem' user (CDDirectRcv ct) Nothing msg sharedMsgId_ brokerTs content ciFile Nothing False M.empty + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] + where + brokerTs = metaBrokerTs msgMeta + + -- TODO remove once XFile is discontinued + processGroupFileInvitation' :: GroupInfo -> GroupMember -> FileInvitation -> RcvMessage -> UTCTime -> CM () + processGroupFileInvitation' gInfo m fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} brokerTs = do + ChatConfig {fileChunkSize} <- asks config + inline <- receiveInlineMode fInv Nothing fileChunkSize + RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvGroupFileTransfer db userId m fInv inline fileChunkSize + let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP + ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} + content = ciContentNoParse $ CIRcvMsgContent $ MCFile "" + ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) Nothing msg sharedMsgId_ brokerTs content ciFile Nothing False M.empty + ci' <- blockedMember m ci $ withStore' $ \db -> markGroupChatItemBlocked db user gInfo ci + groupMsgToView gInfo ci' + + blockedMember :: Monad m' => GroupMember -> ChatItem c d -> m' (ChatItem c d) -> m' (ChatItem c d) + blockedMember m ci blockedCI + | showMessages (memberSettings m) = pure ci + | otherwise = blockedCI + + receiveInlineMode :: FileInvitation -> Maybe MsgContent -> Integer -> CM (Maybe InlineFileMode) + receiveInlineMode FileInvitation {fileSize, fileInline, fileDescr} mc_ chSize = case (fileInline, fileDescr) of + (Just mode, Nothing) -> do + InlineFilesConfig {receiveChunks, receiveInstant} <- asks $ inlineFiles . config + pure $ if fileSize <= receiveChunks * chSize then inline' receiveInstant else Nothing + where + inline' receiveInstant = if mode == IFMOffer || (receiveInstant && maybe False isVoice mc_) then fileInline else Nothing + _ -> pure Nothing + + xFileCancel :: Contact -> SharedMsgId -> CM () + xFileCancel Contact {contactId} sharedMsgId = do + fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId + ft <- withStore (\db -> getRcvFileTransfer db user fileId) + unless (rcvFileCompleteOrCancelled ft) $ do + cancelRcvFileTransfer user ft >>= mapM_ deleteAgentConnectionAsync + ci <- withStore $ \db -> getChatItemByFileId db vr user fileId + toView $ CEvtRcvFileSndCancelled user ci ft + + xFileAcptInv :: Contact -> SharedMsgId -> Maybe ConnReqInvitation -> String -> CM () + xFileAcptInv ct sharedMsgId fileConnReq_ fName = do + fileId <- withStore $ \db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId + (AChatItem _ _ _ ci) <- withStore $ \db -> getChatItemByFileId db vr user fileId + assertSMPAcceptNotProhibited ci + ft@FileTransferMeta {fileName, fileSize, fileInline, cancelled} <- withStore (\db -> getFileTransferMeta db user fileId) + -- [async agent commands] no continuation needed, but command should be asynchronous for stability + if fName == fileName + then unless cancelled $ case fileConnReq_ of + -- receiving via a separate connection + Just fileConnReq -> do + subMode <- chatReadVar subscriptionMode + dm <- encodeConnInfo XOk + connIds <- joinAgentConnectionAsync user True fileConnReq dm subMode + withStore' $ \db -> createSndDirectFTConnection db vr user fileId connIds subMode + -- receiving inline + _ -> do + event <- withStore $ \db -> do + ci' <- updateDirectCIFileStatus db vr user fileId $ CIFSSndTransfer 0 1 + sft <- createSndDirectInlineFT db ct ft + pure $ CEvtSndFileStart user ci' sft + toView event + ifM + (allowSendInline fileSize fileInline) + (sendDirectFileInline user ct ft sharedMsgId) + (messageError "x.file.acpt.inv: fileSize is bigger than allowed to send inline") + else messageError "x.file.acpt.inv: fileName is different from expected" + + assertSMPAcceptNotProhibited :: ChatItem c d -> CM () + assertSMPAcceptNotProhibited ChatItem {file = Just CIFile {fileId, fileProtocol}, content} + | fileProtocol == FPXFTP && not (imageOrVoice content) = throwChatError $ CEFallbackToSMPProhibited fileId + | otherwise = pure () + where + imageOrVoice :: CIContent d -> Bool + imageOrVoice (CISndMsgContent (MCImage _ _)) = True + imageOrVoice (CISndMsgContent (MCVoice _ _)) = True + imageOrVoice _ = False + assertSMPAcceptNotProhibited _ = pure () + + checkSndInlineFTComplete :: Connection -> AgentMsgId -> CM () + checkSndInlineFTComplete conn agentMsgId = do + sft_ <- withStore' $ \db -> getSndFTViaMsgDelivery db user conn agentMsgId + forM_ sft_ $ \sft@SndFileTransfer {fileId} -> do + ci@(AChatItem _ _ _ ChatItem {file}) <- withStore $ \db -> do + liftIO $ updateSndFileStatus db sft FSComplete + liftIO $ deleteSndFileChunks db sft + updateDirectCIFileStatus db vr user fileId CIFSSndComplete + case file of + Just CIFile {fileProtocol = FPXFTP} -> do + ft <- withStore $ \db -> getFileTransferMeta db user fileId + toView $ CEvtSndFileCompleteXFTP user ci ft + _ -> toView $ CEvtSndFileComplete user ci sft + + allowSendInline :: Integer -> Maybe InlineFileMode -> CM Bool + allowSendInline fileSize = \case + Just IFMOffer -> do + ChatConfig {fileChunkSize, inlineFiles} <- asks config + pure $ fileSize <= fileChunkSize * offerChunks inlineFiles + _ -> pure False + + bFileChunk :: Contact -> SharedMsgId -> FileChunk -> MsgMeta -> CM () + bFileChunk ct sharedMsgId chunk meta = do + ft <- withStore $ \db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId >>= getRcvFileTransfer db user + receiveInlineChunk ft chunk meta + + bFileChunkGroup :: GroupInfo -> SharedMsgId -> FileChunk -> MsgMeta -> CM () + bFileChunkGroup GroupInfo {groupId} sharedMsgId chunk meta = do + ft <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId >>= getRcvFileTransfer db user + receiveInlineChunk ft chunk meta + + receiveInlineChunk :: RcvFileTransfer -> FileChunk -> MsgMeta -> CM () + receiveInlineChunk RcvFileTransfer {fileId, fileStatus = RFSNew} FileChunk {chunkNo} _ + | chunkNo == 1 = throwChatError $ CEInlineFileProhibited fileId + | otherwise = pure () + receiveInlineChunk ft@RcvFileTransfer {fileId} chunk meta = do + case chunk of + FileChunk {chunkNo} -> when (chunkNo == 1) $ startReceivingFile user fileId + _ -> pure () + receiveFileChunk ft Nothing meta chunk + + xFileCancelGroup :: GroupInfo -> GroupMember -> SharedMsgId -> CM () + xFileCancelGroup g@GroupInfo {groupId} GroupMember {groupMemberId, memberId} sharedMsgId = do + fileId <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId + CChatItem msgDir ChatItem {chatDir} <- withStore $ \db -> getGroupChatItemBySharedMsgId db user g groupMemberId sharedMsgId + case (msgDir, chatDir) of + (SMDRcv, CIGroupRcv m) -> do + if sameMemberId memberId m + then do + ft <- withStore (\db -> getRcvFileTransfer db user fileId) + unless (rcvFileCompleteOrCancelled ft) $ do + cancelRcvFileTransfer user ft >>= mapM_ deleteAgentConnectionAsync + ci <- withStore $ \db -> getChatItemByFileId db vr user fileId + toView $ CEvtRcvFileSndCancelled user ci ft + else messageError "x.file.cancel: group member attempted to cancel file of another member" -- shouldn't happen now that query includes group member id + (SMDSnd, _) -> messageError "x.file.cancel: group member attempted invalid file cancel" + + xFileAcptInvGroup :: GroupInfo -> GroupMember -> SharedMsgId -> Maybe ConnReqInvitation -> String -> CM () + xFileAcptInvGroup GroupInfo {groupId} m@GroupMember {activeConn} sharedMsgId fileConnReq_ fName = do + fileId <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId + (AChatItem _ _ _ ci) <- withStore $ \db -> getChatItemByFileId db vr user fileId + assertSMPAcceptNotProhibited ci + -- TODO check that it's not already accepted + ft@FileTransferMeta {fileName, fileSize, fileInline, cancelled} <- withStore (\db -> getFileTransferMeta db user fileId) + if fName == fileName + then unless cancelled $ case (fileConnReq_, activeConn) of + (Just fileConnReq, _) -> do + subMode <- chatReadVar subscriptionMode + -- receiving via a separate connection + -- [async agent commands] no continuation needed, but command should be asynchronous for stability + dm <- encodeConnInfo XOk + connIds <- joinAgentConnectionAsync user True fileConnReq dm subMode + withStore' $ \db -> createSndGroupFileTransferConnection db vr user fileId connIds m subMode + (_, Just conn) -> do + -- receiving inline + event <- withStore $ \db -> do + ci' <- updateDirectCIFileStatus db vr user fileId $ CIFSSndTransfer 0 1 + sft <- liftIO $ createSndGroupInlineFT db m conn ft + pure $ CEvtSndFileStart user ci' sft + toView event + ifM + (allowSendInline fileSize fileInline) + (sendMemberFileInline m conn ft sharedMsgId) + (messageError "x.file.acpt.inv: fileSize is bigger than allowed to send inline") + _ -> messageError "x.file.acpt.inv: member connection is not active" + else messageError "x.file.acpt.inv: fileName is different from expected" + + groupMsgToView :: forall d. MsgDirectionI d => GroupInfo -> ChatItem 'CTGroup d -> CM () + groupMsgToView gInfo ci = + toView $ CEvtNewChatItems user [AChatItem SCTGroup (msgDirection @d) (GroupChat gInfo) ci] + + processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> CM () + processGroupInvitation ct inv msg msgMeta = do + let Contact {localDisplayName = c, activeConn} = ct + GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} = inv + forM_ activeConn $ \Connection {connId, connChatVersion, peerChatVRange, customUserProfileId, groupLinkId = groupLinkId'} -> do + when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c) + when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId + -- [incognito] if direct connection with host is incognito, create membership using the same incognito profile + (gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership}, hostId) <- withStore $ \db -> createGroupInvitation db vr user ct inv customUserProfileId + let GroupMember {groupMemberId, memberId = membershipMemId} = membership + if sameGroupLinkId groupLinkId groupLinkId' + then do + subMode <- chatReadVar subscriptionMode + dm <- encodeConnInfo $ XGrpAcpt membershipMemId + connIds <- joinAgentConnectionAsync user True connRequest dm subMode + withStore' $ \db -> do + setViaGroupLinkHash db groupId connId + createMemberConnectionAsync db user hostId connIds connChatVersion peerChatVRange subMode + updateGroupMemberStatusById db userId hostId GSMemAccepted + updateGroupMemberStatus db userId membership GSMemAccepted + toView $ CEvtUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct) + else do + let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole + ci <- saveRcvChatItemNoParse user (CDDirectRcv ct) msg brokerTs content + withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci) + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] + toView $ CEvtReceivedGroupInvitation {user, groupInfo = gInfo, contact = ct, fromMemberRole = fromRole, memberRole = memRole} + where + brokerTs = metaBrokerTs msgMeta + sameGroupLinkId :: Maybe GroupLinkId -> Maybe GroupLinkId -> Bool + sameGroupLinkId (Just gli) (Just gli') = gli == gli' + sameGroupLinkId _ _ = False + + checkIntegrityCreateItem :: forall c. ChatTypeI c => ChatDirection c 'MDRcv -> MsgMeta -> CM () + checkIntegrityCreateItem cd MsgMeta {integrity, broker = (_, brokerTs)} = case integrity of + MsgOk -> pure () + MsgError e -> createInternalChatItem user cd (CIRcvIntegrityError e) (Just brokerTs) + + xInfo :: Contact -> Profile -> CM () + xInfo c p' = void $ processContactProfileUpdate c p' True + + xDirectDel :: Contact -> RcvMessage -> MsgMeta -> CM () + xDirectDel c msg msgMeta = + if directOrUsed c + then do + ct' <- withStore' $ \db -> updateContactStatus db user c CSDeleted + contactConns <- withStore' $ \db -> getContactConnections db vr userId ct' + deleteAgentConnectionsAsync $ map aConnId contactConns + forM_ contactConns $ \conn -> withStore' $ \db -> updateConnectionStatus db conn ConnDeleted + activeConn' <- forM (contactConn ct') $ \conn -> pure conn {connStatus = ConnDeleted} + let ct'' = ct' {activeConn = activeConn'} :: Contact + ci <- saveRcvChatItemNoParse user (CDDirectRcv ct'') msg brokerTs (CIRcvDirectEvent RDEContactDeleted) + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct'') ci] + toView $ CEvtContactDeletedByContact user ct'' + else do + contactConns <- withStore' $ \db -> getContactConnections db vr userId c + deleteAgentConnectionsAsync $ map aConnId contactConns + withStore $ \db -> deleteContact db user c + where + brokerTs = metaBrokerTs msgMeta + + processContactProfileUpdate :: Contact -> Profile -> Bool -> CM Contact + processContactProfileUpdate c@Contact {profile = lp} p' createItems + | p /= p' = do + c' <- withStore $ \db -> + if userTTL == rcvTTL + then updateContactProfile db user c p' + else do + c' <- liftIO $ updateContactUserPreferences db user c ctUserPrefs' + updateContactProfile db user c' p' + when (directOrUsed c' && createItems) $ do + createProfileUpdatedItem c' + lift $ createRcvFeatureItems user c c' + toView $ CEvtContactUpdated user c c' + pure c' + | otherwise = + pure c + where + p = fromLocalProfile lp + Contact {userPreferences = ctUserPrefs@Preferences {timedMessages = ctUserTMPref}} = c + userTTL = prefParam $ getPreference SCFTimedMessages ctUserPrefs + Profile {preferences = rcvPrefs_} = p' + rcvTTL = prefParam $ getPreference SCFTimedMessages rcvPrefs_ + ctUserPrefs' = + let userDefault = getPreference SCFTimedMessages (fullPreferences user) + userDefaultTTL = prefParam userDefault + ctUserTMPref' = case ctUserTMPref of + Just userTM -> Just (userTM :: TimedMessagesPreference) {ttl = rcvTTL} + _ + | rcvTTL /= userDefaultTTL -> Just (userDefault :: TimedMessagesPreference) {ttl = rcvTTL} + | otherwise -> Nothing + in setPreference_ SCFTimedMessages ctUserTMPref' ctUserPrefs + createProfileUpdatedItem c' = + when visibleProfileUpdated $ do + let ciContent = CIRcvDirectEvent $ RDEProfileUpdated p p' + createInternalChatItem user (CDDirectRcv c') ciContent Nothing + where + visibleProfileUpdated = + n' /= n || fn' /= fn || i' /= i || cl' /= cl + Profile {displayName = n, fullName = fn, image = i, contactLink = cl} = p + Profile {displayName = n', fullName = fn', image = i', contactLink = cl'} = p' + + xInfoMember :: GroupInfo -> GroupMember -> Profile -> UTCTime -> CM () + xInfoMember gInfo m p' brokerTs = void $ processMemberProfileUpdate gInfo m p' True (Just brokerTs) + + xGrpLinkMem :: GroupInfo -> GroupMember -> Connection -> Profile -> CM () + xGrpLinkMem gInfo@GroupInfo {membership, businessChat} m@GroupMember {groupMemberId, memberCategory, memberStatus} Connection {viaGroupLink} p' = do + xGrpLinkMemReceived <- withStore $ \db -> getXGrpLinkMemReceived db groupMemberId + if (viaGroupLink || isJust businessChat) && isNothing (memberContactId m) && memberCategory == GCHostMember && not xGrpLinkMemReceived + then do + m' <- processMemberProfileUpdate gInfo m p' False Nothing + withStore' $ \db -> setXGrpLinkMemReceived db groupMemberId True + unless (memberStatus == GSMemPendingApproval) $ do + let connectedIncognito = memberIncognito membership + probeMatchingMemberContact m' connectedIncognito + else messageError "x.grp.link.mem error: invalid group link host profile update" + + xGrpLinkAcpt :: GroupInfo -> GroupMember -> GroupMemberRole -> CM () + xGrpLinkAcpt gInfo@GroupInfo {membership} m role = do + membership' <- withStore' $ \db -> do + updateGroupMemberStatus db userId m GSMemConnected + updateGroupMemberAccepted db user membership role + let m' = m {memberStatus = GSMemConnected} + toView $ CEvtUserJoinedGroup user gInfo {membership = membership'} m' + let connectedIncognito = memberIncognito membership + probeMatchingMemberContact m' connectedIncognito + + processMemberProfileUpdate :: GroupInfo -> GroupMember -> Profile -> Bool -> Maybe UTCTime -> CM GroupMember + processMemberProfileUpdate gInfo m@GroupMember {memberProfile = p, memberContactId} p' createItems itemTs_ + | redactedMemberProfile (fromLocalProfile p) /= redactedMemberProfile p' = do + updateBusinessChatProfile gInfo + case memberContactId of + Nothing -> do + m' <- withStore $ \db -> updateMemberProfile db user m p' + createProfileUpdatedItem m' + toView $ CEvtGroupMemberUpdated user gInfo m m' + pure m' + Just mContactId -> do + mCt <- withStore $ \db -> getContact db vr user mContactId + if canUpdateProfile mCt + then do + (m', ct') <- withStore $ \db -> updateContactMemberProfile db user m mCt p' + createProfileUpdatedItem m' + toView $ CEvtGroupMemberUpdated user gInfo m m' + toView $ CEvtContactUpdated user mCt ct' + pure m' + else pure m + where + canUpdateProfile ct + | not (contactActive ct) = True + | otherwise = case contactConn ct of + Nothing -> True + Just conn -> not (connReady conn) || (authErrCounter conn >= 1) + | otherwise = + pure m + where + updateBusinessChatProfile g@GroupInfo {businessChat} = case businessChat of + Just bc | isMainBusinessMember bc m -> do + g' <- withStore $ \db -> updateGroupProfileFromMember db user g p' + toView $ CEvtGroupUpdated user g g' (Just m) + _ -> pure () + isMainBusinessMember BusinessChatInfo {chatType, businessId, customerId} GroupMember {memberId} = case chatType of + BCBusiness -> businessId == memberId + BCCustomer -> customerId == memberId + createProfileUpdatedItem m' = + when createItems $ do + let ciContent = CIRcvGroupEvent $ RGEMemberProfileUpdated (fromLocalProfile p) p' + createInternalChatItem user (CDGroupRcv gInfo m') ciContent itemTs_ + + createFeatureEnabledItems :: Contact -> CM () + createFeatureEnabledItems ct@Contact {mergedPreferences} = + forM_ allChatFeatures $ \(ACF f) -> do + let state = featureState $ getContactUserPreference f mergedPreferences + createInternalChatItem user (CDDirectRcv ct) (uncurry (CIRcvChatFeature $ chatFeature f) state) Nothing + + xInfoProbe :: ContactOrMember -> Probe -> CM () + xInfoProbe cgm2 probe = do + contactMerge <- readTVarIO =<< asks contactMergeEnabled + -- [incognito] unless connected incognito + when (contactMerge && not (contactOrMemberIncognito cgm2)) $ do + cgm1s <- withStore' $ \db -> matchReceivedProbe db vr user cgm2 probe + let cgm1s' = filter (not . contactOrMemberIncognito) cgm1s + probeMatches cgm1s' cgm2 + where + probeMatches :: [ContactOrMember] -> ContactOrMember -> CM () + probeMatches [] _ = pure () + probeMatches (cgm1' : cgm1s') cgm2' = do + cgm2''_ <- probeMatch cgm1' cgm2' probe `catchChatError` \_ -> pure (Just cgm2') + let cgm2'' = fromMaybe cgm2' cgm2''_ + probeMatches cgm1s' cgm2'' + + xInfoProbeCheck :: ContactOrMember -> ProbeHash -> CM () + xInfoProbeCheck cgm1 probeHash = do + contactMerge <- readTVarIO =<< asks contactMergeEnabled + -- [incognito] unless connected incognito + when (contactMerge && not (contactOrMemberIncognito cgm1)) $ do + cgm2Probe_ <- withStore' $ \db -> matchReceivedProbeHash db vr user cgm1 probeHash + forM_ cgm2Probe_ $ \(cgm2, probe) -> + unless (contactOrMemberIncognito cgm2) . void $ + probeMatch cgm1 cgm2 probe + + probeMatch :: ContactOrMember -> ContactOrMember -> Probe -> CM (Maybe ContactOrMember) + probeMatch cgm1 cgm2 probe = + case cgm1 of + COMContact c1@Contact {contactId = cId1, profile = p1} -> + case cgm2 of + COMContact c2@Contact {contactId = cId2, profile = p2} + | cId1 /= cId2 && profilesMatch p1 p2 -> do + void . sendDirectContactMessage user c1 $ XInfoProbeOk probe + COMContact <$$> mergeContacts c1 c2 + | otherwise -> messageWarning "probeMatch ignored: profiles don't match or same contact id" >> pure Nothing + COMGroupMember m2@GroupMember {memberProfile = p2, memberContactId} + | isNothing memberContactId && profilesMatch p1 p2 -> do + void . sendDirectContactMessage user c1 $ XInfoProbeOk probe + COMContact <$$> associateMemberAndContact c1 m2 + | otherwise -> messageWarning "probeMatch ignored: profiles don't match or member already has contact" >> pure Nothing + COMGroupMember GroupMember {activeConn = Nothing} -> pure Nothing + COMGroupMember m1@GroupMember {groupId, memberProfile = p1, memberContactId, activeConn = Just conn} -> + case cgm2 of + COMContact c2@Contact {profile = p2} + | memberCurrent m1 && isNothing memberContactId && profilesMatch p1 p2 -> do + void $ sendDirectMemberMessage conn (XInfoProbeOk probe) groupId + COMContact <$$> associateMemberAndContact c2 m1 + | otherwise -> messageWarning "probeMatch ignored: profiles don't match or member already has contact or member not current" >> pure Nothing + COMGroupMember _ -> messageWarning "probeMatch ignored: members are not matched with members" >> pure Nothing + + xInfoProbeOk :: ContactOrMember -> Probe -> CM () + xInfoProbeOk cgm1 probe = do + cgm2 <- withStore' $ \db -> matchSentProbe db vr user cgm1 probe + case cgm1 of + COMContact c1@Contact {contactId = cId1} -> + case cgm2 of + Just (COMContact c2@Contact {contactId = cId2}) + | cId1 /= cId2 -> void $ mergeContacts c1 c2 + | otherwise -> messageWarning "xInfoProbeOk ignored: same contact id" + Just (COMGroupMember m2@GroupMember {memberContactId}) + | isNothing memberContactId -> void $ associateMemberAndContact c1 m2 + | otherwise -> messageWarning "xInfoProbeOk ignored: member already has contact" + _ -> pure () + COMGroupMember m1@GroupMember {memberContactId} -> + case cgm2 of + Just (COMContact c2) + | isNothing memberContactId -> void $ associateMemberAndContact c2 m1 + | otherwise -> messageWarning "xInfoProbeOk ignored: member already has contact" + Just (COMGroupMember _) -> messageWarning "xInfoProbeOk ignored: members are not matched with members" + _ -> pure () + + -- to party accepting call + xCallInv :: Contact -> CallId -> CallInvitation -> RcvMessage -> MsgMeta -> CM () + xCallInv ct@Contact {contactId} callId CallInvitation {callType, callDhPubKey} msg@RcvMessage {sharedMsgId_} msgMeta = do + if featureAllowed SCFCalls forContact ct + then do + g <- asks random + dhKeyPair <- atomically $ if encryptedCall callType then Just <$> C.generateKeyPair g else pure Nothing + ci <- saveCallItem CISCallPending + callUUID <- UUID.toText <$> liftIO V4.nextRandom + let sharedKey = C.Key . C.dhBytes' <$> (C.dh' <$> callDhPubKey <*> (snd <$> dhKeyPair)) + callState = CallInvitationReceived {peerCallType = callType, localDhPubKey = fst <$> dhKeyPair, sharedKey} + call' = Call {contactId, callId, callUUID, chatItemId = chatItemId' ci, callState, callTs = chatItemTs' ci} + calls <- asks currentCalls + -- theoretically, the new call invitation for the current contact can mark the in-progress call as ended + -- (and replace it in ChatController) + -- practically, this should not happen + withStore' $ \db -> createCall db user call' $ chatItemTs' ci + call_ <- atomically (TM.lookupInsert contactId call' calls) + forM_ call_ $ \call -> updateCallItemStatus user ct call WCSDisconnected Nothing + toView $ CEvtCallInvitation RcvCallInvitation {user, contact = ct, callType, sharedKey, callUUID, callTs = chatItemTs' ci} + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] + else featureRejected CFCalls + where + brokerTs = metaBrokerTs msgMeta + saveCallItem status = saveRcvChatItemNoParse user (CDDirectRcv ct) msg brokerTs (CIRcvCall status 0) + featureRejected f = do + let content = ciContentNoParse $ CIRcvChatFeatureRejected f + ci <- saveRcvChatItem' user (CDDirectRcv ct) Nothing msg sharedMsgId_ brokerTs content Nothing Nothing False M.empty + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] + + -- to party initiating call + xCallOffer :: Contact -> CallId -> CallOffer -> RcvMessage -> CM () + xCallOffer ct callId CallOffer {callType, rtcSession, callDhPubKey} msg = do + msgCurrentCall ct callId "x.call.offer" msg $ + \call -> case callState call of + CallInvitationSent {localCallType, localDhPrivKey} -> do + let sharedKey = C.Key . C.dhBytes' <$> (C.dh' <$> callDhPubKey <*> localDhPrivKey) + callState' = CallOfferReceived {localCallType, peerCallType = callType, peerCallSession = rtcSession, sharedKey} + askConfirmation = encryptedCall localCallType && not (encryptedCall callType) + toView CEvtCallOffer {user, contact = ct, callType, offer = rtcSession, sharedKey, askConfirmation} + pure (Just call {callState = callState'}, Just . ACIContent SMDSnd $ CISndCall CISCallAccepted 0) + _ -> do + msgCallStateError "x.call.offer" call + pure (Just call, Nothing) + + -- to party accepting call + xCallAnswer :: Contact -> CallId -> CallAnswer -> RcvMessage -> CM () + xCallAnswer ct callId CallAnswer {rtcSession} msg = do + msgCurrentCall ct callId "x.call.answer" msg $ + \call -> case callState call of + CallOfferSent {localCallType, peerCallType, localCallSession, sharedKey} -> do + let callState' = CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession = rtcSession, sharedKey} + toView $ CEvtCallAnswer user ct rtcSession + pure (Just call {callState = callState'}, Just . ACIContent SMDRcv $ CIRcvCall CISCallNegotiated 0) + _ -> do + msgCallStateError "x.call.answer" call + pure (Just call, Nothing) + + -- to any call party + xCallExtra :: Contact -> CallId -> CallExtraInfo -> RcvMessage -> CM () + xCallExtra ct callId CallExtraInfo {rtcExtraInfo} msg = do + msgCurrentCall ct callId "x.call.extra" msg $ + \call -> case callState call of + CallOfferReceived {localCallType, peerCallType, peerCallSession, sharedKey} -> do + -- TODO update the list of ice servers in peerCallSession + let callState' = CallOfferReceived {localCallType, peerCallType, peerCallSession, sharedKey} + toView $ CEvtCallExtraInfo user ct rtcExtraInfo + pure (Just call {callState = callState'}, Nothing) + CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession, sharedKey} -> do + -- TODO update the list of ice servers in peerCallSession + let callState' = CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession, sharedKey} + toView $ CEvtCallExtraInfo user ct rtcExtraInfo + pure (Just call {callState = callState'}, Nothing) + _ -> do + msgCallStateError "x.call.extra" call + pure (Just call, Nothing) + + -- to any call party + xCallEnd :: Contact -> CallId -> RcvMessage -> CM () + xCallEnd ct callId msg = + msgCurrentCall ct callId "x.call.end" msg $ \Call {chatItemId} -> do + toView $ CEvtCallEnded user ct + (Nothing,) <$> callStatusItemContent user ct chatItemId WCSDisconnected + + msgCurrentCall :: Contact -> CallId -> Text -> RcvMessage -> (Call -> CM (Maybe Call, Maybe ACIContent)) -> CM () + msgCurrentCall ct@Contact {contactId = ctId'} callId' eventName RcvMessage {msgId} action = do + calls <- asks currentCalls + atomically (TM.lookup ctId' calls) >>= \case + Nothing -> messageError $ eventName <> ": no current call" + Just call@Call {contactId, callId, chatItemId} + | contactId /= ctId' || callId /= callId' -> messageError $ eventName <> ": wrong contact or callId" + | otherwise -> do + (call_, aciContent_) <- action call + case call_ of + Just call' -> do + unless (isRcvInvitation call') $ withStore' $ \db -> deleteCalls db user ctId' + atomically $ TM.insert ctId' call' calls + _ -> do + withStore' $ \db -> deleteCalls db user ctId' + atomically $ TM.delete ctId' calls + forM_ aciContent_ $ \aciContent -> do + timed_ <- callTimed ct aciContent + updateDirectChatItemView user ct chatItemId aciContent False False timed_ $ Just msgId + forM_ (timed_ >>= timedDeleteAt') $ + startProximateTimedItemThread user (ChatRef CTDirect ctId', chatItemId) + + msgCallStateError :: Text -> Call -> CM () + msgCallStateError eventName Call {callState} = + messageError $ eventName <> ": wrong call state " <> T.pack (show $ callStateTag callState) + + mergeContacts :: Contact -> Contact -> CM (Maybe Contact) + mergeContacts c1 c2 = do + let Contact {localDisplayName = cLDN1, profile = LocalProfile {displayName}} = c1 + Contact {localDisplayName = cLDN2} = c2 + case (suffixOrd displayName cLDN1, suffixOrd displayName cLDN2) of + (Just cOrd1, Just cOrd2) + | cOrd1 < cOrd2 -> merge c1 c2 + | cOrd2 < cOrd1 -> merge c2 c1 + | otherwise -> pure Nothing + _ -> pure Nothing + where + merge c1' c2' = do + c2'' <- withStore $ \db -> mergeContactRecords db vr user c1' c2' + toView $ CEvtContactsMerged user c1' c2' c2'' + when (directOrUsed c2'') $ showSecurityCodeChanged c2'' + pure $ Just c2'' + where + showSecurityCodeChanged mergedCt = do + let sc1_ = contactSecurityCode c1' + sc2_ = contactSecurityCode c2' + scMerged_ = contactSecurityCode mergedCt + case (sc1_, sc2_) of + (Just sc1, Nothing) + | scMerged_ /= Just sc1 -> securityCodeChanged mergedCt + | otherwise -> pure () + (Nothing, Just sc2) + | scMerged_ /= Just sc2 -> securityCodeChanged mergedCt + | otherwise -> pure () + _ -> pure () + + associateMemberAndContact :: Contact -> GroupMember -> CM (Maybe Contact) + associateMemberAndContact c m = do + let Contact {localDisplayName = cLDN, profile = LocalProfile {displayName}} = c + GroupMember {localDisplayName = mLDN} = m + case (suffixOrd displayName cLDN, suffixOrd displayName mLDN) of + (Just cOrd, Just mOrd) + | cOrd < mOrd -> Just <$> associateMemberWithContact c m + | mOrd < cOrd -> Just <$> associateContactWithMember m c + | otherwise -> pure Nothing + _ -> pure Nothing + + suffixOrd :: ContactName -> ContactName -> Maybe Int + suffixOrd displayName localDisplayName + | localDisplayName == displayName = Just 0 + | otherwise = case T.stripPrefix (displayName <> "_") localDisplayName of + Just suffix -> readMaybe $ T.unpack suffix + Nothing -> Nothing + + associateMemberWithContact :: Contact -> GroupMember -> CM Contact + associateMemberWithContact c1 m2@GroupMember {groupId} = do + withStore' $ \db -> associateMemberWithContactRecord db user c1 m2 + g <- withStore $ \db -> getGroupInfo db vr user groupId + toView $ CEvtContactAndMemberAssociated user c1 g m2 c1 + pure c1 + + associateContactWithMember :: GroupMember -> Contact -> CM Contact + associateContactWithMember m1@GroupMember {groupId} c2 = do + c2' <- withStore $ \db -> associateContactWithMemberRecord db vr user m1 c2 + g <- withStore $ \db -> getGroupInfo db vr user groupId + toView $ CEvtContactAndMemberAssociated user c2 g m1 c2' + pure c2' + + saveConnInfo :: Connection -> ConnInfo -> CM (Connection, Bool) + saveConnInfo activeConn connInfo = do + ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage activeConn connInfo + conn' <- updatePeerChatVRange activeConn chatVRange + case chatMsgEvent of + XInfo p -> do + ct <- withStore $ \db -> createDirectContact db user conn' p + toView $ CEvtContactConnecting user ct + pure (conn', False) + XGrpLinkInv glInv -> do + (gInfo, host) <- withStore $ \db -> createGroupInvitedViaLink db vr user conn' glInv + toView $ CEvtGroupLinkConnecting user gInfo host + pure (conn', True) + XGrpLinkReject glRjct@GroupLinkRejection {rejectionReason} -> do + (gInfo, host) <- withStore $ \db -> createGroupRejectedViaLink db vr user conn' glRjct + toView $ CEvtGroupLinkConnecting user gInfo host + toViewTE $ TEGroupLinkRejected user gInfo rejectionReason + pure (conn', True) + -- TODO show/log error, other events in SMP confirmation + _ -> pure (conn', False) + + xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> RcvMessage -> UTCTime -> CM () + xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ _) msg brokerTs = do + checkHostRole m memRole + unless (sameMemberId memId $ membership gInfo) $ + withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case + Right unknownMember@GroupMember {memberStatus = GSMemUnknown} -> do + updatedMember <- withStore $ \db -> updateUnknownMemberAnnounced db vr user m unknownMember memInfo + toView $ CEvtUnknownMemberAnnounced user gInfo m unknownMember updatedMember + memberAnnouncedToView updatedMember + Right _ -> messageError "x.grp.mem.new error: member already exists" + Left _ -> do + newMember <- withStore $ \db -> createNewGroupMember db user gInfo m memInfo GCPostMember GSMemAnnounced + memberAnnouncedToView newMember + where + memberAnnouncedToView announcedMember@GroupMember {groupMemberId, memberProfile} = do + let event = RGEMemberAdded groupMemberId (fromLocalProfile memberProfile) + ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent event) + groupMsgToView gInfo ci + toView $ CEvtJoinedGroupMemberConnecting user gInfo m announcedMember + + xGrpMemIntro :: GroupInfo -> GroupMember -> MemberInfo -> Maybe MemberRestrictions -> CM () + xGrpMemIntro gInfo@GroupInfo {chatSettings} m@GroupMember {memberRole, localDisplayName = c} memInfo@(MemberInfo memId _ memChatVRange _) memRestrictions = do + case memberCategory m of + GCHostMember -> + withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case + Right _ -> messageError "x.grp.mem.intro ignored: member already exists" + Left _ -> do + when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole c) + case memChatVRange of + Nothing -> messageError "x.grp.mem.intro: member chat version range incompatible" + Just (ChatVersionRange mcvr) + | maxVersion mcvr >= groupDirectInvVersion -> do + subMode <- chatReadVar subscriptionMode + -- [async agent commands] commands should be asynchronous, continuation is to send XGrpMemInv - have to remember one has completed and process on second + groupConnIds <- createConn subMode + let chatV = maybe (minVersion vr) (\peerVR -> vr `peerConnChatVersion` fromChatVRange peerVR) memChatVRange + void $ withStore $ \db -> createIntroReMember db user gInfo m chatV memInfo memRestrictions groupConnIds subMode + | otherwise -> messageError "x.grp.mem.intro: member chat version range incompatible" + _ -> messageError "x.grp.mem.intro can be only sent by host member" + where + createConn subMode = createAgentConnectionAsync user CFCreateConnGrpMemInv (chatHasNtfs chatSettings) SCMInvitation subMode + + sendXGrpMemInv :: Int64 -> Maybe ConnReqInvitation -> XGrpMemIntroCont -> CM () + sendXGrpMemInv hostConnId directConnReq XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} = do + hostConn <- withStore $ \db -> getConnectionById db vr user hostConnId + let msg = XGrpMemInv memberId IntroInvitation {groupConnReq, directConnReq} + void $ sendDirectMemberMessage hostConn msg groupId + withStore' $ \db -> updateGroupMemberStatusById db userId groupMemberId GSMemIntroInvited + + xGrpMemInv :: GroupInfo -> GroupMember -> MemberId -> IntroInvitation -> CM () + xGrpMemInv gInfo m memId introInv = do + case memberCategory m of + GCInviteeMember -> + withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case + Left _ -> messageError "x.grp.mem.inv error: referenced member does not exist" + Right reMember -> do + GroupMemberIntro {introId} <- withStore $ \db -> saveIntroInvitation db reMember m introInv + sendGroupMemberMessage gInfo reMember (XGrpMemFwd (memberInfo m) introInv) (Just introId) $ + withStore' $ + \db -> updateIntroStatus db introId GMIntroInvForwarded + _ -> messageError "x.grp.mem.inv can be only sent by invitee member" + + xGrpMemFwd :: GroupInfo -> GroupMember -> MemberInfo -> IntroInvitation -> CM () + xGrpMemFwd gInfo@GroupInfo {membership, chatSettings} m memInfo@(MemberInfo memId memRole memChatVRange _) introInv@IntroInvitation {groupConnReq, directConnReq} = do + let GroupMember {memberId = membershipMemId} = membership + checkHostRole m memRole + toMember <- + withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case + -- TODO if the missed messages are correctly sent as soon as there is connection before anything else is sent + -- the situation when member does not exist is an error + -- member receiving x.grp.mem.fwd should have also received x.grp.mem.new prior to that. + -- For now, this branch compensates for the lack of delayed message delivery. + Left _ -> withStore $ \db -> createNewGroupMember db user gInfo m memInfo GCPostMember GSMemAnnounced + Right m' -> pure m' + withStore' $ \db -> saveMemberInvitation db toMember introInv + subMode <- chatReadVar subscriptionMode + -- [incognito] send membership incognito profile, create direct connection as incognito + let membershipProfile = redactedMemberProfile $ fromLocalProfile $ memberProfile membership + dm <- encodeConnInfo $ XGrpMemInfo membershipMemId membershipProfile + -- [async agent commands] no continuation needed, but commands should be asynchronous for stability + groupConnIds <- joinAgentConnectionAsync user (chatHasNtfs chatSettings) groupConnReq dm subMode + directConnIds <- forM directConnReq $ \dcr -> joinAgentConnectionAsync user True dcr dm subMode + let customUserProfileId = localProfileId <$> incognitoMembershipProfile gInfo + mcvr = maybe chatInitialVRange fromChatVRange memChatVRange + chatV = vr `peerConnChatVersion` mcvr + withStore' $ \db -> createIntroToMemberContact db user m toMember chatV mcvr groupConnIds directConnIds customUserProfileId subMode + + xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> RcvMessage -> UTCTime -> CM () + xGrpMemRole gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId memRole msg brokerTs + | membershipMemId == memId = + let gInfo' = gInfo {membership = membership {memberRole = memRole}} + in changeMemberRole gInfo' membership $ RGEUserRole memRole + | otherwise = + withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case + Right member -> changeMemberRole gInfo member $ RGEMemberRole (groupMemberId' member) (fromLocalProfile $ memberProfile member) memRole + Left _ -> messageError "x.grp.mem.role with unknown member ID" + where + GroupMember {memberId = membershipMemId} = membership + changeMemberRole gInfo' member@GroupMember {memberRole = fromRole} gEvent + | senderRole < GRAdmin || senderRole < fromRole = messageError "x.grp.mem.role with insufficient member permissions" + | otherwise = do + withStore' $ \db -> updateGroupMemberRole db user member memRole + ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent gEvent) + groupMsgToView gInfo ci + toView CEvtMemberRole {user, groupInfo = gInfo', byMember = m, member = member {memberRole = memRole}, fromRole, toRole = memRole} + + checkHostRole :: GroupMember -> GroupMemberRole -> CM () + checkHostRole GroupMember {memberRole, localDisplayName} memRole = + when (memberRole < GRAdmin || memberRole < memRole) $ throwChatError (CEGroupContactRole localDisplayName) + + xGrpMemRestrict :: GroupInfo -> GroupMember -> MemberId -> MemberRestrictions -> RcvMessage -> UTCTime -> CM () + xGrpMemRestrict + gInfo@GroupInfo {membership = GroupMember {memberId = membershipMemId}} + m@GroupMember {memberRole = senderRole} + memId + MemberRestrictions {restriction} + msg + brokerTs + | membershipMemId == memId = + -- member shouldn't receive this message about themselves + messageError "x.grp.mem.restrict: admin blocks you" + | otherwise = + withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case + Right bm@GroupMember {groupMemberId = bmId, memberRole, blockedByAdmin, memberProfile = bmp} + | blockedByAdmin == mrsBlocked restriction -> pure () + | senderRole < GRModerator || senderRole < memberRole -> messageError "x.grp.mem.restrict with insufficient member permissions" + | otherwise -> do + bm' <- setMemberBlocked bm + toggleNtf bm' (not blocked) + let ciContent = CIRcvGroupEvent $ RGEMemberBlocked bmId (fromLocalProfile bmp) blocked + ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs ciContent + groupMsgToView gInfo ci + toView CEvtMemberBlockedForAll {user, groupInfo = gInfo, byMember = m, member = bm, blocked} + Left (SEGroupMemberNotFoundByMemberId _) -> do + bm <- createUnknownMember gInfo memId + bm' <- setMemberBlocked bm + toView $ CEvtUnknownMemberBlocked user gInfo m bm' + Left e -> throwError $ ChatErrorStore e + where + setMemberBlocked bm = withStore' $ \db -> updateGroupMemberBlocked db user gInfo restriction bm + blocked = mrsBlocked restriction + + xGrpMemCon :: GroupInfo -> GroupMember -> MemberId -> CM () + xGrpMemCon gInfo sendingMember memId = do + refMember <- withStore $ \db -> getGroupMemberByMemberId db vr user gInfo memId + case (memberCategory sendingMember, memberCategory refMember) of + (GCInviteeMember, GCInviteeMember) -> + withStore' (\db -> runExceptT $ getIntroduction db refMember sendingMember) >>= \case + Right intro -> inviteeXGrpMemCon intro + Left _ -> + withStore' (\db -> runExceptT $ getIntroduction db sendingMember refMember) >>= \case + Right intro -> forwardMemberXGrpMemCon intro + Left _ -> messageWarning "x.grp.mem.con: no introduction" + (GCInviteeMember, _) -> + withStore' (\db -> runExceptT $ getIntroduction db refMember sendingMember) >>= \case + Right intro -> inviteeXGrpMemCon intro + Left _ -> messageWarning "x.grp.mem.con: no introduction" + (_, GCInviteeMember) -> + withStore' (\db -> runExceptT $ getIntroduction db sendingMember refMember) >>= \case + Right intro -> forwardMemberXGrpMemCon intro + Left _ -> messageWarning "x.grp.mem.con: no introductiosupportn" + -- Note: we can allow XGrpMemCon to all member categories if we decide to support broader group forwarding, + -- deduplication (see saveGroupRcvMsg, saveGroupFwdRcvMsg) already supports sending XGrpMemCon + -- to any forwarding member, not only host/inviting member; + -- database would track all members connections then + -- (currently it's done via group_member_intros for introduced connections only) + _ -> + messageWarning "x.grp.mem.con: neither member is invitee" + where + inviteeXGrpMemCon :: GroupMemberIntro -> CM () + inviteeXGrpMemCon GroupMemberIntro {introId, introStatus} = case introStatus of + GMIntroReConnected -> updateStatus introId GMIntroConnected + GMIntroToConnected -> pure () + GMIntroConnected -> pure () + _ -> updateStatus introId GMIntroToConnected + forwardMemberXGrpMemCon :: GroupMemberIntro -> CM () + forwardMemberXGrpMemCon GroupMemberIntro {introId, introStatus} = case introStatus of + GMIntroToConnected -> updateStatus introId GMIntroConnected + GMIntroReConnected -> pure () + GMIntroConnected -> pure () + _ -> updateStatus introId GMIntroReConnected + updateStatus introId status = withStore' $ \db -> updateIntroStatus db introId status + + xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> Bool -> RcvMessage -> UTCTime -> CM () + xGrpMemDel gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId withMessages msg brokerTs = do + let GroupMember {memberId = membershipMemId} = membership + if membershipMemId == memId + then checkRole membership $ do + deleteGroupLinkIfExists user gInfo + -- member records are not deleted to keep history + members <- withStore' $ \db -> getGroupMembers db vr user gInfo + deleteMembersConnections user members + withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemRemoved + when withMessages $ deleteMessages membership SMDSnd + deleteMemberItem RGEUserDeleted + toView $ CEvtDeletedMemberUser user gInfo {membership = membership {memberStatus = GSMemRemoved}} m withMessages + else + withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case + Left _ -> messageError "x.grp.mem.del with unknown member ID" + Right member@GroupMember {groupMemberId, memberProfile} -> + checkRole member $ do + -- ? prohibit deleting member if it's the sender - sender should use x.grp.leave + deleteMemberConnection member + -- undeleted "member connected" chat item will prevent deletion of member record + deleteOrUpdateMemberRecord user member + when withMessages $ deleteMessages member SMDRcv + deleteMemberItem $ RGEMemberDeleted groupMemberId (fromLocalProfile memberProfile) + toView $ CEvtDeletedMember user gInfo m member {memberStatus = GSMemRemoved} withMessages + where + checkRole GroupMember {memberRole} a + | senderRole < GRAdmin || senderRole < memberRole = + messageError "x.grp.mem.del with insufficient member permissions" + | otherwise = a + deleteMemberItem gEvent = do + ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent gEvent) + groupMsgToView gInfo ci + deleteMessages :: MsgDirectionI d => GroupMember -> SMsgDirection d -> CM () + deleteMessages delMem msgDir + | groupFeatureMemberAllowed SGFFullDelete m gInfo = deleteGroupMemberCIs user gInfo delMem m msgDir + | otherwise = markGroupMemberCIsDeleted user gInfo delMem m + + xGrpLeave :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM () + xGrpLeave gInfo m msg brokerTs = do + deleteMemberConnection m + -- member record is not deleted to allow creation of "member left" chat item + withStore' $ \db -> updateGroupMemberStatus db userId m GSMemLeft + ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent RGEMemberLeft) + groupMsgToView gInfo ci + toView $ CEvtLeftMember user gInfo m {memberStatus = GSMemLeft} + + xGrpDel :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM () + xGrpDel gInfo@GroupInfo {membership} m@GroupMember {memberRole} msg brokerTs = do + when (memberRole /= GROwner) $ throwChatError $ CEGroupUserRole gInfo GROwner + ms <- withStore' $ \db -> do + members <- getGroupMembers db vr user gInfo + updateGroupMemberStatus db userId membership GSMemGroupDeleted + pure members + -- member records are not deleted to keep history + deleteMembersConnections user ms + ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent RGEGroupDeleted) + groupMsgToView gInfo ci + toView $ CEvtGroupDeleted user gInfo {membership = membership {memberStatus = GSMemGroupDeleted}} m + + xGrpInfo :: GroupInfo -> GroupMember -> GroupProfile -> RcvMessage -> UTCTime -> CM () + xGrpInfo g@GroupInfo {groupProfile = p, businessChat} m@GroupMember {memberRole} p' msg brokerTs + | memberRole < GROwner = messageError "x.grp.info with insufficient member permissions" + | otherwise = case businessChat of + Nothing -> unless (p == p') $ do + g' <- withStore $ \db -> updateGroupProfile db user g p' + toView $ CEvtGroupUpdated user g g' (Just m) + let cd = CDGroupRcv g' m + unless (sameGroupProfileInfo p p') $ do + ci <- saveRcvChatItemNoParse user cd msg brokerTs (CIRcvGroupEvent $ RGEGroupUpdated p') + groupMsgToView g' ci + createGroupFeatureChangedItems user cd CIRcvGroupFeature g g' + Just _ -> updateGroupPrefs_ g m $ fromMaybe defaultBusinessGroupPrefs $ groupPreferences p' + + xGrpPrefs :: GroupInfo -> GroupMember -> GroupPreferences -> CM () + xGrpPrefs g m@GroupMember {memberRole} ps' + | memberRole < GROwner = messageError "x.grp.prefs with insufficient member permissions" + | otherwise = updateGroupPrefs_ g m ps' + + updateGroupPrefs_ :: GroupInfo -> GroupMember -> GroupPreferences -> CM () + updateGroupPrefs_ g@GroupInfo {groupProfile = p} m ps' = + unless (groupPreferences p == Just ps') $ do + g' <- withStore' $ \db -> updateGroupPreferences db user g ps' + toView $ CEvtGroupUpdated user g g' (Just m) + let cd = CDGroupRcv g' m + createGroupFeatureChangedItems user cd CIRcvGroupFeature g g' + + xGrpDirectInv :: GroupInfo -> GroupMember -> Connection -> ConnReqInvitation -> Maybe MsgContent -> RcvMessage -> UTCTime -> CM () + xGrpDirectInv g m mConn connReq mContent_ msg brokerTs = do + unless (groupFeatureMemberAllowed SGFDirectMessages m g) $ messageError "x.grp.direct.inv: direct messages not allowed" + let GroupMember {memberContactId} = m + subMode <- chatReadVar subscriptionMode + case memberContactId of + Nothing -> createNewContact subMode + Just mContactId -> do + mCt <- withStore $ \db -> getContact db vr user mContactId + let Contact {activeConn, contactGrpInvSent} = mCt + forM_ activeConn $ \Connection {connId} -> + if contactGrpInvSent + then do + ownConnReq <- withStore $ \db -> getConnReqInv db connId + -- in case both members sent x.grp.direct.inv before receiving other's for processing, + -- only the one who received greater connReq joins, the other creates items and waits for confirmation + if strEncode connReq > strEncode ownConnReq + then joinExistingContact subMode mCt + else createItems mCt m + else joinExistingContact subMode mCt + where + joinExistingContact subMode mCt = do + connIds <- joinConn subMode + mCt' <- withStore $ \db -> updateMemberContactInvited db user connIds g mConn mCt subMode + createItems mCt' m + securityCodeChanged mCt' + createNewContact subMode = do + connIds <- joinConn subMode + -- [incognito] reuse membership incognito profile + (mCt', m') <- withStore' $ \db -> createMemberContactInvited db user connIds g m mConn subMode + createItems mCt' m' + joinConn subMode = do + -- [incognito] send membership incognito profile + let p = userProfileToSend user (fromLocalProfile <$> incognitoMembershipProfile g) Nothing False + -- TODO PQ should negotitate contact connection with PQSupportOn? (use encodeConnInfoPQ) + dm <- encodeConnInfo $ XInfo p + joinAgentConnectionAsync user True connReq dm subMode + createItems mCt' m' = do + createInternalChatItem user (CDGroupRcv g m') (CIRcvGroupEvent RGEMemberCreatedContact) Nothing + toView $ CEvtNewMemberContactReceivedInv user mCt' g m' + forM_ mContent_ $ \mc -> do + ci <- saveRcvChatItem user (CDDirectRcv mCt') msg brokerTs (CIRcvMsgContent mc, msgContentTexts mc) + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat mCt') ci] + + securityCodeChanged :: Contact -> CM () + securityCodeChanged ct = do + toViewTE $ TEContactVerificationReset user ct + createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent RCEVerificationCodeReset) Nothing + + xGrpMsgForward :: GroupInfo -> GroupMember -> MemberId -> ChatMessage 'Json -> UTCTime -> CM () + xGrpMsgForward gInfo@GroupInfo {groupId} m@GroupMember {memberRole, localDisplayName} memberId msg msgTs = do + when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole localDisplayName) + withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memberId) >>= \case + Right author -> processForwardedMsg author msg + Left (SEGroupMemberNotFoundByMemberId _) -> do + unknownAuthor <- createUnknownMember gInfo memberId + toView $ CEvtUnknownMemberCreated user gInfo m unknownAuthor + processForwardedMsg unknownAuthor msg + Left e -> throwError $ ChatErrorStore e + where + -- Note: forwarded group events (see forwardedGroupMsg) should include msgId to be deduplicated + processForwardedMsg :: GroupMember -> ChatMessage 'Json -> CM () + processForwardedMsg author chatMsg = do + let body = LB.toStrict $ J.encode msg + rcvMsg@RcvMessage {chatMsgEvent = ACME _ event} <- saveGroupFwdRcvMsg user groupId m author body chatMsg + case event of + XMsgNew mc -> memberCanSend author $ newGroupContentMessage gInfo author mc rcvMsg msgTs True + XMsgFileDescr sharedMsgId fileDescr -> memberCanSend author $ groupMessageFileDescription gInfo author sharedMsgId fileDescr + XMsgUpdate sharedMsgId mContent mentions ttl live -> memberCanSend author $ groupMessageUpdate gInfo author sharedMsgId mContent mentions rcvMsg msgTs ttl live + XMsgDel sharedMsgId memId -> groupMessageDelete gInfo author sharedMsgId memId rcvMsg msgTs + XMsgReact sharedMsgId (Just memId) reaction add -> groupMsgReaction gInfo author sharedMsgId memId reaction add rcvMsg msgTs + XFileCancel sharedMsgId -> xFileCancelGroup gInfo author sharedMsgId + XInfo p -> xInfoMember gInfo author p msgTs + XGrpMemNew memInfo -> xGrpMemNew gInfo author memInfo rcvMsg msgTs + XGrpMemRole memId memRole -> xGrpMemRole gInfo author memId memRole rcvMsg msgTs + XGrpMemDel memId withMessages -> xGrpMemDel gInfo author memId withMessages rcvMsg msgTs + XGrpLeave -> xGrpLeave gInfo author rcvMsg msgTs + XGrpDel -> xGrpDel gInfo author rcvMsg msgTs + XGrpInfo p' -> xGrpInfo gInfo author p' rcvMsg msgTs + XGrpPrefs ps' -> xGrpPrefs gInfo author ps' + _ -> messageError $ "x.grp.msg.forward: unsupported forwarded event " <> T.pack (show $ toCMEventTag event) + + createUnknownMember :: GroupInfo -> MemberId -> CM GroupMember + createUnknownMember gInfo memberId = do + let name = nameFromMemberId memberId + withStore $ \db -> createNewUnknownGroupMember db vr user gInfo memberId name + + directMsgReceived :: Contact -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> CM () + directMsgReceived ct conn@Connection {connId} msgMeta msgRcpts = do + checkIntegrityCreateItem (CDDirectRcv ct) msgMeta `catchChatError` \_ -> pure () + forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do + withStore' $ \db -> updateSndMsgDeliveryStatus db connId agentMsgId $ MDSSndRcvd msgRcptStatus + updateDirectItemStatus ct conn agentMsgId $ CISSndRcvd msgRcptStatus SSPComplete + + groupMsgReceived :: GroupInfo -> GroupMember -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> CM () + groupMsgReceived gInfo m conn@Connection {connId} msgMeta msgRcpts = do + checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure () + forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do + withStore' $ \db -> updateSndMsgDeliveryStatus db connId agentMsgId $ MDSSndRcvd msgRcptStatus + updateGroupItemsStatus gInfo m conn agentMsgId (GSSRcvd msgRcptStatus) Nothing + + -- Searches chat items for many agent message IDs and updates their status + updateDirectItemsStatusMsgs :: Contact -> Connection -> [AgentMsgId] -> CIStatus 'MDSnd -> CM () + updateDirectItemsStatusMsgs ct conn msgIds newStatus = do + cis <- withStore' $ \db -> forM msgIds $ \msgId -> runExceptT $ updateDirectItemsStatus' db ct conn msgId newStatus + let acis = map ctItem $ concat $ rights cis + unless (null acis) $ toView $ CEvtChatItemsStatusesUpdated user acis + where + ctItem = AChatItem SCTDirect SMDSnd (DirectChat ct) + + updateDirectItemStatus :: Contact -> Connection -> AgentMsgId -> CIStatus 'MDSnd -> CM () + updateDirectItemStatus ct conn msgId newStatus = do + cis <- withStore $ \db -> updateDirectItemsStatus' db ct conn msgId newStatus + let acis = map ctItem cis + unless (null acis) $ toView $ CEvtChatItemsStatusesUpdated user acis + where + ctItem = AChatItem SCTDirect SMDSnd (DirectChat ct) + + updateDirectItemsStatus' :: DB.Connection -> Contact -> Connection -> AgentMsgId -> CIStatus 'MDSnd -> ExceptT StoreError IO [ChatItem 'CTDirect 'MDSnd] + updateDirectItemsStatus' db ct@Contact {contactId} Connection {connId} msgId newStatus = do + items <- liftIO $ getDirectChatItemsByAgentMsgId db user contactId connId msgId + catMaybes <$> mapM updateItem items + where + updateItem :: CChatItem 'CTDirect -> ExceptT StoreError IO (Maybe (ChatItem 'CTDirect 'MDSnd)) + updateItem = \case + (CChatItem SMDSnd ChatItem {meta = CIMeta {itemStatus = CISSndRcvd _ _}}) -> pure Nothing + (CChatItem SMDSnd ChatItem {meta = CIMeta {itemId, itemStatus}}) + | itemStatus == newStatus -> pure Nothing + | otherwise -> Just <$> updateDirectChatItemStatus db user ct itemId newStatus + _ -> pure Nothing + + updateGroupMemSndStatus' :: DB.Connection -> ChatItemId -> GroupMemberId -> GroupSndStatus -> IO Bool + updateGroupMemSndStatus' db itemId groupMemberId newStatus = + runExceptT (getGroupSndStatus db itemId groupMemberId) >>= \case + Right (GSSRcvd _) -> pure False + Right memStatus + | memStatus == newStatus -> pure False + | otherwise -> updateGroupSndStatus db itemId groupMemberId newStatus $> True + _ -> pure False + + updateGroupItemsStatus :: GroupInfo -> GroupMember -> Connection -> AgentMsgId -> GroupSndStatus -> Maybe Bool -> CM () + updateGroupItemsStatus gInfo@GroupInfo {groupId} GroupMember {groupMemberId} Connection {connId} msgId newMemStatus viaProxy_ = do + items <- withStore' (\db -> getGroupChatItemsByAgentMsgId db user groupId connId msgId) + cis <- catMaybes <$> withStore (\db -> mapM (updateItem db) items) + let acis = map gItem cis + unless (null acis) $ toView $ CEvtChatItemsStatusesUpdated user acis + where + gItem = AChatItem SCTGroup SMDSnd (GroupChat gInfo) + updateItem :: DB.Connection -> CChatItem 'CTGroup -> ExceptT StoreError IO (Maybe (ChatItem 'CTGroup 'MDSnd)) + updateItem db = \case + (CChatItem SMDSnd ChatItem {meta = CIMeta {itemStatus = CISSndRcvd _ SSPComplete}}) -> pure Nothing + (CChatItem SMDSnd ChatItem {meta = CIMeta {itemId, itemStatus}}) -> do + forM_ viaProxy_ $ \viaProxy -> liftIO $ setGroupSndViaProxy db itemId groupMemberId viaProxy + memStatusChanged <- liftIO $ updateGroupMemSndStatus' db itemId groupMemberId newMemStatus + if memStatusChanged + then do + memStatusCounts <- liftIO $ getGroupSndStatusCounts db itemId + let newStatus = membersGroupItemStatus memStatusCounts + if newStatus /= itemStatus + then Just <$> updateGroupChatItemStatus db user gInfo itemId newStatus + else pure Nothing + else pure Nothing + _ -> pure Nothing diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs index 6ee4898e3d..9811556fe0 100644 --- a/src/Simplex/Chat/Markdown.hs +++ b/src/Simplex/Chat/Markdown.hs @@ -4,6 +4,7 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} {-# HLINT ignore "Use newtype instead of data" #-} @@ -16,25 +17,24 @@ import qualified Data.Aeson as J import qualified Data.Aeson.TH as JQ import Data.Attoparsec.Text (Parser) import qualified Data.Attoparsec.Text as A -import Data.Char (isDigit, isPunctuation) +import Data.Char (isAlpha, isAscii, isDigit, isPunctuation, isSpace) import Data.Either (fromRight) import Data.Functor (($>)) import Data.List (foldl', intercalate) import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L -import Data.Maybe (fromMaybe, isNothing) +import Data.Maybe (fromMaybe, isNothing, mapMaybe) import Data.Semigroup (sconcat) import Data.String import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Simplex.Chat.Types -import Simplex.Chat.Types.Util -import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), ConnReqScheme (..), ConnReqUriData (..), ConnectionRequestUri (..), SMPQueue (..)) +import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnReqUriData (..), ConnShortLink (..), ConnectionLink (..), ConnectionRequestUri (..), ContactConnType (..), SMPQueue (..), simplexConnReqUri, simplexShortLink) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fstToLower, sumTypeJSON) import Simplex.Messaging.Protocol (ProtocolServer (..)) -import Simplex.Messaging.Util (safeDecodeUtf8) +import Simplex.Messaging.Util (decodeJSON, safeDecodeUtf8) import System.Console.ANSI.Types import qualified Text.Email.Validate as Email @@ -49,19 +49,29 @@ data Format | Secret | Colored {color :: FormatColor} | Uri - | SimplexLink {linkType :: SimplexLinkType, simplexUri :: Text, smpHosts :: NonEmpty Text} + | SimplexLink {linkType :: SimplexLinkType, simplexUri :: AConnectionLink, smpHosts :: NonEmpty Text} + | Mention {memberName :: Text} | Email | Phone deriving (Eq, Show) -data SimplexLinkType = XLContact | XLInvitation | XLGroup +mentionedNames :: MarkdownList -> [Text] +mentionedNames = mapMaybe (\(FormattedText f _) -> mentionedName =<< f) + where + mentionedName = \case + Mention name -> Just name + _ -> Nothing + +data SimplexLinkType = XLContact | XLInvitation | XLGroup | XLChannel deriving (Eq, Show) colored :: Color -> Format colored = Colored . FormatColor +{-# INLINE colored #-} markdown :: Format -> Text -> Markdown markdown = Markdown . Just +{-# INLINE markdown #-} instance Semigroup Markdown where m <> (Markdown _ "") = m @@ -143,6 +153,11 @@ markdownToList (m1 :|: m2) = markdownToList m1 <> markdownToList m2 parseMarkdown :: Text -> Markdown parseMarkdown s = fromRight (unmarked s) $ A.parseOnly (markdownP <* A.endOfInput) s +isSimplexLink :: Format -> Bool +isSimplexLink = \case + SimplexLink {} -> True + _ -> False + markdownP :: Parser Markdown markdownP = mconcat <$> A.many' fragmentP where @@ -158,6 +173,7 @@ markdownP = mconcat <$> A.many' fragmentP '`' -> formattedP '`' Snippet '#' -> A.char '#' *> secretP '!' -> coloredP <|> wordP + '@' -> mentionP <|> wordP _ | isDigit c -> phoneP <|> wordP | otherwise -> wordP @@ -187,14 +203,20 @@ markdownP = mconcat <$> A.many' fragmentP if T.null s || T.last s == ' ' then fail "not colored" else pure $ markdown (colored clr) s + mentionP = do + c <- A.char '@' *> A.peekChar' + (name, punct) <- displayNameTextP_ + let sName = if c == '\'' then '\'' `T.cons` name `T.snoc` '\'' else name + mention = markdown (Mention name) ('@' `T.cons` sName) + pure $ if T.null punct then mention else mention :|: unmarked punct colorP = A.anyChar >>= \case - 'r' -> "ed" $> Red <|> pure Red - 'g' -> "reen" $> Green <|> pure Green - 'b' -> "lue" $> Blue <|> pure Blue - 'y' -> "ellow" $> Yellow <|> pure Yellow - 'c' -> "yan" $> Cyan <|> pure Cyan - 'm' -> "agenta" $> Magenta <|> pure Magenta + 'r' -> optional "ed" $> Red + 'g' -> optional "reen" $> Green + 'b' -> optional "lue" $> Blue + 'y' -> optional "ellow" $> Yellow + 'c' -> optional "yan" $> Cyan + 'm' -> optional "agenta" $> Magenta '1' -> pure Red '2' -> pure Green '3' -> pure Blue @@ -216,31 +238,105 @@ markdownP = mconcat <$> A.many' fragmentP wordMD :: Text -> Markdown wordMD s | T.null s = unmarked s - | isUri s = - let t = T.takeWhileEnd isPunctuation s - uri = uriMarkdown $ T.dropWhileEnd isPunctuation s - in if T.null t then uri else uri :|: unmarked t - | isEmail s = markdown Email s + | isUri s' = res $ uriMarkdown s' + | isDomain s' = res $ markdown Uri s' + | isEmail s' = res $ markdown Email s' | otherwise = unmarked s + where + punct = T.takeWhileEnd isPunctuation' s + s' = T.dropWhileEnd isPunctuation' s + res md' = if T.null punct then md' else md' :|: unmarked punct + isPunctuation' = \case + '/' -> False + ')' -> False + c -> isPunctuation c uriMarkdown s = case strDecode $ encodeUtf8 s of - Right cReq -> markdown (simplexUriFormat cReq) s + Right cLink -> markdown (simplexUriFormat cLink) s _ -> markdown Uri s isUri s = T.length s >= 10 && any (`T.isPrefixOf` s) ["http://", "https://", "simplex:/"] + -- matches what is likely to be a domain, not all valid domain names + isDomain s = case T.splitOn "." s of + [name, tld] -> isDomain_ name tld + [sub, name, tld] -> T.length sub >= 3 && T.length sub <= 8 && isDomain_ name tld + _ -> False + where + isDomain_ name tld = + (let n = T.length name in n >= 1 && n <= 24) + && (let n = T.length tld in n >= 2 && n <= 8) + && (let p c = isAscii c && isAlpha c in T.all p name && T.all p tld) isEmail s = T.any (== '@') s && Email.isValid (encodeUtf8 s) noFormat = pure . unmarked - simplexUriFormat :: AConnectionRequestUri -> Format + simplexUriFormat :: AConnectionLink -> Format simplexUriFormat = \case - ACR _ (CRContactUri crData) -> - let uri = safeDecodeUtf8 . strEncode $ CRContactUri crData {crScheme = CRSSimplex} - in SimplexLink (linkType' crData) uri $ uriHosts crData - ACR _ (CRInvitationUri crData e2e) -> - let uri = safeDecodeUtf8 . strEncode $ CRInvitationUri crData {crScheme = CRSSimplex} e2e - in SimplexLink XLInvitation uri $ uriHosts crData - where - uriHosts ConnReqUriData {crSmpQueues} = L.map (safeDecodeUtf8 . strEncode) $ sconcat $ L.map (host . qServer) crSmpQueues - linkType' ConnReqUriData {crClientData} = case crClientData >>= decodeJSON of - Just (CRDataGroup _) -> XLGroup - Nothing -> XLContact + ACL m (CLFull cReq) -> case cReq of + CRContactUri crData -> SimplexLink (linkType' crData) cLink $ uriHosts crData + CRInvitationUri crData _ -> SimplexLink XLInvitation cLink $ uriHosts crData + where + cLink = ACL m $ CLFull $ simplexConnReqUri cReq + uriHosts ConnReqUriData {crSmpQueues} = L.map strEncodeText $ sconcat $ L.map (host . qServer) crSmpQueues + linkType' ConnReqUriData {crClientData} = case crClientData >>= decodeJSON of + Just (CRDataGroup _) -> XLGroup + Nothing -> XLContact + ACL m (CLShort sLnk) -> case sLnk of + CSLContact _ ct srv _ -> SimplexLink (linkType' ct) cLink $ uriHosts srv + CSLInvitation _ srv _ _ -> SimplexLink XLInvitation cLink $ uriHosts srv + where + cLink = ACL m $ CLShort $ simplexShortLink sLnk + uriHosts srv = L.map strEncodeText $ host srv + linkType' = \case + CCTGroup -> XLGroup + CCTChannel -> XLChannel + CCTContact -> XLContact + strEncodeText :: StrEncoding a => a -> Text + strEncodeText = safeDecodeUtf8 . strEncode + +markdownText :: FormattedText -> Text +markdownText (FormattedText f_ t) = case f_ of + Nothing -> t + Just f -> case f of + Bold -> around '*' + Italic -> around '_' + StrikeThrough -> around '~' + Snippet -> around '`' + Secret -> around '#' + Colored (FormatColor c) -> color c + Uri -> t + SimplexLink {} -> t + Mention _ -> t + Email -> t + Phone -> t + where + around c = c `T.cons` t `T.snoc` c + color c = case colorStr c of + Just cStr -> cStr <> t `T.snoc` '!' + Nothing -> t + colorStr = \case + Red -> Just "!1 " + Green -> Just "!2 " + Blue -> Just "!3 " + Yellow -> Just "!4 " + Cyan -> Just "!5 " + Magenta -> Just "!6 " + Black -> Nothing + White -> Nothing + +displayNameTextP :: Parser Text +displayNameTextP = displayNameTextP_ >>= \(t, sfx) -> if T.null sfx then pure t else fail "Name ends with punctuation" +{-# INLINE displayNameTextP #-} + +displayNameTextP_ :: Parser (Text, Text) +displayNameTextP_ = (,"") <$> quoted '\'' <|> splitPunctuation <$> takeNameTill isSpace + where + takeNameTill p = + A.peekChar' >>= \c -> + if refChar c then A.takeTill p else fail "invalid first character in display name" + splitPunctuation s = (T.dropWhileEnd isPunctuation s, T.takeWhileEnd isPunctuation s) + quoted c = A.char c *> takeNameTill (== c) <* A.char c + refChar c = c > ' ' && c /= '#' && c /= '@' && c /= '\'' + +-- quotes names that contain spaces or end on punctuation +viewName :: Text -> Text +viewName s = if T.any isSpace s || maybe False (isPunctuation . snd) (T.unsnoc s) then "'" <> s <> "'" else s $(JQ.deriveJSON (enumJSON $ dropPrefix "XL") ''SimplexLinkType) diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 74b41dc9f2..79d416dee5 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -5,50 +5,60 @@ {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} +{-# OPTIONS_GHC -fno-warn-operator-whitespace #-} module Simplex.Chat.Messages where import Control.Applicative ((<|>)) +import Control.Monad ((>=>)) import Data.Aeson (FromJSON, ToJSON, (.:)) import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Base64 as B64 -import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Lazy.Char8 as LB import Data.Char (isSpace) import Data.Int (Int64) +import Data.Kind (Constraint) +import Data.List.NonEmpty (NonEmpty) +import Data.Map.Strict (Map) import Data.Maybe (fromMaybe, isJust, isNothing) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1, encodeUtf8) -import Data.Time.Clock (UTCTime, diffUTCTime, nominalDay) +import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime, nominalDay) import Data.Type.Equality import Data.Typeable (Typeable) -import Database.SQLite.Simple.FromField (FromField (..)) -import Database.SQLite.Simple.ToField (ToField (..)) +import GHC.TypeLits (ErrorMessage (ShowType, type (:<>:)), TypeError) +import qualified GHC.TypeLits as Type import Simplex.Chat.Markdown import Simplex.Chat.Messages.CIContent +import Simplex.Chat.Options.DB (FromField (..), ToField (..)) import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared +import Simplex.Chat.Types.Util (textParseJSON) import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgMeta (..), MsgReceiptStatus (..)) +import Simplex.Messaging.Agent.Store.DB (fromTextField_) import Simplex.Messaging.Crypto.File (CryptoFile (..)) import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, parseAll, sumTypeJSON) -import Simplex.Messaging.Protocol (MsgBody) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, sumTypeJSON) +import Simplex.Messaging.Protocol (BlockingInfo, MsgBody) import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>)) -data ChatType = CTDirect | CTGroup | CTContactRequest | CTContactConnection +data ChatType = CTDirect | CTGroup | CTLocal | CTContactRequest | CTContactConnection deriving (Eq, Show, Ord) data ChatName = ChatName {chatType :: ChatType, chatName :: Text} @@ -58,6 +68,7 @@ chatTypeStr :: ChatType -> Text chatTypeStr = \case CTDirect -> "@" CTGroup -> "#" + CTLocal -> "*" CTContactRequest -> "<@" CTContactConnection -> ":" @@ -70,6 +81,7 @@ data ChatRef = ChatRef ChatType Int64 data ChatInfo (c :: ChatType) where DirectChat :: Contact -> ChatInfo 'CTDirect GroupChat :: GroupInfo -> ChatInfo 'CTGroup + LocalChat :: NoteFolder -> ChatInfo 'CTLocal ContactRequest :: UserContactRequest -> ChatInfo 'CTContactRequest ContactConnection :: PendingContactConnection -> ChatInfo 'CTContactConnection @@ -81,17 +93,11 @@ chatInfoChatTs = \case GroupChat GroupInfo {chatTs} -> chatTs _ -> Nothing -chatInfoUpdatedAt :: ChatInfo c -> UTCTime -chatInfoUpdatedAt = \case - DirectChat Contact {updatedAt} -> updatedAt - GroupChat GroupInfo {updatedAt} -> updatedAt - ContactRequest UserContactRequest {updatedAt} -> updatedAt - ContactConnection PendingContactConnection {updatedAt} -> updatedAt - chatInfoToRef :: ChatInfo c -> ChatRef chatInfoToRef = \case DirectChat Contact {contactId} -> ChatRef CTDirect contactId GroupChat GroupInfo {groupId} -> ChatRef CTGroup groupId + LocalChat NoteFolder {noteFolderId} -> ChatRef CTLocal noteFolderId ContactRequest UserContactRequest {contactRequestId} -> ChatRef CTContactRequest contactRequestId ContactConnection PendingContactConnection {pccConnId} -> ChatRef CTContactConnection pccConnId @@ -103,6 +109,7 @@ chatInfoMembership = \case data JSONChatInfo = JCInfoDirect {contact :: Contact} | JCInfoGroup {groupInfo :: GroupInfo} + | JCInfoLocal {noteFolder :: NoteFolder} | JCInfoContactRequest {contactRequest :: UserContactRequest} | JCInfoContactConnection {contactConnection :: PendingContactConnection} @@ -119,6 +126,7 @@ jsonChatInfo :: ChatInfo c -> JSONChatInfo jsonChatInfo = \case DirectChat c -> JCInfoDirect c GroupChat g -> JCInfoGroup g + LocalChat l -> JCInfoLocal l ContactRequest g -> JCInfoContactRequest g ContactConnection c -> JCInfoContactConnection c @@ -130,6 +138,7 @@ jsonAChatInfo :: JSONChatInfo -> AChatInfo jsonAChatInfo = \case JCInfoDirect c -> AChatInfo SCTDirect $ DirectChat c JCInfoGroup g -> AChatInfo SCTGroup $ GroupChat g + JCInfoLocal l -> AChatInfo SCTLocal $ LocalChat l JCInfoContactRequest g -> AChatInfo SCTContactRequest $ ContactRequest g JCInfoContactConnection c -> AChatInfo SCTContactConnection $ ContactConnection c @@ -144,6 +153,9 @@ data ChatItem (c :: ChatType) (d :: MsgDirection) = ChatItem { chatDir :: CIDirection c d, meta :: CIMeta c d, content :: CIContent d, + -- The `mentions` map prevents loading all members from UI. + -- The key is a name used in the message text, used to look up CIMention. + mentions :: Map MemberName CIMention, formattedText :: Maybe MarkdownList, quotedItem :: Maybe (CIQuote c), reactions :: [CIReactionCount], @@ -151,24 +163,33 @@ data ChatItem (c :: ChatType) (d :: MsgDirection) = ChatItem } deriving (Show) -isMention :: ChatItem c d -> Bool -isMention ChatItem {chatDir, quotedItem} = case chatDir of - CIDirectRcv -> userItem quotedItem - CIGroupRcv _ -> userItem quotedItem - _ -> False - where - userItem = \case - Nothing -> False - Just CIQuote {chatDir = cd} -> case cd of - CIQDirectSnd -> True - CIQGroupSnd -> True - _ -> False +data NotInHistory = NotInHistory + +data CIMention = CIMention + { memberId :: MemberId, + -- member record can be created later than the mention is received + memberRef :: Maybe CIMentionMember + } + deriving (Eq, Show) + +data CIMentionMember = CIMentionMember + { groupMemberId :: GroupMemberId, + displayName :: Text, -- use `displayName` in copy/share actions + localAlias :: Maybe Text, -- use `fromMaybe displayName localAlias` in chat view + memberRole :: GroupMemberRole -- shown for admins/owners in the message + } + deriving (Eq, Show) + +isUserMention :: ChatItem c d -> Bool +isUserMention ChatItem {meta = CIMeta {userMention}} = userMention data CIDirection (c :: ChatType) (d :: MsgDirection) where CIDirectSnd :: CIDirection 'CTDirect 'MDSnd CIDirectRcv :: CIDirection 'CTDirect 'MDRcv CIGroupSnd :: CIDirection 'CTGroup 'MDSnd CIGroupRcv :: GroupMember -> CIDirection 'CTGroup 'MDRcv + CILocalSnd :: CIDirection 'CTLocal 'MDSnd + CILocalRcv :: CIDirection 'CTLocal 'MDRcv deriving instance Show (CIDirection c d) @@ -181,6 +202,8 @@ data JSONCIDirection | JCIDirectRcv | JCIGroupSnd | JCIGroupRcv {groupMember :: GroupMember} + | JCILocalSnd + | JCILocalRcv deriving (Show) jsonCIDirection :: CIDirection c d -> JSONCIDirection @@ -189,6 +212,8 @@ jsonCIDirection = \case CIDirectRcv -> JCIDirectRcv CIGroupSnd -> JCIGroupSnd CIGroupRcv m -> JCIGroupRcv m + CILocalSnd -> JCILocalSnd + CILocalRcv -> JCILocalRcv jsonACIDirection :: JSONCIDirection -> ACIDirection jsonACIDirection = \case @@ -196,6 +221,8 @@ jsonACIDirection = \case JCIDirectRcv -> ACID SCTDirect SMDRcv CIDirectRcv JCIGroupSnd -> ACID SCTGroup SMDSnd CIGroupSnd JCIGroupRcv m -> ACID SCTGroup SMDRcv $ CIGroupRcv m + JCILocalSnd -> ACID SCTLocal SMDSnd CILocalSnd + JCILocalRcv -> ACID SCTLocal SMDRcv CILocalRcv data CIReactionCount = CIReactionCount {reaction :: MsgReaction, userReacted :: Bool, totalReacted :: Int} deriving (Show) @@ -204,8 +231,8 @@ data CChatItem c = forall d. MsgDirectionI d => CChatItem (SMsgDirection d) (Cha deriving instance Show (CChatItem c) -cchatItemId :: CChatItem c -> ChatItemId -cchatItemId (CChatItem _ ci) = chatItemId' ci +cChatItemId :: CChatItem c -> ChatItemId +cChatItemId (CChatItem _ ci) = chatItemId' ci chatItemId' :: ChatItem c d -> ChatItemId chatItemId' ChatItem {meta = CIMeta {itemId}} = itemId @@ -216,6 +243,12 @@ chatItemTs (CChatItem _ ci) = chatItemTs' ci chatItemTs' :: ChatItem c d -> UTCTime chatItemTs' ChatItem {meta = CIMeta {itemTs}} = itemTs +ciCreatedAt :: CChatItem c -> UTCTime +ciCreatedAt (CChatItem _ ci) = ciCreatedAt' ci + +ciCreatedAt' :: ChatItem c d -> UTCTime +ciCreatedAt' ChatItem {meta = CIMeta {createdAt}} = createdAt + chatItemTimed :: ChatItem c d -> Maybe CITimed chatItemTimed ChatItem {meta = CIMeta {itemTimed}} = itemTimed @@ -236,6 +269,8 @@ data ChatDirection (c :: ChatType) (d :: MsgDirection) where CDDirectRcv :: Contact -> ChatDirection 'CTDirect 'MDRcv CDGroupSnd :: GroupInfo -> ChatDirection 'CTGroup 'MDSnd CDGroupRcv :: GroupInfo -> GroupMember -> ChatDirection 'CTGroup 'MDRcv + CDLocalSnd :: NoteFolder -> ChatDirection 'CTLocal 'MDSnd + CDLocalRcv :: NoteFolder -> ChatDirection 'CTLocal 'MDRcv toCIDirection :: ChatDirection c d -> CIDirection c d toCIDirection = \case @@ -243,6 +278,8 @@ toCIDirection = \case CDDirectRcv _ -> CIDirectRcv CDGroupSnd _ -> CIGroupSnd CDGroupRcv _ m -> CIGroupRcv m + CDLocalSnd _ -> CILocalSnd + CDLocalRcv _ -> CILocalRcv toChatInfo :: ChatDirection c d -> ChatInfo c toChatInfo = \case @@ -250,6 +287,14 @@ toChatInfo = \case CDDirectRcv c -> DirectChat c CDGroupSnd g -> GroupChat g CDGroupRcv g _ -> GroupChat g + CDLocalSnd l -> LocalChat l + CDLocalRcv l -> LocalChat l + +contactChatDeleted :: ChatDirection c d -> Bool +contactChatDeleted = \case + CDDirectSnd Contact {chatDeleted} -> chatDeleted + CDDirectRcv Contact {chatDeleted} -> chatDeleted + _ -> False data NewChatItem d = NewChatItem { createdByMsgId :: Maybe MessageId, @@ -277,12 +322,23 @@ data AChat = forall c. ChatTypeI c => AChat (SChatType c) (Chat c) deriving instance Show AChat data ChatStats = ChatStats - { unreadCount :: Int, + { unreadCount :: Int, -- returned both in /_get chat initial API and in /_get chats API + unreadMentions :: Int, -- returned both in /_get chat initial API and in /_get chats API + reportsCount :: Int, -- returned both in /_get chat initial API and in /_get chats API minUnreadItemId :: ChatItemId, unreadChat :: Bool } deriving (Show) +emptyChatStats :: ChatStats +emptyChatStats = ChatStats 0 0 0 0 False + +data NavigationInfo = NavigationInfo + { afterUnread :: Int, + afterTotal :: Int + } + deriving (Show) + -- | type to show a mix of messages from multiple chats data AChatItem = forall c d. (ChatTypeI c, MsgDirectionI d) => AChatItem (SChatType c) (SMsgDirection d) (ChatInfo c) (ChatItem c d) @@ -301,6 +357,9 @@ aChatItemId (AChatItem _ _ _ ci) = chatItemId' ci aChatItemTs :: AChatItem -> UTCTime aChatItemTs (AChatItem _ _ _ ci) = chatItemTs' ci +aChatItemDir :: AChatItem -> MsgDirection +aChatItemDir (AChatItem _ sMsgDir _ _) = toMsgDirection sMsgDir + updateFileStatus :: forall c d. ChatItem c d -> CIFileStatus d -> ChatItem c d updateFileStatus ci@ChatItem {file} status = case file of Just f -> ci {file = Just (f :: CIFile d) {fileStatus = status}} @@ -312,11 +371,15 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta itemTs :: ChatItemTs, itemText :: Text, itemStatus :: CIStatus d, + sentViaProxy :: Maybe Bool, itemSharedMsgId :: Maybe SharedMsgId, + itemForwarded :: Maybe CIForwardedFrom, itemDeleted :: Maybe (CIDeleted c), itemEdited :: Bool, itemTimed :: Maybe CITimed, itemLive :: Maybe Bool, + userMention :: Bool, -- True for messages that mention user or reply to user messages + deletable :: Bool, editable :: Bool, forwardedByMember :: Maybe GroupMemberId, createdAt :: UTCTime, @@ -324,12 +387,42 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta } deriving (Show) -mkCIMeta :: ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe SharedMsgId -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> UTCTime -> CIMeta c d -mkCIMeta itemId itemContent itemText itemStatus itemSharedMsgId itemDeleted itemEdited itemTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt = - let editable = case itemContent of - CISndMsgContent _ -> diffUTCTime currentTs itemTs < nominalDay && isNothing itemDeleted - _ -> False - in CIMeta {itemId, itemTs, itemText, itemStatus, itemSharedMsgId, itemDeleted, itemEdited, itemTimed, itemLive, editable, forwardedByMember, createdAt, updatedAt} +mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe Bool -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> UTCTime -> CIMeta c d +mkCIMeta itemId itemContent itemText itemStatus sentViaProxy itemSharedMsgId itemForwarded itemDeleted itemEdited itemTimed itemLive userMention currentTs itemTs forwardedByMember createdAt updatedAt = + let deletable = deletable' itemContent itemDeleted itemTs nominalDay currentTs + editable = deletable && isNothing itemForwarded + in CIMeta {itemId, itemTs, itemText, itemStatus, sentViaProxy, itemSharedMsgId, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, userMention, deletable, editable, forwardedByMember, createdAt, updatedAt} + +deletable' :: forall c d. ChatTypeI c => CIContent d -> Maybe (CIDeleted c) -> UTCTime -> NominalDiffTime -> UTCTime -> Bool +deletable' itemContent itemDeleted itemTs allowedInterval currentTs = + case itemContent of + CISndMsgContent _ -> + case chatTypeI @c of + SCTLocal -> isNothing itemDeleted + _ -> diffUTCTime currentTs itemTs < allowedInterval && isNothing itemDeleted + _ -> False + +dummyMeta :: ChatItemId -> UTCTime -> Text -> CIMeta c 'MDSnd +dummyMeta itemId ts itemText = + CIMeta + { itemId, + itemTs = ts, + itemText, + itemStatus = CISSndNew, + sentViaProxy = Nothing, + itemSharedMsgId = Nothing, + itemForwarded = Nothing, + itemDeleted = Nothing, + itemEdited = False, + itemTimed = Nothing, + itemLive = Nothing, + userMention = False, + deletable = False, + editable = False, + forwardedByMember = Nothing, + createdAt = ts, + updatedAt = ts + } data CITimed = CITimed { ttl :: Int, -- seconds @@ -345,7 +438,9 @@ contactTimedTTL Contact {mergedPreferences = ContactUserPreferences {timedMessag | forUser enabled && forContact enabled = Just ttl | otherwise = Nothing where - TimedMessagesPreference {ttl} = userPreference.preference + TimedMessagesPreference {ttl} = case userPreference of + CUPContact {preference} -> preference + CUPUser {preference} -> preference groupTimedTTL :: GroupInfo -> Maybe (Maybe Int) groupTimedTTL GroupInfo {fullGroupPreferences = FullGroupPreferences {timedMessages = TimedMessagesGroupPreference {enable, ttl}}} @@ -390,6 +485,18 @@ deriving instance Show ACIReaction data JSONCIReaction c d = JSONCIReaction {chatInfo :: ChatInfo c, chatReaction :: CIReaction c d} +data MemberReaction = MemberReaction + { groupMember :: GroupMember, + reactionTs :: UTCTime + } + deriving (Show) + +type family ChatTypeQuotable (a :: ChatType) :: Constraint where + ChatTypeQuotable 'CTDirect = () + ChatTypeQuotable 'CTGroup = () + ChatTypeQuotable a = + (Int ~ Bool, TypeError ('Type.Text "ChatType " ':<>: 'ShowType a ':<>: 'Type.Text " cannot be quoted")) + data CIQDirection (c :: ChatType) where CIQDirectSnd :: CIQDirection 'CTDirect CIQDirectRcv :: CIQDirection 'CTDirect @@ -398,7 +505,7 @@ data CIQDirection (c :: ChatType) where deriving instance Show (CIQDirection c) -data ACIQDirection = forall c. ChatTypeI c => ACIQDirection (SChatType c) (CIQDirection c) +data ACIQDirection = forall c. (ChatTypeI c, ChatTypeQuotable c) => ACIQDirection (SChatType c) (CIQDirection c) jsonCIQDirection :: CIQDirection c -> Maybe JSONCIDirection jsonCIQDirection = \case @@ -408,13 +515,15 @@ jsonCIQDirection = \case CIQGroupRcv (Just m) -> Just $ JCIGroupRcv m CIQGroupRcv Nothing -> Nothing -jsonACIQDirection :: Maybe JSONCIDirection -> ACIQDirection +jsonACIQDirection :: Maybe JSONCIDirection -> Either String ACIQDirection jsonACIQDirection = \case - Just JCIDirectSnd -> ACIQDirection SCTDirect CIQDirectSnd - Just JCIDirectRcv -> ACIQDirection SCTDirect CIQDirectRcv - Just JCIGroupSnd -> ACIQDirection SCTGroup CIQGroupSnd - Just (JCIGroupRcv m) -> ACIQDirection SCTGroup $ CIQGroupRcv (Just m) - Nothing -> ACIQDirection SCTGroup $ CIQGroupRcv Nothing + Just JCIDirectSnd -> Right $ ACIQDirection SCTDirect CIQDirectSnd + Just JCIDirectRcv -> Right $ ACIQDirection SCTDirect CIQDirectRcv + Just JCIGroupSnd -> Right $ ACIQDirection SCTGroup CIQGroupSnd + Just (JCIGroupRcv m) -> Right $ ACIQDirection SCTGroup $ CIQGroupRcv (Just m) + Nothing -> Right $ ACIQDirection SCTGroup $ CIQGroupRcv Nothing + Just JCILocalSnd -> Left "unquotable" + Just JCILocalRcv -> Left "unquotable" quoteMsgDirection :: CIQDirection c -> MsgDirection quoteMsgDirection = \case @@ -433,7 +542,7 @@ data CIFile (d :: MsgDirection) = CIFile } deriving (Show) -data FileProtocol = FPSMP | FPXFTP +data FileProtocol = FPSMP | FPXFTP | FPLocal deriving (Eq, Show, Ord) instance FromField FileProtocol where fromField = fromTextField_ textDecode @@ -451,23 +560,28 @@ instance TextEncoding FileProtocol where textDecode = \case "smp" -> Just FPSMP "xftp" -> Just FPXFTP + "local" -> Just FPLocal _ -> Nothing textEncode = \case FPSMP -> "smp" FPXFTP -> "xftp" + FPLocal -> "local" data CIFileStatus (d :: MsgDirection) where CIFSSndStored :: CIFileStatus 'MDSnd CIFSSndTransfer :: {sndProgress :: Int64, sndTotal :: Int64} -> CIFileStatus 'MDSnd CIFSSndCancelled :: CIFileStatus 'MDSnd CIFSSndComplete :: CIFileStatus 'MDSnd - CIFSSndError :: CIFileStatus 'MDSnd + CIFSSndError :: {sndFileError :: FileError} -> CIFileStatus 'MDSnd + CIFSSndWarning :: {sndFileError :: FileError} -> CIFileStatus 'MDSnd CIFSRcvInvitation :: CIFileStatus 'MDRcv CIFSRcvAccepted :: CIFileStatus 'MDRcv CIFSRcvTransfer :: {rcvProgress :: Int64, rcvTotal :: Int64} -> CIFileStatus 'MDRcv + CIFSRcvAborted :: CIFileStatus 'MDRcv CIFSRcvComplete :: CIFileStatus 'MDRcv CIFSRcvCancelled :: CIFileStatus 'MDRcv - CIFSRcvError :: CIFileStatus 'MDRcv + CIFSRcvError :: {rcvFileError :: FileError} -> CIFileStatus 'MDRcv + CIFSRcvWarning :: {rcvFileError :: FileError} -> CIFileStatus 'MDRcv CIFSInvalid :: {text :: Text} -> CIFileStatus 'MDSnd deriving instance Eq (CIFileStatus d) @@ -480,15 +594,57 @@ ciFileEnded = \case CIFSSndTransfer {} -> False CIFSSndCancelled -> True CIFSSndComplete -> True - CIFSSndError -> True + CIFSSndError {} -> True + CIFSSndWarning {} -> False CIFSRcvInvitation -> False CIFSRcvAccepted -> False CIFSRcvTransfer {} -> False + CIFSRcvAborted -> True CIFSRcvCancelled -> True CIFSRcvComplete -> True - CIFSRcvError -> True + CIFSRcvError {} -> True + CIFSRcvWarning {} -> False CIFSInvalid {} -> True +ciFileLoaded :: CIFileStatus d -> Bool +ciFileLoaded = \case + CIFSSndStored -> True + CIFSSndTransfer {} -> True + CIFSSndComplete -> True + CIFSSndCancelled -> True + CIFSSndError {} -> True + CIFSSndWarning {} -> True + CIFSRcvInvitation -> False + CIFSRcvAccepted -> False + CIFSRcvTransfer {} -> False + CIFSRcvAborted -> False + CIFSRcvCancelled -> False + CIFSRcvComplete -> True + CIFSRcvError {} -> False + CIFSRcvWarning {} -> False + CIFSInvalid {} -> False + +data ForwardFileError = FFENotAccepted FileTransferId | FFEInProgress | FFEFailed | FFEMissing + deriving (Eq, Ord) + +ciFileForwardError :: FileTransferId -> CIFileStatus d -> Maybe ForwardFileError +ciFileForwardError fId = \case + CIFSSndStored -> Nothing + CIFSSndTransfer {} -> Nothing + CIFSSndComplete -> Nothing + CIFSSndCancelled -> Nothing + CIFSSndError {} -> Nothing + CIFSSndWarning {} -> Nothing + CIFSRcvInvitation -> Just $ FFENotAccepted fId + CIFSRcvAccepted -> Just FFEInProgress + CIFSRcvTransfer {} -> Just FFEInProgress + CIFSRcvAborted -> Just $ FFENotAccepted fId + CIFSRcvCancelled -> Just FFEFailed + CIFSRcvComplete -> Nothing + CIFSRcvError {} -> Just FFEFailed + CIFSRcvWarning {} -> Just FFEFailed + CIFSInvalid {} -> Just FFEFailed + data ACIFileStatus = forall d. MsgDirectionI d => AFS (SMsgDirection d) (CIFileStatus d) deriving instance Show ACIFileStatus @@ -499,13 +655,16 @@ instance MsgDirectionI d => StrEncoding (CIFileStatus d) where CIFSSndTransfer sent total -> strEncode (Str "snd_transfer", sent, total) CIFSSndCancelled -> "snd_cancelled" CIFSSndComplete -> "snd_complete" - CIFSSndError -> "snd_error" + CIFSSndError sndFileErr -> "snd_error " <> strEncode sndFileErr + CIFSSndWarning sndFileErr -> "snd_warning " <> strEncode sndFileErr CIFSRcvInvitation -> "rcv_invitation" CIFSRcvAccepted -> "rcv_accepted" CIFSRcvTransfer rcvd total -> strEncode (Str "rcv_transfer", rcvd, total) + CIFSRcvAborted -> "rcv_aborted" CIFSRcvComplete -> "rcv_complete" CIFSRcvCancelled -> "rcv_cancelled" - CIFSRcvError -> "rcv_error" + CIFSRcvError rcvFileErr -> "rcv_error " <> strEncode rcvFileErr + CIFSRcvWarning rcvFileErr -> "rcv_warning " <> strEncode rcvFileErr CIFSInvalid {} -> "invalid" strP = (\(AFS _ st) -> checkDirection st) <$?> strP @@ -521,13 +680,16 @@ instance StrEncoding ACIFileStatus where "snd_transfer" -> AFS SMDSnd <$> progress CIFSSndTransfer "snd_cancelled" -> pure $ AFS SMDSnd CIFSSndCancelled "snd_complete" -> pure $ AFS SMDSnd CIFSSndComplete - "snd_error" -> pure $ AFS SMDSnd CIFSSndError + "snd_error" -> AFS SMDSnd . CIFSSndError <$> ((A.space *> strP) <|> pure (FileErrOther "")) -- alternative for backwards compatibility + "snd_warning" -> AFS SMDSnd . CIFSSndWarning <$> (A.space *> strP) "rcv_invitation" -> pure $ AFS SMDRcv CIFSRcvInvitation "rcv_accepted" -> pure $ AFS SMDRcv CIFSRcvAccepted "rcv_transfer" -> AFS SMDRcv <$> progress CIFSRcvTransfer + "rcv_aborted" -> pure $ AFS SMDRcv CIFSRcvAborted "rcv_complete" -> pure $ AFS SMDRcv CIFSRcvComplete "rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled - "rcv_error" -> pure $ AFS SMDRcv CIFSRcvError + "rcv_error" -> AFS SMDRcv . CIFSRcvError <$> ((A.space *> strP) <|> pure (FileErrOther "")) -- alternative for backwards compatibility + "rcv_warning" -> AFS SMDRcv . CIFSRcvWarning <$> (A.space *> strP) _ -> fail "bad file status" progress :: (Int64 -> Int64 -> a) -> A.Parser a progress f = f <$> num <*> num <|> pure (f 0 1) @@ -538,13 +700,16 @@ data JSONCIFileStatus | JCIFSSndTransfer {sndProgress :: Int64, sndTotal :: Int64} | JCIFSSndCancelled | JCIFSSndComplete - | JCIFSSndError + | JCIFSSndError {sndFileError :: FileError} + | JCIFSSndWarning {sndFileError :: FileError} | JCIFSRcvInvitation | JCIFSRcvAccepted | JCIFSRcvTransfer {rcvProgress :: Int64, rcvTotal :: Int64} + | JCIFSRcvAborted | JCIFSRcvComplete | JCIFSRcvCancelled - | JCIFSRcvError + | JCIFSRcvError {rcvFileError :: FileError} + | JCIFSRcvWarning {rcvFileError :: FileError} | JCIFSInvalid {text :: Text} jsonCIFileStatus :: CIFileStatus d -> JSONCIFileStatus @@ -553,13 +718,16 @@ jsonCIFileStatus = \case CIFSSndTransfer sent total -> JCIFSSndTransfer sent total CIFSSndCancelled -> JCIFSSndCancelled CIFSSndComplete -> JCIFSSndComplete - CIFSSndError -> JCIFSSndError + CIFSSndError sndFileErr -> JCIFSSndError sndFileErr + CIFSSndWarning sndFileErr -> JCIFSSndWarning sndFileErr CIFSRcvInvitation -> JCIFSRcvInvitation CIFSRcvAccepted -> JCIFSRcvAccepted CIFSRcvTransfer rcvd total -> JCIFSRcvTransfer rcvd total + CIFSRcvAborted -> JCIFSRcvAborted CIFSRcvComplete -> JCIFSRcvComplete CIFSRcvCancelled -> JCIFSRcvCancelled - CIFSRcvError -> JCIFSRcvError + CIFSRcvError rcvFileErr -> JCIFSRcvError rcvFileErr + CIFSRcvWarning rcvFileErr -> JCIFSRcvWarning rcvFileErr CIFSInvalid text -> JCIFSInvalid text aciFileStatusJSON :: JSONCIFileStatus -> ACIFileStatus @@ -568,15 +736,42 @@ aciFileStatusJSON = \case JCIFSSndTransfer sent total -> AFS SMDSnd $ CIFSSndTransfer sent total JCIFSSndCancelled -> AFS SMDSnd CIFSSndCancelled JCIFSSndComplete -> AFS SMDSnd CIFSSndComplete - JCIFSSndError -> AFS SMDSnd CIFSSndError + JCIFSSndError sndFileErr -> AFS SMDSnd (CIFSSndError sndFileErr) + JCIFSSndWarning sndFileErr -> AFS SMDSnd (CIFSSndWarning sndFileErr) JCIFSRcvInvitation -> AFS SMDRcv CIFSRcvInvitation JCIFSRcvAccepted -> AFS SMDRcv CIFSRcvAccepted JCIFSRcvTransfer rcvd total -> AFS SMDRcv $ CIFSRcvTransfer rcvd total + JCIFSRcvAborted -> AFS SMDRcv CIFSRcvAborted JCIFSRcvComplete -> AFS SMDRcv CIFSRcvComplete JCIFSRcvCancelled -> AFS SMDRcv CIFSRcvCancelled - JCIFSRcvError -> AFS SMDRcv CIFSRcvError + JCIFSRcvError rcvFileErr -> AFS SMDRcv (CIFSRcvError rcvFileErr) + JCIFSRcvWarning rcvFileErr -> AFS SMDRcv (CIFSRcvWarning rcvFileErr) JCIFSInvalid text -> AFS SMDSnd $ CIFSInvalid text +data FileError + = FileErrAuth + | FileErrBlocked {server :: String, blockInfo :: BlockingInfo} + | FileErrNoFile + | FileErrRelay {srvError :: SrvError} + | FileErrOther {fileError :: Text} + deriving (Eq, Show) + +instance StrEncoding FileError where + strEncode = \case + FileErrAuth -> "auth" + FileErrBlocked srv info -> "blocked " <> strEncode (srv, info) + FileErrNoFile -> "no_file" + FileErrRelay srvErr -> "relay " <> strEncode srvErr + FileErrOther e -> "other " <> encodeUtf8 e + strP = + A.takeWhile1 (/= ' ') >>= \case + "auth" -> pure FileErrAuth + "blocked" -> FileErrBlocked <$> _strP <*> _strP + "no_file" -> pure FileErrNoFile + "relay" -> FileErrRelay <$> _strP + "other" -> FileErrOther . safeDecodeUtf8 <$> (A.space *> A.takeByteString) + s -> FileErrOther . safeDecodeUtf8 . (s <>) <$> A.takeByteString + -- to conveniently read file data from db data CIFileInfo = CIFileInfo { fileId :: Int64, @@ -597,8 +792,9 @@ data CIStatus (d :: MsgDirection) where CISSndNew :: CIStatus 'MDSnd CISSndSent :: SndCIStatusProgress -> CIStatus 'MDSnd CISSndRcvd :: MsgReceiptStatus -> SndCIStatusProgress -> CIStatus 'MDSnd - CISSndErrorAuth :: CIStatus 'MDSnd - CISSndError :: String -> CIStatus 'MDSnd + CISSndErrorAuth :: CIStatus 'MDSnd -- deprecated + CISSndError :: SndError -> CIStatus 'MDSnd + CISSndWarning :: SndError -> CIStatus 'MDSnd CISRcvNew :: CIStatus 'MDRcv CISRcvRead :: CIStatus 'MDRcv CISInvalid :: Text -> CIStatus 'MDSnd @@ -617,7 +813,8 @@ instance MsgDirectionI d => StrEncoding (CIStatus d) where CISSndSent sndProgress -> "snd_sent " <> strEncode sndProgress CISSndRcvd msgRcptStatus sndProgress -> "snd_rcvd " <> strEncode msgRcptStatus <> " " <> strEncode sndProgress CISSndErrorAuth -> "snd_error_auth" - CISSndError e -> "snd_error " <> encodeUtf8 (T.pack e) + CISSndError sndErr -> "snd_error " <> strEncode sndErr + CISSndWarning sndErr -> "snd_warning " <> strEncode sndErr CISRcvNew -> "rcv_new" CISRcvRead -> "rcv_read" CISInvalid {} -> "invalid" @@ -635,17 +832,68 @@ instance StrEncoding ACIStatus where "snd_sent" -> ACIStatus SMDSnd . CISSndSent <$> ((A.space *> strP) <|> pure SSPComplete) "snd_rcvd" -> ACIStatus SMDSnd <$> (CISSndRcvd <$> (A.space *> strP) <*> ((A.space *> strP) <|> pure SSPComplete)) "snd_error_auth" -> pure $ ACIStatus SMDSnd CISSndErrorAuth - "snd_error" -> ACIStatus SMDSnd . CISSndError . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeByteString) + "snd_error" -> ACIStatus SMDSnd . CISSndError <$> (A.space *> strP) + "snd_warning" -> ACIStatus SMDSnd . CISSndWarning <$> (A.space *> strP) "rcv_new" -> pure $ ACIStatus SMDRcv CISRcvNew "rcv_read" -> pure $ ACIStatus SMDRcv CISRcvRead _ -> fail "bad status" +-- see serverHostError in agent +data SndError + = SndErrAuth + | SndErrQuota + | SndErrExpired -- TIMEOUT/NETWORK errors + | SndErrRelay {srvError :: SrvError} -- BROKER errors (other than TIMEOUT/NETWORK) + | SndErrProxy {proxyServer :: String, srvError :: SrvError} -- SMP PROXY errors + | SndErrProxyRelay {proxyServer :: String, srvError :: SrvError} -- PROXY BROKER errors + | SndErrOther {sndError :: Text} -- other errors + deriving (Eq, Show) + +data SrvError + = SrvErrHost + | SrvErrVersion + | SrvErrOther {srvError :: Text} + deriving (Eq, Show) + +instance StrEncoding SndError where + strEncode = \case + SndErrAuth -> "auth" + SndErrQuota -> "quota" + SndErrExpired -> "expired" + SndErrRelay srvErr -> "relay " <> strEncode srvErr + SndErrProxy proxy srvErr -> "proxy " <> encodeUtf8 (T.pack proxy) <> " " <> strEncode srvErr + SndErrProxyRelay proxy srvErr -> "proxy_relay " <> encodeUtf8 (T.pack proxy) <> " " <> strEncode srvErr + SndErrOther e -> "other " <> encodeUtf8 e + strP = + A.takeWhile1 (/= ' ') >>= \case + "auth" -> pure SndErrAuth + "quota" -> pure SndErrQuota + "expired" -> pure SndErrExpired + "relay" -> SndErrRelay <$> (A.space *> strP) + "proxy" -> SndErrProxy . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeWhile1 (/= ' ') <* A.space) <*> strP + "proxy_relay" -> SndErrProxyRelay . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeWhile1 (/= ' ') <* A.space) <*> strP + "other" -> SndErrOther . safeDecodeUtf8 <$> (A.space *> A.takeByteString) + s -> SndErrOther . safeDecodeUtf8 . (s <>) <$> A.takeByteString -- for backward compatibility with `CISSndError String` + +instance StrEncoding SrvError where + strEncode = \case + SrvErrHost -> "host" + SrvErrVersion -> "version" + SrvErrOther e -> "other " <> encodeUtf8 e + strP = + A.takeWhile1 (/= ' ') >>= \case + "host" -> pure SrvErrHost + "version" -> pure SrvErrVersion + "other" -> SrvErrOther . safeDecodeUtf8 <$> (A.space *> A.takeByteString) + _ -> fail "bad SrvError" + data JSONCIStatus = JCISSndNew | JCISSndSent {sndProgress :: SndCIStatusProgress} | JCISSndRcvd {msgRcptStatus :: MsgReceiptStatus, sndProgress :: SndCIStatusProgress} - | JCISSndErrorAuth - | JCISSndError {agentError :: String} + | JCISSndErrorAuth -- deprecated + | JCISSndError {agentError :: SndError} + | JCISSndWarning {agentError :: SndError} | JCISRcvNew | JCISRcvRead | JCISInvalid {text :: Text} @@ -657,7 +905,8 @@ jsonCIStatus = \case CISSndSent sndProgress -> JCISSndSent sndProgress CISSndRcvd msgRcptStatus sndProgress -> JCISSndRcvd msgRcptStatus sndProgress CISSndErrorAuth -> JCISSndErrorAuth - CISSndError e -> JCISSndError e + CISSndError sndErr -> JCISSndError sndErr + CISSndWarning sndErr -> JCISSndWarning sndErr CISRcvNew -> JCISRcvNew CISRcvRead -> JCISRcvRead CISInvalid text -> JCISInvalid text @@ -668,7 +917,8 @@ jsonACIStatus = \case JCISSndSent sndProgress -> ACIStatus SMDSnd $ CISSndSent sndProgress JCISSndRcvd msgRcptStatus sndProgress -> ACIStatus SMDSnd $ CISSndRcvd msgRcptStatus sndProgress JCISSndErrorAuth -> ACIStatus SMDSnd CISSndErrorAuth - JCISSndError e -> ACIStatus SMDSnd $ CISSndError e + JCISSndError sndErr -> ACIStatus SMDSnd $ CISSndError sndErr + JCISSndWarning sndErr -> ACIStatus SMDSnd $ CISSndWarning sndErr JCISRcvNew -> ACIStatus SMDRcv CISRcvNew JCISRcvRead -> ACIStatus SMDRcv CISRcvRead JCISInvalid text -> ACIStatus SMDSnd $ CISInvalid text @@ -683,7 +933,7 @@ ciCreateStatus content = case msgDirection @d of SMDSnd -> ciStatusNew SMDRcv -> if ciRequiresAttention content then ciStatusNew else CISRcvRead -membersGroupItemStatus :: [(CIStatus 'MDSnd, Int)] -> CIStatus 'MDSnd +membersGroupItemStatus :: [(GroupSndStatus, Int)] -> CIStatus 'MDSnd membersGroupItemStatus memStatusCounts | rcvdOk == total = CISSndRcvd MROk SSPComplete | rcvdOk + rcvdBad == total = CISSndRcvd MRBadMsgHash SSPComplete @@ -694,9 +944,9 @@ membersGroupItemStatus memStatusCounts | otherwise = CISSndNew where total = sum $ map snd memStatusCounts - rcvdOk = fromMaybe 0 $ lookup (CISSndRcvd MROk SSPComplete) memStatusCounts - rcvdBad = fromMaybe 0 $ lookup (CISSndRcvd MRBadMsgHash SSPComplete) memStatusCounts - sent = fromMaybe 0 $ lookup (CISSndSent SSPComplete) memStatusCounts + rcvdOk = fromMaybe 0 $ lookup (GSSRcvd MROk) memStatusCounts + rcvdBad = fromMaybe 0 $ lookup (GSSRcvd MRBadMsgHash) memStatusCounts + sent = fromMaybe 0 $ lookup GSSSent memStatusCounts data SndCIStatusProgress = SSPPartial @@ -713,6 +963,47 @@ instance StrEncoding SndCIStatusProgress where "complete" -> pure SSPComplete _ -> fail "bad SndCIStatusProgress" +data GroupSndStatus + = GSSNew + | GSSForwarded + | GSSInactive + | GSSSent + | GSSRcvd {msgRcptStatus :: MsgReceiptStatus} + | GSSError {agentError :: SndError} + | GSSWarning {agentError :: SndError} + | GSSInvalid {text :: Text} + +deriving instance Eq GroupSndStatus + +deriving instance Show GroupSndStatus + +-- Preserve CIStatus encoding for backwards compatibility +instance StrEncoding GroupSndStatus where + strEncode = \case + GSSNew -> "snd_new" + GSSForwarded -> "snd_forwarded" + GSSInactive -> "snd_inactive" + GSSSent -> "snd_sent complete" + GSSRcvd msgRcptStatus -> "snd_rcvd " <> strEncode msgRcptStatus <> " complete" + GSSError sndErr -> "snd_error " <> strEncode sndErr + GSSWarning sndErr -> "snd_warning " <> strEncode sndErr + GSSInvalid {} -> "invalid" + strP = + (statusP <* A.endOfInput) -- see ACIStatus decoding + <|> (GSSInvalid . safeDecodeUtf8 <$> A.takeByteString) + where + statusP = + A.takeTill (== ' ') >>= \case + "snd_new" -> pure GSSNew + "snd_forwarded" -> pure GSSForwarded + "snd_inactive" -> pure GSSInactive + "snd_sent" -> GSSSent <$ " complete" + "snd_rcvd" -> GSSRcvd <$> (_strP <* " complete") + "snd_error_auth" -> pure $ GSSError SndErrAuth + "snd_error" -> GSSError <$> (A.space *> strP) + "snd_warning" -> GSSWarning <$> (A.space *> strP) + _ -> fail "bad status" + type ChatItemId = Int64 type ChatItemTs = UTCTime @@ -720,6 +1011,7 @@ type ChatItemTs = UTCTime data SChatType (c :: ChatType) where SCTDirect :: SChatType 'CTDirect SCTGroup :: SChatType 'CTGroup + SCTLocal :: SChatType 'CTLocal SCTContactRequest :: SChatType 'CTContactRequest SCTContactConnection :: SChatType 'CTContactConnection @@ -728,6 +1020,7 @@ deriving instance Show (SChatType c) instance TestEquality SChatType where testEquality SCTDirect SCTDirect = Just Refl testEquality SCTGroup SCTGroup = Just Refl + testEquality SCTLocal SCTLocal = Just Refl testEquality SCTContactRequest SCTContactRequest = Just Refl testEquality SCTContactConnection SCTContactConnection = Just Refl testEquality _ _ = Nothing @@ -741,6 +1034,8 @@ instance ChatTypeI 'CTDirect where chatTypeI = SCTDirect instance ChatTypeI 'CTGroup where chatTypeI = SCTGroup +instance ChatTypeI 'CTLocal where chatTypeI = SCTLocal + instance ChatTypeI 'CTContactRequest where chatTypeI = SCTContactRequest instance ChatTypeI 'CTContactConnection where chatTypeI = SCTContactConnection @@ -749,6 +1044,7 @@ toChatType :: SChatType c -> ChatType toChatType = \case SCTDirect -> CTDirect SCTGroup -> CTGroup + SCTLocal -> CTLocal SCTContactRequest -> CTContactRequest SCTContactConnection -> CTContactConnection @@ -756,6 +1052,7 @@ aChatType :: ChatType -> AChatType aChatType = \case CTDirect -> ACT SCTDirect CTGroup -> ACT SCTGroup + CTLocal -> ACT SCTLocal CTContactRequest -> ACT SCTContactRequest CTContactConnection -> ACT SCTContactConnection @@ -764,12 +1061,10 @@ checkChatType x = case testEquality (chatTypeI @c) (chatTypeI @c') of Just Refl -> Right x Nothing -> Left "bad chat type" -type LazyMsgBody = L.ByteString - data SndMessage = SndMessage { msgId :: MessageId, sharedMsgId :: SharedMsgId, - msgBody :: LazyMsgBody + msgBody :: MsgBody } deriving (Show) @@ -788,13 +1083,6 @@ data RcvMessage = RcvMessage forwardedByMember :: Maybe GroupMemberId } -data PendingGroupMessage = PendingGroupMessage - { msgId :: MessageId, - cmEventTag :: ACMEventTag, - msgBody :: LazyMsgBody, - introId_ :: Maybe Int64 - } - type MessageId = Int64 data ConnOrGroupId = ConnectionId Int64 | GroupId Int64 @@ -808,8 +1096,16 @@ data SndMsgDelivery = SndMsgDelivery data RcvMsgDelivery = RcvMsgDelivery { connId :: Int64, agentMsgId :: AgentMsgId, - agentMsgMeta :: MsgMeta, - agentAckCmdId :: CommandId + agentMsgMeta :: MsgMeta + } + deriving (Show) + +data RcvMsgInfo = RcvMsgInfo + { msgId :: Int64, + msgDeliveryId :: Int64, + msgDeliveryStatus :: Text, + agentMsgId :: AgentMsgId, + agentMsgMeta :: Text } deriving (Show) @@ -836,7 +1132,7 @@ msgMetaToJson MsgMeta {integrity, recipient = (rcvId, rcvTs), broker = (serverId data MsgDeliveryStatus (d :: MsgDirection) where MDSRcvAgent :: MsgDeliveryStatus 'MDRcv - MDSRcvAcknowledged :: MsgDeliveryStatus 'MDRcv + MDSRcvAcknowledged :: MsgDeliveryStatus 'MDRcv -- not used MDSSndPending :: MsgDeliveryStatus 'MDSnd MDSSndAgent :: MsgDeliveryStatus 'MDSnd MDSSndSent :: MsgDeliveryStatus 'MDSnd @@ -884,6 +1180,7 @@ msgDeliveryStatusT' s = data CIDeleted (c :: ChatType) where CIDeleted :: Maybe UTCTime -> CIDeleted c CIBlocked :: Maybe UTCTime -> CIDeleted 'CTGroup + CIBlockedByAdmin :: Maybe UTCTime -> CIDeleted 'CTGroup CIModerated :: Maybe UTCTime -> GroupMember -> CIDeleted 'CTGroup deriving instance Show (CIDeleted c) @@ -893,6 +1190,7 @@ data ACIDeleted = forall c. ChatTypeI c => ACIDeleted (SChatType c) (CIDeleted c data JSONCIDeleted = JCIDDeleted {deletedTs :: Maybe UTCTime, chatType :: ChatType} | JCIDBlocked {deletedTs :: Maybe UTCTime} + | JCIDBlockedByAdmin {deletedTs :: Maybe UTCTime} | JCIDModerated {deletedTs :: Maybe UTCTime, byGroupMember :: GroupMember} deriving (Show) @@ -900,25 +1198,60 @@ jsonCIDeleted :: forall d. ChatTypeI d => CIDeleted d -> JSONCIDeleted jsonCIDeleted = \case CIDeleted ts -> JCIDDeleted ts (toChatType $ chatTypeI @d) CIBlocked ts -> JCIDBlocked ts + CIBlockedByAdmin ts -> JCIDBlockedByAdmin ts CIModerated ts m -> JCIDModerated ts m jsonACIDeleted :: JSONCIDeleted -> ACIDeleted jsonACIDeleted = \case JCIDDeleted ts cType -> case aChatType cType of ACT c -> ACIDeleted c $ CIDeleted ts JCIDBlocked ts -> ACIDeleted SCTGroup $ CIBlocked ts + JCIDBlockedByAdmin ts -> ACIDeleted SCTGroup $ CIBlockedByAdmin ts JCIDModerated ts m -> ACIDeleted SCTGroup (CIModerated ts m) itemDeletedTs :: CIDeleted d -> Maybe UTCTime itemDeletedTs = \case CIDeleted ts -> ts CIBlocked ts -> ts + CIBlockedByAdmin ts -> ts CIModerated ts _ -> ts +data CIForwardedFrom + = CIFFUnknown + | CIFFContact {chatName :: Text, msgDir :: MsgDirection, contactId :: Maybe ContactId, chatItemId :: Maybe ChatItemId} + | 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_ + | CIFFGroup_ + +instance FromField CIForwardedFromTag where fromField = fromTextField_ textDecode + +instance ToField CIForwardedFromTag where toField = toField . textEncode + +instance TextEncoding CIForwardedFromTag where + textDecode = \case + "unknown" -> Just CIFFUnknown_ + "contact" -> Just CIFFContact_ + "group" -> Just CIFFGroup_ + _ -> Nothing + textEncode = \case + CIFFUnknown_ -> "unknown" + CIFFContact_ -> "contact" + CIFFGroup_ -> "group" + data ChatItemInfo = ChatItemInfo { itemVersions :: [ChatItemVersion], - memberDeliveryStatuses :: Maybe [MemberDeliveryStatus] + memberDeliveryStatuses :: Maybe (NonEmpty MemberDeliveryStatus), + forwardedFromChatItem :: Maybe AChatItem } - deriving (Eq, Show) + deriving (Show) data ChatItemVersion = ChatItemVersion { chatItemVersionId :: Int64, @@ -930,21 +1263,22 @@ data ChatItemVersion = ChatItemVersion deriving (Eq, Show) mkItemVersion :: ChatItem c d -> Maybe ChatItemVersion -mkItemVersion ChatItem {content, meta} = version <$> ciMsgContent content +mkItemVersion ChatItem {content, formattedText, meta} = version <$> ciMsgContent content where CIMeta {itemId, itemTs, createdAt} = meta version mc = ChatItemVersion { chatItemVersionId = itemId, msgContent = mc, - formattedText = parseMaybeMarkdownList $ msgContentText mc, + formattedText, itemVersionTs = itemTs, createdAt = createdAt } data MemberDeliveryStatus = MemberDeliveryStatus { groupMemberId :: GroupMemberId, - memberDeliveryStatus :: CIStatus 'MDSnd + memberDeliveryStatus :: GroupSndStatus, + sentViaProxy :: Maybe Bool } deriving (Eq, Show) @@ -976,10 +1310,16 @@ instance ChatTypeI c => ToJSON (CIDeleted c) where toJSON = J.toJSON . jsonCIDeleted toEncoding = J.toEncoding . jsonCIDeleted +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CIFF") ''CIForwardedFrom) + $(JQ.deriveJSON defaultJSON ''CITimed) $(JQ.deriveJSON (enumJSON $ dropPrefix "SSP") ''SndCIStatusProgress) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "SrvErr") ''SrvError) + +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "SndErr") ''SndError) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCIS") ''JSONCIStatus) instance MsgDirectionI d => FromJSON (CIStatus d) where @@ -995,12 +1335,16 @@ instance (Typeable d, MsgDirectionI d) => FromField (CIStatus d) where fromField instance FromField ACIStatus where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8 +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GSS") ''GroupSndStatus) + +instance ToField GroupSndStatus where toField = toField . decodeLatin1 . strEncode + +instance FromField GroupSndStatus where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8 + $(JQ.deriveJSON defaultJSON ''MemberDeliveryStatus) $(JQ.deriveJSON defaultJSON ''ChatItemVersion) -$(JQ.deriveJSON defaultJSON ''ChatItemInfo) - instance (ChatTypeI c, MsgDirectionI d) => FromJSON (CIMeta c d) where parseJSON = $(JQ.mkParseJSON defaultJSON ''CIMeta) @@ -1008,6 +1352,8 @@ instance ChatTypeI c => ToJSON (CIMeta c d) where toJSON = $(JQ.mkToJSON defaultJSON ''CIMeta) toEncoding = $(JQ.mkToEncoding defaultJSON ''CIMeta) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FileErr") ''FileError) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCIFS") ''JSONCIFileStatus) instance MsgDirectionI d => FromJSON (CIFileStatus d) where @@ -1044,7 +1390,7 @@ instance FromJSON ACIDirection where parseJSON v = jsonACIDirection <$> J.parseJSON v instance ChatTypeI c => FromJSON (CIQDirection c) where - parseJSON v = (\(ACIQDirection _ x) -> checkChatType x) . jsonACIQDirection <$?> J.parseJSON v + parseJSON v = (jsonACIQDirection >=> \(ACIQDirection _ x) -> checkChatType x) <$?> J.parseJSON v instance ToJSON (CIQDirection c) where toJSON = J.toJSON . jsonCIQDirection @@ -1057,6 +1403,10 @@ $(JQ.deriveToJSON defaultJSON ''CIQuote) $(JQ.deriveJSON defaultJSON ''CIReactionCount) +$(JQ.deriveJSON defaultJSON ''CIMentionMember) + +$(JQ.deriveJSON defaultJSON ''CIMention) + instance (ChatTypeI c, MsgDirectionI d) => FromJSON (ChatItem c d) where parseJSON = $(JQ.mkParseJSON defaultJSON ''ChatItem) @@ -1090,8 +1440,12 @@ instance ChatTypeI c => ToJSON (CChatItem c) where toJSON (CChatItem _ ci) = J.toJSON ci toEncoding (CChatItem _ ci) = J.toEncoding ci +$(JQ.deriveJSON defaultJSON ''ChatItemInfo) + $(JQ.deriveJSON defaultJSON ''ChatStats) +$(JQ.deriveJSON defaultJSON ''NavigationInfo) + instance ChatTypeI c => ToJSON (Chat c) where toJSON = $(JQ.mkToJSON defaultJSON ''Chat) toEncoding = $(JQ.mkToEncoding defaultJSON ''Chat) @@ -1135,7 +1489,11 @@ instance ToJSON ACIReaction where toJSON (ACIReaction _ _ cInfo reaction) = J.toJSON $ JSONCIReaction cInfo reaction toEncoding (ACIReaction _ _ cInfo reaction) = J.toEncoding $ JSONCIReaction cInfo reaction +$(JQ.deriveJSON defaultJSON ''MemberReaction) + $(JQ.deriveJSON defaultJSON ''MsgMetaJSON) msgMetaJson :: MsgMeta -> Text msgMetaJson = decodeLatin1 . LB.toStrict . J.encode . msgMetaToJson + +$(JQ.deriveJSON defaultJSON ''RcvMsgInfo) diff --git a/src/Simplex/Chat/Messages/Batch.hs b/src/Simplex/Chat/Messages/Batch.hs index 8b06873a33..c1c45d7b0a 100644 --- a/src/Simplex/Chat/Messages/Batch.hs +++ b/src/Simplex/Chat/Messages/Batch.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} module Simplex.Chat.Messages.Batch @@ -9,33 +10,30 @@ module Simplex.Chat.Messages.Batch ) where -import Data.ByteString.Builder (Builder, charUtf8, lazyByteString) -import qualified Data.ByteString.Lazy as LB -import Data.Int (Int64) +import Data.ByteString.Char8 (ByteString) +import qualified Data.ByteString.Char8 as B import Simplex.Chat.Controller (ChatError (..), ChatErrorType (..)) import Simplex.Chat.Messages -data MsgBatch = MsgBatch Builder [SndMessage] - deriving (Show) +data MsgBatch = MsgBatch ByteString [SndMessage] --- | Batches [SndMessage] into batches of ByteString builders in form of JSON arrays. +-- | Batches SndMessages in [Either ChatError SndMessage] into batches of ByteStrings in form of JSON arrays. +-- Preserves original errors in the list. -- Does not check if the resulting batch is a valid JSON. -- If a single element is passed, it is returned as is (a JSON string). -- If an element exceeds maxLen, it is returned as ChatError. -batchMessages :: Int64 -> [SndMessage] -> [Either ChatError MsgBatch] -batchMessages maxLen msgs = - let (batches, batch, _, n) = foldr addToBatch ([], [], 0, 0) msgs - in if n == 0 then batches else msgBatch batch : batches +batchMessages :: Int -> [Either ChatError SndMessage] -> [Either ChatError MsgBatch] +batchMessages maxLen = addBatch . foldr addToBatch ([], [], 0, 0) where msgBatch batch = Right (MsgBatch (encodeMessages batch) batch) - addToBatch :: SndMessage -> ([Either ChatError MsgBatch], [SndMessage], Int64, Int) -> ([Either ChatError MsgBatch], [SndMessage], Int64, Int) - addToBatch msg@SndMessage {msgBody} (batches, batch, len, n) + addToBatch :: Either ChatError SndMessage -> ([Either ChatError MsgBatch], [SndMessage], Int, Int) -> ([Either ChatError MsgBatch], [SndMessage], Int, Int) + addToBatch (Left err) acc = (Left err : addBatch acc, [], 0, 0) -- step over original error + addToBatch (Right msg@SndMessage {msgBody}) acc@(batches, batch, len, n) | batchLen <= maxLen = (batches, msg : batch, len', n + 1) - | msgLen <= maxLen = (batches', [msg], msgLen, 1) - | otherwise = (errLarge msg : (if n == 0 then batches else batches'), [], 0, 0) + | msgLen <= maxLen = (addBatch acc, [msg], msgLen, 1) + | otherwise = (errLarge msg : addBatch acc, [], 0, 0) where - msgLen = LB.length msgBody - batches' = msgBatch batch : batches + msgLen = B.length msgBody len' | n == 0 = msgLen | otherwise = msgLen + len + 1 -- 1 accounts for comma @@ -43,11 +41,11 @@ batchMessages maxLen msgs = | n == 0 = len' | otherwise = len' + 2 -- 2 accounts for opening and closing brackets errLarge SndMessage {msgId} = Left $ ChatError $ CEInternalError ("large message " <> show msgId) - -encodeMessages :: [SndMessage] -> Builder -encodeMessages = \case - [] -> mempty - [msg] -> encodeMsg msg - (msg : msgs) -> charUtf8 '[' <> encodeMsg msg <> mconcat [charUtf8 ',' <> encodeMsg msg' | msg' <- msgs] <> charUtf8 ']' - where - encodeMsg SndMessage {msgBody} = lazyByteString msgBody + addBatch :: ([Either ChatError MsgBatch], [SndMessage], Int, Int) -> [Either ChatError MsgBatch] + addBatch (batches, batch, _, n) = if n == 0 then batches else msgBatch batch : batches + encodeMessages :: [SndMessage] -> ByteString + encodeMessages = \case + [] -> mempty + [msg] -> body msg + msgs -> B.concat ["[", B.intercalate "," (map body msgs), "]"] + body SndMessage {msgBody} = msgBody diff --git a/src/Simplex/Chat/Messages/CIContent.hs b/src/Simplex/Chat/Messages/CIContent.hs index b878949cab..60d5464b79 100644 --- a/src/Simplex/Chat/Messages/CIContent.hs +++ b/src/Simplex/Chat/Messages/CIContent.hs @@ -6,6 +6,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} @@ -16,22 +17,23 @@ module Simplex.Chat.Messages.CIContent where import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as J import qualified Data.Aeson.TH as JQ +import qualified Data.Attoparsec.ByteString.Char8 as A import Data.Int (Int64) import Data.Text (Text) import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Type.Equality import Data.Word (Word32) -import Database.SQLite.Simple.FromField (FromField (..)) -import Database.SQLite.Simple.ToField (ToField (..)) import Simplex.Chat.Messages.CIContent.Events +import Simplex.Chat.Options.DB (FromField (..), ToField (..)) import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Types.Preferences -import Simplex.Chat.Types.Util +import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol (MsgErrorType (..), RatchetSyncState (..), SwitchPhase (..)) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption, pattern PQEncOff, pattern PQEncOn) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fstToLower, singleFieldJSON, sumTypeJSON) -import Simplex.Messaging.Util (safeDecodeUtf8, tshow, (<$?>)) +import Simplex.Messaging.Util (encodeJSON, safeDecodeUtf8, tshow, (<$?>)) data MsgDirection = MDRcv | MDSnd deriving (Eq, Show) @@ -40,6 +42,8 @@ $(JQ.deriveJSON (enumJSON $ dropPrefix "MD") ''MsgDirection) instance FromField AMsgDirection where fromField = fromIntField_ $ fmap fromMsgDirection . msgDirectionIntP +instance FromField MsgDirection where fromField = fromIntField_ msgDirectionIntP + instance ToField MsgDirection where toField = toField . msgDirectionInt data SMsgDirection (d :: MsgDirection) where @@ -99,15 +103,33 @@ msgDirectionIntP = \case 1 -> Just MDSnd _ -> Nothing -data CIDeleteMode = CIDMBroadcast | CIDMInternal +data CIDeleteMode = CIDMBroadcast | CIDMInternal | CIDMInternalMark deriving (Show) -$(JQ.deriveJSON (enumJSON $ dropPrefix "CIDM") ''CIDeleteMode) +instance StrEncoding CIDeleteMode where + strEncode = \case + CIDMBroadcast -> "broadcast" + CIDMInternal -> "internal" + CIDMInternalMark -> "internalMark" + strP = + A.takeTill (== ' ') >>= \case + "broadcast" -> pure CIDMBroadcast + "internal" -> pure CIDMInternal + "internalMark" -> pure CIDMInternalMark + _ -> fail "bad CIDeleteMode" + +instance ToJSON CIDeleteMode where + toJSON = strToJSON + toEncoding = strToJEncoding + +instance FromJSON CIDeleteMode where + parseJSON = strParseJSON "CIDeleteMode" ciDeleteModeToText :: CIDeleteMode -> Text ciDeleteModeToText = \case CIDMBroadcast -> "this item is deleted (broadcast)" - CIDMInternal -> "this item is deleted (internal)" + CIDMInternal -> "this item is deleted (locally)" + CIDMInternalMark -> "this item is deleted (locally)" -- This type is used both in API and in DB, so we use different JSON encodings for the database and for the API -- ! Nested sum types also have to use different encodings for database and API @@ -132,19 +154,28 @@ data CIContent (d :: MsgDirection) where CISndChatFeature :: ChatFeature -> PrefEnabled -> Maybe Int -> CIContent 'MDSnd CIRcvChatPreference :: ChatFeature -> FeatureAllowed -> Maybe Int -> CIContent 'MDRcv CISndChatPreference :: ChatFeature -> FeatureAllowed -> Maybe Int -> CIContent 'MDSnd - CIRcvGroupFeature :: GroupFeature -> GroupPreference -> Maybe Int -> CIContent 'MDRcv - CISndGroupFeature :: GroupFeature -> GroupPreference -> Maybe Int -> CIContent 'MDSnd + CIRcvGroupFeature :: GroupFeature -> GroupPreference -> Maybe Int -> Maybe GroupMemberRole -> CIContent 'MDRcv + CISndGroupFeature :: GroupFeature -> GroupPreference -> Maybe Int -> Maybe GroupMemberRole -> CIContent 'MDSnd CIRcvChatFeatureRejected :: ChatFeature -> CIContent 'MDRcv CIRcvGroupFeatureRejected :: GroupFeature -> CIContent 'MDRcv CISndModerated :: CIContent 'MDSnd CIRcvModerated :: CIContent 'MDRcv - CIInvalidJSON :: Text -> CIContent d + CIRcvBlocked :: CIContent 'MDRcv + CISndDirectE2EEInfo :: E2EInfo -> CIContent 'MDSnd + CIRcvDirectE2EEInfo :: E2EInfo -> CIContent 'MDRcv + CISndGroupE2EEInfo :: E2EInfo -> CIContent 'MDSnd -- when new group is created + CIRcvGroupE2EEInfo :: E2EInfo -> CIContent 'MDRcv -- when enabled with some member + CIInvalidJSON :: Text -> CIContent d -- this is also used for logical database errors, e.g. SEBadChatItem + -- ^ This type is used both in API and in DB, so we use different JSON encodings for the database and for the API -- ! ^ Nested sum types also have to use different encodings for database and API -- ! ^ to avoid breaking cross-platform compatibility, see RcvGroupEvent and SndGroupEvent deriving instance Show (CIContent d) +data E2EInfo = E2EInfo {pqEnabled :: PQEncryption} + deriving (Eq, Show) + ciMsgContent :: CIContent d -> Maybe MsgContent ciMsgContent = \case CISndMsgContent mc -> Just mc @@ -169,12 +200,15 @@ ciRequiresAttention content = case msgDirection @d of CIRcvIntegrityError _ -> True CIRcvDecryptionError {} -> True CIRcvGroupInvitation {} -> True - CIRcvDirectEvent _ -> False + CIRcvDirectEvent rde -> case rde of + RDEContactDeleted -> False + RDEProfileUpdated {} -> False CIRcvGroupEvent rge -> case rge of RGEMemberAdded {} -> False RGEMemberConnected -> False RGEMemberLeft -> False RGEMemberRole {} -> False + RGEMemberBlocked {} -> False RGEUserRole _ -> True RGEMemberDeleted {} -> False RGEUserDeleted -> True @@ -182,6 +216,7 @@ ciRequiresAttention content = case msgDirection @d of RGEGroupUpdated _ -> False RGEInvitedViaGroupLink -> False RGEMemberCreatedContact -> False + RGEMemberProfileUpdated {} -> False CIRcvConnEvent _ -> True CIRcvChatFeature {} -> False CIRcvChatPreference {} -> False @@ -189,6 +224,9 @@ ciRequiresAttention content = case msgDirection @d of CIRcvChatFeatureRejected _ -> True CIRcvGroupFeatureRejected _ -> True CIRcvModerated -> True + CIRcvBlocked -> False + CIRcvDirectE2EEInfo _ -> False + CIRcvGroupE2EEInfo _ -> False CIInvalidJSON _ -> False newtype DBMsgErrorType = DBME MsgErrorType @@ -237,14 +275,35 @@ ciContentToText = \case CISndChatFeature feature enabled param -> featureStateText feature enabled param CIRcvChatPreference feature allowed param -> prefStateText feature allowed param CISndChatPreference feature allowed param -> "you " <> prefStateText feature allowed param - CIRcvGroupFeature feature pref param -> groupPrefStateText feature pref param - CISndGroupFeature feature pref param -> groupPrefStateText feature pref param + CIRcvGroupFeature feature pref param role -> groupPrefStateText feature pref param role + CISndGroupFeature feature pref param role -> groupPrefStateText feature pref param role CIRcvChatFeatureRejected feature -> chatFeatureNameText feature <> ": received, prohibited" CIRcvGroupFeatureRejected feature -> groupFeatureNameText feature <> ": received, prohibited" CISndModerated -> ciModeratedText CIRcvModerated -> ciModeratedText + CIRcvBlocked -> "blocked" + CISndDirectE2EEInfo e2eeInfo -> directE2EInfoToText e2eeInfo + CIRcvDirectE2EEInfo e2eeInfo -> directE2EInfoToText e2eeInfo + CISndGroupE2EEInfo e2eeInfo -> groupE2EInfoToText e2eeInfo + CIRcvGroupE2EEInfo e2eeInfo -> groupE2EInfoToText e2eeInfo CIInvalidJSON _ -> "invalid content JSON" +directE2EInfoToText :: E2EInfo -> Text +directE2EInfoToText E2EInfo {pqEnabled} = case pqEnabled of + PQEncOn -> e2eInfoPQText + PQEncOff -> e2eInfoNoPQText + +groupE2EInfoToText :: E2EInfo -> Text +groupE2EInfoToText _e2eeInfo = e2eInfoNoPQText + +e2eInfoNoPQText :: Text +e2eInfoNoPQText = + "This conversation is protected by end-to-end encryption with perfect forward secrecy, repudiation and break-in recovery." + +e2eInfoPQText :: Text +e2eInfoPQText = + "This conversation is protected by quantum resistant end-to-end encryption. It has perfect forward secrecy, repudiation and quantum resistant break-in recovery." + ciGroupInvitationToText :: CIGroupInvitation -> GroupMemberRole -> Text ciGroupInvitationToText CIGroupInvitation {groupProfile = GroupProfile {displayName, fullName}} role = "invitation to join group " <> displayName <> optionalFullName displayName fullName <> " as " <> (decodeLatin1 . strEncode $ role) @@ -252,6 +311,7 @@ ciGroupInvitationToText CIGroupInvitation {groupProfile = GroupProfile {displayN rcvDirectEventToText :: RcvDirectEvent -> Text rcvDirectEventToText = \case RDEContactDeleted -> "contact deleted" + RDEProfileUpdated {} -> "updated profile" rcvGroupEventToText :: RcvGroupEvent -> Text rcvGroupEventToText = \case @@ -259,6 +319,7 @@ rcvGroupEventToText = \case RGEMemberConnected -> "connected" RGEMemberLeft -> "left" RGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> safeDecodeUtf8 (strEncode r) + RGEMemberBlocked _ p blocked -> (if blocked then "blocked" else "unblocked") <> " " <> profileToText p RGEUserRole r -> "changed your role to " <> safeDecodeUtf8 (strEncode r) RGEMemberDeleted _ p -> "removed " <> profileToText p RGEUserDeleted -> "removed you" @@ -266,10 +327,12 @@ rcvGroupEventToText = \case RGEGroupUpdated _ -> "group profile updated" RGEInvitedViaGroupLink -> "invited via your group link" RGEMemberCreatedContact -> "started direct connection with you" + RGEMemberProfileUpdated {} -> "updated profile" sndGroupEventToText :: SndGroupEvent -> Text sndGroupEventToText = \case SGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> safeDecodeUtf8 (strEncode r) + SGEMemberBlocked _ p blocked -> (if blocked then "blocked" else "unblocked") <> " " <> profileToText p SGEUserRole r -> "changed role for yourself to " <> safeDecodeUtf8 (strEncode r) SGEMemberDeleted _ p -> "removed " <> profileToText p SGEUserLeft -> "left" @@ -284,6 +347,9 @@ rcvConnEventToText = \case SPCompleted -> "changed address for you" RCERatchetSync syncStatus -> ratchetSyncStatusToText syncStatus RCEVerificationCodeReset -> "security code changed" + RCEPqEnabled pqEnc -> case pqEnc of + PQEncOn -> "quantum resistant e2e encryption" + PQEncOff -> "standard end-to-end encryption" ratchetSyncStatusToText :: RatchetSyncState -> Text ratchetSyncStatusToText = \case @@ -301,6 +367,9 @@ sndConnEventToText = \case SPSecured -> "secured new address" <> forMember m <> "..." SPCompleted -> "you changed address" <> forMember m SCERatchetSync syncStatus m -> ratchetSyncStatusToText syncStatus <> forMember m + SCEPqEnabled pqEnc -> case pqEnc of + PQEncOn -> "quantum resistant e2e encryption" + PQEncOff -> "standard end-to-end encryption" where forMember member_ = maybe "" (\GroupMemberRef {profile = Profile {displayName}} -> " for " <> displayName) member_ @@ -364,12 +433,17 @@ data JSONCIContent | JCISndChatFeature {feature :: ChatFeature, enabled :: PrefEnabled, param :: Maybe Int} | JCIRcvChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} | JCISndChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} - | JCIRcvGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} - | JCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} + | JCIRcvGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int, memberRole_ :: Maybe GroupMemberRole} + | JCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int, memberRole_ :: Maybe GroupMemberRole} | JCIRcvChatFeatureRejected {feature :: ChatFeature} | JCIRcvGroupFeatureRejected {groupFeature :: GroupFeature} | JCISndModerated | JCIRcvModerated + | JCIRcvBlocked + | JCISndDirectE2EEInfo {e2eeInfo :: E2EInfo} + | JCIRcvDirectE2EEInfo {e2eeInfo :: E2EInfo} + | JCISndGroupE2EEInfo {e2eeInfo :: E2EInfo} + | JCIRcvGroupE2EEInfo {e2eeInfo :: E2EInfo} | JCIInvalidJSON {direction :: MsgDirection, json :: Text} jsonCIContent :: forall d. MsgDirectionI d => CIContent d -> JSONCIContent @@ -393,12 +467,17 @@ jsonCIContent = \case CISndChatFeature feature enabled param -> JCISndChatFeature {feature, enabled, param} CIRcvChatPreference feature allowed param -> JCIRcvChatPreference {feature, allowed, param} CISndChatPreference feature allowed param -> JCISndChatPreference {feature, allowed, param} - CIRcvGroupFeature groupFeature preference param -> JCIRcvGroupFeature {groupFeature, preference, param} - CISndGroupFeature groupFeature preference param -> JCISndGroupFeature {groupFeature, preference, param} + CIRcvGroupFeature groupFeature preference param memberRole_ -> JCIRcvGroupFeature {groupFeature, preference, param, memberRole_} + CISndGroupFeature groupFeature preference param memberRole_ -> JCISndGroupFeature {groupFeature, preference, param, memberRole_} CIRcvChatFeatureRejected feature -> JCIRcvChatFeatureRejected {feature} CIRcvGroupFeatureRejected groupFeature -> JCIRcvGroupFeatureRejected {groupFeature} CISndModerated -> JCISndModerated CIRcvModerated -> JCIRcvModerated + CIRcvBlocked -> JCIRcvBlocked + CISndDirectE2EEInfo e2eeInfo -> JCISndDirectE2EEInfo e2eeInfo + CIRcvDirectE2EEInfo e2eeInfo -> JCIRcvDirectE2EEInfo e2eeInfo + CISndGroupE2EEInfo e2eeInfo -> JCISndGroupE2EEInfo e2eeInfo + CIRcvGroupE2EEInfo e2eeInfo -> JCIRcvGroupE2EEInfo e2eeInfo CIInvalidJSON json -> JCIInvalidJSON (toMsgDirection $ msgDirection @d) json aciContentJSON :: JSONCIContent -> ACIContent @@ -422,12 +501,17 @@ aciContentJSON = \case JCISndChatFeature {feature, enabled, param} -> ACIContent SMDSnd $ CISndChatFeature feature enabled param JCIRcvChatPreference {feature, allowed, param} -> ACIContent SMDRcv $ CIRcvChatPreference feature allowed param JCISndChatPreference {feature, allowed, param} -> ACIContent SMDSnd $ CISndChatPreference feature allowed param - JCIRcvGroupFeature {groupFeature, preference, param} -> ACIContent SMDRcv $ CIRcvGroupFeature groupFeature preference param - JCISndGroupFeature {groupFeature, preference, param} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference param + JCIRcvGroupFeature {groupFeature, preference, param, memberRole_} -> ACIContent SMDRcv $ CIRcvGroupFeature groupFeature preference param memberRole_ + JCISndGroupFeature {groupFeature, preference, param, memberRole_} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference param memberRole_ JCIRcvChatFeatureRejected {feature} -> ACIContent SMDRcv $ CIRcvChatFeatureRejected feature JCIRcvGroupFeatureRejected {groupFeature} -> ACIContent SMDRcv $ CIRcvGroupFeatureRejected groupFeature JCISndModerated -> ACIContent SMDSnd CISndModerated JCIRcvModerated -> ACIContent SMDRcv CIRcvModerated + JCIRcvBlocked -> ACIContent SMDRcv CIRcvBlocked + JCISndDirectE2EEInfo {e2eeInfo} -> ACIContent SMDSnd $ CISndDirectE2EEInfo e2eeInfo + JCIRcvDirectE2EEInfo {e2eeInfo} -> ACIContent SMDRcv $ CIRcvDirectE2EEInfo e2eeInfo + JCISndGroupE2EEInfo {e2eeInfo} -> ACIContent SMDSnd $ CISndGroupE2EEInfo e2eeInfo + JCIRcvGroupE2EEInfo {e2eeInfo} -> ACIContent SMDRcv $ CIRcvGroupE2EEInfo e2eeInfo JCIInvalidJSON dir json -> case fromMsgDirection dir of AMsgDirection d -> ACIContent d $ CIInvalidJSON json @@ -452,12 +536,17 @@ data DBJSONCIContent | DBJCISndChatFeature {feature :: ChatFeature, enabled :: PrefEnabled, param :: Maybe Int} | DBJCIRcvChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} | DBJCISndChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} - | DBJCIRcvGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} - | DBJCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} + | DBJCIRcvGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int, memberRole_ :: Maybe GroupMemberRole} + | DBJCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int, memberRole_ :: Maybe GroupMemberRole} | DBJCIRcvChatFeatureRejected {feature :: ChatFeature} | DBJCIRcvGroupFeatureRejected {groupFeature :: GroupFeature} | DBJCISndModerated | DBJCIRcvModerated + | DBJCIRcvBlocked + | DBJCISndDirectE2EEInfo {e2eeInfo :: E2EInfo} + | DBJCIRcvDirectE2EEInfo {e2eeInfo :: E2EInfo} + | DBJCISndGroupE2EEInfo {e2eeInfo :: E2EInfo} + | DBJCIRcvGroupE2EEInfo {e2eeInfo :: E2EInfo} | DBJCIInvalidJSON {direction :: MsgDirection, json :: Text} dbJsonCIContent :: forall d. MsgDirectionI d => CIContent d -> DBJSONCIContent @@ -481,12 +570,17 @@ dbJsonCIContent = \case CISndChatFeature feature enabled param -> DBJCISndChatFeature {feature, enabled, param} CIRcvChatPreference feature allowed param -> DBJCIRcvChatPreference {feature, allowed, param} CISndChatPreference feature allowed param -> DBJCISndChatPreference {feature, allowed, param} - CIRcvGroupFeature groupFeature preference param -> DBJCIRcvGroupFeature {groupFeature, preference, param} - CISndGroupFeature groupFeature preference param -> DBJCISndGroupFeature {groupFeature, preference, param} + CIRcvGroupFeature groupFeature preference param memberRole_ -> DBJCIRcvGroupFeature {groupFeature, preference, param, memberRole_} + CISndGroupFeature groupFeature preference param memberRole_ -> DBJCISndGroupFeature {groupFeature, preference, param, memberRole_} CIRcvChatFeatureRejected feature -> DBJCIRcvChatFeatureRejected {feature} CIRcvGroupFeatureRejected groupFeature -> DBJCIRcvGroupFeatureRejected {groupFeature} CISndModerated -> DBJCISndModerated CIRcvModerated -> DBJCIRcvModerated + CIRcvBlocked -> DBJCIRcvBlocked + CISndDirectE2EEInfo e2eeInfo -> DBJCISndDirectE2EEInfo e2eeInfo + CIRcvDirectE2EEInfo e2eeInfo -> DBJCIRcvDirectE2EEInfo e2eeInfo + CISndGroupE2EEInfo e2eeInfo -> DBJCISndGroupE2EEInfo e2eeInfo + CIRcvGroupE2EEInfo e2eeInfo -> DBJCIRcvGroupE2EEInfo e2eeInfo CIInvalidJSON json -> DBJCIInvalidJSON (toMsgDirection $ msgDirection @d) json aciContentDBJSON :: DBJSONCIContent -> ACIContent @@ -510,12 +604,17 @@ aciContentDBJSON = \case DBJCISndChatFeature {feature, enabled, param} -> ACIContent SMDSnd $ CISndChatFeature feature enabled param DBJCIRcvChatPreference {feature, allowed, param} -> ACIContent SMDRcv $ CIRcvChatPreference feature allowed param DBJCISndChatPreference {feature, allowed, param} -> ACIContent SMDSnd $ CISndChatPreference feature allowed param - DBJCIRcvGroupFeature {groupFeature, preference, param} -> ACIContent SMDRcv $ CIRcvGroupFeature groupFeature preference param - DBJCISndGroupFeature {groupFeature, preference, param} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference param + DBJCIRcvGroupFeature {groupFeature, preference, param, memberRole_} -> ACIContent SMDRcv $ CIRcvGroupFeature groupFeature preference param memberRole_ + DBJCISndGroupFeature {groupFeature, preference, param, memberRole_} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference param memberRole_ DBJCIRcvChatFeatureRejected {feature} -> ACIContent SMDRcv $ CIRcvChatFeatureRejected feature DBJCIRcvGroupFeatureRejected {groupFeature} -> ACIContent SMDRcv $ CIRcvGroupFeatureRejected groupFeature DBJCISndModerated -> ACIContent SMDSnd CISndModerated DBJCIRcvModerated -> ACIContent SMDRcv CIRcvModerated + DBJCIRcvBlocked -> ACIContent SMDRcv CIRcvBlocked + DBJCISndDirectE2EEInfo e2eeInfo -> ACIContent SMDSnd $ CISndDirectE2EEInfo e2eeInfo + DBJCIRcvDirectE2EEInfo e2eeInfo -> ACIContent SMDRcv $ CIRcvDirectE2EEInfo e2eeInfo + DBJCISndGroupE2EEInfo e2eeInfo -> ACIContent SMDSnd $ CISndGroupE2EEInfo e2eeInfo + DBJCIRcvGroupE2EEInfo e2eeInfo -> ACIContent SMDRcv $ CIRcvGroupE2EEInfo e2eeInfo DBJCIInvalidJSON dir json -> case fromMsgDirection dir of AMsgDirection d -> ACIContent d $ CIInvalidJSON json @@ -541,6 +640,16 @@ ciCallInfoText status duration = case status of CISCallEnded -> "ended " <> durationText duration CISCallError -> "error" +callComplete :: CICallStatus -> Bool +callComplete = \case + CISCallMissed -> True + CISCallRejected -> True + CISCallEnded -> True + CISCallError -> True + _ -> False + +$(JQ.deriveJSON defaultJSON ''E2EInfo) + $(JQ.deriveJSON (enumJSON $ dropPrefix "MDE") ''MsgDecryptError) $(JQ.deriveJSON (enumJSON $ dropPrefix "CIGIS") ''CIGroupInvitationStatus) @@ -608,4 +717,9 @@ toCIContentTag ciContent = case ciContent of CIRcvGroupFeatureRejected _ -> "rcvGroupFeatureRejected" CISndModerated -> "sndModerated" CIRcvModerated -> "rcvModerated" + CIRcvBlocked -> "rcvBlocked" + CISndDirectE2EEInfo _ -> "sndDirectE2EEInfo" + CIRcvDirectE2EEInfo _ -> "rcvDirectE2EEInfo" + CISndGroupE2EEInfo _ -> "sndGroupE2EEInfo" + CIRcvGroupE2EEInfo _ -> "rcvGroupE2EEInfo" CIInvalidJSON _ -> "invalidJSON" diff --git a/src/Simplex/Chat/Messages/CIContent/Events.hs b/src/Simplex/Chat/Messages/CIContent/Events.hs index 16851859e3..054530e06f 100644 --- a/src/Simplex/Chat/Messages/CIContent/Events.hs +++ b/src/Simplex/Chat/Messages/CIContent/Events.hs @@ -7,14 +7,17 @@ module Simplex.Chat.Messages.CIContent.Events where import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson.TH as J import Simplex.Chat.Types +import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol (RatchetSyncState (..), SwitchPhase (..)) import Simplex.Messaging.Parsers (dropPrefix, singleFieldJSON, sumTypeJSON) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption) data RcvGroupEvent = RGEMemberAdded {groupMemberId :: GroupMemberId, profile :: Profile} -- CRJoinedGroupMemberConnecting | RGEMemberConnected -- CRUserJoinedGroup, CRJoinedGroupMember, CRConnectedToGroupMember | RGEMemberLeft -- CRLeftMember | RGEMemberRole {groupMemberId :: GroupMemberId, profile :: Profile, role :: GroupMemberRole} + | RGEMemberBlocked {groupMemberId :: GroupMemberId, profile :: Profile, blocked :: Bool} -- CRMemberBlockedForAll | RGEUserRole {role :: GroupMemberRole} | RGEMemberDeleted {groupMemberId :: GroupMemberId, profile :: Profile} -- CRDeletedMember | RGEUserDeleted -- CRDeletedMemberUser @@ -25,12 +28,14 @@ data RcvGroupEvent -- and be created as unread without adding / working around new status for sent items | RGEInvitedViaGroupLink -- CRSentGroupInvitationViaLink | RGEMemberCreatedContact -- CRNewMemberContactReceivedInv + | RGEMemberProfileUpdated {fromProfile :: Profile, toProfile :: Profile} -- CRGroupMemberUpdated deriving (Show) data SndGroupEvent = SGEMemberRole {groupMemberId :: GroupMemberId, profile :: Profile, role :: GroupMemberRole} + | SGEMemberBlocked {groupMemberId :: GroupMemberId, profile :: Profile, blocked :: Bool} -- CRMembersBlockedForAllUser | SGEUserRole {role :: GroupMemberRole} - | SGEMemberDeleted {groupMemberId :: GroupMemberId, profile :: Profile} -- CRUserDeletedMember + | SGEMemberDeleted {groupMemberId :: GroupMemberId, profile :: Profile} -- CRUserDeletedMembers | SGEUserLeft -- CRLeftMemberUser | SGEGroupUpdated {groupProfile :: GroupProfile} -- CRGroupUpdated deriving (Show) @@ -39,16 +44,18 @@ data RcvConnEvent = RCESwitchQueue {phase :: SwitchPhase} | RCERatchetSync {syncStatus :: RatchetSyncState} | RCEVerificationCodeReset + | RCEPqEnabled {enabled :: PQEncryption} deriving (Show) data SndConnEvent = SCESwitchQueue {phase :: SwitchPhase, member :: Maybe GroupMemberRef} | SCERatchetSync {syncStatus :: RatchetSyncState, member :: Maybe GroupMemberRef} + | SCEPqEnabled {enabled :: PQEncryption} deriving (Show) data RcvDirectEvent - = -- RDEProfileChanged {...} - RDEContactDeleted + = RDEContactDeleted + | RDEProfileUpdated {fromProfile :: Profile, toProfile :: Profile} -- CRContactUpdated deriving (Show) -- platform-specific JSON encoding (used in API) diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 3671844d7f..194fc1bb06 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} @@ -12,21 +13,19 @@ import Control.Concurrent.STM import Control.Exception (SomeException, catch) import Control.Monad.Except import Control.Monad.Reader +import Data.Aeson (ToJSON (..)) import qualified Data.Aeson as J import qualified Data.Aeson.TH as JQ import Data.Bifunctor (first) -import Data.ByteArray (ScrubbedBytes) -import qualified Data.ByteArray as BA import qualified Data.ByteString.Base64.URL as U import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Lazy.Char8 as LB import Data.Functor (($>)) import Data.List (find) import qualified Data.List.NonEmpty as L import Data.Maybe (fromMaybe) import Data.Word (Word8) -import Database.SQLite.Simple (SQLError (..)) -import qualified Database.SQLite.Simple as DB import Foreign.C.String import Foreign.C.Types (CInt (..)) import Foreign.Ptr @@ -35,26 +34,34 @@ import Foreign.Storable (poke) import GHC.IO.Encoding (setFileSystemEncoding, setForeignEncoding, setLocaleEncoding) import Simplex.Chat import Simplex.Chat.Controller +import Simplex.Chat.Library.Commands import Simplex.Chat.Markdown (ParsedMarkdown (..), parseMaybeMarkdownList) import Simplex.Chat.Mobile.File import Simplex.Chat.Mobile.Shared import Simplex.Chat.Mobile.WebRTC import Simplex.Chat.Options +import Simplex.Chat.Options.DB import Simplex.Chat.Remote.Types import Simplex.Chat.Store import Simplex.Chat.Store.Profiles import Simplex.Chat.Types import Simplex.Messaging.Agent.Client (agentClientStore) import Simplex.Messaging.Agent.Env.SQLite (createAgentStore) -import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, closeSQLiteStore, reopenSQLiteStore) -import Simplex.Messaging.Client (defaultNetworkConfig) +import Simplex.Messaging.Agent.Store.Interface (closeDBStore, reopenDBStore) +import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..), MigrationError) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), BasicAuth (..), CorrId (..), ProtoServerWithAuth (..), ProtocolServer (..)) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..)) import Simplex.Messaging.Util (catchAll, liftEitherWith, safeDecodeUtf8) import System.IO (utf8) import System.Timeout (timeout) +#if !defined(dbPostgres) +import Data.ByteArray (ScrubbedBytes) +import Database.SQLite.Simple (SQLError (..)) +import qualified Database.SQLite.Simple as DB +import qualified Simplex.Messaging.Agent.Store.DB as DB +#endif data DBMigrationResult = DBMOk @@ -66,9 +73,19 @@ data DBMigrationResult $(JQ.deriveToJSON (sumTypeJSON $ dropPrefix "DBM") ''DBMigrationResult) -data APIResponse = APIResponse {corr :: Maybe CorrId, remoteHostId :: Maybe RemoteHostId, resp :: ChatResponse} +data APIResult r + = APIResult {remoteHostId :: Maybe RemoteHostId, result :: r} + | APIError {remoteHostId :: Maybe RemoteHostId, error :: ChatError} -$(JQ.deriveToJSON defaultJSON ''APIResponse) +eitherToResult :: Maybe RemoteHostId -> Either ChatError r -> APIResult r +eitherToResult rhId = either (APIError rhId) (APIResult rhId) +{-# INLINE eitherToResult #-} + +$(pure []) + +instance ToJSON r => ToJSON (APIResult r) where + toEncoding = $(JQ.mkToEncoding (defaultJSON {J.sumEncoding = J.UntaggedValue}) ''APIResult) + toJSON = $(JQ.mkToJSON (defaultJSON {J.sumEncoding = J.UntaggedValue}) ''APIResult) foreign export ccall "chat_migrate_init" cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString @@ -94,6 +111,8 @@ foreign export ccall "chat_password_hash" cChatPasswordHash :: CString -> CStrin foreign export ccall "chat_valid_name" cChatValidName :: CString -> IO CString +foreign export ccall "chat_json_length" cChatJsonLength :: CString -> IO CInt + foreign export ccall "chat_encrypt_media" cChatEncryptMedia :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CString foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString @@ -107,9 +126,11 @@ foreign export ccall "chat_encrypt_file" cChatEncryptFile :: StablePtr ChatContr foreign export ccall "chat_decrypt_file" cChatDecryptFile :: CString -> CString -> CString -> CString -> IO CString -- | check / migrate database and initialize chat controller on success +-- For postgres first param is schema prefix, second param is database connection string. cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString cChatMigrateInit fp key conf = cChatMigrateInitKey fp key 0 conf 0 +-- For postgres first param is schema prefix, second param is database connection string. cChatMigrateInitKey :: CString -> CString -> CInt -> CString -> CInt -> Ptr (StablePtr ChatController) -> IO CJSONString cChatMigrateInitKey fp key keepKey conf background ctrl = do -- ensure we are set to UTF-8; iOS does not have locale, and will default to @@ -118,11 +139,10 @@ cChatMigrateInitKey fp key keepKey conf background ctrl = do setFileSystemEncoding utf8 setForeignEncoding utf8 - dbPath <- peekCString fp - dbKey <- BA.convert <$> B.packCString key + chatDbOpts <- mobileDbOpts fp key confirm <- peekCAString conf r <- - chatMigrateInitKey dbPath dbKey (keepKey /= 0) confirm (background /= 0) >>= \case + chatMigrateInitKey chatDbOpts (keepKey /= 0) confirm (background /= 0) >>= \case Right cc -> (newStablePtr cc >>= poke ctrl) $> DBMOk Left e -> pure e newCStringFromLazyBS $ J.encode r @@ -176,29 +196,35 @@ cChatPasswordHash cPwd cSalt = do cChatValidName :: CString -> IO CString cChatValidName cName = newCString . mkValidName =<< peekCString cName -mobileChatOpts :: String -> ChatOpts -mobileChatOpts dbFilePrefix = +-- | returns length of JSON encoded string +cChatJsonLength :: CString -> IO CInt +cChatJsonLength s = fromIntegral . subtract 2 . LB.length . J.encode . safeDecodeUtf8 <$> B.packCString s + +mobileChatOpts :: ChatDbOpts -> ChatOpts +mobileChatOpts dbOptions = ChatOpts { coreOptions = CoreChatOpts - { dbFilePrefix, - dbKey = "", -- for API database is already opened, and the key in options is not used + { dbOptions, smpServers = [], xftpServers = [], - networkConfig = defaultNetworkConfig, + simpleNetCfg = defaultSimpleNetCfg, logLevel = CLLImportant, logConnections = False, logServerHosts = True, logAgent = Nothing, logFile = Nothing, - tbqSize = 1024, - highlyAvailable = False + tbqSize = 4096, + deviceName = Nothing, + highlyAvailable = False, + yesToUpMigrations = False }, - deviceName = Nothing, chatCmd = "", chatCmdDelay = 3, + chatCmdLog = CCLNone, chatServerPort = Nothing, optFilesFolder = Nothing, + optTempDirectory = Nothing, showReactions = False, allowInstantFiles = True, autoAcceptFileSize = 0, @@ -216,42 +242,53 @@ defaultMobileConfig = deviceNameForRemote = "Mobile" } -getActiveUser_ :: SQLiteStore -> IO (Maybe User) +getActiveUser_ :: DBStore -> IO (Maybe User) getActiveUser_ st = find activeUser <$> withTransaction st getUsers +#if !defined(dbPostgres) +-- only used in tests chatMigrateInit :: String -> ScrubbedBytes -> String -> IO (Either DBMigrationResult ChatController) -chatMigrateInit dbFilePrefix dbKey confirm = chatMigrateInitKey dbFilePrefix dbKey False confirm False +chatMigrateInit dbFilePrefix dbKey confirm = do + let chatDBOpts = ChatDbOpts {dbFilePrefix, dbKey, trackQueries = DB.TQSlow 5000, vacuumOnMigration = True} + chatMigrateInitKey chatDBOpts False confirm False +#endif -chatMigrateInitKey :: String -> ScrubbedBytes -> Bool -> String -> Bool -> IO (Either DBMigrationResult ChatController) -chatMigrateInitKey dbFilePrefix dbKey keepKey confirm backgroundMode = runExceptT $ do +chatMigrateInitKey :: ChatDbOpts -> Bool -> String -> Bool -> IO (Either DBMigrationResult ChatController) +chatMigrateInitKey chatDbOpts keepKey confirm backgroundMode = runExceptT $ do confirmMigrations <- liftEitherWith (const DBMInvalidConfirmation) $ strDecode $ B.pack confirm - chatStore <- migrate createChatStore (chatStoreFile dbFilePrefix) confirmMigrations - agentStore <- migrate createAgentStore (agentStoreFile dbFilePrefix) confirmMigrations + chatStore <- migrate createChatStore (toDBOpts chatDbOpts chatSuffix keepKey) confirmMigrations + agentStore <- migrate createAgentStore (toDBOpts chatDbOpts agentSuffix keepKey) confirmMigrations liftIO $ initialize chatStore ChatDatabase {chatStore, agentStore} where + opts = mobileChatOpts $ removeDbKey chatDbOpts initialize st db = do user_ <- getActiveUser_ st - newChatController db user_ defaultMobileConfig (mobileChatOpts dbFilePrefix) backgroundMode - migrate createStore dbFile confirmMigrations = + newChatController db user_ defaultMobileConfig opts backgroundMode + migrate createStore dbOpts confirmMigrations = ExceptT $ - (first (DBMErrorMigration dbFile) <$> createStore dbFile dbKey keepKey confirmMigrations) + (first (DBMErrorMigration errDbStr) <$> createStore dbOpts confirmMigrations) +#if !defined(dbPostgres) `catch` (pure . checkDBError) +#endif `catchAll` (pure . dbError) where + errDbStr = errorDbStr dbOpts +#if !defined(dbPostgres) checkDBError e = case sqlError e of - DB.ErrorNotADatabase -> Left $ DBMErrorNotADatabase dbFile + DB.ErrorNotADatabase -> Left $ DBMErrorNotADatabase errDbStr _ -> dbError e - dbError e = Left . DBMErrorSQL dbFile $ show e +#endif + dbError e = Left . DBMErrorSQL errDbStr $ show e chatCloseStore :: ChatController -> IO String chatCloseStore ChatController {chatStore, smpAgent} = handleErr $ do - closeSQLiteStore chatStore - closeSQLiteStore $ agentClientStore smpAgent + closeDBStore chatStore + closeDBStore $ agentClientStore smpAgent chatReopenStore :: ChatController -> IO String chatReopenStore ChatController {chatStore, smpAgent} = handleErr $ do - reopenSQLiteStore chatStore - reopenSQLiteStore (agentClientStore smpAgent) + reopenDBStore chatStore + reopenDBStore (agentClientStore smpAgent) handleErr :: IO () -> IO String handleErr a = (a $> "") `catch` (pure . show @SomeException) @@ -260,12 +297,15 @@ chatSendCmd :: ChatController -> B.ByteString -> IO JSONByteString chatSendCmd cc = chatSendRemoteCmd cc Nothing chatSendRemoteCmd :: ChatController -> Maybe RemoteHostId -> B.ByteString -> IO JSONByteString -chatSendRemoteCmd cc rh s = J.encode . APIResponse Nothing rh <$> runReaderT (execChatCommand rh s) cc +chatSendRemoteCmd cc rh s = J.encode . eitherToResult rh <$> runReaderT (execChatCommand rh s) cc chatRecvMsg :: ChatController -> IO JSONByteString -chatRecvMsg ChatController {outputQ} = json <$> atomically (readTBQueue outputQ) +chatRecvMsg ChatController {outputQ} = J.encode . uncurry eitherToResult <$> readChatResponse where - json (corr, remoteHostId, resp) = J.encode APIResponse {corr, remoteHostId, resp} + readChatResponse = + atomically (readTBQueue outputQ) >>= \case + (_, Right CEvtTerminalEvent {}) -> readChatResponse + out -> pure out chatRecvMsgWait :: ChatController -> Int -> IO JSONByteString chatRecvMsgWait cc time = fromMaybe "" <$> timeout time (chatRecvMsg cc) diff --git a/src/Simplex/Chat/Mobile/Shared.hs b/src/Simplex/Chat/Mobile/Shared.hs index a4961c15f3..fcde581ea3 100644 --- a/src/Simplex/Chat/Mobile/Shared.hs +++ b/src/Simplex/Chat/Mobile/Shared.hs @@ -3,7 +3,7 @@ module Simplex.Chat.Mobile.Shared where import qualified Data.ByteString as B -import Data.ByteString.Internal (ByteString (..), memcpy) +import Data.ByteString.Internal (ByteString (..)) import qualified Data.ByteString.Lazy as LB import qualified Data.ByteString.Lazy.Internal as LB import Foreign @@ -16,12 +16,12 @@ type JSONByteString = LB.ByteString getByteString :: Ptr Word8 -> CInt -> IO ByteString getByteString ptr len = do fp <- newForeignPtr_ ptr - pure $ BS fp $ fromIntegral len + pure $ PS fp 0 (fromIntegral len) {-# INLINE getByteString #-} putByteString :: Ptr Word8 -> ByteString -> IO () -putByteString ptr (BS fp len) = - withForeignPtr fp $ \p -> memcpy ptr p len +putByteString ptr (PS fp offset len) = + withForeignPtr fp $ \p -> copyBytes ptr (p `plusPtr` offset) len {-# INLINE putByteString #-} putLazyByteString :: Ptr Word8 -> LB.ByteString -> IO () diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs new file mode 100644 index 0000000000..8c4490a2c4 --- /dev/null +++ b/src/Simplex/Chat/Operators.hs @@ -0,0 +1,538 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilyDependencies #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} + +module Simplex.Chat.Operators where + +import Control.Applicative ((<|>)) +import Data.Aeson (FromJSON (..), ToJSON (..)) +import qualified Data.Aeson as J +import qualified Data.Aeson.Encoding as JE +import qualified Data.Aeson.TH as JQ +import Data.FileEmbed +import Data.Foldable (foldMap') +import Data.Functor.Identity +import Data.IORef +import Data.Int (Int64) +import Data.Kind +import Data.List (find, foldl') +import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as L +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as M +import Data.Maybe (fromMaybe, isNothing, mapMaybe) +import Data.Scientific (floatingOrInteger) +import Data.Set (Set) +import qualified Data.Set as S +import Data.Text (Text) +import qualified Data.Text as T +import Data.Time (addUTCTime) +import Data.Time.Clock (UTCTime, nominalDay) +import Language.Haskell.TH.Syntax (lift) +import Simplex.Chat.Operators.Conditions +import Simplex.Chat.Options.DB (FromField (..), ToField (..)) +import Simplex.Chat.Types (User) +import Simplex.Chat.Types.Util (textParseJSON) +import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles) +import Simplex.Messaging.Agent.Store.DB (fromTextField_) +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, sumTypeJSON) +import Simplex.Messaging.Protocol (AProtocolType (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI, SProtocolType (..), UserProtocol) +import Simplex.Messaging.Transport.Client (TransportHost (..)) +import Simplex.Messaging.Util (atomicModifyIORef'_, safeDecodeUtf8) + +usageConditionsCommit :: Text +usageConditionsCommit = "7471fd2af5838dc0467aebc570b5ea75e5df3209" + +previousConditionsCommit :: Text +previousConditionsCommit = "a5061f3147165a05979d6ace33960aced2d6ac03" + +usageConditionsText :: Text +usageConditionsText = + $( let s = $(embedFile =<< makeRelativeToProject "PRIVACY.md") + in [|stripFrontMatter $(lift (safeDecodeUtf8 s))|] + ) + +data DBStored = DBStored | DBNew + +data SDBStored (s :: DBStored) where + SDBStored :: SDBStored 'DBStored + SDBNew :: SDBStored 'DBNew + +deriving instance Show (SDBStored s) + +class DBStoredI s where sdbStored :: SDBStored s + +instance DBStoredI 'DBStored where sdbStored = SDBStored + +instance DBStoredI 'DBNew where sdbStored = SDBNew + +data DBEntityId' (s :: DBStored) where + DBEntityId :: Int64 -> DBEntityId' 'DBStored + DBNewEntity :: DBEntityId' 'DBNew + +deriving instance Show (DBEntityId' s) + +deriving instance Eq (DBEntityId' s) + +type DBEntityId = DBEntityId' 'DBStored + +type DBNewEntity = DBEntityId' 'DBNew + +data OperatorTag = OTSimplex | OTFlux + deriving (Eq, Ord, Show) + +instance FromField OperatorTag where fromField = fromTextField_ textDecode + +instance ToField OperatorTag where toField = toField . textEncode + +instance FromJSON OperatorTag where + parseJSON = textParseJSON "OperatorTag" + +instance ToJSON OperatorTag where + toJSON = J.String . textEncode + toEncoding = JE.text . textEncode + +instance TextEncoding OperatorTag where + textDecode = \case + "simplex" -> Just OTSimplex + "flux" -> Just OTFlux + _ -> Nothing + textEncode = \case + OTSimplex -> "simplex" + OTFlux -> "flux" + +-- this and other types only define instances of serialization for known DB IDs only, +-- entities without IDs cannot be serialized to JSON +instance FromField DBEntityId +#if defined(dbPostgres) + where + fromField f dat = DBEntityId <$> fromField f dat +#else + where + fromField f = DBEntityId <$> fromField f +#endif + +instance ToField DBEntityId where toField (DBEntityId i) = toField i + +data UsageConditions = UsageConditions + { conditionsId :: Int64, + conditionsCommit :: Text, + notifiedAt :: Maybe UTCTime, + createdAt :: UTCTime + } + deriving (Show) + +data UsageConditionsAction + = UCAReview {operators :: [ServerOperator], deadline :: Maybe UTCTime, showNotice :: Bool} + | UCAAccepted {operators :: [ServerOperator]} + deriving (Show) + +data ServerOperatorConditions = ServerOperatorConditions + { serverOperators :: [ServerOperator], + currentConditions :: UsageConditions, + conditionsAction :: Maybe UsageConditionsAction + } + deriving (Show) + +usageConditionsAction :: [ServerOperator] -> UsageConditions -> UTCTime -> Maybe UsageConditionsAction +usageConditionsAction operators UsageConditions {createdAt, notifiedAt} now = do + let enabledOperators = filter (\ServerOperator {enabled} -> enabled) operators + if + | null enabledOperators -> Nothing + | all conditionsAccepted enabledOperators -> + let acceptedForOperators = filter conditionsAccepted operators + in Just $ UCAAccepted acceptedForOperators + | otherwise -> + let acceptForOperators = filter (not . conditionsAccepted) enabledOperators + deadline = conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) + showNotice = isNothing notifiedAt + in Just $ UCAReview acceptForOperators deadline showNotice + +conditionsRequiredOrDeadline :: UTCTime -> UTCTime -> Maybe UTCTime +conditionsRequiredOrDeadline createdAt notifiedAtOrNow = + if notifiedAtOrNow < addUTCTime (14 * nominalDay) createdAt + then Just $ conditionsDeadline notifiedAtOrNow + else Nothing -- required + where + conditionsDeadline :: UTCTime -> UTCTime + conditionsDeadline = addUTCTime (31 * nominalDay) + +data ConditionsAcceptance + = CAAccepted {acceptedAt :: Maybe UTCTime, autoAccepted :: Bool} + | CARequired {deadline :: Maybe UTCTime} + deriving (Show) + +type ServerOperator = ServerOperator' 'DBStored + +type NewServerOperator = ServerOperator' 'DBNew + +data AServerOperator = forall s. ASO (SDBStored s) (ServerOperator' s) + +deriving instance Show AServerOperator + +data ServerOperator' s = ServerOperator + { operatorId :: DBEntityId' s, + operatorTag :: Maybe OperatorTag, + tradeName :: Text, + legalName :: Maybe Text, + serverDomains :: [Text], + conditionsAcceptance :: ConditionsAcceptance, + enabled :: Bool, + smpRoles :: ServerRoles, + xftpRoles :: ServerRoles + } + deriving (Show) + +data ServerOperatorRoles = ServerOperatorRoles + { operatorId' :: Int64, + enabled' :: Bool, + smpRoles' :: ServerRoles, + xftpRoles' :: ServerRoles + } + deriving (Show) + +operatorRoles :: UserProtocol p => SProtocolType p -> ServerOperator -> ServerRoles +operatorRoles p op = case p of + SPSMP -> smpRoles op + SPXFTP -> xftpRoles op + +conditionsAccepted :: ServerOperator -> Bool +conditionsAccepted ServerOperator {conditionsAcceptance} = case conditionsAcceptance of + CAAccepted {} -> True + _ -> False + +data UserOperatorServers = UserOperatorServers + { operator :: Maybe ServerOperator, + smpServers :: [UserServer 'PSMP], + xftpServers :: [UserServer 'PXFTP] + } + deriving (Show) + +data UpdatedUserOperatorServers = UpdatedUserOperatorServers + { operator :: Maybe ServerOperator, + smpServers :: [AUserServer 'PSMP], + xftpServers :: [AUserServer 'PXFTP] + } + deriving (Show) + +data ValidatedProtoServer p = ValidatedProtoServer {unVPS :: Either Text (ProtoServerWithAuth p)} + deriving (Show) + +class UserServersClass u where + type AServer u = (s :: ProtocolType -> Type) | s -> u + operator' :: u -> Maybe ServerOperator + aUserServer' :: AServer u p -> AUserServer p + servers' :: UserProtocol p => SProtocolType p -> u -> [AServer u p] + +instance UserServersClass UserOperatorServers where + type AServer UserOperatorServers = UserServer' 'DBStored + operator' UserOperatorServers {operator} = operator + aUserServer' = AUS SDBStored + servers' p UserOperatorServers {smpServers, xftpServers} = case p of + SPSMP -> smpServers + SPXFTP -> xftpServers + +instance UserServersClass UpdatedUserOperatorServers where + type AServer UpdatedUserOperatorServers = AUserServer + operator' UpdatedUserOperatorServers {operator} = operator + aUserServer' = id + servers' p UpdatedUserOperatorServers {smpServers, xftpServers} = case p of + SPSMP -> smpServers + SPXFTP -> xftpServers + +type UserServer p = UserServer' 'DBStored p + +type NewUserServer p = UserServer' 'DBNew p + +data AUserServer p = forall s. AUS (SDBStored s) (UserServer' s p) + +deriving instance Show (AUserServer p) + +data UserServer' s (p :: ProtocolType) = UserServer + { serverId :: DBEntityId' s, + server :: ProtoServerWithAuth p, + preset :: Bool, + tested :: Maybe Bool, + enabled :: Bool, + deleted :: Bool + } + deriving (Show) + +presetServerAddress :: UserServer' s p -> ProtocolServer p +presetServerAddress UserServer {server = ProtoServerWithAuth srv _} = srv +{-# INLINE presetServerAddress #-} + +data PresetOperator = PresetOperator + { operator :: Maybe NewServerOperator, + smp :: [NewUserServer 'PSMP], + useSMP :: Int, + xftp :: [NewUserServer 'PXFTP], + useXFTP :: Int + } + deriving (Show) + +pOperator :: PresetOperator -> Maybe NewServerOperator +pOperator PresetOperator {operator} = operator + +pServers :: UserProtocol p => SProtocolType p -> PresetOperator -> [NewUserServer p] +pServers p PresetOperator {smp, xftp} = case p of + SPSMP -> smp + SPXFTP -> xftp + +operatorServersToUse :: UserProtocol p => SProtocolType p -> PresetOperator -> Int +operatorServersToUse p PresetOperator {useSMP, useXFTP} = case p of + SPSMP -> useSMP + SPXFTP -> useXFTP + +presetServer' :: Bool -> ProtocolServer p -> NewUserServer p +presetServer' enabled = presetServer enabled . (`ProtoServerWithAuth` Nothing) + +presetServer :: Bool -> ProtoServerWithAuth p -> NewUserServer p +presetServer = newUserServer_ True + +newUserServer :: ProtoServerWithAuth p -> NewUserServer p +newUserServer = newUserServer_ False True + +newUserServer_ :: Bool -> Bool -> ProtoServerWithAuth p -> NewUserServer p +newUserServer_ preset enabled server = + UserServer {serverId = DBNewEntity, server, preset, tested = Nothing, enabled, deleted = False} + +-- This function should be used inside DB transaction to update conditions in the database +-- it evaluates to (current conditions, and conditions to add) +usageConditionsToAdd :: Bool -> UTCTime -> [UsageConditions] -> (UsageConditions, [UsageConditions]) +usageConditionsToAdd = usageConditionsToAdd' previousConditionsCommit usageConditionsCommit + +-- This function is used in unit tests +usageConditionsToAdd' :: Text -> Text -> Bool -> UTCTime -> [UsageConditions] -> (UsageConditions, [UsageConditions]) +usageConditionsToAdd' prevCommit sourceCommit newUser createdAt = \case + [] + | newUser -> (sourceCond, [sourceCond]) + | otherwise -> (sourceCond, [prevCond, sourceCond]) + where + prevCond = conditions 1 prevCommit + sourceCond = conditions 2 sourceCommit + conds + | hasSourceCond -> (last conds, []) + | otherwise -> (sourceCond, [sourceCond]) + where + hasSourceCond = any ((sourceCommit ==) . conditionsCommit) conds + sourceCond = conditions cId sourceCommit + cId = maximum (map conditionsId conds) + 1 + where + conditions cId commit = UsageConditions {conditionsId = cId, conditionsCommit = commit, notifiedAt = Nothing, createdAt} + +presetUserServers :: [(Maybe PresetOperator, Maybe ServerOperator)] -> [UpdatedUserOperatorServers] +presetUserServers = mapMaybe $ \(presetOp_, op) -> mkUS op <$> presetOp_ + where + mkUS op PresetOperator {smp, xftp} = + UpdatedUserOperatorServers op (map (AUS SDBNew) smp) (map (AUS SDBNew) xftp) + +-- This function should be used inside DB transaction to update operators. +-- It allows to add/remove/update preset operators in the database preserving enabled and roles settings, +-- and preserves custom operators without tags for forward compatibility. +updatedServerOperators :: NonEmpty PresetOperator -> [ServerOperator] -> [(Maybe PresetOperator, Maybe AServerOperator)] +updatedServerOperators presetOps storedOps = + foldr addPreset [] presetOps + <> map (\op -> (Nothing, Just $ ASO SDBStored op)) (filter (isNothing . operatorTag) storedOps) + where + -- TODO remove domains of preset operators from custom + addPreset op = ((Just op, storedOp' <$> pOperator op) :) + where + storedOp' presetOp = case find ((operatorTag presetOp ==) . operatorTag) storedOps of + Just ServerOperator {operatorId, conditionsAcceptance, enabled, smpRoles, xftpRoles} -> + ASO SDBStored presetOp {operatorId, conditionsAcceptance, enabled, smpRoles, xftpRoles} + Nothing -> ASO SDBNew presetOp + +-- This function should be used inside DB transaction to update servers. +updatedUserServers :: (Maybe PresetOperator, UserOperatorServers) -> UpdatedUserOperatorServers +updatedUserServers (presetOp_, UserOperatorServers {operator, smpServers, xftpServers}) = + UpdatedUserOperatorServers {operator, smpServers = smp', xftpServers = xftp'} + where + stored = map (AUS SDBStored) + (smp', xftp') = case presetOp_ of + Nothing -> (stored smpServers, stored xftpServers) + Just presetOp -> (updated SPSMP smpServers, updated SPXFTP xftpServers) + where + updated :: forall p. UserProtocol p => SProtocolType p -> [UserServer p] -> [AUserServer p] + updated p srvs = map userServer presetSrvs <> stored (filter customServer srvs) + where + storedSrvs :: Map (ProtoServerWithAuth p) (UserServer p) + storedSrvs = foldl' (\ss srv@UserServer {server} -> M.insert server srv ss) M.empty srvs + customServer :: UserServer p -> Bool + customServer srv = not (preset srv) && all (`S.notMember` presetHosts) (srvHost srv) + presetSrvs :: [NewUserServer p] + presetSrvs = pServers p presetOp + presetHosts :: Set TransportHost + presetHosts = foldMap' (S.fromList . L.toList . srvHost) presetSrvs + userServer :: NewUserServer p -> AUserServer p + userServer srv@UserServer {server} = maybe (AUS SDBNew srv) (AUS SDBStored) (M.lookup server storedSrvs) + +srvHost :: UserServer' s p -> NonEmpty TransportHost +srvHost UserServer {server = ProtoServerWithAuth srv _} = host srv + +agentServerCfgs :: UserProtocol p => SProtocolType p -> [(Text, ServerOperator)] -> [UserServer' s p] -> [ServerCfg p] +agentServerCfgs p opDomains = mapMaybe agentServer + where + agentServer :: UserServer' s p -> Maybe (ServerCfg p) + agentServer srv@UserServer {server, enabled} = + case find (\(d, _) -> any (matchingHost d) (srvHost srv)) opDomains of + Just (_, op@ServerOperator {operatorId = DBEntityId opId, enabled = opEnabled}) + | opEnabled -> Just ServerCfg {server, enabled, operator = Just opId, roles = operatorRoles p op} + | otherwise -> Nothing + Nothing -> + Just ServerCfg {server, enabled, operator = Nothing, roles = allRoles} + +matchingHost :: Text -> TransportHost -> Bool +matchingHost d = \case + THDomainName h -> d `T.isSuffixOf` T.pack h + _ -> False + +operatorDomains :: [ServerOperator' s] -> [(Text, ServerOperator' s)] +operatorDomains = foldr (\op ds -> foldr (\d -> ((d, op) :)) ds (serverDomains op)) [] + +class Box b where + box :: a -> b a + unbox :: b a -> a + +instance Box Identity where + box = Identity + unbox = runIdentity + +instance Box ((,) (Maybe a)) where + box = (Nothing,) + unbox = snd + +groupByOperator :: ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> IO [UserOperatorServers] +groupByOperator (ops, smpSrvs, xftpSrvs) = map runIdentity <$> groupByOperator_ (map Identity ops, smpSrvs, xftpSrvs) + +-- For the initial app start this function relies on tuple being Functor/Box +-- to preserve the information about operator being DBNew or DBStored +groupByOperator' :: ([(Maybe PresetOperator, Maybe ServerOperator)], [UserServer 'PSMP], [UserServer 'PXFTP]) -> IO [(Maybe PresetOperator, UserOperatorServers)] +groupByOperator' = groupByOperator_ +{-# INLINE groupByOperator' #-} + +groupByOperator_ :: forall f. (Box f, Traversable f) => ([f (Maybe ServerOperator)], [UserServer 'PSMP], [UserServer 'PXFTP]) -> IO [f UserOperatorServers] +groupByOperator_ (ops, smpSrvs, xftpSrvs) = do + let ops' = mapMaybe sequence ops + customOp_ = find (isNothing . unbox) ops + ss <- mapM ((\op -> (serverDomains (unbox op),) <$> newIORef (mkUS . Just <$> op))) ops' + custom <- newIORef $ maybe (box $ mkUS Nothing) (mkUS <$>) customOp_ + mapM_ (addServer ss custom addSMP) (reverse smpSrvs) + mapM_ (addServer ss custom addXFTP) (reverse xftpSrvs) + opSrvs <- mapM (readIORef . snd) ss + customSrvs <- readIORef custom + pure $ opSrvs <> [customSrvs] + where + mkUS op = UserOperatorServers op [] [] + addServer :: [([Text], IORef (f UserOperatorServers))] -> IORef (f UserOperatorServers) -> (UserServer p -> UserOperatorServers -> UserOperatorServers) -> UserServer p -> IO () + addServer ss custom add srv = + let v = maybe custom snd $ find (\(ds, _) -> any (\d -> any (matchingHost d) (srvHost srv)) ds) ss + in atomicModifyIORef'_ v (add srv <$>) + addSMP srv s@UserOperatorServers {smpServers} = (s :: UserOperatorServers) {smpServers = srv : smpServers} + addXFTP srv s@UserOperatorServers {xftpServers} = (s :: UserOperatorServers) {xftpServers = srv : xftpServers} + +data UserServersError + = USENoServers {protocol :: AProtocolType, user :: Maybe User} + | USEStorageMissing {protocol :: AProtocolType, user :: Maybe User} + | USEProxyMissing {protocol :: AProtocolType, user :: Maybe User} + | USEDuplicateServer {protocol :: AProtocolType, duplicateServer :: Text, duplicateHost :: TransportHost} + deriving (Show) + +validateUserServers :: UserServersClass u' => [u'] -> [(User, [UserOperatorServers])] -> [UserServersError] +validateUserServers curr others = currUserErrs <> concatMap otherUserErrs others + where + currUserErrs = noServersErrs SPSMP Nothing curr <> noServersErrs SPXFTP Nothing curr <> serverErrs SPSMP curr <> serverErrs SPXFTP curr + otherUserErrs (user, uss) = noServersErrs SPSMP (Just user) uss <> noServersErrs SPXFTP (Just user) uss + noServersErrs :: (UserServersClass u, ProtocolTypeI p, UserProtocol p) => SProtocolType p -> Maybe User -> [u] -> [UserServersError] + noServersErrs p user uss + | noServers opEnabled = [USENoServers p' user] + | otherwise = [USEStorageMissing p' user | noServers (hasRole storage)] <> [USEProxyMissing p' user | noServers (hasRole proxy)] + where + p' = AProtocolType p + noServers cond = not $ any srvEnabled $ userServers p $ filter cond uss + opEnabled = maybe True (\ServerOperator {enabled} -> enabled) . operator' + hasRole roleSel = maybe True (\op@ServerOperator {enabled} -> enabled && roleSel (operatorRoles p op)) . operator' + srvEnabled (AUS _ UserServer {deleted, enabled}) = enabled && not deleted + serverErrs :: (UserServersClass u, ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [u] -> [UserServersError] + serverErrs p uss = mapMaybe duplicateErr_ srvs + where + p' = AProtocolType p + srvs = filter (\(AUS _ UserServer {deleted}) -> not deleted) $ userServers p uss + duplicateErr_ (AUS _ srv@UserServer {server}) = + USEDuplicateServer p' (safeDecodeUtf8 $ strEncode server) + <$> find (`S.member` duplicateHosts) (srvHost srv) + duplicateHosts = snd $ foldl' addHost (S.empty, S.empty) allHosts + allHosts = concatMap (\(AUS _ srv) -> L.toList $ srvHost srv) srvs + addHost (hs, dups) h + | h `S.member` hs = (hs, S.insert h dups) + | otherwise = (S.insert h hs, dups) + userServers :: (UserServersClass u, UserProtocol p) => SProtocolType p -> [u] -> [AUserServer p] + userServers p = map aUserServer' . concatMap (servers' p) + +instance ToJSON (DBEntityId' s) where + toEncoding = \case + DBEntityId i -> toEncoding i + DBNewEntity -> JE.null_ + toJSON = \case + DBEntityId i -> toJSON i + DBNewEntity -> J.Null + +instance DBStoredI s => FromJSON (DBEntityId' s) where + parseJSON v = case (v, sdbStored @s) of + (J.Null, SDBNew) -> pure DBNewEntity + (J.Number n, SDBStored) -> case floatingOrInteger n of + Left (_ :: Double) -> fail "bad DBEntityId" + Right i -> pure $ DBEntityId (fromInteger i) + _ -> fail "bad DBEntityId" + omittedField = case sdbStored @s of + SDBStored -> Nothing + SDBNew -> Just DBNewEntity + +$(JQ.deriveJSON defaultJSON ''UsageConditions) + +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CA") ''ConditionsAcceptance) + +instance ToJSON (ServerOperator' s) where + toEncoding = $(JQ.mkToEncoding defaultJSON ''ServerOperator') + toJSON = $(JQ.mkToJSON defaultJSON ''ServerOperator') + +instance DBStoredI s => FromJSON (ServerOperator' s) where + parseJSON = $(JQ.mkParseJSON defaultJSON ''ServerOperator') + +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "UCA") ''UsageConditionsAction) + +$(JQ.deriveJSON defaultJSON ''ServerOperatorConditions) + +instance ProtocolTypeI p => ToJSON (UserServer' s p) where + toEncoding = $(JQ.mkToEncoding defaultJSON ''UserServer') + toJSON = $(JQ.mkToJSON defaultJSON ''UserServer') + +instance (DBStoredI s, ProtocolTypeI p) => FromJSON (UserServer' s p) where + parseJSON = $(JQ.mkParseJSON defaultJSON ''UserServer') + +instance ProtocolTypeI p => FromJSON (AUserServer p) where + parseJSON v = (AUS SDBStored <$> parseJSON v) <|> (AUS SDBNew <$> parseJSON v) + +$(JQ.deriveJSON defaultJSON ''UserOperatorServers) + +instance FromJSON UpdatedUserOperatorServers where + parseJSON = $(JQ.mkParseJSON defaultJSON ''UpdatedUserOperatorServers) + +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "USE") ''UserServersError) diff --git a/src/Simplex/Chat/Operators/Conditions.hs b/src/Simplex/Chat/Operators/Conditions.hs new file mode 100644 index 0000000000..a314c1901a --- /dev/null +++ b/src/Simplex/Chat/Operators/Conditions.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Simplex.Chat.Operators.Conditions where + +import Data.Char (isSpace) +import Data.Text (Text) +import qualified Data.Text as T + +stripFrontMatter :: Text -> Text +stripFrontMatter = + T.unlines + -- . dropWhile ("# " `T.isPrefixOf`) -- strip title + . dropWhile (T.all isSpace) + . dropWhile fm + . (\ls -> let ls' = dropWhile (not . fm) ls in if null ls' then ls else ls') + . dropWhile fm + . T.lines + where + fm = ("---" `T.isPrefixOf`) diff --git a/src/Simplex/Chat/Operators/Presets.hs b/src/Simplex/Chat/Operators/Presets.hs new file mode 100644 index 0000000000..06c2e19fab --- /dev/null +++ b/src/Simplex/Chat/Operators/Presets.hs @@ -0,0 +1,117 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} + +module Simplex.Chat.Operators.Presets where + +import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as L +import Simplex.Chat.Operators +import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..), allRoles) +import Simplex.Messaging.Protocol (ProtocolType (..), SMPServer) + +operatorSimpleXChat :: NewServerOperator +operatorSimpleXChat = + ServerOperator + { operatorId = DBNewEntity, + operatorTag = Just OTSimplex, + tradeName = "SimpleX Chat", + legalName = Just "SimpleX Chat Ltd", + serverDomains = ["simplex.im"], + conditionsAcceptance = CARequired Nothing, + enabled = True, + smpRoles = allRoles, + xftpRoles = allRoles + } + +operatorFlux :: NewServerOperator +operatorFlux = + ServerOperator + { operatorId = DBNewEntity, + operatorTag = Just OTFlux, + tradeName = "Flux", + legalName = Just "InFlux Technologies Limited", + serverDomains = ["simplexonflux.com"], + conditionsAcceptance = CARequired Nothing, + enabled = False, + smpRoles = ServerRoles {storage = False, proxy = True}, + xftpRoles = ServerRoles {storage = False, proxy = True} + } + +-- Please note: if any servers are removed from the lists below, they MUST be added here. +-- Otherwise previously created short links won't work. +-- +-- !!! Also, if any servers need to be added, shortLinkPresetServers will need to be be split to two, +-- so that option used for restoring links is updated earlier, for backward/forward compatibility. +allPresetServers :: NonEmpty SMPServer +allPresetServers = enabledSimplexChatSMPServers <> disabledSimplexChatSMPServers <> fluxSMPServers_ + -- added for testing, not preset in the clients + <> ["smp://8Af90NX2TTkKEJAF1RCg69P_Odg2Z-6_J6DOKUqK3rQ=@smp7.simplex.im,dbxqutskmmbkbrs7ofi7pmopeyhgi5cxbjbh4ummgmep4r6bz4cbrcid.onion"] + +simplexChatSMPServers :: [NewUserServer 'PSMP] +simplexChatSMPServers = + map (presetServer' True) (L.toList enabledSimplexChatSMPServers) + <> map (presetServer' False) (L.toList disabledSimplexChatSMPServers) + +-- Please note: if any servers are removed from this list, they MUST be added to allPresetServers. +-- Otherwise previously created short links won't work. +-- +-- !!! Also, if any servers need to be added, shortLinkPresetServers will need to be be split to two, +-- so that option used for restoring links is updated earlier, for backward/forward compatibility. +enabledSimplexChatSMPServers :: NonEmpty SMPServer +enabledSimplexChatSMPServers = + [ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion", + "smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion", + "smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion", + "smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion", + "smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion", + "smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion", + "smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion", + "smp://hejn2gVIqNU6xjtGM3OwQeuk8ZEbDXVJXAlnSBJBWUA=@smp16.simplex.im,p3ktngodzi6qrf7w64mmde3syuzrv57y55hxabqcq3l5p6oi7yzze6qd.onion", + "smp://ZKe4uxF4Z_aLJJOEsC-Y6hSkXgQS5-oc442JQGkyP8M=@smp17.simplex.im,ogtwfxyi3h2h5weftjjpjmxclhb5ugufa5rcyrmg7j4xlch7qsr5nuqd.onion", + "smp://PtsqghzQKU83kYTlQ1VKg996dW4Cw4x_bvpKmiv8uns=@smp18.simplex.im,lyqpnwbs2zqfr45jqkncwpywpbtq7jrhxnib5qddtr6npjyezuwd3nqd.onion", + "smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion" + ] + +-- Please note: if any servers are removed from this list, they MUST be added to allPresetServers. +-- Otherwise previously created short links won't work. +-- +-- !!! Also, if any servers need to be added, shortLinkPresetServers will need to be be split to two, +-- so that option used for restoring links is updated earlier, for backward/forward compatibility. +disabledSimplexChatSMPServers :: NonEmpty SMPServer +disabledSimplexChatSMPServers = + [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", + "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", + "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" + ] + +fluxSMPServers :: [NewUserServer 'PSMP] +fluxSMPServers = map (presetServer' True) (L.toList fluxSMPServers_) + +-- Please note: if any servers are removed from this list, they MUST be added to allPresetServers. +-- Otherwise previously created short links won't work. +-- +-- !!! Also, if any servers need to be added, shortLinkPresetServers will need to be be split to two, +-- so that option used for restoring links is updated earlier, for backward/forward compatibility. +fluxSMPServers_ :: NonEmpty SMPServer +fluxSMPServers_ = + [ "smp://xQW_ufMkGE20UrTlBl8QqceG1tbuylXhr9VOLPyRJmw=@smp1.simplexonflux.com,qb4yoanyl4p7o33yrknv4rs6qo7ugeb2tu2zo66sbebezs4cpyosarid.onion", + "smp://LDnWZVlAUInmjmdpQQoIo6FUinRXGe0q3zi5okXDE4s=@smp2.simplexonflux.com,yiqtuh3q4x7hgovkomafsod52wvfjucdljqbbipg5sdssnklgongxbqd.onion", + "smp://1jne379u7IDJSxAvXbWb_JgoE7iabcslX0LBF22Rej0=@smp3.simplexonflux.com,a5lm4k7ufei66cdck6fy63r4lmkqy3dekmmb7jkfdm5ivi6kfaojshad.onion", + "smp://xmAmqj75I9mWrUihLUlI0ZuNLXlIwFIlHRq5Pb6cHAU=@smp4.simplexonflux.com,qpcz2axyy66u26hfdd2e23uohcf3y6c36mn7dcuilcgnwjasnrvnxjqd.onion", + "smp://rWvBYyTamuRCBYb_KAn-nsejg879ndhiTg5Sq3k0xWA=@smp5.simplexonflux.com,4ao347qwiuluyd45xunmii4skjigzuuox53hpdsgbwxqafd4yrticead.onion", + "smp://PN7-uqLBToqlf1NxHEaiL35lV2vBpXq8Nj8BW11bU48=@smp6.simplexonflux.com,hury6ot3ymebbr2535mlp7gcxzrjpc6oujhtfxcfh2m4fal4xw5fq6qd.onion" + ] + +fluxXFTPServers :: [NewUserServer 'PXFTP] +fluxXFTPServers = + map + (presetServer True) + [ "xftp://92Sctlc09vHl_nAqF2min88zKyjdYJ9mgxRCJns5K2U=@xftp1.simplexonflux.com,apl3pumq3emwqtrztykyyoomdx4dg6ysql5zek2bi3rgznz7ai3odkid.onion", + "xftp://YBXy4f5zU1CEhnbbCzVWTNVNsaETcAGmYqGNxHntiE8=@xftp2.simplexonflux.com,c5jjecisncnngysah3cz2mppediutfelco4asx65mi75d44njvua3xid.onion", + "xftp://ARQO74ZSvv2OrulRF3CdgwPz_AMy27r0phtLSq5b664=@xftp3.simplexonflux.com,dc4mohiubvbnsdfqqn7xhlhpqs5u4tjzp7xpz6v6corwvzvqjtaqqiqd.onion", + "xftp://ub2jmAa9U0uQCy90O-fSUNaYCj6sdhl49Jh3VpNXP58=@xftp4.simplexonflux.com,4qq5pzier3i4yhpuhcrhfbl6j25udc4czoyascrj4yswhodhfwev3nyd.onion", + "xftp://Rh19D5e4Eez37DEE9hAlXDB3gZa1BdFYJTPgJWPO9OI=@xftp5.simplexonflux.com,q7itltdn32hjmgcqwhow4tay5ijetng3ur32bolssw32fvc5jrwvozad.onion", + "xftp://0AznwoyfX8Od9T_acp1QeeKtxUi676IBIiQjXVwbdyU=@xftp6.simplexonflux.com,upvzf23ou6nrmaf3qgnhd6cn3d74tvivlmz3p7wdfwq6fhthjrjiiqid.onion" + ] diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index 85298ae310..ff2af56b85 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -8,37 +8,42 @@ module Simplex.Chat.Options ( ChatOpts (..), CoreChatOpts (..), + ChatCmdLog (..), chatOptsP, coreChatOptsP, getChatOpts, protocolServersP, - fullNetworkConfig, + defaultHostMode, + printDbOpts, ) where import Control.Logger.Simple (LogLevel (..)) import qualified Data.Attoparsec.ByteString.Char8 as A -import Data.ByteArray (ScrubbedBytes) import qualified Data.ByteString.Char8 as B +import Data.Maybe (fromMaybe) import Data.Text (Text) +import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) import Numeric.Natural (Natural) import Options.Applicative -import Simplex.Chat.Controller (ChatLogLevel (..), updateStr, versionNumber, versionString) +import Simplex.Chat.Controller (ChatLogLevel (..), SimpleNetCfg (..), updateStr, versionNumber, versionString) import Simplex.FileTransfer.Description (mb) -import Simplex.Messaging.Client (NetworkConfig (..), defaultNetworkConfig) +import Simplex.Messaging.Client (HostMode (..), SMPWebPortServers (..), SocksMode (..), textToHostMode) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth) -import Simplex.Messaging.Transport.Client (SocksProxy, defaultSocksProxy) -import System.FilePath (combine) +import Simplex.Messaging.Transport.Client (SocksProxyWithAuth (..), SocksAuth (..), defaultSocksProxyWithAuth) +import Simplex.Chat.Options.DB data ChatOpts = ChatOpts { coreOptions :: CoreChatOpts, - deviceName :: Maybe Text, chatCmd :: String, chatCmdDelay :: Int, + chatCmdLog :: ChatCmdLog, chatServerPort :: Maybe String, optFilesFolder :: Maybe FilePath, + optTempDirectory :: Maybe FilePath, showReactions :: Bool, allowInstantFiles :: Bool, autoAcceptFileSize :: Integer, @@ -48,20 +53,24 @@ data ChatOpts = ChatOpts } data CoreChatOpts = CoreChatOpts - { dbFilePrefix :: String, - dbKey :: ScrubbedBytes, + { dbOptions :: ChatDbOpts, smpServers :: [SMPServerWithAuth], xftpServers :: [XFTPServerWithAuth], - networkConfig :: NetworkConfig, + simpleNetCfg :: SimpleNetCfg, logLevel :: ChatLogLevel, logConnections :: Bool, logServerHosts :: Bool, logAgent :: Maybe LogLevel, logFile :: Maybe FilePath, tbqSize :: Natural, - highlyAvailable :: Bool + deviceName :: Maybe Text, + highlyAvailable :: Bool, + yesToUpMigrations :: Bool } +data ChatCmdLog = CCLAll | CCLMessages | CCLNone + deriving (Eq) + agentLogLevel :: ChatLogLevel -> LogLevel agentLogLevel = \case CLLDebug -> LogDebug @@ -71,24 +80,8 @@ agentLogLevel = \case CLLImportant -> LogInfo coreChatOptsP :: FilePath -> FilePath -> Parser CoreChatOpts -coreChatOptsP appDir defaultDbFileName = do - dbFilePrefix <- - strOption - ( long "database" - <> short 'd' - <> metavar "DB_FILE" - <> help "Path prefix to chat and agent database files" - <> value defaultDbFilePath - <> showDefault - ) - dbKey <- - strOption - ( long "key" - <> short 'k' - <> metavar "KEY" - <> help "Database encryption key/pass-phrase" - <> value "" - ) +coreChatOptsP appDir defaultDbName = do + dbOptions <- chatDbOptsP appDir defaultDbName smpServers <- option parseProtocolServers @@ -115,20 +108,68 @@ coreChatOptsP appDir defaultDbFileName = do <> value [] ) socksProxy <- - flag' (Just defaultSocksProxy) (short 'x' <> help "Use local SOCKS5 proxy at :9050") + flag' (Just defaultSocksProxyWithAuth) (short 'x' <> help "Use local SOCKS5 proxy at :9050") <|> option - parseSocksProxy + strParse ( long "socks-proxy" <> metavar "SOCKS5" <> help "Use SOCKS5 proxy at `ipv4:port` or `:port`" <> value Nothing ) + socksMode <- + option + strParse + ( long "socks-mode" + <> metavar "SOCKS_MODE" + <> help "Use SOCKS5 proxy: always (default), onion (with onion-only relays)" + <> value SMAlways + ) + hostMode_ <- + optional $ + option + parseHostMode + ( long "host-mode" + <> metavar "HOST_MODE" + <> help "Preferred server host type: onion (when SOCKS proxy with isolate-by-auth is used), public" + ) + requiredHostMode <- + switch + ( long "required-host-mode" + <> help "Refuse connection if preferred server host type is not available" + ) + smpProxyMode_ <- + optional $ + option + strParse + ( long "smp-proxy" + <> metavar "SMP_PROXY_MODE" + <> help "Use private message routing: always, unknown (default), unprotected, never" + ) + smpProxyFallback_ <- + optional $ + option + strParse + ( long "smp-proxy-fallback" + <> metavar "SMP_PROXY_FALLBACK_MODE" + <> help "Allow downgrade and connect directly: no, [when IP address is] protected (default), yes" + ) + smpWebPortServers <- + flag' SWPAll + ( long "smp-web-port" + <> help "Use port 443 with SMP servers when not specified" + ) + <|> option + strParse + ( long "smp-web-port-servers" + <> help "Use port 443 with SMP servers when not specified: all, preset (default), off" + <> value SWPPreset + ) t <- option auto ( long "tcp-timeout" <> metavar "TIMEOUT" - <> help "TCP timeout, seconds (default: 5/10 without/with SOCKS5 proxy)" + <> help "TCP timeout, seconds (default: 7/15 without/with SOCKS5 proxy)" <> value 0 ) logLevel <- @@ -143,7 +184,7 @@ coreChatOptsP appDir defaultDbFileName = do logTLSErrors <- switch ( long "log-tls-errors" - <> help "Log TLS errors (also enabled with `-l debug`)" + <> help "Log TLS errors" ) logConnections <- switch @@ -177,33 +218,6 @@ coreChatOptsP appDir defaultDbFileName = do <> value 1024 <> showDefault ) - highlyAvailable <- - switch - ( long "ha" - <> help "Run as a highly available client (this may increase traffic in groups)" - ) - pure - CoreChatOpts - { dbFilePrefix, - dbKey, - smpServers, - xftpServers, - networkConfig = fullNetworkConfig socksProxy (useTcpTimeout socksProxy t) (logTLSErrors || logLevel == CLLDebug), - logLevel, - logConnections = logConnections || logLevel <= CLLInfo, - logServerHosts = logServerHosts || logLevel <= CLLInfo, - logAgent = if logAgent || logLevel == CLLDebug then Just $ agentLogLevel logLevel else Nothing, - logFile, - tbqSize, - highlyAvailable - } - where - useTcpTimeout p t = 1000000 * if t > 0 then t else maybe 5 (const 10) p - defaultDbFilePath = combine appDir defaultDbFileName - -chatOptsP :: FilePath -> FilePath -> Parser ChatOpts -chatOptsP appDir defaultDbFileName = do - coreOptions <- coreChatOptsP appDir defaultDbFileName deviceName <- optional $ strOption @@ -211,6 +225,55 @@ chatOptsP appDir defaultDbFileName = do <> metavar "DEVICE" <> help "Device name to use in connections with remote hosts and controller" ) + highlyAvailable <- + switch + ( long "ha" + <> help "Run as a highly available client (this may increase traffic in groups)" + ) + yesToUpMigrations <- + switch + ( long "yes-migrate" + <> short 'y' + <> help "Automatically confirm \"up\" database migrations" + ) + pure + CoreChatOpts + { dbOptions, + smpServers, + xftpServers, + simpleNetCfg = + SimpleNetCfg + { socksProxy, + socksMode, + hostMode = fromMaybe (defaultHostMode socksProxy) hostMode_, + requiredHostMode, + smpProxyMode_, + smpProxyFallback_, + smpWebPortServers, + tcpTimeout_ = Just $ useTcpTimeout socksProxy t, + logTLSErrors + }, + logLevel, + logConnections = logConnections || logLevel <= CLLInfo, + logServerHosts = logServerHosts || logLevel <= CLLInfo, + logAgent = if logAgent || logLevel == CLLDebug then Just $ agentLogLevel logLevel else Nothing, + logFile, + tbqSize, + deviceName, + highlyAvailable, + yesToUpMigrations + } + where + useTcpTimeout p t = 1000000 * if t > 0 then t else maybe 7 (const 15) p + +defaultHostMode :: Maybe SocksProxyWithAuth -> HostMode +defaultHostMode = \case + Just (SocksProxyWithAuth SocksIsolateByAuth _) -> HMOnionViaSocks; + _ -> HMPublic + +chatOptsP :: FilePath -> FilePath -> Parser ChatOpts +chatOptsP appDir defaultDbName = do + coreOptions <- coreChatOptsP appDir defaultDbName chatCmd <- strOption ( long "execute" @@ -229,6 +292,14 @@ chatOptsP appDir defaultDbFileName = do <> value 3 <> showDefault ) + chatCmdLog <- + option + parseChatCmdLog + ( long "execute-log" + <> metavar "EXEC_LOG" + <> help "Log during command execution: all, messages, none (default)" + <> value CCLNone + ) chatServerPort <- option parseServerPort @@ -245,6 +316,13 @@ chatOptsP appDir defaultDbFileName = do <> metavar "FOLDER" <> help "Folder to use for sent and received files" ) + optTempDirectory <- + optional $ + strOption + ( long "temp-folder" + <> metavar "FOLDER" + <> help "Folder for temporary encrypted files (default: system temp directory)" + ) showReactions <- switch ( long "reactions" @@ -285,11 +363,12 @@ chatOptsP appDir defaultDbFileName = do pure ChatOpts { coreOptions, - deviceName, chatCmd, chatCmdDelay, + chatCmdLog, chatServerPort, optFilesFolder, + optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize, @@ -298,16 +377,14 @@ chatOptsP appDir defaultDbFileName = do maintenance } -fullNetworkConfig :: Maybe SocksProxy -> Int -> Bool -> NetworkConfig -fullNetworkConfig socksProxy tcpTimeout logTLSErrors = - let tcpConnectTimeout = (tcpTimeout * 3) `div` 2 - in defaultNetworkConfig {socksProxy, tcpTimeout, tcpConnectTimeout, logTLSErrors} - parseProtocolServers :: ProtocolTypeI p => ReadM [ProtoServerWithAuth p] parseProtocolServers = eitherReader $ parseAll protocolServersP . B.pack -parseSocksProxy :: ReadM (Maybe SocksProxy) -parseSocksProxy = eitherReader $ parseAll strP . B.pack +strParse :: StrEncoding a => ReadM a +strParse = eitherReader $ parseAll strP . encodeUtf8 . T.pack + +parseHostMode :: ReadM HostMode +parseHostMode = eitherReader $ textToHostMode . T.pack parseServerPort :: ReadM (Maybe String) parseServerPort = eitherReader $ parseAll serverPortP . B.pack @@ -327,13 +404,23 @@ parseLogLevel = eitherReader $ \case "important" -> Right CLLImportant _ -> Left "Invalid log level" +parseChatCmdLog :: ReadM ChatCmdLog +parseChatCmdLog = eitherReader $ \case + "all" -> Right CCLAll + "messages" -> Right CCLMessages + "none" -> Right CCLNone + _ -> Left "Invalid chat command log level" + getChatOpts :: FilePath -> FilePath -> IO ChatOpts -getChatOpts appDir defaultDbFileName = +getChatOpts appDir defaultDbName = execParser $ info - (helper <*> versionOption <*> chatOptsP appDir defaultDbFileName) + (helper <*> versionOption <*> chatOptsP appDir defaultDbName) (header versionStr <> fullDesc <> progDesc "Start chat with DB_FILE file and use SERVER as SMP server") where versionStr = versionString versionNumber versionOption = infoOption versionAndUpdate (long "version" <> short 'v' <> help "Show version") versionAndUpdate = versionStr <> "\n" <> updateStr + +printDbOpts :: CoreChatOpts -> IO () +printDbOpts opts = putStrLn $ "db: " <> dbString (dbOptions opts) diff --git a/src/Simplex/Chat/Options/DB.hs b/src/Simplex/Chat/Options/DB.hs new file mode 100644 index 0000000000..7e20e93e88 --- /dev/null +++ b/src/Simplex/Chat/Options/DB.hs @@ -0,0 +1,25 @@ +{-# LANGUAGE CPP #-} + +module Simplex.Chat.Options.DB + +#if defined(dbPostgres) + ( module Simplex.Chat.Options.Postgres, + FromField (..), + ToField (..), + ) + where +import Simplex.Chat.Options.Postgres +import Database.PostgreSQL.Simple.FromField (FromField (..)) +import Database.PostgreSQL.Simple.ToField (ToField (..)) + +#else + ( module Simplex.Chat.Options.SQLite, + FromField (..), + ToField (..), + ) + where +import Simplex.Chat.Options.SQLite +import Database.SQLite.Simple.FromField (FromField (..)) +import Database.SQLite.Simple.ToField (ToField (..)) + +#endif diff --git a/src/Simplex/Chat/Options/Postgres.hs b/src/Simplex/Chat/Options/Postgres.hs new file mode 100644 index 0000000000..f7c429e93e --- /dev/null +++ b/src/Simplex/Chat/Options/Postgres.hs @@ -0,0 +1,89 @@ +{-# LANGUAGE ApplicativeDo #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} + +module Simplex.Chat.Options.Postgres where + +import qualified Data.ByteString.Char8 as B +import Foreign.C.String +import Options.Applicative +import Numeric.Natural (Natural) +import Simplex.Messaging.Agent.Store.Interface (DBOpts (..)) + +data ChatDbOpts = ChatDbOpts + { dbConnstr :: String, + dbSchemaPrefix :: String, + dbPoolSize :: Natural, + dbCreateSchema :: Bool + } + +chatDbOptsP :: FilePath -> String -> Parser ChatDbOpts +chatDbOptsP _appDir defaultDbName = do + dbConnstr <- + strOption + ( long "database" + <> short 'd' + <> metavar "DB_CONN" + <> help "Database connection string" + <> value ("postgresql://simplex@/" <> defaultDbName) + <> showDefault + ) + dbSchemaPrefix <- + strOption + ( long "schema-prefix" + <> metavar "DB_SCHEMA_PREFIX" + <> help "Database schema prefix" + <> value "simplex_v1" + <> showDefault + ) + dbPoolSize <- + option + auto + ( long "pool-size" + <> metavar "DB_POOL_SIZE" + <> help "Database connection pool size" + <> value 1 + <> showDefault + ) + dbCreateSchema <- + switch + ( long "create-schema" + <> help "Create database schema when it does not exist" + ) + pure ChatDbOpts {dbConnstr, dbSchemaPrefix, dbPoolSize, dbCreateSchema} + +dbString :: ChatDbOpts -> String +dbString ChatDbOpts {dbConnstr} = dbConnstr + +toDBOpts :: ChatDbOpts -> String -> Bool -> DBOpts +toDBOpts ChatDbOpts {dbConnstr, dbSchemaPrefix, dbPoolSize, dbCreateSchema} dbSuffix _keepKey = + DBOpts + { connstr = B.pack dbConnstr, + schema = B.pack $ if null dbSchemaPrefix then "simplex_v1" <> dbSuffix else dbSchemaPrefix <> dbSuffix, + poolSize = dbPoolSize, + createSchema = dbCreateSchema + } + +chatSuffix :: String +chatSuffix = "_chat_schema" + +agentSuffix :: String +agentSuffix = "_agent_schema" + +mobileDbOpts :: CString -> CString -> IO ChatDbOpts +mobileDbOpts schemaPrefix connstr = do + dbSchemaPrefix <- peekCString schemaPrefix + dbConnstr <- peekCString connstr + pure $ + ChatDbOpts + { dbConnstr, + dbSchemaPrefix, + dbPoolSize = 1, + dbCreateSchema = True + } + +removeDbKey :: ChatDbOpts -> ChatDbOpts +removeDbKey = id + +errorDbStr :: DBOpts -> String +errorDbStr DBOpts {schema} = B.unpack schema diff --git a/src/Simplex/Chat/Options/SQLite.hs b/src/Simplex/Chat/Options/SQLite.hs new file mode 100644 index 0000000000..0507076613 --- /dev/null +++ b/src/Simplex/Chat/Options/SQLite.hs @@ -0,0 +1,94 @@ +{-# LANGUAGE ApplicativeDo #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} + +module Simplex.Chat.Options.SQLite where + +import Data.ByteArray (ScrubbedBytes) +import qualified Data.ByteArray as BA +import qualified Data.ByteString.Char8 as B +import Foreign.C.String +import Options.Applicative +import Simplex.Messaging.Agent.Store.Interface (DBOpts (..)) +import Simplex.Messaging.Agent.Store.SQLite.DB (TrackQueries (..)) +import System.FilePath (combine) + +data ChatDbOpts = ChatDbOpts + { dbFilePrefix :: String, + dbKey :: ScrubbedBytes, + trackQueries :: TrackQueries, + vacuumOnMigration :: Bool + } + +chatDbOptsP :: FilePath -> FilePath -> Parser ChatDbOpts +chatDbOptsP appDir defaultDbName = do + dbFilePrefix <- + strOption + ( long "database" + <> short 'd' + <> metavar "DB_FILE" + <> help "Path prefix to chat and agent database files" + <> value (combine appDir defaultDbName) + <> showDefault + ) + dbKey <- + strOption + ( long "key" + <> short 'k' + <> metavar "KEY" + <> help "Database encryption key/pass-phrase" + <> value "" + ) + disableVacuum <- + switch + ( long "disable-vacuum" + <> help "Do not vacuum database after migrations" + ) + pure + ChatDbOpts + { dbFilePrefix, + dbKey, + trackQueries = TQSlow 5000, -- 5ms + vacuumOnMigration = not disableVacuum + } + +dbString :: ChatDbOpts -> String +dbString ChatDbOpts {dbFilePrefix} = dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db" + +toDBOpts :: ChatDbOpts -> String -> Bool -> DBOpts +toDBOpts ChatDbOpts {dbFilePrefix, dbKey, trackQueries, vacuumOnMigration} dbSuffix keepKey = do + DBOpts + { dbFilePath = dbFilePrefix <> dbSuffix, + dbKey, + keepKey, + track = trackQueries, + vacuum = vacuumOnMigration + } + +chatSuffix :: String +chatSuffix = "_chat.db" + +agentSuffix :: String +agentSuffix = "_agent.db" + +mobileDbOpts :: CString -> CString -> IO ChatDbOpts +mobileDbOpts fp key = do + dbFilePrefix <- peekCString fp + dbKey <- BA.convert <$> B.packCString key + pure $ + ChatDbOpts + { dbFilePrefix, + dbKey, + trackQueries = TQSlow 5000, -- 5ms + vacuumOnMigration = True + } + +-- used to create new chat controller, +-- at that point database is already opened, and the key in options is not used +removeDbKey :: ChatDbOpts -> ChatDbOpts +removeDbKey opts = opts {dbKey = ""} :: ChatDbOpts + +errorDbStr :: DBOpts -> String +errorDbStr DBOpts {dbFilePath} = dbFilePath diff --git a/src/Simplex/Chat/ProfileGenerator.hs b/src/Simplex/Chat/ProfileGenerator.hs index 95f5f16207..5b6c9222cc 100644 --- a/src/Simplex/Chat/ProfileGenerator.hs +++ b/src/Simplex/Chat/ProfileGenerator.hs @@ -1492,7 +1492,6 @@ adjectives = "Recommendable", "Rectangular", "Recuperative", - "Red", "Refined", "Reflecting", "Reflective", @@ -2929,7 +2928,6 @@ nouns = "Sister", "Size", "Skill", - "Skin", "Skipper", "Sleek", "Slick", diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index a0df9f9865..2281c1aefa 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -1,12 +1,15 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} @@ -29,10 +32,13 @@ import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.ByteString.Internal (c2w, w2c) -import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Lazy.Char8 as LB -import Data.Int (Int64) -import Data.Maybe (fromMaybe) +import Data.Either (fromRight) +import Data.List.NonEmpty (NonEmpty (..)) +import qualified Data.List.NonEmpty as L +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as M +import Data.Maybe (fromMaybe, mapMaybe) import Data.String import Data.Text (Text) import qualified Data.Text as T @@ -41,46 +47,100 @@ import Data.Time.Clock (UTCTime) import Data.Type.Equality import Data.Typeable (Typeable) import Data.Word (Word32) -import Database.SQLite.Simple.FromField (FromField (..)) -import Database.SQLite.Simple.ToField (ToField (..)) import Simplex.Chat.Call +import Simplex.Chat.Options.DB (FromField (..), ToField (..)) import Simplex.Chat.Types -import Simplex.Chat.Types.Util +import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared +import Simplex.Messaging.Agent.Protocol (VersionSMPA, pqdrSMPAgentVersion) +import Simplex.Messaging.Agent.Store.DB (fromTextField_) +import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Compression (Compressed, compress1, decompress1) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, fstToLower, parseAll, sumTypeJSON, taggedObjectJSON) -import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>)) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fstToLower, parseAll, sumTypeJSON, taggedObjectJSON) +import Simplex.Messaging.Protocol (MsgBody) +import Simplex.Messaging.Util (decodeJSON, eitherToMaybe, encodeJSON, safeDecodeUtf8, (<$?>)) import Simplex.Messaging.Version hiding (version) +-- Chat version history: +-- 1 - support chat versions in connections (9/1/2023) +-- 2 - create contacts for group members only via x.grp.direct.inv (9/16/2023) +-- 3 - faster joining via group links without creating contact (10/30/2023) +-- 4 - group message forwarding (11/18/2023) +-- 5 - batch sending messages (12/23/2023) +-- 6 - send group welcome message after history (12/29/2023) +-- 7 - update member profiles (1/15/2024) +-- 8 - compress messages and PQ e2e encryption (2024-03-08) +-- 9 - batch sending in direct connections (2024-07-24) +-- 10 - business chats (2024-11-29) +-- 11 - fix profile update in business chats (2024-12-05) +-- 12 - support sending and receiving content reports (2025-01-03) +-- 14 - support sending and receiving group join rejection (2025-02-24) + -- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig. -- This indirection is needed for backward/forward compatibility testing. -- Testing with real app versions is still needed, as tests use the current code with different version ranges, not the old code. -currentChatVersion :: Version -currentChatVersion = 5 +currentChatVersion :: VersionChat +currentChatVersion = VersionChat 14 -- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above) -supportedChatVRange :: VersionRange -supportedChatVRange = mkVersionRange 1 currentChatVersion +supportedChatVRange :: VersionRangeChat +supportedChatVRange = mkVersionRange initialChatVersion currentChatVersion +{-# INLINE supportedChatVRange #-} --- version range that supports skipping establishing direct connections in a group -groupNoDirectVRange :: VersionRange -groupNoDirectVRange = mkVersionRange 2 currentChatVersion - --- version range that supports establishing direct connection via x.grp.direct.inv with a group member -xGrpDirectInvVRange :: VersionRange -xGrpDirectInvVRange = mkVersionRange 2 currentChatVersion +-- version range that supports skipping establishing direct connections in a group and establishing direct connection via x.grp.direct.inv +groupDirectInvVersion :: VersionChat +groupDirectInvVersion = VersionChat 2 -- version range that supports joining group via group link without creating direct contact -groupLinkNoContactVRange :: VersionRange -groupLinkNoContactVRange = mkVersionRange 3 currentChatVersion +groupFastLinkJoinVersion :: VersionChat +groupFastLinkJoinVersion = VersionChat 3 -- version range that supports group forwarding -groupForwardVRange :: VersionRange -groupForwardVRange = mkVersionRange 4 currentChatVersion +groupForwardVersion :: VersionChat +groupForwardVersion = VersionChat 4 -- version range that supports batch sending in groups -batchSendVRange :: VersionRange -batchSendVRange = mkVersionRange 5 currentChatVersion +batchSendVersion :: VersionChat +batchSendVersion = VersionChat 5 + +-- version range that supports sending group welcome message in group history +groupHistoryIncludeWelcomeVersion :: VersionChat +groupHistoryIncludeWelcomeVersion = VersionChat 6 + +-- version range that supports sending member profile updates to groups +memberProfileUpdateVersion :: VersionChat +memberProfileUpdateVersion = VersionChat 7 + +-- version range that supports compressing messages and PQ e2e encryption +pqEncryptionCompressionVersion :: VersionChat +pqEncryptionCompressionVersion = VersionChat 8 + +-- version range that supports batch sending in direct connections, and forwarding batched messages in groups +batchSend2Version :: VersionChat +batchSend2Version = VersionChat 9 + +-- supports differentiating business chats when joining contact addresses +businessChatsVersion :: VersionChat +businessChatsVersion = VersionChat 10 + +-- support updating preferences in business chats (XGrpPrefs message) +businessChatPrefsVersion :: VersionChat +businessChatPrefsVersion = VersionChat 11 + +-- support sending and receiving content reports (MCReport message content) +contentReportsVersion :: VersionChat +contentReportsVersion = VersionChat 12 + +-- support sending and receiving group join rejection (XGrpLinkReject) +groupJoinRejectVersion :: VersionChat +groupJoinRejectVersion = VersionChat 14 + +agentToChatVersion :: VersionSMPA -> VersionChat +agentToChatVersion v + | v < pqdrSMPAgentVersion = initialChatVersion + | otherwise = pqEncryptionCompressionVersion data ConnectionEntity = RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact} @@ -92,6 +152,17 @@ data ConnectionEntity $(JQ.deriveJSON (sumTypeJSON fstToLower) ''ConnectionEntity) +connEntityInfo :: ConnectionEntity -> String +connEntityInfo = \case + RcvDirectMsgConnection c ct_ -> ctInfo ct_ <> ", status: " <> show (connStatus c) + RcvGroupMsgConnection c g m -> mInfo g m <> ", status: " <> show (connStatus c) + SndFileConnection c _ft -> "snd file, status: " <> show (connStatus c) + RcvFileConnection c _ft -> "rcv file, status: " <> show (connStatus c) + UserContactConnection c _uc -> "user address, status: " <> show (connStatus c) + where + ctInfo = maybe "connection" $ \Contact {contactId} -> "contact " <> show contactId + mInfo GroupInfo {groupId} GroupMember {groupMemberId} = "group " <> show groupId <> ", member " <> show groupMemberId + updateEntityConnStatus :: ConnectionEntity -> ConnStatus -> ConnectionEntity updateEntityConnStatus connEntity connStatus = case connEntity of RcvDirectMsgConnection c ct_ -> RcvDirectMsgConnection (st c) ((\ct -> (ct :: Contact) {activeConn = Just $ st c}) <$> ct_) @@ -156,10 +227,9 @@ instance StrEncoding AppMessageBinary where newtype SharedMsgId = SharedMsgId ByteString deriving (Eq, Show) + deriving newtype (FromField) -instance FromField SharedMsgId where fromField f = SharedMsgId <$> fromField f - -instance ToField SharedMsgId where toField (SharedMsgId m) = toField m +instance ToField SharedMsgId where toField (SharedMsgId m) = toField $ DB.Binary m instance StrEncoding SharedMsgId where strEncode (SharedMsgId m) = strEncode m @@ -191,6 +261,9 @@ data LinkPreview = LinkPreview {uri :: Text, title :: Text, description :: Text, data LinkContent = LCPage | LCImage | LCVideo {duration :: Maybe Int} | LCUnknown {tag :: Text, json :: J.Object} deriving (Eq, Show) +data ReportReason = RRSpam | RRContent | RRCommunity | RRProfile | RROther | RRUnknown Text + deriving (Eq, Show) + $(pure []) instance FromJSON LinkContent where @@ -210,8 +283,32 @@ instance ToJSON LinkContent where $(JQ.deriveJSON defaultJSON ''LinkPreview) +instance StrEncoding ReportReason where + strEncode = \case + RRSpam -> "spam" + RRContent -> "content" + RRCommunity -> "community" + RRProfile -> "profile" + RROther -> "other" + RRUnknown t -> encodeUtf8 t + strP = + A.takeTill (== ' ') >>= \case + "spam" -> pure RRSpam + "content" -> pure RRContent + "community" -> pure RRCommunity + "profile" -> pure RRProfile + "other" -> pure RROther + t -> pure $ RRUnknown $ safeDecodeUtf8 t + +instance FromJSON ReportReason where + parseJSON = strParseJSON "ReportReason" + +instance ToJSON ReportReason where + toJSON = strToJSON + toEncoding = strToJEncoding + data ChatMessage e = ChatMessage - { chatVRange :: VersionRange, + { chatVRange :: VersionRangeChat, msgId :: Maybe SharedMsgId, chatMsgEvent :: ChatMsgEvent e } @@ -222,7 +319,7 @@ data AChatMessage = forall e. MsgEncodingI e => ACMsg (SMsgEncoding e) (ChatMess data ChatMsgEvent (e :: MsgEncoding) where XMsgNew :: MsgContainer -> ChatMsgEvent 'Json XMsgFileDescr :: {msgId :: SharedMsgId, fileDescr :: FileDescr} -> ChatMsgEvent 'Json - XMsgUpdate :: {msgId :: SharedMsgId, content :: MsgContent, ttl :: Maybe Int, live :: Maybe Bool} -> ChatMsgEvent 'Json + XMsgUpdate :: {msgId :: SharedMsgId, content :: MsgContent, mentions :: Map MemberName MsgMention, ttl :: Maybe Int, live :: Maybe Bool} -> ChatMsgEvent 'Json XMsgDel :: SharedMsgId -> Maybe MemberId -> ChatMsgEvent 'Json XMsgDeleted :: ChatMsgEvent 'Json XMsgReact :: {msgId :: SharedMsgId, memberId :: Maybe MemberId, reaction :: MsgReaction, add :: Bool} -> ChatMsgEvent 'Json @@ -236,19 +333,23 @@ data ChatMsgEvent (e :: MsgEncoding) where XGrpInv :: GroupInvitation -> ChatMsgEvent 'Json XGrpAcpt :: MemberId -> ChatMsgEvent 'Json XGrpLinkInv :: GroupLinkInvitation -> ChatMsgEvent 'Json + XGrpLinkReject :: GroupLinkRejection -> ChatMsgEvent 'Json XGrpLinkMem :: Profile -> ChatMsgEvent 'Json + XGrpLinkAcpt :: GroupMemberRole -> ChatMsgEvent 'Json XGrpMemNew :: MemberInfo -> ChatMsgEvent 'Json - XGrpMemIntro :: MemberInfo -> ChatMsgEvent 'Json + XGrpMemIntro :: MemberInfo -> Maybe MemberRestrictions -> ChatMsgEvent 'Json XGrpMemInv :: MemberId -> IntroInvitation -> ChatMsgEvent 'Json XGrpMemFwd :: MemberInfo -> IntroInvitation -> ChatMsgEvent 'Json XGrpMemInfo :: MemberId -> Profile -> ChatMsgEvent 'Json XGrpMemRole :: MemberId -> GroupMemberRole -> ChatMsgEvent 'Json + XGrpMemRestrict :: MemberId -> MemberRestrictions -> ChatMsgEvent 'Json XGrpMemCon :: MemberId -> ChatMsgEvent 'Json XGrpMemConAll :: MemberId -> ChatMsgEvent 'Json -- TODO not implemented - XGrpMemDel :: MemberId -> ChatMsgEvent 'Json + XGrpMemDel :: MemberId -> Bool -> ChatMsgEvent 'Json XGrpLeave :: ChatMsgEvent 'Json XGrpDel :: ChatMsgEvent 'Json XGrpInfo :: GroupProfile -> ChatMsgEvent 'Json + XGrpPrefs :: GroupPreferences -> ChatMsgEvent 'Json XGrpDirectInv :: ConnReqInvitation -> Maybe MsgContent -> ChatMsgEvent 'Json XGrpMsgForward :: MemberId -> ChatMessage 'Json -> UTCTime -> ChatMsgEvent 'Json XInfoProbe :: Probe -> ChatMsgEvent 'Json @@ -284,10 +385,12 @@ isForwardedGroupMsg ev = case ev of XInfo _ -> True XGrpMemNew _ -> True XGrpMemRole {} -> True - XGrpMemDel _ -> True -- TODO there should be a special logic when deleting host member (e.g., host forwards it before deleting connections) + XGrpMemRestrict {} -> True + XGrpMemDel {} -> True -- TODO there should be a special logic when deleting host member (e.g., host forwards it before deleting connections) XGrpLeave -> True XGrpDel -> True -- TODO there should be a special logic - host should forward before deleting connections XGrpInfo _ -> True + XGrpPrefs _ -> True _ -> False forwardedGroupMsg :: forall e. MsgEncodingI e => ChatMessage e -> Maybe (ChatMessage 'Json) @@ -295,12 +398,49 @@ forwardedGroupMsg msg@ChatMessage {chatMsgEvent} = case encoding @e of SJson | isForwardedGroupMsg chatMsgEvent -> Just msg _ -> Nothing +-- applied after checking forwardedGroupMsg and building list of group members to forward to, see Chat; +-- +-- this filters out members if any of forwarded events in batch is an XGrpMemRestrict event referring to them, +-- but practically XGrpMemRestrict is not batched with other events so it wouldn't prevent forwarding of other events +-- to these members; +-- +-- same for reports (MCReport) - they are not batched with other events, so we can safely filter out +-- members with role less than moderator when forwarding +forwardedToGroupMembers :: forall e. MsgEncodingI e => [GroupMember] -> NonEmpty (ChatMessage e) -> [GroupMember] +forwardedToGroupMembers ms forwardedMsgs = + filter forwardToMember ms + where + forwardToMember GroupMember {memberId, memberRole} = + (memberId `notElem` restrictMemberIds) + && (not hasReport || memberRole >= GRModerator) + restrictMemberIds = mapMaybe restrictMemberId $ L.toList forwardedMsgs + restrictMemberId ChatMessage {chatMsgEvent} = case encoding @e of + SJson -> case chatMsgEvent of + XGrpMemRestrict mId _ -> Just mId + _ -> Nothing + _ -> Nothing + hasReport = any isReportEvent forwardedMsgs + isReportEvent ChatMessage {chatMsgEvent} = case encoding @e of + SJson -> case chatMsgEvent of + XMsgNew mc -> case mcExtMsgContent mc of + ExtMsgContent {content = MCReport {}} -> True + _ -> False + _ -> False + _ -> False + data MsgReaction = MREmoji {emoji :: MREmojiChar} | MRUnknown {tag :: Text, json :: J.Object} deriving (Eq, Show) emojiTag :: IsString a => a emojiTag = "emoji" +knownReaction :: MsgReaction -> Either String MsgReaction +knownReaction = \case + r@MREmoji {} -> Right r + MRUnknown {} -> Left "unknown MsgReaction" + +-- parseJSON for MsgReaction parses unknown emoji reactions as MRUnknown with type "emoji", +-- allowing to add new emojis in a backwards compatible way - UI shows them as ? instance FromJSON MsgReaction where parseJSON (J.Object v) = do tag <- v .: "type" @@ -336,7 +476,7 @@ instance FromJSON MREmojiChar where mrEmojiChar :: Char -> Either String MREmojiChar mrEmojiChar c - | c `elem` ("👍👎😀😢❤️🚀" :: String) = Right $ MREmojiChar c + | c `elem` ("👍👎😀😂😢❤️🚀✅" :: String) = Right $ MREmojiChar c | otherwise = Left "bad emoji" data FileChunk = FileChunk {chunkNo :: Integer, chunkBytes :: ByteString} | FileChunkCancel @@ -377,8 +517,8 @@ cmToQuotedMsg = \case ACME _ (XMsgNew (MCQuote quotedMsg _)) -> Just quotedMsg _ -> Nothing -data MsgContentTag = MCText_ | MCLink_ | MCImage_ | MCVideo_ | MCVoice_ | MCFile_ | MCUnknown_ Text - deriving (Eq) +data MsgContentTag = MCText_ | MCLink_ | MCImage_ | MCVideo_ | MCVoice_ | MCFile_ | MCReport_ | MCUnknown_ Text + deriving (Eq, Show) instance StrEncoding MsgContentTag where strEncode = \case @@ -388,6 +528,7 @@ instance StrEncoding MsgContentTag where MCVideo_ -> "video" MCFile_ -> "file" MCVoice_ -> "voice" + MCReport_ -> "report" MCUnknown_ t -> encodeUtf8 t strDecode = \case "text" -> Right MCText_ @@ -396,6 +537,7 @@ instance StrEncoding MsgContentTag where "video" -> Right MCVideo_ "voice" -> Right MCVoice_ "file" -> Right MCFile_ + "report" -> Right MCReport_ t -> Right . MCUnknown_ $ safeDecodeUtf8 t strP = strDecode <$?> A.takeTill (== ' ') @@ -406,9 +548,12 @@ instance ToJSON MsgContentTag where toJSON = strToJSON toEncoding = strToJEncoding +instance ToField MsgContentTag where toField = toField . safeDecodeUtf8 . strEncode + data MsgContainer = MCSimple ExtMsgContent | MCQuote QuotedMsg ExtMsgContent + | MCComment MsgRef ExtMsgContent | MCForward ExtMsgContent deriving (Eq, Show) @@ -416,20 +561,22 @@ mcExtMsgContent :: MsgContainer -> ExtMsgContent mcExtMsgContent = \case MCSimple c -> c MCQuote _ c -> c + MCComment _ c -> c MCForward c -> c -isQuote :: MsgContainer -> Bool -isQuote = \case - MCQuote {} -> True +isMCForward :: MsgContainer -> Bool +isMCForward = \case + MCForward _ -> True _ -> False data MsgContent - = MCText Text + = MCText {text :: Text} | MCLink {text :: Text, preview :: LinkPreview} | MCImage {text :: Text, image :: ImageData} | MCVideo {text :: Text, image :: ImageData, duration :: Int} | MCVoice {text :: Text, duration :: Int} - | MCFile Text + | MCFile {text :: Text} + | MCReport {text :: Text, reason :: ReportReason} | MCUnknown {tag :: Text, text :: Text, json :: J.Object} deriving (Eq, Show) @@ -444,11 +591,12 @@ msgContentText = \case where msg = "voice message " <> durationText duration MCFile t -> t + MCReport {text, reason} -> + if T.null text then msg else msg <> ": " <> text + where + msg = "report " <> safeDecodeUtf8 (strEncode reason) MCUnknown {text} -> text -toMCText :: MsgContent -> MsgContent -toMCText = MCText . msgContentText - durationText :: Int -> Text durationText duration = let (mins, secs) = duration `divMod` 60 in T.pack $ "(" <> with0 mins <> ":" <> with0 secs <> ")" @@ -458,22 +606,21 @@ durationText duration = | otherwise = show n msgContentHasText :: MsgContent -> Bool -msgContentHasText = \case - MCText t -> hasText t - MCLink {text} -> hasText text - MCImage {text} -> hasText text - MCVideo {text} -> hasText text - MCVoice {text} -> hasText text - MCFile t -> hasText t - MCUnknown {text} -> hasText text - where - hasText = not . T.null +msgContentHasText = + not . T.null . \case + MCVoice {text} -> text + mc -> msgContentText mc isVoice :: MsgContent -> Bool isVoice = \case MCVoice {} -> True _ -> False +isReport :: MsgContent -> Bool +isReport = \case + MCReport {} -> True + _ -> False + msgContentTag :: MsgContent -> MsgContentTag msgContentTag = \case MCText _ -> MCText_ @@ -482,29 +629,56 @@ msgContentTag = \case MCVideo {} -> MCVideo_ MCVoice {} -> MCVoice_ MCFile {} -> MCFile_ + MCReport {} -> MCReport_ MCUnknown {tag} -> MCUnknown_ tag -data ExtMsgContent = ExtMsgContent {content :: MsgContent, file :: Maybe FileInvitation, ttl :: Maybe Int, live :: Maybe Bool} +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 + } deriving (Eq, Show) +data MsgMention = MsgMention {memberId :: MemberId} + deriving (Eq, Show) + +$(JQ.deriveJSON defaultJSON ''MsgMention) + $(JQ.deriveJSON defaultJSON ''QuotedMsg) -- this limit reserves space for metadata in forwarded messages --- 15780 (limit used for fileChunkSize) - 161 (x.grp.msg.forward overhead) = 15619, round to 15610 -maxChatMsgSize :: Int64 -maxChatMsgSize = 15610 +-- 15780 (limit used for fileChunkSize) - 161 (x.grp.msg.forward overhead) = 15619, - 16 for block encryption ("rounded" to 15602) +maxEncodedMsgLength :: Int +maxEncodedMsgLength = 15602 -data EncodedChatMessage = ECMEncoded L.ByteString | ECMLarge +-- maxEncodedMsgLength - 2222, see e2eEncUserMsgLength in agent +maxCompressedMsgLength :: Int +maxCompressedMsgLength = 13380 -encodeChatMessage :: MsgEncodingI e => ChatMessage e -> EncodedChatMessage -encodeChatMessage msg = do +-- maxEncodedMsgLength - delta between MSG and INFO + 100 (returned for forward overhead) +-- delta between MSG and INFO = e2eEncUserMsgLength (no PQ) - e2eEncConnInfoLength (no PQ) = 1008 +maxEncodedInfoLength :: Int +maxEncodedInfoLength = 14694 + +maxCompressedInfoLength :: Int +maxCompressedInfoLength = 10968 -- maxEncodedInfoLength - 3726, see e2eEncConnInfoLength in agent + +data EncodedChatMessage = ECMEncoded ByteString | ECMLarge + +encodeChatMessage :: MsgEncodingI e => Int -> ChatMessage e -> EncodedChatMessage +encodeChatMessage maxSize msg = do case chatToAppMessage msg of AMJson m -> do - let body = J.encode m - if LB.length body > maxChatMsgSize + let body = LB.toStrict $ J.encode m + if B.length body > maxSize then ECMLarge else ECMEncoded body - AMBinary m -> ECMEncoded . LB.fromStrict $ strEncode m + AMBinary m -> ECMEncoded $ strEncode m parseChatMessages :: ByteString -> [Either String AChatMessage] parseChatMessages "" = [Left "empty string"] @@ -513,21 +687,43 @@ parseChatMessages s = case B.head s of '[' -> case J.eitherDecodeStrict' s of Right v -> map parseItem v Left e -> [Left e] + 'X' -> decodeCompressed (B.drop 1 s) _ -> [ACMsg SBinary <$> (appBinaryToCM =<< strDecode s)] where parseItem :: J.Value -> Either String AChatMessage parseItem v = ACMsg SJson <$> JT.parseEither parseJSON v + decodeCompressed :: ByteString -> [Either String AChatMessage] + decodeCompressed s' = case smpDecode s' of + Left e -> [Left e] + Right (compressed :: L.NonEmpty Compressed) -> concatMap (either (pure . Left) parseChatMessages . decompress1) compressed + +compressedBatchMsgBody_ :: MsgBody -> ByteString +compressedBatchMsgBody_ = markCompressedBatch . smpEncode . (L.:| []) . compress1 + +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 = ExtMsgContent <$> v .: "content" <*> v .:? "file" <*> v .:? "ttl" <*> v .:? "live" + mc = do + content <- v .: "content" + file <- v .:? "file" + ttl <- v .:? "ttl" + live <- v .:? "live" + mentions <- fromMaybe M.empty <$> (v .:? "mentions") + pure ExtMsgContent {content, mentions, file, ttl, live} extMsgContent :: MsgContent -> Maybe FileInvitation -> ExtMsgContent -extMsgContent mc file = ExtMsgContent mc file Nothing Nothing +extMsgContent mc file = ExtMsgContent mc M.empty file Nothing Nothing justTrue :: Bool -> Maybe Bool justTrue True = Just True @@ -555,6 +751,10 @@ instance FromJSON MsgContent where duration <- v .: "duration" pure MCVoice {text, duration} MCFile_ -> MCFile <$> v .: "text" + MCReport_ -> do + text <- v .: "text" + reason <- v .: "reason" + pure MCReport {text, reason} MCUnknown_ tag -> do text <- fromMaybe unknownMsgType <$> v .:? "text" pure MCUnknown {tag, text, json = v} @@ -567,11 +767,17 @@ 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 c file ttl live) = ("file" .=? file) $ ("ttl" .=? ttl) $ ("live" .=? live) ["content" .= c] + msgContent ExtMsgContent {content, mentions, file, ttl, live} = + ("file" .=? file) $ ("ttl" .=? ttl) $ ("live" .=? live) $ ("mentions" .=? nonEmptyMap mentions) ["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 @@ -582,6 +788,7 @@ instance ToJSON MsgContent where 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] toEncoding = \case MCUnknown {json} -> JE.value $ J.Object json MCText t -> J.pairs $ "type" .= MCText_ <> "text" .= t @@ -590,6 +797,7 @@ instance ToJSON MsgContent where 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 instance ToField MsgContent where toField = toField . encodeJSON @@ -616,19 +824,23 @@ data CMEventTag (e :: MsgEncoding) where XGrpInv_ :: CMEventTag 'Json XGrpAcpt_ :: CMEventTag 'Json XGrpLinkInv_ :: CMEventTag 'Json + XGrpLinkReject_ :: CMEventTag 'Json XGrpLinkMem_ :: CMEventTag 'Json + XGrpLinkAcpt_ :: CMEventTag 'Json XGrpMemNew_ :: CMEventTag 'Json XGrpMemIntro_ :: CMEventTag 'Json XGrpMemInv_ :: CMEventTag 'Json XGrpMemFwd_ :: CMEventTag 'Json XGrpMemInfo_ :: CMEventTag 'Json XGrpMemRole_ :: CMEventTag 'Json + XGrpMemRestrict_ :: CMEventTag 'Json XGrpMemCon_ :: CMEventTag 'Json XGrpMemConAll_ :: CMEventTag 'Json XGrpMemDel_ :: CMEventTag 'Json XGrpLeave_ :: CMEventTag 'Json XGrpDel_ :: CMEventTag 'Json XGrpInfo_ :: CMEventTag 'Json + XGrpPrefs_ :: CMEventTag 'Json XGrpDirectInv_ :: CMEventTag 'Json XGrpMsgForward_ :: CMEventTag 'Json XInfoProbe_ :: CMEventTag 'Json @@ -665,19 +877,23 @@ instance MsgEncodingI e => StrEncoding (CMEventTag e) where XGrpInv_ -> "x.grp.inv" XGrpAcpt_ -> "x.grp.acpt" XGrpLinkInv_ -> "x.grp.link.inv" + XGrpLinkReject_ -> "x.grp.link.reject" XGrpLinkMem_ -> "x.grp.link.mem" + XGrpLinkAcpt_ -> "x.grp.link.acpt" XGrpMemNew_ -> "x.grp.mem.new" XGrpMemIntro_ -> "x.grp.mem.intro" XGrpMemInv_ -> "x.grp.mem.inv" XGrpMemFwd_ -> "x.grp.mem.fwd" XGrpMemInfo_ -> "x.grp.mem.info" XGrpMemRole_ -> "x.grp.mem.role" + XGrpMemRestrict_ -> "x.grp.mem.restrict" XGrpMemCon_ -> "x.grp.mem.con" XGrpMemConAll_ -> "x.grp.mem.con.all" XGrpMemDel_ -> "x.grp.mem.del" XGrpLeave_ -> "x.grp.leave" XGrpDel_ -> "x.grp.del" XGrpInfo_ -> "x.grp.info" + XGrpPrefs_ -> "x.grp.prefs" XGrpDirectInv_ -> "x.grp.direct.inv" XGrpMsgForward_ -> "x.grp.msg.forward" XInfoProbe_ -> "x.info.probe" @@ -715,19 +931,23 @@ instance StrEncoding ACMEventTag where "x.grp.inv" -> XGrpInv_ "x.grp.acpt" -> XGrpAcpt_ "x.grp.link.inv" -> XGrpLinkInv_ + "x.grp.link.reject" -> XGrpLinkReject_ "x.grp.link.mem" -> XGrpLinkMem_ + "x.grp.link.acpt" -> XGrpLinkAcpt_ "x.grp.mem.new" -> XGrpMemNew_ "x.grp.mem.intro" -> XGrpMemIntro_ "x.grp.mem.inv" -> XGrpMemInv_ "x.grp.mem.fwd" -> XGrpMemFwd_ "x.grp.mem.info" -> XGrpMemInfo_ "x.grp.mem.role" -> XGrpMemRole_ + "x.grp.mem.restrict" -> XGrpMemRestrict_ "x.grp.mem.con" -> XGrpMemCon_ "x.grp.mem.con.all" -> XGrpMemConAll_ "x.grp.mem.del" -> XGrpMemDel_ "x.grp.leave" -> XGrpLeave_ "x.grp.del" -> XGrpDel_ "x.grp.info" -> XGrpInfo_ + "x.grp.prefs" -> XGrpPrefs_ "x.grp.direct.inv" -> XGrpDirectInv_ "x.grp.msg.forward" -> XGrpMsgForward_ "x.info.probe" -> XInfoProbe_ @@ -761,19 +981,23 @@ toCMEventTag msg = case msg of XGrpInv _ -> XGrpInv_ XGrpAcpt _ -> XGrpAcpt_ XGrpLinkInv _ -> XGrpLinkInv_ + XGrpLinkReject _ -> XGrpLinkReject_ XGrpLinkMem _ -> XGrpLinkMem_ + XGrpLinkAcpt _ -> XGrpLinkAcpt_ XGrpMemNew _ -> XGrpMemNew_ - XGrpMemIntro _ -> XGrpMemIntro_ + XGrpMemIntro _ _ -> XGrpMemIntro_ XGrpMemInv _ _ -> XGrpMemInv_ XGrpMemFwd _ _ -> XGrpMemFwd_ XGrpMemInfo _ _ -> XGrpMemInfo_ XGrpMemRole _ _ -> XGrpMemRole_ + XGrpMemRestrict _ _ -> XGrpMemRestrict_ XGrpMemCon _ -> XGrpMemCon_ XGrpMemConAll _ -> XGrpMemConAll_ - XGrpMemDel _ -> XGrpMemDel_ + XGrpMemDel {} -> XGrpMemDel_ XGrpLeave -> XGrpLeave_ XGrpDel -> XGrpDel_ XGrpInfo _ -> XGrpInfo_ + XGrpPrefs _ -> XGrpPrefs_ XGrpDirectInv _ _ -> XGrpDirectInv_ XGrpMsgForward {} -> XGrpMsgForward_ XInfoProbe _ -> XInfoProbe_ @@ -846,7 +1070,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do msg = \case XMsgNew_ -> XMsgNew <$> JT.parseEither parseMsgContainer params XMsgFileDescr_ -> XMsgFileDescr <$> p "msgId" <*> p "fileDescr" - XMsgUpdate_ -> XMsgUpdate <$> p "msgId" <*> p "content" <*> opt "ttl" <*> opt "live" + XMsgUpdate_ -> XMsgUpdate <$> p "msgId" <*> p "content" <*> (fromMaybe M.empty <$> opt "mentions") <*> opt "ttl" <*> opt "live" XMsgDel_ -> XMsgDel <$> p "msgId" <*> opt "memberId" XMsgDeleted_ -> pure XMsgDeleted XMsgReact_ -> XMsgReact <$> p "msgId" <*> opt "memberId" <*> p "reaction" <*> p "add" @@ -860,19 +1084,23 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do XGrpInv_ -> XGrpInv <$> p "groupInvitation" XGrpAcpt_ -> XGrpAcpt <$> p "memberId" XGrpLinkInv_ -> XGrpLinkInv <$> p "groupLinkInvitation" + XGrpLinkReject_ -> XGrpLinkReject <$> p "groupLinkRejection" XGrpLinkMem_ -> XGrpLinkMem <$> p "profile" + XGrpLinkAcpt_ -> XGrpLinkAcpt <$> p "role" XGrpMemNew_ -> XGrpMemNew <$> p "memberInfo" - XGrpMemIntro_ -> XGrpMemIntro <$> p "memberInfo" + XGrpMemIntro_ -> XGrpMemIntro <$> p "memberInfo" <*> opt "memberRestrictions" XGrpMemInv_ -> XGrpMemInv <$> p "memberId" <*> p "memberIntro" XGrpMemFwd_ -> XGrpMemFwd <$> p "memberInfo" <*> p "memberIntro" XGrpMemInfo_ -> XGrpMemInfo <$> p "memberId" <*> p "profile" XGrpMemRole_ -> XGrpMemRole <$> p "memberId" <*> p "role" + XGrpMemRestrict_ -> XGrpMemRestrict <$> p "memberId" <*> p "memberRestrictions" XGrpMemCon_ -> XGrpMemCon <$> p "memberId" XGrpMemConAll_ -> XGrpMemConAll <$> p "memberId" - XGrpMemDel_ -> XGrpMemDel <$> p "memberId" + XGrpMemDel_ -> XGrpMemDel <$> p "memberId" <*> Right (fromRight False $ p "messages") XGrpLeave_ -> pure XGrpLeave XGrpDel_ -> pure XGrpDel XGrpInfo_ -> XGrpInfo <$> p "groupProfile" + XGrpPrefs_ -> XGrpPrefs <$> p "groupPreferences" XGrpDirectInv_ -> XGrpDirectInv <$> p "connReq" <*> opt "content" XGrpMsgForward_ -> XGrpMsgForward <$> p "memberId" <*> p "msg" <*> p "msgTs" XInfoProbe_ -> XInfoProbe <$> p "probe" @@ -906,7 +1134,7 @@ chatToAppMessage ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @ params = \case XMsgNew container -> msgContainerJSON container XMsgFileDescr msgId' fileDescr -> o ["msgId" .= msgId', "fileDescr" .= fileDescr] - XMsgUpdate msgId' content ttl live -> o $ ("ttl" .=? ttl) $ ("live" .=? live) ["msgId" .= msgId', "content" .= content] + XMsgUpdate msgId' content mentions ttl live -> o $ ("ttl" .=? ttl) $ ("live" .=? live) $ ("mentions" .=? nonEmptyMap mentions) ["msgId" .= msgId', "content" .= content] XMsgDel msgId' memberId -> o $ ("memberId" .=? memberId) ["msgId" .= msgId'] XMsgDeleted -> JM.empty XMsgReact msgId' memberId reaction add -> o $ ("memberId" .=? memberId) ["msgId" .= msgId', "reaction" .= reaction, "add" .= add] @@ -920,19 +1148,23 @@ chatToAppMessage ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @ XGrpInv groupInv -> o ["groupInvitation" .= groupInv] XGrpAcpt memId -> o ["memberId" .= memId] XGrpLinkInv groupLinkInv -> o ["groupLinkInvitation" .= groupLinkInv] + XGrpLinkReject groupLinkRjct -> o ["groupLinkRejection" .= groupLinkRjct] XGrpLinkMem profile -> o ["profile" .= profile] + XGrpLinkAcpt role -> o ["role" .= role] XGrpMemNew memInfo -> o ["memberInfo" .= memInfo] - XGrpMemIntro memInfo -> o ["memberInfo" .= memInfo] + XGrpMemIntro memInfo memRestrictions -> o $ ("memberRestrictions" .=? memRestrictions) ["memberInfo" .= memInfo] XGrpMemInv memId memIntro -> o ["memberId" .= memId, "memberIntro" .= memIntro] XGrpMemFwd memInfo memIntro -> o ["memberInfo" .= memInfo, "memberIntro" .= memIntro] XGrpMemInfo memId profile -> o ["memberId" .= memId, "profile" .= profile] XGrpMemRole memId role -> o ["memberId" .= memId, "role" .= role] + XGrpMemRestrict memId memRestrictions -> o ["memberId" .= memId, "memberRestrictions" .= memRestrictions] XGrpMemCon memId -> o ["memberId" .= memId] XGrpMemConAll memId -> o ["memberId" .= memId] - XGrpMemDel memId -> o ["memberId" .= memId] + XGrpMemDel memId messages -> o $ ("messages" .=? if messages then Just True else Nothing) ["memberId" .= memId] XGrpLeave -> JM.empty XGrpDel -> JM.empty XGrpInfo p -> o ["groupProfile" .= p] + XGrpPrefs p -> o ["groupPreferences" .= p] XGrpDirectInv connReq content -> o $ ("content" .=? content) ["connReq" .= connReq] XGrpMsgForward memberId msg msgTs -> o ["memberId" .= memberId, "msg" .= msg, "msgTs" .= msgTs] XInfoProbe probe -> o ["probe" .= probe] diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index ee41fba594..d41957bbac 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -37,7 +37,6 @@ import Data.Word (Word16, Word32) import qualified Network.HTTP.Types as N import Network.HTTP2.Server (responseStreaming) import qualified Paths_simplex_chat as SC -import Simplex.Chat.Archive (archiveFilesFolder) import Simplex.Chat.Controller import Simplex.Chat.Files import Simplex.Chat.Messages (chatNameStr) @@ -50,10 +49,11 @@ import Simplex.Chat.Store.Files import Simplex.Chat.Store.Remote import Simplex.Chat.Store.Shared import Simplex.Chat.Types -import Simplex.Chat.Util (encryptFile) +import Simplex.Chat.Util (encryptFile, liftIOEither) import Simplex.FileTransfer.Description (FileDigest (..)) import Simplex.Messaging.Agent import Simplex.Messaging.Agent.Protocol (AgentErrorType (RCP)) +import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding.String (StrEncoding (..)) @@ -70,13 +70,16 @@ import UnliftIO import UnliftIO.Concurrent (forkIO) import UnliftIO.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExist, removeDirectoryRecursive, renameFile) +remoteFilesFolder :: String +remoteFilesFolder = "simplex_v1_files" + -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [5, 4, 2, 0] +minRemoteCtrlVersion = AppVersion [6, 3, 4, 1] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [5, 4, 2, 0] +minRemoteHostVersion = AppVersion [6, 3, 4, 1] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version @@ -95,7 +98,7 @@ discoveryTimeout = 60000000 -- * Desktop side -getRemoteHostClient :: ChatMonad m => RemoteHostId -> m RemoteHostClient +getRemoteHostClient :: RemoteHostId -> CM RemoteHostClient getRemoteHostClient rhId = do sessions <- asks remoteHostSessions liftIOEither . atomically $ @@ -106,7 +109,7 @@ getRemoteHostClient rhId = do where rhKey = RHId rhId -withRemoteHostSession :: ChatMonad m => RHKey -> SessionSeq -> (RemoteHostSession -> Either ChatError (a, RemoteHostSession)) -> m a +withRemoteHostSession :: RHKey -> SessionSeq -> (RemoteHostSession -> Either ChatError (a, RemoteHostSession)) -> CM a withRemoteHostSession rhKey sseq f = do sessions <- asks remoteHostSessions r <- @@ -121,7 +124,7 @@ withRemoteHostSession rhKey sseq f = do liftEither r -- | Transition session state with a 'RHNew' ID to an assigned 'RemoteHostId' -setNewRemoteHostId :: ChatMonad m => SessionSeq -> RemoteHostId -> m () +setNewRemoteHostId :: SessionSeq -> RemoteHostId -> CM () setNewRemoteHostId sseq rhId = do sessions <- asks remoteHostSessions liftIOEither . atomically $ do @@ -136,13 +139,13 @@ setNewRemoteHostId sseq rhId = do where err = pure . Left . ChatErrorRemoteHost RHNew -startRemoteHost :: ChatMonad m => Maybe (RemoteHostId, Bool) -> Maybe RCCtrlAddress -> Maybe Word16 -> m (NonEmpty RCCtrlAddress, Maybe RemoteHostInfo, RCSignedInvitation) +startRemoteHost :: Maybe (RemoteHostId, Bool) -> Maybe RCCtrlAddress -> Maybe Word16 -> CM (NonEmpty RCCtrlAddress, Maybe RemoteHostInfo, RCSignedInvitation) startRemoteHost rh_ rcAddrPrefs_ port_ = do (rhKey, multicast, remoteHost_, pairing) <- case rh_ of Just (rhId, multicast) -> do rh@RemoteHost {hostPairing} <- withStore $ \db -> getRemoteHost db rhId pure (RHId rhId, multicast, Just $ remoteHostInfo rh $ Just RHSStarting, hostPairing) -- get from the database, start multicast if requested - Nothing -> withAgent $ \a -> (RHNew,False,Nothing,) <$> rcNewHostPairing a + Nothing -> lift . withAgent' $ \a -> (RHNew,False,Nothing,) <$> rcNewHostPairing a sseq <- startRemoteHostSession rhKey ctrlAppInfo <- mkCtrlAppInfo (localAddrs, invitation, rchClient, vars) <- handleConnectError rhKey sseq . withAgent $ \a -> rcConnectHost a pairing (J.toJSON ctrlAppInfo) multicast rcAddrPrefs_ port_ @@ -170,18 +173,18 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do unless (isAppCompatible appVersion ctrlAppVersionRange) $ throwError $ RHEBadVersion appVersion when (encoding == PEKotlin && localEncoding == PESwift) $ throwError $ RHEProtocolError RPEIncompatibleEncoding pure hostInfo - handleConnectError :: ChatMonad m => RHKey -> SessionSeq -> m a -> m a + handleConnectError :: RHKey -> SessionSeq -> CM a -> CM a handleConnectError rhKey sessSeq action = action `catchChatError` \err -> do logError $ "startRemoteHost.rcConnectHost crashed: " <> tshow err cancelRemoteHostSession (Just (sessSeq, RHSRConnectionFailed err)) rhKey throwError err - handleHostError :: ChatMonad m => SessionSeq -> TVar RHKey -> m () -> m () + handleHostError :: SessionSeq -> TVar RHKey -> CM () -> CM () handleHostError sessSeq rhKeyVar action = action `catchChatError` \err -> do logError $ "startRemoteHost.waitForHostSession crashed: " <> tshow err readTVarIO rhKeyVar >>= cancelRemoteHostSession (Just (sessSeq, RHSRCrashed err)) - waitForHostSession :: ChatMonad m => Maybe RemoteHostInfo -> RHKey -> SessionSeq -> Maybe RCCtrlAddress -> TVar RHKey -> RCStepTMVar (ByteString, TLS, RCStepTMVar (RCHostSession, RCHostHello, RCHostPairing)) -> m () + waitForHostSession :: Maybe RemoteHostInfo -> RHKey -> SessionSeq -> Maybe RCCtrlAddress -> TVar RHKey -> RCStepTMVar (ByteString, TLS, RCStepTMVar (RCHostSession, RCHostHello, RCHostPairing)) -> CM () waitForHostSession remoteHost_ rhKey sseq rcAddr_ rhKeyVar vars = do (sessId, tls, vars') <- timeoutThrow (ChatErrorRemoteHost rhKey RHETimeout) 60000000 $ takeRCStep vars let sessionCode = verificationCode sessId @@ -189,7 +192,7 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do RHSessionConnecting _inv rhs' -> Right ((), RHSessionPendingConfirmation sessionCode tls rhs') _ -> Left $ ChatErrorRemoteHost rhKey RHEBadState let rh_' = (\rh -> (rh :: RemoteHostInfo) {sessionState = Just RHSPendingConfirmation {sessionCode}}) <$> remoteHost_ - toView CRRemoteHostSessionCode {remoteHost_ = rh_', sessionCode} + toView CEvtRemoteHostSessionCode {remoteHost_ = rh_', sessionCode} (RCHostSession {sessionKeys}, rhHello, pairing') <- timeoutThrow (ChatErrorRemoteHost rhKey RHETimeout) 60000000 $ takeRCStep vars' hostInfo@HostAppInfo {deviceName = hostDeviceName} <- liftError (ChatErrorRemoteHost rhKey) $ parseHostAppInfo rhHello @@ -200,18 +203,18 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do let rhKey' = RHId remoteHostId -- rhKey may be invalid after upserting on RHNew when (rhKey' /= rhKey) $ do atomically $ writeTVar rhKeyVar rhKey' - toView $ CRNewRemoteHost rhi + toView $ CEvtNewRemoteHost rhi -- set up HTTP transport and remote profile protocol disconnected <- toIO $ onDisconnected rhKey' sseq - httpClient <- liftEitherError (httpError remoteHostId) $ attachRevHTTP2Client disconnected tls + httpClient <- liftError' (httpError remoteHostId) $ attachRevHTTP2Client disconnected tls rhClient <- mkRemoteHostClient httpClient sessionKeys sessId storePath hostInfo pollAction <- async $ pollEvents remoteHostId rhClient withRemoteHostSession rhKey' sseq $ \case RHSessionConfirmed _ RHPendingSession {rchClient} -> Right ((), RHSessionConnected {rchClient, tls, rhClient, pollAction, storePath}) _ -> Left $ ChatErrorRemoteHost rhKey RHEBadState chatWriteVar currentRemoteHost $ Just remoteHostId -- this is required for commands to be passed to remote host - toView $ CRRemoteHostConnected rhi {sessionState = Just RHSConnected {sessionCode}} - upsertRemoteHost :: ChatMonad m => RCHostPairing -> Maybe RemoteHostInfo -> Maybe RCCtrlAddress -> Text -> SessionSeq -> RemoteHostSessionState -> m RemoteHostInfo + toView $ CEvtRemoteHostConnected rhi {sessionState = Just RHSConnected {sessionCode}} + upsertRemoteHost :: RCHostPairing -> Maybe RemoteHostInfo -> Maybe RCCtrlAddress -> Text -> SessionSeq -> RemoteHostSessionState -> CM RemoteHostInfo upsertRemoteHost pairing'@RCHostPairing {knownHost = kh_} rhi_ rcAddr_ hostDeviceName sseq state = do KnownHostPairing {hostDhPubKey = hostDhPubKey'} <- maybe (throwError . ChatError $ CEInternalError "KnownHost is known after verification") pure kh_ case rhi_ of @@ -223,20 +226,20 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do Just rhi@RemoteHostInfo {remoteHostId} -> do withStore' $ \db -> updateHostPairing db remoteHostId hostDeviceName hostDhPubKey' rcAddr_ port_ pure (rhi :: RemoteHostInfo) {sessionState = Just state} - onDisconnected :: ChatMonad m => RHKey -> SessionSeq -> m () + onDisconnected :: RHKey -> SessionSeq -> CM () onDisconnected rhKey sseq = do logDebug $ "HTTP2 client disconnected: " <> tshow (rhKey, sseq) cancelRemoteHostSession (Just (sseq, RHSRDisconnected)) rhKey - pollEvents :: ChatMonad m => RemoteHostId -> RemoteHostClient -> m () + pollEvents :: RemoteHostId -> RemoteHostClient -> CM () pollEvents rhId rhClient = do oq <- asks outputQ forever $ do r_ <- liftRH rhId $ remoteRecv rhClient 10000000 - forM r_ $ \r -> atomically $ writeTBQueue oq (Nothing, Just rhId, r) + forM r_ $ \r -> atomically $ writeTBQueue oq (Just rhId, r) httpError :: RemoteHostId -> HTTP2ClientError -> ChatError httpError rhId = ChatErrorRemoteHost (RHId rhId) . RHEProtocolError . RPEHTTP2 . tshow -startRemoteHostSession :: ChatMonad m => RHKey -> m SessionSeq +startRemoteHostSession :: RHKey -> CM SessionSeq startRemoteHostSession rhKey = do sessions <- asks remoteHostSessions nextSessionSeq <- asks remoteSessionSeq @@ -247,12 +250,12 @@ startRemoteHostSession rhKey = do sessionSeq <- stateTVar nextSessionSeq $ \s -> (s, s + 1) Right sessionSeq <$ TM.insert rhKey (sessionSeq, RHSessionStarting) sessions -closeRemoteHost :: ChatMonad m => RHKey -> m () +closeRemoteHost :: RHKey -> CM () closeRemoteHost rhKey = do logNote $ "Closing remote host session for " <> tshow rhKey cancelRemoteHostSession Nothing rhKey -cancelRemoteHostSession :: ChatMonad m => Maybe (SessionSeq, RemoteHostStopReason) -> RHKey -> m () +cancelRemoteHostSession :: Maybe (SessionSeq, RemoteHostStopReason) -> RHKey -> CM () cancelRemoteHostSession handlerInfo_ rhKey = do sessions <- asks remoteHostSessions crh <- asks currentRemoteHost @@ -268,7 +271,7 @@ cancelRemoteHostSession handlerInfo_ rhKey = do forM_ deregistered $ \session -> do liftIO $ cancelRemoteHost handlingError session `catchAny` (logError . tshow) forM_ (snd <$> handlerInfo_) $ \rhStopReason -> - toView CRRemoteHostStopped {remoteHostId_, rhsState = rhsSessionState session, rhStopReason} + toView CEvtRemoteHostStopped {remoteHostId_, rhsState = rhsSessionState session, rhStopReason} where handlingError = isJust handlerInfo_ remoteHostId_ = case rhKey of @@ -299,7 +302,7 @@ cancelRemoteHost handlingError = \case randomStorePath :: IO FilePath randomStorePath = B.unpack . B64U.encode <$> getRandomBytes 12 -listRemoteHosts :: ChatMonad m => m [RemoteHostInfo] +listRemoteHosts :: CM [RemoteHostInfo] listRemoteHosts = do sessions <- chatReadVar remoteHostSessions map (rhInfo sessions) <$> withStore' getRemoteHosts @@ -307,7 +310,7 @@ listRemoteHosts = do rhInfo sessions rh@RemoteHost {remoteHostId} = remoteHostInfo rh $ rhsSessionState . snd <$> M.lookup (RHId remoteHostId) sessions -switchRemoteHost :: ChatMonad m => Maybe RemoteHostId -> m (Maybe RemoteHostInfo) +switchRemoteHost :: Maybe RemoteHostId -> CM (Maybe RemoteHostInfo) switchRemoteHost rhId_ = do rhi_ <- forM rhId_ $ \rhId -> do let rhKey = RHId rhId @@ -322,7 +325,7 @@ remoteHostInfo :: RemoteHost -> Maybe RemoteHostSessionState -> RemoteHostInfo remoteHostInfo RemoteHost {remoteHostId, storePath, hostDeviceName, bindAddress_, bindPort_} sessionState = RemoteHostInfo {remoteHostId, storePath, hostDeviceName, bindAddress_, bindPort_, sessionState} -deleteRemoteHost :: ChatMonad m => RemoteHostId -> m () +deleteRemoteHost :: RemoteHostId -> CM () deleteRemoteHost rhId = do RemoteHost {storePath} <- withStore (`getRemoteHost` rhId) chatReadVar remoteHostsFolder >>= \case @@ -333,7 +336,7 @@ deleteRemoteHost rhId = do Nothing -> logWarn "Local file store not available while deleting remote host" withStore' (`deleteRemoteHostRecord` rhId) -storeRemoteFile :: forall m. ChatMonad m => RemoteHostId -> Maybe Bool -> FilePath -> m CryptoFile +storeRemoteFile :: RemoteHostId -> Maybe Bool -> FilePath -> CM CryptoFile storeRemoteFile rhId encrypted_ localPath = do c@RemoteHostClient {encryptHostFiles, storePath} <- getRemoteHostClient rhId let encrypt = fromMaybe encryptHostFiles encrypted_ @@ -341,44 +344,45 @@ storeRemoteFile rhId encrypted_ localPath = do filePath' <- liftRH rhId $ remoteStoreFile c filePath (takeFileName localPath) hf_ <- chatReadVar remoteHostsFolder forM_ hf_ $ \hf -> do - let rhf = hf </> storePath </> archiveFilesFolder + let rhf = hf </> storePath </> remoteFilesFolder hPath = rhf </> takeFileName filePath' createDirectoryIfMissing True rhf (if encrypt then renameFile else copyFile) filePath hPath pure (cf :: CryptoFile) {filePath = filePath'} where - encryptLocalFile :: m CryptoFile + encryptLocalFile :: CM CryptoFile encryptLocalFile = do - tmpDir <- getChatTempDirectory + tmpDir <- lift getChatTempDirectory createDirectoryIfMissing True tmpDir - tmpFile <- tmpDir `uniqueCombine` takeFileName localPath + tmpFile <- liftIO $ tmpDir `uniqueCombine` takeFileName localPath cfArgs <- atomically . CF.randomArgs =<< asks random liftError (ChatError . CEFileWrite tmpFile) $ encryptFile localPath tmpFile cfArgs pure $ CryptoFile tmpFile $ Just cfArgs -getRemoteFile :: ChatMonad m => RemoteHostId -> RemoteFile -> m () +getRemoteFile :: RemoteHostId -> RemoteFile -> CM () getRemoteFile rhId rf = do c@RemoteHostClient {storePath} <- getRemoteHostClient rhId - dir <- (</> storePath </> archiveFilesFolder) <$> (maybe getDefaultFilesFolder pure =<< chatReadVar remoteHostsFolder) + dir <- lift $ (</> storePath </> remoteFilesFolder) <$> (maybe getDefaultFilesFolder pure =<< chatReadVar' remoteHostsFolder) createDirectoryIfMissing True dir liftRH rhId $ remoteGetFile c dir rf -processRemoteCommand :: ChatMonad m => RemoteHostId -> RemoteHostClient -> ChatCommand -> ByteString -> m ChatResponse +processRemoteCommand :: RemoteHostId -> RemoteHostClient -> ChatCommand -> ByteString -> CM ChatResponse processRemoteCommand remoteHostId c cmd s = case cmd of SendFile chatName f -> sendFile "/f" chatName f SendImage chatName f -> sendFile "/img" chatName f - _ -> liftRH remoteHostId $ remoteSend c s + _ -> chatRemoteSend s where sendFile cmdName chatName (CryptoFile path cfArgs) = do -- don't encrypt in host if already encrypted locally CryptoFile path' cfArgs' <- storeRemoteFile remoteHostId (cfArgs $> False) path let f = CryptoFile path' (cfArgs <|> cfArgs') -- use local or host encryption - liftRH remoteHostId $ remoteSend c $ B.unwords [cmdName, B.pack (chatNameStr chatName), cryptoFileStr f] + chatRemoteSend $ B.unwords [cmdName, B.pack (chatNameStr chatName), cryptoFileStr f] cryptoFileStr CryptoFile {filePath, cryptoArgs} = maybe "" (\(CFArgs key nonce) -> "key=" <> strEncode key <> " nonce=" <> strEncode nonce <> " ") cryptoArgs <> encodeUtf8 (T.pack filePath) + chatRemoteSend = either throwError pure <=< liftRH remoteHostId . remoteSend c -liftRH :: ChatMonad m => RemoteHostId -> ExceptT RemoteProtocolError IO a -> m a +liftRH :: RemoteHostId -> ExceptT RemoteProtocolError IO a -> CM a liftRH rhId = liftError (ChatErrorRemoteHost (RHId rhId) . RHEProtocolError) -- * Mobile side @@ -386,7 +390,7 @@ liftRH rhId = liftError (ChatErrorRemoteHost (RHId rhId) . RHEProtocolError) -- ** QR/link -- | Use provided OOB link as an annouce -connectRemoteCtrlURI :: ChatMonad m => RCSignedInvitation -> m (Maybe RemoteCtrlInfo, CtrlAppInfo) +connectRemoteCtrlURI :: RCSignedInvitation -> CM (Maybe RemoteCtrlInfo, CtrlAppInfo) connectRemoteCtrlURI signedInv = do verifiedInv <- maybe (throwError $ ChatErrorRemoteCtrl RCEBadInvitation) pure $ verifySignedInvitation signedInv sseq <- startRemoteCtrlSession @@ -394,7 +398,7 @@ connectRemoteCtrlURI signedInv = do -- ** Multicast -findKnownRemoteCtrl :: ChatMonad m => m () +findKnownRemoteCtrl :: CM () findKnownRemoteCtrl = do knownCtrls <- withStore' getRemoteCtrls pairings <- case nonEmpty knownCtrls of @@ -414,13 +418,13 @@ findKnownRemoteCtrl = do Just rc -> pure rc atomically $ putTMVar foundCtrl (rc, inv) let compatible = isJust $ compatibleAppVersion hostAppVersionRange . appVersionRange =<< ctrlAppInfo_ - toView CRRemoteCtrlFound {remoteCtrl = remoteCtrlInfo rc (Just RCSSearching), ctrlAppInfo_, appVersion = currentAppVersion, compatible} + toView CEvtRemoteCtrlFound {remoteCtrl = remoteCtrlInfo rc (Just RCSSearching), ctrlAppInfo_, appVersion = currentAppVersion, compatible} updateRemoteCtrlSession sseq $ \case RCSessionStarting -> Right RCSessionSearching {action, foundCtrl} _ -> Left $ ChatErrorRemoteCtrl RCEBadState atomically $ putTMVar cmdOk () -confirmRemoteCtrl :: ChatMonad m => RemoteCtrlId -> m (RemoteCtrlInfo, CtrlAppInfo) +confirmRemoteCtrl :: RemoteCtrlId -> CM (RemoteCtrlInfo, CtrlAppInfo) confirmRemoteCtrl rcId = do session <- asks remoteCtrlSession (sseq, listener, found) <- liftIOEither $ atomically $ do @@ -438,7 +442,7 @@ confirmRemoteCtrl rcId = do -- ** Common -startRemoteCtrlSession :: ChatMonad m => m SessionSeq +startRemoteCtrlSession :: CM SessionSeq startRemoteCtrlSession = do session <- asks remoteCtrlSession nextSessionSeq <- asks remoteSessionSeq @@ -449,7 +453,7 @@ startRemoteCtrlSession = do sseq <- stateTVar nextSessionSeq $ \s -> (s, s + 1) Right sseq <$ writeTVar session (Just (sseq, RCSessionStarting)) -connectRemoteCtrl :: ChatMonad m => RCVerifiedInvitation -> SessionSeq -> m (Maybe RemoteCtrlInfo, CtrlAppInfo) +connectRemoteCtrl :: RCVerifiedInvitation -> SessionSeq -> CM (Maybe RemoteCtrlInfo, CtrlAppInfo) connectRemoteCtrl verifiedInv@(RCVerifiedInvitation inv@RCInvitation {ca, app}) sseq = handleCtrlError sseq RCSRConnectionFailed "connectRemoteCtrl" $ do ctrlInfo@CtrlAppInfo {deviceName = ctrlDeviceName} <- parseCtrlAppInfo app v <- checkAppVersion ctrlInfo @@ -470,7 +474,7 @@ connectRemoteCtrl verifiedInv@(RCVerifiedInvitation inv@RCInvitation {ca, app}) where validateRemoteCtrl RCInvitation {idkey} RemoteCtrl {ctrlPairing = RCCtrlPairing {idPubKey}} = unless (idkey == idPubKey) $ throwError $ ChatErrorRemoteCtrl $ RCEProtocolError $ PRERemoteControl RCEIdentity - waitForCtrlSession :: ChatMonad m => Maybe RemoteCtrl -> Text -> RCCtrlClient -> RCStepTMVar (ByteString, TLS, RCStepTMVar (RCCtrlSession, RCCtrlPairing)) -> m () + waitForCtrlSession :: Maybe RemoteCtrl -> Text -> RCCtrlClient -> RCStepTMVar (ByteString, TLS, RCStepTMVar (RCCtrlSession, RCCtrlPairing)) -> CM () waitForCtrlSession rc_ ctrlName rcsClient vars = do (uniq, tls, rcsWaitConfirmation) <- timeoutThrow (ChatErrorRemoteCtrl RCETimeout) networkIOTimeout $ takeRCStep vars let sessionCode = verificationCode uniq @@ -479,7 +483,7 @@ connectRemoteCtrl verifiedInv@(RCVerifiedInvitation inv@RCInvitation {ca, app}) let remoteCtrlId_ = remoteCtrlId' <$> rc_ in Right RCSessionPendingConfirmation {remoteCtrlId_, ctrlDeviceName = ctrlName, rcsClient, tls, sessionCode, rcsWaitSession, rcsWaitConfirmation} _ -> Left $ ChatErrorRemoteCtrl RCEBadState - toView CRRemoteCtrlSessionCode {remoteCtrl_ = (`remoteCtrlInfo` Just RCSPendingConfirmation {sessionCode}) <$> rc_, sessionCode} + toView CEvtRemoteCtrlSessionCode {remoteCtrl_ = (`remoteCtrlInfo` Just RCSPendingConfirmation {sessionCode}) <$> rc_, sessionCode} checkAppVersion CtrlAppInfo {appVersionRange} = case compatibleAppVersion hostAppVersionRange appVersionRange of Just (AppCompatible v) -> pure v @@ -489,87 +493,93 @@ connectRemoteCtrl verifiedInv@(RCVerifiedInvitation inv@RCInvitation {ca, app}) encryptFiles <- chatReadVar encryptLocalFiles pure HostAppInfo {appVersion, deviceName = hostDeviceName, encoding = localEncoding, encryptFiles} -parseCtrlAppInfo :: ChatMonad m => JT.Value -> m CtrlAppInfo +parseCtrlAppInfo :: JT.Value -> CM CtrlAppInfo parseCtrlAppInfo ctrlAppInfo = do liftEitherWith (const $ ChatErrorRemoteCtrl RCEBadInvitation) $ JT.parseEither J.parseJSON ctrlAppInfo -handleRemoteCommand :: forall m. ChatMonad m => (ByteString -> m ChatResponse) -> RemoteCrypto -> TBQueue ChatResponse -> HTTP2Request -> m () +handleRemoteCommand :: (ByteString -> CM' (Either ChatError ChatResponse)) -> RemoteCrypto -> TBQueue (Either ChatError ChatEvent) -> HTTP2Request -> CM' () handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do logDebug "handleRemoteCommand" - liftRC (tryRemoteError parseRequest) >>= \case - Right (getNext, rc) -> do - chatReadVar currentUser >>= \case + liftIO (tryRemoteError' parseRequest) >>= \case + Right (rfKN, getNext, rc) -> do + chatReadVar' currentUser >>= \case Nothing -> replyError $ ChatError CENoActiveUser - Just user -> processCommand user getNext rc `catchChatError` replyError + Just user -> processCommand user rfKN getNext rc `catchChatError'` replyError Left e -> reply $ RRProtocolError e where - parseRequest :: ExceptT RemoteProtocolError IO (GetChunk, RemoteCommand) + parseRequest :: ExceptT RemoteProtocolError IO (C.SbKeyNonce, GetChunk, RemoteCommand) parseRequest = do - (header, getNext) <- parseDecryptHTTP2Body encryption request reqBody - (getNext,) <$> liftEitherWith RPEInvalidJSON (J.eitherDecode header) - replyError = reply . RRChatResponse . CRChatCmdError Nothing - processCommand :: User -> GetChunk -> RemoteCommand -> m () - processCommand user getNext = \case - RCSend {command} -> handleSend execChatCommand command >>= reply - RCRecv {wait = time} -> handleRecv time remoteOutputQ >>= reply - RCStoreFile {fileName, fileSize, fileDigest} -> handleStoreFile encryption fileName fileSize fileDigest getNext >>= reply - RCGetFile {file} -> handleGetFile encryption user file replyWith - reply :: RemoteResponse -> m () - reply = (`replyWith` \_ -> pure ()) - replyWith :: Respond m + (rfKN, header, getNext) <- parseDecryptHTTP2Body encryption request reqBody + (rfKN,getNext,) <$> liftEitherWith RPEInvalidJSON (J.eitherDecode header) + replyError = reply . RRChatResponse . RRError + processCommand :: User -> C.SbKeyNonce -> GetChunk -> RemoteCommand -> CM () + processCommand user rfKN getNext = \case + RCSend {command} -> lift $ handleSend execChatCommand command >>= reply + RCRecv {wait = time} -> lift $ liftIO (handleRecv time remoteOutputQ) >>= reply + RCStoreFile {fileName, fileSize, fileDigest} -> lift $ handleStoreFile rfKN fileName fileSize fileDigest getNext >>= reply + RCGetFile {file} -> handleGetFile user file replyWith + reply :: RemoteResponse -> CM' () + reply = (`replyWith` \_ _ -> pure ()) + replyWith :: Respond replyWith rr attach = do - resp <- liftRC $ encryptEncodeHTTP2Body encryption $ J.encode rr - liftIO . sendResponse . responseStreaming N.status200 [] $ \send flush -> do - send resp - attach send - flush + (corrId, cmdKN, sfKN) <- atomically $ getRemoteSndKeys encryption + liftIO (tryRemoteError' . encryptEncodeHTTP2Body corrId cmdKN encryption $ J.encode rr) >>= \case + Right resp -> liftIO . sendResponse . responseStreaming N.status200 [] $ \send flush -> do + send resp + attach sfKN send + flush + Left e -> eToView' $ ChatErrorRemoteCtrl $ RCEProtocolError e -takeRCStep :: ChatMonad m => RCStepTMVar a -> m a -takeRCStep = liftEitherError (\e -> ChatErrorAgent {agentError = RCP e, connectionEntity_ = Nothing}) . atomically . takeTMVar +takeRCStep :: RCStepTMVar a -> CM a +takeRCStep = liftError' (\e -> ChatErrorAgent {agentError = RCP e, connectionEntity_ = Nothing}) . atomically . takeTMVar type GetChunk = Int -> IO ByteString type SendChunk = Builder -> IO () -type Respond m = RemoteResponse -> (SendChunk -> IO ()) -> m () +type Respond = RemoteResponse -> (C.SbKeyNonce -> SendChunk -> IO ()) -> CM' () -liftRC :: ChatMonad m => ExceptT RemoteProtocolError IO a -> m a +liftRC :: ExceptT RemoteProtocolError IO a -> CM a liftRC = liftError (ChatErrorRemoteCtrl . RCEProtocolError) tryRemoteError :: ExceptT RemoteProtocolError IO a -> ExceptT RemoteProtocolError IO (Either RemoteProtocolError a) tryRemoteError = tryAllErrors (RPEException . tshow) {-# INLINE tryRemoteError #-} -handleSend :: ChatMonad m => (ByteString -> m ChatResponse) -> Text -> m RemoteResponse +tryRemoteError' :: ExceptT RemoteProtocolError IO a -> IO (Either RemoteProtocolError a) +tryRemoteError' = tryAllErrors' (RPEException . tshow) +{-# INLINE tryRemoteError' #-} + +handleSend :: (ByteString -> CM' (Either ChatError ChatResponse)) -> Text -> CM' RemoteResponse handleSend execChatCommand command = do logDebug $ "Send: " <> tshow command -- execChatCommand checks for remote-allowed commands - -- convert errors thrown in ChatMonad into error responses to prevent aborting the protocol wrapper - RRChatResponse <$> execChatCommand (encodeUtf8 command) `catchError` (pure . CRChatError Nothing) + -- convert errors thrown in execChatCommand into error responses to prevent aborting the protocol wrapper + RRChatResponse . eitherToResult <$> execChatCommand (encodeUtf8 command) -handleRecv :: MonadUnliftIO m => Int -> TBQueue ChatResponse -> m RemoteResponse +handleRecv :: Int -> TBQueue (Either ChatError ChatEvent) -> IO RemoteResponse handleRecv time events = do logDebug $ "Recv: " <> tshow time - RRChatEvent <$> (timeout time . atomically $ readTBQueue events) + RRChatEvent . fmap eitherToResult <$> (timeout time . atomically $ readTBQueue events) -- TODO this command could remember stored files and return IDs to allow removing files that are not needed. -- Also, there should be some process removing unused files uploaded to remote host (possibly, all unused files). -handleStoreFile :: forall m. ChatMonad m => RemoteCrypto -> FilePath -> Word32 -> FileDigest -> GetChunk -> m RemoteResponse -handleStoreFile encryption fileName fileSize fileDigest getChunk = - either RRProtocolError RRFileStored <$> (chatReadVar filesFolder >>= storeFile) +handleStoreFile :: C.SbKeyNonce -> FilePath -> Word32 -> FileDigest -> GetChunk -> CM' RemoteResponse +handleStoreFile rfKN fileName fileSize fileDigest getChunk = + either RRProtocolError RRFileStored <$> (chatReadVar' filesFolder >>= storeFile) where - storeFile :: Maybe FilePath -> m (Either RemoteProtocolError FilePath) + storeFile :: Maybe FilePath -> CM' (Either RemoteProtocolError FilePath) storeFile = \case Just ff -> takeFileName <$$> storeFileTo ff Nothing -> storeFileTo =<< getDefaultFilesFolder - storeFileTo :: FilePath -> m (Either RemoteProtocolError FilePath) - storeFileTo dir = liftRC . tryRemoteError $ do - filePath <- dir `uniqueCombine` fileName - receiveEncryptedFile encryption getChunk fileSize fileDigest filePath + storeFileTo :: FilePath -> CM' (Either RemoteProtocolError FilePath) + storeFileTo dir = liftIO . tryRemoteError' $ do + filePath <- liftIO $ dir `uniqueCombine` fileName + receiveEncryptedFile rfKN getChunk fileSize fileDigest filePath pure filePath -handleGetFile :: ChatMonad m => RemoteCrypto -> User -> RemoteFile -> Respond m -> m () -handleGetFile encryption User {userId} RemoteFile {userId = commandUserId, fileId, sent, fileSource = cf'@CryptoFile {filePath}} reply = do +handleGetFile :: User -> RemoteFile -> Respond -> CM () +handleGetFile User {userId} RemoteFile {userId = commandUserId, fileId, sent, fileSource = cf'@CryptoFile {filePath}} reply = do logDebug $ "GetFile: " <> tshow filePath unless (userId == commandUserId) $ throwChatError $ CEDifferentActiveUser {commandUserId, activeUserId = userId} path <- maybe filePath (</> filePath) <$> chatReadVar filesFolder @@ -577,13 +587,14 @@ handleGetFile encryption User {userId} RemoteFile {userId = commandUserId, fileI cf <- getLocalCryptoFile db commandUserId fileId sent unless (cf == cf') $ throwError $ SEFileNotFound fileId liftRC (tryRemoteError $ getFileInfo path) >>= \case - Left e -> reply (RRProtocolError e) $ \_ -> pure () + Left e -> lift $ reply (RRProtocolError e) $ \_ _ -> pure () Right (fileSize, fileDigest) -> - withFile path ReadMode $ \h -> do - encFile <- liftRC $ prepareEncryptedFile encryption (h, fileSize) - reply RRFile {fileSize, fileDigest} $ sendEncryptedFile encFile + lift . withFile path ReadMode $ \h -> do + reply RRFile {fileSize, fileDigest} $ \sfKN send -> void . runExceptT $ do + encFile <- prepareEncryptedFile sfKN (h, fileSize) + liftIO $ sendEncryptedFile encFile send -listRemoteCtrls :: ChatMonad m => m [RemoteCtrlInfo] +listRemoteCtrls :: CM [RemoteCtrlInfo] listRemoteCtrls = do session <- snd <$$> chatReadVar remoteCtrlSession let rcId = sessionRcId =<< session @@ -604,7 +615,7 @@ remoteCtrlInfo RemoteCtrl {remoteCtrlId, ctrlDeviceName} sessionState = RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName, sessionState} -- | Take a look at emoji of tlsunique, commit pairing, and start session server -verifyRemoteCtrlSession :: ChatMonad m => (ByteString -> m ChatResponse) -> Text -> m RemoteCtrlInfo +verifyRemoteCtrlSession :: (ByteString -> CM' (Either ChatError ChatResponse)) -> Text -> CM RemoteCtrlInfo verifyRemoteCtrlSession execChatCommand sessCode' = do (sseq, client, ctrlName, sessionCode, vars) <- chatReadVar remoteCtrlSession >>= \case @@ -619,14 +630,15 @@ verifyRemoteCtrlSession execChatCommand sessCode' = do rc@RemoteCtrl {remoteCtrlId} <- upsertRemoteCtrl ctrlName rcCtrlPairing remoteOutputQ <- asks (tbqSize . config) >>= newTBQueueIO encryption <- mkCtrlRemoteCrypto sessionKeys $ tlsUniq tls - http2Server <- async $ attachHTTP2Server tls $ handleRemoteCommand execChatCommand encryption remoteOutputQ + cc <- ask + http2Server <- liftIO . async $ attachHTTP2Server tls $ \req -> handleRemoteCommand execChatCommand encryption remoteOutputQ req `runReaderT` cc void . forkIO $ monitor sseq http2Server updateRemoteCtrlSession sseq $ \case RCSessionPendingConfirmation {} -> Right RCSessionConnected {remoteCtrlId, rcsClient = client, rcsSession, tls, http2Server, remoteOutputQ} _ -> Left $ ChatErrorRemoteCtrl RCEBadState pure $ remoteCtrlInfo rc $ Just RCSConnected {sessionCode = tlsSessionCode tls} where - upsertRemoteCtrl :: ChatMonad m => Text -> RCCtrlPairing -> m RemoteCtrl + upsertRemoteCtrl :: Text -> RCCtrlPairing -> CM RemoteCtrl upsertRemoteCtrl ctrlName rcCtrlPairing = withStore $ \db -> do rc_ <- liftIO $ getRemoteCtrlByFingerprint db (ctrlFingerprint rcCtrlPairing) case rc_ of @@ -635,16 +647,16 @@ verifyRemoteCtrlSession execChatCommand sessCode' = do let dhPrivKey' = dhPrivKey rcCtrlPairing liftIO $ updateRemoteCtrl db rc ctrlName dhPrivKey' pure rc {ctrlDeviceName = ctrlName, ctrlPairing = ctrlPairing {dhPrivKey = dhPrivKey'}} - monitor :: ChatMonad m => SessionSeq -> Async () -> m () + monitor :: SessionSeq -> Async () -> CM () monitor sseq server = do res <- waitCatch server logInfo $ "HTTP2 server stopped: " <> tshow res cancelActiveRemoteCtrl $ Just (sseq, RCSRDisconnected) -stopRemoteCtrl :: ChatMonad m => m () +stopRemoteCtrl :: CM () stopRemoteCtrl = cancelActiveRemoteCtrl Nothing -handleCtrlError :: ChatMonad m => SessionSeq -> (ChatError -> RemoteCtrlStopReason) -> Text -> m a -> m a +handleCtrlError :: SessionSeq -> (ChatError -> RemoteCtrlStopReason) -> Text -> CM a -> CM a handleCtrlError sseq mkReason name action = action `catchChatError` \e -> do logError $ name <> " remote ctrl error: " <> tshow e @@ -652,7 +664,7 @@ handleCtrlError sseq mkReason name action = throwError e -- | Stop session controller, unless session update key is present but stale -cancelActiveRemoteCtrl :: ChatMonad m => Maybe (SessionSeq, RemoteCtrlStopReason) -> m () +cancelActiveRemoteCtrl :: Maybe (SessionSeq, RemoteCtrlStopReason) -> CM () cancelActiveRemoteCtrl handlerInfo_ = handleAny (logError . tshow) $ do var <- asks remoteCtrlSession session_ <- @@ -664,7 +676,7 @@ cancelActiveRemoteCtrl handlerInfo_ = handleAny (logError . tshow) $ do forM_ session_ $ \session -> do liftIO $ cancelRemoteCtrl handlingError session forM_ (snd <$> handlerInfo_) $ \rcStopReason -> - toView CRRemoteCtrlStopped {rcsState = rcsSessionState session, rcStopReason} + toView CEvtRemoteCtrlStopped {rcsState = rcsSessionState session, rcStopReason} where handlingError = isJust handlerInfo_ @@ -685,18 +697,18 @@ cancelRemoteCtrl handlingError = \case cancelCtrlClient rcsClient closeConnection tls -deleteRemoteCtrl :: ChatMonad m => RemoteCtrlId -> m () +deleteRemoteCtrl :: RemoteCtrlId -> CM () deleteRemoteCtrl rcId = do checkNoRemoteCtrlSession -- TODO check it exists withStore' (`deleteRemoteCtrlRecord` rcId) -checkNoRemoteCtrlSession :: ChatMonad m => m () +checkNoRemoteCtrlSession :: CM () checkNoRemoteCtrlSession = chatReadVar remoteCtrlSession >>= maybe (pure ()) (\_ -> throwError $ ChatErrorRemoteCtrl RCEBusy) -- | Transition controller to a new state, unless session update key is stale -updateRemoteCtrlSession :: ChatMonad m => SessionSeq -> (RemoteCtrlSession -> Either ChatError RemoteCtrlSession) -> m () +updateRemoteCtrlSession :: SessionSeq -> (RemoteCtrlSession -> Either ChatError RemoteCtrlSession) -> CM () updateRemoteCtrlSession sseq state = do session <- asks remoteCtrlSession r <- atomically $ do diff --git a/src/Simplex/Chat/Remote/Protocol.hs b/src/Simplex/Chat/Remote/Protocol.hs index 7b3d70ff97..207eade665 100644 --- a/src/Simplex/Chat/Remote/Protocol.hs +++ b/src/Simplex/Chat/Remote/Protocol.hs @@ -16,11 +16,11 @@ import Control.Monad.Except import Control.Monad.Reader import Crypto.Hash (SHA512) import qualified Crypto.Hash as CH -import Data.Aeson ((.=)) +import Data.Aeson (FromJSON (..), ToJSON (..), (.=)) import qualified Data.Aeson as J import qualified Data.Aeson.Key as JK import qualified Data.Aeson.KeyMap as JM -import Data.Aeson.TH (deriveJSON) +import qualified Data.Aeson.TH as JQ import qualified Data.Aeson.Types as JT import qualified Data.ByteArray as BA import Data.ByteString (ByteString) @@ -42,11 +42,13 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..)) import Simplex.Messaging.Crypto.Lazy (LazyByteString) import Simplex.Messaging.Encoding -import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON, pattern SingleFieldJSONTag, pattern TaggedObjectJSONData, pattern TaggedObjectJSONTag) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, taggedObjectJSON, pattern SingleFieldJSONTag, pattern TaggedObjectJSONData, pattern TaggedObjectJSONTag) +import qualified Simplex.Messaging.TMap as TM +import Simplex.Messaging.Transport (TSbChainKeys) import Simplex.Messaging.Transport.Buffer (getBuffered) import Simplex.Messaging.Transport.HTTP2 (HTTP2Body (..), HTTP2BodyChunk, getBodyChunk) import Simplex.Messaging.Transport.HTTP2.Client (HTTP2Client, HTTP2Response (..), closeHTTP2Client, sendRequestDirect) -import Simplex.Messaging.Util (liftEitherError, liftEitherWith, liftError, tshow) +import Simplex.Messaging.Util (liftError', liftEitherWith, liftError, tshow) import Simplex.RemoteControl.Client (xrcpBlockSize) import qualified Simplex.RemoteControl.Client as RC import Simplex.RemoteControl.Types (CtrlSessKeys (..), HostSessKeys (..), RCErrorType (..), SessionCode) @@ -62,26 +64,48 @@ data RemoteCommand deriving (Show) data RemoteResponse - = RRChatResponse {chatResponse :: ChatResponse} - | RRChatEvent {chatEvent :: Maybe ChatResponse} -- 'Nothing' on poll timeout + = RRChatResponse {chatResponse :: RRResult ChatResponse} + | RRChatEvent {chatEvent :: Maybe (RRResult ChatEvent)} -- 'Nothing' on poll timeout | RRFileStored {filePath :: String} | RRFile {fileSize :: Word32, fileDigest :: FileDigest} -- provides attachment , fileDigest :: FileDigest | RRProtocolError {remoteProcotolError :: RemoteProtocolError} -- The protocol error happened on the server side deriving (Show) +data RRResult r + = RRResult {result :: r} + | RRError {error :: ChatError} + deriving (Show) + +resultToEither :: RRResult r -> Either ChatError r +resultToEither = \case + RRResult r -> Right r + RRError e -> Left e +{-# INLINE resultToEither #-} + +eitherToResult :: Either ChatError r -> RRResult r +eitherToResult = either RRError RRResult +{-# INLINE eitherToResult #-} + +$(pure []) + -- Force platform-independent encoding as the types aren't UI-visible -$(deriveJSON (taggedObjectJSON $ dropPrefix "RC") ''RemoteCommand) -$(deriveJSON (taggedObjectJSON $ dropPrefix "RR") ''RemoteResponse) +instance ToJSON r => ToJSON (RRResult r) where + toEncoding = $(JQ.mkToEncoding (defaultJSON {J.sumEncoding = J.UntaggedValue}) ''RRResult) + toJSON = $(JQ.mkToJSON (defaultJSON {J.sumEncoding = J.UntaggedValue}) ''RRResult) + +instance FromJSON r => FromJSON (RRResult r) where + parseJSON = $(JQ.mkParseJSON (defaultJSON {J.sumEncoding = J.UntaggedValue}) ''RRResult) + +$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "RC") ''RemoteCommand) +$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "RR") ''RemoteResponse) -- * Client side / desktop -mkRemoteHostClient :: ChatMonad m => HTTP2Client -> HostSessKeys -> SessionCode -> FilePath -> HostAppInfo -> m RemoteHostClient +mkRemoteHostClient :: HTTP2Client -> HostSessKeys -> SessionCode -> FilePath -> HostAppInfo -> CM RemoteHostClient mkRemoteHostClient httpClient sessionKeys sessionCode storePath HostAppInfo {encoding, deviceName, encryptFiles} = do - drg <- asks random - counter <- newTVarIO 1 - let HostSessKeys {hybridKey, idPrivKey, sessPrivKey} = sessionKeys + let HostSessKeys {chainKeys, idPrivKey, sessPrivKey} = sessionKeys signatures = RSSign {idPrivKey, sessPrivKey} - encryption = RemoteCrypto {drg, counter, sessionCode, hybridKey, signatures} + encryption <- liftIO $ mkRemoteCrypto sessionCode chainKeys signatures pure RemoteHostClient { hostEncoding = encoding, @@ -92,28 +116,33 @@ mkRemoteHostClient httpClient sessionKeys sessionCode storePath HostAppInfo {enc storePath } -mkCtrlRemoteCrypto :: ChatMonad m => CtrlSessKeys -> SessionCode -> m RemoteCrypto -mkCtrlRemoteCrypto CtrlSessKeys {hybridKey, idPubKey, sessPubKey} sessionCode = do - drg <- asks random - counter <- newTVarIO 1 +mkCtrlRemoteCrypto :: CtrlSessKeys -> SessionCode -> CM RemoteCrypto +mkCtrlRemoteCrypto CtrlSessKeys {chainKeys, idPubKey, sessPubKey} sessionCode = let signatures = RSVerify {idPubKey, sessPubKey} - pure RemoteCrypto {drg, counter, sessionCode, hybridKey, signatures} + in liftIO $ mkRemoteCrypto sessionCode chainKeys signatures -closeRemoteHostClient :: MonadIO m => RemoteHostClient -> m () -closeRemoteHostClient RemoteHostClient {httpClient} = liftIO $ closeHTTP2Client httpClient +mkRemoteCrypto :: SessionCode -> TSbChainKeys -> RemoteSignatures -> IO RemoteCrypto +mkRemoteCrypto sessionCode chainKeys signatures = do + sndCounter <- newTVarIO 0 + rcvCounter <- newTVarIO 0 + skippedKeys <- liftIO TM.emptyIO + pure RemoteCrypto {sessionCode, sndCounter, rcvCounter, chainKeys, skippedKeys, signatures} + +closeRemoteHostClient :: RemoteHostClient -> IO () +closeRemoteHostClient RemoteHostClient {httpClient} = closeHTTP2Client httpClient -- ** Commands -remoteSend :: RemoteHostClient -> ByteString -> ExceptT RemoteProtocolError IO ChatResponse +remoteSend :: RemoteHostClient -> ByteString -> ExceptT RemoteProtocolError IO (Either ChatError ChatResponse) remoteSend c cmd = sendRemoteCommand' c Nothing RCSend {command = decodeUtf8 cmd} >>= \case - RRChatResponse cr -> pure cr + RRChatResponse cr -> pure $ resultToEither cr r -> badResponse r -remoteRecv :: RemoteHostClient -> Int -> ExceptT RemoteProtocolError IO (Maybe ChatResponse) +remoteRecv :: RemoteHostClient -> Int -> ExceptT RemoteProtocolError IO (Maybe (Either ChatError ChatEvent)) remoteRecv c ms = sendRemoteCommand' c Nothing RCRecv {wait = ms} >>= \case - RRChatEvent cr_ -> pure cr_ + RRChatEvent cEvt_ -> pure $ resultToEither <$> cEvt_ r -> badResponse r remoteStoreFile :: RemoteHostClient -> FilePath -> FilePath -> ExceptT RemoteProtocolError IO FilePath @@ -125,26 +154,30 @@ remoteStoreFile c localPath fileName = do r -> badResponse r remoteGetFile :: RemoteHostClient -> FilePath -> RemoteFile -> ExceptT RemoteProtocolError IO () -remoteGetFile c@RemoteHostClient {encryption} destDir rf@RemoteFile {fileSource = CryptoFile {filePath}} = +remoteGetFile c destDir rf@RemoteFile {fileSource = CryptoFile {filePath}} = sendRemoteCommand c Nothing RCGetFile {file = rf} >>= \case - (getChunk, RRFile {fileSize, fileDigest}) -> do + (rfKN, getChunk, RRFile {fileSize, fileDigest}) -> do -- TODO we could optimize by checking size and hash before receiving the file let localPath = destDir </> takeFileName filePath - receiveEncryptedFile encryption getChunk fileSize fileDigest localPath - (_, r) -> badResponse r + receiveEncryptedFile rfKN getChunk fileSize fileDigest localPath + (_, _, r) -> badResponse r -- TODO validate there is no attachment in response sendRemoteCommand' :: RemoteHostClient -> Maybe (Handle, Word32) -> RemoteCommand -> ExceptT RemoteProtocolError IO RemoteResponse -sendRemoteCommand' c attachment_ rc = snd <$> sendRemoteCommand c attachment_ rc +sendRemoteCommand' c attachment_ rc = do + (_, _, r) <- sendRemoteCommand c attachment_ rc + pure r -sendRemoteCommand :: RemoteHostClient -> Maybe (Handle, Word32) -> RemoteCommand -> ExceptT RemoteProtocolError IO (Int -> IO ByteString, RemoteResponse) +sendRemoteCommand :: RemoteHostClient -> Maybe (Handle, Word32) -> RemoteCommand -> ExceptT RemoteProtocolError IO (C.SbKeyNonce, Int -> IO ByteString, RemoteResponse) sendRemoteCommand RemoteHostClient {httpClient, hostEncoding, encryption} file_ cmd = do - encFile_ <- mapM (prepareEncryptedFile encryption) file_ - req <- httpRequest encFile_ <$> encryptEncodeHTTP2Body encryption (J.encode cmd) - HTTP2Response {response, respBody} <- liftEitherError (RPEHTTP2 . tshow) $ sendRequestDirect httpClient req Nothing - (header, getNext) <- parseDecryptHTTP2Body encryption response respBody + (corrId, cmdKN, sfKN) <- atomically $ getRemoteSndKeys encryption + encCmd <- encryptEncodeHTTP2Body corrId cmdKN encryption $ J.encode cmd + encFile_ <- mapM (prepareEncryptedFile sfKN) file_ + let req = httpRequest encFile_ encCmd + HTTP2Response {response, respBody} <- liftError' (RPEHTTP2 . tshow) $ sendRequestDirect httpClient req Nothing + (rfKN, header, getNext) <- parseDecryptHTTP2Body encryption response respBody rr <- liftEitherWith (RPEInvalidJSON . fromString) $ J.eitherDecode header >>= JT.parseEither J.parseJSON . convertJSON hostEncoding localEncoding - pure (getNext, rr) + pure (rfKN, getNext, rr) where httpRequest encFile_ cmdBld = H.requestStreaming N.methodPost "/" mempty $ \send flush -> do send cmdBld @@ -163,7 +196,7 @@ convertJSON :: PlatformEncoding -> PlatformEncoding -> J.Value -> J.Value convertJSON _remote@PEKotlin _local@PEKotlin = id convertJSON PESwift PESwift = id convertJSON PESwift PEKotlin = owsf2tagged -convertJSON PEKotlin PESwift = error "unsupported convertJSON: K/S" -- guarded by handshake +convertJSON PEKotlin PESwift = Prelude.error "unsupported convertJSON: K/S" -- guarded by handshake -- | Convert swift single-field sum encoding into tagged/discriminator-field owsf2tagged :: J.Value -> J.Value @@ -213,13 +246,11 @@ pattern OwsfTag = (SingleFieldJSONTag, J.Bool True) -- See https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2023-10-25-remote-control.md for encoding -encryptEncodeHTTP2Body :: RemoteCrypto -> LazyByteString -> ExceptT RemoteProtocolError IO Builder -encryptEncodeHTTP2Body RemoteCrypto {drg, counter, sessionCode, hybridKey, signatures} s = do - corrId <- atomically $ stateTVar counter $ \c -> (c, c + 1) - let pfx = smpEncode (sessionCode, corrId) - (nonce, ct) <- liftError PRERemoteControl $ RC.rcEncryptBody drg hybridKey $ LB.fromStrict pfx <> s +encryptEncodeHTTP2Body :: Word32 -> C.SbKeyNonce -> RemoteCrypto -> LazyByteString -> ExceptT RemoteProtocolError IO Builder +encryptEncodeHTTP2Body corrId cmdKN RemoteCrypto {sessionCode, signatures} s = do + ct <- liftError PRERemoteControl $ RC.rcEncryptBody cmdKN $ LB.fromStrict (smpEncode sessionCode) <> s let ctLen = encodeWord32 (fromIntegral $ LB.length ct) - signed = LB.fromStrict (smpEncode nonce <> ctLen) <> ct + signed = LB.fromStrict (encodeWord32 corrId <> ctLen) <> ct sigs <- bodySignatures signed pure $ lazyByteString signed <> sigs where @@ -235,24 +266,25 @@ encryptEncodeHTTP2Body RemoteCrypto {drg, counter, sessionCode, hybridKey, signa sign k = C.signatureBytes . C.sign' k . BA.convert . CH.hashFinalize -- | Parse and decrypt HTTP2 request/response -parseDecryptHTTP2Body :: HTTP2BodyChunk a => RemoteCrypto -> a -> HTTP2Body -> ExceptT RemoteProtocolError IO (LazyByteString, Int -> IO ByteString) -parseDecryptHTTP2Body RemoteCrypto {hybridKey, sessionCode, signatures} hr HTTP2Body {bodyBuffer} = do - (nonce, ct) <- getBody - s <- liftError PRERemoteControl $ RC.rcDecryptBody hybridKey nonce ct - (,getNext) <$> parseBody s +parseDecryptHTTP2Body :: HTTP2BodyChunk a => RemoteCrypto -> a -> HTTP2Body -> ExceptT RemoteProtocolError IO (C.SbKeyNonce, LazyByteString, Int -> IO ByteString) +parseDecryptHTTP2Body rc@RemoteCrypto {sessionCode, signatures} hr HTTP2Body {bodyBuffer} = do + (corrId, ct) <- getBody + (cmdKN, rfKN) <- ExceptT $ atomically $ getRemoteRcvKeys rc corrId + s <- liftError PRERemoteControl $ RC.rcDecryptBody cmdKN ct + s' <- parseBody s + pure (rfKN, s', getNext) where - getBody :: ExceptT RemoteProtocolError IO (C.CbNonce, LazyByteString) + getBody :: ExceptT RemoteProtocolError IO (Word32, LazyByteString) getBody = do - nonceStr <- liftIO $ getNext 24 - nonce <- liftEitherWith RPEInvalidBody $ smpDecode nonceStr + corrIdStr <- liftIO $ getNext 4 ctLenStr <- liftIO $ getNext 4 let ctLen = decodeWord32 ctLenStr when (ctLen > fromIntegral (maxBound :: Int)) $ throwError RPEInvalidSize chunks <- liftIO $ getLazy $ fromIntegral ctLen - let hc = CH.hashUpdates (CH.hashInit @SHA512) [nonceStr, ctLenStr] + let hc = CH.hashUpdates (CH.hashInit @SHA512) [corrIdStr, ctLenStr] hc' = CH.hashUpdates hc chunks verifySignatures hc' - pure (nonce, LB.fromChunks chunks) + pure (decodeWord32 corrIdStr, LB.fromChunks chunks) getLazy :: Int -> IO [ByteString] getLazy 0 = pure [] getLazy n = do @@ -271,7 +303,7 @@ parseDecryptHTTP2Body RemoteCrypto {hybridKey, sessionCode, signatures} hr HTTP2 where getSig = do len <- liftIO $ B.head <$> getNext 1 - liftEitherError RPEInvalidBody $ C.decodeSignature <$> getNext (fromIntegral len) + liftError' RPEInvalidBody $ C.decodeSignature <$> getNext (fromIntegral len) verifySig key sig hc' = do let signed = BA.convert $ CH.hashFinalize hc' unless (C.verify' key sig signed) $ throwError $ PRERemoteControl RCECtrlAuth @@ -279,9 +311,8 @@ parseDecryptHTTP2Body RemoteCrypto {hybridKey, sessionCode, signatures} hr HTTP2 parseBody s = case LB.uncons s of Nothing -> throwError $ RPEInvalidBody "empty body" Just (scLen, rest) -> do - (sessCode', rest') <- takeBytes (fromIntegral scLen) rest + (sessCode', s') <- takeBytes (fromIntegral scLen) rest unless (sessCode' == sessionCode) $ throwError PRESessionCode - (_corrId, s') <- takeBytes 8 rest' pure s' where takeBytes n s' = do diff --git a/src/Simplex/Chat/Remote/RevHTTP.hs b/src/Simplex/Chat/Remote/RevHTTP.hs index a37d77e20a..4df5bcac2a 100644 --- a/src/Simplex/Chat/Remote/RevHTTP.hs +++ b/src/Simplex/Chat/Remote/RevHTTP.hs @@ -13,19 +13,17 @@ import Simplex.Messaging.Transport.HTTP2 (defaultHTTP2BufferSize, getHTTP2Body) import Simplex.Messaging.Transport.HTTP2.Client (HTTP2Client, HTTP2ClientError (..), attachHTTP2Client, bodyHeadSize, connTimeout, defaultHTTP2ClientConfig) import Simplex.Messaging.Transport.HTTP2.Server (HTTP2Request (..), runHTTP2ServerWith) import Simplex.RemoteControl.Discovery -import UnliftIO attachRevHTTP2Client :: IO () -> TLS -> IO (Either HTTP2ClientError HTTP2Client) attachRevHTTP2Client disconnected = attachHTTP2Client config ANY_ADDR_V4 "0" disconnected defaultHTTP2BufferSize where config = defaultHTTP2ClientConfig {bodyHeadSize = doNotPrefetchHead, connTimeout = maxBound} -attachHTTP2Server :: MonadUnliftIO m => TLS -> (HTTP2Request -> m ()) -> m () -attachHTTP2Server tls processRequest = do - withRunInIO $ \unlift -> - runHTTP2ServerWith defaultHTTP2BufferSize ($ tls) $ \sessionId r sendResponse -> do - reqBody <- getHTTP2Body r doNotPrefetchHead - unlift $ processRequest HTTP2Request {sessionId, request = r, reqBody, sendResponse} +attachHTTP2Server :: TLS -> (HTTP2Request -> IO ()) -> IO () +attachHTTP2Server tls processRequest = + runHTTP2ServerWith defaultHTTP2BufferSize ($ tls) $ \sessionId sessionALPN r sendResponse -> do + reqBody <- getHTTP2Body r doNotPrefetchHead + processRequest HTTP2Request {sessionId, sessionALPN, request = r, reqBody, sendResponse} -- | Suppress storing initial chunk in bodyHead, forcing clients and servers to stream chunks doNotPrefetchHead :: Int diff --git a/src/Simplex/Chat/Remote/Transport.hs b/src/Simplex/Chat/Remote/Transport.hs index 1c9c3f08eb..933936aa4d 100644 --- a/src/Simplex/Chat/Remote/Transport.hs +++ b/src/Simplex/Chat/Remote/Transport.hs @@ -1,5 +1,6 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} module Simplex.Chat.Remote.Transport where @@ -15,33 +16,31 @@ import Simplex.FileTransfer.Transport (ReceiveFileError (..), receiveSbFile, sen import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto.Lazy as LC import Simplex.Messaging.Encoding -import Simplex.Messaging.Util (liftEitherError, liftEitherWith) +import Simplex.Messaging.Util (liftError', liftEitherWith) import Simplex.RemoteControl.Types (RCErrorType (..)) import UnliftIO import UnliftIO.Directory (getFileSize) -type EncryptedFile = ((Handle, Word32), C.CbNonce, LC.SbState) +type EncryptedFile = ((Handle, Word32), LC.SbState) -prepareEncryptedFile :: RemoteCrypto -> (Handle, Word32) -> ExceptT RemoteProtocolError IO EncryptedFile -prepareEncryptedFile RemoteCrypto {drg, hybridKey} f = do - nonce <- atomically $ C.randomCbNonce drg - sbState <- liftEitherWith (const $ PRERemoteControl RCEEncrypt) $ LC.kcbInit hybridKey nonce - pure (f, nonce, sbState) +prepareEncryptedFile :: C.SbKeyNonce -> (Handle, Word32) -> ExceptT RemoteProtocolError IO EncryptedFile +prepareEncryptedFile (sk, nonce) f = do + sbState <- liftEitherWith (const $ PRERemoteControl RCEEncrypt) $ LC.sbInit sk nonce + pure (f, sbState) sendEncryptedFile :: EncryptedFile -> (Builder -> IO ()) -> IO () -sendEncryptedFile ((h, sz), nonce, sbState) send = do - send $ byteString $ smpEncode ('\x01', nonce, sz + fromIntegral C.authTagSize) +sendEncryptedFile ((h, sz), sbState) send = do + send $ byteString $ smpEncode ('\x01', sz + fromIntegral C.authTagSize) sendEncFile h send sbState sz -receiveEncryptedFile :: RemoteCrypto -> (Int -> IO ByteString) -> Word32 -> FileDigest -> FilePath -> ExceptT RemoteProtocolError IO () -receiveEncryptedFile RemoteCrypto {hybridKey} getChunk fileSize fileDigest toPath = do +receiveEncryptedFile :: C.SbKeyNonce -> (Int -> IO ByteString) -> Word32 -> FileDigest -> FilePath -> ExceptT RemoteProtocolError IO () +receiveEncryptedFile (sk, nonce) getChunk fileSize fileDigest toPath = do c <- liftIO $ getChunk 1 unless (c == "\x01") $ throwError RPENoFile - nonce <- liftEitherError RPEInvalidBody $ smpDecode <$> getChunk 24 - size <- liftEitherError RPEInvalidBody $ smpDecode <$> getChunk 4 + size <- liftError' RPEInvalidBody $ smpDecode <$> getChunk 4 unless (size == fileSize + fromIntegral C.authTagSize) $ throwError RPEFileSize - sbState <- liftEitherWith (const $ PRERemoteControl RCEDecrypt) $ LC.kcbInit hybridKey nonce - liftEitherError fErr $ withFile toPath WriteMode $ \h -> receiveSbFile getChunk h sbState fileSize + sbState <- liftEitherWith (const $ PRERemoteControl RCEDecrypt) $ LC.sbInit sk nonce + liftError' fErr $ withFile toPath WriteMode $ \h -> receiveSbFile getChunk h sbState fileSize digest <- liftIO $ LC.sha512Hash <$> LB.readFile toPath unless (FileDigest digest == fileDigest) $ throwError RPEFileDigest where diff --git a/src/Simplex/Chat/Remote/Types.hs b/src/Simplex/Chat/Remote/Types.hs index d85dde9e87..a5396e7945 100644 --- a/src/Simplex/Chat/Remote/Types.hs +++ b/src/Simplex/Chat/Remote/Types.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} @@ -11,22 +12,22 @@ module Simplex.Chat.Remote.Types where import Control.Concurrent.Async (Async) -import Control.Concurrent.STM (TVar) +import Control.Concurrent.STM import Control.Exception (Exception) -import Crypto.Random (ChaChaDRG) +import Control.Monad (when) import qualified Data.Aeson.TH as J import Data.ByteString (ByteString) import Data.Int (Int64) import Data.Text (Text) -import Data.Word (Word16) +import Data.Word (Word16, Word32) import Simplex.Chat.Remote.AppVersion import Simplex.Chat.Types (verificationCode) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile) -import Simplex.Messaging.Crypto.SNTRUP761 (KEMHybridSecret) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON) -import Simplex.Messaging.Transport (TLS (..)) +import Simplex.Messaging.Transport (TLS (..), TSbChainKeys (..)) import Simplex.Messaging.Transport.HTTP2.Client (HTTP2Client) +import qualified Simplex.Messaging.TMap as TM import Simplex.RemoteControl.Client import Simplex.RemoteControl.Types @@ -40,13 +41,45 @@ data RemoteHostClient = RemoteHostClient } data RemoteCrypto = RemoteCrypto - { drg :: TVar ChaChaDRG, - counter :: TVar Int64, - sessionCode :: ByteString, - hybridKey :: KEMHybridSecret, + { sessionCode :: ByteString, + sndCounter :: TVar Word32, + rcvCounter :: TVar Word32, + chainKeys :: TSbChainKeys, + skippedKeys :: TM.TMap Word32 (C.SbKeyNonce, C.SbKeyNonce), signatures :: RemoteSignatures } +getRemoteSndKeys :: RemoteCrypto -> STM (Word32, C.SbKeyNonce, C.SbKeyNonce) +getRemoteSndKeys RemoteCrypto {sndCounter, chainKeys = TSbChainKeys {sndKey}} = do + corrId <- stateTVar sndCounter $ \c -> let !c' = c + 1 in (c', c') + cmdKN <- stateTVar sndKey C.sbcHkdf + fileKN <- stateTVar sndKey C.sbcHkdf + pure (corrId, cmdKN, fileKN) + +getRemoteRcvKeys :: RemoteCrypto -> Word32 -> STM (Either RemoteProtocolError (C.SbKeyNonce, C.SbKeyNonce)) +getRemoteRcvKeys RemoteCrypto {rcvCounter, chainKeys = TSbChainKeys {rcvKey}, skippedKeys} !corrId = + readTVar rcvCounter >>= getRcvKeys + where + getRcvKeys prevCorrId + | prevCorrId > corrId = + let err = PREEarlierId $ prevCorrId - corrId + in maybe (Left err) Right <$> TM.lookupDelete corrId skippedKeys + | prevCorrId == corrId = + pure $ Left PREDuplicateId + | prevCorrId + maxSkip < corrId = + pure $ Left $ RPEManySkippedIds (corrId - prevCorrId) + | otherwise = do -- prevCorrId < corrId + writeTVar rcvCounter corrId + skipKeys (prevCorrId + 1) + Right <$> getKeys + maxSkip = 256 + getKeys = (,) <$> stateTVar rcvKey C.sbcHkdf <*> stateTVar rcvKey C.sbcHkdf + skipKeys !cId = + when (cId < corrId) $ do + keys <- getKeys + TM.insert cId keys skippedKeys + skipKeys (cId + 1) + data RemoteSignatures = RSSign { idPrivKey :: C.PrivateKeyEd25519, @@ -110,6 +143,9 @@ data RemoteProtocolError | RPENoFile | RPEFileSize | RPEFileDigest + | RPEManySkippedIds Word32 + | PREEarlierId Word32 + | PREDuplicateId | -- | Wrong response received for the command sent RPEUnexpectedResponse {response :: Text} | -- | A file already exists in the destination position diff --git a/src/Simplex/Chat/Stats.hs b/src/Simplex/Chat/Stats.hs new file mode 100644 index 0000000000..21ad25b311 --- /dev/null +++ b/src/Simplex/Chat/Stats.hs @@ -0,0 +1,353 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} + +module Simplex.Chat.Stats where + +import qualified Data.Aeson.TH as J +import Data.List (partition) +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as M +import Data.Maybe (fromMaybe, isJust) +import Data.Time.Clock (UTCTime) +import Simplex.Chat.Types +import Simplex.Messaging.Agent.Client +import Simplex.Messaging.Agent.Protocol (UserId) +import Simplex.Messaging.Agent.Stats +import Simplex.Messaging.Parsers (defaultJSON) +import Simplex.Messaging.Protocol + +data PresentedServersSummary = PresentedServersSummary + { statsStartedAt :: UTCTime, + allUsersSMP :: SMPServersSummary, + allUsersXFTP :: XFTPServersSummary, + allUsersNtf :: NtfServersSummary, + currentUserSMP :: SMPServersSummary, + currentUserXFTP :: XFTPServersSummary, + currentUserNtf :: NtfServersSummary + } + deriving (Show) + +-- Presentation of servers will be split into separate categories, +-- so users can differentiate currently used (connected) servers, +-- previously connected servers that were in use in previous sessions, +-- and servers that are only proxied (not connected directly). +data SMPServersSummary = SMPServersSummary + { -- SMP totals are calculated from all accounted SMP server summaries + smpTotals :: SMPTotals, + -- currently used SMP servers are those with Just in sessions and/or subs in SMPServerSummary; + -- all other servers would fall either into previously used or only proxied servers category + currentlyUsedSMPServers :: [SMPServerSummary], + -- previously used SMP servers are those with Nothing in sessions and subs, + -- and have any of sentDirect, sentProxied, recvMsgs, etc. > 0 in server stats (see toPresentedServersSummary); + -- remaining servers would fall into only proxied servers category + previouslyUsedSMPServers :: [SMPServerSummary], + -- only proxied SMP servers are those that aren't (according to current state - sessions and subs) + -- and weren't (according to stats) connected directly; they would have Nothing in sessions and subs, + -- and have all of sentDirect, sentProxied, recvMsgs, etc. = 0 in server stats + onlyProxiedSMPServers :: [SMPServerSummary] + } + deriving (Show) + +data SMPTotals = SMPTotals + { sessions :: ServerSessions, + subs :: SMPServerSubs, + stats :: AgentSMPServerStatsData + } + deriving (Show) + +data SMPServerSummary = SMPServerSummary + { smpServer :: SMPServer, + -- known: + -- for simplicity always Nothing in totalServersSummary - allows us to load configured servers only for current user, + -- and also unnecessary unless we want to add navigation to other users servers settings; + -- always Just in currentUserServers - True if server is in list of user servers, otherwise False; + -- True - allows to navigate to server settings, False - allows to add server to configured as known (SEKnown) + known :: Maybe Bool, + sessions :: Maybe ServerSessions, + subs :: Maybe SMPServerSubs, + -- stats: + -- even if sessions and subs are Nothing, stats can be Just - server could be used earlier in session, + -- or in previous sessions and stats for it were restored; server would fall into a category of + -- previously used or only proxied servers - see ServersSummary above + stats :: Maybe AgentSMPServerStatsData + } + deriving (Show) + +data XFTPServersSummary = XFTPServersSummary + { -- XFTP totals are calculated from all accounted XFTP server summaries + xftpTotals :: XFTPTotals, + -- currently used XFTP servers are those with Just in sessions in XFTPServerSummary, + -- and/or have upload/download/deletion in progress; + -- all other servers would fall into previously used servers category + currentlyUsedXFTPServers :: [XFTPServerSummary], + -- previously used XFTP servers are those with Nothing in sessions and don't have any process in progress + previouslyUsedXFTPServers :: [XFTPServerSummary] + } + deriving (Show) + +data XFTPTotals = XFTPTotals + { sessions :: ServerSessions, + stats :: AgentXFTPServerStatsData + } + deriving (Show) + +data XFTPServerSummary = XFTPServerSummary + { xftpServer :: XFTPServer, + known :: Maybe Bool, -- same as for SMPServerSummary + sessions :: Maybe ServerSessions, + stats :: Maybe AgentXFTPServerStatsData, + rcvInProgress :: Bool, + sndInProgress :: Bool, + delInProgress :: Bool + } + deriving (Show) + +data NtfServersSummary = NtfServersSummary + { ntfTotals :: NtfTotals, + currentlyUsedNtfServers :: [NtfServerSummary], + previouslyUsedNtfServers :: [NtfServerSummary] + } + deriving (Show) + +data NtfTotals = NtfTotals + { sessions :: ServerSessions, + stats :: AgentNtfServerStatsData + } + deriving (Show) + +data NtfServerSummary = NtfServerSummary + { ntfServer :: NtfServer, + known :: Maybe Bool, + sessions :: Maybe ServerSessions, + stats :: Maybe AgentNtfServerStatsData + } + deriving (Show) + +-- Maps AgentServersSummary to PresentedServersSummary: +-- - currentUserServers is for currentUser; +-- - users are passed to exclude hidden users from totalServersSummary; +-- - if currentUser is hidden, it should be accounted in totalServersSummary; +-- - known is set only in user level summaries based on passed userSMPSrvs and userXFTPSrvs +toPresentedServersSummary :: AgentServersSummary -> [User] -> User -> [SMPServer] -> [XFTPServer] -> [NtfServer] -> PresentedServersSummary +toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrvs userNtfSrvs = do + let (userSMPSrvsSumms, allSMPSrvsSumms) = accSMPSrvsSummaries + (userSMPCurr, userSMPPrev, userSMPProx) = smpSummsIntoCategories userSMPSrvsSumms + (allSMPCurr, allSMPPrev, allSMPProx) = smpSummsIntoCategories allSMPSrvsSumms + let (userXFTPSrvsSumms, allXFTPSrvsSumms) = accXFTPSrvsSummaries + (userXFTPCurr, userXFTPPrev) = xftpSummsIntoCategories userXFTPSrvsSumms + (allXFTPCurr, allXFTPPrev) = xftpSummsIntoCategories allXFTPSrvsSumms + let (userNtfSrvsSumms, allNtfSrvsSumms) = accNtfSrvsSummaries + (userNtfCurr, userNtfPrev) = ntfSummsIntoCategories userNtfSrvsSumms + (allNtfCurr, allNtfPrev) = ntfSummsIntoCategories allNtfSrvsSumms + PresentedServersSummary + { statsStartedAt, + allUsersSMP = + SMPServersSummary + { smpTotals = accSMPTotals allSMPSrvsSumms, + currentlyUsedSMPServers = allSMPCurr, + previouslyUsedSMPServers = allSMPPrev, + onlyProxiedSMPServers = allSMPProx + }, + allUsersXFTP = + XFTPServersSummary + { xftpTotals = accXFTPTotals allXFTPSrvsSumms, + currentlyUsedXFTPServers = allXFTPCurr, + previouslyUsedXFTPServers = allXFTPPrev + }, + allUsersNtf = + NtfServersSummary + { ntfTotals = accNtfTotals allNtfSrvsSumms, + currentlyUsedNtfServers = allNtfCurr, + previouslyUsedNtfServers = allNtfPrev + }, + currentUserSMP = + SMPServersSummary + { smpTotals = accSMPTotals userSMPSrvsSumms, + currentlyUsedSMPServers = userSMPCurr, + previouslyUsedSMPServers = userSMPPrev, + onlyProxiedSMPServers = userSMPProx + }, + currentUserXFTP = + XFTPServersSummary + { xftpTotals = accXFTPTotals userXFTPSrvsSumms, + currentlyUsedXFTPServers = userXFTPCurr, + previouslyUsedXFTPServers = userXFTPPrev + }, + currentUserNtf = + NtfServersSummary + { ntfTotals = accNtfTotals userNtfSrvsSumms, + currentlyUsedNtfServers = userNtfCurr, + previouslyUsedNtfServers = userNtfPrev + } + } + where + AgentServersSummary {statsStartedAt, smpServersSessions, smpServersSubs, smpServersStats, xftpServersSessions, xftpServersStats, xftpRcvInProgress, xftpSndInProgress, xftpDelInProgress, ntfServersSessions, ntfServersStats} = agentSummary + countUserInAll auId = countUserInAllStats (AgentUserId auId) currentUser users + accSMPTotals :: Map SMPServer SMPServerSummary -> SMPTotals + accSMPTotals = M.foldr' addTotals initialTotals + where + initialTotals = SMPTotals {sessions = ServerSessions 0 0 0, subs = SMPServerSubs 0 0, stats = newAgentSMPServerStatsData} + addTotals SMPServerSummary {sessions, subs, stats} SMPTotals {sessions = accSess, subs = accSubs, stats = accStats} = + SMPTotals + { sessions = maybe accSess (accSess `addServerSessions`) sessions, + subs = maybe accSubs (accSubs `addSMPSubs`) subs, + stats = maybe accStats (accStats `addSMPStatsData`) stats + } + accXFTPTotals :: Map XFTPServer XFTPServerSummary -> XFTPTotals + accXFTPTotals = M.foldr' addTotals initialTotals + where + initialTotals = XFTPTotals {sessions = ServerSessions 0 0 0, stats = newAgentXFTPServerStatsData} + addTotals XFTPServerSummary {sessions, stats} XFTPTotals {sessions = accSess, stats = accStats} = + XFTPTotals + { sessions = maybe accSess (accSess `addServerSessions`) sessions, + stats = maybe accStats (accStats `addXFTPStatsData`) stats + } + accNtfTotals :: Map NtfServer NtfServerSummary -> NtfTotals + accNtfTotals = M.foldr' addTotals initialTotals + where + initialTotals = NtfTotals {sessions = ServerSessions 0 0 0, stats = newAgentNtfServerStatsData} + addTotals NtfServerSummary {sessions, stats} NtfTotals {sessions = accSess, stats = accStats} = + NtfTotals + { sessions = maybe accSess (accSess `addServerSessions`) sessions, + stats = maybe accStats (accStats `addNtfStatsData`) stats + } + smpSummsIntoCategories :: Map SMPServer SMPServerSummary -> ([SMPServerSummary], [SMPServerSummary], [SMPServerSummary]) + smpSummsIntoCategories = M.foldr' addSummary ([], [], []) + where + addSummary srvSumm (curr, prev, prox) + | isCurrentlyUsed srvSumm = (srvSumm : curr, prev, prox) + | isPreviouslyUsed srvSumm = (curr, srvSumm : prev, prox) + | otherwise = (curr, prev, srvSumm : prox) + isCurrentlyUsed SMPServerSummary {sessions, subs} = isJust sessions || isJust subs + isPreviouslyUsed SMPServerSummary {stats} = case stats of + Nothing -> False + -- add connCompleted, connDeleted? + -- check: should connCompleted be counted for proxy? is it? + Just AgentSMPServerStatsData {_sentDirect, _sentProxied, _sentDirectAttempts, _sentProxiedAttempts, _recvMsgs, _connCreated, _connSecured, _connSubscribed, _connSubAttempts} -> + _sentDirect > 0 || _sentProxied > 0 || _sentDirectAttempts > 0 || _sentProxiedAttempts > 0 || _recvMsgs > 0 || _connCreated > 0 || _connSecured > 0 || _connSubscribed > 0 || _connSubAttempts > 0 + xftpSummsIntoCategories :: Map XFTPServer XFTPServerSummary -> ([XFTPServerSummary], [XFTPServerSummary]) + xftpSummsIntoCategories = partition isCurrentlyUsed . M.elems + where + isCurrentlyUsed XFTPServerSummary {sessions, rcvInProgress, sndInProgress, delInProgress} = + isJust sessions || rcvInProgress || sndInProgress || delInProgress + ntfSummsIntoCategories :: Map NtfServer NtfServerSummary -> ([NtfServerSummary], [NtfServerSummary]) + ntfSummsIntoCategories = partition isCurrentlyUsed . M.elems + where + isCurrentlyUsed NtfServerSummary {sessions} = isJust sessions + accSMPSrvsSummaries :: (Map SMPServer SMPServerSummary, Map SMPServer SMPServerSummary) + accSMPSrvsSummaries = M.foldrWithKey' (addServerData addStats) summs2 smpServersStats + where + summs1 = M.foldrWithKey' (addServerData addSessions) (M.empty, M.empty) smpServersSessions + summs2 = M.foldrWithKey' (addServerData addSubs) summs1 smpServersSubs + addServerData = addServerData_ newSummary newUserSummary + newUserSummary srv = (newSummary srv :: SMPServerSummary) {known = Just $ srv `elem` userSMPSrvs} + newSummary srv = + SMPServerSummary + { smpServer = srv, + known = Nothing, + sessions = Nothing, + subs = Nothing, + stats = Nothing + } + addSessions :: ServerSessions -> SMPServerSummary -> SMPServerSummary + addSessions s summ@SMPServerSummary {sessions} = summ {sessions = Just $ maybe s (s `addServerSessions`) sessions} + addSubs :: SMPServerSubs -> SMPServerSummary -> SMPServerSummary + addSubs s summ@SMPServerSummary {subs} = summ {subs = Just $ maybe s (s `addSMPSubs`) subs} + addStats :: AgentSMPServerStatsData -> SMPServerSummary -> SMPServerSummary + addStats s summ@SMPServerSummary {stats} = summ {stats = Just $ maybe s (s `addSMPStatsData`) stats} + accXFTPSrvsSummaries :: (Map XFTPServer XFTPServerSummary, Map XFTPServer XFTPServerSummary) + accXFTPSrvsSummaries = M.foldrWithKey' (addServerData addStats) summs1 xftpServersStats + where + summs1 = M.foldrWithKey' (addServerData addSessions) (M.empty, M.empty) xftpServersSessions + addServerData = addServerData_ newSummary newUserSummary + addSessions :: ServerSessions -> XFTPServerSummary -> XFTPServerSummary + addSessions s summ@XFTPServerSummary {sessions} = summ {sessions = Just $ maybe s (s `addServerSessions`) sessions} + addStats :: AgentXFTPServerStatsData -> XFTPServerSummary -> XFTPServerSummary + addStats s summ@XFTPServerSummary {stats} = summ {stats = Just $ maybe s (s `addXFTPStatsData`) stats} + newUserSummary srv = (newSummary srv :: XFTPServerSummary) {known = Just $ srv `elem` userXFTPSrvs} + newSummary srv = + XFTPServerSummary + { xftpServer = srv, + known = Nothing, + sessions = Nothing, + stats = Nothing, + rcvInProgress = srv `elem` xftpRcvInProgress, + sndInProgress = srv `elem` xftpSndInProgress, + delInProgress = srv `elem` xftpDelInProgress + } + accNtfSrvsSummaries :: (Map NtfServer NtfServerSummary, Map NtfServer NtfServerSummary) + accNtfSrvsSummaries = M.foldrWithKey' (addServerData addStats) summs1 ntfServersStats + where + summs1 = M.foldrWithKey' (addServerData addSessions) (M.empty, M.empty) ntfServersSessions + addServerData = addServerData_ newSummary newUserSummary + addSessions :: ServerSessions -> NtfServerSummary -> NtfServerSummary + addSessions s summ@NtfServerSummary {sessions} = summ {sessions = Just $ maybe s (s `addServerSessions`) sessions} + addStats :: AgentNtfServerStatsData -> NtfServerSummary -> NtfServerSummary + addStats s summ@NtfServerSummary {stats} = summ {stats = Just $ maybe s (s `addNtfStatsData`) stats} + newUserSummary srv = (newSummary srv :: NtfServerSummary) {known = Just $ srv `elem` userNtfSrvs} + newSummary srv = + NtfServerSummary + { ntfServer = srv, + known = Nothing, + sessions = Nothing, + stats = Nothing + } + addServerData_ :: + (ProtocolServer p -> s) -> + (ProtocolServer p -> s) -> + (a -> s -> s) -> + (UserId, ProtocolServer p) -> + a -> + (Map (ProtocolServer p) s, Map (ProtocolServer p) s) -> + (Map (ProtocolServer p) s, Map (ProtocolServer p) s) + addServerData_ newSummary newUserSummary addData (userId, srv) d (userSumms, allUsersSumms) = (userSumms', allUsersSumms') + where + userSumms' + | userId == aUserId currentUser = alterSumms (newUserSummary srv) userSumms + | otherwise = userSumms + allUsersSumms' + | countUserInAll userId = alterSumms (newSummary srv) allUsersSumms + | otherwise = allUsersSumms + alterSumms n = M.alter (Just . addData d . fromMaybe n) srv + addServerSessions :: ServerSessions -> ServerSessions -> ServerSessions + addServerSessions ss1 ss2 = + ServerSessions + { ssConnected = ssConnected ss1 + ssConnected ss2, + ssErrors = ssErrors ss1 + ssErrors ss2, + ssConnecting = ssConnecting ss1 + ssConnecting ss2 + } + +countUserInAllStats :: AgentUserId -> User -> [User] -> Bool +countUserInAllStats (AgentUserId auId) currentUser users = + auId == aUserId currentUser || auId `notElem` hiddenUserIds + where + hiddenUserIds = map aUserId $ filter (isJust . viewPwdHash) users + +addSMPSubs :: SMPServerSubs -> SMPServerSubs -> SMPServerSubs +addSMPSubs ss1 ss2 = + SMPServerSubs + { ssActive = ssActive ss1 + ssActive ss2, + ssPending = ssPending ss1 + ssPending ss2 + } + +$(J.deriveJSON defaultJSON ''SMPTotals) + +$(J.deriveJSON defaultJSON ''SMPServerSummary) + +$(J.deriveJSON defaultJSON ''SMPServersSummary) + +$(J.deriveJSON defaultJSON ''XFTPTotals) + +$(J.deriveJSON defaultJSON ''XFTPServerSummary) + +$(J.deriveJSON defaultJSON ''XFTPServersSummary) + +$(J.deriveJSON defaultJSON ''NtfTotals) + +$(J.deriveJSON defaultJSON ''NtfServerSummary) + +$(J.deriveJSON defaultJSON ''NtfServersSummary) + +$(J.deriveJSON defaultJSON ''PresentedServersSummary) diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 91021713b1..a41641f88d 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -1,28 +1,29 @@ +{-# LANGUAGE CPP #-} + module Simplex.Chat.Store - ( SQLiteStore, + ( DBStore, StoreError (..), + ChatLockEntity (..), UserMsgReceiptSettings (..), UserContactLink (..), + GroupLinkInfo (..), AutoAccept (..), createChatStore, migrations, -- used in tests - chatStoreFile, - agentStoreFile, withTransaction, ) where -import Data.ByteArray (ScrubbedBytes) -import Simplex.Chat.Store.Migrations import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Shared -import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, MigrationError, SQLiteStore (..), createSQLiteStore, withTransaction) +import Simplex.Messaging.Agent.Store.Common (DBStore (..), withTransaction) +import Simplex.Messaging.Agent.Store.Interface (DBOpts, createDBStore) +import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation, MigrationError) +#if defined(dbPostgres) +import Simplex.Chat.Store.Postgres.Migrations +#else +import Simplex.Chat.Store.SQLite.Migrations +#endif -createChatStore :: FilePath -> ScrubbedBytes -> Bool -> MigrationConfirmation -> IO (Either MigrationError SQLiteStore) -createChatStore dbPath key keepKey = createSQLiteStore dbPath key keepKey migrations - -chatStoreFile :: FilePath -> FilePath -chatStoreFile = (<> "_chat.db") - -agentStoreFile :: FilePath -> FilePath -agentStoreFile = (<> "_agent.db") +createChatStore :: DBOpts -> MigrationConfirmation -> IO (Either MigrationError DBStore) +createChatStore dbCreateOpts = createDBStore dbCreateOpts migrations diff --git a/src/Simplex/Chat/Store/AppSettings.hs b/src/Simplex/Chat/Store/AppSettings.hs new file mode 100644 index 0000000000..dbdd538cf4 --- /dev/null +++ b/src/Simplex/Chat/Store/AppSettings.hs @@ -0,0 +1,27 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE OverloadedStrings #-} + +module Simplex.Chat.Store.AppSettings where + +import Control.Monad (join) +import Control.Monad.IO.Class (liftIO) +import qualified Data.Aeson as J +import Data.Maybe (fromMaybe) +import Simplex.Chat.AppSettings (AppSettings (..), combineAppSettings, defaultAppSettings, defaultParseAppSettings) +import Simplex.Messaging.Agent.Store.AgentStore (maybeFirstRow) +import qualified Simplex.Messaging.Agent.Store.DB as DB +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (Only (..)) +#else +import Database.SQLite.Simple (Only (..)) +#endif + +saveAppSettings :: DB.Connection -> AppSettings -> IO () +saveAppSettings db appSettings = do + DB.execute_ db "DELETE FROM app_settings" + DB.execute db "INSERT INTO app_settings (app_settings) VALUES (?)" (Only $ J.encode appSettings) + +getAppSettings :: DB.Connection -> Maybe AppSettings -> IO AppSettings +getAppSettings db platformDefaults = do + stored_ <- join <$> liftIO (maybeFirstRow (J.decodeStrict . fromOnly) $ DB.query_ db "SELECT app_settings FROM app_settings") + pure $ combineAppSettings (fromMaybe defaultAppSettings platformDefaults) (fromMaybe defaultParseAppSettings stored_) diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 580120b2ae..5c177969b9 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -1,43 +1,79 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeOperators #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module Simplex.Chat.Store.Connections - ( getConnectionEntity, + ( getChatLockEntity, + getConnectionEntity, getConnectionEntityByConnReq, + getConnectionEntityViaShortLink, getContactConnEntityByConnReqHash, getConnectionsToSubscribe, unsetConnectionToSubscribe, + deleteConnectionRecord, ) where import Control.Applicative ((<|>)) import Control.Monad import Control.Monad.Except +import Control.Monad.IO.Class +import Data.Bitraversable (bitraverse) import Data.Int (Int64) import Data.Maybe (catMaybes, fromMaybe) -import Data.Text (Text) -import Data.Time.Clock (UTCTime (..)) -import Database.SQLite.Simple (Only (..), (:.) (..)) -import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Protocol +import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Files import Simplex.Chat.Store.Groups import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Shared import Simplex.Chat.Types -import Simplex.Chat.Types.Preferences -import Simplex.Messaging.Agent.Protocol (ConnId) -import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow) -import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Messaging.Agent.Protocol (ConnId, ConnShortLink, ConnectionMode (..)) +import Simplex.Messaging.Agent.Store.AgentStore (firstRow, firstRow', maybeFirstRow) +import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) +import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Util (eitherToMaybe) -import Simplex.Messaging.Version (VersionRange) +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (Only (..), (:.) (..)) +import Database.PostgreSQL.Simple.SqlQQ (sql) +#else +import Database.SQLite.Simple (Only (..), (:.) (..)) +import Database.SQLite.Simple.QQ (sql) +#endif -getConnectionEntity :: DB.Connection -> VersionRange -> User -> AgentConnId -> ExceptT StoreError IO ConnectionEntity +getChatLockEntity :: DB.Connection -> AgentConnId -> ExceptT StoreError IO ChatLockEntity +getChatLockEntity db agentConnId = do + ((connId, connType) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId)) <- + ExceptT . firstRow id (SEConnectionNotFound agentConnId) $ + DB.query + db + [sql| + SELECT connection_id, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id + FROM connections + WHERE agent_conn_id = ? + |] + (Only agentConnId) + let err = throwError $ SEInternalError $ "connection " <> show connType <> " without entity" + case connType of + ConnMember -> maybe err (fmap CLGroup . getMemberGroupId) groupMemberId + ConnContact -> pure $ maybe (CLConnection connId) CLContact contactId + ConnSndFile -> maybe err (pure . CLFile) sndFileId + ConnRcvFile -> maybe err (pure . CLFile) rcvFileId + ConnUserContact -> maybe err (pure . CLUserContact) userContactLinkId + where + getMemberGroupId :: GroupMemberId -> ExceptT StoreError IO GroupId + getMemberGroupId groupMemberId = + ExceptT . firstRow fromOnly (SEInternalError "group member connection group_id not found") $ + DB.query db "SELECT group_id FROM group_members WHERE group_member_id = ?" (Only groupMemberId) + +getConnectionEntity :: DB.Connection -> VersionRangeChat -> User -> AgentConnId -> ExceptT StoreError IO ConnectionEntity getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do c@Connection {connType, entityId} <- getConnection_ case entityId of @@ -55,68 +91,76 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do where getConnection_ :: ExceptT StoreError IO Connection getConnection_ = ExceptT $ do - firstRow toConnection (SEConnectionNotFound agentConnId) $ + firstRow (toConnection vr) (SEConnectionNotFound agentConnId) $ DB.query db [sql| SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, - conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter, - peer_chat_min_version, peer_chat_max_version + conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, + created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, + conn_chat_version, peer_chat_min_version, peer_chat_max_version FROM connections WHERE user_id = ? AND agent_conn_id = ? |] (userId, agentConnId) getContactRec_ :: Int64 -> Connection -> ExceptT StoreError IO Contact getContactRec_ contactId c = ExceptT $ do - toContact' contactId c - <$> DB.query + chatTags <- getDirectChatTags db contactId + firstRow (toContact' contactId c chatTags) (SEInternalError "referenced contact not found") $ + DB.query db [sql| SELECT - c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.via_group, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, - p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent + c.contact_profile_id, c.local_display_name, c.via_group, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, + p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0 |] (userId, contactId) - toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)] -> Either StoreError Contact - toContact' contactId conn [(profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)] = + toContact' :: Int64 -> Connection -> [ChatTagId] -> ContactRow' -> Contact + toContact' contactId conn chatTags ((profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. (contactGroupMemberId, BI contactGrpInvSent, uiThemes, BI chatDeleted, customData, chatItemTTL)) = let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} - chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} + chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn activeConn = Just conn - in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent} - toContact' _ _ _ = Left $ SEInternalError "referenced contact not found" + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember) - getGroupAndMember_ groupMemberId c = ExceptT $ do - firstRow (toGroupAndMember c) (SEInternalError "referenced group member not found") $ - DB.query - db - [sql| - SELECT - -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.created_at, g.updated_at, g.chat_ts, - -- GroupInfo {membership} - mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, - mu.member_status, mu.show_messages, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, - -- GroupInfo {membership = GroupMember {memberProfile}} - pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, - -- from GroupMember - m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences - FROM group_members m - JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) - JOIN groups g ON g.group_id = m.group_id - JOIN group_profiles gp USING (group_profile_id) - JOIN group_members mu ON g.group_id = mu.group_id - JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) - WHERE m.group_member_id = ? AND g.user_id = ? AND mu.contact_id = ? - |] - (groupMemberId, userId, userContactId) + getGroupAndMember_ groupMemberId c = do + gm <- + ExceptT $ + firstRow (toGroupAndMember c) (SEInternalError "referenced group member not found") $ + DB.query + db + [sql| + SELECT + -- GroupInfo + g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + -- GroupInfo {membership} + mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, + mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, + -- GroupInfo {membership = GroupMember {memberProfile}} + pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at, + -- from GroupMember + m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + JOIN groups g ON g.group_id = m.group_id + JOIN group_profiles gp USING (group_profile_id) + JOIN group_members mu ON g.group_id = mu.group_id + JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) + WHERE m.group_member_id = ? AND g.user_id = ? AND mu.contact_id = ? + |] + (groupMemberId, userId, userContactId) + liftIO $ bitraverse (addGroupChatTags db) pure gm toGroupAndMember :: Connection -> GroupInfoRow :. GroupMemberRow -> (GroupInfo, GroupMember) toGroupAndMember c (groupInfoRow :. memberRow) = - let groupInfo = toGroupInfo vr userContactId groupInfoRow + let groupInfo = toGroupInfo vr userContactId [] groupInfoRow member = toGroupMember userContactId memberRow in (groupInfo, (member :: GroupMember) {activeConn = Just c}) getConnSndFileTransfer_ :: Int64 -> Connection -> ExceptT StoreError IO SndFileTransfer @@ -155,18 +199,38 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do userContact_ [(cReq, groupId)] = Right UserContact {userContactLinkId, connReqContact = cReq, groupId} userContact_ _ = Left SEUserContactLinkNotFound -getConnectionEntityByConnReq :: DB.Connection -> VersionRange -> User -> (ConnReqInvitation, ConnReqInvitation) -> IO (Maybe ConnectionEntity) +getConnectionEntityByConnReq :: DB.Connection -> VersionRangeChat -> User -> (ConnReqInvitation, ConnReqInvitation) -> IO (Maybe ConnectionEntity) getConnectionEntityByConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) = do connId_ <- maybeFirstRow fromOnly $ DB.query db "SELECT agent_conn_id FROM connections WHERE user_id = ? AND conn_req_inv IN (?,?) LIMIT 1" (userId, cReqSchema1, cReqSchema2) maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db vr user) connId_ +getConnectionEntityViaShortLink :: DB.Connection -> VersionRangeChat -> User -> ConnShortLink 'CMInvitation -> IO (Maybe (ConnReqInvitation, ConnectionEntity)) +getConnectionEntityViaShortLink db vr user@User {userId} shortLink = fmap eitherToMaybe $ runExceptT $ do + (cReq, connId) <- ExceptT getConnReqConnId + (cReq,) <$> getConnectionEntity db vr user connId + where + getConnReqConnId = + firstRow' toConnReqConnId (SEInternalError "connection not found") $ + DB.query + db + [sql| + SELECT conn_req_inv, agent_conn_id + FROM connections + WHERE user_id = ? AND short_link_inv = ? LIMIT 1 + |] + (userId, shortLink) + -- cReq is Maybe - it is removed when connection is established + toConnReqConnId = \case + (Just cReq, connId) -> Right (cReq, connId) + _ -> Left $ SEInternalError "no connection request" + -- search connection for connection plan: -- multiple connections can have same via_contact_uri_hash if request was repeated; -- this function searches for latest connection with contact so that "known contact" plan would be chosen; -- deleted connections are filtered out to allow re-connecting via same contact address -getContactConnEntityByConnReqHash :: DB.Connection -> VersionRange -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe ConnectionEntity) +getContactConnEntityByConnReqHash :: DB.Connection -> VersionRangeChat -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe ConnectionEntity) getContactConnEntityByConnReqHash db vr user@User {userId} (cReqHash1, cReqHash2) = do connId_ <- maybeFirstRow fromOnly $ @@ -181,12 +245,12 @@ getContactConnEntityByConnReqHash db vr user@User {userId} (cReqHash1, cReqHash2 WHERE user_id = ? AND via_contact_uri_hash IN (?,?) AND conn_status != ? ORDER BY conn_ord DESC, created_at DESC LIMIT 1 - ) + ) c |] (userId, cReqHash1, cReqHash2, ConnDeleted) maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db vr user) connId_ -getConnectionsToSubscribe :: DB.Connection -> VersionRange -> IO ([ConnId], [ConnectionEntity]) +getConnectionsToSubscribe :: DB.Connection -> VersionRangeChat -> IO ([ConnId], [ConnectionEntity]) getConnectionsToSubscribe db vr = do aConnIds <- map fromOnly <$> DB.query_ db "SELECT agent_conn_id FROM connections where to_subscribe = 1" entities <- forM aConnIds $ \acId -> do @@ -199,3 +263,7 @@ getConnectionsToSubscribe db vr = do unsetConnectionToSubscribe :: DB.Connection -> IO () unsetConnectionToSubscribe db = DB.execute_ db "UPDATE connections SET to_subscribe = 0 WHERE to_subscribe = 1" + +deleteConnectionRecord :: DB.Connection -> User -> Int64 -> IO () +deleteConnectionRecord db User {userId} cId = do + DB.execute db "DELETE FROM connections WHERE user_id = ? AND connection_id = ?" (userId, cId) diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 7504f19c95..9318f62f76 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -1,8 +1,8 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -10,9 +10,13 @@ {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module Simplex.Chat.Store.Direct - ( updateContact_, + ( updateContactLDN_, updateContactProfile_, updateContactProfile_', + updateMemberContactProfileReset_', + updateMemberContactProfileReset_, + updateMemberContactProfile_, + updateMemberContactProfile_', deleteContactProfile_, deleteUnusedProfile_, @@ -27,10 +31,10 @@ module Simplex.Chat.Store.Direct getConnReqContactXContactId, getContactByConnReqHash, createDirectContact, - deleteContactConnectionsAndFiles, + deleteContactConnections, + deleteContactFiles, deleteContact, deleteContactWithoutGroups, - setContactDeleted, getDeletedContacts, getContactByName, getContact, @@ -41,30 +45,43 @@ module Simplex.Chat.Store.Direct updateContactConnectionAlias, updatePCCIncognito, deletePCCIncognitoProfile, - updateContactUsed, updateContactUnreadChat, setUserChatsRead, updateContactStatus, updateGroupUnreadChat, setConnectionVerified, - incConnectionAuthErrCounter, - setConnectionAuthErrCounter, + incAuthErrCounter, + setAuthErrCounter, + incQuotaErrCounter, + setQuotaErrCounter, getUserContacts, createOrUpdateContactRequest, + getUserContactLinkIdByCReq, getContactRequest', getContactRequest, getContactRequestIdByName, deleteContactRequest, createAcceptedContact, + deleteContactRequestRec, + updateContactAccepted, getUserByContactRequestId, getPendingContactConnections, getContactConnections, getConnectionById, getConnectionsContacts, updateConnectionStatus, + updateConnectionStatusFromTo, updateContactSettings, setConnConnReqInv, resetContactConnInitiated, + setContactCustomData, + setContactUIThemes, + setContactChatDeleted, + getDirectChatTags, + updateDirectChatTags, + setDirectChatTTL, + getDirectChatTTL, + getUserContactsToExpire ) where @@ -77,17 +94,26 @@ import Data.Int (Int64) import Data.Maybe (fromMaybe, isJust, isNothing) import Data.Text (Text) import Data.Time.Clock (UTCTime (..), getCurrentTime) -import Database.SQLite.Simple (NamedParam (..), Only (..), (:.) (..)) -import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Messages import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences -import Simplex.Messaging.Agent.Protocol (ConnId, InvitationId, UserId) -import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) -import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Chat.Types.UITheme +import Simplex.Messaging.Agent.Protocol (ConnId, CreatedConnLink (..), InvitationId, UserId) +import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) +import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) +import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Crypto.Ratchet (PQSupport) import Simplex.Messaging.Protocol (SubscriptionMode (..)) +import Simplex.Messaging.Util ((<$$>)) import Simplex.Messaging.Version +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (Only (..), (:.) (..)) +import Database.PostgreSQL.Simple.SqlQQ (sql) +#else +import Database.SQLite.Simple (Only (..), (:.) (..)) +import Database.SQLite.Simple.QQ (sql) +#endif getPendingContactConnection :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO PendingContactConnection getPendingContactConnection db userId connId = do @@ -95,7 +121,7 @@ getPendingContactConnection db userId connId = do DB.query db [sql| - SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at + SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at FROM connections WHERE user_id = ? AND connection_id = ? @@ -121,14 +147,14 @@ deletePendingContactConnection db userId connId = |] (userId, connId, ConnContact) -createAddressContactConnection :: DB.Connection -> User -> Contact -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> SubscriptionMode -> ExceptT StoreError IO Contact -createAddressContactConnection db user@User {userId} Contact {contactId} acId cReqHash xContactId incognitoProfile subMode = do - PendingContactConnection {pccConnId} <- liftIO $ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile Nothing subMode +createAddressContactConnection :: DB.Connection -> VersionRangeChat -> User -> Contact -> ConnId -> ConnReqUriHash -> Maybe ShortLinkContact -> XContactId -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> ExceptT StoreError IO (Int64, Contact) +createAddressContactConnection db vr user@User {userId} Contact {contactId} acId cReqHash sLnk xContactId incognitoProfile subMode chatV pqSup = do + PendingContactConnection {pccConnId} <- liftIO $ createConnReqConnection db userId acId cReqHash sLnk xContactId incognitoProfile Nothing subMode chatV pqSup liftIO $ DB.execute db "UPDATE connections SET contact_id = ? WHERE connection_id = ?" (contactId, pccConnId) - getContact db user contactId + (pccConnId,) <$> getContact db vr user contactId -createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> IO PendingContactConnection -createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId subMode = do +createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> Maybe ShortLinkContact -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection +createConnReqConnection db userId acId cReqHash sLnk xContactId incognitoProfile groupLinkId subMode chatV pqSup = do createdAt <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile let pccConnStatus = ConnJoined @@ -137,16 +163,20 @@ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile grou [sql| INSERT INTO connections ( user_id, agent_conn_id, conn_status, conn_type, contact_conn_initiated, - via_contact_uri_hash, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, created_at, updated_at, to_subscribe - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + via_contact_uri_hash, via_short_link_contact, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, + created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ((userId, acId, pccConnStatus, ConnContact, True, cReqHash, xContactId) :. (customUserProfileId, isJust groupLinkId, groupLinkId, createdAt, createdAt, subMode == SMOnlyCreate)) + ( (userId, acId, pccConnStatus, ConnContact, BI True, cReqHash, sLnk, xContactId) + :. (customUserProfileId, BI (isJust groupLinkId), groupLinkId) + :. (createdAt, createdAt, BI (subMode == SMOnlyCreate), chatV, pqSup, pqSup) + ) pccConnId <- insertedRowId db - pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connReqInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt} + pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connLinkInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt} -getConnReqContactXContactId :: DB.Connection -> User -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId) -getConnReqContactXContactId db user@User {userId} cReqHash = do - getContactByConnReqHash db user cReqHash >>= \case +getConnReqContactXContactId :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId) +getConnReqContactXContactId db vr user@User {userId} cReqHash = do + getContactByConnReqHash db vr user cReqHash >>= \case c@(Just _) -> pure (c, Nothing) Nothing -> (Nothing,) <$> getXContactId where @@ -158,31 +188,33 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do "SELECT xcontact_id FROM connections WHERE user_id = ? AND via_contact_uri_hash = ? LIMIT 1" (userId, cReqHash) -getContactByConnReqHash :: DB.Connection -> User -> ConnReqUriHash -> IO (Maybe Contact) -getContactByConnReqHash db user@User {userId} cReqHash = - maybeFirstRow (toContact user) $ - DB.query - db - [sql| - SELECT - -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, - -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, - c.peer_chat_min_version, c.peer_chat_max_version - FROM contacts ct - JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id - JOIN connections c ON c.contact_id = ct.contact_id - WHERE c.user_id = ? AND c.via_contact_uri_hash = ? AND ct.contact_status = ? AND ct.deleted = 0 - ORDER BY c.created_at DESC - LIMIT 1 - |] - (userId, cReqHash, CSActive) +getContactByConnReqHash :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> IO (Maybe Contact) +getContactByConnReqHash db vr user@User {userId} cReqHash = do + ct_ <- + maybeFirstRow (toContact vr user []) $ + DB.query + db + [sql| + SELECT + -- Contact + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, + -- Connection + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM contacts ct + JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id + JOIN connections c ON c.contact_id = ct.contact_id + WHERE c.user_id = ? AND c.via_contact_uri_hash = ? AND ct.contact_status = ? AND ct.deleted = 0 + ORDER BY c.created_at DESC + LIMIT 1 + |] + (userId, cReqHash, CSActive) + mapM (addDirectChatTags db) ct_ -createDirectConnection :: DB.Connection -> User -> ConnId -> ConnReqInvitation -> ConnStatus -> Maybe Profile -> SubscriptionMode -> IO PendingContactConnection -createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile subMode = do +createDirectConnection :: DB.Connection -> User -> ConnId -> CreatedLinkInvitation -> ConnStatus -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection +createDirectConnection db User {userId} acId ccLink@(CCLink cReq shortLinkInv) pccConnStatus incognitoProfile subMode chatV pqSup = do createdAt <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile let contactConnInitiated = pccConnStatus == ConnNew @@ -190,29 +222,55 @@ createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile db [sql| INSERT INTO connections - (user_id, agent_conn_id, conn_req_inv, conn_status, conn_type, contact_conn_initiated, custom_user_profile_id, created_at, updated_at, to_subscribe) VALUES (?,?,?,?,?,?,?,?,?,?) + (user_id, agent_conn_id, conn_req_inv, short_link_inv, conn_status, conn_type, contact_conn_initiated, custom_user_profile_id, + created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - (userId, acId, cReq, pccConnStatus, ConnContact, contactConnInitiated, customUserProfileId, createdAt, createdAt, subMode == SMOnlyCreate) + ( (userId, acId, cReq, shortLinkInv, pccConnStatus, ConnContact, BI contactConnInitiated, customUserProfileId) + :. (createdAt, createdAt, BI (subMode == SMOnlyCreate), chatV, pqSup, pqSup) + ) pccConnId <- insertedRowId db - pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, groupLinkId = Nothing, customUserProfileId, connReqInv = Just cReq, localAlias = "", createdAt, updatedAt = createdAt} + pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, groupLinkId = Nothing, customUserProfileId, connLinkInv = Just ccLink, localAlias = "", createdAt, updatedAt = createdAt} createIncognitoProfile :: DB.Connection -> User -> Profile -> IO Int64 createIncognitoProfile db User {userId} p = do createdAt <- getCurrentTime createIncognitoProfile_ db userId createdAt p -createDirectContact :: DB.Connection -> User -> Connection -> Profile -> Bool -> ExceptT StoreError IO Contact -createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p@Profile {preferences} contactUsed = do +createDirectContact :: DB.Connection -> User -> Connection -> Profile -> ExceptT StoreError IO Contact +createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p@Profile {preferences} = do currentTs <- liftIO getCurrentTime - (localDisplayName, contactId, profileId) <- createContact_ db userId p localAlias Nothing currentTs contactUsed + (localDisplayName, contactId, profileId) <- createContact_ db userId p localAlias Nothing currentTs liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, connId) let profile = toLocalProfile profileId p localAlias userPreferences = emptyChatPrefs mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn - pure $ Contact {contactId, localDisplayName, profile, activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False} + pure $ + Contact + { contactId, + localDisplayName, + profile, + activeConn = Just conn, + viaGroup = Nothing, + contactUsed = True, + contactStatus = CSActive, + chatSettings = defaultChatSettings, + userPreferences, + mergedPreferences, + createdAt = currentTs, + updatedAt = currentTs, + chatTs = Just currentTs, + contactGroupMemberId = Nothing, + contactGrpInvSent = False, + chatTags = [], + chatItemTTL = Nothing, + uiThemes = Nothing, + chatDeleted = False, + customData = Nothing + } -deleteContactConnectionsAndFiles :: DB.Connection -> UserId -> Contact -> IO () -deleteContactConnectionsAndFiles db userId Contact {contactId} = do +deleteContactConnections :: DB.Connection -> User -> Contact -> IO () +deleteContactConnections db User {userId} Contact {contactId} = do DB.execute db [sql| @@ -224,47 +282,52 @@ deleteContactConnectionsAndFiles db userId Contact {contactId} = do ) |] (userId, contactId) + +deleteContactFiles :: DB.Connection -> User -> Contact -> IO () +deleteContactFiles db User {userId} Contact {contactId} = do DB.execute db "DELETE FROM files WHERE user_id = ? AND contact_id = ?" (userId, contactId) -deleteContact :: DB.Connection -> User -> Contact -> IO () -deleteContact db user@User {userId} Contact {contactId, localDisplayName, activeConn} = do - DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND contact_id = ?" (userId, contactId) - ctMember :: (Maybe ContactId) <- maybeFirstRow fromOnly $ DB.query db "SELECT contact_id FROM group_members WHERE user_id = ? AND contact_id = ? LIMIT 1" (userId, contactId) - if isNothing ctMember - then do - deleteContactProfile_ db userId contactId - DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) - else do - currentTs <- getCurrentTime - DB.execute db "UPDATE group_members SET contact_id = NULL, updated_at = ? WHERE user_id = ? AND contact_id = ?" (currentTs, userId, contactId) - DB.execute db "DELETE FROM contacts WHERE user_id = ? AND contact_id = ?" (userId, contactId) - forM_ activeConn $ \Connection {customUserProfileId} -> - forM_ customUserProfileId $ \profileId -> - deleteUnusedIncognitoProfileById_ db user profileId +deleteContact :: DB.Connection -> User -> Contact -> ExceptT StoreError IO () +deleteContact db user@User {userId} ct@Contact {contactId, localDisplayName, activeConn} = do + assertNotUser db user ct + liftIO $ do + DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND contact_id = ?" (userId, contactId) + ctMember :: (Maybe ContactId) <- maybeFirstRow fromOnly $ DB.query db "SELECT contact_id FROM group_members WHERE contact_id = ? LIMIT 1" (Only contactId) + if isNothing ctMember + then do + deleteContactProfile_ db userId contactId + -- user's local display name already checked in assertNotUser + DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) + else do + currentTs <- getCurrentTime + DB.execute db "UPDATE group_members SET contact_id = NULL, updated_at = ? WHERE user_id = ? AND contact_id = ?" (currentTs, userId, contactId) + DB.execute db "DELETE FROM contacts WHERE user_id = ? AND contact_id = ?" (userId, contactId) + forM_ activeConn $ \Connection {customUserProfileId} -> + forM_ customUserProfileId $ \profileId -> + deleteUnusedIncognitoProfileById_ db user profileId -- should only be used if contact is not member of any groups -deleteContactWithoutGroups :: DB.Connection -> User -> Contact -> IO () -deleteContactWithoutGroups db user@User {userId} Contact {contactId, localDisplayName, activeConn} = do - DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND contact_id = ?" (userId, contactId) - deleteContactProfile_ db userId contactId - DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) - DB.execute db "DELETE FROM contacts WHERE user_id = ? AND contact_id = ?" (userId, contactId) - forM_ activeConn $ \Connection {customUserProfileId} -> - forM_ customUserProfileId $ \profileId -> - deleteUnusedIncognitoProfileById_ db user profileId +deleteContactWithoutGroups :: DB.Connection -> User -> Contact -> ExceptT StoreError IO () +deleteContactWithoutGroups db user@User {userId} ct@Contact {contactId, localDisplayName, activeConn} = do + assertNotUser db user ct + liftIO $ do + DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND contact_id = ?" (userId, contactId) + deleteContactProfile_ db userId contactId + -- user's local display name already checked in assertNotUser + DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) + DB.execute db "DELETE FROM contacts WHERE user_id = ? AND contact_id = ?" (userId, contactId) + forM_ activeConn $ \Connection {customUserProfileId} -> + forM_ customUserProfileId $ \profileId -> + deleteUnusedIncognitoProfileById_ db user profileId -setContactDeleted :: DB.Connection -> User -> Contact -> IO () -setContactDeleted db User {userId} Contact {contactId} = do - currentTs <- getCurrentTime - DB.execute db "UPDATE contacts SET deleted = 1, updated_at = ? WHERE user_id = ? AND contact_id = ?" (currentTs, userId, contactId) - -getDeletedContacts :: DB.Connection -> User -> IO [Contact] -getDeletedContacts db user@User {userId} = do +-- TODO remove in future versions: only used for legacy contact cleanup +getDeletedContacts :: DB.Connection -> VersionRangeChat -> User -> IO [Contact] +getDeletedContacts db vr user@User {userId} = do contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 1" (Only userId) - rights <$> mapM (runExceptT . getDeletedContact db user) contactIds + rights <$> mapM (runExceptT . getDeletedContact db vr user) contactIds -getDeletedContact :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO Contact -getDeletedContact db user contactId = getContact_ db user contactId True +getDeletedContact :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO Contact +getDeletedContact db vr user contactId = getContact_ db vr user contactId True deleteContactProfile_ :: DB.Connection -> UserId -> ContactId -> IO () deleteContactProfile_ db userId contactId = @@ -282,31 +345,33 @@ deleteContactProfile_ db userId contactId = deleteUnusedProfile_ :: DB.Connection -> UserId -> ProfileId -> IO () deleteUnusedProfile_ db userId profileId = - DB.executeNamed + DB.execute db [sql| DELETE FROM contact_profiles - WHERE user_id = :user_id AND contact_profile_id = :profile_id + WHERE user_id = ? AND contact_profile_id = ? AND 1 NOT IN ( SELECT 1 FROM connections - WHERE user_id = :user_id AND custom_user_profile_id = :profile_id LIMIT 1 + WHERE user_id = ? AND custom_user_profile_id = ? LIMIT 1 ) AND 1 NOT IN ( SELECT 1 FROM contacts - WHERE user_id = :user_id AND contact_profile_id = :profile_id LIMIT 1 + WHERE user_id = ? AND contact_profile_id = ? LIMIT 1 ) AND 1 NOT IN ( SELECT 1 FROM contact_requests - WHERE user_id = :user_id AND contact_profile_id = :profile_id LIMIT 1 + WHERE user_id = ? AND contact_profile_id = ? LIMIT 1 ) AND 1 NOT IN ( SELECT 1 FROM group_members - WHERE user_id = :user_id - AND (member_profile_id = :profile_id OR contact_profile_id = :profile_id) + WHERE user_id = ? + AND (member_profile_id = ? OR contact_profile_id = ?) LIMIT 1 ) |] - [":user_id" := userId, ":profile_id" := profileId] + ( (userId, profileId, userId, profileId, userId, profileId) + :. (userId, profileId, userId, profileId, profileId) + ) updateContactProfile :: DB.Connection -> User -> Contact -> Profile -> ExceptT StoreError IO Contact updateContactProfile db user@User {userId} c p' @@ -317,7 +382,7 @@ updateContactProfile db user@User {userId} c p' ExceptT . withLocalDisplayName db userId newName $ \ldn -> do currentTs <- getCurrentTime updateContactProfile_' db userId profileId p' currentTs - updateContact_ db userId contactId localDisplayName ldn currentTs + updateContactLDN_ db user contactId localDisplayName ldn currentTs pure $ Right c {localDisplayName = ldn, profile, mergedPreferences} where Contact {contactId, localDisplayName, profile = LocalProfile {profileId, displayName, localAlias}, userPreferences} = c @@ -384,21 +449,17 @@ deletePCCIncognitoProfile db User {userId} profileId = |] (userId, profileId) -updateContactUsed :: DB.Connection -> User -> Contact -> IO () -updateContactUsed db User {userId} Contact {contactId} = do - updatedAt <- getCurrentTime - DB.execute db "UPDATE contacts SET contact_used = 1, updated_at = ? WHERE user_id = ? AND contact_id = ?" (updatedAt, userId, contactId) - updateContactUnreadChat :: DB.Connection -> User -> Contact -> Bool -> IO () updateContactUnreadChat db User {userId} Contact {contactId} unreadChat = do updatedAt <- getCurrentTime - DB.execute db "UPDATE contacts SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (unreadChat, updatedAt, userId, contactId) + DB.execute db "UPDATE contacts SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (BI unreadChat, updatedAt, userId, contactId) setUserChatsRead :: DB.Connection -> User -> IO () setUserChatsRead db User {userId} = do updatedAt <- getCurrentTime - DB.execute db "UPDATE contacts SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND unread_chat = ?" (False, updatedAt, userId, True) - DB.execute db "UPDATE groups SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND unread_chat = ?" (False, updatedAt, userId, True) + DB.execute db "UPDATE contacts SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND unread_chat = ?" (BI False, updatedAt, userId, BI True) + DB.execute db "UPDATE groups SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND unread_chat = ?" (BI False, updatedAt, userId, BI True) + DB.execute db "UPDATE note_folders SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND unread_chat = ?" (BI False, updatedAt, userId, BI True) DB.execute db "UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND item_status = ?" (CISRcvRead, updatedAt, userId, CISRcvNew) updateContactStatus :: DB.Connection -> User -> Contact -> ContactStatus -> IO Contact @@ -417,26 +478,39 @@ updateContactStatus db User {userId} ct@Contact {contactId} contactStatus = do updateGroupUnreadChat :: DB.Connection -> User -> GroupInfo -> Bool -> IO () updateGroupUnreadChat db User {userId} GroupInfo {groupId} unreadChat = do updatedAt <- getCurrentTime - DB.execute db "UPDATE groups SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (unreadChat, updatedAt, userId, groupId) + DB.execute db "UPDATE groups SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (BI unreadChat, updatedAt, userId, groupId) setConnectionVerified :: DB.Connection -> User -> Int64 -> Maybe Text -> IO () setConnectionVerified db User {userId} connId code = do updatedAt <- getCurrentTime DB.execute db "UPDATE connections SET security_code = ?, security_code_verified_at = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (code, code $> updatedAt, updatedAt, userId, connId) -incConnectionAuthErrCounter :: DB.Connection -> User -> Connection -> IO Int -incConnectionAuthErrCounter db User {userId} Connection {connId, authErrCounter} = do +incAuthErrCounter :: DB.Connection -> User -> Connection -> IO Int +incAuthErrCounter db User {userId} Connection {connId, authErrCounter} = do updatedAt <- getCurrentTime (counter_ :: Maybe Int) <- maybeFirstRow fromOnly $ DB.query db "SELECT auth_err_counter FROM connections WHERE user_id = ? AND connection_id = ?" (userId, connId) let counter' = fromMaybe authErrCounter counter_ + 1 DB.execute db "UPDATE connections SET auth_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter', updatedAt, userId, connId) pure counter' -setConnectionAuthErrCounter :: DB.Connection -> User -> Connection -> Int -> IO () -setConnectionAuthErrCounter db User {userId} Connection {connId} counter = do +setAuthErrCounter :: DB.Connection -> User -> Connection -> Int -> IO () +setAuthErrCounter db User {userId} Connection {connId} counter = do updatedAt <- getCurrentTime DB.execute db "UPDATE connections SET auth_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter, updatedAt, userId, connId) +incQuotaErrCounter :: DB.Connection -> User -> Connection -> IO Int +incQuotaErrCounter db User {userId} Connection {connId, quotaErrCounter} = do + updatedAt <- getCurrentTime + (counter_ :: Maybe Int) <- maybeFirstRow fromOnly $ DB.query db "SELECT quota_err_counter FROM connections WHERE user_id = ? AND connection_id = ?" (userId, connId) + let counter' = fromMaybe quotaErrCounter counter_ + 1 + DB.execute db "UPDATE connections SET quota_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter', updatedAt, userId, connId) + pure counter' + +setQuotaErrCounter :: DB.Connection -> User -> Connection -> Int -> IO () +setQuotaErrCounter db User {userId} Connection {connId} counter = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE connections SET quota_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter, updatedAt, userId, connId) + updateContactProfile_ :: DB.Connection -> UserId -> ProfileId -> Profile -> IO () updateContactProfile_ db userId profileId profile = do currentTs <- getCurrentTime @@ -453,8 +527,42 @@ updateContactProfile_' db userId profileId Profile {displayName, fullName, image |] (displayName, fullName, image, contactLink, preferences, updatedAt, userId, profileId) -updateContact_ :: DB.Connection -> UserId -> Int64 -> ContactName -> ContactName -> UTCTime -> IO () -updateContact_ db userId contactId displayName newName updatedAt = do +-- update only member profile fields (when member doesn't have associated contact - we can reset contactLink and prefs) +updateMemberContactProfileReset_ :: DB.Connection -> UserId -> ProfileId -> Profile -> IO () +updateMemberContactProfileReset_ db userId profileId profile = do + currentTs <- getCurrentTime + updateMemberContactProfileReset_' db userId profileId profile currentTs + +updateMemberContactProfileReset_' :: DB.Connection -> UserId -> ProfileId -> Profile -> UTCTime -> IO () +updateMemberContactProfileReset_' db userId profileId Profile {displayName, fullName, image} updatedAt = do + DB.execute + db + [sql| + UPDATE contact_profiles + SET display_name = ?, full_name = ?, image = ?, contact_link = NULL, preferences = NULL, updated_at = ? + WHERE user_id = ? AND contact_profile_id = ? + |] + (displayName, fullName, image, updatedAt, userId, profileId) + +-- update only member profile fields (when member has associated contact - we keep contactLink and prefs) +updateMemberContactProfile_ :: DB.Connection -> UserId -> ProfileId -> Profile -> IO () +updateMemberContactProfile_ db userId profileId profile = do + currentTs <- getCurrentTime + updateMemberContactProfile_' db userId profileId profile currentTs + +updateMemberContactProfile_' :: DB.Connection -> UserId -> ProfileId -> Profile -> UTCTime -> IO () +updateMemberContactProfile_' db userId profileId Profile {displayName, fullName, image} updatedAt = do + DB.execute + db + [sql| + UPDATE contact_profiles + SET display_name = ?, full_name = ?, image = ?, updated_at = ? + WHERE user_id = ? AND contact_profile_id = ? + |] + (displayName, fullName, image, updatedAt, userId, profileId) + +updateContactLDN_ :: DB.Connection -> User -> Int64 -> ContactName -> ContactName -> UTCTime -> IO () +updateContactLDN_ db user@User {userId} contactId displayName newName updatedAt = do DB.execute db "UPDATE contacts SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" @@ -463,33 +571,37 @@ updateContact_ db userId contactId displayName newName updatedAt = do db "UPDATE group_members SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (newName, updatedAt, userId, contactId) - DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (displayName, userId) + safeDeleteLDN db user displayName -getContactByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Contact -getContactByName db user localDisplayName = do +getContactByName :: DB.Connection -> VersionRangeChat -> User -> ContactName -> ExceptT StoreError IO Contact +getContactByName db vr user localDisplayName = do cId <- getContactIdByName db user localDisplayName - getContact db user cId + getContact db vr user cId -getUserContacts :: DB.Connection -> User -> IO [Contact] -getUserContacts db user@User {userId} = do +getUserContacts :: DB.Connection -> VersionRangeChat -> User -> IO [Contact] +getUserContacts db vr user@User {userId} = do contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 0" (Only userId) - contacts <- rights <$> mapM (runExceptT . getContact db user) contactIds + contacts <- rights <$> mapM (runExceptT . getContact db vr user) contactIds pure $ filter (\Contact {activeConn} -> isJust activeConn) contacts -createOrUpdateContactRequest :: DB.Connection -> User -> Int64 -> InvitationId -> VersionRange -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest -createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (VersionRange minV maxV) Profile {displayName, fullName, image, contactLink, preferences} xContactId_ = - liftIO (maybeM getContact' xContactId_) >>= \case - Just contact -> pure $ CORContact contact +createOrUpdateContactRequest :: DB.Connection -> VersionRangeChat -> User -> Int64 -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> PQSupport -> ExceptT StoreError IO ChatOrRequest +createOrUpdateContactRequest db vr user@User {userId, userContactId} userContactLinkId invId (VersionRange minV maxV) Profile {displayName, fullName, image, contactLink, preferences} xContactId_ pqSup = + liftIO (maybeM getContactOrGroup xContactId_) >>= \case + Just cr -> pure cr Nothing -> CORRequest <$> createOrUpdate_ where maybeM = maybe (pure Nothing) + getContactOrGroup xContactId = + getContact' xContactId >>= \case + Just ct -> pure $ Just $ CORContact ct + Nothing -> CORGroup <$$> getGroupInfo' xContactId createOrUpdate_ :: ExceptT StoreError IO UserContactRequest createOrUpdate_ = do cReqId <- ExceptT $ maybeM getContactRequestByXContactId xContactId_ >>= \case Nothing -> createContactRequest - Just cr -> updateContactRequest cr $> Right cr.contactRequestId + Just cr@UserContactRequest {contactRequestId} -> updateContactRequest cr $> Right contactRequestId getContactRequest db user cReqId createContactRequest :: IO (Either StoreError Int64) createContactRequest = do @@ -506,33 +618,47 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers db [sql| INSERT INTO contact_requests - (user_contact_link_id, agent_invitation_id, peer_chat_min_version, peer_chat_max_version, contact_profile_id, local_display_name, user_id, created_at, updated_at, xcontact_id) - VALUES (?,?,?,?,?,?,?,?,?,?) + (user_contact_link_id, agent_invitation_id, peer_chat_min_version, peer_chat_max_version, contact_profile_id, local_display_name, user_id, + created_at, updated_at, xcontact_id, pq_support) + VALUES (?,?,?,?,?,?,?,?,?,?,?) |] - (userContactLinkId, invId, minV, maxV, profileId, ldn, userId, currentTs, currentTs, xContactId_) + ( (userContactLinkId, Binary invId, minV, maxV, profileId, ldn, userId) + :. (currentTs, currentTs, xContactId_, pqSup) + ) insertedRowId db getContact' :: XContactId -> IO (Maybe Contact) - getContact' xContactId = - maybeFirstRow (toContact user) $ - DB.query - db - [sql| - SELECT - -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, - -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, - c.peer_chat_min_version, c.peer_chat_max_version - FROM contacts ct - JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id - LEFT JOIN connections c ON c.contact_id = ct.contact_id - WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0 - ORDER BY c.created_at DESC - LIMIT 1 - |] - (userId, xContactId) + getContact' xContactId = do + ct_ <- + maybeFirstRow (toContact vr user []) $ + DB.query + db + [sql| + SELECT + -- Contact + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, + -- Connection + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM contacts ct + JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id + LEFT JOIN connections c ON c.contact_id = ct.contact_id + WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0 + ORDER BY c.created_at DESC + LIMIT 1 + |] + (userId, xContactId) + mapM (addDirectChatTags db) ct_ + getGroupInfo' :: XContactId -> IO (Maybe GroupInfo) + getGroupInfo' xContactId = do + g_ <- + maybeFirstRow (toGroupInfo vr userContactId []) $ + DB.query + db + (groupInfoQuery <> " WHERE g.business_xcontact_id = ? AND g.user_id = ? AND mu.contact_id = ?") + (xContactId, userId, userContactId) + mapM (addGroupChatTags db) g_ getContactRequestByXContactId :: XContactId -> IO (Maybe UserContactRequest) getContactRequestByXContactId xContactId = maybeFirstRow toContactRequest $ @@ -540,8 +666,8 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers db [sql| SELECT - cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at, + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr JOIN connections c USING (user_contact_link_id) @@ -562,21 +688,21 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers db [sql| UPDATE contact_requests - SET agent_invitation_id = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, updated_at = ? + SET agent_invitation_id = ?, pq_support = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, updated_at = ? WHERE user_id = ? AND contact_request_id = ? |] - (invId, minV, maxV, currentTs, userId, cReqId) + (Binary invId, pqSup, minV, maxV, currentTs, userId, cReqId) else withLocalDisplayName db userId displayName $ \ldn -> Right <$> do DB.execute db [sql| UPDATE contact_requests - SET agent_invitation_id = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, local_display_name = ?, updated_at = ? + SET agent_invitation_id = ?, pq_support = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, local_display_name = ?, updated_at = ? WHERE user_id = ? AND contact_request_id = ? |] - (invId, minV, maxV, ldn, currentTs, userId, cReqId) - DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (oldLdn, userId) + (Binary invId, pqSup, minV, maxV, ldn, currentTs, userId, cReqId) + safeDeleteLDN db user oldLdn where updateProfile currentTs = DB.execute @@ -597,6 +723,11 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers |] (displayName, fullName, image, contactLink, currentTs, userId, cReqId) +getUserContactLinkIdByCReq :: DB.Connection -> Int64 -> ExceptT StoreError IO Int64 +getUserContactLinkIdByCReq db contactRequestId = + ExceptT . firstRow fromOnly (SEContactRequestNotFound contactRequestId) $ + DB.query db "SELECT user_contact_link_id FROM contact_requests WHERE contact_request_id = ?" (Only contactRequestId) + getContactRequest' :: DB.Connection -> Int64 -> ExceptT StoreError IO (User, UserContactRequest) getContactRequest' db contactRequestId = do user <- getUserByContactRequestId db contactRequestId @@ -609,8 +740,8 @@ getContactRequest db User {userId} contactRequestId = db [sql| SELECT - cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at, + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr JOIN connections c USING (user_contact_link_id) @@ -646,13 +777,13 @@ deleteContactRequest db User {userId} contactRequestId = do SELECT local_display_name FROM contact_requests WHERE user_id = ? AND contact_request_id = ? ) + AND local_display_name NOT IN (SELECT local_display_name FROM users WHERE user_id = ?) |] - (userId, userId, contactRequestId) + (userId, userId, contactRequestId, userId) DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId) -createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionRange -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> Bool -> IO Contact -createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode contactUsed = do - DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) +createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO (Contact, Connection) +createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId connChatVersion cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed = do createdAt <- getCurrentTime customUserProfileId <- forM incognitoProfile $ \case NewIncognito p -> createIncognitoProfile_ db userId createdAt p @@ -661,34 +792,70 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences} DB.execute db "INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, chat_ts, xcontact_id, contact_used) VALUES (?,?,?,?,?,?,?,?,?,?)" - (userId, localDisplayName, profileId, True, userPreferences, createdAt, createdAt, createdAt, xContactId, contactUsed) + (userId, localDisplayName, profileId, BI True, userPreferences, createdAt, createdAt, createdAt, xContactId, BI contactUsed) contactId <- insertedRowId db - conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode + DB.execute db "UPDATE contact_requests SET contact_id = ? WHERE user_id = ? AND local_display_name = ?" (contactId, userId, localDisplayName) + conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId ConnNew connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn - pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False} + ct = + Contact + { contactId, + localDisplayName, + profile = toLocalProfile profileId profile "", + activeConn = Just conn, + viaGroup = Nothing, + contactUsed, + contactStatus = CSActive, + chatSettings = defaultChatSettings, + userPreferences, + mergedPreferences, + createdAt, + updatedAt = createdAt, + chatTs = Just createdAt, + contactGroupMemberId = Nothing, + contactGrpInvSent = False, + chatTags = [], + chatItemTTL = Nothing, + uiThemes = Nothing, + chatDeleted = False, + customData = Nothing + } + pure (ct, conn) + +deleteContactRequestRec :: DB.Connection -> User -> UserContactRequest -> IO () +deleteContactRequestRec db User {userId} UserContactRequest {contactRequestId} = + DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId) + +updateContactAccepted :: DB.Connection -> User -> Contact -> Bool -> IO () +updateContactAccepted db User {userId} Contact {contactId} contactUsed = + DB.execute + db + "UPDATE contacts SET contact_used = ? WHERE user_id = ? AND contact_id = ?" + (BI contactUsed, userId, contactId) getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64 getContactIdByName db User {userId} cName = ExceptT . firstRow fromOnly (SEContactNotFoundByName cName) $ DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND local_display_name = ? AND deleted = 0" (userId, cName) -getContact :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO Contact -getContact db user contactId = getContact_ db user contactId False +getContact :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO Contact +getContact db vr user contactId = getContact_ db vr user contactId False -getContact_ :: DB.Connection -> User -> Int64 -> Bool -> ExceptT StoreError IO Contact -getContact_ db user@User {userId} contactId deleted = - ExceptT . firstRow (toContact user) (SEContactNotFound contactId) $ +getContact_ :: DB.Connection -> VersionRangeChat -> User -> Int64 -> Bool -> ExceptT StoreError IO Contact +getContact_ db vr user@User {userId} contactId deleted = do + chatTags <- liftIO $ getDirectChatTags db contactId + ExceptT . firstRow (toContact vr user chatTags) (SEContactNotFound contactId) $ DB.query db [sql| SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, - c.peer_chat_min_version, c.peer_chat_max_version + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id LEFT JOIN connections c ON c.contact_id = ct.contact_id @@ -705,12 +872,12 @@ getContact_ db user@User {userId} contactId deleted = WHERE cc.user_id = ct.user_id AND cc.contact_id = ct.contact_id ORDER BY cc_conn_status_ord DESC, cc_created_at DESC LIMIT 1 - ) + ) cc ) OR c.connection_id IS NULL ) |] - (userId, contactId, deleted, ConnReady, ConnSndReady) + (userId, contactId, BI deleted, ConnReady, ConnSndReady) getUserByContactRequestId :: DB.Connection -> Int64 -> ExceptT StoreError IO User getUserByContactRequestId db contactRequestId = @@ -720,19 +887,19 @@ getUserByContactRequestId db contactRequestId = getPendingContactConnections :: DB.Connection -> User -> IO [PendingContactConnection] getPendingContactConnections db User {userId} = do map toPendingContactConnection - <$> DB.queryNamed + <$> DB.query db [sql| - SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at + SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at FROM connections - WHERE user_id = :user_id - AND conn_type = :conn_type + WHERE user_id = ? + AND conn_type = ? AND contact_id IS NULL |] - [":user_id" := userId, ":conn_type" := ConnContact] + (userId, ConnContact) -getContactConnections :: DB.Connection -> UserId -> Contact -> IO [Connection] -getContactConnections db userId Contact {contactId} = +getContactConnections :: DB.Connection -> VersionRangeChat -> UserId -> Contact -> IO [Connection] +getContactConnections db vr userId Contact {contactId} = connections =<< liftIO getConnections_ where getConnections_ = @@ -740,25 +907,27 @@ getContactConnections db userId Contact {contactId} = db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, - c.peer_chat_min_version, c.peer_chat_max_version + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM connections c JOIN contacts ct ON ct.contact_id = c.contact_id WHERE c.user_id = ? AND ct.user_id = ? AND ct.contact_id = ? |] (userId, userId, contactId) connections [] = pure [] - connections rows = pure $ map toConnection rows + connections rows = pure $ map (toConnection vr) rows -getConnectionById :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO Connection -getConnectionById db User {userId} connId = ExceptT $ do - firstRow toConnection (SEConnectionNotFoundById connId) $ +getConnectionById :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO Connection +getConnectionById db vr User {userId} connId = ExceptT $ do + firstRow (toConnection vr) (SEConnectionNotFoundById connId) $ DB.query db [sql| SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, - conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter, - peer_chat_min_version, peer_chat_max_version + conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, + created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, + conn_chat_version, peer_chat_min_version, peer_chat_max_version FROM connections WHERE user_id = ? AND connection_id = ? |] @@ -766,9 +935,13 @@ getConnectionById db User {userId} connId = ExceptT $ do getConnectionsContacts :: DB.Connection -> [ConnId] -> IO [ContactRef] getConnectionsContacts db agentConnIds = do - DB.execute_ db "DROP TABLE IF EXISTS temp.conn_ids" - DB.execute_ db "CREATE TABLE temp.conn_ids (conn_id BLOB)" - DB.executeMany db "INSERT INTO temp.conn_ids (conn_id) VALUES (?)" $ map Only agentConnIds + DB.execute_ db "DROP TABLE IF EXISTS temp_conn_ids" +#if defined(dbPostgres) + DB.execute_ db "CREATE TABLE temp_conn_ids (conn_id BYTEA)" +#else + DB.execute_ db "CREATE TABLE temp_conn_ids (conn_id BLOB)" +#endif + DB.executeMany db "INSERT INTO temp_conn_ids (conn_id) VALUES (?)" $ map Only agentConnIds conns <- map toContactRef <$> DB.query @@ -777,27 +950,37 @@ getConnectionsContacts db agentConnIds = do SELECT ct.contact_id, c.connection_id, c.agent_conn_id, ct.local_display_name FROM contacts ct JOIN connections c ON c.contact_id = ct.contact_id - WHERE c.agent_conn_id IN (SELECT conn_id FROM temp.conn_ids) + WHERE c.agent_conn_id IN (SELECT conn_id FROM temp_conn_ids) AND c.conn_type = ? AND ct.deleted = 0 |] (Only ConnContact) - DB.execute_ db "DROP TABLE temp.conn_ids" + DB.execute_ db "DROP TABLE temp_conn_ids" pure conns where toContactRef :: (ContactId, Int64, ConnId, ContactName) -> ContactRef toContactRef (contactId, connId, acId, localDisplayName) = ContactRef {contactId, connId, agentConnId = AgentConnId acId, localDisplayName} updateConnectionStatus :: DB.Connection -> Connection -> ConnStatus -> IO () -updateConnectionStatus db Connection {connId} connStatus = do +updateConnectionStatus db Connection {connId} = updateConnectionStatus_ db connId +{-# INLINE updateConnectionStatus #-} + +updateConnectionStatusFromTo :: DB.Connection -> Int64 -> ConnStatus -> ConnStatus -> IO () +updateConnectionStatusFromTo db connId fromStatus toStatus = do + maybeFirstRow fromOnly (DB.query db "SELECT conn_status FROM connections WHERE connection_id = ?" (Only connId)) >>= \case + Just status | status == fromStatus -> updateConnectionStatus_ db connId toStatus + _ -> pure () + +updateConnectionStatus_ :: DB.Connection -> Int64 -> ConnStatus -> IO () +updateConnectionStatus_ db connId connStatus = do currentTs <- getCurrentTime if connStatus == ConnReady - then DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ?, conn_req_inv = NULL WHERE connection_id = ?" (connStatus, currentTs, connId) + then DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ?, conn_req_inv = NULL, short_link_inv = NULL WHERE connection_id = ?" (connStatus, currentTs, connId) else DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ? WHERE connection_id = ?" (connStatus, currentTs, connId) updateContactSettings :: DB.Connection -> User -> Int64 -> ChatSettings -> IO () updateContactSettings db User {userId} contactId ChatSettings {enableNtfs, sendRcpts, favorite} = - DB.execute db "UPDATE contacts SET enable_ntfs = ?, send_rcpts = ?, favorite = ? WHERE user_id = ? AND contact_id = ?" (enableNtfs, sendRcpts, favorite, userId, contactId) + DB.execute db "UPDATE contacts SET enable_ntfs = ?, send_rcpts = ?, favorite = ? WHERE user_id = ? AND contact_id = ?" (enableNtfs, BI <$> sendRcpts, BI favorite, userId, contactId) setConnConnReqInv :: DB.Connection -> User -> Int64 -> ConnReqInvitation -> IO () setConnConnReqInv db User {userId} connId connReq = do @@ -822,3 +1005,70 @@ resetContactConnInitiated db User {userId} Connection {connId} = do WHERE user_id = ? AND connection_id = ? |] (updatedAt, userId, connId) + +setContactCustomData :: DB.Connection -> User -> Contact -> Maybe CustomData -> IO () +setContactCustomData db User {userId} Contact {contactId} customData = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE contacts SET custom_data = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (customData, updatedAt, userId, contactId) + +setContactUIThemes :: DB.Connection -> User -> Contact -> Maybe UIThemeEntityOverrides -> IO () +setContactUIThemes db User {userId} Contact {contactId} uiThemes = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE contacts SET ui_themes = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (uiThemes, updatedAt, userId, contactId) + +setContactChatDeleted :: DB.Connection -> User -> Contact -> Bool -> IO () +setContactChatDeleted db User {userId} Contact {contactId} chatDeleted = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE contacts SET chat_deleted = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (BI chatDeleted, updatedAt, userId, contactId) + +updateDirectChatTags :: DB.Connection -> ContactId -> [ChatTagId] -> IO () +updateDirectChatTags db contactId tIds = do + currentTags <- getDirectChatTags db contactId + let tagsToAdd = filter (`notElem` currentTags) tIds + tagsToDelete = filter (`notElem` tIds) currentTags + forM_ tagsToDelete $ untagDirectChat db contactId + forM_ tagsToAdd $ tagDirectChat db contactId + +tagDirectChat :: DB.Connection -> ContactId -> ChatTagId -> IO () +tagDirectChat db contactId tId = + DB.execute + db + [sql| + INSERT INTO chat_tags_chats (contact_id, chat_tag_id) + VALUES (?,?) + |] + (contactId, tId) + +untagDirectChat :: DB.Connection -> ContactId -> ChatTagId -> IO () +untagDirectChat db contactId tId = + DB.execute + db + [sql| + DELETE FROM chat_tags_chats + WHERE contact_id = ? AND chat_tag_id = ? + |] + (contactId, tId) + +getDirectChatTags :: DB.Connection -> ContactId -> IO [ChatTagId] +getDirectChatTags db contactId = map fromOnly <$> DB.query db "SELECT chat_tag_id FROM chat_tags_chats WHERE contact_id = ?" (Only contactId) + +addDirectChatTags :: DB.Connection -> Contact -> IO Contact +addDirectChatTags db ct = do + chatTags <- getDirectChatTags db $ contactId' ct + pure (ct :: Contact) {chatTags} + +setDirectChatTTL :: DB.Connection -> ContactId -> Maybe Int64 -> IO () +setDirectChatTTL db ctId ttl = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE contacts SET chat_item_ttl = ?, updated_at = ? WHERE contact_id = ?" (ttl, updatedAt, ctId) + +getDirectChatTTL :: DB.Connection -> ContactId -> IO (Maybe Int64) +getDirectChatTTL db ctId = + fmap join . maybeFirstRow fromOnly $ + DB.query db "SELECT chat_item_ttl FROM contacts WHERE contact_id = ? LIMIT 1" (Only ctId) + +getUserContactsToExpire :: DB.Connection -> User -> Int64 -> IO [ContactId] +getUserContactsToExpire db User {userId} globalTTL = + map fromOnly <$> DB.query db ("SELECT contact_id FROM contacts WHERE user_id = ? AND chat_item_ttl > 0" <> cond) (Only userId) + where + cond = if globalTTL == 0 then "" else " OR chat_item_ttl IS NULL" diff --git a/src/Simplex/Chat/Store/Files.hs b/src/Simplex/Chat/Store/Files.hs index 4d419c5727..95e169e400 100644 --- a/src/Simplex/Chat/Store/Files.hs +++ b/src/Simplex/Chat/Store/Files.hs @@ -1,8 +1,9 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -14,7 +15,6 @@ module Simplex.Chat.Store.Files ( getLiveSndFileTransfers, getLiveRcvFileTransfers, getPendingSndChunks, - createSndDirectFileTransfer, createSndDirectFTConnection, createSndGroupFileTransfer, createSndGroupFileTransferConnection, @@ -30,8 +30,8 @@ module Simplex.Chat.Store.Files createExtraSndFTDescrs, updateSndFTDeliveryXFTP, setSndFTAgentDeleted, - getXFTPSndFileDBId, - getXFTPRcvFileDBId, + getXFTPSndFileDBIds, + getXFTPRcvFileDBIds, updateFileCancelled, updateCIFileStatus, getSharedMsgIdByFileId, @@ -39,6 +39,7 @@ module Simplex.Chat.Store.Files getGroupFileIdBySharedMsgId, getDirectFileIdBySharedMsgId, getChatRefByFileId, + lookupChatRefByFileId, updateSndFileStatus, createSndFileChunk, updateSndFileChunkMsg, @@ -46,6 +47,7 @@ module Simplex.Chat.Store.Files deleteSndFileChunks, createRcvFileTransfer, createRcvGroupFileTransfer, + createRcvStandaloneFileTransfer, appendRcvFD, getRcvFileDescrByRcvFileId, getRcvFileDescrBySndFileId, @@ -70,10 +72,14 @@ module Simplex.Chat.Store.Files getFileTransfer, getFileTransferProgress, getFileTransferMeta, + lookupFileTransferRedirectMeta, getSndFileTransfer, getSndFileTransfers, getContactFileInfo, + getNoteFolderFileInfo, + createLocalFile, getLocalCryptoFile, + getLocalFileMeta, updateDirectCIFileStatus, ) where @@ -83,14 +89,14 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class import Data.Either (rights) +import Data.Functor ((<&>)) import Data.Int (Int64) import Data.Maybe (fromMaybe, isJust, listToMaybe) import Data.Text (Text) import Data.Time (addUTCTime) import Data.Time.Clock (UTCTime (..), getCurrentTime, nominalDay) import Data.Type.Equality -import Database.SQLite.Simple (Only (..), (:.) (..)) -import Database.SQLite.Simple.QQ (sql) +import Data.Word (Word32) import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol @@ -101,13 +107,25 @@ import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Util (week) import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, UserId) -import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) -import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Messaging.Agent.Store.AgentStore (firstRow, firstRow', maybeFirstRow) +import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) +import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF +import Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Protocol (SubscriptionMode (..)) -import Simplex.Messaging.Version (VersionRange) +import Simplex.Messaging.Version +import System.FilePath (takeFileName) +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (Only (..), (:.) (..)) +import Database.PostgreSQL.Simple.SqlQQ (sql) +import Database.PostgreSQL.Simple.ToField (ToField) +#else +import Database.SQLite.Simple (Only (..), (:.) (..)) +import Database.SQLite.Simple.QQ (sql) +import Database.SQLite.Simple.ToField (ToField) +#endif getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer] getLiveSndFileTransfers db User {userId} = do @@ -164,27 +182,10 @@ getPendingSndChunks db fileId connId = |] (fileId, connId) -createSndDirectFileTransfer :: DB.Connection -> UserId -> Contact -> FilePath -> FileInvitation -> Maybe ConnId -> Integer -> SubscriptionMode -> IO FileTransferMeta -createSndDirectFileTransfer db userId Contact {contactId} filePath FileInvitation {fileName, fileSize, fileInline} acId_ chunkSize subMode = do +createSndDirectFTConnection :: DB.Connection -> VersionRangeChat -> User -> Int64 -> (CommandId, ConnId) -> SubscriptionMode -> IO () +createSndDirectFTConnection db vr user@User {userId} fileId (cmdId, acId) subMode = do currentTs <- getCurrentTime - DB.execute - db - "INSERT INTO files (user_id, contact_id, file_name, file_path, file_size, chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)" - ((userId, contactId, fileName, filePath, fileSize, chunkSize) :. (fileInline, CIFSSndStored, FPSMP, currentTs, currentTs)) - fileId <- insertedRowId db - forM_ acId_ $ \acId -> do - Connection {connId} <- createSndFileConnection_ db userId fileId acId subMode - let fileStatus = FSNew - DB.execute - db - "INSERT INTO snd_files (file_id, file_status, file_inline, connection_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" - (fileId, fileStatus, fileInline, connId, currentTs, currentTs) - pure FileTransferMeta {fileId, xftpSndFile = Nothing, fileName, filePath, fileSize, fileInline, chunkSize, cancelled = False} - -createSndDirectFTConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> SubscriptionMode -> IO () -createSndDirectFTConnection db user@User {userId} fileId (cmdId, acId) subMode = do - currentTs <- getCurrentTime - Connection {connId} <- createSndFileConnection_ db userId fileId acId subMode + Connection {connId} <- createSndFileConnection_ db vr userId fileId acId subMode setCommandConnId db user cmdId connId DB.execute db @@ -199,12 +200,12 @@ createSndGroupFileTransfer db userId GroupInfo {groupId} filePath FileInvitation "INSERT INTO files (user_id, group_id, file_name, file_path, file_size, chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)" ((userId, groupId, fileName, filePath, fileSize, chunkSize) :. (fileInline, CIFSSndStored, FPSMP, currentTs, currentTs)) fileId <- insertedRowId db - pure FileTransferMeta {fileId, xftpSndFile = Nothing, fileName, filePath, fileSize, fileInline, chunkSize, cancelled = False} + pure FileTransferMeta {fileId, xftpSndFile = Nothing, xftpRedirectFor = Nothing, fileName, filePath, fileSize, fileInline, chunkSize, cancelled = False} -createSndGroupFileTransferConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> GroupMember -> SubscriptionMode -> IO () -createSndGroupFileTransferConnection db user@User {userId} fileId (cmdId, acId) GroupMember {groupMemberId} subMode = do +createSndGroupFileTransferConnection :: DB.Connection -> VersionRangeChat -> User -> Int64 -> (CommandId, ConnId) -> GroupMember -> SubscriptionMode -> IO () +createSndGroupFileTransferConnection db vr user@User {userId} fileId (cmdId, acId) GroupMember {groupMemberId} subMode = do currentTs <- getCurrentTime - Connection {connId} <- createSndFileConnection_ db userId fileId acId subMode + Connection {connId} <- createSndFileConnection_ db vr userId fileId acId subMode setCommandConnId db user cmdId connId DB.execute db @@ -272,16 +273,16 @@ getSndFTViaMsgDelivery db User {userId} Connection {connId, agentConnId} agentMs (\n -> SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, groupMemberId, recipientDisplayName = n, connId, agentConnId}) <$> (contactName_ <|> memberName_) -createSndFileTransferXFTP :: DB.Connection -> User -> ContactOrGroup -> CryptoFile -> FileInvitation -> AgentSndFileId -> Integer -> IO FileTransferMeta -createSndFileTransferXFTP db User {userId} contactOrGroup (CryptoFile filePath cryptoArgs) FileInvitation {fileName, fileSize} agentSndFileId chunkSize = do +createSndFileTransferXFTP :: DB.Connection -> User -> Maybe ContactOrGroup -> CryptoFile -> FileInvitation -> AgentSndFileId -> Maybe FileTransferId -> Integer -> IO FileTransferMeta +createSndFileTransferXFTP db User {userId} contactOrGroup_ (CryptoFile filePath cryptoArgs) FileInvitation {fileName, fileSize} agentSndFileId xftpRedirectFor chunkSize = do currentTs <- getCurrentTime let xftpSndFile = Just XFTPSndFile {agentSndFileId, privateSndFileDescr = Nothing, agentSndFileDeleted = False, cryptoArgs} DB.execute db - "INSERT INTO files (contact_id, group_id, user_id, file_name, file_path, file_crypto_key, file_crypto_nonce, file_size, chunk_size, agent_snd_file_id, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)" - (contactAndGroupIds contactOrGroup :. (userId, fileName, filePath, CF.fileKey <$> cryptoArgs, CF.fileNonce <$> cryptoArgs, fileSize, chunkSize, agentSndFileId, CIFSSndStored, FPXFTP, currentTs, currentTs)) + "INSERT INTO files (contact_id, group_id, user_id, file_name, file_path, file_crypto_key, file_crypto_nonce, file_size, chunk_size, redirect_file_id, agent_snd_file_id, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + (maybe (Nothing, Nothing) contactAndGroupIds contactOrGroup_ :. (userId, fileName, filePath, CF.fileKey <$> cryptoArgs, CF.fileNonce <$> cryptoArgs, fileSize, chunkSize) :. (xftpRedirectFor, agentSndFileId, CIFSSndStored, FPXFTP, currentTs, currentTs)) fileId <- insertedRowId db - pure FileTransferMeta {fileId, xftpSndFile, fileName, filePath, fileSize, fileInline = Nothing, chunkSize, cancelled = False} + pure FileTransferMeta {fileId, xftpSndFile, xftpRedirectFor, fileName, filePath, fileSize, fileInline = Nothing, chunkSize, cancelled = False} createSndFTDescrXFTP :: DB.Connection -> User -> Maybe GroupMember -> Connection -> FileTransferMeta -> FileDescr -> IO () createSndFTDescrXFTP db User {userId} m Connection {connId} FileTransferMeta {fileId} FileDescr {fileDescrText, fileDescrPartNo, fileDescrComplete} = do @@ -290,7 +291,7 @@ createSndFTDescrXFTP db User {userId} m Connection {connId} FileTransferMeta {fi DB.execute db "INSERT INTO xftp_file_descriptions (user_id, file_descr_text, file_descr_part_no, file_descr_complete, created_at, updated_at) VALUES (?,?,?,?,?,?)" - (userId, fileDescrText, fileDescrPartNo, fileDescrComplete, currentTs, currentTs) + (userId, fileDescrText, fileDescrPartNo, BI fileDescrComplete, currentTs, currentTs) fileDescrId <- insertedRowId db DB.execute db @@ -315,7 +316,7 @@ updateSndFTDescrXFTP db user@User {userId} sft@SndFileTransfer {fileId, fileDesc SET file_descr_text = ?, file_descr_part_no = ?, file_descr_complete = ?, updated_at = ? WHERE user_id = ? AND file_descr_id = ? |] - (rfdText, 1 :: Int, True, currentTs, userId, fileDescrId) + (rfdText, 1 :: Int, BI True, currentTs, userId, fileDescrId) updateCIFileStatus db user fileId $ CIFSSndTransfer 1 1 updateSndFileStatus db sft FSConnected @@ -343,15 +344,37 @@ setSndFTAgentDeleted db User {userId} fileId = do "UPDATE files SET agent_snd_file_deleted = 1, updated_at = ? WHERE user_id = ? AND file_id = ?" (currentTs, userId, fileId) -getXFTPSndFileDBId :: DB.Connection -> User -> AgentSndFileId -> ExceptT StoreError IO FileTransferId -getXFTPSndFileDBId db User {userId} aSndFileId = - ExceptT . firstRow fromOnly (SESndFileNotFoundXFTP aSndFileId) $ - DB.query db "SELECT file_id FROM files WHERE user_id = ? AND agent_snd_file_id = ?" (userId, aSndFileId) +getXFTPSndFileDBIds :: DB.Connection -> AgentSndFileId -> ExceptT StoreError IO (Maybe ChatRef, FileTransferId) +getXFTPSndFileDBIds db aSndFileId = + ExceptT . firstRow' toFileRef (SESndFileNotFoundXFTP aSndFileId) $ + DB.query + db + [sql| + SELECT file_id, contact_id, group_id, note_folder_id + FROM files + WHERE agent_snd_file_id = ? + |] + (Only aSndFileId) -getXFTPRcvFileDBId :: DB.Connection -> AgentRcvFileId -> ExceptT StoreError IO FileTransferId -getXFTPRcvFileDBId db aRcvFileId = - ExceptT . firstRow fromOnly (SERcvFileNotFoundXFTP aRcvFileId) $ - DB.query db "SELECT file_id FROM rcv_files WHERE agent_rcv_file_id = ?" (Only aRcvFileId) +getXFTPRcvFileDBIds :: DB.Connection -> AgentRcvFileId -> ExceptT StoreError IO (Maybe ChatRef, FileTransferId) +getXFTPRcvFileDBIds db aRcvFileId = + ExceptT . firstRow' toFileRef (SERcvFileNotFoundXFTP aRcvFileId) $ + DB.query + db + [sql| + SELECT rf.file_id, f.contact_id, f.group_id, f.note_folder_id + FROM rcv_files rf + JOIN files f ON f.file_id = rf.file_id + WHERE rf.agent_rcv_file_id = ? + |] + (Only aRcvFileId) + +toFileRef :: (FileTransferId, Maybe Int64, Maybe Int64, Maybe Int64) -> Either StoreError (Maybe ChatRef, FileTransferId) +toFileRef = \case + (fileId, Just contactId, Nothing, Nothing) -> Right (Just $ ChatRef CTDirect contactId, fileId) + (fileId, Nothing, Just groupId, Nothing) -> Right (Just $ ChatRef CTGroup groupId, fileId) + (fileId, Nothing, Nothing, Just folderId) -> Right (Just $ ChatRef CTLocal folderId, fileId) + (fileId, _, _, _) -> Right (Nothing, fileId) updateFileCancelled :: MsgDirectionI d => DB.Connection -> User -> Int64 -> CIFileStatus d -> IO () updateFileCancelled db User {userId} fileId ciFileStatus = do @@ -416,11 +439,14 @@ getDirectFileIdBySharedMsgId db User {userId} Contact {contactId} sharedMsgId = (userId, contactId, sharedMsgId) getChatRefByFileId :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO ChatRef -getChatRefByFileId db User {userId} fileId = - liftIO getChatRef >>= \case - [(Just contactId, Nothing)] -> pure $ ChatRef CTDirect contactId - [(Nothing, Just groupId)] -> pure $ ChatRef CTGroup groupId - _ -> throwError $ SEInternalError "could not retrieve chat ref by file id" +getChatRefByFileId db user fileId = liftIO (lookupChatRefByFileId db user fileId) >>= maybe (throwError $ SEInternalError "could not retrieve chat ref by file id") pure + +lookupChatRefByFileId :: DB.Connection -> User -> Int64 -> IO (Maybe ChatRef) +lookupChatRefByFileId db User {userId} fileId = + getChatRef <&> \case + [(Just contactId, Nothing)] -> Just $ ChatRef CTDirect contactId + [(Nothing, Just groupId)] -> Just $ ChatRef CTGroup groupId + _ -> Nothing where getChatRef = DB.query @@ -433,10 +459,11 @@ getChatRefByFileId db User {userId} fileId = |] (userId, fileId) -createSndFileConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> SubscriptionMode -> IO Connection -createSndFileConnection_ db userId fileId agentConnId subMode = do +-- TODO v6.0 remove +createSndFileConnection_ :: DB.Connection -> VersionRangeChat -> UserId -> Int64 -> ConnId -> SubscriptionMode -> IO Connection +createSndFileConnection_ db vr userId fileId agentConnId subMode = do currentTs <- getCurrentTime - createConnection_ db userId ConnSndFile (Just fileId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode + createConnection_ db userId ConnSndFile (Just fileId) agentConnId ConnNew (minVersion vr) chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff updateSndFileStatus :: DB.Connection -> SndFileTransfer -> FileStatus -> IO () updateSndFileStatus db SndFileTransfer {fileId, connId} status = do @@ -495,7 +522,7 @@ createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@File rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_ -- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it - xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_ + xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False, userApprovedRelays = False}) <$> rfd_ fileProtocol = if isJust rfd_ then FPXFTP else FPSMP fileId <- liftIO $ do DB.execute @@ -516,7 +543,7 @@ createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localD rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_ -- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it - xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_ + xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False, userApprovedRelays = False}) <$> rfd_ fileProtocol = if isJust rfd_ then FPXFTP else FPSMP fileId <- liftIO $ do DB.execute @@ -531,6 +558,23 @@ createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localD (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, groupMemberId, rfdId, currentTs, currentTs) pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Just groupMemberId, cryptoArgs = Nothing} +createRcvStandaloneFileTransfer :: DB.Connection -> UserId -> CryptoFile -> Int64 -> Word32 -> ExceptT StoreError IO Int64 +createRcvStandaloneFileTransfer db userId (CryptoFile filePath cfArgs_) fileSize chunkSize = do + currentTs <- liftIO getCurrentTime + fileId <- liftIO $ do + DB.execute + db + "INSERT INTO files (user_id, file_name, file_path, file_size, chunk_size, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" + (userId, takeFileName filePath, filePath, fileSize, chunkSize, CIFSRcvInvitation, FPXFTP, currentTs, currentTs) + insertedRowId db + liftIO . forM_ cfArgs_ $ \cfArgs -> setFileCryptoArgs_ db fileId cfArgs currentTs + liftIO $ + DB.execute + db + "INSERT INTO rcv_files (file_id, file_status, created_at, updated_at) VALUES (?,?,?,?)" + (fileId, FSNew, currentTs, currentTs) + pure fileId + createRcvFD_ :: DB.Connection -> UserId -> UTCTime -> FileDescr -> ExceptT StoreError IO RcvFileDescr createRcvFD_ db userId currentTs FileDescr {fileDescrText, fileDescrPartNo, fileDescrComplete} = do when (fileDescrPartNo /= 0) $ throwError SERcvFileInvalidDescrPart @@ -538,7 +582,7 @@ createRcvFD_ db userId currentTs FileDescr {fileDescrText, fileDescrPartNo, file DB.execute db "INSERT INTO xftp_file_descriptions (user_id, file_descr_text, file_descr_part_no, file_descr_complete, created_at, updated_at) VALUES (?,?,?,?,?,?)" - (userId, fileDescrText, fileDescrPartNo, fileDescrComplete, currentTs, currentTs) + (userId, fileDescrText, fileDescrPartNo, BI fileDescrComplete, currentTs, currentTs) insertedRowId db pure RcvFileDescr {fileDescrId, fileDescrPartNo, fileDescrText, fileDescrComplete} @@ -571,7 +615,7 @@ appendRcvFD db userId fileId fd@FileDescr {fileDescrText, fileDescrPartNo, fileD SET file_descr_text = ?, file_descr_part_no = ?, file_descr_complete = ? WHERE file_descr_id = ? |] - (fileDescrText', fileDescrPartNo, fileDescrComplete, fileDescrId) + (fileDescrText', fileDescrPartNo, BI fileDescrComplete, fileDescrId) pure RcvFileDescr {fileDescrId, fileDescrText = fileDescrText', fileDescrPartNo, fileDescrComplete} getRcvFileDescrByRcvFileId :: DB.Connection -> FileTransferId -> ExceptT StoreError IO RcvFileDescr @@ -614,8 +658,8 @@ getRcvFileDescrBySndFileId_ db fileId = |] (Only fileId) -toRcvFileDescr :: (Int64, Text, Int, Bool) -> RcvFileDescr -toRcvFileDescr (fileDescrId, fileDescrText, fileDescrPartNo, fileDescrComplete) = +toRcvFileDescr :: (Int64, Text, Int, BoolInt) -> RcvFileDescr +toRcvFileDescr (fileDescrId, fileDescrText, fileDescrPartNo, BI fileDescrComplete) = RcvFileDescr {fileDescrId, fileDescrText, fileDescrPartNo, fileDescrComplete} updateRcvFileAgentId :: DB.Connection -> FileTransferId -> Maybe AgentRcvFileId -> IO () @@ -640,12 +684,14 @@ getRcvFileTransfer_ db userId fileId = do [sql| SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name, f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name, - f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline, r.agent_rcv_file_id, r.agent_rcv_file_deleted, c.connection_id, c.agent_conn_id + f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline, + r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays, + c.connection_id, c.agent_conn_id FROM rcv_files r JOIN files f USING (file_id) LEFT JOIN connections c ON r.file_id = c.rcv_file_id - LEFT JOIN contacts cs USING (contact_id) - LEFT JOIN group_members m USING (group_member_id) + LEFT JOIN contacts cs ON cs.contact_id = f.contact_id + LEFT JOIN group_members m ON m.group_member_id = r.group_member_id WHERE f.user_id = ? AND f.file_id = ? |] (userId, fileId) @@ -654,12 +700,12 @@ getRcvFileTransfer_ db userId fileId = do where rcvFileTransfer :: Maybe RcvFileDescr -> - (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, Bool) :. (Maybe Int64, Maybe AgentConnId) -> + (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe BoolInt) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, BoolInt, BoolInt) :. (Maybe Int64, Maybe AgentConnId) -> ExceptT StoreError IO RcvFileTransfer - rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, agentRcvFileDeleted) :. (connId_, agentConnId_)) = - case contactName_ <|> memberName_ of + rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, BI agentRcvFileDeleted, BI userApprovedRelays) :. (connId_, agentConnId_)) = + case contactName_ <|> memberName_ <|> standaloneName_ of Nothing -> throwError $ SERcvFileInvalid fileId - Just name -> do + Just name -> case fileStatus' of FSNew -> pure $ ft name RFSNew FSAccepted -> ft name . RFSAccepted <$> rfi @@ -667,67 +713,77 @@ getRcvFileTransfer_ db userId fileId = do FSComplete -> ft name . RFSComplete <$> rfi FSCancelled -> ft name . RFSCancelled <$> rfi_ where + standaloneName_ = case (connId_, agentRcvFileId, filePath_) of + (Nothing, Just _, Just _) -> Just "" -- filePath marks files that are accepted from contact or, in this case, set by createRcvDirectFileTransfer + _ -> Nothing ft senderDisplayName fileStatus = let fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing} cryptoArgs = CFArgs <$> fileKey <*> fileNonce - xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted}) <$> rfd_ + xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted, userApprovedRelays}) <$> rfd_ in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId, cryptoArgs} rfi = maybe (throwError $ SERcvFileInvalid fileId) pure =<< rfi_ rfi_ = case (filePath_, connId_, agentConnId_) of (Just filePath, connId, agentConnId) -> pure $ Just RcvFileInfo {filePath, connId, agentConnId} _ -> pure Nothing - cancelled = fromMaybe False cancelled_ + cancelled = maybe False unBI cancelled_ -acceptRcvFileTransfer :: DB.Connection -> VersionRange -> User -> Int64 -> (CommandId, ConnId) -> ConnStatus -> FilePath -> SubscriptionMode -> ExceptT StoreError IO AChatItem +acceptRcvFileTransfer :: DB.Connection -> VersionRangeChat -> User -> Int64 -> (CommandId, ConnId) -> ConnStatus -> FilePath -> SubscriptionMode -> ExceptT StoreError IO AChatItem acceptRcvFileTransfer db vr user@User {userId} fileId (cmdId, acId) connStatus filePath subMode = ExceptT $ do currentTs <- getCurrentTime - acceptRcvFT_ db user fileId filePath Nothing currentTs + acceptRcvFT_ db user fileId filePath False Nothing currentTs DB.execute db "INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id, created_at, updated_at, to_subscribe) VALUES (?,?,?,?,?,?,?,?)" - (acId, connStatus, ConnRcvFile, fileId, userId, currentTs, currentTs, subMode == SMOnlyCreate) + (acId, connStatus, ConnRcvFile, fileId, userId, currentTs, currentTs, BI (subMode == SMOnlyCreate)) connId <- insertedRowId db setCommandConnId db user cmdId connId runExceptT $ getChatItemByFileId db vr user fileId -getContactByFileId :: DB.Connection -> User -> FileTransferId -> ExceptT StoreError IO Contact -getContactByFileId db user@User {userId} fileId = do +getContactByFileId :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> ExceptT StoreError IO Contact +getContactByFileId db vr user@User {userId} fileId = do cId <- getContactIdByFileId - getContact db user cId + getContact db vr user cId where getContactIdByFileId = ExceptT . firstRow fromOnly (SEContactNotFoundByFileId fileId) $ DB.query db "SELECT contact_id FROM files WHERE user_id = ? AND file_id = ?" (userId, fileId) -acceptRcvInlineFT :: DB.Connection -> VersionRange -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem +acceptRcvInlineFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem acceptRcvInlineFT db vr user fileId filePath = do - liftIO $ acceptRcvFT_ db user fileId filePath (Just IFMOffer) =<< getCurrentTime + liftIO $ acceptRcvFT_ db user fileId filePath False (Just IFMOffer) =<< getCurrentTime getChatItemByFileId db vr user fileId startRcvInlineFT :: DB.Connection -> User -> RcvFileTransfer -> FilePath -> Maybe InlineFileMode -> IO () startRcvInlineFT db user RcvFileTransfer {fileId} filePath rcvFileInline = - acceptRcvFT_ db user fileId filePath rcvFileInline =<< getCurrentTime + acceptRcvFT_ db user fileId filePath False rcvFileInline =<< getCurrentTime -xftpAcceptRcvFT :: DB.Connection -> VersionRange -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem -xftpAcceptRcvFT db vr user fileId filePath = do - liftIO $ acceptRcvFT_ db user fileId filePath Nothing =<< getCurrentTime +xftpAcceptRcvFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> Bool -> ExceptT StoreError IO AChatItem +xftpAcceptRcvFT db vr user fileId filePath userApprovedRelays = do + liftIO $ acceptRcvFT_ db user fileId filePath userApprovedRelays Nothing =<< getCurrentTime getChatItemByFileId db vr user fileId -acceptRcvFT_ :: DB.Connection -> User -> FileTransferId -> FilePath -> Maybe InlineFileMode -> UTCTime -> IO () -acceptRcvFT_ db User {userId} fileId filePath rcvFileInline currentTs = do +acceptRcvFT_ :: DB.Connection -> User -> FileTransferId -> FilePath -> Bool -> Maybe InlineFileMode -> UTCTime -> IO () +acceptRcvFT_ db User {userId} fileId filePath userApprovedRelays rcvFileInline currentTs = do DB.execute db "UPDATE files SET file_path = ?, ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ?" (filePath, CIFSRcvAccepted, currentTs, userId, fileId) DB.execute db - "UPDATE rcv_files SET rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?" - (rcvFileInline, FSAccepted, currentTs, fileId) + "UPDATE rcv_files SET user_approved_relays = ?, rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?" + (BI userApprovedRelays, rcvFileInline, FSAccepted, currentTs, fileId) -setRcvFileToReceive :: DB.Connection -> FileTransferId -> Maybe CryptoFileArgs -> IO () -setRcvFileToReceive db fileId cfArgs_ = do +setRcvFileToReceive :: DB.Connection -> FileTransferId -> Bool -> Maybe CryptoFileArgs -> IO () +setRcvFileToReceive db fileId userApprovedRelays cfArgs_ = do currentTs <- getCurrentTime - DB.execute db "UPDATE rcv_files SET to_receive = 1, updated_at = ? WHERE file_id = ?" (currentTs, fileId) + DB.execute + db + [sql| + UPDATE rcv_files + SET to_receive = 1, user_approved_relays = ?, updated_at = ? + WHERE file_id = ? + |] + (BI userApprovedRelays, currentTs, fileId) forM_ cfArgs_ $ \cfArgs -> setFileCryptoArgs_ db fileId cfArgs currentTs setFileCryptoArgs :: DB.Connection -> FileTransferId -> CryptoFileArgs -> IO () @@ -840,18 +896,19 @@ getFileTransfer :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO FileT getFileTransfer db user@User {userId} fileId = fileTransfer =<< liftIO (getFileTransferRow_ db userId fileId) where - fileTransfer :: [(Maybe Int64, Maybe Int64)] -> ExceptT StoreError IO FileTransfer - fileTransfer [(Nothing, Just _)] = FTRcv <$> getRcvFileTransfer db user fileId + fileTransfer :: [(Maybe Int64, Maybe Int64, FileProtocol)] -> ExceptT StoreError IO FileTransfer + fileTransfer [(_, _, FPLocal)] = throwError $ SELocalFileNoTransfer fileId + fileTransfer [(Nothing, Just _, _)] = FTRcv <$> getRcvFileTransfer db user fileId fileTransfer _ = do (ftm, fts) <- getSndFileTransfer db user fileId pure $ FTSnd {fileTransferMeta = ftm, sndFileTransfers = fts} -getFileTransferRow_ :: DB.Connection -> UserId -> Int64 -> IO [(Maybe Int64, Maybe Int64)] +getFileTransferRow_ :: DB.Connection -> UserId -> Int64 -> IO [(Maybe Int64, Maybe Int64, FileProtocol)] getFileTransferRow_ db userId fileId = DB.query db [sql| - SELECT s.file_id, r.file_id + SELECT s.file_id, r.file_id, f.protocol FROM files f LEFT JOIN snd_files s ON s.file_id = f.file_id LEFT JOIN rcv_files r ON r.file_id = f.file_id @@ -879,8 +936,8 @@ getSndFileTransfers_ db userId fileId = FROM snd_files s JOIN files f USING (file_id) JOIN connections c USING (connection_id) - LEFT JOIN contacts cs USING (contact_id) - LEFT JOIN group_members m USING (group_member_id) + LEFT JOIN contacts cs ON cs.contact_id = f.contact_id + LEFT JOIN group_members m ON m.group_member_id = s.group_member_id WHERE f.user_id = ? AND f.file_id = ? |] (userId, fileId) @@ -900,38 +957,89 @@ getFileTransferMeta_ db userId fileId = DB.query db [sql| - SELECT file_name, file_size, chunk_size, file_path, file_crypto_key, file_crypto_nonce, file_inline, agent_snd_file_id, agent_snd_file_deleted, private_snd_file_descr, cancelled + SELECT file_name, file_size, chunk_size, file_path, file_crypto_key, file_crypto_nonce, file_inline, agent_snd_file_id, agent_snd_file_deleted, private_snd_file_descr, cancelled, redirect_file_id FROM files WHERE user_id = ? AND file_id = ? |] (userId, fileId) where - fileTransferMeta :: (String, Integer, Integer, FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe AgentSndFileId, Bool, Maybe Text, Maybe Bool) -> FileTransferMeta - fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileKey, fileNonce, fileInline, aSndFileId_, agentSndFileDeleted, privateSndFileDescr, cancelled_) = + fileTransferMeta :: (String, Integer, Integer, FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe AgentSndFileId, BoolInt, Maybe Text, Maybe BoolInt, Maybe FileTransferId) -> FileTransferMeta + fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileKey, fileNonce, fileInline, aSndFileId_, BI agentSndFileDeleted, privateSndFileDescr, cancelled_, xftpRedirectFor) = let cryptoArgs = CFArgs <$> fileKey <*> fileNonce xftpSndFile = (\fId -> XFTPSndFile {agentSndFileId = fId, privateSndFileDescr, agentSndFileDeleted, cryptoArgs}) <$> aSndFileId_ - in FileTransferMeta {fileId, xftpSndFile, fileName, fileSize, chunkSize, filePath, fileInline, cancelled = fromMaybe False cancelled_} + in FileTransferMeta {fileId, xftpSndFile, xftpRedirectFor, fileName, fileSize, chunkSize, filePath, fileInline, cancelled = maybe False unBI cancelled_} + +lookupFileTransferRedirectMeta :: DB.Connection -> User -> Int64 -> IO [FileTransferMeta] +lookupFileTransferRedirectMeta db User {userId} fileId = do + redirects <- DB.query db "SELECT file_id FROM files WHERE user_id = ? AND redirect_file_id = ?" (userId, fileId) + rights <$> mapM (runExceptT . getFileTransferMeta_ db userId . fromOnly) redirects + +createLocalFile :: ToField (CIFileStatus d) => CIFileStatus d -> DB.Connection -> User -> NoteFolder -> UTCTime -> CryptoFile -> Integer -> Integer -> IO Int64 +createLocalFile fileStatus db User {userId} NoteFolder {noteFolderId} itemTs CryptoFile {filePath, cryptoArgs} fileSize fileChunkSize = do + DB.execute + db + [sql| + INSERT INTO files + ( user_id, note_folder_id, + file_name, file_path, file_size, + file_crypto_key, file_crypto_nonce, + chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at + ) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (userId, noteFolderId) + :. (takeFileName filePath, filePath, fileSize) + :. maybe (Nothing, Nothing) (\(CFArgs key nonce) -> (Just key, Just nonce)) cryptoArgs + :. (fileChunkSize, Nothing :: Maybe InlineFileMode, fileStatus, FPLocal, itemTs, itemTs) + ) + insertedRowId db + +getLocalFileMeta :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalFileMeta +getLocalFileMeta db userId fileId = + ExceptT . firstRow localFileMeta (SEFileNotFound fileId) $ + DB.query + db + [sql| + SELECT file_name, file_size, file_path, file_crypto_key, file_crypto_nonce + FROM files + WHERE user_id = ? AND file_id = ? + |] + (userId, fileId) + where + localFileMeta :: (FilePath, Integer, FilePath, Maybe C.SbKey, Maybe C.CbNonce) -> LocalFileMeta + localFileMeta (fileName, fileSize, filePath, fileKey, fileNonce) = + let fileCryptoArgs = CFArgs <$> fileKey <*> fileNonce + in LocalFileMeta {fileId, fileName, fileSize, filePath, fileCryptoArgs} getContactFileInfo :: DB.Connection -> User -> Contact -> IO [CIFileInfo] getContactFileInfo db User {userId} Contact {contactId} = map toFileInfo <$> DB.query db (fileInfoQuery <> " WHERE i.user_id = ? AND i.contact_id = ?") (userId, contactId) +getNoteFolderFileInfo :: DB.Connection -> User -> NoteFolder -> IO [CIFileInfo] +getNoteFolderFileInfo db User {userId} NoteFolder {noteFolderId} = + map toFileInfo + <$> DB.query db (fileInfoQuery <> " WHERE i.user_id = ? AND i.note_folder_id = ?") (userId, noteFolderId) + getLocalCryptoFile :: DB.Connection -> UserId -> Int64 -> Bool -> ExceptT StoreError IO CryptoFile getLocalCryptoFile db userId fileId sent = liftIO (getFileTransferRow_ db userId fileId) >>= \case - [(Nothing, Just _)] -> do + [(Nothing, Just _, _)] -> do when sent $ throwError $ SEFileNotFound fileId RcvFileTransfer {fileStatus, cryptoArgs} <- getRcvFileTransfer_ db userId fileId case fileStatus of RFSComplete RcvFileInfo {filePath} -> pure $ CryptoFile filePath cryptoArgs _ -> throwError $ SEFileNotFound fileId - _ -> do + [(Just _, Nothing, _)] -> do unless sent $ throwError $ SEFileNotFound fileId FileTransferMeta {filePath, xftpSndFile} <- getFileTransferMeta_ db userId fileId - pure $ CryptoFile filePath $ xftpSndFile >>= \f -> f.cryptoArgs + pure $ CryptoFile filePath $ xftpSndFile >>= \XFTPSndFile {cryptoArgs} -> cryptoArgs + [(Nothing, Nothing, FPLocal)] -> do + LocalFileMeta {filePath, fileCryptoArgs} <- getLocalFileMeta db userId fileId + pure $ CryptoFile filePath fileCryptoArgs + _ -> throwError $ SEFileNotFound fileId -updateDirectCIFileStatus :: forall d. MsgDirectionI d => DB.Connection -> VersionRange -> User -> Int64 -> CIFileStatus d -> ExceptT StoreError IO AChatItem +updateDirectCIFileStatus :: forall d. MsgDirectionI d => DB.Connection -> VersionRangeChat -> User -> Int64 -> CIFileStatus d -> ExceptT StoreError IO AChatItem updateDirectCIFileStatus db vr user fileId fileStatus = do aci@(AChatItem cType d cInfo ci) <- getChatItemByFileId db vr user fileId case (cType, testEquality d $ msgDirection @d) of diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 2066626364..fc23c9ef44 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -1,9 +1,10 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -30,38 +31,47 @@ module Simplex.Chat.Store.Groups getGroupAndMember, createNewGroup, createGroupInvitation, + deleteContactCardKeepConn, createGroupInvitedViaLink, + createGroupRejectedViaLink, setViaGroupLinkHash, setGroupInvitationChatItemId, getGroup, getGroupInfo, getGroupInfoByUserContactLinkConnReq, + getGroupInfoViaUserShortLink, getGroupInfoByGroupLinkHash, updateGroupProfile, + updateGroupPreferences, + updateGroupProfileFromMember, getGroupIdByName, getGroupMemberIdByName, getActiveMembersByName, getGroupInfoByName, getGroupMember, + getMentionedGroupMember, + getMentionedMemberByMemberId, getGroupMemberById, getGroupMemberByMemberId, getGroupMembers, + getGroupModerators, getGroupMembersForExpiration, getGroupCurrentMembersCount, - deleteGroupConnectionsAndFiles, - deleteGroupItemsAndMembers, + deleteGroupChatItems, + deleteGroupMembers, + cleanupHostGroupLinkConn, deleteGroup, - getUserGroups, + getUserGroupsToSubscribe, getUserGroupDetails, getUserGroupsWithSummary, getGroupSummary, getContactGroupPreferences, - checkContactHasGroups, getGroupInvitation, createNewContactMember, createNewContactMemberAsync, - createAcceptedMember, - createAcceptedMemberConnection, + createJoiningMember, + createJoiningMemberConnection, + createBusinessRequestGroup, getContactViaMember, setNewContactMemberConnRequest, getMemberInvitation, @@ -69,6 +79,7 @@ module Simplex.Chat.Store.Groups createMemberConnectionAsync, updateGroupMemberStatus, updateGroupMemberStatusById, + updateGroupMemberAccepted, createNewGroupMember, checkGroupMemberHasItems, deleteGroupMember, @@ -99,8 +110,8 @@ module Simplex.Chat.Store.Groups deleteOldProbes, updateGroupSettings, updateGroupMemberSettings, + updateGroupMemberBlocked, getXGrpMemIntroContDirect, - getXGrpMemIntroContGroup, getHostConnId, createMemberContact, getMemberContact, @@ -109,8 +120,20 @@ module Simplex.Chat.Store.Groups updateMemberContactInvited, resetMemberContactFields, updateMemberProfile, + updateContactMemberProfile, getXGrpLinkMemReceived, setXGrpLinkMemReceived, + createNewUnknownGroupMember, + updateUnknownMemberAnnounced, + updateUserMemberProfileSentAt, + setGroupCustomData, + setGroupUIThemes, + updateGroupChatTags, + getGroupChatTags, + setGroupChatTTL, + getGroupChatTTL, + getUserGroupsToExpire, + updateGroupAlias, ) where @@ -118,6 +141,8 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class import Crypto.Random (ChaChaDRG) +import Data.Bifunctor (second) +import Data.Bitraversable (bitraverse) import Data.Either (rights) import Data.Int (Int64) import Data.List (partition, sortOn) @@ -125,71 +150,60 @@ import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing) import Data.Ord (Down (..)) import Data.Text (Text) import Data.Time.Clock (UTCTime (..), getCurrentTime) -import Database.SQLite.Simple (NamedParam (..), Only (..), Query (..), (:.) (..)) -import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Messages -import Simplex.Chat.Protocol (groupForwardVRange) +import Simplex.Chat.Protocol (MsgMention (..), groupForwardVersion) import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences -import Simplex.Messaging.Agent.Protocol (ConnId, UserId) -import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) -import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Chat.Types.Shared +import Simplex.Chat.Types.UITheme +import Simplex.Messaging.Agent.Protocol (ConnId, CreatedConnLink (..), UserId) +import Simplex.Messaging.Agent.Store.AgentStore (firstRow, fromOnlyBI, maybeFirstRow) +import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) +import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.Ratchet (pattern PQEncOff, pattern PQSupportOff) import Simplex.Messaging.Protocol (SubscriptionMode (..)) -import Simplex.Messaging.Util (eitherToMaybe, ($>>=), (<$$>)) +import Simplex.Messaging.Util (eitherToMaybe, firstRow', ($>>=), (<$$>)) import Simplex.Messaging.Version import UnliftIO.STM +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (Only (..), Query, (:.) (..)) +import Database.PostgreSQL.Simple.SqlQQ (sql) +#else +import Database.SQLite.Simple (Only (..), Query, (:.) (..)) +import Database.SQLite.Simple.QQ (sql) +#endif -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime) :. GroupMemberRow - -type GroupMemberRow = ((Int64, Int64, MemberId, Version, Version, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, Bool) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences)) - -type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe Version, Maybe Version, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe Bool) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences)) - -toGroupInfo :: VersionRange -> Int64 -> GroupInfoRow -> GroupInfo -toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs) :. userMemberRow) = - let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = JVersionRange vr} - chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} - fullGroupPreferences = mergeGroupPreferences groupPreferences - groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences} - in GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs} - -toGroupMember :: Int64 -> GroupMemberRow -> GroupMember -toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences)) = - let memberProfile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} - memberSettings = GroupMemberSettings {showMessages} - invitedBy = toInvitedBy userContactId invitedById - activeConn = Nothing - memberChatVRange = JVersionRange $ fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer - in GroupMember {..} +type MaybeGroupMemberRow = (Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe LocalAlias, Maybe Preferences) :. (Maybe UTCTime, Maybe UTCTime) toMaybeGroupMember :: Int64 -> MaybeGroupMemberRow -> Maybe GroupMember -toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages) :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId, Just profileId, Just displayName, Just fullName, image, contactLink, Just localAlias, contactPreferences)) = - Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, contactPreferences)) +toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked) :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId, Just profileId, Just displayName, Just fullName, image, contactLink, Just localAlias, contactPreferences) :. (Just createdAt, Just updatedAt)) = + Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, contactPreferences) :. (createdAt, updatedAt)) toMaybeGroupMember _ _ = Nothing -createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> ConnReqContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO () -createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} agentConnId cReq groupLinkId memberRole subMode = +createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> CreatedLinkContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO () +createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} agentConnId (CCLink cReq shortLink) groupLinkId memberRole subMode = checkConstraint (SEDuplicateGroupLink groupInfo) . liftIO $ do currentTs <- getCurrentTime DB.execute db - "INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" - (userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, memberRole, True, currentTs, currentTs) + "INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, short_link_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)" + (userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, shortLink, memberRole, BI True, currentTs, currentTs) userContactLinkId <- insertedRowId db - void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode + void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode PQSupportOff -getGroupLinkConnection :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO Connection -getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} = - ExceptT . firstRow toConnection (SEGroupLinkNotFound groupInfo) $ +getGroupLinkConnection :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> ExceptT StoreError IO Connection +getGroupLinkConnection db vr User {userId} groupInfo@GroupInfo {groupId} = + ExceptT . firstRow (toConnection vr) (SEGroupLinkNotFound groupInfo) $ DB.query db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, - c.peer_chat_min_version, c.peer_chat_max_version + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM connections c JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id WHERE c.user_id = ? AND uc.user_id = ? AND uc.group_id = ? @@ -220,8 +234,9 @@ deleteGroupLink db User {userId} GroupInfo {groupId} = do JOIN user_contact_links uc USING (user_contact_link_id) WHERE uc.user_id = ? AND uc.group_id = ? ) + AND local_display_name NOT IN (SELECT local_display_name FROM users WHERE user_id = ?) |] - (userId, userId, groupId) + (userId, userId, groupId, userId) DB.execute db [sql| @@ -236,12 +251,12 @@ deleteGroupLink db User {userId} GroupInfo {groupId} = do (userId, groupId) DB.execute db "DELETE FROM user_contact_links WHERE user_id = ? AND group_id = ?" (userId, groupId) -getGroupLink :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO (Int64, ConnReqContact, GroupMemberRole) +getGroupLink :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO (Int64, CreatedLinkContact, GroupMemberRole) getGroupLink db User {userId} gInfo@GroupInfo {groupId} = ExceptT . firstRow groupLink (SEGroupLinkNotFound gInfo) $ - DB.query db "SELECT user_contact_link_id, conn_req_contact, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId) + DB.query db "SELECT user_contact_link_id, conn_req_contact, short_link_contact, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId) where - groupLink (linkId, cReq, mRole_) = (linkId, cReq, fromMaybe GRMember mRole_) + groupLink (linkId, cReq, shortLink, mRole_) = (linkId, CCLink cReq shortLink, fromMaybe GRMember mRole_) getGroupLinkId :: DB.Connection -> User -> GroupInfo -> IO (Maybe GroupLinkId) getGroupLinkId db User {userId} GroupInfo {groupId} = @@ -252,51 +267,58 @@ setGroupLinkMemberRole :: DB.Connection -> User -> Int64 -> GroupMemberRole -> I setGroupLinkMemberRole db User {userId} userContactLinkId memberRole = DB.execute db "UPDATE user_contact_links SET group_link_member_role = ? WHERE user_id = ? AND user_contact_link_id = ?" (memberRole, userId, userContactLinkId) -getGroupAndMember :: DB.Connection -> User -> Int64 -> VersionRange -> ExceptT StoreError IO (GroupInfo, GroupMember) -getGroupAndMember db User {userId, userContactId} groupMemberId vr = - ExceptT . firstRow toGroupAndMember (SEInternalError "referenced group member not found") $ - DB.query - db - [sql| - SELECT - -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.created_at, g.updated_at, g.chat_ts, - -- GroupInfo {membership} - mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, - mu.member_status, mu.show_messages, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, - -- GroupInfo {membership = GroupMember {memberProfile}} - pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, - -- from GroupMember - m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, - c.peer_chat_min_version, c.peer_chat_max_version - FROM group_members m - JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) - JOIN groups g ON g.group_id = m.group_id - JOIN group_profiles gp USING (group_profile_id) - JOIN group_members mu ON g.group_id = mu.group_id - JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) - LEFT JOIN connections c ON c.connection_id = ( - SELECT max(cc.connection_id) - FROM connections cc - where cc.user_id = ? AND cc.group_member_id = m.group_member_id - ) - WHERE m.group_member_id = ? AND g.user_id = ? AND mu.contact_id = ? - |] - (userId, groupMemberId, userId, userContactId) +getGroupAndMember :: DB.Connection -> User -> Int64 -> VersionRangeChat -> ExceptT StoreError IO (GroupInfo, GroupMember) +getGroupAndMember db User {userId, userContactId} groupMemberId vr = do + gm <- + ExceptT . firstRow toGroupAndMember (SEInternalError "referenced group member not found") $ + DB.query + db + [sql| + SELECT + -- GroupInfo + g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + -- GroupInfo {membership} + mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, + mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, + -- GroupInfo {membership = GroupMember {memberProfile}} + pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at, + -- from GroupMember + m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + JOIN groups g ON g.group_id = m.group_id + JOIN group_profiles gp USING (group_profile_id) + JOIN group_members mu ON g.group_id = mu.group_id + JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) + LEFT JOIN connections c ON c.connection_id = ( + SELECT max(cc.connection_id) + FROM connections cc + where cc.user_id = ? AND cc.group_member_id = m.group_member_id + ) + WHERE m.group_member_id = ? AND g.user_id = ? AND mu.contact_id = ? + |] + (userId, groupMemberId, userId, userContactId) + liftIO $ bitraverse (addGroupChatTags db) pure gm where toGroupAndMember :: (GroupInfoRow :. GroupMemberRow :. MaybeConnectionRow) -> (GroupInfo, GroupMember) toGroupAndMember (groupInfoRow :. memberRow :. connRow) = - let groupInfo = toGroupInfo vr userContactId groupInfoRow + let groupInfo = toGroupInfo vr userContactId [] groupInfoRow member = toGroupMember userContactId memberRow - in (groupInfo, (member :: GroupMember) {activeConn = toMaybeConnection connRow}) + in (groupInfo, (member :: GroupMember) {activeConn = toMaybeConnection vr connRow}) -- | creates completely new group with a single member - the current user -createNewGroup :: DB.Connection -> VersionRange -> TVar ChaChaDRG -> User -> GroupProfile -> Maybe Profile -> ExceptT StoreError IO GroupInfo +createNewGroup :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> GroupProfile -> Maybe Profile -> ExceptT StoreError IO GroupInfo createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = ExceptT $ do - let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile + let GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} = groupProfile fullGroupPreferences = mergeGroupPreferences groupPreferences currentTs <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile @@ -304,23 +326,46 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc groupId <- liftIO $ do DB.execute db - "INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" - (displayName, fullName, description, image, userId, groupPreferences, currentTs, currentTs) + "INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, member_admission, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" + (displayName, fullName, description, image, userId, groupPreferences, memberAdmission, currentTs, currentTs) profileId <- insertedRowId db DB.execute db - "INSERT INTO groups (local_display_name, user_id, group_profile_id, enable_ntfs, created_at, updated_at, chat_ts) VALUES (?,?,?,?,?,?,?)" - (ldn, userId, profileId, True, currentTs, currentTs, currentTs) + [sql| + INSERT INTO groups + (local_display_name, user_id, group_profile_id, enable_ntfs, + created_at, updated_at, chat_ts, user_member_profile_sent_at) + VALUES (?,?,?,?,?,?,?,?) + |] + (ldn, userId, profileId, BI True, currentTs, currentTs, currentTs, currentTs) insertedRowId db memberId <- liftIO $ encodedRandomBytes gVar 12 membership <- createContactMemberInv_ db user groupId Nothing user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser customUserProfileId currentTs vr let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False} - pure GroupInfo {groupId, localDisplayName = ldn, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId = Nothing, chatSettings, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs} + pure + GroupInfo + { groupId, + localDisplayName = ldn, + groupProfile, + localAlias = "", + businessChat = Nothing, + fullGroupPreferences, + membership, + chatSettings, + createdAt = currentTs, + updatedAt = currentTs, + chatTs = Just currentTs, + userMemberProfileSentAt = Just currentTs, + chatTags = [], + chatItemTTL = Nothing, + uiThemes = Nothing, + customData = Nothing + } -- | creates a new group record for the group the current user was invited to, or returns an existing one -createGroupInvitation :: DB.Connection -> VersionRange -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO (GroupInfo, GroupMemberId) +createGroupInvitation :: DB.Connection -> VersionRangeChat -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO (GroupInfo, GroupMemberId) createGroupInvitation _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ = throwError $ SEContactNotReady localDisplayName -createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activeConn = Just hostConn@Connection {customUserProfileId}} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile} incognitoProfileId = do +createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activeConn = Just Connection {peerChatVRange}} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile, business} incognitoProfileId = do liftIO getInvitationGroupId_ >>= \case Nothing -> createGroupInvitation_ Just gId -> do @@ -342,7 +387,7 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ DB.query db "SELECT group_id FROM groups WHERE inv_queue_info = ? AND user_id = ? LIMIT 1" (connRequest, userId) createGroupInvitation_ :: ExceptT StoreError IO (GroupInfo, GroupMemberId) createGroupInvitation_ = do - let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile + let GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} = groupProfile fullGroupPreferences = mergeGroupPreferences groupPreferences ExceptT $ withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do @@ -350,27 +395,62 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ groupId <- liftIO $ do DB.execute db - "INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" - (displayName, fullName, description, image, userId, groupPreferences, currentTs, currentTs) + "INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, member_admission, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" + (displayName, fullName, description, image, userId, groupPreferences, memberAdmission, currentTs, currentTs) profileId <- insertedRowId db DB.execute db - "INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, host_conn_custom_user_profile_id, user_id, enable_ntfs, created_at, updated_at, chat_ts) VALUES (?,?,?,?,?,?,?,?,?)" - (profileId, localDisplayName, connRequest, customUserProfileId, userId, True, currentTs, currentTs, currentTs) + [sql| + INSERT INTO groups + (group_profile_id, local_display_name, inv_queue_info, user_id, enable_ntfs, + created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat, business_member_id, customer_member_id) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + |] + ((profileId, localDisplayName, connRequest, userId, BI True, currentTs, currentTs, currentTs, currentTs) :. businessChatInfoRow business) insertedRowId db - let JVersionRange hostVRange = hostConn.peerChatVRange + let hostVRange = adjustedMemberVRange vr peerChatVRange GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId Nothing contact fromMember GCHostMember GSMemInvited IBUnknown Nothing currentTs hostVRange membership <- createContactMemberInv_ db user groupId (Just groupMemberId) user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfileId currentTs vr let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False} - pure (GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId = customUserProfileId, chatSettings, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs}, groupMemberId) + pure + ( GroupInfo + { groupId, + localDisplayName, + groupProfile, + localAlias = "", + businessChat = Nothing, + fullGroupPreferences, + membership, + chatSettings, + createdAt = currentTs, + updatedAt = currentTs, + chatTs = Just currentTs, + userMemberProfileSentAt = Just currentTs, + chatTags = [], + chatItemTTL = Nothing, + uiThemes = Nothing, + customData = Nothing + }, + groupMemberId + ) + +businessChatInfoRow :: Maybe BusinessChatInfo -> BusinessChatInfoRow +businessChatInfoRow = \case + Just BusinessChatInfo {chatType, businessId, customerId} -> (Just chatType, Just businessId, Just customerId) + Nothing -> (Nothing, Nothing, Nothing) + +adjustedMemberVRange :: VersionRangeChat -> VersionRangeChat -> VersionRangeChat +adjustedMemberVRange chatVR vr@(VersionRange minV maxV) = + let maxV' = min maxV (maxVersion chatVR) + in fromMaybe vr $ safeVersionRange minV (max minV maxV') getHostMemberId_ :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO GroupMemberId getHostMemberId_ db User {userId} groupId = ExceptT . firstRow fromOnly (SEHostMemberIdNotFound groupId) $ DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND group_id = ? AND member_category = ?" (userId, groupId, GCHostMember) -createContactMemberInv_ :: IsContact a => DB.Connection -> User -> GroupId -> Maybe GroupMemberId -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe ProfileId -> UTCTime -> VersionRange -> ExceptT StoreError IO GroupMember -createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMemberId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy incognitoProfileId createdAt memberChatVRange@(VersionRange minV maxV) = do +createContactMemberInv_ :: IsContact a => DB.Connection -> User -> GroupId -> Maybe GroupMemberId -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe ProfileId -> UTCTime -> VersionRangeChat -> ExceptT StoreError IO GroupMember +createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMemberId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy incognitoProfileId createdAt vr = do incognitoProfile <- forM incognitoProfileId $ \profileId -> getProfileById db userId profileId (localDisplayName, memberProfile) <- case (incognitoProfile, incognitoProfileId) of (Just profile@LocalProfile {displayName}, Just profileId) -> @@ -386,6 +466,7 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe memberCategory, memberStatus, memberSettings = defaultMemberSettings, + blockedByAdmin = False, invitedBy, invitedByGroupMemberId, localDisplayName, @@ -393,9 +474,12 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe memberContactId = Just $ contactId' userOrContact, memberContactProfileId = localProfileId (profile' userOrContact), activeConn = Nothing, - memberChatVRange = JVersionRange memberChatVRange + memberChatVRange, + createdAt, + updatedAt = createdAt } where + memberChatVRange@(VersionRange minV maxV) = vr insertMember_ :: IO ContactName insertMember_ = do let localDisplayName = localDisplayName' userOrContact @@ -431,54 +515,79 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe ) pure $ Right incognitoLdn -createGroupInvitedViaLink :: DB.Connection -> VersionRange -> User -> Connection -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember) -createGroupInvitedViaLink +deleteContactCardKeepConn :: DB.Connection -> Int64 -> Contact -> IO () +deleteContactCardKeepConn db connId Contact {contactId, profile = LocalProfile {profileId}} = do + DB.execute db "UPDATE connections SET contact_id = NULL WHERE connection_id = ?" (Only connId) + DB.execute db "DELETE FROM contacts WHERE contact_id = ?" (Only contactId) + DB.execute db "DELETE FROM contact_profiles WHERE contact_profile_id = ?" (Only profileId) + +createGroupInvitedViaLink :: DB.Connection -> VersionRangeChat -> User -> Connection -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember) +createGroupInvitedViaLink db vr user conn GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, accepted, business} = do + let fromMemberProfile = profileFromName fromMemberName + initialStatus = maybe GSMemAccepted acceptanceToStatus accepted + createGroupViaLink' db vr user conn fromMember fromMemberProfile invitedMember groupProfile business initialStatus + +createGroupRejectedViaLink :: DB.Connection -> VersionRangeChat -> User -> Connection -> GroupLinkRejection -> ExceptT StoreError IO (GroupInfo, GroupMember) +createGroupRejectedViaLink db vr user conn GroupLinkRejection {fromMember = fromMember@MemberIdRole {memberId}, invitedMember, groupProfile} = do + let fromMemberProfile = profileFromName $ nameFromMemberId memberId + createGroupViaLink' db vr user conn fromMember fromMemberProfile invitedMember groupProfile Nothing GSMemRejected + +createGroupViaLink' :: DB.Connection -> VersionRangeChat -> User -> Connection -> MemberIdRole -> Profile -> MemberIdRole -> GroupProfile -> Maybe BusinessChatInfo -> GroupMemberStatus -> ExceptT StoreError IO (GroupInfo, GroupMember) +createGroupViaLink' db vr user@User {userId, userContactId} Connection {connId, customUserProfileId} - GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile} = do + fromMember + fromMemberProfile + invitedMember + groupProfile + business + memStatus = do currentTs <- liftIO getCurrentTime groupId <- insertGroup_ currentTs hostMemberId <- insertHost_ currentTs groupId liftIO $ DB.execute db "UPDATE connections SET conn_type = ?, group_member_id = ?, updated_at = ? WHERE connection_id = ?" (ConnMember, hostMemberId, currentTs, connId) -- using IBUnknown since host is created without contact - void $ createContactMemberInv_ db user groupId (Just hostMemberId) user invitedMember GCUserMember GSMemAccepted IBUnknown customUserProfileId currentTs vr + void $ createContactMemberInv_ db user groupId (Just hostMemberId) user invitedMember GCUserMember memStatus IBUnknown customUserProfileId currentTs vr liftIO $ setViaGroupLinkHash db groupId connId - (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db user hostMemberId + (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user hostMemberId where insertGroup_ currentTs = ExceptT $ do - let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile + let GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} = groupProfile withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do liftIO $ do DB.execute db - "INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" - (displayName, fullName, description, image, userId, groupPreferences, currentTs, currentTs) + "INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, member_admission, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" + (displayName, fullName, description, image, userId, groupPreferences, memberAdmission, currentTs, currentTs) profileId <- insertedRowId db - DB.execute - db - "INSERT INTO groups (group_profile_id, local_display_name, host_conn_custom_user_profile_id, user_id, enable_ntfs, created_at, updated_at, chat_ts) VALUES (?,?,?,?,?,?,?,?)" - (profileId, localDisplayName, customUserProfileId, userId, True, currentTs, currentTs, currentTs) - insertedRowId db - insertHost_ currentTs groupId = ExceptT $ do - let fromMemberProfile = profileFromName fromMemberName - withLocalDisplayName db userId fromMemberName $ \localDisplayName -> runExceptT $ do - (_, profileId) <- createNewMemberProfile_ db user fromMemberProfile currentTs - let MemberIdRole {memberId, memberRole} = fromMember - liftIO $ do DB.execute db [sql| - INSERT INTO group_members - ( group_id, member_id, member_role, member_category, member_status, invited_by, - user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + INSERT INTO groups + (group_profile_id, local_display_name, user_id, enable_ntfs, + created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat, business_member_id, customer_member_id) + VALUES (?,?,?,?,?,?,?,?,?,?,?) |] - ( (groupId, memberId, memberRole, GCHostMember, GSMemAccepted, fromInvitedBy userContactId IBUnknown) - :. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs) - ) + ((profileId, localDisplayName, userId, BI True, currentTs, currentTs, currentTs, currentTs) :. businessChatInfoRow business) insertedRowId db + insertHost_ currentTs groupId = do + (localDisplayName, profileId) <- createNewMemberProfile_ db user fromMemberProfile currentTs + let MemberIdRole {memberId, memberRole} = fromMember + liftIO $ do + DB.execute + db + [sql| + INSERT INTO group_members + ( group_id, member_id, member_role, member_category, member_status, invited_by, + user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (groupId, memberId, memberRole, GCHostMember, memStatus, fromInvitedBy userContactId IBUnknown) + :. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs) + ) + insertedRowId db setViaGroupLinkHash :: DB.Connection -> GroupId -> Int64 -> IO () setViaGroupLinkHash db groupId connId = @@ -498,45 +607,132 @@ setGroupInvitationChatItemId db User {userId} groupId chatItemId = do -- TODO return the last connection that is ready, not any last connection -- requires updating connection status -getGroup :: DB.Connection -> VersionRange -> User -> GroupId -> ExceptT StoreError IO Group +getGroup :: DB.Connection -> VersionRangeChat -> User -> GroupId -> ExceptT StoreError IO Group getGroup db vr user groupId = do gInfo <- getGroupInfo db vr user groupId - members <- liftIO $ getGroupMembers db user gInfo + members <- liftIO $ getGroupMembers db vr user gInfo pure $ Group gInfo members -deleteGroupConnectionsAndFiles :: DB.Connection -> User -> GroupInfo -> [GroupMember] -> IO () -deleteGroupConnectionsAndFiles db User {userId} GroupInfo {groupId} members = do - forM_ members $ \m -> DB.execute db "DELETE FROM connections WHERE user_id = ? AND group_member_id = ?" (userId, groupMemberId' m) - DB.execute db "DELETE FROM files WHERE user_id = ? AND group_id = ?" (userId, groupId) - -deleteGroupItemsAndMembers :: DB.Connection -> User -> GroupInfo -> [GroupMember] -> IO () -deleteGroupItemsAndMembers db user@User {userId} g@GroupInfo {groupId} members = do - DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND group_id = ?" (userId, groupId) - void $ runExceptT cleanupHostGroupLinkConn_ -- to allow repeat connection via the same group link if one was used - DB.execute db "DELETE FROM group_members WHERE user_id = ? AND group_id = ?" (userId, groupId) - forM_ members $ cleanupMemberProfileAndName_ db user - forM_ (incognitoMembershipProfile g) $ deleteUnusedIncognitoProfileById_ db user . localProfileId +getGroupToSubscribe :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO ShortGroup +getGroupToSubscribe db User {userId, userContactId} groupId = do + shortInfo <- getGroupInfoToSubscribe + members <- liftIO getGroupMembersToSubscribe + pure $ ShortGroup shortInfo members where - cleanupHostGroupLinkConn_ = do - hostId <- getHostMemberId_ db user groupId - liftIO $ - DB.execute + getGroupInfoToSubscribe :: ExceptT StoreError IO ShortGroupInfo + getGroupInfoToSubscribe = ExceptT $ do + firstRow toInfo (SEGroupNotFound groupId) $ + DB.query db [sql| - UPDATE connections SET via_contact_uri_hash = NULL, xcontact_id = NULL - WHERE user_id = ? AND via_group_link = 1 AND contact_id IN ( - SELECT contact_id - FROM group_members - WHERE user_id = ? AND group_member_id = ? - ) + SELECT g.local_display_name, mu.member_status + FROM groups g + JOIN group_members mu ON mu.group_id = g.group_id + WHERE g.group_id = ? AND g.user_id = ? AND mu.contact_id = ? + AND mu.member_status NOT IN (?,?,?) |] - (userId, userId, hostId) + (groupId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted) + where + toInfo :: (GroupName, GroupMemberStatus) -> ShortGroupInfo + toInfo (groupName, membershipStatus) = + ShortGroupInfo groupId groupName membershipStatus + getGroupMembersToSubscribe :: IO [ShortGroupMember] + getGroupMembersToSubscribe = do + map toShortMember + <$> DB.query + db + [sql| + SELECT m.group_member_id, m.local_display_name, c.agent_conn_id + FROM group_members m + JOIN connections c ON c.connection_id = ( + SELECT max(cc.connection_id) + FROM connections cc + WHERE cc.user_id = ? AND cc.group_member_id = m.group_member_id + ) + WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) + AND m.member_status NOT IN (?,?,?) + |] + (userId, userId, groupId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted) + where + toShortMember :: (GroupMemberId, ContactName, AgentConnId) -> ShortGroupMember + toShortMember (groupMemberId, localDisplayName, agentConnId) = + ShortGroupMember groupMemberId groupId localDisplayName agentConnId + +deleteGroupChatItems :: DB.Connection -> User -> GroupInfo -> IO () +deleteGroupChatItems db User {userId} GroupInfo {groupId} = + DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND group_id = ?" (userId, groupId) + +deleteGroupMembers :: DB.Connection -> User -> GroupInfo -> IO () +deleteGroupMembers db User {userId} GroupInfo {groupId} = do + DB.execute_ db "DROP TABLE IF EXISTS temp_delete_members" +#if defined(dbPostgres) + DB.execute_ db "CREATE TABLE temp_delete_members (contact_profile_id BIGINT, member_profile_id BIGINT, local_display_name TEXT)" +#else + DB.execute_ db "CREATE TABLE temp_delete_members (contact_profile_id INTEGER, member_profile_id INTEGER, local_display_name TEXT)" +#endif + DB.execute + db + [sql| + INSERT INTO temp_delete_members (contact_profile_id, member_profile_id, local_display_name) + SELECT contact_profile_id, member_profile_id, local_display_name FROM group_members WHERE group_id = ? + |] + (Only groupId) + DB.execute db "DELETE FROM group_members WHERE user_id = ? AND group_id = ?" (userId, groupId) + DB.execute + db + [sql| + DELETE FROM contact_profiles + WHERE + user_id = ? + AND (contact_profile_id IN (SELECT contact_profile_id FROM temp_delete_members) + OR contact_profile_id IN (SELECT member_profile_id FROM temp_delete_members WHERE member_profile_id IS NOT NULL)) + AND contact_profile_id NOT IN (SELECT contact_profile_id FROM group_members) + AND contact_profile_id NOT IN (SELECT member_profile_id FROM group_members) + AND contact_profile_id NOT IN (SELECT contact_profile_id FROM contacts) + AND contact_profile_id NOT IN (SELECT contact_profile_id FROM contact_requests) + AND contact_profile_id NOT IN (SELECT custom_user_profile_id FROM connections) + |] + (Only userId) + DB.execute + db + [sql| + DELETE FROM display_names + WHERE + user_id = ? + AND local_display_name IN (SELECT local_display_name FROM temp_delete_members) + AND local_display_name NOT IN (SELECT local_display_name FROM group_members) + AND local_display_name NOT IN (SELECT local_display_name FROM contacts) + AND local_display_name NOT IN (SELECT local_display_name FROM users) + AND local_display_name NOT IN (SELECT local_display_name FROM groups) + AND local_display_name NOT IN (SELECT local_display_name FROM user_contact_links) + AND local_display_name NOT IN (SELECT local_display_name FROM contact_requests) + |] + (Only userId) + DB.execute_ db "DROP TABLE temp_delete_members" + +-- to allow repeat connection via the same group link if one was used +cleanupHostGroupLinkConn :: DB.Connection -> User -> GroupInfo -> IO () +cleanupHostGroupLinkConn db user@User {userId} GroupInfo {groupId} = do + runExceptT (getHostMemberId_ db user groupId) >>= \case + Left _ -> pure () + Right hostId -> + DB.execute + db + [sql| + UPDATE connections SET via_contact_uri_hash = NULL, xcontact_id = NULL + WHERE user_id = ? AND via_group_link = 1 AND contact_id IN ( + SELECT contact_id + FROM group_members + WHERE user_id = ? AND group_member_id = ? + ) + |] + (userId, userId, hostId) deleteGroup :: DB.Connection -> User -> GroupInfo -> IO () deleteGroup db user@User {userId} g@GroupInfo {groupId, localDisplayName} = do deleteGroupProfile_ db userId groupId DB.execute db "DELETE FROM groups WHERE user_id = ? AND group_id = ?" (userId, groupId) - DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) + safeDeleteLDN db user localDisplayName forM_ (incognitoMembershipProfile g) $ deleteUnusedIncognitoProfileById_ db user . localProfileId deleteGroupProfile_ :: DB.Connection -> UserId -> GroupId -> IO () @@ -553,32 +749,41 @@ deleteGroupProfile_ db userId groupId = |] (userId, groupId) -getUserGroups :: DB.Connection -> VersionRange -> User -> IO [Group] -getUserGroups db vr user@User {userId} = do +getUserGroupsToSubscribe :: DB.Connection -> User -> IO [ShortGroup] +getUserGroupsToSubscribe db user@User {userId} = do groupIds <- map fromOnly <$> DB.query db "SELECT group_id FROM groups WHERE user_id = ?" (Only userId) - rights <$> mapM (runExceptT . getGroup db vr user) groupIds + rights <$> mapM (runExceptT . getGroupToSubscribe db user) groupIds -getUserGroupDetails :: DB.Connection -> VersionRange -> User -> Maybe ContactId -> Maybe String -> IO [GroupInfo] -getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = - map (toGroupInfo vr userContactId) - <$> DB.query - db - [sql| - SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.created_at, g.updated_at, g.chat_ts, - mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, - mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences - FROM groups g - JOIN group_profiles gp USING (group_profile_id) - JOIN group_members mu USING (group_id) - JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) - WHERE g.user_id = ? AND mu.contact_id = ? - AND (gp.display_name LIKE '%' || ? || '%' OR gp.full_name LIKE '%' || ? || '%' OR gp.description LIKE '%' || ? || '%') - |] - (userId, userContactId, search, search, search) +getUserGroupDetails :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe String -> IO [GroupInfo] +getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = do + g_ <- + map (toGroupInfo vr userContactId []) + <$> DB.query + db + [sql| + SELECT + g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, + mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at + FROM groups g + JOIN group_profiles gp USING (group_profile_id) + JOIN group_members mu USING (group_id) + JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) + WHERE g.user_id = ? AND mu.contact_id = ? + AND (LOWER(gp.display_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(gp.full_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(gp.description) LIKE '%' || LOWER(?) || '%' + ) + |] + (userId, userContactId, search, search, search) + mapM (addGroupChatTags db) g_ where search = fromMaybe "" search_ -getUserGroupsWithSummary :: DB.Connection -> VersionRange -> User -> Maybe ContactId -> Maybe String -> IO [(GroupInfo, GroupSummary)] +getUserGroupsWithSummary :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe String -> IO [(GroupInfo, GroupSummary)] getUserGroupsWithSummary db vr user _contactId_ search_ = getUserGroupDetails db vr user _contactId_ search_ >>= mapM (\g@GroupInfo {groupId} -> (g,) <$> getGroupSummary db user groupId) @@ -596,20 +801,18 @@ getGroupSummary db User {userId} groupId = do JOIN group_members m USING (group_id) WHERE g.user_id = ? AND g.group_id = ? - AND m.member_status != ? - AND m.member_status != ? - AND m.member_status != ? + AND m.member_status NOT IN (?,?,?,?,?) |] - (userId, groupId, GSMemRemoved, GSMemLeft, GSMemInvited) + (userId, groupId, GSMemRejected, GSMemRemoved, GSMemLeft, GSMemUnknown, GSMemInvited) pure GroupSummary {currentMembers = fromMaybe 0 currentMembers_} -getContactGroupPreferences :: DB.Connection -> User -> Contact -> IO [FullGroupPreferences] +getContactGroupPreferences :: DB.Connection -> User -> Contact -> IO [(GroupMemberRole, FullGroupPreferences)] getContactGroupPreferences db User {userId} Contact {contactId} = do - map (mergeGroupPreferences . fromOnly) + map (second mergeGroupPreferences) <$> DB.query db [sql| - SELECT gp.preferences + SELECT m.member_role, gp.preferences FROM groups g JOIN group_profiles gp USING (group_profile_id) JOIN group_members m USING (group_id) @@ -617,11 +820,7 @@ getContactGroupPreferences db User {userId} Contact {contactId} = do |] (userId, contactId) -checkContactHasGroups :: DB.Connection -> User -> Contact -> IO (Maybe GroupId) -checkContactHasGroups db User {userId} Contact {contactId} = - maybeFirstRow fromOnly $ DB.query db "SELECT group_id FROM group_members WHERE user_id = ? AND contact_id = ? LIMIT 1" (userId, contactId) - -getGroupInfoByName :: DB.Connection -> VersionRange -> User -> GroupName -> ExceptT StoreError IO GroupInfo +getGroupInfoByName :: DB.Connection -> VersionRangeChat -> User -> GroupName -> ExceptT StoreError IO GroupInfo getGroupInfoByName db vr user gName = do gId <- getGroupIdByName db user gName getGroupInfo db vr user gId @@ -630,11 +829,13 @@ groupMemberQuery :: Query groupMemberQuery = [sql| SELECT - m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, + m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, - c.peer_chat_min_version, c.peer_chat_max_version + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.connection_id = ( @@ -644,57 +845,98 @@ groupMemberQuery = ) |] -getGroupMember :: DB.Connection -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember -getGroupMember db user@User {userId} groupId groupMemberId = - ExceptT . firstRow (toContactMember user) (SEGroupMemberNotFound groupMemberId) $ +getGroupMember :: DB.Connection -> VersionRangeChat -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember +getGroupMember db vr user@User {userId} groupId groupMemberId = + ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFound groupMemberId) $ DB.query db (groupMemberQuery <> " WHERE m.group_id = ? AND m.group_member_id = ? AND m.user_id = ?") (userId, groupId, groupMemberId, userId) -getGroupMemberById :: DB.Connection -> User -> GroupMemberId -> ExceptT StoreError IO GroupMember -getGroupMemberById db user@User {userId} groupMemberId = - ExceptT . firstRow (toContactMember user) (SEGroupMemberNotFound groupMemberId) $ +getMentionedGroupMember :: DB.Connection -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO CIMention +getMentionedGroupMember db User {userId} groupId gmId = + ExceptT $ + firstRow toMentionedMember (SEGroupMemberNotFound gmId) $ + DB.query + db + (mentionedMemberQuery <> " WHERE m.group_id = ? AND m.group_member_id = ? AND m.user_id = ?") + (groupId, gmId, userId) + +getMentionedMemberByMemberId :: DB.Connection -> User -> GroupId -> MsgMention -> IO CIMention +getMentionedMemberByMemberId db User {userId} groupId MsgMention {memberId} = + fmap (fromMaybe mentionedMember) $ + maybeFirstRow toMentionedMember $ + DB.query + db + (mentionedMemberQuery <> " WHERE m.group_id = ? AND m.member_id = ? AND m.user_id = ?") + (groupId, memberId, userId) + where + mentionedMember = CIMention {memberId, memberRef = Nothing} + +mentionedMemberQuery :: Query +mentionedMemberQuery = + [sql| + SELECT m.group_member_id, m.member_id, m.member_role, p.display_name, p.local_alias + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + |] + +toMentionedMember :: (GroupMemberId, MemberId, GroupMemberRole, Text, Maybe Text) -> CIMention +toMentionedMember (groupMemberId, memberId, memberRole, displayName, localAlias) = + let memberRef = Just CIMentionMember {groupMemberId, displayName, localAlias, memberRole} + in CIMention {memberId, memberRef} + +getGroupMemberById :: DB.Connection -> VersionRangeChat -> User -> GroupMemberId -> ExceptT StoreError IO GroupMember +getGroupMemberById db vr user@User {userId} groupMemberId = + ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFound groupMemberId) $ DB.query db (groupMemberQuery <> " WHERE m.group_member_id = ? AND m.user_id = ?") (userId, groupMemberId, userId) -getGroupMemberByMemberId :: DB.Connection -> User -> GroupInfo -> MemberId -> ExceptT StoreError IO GroupMember -getGroupMemberByMemberId db user@User {userId} GroupInfo {groupId} memberId = - ExceptT . firstRow (toContactMember user) (SEGroupMemberNotFoundByMemberId memberId) $ +getGroupMemberByMemberId :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> MemberId -> ExceptT StoreError IO GroupMember +getGroupMemberByMemberId db vr user@User {userId} GroupInfo {groupId} memberId = + ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFoundByMemberId memberId) $ DB.query db (groupMemberQuery <> " WHERE m.group_id = ? AND m.member_id = ?") (userId, groupId, memberId) -getGroupMembers :: DB.Connection -> User -> GroupInfo -> IO [GroupMember] -getGroupMembers db user@User {userId, userContactId} GroupInfo {groupId} = do - map (toContactMember user) +getGroupMembers :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember] +getGroupMembers db vr user@User {userId, userContactId} GroupInfo {groupId} = do + map (toContactMember vr user) <$> DB.query db - (groupMemberQuery <> " WHERE m.group_id = ? AND m.user_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?)") - (userId, groupId, userId, userContactId) + (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?)") + (userId, userId, groupId, userContactId) -getGroupMembersForExpiration :: DB.Connection -> User -> GroupInfo -> IO [GroupMember] -getGroupMembersForExpiration db user@User {userId, userContactId} GroupInfo {groupId} = do - map (toContactMember user) +getGroupModerators :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember] +getGroupModerators db vr user@User {userId, userContactId} GroupInfo {groupId} = do + map (toContactMember vr user) + <$> DB.query + db + (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?,?)") + (userId, userId, groupId, userContactId, GRModerator, GRAdmin, GROwner) + +getGroupMembersForExpiration :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember] +getGroupMembersForExpiration db vr user@User {userId, userContactId} GroupInfo {groupId} = do + map (toContactMember vr user) <$> DB.query db ( groupMemberQuery <> [sql| WHERE m.group_id = ? AND m.user_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) - AND m.member_status IN (?, ?, ?) + AND m.member_status IN (?, ?, ?, ?) AND m.group_member_id NOT IN ( SELECT DISTINCT group_member_id FROM chat_items ) |] ) - (userId, groupId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted) + (userId, groupId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted, GSMemUnknown) -toContactMember :: User -> (GroupMemberRow :. MaybeConnectionRow) -> GroupMember -toContactMember User {userContactId} (memberRow :. connRow) = - (toGroupMember userContactId memberRow) {activeConn = toMaybeConnection connRow} +toContactMember :: VersionRangeChat -> User -> (GroupMemberRow :. MaybeConnectionRow) -> GroupMember +toContactMember vr User {userContactId} (memberRow :. connRow) = + (toGroupMember userContactId memberRow) {activeConn = toMaybeConnection vr connRow} getGroupCurrentMembersCount :: DB.Connection -> User -> GroupInfo -> IO Int getGroupCurrentMembersCount db User {userId} GroupInfo {groupId} = do @@ -710,14 +952,14 @@ getGroupCurrentMembersCount db User {userId} GroupInfo {groupId} = do (groupId, userId) pure $ length $ filter memberCurrent' statuses -getGroupInvitation :: DB.Connection -> VersionRange -> User -> GroupId -> ExceptT StoreError IO ReceivedGroupInvitation +getGroupInvitation :: DB.Connection -> VersionRangeChat -> User -> GroupId -> ExceptT StoreError IO ReceivedGroupInvitation getGroupInvitation db vr user groupId = getConnRec_ user >>= \case Just connRequest -> do groupInfo@GroupInfo {membership} <- getGroupInfo db vr user groupId when (memberStatus membership /= GSMemInvited) $ throwError SEGroupAlreadyJoined hostId <- getHostMemberId_ db user groupId - fromMember <- getGroupMember db user groupId hostId + fromMember <- getGroupMember db vr user groupId hostId pure ReceivedGroupInvitation {fromMember, connRequest, groupInfo} _ -> throwError SEGroupInvitationNotFound where @@ -728,14 +970,14 @@ getGroupInvitation db vr user groupId = createNewContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> SubscriptionMode -> ExceptT StoreError IO GroupMember createNewContactMember _ _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ _ _ = throwError $ SEContactNotReady localDisplayName -createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile, activeConn = Just Connection {peerChatVRange}} memberRole agentConnId connRequest subMode = +createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile, activeConn = Just Connection {connChatVersion, peerChatVRange}} memberRole agentConnId connRequest subMode = createWithRandomId gVar $ \memId -> do createdAt <- liftIO getCurrentTime member@GroupMember {groupMemberId} <- createMember_ (MemberId memId) createdAt - void $ createMemberConnection_ db userId groupMemberId agentConnId (fromJVersionRange peerChatVRange) Nothing 0 createdAt subMode + void $ createMemberConnection_ db userId groupMemberId agentConnId connChatVersion peerChatVRange Nothing 0 createdAt subMode pure member where - JVersionRange (VersionRange minV maxV) = peerChatVRange + VersionRange minV maxV = peerChatVRange invitedByGroupMemberId = groupMemberId' membership createMember_ memberId createdAt = do insertMember_ @@ -749,6 +991,7 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, memberCategory = GCInviteeMember, memberStatus = GSMemInvited, memberSettings = defaultMemberSettings, + blockedByAdmin = False, invitedBy = IBUser, invitedByGroupMemberId = Just invitedByGroupMemberId, localDisplayName, @@ -756,7 +999,9 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, memberContactId = Just contactId, memberContactProfileId = localProfileId profile, activeConn = Nothing, - memberChatVRange = peerChatVRange + memberChatVRange = peerChatVRange, + createdAt, + updatedAt = createdAt } where insertMember_ = @@ -774,13 +1019,13 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, :. (minV, maxV) ) -createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> VersionRange -> SubscriptionMode -> ExceptT StoreError IO () -createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) peerChatVRange subMode = +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 = createWithRandomId gVar $ \memId -> do createdAt <- liftIO getCurrentTime insertMember_ (MemberId memId) createdAt groupMemberId <- liftIO $ insertedRowId db - Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 createdAt subMode + Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange Nothing 0 createdAt subMode setCommandConnId db user cmdId connId where VersionRange minV maxV = peerChatVRange @@ -799,23 +1044,22 @@ createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo :. (minV, maxV) ) -createAcceptedMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> UserContactRequest -> GroupMemberRole -> ExceptT StoreError IO (GroupMemberId, MemberId) -createAcceptedMember +createJoiningMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> UserContactRequest -> GroupMemberRole -> GroupMemberStatus -> ExceptT StoreError IO (GroupMemberId, MemberId) +createJoiningMember db gVar User {userId, userContactId} GroupInfo {groupId, membership} UserContactRequest {cReqChatVRange, localDisplayName, profileId} - memberRole = do - liftIO $ - DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) + memberRole + memberStatus = createWithRandomId gVar $ \memId -> do createdAt <- liftIO getCurrentTime insertMember_ (MemberId memId) createdAt groupMemberId <- liftIO $ insertedRowId db pure (groupMemberId, MemberId memId) where - JVersionRange (VersionRange minV maxV) = cReqChatVRange + VersionRange minV maxV = cReqChatVRange insertMember_ memberId createdAt = DB.execute db @@ -826,25 +1070,89 @@ createAcceptedMember peer_chat_min_version, peer_chat_max_version) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (groupId, memberId, memberRole, GCInviteeMember, GSMemAccepted, fromInvitedBy userContactId IBUser, groupMemberId' membership) + ( (groupId, memberId, memberRole, GCInviteeMember, memberStatus, fromInvitedBy userContactId IBUser, groupMemberId' membership) :. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, createdAt, createdAt) :. (minV, maxV) ) -createAcceptedMemberConnection :: DB.Connection -> User -> (CommandId, ConnId) -> UserContactRequest -> GroupMemberId -> SubscriptionMode -> IO () -createAcceptedMemberConnection +createJoiningMemberConnection :: DB.Connection -> User -> (CommandId, ConnId) -> VersionChat -> UserContactRequest -> GroupMemberId -> SubscriptionMode -> IO () +createJoiningMemberConnection db user@User {userId} (cmdId, agentConnId) + chatV UserContactRequest {cReqChatVRange, userContactLinkId} groupMemberId subMode = do createdAt <- liftIO getCurrentTime - Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId (fromJVersionRange cReqChatVRange) Nothing (Just userContactLinkId) Nothing 0 createdAt subMode + Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV cReqChatVRange Nothing (Just userContactLinkId) Nothing 0 createdAt subMode PQSupportOff setCommandConnId db user cmdId connId -getContactViaMember :: DB.Connection -> User -> GroupMember -> ExceptT StoreError IO Contact -getContactViaMember db user@User {userId} GroupMember {groupMemberId} = do +createBusinessRequestGroup :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> UserContactRequest -> GroupPreferences -> ExceptT StoreError IO (GroupInfo, GroupMember) +createBusinessRequestGroup + db + vr + gVar + user@User {userId, userContactId} + UserContactRequest {cReqChatVRange, xContactId, profile = Profile {displayName, fullName, image, contactLink, preferences}} + groupPreferences = do + currentTs <- liftIO getCurrentTime + (groupId, membership@GroupMember {memberId = userMemberId}) <- insertGroup_ currentTs + (groupMemberId, memberId) <- insertClientMember_ currentTs groupId membership + liftIO $ DB.execute db "UPDATE groups SET business_member_id = ?, customer_member_id = ? WHERE group_id = ?" (userMemberId, memberId, groupId) + groupInfo <- getGroupInfo db vr user groupId + clientMember <- getGroupMemberById db vr user groupMemberId + pure (groupInfo, clientMember) + where + insertGroup_ currentTs = ExceptT $ + withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do + groupId <- liftIO $ do + DB.execute + db + "INSERT INTO group_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" + (displayName, fullName, image, userId, groupPreferences, currentTs, currentTs) + profileId <- insertedRowId db + DB.execute + db + [sql| + INSERT INTO groups + (group_profile_id, local_display_name, user_id, enable_ntfs, + created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat, business_xcontact_id) + VALUES (?,?,?,?,?,?,?,?,?,?) + |] + (profileId, localDisplayName, userId, BI True, currentTs, currentTs, currentTs, currentTs, BCCustomer, xContactId) + insertedRowId db + memberId <- liftIO $ encodedRandomBytes gVar 12 + membership <- createContactMemberInv_ db user groupId Nothing user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser Nothing currentTs vr + pure (groupId, membership) + VersionRange minV maxV = cReqChatVRange + insertClientMember_ currentTs groupId membership = ExceptT $ do + withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do + liftIO $ + DB.execute + db + "INSERT INTO contact_profiles (display_name, full_name, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" + (displayName, fullName, image, contactLink, userId, preferences, currentTs, currentTs) + profileId <- liftIO $ insertedRowId db + createWithRandomId gVar $ \memId -> do + DB.execute + db + [sql| + INSERT INTO group_members + ( group_id, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, + user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at, + peer_chat_min_version, peer_chat_max_version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (groupId, MemberId memId, GRMember, GCInviteeMember, GSMemAccepted, fromInvitedBy userContactId IBUser, groupMemberId' membership) + :. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs) + :. (minV, maxV) + ) + groupMemberId <- liftIO $ insertedRowId db + pure (groupMemberId, MemberId memId) + +getContactViaMember :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> ExceptT StoreError IO Contact +getContactViaMember db vr user@User {userId} GroupMember {groupMemberId} = do contactId <- ExceptT $ firstRow fromOnly (SEContactNotFoundByMemberId groupMemberId) $ @@ -858,7 +1166,7 @@ getContactViaMember db user@User {userId} GroupMember {groupMemberId} = do LIMIT 1 |] (userId, groupMemberId) - getContact db user contactId + getContact db vr user contactId setNewContactMemberConnRequest :: DB.Connection -> User -> GroupMember -> ConnReqInvitation -> IO () setNewContactMemberConnRequest db User {userId} GroupMember {groupMemberId} connRequest = do @@ -870,15 +1178,15 @@ getMemberInvitation db User {userId} groupMemberId = fmap join . maybeFirstRow fromOnly $ DB.query db "SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ?" (groupMemberId, userId) -createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionRange -> SubscriptionMode -> IO () -createMemberConnection db userId GroupMember {groupMemberId} agentConnId peerChatVRange subMode = do +createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionChat -> VersionRangeChat -> SubscriptionMode -> IO Connection +createMemberConnection db userId GroupMember {groupMemberId} agentConnId chatV peerChatVRange subMode = do currentTs <- getCurrentTime - void $ createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 currentTs subMode + createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange Nothing 0 currentTs subMode -createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> VersionRange -> SubscriptionMode -> IO () -createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) peerChatVRange subMode = do +createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> VersionChat -> VersionRangeChat -> SubscriptionMode -> IO () +createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) chatV peerChatVRange subMode = do currentTs <- getCurrentTime - Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 currentTs subMode + Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange Nothing 0 currentTs subMode setCommandConnId db user cmdId connId updateGroupMemberStatus :: DB.Connection -> UserId -> GroupMember -> GroupMemberStatus -> IO () @@ -896,6 +1204,19 @@ updateGroupMemberStatusById db userId groupMemberId memStatus = do |] (memStatus, currentTs, userId, groupMemberId) +updateGroupMemberAccepted :: DB.Connection -> User -> GroupMember -> GroupMemberRole -> IO GroupMember +updateGroupMemberAccepted db User {userId} m@GroupMember {groupMemberId} role = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + UPDATE group_members + SET member_status = ?, member_role = ?, updated_at = ? + WHERE user_id = ? AND group_member_id = ? + |] + (GSMemConnected, role, currentTs, userId, groupMemberId) + pure m {memberStatus = GSMemConnected, memberRole = role, updatedAt = currentTs} + -- | add new member with profile createNewGroupMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> GroupMemberCategory -> GroupMemberStatus -> ExceptT StoreError IO GroupMember createNewGroupMember db user gInfo invitingMember memInfo@MemberInfo {profile} memCategory memStatus = do @@ -906,6 +1227,7 @@ createNewGroupMember db user gInfo invitingMember memInfo@MemberInfo {profile} m { memInfo, memCategory, memStatus, + memRestriction = Nothing, memInvitedBy = IBUnknown, memInvitedByGroupMemberId = Just $ groupMemberId' invitingMember, localDisplayName, @@ -933,6 +1255,7 @@ createNewMember_ { memInfo = MemberInfo memberId memberRole memChatVRange memberProfile, memCategory = memberCategory, memStatus = memberStatus, + memRestriction, memInvitedBy = invitedBy, memInvitedByGroupMemberId, localDisplayName, @@ -942,17 +1265,17 @@ createNewMember_ createdAt = do let invitedById = fromInvitedBy userContactId invitedBy activeConn = Nothing - mcvr@(VersionRange minV maxV) = maybe chatInitialVRange fromChatVRange memChatVRange + memberChatVRange@(VersionRange minV maxV) = maybe chatInitialVRange fromChatVRange memChatVRange DB.execute db [sql| INSERT INTO group_members - (group_id, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, + (group_id, member_id, member_role, member_category, member_status, member_restriction, invited_by, invited_by_group_member_id, user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at, peer_chat_min_version, peer_chat_max_version) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (groupId, memberId, memberRole, memberCategory, memberStatus, invitedById, memInvitedByGroupMemberId) + ( (groupId, memberId, memberRole, memberCategory, memberStatus, memRestriction, invitedById, memInvitedByGroupMemberId) :. (userId, localDisplayName, memberContactId, memberContactProfileId, createdAt, createdAt) :. (minV, maxV) ) @@ -966,6 +1289,7 @@ createNewMember_ memberCategory, memberStatus, memberSettings = defaultMemberSettings, + blockedByAdmin = maybe False mrsBlocked memRestriction, invitedBy, invitedByGroupMemberId = memInvitedByGroupMemberId, localDisplayName, @@ -973,7 +1297,9 @@ createNewMember_ memberContactId, memberContactProfileId, activeConn, - memberChatVRange = JVersionRange mcvr + memberChatVRange, + createdAt, + updatedAt = createdAt } checkGroupMemberHasItems :: DB.Connection -> User -> GroupMember -> IO (Maybe ChatItemId) @@ -989,14 +1315,14 @@ deleteGroupMember db user@User {userId} m@GroupMember {groupMemberId, groupId, m when (memberIncognito m) $ deleteUnusedIncognitoProfileById_ db user $ localProfileId memberProfile cleanupMemberProfileAndName_ :: DB.Connection -> User -> GroupMember -> IO () -cleanupMemberProfileAndName_ db User {userId} GroupMember {groupMemberId, memberContactId, memberContactProfileId, localDisplayName} = +cleanupMemberProfileAndName_ db user@User {userId} GroupMember {groupMemberId, memberContactId, memberContactProfileId, localDisplayName} = -- check record has no memberContactId (contact_id) - it means contact has been deleted and doesn't use profile & ldn when (isNothing memberContactId) $ do -- check other group member records don't use profile & ldn sameProfileMember :: (Maybe GroupMemberId) <- maybeFirstRow fromOnly $ DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND contact_profile_id = ? AND group_member_id != ? LIMIT 1" (userId, memberContactProfileId, groupMemberId) when (isNothing sameProfileMember) $ do DB.execute db "DELETE FROM contact_profiles WHERE user_id = ? AND contact_profile_id = ?" (userId, memberContactProfileId) - DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) + safeDeleteLDN db user localDisplayName deleteGroupMemberConnection :: DB.Connection -> User -> GroupMember -> IO () deleteGroupMemberConnection db User {userId} GroupMember {groupMemberId} = @@ -1006,82 +1332,85 @@ updateGroupMemberRole :: DB.Connection -> User -> GroupMember -> GroupMemberRole updateGroupMemberRole db User {userId} GroupMember {groupMemberId} memRole = DB.execute db "UPDATE group_members SET member_role = ? WHERE user_id = ? AND group_member_id = ?" (memRole, userId, groupMemberId) -createIntroductions :: DB.Connection -> Version -> [GroupMember] -> GroupMember -> IO [GroupMemberIntro] +createIntroductions :: DB.Connection -> VersionChat -> [GroupMember] -> GroupMember -> IO [GroupMemberIntro] createIntroductions db chatV members toMember = do let reMembers = filter (\m -> memberCurrent m && groupMemberId' m /= groupMemberId' toMember) members if null reMembers then pure [] else do currentTs <- getCurrentTime - mapM (insertIntro_ currentTs) reMembers + catMaybes <$> mapM (createIntro_ currentTs) reMembers where - insertIntro_ :: UTCTime -> GroupMember -> IO GroupMemberIntro - insertIntro_ ts reMember = do - DB.execute - db - [sql| - INSERT INTO group_member_intros - (re_group_member_id, to_group_member_id, intro_status, intro_chat_protocol_version, created_at, updated_at) - VALUES (?,?,?,?,?,?) - |] - (groupMemberId' reMember, groupMemberId' toMember, GMIntroPending, chatV, ts, ts) - introId <- insertedRowId db - pure GroupMemberIntro {introId, reMember, toMember, introStatus = GMIntroPending, introInvitation = Nothing} + createIntro_ :: UTCTime -> GroupMember -> IO (Maybe GroupMemberIntro) + createIntro_ ts reMember = + -- when members connect concurrently, host would try to create introductions between them in both directions; + -- this check avoids creating second (redundant) introduction + checkInverseIntro >>= \case + Just _ -> pure Nothing + Nothing -> do + DB.execute + db + [sql| + INSERT INTO group_member_intros + (re_group_member_id, to_group_member_id, intro_status, intro_chat_protocol_version, created_at, updated_at) + VALUES (?,?,?,?,?,?) + |] + (groupMemberId' reMember, groupMemberId' toMember, GMIntroPending, chatV, ts, ts) + introId <- insertedRowId db + pure $ Just GroupMemberIntro {introId, reMember, toMember, introStatus = GMIntroPending, introInvitation = Nothing} + where + checkInverseIntro :: IO (Maybe Int64) + checkInverseIntro = + maybeFirstRow fromOnly $ + DB.query + db + "SELECT 1 FROM group_member_intros WHERE re_group_member_id = ? AND to_group_member_id = ? LIMIT 1" + (groupMemberId' toMember, groupMemberId' reMember) updateIntroStatus :: DB.Connection -> Int64 -> GroupMemberIntroStatus -> IO () updateIntroStatus db introId introStatus = do currentTs <- getCurrentTime - DB.executeNamed + DB.execute db [sql| UPDATE group_member_intros - SET intro_status = :intro_status, updated_at = :updated_at - WHERE group_member_intro_id = :intro_id + SET intro_status = ?, updated_at = ? + WHERE group_member_intro_id = ? |] - [":intro_status" := introStatus, ":updated_at" := currentTs, ":intro_id" := introId] + (introStatus, currentTs, introId) saveIntroInvitation :: DB.Connection -> GroupMember -> GroupMember -> IntroInvitation -> ExceptT StoreError IO GroupMemberIntro -saveIntroInvitation db reMember toMember introInv = do +saveIntroInvitation db reMember toMember introInv@IntroInvitation {groupConnReq} = do intro <- getIntroduction db reMember toMember liftIO $ do currentTs <- getCurrentTime - DB.executeNamed + DB.execute db [sql| UPDATE group_member_intros - SET intro_status = :intro_status, - group_queue_info = :group_queue_info, - direct_queue_info = :direct_queue_info, - updated_at = :updated_at - WHERE group_member_intro_id = :intro_id + SET intro_status = ?, + group_queue_info = ?, + direct_queue_info = ?, + updated_at = ? + WHERE group_member_intro_id = ? |] - [ ":intro_status" := GMIntroInvReceived, - ":group_queue_info" := introInv.groupConnReq, - ":direct_queue_info" := directConnReq introInv, - ":updated_at" := currentTs, - ":intro_id" := introId intro - ] + (GMIntroInvReceived, groupConnReq, directConnReq introInv, currentTs, introId intro) pure intro {introInvitation = Just introInv, introStatus = GMIntroInvReceived} saveMemberInvitation :: DB.Connection -> GroupMember -> IntroInvitation -> IO () saveMemberInvitation db GroupMember {groupMemberId} IntroInvitation {groupConnReq, directConnReq} = do currentTs <- getCurrentTime - DB.executeNamed + DB.execute db [sql| UPDATE group_members - SET member_status = :member_status, - group_queue_info = :group_queue_info, - direct_queue_info = :direct_queue_info, - updated_at = :updated_at - WHERE group_member_id = :group_member_id + SET member_status = ?, + group_queue_info = ?, + direct_queue_info = ?, + updated_at = ? + WHERE group_member_id = ? |] - [ ":member_status" := GSMemIntroInvited, - ":group_queue_info" := groupConnReq, - ":direct_queue_info" := directConnReq, - ":updated_at" := currentTs, - ":group_member_id" := groupMemberId - ] + (GSMemIntroInvited, groupConnReq, directConnReq, currentTs, groupMemberId) getIntroduction :: DB.Connection -> GroupMember -> GroupMember -> ExceptT StoreError IO GroupMemberIntro getIntroduction db reMember toMember = ExceptT $ do @@ -1101,10 +1430,10 @@ getIntroduction db reMember toMember = ExceptT $ do in Right GroupMemberIntro {introId, reMember, toMember, introStatus, introInvitation} toIntro _ = Left SEIntroNotFound -getForwardIntroducedMembers :: DB.Connection -> User -> GroupMember -> Bool -> IO [GroupMember] -getForwardIntroducedMembers db user invitee highlyAvailable = do +getForwardIntroducedMembers :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> Bool -> IO [GroupMember] +getForwardIntroducedMembers db vr user invitee highlyAvailable = do memberIds <- map fromOnly <$> query - filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db user) memberIds + filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db vr user) memberIds where mId = groupMemberId' invitee query @@ -1113,7 +1442,7 @@ getForwardIntroducedMembers db user invitee highlyAvailable = do DB.query db (q <> " AND intro_chat_protocol_version >= ?") - (mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected, minVersion groupForwardVRange) + (mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected, groupForwardVersion) q = [sql| SELECT re_group_member_id @@ -1121,10 +1450,10 @@ getForwardIntroducedMembers db user invitee highlyAvailable = do WHERE to_group_member_id = ? AND intro_status NOT IN (?,?,?) |] -getForwardInvitedMembers :: DB.Connection -> User -> GroupMember -> Bool -> IO [GroupMember] -getForwardInvitedMembers db user forwardMember highlyAvailable = do +getForwardInvitedMembers :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> Bool -> IO [GroupMember] +getForwardInvitedMembers db vr user forwardMember highlyAvailable = do memberIds <- map fromOnly <$> query - filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db user) memberIds + filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db vr user) memberIds where mId = groupMemberId' forwardMember query @@ -1133,7 +1462,7 @@ getForwardInvitedMembers db user forwardMember highlyAvailable = do DB.query db (q <> " AND intro_chat_protocol_version >= ?") - (mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected, minVersion groupForwardVRange) + (mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected, groupForwardVersion) q = [sql| SELECT to_group_member_id @@ -1141,35 +1470,37 @@ getForwardInvitedMembers db user forwardMember highlyAvailable = do WHERE re_group_member_id = ? AND intro_status NOT IN (?,?,?) |] -createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> ExceptT StoreError IO GroupMember -createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memChatVRange memberProfile) (groupCmdId, groupAgentConnId) directConnIds customUserProfileId subMode = do - let mcvr = maybe chatInitialVRange fromChatVRange memChatVRange - cLevel = 1 + maybe 0 (\Connection {connLevel} -> connLevel) activeConn - currentTs <- liftIO getCurrentTime - newMember <- case directConnIds of - Just (directCmdId, directAgentConnId) -> do - Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId mcvr memberContactId Nothing customUserProfileId cLevel currentTs subMode - liftIO $ setCommandConnId db user directCmdId directConnId - (localDisplayName, contactId, memProfileId) <- createContact_ db userId memberProfile "" (Just groupId) currentTs False - liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, directConnId) - pure $ NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memInvitedBy = IBUnknown, memInvitedByGroupMemberId = Nothing, localDisplayName, memContactId = Just contactId, memProfileId} - Nothing -> do - (localDisplayName, memProfileId) <- createNewMemberProfile_ db user memberProfile currentTs - pure $ NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memInvitedBy = IBUnknown, memInvitedByGroupMemberId = Nothing, localDisplayName, memContactId = Nothing, memProfileId} - liftIO $ do - member <- createNewMember_ db user gInfo newMember currentTs - conn@Connection {connId = groupConnId} <- createMemberConnection_ db userId (groupMemberId' member) groupAgentConnId mcvr memberContactId cLevel currentTs subMode - liftIO $ setCommandConnId db user groupCmdId groupConnId - pure (member :: GroupMember) {activeConn = Just conn} +createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> VersionChat -> MemberInfo -> Maybe MemberRestrictions -> (CommandId, ConnId) -> SubscriptionMode -> ExceptT StoreError IO GroupMember +createIntroReMember + db + user@User {userId} + gInfo + _host@GroupMember {memberContactId, activeConn} + chatV + memInfo@(MemberInfo _ _ memChatVRange memberProfile) + memRestrictions_ + (groupCmdId, groupAgentConnId) + subMode = do + let mcvr = maybe chatInitialVRange fromChatVRange memChatVRange + cLevel = 1 + maybe 0 (\Connection {connLevel} -> connLevel) activeConn + memRestriction = restriction <$> memRestrictions_ + currentTs <- liftIO getCurrentTime + (localDisplayName, memProfileId) <- createNewMemberProfile_ db user memberProfile currentTs + let newMember = NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memRestriction, memInvitedBy = IBUnknown, memInvitedByGroupMemberId = Nothing, localDisplayName, memContactId = Nothing, memProfileId} + liftIO $ do + member <- createNewMember_ db user gInfo newMember currentTs + conn@Connection {connId = groupConnId} <- createMemberConnection_ db userId (groupMemberId' member) groupAgentConnId chatV mcvr memberContactId cLevel currentTs subMode + liftIO $ setCommandConnId db user groupCmdId groupConnId + pure (member :: GroupMember) {activeConn = Just conn} -createIntroToMemberContact :: DB.Connection -> User -> GroupMember -> GroupMember -> VersionRange -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> IO () -createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} mcvr (groupCmdId, groupAgentConnId) directConnIds customUserProfileId subMode = do +createIntroToMemberContact :: DB.Connection -> User -> GroupMember -> GroupMember -> VersionChat -> VersionRangeChat -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> IO () +createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} chatV mcvr (groupCmdId, groupAgentConnId) directConnIds customUserProfileId subMode = do let cLevel = 1 + maybe 0 (\Connection {connLevel} -> connLevel) activeConn currentTs <- getCurrentTime - Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId mcvr viaContactId cLevel currentTs subMode + Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId chatV mcvr viaContactId cLevel currentTs subMode setCommandConnId db user groupCmdId groupConnId forM_ directConnIds $ \(directCmdId, directAgentConnId) -> do - Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId mcvr viaContactId Nothing customUserProfileId cLevel currentTs subMode + Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId ConnNew chatV mcvr viaContactId Nothing customUserProfileId cLevel currentTs subMode PQSupportOff setCommandConnId db user directCmdId directConnId contactId <- createMemberContact_ directConnId currentTs updateMember_ contactId currentTs @@ -1190,62 +1521,70 @@ createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = pure contactId updateMember_ :: Int64 -> UTCTime -> IO () updateMember_ contactId ts = - DB.executeNamed + DB.execute db [sql| UPDATE group_members - SET contact_id = :contact_id, updated_at = :updated_at - WHERE group_member_id = :group_member_id + SET contact_id = ?, updated_at = ? + WHERE group_member_id = ? |] - [":contact_id" := contactId, ":updated_at" := ts, ":group_member_id" := groupMemberId] + (contactId, ts, groupMemberId) -createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> VersionRange -> Maybe Int64 -> Int -> UTCTime -> SubscriptionMode -> IO Connection -createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange viaContact = createConnection_ db userId ConnMember (Just groupMemberId) agentConnId peerChatVRange viaContact Nothing Nothing +createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> VersionChat -> VersionRangeChat -> Maybe Int64 -> Int -> UTCTime -> SubscriptionMode -> IO Connection +createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange viaContact connLevel currentTs subMode = + createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV peerChatVRange viaContact Nothing Nothing connLevel currentTs subMode PQSupportOff -getViaGroupMember :: DB.Connection -> VersionRange -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember)) -getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = - maybeFirstRow toGroupAndMember $ - DB.query - db - [sql| - SELECT - -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.created_at, g.updated_at, g.chat_ts, - -- GroupInfo {membership} - mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, - mu.member_status, mu.show_messages, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, - -- GroupInfo {membership = GroupMember {memberProfile}} - pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, - -- via GroupMember - m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, - c.peer_chat_min_version, c.peer_chat_max_version - FROM group_members m - JOIN contacts ct ON ct.contact_id = m.contact_id - JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) - JOIN groups g ON g.group_id = m.group_id AND g.group_id = ct.via_group - JOIN group_profiles gp USING (group_profile_id) - JOIN group_members mu ON g.group_id = mu.group_id - JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) - LEFT JOIN connections c ON c.connection_id = ( - SELECT max(cc.connection_id) - FROM connections cc - where cc.user_id = ? AND cc.group_member_id = m.group_member_id - ) - WHERE ct.user_id = ? AND ct.contact_id = ? AND mu.contact_id = ? AND ct.deleted = 0 - |] - (userId, userId, contactId, userContactId) +getViaGroupMember :: DB.Connection -> VersionRangeChat -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember)) +getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = do + gm_ <- + maybeFirstRow toGroupAndMember $ + DB.query + db + [sql| + SELECT + -- GroupInfo + g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + -- GroupInfo {membership} + mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, + mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, + -- GroupInfo {membership = GroupMember {memberProfile}} + pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at, + -- via GroupMember + m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM group_members m + JOIN contacts ct ON ct.contact_id = m.contact_id + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + JOIN groups g ON g.group_id = m.group_id AND g.group_id = ct.via_group + JOIN group_profiles gp USING (group_profile_id) + JOIN group_members mu ON g.group_id = mu.group_id + JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) + LEFT JOIN connections c ON c.connection_id = ( + SELECT max(cc.connection_id) + FROM connections cc + where cc.user_id = ? AND cc.group_member_id = m.group_member_id + ) + WHERE ct.user_id = ? AND ct.contact_id = ? AND mu.contact_id = ? AND ct.deleted = 0 + |] + (userId, userId, contactId, userContactId) + mapM (bitraverse (addGroupChatTags db) pure) gm_ where toGroupAndMember :: (GroupInfoRow :. GroupMemberRow :. MaybeConnectionRow) -> (GroupInfo, GroupMember) toGroupAndMember (groupInfoRow :. memberRow :. connRow) = - let groupInfo = toGroupInfo vr userContactId groupInfoRow + let groupInfo = toGroupInfo vr userContactId [] groupInfoRow member = toGroupMember userContactId memberRow - in (groupInfo, (member :: GroupMember) {activeConn = toMaybeConnection connRow}) + in (groupInfo, (member :: GroupMember) {activeConn = toMaybeConnection vr connRow}) -getViaGroupContact :: DB.Connection -> User -> GroupMember -> IO (Maybe Contact) -getViaGroupContact db user@User {userId} GroupMember {groupMemberId} = do +getViaGroupContact :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> IO (Maybe Contact) +getViaGroupContact db vr user@User {userId} GroupMember {groupMemberId} = do contactId_ <- maybeFirstRow fromOnly $ DB.query @@ -1259,10 +1598,10 @@ getViaGroupContact db user@User {userId} GroupMember {groupMemberId} = do LIMIT 1 |] (userId, groupMemberId) - maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db user) contactId_ + maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db vr user) contactId_ updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo -updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, description, image, groupPreferences} +updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, description, image, groupPreferences, memberAdmission} | displayName == newName = liftIO $ do currentTs <- getCurrentTime updateGroupProfile_ currentTs @@ -1280,46 +1619,74 @@ updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, grou db [sql| UPDATE group_profiles - SET display_name = ?, full_name = ?, description = ?, image = ?, preferences = ?, updated_at = ? + SET display_name = ?, full_name = ?, description = ?, image = ?, preferences = ?, member_admission = ?, updated_at = ? WHERE group_profile_id IN ( SELECT group_profile_id FROM groups WHERE user_id = ? AND group_id = ? ) |] - (newName, fullName, description, image, groupPreferences, currentTs, userId, groupId) + (newName, fullName, description, image, groupPreferences, memberAdmission, currentTs, userId, groupId) updateGroup_ ldn currentTs = do DB.execute db "UPDATE groups SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (ldn, currentTs, userId, groupId) - DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (localDisplayName, userId) + safeDeleteLDN db user localDisplayName -getGroupInfo :: DB.Connection -> VersionRange -> User -> Int64 -> ExceptT StoreError IO GroupInfo -getGroupInfo db vr User {userId, userContactId} groupId = - ExceptT . firstRow (toGroupInfo vr userContactId) (SEGroupNotFound groupId) $ +updateGroupPreferences :: DB.Connection -> User -> GroupInfo -> GroupPreferences -> IO GroupInfo +updateGroupPreferences db User {userId} g@GroupInfo {groupId, groupProfile = p} ps = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + UPDATE group_profiles + SET preferences = ?, updated_at = ? + WHERE group_profile_id IN ( + SELECT group_profile_id + FROM groups + WHERE user_id = ? AND group_id = ? + ) + |] + (ps, currentTs, userId, groupId) + pure (g :: GroupInfo) {groupProfile = p {groupPreferences = Just ps}, fullGroupPreferences = mergeGroupPreferences $ Just ps} + +updateGroupProfileFromMember :: DB.Connection -> User -> GroupInfo -> Profile -> ExceptT StoreError IO GroupInfo +updateGroupProfileFromMember db user g@GroupInfo {groupId} Profile {displayName = n, fullName = fn, image = img} = do + p <- getGroupProfile -- to avoid any race conditions with UI + let g' = g {groupProfile = p} :: GroupInfo + p' = p {displayName = n, fullName = fn, image = img} :: GroupProfile + updateGroupProfile db user g' p' + where + getGroupProfile = + ExceptT $ + firstRow toGroupProfile (SEGroupNotFound groupId) $ + DB.query + db + [sql| + SELECT gp.display_name, gp.full_name, gp.description, gp.image, gp.preferences, gp.member_admission + FROM group_profiles gp + JOIN groups g ON gp.group_profile_id = g.group_profile_id + WHERE g.group_id = ? + |] + (Only groupId) + toGroupProfile (displayName, fullName, description, image, groupPreferences, memberAdmission) = + GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} + +getGroupInfo :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO GroupInfo +getGroupInfo db vr User {userId, userContactId} groupId = ExceptT $ do + chatTags <- getGroupChatTags db groupId + firstRow (toGroupInfo vr userContactId chatTags) (SEGroupNotFound groupId) $ DB.query db - [sql| - SELECT - -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.created_at, g.updated_at, g.chat_ts, - -- GroupMember - membership - mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, - mu.member_status, mu.show_messages, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, - pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences - FROM groups g - JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id - JOIN group_members mu ON mu.group_id = g.group_id - JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) - WHERE g.group_id = ? AND g.user_id = ? AND mu.contact_id = ? - |] + (groupInfoQuery <> " WHERE g.group_id = ? AND g.user_id = ? AND mu.contact_id = ?") (groupId, userId, userContactId) -getGroupInfoByUserContactLinkConnReq :: DB.Connection -> VersionRange -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo) +getGroupInfoByUserContactLinkConnReq :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo) getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) = do + -- fmap join is to support group_id = NULL if non-group contact request is sent to this function (e.g., if client data is appended). groupId_ <- - maybeFirstRow fromOnly $ + fmap join . maybeFirstRow fromOnly $ DB.query db [sql| @@ -1330,7 +1697,27 @@ getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReq (userId, cReqSchema1, cReqSchema2) maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db vr user) groupId_ -getGroupInfoByGroupLinkHash :: DB.Connection -> VersionRange -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe GroupInfo) +getGroupInfoViaUserShortLink :: DB.Connection -> VersionRangeChat -> User -> ShortLinkContact -> IO (Maybe (ConnReqContact, GroupInfo)) +getGroupInfoViaUserShortLink db vr user@User {userId} shortLink = fmap eitherToMaybe $ runExceptT $ do + (cReq, groupId) <- ExceptT getConnReqGroup + (cReq,) <$> getGroupInfo db vr user groupId + where + getConnReqGroup = + firstRow' toConnReqGroupId (SEInternalError "group link not found") $ + DB.query + db + [sql| + SELECT conn_req_contact, group_id + FROM user_contact_links + WHERE user_id = ? AND short_link_contact = ? + |] + (userId, shortLink) + toConnReqGroupId = \case + -- cReq is "not null", group_id is nullable + (cReq, Just groupId) -> Right (cReq, groupId) + _ -> Left $ SEInternalError "no conn req or group ID" + +getGroupInfoByGroupLinkHash :: DB.Connection -> VersionRangeChat -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe GroupInfo) getGroupInfoByGroupLinkHash db vr user@User {userId, userContactId} (groupLinkHash1, groupLinkHash2) = do groupId_ <- maybeFirstRow fromOnly $ @@ -1341,10 +1728,10 @@ getGroupInfoByGroupLinkHash db vr user@User {userId, userContactId} (groupLinkHa FROM groups g JOIN group_members mu ON mu.group_id = g.group_id WHERE g.user_id = ? AND g.via_group_link_uri_hash IN (?,?) - AND mu.contact_id = ? AND mu.member_status NOT IN (?,?,?) + AND mu.contact_id = ? AND mu.member_status NOT IN (?,?,?,?) LIMIT 1 |] - (userId, groupLinkHash1, groupLinkHash2, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted) + (userId, groupLinkHash1, groupLinkHash2, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted, GSMemUnknown) maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db vr user) groupId_ getGroupIdByName :: DB.Connection -> User -> GroupName -> ExceptT StoreError IO GroupId @@ -1357,7 +1744,7 @@ getGroupMemberIdByName db User {userId} groupId groupMemberName = ExceptT . firstRow fromOnly (SEGroupMemberNameNotFound groupId groupMemberName) $ DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND group_id = ? AND local_display_name = ?" (userId, groupId, groupMemberName) -getActiveMembersByName :: DB.Connection -> VersionRange -> User -> ContactName -> ExceptT StoreError IO [(GroupInfo, GroupMember)] +getActiveMembersByName :: DB.Connection -> VersionRangeChat -> User -> ContactName -> ExceptT StoreError IO [(GroupInfo, GroupMember)] getActiveMembersByName db vr user@User {userId} groupMemberName = do groupMemberIds :: [(GroupId, GroupMemberId)] <- liftIO $ @@ -1372,19 +1759,19 @@ getActiveMembersByName db vr user@User {userId} groupMemberName = do (userId, groupMemberName, GSMemConnected, GSMemComplete, GCUserMember) possibleMembers <- forM groupMemberIds $ \(groupId, groupMemberId) -> do groupInfo <- getGroupInfo db vr user groupId - groupMember <- getGroupMember db user groupId groupMemberId + groupMember <- getGroupMember db vr user groupId groupMemberId pure (groupInfo, groupMember) pure $ sortOn (Down . ts . fst) possibleMembers where ts GroupInfo {chatTs, updatedAt} = fromMaybe updatedAt chatTs -getMatchingContacts :: DB.Connection -> User -> Contact -> IO [Contact] -getMatchingContacts db user@User {userId} Contact {contactId, profile = LocalProfile {displayName, fullName, image}} = do +getMatchingContacts :: DB.Connection -> VersionRangeChat -> User -> Contact -> IO [Contact] +getMatchingContacts db vr user@User {userId} Contact {contactId, profile = LocalProfile {displayName, fullName, image}} = do contactIds <- map fromOnly <$> case image of Just img -> DB.query db (q <> " AND p.image = ?") (userId, contactId, CSActive, displayName, fullName, img) Nothing -> DB.query db (q <> " AND p.image is NULL") (userId, contactId, CSActive, displayName, fullName) - rights <$> mapM (runExceptT . getContact db user) contactIds + rights <$> mapM (runExceptT . getContact db vr user) contactIds where -- this query is different from one in getMatchingMemberContacts -- it checks that it's not the same contact @@ -1394,17 +1781,17 @@ getMatchingContacts db user@User {userId} Contact {contactId, profile = LocalPro FROM contacts ct JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id WHERE ct.user_id = ? AND ct.contact_id != ? - AND ct.contact_status = ? AND ct.deleted = 0 + AND ct.contact_status = ? AND ct.deleted = 0 AND ct.is_user = 0 AND p.display_name = ? AND p.full_name = ? |] -getMatchingMembers :: DB.Connection -> User -> Contact -> IO [GroupMember] -getMatchingMembers db user@User {userId} Contact {profile = LocalProfile {displayName, fullName, image}} = do +getMatchingMembers :: DB.Connection -> VersionRangeChat -> User -> Contact -> IO [GroupMember] +getMatchingMembers db vr user@User {userId} Contact {profile = LocalProfile {displayName, fullName, image}} = do memberIds <- map fromOnly <$> case image of Just img -> DB.query db (q <> " AND p.image = ?") (userId, GCUserMember, displayName, fullName, img) Nothing -> DB.query db (q <> " AND p.image is NULL") (userId, GCUserMember, displayName, fullName) - filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db user) memberIds + filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db vr user) memberIds where -- only match with members without associated contact q = @@ -1417,14 +1804,14 @@ getMatchingMembers db user@User {userId} Contact {profile = LocalProfile {displa AND p.display_name = ? AND p.full_name = ? |] -getMatchingMemberContacts :: DB.Connection -> User -> GroupMember -> IO [Contact] -getMatchingMemberContacts _ _ GroupMember {memberContactId = Just _} = pure [] -getMatchingMemberContacts db user@User {userId} GroupMember {memberProfile = LocalProfile {displayName, fullName, image}} = do +getMatchingMemberContacts :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> IO [Contact] +getMatchingMemberContacts _ _ _ GroupMember {memberContactId = Just _} = pure [] +getMatchingMemberContacts db vr user@User {userId} GroupMember {memberProfile = LocalProfile {displayName, fullName, image}} = do contactIds <- map fromOnly <$> case image of Just img -> DB.query db (q <> " AND p.image = ?") (userId, CSActive, displayName, fullName, img) Nothing -> DB.query db (q <> " AND p.image is NULL") (userId, CSActive, displayName, fullName) - rights <$> mapM (runExceptT . getContact db user) contactIds + rights <$> mapM (runExceptT . getContact db vr user) contactIds where q = [sql| @@ -1432,7 +1819,7 @@ getMatchingMemberContacts db user@User {userId} GroupMember {memberProfile = Loc FROM contacts ct JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id WHERE ct.user_id = ? - AND ct.contact_status = ? AND ct.deleted = 0 + AND ct.contact_status = ? AND ct.deleted = 0 AND ct.is_user = 0 AND p.display_name = ? AND p.full_name = ? |] @@ -1444,7 +1831,7 @@ createSentProbe db gVar userId to = DB.execute db "INSERT INTO sent_probes (contact_id, group_member_id, probe, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" - (ctId, gmId, probe, userId, currentTs, currentTs) + (ctId, gmId, Binary probe, userId, currentTs, currentTs) (Probe probe,) <$> insertedRowId db createSentProbeHash :: DB.Connection -> UserId -> Int64 -> ContactOrMember -> IO () @@ -1456,8 +1843,8 @@ createSentProbeHash db userId probeId to = do "INSERT INTO sent_probe_hashes (sent_probe_id, contact_id, group_member_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" (probeId, ctId, gmId, userId, currentTs, currentTs) -matchReceivedProbe :: DB.Connection -> User -> ContactOrMember -> Probe -> IO [ContactOrMember] -matchReceivedProbe db user@User {userId} from (Probe probe) = do +matchReceivedProbe :: DB.Connection -> VersionRangeChat -> User -> ContactOrMember -> Probe -> IO [ContactOrMember] +matchReceivedProbe db vr user@User {userId} from (Probe probe) = do let probeHash = C.sha256Hash probe cgmIds <- DB.query @@ -1470,15 +1857,15 @@ matchReceivedProbe db user@User {userId} from (Probe probe) = do LEFT JOIN groups g ON g.group_id = m.group_id WHERE r.user_id = ? AND r.probe_hash = ? AND r.probe IS NULL |] - (userId, probeHash) + (userId, Binary probeHash) currentTs <- getCurrentTime let (ctId, gmId) = contactOrMemberIds from DB.execute db "INSERT INTO received_probes (contact_id, group_member_id, probe, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" - (ctId, gmId, probe, probeHash, userId, currentTs, currentTs) + (ctId, gmId, Binary probe, Binary probeHash, userId, currentTs, currentTs) let cgmIds' = filterFirstContactId cgmIds - catMaybes <$> mapM (getContactOrMember_ db user) cgmIds' + catMaybes <$> mapM (getContactOrMember_ db vr user) cgmIds' where filterFirstContactId :: [(Maybe ContactId, Maybe GroupId, Maybe GroupMemberId)] -> [(Maybe ContactId, Maybe GroupId, Maybe GroupMemberId)] filterFirstContactId cgmIds = do @@ -1488,8 +1875,8 @@ matchReceivedProbe db user@User {userId} from (Probe probe) = do (x : _) -> [x] ctIds' <> memIds -matchReceivedProbeHash :: DB.Connection -> User -> ContactOrMember -> ProbeHash -> IO (Maybe (ContactOrMember, Probe)) -matchReceivedProbeHash db user@User {userId} from (ProbeHash probeHash) = do +matchReceivedProbeHash :: DB.Connection -> VersionRangeChat -> User -> ContactOrMember -> ProbeHash -> IO (Maybe (ContactOrMember, Probe)) +matchReceivedProbeHash db vr user@User {userId} from (ProbeHash probeHash) = do probeIds <- maybeFirstRow id $ DB.query @@ -1502,18 +1889,18 @@ matchReceivedProbeHash db user@User {userId} from (ProbeHash probeHash) = do LEFT JOIN groups g ON g.group_id = m.group_id WHERE r.user_id = ? AND r.probe_hash = ? AND r.probe IS NOT NULL |] - (userId, probeHash) + (userId, Binary probeHash) currentTs <- getCurrentTime let (ctId, gmId) = contactOrMemberIds from DB.execute db "INSERT INTO received_probes (contact_id, group_member_id, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" - (ctId, gmId, probeHash, userId, currentTs, currentTs) - pure probeIds $>>= \(Only probe :. cgmIds) -> (,Probe probe) <$$> getContactOrMember_ db user cgmIds + (ctId, gmId, Binary probeHash, userId, currentTs, currentTs) + pure probeIds $>>= \(Only probe :. cgmIds) -> (,Probe probe) <$$> getContactOrMember_ db vr user cgmIds -matchSentProbe :: DB.Connection -> User -> ContactOrMember -> Probe -> IO (Maybe ContactOrMember) -matchSentProbe db user@User {userId} _from (Probe probe) = do - cgmIds $>>= getContactOrMember_ db user +matchSentProbe :: DB.Connection -> VersionRangeChat -> User -> ContactOrMember -> Probe -> IO (Maybe ContactOrMember) +matchSentProbe db vr user@User {userId} _from (Probe probe) = do + cgmIds $>>= getContactOrMember_ db vr user where (ctId, gmId) = contactOrMemberIds _from cgmIds = @@ -1530,29 +1917,25 @@ matchSentProbe db user@User {userId} _from (Probe probe) = do WHERE s.user_id = ? AND s.probe = ? AND (h.contact_id = ? OR h.group_member_id = ?) |] - (userId, probe, ctId, gmId) + (userId, Binary probe, ctId, gmId) -getContactOrMember_ :: DB.Connection -> User -> (Maybe ContactId, Maybe GroupId, Maybe GroupMemberId) -> IO (Maybe ContactOrMember) -getContactOrMember_ db user ids = +getContactOrMember_ :: DB.Connection -> VersionRangeChat -> User -> (Maybe ContactId, Maybe GroupId, Maybe GroupMemberId) -> IO (Maybe ContactOrMember) +getContactOrMember_ db vr user ids = fmap eitherToMaybe . runExceptT $ case ids of - (Just ctId, _, _) -> COMContact <$> getContact db user ctId - (_, Just gId, Just gmId) -> COMGroupMember <$> getGroupMember db user gId gmId + (Just ctId, _, _) -> COMContact <$> getContact db vr user ctId + (_, Just gId, Just gmId) -> COMGroupMember <$> getGroupMember db vr user gId gmId _ -> throwError $ SEInternalError "" -- if requested merge direction is overruled (toFromContacts), keepLDN is kept -mergeContactRecords :: DB.Connection -> User -> Contact -> Contact -> ExceptT StoreError IO Contact -mergeContactRecords db user@User {userId} to@Contact {localDisplayName = keepLDN} from = do +mergeContactRecords :: DB.Connection -> VersionRangeChat -> User -> Contact -> Contact -> ExceptT StoreError IO Contact +mergeContactRecords db vr user@User {userId} to@Contact {localDisplayName = keepLDN} from = do let (toCt, fromCt) = toFromContacts to from Contact {contactId = toContactId, localDisplayName = toLDN} = toCt Contact {contactId = fromContactId, localDisplayName = fromLDN} = fromCt + assertNotUser db user toCt + assertNotUser db user fromCt liftIO $ do currentTs <- getCurrentTime - -- next query fixes incorrect unused contacts deletion - when (contactDirect toCt && not (contactUsed toCt)) $ - DB.execute - db - "UPDATE contacts SET contact_used = 1, updated_at = ? WHERE user_id = ? AND contact_id = ?" - (currentTs, userId, toContactId) DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?" @@ -1569,22 +1952,18 @@ mergeContactRecords db user@User {userId} to@Contact {localDisplayName = keepLDN db "UPDATE chat_items SET contact_id = ?, updated_at = ? WHERE contact_id = ? AND user_id = ?" (toContactId, currentTs, fromContactId, userId) - DB.executeNamed + DB.execute db [sql| UPDATE group_members - SET contact_id = :to_contact_id, - local_display_name = (SELECT local_display_name FROM contacts WHERE contact_id = :to_contact_id), - contact_profile_id = (SELECT contact_profile_id FROM contacts WHERE contact_id = :to_contact_id), - updated_at = :updated_at - WHERE contact_id = :from_contact_id - AND user_id = :user_id + SET contact_id = ?, + local_display_name = (SELECT local_display_name FROM contacts WHERE contact_id = ?), + contact_profile_id = (SELECT contact_profile_id FROM contacts WHERE contact_id = ?), + updated_at = ? + WHERE contact_id = ? + AND user_id = ? |] - [ ":to_contact_id" := toContactId, - ":from_contact_id" := fromContactId, - ":user_id" := userId, - ":updated_at" := currentTs - ] + (toContactId, toContactId, toContactId, currentTs, fromContactId, userId) deleteContactProfile_ db userId fromContactId DB.execute db "DELETE FROM contacts WHERE contact_id = ? AND user_id = ?" (fromContactId, userId) deleteUnusedDisplayName_ db userId fromLDN @@ -1597,7 +1976,7 @@ mergeContactRecords db user@User {userId} to@Contact {localDisplayName = keepLDN WHERE user_id = ? AND local_display_name = ? |] (keepLDN, currentTs, userId, toLDN) - getContact db user toContactId + getContact db vr user toContactId where toFromContacts :: Contact -> Contact -> (Contact, Contact) toFromContacts c1 c2 @@ -1628,9 +2007,10 @@ associateMemberWithContactRecord when (memProfileId /= profileId) $ deleteUnusedProfile_ db userId memProfileId when (memLDN /= localDisplayName) $ deleteUnusedDisplayName_ db userId memLDN -associateContactWithMemberRecord :: DB.Connection -> User -> GroupMember -> Contact -> ExceptT StoreError IO Contact +associateContactWithMemberRecord :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> Contact -> ExceptT StoreError IO Contact associateContactWithMemberRecord db + vr user@User {userId} GroupMember {groupId, groupMemberId, localDisplayName = memLDN, memberProfile = LocalProfile {profileId = memProfileId}} Contact {contactId, localDisplayName, profile = LocalProfile {profileId}} = do @@ -1654,45 +2034,48 @@ associateContactWithMemberRecord (memLDN, memProfileId, currentTs, userId, contactId) when (profileId /= memProfileId) $ deleteUnusedProfile_ db userId profileId when (localDisplayName /= memLDN) $ deleteUnusedDisplayName_ db userId localDisplayName - getContact db user contactId + getContact db vr user contactId deleteUnusedDisplayName_ :: DB.Connection -> UserId -> ContactName -> IO () deleteUnusedDisplayName_ db userId localDisplayName = - DB.executeNamed + DB.execute db [sql| DELETE FROM display_names - WHERE user_id = :user_id AND local_display_name = :local_display_name + WHERE user_id = ? AND local_display_name = ? AND 1 NOT IN ( SELECT 1 FROM users - WHERE local_display_name = :local_display_name LIMIT 1 + WHERE local_display_name = ? LIMIT 1 ) AND 1 NOT IN ( SELECT 1 FROM contacts - WHERE user_id = :user_id AND local_display_name = :local_display_name LIMIT 1 + WHERE user_id = ? AND local_display_name = ? LIMIT 1 ) AND 1 NOT IN ( SELECT 1 FROM groups - WHERE user_id = :user_id AND local_display_name = :local_display_name LIMIT 1 + WHERE user_id = ? AND local_display_name = ? LIMIT 1 ) AND 1 NOT IN ( SELECT 1 FROM group_members - WHERE user_id = :user_id AND local_display_name = :local_display_name LIMIT 1 + WHERE user_id = ? AND local_display_name = ? LIMIT 1 ) AND 1 NOT IN ( SELECT 1 FROM user_contact_links - WHERE user_id = :user_id AND local_display_name = :local_display_name LIMIT 1 + WHERE user_id = ? AND local_display_name = ? LIMIT 1 ) AND 1 NOT IN ( SELECT 1 FROM contact_requests - WHERE user_id = :user_id AND local_display_name = :local_display_name LIMIT 1 + WHERE user_id = ? AND local_display_name = ? LIMIT 1 ) AND 1 NOT IN ( SELECT 1 FROM contact_requests - WHERE user_id = :user_id AND local_display_name = :local_display_name LIMIT 1 + WHERE user_id = ? AND local_display_name = ? LIMIT 1 ) |] - [":user_id" := userId, ":local_display_name" := localDisplayName] + ( (userId, localDisplayName, localDisplayName, userId, localDisplayName, userId, localDisplayName) + :. (userId, localDisplayName, userId, localDisplayName, userId, localDisplayName) + :. (userId, localDisplayName) + ) deleteOldProbes :: DB.Connection -> UTCTime -> IO () deleteOldProbes db createdAtCutoff = do @@ -1702,7 +2085,7 @@ deleteOldProbes db createdAtCutoff = do updateGroupSettings :: DB.Connection -> User -> Int64 -> ChatSettings -> IO () updateGroupSettings db User {userId} groupId ChatSettings {enableNtfs, sendRcpts, favorite} = - DB.execute db "UPDATE groups SET enable_ntfs = ?, send_rcpts = ?, favorite = ? WHERE user_id = ? AND group_id = ?" (enableNtfs, sendRcpts, favorite, userId, groupId) + DB.execute db "UPDATE groups SET enable_ntfs = ?, send_rcpts = ?, favorite = ? WHERE user_id = ? AND group_id = ?" (enableNtfs, BI <$> sendRcpts, BI favorite, userId, groupId) updateGroupMemberSettings :: DB.Connection -> User -> GroupId -> GroupMemberId -> GroupMemberSettings -> IO () updateGroupMemberSettings db User {userId} gId gMemberId GroupMemberSettings {showMessages} = do @@ -1714,7 +2097,20 @@ updateGroupMemberSettings db User {userId} gId gMemberId GroupMemberSettings {sh SET show_messages = ?, updated_at = ? WHERE user_id = ? AND group_id = ? AND group_member_id = ? |] - (showMessages, currentTs, userId, gId, gMemberId) + (BI showMessages, currentTs, userId, gId, gMemberId) + +updateGroupMemberBlocked :: DB.Connection -> User -> GroupInfo -> MemberRestrictionStatus -> GroupMember -> IO GroupMember +updateGroupMemberBlocked db User {userId} GroupInfo {groupId} mrs m@GroupMember {groupMemberId} = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + UPDATE group_members + SET member_restriction = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND group_member_id = ? + |] + (mrs, currentTs, userId, groupId, groupMemberId) + pure m {blockedByAdmin = mrsBlocked mrs} getXGrpMemIntroContDirect :: DB.Connection -> User -> Contact -> IO (Maybe (Int64, XGrpMemIntroCont)) getXGrpMemIntroContDirect db User {userId} Contact {contactId} = do @@ -1746,36 +2142,6 @@ getXGrpMemIntroContDirect db User {userId} Contact {contactId} = do Just groupConnReq -> Just (hostConnId, XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq}) _ -> Nothing -getXGrpMemIntroContGroup :: DB.Connection -> User -> GroupMember -> IO (Maybe (Int64, ConnReqInvitation)) -getXGrpMemIntroContGroup db User {userId} GroupMember {groupMemberId} = do - fmap join . maybeFirstRow toCont $ - DB.query - db - [sql| - SELECT ch.connection_id, c.conn_req_inv - FROM group_members m - JOIN contacts ct ON ct.contact_id = m.contact_id - LEFT JOIN connections c ON c.connection_id = ( - SELECT MAX(cc.connection_id) - FROM connections cc - WHERE cc.contact_id = ct.contact_id - ) - JOIN groups g ON g.group_id = m.group_id AND g.group_id = ct.via_group - JOIN group_members mh ON mh.group_id = g.group_id - LEFT JOIN connections ch ON ch.connection_id = ( - SELECT max(cc.connection_id) - FROM connections cc - where cc.user_id = ? AND cc.group_member_id = mh.group_member_id - ) - WHERE m.user_id = ? AND m.group_member_id = ? AND mh.member_category = ? AND ct.deleted = 0 - |] - (userId, userId, groupMemberId, GCHostMember) - where - toCont :: (Int64, Maybe ConnReqInvitation) -> Maybe (Int64, ConnReqInvitation) - toCont (hostConnId, connReq_) = case connReq_ of - Just connReq -> Just (hostConnId, connReq) - _ -> Nothing - getHostConnId :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO GroupMemberId getHostConnId db user@User {userId} groupId = do hostMemberId <- getHostMemberId_ db user groupId @@ -1790,7 +2156,7 @@ createMemberContact cReq gInfo GroupMember {groupMemberId, localDisplayName, memberProfile, memberContactProfileId} - Connection {connLevel, peerChatVRange = peerChatVRange@(JVersionRange (VersionRange minV maxV))} + Connection {connLevel, connChatVersion, peerChatVRange = peerChatVRange@(VersionRange minV maxV)} subMode = do currentTs <- getCurrentTime let incognitoProfile = incognitoMembershipProfile gInfo @@ -1804,38 +2170,63 @@ createMemberContact contact_group_member_id, contact_grp_inv_sent, created_at, updated_at, chat_ts ) VALUES (?,?,?,?,?,?,?,?,?,?,?) |] - ( (userId, localDisplayName, memberContactProfileId, True, userPreferences, True) - :. (groupMemberId, False, currentTs, currentTs, currentTs) + ( (userId, localDisplayName, memberContactProfileId, BI True, userPreferences, BI True) + :. (groupMemberId, BI False, currentTs, currentTs, currentTs) ) contactId <- insertedRowId db DB.execute db - "UPDATE group_members SET contact_id = ?, updated_at = ? WHERE group_member_id = ?" - (contactId, currentTs, groupMemberId) + "UPDATE group_members SET contact_id = ?, updated_at = ? WHERE contact_profile_id = ?" + (contactId, currentTs, memberContactProfileId) DB.execute db [sql| INSERT INTO connections ( user_id, agent_conn_id, conn_req_inv, conn_level, conn_status, conn_type, contact_conn_initiated, contact_id, custom_user_profile_id, - peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + conn_chat_version, peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (userId, acId, cReq, connLevel, ConnNew, ConnContact, True, contactId, customUserProfileId) - :. (minV, maxV, currentTs, currentTs, subMode == SMOnlyCreate) + ( (userId, acId, cReq, connLevel, ConnNew, ConnContact, BI True, contactId, customUserProfileId) + :. (connChatVersion, minV, maxV, currentTs, currentTs, BI (subMode == SMOnlyCreate)) ) connId <- insertedRowId db - let ctConn = Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connType = ConnContact, contactConnInitiated = True, entityId = Just contactId, viaContact = Nothing, viaUserContactLink = Nothing, viaGroupLink = False, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0} + let ctConn = + Connection + { connId, + agentConnId = AgentConnId acId, + peerChatVRange, + connChatVersion, + connType = ConnContact, + contactConnInitiated = True, + entityId = Just contactId, + viaContact = Nothing, + viaUserContactLink = Nothing, + viaGroupLink = False, + groupLinkId = Nothing, + customUserProfileId, + connLevel, + connStatus = ConnNew, + localAlias = "", + createdAt = currentTs, + connectionCode = Nothing, + pqSupport = PQSupportOff, + pqEncryption = PQEncOff, + pqSndEnabled = Nothing, + pqRcvEnabled = Nothing, + authErrCounter = 0, + quotaErrCounter = 0 + } mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn - pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False} + pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing} -getMemberContact :: DB.Connection -> VersionRange -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation) +getMemberContact :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation) getMemberContact db vr user contactId = do - ct <- getContact db user contactId + ct <- getContact db vr user contactId let Contact {contactGroupMemberId, activeConn} = ct case (activeConn, contactGroupMemberId) of (Just Connection {connId}, Just groupMemberId) -> do cReq <- getConnReqInv db connId - m@GroupMember {groupId} <- getGroupMemberById db user groupMemberId + m@GroupMember {groupId} <- getGroupMemberById db vr user groupMemberId g <- getGroupInfo db vr user groupId pure (g, m, ct, cReq) _ -> @@ -1847,7 +2238,7 @@ setContactGrpInvSent db Contact {contactId} xGrpDirectInvSent = do DB.execute db "UPDATE contacts SET contact_grp_inv_sent = ?, updated_at = ? WHERE contact_id = ?" - (xGrpDirectInvSent, currentTs, contactId) + (BI xGrpDirectInvSent, currentTs, contactId) createMemberContactInvited :: DB.Connection -> User -> (CommandId, ConnId) -> GroupInfo -> GroupMember -> Connection -> SubscriptionMode -> IO (Contact, GroupMember) createMemberContactInvited @@ -1855,7 +2246,7 @@ createMemberContactInvited user@User {userId, profile = LocalProfile {preferences}} connIds gInfo - m@GroupMember {groupMemberId, localDisplayName = memberLDN, memberProfile, memberContactProfileId} + m@GroupMember {localDisplayName = memberLDN, memberProfile, memberContactProfileId} mConn subMode = do currentTs <- liftIO getCurrentTime @@ -1863,7 +2254,7 @@ createMemberContactInvited contactId <- createContactUpdateMember currentTs userPreferences ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn - mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False} + mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing} m' = m {memberContactId = Just contactId} pure (mCt', m') where @@ -1877,14 +2268,14 @@ createMemberContactInvited created_at, updated_at, chat_ts ) VALUES (?,?,?,?,?,?,?,?,?) |] - ( (userId, memberLDN, memberContactProfileId, True, userPreferences, True) + ( (userId, memberLDN, memberContactProfileId, BI True, userPreferences, BI True) :. (currentTs, currentTs, currentTs) ) contactId <- insertedRowId db DB.execute db - "UPDATE group_members SET contact_id = ?, updated_at = ? WHERE group_member_id = ?" - (contactId, currentTs, groupMemberId) + "UPDATE group_members SET contact_id = ?, updated_at = ? WHERE contact_profile_id = ?" + (contactId, currentTs, memberContactProfileId) pure contactId updateMemberContactInvited :: DB.Connection -> User -> (CommandId, ConnId) -> GroupInfo -> Connection -> Contact -> SubscriptionMode -> ExceptT StoreError IO Contact @@ -1915,7 +2306,7 @@ createMemberContactConn_ user@User {userId} (cmdId, acId) gInfo - _memberConn@Connection {connLevel, peerChatVRange = peerChatVRange@(JVersionRange (VersionRange minV maxV))} + _memberConn@Connection {connLevel, connChatVersion, peerChatVRange = peerChatVRange@(VersionRange minV maxV)} contactId subMode = do currentTs <- liftIO getCurrentTime @@ -1925,39 +2316,80 @@ createMemberContactConn_ [sql| INSERT INTO connections ( user_id, agent_conn_id, conn_level, conn_status, conn_type, contact_id, custom_user_profile_id, - peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + conn_chat_version, peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) |] ( (userId, acId, connLevel, ConnJoined, ConnContact, contactId, customUserProfileId) - :. (minV, maxV, currentTs, currentTs, subMode == SMOnlyCreate) + :. (connChatVersion, minV, maxV, currentTs, currentTs, BI (subMode == SMOnlyCreate)) ) connId <- insertedRowId db setCommandConnId db user cmdId connId - pure Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connType = ConnContact, contactConnInitiated = False, entityId = Just contactId, viaContact = Nothing, viaUserContactLink = Nothing, viaGroupLink = False, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnJoined, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0} + pure + Connection + { connId, + agentConnId = AgentConnId acId, + connChatVersion, + peerChatVRange, + connType = ConnContact, + contactConnInitiated = False, + entityId = Just contactId, + viaContact = Nothing, + viaUserContactLink = Nothing, + viaGroupLink = False, + groupLinkId = Nothing, + customUserProfileId, + connLevel, + connStatus = ConnJoined, + localAlias = "", + createdAt = currentTs, + connectionCode = Nothing, + pqSupport = PQSupportOff, + pqEncryption = PQEncOff, + pqSndEnabled = Nothing, + pqRcvEnabled = Nothing, + authErrCounter = 0, + quotaErrCounter = 0 + } updateMemberProfile :: DB.Connection -> User -> GroupMember -> Profile -> ExceptT StoreError IO GroupMember -updateMemberProfile db User {userId} m p' +updateMemberProfile db user@User {userId} m p' | displayName == newName = do - liftIO $ updateContactProfile_ db userId profileId p' + liftIO $ updateMemberContactProfileReset_ db userId profileId p' pure m {memberProfile = profile} | otherwise = ExceptT . withLocalDisplayName db userId newName $ \ldn -> do currentTs <- getCurrentTime - updateContactProfile_' db userId profileId p' currentTs + updateMemberContactProfileReset_' db userId profileId p' currentTs DB.execute db "UPDATE group_members SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND group_member_id = ?" (ldn, currentTs, userId, groupMemberId) - DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (localDisplayName, userId) + safeDeleteLDN db user localDisplayName pure $ Right m {localDisplayName = ldn, memberProfile = profile} where GroupMember {groupMemberId, localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m Profile {displayName = newName} = p' profile = toLocalProfile profileId p' localAlias +updateContactMemberProfile :: DB.Connection -> User -> GroupMember -> Contact -> Profile -> ExceptT StoreError IO (GroupMember, Contact) +updateContactMemberProfile db user@User {userId} m ct@Contact {contactId} p' + | displayName == newName = do + liftIO $ updateMemberContactProfile_ db userId profileId p' + pure (m {memberProfile = profile}, ct {profile} :: Contact) + | otherwise = + ExceptT . withLocalDisplayName db userId newName $ \ldn -> do + currentTs <- getCurrentTime + updateMemberContactProfile_' db userId profileId p' currentTs + updateContactLDN_ db user contactId localDisplayName ldn currentTs + pure $ Right (m {localDisplayName = ldn, memberProfile = profile}, ct {localDisplayName = ldn, profile} :: Contact) + where + GroupMember {localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m + Profile {displayName = newName} = p' + profile = toLocalProfile profileId p' localAlias + getXGrpLinkMemReceived :: DB.Connection -> GroupMemberId -> ExceptT StoreError IO Bool getXGrpLinkMemReceived db mId = - ExceptT . firstRow fromOnly (SEGroupMemberNotFound mId) $ + ExceptT . firstRow fromOnlyBI (SEGroupMemberNotFound mId) $ DB.query db "SELECT xgrplinkmem_received FROM group_members WHERE group_member_id = ?" (Only mId) setXGrpLinkMemReceived :: DB.Connection -> GroupMemberId -> Bool -> IO () @@ -1966,4 +2398,123 @@ setXGrpLinkMemReceived db mId xGrpLinkMemReceived = do DB.execute db "UPDATE group_members SET xgrplinkmem_received = ?, updated_at = ? WHERE group_member_id = ?" - (xGrpLinkMemReceived, currentTs, mId) + (BI xGrpLinkMemReceived, currentTs, mId) + +createNewUnknownGroupMember :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> MemberId -> Text -> ExceptT StoreError IO GroupMember +createNewUnknownGroupMember db vr user@User {userId, userContactId} GroupInfo {groupId} memberId memberName = do + currentTs <- liftIO getCurrentTime + let memberProfile = profileFromName memberName + (localDisplayName, profileId) <- createNewMemberProfile_ db user memberProfile currentTs + groupMemberId <- liftIO $ do + DB.execute + db + [sql| + INSERT INTO group_members + ( group_id, member_id, member_role, member_category, member_status, invited_by, + user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at, + peer_chat_min_version, peer_chat_max_version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (groupId, memberId, GRAuthor, GCPreMember, GSMemUnknown, fromInvitedBy userContactId IBUnknown) + :. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs) + :. (minV, maxV) + ) + insertedRowId db + getGroupMemberById db vr user groupMemberId + where + VersionRange minV maxV = vr + +updateUnknownMemberAnnounced :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> GroupMember -> MemberInfo -> ExceptT StoreError IO GroupMember +updateUnknownMemberAnnounced db vr user@User {userId} invitingMember unknownMember@GroupMember {groupMemberId, memberChatVRange} MemberInfo {memberRole, v, profile} = do + _ <- updateMemberProfile db user unknownMember profile + currentTs <- liftIO getCurrentTime + liftIO $ + DB.execute + db + [sql| + UPDATE group_members + SET member_role = ?, + member_category = ?, + member_status = ?, + invited_by_group_member_id = ?, + peer_chat_min_version = ?, + peer_chat_max_version = ?, + updated_at = ? + WHERE user_id = ? AND group_member_id = ? + |] + ( (memberRole, GCPostMember, GSMemAnnounced, groupMemberId' invitingMember) + :. (minV, maxV, currentTs, userId, groupMemberId) + ) + getGroupMemberById db vr user groupMemberId + where + VersionRange minV maxV = maybe memberChatVRange fromChatVRange v + +updateUserMemberProfileSentAt :: DB.Connection -> User -> GroupInfo -> UTCTime -> IO () +updateUserMemberProfileSentAt db User {userId} GroupInfo {groupId} sentTs = + DB.execute + db + "UPDATE groups SET user_member_profile_sent_at = ? WHERE user_id = ? AND group_id = ?" + (sentTs, userId, groupId) + +setGroupCustomData :: DB.Connection -> User -> GroupInfo -> Maybe CustomData -> IO () +setGroupCustomData db User {userId} GroupInfo {groupId} customData = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE groups SET custom_data = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (customData, updatedAt, userId, groupId) + +setGroupUIThemes :: DB.Connection -> User -> GroupInfo -> Maybe UIThemeEntityOverrides -> IO () +setGroupUIThemes db User {userId} GroupInfo {groupId} uiThemes = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE groups SET ui_themes = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (uiThemes, updatedAt, userId, groupId) + +updateGroupChatTags :: DB.Connection -> GroupId -> [ChatTagId] -> IO () +updateGroupChatTags db gId tIds = do + currentTags <- getGroupChatTags db gId + let tagsToAdd = filter (`notElem` currentTags) tIds + tagsToDelete = filter (`notElem` tIds) currentTags + forM_ tagsToDelete $ untagGroupChat db gId + forM_ tagsToAdd $ tagGroupChat db gId + +tagGroupChat :: DB.Connection -> GroupId -> ChatTagId -> IO () +tagGroupChat db groupId tId = + DB.execute + db + [sql| + INSERT INTO chat_tags_chats (group_id, chat_tag_id) + VALUES (?,?) + |] + (groupId, tId) + +untagGroupChat :: DB.Connection -> GroupId -> ChatTagId -> IO () +untagGroupChat db groupId tId = + DB.execute + db + [sql| + DELETE FROM chat_tags_chats + WHERE group_id = ? AND chat_tag_id = ? + |] + (groupId, tId) + +setGroupChatTTL :: DB.Connection -> GroupId -> Maybe Int64 -> IO () +setGroupChatTTL db gId ttl = do + updatedAt <- getCurrentTime + DB.execute + db + "UPDATE groups SET chat_item_ttl = ?, updated_at = ? WHERE group_id = ?" + (ttl, updatedAt, gId) + +getGroupChatTTL :: DB.Connection -> GroupId -> IO (Maybe Int64) +getGroupChatTTL db gId = + fmap join . maybeFirstRow fromOnly $ + DB.query db "SELECT chat_item_ttl FROM groups WHERE group_id = ? LIMIT 1" (Only gId) + +getUserGroupsToExpire :: DB.Connection -> User -> Int64 -> IO [GroupId] +getUserGroupsToExpire db User {userId} globalTTL = + map fromOnly <$> DB.query db ("SELECT group_id FROM groups WHERE user_id = ? AND chat_item_ttl > 0" <> cond) (Only userId) + where + cond = if globalTTL == 0 then "" else " OR chat_item_ttl IS NULL" + +updateGroupAlias :: DB.Connection -> UserId -> GroupInfo -> LocalAlias -> IO GroupInfo +updateGroupAlias db userId g@GroupInfo {groupId} localAlias = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE groups SET local_alias = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (localAlias, updatedAt, userId, groupId) + pure (g :: GroupInfo) {localAlias = localAlias} diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index fc840849ac..752a4a2c6d 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -1,8 +1,10 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} @@ -19,13 +21,14 @@ module Simplex.Chat.Store.Messages -- * Message and chat item functions deleteContactCIs, getGroupFileInfo, - deleteGroupCIs, + getGroupMemberFileInfo, + deleteGroupChatItemsMessages, createNewSndMessage, createSndMsgDelivery, createNewMessageAndRcvMsgDelivery, + getLastRcvMsgInfo, createNewRcvMessage, updateSndMsgDeliveryStatus, - updateRcvMsgDeliveryStatus, createPendingGroupMessage, getPendingGroupMessages, deletePendingGroupMessage, @@ -34,12 +37,15 @@ module Simplex.Chat.Store.Messages createNewSndChatItem, createNewRcvChatItem, createNewChatItemNoMsg, + createNewChatItem_, getChatPreviews, getDirectChat, getGroupChat, - getDirectChatItemsLast, + getLocalChat, + getDirectChatItemLast, getAllChatItems, getAChatItem, + getAChatItemBySharedMsgId, updateDirectChatItem, updateDirectChatItem', addInitialAndNewCIVersions, @@ -48,16 +54,29 @@ module Simplex.Chat.Store.Messages markDirectChatItemDeleted, updateGroupChatItemStatus, updateGroupChatItem, + createGroupCIMentions, + updateGroupCIMentions, deleteGroupChatItem, updateGroupChatItemModerated, + updateMemberCIsModerated, + updateGroupCIBlockedByAdmin, markGroupChatItemDeleted, + markMemberCIsDeleted, markGroupChatItemBlocked, + markGroupCIBlockedByAdmin, + markMessageReportsDeleted, + markReceivedGroupReportsDeleted, + deleteLocalChatItem, updateDirectChatItemsRead, getDirectUnreadTimedItems, - setDirectChatItemDeleteAt, + updateDirectChatItemsReadList, + setDirectChatItemRead, + setDirectChatItemsDeleteAt, updateGroupChatItemsRead, getGroupUnreadTimedItems, - setGroupChatItemDeleteAt, + updateGroupChatItemsReadList, + setGroupChatItemsDeleteAt, + updateLocalChatItemsRead, getChatRefViaItemId, getChatItemVersions, getDirectCIReactions, @@ -66,27 +85,35 @@ module Simplex.Chat.Store.Messages getGroupCIReactions, getGroupReactions, setGroupReaction, - getChatItemIdByAgentMsgId, + getReactionMembers, + getChatItemIdsByAgentMsgId, getDirectChatItem, getDirectCIWithReactions, getDirectChatItemBySharedMsgId, - getDirectChatItemByAgentMsgId, + getDirectChatItemsByAgentMsgId, getGroupChatItem, getGroupCIWithReactions, getGroupChatItemBySharedMsgId, getGroupMemberCIBySharedMsgId, - getGroupChatItemByAgentMsgId, + getGroupChatItemsByAgentMsgId, getGroupMemberChatItemLast, + getLocalChatItem, + updateLocalChatItem', getDirectChatItemIdByText, getDirectChatItemIdByText', getGroupChatItemIdByText, getGroupChatItemIdByText', + getLocalChatItemIdByText, + getLocalChatItemIdByText', getChatItemByFileId, + lookupChatItemByFileId, getChatItemByGroupId, updateDirectChatItemStatus, + setDirectSndChatItemViaProxy, getTimedItems, getChatItemTTL, setChatItemTTL, + getChatTTLCount, getContactExpiredFileInfo, deleteContactExpiredCIs, getGroupExpiredFileInfo, @@ -97,12 +124,14 @@ module Simplex.Chat.Store.Messages createGroupSndStatus, getGroupSndStatus, updateGroupSndStatus, + setGroupSndViaProxy, getGroupSndStatuses, getGroupSndStatusCounts, getGroupHistoryItems, ) where +import qualified Control.Exception as E import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class @@ -112,13 +141,16 @@ import Data.ByteString.Char8 (ByteString) import Data.Either (fromRight, rights) import Data.Int (Int64) import Data.List (sortBy) -import Data.Maybe (fromMaybe, isJust, mapMaybe) +import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as L +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as M +import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, mapMaybe) import Data.Ord (Down (..), comparing) import Data.Text (Text) +import qualified Data.Text as T import Data.Time (addUTCTime) import Data.Time.Clock (UTCTime (..), getCurrentTime) -import Database.SQLite.Simple (NamedParam (..), Only (..), Query, (:.) (..)) -import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Controller (ChatListQuery (..), ChatPagination (..), PaginationByTime (..)) import Simplex.Chat.Markdown import Simplex.Chat.Messages @@ -126,16 +158,25 @@ import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Groups +import Simplex.Chat.Store.NoteFolders import Simplex.Chat.Store.Shared import Simplex.Chat.Types -import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, MsgMeta (..), UserId) -import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow) -import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Chat.Types.Shared +import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, ConnShortLink, ConnectionMode (..), MsgMeta (..), UserId) +import Simplex.Messaging.Agent.Store.AgentStore (firstRow, firstRow', maybeFirstRow) +import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) +import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import Simplex.Messaging.Util (eitherToMaybe) -import Simplex.Messaging.Version (VersionRange) import UnliftIO.STM +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (FromRow, In (..), Only (..), Query, ToRow, (:.) (..)) +import Database.PostgreSQL.Simple.SqlQQ (sql) +#else +import Database.SQLite.Simple (FromRow, Only (..), Query, ToRow, (:.) (..)) +import Database.SQLite.Simple.QQ (sql) +#endif deleteContactCIs :: DB.Connection -> User -> Contact -> IO () deleteContactCIs db user@User {userId} ct@Contact {contactId} = do @@ -155,8 +196,13 @@ getGroupFileInfo db User {userId} GroupInfo {groupId} = map toFileInfo <$> DB.query db (fileInfoQuery <> " WHERE i.user_id = ? AND i.group_id = ?") (userId, groupId) -deleteGroupCIs :: DB.Connection -> User -> GroupInfo -> IO () -deleteGroupCIs db User {userId} GroupInfo {groupId} = do +getGroupMemberFileInfo :: DB.Connection -> User -> GroupInfo -> GroupMember -> IO [CIFileInfo] +getGroupMemberFileInfo db User {userId} GroupInfo {groupId} GroupMember {groupMemberId} = + map toFileInfo + <$> DB.query db (fileInfoQuery <> " WHERE i.user_id = ? AND i.group_id = ? AND i.group_member_id = ?") (userId, groupId, groupMemberId) + +deleteGroupChatItemsMessages :: DB.Connection -> User -> GroupInfo -> IO () +deleteGroupChatItemsMessages db User {userId} GroupInfo {groupId} = do DB.execute db "DELETE FROM messages WHERE group_id = ?" (Only groupId) DB.execute db "DELETE FROM chat_item_reactions WHERE group_id = ?" (Only groupId) DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND group_id = ?" (userId, groupId) @@ -176,7 +222,7 @@ createNewSndMessage db gVar connOrGroupId chatMsgEvent encodeMessage = shared_msg_id, shared_msg_id_user, created_at, updated_at ) VALUES (?,?,?,?,?,?,?,?,?) |] - (MDSnd, toCMEventTag chatMsgEvent, msgBody, connId_, groupId_, sharedMsgId, Just True, createdAt, createdAt) + (MDSnd, toCMEventTag chatMsgEvent, DB.Binary msgBody, connId_, groupId_, DB.Binary sharedMsgId, Just (BI True), createdAt, createdAt) msgId <- insertedRowId db pure $ Right SndMessage {msgId, sharedMsgId = SharedMsgId sharedMsgId, msgBody} where @@ -198,7 +244,7 @@ createSndMsgDelivery db SndMsgDelivery {connId, agentMsgId} messageId = do insertedRowId db createNewMessageAndRcvMsgDelivery :: forall e. MsgEncodingI e => DB.Connection -> ConnOrGroupId -> NewRcvMessage e -> Maybe SharedMsgId -> RcvMsgDelivery -> Maybe GroupMemberId -> ExceptT StoreError IO RcvMessage -createNewMessageAndRcvMsgDelivery db connOrGroupId newMessage sharedMsgId_ RcvMsgDelivery {connId, agentMsgId, agentMsgMeta, agentAckCmdId} authorGroupMemberId_ = do +createNewMessageAndRcvMsgDelivery db connOrGroupId newMessage sharedMsgId_ RcvMsgDelivery {connId, agentMsgId, agentMsgMeta} authorGroupMemberId_ = do msg@RcvMessage {msgId} <- createNewRcvMessage db connOrGroupId newMessage sharedMsgId_ authorGroupMemberId_ Nothing liftIO $ do currentTs <- getCurrentTime @@ -206,12 +252,29 @@ createNewMessageAndRcvMsgDelivery db connOrGroupId newMessage sharedMsgId_ RcvMs db [sql| INSERT INTO msg_deliveries - (message_id, connection_id, agent_msg_id, agent_msg_meta, agent_ack_cmd_id, chat_ts, created_at, updated_at, delivery_status) - VALUES (?,?,?,?,?,?,?,?,?) + (message_id, connection_id, agent_msg_id, agent_msg_meta, chat_ts, created_at, updated_at, delivery_status) + VALUES (?,?,?,?,?,?,?,?) |] - (msgId, connId, agentMsgId, msgMetaJson agentMsgMeta, agentAckCmdId, snd $ broker agentMsgMeta, currentTs, currentTs, MDSRcvAgent) + (msgId, connId, agentMsgId, msgMetaJson agentMsgMeta, snd $ broker agentMsgMeta, currentTs, currentTs, MDSRcvAgent) pure msg +getLastRcvMsgInfo :: DB.Connection -> Int64 -> IO (Maybe RcvMsgInfo) +getLastRcvMsgInfo db connId = + maybeFirstRow rcvMsgInfo $ + DB.query + db + [sql| + SELECT message_id, msg_delivery_id, delivery_status, agent_msg_id, agent_msg_meta + FROM msg_deliveries + WHERE connection_id = ? AND delivery_status IN (?, ?) + ORDER BY created_at DESC, msg_delivery_id DESC + LIMIT 1 + |] + (connId, MDSRcvAgent, MDSRcvAcknowledged) + where + rcvMsgInfo (msgId, msgDeliveryId, msgDeliveryStatus, agentMsgId, agentMsgMeta) = + RcvMsgInfo {msgId, msgDeliveryId, msgDeliveryStatus, agentMsgId, agentMsgMeta} + createNewRcvMessage :: forall e. MsgEncodingI e => DB.Connection -> ConnOrGroupId -> NewRcvMessage e -> Maybe SharedMsgId -> Maybe GroupMemberId -> Maybe GroupMemberId -> ExceptT StoreError IO RcvMessage createNewRcvMessage db connOrGroupId NewRcvMessage {chatMsgEvent, msgBody} sharedMsgId_ authorMember forwardedByMember = case connOrGroupId of @@ -244,7 +307,7 @@ createNewRcvMessage db connOrGroupId NewRcvMessage {chatMsgEvent, msgBody} share (msg_sent, chat_msg_event, msg_body, created_at, updated_at, connection_id, group_id, shared_msg_id, author_group_member_id, forwarded_by_group_member_id) VALUES (?,?,?,?,?,?,?,?,?,?) |] - (MDRcv, toCMEventTag chatMsgEvent, msgBody, currentTs, currentTs, connId_, groupId_, sharedMsgId_, authorMember, forwardedByMember) + (MDRcv, toCMEventTag chatMsgEvent, DB.Binary msgBody, currentTs, currentTs, connId_, groupId_, sharedMsgId_, authorMember, forwardedByMember) msgId <- insertedRowId db pure RcvMessage {msgId, chatMsgEvent = ACME (encoding @e) chatMsgEvent, sharedMsgId_, msgBody, authorMember, forwardedByMember} @@ -260,18 +323,6 @@ updateSndMsgDeliveryStatus db connId agentMsgId sndMsgDeliveryStatus = do |] (sndMsgDeliveryStatus, currentTs, connId, agentMsgId) -updateRcvMsgDeliveryStatus :: DB.Connection -> Int64 -> CommandId -> MsgDeliveryStatus 'MDRcv -> IO () -updateRcvMsgDeliveryStatus db connId cmdId rcvMsgDeliveryStatus = do - currentTs <- getCurrentTime - DB.execute - db - [sql| - UPDATE msg_deliveries - SET delivery_status = ?, updated_at = ? - WHERE connection_id = ? AND agent_ack_cmd_id = ? - |] - (rcvMsgDeliveryStatus, currentTs, connId, cmdId) - createPendingGroupMessage :: DB.Connection -> Int64 -> MessageId -> Maybe Int64 -> IO () createPendingGroupMessage db groupMemberId messageId introId_ = do currentTs <- getCurrentTime @@ -283,22 +334,22 @@ createPendingGroupMessage db groupMemberId messageId introId_ = do |] (groupMemberId, messageId, introId_, currentTs, currentTs) -getPendingGroupMessages :: DB.Connection -> Int64 -> IO [PendingGroupMessage] +getPendingGroupMessages :: DB.Connection -> Int64 -> IO [(SndMessage, ACMEventTag, Maybe Int64)] getPendingGroupMessages db groupMemberId = map pendingGroupMessage <$> DB.query db [sql| - SELECT pgm.message_id, m.chat_msg_event, m.msg_body, pgm.group_member_intro_id + SELECT pgm.message_id, m.shared_msg_id, m.msg_body, m.chat_msg_event, pgm.group_member_intro_id FROM pending_group_messages pgm JOIN messages m USING (message_id) WHERE pgm.group_member_id = ? - ORDER BY pgm.message_id ASC + ORDER BY pgm.created_at ASC, pgm.message_id ASC |] (Only groupMemberId) where - pendingGroupMessage (msgId, cmEventTag, msgBody, introId_) = - PendingGroupMessage {msgId, cmEventTag, msgBody, introId_} + pendingGroupMessage (msgId, sharedMsgId, msgBody, cmEventTag, introId_) = + (SndMessage {msgId, sharedMsgId, msgBody}, cmEventTag, introId_) deletePendingGroupMessage :: DB.Connection -> Int64 -> MessageId -> IO () deletePendingGroupMessage db groupMemberId messageId = @@ -315,18 +366,23 @@ updateChatTs db User {userId} chatDirection chatTs = case toChatInfo chatDirecti DirectChat Contact {contactId} -> DB.execute db - "UPDATE contacts SET chat_ts = ? WHERE user_id = ? AND contact_id = ?" + "UPDATE contacts SET chat_ts = ?, chat_deleted = 0 WHERE user_id = ? AND contact_id = ?" (chatTs, userId, contactId) GroupChat GroupInfo {groupId} -> DB.execute db "UPDATE groups SET chat_ts = ? WHERE user_id = ? AND group_id = ?" (chatTs, userId, groupId) + LocalChat NoteFolder {noteFolderId} -> + DB.execute + db + "UPDATE note_folders SET chat_ts = ? WHERE user_id = ? AND note_folder_id = ?" + (chatTs, userId, noteFolderId) _ -> pure () -createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CITimed -> Bool -> UTCTime -> IO ChatItemId -createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciContent quotedItem timed live createdAt = - createNewChatItem_ db user chatDirection createdByMsgId (Just sharedMsgId) ciContent quoteRow timed live createdAt Nothing createdAt +createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> Maybe NotInHistory -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> UTCTime -> IO ChatItemId +createNewSndChatItem db user chatDirection notInHistory_ SndMessage {msgId, sharedMsgId} ciContent quotedItem itemForwarded timed live createdAt = + createNewChatItem_ db user chatDirection notInHistory_ createdByMsgId (Just sharedMsgId) ciContent quoteRow itemForwarded timed live False createdAt Nothing createdAt where createdByMsgId = if msgId == 0 then Nothing else Just msgId quoteRow :: NewQuoteRow @@ -340,12 +396,13 @@ createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciCon CIQGroupRcv (Just GroupMember {memberId}) -> (Just False, Just memberId) CIQGroupRcv Nothing -> (Just False, Nothing) -createNewRcvChatItem :: DB.Connection -> User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> Maybe CITimed -> Bool -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c)) -createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, forwardedByMember} sharedMsgId_ ciContent timed live itemTs createdAt = do - ciId <- createNewChatItem_ db user chatDirection (Just msgId) sharedMsgId_ ciContent quoteRow timed live itemTs forwardedByMember createdAt +createNewRcvChatItem :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> Maybe NotInHistory -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> Maybe CITimed -> Bool -> Bool -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c), Maybe CIForwardedFrom) +createNewRcvChatItem db user chatDirection notInHistory_ RcvMessage {msgId, chatMsgEvent, forwardedByMember} sharedMsgId_ ciContent timed live userMention itemTs createdAt = do + ciId <- createNewChatItem_ db user chatDirection notInHistory_ (Just msgId) sharedMsgId_ ciContent quoteRow itemForwarded timed live userMention itemTs forwardedByMember createdAt quotedItem <- mapM (getChatItemQuote_ db user chatDirection) quotedMsg - pure (ciId, quotedItem) + pure (ciId, quotedItem, itemForwarded) where + itemForwarded = cmForwardedFrom chatMsgEvent quotedMsg = cmToQuotedMsg chatMsgEvent quoteRow :: NewQuoteRow quoteRow = case quotedMsg of @@ -358,39 +415,58 @@ createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, forw createNewChatItemNoMsg :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> CIContent d -> UTCTime -> UTCTime -> IO ChatItemId createNewChatItemNoMsg db user chatDirection ciContent itemTs = - createNewChatItem_ db user chatDirection Nothing Nothing ciContent quoteRow Nothing False itemTs Nothing + createNewChatItem_ db user chatDirection Nothing Nothing Nothing ciContent quoteRow Nothing Nothing False False itemTs Nothing where quoteRow :: NewQuoteRow quoteRow = (Nothing, Nothing, Nothing, Nothing, Nothing) -createNewChatItem_ :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> Maybe MessageId -> Maybe SharedMsgId -> CIContent d -> NewQuoteRow -> Maybe CITimed -> Bool -> UTCTime -> Maybe GroupMemberId -> UTCTime -> IO ChatItemId -createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent quoteRow timed live itemTs forwardedByMember createdAt = do +createNewChatItem_ :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> Maybe NotInHistory -> Maybe MessageId -> Maybe SharedMsgId -> CIContent d -> NewQuoteRow -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> UTCTime -> Maybe GroupMemberId -> UTCTime -> IO ChatItemId +createNewChatItem_ db User {userId} chatDirection notInHistory_ msgId_ sharedMsgId ciContent quoteRow itemForwarded timed live userMention itemTs forwardedByMember createdAt = do DB.execute db [sql| INSERT INTO chat_items ( -- user and IDs - user_id, created_by_msg_id, contact_id, group_id, group_member_id, + user_id, created_by_msg_id, contact_id, group_id, group_member_id, note_folder_id, -- meta - item_sent, item_ts, item_content, item_content_tag, item_text, item_status, shared_msg_id, - forwarded_by_group_member_id, created_at, updated_at, item_live, timed_ttl, timed_delete_at, + item_sent, item_ts, item_content, item_content_tag, item_text, item_status, msg_content_tag, shared_msg_id, + forwarded_by_group_member_id, include_in_history, created_at, updated_at, item_live, user_mention, timed_ttl, timed_delete_at, -- quote - quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id, + -- forwarded from + fwd_from_tag, fwd_from_chat_name, fwd_from_msg_dir, fwd_from_contact_id, fwd_from_group_id, fwd_from_chat_item_id + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ((userId, msgId_) :. idsRow :. itemRow :. quoteRow) + ((userId, msgId_) :. idsRow :. itemRow :. quoteRow' :. forwardedFromRow) ciId <- insertedRowId db forM_ msgId_ $ \msgId -> insertChatItemMessage_ db ciId msgId createdAt pure ciId where - itemRow :: (SMsgDirection d, UTCTime, CIContent d, Text, Text, CIStatus d, Maybe SharedMsgId, Maybe GroupMemberId) :. (UTCTime, UTCTime, Maybe Bool) :. (Maybe Int, Maybe UTCTime) - itemRow = (msgDirection @d, itemTs, ciContent, toCIContentTag ciContent, ciContentToText ciContent, ciCreateStatus ciContent, sharedMsgId, forwardedByMember) :. (createdAt, createdAt, justTrue live) :. ciTimedRow timed - idsRow :: (Maybe Int64, Maybe Int64, Maybe Int64) + itemRow :: (SMsgDirection d, UTCTime, CIContent d, Text, Text, CIStatus d, Maybe MsgContentTag, Maybe SharedMsgId, Maybe GroupMemberId, BoolInt) :. (UTCTime, UTCTime, Maybe BoolInt, BoolInt) :. (Maybe Int, Maybe UTCTime) + itemRow = (msgDirection @d, itemTs, ciContent, toCIContentTag ciContent, ciContentToText ciContent, ciCreateStatus ciContent, msgContentTag <$> ciMsgContent ciContent, sharedMsgId, forwardedByMember, BI includeInHistory) :. (createdAt, createdAt, BI <$> (justTrue live), BI userMention) :. ciTimedRow timed + quoteRow' = let (a, b, c, d, e) = quoteRow in (a, b, c, BI <$> d, e) + idsRow :: (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64) idsRow = case chatDirection of - CDDirectRcv Contact {contactId} -> (Just contactId, Nothing, Nothing) - CDDirectSnd Contact {contactId} -> (Just contactId, Nothing, Nothing) - CDGroupRcv GroupInfo {groupId} GroupMember {groupMemberId} -> (Nothing, Just groupId, Just groupMemberId) - CDGroupSnd GroupInfo {groupId} -> (Nothing, Just groupId, Nothing) + CDDirectRcv Contact {contactId} -> (Just contactId, Nothing, Nothing, Nothing) + CDDirectSnd Contact {contactId} -> (Just contactId, Nothing, Nothing, Nothing) + CDGroupRcv GroupInfo {groupId} GroupMember {groupMemberId} -> (Nothing, Just groupId, Just groupMemberId, Nothing) + CDGroupSnd GroupInfo {groupId} -> (Nothing, Just groupId, Nothing, Nothing) + CDLocalRcv NoteFolder {noteFolderId} -> (Nothing, Nothing, Nothing, Just noteFolderId) + CDLocalSnd NoteFolder {noteFolderId} -> (Nothing, Nothing, Nothing, Just noteFolderId) + includeInHistory :: Bool + includeInHistory = + let (_, groupId_, _, _) = idsRow + in isJust groupId_ && isNothing notInHistory_ && isJust (ciMsgContent ciContent) && ((msgContentTag <$> ciMsgContent ciContent) /= Just MCReport_) + forwardedFromRow :: (Maybe CIForwardedFromTag, Maybe Text, Maybe MsgDirection, Maybe Int64, Maybe Int64, Maybe Int64) + forwardedFromRow = case itemForwarded of + Nothing -> + (Nothing, Nothing, Nothing, Nothing, Nothing, Nothing) + Just CIFFUnknown -> + (Just CIFFUnknown_, Nothing, Nothing, Nothing, Nothing, Nothing) + Just CIFFContact {chatName, msgDir, contactId, chatItemId} -> + (Just CIFFContact_, Just chatName, Just msgDir, contactId, Nothing, chatItemId) + Just CIFFGroup {chatName, msgDir, groupId, chatItemId} -> + (Just CIFFGroup_, Just chatName, Just msgDir, Nothing, groupId, chatItemId) ciTimedRow :: Maybe CITimed -> (Maybe Int, Maybe UTCTime) ciTimedRow (Just CITimed {ttl, deleteAt}) = (Just ttl, deleteAt) @@ -399,15 +475,15 @@ ciTimedRow _ = (Nothing, Nothing) insertChatItemMessage_ :: DB.Connection -> ChatItemId -> MessageId -> UTCTime -> IO () insertChatItemMessage_ db ciId msgId ts = DB.execute db "INSERT INTO chat_item_messages (chat_item_id, message_id, created_at, updated_at) VALUES (?,?,?,?)" (ciId, msgId, ts, ts) -getChatItemQuote_ :: DB.Connection -> User -> ChatDirection c 'MDRcv -> QuotedMsg -> IO (CIQuote c) +getChatItemQuote_ :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> QuotedMsg -> IO (CIQuote c) getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRef = MsgRef {msgId, sentAt, sent, memberId}, content} = case chatDirection of CDDirectRcv Contact {contactId} -> getDirectChatItemQuote_ contactId (not sent) - CDGroupRcv GroupInfo {groupId, membership = GroupMember {memberId = userMemberId}} sender@GroupMember {memberId = senderMemberId} -> + CDGroupRcv GroupInfo {groupId, membership = GroupMember {memberId = userMemberId}} sender@GroupMember {groupMemberId = senderGMId, memberId = senderMemberId} -> case memberId of Just mId | mId == userMemberId -> (`ciQuote` CIQGroupSnd) <$> getUserGroupChatItemId_ groupId - | mId == senderMemberId -> (`ciQuote` CIQGroupRcv (Just sender)) <$> getGroupChatItemId_ groupId mId + | mId == senderMemberId -> (`ciQuote` CIQGroupRcv (Just sender)) <$> getGroupChatItemId_ groupId senderGMId | otherwise -> getGroupChatItemQuote_ groupId mId _ -> pure . ciQuote Nothing $ CIQGroupRcv Nothing where @@ -419,7 +495,7 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe DB.query db "SELECT chat_item_id FROM chat_items WHERE user_id = ? AND contact_id = ? AND shared_msg_id = ? AND item_sent = ?" - (userId, contactId, msgId, userSent) + (userId, contactId, msgId, BI userSent) where ciQuoteDirect :: Maybe ChatItemId -> CIQuote 'CTDirect ciQuoteDirect = (`ciQuote` if userSent then CIQDirectSnd else CIQDirectRcv) @@ -430,51 +506,55 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe db "SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND shared_msg_id = ? AND item_sent = ? AND group_member_id IS NULL" (userId, groupId, msgId, MDSnd) - getGroupChatItemId_ :: Int64 -> MemberId -> IO (Maybe ChatItemId) - getGroupChatItemId_ groupId mId = + getGroupChatItemId_ :: Int64 -> GroupMemberId -> IO (Maybe ChatItemId) + getGroupChatItemId_ groupId groupMemberId = maybeFirstRow fromOnly $ DB.query db "SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND shared_msg_id = ? AND item_sent = ? AND group_member_id = ?" - (userId, groupId, msgId, MDRcv, mId) + (userId, groupId, msgId, MDRcv, groupMemberId) getGroupChatItemQuote_ :: Int64 -> MemberId -> IO (CIQuote 'CTGroup) getGroupChatItemQuote_ groupId mId = do ciQuoteGroup - <$> DB.queryNamed + <$> DB.query db [sql| SELECT i.chat_item_id, -- GroupMember m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, - m.member_status, m.show_messages, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, - p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences + m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, + p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN contacts c ON m.contact_id = c.contact_id - LEFT JOIN chat_items i ON i.group_id = m.group_id + LEFT JOIN chat_items i ON i.user_id = m.user_id + AND i.group_id = m.group_id AND m.group_member_id = i.group_member_id - AND i.shared_msg_id = :msg_id - WHERE m.user_id = :user_id AND m.group_id = :group_id AND m.member_id = :member_id + AND i.shared_msg_id = ? + WHERE m.user_id = ? AND m.group_id = ? AND m.member_id = ? |] - [":user_id" := userId, ":group_id" := groupId, ":member_id" := mId, ":msg_id" := msgId] + (msgId, userId, groupId, mId) where ciQuoteGroup :: [Only (Maybe ChatItemId) :. GroupMemberRow] -> CIQuote 'CTGroup ciQuoteGroup [] = ciQuote Nothing $ CIQGroupRcv Nothing ciQuoteGroup ((Only itemId :. memberRow) : _) = ciQuote itemId . CIQGroupRcv . Just $ toGroupMember userContactId memberRow -getChatPreviews :: DB.Connection -> VersionRange -> User -> Bool -> PaginationByTime -> ChatListQuery -> IO [Either StoreError AChat] +getChatPreviews :: DB.Connection -> VersionRangeChat -> User -> Bool -> PaginationByTime -> ChatListQuery -> IO [Either StoreError AChat] getChatPreviews db vr user withPCC pagination query = do directChats <- findDirectChatPreviews_ db user pagination query groupChats <- findGroupChatPreviews_ db user pagination query + localChats <- findLocalChatPreviews_ db user pagination query cReqChats <- getContactRequestChatPreviews_ db user pagination query connChats <- if withPCC then getContactConnectionChatPreviews_ db user pagination query else pure [] - let refs = sortTake $ concat [directChats, groupChats, cReqChats, connChats] + let refs = sortTake $ concat [directChats, groupChats, localChats, cReqChats, connChats] mapM (runExceptT <$> getChatPreview) refs where ts :: AChatPreviewData -> UTCTime ts (ACPD _ cpd) = case cpd of (DirectChatPD t _ _ _) -> t (GroupChatPD t _ _ _) -> t + (LocalChatPD t _ _ _) -> t (ContactRequestPD t _) -> t (ContactConnectionPD t _) -> t sortTake = case pagination of @@ -483,29 +563,30 @@ getChatPreviews db vr user withPCC pagination query = do PTBefore _ count -> take count . sortBy (comparing $ Down . ts) getChatPreview :: AChatPreviewData -> ExceptT StoreError IO AChat getChatPreview (ACPD cType cpd) = case cType of - SCTDirect -> getDirectChatPreview_ db user cpd + SCTDirect -> getDirectChatPreview_ db vr user cpd SCTGroup -> getGroupChatPreview_ db vr user cpd + SCTLocal -> getLocalChatPreview_ db user cpd SCTContactRequest -> let (ContactRequestPD _ chat) = cpd in pure chat SCTContactConnection -> let (ContactConnectionPD _ chat) = cpd in pure chat data ChatPreviewData (c :: ChatType) where DirectChatPD :: UTCTime -> ContactId -> Maybe ChatItemId -> ChatStats -> ChatPreviewData 'CTDirect GroupChatPD :: UTCTime -> GroupId -> Maybe ChatItemId -> ChatStats -> ChatPreviewData 'CTGroup + LocalChatPD :: UTCTime -> NoteFolderId -> Maybe ChatItemId -> ChatStats -> ChatPreviewData 'CTLocal ContactRequestPD :: UTCTime -> AChat -> ChatPreviewData 'CTContactRequest ContactConnectionPD :: UTCTime -> AChat -> ChatPreviewData 'CTContactConnection data AChatPreviewData = forall c. ChatTypeI c => ACPD (SChatType c) (ChatPreviewData c) -paginationByTimeFilter :: PaginationByTime -> (Query, [NamedParam]) -paginationByTimeFilter = \case - PTLast count -> ("\nORDER BY ts DESC LIMIT :count", [":count" := count]) - PTAfter ts count -> ("\nAND ts > :ts ORDER BY ts ASC LIMIT :count", [":ts" := ts, ":count" := count]) - PTBefore ts count -> ("\nAND ts < :ts ORDER BY ts DESC LIMIT :count", [":ts" := ts, ":count" := count]) - -type ChatStatsRow = (Int, ChatItemId, Bool) +type ChatStatsRow = (Int, Int, ChatItemId, BoolInt) toChatStats :: ChatStatsRow -> ChatStats -toChatStats (unreadCount, minUnreadItemId, unreadChat) = ChatStats {unreadCount, minUnreadItemId, unreadChat} +toChatStats (unreadCount, reportsCount, minUnreadItemId, BI unreadChat) = ChatStats {unreadCount, unreadMentions = 0, reportsCount, minUnreadItemId, unreadChat} + +type GroupStatsRow = (Int, Int, Int, ChatItemId, BoolInt) + +toGroupStats :: GroupStatsRow -> ChatStats +toGroupStats (unreadCount, unreadMentions, reportsCount, minUnreadItemId, BI unreadChat) = ChatStats {unreadCount, unreadMentions, reportsCount, minUnreadItemId, unreadChat} findDirectChatPreviews_ :: DB.Connection -> User -> PaginationByTime -> ChatListQuery -> IO [AChatPreviewData] findDirectChatPreviews_ db User {userId} pagination clq = @@ -516,87 +597,90 @@ findDirectChatPreviews_ db User {userId} pagination clq = ACPD SCTDirect $ DirectChatPD ts contactId lastItemId_ (toChatStats statsRow) baseQuery = [sql| - SELECT ct.contact_id, ct.chat_ts as ts, LastItems.chat_item_id, COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat + SELECT + ct.contact_id, + ct.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.contact_id = ct.contact_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + ct.unread_chat FROM contacts ct - LEFT JOIN ( - SELECT contact_id, chat_item_id, MAX(created_at) - FROM chat_items - GROUP BY contact_id - ) LastItems ON LastItems.contact_id = ct.contact_id LEFT JOIN ( SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread FROM chat_items - WHERE item_status = :rcv_new + WHERE user_id = ? AND contact_id IS NOT NULL AND item_status = ? GROUP BY contact_id ) ChatStats ON ChatStats.contact_id = ct.contact_id |] - (pagQuery, pagParams) = paginationByTimeFilter pagination + baseParams = (userId, userId, CISRcvNew) getPreviews = case clq of - CLQFilters {favorite = False, unread = False} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used - |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) - CLQFilters {favorite = True, unread = False} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used - AND ct.favorite = 1 - |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) - CLQFilters {favorite = False, unread = True} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used - AND (ct.unread_chat = 1 OR ChatStats.UnreadCount > 0) - |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) - CLQFilters {favorite = True, unread = True} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used - AND (ct.favorite = 1 - OR ct.unread_chat = 1 OR ChatStats.UnreadCount > 0) - |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) - CLQSearch {search} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id - WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used - AND ( - ct.local_display_name LIKE '%' || :search || '%' - OR cp.display_name LIKE '%' || :search || '%' - OR cp.full_name LIKE '%' || :search || '%' - OR cp.local_alias LIKE '%' || :search || '%' - ) - |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew, ":search" := search] <> pagParams) + CLQFilters {favorite = False, unread = False} -> do + let q = baseQuery <> " WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used = 1" + p = baseParams :. Only userId + queryWithPagination q p + CLQFilters {favorite = True, unread = False} -> do + let q = + baseQuery + <> " " + <> [sql| + WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used = 1 + AND ct.favorite = 1 + |] + p = baseParams :. Only userId + queryWithPagination q p + CLQFilters {favorite = False, unread = True} -> do + let q = + baseQuery + <> " " + <> [sql| + WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used = 1 + AND (ct.unread_chat = 1 OR ChatStats.UnreadCount > 0) + |] + p = baseParams :. Only userId + queryWithPagination q p + CLQFilters {favorite = True, unread = True} -> do + let q = + baseQuery + <> " " + <> [sql| + WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used = 1 + AND (ct.favorite = 1 + OR ct.unread_chat = 1 OR ChatStats.UnreadCount > 0) + |] + p = baseParams :. Only userId + queryWithPagination q p + CLQSearch {search} -> do + let q = + baseQuery + <> " " + <> [sql| + JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id + WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used = 1 + AND ( + LOWER(ct.local_display_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(cp.display_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(cp.full_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(cp.local_alias) LIKE '%' || LOWER(?) || '%' + ) + |] + p = baseParams :. (userId, search, search, search, search) + queryWithPagination q p + queryWithPagination :: ToRow p => Query -> p -> IO [(ContactId, UTCTime, Maybe ChatItemId) :. ChatStatsRow] + queryWithPagination query params = case pagination of + PTLast count -> DB.query db (query <> " ORDER BY ct.chat_ts DESC LIMIT ?") (params :. Only count) + PTAfter ts count -> DB.query db (query <> " AND ct.chat_ts > ? ORDER BY ct.chat_ts ASC LIMIT ?") (params :. (ts, count)) + PTBefore ts count -> DB.query db (query <> " AND ct.chat_ts < ? ORDER BY ct.chat_ts DESC LIMIT ?") (params :. (ts, count)) -getDirectChatPreview_ :: DB.Connection -> User -> ChatPreviewData 'CTDirect -> ExceptT StoreError IO AChat -getDirectChatPreview_ db user (DirectChatPD _ contactId lastItemId_ stats) = do - contact <- getContact db user contactId +getDirectChatPreview_ :: DB.Connection -> VersionRangeChat -> User -> ChatPreviewData 'CTDirect -> ExceptT StoreError IO AChat +getDirectChatPreview_ db vr user (DirectChatPD _ contactId lastItemId_ stats) = do + contact <- getContact db vr user contactId lastItem <- case lastItemId_ of Just lastItemId -> (: []) <$> getDirectChatItem db user contactId lastItemId Nothing -> pure [] @@ -606,304 +690,621 @@ findGroupChatPreviews_ :: DB.Connection -> User -> PaginationByTime -> ChatListQ findGroupChatPreviews_ db User {userId} pagination clq = map toPreview <$> getPreviews where - toPreview :: (GroupId, UTCTime, Maybe ChatItemId) :. ChatStatsRow -> AChatPreviewData + toPreview :: (GroupId, UTCTime, Maybe ChatItemId) :. GroupStatsRow -> AChatPreviewData toPreview ((groupId, ts, lastItemId_) :. statsRow) = - ACPD SCTGroup $ GroupChatPD ts groupId lastItemId_ (toChatStats statsRow) + ACPD SCTGroup $ GroupChatPD ts groupId lastItemId_ (toGroupStats statsRow) baseQuery = [sql| - SELECT g.group_id, g.chat_ts as ts, LastItems.chat_item_id, COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), g.unread_chat + SELECT + g.group_id, + g.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.group_id = g.group_id + ORDER BY ci.item_ts DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + COALESCE(ChatStats.UnreadMentions, 0), + COALESCE(ReportCount.Count, 0), + COALESCE(ChatStats.MinUnread, 0), + g.unread_chat FROM groups g LEFT JOIN ( - SELECT group_id, chat_item_id, MAX(item_ts) + SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread FROM chat_items - GROUP BY group_id - ) LastItems ON LastItems.group_id = g.group_id - LEFT JOIN ( - SELECT group_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread - FROM chat_items - WHERE item_status = :rcv_new + WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? GROUP BY group_id ) ChatStats ON ChatStats.group_id = g.group_id + LEFT JOIN ( + SELECT group_id, COUNT(1) AS Count + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL + AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 + GROUP BY group_id + ) ReportCount ON ReportCount.group_id = g.group_id |] - (pagQuery, pagParams) = paginationByTimeFilter pagination + baseParams = (userId, userId, CISRcvNew, userId, MCReport_, BI False) getPreviews = case clq of - CLQFilters {favorite = False, unread = False} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - WHERE g.user_id = :user_id - |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) - CLQFilters {favorite = True, unread = False} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - WHERE g.user_id = :user_id - AND g.favorite = 1 - |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) - CLQFilters {favorite = False, unread = True} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - WHERE g.user_id = :user_id - AND (g.unread_chat = 1 OR ChatStats.UnreadCount > 0) - |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) - CLQFilters {favorite = True, unread = True} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - WHERE g.user_id = :user_id - AND (g.favorite = 1 - OR g.unread_chat = 1 OR ChatStats.UnreadCount > 0) - |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) - CLQSearch {search} -> - DB.queryNamed - db - ( baseQuery - <> [sql| - JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id - WHERE g.user_id = :user_id - AND ( - g.local_display_name LIKE '%' || :search || '%' - OR gp.display_name LIKE '%' || :search || '%' - OR gp.full_name LIKE '%' || :search || '%' - OR gp.description LIKE '%' || :search || '%' - ) - |] - <> pagQuery - ) - ([":user_id" := userId, ":rcv_new" := CISRcvNew, ":search" := search] <> pagParams) + CLQFilters {favorite = False, unread = False} -> do + let q = baseQuery <> " WHERE g.user_id = ?" + p = baseParams :. Only userId + queryWithPagination q p + CLQFilters {favorite = True, unread = False} -> do + let q = + baseQuery + <> " " + <> [sql| + WHERE g.user_id = ? + AND g.favorite = 1 + |] + p = baseParams :. Only userId + queryWithPagination q p + CLQFilters {favorite = False, unread = True} -> do + let q = + baseQuery + <> " " + <> [sql| + WHERE g.user_id = ? + AND (g.unread_chat = 1 OR ChatStats.UnreadCount > 0) + |] + p = baseParams :. Only userId + queryWithPagination q p + CLQFilters {favorite = True, unread = True} -> do + let q = + baseQuery + <> " " + <> [sql| + WHERE g.user_id = ? + AND (g.favorite = 1 + OR g.unread_chat = 1 OR ChatStats.UnreadCount > 0) + |] + p = baseParams :. Only userId + queryWithPagination q p + CLQSearch {search} -> do + let q = + baseQuery + <> " " + <> [sql| + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + WHERE g.user_id = ? + AND ( + LOWER(g.local_display_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(gp.display_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(gp.full_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(gp.description) LIKE '%' || LOWER(?) || '%' + ) + |] + p = baseParams :. (userId, search, search, search, search) + queryWithPagination q p + queryWithPagination :: ToRow p => Query -> p -> IO [(GroupId, UTCTime, Maybe ChatItemId) :. GroupStatsRow] + queryWithPagination query params = case pagination of + PTLast count -> DB.query db (query <> " ORDER BY g.chat_ts DESC LIMIT ?") (params :. Only count) + PTAfter ts count -> DB.query db (query <> " AND g.chat_ts > ? ORDER BY g.chat_ts ASC LIMIT ?") (params :. (ts, count)) + PTBefore ts count -> DB.query db (query <> " AND g.chat_ts < ? ORDER BY g.chat_ts DESC LIMIT ?") (params :. (ts, count)) -getGroupChatPreview_ :: DB.Connection -> VersionRange -> User -> ChatPreviewData 'CTGroup -> ExceptT StoreError IO AChat +getGroupChatPreview_ :: DB.Connection -> VersionRangeChat -> User -> ChatPreviewData 'CTGroup -> ExceptT StoreError IO AChat getGroupChatPreview_ db vr user (GroupChatPD _ groupId lastItemId_ stats) = do groupInfo <- getGroupInfo db vr user groupId lastItem <- case lastItemId_ of - Just lastItemId -> (: []) <$> getGroupChatItem db user groupId lastItemId + Just lastItemId -> (: []) <$> getGroupCIWithReactions db user groupInfo lastItemId Nothing -> pure [] pure $ AChat SCTGroup (Chat (GroupChat groupInfo) lastItem stats) +findLocalChatPreviews_ :: DB.Connection -> User -> PaginationByTime -> ChatListQuery -> IO [AChatPreviewData] +findLocalChatPreviews_ db User {userId} pagination clq = + map toPreview <$> getPreviews + where + toPreview :: (NoteFolderId, UTCTime, Maybe ChatItemId) :. ChatStatsRow -> AChatPreviewData + toPreview ((noteFolderId, ts, lastItemId_) :. statsRow) = + ACPD SCTLocal $ LocalChatPD ts noteFolderId lastItemId_ (toChatStats statsRow) + baseQuery = + [sql| + SELECT + nf.note_folder_id, + nf.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.note_folder_id = nf.note_folder_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + nf.unread_chat + FROM note_folders nf + LEFT JOIN ( + SELECT note_folder_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND note_folder_id IS NOT NULL AND item_status = ? + GROUP BY note_folder_id + ) ChatStats ON ChatStats.note_folder_id = nf.note_folder_id + |] + baseParams = (userId, userId, CISRcvNew) + getPreviews = case clq of + CLQFilters {favorite = False, unread = False} -> do + let q = baseQuery <> " WHERE nf.user_id = ?" + p = baseParams :. Only userId + queryWithPagination q p + CLQFilters {favorite = True, unread = False} -> do + let q = + baseQuery + <> " " + <> [sql| + WHERE nf.user_id = ? + AND nf.favorite = 1 + |] + p = baseParams :. Only userId + queryWithPagination q p + CLQFilters {favorite = False, unread = True} -> do + let q = + baseQuery + <> " " + <> [sql| + WHERE nf.user_id = ? + AND (nf.unread_chat = 1 OR ChatStats.UnreadCount > 0) + |] + p = baseParams :. Only userId + queryWithPagination q p + CLQFilters {favorite = True, unread = True} -> do + let q = + baseQuery + <> " " + <> [sql| + WHERE nf.user_id = ? + AND (nf.favorite = 1 + OR nf.unread_chat = 1 OR ChatStats.UnreadCount > 0) + |] + p = baseParams :. Only userId + queryWithPagination q p + CLQSearch {} -> pure [] + queryWithPagination :: ToRow p => Query -> p -> IO [(NoteFolderId, UTCTime, Maybe ChatItemId) :. ChatStatsRow] + queryWithPagination query params = case pagination of + PTLast count -> DB.query db (query <> " ORDER BY nf.chat_ts DESC LIMIT ?") (params :. Only count) + PTAfter ts count -> DB.query db (query <> " AND nf.chat_ts > ? ORDER BY nf.chat_ts ASC LIMIT ?") (params :. (ts, count)) + PTBefore ts count -> DB.query db (query <> " AND nf.chat_ts < ? ORDER BY nf.chat_ts DESC LIMIT ?") (params :. (ts, count)) + +getLocalChatPreview_ :: DB.Connection -> User -> ChatPreviewData 'CTLocal -> ExceptT StoreError IO AChat +getLocalChatPreview_ db user (LocalChatPD _ noteFolderId lastItemId_ stats) = do + nf <- getNoteFolder db user noteFolderId + lastItem <- case lastItemId_ of + Just lastItemId -> (: []) <$> getLocalChatItem db user noteFolderId lastItemId + Nothing -> pure [] + pure $ AChat SCTLocal (Chat (LocalChat nf) lastItem stats) + +-- this function can be changed so it never fails, not only avoid failure on invalid json +toLocalChatItem :: UTCTime -> ChatItemRow -> Either StoreError (CChatItem 'CTLocal) +toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) = + chatItem $ fromRight invalid $ dbParseACIContent itemContentText + where + invalid = ACIContent msgDir $ CIInvalidJSON itemContentText + chatItem itemContent = case (itemContent, itemStatus, fileStatus_) of + (ACIContent SMDSnd ciContent, ACIStatus SMDSnd ciStatus, Just (AFS SMDSnd fileStatus)) -> + Right $ cItem SMDSnd CILocalSnd ciStatus ciContent (maybeCIFile fileStatus) + (ACIContent SMDSnd ciContent, ACIStatus SMDSnd ciStatus, Nothing) -> + Right $ cItem SMDSnd CILocalSnd ciStatus ciContent Nothing + (ACIContent SMDRcv ciContent, ACIStatus SMDRcv ciStatus, Just (AFS SMDRcv fileStatus)) -> + Right $ cItem SMDRcv CILocalRcv ciStatus ciContent (maybeCIFile fileStatus) + (ACIContent SMDRcv ciContent, ACIStatus SMDRcv ciStatus, Nothing) -> + Right $ cItem SMDRcv CILocalRcv ciStatus ciContent Nothing + _ -> badItem + maybeCIFile :: CIFileStatus d -> Maybe (CIFile d) + maybeCIFile fileStatus = + case (fileId_, fileName_, fileSize_, fileProtocol_) of + (Just fileId, Just fileName, Just fileSize, Just fileProtocol) -> + let cfArgs = CFArgs <$> fileKey <*> fileNonce + fileSource = (`CryptoFile` cfArgs) <$> filePath + in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol} + _ -> Nothing + cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTLocal d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTLocal + cItem d chatDir ciStatus content file = + CChatItem d ChatItem {chatDir, meta = ciMeta content ciStatus, content, mentions = M.empty, formattedText = parseMaybeMarkdownList itemText, quotedItem = Nothing, reactions = [], file} + badItem = Left $ SEBadChatItem itemId (Just itemTs) + ciMeta :: CIContent d -> CIStatus d -> CIMeta 'CTLocal d + ciMeta content status = + let itemDeleted' = case itemDeleted of + DBCINotDeleted -> Nothing + _ -> Just (CIDeleted @'CTLocal deletedTs) + itemEdited' = maybe False unBI itemEdited + itemForwarded = toCIForwardedFrom forwardedFromRow + in mkCIMeta itemId content itemText status (unBI <$> sentViaProxy) sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed (unBI <$> itemLive) userMention currentTs itemTs Nothing createdAt updatedAt + ciTimed :: Maybe CITimed + ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} + getContactRequestChatPreviews_ :: DB.Connection -> User -> PaginationByTime -> ChatListQuery -> IO [AChatPreviewData] getContactRequestChatPreviews_ db User {userId} pagination clq = case clq of - CLQFilters {favorite = False, unread = False} -> query "" + CLQFilters {favorite = False, unread = False} -> map toPreview <$> getPreviews "" CLQFilters {favorite = True, unread = False} -> pure [] - CLQFilters {favorite = False, unread = True} -> query "" - CLQFilters {favorite = True, unread = True} -> query "" - CLQSearch {search} -> query search + CLQFilters {favorite = False, unread = True} -> map toPreview <$> getPreviews "" + CLQFilters {favorite = True, unread = True} -> map toPreview <$> getPreviews "" + CLQSearch {search} -> map toPreview <$> getPreviews search where - (pagQuery, pagParams) = paginationByTimeFilter pagination - query search = - map toPreview - <$> DB.queryNamed - db - ( [sql| - SELECT - cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, - cr.created_at, cr.updated_at as ts, - cr.peer_chat_min_version, cr.peer_chat_max_version - FROM contact_requests cr - JOIN connections c ON c.user_contact_link_id = cr.user_contact_link_id - JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id - JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id - WHERE cr.user_id = :user_id - AND uc.user_id = :user_id - AND uc.local_display_name = '' - AND uc.group_id IS NULL - AND ( - cr.local_display_name LIKE '%' || :search || '%' - OR p.display_name LIKE '%' || :search || '%' - OR p.full_name LIKE '%' || :search || '%' - ) - |] - <> pagQuery + query = + [sql| + SELECT + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, + cr.created_at, cr.updated_at, + cr.peer_chat_min_version, cr.peer_chat_max_version + FROM contact_requests cr + JOIN connections c ON c.user_contact_link_id = cr.user_contact_link_id + JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id + JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id + WHERE cr.user_id = ? + AND uc.user_id = ? + AND uc.local_display_name = '' + AND uc.group_id IS NULL + AND ( + LOWER(cr.local_display_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(p.display_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(p.full_name) LIKE '%' || LOWER(?) || '%' ) - ([":user_id" := userId, ":search" := search] <> pagParams) + |] + params search = (userId, userId, search, search, search) + getPreviews search = case pagination of + PTLast count -> DB.query db (query <> " ORDER BY cr.updated_at DESC LIMIT ?") (params search :. Only count) + PTAfter ts count -> DB.query db (query <> " AND cr.updated_at > ? ORDER BY cr.updated_at ASC LIMIT ?") (params search :. (ts, count)) + PTBefore ts count -> DB.query db (query <> " AND cr.updated_at < ? ORDER BY cr.updated_at DESC LIMIT ?") (params search :. (ts, count)) toPreview :: ContactRequestRow -> AChatPreviewData toPreview cReqRow = let cReq@UserContactRequest {updatedAt} = toContactRequest cReqRow - stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - aChat = AChat SCTContactRequest $ Chat (ContactRequest cReq) [] stats + aChat = AChat SCTContactRequest $ Chat (ContactRequest cReq) [] emptyChatStats in ACPD SCTContactRequest $ ContactRequestPD updatedAt aChat getContactConnectionChatPreviews_ :: DB.Connection -> User -> PaginationByTime -> ChatListQuery -> IO [AChatPreviewData] getContactConnectionChatPreviews_ db User {userId} pagination clq = case clq of - CLQFilters {favorite = False, unread = False} -> query "" + CLQFilters {favorite = False, unread = False} -> map toPreview <$> getPreviews "" CLQFilters {favorite = True, unread = False} -> pure [] CLQFilters {favorite = False, unread = True} -> pure [] CLQFilters {favorite = True, unread = True} -> pure [] - CLQSearch {search} -> query search + CLQSearch {search} -> map toPreview <$> getPreviews search where - (pagQuery, pagParams) = paginationByTimeFilter pagination - query search = - map toPreview - <$> DB.queryNamed - db - ( [sql| - SELECT - connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, - custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at as ts - FROM connections - WHERE user_id = :user_id - AND conn_type = :conn_contact - AND contact_id IS NULL - AND conn_level = 0 - AND via_contact IS NULL - AND (via_group_link = 0 || (via_group_link = 1 AND group_link_id IS NOT NULL)) - AND local_alias LIKE '%' || :search || '%' - |] - <> pagQuery - ) - ([":user_id" := userId, ":conn_contact" := ConnContact, ":search" := search] <> pagParams) - toPreview :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, LocalAlias, UTCTime, UTCTime) -> AChatPreviewData + query = + [sql| + SELECT + connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, + custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at + FROM connections + WHERE user_id = ? + AND conn_type = ? + AND conn_status != ? + AND contact_id IS NULL + AND conn_level = 0 + AND via_contact IS NULL + AND (via_group_link = 0 OR (via_group_link = 1 AND group_link_id IS NOT NULL)) + AND LOWER(local_alias) LIKE '%' || LOWER(?) || '%' + |] + params search = (userId, ConnContact, ConnPrepared, search) + getPreviews search = case pagination of + PTLast count -> DB.query db (query <> " ORDER BY updated_at DESC LIMIT ?") (params search :. Only count) + PTAfter ts count -> DB.query db (query <> " AND updated_at > ? ORDER BY updated_at ASC LIMIT ?") (params search :. (ts, count)) + PTBefore ts count -> DB.query db (query <> " AND updated_at < ? ORDER BY updated_at DESC LIMIT ?") (params search :. (ts, count)) + toPreview :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, Maybe (ConnShortLink 'CMInvitation), LocalAlias, UTCTime, UTCTime) -> AChatPreviewData toPreview connRow = let conn@PendingContactConnection {updatedAt} = toPendingContactConnection connRow - stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - aChat = AChat SCTContactConnection $ Chat (ContactConnection conn) [] stats + aChat = AChat SCTContactConnection $ Chat (ContactConnection conn) [] emptyChatStats in ACPD SCTContactConnection $ ContactConnectionPD updatedAt aChat -getDirectChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTDirect) -getDirectChat db user contactId pagination search_ = do +getDirectChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) +getDirectChat db vr user contactId pagination search_ = do let search = fromMaybe "" search_ - ct <- getContact db user contactId - liftIO . getDirectChatReactions_ db ct =<< case pagination of - CPLast count -> getDirectChatLast_ db user ct count search - CPAfter afterId count -> getDirectChatAfter_ db user ct afterId count search - CPBefore beforeId count -> getDirectChatBefore_ db user ct beforeId count search - -getDirectChatLast_ :: DB.Connection -> User -> Contact -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect) -getDirectChatLast_ db user ct@Contact {contactId} count search = do - let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - chatItems <- getDirectChatItemsLast db user contactId count search - pure $ Chat (DirectChat ct) (reverse chatItems) stats + ct <- getContact db vr user contactId + case pagination of + CPLast count -> liftIO $ (,Nothing) <$> getDirectChatLast_ db user ct count search + CPAfter afterId count -> (,Nothing) <$> getDirectChatAfter_ db user ct afterId count search + CPBefore beforeId count -> (,Nothing) <$> getDirectChatBefore_ db user ct beforeId count search + CPAround aroundId count -> getDirectChatAround_ db user ct aroundId count search + CPInitial count -> do + unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search" + getDirectChatInitial_ db user ct count -- the last items in reverse order (the last item in the conversation is the first in the returned list) -getDirectChatItemsLast :: DB.Connection -> User -> ContactId -> Int -> String -> ExceptT StoreError IO [CChatItem 'CTDirect] -getDirectChatItemsLast db User {userId} contactId count search = ExceptT $ do - currentTs <- getCurrentTime - mapM (toDirectChatItem currentTs) +getDirectChatLast_ :: DB.Connection -> User -> Contact -> Int -> String -> IO (Chat 'CTDirect) +getDirectChatLast_ db user ct count search = do + ciIds <- getDirectChatItemIdsLast_ db user ct count search + ts <- getCurrentTime + cis <- mapM (safeGetDirectItem db user ct ts) ciIds + pure $ Chat (DirectChat ct) (reverse cis) emptyChatStats + +getDirectChatItemIdsLast_ :: DB.Connection -> User -> Contact -> Int -> String -> IO [ChatItemId] +getDirectChatItemIdsLast_ db User {userId} Contact {contactId} count search = + map fromOnly <$> DB.query db [sql| - SELECT - -- ChatItem - i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, - -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, - -- DirectQuote - ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent - FROM chat_items i - LEFT JOIN files f ON f.chat_item_id = i.chat_item_id - LEFT JOIN chat_items ri ON ri.user_id = i.user_id AND ri.contact_id = i.contact_id AND ri.shared_msg_id = i.quoted_shared_msg_id - WHERE i.user_id = ? AND i.contact_id = ? AND i.item_text LIKE '%' || ? || '%' - ORDER BY i.created_at DESC, i.chat_item_id DESC + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' + ORDER BY created_at DESC, chat_item_id DESC LIMIT ? |] (userId, contactId, search, count) -getDirectChatAfter_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect) -getDirectChatAfter_ db User {userId} ct@Contact {contactId} afterChatItemId count search = do - let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - chatItems <- ExceptT getDirectChatItemsAfter_ - pure $ Chat (DirectChat ct) chatItems stats +safeGetDirectItem :: DB.Connection -> User -> Contact -> UTCTime -> ChatItemId -> IO (CChatItem 'CTDirect) +safeGetDirectItem db user ct currentTs itemId = + runExceptT (getDirectCIWithReactions db user ct itemId) + >>= pure <$> safeToDirectItem currentTs itemId + +safeToDirectItem :: UTCTime -> ChatItemId -> Either StoreError (CChatItem 'CTDirect) -> CChatItem 'CTDirect +safeToDirectItem currentTs itemId = \case + Right ci -> ci + Left e@(SEBadChatItem _ (Just itemTs)) -> badDirectItem itemTs e + Left e -> badDirectItem currentTs e where - getDirectChatItemsAfter_ :: IO (Either StoreError [CChatItem 'CTDirect]) - getDirectChatItemsAfter_ = do - currentTs <- getCurrentTime - mapM (toDirectChatItem currentTs) - <$> DB.query - db - [sql| - SELECT - -- ChatItem - i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, - -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, - -- DirectQuote - ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent - FROM chat_items i - LEFT JOIN files f ON f.chat_item_id = i.chat_item_id - LEFT JOIN chat_items ri ON ri.user_id = i.user_id AND ri.contact_id = i.contact_id AND ri.shared_msg_id = i.quoted_shared_msg_id - WHERE i.user_id = ? AND i.contact_id = ? AND i.item_text LIKE '%' || ? || '%' - AND i.chat_item_id > ? - ORDER BY i.created_at ASC, i.chat_item_id ASC - LIMIT ? - |] - (userId, contactId, search, afterChatItemId, count) + badDirectItem :: UTCTime -> StoreError -> CChatItem 'CTDirect + badDirectItem ts e = + let errorText = T.pack $ show e + in CChatItem + SMDSnd + ChatItem + { chatDir = CIDirectSnd, + meta = dummyMeta itemId ts errorText, + content = CIInvalidJSON errorText, + mentions = M.empty, + formattedText = Nothing, + quotedItem = Nothing, + reactions = [], + file = Nothing + } + +getDirectChatItemLast :: DB.Connection -> User -> ContactId -> ExceptT StoreError IO (CChatItem 'CTDirect) +getDirectChatItemLast db user@User {userId} contactId = do + chatItemId <- + ExceptT . firstRow fromOnly (SEChatItemNotFoundByContactId contactId) $ + DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? + ORDER BY created_at DESC, chat_item_id DESC + LIMIT 1 + |] + (userId, contactId) + getDirectChatItem db user contactId chatItemId + +getDirectChatAfter_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect) +getDirectChatAfter_ db user ct@Contact {contactId} afterId count search = do + afterCI <- getDirectChatItem db user contactId afterId + ciIds <- liftIO $ getDirectCIsAfter_ db user ct afterCI count search + ts <- liftIO getCurrentTime + cis <- liftIO $ mapM (safeGetDirectItem db user ct ts) ciIds + pure $ Chat (DirectChat ct) cis emptyChatStats + +getDirectCIsAfter_ :: DB.Connection -> User -> Contact -> CChatItem 'CTDirect -> Int -> String -> IO [ChatItemId] +getDirectCIsAfter_ db User {userId} Contact {contactId} afterCI count search = + map fromOnly + <$> DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' + AND (created_at > ? OR (created_at = ? AND chat_item_id > ?)) + ORDER BY created_at ASC, chat_item_id ASC + LIMIT ? + |] + (userId, contactId, search, ciCreatedAt afterCI, ciCreatedAt afterCI, cChatItemId afterCI, count) getDirectChatBefore_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect) -getDirectChatBefore_ db User {userId} ct@Contact {contactId} beforeChatItemId count search = do - let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - chatItems <- ExceptT getDirectChatItemsBefore_ - pure $ Chat (DirectChat ct) (reverse chatItems) stats +getDirectChatBefore_ db user ct@Contact {contactId} beforeId count search = do + beforeCI <- getDirectChatItem db user contactId beforeId + ciIds <- liftIO $ getDirectCIsBefore_ db user ct beforeCI count search + ts <- liftIO getCurrentTime + cis <- liftIO $ mapM (safeGetDirectItem db user ct ts) ciIds + pure $ Chat (DirectChat ct) (reverse cis) emptyChatStats + +getDirectCIsBefore_ :: DB.Connection -> User -> Contact -> CChatItem 'CTDirect -> Int -> String -> IO [ChatItemId] +getDirectCIsBefore_ db User {userId} Contact {contactId} beforeCI count search = + map fromOnly + <$> DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' + AND (created_at < ? OR (created_at = ? AND chat_item_id < ?)) + ORDER BY created_at DESC, chat_item_id DESC + LIMIT ? + |] + (userId, contactId, search, ciCreatedAt beforeCI, ciCreatedAt beforeCI, cChatItemId beforeCI, count) + +getDirectChatAround_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) +getDirectChatAround_ db user ct aroundId count search = do + stats <- liftIO $ getContactStats_ db user ct + getDirectChatAround' db user ct aroundId count search stats + +getDirectChatAround' :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ChatStats -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) +getDirectChatAround' db user ct@Contact {contactId} aroundId count search stats = do + aroundCI <- getDirectChatItem db user contactId aroundId + beforeIds <- liftIO $ getDirectCIsBefore_ db user ct aroundCI count search + afterIds <- liftIO $ getDirectCIsAfter_ db user ct aroundCI count search + ts <- liftIO getCurrentTime + beforeCIs <- liftIO $ mapM (safeGetDirectItem db user ct ts) beforeIds + afterCIs <- liftIO $ mapM (safeGetDirectItem db user ct ts) afterIds + let cis = reverse beforeCIs <> [aroundCI] <> afterCIs + navInfo <- liftIO $ getNavInfo cis + pure (Chat (DirectChat ct) cis stats, Just navInfo) where - getDirectChatItemsBefore_ :: IO (Either StoreError [CChatItem 'CTDirect]) - getDirectChatItemsBefore_ = do - currentTs <- getCurrentTime - mapM (toDirectChatItem currentTs) + getNavInfo cis_ = case cis_ of + [] -> pure $ NavigationInfo 0 0 + cis -> getContactNavInfo_ db user ct (last cis) + +getDirectChatInitial_ :: DB.Connection -> User -> Contact -> Int -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) +getDirectChatInitial_ db user ct count = do + liftIO (getContactMinUnreadId_ db user ct) >>= \case + Just minUnreadItemId -> do + unreadCount <- liftIO $ getContactUnreadCount_ db user ct + let stats = emptyChatStats {unreadCount, minUnreadItemId} + getDirectChatAround' db user ct minUnreadItemId count "" stats + Nothing -> liftIO $ (,Just $ NavigationInfo 0 0) <$> getDirectChatLast_ db user ct count "" + +getContactStats_ :: DB.Connection -> User -> Contact -> IO ChatStats +getContactStats_ db user ct = do + minUnreadItemId <- fromMaybe 0 <$> getContactMinUnreadId_ db user ct + unreadCount <- getContactUnreadCount_ db user ct + pure emptyChatStats {unreadCount, minUnreadItemId} + +getContactMinUnreadId_ :: DB.Connection -> User -> Contact -> IO (Maybe ChatItemId) +getContactMinUnreadId_ db User {userId} Contact {contactId} = + fmap join . maybeFirstRow fromOnly $ + DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_status = ? + ORDER BY created_at ASC, chat_item_id ASC + LIMIT 1 + |] + (userId, contactId, CISRcvNew) + +getContactUnreadCount_ :: DB.Connection -> User -> Contact -> IO Int +getContactUnreadCount_ db User {userId} Contact {contactId} = + fromOnly . head + <$> DB.query + db + [sql| + SELECT COUNT(1) + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_status = ? + |] + (userId, contactId, CISRcvNew) + +getContactNavInfo_ :: DB.Connection -> User -> Contact -> CChatItem 'CTDirect -> IO NavigationInfo +getContactNavInfo_ db User {userId} Contact {contactId} afterCI = do + afterUnread <- getAfterUnreadCount + afterTotal <- getAfterTotalCount + pure NavigationInfo {afterUnread, afterTotal} + where + getAfterUnreadCount :: IO Int + getAfterUnreadCount = + fromOnly . head <$> DB.query db [sql| - SELECT - -- ChatItem - i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, - -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, - -- DirectQuote - ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent - FROM chat_items i - LEFT JOIN files f ON f.chat_item_id = i.chat_item_id - LEFT JOIN chat_items ri ON ri.user_id = i.user_id AND ri.contact_id = i.contact_id AND ri.shared_msg_id = i.quoted_shared_msg_id - WHERE i.user_id = ? AND i.contact_id = ? AND i.item_text LIKE '%' || ? || '%' - AND i.chat_item_id < ? - ORDER BY i.created_at DESC, i.chat_item_id DESC - LIMIT ? + SELECT COUNT(1) + FROM ( + SELECT 1 + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_status = ? + AND created_at > ? + UNION ALL + SELECT 1 + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_status = ? + AND created_at = ? AND chat_item_id > ? + ) ci |] - (userId, contactId, search, beforeChatItemId, count) + ( (userId, contactId, CISRcvNew, ciCreatedAt afterCI) + :. (userId, contactId, CISRcvNew, ciCreatedAt afterCI, cChatItemId afterCI) + ) + getAfterTotalCount :: IO Int + getAfterTotalCount = + fromOnly . head + <$> DB.query + db + [sql| + SELECT COUNT(1) + FROM ( + SELECT 1 + FROM chat_items + WHERE user_id = ? AND contact_id = ? + AND created_at > ? + UNION ALL + SELECT 1 + FROM chat_items + WHERE user_id = ? AND contact_id = ? + AND created_at = ? AND chat_item_id > ? + ) ci + |] + ( (userId, contactId, ciCreatedAt afterCI) + :. (userId, contactId, ciCreatedAt afterCI, cChatItemId afterCI) + ) -getGroupChat :: DB.Connection -> VersionRange -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup) -getGroupChat db vr user groupId pagination search_ = do +getGroupChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> Maybe MsgContentTag -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) +getGroupChat db vr user groupId contentFilter pagination search_ = do let search = fromMaybe "" search_ g <- getGroupInfo db vr user groupId case pagination of - CPLast count -> getGroupChatLast_ db user g count search - CPAfter afterId count -> getGroupChatAfter_ db user g afterId count search - CPBefore beforeId count -> getGroupChatBefore_ db user g beforeId count search + CPLast count -> liftIO $ (,Nothing) <$> getGroupChatLast_ db user g contentFilter count search emptyChatStats + CPAfter afterId count -> (,Nothing) <$> getGroupChatAfter_ db user g contentFilter afterId count search + CPBefore beforeId count -> (,Nothing) <$> getGroupChatBefore_ db user g contentFilter beforeId count search + CPAround aroundId count -> getGroupChatAround_ db user g contentFilter aroundId count search + CPInitial count -> do + unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search" + getGroupChatInitial_ db user g contentFilter count -getGroupChatLast_ :: DB.Connection -> User -> GroupInfo -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup) -getGroupChatLast_ db user@User {userId} g@GroupInfo {groupId} count search = do - let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - chatItemIds <- liftIO getGroupChatItemIdsLast_ - chatItems <- mapM (getGroupCIWithReactions db user g) chatItemIds - pure $ Chat (GroupChat g) (reverse chatItems) stats +getGroupChatLast_ :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> Int -> String -> ChatStats -> IO (Chat 'CTGroup) +getGroupChatLast_ db user g contentFilter count search stats = do + ciIds <- getGroupChatItemIDs db user g contentFilter GRLast count search + ts <- getCurrentTime + cis <- mapM (safeGetGroupItem db user g ts) ciIds + pure $ Chat (GroupChat g) (reverse cis) stats + +data GroupItemIDsRange = GRLast | GRAfter UTCTime ChatItemId | GRBefore UTCTime ChatItemId + +getGroupChatItemIDs :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> GroupItemIDsRange -> Int -> String -> IO [ChatItemId] +getGroupChatItemIDs db User {userId} GroupInfo {groupId} contentFilter range count search = case contentFilter of + Just mcTag -> idsQuery (baseCond <> " AND msg_content_tag = ? ") (userId, groupId, mcTag) + Nothing -> idsQuery baseCond (userId, groupId) where - getGroupChatItemIdsLast_ :: IO [ChatItemId] - getGroupChatItemIdsLast_ = - map fromOnly - <$> DB.query - db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%' - ORDER BY item_ts DESC, chat_item_id DESC - LIMIT ? - |] - (userId, groupId, search, count) + baseQuery = " SELECT chat_item_id FROM chat_items WHERE " + baseCond = " user_id = ? AND group_id = ? " + idsQuery :: ToRow p => Query -> p -> IO [ChatItemId] + idsQuery c p = case range of + GRLast -> rangeQuery c p " ORDER BY item_ts DESC, chat_item_id DESC " + GRAfter ts itemId -> + rangeQuery + (" item_ts > ? " `orCond` " item_ts = ? AND chat_item_id > ? ") + (orParams ts itemId) + " ORDER BY item_ts ASC, chat_item_id ASC " + GRBefore ts itemId -> + rangeQuery + (" item_ts < ? " `orCond` " item_ts = ? AND chat_item_id < ? ") + (orParams ts itemId) + " ORDER BY item_ts DESC, chat_item_id DESC " + where + orCond c1 c2 = " ((" <> c <> " AND " <> c1 <> ") OR (" <> c <> " AND " <> c2 <> ")) " + orParams ts itemId = (p :. (Only ts) :. p :. (ts, itemId)) + rangeQuery :: ToRow p => Query -> p -> Query -> IO [ChatItemId] + rangeQuery c p ob + | null search = searchQuery "" () + | otherwise = searchQuery " AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' " (Only search) + where + searchQuery :: ToRow p' => Query -> p' -> IO [ChatItemId] + searchQuery c' p' = + map fromOnly <$> DB.query db (baseQuery <> c <> c' <> ob <> " LIMIT ?") (p :. p' :. Only count) + +safeGetGroupItem :: DB.Connection -> User -> GroupInfo -> UTCTime -> ChatItemId -> IO (CChatItem 'CTGroup) +safeGetGroupItem db user g currentTs itemId = + runExceptT (getGroupCIWithReactions db user g itemId) + >>= pure <$> safeToGroupItem currentTs itemId + +safeToGroupItem :: UTCTime -> ChatItemId -> Either StoreError (CChatItem 'CTGroup) -> CChatItem 'CTGroup +safeToGroupItem currentTs itemId = \case + Right ci -> ci + Left e@(SEBadChatItem _ (Just itemTs)) -> badGroupItem itemTs e + Left e -> badGroupItem currentTs e + where + badGroupItem :: UTCTime -> StoreError -> CChatItem 'CTGroup + badGroupItem ts e = + let errorText = T.pack $ show e + in CChatItem + SMDSnd + ChatItem + { chatDir = CIGroupSnd, + meta = dummyMeta itemId ts errorText, + content = CIInvalidJSON errorText, + mentions = M.empty, + formattedText = Nothing, + quotedItem = Nothing, + reactions = [], + file = Nothing + } getGroupMemberChatItemLast :: DB.Connection -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO (CChatItem 'CTGroup) getGroupMemberChatItemLast db user@User {userId} groupId groupMemberId = do @@ -921,174 +1322,534 @@ getGroupMemberChatItemLast db user@User {userId} groupId groupMemberId = do (userId, groupId, groupMemberId) getGroupChatItem db user groupId chatItemId -getGroupChatAfter_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup) -getGroupChatAfter_ db user@User {userId} g@GroupInfo {groupId} afterChatItemId count search = do - let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - afterChatItem <- getGroupChatItem db user groupId afterChatItemId - chatItemIds <- liftIO $ getGroupChatItemIdsAfter_ (chatItemTs afterChatItem) - chatItems <- mapM (getGroupCIWithReactions db user g) chatItemIds - pure $ Chat (GroupChat g) chatItems stats +getGroupChatAfter_ :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup) +getGroupChatAfter_ db user g@GroupInfo {groupId} contentFilter afterId count search = do + afterCI <- getGroupChatItem db user groupId afterId + let range = GRAfter (chatItemTs afterCI) (cChatItemId afterCI) + ciIds <- liftIO $ getGroupChatItemIDs db user g contentFilter range count search + ts <- liftIO getCurrentTime + cis <- liftIO $ mapM (safeGetGroupItem db user g ts) ciIds + pure $ Chat (GroupChat g) cis emptyChatStats + +getGroupChatBefore_ :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup) +getGroupChatBefore_ db user g@GroupInfo {groupId} contentFilter beforeId count search = do + beforeCI <- getGroupChatItem db user groupId beforeId + let range = GRBefore (chatItemTs beforeCI) (cChatItemId beforeCI) + ciIds <- liftIO $ getGroupChatItemIDs db user g contentFilter range count search + ts <- liftIO getCurrentTime + cis <- liftIO $ mapM (safeGetGroupItem db user g ts) ciIds + pure $ Chat (GroupChat g) (reverse cis) emptyChatStats + +getGroupChatAround_ :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) +getGroupChatAround_ db user g contentFilter aroundId count search = do + stats <- liftIO $ getGroupStats_ db user g + getGroupChatAround' db user g contentFilter aroundId count search stats + +getGroupChatAround' :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> String -> ChatStats -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) +getGroupChatAround' db user g contentFilter aroundId count search stats = do + aroundCI <- getGroupCIWithReactions db user g aroundId + let beforeRange = GRBefore (chatItemTs aroundCI) (cChatItemId aroundCI) + afterRange = GRAfter (chatItemTs aroundCI) (cChatItemId aroundCI) + beforeIds <- liftIO $ getGroupChatItemIDs db user g contentFilter beforeRange count search + afterIds <- liftIO $ getGroupChatItemIDs db user g contentFilter afterRange count search + ts <- liftIO getCurrentTime + beforeCIs <- liftIO $ mapM (safeGetGroupItem db user g ts) beforeIds + afterCIs <- liftIO $ mapM (safeGetGroupItem db user g ts) afterIds + let cis = reverse beforeCIs <> [aroundCI] <> afterCIs + navInfo <- liftIO $ getNavInfo cis + pure (Chat (GroupChat g) cis stats, Just navInfo) where - getGroupChatItemIdsAfter_ :: UTCTime -> IO [ChatItemId] - getGroupChatItemIdsAfter_ afterChatItemTs = - map fromOnly + getNavInfo cis_ = case cis_ of + [] -> pure $ NavigationInfo 0 0 + cis -> getGroupNavInfo_ db user g (last cis) + +getGroupChatInitial_ :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> Int -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) +getGroupChatInitial_ db user g contentFilter count = do + liftIO (getGroupMinUnreadId_ db user g contentFilter) >>= \case + Just minUnreadItemId -> do + stats <- liftIO $ getStats minUnreadItemId =<< getGroupUnreadCount_ db user g Nothing + getGroupChatAround' db user g contentFilter minUnreadItemId count "" stats + Nothing -> liftIO $ do + stats <- getStats 0 (0, 0) + (,Just $ NavigationInfo 0 0) <$> getGroupChatLast_ db user g contentFilter count "" stats + where + getStats minUnreadItemId (unreadCount, unreadMentions) = do + reportsCount <- getGroupReportsCount_ db user g False + pure ChatStats {unreadCount, unreadMentions, reportsCount, minUnreadItemId, unreadChat = False} + +getGroupStats_ :: DB.Connection -> User -> GroupInfo -> IO ChatStats +getGroupStats_ db user g = do + minUnreadItemId <- fromMaybe 0 <$> getGroupMinUnreadId_ db user g Nothing + (unreadCount, unreadMentions) <- getGroupUnreadCount_ db user g Nothing + reportsCount <- getGroupReportsCount_ db user g False + pure ChatStats {unreadCount, unreadMentions, reportsCount, minUnreadItemId, unreadChat = False} + +getGroupMinUnreadId_ :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> IO (Maybe ChatItemId) +getGroupMinUnreadId_ db user g contentFilter = + fmap join . maybeFirstRow fromOnly $ + queryUnreadGroupItems db user g contentFilter baseQuery orderLimit + where + baseQuery = "SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? " + orderLimit = " ORDER BY item_ts ASC, chat_item_id ASC LIMIT 1" + +getGroupUnreadCount_ :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> IO (Int, Int) +getGroupUnreadCount_ db user g contentFilter = + head <$> queryUnreadGroupItems db user g contentFilter baseQuery "" + where + baseQuery = "SELECT COUNT(1), COALESCE(SUM(user_mention), 0) FROM chat_items WHERE user_id = ? AND group_id = ? " + +getGroupReportsCount_ :: DB.Connection -> User -> GroupInfo -> Bool -> IO Int +getGroupReportsCount_ db User {userId} GroupInfo {groupId} archived = + fromOnly . head + <$> DB.query + db + "SELECT COUNT(1) FROM chat_items WHERE user_id = ? AND group_id = ? AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0" + (userId, groupId, MCReport_, BI archived) + +queryUnreadGroupItems :: FromRow r => DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> Query -> Query -> IO [r] +queryUnreadGroupItems db User {userId} GroupInfo {groupId} contentFilter baseQuery orderLimit = + case contentFilter of + Just mcTag -> + DB.query + db + (baseQuery <> " AND msg_content_tag = ? AND item_status = ? " <> orderLimit) + (userId, groupId, mcTag, CISRcvNew) + Nothing -> + DB.query + db + (baseQuery <> " AND item_status = ? " <> orderLimit) + (userId, groupId, CISRcvNew) + +getGroupNavInfo_ :: DB.Connection -> User -> GroupInfo -> CChatItem 'CTGroup -> IO NavigationInfo +getGroupNavInfo_ db User {userId} GroupInfo {groupId} afterCI = do + afterUnread <- getAfterUnreadCount + afterTotal <- getAfterTotalCount + pure NavigationInfo {afterUnread, afterTotal} + where + getAfterUnreadCount :: IO Int + getAfterUnreadCount = + fromOnly . head <$> DB.query db [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%' - AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?)) - ORDER BY item_ts ASC, chat_item_id ASC - LIMIT ? + SELECT COUNT(1) + FROM ( + SELECT 1 + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_status = ? + AND item_ts > ? + UNION ALL + SELECT 1 + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_status = ? + AND item_ts = ? AND chat_item_id > ? + ) ci |] - (userId, groupId, search, afterChatItemTs, afterChatItemTs, afterChatItemId, count) - -getGroupChatBefore_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup) -getGroupChatBefore_ db user@User {userId} g@GroupInfo {groupId} beforeChatItemId count search = do - let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - beforeChatItem <- getGroupChatItem db user groupId beforeChatItemId - chatItemIds <- liftIO $ getGroupChatItemIdsBefore_ (chatItemTs beforeChatItem) - chatItems <- mapM (getGroupCIWithReactions db user g) chatItemIds - pure $ Chat (GroupChat g) (reverse chatItems) stats - where - getGroupChatItemIdsBefore_ :: UTCTime -> IO [ChatItemId] - getGroupChatItemIdsBefore_ beforeChatItemTs = - map fromOnly + ( (userId, groupId, CISRcvNew, chatItemTs afterCI) + :. (userId, groupId, CISRcvNew, chatItemTs afterCI, cChatItemId afterCI) + ) + getAfterTotalCount :: IO Int + getAfterTotalCount = + fromOnly . head <$> DB.query db [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%' - AND (item_ts < ? OR (item_ts = ? AND chat_item_id < ?)) - ORDER BY item_ts DESC, chat_item_id DESC - LIMIT ? + SELECT COUNT(1) + FROM ( + SELECT 1 + FROM chat_items + WHERE user_id = ? AND group_id = ? + AND item_ts > ? + UNION ALL + SELECT 1 + FROM chat_items + WHERE user_id = ? AND group_id = ? + AND item_ts = ? AND chat_item_id > ? + ) ci |] - (userId, groupId, search, beforeChatItemTs, beforeChatItemTs, beforeChatItemId, count) + ( (userId, groupId, chatItemTs afterCI) + :. (userId, groupId, chatItemTs afterCI, cChatItemId afterCI) + ) -toChatItemRef :: (ChatItemId, Maybe Int64, Maybe Int64) -> Either StoreError (ChatRef, ChatItemId) +getLocalChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo) +getLocalChat db user folderId pagination search_ = do + let search = fromMaybe "" search_ + nf <- getNoteFolder db user folderId + case pagination of + CPLast count -> liftIO $ (,Nothing) <$> getLocalChatLast_ db user nf count search + CPAfter afterId count -> (,Nothing) <$> getLocalChatAfter_ db user nf afterId count search + CPBefore beforeId count -> (,Nothing) <$> getLocalChatBefore_ db user nf beforeId count search + CPAround aroundId count -> getLocalChatAround_ db user nf aroundId count search + CPInitial count -> do + unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search" + getLocalChatInitial_ db user nf count + +getLocalChatLast_ :: DB.Connection -> User -> NoteFolder -> Int -> String -> IO (Chat 'CTLocal) +getLocalChatLast_ db user nf count search = do + ciIds <- getLocalChatItemIdsLast_ db user nf count search + ts <- getCurrentTime + cis <- mapM (safeGetLocalItem db user nf ts) ciIds + pure $ Chat (LocalChat nf) (reverse cis) emptyChatStats + +getLocalChatItemIdsLast_ :: DB.Connection -> User -> NoteFolder -> Int -> String -> IO [ChatItemId] +getLocalChatItemIdsLast_ db User {userId} NoteFolder {noteFolderId} count search = + map fromOnly + <$> DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' + ORDER BY created_at DESC, chat_item_id DESC + LIMIT ? + |] + (userId, noteFolderId, search, count) + +safeGetLocalItem :: DB.Connection -> User -> NoteFolder -> UTCTime -> ChatItemId -> IO (CChatItem 'CTLocal) +safeGetLocalItem db user NoteFolder {noteFolderId} currentTs itemId = + runExceptT (getLocalChatItem db user noteFolderId itemId) + >>= pure <$> safeToLocalItem currentTs itemId + +safeToLocalItem :: UTCTime -> ChatItemId -> Either StoreError (CChatItem 'CTLocal) -> CChatItem 'CTLocal +safeToLocalItem currentTs itemId = \case + Right ci -> ci + Left e@(SEBadChatItem _ (Just itemTs)) -> badLocalItem itemTs e + Left e -> badLocalItem currentTs e + where + badLocalItem :: UTCTime -> StoreError -> CChatItem 'CTLocal + badLocalItem ts e = + let errorText = T.pack $ show e + in CChatItem + SMDSnd + ChatItem + { chatDir = CILocalSnd, + meta = dummyMeta itemId ts errorText, + content = CIInvalidJSON errorText, + mentions = M.empty, + formattedText = Nothing, + quotedItem = Nothing, + reactions = [], + file = Nothing + } + +getLocalChatAfter_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal) +getLocalChatAfter_ db user nf@NoteFolder {noteFolderId} afterId count search = do + afterCI <- getLocalChatItem db user noteFolderId afterId + ciIds <- liftIO $ getLocalCIsAfter_ db user nf afterCI count search + ts <- liftIO getCurrentTime + cis <- liftIO $ mapM (safeGetLocalItem db user nf ts) ciIds + pure $ Chat (LocalChat nf) cis emptyChatStats + +getLocalCIsAfter_ :: DB.Connection -> User -> NoteFolder -> CChatItem 'CTLocal -> Int -> String -> IO [ChatItemId] +getLocalCIsAfter_ db User {userId} NoteFolder {noteFolderId} afterCI count search = + map fromOnly + <$> DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' + AND (created_at > ? OR (created_at = ? AND chat_item_id > ?)) + ORDER BY created_at ASC, chat_item_id ASC + LIMIT ? + |] + (userId, noteFolderId, search, ciCreatedAt afterCI, ciCreatedAt afterCI, cChatItemId afterCI, count) + +getLocalChatBefore_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal) +getLocalChatBefore_ db user nf@NoteFolder {noteFolderId} beforeId count search = do + beforeCI <- getLocalChatItem db user noteFolderId beforeId + ciIds <- liftIO $ getLocalCIsBefore_ db user nf beforeCI count search + ts <- liftIO getCurrentTime + cis <- liftIO $ mapM (safeGetLocalItem db user nf ts) ciIds + pure $ Chat (LocalChat nf) (reverse cis) emptyChatStats + +getLocalCIsBefore_ :: DB.Connection -> User -> NoteFolder -> CChatItem 'CTLocal -> Int -> String -> IO [ChatItemId] +getLocalCIsBefore_ db User {userId} NoteFolder {noteFolderId} beforeCI count search = + map fromOnly + <$> DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' + AND (created_at < ? OR (created_at = ? AND chat_item_id < ?)) + ORDER BY created_at DESC, chat_item_id DESC + LIMIT ? + |] + (userId, noteFolderId, search, ciCreatedAt beforeCI, ciCreatedAt beforeCI, cChatItemId beforeCI, count) + +getLocalChatAround_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo) +getLocalChatAround_ db user nf aroundId count search = do + stats <- liftIO $ getLocalStats_ db user nf + getLocalChatAround' db user nf aroundId count search stats + +getLocalChatAround' :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ChatStats -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo) +getLocalChatAround' db user nf@NoteFolder {noteFolderId} aroundId count search stats = do + aroundCI <- getLocalChatItem db user noteFolderId aroundId + beforeIds <- liftIO $ getLocalCIsBefore_ db user nf aroundCI count search + afterIds <- liftIO $ getLocalCIsAfter_ db user nf aroundCI count search + ts <- liftIO getCurrentTime + beforeCIs <- liftIO $ mapM (safeGetLocalItem db user nf ts) beforeIds + afterCIs <- liftIO $ mapM (safeGetLocalItem db user nf ts) afterIds + let cis = reverse beforeCIs <> [aroundCI] <> afterCIs + navInfo <- liftIO $ getNavInfo cis + pure (Chat (LocalChat nf) cis stats, Just navInfo) + where + getNavInfo cis_ = case cis_ of + [] -> pure $ NavigationInfo 0 0 + cis -> getLocalNavInfo_ db user nf (last cis) + +getLocalChatInitial_ :: DB.Connection -> User -> NoteFolder -> Int -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo) +getLocalChatInitial_ db user nf count = do + liftIO (getLocalMinUnreadId_ db user nf) >>= \case + Just minUnreadItemId -> do + unreadCount <- liftIO $ getLocalUnreadCount_ db user nf + let stats = emptyChatStats {unreadCount, minUnreadItemId} + getLocalChatAround' db user nf minUnreadItemId count "" stats + Nothing -> liftIO $ (,Just $ NavigationInfo 0 0) <$> getLocalChatLast_ db user nf count "" + +getLocalStats_ :: DB.Connection -> User -> NoteFolder -> IO ChatStats +getLocalStats_ db user nf = do + minUnreadItemId <- fromMaybe 0 <$> getLocalMinUnreadId_ db user nf + unreadCount <- getLocalUnreadCount_ db user nf + pure emptyChatStats {unreadCount, minUnreadItemId} + +getLocalMinUnreadId_ :: DB.Connection -> User -> NoteFolder -> IO (Maybe ChatItemId) +getLocalMinUnreadId_ db User {userId} NoteFolder {noteFolderId} = + fmap join . maybeFirstRow fromOnly $ + DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND item_status = ? + ORDER BY created_at ASC, chat_item_id ASC + LIMIT 1 + |] + (userId, noteFolderId, CISRcvNew) + +getLocalUnreadCount_ :: DB.Connection -> User -> NoteFolder -> IO Int +getLocalUnreadCount_ db User {userId} NoteFolder {noteFolderId} = + fromOnly . head + <$> DB.query + db + [sql| + SELECT COUNT(1) + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND item_status = ? + |] + (userId, noteFolderId, CISRcvNew) + +getLocalNavInfo_ :: DB.Connection -> User -> NoteFolder -> CChatItem 'CTLocal -> IO NavigationInfo +getLocalNavInfo_ db User {userId} NoteFolder {noteFolderId} afterCI = do + afterUnread <- getAfterUnreadCount + afterTotal <- getAfterTotalCount + pure NavigationInfo {afterUnread, afterTotal} + where + getAfterUnreadCount :: IO Int + getAfterUnreadCount = + fromOnly . head + <$> DB.query + db + [sql| + SELECT COUNT(1) + FROM ( + SELECT 1 + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND item_status = ? + AND created_at > ? + UNION ALL + SELECT 1 + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND item_status = ? + AND created_at = ? AND chat_item_id > ? + ) ci + |] + ( (userId, noteFolderId, CISRcvNew, ciCreatedAt afterCI) + :. (userId, noteFolderId, CISRcvNew, ciCreatedAt afterCI, cChatItemId afterCI) + ) + getAfterTotalCount :: IO Int + getAfterTotalCount = + fromOnly . head + <$> DB.query + db + [sql| + SELECT COUNT(1) + FROM ( + SELECT 1 + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? + AND created_at > ? + UNION ALL + SELECT 1 + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? + AND created_at = ? AND chat_item_id > ? + ) ci + |] + ( (userId, noteFolderId, ciCreatedAt afterCI) + :. (userId, noteFolderId, ciCreatedAt afterCI, cChatItemId afterCI) + ) + +toChatItemRef :: (ChatItemId, Maybe Int64, Maybe Int64, Maybe Int64) -> Either StoreError (ChatRef, ChatItemId) toChatItemRef = \case - (itemId, Just contactId, Nothing) -> Right (ChatRef CTDirect contactId, itemId) - (itemId, Nothing, Just groupId) -> Right (ChatRef CTGroup groupId, itemId) - (itemId, _, _) -> Left $ SEBadChatItem itemId + (itemId, Just contactId, Nothing, Nothing) -> Right (ChatRef CTDirect contactId, itemId) + (itemId, Nothing, Just groupId, Nothing) -> Right (ChatRef CTGroup groupId, itemId) + (itemId, Nothing, Nothing, Just folderId) -> Right (ChatRef CTLocal folderId, itemId) + (itemId, _, _, _) -> Left $ SEBadChatItem itemId Nothing -updateDirectChatItemsRead :: DB.Connection -> User -> ContactId -> Maybe (ChatItemId, ChatItemId) -> IO () -updateDirectChatItemsRead db User {userId} contactId itemsRange_ = do +updateDirectChatItemsRead :: DB.Connection -> User -> ContactId -> IO () +updateDirectChatItemsRead db User {userId} contactId = do currentTs <- getCurrentTime - case itemsRange_ of - Just (fromItemId, toItemId) -> - DB.execute - db - [sql| - UPDATE chat_items SET item_status = ?, updated_at = ? - WHERE user_id = ? AND contact_id = ? AND chat_item_id >= ? AND chat_item_id <= ? AND item_status = ? - |] - (CISRcvRead, currentTs, userId, contactId, fromItemId, toItemId, CISRcvNew) - _ -> - DB.execute - db - [sql| - UPDATE chat_items SET item_status = ?, updated_at = ? - WHERE user_id = ? AND contact_id = ? AND item_status = ? - |] - (CISRcvRead, currentTs, userId, contactId, CISRcvNew) + DB.execute + db + [sql| + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND contact_id = ? AND item_status = ? + |] + (CISRcvRead, currentTs, userId, contactId, CISRcvNew) -getDirectUnreadTimedItems :: DB.Connection -> User -> ContactId -> Maybe (ChatItemId, ChatItemId) -> IO [(ChatItemId, Int)] -getDirectUnreadTimedItems db User {userId} contactId itemsRange_ = case itemsRange_ of - Just (fromItemId, toItemId) -> - DB.query - db - [sql| - SELECT chat_item_id, timed_ttl - FROM chat_items - WHERE user_id = ? AND contact_id = ? - AND chat_item_id >= ? AND chat_item_id <= ? - AND item_status = ? - AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL - AND (item_live IS NULL OR item_live = ?) - |] - (userId, contactId, fromItemId, toItemId, CISRcvNew, False) - _ -> - DB.query - db - [sql| - SELECT chat_item_id, timed_ttl - FROM chat_items - WHERE user_id = ? AND contact_id = ? AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL - |] - (userId, contactId, CISRcvNew) +getDirectUnreadTimedItems :: DB.Connection -> User -> ContactId -> IO [(ChatItemId, Int)] +getDirectUnreadTimedItems db User {userId} contactId = + DB.query + db + [sql| + SELECT chat_item_id, timed_ttl + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL + |] + (userId, contactId, CISRcvNew) -setDirectChatItemDeleteAt :: DB.Connection -> User -> ContactId -> ChatItemId -> UTCTime -> IO () -setDirectChatItemDeleteAt db User {userId} contactId chatItemId deleteAt = +updateDirectChatItemsReadList :: DB.Connection -> User -> ContactId -> NonEmpty ChatItemId -> IO [(ChatItemId, Int)] +updateDirectChatItemsReadList db user@User {userId} contactId itemIds = do + currentTs <- getCurrentTime + catMaybes . L.toList <$> mapM (getUpdateDirectItem currentTs) itemIds + where + getUpdateDirectItem currentTs itemId = do + ttl_ <- maybeFirstRow fromOnly getUnreadTimedItem + setDirectChatItemRead_ db user contactId itemId currentTs + pure $ (itemId,) <$> ttl_ + where + getUnreadTimedItem = + DB.query + db + [sql| + SELECT timed_ttl + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_status = ? AND chat_item_id = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL + |] + (userId, contactId, CISRcvNew, itemId) + +setDirectChatItemRead :: DB.Connection -> User -> ContactId -> ChatItemId -> IO () +setDirectChatItemRead db user contactId itemId = + setDirectChatItemRead_ db user contactId itemId =<< getCurrentTime + +setDirectChatItemRead_ :: DB.Connection -> User -> ContactId -> ChatItemId -> UTCTime -> IO () +setDirectChatItemRead_ db User {userId} contactId itemId currentTs = + DB.execute + db + [sql| + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND contact_id = ? AND item_status = ? AND chat_item_id = ? + |] + (CISRcvRead, currentTs, userId, contactId, CISRcvNew, itemId) + +setDirectChatItemsDeleteAt :: DB.Connection -> User -> ContactId -> [(ChatItemId, Int)] -> UTCTime -> IO [(ChatItemId, UTCTime)] +setDirectChatItemsDeleteAt db User {userId} contactId itemIds currentTs = forM itemIds $ \(chatItemId, ttl) -> do + let deleteAt = addUTCTime (realToFrac ttl) currentTs DB.execute db "UPDATE chat_items SET timed_delete_at = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?" (deleteAt, userId, contactId, chatItemId) + pure (chatItemId, deleteAt) -updateGroupChatItemsRead :: DB.Connection -> UserId -> GroupId -> Maybe (ChatItemId, ChatItemId) -> IO () -updateGroupChatItemsRead db userId groupId itemsRange_ = do +updateGroupChatItemsRead :: DB.Connection -> User -> GroupId -> IO () +updateGroupChatItemsRead db User {userId} groupId = do currentTs <- getCurrentTime - case itemsRange_ of - Just (fromItemId, toItemId) -> - DB.execute - db - [sql| - UPDATE chat_items SET item_status = ?, updated_at = ? - WHERE user_id = ? AND group_id = ? AND chat_item_id >= ? AND chat_item_id <= ? AND item_status = ? - |] - (CISRcvRead, currentTs, userId, groupId, fromItemId, toItemId, CISRcvNew) - _ -> - DB.execute - db - [sql| - UPDATE chat_items SET item_status = ?, updated_at = ? - WHERE user_id = ? AND group_id = ? AND item_status = ? - |] - (CISRcvRead, currentTs, userId, groupId, CISRcvNew) + DB.execute + db + [sql| + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND item_status = ? + |] + (CISRcvRead, currentTs, userId, groupId, CISRcvNew) -getGroupUnreadTimedItems :: DB.Connection -> User -> GroupId -> Maybe (ChatItemId, ChatItemId) -> IO [(ChatItemId, Int)] -getGroupUnreadTimedItems db User {userId} groupId itemsRange_ = case itemsRange_ of - Just (fromItemId, toItemId) -> - DB.query - db - [sql| - SELECT chat_item_id, timed_ttl - FROM chat_items - WHERE user_id = ? AND group_id = ? - AND chat_item_id >= ? AND chat_item_id <= ? - AND item_status = ? - AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL - AND (item_live IS NULL OR item_live = ?) - |] - (userId, groupId, fromItemId, toItemId, CISRcvNew, False) - _ -> - DB.query - db - [sql| - SELECT chat_item_id, timed_ttl - FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL - |] - (userId, groupId, CISRcvNew) +getGroupUnreadTimedItems :: DB.Connection -> User -> GroupId -> IO [(ChatItemId, Int)] +getGroupUnreadTimedItems db User {userId} groupId = + DB.query + db + [sql| + SELECT chat_item_id, timed_ttl + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL + |] + (userId, groupId, CISRcvNew) -setGroupChatItemDeleteAt :: DB.Connection -> User -> GroupId -> ChatItemId -> UTCTime -> IO () -setGroupChatItemDeleteAt db User {userId} groupId chatItemId deleteAt = +updateGroupChatItemsReadList :: DB.Connection -> User -> GroupId -> NonEmpty ChatItemId -> IO [(ChatItemId, Int)] +updateGroupChatItemsReadList db User {userId} groupId itemIds = do + currentTs <- getCurrentTime + catMaybes . L.toList <$> mapM (getUpdateGroupItem currentTs) itemIds + where + getUpdateGroupItem currentTs itemId = do + ttl_ <- maybeFirstRow fromOnly getUnreadTimedItem + setItemRead + pure $ (itemId,) <$> ttl_ + where + getUnreadTimedItem = + DB.query + db + [sql| + SELECT timed_ttl + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_status = ? AND chat_item_id = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL + |] + (userId, groupId, CISRcvNew, itemId) + setItemRead = + DB.execute + db + [sql| + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND item_status = ? AND chat_item_id = ? + |] + (CISRcvRead, currentTs, userId, groupId, CISRcvNew, itemId) + +setGroupChatItemsDeleteAt :: DB.Connection -> User -> GroupId -> [(ChatItemId, Int)] -> UTCTime -> IO [(ChatItemId, UTCTime)] +setGroupChatItemsDeleteAt db User {userId} groupId itemIds currentTs = forM itemIds $ \(chatItemId, ttl) -> do + let deleteAt = addUTCTime (realToFrac ttl) currentTs DB.execute db "UPDATE chat_items SET timed_delete_at = ? WHERE user_id = ? AND group_id = ? AND chat_item_id = ?" (deleteAt, userId, groupId, chatItemId) + pure (chatItemId, deleteAt) + +updateLocalChatItemsRead :: DB.Connection -> User -> NoteFolderId -> IO () +updateLocalChatItemsRead db User {userId} noteFolderId = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND note_folder_id = ? AND item_status = ? + |] + (CISRcvRead, currentTs, userId, noteFolderId, CISRcvNew) type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe ACIFileStatus, Maybe FileProtocol) -type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe Bool) +type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe BoolInt, BoolInt) -type ChatItemRow = (Int64, ChatItemTs, AMsgDirection, Text, Text, ACIStatus, Maybe SharedMsgId) :. (Int, Maybe UTCTime, Maybe Bool, UTCTime, UTCTime) :. ChatItemModeRow :. MaybeCIFIleRow +type ChatItemForwardedFromRow = (Maybe CIForwardedFromTag, Maybe Text, Maybe MsgDirection, Maybe Int64, Maybe Int64, Maybe Int64) -type QuoteRow = (Maybe ChatItemId, Maybe SharedMsgId, Maybe UTCTime, Maybe MsgContent, Maybe Bool) +type ChatItemRow = + (Int64, ChatItemTs, AMsgDirection, Text, Text, ACIStatus, Maybe BoolInt, Maybe SharedMsgId) + :. (Int, Maybe UTCTime, Maybe BoolInt, UTCTime, UTCTime) + :. ChatItemForwardedFromRow + :. ChatItemModeRow + :. MaybeCIFIleRow + +type QuoteRow = (Maybe ChatItemId, Maybe SharedMsgId, Maybe UTCTime, Maybe MsgContent, Maybe BoolInt) toDirectQuote :: QuoteRow -> Maybe (CIQuote 'CTDirect) -toDirectQuote qr@(_, _, _, _, quotedSent) = toQuote qr $ direction <$> quotedSent +toDirectQuote qr@(_, _, _, _, quotedSent) = toQuote qr $ direction . unBI <$> quotedSent where direction sent = if sent then CIQDirectSnd else CIQDirectRcv @@ -1098,7 +1859,7 @@ toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _) dir -- this function can be changed so it never fails, not only avoid failure on invalid json toDirectChatItem :: UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect) -toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) = +toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) = chatItem $ fromRight invalid $ dbParseACIContent itemContentText where invalid = ACIContent msgDir $ CIInvalidJSON itemContentText @@ -1122,31 +1883,40 @@ toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentT _ -> Nothing cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTDirect d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTDirect cItem d chatDir ciStatus content file = - CChatItem d ChatItem {chatDir, meta = ciMeta content ciStatus, content, formattedText = parseMaybeMarkdownList itemText, quotedItem = toDirectQuote quoteRow, reactions = [], file} - badItem = Left $ SEBadChatItem itemId + CChatItem d ChatItem {chatDir, meta = ciMeta content ciStatus, content, mentions = M.empty, formattedText = parseMaybeMarkdownList itemText, quotedItem = toDirectQuote quoteRow, reactions = [], file} + badItem = Left $ SEBadChatItem itemId (Just itemTs) ciMeta :: CIContent d -> CIStatus d -> CIMeta 'CTDirect d ciMeta content status = let itemDeleted' = case itemDeleted of DBCINotDeleted -> Nothing _ -> Just (CIDeleted @'CTDirect deletedTs) - itemEdited' = fromMaybe False itemEdited - in mkCIMeta itemId content itemText status sharedMsgId itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt + itemEdited' = maybe False unBI itemEdited + itemForwarded = toCIForwardedFrom forwardedFromRow + in mkCIMeta itemId content itemText status (unBI <$> sentViaProxy) sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed (unBI <$> itemLive) userMention currentTs itemTs Nothing createdAt updatedAt ciTimed :: Maybe CITimed ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} +toCIForwardedFrom :: ChatItemForwardedFromRow -> Maybe CIForwardedFrom +toCIForwardedFrom (fwdFromTag, fwdFromChatName, fwdFromMsgDir, fwdFromContactId, fwdFromGroupId, fwdFromChatItemId) = + case (fwdFromTag, fwdFromChatName, fwdFromMsgDir, fwdFromContactId, fwdFromGroupId, fwdFromChatItemId) of + (Just CIFFUnknown_, Nothing, Nothing, Nothing, Nothing, Nothing) -> Just CIFFUnknown + (Just CIFFContact_, Just chatName, Just msgDir, contactId, Nothing, ciId) -> Just $ CIFFContact chatName msgDir contactId ciId + (Just CIFFGroup_, Just chatName, Just msgDir, Nothing, groupId, ciId) -> Just $ CIFFGroup chatName msgDir groupId ciId + _ -> Nothing + type GroupQuoteRow = QuoteRow :. MaybeGroupMemberRow toGroupQuote :: QuoteRow -> Maybe GroupMember -> Maybe (CIQuote 'CTGroup) toGroupQuote qr@(_, _, _, _, quotedSent) quotedMember_ = toQuote qr $ direction quotedSent quotedMember_ where - direction (Just True) _ = Just CIQGroupSnd - direction (Just False) (Just member) = Just . CIQGroupRcv $ Just member - direction (Just False) Nothing = Just $ CIQGroupRcv Nothing + direction (Just (BI True)) _ = Just CIQGroupSnd + direction (Just (BI False)) (Just member) = Just . CIQGroupRcv $ Just member + direction (Just (BI False)) Nothing = Just $ CIQGroupRcv Nothing direction _ _ = Nothing -- this function can be changed so it never fails, not only avoid failure on invalid json toGroupChatItem :: UTCTime -> Int64 -> ChatItemRow :. Only (Maybe GroupMemberId) :. MaybeGroupMemberRow :. GroupQuoteRow :. MaybeGroupMemberRow -> Either StoreError (CChatItem 'CTGroup) -toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. Only forwardedByMember :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do +toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. Only forwardedByMember :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do chatItem $ fromRight invalid $ dbParseACIContent itemContentText where member_ = toMaybeGroupMember userContactId memberRow_ @@ -1173,27 +1943,35 @@ toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, _ -> Nothing cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTGroup d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTGroup cItem d chatDir ciStatus content file = - CChatItem d ChatItem {chatDir, meta = ciMeta content ciStatus, content, formattedText = parseMaybeMarkdownList itemText, quotedItem = toGroupQuote quoteRow quotedMember_, reactions = [], file} - badItem = Left $ SEBadChatItem itemId + CChatItem d ChatItem {chatDir, meta = ciMeta content ciStatus, content, mentions = M.empty, formattedText = parseMaybeMarkdownList itemText, quotedItem = toGroupQuote quoteRow quotedMember_, reactions = [], file} + badItem = Left $ SEBadChatItem itemId (Just itemTs) ciMeta :: CIContent d -> CIStatus d -> CIMeta 'CTGroup d ciMeta content status = let itemDeleted' = case itemDeleted of DBCINotDeleted -> Nothing DBCIBlocked -> Just (CIBlocked deletedTs) + DBCIBlockedByAdmin -> Just (CIBlockedByAdmin deletedTs) _ -> Just (maybe (CIDeleted @'CTGroup deletedTs) (CIModerated deletedTs) deletedByGroupMember_) - itemEdited' = fromMaybe False itemEdited - in mkCIMeta itemId content itemText status sharedMsgId itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt + itemEdited' = maybe False unBI itemEdited + itemForwarded = toCIForwardedFrom forwardedFromRow + in mkCIMeta itemId content itemText status (unBI <$> sentViaProxy) sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed (unBI <$> itemLive) userMention currentTs itemTs forwardedByMember createdAt updatedAt ciTimed :: Maybe CITimed ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} -getAllChatItems :: DB.Connection -> VersionRange -> User -> ChatPagination -> Maybe String -> ExceptT StoreError IO [AChatItem] +getAllChatItems :: DB.Connection -> VersionRangeChat -> User -> ChatPagination -> Maybe String -> ExceptT StoreError IO [AChatItem] getAllChatItems db vr user@User {userId} pagination search_ = do itemRefs <- rights . map toChatItemRef <$> case pagination of CPLast count -> liftIO $ getAllChatItemsLast_ count CPAfter afterId count -> liftIO . getAllChatItemsAfter_ afterId count . aChatItemTs =<< getAChatItem_ afterId CPBefore beforeId count -> liftIO . getAllChatItemsBefore_ beforeId count . aChatItemTs =<< getAChatItem_ beforeId - mapM (uncurry (getAChatItem db vr user) >=> liftIO . getACIReactions db) itemRefs + CPAround aroundId count -> liftIO . getAllChatItemsAround_ aroundId count . aChatItemTs =<< getAChatItem_ aroundId + CPInitial count -> do + unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search" + liftIO getFirstUnreadItemId_ >>= \case + Just itemId -> liftIO . getAllChatItemsAround_ itemId count . aChatItemTs =<< getAChatItem_ itemId + Nothing -> liftIO $ getAllChatItemsLast_ count + mapM (uncurry (getAChatItem db vr user)) itemRefs where search = fromMaybe "" search_ getAChatItem_ itemId = do @@ -1204,9 +1982,9 @@ getAllChatItems db vr user@User {userId} pagination search_ = do <$> DB.query db [sql| - SELECT chat_item_id, contact_id, group_id + SELECT chat_item_id, contact_id, group_id, note_folder_id FROM chat_items - WHERE user_id = ? AND item_text LIKE '%' || ? || '%' + WHERE user_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? |] @@ -1215,9 +1993,9 @@ getAllChatItems db vr user@User {userId} pagination search_ = do DB.query db [sql| - SELECT chat_item_id, contact_id, group_id + SELECT chat_item_id, contact_id, group_id, note_folder_id FROM chat_items - WHERE user_id = ? AND item_text LIKE '%' || ? || '%' + WHERE user_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?)) ORDER BY item_ts ASC, chat_item_id ASC LIMIT ? @@ -1228,28 +2006,51 @@ getAllChatItems db vr user@User {userId} pagination search_ = do <$> DB.query db [sql| - SELECT chat_item_id, contact_id, group_id + SELECT chat_item_id, contact_id, group_id, note_folder_id FROM chat_items - WHERE user_id = ? AND item_text LIKE '%' || ? || '%' + WHERE user_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' AND (item_ts < ? OR (item_ts = ? AND chat_item_id < ?)) ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? |] (userId, search, beforeTs, beforeTs, beforeId, count) + getChatItem chatId = + DB.query + db + [sql| + SELECT chat_item_id, contact_id, group_id, note_folder_id + FROM chat_items + WHERE chat_item_id = ? + |] + (Only chatId) + getAllChatItemsAround_ aroundId count aroundTs = do + itemsBefore <- getAllChatItemsBefore_ aroundId count aroundTs + item <- getChatItem aroundId + itemsAfter <- getAllChatItemsAfter_ aroundId count aroundTs + pure $ itemsBefore <> item <> itemsAfter + getFirstUnreadItemId_ = + fmap join . maybeFirstRow fromOnly $ + DB.query + db + [sql| + SELECT MIN(chat_item_id) + FROM chat_items + WHERE user_id = ? AND item_status = ? + |] + (userId, CISRcvNew) -getChatItemIdByAgentMsgId :: DB.Connection -> Int64 -> AgentMsgId -> IO (Maybe ChatItemId) -getChatItemIdByAgentMsgId db connId msgId = - fmap join . maybeFirstRow fromOnly $ - DB.query +getChatItemIdsByAgentMsgId :: DB.Connection -> Int64 -> AgentMsgId -> IO [ChatItemId] +getChatItemIdsByAgentMsgId db connId msgId = + map fromOnly + <$> DB.query db [sql| SELECT chat_item_id FROM chat_item_messages - WHERE message_id = ( + WHERE message_id IN ( SELECT message_id FROM msg_deliveries WHERE connection_id = ? AND agent_msg_id = ? - LIMIT 1 ) |] (connId, msgId) @@ -1261,10 +2062,15 @@ updateDirectChatItemStatus db user@User {userId} ct@Contact {contactId} itemId i liftIO $ DB.execute db "UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?" (itemStatus, currentTs, userId, contactId, itemId) pure ci {meta = (meta ci) {itemStatus}} -updateDirectChatItem :: MsgDirectionI d => DB.Connection -> User -> Contact -> ChatItemId -> CIContent d -> Bool -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d) -updateDirectChatItem db user ct@Contact {contactId} itemId newContent live msgId_ = do +setDirectSndChatItemViaProxy :: DB.Connection -> User -> Contact -> ChatItem 'CTDirect 'MDSnd -> Bool -> IO (ChatItem 'CTDirect 'MDSnd) +setDirectSndChatItemViaProxy db User {userId} Contact {contactId} ci viaProxy = do + DB.execute db "UPDATE chat_items SET via_proxy = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?" (BI viaProxy, userId, contactId, chatItemId' ci) + pure ci {meta = (meta ci) {sentViaProxy = Just viaProxy}} + +updateDirectChatItem :: MsgDirectionI d => DB.Connection -> User -> Contact -> ChatItemId -> CIContent d -> Bool -> Bool -> Maybe CITimed -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d) +updateDirectChatItem db user ct@Contact {contactId} itemId newContent edited live timed_ msgId_ = do ci <- liftEither . correctDir =<< getDirectCIWithReactions db user ct itemId - liftIO $ updateDirectChatItem' db user contactId ci newContent live msgId_ + liftIO $ updateDirectChatItem' db user contactId ci newContent edited live timed_ msgId_ getDirectCIWithReactions :: DB.Connection -> User -> Contact -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTDirect) getDirectCIWithReactions db user ct@Contact {contactId} itemId = @@ -1273,25 +2079,27 @@ getDirectCIWithReactions db user ct@Contact {contactId} itemId = correctDir :: MsgDirectionI d => CChatItem c -> Either StoreError (ChatItem c d) correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci -updateDirectChatItem' :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTDirect d -> CIContent d -> Bool -> Maybe MessageId -> IO (ChatItem 'CTDirect d) -updateDirectChatItem' db User {userId} contactId ci newContent live msgId_ = do +updateDirectChatItem' :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTDirect d -> CIContent d -> Bool -> Bool -> Maybe CITimed -> Maybe MessageId -> IO (ChatItem 'CTDirect d) +updateDirectChatItem' db User {userId} contactId ci newContent edited live timed_ msgId_ = do currentTs <- liftIO getCurrentTime - let ci' = updatedChatItem ci newContent live currentTs + let ci' = updatedChatItem ci newContent edited live timed_ currentTs liftIO $ updateDirectChatItem_ db userId contactId ci' msgId_ pure ci' -updatedChatItem :: ChatItem c d -> CIContent d -> Bool -> UTCTime -> ChatItem c d -updatedChatItem ci@ChatItem {meta = meta@CIMeta {itemStatus, itemEdited, itemTimed, itemLive}} newContent live currentTs = +updatedChatItem :: ChatItem c d -> CIContent d -> Bool -> Bool -> Maybe CITimed -> UTCTime -> ChatItem c d +updatedChatItem ci@ChatItem {meta = meta@CIMeta {itemStatus, itemEdited, itemTimed, itemLive}} newContent edited live timed_ currentTs = let newText = ciContentToText newContent - edited' = itemEdited || (itemLive /= Just True) + edited' = itemEdited || edited live' = (live &&) <$> itemLive - timed' = case (itemStatus, itemTimed, itemLive, live) of - (CISRcvNew, _, _, _) -> itemTimed - (_, Just CITimed {ttl, deleteAt = Nothing}, Just True, False) -> - -- timed item, sent or read, not set for deletion, was live, now not live - let deleteAt' = addUTCTime (realToFrac ttl) currentTs - in Just CITimed {ttl, deleteAt = Just deleteAt'} - _ -> itemTimed + timed' = case timed_ of + Just timed -> Just timed + Nothing -> case (itemStatus, itemTimed, itemLive, live) of + (CISRcvNew, _, _, _) -> itemTimed + (_, Just CITimed {ttl, deleteAt = Nothing}, Just True, False) -> + -- timed item, sent or read, not set for deletion, was live, now not live + let deleteAt' = addUTCTime (realToFrac ttl) currentTs + in Just CITimed {ttl, deleteAt = Just deleteAt'} + _ -> itemTimed in ci {content = newContent, meta = meta {itemText = newText, itemEdited = edited', itemTimed = timed', itemLive = live'}, formattedText = parseMaybeMarkdownList newText} -- this function assumes that direct item with correct chat direction already exists, @@ -1308,7 +2116,7 @@ updateDirectChatItem_ db userId contactId ChatItem {meta, content} msgId_ = do SET item_content = ?, item_text = ?, item_status = ?, item_deleted = ?, item_deleted_ts = ?, item_edited = ?, item_live = ?, updated_at = ?, timed_ttl = ?, timed_delete_at = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ? |] - ((content, itemText, itemStatus, itemDeleted', itemDeletedTs', itemEdited, itemLive, updatedAt) :. ciTimedRow itemTimed :. (userId, contactId, itemId)) + ((content, itemText, itemStatus, BI itemDeleted', itemDeletedTs', BI itemEdited, BI <$> itemLive, updatedAt) :. ciTimedRow itemTimed :. (userId, contactId, itemId)) forM_ msgId_ $ \msgId -> liftIO $ insertChatItemMessage_ db itemId msgId updatedAt addInitialAndNewCIVersions :: DB.Connection -> ChatItemId -> (UTCTime, MsgContent) -> (UTCTime, MsgContent) -> IO () @@ -1333,7 +2141,7 @@ createChatItemVersion db itemId itemVersionTs msgContent = INSERT INTO chat_item_versions (chat_item_id, msg_content, item_version_ts) VALUES (?,?,?) |] - (itemId, toMCText msgContent, itemVersionTs) + (itemId, MCText $ msgContentText msgContent, itemVersionTs) deleteDirectChatItem :: DB.Connection -> User -> Contact -> ChatItem 'CTDirect d -> IO () deleteDirectChatItem db User {userId} Contact {contactId} ci = do @@ -1350,28 +2158,27 @@ deleteDirectChatItem db User {userId} Contact {contactId} ci = do (userId, contactId, itemId) deleteChatItemMessages_ :: DB.Connection -> ChatItemId -> IO () -deleteChatItemMessages_ db itemId = - DB.execute - db - [sql| - DELETE FROM messages - WHERE message_id IN ( - SELECT message_id - FROM chat_item_messages - WHERE chat_item_id = ? - ) - |] - (Only itemId) +deleteChatItemMessages_ db itemId = DB.execute db deleteChatItemMessagesQuery (Only itemId) + +deleteChatItemMessagesQuery :: Query +deleteChatItemMessagesQuery = + [sql| + DELETE FROM messages + WHERE message_id IN ( + SELECT message_id + FROM chat_item_messages + WHERE chat_item_id = ? + ) + |] deleteChatItemVersions_ :: DB.Connection -> ChatItemId -> IO () deleteChatItemVersions_ db itemId = DB.execute db "DELETE FROM chat_item_versions WHERE chat_item_id = ?" (Only itemId) -markDirectChatItemDeleted :: DB.Connection -> User -> Contact -> ChatItem 'CTDirect d -> MessageId -> UTCTime -> IO (ChatItem 'CTDirect d) -markDirectChatItemDeleted db User {userId} Contact {contactId} ci@ChatItem {meta} msgId deletedTs = do +markDirectChatItemDeleted :: DB.Connection -> User -> Contact -> ChatItem 'CTDirect d -> UTCTime -> IO (ChatItem 'CTDirect d) +markDirectChatItemDeleted db User {userId} Contact {contactId} ci@ChatItem {meta} deletedTs = do currentTs <- liftIO getCurrentTime let itemId = chatItemId' ci - insertChatItemMessage_ db itemId msgId currentTs DB.execute db [sql| @@ -1380,17 +2187,17 @@ markDirectChatItemDeleted db User {userId} Contact {contactId} ci@ChatItem {meta WHERE user_id = ? AND contact_id = ? AND chat_item_id = ? |] (DBCIDeleted, deletedTs, currentTs, userId, contactId, itemId) - pure ci {meta = meta {itemDeleted = Just $ CIDeleted $ Just deletedTs}} + pure ci {meta = meta {itemDeleted = Just $ CIDeleted $ Just deletedTs, editable = False, deletable = False}} getDirectChatItemBySharedMsgId :: DB.Connection -> User -> ContactId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTDirect) getDirectChatItemBySharedMsgId db user@User {userId} contactId sharedMsgId = do itemId <- getDirectChatItemIdBySharedMsgId_ db userId contactId sharedMsgId getDirectChatItem db user contactId itemId -getDirectChatItemByAgentMsgId :: DB.Connection -> User -> ContactId -> Int64 -> AgentMsgId -> IO (Maybe (CChatItem 'CTDirect)) -getDirectChatItemByAgentMsgId db user contactId connId msgId = do - itemId_ <- getChatItemIdByAgentMsgId db connId msgId - maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getDirectChatItem db user contactId) itemId_ +getDirectChatItemsByAgentMsgId :: DB.Connection -> User -> ContactId -> Int64 -> AgentMsgId -> IO [CChatItem 'CTDirect] +getDirectChatItemsByAgentMsgId db user contactId connId msgId = do + itemIds <- getChatItemIdsByAgentMsgId db connId msgId + catMaybes <$> mapM (fmap eitherToMaybe . runExceptT . getDirectChatItem db user contactId) itemIds getDirectChatItemIdBySharedMsgId_ :: DB.Connection -> UserId -> Int64 -> SharedMsgId -> ExceptT StoreError IO Int64 getDirectChatItemIdBySharedMsgId_ db userId contactId sharedMsgId = @@ -1417,7 +2224,10 @@ getDirectChatItem db User {userId} contactId itemId = ExceptT $ do [sql| SELECT -- ChatItem - i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, + i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id, + i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, + i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, + i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, -- CIFile f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, -- DirectQuote @@ -1469,17 +2279,19 @@ getGroupCIWithReactions db user g@GroupInfo {groupId} itemId = do liftIO . groupCIWithReactions db g =<< getGroupChatItem db user groupId itemId groupCIWithReactions :: DB.Connection -> GroupInfo -> CChatItem 'CTGroup -> IO (CChatItem 'CTGroup) -groupCIWithReactions db g cci@(CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId}}) = case itemSharedMsgId of - Just sharedMsgId -> do - let GroupMember {memberId} = chatItemMember g ci - reactions <- getGroupCIReactions db g memberId sharedMsgId - pure $ CChatItem md ci {reactions} - Nothing -> pure cci +groupCIWithReactions db g cci@(CChatItem md ci@ChatItem {meta = CIMeta {itemId, itemSharedMsgId}}) = do + mentions <- getGroupCIMentions db itemId + case itemSharedMsgId of + Just sharedMsgId -> do + let GroupMember {memberId} = chatItemMember g ci + reactions <- getGroupCIReactions db g memberId sharedMsgId + pure $ CChatItem md ci {reactions, mentions} + Nothing -> pure $ if null mentions then cci else CChatItem md ci {mentions} -updateGroupChatItem :: MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTGroup d -> CIContent d -> Bool -> Maybe MessageId -> IO (ChatItem 'CTGroup d) -updateGroupChatItem db user groupId ci newContent live msgId_ = do +updateGroupChatItem :: MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTGroup d -> CIContent d -> Bool -> Bool -> Maybe MessageId -> IO (ChatItem 'CTGroup d) +updateGroupChatItem db user groupId ci newContent edited live msgId_ = do currentTs <- liftIO getCurrentTime - let ci' = updatedChatItem ci newContent live currentTs + let ci' = updatedChatItem ci newContent edited live Nothing currentTs liftIO $ updateGroupChatItem_ db user groupId ci' msgId_ pure ci' @@ -1497,9 +2309,31 @@ updateGroupChatItem_ db User {userId} groupId ChatItem {content, meta} msgId_ = SET item_content = ?, item_text = ?, item_status = ?, item_deleted = ?, item_deleted_ts = ?, item_edited = ?, item_live = ?, updated_at = ?, timed_ttl = ?, timed_delete_at = ? WHERE user_id = ? AND group_id = ? AND chat_item_id = ? |] - ((content, itemText, itemStatus, itemDeleted', itemDeletedTs', itemEdited, itemLive, updatedAt) :. ciTimedRow itemTimed :. (userId, groupId, itemId)) + ((content, itemText, itemStatus, BI itemDeleted', itemDeletedTs', BI itemEdited, BI <$> itemLive, updatedAt) :. ciTimedRow itemTimed :. (userId, groupId, itemId)) forM_ msgId_ $ \msgId -> insertChatItemMessage_ db itemId msgId updatedAt +createGroupCIMentions :: forall d. DB.Connection -> GroupInfo -> ChatItem 'CTGroup d -> Map MemberName CIMention -> IO (ChatItem 'CTGroup d) +createGroupCIMentions db GroupInfo {groupId} ci mentions = do + DB.executeMany db "INSERT INTO chat_item_mentions (chat_item_id, group_id, member_id, display_name) VALUES (?, ?, ?, ?)" rows + pure (ci :: ChatItem 'CTGroup d) {mentions} + where + rows = map (\(name, CIMention {memberId}) -> (ciId, groupId, memberId, name)) $ M.assocs mentions + ciId = chatItemId' ci + +updateGroupCIMentions :: DB.Connection -> GroupInfo -> ChatItem 'CTGroup d -> Map MemberName CIMention -> IO (ChatItem 'CTGroup d) +updateGroupCIMentions db g ci@ChatItem {mentions} mentions' + | mentions' == mentions = pure ci + | otherwise = do + unless (null mentions) $ deleteMentions + if null mentions' + then pure ci + else -- This is a fallback for the error that should not happen in practice. + -- In theory, it may happen in item mentions in database are different from item record. + createMentions `E.catch` \e -> if constraintError e then deleteMentions >> createMentions else E.throwIO e + where + deleteMentions = DB.execute db "DELETE FROM chat_item_mentions WHERE chat_item_id = ?" (Only $ chatItemId' ci) + createMentions = createGroupCIMentions db g ci mentions' + deleteGroupChatItem :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> IO () deleteGroupChatItem db User {userId} g@GroupInfo {groupId} ci = do let itemId = chatItemId' ci @@ -1531,7 +2365,58 @@ updateGroupChatItemModerated db User {userId} GroupInfo {groupId} ci m@GroupMemb WHERE user_id = ? AND group_id = ? AND chat_item_id = ? |] (deletedTs, groupMemberId, toContent, toText, currentTs, userId, groupId, itemId) - pure $ ci {content = toContent, meta = (meta ci) {itemText = toText, itemDeleted = Just (CIModerated (Just currentTs) m), editable = False}, formattedText = Nothing} + pure ci {content = toContent, meta = (meta ci) {itemText = toText, itemDeleted = Just (CIModerated (Just deletedTs) m), editable = False, deletable = False}, formattedText = Nothing} + +updateMemberCIsModerated :: MsgDirectionI d => DB.Connection -> User -> GroupInfo -> GroupMember -> GroupMember -> SMsgDirection d -> UTCTime -> IO () +updateMemberCIsModerated db User {userId} GroupInfo {groupId, membership} member byGroupMember md deletedTs = do + itemIds <- updateCIs =<< getCurrentTime +#if defined(dbPostgres) + let inItemIds = Only $ In (map fromOnly itemIds) + DB.execute db "DELETE FROM messages WHERE message_id IN (SELECT message_id FROM chat_item_messages WHERE chat_item_id IN ?)" inItemIds + DB.execute db "DELETE FROM chat_item_versions WHERE chat_item_id IN ?" inItemIds +#else + DB.executeMany db deleteChatItemMessagesQuery itemIds + DB.executeMany db "DELETE FROM chat_item_versions WHERE chat_item_id = ?" itemIds +#endif + where + memId = groupMemberId' member + updateQuery = + [sql| + UPDATE chat_items + SET item_deleted = 1, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, item_content = ?, item_text = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? + |] + updateCIs :: UTCTime -> IO [Only Int64] + updateCIs currentTs + | memId == groupMemberId' membership = + DB.query + db + (updateQuery <> " AND group_member_id IS NULL AND item_sent = 1 RETURNING chat_item_id") + (columns :. (userId, groupId)) + | otherwise = + DB.query + db + (updateQuery <> " AND group_member_id = ? RETURNING chat_item_id") + (columns :. (userId, groupId, memId)) + where + columns = (deletedTs, groupMemberId' byGroupMember, msgDirToModeratedContent_ md, ciModeratedText, currentTs) + +updateGroupCIBlockedByAdmin :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> UTCTime -> IO (ChatItem 'CTGroup d) +updateGroupCIBlockedByAdmin db User {userId} GroupInfo {groupId} ci deletedTs = do + currentTs <- getCurrentTime + let itemId = chatItemId' ci + deleteChatItemMessages_ db itemId + deleteChatItemVersions_ db itemId + liftIO $ + DB.execute + db + [sql| + UPDATE chat_items + SET item_deleted = ?, item_deleted_ts = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND chat_item_id = ? + |] + (DBCIBlockedByAdmin, deletedTs, currentTs, userId, groupId, itemId) + pure $ ci {meta = (meta ci) {itemDeleted = Just (CIBlockedByAdmin $ Just deletedTs), editable = False, deletable = False}, formattedText = Nothing} pattern DBCINotDeleted :: Int pattern DBCINotDeleted = 0 @@ -1542,14 +2427,16 @@ pattern DBCIDeleted = 1 pattern DBCIBlocked :: Int pattern DBCIBlocked = 2 -markGroupChatItemDeleted :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> MessageId -> Maybe GroupMember -> UTCTime -> IO (ChatItem 'CTGroup d) -markGroupChatItemDeleted db User {userId} GroupInfo {groupId} ci@ChatItem {meta} msgId byGroupMember_ deletedTs = do +pattern DBCIBlockedByAdmin :: Int +pattern DBCIBlockedByAdmin = 3 + +markGroupChatItemDeleted :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> Maybe GroupMember -> UTCTime -> IO (ChatItem 'CTGroup d) +markGroupChatItemDeleted db User {userId} GroupInfo {groupId} ci@ChatItem {meta} byGroupMember_ deletedTs = do currentTs <- liftIO getCurrentTime let itemId = chatItemId' ci (deletedByGroupMemberId, itemDeleted) = case byGroupMember_ of Just m@GroupMember {groupMemberId} -> (Just groupMemberId, Just $ CIModerated (Just deletedTs) m) _ -> (Nothing, Just $ CIDeleted @'CTGroup (Just deletedTs)) - insertChatItemMessage_ db itemId msgId currentTs DB.execute db [sql| @@ -1558,7 +2445,32 @@ markGroupChatItemDeleted db User {userId} GroupInfo {groupId} ci@ChatItem {meta} WHERE user_id = ? AND group_id = ? AND chat_item_id = ? |] (DBCIDeleted, deletedTs, deletedByGroupMemberId, currentTs, userId, groupId, itemId) - pure ci {meta = meta {itemDeleted}} + pure ci {meta = meta {itemDeleted, editable = False, deletable = False}} + +markMemberCIsDeleted :: DB.Connection -> User -> GroupInfo -> GroupMember -> GroupMember -> UTCTime -> IO () +markMemberCIsDeleted db User {userId} GroupInfo {groupId, membership} member byGroupMember deletedTs = + updateCIs =<< getCurrentTime + where + memId = groupMemberId' member + updateQuery = + [sql| + UPDATE chat_items + SET item_deleted = ?, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? + |] + updateCIs currentTs + | memId == groupMemberId' membership = + DB.execute + db + (updateQuery <> " AND group_member_id IS NULL AND item_sent = 1") + (columns :. (userId, groupId)) + | otherwise = + DB.execute + db + (updateQuery <> " AND group_member_id = ?") + (columns :. (userId, groupId, memId)) + where + columns = (DBCIDeleted, deletedTs, groupMemberId' byGroupMember, currentTs) markGroupChatItemBlocked :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup 'MDRcv -> IO (ChatItem 'CTGroup 'MDRcv) markGroupChatItemBlocked db User {userId} GroupInfo {groupId} ci@ChatItem {meta} = do @@ -1571,10 +2483,51 @@ markGroupChatItemBlocked db User {userId} GroupInfo {groupId} ci@ChatItem {meta} WHERE user_id = ? AND group_id = ? AND chat_item_id = ? |] (DBCIBlocked, deletedTs, deletedTs, userId, groupId, chatItemId' ci) - pure ci {meta = meta {itemDeleted = Just $ CIBlocked $ Just deletedTs}} + pure ci {meta = meta {itemDeleted = Just $ CIBlocked $ Just deletedTs, editable = False, deletable = False}} -getGroupChatItemBySharedMsgId :: DB.Connection -> User -> GroupId -> GroupMemberId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTGroup) -getGroupChatItemBySharedMsgId db user@User {userId} groupId groupMemberId sharedMsgId = do +markGroupCIBlockedByAdmin :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup 'MDRcv -> IO (ChatItem 'CTGroup 'MDRcv) +markGroupCIBlockedByAdmin db User {userId} GroupInfo {groupId} ci@ChatItem {meta} = do + deletedTs <- getCurrentTime + DB.execute + db + [sql| + UPDATE chat_items + SET item_deleted = ?, item_deleted_ts = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND chat_item_id = ? + |] + (DBCIBlockedByAdmin, deletedTs, deletedTs, userId, groupId, chatItemId' ci) + pure ci {meta = meta {itemDeleted = Just $ CIBlockedByAdmin $ Just deletedTs, editable = False, deletable = False}} + +markMessageReportsDeleted :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> GroupMember -> UTCTime -> IO [ChatItemId] +markMessageReportsDeleted db User {userId} GroupInfo {groupId} ChatItem {meta = CIMeta {itemSharedMsgId}} GroupMember {groupMemberId} deletedTs = do + currentTs <- liftIO getCurrentTime + map fromOnly + <$> DB.query + db + [sql| + UPDATE chat_items + SET item_deleted = ?, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND msg_content_tag = ? AND quoted_shared_msg_id = ? AND item_deleted = ? + RETURNING chat_item_id; + |] + (DBCIDeleted, deletedTs, groupMemberId, currentTs, userId, groupId, MCReport_, itemSharedMsgId, DBCINotDeleted) + +markReceivedGroupReportsDeleted :: DB.Connection -> User -> GroupInfo -> UTCTime -> IO [ChatItemId] +markReceivedGroupReportsDeleted db User {userId} GroupInfo {groupId, membership} deletedTs = do + currentTs <- liftIO getCurrentTime + map fromOnly + <$> DB.query + db + [sql| + UPDATE chat_items + SET item_deleted = ?, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 + RETURNING chat_item_id + |] + (DBCIDeleted, deletedTs, groupMemberId' membership, currentTs, userId, groupId, MCReport_, DBCINotDeleted) + +getGroupChatItemBySharedMsgId :: DB.Connection -> User -> GroupInfo -> GroupMemberId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTGroup) +getGroupChatItemBySharedMsgId db user@User {userId} g@GroupInfo {groupId} groupMemberId sharedMsgId = do itemId <- ExceptT . firstRow fromOnly (SEChatItemSharedMsgIdNotFound sharedMsgId) $ DB.query @@ -1587,10 +2540,10 @@ getGroupChatItemBySharedMsgId db user@User {userId} groupId groupMemberId shared LIMIT 1 |] (userId, groupId, groupMemberId, sharedMsgId) - getGroupChatItem db user groupId itemId + getGroupCIWithReactions db user g itemId -getGroupMemberCIBySharedMsgId :: DB.Connection -> User -> GroupId -> MemberId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTGroup) -getGroupMemberCIBySharedMsgId db user@User {userId} groupId memberId sharedMsgId = do +getGroupMemberCIBySharedMsgId :: DB.Connection -> User -> GroupInfo -> MemberId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTGroup) +getGroupMemberCIBySharedMsgId db user@User {userId} g@GroupInfo {groupId} memberId sharedMsgId = do itemId <- ExceptT . firstRow fromOnly (SEChatItemSharedMsgIdNotFound sharedMsgId) $ DB.query @@ -1606,12 +2559,12 @@ getGroupMemberCIBySharedMsgId db user@User {userId} groupId memberId sharedMsgId LIMIT 1 |] (GCUserMember, userId, groupId, memberId, sharedMsgId) - getGroupChatItem db user groupId itemId + getGroupCIWithReactions db user g itemId -getGroupChatItemByAgentMsgId :: DB.Connection -> User -> GroupId -> Int64 -> AgentMsgId -> IO (Maybe (CChatItem 'CTGroup)) -getGroupChatItemByAgentMsgId db user groupId connId msgId = do - itemId_ <- getChatItemIdByAgentMsgId db connId msgId - maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupChatItem db user groupId) itemId_ +getGroupChatItemsByAgentMsgId :: DB.Connection -> User -> GroupId -> Int64 -> AgentMsgId -> IO [CChatItem 'CTGroup] +getGroupChatItemsByAgentMsgId db user groupId connId msgId = do + itemIds <- getChatItemIdsByAgentMsgId db connId msgId + catMaybes <$> mapM (fmap eitherToMaybe . runExceptT . getGroupChatItem db user groupId) itemIds getGroupChatItem :: DB.Connection -> User -> Int64 -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTGroup) getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do @@ -1624,25 +2577,31 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do [sql| SELECT -- ChatItem - i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, + i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id, + i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, + i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, + i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, -- CIFile f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, -- CIMeta forwardedByMember i.forwarded_by_group_member_id, -- GroupMember m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, - m.member_status, m.show_messages, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, + m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, -- quoted ChatItem ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, -- quoted GroupMember rm.group_member_id, rm.group_id, rm.member_id, rm.peer_chat_min_version, rm.peer_chat_max_version, rm.member_role, rm.member_category, - rm.member_status, rm.show_messages, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, + rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, rp.display_name, rp.full_name, rp.image, rp.contact_link, rp.local_alias, rp.preferences, + rm.created_at, rm.updated_at, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category, - dbm.member_status, dbm.show_messages, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, - dbp.display_name, dbp.full_name, dbp.image, dbp.contact_link, dbp.local_alias, dbp.preferences + dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, + dbp.display_name, dbp.full_name, dbp.image, dbp.contact_link, dbp.local_alias, dbp.preferences, + dbm.created_at, dbm.updated_at FROM chat_items i LEFT JOIN files f ON f.chat_item_id = i.chat_item_id LEFT JOIN group_members m ON m.group_member_id = i.group_member_id @@ -1693,8 +2652,7 @@ getGroupChatItemIdByText db User {userId, localDisplayName = userName} groupId c SELECT i.chat_item_id FROM chat_items i JOIN group_members m ON m.group_member_id = i.group_member_id - JOIN contacts c ON c.contact_id = m.contact_id - WHERE i.user_id = ? AND i.group_id = ? AND c.local_display_name = ? AND i.item_text like ? + WHERE i.user_id = ? AND i.group_id = ? AND m.local_display_name = ? AND i.item_text like ? ORDER BY i.chat_item_id DESC LIMIT 1 |] @@ -1714,14 +2672,100 @@ getGroupChatItemIdByText' db User {userId} groupId msg = |] (userId, groupId, msg <> "%") -getChatItemByFileId :: DB.Connection -> VersionRange -> User -> Int64 -> ExceptT StoreError IO AChatItem +getLocalChatItem :: DB.Connection -> User -> Int64 -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTLocal) +getLocalChatItem db User {userId} folderId itemId = ExceptT $ do + currentTs <- getCurrentTime + firstRow' (toLocalChatItem currentTs) (SEChatItemNotFound itemId) getItem + where + getItem = + DB.query + db + [sql| + SELECT + -- ChatItem + i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id, + i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, + i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, + i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, + -- CIFile + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol + FROM chat_items i + LEFT JOIN files f ON f.chat_item_id = i.chat_item_id + WHERE i.user_id = ? AND i.note_folder_id = ? AND i.chat_item_id = ? + |] + (userId, folderId, itemId) + +getLocalChatItemIdByText :: DB.Connection -> User -> NoteFolderId -> SMsgDirection d -> Text -> ExceptT StoreError IO ChatItemId +getLocalChatItemIdByText db User {userId} noteFolderId msgDir quotedMsg = + ExceptT . firstRow fromOnly (SEChatItemNotFoundByText quotedMsg) $ + DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND item_sent = ? AND item_text LIKE ? + ORDER BY chat_item_id DESC + LIMIT 1 + |] + (userId, noteFolderId, msgDir, quotedMsg <> "%") + +getLocalChatItemIdByText' :: DB.Connection -> User -> NoteFolderId -> Text -> ExceptT StoreError IO ChatItemId +getLocalChatItemIdByText' db User {userId} noteFolderId msg = + ExceptT . firstRow fromOnly (SEChatItemNotFoundByText msg) $ + DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE ? + ORDER BY chat_item_id DESC + LIMIT 1 + |] + (userId, noteFolderId, msg <> "%") + +updateLocalChatItem' :: forall d. MsgDirectionI d => DB.Connection -> User -> NoteFolderId -> ChatItem 'CTLocal d -> CIContent d -> Bool -> IO (ChatItem 'CTLocal d) +updateLocalChatItem' db User {userId} noteFolderId ci newContent edited = do + currentTs <- liftIO getCurrentTime + let ci' = updatedChatItem ci newContent edited False Nothing currentTs + liftIO $ updateLocalChatItem_ db userId noteFolderId ci' + pure ci' + +-- this function assumes that local item with correct chat direction already exists, +-- it should be checked before calling it +updateLocalChatItem_ :: forall d. MsgDirectionI d => DB.Connection -> UserId -> NoteFolderId -> ChatItem 'CTLocal d -> IO () +updateLocalChatItem_ db userId noteFolderId ChatItem {meta, content} = do + let CIMeta {itemId, itemText, itemStatus, itemDeleted, itemEdited, updatedAt} = meta + itemDeleted' = isJust itemDeleted + itemDeletedTs' = itemDeletedTs =<< itemDeleted + DB.execute + db + [sql| + UPDATE chat_items + SET item_content = ?, item_text = ?, item_status = ?, item_deleted = ?, item_deleted_ts = ?, item_edited = ?, updated_at = ? + WHERE user_id = ? AND note_folder_id = ? AND chat_item_id = ? + |] + ((content, itemText, itemStatus, BI itemDeleted', itemDeletedTs', BI itemEdited, updatedAt) :. (userId, noteFolderId, itemId)) + +deleteLocalChatItem :: DB.Connection -> User -> NoteFolder -> ChatItem 'CTLocal d -> IO () +deleteLocalChatItem db User {userId} NoteFolder {noteFolderId} ci = do + let itemId = chatItemId' ci + deleteChatItemVersions_ db itemId + DB.execute + db + [sql| + DELETE FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND chat_item_id = ? + |] + (userId, noteFolderId, itemId) + +getChatItemByFileId :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO AChatItem getChatItemByFileId db vr user@User {userId} fileId = do (chatRef, itemId) <- ExceptT . firstRow' toChatItemRef (SEChatItemNotFoundByFileId fileId) $ DB.query db [sql| - SELECT i.chat_item_id, i.contact_id, i.group_id + SELECT i.chat_item_id, i.contact_id, i.group_id, i.note_folder_id FROM chat_items i JOIN files f ON f.chat_item_id = i.chat_item_id WHERE f.user_id = ? AND f.file_id = ? @@ -1730,14 +2774,20 @@ getChatItemByFileId db vr user@User {userId} fileId = do (userId, fileId) getAChatItem db vr user chatRef itemId -getChatItemByGroupId :: DB.Connection -> VersionRange -> User -> GroupId -> ExceptT StoreError IO AChatItem +lookupChatItemByFileId :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO (Maybe AChatItem) +lookupChatItemByFileId db vr user fileId = do + fmap Just (getChatItemByFileId db vr user fileId) `catchError` \case + SEChatItemNotFoundByFileId {} -> pure Nothing + e -> throwError e + +getChatItemByGroupId :: DB.Connection -> VersionRangeChat -> User -> GroupId -> ExceptT StoreError IO AChatItem getChatItemByGroupId db vr user@User {userId} groupId = do (chatRef, itemId) <- ExceptT . firstRow' toChatItemRef (SEChatItemNotFoundByGroupId groupId) $ DB.query db [sql| - SELECT i.chat_item_id, i.contact_id, i.group_id + SELECT i.chat_item_id, i.contact_id, i.group_id, i.note_folder_id FROM chat_items i JOIN groups g ON g.chat_item_id = i.chat_item_id WHERE g.user_id = ? AND g.group_id = ? @@ -1754,19 +2804,34 @@ getChatRefViaItemId db User {userId} itemId = do toChatRef = \case (Just contactId, Nothing) -> Right $ ChatRef CTDirect contactId (Nothing, Just groupId) -> Right $ ChatRef CTGroup groupId - (_, _) -> Left $ SEBadChatItem itemId + (_, _) -> Left $ SEBadChatItem itemId Nothing -getAChatItem :: DB.Connection -> VersionRange -> User -> ChatRef -> ChatItemId -> ExceptT StoreError IO AChatItem -getAChatItem db vr user chatRef itemId = case chatRef of - ChatRef CTDirect contactId -> do - ct <- getContact db user contactId - (CChatItem msgDir ci) <- getDirectChatItem db user contactId itemId +getAChatItem :: DB.Connection -> VersionRangeChat -> User -> ChatRef -> ChatItemId -> ExceptT StoreError IO AChatItem +getAChatItem db vr user chatRef itemId = do + aci <- case chatRef of + ChatRef CTDirect contactId -> do + ct <- getContact db vr user contactId + (CChatItem msgDir ci) <- getDirectChatItem db user contactId itemId + pure $ AChatItem SCTDirect msgDir (DirectChat ct) ci + ChatRef CTGroup groupId -> do + gInfo <- getGroupInfo db vr user groupId + (CChatItem msgDir ci) <- getGroupChatItem db user groupId itemId + pure $ AChatItem SCTGroup msgDir (GroupChat gInfo) ci + ChatRef CTLocal folderId -> do + nf <- getNoteFolder db user folderId + CChatItem msgDir ci <- getLocalChatItem db user folderId itemId + pure $ AChatItem SCTLocal msgDir (LocalChat nf) ci + _ -> throwError $ SEChatItemNotFound itemId + liftIO $ getACIReactions db aci + +getAChatItemBySharedMsgId :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> SharedMsgId -> ExceptT StoreError IO AChatItem +getAChatItemBySharedMsgId db user cd sharedMsgId = case cd of + CDDirectRcv ct@Contact {contactId} -> do + (CChatItem msgDir ci) <- getDirectChatItemBySharedMsgId db user contactId sharedMsgId pure $ AChatItem SCTDirect msgDir (DirectChat ct) ci - ChatRef CTGroup groupId -> do - gInfo <- getGroupInfo db vr user groupId - (CChatItem msgDir ci) <- getGroupChatItem db user groupId itemId - pure $ AChatItem SCTGroup msgDir (GroupChat gInfo) ci - _ -> throwError $ SEChatItemNotFound itemId + CDGroupRcv g GroupMember {groupMemberId} -> do + (CChatItem msgDir ci) <- getGroupChatItemBySharedMsgId db user g groupMemberId sharedMsgId + pure $ AChatItem SCTGroup msgDir (GroupChat g) ci getChatItemVersions :: DB.Connection -> ChatItemId -> IO [ChatItemVersion] getChatItemVersions db itemId = do @@ -1786,11 +2851,6 @@ getChatItemVersions db itemId = do let formattedText = parseMaybeMarkdownList $ msgContentText msgContent in ChatItemVersion {chatItemVersionId, msgContent, formattedText, itemVersionTs, createdAt} -getDirectChatReactions_ :: DB.Connection -> Contact -> Chat 'CTDirect -> IO (Chat 'CTDirect) -getDirectChatReactions_ db ct c@Chat {chatItems} = do - chatItems' <- mapM (directCIWithReactions db ct) chatItems - pure c {chatItems = chatItems'} - directCIWithReactions :: DB.Connection -> Contact -> CChatItem 'CTDirect -> IO (CChatItem 'CTDirect) directCIWithReactions db ct cci@(CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId}}) = case itemSharedMsgId of Just sharedMsgId -> do @@ -1824,6 +2884,28 @@ getGroupCIReactions db GroupInfo {groupId} itemMemberId itemSharedMsgId = |] (groupId, itemMemberId, itemSharedMsgId) +getGroupCIMentions :: DB.Connection -> ChatItemId -> IO (Map MemberName CIMention) +getGroupCIMentions db ciId = + M.fromList . map mentionedMember + <$> DB.query + db + [sql| + SELECT r.display_name, r.member_id, m.group_member_id, m.member_role, p.display_name, p.local_alias + FROM chat_item_mentions r + LEFT JOIN group_members m ON r.group_id = m.group_id AND r.member_id = m.member_id + LEFT JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + WHERE r.chat_item_id = ? + |] + (Only ciId) + where + mentionedMember :: (ContactName, MemberId, Maybe GroupMemberId, Maybe GroupMemberRole, Maybe Text, Maybe Text) -> (ContactName, CIMention) + mentionedMember (name, memberId, gmId_, mRole_, displayName_, localAlias) = + let memberRef = case (gmId_, mRole_, displayName_) of + (Just groupMemberId, Just memberRole, Just displayName) -> + Just CIMentionMember {groupMemberId, displayName, localAlias, memberRole} + _ -> Nothing + in (name, CIMention {memberId, memberRef}) + getACIReactions :: DB.Connection -> AChatItem -> IO AChatItem getACIReactions db aci@(AChatItem _ md chat ci@ChatItem {meta = CIMeta {itemSharedMsgId}}) = case itemSharedMsgId of Just itemSharedMId -> case chat of @@ -1851,8 +2933,8 @@ deleteGroupCIReactions_ db g@GroupInfo {groupId} ci@ChatItem {meta = CIMeta {ite "DELETE FROM chat_item_reactions WHERE group_id = ? AND shared_msg_id = ? AND item_member_id = ?" (groupId, itemSharedMId, memberId) -toCIReaction :: (MsgReaction, Bool, Int) -> CIReactionCount -toCIReaction (reaction, userReacted, totalReacted) = CIReactionCount {reaction, userReacted, totalReacted} +toCIReaction :: (MsgReaction, BoolInt, Int) -> CIReactionCount +toCIReaction (reaction, BI userReacted, totalReacted) = CIReactionCount {reaction, userReacted, totalReacted} getDirectReactions :: DB.Connection -> Contact -> SharedMsgId -> Bool -> IO [MsgReaction] getDirectReactions db ct itemSharedMId sent = @@ -1864,7 +2946,7 @@ getDirectReactions db ct itemSharedMId sent = FROM chat_item_reactions WHERE contact_id = ? AND shared_msg_id = ? AND reaction_sent = ? |] - (contactId' ct, itemSharedMId, sent) + (contactId' ct, itemSharedMId, BI sent) setDirectReaction :: DB.Connection -> Contact -> SharedMsgId -> Bool -> MsgReaction -> Bool -> MessageId -> UTCTime -> IO () setDirectReaction db ct itemSharedMId sent reaction add msgId reactionTs @@ -1876,7 +2958,7 @@ setDirectReaction db ct itemSharedMId sent reaction add msgId reactionTs (contact_id, shared_msg_id, reaction_sent, reaction, created_by_msg_id, reaction_ts) VALUES (?,?,?,?,?,?) |] - (contactId' ct, itemSharedMId, sent, reaction, msgId, reactionTs) + (contactId' ct, itemSharedMId, BI sent, reaction, msgId, reactionTs) | otherwise = DB.execute db @@ -1884,7 +2966,7 @@ setDirectReaction db ct itemSharedMId sent reaction add msgId reactionTs DELETE FROM chat_item_reactions WHERE contact_id = ? AND shared_msg_id = ? AND reaction_sent = ? AND reaction = ? |] - (contactId' ct, itemSharedMId, sent, reaction) + (contactId' ct, itemSharedMId, BI sent, reaction) getGroupReactions :: DB.Connection -> GroupInfo -> GroupMember -> MemberId -> SharedMsgId -> Bool -> IO [MsgReaction] getGroupReactions db GroupInfo {groupId} m itemMemberId itemSharedMId sent = @@ -1896,7 +2978,7 @@ getGroupReactions db GroupInfo {groupId} m itemMemberId itemSharedMId sent = FROM chat_item_reactions WHERE group_id = ? AND group_member_id = ? AND item_member_id = ? AND shared_msg_id = ? AND reaction_sent = ? |] - (groupId, groupMemberId' m, itemMemberId, itemSharedMId, sent) + (groupId, groupMemberId' m, itemMemberId, itemSharedMId, BI sent) setGroupReaction :: DB.Connection -> GroupInfo -> GroupMember -> MemberId -> SharedMsgId -> Bool -> MsgReaction -> Bool -> MessageId -> UTCTime -> IO () setGroupReaction db GroupInfo {groupId} m itemMemberId itemSharedMId sent reaction add msgId reactionTs @@ -1908,7 +2990,7 @@ setGroupReaction db GroupInfo {groupId} m itemMemberId itemSharedMId sent reacti (group_id, group_member_id, item_member_id, shared_msg_id, reaction_sent, reaction, created_by_msg_id, reaction_ts) VALUES (?,?,?,?,?,?,?,?) |] - (groupId, groupMemberId' m, itemMemberId, itemSharedMId, sent, reaction, msgId, reactionTs) + (groupId, groupMemberId' m, itemMemberId, itemSharedMId, BI sent, reaction, msgId, reactionTs) | otherwise = DB.execute db @@ -1916,7 +2998,25 @@ setGroupReaction db GroupInfo {groupId} m itemMemberId itemSharedMId sent reacti DELETE FROM chat_item_reactions WHERE group_id = ? AND group_member_id = ? AND shared_msg_id = ? AND item_member_id = ? AND reaction_sent = ? AND reaction = ? |] - (groupId, groupMemberId' m, itemSharedMId, itemMemberId, sent, reaction) + (groupId, groupMemberId' m, itemSharedMId, itemMemberId, BI sent, reaction) + +getReactionMembers :: DB.Connection -> VersionRangeChat -> User -> GroupId -> SharedMsgId -> MsgReaction -> IO [MemberReaction] +getReactionMembers db vr user groupId itemSharedMId reaction = do + reactions <- + DB.query + db + [sql| + SELECT group_member_id, reaction_ts + FROM chat_item_reactions + WHERE group_id = ? AND shared_msg_id = ? AND reaction = ? + |] + (groupId, itemSharedMId, reaction) + rights <$> mapM (runExceptT . toMemberReaction) reactions + where + toMemberReaction :: (GroupMemberId, UTCTime) -> ExceptT StoreError IO MemberReaction + toMemberReaction (groupMemberId, reactionTs) = do + groupMember <- getGroupMemberById db vr user groupMemberId + pure MemberReaction {groupMember, reactionTs} getTimedItems :: DB.Connection -> User -> UTCTime -> IO [((ChatRef, ChatItemId), UTCTime)] getTimedItems db User {userId} startTimedThreadCutoff = @@ -1936,11 +3036,12 @@ getTimedItems db User {userId} startTimedThreadCutoff = (itemId, Nothing, Just groupId, deleteAt) -> Just ((ChatRef CTGroup groupId, itemId), deleteAt) _ -> Nothing -getChatItemTTL :: DB.Connection -> User -> IO (Maybe Int64) +getChatItemTTL :: DB.Connection -> User -> IO Int64 getChatItemTTL db User {userId} = - fmap join . maybeFirstRow fromOnly $ DB.query db "SELECT chat_item_ttl FROM settings WHERE user_id = ? LIMIT 1" (Only userId) + fmap (fromMaybe 0 . join) . maybeFirstRow fromOnly $ + DB.query db "SELECT chat_item_ttl FROM settings WHERE user_id = ? LIMIT 1" (Only userId) -setChatItemTTL :: DB.Connection -> User -> Maybe Int64 -> IO () +setChatItemTTL :: DB.Connection -> User -> Int64 -> IO () setChatItemTTL db User {userId} chatItemTTL = do currentTs <- getCurrentTime r :: (Maybe Int64) <- maybeFirstRow fromOnly $ DB.query db "SELECT 1 FROM settings WHERE user_id = ? LIMIT 1" (Only userId) @@ -1956,6 +3057,14 @@ setChatItemTTL db User {userId} chatItemTTL = do "INSERT INTO settings (user_id, chat_item_ttl, created_at, updated_at) VALUES (?,?,?,?)" (userId, chatItemTTL, currentTs, currentTs) +getChatTTLCount :: DB.Connection -> User -> IO Int +getChatTTLCount db User {userId} = do + contactCount <- getCount "SELECT COUNT(1) FROM contacts WHERE user_id = ? AND chat_item_ttl > 0" + groupCount <- getCount "SELECT COUNT(1) FROM groups WHERE user_id = ? AND chat_item_ttl > 0" + pure $ contactCount + groupCount + where + getCount q = fromOnly . head <$> DB.query db q (Only userId) + getContactExpiredFileInfo :: DB.Connection -> User -> Contact -> UTCTime -> IO [CIFileInfo] getContactExpiredFileInfo db User {userId} Contact {contactId} expirationDate = map toFileInfo @@ -1997,9 +3106,9 @@ createCIModeration db GroupInfo {groupId} moderatorMember itemMemberId itemShare |] (groupId, groupMemberId' moderatorMember, itemMemberId, itemSharedMId, msgId, moderatedAtTs) -getCIModeration :: DB.Connection -> User -> GroupInfo -> MemberId -> Maybe SharedMsgId -> IO (Maybe CIModeration) -getCIModeration _ _ _ _ Nothing = pure Nothing -getCIModeration db user GroupInfo {groupId} itemMemberId (Just sharedMsgId) = do +getCIModeration :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> MemberId -> Maybe SharedMsgId -> IO (Maybe CIModeration) +getCIModeration _ _ _ _ _ Nothing = pure Nothing +getCIModeration db vr user GroupInfo {groupId} itemMemberId (Just sharedMsgId) = do r_ <- maybeFirstRow id $ DB.query @@ -2013,7 +3122,7 @@ getCIModeration db user GroupInfo {groupId} itemMemberId (Just sharedMsgId) = do (groupId, itemMemberId, sharedMsgId) case r_ of Just (moderationId, moderatorId, createdByMsgId, moderatedAt) -> do - runExceptT (getGroupMember db user groupId moderatorId) >>= \case + runExceptT (getGroupMember db vr user groupId moderatorId) >>= \case Right moderatorMember -> pure (Just CIModeration {moderationId, moderatorMember, createdByMsgId, moderatedAt}) _ -> pure Nothing _ -> pure Nothing @@ -2026,14 +3135,14 @@ deleteCIModeration db GroupInfo {groupId} itemMemberId (Just sharedMsgId) = "DELETE FROM chat_item_moderations WHERE group_id = ? AND item_member_id = ? AND shared_msg_id = ?" (groupId, itemMemberId, sharedMsgId) -createGroupSndStatus :: DB.Connection -> ChatItemId -> GroupMemberId -> CIStatus 'MDSnd -> IO () +createGroupSndStatus :: DB.Connection -> ChatItemId -> GroupMemberId -> GroupSndStatus -> IO () createGroupSndStatus db itemId memberId status = DB.execute db "INSERT INTO group_snd_item_statuses (chat_item_id, group_member_id, group_snd_item_status) VALUES (?,?,?)" (itemId, memberId, status) -getGroupSndStatus :: DB.Connection -> ChatItemId -> GroupMemberId -> ExceptT StoreError IO (CIStatus 'MDSnd) +getGroupSndStatus :: DB.Connection -> ChatItemId -> GroupMemberId -> ExceptT StoreError IO GroupSndStatus getGroupSndStatus db itemId memberId = ExceptT . firstRow fromOnly (SENoGroupSndStatus itemId memberId) $ DB.query @@ -2046,7 +3155,7 @@ getGroupSndStatus db itemId memberId = |] (itemId, memberId) -updateGroupSndStatus :: DB.Connection -> ChatItemId -> GroupMemberId -> CIStatus 'MDSnd -> IO () +updateGroupSndStatus :: DB.Connection -> ChatItemId -> GroupMemberId -> GroupSndStatus -> IO () updateGroupSndStatus db itemId memberId status = do currentTs <- liftIO getCurrentTime DB.execute @@ -2058,18 +3167,33 @@ updateGroupSndStatus db itemId memberId status = do |] (status, currentTs, itemId, memberId) -getGroupSndStatuses :: DB.Connection -> ChatItemId -> IO [(GroupMemberId, CIStatus 'MDSnd)] -getGroupSndStatuses db itemId = - DB.query +setGroupSndViaProxy :: DB.Connection -> ChatItemId -> GroupMemberId -> Bool -> IO () +setGroupSndViaProxy db itemId memberId viaProxy = + DB.execute db [sql| - SELECT group_member_id, group_snd_item_status - FROM group_snd_item_statuses - WHERE chat_item_id = ? + UPDATE group_snd_item_statuses + SET via_proxy = ? + WHERE chat_item_id = ? AND group_member_id = ? |] - (Only itemId) + (BI viaProxy, itemId, memberId) -getGroupSndStatusCounts :: DB.Connection -> ChatItemId -> IO [(CIStatus 'MDSnd, Int)] +getGroupSndStatuses :: DB.Connection -> ChatItemId -> IO [MemberDeliveryStatus] +getGroupSndStatuses db itemId = + map memStatus + <$> DB.query + db + [sql| + SELECT group_member_id, group_snd_item_status, via_proxy + FROM group_snd_item_statuses + WHERE chat_item_id = ? + |] + (Only itemId) + where + memStatus (groupMemberId, memberDeliveryStatus, sentViaProxy) = + MemberDeliveryStatus {groupMemberId, memberDeliveryStatus, sentViaProxy = unBI <$> sentViaProxy} + +getGroupSndStatusCounts :: DB.Connection -> ChatItemId -> IO [(GroupSndStatus, Int)] getGroupSndStatusCounts db itemId = DB.query db @@ -2081,11 +3205,11 @@ getGroupSndStatusCounts db itemId = |] (Only itemId) -getGroupHistoryItems :: DB.Connection -> User -> GroupInfo -> Int -> IO [Either StoreError (CChatItem 'CTGroup)] -getGroupHistoryItems db user@User {userId} GroupInfo {groupId} count = do - chatItemIds <- getLastItemIds_ - -- use getGroupCIWithReactions to read reactions data - reverse <$> mapM (runExceptT . getGroupChatItem db user groupId) chatItemIds +-- TODO [knocking] filter out messages sent to member only +getGroupHistoryItems :: DB.Connection -> User -> GroupInfo -> GroupMember -> Int -> IO [Either StoreError (CChatItem 'CTGroup)] +getGroupHistoryItems db user@User {userId} g@GroupInfo {groupId} m count = do + ciIds <- getLastItemIds_ + reverse <$> mapM (runExceptT . getGroupCIWithReactions db user g) ciIds where getLastItemIds_ :: IO [ChatItemId] getLastItemIds_ = @@ -2093,12 +3217,14 @@ getGroupHistoryItems db user@User {userId} GroupInfo {groupId} count = do <$> DB.query db [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND group_id = ? - AND item_content_tag IN (?,?) - AND item_deleted = 0 - ORDER BY item_ts DESC, chat_item_id DESC + SELECT i.chat_item_id + FROM chat_items i + LEFT JOIN group_snd_item_statuses s ON s.chat_item_id = i.chat_item_id AND s.group_member_id = ? + WHERE s.group_snd_item_status_id IS NULL + AND i.user_id = ? AND i.group_id = ? + AND i.include_in_history = 1 + AND i.item_deleted = 0 + ORDER BY i.item_ts DESC, i.chat_item_id DESC LIMIT ? |] - (userId, groupId, rcvMsgContentTag, sndMsgContentTag, count) + (groupMemberId' m, userId, groupId, count) diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs deleted file mode 100644 index 61fbfe3598..0000000000 --- a/src/Simplex/Chat/Store/Migrations.hs +++ /dev/null @@ -1,197 +0,0 @@ -{-# LANGUAGE NamedFieldPuns #-} - -module Simplex.Chat.Store.Migrations (migrations) where - -import Data.List (sortOn) -import Database.SQLite.Simple (Query (..)) -import Simplex.Chat.Migrations.M20220101_initial -import Simplex.Chat.Migrations.M20220122_v1_1 -import Simplex.Chat.Migrations.M20220205_chat_item_status -import Simplex.Chat.Migrations.M20220210_deduplicate_contact_requests -import Simplex.Chat.Migrations.M20220224_messages_fks -import Simplex.Chat.Migrations.M20220301_smp_servers -import Simplex.Chat.Migrations.M20220302_profile_images -import Simplex.Chat.Migrations.M20220304_msg_quotes -import Simplex.Chat.Migrations.M20220321_chat_item_edited -import Simplex.Chat.Migrations.M20220404_files_status_fields -import Simplex.Chat.Migrations.M20220514_profiles_user_id -import Simplex.Chat.Migrations.M20220626_auto_reply -import Simplex.Chat.Migrations.M20220702_calls -import Simplex.Chat.Migrations.M20220715_groups_chat_item_id -import Simplex.Chat.Migrations.M20220811_chat_items_indices -import Simplex.Chat.Migrations.M20220812_incognito_profiles -import Simplex.Chat.Migrations.M20220818_chat_notifications -import Simplex.Chat.Migrations.M20220822_groups_host_conn_custom_user_profile_id -import Simplex.Chat.Migrations.M20220823_delete_broken_group_event_chat_items -import Simplex.Chat.Migrations.M20220824_profiles_local_alias -import Simplex.Chat.Migrations.M20220909_commands -import Simplex.Chat.Migrations.M20220926_connection_alias -import Simplex.Chat.Migrations.M20220928_settings -import Simplex.Chat.Migrations.M20221001_shared_msg_id_indices -import Simplex.Chat.Migrations.M20221003_delete_broken_integrity_error_chat_items -import Simplex.Chat.Migrations.M20221004_idx_msg_deliveries_message_id -import Simplex.Chat.Migrations.M20221011_user_contact_links_group_id -import Simplex.Chat.Migrations.M20221012_inline_files -import Simplex.Chat.Migrations.M20221019_unread_chat -import Simplex.Chat.Migrations.M20221021_auto_accept__group_links -import Simplex.Chat.Migrations.M20221024_contact_used -import Simplex.Chat.Migrations.M20221025_chat_settings -import Simplex.Chat.Migrations.M20221029_group_link_id -import Simplex.Chat.Migrations.M20221112_server_password -import Simplex.Chat.Migrations.M20221115_server_cfg -import Simplex.Chat.Migrations.M20221129_delete_group_feature_items -import Simplex.Chat.Migrations.M20221130_delete_item_deleted -import Simplex.Chat.Migrations.M20221209_verified_connection -import Simplex.Chat.Migrations.M20221210_idxs -import Simplex.Chat.Migrations.M20221211_group_description -import Simplex.Chat.Migrations.M20221212_chat_items_timed -import Simplex.Chat.Migrations.M20221214_live_message -import Simplex.Chat.Migrations.M20221222_chat_ts -import Simplex.Chat.Migrations.M20221223_idx_chat_items_item_status -import Simplex.Chat.Migrations.M20221230_idxs -import Simplex.Chat.Migrations.M20230107_connections_auth_err_counter -import Simplex.Chat.Migrations.M20230111_users_agent_user_id -import Simplex.Chat.Migrations.M20230117_fkey_indexes -import Simplex.Chat.Migrations.M20230118_recreate_smp_servers -import Simplex.Chat.Migrations.M20230129_drop_chat_items_group_idx -import Simplex.Chat.Migrations.M20230206_item_deleted_by_group_member_id -import Simplex.Chat.Migrations.M20230303_group_link_role -import Simplex.Chat.Migrations.M20230317_hidden_profiles -import Simplex.Chat.Migrations.M20230318_file_description -import Simplex.Chat.Migrations.M20230321_agent_file_deleted -import Simplex.Chat.Migrations.M20230328_files_protocol -import Simplex.Chat.Migrations.M20230402_protocol_servers -import Simplex.Chat.Migrations.M20230411_extra_xftp_file_descriptions -import Simplex.Chat.Migrations.M20230420_rcv_files_to_receive -import Simplex.Chat.Migrations.M20230422_profile_contact_links -import Simplex.Chat.Migrations.M20230504_recreate_msg_delivery_events_cleanup_messages -import Simplex.Chat.Migrations.M20230505_chat_item_versions -import Simplex.Chat.Migrations.M20230511_reactions -import Simplex.Chat.Migrations.M20230519_item_deleted_ts -import Simplex.Chat.Migrations.M20230526_indexes -import Simplex.Chat.Migrations.M20230529_indexes -import Simplex.Chat.Migrations.M20230608_deleted_contacts -import Simplex.Chat.Migrations.M20230618_favorite_chats -import Simplex.Chat.Migrations.M20230621_chat_item_moderations -import Simplex.Chat.Migrations.M20230705_delivery_receipts -import Simplex.Chat.Migrations.M20230721_group_snd_item_statuses -import Simplex.Chat.Migrations.M20230814_indexes -import Simplex.Chat.Migrations.M20230827_file_encryption -import Simplex.Chat.Migrations.M20230829_connections_chat_vrange -import Simplex.Chat.Migrations.M20230903_connections_to_subscribe -import Simplex.Chat.Migrations.M20230913_member_contacts -import Simplex.Chat.Migrations.M20230914_member_probes -import Simplex.Chat.Migrations.M20230926_contact_status -import Simplex.Chat.Migrations.M20231002_conn_initiated -import Simplex.Chat.Migrations.M20231009_via_group_link_uri_hash -import Simplex.Chat.Migrations.M20231010_member_settings -import Simplex.Chat.Migrations.M20231019_indexes -import Simplex.Chat.Migrations.M20231030_xgrplinkmem_received -import Simplex.Chat.Migrations.M20231107_indexes -import Simplex.Chat.Migrations.M20231113_group_forward -import Simplex.Chat.Migrations.M20231114_remote_control -import Simplex.Chat.Migrations.M20231126_remote_ctrl_address -import Simplex.Chat.Migrations.M20231207_chat_list_pagination -import Simplex.Chat.Migrations.M20231214_item_content_tag -import Simplex.Chat.Migrations.M20231215_recreate_msg_deliveries -import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) - -schemaMigrations :: [(String, Query, Maybe Query)] -schemaMigrations = - [ ("20220101_initial", m20220101_initial, Nothing), - ("20220122_v1_1", m20220122_v1_1, Nothing), - ("20220205_chat_item_status", m20220205_chat_item_status, Nothing), - ("20220210_deduplicate_contact_requests", m20220210_deduplicate_contact_requests, Nothing), - ("20220224_messages_fks", m20220224_messages_fks, Nothing), - ("20220301_smp_servers", m20220301_smp_servers, Nothing), - ("20220302_profile_images", m20220302_profile_images, Nothing), - ("20220304_msg_quotes", m20220304_msg_quotes, Nothing), - ("20220321_chat_item_edited", m20220321_chat_item_edited, Nothing), - ("20220404_files_status_fields", m20220404_files_status_fields, Nothing), - ("20220514_profiles_user_id", m20220514_profiles_user_id, Nothing), - ("20220626_auto_reply", m20220626_auto_reply, Nothing), - ("20220702_calls", m20220702_calls, Nothing), - ("20220715_groups_chat_item_id", m20220715_groups_chat_item_id, Nothing), - ("20220811_chat_items_indices", m20220811_chat_items_indices, Nothing), - ("20220812_incognito_profiles", m20220812_incognito_profiles, Nothing), - ("20220818_chat_notifications", m20220818_chat_notifications, Nothing), - ("20220822_groups_host_conn_custom_user_profile_id", m20220822_groups_host_conn_custom_user_profile_id, Nothing), - ("20220823_delete_broken_group_event_chat_items", m20220823_delete_broken_group_event_chat_items, Nothing), - ("20220824_profiles_local_alias", m20220824_profiles_local_alias, Nothing), - ("20220909_commands", m20220909_commands, Nothing), - ("20220926_connection_alias", m20220926_connection_alias, Nothing), - ("20220928_settings", m20220928_settings, Nothing), - ("20221001_shared_msg_id_indices", m20221001_shared_msg_id_indices, Nothing), - ("20221003_delete_broken_integrity_error_chat_items", m20221003_delete_broken_integrity_error_chat_items, Nothing), - ("20221004_idx_msg_deliveries_message_id", m20221004_idx_msg_deliveries_message_id, Nothing), - ("20221011_user_contact_links_group_id", m20221011_user_contact_links_group_id, Nothing), - ("20221012_inline_files", m20221012_inline_files, Nothing), - ("20221019_unread_chat", m20221019_unread_chat, Nothing), - ("20221021_auto_accept__group_links", m20221021_auto_accept__group_links, Nothing), - ("20221024_contact_used", m20221024_contact_used, Nothing), - ("20221025_chat_settings", m20221025_chat_settings, Nothing), - ("20221029_group_link_id", m20221029_group_link_id, Nothing), - ("20221112_server_password", m20221112_server_password, Nothing), - ("20221115_server_cfg", m20221115_server_cfg, Nothing), - ("20221129_delete_group_feature_items", m20221129_delete_group_feature_items, Nothing), - ("20221130_delete_item_deleted", m20221130_delete_item_deleted, Nothing), - ("20221209_verified_connection", m20221209_verified_connection, Nothing), - ("20221210_idxs", m20221210_idxs, Nothing), - ("20221211_group_description", m20221211_group_description, Nothing), - ("20221212_chat_items_timed", m20221212_chat_items_timed, Nothing), - ("20221214_live_message", m20221214_live_message, Nothing), - ("20221222_chat_ts", m20221222_chat_ts, Nothing), - ("20221223_idx_chat_items_item_status", m20221223_idx_chat_items_item_status, Nothing), - ("20221230_idxs", m20221230_idxs, Nothing), - ("20230107_connections_auth_err_counter", m20230107_connections_auth_err_counter, Nothing), - ("20230111_users_agent_user_id", m20230111_users_agent_user_id, Nothing), - ("20230117_fkey_indexes", m20230117_fkey_indexes, Nothing), - ("20230118_recreate_smp_servers", m20230118_recreate_smp_servers, Nothing), - ("20230129_drop_chat_items_group_idx", m20230129_drop_chat_items_group_idx, Nothing), - ("20230206_item_deleted_by_group_member_id", m20230206_item_deleted_by_group_member_id, Nothing), - ("20230303_group_link_role", m20230303_group_link_role, Nothing), - ("20230317_hidden_profiles", m20230317_hidden_profiles, Just down_m20230317_hidden_profiles), - ("20230318_file_description", m20230318_file_description, Just down_m20230318_file_description), - ("20230321_agent_file_deleted", m20230321_agent_file_deleted, Just down_m20230321_agent_file_deleted), - ("20230328_files_protocol", m20230328_files_protocol, Just down_m20230328_files_protocol), - ("20230402_protocol_servers", m20230402_protocol_servers, Just down_m20230402_protocol_servers), - ("20230411_extra_xftp_file_descriptions", m20230411_extra_xftp_file_descriptions, Just down_m20230411_extra_xftp_file_descriptions), - ("20230420_rcv_files_to_receive", m20230420_rcv_files_to_receive, Just down_m20230420_rcv_files_to_receive), - ("20230422_profile_contact_links", m20230422_profile_contact_links, Just down_m20230422_profile_contact_links), - ("20230504_recreate_msg_delivery_events_cleanup_messages", m20230504_recreate_msg_delivery_events_cleanup_messages, Just down_m20230504_recreate_msg_delivery_events_cleanup_messages), - ("20230505_chat_item_versions", m20230505_chat_item_versions, Just down_m20230505_chat_item_versions), - ("20230511_reactions", m20230511_reactions, Just down_m20230511_reactions), - ("20230519_item_deleted_ts", m20230519_item_deleted_ts, Just down_m20230519_item_deleted_ts), - ("20230526_indexes", m20230526_indexes, Just down_m20230526_indexes), - ("20230529_indexes", m20230529_indexes, Just down_m20230529_indexes), - ("20230608_deleted_contacts", m20230608_deleted_contacts, Just down_m20230608_deleted_contacts), - ("20230618_favorite_chats", m20230618_favorite_chats, Just down_m20230618_favorite_chats), - ("20230621_chat_item_moderations", m20230621_chat_item_moderations, Just down_m20230621_chat_item_moderations), - ("20230705_delivery_receipts", m20230705_delivery_receipts, Just down_m20230705_delivery_receipts), - ("20230721_group_snd_item_statuses", m20230721_group_snd_item_statuses, Just down_m20230721_group_snd_item_statuses), - ("20230814_indexes", m20230814_indexes, Just down_m20230814_indexes), - ("20230827_file_encryption", m20230827_file_encryption, Just down_m20230827_file_encryption), - ("20230829_connections_chat_vrange", m20230829_connections_chat_vrange, Just down_m20230829_connections_chat_vrange), - ("20230903_connections_to_subscribe", m20230903_connections_to_subscribe, Just down_m20230903_connections_to_subscribe), - ("20230913_member_contacts", m20230913_member_contacts, Just down_m20230913_member_contacts), - ("20230914_member_probes", m20230914_member_probes, Just down_m20230914_member_probes), - ("20230926_contact_status", m20230926_contact_status, Just down_m20230926_contact_status), - ("20231002_conn_initiated", m20231002_conn_initiated, Just down_m20231002_conn_initiated), - ("20231009_via_group_link_uri_hash", m20231009_via_group_link_uri_hash, Just down_m20231009_via_group_link_uri_hash), - ("20231010_member_settings", m20231010_member_settings, Just down_m20231010_member_settings), - ("20231019_indexes", m20231019_indexes, Just down_m20231019_indexes), - ("20231030_xgrplinkmem_received", m20231030_xgrplinkmem_received, Just down_m20231030_xgrplinkmem_received), - ("20231107_indexes", m20231107_indexes, Just down_m20231107_indexes), - ("20231113_group_forward", m20231113_group_forward, Just down_m20231113_group_forward), - ("20231114_remote_control", m20231114_remote_control, Just down_m20231114_remote_control), - ("20231126_remote_ctrl_address", m20231126_remote_ctrl_address, Just down_m20231126_remote_ctrl_address), - ("20231207_chat_list_pagination", m20231207_chat_list_pagination, Just down_m20231207_chat_list_pagination), - ("20231214_item_content_tag", m20231214_item_content_tag, Just down_m20231214_item_content_tag), - ("20231215_recreate_msg_deliveries", m20231215_recreate_msg_deliveries, Just down_m20231215_recreate_msg_deliveries) - ] - --- | The list of migrations in ascending order by date -migrations :: [Migration] -migrations = sortOn name $ map migration schemaMigrations - where - migration (name, up, down) = Migration {name, up = fromQuery up, down = fromQuery <$> down} diff --git a/src/Simplex/Chat/Store/NoteFolders.hs b/src/Simplex/Chat/Store/NoteFolders.hs new file mode 100644 index 0000000000..8f71f3f21e --- /dev/null +++ b/src/Simplex/Chat/Store/NoteFolders.hs @@ -0,0 +1,76 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Simplex.Chat.Store.NoteFolders where + +import Control.Monad.Except (ExceptT (..), throwError) +import Control.Monad.IO.Class (liftIO) +import Data.Time (getCurrentTime) +import Simplex.Chat.Store.Shared (StoreError (..)) +import Simplex.Chat.Types (NoteFolder (..), NoteFolderId, User (..)) +import Simplex.Messaging.Agent.Protocol (UserId) +import Simplex.Messaging.Agent.Store.AgentStore (firstRow) +import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) +import qualified Simplex.Messaging.Agent.Store.DB as DB +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (Only (..)) +import Database.PostgreSQL.Simple.SqlQQ (sql) +#else +import Database.SQLite.Simple (Only (..)) +import Database.SQLite.Simple.QQ (sql) +#endif + +createNoteFolder :: DB.Connection -> User -> ExceptT StoreError IO () +createNoteFolder db User {userId} = do + liftIO (DB.query db "SELECT note_folder_id FROM note_folders WHERE user_id = ? LIMIT 1" $ Only userId) >>= \case + [] -> liftIO $ DB.execute db "INSERT INTO note_folders (user_id) VALUES (?)" (Only userId) + Only noteFolderId : _ -> throwError $ SENoteFolderAlreadyExists noteFolderId + +getUserNoteFolderId :: DB.Connection -> User -> ExceptT StoreError IO NoteFolderId +getUserNoteFolderId db User {userId} = + ExceptT . firstRow fromOnly SEUserNoteFolderNotFound $ + DB.query db "SELECT note_folder_id FROM note_folders WHERE user_id = ?" (Only userId) + +getNoteFolder :: DB.Connection -> User -> NoteFolderId -> ExceptT StoreError IO NoteFolder +getNoteFolder db User {userId} noteFolderId = + ExceptT . firstRow toNoteFolder (SENoteFolderNotFound noteFolderId) $ + DB.query + db + [sql| + SELECT + created_at, updated_at, chat_ts, favorite, unread_chat + FROM note_folders + WHERE user_id = ? + AND note_folder_id = ? + |] + (userId, noteFolderId) + where + toNoteFolder (createdAt, updatedAt, chatTs, BI favorite, BI unread) = + NoteFolder {noteFolderId, userId, createdAt, updatedAt, chatTs, favorite, unread} + +updateNoteFolderUnreadChat :: DB.Connection -> User -> NoteFolder -> Bool -> IO () +updateNoteFolderUnreadChat db User {userId} NoteFolder {noteFolderId} unreadChat = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE note_folders SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND note_folder_id = ?" (BI unreadChat, updatedAt, userId, noteFolderId) + +deleteNoteFolderFiles :: DB.Connection -> UserId -> NoteFolder -> IO () +deleteNoteFolderFiles db userId NoteFolder {noteFolderId} = do + DB.execute + db + [sql| + DELETE FROM files + WHERE user_id = ? + AND chat_item_id IN ( + SELECT chat_item_id FROM chat_items WHERE user_id = ? AND note_folder_id = ? + ) + |] + (userId, userId, noteFolderId) + +deleteNoteFolderCIs :: DB.Connection -> User -> NoteFolder -> IO () +deleteNoteFolderCIs db User {userId} NoteFolder {noteFolderId} = + DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND note_folder_id = ?" (userId, noteFolderId) diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs new file mode 100644 index 0000000000..c392c17db1 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -0,0 +1,23 @@ +{-# LANGUAGE NamedFieldPuns #-} + +module Simplex.Chat.Store.Postgres.Migrations (migrations) where + +import Data.List (sortOn) +import Data.Text (Text) +import Simplex.Chat.Store.Postgres.Migrations.M20241220_initial +import Simplex.Chat.Store.Postgres.Migrations.M20250402_short_links +import Simplex.Chat.Store.Postgres.Migrations.M20250512_member_admission +import Simplex.Messaging.Agent.Store.Shared (Migration (..)) + +schemaMigrations :: [(String, Text, Maybe Text)] +schemaMigrations = + [ ("20241220_initial", m20241220_initial, Nothing), + ("20250402_short_links", m20250402_short_links, Just down_m20250402_short_links), + ("20250512_member_admission", m20250512_member_admission, Just down_m20250512_member_admission) + ] + +-- | The list of migrations in ascending order by date +migrations :: [Migration] +migrations = sortOn name $ map migration schemaMigrations + where + migration (name, up, down) = Migration {name, up, down} diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20241220_initial.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20241220_initial.hs new file mode 100644 index 0000000000..08ce2d81bc --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20241220_initial.hs @@ -0,0 +1,1055 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20241220_initial where + +import Data.Text (Text) +import qualified Data.Text as T +import Text.RawString.QQ (r) + +m20241220_initial :: Text +m20241220_initial = + T.pack + [r| +CREATE TABLE users( + user_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + contact_id BIGINT NOT NULL UNIQUE, + local_display_name TEXT NOT NULL UNIQUE, + active_user SMALLINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + agent_user_id BIGINT NOT NULL, + view_pwd_hash BYTEA, + view_pwd_salt BYTEA, + show_ntfs SMALLINT NOT NULL DEFAULT 1, + send_rcpts_contacts SMALLINT NOT NULL DEFAULT 0, + send_rcpts_small_groups SMALLINT NOT NULL DEFAULT 0, + user_member_profile_updated_at TIMESTAMPTZ, + ui_themes TEXT, + active_order BIGINT NOT NULL DEFAULT 0 +); +CREATE TABLE contact_profiles( + contact_profile_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + display_name TEXT NOT NULL, + full_name TEXT NOT NULL, + properties TEXT NOT NULL DEFAULT '{}', + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + image TEXT, + user_id BIGINT DEFAULT NULL REFERENCES users ON DELETE CASCADE, + incognito SMALLINT, + local_alias TEXT NOT NULL DEFAULT '', + preferences TEXT, + contact_link BYTEA +); +CREATE TABLE display_names( + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + local_display_name TEXT NOT NULL, + ldn_base TEXT NOT NULL, + ldn_suffix BIGINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + PRIMARY KEY(user_id, local_display_name), + UNIQUE(user_id, ldn_base, ldn_suffix) +); +ALTER TABLE users +ADD CONSTRAINT fk_users_display_names + FOREIGN KEY(user_id, local_display_name) + REFERENCES display_names(user_id, local_display_name) + ON DELETE RESTRICT + ON UPDATE CASCADE + DEFERRABLE INITIALLY DEFERRED; +CREATE TABLE contacts( + contact_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + contact_profile_id BIGINT REFERENCES contact_profiles ON DELETE SET NULL, + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + local_display_name TEXT NOT NULL, + is_user SMALLINT NOT NULL DEFAULT 0, + via_group BIGINT, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL, + xcontact_id BYTEA, + enable_ntfs SMALLINT, + unread_chat SMALLINT NOT NULL DEFAULT 0, + contact_used SMALLINT NOT NULL DEFAULT 0, + user_preferences TEXT NOT NULL DEFAULT '{}', + chat_ts TIMESTAMPTZ, + deleted SMALLINT NOT NULL DEFAULT 0, + favorite SMALLINT NOT NULL DEFAULT 0, + send_rcpts SMALLINT, + contact_group_member_id BIGINT, + contact_grp_inv_sent SMALLINT NOT NULL DEFAULT 0, + contact_status TEXT NOT NULL DEFAULT 'active', + custom_data BYTEA, + ui_themes TEXT, + chat_deleted SMALLINT NOT NULL DEFAULT 0, + chat_item_ttl BIGINT, + FOREIGN KEY(user_id, local_display_name) + REFERENCES display_names(user_id, local_display_name) + ON DELETE CASCADE + ON UPDATE CASCADE, + UNIQUE(user_id, local_display_name), + UNIQUE(user_id, contact_profile_id) +); +ALTER TABLE users +ADD CONSTRAINT fk_users_contacts + FOREIGN KEY(contact_id) + REFERENCES contacts(contact_id) + ON DELETE RESTRICT + DEFERRABLE INITIALLY DEFERRED; +CREATE TABLE known_servers( + server_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + host TEXT NOT NULL, + port TEXT NOT NULL, + key_hash BYTEA, + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + UNIQUE(user_id, host, port) +); +CREATE TABLE group_profiles( + group_profile_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + display_name TEXT NOT NULL, + full_name TEXT NOT NULL, + properties TEXT NOT NULL DEFAULT '{}', + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + image TEXT, + user_id BIGINT DEFAULT NULL REFERENCES users ON DELETE CASCADE, + preferences TEXT, + description TEXT NULL +); +CREATE TABLE groups( + group_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + local_display_name TEXT NOT NULL, + group_profile_id BIGINT REFERENCES group_profiles ON DELETE SET NULL, + inv_queue_info BYTEA, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + chat_item_id BIGINT DEFAULT NULL, + enable_ntfs SMALLINT, + unread_chat SMALLINT NOT NULL DEFAULT 0, + chat_ts TIMESTAMPTZ, + favorite SMALLINT NOT NULL DEFAULT 0, + send_rcpts SMALLINT, + via_group_link_uri_hash BYTEA, + user_member_profile_sent_at TIMESTAMPTZ, + custom_data BYTEA, + ui_themes TEXT, + business_member_id BYTEA NULL, + business_chat TEXT NULL, + business_xcontact_id BYTEA NULL, + customer_member_id BYTEA NULL, + chat_item_ttl BIGINT, + local_alias TEXT DEFAULT '', + FOREIGN KEY(user_id, local_display_name) + REFERENCES display_names(user_id, local_display_name) + ON DELETE CASCADE + ON UPDATE CASCADE, + UNIQUE(user_id, local_display_name), + UNIQUE(user_id, group_profile_id) +); +ALTER TABLE contacts +ADD CONSTRAINT fk_contacts_groups + FOREIGN KEY(via_group) + REFERENCES groups(group_id) ON DELETE SET NULL; +CREATE TABLE group_members( + group_member_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + group_id BIGINT NOT NULL REFERENCES groups ON DELETE CASCADE, + member_id BYTEA NOT NULL, + member_role TEXT NOT NULL, + member_category TEXT NOT NULL, + member_status TEXT NOT NULL, + invited_by BIGINT REFERENCES contacts(contact_id) ON DELETE SET NULL, + sent_inv_queue_info BYTEA, + group_queue_info BYTEA, + direct_queue_info BYTEA, + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + local_display_name TEXT NOT NULL, + contact_profile_id BIGINT NOT NULL REFERENCES contact_profiles ON DELETE CASCADE, + contact_id BIGINT REFERENCES contacts ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + member_profile_id BIGINT REFERENCES contact_profiles ON DELETE SET NULL, + show_messages SMALLINT NOT NULL DEFAULT 1, + xgrplinkmem_received SMALLINT NOT NULL DEFAULT 0, + invited_by_group_member_id BIGINT REFERENCES group_members ON DELETE SET NULL, + peer_chat_min_version INTEGER NOT NULL DEFAULT 1, + peer_chat_max_version INTEGER NOT NULL DEFAULT 1, + member_restriction TEXT, + FOREIGN KEY(user_id, local_display_name) + REFERENCES display_names(user_id, local_display_name) + ON DELETE CASCADE + ON UPDATE CASCADE, + UNIQUE(group_id, member_id) +); +ALTER TABLE contacts +ADD CONSTRAINT fk_contacts_group_members + FOREIGN KEY(contact_group_member_id) + REFERENCES group_members(group_member_id) ON DELETE SET NULL; +CREATE TABLE group_member_intros( + group_member_intro_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + re_group_member_id BIGINT NOT NULL REFERENCES group_members(group_member_id) ON DELETE CASCADE, + to_group_member_id BIGINT NOT NULL REFERENCES group_members(group_member_id) ON DELETE CASCADE, + group_queue_info BYTEA, + direct_queue_info BYTEA, + intro_status TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + intro_chat_protocol_version INTEGER NOT NULL DEFAULT 3, + UNIQUE(re_group_member_id, to_group_member_id) +); +CREATE TABLE files( + file_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + contact_id BIGINT REFERENCES contacts ON DELETE CASCADE, + group_id BIGINT REFERENCES groups ON DELETE CASCADE, + file_name TEXT NOT NULL, + file_path TEXT, + file_size BIGINT NOT NULL, + chunk_size BIGINT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + chat_item_id BIGINT DEFAULT NULL, + updated_at TIMESTAMPTZ NOT NULL, + cancelled SMALLINT, + ci_file_status TEXT, + file_inline TEXT, + agent_snd_file_id BYTEA NULL, + private_snd_file_descr TEXT NULL, + agent_snd_file_deleted SMALLINT NOT NULL DEFAULT 0, + protocol TEXT NOT NULL DEFAULT 'smp', + file_crypto_key BYTEA, + file_crypto_nonce BYTEA, + note_folder_id BIGINT DEFAULT NULL, + redirect_file_id BIGINT REFERENCES files ON DELETE CASCADE +); +CREATE TABLE snd_files( + file_id BIGINT NOT NULL REFERENCES files ON DELETE CASCADE, + connection_id BIGINT NOT NULL, + file_status TEXT NOT NULL, + group_member_id BIGINT REFERENCES group_members ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + file_inline TEXT, + last_inline_msg_delivery_id BIGINT, + file_descr_id BIGINT NULL, + PRIMARY KEY(file_id, connection_id) +); +CREATE TABLE rcv_files( + file_id BIGINT PRIMARY KEY REFERENCES files ON DELETE CASCADE, + file_status TEXT NOT NULL, + group_member_id BIGINT REFERENCES group_members ON DELETE CASCADE, + file_queue_info BYTEA, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + rcv_file_inline TEXT, + file_inline TEXT, + file_descr_id BIGINT NULL, + agent_rcv_file_id BYTEA NULL, + agent_rcv_file_deleted SMALLINT NOT NULL DEFAULT 0, + to_receive SMALLINT, + user_approved_relays SMALLINT NOT NULL DEFAULT 0 +); +CREATE TABLE snd_file_chunks( + file_id BIGINT NOT NULL, + connection_id BIGINT NOT NULL, + chunk_number BIGINT NOT NULL, + chunk_agent_msg_id BIGINT, + chunk_sent SMALLINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + FOREIGN KEY(file_id, connection_id) REFERENCES snd_files ON DELETE CASCADE, + PRIMARY KEY(file_id, connection_id, chunk_number) +); +CREATE TABLE rcv_file_chunks( + file_id BIGINT NOT NULL REFERENCES rcv_files ON DELETE CASCADE, + chunk_number BIGINT NOT NULL, + chunk_agent_msg_id BIGINT NOT NULL, + chunk_stored SMALLINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + PRIMARY KEY(file_id, chunk_number) +); +CREATE TABLE connections( + connection_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + agent_conn_id BYTEA NOT NULL UNIQUE, + conn_level BIGINT NOT NULL DEFAULT 0, + via_contact BIGINT REFERENCES contacts(contact_id) ON DELETE SET NULL, + conn_status TEXT NOT NULL, + conn_type TEXT NOT NULL, + user_contact_link_id BIGINT, + contact_id BIGINT REFERENCES contacts ON DELETE CASCADE, + group_member_id BIGINT REFERENCES group_members ON DELETE CASCADE, + snd_file_id BIGINT, + rcv_file_id BIGINT REFERENCES rcv_files(file_id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + updated_at TIMESTAMPTZ NOT NULL, + via_contact_uri_hash BYTEA, + xcontact_id BYTEA, + via_user_contact_link BIGINT DEFAULT NULL, + custom_user_profile_id BIGINT REFERENCES contact_profiles ON DELETE SET NULL, + conn_req_inv BYTEA, + local_alias TEXT NOT NULL DEFAULT '', + via_group_link SMALLINT NOT NULL DEFAULT 0, + group_link_id BYTEA, + security_code TEXT NULL, + security_code_verified_at TIMESTAMPTZ NULL, + auth_err_counter BIGINT NOT NULL DEFAULT 0, + peer_chat_min_version INTEGER NOT NULL DEFAULT 1, + peer_chat_max_version INTEGER NOT NULL DEFAULT 1, + to_subscribe SMALLINT DEFAULT 0 NOT NULL, + contact_conn_initiated SMALLINT NOT NULL DEFAULT 0, + conn_chat_version INTEGER, + pq_support SMALLINT NOT NULL DEFAULT 0, + pq_encryption SMALLINT NOT NULL DEFAULT 0, + pq_snd_enabled SMALLINT, + pq_rcv_enabled SMALLINT, + quota_err_counter BIGINT NOT NULL DEFAULT 0, + FOREIGN KEY(snd_file_id, connection_id) + REFERENCES snd_files(file_id, connection_id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED +); +ALTER TABLE snd_files +ADD CONSTRAINT fk_snd_files_connections + FOREIGN KEY(connection_id) + REFERENCES connections(connection_id) ON DELETE CASCADE; +CREATE TABLE user_contact_links( + user_contact_link_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + conn_req_contact BYTEA NOT NULL, + local_display_name TEXT NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + updated_at TIMESTAMPTZ NOT NULL, + auto_accept SMALLINT DEFAULT 0, + auto_reply_msg_content TEXT DEFAULT NULL, + group_id BIGINT REFERENCES groups ON DELETE CASCADE, + auto_accept_incognito SMALLINT NOT NULL DEFAULT 0, + group_link_id BYTEA, + group_link_member_role TEXT NULL, + business_address SMALLINT DEFAULT 0, + UNIQUE(user_id, local_display_name) +); +ALTER TABLE connections +ADD CONSTRAINT fk_connections_user_contact_links_user_contact_link_id + FOREIGN KEY(user_contact_link_id) + REFERENCES user_contact_links(user_contact_link_id) ON DELETE CASCADE; +ALTER TABLE connections +ADD CONSTRAINT fk_connections_user_contact_links_via_user_contact_link + FOREIGN KEY(via_user_contact_link) + REFERENCES user_contact_links(user_contact_link_id) ON DELETE SET NULL; +CREATE TABLE contact_requests( + contact_request_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_contact_link_id BIGINT NOT NULL REFERENCES user_contact_links + ON UPDATE CASCADE ON DELETE CASCADE, + agent_invitation_id BYTEA NOT NULL, + contact_profile_id BIGINT REFERENCES contact_profiles + ON DELETE SET NULL + DEFERRABLE INITIALLY DEFERRED, + local_display_name TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + updated_at TIMESTAMPTZ NOT NULL, + xcontact_id BYTEA, + peer_chat_min_version INTEGER NOT NULL DEFAULT 1, + peer_chat_max_version INTEGER NOT NULL DEFAULT 1, + pq_support SMALLINT NOT NULL DEFAULT 0, + contact_id BIGINT REFERENCES contacts ON DELETE CASCADE, + FOREIGN KEY(user_id, local_display_name) + REFERENCES display_names(user_id, local_display_name) + ON UPDATE CASCADE + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + UNIQUE(user_id, local_display_name), + UNIQUE(user_id, contact_profile_id) +); +CREATE TABLE messages( + message_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + msg_sent SMALLINT NOT NULL, + chat_msg_event TEXT NOT NULL, + msg_body BYTEA, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL, + connection_id BIGINT DEFAULT NULL REFERENCES connections ON DELETE CASCADE, + group_id BIGINT DEFAULT NULL REFERENCES groups ON DELETE CASCADE, + shared_msg_id BYTEA, + shared_msg_id_user SMALLINT, + author_group_member_id BIGINT REFERENCES group_members ON DELETE SET NULL, + forwarded_by_group_member_id BIGINT REFERENCES group_members ON DELETE SET NULL +); +CREATE TABLE pending_group_messages( + pending_group_message_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + group_member_id BIGINT NOT NULL REFERENCES group_members ON DELETE CASCADE, + message_id BIGINT NOT NULL REFERENCES messages ON DELETE CASCADE, + group_member_intro_id BIGINT REFERENCES group_member_intros ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()) +); +CREATE TABLE chat_items( + chat_item_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + contact_id BIGINT REFERENCES contacts ON DELETE CASCADE, + group_id BIGINT REFERENCES groups ON DELETE CASCADE, + group_member_id BIGINT REFERENCES group_members ON DELETE SET NULL, + chat_msg_id BIGINT, + created_by_msg_id BIGINT UNIQUE REFERENCES messages(message_id) ON DELETE SET NULL, + item_sent SMALLINT NOT NULL, + item_ts TIMESTAMPTZ NOT NULL, + item_deleted SMALLINT NOT NULL DEFAULT 0, + item_content TEXT NOT NULL, + item_text TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + item_status TEXT NOT NULL, + shared_msg_id BYTEA, + quoted_shared_msg_id BYTEA, + quoted_sent_at TIMESTAMPTZ, + quoted_content TEXT, + quoted_sent SMALLINT, + quoted_member_id BYTEA, + item_edited SMALLINT, + timed_ttl BIGINT, + timed_delete_at TIMESTAMPTZ, + item_live SMALLINT, + item_deleted_by_group_member_id BIGINT REFERENCES group_members ON DELETE SET NULL, + item_deleted_ts TIMESTAMPTZ, + forwarded_by_group_member_id BIGINT REFERENCES group_members ON DELETE SET NULL, + item_content_tag TEXT, + note_folder_id BIGINT DEFAULT NULL, + fwd_from_tag TEXT, + fwd_from_chat_name TEXT, + fwd_from_msg_dir SMALLINT, + fwd_from_contact_id BIGINT REFERENCES contacts ON DELETE SET NULL, + fwd_from_group_id BIGINT REFERENCES groups ON DELETE SET NULL, + fwd_from_chat_item_id BIGINT REFERENCES chat_items ON DELETE SET NULL, + via_proxy SMALLINT, + msg_content_tag TEXT, + include_in_history SMALLINT NOT NULL DEFAULT 0, + user_mention SMALLINT NOT NULL DEFAULT 0 +); +ALTER TABLE groups +ADD CONSTRAINT fk_groups_chat_items + FOREIGN KEY(chat_item_id) + REFERENCES chat_items(chat_item_id) ON DELETE SET NULL; +ALTER TABLE files +ADD CONSTRAINT fk_files_chat_items + FOREIGN KEY(chat_item_id) + REFERENCES chat_items(chat_item_id) ON DELETE CASCADE; +CREATE TABLE chat_item_messages( + chat_item_id BIGINT NOT NULL REFERENCES chat_items ON DELETE CASCADE, + message_id BIGINT NOT NULL UNIQUE REFERENCES messages ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + UNIQUE(chat_item_id, message_id) +); +CREATE TABLE calls( + call_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + contact_id BIGINT NOT NULL REFERENCES contacts ON DELETE CASCADE, + shared_call_id BYTEA NOT NULL, + chat_item_id BIGINT NOT NULL REFERENCES chat_items ON DELETE CASCADE, + call_state BYTEA NOT NULL, + call_ts TIMESTAMPTZ NOT NULL, + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + call_uuid TEXT NOT NULL DEFAULT '' +); +CREATE TABLE commands( + command_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + connection_id BIGINT REFERENCES connections ON DELETE CASCADE, + command_function TEXT NOT NULL, + command_status TEXT NOT NULL, + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()) +); +CREATE TABLE settings( + settings_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + chat_item_ttl BIGINT, + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()) +); +CREATE TABLE protocol_servers( + smp_server_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + host TEXT NOT NULL, + port TEXT NOT NULL, + key_hash BYTEA NOT NULL, + basic_auth TEXT, + preset SMALLINT NOT NULL DEFAULT 0, + tested SMALLINT, + enabled SMALLINT NOT NULL DEFAULT 1, + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + protocol TEXT NOT NULL DEFAULT 'smp', + UNIQUE(user_id, host, port) +); +CREATE TABLE xftp_file_descriptions( + file_descr_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + file_descr_text TEXT NOT NULL, + file_descr_part_no BIGINT NOT NULL DEFAULT(0), + file_descr_complete SMALLINT NOT NULL DEFAULT(0), + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()) +); +ALTER TABLE snd_files +ADD CONSTRAINT fk_snd_files_xftp_file_descriptions + FOREIGN KEY(file_descr_id) + REFERENCES xftp_file_descriptions(file_descr_id) ON DELETE SET NULL; +ALTER TABLE rcv_files +ADD CONSTRAINT fk_rcv_files_xftp_file_descriptions + FOREIGN KEY(file_descr_id) + REFERENCES xftp_file_descriptions(file_descr_id) ON DELETE SET NULL; +CREATE TABLE extra_xftp_file_descriptions( + extra_file_descr_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + file_id BIGINT NOT NULL REFERENCES files ON DELETE CASCADE, + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + file_descr_text TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()) +); +CREATE TABLE chat_item_versions( + chat_item_version_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + chat_item_id BIGINT NOT NULL REFERENCES chat_items ON DELETE CASCADE, + msg_content TEXT NOT NULL, + item_version_ts TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()) +); +CREATE TABLE chat_item_reactions( + chat_item_reaction_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + item_member_id BYTEA, + shared_msg_id BYTEA NOT NULL, + contact_id BIGINT REFERENCES contacts ON DELETE CASCADE, + group_id BIGINT REFERENCES groups ON DELETE CASCADE, + group_member_id BIGINT REFERENCES group_members ON DELETE SET NULL, + created_by_msg_id BIGINT REFERENCES messages(message_id) ON DELETE SET NULL, + reaction TEXT NOT NULL, + reaction_sent SMALLINT NOT NULL, + reaction_ts TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()) +); +CREATE TABLE chat_item_moderations( + chat_item_moderation_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + group_id BIGINT NOT NULL REFERENCES groups ON DELETE CASCADE, + moderator_member_id BIGINT NOT NULL REFERENCES group_members ON DELETE CASCADE, + item_member_id BYTEA NOT NULL, + shared_msg_id BYTEA NOT NULL, + created_by_msg_id BIGINT REFERENCES messages(message_id) ON DELETE SET NULL, + moderated_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()) +); +CREATE TABLE group_snd_item_statuses( + group_snd_item_status_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + chat_item_id BIGINT NOT NULL REFERENCES chat_items ON DELETE CASCADE, + group_member_id BIGINT NOT NULL REFERENCES group_members ON DELETE CASCADE, + group_snd_item_status TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + via_proxy SMALLINT +); +CREATE TABLE sent_probes( + sent_probe_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + contact_id BIGINT REFERENCES contacts ON DELETE CASCADE, + group_member_id BIGINT REFERENCES group_members ON DELETE CASCADE, + probe BYTEA NOT NULL, + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + UNIQUE(user_id, probe) +); +CREATE TABLE sent_probe_hashes( + sent_probe_hash_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + sent_probe_id BIGINT NOT NULL REFERENCES sent_probes ON DELETE CASCADE, + contact_id BIGINT REFERENCES contacts ON DELETE CASCADE, + group_member_id BIGINT REFERENCES group_members ON DELETE CASCADE, + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); +CREATE TABLE received_probes( + received_probe_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + contact_id BIGINT REFERENCES contacts ON DELETE CASCADE, + group_member_id BIGINT REFERENCES group_members ON DELETE CASCADE, + probe BYTEA, + probe_hash BYTEA NOT NULL, + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); +CREATE TABLE remote_hosts( + remote_host_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + host_device_name TEXT NOT NULL, + store_path TEXT NOT NULL, + ca_key BYTEA NOT NULL, + ca_cert BYTEA NOT NULL, + id_key BYTEA NOT NULL, + host_fingerprint BYTEA NOT NULL, + host_dh_pub BYTEA NOT NULL, + bind_addr TEXT, + bind_iface TEXT, + bind_port INTEGER +); +CREATE TABLE remote_controllers( + remote_ctrl_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + ctrl_device_name TEXT NOT NULL, + ca_key BYTEA NOT NULL, + ca_cert BYTEA NOT NULL, + ctrl_fingerprint BYTEA NOT NULL, + id_pub BYTEA NOT NULL, + dh_priv_key BYTEA NOT NULL, + prev_dh_priv_key BYTEA +); +CREATE TABLE msg_deliveries( + msg_delivery_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + message_id BIGINT NOT NULL REFERENCES messages ON DELETE CASCADE, + connection_id BIGINT NOT NULL REFERENCES connections ON DELETE CASCADE, + agent_msg_id BIGINT, + agent_msg_meta TEXT, + chat_ts TIMESTAMPTZ NOT NULL DEFAULT (now()), + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + delivery_status TEXT +); +CREATE TABLE note_folders( + note_folder_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + chat_ts TIMESTAMPTZ NOT NULL DEFAULT (now()), + favorite SMALLINT NOT NULL DEFAULT 0, + unread_chat SMALLINT NOT NULL DEFAULT 0 +); +ALTER TABLE files +ADD CONSTRAINT fk_files_note_folders + FOREIGN KEY(note_folder_id) + REFERENCES note_folders(note_folder_id) ON DELETE CASCADE; +ALTER TABLE chat_items +ADD CONSTRAINT fk_chat_items_note_folders + FOREIGN KEY(note_folder_id) + REFERENCES note_folders(note_folder_id) ON DELETE CASCADE; +CREATE TABLE app_settings(app_settings TEXT NOT NULL); +CREATE TABLE server_operators( + server_operator_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + server_operator_tag TEXT, + trade_name TEXT NOT NULL, + legal_name TEXT, + server_domains TEXT, + enabled SMALLINT NOT NULL DEFAULT 1, + smp_role_storage SMALLINT NOT NULL DEFAULT 1, + smp_role_proxy SMALLINT NOT NULL DEFAULT 1, + xftp_role_storage SMALLINT NOT NULL DEFAULT 1, + xftp_role_proxy SMALLINT NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()) +); +CREATE TABLE usage_conditions( + usage_conditions_id BIGINT PRIMARY KEY, + conditions_commit TEXT NOT NULL UNIQUE, + notified_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()) +); +CREATE TABLE operator_usage_conditions( + operator_usage_conditions_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + server_operator_id BIGINT REFERENCES server_operators(server_operator_id) ON DELETE SET NULL ON UPDATE CASCADE, + server_operator_tag TEXT, + conditions_commit TEXT NOT NULL, + accepted_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + auto_accepted SMALLINT DEFAULT 0 +); +CREATE TABLE chat_tags( + chat_tag_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + user_id BIGINT REFERENCES users, + chat_tag_text TEXT NOT NULL, + chat_tag_emoji TEXT, + tag_order BIGINT NOT NULL +); +CREATE TABLE chat_tags_chats( + contact_id BIGINT REFERENCES contacts ON DELETE CASCADE, + group_id BIGINT REFERENCES groups ON DELETE CASCADE, + chat_tag_id BIGINT NOT NULL REFERENCES chat_tags ON DELETE CASCADE +); +CREATE TABLE chat_item_mentions ( + chat_item_mention_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + chat_item_id BIGINT NOT NULL REFERENCES chat_items ON DELETE CASCADE, + group_id BIGINT NOT NULL REFERENCES groups ON DELETE CASCADE, + member_id BYTEA NOT NULL, + display_name TEXT NOT NULL +); +CREATE INDEX contact_profiles_index ON contact_profiles( + display_name, + full_name +); +CREATE INDEX idx_groups_inv_queue_info ON groups(inv_queue_info); +CREATE INDEX idx_contact_requests_xcontact_id ON contact_requests(xcontact_id); +CREATE INDEX idx_contacts_xcontact_id ON contacts(xcontact_id); +CREATE INDEX idx_messages_shared_msg_id ON messages(shared_msg_id); +CREATE UNIQUE INDEX idx_chat_items_direct_shared_msg_id ON chat_items( + user_id, + contact_id, + shared_msg_id +); +CREATE UNIQUE INDEX idx_chat_items_group_shared_msg_id ON chat_items( + user_id, + group_id, + group_member_id, + shared_msg_id +); +CREATE UNIQUE INDEX idx_user_contact_links_group_id ON user_contact_links( + group_id +); +CREATE UNIQUE INDEX idx_snd_files_last_inline_msg_delivery_id ON snd_files( + last_inline_msg_delivery_id +); +CREATE INDEX idx_messages_connection_id ON messages(connection_id); +CREATE INDEX idx_chat_items_group_member_id ON chat_items(group_member_id); +CREATE INDEX idx_chat_items_contact_id ON chat_items(contact_id); +CREATE INDEX idx_chat_items_item_status ON chat_items(item_status); +CREATE INDEX idx_connections_group_member ON connections( + user_id, + group_member_id +); +CREATE INDEX idx_commands_connection_id ON commands(connection_id); +CREATE INDEX idx_calls_user_id ON calls(user_id); +CREATE INDEX idx_calls_chat_item_id ON calls(chat_item_id); +CREATE INDEX idx_calls_contact_id ON calls(contact_id); +CREATE INDEX idx_commands_user_id ON commands(user_id); +CREATE INDEX idx_connections_custom_user_profile_id ON connections( + custom_user_profile_id +); +CREATE INDEX idx_connections_via_user_contact_link ON connections( + via_user_contact_link +); +CREATE INDEX idx_connections_rcv_file_id ON connections(rcv_file_id); +CREATE INDEX idx_connections_contact_id ON connections(contact_id); +CREATE INDEX idx_connections_user_contact_link_id ON connections( + user_contact_link_id +); +CREATE INDEX idx_connections_via_contact ON connections(via_contact); +CREATE INDEX idx_contact_profiles_user_id ON contact_profiles(user_id); +CREATE INDEX idx_contact_requests_contact_profile_id ON contact_requests( + contact_profile_id +); +CREATE INDEX idx_contact_requests_user_contact_link_id ON contact_requests( + user_contact_link_id +); +CREATE INDEX idx_contacts_via_group ON contacts(via_group); +CREATE INDEX idx_contacts_contact_profile_id ON contacts(contact_profile_id); +CREATE INDEX idx_files_chat_item_id ON files(chat_item_id); +CREATE INDEX idx_files_user_id ON files(user_id); +CREATE INDEX idx_files_group_id ON files(group_id); +CREATE INDEX idx_files_contact_id ON files(contact_id); +CREATE INDEX idx_group_member_intros_to_group_member_id ON group_member_intros( + to_group_member_id +); +CREATE INDEX idx_group_members_user_id_local_display_name ON group_members( + user_id, + local_display_name +); +CREATE INDEX idx_group_members_member_profile_id ON group_members( + member_profile_id +); +CREATE INDEX idx_group_members_contact_id ON group_members(contact_id); +CREATE INDEX idx_group_members_contact_profile_id ON group_members( + contact_profile_id +); +CREATE INDEX idx_group_members_user_id ON group_members(user_id); +CREATE INDEX idx_group_members_invited_by ON group_members(invited_by); +CREATE INDEX idx_group_profiles_user_id ON group_profiles(user_id); +CREATE INDEX idx_groups_chat_item_id ON groups(chat_item_id); +CREATE INDEX idx_groups_group_profile_id ON groups(group_profile_id); +CREATE INDEX idx_messages_group_id ON messages(group_id); +CREATE INDEX idx_pending_group_messages_group_member_intro_id ON pending_group_messages( + group_member_intro_id +); +CREATE INDEX idx_pending_group_messages_message_id ON pending_group_messages( + message_id +); +CREATE INDEX idx_pending_group_messages_group_member_id ON pending_group_messages( + group_member_id +); +CREATE INDEX idx_rcv_file_chunks_file_id ON rcv_file_chunks(file_id); +CREATE INDEX idx_rcv_files_group_member_id ON rcv_files(group_member_id); +CREATE INDEX idx_settings_user_id ON settings(user_id); +CREATE INDEX idx_snd_file_chunks_file_id_connection_id ON snd_file_chunks( + file_id, + connection_id +); +CREATE INDEX idx_snd_files_group_member_id ON snd_files(group_member_id); +CREATE INDEX idx_snd_files_connection_id ON snd_files(connection_id); +CREATE INDEX idx_snd_files_file_id ON snd_files(file_id); +CREATE INDEX idx_smp_servers_user_id ON protocol_servers(user_id); +CREATE INDEX idx_chat_items_item_deleted_by_group_member_id ON chat_items( + item_deleted_by_group_member_id +); +CREATE INDEX idx_snd_files_file_descr_id ON snd_files(file_descr_id); +CREATE INDEX idx_rcv_files_file_descr_id ON rcv_files(file_descr_id); +CREATE INDEX idx_extra_xftp_file_descriptions_file_id ON extra_xftp_file_descriptions( + file_id +); +CREATE INDEX idx_extra_xftp_file_descriptions_user_id ON extra_xftp_file_descriptions( + user_id +); +CREATE INDEX idx_xftp_file_descriptions_user_id ON xftp_file_descriptions( + user_id +); +CREATE INDEX idx_chat_item_versions_chat_item_id ON chat_item_versions( + chat_item_id +); +CREATE INDEX idx_chat_item_reactions_shared_msg_id ON chat_item_reactions( + shared_msg_id +); +CREATE INDEX idx_chat_item_reactions_contact_id ON chat_item_reactions( + contact_id +); +CREATE INDEX idx_chat_item_reactions_group_id ON chat_item_reactions(group_id); +CREATE INDEX idx_chat_item_reactions_group_member_id ON chat_item_reactions( + group_member_id +); +CREATE INDEX idx_chat_item_reactions_contact ON chat_item_reactions( + contact_id, + shared_msg_id +); +CREATE INDEX idx_chat_item_reactions_group ON chat_item_reactions( + group_id, + shared_msg_id +); +CREATE INDEX idx_messages_created_at ON messages(created_at); +CREATE INDEX idx_chat_item_reactions_created_by_msg_id ON chat_item_reactions( + created_by_msg_id +); +CREATE INDEX idx_chat_items_timed_delete_at ON chat_items( + user_id, + timed_delete_at +); +CREATE INDEX idx_group_members_group_id ON group_members(user_id, group_id); +CREATE INDEX idx_chat_item_moderations_group_id ON chat_item_moderations( + group_id +); +CREATE INDEX idx_chat_item_moderations_moderator_member_id ON chat_item_moderations( + moderator_member_id +); +CREATE INDEX idx_chat_item_moderations_created_by_msg_id ON chat_item_moderations( + created_by_msg_id +); +CREATE INDEX idx_chat_item_moderations_group ON chat_item_moderations( + group_id, + item_member_id, + shared_msg_id +); +CREATE INDEX idx_group_snd_item_statuses_chat_item_id ON group_snd_item_statuses( + chat_item_id +); +CREATE INDEX idx_group_snd_item_statuses_group_member_id ON group_snd_item_statuses( + group_member_id +); +CREATE INDEX idx_chat_items_user_id_item_status ON chat_items( + user_id, + item_status +); +CREATE INDEX idx_connections_to_subscribe ON connections(to_subscribe); +CREATE INDEX idx_contacts_contact_group_member_id ON contacts( + contact_group_member_id +); +CREATE INDEX idx_sent_probes_user_id ON sent_probes(user_id); +CREATE INDEX idx_sent_probes_contact_id ON sent_probes(contact_id); +CREATE INDEX idx_sent_probes_group_member_id ON sent_probes(group_member_id); +CREATE INDEX idx_sent_probe_hashes_user_id ON sent_probe_hashes(user_id); +CREATE INDEX idx_sent_probe_hashes_sent_probe_id ON sent_probe_hashes( + sent_probe_id +); +CREATE INDEX idx_sent_probe_hashes_contact_id ON sent_probe_hashes(contact_id); +CREATE INDEX idx_sent_probe_hashes_group_member_id ON sent_probe_hashes( + group_member_id +); +CREATE INDEX idx_received_probes_user_id ON received_probes(user_id); +CREATE INDEX idx_received_probes_contact_id ON received_probes(contact_id); +CREATE INDEX idx_received_probes_probe ON received_probes(probe); +CREATE INDEX idx_received_probes_probe_hash ON received_probes(probe_hash); +CREATE INDEX idx_sent_probes_created_at ON sent_probes(created_at); +CREATE INDEX idx_sent_probe_hashes_created_at ON sent_probe_hashes(created_at); +CREATE INDEX idx_received_probes_created_at ON received_probes(created_at); +CREATE INDEX idx_connections_conn_req_inv ON connections( + user_id, + conn_req_inv +); +CREATE INDEX idx_groups_via_group_link_uri_hash ON groups( + user_id, + via_group_link_uri_hash +); +CREATE INDEX idx_connections_via_contact_uri_hash ON connections( + user_id, + via_contact_uri_hash +); +CREATE INDEX idx_contact_profiles_contact_link ON contact_profiles( + user_id, + contact_link +); +CREATE INDEX idx_group_member_intros_re_group_member_id ON group_member_intros( + re_group_member_id +); +CREATE INDEX idx_group_members_invited_by_group_member_id ON group_members( + invited_by_group_member_id +); +CREATE INDEX idx_messages_author_group_member_id ON messages( + author_group_member_id +); +CREATE INDEX idx_messages_forwarded_by_group_member_id ON messages( + forwarded_by_group_member_id +); +CREATE INDEX idx_messages_group_id_shared_msg_id ON messages( + group_id, + shared_msg_id +); +CREATE INDEX idx_chat_items_forwarded_by_group_member_id ON chat_items( + forwarded_by_group_member_id +); +CREATE UNIQUE INDEX idx_remote_hosts_host_fingerprint ON remote_hosts( + host_fingerprint +); +CREATE UNIQUE INDEX idx_remote_controllers_ctrl_fingerprint ON remote_controllers( + ctrl_fingerprint +); +CREATE INDEX idx_contacts_chat_ts ON contacts(user_id, chat_ts); +CREATE INDEX idx_groups_chat_ts ON groups(user_id, chat_ts); +CREATE INDEX idx_contact_requests_updated_at ON contact_requests( + user_id, + updated_at +); +CREATE INDEX idx_connections_updated_at ON connections(user_id, updated_at); +CREATE INDEX idx_msg_deliveries_message_id ON msg_deliveries(message_id); +CREATE INDEX idx_msg_deliveries_agent_msg_id ON msg_deliveries( + connection_id, + agent_msg_id +); +CREATE INDEX chat_items_note_folder_id ON chat_items(note_folder_id); +CREATE INDEX files_note_folder_id ON files(note_folder_id); +CREATE INDEX note_folders_user_id ON note_folders(user_id); +CREATE INDEX idx_chat_items_contacts_created_at on chat_items( + user_id, + contact_id, + created_at +); +CREATE INDEX idx_chat_items_notes_created_at on chat_items( + user_id, + note_folder_id, + created_at +); +CREATE INDEX idx_files_redirect_file_id on files(redirect_file_id); +CREATE INDEX idx_chat_items_fwd_from_contact_id ON chat_items( + fwd_from_contact_id +); +CREATE INDEX idx_chat_items_fwd_from_group_id ON chat_items(fwd_from_group_id); +CREATE INDEX idx_chat_items_fwd_from_chat_item_id ON chat_items( + fwd_from_chat_item_id +); +CREATE INDEX idx_received_probes_group_member_id on received_probes( + group_member_id +); +CREATE INDEX idx_contact_requests_contact_id ON contact_requests(contact_id); +CREATE INDEX idx_operator_usage_conditions_server_operator_id ON operator_usage_conditions( + server_operator_id +); +CREATE UNIQUE INDEX idx_operator_usage_conditions_conditions_commit ON operator_usage_conditions( + conditions_commit, + server_operator_id +); +CREATE INDEX idx_chat_items_contacts ON chat_items( + user_id, + contact_id, + item_status, + created_at +); +CREATE INDEX idx_chat_items_groups ON chat_items( + user_id, + group_id, + item_status, + item_ts +); +CREATE INDEX idx_chat_items_groups_item_ts ON chat_items( + user_id, + group_id, + item_ts +); +CREATE INDEX idx_chat_items_notes ON chat_items( + user_id, + note_folder_id, + item_status, + created_at +); +CREATE INDEX idx_groups_business_xcontact_id ON groups(business_xcontact_id); +CREATE INDEX idx_chat_tags_user_id ON chat_tags(user_id); +CREATE UNIQUE INDEX idx_chat_tags_user_id_chat_tag_text ON chat_tags( + user_id, + chat_tag_text +); +CREATE UNIQUE INDEX idx_chat_tags_user_id_chat_tag_emoji ON chat_tags( + user_id, + chat_tag_emoji +); +CREATE INDEX idx_chat_tags_chats_chat_tag_id ON chat_tags_chats(chat_tag_id); +CREATE UNIQUE INDEX idx_chat_tags_chats_chat_tag_id_contact_id ON chat_tags_chats( + contact_id, + chat_tag_id +); +CREATE UNIQUE INDEX idx_chat_tags_chats_chat_tag_id_group_id ON chat_tags_chats( + group_id, + chat_tag_id +); +CREATE INDEX idx_chat_items_groups_msg_content_tag_item_ts ON chat_items( + user_id, + group_id, + msg_content_tag, + item_ts +); +CREATE INDEX idx_chat_items_groups_msg_content_tag_deleted ON chat_items( + user_id, + group_id, + msg_content_tag, + item_deleted, + item_sent +); +CREATE INDEX idx_chat_items_groups_history ON chat_items( + user_id, + group_id, + include_in_history, + item_deleted, + item_ts, + chat_item_id +); +CREATE INDEX idx_group_snd_item_statuses_chat_item_id_group_member_id ON group_snd_item_statuses( + chat_item_id, + group_member_id +); +CREATE INDEX idx_chat_item_mentions_group_id ON chat_item_mentions(group_id); +CREATE INDEX idx_chat_item_mentions_chat_item_id ON chat_item_mentions( + chat_item_id +); +CREATE UNIQUE INDEX idx_chat_item_mentions_display_name ON chat_item_mentions( + chat_item_id, + display_name +); +CREATE UNIQUE INDEX idx_chat_item_mentions_member_id ON chat_item_mentions( + chat_item_id, + member_id +); +CREATE INDEX idx_chat_items_groups_user_mention ON chat_items( + user_id, + group_id, + item_status, + user_mention +); +CREATE INDEX idx_chat_items_group_id ON chat_items(group_id); +CREATE INDEX idx_connections_group_member_id ON connections(group_member_id); +CREATE INDEX idx_chat_items_group_id_shared_msg_id ON chat_items( + group_id, + shared_msg_id +); +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20250402_short_links.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20250402_short_links.hs new file mode 100644 index 0000000000..4b3b7e9640 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20250402_short_links.hs @@ -0,0 +1,25 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20250402_short_links where + +import Data.Text (Text) +import qualified Data.Text as T +import Text.RawString.QQ (r) + +m20250402_short_links :: Text +m20250402_short_links = + T.pack + [r| +ALTER TABLE user_contact_links ADD COLUMN short_link_contact BYTEA; +ALTER TABLE connections ADD COLUMN short_link_inv BYTEA; +ALTER TABLE connections ADD COLUMN via_short_link_contact BYTEA; +|] + +down_m20250402_short_links :: Text +down_m20250402_short_links = + T.pack + [r| +ALTER TABLE user_contact_links DROP COLUMN short_link_contact; +ALTER TABLE connections DROP COLUMN short_link_inv; +ALTER TABLE connections DROP COLUMN via_short_link_contact; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20250512_member_admission.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20250512_member_admission.hs new file mode 100644 index 0000000000..eb0d73a523 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20250512_member_admission.hs @@ -0,0 +1,21 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20250512_member_admission where + +import Data.Text (Text) +import qualified Data.Text as T +import Text.RawString.QQ (r) + +m20250512_member_admission :: Text +m20250512_member_admission = + T.pack + [r| +ALTER TABLE group_profiles ADD COLUMN member_admission TEXT; +|] + +down_m20250512_member_admission :: Text +down_m20250512_member_admission = + T.pack + [r| +ALTER TABLE group_profiles DROP COLUMN member_admission; +|] diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index ce1d17859a..a7dc154d9d 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -1,5 +1,9 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} @@ -14,12 +18,12 @@ module Simplex.Chat.Store.Profiles ( AutoAccept (..), UserMsgReceiptSettings (..), UserContactLink (..), + GroupLinkInfo (..), createUserRecord, createUserRecordAt, getUsersInfo, getUsers, setActiveUser, - getSetActiveUser, getUser, getUserIdByName, getUserByAConnId, @@ -27,6 +31,7 @@ module Simplex.Chat.Store.Profiles getUserByARcvFileId, getUserByContactId, getUserByGroupId, + getUserByNoteFolderId, getUserByFileId, getUserFileInfo, deleteUserRecord, @@ -43,11 +48,23 @@ module Simplex.Chat.Store.Profiles deleteUserAddress, getUserAddress, getUserContactLinkById, + getGroupLinkInfo, getUserContactLinkByConnReq, + getUserContactLinkViaShortLink, getContactWithoutConnViaAddress, updateUserAddressAutoAccept, getProtocolServers, - overwriteProtocolServers, + insertProtocolServer, + getUpdateServerOperators, + getServerOperators, + getUserServers, + setServerOperators, + getCurrentUsageConditions, + getLatestAcceptedConditions, + setConditionsNotified, + acceptConditions, + setUserServers, + setUserServers', createCall, deleteCalls, getCalls, @@ -56,6 +73,7 @@ module Simplex.Chat.Store.Profiles deleteCommand, updateCommandStatus, getCommandDataByCorrId, + setUserUIThemes, ) where @@ -67,28 +85,40 @@ import Data.Functor (($>)) import Data.Int (Int64) import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L -import Data.Maybe (fromMaybe) +import Data.Maybe (catMaybes, fromMaybe) import Data.Text (Text) +import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Time.Clock (UTCTime (..), getCurrentTime) -import Database.SQLite.Simple (NamedParam (..), Only (..), (:.) (..)) -import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Call import Simplex.Chat.Messages +import Simplex.Chat.Operators import Simplex.Chat.Protocol import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences -import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, UserId) -import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) -import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Chat.Types.Shared +import Simplex.Chat.Types.UITheme +import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..)) +import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, ConnectionLink (..), CreatedConnLink (..), UserId) +import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) +import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) +import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Crypto as C +import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON) -import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..), SubscriptionMode) +import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode) import Simplex.Messaging.Transport.Client (TransportHost) import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8) +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (Only (..), Query, (:.) (..)) +import Database.PostgreSQL.Simple.SqlQQ (sql) +#else +import Database.SQLite.Simple (Only (..), Query, (:.) (..)) +import Database.SQLite.Simple.QQ (sql) +#endif createUserRecord :: DB.Connection -> AgentUserId -> Profile -> Bool -> ExceptT StoreError IO User createUserRecord db auId p activeUser = createUserRecordAt db auId p activeUser =<< liftIO getCurrentTime @@ -100,10 +130,11 @@ createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, image, let showNtfs = True sendRcptsContacts = True sendRcptsSmallGroups = True + order <- getNextActiveOrder db DB.execute db - "INSERT INTO users (agent_user_id, local_display_name, active_user, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, created_at, updated_at) VALUES (?,?,?,0,?,?,?,?,?)" - (auId, displayName, activeUser, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, currentTs, currentTs) + "INSERT INTO users (agent_user_id, local_display_name, active_user, active_order, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, created_at, updated_at) VALUES (?,?,?,?,0,?,?,?,?,?)" + (auId, displayName, BI activeUser, order, BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, currentTs, currentTs) userId <- insertedRowId db DB.execute db @@ -117,11 +148,12 @@ createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, image, DB.execute db "INSERT INTO contacts (contact_profile_id, local_display_name, user_id, is_user, created_at, updated_at, chat_ts) VALUES (?,?,?,?,?,?,?)" - (profileId, displayName, userId, True, currentTs, currentTs, currentTs) + (profileId, displayName, userId, BI True, currentTs, currentTs, currentTs) contactId <- insertedRowId db DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId) - pure $ toUser $ (userId, auId, contactId, profileId, activeUser, displayName, fullName, image, Nothing, userPreferences) :. (showNtfs, sendRcptsContacts, sendRcptsSmallGroups, Nothing, Nothing) + pure $ toUser $ (userId, auId, contactId, profileId, BI activeUser, order, displayName, fullName, image, Nothing, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, Nothing, Nothing, Nothing, Nothing) +-- TODO [mentions] getUsersInfo :: DB.Connection -> IO [UserInfo] getUsersInfo db = getUsers db >>= mapM getUserInfo where @@ -146,7 +178,8 @@ getUsersInfo db = getUsers db >>= mapM getUserInfo SELECT COUNT(1) FROM chat_items i JOIN groups g USING (group_id) - WHERE i.user_id = ? AND i.item_status = ? AND (g.enable_ntfs = 1 OR g.enable_ntfs IS NULL) + WHERE i.user_id = ? AND i.item_status = ? + AND (g.enable_ntfs = 1 OR g.enable_ntfs IS NULL OR (g.enable_ntfs = 2 AND i.user_mention = 1)) |] (userId, CISRcvNew) pure UserInfo {user, unreadCount = fromMaybe 0 ctCount + fromMaybe 0 gCount} @@ -155,15 +188,19 @@ getUsers :: DB.Connection -> IO [User] getUsers db = map toUser <$> DB.query_ db userQuery -setActiveUser :: DB.Connection -> UserId -> IO () -setActiveUser db userId = do +setActiveUser :: DB.Connection -> User -> IO User +setActiveUser db user@User {userId} = do DB.execute_ db "UPDATE users SET active_user = 0" - DB.execute db "UPDATE users SET active_user = 1 WHERE user_id = ?" (Only userId) + activeOrder <- getNextActiveOrder db + DB.execute db "UPDATE users SET active_user = 1, active_order = ? WHERE user_id = ?" (activeOrder, userId) + pure user {activeUser = True, activeOrder} -getSetActiveUser :: DB.Connection -> UserId -> ExceptT StoreError IO User -getSetActiveUser db userId = do - liftIO $ setActiveUser db userId - getUser db userId +getNextActiveOrder :: DB.Connection -> IO Int64 +getNextActiveOrder db = do + order <- fromMaybe 0 . join <$> maybeFirstRow fromOnly (DB.query_ db "SELECT max(active_order) FROM users") + if order == maxBound + then 0 <$ DB.execute db "UPDATE users SET active_order = active_order - ?" (Only (maxBound :: Int64)) + else pure $ order + 1 getUser :: DB.Connection -> UserId -> ExceptT StoreError IO User getUser db userId = @@ -200,6 +237,11 @@ getUserByGroupId db groupId = ExceptT . firstRow toUser (SEUserNotFoundByGroupId groupId) $ DB.query db (userQuery <> " JOIN groups g ON g.user_id = u.user_id WHERE g.group_id = ?") (Only groupId) +getUserByNoteFolderId :: DB.Connection -> NoteFolderId -> ExceptT StoreError IO User +getUserByNoteFolderId db contactId = + ExceptT . firstRow toUser (SEUserNotFoundByContactId contactId) $ + DB.query db (userQuery <> " JOIN note_folders nf ON nf.user_id = u.user_id WHERE nf.note_folder_id = ?") (Only contactId) + getUserByFileId :: DB.Connection -> FileTransferId -> ExceptT StoreError IO User getUserByFileId db fileId = ExceptT . firstRow toUser (SEUserNotFoundByFileId fileId) $ @@ -223,7 +265,7 @@ updateUserPrivacy db User {userId, showNtfs, viewPwdHash} = SET view_pwd_hash = ?, view_pwd_salt = ?, show_ntfs = ? WHERE user_id = ? |] - (hashSalt viewPwdHash :. (showNtfs, userId)) + (hashSalt viewPwdHash :. (BI showNtfs, userId)) where hashSalt = L.unzip . fmap (\UserPwdHash {hash, salt} -> (hash, salt)) @@ -232,37 +274,46 @@ updateAllContactReceipts db onOff = DB.execute db "UPDATE users SET send_rcpts_contacts = ?, send_rcpts_small_groups = ? WHERE view_pwd_hash IS NULL" - (onOff, onOff) + (BI onOff, BI onOff) updateUserContactReceipts :: DB.Connection -> User -> UserMsgReceiptSettings -> IO () updateUserContactReceipts db User {userId} UserMsgReceiptSettings {enable, clearOverrides} = do - DB.execute db "UPDATE users SET send_rcpts_contacts = ? WHERE user_id = ?" (enable, userId) + DB.execute db "UPDATE users SET send_rcpts_contacts = ? WHERE user_id = ?" (BI enable, userId) when clearOverrides $ DB.execute_ db "UPDATE contacts SET send_rcpts = NULL" updateUserGroupReceipts :: DB.Connection -> User -> UserMsgReceiptSettings -> IO () updateUserGroupReceipts db User {userId} UserMsgReceiptSettings {enable, clearOverrides} = do - DB.execute db "UPDATE users SET send_rcpts_small_groups = ? WHERE user_id = ?" (enable, userId) + DB.execute db "UPDATE users SET send_rcpts_small_groups = ? WHERE user_id = ?" (BI enable, userId) when clearOverrides $ DB.execute_ db "UPDATE groups SET send_rcpts = NULL" updateUserProfile :: DB.Connection -> User -> Profile -> ExceptT StoreError IO User updateUserProfile db user p' - | displayName == newName = do - liftIO $ updateContactProfile_ db userId profileId p' - pure user {profile, fullPreferences} + | displayName == newName = liftIO $ do + updateContactProfile_ db userId profileId p' + currentTs <- getCurrentTime + userMemberProfileUpdatedAt' <- updateUserMemberProfileUpdatedAt_ currentTs + pure user {profile, fullPreferences, userMemberProfileUpdatedAt = userMemberProfileUpdatedAt'} | otherwise = checkConstraint SEDuplicateName . liftIO $ do currentTs <- getCurrentTime DB.execute db "UPDATE users SET local_display_name = ?, updated_at = ? WHERE user_id = ?" (newName, currentTs, userId) + userMemberProfileUpdatedAt' <- updateUserMemberProfileUpdatedAt_ currentTs DB.execute db "INSERT INTO display_names (local_display_name, ldn_base, user_id, created_at, updated_at) VALUES (?,?,?,?,?)" (newName, newName, userId, currentTs, currentTs) updateContactProfile_' db userId profileId p' currentTs - updateContact_ db userId userContactId localDisplayName newName currentTs - pure user {localDisplayName = newName, profile, fullPreferences} + updateContactLDN_ db user userContactId localDisplayName newName currentTs + pure user {localDisplayName = newName, profile, fullPreferences, userMemberProfileUpdatedAt = userMemberProfileUpdatedAt'} where - User {userId, userContactId, localDisplayName, profile = LocalProfile {profileId, displayName, localAlias}} = user - Profile {displayName = newName, preferences} = p' + updateUserMemberProfileUpdatedAt_ currentTs + | userMemberProfileChanged = do + DB.execute db "UPDATE users SET user_member_profile_updated_at = ? WHERE user_id = ?" (currentTs, userId) + pure $ Just currentTs + | otherwise = pure userMemberProfileUpdatedAt + userMemberProfileChanged = newName /= displayName || newFullName /= fullName || newImage /= image + User {userId, userContactId, localDisplayName, profile = LocalProfile {profileId, displayName, fullName, image, localAlias}, userMemberProfileUpdatedAt} = user + Profile {displayName = newName, fullName = newFullName, image = newImage, preferences} = p' profile = toLocalProfile profileId p' localAlias fullPreferences = mergePreferences Nothing preferences @@ -276,11 +327,13 @@ setUserProfileContactLink db user@User {userId, profile = p@LocalProfile {profil SET contact_link = ?, updated_at = ? WHERE user_id = ? AND contact_profile_id = ? |] - (connReqContact_, ts, userId, profileId) - pure (user :: User) {profile = p {contactLink = connReqContact_}} + (contactLink, ts, userId, profileId) + pure (user :: User) {profile = p {contactLink}} where - connReqContact_ = case ucl_ of - Just UserContactLink {connReqContact} -> Just connReqContact + -- TODO [short links] this should be replaced with short links once they are supported by all clients. + -- Or, maybe, we want to allow both, when both are optional. + contactLink = case ucl_ of + Just UserContactLink {connLinkContact = CCLink cReq _} -> Just $ CLFull cReq _ -> Nothing -- only used in tests @@ -296,49 +349,51 @@ getUserContactProfiles db User {userId} = |] (Only userId) where - toContactProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, Maybe Preferences) -> Profile + toContactProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, Maybe Preferences) -> Profile toContactProfile (displayName, fullName, image, contactLink, preferences) = Profile {displayName, fullName, image, contactLink, preferences} -createUserContactLink :: DB.Connection -> User -> ConnId -> ConnReqContact -> SubscriptionMode -> ExceptT StoreError IO () -createUserContactLink db User {userId} agentConnId cReq subMode = +createUserContactLink :: DB.Connection -> User -> ConnId -> CreatedLinkContact -> SubscriptionMode -> ExceptT StoreError IO () +createUserContactLink db User {userId} agentConnId (CCLink cReq shortLink) subMode = checkConstraint SEDuplicateContactLink . liftIO $ do currentTs <- getCurrentTime DB.execute db - "INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?)" - (userId, cReq, currentTs, currentTs) + "INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, created_at, updated_at) VALUES (?,?,?,?,?)" + (userId, cReq, shortLink, currentTs, currentTs) userContactLinkId <- insertedRowId db - void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode + void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff -getUserAddressConnections :: DB.Connection -> User -> ExceptT StoreError IO [Connection] -getUserAddressConnections db User {userId} = do +getUserAddressConnections :: DB.Connection -> VersionRangeChat -> User -> ExceptT StoreError IO [Connection] +getUserAddressConnections db vr User {userId} = do cs <- liftIO getUserAddressConnections_ if null cs then throwError SEUserContactLinkNotFound else pure cs where getUserAddressConnections_ :: IO [Connection] getUserAddressConnections_ = - map toConnection + map (toConnection vr) <$> DB.query db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, - c.peer_chat_min_version, c.peer_chat_max_version + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM connections c JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL |] (userId, userId) -getUserContactLinks :: DB.Connection -> User -> IO [(Connection, UserContact)] -getUserContactLinks db User {userId} = +getUserContactLinks :: DB.Connection -> VersionRangeChat -> User -> IO [(Connection, UserContact)] +getUserContactLinks db vr User {userId} = map toUserContactConnection <$> DB.query db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, - c.peer_chat_min_version, c.peer_chat_max_version, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, uc.user_contact_link_id, uc.conn_req_contact, uc.group_id FROM connections c JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id @@ -347,7 +402,7 @@ getUserContactLinks db User {userId} = (userId, userId) where toUserContactConnection :: (ConnectionRow :. (Int64, ConnReqContact, Maybe GroupId)) -> (Connection, UserContact) - toUserContactConnection (connRow :. (userContactLinkId, connReqContact, groupId)) = (toConnection connRow, UserContact {userContactLinkId, connReqContact, groupId}) + toUserContactConnection (connRow :. (userContactLinkId, connReqContact, groupId)) = (toConnection vr connRow, UserContact {userContactLinkId, connReqContact, groupId}) deleteUserAddress :: DB.Connection -> User -> IO () deleteUserAddress db user@User {userId} = do @@ -362,20 +417,21 @@ deleteUserAddress db user@User {userId} = do ) |] (Only userId) - DB.executeNamed + DB.execute db [sql| DELETE FROM display_names - WHERE user_id = :user_id + WHERE user_id = ? AND local_display_name in ( SELECT cr.local_display_name FROM contact_requests cr JOIN user_contact_links uc USING (user_contact_link_id) - WHERE uc.user_id = :user_id AND uc.local_display_name = '' AND uc.group_id IS NULL + WHERE uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL ) + AND local_display_name NOT IN (SELECT local_display_name FROM users WHERE user_id = ?) |] - [":user_id" := userId] - DB.executeNamed + (userId, userId, userId) + DB.execute db [sql| DELETE FROM contact_profiles @@ -383,10 +439,10 @@ deleteUserAddress db user@User {userId} = do SELECT cr.contact_profile_id FROM contact_requests cr JOIN user_contact_links uc USING (user_contact_link_id) - WHERE uc.user_id = :user_id AND uc.local_display_name = '' AND uc.group_id IS NULL + WHERE uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL ) |] - [":user_id" := userId] + (Only userId) void $ setUserProfileContactLink db user Nothing DB.execute db "DELETE FROM user_contact_links WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL" (Only userId) @@ -397,13 +453,20 @@ data UserMsgReceiptSettings = UserMsgReceiptSettings deriving (Show) data UserContactLink = UserContactLink - { connReqContact :: ConnReqContact, + { connLinkContact :: CreatedLinkContact, autoAccept :: Maybe AutoAccept } deriving (Show) +data GroupLinkInfo = GroupLinkInfo + { groupId :: GroupId, + memberRole :: GroupMemberRole + } + deriving (Show) + data AutoAccept = AutoAccept - { acceptIncognito :: IncognitoEnabled, + { businessAddress :: Bool, -- possibly, it can be wrapped together with acceptIncognito, or AutoAccept made sum type + acceptIncognito :: IncognitoEnabled, autoReply :: Maybe MsgContent } deriving (Show) @@ -412,50 +475,64 @@ $(J.deriveJSON defaultJSON ''AutoAccept) $(J.deriveJSON defaultJSON ''UserContactLink) -toUserContactLink :: (ConnReqContact, Bool, IncognitoEnabled, Maybe MsgContent) -> UserContactLink -toUserContactLink (connReq, autoAccept, acceptIncognito, autoReply) = - UserContactLink connReq $ - if autoAccept then Just AutoAccept {acceptIncognito, autoReply} else Nothing +toUserContactLink :: (ConnReqContact, Maybe ShortLinkContact, BoolInt, BoolInt, BoolInt, Maybe MsgContent) -> UserContactLink +toUserContactLink (connReq, shortLink, BI autoAccept, BI businessAddress, BI acceptIncognito, autoReply) = + UserContactLink (CCLink connReq shortLink) $ + if autoAccept then Just AutoAccept {businessAddress, acceptIncognito, autoReply} else Nothing getUserAddress :: DB.Connection -> User -> ExceptT StoreError IO UserContactLink getUserAddress db User {userId} = ExceptT . firstRow toUserContactLink SEUserContactLinkNotFound $ - DB.query - db - [sql| - SELECT conn_req_contact, auto_accept, auto_accept_incognito, auto_reply_msg_content - FROM user_contact_links - WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL - |] - (Only userId) + DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL") (Only userId) -getUserContactLinkById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO (UserContactLink, Maybe GroupId, GroupMemberRole) +getUserContactLinkById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO (UserContactLink, Maybe GroupLinkInfo) getUserContactLinkById db userId userContactLinkId = - ExceptT . firstRow (\(ucl :. (groupId_, mRole_)) -> (toUserContactLink ucl, groupId_, fromMaybe GRMember mRole_)) SEUserContactLinkNotFound $ + ExceptT . firstRow (\(ucl :. gli) -> (toUserContactLink ucl, toGroupLinkInfo gli)) SEUserContactLinkNotFound $ DB.query db [sql| - SELECT conn_req_contact, auto_accept, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role + SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role FROM user_contact_links - WHERE user_id = ? - AND user_contact_link_id = ? + WHERE user_id = ? AND user_contact_link_id = ? |] (userId, userContactLinkId) +toGroupLinkInfo :: (Maybe GroupId, Maybe GroupMemberRole) -> Maybe GroupLinkInfo +toGroupLinkInfo (groupId_, mRole_) = + (\groupId -> GroupLinkInfo {groupId, memberRole = fromMaybe GRMember mRole_}) + <$> groupId_ + +getGroupLinkInfo :: DB.Connection -> UserId -> GroupId -> IO (Maybe GroupLinkInfo) +getGroupLinkInfo db userId groupId = + fmap join $ maybeFirstRow toGroupLinkInfo $ + DB.query + db + [sql| + SELECT group_id, group_link_member_role + FROM user_contact_links + WHERE user_id = ? AND group_id = ? + |] + (userId, groupId) + getUserContactLinkByConnReq :: DB.Connection -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe UserContactLink) getUserContactLinkByConnReq db User {userId} (cReqSchema1, cReqSchema2) = maybeFirstRow toUserContactLink $ - DB.query - db - [sql| - SELECT conn_req_contact, auto_accept, auto_accept_incognito, auto_reply_msg_content - FROM user_contact_links - WHERE user_id = ? AND conn_req_contact IN (?,?) - |] - (userId, cReqSchema1, cReqSchema2) + DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND conn_req_contact IN (?,?)") (userId, cReqSchema1, cReqSchema2) -getContactWithoutConnViaAddress :: DB.Connection -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe Contact) -getContactWithoutConnViaAddress db user@User {userId} (cReqSchema1, cReqSchema2) = do +getUserContactLinkViaShortLink :: DB.Connection -> User -> ShortLinkContact -> IO (Maybe UserContactLink) +getUserContactLinkViaShortLink db User {userId} shortLink = + maybeFirstRow toUserContactLink $ + DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND short_link_contact = ?") (userId, shortLink) + +userContactLinkQuery :: Query +userContactLinkQuery = + [sql| + SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content + FROM user_contact_links + |] + +getContactWithoutConnViaAddress :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe Contact) +getContactWithoutConnViaAddress db vr user@User {userId} (cReqSchema1, cReqSchema2) = do ctId_ <- maybeFirstRow fromOnly $ DB.query @@ -468,7 +545,7 @@ getContactWithoutConnViaAddress db user@User {userId} (cReqSchema1, cReqSchema2) WHERE cp.user_id = ? AND cp.contact_link IN (?,?) AND c.connection_id IS NULL |] (userId, cReqSchema1, cReqSchema2) - maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db user) ctId_ + maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db vr user) ctId_ updateUserAddressAutoAccept :: DB.Connection -> User -> Maybe AutoAccept -> ExceptT StoreError IO UserContactLink updateUserAddressAutoAccept db user@User {userId} autoAccept = do @@ -480,63 +557,318 @@ updateUserAddressAutoAccept db user@User {userId} autoAccept = do db [sql| UPDATE user_contact_links - SET auto_accept = ?, auto_accept_incognito = ?, auto_reply_msg_content = ? + SET auto_accept = ?, business_address = ?, auto_accept_incognito = ?, auto_reply_msg_content = ? WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL |] (ucl :. Only userId) ucl = case autoAccept of - Just AutoAccept {acceptIncognito, autoReply} -> (True, acceptIncognito, autoReply) - _ -> (False, False, Nothing) + Just AutoAccept {businessAddress, acceptIncognito, autoReply} -> (BI True, BI businessAddress, BI acceptIncognito, autoReply) + _ -> (BI False, BI False, BI False, Nothing) -getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> IO [ServerCfg p] -getProtocolServers db User {userId} = - map toServerCfg +getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> IO [UserServer p] +getProtocolServers db p User {userId} = + map toUserServer <$> DB.query db [sql| - SELECT host, port, key_hash, basic_auth, preset, tested, enabled + SELECT smp_server_id, host, port, key_hash, basic_auth, preset, tested, enabled FROM protocol_servers - WHERE user_id = ? AND protocol = ?; + WHERE user_id = ? AND protocol = ? |] - (userId, decodeLatin1 $ strEncode protocol) + (userId, decodeLatin1 $ strEncode p) where - protocol = protocolTypeI @p - toServerCfg :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Bool, Maybe Bool, Bool) -> ServerCfg p - toServerCfg (host, port, keyHash, auth_, preset, tested, enabled) = - let server = ProtoServerWithAuth (ProtocolServer protocol host port keyHash) (BasicAuth . encodeUtf8 <$> auth_) - in ServerCfg {server, preset, tested, enabled} + toUserServer :: (DBEntityId, NonEmpty TransportHost, String, C.KeyHash, Maybe Text, BoolInt, Maybe BoolInt, BoolInt) -> UserServer p + toUserServer (serverId, host, port, keyHash, auth_, BI preset, tested, BI enabled) = + let server = ProtoServerWithAuth (ProtocolServer p host port keyHash) (BasicAuth . encodeUtf8 <$> auth_) + in UserServer {serverId, server, preset, tested = unBI <$> tested, enabled, deleted = False} -overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO () -overwriteProtocolServers db User {userId} servers = - checkConstraint SEUniqueID . ExceptT $ do - currentTs <- getCurrentTime - DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND protocol = ? " (userId, protocol) - forM_ servers $ \ServerCfg {server, preset, tested, enabled} -> do - let ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_ = server +insertProtocolServer :: forall p. ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> UTCTime -> NewUserServer p -> IO (UserServer p) +insertProtocolServer db p User {userId} ts srv@UserServer {server, preset, tested, enabled} = do + DB.execute + db + [sql| + INSERT INTO protocol_servers + (protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?) + |] + (serverColumns p server :. (BI preset, BI <$> tested, BI enabled, userId, ts, ts)) + sId <- insertedRowId db + pure (srv :: NewUserServer p) {serverId = DBEntityId sId} + +updateProtocolServer :: ProtocolTypeI p => DB.Connection -> SProtocolType p -> UTCTime -> UserServer p -> IO () +updateProtocolServer db p ts UserServer {serverId, server, preset, tested, enabled} = + DB.execute + db + [sql| + UPDATE protocol_servers + SET protocol = ?, host = ?, port = ?, key_hash = ?, basic_auth = ?, + preset = ?, tested = ?, enabled = ?, updated_at = ? + WHERE smp_server_id = ? + |] + (serverColumns p server :. (BI preset, BI <$> tested, BI enabled, ts, serverId)) + +serverColumns :: ProtocolTypeI p => SProtocolType p -> ProtoServerWithAuth p -> (Text, NonEmpty TransportHost, String, C.KeyHash, Maybe Text) +serverColumns p (ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_) = + let protocol = decodeLatin1 $ strEncode p + auth = safeDecodeUtf8 . unBasicAuth <$> auth_ + in (protocol, host, port, keyHash, auth) + +getServerOperators :: DB.Connection -> ExceptT StoreError IO ServerOperatorConditions +getServerOperators db = do + currentConditions <- getCurrentUsageConditions db + liftIO $ do + now <- getCurrentTime + latestAcceptedConds_ <- getLatestAcceptedConditions db + let getConds op = (\ca -> op {conditionsAcceptance = ca}) <$> getOperatorConditions_ db op currentConditions latestAcceptedConds_ now + ops <- mapM getConds =<< getServerOperators_ db + let conditionsAction = usageConditionsAction ops currentConditions now + pure ServerOperatorConditions {serverOperators = ops, currentConditions, conditionsAction} + +getUserServers :: DB.Connection -> User -> ExceptT StoreError IO ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) +getUserServers db user = + (,,) + <$> (map Just . serverOperators <$> getServerOperators db) + <*> liftIO (getProtocolServers db SPSMP user) + <*> liftIO (getProtocolServers db SPXFTP user) + +setServerOperators :: DB.Connection -> NonEmpty ServerOperator -> IO () +setServerOperators db ops = do + currentTs <- getCurrentTime + mapM_ (updateServerOperator db currentTs) ops + +updateServerOperator :: DB.Connection -> UTCTime -> ServerOperator -> IO () +updateServerOperator db currentTs ServerOperator {operatorId, enabled, smpRoles, xftpRoles} = + DB.execute + db + [sql| + UPDATE server_operators + SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ? + WHERE server_operator_id = ? + |] + (BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), currentTs, operatorId) + +getUpdateServerOperators :: DB.Connection -> NonEmpty PresetOperator -> Bool -> IO [(Maybe PresetOperator, Maybe ServerOperator)] +getUpdateServerOperators db presetOps newUser = do + conds <- map toUsageConditions <$> DB.query_ db usageCondsQuery + now <- getCurrentTime + let (currentConds, condsToAdd) = usageConditionsToAdd newUser now conds + mapM_ insertConditions condsToAdd + latestAcceptedConds_ <- getLatestAcceptedConditions db + ops <- updatedServerOperators presetOps <$> getServerOperators_ db + forM ops $ traverse $ mapM $ \(ASO _ op) -> + -- traverse for tuple, mapM for Maybe + case operatorId op of + DBNewEntity -> insertOperator op + DBEntityId _ -> do + updateOperator op + getOperatorConditions_ db op currentConds latestAcceptedConds_ now >>= \case + CARequired (Just ts) | ts < now -> autoAcceptConditions op currentConds now + ca -> pure op {conditionsAcceptance = ca} + where + insertConditions UsageConditions {conditionsId, conditionsCommit, notifiedAt, createdAt} = DB.execute db [sql| - INSERT INTO protocol_servers - (protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?) + INSERT INTO usage_conditions + (usage_conditions_id, conditions_commit, notified_at, created_at) + VALUES (?,?,?,?) |] - ((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_) :. (preset, tested, enabled, userId, currentTs, currentTs)) - pure $ Right () + (conditionsId, conditionsCommit, notifiedAt, createdAt) + updateOperator :: ServerOperator -> IO () + updateOperator ServerOperator {operatorId, tradeName, legalName, serverDomains, enabled, smpRoles, xftpRoles} = + DB.execute + db + [sql| + UPDATE server_operators + SET trade_name = ?, legal_name = ?, server_domains = ?, enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, xftp_role_storage = ?, xftp_role_proxy = ? + WHERE server_operator_id = ? + |] + (tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), operatorId) + insertOperator :: NewServerOperator -> IO ServerOperator + insertOperator op@ServerOperator {operatorTag, tradeName, legalName, serverDomains, enabled, smpRoles, xftpRoles} = do + DB.execute + db + [sql| + INSERT INTO server_operators + (server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy) + VALUES (?,?,?,?,?,?,?,?,?) + |] + (operatorTag, tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles)) + opId <- insertedRowId db + pure op {operatorId = DBEntityId opId} + autoAcceptConditions op UsageConditions {conditionsCommit} now = + acceptConditions_ db op conditionsCommit now True + $> op {conditionsAcceptance = CAAccepted (Just now) True} + +serverOperatorQuery :: Query +serverOperatorQuery = + [sql| + SELECT server_operator_id, server_operator_tag, trade_name, legal_name, + server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy + FROM server_operators + |] + +getServerOperators_ :: DB.Connection -> IO [ServerOperator] +getServerOperators_ db = map toServerOperator <$> DB.query_ db serverOperatorQuery + +toServerOperator :: (DBEntityId, Maybe OperatorTag, Text, Maybe Text, Text, BoolInt) :. (BoolInt, BoolInt) :. (BoolInt, BoolInt) -> ServerOperator +toServerOperator ((operatorId, operatorTag, tradeName, legalName, domains, BI enabled) :. smpRoles' :. xftpRoles') = + ServerOperator + { operatorId, + operatorTag, + tradeName, + legalName, + serverDomains = T.splitOn "," domains, + conditionsAcceptance = CARequired Nothing, + enabled, + smpRoles = serverRoles smpRoles', + xftpRoles = serverRoles xftpRoles' + } where - protocol = decodeLatin1 $ strEncode $ protocolTypeI @p + serverRoles (BI storage, BI proxy) = ServerRoles {storage, proxy} + +getOperatorConditions_ :: DB.Connection -> ServerOperator -> UsageConditions -> Maybe UsageConditions -> UTCTime -> IO ConditionsAcceptance +getOperatorConditions_ db ServerOperator {operatorId} UsageConditions {conditionsCommit = currentCommit, createdAt, notifiedAt} latestAcceptedConds_ now = do + case latestAcceptedConds_ of + Nothing -> pure $ CARequired Nothing -- no conditions accepted by any operator + Just UsageConditions {conditionsCommit = latestAcceptedCommit} -> do + operatorAcceptedConds_ <- + maybeFirstRow id $ + DB.query + db + [sql| + SELECT conditions_commit, accepted_at, auto_accepted + FROM operator_usage_conditions + WHERE server_operator_id = ? + ORDER BY operator_usage_conditions_id DESC + LIMIT 1 + |] + (Only operatorId) + pure $ case operatorAcceptedConds_ of + Just (operatorCommit, acceptedAt_, BI autoAccept) + | operatorCommit /= latestAcceptedCommit -> CARequired Nothing -- TODO should we consider this operator disabled? + | currentCommit /= latestAcceptedCommit -> CARequired $ conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) + | otherwise -> CAAccepted acceptedAt_ autoAccept + _ -> CARequired Nothing -- no conditions were accepted for this operator + +getCurrentUsageConditions :: DB.Connection -> ExceptT StoreError IO UsageConditions +getCurrentUsageConditions db = + ExceptT . firstRow toUsageConditions SEUsageConditionsNotFound $ + DB.query_ db (usageCondsQuery <> " DESC LIMIT 1") + +usageCondsQuery :: Query +usageCondsQuery = + [sql| + SELECT usage_conditions_id, conditions_commit, notified_at, created_at + FROM usage_conditions + ORDER BY usage_conditions_id + |] + +toUsageConditions :: (Int64, Text, Maybe UTCTime, UTCTime) -> UsageConditions +toUsageConditions (conditionsId, conditionsCommit, notifiedAt, createdAt) = + UsageConditions {conditionsId, conditionsCommit, notifiedAt, createdAt} + +getLatestAcceptedConditions :: DB.Connection -> IO (Maybe UsageConditions) +getLatestAcceptedConditions db = + maybeFirstRow toUsageConditions $ + DB.query_ + db + [sql| + SELECT usage_conditions_id, conditions_commit, notified_at, created_at + FROM usage_conditions + WHERE conditions_commit = ( + SELECT conditions_commit + FROM operator_usage_conditions + ORDER BY accepted_at DESC + LIMIT 1 + ) + |] + +setConditionsNotified :: DB.Connection -> Int64 -> UTCTime -> IO () +setConditionsNotified db condId notifiedAt = + DB.execute db "UPDATE usage_conditions SET notified_at = ? WHERE usage_conditions_id = ?" (notifiedAt, condId) + +acceptConditions :: DB.Connection -> Int64 -> NonEmpty Int64 -> UTCTime -> ExceptT StoreError IO () +acceptConditions db condId opIds acceptedAt = do + UsageConditions {conditionsCommit} <- getUsageConditionsById_ db condId + operators <- mapM getServerOperator_ opIds + liftIO $ forM_ operators $ \op -> acceptConditions_ db op conditionsCommit acceptedAt False + where + getServerOperator_ opId = + ExceptT $ + firstRow toServerOperator (SEOperatorNotFound opId) $ + DB.query db (serverOperatorQuery <> " WHERE server_operator_id = ?") (Only opId) + +acceptConditions_ :: DB.Connection -> ServerOperator -> Text -> UTCTime -> Bool -> IO () +acceptConditions_ db ServerOperator {operatorId, operatorTag} conditionsCommit acceptedAt autoAccepted = do + acceptedAt_ :: Maybe (Maybe UTCTime) <- maybeFirstRow fromOnly $ DB.query db "SELECT accepted_at FROM operator_usage_conditions WHERE server_operator_id = ? AND conditions_commit = ?" (operatorId, conditionsCommit) + case acceptedAt_ of + Just Nothing -> + DB.execute + db + (q <> "ON CONFLICT (server_operator_id, conditions_commit) DO UPDATE SET accepted_at = ?, auto_accepted = ?") + (operatorId, operatorTag, conditionsCommit, acceptedAt, BI autoAccepted, acceptedAt, BI autoAccepted) + Just (Just _) -> + DB.execute + db + (q <> "ON CONFLICT (server_operator_id, conditions_commit) DO NOTHING") + (operatorId, operatorTag, conditionsCommit, acceptedAt, BI autoAccepted) + Nothing -> + DB.execute + db + q + (operatorId, operatorTag, conditionsCommit, acceptedAt, BI autoAccepted) + where + q = + [sql| + INSERT INTO operator_usage_conditions + (server_operator_id, server_operator_tag, conditions_commit, accepted_at, auto_accepted) + VALUES (?,?,?,?,?) + |] + +getUsageConditionsById_ :: DB.Connection -> Int64 -> ExceptT StoreError IO UsageConditions +getUsageConditionsById_ db conditionsId = + ExceptT . firstRow toUsageConditions SEUsageConditionsNotFound $ + DB.query + db + [sql| + SELECT usage_conditions_id, conditions_commit, notified_at, created_at + FROM usage_conditions + WHERE usage_conditions_id = ? + |] + (Only conditionsId) + +setUserServers :: DB.Connection -> User -> UTCTime -> UpdatedUserOperatorServers -> ExceptT StoreError IO UserOperatorServers +setUserServers db user ts = checkConstraint SEUniqueID . liftIO . setUserServers' db user ts + +setUserServers' :: DB.Connection -> User -> UTCTime -> UpdatedUserOperatorServers -> IO UserOperatorServers +setUserServers' db user@User {userId} ts UpdatedUserOperatorServers {operator, smpServers, xftpServers} = do + mapM_ (updateServerOperator db ts) operator + smpSrvs' <- catMaybes <$> mapM (upsertOrDelete SPSMP) smpServers + xftpSrvs' <- catMaybes <$> mapM (upsertOrDelete SPXFTP) xftpServers + pure UserOperatorServers {operator, smpServers = smpSrvs', xftpServers = xftpSrvs'} + where + upsertOrDelete :: ProtocolTypeI p => SProtocolType p -> AUserServer p -> IO (Maybe (UserServer p)) + upsertOrDelete p (AUS _ s@UserServer {serverId, deleted}) = case serverId of + DBNewEntity + | deleted -> pure Nothing + | otherwise -> Just <$> insertProtocolServer db p user ts s + DBEntityId srvId + | deleted -> Nothing <$ DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND smp_server_id = ? AND preset = ?" (userId, srvId, BI False) + | otherwise -> Just s <$ updateProtocolServer db p ts s createCall :: DB.Connection -> User -> Call -> UTCTime -> IO () -createCall db user@User {userId} Call {contactId, callId, chatItemId, callState} callTs = do +createCall db user@User {userId} Call {contactId, callId, callUUID, chatItemId, callState} callTs = do currentTs <- getCurrentTime deleteCalls db user contactId DB.execute db [sql| INSERT INTO calls - (contact_id, shared_call_id, chat_item_id, call_state, call_ts, user_id, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?) + (contact_id, shared_call_id, call_uuid, chat_item_id, call_state, call_ts, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?) |] - (contactId, callId, chatItemId, callState, callTs, userId, currentTs, currentTs) + (contactId, callId, callUUID, chatItemId, callState, callTs, userId, currentTs, currentTs) deleteCalls :: DB.Connection -> User -> ContactId -> IO () deleteCalls db User {userId} contactId = do @@ -549,13 +881,13 @@ getCalls db = db [sql| SELECT - contact_id, shared_call_id, chat_item_id, call_state, call_ts + contact_id, shared_call_id, call_uuid, chat_item_id, call_state, call_ts FROM calls ORDER BY call_ts ASC |] where - toCall :: (ContactId, CallId, ChatItemId, CallState, UTCTime) -> Call - toCall (contactId, callId, chatItemId, callState, callTs) = Call {contactId, callId, chatItemId, callState, callTs} + toCall :: (ContactId, CallId, Text, ChatItemId, CallState, UTCTime) -> Call + toCall (contactId, callId, callUUID, chatItemId, callState, callTs) = Call {contactId, callId, callUUID, chatItemId, callState, callTs} createCommand :: DB.Connection -> User -> Maybe Int64 -> CommandFunction -> IO CommandId createCommand db User {userId} connId commandFunction = do @@ -599,3 +931,8 @@ getCommandDataByCorrId db User {userId} corrId = where toCommandData :: (CommandId, Maybe Int64, CommandFunction, CommandStatus) -> CommandData toCommandData (cmdId, cmdConnId, cmdFunction, cmdStatus) = CommandData {cmdId, cmdConnId, cmdFunction, cmdStatus} + +setUserUIThemes :: DB.Connection -> User -> Maybe UIThemeEntityOverrides -> IO () +setUserUIThemes db User {userId} uiThemes = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE users SET ui_themes = ?, updated_at = ? WHERE user_id = ?" (uiThemes, updatedAt, userId) diff --git a/src/Simplex/Chat/Store/Remote.hs b/src/Simplex/Chat/Store/Remote.hs index a88d87a04e..4921369b10 100644 --- a/src/Simplex/Chat/Store/Remote.hs +++ b/src/Simplex/Chat/Store/Remote.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} @@ -8,19 +9,23 @@ module Simplex.Chat.Store.Remote where import Control.Monad.Except import Data.Int (Int64) import Data.Text (Text) -import Data.Text.Encoding (encodeUtf8, decodeASCII) +import Data.Text.Encoding (decodeASCII, encodeUtf8) import Data.Word (Word16) -import Database.SQLite.Simple (Only (..)) -import qualified Database.SQLite.Simple as SQL -import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Remote.Types import Simplex.Chat.Store.Shared -import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) -import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) +import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String (StrEncoding (..)) import Simplex.RemoteControl.Types import UnliftIO +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (Only (..), Query) +import Database.PostgreSQL.Simple.SqlQQ (sql) +#else +import Database.SQLite.Simple (Only (..), Query) +import Database.SQLite.Simple.QQ (sql) +#endif insertRemoteHost :: DB.Connection -> Text -> FilePath -> Maybe RCCtrlAddress -> Maybe Word16 -> RCHostPairing -> ExceptT StoreError IO RemoteHostId insertRemoteHost db hostDeviceName storePath rcAddr_ bindPort_ RCHostPairing {caKey, caCert, idPrivKey, knownHost = kh_} = do @@ -54,7 +59,7 @@ getRemoteHostByFingerprint db fingerprint = maybeFirstRow toRemoteHost $ DB.query db (remoteHostQuery <> " WHERE host_fingerprint = ?") (Only fingerprint) -remoteHostQuery :: SQL.Query +remoteHostQuery :: Query remoteHostQuery = [sql| SELECT remote_host_id, host_device_name, store_path, ca_key, ca_cert, id_key, host_fingerprint, host_dh_pub, bind_iface, bind_addr, bind_port @@ -117,7 +122,7 @@ getRemoteCtrlByFingerprint db fingerprint = maybeFirstRow toRemoteCtrl $ DB.query db (remoteCtrlQuery <> " WHERE ctrl_fingerprint = ?") (Only fingerprint) -remoteCtrlQuery :: SQL.Query +remoteCtrlQuery :: Query remoteCtrlQuery = [sql| SELECT remote_ctrl_id, ctrl_device_name, ca_key, ca_cert, ctrl_fingerprint, id_pub, dh_priv_key, prev_dh_priv_key diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs new file mode 100644 index 0000000000..183d699f01 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -0,0 +1,269 @@ +{-# LANGUAGE NamedFieldPuns #-} + +module Simplex.Chat.Store.SQLite.Migrations (migrations) where + +import Data.List (sortOn) +import Database.SQLite.Simple (Query (..)) +import Simplex.Chat.Store.SQLite.Migrations.M20220101_initial +import Simplex.Chat.Store.SQLite.Migrations.M20220122_v1_1 +import Simplex.Chat.Store.SQLite.Migrations.M20220205_chat_item_status +import Simplex.Chat.Store.SQLite.Migrations.M20220210_deduplicate_contact_requests +import Simplex.Chat.Store.SQLite.Migrations.M20220224_messages_fks +import Simplex.Chat.Store.SQLite.Migrations.M20220301_smp_servers +import Simplex.Chat.Store.SQLite.Migrations.M20220302_profile_images +import Simplex.Chat.Store.SQLite.Migrations.M20220304_msg_quotes +import Simplex.Chat.Store.SQLite.Migrations.M20220321_chat_item_edited +import Simplex.Chat.Store.SQLite.Migrations.M20220404_files_status_fields +import Simplex.Chat.Store.SQLite.Migrations.M20220514_profiles_user_id +import Simplex.Chat.Store.SQLite.Migrations.M20220626_auto_reply +import Simplex.Chat.Store.SQLite.Migrations.M20220702_calls +import Simplex.Chat.Store.SQLite.Migrations.M20220715_groups_chat_item_id +import Simplex.Chat.Store.SQLite.Migrations.M20220811_chat_items_indices +import Simplex.Chat.Store.SQLite.Migrations.M20220812_incognito_profiles +import Simplex.Chat.Store.SQLite.Migrations.M20220818_chat_notifications +import Simplex.Chat.Store.SQLite.Migrations.M20220822_groups_host_conn_custom_user_profile_id +import Simplex.Chat.Store.SQLite.Migrations.M20220823_delete_broken_group_event_chat_items +import Simplex.Chat.Store.SQLite.Migrations.M20220824_profiles_local_alias +import Simplex.Chat.Store.SQLite.Migrations.M20220909_commands +import Simplex.Chat.Store.SQLite.Migrations.M20220926_connection_alias +import Simplex.Chat.Store.SQLite.Migrations.M20220928_settings +import Simplex.Chat.Store.SQLite.Migrations.M20221001_shared_msg_id_indices +import Simplex.Chat.Store.SQLite.Migrations.M20221003_delete_broken_integrity_error_chat_items +import Simplex.Chat.Store.SQLite.Migrations.M20221004_idx_msg_deliveries_message_id +import Simplex.Chat.Store.SQLite.Migrations.M20221011_user_contact_links_group_id +import Simplex.Chat.Store.SQLite.Migrations.M20221012_inline_files +import Simplex.Chat.Store.SQLite.Migrations.M20221019_unread_chat +import Simplex.Chat.Store.SQLite.Migrations.M20221021_auto_accept__group_links +import Simplex.Chat.Store.SQLite.Migrations.M20221024_contact_used +import Simplex.Chat.Store.SQLite.Migrations.M20221025_chat_settings +import Simplex.Chat.Store.SQLite.Migrations.M20221029_group_link_id +import Simplex.Chat.Store.SQLite.Migrations.M20221112_server_password +import Simplex.Chat.Store.SQLite.Migrations.M20221115_server_cfg +import Simplex.Chat.Store.SQLite.Migrations.M20221129_delete_group_feature_items +import Simplex.Chat.Store.SQLite.Migrations.M20221130_delete_item_deleted +import Simplex.Chat.Store.SQLite.Migrations.M20221209_verified_connection +import Simplex.Chat.Store.SQLite.Migrations.M20221210_idxs +import Simplex.Chat.Store.SQLite.Migrations.M20221211_group_description +import Simplex.Chat.Store.SQLite.Migrations.M20221212_chat_items_timed +import Simplex.Chat.Store.SQLite.Migrations.M20221214_live_message +import Simplex.Chat.Store.SQLite.Migrations.M20221222_chat_ts +import Simplex.Chat.Store.SQLite.Migrations.M20221223_idx_chat_items_item_status +import Simplex.Chat.Store.SQLite.Migrations.M20221230_idxs +import Simplex.Chat.Store.SQLite.Migrations.M20230107_connections_auth_err_counter +import Simplex.Chat.Store.SQLite.Migrations.M20230111_users_agent_user_id +import Simplex.Chat.Store.SQLite.Migrations.M20230117_fkey_indexes +import Simplex.Chat.Store.SQLite.Migrations.M20230118_recreate_smp_servers +import Simplex.Chat.Store.SQLite.Migrations.M20230129_drop_chat_items_group_idx +import Simplex.Chat.Store.SQLite.Migrations.M20230206_item_deleted_by_group_member_id +import Simplex.Chat.Store.SQLite.Migrations.M20230303_group_link_role +import Simplex.Chat.Store.SQLite.Migrations.M20230317_hidden_profiles +import Simplex.Chat.Store.SQLite.Migrations.M20230318_file_description +import Simplex.Chat.Store.SQLite.Migrations.M20230321_agent_file_deleted +import Simplex.Chat.Store.SQLite.Migrations.M20230328_files_protocol +import Simplex.Chat.Store.SQLite.Migrations.M20230402_protocol_servers +import Simplex.Chat.Store.SQLite.Migrations.M20230411_extra_xftp_file_descriptions +import Simplex.Chat.Store.SQLite.Migrations.M20230420_rcv_files_to_receive +import Simplex.Chat.Store.SQLite.Migrations.M20230422_profile_contact_links +import Simplex.Chat.Store.SQLite.Migrations.M20230504_recreate_msg_delivery_events_cleanup_messages +import Simplex.Chat.Store.SQLite.Migrations.M20230505_chat_item_versions +import Simplex.Chat.Store.SQLite.Migrations.M20230511_reactions +import Simplex.Chat.Store.SQLite.Migrations.M20230519_item_deleted_ts +import Simplex.Chat.Store.SQLite.Migrations.M20230526_indexes +import Simplex.Chat.Store.SQLite.Migrations.M20230529_indexes +import Simplex.Chat.Store.SQLite.Migrations.M20230608_deleted_contacts +import Simplex.Chat.Store.SQLite.Migrations.M20230618_favorite_chats +import Simplex.Chat.Store.SQLite.Migrations.M20230621_chat_item_moderations +import Simplex.Chat.Store.SQLite.Migrations.M20230705_delivery_receipts +import Simplex.Chat.Store.SQLite.Migrations.M20230721_group_snd_item_statuses +import Simplex.Chat.Store.SQLite.Migrations.M20230814_indexes +import Simplex.Chat.Store.SQLite.Migrations.M20230827_file_encryption +import Simplex.Chat.Store.SQLite.Migrations.M20230829_connections_chat_vrange +import Simplex.Chat.Store.SQLite.Migrations.M20230903_connections_to_subscribe +import Simplex.Chat.Store.SQLite.Migrations.M20230913_member_contacts +import Simplex.Chat.Store.SQLite.Migrations.M20230914_member_probes +import Simplex.Chat.Store.SQLite.Migrations.M20230926_contact_status +import Simplex.Chat.Store.SQLite.Migrations.M20231002_conn_initiated +import Simplex.Chat.Store.SQLite.Migrations.M20231009_via_group_link_uri_hash +import Simplex.Chat.Store.SQLite.Migrations.M20231010_member_settings +import Simplex.Chat.Store.SQLite.Migrations.M20231019_indexes +import Simplex.Chat.Store.SQLite.Migrations.M20231030_xgrplinkmem_received +import Simplex.Chat.Store.SQLite.Migrations.M20231107_indexes +import Simplex.Chat.Store.SQLite.Migrations.M20231113_group_forward +import Simplex.Chat.Store.SQLite.Migrations.M20231114_remote_control +import Simplex.Chat.Store.SQLite.Migrations.M20231126_remote_ctrl_address +import Simplex.Chat.Store.SQLite.Migrations.M20231207_chat_list_pagination +import Simplex.Chat.Store.SQLite.Migrations.M20231214_item_content_tag +import Simplex.Chat.Store.SQLite.Migrations.M20231215_recreate_msg_deliveries +import Simplex.Chat.Store.SQLite.Migrations.M20240102_note_folders +import Simplex.Chat.Store.SQLite.Migrations.M20240104_members_profile_update +import Simplex.Chat.Store.SQLite.Migrations.M20240115_block_member_for_all +import Simplex.Chat.Store.SQLite.Migrations.M20240122_indexes +import Simplex.Chat.Store.SQLite.Migrations.M20240214_redirect_file_id +import Simplex.Chat.Store.SQLite.Migrations.M20240222_app_settings +import Simplex.Chat.Store.SQLite.Migrations.M20240226_users_restrict +import Simplex.Chat.Store.SQLite.Migrations.M20240228_pq +import Simplex.Chat.Store.SQLite.Migrations.M20240313_drop_agent_ack_cmd_id +import Simplex.Chat.Store.SQLite.Migrations.M20240324_custom_data +import Simplex.Chat.Store.SQLite.Migrations.M20240402_item_forwarded +import Simplex.Chat.Store.SQLite.Migrations.M20240430_ui_theme +import Simplex.Chat.Store.SQLite.Migrations.M20240501_chat_deleted +import Simplex.Chat.Store.SQLite.Migrations.M20240510_chat_items_via_proxy +import Simplex.Chat.Store.SQLite.Migrations.M20240515_rcv_files_user_approved_relays +import Simplex.Chat.Store.SQLite.Migrations.M20240528_quota_err_counter +import Simplex.Chat.Store.SQLite.Migrations.M20240827_calls_uuid +import Simplex.Chat.Store.SQLite.Migrations.M20240920_user_order +import Simplex.Chat.Store.SQLite.Migrations.M20241008_indexes +import Simplex.Chat.Store.SQLite.Migrations.M20241010_contact_requests_contact_id +import Simplex.Chat.Store.SQLite.Migrations.M20241023_chat_item_autoincrement_id +import Simplex.Chat.Store.SQLite.Migrations.M20241027_server_operators +import Simplex.Chat.Store.SQLite.Migrations.M20241125_indexes +import Simplex.Chat.Store.SQLite.Migrations.M20241128_business_chats +import Simplex.Chat.Store.SQLite.Migrations.M20241205_business_chat_members +import Simplex.Chat.Store.SQLite.Migrations.M20241222_operator_conditions +import Simplex.Chat.Store.SQLite.Migrations.M20241223_chat_tags +import Simplex.Chat.Store.SQLite.Migrations.M20241230_reports +import Simplex.Chat.Store.SQLite.Migrations.M20250105_indexes +import Simplex.Chat.Store.SQLite.Migrations.M20250115_chat_ttl +import Simplex.Chat.Store.SQLite.Migrations.M20250122_chat_items_include_in_history +import Simplex.Chat.Store.SQLite.Migrations.M20250126_mentions +import Simplex.Chat.Store.SQLite.Migrations.M20250129_delete_unused_contacts +import Simplex.Chat.Store.SQLite.Migrations.M20250130_indexes +import Simplex.Chat.Store.SQLite.Migrations.M20250402_short_links +import Simplex.Chat.Store.SQLite.Migrations.M20250512_member_admission +import Simplex.Messaging.Agent.Store.Shared (Migration (..)) + +schemaMigrations :: [(String, Query, Maybe Query)] +schemaMigrations = + [ ("20220101_initial", m20220101_initial, Nothing), + ("20220122_v1_1", m20220122_v1_1, Nothing), + ("20220205_chat_item_status", m20220205_chat_item_status, Nothing), + ("20220210_deduplicate_contact_requests", m20220210_deduplicate_contact_requests, Nothing), + ("20220224_messages_fks", m20220224_messages_fks, Nothing), + ("20220301_smp_servers", m20220301_smp_servers, Nothing), + ("20220302_profile_images", m20220302_profile_images, Nothing), + ("20220304_msg_quotes", m20220304_msg_quotes, Nothing), + ("20220321_chat_item_edited", m20220321_chat_item_edited, Nothing), + ("20220404_files_status_fields", m20220404_files_status_fields, Nothing), + ("20220514_profiles_user_id", m20220514_profiles_user_id, Nothing), + ("20220626_auto_reply", m20220626_auto_reply, Nothing), + ("20220702_calls", m20220702_calls, Nothing), + ("20220715_groups_chat_item_id", m20220715_groups_chat_item_id, Nothing), + ("20220811_chat_items_indices", m20220811_chat_items_indices, Nothing), + ("20220812_incognito_profiles", m20220812_incognito_profiles, Nothing), + ("20220818_chat_notifications", m20220818_chat_notifications, Nothing), + ("20220822_groups_host_conn_custom_user_profile_id", m20220822_groups_host_conn_custom_user_profile_id, Nothing), + ("20220823_delete_broken_group_event_chat_items", m20220823_delete_broken_group_event_chat_items, Nothing), + ("20220824_profiles_local_alias", m20220824_profiles_local_alias, Nothing), + ("20220909_commands", m20220909_commands, Nothing), + ("20220926_connection_alias", m20220926_connection_alias, Nothing), + ("20220928_settings", m20220928_settings, Nothing), + ("20221001_shared_msg_id_indices", m20221001_shared_msg_id_indices, Nothing), + ("20221003_delete_broken_integrity_error_chat_items", m20221003_delete_broken_integrity_error_chat_items, Nothing), + ("20221004_idx_msg_deliveries_message_id", m20221004_idx_msg_deliveries_message_id, Nothing), + ("20221011_user_contact_links_group_id", m20221011_user_contact_links_group_id, Nothing), + ("20221012_inline_files", m20221012_inline_files, Nothing), + ("20221019_unread_chat", m20221019_unread_chat, Nothing), + ("20221021_auto_accept__group_links", m20221021_auto_accept__group_links, Nothing), + ("20221024_contact_used", m20221024_contact_used, Nothing), + ("20221025_chat_settings", m20221025_chat_settings, Nothing), + ("20221029_group_link_id", m20221029_group_link_id, Nothing), + ("20221112_server_password", m20221112_server_password, Nothing), + ("20221115_server_cfg", m20221115_server_cfg, Nothing), + ("20221129_delete_group_feature_items", m20221129_delete_group_feature_items, Nothing), + ("20221130_delete_item_deleted", m20221130_delete_item_deleted, Nothing), + ("20221209_verified_connection", m20221209_verified_connection, Nothing), + ("20221210_idxs", m20221210_idxs, Nothing), + ("20221211_group_description", m20221211_group_description, Nothing), + ("20221212_chat_items_timed", m20221212_chat_items_timed, Nothing), + ("20221214_live_message", m20221214_live_message, Nothing), + ("20221222_chat_ts", m20221222_chat_ts, Nothing), + ("20221223_idx_chat_items_item_status", m20221223_idx_chat_items_item_status, Nothing), + ("20221230_idxs", m20221230_idxs, Nothing), + ("20230107_connections_auth_err_counter", m20230107_connections_auth_err_counter, Nothing), + ("20230111_users_agent_user_id", m20230111_users_agent_user_id, Nothing), + ("20230117_fkey_indexes", m20230117_fkey_indexes, Nothing), + ("20230118_recreate_smp_servers", m20230118_recreate_smp_servers, Nothing), + ("20230129_drop_chat_items_group_idx", m20230129_drop_chat_items_group_idx, Nothing), + ("20230206_item_deleted_by_group_member_id", m20230206_item_deleted_by_group_member_id, Nothing), + ("20230303_group_link_role", m20230303_group_link_role, Nothing), + ("20230317_hidden_profiles", m20230317_hidden_profiles, Just down_m20230317_hidden_profiles), + ("20230318_file_description", m20230318_file_description, Just down_m20230318_file_description), + ("20230321_agent_file_deleted", m20230321_agent_file_deleted, Just down_m20230321_agent_file_deleted), + ("20230328_files_protocol", m20230328_files_protocol, Just down_m20230328_files_protocol), + ("20230402_protocol_servers", m20230402_protocol_servers, Just down_m20230402_protocol_servers), + ("20230411_extra_xftp_file_descriptions", m20230411_extra_xftp_file_descriptions, Just down_m20230411_extra_xftp_file_descriptions), + ("20230420_rcv_files_to_receive", m20230420_rcv_files_to_receive, Just down_m20230420_rcv_files_to_receive), + ("20230422_profile_contact_links", m20230422_profile_contact_links, Just down_m20230422_profile_contact_links), + ("20230504_recreate_msg_delivery_events_cleanup_messages", m20230504_recreate_msg_delivery_events_cleanup_messages, Just down_m20230504_recreate_msg_delivery_events_cleanup_messages), + ("20230505_chat_item_versions", m20230505_chat_item_versions, Just down_m20230505_chat_item_versions), + ("20230511_reactions", m20230511_reactions, Just down_m20230511_reactions), + ("20230519_item_deleted_ts", m20230519_item_deleted_ts, Just down_m20230519_item_deleted_ts), + ("20230526_indexes", m20230526_indexes, Just down_m20230526_indexes), + ("20230529_indexes", m20230529_indexes, Just down_m20230529_indexes), + ("20230608_deleted_contacts", m20230608_deleted_contacts, Just down_m20230608_deleted_contacts), + ("20230618_favorite_chats", m20230618_favorite_chats, Just down_m20230618_favorite_chats), + ("20230621_chat_item_moderations", m20230621_chat_item_moderations, Just down_m20230621_chat_item_moderations), + ("20230705_delivery_receipts", m20230705_delivery_receipts, Just down_m20230705_delivery_receipts), + ("20230721_group_snd_item_statuses", m20230721_group_snd_item_statuses, Just down_m20230721_group_snd_item_statuses), + ("20230814_indexes", m20230814_indexes, Just down_m20230814_indexes), + ("20230827_file_encryption", m20230827_file_encryption, Just down_m20230827_file_encryption), + ("20230829_connections_chat_vrange", m20230829_connections_chat_vrange, Just down_m20230829_connections_chat_vrange), + ("20230903_connections_to_subscribe", m20230903_connections_to_subscribe, Just down_m20230903_connections_to_subscribe), + ("20230913_member_contacts", m20230913_member_contacts, Just down_m20230913_member_contacts), + ("20230914_member_probes", m20230914_member_probes, Just down_m20230914_member_probes), + ("20230926_contact_status", m20230926_contact_status, Just down_m20230926_contact_status), + ("20231002_conn_initiated", m20231002_conn_initiated, Just down_m20231002_conn_initiated), + ("20231009_via_group_link_uri_hash", m20231009_via_group_link_uri_hash, Just down_m20231009_via_group_link_uri_hash), + ("20231010_member_settings", m20231010_member_settings, Just down_m20231010_member_settings), + ("20231019_indexes", m20231019_indexes, Just down_m20231019_indexes), + ("20231030_xgrplinkmem_received", m20231030_xgrplinkmem_received, Just down_m20231030_xgrplinkmem_received), + ("20231107_indexes", m20231107_indexes, Just down_m20231107_indexes), + ("20231113_group_forward", m20231113_group_forward, Just down_m20231113_group_forward), + ("20231114_remote_control", m20231114_remote_control, Just down_m20231114_remote_control), + ("20231126_remote_ctrl_address", m20231126_remote_ctrl_address, Just down_m20231126_remote_ctrl_address), + ("20231207_chat_list_pagination", m20231207_chat_list_pagination, Just down_m20231207_chat_list_pagination), + ("20231214_item_content_tag", m20231214_item_content_tag, Just down_m20231214_item_content_tag), + ("20231215_recreate_msg_deliveries", m20231215_recreate_msg_deliveries, Just down_m20231215_recreate_msg_deliveries), + ("20240102_note_folders", m20240102_note_folders, Just down_m20240102_note_folders), + ("20240104_members_profile_update", m20240104_members_profile_update, Just down_m20240104_members_profile_update), + ("20240115_block_member_for_all", m20240115_block_member_for_all, Just down_m20240115_block_member_for_all), + ("20240122_indexes", m20240122_indexes, Just down_m20240122_indexes), + ("20240214_redirect_file_id", m20240214_redirect_file_id, Just down_m20240214_redirect_file_id), + ("20240222_app_settings", m20240222_app_settings, Just down_m20240222_app_settings), + ("20240226_users_restrict", m20240226_users_restrict, Just down_m20240226_users_restrict), + ("20240228_pq", m20240228_pq, Just down_m20240228_pq), + ("20240313_drop_agent_ack_cmd_id", m20240313_drop_agent_ack_cmd_id, Just down_m20240313_drop_agent_ack_cmd_id), + ("20240324_custom_data", m20240324_custom_data, Just down_m20240324_custom_data), + ("20240402_item_forwarded", m20240402_item_forwarded, Just down_m20240402_item_forwarded), + ("20240430_ui_theme", m20240430_ui_theme, Just down_m20240430_ui_theme), + ("20240501_chat_deleted", m20240501_chat_deleted, Just down_m20240501_chat_deleted), + ("20240510_chat_items_via_proxy", m20240510_chat_items_via_proxy, Just down_m20240510_chat_items_via_proxy), + ("20240515_rcv_files_user_approved_relays", m20240515_rcv_files_user_approved_relays, Just down_m20240515_rcv_files_user_approved_relays), + ("20240528_quota_err_counter", m20240528_quota_err_counter, Just down_m20240528_quota_err_counter), + ("20240827_calls_uuid", m20240827_calls_uuid, Just down_m20240827_calls_uuid), + ("20240920_user_order", m20240920_user_order, Just down_m20240920_user_order), + ("20241008_indexes", m20241008_indexes, Just down_m20241008_indexes), + ("20241010_contact_requests_contact_id", m20241010_contact_requests_contact_id, Just down_m20241010_contact_requests_contact_id), + ("20241023_chat_item_autoincrement_id", m20241023_chat_item_autoincrement_id, Just down_m20241023_chat_item_autoincrement_id), + ("20241027_server_operators", m20241027_server_operators, Just down_m20241027_server_operators), + ("20241125_indexes", m20241125_indexes, Just down_m20241125_indexes), + ("20241128_business_chats", m20241128_business_chats, Just down_m20241128_business_chats), + ("20241205_business_chat_members", m20241205_business_chat_members, Just down_m20241205_business_chat_members), + ("20241222_operator_conditions", m20241222_operator_conditions, Just down_m20241222_operator_conditions), + ("20241223_chat_tags", m20241223_chat_tags, Just down_m20241223_chat_tags), + ("20241230_reports", m20241230_reports, Just down_m20241230_reports), + ("20250105_indexes", m20250105_indexes, Just down_m20250105_indexes), + ("20250115_chat_ttl", m20250115_chat_ttl, Just down_m20250115_chat_ttl), + ("20250122_chat_items_include_in_history", m20250122_chat_items_include_in_history, Just down_m20250122_chat_items_include_in_history), + ("20250126_mentions", m20250126_mentions, Just down_m20250126_mentions), + ("20250129_delete_unused_contacts", m20250129_delete_unused_contacts, Just down_m20250129_delete_unused_contacts), + ("20250130_indexes", m20250130_indexes, Just down_m20250130_indexes), + ("20250402_short_links", m20250402_short_links, Just down_m20250402_short_links), + ("20250512_member_admission", m20250512_member_admission, Just down_m20250512_member_admission) + ] + +-- | The list of migrations in ascending order by date +migrations :: [Migration] +migrations = sortOn name $ map migration schemaMigrations + where + migration (name, up, down) = Migration {name, up = fromQuery up, down = fromQuery <$> down} diff --git a/src/Simplex/Chat/Migrations/M20220101_initial.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220101_initial.hs similarity index 99% rename from src/Simplex/Chat/Migrations/M20220101_initial.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220101_initial.hs index 2568b0b672..601a90d5d9 100644 --- a/src/Simplex/Chat/Migrations/M20220101_initial.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220101_initial.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220101_initial where +module Simplex.Chat.Store.SQLite.Migrations.M20220101_initial where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220122_v1_1.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220122_v1_1.hs similarity index 99% rename from src/Simplex/Chat/Migrations/M20220122_v1_1.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220122_v1_1.hs index 157f97c333..c84bbdd03f 100644 --- a/src/Simplex/Chat/Migrations/M20220122_v1_1.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220122_v1_1.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220122_v1_1 where +module Simplex.Chat.Store.SQLite.Migrations.M20220122_v1_1 where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220205_chat_item_status.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220205_chat_item_status.hs similarity index 86% rename from src/Simplex/Chat/Migrations/M20220205_chat_item_status.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220205_chat_item_status.hs index 6baca156fb..fe1a6382eb 100644 --- a/src/Simplex/Chat/Migrations/M20220205_chat_item_status.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220205_chat_item_status.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220205_chat_item_status where +module Simplex.Chat.Store.SQLite.Migrations.M20220205_chat_item_status where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220210_deduplicate_contact_requests.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220210_deduplicate_contact_requests.hs similarity index 91% rename from src/Simplex/Chat/Migrations/M20220210_deduplicate_contact_requests.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220210_deduplicate_contact_requests.hs index e2c26e35e0..1c97f82dc0 100644 --- a/src/Simplex/Chat/Migrations/M20220210_deduplicate_contact_requests.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220210_deduplicate_contact_requests.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220210_deduplicate_contact_requests where +module Simplex.Chat.Store.SQLite.Migrations.M20220210_deduplicate_contact_requests where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220224_messages_fks.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220224_messages_fks.hs similarity index 84% rename from src/Simplex/Chat/Migrations/M20220224_messages_fks.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220224_messages_fks.hs index 9bb5db57a5..b9842dd33d 100644 --- a/src/Simplex/Chat/Migrations/M20220224_messages_fks.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220224_messages_fks.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220224_messages_fks where +module Simplex.Chat.Store.SQLite.Migrations.M20220224_messages_fks where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220301_smp_servers.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220301_smp_servers.hs similarity index 87% rename from src/Simplex/Chat/Migrations/M20220301_smp_servers.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220301_smp_servers.hs index 774f2e0168..91bd3194ca 100644 --- a/src/Simplex/Chat/Migrations/M20220301_smp_servers.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220301_smp_servers.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220301_smp_servers where +module Simplex.Chat.Store.SQLite.Migrations.M20220301_smp_servers where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220302_profile_images.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220302_profile_images.hs similarity index 79% rename from src/Simplex/Chat/Migrations/M20220302_profile_images.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220302_profile_images.hs index 72c22b89cb..f6a9444ce1 100644 --- a/src/Simplex/Chat/Migrations/M20220302_profile_images.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220302_profile_images.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220302_profile_images where +module Simplex.Chat.Store.SQLite.Migrations.M20220302_profile_images where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220304_msg_quotes.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220304_msg_quotes.hs similarity index 94% rename from src/Simplex/Chat/Migrations/M20220304_msg_quotes.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220304_msg_quotes.hs index 129c3616a0..fa0df67b10 100644 --- a/src/Simplex/Chat/Migrations/M20220304_msg_quotes.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220304_msg_quotes.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220304_msg_quotes where +module Simplex.Chat.Store.SQLite.Migrations.M20220304_msg_quotes where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220321_chat_item_edited.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220321_chat_item_edited.hs similarity index 76% rename from src/Simplex/Chat/Migrations/M20220321_chat_item_edited.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220321_chat_item_edited.hs index 7a77f00262..5e11ef7519 100644 --- a/src/Simplex/Chat/Migrations/M20220321_chat_item_edited.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220321_chat_item_edited.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220321_chat_item_edited where +module Simplex.Chat.Store.SQLite.Migrations.M20220321_chat_item_edited where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220404_files_status_fields.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220404_files_status_fields.hs similarity index 83% rename from src/Simplex/Chat/Migrations/M20220404_files_status_fields.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220404_files_status_fields.hs index 40623a3be6..f38fe1163a 100644 --- a/src/Simplex/Chat/Migrations/M20220404_files_status_fields.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220404_files_status_fields.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220404_files_status_fields where +module Simplex.Chat.Store.SQLite.Migrations.M20220404_files_status_fields where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220514_profiles_user_id.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220514_profiles_user_id.hs similarity index 88% rename from src/Simplex/Chat/Migrations/M20220514_profiles_user_id.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220514_profiles_user_id.hs index 239f124576..b4b99a27a2 100644 --- a/src/Simplex/Chat/Migrations/M20220514_profiles_user_id.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220514_profiles_user_id.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220514_profiles_user_id where +module Simplex.Chat.Store.SQLite.Migrations.M20220514_profiles_user_id where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220626_auto_reply.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220626_auto_reply.hs similarity index 85% rename from src/Simplex/Chat/Migrations/M20220626_auto_reply.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220626_auto_reply.hs index 6ac72ac804..e85a6438ae 100644 --- a/src/Simplex/Chat/Migrations/M20220626_auto_reply.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220626_auto_reply.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220626_auto_reply where +module Simplex.Chat.Store.SQLite.Migrations.M20220626_auto_reply where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220702_calls.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220702_calls.hs similarity index 91% rename from src/Simplex/Chat/Migrations/M20220702_calls.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220702_calls.hs index 4cbf3dbad6..fc0bdd568c 100644 --- a/src/Simplex/Chat/Migrations/M20220702_calls.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220702_calls.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220702_calls where +module Simplex.Chat.Store.SQLite.Migrations.M20220702_calls where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220715_groups_chat_item_id.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220715_groups_chat_item_id.hs similarity index 78% rename from src/Simplex/Chat/Migrations/M20220715_groups_chat_item_id.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220715_groups_chat_item_id.hs index 0fa5310bfb..44ed8d2e6e 100644 --- a/src/Simplex/Chat/Migrations/M20220715_groups_chat_item_id.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220715_groups_chat_item_id.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220715_groups_chat_item_id where +module Simplex.Chat.Store.SQLite.Migrations.M20220715_groups_chat_item_id where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220811_chat_items_indices.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220811_chat_items_indices.hs similarity index 82% rename from src/Simplex/Chat/Migrations/M20220811_chat_items_indices.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220811_chat_items_indices.hs index a43617d439..18eabbfacd 100644 --- a/src/Simplex/Chat/Migrations/M20220811_chat_items_indices.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220811_chat_items_indices.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220811_chat_items_indices where +module Simplex.Chat.Store.SQLite.Migrations.M20220811_chat_items_indices where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220812_incognito_profiles.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220812_incognito_profiles.hs similarity index 89% rename from src/Simplex/Chat/Migrations/M20220812_incognito_profiles.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220812_incognito_profiles.hs index e03eda2358..59ff18caf8 100644 --- a/src/Simplex/Chat/Migrations/M20220812_incognito_profiles.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220812_incognito_profiles.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220812_incognito_profiles where +module Simplex.Chat.Store.SQLite.Migrations.M20220812_incognito_profiles where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220818_chat_notifications.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220818_chat_notifications.hs similarity index 78% rename from src/Simplex/Chat/Migrations/M20220818_chat_notifications.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220818_chat_notifications.hs index ffb2b15967..42c439e8bc 100644 --- a/src/Simplex/Chat/Migrations/M20220818_chat_notifications.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220818_chat_notifications.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220818_chat_notifications where +module Simplex.Chat.Store.SQLite.Migrations.M20220818_chat_notifications where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220822_groups_host_conn_custom_user_profile_id.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220822_groups_host_conn_custom_user_profile_id.hs similarity index 80% rename from src/Simplex/Chat/Migrations/M20220822_groups_host_conn_custom_user_profile_id.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220822_groups_host_conn_custom_user_profile_id.hs index bbadbd5524..1cd49ccf26 100644 --- a/src/Simplex/Chat/Migrations/M20220822_groups_host_conn_custom_user_profile_id.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220822_groups_host_conn_custom_user_profile_id.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220822_groups_host_conn_custom_user_profile_id where +module Simplex.Chat.Store.SQLite.Migrations.M20220822_groups_host_conn_custom_user_profile_id where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220823_delete_broken_group_event_chat_items.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220823_delete_broken_group_event_chat_items.hs similarity index 80% rename from src/Simplex/Chat/Migrations/M20220823_delete_broken_group_event_chat_items.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220823_delete_broken_group_event_chat_items.hs index 40657f3421..f4f8df826a 100644 --- a/src/Simplex/Chat/Migrations/M20220823_delete_broken_group_event_chat_items.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220823_delete_broken_group_event_chat_items.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220823_delete_broken_group_event_chat_items where +module Simplex.Chat.Store.SQLite.Migrations.M20220823_delete_broken_group_event_chat_items where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220824_profiles_local_alias.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220824_profiles_local_alias.hs similarity index 83% rename from src/Simplex/Chat/Migrations/M20220824_profiles_local_alias.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220824_profiles_local_alias.hs index f0b0ca8385..9252bc43fb 100644 --- a/src/Simplex/Chat/Migrations/M20220824_profiles_local_alias.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220824_profiles_local_alias.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220824_profiles_local_alias where +module Simplex.Chat.Store.SQLite.Migrations.M20220824_profiles_local_alias where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220909_commands.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220909_commands.hs similarity index 91% rename from src/Simplex/Chat/Migrations/M20220909_commands.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220909_commands.hs index 745dff4165..3cc359d56b 100644 --- a/src/Simplex/Chat/Migrations/M20220909_commands.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220909_commands.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220909_commands where +module Simplex.Chat.Store.SQLite.Migrations.M20220909_commands where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220926_connection_alias.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220926_connection_alias.hs similarity index 83% rename from src/Simplex/Chat/Migrations/M20220926_connection_alias.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220926_connection_alias.hs index ede7cc3cfc..7b56ba5fb9 100644 --- a/src/Simplex/Chat/Migrations/M20220926_connection_alias.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220926_connection_alias.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220926_connection_alias where +module Simplex.Chat.Store.SQLite.Migrations.M20220926_connection_alias where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20220928_settings.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20220928_settings.hs similarity index 86% rename from src/Simplex/Chat/Migrations/M20220928_settings.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20220928_settings.hs index 56b3613b05..a159ef1cc4 100644 --- a/src/Simplex/Chat/Migrations/M20220928_settings.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20220928_settings.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20220928_settings where +module Simplex.Chat.Store.SQLite.Migrations.M20220928_settings where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221001_shared_msg_id_indices.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221001_shared_msg_id_indices.hs similarity index 85% rename from src/Simplex/Chat/Migrations/M20221001_shared_msg_id_indices.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221001_shared_msg_id_indices.hs index 10ac0fa5e1..61c5800a1c 100644 --- a/src/Simplex/Chat/Migrations/M20221001_shared_msg_id_indices.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221001_shared_msg_id_indices.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221001_shared_msg_id_indices where +module Simplex.Chat.Store.SQLite.Migrations.M20221001_shared_msg_id_indices where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221003_delete_broken_integrity_error_chat_items.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221003_delete_broken_integrity_error_chat_items.hs similarity index 75% rename from src/Simplex/Chat/Migrations/M20221003_delete_broken_integrity_error_chat_items.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221003_delete_broken_integrity_error_chat_items.hs index 2d451766cd..1843f3316a 100644 --- a/src/Simplex/Chat/Migrations/M20221003_delete_broken_integrity_error_chat_items.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221003_delete_broken_integrity_error_chat_items.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221003_delete_broken_integrity_error_chat_items where +module Simplex.Chat.Store.SQLite.Migrations.M20221003_delete_broken_integrity_error_chat_items where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221004_idx_msg_deliveries_message_id.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221004_idx_msg_deliveries_message_id.hs similarity index 76% rename from src/Simplex/Chat/Migrations/M20221004_idx_msg_deliveries_message_id.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221004_idx_msg_deliveries_message_id.hs index 0e53923b58..8dba932549 100644 --- a/src/Simplex/Chat/Migrations/M20221004_idx_msg_deliveries_message_id.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221004_idx_msg_deliveries_message_id.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221004_idx_msg_deliveries_message_id where +module Simplex.Chat.Store.SQLite.Migrations.M20221004_idx_msg_deliveries_message_id where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221011_user_contact_links_group_id.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221011_user_contact_links_group_id.hs similarity index 81% rename from src/Simplex/Chat/Migrations/M20221011_user_contact_links_group_id.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221011_user_contact_links_group_id.hs index 4ad6fcb8dc..a8c64d32b0 100644 --- a/src/Simplex/Chat/Migrations/M20221011_user_contact_links_group_id.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221011_user_contact_links_group_id.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221011_user_contact_links_group_id where +module Simplex.Chat.Store.SQLite.Migrations.M20221011_user_contact_links_group_id where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221012_inline_files.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221012_inline_files.hs similarity index 92% rename from src/Simplex/Chat/Migrations/M20221012_inline_files.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221012_inline_files.hs index 4b069f0882..cb765d4e64 100644 --- a/src/Simplex/Chat/Migrations/M20221012_inline_files.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221012_inline_files.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221012_inline_files where +module Simplex.Chat.Store.SQLite.Migrations.M20221012_inline_files where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221019_unread_chat.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221019_unread_chat.hs similarity index 87% rename from src/Simplex/Chat/Migrations/M20221019_unread_chat.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221019_unread_chat.hs index db24db947b..c8bccb81d3 100644 --- a/src/Simplex/Chat/Migrations/M20221019_unread_chat.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221019_unread_chat.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221019_unread_chat where +module Simplex.Chat.Store.SQLite.Migrations.M20221019_unread_chat where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221021_auto_accept__group_links.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221021_auto_accept__group_links.hs similarity index 88% rename from src/Simplex/Chat/Migrations/M20221021_auto_accept__group_links.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221021_auto_accept__group_links.hs index cb945cce97..54042855d9 100644 --- a/src/Simplex/Chat/Migrations/M20221021_auto_accept__group_links.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221021_auto_accept__group_links.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221021_auto_accept__group_links where +module Simplex.Chat.Store.SQLite.Migrations.M20221021_auto_accept__group_links where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221024_contact_used.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221024_contact_used.hs similarity index 87% rename from src/Simplex/Chat/Migrations/M20221024_contact_used.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221024_contact_used.hs index 6f677f1c77..de0164d81b 100644 --- a/src/Simplex/Chat/Migrations/M20221024_contact_used.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221024_contact_used.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221024_contact_used where +module Simplex.Chat.Store.SQLite.Migrations.M20221024_contact_used where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221025_chat_settings.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221025_chat_settings.hs similarity index 87% rename from src/Simplex/Chat/Migrations/M20221025_chat_settings.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221025_chat_settings.hs index 712902e85c..0e768e8fb1 100644 --- a/src/Simplex/Chat/Migrations/M20221025_chat_settings.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221025_chat_settings.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221025_chat_settings where +module Simplex.Chat.Store.SQLite.Migrations.M20221025_chat_settings where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221029_group_link_id.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221029_group_link_id.hs similarity index 80% rename from src/Simplex/Chat/Migrations/M20221029_group_link_id.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221029_group_link_id.hs index da290e4158..5d3cf7ee3c 100644 --- a/src/Simplex/Chat/Migrations/M20221029_group_link_id.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221029_group_link_id.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221029_group_link_id where +module Simplex.Chat.Store.SQLite.Migrations.M20221029_group_link_id where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221112_server_password.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221112_server_password.hs similarity index 75% rename from src/Simplex/Chat/Migrations/M20221112_server_password.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221112_server_password.hs index ee8d0e470d..4afc63fe1c 100644 --- a/src/Simplex/Chat/Migrations/M20221112_server_password.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221112_server_password.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221112_server_password where +module Simplex.Chat.Store.SQLite.Migrations.M20221112_server_password where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221115_server_cfg.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221115_server_cfg.hs similarity index 87% rename from src/Simplex/Chat/Migrations/M20221115_server_cfg.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221115_server_cfg.hs index 409da91db0..1def0c4c17 100644 --- a/src/Simplex/Chat/Migrations/M20221115_server_cfg.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221115_server_cfg.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221115_server_cfg where +module Simplex.Chat.Store.SQLite.Migrations.M20221115_server_cfg where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221129_delete_group_feature_items.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221129_delete_group_feature_items.hs similarity index 80% rename from src/Simplex/Chat/Migrations/M20221129_delete_group_feature_items.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221129_delete_group_feature_items.hs index 7baf6b584e..5e631ab88c 100644 --- a/src/Simplex/Chat/Migrations/M20221129_delete_group_feature_items.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221129_delete_group_feature_items.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221129_delete_group_feature_items where +module Simplex.Chat.Store.SQLite.Migrations.M20221129_delete_group_feature_items where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221130_delete_item_deleted.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221130_delete_item_deleted.hs similarity index 78% rename from src/Simplex/Chat/Migrations/M20221130_delete_item_deleted.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221130_delete_item_deleted.hs index 487cb7dceb..f64c3a2b56 100644 --- a/src/Simplex/Chat/Migrations/M20221130_delete_item_deleted.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221130_delete_item_deleted.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221130_delete_item_deleted where +module Simplex.Chat.Store.SQLite.Migrations.M20221130_delete_item_deleted where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221209_verified_connection.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221209_verified_connection.hs similarity index 80% rename from src/Simplex/Chat/Migrations/M20221209_verified_connection.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221209_verified_connection.hs index f7e4a8aee9..0643c3f873 100644 --- a/src/Simplex/Chat/Migrations/M20221209_verified_connection.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221209_verified_connection.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221209_verified_connection where +module Simplex.Chat.Store.SQLite.Migrations.M20221209_verified_connection where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221210_idxs.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221210_idxs.hs similarity index 85% rename from src/Simplex/Chat/Migrations/M20221210_idxs.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221210_idxs.hs index cd18611a53..cea66cf117 100644 --- a/src/Simplex/Chat/Migrations/M20221210_idxs.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221210_idxs.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221210_idxs where +module Simplex.Chat.Store.SQLite.Migrations.M20221210_idxs where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221211_group_description.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221211_group_description.hs similarity index 76% rename from src/Simplex/Chat/Migrations/M20221211_group_description.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221211_group_description.hs index ba406862cb..8b0ca88f67 100644 --- a/src/Simplex/Chat/Migrations/M20221211_group_description.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221211_group_description.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221211_group_description where +module Simplex.Chat.Store.SQLite.Migrations.M20221211_group_description where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221212_chat_items_timed.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221212_chat_items_timed.hs similarity index 82% rename from src/Simplex/Chat/Migrations/M20221212_chat_items_timed.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221212_chat_items_timed.hs index b82b66f3d4..edfcd39c70 100644 --- a/src/Simplex/Chat/Migrations/M20221212_chat_items_timed.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221212_chat_items_timed.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221212_chat_items_timed where +module Simplex.Chat.Store.SQLite.Migrations.M20221212_chat_items_timed where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221214_live_message.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221214_live_message.hs similarity index 75% rename from src/Simplex/Chat/Migrations/M20221214_live_message.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221214_live_message.hs index 959ec75ae4..ff64defef3 100644 --- a/src/Simplex/Chat/Migrations/M20221214_live_message.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221214_live_message.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221214_live_message where +module Simplex.Chat.Store.SQLite.Migrations.M20221214_live_message where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221222_chat_ts.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221222_chat_ts.hs similarity index 81% rename from src/Simplex/Chat/Migrations/M20221222_chat_ts.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221222_chat_ts.hs index 9a83c81821..1a4025b1ae 100644 --- a/src/Simplex/Chat/Migrations/M20221222_chat_ts.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221222_chat_ts.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221222_chat_ts where +module Simplex.Chat.Store.SQLite.Migrations.M20221222_chat_ts where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221223_idx_chat_items_item_status.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221223_idx_chat_items_item_status.hs similarity index 75% rename from src/Simplex/Chat/Migrations/M20221223_idx_chat_items_item_status.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221223_idx_chat_items_item_status.hs index f29b4a9d5c..d834396df2 100644 --- a/src/Simplex/Chat/Migrations/M20221223_idx_chat_items_item_status.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221223_idx_chat_items_item_status.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221223_idx_chat_items_item_status where +module Simplex.Chat.Store.SQLite.Migrations.M20221223_idx_chat_items_item_status where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20221230_idxs.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20221230_idxs.hs similarity index 82% rename from src/Simplex/Chat/Migrations/M20221230_idxs.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20221230_idxs.hs index dbe84357aa..ca58d0cb70 100644 --- a/src/Simplex/Chat/Migrations/M20221230_idxs.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20221230_idxs.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20221230_idxs where +module Simplex.Chat.Store.SQLite.Migrations.M20221230_idxs where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230107_connections_auth_err_counter.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230107_connections_auth_err_counter.hs similarity index 82% rename from src/Simplex/Chat/Migrations/M20230107_connections_auth_err_counter.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230107_connections_auth_err_counter.hs index b3c724e938..3b5062144a 100644 --- a/src/Simplex/Chat/Migrations/M20230107_connections_auth_err_counter.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230107_connections_auth_err_counter.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230107_connections_auth_err_counter where +module Simplex.Chat.Store.SQLite.Migrations.M20230107_connections_auth_err_counter where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230111_users_agent_user_id.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230111_users_agent_user_id.hs similarity index 82% rename from src/Simplex/Chat/Migrations/M20230111_users_agent_user_id.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230111_users_agent_user_id.hs index 531c776a33..9fd108809c 100644 --- a/src/Simplex/Chat/Migrations/M20230111_users_agent_user_id.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230111_users_agent_user_id.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230111_users_agent_user_id where +module Simplex.Chat.Store.SQLite.Migrations.M20230111_users_agent_user_id where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230117_fkey_indexes.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230117_fkey_indexes.hs similarity index 98% rename from src/Simplex/Chat/Migrations/M20230117_fkey_indexes.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230117_fkey_indexes.hs index 5986863093..50f6cf4a21 100644 --- a/src/Simplex/Chat/Migrations/M20230117_fkey_indexes.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230117_fkey_indexes.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230117_fkey_indexes where +module Simplex.Chat.Store.SQLite.Migrations.M20230117_fkey_indexes where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230118_recreate_smp_servers.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230118_recreate_smp_servers.hs similarity index 93% rename from src/Simplex/Chat/Migrations/M20230118_recreate_smp_servers.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230118_recreate_smp_servers.hs index 6253a3a37e..19e60aa792 100644 --- a/src/Simplex/Chat/Migrations/M20230118_recreate_smp_servers.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230118_recreate_smp_servers.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230118_recreate_smp_servers where +module Simplex.Chat.Store.SQLite.Migrations.M20230118_recreate_smp_servers where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230129_drop_chat_items_group_idx.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230129_drop_chat_items_group_idx.hs similarity index 73% rename from src/Simplex/Chat/Migrations/M20230129_drop_chat_items_group_idx.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230129_drop_chat_items_group_idx.hs index c45f513765..be31997235 100644 --- a/src/Simplex/Chat/Migrations/M20230129_drop_chat_items_group_idx.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230129_drop_chat_items_group_idx.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230129_drop_chat_items_group_idx where +module Simplex.Chat.Store.SQLite.Migrations.M20230129_drop_chat_items_group_idx where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230206_item_deleted_by_group_member_id.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230206_item_deleted_by_group_member_id.hs similarity index 82% rename from src/Simplex/Chat/Migrations/M20230206_item_deleted_by_group_member_id.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230206_item_deleted_by_group_member_id.hs index 085e7f7525..9c3d01795a 100644 --- a/src/Simplex/Chat/Migrations/M20230206_item_deleted_by_group_member_id.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230206_item_deleted_by_group_member_id.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230206_item_deleted_by_group_member_id where +module Simplex.Chat.Store.SQLite.Migrations.M20230206_item_deleted_by_group_member_id where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230303_group_link_role.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230303_group_link_role.hs similarity index 78% rename from src/Simplex/Chat/Migrations/M20230303_group_link_role.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230303_group_link_role.hs index ae67e7d776..cf25fb4f6e 100644 --- a/src/Simplex/Chat/Migrations/M20230303_group_link_role.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230303_group_link_role.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230303_group_link_role where +module Simplex.Chat.Store.SQLite.Migrations.M20230303_group_link_role where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230317_hidden_profiles.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230317_hidden_profiles.hs similarity index 88% rename from src/Simplex/Chat/Migrations/M20230317_hidden_profiles.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230317_hidden_profiles.hs index 65e9cfeadd..0106d29118 100644 --- a/src/Simplex/Chat/Migrations/M20230317_hidden_profiles.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230317_hidden_profiles.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230317_hidden_profiles where +module Simplex.Chat.Store.SQLite.Migrations.M20230317_hidden_profiles where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230318_file_description.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230318_file_description.hs similarity index 95% rename from src/Simplex/Chat/Migrations/M20230318_file_description.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230318_file_description.hs index 39f56b2a48..3bb15037ed 100644 --- a/src/Simplex/Chat/Migrations/M20230318_file_description.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230318_file_description.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230318_file_description where +module Simplex.Chat.Store.SQLite.Migrations.M20230318_file_description where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230321_agent_file_deleted.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230321_agent_file_deleted.hs similarity index 90% rename from src/Simplex/Chat/Migrations/M20230321_agent_file_deleted.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230321_agent_file_deleted.hs index 97c213ea48..2a54f05c4c 100644 --- a/src/Simplex/Chat/Migrations/M20230321_agent_file_deleted.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230321_agent_file_deleted.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230321_agent_file_deleted where +module Simplex.Chat.Store.SQLite.Migrations.M20230321_agent_file_deleted where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230328_files_protocol.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230328_files_protocol.hs similarity index 83% rename from src/Simplex/Chat/Migrations/M20230328_files_protocol.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230328_files_protocol.hs index 5690378301..7ea2b4e34f 100644 --- a/src/Simplex/Chat/Migrations/M20230328_files_protocol.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230328_files_protocol.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230328_files_protocol where +module Simplex.Chat.Store.SQLite.Migrations.M20230328_files_protocol where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230402_protocol_servers.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230402_protocol_servers.hs similarity index 86% rename from src/Simplex/Chat/Migrations/M20230402_protocol_servers.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230402_protocol_servers.hs index bffe7ac813..be2d0b96b2 100644 --- a/src/Simplex/Chat/Migrations/M20230402_protocol_servers.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230402_protocol_servers.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230402_protocol_servers where +module Simplex.Chat.Store.SQLite.Migrations.M20230402_protocol_servers where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230411_extra_xftp_file_descriptions.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230411_extra_xftp_file_descriptions.hs similarity index 92% rename from src/Simplex/Chat/Migrations/M20230411_extra_xftp_file_descriptions.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230411_extra_xftp_file_descriptions.hs index 9bfd773c44..739770d84f 100644 --- a/src/Simplex/Chat/Migrations/M20230411_extra_xftp_file_descriptions.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230411_extra_xftp_file_descriptions.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230411_extra_xftp_file_descriptions where +module Simplex.Chat.Store.SQLite.Migrations.M20230411_extra_xftp_file_descriptions where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230420_rcv_files_to_receive.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230420_rcv_files_to_receive.hs similarity index 82% rename from src/Simplex/Chat/Migrations/M20230420_rcv_files_to_receive.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230420_rcv_files_to_receive.hs index 0b6329bc6d..cd97e16c03 100644 --- a/src/Simplex/Chat/Migrations/M20230420_rcv_files_to_receive.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230420_rcv_files_to_receive.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230420_rcv_files_to_receive where +module Simplex.Chat.Store.SQLite.Migrations.M20230420_rcv_files_to_receive where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230422_profile_contact_links.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230422_profile_contact_links.hs similarity index 83% rename from src/Simplex/Chat/Migrations/M20230422_profile_contact_links.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230422_profile_contact_links.hs index ee7ff053d5..f9513acff1 100644 --- a/src/Simplex/Chat/Migrations/M20230422_profile_contact_links.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230422_profile_contact_links.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230422_profile_contact_links where +module Simplex.Chat.Store.SQLite.Migrations.M20230422_profile_contact_links where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230504_recreate_msg_delivery_events_cleanup_messages.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230504_recreate_msg_delivery_events_cleanup_messages.hs similarity index 92% rename from src/Simplex/Chat/Migrations/M20230504_recreate_msg_delivery_events_cleanup_messages.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230504_recreate_msg_delivery_events_cleanup_messages.hs index 009b537b6c..c51db6905b 100644 --- a/src/Simplex/Chat/Migrations/M20230504_recreate_msg_delivery_events_cleanup_messages.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230504_recreate_msg_delivery_events_cleanup_messages.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230504_recreate_msg_delivery_events_cleanup_messages where +module Simplex.Chat.Store.SQLite.Migrations.M20230504_recreate_msg_delivery_events_cleanup_messages where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230505_chat_item_versions.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230505_chat_item_versions.hs similarity index 91% rename from src/Simplex/Chat/Migrations/M20230505_chat_item_versions.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230505_chat_item_versions.hs index 7e2e0f7719..bfc675c87c 100644 --- a/src/Simplex/Chat/Migrations/M20230505_chat_item_versions.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230505_chat_item_versions.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230505_chat_item_versions where +module Simplex.Chat.Store.SQLite.Migrations.M20230505_chat_item_versions where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230511_reactions.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230511_reactions.hs similarity index 96% rename from src/Simplex/Chat/Migrations/M20230511_reactions.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230511_reactions.hs index f01954a373..17ecb97649 100644 --- a/src/Simplex/Chat/Migrations/M20230511_reactions.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230511_reactions.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230511_reactions where +module Simplex.Chat.Store.SQLite.Migrations.M20230511_reactions where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230519_item_deleted_ts.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230519_item_deleted_ts.hs similarity index 83% rename from src/Simplex/Chat/Migrations/M20230519_item_deleted_ts.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230519_item_deleted_ts.hs index 20d5fd7a8d..c05687347c 100644 --- a/src/Simplex/Chat/Migrations/M20230519_item_deleted_ts.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230519_item_deleted_ts.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230519_item_deleted_ts where +module Simplex.Chat.Store.SQLite.Migrations.M20230519_item_deleted_ts where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230526_indexes.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230526_indexes.hs similarity index 87% rename from src/Simplex/Chat/Migrations/M20230526_indexes.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230526_indexes.hs index 56c61b521b..19a9985c80 100644 --- a/src/Simplex/Chat/Migrations/M20230526_indexes.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230526_indexes.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230526_indexes where +module Simplex.Chat.Store.SQLite.Migrations.M20230526_indexes where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230529_indexes.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230529_indexes.hs similarity index 91% rename from src/Simplex/Chat/Migrations/M20230529_indexes.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230529_indexes.hs index afb12a5c6d..d112c861e5 100644 --- a/src/Simplex/Chat/Migrations/M20230529_indexes.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230529_indexes.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230529_indexes where +module Simplex.Chat.Store.SQLite.Migrations.M20230529_indexes where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230608_deleted_contacts.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230608_deleted_contacts.hs similarity index 87% rename from src/Simplex/Chat/Migrations/M20230608_deleted_contacts.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230608_deleted_contacts.hs index b7193300df..354fa49ca4 100644 --- a/src/Simplex/Chat/Migrations/M20230608_deleted_contacts.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230608_deleted_contacts.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230608_deleted_contacts where +module Simplex.Chat.Store.SQLite.Migrations.M20230608_deleted_contacts where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230618_favorite_chats.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230618_favorite_chats.hs similarity index 86% rename from src/Simplex/Chat/Migrations/M20230618_favorite_chats.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230618_favorite_chats.hs index 66f65a926b..4905093b0f 100644 --- a/src/Simplex/Chat/Migrations/M20230618_favorite_chats.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230618_favorite_chats.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230618_favorite_chats where +module Simplex.Chat.Store.SQLite.Migrations.M20230618_favorite_chats where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230621_chat_item_moderations.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230621_chat_item_moderations.hs similarity index 95% rename from src/Simplex/Chat/Migrations/M20230621_chat_item_moderations.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230621_chat_item_moderations.hs index 449e21e209..015a22f8b2 100644 --- a/src/Simplex/Chat/Migrations/M20230621_chat_item_moderations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230621_chat_item_moderations.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230621_chat_item_moderations where +module Simplex.Chat.Store.SQLite.Migrations.M20230621_chat_item_moderations where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230705_delivery_receipts.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230705_delivery_receipts.hs similarity index 90% rename from src/Simplex/Chat/Migrations/M20230705_delivery_receipts.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230705_delivery_receipts.hs index ec59209d6b..1f4a13cc56 100644 --- a/src/Simplex/Chat/Migrations/M20230705_delivery_receipts.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230705_delivery_receipts.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230705_delivery_receipts where +module Simplex.Chat.Store.SQLite.Migrations.M20230705_delivery_receipts where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230721_group_snd_item_statuses.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230721_group_snd_item_statuses.hs similarity index 92% rename from src/Simplex/Chat/Migrations/M20230721_group_snd_item_statuses.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230721_group_snd_item_statuses.hs index 8453da88f5..6ce99450a4 100644 --- a/src/Simplex/Chat/Migrations/M20230721_group_snd_item_statuses.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230721_group_snd_item_statuses.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230721_group_snd_item_statuses where +module Simplex.Chat.Store.SQLite.Migrations.M20230721_group_snd_item_statuses where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230814_indexes.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230814_indexes.hs similarity index 84% rename from src/Simplex/Chat/Migrations/M20230814_indexes.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230814_indexes.hs index a7419037ef..5e5b2122ca 100644 --- a/src/Simplex/Chat/Migrations/M20230814_indexes.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230814_indexes.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230814_indexes where +module Simplex.Chat.Store.SQLite.Migrations.M20230814_indexes where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230827_file_encryption.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230827_file_encryption.hs similarity index 86% rename from src/Simplex/Chat/Migrations/M20230827_file_encryption.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230827_file_encryption.hs index 2e659cac84..2378df1b61 100644 --- a/src/Simplex/Chat/Migrations/M20230827_file_encryption.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230827_file_encryption.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230827_file_encryption where +module Simplex.Chat.Store.SQLite.Migrations.M20230827_file_encryption where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230829_connections_chat_vrange.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230829_connections_chat_vrange.hs similarity index 91% rename from src/Simplex/Chat/Migrations/M20230829_connections_chat_vrange.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230829_connections_chat_vrange.hs index 2588553a92..66d4f32d70 100644 --- a/src/Simplex/Chat/Migrations/M20230829_connections_chat_vrange.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230829_connections_chat_vrange.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230829_connections_chat_vrange where +module Simplex.Chat.Store.SQLite.Migrations.M20230829_connections_chat_vrange where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230903_connections_to_subscribe.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230903_connections_to_subscribe.hs similarity index 86% rename from src/Simplex/Chat/Migrations/M20230903_connections_to_subscribe.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230903_connections_to_subscribe.hs index 48ad8dbf86..449e94510d 100644 --- a/src/Simplex/Chat/Migrations/M20230903_connections_to_subscribe.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230903_connections_to_subscribe.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230903_connections_to_subscribe where +module Simplex.Chat.Store.SQLite.Migrations.M20230903_connections_to_subscribe where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230913_member_contacts.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230913_member_contacts.hs similarity index 90% rename from src/Simplex/Chat/Migrations/M20230913_member_contacts.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230913_member_contacts.hs index b116373518..b3202745fd 100644 --- a/src/Simplex/Chat/Migrations/M20230913_member_contacts.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230913_member_contacts.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230913_member_contacts where +module Simplex.Chat.Store.SQLite.Migrations.M20230913_member_contacts where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230914_member_probes.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230914_member_probes.hs similarity index 98% rename from src/Simplex/Chat/Migrations/M20230914_member_probes.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230914_member_probes.hs index 8772b6cdad..f5725c3f39 100644 --- a/src/Simplex/Chat/Migrations/M20230914_member_probes.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230914_member_probes.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230914_member_probes where +module Simplex.Chat.Store.SQLite.Migrations.M20230914_member_probes where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20230926_contact_status.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20230926_contact_status.hs similarity index 83% rename from src/Simplex/Chat/Migrations/M20230926_contact_status.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20230926_contact_status.hs index b6c5dd9557..0ec499e5f6 100644 --- a/src/Simplex/Chat/Migrations/M20230926_contact_status.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20230926_contact_status.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20230926_contact_status where +module Simplex.Chat.Store.SQLite.Migrations.M20230926_contact_status where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20231002_conn_initiated.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20231002_conn_initiated.hs similarity index 91% rename from src/Simplex/Chat/Migrations/M20231002_conn_initiated.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20231002_conn_initiated.hs index a0f6009af2..c7c84717bd 100644 --- a/src/Simplex/Chat/Migrations/M20231002_conn_initiated.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20231002_conn_initiated.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20231002_conn_initiated where +module Simplex.Chat.Store.SQLite.Migrations.M20231002_conn_initiated where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20231009_via_group_link_uri_hash.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20231009_via_group_link_uri_hash.hs similarity index 88% rename from src/Simplex/Chat/Migrations/M20231009_via_group_link_uri_hash.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20231009_via_group_link_uri_hash.hs index 41c9887a04..87111c77d4 100644 --- a/src/Simplex/Chat/Migrations/M20231009_via_group_link_uri_hash.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20231009_via_group_link_uri_hash.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20231009_via_group_link_uri_hash where +module Simplex.Chat.Store.SQLite.Migrations.M20231009_via_group_link_uri_hash where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20231010_member_settings.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20231010_member_settings.hs similarity index 84% rename from src/Simplex/Chat/Migrations/M20231010_member_settings.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20231010_member_settings.hs index e31203e572..faf6639ce3 100644 --- a/src/Simplex/Chat/Migrations/M20231010_member_settings.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20231010_member_settings.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20231010_member_settings where +module Simplex.Chat.Store.SQLite.Migrations.M20231010_member_settings where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20231019_indexes.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20231019_indexes.hs similarity index 93% rename from src/Simplex/Chat/Migrations/M20231019_indexes.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20231019_indexes.hs index 40412e1778..cb1f5ec104 100644 --- a/src/Simplex/Chat/Migrations/M20231019_indexes.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20231019_indexes.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20231019_indexes where +module Simplex.Chat.Store.SQLite.Migrations.M20231019_indexes where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20231030_xgrplinkmem_received.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20231030_xgrplinkmem_received.hs similarity index 84% rename from src/Simplex/Chat/Migrations/M20231030_xgrplinkmem_received.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20231030_xgrplinkmem_received.hs index cf4aee2531..1a82aa6d70 100644 --- a/src/Simplex/Chat/Migrations/M20231030_xgrplinkmem_received.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20231030_xgrplinkmem_received.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20231030_xgrplinkmem_received where +module Simplex.Chat.Store.SQLite.Migrations.M20231030_xgrplinkmem_received where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20231107_indexes.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20231107_indexes.hs similarity index 84% rename from src/Simplex/Chat/Migrations/M20231107_indexes.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20231107_indexes.hs index a4c9c5295a..07e6ce5888 100644 --- a/src/Simplex/Chat/Migrations/M20231107_indexes.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20231107_indexes.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20231107_indexes where +module Simplex.Chat.Store.SQLite.Migrations.M20231107_indexes where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20231113_group_forward.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20231113_group_forward.hs similarity index 97% rename from src/Simplex/Chat/Migrations/M20231113_group_forward.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20231113_group_forward.hs index f23387f011..b83a2c780d 100644 --- a/src/Simplex/Chat/Migrations/M20231113_group_forward.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20231113_group_forward.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20231113_group_forward where +module Simplex.Chat.Store.SQLite.Migrations.M20231113_group_forward where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20231114_remote_control.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20231114_remote_control.hs similarity index 95% rename from src/Simplex/Chat/Migrations/M20231114_remote_control.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20231114_remote_control.hs index e716b2aa63..f002c9dd3d 100644 --- a/src/Simplex/Chat/Migrations/M20231114_remote_control.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20231114_remote_control.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20231114_remote_control where +module Simplex.Chat.Store.SQLite.Migrations.M20231114_remote_control where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20231126_remote_ctrl_address.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20231126_remote_ctrl_address.hs similarity index 88% rename from src/Simplex/Chat/Migrations/M20231126_remote_ctrl_address.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20231126_remote_ctrl_address.hs index 343e4ca6fa..98a464219c 100644 --- a/src/Simplex/Chat/Migrations/M20231126_remote_ctrl_address.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20231126_remote_ctrl_address.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20231126_remote_ctrl_address where +module Simplex.Chat.Store.SQLite.Migrations.M20231126_remote_ctrl_address where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20231207_chat_list_pagination.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20231207_chat_list_pagination.hs similarity index 92% rename from src/Simplex/Chat/Migrations/M20231207_chat_list_pagination.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20231207_chat_list_pagination.hs index 9a8944c5c5..f02be82919 100644 --- a/src/Simplex/Chat/Migrations/M20231207_chat_list_pagination.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20231207_chat_list_pagination.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20231207_chat_list_pagination where +module Simplex.Chat.Store.SQLite.Migrations.M20231207_chat_list_pagination where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20231214_item_content_tag.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20231214_item_content_tag.hs similarity index 83% rename from src/Simplex/Chat/Migrations/M20231214_item_content_tag.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20231214_item_content_tag.hs index cd4cd136e5..06ef294702 100644 --- a/src/Simplex/Chat/Migrations/M20231214_item_content_tag.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20231214_item_content_tag.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20231214_item_content_tag where +module Simplex.Chat.Store.SQLite.Migrations.M20231214_item_content_tag where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Migrations/M20231215_recreate_msg_deliveries.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20231215_recreate_msg_deliveries.hs similarity index 98% rename from src/Simplex/Chat/Migrations/M20231215_recreate_msg_deliveries.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20231215_recreate_msg_deliveries.hs index 4b39606d8d..fa2d55e7bb 100644 --- a/src/Simplex/Chat/Migrations/M20231215_recreate_msg_deliveries.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20231215_recreate_msg_deliveries.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Migrations.M20231215_recreate_msg_deliveries where +module Simplex.Chat.Store.SQLite.Migrations.M20231215_recreate_msg_deliveries where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20240102_note_folders.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240102_note_folders.hs new file mode 100644 index 0000000000..f06b3aa0ed --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240102_note_folders.hs @@ -0,0 +1,42 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20240102_note_folders where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240102_note_folders :: Query +m20240102_note_folders = + [sql| +CREATE TABLE note_folders ( + note_folder_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')), + chat_ts TEXT NOT NULL DEFAULT(datetime('now')), + favorite INTEGER NOT NULL DEFAULT 0, + unread_chat INTEGER NOT NULL DEFAULT 0 +); + +ALTER TABLE chat_items ADD COLUMN note_folder_id INTEGER DEFAULT NULL REFERENCES note_folders ON DELETE CASCADE; +ALTER TABLE files ADD COLUMN note_folder_id INTEGER DEFAULT NULL REFERENCES note_folders ON DELETE CASCADE; + +CREATE INDEX chat_items_note_folder_id ON chat_items(note_folder_id); +CREATE INDEX files_note_folder_id ON files(note_folder_id); +CREATE INDEX note_folders_user_id ON note_folders(user_id); + +INSERT INTO note_folders (user_id) SELECT user_id FROM users; +|] + +down_m20240102_note_folders :: Query +down_m20240102_note_folders = + [sql| +DROP INDEX chat_items_note_folder_id; +DROP INDEX files_note_folder_id; +DROP INDEX note_folders_user_id; + +ALTER TABLE chat_items DROP COLUMN note_folder_id; +ALTER TABLE files DROP COLUMN note_folder_id; + +DROP TABLE note_folders; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20240104_members_profile_update.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240104_members_profile_update.hs new file mode 100644 index 0000000000..9e9f813a22 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240104_members_profile_update.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20240104_members_profile_update where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240104_members_profile_update :: Query +m20240104_members_profile_update = + [sql| +ALTER TABLE users ADD COLUMN user_member_profile_updated_at TEXT; +ALTER TABLE groups ADD COLUMN user_member_profile_sent_at TEXT; +|] + +down_m20240104_members_profile_update :: Query +down_m20240104_members_profile_update = + [sql| +ALTER TABLE groups DROP COLUMN user_member_profile_sent_at; +ALTER TABLE users DROP COLUMN user_member_profile_updated_at; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20240115_block_member_for_all.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240115_block_member_for_all.hs new file mode 100644 index 0000000000..9a43ffa55e --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240115_block_member_for_all.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20240115_block_member_for_all where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240115_block_member_for_all :: Query +m20240115_block_member_for_all = + [sql| +ALTER TABLE group_members ADD COLUMN member_restriction TEXT; +|] + +down_m20240115_block_member_for_all :: Query +down_m20240115_block_member_for_all = + [sql| +ALTER TABLE group_members DROP COLUMN member_restriction; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20240122_indexes.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240122_indexes.hs new file mode 100644 index 0000000000..cefc5eda7b --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240122_indexes.hs @@ -0,0 +1,26 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20240122_indexes where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240122_indexes :: Query +m20240122_indexes = + [sql| +CREATE INDEX idx_chat_items_contacts_created_at on chat_items (user_id, contact_id, created_at); +CREATE INDEX idx_chat_items_contacts_item_status on chat_items (user_id, contact_id, item_status); +CREATE INDEX idx_chat_items_groups_item_status on chat_items (user_id, group_id, item_status); +CREATE INDEX idx_chat_items_notes_created_at on chat_items (user_id, note_folder_id, created_at); +CREATE INDEX idx_chat_items_notes_item_status on chat_items (user_id, note_folder_id, item_status); +|] + +down_m20240122_indexes :: Query +down_m20240122_indexes = + [sql| +DROP INDEX idx_chat_items_contacts_created_at; +DROP INDEX idx_chat_items_contacts_item_status; +DROP INDEX idx_chat_items_groups_item_status; +DROP INDEX idx_chat_items_notes_created_at; +DROP INDEX idx_chat_items_notes_item_status; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20240214_redirect_file_id.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240214_redirect_file_id.hs new file mode 100644 index 0000000000..010cee5ca7 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240214_redirect_file_id.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20240214_redirect_file_id where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240214_redirect_file_id :: Query +m20240214_redirect_file_id = + [sql| +ALTER TABLE files ADD COLUMN redirect_file_id INTEGER REFERENCES files ON DELETE CASCADE; + +CREATE INDEX idx_files_redirect_file_id on files(redirect_file_id); +|] + +down_m20240214_redirect_file_id :: Query +down_m20240214_redirect_file_id = + [sql| +DROP INDEX idx_files_redirect_file_id; + +ALTER TABLE files DROP COLUMN redirect_file_id; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20240222_app_settings.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240222_app_settings.hs new file mode 100644 index 0000000000..caa9b8ab77 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240222_app_settings.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20240222_app_settings where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240222_app_settings :: Query +m20240222_app_settings = + [sql| +CREATE TABLE app_settings ( + app_settings TEXT NOT NULL +); +|] + +down_m20240222_app_settings :: Query +down_m20240222_app_settings = + [sql| +DROP TABLE app_settings; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20240226_users_restrict.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240226_users_restrict.hs new file mode 100644 index 0000000000..eb1bc2bfea --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240226_users_restrict.hs @@ -0,0 +1,30 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20240226_users_restrict where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240226_users_restrict :: Query +m20240226_users_restrict = + [sql| +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = replace(sql, 'ON DELETE CASCADE', 'ON DELETE RESTRICT') +WHERE name = 'users' AND type = 'table'; + +PRAGMA writable_schema=0; +|] + +down_m20240226_users_restrict :: Query +down_m20240226_users_restrict = + [sql| +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = replace(sql, 'ON DELETE RESTRICT', 'ON DELETE CASCADE') +WHERE name = 'users' AND type = 'table'; + +PRAGMA writable_schema=0; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20240228_pq.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240228_pq.hs new file mode 100644 index 0000000000..5be3dcc458 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240228_pq.hs @@ -0,0 +1,30 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20240228_pq where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240228_pq :: Query +m20240228_pq = + [sql| +ALTER TABLE connections ADD COLUMN conn_chat_version INTEGER; +ALTER TABLE connections ADD COLUMN pq_support INTEGER NOT NULL DEFAULT 0; +ALTER TABLE connections ADD COLUMN pq_encryption INTEGER NOT NULL DEFAULT 0; +ALTER TABLE connections ADD COLUMN pq_snd_enabled INTEGER; +ALTER TABLE connections ADD COLUMN pq_rcv_enabled INTEGER; + +ALTER TABLE contact_requests ADD COLUMN pq_support INTEGER NOT NULL DEFAULT 0; +|] + +down_m20240228_pq :: Query +down_m20240228_pq = + [sql| +ALTER TABLE contact_requests DROP COLUMN pq_support; + +ALTER TABLE connections DROP COLUMN conn_chat_version; +ALTER TABLE connections DROP COLUMN pq_support; +ALTER TABLE connections DROP COLUMN pq_encryption; +ALTER TABLE connections DROP COLUMN pq_snd_enabled; +ALTER TABLE connections DROP COLUMN pq_rcv_enabled; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20240313_drop_agent_ack_cmd_id.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240313_drop_agent_ack_cmd_id.hs new file mode 100644 index 0000000000..6c5d82ab80 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240313_drop_agent_ack_cmd_id.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20240313_drop_agent_ack_cmd_id where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240313_drop_agent_ack_cmd_id :: Query +m20240313_drop_agent_ack_cmd_id = + [sql| +DROP INDEX idx_msg_deliveries_agent_ack_cmd_id; + +ALTER TABLE msg_deliveries DROP COLUMN agent_ack_cmd_id; +|] + +down_m20240313_drop_agent_ack_cmd_id :: Query +down_m20240313_drop_agent_ack_cmd_id = + [sql| +ALTER TABLE msg_deliveries ADD COLUMN agent_ack_cmd_id INTEGER; + +CREATE INDEX idx_msg_deliveries_agent_ack_cmd_id ON msg_deliveries(connection_id, agent_ack_cmd_id); +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20240324_custom_data.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240324_custom_data.hs new file mode 100644 index 0000000000..e084920ab0 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240324_custom_data.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20240324_custom_data where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240324_custom_data :: Query +m20240324_custom_data = + [sql| +ALTER TABLE contacts ADD COLUMN custom_data BLOB; +ALTER TABLE groups ADD COLUMN custom_data BLOB; +|] + +down_m20240324_custom_data :: Query +down_m20240324_custom_data = + [sql| +ALTER TABLE contacts DROP COLUMN custom_data; +ALTER TABLE groups DROP COLUMN custom_data; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20240402_item_forwarded.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240402_item_forwarded.hs new file mode 100644 index 0000000000..a32f210e3c --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240402_item_forwarded.hs @@ -0,0 +1,36 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20240402_item_forwarded where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240402_item_forwarded :: Query +m20240402_item_forwarded = + [sql| +ALTER TABLE chat_items ADD COLUMN fwd_from_tag TEXT; +ALTER TABLE chat_items ADD COLUMN fwd_from_chat_name TEXT; +ALTER TABLE chat_items ADD COLUMN fwd_from_msg_dir INTEGER; +ALTER TABLE chat_items ADD COLUMN fwd_from_contact_id INTEGER REFERENCES contacts ON DELETE SET NULL; +ALTER TABLE chat_items ADD COLUMN fwd_from_group_id INTEGER REFERENCES groups ON DELETE SET NULL; +ALTER TABLE chat_items ADD COLUMN fwd_from_chat_item_id INTEGER REFERENCES chat_items ON DELETE SET NULL; + +CREATE INDEX idx_chat_items_fwd_from_contact_id ON chat_items(fwd_from_contact_id); +CREATE INDEX idx_chat_items_fwd_from_group_id ON chat_items(fwd_from_group_id); +CREATE INDEX idx_chat_items_fwd_from_chat_item_id ON chat_items(fwd_from_chat_item_id); +|] + +down_m20240402_item_forwarded :: Query +down_m20240402_item_forwarded = + [sql| +DROP INDEX idx_chat_items_fwd_from_contact_id; +DROP INDEX idx_chat_items_fwd_from_group_id; +DROP INDEX idx_chat_items_fwd_from_chat_item_id; + +ALTER TABLE chat_items DROP COLUMN fwd_from_tag; +ALTER TABLE chat_items DROP COLUMN fwd_from_chat_name; +ALTER TABLE chat_items DROP COLUMN fwd_from_msg_dir; +ALTER TABLE chat_items DROP COLUMN fwd_from_contact_id; +ALTER TABLE chat_items DROP COLUMN fwd_from_group_id; +ALTER TABLE chat_items DROP COLUMN fwd_from_chat_item_id; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20240430_ui_theme.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240430_ui_theme.hs new file mode 100644 index 0000000000..a646582a89 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240430_ui_theme.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20240430_ui_theme where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240430_ui_theme :: Query +m20240430_ui_theme = + [sql| +ALTER TABLE users ADD COLUMN ui_themes TEXT; +ALTER TABLE contacts ADD COLUMN ui_themes TEXT; +ALTER TABLE groups ADD COLUMN ui_themes TEXT; +|] + +down_m20240430_ui_theme :: Query +down_m20240430_ui_theme = + [sql| +ALTER TABLE users DROP COLUMN ui_themes; +ALTER TABLE contacts DROP COLUMN ui_themes; +ALTER TABLE groups DROP COLUMN ui_themes; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20240501_chat_deleted.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240501_chat_deleted.hs new file mode 100644 index 0000000000..de8135b066 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240501_chat_deleted.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20240501_chat_deleted where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240501_chat_deleted :: Query +m20240501_chat_deleted = + [sql| +ALTER TABLE contacts ADD COLUMN chat_deleted INTEGER NOT NULL DEFAULT 0; +|] + +down_m20240501_chat_deleted :: Query +down_m20240501_chat_deleted = + [sql| +ALTER TABLE contacts DROP COLUMN chat_deleted; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20240510_chat_items_via_proxy.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240510_chat_items_via_proxy.hs new file mode 100644 index 0000000000..a2fc2cef85 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240510_chat_items_via_proxy.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20240510_chat_items_via_proxy where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240510_chat_items_via_proxy :: Query +m20240510_chat_items_via_proxy = + [sql| +ALTER TABLE chat_items ADD COLUMN via_proxy INTEGER; +ALTER TABLE group_snd_item_statuses ADD COLUMN via_proxy INTEGER; +|] + +down_m20240510_chat_items_via_proxy :: Query +down_m20240510_chat_items_via_proxy = + [sql| +ALTER TABLE chat_items DROP COLUMN via_proxy; +ALTER TABLE group_snd_item_statuses DROP COLUMN via_proxy; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20240515_rcv_files_user_approved_relays.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240515_rcv_files_user_approved_relays.hs new file mode 100644 index 0000000000..341b6c2c41 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240515_rcv_files_user_approved_relays.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20240515_rcv_files_user_approved_relays where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240515_rcv_files_user_approved_relays :: Query +m20240515_rcv_files_user_approved_relays = + [sql| +ALTER TABLE rcv_files ADD COLUMN user_approved_relays INTEGER NOT NULL DEFAULT 0; +|] + +down_m20240515_rcv_files_user_approved_relays :: Query +down_m20240515_rcv_files_user_approved_relays = + [sql| +ALTER TABLE rcv_files DROP COLUMN user_approved_relays; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20240528_quota_err_counter.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240528_quota_err_counter.hs new file mode 100644 index 0000000000..c4d121e068 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240528_quota_err_counter.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20240528_quota_err_counter where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240528_quota_err_counter :: Query +m20240528_quota_err_counter = + [sql| +ALTER TABLE connections ADD COLUMN quota_err_counter INTEGER NOT NULL DEFAULT 0; +|] + +down_m20240528_quota_err_counter :: Query +down_m20240528_quota_err_counter = + [sql| +ALTER TABLE connections DROP COLUMN quota_err_counter; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20240827_calls_uuid.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240827_calls_uuid.hs new file mode 100644 index 0000000000..1d24eabb77 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240827_calls_uuid.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20240827_calls_uuid where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240827_calls_uuid :: Query +m20240827_calls_uuid = + [sql| +ALTER TABLE calls ADD COLUMN call_uuid TEXT NOT NULL DEFAULT ""; +|] + +down_m20240827_calls_uuid :: Query +down_m20240827_calls_uuid = + [sql| +ALTER TABLE calls DROP COLUMN call_uuid; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20240920_user_order.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240920_user_order.hs new file mode 100644 index 0000000000..02fcf37245 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240920_user_order.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20240920_user_order where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240920_user_order :: Query +m20240920_user_order = + [sql| +ALTER TABLE users ADD COLUMN active_order INTEGER NOT NULL DEFAULT 0; +|] + +down_m20240920_user_order :: Query +down_m20240920_user_order = + [sql| +ALTER TABLE users DROP COLUMN active_order; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20241008_indexes.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20241008_indexes.hs new file mode 100644 index 0000000000..a6a905a703 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20241008_indexes.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20241008_indexes where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20241008_indexes :: Query +m20241008_indexes = + [sql| +CREATE INDEX idx_received_probes_group_member_id on received_probes(group_member_id); +|] + +down_m20241008_indexes :: Query +down_m20241008_indexes = + [sql| +DROP INDEX idx_received_probes_group_member_id; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20241010_contact_requests_contact_id.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20241010_contact_requests_contact_id.hs new file mode 100644 index 0000000000..b7a9b74d14 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20241010_contact_requests_contact_id.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20241010_contact_requests_contact_id where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20241010_contact_requests_contact_id :: Query +m20241010_contact_requests_contact_id = + [sql| +ALTER TABLE contact_requests ADD COLUMN contact_id INTEGER REFERENCES contacts ON DELETE CASCADE; + +CREATE INDEX idx_contact_requests_contact_id ON contact_requests(contact_id); +|] + +down_m20241010_contact_requests_contact_id :: Query +down_m20241010_contact_requests_contact_id = + [sql| +DROP INDEX idx_contact_requests_contact_id; + +ALTER TABLE contact_requests DROP COLUMN contact_id; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20241023_chat_item_autoincrement_id.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20241023_chat_item_autoincrement_id.hs new file mode 100644 index 0000000000..03b5c40ed3 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20241023_chat_item_autoincrement_id.hs @@ -0,0 +1,34 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20241023_chat_item_autoincrement_id where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20241023_chat_item_autoincrement_id :: Query +m20241023_chat_item_autoincrement_id = + [sql| +INSERT INTO sqlite_sequence (name, seq) +SELECT 'chat_items', MAX(ROWID) FROM chat_items; + +PRAGMA writable_schema=1; + +UPDATE sqlite_master SET sql = replace(sql, 'INTEGER PRIMARY KEY', 'INTEGER PRIMARY KEY AUTOINCREMENT') +WHERE name = 'chat_items' AND type = 'table'; + +PRAGMA writable_schema=0; +|] + +down_m20241023_chat_item_autoincrement_id :: Query +down_m20241023_chat_item_autoincrement_id = + [sql| +DELETE FROM sqlite_sequence WHERE name = 'chat_items'; + +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = replace(sql, 'INTEGER PRIMARY KEY AUTOINCREMENT', 'INTEGER PRIMARY KEY') +WHERE name = 'chat_items' AND type = 'table'; + +PRAGMA writable_schema=0; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20241027_server_operators.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20241027_server_operators.hs new file mode 100644 index 0000000000..462ab09f5d --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20241027_server_operators.hs @@ -0,0 +1,58 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20241027_server_operators where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20241027_server_operators :: Query +m20241027_server_operators = + [sql| +CREATE TABLE server_operators ( + server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT, + server_operator_tag TEXT, + trade_name TEXT NOT NULL, + legal_name TEXT, + server_domains TEXT, + enabled INTEGER NOT NULL DEFAULT 1, + smp_role_storage INTEGER NOT NULL DEFAULT 1, + smp_role_proxy INTEGER NOT NULL DEFAULT 1, + xftp_role_storage INTEGER NOT NULL DEFAULT 1, + xftp_role_proxy INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE usage_conditions ( + usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, + conditions_commit TEXT NOT NULL UNIQUE, + notified_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE operator_usage_conditions ( + operator_usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, + server_operator_id INTEGER REFERENCES server_operators (server_operator_id) ON DELETE SET NULL ON UPDATE CASCADE, + server_operator_tag TEXT, + conditions_commit TEXT NOT NULL, + accepted_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX idx_operator_usage_conditions_server_operator_id ON operator_usage_conditions(server_operator_id); +CREATE UNIQUE INDEX idx_operator_usage_conditions_conditions_commit ON operator_usage_conditions(conditions_commit, server_operator_id); +|] + +down_m20241027_server_operators :: Query +down_m20241027_server_operators = + [sql| +DROP INDEX idx_operator_usage_conditions_conditions_commit; +DROP INDEX idx_operator_usage_conditions_server_operator_id; + +DROP TABLE operator_usage_conditions; +DROP TABLE usage_conditions; +DROP TABLE server_operators; + +DELETE FROM protocol_servers WHERE host LIKE "%.simplexonflux.com,%"; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20241125_indexes.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20241125_indexes.hs new file mode 100644 index 0000000000..e05b111e99 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20241125_indexes.hs @@ -0,0 +1,50 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20241125_indexes where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20241125_indexes :: Query +m20241125_indexes = + [sql| +-- contacts +DROP INDEX idx_chat_items_contacts; +DROP INDEX idx_chat_items_contacts_item_status; + +CREATE INDEX idx_chat_items_contacts ON chat_items(user_id, contact_id, item_status, created_at); + +-- groups +DROP INDEX idx_chat_items_groups; +DROP INDEX idx_chat_items_groups_item_status; + +CREATE INDEX idx_chat_items_groups ON chat_items(user_id, group_id, item_status, item_ts); +CREATE INDEX idx_chat_items_groups_item_ts ON chat_items(user_id, group_id, item_ts); + +-- notes +DROP INDEX idx_chat_items_notes_item_status; + +CREATE INDEX idx_chat_items_notes ON chat_items(user_id, note_folder_id, item_status, created_at); +|] + +down_m20241125_indexes :: Query +down_m20241125_indexes = + [sql| +-- contacts +DROP INDEX idx_chat_items_contacts; + +CREATE INDEX idx_chat_items_contacts ON chat_items(user_id, contact_id, chat_item_id); +CREATE INDEX idx_chat_items_contacts_item_status on chat_items (user_id, contact_id, item_status); + +-- groups +DROP INDEX idx_chat_items_groups; +DROP INDEX idx_chat_items_groups_item_ts; + +CREATE INDEX idx_chat_items_groups ON chat_items(user_id, group_id, item_ts, chat_item_id); +CREATE INDEX idx_chat_items_groups_item_status on chat_items (user_id, group_id, item_status); + +-- notes +DROP INDEX idx_chat_items_notes; + +CREATE INDEX idx_chat_items_notes_item_status on chat_items (user_id, note_folder_id, item_status); +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20241128_business_chats.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20241128_business_chats.hs new file mode 100644 index 0000000000..486250295a --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20241128_business_chats.hs @@ -0,0 +1,28 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20241128_business_chats where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20241128_business_chats :: Query +m20241128_business_chats = + [sql| +ALTER TABLE user_contact_links ADD business_address INTEGER DEFAULT 0; +ALTER TABLE groups ADD COLUMN business_member_id BLOB NULL; +ALTER TABLE groups ADD COLUMN business_chat TEXT NULL; +ALTER TABLE groups ADD COLUMN business_xcontact_id BLOB NULL; + +CREATE INDEX idx_groups_business_xcontact_id ON groups(business_xcontact_id); +|] + +down_m20241128_business_chats :: Query +down_m20241128_business_chats = + [sql| +DROP INDEX idx_groups_business_xcontact_id; + +ALTER TABLE user_contact_links DROP COLUMN business_address; +ALTER TABLE groups DROP COLUMN business_member_id; +ALTER TABLE groups DROP COLUMN business_chat; +ALTER TABLE groups DROP COLUMN business_xcontact_id; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20241205_business_chat_members.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20241205_business_chat_members.hs new file mode 100644 index 0000000000..fa0cbe36da --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20241205_business_chat_members.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20241205_business_chat_members where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20241205_business_chat_members :: Query +m20241205_business_chat_members = + [sql| +ALTER TABLE groups ADD COLUMN customer_member_id BLOB NULL; +|] + +down_m20241205_business_chat_members :: Query +down_m20241205_business_chat_members = + [sql| +ALTER TABLE groups DROP COLUMN customer_member_id; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20241222_operator_conditions.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20241222_operator_conditions.hs new file mode 100644 index 0000000000..761d883ead --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20241222_operator_conditions.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20241222_operator_conditions where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20241222_operator_conditions :: Query +m20241222_operator_conditions = + [sql| +ALTER TABLE operator_usage_conditions ADD COLUMN auto_accepted INTEGER DEFAULT 0; +|] + +down_m20241222_operator_conditions :: Query +down_m20241222_operator_conditions = + [sql| +ALTER TABLE operator_usage_conditions DROP COLUMN auto_accepted; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20241223_chat_tags.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20241223_chat_tags.hs new file mode 100644 index 0000000000..4c2d4d1745 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20241223_chat_tags.hs @@ -0,0 +1,47 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20241223_chat_tags where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20241223_chat_tags :: Query +m20241223_chat_tags = + [sql| +CREATE TABLE chat_tags ( + chat_tag_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER REFERENCES users, + chat_tag_text TEXT NOT NULL, + chat_tag_emoji TEXT, + tag_order INTEGER NOT NULL +); + +CREATE TABLE chat_tags_chats ( + contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, + group_id INTEGER REFERENCES groups ON DELETE CASCADE, + chat_tag_id INTEGER NOT NULL REFERENCES chat_tags ON DELETE CASCADE +); + +CREATE INDEX idx_chat_tags_user_id ON chat_tags(user_id); +CREATE UNIQUE INDEX idx_chat_tags_user_id_chat_tag_text ON chat_tags(user_id, chat_tag_text); +CREATE UNIQUE INDEX idx_chat_tags_user_id_chat_tag_emoji ON chat_tags(user_id, chat_tag_emoji); + +CREATE INDEX idx_chat_tags_chats_chat_tag_id ON chat_tags_chats(chat_tag_id); +CREATE UNIQUE INDEX idx_chat_tags_chats_chat_tag_id_contact_id ON chat_tags_chats(contact_id, chat_tag_id); +CREATE UNIQUE INDEX idx_chat_tags_chats_chat_tag_id_group_id ON chat_tags_chats(group_id, chat_tag_id); +|] + +down_m20241223_chat_tags :: Query +down_m20241223_chat_tags = + [sql| +DROP INDEX idx_chat_tags_user_id; +DROP INDEX idx_chat_tags_user_id_chat_tag_text; +DROP INDEX idx_chat_tags_user_id_chat_tag_emoji; + +DROP INDEX idx_chat_tags_chats_chat_tag_id; +DROP INDEX idx_chat_tags_chats_chat_tag_id_contact_id; +DROP INDEX idx_chat_tags_chats_chat_tag_id_group_id; + +DROP TABLE chat_tags_chats; +DROP TABLE chat_tags; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20241230_reports.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20241230_reports.hs new file mode 100644 index 0000000000..60c2b51525 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20241230_reports.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20241230_reports where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20241230_reports :: Query +m20241230_reports = + [sql| +ALTER TABLE chat_items ADD COLUMN msg_content_tag TEXT; +|] + +down_m20241230_reports :: Query +down_m20241230_reports = + [sql| +ALTER TABLE chat_items DROP COLUMN msg_content_tag; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250105_indexes.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250105_indexes.hs new file mode 100644 index 0000000000..fe465710b7 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250105_indexes.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20250105_indexes where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20250105_indexes :: Query +m20250105_indexes = + [sql| +CREATE INDEX idx_chat_items_groups_msg_content_tag_item_ts ON chat_items(user_id, group_id, msg_content_tag, item_ts); +CREATE INDEX idx_chat_items_groups_msg_content_tag_deleted ON chat_items(user_id, group_id, msg_content_tag, item_deleted, item_sent); +|] + +down_m20250105_indexes :: Query +down_m20250105_indexes = + [sql| +DROP INDEX idx_chat_items_groups_msg_content_tag_item_ts; +DROP INDEX idx_chat_items_groups_msg_content_tag_deleted; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250115_chat_ttl.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250115_chat_ttl.hs new file mode 100644 index 0000000000..3e52890f86 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250115_chat_ttl.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20250115_chat_ttl where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20250115_chat_ttl :: Query +m20250115_chat_ttl = + [sql| +ALTER TABLE contacts ADD COLUMN chat_item_ttl INTEGER; +ALTER TABLE groups ADD COLUMN chat_item_ttl INTEGER; +ALTER TABLE groups ADD COLUMN local_alias TEXT DEFAULT ''; +|] + +down_m20250115_chat_ttl :: Query +down_m20250115_chat_ttl = + [sql| +ALTER TABLE contacts DROP COLUMN chat_item_ttl; +ALTER TABLE groups DROP COLUMN chat_item_ttl; +ALTER TABLE groups DROP COLUMN local_alias; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250122_chat_items_include_in_history.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250122_chat_items_include_in_history.hs new file mode 100644 index 0000000000..afc8544b4a --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250122_chat_items_include_in_history.hs @@ -0,0 +1,39 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20250122_chat_items_include_in_history where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20250122_chat_items_include_in_history :: Query +m20250122_chat_items_include_in_history = + [sql| +ALTER TABLE chat_items ADD COLUMN include_in_history INTEGER NOT NULL DEFAULT 0; + +CREATE INDEX idx_chat_items_groups_history ON chat_items( + user_id, + group_id, + include_in_history, + item_deleted, + item_ts, + chat_item_id +); + +UPDATE chat_items +SET include_in_history = 1 +WHERE group_id IS NOT NULL + AND item_content_tag IN ('rcvMsgContent', 'sndMsgContent') + AND msg_content_tag NOT IN ('report'); + +CREATE INDEX idx_group_snd_item_statuses_chat_item_id_group_member_id ON group_snd_item_statuses(chat_item_id, group_member_id); +|] + +down_m20250122_chat_items_include_in_history :: Query +down_m20250122_chat_items_include_in_history = + [sql| +DROP INDEX idx_group_snd_item_statuses_chat_item_id_group_member_id; + +DROP INDEX idx_chat_items_groups_history; + +ALTER TABLE chat_items DROP COLUMN include_in_history; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250126_mentions.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250126_mentions.hs new file mode 100644 index 0000000000..a920c17859 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250126_mentions.hs @@ -0,0 +1,41 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20250126_mentions where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20250126_mentions :: Query +m20250126_mentions = + [sql| +ALTER TABLE chat_items ADD COLUMN user_mention INTEGER NOT NULL DEFAULT 0; + +CREATE TABLE chat_item_mentions ( + chat_item_mention_id INTEGER PRIMARY KEY AUTOINCREMENT, + group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, + member_id BLOB NOT NULL, + chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, + display_name TEXT NOT NULL +); + +CREATE INDEX idx_chat_item_mentions_group_id ON chat_item_mentions(group_id); +CREATE INDEX idx_chat_item_mentions_chat_item_id ON chat_item_mentions(chat_item_id); +CREATE UNIQUE INDEX idx_chat_item_mentions_display_name ON chat_item_mentions(chat_item_id, display_name); +CREATE UNIQUE INDEX idx_chat_item_mentions_member_id ON chat_item_mentions(chat_item_id, member_id); + +CREATE INDEX idx_chat_items_groups_user_mention ON chat_items(user_id, group_id, item_status, user_mention); +|] + +down_m20250126_mentions :: Query +down_m20250126_mentions = + [sql| +DROP INDEX idx_chat_items_groups_user_mention; + +DROP INDEX idx_chat_item_mentions_group_id; +DROP INDEX idx_chat_item_mentions_chat_item_id; +DROP INDEX idx_chat_item_mentions_display_name; +DROP INDEX idx_chat_item_mentions_member_id; + +DROP TABLE chat_item_mentions; +ALTER TABLE chat_items DROP COLUMN user_mention; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250129_delete_unused_contacts.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250129_delete_unused_contacts.hs new file mode 100644 index 0000000000..91b3f688c1 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250129_delete_unused_contacts.hs @@ -0,0 +1,66 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20250129_delete_unused_contacts where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20250129_delete_unused_contacts :: Query +m20250129_delete_unused_contacts = + [sql| +CREATE TEMPORARY TABLE temp_delete_contacts ( + contact_id INTEGER PRIMARY KEY, + contact_profile_id INTEGER NOT NULL, + local_display_name TEXT NOT NULL +); + +INSERT INTO temp_delete_contacts(contact_id, contact_profile_id, local_display_name) +SELECT contact_id, contact_profile_id, local_display_name +FROM contacts +WHERE contact_used = 0 AND is_user = 0 + AND contact_id NOT IN (SELECT contact_id FROM users) + AND contact_id NOT IN (SELECT contact_id FROM contact_requests); + +CREATE TEMPORARY TABLE temp_delete_profiles (contact_profile_id INTEGER PRIMARY KEY); + +INSERT OR IGNORE INTO temp_delete_profiles(contact_profile_id) +SELECT custom_user_profile_id FROM connections +WHERE contact_id IN (SELECT contact_id FROM temp_delete_contacts) + AND custom_user_profile_id IS NOT NULL; + +UPDATE group_members SET contact_id = NULL +WHERE contact_id IN (SELECT contact_id FROM temp_delete_contacts); + +DELETE FROM connections +WHERE contact_id IN (SELECT contact_id FROM temp_delete_contacts); + +DELETE FROM contacts +WHERE contact_id IN (SELECT contact_id FROM temp_delete_contacts); + +DELETE FROM contact_profiles +WHERE + (contact_profile_id IN (SELECT contact_profile_id FROM temp_delete_profiles) + OR contact_profile_id IN (SELECT contact_profile_id FROM temp_delete_contacts)) + AND contact_profile_id NOT IN (SELECT contact_profile_id FROM group_members) + AND contact_profile_id NOT IN (SELECT member_profile_id FROM group_members) + AND contact_profile_id NOT IN (SELECT contact_profile_id FROM contacts) + AND contact_profile_id NOT IN (SELECT contact_profile_id FROM contact_requests) + AND contact_profile_id NOT IN (SELECT custom_user_profile_id FROM connections); + +DELETE FROM display_names +WHERE local_display_name IN (SELECT local_display_name FROM temp_delete_contacts) + AND local_display_name NOT IN (SELECT local_display_name FROM group_members) + AND local_display_name NOT IN (SELECT local_display_name FROM contacts) + AND local_display_name NOT IN (SELECT local_display_name FROM users) + AND local_display_name NOT IN (SELECT local_display_name FROM groups) + AND local_display_name NOT IN (SELECT local_display_name FROM user_contact_links) + AND local_display_name NOT IN (SELECT local_display_name FROM contact_requests); + +DROP TABLE temp_delete_contacts; +DROP TABLE temp_delete_profiles; +|] + +down_m20250129_delete_unused_contacts :: Query +down_m20250129_delete_unused_contacts = + [sql| +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250130_indexes.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250130_indexes.hs new file mode 100644 index 0000000000..c0dbd05fcf --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250130_indexes.hs @@ -0,0 +1,36 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20250130_indexes where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20250130_indexes :: Query +m20250130_indexes = + [sql| +CREATE INDEX idx_chat_items_group_id ON chat_items(group_id); + +CREATE INDEX idx_connections_group_member_id ON connections(group_member_id); + +DROP INDEX idx_chat_items_shared_msg_id; +CREATE INDEX idx_chat_items_group_id_shared_msg_id ON chat_items(group_id, shared_msg_id); + + +DROP INDEX idx_groups_host_conn_custom_user_profile_id; +ALTER TABLE groups DROP COLUMN host_conn_custom_user_profile_id; +|] + +down_m20250130_indexes :: Query +down_m20250130_indexes = + [sql| +ALTER TABLE groups ADD COLUMN host_conn_custom_user_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL; +CREATE INDEX idx_groups_host_conn_custom_user_profile_id ON groups(host_conn_custom_user_profile_id); + + +DROP INDEX idx_chat_items_group_id_shared_msg_id; +CREATE INDEX idx_chat_items_shared_msg_id ON chat_items(shared_msg_id); + +DROP INDEX idx_connections_group_member_id; + +DROP INDEX idx_chat_items_group_id; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250402_short_links.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250402_short_links.hs new file mode 100644 index 0000000000..62637c0782 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250402_short_links.hs @@ -0,0 +1,23 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20250402_short_links where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20250402_short_links :: Query +m20250402_short_links = + [sql| +ALTER TABLE user_contact_links ADD COLUMN short_link_contact BLOB; +ALTER TABLE connections ADD COLUMN short_link_inv BLOB; +ALTER TABLE connections ADD COLUMN via_short_link_contact BLOB; + +|] + +down_m20250402_short_links :: Query +down_m20250402_short_links = + [sql| +ALTER TABLE user_contact_links DROP COLUMN short_link_contact; +ALTER TABLE connections DROP COLUMN short_link_inv; +ALTER TABLE connections DROP COLUMN via_short_link_contact; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250512_member_admission.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250512_member_admission.hs new file mode 100644 index 0000000000..e1f45beef1 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250512_member_admission.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20250512_member_admission where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20250512_member_admission :: Query +m20250512_member_admission = + [sql| +ALTER TABLE group_profiles ADD COLUMN member_admission TEXT; +|] + +down_m20250512_member_admission :: Query +down_m20250512_member_admission = + [sql| +ALTER TABLE group_profiles DROP COLUMN member_admission; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt new file mode 100644 index 0000000000..a85ba4a4cb --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt @@ -0,0 +1,1177 @@ +Query: + SELECT + f.snd_file_id, f.snd_file_entity_id, f.user_id, f.num_recipients, f.prefix_path, + c.snd_file_chunk_id, c.chunk_no, c.chunk_offset, c.chunk_size, c.digest, + r.snd_file_chunk_replica_id, r.replica_id, r.replica_key, r.replica_status, r.delay, r.retries + FROM snd_file_chunk_replicas r + JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id + JOIN snd_file_chunks c ON c.snd_file_chunk_id = r.snd_file_chunk_id + JOIN snd_files f ON f.snd_file_id = c.snd_file_id + WHERE r.snd_file_chunk_replica_id = ? + +Plan: +SEARCH r USING INTEGER PRIMARY KEY (rowid=?) +SEARCH s USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INTEGER PRIMARY KEY (rowid=?) +SEARCH f USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT + m.msg_type, m.msg_flags, m.msg_body, m.pq_encryption, m.internal_ts, m.internal_snd_id, s.previous_msg_hash, + s.retry_int_slow, s.retry_int_fast, s.msg_encrypt_key, s.padded_msg_len, sb.agent_msg + FROM messages m + JOIN snd_messages s ON s.conn_id = m.conn_id AND s.internal_id = m.internal_id + LEFT JOIN snd_message_bodies sb ON sb.snd_message_body_id = s.snd_message_body_id + WHERE m.conn_id = ? AND m.internal_id = ? + +Plan: +SEARCH m USING PRIMARY KEY (conn_id=? AND internal_id=?) +SEARCH s USING PRIMARY KEY (conn_id=?) +SEARCH sb USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN + +Query: + SELECT + r.snd_file_chunk_replica_id, r.replica_id, r.replica_key, r.replica_status, r.delay, r.retries, + s.xftp_host, s.xftp_port, s.xftp_key_hash + FROM snd_file_chunk_replicas r + JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id + WHERE r.snd_file_chunk_id = ? + +Plan: +SEARCH r USING INDEX idx_snd_file_chunk_replicas_snd_file_chunk_id (snd_file_chunk_id=?) +SEARCH s USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT command_id FROM commands + WHERE conn_id = ? AND host = ? AND port = ? AND failed = 0 + ORDER BY created_at ASC, command_id ASC + LIMIT 1 + +Plan: +SEARCH commands USING INDEX idx_commands_server_commands (host=? AND port=?) + +Query: + SELECT command_id FROM commands + WHERE conn_id = ? AND host IS NULL AND port IS NULL AND failed = 0 + ORDER BY created_at ASC, command_id ASC + LIMIT 1 + +Plan: +SEARCH commands USING INDEX idx_commands_server_commands (host=? AND port=?) + +Query: + SELECT rcv_file_chunk_id, chunk_no, chunk_size, digest, tmp_path + FROM rcv_file_chunks + WHERE rcv_file_id = ? + +Plan: +SEARCH rcv_file_chunks USING INDEX idx_rcv_file_chunks_rcv_file_id (rcv_file_id=?) + +Query: + SELECT snd_file_chunk_id, chunk_no, chunk_offset, chunk_size, digest + FROM snd_file_chunks + WHERE snd_file_id = ? + +Plan: +SEARCH snd_file_chunks USING INDEX idx_snd_file_chunks_snd_file_id (snd_file_id=?) + +Query: + DELETE FROM snd_message_bodies + WHERE NOT EXISTS (SELECT 1 FROM snd_messages WHERE snd_message_body_id = ?) + AND snd_message_body_id = ? + +Plan: +SEARCH snd_message_bodies USING INTEGER PRIMARY KEY (rowid=?) +SCALAR SUBQUERY 1 +SEARCH snd_messages USING COVERING INDEX idx_snd_messages_snd_message_body_id (snd_message_body_id=?) +SEARCH snd_messages USING COVERING INDEX idx_snd_messages_snd_message_body_id (snd_message_body_id=?) + +Query: + SELECT + f.rcv_file_id, f.rcv_file_entity_id, f.user_id, c.rcv_file_chunk_id, c.chunk_no, c.chunk_size, c.digest, f.tmp_path, c.tmp_path, + r.rcv_file_chunk_replica_id, r.replica_id, r.replica_key, r.received, r.delay, r.retries, + f.approved_relays, f.redirect_entity_id + FROM rcv_file_chunk_replicas r + JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id + JOIN rcv_file_chunks c ON c.rcv_file_chunk_id = r.rcv_file_chunk_id + JOIN rcv_files f ON f.rcv_file_id = c.rcv_file_id + WHERE r.rcv_file_chunk_replica_id = ? + +Plan: +SEARCH r USING INTEGER PRIMARY KEY (rowid=?) +SEARCH s USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INTEGER PRIMARY KEY (rowid=?) +SEARCH f USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT + r.rcv_file_chunk_replica_id, r.replica_id, r.replica_key, r.received, r.delay, r.retries, + s.xftp_host, s.xftp_port, s.xftp_key_hash + FROM rcv_file_chunk_replicas r + JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id + WHERE r.rcv_file_chunk_id = ? + +Plan: +SEARCH r USING INDEX idx_rcv_file_chunk_replicas_rcv_file_chunk_id (rcv_file_chunk_id=?) +SEARCH s USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT c.corr_id, cs.user_id, c.command + FROM commands c + JOIN connections cs USING (conn_id) + WHERE c.command_id = ? + +Plan: +SEARCH c USING INTEGER PRIMARY KEY (rowid=?) +SEARCH cs USING PRIMARY KEY (conn_id=?) + +Query: + SELECT internal_id + FROM snd_message_deliveries + WHERE conn_id = ? AND snd_queue_id = ? AND failed = 0 AND internal_id <= ? + ORDER BY internal_id ASC + +Plan: +SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_expired (conn_id=? AND snd_queue_id=? AND failed=? AND internal_id<?) + +Query: + SELECT internal_id + FROM snd_message_deliveries d + WHERE conn_id = ? AND snd_queue_id = ? AND failed = 0 + ORDER BY internal_id ASC + LIMIT 1 + +Plan: +SEARCH d USING COVERING INDEX idx_snd_message_deliveries_expired (conn_id=? AND snd_queue_id=? AND failed=?) + +Query: + SELECT r.deleted_snd_chunk_replica_id + FROM deleted_snd_chunk_replicas r + JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id + WHERE s.xftp_host = ? AND s.xftp_port = ? AND s.xftp_key_hash = ? + AND r.created_at >= ? + AND failed = 0 + ORDER BY r.retries ASC, r.created_at ASC + LIMIT 1 + +Plan: +SEARCH s USING COVERING INDEX sqlite_autoindex_xftp_servers_1 (xftp_host=? AND xftp_port=? AND xftp_key_hash=?) +SEARCH r USING INDEX idx_deleted_snd_chunk_replicas_xftp_server_id (xftp_server_id=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT r.rcv_file_chunk_replica_id, f.rcv_file_id + FROM rcv_file_chunk_replicas r + JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id + JOIN rcv_file_chunks c ON c.rcv_file_chunk_id = r.rcv_file_chunk_id + JOIN rcv_files f ON f.rcv_file_id = c.rcv_file_id + WHERE s.xftp_host = ? AND s.xftp_port = ? AND s.xftp_key_hash = ? + AND r.received = 0 AND r.replica_number = 1 + AND f.status = ? AND f.deleted = 0 AND f.created_at >= ? + AND f.failed = 0 + ORDER BY r.retries ASC, r.created_at ASC + LIMIT 1 + +Plan: +SEARCH s USING COVERING INDEX sqlite_autoindex_xftp_servers_1 (xftp_host=? AND xftp_port=? AND xftp_key_hash=?) +SEARCH r USING INDEX idx_rcv_file_chunk_replicas_pending (received=? AND replica_number=?) +SEARCH c USING INTEGER PRIMARY KEY (rowid=?) +SEARCH f USING INTEGER PRIMARY KEY (rowid=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT r.snd_file_chunk_replica_id, f.snd_file_id + FROM snd_file_chunk_replicas r + JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id + JOIN snd_file_chunks c ON c.snd_file_chunk_id = r.snd_file_chunk_id + JOIN snd_files f ON f.snd_file_id = c.snd_file_id + WHERE s.xftp_host = ? AND s.xftp_port = ? AND s.xftp_key_hash = ? + AND r.replica_status = ? AND r.replica_number = 1 + AND (f.status = ? OR f.status = ?) AND f.deleted = 0 AND f.created_at >= ? + AND f.failed = 0 + ORDER BY r.retries ASC, r.created_at ASC + LIMIT 1 + +Plan: +SEARCH s USING COVERING INDEX sqlite_autoindex_xftp_servers_1 (xftp_host=? AND xftp_port=? AND xftp_key_hash=?) +SEARCH r USING INDEX idx_snd_file_chunk_replicas_pending (replica_status=? AND replica_number=?) +SEARCH c USING INTEGER PRIMARY KEY (rowid=?) +SEARCH f USING INTEGER PRIMARY KEY (rowid=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT rcpt_status, snd_message_body_id FROM snd_messages + WHERE NOT EXISTS (SELECT 1 FROM snd_message_deliveries WHERE conn_id = ? AND internal_id = ? AND failed = 0) + AND conn_id = ? AND internal_id = ? + +Plan: +SEARCH snd_messages USING PRIMARY KEY (conn_id=?) +SCALAR SUBQUERY 1 +SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_expired (conn_id=?) + +Query: + SELECT rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status, deleted, redirect_id, redirect_entity_id, redirect_size, redirect_digest + FROM rcv_files + WHERE rcv_file_id = ? + +Plan: +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT rcv_file_id + FROM rcv_files + WHERE status IN (?,?) AND deleted = 0 AND created_at >= ? + AND failed = 0 + ORDER BY created_at ASC LIMIT 1 + +Plan: +SEARCH rcv_files USING INDEX idx_rcv_files_status_created_at (status=? AND created_at>?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT snd_file_entity_id, user_id, path, src_file_key, src_file_nonce, num_recipients, digest, prefix_path, key, nonce, status, deleted, redirect_size, redirect_digest + FROM snd_files + WHERE snd_file_id = ? + +Plan: +SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT snd_file_id + FROM snd_files + WHERE status IN (?,?,?) AND deleted = 0 AND created_at >= ? + AND failed = 0 + ORDER BY created_at ASC LIMIT 1 + +Plan: +SEARCH snd_files USING INDEX idx_snd_files_status_created_at (status=? AND created_at>?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT MAX(internal_id) + FROM messages + WHERE conn_id = ? AND internal_snd_id IS NOT NULL AND internal_ts < ? + +Plan: +SEARCH messages USING PRIMARY KEY (conn_id=?) + +Query: + SELECT user_id FROM users u + WHERE u.user_id = ? + AND u.deleted = ? + AND NOT EXISTS (SELECT c.conn_id FROM connections c WHERE c.user_id = u.user_id) + +Plan: +SEARCH u USING INTEGER PRIMARY KEY (rowid=?) +CORRELATED SCALAR SUBQUERY 1 +SEARCH c USING COVERING INDEX idx_connections_user (user_id=?) + +Query: + INSERT INTO conn_confirmations + (confirmation_id, conn_id, sender_key, e2e_snd_pub_key, ratchet_state, sender_conn_info, smp_reply_queues, smp_client_version, accepted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0); + +Plan: + +Query: + INSERT INTO conn_invitations + (invitation_id, contact_conn_id, cr_invitation, recipient_conn_info, accepted) VALUES (?, ?, ?, ?, 0); + +Plan: + +Query: + INSERT INTO snd_file_chunk_replica_recipients + (snd_file_chunk_replica_id, rcv_replica_id, rcv_replica_key) + VALUES (?,?,?) + +Plan: + +Query: + SELECT + r.internal_id, m.internal_ts, r.broker_id, r.broker_ts, r.external_snd_id, r.integrity, r.internal_hash, + m.msg_type, m.msg_body, m.pq_encryption, s.internal_id, s.rcpt_status, r.user_ack + FROM rcv_messages r + JOIN messages m ON r.conn_id = m.conn_id AND r.internal_id = m.internal_id + JOIN connections c ON r.conn_id = c.conn_id AND c.last_internal_msg_id = r.internal_id + LEFT JOIN snd_messages s ON s.conn_id = r.conn_id AND s.rcpt_internal_id = r.internal_id + WHERE r.conn_id = ? AND r.broker_id = ? + +Plan: +SEARCH c USING PRIMARY KEY (conn_id=?) +SEARCH m USING PRIMARY KEY (conn_id=? AND internal_id=?) +SEARCH r USING PRIMARY KEY (conn_id=?) +SEARCH s USING PRIMARY KEY (conn_id=?) LEFT-JOIN + +Query: + SELECT + r.internal_id, m.internal_ts, r.broker_id, r.broker_ts, r.external_snd_id, r.integrity, r.internal_hash, + m.msg_type, m.msg_body, m.pq_encryption, s.internal_id, s.rcpt_status, r.user_ack + FROM rcv_messages r + JOIN messages m ON r.conn_id = m.conn_id AND r.internal_id = m.internal_id + LEFT JOIN snd_messages s ON s.conn_id = r.conn_id AND s.rcpt_internal_id = r.internal_id + WHERE r.conn_id = ? AND r.internal_id = ? + +Plan: +SEARCH m USING PRIMARY KEY (conn_id=? AND internal_id=?) +SEARCH r USING PRIMARY KEY (conn_id=?) +SEARCH s USING PRIMARY KEY (conn_id=?) LEFT-JOIN + +Query: + SELECT + r.user_id, r.replica_id, r.replica_key, r.chunk_digest, r.delay, r.retries, + s.xftp_host, s.xftp_port, s.xftp_key_hash + FROM deleted_snd_chunk_replicas r + JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id + WHERE r.deleted_snd_chunk_replica_id = ? + +Plan: +SEARCH r USING INTEGER PRIMARY KEY (rowid=?) +SEARCH s USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT + user_id, conn_id, conn_mode, smp_agent_version, enable_ntfs, + last_external_snd_msg_id, deleted, ratchet_sync_state, pq_support + FROM connections + WHERE conn_id = ? + +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) + +Query: + SELECT DISTINCT + s.xftp_host, s.xftp_port, s.xftp_key_hash + FROM deleted_snd_chunk_replicas r + JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id + WHERE r.created_at >= ? + +Plan: +SEARCH r USING INDEX idx_deleted_snd_chunk_replicas_pending (created_at>?) +SEARCH s USING INTEGER PRIMARY KEY (rowid=?) +USE TEMP B-TREE FOR DISTINCT + +Query: + SELECT DISTINCT + s.xftp_host, s.xftp_port, s.xftp_key_hash + FROM rcv_file_chunk_replicas r + JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id + JOIN rcv_file_chunks c ON c.rcv_file_chunk_id = r.rcv_file_chunk_id + JOIN rcv_files f ON f.rcv_file_id = c.rcv_file_id + WHERE r.received = 0 AND r.replica_number = 1 + AND f.status = ? AND f.deleted = 0 AND f.created_at >= ? + +Plan: +SEARCH r USING INDEX idx_rcv_file_chunk_replicas_pending (received=? AND replica_number=?) +SEARCH c USING INTEGER PRIMARY KEY (rowid=?) +SEARCH f USING INTEGER PRIMARY KEY (rowid=?) +SEARCH s USING INTEGER PRIMARY KEY (rowid=?) +USE TEMP B-TREE FOR DISTINCT + +Query: + SELECT DISTINCT + s.xftp_host, s.xftp_port, s.xftp_key_hash + FROM snd_file_chunk_replicas r + JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id + JOIN snd_file_chunks c ON c.snd_file_chunk_id = r.snd_file_chunk_id + JOIN snd_files f ON f.snd_file_id = c.snd_file_id + WHERE r.replica_status = ? AND r.replica_number = 1 + AND (f.status = ? OR f.status = ?) AND f.deleted = 0 AND f.created_at >= ? + +Plan: +SEARCH r USING INDEX idx_snd_file_chunk_replicas_pending (replica_status=? AND replica_number=?) +SEARCH c USING INTEGER PRIMARY KEY (rowid=?) +SEARCH f USING INTEGER PRIMARY KEY (rowid=?) +SEARCH s USING INTEGER PRIMARY KEY (rowid=?) +USE TEMP B-TREE FOR DISTINCT + +Query: + SELECT DISTINCT c.host, c.port, COALESCE(c.server_key_hash, s.key_hash) + FROM commands c + LEFT JOIN servers s ON s.host = c.host AND s.port = c.port + WHERE conn_id = ? + +Plan: +SEARCH c USING INDEX idx_commands_conn_id (conn_id=?) +SEARCH s USING PRIMARY KEY (host=? AND port=?) LEFT-JOIN +USE TEMP B-TREE FOR DISTINCT + +Query: + SELECT DISTINCT ntf_host, ntf_port, ntf_key_hash + FROM ntf_tokens_to_delete + +Plan: +SCAN ntf_tokens_to_delete +USE TEMP B-TREE FOR DISTINCT + +Query: + SELECT confirmation_id, ratchet_state, own_conn_info, sender_key, e2e_snd_pub_key, sender_conn_info, smp_reply_queues, smp_client_version + FROM conn_confirmations + WHERE conn_id = ? AND accepted = 1; + +Plan: +SEARCH conn_confirmations USING INDEX idx_conn_confirmations_conn_id (conn_id=?) + +Query: + SELECT conn_id, ratchet_state, sender_key, e2e_snd_pub_key, sender_conn_info, smp_reply_queues, smp_client_version + FROM conn_confirmations + WHERE confirmation_id = ?; + +Plan: +SEARCH conn_confirmations USING PRIMARY KEY (confirmation_id=?) + +Query: + SELECT contact_conn_id, cr_invitation, recipient_conn_info, own_conn_info, accepted + FROM conn_invitations + WHERE invitation_id = ? + AND accepted = 0 + +Plan: +SEARCH conn_invitations USING PRIMARY KEY (invitation_id=?) + +Query: + SELECT last_internal_msg_id, last_internal_rcv_msg_id, last_external_snd_msg_id, last_rcv_msg_hash + FROM connections + WHERE conn_id = ? + +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) + +Query: + SELECT last_internal_msg_id, last_internal_snd_msg_id, last_snd_msg_hash + FROM connections + WHERE conn_id = ? + +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) + +Query: + SELECT link_id, snd_private_key + FROM inv_short_links + WHERE host = ? AND port = ? AND snd_id = ? + +Plan: +SEARCH inv_short_links USING INDEX idx_inv_short_links_link_id (host=? AND port=?) + +Query: + SELECT link_key, snd_private_key, snd_id + FROM inv_short_links + WHERE host = ? AND port = ? AND link_id = ? + +Plan: +SEARCH inv_short_links USING INDEX idx_inv_short_links_link_id (host=? AND port=? AND link_id=?) + +Query: + SELECT s.internal_id, m.msg_type, s.internal_hash, s.rcpt_internal_id, s.rcpt_status + FROM snd_messages s + JOIN messages m ON s.conn_id = m.conn_id AND s.internal_id = m.internal_id + WHERE s.conn_id = ? AND s.internal_snd_id = ? + +Plan: +SEARCH s USING PRIMARY KEY (conn_id=? AND internal_snd_id=?) +SEARCH m USING PRIMARY KEY (conn_id=? AND internal_id=?) + +Query: + DELETE FROM conn_confirmations + WHERE conn_id = ? + +Plan: +SEARCH conn_confirmations USING COVERING INDEX idx_conn_confirmations_conn_id (conn_id=?) + +Query: + INSERT INTO connections + (user_id, conn_id, conn_mode, smp_agent_version, enable_ntfs, pq_support, duplex_handshake) VALUES (?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO inv_short_links + (host, port, server_key_hash, link_id, link_key, snd_private_key, snd_id) + VALUES (?,?,?,?,?,?,?) + ON CONFLICT (host, port, link_id) + DO UPDATE SET + server_key_hash = EXCLUDED.server_key_hash, + link_key = EXCLUDED.link_key, + snd_private_key = EXCLUDED.snd_private_key, + snd_id = EXCLUDED.snd_id + +Plan: + +Query: + INSERT INTO messages + (conn_id, internal_id, internal_ts, internal_rcv_id, internal_snd_id, msg_type, msg_flags, msg_body, pq_encryption) + VALUES (?,?,?,?,?,?,?,?,?); + +Plan: +SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_conn_id_internal_id (conn_id=? AND internal_id=?) + +Query: + INSERT INTO messages + (conn_id, internal_id, internal_ts, internal_rcv_id, internal_snd_id, msg_type, msg_flags, msg_body, pq_encryption) + VALUES + (?,?,?,?,?,?,?,?,?); + +Plan: +SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_conn_id_internal_id (conn_id=? AND internal_id=?) + +Query: + INSERT INTO ratchets + (conn_id, ratchet_state, x3dh_pub_key_1, x3dh_pub_key_2, pq_pub_kem) VALUES (?, ?, ?, ?, ?) + ON CONFLICT (conn_id) DO UPDATE SET + ratchet_state = EXCLUDED.ratchet_state, + x3dh_priv_key_1 = NULL, + x3dh_priv_key_2 = NULL, + x3dh_pub_key_1 = EXCLUDED.x3dh_pub_key_1, + x3dh_pub_key_2 = EXCLUDED.x3dh_pub_key_2, + pq_priv_kem = NULL, + pq_pub_kem = EXCLUDED.pq_pub_kem + +Plan: + +Query: + INSERT INTO ratchets (conn_id, ratchet_state) + VALUES (?, ?) + ON CONFLICT (conn_id) DO UPDATE SET + ratchet_state = ?, + x3dh_priv_key_1 = NULL, + x3dh_priv_key_2 = NULL, + x3dh_pub_key_1 = NULL, + x3dh_pub_key_2 = NULL, + pq_priv_kem = NULL, + pq_pub_kem = NULL + +Plan: + +Query: + INSERT INTO rcv_messages + ( conn_id, rcv_queue_id, internal_rcv_id, internal_id, external_snd_id, + broker_id, broker_ts, + internal_hash, external_prev_snd_hash, integrity) + VALUES + (?,?,?,?,?,?,?,?,?,?) + +Plan: +SEARCH messages USING COVERING INDEX idx_messages_conn_id_internal_rcv_id (conn_id=? AND internal_rcv_id=?) + +Query: + INSERT INTO rcv_queues + ( host, port, rcv_id, conn_id, rcv_private_key, rcv_dh_secret, e2e_priv_key, e2e_dh_secret, + snd_id, queue_mode, status, rcv_queue_id, rcv_primary, replace_rcv_queue_id, smp_client_version, server_key_hash, + link_id, link_key, link_priv_sig_key, link_enc_fixed_data + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?); + +Plan: + +Query: + INSERT INTO snd_file_chunk_replicas + (snd_file_chunk_id, replica_number, xftp_server_id, replica_id, replica_key, replica_status) + VALUES (?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO snd_messages + ( conn_id, internal_snd_id, internal_id, internal_hash, previous_msg_hash, msg_encrypt_key, padded_msg_len, snd_message_body_id) + VALUES + (?,?,?,?,?,?,?,?) + +Plan: +SEARCH messages USING COVERING INDEX idx_messages_conn_id_internal_snd_id (conn_id=? AND internal_snd_id=?) + +Query: + INSERT INTO snd_queues + (host, port, snd_id, queue_mode, conn_id, snd_public_key, snd_private_key, e2e_pub_key, e2e_dh_secret, + status, snd_queue_id, snd_primary, replace_snd_queue_id, smp_client_version, server_key_hash) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ON CONFLICT (host, port, snd_id) DO UPDATE SET + host=EXCLUDED.host, + port=EXCLUDED.port, + snd_id=EXCLUDED.snd_id, + queue_mode=EXCLUDED.queue_mode, + conn_id=EXCLUDED.conn_id, + snd_public_key=EXCLUDED.snd_public_key, + snd_private_key=EXCLUDED.snd_private_key, + e2e_pub_key=EXCLUDED.e2e_pub_key, + e2e_dh_secret=EXCLUDED.e2e_dh_secret, + status=EXCLUDED.status, + snd_queue_id=EXCLUDED.snd_queue_id, + snd_primary=EXCLUDED.snd_primary, + replace_snd_queue_id=EXCLUDED.replace_snd_queue_id, + smp_client_version=EXCLUDED.smp_client_version, + server_key_hash=EXCLUDED.server_key_hash + +Plan: + +Query: + SELECT rcv_replica_id, rcv_replica_key + FROM snd_file_chunk_replica_recipients + WHERE snd_file_chunk_replica_id = ? + +Plan: +SEARCH snd_file_chunk_replica_recipients USING INDEX idx_snd_file_chunk_replica_recipients_snd_file_chunk_replica_id (snd_file_chunk_replica_id=?) + +Query: + UPDATE conn_confirmations + SET accepted = 1, + own_conn_info = ? + WHERE confirmation_id = ? + +Plan: +SEARCH conn_confirmations USING PRIMARY KEY (confirmation_id=?) + +Query: + UPDATE conn_invitations + SET accepted = 1, + own_conn_info = ? + WHERE invitation_id = ? + +Plan: +SEARCH conn_invitations USING PRIMARY KEY (invitation_id=?) + +Query: + UPDATE connections + SET last_external_snd_msg_id = ?, + last_rcv_msg_hash = ? + WHERE conn_id = ? + AND last_internal_rcv_msg_id = ? + +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) + +Query: + UPDATE connections + SET last_internal_msg_id = ?, + last_internal_rcv_msg_id = ? + WHERE conn_id = ? + +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) + +Query: + UPDATE connections + SET last_internal_msg_id = ?, + last_internal_snd_msg_id = ? + WHERE conn_id = ? + +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) + +Query: + UPDATE connections + SET last_snd_msg_hash = ? + WHERE conn_id = ? + AND last_internal_snd_msg_id = ?; + +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) + +Query: + UPDATE inv_short_links + SET snd_id = ? + WHERE host = ? AND port = ? AND link_id = ? + +Plan: +SEARCH inv_short_links USING INDEX idx_inv_short_links_link_id (host=? AND port=? AND link_id=?) + +Query: + UPDATE ratchets + SET x3dh_priv_key_1 = ?, x3dh_priv_key_2 = ?, pq_priv_kem = ? + WHERE conn_id = ? + +Plan: +SEARCH ratchets USING PRIMARY KEY (conn_id=?) + +Query: + UPDATE rcv_queues + SET deleted = 1 + WHERE host = ? AND port = ? AND rcv_id = ? + +Plan: +SEARCH rcv_queues USING PRIMARY KEY (host=? AND port=? AND rcv_id=?) + +Query: + UPDATE rcv_queues + SET e2e_dh_secret = ?, + status = ?, + smp_client_version = ? + WHERE host = ? AND port = ? AND rcv_id = ? + +Plan: +SEARCH rcv_queues USING PRIMARY KEY (host=? AND port=? AND rcv_id=?) + +Query: + UPDATE rcv_queues + SET status = ? + WHERE host = ? AND port = ? AND rcv_id = ? + +Plan: +SEARCH rcv_queues USING PRIMARY KEY (host=? AND port=? AND rcv_id=?) + +Query: + UPDATE rcv_queues + SET switch_status = ? + WHERE host = ? AND port = ? AND rcv_id = ? + +Plan: +SEARCH rcv_queues USING PRIMARY KEY (host=? AND port=? AND rcv_id=?) + +Query: + UPDATE snd_queues + SET status = ? + WHERE host = ? AND port = ? AND snd_id = ? + +Plan: +SEARCH snd_queues USING PRIMARY KEY (host=? AND port=? AND snd_id=?) + +Query: + UPDATE snd_queues + SET switch_status = ? + WHERE host = ? AND port = ? AND snd_id = ? + +Plan: +SEARCH snd_queues USING PRIMARY KEY (host=? AND port=? AND snd_id=?) + +Query: + SELECT + c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.snd_id, q.queue_mode, + q.snd_public_key, q.snd_private_key, q.e2e_pub_key, q.e2e_dh_secret, q.status, + q.snd_queue_id, q.snd_primary, q.replace_snd_queue_id, q.switch_status, q.smp_client_version + FROM snd_queues q + JOIN servers s ON q.host = s.host AND q.port = s.port + JOIN connections c ON q.conn_id = c.conn_id + WHERE q.conn_id = ? +Plan: +SEARCH c USING PRIMARY KEY (conn_id=?) +SEARCH q USING INDEX idx_snd_queue_id (conn_id=?) +SEARCH s USING PRIMARY KEY (host=? AND port=?) + +Query: + SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, + q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, + q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, + q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, + q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data + FROM rcv_queues q + JOIN servers s ON q.host = s.host AND q.port = s.port + JOIN connections c ON q.conn_id = c.conn_id + WHERE q.conn_id = ? AND q.deleted = 0 +Plan: +SEARCH c USING PRIMARY KEY (conn_id=?) +SEARCH q USING INDEX idx_rcv_queue_id (conn_id=?) +SEARCH s USING PRIMARY KEY (host=? AND port=?) + +Query: + SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, + q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, + q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, + q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, + q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data + FROM rcv_queues q + JOIN servers s ON q.host = s.host AND q.port = s.port + JOIN connections c ON q.conn_id = c.conn_id + WHERE q.conn_id = ? AND q.host = ? AND q.port = ? AND q.rcv_id = ? AND q.deleted = 0 +Plan: +SEARCH q USING PRIMARY KEY (host=? AND port=? AND rcv_id=?) +SEARCH s USING PRIMARY KEY (host=? AND port=?) +SEARCH c USING PRIMARY KEY (conn_id=?) + +Query: + SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, + q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, + q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, + q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, + q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data + FROM rcv_queues q + JOIN servers s ON q.host = s.host AND q.port = s.port + JOIN connections c ON q.conn_id = c.conn_id + WHERE q.conn_id = ? AND q.host = ? AND q.port = ? AND q.rcv_id = ? AND q.deleted = 1 +Plan: +SEARCH q USING PRIMARY KEY (host=? AND port=? AND rcv_id=?) +SEARCH s USING PRIMARY KEY (host=? AND port=?) +SEARCH c USING PRIMARY KEY (conn_id=?) + +Query: + SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, + q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, + q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, + q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, + q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data + FROM rcv_queues q + JOIN servers s ON q.host = s.host AND q.port = s.port + JOIN connections c ON q.conn_id = c.conn_id + WHERE q.conn_id = ? AND q.rcv_queue_id = ? AND q.deleted = 0 +Plan: +SEARCH c USING PRIMARY KEY (conn_id=?) +SEARCH q USING INDEX idx_rcv_queue_id (conn_id=? AND rcv_queue_id=?) +SEARCH s USING PRIMARY KEY (host=? AND port=?) + +Query: + SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, + q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, + q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, + q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, + q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data + FROM rcv_queues q + JOIN servers s ON q.host = s.host AND q.port = s.port + JOIN connections c ON q.conn_id = c.conn_id + WHERE q.host = ? AND q.port = ? AND q.rcv_id = ? AND q.deleted = 0 +Plan: +SEARCH q USING PRIMARY KEY (host=? AND port=? AND rcv_id=?) +SEARCH s USING PRIMARY KEY (host=? AND port=?) +SEARCH c USING PRIMARY KEY (conn_id=?) + +Query: DELETE FROM commands WHERE command_id = ? +Plan: +SEARCH commands USING INTEGER PRIMARY KEY (rowid=?) + +Query: DELETE FROM conn_invitations WHERE contact_conn_id = ? AND invitation_id = ? +Plan: +SEARCH conn_invitations USING PRIMARY KEY (invitation_id=?) + +Query: DELETE FROM connections WHERE conn_id = ? +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) +SEARCH processed_ratchet_key_hashes USING COVERING INDEX idx_processed_ratchet_key_hashes_hash (conn_id=?) +SEARCH encrypted_rcv_message_hashes USING COVERING INDEX idx_encrypted_rcv_message_hashes_hash (conn_id=?) +SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_conn_id_internal_id (conn_id=?) +SEARCH commands USING COVERING INDEX idx_commands_conn_id (conn_id=?) +SEARCH ratchets USING PRIMARY KEY (conn_id=?) +SEARCH conn_invitations USING COVERING INDEX idx_conn_invitations_contact_conn_id (contact_conn_id=?) +SEARCH conn_confirmations USING COVERING INDEX idx_conn_confirmations_conn_id (conn_id=?) +SEARCH messages USING COVERING INDEX idx_messages_conn_id (conn_id=?) +SEARCH snd_queues USING COVERING INDEX idx_snd_queue_id (conn_id=?) +SEARCH rcv_queues USING COVERING INDEX idx_rcv_queue_id (conn_id=?) + +Query: DELETE FROM deleted_snd_chunk_replicas WHERE deleted_snd_chunk_replica_id = ? +Plan: +SEARCH deleted_snd_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?) + +Query: DELETE FROM inv_short_links WHERE host = ? AND port = ? AND link_id = ? +Plan: +SEARCH inv_short_links USING INDEX idx_inv_short_links_link_id (host=? AND port=? AND link_id=?) + +Query: DELETE FROM messages WHERE conn_id = ? AND internal_id = ?; +Plan: +SEARCH messages USING PRIMARY KEY (conn_id=? AND internal_id=?) +SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_conn_id_internal_id (conn_id=? AND internal_id=?) +SEARCH snd_messages USING COVERING INDEX idx_snd_messages_conn_id_internal_id (conn_id=? AND internal_id=?) +SEARCH rcv_messages USING COVERING INDEX idx_rcv_messages_conn_id_internal_id (conn_id=? AND internal_id=?) + +Query: DELETE FROM ntf_subscriptions WHERE conn_id = ? +Plan: +SEARCH ntf_subscriptions USING PRIMARY KEY (conn_id=?) + +Query: DELETE FROM ratchets WHERE conn_id = ? +Plan: +SEARCH ratchets USING PRIMARY KEY (conn_id=?) +SEARCH skipped_messages USING COVERING INDEX idx_skipped_messages_conn_id (conn_id=?) + +Query: DELETE FROM rcv_files WHERE rcv_file_id = ? +Plan: +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) +SEARCH rcv_file_chunks USING COVERING INDEX idx_rcv_file_chunks_rcv_file_id (rcv_file_id=?) +SEARCH rcv_files USING COVERING INDEX idx_rcv_files_redirect_id (redirect_id=?) + +Query: DELETE FROM rcv_queues WHERE conn_id = ? AND rcv_queue_id = ? +Plan: +SEARCH rcv_queues USING COVERING INDEX idx_rcv_queue_id (conn_id=? AND rcv_queue_id=?) + +Query: DELETE FROM snd_files WHERE snd_file_id = ? +Plan: +SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?) +SEARCH snd_file_chunks USING COVERING INDEX idx_snd_file_chunks_snd_file_id (snd_file_id=?) + +Query: DELETE FROM snd_message_deliveries WHERE conn_id = ? AND snd_queue_id = ? +Plan: +SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries (conn_id=? AND snd_queue_id=?) + +Query: DELETE FROM snd_message_deliveries WHERE conn_id = ? AND snd_queue_id = ? AND internal_id = ? +Plan: +SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_expired (conn_id=? AND snd_queue_id=?) + +Query: DELETE FROM snd_queues WHERE conn_id = ? AND snd_queue_id = ? +Plan: +SEARCH snd_queues USING COVERING INDEX idx_snd_queue_id (conn_id=? AND snd_queue_id=?) + +Query: DELETE FROM users WHERE user_id = ? +Plan: +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) +SEARCH deleted_snd_chunk_replicas USING COVERING INDEX idx_deleted_snd_chunk_replicas_user_id (user_id=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_user_id (user_id=?) +SEARCH rcv_files USING COVERING INDEX idx_rcv_files_user_id (user_id=?) +SEARCH connections USING COVERING INDEX idx_connections_user (user_id=?) + +Query: INSERT INTO commands (host, port, corr_id, conn_id, command_tag, command, server_key_hash, created_at) VALUES (?,?,?,?,?,?,?,?) +Plan: + +Query: INSERT INTO deleted_snd_chunk_replicas (user_id, xftp_server_id, replica_id, replica_key, chunk_digest) VALUES (?,?,?,?,?) +Plan: + +Query: INSERT INTO encrypted_rcv_message_hashes (conn_id, hash) VALUES (?,?) +Plan: + +Query: INSERT INTO processed_ratchet_key_hashes (conn_id, hash) VALUES (?,?) +Plan: + +Query: INSERT INTO ratchets (conn_id, x3dh_priv_key_1, x3dh_priv_key_2, pq_priv_kem) VALUES (?, ?, ?, ?) +Plan: + +Query: INSERT INTO rcv_file_chunk_replicas (replica_number, rcv_file_chunk_id, xftp_server_id, replica_id, replica_key) VALUES (?,?,?,?,?) +Plan: + +Query: INSERT INTO rcv_file_chunks (rcv_file_id, chunk_no, chunk_size, digest) VALUES (?,?,?,?) +Plan: + +Query: INSERT INTO rcv_files (rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status, redirect_id, redirect_entity_id, redirect_digest, redirect_size, approved_relays) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) +Plan: + +Query: INSERT INTO servers (host, port, key_hash) VALUES (?,?,?) +Plan: + +Query: INSERT INTO skipped_messages (conn_id, header_key, msg_n, msg_key) VALUES (?, ?, ?, ?) +Plan: + +Query: INSERT INTO snd_file_chunks (snd_file_id, chunk_no, chunk_offset, chunk_size, digest) VALUES (?,?,?,?,?) +Plan: + +Query: INSERT INTO snd_files (snd_file_entity_id, user_id, path, src_file_key, src_file_nonce, num_recipients, prefix_path, key, nonce, status, redirect_size, redirect_digest) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) +Plan: + +Query: INSERT INTO snd_message_bodies (agent_msg) VALUES (?) RETURNING snd_message_body_id +Plan: +SEARCH snd_messages USING COVERING INDEX idx_snd_messages_snd_message_body_id (snd_message_body_id=?) + +Query: INSERT INTO snd_message_deliveries (conn_id, snd_queue_id, internal_id) VALUES (?, ?, ?) +Plan: + +Query: INSERT INTO users (user_id) VALUES (1) +Plan: + +Query: INSERT INTO users DEFAULT VALUES +Plan: + +Query: INSERT INTO xftp_servers (xftp_host, xftp_port, xftp_key_hash) VALUES (?,?,?) +Plan: + +Query: SELECT 1 FROM encrypted_rcv_message_hashes WHERE conn_id = ? AND hash = ? LIMIT 1 +Plan: +SEARCH encrypted_rcv_message_hashes USING COVERING INDEX idx_encrypted_rcv_message_hashes_hash (conn_id=? AND hash=?) + +Query: SELECT 1 FROM processed_ratchet_key_hashes WHERE conn_id = ? AND hash = ? LIMIT 1 +Plan: +SEARCH processed_ratchet_key_hashes USING COVERING INDEX idx_processed_ratchet_key_hashes_hash (conn_id=? AND hash=?) + +Query: SELECT 1 FROM snd_message_deliveries WHERE conn_id = ? AND failed = 0 LIMIT 1 +Plan: +SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_expired (conn_id=?) + +Query: SELECT DISTINCT conn_id FROM snd_message_deliveries WHERE failed = 0 +Plan: +SCAN snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_expired + +Query: SELECT conn_id FROM connections WHERE user_id = ? +Plan: +SEARCH connections USING COVERING INDEX idx_connections_user (user_id=?) + +Query: SELECT count(1) FROM snd_message_bodies +Plan: +SCAN snd_message_bodies + +Query: SELECT deleted FROM snd_files WHERE snd_file_id = ? +Plan: +SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?) + +Query: SELECT header_key, msg_n, msg_key FROM skipped_messages WHERE conn_id = ? +Plan: +SEARCH skipped_messages USING INDEX idx_skipped_messages_conn_id (conn_id=?) + +Query: SELECT key_hash FROM servers WHERE host = ? AND port = ? +Plan: +SEARCH servers USING PRIMARY KEY (host=? AND port=?) + +Query: SELECT last_insert_rowid() +Plan: +SCAN CONSTANT ROW + +Query: SELECT ratchet_state FROM ratchets WHERE conn_id = ? +Plan: +SEARCH ratchets USING PRIMARY KEY (conn_id=?) + +Query: SELECT ratchet_state, x3dh_pub_key_1, x3dh_pub_key_2, pq_pub_kem FROM ratchets WHERE conn_id = ? +Plan: +SEARCH ratchets USING PRIMARY KEY (conn_id=?) + +Query: SELECT rcv_file_id FROM rcv_files WHERE rcv_file_entity_id = ? +Plan: +SEARCH rcv_files USING COVERING INDEX sqlite_autoindex_rcv_files_1 (rcv_file_entity_id=?) + +Query: SELECT rcv_file_id FROM rcv_files WHERE redirect_id = ? +Plan: +SEARCH rcv_files USING COVERING INDEX idx_rcv_files_redirect_id (redirect_id=?) + +Query: SELECT rcv_queue_id FROM rcv_queues WHERE conn_id = ? AND host = ? AND port = ? AND snd_id = ? +Plan: +SEARCH rcv_queues USING INDEX sqlite_autoindex_rcv_queues_2 (host=? AND port=? AND snd_id=?) + +Query: SELECT rcv_queue_id FROM rcv_queues WHERE conn_id = ? ORDER BY rcv_queue_id DESC LIMIT 1 +Plan: +SEARCH rcv_queues USING COVERING INDEX idx_rcv_queue_id (conn_id=?) + +Query: SELECT rcv_queue_id, broker_id FROM rcv_messages WHERE conn_id = ? AND internal_id = ? +Plan: +SEARCH rcv_messages USING PRIMARY KEY (conn_id=?) + +Query: SELECT snd_file_id FROM snd_files WHERE snd_file_entity_id = ? +Plan: +SEARCH snd_files USING COVERING INDEX idx_snd_files_snd_file_entity_id (snd_file_entity_id=?) + +Query: SELECT snd_queue_id FROM snd_queues WHERE conn_id = ? AND host = ? AND port = ? AND snd_id = ? +Plan: +SEARCH snd_queues USING PRIMARY KEY (host=? AND port=? AND snd_id=?) + +Query: SELECT snd_queue_id FROM snd_queues WHERE conn_id = ? ORDER BY snd_queue_id DESC LIMIT 1 +Plan: +SEARCH snd_queues USING COVERING INDEX idx_snd_queue_id (conn_id=?) + +Query: SELECT started_at, servers_stats FROM servers_stats WHERE servers_stats_id = 1 +Plan: +SEARCH servers_stats USING INTEGER PRIMARY KEY (rowid=?) + +Query: SELECT user_id FROM users WHERE user_id = ? AND deleted = ? +Plan: +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + +Query: SELECT x3dh_priv_key_1, x3dh_priv_key_2, pq_priv_kem FROM ratchets WHERE conn_id = ? +Plan: +SEARCH ratchets USING PRIMARY KEY (conn_id=?) + +Query: SELECT xftp_server_id FROM xftp_servers WHERE xftp_host = ? AND xftp_port = ? AND xftp_key_hash = ? +Plan: +SEARCH xftp_servers USING COVERING INDEX sqlite_autoindex_xftp_servers_1 (xftp_host=? AND xftp_port=? AND xftp_key_hash=?) + +Query: UPDATE connections SET deleted = ? WHERE conn_id = ? +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) + +Query: UPDATE connections SET deleted_at_wait_delivery = ? WHERE conn_id = ? +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) + +Query: UPDATE connections SET enable_ntfs = ? WHERE conn_id = ? +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) + +Query: UPDATE connections SET pq_support = ? WHERE conn_id = ? +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) + +Query: UPDATE connections SET ratchet_sync_state = ? WHERE conn_id = ? +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) + +Query: UPDATE connections SET smp_agent_version = ? WHERE conn_id = ? +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) + +Query: UPDATE messages SET msg_body = x'' WHERE conn_id = ? AND internal_id = ? +Plan: +SEARCH messages USING PRIMARY KEY (conn_id=? AND internal_id=?) + +Query: UPDATE ratchets SET ratchet_state = ? WHERE conn_id = ? +Plan: +SEARCH ratchets USING PRIMARY KEY (conn_id=?) + +Query: UPDATE rcv_file_chunk_replicas SET delay = ?, retries = retries + 1, updated_at = ? WHERE rcv_file_chunk_replica_id = ? +Plan: +SEARCH rcv_file_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE rcv_file_chunk_replicas SET received = 1, updated_at = ? WHERE rcv_file_chunk_replica_id = ? +Plan: +SEARCH rcv_file_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE rcv_file_chunks SET tmp_path = ?, updated_at = ? WHERE rcv_file_chunk_id = ? +Plan: +SEARCH rcv_file_chunks USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE rcv_files SET deleted = 1, updated_at = ? WHERE rcv_file_id = ? +Plan: +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE rcv_files SET key = ?, nonce = ?, chunk_size = ?, updated_at = ? WHERE rcv_file_id = ? +Plan: +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE rcv_files SET status = ?, updated_at = ? WHERE rcv_file_id = ? +Plan: +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE rcv_files SET tmp_path = NULL, error = ?, status = ?, updated_at = ? WHERE rcv_file_id = ? +Plan: +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE rcv_files SET tmp_path = NULL, status = ?, updated_at = ? WHERE rcv_file_id = ? +Plan: +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE rcv_messages SET user_ack = ? WHERE conn_id = ? AND internal_id = ? +Plan: +SEARCH rcv_messages USING COVERING INDEX idx_rcv_messages_conn_id_internal_id (conn_id=? AND internal_id=?) + +Query: UPDATE rcv_queues SET last_broker_ts = ? WHERE conn_id = ? AND rcv_queue_id = ? AND (last_broker_ts IS NULL OR last_broker_ts < ?) +Plan: +SEARCH rcv_queues USING INDEX idx_rcv_queue_id (conn_id=? AND rcv_queue_id=?) + +Query: UPDATE rcv_queues SET rcv_primary = ? WHERE conn_id = ? +Plan: +SEARCH rcv_queues USING COVERING INDEX idx_rcv_queue_id (conn_id=?) + +Query: UPDATE rcv_queues SET rcv_primary = ?, replace_rcv_queue_id = ? WHERE conn_id = ? AND rcv_queue_id = ? +Plan: +SEARCH rcv_queues USING COVERING INDEX idx_rcv_queue_id (conn_id=? AND rcv_queue_id=?) + +Query: UPDATE servers_stats SET servers_stats = ?, updated_at = ? WHERE servers_stats_id = 1 +Plan: +SEARCH servers_stats USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE snd_file_chunk_replicas SET replica_status = ?, updated_at = ? WHERE snd_file_chunk_replica_id = ? +Plan: +SEARCH snd_file_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE snd_files SET deleted = 1, updated_at = ? WHERE snd_file_id = ? +Plan: +SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE snd_files SET prefix_path = NULL, status = ?, updated_at = ? WHERE snd_file_id = ? +Plan: +SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE snd_files SET status = ?, digest = ?, updated_at = ? WHERE snd_file_id = ? +Plan: +SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE snd_files SET status = ?, updated_at = ? WHERE snd_file_id = ? +Plan: +SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE snd_messages SET rcpt_internal_id = ?, rcpt_status = ? WHERE conn_id = ? AND internal_snd_id = ? +Plan: +SEARCH snd_messages USING PRIMARY KEY (conn_id=? AND internal_snd_id=?) + +Query: UPDATE snd_messages SET retry_int_slow = ?, retry_int_fast = ? WHERE conn_id = ? AND internal_id = ? +Plan: +SEARCH snd_messages USING COVERING INDEX idx_snd_messages_conn_id_internal_id (conn_id=? AND internal_id=?) + +Query: UPDATE snd_messages SET snd_message_body_id = NULL WHERE conn_id = ? AND internal_id = ? +Plan: +SEARCH snd_messages USING COVERING INDEX idx_snd_messages_conn_id_internal_id (conn_id=? AND internal_id=?) + +Query: UPDATE snd_queues SET snd_primary = ? WHERE conn_id = ? +Plan: +SEARCH snd_queues USING COVERING INDEX idx_snd_queue_id (conn_id=?) + +Query: UPDATE snd_queues SET snd_primary = ?, replace_snd_queue_id = ? WHERE conn_id = ? AND snd_queue_id = ? +Plan: +SEARCH snd_queues USING COVERING INDEX idx_snd_queue_id (conn_id=? AND snd_queue_id=?) + +Query: UPDATE users SET deleted = ? WHERE user_id = ? +Plan: +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_lint.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_lint.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt new file mode 100644 index 0000000000..e9ade30f93 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -0,0 +1,5980 @@ +Query: + INSERT INTO group_members + ( group_id, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, + user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at, + peer_chat_min_version, peer_chat_max_version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO groups + (group_profile_id, local_display_name, inv_queue_info, user_id, enable_ntfs, + created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat, business_member_id, customer_member_id) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO groups + (group_profile_id, local_display_name, user_id, enable_ntfs, + created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat, business_member_id, customer_member_id) + VALUES (?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO groups + (group_profile_id, local_display_name, user_id, enable_ntfs, + created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat, business_xcontact_id) + VALUES (?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + SELECT + -- GroupInfo + g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + -- GroupInfo {membership} + mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, + mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, + -- GroupInfo {membership = GroupMember {memberProfile}} + pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at, + -- from GroupMember + m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + JOIN groups g ON g.group_id = m.group_id + JOIN group_profiles gp USING (group_profile_id) + JOIN group_members mu ON g.group_id = mu.group_id + JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) + WHERE m.group_member_id = ? AND g.user_id = ? AND mu.contact_id = ? + +Plan: +SEARCH m USING INTEGER PRIMARY KEY (rowid=?) +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) +SEARCH mu USING INDEX idx_group_members_contact_id (contact_id=?) +SEARCH pu USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE contact_requests + SET agent_invitation_id = ?, pq_support = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, local_display_name = ?, updated_at = ? + WHERE user_id = ? AND contact_request_id = ? + +Plan: +SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE contact_requests + SET agent_invitation_id = ?, pq_support = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, updated_at = ? + WHERE user_id = ? AND contact_request_id = ? + +Plan: +SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) + +Query: + INSERT INTO contact_requests + (user_contact_link_id, agent_invitation_id, peer_chat_min_version, peer_chat_max_version, contact_profile_id, local_display_name, user_id, + created_at, updated_at, xcontact_id, pq_support) + VALUES (?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO display_names + (local_display_name, ldn_base, ldn_suffix, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?) + +Plan: +SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_1 (user_id=? AND local_display_name=?) +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + +Query: + INSERT INTO group_member_intros + (re_group_member_id, to_group_member_id, intro_status, intro_chat_protocol_version, created_at, updated_at) + VALUES (?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO group_members + ( group_id, member_id, member_role, member_category, member_status, invited_by, + user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO group_members + ( group_id, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, + user_id, local_display_name, contact_id, contact_profile_id, sent_inv_queue_info, created_at, updated_at, + peer_chat_min_version, peer_chat_max_version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + SELECT + -- Contact + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, + -- Connection + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM contacts ct + JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id + LEFT JOIN connections c ON c.contact_id = ct.contact_id + WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0 + ORDER BY c.created_at DESC + LIMIT 1 + +Plan: +SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?) +SEARCH cp USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_contact_id (contact_id=?) LEFT-JOIN +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT COUNT(1) + FROM chat_items i + JOIN contacts ct USING (contact_id) + WHERE i.user_id = ? AND i.item_status = ? AND (ct.enable_ntfs = 1 OR ct.enable_ntfs IS NULL) AND ct.deleted = 0 + +Plan: +SEARCH i USING COVERING INDEX idx_chat_items_contacts (user_id=?) +SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT COUNT(1) + FROM chat_items i + JOIN groups g USING (group_id) + WHERE i.user_id = ? AND i.item_status = ? + AND (g.enable_ntfs = 1 OR g.enable_ntfs IS NULL OR (g.enable_ntfs = 2 AND i.user_mention = 1)) + +Plan: +SEARCH i USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=?) +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT conditions_commit, accepted_at, auto_accepted + FROM operator_usage_conditions + WHERE server_operator_id = ? + ORDER BY operator_usage_conditions_id DESC + LIMIT 1 + +Plan: +SEARCH operator_usage_conditions USING INDEX idx_operator_usage_conditions_server_operator_id (server_operator_id=?) + +Query: + SELECT g.local_display_name, mu.member_status + FROM groups g + JOIN group_members mu ON mu.group_id = g.group_id + WHERE g.group_id = ? AND g.user_id = ? AND mu.contact_id = ? + AND mu.member_status NOT IN (?,?,?) + +Plan: +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) +SEARCH mu USING INDEX idx_group_members_contact_id (contact_id=?) + +Query: + SELECT m.group_member_id, m.local_display_name, c.agent_conn_id + FROM group_members m + JOIN connections c ON c.connection_id = ( + SELECT max(cc.connection_id) + FROM connections cc + WHERE cc.user_id = ? AND cc.group_member_id = m.group_member_id + ) + WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) + AND m.member_status NOT IN (?,?,?) + +Plan: +SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) +SEARCH c USING INTEGER PRIMARY KEY (rowid=?) +CORRELATED SCALAR SUBQUERY 1 +SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?) + +Query: + SELECT timed_ttl + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_status = ? AND chat_item_id = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL + +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT timed_ttl + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_status = ? AND chat_item_id = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL + +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND item_status = ? AND chat_item_id = ? + +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE contact_profiles + SET display_name = ?, + full_name = ?, + image = ?, + contact_link = ?, + updated_at = ? + WHERE contact_profile_id IN ( + SELECT contact_profile_id + FROM contact_requests + WHERE user_id = ? + AND contact_request_id = ? + ) + +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE xftp_file_descriptions + SET file_descr_text = ?, file_descr_part_no = ?, file_descr_complete = ? + WHERE file_descr_id = ? + +Plan: +SEARCH xftp_file_descriptions USING INTEGER PRIMARY KEY (rowid=?) + +Query: + INSERT INTO contacts ( + user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, contact_used, + created_at, updated_at, chat_ts + ) VALUES (?,?,?,?,?,?,?,?,?) + +Plan: +SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?) + +Query: + INSERT INTO group_members + ( group_id, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, + user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at, + peer_chat_min_version, peer_chat_max_version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO group_members + ( group_id, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, + user_id, local_display_name, contact_id, contact_profile_id, member_profile_id, created_at, updated_at, + peer_chat_min_version, peer_chat_max_version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO messages ( + msg_sent, chat_msg_event, msg_body, connection_id, group_id, + shared_msg_id, shared_msg_id_user, created_at, updated_at + ) VALUES (?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + SELECT + c.contact_profile_id, c.local_display_name, c.via_group, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, + p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl + FROM contacts c + JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id + WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0 + +Plan: +SEARCH c USING INTEGER PRIMARY KEY (rowid=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, cr.created_at, cr.updated_at, + cr.peer_chat_min_version, cr.peer_chat_max_version + FROM contact_requests cr + JOIN connections c USING (user_contact_link_id) + JOIN contact_profiles p USING (contact_profile_id) + WHERE cr.user_id = ? + AND cr.xcontact_id = ? + LIMIT 1 + +Plan: +SEARCH cr USING INDEX idx_contact_requests_xcontact_id (xcontact_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) + +Query: + SELECT COUNT(1) + FROM ( + SELECT 1 + FROM chat_items + WHERE user_id = ? AND contact_id = ? + AND created_at > ? + UNION ALL + SELECT 1 + FROM chat_items + WHERE user_id = ? AND contact_id = ? + AND created_at = ? AND chat_item_id > ? + ) ci + +Plan: +CO-ROUTINE ci +COMPOUND QUERY +LEFT-MOST SUBQUERY +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=? AND created_at>?) +UNION ALL +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=? AND created_at=? AND rowid>?) +SCAN ci + +Query: + SELECT COUNT(1) + FROM ( + SELECT 1 + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_status = ? + AND created_at > ? + UNION ALL + SELECT 1 + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_status = ? + AND created_at = ? AND chat_item_id > ? + ) ci + +Plan: +CO-ROUTINE ci +COMPOUND QUERY +LEFT-MOST SUBQUERY +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts (user_id=? AND contact_id=? AND item_status=? AND created_at>?) +UNION ALL +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts (user_id=? AND contact_id=? AND item_status=? AND created_at=? AND rowid>?) +SCAN ci + +Query: + SELECT COUNT(1) + FROM ( + SELECT 1 + FROM chat_items + WHERE user_id = ? AND group_id = ? + AND item_ts > ? + UNION ALL + SELECT 1 + FROM chat_items + WHERE user_id = ? AND group_id = ? + AND item_ts = ? AND chat_item_id > ? + ) ci + +Plan: +CO-ROUTINE ci +COMPOUND QUERY +LEFT-MOST SUBQUERY +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=? AND item_ts>?) +UNION ALL +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=? AND item_ts=? AND rowid>?) +SCAN ci + +Query: + SELECT COUNT(1) + FROM ( + SELECT 1 + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_status = ? + AND item_ts > ? + UNION ALL + SELECT 1 + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_status = ? + AND item_ts = ? AND chat_item_id > ? + ) ci + +Plan: +CO-ROUTINE ci +COMPOUND QUERY +LEFT-MOST SUBQUERY +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups (user_id=? AND group_id=? AND item_status=? AND item_ts>?) +UNION ALL +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups (user_id=? AND group_id=? AND item_status=? AND item_ts=? AND rowid>?) +SCAN ci + +Query: + SELECT COUNT(1) + FROM ( + SELECT 1 + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? + AND created_at > ? + UNION ALL + SELECT 1 + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? + AND created_at = ? AND chat_item_id > ? + ) ci + +Plan: +CO-ROUTINE ci +COMPOUND QUERY +LEFT-MOST SUBQUERY +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=? AND created_at>?) +UNION ALL +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=? AND created_at=? AND rowid>?) +SCAN ci + +Query: + SELECT COUNT(1) + FROM ( + SELECT 1 + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND item_status = ? + AND created_at > ? + UNION ALL + SELECT 1 + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND item_status = ? + AND created_at = ? AND chat_item_id > ? + ) ci + +Plan: +CO-ROUTINE ci +COMPOUND QUERY +LEFT-MOST SUBQUERY +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes (user_id=? AND note_folder_id=? AND item_status=? AND created_at>?) +UNION ALL +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes (user_id=? AND note_folder_id=? AND item_status=? AND created_at=? AND rowid>?) +SCAN ci + +Query: + SELECT author_group_member_id, forwarded_by_group_member_id + FROM messages + WHERE group_id = ? AND shared_msg_id = ? LIMIT 1 + +Plan: +SEARCH messages USING INDEX idx_messages_group_id_shared_msg_id (group_id=? AND shared_msg_id=?) + +Query: + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM connections c + JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id + WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL + +Plan: +SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) +SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) + +Query: + SELECT chat_item_id, contact_id, group_id, note_folder_id + FROM chat_items + WHERE user_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' + AND (item_ts < ? OR (item_ts = ? AND chat_item_id < ?)) + ORDER BY item_ts DESC, chat_item_id DESC + LIMIT ? + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT chat_item_id, contact_id, group_id, note_folder_id + FROM chat_items + WHERE user_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' + ORDER BY item_ts DESC, chat_item_id DESC + LIMIT ? + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT conn_req_contact, group_id + FROM user_contact_links + WHERE user_id = ? AND short_link_contact = ? + +Plan: +SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) + +Query: + SELECT conn_req_contact, group_id + FROM user_contact_links + WHERE user_id = ? AND user_contact_link_id = ? + +Plan: +SEARCH user_contact_links USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT conn_req_inv, agent_conn_id + FROM connections + WHERE user_id = ? AND short_link_inv = ? LIMIT 1 + +Plan: +SEARCH connections USING INDEX idx_connections_updated_at (user_id=?) + +Query: + SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, + conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, + created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, + conn_chat_version, peer_chat_min_version, peer_chat_max_version + FROM connections + WHERE user_id = ? AND agent_conn_id = ? + +Plan: +SEARCH connections USING INDEX sqlite_autoindex_connections_1 (agent_conn_id=?) + +Query: + SELECT ct.contact_id + FROM group_members m + JOIN contacts ct ON ct.contact_id = m.contact_id + WHERE m.user_id = ? AND m.group_member_id = ? AND ct.deleted = 0 + LIMIT 1 + +Plan: +SEARCH m USING INTEGER PRIMARY KEY (rowid=?) +SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT gp.display_name, gp.full_name, gp.description, gp.image, gp.preferences, gp.member_admission + FROM group_profiles gp + JOIN groups g ON gp.group_profile_id = g.group_profile_id + WHERE g.group_id = ? + +Plan: +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) +SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT i.chat_item_id + FROM chat_items i + LEFT JOIN group_snd_item_statuses s ON s.chat_item_id = i.chat_item_id AND s.group_member_id = ? + WHERE s.group_snd_item_status_id IS NULL + AND i.user_id = ? AND i.group_id = ? + AND i.include_in_history = 1 + AND i.item_deleted = 0 + ORDER BY i.item_ts DESC, i.chat_item_id DESC + LIMIT ? + +Plan: +SEARCH i USING COVERING INDEX idx_chat_items_groups_history (user_id=? AND group_id=? AND include_in_history=? AND item_deleted=?) +SEARCH s USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id_group_member_id (chat_item_id=? AND group_member_id=?) LEFT-JOIN + +Query: + SELECT i.chat_item_id, + -- GroupMember + m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, + m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, + p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN contacts c ON m.contact_id = c.contact_id + LEFT JOIN chat_items i ON i.user_id = m.user_id + AND i.group_id = m.group_id + AND m.group_member_id = i.group_member_id + AND i.shared_msg_id = ? + WHERE m.user_id = ? AND m.group_id = ? AND m.member_id = ? + +Plan: +SEARCH m USING INDEX sqlite_autoindex_group_members_1 (group_id=? AND member_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH i USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=? AND shared_msg_id=?) LEFT-JOIN + +Query: + SELECT i.chat_item_id, i.contact_id, i.group_id, i.note_folder_id + FROM chat_items i + JOIN files f ON f.chat_item_id = i.chat_item_id + WHERE f.user_id = ? AND f.file_id = ? + LIMIT 1 + +Plan: +SEARCH f USING INTEGER PRIMARY KEY (rowid=?) +SEARCH i USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT ldn_suffix FROM display_names + WHERE user_id = ? AND ldn_base = ? + ORDER BY ldn_suffix DESC + LIMIT 1 + +Plan: +SEARCH display_names USING COVERING INDEX sqlite_autoindex_display_names_2 (user_id=? AND ldn_base=?) + +Query: + SELECT s.contact_id, g.group_id, s.group_member_id + FROM sent_probes s + LEFT JOIN contacts c ON s.contact_id = c.contact_id AND c.deleted = 0 + LEFT JOIN group_members m ON s.group_member_id = m.group_member_id + LEFT JOIN groups g ON g.group_id = m.group_id + JOIN sent_probe_hashes h ON h.sent_probe_id = s.sent_probe_id + WHERE s.user_id = ? AND s.probe = ? + AND (h.contact_id = ? OR h.group_member_id = ?) + +Plan: +SEARCH s USING INDEX sqlite_autoindex_sent_probes_1 (user_id=? AND probe=?) +SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +SEARCH h USING INDEX idx_sent_probe_hashes_sent_probe_id (sent_probe_id=?) + +Query: + DELETE FROM chat_item_reactions + WHERE contact_id = ? AND shared_msg_id = ? AND reaction_sent = ? AND reaction = ? + +Plan: +SEARCH chat_item_reactions USING INDEX idx_chat_item_reactions_contact (contact_id=? AND shared_msg_id=?) + +Query: + DELETE FROM chat_item_reactions + WHERE group_id = ? AND group_member_id = ? AND shared_msg_id = ? AND item_member_id = ? AND reaction_sent = ? AND reaction = ? + +Plan: +SEARCH chat_item_reactions USING INDEX idx_chat_item_reactions_group (group_id=? AND shared_msg_id=?) + +Query: + INSERT INTO chat_item_reactions + (contact_id, shared_msg_id, reaction_sent, reaction, created_by_msg_id, reaction_ts) + VALUES (?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO chat_item_reactions + (group_id, group_member_id, item_member_id, shared_msg_id, reaction_sent, reaction, created_by_msg_id, reaction_ts) + VALUES (?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO group_members + ( group_id, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, + user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at, + peer_chat_min_version, peer_chat_max_version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO groups + (local_display_name, user_id, group_profile_id, enable_ntfs, + created_at, updated_at, chat_ts, user_member_profile_sent_at) + VALUES (?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO messages + (msg_sent, chat_msg_event, msg_body, created_at, updated_at, connection_id, group_id, shared_msg_id, author_group_member_id, forwarded_by_group_member_id) + VALUES (?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO server_operators + (server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy) + VALUES (?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO usage_conditions + (usage_conditions_id, conditions_commit, notified_at, created_at) + VALUES (?,?,?,?) + +Plan: + +Query: + SELECT + -- ChatItem + i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id, + i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, + i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, + i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, + -- CIFile + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol + FROM chat_items i + LEFT JOIN files f ON f.chat_item_id = i.chat_item_id + WHERE i.user_id = ? AND i.note_folder_id = ? AND i.chat_item_id = ? + +Plan: +SEARCH i USING INTEGER PRIMARY KEY (rowid=?) +SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) LEFT-JOIN + +Query: + SELECT + -- ChatItem + i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id, + i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, + i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, + i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, + -- CIFile + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, + -- CIMeta forwardedByMember + i.forwarded_by_group_member_id, + -- GroupMember + m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, + m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, + p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, + -- quoted ChatItem + ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, + -- quoted GroupMember + rm.group_member_id, rm.group_id, rm.member_id, rm.peer_chat_min_version, rm.peer_chat_max_version, rm.member_role, rm.member_category, + rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, + rp.display_name, rp.full_name, rp.image, rp.contact_link, rp.local_alias, rp.preferences, + rm.created_at, rm.updated_at, + -- deleted by GroupMember + dbm.group_member_id, dbm.group_id, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category, + dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, + dbp.display_name, dbp.full_name, dbp.image, dbp.contact_link, dbp.local_alias, dbp.preferences, + dbm.created_at, dbm.updated_at + FROM chat_items i + LEFT JOIN files f ON f.chat_item_id = i.chat_item_id + LEFT JOIN group_members m ON m.group_member_id = i.group_member_id + LEFT JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN chat_items ri ON ri.shared_msg_id = i.quoted_shared_msg_id AND ri.group_id = i.group_id + LEFT JOIN group_members rm ON rm.group_member_id = ri.group_member_id + LEFT JOIN contact_profiles rp ON rp.contact_profile_id = COALESCE(rm.member_profile_id, rm.contact_profile_id) + LEFT JOIN group_members dbm ON dbm.group_member_id = i.item_deleted_by_group_member_id + LEFT JOIN contact_profiles dbp ON dbp.contact_profile_id = COALESCE(dbm.member_profile_id, dbm.contact_profile_id) + WHERE i.user_id = ? AND i.group_id = ? AND i.chat_item_id = ? + +Plan: +SEARCH i USING INTEGER PRIMARY KEY (rowid=?) +SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) LEFT-JOIN +SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +SEARCH ri USING INDEX idx_chat_items_group_id_shared_msg_id (group_id=? AND shared_msg_id=?) LEFT-JOIN +SEARCH rm USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +SEARCH rp USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +SEARCH dbm USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +SEARCH dbp USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN + +Query: + SELECT + -- ChatItem + i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id, + i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, + i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, + i.timed_ttl, i.timed_delete_at, i.item_live, i.user_mention, + -- CIFile + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, + -- DirectQuote + ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent + FROM chat_items i + LEFT JOIN files f ON f.chat_item_id = i.chat_item_id + LEFT JOIN chat_items ri ON ri.user_id = i.user_id AND ri.contact_id = i.contact_id AND ri.shared_msg_id = i.quoted_shared_msg_id + WHERE i.user_id = ? AND i.contact_id = ? AND i.chat_item_id = ? + +Plan: +SEARCH i USING INTEGER PRIMARY KEY (rowid=?) +SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) LEFT-JOIN +SEARCH ri USING COVERING INDEX idx_chat_items_direct_shared_msg_id (user_id=? AND contact_id=? AND shared_msg_id=?) LEFT-JOIN + +Query: + SELECT + -- Contact + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, + -- Connection + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM contacts ct + JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id + JOIN connections c ON c.contact_id = ct.contact_id + WHERE c.user_id = ? AND c.via_contact_uri_hash = ? AND ct.contact_status = ? AND ct.deleted = 0 + ORDER BY c.created_at DESC + LIMIT 1 + +Plan: +SEARCH c USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_contact_uri_hash=?) +SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH cp USING INTEGER PRIMARY KEY (rowid=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT + -- GroupInfo + g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + -- GroupInfo {membership} + mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, + mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, + -- GroupInfo {membership = GroupMember {memberProfile}} + pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at, + -- via GroupMember + m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM group_members m + JOIN contacts ct ON ct.contact_id = m.contact_id + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + JOIN groups g ON g.group_id = m.group_id AND g.group_id = ct.via_group + JOIN group_profiles gp USING (group_profile_id) + JOIN group_members mu ON g.group_id = mu.group_id + JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) + LEFT JOIN connections c ON c.connection_id = ( + SELECT max(cc.connection_id) + FROM connections cc + where cc.user_id = ? AND cc.group_member_id = m.group_member_id + ) + WHERE ct.user_id = ? AND ct.contact_id = ? AND mu.contact_id = ? AND ct.deleted = 0 + +Plan: +SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) +SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) +SEARCH m USING INDEX sqlite_autoindex_group_members_1 (group_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH mu USING INDEX idx_group_members_contact_id (contact_id=?) +SEARCH pu USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?) + +Query: + SELECT + g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, + mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at + FROM groups g + JOIN group_profiles gp USING (group_profile_id) + JOIN group_members mu USING (group_id) + JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) + WHERE g.user_id = ? AND mu.contact_id = ? + AND (LOWER(gp.display_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(gp.full_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(gp.description) LIKE '%' || LOWER(?) || '%' + ) + +Plan: +SEARCH mu USING INDEX idx_group_members_contact_id (contact_id=?) +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) +SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) +SEARCH pu USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT 1 FROM users + WHERE (user_id = ? AND local_display_name = ?) + OR contact_id = ? + LIMIT 1 + +Plan: +MULTI-INDEX OR +INDEX 1 +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) +INDEX 2 +SEARCH users USING INDEX sqlite_autoindex_users_1 (contact_id=?) + +Query: + SELECT DISTINCT f.file_id + FROM files f + JOIN snd_files s USING (file_id) + WHERE f.user_id = ? + AND s.file_status IN (?, ?, ?) + AND s.file_descr_id IS NULL + AND s.file_inline IS NULL + AND s.created_at > ? + +Plan: +SEARCH f USING COVERING INDEX idx_files_user_id (user_id=?) +SEARCH s USING INDEX idx_snd_files_file_descr_id (file_descr_id=? AND file_id=?) + +Query: + SELECT agent_conn_id FROM ( + SELECT + agent_conn_id, + (CASE WHEN contact_id IS NOT NULL THEN 1 ELSE 0 END) AS conn_ord + FROM connections + WHERE user_id = ? AND via_contact_uri_hash IN (?,?) AND conn_status != ? + ORDER BY conn_ord DESC, created_at DESC + LIMIT 1 + ) c + +Plan: +SEARCH connections USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_contact_uri_hash=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM connections c + JOIN contacts ct ON ct.contact_id = c.contact_id + WHERE c.user_id = ? AND ct.user_id = ? AND ct.contact_id = ? + +Plan: +SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_contact_id (contact_id=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? + ORDER BY created_at DESC, chat_item_id DESC + LIMIT 1 + +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND group_id = ? AND group_member_id = ? + ORDER BY item_ts DESC, chat_item_id DESC + LIMIT 1 + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND group_id = ? AND group_member_id = ? AND shared_msg_id = ? + ORDER BY chat_item_id DESC + LIMIT 1 + +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=? AND shared_msg_id=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND group_id = ? AND group_member_id IS NULL AND item_text like ? + ORDER BY chat_item_id DESC + LIMIT 1 + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_group_id (group_id=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_text like ? + ORDER BY chat_item_id DESC + LIMIT 1 + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_group_id (group_id=?) + +Query: + SELECT chat_item_id, contact_id, group_id, note_folder_id + FROM chat_items + WHERE chat_item_id = ? + +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT chat_item_id, contact_id, group_id, note_folder_id + FROM chat_items + WHERE user_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' + AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?)) + ORDER BY item_ts ASC, chat_item_id ASC + LIMIT ? + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT chat_item_moderation_id, moderator_member_id, created_by_msg_id, moderated_at + FROM chat_item_moderations + WHERE group_id = ? AND item_member_id = ? AND shared_msg_id = ? + LIMIT 1 + +Plan: +SEARCH chat_item_moderations USING INDEX idx_chat_item_moderations_group (group_id=? AND item_member_id=? AND shared_msg_id=?) + +Query: + SELECT connection_id, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id + FROM connections + WHERE agent_conn_id = ? + +Plan: +SEARCH connections USING INDEX sqlite_autoindex_connections_1 (agent_conn_id=?) + +Query: + SELECT contact_id, group_id + FROM files + WHERE user_id = ? AND file_id = ? + LIMIT 1 + +Plan: +SEARCH files USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT count (m.group_member_id) + FROM groups g + JOIN group_members m USING (group_id) + WHERE g.user_id = ? + AND g.group_id = ? + AND m.member_status NOT IN (?,?,?,?,?) + +Plan: +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) +SEARCH m USING INDEX sqlite_autoindex_group_members_1 (group_id=?) + +Query: + SELECT ct.contact_id + FROM contacts ct + JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id + LEFT JOIN connections c ON c.contact_id = ct.contact_id + WHERE cp.user_id = ? AND cp.contact_link IN (?,?) AND c.connection_id IS NULL + +Plan: +SEARCH cp USING COVERING INDEX idx_contact_profiles_contact_link (user_id=? AND contact_link=?) +SEARCH ct USING COVERING INDEX idx_contacts_contact_profile_id (contact_profile_id=?) +SEARCH c USING COVERING INDEX idx_connections_contact_id (contact_id=?) LEFT-JOIN + +Query: + SELECT ct.contact_id + FROM group_members m + JOIN groups g ON g.group_id = m.group_id + JOIN contacts ct ON ct.contact_id = m.contact_id AND ct.via_group = g.group_id + WHERE m.user_id = ? AND m.group_member_id = ? AND ct.deleted = 0 + LIMIT 1 + +Plan: +SEARCH m USING INTEGER PRIMARY KEY (rowid=?) +SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT ct.contact_id, c.connection_id, c.agent_conn_id, ct.local_display_name + FROM contacts ct + JOIN connections c ON c.contact_id = ct.contact_id + WHERE c.agent_conn_id IN (SELECT conn_id FROM temp_conn_ids) + AND c.conn_type = ? + AND ct.deleted = 0 + +Plan: +SEARCH c USING INDEX sqlite_autoindex_connections_1 (agent_conn_id=?) +LIST SUBQUERY 1 +SCAN temp_conn_ids +SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT f.file_id + FROM files f + JOIN rcv_files r USING (file_id) + WHERE f.user_id = ? AND r.file_status IN (?, ?) + AND r.rcv_file_inline IS NULL + AND r.file_descr_id IS NULL + AND r.created_at > ? + +Plan: +SEARCH f USING COVERING INDEX idx_files_user_id (user_id=?) +SEARCH r USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT g.group_id + FROM groups g + JOIN group_members mu ON mu.group_id = g.group_id + WHERE g.user_id = ? AND g.via_group_link_uri_hash IN (?,?) + AND mu.contact_id = ? AND mu.member_status NOT IN (?,?,?,?) + LIMIT 1 + +Plan: +SEARCH mu USING INDEX idx_group_members_contact_id (contact_id=?) +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT group_id + FROM user_contact_links + WHERE user_id = ? AND conn_req_contact IN (?,?) + +Plan: +SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) + +Query: + SELECT group_id, group_member_id + FROM group_members + WHERE user_id = ? AND local_display_name = ? + AND member_status IN (?,?) AND member_category != ? + +Plan: +SEARCH group_members USING INDEX idx_group_members_user_id_local_display_name (user_id=? AND local_display_name=?) + +Query: + SELECT i.chat_item_id + FROM chat_items i + JOIN group_members m ON m.group_id = i.group_id + AND ((i.group_member_id IS NULL AND m.member_category = ?) + OR i.group_member_id = m.group_member_id) + WHERE i.user_id = ? AND i.group_id = ? AND m.member_id = ? AND i.shared_msg_id = ? + ORDER BY i.chat_item_id DESC + LIMIT 1 + +Plan: +SEARCH m USING INDEX sqlite_autoindex_group_members_1 (group_id=? AND member_id=?) +SEARCH i USING INDEX idx_chat_items_group_id_shared_msg_id (group_id=? AND shared_msg_id=?) + +Query: + SELECT i.chat_item_id + FROM chat_items i + JOIN group_members m ON m.group_member_id = i.group_member_id + WHERE i.user_id = ? AND i.group_id = ? AND m.local_display_name = ? AND i.item_text like ? + ORDER BY i.chat_item_id DESC + LIMIT 1 + +Plan: +SEARCH i USING INDEX idx_chat_items_group_id (group_id=?) +SEARCH m USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT i.chat_item_id, i.contact_id, i.group_id, i.note_folder_id + FROM chat_items i + JOIN groups g ON g.chat_item_id = i.chat_item_id + WHERE g.user_id = ? AND g.group_id = ? + LIMIT 1 + +Plan: +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) +SEARCH i USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT member_status + FROM group_members + WHERE group_id = ? AND user_id = ? + +Plan: +SEARCH group_members USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) + +Query: + SELECT r.file_id + FROM rcv_files r + JOIN files f ON f.file_id = r.file_id + WHERE f.user_id = ? AND r.file_status = ? + AND r.to_receive = 1 AND r.created_at > ? + +Plan: +SEARCH f USING COVERING INDEX idx_files_user_id (user_id=?) +SEARCH r USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name, + f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name, + f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline, + r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays, + c.connection_id, c.agent_conn_id + FROM rcv_files r + JOIN files f USING (file_id) + LEFT JOIN connections c ON r.file_id = c.rcv_file_id + LEFT JOIN contacts cs ON cs.contact_id = f.contact_id + LEFT JOIN group_members m ON m.group_member_id = r.group_member_id + WHERE f.user_id = ? AND f.file_id = ? + +Plan: +SEARCH f USING INTEGER PRIMARY KEY (rowid=?) +SEARCH r USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_rcv_file_id (rcv_file_id=?) LEFT-JOIN +SEARCH cs USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN + +Query: + SELECT r.probe, r.contact_id, g.group_id, r.group_member_id + FROM received_probes r + LEFT JOIN contacts c ON r.contact_id = c.contact_id AND c.deleted = 0 + LEFT JOIN group_members m ON r.group_member_id = m.group_member_id + LEFT JOIN groups g ON g.group_id = m.group_id + WHERE r.user_id = ? AND r.probe_hash = ? AND r.probe IS NOT NULL + +Plan: +SEARCH r USING INDEX idx_received_probes_user_id (user_id=?) +SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN + +Query: + UPDATE connections SET via_contact_uri_hash = NULL, xcontact_id = NULL + WHERE user_id = ? AND via_group_link = 1 AND contact_id IN ( + SELECT contact_id + FROM group_members + WHERE user_id = ? AND group_member_id = ? + ) + +Plan: +SEARCH connections USING INDEX idx_connections_updated_at (user_id=?) +LIST SUBQUERY 1 +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE contacts + SET local_display_name = ?, contact_profile_id = ?, updated_at = ? + WHERE user_id = ? AND contact_id = ? + +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE group_members + SET contact_id = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND group_member_id = ? + +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE group_profiles + SET display_name = ?, full_name = ?, description = ?, image = ?, preferences = ?, member_admission = ?, updated_at = ? + WHERE group_profile_id IN ( + SELECT group_profile_id + FROM groups + WHERE user_id = ? AND group_id = ? + ) + +Plan: +SEARCH group_profiles USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE user_contact_links + SET auto_accept = ?, business_address = ?, auto_accept_incognito = ?, auto_reply_msg_content = ? + WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL + +Plan: +SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) + +Query: + INSERT INTO connections ( + user_id, agent_conn_id, conn_level, conn_status, conn_type, contact_id, custom_user_profile_id, + conn_chat_version, peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO connections ( + user_id, agent_conn_id, conn_req_inv, conn_level, conn_status, conn_type, contact_conn_initiated, contact_id, custom_user_profile_id, + conn_chat_version, peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO contacts ( + user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, contact_used, + contact_group_member_id, contact_grp_inv_sent, created_at, updated_at, chat_ts + ) VALUES (?,?,?,?,?,?,?,?,?,?,?) + +Plan: +SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?) + +Query: + INSERT INTO group_members + ( group_id, member_id, member_role, member_category, member_status, invited_by, + user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at, + peer_chat_min_version, peer_chat_max_version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO group_members + (group_id, member_id, member_role, member_category, member_status, member_restriction, invited_by, invited_by_group_member_id, + user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at, + peer_chat_min_version, peer_chat_max_version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO msg_deliveries + (message_id, connection_id, agent_msg_id, agent_msg_meta, chat_ts, created_at, updated_at, delivery_status) + VALUES (?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO operator_usage_conditions + (server_operator_id, server_operator_tag, conditions_commit, accepted_at, auto_accepted) + VALUES (?,?,?,?,?) + +Plan: + +Query: + INSERT INTO remote_hosts + (host_device_name, store_path, bind_addr, bind_iface, bind_port, ca_key, ca_cert, id_key, host_fingerprint, host_dh_pub) + VALUES + (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + +Plan: + +Query: + SELECT + -- Contact + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, + -- Connection + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM contacts ct + JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id + LEFT JOIN connections c ON c.contact_id = ct.contact_id + WHERE ct.user_id = ? AND ct.contact_id = ? + AND ct.deleted = ? + AND ( + c.connection_id = ( + SELECT cc_connection_id FROM ( + SELECT + cc.connection_id AS cc_connection_id, + cc.created_at AS cc_created_at, + (CASE WHEN cc.conn_status = ? OR cc.conn_status = ? THEN 1 ELSE 0 END) AS cc_conn_status_ord + FROM connections cc + WHERE cc.user_id = ct.user_id AND cc.contact_id = ct.contact_id + ORDER BY cc_conn_status_ord DESC, cc_created_at DESC + LIMIT 1 + ) cc + ) + OR c.connection_id IS NULL + ) + +Plan: +SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH cp USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_contact_id (contact_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 2 +CO-ROUTINE cc +SEARCH cc USING INDEX idx_connections_contact_id (contact_id=?) +USE TEMP B-TREE FOR ORDER BY +SCAN cc + +Query: + SELECT + connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, + custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at + FROM connections + WHERE user_id = ? + AND conn_type = ? + AND conn_status != ? + AND contact_id IS NULL + AND conn_level = 0 + AND via_contact IS NULL + AND (via_group_link = 0 OR (via_group_link = 1 AND group_link_id IS NOT NULL)) + AND LOWER(local_alias) LIKE '%' || LOWER(?) || '%' + AND updated_at < ? ORDER BY updated_at DESC LIMIT ? +Plan: +SEARCH connections USING INDEX idx_connections_updated_at (user_id=? AND updated_at<?) + +Query: + SELECT + connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, + custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at + FROM connections + WHERE user_id = ? + AND conn_type = ? + AND conn_status != ? + AND contact_id IS NULL + AND conn_level = 0 + AND via_contact IS NULL + AND (via_group_link = 0 OR (via_group_link = 1 AND group_link_id IS NOT NULL)) + AND LOWER(local_alias) LIKE '%' || LOWER(?) || '%' + AND updated_at > ? ORDER BY updated_at ASC LIMIT ? +Plan: +SEARCH connections USING INDEX idx_connections_updated_at (user_id=? AND updated_at>?) + +Query: + SELECT + connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, + custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at + FROM connections + WHERE user_id = ? + AND conn_type = ? + AND conn_status != ? + AND contact_id IS NULL + AND conn_level = 0 + AND via_contact IS NULL + AND (via_group_link = 0 OR (via_group_link = 1 AND group_link_id IS NOT NULL)) + AND LOWER(local_alias) LIKE '%' || LOWER(?) || '%' + ORDER BY updated_at DESC LIMIT ? +Plan: +SEARCH connections USING INDEX idx_connections_updated_at (user_id=?) + +Query: + SELECT + contact_id, shared_call_id, call_uuid, chat_item_id, call_state, call_ts + FROM calls + ORDER BY call_ts ASC + +Plan: +SCAN calls +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, + cr.created_at, cr.updated_at, + cr.peer_chat_min_version, cr.peer_chat_max_version + FROM contact_requests cr + JOIN connections c ON c.user_contact_link_id = cr.user_contact_link_id + JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id + JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id + WHERE cr.user_id = ? + AND uc.user_id = ? + AND uc.local_display_name = '' + AND uc.group_id IS NULL + AND ( + LOWER(cr.local_display_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(p.display_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(p.full_name) LIKE '%' || LOWER(?) || '%' + ) + AND cr.updated_at < ? ORDER BY cr.updated_at DESC LIMIT ? +Plan: +SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) +SEARCH cr USING INDEX idx_contact_requests_updated_at (user_id=? AND updated_at<?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) + +Query: + SELECT + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, + cr.created_at, cr.updated_at, + cr.peer_chat_min_version, cr.peer_chat_max_version + FROM contact_requests cr + JOIN connections c ON c.user_contact_link_id = cr.user_contact_link_id + JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id + JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id + WHERE cr.user_id = ? + AND uc.user_id = ? + AND uc.local_display_name = '' + AND uc.group_id IS NULL + AND ( + LOWER(cr.local_display_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(p.display_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(p.full_name) LIKE '%' || LOWER(?) || '%' + ) + AND cr.updated_at > ? ORDER BY cr.updated_at ASC LIMIT ? +Plan: +SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) +SEARCH cr USING INDEX idx_contact_requests_updated_at (user_id=? AND updated_at>?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) + +Query: + SELECT + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, + cr.created_at, cr.updated_at, + cr.peer_chat_min_version, cr.peer_chat_max_version + FROM contact_requests cr + JOIN connections c ON c.user_contact_link_id = cr.user_contact_link_id + JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id + JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id + WHERE cr.user_id = ? + AND uc.user_id = ? + AND uc.local_display_name = '' + AND uc.group_id IS NULL + AND ( + LOWER(cr.local_display_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(p.display_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(p.full_name) LIKE '%' || LOWER(?) || '%' + ) + ORDER BY cr.updated_at DESC LIMIT ? +Plan: +SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) +SEARCH cr USING INDEX idx_contact_requests_updated_at (user_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) + +Query: + SELECT + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, cr.created_at, cr.updated_at, + cr.peer_chat_min_version, cr.peer_chat_max_version + FROM contact_requests cr + JOIN connections c USING (user_contact_link_id) + JOIN contact_profiles p USING (contact_profile_id) + WHERE cr.user_id = ? + AND cr.contact_request_id = ? + +Plan: +SEARCH cr USING INTEGER PRIMARY KEY (rowid=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) + +Query: + SELECT + created_at, updated_at, chat_ts, favorite, unread_chat + FROM note_folders + WHERE user_id = ? + AND note_folder_id = ? + +Plan: +SEARCH note_folders USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT + ct.contact_id, + ct.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.contact_id = ct.contact_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + ct.unread_chat + FROM contacts ct + LEFT JOIN ( + SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND contact_id IS NOT NULL AND item_status = ? + GROUP BY contact_id + ) ChatStats ON ChatStats.contact_id = ct.contact_id + + JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id + WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used = 1 + AND ( + LOWER(ct.local_display_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(cp.display_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(cp.full_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(cp.local_alias) LIKE '%' || LOWER(?) || '%' + ) + ORDER BY ct.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts (user_id=? AND contact_id>?) +SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?) +SEARCH cp USING INTEGER PRIMARY KEY (rowid=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (contact_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) + +Query: + SELECT + ct.contact_id, + ct.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.contact_id = ct.contact_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + ct.unread_chat + FROM contacts ct + LEFT JOIN ( + SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND contact_id IS NOT NULL AND item_status = ? + GROUP BY contact_id + ) ChatStats ON ChatStats.contact_id = ct.contact_id + + WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used = 1 + AND (ct.favorite = 1 + OR ct.unread_chat = 1 OR ChatStats.UnreadCount > 0) + ORDER BY ct.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts (user_id=? AND contact_id>?) +SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (contact_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) + +Query: + SELECT + ct.contact_id, + ct.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.contact_id = ct.contact_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + ct.unread_chat + FROM contacts ct + LEFT JOIN ( + SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND contact_id IS NOT NULL AND item_status = ? + GROUP BY contact_id + ) ChatStats ON ChatStats.contact_id = ct.contact_id + + WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used = 1 + AND (ct.unread_chat = 1 OR ChatStats.UnreadCount > 0) + AND ct.chat_ts < ? ORDER BY ct.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts (user_id=? AND contact_id>?) +SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=? AND chat_ts<?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (contact_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) + +Query: + SELECT + ct.contact_id, + ct.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.contact_id = ct.contact_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + ct.unread_chat + FROM contacts ct + LEFT JOIN ( + SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND contact_id IS NOT NULL AND item_status = ? + GROUP BY contact_id + ) ChatStats ON ChatStats.contact_id = ct.contact_id + + WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used = 1 + AND (ct.unread_chat = 1 OR ChatStats.UnreadCount > 0) + AND ct.chat_ts > ? ORDER BY ct.chat_ts ASC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts (user_id=? AND contact_id>?) +SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=? AND chat_ts>?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (contact_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) + +Query: + SELECT + ct.contact_id, + ct.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.contact_id = ct.contact_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + ct.unread_chat + FROM contacts ct + LEFT JOIN ( + SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND contact_id IS NOT NULL AND item_status = ? + GROUP BY contact_id + ) ChatStats ON ChatStats.contact_id = ct.contact_id + + WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used = 1 + AND (ct.unread_chat = 1 OR ChatStats.UnreadCount > 0) + ORDER BY ct.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts (user_id=? AND contact_id>?) +SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (contact_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) + +Query: + SELECT + ct.contact_id, + ct.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.contact_id = ct.contact_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + ct.unread_chat + FROM contacts ct + LEFT JOIN ( + SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND contact_id IS NOT NULL AND item_status = ? + GROUP BY contact_id + ) ChatStats ON ChatStats.contact_id = ct.contact_id + + WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used = 1 + AND ct.favorite = 1 + AND ct.chat_ts < ? ORDER BY ct.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts (user_id=? AND contact_id>?) +SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=? AND chat_ts<?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (contact_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) + +Query: + SELECT + ct.contact_id, + ct.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.contact_id = ct.contact_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + ct.unread_chat + FROM contacts ct + LEFT JOIN ( + SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND contact_id IS NOT NULL AND item_status = ? + GROUP BY contact_id + ) ChatStats ON ChatStats.contact_id = ct.contact_id + + WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used = 1 + AND ct.favorite = 1 + AND ct.chat_ts > ? ORDER BY ct.chat_ts ASC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts (user_id=? AND contact_id>?) +SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=? AND chat_ts>?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (contact_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) + +Query: + SELECT + ct.contact_id, + ct.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.contact_id = ct.contact_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + ct.unread_chat + FROM contacts ct + LEFT JOIN ( + SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND contact_id IS NOT NULL AND item_status = ? + GROUP BY contact_id + ) ChatStats ON ChatStats.contact_id = ct.contact_id + + WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used = 1 + AND ct.favorite = 1 + ORDER BY ct.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts (user_id=? AND contact_id>?) +SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (contact_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) + +Query: + SELECT + ct.contact_id, + ct.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.contact_id = ct.contact_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + ct.unread_chat + FROM contacts ct + LEFT JOIN ( + SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND contact_id IS NOT NULL AND item_status = ? + GROUP BY contact_id + ) ChatStats ON ChatStats.contact_id = ct.contact_id + WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used = 1 AND ct.chat_ts < ? ORDER BY ct.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts (user_id=? AND contact_id>?) +SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=? AND chat_ts<?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (contact_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) + +Query: + SELECT + ct.contact_id, + ct.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.contact_id = ct.contact_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + ct.unread_chat + FROM contacts ct + LEFT JOIN ( + SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND contact_id IS NOT NULL AND item_status = ? + GROUP BY contact_id + ) ChatStats ON ChatStats.contact_id = ct.contact_id + WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used = 1 AND ct.chat_ts > ? ORDER BY ct.chat_ts ASC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts (user_id=? AND contact_id>?) +SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=? AND chat_ts>?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (contact_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) + +Query: + SELECT + ct.contact_id, + ct.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.contact_id = ct.contact_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + ct.unread_chat + FROM contacts ct + LEFT JOIN ( + SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND contact_id IS NOT NULL AND item_status = ? + GROUP BY contact_id + ) ChatStats ON ChatStats.contact_id = ct.contact_id + WHERE ct.user_id = ? AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used = 1 ORDER BY ct.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts (user_id=? AND contact_id>?) +SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (contact_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) + +Query: + SELECT + g.group_id, + g.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.group_id = g.group_id + ORDER BY ci.item_ts DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + COALESCE(ChatStats.UnreadMentions, 0), + COALESCE(ReportCount.Count, 0), + COALESCE(ChatStats.MinUnread, 0), + g.unread_chat + FROM groups g + LEFT JOIN ( + SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + GROUP BY group_id + ) ChatStats ON ChatStats.group_id = g.group_id + LEFT JOIN ( + SELECT group_id, COUNT(1) AS Count + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL + AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 + GROUP BY group_id + ) ReportCount ON ReportCount.group_id = g.group_id + + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + WHERE g.user_id = ? + AND ( + LOWER(g.local_display_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(gp.display_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(gp.full_name) LIKE '%' || LOWER(?) || '%' + OR LOWER(gp.description) LIKE '%' || LOWER(?) || '%' + ) + ORDER BY g.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +MATERIALIZE ReportCount +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) +SEARCH g USING INDEX idx_groups_chat_ts (user_id=?) +SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) + +Query: + SELECT + g.group_id, + g.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.group_id = g.group_id + ORDER BY ci.item_ts DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + COALESCE(ChatStats.UnreadMentions, 0), + COALESCE(ReportCount.Count, 0), + COALESCE(ChatStats.MinUnread, 0), + g.unread_chat + FROM groups g + LEFT JOIN ( + SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + GROUP BY group_id + ) ChatStats ON ChatStats.group_id = g.group_id + LEFT JOIN ( + SELECT group_id, COUNT(1) AS Count + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL + AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 + GROUP BY group_id + ) ReportCount ON ReportCount.group_id = g.group_id + + WHERE g.user_id = ? + AND (g.favorite = 1 + OR g.unread_chat = 1 OR ChatStats.UnreadCount > 0) + ORDER BY g.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +MATERIALIZE ReportCount +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) +SEARCH g USING INDEX idx_groups_chat_ts (user_id=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) + +Query: + SELECT + g.group_id, + g.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.group_id = g.group_id + ORDER BY ci.item_ts DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + COALESCE(ChatStats.UnreadMentions, 0), + COALESCE(ReportCount.Count, 0), + COALESCE(ChatStats.MinUnread, 0), + g.unread_chat + FROM groups g + LEFT JOIN ( + SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + GROUP BY group_id + ) ChatStats ON ChatStats.group_id = g.group_id + LEFT JOIN ( + SELECT group_id, COUNT(1) AS Count + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL + AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 + GROUP BY group_id + ) ReportCount ON ReportCount.group_id = g.group_id + + WHERE g.user_id = ? + AND (g.unread_chat = 1 OR ChatStats.UnreadCount > 0) + AND g.chat_ts < ? ORDER BY g.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +MATERIALIZE ReportCount +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) +SEARCH g USING INDEX idx_groups_chat_ts (user_id=? AND chat_ts<?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) + +Query: + SELECT + g.group_id, + g.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.group_id = g.group_id + ORDER BY ci.item_ts DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + COALESCE(ChatStats.UnreadMentions, 0), + COALESCE(ReportCount.Count, 0), + COALESCE(ChatStats.MinUnread, 0), + g.unread_chat + FROM groups g + LEFT JOIN ( + SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + GROUP BY group_id + ) ChatStats ON ChatStats.group_id = g.group_id + LEFT JOIN ( + SELECT group_id, COUNT(1) AS Count + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL + AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 + GROUP BY group_id + ) ReportCount ON ReportCount.group_id = g.group_id + + WHERE g.user_id = ? + AND (g.unread_chat = 1 OR ChatStats.UnreadCount > 0) + AND g.chat_ts > ? ORDER BY g.chat_ts ASC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +MATERIALIZE ReportCount +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) +SEARCH g USING INDEX idx_groups_chat_ts (user_id=? AND chat_ts>?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) + +Query: + SELECT + g.group_id, + g.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.group_id = g.group_id + ORDER BY ci.item_ts DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + COALESCE(ChatStats.UnreadMentions, 0), + COALESCE(ReportCount.Count, 0), + COALESCE(ChatStats.MinUnread, 0), + g.unread_chat + FROM groups g + LEFT JOIN ( + SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + GROUP BY group_id + ) ChatStats ON ChatStats.group_id = g.group_id + LEFT JOIN ( + SELECT group_id, COUNT(1) AS Count + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL + AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 + GROUP BY group_id + ) ReportCount ON ReportCount.group_id = g.group_id + + WHERE g.user_id = ? + AND (g.unread_chat = 1 OR ChatStats.UnreadCount > 0) + ORDER BY g.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +MATERIALIZE ReportCount +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) +SEARCH g USING INDEX idx_groups_chat_ts (user_id=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) + +Query: + SELECT + g.group_id, + g.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.group_id = g.group_id + ORDER BY ci.item_ts DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + COALESCE(ChatStats.UnreadMentions, 0), + COALESCE(ReportCount.Count, 0), + COALESCE(ChatStats.MinUnread, 0), + g.unread_chat + FROM groups g + LEFT JOIN ( + SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + GROUP BY group_id + ) ChatStats ON ChatStats.group_id = g.group_id + LEFT JOIN ( + SELECT group_id, COUNT(1) AS Count + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL + AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 + GROUP BY group_id + ) ReportCount ON ReportCount.group_id = g.group_id + + WHERE g.user_id = ? + AND g.favorite = 1 + AND g.chat_ts < ? ORDER BY g.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +MATERIALIZE ReportCount +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) +SEARCH g USING INDEX idx_groups_chat_ts (user_id=? AND chat_ts<?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) + +Query: + SELECT + g.group_id, + g.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.group_id = g.group_id + ORDER BY ci.item_ts DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + COALESCE(ChatStats.UnreadMentions, 0), + COALESCE(ReportCount.Count, 0), + COALESCE(ChatStats.MinUnread, 0), + g.unread_chat + FROM groups g + LEFT JOIN ( + SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + GROUP BY group_id + ) ChatStats ON ChatStats.group_id = g.group_id + LEFT JOIN ( + SELECT group_id, COUNT(1) AS Count + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL + AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 + GROUP BY group_id + ) ReportCount ON ReportCount.group_id = g.group_id + + WHERE g.user_id = ? + AND g.favorite = 1 + AND g.chat_ts > ? ORDER BY g.chat_ts ASC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +MATERIALIZE ReportCount +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) +SEARCH g USING INDEX idx_groups_chat_ts (user_id=? AND chat_ts>?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) + +Query: + SELECT + g.group_id, + g.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.group_id = g.group_id + ORDER BY ci.item_ts DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + COALESCE(ChatStats.UnreadMentions, 0), + COALESCE(ReportCount.Count, 0), + COALESCE(ChatStats.MinUnread, 0), + g.unread_chat + FROM groups g + LEFT JOIN ( + SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + GROUP BY group_id + ) ChatStats ON ChatStats.group_id = g.group_id + LEFT JOIN ( + SELECT group_id, COUNT(1) AS Count + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL + AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 + GROUP BY group_id + ) ReportCount ON ReportCount.group_id = g.group_id + + WHERE g.user_id = ? + AND g.favorite = 1 + ORDER BY g.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +MATERIALIZE ReportCount +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) +SEARCH g USING INDEX idx_groups_chat_ts (user_id=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) + +Query: + SELECT + g.group_id, + g.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.group_id = g.group_id + ORDER BY ci.item_ts DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + COALESCE(ChatStats.UnreadMentions, 0), + COALESCE(ReportCount.Count, 0), + COALESCE(ChatStats.MinUnread, 0), + g.unread_chat + FROM groups g + LEFT JOIN ( + SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + GROUP BY group_id + ) ChatStats ON ChatStats.group_id = g.group_id + LEFT JOIN ( + SELECT group_id, COUNT(1) AS Count + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL + AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 + GROUP BY group_id + ) ReportCount ON ReportCount.group_id = g.group_id + WHERE g.user_id = ? AND g.chat_ts < ? ORDER BY g.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +MATERIALIZE ReportCount +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) +SEARCH g USING INDEX idx_groups_chat_ts (user_id=? AND chat_ts<?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) + +Query: + SELECT + g.group_id, + g.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.group_id = g.group_id + ORDER BY ci.item_ts DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + COALESCE(ChatStats.UnreadMentions, 0), + COALESCE(ReportCount.Count, 0), + COALESCE(ChatStats.MinUnread, 0), + g.unread_chat + FROM groups g + LEFT JOIN ( + SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + GROUP BY group_id + ) ChatStats ON ChatStats.group_id = g.group_id + LEFT JOIN ( + SELECT group_id, COUNT(1) AS Count + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL + AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 + GROUP BY group_id + ) ReportCount ON ReportCount.group_id = g.group_id + WHERE g.user_id = ? AND g.chat_ts > ? ORDER BY g.chat_ts ASC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +MATERIALIZE ReportCount +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) +SEARCH g USING INDEX idx_groups_chat_ts (user_id=? AND chat_ts>?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) + +Query: + SELECT + g.group_id, + g.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.group_id = g.group_id + ORDER BY ci.item_ts DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + COALESCE(ChatStats.UnreadMentions, 0), + COALESCE(ReportCount.Count, 0), + COALESCE(ChatStats.MinUnread, 0), + g.unread_chat + FROM groups g + LEFT JOIN ( + SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + GROUP BY group_id + ) ChatStats ON ChatStats.group_id = g.group_id + LEFT JOIN ( + SELECT group_id, COUNT(1) AS Count + FROM chat_items + WHERE user_id = ? AND group_id IS NOT NULL + AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 + GROUP BY group_id + ) ReportCount ON ReportCount.group_id = g.group_id + WHERE g.user_id = ? ORDER BY g.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +MATERIALIZE ReportCount +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) +SEARCH g USING INDEX idx_groups_chat_ts (user_id=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) + +Query: + SELECT + nf.note_folder_id, + nf.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.note_folder_id = nf.note_folder_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + nf.unread_chat + FROM note_folders nf + LEFT JOIN ( + SELECT note_folder_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND note_folder_id IS NOT NULL AND item_status = ? + GROUP BY note_folder_id + ) ChatStats ON ChatStats.note_folder_id = nf.note_folder_id + + WHERE nf.user_id = ? + AND (nf.favorite = 1 + OR nf.unread_chat = 1 OR ChatStats.UnreadCount > 0) + ORDER BY nf.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes (user_id=? AND note_folder_id>?) +SEARCH nf USING INDEX note_folders_user_id (user_id=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (note_folder_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT + nf.note_folder_id, + nf.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.note_folder_id = nf.note_folder_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + nf.unread_chat + FROM note_folders nf + LEFT JOIN ( + SELECT note_folder_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND note_folder_id IS NOT NULL AND item_status = ? + GROUP BY note_folder_id + ) ChatStats ON ChatStats.note_folder_id = nf.note_folder_id + + WHERE nf.user_id = ? + AND (nf.unread_chat = 1 OR ChatStats.UnreadCount > 0) + AND nf.chat_ts < ? ORDER BY nf.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes (user_id=? AND note_folder_id>?) +SEARCH nf USING INDEX note_folders_user_id (user_id=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (note_folder_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT + nf.note_folder_id, + nf.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.note_folder_id = nf.note_folder_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + nf.unread_chat + FROM note_folders nf + LEFT JOIN ( + SELECT note_folder_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND note_folder_id IS NOT NULL AND item_status = ? + GROUP BY note_folder_id + ) ChatStats ON ChatStats.note_folder_id = nf.note_folder_id + + WHERE nf.user_id = ? + AND (nf.unread_chat = 1 OR ChatStats.UnreadCount > 0) + AND nf.chat_ts > ? ORDER BY nf.chat_ts ASC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes (user_id=? AND note_folder_id>?) +SEARCH nf USING INDEX note_folders_user_id (user_id=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (note_folder_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT + nf.note_folder_id, + nf.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.note_folder_id = nf.note_folder_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + nf.unread_chat + FROM note_folders nf + LEFT JOIN ( + SELECT note_folder_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND note_folder_id IS NOT NULL AND item_status = ? + GROUP BY note_folder_id + ) ChatStats ON ChatStats.note_folder_id = nf.note_folder_id + + WHERE nf.user_id = ? + AND (nf.unread_chat = 1 OR ChatStats.UnreadCount > 0) + ORDER BY nf.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes (user_id=? AND note_folder_id>?) +SEARCH nf USING INDEX note_folders_user_id (user_id=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (note_folder_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT + nf.note_folder_id, + nf.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.note_folder_id = nf.note_folder_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + nf.unread_chat + FROM note_folders nf + LEFT JOIN ( + SELECT note_folder_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND note_folder_id IS NOT NULL AND item_status = ? + GROUP BY note_folder_id + ) ChatStats ON ChatStats.note_folder_id = nf.note_folder_id + + WHERE nf.user_id = ? + AND nf.favorite = 1 + AND nf.chat_ts < ? ORDER BY nf.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes (user_id=? AND note_folder_id>?) +SEARCH nf USING INDEX note_folders_user_id (user_id=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (note_folder_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT + nf.note_folder_id, + nf.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.note_folder_id = nf.note_folder_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + nf.unread_chat + FROM note_folders nf + LEFT JOIN ( + SELECT note_folder_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND note_folder_id IS NOT NULL AND item_status = ? + GROUP BY note_folder_id + ) ChatStats ON ChatStats.note_folder_id = nf.note_folder_id + + WHERE nf.user_id = ? + AND nf.favorite = 1 + AND nf.chat_ts > ? ORDER BY nf.chat_ts ASC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes (user_id=? AND note_folder_id>?) +SEARCH nf USING INDEX note_folders_user_id (user_id=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (note_folder_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT + nf.note_folder_id, + nf.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.note_folder_id = nf.note_folder_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + nf.unread_chat + FROM note_folders nf + LEFT JOIN ( + SELECT note_folder_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND note_folder_id IS NOT NULL AND item_status = ? + GROUP BY note_folder_id + ) ChatStats ON ChatStats.note_folder_id = nf.note_folder_id + + WHERE nf.user_id = ? + AND nf.favorite = 1 + ORDER BY nf.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes (user_id=? AND note_folder_id>?) +SEARCH nf USING INDEX note_folders_user_id (user_id=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (note_folder_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT + nf.note_folder_id, + nf.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.note_folder_id = nf.note_folder_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + nf.unread_chat + FROM note_folders nf + LEFT JOIN ( + SELECT note_folder_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND note_folder_id IS NOT NULL AND item_status = ? + GROUP BY note_folder_id + ) ChatStats ON ChatStats.note_folder_id = nf.note_folder_id + WHERE nf.user_id = ? AND nf.chat_ts < ? ORDER BY nf.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes (user_id=? AND note_folder_id>?) +SEARCH nf USING INDEX note_folders_user_id (user_id=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (note_folder_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT + nf.note_folder_id, + nf.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.note_folder_id = nf.note_folder_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + nf.unread_chat + FROM note_folders nf + LEFT JOIN ( + SELECT note_folder_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND note_folder_id IS NOT NULL AND item_status = ? + GROUP BY note_folder_id + ) ChatStats ON ChatStats.note_folder_id = nf.note_folder_id + WHERE nf.user_id = ? AND nf.chat_ts > ? ORDER BY nf.chat_ts ASC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes (user_id=? AND note_folder_id>?) +SEARCH nf USING INDEX note_folders_user_id (user_id=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (note_folder_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT + nf.note_folder_id, + nf.chat_ts, + ( + SELECT chat_item_id + FROM chat_items ci + WHERE ci.user_id = ? AND ci.note_folder_id = nf.note_folder_id + ORDER BY ci.created_at DESC + LIMIT 1 + ) AS chat_item_id, + COALESCE(ChatStats.UnreadCount, 0), + 0, + COALESCE(ChatStats.MinUnread, 0), + nf.unread_chat + FROM note_folders nf + LEFT JOIN ( + SELECT note_folder_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE user_id = ? AND note_folder_id IS NOT NULL AND item_status = ? + GROUP BY note_folder_id + ) ChatStats ON ChatStats.note_folder_id = nf.note_folder_id + WHERE nf.user_id = ? ORDER BY nf.chat_ts DESC LIMIT ? +Plan: +MATERIALIZE ChatStats +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes (user_id=? AND note_folder_id>?) +SEARCH nf USING INDEX note_folders_user_id (user_id=?) +SEARCH ChatStats USING AUTOMATIC COVERING INDEX (note_folder_id=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH ci USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT COUNT(1) + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_status = ? + +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts (user_id=? AND contact_id=? AND item_status=?) + +Query: + SELECT COUNT(1) + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND item_status = ? + +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes (user_id=? AND note_folder_id=? AND item_status=?) + +Query: + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM connections c + JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id + WHERE c.user_id = ? AND uc.user_id = ? AND uc.group_id = ? + +Plan: +SEARCH uc USING INDEX idx_user_contact_links_group_id (group_id=?) +SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) + +Query: + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, + uc.user_contact_link_id, uc.conn_req_contact, uc.group_id + FROM connections c + JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id + WHERE c.user_id = ? AND uc.user_id = ? + +Plan: +SEARCH c USING INDEX idx_connections_updated_at (user_id=?) +SEARCH uc USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT chat_item_id + FROM chat_item_messages + WHERE message_id IN ( + SELECT message_id + FROM msg_deliveries + WHERE connection_id = ? AND agent_msg_id = ? + ) + +Plan: +SEARCH chat_item_messages USING INDEX sqlite_autoindex_chat_item_messages_1 (message_id=?) +LIST SUBQUERY 1 +SEARCH msg_deliveries USING INDEX idx_msg_deliveries_agent_msg_id (connection_id=? AND agent_msg_id=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' + AND (created_at < ? OR (created_at = ? AND chat_item_id < ?)) + ORDER BY created_at DESC, chat_item_id DESC + LIMIT ? + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' + AND (created_at > ? OR (created_at = ? AND chat_item_id > ?)) + ORDER BY created_at ASC, chat_item_id ASC + LIMIT ? + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' + ORDER BY created_at DESC, chat_item_id DESC + LIMIT ? + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_sent = ? AND item_text LIKE ? + ORDER BY chat_item_id DESC + LIMIT 1 + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_contact_id (contact_id=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_status = ? + ORDER BY created_at ASC, chat_item_id ASC + LIMIT 1 + +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts (user_id=? AND contact_id=? AND item_status=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_text LIKE ? + ORDER BY chat_item_id DESC + LIMIT 1 + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_contact_id (contact_id=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND shared_msg_id = ? + ORDER BY chat_item_id DESC + LIMIT 1 + +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_direct_shared_msg_id (user_id=? AND contact_id=? AND shared_msg_id=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_text like ? + ORDER BY chat_item_id DESC + LIMIT 1 + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_group_id (group_id=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' + AND (created_at < ? OR (created_at = ? AND chat_item_id < ?)) + ORDER BY created_at DESC, chat_item_id DESC + LIMIT ? + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' + AND (created_at > ? OR (created_at = ? AND chat_item_id > ?)) + ORDER BY created_at ASC, chat_item_id ASC + LIMIT ? + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' + ORDER BY created_at DESC, chat_item_id DESC + LIMIT ? + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND item_status = ? + ORDER BY created_at ASC, chat_item_id ASC + LIMIT 1 + +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes (user_id=? AND note_folder_id=? AND item_status=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE ? + ORDER BY chat_item_id DESC + LIMIT 1 + +Plan: +SEARCH chat_items USING INDEX chat_items_note_folder_id (note_folder_id=?) + +Query: + SELECT chat_item_id, contact_id, group_id, timed_delete_at + FROM chat_items + WHERE user_id = ? AND timed_delete_at IS NOT NULL AND timed_delete_at <= ? + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_timed_delete_at (user_id=? AND timed_delete_at>? AND timed_delete_at<?) + +Query: + SELECT chat_item_version_id, msg_content, item_version_ts, created_at + FROM chat_item_versions + WHERE chat_item_id = ? + ORDER BY chat_item_version_id DESC + +Plan: +SEARCH chat_item_versions USING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) + +Query: + SELECT command_id, connection_id, command_function, command_status + FROM commands + WHERE user_id = ? AND command_id = ? + +Plan: +SEARCH commands USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role + FROM user_contact_links + WHERE user_id = ? AND user_contact_link_id = ? + +Plan: +SEARCH user_contact_links USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, + conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, + created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, + conn_chat_version, peer_chat_min_version, peer_chat_max_version + FROM connections + WHERE user_id = ? AND connection_id = ? + +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at + FROM connections + WHERE user_id = ? + AND conn_type = ? + AND contact_id IS NULL + +Plan: +SEARCH connections USING INDEX idx_connections_updated_at (user_id=?) + +Query: + SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at + FROM connections + WHERE user_id = ? + AND connection_id = ? + AND conn_type = ? + AND contact_id IS NULL + AND conn_level = 0 + AND via_contact IS NULL + +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, cp.preferences -- , ct.user_preferences + FROM contact_profiles cp + WHERE cp.user_id = ? AND cp.contact_profile_id = ? + +Plan: +SEARCH cp USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT ct.contact_id + FROM contacts ct + JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id + WHERE ct.user_id = ? + AND ct.contact_status = ? AND ct.deleted = 0 AND ct.is_user = 0 + AND p.display_name = ? AND p.full_name = ? + AND p.image = ? +Plan: +SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT ct.contact_id + FROM contacts ct + JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id + WHERE ct.user_id = ? + AND ct.contact_status = ? AND ct.deleted = 0 AND ct.is_user = 0 + AND p.display_name = ? AND p.full_name = ? + AND p.image is NULL +Plan: +SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT d.file_descr_id, d.file_descr_text, d.file_descr_part_no, d.file_descr_complete + FROM xftp_file_descriptions d + JOIN rcv_files f ON f.file_descr_id = d.file_descr_id + WHERE f.file_id = ? + LIMIT 1 + +Plan: +SEARCH f USING INTEGER PRIMARY KEY (rowid=?) +SEARCH d USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT d.file_descr_id, d.file_descr_text, d.file_descr_part_no, d.file_descr_complete + FROM xftp_file_descriptions d + JOIN snd_files f ON f.file_descr_id = d.file_descr_id + WHERE f.file_id = ? + LIMIT 1 + +Plan: +SEARCH f USING PRIMARY KEY (file_id=?) +SEARCH d USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT display_name, full_name, image, contact_link, preferences + FROM contact_profiles + WHERE user_id = ? + +Plan: +SEARCH contact_profiles USING INDEX idx_contact_profiles_user_id (user_id=?) + +Query: + SELECT f.file_id + FROM files f + JOIN chat_items i ON i.chat_item_id = f.chat_item_id + WHERE i.user_id = ? AND i.contact_id = ? AND i.shared_msg_id = ? + +Plan: +SEARCH i USING COVERING INDEX idx_chat_items_direct_shared_msg_id (user_id=? AND contact_id=? AND shared_msg_id=?) +SEARCH f USING COVERING INDEX idx_files_chat_item_id (chat_item_id=?) + +Query: + SELECT f.file_id + FROM files f + JOIN chat_items i ON i.chat_item_id = f.chat_item_id + WHERE i.user_id = ? AND i.group_id = ? AND i.shared_msg_id = ? + +Plan: +SEARCH i USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=?) +SEARCH f USING COVERING INDEX idx_files_chat_item_id (chat_item_id=?) + +Query: + SELECT file_id, contact_id, group_id, note_folder_id + FROM files + WHERE agent_snd_file_id = ? + +Plan: +SCAN files + +Query: + SELECT file_name, file_size, chunk_size, file_path, file_crypto_key, file_crypto_nonce, file_inline, agent_snd_file_id, agent_snd_file_deleted, private_snd_file_descr, cancelled, redirect_file_id + FROM files + WHERE user_id = ? AND file_id = ? + +Plan: +SEARCH files USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT group_member_id, group_snd_item_status, via_proxy + FROM group_snd_item_statuses + WHERE chat_item_id = ? + +Plan: +SEARCH group_snd_item_statuses USING INDEX idx_group_snd_item_statuses_chat_item_id_group_member_id (chat_item_id=?) + +Query: + SELECT group_member_id, reaction_ts + FROM chat_item_reactions + WHERE group_id = ? AND shared_msg_id = ? AND reaction = ? + +Plan: +SEARCH chat_item_reactions USING INDEX idx_chat_item_reactions_group (group_id=? AND shared_msg_id=?) + +Query: + SELECT group_member_intro_id, group_queue_info, direct_queue_info, intro_status + FROM group_member_intros + WHERE re_group_member_id = ? AND to_group_member_id = ? + +Plan: +SEARCH group_member_intros USING INDEX sqlite_autoindex_group_member_intros_1 (re_group_member_id=? AND to_group_member_id=?) + +Query: + SELECT group_snd_item_status + FROM group_snd_item_statuses + WHERE chat_item_id = ? AND group_member_id = ? + LIMIT 1 + +Plan: +SEARCH group_snd_item_statuses USING INDEX idx_group_snd_item_statuses_chat_item_id_group_member_id (chat_item_id=? AND group_member_id=?) + +Query: + SELECT i.shared_msg_id + FROM chat_items i + JOIN files f ON f.chat_item_id = i.chat_item_id + WHERE f.user_id = ? AND f.file_id = ? + +Plan: +SEARCH f USING INTEGER PRIMARY KEY (rowid=?) +SEARCH i USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT m.group_member_id + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + WHERE m.user_id = ? AND m.contact_id IS NULL + AND m.member_category != ? + AND p.display_name = ? AND p.full_name = ? + AND p.image = ? +Plan: +SEARCH m USING INDEX idx_group_members_user_id (user_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT m.group_member_id + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + WHERE m.user_id = ? AND m.contact_id IS NULL + AND m.member_category != ? + AND p.display_name = ? AND p.full_name = ? + AND p.image is NULL +Plan: +SEARCH m USING INDEX idx_group_members_user_id (user_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT pgm.message_id, m.shared_msg_id, m.msg_body, m.chat_msg_event, pgm.group_member_intro_id + FROM pending_group_messages pgm + JOIN messages m USING (message_id) + WHERE pgm.group_member_id = ? + ORDER BY pgm.created_at ASC, pgm.message_id ASC + +Plan: +SEARCH pgm USING INDEX idx_pending_group_messages_group_member_id (group_member_id=?) +SEARCH m USING INTEGER PRIMARY KEY (rowid=?) +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT r.contact_id, g.group_id, r.group_member_id + FROM received_probes r + LEFT JOIN contacts c ON r.contact_id = c.contact_id AND c.deleted = 0 + LEFT JOIN group_members m ON r.group_member_id = m.group_member_id + LEFT JOIN groups g ON g.group_id = m.group_id + WHERE r.user_id = ? AND r.probe_hash = ? AND r.probe IS NULL + +Plan: +SEARCH r USING INDEX idx_received_probes_user_id (user_id=?) +SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN + +Query: + SELECT r.display_name, r.member_id, m.group_member_id, m.member_role, p.display_name, p.local_alias + FROM chat_item_mentions r + LEFT JOIN group_members m ON r.group_id = m.group_id AND r.member_id = m.member_id + LEFT JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + WHERE r.chat_item_id = ? + +Plan: +SEARCH r USING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?) +SEARCH m USING INDEX sqlite_autoindex_group_members_1 (group_id=? AND member_id=?) LEFT-JOIN +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN + +Query: + SELECT re_group_member_id + FROM group_member_intros + WHERE to_group_member_id = ? AND intro_status NOT IN (?,?,?) + AND intro_chat_protocol_version >= ? +Plan: +SEARCH group_member_intros USING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?) + +Query: + SELECT reaction + FROM chat_item_reactions + WHERE contact_id = ? AND shared_msg_id = ? AND reaction_sent = ? + +Plan: +SEARCH chat_item_reactions USING INDEX idx_chat_item_reactions_contact (contact_id=? AND shared_msg_id=?) + +Query: + SELECT reaction + FROM chat_item_reactions + WHERE group_id = ? AND group_member_id = ? AND item_member_id = ? AND shared_msg_id = ? AND reaction_sent = ? + +Plan: +SEARCH chat_item_reactions USING INDEX idx_chat_item_reactions_group (group_id=? AND shared_msg_id=?) + +Query: + SELECT reaction, MAX(reaction_sent), COUNT(chat_item_reaction_id) + FROM chat_item_reactions + WHERE contact_id = ? AND shared_msg_id = ? + GROUP BY reaction + +Plan: +SEARCH chat_item_reactions USING INDEX idx_chat_item_reactions_contact (contact_id=? AND shared_msg_id=?) +USE TEMP B-TREE FOR GROUP BY + +Query: + SELECT reaction, MAX(reaction_sent), COUNT(chat_item_reaction_id) + FROM chat_item_reactions + WHERE group_id = ? AND item_member_id = ? AND shared_msg_id = ? + GROUP BY reaction + +Plan: +SEARCH chat_item_reactions USING INDEX idx_chat_item_reactions_group (group_id=? AND shared_msg_id=?) +USE TEMP B-TREE FOR GROUP BY + +Query: + SELECT rf.file_id, f.contact_id, f.group_id, f.note_folder_id + FROM rcv_files rf + JOIN files f ON f.file_id = rf.file_id + WHERE rf.agent_rcv_file_id = ? + +Plan: +SCAN rf +SEARCH f USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT s.file_id, s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, s.file_descr_id, s.file_inline, s.group_member_id, c.local_display_name, m.local_display_name + FROM msg_deliveries d + JOIN snd_files s ON s.connection_id = d.connection_id AND s.last_inline_msg_delivery_id = d.msg_delivery_id + JOIN files f ON f.file_id = s.file_id + LEFT JOIN contacts c USING (contact_id) + LEFT JOIN group_members m USING (group_member_id) + WHERE d.connection_id = ? AND d.agent_msg_id = ? AND f.user_id = ? + AND (s.file_descr_id IS NOT NULL OR s.file_inline IS NOT NULL) + +Plan: +SEARCH d USING COVERING INDEX idx_msg_deliveries_agent_msg_id (connection_id=? AND agent_msg_id=?) +SEARCH s USING INDEX idx_snd_files_last_inline_msg_delivery_id (last_inline_msg_delivery_id=?) +SEARCH f USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN + +Query: + SELECT s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, s.file_descr_id, s.file_inline, s.connection_id, c.agent_conn_id, s.group_member_id, + cs.local_display_name, m.local_display_name + FROM snd_files s + JOIN files f USING (file_id) + JOIN connections c USING (connection_id) + LEFT JOIN contacts cs ON cs.contact_id = f.contact_id + LEFT JOIN group_members m ON m.group_member_id = s.group_member_id + WHERE f.user_id = ? AND f.file_id = ? + +Plan: +SEARCH f USING INTEGER PRIMARY KEY (rowid=?) +SEARCH s USING PRIMARY KEY (file_id=?) +SEARCH c USING INTEGER PRIMARY KEY (rowid=?) +SEARCH cs USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN + +Query: + SELECT smp_server_id, host, port, key_hash, basic_auth, preset, tested, enabled + FROM protocol_servers + WHERE user_id = ? AND protocol = ? + +Plan: +SEARCH protocol_servers USING INDEX idx_smp_servers_user_id (user_id=?) + +Query: + SELECT to_group_member_id + FROM group_member_intros + WHERE re_group_member_id = ? AND intro_status NOT IN (?,?,?) + AND intro_chat_protocol_version >= ? +Plan: +SEARCH group_member_intros USING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?) + +Query: + SELECT usage_conditions_id, conditions_commit, notified_at, created_at + FROM usage_conditions + WHERE conditions_commit = ( + SELECT conditions_commit + FROM operator_usage_conditions + ORDER BY accepted_at DESC + LIMIT 1 + ) + +Plan: +SEARCH usage_conditions USING INDEX sqlite_autoindex_usage_conditions_1 (conditions_commit=?) +SCALAR SUBQUERY 1 +SCAN operator_usage_conditions +USE TEMP B-TREE FOR ORDER BY + +Query: + SELECT usage_conditions_id, conditions_commit, notified_at, created_at + FROM usage_conditions + WHERE usage_conditions_id = ? + +Plan: +SEARCH usage_conditions USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE chat_items + SET item_deleted = 1, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, item_content = ?, item_text = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? + AND group_member_id = ? RETURNING chat_item_id +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=?) + +Query: + UPDATE chat_items + SET item_deleted = 1, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, item_content = ?, item_text = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? + AND group_member_id IS NULL AND item_sent = 1 RETURNING chat_item_id +Plan: +SEARCH chat_items USING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=?) + +Query: + UPDATE chat_items + SET item_deleted = 1, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, item_content = ?, item_text = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND chat_item_id = ? + +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE chat_items + SET item_deleted = ?, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? + AND group_member_id = ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=?) + +Query: + UPDATE chat_items + SET item_deleted = ?, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? + AND group_member_id IS NULL AND item_sent = 1 +Plan: +SEARCH chat_items USING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=?) + +Query: + UPDATE chat_items + SET item_deleted = ?, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 + RETURNING chat_item_id + +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id=? AND msg_content_tag=? AND item_deleted=? AND item_sent=?) + +Query: + UPDATE chat_items + SET item_deleted = ?, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND msg_content_tag = ? AND quoted_shared_msg_id = ? AND item_deleted = ? + RETURNING chat_item_id; + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id=? AND msg_content_tag=? AND item_deleted=?) + +Query: + UPDATE chat_items + SET item_deleted = ?, item_deleted_ts = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND chat_item_id = ? + +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE group_member_intros + SET intro_status = ?, + group_queue_info = ?, + direct_queue_info = ?, + updated_at = ? + WHERE group_member_intro_id = ? + +Plan: +SEARCH group_member_intros USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE group_members + SET contact_id = ?, local_display_name = ?, contact_profile_id = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND group_member_id = ? + +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: + DELETE FROM chat_items + WHERE user_id = ? AND contact_id = ? AND chat_item_id = ? + +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) +SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?) +SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) +SEARCH calls USING COVERING INDEX idx_calls_chat_item_id (chat_item_id=?) +SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_2 (chat_item_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_fwd_from_chat_item_id (fwd_from_chat_item_id=?) +SEARCH files USING COVERING INDEX idx_files_chat_item_id (chat_item_id=?) +SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?) + +Query: + DELETE FROM chat_items + WHERE user_id = ? AND group_id = ? AND chat_item_id = ? + +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) +SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?) +SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) +SEARCH calls USING COVERING INDEX idx_calls_chat_item_id (chat_item_id=?) +SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_2 (chat_item_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_fwd_from_chat_item_id (fwd_from_chat_item_id=?) +SEARCH files USING COVERING INDEX idx_files_chat_item_id (chat_item_id=?) +SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?) + +Query: + DELETE FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND chat_item_id = ? + +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) +SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?) +SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) +SEARCH calls USING COVERING INDEX idx_calls_chat_item_id (chat_item_id=?) +SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_2 (chat_item_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_fwd_from_chat_item_id (fwd_from_chat_item_id=?) +SEARCH files USING COVERING INDEX idx_files_chat_item_id (chat_item_id=?) +SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?) + +Query: + DELETE FROM connections + WHERE user_id = ? + AND connection_id = ? + AND conn_type = ? + AND contact_id IS NULL + AND conn_level = 0 + AND via_contact IS NULL + +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) +SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_agent_msg_id (connection_id=?) +SEARCH commands USING COVERING INDEX idx_commands_connection_id (connection_id=?) +SEARCH messages USING COVERING INDEX idx_messages_connection_id (connection_id=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_connection_id (connection_id=?) + +Query: + DELETE FROM connections WHERE connection_id IN ( + SELECT connection_id + FROM connections c + JOIN contacts ct ON ct.contact_id = c.contact_id + WHERE ct.user_id = ? AND ct.contact_id = ? + ) + +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING COVERING INDEX idx_connections_contact_id (contact_id=?) +SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_agent_msg_id (connection_id=?) +SEARCH commands USING COVERING INDEX idx_commands_connection_id (connection_id=?) +SEARCH messages USING COVERING INDEX idx_messages_connection_id (connection_id=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_connection_id (connection_id=?) + +Query: + DELETE FROM connections WHERE connection_id IN ( + SELECT connection_id + FROM connections c + JOIN user_contact_links uc USING (user_contact_link_id) + WHERE uc.user_id = ? AND uc.group_id = ? + ) + +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH uc USING INDEX idx_user_contact_links_group_id (group_id=?) +SEARCH c USING COVERING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) +SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_agent_msg_id (connection_id=?) +SEARCH commands USING COVERING INDEX idx_commands_connection_id (connection_id=?) +SEARCH messages USING COVERING INDEX idx_messages_connection_id (connection_id=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_connection_id (connection_id=?) + +Query: + DELETE FROM connections WHERE connection_id IN ( + SELECT connection_id + FROM connections c + JOIN user_contact_links uc USING (user_contact_link_id) + WHERE uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL + ) + +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) +SEARCH c USING COVERING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) +SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_agent_msg_id (connection_id=?) +SEARCH commands USING COVERING INDEX idx_commands_connection_id (connection_id=?) +SEARCH messages USING COVERING INDEX idx_messages_connection_id (connection_id=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_connection_id (connection_id=?) + +Query: + DELETE FROM contact_profiles + WHERE + user_id = ? + AND (contact_profile_id IN (SELECT contact_profile_id FROM temp_delete_members) + OR contact_profile_id IN (SELECT member_profile_id FROM temp_delete_members WHERE member_profile_id IS NOT NULL)) + AND contact_profile_id NOT IN (SELECT contact_profile_id FROM group_members) + AND contact_profile_id NOT IN (SELECT member_profile_id FROM group_members) + AND contact_profile_id NOT IN (SELECT contact_profile_id FROM contacts) + AND contact_profile_id NOT IN (SELECT contact_profile_id FROM contact_requests) + AND contact_profile_id NOT IN (SELECT custom_user_profile_id FROM connections) + +Plan: +SEARCH contact_profiles USING COVERING INDEX idx_contact_profiles_user_id (user_id=?) +LIST SUBQUERY 1 +SCAN temp_delete_members +LIST SUBQUERY 2 +SCAN temp_delete_members +USING INDEX idx_group_members_contact_profile_id FOR IN-OPERATOR +USING INDEX idx_group_members_member_profile_id FOR IN-OPERATOR +USING INDEX idx_contacts_contact_profile_id FOR IN-OPERATOR +USING INDEX idx_contact_requests_contact_profile_id FOR IN-OPERATOR +USING INDEX idx_connections_custom_user_profile_id FOR IN-OPERATOR +SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) +SEARCH connections USING COVERING INDEX idx_connections_custom_user_profile_id (custom_user_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_member_profile_id (member_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_contact_profile_id (contact_profile_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_contact_profile_id (contact_profile_id=?) + +Query: + DELETE FROM contact_profiles + WHERE contact_profile_id in ( + SELECT contact_profile_id + FROM contact_requests + WHERE user_id = ? AND contact_request_id = ? + ) + +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) +SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) +SEARCH connections USING COVERING INDEX idx_connections_custom_user_profile_id (custom_user_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_member_profile_id (member_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_contact_profile_id (contact_profile_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_contact_profile_id (contact_profile_id=?) + +Query: + DELETE FROM contact_profiles + WHERE contact_profile_id in ( + SELECT contact_profile_id + FROM contacts + WHERE user_id = ? AND contact_id = ? + ) + +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) +SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) +SEARCH connections USING COVERING INDEX idx_connections_custom_user_profile_id (custom_user_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_member_profile_id (member_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_contact_profile_id (contact_profile_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_contact_profile_id (contact_profile_id=?) + +Query: + DELETE FROM contact_profiles + WHERE contact_profile_id in ( + SELECT cr.contact_profile_id + FROM contact_requests cr + JOIN user_contact_links uc USING (user_contact_link_id) + WHERE uc.user_id = ? AND uc.group_id = ? + ) + +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH uc USING INDEX idx_user_contact_links_group_id (group_id=?) +SEARCH cr USING INDEX idx_contact_requests_user_contact_link_id (user_contact_link_id=?) +SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) +SEARCH connections USING COVERING INDEX idx_connections_custom_user_profile_id (custom_user_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_member_profile_id (member_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_contact_profile_id (contact_profile_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_contact_profile_id (contact_profile_id=?) + +Query: + DELETE FROM contact_profiles + WHERE contact_profile_id in ( + SELECT cr.contact_profile_id + FROM contact_requests cr + JOIN user_contact_links uc USING (user_contact_link_id) + WHERE uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL + ) + +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) +SEARCH cr USING INDEX idx_contact_requests_user_contact_link_id (user_contact_link_id=?) +SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) +SEARCH connections USING COVERING INDEX idx_connections_custom_user_profile_id (custom_user_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_member_profile_id (member_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_contact_profile_id (contact_profile_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_contact_profile_id (contact_profile_id=?) + +Query: + DELETE FROM contact_profiles + WHERE user_id = ? AND contact_profile_id = ? + AND 1 NOT IN ( + SELECT 1 FROM connections + WHERE user_id = ? AND custom_user_profile_id = ? LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM contacts + WHERE user_id = ? AND contact_profile_id = ? LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM contact_requests + WHERE user_id = ? AND contact_profile_id = ? LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM group_members + WHERE user_id = ? + AND (member_profile_id = ? OR contact_profile_id = ?) + LIMIT 1 + ) + +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH connections USING INDEX idx_connections_custom_user_profile_id (custom_user_profile_id=?) +LIST SUBQUERY 2 +SEARCH contacts USING COVERING INDEX sqlite_autoindex_contacts_2 (user_id=? AND contact_profile_id=?) +LIST SUBQUERY 3 +SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_2 (user_id=? AND contact_profile_id=?) +LIST SUBQUERY 4 +SEARCH group_members USING INDEX idx_group_members_user_id (user_id=?) +SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) +SEARCH connections USING COVERING INDEX idx_connections_custom_user_profile_id (custom_user_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_member_profile_id (member_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_contact_profile_id (contact_profile_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_contact_profile_id (contact_profile_id=?) + +Query: + DELETE FROM contact_profiles + WHERE user_id = ? AND contact_profile_id = ? AND incognito = 1 + +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) +SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) +SEARCH connections USING COVERING INDEX idx_connections_custom_user_profile_id (custom_user_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_member_profile_id (member_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_contact_profile_id (contact_profile_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_contact_profile_id (contact_profile_id=?) + +Query: + DELETE FROM contact_profiles + WHERE user_id = ? AND contact_profile_id = ? AND incognito = 1 + AND 1 NOT IN ( + SELECT 1 FROM connections + WHERE user_id = ? AND custom_user_profile_id = ? LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM group_members + WHERE user_id = ? AND member_profile_id = ? LIMIT 1 + ) + +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH connections USING INDEX idx_connections_custom_user_profile_id (custom_user_profile_id=?) +LIST SUBQUERY 2 +SEARCH group_members USING INDEX idx_group_members_user_id (user_id=?) +SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) +SEARCH connections USING COVERING INDEX idx_connections_custom_user_profile_id (custom_user_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_member_profile_id (member_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_contact_profile_id (contact_profile_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_contact_profile_id (contact_profile_id=?) + +Query: + DELETE FROM display_names + WHERE + user_id = ? + AND local_display_name IN (SELECT local_display_name FROM temp_delete_members) + AND local_display_name NOT IN (SELECT local_display_name FROM group_members) + AND local_display_name NOT IN (SELECT local_display_name FROM contacts) + AND local_display_name NOT IN (SELECT local_display_name FROM users) + AND local_display_name NOT IN (SELECT local_display_name FROM groups) + AND local_display_name NOT IN (SELECT local_display_name FROM user_contact_links) + AND local_display_name NOT IN (SELECT local_display_name FROM contact_requests) + +Plan: +SEARCH display_names USING PRIMARY KEY (user_id=? AND local_display_name=?) +LIST SUBQUERY 1 +SCAN temp_delete_members +LIST SUBQUERY 2 +SCAN group_members USING COVERING INDEX idx_group_members_user_id_local_display_name +LIST SUBQUERY 3 +SCAN contacts USING COVERING INDEX sqlite_autoindex_contacts_1 +USING INDEX sqlite_autoindex_users_2 FOR IN-OPERATOR +LIST SUBQUERY 5 +SCAN groups USING COVERING INDEX sqlite_autoindex_groups_1 +LIST SUBQUERY 6 +SCAN user_contact_links USING COVERING INDEX sqlite_autoindex_user_contact_links_1 +LIST SUBQUERY 7 +SCAN contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_1 +SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_1 (user_id=? AND local_display_name=?) +SEARCH group_members USING COVERING INDEX idx_group_members_user_id_local_display_name (user_id=? AND local_display_name=?) +SEARCH groups USING COVERING INDEX sqlite_autoindex_groups_1 (user_id=? AND local_display_name=?) +SEARCH contacts USING COVERING INDEX sqlite_autoindex_contacts_1 (user_id=? AND local_display_name=?) +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + +Query: + DELETE FROM display_names + WHERE user_id = ? + AND local_display_name in ( + SELECT cr.local_display_name + FROM contact_requests cr + JOIN user_contact_links uc USING (user_contact_link_id) + WHERE uc.user_id = ? AND uc.group_id = ? + ) + AND local_display_name NOT IN (SELECT local_display_name FROM users WHERE user_id = ?) + +Plan: +SEARCH display_names USING PRIMARY KEY (user_id=? AND local_display_name=?) +LIST SUBQUERY 1 +SEARCH uc USING INDEX idx_user_contact_links_group_id (group_id=?) +SEARCH cr USING INDEX idx_contact_requests_user_contact_link_id (user_contact_link_id=?) +LIST SUBQUERY 2 +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) +SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_1 (user_id=? AND local_display_name=?) +SEARCH group_members USING COVERING INDEX idx_group_members_user_id_local_display_name (user_id=? AND local_display_name=?) +SEARCH groups USING COVERING INDEX sqlite_autoindex_groups_1 (user_id=? AND local_display_name=?) +SEARCH contacts USING COVERING INDEX sqlite_autoindex_contacts_1 (user_id=? AND local_display_name=?) +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + +Query: + DELETE FROM display_names + WHERE user_id = ? + AND local_display_name in ( + SELECT cr.local_display_name + FROM contact_requests cr + JOIN user_contact_links uc USING (user_contact_link_id) + WHERE uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL + ) + AND local_display_name NOT IN (SELECT local_display_name FROM users WHERE user_id = ?) + +Plan: +SEARCH display_names USING PRIMARY KEY (user_id=? AND local_display_name=?) +LIST SUBQUERY 1 +SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) +SEARCH cr USING INDEX idx_contact_requests_user_contact_link_id (user_contact_link_id=?) +LIST SUBQUERY 2 +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) +SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_1 (user_id=? AND local_display_name=?) +SEARCH group_members USING COVERING INDEX idx_group_members_user_id_local_display_name (user_id=? AND local_display_name=?) +SEARCH groups USING COVERING INDEX sqlite_autoindex_groups_1 (user_id=? AND local_display_name=?) +SEARCH contacts USING COVERING INDEX sqlite_autoindex_contacts_1 (user_id=? AND local_display_name=?) +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + +Query: + DELETE FROM display_names + WHERE user_id = ? AND local_display_name = ( + SELECT local_display_name FROM contact_requests + WHERE user_id = ? AND contact_request_id = ? + ) + AND local_display_name NOT IN (SELECT local_display_name FROM users WHERE user_id = ?) + +Plan: +SEARCH display_names USING PRIMARY KEY (user_id=? AND local_display_name=?) +SCALAR SUBQUERY 1 +SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 2 +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) +SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_1 (user_id=? AND local_display_name=?) +SEARCH group_members USING COVERING INDEX idx_group_members_user_id_local_display_name (user_id=? AND local_display_name=?) +SEARCH groups USING COVERING INDEX sqlite_autoindex_groups_1 (user_id=? AND local_display_name=?) +SEARCH contacts USING COVERING INDEX sqlite_autoindex_contacts_1 (user_id=? AND local_display_name=?) +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + +Query: + DELETE FROM display_names + WHERE user_id = ? AND local_display_name = ? + AND 1 NOT IN ( + SELECT 1 FROM users + WHERE local_display_name = ? LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM contacts + WHERE user_id = ? AND local_display_name = ? LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM groups + WHERE user_id = ? AND local_display_name = ? LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM group_members + WHERE user_id = ? AND local_display_name = ? LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM user_contact_links + WHERE user_id = ? AND local_display_name = ? LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM contact_requests + WHERE user_id = ? AND local_display_name = ? LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM contact_requests + WHERE user_id = ? AND local_display_name = ? LIMIT 1 + ) + +Plan: +SEARCH display_names USING PRIMARY KEY (user_id=? AND local_display_name=?) +LIST SUBQUERY 1 +SEARCH users USING COVERING INDEX sqlite_autoindex_users_2 (local_display_name=?) +LIST SUBQUERY 2 +SEARCH contacts USING COVERING INDEX sqlite_autoindex_contacts_1 (user_id=? AND local_display_name=?) +LIST SUBQUERY 3 +SEARCH groups USING COVERING INDEX sqlite_autoindex_groups_1 (user_id=? AND local_display_name=?) +LIST SUBQUERY 4 +SEARCH group_members USING COVERING INDEX idx_group_members_user_id_local_display_name (user_id=? AND local_display_name=?) +LIST SUBQUERY 5 +SEARCH user_contact_links USING COVERING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) +LIST SUBQUERY 6 +SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_1 (user_id=? AND local_display_name=?) +LIST SUBQUERY 7 +SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_1 (user_id=? AND local_display_name=?) +SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_1 (user_id=? AND local_display_name=?) +SEARCH group_members USING COVERING INDEX idx_group_members_user_id_local_display_name (user_id=? AND local_display_name=?) +SEARCH groups USING COVERING INDEX sqlite_autoindex_groups_1 (user_id=? AND local_display_name=?) +SEARCH contacts USING COVERING INDEX sqlite_autoindex_contacts_1 (user_id=? AND local_display_name=?) +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + +Query: + DELETE FROM display_names + WHERE user_id = ? AND local_display_name = ? + AND local_display_name NOT IN (SELECT local_display_name FROM users WHERE user_id = ?) + +Plan: +SEARCH display_names USING PRIMARY KEY (user_id=? AND local_display_name=?) +LIST SUBQUERY 1 +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) +SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_1 (user_id=? AND local_display_name=?) +SEARCH group_members USING COVERING INDEX idx_group_members_user_id_local_display_name (user_id=? AND local_display_name=?) +SEARCH groups USING COVERING INDEX sqlite_autoindex_groups_1 (user_id=? AND local_display_name=?) +SEARCH contacts USING COVERING INDEX sqlite_autoindex_contacts_1 (user_id=? AND local_display_name=?) +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + +Query: + DELETE FROM files + WHERE user_id = ? + AND chat_item_id IN ( + SELECT chat_item_id FROM chat_items WHERE user_id = ? AND note_folder_id = ? + ) + +Plan: +SEARCH files USING INDEX idx_files_user_id (user_id=?) +LIST SUBQUERY 1 +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) +SEARCH extra_xftp_file_descriptions USING COVERING INDEX idx_extra_xftp_file_descriptions_file_id (file_id=?) +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_file_id (file_id=?) +SEARCH files USING COVERING INDEX idx_files_redirect_file_id (redirect_file_id=?) + +Query: + DELETE FROM group_profiles + WHERE group_profile_id in ( + SELECT group_profile_id + FROM groups + WHERE user_id = ? AND group_id = ? + ) + +Plan: +SEARCH group_profiles USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +SEARCH groups USING COVERING INDEX idx_groups_group_profile_id (group_profile_id=?) + +Query: + INSERT INTO calls + (contact_id, shared_call_id, call_uuid, chat_item_id, call_state, call_ts, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO chat_item_moderations + (group_id, moderator_member_id, item_member_id, shared_msg_id, created_by_msg_id, moderated_at) + VALUES (?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO chat_item_versions (chat_item_id, msg_content, item_version_ts) + VALUES (?,?,?) + +Plan: + +Query: + INSERT INTO chat_items ( + -- user and IDs + user_id, created_by_msg_id, contact_id, group_id, group_member_id, note_folder_id, + -- meta + item_sent, item_ts, item_content, item_content_tag, item_text, item_status, msg_content_tag, shared_msg_id, + forwarded_by_group_member_id, include_in_history, created_at, updated_at, item_live, user_mention, timed_ttl, timed_delete_at, + -- quote + quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id, + -- forwarded from + fwd_from_tag, fwd_from_chat_name, fwd_from_msg_dir, fwd_from_contact_id, fwd_from_group_id, fwd_from_chat_item_id + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO commands (connection_id, command_function, command_status, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO connections + (user_id, agent_conn_id, conn_req_inv, short_link_inv, conn_status, conn_type, contact_conn_initiated, custom_user_profile_id, + created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO connections ( + user_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, custom_user_profile_id, conn_status, conn_type, + contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, updated_at, + conn_chat_version, peer_chat_min_version, peer_chat_max_version, to_subscribe, pq_support, pq_encryption + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO connections ( + user_id, agent_conn_id, conn_status, conn_type, contact_conn_initiated, + via_contact_uri_hash, via_short_link_contact, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, + created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO contact_profiles (display_name, full_name, image, user_id, incognito, created_at, updated_at) + VALUES (?,?,?,?,?,?,?) + +Plan: +SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) + +Query: + INSERT INTO files + ( user_id, note_folder_id, + file_name, file_path, file_size, + file_crypto_key, file_crypto_nonce, + chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at + ) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO msg_deliveries + (message_id, connection_id, agent_msg_id, chat_ts, created_at, updated_at, delivery_status) + VALUES (?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO pending_group_messages + (group_member_id, message_id, group_member_intro_id, created_at, updated_at) VALUES (?,?,?,?,?) + +Plan: + +Query: + INSERT INTO protocol_servers + (protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO remote_controllers + (ctrl_device_name, ca_key, ca_cert, ctrl_fingerprint, id_pub, dh_priv_key, prev_dh_priv_key) + VALUES + (?, ?, ?, ?, ?, ?, ?) + +Plan: + +Query: + INSERT INTO temp_delete_members (contact_profile_id, member_profile_id, local_display_name) + SELECT contact_profile_id, member_profile_id, local_display_name FROM group_members WHERE group_id = ? + +Plan: +SEARCH group_members USING INDEX sqlite_autoindex_group_members_1 (group_id=?) + +Query: + SELECT chat_item_id, timed_ttl + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_contacts (user_id=? AND contact_id=? AND item_status=?) + +Query: + SELECT chat_item_id, timed_ttl + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=? AND item_status=?) + +Query: + SELECT group_snd_item_status, COUNT(1) + FROM group_snd_item_statuses + WHERE chat_item_id = ? + GROUP BY group_snd_item_status + +Plan: +SEARCH group_snd_item_statuses USING INDEX idx_group_snd_item_statuses_chat_item_id_group_member_id (chat_item_id=?) +USE TEMP B-TREE FOR GROUP BY + +Query: + SELECT s.file_id, r.file_id, f.protocol + FROM files f + LEFT JOIN snd_files s ON s.file_id = f.file_id + LEFT JOIN rcv_files r ON r.file_id = f.file_id + WHERE user_id = ? AND f.file_id = ? + +Plan: +SEARCH f USING INTEGER PRIMARY KEY (rowid=?) +SEARCH s USING COVERING INDEX idx_snd_files_file_id (file_id=?) LEFT-JOIN +SEARCH r USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN + +Query: + UPDATE chat_items + SET item_content = ?, item_text = ?, item_status = ?, item_deleted = ?, item_deleted_ts = ?, item_edited = ?, item_live = ?, updated_at = ?, timed_ttl = ?, timed_delete_at = ? + WHERE user_id = ? AND contact_id = ? AND chat_item_id = ? + +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE chat_items + SET item_content = ?, item_text = ?, item_status = ?, item_deleted = ?, item_deleted_ts = ?, item_edited = ?, item_live = ?, updated_at = ?, timed_ttl = ?, timed_delete_at = ? + WHERE user_id = ? AND group_id = ? AND chat_item_id = ? + +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE chat_items + SET item_content = ?, item_text = ?, item_status = ?, item_deleted = ?, item_deleted_ts = ?, item_edited = ?, updated_at = ? + WHERE user_id = ? AND note_folder_id = ? AND chat_item_id = ? + +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE chat_items + SET item_deleted = ?, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND chat_item_id = ? + +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE chat_items + SET item_deleted = ?, item_deleted_ts = ?, updated_at = ? + WHERE user_id = ? AND contact_id = ? AND chat_item_id = ? + +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE chat_items + SET item_deleted = ?, item_deleted_ts = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND chat_item_id = ? + +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND contact_id = ? AND item_status = ? + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_contacts (user_id=? AND contact_id=? AND item_status=?) + +Query: + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND contact_id = ? AND item_status = ? AND chat_item_id = ? + +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND item_status = ? + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=? AND item_status=?) + +Query: + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND note_folder_id = ? AND item_status = ? + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_notes (user_id=? AND note_folder_id=? AND item_status=?) + +Query: + UPDATE commands + SET connection_id = ?, updated_at = ? + WHERE user_id = ? AND command_id = ? + +Plan: +SEARCH commands USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE connections + SET conn_chat_version = ?, peer_chat_min_version = ?, peer_chat_max_version = ? + WHERE connection_id = ? + +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE connections + SET conn_req_inv = ?, updated_at = ? + WHERE user_id = ? AND connection_id = ? + +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE connections + SET contact_conn_initiated = 0, updated_at = ? + WHERE user_id = ? AND connection_id = ? + +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE connections + SET custom_user_profile_id = ?, updated_at = ? + WHERE user_id = ? AND connection_id = ? + +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE connections + SET local_alias = ?, updated_at = ? + WHERE user_id = ? AND connection_id = ? + +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE connections + SET pq_rcv_enabled = ? + WHERE connection_id = ? + +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE connections + SET pq_snd_enabled = ? + WHERE connection_id = ? + +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE connections + SET pq_snd_enabled = ?, pq_rcv_enabled = ? + WHERE connection_id = ? + +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE connections + SET pq_support = ?, pq_encryption = ? + WHERE connection_id = ? + +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE contact_profiles + SET contact_link = ?, updated_at = ? + WHERE user_id = ? AND contact_profile_id = ? + +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE contact_profiles + SET display_name = ?, full_name = ?, image = ?, contact_link = ?, preferences = ?, updated_at = ? + WHERE user_id = ? AND contact_profile_id = ? + +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE contact_profiles + SET display_name = ?, full_name = ?, image = ?, contact_link = NULL, preferences = NULL, updated_at = ? + WHERE user_id = ? AND contact_profile_id = ? + +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE contact_profiles + SET display_name = ?, full_name = ?, image = ?, updated_at = ? + WHERE user_id = ? AND contact_profile_id = ? + +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE contact_profiles + SET local_alias = ?, updated_at = ? + WHERE user_id = ? AND contact_profile_id = ? + +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE contacts + SET contact_group_member_id = NULL, contact_grp_inv_sent = 0, updated_at = ? + WHERE contact_id = ? + +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE contacts + SET contact_status = ?, updated_at = ? + WHERE user_id = ? AND contact_id = ? + +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE group_member_intros + SET intro_status = ?, updated_at = ? + WHERE group_member_intro_id = ? + +Plan: +SEARCH group_member_intros USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE group_members + SET member_restriction = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND group_member_id = ? + +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE group_members + SET member_status = ?, + group_queue_info = ?, + direct_queue_info = ?, + updated_at = ? + WHERE group_member_id = ? + +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE group_members + SET member_status = ?, member_role = ?, updated_at = ? + WHERE user_id = ? AND group_member_id = ? + +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE group_members + SET member_status = ?, updated_at = ? + WHERE user_id = ? AND group_member_id = ? + +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE group_members + SET show_messages = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND group_member_id = ? + +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE group_profiles + SET preferences = ?, updated_at = ? + WHERE group_profile_id IN ( + SELECT group_profile_id + FROM groups + WHERE user_id = ? AND group_id = ? + ) + +Plan: +SEARCH group_profiles USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE group_snd_item_statuses + SET group_snd_item_status = ?, updated_at = ? + WHERE chat_item_id = ? AND group_member_id = ? + +Plan: +SEARCH group_snd_item_statuses USING INDEX idx_group_snd_item_statuses_chat_item_id_group_member_id (chat_item_id=? AND group_member_id=?) + +Query: + UPDATE group_snd_item_statuses + SET via_proxy = ? + WHERE chat_item_id = ? AND group_member_id = ? + +Plan: +SEARCH group_snd_item_statuses USING INDEX idx_group_snd_item_statuses_chat_item_id_group_member_id (chat_item_id=? AND group_member_id=?) + +Query: + UPDATE groups + SET via_group_link_uri_hash = (SELECT via_contact_uri_hash FROM connections WHERE connection_id = ?) + WHERE group_id = ? + +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +SCALAR SUBQUERY 1 +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE msg_deliveries + SET delivery_status = ?, updated_at = ? + WHERE connection_id = ? AND agent_msg_id = ? + +Plan: +SEARCH msg_deliveries USING INDEX idx_msg_deliveries_agent_msg_id (connection_id=? AND agent_msg_id=?) + +Query: + UPDATE protocol_servers + SET protocol = ?, host = ?, port = ?, key_hash = ?, basic_auth = ?, + preset = ?, tested = ?, enabled = ?, updated_at = ? + WHERE smp_server_id = ? + +Plan: +SEARCH protocol_servers USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE rcv_files + SET to_receive = 1, user_approved_relays = ?, updated_at = ? + WHERE file_id = ? + +Plan: +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE remote_controllers + SET ctrl_device_name = ?, dh_priv_key = ?, prev_dh_priv_key = dh_priv_key + WHERE remote_ctrl_id = ? + +Plan: +SEARCH remote_controllers USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE remote_hosts + SET host_device_name = ?, host_dh_pub = ?, bind_addr = ?, bind_iface = ?, bind_port = ? + WHERE remote_host_id = ? + +Plan: +SEARCH remote_hosts USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE server_operators + SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ? + WHERE server_operator_id = ? + +Plan: +SEARCH server_operators USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE users + SET view_pwd_hash = ?, view_pwd_salt = ?, show_ntfs = ? + WHERE user_id = ? + +Plan: +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE xftp_file_descriptions + SET file_descr_text = ?, file_descr_part_no = ?, file_descr_complete = ?, updated_at = ? + WHERE user_id = ? AND file_descr_id = ? + +Plan: +SEARCH xftp_file_descriptions USING INTEGER PRIMARY KEY (rowid=?) + +Query: + DELETE FROM messages + WHERE message_id IN ( + SELECT message_id + FROM chat_item_messages + WHERE chat_item_id = ? + ) + +Plan: +SEARCH messages USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_2 (chat_item_id=?) +SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_message_id (message_id=?) +SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_created_by_msg_id (created_by_msg_id=?) +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_created_by_msg_id (created_by_msg_id=?) +SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_1 (message_id=?) +SEARCH chat_items USING COVERING INDEX sqlite_autoindex_chat_items_1 (created_by_msg_id=?) +SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_message_id (message_id=?) + +Query: + SELECT + -- GroupInfo + g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + -- GroupMember - membership + mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, + mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, + pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at + FROM groups g + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + JOIN group_members mu ON mu.group_id = g.group_id + JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) + WHERE g.business_xcontact_id = ? AND g.user_id = ? AND mu.contact_id = ? +Plan: +SEARCH mu USING INDEX idx_group_members_contact_id (contact_id=?) +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) +SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) +SEARCH pu USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT + -- GroupInfo + g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + -- GroupMember - membership + mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, + mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, + pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at + FROM groups g + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + JOIN group_members mu ON mu.group_id = g.group_id + JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) + WHERE g.group_id = ? AND g.user_id = ? AND mu.contact_id = ? +Plan: +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) +SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) +SEARCH mu USING INDEX idx_group_members_contact_id (contact_id=?) +SEARCH pu USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT + m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN connections c ON c.connection_id = ( + SELECT max(cc.connection_id) + FROM connections cc + WHERE cc.user_id = ? AND cc.group_member_id = m.group_member_id + ) + + WHERE m.group_id = ? AND m.user_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) + AND m.member_status IN (?, ?, ?, ?) + AND m.group_member_id NOT IN ( + SELECT DISTINCT group_member_id FROM chat_items + ) + +Plan: +SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) +LIST SUBQUERY 2 +SCAN chat_items USING COVERING INDEX idx_chat_items_group_member_id +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?) + +Query: + SELECT + m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN connections c ON c.connection_id = ( + SELECT max(cc.connection_id) + FROM connections cc + WHERE cc.user_id = ? AND cc.group_member_id = m.group_member_id + ) + WHERE m.group_id = ? AND m.group_member_id = ? AND m.user_id = ? +Plan: +SEARCH m USING INTEGER PRIMARY KEY (rowid=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?) + +Query: + SELECT + m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN connections c ON c.connection_id = ( + SELECT max(cc.connection_id) + FROM connections cc + WHERE cc.user_id = ? AND cc.group_member_id = m.group_member_id + ) + WHERE m.group_id = ? AND m.member_id = ? +Plan: +SEARCH m USING INDEX sqlite_autoindex_group_members_1 (group_id=? AND member_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?) + +Query: + SELECT + m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN connections c ON c.connection_id = ( + SELECT max(cc.connection_id) + FROM connections cc + WHERE cc.user_id = ? AND cc.group_member_id = m.group_member_id + ) + WHERE m.group_member_id = ? AND m.user_id = ? +Plan: +SEARCH m USING INTEGER PRIMARY KEY (rowid=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?) + +Query: + SELECT + m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN connections c ON c.connection_id = ( + SELECT max(cc.connection_id) + FROM connections cc + WHERE cc.user_id = ? AND cc.group_member_id = m.group_member_id + ) + WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) +Plan: +SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?) + +Query: + SELECT + m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, + m.created_at, m.updated_at, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN connections c ON c.connection_id = ( + SELECT max(cc.connection_id) + FROM connections cc + WHERE cc.user_id = ? AND cc.group_member_id = m.group_member_id + ) + WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?,?) +Plan: +SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +CORRELATED SCALAR SUBQUERY 1 +SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?) + +Query: + SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content + FROM user_contact_links + WHERE user_id = ? AND conn_req_contact IN (?,?) +Plan: +SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) + +Query: + SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content + FROM user_contact_links + WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL +Plan: +SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) + +Query: + SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content + FROM user_contact_links + WHERE user_id = ? AND short_link_contact = ? +Plan: +SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) + +Query: + SELECT f.file_id, f.ci_file_status, f.file_path + FROM chat_items i + JOIN files f ON f.chat_item_id = i.chat_item_id + WHERE i.user_id = ? +Plan: +SEARCH i USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=?) +SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) + +Query: + SELECT f.file_id, f.ci_file_status, f.file_path + FROM chat_items i + JOIN files f ON f.chat_item_id = i.chat_item_id + WHERE i.user_id = ? AND i.contact_id = ? +Plan: +SEARCH i USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) +SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) + +Query: + SELECT f.file_id, f.ci_file_status, f.file_path + FROM chat_items i + JOIN files f ON f.chat_item_id = i.chat_item_id + WHERE i.user_id = ? AND i.contact_id = ? AND i.created_at <= ? +Plan: +SEARCH i USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=? AND created_at<?) +SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) + +Query: + SELECT f.file_id, f.ci_file_status, f.file_path + FROM chat_items i + JOIN files f ON f.chat_item_id = i.chat_item_id + WHERE i.user_id = ? AND i.group_id = ? +Plan: +SEARCH i USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=?) +SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) + +Query: + SELECT f.file_id, f.ci_file_status, f.file_path + FROM chat_items i + JOIN files f ON f.chat_item_id = i.chat_item_id + WHERE i.user_id = ? AND i.group_id = ? AND i.group_member_id = ? +Plan: +SEARCH i USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=?) +SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) + +Query: + SELECT f.file_id, f.ci_file_status, f.file_path + FROM chat_items i + JOIN files f ON f.chat_item_id = i.chat_item_id + WHERE i.user_id = ? AND i.note_folder_id = ? +Plan: +SEARCH i USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) +SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) + +Query: + SELECT m.group_member_id, m.member_id, m.member_role, p.display_name, p.local_alias + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + WHERE m.group_id = ? AND m.group_member_id = ? AND m.user_id = ? +Plan: +SEARCH m USING INTEGER PRIMARY KEY (rowid=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT m.group_member_id, m.member_id, m.member_role, p.display_name, p.local_alias + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + WHERE m.group_id = ? AND m.member_id = ? AND m.user_id = ? +Plan: +SEARCH m USING INDEX sqlite_autoindex_group_members_1 (group_id=? AND member_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT remote_ctrl_id, ctrl_device_name, ca_key, ca_cert, ctrl_fingerprint, id_pub, dh_priv_key, prev_dh_priv_key + FROM remote_controllers + +Plan: +SCAN remote_controllers + +Query: + SELECT remote_ctrl_id, ctrl_device_name, ca_key, ca_cert, ctrl_fingerprint, id_pub, dh_priv_key, prev_dh_priv_key + FROM remote_controllers + WHERE ctrl_fingerprint = ? +Plan: +SEARCH remote_controllers USING INDEX idx_remote_controllers_ctrl_fingerprint (ctrl_fingerprint=?) + +Query: + SELECT remote_ctrl_id, ctrl_device_name, ca_key, ca_cert, ctrl_fingerprint, id_pub, dh_priv_key, prev_dh_priv_key + FROM remote_controllers + WHERE remote_ctrl_id = ? +Plan: +SEARCH remote_controllers USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT remote_host_id, host_device_name, store_path, ca_key, ca_cert, id_key, host_fingerprint, host_dh_pub, bind_iface, bind_addr, bind_port + FROM remote_hosts + +Plan: +SCAN remote_hosts + +Query: + SELECT remote_host_id, host_device_name, store_path, ca_key, ca_cert, id_key, host_fingerprint, host_dh_pub, bind_iface, bind_addr, bind_port + FROM remote_hosts + WHERE remote_host_id = ? +Plan: +SEARCH remote_hosts USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT server_operator_id, server_operator_tag, trade_name, legal_name, + server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy + FROM server_operators + +Plan: +SCAN server_operators + +Query: + SELECT server_operator_id, server_operator_tag, trade_name, legal_name, + server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy + FROM server_operators + WHERE server_operator_id = ? +Plan: +SEARCH server_operators USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + FROM users u + JOIN contacts uct ON uct.contact_id = u.contact_id + JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id + +Plan: +SCAN u +SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + FROM users u + JOIN contacts uct ON uct.contact_id = u.contact_id + JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id + JOIN connections c ON c.user_id = u.user_id WHERE c.agent_conn_id = ? +Plan: +SEARCH c USING INDEX sqlite_autoindex_connections_1 (agent_conn_id=?) +SEARCH u USING INTEGER PRIMARY KEY (rowid=?) +SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + FROM users u + JOIN contacts uct ON uct.contact_id = u.contact_id + JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id + JOIN contact_requests cr ON cr.user_id = u.user_id WHERE cr.contact_request_id = ? +Plan: +SEARCH cr USING INTEGER PRIMARY KEY (rowid=?) +SEARCH u USING INTEGER PRIMARY KEY (rowid=?) +SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + FROM users u + JOIN contacts uct ON uct.contact_id = u.contact_id + JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id + JOIN contacts ct ON ct.user_id = u.user_id WHERE ct.contact_id = ? AND ct.deleted = 0 +Plan: +SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH u USING INTEGER PRIMARY KEY (rowid=?) +SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + FROM users u + JOIN contacts uct ON uct.contact_id = u.contact_id + JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id + JOIN files f ON f.user_id = u.user_id JOIN rcv_files r ON r.file_id = f.file_id WHERE r.agent_rcv_file_id = ? +Plan: +SCAN r +SEARCH f USING INTEGER PRIMARY KEY (rowid=?) +SEARCH u USING INTEGER PRIMARY KEY (rowid=?) +SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + FROM users u + JOIN contacts uct ON uct.contact_id = u.contact_id + JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id + JOIN files f ON f.user_id = u.user_id WHERE f.agent_snd_file_id = ? +Plan: +SCAN f +SEARCH u USING INTEGER PRIMARY KEY (rowid=?) +SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + FROM users u + JOIN contacts uct ON uct.contact_id = u.contact_id + JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id + JOIN files f ON f.user_id = u.user_id WHERE f.file_id = ? +Plan: +SEARCH f USING INTEGER PRIMARY KEY (rowid=?) +SEARCH u USING INTEGER PRIMARY KEY (rowid=?) +SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + FROM users u + JOIN contacts uct ON uct.contact_id = u.contact_id + JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id + JOIN groups g ON g.user_id = u.user_id WHERE g.group_id = ? +Plan: +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) +SEARCH u USING INTEGER PRIMARY KEY (rowid=?) +SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + FROM users u + JOIN contacts uct ON uct.contact_id = u.contact_id + JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id + JOIN note_folders nf ON nf.user_id = u.user_id WHERE nf.note_folder_id = ? +Plan: +SEARCH nf USING INTEGER PRIMARY KEY (rowid=?) +SEARCH u USING INTEGER PRIMARY KEY (rowid=?) +SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + FROM users u + JOIN contacts uct ON uct.contact_id = u.contact_id + JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id + WHERE u.user_id = ? +Plan: +SEARCH u USING INTEGER PRIMARY KEY (rowid=?) +SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT usage_conditions_id, conditions_commit, notified_at, created_at + FROM usage_conditions + ORDER BY usage_conditions_id + +Plan: +SCAN usage_conditions + +Query: + SELECT usage_conditions_id, conditions_commit, notified_at, created_at + FROM usage_conditions + ORDER BY usage_conditions_id + DESC LIMIT 1 +Plan: +SCAN usage_conditions + +Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND group_id = ? AND item_ts < ? ) OR ( user_id = ? AND group_id = ? AND item_ts = ? AND chat_item_id < ? )) ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? +Plan: +MULTI-INDEX OR +INDEX 1 +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=? AND item_ts<?) +INDEX 2 +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=? AND item_ts=? AND rowid<?) +USE TEMP B-TREE FOR ORDER BY + +Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND group_id = ? AND item_ts > ? ) OR ( user_id = ? AND group_id = ? AND item_ts = ? AND chat_item_id > ? )) ORDER BY item_ts ASC, chat_item_id ASC LIMIT ? +Plan: +MULTI-INDEX OR +INDEX 1 +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=? AND item_ts>?) +INDEX 2 +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=? AND item_ts=? AND rowid>?) +USE TEMP B-TREE FOR ORDER BY + +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? +Plan: +SEARCH chat_items USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) + +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND msg_content_tag = ? ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_item_ts (user_id=? AND group_id=? AND msg_content_tag=?) + +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) + +Query: CREATE TABLE temp_conn_ids (conn_id BLOB) +Error: SQLite3 returned ErrorError while attempting to perform prepare "explain query plan CREATE TABLE temp_conn_ids (conn_id BLOB)": table temp_conn_ids already exists + +Query: CREATE TABLE temp_delete_members (contact_profile_id INTEGER, member_profile_id INTEGER, local_display_name TEXT) +Error: SQLite3 returned ErrorError while attempting to perform prepare "explain query plan CREATE TABLE temp_delete_members (contact_profile_id INTEGER, member_profile_id INTEGER, local_display_name TEXT)": table temp_delete_members already exists + +Query: DELETE FROM app_settings +Plan: + +Query: DELETE FROM calls WHERE user_id = ? AND contact_id = ? +Plan: +SEARCH calls USING INDEX idx_calls_contact_id (contact_id=?) + +Query: DELETE FROM chat_item_mentions WHERE chat_item_id = ? +Plan: +SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?) + +Query: DELETE FROM chat_item_moderations WHERE group_id = ? AND item_member_id = ? AND shared_msg_id = ? +Plan: +SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_group (group_id=? AND item_member_id=? AND shared_msg_id=?) + +Query: DELETE FROM chat_item_reactions WHERE contact_id = ? +Plan: +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_contact_id (contact_id=?) + +Query: DELETE FROM chat_item_reactions WHERE contact_id = ? AND created_at <= ? +Plan: +SEARCH chat_item_reactions USING INDEX idx_chat_item_reactions_contact_id (contact_id=?) + +Query: DELETE FROM chat_item_reactions WHERE contact_id = ? AND shared_msg_id = ? +Plan: +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_contact (contact_id=? AND shared_msg_id=?) + +Query: DELETE FROM chat_item_reactions WHERE group_id = ? +Plan: +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_id (group_id=?) + +Query: DELETE FROM chat_item_reactions WHERE group_id = ? AND shared_msg_id = ? AND item_member_id = ? +Plan: +SEARCH chat_item_reactions USING INDEX idx_chat_item_reactions_group (group_id=? AND shared_msg_id=?) + +Query: DELETE FROM chat_item_versions WHERE chat_item_id = ? +Plan: +SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) + +Query: DELETE FROM chat_items WHERE user_id = ? AND contact_id = ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) +SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?) +SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) +SEARCH calls USING COVERING INDEX idx_calls_chat_item_id (chat_item_id=?) +SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_2 (chat_item_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_fwd_from_chat_item_id (fwd_from_chat_item_id=?) +SEARCH files USING COVERING INDEX idx_files_chat_item_id (chat_item_id=?) +SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?) + +Query: DELETE FROM chat_items WHERE user_id = ? AND contact_id = ? AND created_at <= ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=? AND created_at<?) +SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?) +SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) +SEARCH calls USING COVERING INDEX idx_calls_chat_item_id (chat_item_id=?) +SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_2 (chat_item_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_fwd_from_chat_item_id (fwd_from_chat_item_id=?) +SEARCH files USING COVERING INDEX idx_files_chat_item_id (chat_item_id=?) +SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?) + +Query: DELETE FROM chat_items WHERE user_id = ? AND group_id = ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=?) +SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?) +SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) +SEARCH calls USING COVERING INDEX idx_calls_chat_item_id (chat_item_id=?) +SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_2 (chat_item_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_fwd_from_chat_item_id (fwd_from_chat_item_id=?) +SEARCH files USING COVERING INDEX idx_files_chat_item_id (chat_item_id=?) +SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?) + +Query: DELETE FROM chat_items WHERE user_id = ? AND group_id = ? AND group_member_id = ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=?) +SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?) +SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) +SEARCH calls USING COVERING INDEX idx_calls_chat_item_id (chat_item_id=?) +SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_2 (chat_item_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_fwd_from_chat_item_id (fwd_from_chat_item_id=?) +SEARCH files USING COVERING INDEX idx_files_chat_item_id (chat_item_id=?) +SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?) + +Query: DELETE FROM chat_items WHERE user_id = ? AND note_folder_id = ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) +SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?) +SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) +SEARCH calls USING COVERING INDEX idx_calls_chat_item_id (chat_item_id=?) +SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_2 (chat_item_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_fwd_from_chat_item_id (fwd_from_chat_item_id=?) +SEARCH files USING COVERING INDEX idx_files_chat_item_id (chat_item_id=?) +SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?) + +Query: DELETE FROM commands WHERE user_id = ? AND command_id = ? +Plan: +SEARCH commands USING INTEGER PRIMARY KEY (rowid=?) + +Query: DELETE FROM connections WHERE user_id = ? AND connection_id = ? +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) +SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_agent_msg_id (connection_id=?) +SEARCH commands USING COVERING INDEX idx_commands_connection_id (connection_id=?) +SEARCH messages USING COVERING INDEX idx_messages_connection_id (connection_id=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_connection_id (connection_id=?) + +Query: DELETE FROM connections WHERE user_id = ? AND group_member_id = ? +Plan: +SEARCH connections USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?) +SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_agent_msg_id (connection_id=?) +SEARCH commands USING COVERING INDEX idx_commands_connection_id (connection_id=?) +SEARCH messages USING COVERING INDEX idx_messages_connection_id (connection_id=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_connection_id (connection_id=?) + +Query: DELETE FROM contact_profiles WHERE user_id = ? AND contact_profile_id = ? +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) +SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) +SEARCH connections USING COVERING INDEX idx_connections_custom_user_profile_id (custom_user_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_member_profile_id (member_profile_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_contact_profile_id (contact_profile_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_contact_profile_id (contact_profile_id=?) + +Query: DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ? +Plan: +SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) + +Query: DELETE FROM contacts WHERE user_id = ? AND contact_id = ? +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) +SEARCH chat_tags_chats USING COVERING INDEX idx_chat_tags_chats_chat_tag_id_contact_id (contact_id=?) +SEARCH received_probes USING COVERING INDEX idx_received_probes_contact_id (contact_id=?) +SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_contact_id (contact_id=?) +SEARCH sent_probes USING COVERING INDEX idx_sent_probes_contact_id (contact_id=?) +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_contact_id (contact_id=?) +SEARCH calls USING COVERING INDEX idx_calls_contact_id (contact_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_fwd_from_contact_id (fwd_from_contact_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_contact_id (contact_id=?) +SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_id (contact_id=?) +SEARCH connections USING COVERING INDEX idx_connections_contact_id (contact_id=?) +SEARCH connections USING COVERING INDEX idx_connections_via_contact (via_contact=?) +SEARCH files USING COVERING INDEX idx_files_contact_id (contact_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_contact_id (contact_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_invited_by (invited_by=?) +SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?) + +Query: DELETE FROM display_names WHERE user_id = ? AND local_display_name = ? +Plan: +SEARCH display_names USING PRIMARY KEY (user_id=? AND local_display_name=?) +SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_1 (user_id=? AND local_display_name=?) +SEARCH group_members USING COVERING INDEX idx_group_members_user_id_local_display_name (user_id=? AND local_display_name=?) +SEARCH groups USING COVERING INDEX sqlite_autoindex_groups_1 (user_id=? AND local_display_name=?) +SEARCH contacts USING COVERING INDEX sqlite_autoindex_contacts_1 (user_id=? AND local_display_name=?) +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + +Query: DELETE FROM files WHERE user_id = ? AND contact_id = ? +Plan: +SEARCH files USING INDEX idx_files_contact_id (contact_id=?) +SEARCH extra_xftp_file_descriptions USING COVERING INDEX idx_extra_xftp_file_descriptions_file_id (file_id=?) +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_file_id (file_id=?) +SEARCH files USING COVERING INDEX idx_files_redirect_file_id (redirect_file_id=?) + +Query: DELETE FROM group_members WHERE user_id = ? AND group_id = ? +Plan: +SEARCH group_members USING COVERING INDEX idx_group_members_group_id (user_id=? AND group_id=?) +SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?) +SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?) +SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) +SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?) +SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?) +SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?) +SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?) +SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?) +SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?) +SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?) +SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?) + +Query: DELETE FROM group_members WHERE user_id = ? AND group_member_id = ? +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?) +SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?) +SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) +SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?) +SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?) +SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?) +SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?) +SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?) +SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?) +SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?) +SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?) + +Query: DELETE FROM groups WHERE user_id = ? AND group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_group_id (group_id=?) +SEARCH chat_tags_chats USING COVERING INDEX idx_chat_tags_chats_chat_tag_id_group_id (group_id=?) +SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_group_id (group_id=?) +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_id (group_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_fwd_from_group_id (fwd_from_group_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_id (group_id=?) +SEARCH messages USING COVERING INDEX idx_messages_group_id (group_id=?) +SEARCH user_contact_links USING COVERING INDEX idx_user_contact_links_group_id (group_id=?) +SEARCH files USING COVERING INDEX idx_files_group_id (group_id=?) +SEARCH group_members USING COVERING INDEX sqlite_autoindex_group_members_1 (group_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_via_group (via_group=?) + +Query: DELETE FROM messages WHERE connection_id = ? +Plan: +SEARCH messages USING COVERING INDEX idx_messages_connection_id (connection_id=?) +SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_message_id (message_id=?) +SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_created_by_msg_id (created_by_msg_id=?) +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_created_by_msg_id (created_by_msg_id=?) +SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_1 (message_id=?) +SEARCH chat_items USING COVERING INDEX sqlite_autoindex_chat_items_1 (created_by_msg_id=?) +SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_message_id (message_id=?) + +Query: DELETE FROM messages WHERE connection_id = ? AND created_at <= ? +Plan: +SEARCH messages USING INDEX idx_messages_connection_id (connection_id=?) +SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_message_id (message_id=?) +SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_created_by_msg_id (created_by_msg_id=?) +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_created_by_msg_id (created_by_msg_id=?) +SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_1 (message_id=?) +SEARCH chat_items USING COVERING INDEX sqlite_autoindex_chat_items_1 (created_by_msg_id=?) +SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_message_id (message_id=?) + +Query: DELETE FROM messages WHERE created_at <= ? +Plan: +SEARCH messages USING COVERING INDEX idx_messages_created_at (created_at<?) +SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_message_id (message_id=?) +SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_created_by_msg_id (created_by_msg_id=?) +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_created_by_msg_id (created_by_msg_id=?) +SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_1 (message_id=?) +SEARCH chat_items USING COVERING INDEX sqlite_autoindex_chat_items_1 (created_by_msg_id=?) +SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_message_id (message_id=?) + +Query: DELETE FROM messages WHERE group_id = ? +Plan: +SEARCH messages USING COVERING INDEX idx_messages_group_id (group_id=?) +SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_message_id (message_id=?) +SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_created_by_msg_id (created_by_msg_id=?) +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_created_by_msg_id (created_by_msg_id=?) +SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_1 (message_id=?) +SEARCH chat_items USING COVERING INDEX sqlite_autoindex_chat_items_1 (created_by_msg_id=?) +SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_message_id (message_id=?) + +Query: DELETE FROM pending_group_messages WHERE group_member_id = ? AND message_id = ? +Plan: +SEARCH pending_group_messages USING INDEX idx_pending_group_messages_group_member_id (group_member_id=?) + +Query: DELETE FROM protocol_servers WHERE user_id = ? AND smp_server_id = ? AND preset = ? +Plan: +SEARCH protocol_servers USING INTEGER PRIMARY KEY (rowid=?) + +Query: DELETE FROM rcv_file_chunks WHERE file_id = ? +Plan: +SEARCH rcv_file_chunks USING COVERING INDEX idx_rcv_file_chunks_file_id (file_id=?) + +Query: DELETE FROM received_probes WHERE created_at <= ? +Plan: +SEARCH received_probes USING COVERING INDEX idx_received_probes_created_at (created_at<?) + +Query: DELETE FROM remote_controllers WHERE remote_ctrl_id = ? +Plan: +SEARCH remote_controllers USING INTEGER PRIMARY KEY (rowid=?) + +Query: DELETE FROM remote_hosts WHERE remote_host_id = ? +Plan: +SEARCH remote_hosts USING INTEGER PRIMARY KEY (rowid=?) + +Query: DELETE FROM sent_probe_hashes WHERE created_at <= ? +Plan: +SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_created_at (created_at<?) + +Query: DELETE FROM sent_probes WHERE created_at <= ? +Plan: +SEARCH sent_probes USING COVERING INDEX idx_sent_probes_created_at (created_at<?) +SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_sent_probe_id (sent_probe_id=?) + +Query: DELETE FROM snd_file_chunks WHERE file_id = ? AND connection_id = ? +Plan: +SEARCH snd_file_chunks USING COVERING INDEX idx_snd_file_chunks_file_id_connection_id (file_id=? AND connection_id=?) + +Query: DELETE FROM user_contact_links WHERE user_id = ? AND group_id = ? +Plan: +SEARCH user_contact_links USING INDEX idx_user_contact_links_group_id (group_id=?) +SEARCH contact_requests USING COVERING INDEX idx_contact_requests_user_contact_link_id (user_contact_link_id=?) +SEARCH connections USING COVERING INDEX idx_connections_via_user_contact_link (via_user_contact_link=?) +SEARCH connections USING COVERING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) + +Query: DELETE FROM user_contact_links WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL +Plan: +SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) +SEARCH contact_requests USING COVERING INDEX idx_contact_requests_user_contact_link_id (user_contact_link_id=?) +SEARCH connections USING COVERING INDEX idx_connections_via_user_contact_link (via_user_contact_link=?) +SEARCH connections USING COVERING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) + +Query: DELETE FROM users WHERE user_id = ? +Plan: +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) +SEARCH chat_tags USING COVERING INDEX idx_chat_tags_user_id (user_id=?) +SEARCH note_folders USING COVERING INDEX note_folders_user_id (user_id=?) +SEARCH received_probes USING COVERING INDEX idx_received_probes_user_id (user_id=?) +SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_user_id (user_id=?) +SEARCH sent_probes USING COVERING INDEX idx_sent_probes_user_id (user_id=?) +SEARCH extra_xftp_file_descriptions USING COVERING INDEX idx_extra_xftp_file_descriptions_user_id (user_id=?) +SEARCH xftp_file_descriptions USING COVERING INDEX idx_xftp_file_descriptions_user_id (user_id=?) +SEARCH protocol_servers USING COVERING INDEX idx_smp_servers_user_id (user_id=?) +SEARCH settings USING COVERING INDEX idx_settings_user_id (user_id=?) +SEARCH commands USING COVERING INDEX idx_commands_user_id (user_id=?) +SEARCH calls USING COVERING INDEX idx_calls_user_id (user_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=?) +SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_2 (user_id=?) +SEARCH user_contact_links USING COVERING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) +SEARCH connections USING COVERING INDEX idx_connections_group_member (user_id=?) +SEARCH files USING COVERING INDEX idx_files_user_id (user_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_user_id (user_id=?) +SEARCH groups USING COVERING INDEX sqlite_autoindex_groups_2 (user_id=?) +SEARCH group_profiles USING COVERING INDEX idx_group_profiles_user_id (user_id=?) +SEARCH known_servers USING COVERING INDEX sqlite_autoindex_known_servers_1 (user_id=?) +SEARCH contacts USING COVERING INDEX sqlite_autoindex_contacts_2 (user_id=?) +SEARCH display_names USING COVERING INDEX sqlite_autoindex_display_names_2 (user_id=?) +SEARCH contact_profiles USING COVERING INDEX idx_contact_profiles_user_id (user_id=?) + +Query: DROP TABLE IF EXISTS temp_conn_ids +Plan: + +Query: DROP TABLE IF EXISTS temp_delete_members +Plan: + +Query: DROP TABLE temp_conn_ids +Plan: + +Query: DROP TABLE temp_delete_members +Plan: + +Query: INSERT INTO app_settings (app_settings) VALUES (?) +Plan: + +Query: INSERT INTO chat_item_mentions (chat_item_id, group_id, member_id, display_name) VALUES (?, ?, ?, ?) +Plan: + +Query: INSERT INTO chat_item_messages (chat_item_id, message_id, created_at, updated_at) VALUES (?,?,?,?) +Plan: + +Query: INSERT INTO contact_profiles (display_name, full_name, image, contact_link, user_id, local_alias, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?) +Plan: +SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) + +Query: INSERT INTO contact_profiles (display_name, full_name, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?) +Plan: +SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) + +Query: INSERT INTO contact_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?) +Plan: +SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) + +Query: INSERT INTO contacts (contact_profile_id, local_display_name, user_id, is_user, created_at, updated_at, chat_ts) VALUES (?,?,?,?,?,?,?) +Plan: +SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?) + +Query: INSERT INTO contacts (contact_profile_id, local_display_name, user_id, via_group, created_at, updated_at, chat_ts, contact_used) VALUES (?,?,?,?,?,?,?,?) +Plan: +SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?) + +Query: INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, chat_ts, xcontact_id, contact_used) VALUES (?,?,?,?,?,?,?,?,?,?) +Plan: +SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?) + +Query: INSERT INTO display_names (local_display_name, ldn_base, user_id, created_at, updated_at) VALUES (?,?,?,?,?) +Plan: +SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_1 (user_id=? AND local_display_name=?) +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + +Query: INSERT INTO extra_xftp_file_descriptions (file_id, user_id, file_descr_text, created_at, updated_at) VALUES (?,?,?,?,?) +Plan: + +Query: INSERT INTO files (contact_id, group_id, user_id, file_name, file_path, file_crypto_key, file_crypto_nonce, file_size, chunk_size, redirect_file_id, agent_snd_file_id, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) +Plan: + +Query: INSERT INTO files (user_id, contact_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?) +Plan: + +Query: INSERT INTO files (user_id, file_name, file_path, file_size, chunk_size, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?) +Plan: + +Query: INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?) +Plan: + +Query: INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, member_admission, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?) +Plan: + +Query: INSERT INTO group_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?) +Plan: + +Query: INSERT INTO group_snd_item_statuses (chat_item_id, group_member_id, group_snd_item_status) VALUES (?,?,?) +Plan: + +Query: INSERT INTO note_folders (user_id) VALUES (?) +Plan: + +Query: INSERT INTO rcv_files (file_id, file_status, created_at, updated_at) VALUES (?,?,?,?) +Plan: + +Query: INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, file_descr_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?) +Plan: + +Query: INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, group_member_id, file_descr_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?) +Plan: + +Query: INSERT INTO received_probes (contact_id, group_member_id, probe, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?) +Plan: + +Query: INSERT INTO received_probes (contact_id, group_member_id, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?) +Plan: + +Query: INSERT INTO sent_probe_hashes (sent_probe_id, contact_id, group_member_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?) +Plan: + +Query: INSERT INTO sent_probes (contact_id, group_member_id, probe, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?) +Plan: + +Query: INSERT INTO settings (user_id, chat_item_ttl, created_at, updated_at) VALUES (?,?,?,?) +Plan: + +Query: INSERT INTO snd_files (file_id, file_status, file_descr_id, group_member_id, connection_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?) +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: INSERT INTO temp_conn_ids (conn_id) VALUES (?) +Plan: + +Query: INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, created_at, updated_at) VALUES (?,?,?,?,?) +Plan: + +Query: INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, short_link_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?) +Plan: + +Query: INSERT INTO users (agent_user_id, local_display_name, active_user, active_order, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, created_at, updated_at) VALUES (?,?,?,?,0,?,?,?,?,?) +Plan: + +Query: INSERT INTO xftp_file_descriptions (user_id, file_descr_text, file_descr_part_no, file_descr_complete, created_at, updated_at) VALUES (?,?,?,?,?,?) +Plan: + +Query: SELECT 1 FROM group_member_intros WHERE re_group_member_id = ? AND to_group_member_id = ? LIMIT 1 +Plan: +SEARCH group_member_intros USING COVERING INDEX sqlite_autoindex_group_member_intros_1 (re_group_member_id=? AND to_group_member_id=?) + +Query: SELECT 1 FROM settings WHERE user_id = ? LIMIT 1 +Plan: +SEARCH settings USING COVERING INDEX idx_settings_user_id (user_id=?) + +Query: SELECT COUNT(1) FROM chat_item_versions WHERE chat_item_id = ? +Plan: +SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) + +Query: SELECT COUNT(1) FROM chat_items WHERE user_id = ? AND group_id = ? AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0 +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id=? AND msg_content_tag=? AND item_deleted=? AND item_sent=?) + +Query: SELECT COUNT(1) FROM contacts WHERE user_id = ? AND chat_item_ttl > 0 +Plan: +SEARCH contacts USING INDEX idx_contacts_chat_ts (user_id=?) + +Query: SELECT COUNT(1) FROM groups WHERE user_id = ? AND chat_item_ttl > 0 +Plan: +SEARCH groups USING INDEX idx_groups_chat_ts (user_id=?) + +Query: SELECT COUNT(1), COALESCE(SUM(user_mention), 0) FROM chat_items WHERE user_id = ? AND group_id = ? AND item_status = ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=? AND item_status=?) + +Query: SELECT accepted_at FROM operator_usage_conditions WHERE server_operator_id = ? AND conditions_commit = ? +Plan: +SEARCH operator_usage_conditions USING INDEX idx_operator_usage_conditions_conditions_commit (conditions_commit=? AND server_operator_id=?) + +Query: SELECT agent_conn_id FROM connections WHERE user_id = ? AND conn_req_inv IN (?,?) LIMIT 1 +Plan: +SEARCH connections USING INDEX idx_connections_conn_req_inv (user_id=? AND conn_req_inv=?) + +Query: SELECT agent_conn_id FROM connections where to_subscribe = 1 +Plan: +SEARCH connections USING INDEX idx_connections_to_subscribe (to_subscribe=?) + +Query: SELECT app_settings FROM app_settings +Plan: +SCAN app_settings + +Query: SELECT auth_err_counter FROM connections WHERE user_id = ? AND connection_id = ? +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND contact_id = ? AND shared_msg_id = ? AND item_sent = ? +Plan: +SEARCH chat_items USING INDEX idx_chat_items_direct_shared_msg_id (user_id=? AND contact_id=? AND shared_msg_id=?) + +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND item_status = ? ORDER BY item_ts ASC, chat_item_id ASC LIMIT 1 +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_groups (user_id=? AND group_id=? AND item_status=?) + +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND group_member_id = ? LIMIT 1 +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=?) + +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND shared_msg_id = ? AND item_sent = ? AND group_member_id = ? +Plan: +SEARCH chat_items USING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=? AND shared_msg_id=?) + +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND shared_msg_id = ? AND item_sent = ? AND group_member_id IS NULL +Plan: +SEARCH chat_items USING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=? AND shared_msg_id=?) + +Query: SELECT chat_item_ttl FROM contacts WHERE contact_id = ? LIMIT 1 +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + +Query: SELECT chat_item_ttl FROM settings WHERE user_id = ? LIMIT 1 +Plan: +SEARCH settings USING INDEX idx_settings_user_id (user_id=?) + +Query: SELECT chat_tag_id FROM chat_tags_chats WHERE contact_id = ? +Plan: +SEARCH chat_tags_chats USING COVERING INDEX idx_chat_tags_chats_chat_tag_id_contact_id (contact_id=?) + +Query: SELECT chat_tag_id FROM chat_tags_chats WHERE group_id = ? +Plan: +SEARCH chat_tags_chats USING COVERING INDEX idx_chat_tags_chats_chat_tag_id_group_id (group_id=?) + +Query: SELECT conn_req_inv FROM connections WHERE connection_id = ? +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: SELECT conn_status FROM connections WHERE connection_id = ? +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: SELECT connection_id FROM connections WHERE user_id = ? AND contact_id = ? +Plan: +SEARCH connections USING INDEX idx_connections_contact_id (contact_id=?) + +Query: SELECT connection_id FROM connections WHERE user_id = ? AND group_member_id = ? +Plan: +SEARCH connections USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?) + +Query: SELECT contact_id FROM contacts WHERE user_id = ? AND chat_item_ttl > 0 OR chat_item_ttl IS NULL +Plan: +SCAN contacts + +Query: SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 0 +Plan: +SEARCH contacts USING INDEX idx_contacts_chat_ts (user_id=?) + +Query: SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 1 +Plan: +SEARCH contacts USING INDEX idx_contacts_chat_ts (user_id=?) + +Query: SELECT contact_id FROM contacts WHERE user_id = ? AND local_display_name = ? AND deleted = 0 +Plan: +SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (user_id=? AND local_display_name=?) + +Query: SELECT contact_id FROM group_members WHERE contact_id = ? LIMIT 1 +Plan: +SEARCH group_members USING COVERING INDEX idx_group_members_contact_id (contact_id=?) + +Query: SELECT contact_id, group_id FROM chat_items WHERE user_id = ? AND chat_item_id = ? +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: SELECT contact_request_id FROM contact_requests WHERE user_id = ? AND local_display_name = ? +Plan: +SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_1 (user_id=? AND local_display_name=?) + +Query: SELECT count(1) FROM chat_items WHERE chat_item_id > ? +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid>?) + +Query: SELECT count(1) FROM group_members +Plan: +SCAN group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id + +Query: SELECT count(1) FROM pending_group_messages +Plan: +SCAN pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id + +Query: SELECT file_id FROM files WHERE user_id = ? AND redirect_file_id = ? +Plan: +SEARCH files USING INDEX idx_files_redirect_file_id (redirect_file_id=?) + +Query: SELECT g.inv_queue_info FROM groups g WHERE g.group_id = ? AND g.user_id = ? +Plan: +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) + +Query: SELECT group_id FROM group_members WHERE group_member_id = ? +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: SELECT group_id FROM groups WHERE inv_queue_info = ? AND user_id = ? LIMIT 1 +Plan: +SEARCH groups USING INDEX idx_groups_chat_ts (user_id=?) + +Query: SELECT group_id FROM groups WHERE user_id = ? +Plan: +SEARCH groups USING COVERING INDEX sqlite_autoindex_groups_2 (user_id=?) + +Query: SELECT group_id FROM groups WHERE user_id = ? AND chat_item_ttl > 0 OR chat_item_ttl IS NULL +Plan: +SCAN groups + +Query: SELECT group_id FROM groups WHERE user_id = ? AND local_display_name = ? +Plan: +SEARCH groups USING COVERING INDEX sqlite_autoindex_groups_1 (user_id=? AND local_display_name=?) + +Query: SELECT group_id FROM user_contact_links WHERE user_id = ? AND user_contact_link_id = ? AND group_id IS NOT NULL +Plan: +SEARCH user_contact_links USING INTEGER PRIMARY KEY (rowid=?) + +Query: SELECT group_member_id FROM group_members WHERE user_id = ? AND contact_profile_id = ? AND group_member_id != ? LIMIT 1 +Plan: +SEARCH group_members USING INDEX idx_group_members_user_id (user_id=?) + +Query: SELECT group_member_id FROM group_members WHERE user_id = ? AND group_id = ? AND local_display_name = ? +Plan: +SEARCH group_members USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) + +Query: SELECT group_member_id FROM group_members WHERE user_id = ? AND group_id = ? AND member_category = ? +Plan: +SEARCH group_members USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) + +Query: SELECT image FROM contact_profiles WHERE display_name = ? LIMIT 1 +Plan: +SEARCH contact_profiles USING INDEX contact_profiles_index (display_name=?) + +Query: SELECT last_insert_rowid() +Plan: +SCAN CONSTANT ROW + +Query: SELECT max(active_order) FROM users +Plan: +SEARCH users + +Query: SELECT note_folder_id FROM note_folders WHERE user_id = ? +Plan: +SEARCH note_folders USING COVERING INDEX note_folders_user_id (user_id=?) + +Query: SELECT note_folder_id FROM note_folders WHERE user_id = ? LIMIT 1 +Plan: +SEARCH note_folders USING COVERING INDEX note_folders_user_id (user_id=?) + +Query: SELECT quota_err_counter FROM connections WHERE user_id = ? AND connection_id = ? +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ? +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: SELECT user_contact_link_id FROM contact_requests WHERE contact_request_id = ? +Plan: +SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) + +Query: SELECT user_contact_link_id, conn_req_contact, short_link_contact, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1 +Plan: +SEARCH user_contact_links USING INDEX idx_user_contact_links_group_id (group_id=?) + +Query: SELECT user_id FROM users WHERE local_display_name = ? +Plan: +SEARCH users USING COVERING INDEX sqlite_autoindex_users_2 (local_display_name=?) + +Query: SELECT xcontact_id FROM connections WHERE user_id = ? AND via_contact_uri_hash = ? LIMIT 1 +Plan: +SEARCH connections USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_contact_uri_hash=?) + +Query: SELECT xgrplinkmem_received FROM group_members WHERE group_member_id = ? +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ? +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND group_id = ? AND chat_item_id = ? +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND item_status = ? +Plan: +SEARCH chat_items USING INDEX idx_chat_items_user_id_item_status (user_id=? AND item_status=?) + +Query: UPDATE chat_items SET timed_delete_at = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ? +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE chat_items SET timed_delete_at = ? WHERE user_id = ? AND group_id = ? AND chat_item_id = ? +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE chat_items SET via_proxy = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ? +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE connections SET auth_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ? +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE connections SET conn_status = ?, updated_at = ? WHERE connection_id = ? +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE connections SET conn_status = ?, updated_at = ?, conn_req_inv = NULL, short_link_inv = NULL WHERE connection_id = ? +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE connections SET conn_status='deleted' WHERE group_member_id = 3 +Plan: +SEARCH connections USING INDEX idx_connections_group_member_id (group_member_id=?) + +Query: UPDATE connections SET conn_type = ?, group_member_id = ?, updated_at = ? WHERE connection_id = ? +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE connections SET contact_id = ? WHERE connection_id = ? +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ? +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE connections SET quota_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ? +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE connections SET security_code = ?, security_code_verified_at = ?, updated_at = ? WHERE user_id = ? AND connection_id = ? +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE connections SET to_subscribe = 0 WHERE to_subscribe = 1 +Plan: +SEARCH connections USING INDEX idx_connections_to_subscribe (to_subscribe=?) + +Query: UPDATE contact_requests SET contact_id = ? WHERE user_id = ? AND local_display_name = ? +Plan: +SEARCH contact_requests USING INDEX sqlite_autoindex_contact_requests_1 (user_id=? AND local_display_name=?) + +Query: UPDATE contacts SET chat_deleted = ?, updated_at = ? WHERE user_id = ? AND contact_id = ? +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE contacts SET chat_item_ttl = ?, updated_at = ? WHERE contact_id = ? +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE contacts SET chat_ts = ?, chat_deleted = 0 WHERE user_id = ? AND contact_id = ? +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE contacts SET contact_grp_inv_sent = ?, updated_at = ? WHERE contact_id = ? +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE contacts SET contact_used = ? WHERE user_id = ? AND contact_id = ? +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE contacts SET enable_ntfs = ?, send_rcpts = ?, favorite = ? WHERE user_id = ? AND contact_id = ? +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE contacts SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND contact_id = ? +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE contacts SET send_rcpts = NULL +Plan: +SCAN contacts + +Query: UPDATE contacts SET ui_themes = ?, updated_at = ? WHERE user_id = ? AND contact_id = ? +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE contacts SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND contact_id = ? +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE contacts SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND unread_chat = ? +Plan: +SEARCH contacts USING INDEX idx_contacts_chat_ts (user_id=?) + +Query: UPDATE contacts SET user_preferences = ?, updated_at = ? WHERE user_id = ? AND contact_id = ? +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE files SET agent_snd_file_deleted = 1, updated_at = ? WHERE user_id = ? AND file_id = ? +Plan: +SEARCH files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE files SET cancelled = 1, ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ? +Plan: +SEARCH files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE files SET chat_item_id = ?, updated_at = ? WHERE file_id = ? +Plan: +SEARCH files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE files SET ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ? +Plan: +SEARCH files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE files SET file_crypto_key = ?, file_crypto_nonce = ?, updated_at = ? WHERE file_id = ? +Plan: +SEARCH files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE files SET file_path = ?, ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ? +Plan: +SEARCH files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE files SET private_snd_file_descr = ?, updated_at = ? WHERE user_id = ? AND file_id = ? +Plan: +SEARCH files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE group_member_intros SET intro_status='con' +Plan: +SCAN group_member_intros + +Query: UPDATE group_member_intros SET intro_status='fwd' +Plan: +SCAN group_member_intros + +Query: UPDATE group_members SET contact_id = ?, updated_at = ? WHERE contact_profile_id = ? +Plan: +SEARCH group_members USING COVERING INDEX idx_group_members_contact_profile_id (contact_profile_id=?) + +Query: UPDATE group_members SET contact_id = NULL, updated_at = ? WHERE user_id = ? AND contact_id = ? +Plan: +SEARCH group_members USING INDEX idx_group_members_user_id (user_id=?) + +Query: UPDATE group_members SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND contact_id = ? +Plan: +SEARCH group_members USING INDEX idx_group_members_user_id (user_id=?) + +Query: UPDATE group_members SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND group_member_id = ? +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE group_members SET member_id = ?, member_role = ? WHERE group_member_id = ? +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE group_members SET member_role = ? WHERE user_id = ? AND group_member_id = ? +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE group_members SET member_status='pending_approval' WHERE group_member_id = 1 +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE group_members SET member_status='pending_approval' WHERE group_member_id = 2 +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE group_members SET xgrplinkmem_received = ?, updated_at = ? WHERE group_member_id = ? +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE groups SET business_member_id = ?, customer_member_id = ? WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE groups SET chat_item_id = ?, updated_at = ? WHERE user_id = ? AND group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE groups SET chat_ts = ? WHERE user_id = ? AND group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE groups SET enable_ntfs = ?, send_rcpts = ?, favorite = ? WHERE user_id = ? AND group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE groups SET local_alias = ?, updated_at = ? WHERE user_id = ? AND group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE groups SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE groups SET send_rcpts = NULL +Plan: +SCAN groups + +Query: UPDATE groups SET ui_themes = ?, updated_at = ? WHERE user_id = ? AND group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE groups SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE groups SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND unread_chat = ? +Plan: +SEARCH groups USING INDEX idx_groups_chat_ts (user_id=?) + +Query: UPDATE groups SET user_member_profile_sent_at = ? WHERE user_id = ? AND group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE note_folders SET chat_ts = ? WHERE user_id = ? AND note_folder_id = ? +Plan: +SEARCH note_folders USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE note_folders SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND note_folder_id = ? +Plan: +SEARCH note_folders USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE note_folders SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND unread_chat = ? +Plan: +SEARCH note_folders USING INDEX note_folders_user_id (user_id=?) + +Query: UPDATE rcv_files SET agent_rcv_file_deleted = 1, updated_at = ? WHERE file_id = ? +Plan: +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE rcv_files SET agent_rcv_file_id = ?, updated_at = ? WHERE file_id = ? +Plan: +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE rcv_files SET file_status = ?, updated_at = ? WHERE file_id = ? +Plan: +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE rcv_files SET user_approved_relays = ?, rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ? +Plan: +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE settings SET chat_item_ttl = ?, updated_at = ? WHERE user_id = ? +Plan: +SEARCH settings USING INDEX idx_settings_user_id (user_id=?) + +Query: UPDATE snd_files SET file_status = ?, updated_at = ? WHERE file_id = ? AND connection_id = ? +Plan: +SEARCH snd_files USING PRIMARY KEY (file_id=? AND connection_id=?) + +Query: UPDATE snd_files SET last_inline_msg_delivery_id = ? WHERE connection_id = ? AND file_id = ? AND file_descr_id = ? +Plan: +SEARCH snd_files USING PRIMARY KEY (file_id=? AND connection_id=?) + +Query: UPDATE usage_conditions SET notified_at = ? WHERE usage_conditions_id = ? +Plan: +SEARCH usage_conditions USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE users SET active_user = 0 +Plan: +SCAN users + +Query: UPDATE users SET active_user = 1, active_order = ? WHERE user_id = ? +Plan: +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE users SET contact_id = ? WHERE user_id = ? +Plan: +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE users SET local_display_name = ?, updated_at = ? WHERE user_id = ? +Plan: +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE users SET send_rcpts_contacts = ? WHERE user_id = ? +Plan: +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE users SET send_rcpts_contacts = ?, send_rcpts_small_groups = ? WHERE view_pwd_hash IS NULL +Plan: +SCAN users + +Query: UPDATE users SET send_rcpts_small_groups = ? WHERE user_id = ? +Plan: +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE users SET ui_themes = ?, updated_at = ? WHERE user_id = ? +Plan: +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE users SET user_member_profile_updated_at = ? WHERE user_id = ? +Plan: +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql similarity index 81% rename from src/Simplex/Chat/Migrations/chat_schema.sql rename to src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 7a3be6e3ae..6fbed97d27 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -22,7 +22,7 @@ CREATE TABLE contact_profiles( ); CREATE TABLE users( user_id INTEGER PRIMARY KEY, - contact_id INTEGER NOT NULL UNIQUE REFERENCES contacts ON DELETE CASCADE + contact_id INTEGER NOT NULL UNIQUE REFERENCES contacts ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED, local_display_name TEXT NOT NULL UNIQUE, active_user INTEGER NOT NULL DEFAULT 0, @@ -33,10 +33,13 @@ CREATE TABLE users( view_pwd_salt BLOB, show_ntfs INTEGER NOT NULL DEFAULT 1, send_rcpts_contacts INTEGER NOT NULL DEFAULT 0, - send_rcpts_small_groups INTEGER NOT NULL DEFAULT 0, -- 1 for active user + send_rcpts_small_groups INTEGER NOT NULL DEFAULT 0, + user_member_profile_updated_at TEXT, + ui_themes TEXT, + active_order INTEGER NOT NULL DEFAULT 0, -- 1 for active user FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) - ON DELETE CASCADE + ON DELETE RESTRICT ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED ); @@ -72,6 +75,10 @@ CREATE TABLE contacts( REFERENCES group_members(group_member_id) ON DELETE SET NULL, contact_grp_inv_sent INTEGER NOT NULL DEFAULT 0, contact_status TEXT NOT NULL DEFAULT 'active', + custom_data BLOB, + ui_themes TEXT, + chat_deleted INTEGER NOT NULL DEFAULT 0, + chat_item_ttl INTEGER, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -101,7 +108,8 @@ CREATE TABLE group_profiles( image TEXT, user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE, preferences TEXT, - description TEXT NULL + description TEXT NULL, + member_admission TEXT ); CREATE TABLE groups( group_id INTEGER PRIMARY KEY, -- local group ID @@ -113,12 +121,20 @@ CREATE TABLE groups( updated_at TEXT CHECK(updated_at NOT NULL), chat_item_id INTEGER DEFAULT NULL REFERENCES chat_items ON DELETE SET NULL, enable_ntfs INTEGER, - host_conn_custom_user_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL, unread_chat INTEGER DEFAULT 0 CHECK(unread_chat NOT NULL), chat_ts TEXT, favorite INTEGER NOT NULL DEFAULT 0, send_rcpts INTEGER, - via_group_link_uri_hash BLOB, -- received + via_group_link_uri_hash BLOB, + user_member_profile_sent_at TEXT, + custom_data BLOB, + ui_themes TEXT, + business_member_id BLOB NULL, + business_chat TEXT NULL, + business_xcontact_id BLOB NULL, + customer_member_id BLOB NULL, + chat_item_ttl INTEGER, + local_alias TEXT DEFAULT '', -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -150,6 +166,7 @@ CREATE TABLE group_members( invited_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL, peer_chat_min_version INTEGER NOT NULL DEFAULT 1, peer_chat_max_version INTEGER NOT NULL DEFAULT 1, + member_restriction TEXT, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -189,7 +206,9 @@ CREATE TABLE files( agent_snd_file_deleted INTEGER DEFAULT 0 CHECK(agent_snd_file_deleted NOT NULL), protocol TEXT NOT NULL DEFAULT 'smp', file_crypto_key BLOB, - file_crypto_nonce BLOB + file_crypto_nonce BLOB, + note_folder_id INTEGER DEFAULT NULL REFERENCES note_folders ON DELETE CASCADE, + redirect_file_id INTEGER REFERENCES files ON DELETE CASCADE ); CREATE TABLE snd_files( file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE, @@ -218,7 +237,8 @@ CREATE TABLE rcv_files( REFERENCES xftp_file_descriptions ON DELETE SET NULL, agent_rcv_file_id BLOB NULL, agent_rcv_file_deleted INTEGER DEFAULT 0 CHECK(agent_rcv_file_deleted NOT NULL), - to_receive INTEGER + to_receive INTEGER, + user_approved_relays INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE snd_file_chunks( file_id INTEGER NOT NULL, @@ -272,6 +292,14 @@ CREATE TABLE connections( peer_chat_max_version INTEGER NOT NULL DEFAULT 1, to_subscribe INTEGER DEFAULT 0 NOT NULL, contact_conn_initiated INTEGER NOT NULL DEFAULT 0, + conn_chat_version INTEGER, + pq_support INTEGER NOT NULL DEFAULT 0, + pq_encryption INTEGER NOT NULL DEFAULT 0, + pq_snd_enabled INTEGER, + pq_rcv_enabled INTEGER, + quota_err_counter INTEGER NOT NULL DEFAULT 0, + short_link_inv BLOB, + via_short_link_contact BLOB, FOREIGN KEY(snd_file_id, connection_id) REFERENCES snd_files(file_id, connection_id) ON DELETE CASCADE @@ -290,6 +318,8 @@ CREATE TABLE user_contact_links( auto_accept_incognito INTEGER DEFAULT 0 CHECK(auto_accept_incognito NOT NULL), group_link_id BLOB, group_link_member_role TEXT NULL, + business_address INTEGER DEFAULT 0, + short_link_contact BLOB, UNIQUE(user_id, local_display_name) ); CREATE TABLE contact_requests( @@ -307,6 +337,8 @@ CREATE TABLE contact_requests( xcontact_id BLOB, peer_chat_min_version INTEGER NOT NULL DEFAULT 1, peer_chat_max_version INTEGER NOT NULL DEFAULT 1, + pq_support INTEGER NOT NULL DEFAULT 0, + contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON UPDATE CASCADE @@ -339,7 +371,7 @@ CREATE TABLE pending_group_messages( updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); CREATE TABLE chat_items( - chat_item_id INTEGER PRIMARY KEY, + chat_item_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, group_id INTEGER REFERENCES groups ON DELETE CASCADE, @@ -368,8 +400,20 @@ CREATE TABLE chat_items( item_deleted_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL, item_deleted_ts TEXT, forwarded_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL, - item_content_tag TEXT + item_content_tag TEXT, + note_folder_id INTEGER DEFAULT NULL REFERENCES note_folders ON DELETE CASCADE, + fwd_from_tag TEXT, + fwd_from_chat_name TEXT, + fwd_from_msg_dir INTEGER, + fwd_from_contact_id INTEGER REFERENCES contacts ON DELETE SET NULL, + fwd_from_group_id INTEGER REFERENCES groups ON DELETE SET NULL, + fwd_from_chat_item_id INTEGER REFERENCES chat_items ON DELETE SET NULL, + via_proxy INTEGER, + msg_content_tag TEXT, + include_in_history INTEGER NOT NULL DEFAULT 0, + user_mention INTEGER NOT NULL DEFAULT 0 ); +CREATE TABLE sqlite_sequence(name,seq); CREATE TABLE chat_item_messages( chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, message_id INTEGER NOT NULL UNIQUE REFERENCES messages ON DELETE CASCADE, @@ -388,6 +432,8 @@ CREATE TABLE calls( user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) + , + call_uuid TEXT NOT NULL DEFAULT "" ); CREATE TABLE commands( command_id INTEGER PRIMARY KEY AUTOINCREMENT, -- used as ACorrId @@ -398,7 +444,6 @@ CREATE TABLE commands( created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); -CREATE TABLE sqlite_sequence(name,seq); CREATE TABLE settings( settings_id INTEGER PRIMARY KEY, chat_item_ttl INTEGER, @@ -479,6 +524,8 @@ CREATE TABLE group_snd_item_statuses( group_snd_item_status TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) + , + via_proxy INTEGER ); CREATE TABLE IF NOT EXISTS "sent_probes"( sent_probe_id INTEGER PRIMARY KEY, @@ -544,9 +591,68 @@ CREATE TABLE IF NOT EXISTS "msg_deliveries"( chat_ts TEXT NOT NULL DEFAULT(datetime('now')), created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL), - agent_ack_cmd_id INTEGER, -- broker_ts for received, created_at for sent delivery_status TEXT -- MsgDeliveryStatus ); +CREATE TABLE note_folders( + note_folder_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')), + chat_ts TEXT NOT NULL DEFAULT(datetime('now')), + favorite INTEGER NOT NULL DEFAULT 0, + unread_chat INTEGER NOT NULL DEFAULT 0 +); +CREATE TABLE app_settings(app_settings TEXT NOT NULL); +CREATE TABLE server_operators( + server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT, + server_operator_tag TEXT, + trade_name TEXT NOT NULL, + legal_name TEXT, + server_domains TEXT, + enabled INTEGER NOT NULL DEFAULT 1, + smp_role_storage INTEGER NOT NULL DEFAULT 1, + smp_role_proxy INTEGER NOT NULL DEFAULT 1, + xftp_role_storage INTEGER NOT NULL DEFAULT 1, + xftp_role_proxy INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +); +CREATE TABLE usage_conditions( + usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, + conditions_commit TEXT NOT NULL UNIQUE, + notified_at TEXT, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +); +CREATE TABLE operator_usage_conditions( + operator_usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, + server_operator_id INTEGER REFERENCES server_operators(server_operator_id) ON DELETE SET NULL ON UPDATE CASCADE, + server_operator_tag TEXT, + conditions_commit TEXT NOT NULL, + accepted_at TEXT, + created_at TEXT NOT NULL DEFAULT(datetime('now')) + , + auto_accepted INTEGER DEFAULT 0 +); +CREATE TABLE chat_tags( + chat_tag_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER REFERENCES users, + chat_tag_text TEXT NOT NULL, + chat_tag_emoji TEXT, + tag_order INTEGER NOT NULL +); +CREATE TABLE chat_tags_chats( + contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, + group_id INTEGER REFERENCES groups ON DELETE CASCADE, + chat_tag_id INTEGER NOT NULL REFERENCES chat_tags ON DELETE CASCADE +); +CREATE TABLE chat_item_mentions( + chat_item_mention_id INTEGER PRIMARY KEY AUTOINCREMENT, + group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, + member_id BLOB NOT NULL, + chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, + display_name TEXT NOT NULL +); CREATE INDEX contact_profiles_index ON contact_profiles( display_name, full_name @@ -555,18 +661,6 @@ CREATE INDEX idx_groups_inv_queue_info ON groups(inv_queue_info); CREATE INDEX idx_contact_requests_xcontact_id ON contact_requests(xcontact_id); CREATE INDEX idx_contacts_xcontact_id ON contacts(xcontact_id); CREATE INDEX idx_messages_shared_msg_id ON messages(shared_msg_id); -CREATE INDEX idx_chat_items_shared_msg_id ON chat_items(shared_msg_id); -CREATE INDEX idx_chat_items_groups ON chat_items( - user_id, - group_id, - item_ts, - chat_item_id -); -CREATE INDEX idx_chat_items_contacts ON chat_items( - user_id, - contact_id, - chat_item_id -); CREATE UNIQUE INDEX idx_chat_items_direct_shared_msg_id ON chat_items( user_id, contact_id, @@ -639,9 +733,6 @@ CREATE INDEX idx_group_members_contact_profile_id ON group_members( CREATE INDEX idx_group_members_user_id ON group_members(user_id); CREATE INDEX idx_group_members_invited_by ON group_members(invited_by); CREATE INDEX idx_group_profiles_user_id ON group_profiles(user_id); -CREATE INDEX idx_groups_host_conn_custom_user_profile_id ON groups( - host_conn_custom_user_profile_id -); CREATE INDEX idx_groups_chat_item_id ON groups(chat_item_id); CREATE INDEX idx_groups_group_profile_id ON groups(group_profile_id); CREATE INDEX idx_messages_group_id ON messages(group_id); @@ -804,11 +895,130 @@ CREATE INDEX idx_contact_requests_updated_at ON contact_requests( ); CREATE INDEX idx_connections_updated_at ON connections(user_id, updated_at); CREATE INDEX idx_msg_deliveries_message_id ON "msg_deliveries"(message_id); -CREATE INDEX idx_msg_deliveries_agent_ack_cmd_id ON "msg_deliveries"( - connection_id, - agent_ack_cmd_id -); CREATE INDEX idx_msg_deliveries_agent_msg_id ON "msg_deliveries"( connection_id, agent_msg_id ); +CREATE INDEX chat_items_note_folder_id ON chat_items(note_folder_id); +CREATE INDEX files_note_folder_id ON files(note_folder_id); +CREATE INDEX note_folders_user_id ON note_folders(user_id); +CREATE INDEX idx_chat_items_contacts_created_at on chat_items( + user_id, + contact_id, + created_at +); +CREATE INDEX idx_chat_items_notes_created_at on chat_items( + user_id, + note_folder_id, + created_at +); +CREATE INDEX idx_files_redirect_file_id on files(redirect_file_id); +CREATE INDEX idx_chat_items_fwd_from_contact_id ON chat_items( + fwd_from_contact_id +); +CREATE INDEX idx_chat_items_fwd_from_group_id ON chat_items(fwd_from_group_id); +CREATE INDEX idx_chat_items_fwd_from_chat_item_id ON chat_items( + fwd_from_chat_item_id +); +CREATE INDEX idx_received_probes_group_member_id on received_probes( + group_member_id +); +CREATE INDEX idx_contact_requests_contact_id ON contact_requests(contact_id); +CREATE INDEX idx_operator_usage_conditions_server_operator_id ON operator_usage_conditions( + server_operator_id +); +CREATE UNIQUE INDEX idx_operator_usage_conditions_conditions_commit ON operator_usage_conditions( + conditions_commit, + server_operator_id +); +CREATE INDEX idx_chat_items_contacts ON chat_items( + user_id, + contact_id, + item_status, + created_at +); +CREATE INDEX idx_chat_items_groups ON chat_items( + user_id, + group_id, + item_status, + item_ts +); +CREATE INDEX idx_chat_items_groups_item_ts ON chat_items( + user_id, + group_id, + item_ts +); +CREATE INDEX idx_chat_items_notes ON chat_items( + user_id, + note_folder_id, + item_status, + created_at +); +CREATE INDEX idx_groups_business_xcontact_id ON groups(business_xcontact_id); +CREATE INDEX idx_chat_tags_user_id ON chat_tags(user_id); +CREATE UNIQUE INDEX idx_chat_tags_user_id_chat_tag_text ON chat_tags( + user_id, + chat_tag_text +); +CREATE UNIQUE INDEX idx_chat_tags_user_id_chat_tag_emoji ON chat_tags( + user_id, + chat_tag_emoji +); +CREATE INDEX idx_chat_tags_chats_chat_tag_id ON chat_tags_chats(chat_tag_id); +CREATE UNIQUE INDEX idx_chat_tags_chats_chat_tag_id_contact_id ON chat_tags_chats( + contact_id, + chat_tag_id +); +CREATE UNIQUE INDEX idx_chat_tags_chats_chat_tag_id_group_id ON chat_tags_chats( + group_id, + chat_tag_id +); +CREATE INDEX idx_chat_items_groups_msg_content_tag_item_ts ON chat_items( + user_id, + group_id, + msg_content_tag, + item_ts +); +CREATE INDEX idx_chat_items_groups_msg_content_tag_deleted ON chat_items( + user_id, + group_id, + msg_content_tag, + item_deleted, + item_sent +); +CREATE INDEX idx_chat_items_groups_history ON chat_items( + user_id, + group_id, + include_in_history, + item_deleted, + item_ts, + chat_item_id +); +CREATE INDEX idx_group_snd_item_statuses_chat_item_id_group_member_id ON group_snd_item_statuses( + chat_item_id, + group_member_id +); +CREATE INDEX idx_chat_item_mentions_group_id ON chat_item_mentions(group_id); +CREATE INDEX idx_chat_item_mentions_chat_item_id ON chat_item_mentions( + chat_item_id +); +CREATE UNIQUE INDEX idx_chat_item_mentions_display_name ON chat_item_mentions( + chat_item_id, + display_name +); +CREATE UNIQUE INDEX idx_chat_item_mentions_member_id ON chat_item_mentions( + chat_item_id, + member_id +); +CREATE INDEX idx_chat_items_groups_user_mention ON chat_items( + user_id, + group_id, + item_status, + user_mention +); +CREATE INDEX idx_chat_items_group_id ON chat_items(group_id); +CREATE INDEX idx_connections_group_member_id ON connections(group_member_id); +CREATE INDEX idx_chat_items_group_id_shared_msg_id ON chat_items( + group_id, + shared_msg_id +); diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index a5f4f0e0e5..b32fd07bb5 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -1,12 +1,17 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeOperators #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module Simplex.Chat.Store.Shared where @@ -24,23 +29,43 @@ import Data.Maybe (fromMaybe, isJust, listToMaybe) import Data.Text (Text) import qualified Data.Text as T import Data.Time.Clock (UTCTime (..), getCurrentTime) -import Database.SQLite.Simple (NamedParam (..), Only (..), Query, SQLError, (:.) (..)) -import qualified Database.SQLite.Simple as SQL -import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Messages import Simplex.Chat.Protocol import Simplex.Chat.Remote.Types import Simplex.Chat.Types import Simplex.Chat.Types.Preferences -import Simplex.Messaging.Agent.Protocol (ConnId, UserId) -import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) -import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Chat.Types.Shared +import Simplex.Chat.Types.UITheme +import Simplex.Messaging.Agent.Protocol (ConnId, ConnShortLink, ConnectionMode (..), CreatedConnLink (..), UserId) +import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) +import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) +import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..)) +import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (SubscriptionMode (..)) import Simplex.Messaging.Util (allFinally) import Simplex.Messaging.Version import UnliftIO.STM +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (Only (..), Query, SqlError, (:.) (..)) +import Database.PostgreSQL.Simple.Errors (constraintViolation) +import Database.PostgreSQL.Simple.SqlQQ (sql) +#else +import Database.SQLite.Simple (Only (..), Query, SQLError, (:.) (..)) +import qualified Database.SQLite.Simple as SQL +import Database.SQLite.Simple.QQ (sql) +#endif + +data ChatLockEntity + = CLInvitation ByteString + | CLConnection Int64 + | CLContact ContactId + | CLGroup GroupId + | CLUserContact Int64 + | CLFile Int64 + deriving (Eq, Ord) -- These error type constructors must be added to mobile apps data StoreError @@ -69,6 +94,9 @@ data StoreError | SEDuplicateGroupMember | SEGroupAlreadyJoined | SEGroupInvitationNotFound + | SENoteFolderAlreadyExists {noteFolderId :: NoteFolderId} + | SENoteFolderNotFound {noteFolderId :: NoteFolderId} + | SEUserNoteFolderNotFound | SESndFileNotFound {fileId :: FileTransferId} | SESndFileInvalid {fileId :: FileTransferId} | SERcvFileNotFound {fileId :: FileTransferId} @@ -76,6 +104,7 @@ data StoreError | SEFileNotFound {fileId :: FileTransferId} | SERcvFileInvalid {fileId :: FileTransferId} | SERcvFileInvalidDescrPart + | SELocalFileNoTransfer {fileId :: FileTransferId} | SESharedMsgIdNotFoundByFileId {fileId :: FileTransferId} | SEFileIdNotFoundBySharedMsgId {sharedMsgId :: SharedMsgId} | SESndFileNotFoundXFTP {agentSndFileId :: AgentSndFileId} @@ -88,11 +117,14 @@ data StoreError | SEUniqueID | SELargeMsg | SEInternalError {message :: String} - | SEBadChatItem {itemId :: ChatItemId} + | SEDBException {message :: String} + | SEDBBusyError {message :: String} + | SEBadChatItem {itemId :: ChatItemId, itemTs :: Maybe ChatItemTs} | SEChatItemNotFound {itemId :: ChatItemId} | SEChatItemNotFoundByText {text :: Text} | SEChatItemSharedMsgIdNotFound {sharedMsgId :: SharedMsgId} | SEChatItemNotFoundByFileId {fileId :: FileTransferId} + | SEChatItemNotFoundByContactId {contactId :: ContactId} | SEChatItemNotFoundByGroupId {groupId :: GroupId} | SEProfileNotFound {profileId :: Int64} | SEDuplicateGroupLink {groupInfo :: GroupInfo} @@ -106,19 +138,42 @@ data StoreError | SERemoteHostDuplicateCA | SERemoteCtrlNotFound {remoteCtrlId :: RemoteCtrlId} | SERemoteCtrlDuplicateCA + | SEProhibitedDeleteUser {userId :: UserId, contactId :: ContactId} + | SEOperatorNotFound {serverOperatorId :: Int64} + | SEUsageConditionsNotFound + | SEInvalidQuote + | SEInvalidMention deriving (Show, Exception) $(J.deriveJSON (sumTypeJSON $ dropPrefix "SE") ''StoreError) insertedRowId :: DB.Connection -> IO Int64 -insertedRowId db = fromOnly . head <$> DB.query_ db "SELECT last_insert_rowid()" +insertedRowId db = fromOnly . head <$> DB.query_ db q + where +#if defined(dbPostgres) + q = "SELECT lastval()" +#else + q = "SELECT last_insert_rowid()" +#endif checkConstraint :: StoreError -> ExceptT StoreError IO a -> ExceptT StoreError IO a checkConstraint err action = ExceptT $ runExceptT action `E.catch` (pure . Left . handleSQLError err) +#if defined(dbPostgres) +type SQLError = SqlError +#endif + +constraintError :: SQLError -> Bool +#if defined(dbPostgres) +constraintError = isJust . constraintViolation +#else +constraintError e = SQL.sqlError e == SQL.ErrorConstraint +#endif +{-# INLINE constraintError #-} + handleSQLError :: StoreError -> SQLError -> StoreError handleSQLError err e - | SQL.sqlError e == SQL.ErrorConstraint = err + | constraintError e = err | otherwise = SEInternalError $ show e storeFinally :: ExceptT StoreError IO a -> ExceptT StoreError IO b -> ExceptT StoreError IO a @@ -142,17 +197,39 @@ toFileInfo (fileId, fileStatus, filePath) = CIFileInfo {fileId, fileStatus, file type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64) -type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, Bool, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, Int, Version, Version) +type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, BoolInt, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, BoolInt, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, PQSupport, PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Int, Int, Maybe VersionChat, VersionChat, VersionChat) -type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Bool, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe Int, Maybe Version, Maybe Version) +type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe BoolInt, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe BoolInt, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe PQSupport, Maybe PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Maybe Int, Maybe Int, Maybe VersionChat, Maybe VersionChat, Maybe VersionChat) -toConnection :: ConnectionRow -> Connection -toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter, minVer, maxVer)) = - let entityId = entityId_ connType - connectionCode = SecurityCode <$> code_ <*> verifiedAt_ - peerChatVRange = JVersionRange $ fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer - in Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias, entityId, connectionCode, authErrCounter, createdAt} +toConnection :: VersionRangeChat -> ConnectionRow -> Connection +toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, BI viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, BI contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, quotaErrCounter, chatV, minVer, maxVer)) = + Connection + { connId, + agentConnId = AgentConnId acId, + connChatVersion = fromMaybe (vr `peerConnChatVersion` peerChatVRange) chatV, + peerChatVRange = peerChatVRange, + connLevel, + viaContact, + viaUserContactLink, + viaGroupLink, + groupLinkId, + customUserProfileId, + connStatus, + connType, + contactConnInitiated, + localAlias, + entityId = entityId_ connType, + connectionCode = SecurityCode <$> code_ <*> verifiedAt_, + pqSupport, + pqEncryption, + pqSndEnabled, + pqRcvEnabled, + authErrCounter, + quotaErrCounter, + createdAt + } where + peerChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer entityId_ :: ConnType -> Maybe Int64 entityId_ ConnContact = contactId entityId_ ConnMember = groupMemberId @@ -160,13 +237,13 @@ toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroup entityId_ ConnSndFile = sndFileId entityId_ ConnUserContact = userContactLinkId -toMaybeConnection :: MaybeConnectionRow -> Maybe Connection -toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just contactConnInitiated, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just authErrCounter, Just minVer, Just maxVer)) = - Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter, minVer, maxVer)) -toMaybeConnection _ = Nothing +toMaybeConnection :: VersionRangeChat -> MaybeConnectionRow -> Maybe Connection +toMaybeConnection vr ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just contactConnInitiated, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just pqSupport, Just pqEncryption, pqSndEnabled_, pqRcvEnabled_, Just authErrCounter, Just quotaErrCounter, connChatVersion, Just minVer, Just maxVer)) = + Just $ toConnection vr ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled_, pqRcvEnabled_, authErrCounter, quotaErrCounter, connChatVersion, minVer, maxVer)) +toMaybeConnection _ _ = Nothing -createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> VersionRange -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> SubscriptionMode -> IO Connection -createConnection_ db userId connType entityId acId peerChatVRange@(VersionRange minV maxV) viaContact viaUserContactLink customUserProfileId connLevel currentTs subMode = do +createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> ConnStatus -> VersionChat -> VersionRangeChat -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> SubscriptionMode -> PQSupport -> IO Connection +createConnection_ db userId connType entityId acId connStatus connChatVersion peerChatVRange@(VersionRange minV maxV) viaContact viaUserContactLink customUserProfileId connLevel currentTs subMode pqSup = do viaLinkGroupId :: Maybe Int64 <- fmap join . forM viaUserContactLink $ \ucLinkId -> maybeFirstRow fromOnly $ DB.query db "SELECT group_id FROM user_contact_links WHERE user_id = ? AND user_contact_link_id = ? AND group_id IS NOT NULL" (userId, ucLinkId) let viaGroupLink = isJust viaLinkGroupId @@ -176,15 +253,40 @@ createConnection_ db userId connType entityId acId peerChatVRange@(VersionRange INSERT INTO connections ( user_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, custom_user_profile_id, conn_status, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, updated_at, - peer_chat_min_version, peer_chat_max_version, to_subscribe - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + conn_chat_version, peer_chat_min_version, peer_chat_max_version, to_subscribe, pq_support, pq_encryption + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (userId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, ConnNew, connType) + ( (userId, acId, connLevel, viaContact, viaUserContactLink, BI viaGroupLink, customUserProfileId, connStatus, connType) :. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs) - :. (minV, maxV, subMode == SMOnlyCreate) + :. (connChatVersion, minV, maxV, BI (subMode == SMOnlyCreate), pqSup, pqSup) ) connId <- insertedRowId db - pure Connection {connId, agentConnId = AgentConnId acId, peerChatVRange = JVersionRange peerChatVRange, connType, contactConnInitiated = False, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0} + pure + Connection + { connId, + agentConnId = AgentConnId acId, + connChatVersion, + peerChatVRange, + connType, + contactConnInitiated = False, + entityId, + viaContact, + viaUserContactLink, + viaGroupLink, + groupLinkId = Nothing, + customUserProfileId, + connLevel, + connStatus, + localAlias = "", + createdAt = currentTs, + connectionCode = Nothing, + pqSupport = pqSup, + pqEncryption = CR.pqSupportToEnc pqSup, + pqSndEnabled = Nothing, + pqRcvEnabled = Nothing, + authErrCounter = 0, + quotaErrCounter = 0 + } where ent ct = if connType == ct then entityId else Nothing @@ -196,21 +298,65 @@ createIncognitoProfile_ db userId createdAt Profile {displayName, fullName, imag INSERT INTO contact_profiles (display_name, full_name, image, user_id, incognito, created_at, updated_at) VALUES (?,?,?,?,?,?,?) |] - (displayName, fullName, image, userId, Just True, createdAt, createdAt) + (displayName, fullName, image, userId, Just (BI True), createdAt, createdAt) insertedRowId db -setPeerChatVRange :: DB.Connection -> Int64 -> VersionRange -> IO () -setPeerChatVRange db connId (VersionRange minVer maxVer) = +updateConnSupportPQ :: DB.Connection -> Int64 -> PQSupport -> PQEncryption -> IO () +updateConnSupportPQ db connId pqSup pqEnc = DB.execute db [sql| UPDATE connections - SET peer_chat_min_version = ?, peer_chat_max_version = ? + SET pq_support = ?, pq_encryption = ? WHERE connection_id = ? |] - (minVer, maxVer, connId) + (pqSup, pqEnc, connId) -setMemberChatVRange :: DB.Connection -> GroupMemberId -> VersionRange -> IO () +updateConnPQSndEnabled :: DB.Connection -> Int64 -> PQEncryption -> IO () +updateConnPQSndEnabled db connId pqSndEnabled = + DB.execute + db + [sql| + UPDATE connections + SET pq_snd_enabled = ? + WHERE connection_id = ? + |] + (pqSndEnabled, connId) + +updateConnPQRcvEnabled :: DB.Connection -> Int64 -> PQEncryption -> IO () +updateConnPQRcvEnabled db connId pqRcvEnabled = + DB.execute + db + [sql| + UPDATE connections + SET pq_rcv_enabled = ? + WHERE connection_id = ? + |] + (pqRcvEnabled, connId) + +updateConnPQEnabledCON :: DB.Connection -> Int64 -> PQEncryption -> IO () +updateConnPQEnabledCON db connId pqEnabled = + DB.execute + db + [sql| + UPDATE connections + SET pq_snd_enabled = ?, pq_rcv_enabled = ? + WHERE connection_id = ? + |] + (pqEnabled, pqEnabled, connId) + +setPeerChatVRange :: DB.Connection -> Int64 -> VersionChat -> VersionRangeChat -> IO () +setPeerChatVRange db connId chatV (VersionRange minVer maxVer) = + DB.execute + db + [sql| + UPDATE connections + SET conn_chat_version = ?, peer_chat_min_version = ?, peer_chat_max_version = ? + WHERE connection_id = ? + |] + (chatV, minVer, maxVer, connId) + +setMemberChatVRange :: DB.Connection -> GroupMemberId -> VersionRangeChat -> IO () setMemberChatVRange db mId (VersionRange minVer maxVer) = DB.execute db @@ -236,10 +382,10 @@ setCommandConnId db User {userId} cmdId connId = do createContact :: DB.Connection -> User -> Profile -> ExceptT StoreError IO () createContact db User {userId} profile = do currentTs <- liftIO getCurrentTime - void $ createContact_ db userId profile "" Nothing currentTs True + void $ createContact_ db userId profile "" Nothing currentTs -createContact_ :: DB.Connection -> UserId -> Profile -> LocalAlias -> Maybe Int64 -> UTCTime -> Bool -> ExceptT StoreError IO (Text, ContactId, ProfileId) -createContact_ db userId Profile {displayName, fullName, image, contactLink, preferences} localAlias viaGroup currentTs contactUsed = +createContact_ :: DB.Connection -> UserId -> Profile -> LocalAlias -> Maybe Int64 -> UTCTime -> ExceptT StoreError IO (Text, ContactId, ProfileId) +createContact_ db userId Profile {displayName, fullName, image, contactLink, preferences} localAlias viaGroup currentTs = ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do DB.execute db @@ -249,38 +395,40 @@ createContact_ db userId Profile {displayName, fullName, image, contactLink, pre DB.execute db "INSERT INTO contacts (contact_profile_id, local_display_name, user_id, via_group, created_at, updated_at, chat_ts, contact_used) VALUES (?,?,?,?,?,?,?,?)" - (profileId, ldn, userId, viaGroup, currentTs, currentTs, currentTs, contactUsed) + (profileId, ldn, userId, viaGroup, currentTs, currentTs, currentTs, BI True) contactId <- insertedRowId db pure $ Right (ldn, contactId, profileId) deleteUnusedIncognitoProfileById_ :: DB.Connection -> User -> ProfileId -> IO () deleteUnusedIncognitoProfileById_ db User {userId} profileId = - DB.executeNamed + DB.execute db [sql| DELETE FROM contact_profiles - WHERE user_id = :user_id AND contact_profile_id = :profile_id AND incognito = 1 + WHERE user_id = ? AND contact_profile_id = ? AND incognito = 1 AND 1 NOT IN ( SELECT 1 FROM connections - WHERE user_id = :user_id AND custom_user_profile_id = :profile_id LIMIT 1 + WHERE user_id = ? AND custom_user_profile_id = ? LIMIT 1 ) AND 1 NOT IN ( SELECT 1 FROM group_members - WHERE user_id = :user_id AND member_profile_id = :profile_id LIMIT 1 + WHERE user_id = ? AND member_profile_id = ? LIMIT 1 ) |] - [":user_id" := userId, ":profile_id" := profileId] + (userId, profileId, userId, profileId, userId, profileId) -type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool) +type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. (Maybe GroupMemberId, BoolInt, Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) -toContact :: User -> ContactRow :. MaybeConnectionRow -> Contact -toContact user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)) :. connRow) = +type ContactRow = Only ContactId :. ContactRow' + +toContact :: VersionRangeChat -> User -> [ChatTagId] -> ContactRow :. MaybeConnectionRow -> Contact +toContact vr user chatTags ((Only contactId :. (profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. (contactGroupMemberId, BI contactGrpInvSent, uiThemes, BI chatDeleted, customData, chatItemTTL)) :. connRow) = let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} - activeConn = toMaybeConnection connRow - chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} + activeConn = toMaybeConnection vr connRow + chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} incognito = maybe False connIncognito activeConn mergedPreferences = contactUserPreferences user userPreferences preferences incognito - in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent} + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalProfile getProfileById db userId profileId = @@ -294,38 +442,39 @@ getProfileById db userId profileId = |] (userId, profileId) where - toProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences) -> LocalProfile + toProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, Maybe Preferences) -> LocalProfile toProfile (displayName, fullName, image, contactLink, localAlias, preferences) = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} -type ContactRequestRow = (Int64, ContactName, AgentInvId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact) :. (Maybe XContactId, Maybe Preferences, UTCTime, UTCTime, Version, Version) +type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact) :. (Maybe XContactId, PQSupport, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) toContactRequest :: ContactRequestRow -> UserContactRequest -toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, preferences, createdAt, updatedAt, minVer, maxVer)) = do +toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, contactId_, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, pqSupport, preferences, createdAt, updatedAt, minVer, maxVer)) = do let profile = Profile {displayName, fullName, image, contactLink, preferences} - cReqChatVRange = JVersionRange $ fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer - in UserContactRequest {contactRequestId, agentInvitationId, userContactLinkId, agentContactConnId, cReqChatVRange, localDisplayName, profileId, profile, xContactId, createdAt, updatedAt} + cReqChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer + in UserContactRequest {contactRequestId, agentInvitationId, contactId_, userContactLinkId, agentContactConnId, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, createdAt, updatedAt} userQuery :: Query userQuery = [sql| - SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt + SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences, + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id |] -toUser :: (UserId, UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, Maybe Preferences) :. (Bool, Bool, Bool, Maybe B64UrlByteString, Maybe B64UrlByteString) -> User -toUser ((userId, auId, userContactId, profileId, activeUser, displayName, fullName, image, contactLink, userPreferences) :. (showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash_, viewPwdSalt_)) = - User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash} +toUser :: (UserId, UserId, ContactId, ProfileId, BoolInt, Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, Maybe UIThemeEntityOverrides) -> User +toUser ((userId, auId, userContactId, profileId, BI activeUser, activeOrder, displayName, fullName, image, contactLink, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, uiThemes)) = + User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, activeOrder, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash, userMemberProfileUpdatedAt, uiThemes} where profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences = userPreferences, localAlias = ""} fullPreferences = mergePreferences Nothing userPreferences viewPwdHash = UserPwdHash <$> viewPwdHash_ <*> viewPwdSalt_ -toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, LocalAlias, UTCTime, UTCTime) -> PendingContactConnection -toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, viaUserContactLink, groupLinkId, customUserProfileId, connReqInv, localAlias, createdAt, updatedAt) = - PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = isJust connReqHash, viaUserContactLink, groupLinkId, customUserProfileId, connReqInv, localAlias, createdAt, updatedAt} +toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, Maybe (ConnShortLink 'CMInvitation), LocalAlias, UTCTime, UTCTime) -> PendingContactConnection +toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, viaUserContactLink, groupLinkId, customUserProfileId, connReqInv, shortLinkInv, localAlias, createdAt, updatedAt) = + let connLinkInv = (`CCLink` shortLinkInv) <$> connReqInv + in PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = isJust connReqHash, viaUserContactLink, groupLinkId, customUserProfileId, connLinkInv, localAlias, createdAt, updatedAt} getConnReqInv :: DB.Connection -> Int64 -> ExceptT StoreError IO ConnReqInvitation getConnReqInv db connId = @@ -343,15 +492,15 @@ withLocalDisplayName db userId displayName action = getLdnSuffix >>= (`tryCreate getLdnSuffix :: IO Int getLdnSuffix = maybe 0 ((+ 1) . fromOnly) . listToMaybe - <$> DB.queryNamed + <$> DB.query db [sql| SELECT ldn_suffix FROM display_names - WHERE user_id = :user_id AND ldn_base = :display_name + WHERE user_id = ? AND ldn_base = ? ORDER BY ldn_suffix DESC LIMIT 1 |] - [":user_id" := userId, ":display_name" := displayName] + (userId, displayName) tryCreateName :: Int -> Int -> IO (Either StoreError a) tryCreateName _ 0 = pure $ Left SEDuplicateName tryCreateName ldnSuffix attempts = do @@ -360,7 +509,7 @@ withLocalDisplayName db userId displayName action = getLdnSuffix >>= (`tryCreate E.try (insertName ldn currentTs) >>= \case Right () -> action ldn Left e - | SQL.sqlError e == SQL.ErrorConstraint -> tryCreateName (ldnSuffix + 1) (attempts - 1) + | constraintError e -> tryCreateName (ldnSuffix + 1) (attempts - 1) | otherwise -> E.throwIO e where insertName ldn ts = @@ -392,8 +541,159 @@ createWithRandomBytes' size gVar create = tryCreate 3 liftIO (E.try $ create id') >>= \case Right x -> liftEither x Left e - | SQL.sqlError e == SQL.ErrorConstraint -> tryCreate (n - 1) + | constraintError e -> tryCreate (n - 1) | otherwise -> throwError . SEInternalError $ show e encodedRandomBytes :: TVar ChaChaDRG -> Int -> IO ByteString encodedRandomBytes gVar n = atomically $ B64.encode <$> C.randomBytes n gVar + +assertNotUser :: DB.Connection -> User -> Contact -> ExceptT StoreError IO () +assertNotUser db User {userId} Contact {contactId, localDisplayName} = do + r :: (Maybe Int64) <- + -- This query checks that the foreign keys in the users table + -- are not referencing the contact about to be deleted. + -- With the current schema it would cause cascade delete of user, + -- with mofified schema (in v5.6.0-beta.0) it would cause foreign key violation error. + liftIO . maybeFirstRow fromOnly $ + DB.query + db + [sql| + SELECT 1 FROM users + WHERE (user_id = ? AND local_display_name = ?) + OR contact_id = ? + LIMIT 1 + |] + (userId, localDisplayName, contactId) + when (isJust r) $ throwError $ SEProhibitedDeleteUser userId contactId + +safeDeleteLDN :: DB.Connection -> User -> ContactName -> IO () +safeDeleteLDN db User {userId} localDisplayName = do + DB.execute + db + [sql| + DELETE FROM display_names + WHERE user_id = ? AND local_display_name = ? + AND local_display_name NOT IN (SELECT local_display_name FROM users WHERE user_id = ?) + |] + (userId, localDisplayName, userId) + +type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe MemberId) + +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Text, Maybe Text, Maybe ImageData, Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Maybe CustomData, Maybe Int64) :. GroupMemberRow + +type GroupMemberRow = (Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, Maybe Preferences) :. (UTCTime, UTCTime) + +toGroupInfo :: VersionRangeChat -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo +toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, localAlias, description, image, enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. businessRow :. (uiThemes, customData, chatItemTTL) :. userMemberRow) = + let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr} + chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} + fullGroupPreferences = mergeGroupPreferences groupPreferences + groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} + businessChat = toBusinessChatInfo businessRow + in GroupInfo {groupId, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, chatTags, chatItemTTL, uiThemes, customData} + +toGroupMember :: Int64 -> GroupMemberRow -> GroupMember +toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences) :. (createdAt, updatedAt)) = + let memberProfile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} + memberSettings = GroupMemberSettings {showMessages} + blockedByAdmin = maybe False mrsBlocked memberRestriction_ + invitedBy = toInvitedBy userContactId invitedById + activeConn = Nothing + memberChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer + in GroupMember {..} + +toBusinessChatInfo :: BusinessChatInfoRow -> Maybe BusinessChatInfo +toBusinessChatInfo (Just chatType, Just businessId, Just customerId) = Just BusinessChatInfo {chatType, businessId, customerId} +toBusinessChatInfo _ = Nothing + +groupInfoQuery :: Query +groupInfoQuery = + [sql| + SELECT + -- GroupInfo + g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, + g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + -- GroupMember - membership + mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, + mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, + pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, + mu.created_at, mu.updated_at + FROM groups g + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + JOIN group_members mu ON mu.group_id = g.group_id + JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) + |] + +createChatTag :: DB.Connection -> User -> Maybe Text -> Text -> IO ChatTagId +createChatTag db User {userId} emoji text = do + DB.execute + db + [sql| + INSERT INTO chat_tags (user_id, chat_tag_emoji, chat_tag_text, tag_order) + VALUES (?,?,?, COALESCE((SELECT MAX(tag_order) + 1 FROM chat_tags WHERE user_id = ?), 1)) + |] + (userId, emoji, text, userId) + insertedRowId db + +deleteChatTag :: DB.Connection -> User -> ChatTagId -> IO () +deleteChatTag db User {userId} tId = + DB.execute + db + [sql| + DELETE FROM chat_tags + WHERE user_id = ? AND chat_tag_id = ? + |] + (userId, tId) + +updateChatTag :: DB.Connection -> User -> ChatTagId -> Maybe Text -> Text -> IO () +updateChatTag db User {userId} tId emoji text = + DB.execute + db + [sql| + UPDATE chat_tags + SET chat_tag_emoji = ?, chat_tag_text = ? + WHERE user_id = ? AND chat_tag_id = ? + |] + (emoji, text, userId, tId) + +updateChatTagOrder :: DB.Connection -> User -> ChatTagId -> Int -> IO () +updateChatTagOrder db User {userId} tId order = + DB.execute + db + [sql| + UPDATE chat_tags + SET tag_order = ? + WHERE user_id = ? AND chat_tag_id = ? + |] + (order, userId, tId) + +reorderChatTags :: DB.Connection -> User -> [ChatTagId] -> IO () +reorderChatTags db user tIds = + forM_ (zip [1 ..] tIds) $ \(order, tId) -> + updateChatTagOrder db user tId order + +getUserChatTags :: DB.Connection -> User -> IO [ChatTag] +getUserChatTags db User {userId} = + map toChatTag + <$> DB.query + db + [sql| + SELECT chat_tag_id, chat_tag_emoji, chat_tag_text + FROM chat_tags + WHERE user_id = ? + ORDER BY tag_order + |] + (Only userId) + where + toChatTag :: (ChatTagId, Maybe Text, Text) -> ChatTag + toChatTag (chatTagId, chatTagEmoji, chatTagText) = ChatTag {chatTagId, chatTagEmoji, chatTagText} + +getGroupChatTags :: DB.Connection -> GroupId -> IO [ChatTagId] +getGroupChatTags db groupId = + map fromOnly <$> DB.query db "SELECT chat_tag_id FROM chat_tags_chats WHERE group_id = ?" (Only groupId) + +addGroupChatTags :: DB.Connection -> GroupInfo -> IO GroupInfo +addGroupChatTags db g@GroupInfo {groupId} = do + chatTags <- getGroupChatTags db groupId + pure (g :: GroupInfo) {chatTags} diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs index c27675678e..e432343839 100644 --- a/src/Simplex/Chat/Terminal.hs +++ b/src/Simplex/Chat/Terminal.hs @@ -1,57 +1,100 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} module Simplex.Chat.Terminal where -import Control.Exception (handle, throwIO) import Control.Monad import qualified Data.List.NonEmpty as L -import Database.SQLite.Simple (SQLError (..)) -import qualified Database.SQLite.Simple as DB import Simplex.Chat (defaultChatConfig) import Simplex.Chat.Controller import Simplex.Chat.Core import Simplex.Chat.Help (chatWelcome) +import Simplex.Chat.Library.Commands (_defaultNtfServers) +import Simplex.Chat.Operators +import Simplex.Chat.Operators.Presets (operatorSimpleXChat) import Simplex.Chat.Options import Simplex.Chat.Terminal.Input import Simplex.Chat.Terminal.Output import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) -import Simplex.Messaging.Client (defaultNetworkConfig) +import Simplex.Messaging.Client (NetworkConfig (..), SMPProxyFallback (..), SMPProxyMode (..), defaultNetworkConfig) import Simplex.Messaging.Util (raceAny_) -import System.Exit (exitFailure) +#if !defined(dbPostgres) +import Control.Exception (handle, throwIO) +import qualified Data.ByteArray as BA +import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) +import Database.SQLite.Simple (SQLError (..)) +import qualified Database.SQLite.Simple as DB +import Simplex.Chat.Options.DB +import System.IO (hFlush, hSetEcho, stdin, stdout) +#endif terminalChatConfig :: ChatConfig terminalChatConfig = defaultChatConfig - { defaultServers = - DefaultAgentServers - { smp = - L.fromList - [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", - "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", - "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" - ], - ntf = ["ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,ntg7jdjy2i3qbib3sykiho3enekwiaqg3icctliqhtqcg6jmoh6cxiad.onion"], - xftp = defaultXFTPServers, - netCfg = defaultNetworkConfig + { presetServers = + PresetServers + { operators = + [ PresetOperator + { operator = Just operatorSimpleXChat, + smp = + map + (presetServer True) + [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", + "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", + "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" + ], + useSMP = 3, + xftp = map (presetServer True) $ L.toList defaultXFTPServers, + useXFTP = 3 + } + ], + ntf = _defaultNtfServers, + netCfg = + defaultNetworkConfig + { smpProxyMode = SPMUnknown, + smpProxyFallback = SPFAllowProtected + } }, deviceNameForRemote = "SimpleX CLI" } simplexChatTerminal :: WithTerminal t => ChatConfig -> ChatOpts -> t -> IO () -simplexChatTerminal cfg opts t = - handle checkDBKeyError . simplexChatCore cfg opts $ \u cc -> do - ct <- newChatTerminal t opts - when (firstTime cc) . printToTerminal ct $ chatWelcome u - runChatTerminal ct cc opts - -checkDBKeyError :: SQLError -> IO () -checkDBKeyError e = case sqlError e of - DB.ErrorNotADatabase -> do - putStrLn "Database file is invalid or you passed an incorrect encryption key" - exitFailure - _ -> throwIO e +simplexChatTerminal cfg options t = run options + where +#if defined(dbPostgres) + run opts = + simplexChatCore cfg opts $ \u cc -> do + ct <- newChatTerminal t opts + when (firstTime cc) . printToTerminal ct $ chatWelcome u + runChatTerminal ct cc opts +#else + run opts@ChatOpts {coreOptions = coreOptions@CoreChatOpts {dbOptions}} = + handle checkDBKeyError . simplexChatCore cfg opts $ \u cc -> do + ct <- newChatTerminal t opts + when (firstTime cc) . printToTerminal ct $ chatWelcome u + runChatTerminal ct cc opts + where + checkDBKeyError :: SQLError -> IO () + checkDBKeyError e = case sqlError e of + DB.ErrorNotADatabase -> do + putStrLn $ "Database file is invalid or " <> if BA.null (dbKey dbOptions) then "encrypted." else "you passed an incorrect encryption key." + run =<< getKeyOpts + _ -> throwIO e + getKeyOpts :: IO ChatOpts + getKeyOpts = do + putStr "Enter database encryption key (Ctrl-C to exit):" + hFlush stdout + hSetEcho stdin False + key <- getLine + hSetEcho stdin True + putStrLn "" + pure opts {coreOptions = coreOptions {dbOptions = dbOptions {dbKey = BA.convert $ encodeUtf8 $ T.pack key}}} +#endif runChatTerminal :: ChatTerminal -> ChatController -> ChatOpts -> IO () runChatTerminal ct cc opts = raceAny_ [runTerminalInput ct cc, runTerminalOutput ct cc opts, runInputLoop ct cc] diff --git a/src/Simplex/Chat/Terminal/Input.hs b/src/Simplex/Chat/Terminal/Input.hs index 7b96abc1ce..7c52f59a50 100644 --- a/src/Simplex/Chat/Terminal/Input.hs +++ b/src/Simplex/Chat/Terminal/Input.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} @@ -25,23 +26,27 @@ import Data.Maybe (isJust, isNothing) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) -import Database.SQLite.Simple (Only (..)) -import qualified Database.SQLite.Simple as SQL -import Database.SQLite.Simple.QQ (sql) import GHC.Weak (deRefWeak) -import Simplex.Chat import Simplex.Chat.Controller +import Simplex.Chat.Library.Commands import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent import Simplex.Chat.Styled import Simplex.Chat.Terminal.Output import Simplex.Chat.Types (User (..)) -import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore, withTransaction) -import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction) +import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Util (catchAll_, safeDecodeUtf8, whenM) import System.Exit (exitSuccess) import System.Terminal hiding (insertChars) import UnliftIO.STM +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (Only (..), Query, ToRow) +import Database.PostgreSQL.Simple.SqlQQ (sql) +#else +import Database.SQLite.Simple (Only (..), Query, ToRow) +import Database.SQLite.Simple.QQ (sql) +#endif getKey :: MonadTerminal m => m (Key, Modifiers) getKey = @@ -59,24 +64,24 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do rh' = if either (const False) allowRemoteCommand cmd then rh else Nothing unless (isMessage cmd) $ echo s r <- runReaderT (execChatCommand rh' bs) cc - processResp s cmd rh r + case r of + Right r' -> processResp cmd rh r' + Left _ -> when (isMessage cmd) $ echo s printRespToTerminal ct cc False rh r - startLiveMessage cmd r + mapM_ (startLiveMessage cmd) r where echo s = printToTerminal ct [plain s] - processResp s cmd rh = \case + processResp cmd rh = \case CRActiveUser u -> case rh of Nothing -> setActive ct "" Just rhId -> updateRemoteUser ct u rhId CRChatItems u chatName_ _ -> whenCurrUser cc u $ mapM_ (setActive ct . chatActiveTo) chatName_ - CRNewChatItem u (AChatItem _ SMDSnd cInfo _) -> whenCurrUser cc u $ setActiveChat ct cInfo + CRNewChatItems u ((AChatItem _ SMDSnd cInfo _) : _) -> whenCurrUser cc u $ setActiveChat ct cInfo CRChatItemUpdated u (AChatItem _ SMDSnd cInfo _) -> whenCurrUser cc u $ setActiveChat ct cInfo - CRChatItemDeleted u (AChatItem _ _ cInfo _) _ _ _ -> whenCurrUser cc u $ setActiveChat ct cInfo + CRChatItemsDeleted u ((ChatItemDeletion (AChatItem _ _ cInfo _) _) : _) _ _ -> whenCurrUser cc u $ setActiveChat ct cInfo CRContactDeleted u c -> whenCurrUser cc u $ unsetActiveContact ct c CRGroupDeletedUser u g -> whenCurrUser cc u $ unsetActiveGroup ct g CRSentGroupInvitation u g _ _ -> whenCurrUser cc u $ setActiveGroup ct g - CRChatCmdError _ _ -> when (isMessage cmd) $ echo s - CRChatError _ _ -> when (isMessage cmd) $ echo s CRCmdOk _ -> case cmd of Right APIDeleteUser {} -> setActive ct "" _ -> pure () @@ -86,11 +91,14 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do Right SendLiveMessage {} -> True Right SendFile {} -> True Right SendMessageQuote {} -> True + Right ForwardMessage {} -> True + Right ForwardLocalMessage {} -> True Right SendGroupMessageQuote {} -> True + Right ForwardGroupMessage {} -> True Right SendMessageBroadcast {} -> True _ -> False startLiveMessage :: Either a ChatCommand -> ChatResponse -> IO () - startLiveMessage (Right (SendLiveMessage chatName msg)) (CRNewChatItem _ (AChatItem cType SMDSnd _ ChatItem {meta = CIMeta {itemId}})) = do + startLiveMessage (Right (SendLiveMessage chatName msg)) (CRNewChatItems {chatItems = [AChatItem cType SMDSnd _ ChatItem {meta = CIMeta {itemId}}]}) = do whenM (isNothing <$> readTVarIO liveMessageState) $ do let s = T.unpack msg int = case cType of SCTGroup -> 5000000; _ -> 3000000 :: Int @@ -125,7 +133,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do updateLiveMessage typedMsg lm = case liveMessageToSend typedMsg lm of Just sentMsg -> sendUpdatedLiveMessage cc sentMsg lm True >>= \case - CRChatItemUpdated {} -> setLiveMessage lm {sentMsg, typedMsg} + Right CRChatItemUpdated {} -> setLiveMessage lm {sentMsg, typedMsg} _ -> do -- TODO print error setLiveMessage lm {typedMsg} @@ -139,10 +147,10 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do | otherwise = (s <> reverse (c : w), "") startLiveMessage _ _ = pure () -sendUpdatedLiveMessage :: ChatController -> String -> LiveMessage -> Bool -> IO ChatResponse +sendUpdatedLiveMessage :: ChatController -> String -> LiveMessage -> Bool -> IO (Either ChatError ChatResponse) sendUpdatedLiveMessage cc sentMsg LiveMessage {chatName, chatItemId} live = do let cmd = UpdateLiveMessage chatName chatItemId live $ T.pack sentMsg - either (CRChatCmdError Nothing) id <$> runExceptT (processChatCommand cmd) `runReaderT` cc + runExceptT (processChatCommand cmd) `runReaderT` cc runTerminalInput :: ChatTerminal -> ChatController -> IO () runTerminalInput ct cc = withChatTerm ct $ do @@ -220,7 +228,7 @@ data AutoComplete | ACCommand Text | ACNone -updateTermState :: Maybe User -> SQLiteStore -> String -> Bool -> Int -> (Key, Modifiers) -> TerminalState -> IO TerminalState +updateTermState :: Maybe User -> DBStore -> String -> Bool -> Int -> (Key, Modifiers) -> TerminalState -> IO TerminalState updateTermState user_ st chatPrefix live tw (key, ms) ts@TerminalState {inputString = s, inputPosition = p, autoComplete = acp} = case key of CharKey c | ms == mempty || ms == shiftKey -> pure $ insertChars $ charsWithContact [c] @@ -318,7 +326,7 @@ updateTermState user_ st chatPrefix live tw (key, ms) ts@TerminalState {inputStr getNameSfxs table pfx = getNameSfxs_ pfx (userId, pfx <> "%") $ "SELECT local_display_name FROM " <> table <> " WHERE user_id = ? AND local_display_name LIKE ?" - getNameSfxs_ :: SQL.ToRow p => Text -> p -> SQL.Query -> IO [String] + getNameSfxs_ :: ToRow p => Text -> p -> Query -> IO [String] getNameSfxs_ pfx ps q = withTransaction st (\db -> hasPfx pfx . map fromOnly <$> DB.query db q ps) `catchAll_` pure [] commands = diff --git a/src/Simplex/Chat/Terminal/Main.hs b/src/Simplex/Chat/Terminal/Main.hs new file mode 100644 index 0000000000..38b0e91a8a --- /dev/null +++ b/src/Simplex/Chat/Terminal/Main.hs @@ -0,0 +1,71 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} + +module Simplex.Chat.Terminal.Main where + +import Control.Concurrent (forkIO, threadDelay) +import Control.Concurrent.STM +import Control.Monad +import Data.Maybe (fromMaybe) +import Network.Socket +import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatError, ChatEvent (..), PresetServers (..), SimpleNetCfg (..), currentRemoteHost, versionNumber, versionString) +import Simplex.Chat.Core +import Simplex.Chat.Options +import Simplex.Chat.Options.DB +import Simplex.Chat.Terminal +import Simplex.Chat.View (ChatResponseEvent, smpProxyModeStr) +import Simplex.Messaging.Client (NetworkConfig (..), SocksMode (..)) +import System.Directory (getAppUserDataDirectory) +import System.Exit (exitFailure) +import System.Terminal (withTerminal) + +simplexChatCLI :: ChatConfig -> Maybe (ServiceName -> ChatConfig -> ChatOpts -> IO ()) -> IO () +simplexChatCLI cfg server_ = do + appDir <- getAppUserDataDirectory "simplex" + opts <- getChatOpts appDir "simplex_v1" + simplexChatCLI' cfg opts server_ + +simplexChatCLI' :: ChatConfig -> ChatOpts -> Maybe (ServiceName -> ChatConfig -> ChatOpts -> IO ()) -> IO () +simplexChatCLI' cfg opts@ChatOpts {chatCmd, chatCmdLog, chatCmdDelay, chatServerPort} server_ = do + if null chatCmd + then case chatServerPort of + Just chatPort -> case server_ of + Just server -> server chatPort cfg opts + Nothing -> putStrLn "Not allowed to run as a WebSockets server" >> exitFailure + _ -> runCLI + else simplexChatCore cfg opts runCommand + where + runCLI = do + welcome cfg opts + t <- withTerminal pure + simplexChatTerminal cfg opts t + runCommand user cc = do + when (chatCmdLog /= CCLNone) . void . forkIO . forever $ do + (_, r) <- atomically . readTBQueue $ outputQ cc + case r of + Right CEvtNewChatItems {} -> printResponse r + _ -> when (chatCmdLog == CCLAll) $ printResponse r + sendChatCmdStr cc chatCmd >>= printResponse + threadDelay $ chatCmdDelay * 1000000 + where + printResponse :: ChatResponseEvent r => Either ChatError r -> IO () + printResponse r = do + rh <- readTVarIO $ currentRemoteHost cc + printResponseEvent (rh, Just user) cfg r + +welcome :: ChatConfig -> ChatOpts -> IO () +welcome ChatConfig {presetServers = PresetServers {netCfg}} ChatOpts {coreOptions = CoreChatOpts {dbOptions, simpleNetCfg = SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_}}} = + mapM_ + putStrLn + [ versionString versionNumber, + "db: " <> dbString dbOptions, + maybe + "direct network connection - use `/network` command or `-x` CLI option to connect via SOCKS5 at :9050" + ((\sp -> "using SOCKS5 proxy " <> sp <> if socksMode == SMOnion then " for onion servers ONLY." else " for ALL servers.") . show) + socksProxy, + smpProxyModeStr + (fromMaybe (smpProxyMode netCfg) smpProxyMode_) + (fromMaybe (smpProxyFallback netCfg) smpProxyFallback_), + "type \"/help\" or \"/h\" for usage info" + ] diff --git a/src/Simplex/Chat/Terminal/Output.hs b/src/Simplex/Chat/Terminal/Output.hs index be8aa12cfe..79fc08397c 100644 --- a/src/Simplex/Chat/Terminal/Output.hs +++ b/src/Simplex/Chat/Terminal/Output.hs @@ -3,9 +3,11 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} module Simplex.Chat.Terminal.Output where @@ -20,8 +22,8 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Time.Clock (getCurrentTime) import Data.Time.LocalTime (getCurrentTimeZone) -import Simplex.Chat (execChatCommand, processChatCommand) import Simplex.Chat.Controller +import Simplex.Chat.Library.Commands (execChatCommand, processChatCommand) import Simplex.Chat.Markdown import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent (CIContent (..), SMsgDirection (..)) @@ -145,38 +147,39 @@ withTermLock ChatTerminal {termLock} action = do runTerminalOutput :: ChatTerminal -> ChatController -> ChatOpts -> IO () runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} ChatOpts {markRead} = do forever $ do - (_, outputRH, r) <- atomically $ readTBQueue outputQ - case r of - CRNewChatItem u ci -> when markRead $ markChatItemRead u ci - CRChatItemUpdated u ci -> when markRead $ markChatItemRead u ci - CRRemoteHostConnected {remoteHost = RemoteHostInfo {remoteHostId}} -> getRemoteUser remoteHostId - CRRemoteHostStopped {remoteHostId_} -> mapM_ removeRemoteUser remoteHostId_ + (outputRH, r_) <- atomically $ readTBQueue outputQ + forM_ r_ $ \case + CEvtNewChatItems u (ci : _) -> when markRead $ markChatItemRead u ci -- At the moment of writing received items are created one at a time + CEvtChatItemUpdated u ci -> when markRead $ markChatItemRead u ci + CEvtRemoteHostConnected {remoteHost = RemoteHostInfo {remoteHostId}} -> getRemoteUser remoteHostId + CEvtRemoteHostStopped {remoteHostId_} -> mapM_ removeRemoteUser remoteHostId_ _ -> pure () - let printResp = case logFilePath of - Just path -> if logResponseToFile r then logResponse path else printToTerminal ct + let printEvent = case logFilePath of + Just path -> if either (const True) logEventToFile r_ then logResponse path else printToTerminal ct _ -> printToTerminal ct liveItems <- readTVarIO showLiveItems - responseString ct cc liveItems outputRH r >>= printResp - responseNotification ct cc r + responseString ct cc liveItems outputRH r_ >>= printEvent + mapM_ (chatEventNotification ct cc) r_ where markChatItemRead u (AChatItem _ _ chat ci@ChatItem {chatDir, meta = CIMeta {itemStatus}}) = - case (chatDirNtf u chat chatDir (isMention ci), itemStatus) of + case (chatDirNtf u chat chatDir (isUserMention ci), itemStatus) of (True, CISRcvNew) -> do let itemId = chatItemId' ci chatRef = chatInfoToRef chat - void $ runReaderT (runExceptT $ processChatCommand (APIChatRead chatRef (Just (itemId, itemId)))) cc + void $ runReaderT (runExceptT $ processChatCommand (APIChatItemsRead chatRef [itemId])) cc _ -> pure () logResponse path s = withFile path AppendMode $ \h -> mapM_ (hPutStrLn h . unStyle) s getRemoteUser rhId = runReaderT (execChatCommand (Just rhId) "/user") cc >>= \case - CRActiveUser {user} -> updateRemoteUser ct user rhId + Right CRActiveUser {user} -> updateRemoteUser ct user rhId cr -> logError $ "Unexpected reply while getting remote user: " <> tshow cr removeRemoteUser rhId = atomically $ TM.delete rhId (currentRemoteUsers ct) -responseNotification :: ChatTerminal -> ChatController -> ChatResponse -> IO () -responseNotification t@ChatTerminal {sendNotification} cc = \case - CRNewChatItem u (AChatItem _ SMDRcv cInfo ci@ChatItem {chatDir, content = CIRcvMsgContent mc, formattedText}) -> - when (chatDirNtf u cInfo chatDir $ isMention ci) $ do +chatEventNotification :: ChatTerminal -> ChatController -> ChatEvent -> IO () +chatEventNotification t@ChatTerminal {sendNotification} cc = \case + -- At the moment of writing received items are created one at a time + CEvtNewChatItems u ((AChatItem _ SMDRcv cInfo ci@ChatItem {chatDir, content = CIRcvMsgContent mc, formattedText}) : _) -> + when (chatDirNtf u cInfo chatDir $ isUserMention ci) $ do whenCurrUser cc u $ setActiveChat t cInfo case (cInfo, chatDir) of (DirectChat ct, _) -> sendNtf (viewContactName ct <> "> ", text) @@ -184,27 +187,29 @@ responseNotification t@ChatTerminal {sendNotification} cc = \case _ -> pure () where text = msgText mc formattedText - CRChatItemUpdated u (AChatItem _ SMDRcv cInfo ci@ChatItem {chatDir, content = CIRcvMsgContent _}) -> - whenCurrUser cc u $ when (chatDirNtf u cInfo chatDir $ isMention ci) $ setActiveChat t cInfo - CRContactConnected u ct _ -> when (contactNtf u ct False) $ do + CEvtChatItemUpdated u (AChatItem _ SMDRcv cInfo ci@ChatItem {chatDir, content = CIRcvMsgContent _}) -> + whenCurrUser cc u $ when (chatDirNtf u cInfo chatDir $ isUserMention ci) $ setActiveChat t cInfo + CEvtContactConnected u ct _ -> when (contactNtf u ct False) $ do whenCurrUser cc u $ setActiveContact t ct sendNtf (viewContactName ct <> "> ", "connected") - CRContactAnotherClient u ct -> do + CEvtContactSndReady u ct -> + whenCurrUser cc u $ setActiveContact t ct + CEvtContactAnotherClient u ct -> do whenCurrUser cc u $ unsetActiveContact t ct when (contactNtf u ct False) $ sendNtf (viewContactName ct <> "> ", "connected to another client") - CRContactsDisconnected srv _ -> serverNtf srv "disconnected" - CRContactsSubscribed srv _ -> serverNtf srv "connected" - CRReceivedGroupInvitation u g ct _ _ -> + CEvtContactsDisconnected srv _ -> serverNtf srv "disconnected" + CEvtContactsSubscribed srv _ -> serverNtf srv "connected" + CEvtReceivedGroupInvitation u g ct _ _ -> when (contactNtf u ct False) $ sendNtf ("#" <> viewGroupName g <> " " <> viewContactName ct <> "> ", "invited you to join the group") - CRUserJoinedGroup u g _ -> when (groupNtf u g False) $ do + CEvtUserJoinedGroup u g _ -> when (groupNtf u g False) $ do whenCurrUser cc u $ setActiveGroup t g sendNtf ("#" <> viewGroupName g, "you are connected to group") - CRJoinedGroupMember u g m -> + CEvtJoinedGroupMember u g m -> when (groupNtf u g False) $ sendNtf ("#" <> viewGroupName g, "member " <> viewMemberName m <> " is connected") - CRConnectedToGroupMember u g m _ -> + CEvtConnectedToGroupMember u g m _ -> when (groupNtf u g False) $ sendNtf ("#" <> viewGroupName g, "member " <> viewMemberName m <> " is connected") - CRReceivedContactRequest u UserContactRequest {localDisplayName = n} -> + CEvtReceivedContactRequest u UserContactRequest {localDisplayName = n} -> when (userNtf u) $ sendNtf (viewName n <> ">", "wants to connect to you") _ -> pure () where @@ -267,15 +272,17 @@ whenCurrUser cc u a = do where sameUser User {userId = uId} = maybe False $ \User {userId} -> userId == uId -printRespToTerminal :: ChatTerminal -> ChatController -> Bool -> Maybe RemoteHostId -> ChatResponse -> IO () +printRespToTerminal :: ChatTerminal -> ChatController -> Bool -> Maybe RemoteHostId -> Either ChatError ChatResponse -> IO () printRespToTerminal ct cc liveItems outputRH r = responseString ct cc liveItems outputRH r >>= printToTerminal ct -responseString :: ChatTerminal -> ChatController -> Bool -> Maybe RemoteHostId -> ChatResponse -> IO [StyledString] -responseString ct cc liveItems outputRH r = do - cu <- getCurrentUser ct cc - ts <- getCurrentTime - tz <- getCurrentTimeZone - pure $ responseToView cu (config cc) liveItems ts tz outputRH r +responseString :: forall r. ChatResponseEvent r => ChatTerminal -> ChatController -> Bool -> Maybe RemoteHostId -> Either ChatError r -> IO [StyledString] +responseString ct cc liveItems outputRH = \case + Right r -> do + cu <- getCurrentUser ct cc + ts <- getCurrentTime + tz <- getCurrentTimeZone + pure $ responseToView cu (config cc) liveItems ts tz outputRH r + Left e -> pure $ chatErrorToView (isCommandResponse @r) (config cc) e updateRemoteUser :: ChatTerminal -> User -> RemoteHostId -> IO () updateRemoteUser ct user rhId = atomically $ TM.insert rhId user (currentRemoteUsers ct) diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index de2dfa8b58..0063f711c7 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -1,18 +1,21 @@ {-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE CPP #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilyDependencies #-} @@ -23,36 +26,49 @@ module Simplex.Chat.Types where +import Control.Applicative ((<|>)) import Crypto.Number.Serialize (os2ip) -import Data.Aeson (FromJSON (..), ToJSON (..), (.:), (.=)) +import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ -import qualified Data.Aeson.Types as JT import qualified Data.Attoparsec.ByteString.Char8 as A +import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString, pack, unpack) -import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Lazy as LB +import Data.Functor (($>)) import Data.Int (Int64) import Data.Maybe (isJust) import Data.Text (Text) import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) import Data.Time.Clock (UTCTime) import Data.Typeable (Typeable) -import Database.SQLite.Simple (ResultError (..), SQLData (..)) -import Database.SQLite.Simple.FromField (FromField (..), returnError) -import Database.SQLite.Simple.Internal (Field (..)) -import Database.SQLite.Simple.Ok -import Database.SQLite.Simple.ToField (ToField (..)) +import Data.Word (Word16) import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared +import Simplex.Chat.Types.UITheme import Simplex.Chat.Types.Util import Simplex.FileTransfer.Description (FileDigest) -import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, SAEntity (..), UserId) +import Simplex.FileTransfer.Types (RcvFileId, SndFileId) +import Simplex.Messaging.Agent.Protocol (ACorrId, AEventTag (..), AEvtTag (..), ConnId, ConnShortLink, ConnectionLink, ConnectionMode (..), ConnectionRequestUri, CreatedConnLink, InvitationId, SAEntity (..), UserId) +import Simplex.Messaging.Agent.Store.DB (Binary (..), blobFieldDecoder, fromTextField_) import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff) import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, sumTypeJSON, taggedObjectJSON) -import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI) -import Simplex.Messaging.Util ((<$?>)) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON) +import Simplex.Messaging.Util (decodeJSON, encodeJSON, safeDecodeUtf8, (<$?>)) import Simplex.Messaging.Version +import Simplex.Messaging.Version.Internal +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (ResultError (..)) +import Database.PostgreSQL.Simple.FromField (FromField(..), FieldParser, returnError) +import Database.PostgreSQL.Simple.ToField (ToField (..)) +#else +import Database.SQLite.Simple (ResultError (..)) +import Database.SQLite.Simple.FromField (FromField (..), FieldParser, returnError) +import Database.SQLite.Simple.ToField (ToField (..)) +#endif class IsContact a where contactId' :: a -> ContactId @@ -61,21 +77,21 @@ class IsContact a where preferences' :: a -> Maybe Preferences instance IsContact User where - contactId' u = u.userContactId + contactId' User {userContactId} = userContactId {-# INLINE contactId' #-} - profile' u = u.profile + profile' User {profile} = profile {-# INLINE profile' #-} - localDisplayName' u = u.localDisplayName + localDisplayName' User {localDisplayName} = localDisplayName {-# INLINE localDisplayName' #-} preferences' User {profile = LocalProfile {preferences}} = preferences {-# INLINE preferences' #-} instance IsContact Contact where - contactId' c = c.contactId + contactId' Contact {contactId} = contactId {-# INLINE contactId' #-} - profile' c = c.profile + profile' Contact {profile} = profile {-# INLINE profile' #-} - localDisplayName' c = c.localDisplayName + localDisplayName' Contact {localDisplayName} = localDisplayName {-# INLINE localDisplayName' #-} preferences' Contact {profile = LocalProfile {preferences}} = preferences {-# INLINE preferences' #-} @@ -95,7 +111,7 @@ instance ToJSON AgentUserId where toJSON = strToJSON toEncoding = strToJEncoding -instance FromField AgentUserId where fromField f = AgentUserId <$> fromField f +deriving newtype instance FromField AgentUserId instance ToField AgentUserId where toField (AgentUserId uId) = toField uId @@ -110,26 +126,27 @@ data User = User profile :: LocalProfile, fullPreferences :: FullPreferences, activeUser :: Bool, + activeOrder :: Int64, viewPwdHash :: Maybe UserPwdHash, showNtfs :: Bool, sendRcptsContacts :: Bool, - sendRcptsSmallGroups :: Bool + sendRcptsSmallGroups :: Bool, + userMemberProfileUpdatedAt :: Maybe UTCTime, + uiThemes :: Maybe UIThemeEntityOverrides } deriving (Show) data NewUser = NewUser { profile :: Maybe Profile, - sameServers :: Bool, pastTimestamp :: Bool } deriving (Show) newtype B64UrlByteString = B64UrlByteString ByteString deriving (Eq, Show) + deriving newtype (FromField) -instance FromField B64UrlByteString where fromField f = B64UrlByteString <$> fromField f - -instance ToField B64UrlByteString where toField (B64UrlByteString m) = toField m +instance ToField B64UrlByteString where toField (B64UrlByteString m) = toField $ Binary m instance StrEncoding B64UrlByteString where strEncode (B64UrlByteString m) = strEncode m @@ -155,6 +172,8 @@ type ContactId = Int64 type ProfileId = Int64 +type ChatTagId = Int64 + data Contact = Contact { contactId :: ContactId, localDisplayName :: ContactName, @@ -170,10 +189,29 @@ data Contact = Contact updatedAt :: UTCTime, chatTs :: Maybe UTCTime, contactGroupMemberId :: Maybe GroupMemberId, - contactGrpInvSent :: Bool + contactGrpInvSent :: Bool, + chatTags :: [ChatTagId], + chatItemTTL :: Maybe Int64, + uiThemes :: Maybe UIThemeEntityOverrides, + chatDeleted :: Bool, + customData :: Maybe CustomData } deriving (Eq, Show) +newtype CustomData = CustomData J.Object + deriving (Eq, Show) + +instance ToJSON CustomData where + toJSON (CustomData v) = toJSON v + toEncoding (CustomData v) = toEncoding v + +instance FromJSON CustomData where + parseJSON = J.withObject "CustomData" (pure . CustomData) + +instance ToField CustomData where toField (CustomData v) = toField . Binary . LB.toStrict $ J.encode v + +instance FromField CustomData where fromField = blobFieldDecoder J.eitherDecodeStrict + contactConn :: Contact -> Maybe Connection contactConn Contact {activeConn} = activeConn @@ -182,6 +220,8 @@ contactConnId c = aConnId <$> contactConn c type IncognitoEnabled = Bool +type CreateShortLink = Bool + contactConnIncognito :: Contact -> IncognitoEnabled contactConnIncognito = maybe False connIncognito . contactConn @@ -196,7 +236,7 @@ directOrUsed ct@Contact {contactUsed} = contactDirect ct || contactUsed anyDirectOrUsed :: Contact -> Bool -anyDirectOrUsed Contact {contactUsed, activeConn} = ((\c -> c.connLevel) <$> activeConn) == Just 0 || contactUsed +anyDirectOrUsed Contact {contactUsed, activeConn} = ((\Connection {connLevel} -> connLevel) <$> activeConn) == Just 0 || contactUsed contactReady :: Contact -> Bool contactReady Contact {activeConn} = maybe False connReady activeConn @@ -205,14 +245,18 @@ contactActive :: Contact -> Bool contactActive Contact {contactStatus} = contactStatus == CSActive contactDeleted :: Contact -> Bool -contactDeleted Contact {contactStatus} = contactStatus == CSDeleted +contactDeleted Contact {contactStatus} = contactStatus == CSDeleted || contactStatus == CSDeletedByUser contactSecurityCode :: Contact -> Maybe SecurityCode contactSecurityCode Contact {activeConn} = connectionCode =<< activeConn +contactPQEnabled :: Contact -> PQEncryption +contactPQEnabled Contact {activeConn} = maybe PQEncOff connPQEnabled activeConn + data ContactStatus = CSActive - | CSDeleted -- contact deleted by contact + | CSDeleted + | CSDeletedByUser deriving (Eq, Show, Ord) instance FromField ContactStatus where fromField = fromTextField_ textDecode @@ -230,10 +274,12 @@ instance TextEncoding ContactStatus where textDecode = \case "active" -> Just CSActive "deleted" -> Just CSDeleted + "deletedByUser" -> Just CSDeletedByUser _ -> Nothing textEncode = \case CSActive -> "active" CSDeleted -> "deleted" + CSDeletedByUser -> "deletedByUser" data ContactRef = ContactRef { contactId :: ContactId, @@ -269,24 +315,25 @@ userContactGroupId UserContact {groupId} = groupId data UserContactRequest = UserContactRequest { contactRequestId :: Int64, agentInvitationId :: AgentInvId, + contactId_ :: Maybe ContactId, userContactLinkId :: Int64, agentContactConnId :: AgentConnId, -- connection id of user contact - cReqChatVRange :: JVersionRange, + cReqChatVRange :: VersionRangeChat, localDisplayName :: ContactName, profileId :: Int64, profile :: Profile, createdAt :: UTCTime, updatedAt :: UTCTime, - xContactId :: Maybe XContactId + xContactId :: Maybe XContactId, + pqSupport :: PQSupport } deriving (Eq, Show) newtype XContactId = XContactId ByteString deriving (Eq, Show) + deriving newtype (FromField) -instance FromField XContactId where fromField f = XContactId <$> fromField f - -instance ToField XContactId where toField (XContactId m) = toField m +instance ToField XContactId where toField (XContactId m) = toField $ Binary m instance StrEncoding XContactId where strEncode (XContactId m) = strEncode m @@ -302,10 +349,9 @@ instance ToJSON XContactId where newtype ConnReqUriHash = ConnReqUriHash {unConnReqUriHash :: ByteString} deriving (Eq, Show) + deriving newtype (FromField) -instance FromField ConnReqUriHash where fromField f = ConnReqUriHash <$> fromField f - -instance ToField ConnReqUriHash where toField (ConnReqUriHash m) = toField m +instance ToField ConnReqUriHash where toField (ConnReqUriHash m) = toField $ Binary m instance StrEncoding ConnReqUriHash where strEncode (ConnReqUriHash m) = strEncode m @@ -319,12 +365,14 @@ instance ToJSON ConnReqUriHash where toJSON = strToJSON toEncoding = strToJEncoding -data ContactOrRequest = CORContact Contact | CORRequest UserContactRequest +data ChatOrRequest = CORContact Contact | CORGroup GroupInfo | CORRequest UserContactRequest type UserName = Text type ContactName = Text +type MemberName = Text + type GroupName = Text optionalFullName :: ContactName -> Text -> Text @@ -332,6 +380,26 @@ optionalFullName displayName fullName | T.null fullName || displayName == fullName = "" | otherwise = " (" <> fullName <> ")" +data ShortGroup = ShortGroup + { shortInfo :: ShortGroupInfo, + members :: [ShortGroupMember] + } + +data ShortGroupInfo = ShortGroupInfo + { groupId :: GroupId, + groupName :: GroupName, + membershipStatus :: GroupMemberStatus + } + deriving (Eq, Show) + +data ShortGroupMember = ShortGroupMember + { groupMemberId :: GroupMemberId, + groupId :: GroupId, + memberName :: ContactName, + connId :: AgentConnId + } + deriving (Show) + data Group = Group {groupInfo :: GroupInfo, members :: [GroupMember]} deriving (Eq, Show) @@ -341,16 +409,40 @@ data GroupInfo = GroupInfo { groupId :: GroupId, localDisplayName :: GroupName, groupProfile :: GroupProfile, + localAlias :: Text, + businessChat :: Maybe BusinessChatInfo, fullGroupPreferences :: FullGroupPreferences, membership :: GroupMember, - hostConnCustomUserProfileId :: Maybe ProfileId, chatSettings :: ChatSettings, createdAt :: UTCTime, updatedAt :: UTCTime, - chatTs :: Maybe UTCTime + chatTs :: Maybe UTCTime, + userMemberProfileSentAt :: Maybe UTCTime, + chatTags :: [ChatTagId], + chatItemTTL :: Maybe Int64, + uiThemes :: Maybe UIThemeEntityOverrides, + customData :: Maybe CustomData } deriving (Eq, Show) +data BusinessChatType + = BCBusiness -- used on the customer side + | BCCustomer -- used on the business side + deriving (Eq, Show) + +instance TextEncoding BusinessChatType where + textEncode = \case + BCBusiness -> "business" + BCCustomer -> "customer" + textDecode = \case + "business" -> Just BCBusiness + "customer" -> Just BCCustomer + _ -> Nothing + +instance FromField BusinessChatType where fromField = fromTextField_ textDecode + +instance ToField BusinessChatType where toField = toField . textEncode + groupName' :: GroupInfo -> GroupName groupName' GroupInfo {localDisplayName = g} = g @@ -359,12 +451,12 @@ data GroupSummary = GroupSummary } deriving (Show) -data ContactOrGroup = CGContact Contact | CGGroup Group +data ContactOrGroup = CGContact Contact | CGGroup GroupInfo [GroupMember] contactAndGroupIds :: ContactOrGroup -> (Maybe ContactId, Maybe GroupId) contactAndGroupIds = \case CGContact Contact {contactId} -> (Just contactId, Nothing) - CGGroup (Group GroupInfo {groupId} _) -> (Nothing, Just groupId) + CGGroup GroupInfo {groupId} _ -> (Nothing, Just groupId) -- TODO when more settings are added we should create another type to allow partial setting updates (with all Maybe properties) data ChatSettings = ChatSettings @@ -401,22 +493,29 @@ msgFilterIntP = \case 2 -> Just MFMentions _ -> Just MFAll -fromIntField_ :: Typeable a => (Int64 -> Maybe a) -> Field -> Ok a -fromIntField_ fromInt = \case - f@(Field (SQLInteger i) _) -> - case fromInt i of - Just x -> Ok x - _ -> returnError ConversionFailed f ("invalid integer: " <> show i) - f -> returnError ConversionFailed f "expecting SQLInteger column type" +fromIntField_ :: Typeable a => (Int64 -> Maybe a) -> FieldParser a +#if defined(dbPostgres) +fromIntField_ fromInt f val = fromField f val >>= parseInt +#else +fromIntField_ fromInt f = fromField f >>= parseInt +#endif + where + parseInt i = case fromInt i of + Just x -> pure x + _ -> returnError ConversionFailed f $ "invalid integer: " <> show i featureAllowed :: SChatFeature f -> (PrefEnabled -> Bool) -> Contact -> Bool featureAllowed feature forWhom Contact {mergedPreferences} = let ContactUserPreference {enabled} = getContactUserPreference feature mergedPreferences in forWhom enabled -groupFeatureAllowed :: GroupFeatureI f => SGroupFeature f -> GroupInfo -> Bool +groupFeatureAllowed :: GroupFeatureNoRoleI f => SGroupFeature f -> GroupInfo -> Bool groupFeatureAllowed feature gInfo = groupFeatureAllowed' feature $ fullGroupPreferences gInfo +groupFeatureMemberAllowed :: GroupFeatureRoleI f => SGroupFeature f -> GroupMember -> GroupInfo -> Bool +groupFeatureMemberAllowed feature GroupMember {memberRole} = + groupFeatureMemberAllowed' feature memberRole . fullGroupPreferences + mergeUserChatPrefs :: User -> Contact -> FullPreferences mergeUserChatPrefs user ct = mergeUserChatPrefs' user (contactConnIncognito ct) (userPreferences ct) @@ -462,7 +561,7 @@ data Profile = Profile { displayName :: ContactName, fullName :: Text, image :: Maybe ImageData, - contactLink :: Maybe ConnReqContact, + contactLink :: Maybe ConnLinkContact, preferences :: Maybe Preferences -- fields that should not be read into this data type to prevent sending them as part of profile to contacts: -- - contact_profile_id @@ -482,6 +581,10 @@ profilesMatch LocalProfile {displayName = n2, fullName = fn2, image = i2} = n1 == n2 && fn1 == fn2 && i1 == i2 +redactedMemberProfile :: Profile -> Profile +redactedMemberProfile Profile {displayName, fullName, image} = + Profile {displayName, fullName, image, contactLink = Nothing, preferences = Nothing} + data IncognitoProfile = NewIncognito Profile | ExistingIncognito LocalProfile type LocalAlias = Text @@ -491,7 +594,7 @@ data LocalProfile = LocalProfile displayName :: ContactName, fullName :: Text, image :: Maybe ImageData, - contactLink :: Maybe ConnReqContact, + contactLink :: Maybe ConnLinkContact, preferences :: Maybe Preferences, localAlias :: LocalAlias } @@ -513,10 +616,24 @@ data GroupProfile = GroupProfile fullName :: Text, description :: Maybe Text, image :: Maybe ImageData, - groupPreferences :: Maybe GroupPreferences + groupPreferences :: Maybe GroupPreferences, + memberAdmission :: Maybe GroupMemberAdmission } deriving (Eq, Show) +data GroupMemberAdmission = GroupMemberAdmission + { -- names :: Maybe MemberCriteria, + -- captcha :: Maybe MemberCriteria, + review :: Maybe MemberCriteria + } + deriving (Eq, Show) + +data MemberCriteria = MCAll + deriving (Eq, Show) + +emptyGroupMemberAdmission :: GroupMemberAdmission +emptyGroupMemberAdmission = GroupMemberAdmission Nothing + newtype ImageData = ImageData Text deriving (Eq, Show) @@ -529,16 +646,15 @@ instance ToJSON ImageData where instance ToField ImageData where toField (ImageData t) = toField t -instance FromField ImageData where fromField = fmap ImageData . fromField +deriving newtype instance FromField ImageData data CReqClientData = CRDataGroup {groupLinkId :: GroupLinkId} newtype GroupLinkId = GroupLinkId {unGroupLinkId :: ByteString} -- used to identify invitation via group link deriving (Eq, Show) + deriving newtype (FromField) -instance FromField GroupLinkId where fromField f = GroupLinkId <$> fromField f - -instance ToField GroupLinkId where toField (GroupLinkId g) = toField g +instance ToField GroupLinkId where toField (GroupLinkId g) = toField $ Binary g instance StrEncoding GroupLinkId where strEncode (GroupLinkId g) = strEncode g @@ -557,7 +673,9 @@ data GroupInvitation = GroupInvitation invitedMember :: MemberIdRole, connRequest :: ConnReqInvitation, groupProfile :: GroupProfile, - groupLinkId :: Maybe GroupLinkId + business :: Maybe BusinessChatInfo, + groupLinkId :: Maybe GroupLinkId, + groupSize :: Maybe Int } deriving (Eq, Show) @@ -565,10 +683,48 @@ data GroupLinkInvitation = GroupLinkInvitation { fromMember :: MemberIdRole, fromMemberName :: ContactName, invitedMember :: MemberIdRole, - groupProfile :: GroupProfile + groupProfile :: GroupProfile, + accepted :: Maybe GroupAcceptance, + business :: Maybe BusinessChatInfo, + groupSize :: Maybe Int } deriving (Eq, Show) +data GroupLinkRejection = GroupLinkRejection + { fromMember :: MemberIdRole, + invitedMember :: MemberIdRole, + groupProfile :: GroupProfile, + rejectionReason :: GroupRejectionReason + } + deriving (Eq, Show) + +data GroupRejectionReason + = GRRLongName + | GRRBlockedName + | GRRUnknown {text :: Text} + deriving (Eq, Show) + +instance FromField GroupRejectionReason where fromField = blobFieldDecoder strDecode + +instance ToField GroupRejectionReason where toField = toField . strEncode + +instance StrEncoding GroupRejectionReason where + strEncode = \case + GRRLongName -> "long_name" + GRRBlockedName -> "blocked_name" + GRRUnknown text -> encodeUtf8 text + strP = + "long_name" $> GRRLongName + <|> "blocked_name" $> GRRBlockedName + <|> GRRUnknown . safeDecodeUtf8 <$> A.takeByteString + +instance FromJSON GroupRejectionReason where + parseJSON = strParseJSON "GroupRejectionReason" + +instance ToJSON GroupRejectionReason where + toJSON = strToJSON + toEncoding = strToJEncoding + data MemberIdRole = MemberIdRole { memberId :: MemberId, memberRole :: GroupMemberRole @@ -589,11 +745,64 @@ data MemberInfo = MemberInfo } deriving (Eq, Show) +data BusinessChatInfo = BusinessChatInfo + { chatType :: BusinessChatType, + businessId :: MemberId, + customerId :: MemberId + } + deriving (Eq, Show) + memberInfo :: GroupMember -> MemberInfo memberInfo GroupMember {memberId, memberRole, memberProfile, activeConn} = - MemberInfo memberId memberRole cvr (fromLocalProfile memberProfile) - where - cvr = ChatVersionRange . fromJVersionRange . peerChatVRange <$> activeConn + MemberInfo + { memberId, + memberRole, + v = ChatVersionRange . peerChatVRange <$> activeConn, + profile = redactedMemberProfile $ fromLocalProfile memberProfile + } + +data MemberRestrictionStatus + = MRSBlocked + | MRSUnrestricted + | MRSUnknown Text + deriving (Eq, Show) + +instance FromField MemberRestrictionStatus where fromField = blobFieldDecoder strDecode + +instance ToField MemberRestrictionStatus where toField = toField . strEncode + +instance StrEncoding MemberRestrictionStatus where + strEncode = \case + MRSBlocked -> "blocked" + MRSUnrestricted -> "unrestricted" + MRSUnknown tag -> encodeUtf8 tag + strDecode s = Right $ case s of + "blocked" -> MRSBlocked + "unrestricted" -> MRSUnrestricted + tag -> MRSUnknown $ safeDecodeUtf8 tag + strP = strDecode <$?> A.takeByteString + +instance FromJSON MemberRestrictionStatus where + parseJSON = strParseJSON "MemberRestrictionStatus" + +instance ToJSON MemberRestrictionStatus where + toJSON = strToJSON + toEncoding = strToJEncoding + +mrsBlocked :: MemberRestrictionStatus -> Bool +mrsBlocked = \case + MRSBlocked -> True + _ -> False + +data MemberRestrictions = MemberRestrictions + { restriction :: MemberRestrictionStatus + } + deriving (Eq, Show) + +memberRestrictions :: GroupMember -> Maybe MemberRestrictions +memberRestrictions m + | blockedByAdmin m = Just MemberRestrictions {restriction = MRSBlocked} + | otherwise = Nothing data ReceivedGroupInvitation = ReceivedGroupInvitation { fromMember :: GroupMember, @@ -614,6 +823,7 @@ data GroupMember = GroupMember memberCategory :: GroupMemberCategory, memberStatus :: GroupMemberStatus, memberSettings :: GroupMemberSettings, + blockedByAdmin :: Bool, invitedBy :: InvitedBy, invitedByGroupMemberId :: Maybe GroupMemberId, localDisplayName :: ContactName, @@ -629,7 +839,9 @@ data GroupMember = GroupMember -- member chat protocol version range; if member has active connection, its version range is preferred; -- for membership current supportedChatVRange is set, it's not updated on protocol version increase in database, -- but it's correctly set on read (see toGroupInfo) - memberChatVRange :: JVersionRange + memberChatVRange :: VersionRangeChat, + createdAt :: UTCTime, + updatedAt :: UTCTime } deriving (Eq, Show) @@ -646,11 +858,16 @@ memberConn GroupMember {activeConn} = activeConn memberConnId :: GroupMember -> Maybe ConnId memberConnId GroupMember {activeConn} = aConnId <$> activeConn -memberChatVRange' :: GroupMember -> VersionRange -memberChatVRange' GroupMember {activeConn, memberChatVRange} = - fromJVersionRange $ case activeConn of - Just Connection {peerChatVRange} -> peerChatVRange - Nothing -> memberChatVRange +sameMemberId :: MemberId -> GroupMember -> Bool +sameMemberId memId GroupMember {memberId} = memId == memberId + +memberChatVRange' :: GroupMember -> VersionRangeChat +memberChatVRange' GroupMember {activeConn, memberChatVRange} = case activeConn of + Just Connection {peerChatVRange} -> peerChatVRange + Nothing -> memberChatVRange + +supportsVersion :: GroupMember -> VersionChat -> Bool +supportsVersion m v = maxVersion (memberChatVRange' m) >= v groupMemberId' :: GroupMember -> GroupMemberId groupMemberId' GroupMember {groupMemberId} = groupMemberId @@ -674,6 +891,7 @@ data NewGroupMember = NewGroupMember { memInfo :: MemberInfo, memCategory :: GroupMemberCategory, memStatus :: GroupMemberStatus, + memRestriction :: Maybe MemberRestrictionStatus, memInvitedBy :: InvitedBy, memInvitedByGroupMemberId :: Maybe GroupMemberId, localDisplayName :: ContactName, @@ -682,11 +900,10 @@ data NewGroupMember = NewGroupMember } newtype MemberId = MemberId {unMemberId :: ByteString} - deriving (Eq, Show) + deriving (Eq, Ord, Show) + deriving newtype (FromField) -instance FromField MemberId where fromField f = MemberId <$> fromField f - -instance ToField MemberId where toField (MemberId m) = toField m +instance ToField MemberId where toField (MemberId m) = toField $ Binary m instance StrEncoding MemberId where strEncode (MemberId m) = strEncode m @@ -700,6 +917,9 @@ instance ToJSON MemberId where toJSON = strToJSON toEncoding = strToJEncoding +nameFromMemberId :: MemberId -> ContactName +nameFromMemberId = T.take 7 . safeDecodeUtf8 . B64.encode . unMemberId + data InvitedBy = IBContact {byContactId :: Int64} | IBUser | IBUnknown deriving (Eq, Show) @@ -715,41 +935,6 @@ fromInvitedBy userCtId = \case IBContact ctId -> Just ctId IBUser -> Just userCtId -data GroupMemberRole - = GRObserver -- connects to all group members and receives all messages, can't send messages - | GRAuthor -- reserved, unused - | GRMember -- + can send messages to all group members - | GRAdmin -- + add/remove members, change member role (excl. Owners) - | GROwner -- + delete and change group information, add/remove/change roles for Owners - deriving (Eq, Show, Ord) - -instance FromField GroupMemberRole where fromField = fromBlobField_ strDecode - -instance ToField GroupMemberRole where toField = toField . strEncode - -instance StrEncoding GroupMemberRole where - strEncode = \case - GROwner -> "owner" - GRAdmin -> "admin" - GRMember -> "member" - GRAuthor -> "author" - GRObserver -> "observer" - strDecode = \case - "owner" -> Right GROwner - "admin" -> Right GRAdmin - "member" -> Right GRMember - "author" -> Right GRAuthor - "observer" -> Right GRObserver - r -> Left $ "bad GroupMemberRole " <> B.unpack r - strP = strDecode <$?> A.takeByteString - -instance FromJSON GroupMemberRole where - parseJSON = strParseJSON "GroupMemberRole" - -instance ToJSON GroupMemberRole where - toJSON = strToJSON - toEncoding = strToJEncoding - data GroupMemberSettings = GroupMemberSettings { showMessages :: Bool } @@ -823,10 +1008,13 @@ instance TextEncoding GroupMemberCategory where GCPostMember -> "post" data GroupMemberStatus - = GSMemRemoved -- member who was removed from the group + = GSMemRejected -- joining member who was rejected by the host, or host that rejected the join + | GSMemRemoved -- member who was removed from the group | GSMemLeft -- member who left the group | GSMemGroupDeleted -- user member of the deleted group + | GSMemUnknown -- unknown member, whose message was forwarded by an admin (likely member wasn't introduced due to not being a current member, but message was included in history) | GSMemInvited -- member is sent to or received invitation to join the group + | GSMemPendingApproval -- member is connected to host but pending host approval before connecting to other members ("knocking") | GSMemIntroduced -- user received x.grp.mem.intro for this member (only with GCPreMember) | GSMemIntroInvited -- member is sent to or received from intro invitation | GSMemAccepted -- member accepted invitation (only User and Invitee) @@ -847,12 +1035,20 @@ instance ToJSON GroupMemberStatus where toJSON = J.String . textEncode toEncoding = JE.text . textEncode +acceptanceToStatus :: GroupAcceptance -> GroupMemberStatus +acceptanceToStatus = \case + GAAccepted -> GSMemAccepted + GAPending -> GSMemPendingApproval + memberActive :: GroupMember -> Bool memberActive m = case memberStatus m of + GSMemRejected -> False GSMemRemoved -> False GSMemLeft -> False GSMemGroupDeleted -> False + GSMemUnknown -> False GSMemInvited -> False + GSMemPendingApproval -> True GSMemIntroduced -> False GSMemIntroInvited -> False GSMemAccepted -> False @@ -867,10 +1063,13 @@ memberCurrent = memberCurrent' . memberStatus -- update getGroupSummary if this is changed memberCurrent' :: GroupMemberStatus -> Bool memberCurrent' = \case + GSMemRejected -> False GSMemRemoved -> False GSMemLeft -> False GSMemGroupDeleted -> False + GSMemUnknown -> False GSMemInvited -> False + GSMemPendingApproval -> False GSMemIntroduced -> True GSMemIntroInvited -> True GSMemAccepted -> True @@ -881,10 +1080,13 @@ memberCurrent' = \case memberRemoved :: GroupMember -> Bool memberRemoved m = case memberStatus m of + GSMemRejected -> True GSMemRemoved -> True GSMemLeft -> True GSMemGroupDeleted -> True + GSMemUnknown -> False GSMemInvited -> False + GSMemPendingApproval -> False GSMemIntroduced -> False GSMemIntroInvited -> False GSMemAccepted -> False @@ -895,10 +1097,13 @@ memberRemoved m = case memberStatus m of instance TextEncoding GroupMemberStatus where textDecode = \case + "rejected" -> Just GSMemRejected "removed" -> Just GSMemRemoved "left" -> Just GSMemLeft "deleted" -> Just GSMemGroupDeleted + "unknown" -> Just GSMemUnknown "invited" -> Just GSMemInvited + "pending_approval" -> Just GSMemPendingApproval "introduced" -> Just GSMemIntroduced "intro-inv" -> Just GSMemIntroInvited "accepted" -> Just GSMemAccepted @@ -908,10 +1113,13 @@ instance TextEncoding GroupMemberStatus where "creator" -> Just GSMemCreator _ -> Nothing textEncode = \case + GSMemRejected -> "rejected" GSMemRemoved -> "removed" GSMemLeft -> "left" GSMemGroupDeleted -> "deleted" + GSMemUnknown -> "unknown" GSMemInvited -> "invited" + GSMemPendingApproval -> "pending_approval" GSMemIntroduced -> "introduced" GSMemIntroInvited -> "intro-inv" GSMemAccepted -> "accepted" @@ -1009,7 +1217,8 @@ data RcvFileTransfer = RcvFileTransfer data XFTPRcvFile = XFTPRcvFile { rcvFileDescription :: RcvFileDescr, agentRcvFileId :: Maybe AgentRcvFileId, - agentRcvFileDeleted :: Bool + agentRcvFileDeleted :: Bool, + userApprovedRelays :: Bool } deriving (Eq, Show) @@ -1065,6 +1274,9 @@ liveRcvFileTransferPath ft = fp <$> liveRcvFileTransferInfo ft newtype AgentConnId = AgentConnId ConnId deriving (Eq, Ord, Show) + deriving newtype (FromField) + +instance ToField AgentConnId where toField (AgentConnId m) = toField $ Binary m instance StrEncoding AgentConnId where strEncode (AgentConnId connId) = strEncode connId @@ -1078,12 +1290,11 @@ instance ToJSON AgentConnId where toJSON = strToJSON toEncoding = strToJEncoding -instance FromField AgentConnId where fromField f = AgentConnId <$> fromField f - -instance ToField AgentConnId where toField (AgentConnId m) = toField m - -newtype AgentSndFileId = AgentSndFileId ConnId +newtype AgentSndFileId = AgentSndFileId SndFileId deriving (Eq, Show) + deriving newtype (FromField) + +instance ToField AgentSndFileId where toField (AgentSndFileId m) = toField $ Binary m instance StrEncoding AgentSndFileId where strEncode (AgentSndFileId connId) = strEncode connId @@ -1097,12 +1308,11 @@ instance ToJSON AgentSndFileId where toJSON = strToJSON toEncoding = strToJEncoding -instance FromField AgentSndFileId where fromField f = AgentSndFileId <$> fromField f - -instance ToField AgentSndFileId where toField (AgentSndFileId m) = toField m - -newtype AgentRcvFileId = AgentRcvFileId ConnId +newtype AgentRcvFileId = AgentRcvFileId RcvFileId deriving (Eq, Show) + deriving newtype (FromField) + +instance ToField AgentRcvFileId where toField (AgentRcvFileId m) = toField $ Binary m instance StrEncoding AgentRcvFileId where strEncode (AgentRcvFileId connId) = strEncode connId @@ -1116,10 +1326,6 @@ instance ToJSON AgentRcvFileId where toJSON = strToJSON toEncoding = strToJEncoding -instance FromField AgentRcvFileId where fromField f = AgentRcvFileId <$> fromField f - -instance ToField AgentRcvFileId where toField (AgentRcvFileId m) = toField m - newtype AgentInvId = AgentInvId InvitationId deriving (Eq, Show) @@ -1135,7 +1341,7 @@ instance ToJSON AgentInvId where toJSON = strToJSON toEncoding = strToJEncoding -instance FromField AgentInvId where fromField f = AgentInvId <$> fromField f +deriving newtype instance FromField AgentInvId instance ToField AgentInvId where toField (AgentInvId m) = toField m @@ -1150,6 +1356,7 @@ data FileTransfer data FileTransferMeta = FileTransferMeta { fileId :: FileTransferId, xftpSndFile :: Maybe XFTPSndFile, + xftpRedirectFor :: Maybe FileTransferId, fileName :: String, filePath :: String, fileSize :: Integer, @@ -1159,6 +1366,15 @@ data FileTransferMeta = FileTransferMeta } deriving (Eq, Show) +data LocalFileMeta = LocalFileMeta + { fileId :: FileTransferId, + fileName :: String, + filePath :: String, + fileSize :: Integer, + fileCryptoArgs :: Maybe CryptoFileArgs + } + deriving (Eq, Show) + data XFTPSndFile = XFTPSndFile { agentSndFileId :: AgentSndFileId, privateSndFileDescr :: Maybe Text, @@ -1207,10 +1423,19 @@ type ConnReqInvitation = ConnectionRequestUri 'CMInvitation type ConnReqContact = ConnectionRequestUri 'CMContact +type CreatedLinkInvitation = CreatedConnLink 'CMInvitation + +type CreatedLinkContact = CreatedConnLink 'CMContact + +type ConnLinkContact = ConnectionLink 'CMContact + +type ShortLinkContact = ConnShortLink 'CMContact + data Connection = Connection { connId :: Int64, agentConnId :: AgentConnId, - peerChatVRange :: JVersionRange, + connChatVersion :: VersionChat, + peerChatVRange :: VersionRangeChat, connLevel :: Int, viaContact :: Maybe Int64, -- group member contact ID, if not direct connection viaUserContactLink :: Maybe Int64, -- user contact link ID, if connected via "user address" @@ -1223,7 +1448,12 @@ data Connection = Connection localAlias :: Text, entityId :: Maybe Int64, -- contact, group member, file ID or user contact ID connectionCode :: Maybe SecurityCode, + pqSupport :: PQSupport, + pqEncryption :: PQEncryption, + pqSndEnabled :: Maybe PQEncryption, + pqRcvEnabled :: Maybe PQEncryption, authErrCounter :: Int, + quotaErrCounter :: Int, -- if exceeds limit messages to group members are created as pending; sending to contacts is unaffected by this createdAt :: UTCTime } deriving (Eq, Show) @@ -1237,6 +1467,15 @@ authErrDisableCount = 10 connDisabled :: Connection -> Bool connDisabled Connection {authErrCounter} = authErrCounter >= authErrDisableCount +quotaErrInactiveCount :: Int +quotaErrInactiveCount = 5 + +quotaErrSetOnMERR :: Int +quotaErrSetOnMERR = 999 + +connInactive :: Connection -> Bool +connInactive Connection {quotaErrCounter} = quotaErrCounter >= quotaErrInactiveCount + data SecurityCode = SecurityCode {securityCode :: Text, verifiedAt :: UTCTime} deriving (Eq, Show) @@ -1257,6 +1496,10 @@ aConnId Connection {agentConnId = AgentConnId cId} = cId connIncognito :: Connection -> Bool connIncognito Connection {customUserProfileId} = isJust customUserProfileId +connPQEnabled :: Connection -> PQEncryption +connPQEnabled Connection {pqSndEnabled = Just (PQEncryption s), pqRcvEnabled = Just (PQEncryption r)} = PQEncryption $ s && r +connPQEnabled _ = PQEncOff + data PendingContactConnection = PendingContactConnection { pccConnId :: Int64, pccAgentConnId :: AgentConnId, @@ -1265,7 +1508,7 @@ data PendingContactConnection = PendingContactConnection viaUserContactLink :: Maybe Int64, groupLinkId :: Maybe GroupLinkId, customUserProfileId :: Maybe Int64, - connReqInv :: Maybe ConnReqInvitation, + connLinkInv :: Maybe CreatedLinkInvitation, localAlias :: Text, createdAt :: UTCTime, updatedAt :: UTCTime @@ -1278,13 +1521,15 @@ aConnId' PendingContactConnection {pccAgentConnId = AgentConnId cId} = cId data ConnStatus = -- | connection is created by initiating party with agent NEW command (createConnection) ConnNew + | -- | connection is prepared, to avoid changing keys on invitation links when retrying. + ConnPrepared | -- | connection is joined by joining party with agent JOIN command (joinConnection) ConnJoined | -- | initiating party received CONF notification (to be renamed to REQ) ConnRequested | -- | initiating party accepted connection with agent LET command (to be renamed to ACPT) (allowConnection) ConnAccepted - | -- | connection can be sent messages to (after joining party received INFO notification) + | -- | connection can be sent messages to (after joining party received INFO notification, or after securing snd queue on join) ConnSndReady | -- | connection is ready for both parties to send and receive messages ConnReady @@ -1306,6 +1551,7 @@ instance ToJSON ConnStatus where instance TextEncoding ConnStatus where textDecode = \case "new" -> Just ConnNew + "prepared" -> Just ConnPrepared "joined" -> Just ConnJoined "requested" -> Just ConnRequested "accepted" -> Just ConnAccepted @@ -1315,6 +1561,7 @@ instance TextEncoding ConnStatus where _ -> Nothing textEncode = \case ConnNew -> "new" + ConnPrepared -> "prepared" ConnJoined -> "joined" ConnRequested -> "requested" ConnAccepted -> "accepted" @@ -1401,9 +1648,6 @@ serializeIntroStatus = \case GMIntroToConnected -> "to-con" GMIntroConnected -> "con" -textParseJSON :: TextEncoding a => String -> J.Value -> JT.Parser a -textParseJSON name = J.withText name $ maybe (fail $ "bad " <> name) pure . textDecode - data NetworkStatus = NSUnknown | NSConnected @@ -1461,7 +1705,7 @@ data CommandFunction | CFJoinConn | CFAllowConn | CFAcceptContact - | CFAckMessage + | CFAckMessage -- not used | CFDeleteConn -- not used deriving (Eq, Show) @@ -1492,19 +1736,19 @@ instance TextEncoding CommandFunction where CFAckMessage -> "ack_message" CFDeleteConn -> "delete_conn" -commandExpectedResponse :: CommandFunction -> APartyCmdTag 'Agent +commandExpectedResponse :: CommandFunction -> AEvtTag commandExpectedResponse = \case CFCreateConnGrpMemInv -> t INV_ CFCreateConnGrpInv -> t INV_ CFCreateConnFileInvDirect -> t INV_ CFCreateConnFileInvGroup -> t INV_ - CFJoinConn -> t OK_ + CFJoinConn -> t JOINED_ CFAllowConn -> t OK_ - CFAcceptContact -> t OK_ + CFAcceptContact -> t JOINED_ CFAckMessage -> t OK_ CFDeleteConn -> t OK_ where - t = APCT SAEConn + t = AEvtTag SAEConn data CommandData = CommandData { cmdId :: CommandId, @@ -1514,6 +1758,13 @@ data CommandData = CommandData } deriving (Show) +data ChatTag = ChatTag + { chatTagId :: Int64, + chatTagText :: Text, + chatTagEmoji :: Maybe Text + } + deriving (Show) + -- ad-hoc type for data required for XGrpMemIntro continuation data XGrpMemIntroCont = XGrpMemIntroCont { groupId :: GroupId, @@ -1523,18 +1774,46 @@ data XGrpMemIntroCont = XGrpMemIntroCont } deriving (Show) -data ServerCfg p = ServerCfg - { server :: ProtoServerWithAuth p, - preset :: Bool, - tested :: Maybe Bool, - enabled :: Bool +-- | Entity for local chats +data NoteFolder = NoteFolder + { noteFolderId :: NoteFolderId, + userId :: UserId, + createdAt :: UTCTime, + updatedAt :: UTCTime, + chatTs :: UTCTime, + favorite :: Bool, + unread :: Bool } - deriving (Show) + deriving (Eq, Show) -newtype ChatVersionRange = ChatVersionRange {fromChatVRange :: VersionRange} deriving (Eq, Show) +type NoteFolderId = Int64 -chatInitialVRange :: VersionRange -chatInitialVRange = versionToRange 1 +data ChatVersion + +instance VersionScope ChatVersion + +type VersionChat = Version ChatVersion + +type VersionRangeChat = VersionRange ChatVersion + +pattern VersionChat :: Word16 -> VersionChat +pattern VersionChat v = Version v + +-- this newtype exists to have a concise JSON encoding of version ranges in chat protocol messages in the form of "1-2" or just "1" +newtype ChatVersionRange = ChatVersionRange {fromChatVRange :: VersionRangeChat} deriving (Eq, Show) + +-- TODO v6.0 review +peerConnChatVersion :: VersionRangeChat -> VersionRangeChat -> VersionChat +peerConnChatVersion _local@(VersionRange lmin lmax) _peer@(VersionRange rmin rmax) + | lmin <= rmax && rmin <= lmax = min lmax rmax -- compatible + | rmin > lmax = rmin + | otherwise = rmax + +initialChatVersion :: VersionChat +initialChatVersion = VersionChat 1 + +chatInitialVRange :: VersionRangeChat +chatInitialVRange = versionToRange initialChatVersion instance FromJSON ChatVersionRange where parseJSON v = ChatVersionRange <$> strParseJSON "ChatVersionRange" v @@ -1543,18 +1822,6 @@ instance ToJSON ChatVersionRange where toJSON (ChatVersionRange vr) = strToJSON vr toEncoding (ChatVersionRange vr) = strToJEncoding vr -newtype JVersionRange = JVersionRange {fromJVersionRange :: VersionRange} deriving (Eq, Show) - -instance FromJSON JVersionRange where - parseJSON = J.withObject "JVersionRange" $ \o -> do - minv <- o .: "minVersion" - maxv <- o .: "maxVersion" - maybe (fail "bad version range") (pure . JVersionRange) $ safeVersionRange minv maxv - -instance ToJSON JVersionRange where - toJSON (JVersionRange (VersionRange minV maxV)) = J.object ["minVersion" .= minV, "maxVersion" .= maxV] - toEncoding (JVersionRange (VersionRange minV maxV)) = J.pairs $ "minVersion" .= minV <> "maxVersion" .= maxV - $(JQ.deriveJSON defaultJSON ''UserContact) $(JQ.deriveJSON defaultJSON ''Profile) @@ -1563,6 +1830,16 @@ $(JQ.deriveJSON defaultJSON ''LocalProfile) $(JQ.deriveJSON defaultJSON ''UserContactRequest) +$(JQ.deriveJSON (enumJSON $ dropPrefix "MC") {J.tagSingleConstructors = True} ''MemberCriteria) + +$(JQ.deriveJSON defaultJSON ''GroupMemberAdmission) + +instance ToField GroupMemberAdmission where + toField = toField . encodeJSON + +instance FromField GroupMemberAdmission where + fromField = fromTextField_ decodeJSON + $(JQ.deriveJSON defaultJSON ''GroupProfile) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "IB") ''InvitedBy) @@ -1585,6 +1862,10 @@ $(JQ.deriveJSON (enumJSON $ dropPrefix "MF") ''MsgFilter) $(JQ.deriveJSON defaultJSON ''ChatSettings) +$(JQ.deriveJSON (enumJSON $ dropPrefix "BC") ''BusinessChatType) + +$(JQ.deriveJSON defaultJSON ''BusinessChatInfo) + $(JQ.deriveJSON defaultJSON ''GroupInfo) $(JQ.deriveJSON defaultJSON ''Group) @@ -1595,17 +1876,21 @@ instance FromField MsgFilter where fromField = fromIntField_ msgFilterIntP instance ToField MsgFilter where toField = toField . msgFilterInt -$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "CRData") ''CReqClientData) +$(JQ.deriveJSON defaultJSON ''CReqClientData) $(JQ.deriveJSON defaultJSON ''MemberIdRole) +$(JQ.deriveJSON defaultJSON ''MemberInfo) + $(JQ.deriveJSON defaultJSON ''GroupInvitation) $(JQ.deriveJSON defaultJSON ''GroupLinkInvitation) +$(JQ.deriveJSON defaultJSON ''GroupLinkRejection) + $(JQ.deriveJSON defaultJSON ''IntroInvitation) -$(JQ.deriveJSON defaultJSON ''MemberInfo) +$(JQ.deriveJSON defaultJSON ''MemberRestrictions) $(JQ.deriveJSON defaultJSON ''GroupMemberRef) @@ -1629,6 +1914,8 @@ $(JQ.deriveJSON defaultJSON ''XFTPSndFile) $(JQ.deriveJSON defaultJSON ''FileTransferMeta) +$(JQ.deriveJSON defaultJSON ''LocalFileMeta) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FT") ''FileTransfer) $(JQ.deriveJSON defaultJSON ''UserPwdHash) @@ -1643,9 +1930,10 @@ $(JQ.deriveJSON defaultJSON ''Contact) $(JQ.deriveJSON defaultJSON ''ContactRef) -instance ProtocolTypeI p => ToJSON (ServerCfg p) where - toEncoding = $(JQ.mkToEncoding defaultJSON ''ServerCfg) - toJSON = $(JQ.mkToJSON defaultJSON ''ServerCfg) +$(JQ.deriveJSON defaultJSON ''NoteFolder) -instance ProtocolTypeI p => FromJSON (ServerCfg p) where - parseJSON = $(JQ.mkParseJSON defaultJSON ''ServerCfg) +$(JQ.deriveJSON defaultJSON ''ChatTag) + +$(JQ.deriveJSON defaultJSON ''ShortGroupInfo) + +$(JQ.deriveJSON defaultJSON ''ShortGroupMember) diff --git a/src/Simplex/Chat/Types/Preferences.hs b/src/Simplex/Chat/Types/Preferences.hs index 18a10a83f4..63d80657dc 100644 --- a/src/Simplex/Chat/Types/Preferences.hs +++ b/src/Simplex/Chat/Types/Preferences.hs @@ -7,10 +7,10 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilyDependencies #-} @@ -29,13 +29,13 @@ import qualified Data.ByteString.Char8 as B import Data.Maybe (fromMaybe, isJust) import Data.Text (Text) import qualified Data.Text as T -import Database.SQLite.Simple.FromField (FromField (..)) -import Database.SQLite.Simple.ToField (ToField (..)) import GHC.Records.Compat -import Simplex.Chat.Types.Util +import Simplex.Chat.Options.DB (FromField (..), ToField (..)) +import Simplex.Chat.Types.Shared +import Simplex.Messaging.Agent.Store.DB (blobFieldDecoder, fromTextField_) import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, sumTypeJSON) -import Simplex.Messaging.Util (safeDecodeUtf8, (<$?>)) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON) +import Simplex.Messaging.Util (decodeJSON, encodeJSON, safeDecodeUtf8, (<$?>)) data ChatFeature = CFTimedMessages @@ -79,12 +79,12 @@ allChatFeatures = ] chatPrefSel :: SChatFeature f -> Preferences -> Maybe (FeaturePreference f) -chatPrefSel f ps = case f of - SCFTimedMessages -> ps.timedMessages - SCFFullDelete -> ps.fullDelete - SCFReactions -> ps.reactions - SCFVoice -> ps.voice - SCFCalls -> ps.calls +chatPrefSel f Preferences {timedMessages, fullDelete, reactions, voice, calls} = case f of + SCFTimedMessages -> timedMessages + SCFFullDelete -> fullDelete + SCFReactions -> reactions + SCFVoice -> voice + SCFCalls -> calls chatFeature :: SChatFeature f -> ChatFeature chatFeature = \case @@ -104,12 +104,12 @@ instance PreferenceI (Maybe Preferences) where getPreference f prefs = fromMaybe (getPreference f defaultChatPrefs) (chatPrefSel f =<< prefs) instance PreferenceI FullPreferences where - getPreference f ps = case f of - SCFTimedMessages -> ps.timedMessages - SCFFullDelete -> ps.fullDelete - SCFReactions -> ps.reactions - SCFVoice -> ps.voice - SCFCalls -> ps.calls + getPreference f FullPreferences {timedMessages, fullDelete, reactions, voice, calls} = case f of + SCFTimedMessages -> timedMessages + SCFFullDelete -> fullDelete + SCFReactions -> reactions + SCFVoice -> voice + SCFCalls -> calls {-# INLINE getPreference #-} setPreference :: forall f. FeatureI f => SChatFeature f -> Maybe FeatureAllowed -> Maybe Preferences -> Preferences @@ -149,6 +149,8 @@ data GroupFeature | GFReactions | GFVoice | GFFiles + | GFSimplexLinks + | GFReports | GFHistory deriving (Show) @@ -159,14 +161,24 @@ data SGroupFeature (f :: GroupFeature) where SGFReactions :: SGroupFeature 'GFReactions SGFVoice :: SGroupFeature 'GFVoice SGFFiles :: SGroupFeature 'GFFiles + SGFSimplexLinks :: SGroupFeature 'GFSimplexLinks + SGFReports :: SGroupFeature 'GFReports SGFHistory :: SGroupFeature 'GFHistory deriving instance Show (SGroupFeature f) data AGroupFeature = forall f. GroupFeatureI f => AGF (SGroupFeature f) +data AGroupFeatureNoRole = forall f. GroupFeatureNoRoleI f => AGFNR (SGroupFeature f) + +data AGroupFeatureRole = forall f. GroupFeatureRoleI f => AGFR (SGroupFeature f) + deriving instance Show AGroupFeature +deriving instance Show AGroupFeatureNoRole + +deriving instance Show AGroupFeatureRole + groupFeatureNameText :: GroupFeature -> Text groupFeatureNameText = \case GFTimedMessages -> "Disappearing messages" @@ -175,37 +187,46 @@ groupFeatureNameText = \case GFReactions -> "Message reactions" GFVoice -> "Voice messages" GFFiles -> "Files and media" + GFSimplexLinks -> "SimpleX links" + GFReports -> "Member reports" GFHistory -> "Recent history" groupFeatureNameText' :: SGroupFeature f -> Text groupFeatureNameText' = groupFeatureNameText . toGroupFeature -groupFeatureAllowed' :: GroupFeatureI f => SGroupFeature f -> FullGroupPreferences -> Bool +groupFeatureAllowed' :: GroupFeatureNoRoleI f => SGroupFeature f -> FullGroupPreferences -> Bool groupFeatureAllowed' feature prefs = getField @"enable" (getGroupPreference feature prefs) == FEOn -allGroupFeatureItems :: [AGroupFeature] -allGroupFeatureItems = +groupFeatureMemberAllowed' :: GroupFeatureRoleI f => SGroupFeature f -> GroupMemberRole -> FullGroupPreferences -> Bool +groupFeatureMemberAllowed' feature role prefs = + let pref = getGroupPreference feature prefs + in getField @"enable" pref == FEOn && maybe True (role >=) (getField @"role" pref) + +allGroupFeatures :: [AGroupFeature] +allGroupFeatures = [ AGF SGFTimedMessages, AGF SGFDirectMessages, AGF SGFFullDelete, AGF SGFReactions, AGF SGFVoice, - AGF SGFFiles + AGF SGFFiles, + AGF SGFSimplexLinks, + AGF SGFReports, + AGF SGFHistory ] -allGroupFeatures :: [AGroupFeature] -allGroupFeatures = allGroupFeatureItems <> [AGF SGFHistory] - groupPrefSel :: SGroupFeature f -> GroupPreferences -> Maybe (GroupFeaturePreference f) -groupPrefSel f ps = case f of - SGFTimedMessages -> ps.timedMessages - SGFDirectMessages -> ps.directMessages - SGFFullDelete -> ps.fullDelete - SGFReactions -> ps.reactions - SGFVoice -> ps.voice - SGFFiles -> ps.files - SGFHistory -> ps.history +groupPrefSel f GroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, reports, history} = case f of + SGFTimedMessages -> timedMessages + SGFDirectMessages -> directMessages + SGFFullDelete -> fullDelete + SGFReactions -> reactions + SGFVoice -> voice + SGFFiles -> files + SGFSimplexLinks -> simplexLinks + SGFReports -> reports + SGFHistory -> history toGroupFeature :: SGroupFeature f -> GroupFeature toGroupFeature = \case @@ -215,6 +236,8 @@ toGroupFeature = \case SGFReactions -> GFReactions SGFVoice -> GFVoice SGFFiles -> GFFiles + SGFSimplexLinks -> GFSimplexLinks + SGFReports -> GFReports SGFHistory -> GFHistory class GroupPreferenceI p where @@ -227,14 +250,16 @@ instance GroupPreferenceI (Maybe GroupPreferences) where getGroupPreference pt prefs = fromMaybe (getGroupPreference pt defaultGroupPrefs) (groupPrefSel pt =<< prefs) instance GroupPreferenceI FullGroupPreferences where - getGroupPreference f ps = case f of - SGFTimedMessages -> ps.timedMessages - SGFDirectMessages -> ps.directMessages - SGFFullDelete -> ps.fullDelete - SGFReactions -> ps.reactions - SGFVoice -> ps.voice - SGFFiles -> ps.files - SGFHistory -> ps.history + getGroupPreference f FullGroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, reports, history} = case f of + SGFTimedMessages -> timedMessages + SGFDirectMessages -> directMessages + SGFFullDelete -> fullDelete + SGFReactions -> reactions + SGFVoice -> voice + SGFFiles -> files + SGFSimplexLinks -> simplexLinks + SGFReports -> reports + SGFHistory -> history {-# INLINE getGroupPreference #-} -- collection of optional group preferences @@ -245,17 +270,26 @@ data GroupPreferences = GroupPreferences reactions :: Maybe ReactionsGroupPreference, voice :: Maybe VoiceGroupPreference, files :: Maybe FilesGroupPreference, + simplexLinks :: Maybe SimplexLinksGroupPreference, + reports :: Maybe ReportsGroupPreference, history :: Maybe HistoryGroupPreference } deriving (Eq, Show) -setGroupPreference :: forall f. GroupFeatureI f => SGroupFeature f -> GroupFeatureEnabled -> Maybe GroupPreferences -> GroupPreferences +setGroupPreference :: forall f. GroupFeatureNoRoleI f => SGroupFeature f -> GroupFeatureEnabled -> Maybe GroupPreferences -> GroupPreferences setGroupPreference f enable prefs_ = setGroupPreference_ f pref prefs where prefs = mergeGroupPreferences prefs_ pref :: GroupFeaturePreference f pref = setField @"enable" (getGroupPreference f prefs) enable +setGroupPreferenceRole :: forall f. GroupFeatureRoleI f => SGroupFeature f -> GroupFeatureEnabled -> Maybe GroupMemberRole -> Maybe GroupPreferences -> GroupPreferences +setGroupPreferenceRole f enable role prefs_ = setGroupPreference_ f pref prefs + where + prefs = mergeGroupPreferences prefs_ + pref :: GroupFeaturePreference f + pref = setField @"role" (setField @"enable" (getGroupPreference f prefs) enable) role + setGroupPreference' :: SGroupFeature f -> GroupFeaturePreference f -> Maybe GroupPreferences -> GroupPreferences setGroupPreference' f pref prefs_ = setGroupPreference_ f pref prefs where @@ -270,6 +304,8 @@ setGroupPreference_ f pref prefs = SGFReactions -> prefs {reactions = pref} SGFVoice -> prefs {voice = pref} SGFFiles -> prefs {files = pref} + SGFSimplexLinks -> prefs {simplexLinks = pref} + SGFReports -> prefs {reports = pref} SGFHistory -> prefs {history = pref} setGroupTimedMessagesPreference :: TimedMessagesGroupPreference -> Maybe GroupPreferences -> GroupPreferences @@ -298,6 +334,8 @@ data FullGroupPreferences = FullGroupPreferences reactions :: ReactionsGroupPreference, voice :: VoiceGroupPreference, files :: FilesGroupPreference, + simplexLinks :: SimplexLinksGroupPreference, + reports :: ReportsGroupPreference, history :: HistoryGroupPreference } deriving (Eq, Show) @@ -349,16 +387,46 @@ defaultGroupPrefs :: FullGroupPreferences defaultGroupPrefs = FullGroupPreferences { timedMessages = TimedMessagesGroupPreference {enable = FEOff, ttl = Just 86400}, - directMessages = DirectMessagesGroupPreference {enable = FEOff}, - fullDelete = FullDeleteGroupPreference {enable = FEOff}, + directMessages = DirectMessagesGroupPreference {enable = FEOff, role = Nothing}, + fullDelete = FullDeleteGroupPreference {enable = FEOff, role = Nothing}, reactions = ReactionsGroupPreference {enable = FEOn}, - voice = VoiceGroupPreference {enable = FEOn}, - files = FilesGroupPreference {enable = FEOn}, + voice = VoiceGroupPreference {enable = FEOn, role = Nothing}, + files = FilesGroupPreference {enable = FEOn, role = Nothing}, + simplexLinks = SimplexLinksGroupPreference {enable = FEOn, role = Nothing}, + reports = ReportsGroupPreference {enable = FEOn}, history = HistoryGroupPreference {enable = FEOff} } emptyGroupPrefs :: GroupPreferences -emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing +emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing + +businessGroupPrefs :: Preferences -> GroupPreferences +businessGroupPrefs Preferences {timedMessages, fullDelete, reactions, voice} = + defaultBusinessGroupPrefs + { timedMessages = Just TimedMessagesGroupPreference {enable = maybe FEOff enableFeature timedMessages, ttl = maybe Nothing prefParam timedMessages}, + fullDelete = Just FullDeleteGroupPreference {enable = maybe FEOff enableFeature fullDelete, role = Nothing}, + reactions = Just ReactionsGroupPreference {enable = maybe FEOn enableFeature reactions}, + voice = Just VoiceGroupPreference {enable = maybe FEOff enableFeature voice, role = Nothing} + } + where + enableFeature :: FeatureI f => FeaturePreference f -> GroupFeatureEnabled + enableFeature p = case getField @"allow" p of + FANo -> FEOff + _ -> FEOn + +defaultBusinessGroupPrefs :: GroupPreferences +defaultBusinessGroupPrefs = + GroupPreferences + { timedMessages = Just $ TimedMessagesGroupPreference FEOff Nothing, + directMessages = Just $ DirectMessagesGroupPreference FEOff Nothing, + fullDelete = Just $ FullDeleteGroupPreference FEOn (Just GRModerator), + reactions = Just $ ReactionsGroupPreference FEOn, + voice = Just $ VoiceGroupPreference FEOff Nothing, + files = Just $ FilesGroupPreference FEOn Nothing, + simplexLinks = Just $ SimplexLinksGroupPreference FEOn Nothing, + reports = Just $ ReportsGroupPreference FEOff, + history = Just $ HistoryGroupPreference FEOn + } data TimedMessagesPreference = TimedMessagesPreference { allow :: FeatureAllowed, @@ -384,19 +452,19 @@ class (Eq (FeaturePreference f), HasField "allow" (FeaturePreference f) FeatureA prefParam :: FeaturePreference f -> Maybe Int instance HasField "allow" TimedMessagesPreference FeatureAllowed where - hasField p = (\allow -> p {allow}, p.allow) + hasField p@TimedMessagesPreference {allow} = (\a -> p {allow = a}, allow) instance HasField "allow" FullDeletePreference FeatureAllowed where - hasField p = (\allow -> p {allow}, p.allow) + hasField p@FullDeletePreference {allow} = (\a -> p {allow = a}, allow) instance HasField "allow" ReactionsPreference FeatureAllowed where - hasField p = (\allow -> p {allow}, p.allow) + hasField p@ReactionsPreference {allow} = (\a -> p {allow = a}, allow) instance HasField "allow" VoicePreference FeatureAllowed where - hasField p = (\allow -> p {allow}, p.allow) + hasField p@VoicePreference {allow} = (\a -> p {allow = a}, allow) instance HasField "allow" CallsPreference FeatureAllowed where - hasField p = (\allow -> p {allow}, p.allow) + hasField p@CallsPreference {allow} = (\a -> p {allow = a}, allow) instance FeatureI 'CFTimedMessages where type FeaturePreference 'CFTimedMessages = TimedMessagesPreference @@ -434,11 +502,11 @@ data TimedMessagesGroupPreference = TimedMessagesGroupPreference deriving (Eq, Show) data DirectMessagesGroupPreference = DirectMessagesGroupPreference - {enable :: GroupFeatureEnabled} + {enable :: GroupFeatureEnabled, role :: Maybe GroupMemberRole} deriving (Eq, Show) data FullDeleteGroupPreference = FullDeleteGroupPreference - {enable :: GroupFeatureEnabled} + {enable :: GroupFeatureEnabled, role :: Maybe GroupMemberRole} deriving (Eq, Show) data ReactionsGroupPreference = ReactionsGroupPreference @@ -446,10 +514,18 @@ data ReactionsGroupPreference = ReactionsGroupPreference deriving (Eq, Show) data VoiceGroupPreference = VoiceGroupPreference - {enable :: GroupFeatureEnabled} + {enable :: GroupFeatureEnabled, role :: Maybe GroupMemberRole} deriving (Eq, Show) data FilesGroupPreference = FilesGroupPreference + {enable :: GroupFeatureEnabled, role :: Maybe GroupMemberRole} + deriving (Eq, Show) + +data SimplexLinksGroupPreference = SimplexLinksGroupPreference + {enable :: GroupFeatureEnabled, role :: Maybe GroupMemberRole} + deriving (Eq, Show) + +data ReportsGroupPreference = ReportsGroupPreference {enable :: GroupFeatureEnabled} deriving (Eq, Show) @@ -461,71 +537,137 @@ class (Eq (GroupFeaturePreference f), HasField "enable" (GroupFeaturePreference type GroupFeaturePreference (f :: GroupFeature) = p | p -> f sGroupFeature :: SGroupFeature f groupPrefParam :: GroupFeaturePreference f -> Maybe Int + groupPrefRole :: GroupFeaturePreference f -> Maybe GroupMemberRole + +class GroupFeatureI f => GroupFeatureNoRoleI f + +class (GroupFeatureI f, HasField "role" (GroupFeaturePreference f) (Maybe GroupMemberRole)) => GroupFeatureRoleI f instance HasField "enable" GroupPreference GroupFeatureEnabled where - hasField p = (\enable -> p {enable}, p.enable) + hasField p@GroupPreference {enable} = (\e -> p {enable = e}, enable) instance HasField "enable" TimedMessagesGroupPreference GroupFeatureEnabled where - hasField p = (\enable -> p {enable}, p.enable) + hasField p@TimedMessagesGroupPreference {enable} = (\e -> p {enable = e}, enable) instance HasField "enable" DirectMessagesGroupPreference GroupFeatureEnabled where - hasField p = (\enable -> p {enable}, p.enable) + hasField p@DirectMessagesGroupPreference {enable} = (\e -> p {enable = e}, enable) instance HasField "enable" ReactionsGroupPreference GroupFeatureEnabled where - hasField p = (\enable -> p {enable}, p.enable) + hasField p@ReactionsGroupPreference {enable} = (\e -> p {enable = e}, enable) instance HasField "enable" FullDeleteGroupPreference GroupFeatureEnabled where - hasField p = (\enable -> p {enable}, p.enable) + hasField p@FullDeleteGroupPreference {enable} = (\e -> p {enable = e}, enable) instance HasField "enable" VoiceGroupPreference GroupFeatureEnabled where - hasField p = (\enable -> p {enable}, p.enable) + hasField p@VoiceGroupPreference {enable} = (\e -> p {enable = e}, enable) instance HasField "enable" FilesGroupPreference GroupFeatureEnabled where - hasField p = (\enable -> p {enable}, p.enable) + hasField p@FilesGroupPreference {enable} = (\e -> p {enable = e}, enable) + +instance HasField "enable" SimplexLinksGroupPreference GroupFeatureEnabled where + hasField p@SimplexLinksGroupPreference {enable} = (\e -> p {enable = e}, enable) + +instance HasField "enable" ReportsGroupPreference GroupFeatureEnabled where + hasField p@ReportsGroupPreference {enable} = (\e -> p {enable = e}, enable) instance HasField "enable" HistoryGroupPreference GroupFeatureEnabled where - hasField p = (\enable -> p {enable}, p.enable) + hasField p@HistoryGroupPreference {enable} = (\e -> p {enable = e}, enable) instance GroupFeatureI 'GFTimedMessages where type GroupFeaturePreference 'GFTimedMessages = TimedMessagesGroupPreference sGroupFeature = SGFTimedMessages groupPrefParam TimedMessagesGroupPreference {ttl} = ttl + groupPrefRole _ = Nothing instance GroupFeatureI 'GFDirectMessages where type GroupFeaturePreference 'GFDirectMessages = DirectMessagesGroupPreference sGroupFeature = SGFDirectMessages groupPrefParam _ = Nothing + groupPrefRole DirectMessagesGroupPreference {role} = role instance GroupFeatureI 'GFFullDelete where type GroupFeaturePreference 'GFFullDelete = FullDeleteGroupPreference sGroupFeature = SGFFullDelete groupPrefParam _ = Nothing + groupPrefRole FullDeleteGroupPreference {role} = role instance GroupFeatureI 'GFReactions where type GroupFeaturePreference 'GFReactions = ReactionsGroupPreference sGroupFeature = SGFReactions groupPrefParam _ = Nothing + groupPrefRole _ = Nothing instance GroupFeatureI 'GFVoice where type GroupFeaturePreference 'GFVoice = VoiceGroupPreference sGroupFeature = SGFVoice groupPrefParam _ = Nothing + groupPrefRole VoiceGroupPreference {role} = role instance GroupFeatureI 'GFFiles where type GroupFeaturePreference 'GFFiles = FilesGroupPreference sGroupFeature = SGFFiles groupPrefParam _ = Nothing + groupPrefRole FilesGroupPreference {role} = role + +instance GroupFeatureI 'GFSimplexLinks where + type GroupFeaturePreference 'GFSimplexLinks = SimplexLinksGroupPreference + sGroupFeature = SGFSimplexLinks + groupPrefParam _ = Nothing + groupPrefRole SimplexLinksGroupPreference {role} = role + +instance GroupFeatureI 'GFReports where + type GroupFeaturePreference 'GFReports = ReportsGroupPreference + sGroupFeature = SGFReports + groupPrefParam _ = Nothing + groupPrefRole _ = Nothing instance GroupFeatureI 'GFHistory where type GroupFeaturePreference 'GFHistory = HistoryGroupPreference sGroupFeature = SGFHistory groupPrefParam _ = Nothing + groupPrefRole _ = Nothing -groupPrefStateText :: HasField "enable" p GroupFeatureEnabled => GroupFeature -> p -> Maybe Int -> Text -groupPrefStateText feature pref param = +instance GroupFeatureNoRoleI 'GFTimedMessages + +instance GroupFeatureNoRoleI 'GFFullDelete + +instance GroupFeatureNoRoleI 'GFReactions + +instance GroupFeatureNoRoleI 'GFReports + +instance GroupFeatureNoRoleI 'GFHistory + +instance HasField "role" DirectMessagesGroupPreference (Maybe GroupMemberRole) where + hasField p@DirectMessagesGroupPreference {role} = (\r -> p {role = r}, role) + +instance HasField "role" FullDeleteGroupPreference (Maybe GroupMemberRole) where + hasField p@FullDeleteGroupPreference {role} = (\r -> p {role = r}, role) + +instance HasField "role" VoiceGroupPreference (Maybe GroupMemberRole) where + hasField p@VoiceGroupPreference {role} = (\r -> p {role = r}, role) + +instance HasField "role" FilesGroupPreference (Maybe GroupMemberRole) where + hasField p@FilesGroupPreference {role} = (\r -> p {role = r}, role) + +instance HasField "role" SimplexLinksGroupPreference (Maybe GroupMemberRole) where + hasField p@SimplexLinksGroupPreference {role} = (\r -> p {role = r}, role) + +instance GroupFeatureRoleI 'GFDirectMessages + +instance GroupFeatureRoleI 'GFFullDelete + +instance GroupFeatureRoleI 'GFVoice + +instance GroupFeatureRoleI 'GFFiles + +instance GroupFeatureRoleI 'GFSimplexLinks + +groupPrefStateText :: HasField "enable" p GroupFeatureEnabled => GroupFeature -> p -> Maybe Int -> Maybe GroupMemberRole -> Text +groupPrefStateText feature pref param role = let enabled = getField @"enable" pref paramText = if enabled == FEOn then groupParamText_ feature param else "" - in groupFeatureNameText feature <> ": " <> safeDecodeUtf8 (strEncode enabled) <> paramText + roleText = maybe "" (\r -> " for " <> safeDecodeUtf8 (strEncode r) <> "s") role + in groupFeatureNameText feature <> ": " <> safeDecodeUtf8 (strEncode enabled) <> paramText <> roleText groupParamText_ :: GroupFeature -> Maybe Int -> Text groupParamText_ feature param = case feature of @@ -535,7 +677,7 @@ groupParamText_ feature param = case feature of groupPreferenceText :: forall f. GroupFeatureI f => GroupFeaturePreference f -> Text groupPreferenceText pref = let feature = toGroupFeature $ sGroupFeature @f - in groupPrefStateText feature pref $ groupPrefParam pref + in groupPrefStateText feature pref (groupPrefParam pref) (groupPrefRole pref) timedTTLText :: Int -> Text timedTTLText 0 = "0 sec" @@ -567,7 +709,7 @@ data FeatureAllowed | FANo -- do not allow deriving (Eq, Show) -instance FromField FeatureAllowed where fromField = fromBlobField_ strDecode +instance FromField FeatureAllowed where fromField = blobFieldDecoder strDecode instance ToField FeatureAllowed where toField = toField . strEncode @@ -593,7 +735,7 @@ instance ToJSON FeatureAllowed where data GroupFeatureEnabled = FEOn | FEOff deriving (Eq, Show) -instance FromField GroupFeatureEnabled where fromField = fromBlobField_ strDecode +instance FromField GroupFeatureEnabled where fromField = blobFieldDecoder strDecode instance ToField GroupFeatureEnabled where toField = toField . strEncode @@ -605,7 +747,7 @@ instance StrEncoding GroupFeatureEnabled where "on" -> Right FEOn "off" -> Right FEOff r -> Left $ "bad GroupFeatureEnabled " <> B.unpack r - strP = strDecode <$?> A.takeByteString + strP = strDecode <$?> A.takeTill (== ' ') instance FromJSON GroupFeatureEnabled where parseJSON = strParseJSON "GroupFeatureEnabled" @@ -614,11 +756,13 @@ instance ToJSON GroupFeatureEnabled where toJSON = strToJSON toEncoding = strToJEncoding -groupFeatureState :: GroupFeatureI f => GroupFeaturePreference f -> (GroupFeatureEnabled, Maybe Int) +groupFeatureState :: GroupFeatureI f => GroupFeaturePreference f -> (GroupFeatureEnabled, Maybe Int, Maybe GroupMemberRole) groupFeatureState p = let enable = getField @"enable" p - param = if enable == FEOn then groupPrefParam p else Nothing - in (enable, param) + (param, role) + | enable == FEOn = (groupPrefParam p, groupPrefRole p) + | otherwise = (Nothing, Nothing) + in (enable, param, role) mergePreferences :: Maybe Preferences -> Maybe Preferences -> FullPreferences mergePreferences contactPrefs userPreferences = @@ -644,6 +788,8 @@ mergeGroupPreferences groupPreferences = reactions = pref SGFReactions, voice = pref SGFVoice, files = pref SGFFiles, + simplexLinks = pref SGFSimplexLinks, + reports = pref SGFReports, history = pref SGFHistory } where @@ -659,6 +805,8 @@ toGroupPreferences groupPreferences = reactions = pref SGFReactions, voice = pref SGFVoice, files = pref SGFFiles, + simplexLinks = pref SGFSimplexLinks, + reports = pref SGFReports, history = pref SGFHistory } where @@ -722,12 +870,12 @@ preferenceState pref = in (allow, param) getContactUserPreference :: SChatFeature f -> ContactUserPreferences -> ContactUserPreference (FeaturePreference f) -getContactUserPreference f ps = case f of - SCFTimedMessages -> ps.timedMessages - SCFFullDelete -> ps.fullDelete - SCFReactions -> ps.reactions - SCFVoice -> ps.voice - SCFCalls -> ps.calls +getContactUserPreference f ContactUserPreferences {timedMessages, fullDelete, reactions, voice, calls} = case f of + SCFTimedMessages -> timedMessages + SCFFullDelete -> fullDelete + SCFReactions -> reactions + SCFVoice -> voice + SCFCalls -> calls $(J.deriveJSON (enumJSON $ dropPrefix "CF") ''ChatFeature) @@ -765,6 +913,10 @@ $(J.deriveJSON defaultJSON ''VoiceGroupPreference) $(J.deriveJSON defaultJSON ''FilesGroupPreference) +$(J.deriveJSON defaultJSON ''SimplexLinksGroupPreference) + +$(J.deriveJSON defaultJSON ''ReportsGroupPreference) + $(J.deriveJSON defaultJSON ''HistoryGroupPreference) $(J.deriveJSON defaultJSON ''GroupPreferences) diff --git a/src/Simplex/Chat/Types/Shared.hs b/src/Simplex/Chat/Types/Shared.hs new file mode 100644 index 0000000000..4c6adae4e9 --- /dev/null +++ b/src/Simplex/Chat/Types/Shared.hs @@ -0,0 +1,74 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} + +module Simplex.Chat.Types.Shared where + +import Data.Aeson (FromJSON (..), ToJSON (..)) +import qualified Data.Attoparsec.ByteString.Char8 as A +import qualified Data.ByteString.Char8 as B +import Simplex.Chat.Options.DB (FromField (..), ToField (..)) +import Simplex.Messaging.Agent.Store.DB (blobFieldDecoder) +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Util ((<$?>)) + +data GroupMemberRole + = GRObserver -- connects to all group members and receives all messages, can't send messages + | GRAuthor -- reserved, unused + | GRMember -- + can send messages to all group members + | GRModerator -- + moderate messages and block members (excl. Admins and Owners) + | GRAdmin -- + add/remove members, change member role (excl. Owners) + | GROwner -- + delete and change group information, add/remove/change roles for Owners + deriving (Eq, Show, Ord) + +instance FromField GroupMemberRole where fromField = blobFieldDecoder strDecode + +instance ToField GroupMemberRole where toField = toField . strEncode + +instance StrEncoding GroupMemberRole where + strEncode = \case + GROwner -> "owner" + GRAdmin -> "admin" + GRModerator -> "moderator" + GRMember -> "member" + GRAuthor -> "author" + GRObserver -> "observer" + strDecode = \case + "owner" -> Right GROwner + "admin" -> Right GRAdmin + "moderator" -> Right GRModerator + "member" -> Right GRMember + "author" -> Right GRAuthor + "observer" -> Right GRObserver + r -> Left $ "bad GroupMemberRole " <> B.unpack r + strP = strDecode <$?> A.takeByteString + +instance FromJSON GroupMemberRole where + parseJSON = strParseJSON "GroupMemberRole" + +instance ToJSON GroupMemberRole where + toJSON = strToJSON + toEncoding = strToJEncoding + +data GroupAcceptance = GAAccepted | GAPending deriving (Eq, Show) + +-- TODO [knocking] encoding doesn't match field type +instance FromField GroupAcceptance where fromField = blobFieldDecoder strDecode + +instance ToField GroupAcceptance where toField = toField . strEncode + +instance StrEncoding GroupAcceptance where + strEncode = \case + GAAccepted -> "accepted" + GAPending -> "pending" + strDecode = \case + "accepted" -> Right GAAccepted + "pending" -> Right GAPending + r -> Left $ "bad GroupAcceptance " <> B.unpack r + strP = strDecode <$?> A.takeByteString + +instance FromJSON GroupAcceptance where + parseJSON = strParseJSON "GroupAcceptance" + +instance ToJSON GroupAcceptance where + toJSON = strToJSON + toEncoding = strToJEncoding diff --git a/src/Simplex/Chat/Types/UITheme.hs b/src/Simplex/Chat/Types/UITheme.hs new file mode 100644 index 0000000000..d0f23a7307 --- /dev/null +++ b/src/Simplex/Chat/Types/UITheme.hs @@ -0,0 +1,176 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} + +module Simplex.Chat.Types.UITheme where + +import Data.Aeson (FromJSON (..), ToJSON (..)) +import qualified Data.Aeson as J +import qualified Data.Aeson.Encoding as JE +import qualified Data.Aeson.Key as JK +import qualified Data.Aeson.TH as JQ +import Data.Char (toLower) +import Data.Maybe (fromMaybe) +import Data.Text (Text) +import Simplex.Chat.Options.DB (FromField (..), ToField (..)) +import Simplex.Chat.Types.Util +import Simplex.Messaging.Agent.Store.DB (fromTextField_) +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) +import Simplex.Messaging.Util (decodeJSON, encodeJSON) + +data UITheme = UITheme + { themeId :: Text, + base :: ThemeColorScheme, + wallpaper :: Maybe ChatWallpaper, + colors :: UIColors + } + deriving (Eq, Show) + +data UIColorMode = UCMLight | UCMDark + deriving (Eq, Show) + +data UIThemeEntityOverrides = UIThemeEntityOverrides + { light :: Maybe UIThemeEntityOverride, + dark :: Maybe UIThemeEntityOverride + } + deriving (Eq, Show) + +data UIThemeEntityOverride = UIThemeEntityOverride + { mode :: UIColorMode, + wallpaper :: Maybe ChatWallpaper, + colors :: UIColors + } + deriving (Eq, Show) + +data DarkColorScheme = DCSDark | DCSBlack | DCSSimplex + deriving (Eq, Ord, Show) + +data ThemeColorScheme = TCSLight | TCSDark DarkColorScheme + deriving (Eq, Ord, Show) + +data UIColorScheme = UCSSystem | UCSFixed ThemeColorScheme + deriving (Eq, Ord, Show) + +instance TextEncoding DarkColorScheme where + textEncode = \case + DCSDark -> "DARK" + DCSBlack -> "BLACK" + DCSSimplex -> "SIMPLEX" + textDecode s = + Just $ case s of + "DARK" -> DCSDark + "BLACK" -> DCSBlack + "SIMPLEX" -> DCSSimplex + _ -> DCSDark + +instance TextEncoding ThemeColorScheme where + textEncode = \case + TCSLight -> "LIGHT" + TCSDark s -> textEncode s + textDecode = \case + "LIGHT" -> Just TCSLight + s -> TCSDark <$> textDecode s + +instance TextEncoding UIColorScheme where + textEncode = \case + UCSSystem -> "SYSTEM" + UCSFixed s -> textEncode s + textDecode = \case + "SYSTEM" -> Just UCSSystem + s -> UCSFixed <$> textDecode s + +instance FromJSON DarkColorScheme where + parseJSON = textParseJSON "DarkColorScheme" + +instance ToJSON DarkColorScheme where + toJSON = J.String . textEncode + toEncoding = JE.text . textEncode + +instance FromJSON ThemeColorScheme where + parseJSON = textParseJSON "ThemeColorScheme" + +instance ToJSON ThemeColorScheme where + toJSON = J.String . textEncode + toEncoding = JE.text . textEncode + +instance FromJSON UIColorScheme where + parseJSON = textParseJSON "UIColorScheme" + +instance ToJSON UIColorScheme where + toJSON = J.String . textEncode + toEncoding = JE.text . textEncode + +instance J.FromJSONKey ThemeColorScheme where + fromJSONKey = J.FromJSONKeyText $ fromMaybe (TCSDark DCSDark) . textDecode + +instance J.ToJSONKey ThemeColorScheme where + toJSONKey = J.ToJSONKeyText (JK.fromText . textEncode) (JE.text . textEncode) + +data ChatWallpaper = ChatWallpaper + { preset :: Maybe Text, + imageFile :: Maybe FilePath, + background :: Maybe UIColor, + tint :: Maybe UIColor, + scaleType :: Maybe ChatWallpaperScale, + scale :: Maybe Double + } + deriving (Eq, Show) + +data ChatWallpaperScale = CWSFill | CWSFit | CWSRepeat + deriving (Eq, Show) + +data UIColors = UIColors + { accent :: Maybe UIColor, + accentVariant :: Maybe UIColor, + secondary :: Maybe UIColor, + secondaryVariant :: Maybe UIColor, + background :: Maybe UIColor, + menus :: Maybe UIColor, + title :: Maybe UIColor, + accentVariant2 :: Maybe UIColor, + sentMessage :: Maybe UIColor, + sentReply :: Maybe UIColor, + receivedMessage :: Maybe UIColor, + receivedReply :: Maybe UIColor + } + deriving (Eq, Show) + +defaultUIColors :: UIColors +defaultUIColors = UIColors Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing + +newtype UIColor = UIColor String + deriving (Eq, Show) + +instance FromJSON UIColor where + parseJSON v = toColor =<< J.parseJSON v + where + toColor s@('#' : cs) + | length cs == 8 && all hexDigit cs = pure $ UIColor s + toColor _ = fail "bad UIColor" + hexDigit c = (c >= '0' && c <= '9') || (let c' = toLower c in c' >= 'a' && c' <= 'f') + +instance ToJSON UIColor where + toJSON (UIColor t) = J.toJSON t + toEncoding (UIColor t) = J.toEncoding t + +$(JQ.deriveJSON (enumJSON $ dropPrefix "UCM") ''UIColorMode) + +$(JQ.deriveJSON (enumJSON $ dropPrefix "CWS") ''ChatWallpaperScale) + +$(JQ.deriveJSON defaultJSON ''ChatWallpaper) + +$(JQ.deriveJSON defaultJSON ''UIColors) + +$(JQ.deriveJSON defaultJSON ''UIThemeEntityOverride) + +$(JQ.deriveJSON defaultJSON ''UIThemeEntityOverrides) + +$(JQ.deriveJSON defaultJSON ''UITheme) + +instance ToField UIThemeEntityOverrides where + toField = toField . encodeJSON + +instance FromField UIThemeEntityOverrides where + fromField = fromTextField_ $ Just . fromMaybe (UIThemeEntityOverrides Nothing Nothing) . decodeJSON diff --git a/src/Simplex/Chat/Types/Util.hs b/src/Simplex/Chat/Types/Util.hs index 0f41931acf..afea178e41 100644 --- a/src/Simplex/Chat/Types/Util.hs +++ b/src/Simplex/Chat/Types/Util.hs @@ -1,30 +1,8 @@ -{-# LANGUAGE LambdaCase #-} - module Simplex.Chat.Types.Util where -import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as J -import Data.ByteString (ByteString) -import qualified Data.ByteString.Lazy.Char8 as LB -import Data.Text (Text) -import Data.Text.Encoding (encodeUtf8) -import Data.Typeable -import Database.SQLite.Simple (ResultError (..), SQLData (..)) -import Database.SQLite.Simple.FromField (FieldParser, returnError) -import Database.SQLite.Simple.Internal (Field (..)) -import Database.SQLite.Simple.Ok (Ok (Ok)) -import Simplex.Messaging.Util (safeDecodeUtf8) +import qualified Data.Aeson.Types as JT +import Simplex.Messaging.Encoding.String -encodeJSON :: ToJSON a => a -> Text -encodeJSON = safeDecodeUtf8 . LB.toStrict . J.encode - -decodeJSON :: FromJSON a => Text -> Maybe a -decodeJSON = J.decode . LB.fromStrict . encodeUtf8 - -fromBlobField_ :: Typeable k => (ByteString -> Either String k) -> FieldParser k -fromBlobField_ p = \case - f@(Field (SQLBlob b) _) -> - case p b of - Right k -> Ok k - Left e -> returnError ConversionFailed f ("could not parse field: " ++ e) - f -> returnError ConversionFailed f "expecting SQLBlob column type" +textParseJSON :: TextEncoding a => String -> J.Value -> JT.Parser a +textParseJSON name = J.withText name $ maybe (fail $ "bad " <> name) pure . textDecode diff --git a/src/Simplex/Chat/Util.hs b/src/Simplex/Chat/Util.hs index eacaf8d7ef..796b32778b 100644 --- a/src/Simplex/Chat/Util.hs +++ b/src/Simplex/Chat/Util.hs @@ -1,18 +1,28 @@ +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE InstanceSigs #-} +{-# LANGUAGE RankNTypes #-} {-# LANGUAGE TupleSections #-} +{-# OPTIONS_GHC -Wno-orphans #-} -module Simplex.Chat.Util (week, encryptFile, chunkSize, shuffle) where +module Simplex.Chat.Util (week, encryptFile, chunkSize, liftIOEither, shuffle, zipWith3') where +import Control.Exception (Exception) import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class +import Control.Monad.IO.Unlift (MonadUnliftIO (..)) +import Control.Monad.Reader +import Data.Bifunctor (first) import qualified Data.ByteString.Lazy as LB import Data.List (sortBy) +import Data.List.NonEmpty (NonEmpty (..)) import Data.Ord (comparing) import Data.Time (NominalDiffTime) import Data.Word (Word16) import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF import System.Random (randomRIO) +import qualified UnliftIO.Exception as E import UnliftIO.IO (IOMode (..), withFile) week :: NominalDiffTime @@ -42,3 +52,31 @@ shuffle xs = map snd . sortBy (comparing fst) <$> mapM (\x -> (,x) <$> random) x where random :: IO Word16 random = randomRIO (0, 65535) + +zipWith3' :: (a -> b -> c -> d) -> NonEmpty a -> NonEmpty b -> NonEmpty c -> NonEmpty d +zipWith3' f ~(x :| xs) ~(y :| ys) ~(z :| zs) = f x y z :| zipWith3 f xs ys zs + +liftIOEither :: (MonadIO m, MonadError e m) => IO (Either e a) -> m a +liftIOEither a = liftIO a >>= liftEither +{-# INLINE liftIOEither #-} + +newtype InternalException e = InternalException {unInternalException :: e} + deriving (Eq, Show) + +instance Exception e => Exception (InternalException e) + +instance Exception e => MonadUnliftIO (ExceptT e IO) where + {-# INLINE withRunInIO #-} + withRunInIO :: ((forall a. ExceptT e IO a -> IO a) -> IO b) -> ExceptT e IO b + withRunInIO inner = + ExceptT . fmap (first unInternalException) . E.try $ + withRunInIO $ \run -> + inner $ run . (either (E.throwIO . InternalException) pure <=< runExceptT) + +instance Exception e => MonadUnliftIO (ExceptT e (ReaderT r IO)) where + {-# INLINE withRunInIO #-} + withRunInIO :: ((forall a. ExceptT e (ReaderT r IO) a -> IO a) -> IO b) -> ExceptT e (ReaderT r IO) b + withRunInIO inner = + withExceptT unInternalException . ExceptT . E.try $ + withRunInIO $ \run -> + inner $ run . (either (E.throwIO . InternalException) pure <=< runExceptT) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index b0408690ae..4ba5acbb43 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -1,11 +1,14 @@ +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} @@ -14,10 +17,9 @@ module Simplex.Chat.View where import qualified Data.Aeson as J import qualified Data.Aeson.TH as JQ -import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB -import Data.Char (isSpace, toUpper) +import Data.Char (toUpper) import Data.Function (on) import Data.Int (Int64) import Data.List (groupBy, intercalate, intersperse, partition, sortOn) @@ -26,6 +28,7 @@ import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe) +import Data.String import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1) @@ -36,13 +39,14 @@ import Data.Time.Format (defaultTimeLocale, formatTime) import qualified Data.Version as V import qualified Network.HTTP.Types as Q import Numeric (showFFloat) -import Simplex.Chat (defaultChatConfig, maxImageSize) import Simplex.Chat.Call import Simplex.Chat.Controller import Simplex.Chat.Help +import Simplex.Chat.Library.Commands (maxImageSize) import Simplex.Chat.Markdown import Simplex.Chat.Messages hiding (NewChatItem (..)) import Simplex.Chat.Messages.CIContent +import Simplex.Chat.Operators import Simplex.Chat.Protocol import Simplex.Chat.Remote.AppVersion (AppVersion (..), pattern AppVersionRange) import Simplex.Chat.Remote.Types @@ -50,23 +54,29 @@ import Simplex.Chat.Store (AutoAccept (..), StoreError (..), UserContactLink (.. import Simplex.Chat.Styled import Simplex.Chat.Types import Simplex.Chat.Types.Preferences -import qualified Simplex.FileTransfer.Protocol as XFTP +import Simplex.Chat.Types.Shared +import Simplex.Chat.Types.UITheme +import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..)) -import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..)) +import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..), ServerRoles (..)) import Simplex.Messaging.Agent.Protocol -import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) +import Simplex.Messaging.Client (SMPProxyFallback, SMPProxyMode (..), SocksMode (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) +import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, ProtoServerWithAuth, ProtocolServer (..), ProtocolTypeI, SProtocolType (..)) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, BlockingInfo (..), BlockingReason (..), ProtocolServer (..), ProtocolTypeI, SProtocolType (..), UserProtocol) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport.Client (TransportHost (..)) -import Simplex.Messaging.Util (bshow, tshow) +import Simplex.Messaging.Util (safeDecodeUtf8, tshow) import Simplex.Messaging.Version hiding (version) -import Simplex.RemoteControl.Types (RCCtrlAddress (..)) +import Simplex.RemoteControl.Types (RCCtrlAddress (..), RCErrorType (..)) import System.Console.ANSI.Types +#if !defined(dbPostgres) +import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) +#endif type CurrentTime = UTCTime @@ -77,56 +87,73 @@ data WCallCommand $(JQ.deriveToJSON (taggedObjectJSON $ dropPrefix "WCCall") ''WCallCommand) -serializeChatResponse :: (Maybe RemoteHostId, Maybe User) -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> String -serializeChatResponse user_ ts tz remoteHost_ = unlines . map unStyle . responseToView user_ defaultChatConfig False ts tz remoteHost_ +serializeChatError :: Bool -> ChatConfig -> ChatError -> String +serializeChatError isCmd cfg = unlines . map unStyle . chatErrorToView isCmd cfg -responseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> [StyledString] -responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showReceipts, testView} liveItems ts tz outputRH = \case - CRActiveUser User {profile} -> viewUserProfile $ fromLocalProfile profile +serializeChatResponse :: ChatResponseEvent r => (Maybe RemoteHostId, Maybe User) -> ChatConfig -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> r -> String +serializeChatResponse hu cfg ts tz remoteHost_ = unlines . map unStyle . responseToView hu cfg False ts tz remoteHost_ + +class ChatResponseEvent r where + responseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> r -> [StyledString] + isCommandResponse :: Bool + +instance ChatResponseEvent ChatResponse where + responseToView = chatResponseToView + isCommandResponse = True + +instance ChatResponseEvent ChatEvent where + responseToView = chatEventToView + isCommandResponse = False + +chatErrorToView :: Bool -> ChatConfig -> ChatError -> [StyledString] +chatErrorToView isCmd ChatConfig {logLevel, testView} = viewChatError isCmd logLevel testView + +chatResponseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> [StyledString] +chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveItems ts tz outputRH = \case + CRActiveUser User {profile, uiThemes} -> viewUserProfile (fromLocalProfile profile) <> viewUITheme uiThemes CRUsersList users -> viewUsersList users CRChatStarted -> ["chat started"] CRChatRunning -> ["chat is running"] CRChatStopped -> ["chat stopped"] - CRChatSuspended -> ["chat suspended"] - CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [plain . bshow $ J.encode chats] + CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [viewJSON chats] CRChats chats -> viewChats ts tz chats - CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [plain . bshow $ J.encode chat] - CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft] - CRUserProtoServers u userServers -> ttyUser u $ viewUserServers userServers testView + CRApiChat u chat _ -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat] + CRChatTags u tags -> ttyUser u $ [viewJSON tags] CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure + CRServerOperatorConditions (ServerOperatorConditions ops _ ca) -> viewServerOperators ops ca + CRUserServers u uss -> ttyUser u $ concatMap viewUserServers uss <> (if testView then [] else serversUserHelp) + CRUserServersValidation {} -> [] + CRUsageConditions current _ accepted_ -> viewUsageConditions current accepted_ CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl - CRNetworkConfig cfg -> viewNetworkConfig cfg + CRNetworkConfig netCfg -> viewNetworkConfig netCfg CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile CRGroupInfo u g s -> ttyUser u $ viewGroupInfo g s CRGroupMemberInfo u g m cStats -> ttyUser u $ viewGroupMemberInfo g m cStats + CRQueueInfo _ msgInfo qInfo -> + [ "last received msg: " <> maybe "none" viewJSON msgInfo, + "server queue info: " <> viewJSON qInfo + ] CRContactSwitchStarted {} -> ["switch started"] CRGroupMemberSwitchStarted {} -> ["switch started"] CRContactSwitchAborted {} -> ["switch aborted"] CRGroupMemberSwitchAborted {} -> ["switch aborted"] - CRContactSwitch u ct progress -> ttyUser u $ viewContactSwitch ct progress - CRGroupMemberSwitch u g m progress -> ttyUser u $ viewGroupMemberSwitch g m progress CRContactRatchetSyncStarted {} -> ["connection synchronization started"] CRGroupMemberRatchetSyncStarted {} -> ["connection synchronization started"] - CRContactRatchetSync u ct progress -> ttyUser u $ viewContactRatchetSync ct progress - CRGroupMemberRatchetSync u g m progress -> ttyUser u $ viewGroupMemberRatchetSync g m progress - CRContactVerificationReset u ct -> ttyUser u $ viewContactVerificationReset ct - CRGroupMemberVerificationReset u g m -> ttyUser u $ viewGroupMemberVerificationReset g m 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 - CRNewChatItem u (AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewChatItem chat item False ts tz <> viewItemReactions item + 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 CRChatItemInfo u ci ciInfo -> ttyUser u $ viewChatItemInfo ci ciInfo tz CRChatItemId u itemId -> ttyUser u [plain $ maybe "no item" show itemId] - CRChatItemStatusUpdated u ci -> ttyUser u $ viewChatItemStatusUpdated ci ts tz testView showReceipts CRChatItemUpdated u (AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewItemUpdate chat item liveItems ts tz CRChatItemNotChanged u ci -> ttyUser u $ viewItemNotChanged ci - CRChatItemDeleted u (AChatItem _ _ chat deletedItem) toItem byUser timed -> ttyUser u $ unmuted u chat deletedItem $ viewItemDelete chat deletedItem toItem byUser timed ts tz testView + CRTagsUpdated u _ _ -> ttyUser u ["chat tags updated"] + CRChatItemsDeleted u deletions byUser timed -> ttyUser u $ viewChatItemsDeleted (unmuted u) deletions byUser timed ts tz testView + CRGroupChatItemsDeleted u g ciIds byUser member_ -> ttyUser u $ viewGroupChatItemsDeleted g ciIds byUser member_ CRChatItemReaction u added (ACIReaction _ _ chat reaction) -> ttyUser u $ unmutedReaction u chat reaction $ viewItemReaction showReactions chat reaction added ts tz - CRChatItemDeletedNotFound u Contact {localDisplayName = c} _ -> ttyUser u [ttyFrom $ c <> "> [deleted - original message not found]"] + CRReactionMembers u memberReactions -> ttyUser u $ viewReactionMembers memberReactions CRBroadcastSent u mc s f t -> ttyUser u $ viewSentBroadcast mc s f ts tz t - CRMsgIntegrityError u mErr -> ttyUser u $ viewMsgIntegrityError mErr - CRCmdAccepted _ -> [] CRCmdOk u_ -> ttyUser' u_ ["ok"] CRChatHelp section -> case section of HSMain -> chatHelpInfo @@ -142,47 +169,39 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe HSDatabase -> databaseHelpInfo CRWelcome user -> chatWelcome user CRContactsList u cs -> ttyUser u $ viewContactsList cs - CRUserContactLink u UserContactLink {connReqContact, autoAccept} -> ttyUser u $ connReqContact_ "Your chat address:" connReqContact <> autoAcceptStatus_ autoAccept + CRUserContactLink u UserContactLink {connLinkContact, autoAccept} -> ttyUser u $ connReqContact_ "Your chat address:" connLinkContact <> autoAcceptStatus_ autoAccept CRUserContactLinkUpdated u UserContactLink {autoAccept} -> ttyUser u $ autoAcceptStatus_ autoAccept CRContactRequestRejected u UserContactRequest {localDisplayName = c} -> ttyUser u [ttyContact c <> ": contact request rejected"] CRGroupCreated u g -> ttyUser u $ viewGroupCreated g testView CRGroupMembers u g -> ttyUser u $ viewGroupMembers g CRGroupsList u gs -> ttyUser u $ viewGroupsList gs - CRSentGroupInvitation u g c _ -> - ttyUser u $ - case contactConn c of - Just Connection {viaGroupLink} - | viaGroupLink -> [ttyContact' c <> " invited to group " <> ttyGroup' g <> " via your group link"] - | otherwise -> ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c] - Nothing -> [] + CRSentGroupInvitation u g c _ -> ttyUser u $ viewSentGroupInvitation g c CRFileTransferStatus u ftStatus -> ttyUser u $ viewFileTransferStatus ftStatus CRFileTransferStatusXFTP u ci -> ttyUser u $ viewFileTransferStatusXFTP ci CRUserProfile u p -> ttyUser u $ viewUserProfile p CRUserProfileNoChange u -> ttyUser u ["user profile did not change"] - CRUserPrivacy u u' -> ttyUserPrefix u $ viewUserPrivacy u u' + CRUserPrivacy u u' -> ttyUserPrefix hu outputRH u $ viewUserPrivacy u u' CRVersionInfo info _ _ -> viewVersionInfo logLevel info - CRInvitation u cReq _ -> ttyUser u $ viewConnReqInvitation cReq + CRInvitation u ccLink _ -> ttyUser u $ viewConnReqInvitation ccLink CRConnectionIncognitoUpdated u c -> ttyUser u $ viewConnectionIncognitoUpdated c - CRConnectionPlan u connectionPlan -> ttyUser u $ viewConnectionPlan connectionPlan + CRConnectionUserChanged u c c' nu -> ttyUser u $ viewConnectionUserChanged u c nu c' + CRConnectionPlan u _ connectionPlan -> ttyUser u $ viewConnectionPlan cfg connectionPlan CRSentConfirmation u _ -> ttyUser u ["confirmation sent!"] CRSentInvitation u _ customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView CRSentInvitationToContact u _c customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView CRContactDeleted u c -> ttyUser u [ttyContact' c <> ": contact is deleted"] - CRContactDeletedByContact u c -> ttyUser u [ttyFullContact c <> " deleted contact with you"] CRChatCleared u chatInfo -> ttyUser u $ viewChatCleared chatInfo - CRAcceptingContactRequest u c -> ttyUser u [ttyFullContact c <> ": accepting contact request..."] + CRAcceptingContactRequest u c -> ttyUser u $ viewAcceptingContactRequest c CRContactAlreadyExists u c -> ttyUser u [ttyFullContact c <> ": contact already exists"] - CRContactRequestAlreadyAccepted u c -> ttyUser u [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"] - CRUserContactLinkCreated u cReq -> ttyUser u $ connReqContact_ "Your new chat address is created!" cReq + CRUserContactLinkCreated u ccLink -> ttyUser u $ connReqContact_ "Your new chat address is created!" ccLink CRUserContactLinkDeleted u -> ttyUser u viewUserContactLinkDeleted CRUserAcceptedGroupSent u _g _ -> ttyUser u [] -- [ttyGroup' g <> ": joining the group..."] - CRGroupLinkConnecting u g _ -> ttyUser u [ttyGroup' g <> ": joining the group..."] - CRUserDeletedMember u g m -> ttyUser u [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group"] + CRUserDeletedMembers u g members wm -> case members of + [m] -> ttyUser u [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group" <> withMessages wm] + mems' -> ttyUser u [ttyGroup' g <> ": you removed " <> sShow (length mems') <> " members from the group" <> withMessages wm] CRLeftMemberUser u g -> ttyUser u $ [ttyGroup' g <> ": you left the group"] <> groupPreserved g CRGroupDeletedUser u g -> ttyUser u [ttyGroup' g <> ": you deleted the group"] - CRRcvFileDescrReady _ _ -> [] - CRRcvFileDescrNotReady _ _ -> [] - CRRcvFileProgressXFTP {} -> [] + CRForwardPlan u count itemIds fc -> ttyUser u $ viewForwardPlan count itemIds fc CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci CRRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft CRSndFileCancelled u _ ftm fts -> ttyUser u $ viewSndFileCancelled ftm fts @@ -191,95 +210,29 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRUserProfileImage u p -> ttyUser u $ viewUserProfileImage p CRContactPrefsUpdated {user = u, fromContact, toContact} -> ttyUser u $ viewUserContactPrefsUpdated u fromContact toContact CRContactAliasUpdated u c -> ttyUser u $ viewContactAliasUpdated c + CRGroupAliasUpdated u g -> ttyUser u $ viewGroupAliasUpdated g CRConnectionAliasUpdated u c -> ttyUser u $ viewConnectionAliasUpdated c - CRContactUpdated {user = u, fromContact = c, toContact = c'} -> ttyUser u $ viewContactUpdated c c' <> viewContactPrefsUpdated u c c' - CRGroupMemberUpdated {} -> [] - CRContactsMerged u intoCt mergedCt ct' -> ttyUser u $ viewContactsMerged intoCt mergedCt ct' - CRReceivedContactRequest u UserContactRequest {localDisplayName = c, profile} -> ttyUser u $ viewReceivedContactRequest c profile - CRRcvFileStart u ci -> ttyUser u $ receivingFile_' hu testView "started" ci - CRRcvFileComplete u ci -> ttyUser u $ receivingFile_' hu testView "completed" ci - CRRcvFileSndCancelled u _ ft -> ttyUser u $ viewRcvFileSndCancelled ft - CRRcvFileError u ci e -> ttyUser u $ receivingFile_' hu testView "error" ci <> [sShow e] - CRSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft - CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft - CRSndFileStartXFTP {} -> [] - CRSndFileProgressXFTP {} -> [] - CRSndFileCompleteXFTP u ci _ -> ttyUser u $ uploadingFile "completed" ci - CRSndFileCancelledXFTP {} -> [] - CRSndFileError u ci -> ttyUser u $ uploadingFile "error" ci - CRSndFileRcvCancelled u _ ft@SndFileTransfer {recipientDisplayName = c} -> - ttyUser u [ttyContact c <> " cancelled receiving " <> sndFile ft] - CRContactConnecting u _ -> ttyUser u [] - CRContactConnected u ct userCustomProfile -> ttyUser u $ viewContactConnected ct userCustomProfile testView - CRContactAnotherClient u c -> ttyUser u [ttyContact' c <> ": contact is connected to another client"] - CRSubscriptionEnd u acEntity -> ttyUser u [sShow ((entityConnection acEntity).connId) <> ": END"] - CRContactsDisconnected srv cs -> [plain $ "server disconnected " <> showSMPServer srv <> " (" <> contactList cs <> ")"] - CRContactsSubscribed srv cs -> [plain $ "server connected " <> showSMPServer srv <> " (" <> contactList cs <> ")"] - CRContactSubError u c e -> ttyUser u [ttyContact' c <> ": contact error " <> sShow e] - CRContactSubSummary u summary -> - ttyUser u $ [sShow (length subscribed) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)" | not (null subscribed)] <> viewErrorsSummary errors " contact errors" - where - (errors, subscribed) = partition (isJust . contactError) summary - CRUserContactSubSummary u summary -> - ttyUser u $ - map addressSS addresses - <> ([sShow (length groupLinksSubscribed) <> " group links active" | not (null groupLinksSubscribed)] <> viewErrorsSummary groupLinkErrors " group link errors") - where - (addresses, groupLinks) = partition (\UserContactSubStatus {userContact} -> isNothing . userContactGroupId $ userContact) summary - addressSS UserContactSubStatus {userContactError} = maybe ("Your address is active! To show: " <> highlight' "/sa") (\e -> "User address error: " <> sShow e <> ", to delete your address: " <> highlight' "/da") userContactError - (groupLinkErrors, groupLinksSubscribed) = partition (isJust . userContactError) groupLinks - CRNetworkStatus status conns -> if testView then [plain $ show (length conns) <> " connections " <> netStatusStr status] else [] + CRRcvStandaloneFileCreated u ft -> ttyUser u $ receivingFileStandalone "started" ft + CRSndStandaloneFileCreated u ft -> ttyUser u $ uploadingFileStandalone "started" ft + CRStandaloneFileInfo info_ -> maybe ["no file information in URI"] (\j -> [viewJSON j]) info_ CRNetworkStatuses u statuses -> if testView then ttyUser' u $ viewNetworkStatuses statuses else [] - CRGroupInvitation u g -> ttyUser u [groupInvitation' g] - CRReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g c r - CRUserJoinedGroup u g _ -> ttyUser u $ viewUserJoinedGroup g CRJoinedGroupMember u g m -> ttyUser u $ viewJoinedGroupMember g m - CRHostConnected p h -> [plain $ "connected to " <> viewHostEvent p h] - CRHostDisconnected p h -> [plain $ "disconnected from " <> viewHostEvent p h] - CRJoinedGroupMemberConnecting u g host m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting...)"] - CRConnectedToGroupMember u g m _ -> ttyUser u [ttyGroup' g <> ": " <> connectedMember m <> " is connected"] - CRMemberRole u g by m r r' -> ttyUser u $ viewMemberRoleChanged g by m r r' - CRMemberRoleUser u g m r r' -> ttyUser u $ viewMemberRoleUserChanged g m r r' - CRDeletedMemberUser u g by -> ttyUser u $ [ttyGroup' g <> ": " <> ttyMember by <> " removed you from the group"] <> groupPreserved g - CRDeletedMember u g by m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember by <> " removed " <> ttyMember m <> " from the group"] - CRLeftMember u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " left the group"] - CRGroupEmpty u g -> ttyUser u [ttyFullGroup g <> ": group is empty"] - CRGroupRemoved u g -> ttyUser u [ttyFullGroup g <> ": you are no longer a member or group deleted"] - CRGroupDeleted u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " deleted the group", "use " <> highlight ("/d #" <> viewGroupName g) <> " to delete the local copy of the group"] + CRMembersRoleUser u g members r' -> ttyUser u $ viewMemberRoleUserChanged g members r' + CRMembersBlockedForAllUser u g members blocked -> ttyUser u $ viewMembersBlockedForAllUser g members blocked CRGroupUpdated u g g' m -> ttyUser u $ viewGroupUpdated g g' m CRGroupProfile u g -> ttyUser u $ viewGroupProfile g CRGroupDescription u g -> ttyUser u $ viewGroupDescription g - CRGroupLinkCreated u g cReq mRole -> ttyUser u $ groupLink_ "Group link is created!" g cReq mRole - CRGroupLink u g cReq mRole -> ttyUser u $ groupLink_ "Group link:" g cReq mRole + CRGroupLinkCreated u g ccLink mRole -> ttyUser u $ groupLink_ "Group link is created!" g ccLink mRole + CRGroupLink u g ccLink mRole -> ttyUser u $ groupLink_ "Group link:" g ccLink mRole CRGroupLinkDeleted u g -> ttyUser u $ viewGroupLinkDeleted g - CRAcceptingGroupJoinRequest _ g c -> [ttyFullContact c <> ": accepting request to join group " <> ttyGroup' g <> "..."] - CRAcceptingGroupJoinRequestMember _ g m -> [ttyFullMember m <> ": accepting request to join group " <> ttyGroup' g <> "..."] - CRNoMemberContactCreating u g m -> ttyUser u ["member " <> ttyGroup' g <> " " <> ttyMember m <> " does not have direct connection, creating"] CRNewMemberContact u _ g m -> ttyUser u ["contact for member " <> ttyGroup' g <> " " <> ttyMember m <> " is created"] CRNewMemberContactSentInv u _ct g m -> ttyUser u ["sent invitation to connect directly to member " <> ttyGroup' g <> " " <> ttyMember m] - CRNewMemberContactReceivedInv u ct g m -> ttyUser u [ttyGroup' g <> " " <> ttyMember m <> " is creating direct contact " <> ttyContact' ct <> " with you"] - CRContactAndMemberAssociated u ct g m ct' -> ttyUser u $ viewContactAndMemberAssociated ct g m ct' - CRMemberSubError u g m e -> ttyUser u [ttyGroup' g <> " member " <> ttyMember m <> " error: " <> sShow e] - CRMemberSubSummary u summary -> ttyUser u $ viewErrorsSummary (filter (isJust . memberError) summary) " group member errors" - CRGroupSubscribed u g -> ttyUser u $ viewGroupSubscribed g - CRPendingSubSummary u _ -> ttyUser u [] - CRSndFileSubError u SndFileTransfer {fileId, fileName} e -> - ttyUser u ["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] - CRRcvFileSubError u RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} e -> - ttyUser u ["received file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] - CRCallInvitation RcvCallInvitation {user, contact, callType, sharedKey} -> ttyUser user $ viewCallInvitation contact callType sharedKey - CRCallOffer {user = u, contact, callType, offer, sharedKey} -> ttyUser u $ viewCallOffer contact callType offer sharedKey - CRCallAnswer {user = u, contact, answer} -> ttyUser u $ viewCallAnswer contact answer - CRCallExtraInfo {user = u, contact} -> ttyUser u ["call extra info from " <> ttyContact' contact] - CRCallEnded {user = u, contact} -> ttyUser u ["call with " <> ttyContact' contact <> " ended"] CRCallInvitations _ -> [] - CRUserContactLinkSubscribed -> ["Your address is active! To show: " <> highlight' "/sa"] - CRUserContactLinkSubError e -> ["user address error: " <> sShow e, "to delete your address: " <> highlight' "/da"] CRContactConnectionDeleted u PendingContactConnection {pccConnId} -> ttyUser u ["connection :" <> sShow pccConnId <> " deleted"] CRNtfTokenStatus status -> ["device token status: " <> plain (smpEncode status)] - CRNtfToken _ status mode -> ["device token status: " <> plain (smpEncode status) <> ", notifications mode: " <> plain (strEncode mode)] - CRNtfMessages {} -> [] - CRNtfMessage {} -> [] + CRNtfToken _ status mode srv -> ["device token status: " <> plain (smpEncode status) <> ", notifications mode: " <> plain (strEncode mode) <> ", server: " <> sShow srv] + CRNtfConns {ntfConns} -> map (\NtfConn {agentConnId, expectedMsg_} -> plain $ show agentConnId <> " " <> show expectedMsg_) ntfConns + CRConnNtfMessages ntfMsgs -> [sShow ntfMsgs] CRCurrentRemoteHost rhi_ -> [ maybe "Using local profile" @@ -294,40 +247,20 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe ] where started = " started on " <> B.unpack (strEncode address) <> ":" <> ctrlPort - CRRemoteHostSessionCode {remoteHost_, sessionCode} -> - [ maybe "new remote host connecting" (\RemoteHostInfo {remoteHostId = rhId} -> "remote host " <> sShow rhId <> " connecting") remoteHost_, - "Compare session code with host:", - plain sessionCode - ] - CRNewRemoteHost RemoteHostInfo {remoteHostId = rhId, hostDeviceName} -> ["new remote host " <> sShow rhId <> " added: " <> plain hostDeviceName] - CRRemoteHostConnected RemoteHostInfo {remoteHostId = rhId} -> ["remote host " <> sShow rhId <> " connected"] - CRRemoteHostStopped {remoteHostId_} -> - [ maybe "new remote host" (mappend "remote host " . sShow) remoteHostId_ <> " stopped" - ] CRRemoteFileStored rhId (CryptoFile filePath cfArgs_) -> [plain $ "file " <> filePath <> " stored on remote host " <> show rhId] - <> maybe [] ((: []) . plain . cryptoFileArgsStr testView) cfArgs_ + <> maybe [] ((: []) . cryptoFileArgsStr testView) cfArgs_ CRRemoteCtrlList cs -> viewRemoteCtrls cs - CRRemoteCtrlFound {remoteCtrl = RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName}, ctrlAppInfo_, appVersion, compatible} -> - [ ("remote controller " <> sShow remoteCtrlId <> " found: ") - <> maybe (deviceName <> "not compatible") (\info -> viewRemoteCtrl info appVersion compatible) ctrlAppInfo_ - ] - <> ["use " <> highlight ("/confirm remote ctrl " <> show remoteCtrlId) <> " to connect" | isJust ctrlAppInfo_ && compatible] - where - deviceName = if T.null ctrlDeviceName then "" else plain ctrlDeviceName <> ", " CRRemoteCtrlConnecting {remoteCtrl_, ctrlAppInfo, appVersion} -> [ (maybe "connecting new remote controller" (\RemoteCtrlInfo {remoteCtrlId} -> "connecting remote controller " <> sShow remoteCtrlId) remoteCtrl_ <> ": ") <> viewRemoteCtrl ctrlAppInfo appVersion True ] - CRRemoteCtrlSessionCode {remoteCtrl_, sessionCode} -> - [ maybe "new remote controller connected" (\RemoteCtrlInfo {remoteCtrlId} -> "remote controller " <> sShow remoteCtrlId <> " connected") remoteCtrl_, - "Compare session code with controller and use:", - "/verify remote ctrl " <> plain sessionCode -- TODO maybe pass rcId - ] CRRemoteCtrlConnected RemoteCtrlInfo {remoteCtrlId = rcId, ctrlDeviceName} -> ["remote controller " <> sShow rcId <> " session started with " <> plain ctrlDeviceName] - CRRemoteCtrlStopped {} -> ["remote controller stopped"] CRSQLResult rows -> map plain rows +#if !defined(dbPostgres) + CRArchiveExported archiveErrs -> if null archiveErrs then ["ok"] else ["archive export errors: " <> plain (show archiveErrs)] + CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)] CRSlowSQLQueries {chatQueries, agentQueries} -> let viewQuery SlowSQLQuery {query, queryStats = SlowQueryStats {count, timeMax, timeAvg}} = ("count: " <> sShow count) @@ -335,11 +268,14 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe <> (" :: avg: " <> sShow timeAvg <> " ms") <> (" :: " <> plain (T.unwords $ T.lines query)) in ("Chat queries" : map viewQuery chatQueries) <> [""] <> ("Agent queries" : map viewQuery agentQueries) - CRDebugLocks {chatLockName, agentLocks} -> +#endif + CRDebugLocks {chatLockName, chatEntityLocks, agentLocks} -> [ maybe "no chat lock" (("chat lock: " <>) . plain) chatLockName, - plain $ "agent locks: " <> LB.unpack (J.encode agentLocks) + "chat entity locks: " <> viewJSON chatEntityLocks, + "agent locks: " <> viewJSON agentLocks ] - CRAgentStats stats -> map (plain . intercalate ",") stats + CRAgentSubsTotal u subsTotal _ -> ttyUser u ["total subscriptions: " <> sShow subsTotal] + CRAgentServersSummary u serversSummary -> ttyUser u ["agent servers summary: " <> viewJSON serversSummary] CRAgentSubs {activeSubs, pendingSubs, removedSubs} -> [plain $ "Subscriptions: active = " <> show (sum activeSubs) <> ", pending = " <> show (sum pendingSubs) <> ", removed = " <> show (sum $ M.map length removedSubs)] <> ("active subscriptions:" : listSubs activeSubs) @@ -352,47 +288,31 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe ("active subscriptions:" : map sShow activeSubscriptions) <> ("pending subscriptions: " : map sShow pendingSubscriptions) <> ("removed subscriptions: " : map sShow removedSubscriptions) - CRConnectionDisabled entity -> viewConnectionEntityDisabled entity - CRAgentRcvQueueDeleted acId srv aqId err_ -> - [ ("completed deleting rcv queue, agent connection id: " <> sShow acId) - <> (", server: " <> sShow srv) - <> (", agent queue id: " <> sShow aqId) - <> maybe "" (\e -> ", error: " <> sShow e) err_ - | logLevel <= CLLInfo + CRAgentWorkersSummary {agentWorkersSummary} -> ["agent workers summary: " <> viewJSON agentWorkersSummary] + CRAgentWorkersDetails {agentWorkersDetails} -> + [ "agent workers details:", + viewJSON agentWorkersDetails -- this would be huge, but copypastable when has its own line ] - CRAgentConnDeleted acId -> ["completed deleting connection, agent connection id: " <> sShow acId | logLevel <= CLLInfo] - CRAgentUserDeleted auId -> ["completed deleting user" <> if logLevel <= CLLInfo then ", agent user id: " <> sShow auId else ""] - CRMessageError u prefix err -> ttyUser u [plain prefix <> ": " <> plain err | prefix == "error" || logLevel <= CLLWarning] - CRChatCmdError u e -> ttyUserPrefix' u $ viewChatError logLevel testView e - CRChatError u e -> ttyUser' u $ viewChatError logLevel testView e - CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError logLevel testView) errs - CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)] - CRTimedAction _ _ -> [] + CRAgentQueuesInfo {agentQueuesInfo} -> + [ "agent queues info:", + plain . LB.unpack $ J.encode agentQueuesInfo + ] + CRAppSettings as -> ["app settings: " <> viewJSON as] + CRCustomChatResponse u r -> ttyUser' u $ map plain $ T.lines r where ttyUser :: User -> [StyledString] -> [StyledString] - ttyUser user@User {showNtfs, activeUser} ss - | showNtfs || activeUser = ttyUserPrefix user ss + ttyUser user@User {showNtfs, activeUser, viewPwdHash} ss + | (showNtfs && isNothing viewPwdHash) || activeUser = ttyUserPrefix hu outputRH user ss | otherwise = [] - ttyUserPrefix :: User -> [StyledString] -> [StyledString] - ttyUserPrefix _ [] = [] - ttyUserPrefix User {userId, localDisplayName = u} ss - | null prefix = ss - | otherwise = prependFirst ("[" <> mconcat prefix <> "] ") ss - where - prefix = intersperse ", " $ remotePrefix <> userPrefix - remotePrefix = [maybe "local" (("remote: " <>) . highlight . show) outputRH | outputRH /= currentRH] - userPrefix = ["user: " <> highlight u | Just userId /= currentUserId] - currentUserId = (\User {userId = uId} -> uId) <$> user_ ttyUser' :: Maybe User -> [StyledString] -> [StyledString] ttyUser' = maybe id ttyUser - ttyUserPrefix' :: Maybe User -> [StyledString] -> [StyledString] - ttyUserPrefix' = maybe id ttyUserPrefix testViewChats :: [AChat] -> [StyledString] testViewChats chats = [sShow $ map toChatView chats] where toChatView :: AChat -> (Text, Text, Maybe ConnStatus) toChatView (AChat _ (Chat (DirectChat Contact {localDisplayName, activeConn}) items _)) = ("@" <> localDisplayName, toCIPreview items Nothing, connStatus <$> activeConn) toChatView (AChat _ (Chat (GroupChat GroupInfo {membership, localDisplayName}) items _)) = ("#" <> localDisplayName, toCIPreview items (Just membership), Nothing) + toChatView (AChat _ (Chat (LocalChat _) items _)) = ("*", toCIPreview items Nothing, Nothing) toChatView (AChat _ (Chat (ContactRequest UserContactRequest {localDisplayName}) items _)) = ("<@" <> localDisplayName, toCIPreview items Nothing, Nothing) toChatView (AChat _ (Chat (ContactConnection PendingContactConnection {pccConnId, pccConnStatus}) items _)) = (":" <> T.pack (show pccConnId), toCIPreview items Nothing, Just pccConnStatus) toCIPreview :: [CChatItem c] -> Maybe GroupMember -> Text @@ -416,32 +336,220 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe testViewItem (CChatItem _ ci@ChatItem {meta = CIMeta {itemText}}) membership_ = let deleted_ = maybe "" (\t -> " [" <> t <> "]") (chatItemDeletedText ci membership_) in itemText <> deleted_ - viewErrorsSummary :: [a] -> StyledString -> [StyledString] - viewErrorsSummary summary s = [ttyError (T.pack . show $ length summary) <> s <> " (run with -c option to show each error)" | not (null summary)] - contactList :: [ContactRef] -> String - contactList cs = T.unpack . T.intercalate ", " $ map (\ContactRef {localDisplayName = n} -> "@" <> n) cs unmuted :: User -> ChatInfo c -> ChatItem c d -> [StyledString] -> [StyledString] - unmuted u chat ci@ChatItem {chatDir} = unmuted' u chat chatDir $ isMention ci + unmuted u chat ci@ChatItem {chatDir} = unmuted' u chat chatDir $ isUserMention ci unmutedReaction :: User -> ChatInfo c -> CIReaction c d -> [StyledString] -> [StyledString] unmutedReaction u chat CIReaction {chatDir} = unmuted' u chat chatDir False unmuted' :: User -> ChatInfo c -> CIDirection c d -> Bool -> [StyledString] -> [StyledString] unmuted' u chat chatDir mention s | chatDirNtf u chat chatDir mention = s + | testView = map (<> " <muted>") s + | otherwise = [] + withMessages wm = if wm then " with all messages" else "" + +ttyUserPrefix :: (Maybe RemoteHostId, Maybe User) -> Maybe RemoteHostId -> User -> [StyledString] -> [StyledString] +ttyUserPrefix _ _ _ [] = [] +ttyUserPrefix (currentRH, user_) outputRH User {userId, localDisplayName = u} ss + | null prefix = ss + | otherwise = prependFirst ("[" <> mconcat prefix <> "] ") ss + where + prefix = intersperse ", " $ remotePrefix <> userPrefix + remotePrefix = [maybe "local" (("remote: " <>) . highlight . show) outputRH | outputRH /= currentRH] + userPrefix = ["user: " <> highlight u | Just userId /= currentUserId] + currentUserId = (\User {userId = uId} -> uId) <$> user_ + +viewErrorsSummary :: [a] -> StyledString -> [StyledString] +viewErrorsSummary summary s = [ttyError (T.pack . show $ length summary) <> s <> " (run with -c option to show each error)" | not (null summary)] + +contactList :: [ContactRef] -> String +contactList cs = T.unpack . T.intercalate ", " $ map (\ContactRef {localDisplayName = n} -> "@" <> n) cs + +chatEventToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatEvent -> [StyledString] +chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView} liveItems ts tz outputRH = \case + CEvtChatSuspended -> ["chat suspended"] + CEvtContactSwitch u ct progress -> ttyUser u $ viewContactSwitch ct progress + CEvtGroupMemberSwitch u g m progress -> ttyUser u $ viewGroupMemberSwitch g m progress + CEvtContactRatchetSync u ct progress -> ttyUser u $ viewContactRatchetSync ct progress + CEvtGroupMemberRatchetSync u g m progress -> ttyUser u $ viewGroupMemberRatchetSync g m progress + CEvtNewChatItems u chatItems -> viewChatItems ttyUser unmuted u chatItems ts tz + CEvtChatItemsStatusesUpdated u chatItems + | length chatItems <= 20 -> + concatMap + (\ci -> ttyUser u $ viewChatItemStatusUpdated ci ts tz testView showReceipts) + chatItems + | testView && showReceipts -> + ttyUser u [sShow (length chatItems) <> " message statuses updated"] + | otherwise -> [] + CEvtChatItemUpdated u (AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewItemUpdate chat item liveItems ts tz + CEvtChatItemNotChanged u ci -> ttyUser u $ viewItemNotChanged ci + CEvtChatItemReaction u added (ACIReaction _ _ chat reaction) -> ttyUser u $ unmutedReaction u chat reaction $ viewItemReaction showReactions chat reaction added ts tz + CEvtChatItemsDeleted u deletions byUser timed -> ttyUser u $ viewChatItemsDeleted (unmuted u) deletions byUser timed ts tz testView + CEvtGroupChatItemsDeleted u g ciIds byUser member_ -> ttyUser u $ viewGroupChatItemsDeleted g ciIds byUser member_ + CEvtChatItemDeletedNotFound u Contact {localDisplayName = c} _ -> ttyUser u [ttyFrom $ c <> "> [deleted - original message not found]"] + CEvtUserAcceptedGroupSent u _g _ -> ttyUser u [] -- [ttyGroup' g <> ": joining the group..."] + CEvtSentGroupInvitation u g c _ -> ttyUser u $ viewSentGroupInvitation g c + CEvtContactDeletedByContact u c -> ttyUser u [ttyFullContact c <> " deleted contact with you"] + CEvtAcceptingContactRequest u c -> ttyUser u $ viewAcceptingContactRequest c + CEvtAcceptingBusinessRequest u g -> ttyUser u $ viewAcceptingBusinessRequest g + CEvtContactRequestAlreadyAccepted u c -> ttyUser u [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"] + CEvtBusinessRequestAlreadyAccepted u g -> ttyUser u [ttyFullGroup g <> ": sent you a duplicate connection request, but you are already connected, no action needed"] + CEvtGroupLinkConnecting u g _ -> ttyUser u [ttyGroup' g <> ": joining the group..."] + CEvtBusinessLinkConnecting u g _ _ -> ttyUser u [ttyGroup' g <> ": joining the group..."] + CEvtUnknownMemberCreated u g fwdM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember fwdM <> " forwarded a message from an unknown member, creating unknown member record " <> ttyMember um] + CEvtUnknownMemberBlocked u g byM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember byM <> " blocked an unknown member, creating unknown member record " <> ttyMember um] + CEvtUnknownMemberAnnounced u g _ um m -> ttyUser u [ttyGroup' g <> ": unknown member " <> ttyMember um <> " updated to " <> ttyMember m] + CEvtRcvFileDescrReady _ _ _ _ -> [] + CEvtRcvFileAccepted u ci -> ttyUser u $ savingFile' ci + CEvtRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft + CEvtRcvFileProgressXFTP {} -> [] + CEvtContactUpdated {user = u, fromContact = c, toContact = c'} -> ttyUser u $ viewContactUpdated c c' <> viewContactPrefsUpdated u c c' + CEvtGroupMemberUpdated {} -> [] + CEvtContactsMerged u intoCt mergedCt ct' -> ttyUser u $ viewContactsMerged intoCt mergedCt ct' + CEvtReceivedContactRequest u UserContactRequest {localDisplayName = c, profile} -> ttyUser u $ viewReceivedContactRequest c profile + CEvtRcvFileStart u ci -> ttyUser u $ receivingFile_' hu testView "started" ci + CEvtRcvFileComplete u ci -> ttyUser u $ receivingFile_' hu testView "completed" ci + CEvtRcvStandaloneFileComplete u _ ft -> ttyUser u $ receivingFileStandalone "completed" ft + CEvtRcvFileSndCancelled u _ ft -> ttyUser u $ viewRcvFileSndCancelled ft + CEvtRcvFileError u (Just ci) e _ -> ttyUser u $ receivingFile_' hu testView "error" ci <> [sShow e] + CEvtRcvFileError u Nothing e ft -> ttyUser u $ receivingFileStandalone "error" ft <> [sShow e] + CEvtRcvFileWarning u (Just ci) e _ -> ttyUser u $ receivingFile_' hu testView "warning: " ci <> [sShow e] + CEvtRcvFileWarning u Nothing e ft -> ttyUser u $ receivingFileStandalone "warning: " ft <> [sShow e] + CEvtSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft + CEvtSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft + CEvtSndFileStartXFTP {} -> [] + CEvtSndFileProgressXFTP {} -> [] + CEvtSndFileRedirectStartXFTP u ft ftRedirect -> ttyUser u $ standaloneUploadRedirect ft ftRedirect + CEvtSndStandaloneFileComplete u ft uris -> ttyUser u $ standaloneUploadComplete ft uris + CEvtSndFileCompleteXFTP u ci _ -> ttyUser u $ uploadingFile "completed" ci + CEvtSndFileCancelledXFTP {} -> [] + CEvtSndFileError u Nothing ft e -> ttyUser u $ uploadingFileStandalone "error" ft <> [plain e] + CEvtSndFileError u (Just ci) _ e -> ttyUser u $ uploadingFile "error" ci <> [plain e] + CEvtSndFileWarning u Nothing ft e -> ttyUser u $ uploadingFileStandalone "warning: " ft <> [plain e] + CEvtSndFileWarning u (Just ci) _ e -> ttyUser u $ uploadingFile "warning: " ci <> [plain e] + CEvtSndFileRcvCancelled u _ ft@SndFileTransfer {recipientDisplayName = c} -> + ttyUser u [ttyContact c <> " cancelled receiving " <> sndFile ft] + CEvtContactConnecting u _ -> ttyUser u [] + CEvtContactConnected u ct userCustomProfile -> ttyUser u $ viewContactConnected ct userCustomProfile testView + CEvtContactSndReady u ct -> ttyUser u [ttyFullContact ct <> ": you can send messages to contact"] + CEvtContactAnotherClient u c -> ttyUser u [ttyContact' c <> ": contact is connected to another client"] + CEvtSubscriptionEnd u acEntity -> + let Connection {connId} = entityConnection acEntity + in ttyUser u [sShow connId <> ": END"] + CEvtContactsDisconnected srv cs -> [plain $ "server disconnected " <> showSMPServer srv <> " (" <> contactList cs <> ")"] + CEvtContactsSubscribed srv cs -> [plain $ "server connected " <> showSMPServer srv <> " (" <> contactList cs <> ")"] + CEvtContactSubError u c e -> ttyUser u [ttyContact' c <> ": contact error " <> sShow e] + CEvtContactSubSummary u summary -> + ttyUser u $ [sShow (length subscribed) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)" | not (null subscribed)] <> viewErrorsSummary errors " contact errors" + where + (errors, subscribed) = partition (isJust . contactError) summary + CEvtUserContactSubSummary u summary -> + ttyUser u $ + map addressSS addresses + <> ([sShow (length groupLinksSubscribed) <> " group links active" | not (null groupLinksSubscribed)] <> viewErrorsSummary groupLinkErrors " group link errors") + where + (addresses, groupLinks) = partition (\UserContactSubStatus {userContact} -> isNothing . userContactGroupId $ userContact) summary + addressSS UserContactSubStatus {userContactError} = maybe ("Your address is active! To show: " <> highlight' "/sa") (\e -> "User address error: " <> sShow e <> ", to delete your address: " <> highlight' "/da") userContactError + (groupLinkErrors, groupLinksSubscribed) = partition (isJust . userContactError) groupLinks + CEvtNetworkStatus status conns -> if testView then [plain $ show (length conns) <> " connections " <> netStatusStr status] else [] + CEvtNetworkStatuses u statuses -> if testView then ttyUser' u $ viewNetworkStatuses statuses else [] + CEvtReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g c r + CEvtUserJoinedGroup u g _ -> ttyUser u $ viewUserJoinedGroup g + CEvtJoinedGroupMember u g m -> ttyUser u $ viewJoinedGroupMember g m + CEvtHostConnected p h -> [plain $ "connected to " <> viewHostEvent p h] + CEvtHostDisconnected p h -> [plain $ "disconnected from " <> viewHostEvent p h] + CEvtJoinedGroupMemberConnecting u g host m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting...)"] + CEvtConnectedToGroupMember u g m _ -> ttyUser u [ttyGroup' g <> ": " <> connectedMember m <> " is connected"] + CEvtMemberRole u g by m r r' -> ttyUser u $ viewMemberRoleChanged g by m r r' + CEvtMemberBlockedForAll u g by m blocked -> ttyUser u $ viewMemberBlockedForAll g by m blocked + CEvtDeletedMemberUser u g by wm -> ttyUser u $ [ttyGroup' g <> ": " <> ttyMember by <> " removed you from the group" <> withMessages wm] <> groupPreserved g + CEvtDeletedMember u g by m wm -> ttyUser u [ttyGroup' g <> ": " <> ttyMember by <> " removed " <> ttyMember m <> " from the group" <> withMessages wm] + CEvtLeftMember u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " left the group"] + CEvtGroupDeleted u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " deleted the group", "use " <> highlight ("/d #" <> viewGroupName g) <> " to delete the local copy of the group"] + CEvtGroupUpdated u g g' m -> ttyUser u $ viewGroupUpdated g g' m + CEvtAcceptingGroupJoinRequestMember _ g m -> [ttyFullMember m <> ": accepting request to join group " <> ttyGroup' g <> "..."] + CEvtNoMemberContactCreating u g m -> ttyUser u ["member " <> ttyGroup' g <> " " <> ttyMember m <> " does not have direct connection, creating"] + CEvtNewMemberContactReceivedInv u ct g m -> ttyUser u [ttyGroup' g <> " " <> ttyMember m <> " is creating direct contact " <> ttyContact' ct <> " with you"] + CEvtContactAndMemberAssociated u ct g m ct' -> ttyUser u $ viewContactAndMemberAssociated ct g m ct' + CEvtCallInvitation RcvCallInvitation {user, contact, callType, sharedKey} -> ttyUser user $ viewCallInvitation contact callType sharedKey + CEvtCallOffer {user = u, contact, callType, offer, sharedKey} -> ttyUser u $ viewCallOffer contact callType offer sharedKey + CEvtCallAnswer {user = u, contact, answer} -> ttyUser u $ viewCallAnswer contact answer + CEvtCallExtraInfo {user = u, contact} -> ttyUser u ["call extra info from " <> ttyContact' contact] + CEvtCallEnded {user = u, contact} -> ttyUser u ["call with " <> ttyContact' contact <> " ended"] + CEvtNtfMessage {} -> [] + CEvtRemoteHostSessionCode {remoteHost_, sessionCode} -> + [ maybe "new remote host connecting" (\RemoteHostInfo {remoteHostId = rhId} -> "remote host " <> sShow rhId <> " connecting") remoteHost_, + "Compare session code with host:", + plain sessionCode + ] + CEvtNewRemoteHost RemoteHostInfo {remoteHostId = rhId, hostDeviceName} -> ["new remote host " <> sShow rhId <> " added: " <> plain hostDeviceName] + CEvtRemoteHostConnected RemoteHostInfo {remoteHostId = rhId} -> ["remote host " <> sShow rhId <> " connected"] + CEvtRemoteHostStopped {remoteHostId_} -> + [ maybe "new remote host" (mappend "remote host " . sShow) remoteHostId_ <> " stopped" + ] + CEvtRemoteCtrlFound {remoteCtrl = RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName}, ctrlAppInfo_, appVersion, compatible} -> + [ ("remote controller " <> sShow remoteCtrlId <> " found: ") + <> maybe (deviceName <> "not compatible") (\info -> viewRemoteCtrl info appVersion compatible) ctrlAppInfo_ + ] + <> ["use " <> highlight ("/confirm remote ctrl " <> show remoteCtrlId) <> " to connect" | isJust ctrlAppInfo_ && compatible] + where + deviceName = if T.null ctrlDeviceName then "" else plain ctrlDeviceName <> ", " + CEvtRemoteCtrlSessionCode {remoteCtrl_, sessionCode} -> + [ maybe "new remote controller connected" (\RemoteCtrlInfo {remoteCtrlId} -> "remote controller " <> sShow remoteCtrlId <> " connected") remoteCtrl_, + "Compare session code with controller and use:", + "/verify remote ctrl " <> plain sessionCode -- TODO maybe pass rcId + ] + CEvtRemoteCtrlStopped {rcStopReason} -> viewRemoteCtrlStopped rcStopReason + CEvtContactPQEnabled u c (CR.PQEncryption pqOn) -> ttyUser u [ttyContact' c <> ": " <> (if pqOn then "quantum resistant" else "standard") <> " end-to-end encryption enabled"] + CEvtContactDisabled u c -> ttyUser u ["[" <> ttyContact' c <> "] connection is disabled, to enable: " <> highlight ("/enable " <> viewContactName c) <> ", to delete: " <> highlight ("/d " <> viewContactName c)] + CEvtConnectionDisabled entity -> viewConnectionEntityDisabled entity + CEvtConnectionInactive entity inactive -> viewConnectionEntityInactive entity inactive + CEvtAgentRcvQueuesDeleted delQs -> ["completed deleting rcv queues: " <> sShow (length delQs) | logLevel <= CLLInfo] + CEvtAgentConnsDeleted acIds -> ["completed deleting connections: " <> sShow (length acIds) | logLevel <= CLLInfo] + CEvtAgentUserDeleted auId -> ["completed deleting user" <> if logLevel <= CLLInfo then ", agent user id: " <> sShow auId else ""] + CEvtMessageError u prefix err -> ttyUser u [plain prefix <> ": " <> plain err | prefix == "error" || logLevel <= CLLWarning] + CEvtChatErrors errs -> concatMap (viewChatError False logLevel testView) errs + CEvtTimedAction _ _ -> [] + CEvtTerminalEvent te -> case te of + TERejectingGroupJoinRequestMember _ g m reason -> [ttyFullMember m <> ": rejecting request to join group " <> ttyGroup' g <> ", reason: " <> sShow reason] + TEGroupLinkRejected u g reason -> ttyUser u [ttyGroup' g <> ": join rejected, reason: " <> sShow reason] + TENewMemberContact u _ g m -> ttyUser u ["contact for member " <> ttyGroup' g <> " " <> ttyMember m <> " is created"] + TEContactVerificationReset u ct -> ttyUser u $ viewContactVerificationReset ct + TEGroupMemberVerificationReset u g m -> ttyUser u $ viewGroupMemberVerificationReset g m + TEGroupSubscribed u ShortGroupInfo {groupName = g} -> ttyUser u $ viewGroupSubscribed g + TEGroupInvitation u g -> ttyUser u [groupInvitationSub g] + TEGroupEmpty u ShortGroupInfo {groupName = g} -> ttyUser u [ttyGroup g <> ": group is empty"] + TEMemberSubError u ShortGroupInfo {groupName = g} ShortGroupMember {memberName = n} e -> ttyUser u [ttyGroup g <> " member " <> ttyContact n <> " error: " <> sShow e] + TEMemberSubSummary u summary -> ttyUser u $ viewErrorsSummary (filter (isJust . memberError) summary) " group member errors" + TEPendingSubSummary u _ -> ttyUser u [] + TESndFileSubError u SndFileTransfer {fileId, fileName} e -> + ttyUser u ["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] + TERcvFileSubError u RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} e -> + ttyUser u ["received file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] + where + ttyUser :: User -> [StyledString] -> [StyledString] + ttyUser user@User {showNtfs, activeUser, viewPwdHash} ss + | (showNtfs && isNothing viewPwdHash) || activeUser = ttyUserPrefix hu outputRH user ss + | otherwise = [] + ttyUser' :: Maybe User -> [StyledString] -> [StyledString] + ttyUser' = maybe id ttyUser + withMessages wm = if wm then " with all messages" else "" + unmuted :: User -> ChatInfo c -> ChatItem c d -> [StyledString] -> [StyledString] + unmuted u chat ci@ChatItem {chatDir} = unmuted' u chat chatDir $ isUserMention ci + unmutedReaction :: User -> ChatInfo c -> CIReaction c d -> [StyledString] -> [StyledString] + unmutedReaction u chat CIReaction {chatDir} = unmuted' u chat chatDir False + unmuted' :: User -> ChatInfo c -> CIDirection c d -> Bool -> [StyledString] -> [StyledString] + unmuted' u chat chatDir mention s + | chatDirNtf u chat chatDir mention = s + | testView = map (<> " <muted>") s | otherwise = [] userNtf :: User -> Bool userNtf User {showNtfs, activeUser} = showNtfs || activeUser -chatNtf :: User -> ChatInfo c -> Bool -> Bool -chatNtf user cInfo mention = case cInfo of - DirectChat ct -> contactNtf user ct mention - GroupChat g -> groupNtf user g mention - _ -> False - chatDirNtf :: User -> ChatInfo c -> CIDirection c d -> Bool -> Bool chatDirNtf user cInfo chatDir mention = case (cInfo, chatDir) of (DirectChat ct, CIDirectRcv) -> contactNtf user ct mention - (GroupChat g, CIGroupRcv m) -> groupNtf user g mention && showMessages (memberSettings m) + (GroupChat g, CIGroupRcv m) -> groupNtf user g mention && not (blockedByAdmin m) && showMessages (memberSettings m) _ -> True contactNtf :: User -> Contact -> Bool -> Bool @@ -464,6 +572,7 @@ chatItemDeletedText ChatItem {meta = CIMeta {itemDeleted}, content} membership_ CIModerated _ m -> markedDeleted content <> byMember m CIDeleted _ -> markedDeleted content CIBlocked _ -> "blocked" + CIBlockedByAdmin _ -> "blocked by admin" markedDeleted = \case CISndModerated -> "deleted" CIRcvModerated -> "deleted" @@ -490,11 +599,11 @@ viewUsersList us = <> ["muted" | not showNtfs] <> [plain ("unread: " <> show count) | count /= 0] -viewGroupSubscribed :: GroupInfo -> [StyledString] -viewGroupSubscribed g = [membershipIncognito g <> ttyFullGroup g <> ": connected to server(s)"] +viewGroupSubscribed :: GroupName -> [StyledString] +viewGroupSubscribed g = [ttyGroup g <> ": connected to server(s)"] showSMPServer :: SMPServer -> String -showSMPServer srv = B.unpack $ strEncode srv.host +showSMPServer ProtocolServer {host} = B.unpack $ strEncode host viewHostEvent :: AProtocolType -> TransportHost -> String viewHostEvent p h = map toUpper (B.unpack $ strEncode p) <> " host " <> B.unpack (strEncode h) @@ -513,45 +622,88 @@ viewChats ts tz = concatMap chatPreview . reverse GroupChat g -> [" " <> ttyToGroup g] _ -> [] +viewChatItems :: + (User -> [StyledString] -> [StyledString]) -> + (forall c d. User -> ChatInfo c -> ChatItem c d -> [StyledString] -> [StyledString]) -> + User -> + [AChatItem] -> + UTCTime -> + TimeZone -> + [StyledString] +viewChatItems ttyUser unmuted u chatItems ts tz + | length chatItems <= 20 = + concatMap + (\(AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewChatItem chat item False ts tz <> viewItemReactions 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"] + | otherwise = ttyUser u [sShow (length chatItems) <> " new messages created"] + viewChatItem :: forall c d. MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> CurrentTime -> TimeZone -> [StyledString] -viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {forwardedByMember}, content, quotedItem, file} doShow ts tz = +viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwardedByMember, userMention}, content, quotedItem, file} doShow ts tz = withGroupMsgForwarded . withItemDeleted <$> viewCI where viewCI = case chat of DirectChat c -> case chatDir of CIDirectSnd -> case content of - CISndMsgContent mc -> hideLive meta $ withSndFile to $ sndMsg to quote mc + CISndMsgContent mc -> hideLive meta $ withSndFile to $ sndMsg to context mc CISndGroupEvent {} -> showSndItemProhibited to _ -> showSndItem to where to = ttyToContact' c CIDirectRcv -> case content of - CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from quote mc + CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from context mc CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta CIRcvGroupEvent {} -> showRcvItemProhibited from _ -> showRcvItem from where from = ttyFromContact c where - quote = maybe [] (directQuote chatDir) quotedItem + context = + maybe + (maybe [] forwardedFrom itemForwarded) + (directQuote chatDir) + quotedItem GroupChat g -> case chatDir of CIGroupSnd -> case content of - CISndMsgContent mc -> hideLive meta $ withSndFile to $ sndMsg to quote mc + CISndMsgContent mc -> hideLive meta $ withSndFile to $ sndMsg to context mc CISndGroupInvitation {} -> showSndItemProhibited to _ -> showSndItem to where to = ttyToGroup g CIGroupRcv m -> case content of - CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from quote mc + CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from context mc CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta CIRcvGroupInvitation {} -> showRcvItemProhibited from - CIRcvModerated {} -> receivedWithTime_ ts tz (ttyFromGroup g m) quote meta [plainContent content] False + CIRcvModerated {} -> receivedWithTime_ ts tz (ttyFromGroup g m) context meta [plainContent content] False + CIRcvBlocked {} -> receivedWithTime_ ts tz (ttyFromGroup g m) context meta [plainContent content] False _ -> showRcvItem from where - from = ttyFromGroup g m + from = ttyFromGroupAttention g m userMention where - quote = maybe [] (groupQuote g) quotedItem - _ -> [] + context = + maybe + (maybe [] forwardedFrom itemForwarded) + (groupQuote g) + quotedItem + LocalChat _ -> case chatDir of + CILocalSnd -> case content of + CISndMsgContent mc -> hideLive meta $ withLocalFile to $ sndMsg to context mc + CISndGroupEvent {} -> showSndItemProhibited to + _ -> showSndItem to + where + to = "* " + CILocalRcv -> case content of + CIRcvMsgContent mc -> withLocalFile from $ rcvMsg from context mc + CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta + CIRcvGroupEvent {} -> showRcvItemProhibited from + _ -> showRcvItem from + where + from = "* " + where + context = maybe [] forwardedFrom itemForwarded + ContactRequest {} -> [] + ContactConnection {} -> [] withItemDeleted item = case chatItemDeletedText ci (chatInfoMembership chat) of Nothing -> item Just t -> item <> styled (colored Red) (" [" <> t <> "]") @@ -560,13 +712,14 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {forwardedByMember}, Just _ -> item <> styled (colored Yellow) (" [>>]" :: String) withSndFile = withFile viewSentFileInvitation withRcvFile = withFile viewReceivedFileInvitation + withLocalFile = withFile viewLocalFile withFile view dir l = maybe l (\f -> l <> view dir f ts tz meta) file sndMsg = msg viewSentMessage rcvMsg = msg viewReceivedMessage - msg view dir quote mc = case (msgContentText mc, file, quote) of + msg view dir context mc = case (msgContentText mc, file, context) of ("", Just _, []) -> [] - ("", Just CIFile {fileName}, _) -> view dir quote (MCText $ T.pack fileName) ts tz meta - _ -> view dir quote mc ts tz meta + ("", Just CIFile {fileName}, _) -> view dir context (MCText $ T.pack fileName) ts tz meta + _ -> view dir context mc ts tz meta showSndItem to = showItem $ sentWithTime_ ts tz [to <> plainContent content] meta showRcvItem from = showItem $ receivedWithTime_ ts tz from [] meta [plainContent content] False showSndItemProhibited to = showItem $ sentWithTime_ ts tz [to <> plainContent content <> " " <> prohibited] meta @@ -576,11 +729,12 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {forwardedByMember}, prohibited = styled (colored Red) ("[unexpected chat item created, please report to developers]" :: String) viewChatItemInfo :: AChatItem -> ChatItemInfo -> TimeZone -> [StyledString] -viewChatItemInfo (AChatItem _ msgDir _ ChatItem {meta = CIMeta {itemTs, itemTimed, createdAt}}) ChatItemInfo {itemVersions} tz = +viewChatItemInfo (AChatItem _ msgDir _ ChatItem {meta = CIMeta {itemTs, itemTimed, createdAt}}) ChatItemInfo {itemVersions, forwardedFromChatItem} tz = ["sent at: " <> ts itemTs] <> receivedAt <> toBeDeletedAt <> versions + <> forwardedFrom' where ts = styleTime . localTs tz receivedAt = case msgDir of @@ -593,7 +747,21 @@ viewChatItemInfo (AChatItem _ msgDir _ ChatItem {meta = CIMeta {itemTs, itemTime if null itemVersions then [] else ["message history:"] <> concatMap version itemVersions - version ChatItemVersion {msgContent, itemVersionTs} = prependFirst (ts itemVersionTs <> styleTime ": ") $ ttyMsgContent msgContent + where + version ChatItemVersion {msgContent, itemVersionTs} = prependFirst (ts itemVersionTs <> styleTime ": ") $ ttyMsgContent msgContent + forwardedFrom' = + case forwardedFromChatItem of + Just fwdACI@(AChatItem _ fwdMsgDir fwdChatInfo _) -> + [plain $ "forwarded from: " <> maybe "" (<> ", ") fwdDir_ <> fwdItemId] + where + fwdDir_ = case (fwdMsgDir, fwdChatInfo) of + (SMDSnd, DirectChat ct) -> Just $ "you @" <> viewContactName ct + (SMDRcv, DirectChat ct) -> Just $ "@" <> viewContactName ct + (SMDSnd, GroupChat gInfo) -> Just $ "you #" <> viewGroupName gInfo + (SMDRcv, GroupChat gInfo) -> Just $ "#" <> viewGroupName gInfo + _ -> Nothing + fwdItemId = "chat item id: " <> (T.pack . show $ aChatItemId fwdACI) + _ -> [] localTs :: TimeZone -> UTCTime -> String localTs tz ts = do @@ -604,14 +772,10 @@ localTs tz ts = do viewChatItemStatusUpdated :: AChatItem -> CurrentTime -> TimeZone -> Bool -> Bool -> [StyledString] viewChatItemStatusUpdated (AChatItem _ _ chat item@ChatItem {meta = CIMeta {itemStatus}}) ts tz testView showReceipts = case itemStatus of - CISSndRcvd rcptStatus SSPPartial -> - if testView && showReceipts - then prependFirst (viewDeliveryReceiptPartial rcptStatus <> " ") $ viewChatItem chat item False ts tz - else [] - CISSndRcvd rcptStatus SSPComplete -> - if testView && showReceipts - then prependFirst (viewDeliveryReceipt rcptStatus <> " ") $ viewChatItem chat item False ts tz - else [] + CISSndRcvd rcptStatus SSPPartial | testView && showReceipts -> + prependFirst (viewDeliveryReceiptPartial rcptStatus <> " ") $ viewChatItem chat item False ts tz + CISSndRcvd rcptStatus SSPComplete | testView && showReceipts -> + prependFirst (viewDeliveryReceipt rcptStatus <> " ") $ viewChatItem chat item False ts tz _ -> [] viewDeliveryReceiptPartial :: MsgReceiptStatus -> StyledString @@ -625,37 +789,45 @@ viewDeliveryReceipt = \case MRBadMsgHash -> ttyError' "⩗!" viewItemUpdate :: MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> CurrentTime -> TimeZone -> [StyledString] -viewItemUpdate chat ChatItem {chatDir, meta = meta@CIMeta {itemEdited, itemLive}, content, quotedItem} liveItems ts tz = case chat of +viewItemUpdate chat ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, itemEdited, itemLive}, content, quotedItem} liveItems ts tz = case chat of DirectChat c -> case chatDir of CIDirectRcv -> case content of CIRcvMsgContent mc | itemLive == Just True && not liveItems -> [] - | otherwise -> viewReceivedUpdatedMessage from quote mc ts tz meta + | otherwise -> viewReceivedUpdatedMessage from context mc ts tz meta _ -> [] where from = if itemEdited then ttyFromContactEdited c else ttyFromContact c CIDirectSnd -> case content of - CISndMsgContent mc -> hideLive meta $ viewSentMessage to quote mc ts tz meta + CISndMsgContent mc -> hideLive meta $ viewSentMessage to context mc ts tz meta _ -> [] where to = if itemEdited then ttyToContactEdited' c else ttyToContact' c where - quote = maybe [] (directQuote chatDir) quotedItem + context = + maybe + (maybe [] forwardedFrom itemForwarded) + (directQuote chatDir) + quotedItem GroupChat g -> case chatDir of CIGroupRcv m -> case content of CIRcvMsgContent mc | itemLive == Just True && not liveItems -> [] - | otherwise -> viewReceivedUpdatedMessage from quote mc ts tz meta + | otherwise -> viewReceivedUpdatedMessage from context mc ts tz meta _ -> [] where from = if itemEdited then ttyFromGroupEdited g m else ttyFromGroup g m CIGroupSnd -> case content of - CISndMsgContent mc -> hideLive meta $ viewSentMessage to quote mc ts tz meta + CISndMsgContent mc -> hideLive meta $ viewSentMessage to context mc ts tz meta _ -> [] where to = if itemEdited then ttyToGroupEdited g else ttyToGroup g where - quote = maybe [] (groupQuote g) quotedItem + context = + maybe + (maybe [] forwardedFrom itemForwarded) + (groupQuote g) + quotedItem _ -> [] hideLive :: CIMeta c d -> [StyledString] -> [StyledString] @@ -667,6 +839,23 @@ viewItemNotChanged (AChatItem _ msgDir _ _) = case msgDir of SMDSnd -> ["message didn't change"] SMDRcv -> [] +viewChatItemsDeleted :: + (forall c d. ChatInfo c -> ChatItem c d -> [StyledString] -> [StyledString]) -> + [ChatItemDeletion] -> + Bool -> + Bool -> + UTCTime -> + TimeZone -> + Bool -> + [StyledString] +viewChatItemsDeleted unmuted deletions byUser timed ts tz testView = case deletions of + [ChatItemDeletion (AChatItem _ _ chat deletedItem) toItem] -> + unmuted chat deletedItem $ viewItemDelete chat deletedItem toItem byUser timed ts tz testView + deletions' -> [sShow (length deletions') <> " messages deleted"] + +viewGroupChatItemsDeleted :: GroupInfo -> [ChatItemId] -> Bool -> Maybe GroupMember -> [StyledString] +viewGroupChatItemsDeleted g ciIds byUser member_ = [ttyGroup' g <> ": " <> sShow (length ciIds) <> " messages deleted by " <> if byUser then "user" else "member" <> maybe "" (\m -> " " <> ttyMember m) member_] + viewItemDelete :: ChatInfo c -> ChatItem c d -> Maybe AChatItem -> Bool -> Bool -> CurrentTime -> TimeZone -> Bool -> [StyledString] viewItemDelete chat ci@ChatItem {chatDir, meta, content = deletedContent} toItem byUser timed ts tz testView | timed = [plain ("timed message deleted: " <> T.unpack (ciContentToText deletedContent)) | testView] @@ -703,8 +892,15 @@ viewItemReaction showReactions chat CIReaction {chatDir, chatItem = CChatItem md where from = ttyFromGroup g m reactionMsg mc = quoteText mc . ttyQuotedMember . Just $ sentByMember' g itemDir + (LocalChat _, CILocalRcv) -> case ciMsgContent content of + Just mc -> view from $ reactionMsg mc + _ -> [] + where + from = "* " + reactionMsg mc = quoteText mc $ if toMsgDirection md == MDSnd then ">>" else ">" (_, CIDirectSnd) -> [sentText] (_, CIGroupSnd) -> [sentText] + (_, CILocalSnd) -> [sentText] where view from msg | showReactions = viewReceivedReaction from msg reactionText ts tz sentAt @@ -723,6 +919,9 @@ viewItemReactions ChatItem {reactions} = [" " <> viewReactions reactions | viewReaction CIReactionCount {reaction = MREmoji (MREmojiChar emoji), userReacted, totalReacted} = plain [emoji, ' '] <> (if userReacted then styled Italic else plain) (show totalReacted) +viewReactionMembers :: [MemberReaction] -> [StyledString] +viewReactionMembers memberReactions = [sShow (length memberReactions) <> " member(s) reacted"] + directQuote :: forall d'. MsgDirectionI d' => CIDirection 'CTDirect d' -> CIQuote 'CTDirect -> [StyledString] directQuote _ CIQuote {content = qmc, chatDir = quoteDir} = quoteText qmc $ if toMsgDirection (msgDirection @d') == quoteMsgDirection quoteDir then ">>" else ">" @@ -730,6 +929,14 @@ directQuote _ CIQuote {content = qmc, chatDir = quoteDir} = groupQuote :: GroupInfo -> CIQuote 'CTGroup -> [StyledString] groupQuote g CIQuote {content = qmc, chatDir = quoteDir} = quoteText qmc . ttyQuotedMember $ sentByMember g quoteDir +forwardedFrom :: CIForwardedFrom -> [StyledString] +forwardedFrom = \case + CIFFUnknown -> ["-> forwarded"] + CIFFContact c MDSnd _ _ -> ["<- you @" <> (plain . viewName) c] + CIFFContact c MDRcv _ _ -> ["<- @" <> (plain . viewName) c] + CIFFGroup g MDSnd _ _ -> ["<- you #" <> (plain . viewName) g] + CIFFGroup g MDRcv _ _ -> ["<- #" <> (plain . viewName) g] + sentByMember :: GroupInfo -> CIQDirection 'CTGroup -> Maybe GroupMember sentByMember GroupInfo {membership} = \case CIQGroupSnd -> Just membership @@ -764,14 +971,17 @@ viewInvalidConnReq = plain updateStr ] -viewConnReqInvitation :: ConnReqInvitation -> [StyledString] -viewConnReqInvitation cReq = +viewConnReqInvitation :: CreatedLinkInvitation -> [StyledString] +viewConnReqInvitation (CCLink cReq shortLink) = [ "pass this invitation link to your contact (via another channel): ", "", - (plain . strEncode) (simplexChatInvitation cReq), + plain $ maybe cReqStr strEncode shortLink, "", "and ask them to connect: " <> highlight' "/c <invitation_link_above>" ] + <> ["The invitation link for old clients: " <> plain cReqStr | isJust shortLink] + where + cReqStr = strEncode $ simplexChatInvitation cReq simplexChatInvitation :: ConnReqInvitation -> ConnReqInvitation simplexChatInvitation (CRInvitationUri crData e2e) = CRInvitationUri crData {crScheme = simplexChat} e2e @@ -788,7 +998,9 @@ viewChatCleared :: AChatInfo -> [StyledString] viewChatCleared (AChatInfo _ chatInfo) = case chatInfo of DirectChat ct -> [ttyContact' ct <> ": all messages are removed locally ONLY"] GroupChat gi -> [ttyGroup' gi <> ": all messages are removed locally ONLY"] - _ -> [] + LocalChat _ -> ["notes: all messages are removed"] + ContactRequest _ -> [] + ContactConnection _ -> [] viewContactsList :: [Contact] -> [StyledString] viewContactsList = @@ -810,38 +1022,68 @@ viewUserContactLinkDeleted = "To create a new chat address use " <> highlight' "/ad" ] -connReqContact_ :: StyledString -> ConnReqContact -> [StyledString] -connReqContact_ intro cReq = +viewForwardPlan :: Int -> [ChatItemId] -> Maybe ForwardConfirmation -> [StyledString] +viewForwardPlan count itemIds = maybe [forwardCount] $ \fc -> [confirmation fc, forwardCount] + where + confirmation = \case + FCFilesNotAccepted fileIds -> plain $ "Files can be received: " <> intercalate ", " (map show fileIds) + FCFilesInProgress cnt -> plain $ "Still receiving " <> show cnt <> " file(s)" + FCFilesMissing cnt -> plain $ show cnt <> " file(s) are missing" + FCFilesFailed cnt -> plain $ "Receiving " <> show cnt <> " file(s) failed" + forwardCount + | count == len = "all messages can be forwarded" + | len == 0 = "nothing to forward" + | otherwise = plain $ show len <> " message(s) out of " <> show count <> " can be forwarded" + len = length itemIds + +connReqContact_ :: StyledString -> CreatedLinkContact -> [StyledString] +connReqContact_ intro (CCLink cReq shortLink) = [ intro, "", - (plain . strEncode) (simplexChatContact cReq), + plain $ maybe cReqStr strEncode shortLink, "", "Anybody can send you contact requests with: " <> highlight' "/c <contact_link_above>", "to show it again: " <> highlight' "/sa", "to share with your contacts: " <> highlight' "/profile_address on", "to delete it: " <> highlight' "/da" <> " (accepted contacts will remain connected)" ] + <> ["The contact link for old clients: " <> plain cReqStr | isJust shortLink] + where + cReqStr = strEncode $ simplexChatContact cReq simplexChatContact :: ConnReqContact -> ConnReqContact simplexChatContact (CRContactUri crData) = CRContactUri crData {crScheme = simplexChat} +simplexChatContact' :: ConnLinkContact -> ConnLinkContact +simplexChatContact' = \case + CLFull (CRContactUri crData) -> CLFull $ CRContactUri crData {crScheme = simplexChat} + l@(CLShort _) -> l + autoAcceptStatus_ :: Maybe AutoAccept -> [StyledString] autoAcceptStatus_ = \case - Just AutoAccept {acceptIncognito, autoReply} -> - ("auto_accept on" <> if acceptIncognito then ", incognito" else "") + Just AutoAccept {businessAddress, acceptIncognito, autoReply} -> + ("auto_accept on" <> aaInfo) : maybe [] ((["auto reply:"] <>) . ttyMsgContent) autoReply + where + aaInfo + | businessAddress = ", business" + | acceptIncognito = ", incognito" + | otherwise = "" _ -> ["auto_accept off"] -groupLink_ :: StyledString -> GroupInfo -> ConnReqContact -> GroupMemberRole -> [StyledString] -groupLink_ intro g cReq mRole = +groupLink_ :: StyledString -> GroupInfo -> CreatedLinkContact -> GroupMemberRole -> [StyledString] +groupLink_ intro g (CCLink cReq shortLink) mRole = [ intro, "", - (plain . strEncode) (simplexChatContact cReq), + plain $ maybe cReqStr strEncode shortLink, "", "Anybody can connect to you and join group as " <> showRole mRole <> " with: " <> highlight' "/c <group_link_above>", "to show it again: " <> highlight ("/show link #" <> viewGroupName g), "to delete it: " <> highlight ("/delete link #" <> viewGroupName g) <> " (joined members will remain connected to you)" ] + <> ["The group link for old clients: " <> plain cReqStr | isJust shortLink] + where + cReqStr = strEncode $ simplexChatContact cReq viewGroupLinkDeleted :: GroupInfo -> [StyledString] viewGroupLinkDeleted g = @@ -860,6 +1102,14 @@ viewSentInvitation incognitoProfile testView = message = ["connection request sent incognito!"] Nothing -> ["connection request sent!"] +viewAcceptingContactRequest :: Contact -> [StyledString] +viewAcceptingContactRequest ct + | contactReady ct = [ttyFullContact ct <> ": accepting contact request, you can send messages to contact"] + | otherwise = [ttyFullContact ct <> ": accepting contact request..."] + +viewAcceptingBusinessRequest :: GroupInfo -> [StyledString] +viewAcceptingBusinessRequest g = [ttyFullGroup g <> ": accepting business address request..."] + viewReceivedContactRequest :: ContactName -> Profile -> [StyledString] viewReceivedContactRequest c Profile {fullName} = [ ttyFullName c fullName <> " wants to connect to you!", @@ -901,14 +1151,22 @@ viewNetworkStatuses = map viewStatuses . L.groupBy ((==) `on` netStatus) . sortO viewStatuses ss@(s :| _) = plain $ show (L.length ss) <> " connections " <> netStatusStr (netStatus s) viewUserJoinedGroup :: GroupInfo -> [StyledString] -viewUserJoinedGroup g = +viewUserJoinedGroup g@GroupInfo {membership} = case incognitoMembershipProfile g of - Just mp -> [ttyGroup' g <> ": you joined the group incognito as " <> incognitoProfile' (fromLocalProfile mp)] - Nothing -> [ttyGroup' g <> ": you joined the group"] + Just mp -> [ttyGroup' g <> ": you joined the group incognito as " <> incognitoProfile' (fromLocalProfile mp) <> pendingApproval_] + Nothing -> [ttyGroup' g <> ": you joined the group" <> pendingApproval_] + where + pendingApproval_ = case memberStatus membership of + GSMemPendingApproval -> ", pending approval" + _ -> "" viewJoinedGroupMember :: GroupInfo -> GroupMember -> [StyledString] -viewJoinedGroupMember g m = - [ttyGroup' g <> ": " <> ttyMember m <> " joined the group "] +viewJoinedGroupMember g@GroupInfo {groupId} m@GroupMember {groupMemberId, memberStatus} = case memberStatus of + GSMemPendingApproval -> + [ (ttyGroup' g <> ": " <> ttyMember m <> " connected and pending approval, ") + <> ("use " <> highlight ("/_accept member #" <> show groupId <> " " <> show groupMemberId <> " <role>") <> " to accept member") + ] + _ -> [ttyGroup' g <> ": " <> ttyMember m <> " joined the group "] viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> [StyledString] viewReceivedGroupInvitation g c role = @@ -936,13 +1194,19 @@ viewMemberRoleChanged g@GroupInfo {membership} by m r r' memId = groupMemberId' m view s = [ttyGroup' g <> ": " <> ttyMember by <> " changed " <> s <> " from " <> showRole r <> " to " <> showRole r'] -viewMemberRoleUserChanged :: GroupInfo -> GroupMember -> GroupMemberRole -> GroupMemberRole -> [StyledString] -viewMemberRoleUserChanged g@GroupInfo {membership} m r r' - | r == r' = [ttyGroup' g <> ": member role did not change"] - | groupMemberId' membership == groupMemberId' m = view "your role" - | otherwise = view $ "the role of " <> ttyMember m - where - view s = [ttyGroup' g <> ": you changed " <> s <> " from " <> showRole r <> " to " <> showRole r'] +viewMemberRoleUserChanged :: GroupInfo -> [GroupMember] -> GroupMemberRole -> [StyledString] +viewMemberRoleUserChanged g members r = case members of + [m] -> [ttyGroup' g <> ": you changed the role of " <> ttyMember m <> " to " <> showRole r] + mems' -> [ttyGroup' g <> ": you changed the role of " <> sShow (length mems') <> " members to " <> showRole r] + +viewMemberBlockedForAll :: GroupInfo -> GroupMember -> GroupMember -> Bool -> [StyledString] +viewMemberBlockedForAll g by m blocked = + [ttyGroup' g <> ": " <> ttyMember by <> " " <> (if blocked then "blocked" else "unblocked") <> " " <> ttyMember m] + +viewMembersBlockedForAllUser :: GroupInfo -> [GroupMember] -> Bool -> [StyledString] +viewMembersBlockedForAllUser g members blocked = case members of + [m] -> [ttyGroup' g <> ": you " <> (if blocked then "blocked" else "unblocked") <> " " <> ttyMember m] + mems' -> [ttyGroup' g <> ": you " <> (if blocked then "blocked" else "unblocked") <> " " <> sShow (length mems') <> " members"] showRole :: GroupMemberRole -> StyledString showRole = plain . strEncode @@ -950,26 +1214,29 @@ showRole = plain . strEncode viewGroupMembers :: Group -> [StyledString] viewGroupMembers (Group GroupInfo {membership} members) = map groupMember . filter (not . removedOrLeft) $ membership : members where - removedOrLeft m = let s = memberStatus m in s == GSMemRemoved || s == GSMemLeft + removedOrLeft m = let s = memberStatus m in s == GSMemRejected || s == GSMemRemoved || s == GSMemLeft groupMember m = memIncognito m <> ttyFullMember m <> ": " <> plain (intercalate ", " $ [role m] <> category m <> status m <> muted m) role :: GroupMember -> String - role m = B.unpack . strEncode $ m.memberRole + role GroupMember {memberRole} = B.unpack $ strEncode memberRole category m = case memberCategory m of GCUserMember -> ["you"] GCInviteeMember -> ["invited"] GCHostMember -> ["host"] _ -> [] status m = case memberStatus m of + GSMemRejected -> ["rejected"] GSMemRemoved -> ["removed"] GSMemLeft -> ["left"] + GSMemUnknown -> ["status unknown"] GSMemInvited -> ["not yet joined"] GSMemConnected -> ["connected"] GSMemComplete -> ["connected"] GSMemCreator -> ["created group"] _ -> [] muted m - | showMessages (memberSettings m) = [] - | otherwise = ["blocked"] + | blockedByAdmin m = ["blocked by admin"] + | not (showMessages $ memberSettings m) = ["blocked"] + | otherwise = [] viewContactConnected :: Contact -> Maybe Profile -> Bool -> [StyledString] viewContactConnected ct userIncognitoProfile testView = @@ -991,13 +1258,14 @@ viewGroupsList [] = ["you have no groups!", "to create: " <> highlight' "/g <nam viewGroupsList gs = map groupSS $ sortOn (ldn_ . fst) gs where ldn_ :: GroupInfo -> Text - ldn_ g = T.toLower g.localDisplayName + ldn_ GroupInfo {localDisplayName} = T.toLower localDisplayName groupSS (g@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}}, GroupSummary {currentMembers}) = case memberStatus membership of GSMemInvited -> groupInvitation' g - s -> membershipIncognito g <> ttyFullGroup g <> viewMemberStatus s + s -> membershipIncognito g <> ttyFullGroup g <> viewMemberStatus s <> alias g where viewMemberStatus = \case + GSMemRejected -> delete "you are rejected" GSMemRemoved -> delete "you are removed" GSMemLeft -> delete "you left" GSMemGroupDeleted -> delete "group deleted" @@ -1010,6 +1278,16 @@ viewGroupsList gs = map groupSS $ sortOn (ldn_ . fst) gs unmute = "you can " <> highlight ("/unmute #" <> viewGroupName g) delete reason = " (" <> reason <> ", delete local copy: " <> highlight ("/d #" <> viewGroupName g) <> ")" memberCount = sShow currentMembers <> " member" <> if currentMembers == 1 then "" else "s" + alias GroupInfo {localAlias} + | localAlias == "" = "" + | otherwise = " (alias: " <> plain localAlias <> ")" + +viewSentGroupInvitation :: GroupInfo -> Contact -> [StyledString] +viewSentGroupInvitation g c = case contactConn c of + Just Connection {viaGroupLink} + | viaGroupLink -> [ttyContact' c <> " invited to group " <> ttyGroup' g <> " via your group link"] + | otherwise -> ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c] + Nothing -> [] groupInvitation' :: GroupInfo -> StyledString groupInvitation' g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}} = @@ -1025,6 +1303,15 @@ groupInvitation' g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfil Just mp -> " to join as " <> incognitoProfile' (fromLocalProfile mp) <> ", " Nothing -> " to join, " +groupInvitationSub :: ShortGroupInfo -> StyledString +groupInvitationSub ShortGroupInfo {groupName = ldn} = + highlight ("#" <> viewName ldn) + <> " - you are invited (" + <> highlight ("/j " <> viewName ldn) + <> " to join, " + <> highlight ("/d #" <> viewName ldn) + <> " to delete invitation)" + viewContactsMerged :: Contact -> Contact -> Contact -> [StyledString] viewContactsMerged c1 c2 ct' = [ "contact " <> ttyContact' c2 <> " is merged into " <> ttyContact' c1, @@ -1051,27 +1338,43 @@ viewUserPrivacy User {userId} User {userId = userId', localDisplayName = n', sho "profile is " <> if isJust viewPwdHash then "hidden" else "visible" ] -viewUserServers :: AUserProtoServers -> Bool -> [StyledString] -viewUserServers (AUPS UserProtoServers {serverProtocol = p, protoServers, presetServers}) testView = - customServers - <> if testView - then [] - else - [ "", - "use " <> highlight (srvCmd <> " test <srv>") <> " to test " <> pName <> " server connection", - "use " <> highlight (srvCmd <> " <srv1[,srv2,...]>") <> " to configure " <> pName <> " servers", - "use " <> highlight (srvCmd <> " default") <> " to remove configured " <> pName <> " servers and use presets" - ] - <> case p of - SPSMP -> ["(chat option " <> highlight' "-s" <> " (" <> highlight' "--server" <> ") has precedence over saved SMP servers for chat session)"] - SPXFTP -> ["(chat option " <> highlight' "-xftp-servers" <> " has precedence over saved XFTP servers for chat session)"] +viewUserServers :: UserOperatorServers -> [StyledString] +viewUserServers (UserOperatorServers _ [] []) = [] +viewUserServers UserOperatorServers {operator, smpServers, xftpServers} = + [plain $ maybe "Your servers" shortViewOperator operator] + <> viewServers SPSMP smpServers + <> viewServers SPXFTP xftpServers where - srvCmd = "/" <> strEncode p - pName = protocolName p - customServers = - if null protoServers - then ("no " <> pName <> " servers saved, using presets: ") : viewServers id presetServers - else viewServers (\ServerCfg {server} -> server) protoServers + viewServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [UserServer p] -> [StyledString] + viewServers _ [] = [] + viewServers p srvs + | maybe True (\ServerOperator {enabled} -> enabled) operator = + [" " <> protocolName p <> " servers" <> maybe "" ((" " <>) . viewRoles) operator] + <> map (plain . (" " <>) . viewServer) srvs + | otherwise = [] + where + viewServer UserServer {server, preset, tested, enabled} = safeDecodeUtf8 (strEncode server) <> serverInfo + where + serverInfo = if null serverInfo_ then "" else parens $ T.intercalate ", " serverInfo_ + serverInfo_ = ["preset" | preset] <> testedInfo <> ["disabled" | not enabled] + testedInfo = maybe [] (\t -> ["test: " <> if t then "passed" else "failed"]) tested + viewRoles op@ServerOperator {enabled} + | not enabled = "disabled" + | storage rs && proxy rs = "enabled" + | storage rs = "enabled storage" + | proxy rs = "enabled proxy" + | otherwise = "disabled (servers known)" + where + rs = operatorRoles p op + +serversUserHelp :: [StyledString] +serversUserHelp = + [ "", + "use " <> highlight' "/smp test <srv>" <> " to test SMP server connection", + "use " <> highlight' "/smp <srv1[,srv2,...]>" <> " to configure SMP servers", + "or the same commands starting from /xftp for XFTP servers", + "chat options " <> highlight' "-s" <> " (" <> highlight' "--server" <> ") and " <> highlight' "--xftp-servers" <> " have precedence over preset servers for new user profiles" + ] protocolName :: ProtocolTypeI p => SProtocolType p -> StyledString protocolName = plain . map toUpper . T.unpack . decodeLatin1 . strEncode @@ -1080,11 +1383,11 @@ viewServerTestResult :: AProtoServerWithAuth -> Maybe ProtocolTestFailure -> [St viewServerTestResult (AProtoServerWithAuth p _) = \case Just ProtocolTestFailure {testStep, testError} -> result - <> [pName <> " server requires authorization to create queues, check password" | testStep == TSCreateQueue && testError == SMP SMP.AUTH] - <> [pName <> " server requires authorization to upload files, check password" | testStep == TSCreateFile && testError == XFTP XFTP.AUTH] + <> [pName <> " server requires authorization to create queues, check password" | testStep == TSCreateQueue && (case testError of SMP _ SMP.AUTH -> True; _ -> False)] + <> [pName <> " server requires authorization to upload files, check password" | testStep == TSCreateFile && (case testError of XFTP _ XFTP.AUTH -> True; _ -> False)] <> ["Possibly, certificate fingerprint in " <> pName <> " server address is incorrect" | testStep == TSConnect && brokerErr] where - result = [pName <> " server test failed at " <> plain (drop 2 $ show testStep) <> ", error: " <> plain (strEncode testError)] + result = [pName <> " server test failed at " <> plain (drop 2 $ show testStep) <> ", error: " <> sShow testError] brokerErr = case testError of BROKER _ NETWORK -> True _ -> False @@ -1092,49 +1395,130 @@ viewServerTestResult (AProtoServerWithAuth p _) = \case where pName = protocolName p +viewServerOperators :: [ServerOperator] -> Maybe UsageConditionsAction -> [StyledString] +viewServerOperators ops ca = map (plain . viewOperator) ops <> maybe [] viewConditionsAction ca + +viewOperator :: ServerOperator' s -> Text +viewOperator op@ServerOperator {tradeName, legalName, serverDomains, conditionsAcceptance} = + viewOpIdTag op + <> tradeName + <> maybe "" parens legalName + <> (", domains: " <> T.intercalate ", " serverDomains) + <> (", servers: " <> viewOpEnabled op) + <> (", conditions: " <> viewOpConditions conditionsAcceptance) + +shortViewOperator :: ServerOperator -> Text +shortViewOperator ServerOperator {operatorId = DBEntityId opId, tradeName, enabled} = + tshow opId <> ". " <> tradeName <> parens (if enabled then "enabled" else "disabled") + +viewOpIdTag :: ServerOperator' s -> Text +viewOpIdTag ServerOperator {operatorId, operatorTag} = case operatorId of + DBEntityId i -> tshow i <> tag + DBNewEntity -> tag + where + tag = maybe "" (parens . textEncode) operatorTag <> ". " + +viewOpConditions :: ConditionsAcceptance -> Text +viewOpConditions = \case + CAAccepted ts _ -> viewCond "accepted" ts + CARequired ts -> viewCond "required" ts + where + viewCond w ts = w <> maybe "" (parens . tshow) ts + +viewOpEnabled :: ServerOperator' s -> Text +viewOpEnabled ServerOperator {enabled, smpRoles, xftpRoles} + | not enabled = "disabled" + | no smpRoles && no xftpRoles = "disabled (servers known)" + | both smpRoles && both xftpRoles = "enabled" + | otherwise = "SMP " <> viewRoles smpRoles <> ", XFTP " <> viewRoles xftpRoles + where + no rs = not $ storage rs || proxy rs + both rs = storage rs && proxy rs + viewRoles rs + | both rs = "enabled" + | storage rs = "enabled storage" + | proxy rs = "enabled proxy" + | otherwise = "disabled (servers known)" + +viewConditionsAction :: UsageConditionsAction -> [StyledString] +viewConditionsAction = \case + UCAReview {operators, deadline, showNotice} | showNotice -> case deadline of + Just ts -> [plain $ "The new conditions will be accepted for " <> ops <> " at " <> tshow ts] + Nothing -> [plain $ "The new conditions have to be accepted for " <> ops] + where + ops = T.intercalate ", " $ map legalName_ operators + legalName_ ServerOperator {tradeName, legalName} = fromMaybe tradeName legalName + _ -> [] + +viewUsageConditions :: UsageConditions -> Maybe UsageConditions -> [StyledString] +viewUsageConditions current accepted_ = + [plain $ "Current conditions: " <> viewConds current <> maybe "" (\ac -> ", accepted conditions: " <> viewConds ac) accepted_] + where + viewConds UsageConditions {conditionsId, conditionsCommit, notifiedAt} = + tshow conditionsId <> maybe "" (const " (notified)") notifiedAt <> ". " <> conditionsCommit + viewChatItemTTL :: Maybe Int64 -> [StyledString] viewChatItemTTL = \case - Nothing -> ["old messages are not being deleted"] + Nothing -> ["old messages are set to delete according to default user config"] Just ttl + | ttl == 0 -> ["old messages are not being deleted"] | ttl == 86400 -> deletedAfter "one day" | ttl == 7 * 86400 -> deletedAfter "one week" | ttl == 30 * 86400 -> deletedAfter "one month" + | ttl == 365 * 86400 -> deletedAfter "one year" | otherwise -> deletedAfter $ sShow ttl <> " second(s)" where deletedAfter ttlStr = ["old messages are set to be deleted after: " <> ttlStr] viewNetworkConfig :: NetworkConfig -> [StyledString] -viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} = - [ plain $ maybe "direct network connection" (("using SOCKS5 proxy " <>) . show) socksProxy, +viewNetworkConfig NetworkConfig {socksProxy, socksMode, tcpTimeout, smpProxyMode, smpProxyFallback} = + [ plain $ maybe "direct network connection" ((\sp -> "using SOCKS5 proxy " <> sp <> if socksMode == SMOnion then " for onion servers ONLY." else " for ALL servers.") . show) socksProxy, "TCP timeout: " <> sShow tcpTimeout, - "use " <> highlight' "/network socks=<on/off/[ipv4]:port>[ timeout=<seconds>]" <> " to change settings" + plain $ smpProxyModeStr smpProxyMode smpProxyFallback, + "use " <> highlight' "/network socks=<on/off/[ipv4]:port>[ socks-mode=always/onion][ smp-proxy=always/unknown/unprotected/never][ smp-proxy-fallback=no/protected/yes][ timeout=<seconds>]" <> " to change settings" ] +smpProxyModeStr :: SMPProxyMode -> SMPProxyFallback -> String +smpProxyModeStr SPMNever _ = "private message routing disabled." +smpProxyModeStr mode fallback = T.unpack $ safeDecodeUtf8 $ "private message routing mode: " <> strEncode mode <> ", fallback: " <> strEncode fallback + viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledString] -viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn} stats incognitoProfile = +viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn, uiThemes, customData} stats incognitoProfile = ["contact ID: " <> sShow contactId] <> maybe [] viewConnectionStats stats - <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact l)]) contactLink + <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact' l)]) contactLink <> maybe ["you've shared main profile with this contact"] (\p -> ["you've shared incognito profile with this contact: " <> incognitoProfile' p]) incognitoProfile <> ["alias: " <> plain localAlias | localAlias /= ""] <> [viewConnectionVerified (contactSecurityCode ct)] + <> ["quantum resistant end-to-end encryption" | contactPQEnabled ct == CR.PQEncOn] <> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn + <> viewUITheme uiThemes + <> viewCustomData customData viewGroupInfo :: GroupInfo -> GroupSummary -> [StyledString] -viewGroupInfo GroupInfo {groupId} s = +viewGroupInfo GroupInfo {groupId, uiThemes, customData} s = [ "group ID: " <> sShow groupId, "current members: " <> sShow (currentMembers s) ] + <> viewUITheme uiThemes + <> viewCustomData customData + +viewUITheme :: Maybe UIThemeEntityOverrides -> [StyledString] +viewUITheme = maybe [] (\uiThemes -> ["UI themes: " <> viewJSON uiThemes]) + +viewCustomData :: Maybe CustomData -> [StyledString] +viewCustomData = maybe [] (\(CustomData v) -> ["custom data: " <> viewJSON (J.Object v)]) viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString] -viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias}, activeConn} stats = +viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias, contactLink}, activeConn} stats = [ "group ID: " <> sShow groupId, "member ID: " <> sShow groupMemberId ] <> maybe ["member not connected"] viewConnectionStats stats + <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact' l)]) contactLink <> ["alias: " <> plain localAlias | localAlias /= ""] <> [viewConnectionVerified (memberSecurityCode m) | isJust stats] <> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn @@ -1143,17 +1527,14 @@ viewConnectionVerified :: Maybe SecurityCode -> StyledString viewConnectionVerified (Just _) = "connection verified" -- TODO show verification time? viewConnectionVerified _ = "connection not verified, use " <> highlight' "/code" <> " command to see security code" -viewPeerChatVRange :: JVersionRange -> StyledString -viewPeerChatVRange (JVersionRange (VersionRange minVer maxVer)) = "peer chat protocol version range: (" <> sShow minVer <> ", " <> sShow maxVer <> ")" +viewPeerChatVRange :: VersionRangeChat -> StyledString +viewPeerChatVRange (VersionRange minVer maxVer) = "peer chat protocol version range: (" <> sShow minVer <> ", " <> sShow maxVer <> ")" viewConnectionStats :: ConnectionStats -> [StyledString] viewConnectionStats ConnectionStats {rcvQueuesInfo, sndQueuesInfo} = ["receiving messages via: " <> viewRcvQueuesInfo rcvQueuesInfo | not $ null rcvQueuesInfo] <> ["sending messages via: " <> viewSndQueuesInfo sndQueuesInfo | not $ null sndQueuesInfo] -viewServers :: ProtocolTypeI p => (a -> ProtoServerWithAuth p) -> NonEmpty a -> [StyledString] -viewServers f = map (plain . B.unpack . strEncode . f) . L.toList - viewRcvQueuesInfo :: [RcvQueueInfo] -> StyledString viewRcvQueuesInfo = plain . intercalate ", " . map showQueueInfo where @@ -1299,10 +1680,10 @@ countactUserPrefText cup = case cup of viewGroupUpdated :: GroupInfo -> GroupInfo -> Maybe GroupMember -> [StyledString] viewGroupUpdated - GroupInfo {localDisplayName = n, groupProfile = GroupProfile {fullName, description, image, groupPreferences = gps}} - g'@GroupInfo {localDisplayName = n', groupProfile = GroupProfile {fullName = fullName', description = description', image = image', groupPreferences = gps'}} + GroupInfo {localDisplayName = n, groupProfile = GroupProfile {fullName, description, image, groupPreferences = gps, memberAdmission = ma}} + g'@GroupInfo {localDisplayName = n', groupProfile = GroupProfile {fullName = fullName', description = description', image = image', groupPreferences = gps', memberAdmission = ma'}} m = do - let update = groupProfileUpdated <> groupPrefsUpdated + let update = groupProfileUpdated <> groupPrefsUpdated <> memberAdmissionUpdated if null update then [] else memberUpdated <> update @@ -1323,6 +1704,9 @@ viewGroupUpdated | otherwise = Just . plain $ groupPreferenceText (pref gps') where pref = getGroupPreference f . mergeGroupPreferences + memberAdmissionUpdated + | ma == ma' = [] + | otherwise = ["changed member admission rules"] viewGroupProfile :: GroupInfo -> [StyledString] viewGroupProfile g@GroupInfo {groupProfile = GroupProfile {description, image, groupPreferences = gps}} = @@ -1347,6 +1731,11 @@ viewContactAliasUpdated ct@Contact {profile = LocalProfile {localAlias}} | localAlias == "" = ["contact " <> ttyContact' ct <> " alias removed"] | otherwise = ["contact " <> ttyContact' ct <> " alias updated: " <> plain localAlias] +viewGroupAliasUpdated :: GroupInfo -> [StyledString] +viewGroupAliasUpdated g@GroupInfo {localAlias} + | localAlias == "" = ["group " <> ttyGroup' g <> " alias removed"] + | otherwise = ["group " <> ttyGroup' g <> " alias updated: " <> plain localAlias] + viewConnectionAliasUpdated :: PendingContactConnection -> [StyledString] viewConnectionAliasUpdated PendingContactConnection {pccConnId, localAlias} | localAlias == "" = ["connection " <> sShow pccConnId <> " alias removed"] @@ -1357,8 +1746,24 @@ viewConnectionIncognitoUpdated PendingContactConnection {pccConnId, customUserPr | isJust customUserProfileId = ["connection " <> sShow pccConnId <> " changed to incognito"] | otherwise = ["connection " <> sShow pccConnId <> " changed to non incognito"] -viewConnectionPlan :: ConnectionPlan -> [StyledString] -viewConnectionPlan = \case +viewConnectionUserChanged :: User -> PendingContactConnection -> User -> PendingContactConnection -> [StyledString] +viewConnectionUserChanged User {localDisplayName = n} PendingContactConnection {pccConnId} User {localDisplayName = n'} PendingContactConnection {connLinkInv = connLinkInv'} = + case connLinkInv' of + Just ccLink' -> [userChangedStr <> ", new link:"] <> newLink ccLink' + _ -> [userChangedStr] + where + userChangedStr = "connection " <> sShow pccConnId <> " changed from user " <> plain n <> " to user " <> plain n' + newLink (CCLink cReq shortLink) = + [ "", + plain $ maybe cReqStr strEncode shortLink, + "" + ] + <> ["The invitation link for old clients: " <> plain cReqStr | isJust shortLink] + where + cReqStr = strEncode $ simplexChatInvitation cReq + +viewConnectionPlan :: ChatConfig -> ConnectionPlan -> [StyledString] +viewConnectionPlan ChatConfig {logLevel, testView} = \case CPInvitationLink ilp -> case ilp of ILPOk -> [invLink "ok to connect"] ILPOwnLink -> [invLink "own link"] @@ -1387,13 +1792,17 @@ viewConnectionPlan = \case GLPOwnLink g -> [grpLink "own link for group " <> ttyGroup' g] GLPConnectingConfirmReconnect -> [grpLink "connecting, allowed to reconnect"] GLPConnectingProhibit Nothing -> [grpLink "connecting"] - GLPConnectingProhibit (Just g) -> [grpLink ("connecting to group " <> ttyGroup' g)] + GLPConnectingProhibit (Just g) -> [grpOrBiz g <> " link: connecting to " <> grpOrBiz g <> " " <> ttyGroup' g] GLPKnown g -> - [ grpLink ("known group " <> ttyGroup' g), + [ grpOrBiz g <> " link: known " <> grpOrBiz g <> " " <> ttyGroup' g, "use " <> ttyToGroup g <> highlight' "<message>" <> " to send messages" ] where grpLink = ("group link: " <>) + grpOrBiz GroupInfo {businessChat} = case businessChat of + Just _ -> "business" + Nothing -> "group" + CPError e -> viewChatError False logLevel testView e viewContactUpdated :: Contact -> Contact -> [StyledString] viewContactUpdated @@ -1419,17 +1828,17 @@ viewReceivedUpdatedMessage :: StyledString -> [StyledString] -> MsgContent -> Cu viewReceivedUpdatedMessage = viewReceivedMessage_ True viewReceivedMessage_ :: Bool -> StyledString -> [StyledString] -> MsgContent -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString] -viewReceivedMessage_ updated from quote mc ts tz meta = receivedWithTime_ ts tz from quote meta (ttyMsgContent mc) updated +viewReceivedMessage_ updated from context mc ts tz meta = receivedWithTime_ ts tz from context meta (ttyMsgContent mc) updated viewReceivedReaction :: StyledString -> [StyledString] -> StyledString -> CurrentTime -> TimeZone -> UTCTime -> [StyledString] viewReceivedReaction from styledMsg reactionText ts tz reactionTs = prependFirst (ttyMsgTime ts tz reactionTs <> " " <> from) (styledMsg <> [" " <> reactionText]) receivedWithTime_ :: CurrentTime -> TimeZone -> StyledString -> [StyledString] -> CIMeta c d -> [StyledString] -> Bool -> [StyledString] -receivedWithTime_ ts tz from quote CIMeta {itemId, itemTs, itemEdited, itemDeleted, itemLive} styledMsg updated = do - prependFirst (ttyMsgTime ts tz itemTs <> " " <> from) (quote <> prependFirst (indent <> live) styledMsg) +receivedWithTime_ ts tz from context CIMeta {itemId, itemTs, itemEdited, itemDeleted, itemLive} styledMsg updated = do + prependFirst (ttyMsgTime ts tz itemTs <> " " <> from) (context <> prependFirst (indent <> live) styledMsg) where - indent = if null quote then "" else " " + indent = if null context then "" else " " live | itemEdited || isJust itemDeleted = "" | otherwise = case itemLive of @@ -1457,9 +1866,9 @@ recent now tz time = do || (localNow < currentDay12 && localTime >= previousDay18 && localTimeDay < localNowDay) viewSentMessage :: StyledString -> [StyledString] -> MsgContent -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString] -viewSentMessage to quote mc ts tz meta@CIMeta {itemEdited, itemDeleted, itemLive} = sentWithTime_ ts tz (prependFirst to $ quote <> prependFirst (indent <> live) (ttyMsgContent mc)) meta +viewSentMessage to context mc ts tz meta@CIMeta {itemEdited, itemDeleted, itemLive} = sentWithTime_ ts tz (prependFirst to $ context <> prependFirst (indent <> live) (ttyMsgContent mc)) meta where - indent = if null quote then "" else " " + indent = if null context then "" else " " live | itemEdited || isJust itemDeleted = "" | otherwise = case itemLive of @@ -1513,11 +1922,26 @@ sendingFile_ status ft@SndFileTransfer {recipientDisplayName = c} = [status <> " sending " <> sndFile ft <> " to " <> ttyContact c] uploadingFile :: StyledString -> AChatItem -> [StyledString] -uploadingFile status (AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectSnd}) = - [status <> " uploading " <> fileTransferStr fileId fileName <> " for " <> ttyContact c] -uploadingFile status (AChatItem _ _ (GroupChat g) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupSnd}) = - [status <> " uploading " <> fileTransferStr fileId fileName <> " for " <> ttyGroup' g] -uploadingFile status _ = [status <> " uploading file"] -- shouldn't happen +uploadingFile status = \case + AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectSnd} -> + [status <> " uploading " <> fileTransferStr fileId fileName <> " for " <> ttyContact c] + AChatItem _ _ (GroupChat g) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupSnd} -> + [status <> " uploading " <> fileTransferStr fileId fileName <> " for " <> ttyGroup' g] + _ -> [status <> " uploading file"] + +uploadingFileStandalone :: StyledString -> FileTransferMeta -> [StyledString] +uploadingFileStandalone status FileTransferMeta {fileId, fileName} = [status <> " standalone uploading " <> fileTransferStr fileId fileName] + +standaloneUploadRedirect :: FileTransferMeta -> FileTransferMeta -> [StyledString] +standaloneUploadRedirect FileTransferMeta {fileId, fileName} FileTransferMeta {fileId = redirectId} = + [fileTransferStr fileId fileName <> " uploaded, preparing redirect file " <> sShow redirectId] + +standaloneUploadComplete :: FileTransferMeta -> [Text] -> [StyledString] +standaloneUploadComplete FileTransferMeta {fileId, fileName} = \case + [] -> [fileTransferStr fileId fileName <> " upload complete."] + uris -> + fileTransferStr fileId fileName <> " upload complete. download with:" + : map plain uris sndFile :: SndFileTransfer -> StyledString sndFile SndFileTransfer {fileId, fileName} = fileTransferStr fileId fileName @@ -1553,7 +1977,7 @@ receivingFile_' :: (Maybe RemoteHostId, Maybe User) -> Bool -> String -> AChatIt receivingFile_' hu testView status (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileName, fileSource = Just f@(CryptoFile _ cfArgs_)}, chatDir}) = [plain status <> " receiving " <> fileTransferStr fileId fileName <> fileFrom chat chatDir] <> cfArgsStr cfArgs_ <> getRemoteFileStr where - cfArgsStr (Just cfArgs) = [plain (cryptoFileArgsStr testView cfArgs) | status == "completed"] + cfArgsStr (Just cfArgs) = [cryptoFileArgsStr testView cfArgs | status == "completed"] cfArgsStr _ = [] getRemoteFileStr = case hu of (Just rhId, Just User {userId}) @@ -1563,12 +1987,21 @@ receivingFile_' hu testView status (AChatItem _ _ chat ChatItem {file = Just CIF highlight ("/get remote file " <> show rhId <> " " <> LB.unpack (J.encode RemoteFile {userId, fileId, sent = False, fileSource = f})) ] _ -> [] -receivingFile_' _ _ status _ = [plain status <> " receiving file"] -- shouldn't happen +receivingFile_' _ _ status _ = [plain status <> " receiving file"] -cryptoFileArgsStr :: Bool -> CryptoFileArgs -> ByteString +receivingFileStandalone :: String -> RcvFileTransfer -> [StyledString] +receivingFileStandalone status RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} = + [plain status <> " standalone receiving " <> fileTransferStr fileId fileName] + +viewLocalFile :: StyledString -> CIFile d -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString] +viewLocalFile to CIFile {fileId, fileSource} ts tz = case fileSource of + Just (CryptoFile fPath _) -> sentWithTime_ ts tz [to <> fileTransferStr fileId fPath] + _ -> const [] + +cryptoFileArgsStr :: Bool -> CryptoFileArgs -> StyledString cryptoFileArgsStr testView cfArgs@(CFArgs key nonce) - | testView = LB.toStrict $ J.encode cfArgs - | otherwise = "encryption key: " <> strEncode key <> ", nonce: " <> strEncode nonce + | testView = viewJSON cfArgs + | otherwise = plain $ "encryption key: " <> strEncode key <> ", nonce: " <> strEncode nonce fileFrom :: ChatInfo c -> CIDirection c d -> StyledString fileFrom (DirectChat ct) CIDirectRcv = " from " <> ttyContact' ct @@ -1577,7 +2010,7 @@ fileFrom _ _ = "" receivingFile_ :: StyledString -> RcvFileTransfer -> [StyledString] receivingFile_ status ft@RcvFileTransfer {senderDisplayName = c} = - [status <> " receiving " <> rcvFile ft <> " from " <> ttyContact c] + [status <> " receiving " <> rcvFile ft <> if c == "" then "" else " from " <> ttyContact c] rcvFile :: RcvFileTransfer -> StyledString rcvFile RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} = fileTransferStr fileId fileName @@ -1625,13 +2058,16 @@ viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId CIFSSndTransfer progress total -> ["sending " <> fstr <> " in progress " <> fileProgressXFTP progress total fileSize] CIFSSndCancelled -> ["sending " <> fstr <> " cancelled"] CIFSSndComplete -> ["sending " <> fstr <> " complete"] - CIFSSndError -> ["sending " <> fstr <> " error"] + CIFSSndError sndFileErr -> ["sending " <> fstr <> " error: " <> plain (show sndFileErr)] + CIFSSndWarning sndFileErr -> ["sending " <> fstr <> " warning: " <> plain (show sndFileErr)] CIFSRcvInvitation -> ["receiving " <> fstr <> " not accepted yet, use " <> highlight ("/fr " <> show fileId) <> " to receive file"] CIFSRcvAccepted -> ["receiving " <> fstr <> " just started"] CIFSRcvTransfer progress total -> ["receiving " <> fstr <> " progress " <> fileProgressXFTP progress total fileSize] + CIFSRcvAborted -> ["receiving " <> fstr <> " aborted, use " <> highlight ("/fr " <> show fileId) <> " to receive file"] CIFSRcvComplete -> ["receiving " <> fstr <> " complete" <> maybe "" (\(CryptoFile fp _) -> ", path: " <> plain fp) fileSource] CIFSRcvCancelled -> ["receiving " <> fstr <> " cancelled"] - CIFSRcvError -> ["receiving " <> fstr <> " error"] + CIFSRcvError rcvFileErr -> ["receiving " <> fstr <> " error: " <> plain (show rcvFileErr)] + CIFSRcvWarning rcvFileErr -> ["receiving " <> fstr <> " warning: " <> plain (show rcvFileErr)] CIFSInvalid text -> [fstr <> " invalid status: " <> plain text] where fstr = fileTransferStr fileId fileName @@ -1685,7 +2121,7 @@ viewCallAnswer ct WebRTCSession {rtcSession = answer, rtcIceCandidates = iceCand [ ttyContact' ct <> " continued the WebRTC call", "To connect, please paste the data below in your browser window you opened earlier and click Connect button", "", - plain . LB.toStrict . J.encode $ WCCallAnswer {answer, iceCandidates} + viewJSON WCCallAnswer {answer, iceCandidates} ] callMediaStr :: CallType -> StyledString @@ -1710,7 +2146,9 @@ viewVersionInfo logLevel CoreVersionInfo {version, simplexmqVersion, simplexmqCo then [versionString version, updateStr, "simplexmq: " <> simplexmqVersion <> parens simplexmqCommit] else [versionString version, updateStr] where - parens s = " (" <> s <> ")" + +parens :: (IsString a, Semigroup a) => a -> a +parens s = " (" <> s <> ")" viewRemoteHosts :: [RemoteHostInfo] -> [StyledString] viewRemoteHosts = \case @@ -1756,8 +2194,14 @@ viewRemoteCtrl CtrlAppInfo {deviceName, appVersionRange = AppVersionRange _ (App | otherwise = "" showCompatible = if compatible then "" else ", " <> bold' "not compatible" -viewChatError :: ChatLogLevel -> Bool -> ChatError -> [StyledString] -viewChatError logLevel testView = \case +viewRemoteCtrlStopped :: RemoteCtrlStopReason -> [StyledString] +viewRemoteCtrlStopped = \case + RCSRConnectionFailed (ChatErrorAgent (RCP RCEIdentity) _) -> + ["remote controller stopped: this link was used with another controller, please create a new link on the host"] + _ -> ["remote controller stopped"] + +viewChatError :: Bool -> ChatLogLevel -> Bool -> ChatError -> [StyledString] +viewChatError isCmd logLevel testView = \case ChatError err -> case err of CENoActiveUser -> ["error: active user is required"] CENoConnectionUser agentConnId -> ["error: message user not found, conn id: " <> sShow agentConnId | logLevel <= CLLError] @@ -1776,13 +2220,13 @@ viewChatError logLevel testView = \case CEUserNotHidden _ -> ["user is not hidden"] CEInvalidDisplayName {displayName, validName} -> map plain $ - ["invalid display name: " <> viewName displayName] + [if T.null displayName then "display name can't be empty" else "invalid display name: " <> viewName displayName] <> ["you could use this one: " <> viewName validName | not (T.null validName)] CEChatNotStarted -> ["error: chat not started"] CEChatNotStopped -> ["error: chat not stopped"] CEChatStoreChanged -> ["error: chat store changed, please restart chat"] - CEConnectionPlan connectionPlan -> viewConnectionPlan connectionPlan CEInvalidConnReq -> viewInvalidConnReq + CEUnsupportedConnReq -> [ "", "Connection link is not supported by the your app version, please ugrade it.", plain updateStr] CEInvalidChatMessage Connection {connId} msgMeta_ msg e -> [ plain $ ("chat message error: " <> e <> " (" <> T.unpack (T.take 120 msg) <> ")") @@ -1806,9 +2250,15 @@ viewChatError logLevel testView = \case CEGroupContactRole c -> ["contact " <> ttyContact c <> " has insufficient permissions for this group action"] CEGroupNotJoined g -> ["you did not join this group, use " <> highlight ("/join #" <> viewGroupName g)] CEGroupMemberNotActive -> ["your group connection is not active yet, try later"] + CECantBlockMemberForSelf g m showMsgs -> + [ "admins or above can't block member for self, use " + <> highlight + ( (if showMsgs then "/unblock for all" else "/block for all") + <> (" #" <> viewGroupName g <> " " <> viewMemberName m) + ) + ] CEGroupMemberUserRemoved -> ["you are no longer a member of the group"] CEGroupMemberNotFound -> ["group doesn't have this member"] - CEGroupMemberIntroNotFound c -> ["group member intro not found for " <> ttyContact c] CEGroupCantResendInvitation g c -> viewCannotResendInvitation g c CEGroupInternal s -> ["chat group bug: " <> plain s] CEFileNotFound f -> ["file not found: " <> plain f] @@ -1825,11 +2275,10 @@ viewChatError logLevel testView = \case CEFileImageType _ -> ["image type must be jpg, send as a file using " <> highlight' "/f"] CEFileImageSize _ -> ["max image size: " <> sShow maxImageSize <> " bytes, resize it or send as a file using " <> highlight' "/f"] CEFileNotReceived fileId -> ["file " <> sShow fileId <> " not received"] - CEXFTPRcvFile fileId aFileId e -> ["error receiving XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel == CLLError] - CEXFTPSndFile fileId aFileId e -> ["error sending XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel == CLLError] + CEFileNotApproved fileId unknownSrvs -> ["file " <> sShow fileId <> " aborted, unknwon XFTP servers:"] <> map (plain . show) unknownSrvs CEFallbackToSMPProhibited fileId -> ["recipient tried to accept file " <> sShow fileId <> " via old protocol, prohibited"] CEInlineFileProhibited _ -> ["A small file sent without acceptance - you can enable receiving such files with -f option."] - CEInvalidQuote -> ["cannot reply to this message"] + CEInvalidForward -> ["cannot forward message(s)"] CEInvalidChatItemUpdate -> ["cannot update this item"] CEInvalidChatItemDelete -> ["cannot delete this item"] CEHasCurrentCall -> ["call already in progress"] @@ -1844,6 +2293,7 @@ viewChatError logLevel testView = \case CEAgentCommandError e -> ["agent command error: " <> plain e] CEInvalidFileDescription e -> ["invalid file description: " <> plain e] CEConnectionIncognitoChangeProhibited -> ["incognito mode change prohibited"] + CEConnectionUserChangeProhibited -> ["incognito mode change prohibited for user"] CEPeerChatVRangeIncompatible -> ["peer chat protocol version range incompatible"] CEInternalError e -> ["internal chat error: " <> plain e] CEException e -> ["exception: " <> plain e] @@ -1871,6 +2321,7 @@ viewChatError logLevel testView = \case SEDuplicateGroupMessage {groupId, sharedMsgId} | testView -> ["duplicate group message, group id: " <> sShow groupId <> ", message id: " <> sShow sharedMsgId] | otherwise -> [] + SEUserNoteFolderNotFound -> ["no notes folder"] e -> ["chat db error: " <> sShow e] ChatErrorDatabase err -> case err of DBErrorEncrypted -> ["error: chat database is already encrypted"] @@ -1879,16 +2330,25 @@ viewChatError logLevel testView = \case DBErrorOpen e -> ["error opening database after encryption: " <> sqliteError' e] e -> ["chat database error: " <> sShow e] ChatErrorAgent err entity_ -> case err of - CMD PROHIBITED -> [withConnEntity <> "error: command is prohibited"] - SMP SMP.AUTH -> + CMD PROHIBITED cxt -> [withConnEntity <> plain ("error: command is prohibited, " <> cxt)] + SMP _ SMP.AUTH -> [ withConnEntity - <> "error: connection authorization failed - this could happen if connection was deleted,\ - \ secured with different credentials, or due to a bug - please re-create the connection" + <> "error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection" ] - AGENT A_DUPLICATE -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug] - AGENT A_PROHIBITED -> [withConnEntity <> "error: AGENT A_PROHIBITED" | logLevel <= CLLWarning] - CONN NOT_FOUND -> [withConnEntity <> "error: CONN NOT_FOUND" | logLevel <= CLLWarning] - e -> [withConnEntity <> "smp agent error: " <> sShow e | logLevel <= CLLWarning] + SMP _ (SMP.BLOCKED BlockingInfo {reason}) -> + [withConnEntity <> "error: connection blocked by server operator: " <> reasonStr] + where + reasonStr = case reason of + BRSpam -> "spam" + 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_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] + INTERNAL e -> [plain $ "internal error: " <> e] + e -> [withConnEntity <> "smp agent error: " <> sShow e | logLevel <= CLLWarning || isCmd] where withConnEntity = case entity_ of Just entity@(RcvDirectMsgConnection conn contact_) -> case contact_ of @@ -1906,7 +2366,7 @@ viewChatError logLevel testView = \case "[" <> connEntityLabel entity <> ", userContactLinkId: " <> sShow userContactLinkId <> ", connId: " <> cId conn <> "] " Nothing -> "" cId :: Connection -> StyledString - cId conn = sShow conn.connId + cId Connection {connId} = sShow connId ChatErrorRemoteCtrl e -> [plain $ "remote controller error: " <> show e] ChatErrorRemoteHost RHNew e -> [plain $ "new remote host error: " <> show e] ChatErrorRemoteHost (RHId rhId) e -> [plain $ "remote host " <> show rhId <> " error: " <> show e] @@ -1924,6 +2384,14 @@ viewConnectionEntityDisabled entity = case entity of where entityLabel = connEntityLabel entity +viewConnectionEntityInactive :: ConnectionEntity -> Bool -> [StyledString] +viewConnectionEntityInactive entity inactive + | inactive = ["[" <> connEntityLabel entity <> "] connection is marked as inactive"] + | otherwise = ["[" <> connEntityLabel entity <> "] inactive connection is marked as active"] + +viewJSON :: J.ToJSON a => a -> StyledString +viewJSON = plain . LB.toStrict . J.encode + connEntityLabel :: ConnectionEntity -> StyledString connEntityLabel = \case RcvDirectMsgConnection _ (Just Contact {localDisplayName = c}) -> plain c @@ -2004,7 +2472,10 @@ ttyFullGroup GroupInfo {localDisplayName = g, groupProfile = GroupProfile {fullN ttyGroup g <> optFullName g fullName ttyFromGroup :: GroupInfo -> GroupMember -> StyledString -ttyFromGroup g m = membershipIncognito g <> ttyFrom (fromGroup_ g m) +ttyFromGroup g m = ttyFromGroupAttention g m False + +ttyFromGroupAttention :: GroupInfo -> GroupMember -> Bool -> StyledString +ttyFromGroupAttention g m attention = membershipIncognito g <> ttyFrom (fromGroupAttention_ g m attention) ttyFromGroupEdited :: GroupInfo -> GroupMember -> StyledString ttyFromGroupEdited g m = membershipIncognito g <> ttyFrom (fromGroup_ g m <> "[edited] ") @@ -2014,7 +2485,12 @@ ttyFromGroupDeleted g m deletedText_ = membershipIncognito g <> ttyFrom (fromGroup_ g m <> maybe "" (\t -> "[" <> t <> "] ") deletedText_) fromGroup_ :: GroupInfo -> GroupMember -> Text -fromGroup_ g m = "#" <> viewGroupName g <> " " <> viewMemberName m <> "> " +fromGroup_ g m = fromGroupAttention_ g m False + +fromGroupAttention_ :: GroupInfo -> GroupMember -> Bool -> Text +fromGroupAttention_ g m attention = + let attn = if attention then "!" else "" + in "#" <> viewGroupName g <> " " <> viewMemberName m <> attn <> "> " ttyFrom :: Text -> StyledString ttyFrom = styled $ colored Yellow @@ -2028,9 +2504,6 @@ ttyToGroup g = membershipIncognito g <> ttyTo ("#" <> viewGroupName g <> " ") ttyToGroupEdited :: GroupInfo -> StyledString ttyToGroupEdited g = membershipIncognito g <> ttyTo ("#" <> viewGroupName g <> " [edited] ") -viewName :: Text -> Text -viewName s = if T.any isSpace s then "'" <> s <> "'" else s - ttyFilePath :: FilePath -> StyledString ttyFilePath = plain diff --git a/tests/Bots/BroadcastTests.hs b/tests/Bots/BroadcastTests.hs index ed0b9e069a..71142c8b60 100644 --- a/tests/Bots/BroadcastTests.hs +++ b/tests/Bots/BroadcastTests.hs @@ -1,6 +1,6 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} module Bots.BroadcastTests where @@ -8,17 +8,21 @@ module Bots.BroadcastTests where import Broadcast.Bot import Broadcast.Options import ChatClient +import ChatTests.DBUtils import ChatTests.Utils import Control.Concurrent (forkIO, killThread, threadDelay) import Control.Exception (bracket) import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Core -import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..)) +import Simplex.Chat.Options (CoreChatOpts (..)) +import Simplex.Chat.Options.DB import Simplex.Chat.Types (Profile (..)) +import Test.Hspec hiding (it) +#if !defined(dbPostgres) import System.FilePath ((</>)) -import Test.Hspec +#endif -broadcastBotTests :: SpecWith FilePath +broadcastBotTests :: SpecWith TestParams broadcastBotTests = do it "should broadcast message" testBroadcastMessages @@ -31,10 +35,20 @@ withBroadcastBot opts test = broadcastBotProfile :: Profile broadcastBotProfile = Profile {displayName = "broadcast_bot", fullName = "Broadcast Bot", image = Nothing, contactLink = Nothing, preferences = Nothing} -mkBotOpts :: FilePath -> [KnownContact] -> BroadcastBotOpts -mkBotOpts tmp publishers = +mkBotOpts :: TestParams -> [KnownContact] -> BroadcastBotOpts +mkBotOpts ps publishers = BroadcastBotOpts - { coreOptions = testOpts.coreOptions {dbFilePrefix = tmp </> botDbPrefix}, + { coreOptions = + testCoreOpts + { dbOptions = + (dbOptions testCoreOpts) +#if defined(dbPostgres) + {dbSchemaPrefix = "client_" <> botDbPrefix} +#else + {dbFilePrefix = tmpPath ps </> botDbPrefix} +#endif + + }, publishers, welcomeMessage = defaultWelcomeMessage publishers, prohibitedMessage = defaultWelcomeMessage publishers @@ -43,19 +57,19 @@ mkBotOpts tmp publishers = botDbPrefix :: FilePath botDbPrefix = "broadcast_bot" -testBroadcastMessages :: HasCallStack => FilePath -> IO () -testBroadcastMessages tmp = do +testBroadcastMessages :: HasCallStack => TestParams -> IO () +testBroadcastMessages ps = do botLink <- - withNewTestChat tmp botDbPrefix broadcastBotProfile $ \bc_bot -> - withNewTestChat tmp "alice" aliceProfile $ \alice -> do + withNewTestChat ps botDbPrefix broadcastBotProfile $ \bc_bot -> + withNewTestChat ps "alice" aliceProfile $ \alice -> do connectUsers bc_bot alice bc_bot ##> "/ad" getContactLink bc_bot True - let botOpts = mkBotOpts tmp [KnownContact 2 "alice"] + let botOpts = mkBotOpts ps [KnownContact 2 "alice"] withBroadcastBot botOpts $ - withTestChat tmp "alice" $ \alice -> - withNewTestChat tmp "bob" bobProfile $ \bob -> - withNewTestChat tmp "cath" cathProfile $ \cath -> do + withTestChat ps "alice" $ \alice -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do alice <## "1 contacts connected (use /cs for the list)" bob `connectVia` botLink bob #> "@broadcast_bot hello" @@ -64,10 +78,11 @@ testBroadcastMessages tmp = do bob <## "I broadcast messages to all connected users from @alice." cath `connectVia` botLink alice #> "@broadcast_bot hello all!" + alice <# "broadcast_bot> hello all!" -- we broadcast to the sender too, /feed is used by bot bob <# "broadcast_bot> hello all!" cath <# "broadcast_bot> hello all!" alice <# "broadcast_bot> > hello all!" - alice <## " Forwarded to 2 contact(s)" + alice <## " Forwarded to 3 contact(s), 0 errors" where cc `connectVia` botLink = do cc ##> ("/c " <> botLink) diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index 3c6991bb52..f8a5aa8b80 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -1,42 +1,52 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PostfixOperators #-} module Bots.DirectoryTests where import ChatClient +import ChatTests.DBUtils import ChatTests.Utils import Control.Concurrent (forkIO, killThread, threadDelay) import Control.Exception (finally) -import Control.Monad (forM_) -import Directory.Events (viewName) +import Control.Monad (forM_, when) +import qualified Data.Text as T +import Directory.Captcha import Directory.Options import Directory.Service import Directory.Store import GHC.IO.Handle (hClose) import Simplex.Chat.Bot.KnownContacts -import Simplex.Chat.Controller (ChatConfig (..)) +import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHooks) import Simplex.Chat.Core -import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..)) -import Simplex.Chat.Types (GroupMemberRole (..), Profile (..)) +import qualified Simplex.Chat.Markdown as MD +import Simplex.Chat.Options (CoreChatOpts (..)) +import Simplex.Chat.Options.DB +import Simplex.Chat.Types (Profile (..)) +import Simplex.Chat.Types.Shared (GroupMemberRole (..)) import System.FilePath ((</>)) -import Test.Hspec +import Test.Hspec hiding (it) -directoryServiceTests :: SpecWith FilePath +directoryServiceTests :: SpecWith TestParams directoryServiceTests = do it "should register group" testDirectoryService - it "should suspend and resume group" testSuspendResume + it "should suspend and resume group, send message to owner" testSuspendResume + it "should delete group registration" testDeleteGroup + it "admin should delete group registration" testDeleteGroupAdmin + it "should change initial member role" testSetRole it "should join found group via link" testJoinGroup 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 describe "de-listing the group" $ do it "should de-list if owner leaves the group" testDelistedOwnerLeaves it "should de-list if owner is removed from the group" testDelistedOwnerRemoved it "should NOT de-list if another member leaves the group" testNotDelistedMemberLeaves it "should NOT de-list if another member is removed from the group" testNotDelistedMemberRemoved it "should de-list if service is removed from the group" testDelistedServiceRemoved + it "should de-list if group is deleted" testDelistedGroupDeleted it "should de-list/re-list when service/owner roles change" testDelistedRoleChanges it "should NOT de-list if another member role changes" testNotDelistedMemberRoleChanged it "should NOT send to approval if roles are incorrect" testNotSentApprovalBadRoles @@ -44,9 +54,11 @@ directoryServiceTests = do describe "should require re-approval if profile is changed by" $ do it "the registration owner" testRegOwnerChangedProfile it "another owner" testAnotherOwnerChangedProfile + it "another owner not connected to directory" testNotConnectedOwnerChangedProfile describe "should require profile update if group link is removed by " $ do it "the registration owner" testRegOwnerRemovedLink it "another owner" testAnotherOwnerRemovedLink + it "another owner not connected to directory" testNotConnectedOwnerRemovedLink describe "duplicate groups (same display name and full name)" $ do it "should ask for confirmation if a duplicate group is submitted" testDuplicateAskConfirmation it "should prohibit registration if a duplicate group is listed" testDuplicateProhibitRegistration @@ -55,19 +67,42 @@ directoryServiceTests = do it "should prohibit approval if a duplicate group is listed" testDuplicateProhibitApproval describe "list groups" $ do it "should list user's groups" testListUserGroups + describe "member admission" $ do + it "should ask member to pass captcha screen" testCapthaScreening describe "store log" $ do it "should restore directory service state" testRestoreDirectory + describe "captcha" $ do + it "should accept some incorrect spellings" testCaptcha directoryProfile :: Profile directoryProfile = Profile {displayName = "SimpleX-Directory", fullName = "", image = Nothing, contactLink = Nothing, preferences = Nothing} -mkDirectoryOpts :: FilePath -> [KnownContact] -> DirectoryOpts -mkDirectoryOpts tmp superUsers = +mkDirectoryOpts :: TestParams -> [KnownContact] -> Maybe KnownGroup -> DirectoryOpts +mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup = DirectoryOpts - { coreOptions = testOpts.coreOptions {dbFilePrefix = tmp </> serviceDbPrefix}, + { coreOptions = + testCoreOpts + { dbOptions = + (dbOptions testCoreOpts) +#if defined(dbPostgres) + {dbSchemaPrefix = "client_" <> serviceDbPrefix} +#else + {dbFilePrefix = ps </> serviceDbPrefix} +#endif + + }, + adminUsers = [], superUsers, - directoryLog = Just $ tmp </> "directory_service.log", + ownersGroup, + blockedFragmentsFile = Nothing, + blockedWordsFile = Nothing, + blockedExtensionRules = Nothing, + nameSpellingFile = Nothing, + profileNameLimit = maxBound, + captchaGenerator = Nothing, + directoryLog = Just $ ps </> "directory_service.log", serviceName = "SimpleX-Directory", + runCLI = False, searchResults = 3, testing = True } @@ -75,11 +110,14 @@ mkDirectoryOpts tmp superUsers = serviceDbPrefix :: FilePath serviceDbPrefix = "directory_service" -testDirectoryService :: HasCallStack => FilePath -> IO () -testDirectoryService tmp = - withDirectoryService tmp $ \superUser dsLink -> - withNewTestChat tmp "bob" bobProfile $ \bob -> - withNewTestChat tmp "cath" cathProfile $ \cath -> do +viewName :: String -> String +viewName = T.unpack . MD.viewName . T.pack + +testDirectoryService :: HasCallStack => TestParams -> IO () +testDirectoryService ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink bob #> "@SimpleX-Directory privacy" bob <# "SimpleX-Directory> > privacy" @@ -93,7 +131,7 @@ testDirectoryService tmp = bob <# "SimpleX-Directory> You must grant directory service admin role to register the group" bob ##> "/mr PSA SimpleX-Directory admin" -- putStrLn "*** discover service joins group and creates the link for profile" - bob <## "#PSA: you changed the role of SimpleX-Directory from member to admin" + bob <## "#PSA: you changed the role of SimpleX-Directory to admin" bob <# "SimpleX-Directory> Joining the group PSA…" bob <## "#PSA: SimpleX-Directory joined the group" bob <# "SimpleX-Directory> Joined the group PSA, creating the link…" @@ -109,7 +147,7 @@ testDirectoryService tmp = -- putStrLn "*** update profile so that it has link" updateGroupProfile bob welcomeWithLink bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (PSA) is added to the welcome message." - bob <## "You will be notified once the group is added to the directory - it may take up to 24 hours." + bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." approvalRequested superUser welcomeWithLink (1 :: Int) -- putStrLn "*** update profile so that it still has link" let welcomeWithLink' = "Welcome! " <> welcomeWithLink @@ -137,13 +175,27 @@ testDirectoryService tmp = -- putStrLn "*** update profile so that it has link again" updateGroupProfile bob welcomeWithLink' bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (PSA) is added to the welcome message." - bob <## "You will be notified once the group is added to the directory - it may take up to 24 hours." + bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." approvalRequested superUser welcomeWithLink' (1 :: Int) + superUser #> "@SimpleX-Directory /pending" + superUser <# "SimpleX-Directory> > /pending" + superUser <## " 1 registered group(s)" + superUser <# "SimpleX-Directory> 1. PSA (Privacy, Security & Anonymity)" + superUser <## "Welcome message:" + superUser <##. "Welcome! Link to join the group PSA: " + superUser <## "Owner: bob" + superUser <## "2 members" + superUser <## "Status: pending admin approval" superUser #> "@SimpleX-Directory /approve 1:PSA 1" superUser <# "SimpleX-Directory> > /approve 1:PSA 1" superUser <## " Group approved!" - bob <# "SimpleX-Directory> The group ID 1 (PSA) is approved and listed in directory!" + bob <# "SimpleX-Directory> The group ID 1 (PSA) is approved and listed in directory - please moderate it!" bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." + bob <## "" + bob <## "Supported commands:" + bob <## "- /filter 1 - to configure anti-spam filter." + bob <## "- /role 1 - to set default member role." + bob <## "- /help commands - other commands." search bob "privacy" welcomeWithLink' search bob "security" welcomeWithLink' cath `connectVia` dsLink @@ -179,10 +231,10 @@ testDirectoryService tmp = su <## "To approve send:" su <# ("SimpleX-Directory> /approve 1:PSA " <> show grId) -testSuspendResume :: HasCallStack => FilePath -> IO () -testSuspendResume tmp = - withDirectoryService tmp $ \superUser dsLink -> - withNewTestChat tmp "bob" bobProfile $ \bob -> do +testSuspendResume :: HasCallStack => TestParams -> IO () +testSuspendResume ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" groupFound bob "privacy" @@ -196,13 +248,99 @@ testSuspendResume tmp = superUser <## " Group listing resumed!" bob <# "SimpleX-Directory> The group ID 1 (privacy) is listed in the directory again!" groupFound bob "privacy" + superUser #> "@SimpleX-Directory privacy" + groupFoundN_ "" (Just 1) 2 superUser "privacy" + superUser #> "@SimpleX-Directory /link 1:privacy" + superUser <# "SimpleX-Directory> > /link 1:privacy" + superUser <## " The link to join the group ID 1 (privacy):" + superUser <##. "https://simplex.chat/contact" + superUser <## "New member role: member" + superUser #> "@SimpleX-Directory /owner 1:privacy hello there" + superUser <# "SimpleX-Directory> > /owner 1:privacy hello there" + superUser <## " Forwarded to @bob, the owner of the group ID 1 (privacy)" + bob <# "SimpleX-Directory> hello there" -testJoinGroup :: HasCallStack => FilePath -> IO () -testJoinGroup tmp = - withDirectoryServiceCfg tmp testCfgGroupLinkViaContact $ \superUser dsLink -> - withNewTestChatCfg tmp testCfgGroupLinkViaContact "bob" bobProfile $ \bob -> do - withNewTestChatCfg tmp testCfgGroupLinkViaContact "cath" cathProfile $ \cath -> - withNewTestChatCfg tmp testCfgGroupLinkViaContact "dan" danProfile $ \dan -> do +testDeleteGroup :: HasCallStack => TestParams -> IO () +testDeleteGroup ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + groupFound bob "privacy" + bob #> "@SimpleX-Directory /delete 1:privacy" + bob <# "SimpleX-Directory> > /delete 1:privacy" + bob <## " Your group privacy is deleted from the directory" + groupNotFound bob "privacy" + +testDeleteGroupAdmin :: HasCallStack => TestParams -> IO () +testDeleteGroupAdmin ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + withNewTestChat ps "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + cath `connectVia` dsLink + registerGroupId superUser cath "security" "Security" 2 1 + groupFound bob "privacy" + groupFound bob "security" + listUserGroup bob "privacy" "Privacy" + listUserGroup cath "security" "Security" + superUser #> "@SimpleX-Directory /last" + superUser <# "SimpleX-Directory> > /last" + superUser <## " 2 registered group(s)" + memberGroupListing superUser bob 1 "privacy" "Privacy" 2 "active" + memberGroupListing superUser cath 2 "security" "Security" 2 "active" + -- trying to register group with the same name + submitGroup bob "security" "Security" + bob <# "SimpleX-Directory> The group security (Security) is already listed in the directory, please choose another name." + bob ##> "/d #security" + bob <## "#security: you deleted the group" + -- admin can delete the group + superUser #> "@SimpleX-Directory /delete 2:security" + superUser <# "SimpleX-Directory> > /delete 2:security" + superUser <## " The group security is deleted from the directory" + groupFound cath "privacy" + listUserGroup bob "privacy" "Privacy" + groupNotFound bob "security" + sendListCommand cath 0 + -- another user can register the group with the same name + registerGroupId superUser bob "security" "Security" 4 2 + +testSetRole :: HasCallStack => TestParams -> IO () +testSetRole ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + groupFound bob "privacy" + bob #> "@SimpleX-Directory /role 1:privacy observer" + bob <# "SimpleX-Directory> > /role 1:privacy observer" + bob <## " The initial member role for the group privacy is set to observer" + bob <## "" + note <- getTermLine bob + let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note + cath ##> ("/c " <> groupLink) + cath <## "connection request sent!" + cath <## "#privacy: joining the group..." + cath <## "#privacy: you joined the group" + cath <#. "#privacy SimpleX-Directory> Link to join the group privacy: https://simplex.chat/" + cath <## "#privacy: member bob (Bob) is connected" + bob <## "#privacy: SimpleX-Directory added cath (Catherine) to the group (connecting...)" + bob <## "#privacy: new member cath is connected" + bob ##> "/ms #privacy" + bob <## "bob (Bob): owner, you, created group" + bob <## "SimpleX-Directory: admin, invited, connected" + bob <## "cath (Catherine): observer, connected" + cath ##> "#privacy hello" + cath <## "#privacy: you don't have permission to send messages" + +testJoinGroup :: HasCallStack => TestParams -> IO () +testJoinGroup ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" cath `connectVia` dsLink @@ -216,10 +354,10 @@ testJoinGroup tmp = cath <## "2 members" cath ##> ("/c " <> groupLink) cath <## "connection request sent!" - cath <## "SimpleX-Directory_1: contact is connected" - cath <## "contact SimpleX-Directory_1 is merged into SimpleX-Directory" - cath <## "use @SimpleX-Directory <message> to send messages" + cath <## "#privacy: joining the group..." cath <## "#privacy: you joined the group" + cath <## "contact and member are merged: SimpleX-Directory, #privacy SimpleX-Directory_1" + cath <## "use @SimpleX-Directory <message> to send messages" cath <# ("#privacy SimpleX-Directory> " <> welcomeMsg) cath <## "#privacy: member bob (Bob) is connected" bob <## "#privacy: SimpleX-Directory added cath (Catherine) to the group (connecting...)" @@ -231,11 +369,9 @@ testJoinGroup tmp = concurrentlyN_ [ do bob <## "dan (Daniel): accepting request to join group #privacy..." - bob <## "dan (Daniel): contact is connected" - bob <## "dan invited to group #privacy via your group link" bob <## "#privacy: dan joined the group", do - dan <## "bob (Bob): contact is connected" + dan <## "#privacy: joining the group..." dan <## "#privacy: you joined the group" dan <# ("#privacy bob> " <> welcomeMsg) dan @@ -247,10 +383,10 @@ testJoinGroup tmp = cath <## "#privacy: new member dan is connected" ] -testGroupNameWithSpaces :: HasCallStack => FilePath -> IO () -testGroupNameWithSpaces tmp = - withDirectoryService tmp $ \superUser dsLink -> - withNewTestChat tmp "bob" bobProfile $ \bob -> do +testGroupNameWithSpaces :: HasCallStack => TestParams -> IO () +testGroupNameWithSpaces ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> do bob `connectVia` dsLink registerGroup superUser bob "Privacy & Security" "" groupFound bob "Privacy & Security" @@ -265,11 +401,11 @@ testGroupNameWithSpaces tmp = bob <# "SimpleX-Directory> The group ID 1 (Privacy & Security) is listed in the directory again!" groupFound bob "Privacy & Security" -testSearchGroups :: HasCallStack => FilePath -> IO () -testSearchGroups tmp = - withDirectoryService tmp $ \superUser dsLink -> - withNewTestChat tmp "bob" bobProfile $ \bob -> do - withNewTestChat tmp "cath" cathProfile $ \cath -> do +testSearchGroups :: HasCallStack => TestParams -> IO () +testSearchGroups ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink cath `connectVia` dsLink forM_ [1..8 :: Int] $ \i -> registerGroupId superUser bob (groups !! (i - 1)) "" i i @@ -351,11 +487,29 @@ testSearchGroups tmp = u <##. "Link to join the group " u <## (show count <> " members") -testDelistedOwnerLeaves :: HasCallStack => FilePath -> IO () -testDelistedOwnerLeaves tmp = - withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do +testInviteToOwnersGroup :: HasCallStack => TestParams -> IO () +testInviteToOwnersGroup ps = + withDirectoryServiceCfgOwnersGroup ps testCfg True $ \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" + -- second group + registerGroupId superUser bob "security" "Security" 3 2 + superUser <## "Owner is already a member of owners' group" + +testDelistedOwnerLeaves :: HasCallStack => TestParams -> IO () +testDelistedOwnerLeaves ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath @@ -365,13 +519,16 @@ testDelistedOwnerLeaves tmp = bob <## "" bob <## "The group is no longer listed in the directory." superUser <# "SimpleX-Directory> The group ID 1 (privacy) is de-listed (group owner left)." + cath `connectVia` dsLink + cath <## "contact and member are merged: SimpleX-Directory_1, #privacy SimpleX-Directory" + cath <## "use @SimpleX-Directory <message> to send messages" groupNotFound cath "privacy" -testDelistedOwnerRemoved :: HasCallStack => FilePath -> IO () -testDelistedOwnerRemoved tmp = - withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do +testDelistedOwnerRemoved :: HasCallStack => TestParams -> IO () +testDelistedOwnerRemoved ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath @@ -380,38 +537,45 @@ testDelistedOwnerRemoved tmp = bob <## "" bob <## "The group is no longer listed in the directory." superUser <# "SimpleX-Directory> The group ID 1 (privacy) is de-listed (group owner is removed)." + cath `connectVia` dsLink + cath <## "contact and member are merged: SimpleX-Directory_1, #privacy SimpleX-Directory" + cath <## "use @SimpleX-Directory <message> to send messages" groupNotFound cath "privacy" -testNotDelistedMemberLeaves :: HasCallStack => FilePath -> IO () -testNotDelistedMemberLeaves tmp = - withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do +testNotDelistedMemberLeaves :: HasCallStack => TestParams -> IO () +testNotDelistedMemberLeaves ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath leaveGroup "privacy" cath bob <## "#privacy: cath left the group" (superUser </) - groupFound cath "privacy" + cath `connectVia` dsLink + cath #> "@SimpleX-Directory_1 privacy" + groupFoundN_ "_1" Nothing 2 cath "privacy" -testNotDelistedMemberRemoved :: HasCallStack => FilePath -> IO () -testNotDelistedMemberRemoved tmp = - withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do +testNotDelistedMemberRemoved :: HasCallStack => TestParams -> IO () +testNotDelistedMemberRemoved ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath removeMember "privacy" bob cath (superUser </) - groupFound cath "privacy" + cath `connectVia` dsLink + cath #> "@SimpleX-Directory_1 privacy" + groupFoundN_ "_1" Nothing 2 cath "privacy" -testDelistedServiceRemoved :: HasCallStack => FilePath -> IO () -testDelistedServiceRemoved tmp = - withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do +testDelistedServiceRemoved :: HasCallStack => TestParams -> IO () +testDelistedServiceRemoved ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath @@ -422,20 +586,48 @@ testDelistedServiceRemoved tmp = bob <## "" bob <## "The group is no longer listed in the directory." superUser <# "SimpleX-Directory> The group ID 1 (privacy) is de-listed (directory service is removed)." + cath `connectVia` dsLink + groupNotFound_ "_1" cath "privacy" + +testDelistedGroupDeleted :: HasCallStack => TestParams -> IO () +testDelistedGroupDeleted ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + cath `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + connectUsers bob cath + fullAddMember "privacy" "Privacy" bob cath GROwner + joinGroup "privacy" cath bob + cath <## "#privacy: member SimpleX-Directory_1 is connected" + cath <## "contact and member are merged: SimpleX-Directory, #privacy SimpleX-Directory_1" + cath <## "use @SimpleX-Directory <message> to send messages" + bob ##> "/d #privacy" + bob <## "#privacy: you deleted the group" + bob <# "SimpleX-Directory> The group ID 1 (privacy) is deleted." + bob <## "" + bob <## "The group is no longer listed in the directory." + cath <## "#privacy: bob deleted the group" + cath <## "use /d #privacy to delete the local copy of the group" + superUser <# "SimpleX-Directory> The group ID 1 (privacy) is de-listed (group is deleted)." groupNotFound cath "privacy" -testDelistedRoleChanges :: HasCallStack => FilePath -> IO () -testDelistedRoleChanges tmp = - withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do +testDelistedRoleChanges :: HasCallStack => TestParams -> IO () +testDelistedRoleChanges ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath + cath `connectVia` dsLink + cath <## "contact and member are merged: SimpleX-Directory_1, #privacy SimpleX-Directory" + cath <## "use @SimpleX-Directory <message> to send messages" groupFoundN 3 cath "privacy" -- de-listed if service role changed bob ##> "/mr privacy SimpleX-Directory member" - bob <## "#privacy: you changed the role of SimpleX-Directory from admin to member" + bob <## "#privacy: you changed the role of SimpleX-Directory to member" cath <## "#privacy: bob changed the role of SimpleX-Directory from admin to member" bob <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (privacy) is changed to member." bob <## "" @@ -444,7 +636,7 @@ testDelistedRoleChanges tmp = groupNotFound cath "privacy" -- re-listed if service role changed back without profile changes cath ##> "/mr privacy SimpleX-Directory admin" - cath <## "#privacy: you changed the role of SimpleX-Directory from member to admin" + cath <## "#privacy: you changed the role of SimpleX-Directory to admin" bob <## "#privacy: cath changed the role of SimpleX-Directory from member to admin" bob <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (privacy) is changed to admin." bob <## "" @@ -453,7 +645,7 @@ testDelistedRoleChanges tmp = groupFoundN 3 cath "privacy" -- de-listed if owner role changed cath ##> "/mr privacy bob admin" - cath <## "#privacy: you changed the role of bob from owner to admin" + cath <## "#privacy: you changed the role of bob to admin" bob <## "#privacy: cath changed your role from owner to admin" bob <# "SimpleX-Directory> Your role in the group ID 1 (privacy) is changed to admin." bob <## "" @@ -462,7 +654,7 @@ testDelistedRoleChanges tmp = groupNotFound cath "privacy" -- re-listed if owner role changed back without profile changes cath ##> "/mr privacy bob owner" - cath <## "#privacy: you changed the role of bob from admin to owner" + cath <## "#privacy: you changed the role of bob to owner" bob <## "#privacy: cath changed your role from admin to owner" bob <# "SimpleX-Directory> Your role in the group ID 1 (privacy) is changed to owner." bob <## "" @@ -470,35 +662,38 @@ testDelistedRoleChanges tmp = superUser <# "SimpleX-Directory> The group ID 1 (privacy) is listed (user role is set to owner)." groupFoundN 3 cath "privacy" -testNotDelistedMemberRoleChanged :: HasCallStack => FilePath -> IO () -testNotDelistedMemberRoleChanged tmp = - withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do +testNotDelistedMemberRoleChanged :: HasCallStack => TestParams -> IO () +testNotDelistedMemberRoleChanged ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath + cath `connectVia` dsLink + cath <## "contact and member are merged: SimpleX-Directory_1, #privacy SimpleX-Directory" + cath <## "use @SimpleX-Directory <message> to send messages" groupFoundN 3 cath "privacy" bob ##> "/mr privacy cath member" - bob <## "#privacy: you changed the role of cath from owner to member" + bob <## "#privacy: you changed the role of cath to member" cath <## "#privacy: bob changed your role from owner to member" groupFoundN 3 cath "privacy" -testNotSentApprovalBadRoles :: HasCallStack => FilePath -> IO () -testNotSentApprovalBadRoles tmp = - withDirectoryService tmp $ \superUser dsLink -> - withNewTestChat tmp "bob" bobProfile $ \bob -> - withNewTestChat tmp "cath" cathProfile $ \cath -> do +testNotSentApprovalBadRoles :: HasCallStack => TestParams -> IO () +testNotSentApprovalBadRoles ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink cath `connectVia` dsLink submitGroup bob "privacy" "Privacy" welcomeWithLink <- groupAccepted bob "privacy" bob ##> "/mr privacy SimpleX-Directory member" - bob <## "#privacy: you changed the role of SimpleX-Directory from admin to member" + bob <## "#privacy: you changed the role of SimpleX-Directory to member" updateProfileWithLink bob "privacy" welcomeWithLink 1 bob <# "SimpleX-Directory> You must grant directory service admin role to register the group" bob ##> "/mr privacy SimpleX-Directory admin" - bob <## "#privacy: you changed the role of SimpleX-Directory from member to admin" + bob <## "#privacy: you changed the role of SimpleX-Directory to admin" bob <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (privacy) is changed to admin." bob <## "" bob <## "The group is submitted for approval." @@ -507,11 +702,11 @@ testNotSentApprovalBadRoles tmp = approveRegistration superUser bob "privacy" 1 groupFound cath "privacy" -testNotApprovedBadRoles :: HasCallStack => FilePath -> IO () -testNotApprovedBadRoles tmp = - withDirectoryService tmp $ \superUser dsLink -> - withNewTestChat tmp "bob" bobProfile $ \bob -> - withNewTestChat tmp "cath" cathProfile $ \cath -> do +testNotApprovedBadRoles :: HasCallStack => TestParams -> IO () +testNotApprovedBadRoles ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink cath `connectVia` dsLink submitGroup bob "privacy" "Privacy" @@ -519,14 +714,14 @@ testNotApprovedBadRoles tmp = updateProfileWithLink bob "privacy" welcomeWithLink 1 notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1 bob ##> "/mr privacy SimpleX-Directory member" - bob <## "#privacy: you changed the role of SimpleX-Directory from admin to member" + bob <## "#privacy: you changed the role of SimpleX-Directory to member" let approve = "/approve 1:privacy 1" superUser #> ("@SimpleX-Directory " <> approve) superUser <# ("SimpleX-Directory> > " <> approve) - superUser <## " Group is not approved: user is not an owner." + superUser <## " Group is not approved: SimpleX-Directory is not an admin." groupNotFound cath "privacy" bob ##> "/mr privacy SimpleX-Directory admin" - bob <## "#privacy: you changed the role of SimpleX-Directory from member to admin" + bob <## "#privacy: you changed the role of SimpleX-Directory to admin" bob <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (privacy) is changed to admin." bob <## "" bob <## "The group is submitted for approval." @@ -534,11 +729,11 @@ testNotApprovedBadRoles tmp = approveRegistration superUser bob "privacy" 1 groupFound cath "privacy" -testRegOwnerChangedProfile :: HasCallStack => FilePath -> IO () -testRegOwnerChangedProfile tmp = - withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do +testRegOwnerChangedProfile :: HasCallStack => TestParams -> IO () +testRegOwnerChangedProfile ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath @@ -548,35 +743,62 @@ testRegOwnerChangedProfile tmp = bob <## "It is hidden from the directory until approved." cath <## "bob updated group #privacy:" cath <## "full name changed to: Privacy and Security" + cath `connectVia` dsLink + cath <## "contact and member are merged: SimpleX-Directory_1, #privacy SimpleX-Directory" + cath <## "use @SimpleX-Directory <message> to send messages" groupNotFound cath "privacy" superUser <# "SimpleX-Directory> The group ID 1 (privacy) is updated." reapproveGroup 3 superUser bob groupFoundN 3 cath "privacy" -testAnotherOwnerChangedProfile :: HasCallStack => FilePath -> IO () -testAnotherOwnerChangedProfile tmp = - withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do +testAnotherOwnerChangedProfile :: HasCallStack => TestParams -> IO () +testAnotherOwnerChangedProfile ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath + cath `connectVia` dsLink + cath <## "contact and member are merged: SimpleX-Directory_1, #privacy SimpleX-Directory" + cath <## "use @SimpleX-Directory <message> to send messages" cath ##> "/gp privacy privacy Privacy and Security" cath <## "full name changed to: Privacy and Security" bob <## "cath updated group #privacy:" bob <## "full name changed to: Privacy and Security" - bob <# "SimpleX-Directory> The group ID 1 (privacy) is updated!" + bob <# "SimpleX-Directory> The group ID 1 (privacy) is updated by cath!" bob <## "It is hidden from the directory until approved." groupNotFound cath "privacy" - superUser <# "SimpleX-Directory> The group ID 1 (privacy) is updated." + superUser <# "SimpleX-Directory> The group ID 1 (privacy) is updated by cath." reapproveGroup 3 superUser bob groupFoundN 3 cath "privacy" -testRegOwnerRemovedLink :: HasCallStack => FilePath -> IO () -testRegOwnerRemovedLink tmp = - withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do +testNotConnectedOwnerChangedProfile :: HasCallStack => TestParams -> IO () +testNotConnectedOwnerChangedProfile ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + withNewTestChat ps "dan" danProfile $ \dan -> do + bob `connectVia` dsLink + dan `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + addCathAsOwner bob cath + cath ##> "/gp privacy privacy Privacy and Security" + cath <## "full name changed to: Privacy and Security" + bob <## "cath updated group #privacy:" + bob <## "full name changed to: Privacy and Security" + bob <# "SimpleX-Directory> The group ID 1 (privacy) is updated by cath!" + bob <## "It is hidden from the directory until approved." + groupNotFound dan "privacy" + superUser <# "SimpleX-Directory> The group ID 1 (privacy) is updated by cath." + reapproveGroup 3 superUser bob + groupFoundN 3 dan "privacy" + +testRegOwnerRemovedLink :: HasCallStack => TestParams -> IO () +testRegOwnerRemovedLink ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath @@ -593,26 +815,33 @@ testRegOwnerRemovedLink tmp = cath <## "description changed to:" cath <## "Welcome!" superUser <# "SimpleX-Directory> The group link is removed from ID 1 (privacy), de-listed." + cath `connectVia` dsLink + cath <## "contact and member are merged: SimpleX-Directory_1, #privacy SimpleX-Directory" + cath <## "use @SimpleX-Directory <message> to send messages" groupNotFound cath "privacy" - bob ##> ("/set welcome #privacy " <> welcomeWithLink) + let withChangedLink = T.unpack $ T.replace "contact#/?v=2-7&" "contact#/?v=3-7&" $ T.pack welcomeWithLink + bob ##> ("/set welcome #privacy " <> withChangedLink) bob <## "description changed to:" - bob <## welcomeWithLink + bob <## withChangedLink bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message." - bob <## "You will be notified once the group is added to the directory - it may take up to 24 hours." + bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." cath <## "bob updated group #privacy:" cath <## "description changed to:" - cath <## welcomeWithLink + cath <## withChangedLink reapproveGroup 3 superUser bob groupFoundN 3 cath "privacy" -testAnotherOwnerRemovedLink :: HasCallStack => FilePath -> IO () -testAnotherOwnerRemovedLink tmp = - withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do +testAnotherOwnerRemovedLink :: HasCallStack => TestParams -> IO () +testAnotherOwnerRemovedLink ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath + cath `connectVia` dsLink + cath <## "contact and member are merged: SimpleX-Directory_1, #privacy SimpleX-Directory" + cath <## "use @SimpleX-Directory <message> to send messages" bob ##> "/show welcome #privacy" bob <## "Welcome message:" welcomeWithLink <- getTermLine bob @@ -622,7 +851,7 @@ testAnotherOwnerRemovedLink tmp = bob <## "cath updated group #privacy:" bob <## "description changed to:" bob <## "Welcome!" - bob <# "SimpleX-Directory> The group link for ID 1 (privacy) is removed from the welcome message." + bob <# "SimpleX-Directory> The group link for ID 1 (privacy) is removed from the welcome message by cath." bob <## "" bob <## "The group is hidden from the directory until the group link is added and the group is re-approved." superUser <# "SimpleX-Directory> The group link is removed from ID 1 (privacy), de-listed." @@ -633,25 +862,60 @@ testAnotherOwnerRemovedLink tmp = bob <## "cath updated group #privacy:" bob <## "description changed to:" bob <## welcomeWithLink - bob <# "SimpleX-Directory> The group link is added by another group member, your registration will not be processed." - bob <## "" - bob <## "Please update the group profile yourself." - bob ##> ("/set welcome #privacy " <> welcomeWithLink <> " - welcome!") - bob <## "description changed to:" - bob <## (welcomeWithLink <> " - welcome!") - bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message." - bob <## "You will be notified once the group is added to the directory - it may take up to 24 hours." - cath <## "bob updated group #privacy:" - cath <## "description changed to:" - cath <## (welcomeWithLink <> " - welcome!") + bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message by cath." + bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." reapproveGroup 3 superUser bob groupFoundN 3 cath "privacy" -testDuplicateAskConfirmation :: HasCallStack => FilePath -> IO () -testDuplicateAskConfirmation tmp = - withDirectoryService tmp $ \superUser dsLink -> - withNewTestChat tmp "bob" bobProfile $ \bob -> - withNewTestChat tmp "cath" cathProfile $ \cath -> do +testNotConnectedOwnerRemovedLink :: HasCallStack => TestParams -> IO () +testNotConnectedOwnerRemovedLink ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + withNewTestChat ps "dan" danProfile $ \dan -> do + bob `connectVia` dsLink + dan `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + addCathAsOwner bob cath + bob ##> "/show welcome #privacy" + bob <## "Welcome message:" + welcomeWithLink <- getTermLine bob + cath ##> "/set welcome #privacy Welcome!" + cath <## "description changed to:" + cath <## "Welcome!" + bob <## "cath updated group #privacy:" + bob <## "description changed to:" + bob <## "Welcome!" + bob <# "SimpleX-Directory> The group link for ID 1 (privacy) is removed from the welcome message by cath." + bob <## "" + bob <## "The group is hidden from the directory until the group link is added and the group is re-approved." + superUser <# "SimpleX-Directory> The group link is removed from ID 1 (privacy), de-listed." + groupNotFound dan "privacy" + cath ##> ("/set welcome #privacy " <> welcomeWithLink) + cath <## "description changed to:" + cath <## welcomeWithLink + bob <## "cath updated group #privacy:" + bob <## "description changed to:" + bob <## welcomeWithLink + -- bob <# "SimpleX-Directory> The group link is added by another group member, your registration will not be processed." + -- bob <## "" + -- bob <## "Please update the group profile yourself." + -- bob ##> ("/set welcome #privacy " <> welcomeWithLink <> " - welcome!") + -- bob <## "description changed to:" + -- bob <## (welcomeWithLink <> " - welcome!") + bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message by cath." + bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." + -- cath <## "bob updated group #privacy:" + -- cath <## "description changed to:" + -- cath <## (welcomeWithLink <> " - welcome!") + reapproveGroup 3 superUser bob + groupFoundN 3 dan "privacy" + +testDuplicateAskConfirmation :: HasCallStack => TestParams -> IO () +testDuplicateAskConfirmation ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink submitGroup bob "privacy" "Privacy" _ <- groupAccepted bob "privacy" @@ -666,11 +930,11 @@ testDuplicateAskConfirmation tmp = completeRegistration superUser cath "privacy" "Privacy" welcomeWithLink 2 groupFound bob "privacy" -testDuplicateProhibitRegistration :: HasCallStack => FilePath -> IO () -testDuplicateProhibitRegistration tmp = - withDirectoryService tmp $ \superUser dsLink -> - withNewTestChat tmp "bob" bobProfile $ \bob -> - withNewTestChat tmp "cath" cathProfile $ \cath -> do +testDuplicateProhibitRegistration :: HasCallStack => TestParams -> IO () +testDuplicateProhibitRegistration ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" cath `connectVia` dsLink @@ -678,11 +942,11 @@ testDuplicateProhibitRegistration tmp = _ <- submitGroup cath "privacy" "Privacy" cath <# "SimpleX-Directory> The group privacy (Privacy) is already listed in the directory, please choose another name." -testDuplicateProhibitConfirmation :: HasCallStack => FilePath -> IO () -testDuplicateProhibitConfirmation tmp = - withDirectoryService tmp $ \superUser dsLink -> - withNewTestChat tmp "bob" bobProfile $ \bob -> - withNewTestChat tmp "cath" cathProfile $ \cath -> do +testDuplicateProhibitConfirmation :: HasCallStack => TestParams -> IO () +testDuplicateProhibitConfirmation ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink submitGroup bob "privacy" "Privacy" welcomeWithLink <- groupAccepted bob "privacy" @@ -697,11 +961,11 @@ testDuplicateProhibitConfirmation tmp = cath #> "@SimpleX-Directory /confirm 1:privacy" cath <# "SimpleX-Directory> The group privacy (Privacy) is already listed in the directory, please choose another name." -testDuplicateProhibitWhenUpdated :: HasCallStack => FilePath -> IO () -testDuplicateProhibitWhenUpdated tmp = - withDirectoryService tmp $ \superUser dsLink -> - withNewTestChat tmp "bob" bobProfile $ \bob -> - withNewTestChat tmp "cath" cathProfile $ \cath -> do +testDuplicateProhibitWhenUpdated :: HasCallStack => TestParams -> IO () +testDuplicateProhibitWhenUpdated ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink submitGroup bob "privacy" "Privacy" welcomeWithLink <- groupAccepted bob "privacy" @@ -722,17 +986,17 @@ testDuplicateProhibitWhenUpdated tmp = cath ##> "/gp privacy security Security" cath <## "changed to #security (Security)" cath <# "SimpleX-Directory> Thank you! The group link for ID 2 (security) is added to the welcome message." - cath <## "You will be notified once the group is added to the directory - it may take up to 24 hours." + cath <## "You will be notified once the group is added to the directory - it may take up to 48 hours." notifySuperUser superUser cath "security" "Security" welcomeWithLink' 2 approveRegistration superUser cath "security" 2 groupFound bob "security" groupFound cath "security" -testDuplicateProhibitApproval :: HasCallStack => FilePath -> IO () -testDuplicateProhibitApproval tmp = - withDirectoryService tmp $ \superUser dsLink -> - withNewTestChat tmp "bob" bobProfile $ \bob -> - withNewTestChat tmp "cath" cathProfile $ \cath -> do +testDuplicateProhibitApproval :: HasCallStack => TestParams -> IO () +testDuplicateProhibitApproval ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink submitGroup bob "privacy" "Privacy" welcomeWithLink <- groupAccepted bob "privacy" @@ -754,11 +1018,11 @@ testDuplicateProhibitApproval tmp = superUser <# ("SimpleX-Directory> > " <> approve) superUser <## " The group ID 2 (privacy) is already listed in the directory." -testListUserGroups :: HasCallStack => FilePath -> IO () -testListUserGroups tmp = - withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> - withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do +testListUserGroups :: HasCallStack => TestParams -> IO () +testListUserGroups ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink cath `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" @@ -766,22 +1030,15 @@ testListUserGroups tmp = fullAddMember "privacy" "Privacy" bob cath GRMember joinGroup "privacy" cath bob cath <## "#privacy: member SimpleX-Directory_1 is connected" - cath <## "contact SimpleX-Directory_1 is merged into SimpleX-Directory" + cath <## "contact and member are merged: SimpleX-Directory, #privacy SimpleX-Directory_1" cath <## "use @SimpleX-Directory <message> to send messages" registerGroupId superUser bob "security" "Security" 2 2 registerGroupId superUser cath "anonymity" "Anonymity" 3 1 - cath #> "@SimpleX-Directory /list" - cath <# "SimpleX-Directory> > /list" - cath <## " 1 registered group(s)" - cath <# "SimpleX-Directory> 1. anonymity (Anonymity)" - cath <## "Welcome message:" - cath <##. "Link to join the group anonymity: " - cath <## "2 members" - cath <## "Status: active" + listUserGroup cath "anonymity" "Anonymity" -- with de-listed group groupFound cath "anonymity" cath ##> "/mr anonymity SimpleX-Directory member" - cath <## "#anonymity: you changed the role of SimpleX-Directory from admin to member" + cath <## "#anonymity: you changed the role of SimpleX-Directory to member" cath <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (anonymity) is changed to member." cath <## "" cath <## "The group is no longer listed in the directory." @@ -789,51 +1046,131 @@ testListUserGroups tmp = groupNotFound cath "anonymity" listGroups superUser bob cath -testRestoreDirectory :: HasCallStack => FilePath -> IO () -testRestoreDirectory tmp = do - testListUserGroups tmp - restoreDirectoryService tmp 3 3 $ \superUser _dsLink -> - withTestChat tmp "bob" $ \bob -> - withTestChat tmp "cath" $ \cath -> do +testCapthaScreening :: HasCallStack => TestParams -> IO () +testCapthaScreening ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + -- check default role + bob #> "@SimpleX-Directory /role 1" + bob <# "SimpleX-Directory> > /role 1" + bob <## " The initial member role for the group privacy is set to member" + bob <## "Send /role 1 observer to change it." + bob <## "" + note <- getTermLine bob + let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note + -- enable captcha + bob #> "@SimpleX-Directory /filter 1 captcha" + bob <# "SimpleX-Directory> > /filter 1 captcha" + bob <## " Spam filter settings for group privacy set to:" + bob <## "- reject long/inappropriate names: disabled" + bob <## "- pass captcha to join: enabled" + bob <## "" + bob <## "Use /filter 1 [name] [captcha] to enable and /filter 1 off to disable filter." + -- connect with captcha screen + _ <- join cath groupLink + cath #> "#privacy 123" -- sending incorrect captcha + cath <# "#privacy SimpleX-Directory!> > cath 123" + cath <## " Incorrect text, please try again." + captcha <- dropStrPrefix "#privacy SimpleX-Directory> " . dropTime <$> getTermLine cath + sendCaptcha cath captcha + cath <#. "#privacy SimpleX-Directory> Link to join the group privacy: https://" + cath <## "#privacy: member bob (Bob) is connected" + bob <## "#privacy: SimpleX-Directory added cath (Catherine) to the group (connecting...)" + bob <## "#privacy: new member cath is connected" + cath #> "#privacy hello" + bob <# "#privacy cath> hello" + cath ##> "/l privacy" + cath <## "#privacy: you left the group" + cath <## "use /d #privacy to delete the group" + bob <## "#privacy: cath left the group" + cath ##> "/d #privacy" + cath <## "#privacy: you deleted the group" + -- change default role to observer + bob #> "@SimpleX-Directory /role 1 observer" + bob <# "SimpleX-Directory> > /role 1 observer" + bob <## " The initial member role for the group privacy is set to observer" + bob <## "" + bob <##. "Please note: it applies only to members joining via this link: https://" + -- connect with captcha screen again, as observer + captcha' <- join cath groupLink + sendCaptcha cath captcha' + -- message from cath that left + pastMember <- dropStrPrefix "#privacy: SimpleX-Directory forwarded a message from an unknown member, creating unknown member record " <$> getTermLine cath + cath <# ("#privacy " <> pastMember <> "> hello [>>]") + cath <#. "#privacy SimpleX-Directory> Link to join the group privacy: https://" + cath <## "#privacy: member bob (Bob) is connected" + bob <## "#privacy: SimpleX-Directory added cath_1 (Catherine) to the group (connecting...)" + bob <## "#privacy: new member cath_1 is connected" + cath ##> "#privacy hello" + cath <## "#privacy: you don't have permission to send messages" + (bob </) + cath ##> "/ms privacy" + cath <## "cath (Catherine): observer, you, connected" + cath <## "SimpleX-Directory: admin, host, connected" + cath <## "bob (Bob): owner, connected" + cath <## (pastMember <> ": author, status unknown") + where + join cath groupLink = do + cath ##> ("/c " <> groupLink) + cath <## "connection request sent!" + cath <## "#privacy: joining the group..." + cath <## "#privacy: you joined the group, pending approval" + cath <# "#privacy SimpleX-Directory> Captcha is generated by SimpleX Directory service." + cath <## "" + cath <## "Send captcha text to join the group privacy." + dropStrPrefix "#privacy SimpleX-Directory> " . dropTime <$> getTermLine cath + sendCaptcha cath captcha = do + cath #> ("#privacy " <> captcha) + cath <# ("#privacy SimpleX-Directory!> > cath " <> captcha) + cath <## " Correct, you joined the group privacy" + cath <## "#privacy: you joined the group" + +testRestoreDirectory :: HasCallStack => TestParams -> IO () +testRestoreDirectory ps = do + testListUserGroups ps + restoreDirectoryService ps 3 3 $ \superUser _dsLink -> + withTestChat ps "bob" $ \bob -> + withTestChat ps "cath" $ \cath -> do bob <## "2 contacts connected (use /cs for the list)" bob - <### [ "#privacy (Privacy): connected to server(s)", - "#security (Security): connected to server(s)" + <### [ "#privacy: connected to server(s)", + "#security: connected to server(s)" ] cath <## "2 contacts connected (use /cs for the list)" cath - <### [ "#privacy (Privacy): connected to server(s)", - "#anonymity (Anonymity): connected to server(s)" + <### [ "#privacy: connected to server(s)", + "#anonymity: connected to server(s)" ] listGroups superUser bob cath groupFoundN 3 bob "privacy" groupFound bob "security" groupFoundN 3 cath "privacy" - groupFound cath "security" + cath #> "@SimpleX-Directory security" + groupFoundN' 2 cath "security" + +testCaptcha :: HasCallStack => TestParams -> IO () +testCaptcha _ps = do + let captcha = "23456789ABCDEFGHIJKLMNOPQRSTUVWXYZabdefghijkmnpqrty" + matchCaptchaStr captcha captcha `shouldBe` True + matchCaptchaStr captcha "23456789ABcDEFGH1JKLMNoPQRsTuvwxYzabdefghijkmnpqrty" `shouldBe` True + matchCaptchaStr "23456789ABcDEFGH1JKLMNoPQRsTuvwxYzabdefghijkmnpqrty" captcha `shouldBe` True + matchCaptchaStr "OOIICPSUVWXZ" "OOIICPSUVWXZ" `shouldBe` True + matchCaptchaStr "OOIICPSUVWXZ" "0o1lcpsuvwxz" `shouldBe` True + matchCaptchaStr "0o1lcpsuvwxz" "OOIICPSUVWXZ" `shouldBe` True + matchCaptchaStr "OOIICPSUVWXZ" "" `shouldBe` False + matchCaptchaStr "OOIICPSUVWXZ" "0o1lcpsuvwx" `shouldBe` False + matchCaptchaStr "OOIICPSUVWXZ" "0o1lcpsuvwxzz" `shouldBe` False listGroups :: HasCallStack => TestCC -> TestCC -> TestCC -> IO () listGroups superUser bob cath = do - bob #> "@SimpleX-Directory /list" - bob <# "SimpleX-Directory> > /list" - bob <## " 2 registered group(s)" - bob <# "SimpleX-Directory> 1. privacy (Privacy)" - bob <## "Welcome message:" - bob <##. "Link to join the group privacy: " - bob <## "3 members" - bob <## "Status: active" - bob <# "SimpleX-Directory> 2. security (Security)" - bob <## "Welcome message:" - bob <##. "Link to join the group security: " - bob <## "2 members" - bob <## "Status: active" - cath #> "@SimpleX-Directory /list" - cath <# "SimpleX-Directory> > /list" - cath <## " 1 registered group(s)" - cath <# "SimpleX-Directory> 1. anonymity (Anonymity)" - cath <## "Welcome message:" - cath <##. "Link to join the group anonymity: " - cath <## "2 members" - cath <## "Status: suspended because roles changed" + sendListCommand bob 2 + groupListing bob 1 "privacy" "Privacy" 3 "active" + groupListing bob 2 "security" "Security" 2 "active" + sendListCommand cath 1 + groupListing cath 1 "anonymity" "Anonymity" 2 "suspended because roles changed" -- superuser lists all groups bob #> "@SimpleX-Directory /last" bob <# "SimpleX-Directory> > /last" @@ -841,34 +1178,42 @@ listGroups superUser bob cath = do superUser #> "@SimpleX-Directory /last" superUser <# "SimpleX-Directory> > /last" superUser <## " 3 registered group(s)" - superUser <# "SimpleX-Directory> 1. privacy (Privacy)" - superUser <## "Welcome message:" - superUser <##. "Link to join the group privacy: " - superUser <## "Owner: bob" - superUser <## "3 members" - superUser <## "Status: active" - superUser <# "SimpleX-Directory> 2. security (Security)" - superUser <## "Welcome message:" - superUser <##. "Link to join the group security: " - superUser <## "Owner: bob" - superUser <## "2 members" - superUser <## "Status: active" - superUser <# "SimpleX-Directory> 3. anonymity (Anonymity)" - superUser <## "Welcome message:" - superUser <##. "Link to join the group anonymity: " - superUser <## "Owner: cath" - superUser <## "2 members" - superUser <## "Status: suspended because roles changed" + memberGroupListing superUser bob 1 "privacy" "Privacy" 3 "active" + memberGroupListing superUser bob 2 "security" "Security" 2 "active" + memberGroupListing superUser cath 3 "anonymity" "Anonymity" 2 "suspended because roles changed" -- showing last 1 group superUser #> "@SimpleX-Directory /last 1" superUser <# "SimpleX-Directory> > /last 1" superUser <## " 3 registered group(s), showing the last 1" - superUser <# "SimpleX-Directory> 3. anonymity (Anonymity)" - superUser <## "Welcome message:" - superUser <##. "Link to join the group anonymity: " - superUser <## "Owner: cath" - superUser <## "2 members" - superUser <## "Status: suspended because roles changed" + memberGroupListing superUser cath 3 "anonymity" "Anonymity" 2 "suspended because roles changed" + +listUserGroup :: HasCallStack => TestCC -> String -> String -> IO () +listUserGroup u n fn = do + sendListCommand u 1 + groupListing u 1 n fn 2 "active" + +sendListCommand :: HasCallStack => TestCC -> Int -> IO () +sendListCommand u count = do + u #> "@SimpleX-Directory /list" + u <# "SimpleX-Directory> > /list" + u <## (" " <> show count <> " registered group(s)") + +groupListing :: HasCallStack => TestCC -> Int -> String -> String -> Int -> String -> IO () +groupListing u = groupListing_ u Nothing + +memberGroupListing :: HasCallStack => TestCC -> TestCC -> Int -> String -> String -> Int -> String -> IO () +memberGroupListing su owner = groupListing_ su (Just owner) + +groupListing_ :: HasCallStack => TestCC -> Maybe TestCC -> Int -> String -> String -> Int -> String -> IO () +groupListing_ su owner_ gId n fn count status = do + su <# ("SimpleX-Directory> " <> show gId <> ". " <> n <> " (" <> fn <> ")") + su <## "Welcome message:" + su <##. ("Link to join the group " <> n <> ": ") + forM_ owner_ $ \owner -> do + ownerName <- userName owner + su <## ("Owner: " <> ownerName) + su <## (show count <> " members") + su <## ("Status: " <> status) reapproveGroup :: HasCallStack => Int -> TestCC -> TestCC -> IO () reapproveGroup count superUser bob = do @@ -883,8 +1228,13 @@ reapproveGroup count superUser bob = do superUser #> "@SimpleX-Directory /approve 1:privacy 1" superUser <# "SimpleX-Directory> > /approve 1:privacy 1" superUser <## " Group approved!" - bob <# "SimpleX-Directory> The group ID 1 (privacy) is approved and listed in directory!" + bob <# "SimpleX-Directory> The group ID 1 (privacy) is approved and listed in directory - please moderate it!" bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." + bob <## "" + bob <## "Supported commands:" + bob <## "- /filter 1 - to configure anti-spam filter." + bob <## "- /role 1 - to set default member role." + bob <## "- /help commands - other commands." addCathAsOwner :: HasCallStack => TestCC -> TestCC -> IO () addCathAsOwner bob cath = do @@ -893,23 +1243,37 @@ addCathAsOwner bob cath = do joinGroup "privacy" cath bob cath <## "#privacy: member SimpleX-Directory is connected" -withDirectoryService :: HasCallStack => FilePath -> (TestCC -> String -> IO ()) -> IO () -withDirectoryService tmp = withDirectoryServiceCfg tmp testCfg +withDirectoryService :: HasCallStack => TestParams -> (TestCC -> String -> IO ()) -> IO () +withDirectoryService ps = withDirectoryServiceCfg ps testCfg -withDirectoryServiceCfg :: HasCallStack => FilePath -> ChatConfig -> (TestCC -> String -> IO ()) -> IO () -withDirectoryServiceCfg tmp cfg test = do +withDirectoryServiceCfg :: HasCallStack => TestParams -> ChatConfig -> (TestCC -> String -> IO ()) -> IO () +withDirectoryServiceCfg ps cfg = withDirectoryServiceCfgOwnersGroup ps cfg False + +withDirectoryServiceCfgOwnersGroup :: HasCallStack => TestParams -> ChatConfig -> Bool -> (TestCC -> String -> IO ()) -> IO () +withDirectoryServiceCfgOwnersGroup ps cfg createOwnersGroup test = do dsLink <- - withNewTestChatCfg tmp cfg serviceDbPrefix directoryProfile $ \ds -> - withNewTestChatCfg tmp cfg "super_user" aliceProfile $ \superUser -> do + withNewTestChatCfg ps cfg serviceDbPrefix directoryProfile $ \ds -> + withNewTestChatCfg ps cfg "super_user" aliceProfile $ \superUser -> do connectUsers ds superUser + when createOwnersGroup $ do + superUser ##> "/g owners" + superUser <## "group #owners is created" + superUser <## "to add members use /a owners <name> or /create link #owners" + superUser ##> "/a owners SimpleX-Directory admin" + superUser <## "invitation to join the group #owners sent to SimpleX-Directory" + ds <## "#owners: alice invites you to join the group as admin" + ds <## "use /j owners to accept" + ds ##> "/j owners" + ds <## "#owners: you joined the group" + superUser <## "#owners: SimpleX-Directory joined the group" ds ##> "/ad" getContactLink ds True - withDirectory tmp cfg dsLink test + withDirectoryOwnersGroup ps cfg dsLink createOwnersGroup test -restoreDirectoryService :: HasCallStack => FilePath -> Int -> Int -> (TestCC -> String -> IO ()) -> IO () -restoreDirectoryService tmp ctCount grCount test = do +restoreDirectoryService :: HasCallStack => TestParams -> Int -> Int -> (TestCC -> String -> IO ()) -> IO () +restoreDirectoryService ps ctCount grCount test = do dsLink <- - withTestChat tmp serviceDbPrefix $ \ds -> do + withTestChat ps serviceDbPrefix $ \ds -> do ds <## (show ctCount <> " contacts connected (use /cs for the list)") ds <## "Your address is active! To show: /sa" ds <## (show grCount <> " group links active") @@ -918,14 +1282,19 @@ restoreDirectoryService tmp ctCount grCount test = do dsLink <- getContactLink ds False ds <## "auto_accept on" pure dsLink - withDirectory tmp testCfg dsLink test + withDirectory ps testCfg dsLink test -withDirectory :: HasCallStack => FilePath -> ChatConfig -> String -> (TestCC -> String -> IO ()) -> IO () -withDirectory tmp cfg dsLink test = do - let opts = mkDirectoryOpts tmp [KnownContact 2 "alice"] +withDirectory :: HasCallStack => TestParams -> ChatConfig -> String -> (TestCC -> String -> IO ()) -> IO () +withDirectory ps cfg dsLink = withDirectoryOwnersGroup ps cfg dsLink False + +withDirectoryOwnersGroup :: HasCallStack => TestParams -> ChatConfig -> String -> Bool -> (TestCC -> String -> IO ()) -> IO () +withDirectoryOwnersGroup ps cfg dsLink createOwnersGroup test = do + let opts = mkDirectoryOpts ps [KnownContact 2 "alice"] $ if createOwnersGroup then Just $ KnownGroup 1 "owners" else Nothing runDirectory cfg opts $ - withTestChatCfg tmp cfg "super_user" $ \superUser -> do + withTestChatCfg ps cfg "super_user" $ \superUser -> do superUser <## "1 contacts connected (use /cs for the list)" + when createOwnersGroup $ + superUser <## "#owners: connected to server(s)" test superUser dsLink runDirectory :: ChatConfig -> DirectoryOpts -> IO () -> IO () @@ -935,7 +1304,10 @@ runDirectory cfg opts@DirectoryOpts {directoryLog} action = do threadDelay 500000 action `finally` (mapM_ hClose (directoryLogFile st) >> killThread t) where - bot st = simplexChatCore cfg (mkChatOpts opts) $ directoryService st opts + bot st = do + env <- newServiceState opts + let cfg' = cfg {chatHooks = defaultChatHooks {acceptMember = Just $ acceptMemberHook opts env}} + simplexChatCore cfg' (mkChatOpts opts) $ directoryService st opts env registerGroup :: TestCC -> TestCC -> String -> String -> IO () registerGroup su u n fn = registerGroupId su u n fn 1 1 @@ -981,7 +1353,7 @@ updateProfileWithLink u n welcomeWithLink ugId = do u <## "description changed to:" u <## welcomeWithLink u <# ("SimpleX-Directory> Thank you! The group link for ID " <> show ugId <> " (" <> n <> ") is added to the welcome message.") - u <## "You will be notified once the group is added to the directory - it may take up to 24 hours." + u <## "You will be notified once the group is added to the directory - it may take up to 48 hours." notifySuperUser :: TestCC -> TestCC -> String -> String -> String -> Int -> IO () notifySuperUser su u n fn welcomeWithLink gId = do @@ -1006,15 +1378,20 @@ approveRegistrationId su u n gId ugId = do su #> ("@SimpleX-Directory " <> approve) su <# ("SimpleX-Directory> > " <> approve) su <## " Group approved!" - u <# ("SimpleX-Directory> The group ID " <> show ugId <> " (" <> n <> ") is approved and listed in directory!") + u <# ("SimpleX-Directory> The group ID " <> show ugId <> " (" <> n <> ") is approved and listed in directory - please moderate it!") u <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." + u <## "" + u <## "Supported commands:" + u <## ("- /filter " <> show ugId <> " - to configure anti-spam filter.") + u <## ("- /role " <> show ugId <> " - to set default member role.") + u <## "- /help commands - other commands." connectVia :: TestCC -> String -> IO () u `connectVia` dsLink = do u ##> ("/c " <> dsLink) u <## "connection request sent!" - u <## "SimpleX-Directory: contact is connected" - u <# "SimpleX-Directory> Welcome to SimpleX-Directory service!" + u .<## ": contact is connected" + u .<# "> Welcome to SimpleX-Directory service!" u <## "Send a search string to find groups or /help to learn how to add groups to directory." u <## "" u <## "For example, send privacy to find groups about privacy." @@ -1055,15 +1432,25 @@ groupFound = groupFoundN 2 groupFoundN :: Int -> TestCC -> String -> IO () groupFoundN count u name = do u #> ("@SimpleX-Directory " <> name) - u <# ("SimpleX-Directory> > " <> name) + groupFoundN' count u name + +groupFoundN' :: Int -> TestCC -> String -> IO () +groupFoundN' = groupFoundN_ "" Nothing + +groupFoundN_ :: String -> Maybe Int -> Int -> TestCC -> String -> IO () +groupFoundN_ suffix shownId_ count u name = do + u <# ("SimpleX-Directory" <> suffix <> "> > " <> name) u <## " Found 1 group(s)." - u <#. ("SimpleX-Directory> " <> name) + u <#. ("SimpleX-Directory" <> suffix <> "> " <> maybe "" (\gId -> show gId <> ". ") shownId_ <> name) u <## "Welcome message:" u <##. "Link to join the group " u <## (show count <> " members") groupNotFound :: TestCC -> String -> IO () -groupNotFound u s = do - u #> ("@SimpleX-Directory " <> s) - u <# ("SimpleX-Directory> > " <> s) +groupNotFound = groupNotFound_ "" + +groupNotFound_ :: String -> TestCC -> String -> IO () +groupNotFound_ suffix u s = do + u #> ("@SimpleX-Directory" <> suffix <> " " <> s) + u <# ("SimpleX-Directory" <> suffix <> "> > " <> s) u <## " No groups found" diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index d2165db04f..e3bab5a0ec 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -1,56 +1,92 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RankNTypes #-} +{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module ChatClient where +import ChatTests.DBUtils import Control.Concurrent (forkIOWithUnmask, killThread, threadDelay) import Control.Concurrent.Async import Control.Concurrent.STM import Control.Exception (bracket, bracket_) +import Control.Logger.Simple import Control.Monad import Control.Monad.Except -import Data.ByteArray (ScrubbedBytes) +import Control.Monad.Reader import Data.Functor (($>)) import Data.List (dropWhileEnd, find) import Data.Maybe (isNothing) import qualified Data.Text as T import Network.Socket import Simplex.Chat -import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..)) +import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), defaultSimpleNetCfg) import Simplex.Chat.Core +import Simplex.Chat.Library.Commands import Simplex.Chat.Options +import Simplex.Chat.Options.DB +import Simplex.Chat.Protocol (currentChatVersion, pqEncryptionCompressionVersion) import Simplex.Chat.Store import Simplex.Chat.Store.Profiles import Simplex.Chat.Terminal import Simplex.Chat.Terminal.Output (newChatTerminal) -import Simplex.Chat.Types (AgentUserId (..), Profile, User (..)) +import Simplex.Chat.Types import Simplex.FileTransfer.Description (kb, mb) import Simplex.FileTransfer.Server (runXFTPServerBlocking) import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defaultFileExpiration) +import Simplex.FileTransfer.Transport (supportedFileServerVRange) +import Simplex.Messaging.Agent (disposeAgentClient) import Simplex.Messaging.Agent.Env.SQLite +import Simplex.Messaging.Agent.Protocol (currentSMPAgentVersion, duplexHandshakeSMPAgentVersion, pqdrSMPAgentVersion, supportedSMPAgentVRange) import Simplex.Messaging.Agent.RetryInterval -import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..)) -import Simplex.Messaging.Client (ProtocolClientConfig (..), defaultNetworkConfig) +import Simplex.Messaging.Agent.Store.Interface (closeDBStore) +import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..), MigrationError) +import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Client (ProtocolClientConfig (..)) +import Simplex.Messaging.Client.Agent (defaultSMPClientAgentConfig) +import Simplex.Messaging.Crypto.Ratchet (supportedE2EEncryptVRange) +import qualified Simplex.Messaging.Crypto.Ratchet as CR +import Simplex.Messaging.Protocol (srvHostnamesSMPClientVersion) import Simplex.Messaging.Server (runSMPServerBlocking) -import Simplex.Messaging.Server.Env.STM +import Simplex.Messaging.Server.Env.STM (AServerStoreCfg (..), ServerConfig (..), ServerStoreCfg (..), StartOptions (..), StorePaths (..), defaultMessageExpiration, defaultIdleQueueInterval, defaultNtfExpiration, defaultInactiveClientExpiration) +import Simplex.Messaging.Server.MsgStore.Types (SQSType (..), SMSType (..)) import Simplex.Messaging.Transport -import Simplex.Messaging.Transport.Server (defaultTransportServerConfig) +import Simplex.Messaging.Transport.Server (ServerCredentials (..), defaultTransportServerConfig) import Simplex.Messaging.Version +import Simplex.Messaging.Version.Internal import System.Directory (createDirectoryIfMissing, removeDirectoryRecursive) -import System.FilePath ((</>)) import qualified System.Terminal as C import System.Terminal.Internal (VirtualTerminal (..), VirtualTerminalSettings (..), withVirtualTerminal) import System.Timeout (timeout) import Test.Hspec (Expectation, HasCallStack, shouldReturn) +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (ConnectInfo (..), defaultConnectInfo) +#else +import Data.ByteArray (ScrubbedBytes) +import qualified Data.Map.Strict as M +import Simplex.Messaging.Agent.Client (agentClientStore) +import Simplex.Messaging.Agent.Store.Common (withConnection) +import System.FilePath ((</>)) +#endif -testDBPrefix :: FilePath -testDBPrefix = "tests/tmp/test" +#if defined(dbPostgres) +testDBConnstr :: String +testDBConnstr = "postgresql://test_chat_user@/test_chat_db" + +testDBConnectInfo :: ConnectInfo +testDBConnectInfo = + defaultConnectInfo { + connectUser = "test_chat_user", + connectDatabase = "test_chat_db" + } +#endif serverPort :: ServiceName serverPort = "7001" @@ -58,27 +94,13 @@ serverPort = "7001" testOpts :: ChatOpts testOpts = ChatOpts - { coreOptions = - CoreChatOpts - { dbFilePrefix = undefined, - dbKey = "", - -- dbKey = "this is a pass-phrase to encrypt the database", - smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"], - xftpServers = ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"], - networkConfig = defaultNetworkConfig, - logLevel = CLLImportant, - logConnections = False, - logServerHosts = False, - logAgent = Nothing, - logFile = Nothing, - tbqSize = 16, - highlyAvailable = False - }, - deviceName = Nothing, + { coreOptions = testCoreOpts, chatCmd = "", chatCmdDelay = 3, + chatCmdLog = CCLNone, chatServerPort = Nothing, optFilesFolder = Nothing, + optTempDirectory = Nothing, showReactions = True, allowInstantFiles = True, autoAcceptFileSize = 0, @@ -87,8 +109,43 @@ testOpts = maintenance = False } +testCoreOpts :: CoreChatOpts +testCoreOpts = + CoreChatOpts + { + dbOptions = ChatDbOpts +#if defined(dbPostgres) + { dbConnstr = testDBConnstr, + -- dbSchemaPrefix is not used in tests (except bot tests where it's redefined), + -- instead different schema prefix is passed per client so that single test database is used + dbSchemaPrefix = "", + dbPoolSize = 3, + dbCreateSchema = True +#else + { dbFilePrefix = "./simplex_v1", -- dbFilePrefix is not used in tests (except bot tests where it's redefined) + dbKey = "", -- dbKey = "this is a pass-phrase to encrypt the database", + trackQueries = DB.TQAll, + vacuumOnMigration = True +#endif + }, + smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"], + xftpServers = ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"], + simpleNetCfg = defaultSimpleNetCfg, + logLevel = CLLImportant, + logConnections = False, + logServerHosts = False, + logAgent = Nothing, + logFile = Nothing, + tbqSize = 16, + deviceName = Nothing, + highlyAvailable = False, + yesToUpMigrations = False + } + +#if !defined(dbPostgres) getTestOpts :: Bool -> ScrubbedBytes -> ChatOpts -getTestOpts maintenance dbKey = testOpts {maintenance, coreOptions = (coreOptions testOpts) {dbKey}} +getTestOpts maintenance dbKey = testOpts {maintenance, coreOptions = testCoreOpts {dbOptions = (dbOptions testCoreOpts) {dbKey}}} +#endif termSettings :: VirtualTerminalSettings termSettings = @@ -114,8 +171,15 @@ aCfg = (agentConfig defaultChatConfig) {tbqSize = 16} testAgentCfg :: AgentConfig testAgentCfg = aCfg - { reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000}, - xftpNotifyErrsOnRetry = False + { reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000} + } + +testAgentCfgSlow :: AgentConfig +testAgentCfgSlow = + testAgentCfg + { smpClientVRange = mkVersionRange (Version 1) srvHostnamesSMPClientVersion, -- v2 + smpAgentVRange = mkVersionRange duplexHandshakeSMPAgentVersion pqdrSMPAgentVersion, -- v5 + smpCfg = (smpCfg testAgentCfg) {serverVRange = mkVersionRange minClientSMPRelayVersion sendingProxySMPVersion} -- v8 } testCfg :: ChatConfig @@ -123,27 +187,39 @@ testCfg = defaultChatConfig { agentConfig = testAgentCfg, showReceipts = False, + shortLinkPresetServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7001"], testView = True, - tbqSize = 16, - xftpFileConfig = Nothing + tbqSize = 16 } +testCfgSlow :: ChatConfig +testCfgSlow = testCfg {agentConfig = testAgentCfgSlow} + testAgentCfgVPrev :: AgentConfig testAgentCfgVPrev = testAgentCfg - { smpAgentVRange = prevRange $ smpAgentVRange testAgentCfg, - smpClientVRange = prevRange $ smpClientVRange testAgentCfg, - e2eEncryptVRange = prevRange $ e2eEncryptVRange testAgentCfg, + { smpClientVRange = prevRange $ smpClientVRange testAgentCfg, + smpAgentVRange = prevRange supportedSMPAgentVRange, + e2eEncryptVRange = prevRange supportedE2EEncryptVRange, smpCfg = (smpCfg testAgentCfg) {serverVRange = prevRange $ serverVRange $ smpCfg testAgentCfg} } +testAgentCfgVNext :: AgentConfig +testAgentCfgVNext = + testAgentCfg + { smpClientVRange = nextRange $ smpClientVRange testAgentCfg, + smpAgentVRange = mkVersionRange duplexHandshakeSMPAgentVersion $ max pqdrSMPAgentVersion currentSMPAgentVersion, + e2eEncryptVRange = mkVersionRange CR.kdfX3DHE2EEncryptVersion $ max CR.pqRatchetE2EEncryptVersion CR.currentE2EEncryptVersion, + smpCfg = (smpCfg testAgentCfg) {serverVRange = nextRange $ serverVRange $ smpCfg testAgentCfg} + } + testAgentCfgV1 :: AgentConfig testAgentCfgV1 = testAgentCfg { smpClientVRange = v1Range, - smpAgentVRange = v1Range, - e2eEncryptVRange = v1Range, - smpCfg = (smpCfg testAgentCfg) {serverVRange = v1Range} + smpAgentVRange = versionToRange duplexHandshakeSMPAgentVersion, + e2eEncryptVRange = versionToRange CR.kdfX3DHE2EEncryptVersion, + smpCfg = (smpCfg testAgentCfg) {serverVRange = versionToRange minClientSMPRelayVersion} } testCfgVPrev :: ChatConfig @@ -153,6 +229,13 @@ testCfgVPrev = agentConfig = testAgentCfgVPrev } +testCfgVNext :: ChatConfig +testCfgVNext = + testCfg + { chatVRange = mkVersionRange initialChatVersion $ max pqEncryptionCompressionVersion currentChatVersion, + agentConfig = testAgentCfgVNext + } + testCfgV1 :: ChatConfig testCfgV1 = testCfg @@ -160,100 +243,126 @@ testCfgV1 = agentConfig = testAgentCfgV1 } -prevRange :: VersionRange -> VersionRange -prevRange vr = vr {maxVersion = maxVersion vr - 1} +prevRange :: VersionRange v -> VersionRange v +prevRange vr = vr {maxVersion = max (minVersion vr) (prevVersion $ maxVersion vr)} -v1Range :: VersionRange -v1Range = mkVersionRange 1 1 +nextRange :: VersionRange v -> VersionRange v +nextRange vr = vr {maxVersion = max (minVersion vr) (nextVersion $ maxVersion vr)} -testCfgCreateGroupDirect :: ChatConfig -testCfgCreateGroupDirect = - mkCfgCreateGroupDirect testCfg +v1Range :: VersionRange v +v1Range = mkVersionRange (Version 1) (Version 1) -mkCfgCreateGroupDirect :: ChatConfig -> ChatConfig -mkCfgCreateGroupDirect cfg = cfg {chatVRange = groupCreateDirectVRange} +prevVersion :: Version v -> Version v +prevVersion (Version v) = Version (v - 1) -groupCreateDirectVRange :: VersionRange -groupCreateDirectVRange = mkVersionRange 1 1 +nextVersion :: Version v -> Version v +nextVersion (Version v) = Version (v + 1) -testCfgGroupLinkViaContact :: ChatConfig -testCfgGroupLinkViaContact = - mkCfgGroupLinkViaContact testCfg - -mkCfgGroupLinkViaContact :: ChatConfig -> ChatConfig -mkCfgGroupLinkViaContact cfg = cfg {chatVRange = groupLinkViaContactVRange} - -groupLinkViaContactVRange :: VersionRange -groupLinkViaContactVRange = mkVersionRange 1 2 - -createTestChat :: FilePath -> ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC -createTestChat tmp cfg opts@ChatOpts {coreOptions = CoreChatOpts {dbKey}} dbPrefix profile = do - Right db@ChatDatabase {chatStore} <- createChatDatabase (tmp </> dbPrefix) dbKey False MCError +createTestChat :: TestParams -> ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC +createTestChat ps cfg opts@ChatOpts {coreOptions} dbPrefix profile = do + Right db@ChatDatabase {chatStore, agentStore} <- createDatabase ps coreOptions dbPrefix + insertUser agentStore Right user <- withTransaction chatStore $ \db' -> runExceptT $ createUserRecord db' (AgentUserId 1) profile True - startTestChat_ db cfg opts user + startTestChat_ ps db cfg opts user -startTestChat :: FilePath -> ChatConfig -> ChatOpts -> String -> IO TestCC -startTestChat tmp cfg opts@ChatOpts {coreOptions = CoreChatOpts {dbKey}} dbPrefix = do - Right db@ChatDatabase {chatStore} <- createChatDatabase (tmp </> dbPrefix) dbKey False MCError +startTestChat :: TestParams -> ChatConfig -> ChatOpts -> String -> IO TestCC +startTestChat ps cfg opts@ChatOpts {coreOptions} dbPrefix = do + Right db@ChatDatabase {chatStore} <- createDatabase ps coreOptions dbPrefix Just user <- find activeUser <$> withTransaction chatStore getUsers - startTestChat_ db cfg opts user + startTestChat_ ps db cfg opts user -startTestChat_ :: ChatDatabase -> ChatConfig -> ChatOpts -> User -> IO TestCC -startTestChat_ db cfg opts user = do +createDatabase :: TestParams -> CoreChatOpts -> String -> IO (Either MigrationError ChatDatabase) +#if defined(dbPostgres) +createDatabase _params CoreChatOpts {dbOptions} dbPrefix = do + createChatDatabase dbOptions {dbSchemaPrefix = "client_" <> dbPrefix} MCError + +insertUser :: DBStore -> IO () +insertUser st = withTransaction st (`DB.execute_` "INSERT INTO users DEFAULT VALUES") +#else +createDatabase TestParams {tmpPath} CoreChatOpts {dbOptions} dbPrefix = do + createChatDatabase dbOptions {dbFilePrefix = tmpPath </> dbPrefix} MCError + +insertUser :: DBStore -> IO () +insertUser st = withTransaction st (`DB.execute_` "INSERT INTO users (user_id) VALUES (1)") +#endif + +startTestChat_ :: TestParams -> ChatDatabase -> ChatConfig -> ChatOpts -> User -> IO TestCC +startTestChat_ TestParams {printOutput} db cfg opts@ChatOpts {maintenance} user = do t <- withVirtualTerminal termSettings pure ct <- newChatTerminal t opts cc <- newChatController db (Just user) cfg opts False - chatAsync <- async . runSimplexChat opts user cc $ \_u cc' -> runChatTerminal ct cc' opts - atomically . unless (maintenance opts) $ readTVar (agentAsync cc) >>= \a -> when (isNothing a) retry + void $ execChatCommand' (SetTempFolder "tests/tmp/tmp") `runReaderT` cc + chatAsync <- async $ runSimplexChat opts user cc $ \_u cc' -> runChatTerminal ct cc' opts + unless maintenance $ atomically $ readTVar (agentAsync cc) >>= \a -> when (isNothing a) retry termQ <- newTQueueIO termAsync <- async $ readTerminalOutput t termQ - pure TestCC {chatController = cc, virtualTerminal = t, chatAsync, termAsync, termQ, printOutput = False} + pure TestCC {chatController = cc, virtualTerminal = t, chatAsync, termAsync, termQ, printOutput} -stopTestChat :: TestCC -> IO () -stopTestChat TestCC {chatController = cc, chatAsync, termAsync} = do +stopTestChat :: TestParams -> TestCC -> IO () +stopTestChat ps TestCC {chatController = cc@ChatController {smpAgent, chatStore}, chatAsync, termAsync} = do stopChatController cc uninterruptibleCancel termAsync uninterruptibleCancel chatAsync + liftIO $ disposeAgentClient smpAgent +#if !defined(dbPostgres) + chatStats <- withConnection chatStore $ readTVarIO . DB.slow + atomically $ modifyTVar' (chatQueryStats ps) $ M.unionWith combineStats chatStats + agentStats <- withConnection (agentClientStore smpAgent) $ readTVarIO . DB.slow + atomically $ modifyTVar' (agentQueryStats ps) $ M.unionWith combineStats agentStats +#endif + closeDBStore chatStore threadDelay 200000 +#if !defined(dbPostgres) + where + combineStats + DB.SlowQueryStats {count, timeMax, timeAvg, errs} + DB.SlowQueryStats {count = count', timeMax = timeMax', timeAvg = timeAvg', errs = errs'} = + DB.SlowQueryStats + { count = count + count', + timeMax = max timeMax timeMax', + timeAvg = (timeAvg * count + timeAvg' * count') `div` (count + count'), + errs = M.unionWith (+) errs errs' + } +#endif -withNewTestChat :: HasCallStack => FilePath -> String -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a -withNewTestChat tmp = withNewTestChatCfgOpts tmp testCfg testOpts +withNewTestChat :: HasCallStack => TestParams -> String -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a +withNewTestChat ps = withNewTestChatCfgOpts ps testCfg testOpts -withNewTestChatV1 :: HasCallStack => FilePath -> String -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a -withNewTestChatV1 tmp = withNewTestChatCfg tmp testCfgV1 +withNewTestChatV1 :: HasCallStack => TestParams -> String -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a +withNewTestChatV1 ps = withNewTestChatCfg ps testCfgV1 -withNewTestChatCfg :: HasCallStack => FilePath -> ChatConfig -> String -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a -withNewTestChatCfg tmp cfg = withNewTestChatCfgOpts tmp cfg testOpts +withNewTestChatCfg :: HasCallStack => TestParams -> ChatConfig -> String -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a +withNewTestChatCfg ps cfg = withNewTestChatCfgOpts ps cfg testOpts -withNewTestChatOpts :: HasCallStack => FilePath -> ChatOpts -> String -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a -withNewTestChatOpts tmp = withNewTestChatCfgOpts tmp testCfg +withNewTestChatOpts :: HasCallStack => TestParams -> ChatOpts -> String -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a +withNewTestChatOpts ps = withNewTestChatCfgOpts ps testCfg -withNewTestChatCfgOpts :: HasCallStack => FilePath -> ChatConfig -> ChatOpts -> String -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a -withNewTestChatCfgOpts tmp cfg opts dbPrefix profile runTest = +withNewTestChatCfgOpts :: HasCallStack => TestParams -> ChatConfig -> ChatOpts -> String -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a +withNewTestChatCfgOpts ps cfg opts dbPrefix profile runTest = bracket - (createTestChat tmp cfg opts dbPrefix profile) - stopTestChat + (createTestChat ps cfg opts dbPrefix profile) + (stopTestChat ps) (\cc -> runTest cc >>= ((cc <// 100000) $>)) -withTestChatV1 :: HasCallStack => FilePath -> String -> (HasCallStack => TestCC -> IO a) -> IO a -withTestChatV1 tmp = withTestChatCfg tmp testCfgV1 +withTestChatV1 :: HasCallStack => TestParams -> String -> (HasCallStack => TestCC -> IO a) -> IO a +withTestChatV1 ps = withTestChatCfg ps testCfgV1 -withTestChat :: HasCallStack => FilePath -> String -> (HasCallStack => TestCC -> IO a) -> IO a -withTestChat tmp = withTestChatCfgOpts tmp testCfg testOpts +withTestChat :: HasCallStack => TestParams -> String -> (HasCallStack => TestCC -> IO a) -> IO a +withTestChat ps = withTestChatCfgOpts ps testCfg testOpts -withTestChatCfg :: HasCallStack => FilePath -> ChatConfig -> String -> (HasCallStack => TestCC -> IO a) -> IO a -withTestChatCfg tmp cfg = withTestChatCfgOpts tmp cfg testOpts +withTestChatCfg :: HasCallStack => TestParams -> ChatConfig -> String -> (HasCallStack => TestCC -> IO a) -> IO a +withTestChatCfg ps cfg = withTestChatCfgOpts ps cfg testOpts -withTestChatOpts :: HasCallStack => FilePath -> ChatOpts -> String -> (HasCallStack => TestCC -> IO a) -> IO a -withTestChatOpts tmp = withTestChatCfgOpts tmp testCfg +withTestChatOpts :: HasCallStack => TestParams -> ChatOpts -> String -> (HasCallStack => TestCC -> IO a) -> IO a +withTestChatOpts ps = withTestChatCfgOpts ps testCfg -withTestChatCfgOpts :: HasCallStack => FilePath -> ChatConfig -> ChatOpts -> String -> (HasCallStack => TestCC -> IO a) -> IO a -withTestChatCfgOpts tmp cfg opts dbPrefix = bracket (startTestChat tmp cfg opts dbPrefix) (\cc -> cc <// 100000 >> stopTestChat cc) +withTestChatCfgOpts :: HasCallStack => TestParams -> ChatConfig -> ChatOpts -> String -> (HasCallStack => TestCC -> IO a) -> IO a +withTestChatCfgOpts ps cfg opts dbPrefix = bracket (startTestChat ps cfg opts dbPrefix) (\cc -> cc <// 100000 >> stopTestChat ps cc) --- enable output for specific chat controller, use like this: --- withNewTestChat tmp "alice" aliceProfile $ \a -> withTestOutput a $ \alice -> do ... -withTestOutput :: HasCallStack => TestCC -> (HasCallStack => TestCC -> IO a) -> IO a -withTestOutput cc runTest = runTest cc {printOutput = True} +-- enable output for specific test. +-- usage: withTestOutput $ testChat2 aliceProfile bobProfile $ \alice bob -> do ... +withTestOutput :: HasCallStack => (HasCallStack => TestParams -> IO ()) -> TestParams -> IO () +withTestOutput test ps = test ps {printOutput = True} readTerminalOutput :: VirtualTerminal -> TQueue String -> IO () readTerminalOutput t termQ = do @@ -284,68 +393,79 @@ withTmpFiles = (createDirectoryIfMissing False "tests/tmp") (removeDirectoryRecursive "tests/tmp") -testChatN :: HasCallStack => ChatConfig -> ChatOpts -> [Profile] -> (HasCallStack => [TestCC] -> IO ()) -> FilePath -> IO () -testChatN cfg opts ps test tmp = do - tcs <- getTestCCs (zip ps [1 ..]) [] - test tcs - concurrentlyN_ $ map (<// 100000) tcs - concurrentlyN_ $ map stopTestChat tcs +testChatN :: HasCallStack => ChatConfig -> ChatOpts -> [Profile] -> (HasCallStack => [TestCC] -> IO ()) -> TestParams -> IO () +testChatN cfg opts ps test params = + bracket (getTestCCs $ zip ps [1 ..]) endTests test where - getTestCCs :: [(Profile, Int)] -> [TestCC] -> IO [TestCC] - getTestCCs [] tcs = pure tcs - getTestCCs ((p, db) : envs') tcs = (:) <$> createTestChat tmp cfg opts (show db) p <*> getTestCCs envs' tcs + getTestCCs :: [(Profile, Int)] -> IO [TestCC] + getTestCCs [] = pure [] + getTestCCs ((p, db) : envs') = (:) <$> createTestChat params cfg opts (show db) p <*> getTestCCs envs' + endTests tcs = do + mapConcurrently_ (<// 100000) tcs + mapConcurrently_ (stopTestChat params) tcs (<//) :: HasCallStack => TestCC -> Int -> Expectation (<//) cc t = timeout t (getTermLine cc) `shouldReturn` Nothing getTermLine :: HasCallStack => TestCC -> IO String -getTermLine cc = +getTermLine cc@TestCC {printOutput} = 5000000 `timeout` atomically (readTQueue $ termQ cc) >>= \case Just s -> do -- remove condition to always echo virtual terminal -- when True $ do - when (printOutput cc) $ do + when printOutput $ do name <- userName cc putStrLn $ name <> ": " <> s pure s _ -> error "no output for 5 seconds" userName :: TestCC -> IO [Char] -userName (TestCC ChatController {currentUser} _ _ _ _ _) = maybe "no current user" (T.unpack . localDisplayName) <$> readTVarIO currentUser +userName (TestCC ChatController {currentUser} _ _ _ _ _) = + maybe "no current user" (\User {localDisplayName} -> T.unpack localDisplayName) <$> readTVarIO currentUser -testChat2 :: HasCallStack => Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> IO ()) -> FilePath -> IO () +testChat :: HasCallStack => Profile -> (HasCallStack => TestCC -> IO ()) -> TestParams -> IO () +testChat = testChatCfgOpts testCfg testOpts + +testChatCfgOpts :: HasCallStack => ChatConfig -> ChatOpts -> Profile -> (HasCallStack => TestCC -> IO ()) -> TestParams -> IO () +testChatCfgOpts cfg opts p test = testChatN cfg opts [p] test_ + where + test_ :: HasCallStack => [TestCC] -> IO () + test_ [tc] = test tc + test_ _ = error "expected 1 chat client" + +testChat2 :: HasCallStack => Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> IO ()) -> TestParams -> IO () testChat2 = testChatCfgOpts2 testCfg testOpts -testChatCfg2 :: HasCallStack => ChatConfig -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> IO ()) -> FilePath -> IO () +testChatCfg2 :: HasCallStack => ChatConfig -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> IO ()) -> TestParams -> IO () testChatCfg2 cfg = testChatCfgOpts2 cfg testOpts -testChatOpts2 :: HasCallStack => ChatOpts -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> IO ()) -> FilePath -> IO () +testChatOpts2 :: HasCallStack => ChatOpts -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> IO ()) -> TestParams -> IO () testChatOpts2 = testChatCfgOpts2 testCfg -testChatCfgOpts2 :: HasCallStack => ChatConfig -> ChatOpts -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> IO ()) -> FilePath -> IO () +testChatCfgOpts2 :: HasCallStack => ChatConfig -> ChatOpts -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> IO ()) -> TestParams -> IO () testChatCfgOpts2 cfg opts p1 p2 test = testChatN cfg opts [p1, p2] test_ where test_ :: HasCallStack => [TestCC] -> IO () test_ [tc1, tc2] = test tc1 tc2 test_ _ = error "expected 2 chat clients" -testChat3 :: HasCallStack => Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO () +testChat3 :: HasCallStack => Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> TestParams -> IO () testChat3 = testChatCfgOpts3 testCfg testOpts -testChatCfg3 :: HasCallStack => ChatConfig -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO () +testChatCfg3 :: HasCallStack => ChatConfig -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> TestParams -> IO () testChatCfg3 cfg = testChatCfgOpts3 cfg testOpts -testChatCfgOpts3 :: HasCallStack => ChatConfig -> ChatOpts -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO () +testChatCfgOpts3 :: HasCallStack => ChatConfig -> ChatOpts -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> TestParams -> IO () testChatCfgOpts3 cfg opts p1 p2 p3 test = testChatN cfg opts [p1, p2, p3] test_ where test_ :: HasCallStack => [TestCC] -> IO () test_ [tc1, tc2, tc3] = test tc1 tc2 tc3 test_ _ = error "expected 3 chat clients" -testChat4 :: HasCallStack => Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO () +testChat4 :: HasCallStack => Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> TestParams -> IO () testChat4 = testChatCfg4 testCfg -testChatCfg4 :: HasCallStack => ChatConfig -> Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO () +testChatCfg4 :: HasCallStack => ChatConfig -> Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> TestParams -> IO () testChatCfg4 cfg p1 p2 p3 p4 test = testChatN cfg testOpts [p1, p2, p3, p4] test_ where test_ :: HasCallStack => [TestCC] -> IO () @@ -355,37 +475,62 @@ testChatCfg4 cfg p1 p2 p3 p4 test = testChatN cfg testOpts [p1, p2, p3, p4] test concurrentlyN_ :: [IO a] -> IO () concurrentlyN_ = mapConcurrently_ id -serverCfg :: ServerConfig -serverCfg = +smpServerCfg :: ServerConfig +smpServerCfg = ServerConfig - { transports = [(serverPort, transport @TLS)], + { transports = [(serverPort, transport @TLS, False)], tbqSize = 1, - -- serverTbqSize = 1, msgQueueQuota = 16, - queueIdBytes = 12, + maxJournalMsgCount = 24, + maxJournalStateLines = 4, + queueIdBytes = 24, msgIdBytes = 6, - storeLogFile = Nothing, - storeMsgsFile = Nothing, + serverStoreCfg = ASSCfg SQSMemory SMSMemory $ SSCMemory Nothing, + storeNtfsFile = Nothing, allowNewQueues = True, -- server password is disabled as otherwise v1 tests fail newQueueBasicAuth = Nothing, -- Just "server_password", + controlPortUserAuth = Nothing, + controlPortAdminAuth = Nothing, messageExpiration = Just defaultMessageExpiration, + expireMessagesOnStart = False, + idleQueueInterval = defaultIdleQueueInterval, + notificationExpiration = defaultNtfExpiration, inactiveClientExpiration = Just defaultInactiveClientExpiration, - caCertificateFile = "tests/fixtures/tls/ca.crt", - privateKeyFile = "tests/fixtures/tls/server.key", - certificateFile = "tests/fixtures/tls/server.crt", + smpCredentials = + ServerCredentials + { caCertificateFile = Just "tests/fixtures/tls/ca.crt", + privateKeyFile = "tests/fixtures/tls/server.key", + certificateFile = "tests/fixtures/tls/server.crt" + }, + httpCredentials = Nothing, logStatsInterval = Nothing, logStatsStartTime = 0, serverStatsLogFile = "tests/smp-server-stats.daily.log", serverStatsBackupFile = Nothing, - smpServerVRange = supportedSMPServerVRange, + prometheusInterval = Nothing, + prometheusMetricsFile = "tests/smp-server-metrics.txt", + pendingENDInterval = 500000, + ntfDeliveryInterval = 200000, + smpServerVRange = supportedServerSMPRelayVRange, transportConfig = defaultTransportServerConfig, smpHandshakeTimeout = 1000000, - controlPort = Nothing + controlPort = Nothing, + smpAgentCfg = defaultSMPClientAgentConfig, + allowSMPProxy = True, + serverClientConcurrency = 16, + information = Nothing, + startOptions = StartOptions {maintenance = False, logLevel = LogError, compactLog = False, skipWarnings = False, confirmMigrations = MCYesUp} } +persistentServerStoreCfg :: FilePath -> AServerStoreCfg +persistentServerStoreCfg tmp = ASSCfg SQSMemory SMSMemory $ SSCMemory $ Just StorePaths {storeLogFile = tmp <> "/smp-server-store.log", storeMsgsFile = Just $ tmp <> "/smp-server-messages.log"} + withSmpServer :: IO () -> IO () -withSmpServer = serverBracket (`runSMPServerBlocking` serverCfg) +withSmpServer = withSmpServer' smpServerCfg + +withSmpServer' :: ServerConfig -> IO a -> IO a +withSmpServer' cfg = serverBracket (\started -> runSMPServerBlocking started cfg Nothing) xftpTestPort :: ServiceName xftpTestPort = "7002" @@ -401,19 +546,28 @@ xftpServerConfig = storeLogFile = Just "tests/tmp/xftp-server-store.log", filesPath = xftpServerFiles, fileSizeQuota = Nothing, - allowedChunkSizes = [kb 128, kb 256, mb 1, mb 4], + allowedChunkSizes = [kb 64, kb 128, kb 256, mb 1, mb 4], allowNewFiles = True, newFileBasicAuth = Nothing, + controlPortUserAuth = Nothing, + controlPortAdminAuth = Nothing, fileExpiration = Just defaultFileExpiration, + fileTimeout = 10000000, inactiveClientExpiration = Just defaultInactiveClientExpiration, - caCertificateFile = "tests/fixtures/tls/ca.crt", - privateKeyFile = "tests/fixtures/tls/server.key", - certificateFile = "tests/fixtures/tls/server.crt", + xftpCredentials = + ServerCredentials + { caCertificateFile = Just "tests/fixtures/tls/ca.crt", + privateKeyFile = "tests/fixtures/tls/server.key", + certificateFile = "tests/fixtures/tls/server.crt" + }, + xftpServerVRange = supportedFileServerVRange, logStatsInterval = Nothing, logStatsStartTime = 0, serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log", serverStatsBackupFile = Nothing, - transportConfig = defaultTransportServerConfig + controlPort = Nothing, + transportConfig = defaultTransportServerConfig, + responseDelay = 0 } withXFTPServer :: IO () -> IO () @@ -424,15 +578,15 @@ withXFTPServer' cfg = serverBracket ( \started -> do createDirectoryIfMissing False xftpServerFiles - runXFTPServerBlocking started cfg + runXFTPServerBlocking started cfg Nothing ) -serverBracket :: (TMVar Bool -> IO ()) -> IO () -> IO () +serverBracket :: (TMVar Bool -> IO ()) -> IO a -> IO a serverBracket server f = do started <- newEmptyTMVarIO bracket (forkIOWithUnmask ($ server started)) - (\t -> killThread t >> waitFor started "stop") + (\t -> killThread t >> waitFor started "stop" >> threadDelay 100000) (\_ -> waitFor started "start" >> f) where waitFor started s = diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index a00274a541..20fccf6c64 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -1,16 +1,21 @@ module ChatTests where import ChatTests.ChatList +import ChatTests.DBUtils import ChatTests.Direct import ChatTests.Files +import ChatTests.Forward import ChatTests.Groups +import ChatTests.Local import ChatTests.Profiles -import Test.Hspec +import Test.Hspec hiding (it) -chatTests :: SpecWith FilePath +chatTests :: SpecWith TestParams chatTests = do describe "direct tests" chatDirectTests + describe "forward tests" chatForwardTests describe "group tests" chatGroupTests + describe "local chats tests" chatLocalChatsTests describe "file tests" chatFileTests describe "profile tests" chatProfileTests describe "chat list pagination tests" chatListTests diff --git a/tests/ChatTests/ChatList.hs b/tests/ChatTests/ChatList.hs index f42067c7e5..dcd18a9818 100644 --- a/tests/ChatTests/ChatList.hs +++ b/tests/ChatTests/ChatList.hs @@ -1,12 +1,13 @@ module ChatTests.ChatList where import ChatClient +import ChatTests.DBUtils import ChatTests.Utils import Data.Time.Clock (getCurrentTime) import Data.Time.Format.ISO8601 (iso8601Show) -import Test.Hspec +import Test.Hspec hiding (it) -chatListTests :: SpecWith FilePath +chatListTests :: SpecWith TestParams chatListTests = do it "get last chats" testPaginationLast it "get chats before/after timestamp" testPaginationTs @@ -16,7 +17,7 @@ chatListTests = do it "filter favorite or unread" testFilterFavoriteOrUnread it "sort and filter chats of all types" testPaginationAllChatTypes -testPaginationLast :: HasCallStack => FilePath -> IO () +testPaginationLast :: HasCallStack => TestParams -> IO () testPaginationLast = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -32,7 +33,7 @@ testPaginationLast = alice <# "bob> hey" alice <# "@cath hey" -testPaginationTs :: HasCallStack => FilePath -> IO () +testPaginationTs :: HasCallStack => TestParams -> IO () testPaginationTs = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -59,7 +60,7 @@ getChats_ :: HasCallStack => TestCC -> String -> [(String, String)] -> Expectati getChats_ cc query expected = do cc #$> ("/_get chats 1 pcc=on " <> query, chats, expected) -testFilterSearch :: HasCallStack => FilePath -> IO () +testFilterSearch :: HasCallStack => TestParams -> IO () testFilterSearch = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -75,7 +76,7 @@ testFilterSearch = getChats_ alice (query "bob") [("@bob", "hey")] getChats_ alice (query "Bob") [("@bob", "hey")] -testFilterFavorite :: HasCallStack => FilePath -> IO () +testFilterFavorite :: HasCallStack => TestParams -> IO () testFilterFavorite = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -99,7 +100,7 @@ testFilterFavorite = alice <## "ok" getChats_ alice query [("@bob", "hey")] -testFilterUnread :: HasCallStack => FilePath -> IO () +testFilterUnread :: HasCallStack => TestParams -> IO () testFilterUnread = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -123,7 +124,7 @@ testFilterUnread = alice <## "ok" getChats_ alice query [("@bob", "hey")] -testFilterFavoriteOrUnread :: HasCallStack => FilePath -> IO () +testFilterFavoriteOrUnread :: HasCallStack => TestParams -> IO () testFilterFavoriteOrUnread = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -154,7 +155,7 @@ testFilterFavoriteOrUnread = alice <## "ok" getChats_ alice query [("@cath", "hey"), ("@bob", "hey")] -testPaginationAllChatTypes :: HasCallStack => FilePath -> IO () +testPaginationAllChatTypes :: HasCallStack => TestParams -> IO () testPaginationAllChatTypes = testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do @@ -191,17 +192,23 @@ testPaginationAllChatTypes = connectUsers alice dan alice <##> dan - ts6 <- iso8601Show <$> getCurrentTime + _ts6 <- iso8601Show <$> getCurrentTime - getChats_ alice "count=10" [("@dan", "hey"), ("#team", ""), (":3", ""), ("<@cath", ""), ("@bob", "hey")] - getChats_ alice "count=3" [("@dan", "hey"), ("#team", ""), (":3", "")] + -- \* (notes) + createCCNoteFolder alice + alice >* "psst" + + ts7 <- iso8601Show <$> getCurrentTime + + getChats_ alice "count=10" [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", ""), ("<@cath", ""), ("@bob", "hey")] + getChats_ alice "count=3" [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on")] getChats_ alice ("after=" <> ts2 <> " count=2") [(":3", ""), ("<@cath", "")] - getChats_ alice ("before=" <> ts5 <> " count=2") [("#team", ""), (":3", "")] - getChats_ alice ("after=" <> ts3 <> " count=10") [("@dan", "hey"), ("#team", ""), (":3", "")] + 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=" <> ts4 <> " count=10") [(":3", ""), ("<@cath", ""), ("@bob", "hey")] - getChats_ alice ("after=" <> ts1 <> " count=10") [("@dan", "hey"), ("#team", ""), (":3", ""), ("<@cath", ""), ("@bob", "hey")] - getChats_ alice ("before=" <> ts6 <> " count=10") [("@dan", "hey"), ("#team", ""), (":3", ""), ("<@cath", ""), ("@bob", "hey")] - getChats_ alice ("after=" <> ts6 <> " count=10") [] + getChats_ alice ("after=" <> ts1 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", ""), ("<@cath", ""), ("@bob", "hey")] + getChats_ alice ("before=" <> ts7 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", ""), ("<@cath", ""), ("@bob", "hey")] + getChats_ alice ("after=" <> ts7 <> " count=10") [] getChats_ alice ("before=" <> ts1 <> " count=10") [] let queryFavorite = "{\"type\": \"filters\", \"favorite\": true, \"unread\": false}" @@ -212,11 +219,11 @@ testPaginationAllChatTypes = alice ##> "/_settings #1 {\"enableNtfs\":\"all\",\"favorite\":true}" alice <## "ok" - getChats_ alice queryFavorite [("#team", ""), ("@bob", "hey")] + getChats_ alice queryFavorite [("#team", "Recent history: on"), ("@bob", "hey")] getChats_ alice ("before=" <> ts4 <> " count=1 " <> queryFavorite) [("@bob", "hey")] - getChats_ alice ("before=" <> ts5 <> " count=1 " <> queryFavorite) [("#team", "")] + getChats_ alice ("before=" <> ts5 <> " count=1 " <> queryFavorite) [("#team", "Recent history: on")] getChats_ alice ("after=" <> ts1 <> " count=1 " <> queryFavorite) [("@bob", "hey")] - getChats_ alice ("after=" <> ts4 <> " count=1 " <> queryFavorite) [("#team", "")] + getChats_ alice ("after=" <> ts4 <> " count=1 " <> queryFavorite) [("#team", "Recent history: on")] let queryUnread = "{\"type\": \"filters\", \"favorite\": false, \"unread\": true}" diff --git a/tests/ChatTests/DBUtils.hs b/tests/ChatTests/DBUtils.hs new file mode 100644 index 0000000000..f6ac2284da --- /dev/null +++ b/tests/ChatTests/DBUtils.hs @@ -0,0 +1,15 @@ +{-# LANGUAGE CPP #-} + +module ChatTests.DBUtils + +#if defined(dbPostgres) + ( module ChatTests.DBUtils.Postgres, + ) + where +import ChatTests.DBUtils.Postgres +#else + ( module ChatTests.DBUtils.SQLite, + ) + where +import ChatTests.DBUtils.SQLite +#endif diff --git a/tests/ChatTests/DBUtils/Postgres.hs b/tests/ChatTests/DBUtils/Postgres.hs new file mode 100644 index 0000000000..2b160379bb --- /dev/null +++ b/tests/ChatTests/DBUtils/Postgres.hs @@ -0,0 +1,6 @@ +module ChatTests.DBUtils.Postgres where + +data TestParams = TestParams + { tmpPath :: FilePath, + printOutput :: Bool + } diff --git a/tests/ChatTests/DBUtils/SQLite.hs b/tests/ChatTests/DBUtils/SQLite.hs new file mode 100644 index 0000000000..2de94882cd --- /dev/null +++ b/tests/ChatTests/DBUtils/SQLite.hs @@ -0,0 +1,12 @@ +module ChatTests.DBUtils.SQLite where + +import Database.SQLite.Simple (Query) +import Simplex.Messaging.Agent.Store.SQLite.DB +import Simplex.Messaging.TMap (TMap) + +data TestParams = TestParams + { tmpPath :: FilePath, + printOutput :: Bool, + chatQueryStats :: TMap Query SlowQueryStats, + agentQueryStats :: TMap Query SlowQueryStats + } diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 64fa6ff3bf..817e16113f 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -1,11 +1,18 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE PostfixOperators #-} {-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} module ChatTests.Direct where import ChatClient +import ChatTests.DBUtils import ChatTests.Utils import Control.Concurrent (threadDelay) import Control.Concurrent.Async (concurrently_) @@ -14,33 +21,62 @@ import Data.Aeson (ToJSON) import qualified Data.Aeson as J import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB +import Data.List (intercalate) +import qualified Data.Text as T +import Simplex.Chat.AppSettings (defaultAppSettings) +import qualified Simplex.Chat.AppSettings as AS import Simplex.Chat.Call -import Simplex.Chat.Controller (ChatConfig (..)) -import Simplex.Chat.Options (ChatOpts (..)) +import Simplex.Chat.Controller (ChatConfig (..), PresetServers (..)) +import Simplex.Chat.Messages (ChatItemId) +import Simplex.Chat.Options import Simplex.Chat.Protocol (supportedChatVRange) -import Simplex.Chat.Store (agentStoreFile, chatStoreFile) -import Simplex.Chat.Types (authErrDisableCount, sameVerificationCode, verificationCode) +import Simplex.Chat.Types (VersionRangeChat, authErrDisableCount, sameVerificationCode, verificationCode, pattern VersionChat) +import Simplex.Messaging.Agent.Env.SQLite +import Simplex.Messaging.Agent.RetryInterval +import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Server.Env.STM hiding (subscriptions) +import Simplex.Messaging.Transport +import Simplex.Messaging.Util (safeDecodeUtf8) import Simplex.Messaging.Version import System.Directory (copyFile, doesDirectoryExist, doesFileExist) +import Test.Hspec hiding (it) +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (Only (..)) +#else +import Database.SQLite.Simple (Only (..)) +import Simplex.Chat.Options.DB import System.FilePath ((</>)) -import Test.Hspec +#endif -chatDirectTests :: SpecWith FilePath +chatDirectTests :: SpecWith TestParams chatDirectTests = do describe "direct messages" $ do describe "add contact and send/receive messages" testAddContact + it "retry connecting via the same link" testRetryConnecting + xit'' "retry connecting via the same link with client timeout" testRetryConnectingClientTimeout + it "mark multiple messages as read" testMarkReadDirect it "clear chat with contact" testContactClear it "deleting contact deletes profile" testDeleteContactDeletesProfile - it "unused contact is deleted silently" testDeleteUnusedContactSilent + it "delete contact keeping conversation" testDeleteContactKeepConversation + it "delete conversation keeping contact" testDeleteConversationKeepContact it "direct message quoted replies" testDirectMessageQuotedReply it "direct message update" testDirectMessageUpdate it "direct message edit history" testDirectMessageEditHistory it "direct message delete" testDirectMessageDelete + it "direct message delete multiple" testDirectMessageDeleteMultiple + it "direct message delete multiple (many chat batches)" testDirectMessageDeleteMultipleManyBatches it "direct live message" testDirectLiveMessage it "direct timed message" testDirectTimedMessage it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact it "should send multiline message" testMultilineMessage + it "send large message" testLargeMessage + it "initial chat pagination" testChatPaginationInitial + describe "batch send messages" $ do + it "send multiple messages api" testSendMulti + it "send multiple timed messages" testSendMultiTimed + it "send multiple messages, including quote" testSendMultiWithQuote + it "send multiple messages (many chat batches)" testSendMultiManyBatches describe "duplicate contacts" $ do it "duplicate contacts are separate (contacts don't merge)" testDuplicateContactsSeparate it "new contact is separate with multiple duplicate contacts (contacts don't merge)" testDuplicateContactsMultipleSeparate @@ -54,18 +90,33 @@ chatDirectTests = do describe "XFTP servers" $ do it "get and set XFTP servers" testGetSetXFTPServers it "test XFTP server connection" testTestXFTPServer + describe "operators and usage conditions" $ do + it "get and enable operators, accept conditions" testOperators describe "async connection handshake" $ do - it "connect when initiating client goes offline" testAsyncInitiatingOffline - it "connect when accepting client goes offline" testAsyncAcceptingOffline + describe "connect when initiating client goes offline" $ do + it "curr" $ testAsyncInitiatingOffline testCfg testCfg + it "v5" $ testAsyncInitiatingOffline testCfgSlow testCfgSlow + it "v5/curr" $ testAsyncInitiatingOffline testCfgSlow testCfg + it "curr/v5" $ testAsyncInitiatingOffline testCfg testCfgSlow + describe "connect when accepting client goes offline" $ do + it "curr" $ testAsyncAcceptingOffline testCfg testCfg + it "v5" $ testAsyncAcceptingOffline testCfgSlow testCfgSlow + it "v5/curr" $ testAsyncAcceptingOffline testCfgSlow testCfg + it "curr/v5" $ testAsyncAcceptingOffline testCfg testCfgSlow describe "connect, fully asynchronous (when clients are never simultaneously online)" $ do + it "curr" testFullAsyncFast -- fails in CI - xit'' "v2" testFullAsync + xit'' "v5" $ testFullAsyncSlow testCfgSlow testCfgSlow + xit'' "v5/curr" $ testFullAsyncSlow testCfgSlow testCfg + xit'' "curr/v5" $ testFullAsyncSlow testCfg testCfgSlow describe "webrtc calls api" $ do it "negotiate call" testNegotiateCall +#if !defined(dbPostgres) describe "maintenance mode" $ do it "start/stop/export/import chat" testMaintenanceMode it "export/import chat with files" testMaintenanceModeWithFiles it "encrypt/decrypt database" testDatabaseEncryption +#endif describe "coordination between app and NSE" $ do it "should not subscribe in NSE and subscribe in the app" testSubscribeAppNSE describe "mute/unmute messages" $ do @@ -75,7 +126,6 @@ chatDirectTests = do it "create second user" testCreateSecondUser it "multiple users subscribe and receive messages after restart" testUsersSubscribeAfterRestart it "both users have contact link" testMultipleUserAddresses - it "create user with default servers" testCreateUserDefaultServers it "create user with same servers" testCreateUserSameServers it "delete user" testDeleteUser it "users have different chat item TTL configuration, chat items expire" testUsersDifferentCIExpirationTTL @@ -83,9 +133,11 @@ chatDirectTests = do it "chat items only expire for users who configured expiration" testEnableCIExpirationOnlyForOneUser it "disabling chat item expiration doesn't disable it for other users" testDisableCIExpirationOnlyForOneUser it "both users have configured timed messages with contacts, messages expire, restart" testUsersTimedMessages - it "user profile privacy: hide profiles and notificaitons" testUserPrivacy - describe "chat item expiration" $ do - it "set chat item TTL" testSetChatItemTTL + it "user profile privacy: hide profiles and notifications" testUserPrivacy + it "set direct chat expiration TTL" testSetDirectChatTTL + describe "settings" $ do + it "set chat item expiration TTL" testSetChatItemTTL + it "save/get app settings" testAppSettings describe "connection switch" $ do it "switch contact to a different queue" testSwitchContact it "stop switching contact to a different queue" testAbortSwitchContact @@ -98,10 +150,13 @@ chatDirectTests = do sameVerificationCode "123 456 789" "12345 6789" `shouldBe` True it "mark contact verified" testMarkContactVerified it "mark group member verified" testMarkGroupMemberVerified +#if !defined(dbPostgres) + -- TODO [postgres] restore from outdated db backup (same as in agent) describe "message errors" $ do it "show message decryption error" testMsgDecryptError it "should report ratchet de-synchronization, synchronize ratchets" testSyncRatchet it "synchronize ratchets, reset connection code" testSyncRatchetCodeReset +#endif describe "message reactions" $ do it "set message reactions" testSetMessageReactions describe "delivery receipts" $ do @@ -125,10 +180,10 @@ chatDirectTests = do testInvVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnInvChatVRange vr1 vr2 testReqVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnReqChatVRange vr1 vr2 -testAddContact :: HasCallStack => SpecWith FilePath +testAddContact :: HasCallStack => SpecWith TestParams testAddContact = versionTestMatrix2 runTestAddContact where - runTestAddContact alice bob = do + runTestAddContact pqExpected alice bob = do alice ##> "/_connect 1" inv <- getInvitation alice bob ##> ("/_connect 1 " <> inv) @@ -137,48 +192,219 @@ testAddContact = versionTestMatrix2 runTestAddContact (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") threadDelay 100000 - chatsEmpty alice bob + chatsEmpty alice #> "@bob hello there 🙂" bob <# "alice> hello there 🙂" alice ##> "/_unread chat @2 on" alice <## "ok" alice ##> "/_unread chat @2 off" alice <## "ok" - chatsOneMessage alice bob + chatsOneMessage bob #> "@alice hello there" alice <# "bob> hello there" bob #> "@alice how are you?" alice <# "bob> how are you?" - chatsManyMessages alice bob - chatsEmpty alice bob = do - alice @@@ [("@bob", lastChatFeature)] - alice #$> ("/_get chat @2 count=100", chat, chatFeatures) - bob @@@ [("@alice", lastChatFeature)] - bob #$> ("/_get chat @2 count=100", chat, chatFeatures) - chatsOneMessage alice bob = do - alice @@@ [("@bob", "hello there 🙂")] - alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hello there 🙂")]) - bob @@@ [("@alice", "hello there 🙂")] - bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hello there 🙂")]) - chatsManyMessages alice bob = do - alice @@@ [("@bob", "how are you?")] - alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hello there 🙂"), (0, "hello there"), (0, "how are you?")]) - bob @@@ [("@alice", "how are you?")] - bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hello there 🙂"), (1, "hello there"), (1, "how are you?")]) - -- pagination - alice #$> ("/_get chat @2 after=" <> itemId 1 <> " count=100", chat, [(0, "hello there"), (0, "how are you?")]) - alice #$> ("/_get chat @2 before=" <> itemId 2 <> " count=100", chat, chatFeatures <> [(1, "hello there 🙂")]) - -- search - alice #$> ("/_get chat @2 count=100 search=ello ther", chat, [(1, "hello there 🙂"), (0, "hello there")]) - -- read messages - alice #$> ("/_read chat @2 from=1 to=100", id, "ok") - bob #$> ("/_read chat @2 from=1 to=100", id, "ok") - alice #$> ("/_read chat @2", id, "ok") - bob #$> ("/_read chat @2", id, "ok") - alice #$> ("/read user", id, "ok") - alice #$> ("/_read user 1", id, "ok") + chatsManyMessages + where + chatsEmpty = do + alice @@@ [("@bob", lastChatFeature)] + alice #$> ("/_get chat @2 count=100", chat, features) + bob @@@ [("@alice", lastChatFeature)] + bob #$> ("/_get chat @2 count=100", chat, features) + chatsOneMessage = do + alice @@@ [("@bob", "hello there 🙂")] + alice #$> ("/_get chat @2 count=100", chat, features <> [(1, "hello there 🙂")]) + bob @@@ [("@alice", "hello there 🙂")] + bob #$> ("/_get chat @2 count=100", chat, features <> [(0, "hello there 🙂")]) + chatsManyMessages = do + alice @@@ [("@bob", "how are you?")] + alice #$> ("/_get chat @2 count=100", chat, features <> [(1, "hello there 🙂"), (0, "hello there"), (0, "how are you?")]) + bob @@@ [("@alice", "how are you?")] + bob #$> ("/_get chat @2 count=100", chat, features <> [(0, "hello there 🙂"), (1, "hello there"), (1, "how are you?")]) + -- pagination + alice #$> ("/_get chat @2 after=" <> itemId 1 <> " count=100", chat, [(0, "hello there"), (0, "how are you?")]) + alice #$> ("/_get chat @2 before=" <> itemId 2 <> " count=100", chat, features <> [(1, "hello there 🙂")]) + alice #$> ("/_get chat @2 around=" <> itemId 2 <> " count=2", chat, [(0, "Audio/video calls: enabled"), (1, "hello there 🙂"), (0, "hello there"), (0, "how are you?")]) + -- search + alice #$> ("/_get chat @2 count=100 search=ello ther", chat, [(1, "hello there 🙂"), (0, "hello there")]) + -- read messages + alice #$> ("/_read chat @2", id, "ok") + bob #$> ("/_read chat @2", id, "ok") + alice #$> ("/read user", id, "ok") + alice #$> ("/_read user 1", id, "ok") + features = + if pqExpected + then chatFeatures + else (0, e2eeInfoNoPQStr) : tail chatFeatures -testDuplicateContactsSeparate :: HasCallStack => FilePath -> IO () +testRetryConnecting :: HasCallStack => TestParams -> IO () +testRetryConnecting ps = testChatCfgOpts2 cfg' opts' aliceProfile bobProfile test ps + where + tmp = tmpPath ps + test alice bob = do + inv <- withSmpServer' serverCfg' $ do + alice ##> "/_connect 1" + getInvitation alice + alice <## "server disconnected localhost ()" + bob ##> ("/_connect plan 1 " <> inv) + bob <## "invitation link: ok to connect" + bob ##> ("/_connect 1 " <> inv) + bob <##. "smp agent error: BROKER" + withSmpServer' serverCfg' $ do + alice <## "server connected localhost ()" + threadDelay 250000 + bob ##> ("/_connect plan 1 " <> inv) + bob <## "invitation link: ok to connect" + bob ##> ("/_connect 1 " <> inv) + bob <## "confirmation sent!" + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice #> "@bob message 1" + bob <# "alice> message 1" + bob #> "@alice message 2" + alice <# "bob> message 2" + bob <## "server disconnected localhost (@alice)" + alice <## "server disconnected localhost (@bob)" + serverCfg' = + smpServerCfg + { transports = [("7003", transport @TLS, False)], + msgQueueQuota = 2, + serverStoreCfg = persistentServerStoreCfg tmp + } + fastRetryInterval = defaultReconnectInterval {initialInterval = 50000} -- same as in agent tests + cfg' = + testCfg + { agentConfig = + testAgentCfg + { quotaExceededTimeout = 1, + messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval} + } + } + opts' = + testOpts + { coreOptions = + testCoreOpts + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] + } + } + +testRetryConnectingClientTimeout :: HasCallStack => TestParams -> IO () +testRetryConnectingClientTimeout ps = do + inv <- withSmpServer' serverCfg' $ do + withNewTestChatCfgOpts ps cfg' opts' "alice" aliceProfile $ \alice -> do + alice ##> "/_connect 1" + inv <- getInvitation alice + + withNewTestChatCfgOpts ps cfgZeroTimeout opts' "bob" bobProfile $ \bob -> do + bob ##> ("/_connect plan 1 " <> inv) + bob <## "invitation link: ok to connect" + bob ##> ("/_connect 1 " <> inv) + bob <## "smp agent error: BROKER {brokerAddress = \"smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7003\", brokerErr = TIMEOUT}" + + pure inv + + logFile <- readFile $ tmp <> "/smp-server-store.log" + logFile `shouldContain` "SECURE" + + withSmpServer' serverCfg' $ do + withTestChatCfgOpts ps cfg' opts' "alice" $ \alice -> do + withTestChatCfgOpts ps cfg' opts' "bob" $ \bob -> do + bob ##> ("/_connect plan 1 " <> inv) + bob <## "invitation link: ok to connect" + bob ##> ("/_connect 1 " <> inv) + bob <## "confirmation sent!" + + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice #> "@bob message 1" + bob <# "alice> message 1" + bob #> "@alice message 2" + alice <# "bob> message 2" + where + tmp = tmpPath ps + serverCfg' = + smpServerCfg + { transports = [("7003", transport @TLS, False)], + msgQueueQuota = 2, + serverStoreCfg = persistentServerStoreCfg tmp + } + fastRetryInterval = defaultReconnectInterval {initialInterval = 50000} -- same as in agent tests + cfg' = + testCfg + { agentConfig = + testAgentCfg + { quotaExceededTimeout = 1, + messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval} + } + } + cfgZeroTimeout = + (testCfg :: ChatConfig) + { agentConfig = + testAgentCfg + { quotaExceededTimeout = 1, + messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval} + }, + presetServers = + let def@PresetServers {netCfg} = presetServers testCfg + in def {netCfg = (netCfg :: NetworkConfig) {tcpTimeout = 10}} + } + opts' = + testOpts + { coreOptions = + testCoreOpts + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] + } + } + +testMarkReadDirect :: HasCallStack => TestParams -> IO () +testMarkReadDirect = testChat2 aliceProfile bobProfile $ \alice bob -> do + connectUsers alice bob + alice #> "@bob 1" + alice #> "@bob 2" + alice #> "@bob 3" + alice #> "@bob 4" + bob <# "alice> 1" + bob <# "alice> 2" + bob <# "alice> 3" + bob <# "alice> 4" + bob ##> "/last_item_id" + i :: ChatItemId <- read <$> getTermLine bob + let itemIds = intercalate "," $ map show [i - 3 .. i] + bob #$> ("/_read chat items @2 " <> itemIds, id, "ok") + +testChatPaginationInitial :: HasCallStack => TestParams -> IO () +testChatPaginationInitial = testChatOpts2 opts aliceProfile bobProfile $ \alice bob -> do + connectUsers alice bob + -- Wait, otherwise ids are going to be wrong. + threadDelay 1000000 + + -- Send messages from alice to bob + forM_ ([1 .. 10] :: [Int]) $ \n -> alice #> ("@bob " <> show n) + + -- Bob receives the messages. + forM_ ([1 .. 10] :: [Int]) $ \n -> bob <# ("alice> " <> show n) + + -- All messages are unread for bob, should return area around unread + bob #$> ("/_get chat @2 initial=2", chat, [(0, "Voice messages: enabled"), (0, "Audio/video calls: enabled"), (0, "1"), (0, "2"), (0, "3")]) + + -- Read next 2 items + let itemIds = intercalate "," $ map itemId [1 .. 2] + bob #$> ("/_read chat items @2 " <> itemIds, id, "ok") + bob #$> ("/_get chat @2 initial=2", chat, [(0, "1"), (0, "2"), (0, "3"), (0, "4"), (0, "5")]) + + -- Read all items + bob #$> ("/_read chat @2", id, "ok") + bob #$> ("/_get chat @2 initial=3", chat, [(0, "8"), (0, "9"), (0, "10")]) + bob #$> ("/_get chat @2 initial=5", chat, [(0, "6"), (0, "7"), (0, "8"), (0, "9"), (0, "10")]) + where + opts = + testOpts + { markRead = False + } + +testDuplicateContactsSeparate :: HasCallStack => TestParams -> IO () testDuplicateContactsSeparate = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -204,7 +430,7 @@ testDuplicateContactsSeparate = bob @@@ [("@alice", "hey"), ("@alice_1", "2")] bob `hasContactProfiles` ["bob", "alice", "alice"] -testDuplicateContactsMultipleSeparate :: HasCallStack => FilePath -> IO () +testDuplicateContactsMultipleSeparate :: HasCallStack => TestParams -> IO () testDuplicateContactsMultipleSeparate = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -244,7 +470,7 @@ testDuplicateContactsMultipleSeparate = alice `hasContactProfiles` ["alice", "bob", "bob", "bob"] bob `hasContactProfiles` ["bob", "alice", "alice", "alice"] -testPlanInvitationLinkOk :: HasCallStack => FilePath -> IO () +testPlanInvitationLinkOk :: HasCallStack => TestParams -> IO () testPlanInvitationLinkOk = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -263,9 +489,9 @@ testPlanInvitationLinkOk = bob <## "invitation link: ok to connect" -- conn_req_inv is forgotten after connection alice <##> bob -testPlanInvitationLinkOwn :: HasCallStack => FilePath -> IO () -testPlanInvitationLinkOwn tmp = - withNewTestChat tmp "alice" aliceProfile $ \alice -> do +testPlanInvitationLinkOwn :: HasCallStack => TestParams -> IO () +testPlanInvitationLinkOwn ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do alice ##> "/c" inv <- getInvitation alice alice ##> ("/_connect plan 1 " <> inv) @@ -284,6 +510,7 @@ testPlanInvitationLinkOwn tmp = alice ##> ("/_connect plan 1 " <> inv) alice <## "invitation link: ok to connect" -- conn_req_inv is forgotten after connection + threadDelay 100000 alice @@@ [("@alice_1", lastChatFeature), ("@alice_2", lastChatFeature)] alice `send` "@alice_2 hi" alice @@ -297,12 +524,12 @@ testPlanInvitationLinkOwn tmp = ] alice @@@ [("@alice_1", "hey"), ("@alice_2", "hey")] -testPlanInvitationLinkConnecting :: HasCallStack => FilePath -> IO () -testPlanInvitationLinkConnecting tmp = do - inv <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do +testPlanInvitationLinkConnecting :: HasCallStack => TestParams -> IO () +testPlanInvitationLinkConnecting ps = do + inv <- withNewTestChat ps "alice" aliceProfile $ \alice -> do alice ##> "/c" getInvitation alice - withNewTestChat tmp "bob" bobProfile $ \bob -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do bob ##> ("/c " <> inv) bob <## "confirmation sent!" @@ -313,7 +540,7 @@ testPlanInvitationLinkConnecting tmp = do bob ##> ("/_connect plan 1 " <> invSchema2) bob <## "invitation link: connecting" -testContactClear :: HasCallStack => FilePath -> IO () +testContactClear :: HasCallStack => TestParams -> IO () testContactClear = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -325,14 +552,14 @@ testContactClear = bob #$> ("/clear alice", id, "alice: all messages are removed locally ONLY") bob #$> ("/_get chat @2 count=100", chat, []) -testDeleteContactDeletesProfile :: HasCallStack => FilePath -> IO () +testDeleteContactDeletesProfile :: HasCallStack => TestParams -> IO () testDeleteContactDeletesProfile = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob alice <##> bob -- alice deletes contact, profile is deleted - alice ##> "/d bob" + alice ##> "/_delete @2 full notify=on" alice <## "bob: contact is deleted" bob <## "alice (Alice) deleted contact with you" alice ##> "/_contacts 1" @@ -345,43 +572,44 @@ testDeleteContactDeletesProfile = (bob </) bob `hasContactProfiles` ["bob"] -testDeleteUnusedContactSilent :: HasCallStack => FilePath -> IO () -testDeleteUnusedContactSilent = - testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ - \alice bob cath -> do - createGroup3 "team" alice bob cath - bob ##> "/contacts" - bob <### ["alice (Alice)", "cath (Catherine)"] - bob `hasContactProfiles` ["bob", "alice", "cath"] - cath ##> "/contacts" - cath <### ["alice (Alice)", "bob (Bob)"] - cath `hasContactProfiles` ["cath", "alice", "bob"] - -- bob deletes cath, cath's bob contact is deleted silently - bob ##> "/d cath" - bob <## "cath: contact is deleted" - bob ##> "/contacts" - bob <## "alice (Alice)" - threadDelay 50000 - cath ##> "/contacts" - cath <## "alice (Alice)" - -- group messages work - alice #> "#team hello" - concurrentlyN_ - [ bob <# "#team alice> hello", - cath <# "#team alice> hello" - ] - bob #> "#team hi there" - concurrentlyN_ - [ alice <# "#team bob> hi there", - cath <# "#team bob> hi there" - ] - cath #> "#team hey" - concurrentlyN_ - [ alice <# "#team cath> hey", - bob <# "#team cath> hey" - ] +testDeleteContactKeepConversation :: HasCallStack => TestParams -> IO () +testDeleteContactKeepConversation = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + alice <##> bob -testDirectMessageQuotedReply :: HasCallStack => FilePath -> IO () + alice ##> "/_delete @2 entity notify=on" + alice <## "bob: contact is deleted" + bob <## "alice (Alice) deleted contact with you" + + alice @@@ [("@bob", "hey")] + alice ##> "@bob hi" + alice <## "bob: not ready" + bob @@@ [("@alice", "contact deleted")] + bob ##> "@alice hey" + bob <## "alice: not ready" + +testDeleteConversationKeepContact :: HasCallStack => TestParams -> IO () +testDeleteConversationKeepContact = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + alice <##> bob + + alice @@@ [("@bob", "hey")] + + alice ##> "/_delete @2 messages" + alice <## "bob: contact is deleted" + + alice @@@ [("@bob", "")] -- UI would filter + bob @@@ [("@alice", "hey")] + bob #> "@alice hi" + alice <# "bob> hi" + alice @@@ [("@bob", "hi")] + alice <##> bob + +testDirectMessageQuotedReply :: HasCallStack => TestParams -> IO () testDirectMessageQuotedReply = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -406,7 +634,7 @@ testDirectMessageQuotedReply = bob #$> ("/_get chat @2 count=1", chat', [((1, "will tell more"), Just (1, "all good - you?"))]) alice #$> ("/_get chat @2 count=1", chat', [((0, "will tell more"), Just (0, "all good - you?"))]) -testDirectMessageUpdate :: HasCallStack => FilePath -> IO () +testDirectMessageUpdate :: HasCallStack => TestParams -> IO () testDirectMessageUpdate = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -470,7 +698,7 @@ testDirectMessageUpdate = alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((1, "greetings 🤝"), Nothing), ((0, "hey Alice"), Just (1, "hello 🙂")), ((0, "greetings Alice"), Just (1, "hey 👋"))]) bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "greetings 🤝"), Nothing), ((1, "hey Alice"), Just (0, "hello 🙂")), ((1, "greetings Alice"), Just (0, "hey 👋"))]) -testDirectMessageEditHistory :: HasCallStack => FilePath -> IO () +testDirectMessageEditHistory :: HasCallStack => TestParams -> IO () testDirectMessageEditHistory = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -541,7 +769,7 @@ testDirectMessageEditHistory = bob <## "message history:" bob .<## ": hey there" -testDirectMessageDelete :: HasCallStack => FilePath -> IO () +testDirectMessageDelete :: HasCallStack => TestParams -> IO () testDirectMessageDelete = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -571,7 +799,7 @@ testDirectMessageDelete = alice @@@ [("@bob", lastChatFeature)] alice #$> ("/_get chat @2 count=100", chat, chatFeatures) - -- alice: msg id 1 + -- alice: msg id 3 bob ##> ("/_update item @2 " <> itemId 2 <> " text hey alice") bob <# "@alice [edited] > hello 🙂" bob <## " hey alice" @@ -586,12 +814,12 @@ testDirectMessageDelete = alice @@@ [("@bob", "hey alice [marked deleted]")] alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hey alice [marked deleted]")]) - -- alice: deletes msg id 1 that was broadcast deleted by bob - alice #$> ("/_delete item @2 " <> itemId 1 <> " internal", id, "message deleted") + -- alice: deletes msg id 3 that was broadcast deleted by bob + alice #$> ("/_delete item @2 " <> itemId 3 <> " internal", id, "message deleted") alice @@@ [("@bob", lastChatFeature)] alice #$> ("/_get chat @2 count=100", chat, chatFeatures) - -- alice: msg id 1, bob: msg id 3 (quoting message alice deleted locally) + -- alice: msg id 4, bob: msg id 3 (quoting message alice deleted locally) bob `send` "> @alice (hello 🙂) do you receive my messages?" bob <# "@alice > hello 🙂" bob <## " do you receive my messages?" @@ -599,14 +827,14 @@ testDirectMessageDelete = alice <## " do you receive my messages?" alice @@@ [("@bob", "do you receive my messages?")] alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "do you receive my messages?"), Just (1, "hello 🙂"))]) - alice #$> ("/_delete item @2 " <> itemId 1 <> " broadcast", id, "cannot delete this item") + alice #$> ("/_delete item @2 " <> itemId 4 <> " broadcast", id, "cannot delete this item") - -- alice: msg id 2, bob: msg id 4 + -- alice: msg id 5, bob: msg id 4 bob #> "@alice how are you?" alice <# "bob> how are you?" - -- alice: deletes msg id 2 - alice #$> ("/_delete item @2 " <> itemId 2 <> " internal", id, "message deleted") + -- alice: deletes msg id 5 + alice #$> ("/_delete item @2 " <> itemId 5 <> " internal", id, "message deleted") -- bob: marks deleted msg id 4 (that alice deleted locally) bob #$> ("/_delete item @2 " <> itemId 4 <> " broadcast", id, "message marked deleted") @@ -622,19 +850,70 @@ testDirectMessageDelete = bob #$> ("/_delete item @2 " <> itemId 4 <> " internal", id, "message deleted") bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "hello 🙂"), Nothing), ((1, "do you receive my messages?"), Just (0, "hello 🙂"))]) -testDirectLiveMessage :: HasCallStack => FilePath -> IO () +testDirectMessageDeleteMultiple :: HasCallStack => TestParams -> IO () +testDirectMessageDeleteMultiple = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + + alice #> "@bob hello" + bob <# "alice> hello" + msgId1 <- lastItemId alice + + alice #> "@bob hey" + bob <# "alice> hey" + msgId2 <- lastItemId alice + + alice ##> ("/_delete item @2 " <> msgId1 <> "," <> msgId2 <> " broadcast") + alice <## "2 messages deleted" + bob <# "alice> [marked deleted] hello" + bob <# "alice> [marked deleted] hey" + + alice #$> ("/_get chat @2 count=2", chat, [(1, "hello [marked deleted]"), (1, "hey [marked deleted]")]) + bob #$> ("/_get chat @2 count=2", chat, [(0, "hello [marked deleted]"), (0, "hey [marked deleted]")]) + +testDirectMessageDeleteMultipleManyBatches :: HasCallStack => TestParams -> IO () +testDirectMessageDeleteMultipleManyBatches = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + + msgIdZero <- lastItemId alice + + let cm i = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message " <> show i <> "\"}}" + cms = intercalate ", " (map cm [1 .. 300 :: Int]) + + alice `send` ("/_send @2 json [" <> cms <> "]") + _ <- getTermLine alice + + alice <## "300 messages sent" + msgIdLast <- lastItemId alice + + forM_ [(1 :: Int) .. 300] $ \i -> do + bob <# ("alice> message " <> show i) + + let mIdFirst = (read msgIdZero :: Int) + 1 + mIdLast = read msgIdLast :: Int + deleteIds = intercalate "," (map show [mIdFirst .. mIdLast]) + alice `send` ("/_delete item @2 " <> deleteIds <> " broadcast") + _ <- getTermLine alice + alice <## "300 messages deleted" + forM_ [(1 :: Int) .. 300] $ \i -> do + bob <# ("alice> [marked deleted] message " <> show i) + +testDirectLiveMessage :: HasCallStack => TestParams -> IO () testDirectLiveMessage = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob -- non-empty live message is sent instantly alice `send` "/live @bob hello" - bob <# "alice> [LIVE started] use /show [on/off/6] hello" + bob <# "alice> [LIVE started] use /show [on/off/7] hello" alice ##> ("/_update item @2 " <> itemId 1 <> " text hello there") alice <# "@bob [LIVE] hello there" bob <# "alice> [LIVE ended] hello there" -- empty live message is also sent instantly alice `send` "/live @bob" - bob <# "alice> [LIVE started] use /show [on/off/7]" + bob <# "alice> [LIVE started] use /show [on/off/8]" alice ##> ("/_update item @2 " <> itemId 2 <> " text hello 2") alice <# "@bob [LIVE] hello 2" bob <# "alice> [LIVE ended] hello 2" @@ -651,7 +930,7 @@ testDirectLiveMessage = bob .<## ": hello 2" bob .<## ":" -testDirectTimedMessage :: HasCallStack => FilePath -> IO () +testDirectTimedMessage :: HasCallStack => TestParams -> IO () testDirectTimedMessage = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -677,7 +956,7 @@ testDirectTimedMessage = alice <# "@bob hi" bob <# "alice> hi" -testRepeatAuthErrorsDisableContact :: HasCallStack => FilePath -> IO () +testRepeatAuthErrorsDisableContact :: HasCallStack => TestParams -> IO () testRepeatAuthErrorsDisableContact = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob @@ -697,7 +976,7 @@ testRepeatAuthErrorsDisableContact = alice #> "@bob hey" alice <## "[bob, contactId: 2, connId: 1] error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection" -testMultilineMessage :: HasCallStack => FilePath -> IO () +testMultilineMessage :: HasCallStack => TestParams -> IO () testMultilineMessage = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do connectUsers alice bob connectUsers alice cath @@ -714,26 +993,160 @@ testMultilineMessage = testChat3 aliceProfile bobProfile cathProfile $ \alice bo cath <# "alice> hello" cath <## "there" -testGetSetSMPServers :: HasCallStack => FilePath -> IO () -testGetSetSMPServers = +testLargeMessage :: HasCallStack => TestParams -> IO () +testLargeMessage = testChat2 aliceProfile bobProfile $ - \alice _ -> do - alice #$> ("/_servers 1 smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001") + \alice bob -> do + connectUsers alice bob + + img <- genProfileImg + let profileImage = "data:image/png;base64," <> B.unpack img + alice `send` ("/_profile 1 {\"displayName\": \"alice2\", \"fullName\": \"\", \"image\": \"" <> profileImage <> "\"}") + _trimmedCmd1 <- getTermLine alice + alice <## "user profile is changed to alice2 (your 1 contacts are notified)" + bob <## "contact alice changed to alice2" + bob <## "use @alice2 <message> to send messages" + +testSendMulti :: HasCallStack => TestParams -> IO () +testSendMulti = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + + alice ##> "/_send @2 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]" + alice <# "@bob test 1" + alice <# "@bob test 2" + bob <# "alice> test 1" + bob <# "alice> test 2" + +testSendMultiTimed :: HasCallStack => TestParams -> IO () +testSendMultiTimed = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + + alice ##> "/_send @2 ttl=1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]" + alice <# "@bob test 1" + alice <# "@bob test 2" + bob <# "alice> test 1" + bob <# "alice> test 2" + + alice + <### [ "timed message deleted: test 1", + "timed message deleted: test 2" + ] + bob + <### [ "timed message deleted: test 1", + "timed message deleted: test 2" + ] + +testSendMultiWithQuote :: HasCallStack => TestParams -> IO () +testSendMultiWithQuote = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + + alice #> "@bob hello" + bob <# "alice> hello" + msgId1 <- lastItemId alice + + threadDelay 1000000 + + bob #> "@alice hi" + alice <# "bob> hi" + msgId2 <- lastItemId alice + + let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message 1\"}}" + cm2 = "{\"quotedItemId\": " <> msgId1 <> ", \"msgContent\": {\"type\": \"text\", \"text\": \"message 2\"}}" + cm3 = "{\"quotedItemId\": " <> msgId2 <> ", \"msgContent\": {\"type\": \"text\", \"text\": \"message 3\"}}" + + alice ##> ("/_send @2 json [" <> cm1 <> ", " <> cm2 <> ", " <> cm3 <> "]") + alice <## "bad chat command: invalid multi send: live and more than one quote not supported" + + alice ##> ("/_send @2 json [" <> cm1 <> ", " <> cm2 <> "]") + + alice <# "@bob message 1" + alice <# "@bob >> hello" + alice <## " message 2" + + bob <# "alice> message 1" + bob <# "alice> >> hello" + bob <## " message 2" + + alice ##> ("/_send @2 json [" <> cm3 <> ", " <> cm1 <> "]") + + alice <# "@bob > hi" + alice <## " message 3" + alice <# "@bob message 1" + + bob <# "alice> > hi" + bob <## " message 3" + bob <# "alice> message 1" + +testSendMultiManyBatches :: HasCallStack => TestParams -> IO () +testSendMultiManyBatches = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + + threadDelay 1000000 + + msgIdAlice <- lastItemId alice + msgIdBob <- lastItemId bob + + let cm i = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message " <> show i <> "\"}}" + cms = intercalate ", " (map cm [1 .. 300 :: Int]) + + alice `send` ("/_send @2 json [" <> cms <> "]") + _ <- getTermLine alice + + alice <## "300 messages sent" + + forM_ [(1 :: Int) .. 300] $ \i -> + bob <# ("alice> message " <> show i) + + aliceItemsCount <- withCCTransaction alice $ \db -> + DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdAlice) :: IO [[Int]] + aliceItemsCount `shouldBe` [[300]] + + bobItemsCount <- withCCTransaction bob $ \db -> + DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdBob) :: IO [[Int]] + bobItemsCount `shouldBe` [[300]] + + threadDelay 1000000 + +testGetSetSMPServers :: HasCallStack => TestParams -> IO () +testGetSetSMPServers = + testChat aliceProfile $ + \alice -> do + alice ##> "/_servers 1" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001" + alice <## " XFTP servers" + alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002" alice #$> ("/smp smp://1234-w==@smp1.example.im", id, "ok") - alice #$> ("/smp", id, "smp://1234-w==@smp1.example.im") + alice ##> "/smp" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://1234-w==@smp1.example.im" alice #$> ("/smp smp://1234-w==:password@smp1.example.im", id, "ok") - alice #$> ("/smp", id, "smp://1234-w==:password@smp1.example.im") + -- alice #$> ("/smp", id, "smp://1234-w==:password@smp1.example.im") + alice ##> "/smp" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://1234-w==:password@smp1.example.im" alice #$> ("/smp smp://2345-w==@smp2.example.im smp://3456-w==@smp3.example.im:5224", id, "ok") alice ##> "/smp" - alice <## "smp://2345-w==@smp2.example.im" - alice <## "smp://3456-w==@smp3.example.im:5224" - alice #$> ("/smp default", id, "ok") - alice #$> ("/smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001") + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://2345-w==@smp2.example.im" + alice <## " smp://3456-w==@smp3.example.im:5224" -testTestSMPServerConnection :: HasCallStack => FilePath -> IO () +testTestSMPServerConnection :: HasCallStack => TestParams -> IO () testTestSMPServerConnection = - testChat2 aliceProfile bobProfile $ - \alice _ -> do + testChat aliceProfile $ + \alice -> do alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7001" alice <## "SMP server test passed" -- to test with password: @@ -742,29 +1155,40 @@ testTestSMPServerConnection = alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001" alice <## "SMP server test passed" alice ##> "/smp test smp://LcJU@localhost:7001" - alice <## "SMP server test failed at Connect, error: BROKER smp://LcJU@localhost:7001 NETWORK" + alice <## "SMP server test failed at Connect, error: BROKER {brokerAddress = \"smp://LcJU@localhost:7001\", brokerErr = NETWORK}" alice <## "Possibly, certificate fingerprint in SMP server address is incorrect" -testGetSetXFTPServers :: HasCallStack => FilePath -> IO () +testGetSetXFTPServers :: HasCallStack => TestParams -> IO () testGetSetXFTPServers = - testChat2 aliceProfile bobProfile $ - \alice _ -> withXFTPServer $ do - alice #$> ("/_servers 1 xftp", id, "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002") + testChat aliceProfile $ + \alice -> withXFTPServer $ do + alice ##> "/_servers 1" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001" + alice <## " XFTP servers" + alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002" alice #$> ("/xftp xftp://1234-w==@xftp1.example.im", id, "ok") - alice #$> ("/xftp", id, "xftp://1234-w==@xftp1.example.im") + alice ##> "/xftp" + alice <## "Your servers" + alice <## " XFTP servers" + alice <## " xftp://1234-w==@xftp1.example.im" alice #$> ("/xftp xftp://1234-w==:password@xftp1.example.im", id, "ok") - alice #$> ("/xftp", id, "xftp://1234-w==:password@xftp1.example.im") + alice ##> "/xftp" + alice <## "Your servers" + alice <## " XFTP servers" + alice <## " xftp://1234-w==:password@xftp1.example.im" alice #$> ("/xftp xftp://2345-w==@xftp2.example.im xftp://3456-w==@xftp3.example.im:5224", id, "ok") alice ##> "/xftp" - alice <## "xftp://2345-w==@xftp2.example.im" - alice <## "xftp://3456-w==@xftp3.example.im:5224" - alice #$> ("/xftp default", id, "ok") - alice #$> ("/xftp", id, "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002") + alice <## "Your servers" + alice <## " XFTP servers" + alice <## " xftp://2345-w==@xftp2.example.im" + alice <## " xftp://3456-w==@xftp3.example.im:5224" -testTestXFTPServer :: HasCallStack => FilePath -> IO () +testTestXFTPServer :: HasCallStack => TestParams -> IO () testTestXFTPServer = - testChat2 aliceProfile bobProfile $ - \alice _ -> withXFTPServer $ do + testChat aliceProfile $ + \alice -> withXFTPServer $ do alice ##> "/xftp test xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7002" alice <## "XFTP server test passed" -- to test with password: @@ -773,189 +1197,106 @@ testTestXFTPServer = alice ##> "/xftp test xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002" alice <## "XFTP server test passed" alice ##> "/xftp test xftp://LcJU@localhost:7002" - alice <## "XFTP server test failed at Connect, error: BROKER xftp://LcJU@localhost:7002 NETWORK" + alice <## "XFTP server test failed at Connect, error: BROKER {brokerAddress = \"xftp://LcJU@localhost:7002\", brokerErr = NETWORK}" alice <## "Possibly, certificate fingerprint in XFTP server address is incorrect" -testAsyncInitiatingOffline :: HasCallStack => FilePath -> IO () -testAsyncInitiatingOffline tmp = do - putStrLn "testAsyncInitiatingOffline" - inv <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do +testOperators :: HasCallStack => TestParams -> IO () +testOperators = + testChatCfgOpts testCfg opts' aliceProfile $ + \alice -> do + -- initial load + alice ##> "/_conditions" + alice <##. "Current conditions: 2." + alice ##> "/_operators" + alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: required" + alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: disabled, conditions: required" + alice <##. "The new conditions will be accepted for SimpleX Chat Ltd at " + -- set conditions notified + alice ##> "/_conditions_notified 2" + alice <## "ok" + alice ##> "/_operators" + alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: required" + alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: disabled, conditions: required" + alice ##> "/_conditions" + alice <##. "Current conditions: 2 (notified)." + -- accept conditions + alice ##> "/_accept_conditions 2 1,2" + alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: accepted (" + alice <##. "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: disabled, conditions: accepted (" + -- update operators + alice ##> "/operators 2:on:smp=proxy:xftp=off" + alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: accepted (" + alice <##. "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP disabled (servers known), conditions: accepted (" + where + opts' = testOpts {coreOptions = testCoreOpts {smpServers = [], xftpServers = []}} + +testAsyncInitiatingOffline :: HasCallStack => ChatConfig -> ChatConfig -> TestParams -> IO () +testAsyncInitiatingOffline aliceCfg bobCfg ps = do + inv <- withNewTestChatCfg ps aliceCfg "alice" aliceProfile $ \alice -> do threadDelay 250000 alice ##> "/c" getInvitation alice - withNewTestChat tmp "bob" bobProfile $ \bob -> do + withNewTestChatCfg ps bobCfg "bob" bobProfile $ \bob -> do threadDelay 250000 bob ##> ("/c " <> inv) bob <## "confirmation sent!" - withTestChat tmp "alice" $ \alice -> do + withTestChatCfg ps aliceCfg "alice" $ \alice -> do concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") -testAsyncAcceptingOffline :: HasCallStack => FilePath -> IO () -testAsyncAcceptingOffline tmp = do - putStrLn "testAsyncAcceptingOffline" - inv <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do +testAsyncAcceptingOffline :: HasCallStack => ChatConfig -> ChatConfig -> TestParams -> IO () +testAsyncAcceptingOffline aliceCfg bobCfg ps = do + inv <- withNewTestChatCfg ps aliceCfg "alice" aliceProfile $ \alice -> do alice ##> "/c" getInvitation alice - withNewTestChat tmp "bob" bobProfile $ \bob -> do + withNewTestChatCfg ps bobCfg "bob" bobProfile $ \bob -> do threadDelay 250000 bob ##> ("/c " <> inv) bob <## "confirmation sent!" - withTestChat tmp "alice" $ \alice -> do - withTestChat tmp "bob" $ \bob -> do + withTestChatCfg ps aliceCfg "alice" $ \alice -> do + withTestChatCfg ps bobCfg "bob" $ \bob -> do concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") -testFullAsync :: HasCallStack => FilePath -> IO () -testFullAsync tmp = do - putStrLn "testFullAsync" - inv <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do +testFullAsyncFast :: HasCallStack => TestParams -> IO () +testFullAsyncFast ps = do + inv <- withNewTestChat ps "alice" aliceProfile $ \alice -> do threadDelay 250000 alice ##> "/c" getInvitation alice - withNewTestChat tmp "bob" bobProfile $ \bob -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do threadDelay 250000 bob ##> ("/c " <> inv) bob <## "confirmation sent!" - withTestChat tmp "alice" $ \_ -> pure () -- connecting... notification in UI - withTestChat tmp "bob" $ \_ -> pure () -- connecting... notification in UI - withTestChat tmp "alice" $ \alice -> do - alice <## "1 contacts connected (use /cs for the list)" + threadDelay 250000 + withTestChat ps "alice" $ \alice -> alice <## "bob (Bob): contact is connected" - withTestChat tmp "bob" $ \bob -> do - bob <## "1 contacts connected (use /cs for the list)" + withTestChat ps "bob" $ \bob -> bob <## "alice (Alice): contact is connected" -testFullAsyncV1 :: HasCallStack => FilePath -> IO () -testFullAsyncV1 tmp = do - putStrLn "testFullAsyncV1" - inv <- withNewAlice $ \alice -> do - putStrLn "1" +testFullAsyncSlow :: HasCallStack => ChatConfig -> ChatConfig -> TestParams -> IO () +testFullAsyncSlow aliceCfg bobCfg ps = do + inv <- withNewTestChatCfg ps aliceCfg "alice" aliceProfile $ \alice -> do + threadDelay 250000 alice ##> "/c" - putStrLn "2" getInvitation alice - putStrLn "3" - withNewBob $ \bob -> do - putStrLn "4" + withNewTestChatCfg ps bobCfg "bob" bobProfile $ \bob -> do + threadDelay 250000 bob ##> ("/c " <> inv) - putStrLn "5" bob <## "confirmation sent!" - putStrLn "6" - withAlice $ \_ -> pure () - putStrLn "7" - withBob $ \_ -> pure () - putStrLn "8" + withAlice $ \_ -> pure () -- connecting... notification in UI + withBob $ \_ -> pure () -- connecting... notification in UI withAlice $ \alice -> do - putStrLn "9" alice <## "1 contacts connected (use /cs for the list)" - putStrLn "10" - withBob $ \_ -> pure () - putStrLn "11" - withAlice $ \alice -> do - putStrLn "12" - alice <## "1 contacts connected (use /cs for the list)" - putStrLn "13" alice <## "bob (Bob): contact is connected" - putStrLn "14" withBob $ \bob -> do - putStrLn "15" bob <## "1 contacts connected (use /cs for the list)" - putStrLn "16" bob <## "alice (Alice): contact is connected" where - withNewAlice = withNewTestChatV1 tmp "alice" aliceProfile - withAlice = withTestChatV1 tmp "alice" - withNewBob = withNewTestChatV1 tmp "bob" bobProfile - withBob = withTestChatV1 tmp "bob" - -testFullAsyncV1toV2 :: HasCallStack => FilePath -> IO () -testFullAsyncV1toV2 tmp = do - putStrLn "testFullAsyncV1toV2" - inv <- withNewAlice $ \alice -> do - putStrLn "1" - alice ##> "/c" - putStrLn "2" - getInvitation alice - putStrLn "3" - withNewBob $ \bob -> do - putStrLn "4" - bob ##> ("/c " <> inv) - putStrLn "5" - bob <## "confirmation sent!" - withAlice $ \_ -> pure () - putStrLn "6" - withBob $ \_ -> pure () - putStrLn "7" - withAlice $ \alice -> do - putStrLn "8" - alice <## "1 contacts connected (use /cs for the list)" - putStrLn "9" - withBob $ \_ -> pure () - putStrLn "10" - withAlice $ \alice -> do - putStrLn "11" - alice <## "1 contacts connected (use /cs for the list)" - putStrLn "12" - alice <## "bob (Bob): contact is connected" - putStrLn "13" - withBob $ \bob -> do - putStrLn "14" - bob <## "1 contacts connected (use /cs for the list)" - putStrLn "15" - bob <## "alice (Alice): contact is connected" - where - withNewAlice = withNewTestChat tmp "alice" aliceProfile - withAlice = withTestChat tmp "alice" - withNewBob = withNewTestChatV1 tmp "bob" bobProfile - withBob = withTestChatV1 tmp "bob" - -testFullAsyncV2toV1 :: HasCallStack => FilePath -> IO () -testFullAsyncV2toV1 tmp = do - putStrLn "testFullAsyncV2toV1" - inv <- withNewAlice $ \alice -> do - putStrLn "1" - alice ##> "/c" - putStrLn "2" - getInvitation alice - putStrLn "3" - withNewBob $ \bob -> do - putStrLn "4" - bob ##> ("/c " <> inv) - putStrLn "5" - bob <## "confirmation sent!" - putStrLn "6" - withAlice $ \_ -> pure () - putStrLn "7" - withBob $ \_ -> pure () - putStrLn "8" - withAlice $ \alice -> do - putStrLn "9" - alice <## "1 contacts connected (use /cs for the list)" - putStrLn "10" - withBob $ \_ -> pure () - putStrLn "11" - withAlice $ \alice -> do - putStrLn "12" - alice <## "1 contacts connected (use /cs for the list)" - putStrLn "13" - alice <## "bob (Bob): contact is connected" - putStrLn "14" - withBob $ \bob -> do - putStrLn "15" - bob <## "1 contacts connected (use /cs for the list)" - putStrLn "16" - bob <## "alice (Alice): contact is connected" - where - withNewAlice = withNewTestChatV1 tmp "alice" aliceProfile - {-# INLINE withNewAlice #-} - withAlice = withTestChatV1 tmp "alice" - {-# INLINE withAlice #-} - withNewBob = withNewTestChat tmp "bob" bobProfile - {-# INLINE withNewBob #-} - withBob = withTestChat tmp "bob" - {-# INLINE withBob #-} + withAlice = withTestChatCfg ps aliceCfg "alice" + withBob = withTestChatCfg ps aliceCfg "bob" testCallType :: CallType testCallType = CallType {media = CMVideo, capabilities = CallCapabilities {encryption = True}} @@ -980,7 +1321,7 @@ serialize = B.unpack . LB.toStrict . J.encode repeatM_ :: Int -> IO a -> IO () repeatM_ n a = forM_ [1 .. n] $ const a -testNegotiateCall :: HasCallStack => FilePath -> IO () +testNegotiateCall :: HasCallStack => TestParams -> IO () testNegotiateCall = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob @@ -999,32 +1340,38 @@ testNegotiateCall = bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: accepted")]) alice <## "bob accepted your WebRTC video call (e2e encrypted)" repeatM_ 3 $ getTermLine alice + threadDelay 100000 alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: accepted")]) -- alice confirms call by sending WebRTC answer alice ##> ("/_call answer @2 " <> serialize testWebRTCSession) alice <## "ok" + threadDelay 100000 alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: connecting...")]) bob <## "alice continued the WebRTC call" repeatM_ 3 $ getTermLine bob + threadDelay 100000 bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: connecting...")]) -- participants can update calls as connected alice ##> "/_call status @2 connected" alice <## "ok" + threadDelay 100000 alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: in progress (00:00)")]) bob ##> "/_call status @2 connected" bob <## "ok" + threadDelay 100000 bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: in progress (00:00)")]) -- either party can end the call bob ##> "/_call end @2" bob <## "ok" + threadDelay 100000 bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: ended (00:00)")]) alice <## "call with bob ended" alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: ended (00:00)")]) -testMaintenanceMode :: HasCallStack => FilePath -> IO () -testMaintenanceMode tmp = do - withNewTestChat tmp "bob" bobProfile $ \bob -> do - withNewTestChatOpts tmp testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do +testMaintenanceMode :: HasCallStack => TestParams -> IO () +testMaintenanceMode ps = do + withNewTestChat ps "bob" bobProfile $ \bob -> do + withNewTestChatOpts ps testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do alice ##> "/c" alice <## "error: chat not started" alice ##> "/_start" @@ -1056,7 +1403,7 @@ testMaintenanceMode tmp = do alice ##> "/_start" alice <## "error: chat store changed, please restart chat" -- works after full restart - withTestChat tmp "alice" $ \alice -> testChatWorking alice bob + withTestChat ps "alice" $ \alice -> testChatWorking alice bob testChatWorking :: HasCallStack => TestCC -> TestCC -> IO () testChatWorking alice bob = do @@ -1066,21 +1413,35 @@ testChatWorking alice bob = do bob #> "@alice hello too" alice <# "bob> hello too" -testMaintenanceModeWithFiles :: HasCallStack => FilePath -> IO () -testMaintenanceModeWithFiles tmp = do - withNewTestChat tmp "bob" bobProfile $ \bob -> do - withNewTestChatOpts tmp testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do +testMaintenanceModeWithFiles :: HasCallStack => TestParams -> IO () +testMaintenanceModeWithFiles ps = withXFTPServer $ do + withNewTestChat ps "bob" bobProfile $ \bob -> do + withNewTestChatOpts ps testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do alice ##> "/_start" alice <## "chat started" alice ##> "/_files_folder ./tests/tmp/alice_files" alice <## "ok" connectUsers alice bob - startFileTransferWithDest' bob alice "test.jpg" "136.5 KiB / 139737 bytes" Nothing - bob <## "completed sending file 1 (test.jpg) to alice" + + bob #> "/f @alice ./tests/fixtures/test.jpg" + bob <## "use /fc 1 to cancel sending" + alice <# "bob> sends file test.jpg (136.5 KiB / 139737 bytes)" + alice <## "use /fr 1 [<dir>/ | <path>] to receive it" + bob <## "completed uploading file 1 (test.jpg) for alice" + + alice ##> "/fr 1" + alice + <### [ "saving file 1 from bob to test.jpg", + "started receiving file 1 (test.jpg) from bob" + ] alice <## "completed receiving file 1 (test.jpg) from bob" + src <- B.readFile "./tests/fixtures/test.jpg" - B.readFile "./tests/tmp/alice_files/test.jpg" `shouldReturn` src + dest <- B.readFile "./tests/tmp/alice_files/test.jpg" + dest `shouldBe` src + threadDelay 500000 + alice ##> "/_stop" alice <## "chat stopped" alice ##> "/_db export {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}" @@ -1095,12 +1456,13 @@ testMaintenanceModeWithFiles tmp = do alice <## "ok" B.readFile "./tests/tmp/alice_files/test.jpg" `shouldReturn` src -- works after full restart - withTestChat tmp "alice" $ \alice -> testChatWorking alice bob + withTestChat ps "alice" $ \alice -> testChatWorking alice bob -testDatabaseEncryption :: HasCallStack => FilePath -> IO () -testDatabaseEncryption tmp = do - withNewTestChat tmp "bob" bobProfile $ \bob -> do - withNewTestChatOpts tmp testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do +#if !defined(dbPostgres) +testDatabaseEncryption :: HasCallStack => TestParams -> IO () +testDatabaseEncryption ps = do + withNewTestChat ps "bob" bobProfile $ \bob -> do + withNewTestChatOpts ps testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do alice ##> "/_start" alice <## "chat started" connectUsers alice bob @@ -1118,19 +1480,23 @@ testDatabaseEncryption tmp = do alice <## "ok" alice ##> "/_start" alice <## "error: chat store changed, please restart chat" - withTestChatOpts tmp (getTestOpts True "mykey") "alice" $ \alice -> do + withTestChatOpts ps (getTestOpts True "mykey") "alice" $ \alice -> do alice ##> "/_start" alice <## "chat started" testChatWorking alice bob alice ##> "/_stop" alice <## "chat stopped" + alice ##> "/db test key wrongkey" + alice <## "error opening database after encryption: wrong passphrase or invalid database file" + alice ##> "/db test key mykey" + alice <## "ok" alice ##> "/db key wrongkey nextkey" alice <## "error encrypting database: wrong passphrase or invalid database file" alice ##> "/db key mykey nextkey" alice <## "ok" alice ##> "/_db encryption {\"currentKey\":\"nextkey\",\"newKey\":\"anotherkey\"}" alice <## "ok" - withTestChatOpts tmp (getTestOpts True "anotherkey") "alice" $ \alice -> do + withTestChatOpts ps (getTestOpts True "anotherkey") "alice" $ \alice -> do alice ##> "/_start" alice <## "chat started" testChatWorking alice bob @@ -1138,19 +1504,21 @@ testDatabaseEncryption tmp = do alice <## "chat stopped" alice ##> "/db decrypt anotherkey" alice <## "ok" - withTestChat tmp "alice" $ \alice -> do + withTestChat ps "alice" $ \alice -> do testChatWorking alice bob +#endif -testSubscribeAppNSE :: HasCallStack => FilePath -> IO () -testSubscribeAppNSE tmp = - withNewTestChat tmp "bob" bobProfile $ \bob -> do - withNewTestChat tmp "alice" aliceProfile $ \alice -> do - withTestChatOpts tmp testOpts {maintenance = True} "alice" $ \nseAlice -> do +testSubscribeAppNSE :: HasCallStack => TestParams -> IO () +testSubscribeAppNSE ps = + withNewTestChat ps "bob" bobProfile $ \bob -> do + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withTestChatOpts ps testOpts {maintenance = True} "alice" $ \nseAlice -> do alice ##> "/_app suspend 1" alice <## "ok" alice <## "chat suspended" - nseAlice ##> "/_start subscribe=off expire=off xftp=off" + nseAlice ##> "/_start main=off" nseAlice <## "chat started" + threadDelay 100000 nseAlice ##> "/ad" cLink <- getContactLink nseAlice True bob ##> ("/c " <> cLink) @@ -1163,14 +1531,14 @@ testSubscribeAppNSE tmp = alice <## "to accept: /ac bob" alice <## "to reject: /rc bob (the sender will NOT be notified)" alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") threadDelay 100000 alice <##> bob -testMuteContact :: HasCallStack => FilePath -> IO () +testMuteContact :: HasCallStack => TestParams -> IO () testMuteContact = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -1180,7 +1548,7 @@ testMuteContact = bob ##> "/mute @alice" bob <## "ok" alice #> "@bob hi" - (bob </) + bob <# "alice> hi <muted>" bob ##> "/contacts" bob <## "alice (Alice) (muted, you can /unmute @alice)" bob ##> "/unmute @alice" @@ -1190,7 +1558,7 @@ testMuteContact = alice #> "@bob hi again" bob <# "alice> hi again" -testMuteGroup :: HasCallStack => FilePath -> IO () +testMuteGroup :: HasCallStack => TestParams -> IO () testMuteGroup = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -1204,7 +1572,7 @@ testMuteGroup = bob <## "ok" alice #> "#team hi" concurrently_ - (bob </) + (bob <# "#team alice> hi <muted>") (cath <# "#team alice> hi") bob #> "#team hello" concurrently_ @@ -1214,7 +1582,9 @@ testMuteGroup = cath <# "#team > bob hello" cath <## " hello too!" concurrentlyN_ - [ (bob </), + [ do + bob <# "#team cath!> > bob hello <muted>" + bob <## " hello too! <muted>", do alice <# "#team cath> > bob hello" alice <## " hello too!" @@ -1226,7 +1596,7 @@ testMuteGroup = alice <## " hey bob!" concurrentlyN_ [ do - bob <# "#team alice> > bob hello" + bob <# "#team alice!> > bob hello" bob <## " hey bob!", do cath <# "#team alice> > bob hello" @@ -1236,29 +1606,43 @@ testMuteGroup = alice <# "#team > cath hello too!" alice <## " hey cath!" concurrentlyN_ - [ (bob </), + [ do + bob <# "#team alice> > cath hello too! <muted>" + bob <## " hey cath! <muted>", do - cath <# "#team alice> > cath hello too!" + cath <# "#team alice!> > cath hello too!" cath <## " hey cath!" ] bob ##> "/gs" bob <## "#team (3 members, mentions only, you can /unmute #team)" bob ##> "/unmute #team" bob <## "ok" + + threadDelay 1000000 + alice #> "#team hi again" concurrently_ (bob <# "#team alice> hi again") (cath <# "#team alice> hi again") bob ##> "/block #team alice" + bob <## "admins or above can't block member for self, use /block for all #team alice" + -- can bypass with api + bob ##> "/_member settings #1 1 {\"showMessages\": false}" bob <## "ok" bob ##> "/ms team" bob <## "bob (Bob): admin, you, connected" bob <## "alice (Alice): owner, host, connected, blocked" bob <## "cath (Catherine): admin, connected" + + threadDelay 1000000 + alice #> "#team test 1" concurrently_ - (bob </) + (bob <# "#team alice> test 1 [blocked] <muted>") (cath <# "#team alice> test 1") + + threadDelay 1000000 + cath #> "#team test 2" concurrently_ (bob <# "#team cath> test 2") @@ -1269,6 +1653,9 @@ testMuteGroup = bob <# "#team cath> test 2" threadDelay 1000000 bob ##> "/unblock #team alice" + bob <## "admins or above can't block member for self, use /unblock for all #team alice" + -- can bypass with api + bob ##> "/_member settings #1 1 {\"showMessages\": true}" bob <## "ok" bob ##> "/ms team" bob <## "bob (Bob): admin, you, connected" @@ -1285,7 +1672,7 @@ testMuteGroup = bob ##> "/gs" bob <## "#team (3 members)" -testCreateSecondUser :: HasCallStack => FilePath -> IO () +testCreateSecondUser :: HasCallStack => TestParams -> IO () testCreateSecondUser = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -1336,10 +1723,10 @@ testCreateSecondUser = alice ##> "/_user 2" showActiveUser alice "alisa" -testUsersSubscribeAfterRestart :: HasCallStack => FilePath -> IO () -testUsersSubscribeAfterRestart tmp = do - withNewTestChat tmp "bob" bobProfile $ \bob -> do - withNewTestChat tmp "alice" aliceProfile $ \alice -> do +testUsersSubscribeAfterRestart :: HasCallStack => TestParams -> IO () +testUsersSubscribeAfterRestart ps = do + withNewTestChat ps "bob" bobProfile $ \bob -> do + withNewTestChat ps "alice" aliceProfile $ \alice -> do connectUsers alice bob alice <##> bob @@ -1348,7 +1735,7 @@ testUsersSubscribeAfterRestart tmp = do connectUsers alice bob alice <##> bob - withTestChat tmp "alice" $ \alice -> do + withTestChat ps "alice" $ \alice -> do -- second user is active alice <## "1 contacts connected (use /cs for the list)" alice <## "[user: alice] 1 contacts connected (use /cs for the list)" @@ -1360,7 +1747,7 @@ testUsersSubscribeAfterRestart tmp = do bob #> "@alice hey alice" (alice, "alice") $<# "bob> hey alice" -testMultipleUserAddresses :: HasCallStack => FilePath -> IO () +testMultipleUserAddresses :: HasCallStack => TestParams -> IO () testMultipleUserAddresses = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -1370,7 +1757,7 @@ testMultipleUserAddresses = alice <#? bob alice @@@ [("<@bob", "")] alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -1386,14 +1773,14 @@ testMultipleUserAddresses = cLinkAlisa <- getContactLink alice True bob ##> ("/c " <> cLinkAlisa) alice <#? bob - alice #$> ("/_get chats 2 pcc=on", chats, [("<@bob", "")]) + alice #$> ("/_get chats 2 pcc=on", chats, [("<@bob", ""), ("@SimpleX Chat team", ""), ("@SimpleX-Status", ""), ("*", "")]) alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alisa: contact is connected") (alice <## "bob (Bob): contact is connected") threadDelay 100000 - alice #$> ("/_get chats 2 pcc=on", chats, [("@bob", lastChatFeature)]) + alice #$> ("/_get chats 2 pcc=on", chats, [("@bob", lastChatFeature), ("@SimpleX Chat team", ""), ("@SimpleX-Status", ""), ("*", "")]) alice <##> bob bob #> "@alice hey alice" @@ -1419,12 +1806,12 @@ testMultipleUserAddresses = showActiveUser alice "alisa" alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" concurrently_ (cath <## "alisa: contact is connected") (alice <## "cath (Catherine): contact is connected") threadDelay 100000 - alice #$> ("/_get chats 2 pcc=on", chats, [("@cath", lastChatFeature), ("@bob", "hey")]) + alice #$> ("/_get chats 2 pcc=on", chats, [("@cath", lastChatFeature), ("@bob", "hey"), ("@SimpleX Chat team", ""), ("@SimpleX-Status", ""), ("*", "")]) alice <##> cath -- first user doesn't have cath as contact @@ -1432,8 +1819,8 @@ testMultipleUserAddresses = showActiveUser alice "alice (Alice)" alice @@@ [("@bob", "hey alice")] -testCreateUserDefaultServers :: HasCallStack => FilePath -> IO () -testCreateUserDefaultServers = +testCreateUserSameServers :: HasCallStack => TestParams -> IO () +testCreateUserSameServers = testChat2 aliceProfile bobProfile $ \alice _ -> do alice #$> ("/smp smp://2345-w==@smp2.example.im smp://3456-w==@smp3.example.im:5224", id, "ok") @@ -1443,50 +1830,21 @@ testCreateUserDefaultServers = alice ##> "/create user alisa" showActiveUser alice "alisa" - alice #$> ("/smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001") - alice #$> ("/xftp", id, "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002") - - -- with same_servers=off - alice ##> "/user alice" - showActiveUser alice "alice (Alice)" - checkCustomServers alice - - alice ##> "/create user same_servers=off alisa2" - showActiveUser alice "alisa2" - - alice #$> ("/smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001") - alice #$> ("/xftp", id, "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002") - where - checkCustomServers alice = do - alice ##> "/smp" - alice <## "smp://2345-w==@smp2.example.im" - alice <## "smp://3456-w==@smp3.example.im:5224" - alice ##> "/xftp" - alice <## "xftp://2345-w==@xftp2.example.im" - alice <## "xftp://3456-w==@xftp3.example.im:5224" - -testCreateUserSameServers :: HasCallStack => FilePath -> IO () -testCreateUserSameServers = - testChat2 aliceProfile bobProfile $ - \alice _ -> do - alice #$> ("/smp smp://2345-w==@smp2.example.im smp://3456-w==@smp3.example.im:5224", id, "ok") - alice #$> ("/xftp xftp://2345-w==@xftp2.example.im xftp://3456-w==@xftp3.example.im:5224", id, "ok") - checkCustomServers alice - - alice ##> "/create user same_servers=on alisa" - showActiveUser alice "alisa" - checkCustomServers alice where checkCustomServers alice = do alice ##> "/smp" - alice <## "smp://2345-w==@smp2.example.im" - alice <## "smp://3456-w==@smp3.example.im:5224" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://2345-w==@smp2.example.im" + alice <## " smp://3456-w==@smp3.example.im:5224" alice ##> "/xftp" - alice <## "xftp://2345-w==@xftp2.example.im" - alice <## "xftp://3456-w==@xftp3.example.im:5224" + alice <## "Your servers" + alice <## " XFTP servers" + alice <## " xftp://2345-w==@xftp2.example.im" + alice <## " xftp://3456-w==@xftp3.example.im:5224" -testDeleteUser :: HasCallStack => FilePath -> IO () +testDeleteUser :: HasCallStack => TestParams -> IO () testDeleteUser = testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do @@ -1559,11 +1917,24 @@ testDeleteUser = alice ##> "/create user alisa3" showActiveUser alice "alisa3" + alice ##> "/delete user alisa3 del_smp=on" + alice <### ["ok", "completed deleting user"] + alice ##> "/users" + alice <## "no users" -testUsersDifferentCIExpirationTTL :: HasCallStack => FilePath -> IO () -testUsersDifferentCIExpirationTTL tmp = do - withNewTestChat tmp "bob" bobProfile $ \bob -> do - withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do + alice ##> "/create user alisa4" + showActiveUser alice "alisa4" + connectUsers alice bob + alice <##> bob + alice ##> "/delete user alisa4 del_smp=on" + alice <### ["ok", "completed deleting user"] + alice ##> "/users" + alice <## "no users" + +testUsersDifferentCIExpirationTTL :: HasCallStack => TestParams -> IO () +testUsersDifferentCIExpirationTTL ps = do + withNewTestChat ps "bob" bobProfile $ \bob -> do + withNewTestChatCfg ps cfg "alice" aliceProfile $ \alice -> do -- first user messages connectUsers alice bob @@ -1585,17 +1956,17 @@ testUsersDifferentCIExpirationTTL tmp = do -- set ttl for first user alice ##> "/user alice" showActiveUser alice "alice (Alice)" - alice #$> ("/_ttl 1 1", id, "ok") + alice #$> ("/_ttl 1 2", id, "ok") -- set ttl for second user alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_ttl 2 3", id, "ok") + alice #$> ("/_ttl 2 4", id, "ok") -- first user messages alice ##> "/user alice" showActiveUser alice "alice (Alice)" - alice #$> ("/ttl", id, "old messages are set to be deleted after: 1 second(s)") + alice #$> ("/ttl", id, "old messages are set to be deleted after: 2 second(s)") alice #> "@bob alice 3" bob <# "alice> alice 3" @@ -1607,16 +1978,16 @@ testUsersDifferentCIExpirationTTL tmp = do -- second user messages alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/ttl", id, "old messages are set to be deleted after: 3 second(s)") + alice #$> ("/ttl", id, "old messages are set to be deleted after: 4 second(s)") alice #> "@bob alisa 3" bob <# "alisa> alisa 3" bob #> "@alisa alisa 4" alice <# "bob> alisa 4" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) - threadDelay 2000000 + threadDelay 3000000 -- messages both before and after setting chat item ttl are deleted -- first user messages @@ -1627,18 +1998,18 @@ testUsersDifferentCIExpirationTTL tmp = do -- second user messages alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 2000000 - alice #$> ("/_get chat @4 count=100", chat, []) + alice #$> ("/_get chat @6 count=100", chat, []) where cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000} -testUsersRestartCIExpiration :: HasCallStack => FilePath -> IO () -testUsersRestartCIExpiration tmp = do - withNewTestChat tmp "bob" bobProfile $ \bob -> do - withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do +testUsersRestartCIExpiration :: HasCallStack => TestParams -> IO () +testUsersRestartCIExpiration ps = do + withNewTestChat ps "bob" bobProfile $ \bob -> do + withNewTestChatCfg ps cfg "alice" aliceProfile $ \alice -> do -- set ttl for first user alice #$> ("/_ttl 1 2", id, "ok") connectUsers alice bob @@ -1671,7 +2042,7 @@ testUsersRestartCIExpiration tmp = do alice ##> "/user alice" showActiveUser alice "alice (Alice)" - withTestChatCfg tmp cfg "alice" $ \alice -> do + withTestChatCfg ps cfg "alice" $ \alice -> do alice <## "1 contacts connected (use /cs for the list)" alice <## "[user: alisa] 1 contacts connected (use /cs for the list)" @@ -1697,7 +2068,7 @@ testUsersRestartCIExpiration tmp = do bob #> "@alisa alisa 4" alice <# "bob> alisa 4" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 3000000 @@ -1710,18 +2081,18 @@ testUsersRestartCIExpiration tmp = do -- second user messages alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) - threadDelay 3000000 + threadDelay 4000000 - alice #$> ("/_get chat @4 count=100", chat, []) + alice #$> ("/_get chat @6 count=100", chat, []) where cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000} -testEnableCIExpirationOnlyForOneUser :: HasCallStack => FilePath -> IO () -testEnableCIExpirationOnlyForOneUser tmp = do - withNewTestChat tmp "bob" bobProfile $ \bob -> do - withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do +testEnableCIExpirationOnlyForOneUser :: HasCallStack => TestParams -> IO () +testEnableCIExpirationOnlyForOneUser ps = do + withNewTestChat ps "bob" bobProfile $ \bob -> do + withNewTestChatCfg ps cfg "alice" aliceProfile $ \alice -> do -- first user messages connectUsers alice bob @@ -1756,7 +2127,7 @@ testEnableCIExpirationOnlyForOneUser tmp = do bob #> "@alisa alisa 4" alice <# "bob> alisa 4" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 2000000 @@ -1768,14 +2139,14 @@ testEnableCIExpirationOnlyForOneUser tmp = do -- messages are not deleted for second user alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) - withTestChatCfg tmp cfg "alice" $ \alice -> do + withTestChatCfg ps cfg "alice" $ \alice -> do alice <## "1 contacts connected (use /cs for the list)" alice <## "[user: alice] 1 contacts connected (use /cs for the list)" -- messages are not deleted for second user after restart - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) alice #> "@bob alisa 5" bob <# "alisa> alisa 5" @@ -1785,14 +2156,14 @@ testEnableCIExpirationOnlyForOneUser tmp = do threadDelay 2000000 -- new messages are not deleted for second user - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4"), (1, "alisa 5"), (0, "alisa 6")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4"), (1, "alisa 5"), (0, "alisa 6")]) where cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000} -testDisableCIExpirationOnlyForOneUser :: HasCallStack => FilePath -> IO () -testDisableCIExpirationOnlyForOneUser tmp = do - withNewTestChat tmp "bob" bobProfile $ \bob -> do - withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do +testDisableCIExpirationOnlyForOneUser :: HasCallStack => TestParams -> IO () +testDisableCIExpirationOnlyForOneUser ps = do + withNewTestChat ps "bob" bobProfile $ \bob -> do + withNewTestChatCfg ps cfg "alice" aliceProfile $ \alice -> do -- set ttl for first user alice #$> ("/_ttl 1 1", id, "ok") connectUsers alice bob @@ -1819,14 +2190,14 @@ testDisableCIExpirationOnlyForOneUser tmp = do bob #> "@alisa alisa 2" alice <# "bob> alisa 2" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2")]) threadDelay 2000000 -- second user messages are deleted - alice #$> ("/_get chat @4 count=100", chat, []) + alice #$> ("/_get chat @6 count=100", chat, []) - withTestChatCfg tmp cfg "alice" $ \alice -> do + withTestChatCfg ps cfg "alice" $ \alice -> do alice <## "1 contacts connected (use /cs for the list)" alice <## "[user: alice] 1 contacts connected (use /cs for the list)" @@ -1838,27 +2209,27 @@ testDisableCIExpirationOnlyForOneUser tmp = do bob #> "@alisa alisa 4" alice <# "bob> alisa 4" - alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")]) threadDelay 2000000 -- second user messages are deleted - alice #$> ("/_get chat @4 count=100", chat, []) + alice #$> ("/_get chat @6 count=100", chat, []) where cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000} -testUsersTimedMessages :: HasCallStack => FilePath -> IO () -testUsersTimedMessages tmp = do - withNewTestChat tmp "bob" bobProfile $ \bob -> do - withNewTestChat tmp "alice" aliceProfile $ \alice -> do +testUsersTimedMessages :: HasCallStack => TestParams -> IO () +testUsersTimedMessages ps = do + withNewTestChat ps "bob" bobProfile $ \bob -> do + withNewTestChat ps "alice" aliceProfile $ \alice -> do connectUsers alice bob - configureTimedMessages alice bob "2" "1" + configureTimedMessages alice bob "2" "2" -- create second user and configure timed messages for contact alice ##> "/create user alisa" showActiveUser alice "alisa" connectUsers alice bob - configureTimedMessages alice bob "4" "2" + configureTimedMessages alice bob "6" "3" -- first user messages alice ##> "/user alice" @@ -1879,7 +2250,7 @@ testUsersTimedMessages tmp = do alice <# "bob> alisa 2" -- messages are deleted after ttl - threadDelay 500000 + threadDelay 1500000 alice ##> "/user alice" showActiveUser alice "alice (Alice)" @@ -1887,7 +2258,7 @@ testUsersTimedMessages tmp = do alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 1"), (0, "alisa 2")]) + alice #$> ("/_get chat @6 count=100", chat, [(1, "alisa 1"), (0, "alisa 2")]) threadDelay 1000000 @@ -1902,7 +2273,7 @@ testUsersTimedMessages tmp = do alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 1"), (0, "alisa 2")]) + alice #$> ("/_get chat @6 count=100", chat, [(1, "alisa 1"), (0, "alisa 2")]) threadDelay 1000000 @@ -1913,7 +2284,7 @@ testUsersTimedMessages tmp = do alice ##> "/user" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, []) + alice #$> ("/_get chat @6 count=100", chat, []) -- first user messages alice ##> "/user alice" @@ -1933,7 +2304,7 @@ testUsersTimedMessages tmp = do bob #> "@alisa alisa 4" alice <# "bob> alisa 4" - withTestChat tmp "alice" $ \alice -> do + withTestChat ps "alice" $ \alice -> do alice <## "1 contacts connected (use /cs for the list)" alice <## "[user: alice] 1 contacts connected (use /cs for the list)" @@ -1943,7 +2314,7 @@ testUsersTimedMessages tmp = do alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")]) -- messages are deleted after restart threadDelay 1000000 @@ -1959,7 +2330,7 @@ testUsersTimedMessages tmp = do alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")]) threadDelay 1000000 @@ -1970,7 +2341,7 @@ testUsersTimedMessages tmp = do alice ##> "/user" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, []) + alice #$> ("/_get chat @6 count=100", chat, []) where configureTimedMessages :: HasCallStack => TestCC -> TestCC -> String -> String -> IO () configureTimedMessages alice bob bobId ttl = do @@ -1982,7 +2353,7 @@ testUsersTimedMessages tmp = do bob <## ("Disappearing messages: enabled (you allow: yes (" <> ttl <> " sec), contact allows: yes (" <> ttl <> " sec))") alice #$> ("/clear bob", id, "bob: all messages are removed locally ONLY") -- to remove feature items -testUserPrivacy :: HasCallStack => FilePath -> IO () +testUserPrivacy :: HasCallStack => TestParams -> IO () testUserPrivacy = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -1997,6 +2368,8 @@ testUserPrivacy = bob <# "alisa> hello" bob #> "@alisa hey" alice <# "bob> hey" + bob #> "@alice hey" + (alice, "[user: alice] ") ^<# "bob> hey" -- hide user profile alice ##> "/hide user my_password" userHidden alice "current " @@ -2028,15 +2401,24 @@ testUserPrivacy = alice <##? chatHistory alice ##> "/_get items count=10" alice <##? chatHistory - alice ##> "/_get items before=11 count=10" + alice ##> "/_get items before=13 count=10" alice - <##? [ "bob> Disappearing messages: allowed", + <##? [ ConsoleString ("bob> " <> e2eeInfoPQStr), + "bob> Disappearing messages: allowed", "bob> Full deletion: off", "bob> Message reactions: enabled", "bob> Voice messages: enabled", "bob> Audio/video calls: enabled" ] - alice ##> "/_get items after=10 count=10" + alice ##> "/_get items around=11 count=2" + alice + <##? [ "bob> Full deletion: off", + "bob> Message reactions: enabled", + "bob> Voice messages: enabled", + "bob> Audio/video calls: enabled", + "@bob hello" + ] + alice ##> "/_get items after=12 count=10" alice <##? [ "@bob hello", "bob> hey", @@ -2100,7 +2482,8 @@ testUserPrivacy = alice <## "messages are shown" alice <## "profile is visible" chatHistory = - [ "bob> Disappearing messages: allowed", + [ ConsoleString ("bob> " <> e2eeInfoPQStr), + "bob> Disappearing messages: allowed", "bob> Full deletion: off", "bob> Message reactions: enabled", "bob> Voice messages: enabled", @@ -2111,7 +2494,7 @@ testUserPrivacy = "bob> this won't show" ] -testSetChatItemTTL :: HasCallStack => FilePath -> IO () +testSetChatItemTTL :: HasCallStack => TestParams -> IO () testSetChatItemTTL = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -2123,7 +2506,7 @@ testSetChatItemTTL = -- chat item with file alice #$> ("/_files_folder ./tests/tmp/app_files", id, "ok") copyFile "./tests/fixtures/test.jpg" "./tests/tmp/app_files/test.jpg" - alice ##> "/_send @2 json {\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" + alice ##> "/_send @2 json [{\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]" alice <# "/f @bob test.jpg" alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" @@ -2145,7 +2528,101 @@ testSetChatItemTTL = alice #$> ("/ttl none", id, "ok") alice #$> ("/ttl", id, "old messages are not being deleted") -testSwitchContact :: HasCallStack => FilePath -> IO () +testSetDirectChatTTL :: HasCallStack => TestParams -> IO () +testSetDirectChatTTL = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + connectUsers alice bob + connectUsers alice cath + alice #> "@bob 1" + bob <# "alice> 1" + bob #> "@alice 2" + alice <# "bob> 2" + -- above items should be deleted after we set ttl + alice #> "@cath 10" + cath <# "alice> 10" + cath #> "@alice 11" + alice <# "cath> 11" + alice #$> ("/ttl @cath none", id, "ok") + alice #$> ("/ttl @cath", id, "old messages are not being deleted") + + threadDelay 3000000 + alice #> "@bob 3" + bob <# "alice> 3" + bob #> "@alice 4" + alice <# "bob> 4" + alice #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((1, "1"), Nothing), ((0, "2"), Nothing), ((1, "3"), Nothing), ((0, "4"), Nothing)]) + alice #$> ("/_ttl 1 2", id, "ok") + -- when expiration is turned on, first cycle is synchronous + alice #$> ("/_get chat @2 count=100", chat, [(1, "3"), (0, "4")]) + + -- chat @3 doesn't expire since it was set to not expire + alice #$> ("/_get chat @3 count=100", chat, chatFeatures <> [(1, "10"), (0, "11")]) + bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "1"), (1, "2"), (0, "3"), (1, "4")]) + + -- remove global ttl + alice #$> ("/ttl none", id, "ok") + alice #> "@bob 5" + bob <# "alice> 5" + bob #> "@alice 6" + alice <# "bob> 6" + alice #$> ("/_get chat @3 count=100", chat, chatFeatures <> [(1, "10"), (0, "11")]) + alice #$> ("/_get chat @2 count=100", chat, [(1, "3"), (0, "4"), (1, "5"), (0, "6")]) + + -- set ttl for chat @3, only chat @3 is affected since global ttl is disabled + alice #$> ("/_ttl 1 @3 1", id, "ok") + alice #$> ("/ttl @cath", id, "old messages are set to be deleted after: 1 second(s)") + threadDelay 3000000 + alice #$> ("/_get chat @3 count=100", chat, []) + alice #$> ("/_get chat @2 count=100", chat, [(1, "3"), (0, "4"), (1, "5"), (0, "6")]) + bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "1"), (1, "2"), (0, "3"), (1, "4"), (0, "5"), (1, "6")]) + + -- set ttl to never expire again + alice #$> ("/ttl @cath none", id, "ok") + alice #> "@cath 12" + cath <# "alice> 12" + cath #> "@alice 13" + alice <# "cath> 13" + threadDelay 3000000 + alice #$> ("/_get chat @3 count=100", chat, [(1, "12"), (0, "13")]) + alice #$> ("/_get chat @2 count=100", chat, [(1, "3"), (0, "4"), (1, "5"), (0, "6")]) + bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "1"), (1, "2"), (0, "3"), (1, "4"), (0, "5"), (1, "6")]) + + -- set ttl back to default + alice #$> ("/ttl @cath default", id, "ok") + alice #$> ("/ttl @cath", id, "old messages are set to delete according to default user config") + alice #$> ("/_ttl 1 2", id, "ok") + alice #$> ("/_get chat @3 count=100", chat, []) + alice #$> ("/_get chat @2 count=100", chat, []) + + alice #$> ("/ttl @cath day", id, "ok") + alice #$> ("/ttl @cath", id, "old messages are set to be deleted after: one day") + alice #$> ("/ttl @cath week", id, "ok") + alice #$> ("/ttl @cath", id, "old messages are set to be deleted after: one week") + alice #$> ("/ttl @cath month", id, "ok") + alice #$> ("/ttl @cath", id, "old messages are set to be deleted after: one month") + alice #$> ("/ttl @cath year", id, "ok") + alice #$> ("/ttl @cath", id, "old messages are set to be deleted after: one year") + +testAppSettings :: HasCallStack => TestParams -> IO () +testAppSettings ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + let settings = T.unpack . safeDecodeUtf8 . LB.toStrict $ J.encode defaultAppSettings + settingsApp = T.unpack . safeDecodeUtf8 . LB.toStrict $ J.encode defaultAppSettings {AS.webrtcICEServers = Just ["non-default.value.com"]} + -- app-provided defaults + alice ##> ("/_get app settings " <> settingsApp) + alice <## ("app settings: " <> settingsApp) + -- parser defaults fallback + alice ##> "/_get app settings" + alice <## ("app settings: " <> settings) + -- store + alice ##> ("/_save app settings " <> settingsApp) + alice <## "ok" + -- read back + alice ##> "/_get app settings" + alice <## ("app settings: " <> settingsApp) + +testSwitchContact :: HasCallStack => TestParams -> IO () testSwitchContact = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -2155,26 +2632,27 @@ testSwitchContact = alice <## "bob: you started changing address" bob <## "alice changed address for you" alice <## "bob: you changed address" + threadDelay 100000 alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "started changing address..."), (1, "you changed address")]) bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "started changing address for you..."), (0, "changed address for you")]) alice <##> bob -testAbortSwitchContact :: HasCallStack => FilePath -> IO () -testAbortSwitchContact tmp = do - withNewTestChat tmp "alice" aliceProfile $ \alice -> do - withNewTestChat tmp "bob" bobProfile $ \bob -> do +testAbortSwitchContact :: HasCallStack => TestParams -> IO () +testAbortSwitchContact ps = do + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do connectUsers alice bob alice #$> ("/switch bob", id, "switch started") alice <## "bob: you started changing address" -- repeat switch is prohibited alice ##> "/switch bob" - alice <## "error: command is prohibited" + alice <## "error: command is prohibited, switchConnectionAsync: already switching" -- stop switch alice #$> ("/abort switch bob", id, "switch aborted") -- repeat switch stop is prohibited alice ##> "/abort switch bob" - alice <## "error: command is prohibited" - withTestChatContactConnected tmp "bob" $ \bob -> do + alice <## "error: command is prohibited, abortConnectionSwitch: not allowed" + withTestChatContactConnected ps "bob" $ \bob -> do bob <## "alice started changing address for you" -- alice changes address again alice #$> ("/switch bob", id, "switch started") @@ -2182,11 +2660,12 @@ testAbortSwitchContact tmp = do bob <## "alice started changing address for you" bob <## "alice changed address for you" alice <## "bob: you changed address" + threadDelay 100000 alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "started changing address..."), (1, "started changing address..."), (1, "you changed address")]) bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "started changing address for you..."), (0, "started changing address for you..."), (0, "changed address for you")]) alice <##> bob -testSwitchGroupMember :: HasCallStack => FilePath -> IO () +testSwitchGroupMember :: HasCallStack => TestParams -> IO () testSwitchGroupMember = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -2196,29 +2675,30 @@ testSwitchGroupMember = alice <## "#team: you started changing address for bob" bob <## "#team: alice changed address for you" alice <## "#team: you changed address for bob" - alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "started changing address for bob..."), (1, "you changed address for bob")]) + threadDelay 100000 + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "started changing address for bob..."), (1, "you changed address for bob")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "started changing address for you..."), (0, "changed address for you")]) alice #> "#team hey" bob <# "#team alice> hey" bob #> "#team hi" alice <# "#team bob> hi" -testAbortSwitchGroupMember :: HasCallStack => FilePath -> IO () -testAbortSwitchGroupMember tmp = do - withNewTestChat tmp "alice" aliceProfile $ \alice -> do - withNewTestChat tmp "bob" bobProfile $ \bob -> do +testAbortSwitchGroupMember :: HasCallStack => TestParams -> IO () +testAbortSwitchGroupMember ps = do + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do createGroup2 "team" alice bob alice #$> ("/switch #team bob", id, "switch started") alice <## "#team: you started changing address for bob" -- repeat switch is prohibited alice ##> "/switch #team bob" - alice <## "error: command is prohibited" + alice <## "error: command is prohibited, switchConnectionAsync: already switching" -- stop switch alice #$> ("/abort switch #team bob", id, "switch aborted") -- repeat switch stop is prohibited alice ##> "/abort switch #team bob" - alice <## "error: command is prohibited" - withTestChatContactConnected tmp "bob" $ \bob -> do + alice <## "error: command is prohibited, abortConnectionSwitch: not allowed" + withTestChatContactConnected ps "bob" $ \bob -> do bob <## "#team: connected to server(s)" bob <## "#team: alice started changing address for you" -- alice changes address again @@ -2227,14 +2707,15 @@ testAbortSwitchGroupMember tmp = do bob <## "#team: alice started changing address for you" bob <## "#team: alice changed address for you" alice <## "#team: you changed address for bob" - alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "started changing address for bob..."), (1, "started changing address for bob..."), (1, "you changed address for bob")]) + threadDelay 100000 + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "started changing address for bob..."), (1, "started changing address for bob..."), (1, "you changed address for bob")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "started changing address for you..."), (0, "started changing address for you..."), (0, "changed address for you")]) alice #> "#team hey" bob <# "#team alice> hey" bob #> "#team hi" alice <# "#team bob> hi" -testMarkContactVerified :: HasCallStack => FilePath -> IO () +testMarkContactVerified :: HasCallStack => TestParams -> IO () testMarkContactVerified = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob @@ -2263,13 +2744,14 @@ testMarkContactVerified = alice <## "sending messages via: localhost" alice <## "you've shared main profile with this contact" alice <## connVerified + alice <## "quantum resistant end-to-end encryption" alice <## currentChatVRangeInfo where connVerified | verified = "connection verified" | otherwise = "connection not verified, use /code command to see security code" -testMarkGroupMemberVerified :: HasCallStack => FilePath -> IO () +testMarkGroupMemberVerified :: HasCallStack => TestParams -> IO () testMarkGroupMemberVerified = testChat2 aliceProfile bobProfile $ \alice bob -> do createGroup2 "team" alice bob @@ -2304,28 +2786,29 @@ testMarkGroupMemberVerified = | verified = "connection verified" | otherwise = "connection not verified, use /code command to see security code" -testMsgDecryptError :: HasCallStack => FilePath -> IO () -testMsgDecryptError tmp = - withNewTestChat tmp "alice" aliceProfile $ \alice -> do - withNewTestChat tmp "bob" bobProfile $ \bob -> do +#if !defined(dbPostgres) +testMsgDecryptError :: HasCallStack => TestParams -> IO () +testMsgDecryptError ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do connectUsers alice bob alice #> "@bob hi" bob <# "alice> hi" bob #> "@alice hey" alice <# "bob> hey" - setupDesynchronizedRatchet tmp alice - withTestChat tmp "bob" $ \bob -> do + setupDesynchronizedRatchet ps alice + withTestChat ps "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" alice #> "@bob hello again" - bob <# "alice> skipped message ID 10..12" + bob <# "alice> skipped message ID 9..11" bob <# "alice> hello again" bob #> "@alice received!" alice <# "bob> received!" -setupDesynchronizedRatchet :: HasCallStack => FilePath -> TestCC -> IO () -setupDesynchronizedRatchet tmp alice = do +setupDesynchronizedRatchet :: HasCallStack => TestParams -> TestCC -> IO () +setupDesynchronizedRatchet ps alice = do copyDb "bob" "bob_old" - withTestChat tmp "bob" $ \bob -> do + withTestChat ps "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" alice #> "@bob 1" bob <# "alice> 1" @@ -2336,10 +2819,10 @@ setupDesynchronizedRatchet tmp alice = do bob #> "@alice 4" alice <# "bob> 4" threadDelay 500000 - withTestChat tmp "bob_old" $ \bob -> do + withTestChat ps "bob_old" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob ##> "/sync alice" - bob <## "error: command is prohibited" + bob <## "error: command is prohibited, synchronizeRatchet: not allowed" alice #> "@bob 1" bob <## "alice: decryption error (connection out of sync), synchronization required" bob <## "use /sync alice to synchronize" @@ -2349,24 +2832,25 @@ setupDesynchronizedRatchet tmp alice = do bob ##> "/tail @alice 1" bob <# "alice> decryption error, possibly due to the device change (header, 3 messages)" bob ##> "@alice 1" - bob <## "error: command is prohibited" + bob <## "error: command is prohibited, sendMessagesB: send prohibited" (alice </) where + tmp = tmpPath ps copyDb from to = do - copyFile (chatStoreFile $ tmp </> from) (chatStoreFile $ tmp </> to) - copyFile (agentStoreFile $ tmp </> from) (agentStoreFile $ tmp </> to) + copyFile (tmp </> (from <> chatSuffix)) (tmp </> (to <> chatSuffix)) + copyFile (tmp </> (from <> agentSuffix)) (tmp </> (to <> agentSuffix)) -testSyncRatchet :: HasCallStack => FilePath -> IO () -testSyncRatchet tmp = - withNewTestChat tmp "alice" aliceProfile $ \alice -> do - withNewTestChat tmp "bob" bobProfile $ \bob -> do +testSyncRatchet :: HasCallStack => TestParams -> IO () +testSyncRatchet ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do connectUsers alice bob alice #> "@bob hi" bob <# "alice> hi" bob #> "@alice hey" alice <# "bob> hey" - setupDesynchronizedRatchet tmp alice - withTestChat tmp "bob_old" $ \bob -> do + setupDesynchronizedRatchet ps alice + withTestChat ps "bob_old" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob ##> "/sync alice" bob <## "connection synchronization started" @@ -2375,6 +2859,7 @@ testSyncRatchet tmp = alice <## "bob: connection synchronized" bob <## "alice: connection synchronized" + threadDelay 100000 bob #$> ("/_get chat @2 count=3", chat, [(1, "connection synchronization started"), (0, "connection synchronization agreed"), (0, "connection synchronized")]) alice #$> ("/_get chat @2 count=2", chat, [(0, "connection synchronization agreed"), (0, "connection synchronized")]) @@ -2383,10 +2868,10 @@ testSyncRatchet tmp = bob #> "@alice received!" alice <# "bob> received!" -testSyncRatchetCodeReset :: HasCallStack => FilePath -> IO () -testSyncRatchetCodeReset tmp = - withNewTestChat tmp "alice" aliceProfile $ \alice -> do - withNewTestChat tmp "bob" bobProfile $ \bob -> do +testSyncRatchetCodeReset :: HasCallStack => TestParams -> IO () +testSyncRatchetCodeReset ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do connectUsers alice bob alice #> "@bob hi" bob <# "alice> hi" @@ -2403,8 +2888,8 @@ testSyncRatchetCodeReset tmp = -- connection verified bob ##> "/i alice" aliceInfo bob True - setupDesynchronizedRatchet tmp alice - withTestChat tmp "bob_old" $ \bob -> do + setupDesynchronizedRatchet ps alice + withTestChat ps "bob_old" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob ##> "/sync alice" bob <## "connection synchronization started" @@ -2414,6 +2899,7 @@ testSyncRatchetCodeReset tmp = alice <## "bob: connection synchronized" bob <## "alice: connection synchronized" + threadDelay 100000 bob #$> ("/_get chat @2 count=4", chat, [(1, "connection synchronization started"), (0, "connection synchronization agreed"), (0, "security code changed"), (0, "connection synchronized")]) alice #$> ("/_get chat @2 count=2", chat, [(0, "connection synchronization agreed"), (0, "connection synchronized")]) @@ -2433,13 +2919,15 @@ testSyncRatchetCodeReset tmp = bob <## "sending messages via: localhost" bob <## "you've shared main profile with this contact" bob <## connVerified + bob <## "quantum resistant end-to-end encryption" bob <## currentChatVRangeInfo where connVerified | verified = "connection verified" | otherwise = "connection not verified, use /code command to see security code" +#endif -testSetMessageReactions :: HasCallStack => FilePath -> IO () +testSetMessageReactions :: HasCallStack => TestParams -> IO () testSetMessageReactions = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -2487,10 +2975,10 @@ testSetMessageReactions = bob <# "alice> hi" bob <## " 👍 1" -testSendDeliveryReceipts :: HasCallStack => FilePath -> IO () -testSendDeliveryReceipts tmp = - withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do - withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do +testSendDeliveryReceipts :: HasCallStack => TestParams -> IO () +testSendDeliveryReceipts ps = + withNewTestChatCfg ps cfg "alice" aliceProfile $ \alice -> do + withNewTestChatCfg ps cfg "bob" bobProfile $ \bob -> do connectUsers alice bob alice #> "@bob hi" @@ -2503,11 +2991,11 @@ testSendDeliveryReceipts tmp = where cfg = testCfg {showReceipts = True} -testConfigureDeliveryReceipts :: HasCallStack => FilePath -> IO () -testConfigureDeliveryReceipts tmp = - withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do - withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do - withNewTestChatCfg tmp cfg "cath" cathProfile $ \cath -> do +testConfigureDeliveryReceipts :: HasCallStack => TestParams -> IO () +testConfigureDeliveryReceipts ps = + withNewTestChatCfg ps cfg "alice" aliceProfile $ \alice -> do + withNewTestChatCfg ps cfg "bob" bobProfile $ \bob -> do + withNewTestChatCfg ps cfg "cath" cathProfile $ \cath -> do connectUsers alice bob connectUsers alice cath @@ -2581,10 +3069,10 @@ testConfigureDeliveryReceipts tmp = cc2 <# (name1 <> "> " <> msg) cc1 <// 50000 -testConnInvChatVRange :: HasCallStack => VersionRange -> VersionRange -> FilePath -> IO () -testConnInvChatVRange ct1VRange ct2VRange tmp = - withNewTestChatCfg tmp testCfg {chatVRange = ct1VRange} "alice" aliceProfile $ \alice -> do - withNewTestChatCfg tmp testCfg {chatVRange = ct2VRange} "bob" bobProfile $ \bob -> do +testConnInvChatVRange :: HasCallStack => VersionRangeChat -> VersionRangeChat -> TestParams -> IO () +testConnInvChatVRange ct1VRange ct2VRange ps = + withNewTestChatCfg ps testCfg {chatVRange = ct1VRange} "alice" aliceProfile $ \alice -> do + withNewTestChatCfg ps testCfg {chatVRange = ct2VRange} "bob" bobProfile $ \bob -> do connectUsers alice bob alice ##> "/i bob" @@ -2593,16 +3081,16 @@ testConnInvChatVRange ct1VRange ct2VRange tmp = bob ##> "/i alice" contactInfoChatVRange bob ct1VRange -testConnReqChatVRange :: HasCallStack => VersionRange -> VersionRange -> FilePath -> IO () -testConnReqChatVRange ct1VRange ct2VRange tmp = - withNewTestChatCfg tmp testCfg {chatVRange = ct1VRange} "alice" aliceProfile $ \alice -> do - withNewTestChatCfg tmp testCfg {chatVRange = ct2VRange} "bob" bobProfile $ \bob -> do +testConnReqChatVRange :: HasCallStack => VersionRangeChat -> VersionRangeChat -> TestParams -> IO () +testConnReqChatVRange ct1VRange ct2VRange ps = + withNewTestChatCfg ps testCfg {chatVRange = ct1VRange} "alice" aliceProfile $ \alice -> do + withNewTestChatCfg ps testCfg {chatVRange = ct2VRange} "bob" bobProfile $ \bob -> do alice ##> "/ad" cLink <- getContactLink alice True bob ##> ("/c " <> cLink) alice <#? bob alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -2613,10 +3101,10 @@ testConnReqChatVRange ct1VRange ct2VRange tmp = bob ##> "/i alice" contactInfoChatVRange bob ct1VRange -testUpdatePeerChatVRange :: HasCallStack => FilePath -> IO () -testUpdatePeerChatVRange tmp = - withNewTestChat tmp "alice" aliceProfile $ \alice -> do - withNewTestChatCfg tmp cfg11 "bob" bobProfile $ \bob -> do +testUpdatePeerChatVRange :: HasCallStack => TestParams -> IO () +testUpdatePeerChatVRange ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChatCfg ps cfg11 "bob" bobProfile $ \bob -> do connectUsers alice bob alice ##> "/i bob" @@ -2625,7 +3113,7 @@ testUpdatePeerChatVRange tmp = bob ##> "/i alice" contactInfoChatVRange bob supportedChatVRange - withTestChat tmp "bob" $ \bob -> do + withTestChat ps "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob #> "@alice hello 1" @@ -2637,7 +3125,7 @@ testUpdatePeerChatVRange tmp = bob ##> "/i alice" contactInfoChatVRange bob supportedChatVRange - withTestChatCfg tmp cfg11 "bob" $ \bob -> do + withTestChatCfg ps cfg11 "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob #> "@alice hello 2" @@ -2651,28 +3139,29 @@ testUpdatePeerChatVRange tmp = where cfg11 = testCfg {chatVRange = vr11} :: ChatConfig -testGetNetworkStatuses :: HasCallStack => FilePath -> IO () -testGetNetworkStatuses tmp = do - withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do - withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do +testGetNetworkStatuses :: HasCallStack => TestParams -> IO () +testGetNetworkStatuses ps = do + withNewTestChatCfg ps cfg "alice" aliceProfile $ \alice -> do + withNewTestChatCfg ps cfg "bob" bobProfile $ \bob -> do connectUsers alice bob alice ##> "/_network_statuses" alice <## "1 connections connected" - withTestChatCfg tmp cfg "alice" $ \alice -> - withTestChatCfg tmp cfg "bob" $ \bob -> do + withTestChatCfg ps cfg "alice" $ \alice -> + withTestChatCfg ps cfg "bob" $ \bob -> do alice <## "1 connections connected" bob <## "1 connections connected" where cfg = testCfg {coreApi = True} -vr11 :: VersionRange -vr11 = mkVersionRange 1 1 +vr11 :: VersionRangeChat +vr11 = mkVersionRange (VersionChat 1) (VersionChat 1) -contactInfoChatVRange :: TestCC -> VersionRange -> IO () +contactInfoChatVRange :: TestCC -> VersionRangeChat -> IO () contactInfoChatVRange cc (VersionRange minVer maxVer) = do cc <## "contact ID: 2" cc <## "receiving messages via: localhost" cc <## "sending messages via: localhost" cc <## "you've shared main profile with this contact" cc <## "connection not verified, use /code command to see security code" + cc <## "quantum resistant end-to-end encryption" cc <## ("peer chat protocol version range: (" <> show minVer <> ", " <> show maxVer <> ")") diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs index b987bbf5e0..c573a1957a 100644 --- a/tests/ChatTests/Files.hs +++ b/tests/ChatTests/Files.hs @@ -6,61 +6,40 @@ module ChatTests.Files where import ChatClient +import ChatTests.DBUtils import ChatTests.Utils import Control.Concurrent (threadDelay) import Control.Concurrent.Async (concurrently_) +import Control.Logger.Simple import qualified Data.Aeson as J import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB -import Simplex.Chat (roundedFDCount) -import Simplex.Chat.Controller (ChatConfig (..), InlineFilesConfig (..), XFTPFileConfig (..), defaultInlineFilesConfig) +import Network.HTTP.Types.URI (urlEncode) +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.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Util (unlessM) import System.Directory (copyFile, createDirectoryIfMissing, doesFileExist, getFileSize) -import Test.Hspec +import Test.Hspec hiding (it) -chatFileTests :: SpecWith FilePath +chatFileTests :: SpecWith TestParams chatFileTests = do - describe "sending and receiving files" $ do - describe "send and receive file" $ fileTestMatrix2 runTestFileTransfer - describe "send file, receive and locally encrypt file" $ fileTestMatrix2 runTestFileTransferEncrypted - it "send and receive file inline (without accepting)" testInlineFileTransfer - it "send inline file, receive (without accepting) and locally encrypt" testInlineFileTransferEncrypted - xit'' "accept inline file transfer, sender cancels during transfer" testAcceptInlineFileSndCancelDuringTransfer - it "send and receive small file inline (default config)" testSmallInlineFileTransfer - it "small file sent without acceptance is ignored in terminal by default" testSmallInlineFileIgnored - it "receive file inline with inline=on option" testReceiveInline - describe "send and receive a small file" $ fileTestMatrix2 runTestSmallFileTransfer - describe "sender cancelled file transfer before transfer" $ fileTestMatrix2 runTestFileSndCancelBeforeTransfer - it "sender cancelled file transfer during transfer" testFileSndCancelDuringTransfer - it "recipient cancelled file transfer" testFileRcvCancel - describe "send and receive file to group" $ fileTestMatrix3 runTestGroupFileTransfer - it "send and receive file inline to group (without accepting)" testInlineGroupFileTransfer - it "send and receive small file inline to group (default config)" testSmallInlineGroupFileTransfer - it "small file sent without acceptance is ignored in terminal by default" testSmallInlineGroupFileIgnored - describe "sender cancelled group file transfer before transfer" $ fileTestMatrix3 runTestGroupFileSndCancelBeforeTransfer describe "messages with files" $ do - describe "send and receive message with file" $ fileTestMatrix2 runTestMessageWithFile + it "send and receive message with file" runTestMessageWithFile it "send and receive image" testSendImage - it "sender marking chat item deleted during file transfer cancels file" testSenderMarkItemDeletedTransfer + it "sender marking chat item deleted cancels file" testSenderMarkItemDeleted it "files folder: send and receive image" testFilesFoldersSendImage - it "files folder: sender deleted file during transfer" testFilesFoldersImageSndDelete - it "files folder: recipient deleted file during transfer" testFilesFoldersImageRcvDelete + it "files folder: sender deleted file" testFilesFoldersImageSndDelete -- TODO add test deleting during upload + it "files folder: recipient deleted file" testFilesFoldersImageRcvDelete -- TODO add test deleting during download it "send and receive image with text and quote" testSendImageWithTextAndQuote it "send and receive image to group" testGroupSendImage it "send and receive image with text and quote to group" testGroupSendImageWithTextAndQuote - describe "async sending and receiving files" $ do - -- fails on CI - xit'' "send and receive file, sender restarts" testAsyncFileTransferSenderRestarts - xit'' "send and receive file, receiver restarts" testAsyncFileTransferReceiverRestarts - xdescribe "send and receive file, fully asynchronous" $ do - it "v2" testAsyncFileTransfer - it "v1" testAsyncFileTransferV1 - xit "send and receive file to group, fully asynchronous" testAsyncGroupFileTransfer + describe "batch send messages with files" $ do + it "with files folder: send multiple files to contact" testSendMultiFilesDirect + it "with files folder: send multiple files to group" testSendMultiFilesGroup describe "file transfer over XFTP" $ do it "round file description count" $ const testXFTPRoundFDCount it "send and receive file" testXFTPFileTransfer @@ -69,7 +48,6 @@ chatFileTests = do it "send and receive file in group" testXFTPGroupFileTransfer it "delete uploaded file" testXFTPDeleteUploadedFile it "delete uploaded file in group" testXFTPDeleteUploadedFileGroup - it "with changed XFTP config: send and receive file" testXFTPWithChangedConfig it "with relative paths: send and receive file" testXFTPWithRelativePaths xit' "continue receiving file after restart" testXFTPContinueRcv xit' "receive file marked to receive on chat start" testXFTPMarkToReceive @@ -77,521 +55,60 @@ chatFileTests = do it "cancel receiving file, repeat receive" testXFTPCancelRcvRepeat it "should accept file automatically with CLI option" testAutoAcceptFile it "should prohibit file transfers in groups based on preference" testProhibitFiles + describe "file transfer over XFTP without chat items" $ do + it "send and receive small standalone file" testXFTPStandaloneSmall + it "send and receive small standalone file with extra information" testXFTPStandaloneSmallInfo + it "send and receive large standalone file" testXFTPStandaloneLarge + it "send and receive large standalone file with extra information" testXFTPStandaloneLargeInfo + it "send and receive large standalone file using relative paths" testXFTPStandaloneRelativePaths + xit "removes sent file from server" testXFTPStandaloneCancelSnd -- no error shown in tests + it "removes received temporary files" testXFTPStandaloneCancelRcv -runTestFileTransfer :: HasCallStack => TestCC -> TestCC -> IO () -runTestFileTransfer alice bob = do +runTestMessageWithFile :: HasCallStack => TestParams -> IO () +runTestMessageWithFile = testChat2 aliceProfile bobProfile $ \alice bob -> withXFTPServer $ do connectUsers alice bob - startFileTransfer' alice bob "test.pdf" "266.0 KiB / 272376 bytes" - concurrentlyN_ - [ do - bob #> "@alice receiving here..." - bob <## "completed receiving file 1 (test.pdf) from alice", - alice - <### [ WithTime "bob> receiving here...", - "completed sending file 1 (test.pdf) to bob" - ] - ] - src <- B.readFile "./tests/fixtures/test.pdf" - dest <- B.readFile "./tests/tmp/test.pdf" - dest `shouldBe` src -runTestFileTransferEncrypted :: HasCallStack => TestCC -> TestCC -> IO () -runTestFileTransferEncrypted alice bob = do - connectUsers alice bob - alice #> "/f @bob ./tests/fixtures/test.pdf" - alice <## "use /fc 1 to cancel sending" - bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" - bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - bob ##> "/fr 1 encrypt=on ./tests/tmp" - bob <## "saving file 1 from alice to ./tests/tmp/test.pdf" - concurrently_ - (bob <## "started receiving file 1 (test.pdf) from alice") - (alice <## "started sending file 1 (test.pdf) to bob") - - concurrentlyN_ - [ do - bob #> "@alice receiving here..." - -- uncomment this and below to test encryption error in encryptFile - -- bob <## "cannot write file ./tests/tmp/test.pdf: test error, received file not encrypted" - bob <## "completed receiving file 1 (test.pdf) from alice", - alice - <### [ WithTime "bob> receiving here...", - "completed sending file 1 (test.pdf) to bob" - ] - ] - Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob - src <- B.readFile "./tests/fixtures/test.pdf" - -- dest <- B.readFile "./tests/tmp/test.pdf" - -- dest `shouldBe` src - Right dest <- chatReadFile "./tests/tmp/test.pdf" (strEncode key) (strEncode nonce) - LB.toStrict dest `shouldBe` src - -testInlineFileTransfer :: HasCallStack => FilePath -> IO () -testInlineFileTransfer = - testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do - connectUsers alice bob - bob ##> "/_files_folder ./tests/tmp/" - bob <## "ok" - alice ##> "/_send @2 json {\"msgContent\":{\"type\":\"voice\", \"duration\":10, \"text\":\"\"}, \"filePath\":\"./tests/fixtures/test.jpg\"}" - alice <# "@bob voice message (00:10)" - alice <# "/f @bob ./tests/fixtures/test.jpg" - -- below is not shown in "sent" mode - -- alice <## "use /fc 1 to cancel sending" - bob <# "alice> voice message (00:10)" - bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" - -- below is not shown in "sent" mode - -- bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - bob <## "started receiving file 1 (test.jpg) from alice" - concurrently_ - (alice <## "completed sending file 1 (test.jpg) to bob") - (bob <## "completed receiving file 1 (test.jpg) from alice") - src <- B.readFile "./tests/fixtures/test.jpg" - dest <- B.readFile "./tests/tmp/test.jpg" - dest `shouldBe` src - where - cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, sendChunks = 100, receiveChunks = 100}} - -testInlineFileTransferEncrypted :: HasCallStack => FilePath -> IO () -testInlineFileTransferEncrypted = - testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do - connectUsers alice bob - bob ##> "/_files_folder ./tests/tmp/" - bob <## "ok" - bob ##> "/_files_encrypt on" - bob <## "ok" - alice ##> "/_send @2 json {\"msgContent\":{\"type\":\"voice\", \"duration\":10, \"text\":\"\"}, \"filePath\":\"./tests/fixtures/test.jpg\"}" - alice <# "@bob voice message (00:10)" - alice <# "/f @bob ./tests/fixtures/test.jpg" - -- below is not shown in "sent" mode - -- alice <## "use /fc 1 to cancel sending" - bob <# "alice> voice message (00:10)" - bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" - -- below is not shown in "sent" mode - -- bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - bob <## "started receiving file 1 (test.jpg) from alice" - concurrently_ - (alice <## "completed sending file 1 (test.jpg) to bob") - (bob <## "completed receiving file 1 (test.jpg) from alice") - Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob - src <- B.readFile "./tests/fixtures/test.jpg" - Right dest <- chatReadFile "./tests/tmp/test.jpg" (strEncode key) (strEncode nonce) - LB.toStrict dest `shouldBe` src - where - cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, sendChunks = 100, receiveChunks = 100}} - -testAcceptInlineFileSndCancelDuringTransfer :: HasCallStack => FilePath -> IO () -testAcceptInlineFileSndCancelDuringTransfer = - testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do - connectUsers alice bob - bob ##> "/_files_folder ./tests/tmp/" - bob <## "ok" - alice #> "/f @bob ./tests/fixtures/test_1MB.pdf" - alice <## "use /fc 1 to cancel sending" - bob <# "alice> sends file test_1MB.pdf (1017.7 KiB / 1042157 bytes)" - bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - bob ##> "/fr 1 inline=on" - bob <## "saving file 1 from alice to test_1MB.pdf" - alice <## "started sending file 1 (test_1MB.pdf) to bob" - bob <## "started receiving file 1 (test_1MB.pdf) from alice" - alice ##> "/fc 1" -- test that inline file cancel doesn't delete contact connection - concurrentlyN_ - [ do - alice <##. "cancelled sending file 1 (test_1MB.pdf)" - alice <## "completed sending file 1 (test_1MB.pdf) to bob", - bob <## "completed receiving file 1 (test_1MB.pdf) from alice" - ] - alice #> "@bob hi" - bob <# "alice> hi" - bob #> "@alice hey" - alice <# "bob> hey" - where - cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, receiveChunks = 50}} - -testSmallInlineFileTransfer :: HasCallStack => FilePath -> IO () -testSmallInlineFileTransfer = - testChat2 aliceProfile bobProfile $ \alice bob -> do - connectUsers alice bob - bob ##> "/_files_folder ./tests/tmp/" - bob <## "ok" - alice ##> "/_send @2 json {\"msgContent\":{\"type\":\"voice\", \"duration\":10, \"text\":\"\"}, \"filePath\":\"./tests/fixtures/logo.jpg\"}" - alice <# "@bob voice message (00:10)" - alice <# "/f @bob ./tests/fixtures/logo.jpg" - -- below is not shown in "sent" mode - -- alice <## "use /fc 1 to cancel sending" - bob <# "alice> voice message (00:10)" - bob <# "alice> sends file logo.jpg (31.3 KiB / 32080 bytes)" - -- below is not shown in "sent" mode - -- bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - bob <## "started receiving file 1 (logo.jpg) from alice" - concurrently_ - (alice <## "completed sending file 1 (logo.jpg) to bob") - (bob <## "completed receiving file 1 (logo.jpg) from alice") - src <- B.readFile "./tests/fixtures/logo.jpg" - dest <- B.readFile "./tests/tmp/logo.jpg" - dest `shouldBe` src - -testSmallInlineFileIgnored :: HasCallStack => FilePath -> IO () -testSmallInlineFileIgnored tmp = do - withNewTestChat tmp "alice" aliceProfile $ \alice -> - withNewTestChatOpts tmp testOpts {allowInstantFiles = False} "bob" bobProfile $ \bob -> do - connectUsers alice bob - bob ##> "/_files_folder ./tests/tmp/" - bob <## "ok" - alice ##> "/_send @2 json {\"msgContent\":{\"type\":\"voice\", \"duration\":10, \"text\":\"\"}, \"filePath\":\"./tests/fixtures/logo.jpg\"}" - alice <# "@bob voice message (00:10)" - alice <# "/f @bob ./tests/fixtures/logo.jpg" - -- below is not shown in "sent" mode - -- alice <## "use /fc 1 to cancel sending" - bob <# "alice> voice message (00:10)" - bob <# "alice> sends file logo.jpg (31.3 KiB / 32080 bytes)" - bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - bob <## "A small file sent without acceptance - you can enable receiving such files with -f option." - -- below is not shown in "sent" mode - -- bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - alice <## "completed sending file 1 (logo.jpg) to bob" - bob ##> "/fr 1" - bob <## "file is already being received: logo.jpg" - -testReceiveInline :: HasCallStack => FilePath -> IO () -testReceiveInline = - testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do - connectUsers alice bob - alice #> "/f @bob ./tests/fixtures/test.jpg" - alice <## "use /fc 1 to cancel sending" - bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" - bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - bob ##> "/fr 1 inline=on ./tests/tmp" - bob <## "saving file 1 from alice to ./tests/tmp/test.jpg" - alice <## "started sending file 1 (test.jpg) to bob" - alice <## "completed sending file 1 (test.jpg) to bob" - bob <## "started receiving file 1 (test.jpg) from alice" - bob <## "completed receiving file 1 (test.jpg) from alice" - src <- B.readFile "./tests/fixtures/test.jpg" - dest <- B.readFile "./tests/tmp/test.jpg" - dest `shouldBe` src - where - cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 10, receiveChunks = 5}} - -runTestSmallFileTransfer :: HasCallStack => TestCC -> TestCC -> IO () -runTestSmallFileTransfer alice bob = do - connectUsers alice bob - alice #> "/f @bob ./tests/fixtures/test.txt" - alice <## "use /fc 1 to cancel sending" - bob <# "alice> sends file test.txt (11 bytes / 11 bytes)" - bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - bob ##> "/fr 1 ./tests/tmp" - bob <## "saving file 1 from alice to ./tests/tmp/test.txt" - concurrentlyN_ - [ do - bob <## "started receiving file 1 (test.txt) from alice" - bob <## "completed receiving file 1 (test.txt) from alice", - do - alice <## "started sending file 1 (test.txt) to bob" - alice <## "completed sending file 1 (test.txt) to bob" - ] - src <- B.readFile "./tests/fixtures/test.txt" - dest <- B.readFile "./tests/tmp/test.txt" - dest `shouldBe` src - -runTestFileSndCancelBeforeTransfer :: HasCallStack => TestCC -> TestCC -> IO () -runTestFileSndCancelBeforeTransfer alice bob = do - connectUsers alice bob - alice #> "/f @bob ./tests/fixtures/test.txt" - alice <## "use /fc 1 to cancel sending" - bob <# "alice> sends file test.txt (11 bytes / 11 bytes)" - bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - alice ##> "/fc 1" - concurrentlyN_ - [ alice <##. "cancelled sending file 1 (test.txt)", - bob <## "alice cancelled sending file 1 (test.txt)" - ] - alice ##> "/fs 1" - alice - <##.. [ "sending file 1 (test.txt): no file transfers", - "sending file 1 (test.txt) cancelled: bob" - ] - alice <## "file transfer cancelled" - bob ##> "/fs 1" - bob <## "receiving file 1 (test.txt) cancelled" - bob ##> "/fr 1 ./tests/tmp" - bob <## "file cancelled: test.txt" - -testFileSndCancelDuringTransfer :: HasCallStack => FilePath -> IO () -testFileSndCancelDuringTransfer = - testChat2 aliceProfile bobProfile $ - \alice bob -> do - connectUsers alice bob - startFileTransfer' alice bob "test_1MB.pdf" "1017.7 KiB / 1042157 bytes" - alice ##> "/fc 1" - concurrentlyN_ - [ do - alice <## "cancelled sending file 1 (test_1MB.pdf) to bob" - alice ##> "/fs 1" - alice <## "sending file 1 (test_1MB.pdf) cancelled: bob" - alice <## "file transfer cancelled", - do - bob <## "alice cancelled sending file 1 (test_1MB.pdf)" - bob ##> "/fs 1" - bob <## "receiving file 1 (test_1MB.pdf) cancelled, received part path: ./tests/tmp/test_1MB.pdf" - ] - checkPartialTransfer "test_1MB.pdf" - -testFileRcvCancel :: HasCallStack => FilePath -> IO () -testFileRcvCancel = - testChat2 aliceProfile bobProfile $ - \alice bob -> do - connectUsers alice bob - startFileTransfer alice bob - bob ##> "/fs 1" - getTermLine bob >>= (`shouldStartWith` "receiving file 1 (test.jpg) progress") - waitFileExists "./tests/tmp/test.jpg" - bob ##> "/fc 1" - concurrentlyN_ - [ do - bob <## "cancelled receiving file 1 (test.jpg) from alice" - bob ##> "/fs 1" - bob <## "receiving file 1 (test.jpg) cancelled, received part path: ./tests/tmp/test.jpg", - do - alice <## "bob cancelled receiving file 1 (test.jpg)" - alice ##> "/fs 1" - alice <## "sending file 1 (test.jpg) cancelled: bob" - alice <## "file transfer cancelled" - ] - checkPartialTransfer "test.jpg" - -runTestGroupFileTransfer :: HasCallStack => TestCC -> TestCC -> TestCC -> IO () -runTestGroupFileTransfer alice bob cath = do - createGroup3 "team" alice bob cath - alice #> "/f #team ./tests/fixtures/test.jpg" - alice <## "use /fc 1 to cancel sending" - concurrentlyN_ - [ do - bob <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" - bob <## "use /fr 1 [<dir>/ | <path>] to receive it", - do - cath <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" - cath <## "use /fr 1 [<dir>/ | <path>] to receive it" - ] - alice ##> "/fs 1" - getTermLine alice >>= (`shouldStartWith` "sending file 1 (test.jpg): no file transfers") - bob ##> "/fr 1 ./tests/tmp/" - bob <## "saving file 1 from alice to ./tests/tmp/test.jpg" - concurrentlyN_ - [ do - alice <## "started sending file 1 (test.jpg) to bob" - alice <## "completed sending file 1 (test.jpg) to bob" - alice ##> "/fs 1" - alice <## "sending file 1 (test.jpg) complete: bob", - do - bob <## "started receiving file 1 (test.jpg) from alice" - bob <## "completed receiving file 1 (test.jpg) from alice" - ] - cath ##> "/fr 1 ./tests/tmp/" - cath <## "saving file 1 from alice to ./tests/tmp/test_1.jpg" - concurrentlyN_ - [ do - alice <## "started sending file 1 (test.jpg) to cath" - alice <## "completed sending file 1 (test.jpg) to cath" - alice ##> "/fs 1" - getTermLine alice >>= (`shouldStartWith` "sending file 1 (test.jpg) complete"), - do - cath <## "started receiving file 1 (test.jpg) from alice" - cath <## "completed receiving file 1 (test.jpg) from alice" - ] - src <- B.readFile "./tests/fixtures/test.jpg" - dest1 <- B.readFile "./tests/tmp/test.jpg" - dest2 <- B.readFile "./tests/tmp/test_1.jpg" - dest1 `shouldBe` src - dest2 `shouldBe` src - -testInlineGroupFileTransfer :: HasCallStack => FilePath -> IO () -testInlineGroupFileTransfer = - testChatCfg3 cfg aliceProfile bobProfile cathProfile $ - \alice bob cath -> do - createGroup3 "team" alice bob cath - bob ##> "/_files_folder ./tests/tmp/bob/" - bob <## "ok" - cath ##> "/_files_folder ./tests/tmp/cath/" - cath <## "ok" - alice ##> "/_send #1 json {\"msgContent\":{\"type\":\"voice\", \"duration\":10, \"text\":\"\"}, \"filePath\":\"./tests/fixtures/logo.jpg\"}" - alice <# "#team voice message (00:10)" - alice <# "/f #team ./tests/fixtures/logo.jpg" - -- below is not shown in "sent" mode - -- alice <## "use /fc 1 to cancel sending" - concurrentlyN_ - [ do - alice - <### [ "completed sending file 1 (logo.jpg) to bob", - "completed sending file 1 (logo.jpg) to cath" - ] - alice ##> "/fs 1" - alice <##. "sending file 1 (logo.jpg) complete", - do - bob <# "#team alice> voice message (00:10)" - bob <# "#team alice> sends file logo.jpg (31.3 KiB / 32080 bytes)" - bob <## "started receiving file 1 (logo.jpg) from alice" - bob <## "completed receiving file 1 (logo.jpg) from alice", - do - cath <# "#team alice> voice message (00:10)" - cath <# "#team alice> sends file logo.jpg (31.3 KiB / 32080 bytes)" - cath <## "started receiving file 1 (logo.jpg) from alice" - cath <## "completed receiving file 1 (logo.jpg) from alice" - ] - src <- B.readFile "./tests/fixtures/logo.jpg" - dest1 <- B.readFile "./tests/tmp/bob/logo.jpg" - dest2 <- B.readFile "./tests/tmp/cath/logo.jpg" - dest1 `shouldBe` src - dest2 `shouldBe` src - where - cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, sendChunks = 100, totalSendChunks = 100, receiveChunks = 100}} - -testSmallInlineGroupFileTransfer :: HasCallStack => FilePath -> IO () -testSmallInlineGroupFileTransfer = - testChatCfg3 testCfg aliceProfile bobProfile cathProfile $ - \alice bob cath -> do - createGroup3 "team" alice bob cath - bob ##> "/_files_folder ./tests/tmp/bob/" - bob <## "ok" - cath ##> "/_files_folder ./tests/tmp/cath/" - cath <## "ok" - alice ##> "/_send #1 json {\"msgContent\":{\"type\":\"voice\", \"duration\":10, \"text\":\"\"}, \"filePath\":\"./tests/fixtures/logo.jpg\"}" - alice <# "#team voice message (00:10)" - alice <# "/f #team ./tests/fixtures/logo.jpg" - -- below is not shown in "sent" mode - -- alice <## "use /fc 1 to cancel sending" - concurrentlyN_ - [ do - alice - <### [ "completed sending file 1 (logo.jpg) to bob", - "completed sending file 1 (logo.jpg) to cath" - ] - alice ##> "/fs 1" - alice <##. "sending file 1 (logo.jpg) complete", - do - bob <# "#team alice> voice message (00:10)" - bob <# "#team alice> sends file logo.jpg (31.3 KiB / 32080 bytes)" - bob <## "started receiving file 1 (logo.jpg) from alice" - bob <## "completed receiving file 1 (logo.jpg) from alice", - do - cath <# "#team alice> voice message (00:10)" - cath <# "#team alice> sends file logo.jpg (31.3 KiB / 32080 bytes)" - cath <## "started receiving file 1 (logo.jpg) from alice" - cath <## "completed receiving file 1 (logo.jpg) from alice" - ] - src <- B.readFile "./tests/fixtures/logo.jpg" - dest1 <- B.readFile "./tests/tmp/bob/logo.jpg" - dest2 <- B.readFile "./tests/tmp/cath/logo.jpg" - dest1 `shouldBe` src - dest2 `shouldBe` src - -testSmallInlineGroupFileIgnored :: HasCallStack => FilePath -> IO () -testSmallInlineGroupFileIgnored tmp = do - withNewTestChat tmp "alice" aliceProfile $ \alice -> - withNewTestChatOpts tmp testOpts {allowInstantFiles = False} "bob" bobProfile $ \bob -> do - withNewTestChatOpts tmp testOpts {allowInstantFiles = False} "cath" cathProfile $ \cath -> do - createGroup3 "team" alice bob cath - bob ##> "/_files_folder ./tests/tmp/bob/" - bob <## "ok" - cath ##> "/_files_folder ./tests/tmp/cath/" - cath <## "ok" - alice ##> "/_send #1 json {\"msgContent\":{\"type\":\"voice\", \"duration\":10, \"text\":\"\"}, \"filePath\":\"./tests/fixtures/logo.jpg\"}" - alice <# "#team voice message (00:10)" - alice <# "/f #team ./tests/fixtures/logo.jpg" - -- below is not shown in "sent" mode - -- alice <## "use /fc 1 to cancel sending" - concurrentlyN_ - [ do - alice - <### [ "completed sending file 1 (logo.jpg) to bob", - "completed sending file 1 (logo.jpg) to cath" - ] - alice ##> "/fs 1" - alice <##. "sending file 1 (logo.jpg) complete", - do - bob <# "#team alice> voice message (00:10)" - bob <# "#team alice> sends file logo.jpg (31.3 KiB / 32080 bytes)" - bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - bob <## "A small file sent without acceptance - you can enable receiving such files with -f option." - bob ##> "/fr 1" - bob <## "file is already being received: logo.jpg", - do - cath <# "#team alice> voice message (00:10)" - cath <# "#team alice> sends file logo.jpg (31.3 KiB / 32080 bytes)" - cath <## "use /fr 1 [<dir>/ | <path>] to receive it" - cath <## "A small file sent without acceptance - you can enable receiving such files with -f option." - cath ##> "/fr 1" - cath <## "file is already being received: logo.jpg" - ] - -runTestGroupFileSndCancelBeforeTransfer :: HasCallStack => TestCC -> TestCC -> TestCC -> IO () -runTestGroupFileSndCancelBeforeTransfer alice bob cath = do - createGroup3 "team" alice bob cath - alice #> "/f #team ./tests/fixtures/test.txt" - alice <## "use /fc 1 to cancel sending" - concurrentlyN_ - [ do - bob <# "#team alice> sends file test.txt (11 bytes / 11 bytes)" - bob <## "use /fr 1 [<dir>/ | <path>] to receive it", - do - cath <# "#team alice> sends file test.txt (11 bytes / 11 bytes)" - cath <## "use /fr 1 [<dir>/ | <path>] to receive it" - ] - alice ##> "/fc 1" - concurrentlyN_ - [ alice <## "cancelled sending file 1 (test.txt)", - bob <## "alice cancelled sending file 1 (test.txt)", - cath <## "alice cancelled sending file 1 (test.txt)" - ] - alice ##> "/fs 1" - alice <## "sending file 1 (test.txt): no file transfers" - alice <## "file transfer cancelled" - bob ##> "/fs 1" - bob <## "receiving file 1 (test.txt) cancelled" - bob ##> "/fr 1 ./tests/tmp" - bob <## "file cancelled: test.txt" - -runTestMessageWithFile :: HasCallStack => TestCC -> TestCC -> IO () -runTestMessageWithFile alice bob = do - connectUsers alice bob - alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}" + alice ##> "/_send @2 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}]" alice <# "@bob hi, sending a file" alice <# "/f @bob ./tests/fixtures/test.jpg" alice <## "use /fc 1 to cancel sending" bob <# "alice> hi, sending a file" bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" bob <## "use /fr 1 [<dir>/ | <path>] to receive it" + alice <## "completed uploading file 1 (test.jpg) for bob" + bob ##> "/fr 1 ./tests/tmp" - bob <## "saving file 1 from alice to ./tests/tmp/test.jpg" - concurrently_ - (bob <## "started receiving file 1 (test.jpg) from alice") - (alice <## "started sending file 1 (test.jpg) to bob") - concurrently_ - (bob <## "completed receiving file 1 (test.jpg) from alice") - (alice <## "completed sending file 1 (test.jpg) to bob") + bob + <### [ "saving file 1 from alice to ./tests/tmp/test.jpg", + "started receiving file 1 (test.jpg) from alice" + ] + bob <## "completed receiving file 1 (test.jpg) from alice" + src <- B.readFile "./tests/fixtures/test.jpg" dest <- B.readFile "./tests/tmp/test.jpg" dest `shouldBe` src alice #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((1, "hi, sending a file"), Just "./tests/fixtures/test.jpg")]) bob #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((0, "hi, sending a file"), Just "./tests/tmp/test.jpg")]) -testSendImage :: HasCallStack => FilePath -> IO () +testSendImage :: HasCallStack => TestParams -> IO () testSendImage = testChat2 aliceProfile bobProfile $ - \alice bob -> do + \alice bob -> withXFTPServer $ do connectUsers alice bob - alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" + alice ##> "/_send @2 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]" alice <# "/f @bob ./tests/fixtures/test.jpg" alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" bob <## "use /fr 1 [<dir>/ | <path>] to receive it" + alice <## "completed uploading file 1 (test.jpg) for bob" + bob ##> "/fr 1 ./tests/tmp" - bob <## "saving file 1 from alice to ./tests/tmp/test.jpg" - concurrently_ - (bob <## "started receiving file 1 (test.jpg) from alice") - (alice <## "started sending file 1 (test.jpg) to bob") - concurrently_ - (bob <## "completed receiving file 1 (test.jpg) from alice") - (alice <## "completed sending file 1 (test.jpg) to bob") + bob + <### [ "saving file 1 from alice to ./tests/tmp/test.jpg", + "started receiving file 1 (test.jpg) from alice" + ] + bob <## "completed receiving file 1 (test.jpg) from alice" + src <- B.readFile "./tests/fixtures/test.jpg" dest <- B.readFile "./tests/tmp/test.jpg" dest `shouldBe` src @@ -604,56 +121,50 @@ testSendImage = fileExists <- doesFileExist "./tests/tmp/test.jpg" fileExists `shouldBe` True -testSenderMarkItemDeletedTransfer :: HasCallStack => FilePath -> IO () -testSenderMarkItemDeletedTransfer = +testSenderMarkItemDeleted :: HasCallStack => TestParams -> IO () +testSenderMarkItemDeleted = testChat2 aliceProfile bobProfile $ - \alice bob -> do + \alice bob -> withXFTPServer $ do connectUsers alice bob - alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test_1MB.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}" + alice ##> "/_send @2 json [{\"filePath\": \"./tests/fixtures/test_1MB.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}]" alice <# "@bob hi, sending a file" alice <# "/f @bob ./tests/fixtures/test_1MB.pdf" alice <## "use /fc 1 to cancel sending" bob <# "alice> hi, sending a file" bob <# "alice> sends file test_1MB.pdf (1017.7 KiB / 1042157 bytes)" bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - bob ##> "/fr 1 ./tests/tmp" - bob <## "saving file 1 from alice to ./tests/tmp/test_1MB.pdf" - concurrently_ - (bob <## "started receiving file 1 (test_1MB.pdf) from alice") - (alice <## "started sending file 1 (test_1MB.pdf) to bob") + alice <## "completed uploading file 1 (test_1MB.pdf) for bob" alice #$> ("/_delete item @2 " <> itemId 1 <> " broadcast", id, "message marked deleted") - - alice ##> "/fs 1" - alice <## "sending file 1 (test_1MB.pdf) cancelled: bob" - alice <## "file transfer cancelled" - bob <# "alice> [marked deleted] hi, sending a file" + + bob ##> "/fr 1 ./tests/tmp" + bob <## "file cancelled: test_1MB.pdf" + bob ##> "/fs 1" - bob <## "receiving file 1 (test_1MB.pdf) cancelled, received part path: ./tests/tmp/test_1MB.pdf" + bob <## "receiving file 1 (test_1MB.pdf) cancelled" - checkPartialTransfer "test_1MB.pdf" - -testFilesFoldersSendImage :: HasCallStack => FilePath -> IO () +testFilesFoldersSendImage :: HasCallStack => TestParams -> IO () testFilesFoldersSendImage = testChat2 aliceProfile bobProfile $ - \alice bob -> do + \alice bob -> withXFTPServer $ do connectUsers alice bob alice #$> ("/_files_folder ./tests/fixtures", id, "ok") bob #$> ("/_files_folder ./tests/tmp/app_files", id, "ok") - alice ##> "/_send @2 json {\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" + alice ##> "/_send @2 json [{\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]" alice <# "/f @bob test.jpg" alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" bob <## "use /fr 1 [<dir>/ | <path>] to receive it" + alice <## "completed uploading file 1 (test.jpg) for bob" + bob ##> "/fr 1" - bob <## "saving file 1 from alice to test.jpg" - concurrently_ - (bob <## "started receiving file 1 (test.jpg) from alice") - (alice <## "started sending file 1 (test.jpg) to bob") - concurrently_ - (bob <## "completed receiving file 1 (test.jpg) from alice") - (alice <## "completed sending file 1 (test.jpg) to bob") + bob + <### [ "saving file 1 from alice to test.jpg", + "started receiving file 1 (test.jpg) from alice" + ] + bob <## "completed receiving file 1 (test.jpg) from alice" + src <- B.readFile "./tests/fixtures/test.jpg" dest <- B.readFile "./tests/tmp/app_files/test.jpg" dest `shouldBe` src @@ -665,74 +176,74 @@ testFilesFoldersSendImage = bob <## "alice: contact is deleted" alice <## "bob (Bob) deleted contact with you" -testFilesFoldersImageSndDelete :: HasCallStack => FilePath -> IO () +testFilesFoldersImageSndDelete :: HasCallStack => TestParams -> IO () testFilesFoldersImageSndDelete = testChat2 aliceProfile bobProfile $ - \alice bob -> do + \alice bob -> withXFTPServer $ do connectUsers alice bob alice #$> ("/_files_folder ./tests/tmp/alice_app_files", id, "ok") copyFile "./tests/fixtures/test_1MB.pdf" "./tests/tmp/alice_app_files/test_1MB.pdf" bob #$> ("/_files_folder ./tests/tmp/bob_app_files", id, "ok") - alice ##> "/_send @2 json {\"filePath\": \"test_1MB.pdf\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" + alice ##> "/_send @2 json [{\"filePath\": \"test_1MB.pdf\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]" alice <# "/f @bob test_1MB.pdf" alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test_1MB.pdf (1017.7 KiB / 1042157 bytes)" bob <## "use /fr 1 [<dir>/ | <path>] to receive it" + alice <## "completed uploading file 1 (test_1MB.pdf) for bob" + bob ##> "/fr 1" - bob <## "saving file 1 from alice to test_1MB.pdf" - concurrently_ - (bob <## "started receiving file 1 (test_1MB.pdf) from alice") - (alice <## "started sending file 1 (test_1MB.pdf) to bob") - -- deleting contact should cancel and remove file + bob + <### [ "saving file 1 from alice to test_1MB.pdf", + "started receiving file 1 (test_1MB.pdf) from alice" + ] + bob <## "completed receiving file 1 (test_1MB.pdf) from alice" + + -- deleting contact should remove file checkActionDeletesFile "./tests/tmp/alice_app_files/test_1MB.pdf" $ do alice ##> "/d bob" alice <## "bob: contact is deleted" bob <## "alice (Alice) deleted contact with you" bob ##> "/fs 1" - bob <##. "receiving file 1 (test_1MB.pdf) progress" - -- deleting contact should remove cancelled file + bob <##. "receiving file 1 (test_1MB.pdf) complete" checkActionDeletesFile "./tests/tmp/bob_app_files/test_1MB.pdf" $ do bob ##> "/d alice" bob <## "alice: contact is deleted" -testFilesFoldersImageRcvDelete :: HasCallStack => FilePath -> IO () +testFilesFoldersImageRcvDelete :: HasCallStack => TestParams -> IO () testFilesFoldersImageRcvDelete = testChat2 aliceProfile bobProfile $ - \alice bob -> do + \alice bob -> withXFTPServer $ do connectUsers alice bob alice #$> ("/_files_folder ./tests/fixtures", id, "ok") bob #$> ("/_files_folder ./tests/tmp/app_files", id, "ok") - alice ##> "/_send @2 json {\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" + alice ##> "/_send @2 json [{\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]" alice <# "/f @bob test.jpg" alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" bob <## "use /fr 1 [<dir>/ | <path>] to receive it" + alice <## "completed uploading file 1 (test.jpg) for bob" + bob ##> "/fr 1" - bob <## "saving file 1 from alice to test.jpg" - concurrently_ - (bob <## "started receiving file 1 (test.jpg) from alice") - (alice <## "started sending file 1 (test.jpg) to bob") - -- deleting contact should cancel and remove file - waitFileExists "./tests/tmp/app_files/test.jpg" + bob + <### [ "saving file 1 from alice to test.jpg", + "started receiving file 1 (test.jpg) from alice" + ] + bob <## "completed receiving file 1 (test.jpg) from alice" + + -- deleting contact should remove file checkActionDeletesFile "./tests/tmp/app_files/test.jpg" $ do bob ##> "/d alice" bob <## "alice: contact is deleted" - alice - <### [ "bob (Bob) deleted contact with you", - "bob cancelled receiving file 1 (test.jpg)" - ] - alice ##> "/fs 1" - alice <## "sending file 1 (test.jpg) cancelled: bob" - alice <## "file transfer cancelled" + alice <## "bob (Bob) deleted contact with you" -testSendImageWithTextAndQuote :: HasCallStack => FilePath -> IO () +testSendImageWithTextAndQuote :: HasCallStack => TestParams -> IO () testSendImageWithTextAndQuote = testChat2 aliceProfile bobProfile $ - \alice bob -> do + \alice bob -> withXFTPServer $ do connectUsers alice bob bob #> "@alice hi alice" alice <# "bob> hi alice" - alice ##> ("/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> itemId 1 <> ", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}") + alice ##> ("/_send @2 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> itemId 1 <> ", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]") alice <# "@bob > hi alice" alice <## " hey bob" alice <# "/f @bob ./tests/fixtures/test.jpg" @@ -741,22 +252,24 @@ testSendImageWithTextAndQuote = bob <## " hey bob" bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" bob <## "use /fr 1 [<dir>/ | <path>] to receive it" + alice <## "completed uploading file 1 (test.jpg) for bob" + bob ##> "/fr 1 ./tests/tmp" - bob <## "saving file 1 from alice to ./tests/tmp/test.jpg" - concurrently_ - (bob <## "started receiving file 1 (test.jpg) from alice") - (alice <## "started sending file 1 (test.jpg) to bob") - concurrently_ - (bob <## "completed receiving file 1 (test.jpg) from alice") - (alice <## "completed sending file 1 (test.jpg) to bob") + bob + <### [ "saving file 1 from alice to ./tests/tmp/test.jpg", + "started receiving file 1 (test.jpg) from alice" + ] + bob <## "completed receiving file 1 (test.jpg) from alice" + src <- B.readFile "./tests/fixtures/test.jpg" B.readFile "./tests/tmp/test.jpg" `shouldReturn` src alice #$> ("/_get chat @2 count=100", chat'', chatFeatures'' <> [((0, "hi alice"), Nothing, Nothing), ((1, "hey bob"), Just (0, "hi alice"), Just "./tests/fixtures/test.jpg")]) alice @@@ [("@bob", "hey bob")] bob #$> ("/_get chat @2 count=100", chat'', chatFeatures'' <> [((1, "hi alice"), Nothing, Nothing), ((0, "hey bob"), Just (1, "hi alice"), Just "./tests/tmp/test.jpg")]) bob @@@ [("@alice", "hey bob")] + -- quoting (file + text) with file uses quoted text - bob ##> ("/_send @2 json {\"filePath\": \"./tests/fixtures/test.pdf\", \"quotedItemId\": " <> itemId 2 <> ", \"msgContent\": {\"text\":\"\",\"type\":\"file\"}}") + bob ##> ("/_send @2 json [{\"filePath\": \"./tests/fixtures/test.pdf\", \"quotedItemId\": " <> itemId 2 <> ", \"msgContent\": {\"text\":\"\",\"type\":\"file\"}}]") bob <# "@alice > hey bob" bob <## " test.pdf" bob <# "/f @alice ./tests/fixtures/test.pdf" @@ -765,18 +278,20 @@ testSendImageWithTextAndQuote = alice <## " test.pdf" alice <# "bob> sends file test.pdf (266.0 KiB / 272376 bytes)" alice <## "use /fr 2 [<dir>/ | <path>] to receive it" + bob <## "completed uploading file 2 (test.pdf) for alice" + alice ##> "/fr 2 ./tests/tmp" - alice <## "saving file 2 from bob to ./tests/tmp/test.pdf" - concurrently_ - (alice <## "started receiving file 2 (test.pdf) from bob") - (bob <## "started sending file 2 (test.pdf) to alice") - concurrently_ - (alice <## "completed receiving file 2 (test.pdf) from bob") - (bob <## "completed sending file 2 (test.pdf) to alice") + alice + <### [ "saving file 2 from bob to ./tests/tmp/test.pdf", + "started receiving file 2 (test.pdf) from bob" + ] + alice <## "completed receiving file 2 (test.pdf) from bob" + txtSrc <- B.readFile "./tests/fixtures/test.pdf" B.readFile "./tests/tmp/test.pdf" `shouldReturn` txtSrc + -- quoting (file without text) with file uses file name - alice ##> ("/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> itemId 3 <> ", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}") + alice ##> ("/_send @2 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> itemId 3 <> ", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]") alice <# "@bob > test.pdf" alice <## " test.jpg" alice <# "/f @bob ./tests/fixtures/test.jpg" @@ -785,23 +300,24 @@ testSendImageWithTextAndQuote = bob <## " test.jpg" bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" bob <## "use /fr 3 [<dir>/ | <path>] to receive it" + alice <## "completed uploading file 3 (test.jpg) for bob" + bob ##> "/fr 3 ./tests/tmp" - bob <## "saving file 3 from alice to ./tests/tmp/test_1.jpg" - concurrently_ - (bob <## "started receiving file 3 (test.jpg) from alice") - (alice <## "started sending file 3 (test.jpg) to bob") - concurrently_ - (bob <## "completed receiving file 3 (test.jpg) from alice") - (alice <## "completed sending file 3 (test.jpg) to bob") + bob + <### [ "saving file 3 from alice to ./tests/tmp/test_1.jpg", + "started receiving file 3 (test.jpg) from alice" + ] + bob <## "completed receiving file 3 (test.jpg) from alice" + B.readFile "./tests/tmp/test_1.jpg" `shouldReturn` src -testGroupSendImage :: HasCallStack => FilePath -> IO () +testGroupSendImage :: HasCallStack => TestParams -> IO () testGroupSendImage = testChat3 aliceProfile bobProfile cathProfile $ - \alice bob cath -> do + \alice bob cath -> withXFTPServer $ do createGroup3 "team" alice bob cath threadDelay 1000000 - alice ##> "/_send #1 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" + alice ##> "/_send #1 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]" alice <# "/f #team ./tests/fixtures/test.jpg" alice <## "use /fc 1 to cancel sending" concurrentlyN_ @@ -812,26 +328,22 @@ testGroupSendImage = cath <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" cath <## "use /fr 1 [<dir>/ | <path>] to receive it" ] - bob ##> "/fr 1 ./tests/tmp/" - bob <## "saving file 1 from alice to ./tests/tmp/test.jpg" - concurrentlyN_ - [ do - alice <## "started sending file 1 (test.jpg) to bob" - alice <## "completed sending file 1 (test.jpg) to bob", - do - bob <## "started receiving file 1 (test.jpg) from alice" - bob <## "completed receiving file 1 (test.jpg) from alice" - ] - cath ##> "/fr 1 ./tests/tmp/" - cath <## "saving file 1 from alice to ./tests/tmp/test_1.jpg" - concurrentlyN_ - [ do - alice <## "started sending file 1 (test.jpg) to cath" - alice <## "completed sending file 1 (test.jpg) to cath", - do - cath <## "started receiving file 1 (test.jpg) from alice" - cath <## "completed receiving file 1 (test.jpg) from alice" - ] + alice <## "completed uploading file 1 (test.jpg) for #team" + + bob ##> "/fr 1 ./tests/tmp" + bob + <### [ "saving file 1 from alice to ./tests/tmp/test.jpg", + "started receiving file 1 (test.jpg) from alice" + ] + bob <## "completed receiving file 1 (test.jpg) from alice" + + cath ##> "/fr 1 ./tests/tmp" + cath + <### [ "saving file 1 from alice to ./tests/tmp/test_1.jpg", + "started receiving file 1 (test.jpg) from alice" + ] + cath <## "completed receiving file 1 (test.jpg) from alice" + src <- B.readFile "./tests/fixtures/test.jpg" dest <- B.readFile "./tests/tmp/test.jpg" dest `shouldBe` src @@ -841,10 +353,10 @@ testGroupSendImage = bob #$> ("/_get chat #1 count=1", chatF, [((0, ""), Just "./tests/tmp/test.jpg")]) cath #$> ("/_get chat #1 count=1", chatF, [((0, ""), Just "./tests/tmp/test_1.jpg")]) -testGroupSendImageWithTextAndQuote :: HasCallStack => FilePath -> IO () +testGroupSendImageWithTextAndQuote :: HasCallStack => TestParams -> IO () testGroupSendImageWithTextAndQuote = testChat3 aliceProfile bobProfile cathProfile $ - \alice bob cath -> do + \alice bob cath -> withXFTPServer $ do createGroup3 "team" alice bob cath threadDelay 1000000 bob #> "#team hi team" @@ -853,16 +365,16 @@ testGroupSendImageWithTextAndQuote = (cath <# "#team bob> hi team") threadDelay 1000000 msgItemId <- lastItemId alice - alice ##> ("/_send #1 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> msgItemId <> ", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}") + alice ##> ("/_send #1 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> msgItemId <> ", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]") alice <# "#team > bob hi team" alice <## " hey bob" alice <# "/f #team ./tests/fixtures/test.jpg" alice <## "use /fc 1 to cancel sending" concurrentlyN_ [ do - bob <# "#team alice> > bob hi team" + bob <# "#team alice!> > bob hi team" bob <## " hey bob" - bob <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" + bob <# "#team alice!> sends file test.jpg (136.5 KiB / 139737 bytes)" bob <## "use /fr 1 [<dir>/ | <path>] to receive it", do cath <# "#team alice> > bob hi team" @@ -870,26 +382,22 @@ testGroupSendImageWithTextAndQuote = cath <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" cath <## "use /fr 1 [<dir>/ | <path>] to receive it" ] - bob ##> "/fr 1 ./tests/tmp/" - bob <## "saving file 1 from alice to ./tests/tmp/test.jpg" - concurrentlyN_ - [ do - alice <## "started sending file 1 (test.jpg) to bob" - alice <## "completed sending file 1 (test.jpg) to bob", - do - bob <## "started receiving file 1 (test.jpg) from alice" - bob <## "completed receiving file 1 (test.jpg) from alice" - ] - cath ##> "/fr 1 ./tests/tmp/" - cath <## "saving file 1 from alice to ./tests/tmp/test_1.jpg" - concurrentlyN_ - [ do - alice <## "started sending file 1 (test.jpg) to cath" - alice <## "completed sending file 1 (test.jpg) to cath", - do - cath <## "started receiving file 1 (test.jpg) from alice" - cath <## "completed receiving file 1 (test.jpg) from alice" - ] + alice <## "completed uploading file 1 (test.jpg) for #team" + + bob ##> "/fr 1 ./tests/tmp" + bob + <### [ "saving file 1 from alice to ./tests/tmp/test.jpg", + "started receiving file 1 (test.jpg) from alice" + ] + bob <## "completed receiving file 1 (test.jpg) from alice" + + cath ##> "/fr 1 ./tests/tmp" + cath + <### [ "saving file 1 from alice to ./tests/tmp/test_1.jpg", + "started receiving file 1 (test.jpg) from alice" + ] + cath <## "completed receiving file 1 (test.jpg) from alice" + src <- B.readFile "./tests/fixtures/test.jpg" dest <- B.readFile "./tests/tmp/test.jpg" dest `shouldBe` src @@ -902,141 +410,165 @@ testGroupSendImageWithTextAndQuote = cath #$> ("/_get chat #1 count=2", chat'', [((0, "hi team"), Nothing, Nothing), ((0, "hey bob"), Just (0, "hi team"), Just "./tests/tmp/test_1.jpg")]) cath @@@ [("#team", "hey bob"), ("@alice", "received invitation to join group team as admin")] -testAsyncFileTransferSenderRestarts :: HasCallStack => FilePath -> IO () -testAsyncFileTransferSenderRestarts tmp = do - withNewTestChat tmp "bob" bobProfile $ \bob -> do - withNewTestChat tmp "alice" aliceProfile $ \alice -> do +testSendMultiFilesDirect :: HasCallStack => TestParams -> IO () +testSendMultiFilesDirect = + testChat2 aliceProfile bobProfile $ \alice bob -> do + withXFTPServer $ do connectUsers alice bob - startFileTransfer' alice bob "test_1MB.pdf" "1017.7 KiB / 1042157 bytes" - threadDelay 100000 - withTestChatContactConnected tmp "alice" $ \alice -> do - alice <## "completed sending file 1 (test_1MB.pdf) to bob" - bob <## "completed receiving file 1 (test_1MB.pdf) from alice" - src <- B.readFile "./tests/fixtures/test_1MB.pdf" - dest <- B.readFile "./tests/tmp/test_1MB.pdf" - dest `shouldBe` src -testAsyncFileTransferReceiverRestarts :: HasCallStack => FilePath -> IO () -testAsyncFileTransferReceiverRestarts tmp = do - withNewTestChat tmp "alice" aliceProfile $ \alice -> do - withNewTestChat tmp "bob" bobProfile $ \bob -> do - connectUsers alice bob - startFileTransfer' alice bob "test_1MB.pdf" "1017.7 KiB / 1042157 bytes" - threadDelay 100000 - withTestChatContactConnected tmp "bob" $ \bob -> do - alice <## "completed sending file 1 (test_1MB.pdf) to bob" - bob <## "completed receiving file 1 (test_1MB.pdf) from alice" - src <- B.readFile "./tests/fixtures/test_1MB.pdf" - dest <- B.readFile "./tests/tmp/test_1MB.pdf" - dest `shouldBe` src + alice #$> ("/_files_folder ./tests/tmp/alice_app_files", id, "ok") + copyFile "./tests/fixtures/test.jpg" "./tests/tmp/alice_app_files/test.jpg" + copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_app_files/test.pdf" + bob #$> ("/_files_folder ./tests/tmp/bob_app_files", id, "ok") -testAsyncFileTransfer :: HasCallStack => FilePath -> IO () -testAsyncFileTransfer tmp = do - withNewTestChat tmp "alice" aliceProfile $ \alice -> - withNewTestChat tmp "bob" bobProfile $ \bob -> - connectUsers alice bob - withTestChatContactConnected tmp "alice" $ \alice -> do - alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\":\"text\", \"text\": \"hi, sending a file\"}}" - alice <# "@bob hi, sending a file" - alice <# "/f @bob ./tests/fixtures/test.jpg" - alice <## "use /fc 1 to cancel sending" - withTestChatContactConnected tmp "bob" $ \bob -> do - bob <# "alice> hi, sending a file" - bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" - bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - bob ##> "/fr 1 ./tests/tmp" - bob <## "saving file 1 from alice to ./tests/tmp/test.jpg" - -- withTestChatContactConnected' tmp "alice" -- TODO not needed in v2 - -- withTestChatContactConnected' tmp "bob" -- TODO not needed in v2 - withTestChatContactConnected' tmp "alice" - withTestChatContactConnected' tmp "bob" - withTestChatContactConnected tmp "alice" $ \alice -> do - alice <## "started sending file 1 (test.jpg) to bob" - alice <## "completed sending file 1 (test.jpg) to bob" - withTestChatContactConnected tmp "bob" $ \bob -> do - bob <## "started receiving file 1 (test.jpg) from alice" - bob <## "completed receiving file 1 (test.jpg) from alice" - src <- B.readFile "./tests/fixtures/test.jpg" - dest <- B.readFile "./tests/tmp/test.jpg" - dest `shouldBe` src + let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message without file\"}}" + cm2 = "{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 1\"}}" + cm3 = "{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 2\"}}" + alice ##> ("/_send @2 json [" <> cm1 <> "," <> cm2 <> "," <> cm3 <> "]") -testAsyncFileTransferV1 :: HasCallStack => FilePath -> IO () -testAsyncFileTransferV1 tmp = do - withNewTestChatV1 tmp "alice" aliceProfile $ \alice -> - withNewTestChatV1 tmp "bob" bobProfile $ \bob -> - connectUsers alice bob - withTestChatContactConnectedV1 tmp "alice" $ \alice -> do - alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\":\"text\", \"text\": \"hi, sending a file\"}}" - alice <# "@bob hi, sending a file" - alice <# "/f @bob ./tests/fixtures/test.jpg" - alice <## "use /fc 1 to cancel sending" - withTestChatContactConnectedV1 tmp "bob" $ \bob -> do - bob <# "alice> hi, sending a file" - bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" - bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - bob ##> "/fr 1 ./tests/tmp" - bob <## "saving file 1 from alice to ./tests/tmp/test.jpg" - withTestChatContactConnectedV1' tmp "alice" -- TODO not needed in v2 - withTestChatContactConnectedV1' tmp "bob" -- TODO not needed in v2 - withTestChatContactConnectedV1' tmp "alice" - withTestChatContactConnectedV1' tmp "bob" - withTestChatContactConnectedV1 tmp "alice" $ \alice -> do - alice <## "started sending file 1 (test.jpg) to bob" - alice <## "completed sending file 1 (test.jpg) to bob" - withTestChatContactConnectedV1 tmp "bob" $ \bob -> do - bob <## "started receiving file 1 (test.jpg) from alice" - bob <## "completed receiving file 1 (test.jpg) from alice" - src <- B.readFile "./tests/fixtures/test.jpg" - dest <- B.readFile "./tests/tmp/test.jpg" - dest `shouldBe` src + alice <# "@bob message without file" -testAsyncGroupFileTransfer :: HasCallStack => FilePath -> IO () -testAsyncGroupFileTransfer tmp = do - withNewTestChat tmp "alice" aliceProfile $ \alice -> - withNewTestChat tmp "bob" bobProfile $ \bob -> - withNewTestChat tmp "cath" cathProfile $ \cath -> - createGroup3 "team" alice bob cath - withTestChatGroup3Connected tmp "alice" $ \alice -> do - alice ##> "/_send #1 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"text\"}}" - alice <# "/f #team ./tests/fixtures/test.jpg" - alice <## "use /fc 1 to cancel sending" - withTestChatGroup3Connected tmp "bob" $ \bob -> do - bob <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" - bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - bob ##> "/fr 1 ./tests/tmp/" - bob <## "saving file 1 from alice to ./tests/tmp/test.jpg" - withTestChatGroup3Connected tmp "cath" $ \cath -> do - cath <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" - cath <## "use /fr 1 [<dir>/ | <path>] to receive it" - cath ##> "/fr 1 ./tests/tmp/" - cath <## "saving file 1 from alice to ./tests/tmp/test_1.jpg" - withTestChatGroup3Connected' tmp "alice" - withTestChatGroup3Connected' tmp "bob" - withTestChatGroup3Connected' tmp "cath" - -- withTestChatGroup3Connected' tmp "alice" -- TODO not needed in v2 - -- withTestChatGroup3Connected' tmp "bob" -- TODO not needed in v2 - -- withTestChatGroup3Connected' tmp "cath" -- TODO not needed in v2 - withTestChatGroup3Connected' tmp "alice" - withTestChatGroup3Connected tmp "bob" $ \bob -> do - bob <## "started receiving file 1 (test.jpg) from alice" - withTestChatGroup3Connected tmp "cath" $ \cath -> do - cath <## "started receiving file 1 (test.jpg) from alice" - withTestChatGroup3Connected tmp "alice" $ \alice -> do - alice - <### [ "started sending file 1 (test.jpg) to bob", - "completed sending file 1 (test.jpg) to bob", - "started sending file 1 (test.jpg) to cath", - "completed sending file 1 (test.jpg) to cath" - ] - withTestChatGroup3Connected tmp "bob" $ \bob -> do - bob <## "completed receiving file 1 (test.jpg) from alice" - withTestChatGroup3Connected tmp "cath" $ \cath -> do - cath <## "completed receiving file 1 (test.jpg) from alice" - src <- B.readFile "./tests/fixtures/test.jpg" - dest <- B.readFile "./tests/tmp/test.jpg" - dest `shouldBe` src - dest2 <- B.readFile "./tests/tmp/test_1.jpg" - dest2 `shouldBe` src + alice <# "@bob sending file 1" + alice <# "/f @bob test.jpg" + alice <## "use /fc 1 to cancel sending" + + alice <# "@bob sending file 2" + alice <# "/f @bob test.pdf" + alice <## "use /fc 2 to cancel sending" + + bob <# "alice> message without file" + + bob <# "alice> sending file 1" + bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" + bob <## "use /fr 1 [<dir>/ | <path>] to receive it" + + bob <# "alice> sending file 2" + bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" + bob <## "use /fr 2 [<dir>/ | <path>] to receive it" + + alice <## "completed uploading file 1 (test.jpg) for bob" + alice <## "completed uploading file 2 (test.pdf) for bob" + + bob ##> "/fr 1" + bob + <### [ "saving file 1 from alice to test.jpg", + "started receiving file 1 (test.jpg) from alice" + ] + bob <## "completed receiving file 1 (test.jpg) from alice" + + bob ##> "/fr 2" + bob + <### [ "saving file 2 from alice to test.pdf", + "started receiving file 2 (test.pdf) from alice" + ] + bob <## "completed receiving file 2 (test.pdf) from alice" + + src1 <- B.readFile "./tests/tmp/alice_app_files/test.jpg" + dest1 <- B.readFile "./tests/tmp/bob_app_files/test.jpg" + dest1 `shouldBe` src1 + + src2 <- B.readFile "./tests/tmp/alice_app_files/test.pdf" + dest2 <- B.readFile "./tests/tmp/bob_app_files/test.pdf" + dest2 `shouldBe` src2 + + alice #$> ("/_get chat @2 count=3", chatF, [((1, "message without file"), Nothing), ((1, "sending file 1"), Just "test.jpg"), ((1, "sending file 2"), Just "test.pdf")]) + bob #$> ("/_get chat @2 count=3", chatF, [((0, "message without file"), Nothing), ((0, "sending file 1"), Just "test.jpg"), ((0, "sending file 2"), Just "test.pdf")]) + +testSendMultiFilesGroup :: HasCallStack => TestParams -> IO () +testSendMultiFilesGroup = + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + withXFTPServer $ do + createGroup3 "team" alice bob cath + + threadDelay 1000000 + + alice #$> ("/_files_folder ./tests/tmp/alice_app_files", id, "ok") + copyFile "./tests/fixtures/test.jpg" "./tests/tmp/alice_app_files/test.jpg" + copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_app_files/test.pdf" + bob #$> ("/_files_folder ./tests/tmp/bob_app_files", id, "ok") + cath #$> ("/_files_folder ./tests/tmp/cath_app_files", id, "ok") + + let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message without file\"}}" + cm2 = "{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 1\"}}" + cm3 = "{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 2\"}}" + alice ##> ("/_send #1 json [" <> cm1 <> "," <> cm2 <> "," <> cm3 <> "]") + + alice <# "#team message without file" + + alice <# "#team sending file 1" + alice <# "/f #team test.jpg" + alice <## "use /fc 1 to cancel sending" + + alice <# "#team sending file 2" + alice <# "/f #team test.pdf" + alice <## "use /fc 2 to cancel sending" + + bob <# "#team alice> message without file" + + bob <# "#team alice> sending file 1" + bob <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" + bob <## "use /fr 1 [<dir>/ | <path>] to receive it" + + bob <# "#team alice> sending file 2" + bob <# "#team alice> sends file test.pdf (266.0 KiB / 272376 bytes)" + bob <## "use /fr 2 [<dir>/ | <path>] to receive it" + + cath <# "#team alice> message without file" + + cath <# "#team alice> sending file 1" + cath <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" + cath <## "use /fr 1 [<dir>/ | <path>] to receive it" + + cath <# "#team alice> sending file 2" + cath <# "#team alice> sends file test.pdf (266.0 KiB / 272376 bytes)" + cath <## "use /fr 2 [<dir>/ | <path>] to receive it" + + alice <## "completed uploading file 1 (test.jpg) for #team" + alice <## "completed uploading file 2 (test.pdf) for #team" + + bob ##> "/fr 1" + bob + <### [ "saving file 1 from alice to test.jpg", + "started receiving file 1 (test.jpg) from alice" + ] + bob <## "completed receiving file 1 (test.jpg) from alice" + + bob ##> "/fr 2" + bob + <### [ "saving file 2 from alice to test.pdf", + "started receiving file 2 (test.pdf) from alice" + ] + bob <## "completed receiving file 2 (test.pdf) from alice" + + cath ##> "/fr 1" + cath + <### [ "saving file 1 from alice to test.jpg", + "started receiving file 1 (test.jpg) from alice" + ] + cath <## "completed receiving file 1 (test.jpg) from alice" + + cath ##> "/fr 2" + cath + <### [ "saving file 2 from alice to test.pdf", + "started receiving file 2 (test.pdf) from alice" + ] + cath <## "completed receiving file 2 (test.pdf) from alice" + + src1 <- B.readFile "./tests/tmp/alice_app_files/test.jpg" + dest1_1 <- B.readFile "./tests/tmp/bob_app_files/test.jpg" + dest1_2 <- B.readFile "./tests/tmp/cath_app_files/test.jpg" + dest1_1 `shouldBe` src1 + dest1_2 `shouldBe` src1 + + src2 <- B.readFile "./tests/tmp/alice_app_files/test.pdf" + dest2_1 <- B.readFile "./tests/tmp/bob_app_files/test.pdf" + dest2_2 <- B.readFile "./tests/tmp/cath_app_files/test.pdf" + dest2_1 `shouldBe` src2 + dest2_2 `shouldBe` src2 + + alice #$> ("/_get chat #1 count=3", chatF, [((1, "message without file"), Nothing), ((1, "sending file 1"), Just "test.jpg"), ((1, "sending file 2"), Just "test.pdf")]) + bob #$> ("/_get chat #1 count=3", chatF, [((0, "message without file"), Nothing), ((0, "sending file 1"), Just "test.jpg"), ((0, "sending file 2"), Just "test.pdf")]) + cath #$> ("/_get chat #1 count=3", chatF, [((0, "message without file"), Nothing), ((0, "sending file 1"), Just "test.jpg"), ((0, "sending file 2"), Just "test.pdf")]) testXFTPRoundFDCount :: Expectation testXFTPRoundFDCount = do @@ -1051,15 +583,14 @@ testXFTPRoundFDCount = do roundedFDCount 128 `shouldBe` 128 roundedFDCount 500 `shouldBe` 512 -testXFTPFileTransfer :: HasCallStack => FilePath -> IO () +testXFTPFileTransfer :: HasCallStack => TestParams -> IO () testXFTPFileTransfer = - testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do + testChat2 aliceProfile bobProfile $ \alice bob -> do withXFTPServer $ do connectUsers alice bob alice #> "/f @bob ./tests/fixtures/test.pdf" alice <## "use /fc 1 to cancel sending" - -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" bob <## "use /fr 1 [<dir>/ | <path>] to receive it" bob ##> "/fr 1 ./tests/tmp" @@ -1080,12 +611,10 @@ testXFTPFileTransfer = src <- B.readFile "./tests/fixtures/test.pdf" dest <- B.readFile "./tests/tmp/test.pdf" dest `shouldBe` src - where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} -testXFTPFileTransferEncrypted :: HasCallStack => FilePath -> IO () +testXFTPFileTransferEncrypted :: HasCallStack => TestParams -> IO () testXFTPFileTransferEncrypted = - testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do + testChat2 aliceProfile bobProfile $ \alice bob -> do src <- B.readFile "./tests/fixtures/test.pdf" srcLen <- getFileSize "./tests/fixtures/test.pdf" let srcPath = "./tests/tmp/alice/test.pdf" @@ -1095,7 +624,7 @@ testXFTPFileTransferEncrypted = let fileJSON = LB.unpack $ J.encode $ CryptoFile srcPath $ Just cfArgs withXFTPServer $ do connectUsers alice bob - alice ##> ("/_send @2 json {\"msgContent\":{\"type\":\"file\", \"text\":\"\"}, \"fileSource\": " <> fileJSON <> "}") + alice ##> ("/_send @2 json [{\"msgContent\":{\"type\":\"file\", \"text\":\"\"}, \"fileSource\": " <> fileJSON <> "}]") alice <# "/f @bob ./tests/tmp/alice/test.pdf" alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" @@ -1109,12 +638,10 @@ testXFTPFileTransferEncrypted = Right dest <- chatReadFile "./tests/tmp/bob/test.pdf" (strEncode key) (strEncode nonce) LB.length dest `shouldBe` fromIntegral srcLen LB.toStrict dest `shouldBe` src - where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} -testXFTPAcceptAfterUpload :: HasCallStack => FilePath -> IO () +testXFTPAcceptAfterUpload :: HasCallStack => TestParams -> IO () testXFTPAcceptAfterUpload = - testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do + testChat2 aliceProfile bobProfile $ \alice bob -> do withXFTPServer $ do connectUsers alice bob @@ -1122,7 +649,6 @@ testXFTPAcceptAfterUpload = alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? alice <## "completed uploading file 1 (test.pdf) for bob" threadDelay 100000 @@ -1137,12 +663,10 @@ testXFTPAcceptAfterUpload = src <- B.readFile "./tests/fixtures/test.pdf" dest <- B.readFile "./tests/tmp/test.pdf" dest `shouldBe` src - where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} -testXFTPGroupFileTransfer :: HasCallStack => FilePath -> IO () +testXFTPGroupFileTransfer :: HasCallStack => TestParams -> IO () testXFTPGroupFileTransfer = - testChatCfg3 cfg aliceProfile bobProfile cathProfile $ \alice bob cath -> do + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do withXFTPServer $ do createGroup3 "team" alice bob cath @@ -1156,7 +680,6 @@ testXFTPGroupFileTransfer = cath <# "#team alice> sends file test.pdf (266.0 KiB / 272376 bytes)" cath <## "use /fr 1 [<dir>/ | <path>] to receive it" ] - -- alice <## "started sending file 1 (test.pdf) to #team" -- TODO "started uploading" ? alice <## "completed uploading file 1 (test.pdf) for #team" bob ##> "/fr 1 ./tests/tmp" @@ -1178,12 +701,10 @@ testXFTPGroupFileTransfer = dest2 <- B.readFile "./tests/tmp/test_1.pdf" dest1 `shouldBe` src dest2 `shouldBe` src - where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} -testXFTPDeleteUploadedFile :: HasCallStack => FilePath -> IO () +testXFTPDeleteUploadedFile :: HasCallStack => TestParams -> IO () testXFTPDeleteUploadedFile = - testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do + testChat2 aliceProfile bobProfile $ \alice bob -> do withXFTPServer $ do connectUsers alice bob @@ -1191,7 +712,6 @@ testXFTPDeleteUploadedFile = alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? alice <## "completed uploading file 1 (test.pdf) for bob" alice ##> "/fc 1" @@ -1202,12 +722,10 @@ testXFTPDeleteUploadedFile = bob ##> "/fr 1 ./tests/tmp" bob <## "file cancelled: test.pdf" - where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} -testXFTPDeleteUploadedFileGroup :: HasCallStack => FilePath -> IO () +testXFTPDeleteUploadedFileGroup :: HasCallStack => TestParams -> IO () testXFTPDeleteUploadedFileGroup = - testChatCfg3 cfg aliceProfile bobProfile cathProfile $ \alice bob cath -> do + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do withXFTPServer $ do createGroup3 "team" alice bob cath @@ -1221,7 +739,6 @@ testXFTPDeleteUploadedFileGroup = cath <# "#team alice> sends file test.pdf (266.0 KiB / 272376 bytes)" cath <## "use /fr 1 [<dir>/ | <path>] to receive it" ] - -- alice <## "started sending file 1 (test.pdf) to #team" -- TODO "started uploading" ? alice <## "completed uploading file 1 (test.pdf) for #team" bob ##> "/fr 1 ./tests/tmp" @@ -1257,67 +774,19 @@ testXFTPDeleteUploadedFileGroup = cath ##> "/fr 1 ./tests/tmp" cath <## "file cancelled: test.pdf" - where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} -testXFTPWithChangedConfig :: HasCallStack => FilePath -> IO () -testXFTPWithChangedConfig = - testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do - withXFTPServer $ do - alice #$> ("/_xftp off", id, "ok") - alice #$> ("/_xftp on {\"minFileSize\":1024}", id, "ok") - - bob #$> ("/xftp off", id, "ok") - bob #$> ("/xftp on size=1kb", id, "ok") - - connectUsers alice bob - - alice #> "/f @bob ./tests/fixtures/test.pdf" - alice <## "use /fc 1 to cancel sending" - -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? - bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" - bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - bob ##> "/fr 1 ./tests/tmp" - concurrentlyN_ - [ alice <## "completed uploading file 1 (test.pdf) for bob", - bob - <### [ "saving file 1 from alice to ./tests/tmp/test.pdf", - "started receiving file 1 (test.pdf) from alice" - ] - ] - bob <## "completed receiving file 1 (test.pdf) from alice" - - src <- B.readFile "./tests/fixtures/test.pdf" - dest <- B.readFile "./tests/tmp/test.pdf" - dest `shouldBe` src - where - cfg = testCfg {tempDir = Just "./tests/tmp"} - -testXFTPWithRelativePaths :: HasCallStack => FilePath -> IO () +testXFTPWithRelativePaths :: HasCallStack => TestParams -> IO () testXFTPWithRelativePaths = - testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do + testChat2 aliceProfile bobProfile $ \alice bob -> do withXFTPServer $ do -- agent is passed xftp work directory only on chat start, -- so for test we work around by stopping and starting chat - alice ##> "/_stop" - alice <## "chat stopped" - alice #$> ("/_files_folder ./tests/fixtures", id, "ok") - alice #$> ("/_temp_folder ./tests/tmp/alice_xftp", id, "ok") - alice ##> "/_start" - alice <## "chat started" - - bob ##> "/_stop" - bob <## "chat stopped" - bob #$> ("/_files_folder ./tests/tmp/bob_files", id, "ok") - bob #$> ("/_temp_folder ./tests/tmp/bob_xftp", id, "ok") - bob ##> "/_start" - bob <## "chat started" - + setRelativePaths alice "./tests/fixtures" "./tests/tmp/alice_xftp" + setRelativePaths bob "./tests/tmp/bob_files" "./tests/tmp/bob_xftp" connectUsers alice bob alice #> "/f @bob test.pdf" alice <## "use /fc 1 to cancel sending" - -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" bob <## "use /fr 1 [<dir>/ | <path>] to receive it" bob ##> "/fr 1" @@ -1333,25 +802,22 @@ testXFTPWithRelativePaths = src <- B.readFile "./tests/fixtures/test.pdf" dest <- B.readFile "./tests/tmp/bob_files/test.pdf" dest `shouldBe` src - where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}} -testXFTPContinueRcv :: HasCallStack => FilePath -> IO () -testXFTPContinueRcv tmp = do +testXFTPContinueRcv :: HasCallStack => TestParams -> IO () +testXFTPContinueRcv ps = do withXFTPServer $ do - withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do - withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do connectUsers alice bob alice #> "/f @bob ./tests/fixtures/test.pdf" alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? alice <## "completed uploading file 1 (test.pdf) for bob" -- server is down - file is not received - withTestChatCfg tmp cfg "bob" $ \bob -> do + withTestChat ps "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob ##> "/fr 1 ./tests/tmp" bob @@ -1366,18 +832,16 @@ testXFTPContinueRcv tmp = do withXFTPServer $ do -- server is up - file reception is continued - withTestChatCfg tmp cfg "bob" $ \bob -> do + withTestChat ps "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob <## "completed receiving file 1 (test.pdf) from alice" src <- B.readFile "./tests/fixtures/test.pdf" dest <- B.readFile "./tests/tmp/test.pdf" dest `shouldBe` src - where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} -testXFTPMarkToReceive :: HasCallStack => FilePath -> IO () +testXFTPMarkToReceive :: HasCallStack => TestParams -> IO () testXFTPMarkToReceive = do - testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do + testChat2 aliceProfile bobProfile $ \alice bob -> do withXFTPServer $ do connectUsers alice bob @@ -1385,7 +849,6 @@ testXFTPMarkToReceive = do alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? alice <## "completed uploading file 1 (test.pdf) for bob" bob #$> ("/_set_file_to_receive 1", id, "ok") @@ -1412,26 +875,23 @@ testXFTPMarkToReceive = do src <- B.readFile "./tests/fixtures/test.pdf" dest <- B.readFile "./tests/tmp/bob_files/test.pdf" dest `shouldBe` src - where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}} -testXFTPRcvError :: HasCallStack => FilePath -> IO () -testXFTPRcvError tmp = do +testXFTPRcvError :: HasCallStack => TestParams -> IO () +testXFTPRcvError ps = do withXFTPServer $ do - withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do - withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do connectUsers alice bob alice #> "/f @bob ./tests/fixtures/test.pdf" alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" bob <## "use /fr 1 [<dir>/ | <path>] to receive it" - -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? 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 - withTestChatCfg tmp cfg "bob" $ \bob -> do + withTestChat ps "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob ##> "/fr 1 ./tests/tmp" bob @@ -1442,11 +902,9 @@ testXFTPRcvError tmp = do _ <- getTermLine bob bob ##> "/fs 1" - bob <## "receiving file 1 (test.pdf) error" - where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} + bob <## "receiving file 1 (test.pdf) error: FileErrAuth" -testXFTPCancelRcvRepeat :: HasCallStack => FilePath -> IO () +testXFTPCancelRcvRepeat :: HasCallStack => TestParams -> IO () testXFTPCancelRcvRepeat = testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do withXFTPServer $ do @@ -1456,7 +914,6 @@ testXFTPCancelRcvRepeat = alice #> "/f @bob ./tests/tmp/testfile" alice <## "use /fc 1 to cancel sending" - -- alice <## "started sending file 1 (testfile) to bob" -- TODO "started uploading" ? bob <# "alice> sends file testfile (17.0 MiB / 17825792 bytes)" bob <## "use /fr 1 [<dir>/ | <path>] to receive it" bob ##> "/fr 1 ./tests/tmp" @@ -1482,7 +939,8 @@ testXFTPCancelRcvRepeat = bob ##> "/fr 1 ./tests/tmp" bob <### [ "saving file 1 from alice to ./tests/tmp/testfile_1", - "started receiving file 1 (testfile) from alice" + "started receiving file 1 (testfile) from alice", + StartsWith "chat db error: SERcvFileNotFoundXFTP" ] bob <## "completed receiving file 1 (testfile) from alice" @@ -1493,11 +951,11 @@ testXFTPCancelRcvRepeat = dest <- B.readFile "./tests/tmp/testfile_1" dest `shouldBe` src where - cfg = testCfg {xftpDescrPartSize = 200, xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} + cfg = testCfg {xftpDescrPartSize = 200} -testAutoAcceptFile :: HasCallStack => FilePath -> IO () +testAutoAcceptFile :: HasCallStack => TestParams -> IO () testAutoAcceptFile = - testChatCfgOpts2 cfg opts aliceProfile bobProfile $ \alice bob -> withXFTPServer $ do + testChatOpts2 opts aliceProfile bobProfile $ \alice bob -> withXFTPServer $ do connectUsers alice bob bob ##> "/_files_folder ./tests/tmp/bob_files" bob <## "ok" @@ -1518,12 +976,11 @@ testAutoAcceptFile = -- no auto accept for large files (bob </) where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} opts = (testOpts :: ChatOpts) {autoAcceptFileSize = 200000} -testProhibitFiles :: HasCallStack => FilePath -> IO () +testProhibitFiles :: HasCallStack => TestParams -> IO () testProhibitFiles = - testChatCfg3 cfg aliceProfile bobProfile cathProfile $ \alice bob cath -> withXFTPServer $ do + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> withXFTPServer $ do createGroup3 "team" alice bob cath alice ##> "/set files #team off" alice <## "updated group preferences:" @@ -1542,22 +999,207 @@ testProhibitFiles = alice <## "bad chat command: feature not allowed Files and media" (bob </) (cath </) - where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} -startFileTransfer :: HasCallStack => TestCC -> TestCC -> IO () -startFileTransfer alice bob = - startFileTransfer' alice bob "test.jpg" "136.5 KiB / 139737 bytes" +testXFTPStandaloneSmall :: HasCallStack => TestParams -> IO () +testXFTPStandaloneSmall = testChat2 aliceProfile aliceDesktopProfile $ \src dst -> do + withXFTPServer $ do + logNote "sending" + src ##> "/_upload 1 ./tests/fixtures/logo.jpg" + src <## "started standalone uploading file 1 (logo.jpg)" + -- silent progress events + threadDelay 250000 + src <## "file 1 (logo.jpg) upload complete. download with:" + -- file description fits, enjoy the direct URIs + _uri1 <- getTermLine src + _uri2 <- getTermLine src + uri3 <- getTermLine src + _uri4 <- getTermLine src -startFileTransfer' :: HasCallStack => TestCC -> TestCC -> String -> String -> IO () -startFileTransfer' cc1 cc2 fName fSize = startFileTransferWithDest' cc1 cc2 fName fSize $ Just "./tests/tmp" + logNote "receiving" + let dstFile = "./tests/tmp/logo.jpg" + dst ##> ("/_download 1 " <> uri3 <> " " <> dstFile) + dst <## "started standalone receiving file 1 (logo.jpg)" + -- silent progress events + threadDelay 250000 + dst <## "completed standalone receiving file 1 (logo.jpg)" + srcBody <- B.readFile "./tests/fixtures/logo.jpg" + B.readFile dstFile `shouldReturn` srcBody -checkPartialTransfer :: HasCallStack => String -> IO () -checkPartialTransfer fileName = do - src <- B.readFile $ "./tests/fixtures/" <> fileName - dest <- B.readFile $ "./tests/tmp/" <> fileName - B.unpack src `shouldStartWith` B.unpack dest - B.length src > B.length dest `shouldBe` True +testXFTPStandaloneSmallInfo :: HasCallStack => TestParams -> IO () +testXFTPStandaloneSmallInfo = testChat2 aliceProfile aliceDesktopProfile $ \src dst -> do + withXFTPServer $ do + logNote "sending" + src ##> "/_upload 1 ./tests/fixtures/logo.jpg" + src <## "started standalone uploading file 1 (logo.jpg)" + -- silent progress events + threadDelay 250000 + src <## "file 1 (logo.jpg) upload complete. download with:" + -- file description fits, enjoy the direct URIs + _uri1 <- getTermLine src + _uri2 <- getTermLine src + uri3 <- getTermLine src + _uri4 <- getTermLine src + let uri = uri3 <> "&data=" <> B.unpack (urlEncode False . LB.toStrict . J.encode $ J.object ["secret" J..= J.String "*********"]) -waitFileExists :: HasCallStack => FilePath -> IO () -waitFileExists f = unlessM (doesFileExist f) $ waitFileExists f + logNote "info" + dst ##> ("/_download info " <> uri) + dst <## "{\"secret\":\"*********\"}" + + logNote "receiving" + let dstFile = "./tests/tmp/logo.jpg" + dst ##> ("/_download 1 " <> uri <> " " <> dstFile) -- download sucessfully discarded extra info + dst <## "started standalone receiving file 1 (logo.jpg)" + -- silent progress events + threadDelay 250000 + dst <## "completed standalone receiving file 1 (logo.jpg)" + srcBody <- B.readFile "./tests/fixtures/logo.jpg" + B.readFile dstFile `shouldReturn` srcBody + +testXFTPStandaloneLarge :: HasCallStack => TestParams -> IO () +testXFTPStandaloneLarge = testChat2 aliceProfile aliceDesktopProfile $ \src dst -> do + withXFTPServer $ do + xftpCLI ["rand", "./tests/tmp/testfile.in", "17mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile.in"] + + logNote "sending" + src ##> "/_upload 1 ./tests/tmp/testfile.in" + src <## "started standalone uploading file 1 (testfile.in)" + -- silent progress events + threadDelay 250000 + src <## "file 1 (testfile.in) uploaded, preparing redirect file 2" + src <## "file 1 (testfile.in) upload complete. download with:" + uri <- getTermLine src + _uri2 <- getTermLine src + _uri3 <- getTermLine src + _uri4 <- getTermLine src + + logNote "receiving" + let dstFile = "./tests/tmp/testfile.out" + dst ##> ("/_download 1 " <> uri <> " " <> dstFile) + dst <## "started standalone receiving file 1 (testfile.out)" + -- silent progress events + threadDelay 250000 + dst <## "completed standalone receiving file 1 (testfile.out)" + srcBody <- B.readFile "./tests/tmp/testfile.in" + B.readFile dstFile `shouldReturn` srcBody + +testXFTPStandaloneLargeInfo :: HasCallStack => TestParams -> IO () +testXFTPStandaloneLargeInfo = testChat2 aliceProfile aliceDesktopProfile $ \src dst -> do + withXFTPServer $ do + xftpCLI ["rand", "./tests/tmp/testfile.in", "17mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile.in"] + + logNote "sending" + src ##> "/_upload 1 ./tests/tmp/testfile.in" + src <## "started standalone uploading file 1 (testfile.in)" + + -- silent progress events + threadDelay 250000 + src <## "file 1 (testfile.in) uploaded, preparing redirect file 2" + src <## "file 1 (testfile.in) upload complete. download with:" + uri1 <- getTermLine src + _uri2 <- getTermLine src + _uri3 <- getTermLine src + _uri4 <- getTermLine src + let uri = uri1 <> "&data=" <> B.unpack (urlEncode False . LB.toStrict . J.encode $ J.object ["secret" J..= J.String "*********"]) + + logNote "info" + dst ##> ("/_download info " <> uri) + dst <## "{\"secret\":\"*********\"}" + + logNote "receiving" + let dstFile = "./tests/tmp/testfile.out" + dst ##> ("/_download 1 " <> uri <> " " <> dstFile) + dst <## "started standalone receiving file 1 (testfile.out)" + -- silent progress events + threadDelay 250000 + dst <## "completed standalone receiving file 1 (testfile.out)" + srcBody <- B.readFile "./tests/tmp/testfile.in" + B.readFile dstFile `shouldReturn` srcBody + +testXFTPStandaloneCancelSnd :: HasCallStack => TestParams -> IO () +testXFTPStandaloneCancelSnd = testChat2 aliceProfile aliceDesktopProfile $ \src dst -> do + withXFTPServer $ do + xftpCLI ["rand", "./tests/tmp/testfile.in", "17mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile.in"] + + logNote "sending" + src ##> "/_upload 1 ./tests/tmp/testfile.in" + src <## "started standalone uploading file 1 (testfile.in)" + -- silent progress events + threadDelay 250000 + src <## "file 1 (testfile.in) uploaded, preparing redirect file 2" + src <## "file 1 (testfile.in) upload complete. download with:" + uri <- getTermLine src + _uri2 <- getTermLine src + _uri3 <- getTermLine src + _uri4 <- getTermLine src + + logNote "cancelling" + src ##> "/fc 1" + src <## "cancelled sending file 1 (testfile.in)" + threadDelay 1000000 + + logNote "trying to receive cancelled" + dst ##> ("/_download 1 " <> uri <> " " <> "./tests/tmp/should.not.extist") + dst <## "started standalone receiving file 1 (should.not.extist)" + threadDelay 100000 + logWarn "no error?" + dst <## "error receiving file 1 (should.not.extist)" + dst <## "INTERNAL {internalErr = \"XFTP {xftpErr = AUTH}\"}" + +testXFTPStandaloneRelativePaths :: HasCallStack => TestParams -> IO () +testXFTPStandaloneRelativePaths = testChat2 aliceProfile aliceDesktopProfile $ \src dst -> do + withXFTPServer $ do + logNote "sending" + src #$> ("/_files_folder ./tests/tmp/src_files", id, "ok") + src #$> ("/_temp_folder ./tests/tmp/src_xftp_temp", id, "ok") + + xftpCLI ["rand", "./tests/tmp/src_files/testfile.in", "17mb"] `shouldReturn` ["File created: " <> "./tests/tmp/src_files/testfile.in"] + + src ##> "/_upload 1 testfile.in" + src <## "started standalone uploading file 1 (testfile.in)" + -- silent progress events + threadDelay 250000 + src <## "file 1 (testfile.in) uploaded, preparing redirect file 2" + src <## "file 1 (testfile.in) upload complete. download with:" + uri <- getTermLine src + _uri2 <- getTermLine src + _uri3 <- getTermLine src + _uri4 <- getTermLine src + + logNote "receiving" + dst #$> ("/_files_folder ./tests/tmp/dst_files", id, "ok") + dst #$> ("/_temp_folder ./tests/tmp/dst_xftp_temp", id, "ok") + dst ##> ("/_download 1 " <> uri <> " testfile.out") + dst <## "started standalone receiving file 1 (testfile.out)" + -- silent progress events + threadDelay 250000 + dst <## "completed standalone receiving file 1 (testfile.out)" + srcBody <- B.readFile "./tests/tmp/src_files/testfile.in" + B.readFile "./tests/tmp/dst_files/testfile.out" `shouldReturn` srcBody + +testXFTPStandaloneCancelRcv :: HasCallStack => TestParams -> IO () +testXFTPStandaloneCancelRcv = testChat2 aliceProfile aliceDesktopProfile $ \src dst -> do + withXFTPServer $ do + xftpCLI ["rand", "./tests/tmp/testfile.in", "17mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile.in"] + + logNote "sending" + src ##> "/_upload 1 ./tests/tmp/testfile.in" + src <## "started standalone uploading file 1 (testfile.in)" + -- silent progress events + threadDelay 250000 + src <## "file 1 (testfile.in) uploaded, preparing redirect file 2" + src <## "file 1 (testfile.in) upload complete. download with:" + uri <- getTermLine src + _uri2 <- getTermLine src + _uri3 <- getTermLine src + _uri4 <- getTermLine src + + logNote "receiving" + let dstFile = "./tests/tmp/testfile.out" + dst ##> ("/_download 1 " <> uri <> " " <> dstFile) + dst <## "started standalone receiving file 1 (testfile.out)" + threadDelay 25000 -- give workers some time to avoid internal errors from starting tasks + logNote "cancelling" + dst ##> "/fc 1" + dst <## "cancelled receiving file 1 (testfile.out)" + threadDelay 25000 + doesFileExist dstFile `shouldReturn` False diff --git a/tests/ChatTests/Forward.hs b/tests/ChatTests/Forward.hs new file mode 100644 index 0000000000..483c2269b1 --- /dev/null +++ b/tests/ChatTests/Forward.hs @@ -0,0 +1,892 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PostfixOperators #-} + +module ChatTests.Forward where + +import ChatClient +import ChatTests.DBUtils +import ChatTests.Utils +import Control.Concurrent (threadDelay) +import qualified Data.ByteString.Char8 as B +import Data.List (intercalate) +import qualified Data.Text as T +import Simplex.Chat.Library.Commands (fixedImagePreview) +import Simplex.Chat.Types (ImageData (..)) +import System.Directory (copyFile, doesFileExist, removeFile) +import Test.Hspec hiding (it) + +chatForwardTests :: SpecWith TestParams +chatForwardTests = do + describe "forward messages" $ do + it "from contact to contact" testForwardContactToContact + it "from contact to group" testForwardContactToGroup + it "from contact to notes" testForwardContactToNotes + it "from group to contact" testForwardGroupToContact + it "from group to group" testForwardGroupToGroup + it "from group to notes" testForwardGroupToNotes + it "from notes to contact" testForwardNotesToContact + it "from notes to group" testForwardNotesToGroup + it "from notes to notes" testForwardNotesToNotes -- TODO forward between different folders when supported + describe "interactions with forwarded messages" $ do + it "preserve original forward info" testForwardPreserveInfo + it "received forwarded message is saved with new forward info" testForwardRcvMsgNewInfo + it "quoted message is not included" testForwardQuotedMsg + it "editing is prohibited" testForwardEditProhibited + it "delete for other" testForwardDeleteForOther + describe "forward files" $ do + it "from contact to contact" testForwardFileNoFilesFolder + it "with relative paths: from contact to contact" testForwardFileContactToContact + it "with relative paths: from group to notes" testForwardFileGroupToNotes + it "with relative paths: from notes to group" testForwardFileNotesToGroup + describe "multi forward api" $ do + it "from contact to contact" testForwardContactToContactMulti + it "from group to group" testForwardGroupToGroupMulti + it "with relative paths: multiple files from contact to contact" testMultiForwardFiles + +testForwardContactToContact :: HasCallStack => TestParams -> IO () +testForwardContactToContact = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + connectUsers alice bob + connectUsers alice cath + connectUsers bob cath + + alice #> "@bob hi" + bob <# "alice> hi" + msgId <- lastItemId alice + bob #> "@alice hey" + alice <# "bob> hey" + + alice ##> ("/_forward @3 @2 " <> msgId) + alice <# "@cath <- you @bob" + alice <## " hi" + cath <# "alice> -> forwarded" + cath <## " hi" + + alice `send` "@cath <- @bob hey" + alice <# "@cath <- @bob" + alice <## " hey" + cath <# "alice> -> forwarded" + cath <## " hey" + + -- read chat + alice ##> "/tail @cath 2" + alice <# "@cath <- you @bob" + alice <## " hi" + alice <# "@cath <- @bob" + alice <## " hey" + + cath ##> "/tail @alice 2" + cath <# "alice> -> forwarded" + cath <## " hi" + cath <# "alice> -> forwarded" + cath <## " hey" + + -- item info + alice ##> "/item info @cath hey" + alice <##. "sent at: " + alice <## "message history:" + alice .<## ": hey" + alice <##. "forwarded from: @bob, chat item id:" + +testForwardContactToGroup :: HasCallStack => TestParams -> IO () +testForwardContactToGroup = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + connectUsers alice bob + createGroup2 "team" alice cath + + alice #> "@bob hi" + bob <# "alice> hi" + bob #> "@alice hey" + alice <# "bob> hey" + + alice `send` "#team <- @bob hi" + alice <# "#team <- you @bob" + alice <## " hi" + cath <# "#team alice> -> forwarded" + cath <## " hi" + + alice `send` "#team <- @bob hey" + alice <# "#team <- @bob" + alice <## " hey" + cath <# "#team alice> -> forwarded" + cath <## " hey" + +testForwardContactToNotes :: HasCallStack => TestParams -> IO () +testForwardContactToNotes = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + createCCNoteFolder alice + connectUsers alice bob + + alice #> "@bob hi" + bob <# "alice> hi" + bob #> "@alice hey" + alice <# "bob> hey" + + alice `send` "* <- @bob hi" + alice <# "* <- you @bob" + alice <## " hi" + + alice `send` "* <- @bob hey" + alice <# "* <- @bob" + alice <## " hey" + +testForwardGroupToContact :: HasCallStack => TestParams -> IO () +testForwardGroupToContact = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup2 "team" alice bob + connectUsers alice cath + + alice #> "#team hi" + bob <# "#team alice> hi" + bob #> "#team hey" + alice <# "#team bob> hey" + + alice `send` "@cath <- #team hi" + alice <# "@cath <- you #team" + alice <## " hi" + cath <# "alice> -> forwarded" + cath <## " hi" + + alice `send` "@cath <- #team @bob hey" + alice <# "@cath <- #team" + alice <## " hey" + cath <# "alice> -> forwarded" + cath <## " hey" + +testForwardGroupToGroup :: HasCallStack => TestParams -> IO () +testForwardGroupToGroup = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup2 "team" alice bob + createGroup2 "club" alice cath + + alice #> "#team hi" + bob <# "#team alice> hi" + bob #> "#team hey" + alice <# "#team bob> hey" + + threadDelay 1000000 + + alice `send` "#club <- #team hi" + alice <# "#club <- you #team" + alice <## " hi" + cath <# "#club alice> -> forwarded" + cath <## " hi" + + threadDelay 1000000 + + alice `send` "#club <- #team hey" + alice <# "#club <- #team" + alice <## " hey" + cath <# "#club alice> -> forwarded" + cath <## " hey" + + -- read chat + alice ##> "/tail #club 2" + alice <# "#club <- you #team" + alice <## " hi" + alice <# "#club <- #team" + alice <## " hey" + + cath ##> "/tail #club 2" + cath <# "#club alice> -> forwarded" + cath <## " hi" + cath <# "#club alice> -> forwarded" + cath <## " hey" + +testForwardGroupToNotes :: HasCallStack => TestParams -> IO () +testForwardGroupToNotes = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + createCCNoteFolder alice + createGroup2 "team" alice bob + + alice #> "#team hi" + bob <# "#team alice> hi" + bob #> "#team hey" + alice <# "#team bob> hey" + + alice `send` "* <- #team hi" + alice <# "* <- you #team" + alice <## " hi" + + alice `send` "* <- #team hey" + alice <# "* <- #team" + alice <## " hey" + +testForwardNotesToContact :: HasCallStack => TestParams -> IO () +testForwardNotesToContact = + testChat2 aliceProfile cathProfile $ + \alice cath -> do + createCCNoteFolder alice + connectUsers alice cath + + alice >* "hi" + + alice `send` "@cath <- * hi" + alice <# "@cath hi" + cath <# "alice> hi" + +testForwardNotesToGroup :: HasCallStack => TestParams -> IO () +testForwardNotesToGroup = + testChat2 aliceProfile cathProfile $ + \alice cath -> do + createCCNoteFolder alice + createGroup2 "team" alice cath + + alice >* "hi" + + alice `send` "#team <- * hi" + alice <# "#team hi" + cath <# "#team alice> hi" + +testForwardNotesToNotes :: HasCallStack => TestParams -> IO () +testForwardNotesToNotes ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + createCCNoteFolder alice + + alice >* "hi" + + alice `send` "* <- * hi" + alice <# "* hi" + + alice ##> "/tail * 2" + alice <# "* hi" + alice <# "* hi" + +testForwardPreserveInfo :: HasCallStack => TestParams -> IO () +testForwardPreserveInfo = + testChat4 aliceProfile bobProfile cathProfile danProfile $ + \alice bob cath dan -> do + createCCNoteFolder alice + connectUsers alice bob + connectUsers alice cath + createGroup2 "team" alice dan + + bob #> "@alice hey" + alice <# "bob> hey" + + alice `send` "* <- @bob hey" + alice <# "* <- @bob" + alice <## " hey" + + alice `send` "@cath <- * hey" + alice <# "@cath <- @bob" + alice <## " hey" + cath <# "alice> -> forwarded" + cath <## " hey" + + alice `send` "#team <- @cath hey" + alice <# "#team <- @bob" + alice <## " hey" + dan <# "#team alice> -> forwarded" + dan <## " hey" + +testForwardRcvMsgNewInfo :: HasCallStack => TestParams -> IO () +testForwardRcvMsgNewInfo = + testChat4 aliceProfile bobProfile cathProfile danProfile $ + \alice bob cath dan -> do + connectUsers bob dan + createCCNoteFolder alice + connectUsers alice bob + connectUsers alice cath + + dan #> "@bob hey" + bob <# "dan> hey" + + bob `send` "@alice <- @dan hey" + bob <# "@alice <- @dan" + bob <## " hey" + alice <# "bob> -> forwarded" + alice <## " hey" + + alice `send` "* <- @bob hey" + alice <# "* <- @bob" + alice <## " hey" + + alice `send` "@cath <- * hey" + alice <# "@cath <- @bob" + alice <## " hey" + cath <# "alice> -> forwarded" + cath <## " hey" + +testForwardQuotedMsg :: HasCallStack => TestParams -> IO () +testForwardQuotedMsg = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + connectUsers alice bob + connectUsers alice cath + + alice #> "@bob hi" + bob <# "alice> hi" + bob `send` "> @alice (hi) hey" + bob <# "@alice > hi" + bob <## " hey" + alice <# "bob> > hi" + alice <## " hey" + + alice `send` "@cath <- @bob hey" + alice <# "@cath <- @bob" + alice <## " hey" + cath <# "alice> -> forwarded" + cath <## " hey" + + -- read chat + alice ##> "/tail @cath 1" + alice <# "@cath <- @bob" + alice <## " hey" + + cath ##> "/tail @alice 1" + cath <# "alice> -> forwarded" + cath <## " hey" + +testForwardEditProhibited :: HasCallStack => TestParams -> IO () +testForwardEditProhibited = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + connectUsers alice bob + connectUsers alice cath + + bob #> "@alice hey" + alice <# "bob> hey" + + alice `send` "@cath <- @bob hey" + alice <# "@cath <- @bob" + alice <## " hey" + cath <# "alice> -> forwarded" + cath <## " hey" + + msgId <- lastItemId alice + alice ##> ("/_update item @3 " <> msgId <> " text hey edited") + alice <## "cannot update this item" + +testForwardDeleteForOther :: HasCallStack => TestParams -> IO () +testForwardDeleteForOther = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + connectUsers alice bob + connectUsers alice cath + + bob #> "@alice hey" + alice <# "bob> hey" + + alice `send` "@cath <- @bob hey" + alice <# "@cath <- @bob" + alice <## " hey" + cath <# "alice> -> forwarded" + cath <## " hey" + + msgId <- lastItemId alice + alice ##> ("/_delete item @3 " <> msgId <> " broadcast") + alice <## "message marked deleted" + cath <# "alice> [marked deleted] hey" + +testForwardFileNoFilesFolder :: HasCallStack => TestParams -> IO () +testForwardFileNoFilesFolder = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> withXFTPServer $ do + connectUsers alice bob + connectUsers bob cath + + -- send original file + alice ##> "/_send @2 json [{\"filePath\": \"./tests/fixtures/test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}]" + alice <# "@bob hi" + alice <# "/f @bob ./tests/fixtures/test.pdf" + alice <## "use /fc 1 to cancel sending" + bob <# "alice> hi" + bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" + bob <## "use /fr 1 [<dir>/ | <path>] to receive it" + + bob ##> "/fr 1 ./tests/tmp" + concurrentlyN_ + [ alice <## "completed uploading file 1 (test.pdf) for bob", + bob + <### [ "saving file 1 from alice to ./tests/tmp/test.pdf", + "started receiving file 1 (test.pdf) from alice" + ] + ] + bob <## "completed receiving file 1 (test.pdf) from alice" + + src <- B.readFile "./tests/fixtures/test.pdf" + dest <- B.readFile "./tests/tmp/test.pdf" + dest `shouldBe` src + + -- forward file + bob `send` "@cath <- @alice hi" + bob <# "@cath <- @alice" + bob <## " hi" + bob <# "/f @cath ./tests/tmp/test.pdf" + bob <## "use /fc 2 to cancel sending" + cath <# "bob> -> forwarded" + cath <## " hi" + cath <# "bob> sends file test.pdf (266.0 KiB / 272376 bytes)" + cath <## "use /fr 1 [<dir>/ | <path>] to receive it" + + cath ##> "/fr 1 ./tests/tmp" + concurrentlyN_ + [ bob <## "completed uploading file 2 (test.pdf) for cath", + cath + <### [ "saving file 1 from bob to ./tests/tmp/test_1.pdf", + "started receiving file 1 (test.pdf) from bob" + ] + ] + cath <## "completed receiving file 1 (test.pdf) from bob" + + dest2 <- B.readFile "./tests/tmp/test_1.pdf" + dest2 `shouldBe` src + +testForwardFileContactToContact :: HasCallStack => TestParams -> IO () +testForwardFileContactToContact = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> withXFTPServer $ do + setRelativePaths alice "./tests/fixtures" "./tests/tmp/alice_xftp" + setRelativePaths bob "./tests/tmp/bob_files" "./tests/tmp/bob_xftp" + setRelativePaths cath "./tests/tmp/cath_files" "./tests/tmp/cath_xftp" + connectUsers alice bob + connectUsers bob cath + + -- send original file + alice ##> "/_send @2 json [{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}]" + alice <# "@bob hi" + alice <# "/f @bob test.pdf" + alice <## "use /fc 1 to cancel sending" + bob <# "alice> hi" + bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" + bob <## "use /fr 1 [<dir>/ | <path>] to receive it" + + bob ##> "/fr 1" + concurrentlyN_ + [ alice <## "completed uploading file 1 (test.pdf) for bob", + bob + <### [ "saving file 1 from alice to test.pdf", + "started receiving file 1 (test.pdf) from alice" + ] + ] + bob <## "completed receiving file 1 (test.pdf) from alice" + + src <- B.readFile "./tests/fixtures/test.pdf" + dest <- B.readFile "./tests/tmp/bob_files/test.pdf" + dest `shouldBe` src + + -- forward file + bob `send` "@cath <- @alice hi" + bob <# "@cath <- @alice" + bob <## " hi" + bob <# "/f @cath test_1.pdf" + bob <## "use /fc 2 to cancel sending" + cath <# "bob> -> forwarded" + cath <## " hi" + cath <# "bob> sends file test_1.pdf (266.0 KiB / 272376 bytes)" + cath <## "use /fr 1 [<dir>/ | <path>] to receive it" + + cath ##> "/fr 1" + concurrentlyN_ + [ bob <## "completed uploading file 2 (test_1.pdf) for cath", + cath + <### [ "saving file 1 from bob to test_1.pdf", + "started receiving file 1 (test_1.pdf) from bob" + ] + ] + cath <## "completed receiving file 1 (test_1.pdf) from bob" + + src2 <- B.readFile "./tests/tmp/bob_files/test_1.pdf" + src2 `shouldBe` dest + dest2 <- B.readFile "./tests/tmp/cath_files/test_1.pdf" + dest2 `shouldBe` src2 + + -- deleting original file doesn't delete forwarded file + checkActionDeletesFile "./tests/tmp/bob_files/test.pdf" $ do + bob ##> "/clear alice" + bob <## "alice: all messages are removed locally ONLY" + fwdFileExists <- doesFileExist "./tests/tmp/bob_files/test_1.pdf" + fwdFileExists `shouldBe` True + +testForwardFileGroupToNotes :: HasCallStack => TestParams -> IO () +testForwardFileGroupToNotes = + testChat2 aliceProfile cathProfile $ + \alice cath -> withXFTPServer $ do + setRelativePaths alice "./tests/fixtures" "./tests/tmp/alice_xftp" + setRelativePaths cath "./tests/tmp/cath_files" "./tests/tmp/cath_xftp" + createGroup2 "team" alice cath + createCCNoteFolder cath + + -- send original file + alice ##> "/_send #1 json [{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}]" + alice <# "#team hi" + alice <# "/f #team test.pdf" + alice <## "use /fc 1 to cancel sending" + cath <# "#team alice> hi" + cath <# "#team alice> sends file test.pdf (266.0 KiB / 272376 bytes)" + cath <## "use /fr 1 [<dir>/ | <path>] to receive it" + + cath ##> "/fr 1" + concurrentlyN_ + [ alice <## "completed uploading file 1 (test.pdf) for #team", + cath + <### [ "saving file 1 from alice to test.pdf", + "started receiving file 1 (test.pdf) from alice" + ] + ] + cath <## "completed receiving file 1 (test.pdf) from alice" + + src <- B.readFile "./tests/fixtures/test.pdf" + dest <- B.readFile "./tests/tmp/cath_files/test.pdf" + dest `shouldBe` src + + -- forward file + cath `send` "* <- #team hi" + cath <# "* <- #team" + cath <## " hi" + cath <# "* file 2 (test_1.pdf)" + + dest2 <- B.readFile "./tests/tmp/cath_files/test_1.pdf" + dest2 `shouldBe` dest + + -- deleting original file doesn't delete forwarded file + checkActionDeletesFile "./tests/tmp/cath_files/test.pdf" $ do + cath ##> "/clear #team" + cath <## "#team: all messages are removed locally ONLY" + fwdFileExists <- doesFileExist "./tests/tmp/cath_files/test_1.pdf" + fwdFileExists `shouldBe` True + +testForwardFileNotesToGroup :: HasCallStack => TestParams -> IO () +testForwardFileNotesToGroup = + testChat2 aliceProfile cathProfile $ + \alice cath -> withXFTPServer $ do + setRelativePaths alice "./tests/tmp/alice_files" "./tests/tmp/alice_xftp" + setRelativePaths cath "./tests/tmp/cath_files" "./tests/tmp/cath_xftp" + copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_files/test.pdf" + createCCNoteFolder alice + createGroup2 "team" alice cath + + -- create original file + alice ##> "/_create *1 json [{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}]" + alice <# "* hi" + alice <# "* file 1 (test.pdf)" + + -- forward file + alice `send` "#team <- * hi" + alice <# "#team hi" + alice <# "/f #team test_1.pdf" + alice <## "use /fc 2 to cancel sending" + cath <# "#team alice> hi" + cath <# "#team alice> sends file test_1.pdf (266.0 KiB / 272376 bytes)" + cath <## "use /fr 1 [<dir>/ | <path>] to receive it" + + cath ##> "/fr 1" + concurrentlyN_ + [ alice <## "completed uploading file 2 (test_1.pdf) for #team", + cath + <### [ "saving file 1 from alice to test_1.pdf", + "started receiving file 1 (test_1.pdf) from alice" + ] + ] + cath <## "completed receiving file 1 (test_1.pdf) from alice" + + src <- B.readFile "./tests/tmp/alice_files/test.pdf" + src2 <- B.readFile "./tests/tmp/alice_files/test_1.pdf" + src2 `shouldBe` src + dest2 <- B.readFile "./tests/tmp/cath_files/test_1.pdf" + dest2 `shouldBe` src2 + + -- deleting original file doesn't delete forwarded file + checkActionDeletesFile "./tests/tmp/alice_files/test.pdf" $ do + alice ##> "/clear *" + alice <## "notes: all messages are removed" + fwdFileExists <- doesFileExist "./tests/tmp/alice_files/test_1.pdf" + fwdFileExists `shouldBe` True + +testForwardContactToContactMulti :: HasCallStack => TestParams -> IO () +testForwardContactToContactMulti = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + connectUsers alice bob + connectUsers alice cath + connectUsers bob cath + + alice #> "@bob hi" + bob <# "alice> hi" + msgId1 <- lastItemId alice + + threadDelay 1000000 + + bob #> "@alice hey" + alice <# "bob> hey" + msgId2 <- lastItemId alice + + alice ##> ("/_forward plan @2 " <> msgId1 <> "," <> msgId2) + alice <## "all messages can be forwarded" + alice ##> ("/_forward @3 @2 " <> msgId1 <> "," <> msgId2) + alice <# "@cath <- you @bob" + alice <## " hi" + alice <# "@cath <- @bob" + alice <## " hey" + cath <# "alice> -> forwarded" + cath <## " hi" + cath <# "alice> -> forwarded" + cath <## " hey" + +testForwardGroupToGroupMulti :: HasCallStack => TestParams -> IO () +testForwardGroupToGroupMulti = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup2 "team" alice bob + createGroup2 "club" alice cath + + threadDelay 1000000 + + alice #> "#team hi" + bob <# "#team alice> hi" + msgId1 <- lastItemId alice + + threadDelay 1000000 + + bob #> "#team hey" + alice <# "#team bob> hey" + msgId2 <- lastItemId alice + + alice ##> ("/_forward plan #1 " <> msgId1 <> "," <> msgId2) + alice <## "all messages can be forwarded" + alice ##> ("/_forward #2 #1 " <> msgId1 <> "," <> msgId2) + alice <# "#club <- you #team" + alice <## " hi" + alice <# "#club <- #team" + alice <## " hey" + cath <# "#club alice> -> forwarded" + cath <## " hi" + cath <# "#club alice> -> forwarded" + cath <## " hey" + + -- read chat + alice ##> "/tail #club 2" + alice <# "#club <- you #team" + alice <## " hi" + alice <# "#club <- #team" + alice <## " hey" + + cath ##> "/tail #club 2" + cath <# "#club alice> -> forwarded" + cath <## " hi" + cath <# "#club alice> -> forwarded" + cath <## " hey" + +testMultiForwardFiles :: HasCallStack => TestParams -> IO () +testMultiForwardFiles = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> withXFTPServer $ do + setRelativePaths alice "./tests/tmp/alice_app_files" "./tests/tmp/alice_xftp" + copyFile "./tests/fixtures/test.jpg" "./tests/tmp/alice_app_files/test.jpg" + copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_app_files/test.pdf" + copyFile "./tests/fixtures/test_1MB.pdf" "./tests/tmp/alice_app_files/test_1MB.pdf" + copyFile "./tests/fixtures/logo.jpg" "./tests/tmp/alice_app_files/logo.jpg" + setRelativePaths bob "./tests/tmp/bob_app_files" "./tests/tmp/bob_xftp" + setRelativePaths cath "./tests/tmp/cath_app_files" "./tests/tmp/cath_xftp" + connectUsers alice bob + connectUsers bob cath + + threadDelay 1000000 + + msgIdZero <- lastItemId bob + + bob #> "@alice hi" + alice <# "bob> hi" + + -- send original files + let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message without file\"}}" + ImageData img = fixedImagePreview + cm2 = "{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"image\", \"image\":\"" <> T.unpack img <> "\", \"text\": \"\"}}" + cm3 = "{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"file\", \"text\": \"\"}}" + cm4 = "{\"filePath\": \"test_1MB.pdf\", \"msgContent\": {\"type\": \"file\", \"text\": \"message with large file\"}}" + cm5 = "{\"filePath\": \"logo.jpg\", \"msgContent\": {\"type\": \"image\", \"image\":\"" <> T.unpack img <> "\", \"text\": \"\"}}" + alice ##> ("/_send @2 json [" <> intercalate "," [cm1, cm2, cm3, cm4, cm5] <> "]") + + alice <# "@bob message without file" + + alice <# "/f @bob test.jpg" + alice <## "use /fc 1 to cancel sending" + + alice <# "/f @bob test.pdf" + alice <## "use /fc 2 to cancel sending" + + alice <# "@bob message with large file" + alice <# "/f @bob test_1MB.pdf" + alice <## "use /fc 3 to cancel sending" + + alice <# "/f @bob logo.jpg" + alice <## "use /fc 4 to cancel sending" + + bob <# "alice> message without file" + + bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" + bob <## "use /fr 1 [<dir>/ | <path>] to receive it" + + bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" + bob <## "use /fr 2 [<dir>/ | <path>] to receive it" + + bob <# "alice> message with large file" + bob <# "alice> sends file test_1MB.pdf (1017.7 KiB / 1042157 bytes)" + bob <## "use /fr 3 [<dir>/ | <path>] to receive it" + + bob <# "alice> sends file logo.jpg (31.3 KiB / 32080 bytes)" + bob <## "use /fr 4 [<dir>/ | <path>] to receive it" + + alice <## "completed uploading file 1 (test.jpg) for bob" + alice <## "completed uploading file 2 (test.pdf) for bob" + alice <## "completed uploading file 3 (test_1MB.pdf) for bob" + alice <## "completed uploading file 4 (logo.jpg) for bob" + + -- IDs to forward + let msgId1 = (read msgIdZero :: Int) + 1 + msgIds = intercalate "," $ map (show . (msgId1 +)) [0 .. 5] + bob ##> ("/_forward plan @2 " <> msgIds) + bob <## "Files can be received: 1, 2, 3, 4" + bob <## "5 message(s) out of 6 can be forwarded" + + bob ##> "/fr 1" + bob + <### [ "saving file 1 from alice to test.jpg", + "started receiving file 1 (test.jpg) from alice" + ] + bob <## "completed receiving file 1 (test.jpg) from alice" + + bob ##> ("/_forward plan @2 " <> msgIds) + bob <## "Files can be received: 2, 3, 4" + bob <## "5 message(s) out of 6 can be forwarded" + + bob ##> "/fr 2" + bob + <### [ "saving file 2 from alice to test.pdf", + "started receiving file 2 (test.pdf) from alice" + ] + bob <## "completed receiving file 2 (test.pdf) from alice" + + src1 <- B.readFile "./tests/tmp/alice_app_files/test.jpg" + dest1 <- B.readFile "./tests/tmp/bob_app_files/test.jpg" + dest1 `shouldBe` src1 + + src2 <- B.readFile "./tests/tmp/alice_app_files/test.pdf" + dest2 <- B.readFile "./tests/tmp/bob_app_files/test.pdf" + dest2 `shouldBe` src2 + + -- forward file + bob ##> ("/_forward plan @2 " <> msgIds) + bob <## "Files can be received: 3, 4" + bob <## "all messages can be forwarded" + bob ##> ("/_forward @3 @2 " <> msgIds) + + -- messages printed for bob + bob <# "@cath <- you @alice" + bob <## " hi" + + bob <# "@cath <- @alice" + bob <## " message without file" + + bob <# "@cath <- @alice" + + jpgFileName <- T.unpack . T.strip . T.pack <$> getTermLine bob + bob <# ("/f @cath " <> jpgFileName) + bob <## "use /fc 5 to cancel sending" + + bob <# "@cath <- @alice" + bob <## " test_1.pdf" + bob <# "/f @cath test_1.pdf" + bob <## "use /fc 6 to cancel sending" + + bob <# "@cath <- @alice" + bob <## " message with large file" + + bob <# "@cath <- @alice" + bob <## "" + + -- messages printed for cath + cath <# "bob> -> forwarded" + cath <## " hi" + + cath <# "bob> -> forwarded" + cath <## " message without file" + + cath <# "bob> -> forwarded" + cath <## (" " <> jpgFileName) + cath <# ("bob> sends file " <> jpgFileName <> " (136.5 KiB / 139737 bytes)") + cath <## "use /fr 1 [<dir>/ | <path>] to receive it" + + cath <# "bob> -> forwarded" + cath <## " test_1.pdf" + cath <# "bob> sends file test_1.pdf (266.0 KiB / 272376 bytes)" + cath <## "use /fr 2 [<dir>/ | <path>] to receive it" + + cath <# "bob> -> forwarded" + cath <## " message with large file" + + cath <# "bob> -> forwarded" + cath <## "" + + -- file transfer + bob <## ("completed uploading file 5 (" <> jpgFileName <> ") for cath") + bob <## "completed uploading file 6 (test_1.pdf) for cath" + + cath ##> "/fr 1" + cath + <### [ ConsoleString $ "saving file 1 from bob to " <> jpgFileName, + ConsoleString $ "started receiving file 1 (" <> jpgFileName <> ") from bob" + ] + cath <## ("completed receiving file 1 (" <> jpgFileName <> ") from bob") + + cath ##> "/fr 2" + cath + <### [ "saving file 2 from bob to test_1.pdf", + "started receiving file 2 (test_1.pdf) from bob" + ] + cath <## "completed receiving file 2 (test_1.pdf) from bob" + + src1B <- B.readFile ("./tests/tmp/bob_app_files/" <> jpgFileName) + src1B `shouldBe` dest1 + dest1C <- B.readFile ("./tests/tmp/cath_app_files/" <> jpgFileName) + dest1C `shouldBe` src1B + + src2B <- B.readFile "./tests/tmp/bob_app_files/test_1.pdf" + src2B `shouldBe` dest2 + dest2C <- B.readFile "./tests/tmp/cath_app_files/test_1.pdf" + dest2C `shouldBe` src2B + + bob ##> "/fr 3" + bob + <### [ "saving file 3 from alice to test_1MB.pdf", + "started receiving file 3 (test_1MB.pdf) from alice" + ] + bob <## "completed receiving file 3 (test_1MB.pdf) from alice" + + bob ##> ("/_forward plan @2 " <> msgIds) + bob <## "Files can be received: 4" + bob <## "all messages can be forwarded" + + bob ##> "/fr 4" + bob + <### [ "saving file 4 from alice to logo.jpg", + "started receiving file 4 (logo.jpg) from alice" + ] + bob <## "completed receiving file 4 (logo.jpg) from alice" + + bob ##> ("/_forward plan @2 " <> msgIds) + bob <## "all messages can be forwarded" + + removeFile "./tests/tmp/bob_app_files/test_1MB.pdf" + bob ##> ("/_forward plan @2 " <> msgIds) + bob <## "1 file(s) are missing" + bob <## "all messages can be forwarded" + + removeFile "./tests/tmp/bob_app_files/test.pdf" + bob ##> ("/_forward plan @2 " <> msgIds) + bob <## "2 file(s) are missing" + bob <## "5 message(s) out of 6 can be forwarded" + + -- deleting original file doesn't delete forwarded file + checkActionDeletesFile "./tests/tmp/bob_app_files/test.jpg" $ do + bob ##> "/clear alice" + bob <## "alice: all messages are removed locally ONLY" + fwdFileExists <- doesFileExist ("./tests/tmp/bob_app_files/" <> jpgFileName) + fwdFileExists `shouldBe` True diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 76bc520aa4..431f19c77f 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -1,32 +1,57 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PostfixOperators #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module ChatTests.Groups where import ChatClient +import ChatTests.DBUtils import ChatTests.Utils import Control.Concurrent (threadDelay) import Control.Concurrent.Async (concurrently_) -import Control.Monad (void, when) -import qualified Data.ByteString as B -import Data.List (isInfixOf) +import Control.Monad (forM_, void, when) +import Data.Bifunctor (second) +import qualified Data.ByteString.Char8 as B +import Data.List (intercalate, isInfixOf) +import qualified Data.Map.Strict as M import qualified Data.Text as T -import Simplex.Chat.Controller (ChatConfig (..), XFTPFileConfig (..)) -import Simplex.Chat.Protocol (supportedChatVRange) -import Simplex.Chat.Store (agentStoreFile, chatStoreFile) -import Simplex.Chat.Types (GroupMemberRole (..)) -import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHooks) +import Simplex.Chat.Library.Internal (uniqueMsgMentions, updatedMentionNames) +import Simplex.Chat.Markdown (parseMaybeMarkdownList) +import Simplex.Chat.Messages (CIMention (..), CIMentionMember (..), ChatItemId) +import Simplex.Chat.Options +import Simplex.Chat.Protocol (MsgMention (..), MsgContent (..), msgContentText) +import Simplex.Chat.Types +import Simplex.Chat.Types.Shared (GroupMemberRole (..), GroupAcceptance (..)) +import Simplex.Messaging.Agent.Env.SQLite +import Simplex.Messaging.Agent.RetryInterval +import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Server.Env.STM hiding (subscriptions) +import Simplex.Messaging.Transport import Simplex.Messaging.Version +import Test.Hspec hiding (it) +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (Only (..)) +#else +import Database.SQLite.Simple (Only (..)) +import Simplex.Chat.Options.DB import System.Directory (copyFile) import System.FilePath ((</>)) -import Test.Hspec +#endif -chatGroupTests :: SpecWith FilePath +chatGroupTests :: SpecWith TestParams chatGroupTests = do describe "chat groups" $ do describe "add contacts, create group and send/receive messages" testGroupMatrix - it "v1: add contacts, create group and send/receive messages" testGroup - it "v1: add contacts, create group and send/receive messages, check messages" testGroupCheckMessages + it "add contacts, create group and send/receive messages, check messages" testGroupCheckMessages + it "mark multiple messages as read" testMarkReadGroup + it "initial chat pagination" testChatPaginationInitial + it "send large message" testGroupLargeMessage it "create group with incognito membership" testNewGroupIncognito it "create and join group with 4 members" testGroup2 it "create and delete group" testGroupDelete @@ -42,59 +67,67 @@ chatGroupTests = do it "group message update" testGroupMessageUpdate it "group message edit history" testGroupMessageEditHistory it "group message delete" testGroupMessageDelete + it "group message delete multiple" testGroupMessageDeleteMultiple + it "group message delete multiple (many chat batches)" testGroupMessageDeleteMultipleManyBatches it "group live message" testGroupLiveMessage it "update group profile" testUpdateGroupProfile it "update member role" testUpdateMemberRole - it "unused contacts are deleted after all their groups are deleted" testGroupDeleteUnusedContacts it "group description is shown as the first message to new members" testGroupDescription it "moderate message of another group member" testGroupModerate + it "moderate own message (should process as deletion)" testGroupModerateOwn + it "moderate multiple messages" testGroupModerateMultiple it "moderate message of another group member (full delete)" testGroupModerateFullDelete it "moderate message that arrives after the event of moderation" testGroupDelayedModeration it "moderate message that arrives after the event of moderation (full delete)" testGroupDelayedModerationFullDelete + it "remove member with messages (full deletion is enabled)" testDeleteMemberWithMessages + it "remove member with messages mark deleted" testDeleteMemberMarkMessagesDeleted + describe "batch send messages" $ do + it "send multiple messages api" testSendMulti + it "send multiple timed messages" testSendMultiTimed +#if !defined(dbPostgres) + -- TODO [postgres] this test hangs with PostgreSQL + it "send multiple messages (many chat batches)" testSendMultiManyBatches +#endif + xit'' "shared message body is reused" testSharedMessageBody + xit'' "shared batch body is reused" testSharedBatchBody describe "async group connections" $ do xit "create and join group when clients go offline" testGroupAsync describe "group links" $ do it "create group link, join via group link" testGroupLink + it "invitees were previously connected as contacts" testGroupLinkInviteesWereConnected + it "all members were previously connected as contacts" testGroupLinkAllMembersWereConnected it "delete group, re-join via same link" testGroupLinkDeleteGroupRejoin - it "sending message to contact created via group link marks it used" testGroupLinkContactUsed - it "create group link, join via group link - incognito membership" testGroupLinkIncognitoMembership - it "unused host contact is deleted after all groups with it are deleted" testGroupLinkUnusedHostContactDeleted - it "leaving groups with unused host contacts deletes incognito profiles" testGroupLinkIncognitoUnusedHostContactsDeleted + it "host incognito" testGroupLinkHostIncognito + it "invitee incognito" testGroupLinkInviteeIncognito + it "incognito - join/invite" testGroupLinkIncognitoJoinInvite it "group link member role" testGroupLinkMemberRole - it "leaving and deleting the group joined via link should NOT delete previously existing direct contacts" testGroupLinkLeaveDelete + it "host profile received" testGroupLinkHostProfileReceived + it "existing contact merged" testGroupLinkExistingContactMerged + describe "group links - join rejection" $ do + it "reject member joining via group link - blocked name" testGLinkRejectBlockedName + describe "group links - manual acceptance" $ do + it "manually accept member joining via group link" testGLinkManualAcceptMember + it "delete pending member" testGLinkDeletePendingMember describe "group link connection plan" $ do - it "group link ok to connect; known group" testPlanGroupLinkOkKnown - it "group is known if host contact was deleted" testPlanHostContactDeletedGroupLinkKnown + it "ok to connect; known group" testPlanGroupLinkKnown it "own group link" testPlanGroupLinkOwn - it "connecting via group link" testPlanGroupLinkConnecting + it "group link without contact - connecting" testPlanGroupLinkConnecting + it "group link without contact - connecting (slow handshake)" testPlanGroupLinkConnectingSlow it "re-join existing group after leaving" testPlanGroupLinkLeaveRejoin - describe "group links without contact" $ do - it "join via group link without creating contact" testGroupLinkNoContact - it "invitees were previously connected as contacts" testGroupLinkNoContactInviteesWereConnected - it "all members were previously connected as contacts" testGroupLinkNoContactAllMembersWereConnected - it "group link member role" testGroupLinkNoContactMemberRole - it "host incognito" testGroupLinkNoContactHostIncognito - it "invitee incognito" testGroupLinkNoContactInviteeIncognito - it "host profile received" testGroupLinkNoContactHostProfileReceived - it "existing contact merged" testGroupLinkNoContactExistingContactMerged - describe "group links without contact connection plan" $ do - it "group link without contact - known group" testPlanGroupLinkNoContactKnown - it "group link without contact - connecting" testPlanGroupLinkNoContactConnecting +#if !defined(dbPostgres) + -- TODO [postgres] restore from outdated db backup (same as in agent) describe "group message errors" $ do it "show message decryption error" testGroupMsgDecryptError it "should report ratchet de-synchronization, synchronize ratchets" testGroupSyncRatchet it "synchronize ratchets, reset connection code" testGroupSyncRatchetCodeReset +#endif describe "group message reactions" $ do it "set group message reactions" testSetGroupMessageReactions describe "group delivery receipts" $ do it "should send delivery receipts in group" testSendGroupDeliveryReceipts it "should send delivery receipts in group depending on configuration" testConfigureGroupDeliveryReceipts describe "direct connections in group are not established based on chat protocol version" $ do - describe "3 members group" $ do - testNoDirect _0 _0 True - testNoDirect _0 _1 True - testNoDirect _1 _0 False - testNoDirect _1 _1 False + it "direct contacts are not created" testNoGroupDirectConns it "members have different local display names in different groups" testNoDirectDifferentLDNs describe "merge members and contacts" $ do it "new member should merge with existing contact" testMergeMemberExistingContact @@ -109,8 +142,10 @@ chatGroupTests = do it "invited member replaces member contact reference if it already exists" testMemberContactInvitedConnectionReplaced it "share incognito profile" testMemberContactIncognito it "sends and updates profile when creating contact" testMemberContactProfileUpdate + it "re-create member contact after deletion, many groups" testRecreateMemberContactManyGroups describe "group message forwarding" $ do it "forward messages between invitee and introduced (x.msg.new)" testGroupMsgForward + it "forward reports to moderators, don't forward to members (x.msg.new, MCReport)" testGroupMsgForwardReport it "deduplicate forwarded messages" testGroupMsgForwardDeduplicate it "forward message edit (x.msg.update)" testGroupMsgForwardEdit it "forward message reaction (x.msg.react)" testGroupMsgForwardReaction @@ -118,6 +153,7 @@ chatGroupTests = do it "forward file (x.msg.file.descr)" testGroupMsgForwardFile it "forward role change (x.grp.mem.role)" testGroupMsgForwardChangeRole it "forward new member announcement (x.grp.mem.new)" testGroupMsgForwardNewMember + it "forward member leaving (x.grp.leave)" testGroupMsgForwardLeave describe "group history" $ do it "text messages" testGroupHistory it "history is sent when joining via group link" testGroupHistoryGroupLink @@ -131,37 +167,47 @@ chatGroupTests = do it "quoted messages" testGroupHistoryQuotes it "deleted message is not included" testGroupHistoryDeletedMessage it "disappearing message is sent as disappearing" testGroupHistoryDisappearingMessage - where - _0 = supportedChatVRange -- don't create direct connections - _1 = groupCreateDirectVRange - -- having host configured with older version doesn't have effect in tests - -- because host uses current code and sends version in MemberInfo - testNoDirect vrMem2 vrMem3 noConns = - it - ( "host " - <> vRangeStr supportedChatVRange - <> (", 2nd mem " <> vRangeStr vrMem2) - <> (", 3rd mem " <> vRangeStr vrMem3) - <> (if noConns then " : 2 <!!> 3" else " : 2 <##> 3") - ) - $ testNoGroupDirectConns supportedChatVRange vrMem2 vrMem3 noConns + it "welcome message (group description) is sent after history" testGroupHistoryWelcomeMessage + it "unknown member messages are processed" testGroupHistoryUnknownMember + describe "membership profile updates" $ do + it "send profile update on next message to group" testMembershipProfileUpdateNextGroupMessage + it "multiple groups with same member, update is applied only once" testMembershipProfileUpdateSameMember + it "member contact is active" testMembershipProfileUpdateContactActive + it "member contact is deleted" testMembershipProfileUpdateContactDeleted + it "member contact is deleted silently, then considered disabled" testMembershipProfileUpdateContactDisabled + it "profile update without change is ignored" testMembershipProfileUpdateNoChangeIgnored + it "change of profile contact link is ignored" testMembershipProfileUpdateContactLinkIgnored + describe "block member for all" $ do + it "messages are marked blocked" testBlockForAllMarkedBlocked + it "messages are fully deleted" testBlockForAllFullDelete + it "another admin can unblock" testBlockForAllAnotherAdminUnblocks + it "member was blocked before joining group" testBlockForAllBeforeJoining + it "repeat block, unblock" testBlockForAllRepeat + it "block multiple members" testBlockForAllMultipleMembers + describe "group member inactivity" $ do + it "mark member inactive on reaching quota" testGroupMemberInactive + describe "group member reports" $ do + it "should send report to group owner, admins and moderators, but not other users" testGroupMemberReports + describe "group member mentions" $ do + it "should send and edit messages with member mentions" testMemberMention + it "should forward and quote message updating mentioned member name" testForwardQuoteMention + it "should send updated mentions in history" testGroupHistoryWithMentions + describe "uniqueMsgMentions" testUniqueMsgMentions + describe "updatedMentionNames" testUpdatedMentionNames + describe "group direct messages" $ do + it "should send group direct messages" testGroupDirectMessages -testGroup :: HasCallStack => FilePath -> IO () -testGroup = - testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ - \alice bob cath -> testGroupShared alice bob cath False True - -testGroupCheckMessages :: HasCallStack => FilePath -> IO () +testGroupCheckMessages :: HasCallStack => TestParams -> IO () testGroupCheckMessages = - testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ - \alice bob cath -> testGroupShared alice bob cath True True + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> testGroupShared alice bob cath True -testGroupMatrix :: SpecWith FilePath +testGroupMatrix :: SpecWith TestParams testGroupMatrix = - versionTestMatrix3 $ \alice bob cath -> testGroupShared alice bob cath False False + versionTestMatrix3 $ \alice bob cath -> testGroupShared alice bob cath False -testGroupShared :: HasCallStack => TestCC -> TestCC -> TestCC -> Bool -> Bool -> IO () -testGroupShared alice bob cath checkMessages directConnections = do +testGroupShared :: HasCallStack => TestCC -> TestCC -> TestCC -> Bool -> IO () +testGroupShared alice bob cath checkMessages = do connectUsers alice bob connectUsers alice cath alice ##> "/g team" @@ -213,8 +259,6 @@ testGroupShared alice bob cath checkMessages directConnections = do (alice <# "#team cath> hey team") (bob <# "#team cath> hey team") msgItem2 <- lastItemId alice - when directConnections $ - bob <##> cath when checkMessages $ getReadChats msgItem1 msgItem2 -- list groups alice ##> "/gs" @@ -233,7 +277,7 @@ testGroupShared alice bob cath checkMessages directConnections = do -- test observer role alice ##> "/mr team bob observer" concurrentlyN_ - [ alice <## "#team: you changed the role of bob from admin to observer", + [ alice <## "#team: you changed the role of bob to observer", bob <## "#team: alice changed your role from admin to observer", cath <## "#team: alice changed the role of bob from admin to observer" ] @@ -248,7 +292,7 @@ testGroupShared alice bob cath checkMessages directConnections = do ] alice ##> "/mr team bob admin" concurrentlyN_ - [ alice <## "#team: you changed the role of bob from observer to admin", + [ alice <## "#team: you changed the role of bob to admin", bob <## "#team: alice changed your role from observer to admin", cath <## "#team: alice changed the role of bob from observer to admin" ] @@ -271,34 +315,10 @@ testGroupShared alice bob cath checkMessages directConnections = do (cath </) cath ##> "#team hello" cath <## "you are no longer a member of the group" - when directConnections $ - bob <##> cath -- delete contact alice ##> "/d bob" alice <## "bob: contact is deleted" bob <## "alice (Alice) deleted contact with you" - alice `send` "@bob hey" - if directConnections - then - alice - <### [ "@bob hey", - "member #team bob does not have direct connection, creating", - "peer chat protocol version range incompatible" - ] - else do - alice - <### [ WithTime "@bob hey", - "member #team bob does not have direct connection, creating", - "contact for member #team bob is created", - "sent invitation to connect directly to member #team bob", - "bob (Bob): contact is connected" - ] - bob - <### [ "#team alice is creating direct contact alice with you", - WithTime "alice> hey", - "alice: security code changed", - "alice (Alice): contact is connected" - ] when checkMessages $ threadDelay 1000000 alice #> "#team checking connection" bob <# "#team alice> checking connection" @@ -307,7 +327,7 @@ testGroupShared alice bob cath checkMessages directConnections = do alice <# "#team bob> received" when checkMessages $ do alice @@@ [("@cath", "sent invitation to join group team as admin"), ("#team", "received")] - bob @@@ [("@alice", "contact deleted"), ("@cath", "hey"), ("#team", "received")] + bob @@@ [("@alice", "contact deleted"), ("#team", "received")] -- test clearing chat threadDelay 1000000 alice #$> ("/clear #team", id, "#team: all messages are removed locally ONLY") @@ -320,28 +340,88 @@ testGroupShared alice bob cath checkMessages directConnections = do getReadChats :: HasCallStack => String -> String -> IO () getReadChats msgItem1 msgItem2 = do alice @@@ [("#team", "hey team"), ("@cath", "sent invitation to join group team as admin"), ("@bob", "sent invitation to join group team as admin")] - alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there"), (0, "hey team")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there"), (0, "hey team")]) -- "before" and "after" define a chat item id across all chats, -- so we take into account group event items as well as sent group invitations in direct chats alice #$> ("/_get chat #1 after=" <> msgItem1 <> " count=100", chat, [(0, "hi there"), (0, "hey team")]) - alice #$> ("/_get chat #1 before=" <> msgItem2 <> " count=100", chat, [(0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there")]) + alice #$> ("/_get chat #1 before=" <> msgItem2 <> " count=100", chat, sndGroupFeatures <> [(0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there")]) + alice #$> ("/_get chat #1 around=" <> msgItem1 <> " count=2", chat, [(0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there"), (0, "hey team")]) alice #$> ("/_get chat #1 count=100 search=team", chat, [(0, "hey team")]) - bob @@@ [("@cath", "hey"), ("#team", "hey team"), ("@alice", "received invitation to join group team as admin")] + bob @@@ [("#team", "hey team"), ("@alice", "received invitation to join group team as admin")] bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "added cath (Catherine)"), (0, "connected"), (0, "hello"), (1, "hi there"), (0, "hey team")]) - cath @@@ [("@bob", "hey"), ("#team", "hey team"), ("@alice", "received invitation to join group team as admin")] + cath @@@ [("#team", "hey team"), ("@alice", "received invitation to join group team as admin")] cath #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "connected"), (0, "hello"), (0, "hi there"), (1, "hey team")]) - alice #$> ("/_read chat #1 from=1 to=100", id, "ok") - bob #$> ("/_read chat #1 from=1 to=100", id, "ok") - cath #$> ("/_read chat #1 from=1 to=100", id, "ok") alice #$> ("/_read chat #1", id, "ok") bob #$> ("/_read chat #1", id, "ok") cath #$> ("/_read chat #1", id, "ok") alice #$> ("/_unread chat #1 on", id, "ok") alice #$> ("/_unread chat #1 off", id, "ok") -testNewGroupIncognito :: HasCallStack => FilePath -> IO () +testMarkReadGroup :: HasCallStack => TestParams -> IO () +testMarkReadGroup = testChat2 aliceProfile bobProfile $ \alice bob -> do + createGroup2 "team" alice bob + alice #> "#team 1" + alice #> "#team 2" + alice #> "#team 3" + alice #> "#team 4" + bob <# "#team alice> 1" + bob <# "#team alice> 2" + bob <# "#team alice> 3" + bob <# "#team alice> 4" + bob ##> "/last_item_id" + i :: ChatItemId <- read <$> getTermLine bob + let itemIds = intercalate "," $ map show [i - 3 .. i] + bob #$> ("/_read chat items #1 " <> itemIds, id, "ok") + +testChatPaginationInitial :: HasCallStack => TestParams -> IO () +testChatPaginationInitial = testChatOpts2 opts aliceProfile bobProfile $ \alice bob -> do + createGroup2 "team" alice bob + -- Wait, otherwise ids are going to be wrong. + threadDelay 1000000 + lastEventId <- (read :: String -> Int) <$> lastItemId bob + let groupItemId n = show $ lastEventId + n + + -- Send messages from alice to bob + forM_ ([1 .. 10] :: [Int]) $ \n -> alice #> ("#team " <> show n) + + -- Bob receives the messages. + 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")]) + + -- Read next 2 items + let itemIds = intercalate "," $ map groupItemId [1 .. 2] + bob #$> ("/_read chat items #1 " <> itemIds, id, "ok") + bob #$> ("/_get chat #1 initial=2", chat, [(0, "1"), (0, "2"), (0, "3"), (0, "4"), (0, "5")]) + + -- Read all items + bob #$> ("/_read chat #1", id, "ok") + bob #$> ("/_get chat #1 initial=3", chat, [(0, "8"), (0, "9"), (0, "10")]) + bob #$> ("/_get chat #1 initial=5", chat, [(0, "6"), (0, "7"), (0, "8"), (0, "9"), (0, "10")]) + where + opts = + testOpts + { markRead = False + } + +testGroupLargeMessage :: HasCallStack => TestParams -> IO () +testGroupLargeMessage = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + createGroup2 "team" alice bob + + img <- genProfileImg + let profileImage = "data:image/png;base64," <> B.unpack img + alice `send` ("/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"image\": \"" <> profileImage <> "\", \"groupPreferences\": {\"directMessages\": {\"enable\": \"on\"}, \"history\": {\"enable\": \"on\"}}}") + _trimmedCmd1 <- getTermLine alice + alice <## "profile image updated" + bob <## "alice updated group #team:" + bob <## "profile image updated" + +testNewGroupIncognito :: HasCallStack => TestParams -> IO () testNewGroupIncognito = - testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $ + testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob @@ -360,22 +440,35 @@ testNewGroupIncognito = bob ##> ("/c " <> gLink) bob <## "connection request sent!" alice <## "bob_1 (Bob): accepting request to join group #team..." - _ <- getTermLine alice concurrentlyN_ - [ do - alice <## ("bob_1 (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito) - alice <## "use /i bob_1 to print out this incognito profile again" - alice <## "bob_1 invited to group #team via your group link" - alice <## "#team: bob_1 joined the group", + [ alice <## "#team: bob_1 joined the group", do - bob <## (aliceIncognito <> ": contact is connected") + bob <## "#team: joining the group..." bob <## "#team: you joined the group" ] alice <##> bob - alice ?#> "@bob_1 hi, I'm incognito" - bob <# (aliceIncognito <> "> hi, I'm incognito") + alice ##> "@#team bob_1 hi, I'm incognito" + alice + <### [ "member #team bob_1 does not have direct connection, creating", + "contact for member #team bob_1 is created", + "sent invitation to connect directly to member #team bob_1", + WithTime "i @bob_1 hi, I'm incognito" + ] + bob + <### [ ConsoleString ("#team " <> aliceIncognito <> " is creating direct contact " <> aliceIncognito <> " with you"), + WithTime (aliceIncognito <> "> hi, I'm incognito") + ] + bob <## (aliceIncognito <> ": you can send messages to contact") + _ <- getTermLine alice + concurrentlyN_ + [ do + alice <## ("bob_1 (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito) + alice <## "use /i bob_1 to print out this incognito profile again", + bob <## (aliceIncognito <> ": contact is connected") + ] + bob #> ("@" <> aliceIncognito <> " hey, I'm bob") alice ?<# "bob_1> hey, I'm bob" @@ -389,9 +482,9 @@ testNewGroupIncognito = bob ##> "/gs" bob <## "#team (2 members)" -testGroup2 :: HasCallStack => FilePath -> IO () +testGroup2 :: HasCallStack => TestParams -> IO () testGroup2 = - testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $ + testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do connectUsers alice bob connectUsers alice cath @@ -442,14 +535,14 @@ testGroup2 = dan <## "#club: you joined the group" dan <### [ "#club: member alice_1 (Alice) is connected", - "contact alice_1 is merged into alice", + "contact and member are merged: alice, #club alice_1", "use @alice <message> to send messages", "#club: member cath (Catherine) is connected" ], do alice <## "#club: bob added dan_1 (Daniel) to the group (connecting...)" alice <## "#club: new member dan_1 is connected" - alice <## "contact dan_1 is merged into dan" + alice <## "contact and member are merged: dan, #club dan_1" alice <## "use @dan <message> to send messages", do cath <## "#club: bob added dan (Daniel) to the group (connecting...)" @@ -479,21 +572,23 @@ testGroup2 = bob <# "#club dan> how is it going?", cath <# "#club dan> how is it going?" ] - bob <##> cath - dan <##> cath dan <##> alice -- show last messages - alice ##> "/t #club 8" + alice ##> "/t #club 18" alice -- these strings are expected in any order because of sorting by time and rounding of time for sent - <##? [ "#club bob> connected", - "#club cath> connected", - "#club bob> added dan (Daniel)", - "#club dan> connected", - "#club hello", - "#club bob> hi there", - "#club cath> hey", - "#club dan> how is it going?" - ] + <##? + ( map (ConsoleString . ("#club " <> )) groupFeatureStrs + <> + [ "#club bob> connected", + "#club cath> connected", + "#club bob> added dan (Daniel)", + "#club dan> connected", + "#club hello", + "#club bob> hi there", + "#club cath> hey", + "#club dan> how is it going?" + ] + ) alice ##> "/t @dan 2" alice <##? [ "dan> hi", @@ -559,7 +654,6 @@ testGroup2 = dan <## "you are no longer a member of the group" dan ##> "/d #club" dan <## "#club: you deleted the group" - dan <##> cath dan <##> alice -- member leaves bob ##> "/l club" @@ -582,10 +676,9 @@ testGroup2 = bob <## "you are no longer a member of the group" bob ##> "/d #club" bob <## "#club: you deleted the group" - bob <##> cath bob <##> alice -testGroupDelete :: HasCallStack => FilePath -> IO () +testGroupDelete :: HasCallStack => TestParams -> IO () testGroupDelete = testChatCfg3 cfg aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -621,10 +714,11 @@ testGroupDelete = where cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0} -testGroupSameName :: HasCallStack => FilePath -> IO () +testGroupSameName :: HasCallStack => TestParams -> IO () testGroupSameName = testChat2 aliceProfile bobProfile $ \alice _ -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team <name> or /create link #team" @@ -632,7 +726,7 @@ testGroupSameName = alice <## "group #team_1 is created" alice <## "to add members use /a team_1 <name> or /create link #team_1" -testGroupDeleteWhenInvited :: HasCallStack => FilePath -> IO () +testGroupDeleteWhenInvited :: HasCallStack => TestParams -> IO () testGroupDeleteWhenInvited = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -659,7 +753,7 @@ testGroupDeleteWhenInvited = bob <## "use /j team to accept" ] -testGroupReAddInvited :: HasCallStack => FilePath -> IO () +testGroupReAddInvited :: HasCallStack => TestParams -> IO () testGroupReAddInvited = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -694,7 +788,7 @@ testGroupReAddInvited = bob <## "use /j team_1 to accept" ] -testGroupReAddInvitedChangeRole :: HasCallStack => FilePath -> IO () +testGroupReAddInvitedChangeRole :: HasCallStack => TestParams -> IO () testGroupReAddInvitedChangeRole = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -734,7 +828,7 @@ testGroupReAddInvitedChangeRole = alice ##> "/d #team" alice <## "#team: you deleted the group" -testGroupDeleteInvitedContact :: HasCallStack => FilePath -> IO () +testGroupDeleteInvitedContact :: HasCallStack => TestParams -> IO () testGroupDeleteInvitedContact = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -773,12 +867,13 @@ testGroupDeleteInvitedContact = WithTime "alice> hey", "alice: security code changed" ] + bob <## "alice (Alice): you can send messages to contact" concurrently_ (alice <## "bob (Bob): contact is connected") (bob <## "alice (Alice): contact is connected") alice <##> bob -testDeleteGroupMemberProfileKept :: HasCallStack => FilePath -> IO () +testDeleteGroupMemberProfileKept :: HasCallStack => TestParams -> IO () testDeleteGroupMemberProfileKept = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -848,11 +943,14 @@ testDeleteGroupMemberProfileKept = bob #> "#club received" alice <# "#club bob> received" -testGroupRemoveAdd :: HasCallStack => FilePath -> IO () +testGroupRemoveAdd :: HasCallStack => TestParams -> IO () testGroupRemoveAdd = - testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do createGroup3 "team" alice bob cath + + threadDelay 100000 + -- remove member alice ##> "/rm team bob" concurrentlyN_ @@ -862,6 +960,9 @@ testGroupRemoveAdd = bob <## "use /d #team to delete the group", cath <## "#team: alice removed bob from the group" ] + + threadDelay 100000 + alice ##> "/a team bob" alice <## "invitation to join the group #team sent to bob" bob <## "#team_1: alice invites you to join the group as member" @@ -871,14 +972,10 @@ testGroupRemoveAdd = [ alice <## "#team: bob joined the group", do bob <## "#team_1: you joined the group" - bob <## "#team_1: member cath_1 (Catherine) is connected" - bob <## "contact cath_1 is merged into cath" - bob <## "use @cath <message> to send messages", + bob <## "#team_1: member cath_1 (Catherine) is connected", do cath <## "#team: alice added bob_1 (Bob) to the group (connecting...)" cath <## "#team: new member bob_1 is connected" - cath <## "contact bob_1 is merged into bob" - cath <## "use @bob <message> to send messages" ] alice #> "#team hi" concurrently_ @@ -887,13 +984,13 @@ testGroupRemoveAdd = bob #> "#team_1 hey" concurrently_ (alice <# "#team bob> hey") - (cath <# "#team bob> hey") + (cath <# "#team bob_1> hey") cath #> "#team hello" concurrently_ (alice <# "#team cath> hello") - (bob <# "#team_1 cath> hello") + (bob <# "#team_1 cath_1> hello") -testGroupList :: HasCallStack => FilePath -> IO () +testGroupList :: HasCallStack => TestParams -> IO () testGroupList = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -923,9 +1020,9 @@ testGroupList = bob ##> "/gs" bob <## "#team (2 members)" -testGroupMessageQuotedReply :: HasCallStack => FilePath -> IO () +testGroupMessageQuotedReply :: HasCallStack => TestParams -> IO () testGroupMessageQuotedReply = - testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do createGroup3 "team" alice bob cath threadDelay 1000000 @@ -939,7 +1036,7 @@ testGroupMessageQuotedReply = bob <## " hello, all good, you?" concurrently_ ( do - alice <# "#team bob> > alice hello! how are you?" + alice <# "#team bob!> > alice hello! how are you?" alice <## " hello, all good, you?" ) ( do @@ -974,7 +1071,7 @@ testGroupMessageQuotedReply = alice <## " hi there!" ) ( do - bob <# "#team cath> > bob hello, all good, you?" + bob <# "#team cath!> > bob hello, all good, you?" bob <## " hi there!" ) cath #$> ("/_get chat #1 count=1", chat', [((1, "hi there!"), Just (0, "hello, all good, you?"))]) @@ -985,7 +1082,7 @@ testGroupMessageQuotedReply = alice <## " go on" concurrently_ ( do - bob <# "#team alice> > bob will tell more" + bob <# "#team alice!> > bob will tell more" bob <## " go on" ) ( do @@ -993,7 +1090,7 @@ testGroupMessageQuotedReply = cath <## " go on" ) -testGroupMessageUpdate :: HasCallStack => FilePath -> IO () +testGroupMessageUpdate :: HasCallStack => TestParams -> IO () testGroupMessageUpdate = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -1026,7 +1123,7 @@ testGroupMessageUpdate = bob <## " hi alice" concurrently_ ( do - alice <# "#team bob> > alice hey 👋" + alice <# "#team bob!> > alice hey 👋" alice <## " hi alice" ) ( do @@ -1053,7 +1150,7 @@ testGroupMessageUpdate = cath <## " greetings!" concurrently_ ( do - alice <# "#team cath> > alice greetings 🤝" + alice <# "#team cath!> > alice greetings 🤝" alice <## " greetings!" ) ( do @@ -1065,7 +1162,7 @@ testGroupMessageUpdate = bob #$> ("/_get chat #1 count=3", chat', [((0, "greetings 🤝"), Nothing), ((1, "hi alice"), Just (0, "hey 👋")), ((0, "greetings!"), Just (0, "greetings 🤝"))]) cath #$> ("/_get chat #1 count=3", chat', [((0, "greetings 🤝"), Nothing), ((0, "hi alice"), Just (0, "hey 👋")), ((1, "greetings!"), Just (0, "greetings 🤝"))]) -testGroupMessageEditHistory :: HasCallStack => FilePath -> IO () +testGroupMessageEditHistory :: HasCallStack => TestParams -> IO () testGroupMessageEditHistory = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -1139,11 +1236,12 @@ testGroupMessageEditHistory = bob <## "message history:" bob .<## ": hey there" -testGroupMessageDelete :: HasCallStack => FilePath -> IO () +testGroupMessageDelete :: HasCallStack => TestParams -> IO () testGroupMessageDelete = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do createGroup3 "team" alice bob cath + -- disableFullDeletion3 "team" alice bob cath threadDelay 1000000 -- alice, bob: msg id 5, cath: msg id 4 (after group invitations & group events) alice #> "#team hello!" @@ -1155,7 +1253,7 @@ testGroupMessageDelete = msgItemId1 <- lastItemId alice alice #$> ("/_delete item #1 " <> msgItemId1 <> " internal", id, "message deleted") - alice #$> ("/_get chat #1 count=1", chat, [(0, "connected")]) + alice #$> ("/_get chat #1 count=2", chat, [(0, "connected"), (0, "connected")]) bob #$> ("/_get chat #1 count=1", chat, [(0, "hello!")]) cath #$> ("/_get chat #1 count=1", chat, [(0, "hello!")]) @@ -1166,7 +1264,7 @@ testGroupMessageDelete = bob <## " hi alic" concurrently_ ( do - alice <# "#team bob> > alice hello!" + alice <# "#team bob!> > alice hello!" alice <## " hi alic" ) ( do @@ -1181,7 +1279,7 @@ testGroupMessageDelete = msgItemId2 <- lastItemId alice alice #$> ("/_delete item #1 " <> msgItemId2 <> " internal", id, "message deleted") - alice #$> ("/_get chat #1 count=1", chat', [((0, "connected"), Nothing)]) + alice #$> ("/_get chat #1 count=2", chat', [((0, "connected"), Nothing), ((0, "connected"), Nothing)]) bob #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((1, "hi alic"), Just (0, "hello!"))]) cath #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((0, "hi alic"), Just (0, "hello!"))]) @@ -1223,7 +1321,83 @@ testGroupMessageDelete = bob #$> ("/_get chat #1 count=3", chat', [((0, "hello!"), Nothing), ((1, "hi alice"), Just (0, "hello!")), ((0, "how are you? [marked deleted]"), Nothing)]) cath #$> ("/_get chat #1 count=3", chat', [((0, "hello!"), Nothing), ((0, "hi alice"), Just (0, "hello!")), ((1, "how are you? [marked deleted]"), Nothing)]) -testGroupLiveMessage :: HasCallStack => FilePath -> IO () +testGroupMessageDeleteMultiple :: HasCallStack => TestParams -> IO () +testGroupMessageDeleteMultiple = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + -- disableFullDeletion3 "team" alice bob cath + + threadDelay 1000000 + alice #> "#team hello" + concurrently_ + (bob <# "#team alice> hello") + (cath <# "#team alice> hello") + msgId1 <- lastItemId alice + + threadDelay 1000000 + alice #> "#team hey" + concurrently_ + (bob <# "#team alice> hey") + (cath <# "#team alice> hey") + msgId2 <- lastItemId alice + + threadDelay 1000000 + alice ##> ("/_delete item #1 " <> msgId1 <> "," <> msgId2 <> " broadcast") + alice <## "2 messages deleted" + concurrentlyN_ + [ do + bob <# "#team alice> [marked deleted] hello" + bob <# "#team alice> [marked deleted] hey", + do + cath <# "#team alice> [marked deleted] hello" + cath <# "#team alice> [marked deleted] hey" + ] + + alice #$> ("/_get chat #1 count=2", chat, [(1, "hello [marked deleted]"), (1, "hey [marked deleted]")]) + bob #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted]"), (0, "hey [marked deleted]")]) + cath #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted]"), (0, "hey [marked deleted]")]) + +testGroupMessageDeleteMultipleManyBatches :: HasCallStack => TestParams -> IO () +testGroupMessageDeleteMultipleManyBatches = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + -- disableFullDeletion3 "team" alice bob cath + + bob ##> "/set receipts all off" + bob <## "ok" + cath ##> "/set receipts all off" + cath <## "ok" + + msgIdZero <- lastItemId alice + + let cm i = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message " <> show i <> "\"}}" + cms = intercalate ", " (map cm [1 .. 300 :: Int]) + + alice `send` ("/_send #1 json [" <> cms <> "]") + _ <- getTermLine alice + + alice <## "300 messages sent" + + forM_ [(1 :: Int) .. 300] $ \i -> do + concurrently_ + (bob <# ("#team alice> message " <> show i)) + (cath <# ("#team alice> message " <> show i)) + msgIdLast <- lastItemId alice + + let mIdFirst = (read msgIdZero :: Int) + 1 + mIdLast = read msgIdLast :: Int + deleteIds = intercalate "," (map show [mIdFirst .. mIdLast]) + alice `send` ("/_delete item #1 " <> deleteIds <> " broadcast") + _ <- getTermLine alice + alice <## "300 messages deleted" + forM_ [(1 :: Int) .. 300] $ \i -> + concurrently_ + (bob <# ("#team alice> [marked deleted] message " <> show i)) + (cath <# ("#team alice> [marked deleted] message " <> show i)) + +testGroupLiveMessage :: HasCallStack => TestParams -> IO () testGroupLiveMessage = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do createGroup3 "team" alice bob cath @@ -1261,7 +1435,7 @@ testGroupLiveMessage = bob .<## ": hello 2" bob .<## ":" -testUpdateGroupProfile :: HasCallStack => FilePath -> IO () +testUpdateGroupProfile :: HasCallStack => TestParams -> IO () testUpdateGroupProfile = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -1288,7 +1462,7 @@ testUpdateGroupProfile = (alice <# "#my_team bob> hi") (cath <# "#my_team bob> hi") -testUpdateMemberRole :: HasCallStack => FilePath -> IO () +testUpdateMemberRole :: HasCallStack => TestParams -> IO () testUpdateMemberRole = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -1298,7 +1472,7 @@ testUpdateMemberRole = alice <## "to add members use /a team <name> or /create link #team" addMember "team" alice bob GRAdmin alice ##> "/mr team bob member" - alice <## "#team: you changed the role of bob from admin to member" + alice <## "#team: you changed the role of bob to member" bob <## "#team: alice invites you to join the group as member" bob <## "use /j team to accept" bob ##> "/j team" @@ -1310,7 +1484,7 @@ testUpdateMemberRole = bob <## "#team: you have insufficient permissions for this action, the required role is admin" alice ##> "/mr team bob admin" concurrently_ - (alice <## "#team: you changed the role of bob from member to admin") + (alice <## "#team: you changed the role of bob to admin") (bob <## "#team: alice changed your role from member to admin") bob ##> "/a team cath owner" bob <## "#team: you have insufficient permissions for this action, the required role is owner" @@ -1326,107 +1500,17 @@ testUpdateMemberRole = alice <## "#team: new member cath is connected" ] alice ##> "/mr team alice admin" - concurrentlyN_ - [ alice <## "#team: you changed your role from owner to admin", - bob <## "#team: alice changed the role from owner to admin", - cath <## "#team: alice changed the role from owner to admin" - ] - alice ##> "/d #team" - alice <## "#team: you have insufficient permissions for this action, the required role is owner" + alice <## "bad chat command: can't change role for self" -testGroupDeleteUnusedContacts :: HasCallStack => FilePath -> IO () -testGroupDeleteUnusedContacts = - testChatCfg3 cfg aliceProfile bobProfile cathProfile $ - \alice bob cath -> do - -- create group 1 - createGroup3 "team" alice bob cath - -- create group 2 - alice ##> "/g club" - alice <## "group #club is created" - alice <## "to add members use /a club <name> or /create link #club" - alice ##> "/a club bob" - concurrentlyN_ - [ alice <## "invitation to join the group #club sent to bob", - do - bob <## "#club: alice invites you to join the group as member" - bob <## "use /j club to accept" - ] - bob ##> "/j club" - concurrently_ - (alice <## "#club: bob joined the group") - (bob <## "#club: you joined the group") - alice ##> "/a club cath" - concurrentlyN_ - [ alice <## "invitation to join the group #club sent to cath", - do - cath <## "#club: alice invites you to join the group as member" - cath <## "use /j club to accept" - ] - cath ##> "/j club" - concurrentlyN_ - [ alice <## "#club: cath joined the group", - do - cath <## "#club: you joined the group" - cath <## "#club: member bob_1 (Bob) is connected" - cath <## "contact bob_1 is merged into bob" - cath <## "use @bob <message> to send messages", - do - bob <## "#club: alice added cath_1 (Catherine) to the group (connecting...)" - bob <## "#club: new member cath_1 is connected" - bob <## "contact cath_1 is merged into cath" - bob <## "use @cath <message> to send messages" - ] - -- list contacts - bob ##> "/contacts" - bob <## "alice (Alice)" - bob <## "cath (Catherine)" - cath ##> "/contacts" - cath <## "alice (Alice)" - cath <## "bob (Bob)" - -- delete group 1, contacts and profiles are kept - deleteGroup alice bob cath "team" - bob ##> "/contacts" - bob <## "alice (Alice)" - bob <## "cath (Catherine)" - bob `hasContactProfiles` ["alice", "bob", "cath"] - cath ##> "/contacts" - cath <## "alice (Alice)" - cath <## "bob (Bob)" - cath `hasContactProfiles` ["alice", "bob", "cath"] - -- delete group 2, unused contacts and profiles are deleted - deleteGroup alice bob cath "club" - threadDelay 3000000 - bob ##> "/contacts" - bob <## "alice (Alice)" - bob `hasContactProfiles` ["alice", "bob"] - cath ##> "/contacts" - cath <## "alice (Alice)" - cath `hasContactProfiles` ["alice", "cath"] - where - cfg = mkCfgCreateGroupDirect $ testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0} - deleteGroup :: HasCallStack => TestCC -> TestCC -> TestCC -> String -> IO () - deleteGroup alice bob cath group = do - alice ##> ("/d #" <> group) - concurrentlyN_ - [ alice <## ("#" <> group <> ": you deleted the group"), - do - bob <## ("#" <> group <> ": alice deleted the group") - bob <## ("use /d #" <> group <> " to delete the local copy of the group"), - do - cath <## ("#" <> group <> ": alice deleted the group") - cath <## ("use /d #" <> group <> " to delete the local copy of the group") - ] - bob ##> ("/d #" <> group) - bob <## ("#" <> group <> ": you deleted the group") - cath ##> ("/d #" <> group) - cath <## ("#" <> group <> ": you deleted the group") - -testGroupDescription :: HasCallStack => FilePath -> IO () +testGroupDescription :: HasCallStack => TestParams -> IO () testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do connectUsers alice bob alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team <name> or /create link #team" + -- alice ##> "/set delete #team off" + -- alice <## "updated group preferences:" + -- alice <## "Full deletion: off" addMember "team" alice bob GRAdmin bob ##> "/j team" concurrentlyN_ @@ -1435,7 +1519,7 @@ testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile ] alice ##> "/group_profile team" alice <## "#team" - groupInfo alice + groupInfo' alice alice ##> "/group_descr team Welcome to the team!" alice <## "description changed to:" alice <## "Welcome to the team!" @@ -1446,7 +1530,7 @@ testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile alice <## "#team" alice <## "description:" alice <## "Welcome to the team!" - groupInfo alice + groupInfo' alice connectUsers alice cath addMember "team" alice cath GRMember cath ##> "/j team" @@ -1476,8 +1560,8 @@ testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile bobAddedDan cath ] where - groupInfo :: HasCallStack => TestCC -> IO () - groupInfo alice = do + groupInfo' :: HasCallStack => TestCC -> IO () + groupInfo' alice = do alice <## "group preferences:" alice <## "Disappearing messages: off" alice <## "Direct messages: on" @@ -1485,20 +1569,23 @@ testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile alice <## "Message reactions: on" alice <## "Voice messages: on" alice <## "Files and media: on" + alice <## "SimpleX links: on" + alice <## "Member reports: on" alice <## "Recent history: on" bobAddedDan :: HasCallStack => TestCC -> IO () bobAddedDan cc = do cc <## "#team: bob added dan (Daniel) to the group (connecting...)" cc <## "#team: new member dan is connected" -testGroupModerate :: HasCallStack => FilePath -> IO () +testGroupModerate :: HasCallStack => TestParams -> IO () testGroupModerate = - testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do createGroup3 "team" alice bob cath + -- disableFullDeletion3 "team" alice bob cath alice ##> "/mr team cath member" concurrentlyN_ - [ alice <## "#team: you changed the role of cath from admin to member", + [ alice <## "#team: you changed the role of cath to member", bob <## "#team: alice changed the role of cath from admin to member", cath <## "#team: alice changed your role from admin to member" ] @@ -1507,7 +1594,7 @@ testGroupModerate = (bob <# "#team alice> hello") (cath <# "#team alice> hello") bob ##> "\\\\ #team @alice hello" - bob <## "#team: you have insufficient permissions for this action, the required role is owner" + bob <## "cannot delete this item" threadDelay 1000000 cath #> "#team hi" concurrently_ @@ -1522,14 +1609,66 @@ testGroupModerate = bob #$> ("/_get chat #1 count=1", chat, [(0, "hi [marked deleted by you]")]) cath #$> ("/_get chat #1 count=1", chat, [(1, "hi [marked deleted by bob]")]) -testGroupModerateFullDelete :: HasCallStack => FilePath -> IO () -testGroupModerateFullDelete = - testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ +testGroupModerateOwn :: HasCallStack => TestParams -> IO () +testGroupModerateOwn = + withTestOutput $ testChat2 aliceProfile bobProfile $ + \alice bob -> do + createGroup2 "team" alice bob + -- disableFullDeletion2 "team" alice bob + threadDelay 1000000 + alice #> "#team hello" + bob <# "#team alice> hello" + alice ##> "\\\\ #team @alice hello" + alice <## "message marked deleted by you" + bob <# "#team alice> [marked deleted by alice] hello" + alice #$> ("/_get chat #1 count=1", chat, [(1, "hello [marked deleted by you]")]) + bob #$> ("/_get chat #1 count=1", chat, [(0, "hello [marked deleted by alice]")]) + +testGroupModerateMultiple :: HasCallStack => TestParams -> IO () +testGroupModerateMultiple = + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do createGroup3 "team" alice bob cath + -- disableFullDeletion3 "team" alice bob cath + + threadDelay 1000000 + alice #> "#team hello" + concurrently_ + (bob <# "#team alice> hello") + (cath <# "#team alice> hello") + msgId1 <- lastItemId alice + + threadDelay 1000000 + bob #> "#team hey" + concurrently_ + (alice <# "#team bob> hey") + (cath <# "#team bob> hey") + msgId2 <- lastItemId alice + + alice ##> ("/_delete member item #1 " <> msgId1 <> "," <> msgId2) + alice <## "2 messages deleted" + concurrentlyN_ + [ do + bob <# "#team alice> [marked deleted by alice] hello" + bob <# "#team bob> [marked deleted by alice] hey", + do + cath <# "#team alice> [marked deleted by alice] hello" + cath <# "#team bob> [marked deleted by alice] hey" + ] + + alice #$> ("/_get chat #1 count=2", chat, [(1, "hello [marked deleted by you]"), (0, "hey [marked deleted by you]")]) + bob #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted by alice]"), (1, "hey [marked deleted by alice]")]) + cath #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted by alice]"), (0, "hey [marked deleted by alice]")]) + +testGroupModerateFullDelete :: HasCallStack => TestParams -> IO () +testGroupModerateFullDelete = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + -- disableFullDeletion3 "team" alice bob cath alice ##> "/mr team cath member" concurrentlyN_ - [ alice <## "#team: you changed the role of cath from admin to member", + [ alice <## "#team: you changed the role of cath to member", bob <## "#team: alice changed the role of cath from admin to member", cath <## "#team: alice changed your role from admin to member" ] @@ -1560,12 +1699,13 @@ testGroupModerateFullDelete = bob #$> ("/_get chat #1 count=1", chat, [(0, "moderated [deleted by you]")]) cath #$> ("/_get chat #1 count=1", chat, [(1, "moderated [deleted by bob]")]) -testGroupDelayedModeration :: HasCallStack => FilePath -> IO () -testGroupDelayedModeration tmp = do - withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do - withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do +testGroupDelayedModeration :: HasCallStack => TestParams -> IO () +testGroupDelayedModeration ps = do + withNewTestChatCfg ps cfg "alice" aliceProfile $ \alice -> do + withNewTestChatCfg ps cfg "bob" bobProfile $ \bob -> do createGroup2 "team" alice bob - withNewTestChatCfg tmp cfg "cath" cathProfile $ \cath -> do + -- disableFullDeletion2 "team" alice bob + withNewTestChatCfg ps cfg "cath" cathProfile $ \cath -> do connectUsers alice cath addMember "team" alice cath GRMember cath ##> "/j team" @@ -1586,12 +1726,12 @@ testGroupDelayedModeration tmp = do alice ##> "\\\\ #team @cath hi" alice <## "message marked deleted by you" cath <# "#team cath> [marked deleted by alice] hi" - withTestChatCfg tmp cfg "bob" $ \bob -> do + withTestChatCfg ps cfg "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob <## "#team: connected to server(s)" bob <## "#team: alice added cath (Catherine) to the group (connecting...)" - withTestChatCfg tmp cfg "cath" $ \cath -> do - cath <## "2 contacts connected (use /cs for the list)" + withTestChatCfg ps cfg "cath" $ \cath -> do + cath <## "1 contacts connected (use /cs for the list)" cath <## "#team: connected to server(s)" cath <## "#team: member bob (Bob) is connected" bob @@ -1604,14 +1744,16 @@ testGroupDelayedModeration tmp = do r <- chat <$> getTermLine bob r `shouldMatchList` [(0, "connected"), (0, "hi [marked deleted by alice]")] where - cfg = testCfgCreateGroupDirect + -- version before forwarding, so cath doesn't expect alice to forward messages (groupForwardVersion = 4) + cfg = testCfg {chatVRange = mkVersionRange (VersionChat 1) (VersionChat 3)} -testGroupDelayedModerationFullDelete :: HasCallStack => FilePath -> IO () -testGroupDelayedModerationFullDelete tmp = do - withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do - withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do +testGroupDelayedModerationFullDelete :: HasCallStack => TestParams -> IO () +testGroupDelayedModerationFullDelete ps = do + withNewTestChatCfg ps cfg "alice" aliceProfile $ \alice -> do + withNewTestChatCfg ps cfg "bob" bobProfile $ \bob -> do createGroup2 "team" alice bob - withNewTestChatCfg tmp cfg "cath" cathProfile $ \cath -> do + -- disableFullDeletion2 "team" alice bob + withNewTestChatCfg ps cfg "cath" cathProfile $ \cath -> do connectUsers alice cath addMember "team" alice cath GRMember cath ##> "/j team" @@ -1640,15 +1782,15 @@ testGroupDelayedModerationFullDelete tmp = do cath <## "alice updated group #team:" cath <## "updated group preferences:" cath <## "Full deletion: on" - withTestChatCfg tmp cfg "bob" $ \bob -> do + withTestChatCfg ps cfg "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob <## "#team: connected to server(s)" bob <## "#team: alice added cath (Catherine) to the group (connecting...)" bob <## "alice updated group #team:" bob <## "updated group preferences:" bob <## "Full deletion: on" - withTestChatCfg tmp cfg "cath" $ \cath -> do - cath <## "2 contacts connected (use /cs for the list)" + withTestChatCfg ps cfg "cath" $ \cath -> do + cath <## "1 contacts connected (use /cs for the list)" cath <## "#team: connected to server(s)" cath <## "#team: member bob (Bob) is connected" bob @@ -1661,12 +1803,264 @@ testGroupDelayedModerationFullDelete tmp = do r <- chat <$> getTermLine bob r `shouldMatchList` [(0, "Full deletion: on"), (0, "connected"), (0, "moderated [deleted by alice]")] where - cfg = testCfgCreateGroupDirect + -- version before forwarding, so cath doesn't expect alice to forward messages (groupForwardVersion = 4) + cfg = testCfg {chatVRange = mkVersionRange (VersionChat 1) (VersionChat 3)} -testGroupAsync :: HasCallStack => FilePath -> IO () -testGroupAsync tmp = do - withNewTestChat tmp "alice" aliceProfile $ \alice -> do - withNewTestChat tmp "bob" bobProfile $ \bob -> do +testDeleteMemberWithMessages :: HasCallStack => TestParams -> IO () +testDeleteMemberWithMessages = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + threadDelay 750000 + alice ##> "/set delete #team on" + alice <## "updated group preferences:" + alice <## "Full deletion: on" + threadDelay 750000 + concurrentlyN_ + [ do + bob <## "alice updated group #team:" + bob <## "updated group preferences:" + bob <## "Full deletion: on", + do + cath <## "alice updated group #team:" + cath <## "updated group preferences:" + cath <## "Full deletion: on" + ] + threadDelay 750000 + bob #> "#team hello" + concurrently_ + (alice <# "#team bob> hello") + (cath <# "#team bob> hello") + alice #$> ("/_get chat #1 count=1", chat, [(0, "hello")]) + bob #$> ("/_get chat #1 count=1", chat, [(1, "hello")]) + cath #$> ("/_get chat #1 count=1", chat, [(0, "hello")]) + threadDelay 1000000 + alice ##> "/rm #team bob messages=on" + alice <## "#team: you removed bob from the group with all messages" + bob <## "#team: alice removed you from the group with all messages" + bob <## "use /d #team to delete the group" + cath <## "#team: alice removed bob from the group with all messages" + alice #$> ("/_get chat #1 count=2", chat, [(0, "moderated [deleted by you]"), (1, "removed bob (Bob)")]) + bob #$> ("/_get chat #1 count=2", chat, [(1, "moderated [deleted by alice]"), (0, "removed you")]) + cath #$> ("/_get chat #1 count=2", chat, [(0, "moderated [deleted by alice]"), (0, "removed bob (Bob)")]) + +testDeleteMemberMarkMessagesDeleted :: HasCallStack => TestParams -> IO () +testDeleteMemberMarkMessagesDeleted = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + threadDelay 1000000 + bob #> "#team hello" + concurrently_ + (alice <# "#team bob> hello") + (cath <# "#team bob> hello") + alice #$> ("/_get chat #1 count=1", chat, [(0, "hello")]) + bob #$> ("/_get chat #1 count=1", chat, [(1, "hello")]) + cath #$> ("/_get chat #1 count=1", chat, [(0, "hello")]) + threadDelay 1000000 + alice ##> "/rm #team bob messages=on" + alice <## "#team: you removed bob from the group with all messages" + bob <## "#team: alice removed you from the group with all messages" + bob <## "use /d #team to delete the group" + cath <## "#team: alice removed bob from the group with all messages" + alice #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted by you]"), (1, "removed bob (Bob)")]) + bob #$> ("/_get chat #1 count=2", chat, [(1, "hello [marked deleted by alice]"), (0, "removed you")]) + cath #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted by alice]"), (0, "removed bob (Bob)")]) + +testSendMulti :: HasCallStack => TestParams -> IO () +testSendMulti = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + alice ##> "/_send #1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]" + alice <# "#team test 1" + alice <# "#team test 2" + bob <# "#team alice> test 1" + bob <# "#team alice> test 2" + cath <# "#team alice> test 1" + cath <# "#team alice> test 2" + +testSendMultiTimed :: HasCallStack => TestParams -> IO () +testSendMultiTimed = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + alice ##> "/set disappear #team on 1" + alice <## "updated group preferences:" + alice <## "Disappearing messages: on (1 sec)" + bob <## "alice updated group #team:" + bob <## "updated group preferences:" + bob <## "Disappearing messages: on (1 sec)" + cath <## "alice updated group #team:" + cath <## "updated group preferences:" + cath <## "Disappearing messages: on (1 sec)" + + alice ##> "/_send #1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]" + alice <# "#team test 1" + alice <# "#team test 2" + bob <# "#team alice> test 1" + bob <# "#team alice> test 2" + cath <# "#team alice> test 1" + cath <# "#team alice> test 2" + + alice + <### [ "timed message deleted: test 1", + "timed message deleted: test 2" + ] + bob + <### [ "timed message deleted: test 1", + "timed message deleted: test 2" + ] + cath + <### [ "timed message deleted: test 1", + "timed message deleted: test 2" + ] + +testSendMultiManyBatches :: HasCallStack => TestParams -> IO () +testSendMultiManyBatches = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + msgIdAlice <- lastItemId alice + msgIdBob <- lastItemId bob + msgIdCath <- lastItemId cath + + let cm i = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message " <> show i <> "\"}}" + cms = intercalate ", " (map cm [1 .. 300 :: Int]) + + alice `send` ("/_send #1 json [" <> cms <> "]") + _ <- getTermLine alice + + alice <## "300 messages sent" + + forM_ [(1 :: Int) .. 300] $ \i -> do + concurrently_ + (bob <# ("#team alice> message " <> show i)) + (cath <# ("#team alice> message " <> show i)) + + aliceItemsCount <- withCCTransaction alice $ \db -> + DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdAlice) :: IO [[Int]] + aliceItemsCount `shouldBe` [[300]] + + bobItemsCount <- withCCTransaction bob $ \db -> + DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdBob) :: IO [[Int]] + bobItemsCount `shouldBe` [[300]] + + cathItemsCount <- withCCTransaction cath $ \db -> + DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdCath) :: IO [[Int]] + cathItemsCount `shouldBe` [[300]] + +testSharedMessageBody :: HasCallStack => TestParams -> IO () +testSharedMessageBody ps = + withNewTestChatOpts ps opts' "alice" aliceProfile $ \alice -> do + withSmpServer' serverCfg' $ + withNewTestChatOpts ps opts' "bob" bobProfile $ \bob -> + withNewTestChatOpts ps opts' "cath" cathProfile $ \cath -> do + createGroup3 "team" alice bob cath + + alice <##. "server disconnected localhost" + alice #> "#team hello" + bodiesCount1 <- withCCAgentTransaction alice $ \db -> + DB.query_ db "SELECT count(1) FROM snd_message_bodies" :: IO [[Int]] + bodiesCount1 `shouldBe` [[1]] + + withSmpServer' serverCfg' $ + withTestChatOpts ps opts' "bob" $ \bob -> + withTestChatOpts ps opts' "cath" $ \cath -> do + concurrentlyN_ + [ alice <##. "server connected localhost", + do + bob <## "1 contacts connected (use /cs for the list)" + bob <## "#team: connected to server(s)", + do + cath <## "1 contacts connected (use /cs for the list)" + cath <## "#team: connected to server(s)" + ] + bob <# "#team alice> hello" + cath <# "#team alice> hello" + bodiesCount2 <- withCCAgentTransaction alice $ \db -> + DB.query_ db "SELECT count(1) FROM snd_message_bodies" :: IO [[Int]] + bodiesCount2 `shouldBe` [[0]] + + alice <##. "server disconnected localhost" + where + tmp = tmpPath ps + serverCfg' = + smpServerCfg + { transports = [("7003", transport @TLS, False)], + serverStoreCfg = persistentServerStoreCfg tmp + } + opts' = + testOpts + { coreOptions = + testCoreOpts + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] + } + } + +testSharedBatchBody :: HasCallStack => TestParams -> IO () +testSharedBatchBody ps = + withNewTestChatOpts ps opts' "alice" aliceProfile $ \alice -> do + withSmpServer' serverCfg' $ + withNewTestChatOpts ps opts' "bob" bobProfile $ \bob -> + withNewTestChatOpts ps opts' "cath" cathProfile $ \cath -> do + createGroup3 "team" alice bob cath + + alice <##. "server disconnected localhost" + + let cm i = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message " <> show i <> "\"}}" + cms = intercalate ", " (map cm [1 .. 300 :: Int]) + alice `send` ("/_send #1 json [" <> cms <> "]") + _ <- getTermLine alice + alice <## "300 messages sent" + + bodiesCount1 <- withCCAgentTransaction alice $ \db -> + DB.query_ db "SELECT count(1) FROM snd_message_bodies" :: IO [[Int]] + bodiesCount1 `shouldBe` [[3]] + + withSmpServer' serverCfg' $ + withTestChatOpts ps opts' "bob" $ \bob -> + withTestChatOpts ps opts' "cath" $ \cath -> do + concurrentlyN_ + [ alice <##. "server connected localhost", + do + bob <## "1 contacts connected (use /cs for the list)" + bob <## "#team: connected to server(s)", + do + cath <## "1 contacts connected (use /cs for the list)" + cath <## "#team: connected to server(s)" + ] + forM_ [(1 :: Int) .. 300] $ \i -> do + concurrently_ + (bob <# ("#team alice> message " <> show i)) + (cath <# ("#team alice> message " <> show i)) + bodiesCount2 <- withCCAgentTransaction alice $ \db -> + DB.query_ db "SELECT count(1) FROM snd_message_bodies" :: IO [[Int]] + bodiesCount2 `shouldBe` [[0]] + + alice <##. "server disconnected localhost" + where + tmp = tmpPath ps + serverCfg' = + smpServerCfg + { transports = [("7003", transport @TLS, False)], + serverStoreCfg = persistentServerStoreCfg tmp + } + opts' = + testOpts + { coreOptions = + testCoreOpts + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] + } + } + +testGroupAsync :: HasCallStack => TestParams -> IO () +testGroupAsync ps = do + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do connectUsers alice bob alice ##> "/g team" alice <## "group #team is created" @@ -1684,9 +2078,8 @@ testGroupAsync tmp = do (bob <## "#team: you joined the group") alice #> "#team hello bob" bob <# "#team alice> hello bob" - print (1 :: Integer) - withTestChat tmp "alice" $ \alice -> do - withNewTestChat tmp "cath" cathProfile $ \cath -> do + withTestChat ps "alice" $ \alice -> do + withNewTestChat ps "cath" cathProfile $ \cath -> do alice <## "1 contacts connected (use /cs for the list)" alice <## "#team: connected to server(s)" connectUsers alice cath @@ -1704,9 +2097,8 @@ testGroupAsync tmp = do ] alice #> "#team hello cath" cath <# "#team alice> hello cath" - print (2 :: Integer) - withTestChat tmp "bob" $ \bob -> do - withTestChat tmp "cath" $ \cath -> do + withTestChat ps "bob" $ \bob -> do + withTestChat ps "cath" $ \cath -> do concurrentlyN_ [ do bob <## "1 contacts connected (use /cs for the list)" @@ -1720,9 +2112,8 @@ testGroupAsync tmp = do cath <## "#team: member bob (Bob) is connected" ] threadDelay 500000 - print (3 :: Integer) - withTestChat tmp "bob" $ \bob -> do - withNewTestChat tmp "dan" danProfile $ \dan -> do + withTestChat ps "bob" $ \bob -> do + withNewTestChat ps "dan" danProfile $ \dan -> do bob <## "2 contacts connected (use /cs for the list)" bob <## "#team: connected to server(s)" connectUsers bob dan @@ -1740,10 +2131,9 @@ testGroupAsync tmp = do ] threadDelay 1000000 threadDelay 1000000 - print (4 :: Integer) - withTestChat tmp "alice" $ \alice -> do - withTestChat tmp "cath" $ \cath -> do - withTestChat tmp "dan" $ \dan -> do + withTestChat ps "alice" $ \alice -> do + withTestChat ps "cath" $ \cath -> do + withTestChat ps "dan" $ \dan -> do concurrentlyN_ [ do alice <## "2 contacts connected (use /cs for the list)" @@ -1762,11 +2152,10 @@ testGroupAsync tmp = do dan <## "#team: member cath (Catherine) is connected" ] threadDelay 1000000 - print (5 :: Integer) - withTestChat tmp "alice" $ \alice -> do - withTestChat tmp "bob" $ \bob -> do - withTestChat tmp "cath" $ \cath -> do - withTestChat tmp "dan" $ \dan -> do + withTestChat ps "alice" $ \alice -> do + withTestChat ps "bob" $ \bob -> do + withTestChat ps "cath" $ \cath -> do + withTestChat ps "dan" $ \dan -> do concurrentlyN_ [ do alice <## "3 contacts connected (use /cs for the list)" @@ -1809,111 +2198,11 @@ testGroupAsync tmp = do dan <##> cath dan <##> alice -testGroupLink :: HasCallStack => FilePath -> IO () -testGroupLink = - testChatCfg3 testCfgGroupLinkViaContact aliceProfile bobProfile cathProfile $ - \alice bob cath -> do - alice ##> "/g team" - alice <## "group #team is created" - alice <## "to add members use /a team <name> or /create link #team" - alice ##> "/show link #team" - alice <## "no group link, to create: /create link #team" - alice ##> "/create link #team" - _ <- getGroupLink alice "team" GRMember True - alice ##> "/delete link #team" - alice <## "Group link is deleted - joined members will remain connected." - alice <## "To create a new group link use /create link #team" - alice ##> "/create link #team" - gLink <- getGroupLink alice "team" GRMember True - alice ##> "/show link #team" - _ <- getGroupLink alice "team" GRMember False - alice ##> "/create link #team" - alice <## "you already have link for this group, to show: /show link #team" - bob ##> ("/c " <> gLink) - bob <## "connection request sent!" - alice <## "bob (Bob): accepting request to join group #team..." - concurrentlyN_ - [ do - alice <## "bob (Bob): contact is connected" - alice <## "bob invited to group #team via your group link" - alice <## "#team: bob joined the group", - do - bob <## "alice (Alice): contact is connected" - bob <## "#team: you joined the group" - ] - threadDelay 100000 - alice #$> ("/_get chat #1 count=100", chat, [(0, "invited via your group link"), (0, "connected")]) - -- contacts connected via group link are not in chat previews - alice @@@ [("#team", "connected")] - bob @@@ [("#team", "connected")] - alice <##> bob - alice @@@ [("@bob", "hey"), ("#team", "connected")] - - -- user address doesn't interfere - alice ##> "/ad" - cLink <- getContactLink alice True - cath ##> ("/c " <> cLink) - alice <#? cath - alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." - concurrently_ - (cath <## "alice (Alice): contact is connected") - (alice <## "cath (Catherine): contact is connected") - alice <##> cath - - -- third member - cath ##> ("/c " <> gLink) - cath <## "connection request sent!" - alice <## "cath_1 (Catherine): accepting request to join group #team..." - -- if contact existed it is merged - concurrentlyN_ - [ alice - <### [ "cath_1 (Catherine): contact is connected", - "contact cath_1 is merged into cath", - "use @cath <message> to send messages", - EndsWith "invited to group #team via your group link", - EndsWith "joined the group" - ], - cath - <### [ "alice_1 (Alice): contact is connected", - "contact alice_1 is merged into alice", - "use @alice <message> to send messages", - "#team: you joined the group", - "#team: member bob (Bob) is connected" - ], - do - bob <## "#team: alice added cath (Catherine) to the group (connecting...)" - bob <## "#team: new member cath is connected" - ] - alice #> "#team hello" - concurrently_ - (bob <# "#team alice> hello") - (cath <# "#team alice> hello") - bob #> "#team hi there" - concurrently_ - (alice <# "#team bob> hi there") - (cath <# "#team bob> hi there") - cath #> "#team hey team" - concurrently_ - (alice <# "#team cath> hey team") - (bob <# "#team cath> hey team") - - -- leaving team removes link - alice ##> "/l team" - concurrentlyN_ - [ do - alice <## "#team: you left the group" - alice <## "use /d #team to delete the group", - bob <## "#team: alice left the group", - cath <## "#team: alice left the group" - ] - alice ##> "/show link #team" - alice <## "no group link, to create: /create link #team" - -testGroupLinkDeleteGroupRejoin :: HasCallStack => FilePath -> IO () +testGroupLinkDeleteGroupRejoin :: HasCallStack => TestParams -> IO () testGroupLinkDeleteGroupRejoin = - testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $ + testChat2 aliceProfile bobProfile $ \alice bob -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team <name> or /create link #team" @@ -1923,16 +2212,11 @@ testGroupLinkDeleteGroupRejoin = bob <## "connection request sent!" alice <## "bob (Bob): accepting request to join group #team..." concurrentlyN_ - [ do - alice <## "bob (Bob): contact is connected" - alice <## "bob invited to group #team via your group link" - alice <## "#team: bob joined the group", + [ alice <## "#team: bob joined the group", do - bob <## "alice (Alice): contact is connected" + bob <## "#team: joining the group..." bob <## "#team: you joined the group" ] - -- use contact so it's not deleted when deleting group - bob <##> alice bob ##> "/l team" concurrentlyN_ [ do @@ -1947,61 +2231,20 @@ testGroupLinkDeleteGroupRejoin = bob <## "connection request sent!" alice <## "bob_1 (Bob): accepting request to join group #team..." concurrentlyN_ - [ alice - <### [ "bob_1 (Bob): contact is connected", - "contact bob_1 is merged into bob", - "use @bob <message> to send messages", - EndsWith "invited to group #team via your group link", - EndsWith "joined the group" - ], + [ alice <## "#team: bob_1 joined the group", bob - <### [ "alice_1 (Alice): contact is connected", - "contact alice_1 is merged into alice", - "use @alice <message> to send messages", + <### [ "#team: joining the group...", "#team: you joined the group" ] ] alice #> "#team hello" bob <# "#team alice> hello" bob #> "#team hi there" - alice <# "#team bob> hi there" + alice <# "#team bob_1> hi there" -testGroupLinkContactUsed :: HasCallStack => FilePath -> IO () -testGroupLinkContactUsed = - testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $ - \alice bob -> do - alice ##> "/g team" - alice <## "group #team is created" - alice <## "to add members use /a team <name> or /create link #team" - alice ##> "/create link #team" - gLink <- getGroupLink alice "team" GRMember True - bob ##> ("/c " <> gLink) - bob <## "connection request sent!" - alice <## "bob (Bob): accepting request to join group #team..." - concurrentlyN_ - [ do - alice <## "bob (Bob): contact is connected" - alice <## "bob invited to group #team via your group link" - alice <## "#team: bob joined the group", - do - bob <## "alice (Alice): contact is connected" - bob <## "#team: you joined the group" - ] - -- sending/receiving a message marks contact as used - threadDelay 100000 - alice @@@ [("#team", "connected")] - bob @@@ [("#team", "connected")] - alice #> "@bob hello" - bob <# "alice> hello" - threadDelay 500000 - alice #$> ("/clear bob", id, "bob: all messages are removed locally ONLY") - alice @@@ [("@bob", ""), ("#team", "connected")] - bob #$> ("/clear alice", id, "alice: all messages are removed locally ONLY") - bob @@@ [("@alice", ""), ("#team", "connected")] - -testGroupLinkIncognitoMembership :: HasCallStack => FilePath -> IO () -testGroupLinkIncognitoMembership = - testChatCfg4 testCfgGroupLinkViaContact aliceProfile bobProfile cathProfile danProfile $ +testGroupLinkIncognitoJoinInvite :: HasCallStack => TestParams -> IO () +testGroupLinkIncognitoJoinInvite = + testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do -- bob connected incognito to alice alice ##> "/c" @@ -2037,41 +2280,25 @@ testGroupLinkIncognitoMembership = cath ##> ("/c " <> gLink) cath <## "connection request sent!" bob <## "cath (Catherine): accepting request to join group #team..." - _ <- getTermLine bob concurrentlyN_ - [ do - bob <## ("cath (Catherine): contact is connected, your incognito profile for this contact is " <> bobIncognito) - bob <## "use /i cath to print out this incognito profile again" - bob <## "cath invited to group #team via your group link" - bob <## "#team: cath joined the group", + [ bob <## "#team: cath joined the group", do - cath <## (bobIncognito <> ": contact is connected") + cath <## "#team: joining the group..." cath <## "#team: you joined the group" cath <## "#team: member alice (Alice) is connected", do alice <## ("#team: " <> bobIncognito <> " added cath (Catherine) to the group (connecting...)") alice <## "#team: new member cath is connected" ] - bob ?#> "@cath hi, I'm incognito" - cath <# (bobIncognito <> "> hi, I'm incognito") - cath #> ("@" <> bobIncognito <> " hey, I'm cath") - bob ?<# "cath> hey, I'm cath" -- dan joins incognito dan ##> ("/c i " <> gLink) danIncognito <- getTermLine dan dan <## "connection request sent incognito!" bob <## (danIncognito <> ": accepting request to join group #team...") - _ <- getTermLine bob - _ <- getTermLine dan concurrentlyN_ - [ do - bob <## (danIncognito <> ": contact is connected, your incognito profile for this contact is " <> bobIncognito) - bob <## ("use /i " <> danIncognito <> " to print out this incognito profile again") - bob <## (danIncognito <> " invited to group #team via your group link") - bob <## ("#team: " <> danIncognito <> " joined the group"), + [ bob <## ("#team: " <> danIncognito <> " joined the group"), do - dan <## (bobIncognito <> ": contact is connected, your incognito profile for this contact is " <> danIncognito) - dan <## ("use /i " <> bobIncognito <> " to print out this incognito profile again") + dan <## "#team: joining the group..." dan <## ("#team: you joined the group incognito as " <> danIncognito) dan <### [ "#team: member alice (Alice) is connected", @@ -2084,10 +2311,6 @@ testGroupLinkIncognitoMembership = cath <## ("#team: " <> bobIncognito <> " added " <> danIncognito <> " to the group (connecting...)") cath <## ("#team: new member " <> danIncognito <> " is connected") ] - bob ?#> ("@" <> danIncognito <> " hi, I'm incognito") - dan ?<# (bobIncognito <> "> hi, I'm incognito") - dan ?#> ("@" <> bobIncognito <> " hey, me too") - bob ?<# (danIncognito <> "> hey, me too") alice #> "#team hello" concurrentlyN_ [ bob ?<# "#team alice> hello", @@ -2113,368 +2336,10 @@ testGroupLinkIncognitoMembership = cath <# ("#team " <> danIncognito <> "> how is it going?") ] -testGroupLinkUnusedHostContactDeleted :: HasCallStack => FilePath -> IO () -testGroupLinkUnusedHostContactDeleted = - testChatCfg2 cfg aliceProfile bobProfile $ - \alice bob -> do - -- create group 1 - alice ##> "/g team" - alice <## "group #team is created" - alice <## "to add members use /a team <name> or /create link #team" - alice ##> "/create link #team" - gLinkTeam <- getGroupLink alice "team" GRMember True - bob ##> ("/c " <> gLinkTeam) - bob <## "connection request sent!" - alice <## "bob (Bob): accepting request to join group #team..." - concurrentlyN_ - [ do - alice <## "bob (Bob): contact is connected" - alice <## "bob invited to group #team via your group link" - alice <## "#team: bob joined the group", - do - bob <## "alice (Alice): contact is connected" - bob <## "#team: you joined the group" - ] - -- create group 2 - alice ##> "/g club" - alice <## "group #club is created" - alice <## "to add members use /a club <name> or /create link #club" - alice ##> "/create link #club" - gLinkClub <- getGroupLink alice "club" GRMember True - bob ##> ("/c " <> gLinkClub) - bob <## "connection request sent!" - alice <## "bob_1 (Bob): accepting request to join group #club..." - concurrentlyN_ - [ alice - <### [ "bob_1 (Bob): contact is connected", - "contact bob_1 is merged into bob", - "use @bob <message> to send messages", - EndsWith "invited to group #club via your group link", - EndsWith "joined the group" - ], - bob - <### [ "alice_1 (Alice): contact is connected", - "contact alice_1 is merged into alice", - "use @alice <message> to send messages", - "#club: you joined the group" - ] - ] - -- list contacts - bob ##> "/contacts" - bob <## "alice (Alice)" - -- delete group 1, host contact and profile are kept - bobLeaveDeleteGroup alice bob "team" - bob ##> "/contacts" - bob <## "alice (Alice)" - bob `hasContactProfiles` ["alice", "bob"] - -- delete group 2, unused host contact and profile are deleted - bobLeaveDeleteGroup alice bob "club" - threadDelay 3000000 - bob ##> "/contacts" - (bob </) - bob `hasContactProfiles` ["bob"] - where - cfg = mkCfgGroupLinkViaContact $ testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0} - bobLeaveDeleteGroup :: HasCallStack => TestCC -> TestCC -> String -> IO () - bobLeaveDeleteGroup alice bob group = do - bob ##> ("/l " <> group) - concurrentlyN_ - [ do - bob <## ("#" <> group <> ": you left the group") - bob <## ("use /d #" <> group <> " to delete the group"), - alice <## ("#" <> group <> ": bob left the group") - ] - bob ##> ("/d #" <> group) - bob <## ("#" <> group <> ": you deleted the group") - -testGroupLinkIncognitoUnusedHostContactsDeleted :: HasCallStack => FilePath -> IO () -testGroupLinkIncognitoUnusedHostContactsDeleted = - testChatCfg2 cfg aliceProfile bobProfile $ - \alice bob -> do - bobIncognitoTeam <- createGroupBobIncognito alice bob "team" "alice" - bobIncognitoClub <- createGroupBobIncognito alice bob "club" "alice_1" - bobIncognitoTeam `shouldNotBe` bobIncognitoClub - -- list contacts - bob ##> "/contacts" - bob <## "i alice (Alice)" - bob <## "i alice_1 (Alice)" - bob `hasContactProfiles` ["alice", "alice", "bob", T.pack bobIncognitoTeam, T.pack bobIncognitoClub] - -- delete group 1, unused host contact and profile are deleted - bobLeaveDeleteGroup alice bob "team" bobIncognitoTeam - threadDelay 3000000 - bob ##> "/contacts" - bob <## "i alice_1 (Alice)" - bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognitoClub] - -- delete group 2, unused host contact and profile are deleted - bobLeaveDeleteGroup alice bob "club" bobIncognitoClub - threadDelay 3000000 - bob ##> "/contacts" - (bob </) - bob `hasContactProfiles` ["bob"] - where - cfg = mkCfgGroupLinkViaContact $ testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0} - createGroupBobIncognito :: HasCallStack => TestCC -> TestCC -> String -> String -> IO String - createGroupBobIncognito alice bob group bobsAliceContact = do - alice ##> ("/g " <> group) - alice <## ("group #" <> group <> " is created") - alice <## ("to add members use /a " <> group <> " <name> or /create link #" <> group) - alice ##> ("/create link #" <> group) - gLinkTeam <- getGroupLink alice group GRMember True - bob ##> ("/c i " <> gLinkTeam) - bobIncognito <- getTermLine bob - bob <## "connection request sent incognito!" - alice <## (bobIncognito <> ": accepting request to join group #" <> group <> "...") - _ <- getTermLine bob - concurrentlyN_ - [ do - alice <## (bobIncognito <> ": contact is connected") - alice <## (bobIncognito <> " invited to group #" <> group <> " via your group link") - alice <## ("#" <> group <> ": " <> bobIncognito <> " joined the group"), - do - bob <## (bobsAliceContact <> " (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito) - bob <## ("use /i " <> bobsAliceContact <> " to print out this incognito profile again") - bob <## ("#" <> group <> ": you joined the group incognito as " <> bobIncognito) - ] - pure bobIncognito - bobLeaveDeleteGroup :: HasCallStack => TestCC -> TestCC -> String -> String -> IO () - bobLeaveDeleteGroup alice bob group bobIncognito = do - bob ##> ("/l " <> group) - concurrentlyN_ - [ do - bob <## ("#" <> group <> ": you left the group") - bob <## ("use /d #" <> group <> " to delete the group"), - alice <## ("#" <> group <> ": " <> bobIncognito <> " left the group") - ] - bob ##> ("/d #" <> group) - bob <## ("#" <> group <> ": you deleted the group") - -testGroupLinkMemberRole :: HasCallStack => FilePath -> IO () -testGroupLinkMemberRole = - testChatCfg3 testCfgGroupLinkViaContact aliceProfile bobProfile cathProfile $ - \alice bob cath -> do - alice ##> "/g team" - alice <## "group #team is created" - alice <## "to add members use /a team <name> or /create link #team" - alice ##> "/create link #team admin" - alice <## "#team: initial role for group member cannot be admin, use member or observer" - alice ##> "/create link #team observer" - gLink <- getGroupLink alice "team" GRObserver True - bob ##> ("/c " <> gLink) - bob <## "connection request sent!" - alice <## "bob (Bob): accepting request to join group #team..." - concurrentlyN_ - [ do - alice <## "bob (Bob): contact is connected" - alice <## "bob invited to group #team via your group link" - alice <## "#team: bob joined the group", - do - bob <## "alice (Alice): contact is connected" - bob <## "#team: you joined the group" - ] - alice ##> "/set link role #team admin" - alice <## "#team: initial role for group member cannot be admin, use member or observer" - alice ##> "/set link role #team member" - _ <- getGroupLink alice "team" GRMember False - cath ##> ("/c " <> gLink) - cath <## "connection request sent!" - alice <## "cath (Catherine): accepting request to join group #team..." - -- if contact existed it is merged - concurrentlyN_ - [ alice - <### [ "cath (Catherine): contact is connected", - EndsWith "invited to group #team via your group link", - EndsWith "joined the group" - ], - cath - <### [ "alice (Alice): contact is connected", - "#team: you joined the group", - "#team: member bob (Bob) is connected" - ], - do - bob <## "#team: alice added cath (Catherine) to the group (connecting...)" - bob <## "#team: new member cath is connected" - ] - alice #> "#team hello" - concurrently_ - (bob <# "#team alice> hello") - (cath <# "#team alice> hello") - cath #> "#team hello too" - concurrently_ - (alice <# "#team cath> hello too") - (bob <# "#team cath> hello too") - bob ##> "#team hey" - bob <## "#team: you don't have permission to send messages" - alice ##> "/mr #team bob member" - alice <## "#team: you changed the role of bob from observer to member" - concurrently_ - (bob <## "#team: alice changed your role from observer to member") - (cath <## "#team: alice changed the role of bob from observer to member") - bob #> "#team hey now" - concurrently_ - (alice <# "#team bob> hey now") - (cath <# "#team bob> hey now") - -testGroupLinkLeaveDelete :: HasCallStack => FilePath -> IO () -testGroupLinkLeaveDelete = - testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ - \alice bob cath -> do - connectUsers alice bob - connectUsers cath bob - alice ##> "/g team" - alice <## "group #team is created" - alice <## "to add members use /a team <name> or /create link #team" - alice ##> "/create link #team" - gLink <- getGroupLink alice "team" GRMember True - bob ##> ("/c " <> gLink) - bob <## "connection request sent!" - alice <## "bob_1 (Bob): accepting request to join group #team..." - concurrentlyN_ - [ alice - <### [ "bob_1 (Bob): contact is connected", - "contact bob_1 is merged into bob", - "use @bob <message> to send messages", - EndsWith "invited to group #team via your group link", - EndsWith "joined the group" - ], - bob - <### [ "alice_1 (Alice): contact is connected", - "contact alice_1 is merged into alice", - "use @alice <message> to send messages", - "#team: you joined the group" - ] - ] - cath ##> ("/c " <> gLink) - cath <## "connection request sent!" - alice <## "cath (Catherine): accepting request to join group #team..." - concurrentlyN_ - [ alice - <### [ "cath (Catherine): contact is connected", - "cath invited to group #team via your group link", - "#team: cath joined the group" - ], - cath - <### [ "alice (Alice): contact is connected", - "#team: you joined the group", - "#team: member bob_1 (Bob) is connected", - "contact bob_1 is merged into bob", - "use @bob <message> to send messages" - ], - bob - <### [ "#team: alice added cath_1 (Catherine) to the group (connecting...)", - "#team: new member cath_1 is connected", - "contact cath_1 is merged into cath", - "use @cath <message> to send messages" - ] - ] - 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", - cath <## "#team: bob left the group" - ] - bob ##> "/contacts" - bob <## "alice (Alice)" - bob <## "cath (Catherine)" - bob ##> "/d #team" - bob <## "#team: you deleted the group" - bob ##> "/contacts" - bob <## "alice (Alice)" - bob <## "cath (Catherine)" - -testPlanGroupLinkOkKnown :: HasCallStack => FilePath -> IO () -testPlanGroupLinkOkKnown = - testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $ - \alice bob -> do - alice ##> "/g team" - alice <## "group #team is created" - alice <## "to add members use /a team <name> or /create link #team" - alice ##> "/create link #team" - gLink <- getGroupLink alice "team" GRMember True - - bob ##> ("/_connect plan 1 " <> gLink) - bob <## "group link: ok to connect" - - bob ##> ("/c " <> gLink) - bob <## "connection request sent!" - alice <## "bob (Bob): accepting request to join group #team..." - concurrentlyN_ - [ do - alice <## "bob (Bob): contact is connected" - alice <## "bob invited to group #team via your group link" - alice <## "#team: bob joined the group", - do - bob <## "alice (Alice): contact is connected" - bob <## "#team: you joined the group" - ] - alice #> "#team hi" - bob <# "#team alice> hi" - bob #> "#team hey" - alice <# "#team bob> hey" - - bob ##> ("/_connect plan 1 " <> gLink) - bob <## "group link: known group #team" - bob <## "use #team <message> to send messages" - - let gLinkSchema2 = linkAnotherSchema gLink - bob ##> ("/_connect plan 1 " <> gLinkSchema2) - bob <## "group link: known group #team" - bob <## "use #team <message> to send messages" - - bob ##> ("/c " <> gLink) - bob <## "group link: known group #team" - bob <## "use #team <message> to send messages" - -testPlanHostContactDeletedGroupLinkKnown :: HasCallStack => FilePath -> IO () -testPlanHostContactDeletedGroupLinkKnown = - testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $ - \alice bob -> do - alice ##> "/g team" - alice <## "group #team is created" - alice <## "to add members use /a team <name> or /create link #team" - alice ##> "/create link #team" - gLink <- getGroupLink alice "team" GRMember True - - bob ##> ("/c " <> gLink) - bob <## "connection request sent!" - alice <## "bob (Bob): accepting request to join group #team..." - concurrentlyN_ - [ do - alice <## "bob (Bob): contact is connected" - alice <## "bob invited to group #team via your group link" - alice <## "#team: bob joined the group", - do - bob <## "alice (Alice): contact is connected" - bob <## "#team: you joined the group" - ] - alice #> "#team hi" - bob <# "#team alice> hi" - bob #> "#team hey" - alice <# "#team bob> hey" - - alice <##> bob - threadDelay 500000 - bob ##> "/d alice" - bob <## "alice: contact is deleted" - alice <## "bob (Bob) deleted contact with you" - - bob ##> ("/_connect plan 1 " <> gLink) - bob <## "group link: known group #team" - bob <## "use #team <message> to send messages" - - let gLinkSchema2 = linkAnotherSchema gLink - bob ##> ("/_connect plan 1 " <> gLinkSchema2) - bob <## "group link: known group #team" - bob <## "use #team <message> to send messages" - - bob ##> ("/c " <> gLink) - bob <## "group link: known group #team" - bob <## "use #team <message> to send messages" - -testPlanGroupLinkOwn :: HasCallStack => FilePath -> IO () -testPlanGroupLinkOwn tmp = - withNewTestChatCfg tmp testCfgGroupLinkViaContact "alice" aliceProfile $ \alice -> do +testPlanGroupLinkOwn :: HasCallStack => TestParams -> IO () +testPlanGroupLinkOwn ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team <name> or /create link #team" @@ -2492,18 +2357,14 @@ testPlanGroupLinkOwn tmp = alice <## "connection request sent!" alice <## "alice_1 (Alice): accepting request to join group #team..." alice - <### [ "alice_1 (Alice): contact is connected", - "alice_1 invited to group #team via your group link", - "#team: alice_1 joined the group", - "alice_2 (Alice): contact is connected", - "#team_1: you joined the group", - "contact alice_2 is merged into alice_1", - "use @alice_1 <message> to send messages" + <### [ "#team: alice_1 joined the group", + "#team_1: joining the group...", + "#team_1: you joined the group" ] alice `send` "#team 1" alice <### [ WithTime "#team 1", - WithTime "#team_1 alice_1> 1" + WithTime "#team_1 alice_2> 1" ] alice `send` "#team_1 2" alice @@ -2517,67 +2378,11 @@ testPlanGroupLinkOwn tmp = alice ##> ("/_connect plan 1 " <> gLinkSchema2) alice <## "group link: own link for group #team" - -- group works if merged contact is deleted - alice ##> "/d alice_1" - alice <## "alice_1: contact is deleted" - - alice `send` "#team 3" - alice - <### [ WithTime "#team 3", - WithTime "#team_1 alice_1> 3" - ] - alice `send` "#team_1 4" - alice - <### [ WithTime "#team_1 4", - WithTime "#team alice_1> 4" - ] - -testPlanGroupLinkConnecting :: HasCallStack => FilePath -> IO () -testPlanGroupLinkConnecting tmp = do - gLink <- withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do - alice ##> "/g team" - alice <## "group #team is created" - alice <## "to add members use /a team <name> or /create link #team" - alice ##> "/create link #team" - getGroupLink alice "team" GRMember True - withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do - threadDelay 100000 - - bob ##> ("/c " <> gLink) - bob <## "connection request sent!" - - bob ##> ("/_connect plan 1 " <> gLink) - bob <## "group link: connecting, allowed to reconnect" - - let gLinkSchema2 = linkAnotherSchema gLink - bob ##> ("/_connect plan 1 " <> gLinkSchema2) - bob <## "group link: connecting, allowed to reconnect" - - threadDelay 100000 - withTestChatCfg tmp cfg "alice" $ \alice -> do - alice - <### [ "1 group links active", - "#team: group is empty", - "bob (Bob): accepting request to join group #team..." - ] - withTestChatCfg tmp cfg "bob" $ \bob -> do - threadDelay 500000 - bob ##> ("/_connect plan 1 " <> gLink) - bob <## "group link: connecting" - - let gLinkSchema2 = linkAnotherSchema gLink - bob ##> ("/_connect plan 1 " <> gLinkSchema2) - bob <## "group link: connecting" - - bob ##> ("/c " <> gLink) - bob <## "group link: connecting" - where - cfg = testCfgGroupLinkViaContact - -testPlanGroupLinkLeaveRejoin :: HasCallStack => FilePath -> IO () +testPlanGroupLinkLeaveRejoin :: HasCallStack => TestParams -> IO () testPlanGroupLinkLeaveRejoin = - testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $ + testChat2 aliceProfile bobProfile $ \alice bob -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team <name> or /create link #team" @@ -2588,15 +2393,14 @@ testPlanGroupLinkLeaveRejoin = bob <## "connection request sent!" alice <## "bob (Bob): accepting request to join group #team..." concurrentlyN_ - [ do - alice <## "bob (Bob): contact is connected" - alice <## "bob invited to group #team via your group link" - alice <## "#team: bob joined the group", + [ alice <## "#team: bob joined the group", do - bob <## "alice (Alice): contact is connected" + bob <## "#team: joining the group..." bob <## "#team: you joined the group" ] + threadDelay 100000 + bob ##> ("/_connect plan 1 " <> gLink) bob <## "group link: known group #team" bob <## "use #team <message> to send messages" @@ -2605,6 +2409,8 @@ testPlanGroupLinkLeaveRejoin = bob <## "group link: known group #team" bob <## "use #team <message> to send messages" + threadDelay 100000 + bob ##> "/leave #team" concurrentlyN_ [ do @@ -2613,6 +2419,8 @@ testPlanGroupLinkLeaveRejoin = alice <## "#team: bob left the group" ] + threadDelay 100000 + bob ##> ("/_connect plan 1 " <> gLink) bob <## "group link: ok to connect" @@ -2624,25 +2432,17 @@ testPlanGroupLinkLeaveRejoin = bob <## "connection request sent!" alice <## "bob_1 (Bob): accepting request to join group #team..." concurrentlyN_ - [ alice - <### [ "bob_1 (Bob): contact is connected", - EndsWith "invited to group #team via your group link", - EndsWith "joined the group", - "contact bob_1 is merged into bob", - "use @bob <message> to send messages" - ], + [ alice <## "#team: bob_1 joined the group", bob - <### [ "alice_1 (Alice): contact is connected", - "#team_1: you joined the group", - "contact alice_1 is merged into alice", - "use @alice <message> to send messages" + <### [ "#team_1: joining the group...", + "#team_1: you joined the group" ] ] alice #> "#team hi" - bob <# "#team_1 alice> hi" + bob <# "#team_1 alice_1> hi" bob #> "#team_1 hey" - alice <# "#team bob> hey" + alice <# "#team bob_1> hey" bob ##> ("/_connect plan 1 " <> gLink) bob <## "group link: known group #team_1" @@ -2656,10 +2456,11 @@ testPlanGroupLinkLeaveRejoin = bob <## "group link: known group #team_1" bob <## "use #team_1 <message> to send messages" -testGroupLinkNoContact :: HasCallStack => FilePath -> IO () -testGroupLinkNoContact = +testGroupLink :: HasCallStack => TestParams -> IO () +testGroupLink = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team <name> or /create link #team" @@ -2681,7 +2482,7 @@ testGroupLinkNoContact = ] threadDelay 100000 - alice #$> ("/_get chat #1 count=100", chat, [(0, "invited via your group link"), (0, "connected")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(1, "Recent history: off"), (0, "invited via your group link"), (0, "connected")]) alice @@@ [("#team", "connected")] bob @@@ [("#team", "connected")] @@ -2693,16 +2494,34 @@ testGroupLinkNoContact = bob #> "#team hi there" alice <# "#team bob> hi there" + -- user address doesn't interfere + alice ##> "/ad" + cLink <- getContactLink alice True + cath ##> ("/c " <> cLink) + alice <#? cath + alice ##> "/ac cath" + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" + concurrently_ + (cath <## "alice (Alice): contact is connected") + (alice <## "cath (Catherine): contact is connected") + alice <##> cath + + -- third member cath ##> ("/c " <> gLink) cath <## "connection request sent!" concurrentlyN_ [ do - alice <## "cath (Catherine): accepting request to join group #team..." - alice <## "#team: cath joined the group", - do - cath <## "#team: joining the group..." - cath <## "#team: you joined the group" - cath <## "#team: member bob (Bob) is connected", + alice <## "cath_1 (Catherine): accepting request to join group #team..." + alice <## "#team: cath_1 joined the group" + alice <## "contact and member are merged: cath, #team cath_1" + alice <## "use @cath <message> to send messages", + cath + <### [ "#team: joining the group...", + "#team: you joined the group", + "#team: member bob (Bob) is connected", + "contact and member are merged: alice, #team alice_1", + "use @alice <message> to send messages" + ], do bob <## "#team: alice added cath (Catherine) to the group (connecting...)" bob <## "#team: new member cath is connected" @@ -2716,8 +2535,28 @@ testGroupLinkNoContact = alice <# "#team bob> hi cath" cath <# "#team bob> hi cath" -testGroupLinkNoContactInviteesWereConnected :: HasCallStack => FilePath -> IO () -testGroupLinkNoContactInviteesWereConnected = + -- leaving group removes link + alice ##> "/l team" + concurrentlyN_ + [ do + alice <## "#team: you left the group" + alice <## "use /d #team to delete the group", + bob <## "#team: alice left the group", + cath <## "#team: alice left the group" + ] + alice ##> "/show link #team" + alice <## "no group link, to create: /create link #team" + + -- deleting group keeps contacts + alice ##> "/contacts" + alice <## "cath (Catherine)" + alice ##> "/d #team" + alice <## "#team: you deleted the group" + alice ##> "/contacts" + alice <## "cath (Catherine)" + +testGroupLinkInviteesWereConnected :: HasCallStack => TestParams -> IO () +testGroupLinkInviteesWereConnected = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do connectUsers bob cath @@ -2744,7 +2583,7 @@ testGroupLinkNoContactInviteesWereConnected = ] threadDelay 100000 - alice #$> ("/_get chat #1 count=100", chat, [(0, "invited via your group link"), (0, "connected")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(1, "Recent history: off"), (0, "invited via your group link"), (0, "connected")]) alice @@@ [("#team", "connected")] bob @@@ [("#team", "connected"), ("@cath", "hey")] @@ -2788,8 +2627,8 @@ testGroupLinkNoContactInviteesWereConnected = cath #> "#team 3" [alice, bob] *<# "#team cath> 3" -testGroupLinkNoContactAllMembersWereConnected :: HasCallStack => FilePath -> IO () -testGroupLinkNoContactAllMembersWereConnected = +testGroupLinkAllMembersWereConnected :: HasCallStack => TestParams -> IO () +testGroupLinkAllMembersWereConnected = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do connectUsers alice bob @@ -2825,7 +2664,7 @@ testGroupLinkNoContactAllMembersWereConnected = ] threadDelay 100000 - alice #$> ("/_get chat #1 count=100", chat, [(0, "invited via your group link"), (0, "connected")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(1, "Recent history: off"), (0, "invited via your group link"), (0, "connected")]) alice @@@ [("#team", "connected"), ("@bob", "hey"), ("@cath", "hey")] bob @@@ [("#team", "connected"), ("@alice", "hey"), ("@cath", "hey")] @@ -2879,10 +2718,11 @@ testGroupLinkNoContactAllMembersWereConnected = cath #> "#team 3" [alice, bob] *<# "#team cath> 3" -testGroupLinkNoContactMemberRole :: HasCallStack => FilePath -> IO () -testGroupLinkNoContactMemberRole = +testGroupLinkMemberRole :: HasCallStack => TestParams -> IO () +testGroupLinkMemberRole = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team <name> or /create link #team" @@ -2916,7 +2756,7 @@ testGroupLinkNoContactMemberRole = bob <## "#team: you don't have permission to send messages" alice ##> "/mr #team bob member" - alice <## "#team: you changed the role of bob from observer to member" + alice <## "#team: you changed the role of bob to member" bob <## "#team: alice changed your role from observer to member" bob #> "#team hey now" @@ -2946,7 +2786,7 @@ testGroupLinkNoContactMemberRole = cath <## "#team: you don't have permission to send messages" alice ##> "/mr #team cath admin" - alice <## "#team: you changed the role of cath from observer to admin" + alice <## "#team: you changed the role of cath to admin" cath <## "#team: alice changed your role from observer to admin" bob <## "#team: alice changed the role of cath from observer to admin" @@ -2955,12 +2795,12 @@ testGroupLinkNoContactMemberRole = bob <# "#team cath> hey" cath ##> "/mr #team bob admin" - cath <## "#team: you changed the role of bob from member to admin" + cath <## "#team: you changed the role of bob to admin" bob <## "#team: cath changed your role from member to admin" alice <## "#team: cath changed the role of bob from member to admin" -testGroupLinkNoContactHostIncognito :: HasCallStack => FilePath -> IO () -testGroupLinkNoContactHostIncognito = +testGroupLinkHostIncognito :: HasCallStack => TestParams -> IO () +testGroupLinkHostIncognito = testChat2 aliceProfile bobProfile $ \alice bob -> do alice ##> "/g i team" @@ -2980,7 +2820,7 @@ testGroupLinkNoContactHostIncognito = ] threadDelay 100000 - alice #$> ("/_get chat #1 count=100", chat, [(0, "invited via your group link"), (0, "connected")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "invited via your group link"), (0, "connected")]) alice @@@ [("#team", "connected")] bob @@@ [("#team", "connected")] @@ -2992,10 +2832,11 @@ testGroupLinkNoContactHostIncognito = bob #> "#team hi there" alice ?<# "#team bob> hi there" -testGroupLinkNoContactInviteeIncognito :: HasCallStack => FilePath -> IO () -testGroupLinkNoContactInviteeIncognito = +testGroupLinkInviteeIncognito :: HasCallStack => TestParams -> IO () +testGroupLinkInviteeIncognito = testChat2 aliceProfile bobProfile $ \alice bob -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team <name> or /create link #team" @@ -3013,7 +2854,7 @@ testGroupLinkNoContactInviteeIncognito = ] threadDelay 100000 - alice #$> ("/_get chat #1 count=100", chat, [(0, "invited via your group link"), (0, "connected")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "invited via your group link"), (0, "connected")]) alice @@@ [("#team", "connected")] bob @@@ [("#team", "connected")] @@ -3025,8 +2866,8 @@ testGroupLinkNoContactInviteeIncognito = bob ?#> "#team hi there" alice <# ("#team " <> bobIncognito <> "> hi there") -testGroupLinkNoContactHostProfileReceived :: HasCallStack => FilePath -> IO () -testGroupLinkNoContactHostProfileReceived = +testGroupLinkHostProfileReceived :: HasCallStack => TestParams -> IO () +testGroupLinkHostProfileReceived = testChat2 aliceProfile bobProfile $ \alice bob -> do let profileImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=" @@ -3053,8 +2894,8 @@ testGroupLinkNoContactHostProfileReceived = aliceImage <- getProfilePictureByName bob "alice" aliceImage `shouldBe` Just profileImage -testGroupLinkNoContactExistingContactMerged :: HasCallStack => FilePath -> IO () -testGroupLinkNoContactExistingContactMerged = +testGroupLinkExistingContactMerged :: HasCallStack => TestParams -> IO () +testGroupLinkExistingContactMerged = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob @@ -3080,7 +2921,7 @@ testGroupLinkNoContactExistingContactMerged = ] threadDelay 100000 - alice #$> ("/_get chat #1 count=100", chat, [(0, "invited via your group link"), (0, "connected")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "invited via your group link"), (0, "connected")]) alice <##> bob @@ -3096,10 +2937,125 @@ testGroupLinkNoContactExistingContactMerged = bob #> "#team hi there" alice <# "#team bob> hi there" -testPlanGroupLinkNoContactKnown :: HasCallStack => FilePath -> IO () -testPlanGroupLinkNoContactKnown = +testGLinkRejectBlockedName :: HasCallStack => TestParams -> IO () +testGLinkRejectBlockedName = + testChatCfg2 cfg aliceProfile bobProfile $ + \alice bob -> do + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team <name> or /create link #team" + + alice ##> "/create link #team" + gLink <- getGroupLink alice "team" GRMember True + bob ##> ("/c " <> gLink) + bob <## "connection request sent!" + alice <## "bob (Bob): rejecting request to join group #team, reason: GRRBlockedName" + bob <## "#team: joining the group..." + bob <## "#team: join rejected, reason: GRRBlockedName" + + threadDelay 100000 + + alice `hasContactProfiles` ["alice"] + memCount <- withCCTransaction alice $ \db -> + DB.query_ db "SELECT count(1) FROM group_members" :: IO [[Int]] + memCount `shouldBe` [[1]] + + bob ##> ("/c " <> gLink) + bob <## "group link: known group #team" + bob <## "use #team <message> to send messages" + where + cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Left GRRBlockedName)}} + +testGLinkManualAcceptMember :: HasCallStack => TestParams -> IO () +testGLinkManualAcceptMember = + testChatCfg3 cfg aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup2 "team" alice bob + + alice ##> "/create link #team" + gLink <- getGroupLink alice "team" GRMember True + cath ##> ("/c " <> gLink) + cath <## "connection request sent!" + alice <## "cath (Catherine): accepting request to join group #team..." + concurrentlyN_ + [ alice <## "#team: cath connected and pending approval, use /_accept member #1 3 <role> to accept member", + do + cath <## "#team: joining the group..." + cath <## "#team: you joined the group, pending approval" + ] + + -- pending approval member doesn't see messages sent in group + alice #> "#team hi group" + bob <# "#team alice> hi group" + + bob #> "#team hey" + alice <# "#team bob> hey" + + -- pending approval member and host can send messages to each other + alice ##> "/_send #1 @3 text send me proofs" + alice <# "#team send me proofs" + cath <# "#team alice> send me proofs" + + cath ##> "/_send #1 @1 text proofs" + cath <# "#team proofs" + alice <# "#team cath> proofs" + + -- accept member + alice ##> "/_accept member #1 3 member" + concurrentlyN_ + [ alice <## "#team: cath joined the group", + cath + <### [ "#team: you joined the group", + WithTime "#team alice> hi group [>>]", + WithTime "#team bob> hey [>>]", + "#team: member bob (Bob) is connected" + ], + do + bob <## "#team: alice added cath (Catherine) to the group (connecting...)" + bob <## "#team: new member cath is connected" + ] + + alice #> "#team welcome cath" + [bob, cath] *<# "#team alice> welcome cath" + + bob #> "#team hi cath" + [alice, cath] *<# "#team bob> hi cath" + + cath #> "#team hi group" + [alice, bob] *<# "#team cath> hi group" + where + cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Right (GAPending, GRObserver))}} + +testGLinkDeletePendingMember :: HasCallStack => TestParams -> IO () +testGLinkDeletePendingMember = + testChatCfg3 cfg aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup2 "team" alice bob + + alice ##> "/create link #team" + gLink <- getGroupLink alice "team" GRMember True + cath ##> ("/c " <> gLink) + cath <## "connection request sent!" + alice <## "cath (Catherine): accepting request to join group #team..." + concurrentlyN_ + [ alice <## "#team: cath connected and pending approval, use /_accept member #1 3 <role> to accept member", + do + cath <## "#team: joining the group..." + cath <## "#team: you joined the group, pending approval" + ] + + alice ##> "/rm team cath" + alice <## "#team: you removed cath from the group" + cath <## "#team: alice removed you from the group" + cath <## "use /d #team to delete the group" + where + cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Right (GAPending, GRObserver))}} + +testPlanGroupLinkKnown :: HasCallStack => TestParams -> IO () +testPlanGroupLinkKnown = testChat2 aliceProfile bobProfile $ \alice bob -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team <name> or /create link #team" @@ -3132,15 +3088,16 @@ testPlanGroupLinkNoContactKnown = bob <## "group link: known group #team" bob <## "use #team <message> to send messages" -testPlanGroupLinkNoContactConnecting :: HasCallStack => FilePath -> IO () -testPlanGroupLinkNoContactConnecting tmp = do - gLink <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do +testPlanGroupLinkConnecting :: HasCallStack => TestParams -> IO () +testPlanGroupLinkConnecting ps = do + gLink <- withNewTestChat ps "alice" aliceProfile $ \alice -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team <name> or /create link #team" alice ##> "/create link #team" getGroupLink alice "team" GRMember True - withNewTestChat tmp "bob" bobProfile $ \bob -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do threadDelay 100000 bob ##> ("/c " <> gLink) @@ -3154,13 +3111,60 @@ testPlanGroupLinkNoContactConnecting tmp = do bob <## "group link: connecting, allowed to reconnect" threadDelay 100000 - withTestChat tmp "alice" $ \alice -> do + withTestChat ps "alice" $ \alice -> do alice <### [ "1 group links active", "#team: group is empty", "bob (Bob): accepting request to join group #team..." ] - withTestChat tmp "bob" $ \bob -> do + withTestChat ps "bob" $ \bob -> do + threadDelay 500000 + bob <## "#team: joining the group..." + bob <## "#team: you joined the group" + + bob ##> ("/_connect plan 1 " <> gLink) + bob <## "group link: known group #team" + bob <## "use #team <message> to send messages" + + let gLinkSchema2 = linkAnotherSchema gLink + bob ##> ("/_connect plan 1 " <> gLinkSchema2) + bob <## "group link: known group #team" + bob <## "use #team <message> to send messages" + + bob ##> ("/c " <> gLink) + bob <## "group link: known group #team" + bob <## "use #team <message> to send messages" + +testPlanGroupLinkConnectingSlow :: HasCallStack => TestParams -> IO () +testPlanGroupLinkConnectingSlow ps = do + gLink <- withNewTestChatCfg ps testCfgSlow "alice" aliceProfile $ \alice -> do + threadDelay 100000 + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team <name> or /create link #team" + alice ##> "/create link #team" + getGroupLink alice "team" GRMember True + withNewTestChatCfg ps testCfgSlow "bob" bobProfile $ \bob -> do + threadDelay 100000 + + bob ##> ("/c " <> gLink) + bob <## "connection request sent!" + + bob ##> ("/_connect plan 1 " <> gLink) + bob <## "group link: connecting, allowed to reconnect" + + let gLinkSchema2 = linkAnotherSchema gLink + bob ##> ("/_connect plan 1 " <> gLinkSchema2) + bob <## "group link: connecting, allowed to reconnect" + + threadDelay 100000 + withTestChatCfg ps testCfgSlow "alice" $ \alice -> do + alice + <### [ "1 group links active", + "#team: group is empty", + "bob (Bob): accepting request to join group #team..." + ] + withTestChatCfg ps testCfgSlow "bob" $ \bob -> do threadDelay 500000 bob <## "#team: joining the group..." @@ -3174,29 +3178,30 @@ testPlanGroupLinkNoContactConnecting tmp = do bob ##> ("/c " <> gLink) bob <## "group link: connecting to group #team" -testGroupMsgDecryptError :: HasCallStack => FilePath -> IO () -testGroupMsgDecryptError tmp = - withNewTestChat tmp "alice" aliceProfile $ \alice -> do - withNewTestChat tmp "bob" bobProfile $ \bob -> do +#if !defined(dbPostgres) +testGroupMsgDecryptError :: HasCallStack => TestParams -> IO () +testGroupMsgDecryptError ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do createGroup2 "team" alice bob alice #> "#team hi" bob <# "#team alice> hi" bob #> "#team hey" alice <# "#team bob> hey" - setupDesynchronizedRatchet tmp alice - withTestChat tmp "bob" $ \bob -> do + setupDesynchronizedRatchet ps alice + withTestChat ps "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob <## "#team: connected to server(s)" alice #> "#team hello again" - bob <# "#team alice> skipped message ID 10..12" + bob <# "#team alice> skipped message ID 9..11" bob <# "#team alice> hello again" bob #> "#team received!" alice <# "#team bob> received!" -setupDesynchronizedRatchet :: HasCallStack => FilePath -> TestCC -> IO () -setupDesynchronizedRatchet tmp alice = do +setupDesynchronizedRatchet :: HasCallStack => TestParams -> TestCC -> IO () +setupDesynchronizedRatchet ps alice = do copyDb "bob" "bob_old" - withTestChat tmp "bob" $ \bob -> do + withTestChat ps "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob <## "#team: connected to server(s)" alice #> "#team 1" @@ -3207,11 +3212,11 @@ setupDesynchronizedRatchet tmp alice = do bob <# "#team alice> 3" bob #> "#team 4" alice <# "#team bob> 4" - withTestChat tmp "bob_old" $ \bob -> do + withTestChat ps "bob_old" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob <## "#team: connected to server(s)" bob ##> "/sync #team alice" - bob <## "error: command is prohibited" + bob <## "error: command is prohibited, synchronizeRatchet: not allowed" alice #> "#team 1" bob <## "#team alice: decryption error (connection out of sync), synchronization required" bob <## "use /sync #team alice to synchronize" @@ -3221,25 +3226,27 @@ setupDesynchronizedRatchet tmp alice = do bob ##> "/tail #team 1" bob <# "#team alice> decryption error, possibly due to the device change (header, 3 messages)" where + tmp = tmpPath ps copyDb from to = do - copyFile (chatStoreFile $ tmp </> from) (chatStoreFile $ tmp </> to) - copyFile (agentStoreFile $ tmp </> from) (agentStoreFile $ tmp </> to) + copyFile (tmp </> (from <> chatSuffix)) (tmp </> (to <> chatSuffix)) + copyFile (tmp </> (from <> agentSuffix)) (tmp </> (to <> agentSuffix)) -testGroupSyncRatchet :: HasCallStack => FilePath -> IO () -testGroupSyncRatchet tmp = - withNewTestChat tmp "alice" aliceProfile $ \alice -> do - withNewTestChat tmp "bob" bobProfile $ \bob -> do +testGroupSyncRatchet :: HasCallStack => TestParams -> IO () +testGroupSyncRatchet ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do createGroup2 "team" alice bob alice #> "#team hi" bob <# "#team alice> hi" bob #> "#team hey" alice <# "#team bob> hey" - setupDesynchronizedRatchet tmp alice - withTestChat tmp "bob_old" $ \bob -> do + setupDesynchronizedRatchet ps alice + withTestChat ps "bob_old" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob <## "#team: connected to server(s)" bob `send` "#team 1" - bob <## "error: command is prohibited" -- silence? + -- "send prohibited" error is not printed in group as SndMessage is created, + -- but it should be displayed in per member snd statuses bob <# "#team 1" (alice </) -- synchronize bob and alice @@ -3250,6 +3257,7 @@ testGroupSyncRatchet tmp = alice <## "#team bob: connection synchronized" bob <## "#team alice: connection synchronized" + threadDelay 100000 bob #$> ("/_get chat #1 count=3", chat, [(1, "connection synchronization started for alice"), (0, "connection synchronization agreed"), (0, "connection synchronized")]) alice #$> ("/_get chat #1 count=2", chat, [(0, "connection synchronization agreed"), (0, "connection synchronized")]) @@ -3258,10 +3266,10 @@ testGroupSyncRatchet tmp = bob #> "#team received!" alice <# "#team bob> received!" -testGroupSyncRatchetCodeReset :: HasCallStack => FilePath -> IO () -testGroupSyncRatchetCodeReset tmp = - withNewTestChat tmp "alice" aliceProfile $ \alice -> do - withNewTestChat tmp "bob" bobProfile $ \bob -> do +testGroupSyncRatchetCodeReset :: HasCallStack => TestParams -> IO () +testGroupSyncRatchetCodeReset ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do createGroup2 "team" alice bob alice #> "#team hi" bob <# "#team alice> hi" @@ -3278,8 +3286,8 @@ testGroupSyncRatchetCodeReset tmp = -- connection verified bob ##> "/i #team alice" aliceInfo bob True - setupDesynchronizedRatchet tmp alice - withTestChat tmp "bob_old" $ \bob -> do + setupDesynchronizedRatchet ps alice + withTestChat ps "bob_old" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob <## "#team: connected to server(s)" bob ##> "/sync #team alice" @@ -3290,6 +3298,7 @@ testGroupSyncRatchetCodeReset tmp = alice <## "#team bob: connection synchronized" bob <## "#team alice: connection synchronized" + threadDelay 100000 bob #$> ("/_get chat #1 count=4", chat, [(1, "connection synchronization started for alice"), (0, "connection synchronization agreed"), (0, "security code changed"), (0, "connection synchronized")]) alice #$> ("/_get chat #1 count=2", chat, [(0, "connection synchronization agreed"), (0, "connection synchronized")]) @@ -3314,8 +3323,9 @@ testGroupSyncRatchetCodeReset tmp = connVerified | verified = "connection verified" | otherwise = "connection not verified, use /code command to see security code" +#endif -testSetGroupMessageReactions :: HasCallStack => FilePath -> IO () +testSetGroupMessageReactions :: HasCallStack => TestParams -> IO () testSetGroupMessageReactions = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -3362,6 +3372,9 @@ testSetGroupMessageReactions = cath ##> "/tail #team 1" cath <# "#team alice> hi" cath <## " 👍 2 🚀 1" + itemId' <- lastItemId alice + alice ##> ("/_reaction members 1 #1 " <> itemId' <> " {\"type\": \"emoji\", \"emoji\": \"👍\"}") + alice <## "2 member(s) reacted" bob ##> "-1 #team hi" bob <## "removed 👍" alice <# "#team bob> > alice hi" @@ -3384,11 +3397,11 @@ testSetGroupMessageReactions = cath <# "#team alice> hi" cath <## " 👍 1" -testSendGroupDeliveryReceipts :: HasCallStack => FilePath -> IO () -testSendGroupDeliveryReceipts tmp = - withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do - withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do - withNewTestChatCfg tmp cfg "cath" cathProfile $ \cath -> do +testSendGroupDeliveryReceipts :: HasCallStack => TestParams -> IO () +testSendGroupDeliveryReceipts ps = + withNewTestChatCfg ps cfg "alice" aliceProfile $ \alice -> do + withNewTestChatCfg ps cfg "bob" bobProfile $ \bob -> do + withNewTestChatCfg ps cfg "cath" cathProfile $ \cath -> do -- turn off contacts receipts for tests alice ##> "/_set receipts contacts 1 off" alice <## "ok" @@ -3414,11 +3427,11 @@ testSendGroupDeliveryReceipts tmp = where cfg = testCfg {showReceipts = True} -testConfigureGroupDeliveryReceipts :: HasCallStack => FilePath -> IO () -testConfigureGroupDeliveryReceipts tmp = - withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do - withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do - withNewTestChatCfg tmp cfg "cath" cathProfile $ \cath -> do +testConfigureGroupDeliveryReceipts :: HasCallStack => TestParams -> IO () +testConfigureGroupDeliveryReceipts ps = + withNewTestChatCfg ps cfg "alice" aliceProfile $ \alice -> do + withNewTestChatCfg ps cfg "bob" bobProfile $ \bob -> do + withNewTestChatCfg ps cfg "cath" cathProfile $ \cath -> do -- turn off contacts receipts for tests alice ##> "/_set receipts contacts 1 off" alice <## "ok" @@ -3458,14 +3471,10 @@ testConfigureGroupDeliveryReceipts tmp = [ alice <## "#club: cath joined the group", do cath <## "#club: you joined the group" - cath <## "#club: member bob_1 (Bob) is connected" - cath <## "contact bob_1 is merged into bob" - cath <## "use @bob <message> to send messages", + cath <## "#club: member bob_1 (Bob) is connected", do bob <## "#club: alice added cath_1 (Catherine) to the group (connecting...)" bob <## "#club: new member cath_1 is connected" - bob <## "contact cath_1 is merged into cath" - bob <## "use @cath <message> to send messages" ] threadDelay 1000000 @@ -3542,56 +3551,35 @@ testConfigureGroupDeliveryReceipts tmp = receipt bob alice cath "team" "25" noReceipt bob alice cath "club" "26" where - cfg = mkCfgCreateGroupDirect $ testCfg {showReceipts = True} + cfg = testCfg {showReceipts = True} receipt cc1 cc2 cc3 gName msg = do - name1 <- userName cc1 cc1 #> ("#" <> gName <> " " <> msg) - cc2 <# ("#" <> gName <> " " <> name1 <> "> " <> msg) - cc3 <# ("#" <> gName <> " " <> name1 <> "> " <> msg) + cc2 .<## ("> " <> msg) + cc3 .<## ("> " <> msg) cc1 % ("#" <> gName <> " " <> msg) cc1 ⩗ ("#" <> gName <> " " <> msg) partialReceipt cc1 cc2 cc3 gName msg = do - name1 <- userName cc1 cc1 #> ("#" <> gName <> " " <> msg) - cc2 <# ("#" <> gName <> " " <> name1 <> "> " <> msg) - cc3 <# ("#" <> gName <> " " <> name1 <> "> " <> msg) + cc2 .<## ("> " <> msg) + cc3 .<## ("> " <> msg) cc1 % ("#" <> gName <> " " <> msg) noReceipt cc1 cc2 cc3 gName msg = do - name1 <- userName cc1 cc1 #> ("#" <> gName <> " " <> msg) - cc2 <# ("#" <> gName <> " " <> name1 <> "> " <> msg) - cc3 <# ("#" <> gName <> " " <> name1 <> "> " <> msg) + cc2 .<## ("> " <> msg) + cc3 .<## ("> " <> msg) cc1 <// 50000 -testNoGroupDirectConns :: HasCallStack => VersionRange -> VersionRange -> VersionRange -> Bool -> FilePath -> IO () -testNoGroupDirectConns hostVRange mem2VRange mem3VRange noDirectConns tmp = - withNewTestChatCfg tmp testCfg {chatVRange = hostVRange} "alice" aliceProfile $ \alice -> do - withNewTestChatCfg tmp testCfg {chatVRange = mem2VRange} "bob" bobProfile $ \bob -> do - withNewTestChatCfg tmp testCfg {chatVRange = mem3VRange} "cath" cathProfile $ \cath -> do - createGroup3 "team" alice bob cath - if noDirectConns - then contactsDontExist bob cath - else contactsExist bob cath - where - contactsDontExist bob cath = do +testNoGroupDirectConns :: HasCallStack => TestParams -> IO () +testNoGroupDirectConns = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath bob ##> "/contacts" bob <## "alice (Alice)" cath ##> "/contacts" cath <## "alice (Alice)" - contactsExist bob cath = do - bob ##> "/contacts" - bob - <### [ "alice (Alice)", - "cath (Catherine)" - ] - cath ##> "/contacts" - cath - <### [ "alice (Alice)", - "bob (Bob)" - ] - bob <##> cath -testNoDirectDifferentLDNs :: HasCallStack => FilePath -> IO () +testNoDirectDifferentLDNs :: HasCallStack => TestParams -> IO () testNoDirectDifferentLDNs = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -3661,7 +3649,7 @@ testNoDirectDifferentLDNs = bob <# ("#" <> gName <> " " <> cathLDN <> "> hey") ] -testMergeMemberExistingContact :: HasCallStack => FilePath -> IO () +testMergeMemberExistingContact :: HasCallStack => TestParams -> IO () testMergeMemberExistingContact = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -3708,7 +3696,7 @@ testMergeMemberExistingContact = alice `hasContactProfiles` ["alice", "bob", "cath"] cath `hasContactProfiles` ["cath", "alice", "bob"] -testMergeContactExistingMember :: HasCallStack => FilePath -> IO () +testMergeContactExistingMember :: HasCallStack => TestParams -> IO () testMergeContactExistingMember = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -3739,7 +3727,7 @@ testMergeContactExistingMember = bob `hasContactProfiles` ["alice", "bob", "cath"] cath `hasContactProfiles` ["cath", "alice", "bob"] -testMergeContactMultipleMembers :: HasCallStack => FilePath -> IO () +testMergeContactMultipleMembers :: HasCallStack => TestParams -> IO () testMergeContactMultipleMembers = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -3777,9 +3765,9 @@ testMergeContactMultipleMembers = bob `hasContactProfiles` ["alice", "bob", "cath"] cath `hasContactProfiles` ["cath", "alice", "bob"] -testMergeGroupLinkHostMultipleContacts :: HasCallStack => FilePath -> IO () +testMergeGroupLinkHostMultipleContacts :: HasCallStack => TestParams -> IO () testMergeGroupLinkHostMultipleContacts = - testChatCfg2 testCfgGroupLinkViaContact bobProfile cathProfile $ + testChat2 bobProfile cathProfile $ \bob cath -> do connectUsers bob cath @@ -3804,16 +3792,14 @@ testMergeGroupLinkHostMultipleContacts = bob <## "cath_2 (Catherine): accepting request to join group #party..." concurrentlyN_ [ bob - <### [ "cath_2 (Catherine): contact is connected", - EndsWith "invited to group #party via your group link", - EndsWith "joined the group", - StartsWith "contact cath_2 is merged into cath", + <### [ EndsWith "joined the group", + "contact and member are merged: cath, #party cath_2", StartsWith "use @cath" ], cath - <### [ "bob_2 (Bob): contact is connected", + <### [ "#party: joining the group...", "#party: you joined the group", - StartsWith "contact bob_2 is merged into bob", + "contact and member are merged: bob, #party bob_2", StartsWith "use @bob" ] ] @@ -3826,7 +3812,7 @@ testMergeGroupLinkHostMultipleContacts = bob `hasContactProfiles` ["bob", "cath", "cath"] cath `hasContactProfiles` ["cath", "bob", "bob"] -testMemberContactMessage :: HasCallStack => FilePath -> IO () +testMemberContactMessage :: HasCallStack => TestParams -> IO () testMemberContactMessage = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -3851,11 +3837,26 @@ testMemberContactMessage = <### [ "#team alice is creating direct contact alice with you", WithTime "alice> hi" ] + bob <## "alice (Alice): you can send messages to contact" concurrently_ (alice <## "bob (Bob): contact is connected") (bob <## "alice (Alice): contact is connected") bob #$> ("/_get chat #1 count=1", chat, [(0, "started direct connection with you")]) + + -- exchanging messages will enable PQ (see Chat "TODO PQ" - perhaps connection should be negotiated with PQ on) + alice <##> bob + alice <##> bob + + alice `send` "@bob hi" + alice <## "bob: quantum resistant end-to-end encryption enabled" + alice <# "@bob hi" + bob <## "alice: quantum resistant end-to-end encryption enabled" + bob <# "alice> hi" + + bob #> "@alice hey" + alice <# "bob> hey" + alice <##> bob -- bob and cath connect @@ -3870,6 +3871,7 @@ testMemberContactMessage = <### [ "#team bob is creating direct contact bob with you", WithTime "bob> hi" ] + cath <## "bob (Bob): you can send messages to contact" concurrently_ (bob <## "cath (Catherine): contact is connected") (cath <## "bob (Bob): contact is connected") @@ -3877,7 +3879,7 @@ testMemberContactMessage = cath #$> ("/_get chat #1 count=1", chat, [(0, "started direct connection with you")]) bob <##> cath -testMemberContactNoMessage :: HasCallStack => FilePath -> IO () +testMemberContactNoMessage :: HasCallStack => TestParams -> IO () testMemberContactNoMessage = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -3890,6 +3892,7 @@ testMemberContactNoMessage = bob ##> "/_invite member contact @3" bob <## "sent invitation to connect directly to member #team cath" cath <## "#team bob is creating direct contact bob with you" + cath <## "bob (Bob): you can send messages to contact" concurrently_ (bob <## "cath (Catherine): contact is connected") (cath <## "bob (Bob): contact is connected") @@ -3897,7 +3900,7 @@ testMemberContactNoMessage = cath #$> ("/_get chat #1 count=1", chat, [(0, "started direct connection with you")]) bob <##> cath -testMemberContactProhibitedContactExists :: HasCallStack => FilePath -> IO () +testMemberContactProhibitedContactExists :: HasCallStack => TestParams -> IO () testMemberContactProhibitedContactExists = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -3910,7 +3913,7 @@ testMemberContactProhibitedContactExists = alice <# "@bob hi" bob <# "alice> hi" -testMemberContactProhibitedRepeatInv :: HasCallStack => FilePath -> IO () +testMemberContactProhibitedRepeatInv :: HasCallStack => TestParams -> IO () testMemberContactProhibitedRepeatInv = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -3930,17 +3933,18 @@ testMemberContactProhibitedRepeatInv = <### [ "#team bob is creating direct contact bob with you", WithTime "bob> hi" ] + cath <## "bob (Bob): you can send messages to contact" concurrently_ (bob <## "cath (Catherine): contact is connected") (cath <## "bob (Bob): contact is connected") bob <##> cath -testMemberContactInvitedConnectionReplaced :: HasCallStack => FilePath -> IO () -testMemberContactInvitedConnectionReplaced tmp = do - withNewTestChat tmp "alice" aliceProfile $ \alice -> do - withNewTestChat tmp "bob" bobProfile $ \bob -> do - withNewTestChat tmp "cath" cathProfile $ \cath -> do +testMemberContactInvitedConnectionReplaced :: HasCallStack => TestParams -> IO () +testMemberContactInvitedConnectionReplaced ps = do + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do + withNewTestChat ps "cath" cathProfile $ \cath -> do createGroup3 "team" alice bob cath alice ##> "/d bob" @@ -3959,6 +3963,7 @@ testMemberContactInvitedConnectionReplaced tmp = do WithTime "alice> hi", "alice: security code changed" ] + bob <## "alice (Alice): you can send messages to contact" concurrently_ (alice <## "bob (Bob): contact is connected") (bob <## "alice (Alice): contact is connected") @@ -3967,20 +3972,20 @@ testMemberContactInvitedConnectionReplaced tmp = do items <- chat <$> getTermLine bob items `shouldContain` [(0, "security code changed")] - withTestChat tmp "bob" $ \bob -> do + withTestChat ps "bob" $ \bob -> do subscriptions bob 1 checkConnectionsWork alice bob - withTestChat tmp "alice" $ \alice -> do + withTestChat ps "alice" $ \alice -> do subscriptions alice 2 - withTestChat tmp "bob" $ \bob -> do + withTestChat ps "bob" $ \bob -> do subscriptions bob 1 checkConnectionsWork alice bob - withTestChat tmp "cath" $ \cath -> do + withTestChat ps "cath" $ \cath -> do subscriptions cath 1 -- group messages work @@ -4006,11 +4011,12 @@ testMemberContactInvitedConnectionReplaced tmp = do alice @@@ [("@bob", "hey"), ("@cath", "sent invitation to join group team as admin"), ("#team", "connected")] bob @@@ [("@alice", "hey"), ("#team", "started direct connection with you")] -testMemberContactIncognito :: HasCallStack => FilePath -> IO () +testMemberContactIncognito :: HasCallStack => TestParams -> IO () testMemberContactIncognito = - testChatCfg3 testCfgGroupLinkViaContact aliceProfile bobProfile cathProfile $ + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do -- create group, bob joins incognito + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team <name> or /create link #team" @@ -4020,15 +4026,10 @@ testMemberContactIncognito = bobIncognito <- getTermLine bob bob <## "connection request sent incognito!" alice <## (bobIncognito <> ": accepting request to join group #team...") - _ <- getTermLine bob concurrentlyN_ - [ do - alice <## (bobIncognito <> ": contact is connected") - alice <## (bobIncognito <> " invited to group #team via your group link") - alice <## ("#team: " <> bobIncognito <> " joined the group"), + [ alice <## ("#team: " <> bobIncognito <> " joined the group"), do - bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito) - bob <## "use /i alice to print out this incognito profile again" + bob <## "#team: joining the group..." bob <## ("#team: you joined the group incognito as " <> bobIncognito) ] -- cath joins incognito @@ -4036,15 +4037,10 @@ testMemberContactIncognito = cathIncognito <- getTermLine cath cath <## "connection request sent incognito!" alice <## (cathIncognito <> ": accepting request to join group #team...") - _ <- getTermLine cath concurrentlyN_ - [ do - alice <## (cathIncognito <> ": contact is connected") - alice <## (cathIncognito <> " invited to group #team via your group link") - alice <## ("#team: " <> cathIncognito <> " joined the group"), + [ alice <## ("#team: " <> cathIncognito <> " joined the group"), do - cath <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> cathIncognito) - cath <## "use /i alice to print out this incognito profile again" + cath <## "#team: joining the group..." cath <## ("#team: you joined the group incognito as " <> cathIncognito) cath <## ("#team: member " <> bobIncognito <> " is connected"), do @@ -4068,6 +4064,7 @@ testMemberContactIncognito = <### [ ConsoleString ("#team " <> bobIncognito <> " is creating direct contact " <> bobIncognito <> " with you"), WithTime ("i " <> bobIncognito <> "> hi") ] + cath <## (bobIncognito <> ": you can send messages to contact") _ <- getTermLine bob _ <- getTermLine cath concurrentlyN_ @@ -4104,7 +4101,7 @@ testMemberContactIncognito = bob ?<# ("#team " <> cathIncognito <> "> hey") ] -testMemberContactProfileUpdate :: HasCallStack => FilePath -> IO () +testMemberContactProfileUpdate :: HasCallStack => TestParams -> IO () testMemberContactProfileUpdate = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -4124,12 +4121,10 @@ testMemberContactProfileUpdate = bob <# "#team alice> hello" cath <# "#team alice> hello" - bob #> "#team hello too" - alice <# "#team rob> hello too" - cath <# "#team bob> hello too" -- not updated profile - cath #> "#team hello there" - alice <# "#team kate> hello there" - bob <# "#team cath> hello there" -- not updated profile + alice `hasContactProfiles` ["alice", "rob", "kate"] + bob `hasContactProfiles` ["rob", "alice", "cath"] + cath `hasContactProfiles` ["kate", "alice", "bob"] + bob `send` "@cath hi" bob <### [ "member #team cath does not have direct connection, creating", @@ -4141,6 +4136,7 @@ testMemberContactProfileUpdate = <### [ "#team bob is creating direct contact bob with you", WithTime "bob> hi" ] + cath <## "bob (Bob): you can send messages to contact" concurrentlyN_ [ do bob <## "contact cath changed to kate (Kate)" @@ -4173,7 +4169,80 @@ testMemberContactProfileUpdate = alice <# "#team kate> hello there" bob <# "#team kate> hello there" -- updated profile -testGroupMsgForward :: HasCallStack => FilePath -> IO () +testRecreateMemberContactManyGroups :: HasCallStack => TestParams -> IO () +testRecreateMemberContactManyGroups = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + createGroup2' "team" alice bob False + createGroup2' "club" alice bob False + + -- alice can message bob via team and via club + alice ##> "@#team bob 1" + alice <# "@bob 1" + bob <# "alice> 1" + + bob ##> "@#team alice 2" + bob <# "@alice 2" + alice <# "bob> 2" + + alice ##> "@#club bob 3" + alice <# "@bob 3" + bob <# "alice> 3" + + bob ##> "@#club alice 4" + bob <# "@alice 4" + alice <# "bob> 4" + + -- alice deletes contact with bob + alice ##> "/d bob" + alice <## "bob: contact is deleted" + bob <## "alice (Alice) deleted contact with you" + + bob ##> "/d alice" + bob <## "alice: contact is deleted" + + -- group messages work + alice #> "#team hello" + bob <# "#team alice> hello" + bob #> "#team hi there" + alice <# "#team bob> hi there" + + -- alice creates member contact with bob + alice ##> "@#team bob hi" + alice + <### [ "member #team bob does not have direct connection, creating", + "contact for member #team bob is created", + "sent invitation to connect directly to member #team bob", + WithTime "@bob hi" + ] + bob + <### [ "#team alice is creating direct contact alice with you", + WithTime "alice> hi" + ] + bob <## "alice (Alice): you can send messages to contact" + concurrently_ + (alice <## "bob (Bob): contact is connected") + (bob <## "alice (Alice): contact is connected") + + -- alice can message bob via team and via club + alice ##> "@#team bob 1" + alice <# "@bob 1" + bob <# "alice> 1" + + bob ##> "@#team alice 2" + bob <# "@alice 2" + alice <# "bob> 2" + + alice ##> "@#club bob 3" + alice <# "@bob 3" + bob <# "alice> 3" + + bob ##> "@#club alice 4" + bob <# "@alice 4" + alice <# "bob> 4" + +testGroupMsgForward :: HasCallStack => TestParams -> IO () testGroupMsgForward = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -4201,6 +4270,64 @@ testGroupMsgForward = cath <# "#team bob> hi there [>>]" cath <# "#team hey team" +testGroupMsgForwardReport :: HasCallStack => TestParams -> IO () +testGroupMsgForwardReport = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + setupGroupForwarding3 "team" alice bob cath + + bob #> "#team hi there" + alice <# "#team bob> hi there" + cath <# "#team bob> hi there [>>]" + + alice ##> "/mr team bob moderator" + concurrentlyN_ + [ alice <## "#team: you changed the role of bob to moderator", + bob <## "#team: alice changed your role from admin to moderator", + cath <## "#team: alice changed the role of bob from admin to moderator" + ] + + alice ##> "/mr team cath member" + concurrentlyN_ + [ alice <## "#team: you changed the role of cath to member", + bob <## "#team: alice changed the role of cath from admin to member", + cath <## "#team: alice changed your role from admin to member" + ] + cath ##> "/report #team content hi there" + cath <# "#team > bob hi there" + cath <## " report content" + concurrentlyN_ + [ do + alice <# "#team cath> > bob hi there" + alice <## " report content", + do + bob <# "#team cath!> > bob hi there [>>]" + bob <## " report content [>>]" + ] + + alice ##> "/mr team bob member" + concurrentlyN_ + [ alice <## "#team: you changed the role of bob to member", + bob <## "#team: alice changed your role from moderator to member", + cath <## "#team: alice changed the role of bob from moderator to member" + ] + + cath ##> "/report #team content hi there" + cath <# "#team > bob hi there" + cath <## " report content" + concurrentlyN_ + [ do + alice <# "#team cath> > bob hi there" + alice <## " report content", + (bob </) + ] + + -- regular messages are still forwarded + + cath #> "#team hey team" + alice <# "#team cath> hey team" + bob <# "#team cath> hey team [>>]" + setupGroupForwarding3 :: String -> TestCC -> TestCC -> TestCC -> IO () setupGroupForwarding3 gName alice bob cath = do createGroup3 gName alice bob cath @@ -4213,7 +4340,7 @@ setupGroupForwarding3 gName alice bob cath = do void $ withCCTransaction alice $ \db -> DB.execute_ db "UPDATE group_member_intros SET intro_status='fwd'" -testGroupMsgForwardDeduplicate :: HasCallStack => FilePath -> IO () +testGroupMsgForwardDeduplicate :: HasCallStack => TestParams -> IO () testGroupMsgForwardDeduplicate = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -4250,7 +4377,7 @@ testGroupMsgForwardDeduplicate = cath <#. "#team bob> hi there" cath <# "#team hey team" -testGroupMsgForwardEdit :: HasCallStack => FilePath -> IO () +testGroupMsgForwardEdit :: HasCallStack => TestParams -> IO () testGroupMsgForwardEdit = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -4273,7 +4400,7 @@ testGroupMsgForwardEdit = cath ##> "/tail #team 1" cath <# "#team bob> hello there [>>]" -testGroupMsgForwardReaction :: HasCallStack => FilePath -> IO () +testGroupMsgForwardReaction :: HasCallStack => TestParams -> IO () testGroupMsgForwardReaction = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -4290,11 +4417,12 @@ testGroupMsgForwardReaction = bob <# "#team cath> > bob hi there" bob <## " + 👍" -testGroupMsgForwardDeletion :: HasCallStack => FilePath -> IO () +testGroupMsgForwardDeletion :: HasCallStack => TestParams -> IO () testGroupMsgForwardDeletion = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do setupGroupForwarding3 "team" alice bob cath + -- disableFullDeletion3 "team" alice bob cath bob #> "#team hi there" alice <# "#team bob> hi there" @@ -4305,9 +4433,9 @@ testGroupMsgForwardDeletion = alice <# "#team bob> [marked deleted] hi there" cath <# "#team bob> [marked deleted] hi there" -- TODO show as forwarded -testGroupMsgForwardFile :: HasCallStack => FilePath -> IO () +testGroupMsgForwardFile :: HasCallStack => TestParams -> IO () testGroupMsgForwardFile = - testChatCfg3 cfg aliceProfile bobProfile cathProfile $ + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> withXFTPServer $ do setupGroupForwarding3 "team" alice bob cath @@ -4329,21 +4457,19 @@ testGroupMsgForwardFile = src <- B.readFile "./tests/fixtures/test.jpg" dest <- B.readFile "./tests/tmp/test.jpg" dest `shouldBe` src - where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} -testGroupMsgForwardChangeRole :: HasCallStack => FilePath -> IO () +testGroupMsgForwardChangeRole :: HasCallStack => TestParams -> IO () testGroupMsgForwardChangeRole = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do setupGroupForwarding3 "team" alice bob cath cath ##> "/mr #team bob member" - cath <## "#team: you changed the role of bob from admin to member" + cath <## "#team: you changed the role of bob to member" alice <## "#team: cath changed the role of bob from admin to member" bob <## "#team: cath changed your role from admin to member" -- TODO show as forwarded -testGroupMsgForwardNewMember :: HasCallStack => FilePath -> IO () +testGroupMsgForwardNewMember :: HasCallStack => TestParams -> IO () testGroupMsgForwardNewMember = testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do @@ -4384,7 +4510,19 @@ testGroupMsgForwardNewMember = "dan (Daniel): member" ] -testGroupHistory :: HasCallStack => FilePath -> IO () +testGroupMsgForwardLeave :: HasCallStack => TestParams -> IO () +testGroupMsgForwardLeave = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + setupGroupForwarding3 "team" alice bob cath + + bob ##> "/leave #team" + bob <## "#team: you left the group" + bob <## "use /d #team to delete the group" + alice <## "#team: bob left the group" + cath <## "#team: bob left the group" + +testGroupHistory :: HasCallStack => TestParams -> IO () testGroupHistory = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -4428,7 +4566,7 @@ testGroupHistory = cath #> "#team 3" [alice, bob] *<# "#team cath> 3" -testGroupHistoryGroupLink :: HasCallStack => FilePath -> IO () +testGroupHistoryGroupLink :: HasCallStack => TestParams -> IO () testGroupHistoryGroupLink = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -4476,7 +4614,7 @@ testGroupHistoryGroupLink = cath #> "#team 3" [alice, bob] *<# "#team cath> 3" -testGroupHistoryPreferenceOff :: HasCallStack => FilePath -> IO () +testGroupHistoryPreferenceOff :: HasCallStack => TestParams -> IO () testGroupHistoryPreferenceOff = testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do @@ -4561,9 +4699,9 @@ testGroupHistoryPreferenceOff = cc <## "#team: alice added dan (Daniel) to the group (connecting...)" cc <## "#team: new member dan is connected" -testGroupHistoryHostFile :: HasCallStack => FilePath -> IO () +testGroupHistoryHostFile :: HasCallStack => TestParams -> IO () testGroupHistoryHostFile = - testChatCfg3 cfg aliceProfile bobProfile cathProfile $ + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> withXFTPServer $ do createGroup2 "team" alice bob @@ -4599,12 +4737,10 @@ testGroupHistoryHostFile = src <- B.readFile "./tests/fixtures/test.jpg" dest <- B.readFile "./tests/tmp/test.jpg" dest `shouldBe` src - where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} -testGroupHistoryMemberFile :: HasCallStack => FilePath -> IO () +testGroupHistoryMemberFile :: HasCallStack => TestParams -> IO () testGroupHistoryMemberFile = - testChatCfg3 cfg aliceProfile bobProfile cathProfile $ + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> withXFTPServer $ do createGroup2 "team" alice bob @@ -4640,10 +4776,8 @@ testGroupHistoryMemberFile = src <- B.readFile "./tests/fixtures/test.jpg" dest <- B.readFile "./tests/tmp/test.jpg" dest `shouldBe` src - where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} -testGroupHistoryLargeFile :: HasCallStack => FilePath -> IO () +testGroupHistoryLargeFile :: HasCallStack => TestParams -> IO () testGroupHistoryLargeFile = testChatCfg3 cfg aliceProfile bobProfile cathProfile $ \alice bob cath -> withXFTPServer $ do @@ -4651,7 +4785,7 @@ testGroupHistoryLargeFile = createGroup2 "team" alice bob - bob ##> "/_send #1 json {\"filePath\": \"./tests/tmp/testfile\", \"msgContent\": {\"text\":\"hello\",\"type\":\"file\"}}" + bob ##> "/_send #1 json [{\"filePath\": \"./tests/tmp/testfile\", \"msgContent\": {\"text\":\"hello\",\"type\":\"file\"}}]" bob <# "#team hello" bob <# "/f #team ./tests/tmp/testfile" bob <## "use /fc 1 to cancel sending" @@ -4699,11 +4833,11 @@ testGroupHistoryLargeFile = destCath <- B.readFile "./tests/tmp/testfile_2" destCath `shouldBe` src where - cfg = testCfg {xftpDescrPartSize = 200, xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} + cfg = testCfg {xftpDescrPartSize = 200} -testGroupHistoryMultipleFiles :: HasCallStack => FilePath -> IO () +testGroupHistoryMultipleFiles :: HasCallStack => TestParams -> IO () testGroupHistoryMultipleFiles = - testChatCfg3 cfg aliceProfile bobProfile cathProfile $ + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> withXFTPServer $ do xftpCLI ["rand", "./tests/tmp/testfile_bob", "2mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_bob"] xftpCLI ["rand", "./tests/tmp/testfile_alice", "1mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_alice"] @@ -4712,7 +4846,7 @@ testGroupHistoryMultipleFiles = threadDelay 1000000 - bob ##> "/_send #1 json {\"filePath\": \"./tests/tmp/testfile_bob\", \"msgContent\": {\"text\":\"hi alice\",\"type\":\"file\"}}" + bob ##> "/_send #1 json [{\"filePath\": \"./tests/tmp/testfile_bob\", \"msgContent\": {\"text\":\"hi alice\",\"type\":\"file\"}}]" bob <# "#team hi alice" bob <# "/f #team ./tests/tmp/testfile_bob" bob <## "use /fc 1 to cancel sending" @@ -4724,7 +4858,7 @@ testGroupHistoryMultipleFiles = threadDelay 1000000 - alice ##> "/_send #1 json {\"filePath\": \"./tests/tmp/testfile_alice\", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"file\"}}" + alice ##> "/_send #1 json [{\"filePath\": \"./tests/tmp/testfile_alice\", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"file\"}}]" alice <# "#team hey bob" alice <# "/f #team ./tests/tmp/testfile_alice" alice <## "use /fc 2 to cancel sending" @@ -4780,19 +4914,17 @@ testGroupHistoryMultipleFiles = `shouldContain` [ ((0, "hi alice"), Just "./tests/tmp/testfile_bob_1"), ((0, "hey bob"), Just "./tests/tmp/testfile_alice_1") ] - where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} -testGroupHistoryFileCancel :: HasCallStack => FilePath -> IO () +testGroupHistoryFileCancel :: HasCallStack => TestParams -> IO () testGroupHistoryFileCancel = - testChatCfg3 cfg aliceProfile bobProfile cathProfile $ + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> withXFTPServer $ do xftpCLI ["rand", "./tests/tmp/testfile_bob", "2mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_bob"] xftpCLI ["rand", "./tests/tmp/testfile_alice", "1mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_alice"] createGroup2 "team" alice bob - bob ##> "/_send #1 json {\"filePath\": \"./tests/tmp/testfile_bob\", \"msgContent\": {\"text\":\"hi alice\",\"type\":\"file\"}}" + bob ##> "/_send #1 json [{\"filePath\": \"./tests/tmp/testfile_bob\", \"msgContent\": {\"text\":\"hi alice\",\"type\":\"file\"}}]" bob <# "#team hi alice" bob <# "/f #team ./tests/tmp/testfile_bob" bob <## "use /fc 1 to cancel sending" @@ -4808,7 +4940,7 @@ testGroupHistoryFileCancel = threadDelay 1000000 - alice ##> "/_send #1 json {\"filePath\": \"./tests/tmp/testfile_alice\", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"file\"}}" + alice ##> "/_send #1 json [{\"filePath\": \"./tests/tmp/testfile_alice\", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"file\"}}]" alice <# "#team hey bob" alice <# "/f #team ./tests/tmp/testfile_alice" alice <## "use /fc 2 to cancel sending" @@ -4837,12 +4969,10 @@ testGroupHistoryFileCancel = bob <## "#team: alice added cath (Catherine) to the group (connecting...)" bob <## "#team: new member cath is connected" ] - where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} -testGroupHistoryFileCancelNoText :: HasCallStack => FilePath -> IO () +testGroupHistoryFileCancelNoText :: HasCallStack => TestParams -> IO () testGroupHistoryFileCancelNoText = - testChatCfg3 cfg aliceProfile bobProfile cathProfile $ + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> withXFTPServer $ do xftpCLI ["rand", "./tests/tmp/testfile_bob", "2mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_bob"] xftpCLI ["rand", "./tests/tmp/testfile_alice", "1mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_alice"] @@ -4898,10 +5028,8 @@ testGroupHistoryFileCancelNoText = bob <## "#team: alice added cath (Catherine) to the group (connecting...)" bob <## "#team: new member cath is connected" ] - where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} -testGroupHistoryQuotes :: HasCallStack => FilePath -> IO () +testGroupHistoryQuotes :: HasCallStack => TestParams -> IO () testGroupHistoryQuotes = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -4930,7 +5058,7 @@ testGroupHistoryQuotes = alice `send` "> #team @bob (BOB) 2" alice <# "#team > bob BOB" alice <## " 2" - bob <# "#team alice> > bob BOB" + bob <# "#team alice!> > bob BOB" bob <## " 2" threadDelay 1000000 @@ -4938,7 +5066,7 @@ testGroupHistoryQuotes = bob `send` "> #team @alice (ALICE) 3" bob <# "#team > alice ALICE" bob <## " 3" - alice <# "#team bob> > alice ALICE" + alice <# "#team bob!> > alice ALICE" alice <## " 3" threadDelay 1000000 @@ -5007,11 +5135,12 @@ testGroupHistoryQuotes = ((0, "4"), Just (0, "BOB")) ] -testGroupHistoryDeletedMessage :: HasCallStack => FilePath -> IO () +testGroupHistoryDeletedMessage :: HasCallStack => TestParams -> IO () testGroupHistoryDeletedMessage = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do createGroup2 "team" alice bob + -- disableFullDeletion2 "team" alice bob alice #> "#team hello" bob <# "#team alice> hello" @@ -5045,7 +5174,7 @@ testGroupHistoryDeletedMessage = r `shouldContain` [(0, "hello")] r `shouldNotContain` [(0, "hey!")] -testGroupHistoryDisappearingMessage :: HasCallStack => FilePath -> IO () +testGroupHistoryDisappearingMessage :: HasCallStack => TestParams -> IO () testGroupHistoryDisappearingMessage = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -5059,12 +5188,12 @@ testGroupHistoryDisappearingMessage = threadDelay 1000000 -- 3 seconds so that messages 2 and 3 are not deleted for alice before sending history to cath - alice ##> "/set disappear #team on 3" + alice ##> "/set disappear #team on 4" alice <## "updated group preferences:" - alice <## "Disappearing messages: on (3 sec)" + alice <## "Disappearing messages: on (4 sec)" bob <## "alice updated group #team:" bob <## "updated group preferences:" - bob <## "Disappearing messages: on (3 sec)" + bob <## "Disappearing messages: on (4 sec)" bob #> "#team 2" alice <# "#team bob> 2" @@ -5109,15 +5238,18 @@ testGroupHistoryDisappearingMessage = r1 `shouldContain` [(0, "1"), (0, "2"), (0, "3"), (0, "4")] concurrentlyN_ - [ do - alice <## "timed message deleted: 2" - alice <## "timed message deleted: 3", - do - bob <## "timed message deleted: 2" - bob <## "timed message deleted: 3", - do - cath <## "timed message deleted: 2" - cath <## "timed message deleted: 3" + [ alice + <### [ "timed message deleted: 2", + "timed message deleted: 3" + ], + bob + <### [ "timed message deleted: 2", + "timed message deleted: 3" + ], + cath + <### [ "timed message deleted: 2", + "timed message deleted: 3" + ] ] cath ##> "/_get chat #1 count=100" @@ -5125,3 +5257,1392 @@ testGroupHistoryDisappearingMessage = r2 `shouldContain` [(0, "1"), (0, "4")] r2 `shouldNotContain` [(0, "2")] r2 `shouldNotContain` [(0, "3")] + +testGroupHistoryWelcomeMessage :: HasCallStack => TestParams -> IO () +testGroupHistoryWelcomeMessage = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup2 "team" alice bob + + alice ##> "/set welcome #team welcome to team" + alice <## "description changed to:" + alice <## "welcome to team" + + bob <## "alice updated group #team:" + bob <## "description changed to:" + bob <## "welcome to team" + + threadDelay 1000000 + + alice #> "#team hello" + bob <# "#team alice> hello" + + threadDelay 1000000 + + bob #> "#team hey!" + alice <# "#team bob> hey!" + + connectUsers alice cath + addMember "team" alice cath GRAdmin + cath ##> "/j team" + concurrentlyN_ + [ alice <## "#team: cath joined the group", + cath + <### [ "#team: you joined the group", + WithTime "#team alice> hello [>>]", + WithTime "#team bob> hey! [>>]", + WithTime "#team alice> welcome to team", + "#team: member bob (Bob) is connected" + ], + do + bob <## "#team: alice added cath (Catherine) to the group (connecting...)" + bob <## "#team: new member cath is connected" + ] + + cath ##> "/_get chat #1 count=100" + r <- chat <$> getTermLine cath + -- sometimes there are "connected" and feature items in between, + -- so we filter them out; `shouldContain` then checks order is correct + let expected = [(0, "hello"), (0, "hey!"), (0, "welcome to team")] + r' = filter (`elem` expected) r + r' `shouldContain` expected + + -- message delivery works after sending history + alice #> "#team 1" + [bob, cath] *<# "#team alice> 1" + bob #> "#team 2" + [alice, cath] *<# "#team bob> 2" + cath #> "#team 3" + [alice, bob] *<# "#team cath> 3" + +testGroupHistoryUnknownMember :: HasCallStack => TestParams -> IO () +testGroupHistoryUnknownMember = + testChat4 aliceProfile bobProfile cathProfile danProfile $ + \alice bob cath dan -> do + createGroup3 "team" alice bob cath + + threadDelay 1000000 + + alice #> "#team hi from alice" + [bob, cath] *<# "#team alice> hi from alice" + + threadDelay 1000000 + + bob #> "#team hi from bob" + [alice, cath] *<# "#team bob> hi from bob" + + threadDelay 1000000 + + cath #> "#team hi from cath" + [alice, bob] *<# "#team cath> hi from cath" + + 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", + cath <## "#team: bob left the group" + ] + + connectUsers alice dan + addMember "team" alice dan GRAdmin + dan ##> "/j team" + concurrentlyN_ + [ alice <## "#team: dan joined the group", + dan + <### [ "#team: you joined the group", + WithTime "#team alice> hi from alice [>>]", + StartsWith "#team: alice forwarded a message from an unknown member, creating unknown member record", + EndsWith "hi from bob [>>]", + WithTime "#team cath> hi from cath [>>]", + "#team: member cath (Catherine) is connected" + ], + do + cath <## "#team: alice added dan (Daniel) to the group (connecting...)" + cath <## "#team: new member dan is connected" + ] + + dan ##> "/_get chat #1 count=100" + r <- chat <$> getTermLine dan + r `shouldContain` [(0, "hi from alice"), (0, "hi from bob"), (0, "hi from cath")] + + dan ##> "/ms team" + dan + <### [ "dan (Daniel): admin, you, connected", + "alice (Alice): owner, host, connected", + "cath (Catherine): admin, connected", + EndsWith "author, status unknown" + ] + + -- message delivery works after sending history + alice #> "#team 1" + [cath, dan] *<# "#team alice> 1" + cath #> "#team 2" + [alice, dan] *<# "#team cath> 2" + dan #> "#team 3" + [alice, cath] *<# "#team dan> 3" + +testMembershipProfileUpdateNextGroupMessage :: HasCallStack => TestParams -> IO () +testMembershipProfileUpdateNextGroupMessage = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + -- create group 1 + threadDelay 100000 + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team <name> or /create link #team" + alice ##> "/create link #team" + gLinkTeam <- getGroupLink alice "team" GRMember True + bob ##> ("/c " <> gLinkTeam) + bob <## "connection request sent!" + alice <## "bob (Bob): accepting request to join group #team..." + concurrentlyN_ + [ alice <## "#team: bob joined the group", + do + bob <## "#team: joining the group..." + bob <## "#team: you joined the group" + ] + + -- create group 2 + alice ##> "/g club" + alice <## "group #club is created" + alice <## "to add members use /a club <name> or /create link #club" + alice ##> "/create link #club" + gLinkClub <- getGroupLink alice "club" GRMember True + cath ##> ("/c " <> gLinkClub) + cath <## "connection request sent!" + alice <## "cath (Catherine): accepting request to join group #club..." + concurrentlyN_ + [ alice <## "#club: cath joined the group", + do + cath <## "#club: joining the group..." + cath <## "#club: you joined the group" + ] + + -- alice has no contacts + alice ##> "/contacts" + + alice #> "#team hello team" + bob <# "#team alice> hello team" + + alice #> "#club hello club" + cath <# "#club alice> hello club" + + alice ##> "/p alisa" + alice <## "user profile is changed to alisa (your 0 contacts are notified)" + + -- update profile in group 1 + + bob ##> "/ms team" + bob + <### [ "bob (Bob): member, you, connected", + "alice (Alice): owner, host, connected" + ] + + alice #> "#team team 1" + bob <# "#team alisa> team 1" + cath <// 50000 + + bob ##> "/ms team" + bob + <### [ "bob (Bob): member, you, connected", + "alisa: owner, host, connected" + ] + + alice #> "#team team 2" + bob <# "#team alisa> team 2" + + bob ##> "/_get chat #1 count=100" + rb <- chat <$> getTermLine bob + rb `shouldContain` [(0, "updated profile")] + + -- update profile in group 2 + + cath ##> "/ms club" + cath + <### [ "cath (Catherine): member, you, connected", + "alice (Alice): owner, host, connected" + ] + + alice #> "#club club 1" + cath <# "#club alisa> club 1" + + cath ##> "/ms club" + cath + <### [ "cath (Catherine): member, you, connected", + "alisa: owner, host, connected" + ] + + alice #> "#club club 2" + cath <# "#club alisa> club 2" + + cath ##> "/_get chat #1 count=100" + rc <- chat <$> getTermLine cath + rc `shouldContain` [(0, "updated profile")] + +testMembershipProfileUpdateSameMember :: HasCallStack => TestParams -> IO () +testMembershipProfileUpdateSameMember = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + createGroup2 "team" alice bob + createGroup2' "club" alice bob False + + alice ##> "/d bob" + alice <## "bob: contact is deleted" + bob <## "alice (Alice) deleted contact with you" + + alice ##> "/p alisa" + alice <## "user profile is changed to alisa (your 0 contacts are notified)" + + bob `hasContactProfiles` ["alice", "bob"] + + alice #> "#team team 1" + bob <## "contact alice changed to alisa" + bob <## "use @alisa <message> to send messages" + bob <# "#team alisa> team 1" + + -- since members were related to the same contact, both member records are updated + bob `hasContactProfiles` ["alisa", "bob"] + checkMembers bob + checkItems bob + + -- profile update is not processed in second group, since it hasn't changed + alice #> "#club club 1" + bob <# "#club alisa> club 1" + + bob `hasContactProfiles` ["alisa", "bob"] + checkMembers bob + checkItems bob + where + checkMembers bob = do + bob ##> "/ms team" + bob + <### [ "bob (Bob): admin, you, connected", + "alisa: owner, host, connected" + ] + bob ##> "/ms club" + bob + <### [ "bob (Bob): admin, you, connected", + "alisa: owner, host, connected" + ] + checkItems bob = do + bob ##> "/_get chat @2 count=100" + rCt <- chat <$> getTermLine bob + rCt `shouldNotContain` [(0, "updated profile")] + + bob ##> "/_get chat #1 count=100" + rTeam <- chat <$> getTermLine bob + rTeam `shouldContain` [(0, "updated profile")] + + bob ##> "/_get chat #2 count=100" + rClub <- chat <$> getTermLine bob + rClub `shouldNotContain` [(0, "updated profile")] + +testMembershipProfileUpdateContactActive :: HasCallStack => TestParams -> IO () +testMembershipProfileUpdateContactActive = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + createGroup2 "team" alice bob + + alice ##> "/contacts" + alice <## "bob (Bob)" + + alice #> "#team hello team" + bob <# "#team alice> hello team" + + alice ##> "/p alisa" + alice <## "user profile is changed to alisa (your 1 contacts are notified)" + bob <## "contact alice changed to alisa" + bob <## "use @alisa <message> to send messages" + + bob `hasContactProfiles` ["alisa", "bob"] + + alice #> "#team team 1" + bob <# "#team alisa> team 1" + + bob `hasContactProfiles` ["alisa", "bob"] + + checkItems bob + + alice ##> "/ad" + cLink <- getContactLink alice True + alice ##> "/pa on" + alice <## "new contact address set" + bob <## "alisa set new contact address, use /info alisa to view" + + bob `hasContactProfiles` ["alisa", "bob"] + checkAliceProfileLink bob "alisa" cLink + + -- profile update does not remove contact address from profile + alice ##> "/p 'Alice Smith'" + alice <## "user profile is changed to 'Alice Smith' (your 1 contacts are notified)" + bob <## "contact alisa changed to 'Alice Smith'" + bob <## "use @'Alice Smith' <message> to send messages" + + bob `hasContactProfiles` ["Alice Smith", "bob"] + checkAliceProfileLink bob "'Alice Smith'" cLink + + -- receiving group message does not remove contact address from profile + alice #> "#team team 2" + bob <# "#team 'Alice Smith'> team 2" + + bob `hasContactProfiles` ["Alice Smith", "bob"] + checkAliceProfileLink bob "'Alice Smith'" cLink + + checkItems bob + where + checkItems bob = do + bob ##> "/_get chat @2 count=100" + rCt <- chat <$> getTermLine bob + rCt `shouldContain` [(0, "updated profile")] + + bob ##> "/_get chat #1 count=100" + rGrp <- chat <$> getTermLine bob + rGrp `shouldNotContain` [(0, "updated profile")] + checkAliceProfileLink bob name cLink = do + bob ##> ("/info #team " <> name) + bob <## "group ID: 1" + bob <## "member ID: 1" + bob <##. "receiving messages via" + bob <##. "sending messages via" + bob <## ("contact address: " <> cLink) + bob <## "connection not verified, use /code command to see security code" + bob <## currentChatVRangeInfo + +testMembershipProfileUpdateContactDeleted :: HasCallStack => TestParams -> IO () +testMembershipProfileUpdateContactDeleted = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + createGroup2 "team" alice bob + + alice ##> "/contacts" + alice <## "bob (Bob)" + + alice #> "#team hello team" + bob <# "#team alice> hello team" + + alice ##> "/d bob" + alice <## "bob: contact is deleted" + bob <## "alice (Alice) deleted contact with you" + + alice ##> "/p alisa" + alice <## "user profile is changed to alisa (your 0 contacts are notified)" + + bob `hasContactProfiles` ["alice", "bob"] + + alice #> "#team team 1" + bob <## "contact alice changed to alisa" + bob <## "use @alisa <message> to send messages" + bob <# "#team alisa> team 1" + + bob `hasContactProfiles` ["alisa", "bob"] + + checkItems bob + + -- adding contact address to profile does not share it with member + alice ##> "/ad" + _ <- getContactLink alice True + alice ##> "/pa on" + alice <## "new contact address set" + + bob `hasContactProfiles` ["alisa", "bob"] + checkAliceNoProfileLink bob "alisa" + + alice #> "#team team 2" + bob <# "#team alisa> team 2" + + bob `hasContactProfiles` ["alisa", "bob"] + checkAliceNoProfileLink bob "alisa" + + -- profile update does not add contact address to member profile + alice ##> "/p 'Alice Smith'" + alice <## "user profile is changed to 'Alice Smith' (your 0 contacts are notified)" + + bob `hasContactProfiles` ["alisa", "bob"] + checkAliceNoProfileLink bob "alisa" + + alice #> "#team team 3" + bob <## "contact alisa changed to 'Alice Smith'" + bob <## "use @'Alice Smith' <message> to send messages" + bob <# "#team 'Alice Smith'> team 3" + + bob `hasContactProfiles` ["Alice Smith", "bob"] + checkAliceNoProfileLink bob "'Alice Smith'" + + checkItems bob + where + checkItems bob = do + bob ##> "/_get chat @2 count=100" + rCt <- chat <$> getTermLine bob + rCt `shouldNotContain` [(0, "updated profile")] + + bob ##> "/_get chat #1 count=100" + rGrp <- chat <$> getTermLine bob + rGrp `shouldContain` [(0, "updated profile")] + checkAliceNoProfileLink bob name = do + bob ##> ("/info #team " <> name) + bob <## "group ID: 1" + bob <## "member ID: 1" + bob <##. "receiving messages via" + bob <##. "sending messages via" + bob <## "connection not verified, use /code command to see security code" + bob <## currentChatVRangeInfo + +testMembershipProfileUpdateContactDisabled :: HasCallStack => TestParams -> IO () +testMembershipProfileUpdateContactDisabled = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + createGroup2 "team" alice bob + + alice ##> "/contacts" + alice <## "bob (Bob)" + + alice #> "#team hello team" + bob <# "#team alice> hello team" + + alice ##> "/_delete @2 notify=off" + alice <## "bob: contact is deleted" + + alice ##> "/p alisa" + alice <## "user profile is changed to alisa (your 0 contacts are notified)" + + bob `hasContactProfiles` ["alice", "bob"] + + -- bob expects update from contact, so he doesn't update profile + alice #> "#team team 1" + bob <# "#team alice> team 1" + + bob `hasContactProfiles` ["alice", "bob"] + + -- bob sends any message to alice, increases auth err counter + bob `send` "/feed hi all" + bob <##. "/feed (1)" + bob <## "[alice, contactId: 2, connId: 1] error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection" + + -- on next profile update from alice member, bob considers contact disabled for purposes of profile update + alice #> "#team team 2" + bob <# "#team alice> team 2" + + bob `hasContactProfiles` ["alice", "bob"] + + alice ##> "/p 'Alice Smith'" + alice <## "user profile is changed to 'Alice Smith' (your 0 contacts are notified)" + + alice #> "#team team 3" + bob <## "contact alice changed to 'Alice Smith'" + bob <## "use @'Alice Smith' <message> to send messages" + bob <# "#team 'Alice Smith'> team 3" + + bob `hasContactProfiles` ["Alice Smith", "bob"] + + bob ##> "/_get chat @2 count=100" + rCt <- chat <$> getTermLine bob + rCt `shouldNotContain` [(0, "updated profile")] + + bob ##> "/_get chat #1 count=100" + rGrp <- chat <$> getTermLine bob + rGrp `shouldContain` [(0, "updated profile")] + +testMembershipProfileUpdateNoChangeIgnored :: HasCallStack => TestParams -> IO () +testMembershipProfileUpdateNoChangeIgnored = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + createGroup2 "team" alice bob + + alice ##> "/contacts" + alice <## "bob (Bob)" + + alice #> "#team hello team" + bob <# "#team alice> hello team" + + alice ##> "/d bob" + alice <## "bob: contact is deleted" + bob <## "alice (Alice) deleted contact with you" + + alice ##> "/p alisa" + alice <## "user profile is changed to alisa (your 0 contacts are notified)" + + bob `hasContactProfiles` ["alice", "bob"] + + alice ##> "/p alice Alice" + alice <## "user profile is changed to alice (Alice) (your 0 contacts are notified)" + + bob `hasContactProfiles` ["alice", "bob"] + + alice #> "#team team 1" + bob <# "#team alice> team 1" + + bob `hasContactProfiles` ["alice", "bob"] + + bob ##> "/_get chat @2 count=100" + rCt <- chat <$> getTermLine bob + rCt `shouldNotContain` [(0, "updated profile")] + + bob ##> "/_get chat #1 count=100" + rGrp <- chat <$> getTermLine bob + rGrp `shouldNotContain` [(0, "updated profile")] + +testMembershipProfileUpdateContactLinkIgnored :: HasCallStack => TestParams -> IO () +testMembershipProfileUpdateContactLinkIgnored = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + createGroup2 "team" alice bob + + alice ##> "/contacts" + alice <## "bob (Bob)" + + alice #> "#team hello team" + bob <# "#team alice> hello team" + + alice ##> "/d bob" + alice <## "bob: contact is deleted" + bob <## "alice (Alice) deleted contact with you" + + alice ##> "/ad" + _ <- getContactLink alice True + alice ##> "/pa on" + alice <## "new contact address set" + + bob `hasContactProfiles` ["alice", "bob"] + + alice #> "#team team 1" + bob <# "#team alice> team 1" + + bob ##> "/_get chat @2 count=100" + rCt <- chat <$> getTermLine bob + rCt `shouldNotContain` [(0, "updated profile")] + + bob ##> "/_get chat #1 count=100" + rGrp <- chat <$> getTermLine bob + rGrp `shouldNotContain` [(0, "updated profile")] + + bob ##> "/info #team alice" + bob <## "group ID: 1" + bob <## "member ID: 1" + bob <##. "receiving messages via" + bob <##. "sending messages via" + bob <## "connection not verified, use /code command to see security code" + bob <## currentChatVRangeInfo + +testBlockForAllMarkedBlocked :: HasCallStack => TestParams -> IO () +testBlockForAllMarkedBlocked = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + -- disableFullDeletion3 "team" alice bob cath + + threadDelay 1000000 + + bob #> "#team 1" + [alice, cath] *<# "#team bob> 1" + + threadDelay 1000000 + + alice ##> "/block for all #team bob" + alice <## "#team: you blocked bob" + cath <## "#team: alice blocked bob" + bob <// 50000 + + alice ##> "/ms team" + alice + <### [ "alice (Alice): owner, you, created group", + "bob (Bob): admin, invited, connected, blocked by admin", + "cath (Catherine): admin, invited, connected" + ] + + cath ##> "/ms team" + cath + <### [ "cath (Catherine): admin, you, connected", + "alice (Alice): owner, host, connected", + "bob (Bob): admin, connected, blocked by admin" + ] + + bob ##> "/ms team" + bob + <### [ "bob (Bob): admin, you, connected", + "alice (Alice): owner, host, connected", + "cath (Catherine): admin, connected" + ] + + threadDelay 1000000 + + bob #> "#team 2" + alice <# "#team bob> 2 [blocked by admin] <muted>" + cath <# "#team bob> 2 [blocked by admin] <muted>" + + threadDelay 1000000 + + bob #> "#team 3" + alice <# "#team bob> 3 [blocked by admin] <muted>" + cath <# "#team bob> 3 [blocked by admin] <muted>" + + threadDelay 1000000 + + alice ##> "/unblock for all #team bob" + alice <## "#team: you unblocked bob" + cath <## "#team: alice unblocked bob" + bob <// 50000 + + threadDelay 1000000 + + bob #> "#team 4" + [alice, cath] *<# "#team bob> 4" + + alice + #$> ( "/_get chat #1 count=6", + chat, + [ (0, "1"), + (1, "blocked bob (Bob)"), + (0, "2 [blocked by admin]"), + (0, "3 [blocked by admin]"), + (1, "unblocked bob (Bob)"), + (0, "4") + ] + ) + cath + #$> ( "/_get chat #1 count=6", + chat, + [ (0, "1"), + (0, "blocked bob (Bob)"), + (0, "2 [blocked by admin]"), + (0, "3 [blocked by admin]"), + (0, "unblocked bob (Bob)"), + (0, "4") + ] + ) + bob #$> ("/_get chat #1 count=4", chat, [(1, "1"), (1, "2"), (1, "3"), (1, "4")]) + +testBlockForAllFullDelete :: HasCallStack => TestParams -> IO () +testBlockForAllFullDelete = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + -- disableFullDeletion3 "team" alice bob cath + + alice ##> "/set delete #team on" + alice <## "updated group preferences:" + alice <## "Full deletion: on" + concurrentlyN_ + [ do + bob <## "alice updated group #team:" + bob <## "updated group preferences:" + bob <## "Full deletion: on", + do + cath <## "alice updated group #team:" + cath <## "updated group preferences:" + cath <## "Full deletion: on" + ] + + threadDelay 1000000 + + bob #> "#team 1" + [alice, cath] *<# "#team bob> 1" + + threadDelay 1000000 + + alice ##> "/block for all #team bob" + alice <## "#team: you blocked bob" + cath <## "#team: alice blocked bob" + bob <// 50000 + + threadDelay 1000000 + + bob #> "#team 2" + alice <# "#team bob> blocked [blocked by admin] <muted>" + cath <# "#team bob> blocked [blocked by admin] <muted>" + + threadDelay 1000000 + + bob #> "#team 3" + alice <# "#team bob> blocked [blocked by admin] <muted>" + cath <# "#team bob> blocked [blocked by admin] <muted>" + + threadDelay 1000000 + + alice ##> "/unblock for all #team bob" + alice <## "#team: you unblocked bob" + cath <## "#team: alice unblocked bob" + bob <// 50000 + + threadDelay 1000000 + + bob #> "#team 4" + [alice, cath] *<# "#team bob> 4" + + alice + #$> ( "/_get chat #1 count=6", + chat, + [ (0, "1"), + (1, "blocked bob (Bob)"), + (0, "blocked [blocked by admin]"), + (0, "blocked [blocked by admin]"), + (1, "unblocked bob (Bob)"), + (0, "4") + ] + ) + cath + #$> ( "/_get chat #1 count=6", + chat, + [ (0, "1"), + (0, "blocked bob (Bob)"), + (0, "blocked [blocked by admin]"), + (0, "blocked [blocked by admin]"), + (0, "unblocked bob (Bob)"), + (0, "4") + ] + ) + bob #$> ("/_get chat #1 count=4", chat, [(1, "1"), (1, "2"), (1, "3"), (1, "4")]) + +testBlockForAllAnotherAdminUnblocks :: HasCallStack => TestParams -> IO () +testBlockForAllAnotherAdminUnblocks = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + -- disableFullDeletion3 "team" alice bob cath + + bob #> "#team 1" + [alice, cath] *<# "#team bob> 1" + + alice ##> "/block for all #team bob" + alice <## "#team: you blocked bob" + cath <## "#team: alice blocked bob" + bob <// 50000 + + bob #> "#team 2" + alice <# "#team bob> 2 [blocked by admin] <muted>" + cath <# "#team bob> 2 [blocked by admin] <muted>" + + cath ##> "/unblock for all #team bob" + cath <## "#team: you unblocked bob" + alice <## "#team: cath unblocked bob" + bob <// 50000 + + bob #> "#team 3" + [alice, cath] *<# "#team bob> 3" + + bob #$> ("/_get chat #1 count=3", chat, [(1, "1"), (1, "2"), (1, "3")]) + +testBlockForAllBeforeJoining :: HasCallStack => TestParams -> IO () +testBlockForAllBeforeJoining = + testChat4 aliceProfile bobProfile cathProfile danProfile $ + \alice bob cath dan -> do + createGroup3 "team" alice bob cath + -- disableFullDeletion3 "team" alice bob cath + + bob #> "#team 1" + [alice, cath] *<# "#team bob> 1" + + alice ##> "/block for all #team bob" + alice <## "#team: you blocked bob" + cath <## "#team: alice blocked bob" + bob <// 50000 + + bob #> "#team 2" + [alice, cath] *<# "#team bob> 2 [blocked by admin] <muted>" + + connectUsers alice dan + addMember "team" alice dan GRAdmin + dan ##> "/j team" + concurrentlyN_ + [ alice <## "#team: dan joined the group", + do + dan <## "#team: you joined the group" + dan + <### [ "#team: member bob (Bob) is connected", + "#team: member cath (Catherine) is connected" + ], + aliceAddedDan bob, + aliceAddedDan cath + ] + + threadDelay 1000000 + + bob #> "#team 3" + [alice, cath, dan] *<# "#team bob> 3 [blocked by admin] <muted>" + + threadDelay 1000000 + + bob #> "#team 4" + [alice, cath, dan] *<# "#team bob> 4 [blocked by admin] <muted>" + + threadDelay 1000000 + + alice ##> "/unblock for all #team bob" + alice <## "#team: you unblocked bob" + cath <## "#team: alice unblocked bob" + dan <## "#team: alice unblocked bob" + bob <// 50000 + + threadDelay 1000000 + + bob #> "#team 5" + [alice, cath, dan] *<# "#team bob> 5" + + dan ##> "/_get chat #1 count=100" + r <- chat <$> getTermLine dan + r `shouldContain` [(0, "3 [blocked by admin]"), (0, "4 [blocked by admin]"), (0, "unblocked bob (Bob)"), (0, "5")] + r `shouldNotContain` [(0, "1")] + r `shouldNotContain` [(0, "1 [blocked by admin]")] + r `shouldNotContain` [(0, "2")] + r `shouldNotContain` [(0, "2 [blocked by admin]")] + where + aliceAddedDan :: HasCallStack => TestCC -> IO () + aliceAddedDan cc = do + cc <## "#team: alice added dan (Daniel) to the group (connecting...)" + cc <## "#team: new member dan is connected" + +testBlockForAllRepeat :: HasCallStack => TestParams -> IO () +testBlockForAllRepeat = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + -- disableFullDeletion3 "team" alice bob cath + + bob #> "#team 1" + [alice, cath] *<# "#team bob> 1" + + alice ##> "/block for all #team bob" + alice <## "#team: you blocked bob" + cath <## "#team: alice blocked bob" + bob <// 50000 + + alice ##> "/block for all #team bob" + alice <## "#team: you blocked bob" + + cath ##> "/block for all #team bob" + cath <## "#team: you blocked bob" + + bob #> "#team 2" + alice <# "#team bob> 2 [blocked by admin] <muted>" + cath <# "#team bob> 2 [blocked by admin] <muted>" + + cath ##> "/unblock for all #team bob" + cath <## "#team: you unblocked bob" + alice <## "#team: cath unblocked bob" + bob <// 50000 + + alice ##> "/unblock for all #team bob" + alice <## "#team: you unblocked bob" + + cath ##> "/unblock for all #team bob" + cath <## "#team: you unblocked bob" + + bob #> "#team 3" + [alice, cath] *<# "#team bob> 3" + + bob #$> ("/_get chat #1 count=3", chat, [(1, "1"), (1, "2"), (1, "3")]) + +testBlockForAllMultipleMembers :: HasCallStack => TestParams -> IO () +testBlockForAllMultipleMembers = + testChat4 aliceProfile bobProfile cathProfile danProfile $ + \alice bob cath dan -> do + createGroup3 "team" alice bob cath + + connectUsers alice dan + addMember "team" alice dan GRMember + dan ##> "/j team" + concurrentlyN_ + [ alice <## "#team: dan joined the group", + do + dan <## "#team: you joined the group" + dan + <### [ "#team: member bob (Bob) is connected", + "#team: member cath (Catherine) is connected" + ], + do + bob <## "#team: alice added dan (Daniel) to the group (connecting...)" + bob <## "#team: new member dan is connected", + do + cath <## "#team: alice added dan (Daniel) to the group (connecting...)" + cath <## "#team: new member dan is connected" + ] + + -- lower roles to for batch block to be allowed (can't batch block if admins are selected) + alice ##> "/mr team bob member" + concurrentlyN_ + [ alice <## "#team: you changed the role of bob to member", + bob <## "#team: alice changed your role from admin to member", + cath <## "#team: alice changed the role of bob from admin to member", + dan <## "#team: alice changed the role of bob from admin to member" + ] + alice ##> "/mr team cath member" + concurrentlyN_ + [ alice <## "#team: you changed the role of cath to member", + bob <## "#team: alice changed the role of cath from admin to member", + cath <## "#team: alice changed your role from admin to member", + dan <## "#team: alice changed the role of cath from admin to member" + ] + + bob #> "#team 1" + [alice, cath, dan] *<# "#team bob> 1" + + cath #> "#team 2" + [alice, bob, dan] *<# "#team cath> 2" + + alice ##> "/_block #1 2,3 blocked=on" + alice <## "#team: you blocked 2 members" + dan <## "#team: alice blocked bob" + dan <## "#team: alice blocked cath" + bob <// 50000 + cath <// 50000 + + -- bob and cath don't know they are blocked and receive each other's messages + bob #> "#team 3" + [alice, dan] *<# "#team bob> 3 [blocked by admin] <muted>" + cath <# "#team bob> 3" + + cath #> "#team 4" + [alice, dan] *<# "#team cath> 4 [blocked by admin] <muted>" + bob <# "#team cath> 4" + + alice ##> "/_block #1 2,3 blocked=off" + alice <## "#team: you unblocked 2 members" + dan <## "#team: alice unblocked bob" + dan <## "#team: alice unblocked cath" + bob <// 50000 + cath <// 50000 + + bob #> "#team 5" + [alice, cath, dan] *<# "#team bob> 5" + + cath #> "#team 6" + [alice, bob, dan] *<# "#team cath> 6" + +testGroupMemberInactive :: HasCallStack => TestParams -> IO () +testGroupMemberInactive ps = do + withSmpServer' serverCfg' $ do + withNewTestChatCfgOpts ps cfg' opts' "alice" aliceProfile $ \alice -> do + withNewTestChatCfgOpts ps cfg' opts' "bob" bobProfile $ \bob -> do + createGroup2 "team" alice bob + + alice #> "#team hi" + bob <# "#team alice> hi" + bob #> "#team hey" + alice <# "#team bob> hey" + + -- bob is offline + alice #> "#team 1" + alice #> "#team 2" + alice #> "#team 3" + alice <## "[#team bob] connection is marked as inactive" + -- 4 and 5 will be sent to bob as pending messages + alice #> "#team 4" + alice #> "#team 5" + + pgmCount <- withCCTransaction alice $ \db -> + DB.query_ db "SELECT count(1) FROM pending_group_messages" :: IO [[Int]] + pgmCount `shouldBe` [[2]] + + threadDelay 1500000 + + withTestChatCfgOpts ps cfg' opts' "bob" $ \bob -> do + bob <## "1 contacts connected (use /cs for the list)" + bob <## "#team: connected to server(s)" + bob <# "#team alice> 1" + bob <# "#team alice> 2" + bob <#. "#team alice> skipped message ID" + alice <## "[#team bob] inactive connection is marked as active" + + bob <# "#team alice> 4" + bob <# "#team alice> 5" + + pgmCount' <- withCCTransaction alice $ \db -> + DB.query_ db "SELECT count(1) FROM pending_group_messages" :: IO [[Int]] + pgmCount' `shouldBe` [[0]] + + -- delivery works + alice #> "#team hi" + bob <# "#team alice> hi" + bob #> "#team hey" + alice <# "#team bob> hey" + where + serverCfg' = + smpServerCfg + { transports = [("7003", transport @TLS, False)], + msgQueueQuota = 2 + } + fastRetryInterval = defaultReconnectInterval {initialInterval = 50_000} -- same as in agent tests + cfg' = + testCfg + { agentConfig = + testAgentCfg + { quotaExceededTimeout = 1, + messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval} + } + } + opts' = + testOpts + { coreOptions = + testCoreOpts + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] + } + } + +testGroupMemberReports :: HasCallStack => TestParams -> IO () +testGroupMemberReports = + testChat4 aliceProfile bobProfile cathProfile danProfile $ + \alice bob cath dan -> do + createGroup3 "jokes" alice bob cath + -- disableFullDeletion3 "jokes" alice bob cath + alice ##> "/mr jokes bob moderator" + concurrentlyN_ + [ alice <## "#jokes: you changed the role of bob to moderator", + bob <## "#jokes: alice changed your role from admin to moderator", + cath <## "#jokes: alice changed the role of bob from admin to moderator" + ] + alice ##> "/mr jokes cath member" + concurrentlyN_ + [ alice <## "#jokes: you changed the role of cath to member", + bob <## "#jokes: alice changed the role of cath from admin to member", + cath <## "#jokes: alice changed your role from admin to member" + ] + alice ##> "/create link #jokes" + gLink <- getGroupLink alice "jokes" GRMember True + dan ##> ("/c " <> gLink) + dan <## "connection request sent!" + concurrentlyN_ + [ do + alice <## "dan (Daniel): accepting request to join group #jokes..." + alice <## "#jokes: dan joined the group", + do + dan <## "#jokes: joining the group..." + dan <## "#jokes: you joined the group" + dan <### + [ "#jokes: member bob (Bob) is connected", + "#jokes: member cath (Catherine) is connected" + ], + do + bob <## "#jokes: alice added dan (Daniel) to the group (connecting...)" + bob <## "#jokes: new member dan is connected", + do + cath <## "#jokes: alice added dan (Daniel) to the group (connecting...)" + cath <## "#jokes: new member dan is connected" + ] + cath #> "#jokes inappropriate joke" + concurrentlyN_ + [ alice <# "#jokes cath> inappropriate joke", + bob <# "#jokes cath> inappropriate joke", + dan <# "#jokes cath> inappropriate joke" + ] + dan ##> "/report #jokes content inappropriate joke" + dan <# "#jokes > cath inappropriate joke" + dan <## " report content" + concurrentlyN_ + [ do + alice <# "#jokes dan> > cath inappropriate joke" + alice <## " report content", + do + bob <# "#jokes dan> > cath inappropriate joke" + bob <## " report content", + (cath </) + ] + alice #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content")]) + bob #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content")]) + dan #$> ("/_get chat #1 content=report count=100", chat, [(1, "report content")]) + alice ##> "\\\\ #jokes cath inappropriate joke" + concurrentlyN_ + [ do + alice <## "#jokes: 1 messages deleted by user" + alice <## "message marked deleted by you", + do + bob <# "#jokes cath> [marked deleted by alice] inappropriate joke" + bob <## "#jokes: 1 messages deleted by member alice", + cath <# "#jokes cath> [marked deleted by alice] inappropriate joke", + do + dan <# "#jokes cath> [marked deleted by alice] inappropriate joke" + dan <## "#jokes: 1 messages deleted by member alice" + ] + alice #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content [marked deleted by you]")]) + bob #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content [marked deleted by alice]")]) + dan #$> ("/_get chat #1 content=report count=100", chat, [(1, "report content [marked deleted by alice]")]) + -- delete all reports locally + alice #$> ("/clear #jokes", id, "#jokes: all messages are removed locally ONLY") + bob #$> ("/clear #jokes", id, "#jokes: all messages are removed locally ONLY") + dan #$> ("/clear #jokes", id, "#jokes: all messages are removed locally ONLY") + cath #> "#jokes ok joke" + concurrentlyN_ + [ alice <# "#jokes cath> ok joke", + bob <# "#jokes cath> ok joke", + dan <# "#jokes cath> ok joke" + ] + dan ##> "/report #jokes content ok joke" + dan <# "#jokes > cath ok joke" + dan <## " report content" + dan ##> "/report #jokes spam ok joke" + dan <# "#jokes > cath ok joke" + dan <## " report spam" + concurrentlyN_ + [ do + alice <# "#jokes dan> > cath ok joke" + alice <## " report content" + alice <# "#jokes dan> > cath ok joke" + alice <## " report spam", + do + bob <# "#jokes dan> > cath ok joke" + bob <## " report content" + bob <# "#jokes dan> > cath ok joke" + bob <## " report spam", + (cath </) + ] + alice #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content"), (0, "report spam")]) + bob #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content"), (0, "report spam")]) + cath #$> ("/_get chat #1 content=report count=100", chat, []) + dan #$> ("/_get chat #1 content=report count=100", chat, [(1, "report content"), (1, "report spam")]) + alice ##> "/_archive reports #1" + alice <## "#jokes: 2 messages deleted by user" + (bob </) + alice #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content [marked deleted by you]"), (0, "report spam [marked deleted by you]")]) + bob #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content"), (0, "report spam")]) + bob ##> "/_archive reports #1" + bob <## "#jokes: 2 messages deleted by user" + bob #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content [marked deleted by you]"), (0, "report spam [marked deleted by you]")]) + -- delete reports for all admins + alice #$> ("/clear #jokes", id, "#jokes: all messages are removed locally ONLY") + bob #$> ("/clear #jokes", id, "#jokes: all messages are removed locally ONLY") + dan #$> ("/clear #jokes", id, "#jokes: all messages are removed locally ONLY") + cath #> "#jokes ok joke 2" + concurrentlyN_ + [ alice <# "#jokes cath> ok joke 2", + bob <# "#jokes cath> ok joke 2", + dan <# "#jokes cath> ok joke 2" + ] + dan ##> "/report #jokes content ok joke 2" + dan <# "#jokes > cath ok joke 2" + dan <## " report content" + concurrentlyN_ + [ do + alice <# "#jokes dan> > cath ok joke 2" + alice <## " report content", + do + bob <# "#jokes dan> > cath ok joke 2" + bob <## " report content", + (cath </) + ] + alice ##> "/last_item_id" + i :: ChatItemId <- read <$> getTermLine alice + alice ##> ("/_delete reports #1 " <> show i <> " broadcast") + alice <## "message marked deleted by you" + bob <# "#jokes dan> [marked deleted by alice] report content" + alice #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content [marked deleted by you]")]) + bob #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content [marked deleted by alice]")]) + dan #$> ("/_get chat #1 content=report count=100", chat, [(1, "report content")]) + +testMemberMention :: HasCallStack => TestParams -> IO () +testMemberMention = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + alice #> "#team hello!" + concurrentlyN_ + [ bob <# "#team alice> hello!", + cath <# "#team alice> hello!" + ] + bob #> "#team hello @alice" + concurrentlyN_ + [ alice <# "#team bob!> hello @alice", + cath <# "#team bob> hello @alice" + ] + alice #> "#team hello @bob @bob @cath" + concurrentlyN_ + [ bob <# "#team alice!> hello @bob @bob @cath", + cath <# "#team alice!> hello @bob @bob @cath" + ] + cath #> "#team hello @Alice" -- not a mention + concurrentlyN_ + [ alice <# "#team cath> hello @Alice", + bob <# "#team cath> hello @Alice" + ] + cath ##> "! #team hello @alice" -- make it a mention + cath <# "#team [edited] hello @alice" + concurrentlyN_ + [ alice <# "#team cath> [edited] hello @alice", + bob <# "#team cath> [edited] hello @alice" + ] + cath ##> "! #team hello @alice @bob" -- add a mention + cath <# "#team [edited] hello @alice @bob" + concurrentlyN_ + [ alice <# "#team cath> [edited] hello @alice @bob", + bob <# "#team cath> [edited] hello @alice @bob" + ] + +testForwardQuoteMention :: HasCallStack => TestParams -> IO () +testForwardQuoteMention = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + bob #> "#team hello @alice @cath" + concurrentlyN_ + [ alice <# "#team bob!> hello @alice @cath", + cath <# "#team bob!> hello @alice @cath" + ] + -- quote mentions + alice `send` "> #team @bob (hello) hi there!" + alice <# "#team > bob hello @alice @cath" + alice <## " hi there!" + concurrently_ + ( do + bob <# "#team alice!> > bob hello @alice @cath" + bob <## " hi there!" + ) + ( do + cath <# "#team alice> > bob hello @alice @cath" + cath <## " hi there!" + ) + -- forward mentions to the same group + alice `send` "#team <- #team hello" + alice <# "#team <- #team" + alice <## " hello @alice @cath" + concurrentlyN_ + [ do + bob <# "#team alice> -> forwarded" + bob <## " hello @alice @cath", + do + cath <# "#team alice!> -> forwarded" + cath <## " hello @alice @cath" + ] + -- forward mentions + alice `send` "@bob <- #team hello" + alice <# "@bob <- #team" + alice <## " hello @alice @cath" + bob <# "alice> -> forwarded" + bob <## " hello @alice @cath" + -- member renamed to duplicate name + cath ##> "/p alice_1" + cath <## "user profile is changed to alice_1 (your 1 contacts are notified)" + alice <## "contact cath changed to alice_1" + alice <## "use @alice_1 <message> to send messages" + -- mention changed in quoted mentions + alice `send` "> #team @bob (hello) hi there!" + alice <# "#team > bob hello @alice @alice_1" + alice <## " hi there!" + concurrently_ + ( do + bob <# "#team alice!> > bob hello @alice @alice_1" + bob <## " hi there!" + ) + ( do + cath <# "#team alice> > bob hello @alice @alice_1" + cath <## " hi there!" + ) + -- mention changed in forwarded message + alice `send` "@bob <- #team hello" + alice <# "@bob <- #team" + alice <## " hello @alice @alice_1" + bob <# "alice> -> forwarded" + bob <## " hello @alice @alice_1" + +testGroupHistoryWithMentions :: HasCallStack => TestParams -> IO () +testGroupHistoryWithMentions = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup2 "team" alice bob + + threadDelay 1000000 + + alice #> "#team hello @bob" + bob <# "#team alice!> hello @bob" + + bob ##> "/p robert" + bob <## "user profile is changed to robert (your 1 contacts are notified)" + alice <## "contact bob changed to robert" + alice <## "use @robert <message> to send messages" + + alice ##> "/create link #team" + gLink <- getGroupLink alice "team" GRMember True + + cath ##> ("/c " <> gLink) + cath <## "connection request sent!" + alice <## "cath (Catherine): accepting request to join group #team..." + concurrentlyN_ + [ alice <## "#team: cath joined the group", + cath + <### [ "#team: joining the group...", + "#team: you joined the group", + WithTime "#team alice> hello @robert [>>]", + "#team: member robert is connected" + ], + do + bob <## "#team: alice added cath (Catherine) to the group (connecting...)" + bob <## "#team: new member cath is connected" + ] + +testUniqueMsgMentions :: SpecWith TestParams +testUniqueMsgMentions = do + it "1 correct mention" $ \_ -> + uniqueMsgMentions 2 (mm [("alice", "abcd")]) ["alice"] + `shouldBe` (mm [("alice", "abcd")]) + it "2 correct mentions" $ \_ -> + uniqueMsgMentions 2 (mm [("alice", "abcd"), ("bob", "efgh")]) ["alice", "bob"] + `shouldBe` (mm [("alice", "abcd"), ("bob", "efgh")]) + it "2 correct mentions with repetition" $ \_ -> + uniqueMsgMentions 2 (mm [("alice", "abcd"), ("bob", "efgh")]) ["alice", "alice", "alice", "bob", "bob", "bob"] + `shouldBe` (mm [("alice", "abcd"), ("bob", "efgh")]) + it "too many mentions - drop extras" $ \_ -> + uniqueMsgMentions 3 (mm [("a", "abcd"), ("b", "efgh"), ("c", "1234"), ("d", "5678")]) ["a", "a", "a", "b", "b", "c", "d"] + `shouldBe` (mm [("a", "abcd"), ("b", "efgh"), ("c", "1234")]) + it "repeated-with-different name - drop extras" $ \_ -> + uniqueMsgMentions 2 (mm [("alice", "abcd"), ("alice2", "abcd"), ("bob", "efgh"), ("bob2", "efgh")]) ["alice", "alice2", "bob", "bob2"] + `shouldBe` (mm [("alice", "abcd"), ("bob", "efgh")]) + where + mm = M.fromList . map (second $ MsgMention . MemberId) + +testUpdatedMentionNames :: SpecWith TestParams +testUpdatedMentionNames = do + it "keep mentions" $ \_ -> do + test (mm [("alice", Just "alice"), ("bob", Nothing)]) "hello @alice @bob" + `shouldBe` "hello @alice @bob" + test (mm [("alice_1", Just "alice"), ("alice", Just "alice")]) "hello @alice @alice_1" + `shouldBe` "hello @alice @alice_1" + it "keep non-mentions" $ \_ -> do + test (mm []) "hello @alice @bob" + `shouldBe` "hello @alice @bob" + test (mm [("alice", Just "alice")]) "hello @alice @bob" + `shouldBe` "hello @alice @bob" + it "replace changed names" $ \_ -> do + test (mm [("alice", Just "Alice Jones"), ("bob", Just "robert")]) "hello @alice @bob" + `shouldBe` "hello @'Alice Jones' @robert" + test (mm [("alice", Just "alice"), ("cath", Just "alice")]) "hello @alice @cath" + `shouldBe` "hello @alice @alice_1" + where + test mentions t = + let (mc', _, _) = updatedMentionNames (MCText t) (parseMaybeMarkdownList t) mentions + in msgContentText mc' + mm = M.fromList . map (second mentionedMember) + mentionedMember name_ = CIMention {memberId = MemberId "abcd", memberRef = ciMentionMember <$> name_} + where + ciMentionMember name = CIMentionMember {groupMemberId = 1, displayName = name, localAlias = Nothing, memberRole = GRMember} + +testGroupDirectMessages :: HasCallStack => TestParams -> IO () +testGroupDirectMessages = + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + createGroup3 "team" alice bob cath + + alice #> "#team 1" + [bob, cath] *<# "#team alice> 1" + + bob #> "#team 2" + [alice, cath] *<# "#team bob> 2" + + void $ withCCTransaction alice $ \db -> + DB.execute_ db "UPDATE group_members SET member_status='pending_approval' WHERE group_member_id = 2" + + alice ##> "/_send #1 @2 text 3" + alice <# "#team 3" + bob <# "#team alice> 3" + + void $ withCCTransaction bob $ \db -> + DB.execute_ db "UPDATE group_members SET member_status='pending_approval' WHERE group_member_id = 1" + + bob ##> "/_send #1 @1 text 4" + bob <# "#team 4" + alice <# "#team bob> 4" + + -- GSMemPendingApproval members don't receive messages sent to group. + -- Though in test we got here synthetically, in reality this status + -- means they are not yet part of group (not memberCurrent). + alice #> "#team 5" + cath <# "#team alice> 5" + + bob #> "#team 6" + cath <# "#team bob> 6" diff --git a/tests/ChatTests/Local.hs b/tests/ChatTests/Local.hs new file mode 100644 index 0000000000..a5fb31ad77 --- /dev/null +++ b/tests/ChatTests/Local.hs @@ -0,0 +1,231 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PostfixOperators #-} + +module ChatTests.Local where + +import ChatClient +import ChatTests.ChatList (getChats_) +import ChatTests.DBUtils +import ChatTests.Utils +import Data.Time (getCurrentTime) +import Data.Time.Format.ISO8601 (iso8601Show) +import Simplex.Chat.Controller (ChatConfig (..), InlineFilesConfig (..), defaultInlineFilesConfig) +import System.Directory (copyFile, doesFileExist) +import System.FilePath ((</>)) +import Test.Hspec hiding (it) + +chatLocalChatsTests :: SpecWith TestParams +chatLocalChatsTests = do + describe "note folders" $ do + it "create folders, add notes, read, search" testNotes + it "switch users" testUserNotes + it "preview pagination for notes" testPreviewsPagination + it "chat pagination" testChatPagination + it "stores files" testFiles + it "deleting files does not interfere with other chat types" testOtherFiles + describe "batch create messages" $ do + it "create multiple messages api" testCreateMulti + it "create multiple messages with files" testCreateMultiFiles + +testNotes :: TestParams -> IO () +testNotes ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do + createCCNoteFolder alice + + alice ##> "/contacts" + -- not a contact + + alice >* "keep in mind" + alice ##> "/tail" + alice <# "* keep in mind" + alice ##> "/chats" + alice <# "* keep in mind" + alice ##> "/? keep" + alice <# "* keep in mind" + + alice #$> ("/_read chat *1", id, "ok") + alice ##> "/_unread chat *1 on" + alice <## "ok" + + alice ##> "/_delete item *1 1 internal" + alice <## "message deleted" + alice ##> "/tail" + alice ##> "/chats" + + alice >* "ahoy!" + alice ##> "/_update item *1 2 text Greetings." + alice ##> "/tail *" + alice <# "* Greetings." + +testUserNotes :: TestParams -> IO () +testUserNotes ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do + createCCNoteFolder alice + + alice >* "keep in mind" + alice ##> "/tail" + alice <# "* keep in mind" + + alice ##> "/create user secret" + alice <## "user profile: secret" + alice <## "use /p <display name> to change it" + alice <## "(the updated profile will be sent to all your contacts)" + + alice ##> "/tail" + + alice ##> "/_delete item *1 1 internal" + alice <## "chat db error: SENoteFolderNotFound {noteFolderId = 1}" + +testPreviewsPagination :: TestParams -> IO () +testPreviewsPagination ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do + createCCNoteFolder alice + + tsS <- iso8601Show <$> getCurrentTime + alice >* "first" + tsM <- iso8601Show <$> getCurrentTime + alice >* "last" + tsE <- iso8601Show <$> getCurrentTime + + -- there's only one folder that got updated after tsM and before tsE + getChats_ alice "count=3" [("*", "last")] + getChats_ alice ("after=" <> tsE <> " count=10") [] + getChats_ alice ("after=" <> tsS <> " count=10") [("*", "last")] + getChats_ alice ("before=" <> tsM <> " count=10") [] + getChats_ alice ("before=" <> tsE <> " count=10") [("*", "last")] + getChats_ alice ("before=" <> tsS <> " count=10") [] + +testChatPagination :: TestParams -> IO () +testChatPagination ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do + createCCNoteFolder alice + + alice >* "hello world" + alice >* "memento mori" + alice >* "knock-knock" + alice >* "who's there?" + + alice #$> ("/_get chat *1 count=100", chat, [(1, "hello world"), (1, "memento mori"), (1, "knock-knock"), (1, "who's there?")]) + alice #$> ("/_get chat *1 count=1", chat, [(1, "who's there?")]) + alice #$> ("/_get chat *1 around=2 count=1", chat, [(1, "hello world"), (1, "memento mori"), (1, "knock-knock")]) + alice #$> ("/_get chat *1 around=2 count=3", chat, [(1, "hello world"), (1, "memento mori"), (1, "knock-knock"), (1, "who's there?")]) + alice #$> ("/_get chat *1 around=3 count=10", chat, [(1, "hello world"), (1, "memento mori"), (1, "knock-knock"), (1, "who's there?")]) + alice #$> ("/_get chat *1 around=4 count=1", chat, [(1, "knock-knock"), (1, "who's there?")]) + alice #$> ("/_get chat *1 after=2 count=10", chat, [(1, "knock-knock"), (1, "who's there?")]) + alice #$> ("/_get chat *1 after=2 count=2", chat, [(1, "knock-knock"), (1, "who's there?")]) + alice #$> ("/_get chat *1 after=1 count=2", chat, [(1, "memento mori"), (1, "knock-knock")]) + alice #$> ("/_get chat *1 before=3 count=10", chat, [(1, "hello world"), (1, "memento mori")]) + alice #$> ("/_get chat *1 before=3 count=2", chat, [(1, "hello world"), (1, "memento mori")]) + alice #$> ("/_get chat *1 before=4 count=2", chat, [(1, "memento mori"), (1, "knock-knock")]) + + alice #$> ("/_get chat *1 count=10 search=k-k", chat, [(1, "knock-knock")]) + +testFiles :: TestParams -> IO () +testFiles ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do + -- setup + createCCNoteFolder alice + let files = "./tests/tmp/app_files" + alice ##> ("/_files_folder " <> files) + alice <## "ok" + + -- ui-like upload + let source = "./tests/fixtures/test.jpg" + let stored = files </> "test.jpg" + copyFile source stored + alice ##> "/_create *1 json [{\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"hi myself\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]" + alice <# "* hi myself" + alice <# "* file 1 (test.jpg)" + + alice ##> "/tail" + alice <# "* hi myself" + alice <# "* file 1 (test.jpg)" + + alice ##> "/_get chat *1 count=100" + r <- chatF <$> getTermLine alice + r `shouldBe` [((1, "hi myself"), Just "test.jpg")] + + alice ##> "/fs 1" + alice <## "bad chat command: not supported for local files" + + alice ##> "/fc 1" + alice <## "chat db error: SELocalFileNoTransfer {fileId = 1}" + + -- one more file + let stored2 = files </> "another_test.jpg" + copyFile source stored2 + alice ##> "/_create *1 json [{\"filePath\": \"another_test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]" + alice <# "* file 2 (another_test.jpg)" + + alice ##> "/_delete item *1 2 internal" + alice <## "message deleted" + doesFileExist stored2 `shouldReturn` False + doesFileExist stored `shouldReturn` True + + alice ##> "/clear *" + alice <## "notes: all messages are removed" + alice ##> "/fs 1" + alice <## "file 1 not found" + alice ##> "/tail" + doesFileExist stored `shouldReturn` False + +testOtherFiles :: TestParams -> IO () +testOtherFiles = + testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> withXFTPServer $ do + connectUsers alice bob + createCCNoteFolder bob + bob ##> "/_files_folder ./tests/tmp/" + bob <## "ok" + + alice #> "/f @bob ./tests/fixtures/test.jpg" + alice <## "use /fc 1 to cancel sending" + bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" + bob <## "use /fr 1 [<dir>/ | <path>] to receive it" + alice <## "completed uploading file 1 (test.jpg) for bob" + + bob ##> "/fr 1" + bob + <### [ "saving file 1 from alice to test.jpg", + "started receiving file 1 (test.jpg) from alice" + ] + bob <## "completed receiving file 1 (test.jpg) from alice" + + bob >* "test" + bob ##> "/tail *" + bob <# "* test" + bob ##> "/clear *" + bob <## "notes: all messages are removed" + bob ##> "/tail *" + bob ##> "/fs 1" + bob <## "receiving file 1 (test.jpg) complete, path: test.jpg" + doesFileExist "./tests/tmp/test.jpg" `shouldReturn` True + where + cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, sendChunks = 100, receiveChunks = 100}} + +testCreateMulti :: TestParams -> IO () +testCreateMulti ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do + createCCNoteFolder alice + + alice ##> "/_create *1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]" + alice <# "* test 1" + alice <# "* test 2" + +testCreateMultiFiles :: TestParams -> IO () +testCreateMultiFiles ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do + createCCNoteFolder alice + alice #$> ("/_files_folder ./tests/tmp/alice_app_files", id, "ok") + copyFile "./tests/fixtures/test.jpg" "./tests/tmp/alice_app_files/test.jpg" + copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_app_files/test.pdf" + + let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message without file\"}}" + cm2 = "{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 1\"}}" + cm3 = "{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 2\"}}" + alice ##> ("/_create *1 json [" <> cm1 <> "," <> cm2 <> "," <> cm3 <> "]") + + alice <# "* message without file" + alice <# "* sending file 1" + alice <# "* file 1 (test.jpg)" + alice <# "* sending file 2" + alice <# "* file 2 (test.pdf)" + + doesFileExist "./tests/tmp/alice_app_files/test.jpg" `shouldReturn` True + doesFileExist "./tests/tmp/alice_app_files/test.pdf" `shouldReturn` True + + alice ##> "/_get chat *1 count=3" + r <- chatF <$> getTermLine alice + r `shouldBe` [((1, "message without file"), Nothing), ((1, "sending file 1"), Just "test.jpg"), ((1, "sending file 2"), Just "test.pdf")] diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 6813a4cc72..adff745200 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -1,9 +1,14 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PostfixOperators #-} +{-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module ChatTests.Profiles where import ChatClient +import ChatTests.DBUtils import ChatTests.Utils import Control.Concurrent (threadDelay) import Control.Concurrent.Async (concurrently_) @@ -12,13 +17,23 @@ import Control.Monad.Except import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Char8 as B import qualified Data.Text as T +import Simplex.Chat.Controller (ChatConfig (..)) +import Simplex.Chat.Options +import Simplex.Chat.Protocol (currentChatVersion) import Simplex.Chat.Store.Shared (createContact) -import Simplex.Chat.Types (ConnStatus (..), GroupMemberRole (..), Profile (..)) +import Simplex.Chat.Types (ConnStatus (..), Profile (..)) +import Simplex.Chat.Types.Shared (GroupMemberRole (..)) +import Simplex.Chat.Types.UITheme +import Simplex.Messaging.Agent.Env.SQLite +import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Encoding.String (StrEncoding (..)) +import Simplex.Messaging.Server.Env.STM hiding (subscriptions) +import Simplex.Messaging.Transport +import Simplex.Messaging.Util (encodeJSON) import System.Directory (copyFile, createDirectoryIfMissing) -import Test.Hspec +import Test.Hspec hiding (it) -chatProfileTests :: SpecWith FilePath +chatProfileTests :: SpecWith TestParams chatProfileTests = do describe "user profiles" $ do it "update user profile and notify contacts" testUpdateProfile @@ -26,6 +41,7 @@ chatProfileTests = do it "use multiword profile names" testMultiWordProfileNames describe "user contact link" $ do it "create and connect via contact link" testUserContactLink + it "retry accepting connection via contact link" testRetryAcceptingViaContactLink it "add contact link to profile" testProfileLink it "auto accept contact requests" testUserContactLinkAutoAccept it "deduplicate contact requests" testDeduplicateContactRequests @@ -34,10 +50,14 @@ chatProfileTests = do it "delete connection requests when contact link deleted" testDeleteConnectionRequests it "auto-reply message" testAutoReplyMessage it "auto-reply message in incognito" testAutoReplyMessageInIncognito + describe "business address" $ do + it "create and connect via business address" testBusinessAddress + it "update profiles with business address" testBusinessUpdateProfiles describe "contact address connection plan" $ do it "contact address ok to connect; known contact" testPlanAddressOkKnown it "own contact address" testPlanAddressOwn it "connecting via contact address" testPlanAddressConnecting + it "connecting via contact address (slow handshake)" testPlanAddressConnectingSlow it "re-connect with deleted contact" testPlanAddressContactDeletedReconnected it "contact via address" testPlanAddressContactViaAddress describe "incognito" $ do @@ -47,6 +67,7 @@ chatProfileTests = do it "set connection incognito" testSetConnectionIncognito it "reset connection incognito" testResetConnectionIncognito it "set connection incognito prohibited during negotiation" testSetConnectionIncognitoProhibitedDuringNegotiation + it "set connection incognito prohibited during negotiation (slow handshake)" testSetConnectionIncognitoProhibitedDuringNegotiationSlow it "connection incognito unchanged errors" testConnectionIncognitoUnchangedErrors it "set, reset, set connection incognito" testSetResetSetConnectionIncognito it "join group incognito" testJoinGroupIncognito @@ -57,6 +78,13 @@ chatProfileTests = do describe "contact aliases" $ do it "set contact alias" testSetAlias it "set connection alias" testSetConnectionAlias + describe "group aliases" $ do + it "set group alias" testSetGroupAlias + describe "pending connection users" $ do + it "change user for pending connection" testChangePCCUser + it "change from incognito profile connects as new user" testChangePCCUserFromIncognito + it "change user for pending connection and later set incognito connects as incognito in changed profile" testChangePCCUserAndThenIncognito + it "change user for user without matching servers creates new connection" testChangePCCUserDiffSrv describe "preferences" $ do it "set contact preferences" testSetContactPrefs it "feature offers" testFeatureOffers @@ -67,12 +95,25 @@ chatProfileTests = do xit'' "enable timed messages with contact" testEnableTimedMessagesContact it "enable timed messages in group" testEnableTimedMessagesGroup xit'' "timed messages enabled globally, contact turns on" testTimedMessagesEnabledGlobally + it "update multiple user preferences for multiple contacts" testUpdateMultipleUserPrefs + describe "group preferences for specific member role" $ do + it "direct messages" testGroupPrefsDirectForRole + it "files & media" testGroupPrefsFilesForRole + it "SimpleX links" testGroupPrefsSimplexLinksForRole + it "set user, contact and group UI theme" testSetUITheme + describe "short links" $ do + it "should connect via one-time inviation" testShortLinkInvitation + it "should plan and connect via one-time inviation" testPlanShortLinkInvitation + it "should connect via contact address" testShortLinkContactAddress + it "should join group" testShortLinkJoinGroup -testUpdateProfile :: HasCallStack => FilePath -> IO () +testUpdateProfile :: HasCallStack => TestParams -> IO () testUpdateProfile = - testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do - createGroup3 "team" alice bob cath + connectUsers alice bob + connectUsers alice cath + connectUsers bob cath alice ##> "/p" alice <## "user profile: alice (Alice)" alice <## "use /p <display name> to change it" @@ -110,7 +151,7 @@ testUpdateProfile = bob <## "use @cat <message> to send messages" ] -testUpdateProfileImage :: HasCallStack => FilePath -> IO () +testUpdateProfileImage :: HasCallStack => TestParams -> IO () testUpdateProfileImage = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -130,7 +171,7 @@ testUpdateProfileImage = bob <## "use @alice2 <message> to send messages" (bob </) -testMultiWordProfileNames :: HasCallStack => FilePath -> IO () +testMultiWordProfileNames :: HasCallStack => TestParams -> IO () testMultiWordProfileNames = testChat3 aliceProfile' bobProfile' cathProfile' $ \alice bob cath -> do @@ -178,12 +219,15 @@ testMultiWordProfileNames = alice <# "#'Our Team' 'Bob James'> hi" cath <# "#'Our Team' 'Bob James'> hi" alice `send` "@'Cath Johnson' hello" - alice <## "member #'Our Team' 'Cath Johnson' does not have direct connection, creating" - alice <## "contact for member #'Our Team' 'Cath Johnson' is created" - alice <## "sent invitation to connect directly to member #'Our Team' 'Cath Johnson'" - alice <# "@'Cath Johnson' hello" + alice + <### [ "member #'Our Team' 'Cath Johnson' does not have direct connection, creating", + "contact for member #'Our Team' 'Cath Johnson' is created", + "sent invitation to connect directly to member #'Our Team' 'Cath Johnson'", + WithTime "@'Cath Johnson' hello" + ] cath <## "#'Our Team' 'Alice Jones' is creating direct contact 'Alice Jones' with you" cath <# "'Alice Jones'> hello" + cath <## "'Alice Jones': you can send messages to contact" cath <## "'Alice Jones': contact is connected" alice <## "'Cath Johnson': contact is connected" cath ##> "/p 'Cath J'" @@ -200,7 +244,7 @@ testMultiWordProfileNames = cathProfile' = baseProfile {displayName = "Cath Johnson"} baseProfile = Profile {displayName = "", fullName = "", image = Nothing, contactLink = Nothing, preferences = defaultPrefs} -testUserContactLink :: HasCallStack => FilePath -> IO () +testUserContactLink :: HasCallStack => TestParams -> IO () testUserContactLink = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -210,7 +254,7 @@ testUserContactLink = alice <#? bob alice @@@ [("<@bob", "")] alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -222,7 +266,7 @@ testUserContactLink = alice <#? cath alice @@@ [("<@cath", ""), ("@bob", "hey")] alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") @@ -230,7 +274,68 @@ testUserContactLink = alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")] alice <##> cath -testProfileLink :: HasCallStack => FilePath -> IO () +testRetryAcceptingViaContactLink :: HasCallStack => TestParams -> IO () +testRetryAcceptingViaContactLink ps = testChatCfgOpts2 cfg' opts' aliceProfile bobProfile test ps + where + tmp = tmpPath ps + test alice bob = do + cLink <- withSmpServer' serverCfg' $ do + alice ##> "/ad" + getContactLink alice True + alice <## "server disconnected localhost ()" + bob ##> ("/_connect plan 1 " <> cLink) + bob <## "contact address: ok to connect" + bob ##> ("/_connect 1 " <> cLink) + bob <##. "smp agent error: BROKER" + withSmpServer' serverCfg' $ do + alice <## "server connected localhost ()" + threadDelay 250000 + bob ##> ("/_connect plan 1 " <> cLink) + bob <## "contact address: ok to connect" + bob ##> ("/_connect 1 " <> cLink) + alice <#? bob + alice <## "server disconnected localhost ()" + bob <## "server disconnected localhost ()" + alice ##> "/ac bob" + alice <##. "smp agent error: BROKER" + withSmpServer' serverCfg' $ do + alice <## "server connected localhost ()" + bob <## "server connected localhost ()" + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request, you can send messages to contact" + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice #> "@bob message 1" + bob <# "alice> message 1" + bob #> "@alice message 2" + alice <# "bob> message 2" + alice <## "server disconnected localhost (@bob)" + bob <## "server disconnected localhost (@alice)" + serverCfg' = + smpServerCfg + { transports = [("7003", transport @TLS, False)], + msgQueueQuota = 2, + serverStoreCfg = persistentServerStoreCfg tmp + } + fastRetryInterval = defaultReconnectInterval {initialInterval = 50000} -- same as in agent tests + cfg' = + testCfg + { agentConfig = + testAgentCfg + { quotaExceededTimeout = 1, + messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval} + } + } + opts' = + testOpts + { coreOptions = + testCoreOpts + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] + } + } + +testProfileLink :: HasCallStack => TestParams -> IO () testProfileLink = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -240,7 +345,7 @@ testProfileLink = bob ##> ("/c " <> cLink) alice <#? bob alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -255,7 +360,7 @@ testProfileLink = cath ##> ("/c " <> cLink) alice <#? cath alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") @@ -299,6 +404,7 @@ testProfileLink = cc <## ("contact address: " <> cLink) cc <## "you've shared main profile with this contact" cc <## "connection not verified, use /code command to see security code" + cc <## "quantum resistant end-to-end encryption" cc <## currentChatVRangeInfo checkAliceNoProfileLink cc = do cc ##> "/info alice" @@ -307,9 +413,10 @@ testProfileLink = cc <##. "sending messages via" cc <## "you've shared main profile with this contact" cc <## "connection not verified, use /code command to see security code" + cc <## "quantum resistant end-to-end encryption" cc <## currentChatVRangeInfo -testUserContactLinkAutoAccept :: HasCallStack => FilePath -> IO () +testUserContactLinkAutoAccept :: HasCallStack => TestParams -> IO () testUserContactLinkAutoAccept = testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do @@ -320,7 +427,7 @@ testUserContactLinkAutoAccept = alice <#? bob alice @@@ [("<@bob", "")] alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -334,6 +441,7 @@ testUserContactLinkAutoAccept = cath ##> ("/c " <> cLink) cath <## "connection request sent!" alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): you can send messages to contact" concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") @@ -348,7 +456,7 @@ testUserContactLinkAutoAccept = alice <#? dan alice @@@ [("<@dan", ""), ("@cath", "hey"), ("@bob", "hey")] alice ##> "/ac dan" - alice <## "dan (Daniel): accepting contact request..." + alice <## "dan (Daniel): accepting contact request, you can send messages to contact" concurrently_ (dan <## "alice (Alice): contact is connected") (alice <## "dan (Daniel): contact is connected") @@ -356,7 +464,7 @@ testUserContactLinkAutoAccept = alice @@@ [("@dan", lastChatFeature), ("@cath", "hey"), ("@bob", "hey")] alice <##> dan -testDeduplicateContactRequests :: HasCallStack => FilePath -> IO () +testDeduplicateContactRequests :: HasCallStack => TestParams -> IO () testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do alice ##> "/ad" @@ -375,7 +483,7 @@ testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $ bob @@@! [(":3", "", Just ConnJoined), (":2", "", Just ConnJoined), (":1", "", Just ConnJoined)] alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -383,6 +491,7 @@ testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $ bob ##> ("/c " <> cLink) bob <## "contact address: known contact alice" bob <## "use @alice <message> to send messages" + threadDelay 100000 alice @@@ [("@bob", lastChatFeature)] bob @@@ [("@alice", lastChatFeature), (":2", ""), (":1", "")] bob ##> "/_delete :1" @@ -406,7 +515,7 @@ testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $ alice <#? cath alice @@@ [("<@cath", ""), ("@bob", "hey")] alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") @@ -414,7 +523,7 @@ testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $ alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")] alice <##> cath -testDeduplicateContactRequestsProfileChange :: HasCallStack => FilePath -> IO () +testDeduplicateContactRequestsProfileChange :: HasCallStack => TestParams -> IO () testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do alice ##> "/ad" @@ -448,7 +557,7 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile alice ##> "/ac bob" alice <## "no contact request from bob" alice ##> "/ac robert" - alice <## "robert (Robert): accepting contact request..." + alice <## "robert (Robert): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "robert (Robert): contact is connected") @@ -456,6 +565,7 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile bob ##> ("/c " <> cLink) bob <## "contact address: known contact alice" bob <## "use @alice <message> to send messages" + threadDelay 100000 alice @@@ [("@robert", lastChatFeature)] bob @@@ [("@alice", lastChatFeature), (":3", ""), (":2", ""), (":1", "")] bob ##> "/_delete :1" @@ -474,6 +584,7 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile bob <## "use @alice <message> to send messages" alice <##> bob + threadDelay 100000 alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hi"), (0, "hey"), (1, "hi"), (0, "hey")]) bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hi"), (1, "hey"), (0, "hi"), (1, "hey")]) @@ -481,7 +592,7 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile alice <#? cath alice @@@ [("<@cath", ""), ("@robert", "hey")] alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") @@ -489,7 +600,7 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile alice @@@ [("@cath", lastChatFeature), ("@robert", "hey")] alice <##> cath -testRejectContactAndDeleteUserContact :: HasCallStack => FilePath -> IO () +testRejectContactAndDeleteUserContact :: HasCallStack => TestParams -> IO () testRejectContactAndDeleteUserContact = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do alice ##> "/_address 1" @@ -512,7 +623,7 @@ testRejectContactAndDeleteUserContact = testChat3 aliceProfile bobProfile cathPr cath ##> ("/c " <> cLink) cath <## "error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection" -testDeleteConnectionRequests :: HasCallStack => FilePath -> IO () +testDeleteConnectionRequests :: HasCallStack => TestParams -> IO () testDeleteConnectionRequests = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do alice ##> "/ad" @@ -534,7 +645,7 @@ testDeleteConnectionRequests = testChat3 aliceProfile bobProfile cathProfile $ cath ##> ("/c " <> cLink') alice <#? cath -testAutoReplyMessage :: HasCallStack => FilePath -> IO () +testAutoReplyMessage :: HasCallStack => TestParams -> IO () testAutoReplyMessage = testChat2 aliceProfile bobProfile $ \alice bob -> do alice ##> "/ad" @@ -547,16 +658,16 @@ testAutoReplyMessage = testChat2 aliceProfile bobProfile $ bob ##> ("/c " <> cLink) bob <## "connection request sent!" alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): you can send messages to contact" + alice <# "@bob hello!" concurrentlyN_ [ do - bob <## "alice (Alice): contact is connected" - bob <# "alice> hello!", - do - alice <## "bob (Bob): contact is connected" - alice <# "@bob hello!" + bob <# "alice> hello!" + bob <## "alice (Alice): contact is connected", + alice <## "bob (Bob): contact is connected" ] -testAutoReplyMessageInIncognito :: HasCallStack => FilePath -> IO () +testAutoReplyMessageInIncognito :: HasCallStack => TestParams -> IO () testAutoReplyMessageInIncognito = testChat2 aliceProfile bobProfile $ \alice bob -> do alice ##> "/ad" @@ -569,20 +680,204 @@ testAutoReplyMessageInIncognito = testChat2 aliceProfile bobProfile $ bob ##> ("/c " <> cLink) bob <## "connection request sent!" alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): you can send messages to contact" + alice <# "i @bob hello!" aliceIncognito <- getTermLine alice concurrentlyN_ [ do - bob <## (aliceIncognito <> ": contact is connected") - bob <# (aliceIncognito <> "> hello!"), + bob <# (aliceIncognito <> "> hello!") + bob <## (aliceIncognito <> ": contact is connected"), do alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito) - alice - <### [ "use /i bob to print out this incognito profile again", - WithTime "i @bob hello!" - ] + alice <## "use /i bob to print out this incognito profile again" ] -testPlanAddressOkKnown :: HasCallStack => FilePath -> IO () +testBusinessAddress :: HasCallStack => TestParams -> IO () +testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice @ Biz"} bobProfile $ + \biz alice bob -> do + biz ##> "/ad" + cLink <- getContactLink biz True + biz ##> "/auto_accept on business" + biz <## "auto_accept on, business" + bob ##> ("/_connect plan 1 " <> cLink) + bob <## "contact address: ok to connect" + bob ##> ("/c " <> cLink) + bob <## "connection request sent!" + bob ##> ("/_connect plan 1 " <> cLink) + bob <## "contact address: connecting, allowed to reconnect" + biz <## "#bob (Bob): accepting business address request..." + bob <## "#biz: joining the group..." + -- the next command can be prone to race conditions + bob ##> ("/_connect plan 1 " <> cLink) + bob <## "business link: connecting to business #biz" + biz <## "#bob: bob_1 joined the group" + bob <## "#biz: you joined the group" + biz #> "#bob hi" + bob <# "#biz biz_1> hi" + bob #> "#biz hello" + biz <# "#bob bob_1> hello" + bob ##> ("/_connect plan 1 " <> cLink) + bob <## "business link: known business #biz" + bob <## "use #biz <message> to send messages" + connectUsers biz alice + biz <##> alice + biz ##> "/a #bob alice" + biz <## "invitation to join the group #bob sent to alice" + alice <## "#bob (Bob): biz invites you to join the group as member" + alice <## "use /j bob to accept" + alice ##> "/j bob" + concurrentlyN_ + [ do + alice <## "#bob: you joined the group" + alice <### [WithTime "#bob biz> hi [>>]", WithTime "#bob bob_1> hello [>>]"] + alice <## "#bob: member bob_1 (Bob) is connected", + biz <## "#bob: alice joined the group", + do + bob <## "#biz: biz_1 added alice (Alice @ Biz) to the group (connecting...)" + bob <## "#biz: new member alice is connected" + ] + alice #> "#bob hey" + concurrently_ + (bob <# "#biz alice> hey") + (biz <# "#bob alice> hey") + bob #> "#biz hey there" + concurrently_ + (alice <# "#bob bob_1> hey there") + (biz <# "#bob bob_1> hey there") + +testBusinessUpdateProfiles :: HasCallStack => TestParams -> IO () +testBusinessUpdateProfiles = withTestOutput $ testChat4 businessProfile aliceProfile bobProfile cathProfile $ + \biz alice bob cath -> do + biz ##> "/ad" + cLink <- getContactLink biz True + biz ##> "/auto_accept on business text Welcome" + biz <## "auto_accept on, business" + biz <## "auto reply:" + biz <## "Welcome" + alice ##> ("/c " <> cLink) + alice <## "connection request sent!" + biz <## "#alice (Alice): accepting business address request..." + alice <## "#biz: joining the group..." + biz <# "#alice Welcome" -- auto reply + biz <## "#alice: alice_1 joined the group" + alice + <### + [ WithTime "#biz biz_1> Welcome", + "#biz: you joined the group" + ] + biz #> "#alice hi" + alice <# "#biz biz_1> hi" + alice #> "#biz hello" + biz <# "#alice alice_1> hello" + alice ##> "/p alisa" + alice <## "user profile is changed to alisa (your 0 contacts are notified)" + alice #> "#biz hello again" -- profile update is sent with message + biz <## "alice_1 updated group #alice:" + biz <## "changed to #alisa" + biz <# "#alisa alisa_1> hello again" + -- customer can invite members too, if business allows + biz ##> "/mr alisa alisa_1 admin" + biz <## "#alisa: you changed the role of alisa_1 to admin" + alice <## "#biz: biz_1 changed your role from member to admin" + connectUsers alice bob + alice ##> "/a #biz bob" + alice <## "invitation to join the group #biz sent to bob" + bob <## "#biz (Biz Inc): alisa invites you to join the group as member" + bob <## "use /j biz to accept" + bob ##> "/j biz" + concurrentlyN_ + [ do + bob <## "#biz: you joined the group" + bob + <### + [ WithTime "#biz biz_1> Welcome [>>]", + WithTime "#biz biz_1> hi [>>]", + WithTime "#biz alisa> hello [>>]", + WithTime "#biz alisa> hello again [>>]" + ] + bob <## "#biz: member biz_1 (Biz Inc) is connected", + alice <## "#biz: bob joined the group", + do + biz <## "#alisa: alisa_1 added bob (Bob) to the group (connecting...)" + biz <## "#alisa: new member bob is connected" + ] + -- changing other member profiles does not change group profile + bob ##> "/p robert" + bob <## "user profile is changed to robert (your 1 contacts are notified)" + alice <## "contact bob changed to robert" -- only alice receives profile update + alice <## "use @robert <message> to send messages" + bob #> "#biz hi there" -- profile update is sent to group with message + alice <# "#biz robert> hi there" + biz <# "#alisa robert> hi there" + -- add business team member + connectUsers biz cath + biz ##> "/a #alisa cath" + biz <## "invitation to join the group #alisa sent to cath" + cath <## "#alisa: biz invites you to join the group as member" + cath <## "use /j alisa to accept" + cath ##> "/j alisa" + concurrentlyN_ + [ do + cath <## "#alisa: you joined the group" + cath + <### + [ WithTime "#alisa biz> Welcome [>>]", + WithTime "#alisa biz> hi [>>]", + WithTime "#alisa alisa_1> hello [>>]", + WithTime "#alisa alisa_1> hello again [>>]", + WithTime "#alisa robert> hi there [>>]" + ] + cath <## "#alisa: member alisa_1 is connected" + cath <## "#alisa: member robert is connected", + biz <## "#alisa: cath joined the group", + do + alice <## "#biz: biz_1 added cath (Catherine) to the group (connecting...)" + alice <## "#biz: new member cath is connected", + do + bob <## "#biz: biz_1 added cath (Catherine) to the group (connecting...)" + bob <## "#biz: new member cath is connected" + ] + -- both customers receive business profile change + biz ##> "/p business" + biz <## "user profile is changed to business (your 1 contacts are notified)" + biz #> "#alisa hey" + concurrentlyN_ + [ do + alice <## "biz_1 updated group #biz:" + alice <## "changed to #business" + alice <# "#business business_1> hey", + do + bob <## "biz_1 updated group #biz:" + bob <## "changed to #business" + bob <# "#business business_1> hey", + do + cath <## "contact biz changed to business" + cath <## "use @business <message> to send messages" + cath <# "#alisa business> hey" + ] + biz ##> "/set voice #alisa on" + biz <## "updated group preferences:" + biz <## "Voice messages: on" + concurrentlyN_ + [ do + alice <## "business_1 updated group #business:" + alice <## "updated group preferences:" + alice <## "Voice messages: on", + do + bob <## "business_1 updated group #business:" + bob <## "updated group preferences:" + bob <## "Voice messages: on", + do + cath <## "business updated group #alisa:" + cath <## "updated group preferences:" + cath <## "Voice messages: on" + ] + biz #$> ("/_get chat #1 count=1", chat, [(1, "Voice messages: on")]) + alice #$> ("/_get chat #1 count=1", chat, [(0, "Voice messages: on")]) + bob #$> ("/_get chat #1 count=1", chat, [(0, "Voice messages: on")]) + cath #$> ("/_get chat #1 count=1", chat, [(0, "Voice messages: on")]) + +testPlanAddressOkKnown :: HasCallStack => TestParams -> IO () testPlanAddressOkKnown = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -596,7 +891,7 @@ testPlanAddressOkKnown = alice <#? bob alice @@@ [("<@bob", "")] alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -615,9 +910,9 @@ testPlanAddressOkKnown = bob <## "contact address: known contact alice" bob <## "use @alice <message> to send messages" -testPlanAddressOwn :: HasCallStack => FilePath -> IO () -testPlanAddressOwn tmp = - withNewTestChat tmp "alice" aliceProfile $ \alice -> do +testPlanAddressOwn :: HasCallStack => TestParams -> IO () +testPlanAddressOwn ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do alice ##> "/ad" cLink <- getContactLink alice True @@ -635,12 +930,13 @@ testPlanAddressOwn tmp = alice <## "to reject: /rc alice_1 (the sender will NOT be notified)" alice @@@ [("<@alice_1", ""), (":2", "")] alice ##> "/ac alice_1" - alice <## "alice_1 (Alice): accepting contact request..." + alice <## "alice_1 (Alice): accepting contact request, you can send messages to contact" alice <### [ "alice_1 (Alice): contact is connected", "alice_2 (Alice): contact is connected" ] + threadDelay 100000 alice @@@ [("@alice_1", lastChatFeature), ("@alice_2", lastChatFeature)] alice `send` "@alice_2 hi" alice @@ -660,12 +956,12 @@ testPlanAddressOwn tmp = alice ##> ("/c " <> cLink) alice <## "alice_2 (Alice): contact already exists" -testPlanAddressConnecting :: HasCallStack => FilePath -> IO () -testPlanAddressConnecting tmp = do - cLink <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do +testPlanAddressConnecting :: HasCallStack => TestParams -> IO () +testPlanAddressConnecting ps = do + cLink <- withNewTestChat ps "alice" aliceProfile $ \alice -> do alice ##> "/ad" getContactLink alice True - withNewTestChat tmp "bob" bobProfile $ \bob -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do threadDelay 100000 bob ##> ("/c " <> cLink) @@ -679,14 +975,57 @@ testPlanAddressConnecting tmp = do bob <## "contact address: connecting, allowed to reconnect" threadDelay 100000 - withTestChat tmp "alice" $ \alice -> do + withTestChat ps "alice" $ \alice -> do + alice <## "Your address is active! To show: /sa" + alice <## "bob (Bob) wants to connect to you!" + alice <## "to accept: /ac bob" + alice <## "to reject: /rc bob (the sender will NOT be notified)" + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request, you can send messages to contact" + withTestChat ps "bob" $ \bob -> do + threadDelay 500000 + bob <## "alice (Alice): contact is connected" + bob @@@ [("@alice", "Audio/video calls: enabled")] + bob ##> ("/_connect plan 1 " <> cLink) + bob <## "contact address: known contact alice" + bob <## "use @alice <message> to send messages" + + let cLinkSchema2 = linkAnotherSchema cLink + bob ##> ("/_connect plan 1 " <> cLinkSchema2) + bob <## "contact address: known contact alice" + bob <## "use @alice <message> to send messages" + + bob ##> ("/c " <> cLink) + bob <## "contact address: known contact alice" + bob <## "use @alice <message> to send messages" + +testPlanAddressConnectingSlow :: HasCallStack => TestParams -> IO () +testPlanAddressConnectingSlow ps = do + cLink <- withNewTestChatCfg ps testCfgSlow "alice" aliceProfile $ \alice -> do + alice ##> "/ad" + getContactLink alice True + withNewTestChatCfg ps testCfgSlow "bob" bobProfile $ \bob -> do + threadDelay 100000 + + bob ##> ("/c " <> cLink) + bob <## "connection request sent!" + + bob ##> ("/_connect plan 1 " <> cLink) + bob <## "contact address: connecting, allowed to reconnect" + + let cLinkSchema2 = linkAnotherSchema cLink + bob ##> ("/_connect plan 1 " <> cLinkSchema2) + bob <## "contact address: connecting, allowed to reconnect" + + threadDelay 100000 + withTestChatCfg ps testCfgSlow "alice" $ \alice -> do alice <## "Your address is active! To show: /sa" alice <## "bob (Bob) wants to connect to you!" alice <## "to accept: /ac bob" alice <## "to reject: /rc bob (the sender will NOT be notified)" alice ##> "/ac bob" alice <## "bob (Bob): accepting contact request..." - withTestChat tmp "bob" $ \bob -> do + withTestChatCfg ps testCfgSlow "bob" $ \bob -> do threadDelay 500000 bob @@@ [("@alice", "")] bob ##> ("/_connect plan 1 " <> cLink) @@ -699,7 +1038,7 @@ testPlanAddressConnecting tmp = do bob ##> ("/c " <> cLink) bob <## "contact address: connecting to contact alice" -testPlanAddressContactDeletedReconnected :: HasCallStack => FilePath -> IO () +testPlanAddressContactDeletedReconnected :: HasCallStack => TestParams -> IO () testPlanAddressContactDeletedReconnected = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -709,7 +1048,7 @@ testPlanAddressContactDeletedReconnected = bob ##> ("/c " <> cLink) alice <#? bob alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -740,7 +1079,7 @@ testPlanAddressContactDeletedReconnected = alice <## "to accept: /ac bob" alice <## "to reject: /rc bob (the sender will NOT be notified)" alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice_1 (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -762,7 +1101,7 @@ testPlanAddressContactDeletedReconnected = bob <## "contact address: known contact alice_1" bob <## "use @alice_1 <message> to send messages" -testPlanAddressContactViaAddress :: HasCallStack => FilePath -> IO () +testPlanAddressContactViaAddress :: HasCallStack => TestParams -> IO () testPlanAddressContactViaAddress = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -796,16 +1135,20 @@ testPlanAddressContactViaAddress = bob ##> ("/c " <> cLink) connecting alice bob - bob ##> "/_delete @2 notify=off" + bob ##> "/delete @alice" bob <## "alice: contact is deleted" - alice ##> "/_delete @2 notify=off" + alice ##> "/delete @bob" alice <## "bob: contact is deleted" void $ withCCUser bob $ \user -> withCCTransaction bob $ \db -> runExceptT $ createContact db user profile bob @@@ [("@alice", "")] -- GUI api +#if defined(dbPostgres) + bob ##> "/_connect contact 1 4" +#else bob ##> "/_connect contact 1 2" +#endif connecting alice bob where connecting alice bob = do @@ -814,7 +1157,7 @@ testPlanAddressContactViaAddress = alice <## "to accept: /ac bob" alice <## "to reject: /rc bob (the sender will NOT be notified)" alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -822,7 +1165,7 @@ testPlanAddressContactViaAddress = alice <##> bob bob @@@ [("@alice", "hey")] -testConnectIncognitoInvitationLink :: HasCallStack => FilePath -> IO () +testConnectIncognitoInvitationLink :: HasCallStack => TestParams -> IO () testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do alice ##> "/connect incognito" @@ -896,7 +1239,7 @@ testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfi (bob </) bob `hasContactProfiles` ["bob"] -testConnectIncognitoContactAddress :: HasCallStack => FilePath -> IO () +testConnectIncognitoContactAddress :: HasCallStack => TestParams -> IO () testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $ \alice bob -> do alice ##> "/ad" @@ -908,7 +1251,7 @@ testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $ alice <## ("to accept: /ac " <> bobIncognito) alice <## ("to reject: /rc " <> bobIncognito <> " (the sender will NOT be notified)") alice ##> ("/ac " <> bobIncognito) - alice <## (bobIncognito <> ": accepting contact request...") + alice <## (bobIncognito <> ": accepting contact request, you can send messages to contact") _ <- getTermLine bob concurrentlyN_ [ do @@ -934,15 +1277,16 @@ testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $ (bob </) bob `hasContactProfiles` ["bob"] -testAcceptContactRequestIncognito :: HasCallStack => FilePath -> IO () +testAcceptContactRequestIncognito :: HasCallStack => TestParams -> IO () testAcceptContactRequestIncognito = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do alice ##> "/ad" cLink <- getContactLink alice True + -- GUI /_accept api bob ##> ("/c " <> cLink) alice <#? bob - alice ##> "/accept incognito bob" - alice <## "bob (Bob): accepting contact request..." + alice ##> "/_accept incognito=on 1" + alice <## "bob (Bob): accepting contact request, you can send messages to contact" aliceIncognitoBob <- getTermLine alice concurrentlyN_ [ bob <## (aliceIncognitoBob <> ": contact is connected"), @@ -966,11 +1310,11 @@ testAcceptContactRequestIncognito = testChat3 aliceProfile bobProfile cathProfil alice ##> "/contacts" (alice </) alice `hasContactProfiles` ["alice"] - -- /_accept api + -- terminal /accept api cath ##> ("/c " <> cLink) alice <#? cath - alice ##> "/_accept incognito=on 1" - alice <## "cath (Catherine): accepting contact request..." + alice ##> "/accept incognito cath" + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" aliceIncognitoCath <- getTermLine alice concurrentlyN_ [ cath <## (aliceIncognitoCath <> ": contact is connected"), @@ -981,7 +1325,7 @@ testAcceptContactRequestIncognito = testChat3 aliceProfile bobProfile cathProfil alice `hasContactProfiles` ["alice", "cath", T.pack aliceIncognitoCath] cath `hasContactProfiles` ["cath", T.pack aliceIncognitoCath] -testSetConnectionIncognito :: HasCallStack => FilePath -> IO () +testSetConnectionIncognito :: HasCallStack => TestParams -> IO () testSetConnectionIncognito = testChat2 aliceProfile bobProfile $ \alice bob -> do alice ##> "/connect" @@ -1004,7 +1348,7 @@ testSetConnectionIncognito = testChat2 aliceProfile bobProfile $ alice `hasContactProfiles` ["alice", "bob", T.pack aliceIncognito] bob `hasContactProfiles` ["bob", T.pack aliceIncognito] -testResetConnectionIncognito :: HasCallStack => FilePath -> IO () +testResetConnectionIncognito :: HasCallStack => TestParams -> IO () testResetConnectionIncognito = testChat2 aliceProfile bobProfile $ \alice bob -> do alice ##> "/_connect 1 incognito=on" @@ -1020,21 +1364,42 @@ testResetConnectionIncognito = testChat2 aliceProfile bobProfile $ alice `hasContactProfiles` ["alice", "bob"] bob `hasContactProfiles` ["alice", "bob"] -testSetConnectionIncognitoProhibitedDuringNegotiation :: HasCallStack => FilePath -> IO () -testSetConnectionIncognitoProhibitedDuringNegotiation tmp = do - inv <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do +testSetConnectionIncognitoProhibitedDuringNegotiation :: HasCallStack => TestParams -> IO () +testSetConnectionIncognitoProhibitedDuringNegotiation ps = do + inv <- withNewTestChat ps "alice" aliceProfile $ \alice -> do threadDelay 250000 alice ##> "/connect" getInvitation alice - withNewTestChat tmp "bob" bobProfile $ \bob -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do threadDelay 250000 bob ##> ("/c " <> inv) bob <## "confirmation sent!" - withTestChat tmp "alice" $ \alice -> do + withTestChat ps "alice" $ \alice -> do + threadDelay 250000 + alice <## "bob (Bob): contact is connected" + alice ##> "/_set incognito :1 on" + alice <## "chat db error: SEPendingConnectionNotFound {connId = 1}" + withTestChat ps "bob" $ \bob -> do + bob <## "alice (Alice): contact is connected" + alice <##> bob + alice `hasContactProfiles` ["alice", "bob"] + bob `hasContactProfiles` ["alice", "bob"] + +testSetConnectionIncognitoProhibitedDuringNegotiationSlow :: HasCallStack => TestParams -> IO () +testSetConnectionIncognitoProhibitedDuringNegotiationSlow ps = do + inv <- withNewTestChatCfg ps testCfgSlow "alice" aliceProfile $ \alice -> do + threadDelay 250000 + alice ##> "/connect" + getInvitation alice + withNewTestChatCfg ps testCfgSlow "bob" bobProfile $ \bob -> do + threadDelay 250000 + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + withTestChatCfg ps testCfgSlow "alice" $ \alice -> do threadDelay 250000 alice ##> "/_set incognito :1 on" alice <## "chat db error: SEPendingConnectionNotFound {connId = 1}" - withTestChat tmp "bob" $ \bob -> do + withTestChatCfg ps testCfgSlow "bob" $ \bob -> do concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -1042,7 +1407,7 @@ testSetConnectionIncognitoProhibitedDuringNegotiation tmp = do alice `hasContactProfiles` ["alice", "bob"] bob `hasContactProfiles` ["alice", "bob"] -testConnectionIncognitoUnchangedErrors :: HasCallStack => FilePath -> IO () +testConnectionIncognitoUnchangedErrors :: HasCallStack => TestParams -> IO () testConnectionIncognitoUnchangedErrors = testChat2 aliceProfile bobProfile $ \alice bob -> do alice ##> "/connect" @@ -1066,7 +1431,7 @@ testConnectionIncognitoUnchangedErrors = testChat2 aliceProfile bobProfile $ alice `hasContactProfiles` ["alice", "bob"] bob `hasContactProfiles` ["alice", "bob"] -testSetResetSetConnectionIncognito :: HasCallStack => FilePath -> IO () +testSetResetSetConnectionIncognito :: HasCallStack => TestParams -> IO () testSetResetSetConnectionIncognito = testChat2 aliceProfile bobProfile $ \alice bob -> do alice ##> "/_connect 1 incognito=off" @@ -1093,9 +1458,9 @@ testSetResetSetConnectionIncognito = testChat2 aliceProfile bobProfile $ alice `hasContactProfiles` ["alice", "bob", T.pack aliceIncognito] bob `hasContactProfiles` ["bob", T.pack aliceIncognito] -testJoinGroupIncognito :: HasCallStack => FilePath -> IO () +testJoinGroupIncognito :: HasCallStack => TestParams -> IO () testJoinGroupIncognito = - testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $ + testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do -- non incognito connections connectUsers alice bob @@ -1170,13 +1535,13 @@ testJoinGroupIncognito = dan <### [ ConsoleString $ "#secret_club: member " <> cathIncognito <> " is connected", "#secret_club: member bob_1 (Bob) is connected", - "contact bob_1 is merged into bob", + "contact and member are merged: bob, #secret_club bob_1", "use @bob <message> to send messages" ], do bob <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)" bob <## "#secret_club: new member dan_1 is connected" - bob <## "contact dan_1 is merged into dan" + bob <## "contact and member are merged: dan, #secret_club dan_1" bob <## "use @dan <message> to send messages", do cath <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)" @@ -1207,17 +1572,7 @@ testJoinGroupIncognito = bob <# "#secret_club dan> how is it going?", cath ?<# "#secret_club dan_1> how is it going?" ] - -- cath and bob can send messages via new direct connection, cath is incognito - bob #> ("@" <> cathIncognito <> " hi, I'm bob") - cath ?<# "bob_1> hi, I'm bob" - cath ?#> "@bob_1 hey, I'm incognito" - bob <# (cathIncognito <> "> hey, I'm incognito") - -- cath and dan can send messages via new direct connection, cath is incognito - dan #> ("@" <> cathIncognito <> " hi, I'm dan") - cath ?<# "dan_1> hi, I'm dan" - cath ?#> "@dan_1 hey, I'm incognito" - dan <# (cathIncognito <> "> hey, I'm incognito") - -- non incognito connections are separate + -- non incognito direct connections are separate bob <##> cath dan <##> cath -- list groups @@ -1276,13 +1631,8 @@ testJoinGroupIncognito = ] cath ##> "#secret_club hello" cath <## "you are no longer a member of the group" - -- cath can still message members directly - bob #> ("@" <> cathIncognito <> " I removed you from group") - cath ?<# "bob_1> I removed you from group" - cath ?#> "@bob_1 ok" - bob <# (cathIncognito <> "> ok") -testCantInviteContactIncognito :: HasCallStack => FilePath -> IO () +testCantInviteContactIncognito :: HasCallStack => TestParams -> IO () testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $ \alice bob -> do -- alice connected incognito to bob @@ -1306,7 +1656,7 @@ testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $ -- bob doesn't receive invitation (bob </) -testCantSeeGlobalPrefsUpdateIncognito :: HasCallStack => FilePath -> IO () +testCantSeeGlobalPrefsUpdateIncognito :: HasCallStack => TestParams -> IO () testCantSeeGlobalPrefsUpdateIncognito = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do alice ##> "/c i" @@ -1357,7 +1707,7 @@ testCantSeeGlobalPrefsUpdateIncognito = testChat3 aliceProfile bobProfile cathPr cath <## "alice updated preferences for you:" cath <## "Full deletion: off (you allow: default (no), contact allows: yes)" -testDeleteContactThenGroupDeletesIncognitoProfile :: HasCallStack => FilePath -> IO () +testDeleteContactThenGroupDeletesIncognitoProfile :: HasCallStack => TestParams -> IO () testDeleteContactThenGroupDeletesIncognitoProfile = testChat2 aliceProfile bobProfile $ \alice bob -> do -- bob connects incognito to alice @@ -1409,7 +1759,7 @@ testDeleteContactThenGroupDeletesIncognitoProfile = testChat2 aliceProfile bobPr bob <## "#team: you deleted the group" bob `hasContactProfiles` ["bob"] -testDeleteGroupThenContactDeletesIncognitoProfile :: HasCallStack => FilePath -> IO () +testDeleteGroupThenContactDeletesIncognitoProfile :: HasCallStack => TestParams -> IO () testDeleteGroupThenContactDeletesIncognitoProfile = testChat2 aliceProfile bobProfile $ \alice bob -> do -- bob connects incognito to alice @@ -1461,7 +1811,7 @@ testDeleteGroupThenContactDeletesIncognitoProfile = testChat2 aliceProfile bobPr (bob </) bob `hasContactProfiles` ["bob"] -testSetAlias :: HasCallStack => FilePath -> IO () +testSetAlias :: HasCallStack => TestParams -> IO () testSetAlias = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob @@ -1472,7 +1822,165 @@ testSetAlias = testChat2 aliceProfile bobProfile $ alice ##> "/contacts" alice <## "bob (Bob)" -testSetConnectionAlias :: HasCallStack => FilePath -> IO () +testChangePCCUser :: HasCallStack => TestParams -> IO () +testChangePCCUser = testChat2 aliceProfile bobProfile $ + \alice bob -> do + -- Create a new invite + alice ##> "/connect" + _ <- getInvitation alice + -- Create new user and go back to original user + alice ##> "/create user alisa" + showActiveUser alice "alisa" + alice ##> "/create user alisa2" + showActiveUser alice "alisa2" + alice ##> "/user alice" + showActiveUser alice "alice (Alice)" + -- Change connection to newly created user + alice ##> "/_set conn user :1 2" + alice <## "connection 1 changed from user alice to user alisa, new link:" + alice <## "" + _ <- getTermLine alice + alice <## "" + alice ##> "/user alisa" + showActiveUser alice "alisa" + -- Change connection back to other user + alice ##> "/_set conn user :1 3" + alice <## "connection 1 changed from user alisa to user alisa2, new link:" + alice <## "" + inv <- getTermLine alice + alice <## "" + alice ##> "/user alisa2" + showActiveUser alice "alisa2" + -- Connect + bob ##> ("/connect " <> inv) + bob <## "confirmation sent!" + concurrently_ + (alice <## "bob (Bob): contact is connected") + (bob <## "alisa2: contact is connected") + alice <##> bob + +testChangePCCUserFromIncognito :: HasCallStack => TestParams -> IO () +testChangePCCUserFromIncognito = testChat2 aliceProfile bobProfile $ + \alice bob -> do + -- Create a new invite and set as incognito + alice ##> "/connect" + _ <- getInvitation alice + alice ##> "/_set incognito :1 on" + alice <## "connection 1 changed to incognito" + -- Create new user and go back to original user + alice ##> "/create user alisa" + showActiveUser alice "alisa" + alice ##> "/user alice" + showActiveUser alice "alice (Alice)" + -- Change connection to newly created user + alice ##> "/_set conn user :1 2" + alice <## "connection 1 changed from user alice to user alisa, new link:" + alice <## "" + _ <- getTermLine alice + alice <## "" + alice `hasContactProfiles` ["alice"] + alice ##> "/user alisa" + showActiveUser alice "alisa" + -- Change connection back to initial user + alice ##> "/_set conn user :1 1" + alice <## "connection 1 changed from user alisa to user alice, new link:" + alice <## "" + inv <- getTermLine alice + alice <## "" + alice ##> "/user alice" + showActiveUser alice "alice (Alice)" + -- Connect + bob ##> ("/connect " <> inv) + bob <## "confirmation sent!" + concurrently_ + (alice <## "bob (Bob): contact is connected") + (bob <## "alice (Alice): contact is connected") + alice <##> bob + +testChangePCCUserAndThenIncognito :: HasCallStack => TestParams -> IO () +testChangePCCUserAndThenIncognito = testChat2 aliceProfile bobProfile $ + \alice bob -> do + -- Create a new invite and set as incognito + alice ##> "/connect" + _ <- getInvitation alice + -- Create new user and go back to original user + alice ##> "/create user alisa" + showActiveUser alice "alisa" + alice ##> "/user alice" + showActiveUser alice "alice (Alice)" + -- Change connection to newly created user + alice ##> "/_set conn user :1 2" + alice <## "connection 1 changed from user alice to user alisa, new link:" + alice <## "" + inv <- getTermLine alice + alice <## "" + alice ##> "/user alisa" + showActiveUser alice "alisa" + -- Change connection to incognito and make sure it's attached to the newly created user profile + alice ##> "/_set incognito :1 on" + alice <## "connection 1 changed to incognito" + bob ##> ("/connect " <> inv) + bob <## "confirmation sent!" + alisaIncognito <- getTermLine alice + concurrentlyN_ + [ bob <## (alisaIncognito <> ": contact is connected"), + do + alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> alisaIncognito) + alice <## ("use /i bob to print out this incognito profile again") + ] + alice ?#> "@bob hi" + bob <# (alisaIncognito <> "> hi") + bob #> ("@" <> alisaIncognito <> " hey") + alice ?<# "bob> hey" + +testChangePCCUserDiffSrv :: HasCallStack => TestParams -> IO () +testChangePCCUserDiffSrv ps = do + withSmpServer' serverCfg' $ do + withNewTestChatCfgOpts ps testCfg testOpts "alice" aliceProfile $ \alice -> do + withNewTestChatCfgOpts ps testCfg testOpts "bob" bobProfile $ \bob -> do + -- Create a new invite + alice ##> "/connect" + _ <- getInvitation alice + alice ##> "/_set incognito :1 on" + alice <## "connection 1 changed to incognito" + -- Create new user with different servers + alice ##> "/create user alisa" + showActiveUser alice "alisa" + alice ##> "/smp" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001" + alice #$> ("/smp smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@127.0.0.1:7003", id, "ok") + alice ##> "/smp" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@127.0.0.1:7003" + alice ##> "/user alice" + showActiveUser alice "alice (Alice)" + -- Change connection to newly created user and use the newly created connection + alice ##> "/_set conn user :1 2" + alice <## "connection 1 changed from user alice to user alisa, new link:" + alice <## "" + inv <- getTermLine alice + alice <## "" + alice `hasContactProfiles` ["alice"] + alice ##> "/user alisa" + showActiveUser alice "alisa" + -- Connect + bob ##> ("/connect " <> inv) + bob <## "confirmation sent!" + concurrently_ + (alice <## "bob (Bob): contact is connected") + (bob <## "alisa: contact is connected") + alice <##> bob + where + serverCfg' = + smpServerCfg + { transports = [("7003", transport @TLS, False), ("7002", transport @TLS, False)], + msgQueueQuota = 2 + } + +testSetConnectionAlias :: HasCallStack => TestParams -> IO () testSetConnectionAlias = testChat2 aliceProfile bobProfile $ \alice bob -> do alice ##> "/c" @@ -1490,9 +1998,23 @@ testSetConnectionAlias = testChat2 aliceProfile bobProfile $ alice ##> "/contacts" alice <## "bob (Bob) (alias: friend)" -testSetContactPrefs :: HasCallStack => FilePath -> IO () -testSetContactPrefs = testChat2 aliceProfile bobProfile $ +testSetGroupAlias :: HasCallStack => TestParams -> IO () +testSetGroupAlias = testChat2 aliceProfile bobProfile $ \alice bob -> do + createGroup2 "team" alice bob + threadDelay 1500000 + alice ##> "/_set alias #1 friends" + alice <## "group #team alias updated: friends" + alice ##> "/groups" + alice <## "#team (2 members) (alias: friends)" + alice ##> "/_set alias #1" + alice <## "group #team alias removed" + alice ##> "/groups" + alice <## "#team (2 members)" + +testSetContactPrefs :: HasCallStack => TestParams -> IO () +testSetContactPrefs = testChat2 aliceProfile bobProfile $ + \alice bob -> withXFTPServer $ do alice #$> ("/_files_folder ./tests/tmp/alice", id, "ok") bob #$> ("/_files_folder ./tests/tmp/bob", id, "ok") createDirectoryIfMissing True "./tests/tmp/alice" @@ -1508,10 +2030,10 @@ testSetContactPrefs = testChat2 aliceProfile bobProfile $ alice ##> "/_set prefs @2 {}" alice <## "your preferences for bob did not change" (bob </) - let startFeatures = [(0, "Disappearing messages: allowed"), (0, "Full deletion: off"), (0, "Message reactions: enabled"), (0, "Voice messages: off"), (0, "Audio/video calls: enabled")] + let startFeatures = [(0, e2eeInfoPQStr), (0, "Disappearing messages: allowed"), (0, "Full deletion: off"), (0, "Message reactions: enabled"), (0, "Voice messages: off"), (0, "Audio/video calls: enabled")] alice #$> ("/_get chat @2 count=100", chat, startFeatures) bob #$> ("/_get chat @2 count=100", chat, startFeatures) - let sendVoice = "/_send @2 json {\"filePath\": \"test.txt\", \"msgContent\": {\"type\": \"voice\", \"text\": \"\", \"duration\": 10}}" + let sendVoice = "/_send @2 json [{\"filePath\": \"test.txt\", \"msgContent\": {\"type\": \"voice\", \"text\": \"\", \"duration\": 10}}]" voiceNotAllowed = "bad chat command: feature not allowed Voice messages" alice ##> sendVoice alice <## voiceNotAllowed @@ -1527,15 +2049,24 @@ testSetContactPrefs = testChat2 aliceProfile bobProfile $ bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you")]) alice ##> sendVoice alice <## voiceNotAllowed + + -- sending voice message allowed bob ##> sendVoice bob <# "@alice voice message (00:10)" bob <# "/f @alice test.txt" - bob <## "completed sending file 1 (test.txt) to alice" + bob <## "use /fc 1 to cancel sending" alice <# "bob> voice message (00:10)" alice <# "bob> sends file test.txt (11 bytes / 11 bytes)" - alice <## "started receiving file 1 (test.txt) from bob" + alice <## "use /fr 1 [<dir>/ | <path>] to receive it" + bob <## "completed uploading file 1 (test.txt) for alice" + alice ##> "/fr 1" + alice + <### [ "saving file 1 from bob to test_1.txt", + "started receiving file 1 (test.txt) from bob" + ] alice <## "completed receiving file 1 (test.txt) from bob" (bob </) + -- alice ##> "/_profile 1 {\"displayName\": \"alice\", \"fullName\": \"Alice\", \"preferences\": {\"voice\": {\"allow\": \"no\"}}}" alice ##> "/set voice no" alice <## "updated preferences:" @@ -1558,7 +2089,7 @@ testSetContactPrefs = testChat2 aliceProfile bobProfile $ alice <## "contact bob removed full name" alice <## "bob updated preferences for you:" alice <## "Voice messages: enabled (you allow: yes, contact allows: yes)" - alice #$> ("/_get chat @2 count=100", chat, startFeatures <> [(1, "Voice messages: enabled for contact"), (0, "voice message (00:10)"), (1, "Voice messages: off"), (0, "Voice messages: enabled")]) + alice #$> ("/_get chat @2 count=100", chat, startFeatures <> [(1, "Voice messages: enabled for contact"), (0, "voice message (00:10)"), (1, "Voice messages: off"), (0, "updated profile"), (0, "Voice messages: enabled")]) (alice </) bob ##> "/_set prefs @2 {}" bob <## "your preferences for alice did not change" @@ -1569,12 +2100,12 @@ testSetContactPrefs = testChat2 aliceProfile bobProfile $ alice ##> "/_set prefs @2 {\"voice\": {\"allow\": \"no\"}}" alice <## "you updated preferences for bob:" alice <## "Voice messages: off (you allow: no, contact allows: yes)" - alice #$> ("/_get chat @2 count=100", chat, startFeatures <> [(1, "Voice messages: enabled for contact"), (0, "voice message (00:10)"), (1, "Voice messages: off"), (0, "Voice messages: enabled"), (1, "Voice messages: off")]) + alice #$> ("/_get chat @2 count=100", chat, startFeatures <> [(1, "Voice messages: enabled for contact"), (0, "voice message (00:10)"), (1, "Voice messages: off"), (0, "updated profile"), (0, "Voice messages: enabled"), (1, "Voice messages: off")]) bob <## "alice updated preferences for you:" bob <## "Voice messages: off (you allow: default (yes), contact allows: no)" bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you"), (1, "voice message (00:10)"), (0, "Voice messages: off"), (1, "Voice messages: enabled"), (0, "Voice messages: off")]) -testFeatureOffers :: HasCallStack => FilePath -> IO () +testFeatureOffers :: HasCallStack => TestParams -> IO () testFeatureOffers = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob @@ -1593,18 +2124,18 @@ testFeatureOffers = testChat2 aliceProfile bobProfile $ bob <## "Full deletion: off (you allow: default (no), contact allows: no)" bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "offered Full deletion"), (0, "cancelled Full deletion")]) -testUpdateGroupPrefs :: HasCallStack => FilePath -> IO () +testUpdateGroupPrefs :: HasCallStack => TestParams -> IO () testUpdateGroupPrefs = testChat2 aliceProfile bobProfile $ \alice bob -> do createGroup2 "team" alice bob - alice #$> ("/_get chat #1 count=100", chat, [(0, "connected")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected")]) threadDelay 500000 bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected")]) alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"on\"}, \"directMessages\": {\"enable\": \"on\"}, \"history\": {\"enable\": \"on\"}}}" alice <## "updated group preferences:" alice <## "Full deletion: on" - alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Full deletion: on")]) bob <## "alice updated group #team:" bob <## "updated group preferences:" bob <## "Full deletion: on" @@ -1614,7 +2145,7 @@ testUpdateGroupPrefs = alice <## "updated group preferences:" alice <## "Full deletion: off" alice <## "Voice messages: off" - alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off")]) bob <## "alice updated group #team:" bob <## "updated group preferences:" bob <## "Full deletion: off" @@ -1624,7 +2155,7 @@ testUpdateGroupPrefs = alice ##> "/set voice #team on" alice <## "updated group preferences:" alice <## "Voice messages: on" - alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on")]) bob <## "alice updated group #team:" bob <## "updated group preferences:" bob <## "Voice messages: on" @@ -1634,17 +2165,17 @@ testUpdateGroupPrefs = alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}, \"directMessages\": {\"enable\": \"on\"}, \"history\": {\"enable\": \"on\"}}}" -- no update threadDelay 500000 - alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on")]) alice #> "#team hey" bob <# "#team alice> hey" threadDelay 1000000 bob #> "#team hi" alice <# "#team bob> hi" threadDelay 500000 - alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on"), (1, "hey"), (0, "hi")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on"), (1, "hey"), (0, "hi")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on"), (0, "Full deletion: off"), (0, "Voice messages: off"), (0, "Voice messages: on"), (0, "hey"), (1, "hi")]) -testAllowFullDeletionContact :: HasCallStack => FilePath -> IO () +testAllowFullDeletionContact :: HasCallStack => TestParams -> IO () testAllowFullDeletionContact = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -1662,12 +2193,12 @@ testAllowFullDeletionContact = alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hi"), (1, "Full deletion: enabled for contact")]) bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hi"), (0, "Full deletion: enabled for you")]) -testAllowFullDeletionGroup :: HasCallStack => FilePath -> IO () +testAllowFullDeletionGroup :: HasCallStack => TestParams -> IO () testAllowFullDeletionGroup = testChat2 aliceProfile bobProfile $ \alice bob -> do createGroup2 "team" alice bob - threadDelay 1000000 + threadDelay 1500000 alice #> "#team hi" bob <# "#team alice> hi" threadDelay 1000000 @@ -1681,16 +2212,16 @@ testAllowFullDeletionGroup = bob <## "alice updated group #team:" bob <## "updated group preferences:" bob <## "Full deletion: on" - alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "hi"), (0, "hey"), (1, "Full deletion: on")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "hi"), (0, "hey"), (1, "Full deletion: on")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "hi"), (1, "hey"), (0, "Full deletion: on")]) bob #$> ("/_delete item #1 " <> msgItemId <> " broadcast", id, "message deleted") alice <# "#team bob> [deleted] hey" - alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "hi"), (1, "Full deletion: on")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "hi"), (1, "Full deletion: on")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "hi"), (0, "Full deletion: on")]) -testProhibitDirectMessages :: HasCallStack => FilePath -> IO () +testProhibitDirectMessages :: HasCallStack => TestParams -> IO () testProhibitDirectMessages = - testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $ + testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do createGroup3 "team" alice bob cath threadDelay 1000000 @@ -1706,7 +2237,7 @@ testProhibitDirectMessages = alice #> "@cath hello again" cath <# "alice> hello again" bob ##> "@cath hello again" - bob <## "direct messages to indirect contact cath are prohibited" + bob <## "bad chat command: direct messages not allowed" (cath </) connectUsers cath dan addMember "team" cath dan GRMember @@ -1727,14 +2258,14 @@ testProhibitDirectMessages = bob <## "#team: new member dan is connected" ] alice ##> "@dan hi" - alice <## "direct messages to indirect contact dan are prohibited" + alice <## "bad chat command: direct messages not allowed" bob ##> "@dan hi" - bob <## "direct messages to indirect contact dan are prohibited" + bob <## "bad chat command: direct messages not allowed" (dan </) dan ##> "@alice hi" - dan <## "direct messages to indirect contact alice are prohibited" + dan <## "bad chat command: direct messages not allowed" dan ##> "@bob hi" - dan <## "direct messages to indirect contact bob are prohibited" + dan <## "bad chat command: direct messages not allowed" dan #> "@cath hi" cath <# "dan> hi" cath #> "@dan hi" @@ -1746,7 +2277,7 @@ testProhibitDirectMessages = cc <## "updated group preferences:" cc <## "Direct messages: off" -testEnableTimedMessagesContact :: HasCallStack => FilePath -> IO () +testEnableTimedMessagesContact :: HasCallStack => TestParams -> IO () testEnableTimedMessagesContact = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -1791,7 +2322,7 @@ testEnableTimedMessagesContact = alice <## "bob updated preferences for you:" alice <## "Disappearing messages: enabled (you allow: yes (1 week), contact allows: yes (1 week))" -testEnableTimedMessagesGroup :: HasCallStack => FilePath -> IO () +testEnableTimedMessagesGroup :: HasCallStack => TestParams -> IO () testEnableTimedMessagesGroup = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -1807,12 +2338,12 @@ testEnableTimedMessagesGroup = alice #> "#team hi" bob <# "#team alice> hi" threadDelay 500000 - alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Disappearing messages: on (1 sec)"), (1, "hi")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Disappearing messages: on (1 sec)"), (1, "hi")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Disappearing messages: on (1 sec)"), (0, "hi")]) threadDelay 1000000 alice <## "timed message deleted: hi" bob <## "timed message deleted: hi" - alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Disappearing messages: on (1 sec)")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Disappearing messages: on (1 sec)")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Disappearing messages: on (1 sec)")]) -- turn off, messages are not disappearing alice ##> "/set disappear #team off" @@ -1825,7 +2356,7 @@ testEnableTimedMessagesGroup = alice #> "#team hey" bob <# "#team alice> hey" threadDelay 1500000 - alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Disappearing messages: on (1 sec)"), (1, "Disappearing messages: off"), (1, "hey")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Disappearing messages: on (1 sec)"), (1, "Disappearing messages: off"), (1, "hey")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Disappearing messages: on (1 sec)"), (0, "Disappearing messages: off"), (0, "hey")]) -- test api alice ##> "/set disappear #team on 30s" @@ -1841,7 +2372,7 @@ testEnableTimedMessagesGroup = bob <## "updated group preferences:" bob <## "Disappearing messages: on (1 week)" -testTimedMessagesEnabledGlobally :: HasCallStack => FilePath -> IO () +testTimedMessagesEnabledGlobally :: HasCallStack => TestParams -> IO () testTimedMessagesEnabledGlobally = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -1864,3 +2395,377 @@ testTimedMessagesEnabledGlobally = bob <## "timed message deleted: hey" alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)")]) bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)")]) + +testUpdateMultipleUserPrefs :: HasCallStack => TestParams -> IO () +testUpdateMultipleUserPrefs = testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + connectUsers alice bob + alice #> "@bob hi bob" + bob <# "alice> hi bob" + + connectUsers alice cath + alice #> "@cath hi cath" + cath <# "alice> hi cath" + + alice ##> "/_profile 1 {\"displayName\": \"alice\", \"fullName\": \"Alice\", \"preferences\": {\"fullDelete\": {\"allow\": \"always\"}, \"reactions\": {\"allow\": \"no\"}, \"receipts\": {\"allow\": \"yes\", \"activated\": true}}}" + alice <## "updated preferences:" + alice <## "Full deletion allowed: always" + alice <## "Message reactions allowed: no" + + bob <## "alice updated preferences for you:" + bob <## "Full deletion: enabled for you (you allow: default (no), contact allows: always)" + bob <## "Message reactions: off (you allow: default (yes), contact allows: no)" + + cath <## "alice updated preferences for you:" + cath <## "Full deletion: enabled for you (you allow: default (no), contact allows: always)" + cath <## "Message reactions: off (you allow: default (yes), contact allows: no)" + + alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hi bob"), (1, "Full deletion: enabled for contact"), (1, "Message reactions: off")]) + alice #$> ("/_get chat @3 count=100", chat, chatFeatures <> [(1, "hi cath"), (1, "Full deletion: enabled for contact"), (1, "Message reactions: off")]) + +testGroupPrefsDirectForRole :: HasCallStack => TestParams -> IO () +testGroupPrefsDirectForRole = testChat4 aliceProfile bobProfile cathProfile danProfile $ + \alice bob cath dan -> do + createGroup3 "team" alice bob cath + threadDelay 1000000 + alice ##> "/set direct #team on owner" + alice <## "updated group preferences:" + alice <## "Direct messages: on for owners" + directForOwners bob + directForOwners cath + threadDelay 1000000 + bob ##> "@cath hello again" + bob <## "bad chat command: direct messages not allowed" + (cath </) + + connectUsers cath dan + addMember "team" cath dan GRMember + dan ##> "/j #team" + concurrentlyN_ + [ cath <## "#team: dan joined the group", + do + dan <## "#team: you joined the group" + dan + <### [ "#team: member alice (Alice) is connected", + "#team: member bob (Bob) is connected" + ], + do + alice <## "#team: cath added dan (Daniel) to the group (connecting...)" + alice <## "#team: new member dan is connected", + do + bob <## "#team: cath added dan (Daniel) to the group (connecting...)" + bob <## "#team: new member dan is connected" + ] + + -- dan cannot send direct messages to alice + dan ##> "@alice hello alice" + dan <## "bad chat command: direct messages not allowed" + (alice </) + + -- alice (owner) can send direct messages to dan + alice `send` "@dan hello dan" + alice + <### [ "member #team dan does not have direct connection, creating", + "contact for member #team dan is created", + "sent invitation to connect directly to member #team dan", + WithTime "@dan hello dan" + ] + dan + <### [ "#team alice is creating direct contact alice with you", + WithTime "alice> hello dan" + ] + dan <## "alice (Alice): you can send messages to contact" + concurrently_ + (alice <## "dan (Daniel): contact is connected") + (dan <## "alice (Alice): contact is connected") + + -- now dan can send messages to alice + dan #> "@alice hi alice" + alice <# "dan> hi alice" + where + directForOwners :: HasCallStack => TestCC -> IO () + directForOwners cc = do + cc <## "alice updated group #team:" + cc <## "updated group preferences:" + cc <## "Direct messages: on for owners" + +testGroupPrefsFilesForRole :: HasCallStack => TestParams -> IO () +testGroupPrefsFilesForRole = testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> withXFTPServer $ do + alice #$> ("/_files_folder ./tests/tmp/alice", id, "ok") + bob #$> ("/_files_folder ./tests/tmp/bob", id, "ok") + createDirectoryIfMissing True "./tests/tmp/alice" + createDirectoryIfMissing True "./tests/tmp/bob" + copyFile "./tests/fixtures/test.txt" "./tests/tmp/alice/test1.txt" + copyFile "./tests/fixtures/test.txt" "./tests/tmp/bob/test2.txt" + createGroup3 "team" alice bob cath + threadDelay 1000000 + alice ##> "/set files #team on owner" + alice <## "updated group preferences:" + alice <## "Files and media: on for owners" + filesForOwners bob + filesForOwners cath + threadDelay 1000000 + bob ##> "/f #team test2.txt" + bob <## "bad chat command: feature not allowed Files and media" + (alice </) + (cath </) + alice #> "/f #team test1.txt" + alice <## "use /fc 1 to cancel sending" + alice <## "completed uploading file 1 (test1.txt) for #team" + bob <# "#team alice> sends file test1.txt (11 bytes / 11 bytes)" + bob <## "use /fr 1 [<dir>/ | <path>] to receive it" + cath <# "#team alice> sends file test1.txt (11 bytes / 11 bytes)" + cath <## "use /fr 1 [<dir>/ | <path>] to receive it" + where + filesForOwners :: HasCallStack => TestCC -> IO () + filesForOwners cc = do + cc <## "alice updated group #team:" + cc <## "updated group preferences:" + cc <## "Files and media: on for owners" + +testGroupPrefsSimplexLinksForRole :: HasCallStack => TestParams -> IO () +testGroupPrefsSimplexLinksForRole = testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> withXFTPServer $ do + createGroup3 "team" alice bob cath + threadDelay 1000000 + alice ##> "/set links #team on owner" + alice <## "updated group preferences:" + alice <## "SimpleX links: on for owners" + linksForOwners bob + linksForOwners cath + threadDelay 1000000 + bob ##> "/c" + inv <- getInvitation bob + bob ##> ("#team \"" <> inv <> "\\ntest\"") + bob <## "bad chat command: feature not allowed SimpleX links" + bob ##> ("/_send #1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"" <> inv <> "\\ntest\"}}]") + bob <## "bad chat command: feature not allowed SimpleX links" + (alice </) + (cath </) + bob `send` ("@alice \"" <> inv <> "\\ntest\"") + bob <# ("@alice " <> inv) + bob <## "test" + alice <# ("bob> " <> inv) + alice <## "test" + bob ##> "#team <- @alice https://simplex.chat" + bob <## "bad chat command: feature not allowed SimpleX links" + alice #> ("#team " <> inv) + bob <# ("#team alice> " <> inv) + cath <# ("#team alice> " <> inv) + where + linksForOwners :: HasCallStack => TestCC -> IO () + linksForOwners cc = do + cc <## "alice updated group #team:" + cc <## "updated group preferences:" + cc <## "SimpleX links: on for owners" + +testSetUITheme :: HasCallStack => TestParams -> IO () +testSetUITheme = + testChat2 aliceProfile bobProfile $ \alice bob -> do + connectUsers alice bob + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team <name> or /create link #team" + alice #$> ("/_set theme user 1 " <> theme UCMDark, id, "ok") + alice #$> ("/_set theme @2 " <> theme UCMDark, id, "ok") + alice #$> ("/_set theme #1 " <> theme UCMDark, id, "ok") + alice ##> "/u" + userInfo alice "alice (Alice)" + alice <## ("UI themes: " <> theme UCMDark) + alice ##> "/create user alice2" + userInfo alice "alice2" + alice ##> "/u alice" + userInfo alice "alice (Alice)" + alice <## ("UI themes: " <> theme UCMDark) + alice ##> "/i @bob" + contactInfo alice + alice <## ("UI themes: " <> theme UCMDark) + alice ##> "/i #team" + groupInfo alice + alice <## ("UI themes: " <> theme UCMDark) + alice #$> ("/_set theme user 1", id, "ok") + alice #$> ("/_set theme @2", id, "ok") + alice #$> ("/_set theme #1", id, "ok") + alice ##> "/u" + userInfo alice "alice (Alice)" + alice ##> "/i @bob" + contactInfo alice + alice ##> "/i #team" + groupInfo alice + where + theme cm = T.unpack $ encodeJSON UIThemeEntityOverrides {light = Nothing, dark = Just $ UIThemeEntityOverride cm Nothing defaultUIColors} + userInfo a name = do + a <## ("user profile: " <> name) + a <## "use /p <display name> to change it" + a <## "(the updated profile will be sent to all your contacts)" + contactInfo a = do + a <## "contact ID: 2" + a <## "receiving messages via: localhost" + a <## "sending messages via: localhost" + a <## "you've shared main profile with this contact" + a <## "connection not verified, use /code command to see security code" + a <## "quantum resistant end-to-end encryption" + a <## ("peer chat protocol version range: (Version 1, " <> show currentChatVersion <> ")") + groupInfo a = do + a <## "group ID: 1" + a <## "current members: 1" + +testShortLinkInvitation :: HasCallStack => TestParams -> IO () +testShortLinkInvitation = + testChat2 aliceProfile bobProfile $ \alice bob -> do + alice ##> "/c short" + inv <- getShortInvitation alice + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + concurrently_ + (alice <## "bob (Bob): contact is connected") + (bob <## "alice (Alice): contact is connected") + alice #> "@bob hi" + bob <# "alice> hi" + bob #> "@alice hey" + alice <# "bob> hey" + +testPlanShortLinkInvitation :: HasCallStack => TestParams -> IO () +testPlanShortLinkInvitation = + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + alice ##> "/c short" + inv <- getShortInvitation alice + alice ##> ("/_connect plan 1 " <> inv) + alice <## "invitation link: own link" + alice ##> ("/_connect plan 1 " <> slSimplexScheme inv) + alice <## "invitation link: own link" + bob ##> ("/_connect plan 1 " <> inv) + bob <## "invitation link: ok to connect" + -- nobody else can connect + cath ##> ("/_connect plan 1 " <> inv) + cath <##. "error: connection authorization failed" + cath ##> ("/c " <> inv) + cath <##. "error: connection authorization failed" + -- bob can retry "plan" + bob ##> ("/_connect plan 1 " <> inv) + bob <## "invitation link: ok to connect" + -- with simplex: scheme too + bob ##> ("/_connect plan 1 " <> slSimplexScheme inv) + bob <## "invitation link: ok to connect" + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + concurrently_ + (alice <## "bob (Bob): contact is connected") + (bob <## "alice (Alice): contact is connected") + alice #> "@bob hi" + bob <# "alice> hi" + bob #> "@alice hey" + alice <# "bob> hey" + bob ##> ("/_connect plan 1 " <> inv) + bob <##. "error: connection authorization failed" + alice ##> ("/_connect plan 1 " <> inv) + alice <##. "error: connection authorization failed" -- short_link_inv and conn_req_inv are removed after connection + +slSimplexScheme :: String -> String +slSimplexScheme sl = T.unpack $ T.replace "https://localhost/" "simplex:/" (T.pack sl) <> "?h=localhost" + +testShortLinkContactAddress :: HasCallStack => TestParams -> IO () +testShortLinkContactAddress = + testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do + alice ##> "/ad short" + (shortLink, fullLink) <- getShortContactLink alice True + alice ##> ("/_connect plan 1 " <> shortLink) + alice <## "contact address: own address" + alice ##> ("/_connect plan 1 " <> slSimplexScheme shortLink) + alice <## "contact address: own address" + alice ##> ("/_connect plan 1 " <> fullLink) + alice <## "contact address: own address" + (alice, bob) `connectVia` shortLink + bob ##> ("/_connect plan 1 " <> slSimplexScheme shortLink) + bob <## "contact address: known contact alice" + bob <## "use @alice <message> to send messages" + (alice, cath) `connectVia` slSimplexScheme shortLink + cath ##> ("/_connect plan 1 " <> shortLink) + cath <## "contact address: known contact alice" + cath <## "use @alice <message> to send messages" + (alice, dan) `connectVia` fullLink + where + (alice, cc) `connectVia` cLink = do + name <- userName cc + sName <- showName cc + cc ##> ("/_connect plan 1 " <> cLink) + cc <## "contact address: ok to connect" + cc ##> ("/c " <> cLink) + alice <#? cc + alice ##> ("/ac " <> name) + alice <## (sName <> ": accepting contact request, you can send messages to contact") + concurrently_ + (cc <## "alice (Alice): contact is connected") + (alice <## (sName <> ": contact is connected")) + cc ##> ("/_connect plan 1 " <> cLink) + cc <## "contact address: known contact alice" + cc <## "use @alice <message> to send messages" + +testShortLinkJoinGroup :: HasCallStack => TestParams -> IO () +testShortLinkJoinGroup = + testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do + threadDelay 100000 + alice ##> "/ad short" -- create the address to test that it can co-exist with group link + _ <- getShortContactLink alice True + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team <name> or /create link #team" + alice ##> "/create link #team short" + (shortLink, fullLink) <- getShortGroupLink alice "team" GRMember True + alice ##> ("/_connect plan 1 " <> shortLink) + alice <## "group link: own link for group #team" + alice ##> ("/_connect plan 1 " <> slSimplexScheme shortLink) + alice <## "group link: own link for group #team" + alice ##> ("/_connect plan 1 " <> fullLink) + alice <## "group link: own link for group #team" + joinGroup alice bob shortLink + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "group link: known group #team" + bob <## "use #team <message> to send messages" + bob ##> ("/_connect plan 1 " <> slSimplexScheme shortLink) + bob <## "group link: known group #team" + bob <## "use #team <message> to send messages" + joinGroup alice cath $ slSimplexScheme shortLink + concurrentlyN_ + [ do + bob <## "#team: alice added cath (Catherine) to the group (connecting...)" + bob <## "#team: new member cath is connected", + cath <## "#team: member bob (Bob) is connected" + ] + cath ##> ("/_connect plan 1 " <> slSimplexScheme shortLink) + cath <## "group link: known group #team" + cath <## "use #team <message> to send messages" + cath ##> ("/_connect plan 1 " <> shortLink) + cath <## "group link: known group #team" + cath <## "use #team <message> to send messages" + joinGroup alice dan fullLink + concurrentlyN_ + [ do + bob <## "#team: alice added dan (Daniel) to the group (connecting...)" + bob <## "#team: new member dan is connected", + do + cath <## "#team: alice added dan (Daniel) to the group (connecting...)" + cath <## "#team: new member dan is connected", + do + dan <## "#team: member bob (Bob) is connected" + dan <## "#team: member cath (Catherine) is connected" + ] + dan ##> ("/_connect plan 1 " <> fullLink) + dan <## "group link: known group #team" + dan <## "use #team <message> to send messages" + where + joinGroup alice cc link = do + name <- userName cc + sName <- showName cc + cc ##> ("/_connect plan 1 " <> link) + cc <## "group link: ok to connect" + cc ##> ("/c " <> link) + cc <## "connection request sent!" + alice <## (sName <> ": accepting request to join group #team...") + concurrentlyN_ + [ alice <## ("#team: " <> name <> " joined the group"), + do + cc <## "#team: joining the group..." + cc <## "#team: you joined the group" + ] diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 65fa3bc4c1..3ae3f61ca7 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -1,39 +1,57 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RankNTypes #-} module ChatTests.Utils where import ChatClient +import ChatTests.DBUtils import Control.Concurrent (threadDelay) -import Control.Concurrent.Async (concurrently_) +import Control.Concurrent.Async (concurrently_, mapConcurrently_) import Control.Concurrent.STM import Control.Monad (unless, when) +import Control.Monad.Except (runExceptT) +import Data.ByteString (ByteString) +import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Char8 as B import Data.Char (isDigit) import Data.List (isPrefixOf, isSuffixOf) import Data.Maybe (fromMaybe) import Data.String import qualified Data.Text as T -import Database.SQLite.Simple (Only (..)) -import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), InlineFilesConfig (..), defaultInlineFilesConfig) +import Simplex.Chat.Controller (ChatConfig (..), ChatController (..)) +import Simplex.Chat.Messages.CIContent (e2eInfoNoPQText, e2eInfoPQText) import Simplex.Chat.Protocol +import Simplex.Chat.Store.Direct (getContact) +import Simplex.Chat.Store.NoteFolders (createNoteFolder) import Simplex.Chat.Store.Profiles (getUserContactProfiles) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared import Simplex.FileTransfer.Client.Main (xftpClientCLI) -import Simplex.Messaging.Agent.Store.SQLite (maybeFirstRow, withTransaction) -import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Messaging.Agent.Client (agentClientStore) +import Simplex.Messaging.Agent.Store.AgentStore (maybeFirstRow, withTransaction) +import qualified Simplex.Messaging.Agent.Store.DB as DB +import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Version import System.Directory (doesFileExist) import System.Environment (lookupEnv, withArgs) -import System.FilePath ((</>)) import System.IO.Silently (capture_) import System.Info (os) -import Test.Hspec +import Test.Hspec hiding (it) +import qualified Test.Hspec as Hspec +import UnliftIO (timeout) +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (Only (..)) +#else +import Database.SQLite.Simple (Only (..)) +#endif defaultPrefs :: Maybe Preferences defaultPrefs = Just $ toChatPrefs defaultChatPrefs @@ -53,11 +71,20 @@ cathProfile = Profile {displayName = "cath", fullName = "Catherine", image = Not danProfile :: Profile danProfile = Profile {displayName = "dan", fullName = "Daniel", image = Nothing, contactLink = Nothing, preferences = defaultPrefs} -xit' :: (HasCallStack, Example a) => String -> a -> SpecWith (Arg a) +businessProfile :: Profile +businessProfile = Profile {displayName = "biz", fullName = "Biz Inc", image = Nothing, contactLink = Nothing, preferences = defaultPrefs} + +it :: HasCallStack => String -> (TestParams -> Expectation) -> SpecWith (Arg (TestParams -> Expectation)) +it name test = + Hspec.it name $ \tmp -> timeout t (test tmp) >>= maybe (error "test timed out") pure + where + t = 90 * 1000000 + +xit' :: HasCallStack => String -> (TestParams -> Expectation) -> SpecWith (Arg (TestParams -> Expectation)) xit' = if os == "linux" then xit else it xit'' :: (HasCallStack, Example a) => String -> a -> SpecWith (Arg a) -xit'' = ifCI xit it +xit'' = ifCI xit Hspec.it xdescribe'' :: HasCallStack => String -> SpecWith a -> SpecWith a xdescribe'' = ifCI xdescribe describe @@ -67,17 +94,21 @@ ifCI xrun run d t = do ci <- runIO $ lookupEnv "CI" (if ci == Just "true" then xrun else run) d t -versionTestMatrix2 :: (HasCallStack => TestCC -> TestCC -> IO ()) -> SpecWith FilePath -versionTestMatrix2 runTest = do - it "current" $ testChat2 aliceProfile bobProfile runTest - it "prev" $ testChatCfg2 testCfgVPrev aliceProfile bobProfile runTest - it "prev to curr" $ runTestCfg2 testCfg testCfgVPrev runTest - it "curr to prev" $ runTestCfg2 testCfgVPrev testCfg runTest - it "v1" $ testChatCfg2 testCfgV1 aliceProfile bobProfile runTest - it "v1 to v2" $ runTestCfg2 testCfg testCfgV1 runTest - it "v2 to v1" $ runTestCfg2 testCfgV1 testCfg runTest +skip :: String -> SpecWith a -> SpecWith a +skip = before_ . pendingWith -versionTestMatrix3 :: (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> SpecWith FilePath +-- Bool is pqExpected - see testAddContact +versionTestMatrix2 :: (HasCallStack => Bool -> TestCC -> TestCC -> IO ()) -> SpecWith TestParams +versionTestMatrix2 runTest = do + it "current" $ testChat2 aliceProfile bobProfile (runTest True) + it "prev" $ testChatCfg2 testCfgVPrev aliceProfile bobProfile (runTest False) + it "prev to curr" $ runTestCfg2 testCfg testCfgVPrev (runTest False) + it "curr to prev" $ runTestCfg2 testCfgVPrev testCfg (runTest False) + it "old (1st supported)" $ testChatCfg2 testCfgV1 aliceProfile bobProfile (runTest False) + it "old to curr" $ runTestCfg2 testCfg testCfgV1 (runTest False) + it "curr to old" $ runTestCfg2 testCfgV1 testCfg (runTest False) + +versionTestMatrix3 :: (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> SpecWith TestParams versionTestMatrix3 runTest = do it "current" $ testChat3 aliceProfile bobProfile cathProfile runTest it "prev" $ testChatCfg3 testCfgVPrev aliceProfile bobProfile cathProfile runTest @@ -86,69 +117,46 @@ versionTestMatrix3 runTest = do it "curr to prev" $ runTestCfg3 testCfgVPrev testCfg testCfg runTest it "curr+prev to prev" $ runTestCfg3 testCfgVPrev testCfg testCfgVPrev runTest -inlineCfg :: Integer -> ChatConfig -inlineCfg n = testCfg {inlineFiles = defaultInlineFilesConfig {sendChunks = 0, offerChunks = n, receiveChunks = n}} - -fileTestMatrix2 :: (HasCallStack => TestCC -> TestCC -> IO ()) -> SpecWith FilePath -fileTestMatrix2 runTest = do - it "via connection" $ runTestCfg2 viaConn viaConn runTest - it "inline (accepting)" $ runTestCfg2 inline inline runTest - it "via connection (inline offered)" $ runTestCfg2 inline viaConn runTest - it "via connection (inline supported)" $ runTestCfg2 viaConn inline runTest - where - inline = inlineCfg 100 - viaConn = inlineCfg 0 - -fileTestMatrix3 :: (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> SpecWith FilePath -fileTestMatrix3 runTest = do - it "via connection" $ runTestCfg3 viaConn viaConn viaConn runTest - it "inline" $ runTestCfg3 inline inline inline runTest - it "via connection (inline offered)" $ runTestCfg3 inline viaConn viaConn runTest - it "via connection (inline supported)" $ runTestCfg3 viaConn inline inline runTest - where - inline = inlineCfg 100 - viaConn = inlineCfg 0 - -runTestCfg2 :: ChatConfig -> ChatConfig -> (HasCallStack => TestCC -> TestCC -> IO ()) -> FilePath -> IO () -runTestCfg2 aliceCfg bobCfg runTest tmp = - withNewTestChatCfg tmp aliceCfg "alice" aliceProfile $ \alice -> - withNewTestChatCfg tmp bobCfg "bob" bobProfile $ \bob -> +runTestCfg2 :: ChatConfig -> ChatConfig -> (HasCallStack => TestCC -> TestCC -> IO ()) -> TestParams -> IO () +runTestCfg2 aliceCfg bobCfg runTest ps = + withNewTestChatCfg ps aliceCfg "alice" aliceProfile $ \alice -> + withNewTestChatCfg ps bobCfg "bob" bobProfile $ \bob -> runTest alice bob -runTestCfg3 :: ChatConfig -> ChatConfig -> ChatConfig -> (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO () -runTestCfg3 aliceCfg bobCfg cathCfg runTest tmp = - withNewTestChatCfg tmp aliceCfg "alice" aliceProfile $ \alice -> - withNewTestChatCfg tmp bobCfg "bob" bobProfile $ \bob -> - withNewTestChatCfg tmp cathCfg "cath" cathProfile $ \cath -> +runTestCfg3 :: ChatConfig -> ChatConfig -> ChatConfig -> (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> TestParams -> IO () +runTestCfg3 aliceCfg bobCfg cathCfg runTest ps = + withNewTestChatCfg ps aliceCfg "alice" aliceProfile $ \alice -> + withNewTestChatCfg ps bobCfg "bob" bobProfile $ \bob -> + withNewTestChatCfg ps cathCfg "cath" cathProfile $ \cath -> runTest alice bob cath -withTestChatGroup3Connected :: HasCallStack => FilePath -> String -> (HasCallStack => TestCC -> IO a) -> IO a -withTestChatGroup3Connected tmp dbPrefix action = do - withTestChat tmp dbPrefix $ \cc -> do +withTestChatGroup3Connected :: HasCallStack => TestParams -> String -> (HasCallStack => TestCC -> IO a) -> IO a +withTestChatGroup3Connected ps dbPrefix action = do + withTestChat ps dbPrefix $ \cc -> do cc <## "2 contacts connected (use /cs for the list)" cc <## "#team: connected to server(s)" action cc -withTestChatGroup3Connected' :: HasCallStack => FilePath -> String -> IO () -withTestChatGroup3Connected' tmp dbPrefix = withTestChatGroup3Connected tmp dbPrefix $ \_ -> pure () +withTestChatGroup3Connected' :: HasCallStack => TestParams -> String -> IO () +withTestChatGroup3Connected' ps dbPrefix = withTestChatGroup3Connected ps dbPrefix $ \_ -> pure () -withTestChatContactConnected :: HasCallStack => FilePath -> String -> (HasCallStack => TestCC -> IO a) -> IO a -withTestChatContactConnected tmp dbPrefix action = - withTestChat tmp dbPrefix $ \cc -> do +withTestChatContactConnected :: HasCallStack => TestParams -> String -> (HasCallStack => TestCC -> IO a) -> IO a +withTestChatContactConnected ps dbPrefix action = + withTestChat ps dbPrefix $ \cc -> do cc <## "1 contacts connected (use /cs for the list)" action cc -withTestChatContactConnected' :: HasCallStack => FilePath -> String -> IO () -withTestChatContactConnected' tmp dbPrefix = withTestChatContactConnected tmp dbPrefix $ \_ -> pure () +withTestChatContactConnected' :: HasCallStack => TestParams -> String -> IO () +withTestChatContactConnected' ps dbPrefix = withTestChatContactConnected ps dbPrefix $ \_ -> pure () -withTestChatContactConnectedV1 :: HasCallStack => FilePath -> String -> (HasCallStack => TestCC -> IO a) -> IO a -withTestChatContactConnectedV1 tmp dbPrefix action = - withTestChatV1 tmp dbPrefix $ \cc -> do +withTestChatContactConnectedV1 :: HasCallStack => TestParams -> String -> (HasCallStack => TestCC -> IO a) -> IO a +withTestChatContactConnectedV1 ps dbPrefix action = + withTestChatV1 ps dbPrefix $ \cc -> do cc <## "1 contacts connected (use /cs for the list)" action cc -withTestChatContactConnectedV1' :: HasCallStack => FilePath -> String -> IO () -withTestChatContactConnectedV1' tmp dbPrefix = withTestChatContactConnectedV1 tmp dbPrefix $ \_ -> pure () +withTestChatContactConnectedV1' :: HasCallStack => TestParams -> String -> IO () +withTestChatContactConnectedV1' ps dbPrefix = withTestChatContactConnectedV1 ps dbPrefix $ \_ -> pure () -- | test sending direct messages (<##>) :: HasCallStack => TestCC -> TestCC -> IO () @@ -180,6 +188,65 @@ cc #$> (cmd, f, res) = do cc ##> cmd (f <$> getTermLine cc) `shouldReturn` res +-- / PQ combinators + +(\#>) :: HasCallStack => (TestCC, String) -> TestCC -> IO () +(\#>) = sndRcv PQEncOff False + +(+#>) :: HasCallStack => (TestCC, String) -> TestCC -> IO () +(+#>) = sndRcv PQEncOn False + +(++#>) :: HasCallStack => (TestCC, String) -> TestCC -> IO () +(++#>) = sndRcv PQEncOn True + +sndRcv :: HasCallStack => PQEncryption -> Bool -> (TestCC, String) -> TestCC -> IO () +sndRcv pqEnc enabled (cc1, msg) cc2 = do + name1 <- userName cc1 + name2 <- userName cc2 + let cmd = "@" <> name2 <> " " <> msg + cc1 `send` cmd + when enabled $ cc1 <## (name2 <> ": quantum resistant end-to-end encryption enabled") + cc1 <# cmd + cc1 `pqSndForContact` 2 `shouldReturn` pqEnc + when enabled $ cc2 <## (name1 <> ": quantum resistant end-to-end encryption enabled") + cc2 <# (name1 <> "> " <> msg) + cc2 `pqRcvForContact` 2 `shouldReturn` pqEnc + +(\:#>) :: HasCallStack => (TestCC, String, VersionChat) -> (TestCC, VersionChat) -> IO () +(\:#>) = sndRcvImg PQEncOff False + +(+:#>) :: HasCallStack => (TestCC, String, VersionChat) -> (TestCC, VersionChat) -> IO () +(+:#>) = sndRcvImg PQEncOn False + +(++:#>) :: HasCallStack => (TestCC, String, VersionChat) -> (TestCC, VersionChat) -> IO () +(++:#>) = sndRcvImg PQEncOn True + +sndRcvImg :: HasCallStack => PQEncryption -> Bool -> (TestCC, String, VersionChat) -> (TestCC, VersionChat) -> IO () +sndRcvImg pqEnc enabled (cc1, msg, v1) (cc2, v2) = do + name1 <- userName cc1 + name2 <- userName cc2 + g <- C.newRandom + img <- atomically $ B64.encode <$> C.randomBytes lrgLen g + cc1 `send` ("/_send @2 json {\"msgContent\":{\"type\":\"image\",\"text\":\"" <> msg <> "\",\"image\":\"" <> B.unpack img <> "\"}}") + cc1 .<## "}}" + cc1 <### ([ConsoleString (name2 <> ": quantum resistant end-to-end encryption enabled") | enabled] <> [WithTime ("@" <> name2 <> " " <> msg)]) + cc1 `pqSndForContact` 2 `shouldReturn` pqEnc + cc1 `pqVerForContact` 2 `shouldReturn` v1 + cc2 <### ([ConsoleString (name1 <> ": quantum resistant end-to-end encryption enabled") | enabled] <> [WithTime (name1 <> "> " <> msg)]) + cc2 `pqRcvForContact` 2 `shouldReturn` pqEnc + cc2 `pqVerForContact` 2 `shouldReturn` v2 + where + lrgLen = maxEncodedMsgLength * 3 `div` 4 - 110 -- 98 is ~ max size for binary image preview given the rest of the message + +genProfileImg :: IO ByteString +genProfileImg = do + g <- C.newRandom + atomically $ B64.encode <$> C.randomBytes lrgLen g + where + lrgLen = maxEncodedInfoLength * 3 `div` 4 - 420 + +-- PQ combinators / + chat :: String -> [(Int, String)] chat = map (\(a, _, _) -> a) . chat'' @@ -203,28 +270,44 @@ chatFeaturesF = map (\(a, _, c) -> (a, c)) chatFeatures'' chatFeatures'' :: [((Int, String), Maybe (Int, String), Maybe String)] chatFeatures'' = - [ ((0, "Disappearing messages: allowed"), Nothing, Nothing), + [ ((0, e2eeInfoPQStr), Nothing, Nothing), + ((0, "Disappearing messages: allowed"), Nothing, Nothing), ((0, "Full deletion: off"), Nothing, Nothing), ((0, "Message reactions: enabled"), Nothing, Nothing), ((0, "Voice messages: enabled"), Nothing, Nothing), ((0, "Audio/video calls: enabled"), Nothing, Nothing) ] +e2eeInfoNoPQStr :: String +e2eeInfoNoPQStr = T.unpack e2eInfoNoPQText + +e2eeInfoPQStr :: String +e2eeInfoPQStr = T.unpack e2eInfoPQText + lastChatFeature :: String lastChatFeature = snd $ last chatFeatures groupFeatures :: [(Int, String)] -groupFeatures = map (\(a, _, _) -> a) groupFeatures'' +groupFeatures = map (\(a, _, _) -> a) $ groupFeatures'' 0 -groupFeatures'' :: [((Int, String), Maybe (Int, String), Maybe String)] -groupFeatures'' = - [ ((0, "Disappearing messages: off"), Nothing, Nothing), - ((0, "Direct messages: on"), Nothing, Nothing), - ((0, "Full deletion: off"), Nothing, Nothing), - ((0, "Message reactions: on"), Nothing, Nothing), - ((0, "Voice messages: on"), Nothing, Nothing), - ((0, "Files and media: on"), Nothing, Nothing) - -- ((0, "Recent history: on"), Nothing, Nothing) +sndGroupFeatures :: [(Int, String)] +sndGroupFeatures = map (\(a, _, _) -> a) $ groupFeatures'' 1 + +groupFeatureStrs :: [String] +groupFeatureStrs = map (\(a, _, _) -> snd a) $ groupFeatures'' 0 + +groupFeatures'' :: Int -> [((Int, String), Maybe (Int, String), Maybe String)] +groupFeatures'' dir = + [ ((dir, e2eeInfoNoPQStr), Nothing, Nothing), + ((dir, "Disappearing messages: off"), Nothing, Nothing), + ((dir, "Direct messages: on"), Nothing, Nothing), + ((dir, "Full deletion: off"), Nothing, Nothing), + ((dir, "Message reactions: on"), Nothing, Nothing), + ((dir, "Voice messages: on"), Nothing, Nothing), + ((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) ] itemId :: Int -> String @@ -232,7 +315,7 @@ itemId i = show $ length chatFeatures + i (@@@) :: HasCallStack => TestCC -> [(String, String)] -> Expectation (@@@) cc res = do - threadDelay 10000 + threadDelay 100000 getChats mapChats cc res mapChats :: [(String, String, Maybe ConnStatus)] -> [(String, String)] @@ -280,6 +363,13 @@ cc <#. line = do unless prefix $ print ("expected to start from: " <> line, ", got: " <> l) prefix `shouldBe` True +(.<#) :: HasCallStack => TestCC -> String -> Expectation +cc .<# line = do + l <- dropTime <$> getTermLine cc + let suffix = line `isSuffixOf` l + unless suffix $ print ("expected to end with: " <> line, ", got: " <> l) + suffix `shouldBe` True + (<##..) :: HasCallStack => TestCC -> [String] -> Expectation cc <##.. ls = do l <- getTermLine cc @@ -287,6 +377,11 @@ cc <##.. ls = do unless prefix $ print ("expected to start from one of: " <> show ls, ", got: " <> l) prefix `shouldBe` True +(>*) :: HasCallStack => TestCC -> String -> IO () +cc >* note = do + cc `send` ("/* " <> note) + (dropTime <$> getTermLine cc) `shouldReturn` ("* " <> note) + data ConsoleResponse = ConsoleString String | WithTime String @@ -329,7 +424,7 @@ getInAnyOrder f cc ls = do cc <# line = (dropTime <$> getTermLine cc) `shouldReturn` line (*<#) :: HasCallStack => [TestCC] -> String -> Expectation -ccs *<# line = concurrentlyN_ $ map (<# line) ccs +ccs *<# line = mapConcurrently_ (<# line) ccs (?<#) :: HasCallStack => TestCC -> String -> Expectation cc ?<# line = (dropTime <$> getTermLine cc) `shouldReturn` "i " <> line @@ -407,14 +502,27 @@ dropPartialReceipt_ msg = case splitAt 2 msg of _ -> Nothing getInvitation :: HasCallStack => TestCC -> IO String -getInvitation cc = do +getInvitation = getInvitation_ False + +getShortInvitation :: HasCallStack => TestCC -> IO String +getShortInvitation = getInvitation_ True + +getInvitation_ :: HasCallStack => Bool -> TestCC -> IO String +getInvitation_ short cc = do cc <## "pass this invitation link to your contact (via another channel):" cc <## "" inv <- getTermLine cc cc <## "" cc <## "and ask them to connect: /c <invitation_link_above>" + when short $ cc <##. "The invitation link for old clients: https://simplex.chat/invitation#" pure inv +getShortContactLink :: HasCallStack => TestCC -> Bool -> IO (String, String) +getShortContactLink cc created = do + shortLink <- getContactLink cc created + fullLink <- dropLinePrefix "The contact link for old clients: " =<< getTermLine cc + pure (shortLink, fullLink) + getContactLink :: HasCallStack => TestCC -> Bool -> IO String getContactLink cc created = do cc <## if created then "Your new chat address is created!" else "Your chat address:" @@ -427,6 +535,17 @@ getContactLink cc created = do cc <## "to delete it: /da (accepted contacts will remain connected)" pure link +dropLinePrefix :: String -> String -> IO String +dropLinePrefix line s + | line `isPrefixOf` s = pure $ drop (length line) s + | otherwise = error $ "expected to start from: " <> line <> ", got: " <> s + +getShortGroupLink :: HasCallStack => TestCC -> String -> GroupMemberRole -> Bool -> IO (String, String) +getShortGroupLink cc gName mRole created = do + shortLink <- getGroupLink cc gName mRole created + fullLink <- dropLinePrefix "The group link for old clients: " =<< getTermLine cc + pure (shortLink, fullLink) + getGroupLink :: HasCallStack => TestCC -> String -> GroupMemberRole -> Bool -> IO String getGroupLink cc gName mRole created = do cc <## if created then "Group link is created!" else "Group link:" @@ -462,12 +581,50 @@ withCCTransaction :: TestCC -> (DB.Connection -> IO a) -> IO a withCCTransaction cc action = withTransaction (chatStore $ chatController cc) $ \db -> action db +withCCAgentTransaction :: TestCC -> (DB.Connection -> IO a) -> IO a +withCCAgentTransaction TestCC {chatController = ChatController {smpAgent}} action = + withTransaction (agentClientStore smpAgent) $ \db -> action db + +createCCNoteFolder :: TestCC -> IO () +createCCNoteFolder cc = + withCCTransaction cc $ \db -> + withCCUser cc $ \user -> + runExceptT (createNoteFolder db user) >>= either (fail . show) pure + getProfilePictureByName :: TestCC -> String -> IO (Maybe String) getProfilePictureByName cc displayName = withTransaction (chatStore $ chatController cc) $ \db -> maybeFirstRow fromOnly $ DB.query db "SELECT image FROM contact_profiles WHERE display_name = ? LIMIT 1" (Only displayName) +pqSndForContact :: TestCC -> ContactId -> IO PQEncryption +pqSndForContact = pqForContact_ pqSndEnabled PQEncOff + +pqRcvForContact :: TestCC -> ContactId -> IO PQEncryption +pqRcvForContact = pqForContact_ pqRcvEnabled PQEncOff + +pqForContact :: TestCC -> ContactId -> IO PQEncryption +pqForContact = pqForContact_ (Just . connPQEnabled) (error "impossible") + +pqSupportForCt :: TestCC -> ContactId -> IO PQSupport +pqSupportForCt = pqForContact_ (\Connection {pqSupport} -> Just pqSupport) PQSupportOff + +pqVerForContact :: TestCC -> ContactId -> IO VersionChat +pqVerForContact = pqForContact_ (Just . connChatVersion) (error "impossible") + +pqForContact_ :: (Connection -> Maybe a) -> a -> TestCC -> ContactId -> IO a +pqForContact_ pqSel def cc contactId = (fromMaybe def . pqSel) <$> getCtConn cc contactId + +getCtConn :: TestCC -> ContactId -> IO Connection +getCtConn cc contactId = getTestCCContact cc contactId >>= maybe (fail "no connection") pure . contactConn + +getTestCCContact :: TestCC -> ContactId -> IO Contact +getTestCCContact cc contactId = do + let TestCC {chatController = ChatController {config = ChatConfig {chatVRange = vr}}} = cc + withCCTransaction cc $ \db -> + withCCUser cc $ \user -> + runExceptT (getContact db vr user contactId) >>= either (fail . show) pure + lastItemId :: HasCallStack => TestCC -> IO String lastItemId cc = do cc ##> "/last_item_id" @@ -512,6 +669,16 @@ createGroup2' gName cc1 cc2 doConnectUsers = do (cc1 <## ("#" <> gName <> ": " <> name2 <> " joined the group")) (cc2 <## ("#" <> gName <> ": you joined the group")) +disableFullDeletion2 :: HasCallStack => String -> TestCC -> TestCC -> IO () +disableFullDeletion2 gName cc1 cc2 = do + cc1 ##> ("/set delete #" <> gName <> " off") + cc1 <## "updated group preferences:" + cc1 <## "Full deletion: off" + name1 <- userName cc1 + cc2 <## (name1 <> " updated group #" <> gName <> ":") + cc2 <## "updated group preferences:" + cc2 <## "Full deletion: off" + createGroup3 :: HasCallStack => String -> TestCC -> TestCC -> TestCC -> IO () createGroup3 gName cc1 cc2 cc3 = do createGroup2 gName cc1 cc2 @@ -532,6 +699,14 @@ createGroup3 gName cc1 cc2 cc3 = do cc2 <## ("#" <> gName <> ": new member " <> name3 <> " is connected") ] +disableFullDeletion3 :: HasCallStack => String -> TestCC -> TestCC -> TestCC -> IO () +disableFullDeletion3 gName cc1 cc2 cc3 = do + disableFullDeletion2 gName cc1 cc2 + name1 <- userName cc1 + cc3 <## (name1 <> " updated group #" <> gName <> ":") + cc3 <## "updated group preferences:" + cc3 <## "Full deletion: off" + create2Groups3 :: HasCallStack => String -> String -> TestCC -> TestCC -> TestCC -> IO () create2Groups3 gName1 gName2 cc1 cc2 cc3 = do createGroup3 gName1 cc1 cc2 cc3 @@ -574,25 +749,11 @@ checkActionDeletesFile file action = do fileExistsAfter <- doesFileExist file fileExistsAfter `shouldBe` False -startFileTransferWithDest' :: HasCallStack => TestCC -> TestCC -> String -> String -> Maybe String -> IO () -startFileTransferWithDest' cc1 cc2 fileName fileSize fileDest_ = do - name1 <- userName cc1 - name2 <- userName cc2 - cc1 #> ("/f @" <> name2 <> " ./tests/fixtures/" <> fileName) - cc1 <## "use /fc 1 to cancel sending" - cc2 <# (name1 <> "> sends file " <> fileName <> " (" <> fileSize <> ")") - cc2 <## "use /fr 1 [<dir>/ | <path>] to receive it" - cc2 ##> ("/fr 1" <> maybe "" (" " <>) fileDest_) - cc2 <## ("saving file 1 from " <> name1 <> " to " <> maybe id (</>) fileDest_ fileName) - concurrently_ - (cc2 <## ("started receiving file 1 (" <> fileName <> ") from " <> name1)) - (cc1 <## ("started sending file 1 (" <> fileName <> ") to " <> name2)) - currentChatVRangeInfo :: String currentChatVRangeInfo = "peer chat protocol version range: " <> vRangeStr supportedChatVRange -vRangeStr :: VersionRange -> String +vRangeStr :: VersionRange v -> String vRangeStr (VersionRange minVer maxVer) = "(" <> show minVer <> ", " <> show maxVer <> ")" linkAnotherSchema :: String -> String @@ -605,3 +766,12 @@ linkAnotherSchema link xftpCLI :: [String] -> IO [String] xftpCLI params = lines <$> capture_ (withArgs params xftpClientCLI) + +setRelativePaths :: HasCallStack => TestCC -> String -> String -> IO () +setRelativePaths cc filesFolder tempFolder = do + cc ##> "/_stop" + cc <## "chat stopped" + cc #$> ("/_files_folder " <> filesFolder, id, "ok") + cc #$> ("/_temp_folder " <> tempFolder, id, "ok") + cc ##> "/_start" + cc <## "chat started" diff --git a/tests/JSONFixtures.hs b/tests/JSONFixtures.hs new file mode 100644 index 0000000000..bd74f44022 --- /dev/null +++ b/tests/JSONFixtures.hs @@ -0,0 +1,62 @@ +{-# LANGUAGE OverloadedStrings #-} + +module JSONFixtures where + +import qualified Data.ByteString.Lazy.Char8 as LB + +noActiveUserSwift :: LB.ByteString +noActiveUserSwift = "{\"error\":{\"_owsf\":true,\"error\":{\"errorType\":{\"_owsf\":true,\"noActiveUser\":{}}}}}" + +noActiveUserTagged :: LB.ByteString +noActiveUserTagged = "{\"error\":{\"type\":\"error\",\"errorType\":{\"type\":\"noActiveUser\"}}}" + +activeUserExistsSwift :: LB.ByteString +activeUserExistsSwift = "{\"error\":{\"_owsf\":true,\"error\":{\"errorType\":{\"_owsf\":true,\"userExists\":{\"contactName\":\"alice\"}}}}}" + +activeUserExistsTagged :: LB.ByteString +activeUserExistsTagged = "{\"error\":{\"type\":\"error\",\"errorType\":{\"type\":\"userExists\",\"contactName\":\"alice\"}}}" + +activeUserSwift :: LB.ByteString +activeUserSwift = "{\"result\":{\"_owsf\":true,\"activeUser\":{\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}}" + +activeUserTagged :: LB.ByteString +activeUserTagged = "{\"result\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}" + +chatStartedSwift :: LB.ByteString +chatStartedSwift = "{\"result\":{\"_owsf\":true,\"chatStarted\":{}}}" + +chatStartedTagged :: LB.ByteString +chatStartedTagged = "{\"result\":{\"type\":\"chatStarted\"}}" + +networkStatusesSwift :: LB.ByteString +networkStatusesSwift = "{\"result\":{\"_owsf\":true,\"networkStatuses\":{\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}}" + +networkStatusesTagged :: LB.ByteString +networkStatusesTagged = "{\"result\":{\"type\":\"networkStatuses\",\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}" + +userJSON :: LB.ByteString +userJSON = "{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}" + +memberSubSummarySwift :: LB.ByteString +memberSubSummarySwift = "{\"result\":{\"_owsf\":true,\"memberSubSummary\":{\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}}" + +memberSubSummaryTagged :: LB.ByteString +memberSubSummaryTagged = "{\"result\":{\"type\":\"memberSubSummary\",\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}" + +userContactSubSummarySwift :: LB.ByteString +userContactSubSummarySwift = "{\"result\":{\"_owsf\":true,\"userContactSubSummary\":{\"user\":" <> userJSON <> ",\"userContactSubscriptions\":[]}}}" + +userContactSubSummaryTagged :: LB.ByteString +userContactSubSummaryTagged = "{\"result\":{\"type\":\"userContactSubSummary\",\"user\":" <> userJSON <> ",\"userContactSubscriptions\":[]}}" + +pendingSubSummarySwift :: LB.ByteString +pendingSubSummarySwift = "{\"result\":{\"_owsf\":true,\"pendingSubSummary\":{\"user\":" <> userJSON <> ",\"pendingSubscriptions\":[]}}}" + +pendingSubSummaryTagged :: LB.ByteString +pendingSubSummaryTagged = "{\"result\":{\"type\":\"pendingSubSummary\",\"user\":" <> userJSON <> ",\"pendingSubscriptions\":[]}}" + +parsedMarkdownSwift :: LB.ByteString +parsedMarkdownSwift = "{\"formattedText\":[{\"format\":{\"_owsf\":true,\"bold\":{}},\"text\":\"hello\"}]}" + +parsedMarkdownTagged :: LB.ByteString +parsedMarkdownTagged = "{\"formattedText\":[{\"format\":{\"type\":\"bold\"},\"text\":\"hello\"}]}" diff --git a/tests/JSONTests.hs b/tests/JSONTests.hs index a17a69fae8..400db87b36 100644 --- a/tests/JSONTests.hs +++ b/tests/JSONTests.hs @@ -12,7 +12,7 @@ import Data.ByteString.Builder (toLazyByteString) import qualified Data.ByteString.Lazy.Char8 as LB import GHC.Generics (Generic) import Generic.Random (genericArbitraryU) -import MobileTests +import JSONFixtures import Simplex.Chat.Remote.Protocol (owsf2tagged) import Simplex.Messaging.Parsers import Test.Hspec diff --git a/tests/MarkdownTests.hs b/tests/MarkdownTests.hs index 1cd2aa2c47..05d5362cf1 100644 --- a/tests/MarkdownTests.hs +++ b/tests/MarkdownTests.hs @@ -7,7 +7,10 @@ module MarkdownTests where import Data.List.NonEmpty (NonEmpty) import Data.Text (Text) +import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) import Simplex.Chat.Markdown +import Simplex.Messaging.Encoding.String import System.Console.ANSI.Types import Test.Hspec @@ -19,81 +22,109 @@ markdownTests = do textWithUri textWithEmail textWithPhone + textWithMentions multilineMarkdownList +infixr 1 ==>, <==, <==>, ==>>, <<==, <<==>> + +(==>) :: Text -> Markdown -> Expectation +s ==> m = parseMarkdown s `shouldBe` m + +(<==) :: Text -> Markdown -> Expectation +s <== m = s <<== markdownToList m + +(<==>) :: Text -> Markdown -> Expectation +s <==> m = (s ==> m) >> (s <== m) + +(==>>) :: Text -> MarkdownList -> Expectation +s ==>> ft = parseMaybeMarkdownList s `shouldBe` Just ft + +(<<==) :: Text -> MarkdownList -> Expectation +s <<== ft = T.concat (map markdownText ft) `shouldBe` s + +(<<==>>) :: Text -> MarkdownList -> Expectation +s <<==>> ft = (s ==>> ft) >> (s <<== ft) + +bold :: Text -> Markdown +bold = markdown Bold + textFormat :: Spec textFormat = describe "text format (bold)" do it "correct markdown" do - parseMarkdown "this is *bold formatted* text" - `shouldBe` "this is " <> markdown Bold "bold formatted" <> " text" - parseMarkdown "*bold formatted* text" - `shouldBe` markdown Bold "bold formatted" <> " text" - parseMarkdown "this is *bold*" - `shouldBe` "this is " <> markdown Bold "bold" - parseMarkdown " *bold* text" - `shouldBe` " " <> markdown Bold "bold" <> " text" - parseMarkdown " *bold* text" - `shouldBe` " " <> markdown Bold "bold" <> " text" - parseMarkdown "this is *bold* " - `shouldBe` "this is " <> markdown Bold "bold" <> " " - parseMarkdown "this is *bold* " - `shouldBe` "this is " <> markdown Bold "bold" <> " " + "this is *bold formatted* text" + <==> "this is " <> bold "bold formatted" <> " text" + "*bold formatted* text" + <==> bold "bold formatted" <> " text" + "this is *bold*" + <==> "this is " <> bold "bold" + " *bold* text" + <==> " " <> bold "bold" <> " text" + " *bold* text" + <==> " " <> bold "bold" <> " text" + "this is *bold* " + <==> "this is " <> bold "bold" <> " " + "this is *bold* " + <==> "this is " <> bold "bold" <> " " it "ignored as markdown" do - parseMarkdown "this is * unformatted * text" - `shouldBe` "this is * unformatted * text" - parseMarkdown "this is *unformatted * text" - `shouldBe` "this is *unformatted * text" - parseMarkdown "this is * unformatted* text" - `shouldBe` "this is * unformatted* text" - parseMarkdown "this is **unformatted** text" - `shouldBe` "this is **unformatted** text" - parseMarkdown "this is*unformatted* text" - `shouldBe` "this is*unformatted* text" - parseMarkdown "this is *unformatted text" - `shouldBe` "this is *unformatted text" + "this is * unformatted * text" + <==> "this is * unformatted * text" + "this is *unformatted * text" + <==> "this is *unformatted * text" + "this is * unformatted* text" + <==> "this is * unformatted* text" + "this is **unformatted** text" + <==> "this is **unformatted** text" + "this is*unformatted* text" + <==> "this is*unformatted* text" + "this is *unformatted text" + <==> "this is *unformatted text" + "*this* is *unformatted text" + <==> bold "this" <> " is *unformatted text" it "ignored internal markdown" do - parseMarkdown "this is *long _bold_ (not italic)* text" - `shouldBe` "this is " <> markdown Bold "long _bold_ (not italic)" <> " text" - parseMarkdown "snippet: `this is *bold text*`" - `shouldBe` "snippet: " <> markdown Snippet "this is *bold text*" + "this is *long _bold_ (not italic)* text" + <==> "this is " <> bold "long _bold_ (not italic)" <> " text" + "snippet: `this is *bold text*`" + <==> "snippet: " <> markdown Snippet "this is *bold text*" secretText :: Spec secretText = describe "secret text" do it "correct markdown" do - parseMarkdown "this is #black_secret# text" - `shouldBe` "this is " <> markdown Secret "black_secret" <> " text" - parseMarkdown "##black_secret### text" - `shouldBe` markdown Secret "#black_secret##" <> " text" - parseMarkdown "this is #black secret# text" - `shouldBe` "this is " <> markdown Secret "black secret" <> " text" - parseMarkdown "##black secret### text" - `shouldBe` markdown Secret "#black secret##" <> " text" - parseMarkdown "this is #secret#" - `shouldBe` "this is " <> markdown Secret "secret" - parseMarkdown " #secret# text" - `shouldBe` " " <> markdown Secret "secret" <> " text" - parseMarkdown " #secret# text" - `shouldBe` " " <> markdown Secret "secret" <> " text" - parseMarkdown "this is #secret# " - `shouldBe` "this is " <> markdown Secret "secret" <> " " - parseMarkdown "this is #secret# " - `shouldBe` "this is " <> markdown Secret "secret" <> " " + "this is #black_secret# text" + <==> "this is " <> markdown Secret "black_secret" <> " text" + "##black_secret### text" + <==> markdown Secret "#black_secret##" <> " text" + "this is #black secret# text" + <==> "this is " <> markdown Secret "black secret" <> " text" + "##black secret### text" + <==> markdown Secret "#black secret##" <> " text" + "this is #secret#" + <==> "this is " <> markdown Secret "secret" + " #secret# text" + <==> " " <> markdown Secret "secret" <> " text" + " #secret# text" + <==> " " <> markdown Secret "secret" <> " text" + "this is #secret# " + <==> "this is " <> markdown Secret "secret" <> " " + "this is #secret# " + <==> "this is " <> markdown Secret "secret" <> " " it "ignored as markdown" do - parseMarkdown "this is # unformatted # text" - `shouldBe` "this is # unformatted # text" - parseMarkdown "this is #unformatted # text" - `shouldBe` "this is #unformatted # text" - parseMarkdown "this is # unformatted# text" - `shouldBe` "this is # unformatted# text" - parseMarkdown "this is ## unformatted ## text" - `shouldBe` "this is ## unformatted ## text" - parseMarkdown "this is#unformatted# text" - `shouldBe` "this is#unformatted# text" - parseMarkdown "this is #unformatted text" - `shouldBe` "this is #unformatted text" + "this is # unformatted # text" + <==> "this is # unformatted # text" + "this is #unformatted # text" + <==> "this is #unformatted # text" + "this is # unformatted# text" + <==> "this is # unformatted# text" + "this is ## unformatted ## text" + <==> "this is ## unformatted ## text" + "this is#unformatted# text" + <==> "this is#unformatted# text" + "this is #unformatted text" + <==> "this is #unformatted text" + "*this* is #unformatted text" + <==> bold "this" <> " is #unformatted text" it "ignored internal markdown" do - parseMarkdown "snippet: `this is #secret_text#`" - `shouldBe` "snippet: " <> markdown Snippet "this is #secret_text#" + "snippet: `this is #secret_text#`" + <==> "snippet: " <> markdown Snippet "this is #secret_text#" red :: Text -> Markdown red = markdown (colored Red) @@ -101,66 +132,94 @@ red = markdown (colored Red) textColor :: Spec textColor = describe "text color (red)" do it "correct markdown" do - parseMarkdown "this is !1 red color! text" - `shouldBe` "this is " <> red "red color" <> " text" - parseMarkdown "!1 red! text" - `shouldBe` red "red" <> " text" - parseMarkdown "this is !1 red!" - `shouldBe` "this is " <> red "red" - parseMarkdown " !1 red! text" - `shouldBe` " " <> red "red" <> " text" - parseMarkdown " !1 red! text" - `shouldBe` " " <> red "red" <> " text" - parseMarkdown "this is !1 red! " - `shouldBe` "this is " <> red "red" <> " " - parseMarkdown "this is !1 red! " - `shouldBe` "this is " <> red "red" <> " " + "this is !1 red color! text" + <==> "this is " <> red "red color" <> " text" + "!1 red! text" + <==> red "red" <> " text" + "this is !1 red!" + <==> "this is " <> red "red" + " !1 red! text" + <==> " " <> red "red" <> " text" + " !1 red! text" + <==> " " <> red "red" <> " text" + "this is !1 red! " + <==> "this is " <> red "red" <> " " + "this is !1 red! " + <==> "this is " <> red "red" <> " " it "ignored as markdown" do - parseMarkdown "this is !1 unformatted ! text" - `shouldBe` "this is !1 unformatted ! text" - parseMarkdown "this is !1 unformatted ! text" - `shouldBe` "this is !1 unformatted ! text" - parseMarkdown "this is !1 unformatted! text" - `shouldBe` "this is !1 unformatted! text" - -- parseMarkdown "this is !!1 unformatted!! text" - -- `shouldBe` "this is " <> "!!1" <> "unformatted!! text" - parseMarkdown "this is!1 unformatted! text" - `shouldBe` "this is!1 unformatted! text" - parseMarkdown "this is !1 unformatted text" - `shouldBe` "this is !1 unformatted text" + "this is !1 unformatted ! text" + <==> "this is !1 unformatted ! text" + "this is !1 unformatted ! text" + <==> "this is !1 unformatted ! text" + "this is !1 unformatted! text" + <==> "this is !1 unformatted! text" + -- "this is !!1 unformatted!! text" + -- <==> "this is " <> "!!1" <> "unformatted!! text" + "this is!1 unformatted! text" + <==> "this is!1 unformatted! text" + "this is !1 unformatted text" + <==> "this is !1 unformatted text" + "*this* is !1 unformatted text" + <==> bold "this" <> " is !1 unformatted text" it "ignored internal markdown" do - parseMarkdown "this is !1 long *red* (not bold)! text" - `shouldBe` "this is " <> red "long *red* (not bold)" <> " text" - parseMarkdown "snippet: `this is !1 red text!`" - `shouldBe` "snippet: " <> markdown Snippet "this is !1 red text!" + "this is !1 long *red* (not bold)! text" + <==> "this is " <> red "long *red* (not bold)" <> " text" + "snippet: `this is !1 red text!`" + <==> "snippet: " <> markdown Snippet "this is !1 red text!" uri :: Text -> Markdown uri = Markdown $ Just Uri simplexLink :: SimplexLinkType -> Text -> NonEmpty Text -> Text -> Markdown -simplexLink linkType simplexUri smpHosts = Markdown $ Just SimplexLink {linkType, simplexUri, smpHosts} +simplexLink linkType uriText smpHosts t = Markdown (simplexLinkFormat linkType uriText smpHosts) t + +simplexLinkFormat :: SimplexLinkType -> Text -> NonEmpty Text -> Maybe Format +simplexLinkFormat linkType uriText smpHosts = case strDecode $ encodeUtf8 uriText of + Right simplexUri -> Just SimplexLink {linkType, simplexUri, smpHosts} + Left e -> error e textWithUri :: Spec textWithUri = describe "text with Uri" do it "correct markdown" do - parseMarkdown "https://simplex.chat" `shouldBe` uri "https://simplex.chat" - parseMarkdown "https://simplex.chat." `shouldBe` uri "https://simplex.chat" <> "." - parseMarkdown "https://simplex.chat, hello" `shouldBe` uri "https://simplex.chat" <> ", hello" - parseMarkdown "http://simplex.chat" `shouldBe` uri "http://simplex.chat" - parseMarkdown "this is https://simplex.chat" `shouldBe` "this is " <> uri "https://simplex.chat" - parseMarkdown "https://simplex.chat site" `shouldBe` uri "https://simplex.chat" <> " site" + "https://simplex.chat" <==> uri "https://simplex.chat" + "https://simplex.chat." <==> uri "https://simplex.chat" <> "." + "https://simplex.chat, hello" <==> uri "https://simplex.chat" <> ", hello" + "http://simplex.chat" <==> uri "http://simplex.chat" + "this is https://simplex.chat" <==> "this is " <> uri "https://simplex.chat" + "https://simplex.chat site" <==> uri "https://simplex.chat" <> " site" + "SimpleX on GitHub: https://github.com/simplex-chat/" <==> "SimpleX on GitHub: " <> uri "https://github.com/simplex-chat/" + "SimpleX on GitHub: https://github.com/simplex-chat." <==> "SimpleX on GitHub: " <> uri "https://github.com/simplex-chat" <> "." + "https://github.com/simplex-chat/ - SimpleX on GitHub" <==> uri "https://github.com/simplex-chat/" <> " - SimpleX on GitHub" + -- "SimpleX on GitHub (https://github.com/simplex-chat/)" <==> "SimpleX on GitHub (" <> uri "https://github.com/simplex-chat/" <> ")" + "https://en.m.wikipedia.org/wiki/Servo_(software)" <==> uri "https://en.m.wikipedia.org/wiki/Servo_(software)" + "example.com" <==> uri "example.com" + "example.com." <==> uri "example.com" <> "." + "example.com..." <==> uri "example.com" <> "..." + "www.example.com" <==> uri "www.example.com" + "example.academy" <==> uri "example.academy" + "this is example.com" <==> "this is " <> uri "example.com" + "x.com" <==> uri "x.com" it "ignored as markdown" do - parseMarkdown "_https://simplex.chat" `shouldBe` "_https://simplex.chat" - parseMarkdown "this is _https://simplex.chat" `shouldBe` "this is _https://simplex.chat" + "_https://simplex.chat" <==> "_https://simplex.chat" + "this is _https://simplex.chat" <==> "this is _https://simplex.chat" + "this is https://" <==> "this is https://" + "example.c" <==> "example.c" + "www.www.example.com" <==> "www.www.example.com" + "www.example1.com" <==> "www.example1.com" + "www." <==> "www." + ".com" <==> ".com" + "example.academytoolong" <==> "example.academytoolong" it "SimpleX links" do - let inv = "/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" - parseMarkdown ("https://simplex.chat" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] ("https://simplex.chat" <> inv) - parseMarkdown ("simplex:" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] ("simplex:" <> inv) - parseMarkdown ("https://example.com" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] ("https://example.com" <> inv) - let ct = "/contact#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D" - parseMarkdown ("https://simplex.chat" <> ct) `shouldBe` simplexLink XLContact ("simplex:" <> ct) ["smp.simplex.im"] ("https://simplex.chat" <> ct) - let gr = "/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FWHV0YU1sYlU7NqiEHkHDB6gxO1ofTync%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAWbebOqVYuBXaiqHcXYjEHCpYi6VzDlu6CVaijDTmsQU%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22mL-7Divb94GGmGmRBef5Dg%3D%3D%22%7D" - parseMarkdown ("https://simplex.chat" <> gr) `shouldBe` simplexLink XLGroup ("simplex:" <> gr) ["smp4.simplex.im", "o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"] ("https://simplex.chat" <> gr) + let inv = "/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" + ("https://simplex.chat" <> inv) <==> simplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] ("https://simplex.chat" <> inv) + ("simplex:" <> inv) <==> simplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] ("simplex:" <> inv) + ("https://example.com" <> inv) <==> simplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] ("https://example.com" <> inv) + let ct = "/contact#/?v=2&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D" + ("https://simplex.chat" <> ct) <==> simplexLink XLContact ("simplex:" <> ct) ["smp.simplex.im"] ("https://simplex.chat" <> ct) + ("simplex:" <> ct) <==> simplexLink XLContact ("simplex:" <> ct) ["smp.simplex.im"] ("simplex:" <> ct) + let gr = "/contact#/?v=2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FWHV0YU1sYlU7NqiEHkHDB6gxO1ofTync%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAWbebOqVYuBXaiqHcXYjEHCpYi6VzDlu6CVaijDTmsQU%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22mL-7Divb94GGmGmRBef5Dg%3D%3D%22%7D" + ("https://simplex.chat" <> gr) <==> simplexLink XLGroup ("simplex:" <> gr) ["smp4.simplex.im", "o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"] ("https://simplex.chat" <> gr) + ("simplex:" <> gr) <==> simplexLink XLGroup ("simplex:" <> gr) ["smp4.simplex.im", "o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"] ("simplex:" <> gr) email :: Text -> Markdown email = Markdown $ Just Email @@ -168,15 +227,20 @@ email = Markdown $ Just Email textWithEmail :: Spec textWithEmail = describe "text with Email" do it "correct markdown" do - parseMarkdown "chat@simplex.chat" `shouldBe` email "chat@simplex.chat" - parseMarkdown "test chat@simplex.chat" `shouldBe` "test " <> email "chat@simplex.chat" - parseMarkdown "test chat+123@simplex.chat" `shouldBe` "test " <> email "chat+123@simplex.chat" - parseMarkdown "test chat.chat+123@simplex.chat" `shouldBe` "test " <> email "chat.chat+123@simplex.chat" - parseMarkdown "chat@simplex.chat test" `shouldBe` email "chat@simplex.chat" <> " test" - parseMarkdown "test1 chat@simplex.chat test2" `shouldBe` "test1 " <> email "chat@simplex.chat" <> " test2" - it "ignored as markdown" do - parseMarkdown "chat @simplex.chat" `shouldBe` "chat @simplex.chat" - parseMarkdown "this is chat @simplex.chat" `shouldBe` "this is chat @simplex.chat" + "chat@simplex.chat" <==> email "chat@simplex.chat" + "test chat@simplex.chat" <==> "test " <> email "chat@simplex.chat" + "test chat+123@simplex.chat" <==> "test " <> email "chat+123@simplex.chat" + "test chat.chat+123@simplex.chat" <==> "test " <> email "chat.chat+123@simplex.chat" + "chat@simplex.chat test" <==> email "chat@simplex.chat" <> " test" + "test1 chat@simplex.chat test2" <==> "test1 " <> email "chat@simplex.chat" <> " test2" + "test chat@simplex.chat." <==> "test " <> email "chat@simplex.chat" <> "." + "test chat@simplex.chat..." <==> "test " <> email "chat@simplex.chat" <> "..." + it "ignored as email markdown" do + "chat @simplex.chat" <==> "chat " <> mention "simplex.chat" "@simplex.chat" + "this is chat @simplex.chat" <==> "this is chat " <> mention "simplex.chat" "@simplex.chat" + "this is chat@ simplex.chat" <==> "this is chat@ " <> uri "simplex.chat" + "this is chat @ simplex.chat" <==> "this is chat @ " <> uri "simplex.chat" + "*this* is chat @ simplex.chat" <==> bold "this" <> " is chat @ " <> uri "simplex.chat" phone :: Text -> Markdown phone = Markdown $ Just Phone @@ -184,20 +248,44 @@ phone = Markdown $ Just Phone textWithPhone :: Spec textWithPhone = describe "text with Phone" do it "correct markdown" do - parseMarkdown "07777777777" `shouldBe` phone "07777777777" - parseMarkdown "test 07777777777" `shouldBe` "test " <> phone "07777777777" - parseMarkdown "07777777777 test" `shouldBe` phone "07777777777" <> " test" - parseMarkdown "test1 07777777777 test2" `shouldBe` "test1 " <> phone "07777777777" <> " test2" - parseMarkdown "test 07777 777 777 test" `shouldBe` "test " <> phone "07777 777 777" <> " test" - parseMarkdown "test +447777777777 test" `shouldBe` "test " <> phone "+447777777777" <> " test" - parseMarkdown "test +44 (0) 7777 777 777 test" `shouldBe` "test " <> phone "+44 (0) 7777 777 777" <> " test" - parseMarkdown "test +44-7777-777-777 test" `shouldBe` "test " <> phone "+44-7777-777-777" <> " test" - parseMarkdown "test +44 (0) 7777.777.777 https://simplex.chat test" - `shouldBe` "test " <> phone "+44 (0) 7777.777.777" <> " " <> uri "https://simplex.chat" <> " test" + "07777777777" <==> phone "07777777777" + "test 07777777777" <==> "test " <> phone "07777777777" + "07777777777 test" <==> phone "07777777777" <> " test" + "test1 07777777777 test2" <==> "test1 " <> phone "07777777777" <> " test2" + "test 07777 777 777 test" <==> "test " <> phone "07777 777 777" <> " test" + "test +447777777777 test" <==> "test " <> phone "+447777777777" <> " test" + "test +44 (0) 7777 777 777 test" <==> "test " <> phone "+44 (0) 7777 777 777" <> " test" + "test +44-7777-777-777 test" <==> "test " <> phone "+44-7777-777-777" <> " test" + "test +44 (0) 7777.777.777 https://simplex.chat test" + <==> "test " <> phone "+44 (0) 7777.777.777" <> " " <> uri "https://simplex.chat" <> " test" it "ignored as markdown (too short)" $ - parseMarkdown "test 077777 test" `shouldBe` "test 077777 test" - it "ignored as markdown (double spaces)" $ - parseMarkdown "test 07777 777 777 test" `shouldBe` "test 07777 777 777 test" + "test 077777 test" <==> "test 077777 test" + it "ignored as markdown (double spaces)" $ do + "test 07777 777 777 test" <==> "test 07777 777 777 test" + "*test* 07777 777 777 test" <==> bold "test" <> " 07777 777 777 test" + +mention :: Text -> Text -> Markdown +mention = Markdown . Just . Mention + +textWithMentions :: Spec +textWithMentions = describe "text with mentions" do + it "correct markdown" do + "@alice" <==> mention "alice" "@alice" + "hello @alice" <==> "hello " <> mention "alice" "@alice" + "hello @alice !" <==> "hello " <> mention "alice" "@alice" <> " !" + "hello @alice!" <==> "hello " <> mention "alice" "@alice" <> "!" + "hello @alice..." <==> "hello " <> mention "alice" "@alice" <> "..." + "hello @alice@example.com" <==> "hello " <> mention "alice@example.com" "@alice@example.com" + "hello @'alice @ example.com'" <==> "hello " <> mention "alice @ example.com" "@'alice @ example.com'" + "@'alice jones'" <==> mention "alice jones" "@'alice jones'" + "hello @'alice jones'!" <==> "hello " <> mention "alice jones" "@'alice jones'" <> "!" + "hello @'a.j.'!" <==> "hello " <> mention "a.j." "@'a.j.'" <> "!" + it "ignored as markdown" $ do + "hello @'alice jones!" <==> "hello @'alice jones!" + "hello @bob @'alice jones!" <==> "hello " <> mention "bob" "@bob" <> " @'alice jones!" + "hello @ alice!" <==> "hello @ alice!" + "hello @bob @ alice!" <==> "hello " <> mention "bob" "@bob" <> " @ alice!" + "hello @bob @" <==> "hello " <> mention "bob" "@bob" <> " @" uri' :: Text -> FormattedText uri' = FormattedText $ Just Uri @@ -205,8 +293,15 @@ uri' = FormattedText $ Just Uri multilineMarkdownList :: Spec multilineMarkdownList = describe "multiline markdown" do it "correct markdown" do - parseMaybeMarkdownList "http://simplex.chat\nhttp://app.simplex.chat" `shouldBe` Just [uri' "http://simplex.chat", "\n", uri' "http://app.simplex.chat"] + "http://simplex.chat\nhttp://app.simplex.chat" <<==>> [uri' "http://simplex.chat", "\n", uri' "http://app.simplex.chat"] it "combines the same formats" do - parseMaybeMarkdownList "http://simplex.chat\ntext 1\ntext 2\nhttp://app.simplex.chat" `shouldBe` Just [uri' "http://simplex.chat", "\ntext 1\ntext 2\n", uri' "http://app.simplex.chat"] + "http://simplex.chat\ntext 1\ntext 2\nhttp://app.simplex.chat" <<==>> [uri' "http://simplex.chat", "\ntext 1\ntext 2\n", uri' "http://app.simplex.chat"] it "no markdown" do parseMaybeMarkdownList "not a\nmarkdown" `shouldBe` Nothing + let inv = "/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" + it "multiline with simplex link" do + ("https://simplex.chat" <> inv <> "\ntext") + <<==>> + [ FormattedText (simplexLinkFormat XLInvitation ("simplex:" <> inv) ["smp.simplex.im"]) ("https://simplex.chat" <> inv), + "\ntext" + ] diff --git a/tests/MessageBatching.hs b/tests/MessageBatching.hs index 809a4e3bd7..e7728b9be4 100644 --- a/tests/MessageBatching.hs +++ b/tests/MessageBatching.hs @@ -7,8 +7,8 @@ module MessageBatching (batchingTests) where import Crypto.Number.Serialize (os2ip) -import Data.ByteString.Builder (toLazyByteString) -import qualified Data.ByteString.Lazy as LB +import Data.ByteString (ByteString) +import qualified Data.ByteString as B import Data.Either (partitionEithers) import Data.Int (Int64) import Data.String (IsString (..)) @@ -17,7 +17,7 @@ import Data.Text.Encoding (encodeUtf8) import Simplex.Chat.Messages.Batch import Simplex.Chat.Controller (ChatError (..), ChatErrorType (..)) import Simplex.Chat.Messages (SndMessage (..)) -import Simplex.Chat.Protocol (SharedMsgId (..), maxChatMsgSize) +import Simplex.Chat.Protocol (SharedMsgId (..), maxEncodedMsgLength) import Test.Hspec batchingTests :: Spec @@ -26,7 +26,7 @@ batchingTests = describe "message batching tests" $ do it "image x.msg.new and x.msg.file.descr should fit into single batch" testImageFitsSingleBatch instance IsString SndMessage where - fromString s = SndMessage {msgId, sharedMsgId = SharedMsgId "", msgBody = LB.fromStrict s'} + fromString s = SndMessage {msgId, sharedMsgId = SharedMsgId "", msgBody = s'} where s' = encodeUtf8 $ T.pack s msgId = fromInteger $ os2ip s' @@ -94,14 +94,14 @@ testImageFitsSingleBatch = do -- 261_120 bytes (MAX_IMAGE_SIZE in UI), rounded up, example was 743 let descrRoundedSize = 800 - let xMsgNewStr = LB.replicate xMsgNewRoundedSize 1 - descrStr = LB.replicate descrRoundedSize 2 + let xMsgNewStr = B.replicate xMsgNewRoundedSize 1 + descrStr = B.replicate descrRoundedSize 2 msg s = SndMessage {msgId = 0, sharedMsgId = SharedMsgId "", msgBody = s} batched = "[" <> xMsgNewStr <> "," <> descrStr <> "]" - runBatcherTest' maxChatMsgSize [msg xMsgNewStr, msg descrStr] [] [batched] + runBatcherTest' maxEncodedMsgLength [msg xMsgNewStr, msg descrStr] [] [batched] -runBatcherTest :: Int64 -> [SndMessage] -> [ChatError] -> [LB.ByteString] -> Spec +runBatcherTest :: Int -> [SndMessage] -> [ChatError] -> [ByteString] -> Spec runBatcherTest maxLen msgs expectedErrors expectedBatches = it ( (show (map (\SndMessage {msgBody} -> msgBody) msgs) <> ", limit " <> show maxLen <> ": should return ") @@ -110,10 +110,10 @@ runBatcherTest maxLen msgs expectedErrors expectedBatches = ) (runBatcherTest' maxLen msgs expectedErrors expectedBatches) -runBatcherTest' :: Int64 -> [SndMessage] -> [ChatError] -> [LB.ByteString] -> IO () +runBatcherTest' :: Int -> [SndMessage] -> [ChatError] -> [ByteString] -> IO () runBatcherTest' maxLen msgs expectedErrors expectedBatches = do - let (errors, batches) = partitionEithers $ batchMessages maxLen msgs - batchedStrs = map (\(MsgBatch builder _) -> toLazyByteString builder) batches + let (errors, batches) = partitionEithers $ batchMessages maxLen (map Right msgs) + batchedStrs = map (\(MsgBatch batchBody _) -> batchBody) batches testErrors errors `shouldBe` testErrors expectedErrors batchedStrs `shouldBe` expectedBatches where diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index a6231fa27e..11a89bc62e 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -7,6 +7,7 @@ module MobileTests where +import ChatTests.DBUtils import ChatTests.Utils import Control.Concurrent.STM import Control.Monad.Except @@ -16,24 +17,29 @@ import qualified Data.Aeson.TH as JQ import Data.ByteString (ByteString) import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as BS -import Data.ByteString.Internal (create, memcpy) +import Data.ByteString.Internal (create) import qualified Data.ByteString.Lazy.Char8 as LB import Data.Word (Word8, Word32) import Foreign.C import Foreign.Marshal.Alloc (mallocBytes) +import Foreign.Marshal.Utils (copyBytes) import Foreign.Ptr import Foreign.StablePtr import Foreign.Storable (peek) import GHC.IO.Encoding (setLocaleEncoding, setFileSystemEncoding, setForeignEncoding) +import JSONFixtures import Simplex.Chat.Controller (ChatController (..)) -import Simplex.Chat.Mobile +import Simplex.Chat.Mobile hiding (error) import Simplex.Chat.Mobile.File import Simplex.Chat.Mobile.Shared import Simplex.Chat.Mobile.WebRTC +import Simplex.Chat.Options.DB import Simplex.Chat.Store import Simplex.Chat.Store.Profiles import Simplex.Chat.Types (AgentUserId (..), Profile (..)) -import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..)) +import Simplex.Messaging.Agent.Store.Interface +import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..)) +import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile(..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF @@ -42,9 +48,9 @@ import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import System.Directory (copyFile) import System.FilePath ((</>)) import System.IO (utf8) -import Test.Hspec +import Test.Hspec hiding (it) -mobileTests :: HasCallStack => SpecWith FilePath +mobileTests :: HasCallStack => SpecWith TestParams mobileTests = do describe "mobile API" $ do runIO $ do @@ -67,6 +73,8 @@ mobileTests = do it "no exception on missing file" testMissingFileEncryptionCApi describe "validate name" $ do it "should convert invalid name to a valid name" testValidNameCApi + describe "JSON length" $ do + it "should compute length of JSON encoded string" testChatJsonLengthCApi noActiveUser :: LB.ByteString noActiveUser = @@ -76,12 +84,6 @@ noActiveUser = noActiveUserTagged #endif -noActiveUserSwift :: LB.ByteString -noActiveUserSwift = "{\"resp\":{\"_owsf\":true,\"chatCmdError\":{\"chatError\":{\"_owsf\":true,\"error\":{\"errorType\":{\"_owsf\":true,\"noActiveUser\":{}}}}}}}" - -noActiveUserTagged :: LB.ByteString -noActiveUserTagged = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"noActiveUser\"}}}}" - activeUserExists :: LB.ByteString activeUserExists = #if defined(darwin_HOST_OS) && defined(swiftJSON) @@ -90,12 +92,6 @@ activeUserExists = activeUserExistsTagged #endif -activeUserExistsSwift :: LB.ByteString -activeUserExistsSwift = "{\"resp\":{\"_owsf\":true,\"chatCmdError\":{\"user_\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true},\"chatError\":{\"_owsf\":true,\"error\":{\"errorType\":{\"_owsf\":true,\"userExists\":{\"contactName\":\"alice\"}}}}}}}" - -activeUserExistsTagged :: LB.ByteString -activeUserExistsTagged = "{\"resp\":{\"type\":\"chatCmdError\",\"user_\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true},\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"userExists\",\"contactName\":\"alice\"}}}}" - activeUser :: LB.ByteString activeUser = #if defined(darwin_HOST_OS) && defined(swiftJSON) @@ -104,12 +100,6 @@ activeUser = activeUserTagged #endif -activeUserSwift :: LB.ByteString -activeUserSwift = "{\"resp\":{\"_owsf\":true,\"activeUser\":{\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}}" - -activeUserTagged :: LB.ByteString -activeUserTagged = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}" - chatStarted :: LB.ByteString chatStarted = #if defined(darwin_HOST_OS) && defined(swiftJSON) @@ -118,12 +108,6 @@ chatStarted = chatStartedTagged #endif -chatStartedSwift :: LB.ByteString -chatStartedSwift = "{\"resp\":{\"_owsf\":true,\"chatStarted\":{}}}" - -chatStartedTagged :: LB.ByteString -chatStartedTagged = "{\"resp\":{\"type\":\"chatStarted\"}}" - networkStatuses :: LB.ByteString networkStatuses = #if defined(darwin_HOST_OS) && defined(swiftJSON) @@ -132,12 +116,6 @@ networkStatuses = networkStatusesTagged #endif -networkStatusesSwift :: LB.ByteString -networkStatusesSwift = "{\"resp\":{\"_owsf\":true,\"networkStatuses\":{\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}}" - -networkStatusesTagged :: LB.ByteString -networkStatusesTagged = "{\"resp\":{\"type\":\"networkStatuses\",\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}" - memberSubSummary :: LB.ByteString memberSubSummary = #if defined(darwin_HOST_OS) && defined(swiftJSON) @@ -146,12 +124,6 @@ memberSubSummary = memberSubSummaryTagged #endif -memberSubSummarySwift :: LB.ByteString -memberSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"memberSubSummary\":{\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}}" - -memberSubSummaryTagged :: LB.ByteString -memberSubSummaryTagged = "{\"resp\":{\"type\":\"memberSubSummary\",\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}" - userContactSubSummary :: LB.ByteString userContactSubSummary = #if defined(darwin_HOST_OS) && defined(swiftJSON) @@ -160,12 +132,6 @@ userContactSubSummary = userContactSubSummaryTagged #endif -userContactSubSummarySwift :: LB.ByteString -userContactSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"userContactSubSummary\":{\"user\":" <> userJSON <> ",\"userContactSubscriptions\":[]}}}" - -userContactSubSummaryTagged :: LB.ByteString -userContactSubSummaryTagged = "{\"resp\":{\"type\":\"userContactSubSummary\",\"user\":" <> userJSON <> ",\"userContactSubscriptions\":[]}}" - pendingSubSummary :: LB.ByteString pendingSubSummary = #if defined(darwin_HOST_OS) && defined(swiftJSON) @@ -174,15 +140,6 @@ pendingSubSummary = pendingSubSummaryTagged #endif -pendingSubSummarySwift :: LB.ByteString -pendingSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"pendingSubSummary\":{\"user\":" <> userJSON <> ",\"pendingSubscriptions\":[]}}}" - -pendingSubSummaryTagged :: LB.ByteString -pendingSubSummaryTagged = "{\"resp\":{\"type\":\"pendingSubSummary\",\"user\":" <> userJSON <> ",\"pendingSubscriptions\":[]}}" - -userJSON :: LB.ByteString -userJSON = "{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}" - parsedMarkdown :: LB.ByteString parsedMarkdown = #if defined(darwin_HOST_OS) && defined(swiftJSON) @@ -191,15 +148,10 @@ parsedMarkdown = parsedMarkdownTagged #endif -parsedMarkdownSwift :: LB.ByteString -parsedMarkdownSwift = "{\"formattedText\":[{\"format\":{\"_owsf\":true,\"bold\":{}},\"text\":\"hello\"}]}" - -parsedMarkdownTagged :: LB.ByteString -parsedMarkdownTagged = "{\"formattedText\":[{\"format\":{\"type\":\"bold\"},\"text\":\"hello\"}]}" - -testChatApiNoUser :: FilePath -> IO () -testChatApiNoUser tmp = do - let dbPrefix = tmp </> "1" +testChatApiNoUser :: TestParams -> IO () +testChatApiNoUser ps = do + let tmp = tmpPath ps + dbPrefix = tmp </> "1" Right cc <- chatMigrateInit dbPrefix "" "yesUp" Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "myKey" "yesUp" chatSendCmd cc "/u" `shouldReturn` noActiveUser @@ -207,11 +159,12 @@ testChatApiNoUser tmp = do chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUser chatSendCmd cc "/_start" `shouldReturn` chatStarted -testChatApi :: FilePath -> IO () -testChatApi tmp = do - let dbPrefix = tmp </> "1" - f = chatStoreFile dbPrefix - Right st <- createChatStore f "myKey" False MCYesUp +testChatApi :: TestParams -> IO () +testChatApi ps = do + let tmp = tmpPath ps + dbPrefix = tmp </> "1" + f = dbPrefix <> chatSuffix + Right st <- createChatStore (DBOpts f "myKey" False True DB.TQOff) MCYesUp Right _ <- withTransaction st $ \db -> runExceptT $ createUserRecord db (AgentUserId 1) aliceProfile {preferences = Nothing} True Right cc <- chatMigrateInit dbPrefix "myKey" "yesUp" Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "" "yesUp" @@ -220,15 +173,13 @@ testChatApi tmp = do chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUserExists chatSendCmd cc "/_start" `shouldReturn` chatStarted chatRecvMsg cc `shouldReturn` networkStatuses - chatRecvMsg cc `shouldReturn` userContactSubSummary - chatRecvMsg cc `shouldReturn` memberSubSummary - chatRecvMsgWait cc 10000 `shouldReturn` pendingSubSummary chatRecvMsgWait cc 10000 `shouldReturn` "" chatParseMarkdown "hello" `shouldBe` "{}" chatParseMarkdown "*hello*" `shouldBe` parsedMarkdown -testMediaApi :: HasCallStack => FilePath -> IO () -testMediaApi tmp = do +testMediaApi :: HasCallStack => TestParams -> IO () +testMediaApi ps = do + let tmp = tmpPath ps Right c@ChatController {random = g} <- chatMigrateInit (tmp </> "1") "" "yesUp" cc <- newStablePtr c key <- atomically $ C.randomBytes 32 g @@ -241,8 +192,9 @@ testMediaApi tmp = do B.length encrypted `shouldBe` B.length frame' runExceptT (chatDecryptMedia keyStr encrypted) `shouldReturn` Right frame' -testMediaCApi :: HasCallStack => FilePath -> IO () -testMediaCApi tmp = do +testMediaCApi :: HasCallStack => TestParams -> IO () +testMediaCApi ps = do + let tmp = tmpPath ps Right c@ChatController {random = g} <- chatMigrateInit (tmp </> "1") "" "yesUp" cc <- newStablePtr c key <- atomically $ C.randomBytes 32 g @@ -270,8 +222,9 @@ instance FromJSON WriteFileResult where instance FromJSON ReadFileResult where parseJSON = $(JQ.mkParseJSON (sumTypeJSON $ dropPrefix "RF") ''ReadFileResult) -testFileCApi :: FilePath -> FilePath -> IO () -testFileCApi fileName tmp = do +testFileCApi :: FilePath -> TestParams -> IO () +testFileCApi fileName ps = do + let tmp = tmpPath ps cc <- mkCCPtr tmp src <- B.readFile "./tests/fixtures/test.pdf" let path = tmp </> (fileName <> ".pdf") @@ -291,12 +244,13 @@ testFileCApi fileName tmp = do peek ptr' `shouldReturn` (0 :: Word8) sz :: Word32 <- peek (ptr' `plusPtr` 1) let sz' = fromIntegral sz - contents <- create sz' $ \toPtr -> memcpy toPtr (ptr' `plusPtr` 5) sz' + contents <- create sz' $ \toPtr -> copyBytes toPtr (ptr' `plusPtr` 5) sz' contents `shouldBe` src - sz' `shouldBe` fromIntegral len + sz' `shouldBe` len -testMissingFileCApi :: FilePath -> IO () -testMissingFileCApi tmp = do +testMissingFileCApi :: TestParams -> IO () +testMissingFileCApi ps = do + let tmp = tmpPath ps let path = tmp </> "missing_file" cPath <- newCString path CFArgs key nonce <- atomically . CF.randomArgs =<< C.newRandom @@ -307,8 +261,9 @@ testMissingFileCApi tmp = do err <- peekCAString (ptr `plusPtr` 1) err `shouldContain` "missing_file: openBinaryFile: does not exist" -testFileEncryptionCApi :: FilePath -> FilePath -> IO () -testFileEncryptionCApi fileName tmp = do +testFileEncryptionCApi :: FilePath -> TestParams -> IO () +testFileEncryptionCApi fileName ps = do + let tmp = tmpPath ps cc <- mkCCPtr tmp let fromPath = tmp </> (fileName <> ".source.pdf") copyFile "./tests/fixtures/test.pdf" fromPath @@ -326,8 +281,9 @@ testFileEncryptionCApi fileName tmp = do "" <- peekCAString =<< cChatDecryptFile cToPath cKey cNonce cToPath' B.readFile toPath' `shouldReturn` src -testMissingFileEncryptionCApi :: FilePath -> IO () -testMissingFileEncryptionCApi tmp = do +testMissingFileEncryptionCApi :: TestParams -> IO () +testMissingFileEncryptionCApi ps = do + let tmp = tmpPath ps cc <- mkCCPtr tmp let fromPath = tmp </> "missing_file.source.pdf" toPath = tmp </> "missing_file.encrypted.pdf" @@ -347,7 +303,7 @@ testMissingFileEncryptionCApi tmp = do mkCCPtr :: FilePath -> IO (StablePtr ChatController) mkCCPtr tmp = either (error . show) newStablePtr =<< chatMigrateInit (tmp </> "1") "" "yesUp" -testValidNameCApi :: FilePath -> IO () +testValidNameCApi :: TestParams -> IO () testValidNameCApi _ = do let goodName = "Джон Доу 👍" cName1 <- cChatValidName =<< newCString goodName @@ -355,6 +311,13 @@ testValidNameCApi _ = do cName2 <- cChatValidName =<< newCString " @'Джон' Доу 👍 " peekCString cName2 `shouldReturn` goodName +testChatJsonLengthCApi :: TestParams -> IO () +testChatJsonLengthCApi _ = do + cInt1 <- cChatJsonLength =<< newCString "Hello!" + cInt1 `shouldBe` 6 + cInt2 <- cChatJsonLength =<< newCString "こんにちは!" + cInt2 `shouldBe` 18 + jDecode :: FromJSON a => String -> IO (Maybe a) jDecode = pure . J.decode . LB.pack diff --git a/tests/OperatorTests.hs b/tests/OperatorTests.hs new file mode 100644 index 0000000000..dbfde6a03d --- /dev/null +++ b/tests/OperatorTests.hs @@ -0,0 +1,133 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -Wno-orphans #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} + +module OperatorTests (operatorTests) where + +import Data.Bifunctor (second) +import qualified Data.List.NonEmpty as L +import Simplex.Chat +import Simplex.Chat.Controller (ChatConfig (..), PresetServers (..)) +import Simplex.Chat.Operators +import Simplex.Chat.Operators.Presets +import Simplex.Chat.Types +import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) +import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..), allRoles) +import Simplex.Messaging.Protocol +import Test.Hspec + +operatorTests :: Spec +operatorTests = describe "managing server operators" $ do + validateServersTest + updatedServersTest + +validateServersTest :: Spec +validateServersTest = describe "validate user servers" $ do + it "should pass valid user servers" $ validateUserServers [valid] [] `shouldBe` [] + it "should fail without servers" $ do + validateUserServers [invalidNoServers] [] `shouldBe` [USENoServers aSMP Nothing] + validateUserServers [invalidDisabled] [] `shouldBe` [USENoServers aSMP Nothing] + validateUserServers [invalidDisabledOp] [] `shouldBe` [USENoServers aSMP Nothing, USENoServers aXFTP Nothing] + it "should fail without servers with storage role" $ do + validateUserServers [invalidNoStorage] [] `shouldBe` [USEStorageMissing aSMP Nothing] + it "should fail with duplicate host" $ do + validateUserServers [invalidDuplicate] [] `shouldBe` + [ USEDuplicateServer aSMP "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion" "smp8.simplex.im", + USEDuplicateServer aSMP "smp://abcd@smp8.simplex.im" "smp8.simplex.im" + ] + where + aSMP = AProtocolType SPSMP + aXFTP = AProtocolType SPXFTP + +updatedServersTest :: Spec +updatedServersTest = describe "validate user servers" $ do + it "adding preset operators on first start" $ do + let ops' :: [(Maybe PresetOperator, Maybe AServerOperator)] = + updatedServerOperators operators [] + length ops' `shouldBe` 2 + all addedPreset ops' `shouldBe` True + let ops'' :: [(Maybe PresetOperator, Maybe ServerOperator)] = + saveOps ops' -- mock getUpdateServerOperators + uss <- groupByOperator' (ops'', [], []) -- no stored servers + length uss `shouldBe` 3 + [op1, op2, op3] <- pure $ map updatedUserServers uss + [p1, p2] <- pure operators -- presets + sameServers p1 op1 + sameServers p2 op2 + null (servers' SPSMP op3) `shouldBe` True + null (servers' SPXFTP op3) `shouldBe` True + it "adding preset operators and assiging servers to operator for existing users" $ do + let ops' = updatedServerOperators operators [] + ops'' = saveOps ops' + uss <- + groupByOperator' + ( ops'', + saveSrvs $ take 3 simplexChatSMPServers <> [newUserServer "smp://abcd@smp.example.im"], + saveSrvs $ map (presetServer True) $ L.take 3 defaultXFTPServers + ) + [op1, op2, op3] <- pure $ map updatedUserServers uss + [p1, p2] <- pure operators -- presets + sameServers p1 op1 + sameServers p2 op2 + map srvHost' (servers' SPSMP op3) `shouldBe` [["smp.example.im"]] + null (servers' SPXFTP op3) `shouldBe` True + where + addedPreset = \case + (Just PresetOperator {operator = Just op}, Just (ASO SDBNew op')) -> operatorTag op == operatorTag op' + _ -> False + saveOps = zipWith (\i -> second ((\(ASO _ op) -> op {operatorId = DBEntityId i}) <$>)) [1..] + saveSrvs = zipWith (\i srv -> srv {serverId = DBEntityId i}) [1..] + sameServers preset op = do + map srvHost (pServers SPSMP preset) `shouldBe` map srvHost' (servers' SPSMP op) + map srvHost (pServers SPXFTP preset) `shouldBe` map srvHost' (servers' SPXFTP op) + srvHost' (AUS _ s) = srvHost s + PresetServers {operators} = presetServers defaultChatConfig + +deriving instance Eq User + +deriving instance Eq UserServersError + +valid :: UpdatedUserOperatorServers +valid = + UpdatedUserOperatorServers + { operator = Just operatorSimpleXChat {operatorId = DBEntityId 1}, + smpServers = map (AUS SDBNew) simplexChatSMPServers, + xftpServers = map (AUS SDBNew . presetServer True) $ L.toList defaultXFTPServers + } + +invalidNoServers :: UpdatedUserOperatorServers +invalidNoServers = (valid :: UpdatedUserOperatorServers) {smpServers = []} + +invalidDisabled :: UpdatedUserOperatorServers +invalidDisabled = + (valid :: UpdatedUserOperatorServers) + { smpServers = map (AUS SDBNew . (\srv -> (srv :: NewUserServer 'PSMP) {enabled = False})) simplexChatSMPServers + } + +invalidDisabledOp :: UpdatedUserOperatorServers +invalidDisabledOp = + (valid :: UpdatedUserOperatorServers) + { operator = Just operatorSimpleXChat {operatorId = DBEntityId 1, enabled = False} + } + +invalidNoStorage :: UpdatedUserOperatorServers +invalidNoStorage = + (valid :: UpdatedUserOperatorServers) + { operator = Just operatorSimpleXChat {operatorId = DBEntityId 1, smpRoles = allRoles {storage = False}} + } + +invalidDuplicate :: UpdatedUserOperatorServers +invalidDuplicate = + (valid :: UpdatedUserOperatorServers) + { smpServers = map (AUS SDBNew) $ simplexChatSMPServers <> [presetServer True "smp://abcd@smp8.simplex.im"] + } diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 23fbce249e..1d37a52459 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -2,20 +2,22 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} module ProtocolTests where import qualified Data.Aeson as J import Data.ByteString.Char8 (ByteString) -import qualified Data.ByteString.Lazy.Char8 as LB import Data.Time.Clock.System (SystemTime (..), systemToUTCTime) import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet -import Simplex.Messaging.Protocol (supportedSMPClientVRange) +import Simplex.Messaging.Protocol (EntityId (..), supportedSMPClientVRange) +import Simplex.Messaging.ServiceScheme import Simplex.Messaging.Version import Test.Hspec @@ -31,15 +33,16 @@ queue = supportedSMPClientVRange SMPQueueAddress { smpServer = srv, - senderId = "\223\142z\251", - dhPublicKey = "MCowBQYDK2VuAyEAjiswwI3O/NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o=" + senderId = EntityId "\223\142z\251", + dhPublicKey = "MCowBQYDK2VuAyEAjiswwI3O/NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o=", + queueMode = Nothing } connReqData :: ConnReqUriData connReqData = ConnReqUriData - { crScheme = CRSSimplex, - crAgentVRange = mkVersionRange 1 1, + { crScheme = SSSimplex, + crAgentVRange = mkVersionRange (VersionSMPA 1) (VersionSMPA 1), crSmpQueues = [queue], crClientData = Nothing } @@ -47,8 +50,8 @@ connReqData = testDhPubKey :: C.PublicKeyX448 testDhPubKey = "MEIwBQYDK2VvAzkAmKuSYeQ/m0SixPDS8Wq8VBaTS1cW+Lp0n0h4Diu+kUpR+qXx4SDJ32YGEFoGFGSbGPry5Ychr6U=" -testE2ERatchetParams :: E2ERatchetParamsUri 'C.X448 -testE2ERatchetParams = E2ERatchetParamsUri supportedE2EEncryptVRange testDhPubKey testDhPubKey +testE2ERatchetParams :: RcvE2ERatchetParamsUri 'C.X448 +testE2ERatchetParams = E2ERatchetParamsUri supportedE2EEncryptVRange testDhPubKey testDhPubKey Nothing testConnReq :: ConnectionRequestUri 'CMInvitation testConnReq = CRInvitationUri connReqData testE2ERatchetParams @@ -71,12 +74,12 @@ s ==## msg = do (##==) :: MsgEncodingI e => ByteString -> ChatMessage e -> Expectation s ##== msg = do - let r = encodeChatMessage msg + let r = encodeChatMessage maxEncodedMsgLength msg case r of ECMEncoded encodedBody -> - J.eitherDecodeStrict' (LB.toStrict encodedBody) + J.eitherDecodeStrict' encodedBody `shouldBe` (J.eitherDecodeStrict' s :: Either String J.Value) - ECMLarge -> expectationFailure $ "large message" + ECMLarge -> expectationFailure "large message" (##==##) :: MsgEncodingI e => ByteString -> ChatMessage e -> Expectation s ##==## msg = do @@ -98,13 +101,13 @@ testChatPreferences :: Maybe Preferences testChatPreferences = Just Preferences {voice = Just VoicePreference {allow = FAYes}, fullDelete = Nothing, timedMessages = Nothing, calls = Nothing, reactions = Just ReactionsPreference {allow = FAYes}} testGroupPreferences :: Maybe GroupPreferences -testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn}, files = Nothing, fullDelete = Nothing, history = 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} testProfile :: Profile testProfile = Profile {displayName = "alice", fullName = "Alice", image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), contactLink = Nothing, preferences = testChatPreferences} testGroupProfile :: GroupProfile -testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, image = Nothing, groupPreferences = testGroupPreferences} +testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, image = Nothing, groupPreferences = testGroupPreferences, memberAdmission = Nothing} decodeChatMessageTest :: Spec decodeChatMessageTest = describe "Chat message encoding/decoding" $ do @@ -113,10 +116,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do #==# XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)) 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)) + #==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") [] Nothing (Just 3600) Nothing)) 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))) + #==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") [] Nothing Nothing (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)) @@ -130,7 +133,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) it "x.msg.new chat message with chat version range" $ - "{\"v\":\"1-5\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"1-14\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) it "x.msg.new quote" $ "{\"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\"}}}}" @@ -143,22 +146,22 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") - (XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") Nothing (Just 3600) Nothing))) + (XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") [] Nothing (Just 3600) Nothing))) 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)))) + (XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") [] Nothing Nothing (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)) 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)) + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") [] Nothing (Just 3600) Nothing)) 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))) + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") [] Nothing Nothing (Just True))) it "x.msg.new simple text with file" $ "{\"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}))) @@ -179,12 +182,18 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do ) ) ) + 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))) 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}))) 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 + #==# XMsgUpdate (SharedMsgId "\1\2\3\4") (MCText "hello") [] Nothing Nothing it "x.msg.del" $ "{\"v\":\"1\",\"event\":\"x.msg.del\",\"params\":{\"msgId\":\"AQIDBA==\"}}" #==# XMsgDel (SharedMsgId "\1\2\3\4") Nothing @@ -192,7 +201,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.msg.deleted\",\"params\":{}}" #==# XMsgDeleted it "x.file" $ - "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" + "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" #==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Just testConnReq, fileInline = Nothing, fileDescr = Nothing} it "x.file without file invitation" $ "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" @@ -201,7 +210,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}" #==# XFileAcpt "photo.jpg" it "x.file.acpt.inv" $ - "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" + "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" #==# XFileAcptInv (SharedMsgId "\1\2\3\4") (Just testConnReq) "photo.jpg" it "x.file.acpt.inv" $ "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\"}}" @@ -228,11 +237,11 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" ==# XContact testProfile Nothing it "x.grp.inv" $ - "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" - #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Nothing} + "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" + #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, business = Nothing, groupLinkId = Nothing, groupSize = Nothing} it "x.grp.inv with group link id" $ - "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}" - #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Just $ GroupLinkId "\1\2\3\4"} + "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}" + #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, business = Nothing, groupLinkId = Just $ GroupLinkId "\1\2\3\4", groupSize = Nothing} it "x.grp.acpt without incognito profile" $ "{\"v\":\"1\",\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}" #==# XGrpAcpt (MemberId "\1\2\3\4") @@ -240,25 +249,28 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} it "x.grp.mem.new with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-5\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-14\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} it "x.grp.mem.intro" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" - #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} + #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} Nothing it "x.grp.mem.intro with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-5\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" - #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} + "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-14\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} Nothing + it "x.grp.mem.intro with member restrictions" $ + "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} (Just MemberRestrictions {restriction = MRSBlocked}) it "x.grp.mem.inv" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} it "x.grp.mem.inv w/t directConnReq" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.fwd" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-5\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-14\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.info" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" @@ -271,7 +283,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do #==# XGrpMemConAll (MemberId "\1\2\3\4") it "x.grp.mem.del" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\"}}" - #==# XGrpMemDel (MemberId "\1\2\3\4") + #==# XGrpMemDel (MemberId "\1\2\3\4") False it "x.grp.leave" $ "{\"v\":\"1\",\"event\":\"x.grp.leave\",\"params\":{}}" ==# XGrpLeave @@ -279,10 +291,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.del\",\"params\":{}}" ==# XGrpDel it "x.grp.direct.inv" $ - "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" #==# XGrpDirectInv testConnReq (Just $ MCText "hello") it "x.grp.direct.inv without content" $ - "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" + "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" #==# XGrpDirectInv testConnReq Nothing -- it "x.grp.msg.forward" -- $ "{\"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\"}}}" diff --git a/tests/RandomServers.hs b/tests/RandomServers.hs new file mode 100644 index 0000000000..9b83be26c4 --- /dev/null +++ b/tests/RandomServers.hs @@ -0,0 +1,66 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TypeSynonymInstances #-} +{-# OPTIONS_GHC -Wno-orphans #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} + +module RandomServers where + +import Control.Monad (replicateM) +import Data.Foldable (foldMap') +import Data.List (sortOn) +import Data.List.NonEmpty (NonEmpty) +import Data.Monoid (Sum (..)) +import Simplex.Chat (defaultChatConfig, chooseRandomServers) +import Simplex.Chat.Controller (ChatConfig (..), PresetServers (..)) +import Simplex.Chat.Operators +import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..)) +import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), SProtocolType (..), UserProtocol) +import Test.Hspec + +randomServersTests :: Spec +randomServersTests = describe "choosig random servers" $ do + it "should choose 4 + 3 random SMP servers and keep the rest disabled" testRandomSMPServers + it "should choose 3 + 3 random XFTP servers and keep the rest disabled" testRandomXFTPServers + +deriving instance Eq ServerRoles + +deriving instance Eq (UserServer' s p) + +testRandomSMPServers :: IO () +testRandomSMPServers = do + [srvs1, srvs2, srvs3] <- + replicateM 3 $ + checkEnabled SPSMP 7 False =<< chooseRandomServers (presetServers defaultChatConfig) + (srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` False -- && to avoid rare failures + +testRandomXFTPServers :: IO () +testRandomXFTPServers = do + [srvs1, srvs2, srvs3] <- + replicateM 3 $ + checkEnabled SPXFTP 6 False =<< chooseRandomServers (presetServers defaultChatConfig) + (srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` False -- && to avoid rare failures + +checkEnabled :: UserProtocol p => SProtocolType p -> Int -> Bool -> NonEmpty (PresetOperator) -> IO [NewUserServer p] +checkEnabled p n allUsed presetOps' = do + let PresetServers {operators = presetOps} = presetServers defaultChatConfig + presetSrvs = sortOn server' $ concatMap (pServers p) presetOps + srvs' = sortOn server' $ concatMap (pServers p) presetOps' + Sum toUse = foldMap' (Sum . operatorServersToUse p) presetOps + Sum toUse' = foldMap' (Sum . operatorServersToUse p) presetOps' + length presetOps `shouldBe` length presetOps' + toUse `shouldBe` toUse' + srvs' == presetSrvs `shouldBe` allUsed + map enable srvs' `shouldBe` map enable presetSrvs + let enbldSrvs = filter (\UserServer {enabled} -> enabled) srvs' + toUse `shouldBe` n + length enbldSrvs `shouldBe` n + pure enbldSrvs + where + server' UserServer {server = ProtoServerWithAuth srv _} = srv + enable :: forall p. NewUserServer p -> NewUserServer p + enable srv = (srv :: NewUserServer p) {enabled = False} diff --git a/tests/RemoteTests.hs b/tests/RemoteTests.hs index ff0e5cb2d1..f18b097e9c 100644 --- a/tests/RemoteTests.hs +++ b/tests/RemoteTests.hs @@ -6,27 +6,28 @@ module RemoteTests where import ChatClient +import ChatTests.DBUtils import ChatTests.Utils import Control.Logger.Simple import qualified Data.Aeson as J import qualified Data.ByteString as B import qualified Data.ByteString.Lazy.Char8 as LB import qualified Data.Map.Strict as M -import Simplex.Chat.Archive (archiveFilesFolder) -import Simplex.Chat.Controller (ChatConfig (..), XFTPFileConfig (..), versionNumber) +import Simplex.Chat.Controller (versionNumber) import qualified Simplex.Chat.Controller as Controller import Simplex.Chat.Mobile.File +import Simplex.Chat.Remote (remoteFilesFolder) import Simplex.Chat.Remote.Types import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) import Simplex.Messaging.Encoding.String (strEncode) import Simplex.Messaging.Util import System.FilePath ((</>)) -import Test.Hspec +import Test.Hspec hiding (it) import UnliftIO import UnliftIO.Concurrent import UnliftIO.Directory -remoteTests :: SpecWith FilePath +remoteTests :: SpecWith TestParams remoteTests = describe "Remote" $ do describe "protocol handshake" $ do it "connects with new pairing (stops mobile)" $ remoteHandshakeTest False @@ -45,7 +46,7 @@ remoteTests = describe "Remote" $ do -- * Chat commands -remoteHandshakeTest :: HasCallStack => Bool -> FilePath -> IO () +remoteHandshakeTest :: HasCallStack => Bool -> TestParams -> IO () remoteHandshakeTest viaDesktop = testChat2 aliceProfile aliceDesktopProfile $ \mobile desktop -> do desktop ##> "/list remote hosts" desktop <## "No remote hosts" @@ -74,7 +75,7 @@ remoteHandshakeTest viaDesktop = testChat2 aliceProfile aliceDesktopProfile $ \m mobile ##> "/list remote ctrls" mobile <## "No remote controllers" -remoteHandshakeStoredTest :: HasCallStack => FilePath -> IO () +remoteHandshakeStoredTest :: HasCallStack => TestParams -> IO () remoteHandshakeStoredTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile desktop -> do logNote "Starting new session" startRemote mobile desktop @@ -95,7 +96,7 @@ remoteHandshakeStoredTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile startRemoteStored mobile desktop stopMobile mobile desktop `catchAny` (logError . tshow) -remoteHandshakeDiscoverTest :: HasCallStack => FilePath -> IO () +remoteHandshakeDiscoverTest :: HasCallStack => TestParams -> IO () remoteHandshakeDiscoverTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile desktop -> do logNote "Preparing new session" startRemote mobile desktop @@ -105,7 +106,7 @@ remoteHandshakeDiscoverTest = testChat2 aliceProfile aliceDesktopProfile $ \mobi startRemoteDiscover mobile desktop stopMobile mobile desktop `catchAny` (logError . tshow) -remoteHandshakeRejectTest :: HasCallStack => FilePath -> IO () +remoteHandshakeRejectTest :: HasCallStack => TestParams -> IO () remoteHandshakeRejectTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop mobileBob -> do logNote "Starting new session" startRemote mobile desktop @@ -119,7 +120,7 @@ remoteHandshakeRejectTest = testChat3 aliceProfile aliceDesktopProfile bobProfil inv <- getTermLine desktop mobileBob ##> ("/connect remote ctrl " <> inv) mobileBob <## ("connecting new remote controller: My desktop, v" <> versionNumber) - mobileBob <## "remote controller stopped" + mobileBob <## "remote controller stopped: this link was used with another controller, please create a new link on the host" -- the server remains active after rejecting invalid client mobile ##> ("/connect remote ctrl " <> inv) @@ -135,14 +136,14 @@ remoteHandshakeRejectTest = testChat3 aliceProfile aliceDesktopProfile bobProfil desktop <## "remote host 1 connected" stopMobile mobile desktop -storedBindingsTest :: HasCallStack => FilePath -> IO () +storedBindingsTest :: HasCallStack => TestParams -> IO () storedBindingsTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile desktop -> do desktop ##> "/set device name My desktop" desktop <## "ok" mobile ##> "/set device name Mobile" mobile <## "ok" - desktop ##> "/start remote host new addr=127.0.0.1 iface=lo port=52230" + desktop ##> "/start remote host new addr=127.0.0.1 iface=\"lo\" port=52230" desktop <##. "new remote host started on 127.0.0.1:52230" -- TODO: show ip? desktop <## "Remote session invitation:" inv <- getTermLine desktop @@ -166,7 +167,7 @@ storedBindingsTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile deskto -- TODO: more parser tests -remoteMessageTest :: HasCallStack => FilePath -> IO () +remoteMessageTest :: HasCallStack => TestParams -> IO () remoteMessageTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> do startRemote mobile desktop contactBob desktop bob @@ -192,9 +193,9 @@ remoteMessageTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mob threadDelay 1000000 logNote "done" -remoteStoreFileTest :: HasCallStack => FilePath -> IO () +remoteStoreFileTest :: HasCallStack => TestParams -> IO () remoteStoreFileTest = - testChatCfg3 cfg aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> + testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> withXFTPServer $ do let mobileFiles = "./tests/tmp/mobile_files" mobile ##> ("/_files_folder " <> mobileFiles) @@ -214,7 +215,7 @@ remoteStoreFileTest = rhs <- readTVarIO (Controller.remoteHostSessions $ chatController desktop) desktopHostStore <- case M.lookup (RHId 1) rhs of - Just (_, RHSessionConnected {storePath}) -> pure $ desktopHostFiles </> storePath </> archiveFilesFolder + Just (_, RHSessionConnected {storePath}) -> pure $ desktopHostFiles </> storePath </> remoteFilesFolder _ -> fail "Host session 1 should be started" desktop ##> "/store remote file 1 tests/fixtures/test.pdf" desktop <## "file test.pdf stored on remote host 1" @@ -238,7 +239,7 @@ remoteStoreFileTest = desktop ##> "/get remote file 1 {\"userId\": 1, \"fileId\": 1, \"sent\": true, \"fileSource\": {\"filePath\": \"test_1.pdf\"}}" hostError desktop "SEFileNotFound" -- send file not encrypted locally on mobile host - desktop ##> "/_send @2 json {\"filePath\": \"test_1.pdf\", \"msgContent\": {\"type\": \"file\", \"text\": \"sending a file\"}}" + desktop ##> "/_send @2 json [{\"filePath\": \"test_1.pdf\", \"msgContent\": {\"type\": \"file\", \"text\": \"sending a file\"}}]" desktop <# "@bob sending a file" desktop <# "/f @bob test_1.pdf" desktop <## "use /fc 1 to cancel sending" @@ -268,7 +269,7 @@ remoteStoreFileTest = B.readFile (desktopHostStore </> "test_1.pdf") `shouldReturn` src -- send file encrypted locally on mobile host - desktop ##> ("/_send @2 json {\"fileSource\": {\"filePath\":\"test_2.pdf\", \"cryptoArgs\": " <> LB.unpack (J.encode cfArgs) <> "}, \"msgContent\": {\"type\": \"file\", \"text\": \"\"}}") + desktop ##> ("/_send @2 json [{\"fileSource\": {\"filePath\":\"test_2.pdf\", \"cryptoArgs\": " <> LB.unpack (J.encode cfArgs) <> "}, \"msgContent\": {\"type\": \"file\", \"text\": \"\"}}]") desktop <# "/f @bob test_2.pdf" desktop <## "use /fc 2 to cancel sending" bob <# "alice> sends file test_2.pdf (266.0 KiB / 272376 bytes)" @@ -317,15 +318,13 @@ remoteStoreFileTest = stopMobile mobile desktop where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp/tmp"} hostError cc err = do r <- getTermLine cc r `shouldStartWith` "remote host 1 error" r `shouldContain` err -remoteCLIFileTest :: HasCallStack => FilePath -> IO () -remoteCLIFileTest = testChatCfg3 cfg aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> withXFTPServer $ do - createDirectoryIfMissing True "./tests/tmp/tmp/" +remoteCLIFileTest :: HasCallStack => TestParams -> IO () +remoteCLIFileTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> withXFTPServer $ do let mobileFiles = "./tests/tmp/mobile_files" mobile ##> ("/_files_folder " <> mobileFiles) mobile <## "ok" @@ -340,7 +339,7 @@ remoteCLIFileTest = testChatCfg3 cfg aliceProfile aliceDesktopProfile bobProfile rhs <- readTVarIO (Controller.remoteHostSessions $ chatController desktop) desktopHostStore <- case M.lookup (RHId 1) rhs of - Just (_, RHSessionConnected {storePath}) -> pure $ desktopHostFiles </> storePath </> archiveFilesFolder + Just (_, RHSessionConnected {storePath}) -> pure $ desktopHostFiles </> storePath </> remoteFilesFolder _ -> fail "Host session 1 should be started" mobileName <- userName mobile @@ -392,10 +391,8 @@ remoteCLIFileTest = testChatCfg3 cfg aliceProfile aliceDesktopProfile bobProfile B.readFile (bobFiles </> "test.jpg") `shouldReturn` src' stopMobile mobile desktop - where - cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp/tmp"} -switchRemoteHostTest :: FilePath -> IO () +switchRemoteHostTest :: TestParams -> IO () switchRemoteHostTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> do startRemote mobile desktop contactBob desktop bob @@ -421,7 +418,7 @@ switchRemoteHostTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \ desktop <## "remote host 1 error: RHEInactive" desktop ##> "/contacts" -indicateRemoteHostTest :: FilePath -> IO () +indicateRemoteHostTest :: TestParams -> IO () indicateRemoteHostTest = testChat4 aliceProfile aliceDesktopProfile bobProfile cathProfile $ \mobile desktop bob cath -> do connectUsers desktop cath startRemote mobile desktop @@ -445,7 +442,7 @@ indicateRemoteHostTest = testChat4 aliceProfile aliceDesktopProfile bobProfile c desktop <##> cath cath <##> desktop -multipleProfilesTest :: FilePath -> IO () +multipleProfilesTest :: TestParams -> IO () multipleProfilesTest = testChat4 aliceProfile aliceDesktopProfile bobProfile cathProfile $ \mobile desktop bob cath -> do connectUsers desktop cath diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs index 77269a9b33..807fa4a0cb 100644 --- a/tests/SchemaDump.hs +++ b/tests/SchemaDump.hs @@ -1,19 +1,33 @@ +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} module SchemaDump where import ChatClient (withTmpFiles) +import ChatTests.DBUtils +import Control.Concurrent.STM import Control.DeepSeq +import qualified Control.Exception as E import Control.Monad (unless, void) -import Data.List (dropWhileEnd) +import Data.List (dropWhileEnd, sort) +import qualified Data.Map.Strict as M import Data.Maybe (fromJust, isJust) +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.IO as T +import Database.SQLite.Simple (Query (..)) import Simplex.Chat.Store (createChatStore) import qualified Simplex.Chat.Store as Store -import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), closeSQLiteStore, createSQLiteStore) -import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..), MigrationsToRun (..), toDownMigration) +import Simplex.Messaging.Agent.Env.SQLite (createAgentStore) +import Simplex.Messaging.Agent.Store.Common (withConnection) +import Simplex.Messaging.Agent.Store.DB (TrackQueries (..)) +import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Agent.Store.Interface import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations -import Simplex.Messaging.Util (ifM, whenM) +import Simplex.Messaging.Agent.Store.Shared (Migration (..), MigrationConfirmation (..), MigrationsToRun (..), toDownMigration) +import Simplex.Messaging.Util (ifM, tshow, whenM) import System.Directory (doesFileExist, removeFile) import System.Process (readCreateProcess, shell) import Test.Hspec @@ -21,8 +35,20 @@ import Test.Hspec testDB :: FilePath testDB = "tests/tmp/test_chat.db" +testAgentDB :: FilePath +testAgentDB = "tests/tmp/test_agent.db" + appSchema :: FilePath -appSchema = "src/Simplex/Chat/Migrations/chat_schema.sql" +appSchema = "src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql" + +appLint :: FilePath +appLint = "src/Simplex/Chat/Store/SQLite/Migrations/chat_lint.sql" + +appChatQueryPlans :: FilePath +appChatQueryPlans = "src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt" + +appAgentQueryPlans :: FilePath +appAgentQueryPlans = "src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt" testSchema :: FilePath testSchema = "tests/tmp/test_agent_schema.sql" @@ -30,22 +56,31 @@ testSchema = "tests/tmp/test_agent_schema.sql" schemaDumpTest :: Spec schemaDumpTest = do it "verify and overwrite schema dump" testVerifySchemaDump + it "verify .lint fkey-indexes" testVerifyLintFKeyIndexes it "verify schema down migrations" testSchemaMigrations testVerifySchemaDump :: IO () testVerifySchemaDump = withTmpFiles $ do savedSchema <- ifM (doesFileExist appSchema) (readFile appSchema) (pure "") savedSchema `deepseq` pure () - void $ createChatStore testDB "" False MCError + void $ createChatStore (DBOpts testDB "" False True TQOff) MCError getSchema testDB appSchema `shouldReturn` savedSchema removeFile testDB +testVerifyLintFKeyIndexes :: IO () +testVerifyLintFKeyIndexes = withTmpFiles $ do + savedLint <- ifM (doesFileExist appLint) (readFile appLint) (pure "") + savedLint `deepseq` pure () + void $ createChatStore (DBOpts testDB "" False True TQOff) MCError + getLintFKeyIndexes testDB "tests/tmp/chat_lint.sql" `shouldReturn` savedLint + removeFile testDB + testSchemaMigrations :: IO () testSchemaMigrations = withTmpFiles $ do let noDownMigrations = dropWhileEnd (\Migration {down} -> isJust down) Store.migrations - Right st <- createSQLiteStore testDB "" False noDownMigrations MCError + Right st <- createDBStore (DBOpts testDB "" False True TQOff) noDownMigrations MCError mapM_ (testDownMigration st) $ drop (length noDownMigrations) Store.migrations - closeSQLiteStore st + closeDBStore st removeFile testDB whenM (doesFileExist testSchema) $ removeFile testSchema where @@ -53,17 +88,24 @@ testSchemaMigrations = withTmpFiles $ do putStrLn $ "down migration " <> name m let downMigr = fromJust $ toDownMigration m schema <- getSchema testDB testSchema - Migrations.run st $ MTRUp [m] + Migrations.run st True $ MTRUp [m] schema' <- getSchema testDB testSchema - schema' `shouldNotBe` schema - Migrations.run st $ MTRDown [downMigr] + unless (name m `elem` skipComparisonForUpMigrations) $ + schema' `shouldNotBe` schema + Migrations.run st True $ MTRDown [downMigr] unless (name m `elem` skipComparisonForDownMigrations) $ do schema'' <- getSchema testDB testSchema schema'' `shouldBe` schema - Migrations.run st $ MTRUp [m] + Migrations.run st True $ MTRUp [m] schema''' <- getSchema testDB testSchema schema''' `shouldBe` schema' +skipComparisonForUpMigrations :: [String] +skipComparisonForUpMigrations = + [ -- schema doesn't change + "20250129_delete_unused_contacts" + ] + skipComparisonForDownMigrations :: [String] skipComparisonForDownMigrations = [ -- on down migration msg_delivery_events table moves down to the end of the file @@ -75,11 +117,69 @@ skipComparisonForDownMigrations = -- on down migration idx_connections_via_contact_uri_hash index moves down to the end of the file "20231019_indexes", -- table and indexes move down to the end of the file - "20231215_recreate_msg_deliveries" + "20231215_recreate_msg_deliveries", + -- on down migration idx_msg_deliveries_agent_ack_cmd_id index moves down to the end of the file + "20240313_drop_agent_ack_cmd_id", + -- sequence table moves down to the end of the file + "20241023_chat_item_autoincrement_id", + -- indexes move down to the end of the file + "20241125_indexes", + -- indexes move down to the end of the file + "20250130_indexes", + -- index moves down to the end of the file + "20250227_member_acceptance" ] getSchema :: FilePath -> FilePath -> IO String -getSchema dpPath schemaPath = do - void $ readCreateProcess (shell $ "sqlite3 " <> dpPath <> " '.schema --indent' > " <> schemaPath) "" +getSchema dbPath schemaPath = do + void $ readCreateProcess (shell $ "sqlite3 " <> dbPath <> " '.schema --indent' > " <> schemaPath) "" sch <- readFile schemaPath sch `deepseq` pure sch + +getLintFKeyIndexes :: FilePath -> FilePath -> IO String +getLintFKeyIndexes dbPath lintPath = do + void $ readCreateProcess (shell $ "sqlite3 " <> dbPath <> " '.lint fkey-indexes' > " <> lintPath) "" + lint <- readFile lintPath + lint `deepseq` pure lint + +saveQueryPlans :: SpecWith TestParams +saveQueryPlans = it "verify and overwrite query plans" $ \TestParams {chatQueryStats, agentQueryStats} -> do + (chatSavedPlans, chatSavedPlans') <- + updatePlans + appChatQueryPlans + chatQueryStats + (createChatStore (DBOpts testDB "" False True TQOff) MCError) + (\db -> do + DB.execute_ db "CREATE TABLE IF NOT EXISTS temp_conn_ids (conn_id BLOB)" + DB.execute_ db "CREATE TABLE IF NOT EXISTS temp_delete_members (contact_profile_id INTEGER, member_profile_id INTEGER, local_display_name TEXT)" + ) + (agentSavedPlans, agentSavedPlans') <- + updatePlans + appAgentQueryPlans + agentQueryStats + (createAgentStore (DBOpts testAgentDB "" False True TQOff) MCError) + (const $ pure ()) + chatSavedPlans' == chatSavedPlans `shouldBe` True + agentSavedPlans' == agentSavedPlans `shouldBe` True + removeFile testDB + removeFile testAgentDB + where + updatePlans plansFile statsSel createStore prepareStore = do + savedPlans <- ifM (doesFileExist plansFile) (T.readFile plansFile) (pure "") + savedPlans `deepseq` pure () + queries <- sort . M.keys <$> readTVarIO statsSel + Right st <- createStore + plans' <- withConnection st $ \db -> do + void $ prepareStore db + mapM (getQueryPlan db) queries + let savedPlans' = T.unlines plans' + T.writeFile plansFile savedPlans' + pure (savedPlans, savedPlans') + getQueryPlan :: DB.Connection -> Query -> IO Text + getQueryPlan db q = + (("Query: " <> fromQuery q) <>) . result <$> E.try (DB.query_ db $ "explain query plan " <> q) + result = \case + Right r -> "\nPlan:\n" <> T.unlines (map planDetail r) + Left (e :: E.SomeException) -> "\nError: " <> tshow e <> "\n" + planDetail :: (Int, Int, Int, Text) -> Text + planDetail (_, _, _, detail) = detail diff --git a/tests/Test.hs b/tests/Test.hs index 9a942c556b..1d8d45ebb4 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -1,43 +1,86 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE TupleSections #-} + import Bots.BroadcastTests import Bots.DirectoryTests import ChatClient import ChatTests +import ChatTests.DBUtils import ChatTests.Utils (xdescribe'') import Control.Logger.Simple import Data.Time.Clock.System import JSONTests import MarkdownTests import MessageBatching -import MobileTests import ProtocolTests +import OperatorTests +import RandomServers import RemoteTests -import SchemaDump -import Test.Hspec +import Test.Hspec hiding (it) import UnliftIO.Temporary (withTempDirectory) import ValidNames import ViewTests +#if defined(dbPostgres) +import Simplex.Messaging.Agent.Store.Postgres.Util (createDBAndUserIfNotExists, dropAllSchemasExceptSystem, dropDatabaseAndUser) +#else +import qualified Simplex.Messaging.TMap as TM +import MobileTests +import SchemaDump import WebRTCTests +#endif main :: IO () main = do setLogLevel LogError - withGlobalLogging logCfg . hspec $ do - describe "Schema dump" schemaDumpTest - describe "SimpleX chat markdown" markdownTests - describe "JSON Tests" jsonTests - describe "SimpleX chat view" viewTests - describe "SimpleX chat protocol" protocolTests - around tmpBracket $ describe "WebRTC encryption" webRTCTests - describe "Valid names" validNameTests - describe "Message batching" batchingTests - around testBracket $ do - describe "Mobile API Tests" mobileTests - describe "SimpleX chat client" chatTests - xdescribe'' "SimpleX Broadcast bot" broadcastBotTests - xdescribe'' "SimpleX Directory service bot" directoryServiceTests - describe "Remote session" remoteTests +#if !defined(dbPostgres) + chatQueryStats <- TM.emptyIO + agentQueryStats <- TM.emptyIO +#endif + withGlobalLogging logCfg . hspec +#if defined(dbPostgres) + . beforeAll_ (dropDatabaseAndUser testDBConnectInfo >> createDBAndUserIfNotExists testDBConnectInfo) + . afterAll_ (dropDatabaseAndUser testDBConnectInfo) +#endif + $ do +-- TODO [postgres] schema dump for postgres +#if !defined(dbPostgres) + describe "Schema dump" schemaDumpTest + around tmpBracket $ describe "WebRTC encryption" webRTCTests +#endif + describe "SimpleX chat markdown" markdownTests + describe "JSON Tests" jsonTests + describe "SimpleX chat view" viewTests + describe "SimpleX chat protocol" protocolTests + describe "Valid names" validNameTests + describe "Message batching" batchingTests + describe "Operators" operatorTests + describe "Random servers" randomServersTests +#if defined(dbPostgres) + around testBracket + . after_ (dropAllSchemasExceptSystem testDBConnectInfo) +#else + around (testBracket chatQueryStats agentQueryStats) +#endif + $ do +#if !defined(dbPostgres) + describe "Mobile API Tests" mobileTests +#endif + describe "SimpleX chat client" chatTests + xdescribe'' "SimpleX Broadcast bot" broadcastBotTests + xdescribe'' "SimpleX Directory service bot" directoryServiceTests + describe "Remote session" remoteTests +#if !defined(dbPostgres) + xdescribe'' "Save query plans" saveQueryPlans +#endif where - testBracket test = withSmpServer $ tmpBracket test +#if defined(dbPostgres) + testBracket test = withSmpServer $ tmpBracket $ \tmpPath -> test TestParams {tmpPath, printOutput = False} +#else + testBracket chatQueryStats agentQueryStats test = + withSmpServer $ tmpBracket $ \tmpPath -> test TestParams {tmpPath, chatQueryStats, agentQueryStats, printOutput = False} +#endif tmpBracket test = do t <- getSystemTime let ts = show (systemSeconds t) <> show (systemNanoseconds t) diff --git a/tests/ValidNames.hs b/tests/ValidNames.hs index 0700d80846..22ac4a695d 100644 --- a/tests/ValidNames.hs +++ b/tests/ValidNames.hs @@ -1,6 +1,6 @@ module ValidNames where -import Simplex.Chat +import Simplex.Chat.Library.Commands import Test.Hspec validNameTests :: Spec @@ -21,6 +21,7 @@ testMkValidName = do mkValidName "J . . Doe" `shouldBe` "J . Doe" mkValidName "@alice" `shouldBe` "alice" mkValidName "#alice" `shouldBe` "alice" + mkValidName "'alice" `shouldBe` "alice" mkValidName " alice" `shouldBe` "alice" mkValidName "alice " `shouldBe` "alice" mkValidName "John Doe" `shouldBe` "John Doe" @@ -37,3 +38,5 @@ testMkValidName = do mkValidName "alice@example.com" `shouldBe` "alice@example.com" mkValidName "alice <> bob" `shouldBe` "alice <> bob" mkValidName "alice -> bob" `shouldBe` "alice -> bob" + mkValidName "01234567890123456789012345678901234567890123456789extra" `shouldBe` "01234567890123456789012345678901234567890123456789" + mkValidName "0123456789012345678901234567890123456789012345678 extra" `shouldBe` "0123456789012345678901234567890123456789012345678" diff --git a/website/.eleventy.js b/website/.eleventy.js index 09fc7c2c44..a0a35f3366 100644 --- a/website/.eleventy.js +++ b/website/.eleventy.js @@ -6,6 +6,7 @@ const uri = require('fast-uri') const i18n = require('eleventy-plugin-i18n') const fs = require("fs") const path = require("path") +const matter = require('gray-matter') const pluginRss = require('@11ty/eleventy-plugin-rss') const { JSDOM } = require('jsdom') @@ -388,13 +389,36 @@ module.exports = function (ty) { linkify: true, replaceLink: function (link, _env) { let parsed = uri.parse(link) - if (parsed.scheme || parsed.host || !parsed.path.endsWith(".md")) { - return link + if (parsed.scheme || parsed.host) return link + + let hostFile = path.resolve(_env.page.inputPath) + let linkFile = path.resolve(hostFile, '..', parsed.path) + if (parsed.path.startsWith('/')) { + let srcIndex = hostFile.indexOf("/src") + if (srcIndex !== -1) { + linkFile = path.join(hostFile.slice(0, srcIndex + 4), parsed.path) + } } - if (parsed.path.startsWith("../../blog")) { - parsed.path = parsed.path.replace("../../blog", "/blog") + + if (fs.existsSync(linkFile) && fs.statSync(linkFile).isFile()) { + // this condition works if the link is a valid website file + const fileContent = fs.readFileSync(linkFile, 'utf8') + parsed.path = (matter(fileContent).data?.permalink || parsed.path).replace(/\.md$/, ".html").toLowerCase() + } else if (!fs.existsSync(linkFile)) { + linkFile = linkFile.replace('/website/src', '') + if (fs.existsSync(linkFile)) { + // this condition works if the link is a valid project file + const githubUrl = "https://github.com/simplex-chat/simplex-chat/blob/stable" + const keyword = "/simplex-chat" + index = linkFile.indexOf(keyword) + linkFile = linkFile.substring(index + keyword.length) + parsed.path = `${githubUrl}${linkFile}` + } else { + // if the link is not a valid website file or project file + throw new Error(`Broken link: ${parsed.path} in ${hostFile}`) + } } - parsed.path = parsed.path.replace(/\.md$/, ".html").toLowerCase() + return uri.serialize(parsed) } }).use(markdownItAnchor, { @@ -419,4 +443,4 @@ module.exports = function (ty) { htmlTemplateEngine: 'njk', dataTemplateEngine: 'njk', } -} +} \ No newline at end of file diff --git a/website/langs/ar.json b/website/langs/ar.json index 73a0da295a..0c1fb4515a 100644 --- a/website/langs/ar.json +++ b/website/langs/ar.json @@ -8,60 +8,60 @@ "simplex-explained-tab-1-text": "1. تجربة المستخدم", "simplex-privacy": "خصوصية SimpleX", "simplex-explained-tab-3-text": "3. ما الذي تراة الخوادم", - "simplex-explained-tab-1-p-1": "يمكنك إنشاء جهات اتصال ومجموعات وإجراء محادثات ثنائية الاتجاه، كما هو الحال في أي برنامج مراسلة آخر.", - "simplex-explained-tab-3-p-2": "يمكن للمستخدمين زيادة تحسين خصوصية البيانات الوصفية باستخدام Tor للوصول إلى الخوادم، ومنع الارتباط بواسطة عنوان IP.", + "simplex-explained-tab-1-p-1": "يمكنك إنشاء جهات اتصال ومجموعات وإجراء مُحادثات ثنائية الاتجاه، كما هو الحال في أي برنامج مُراسلة آخر.", + "simplex-explained-tab-3-p-2": "يمكن للمستخدمين زيادة تحسين خصوصية البيانات الوصفية باستخدام تور للوصول إلى الخوادم، ومنع الارتباط بواسطة عنوان IP.", "chat-bot-example": "مثال بوت الدردشة", "smp-protocol": "بروتوكول SMP", "chat-protocol": "بروتوكول الدردشة", "donate": "تبرّع", "terminal-cli": "طرفية CLI", - "terms-and-privacy-policy": "الشروط وسياسة الخصوصية", + "terms-and-privacy-policy": "سياسة الخصوصية", "hero-header": "إعادة تعريف الخصوصية", - "hero-overlay-1-textlink": "لماذا تعتبر معرفات المستخدم ضارة بالخصوصية؟", + "hero-overlay-1-textlink": "لماذا تعتبر معرّفات المستخدم ضارة بالخصوصية؟", "hero-overlay-2-textlink": "كيف يعمل SimpleX؟", - "hero-overlay-2-title": "لماذا تعتبر معرفات المستخدم ضارة بالخصوصية؟", - "feature-2-title": "تشفير<br>الصور والفيديوهات والملفات بين الطرفين", - "feature-3-title": "مجموعات لامركزية مشفرة — المستخدمون فقط يعرفون بوجودها", - "feature-5-title": "محادثات سرية اختفائية", - "feature-6-title": "تشفير المكالمات الصوتية والفيديو <br> بين الطرفين", - "simplex-network-overlay-1-title": "مقارنة مع بروتوكولات المراسلة P2P", - "simplex-private-4-title": "وصول <br> اختياري عبر Tor", + "hero-overlay-2-title": "لماذا تعتبر معرّفات المستخدم ضارة بالخصوصية؟", + "feature-2-title": "تعمية<br>الصور والفيديوهات والملفات بين الطرفين", + "feature-3-title": "المجموعات اللامركزية مُعمَّاة — المستخدمون فقط يعرفون بوجودها", + "feature-5-title": "رسائل ذاتية الاختفاء", + "feature-6-title": "تعمية المكالمات الصوتية والفيديو <br> بين الطرفين", + "simplex-network-overlay-1-title": "مقارنة مع بروتوكولات المُراسلة P2P", + "simplex-private-4-title": "وصول <br> اختياري عبر تور", "simplex-explained-tab-2-text": "2. كيف يعمل", - "simplex-explained-tab-1-p-2": "كيف يمكن أن تعمل مع قوائم انتظار أحادية الاتجاه وبدون معرفات ملف تعريف المستخدم؟", - "simplex-explained-tab-2-p-1": "لكل اتصال، تستخدم قائمتي انتظار منفصلتين للمراسلة لإرسال واستقبال الرسائل عبر خوادم مختلفة.", - "simplex-explained-tab-2-p-2": "تقوم الخوادم بتمرير الرسائل في اتجاه واحد فقط، دون الحصول على الصورة الكاملة لمحادثات المستخدم أو اتصالاته.", + "simplex-explained-tab-1-p-2": "كيف يمكن أن تعمل مع قوائم انتظار أحادية الاتجاه وبدون معرّفات ملف تعريف المستخدم؟", + "simplex-explained-tab-2-p-1": "لكل اتصال، تستخدم قائمتي انتظار منفصلتين للمُراسلة لإرسال واستلام الرسائل عبر خوادم مختلفة.", + "simplex-explained-tab-2-p-2": "تمرّر الخوادم الرسائل في اتجاه واحد فقط، دون الحصول على الصورة الكاملة لمُحادثات المستخدم أو اتصالاته.", "simplex-explained-tab-3-p-1": "تحتوي الخوادم على بيانات اعتماد مجهولة منفصلة لكل قائمة انتظار، ولا تعرف المستخدمين الذين ينتمون إليهم.", - "copyright-label": "مشروع مفتوح المصدر © SimpleX 2020-2023", + "copyright-label": "مشروع مفتوح المصدر © SimpleX 2020-2025", "simplex-chat-protocol": "بروتوكول دردشة SimpleX", "developers": "المطورين", - "hero-subheader": "أول نظام مراسلة<br> بدون معرفات المستخدم", + "hero-subheader": "أول نظام مُراسلة<br> دون معرّفات مُستخدم", "hero-p-1": "تحتوي التطبيقات الأخرى على معرّفات مستخدم: Signal ،Matrix ،Session ،Briar ،Jami ،Cwtch وما إلى ذلك. <br> SimpleX ليس لديه، <strong> ولا حتى أرقام عشوائية.</strong><br> وهذا يحسن خصوصيتك بشكل جذري.", "features": "الميزات", - "hero-2-header": "قم بإجراء اتصال خاص", + "hero-2-header": "أجري اتصال خاص", "hero-overlay-1-title": "كيف يعمل SimpleX؟", "hero-2-header-desc": "يُظهر الفيديو كيفية اتصالك بصديقك عبر رمز الاستجابة السريعة لمرة واحدة (QR code)، شخصيًا أو عبر رابط فيديو. يمكنك أيضًا الاتصال من خلال مشاركة رابط الدعوة.", - "feature-1-title": "تشفير الرسائل بين الطرفين مع دعم ماركداون والتحرير", - "feature-4-title": "تشفير الرسائل الصوتية بين الطرفين", + "feature-1-title": "تعمية الرسائل بين الطرفين مع دعم ماركداون والتعديل", + "feature-4-title": "تعمية الرسائل الصوتية بين الطرفين", "privacy-matters-overlay-card-1-p-1": "تستخدم العديد من الشركات الكبيرة معلومات حول من تتصل به لتقدير دخلك، وبيع المنتجات التي لا تحتاجها حقًا، ولتحديد الأسعار.", - "feature-7-title": "تخزين التطبيقات المشفرة المحمولة — نقل ملف التعريف إلى جهاز آخر", + "feature-7-title": "تخزين بيانات التطبيق مُعمَّاة ومحمولة — نقل ملف التعريف إلى جهاز آخر", "feature-8-title": "وضع التخفي — <br> فريد من نوعه لـ SimpleX Chat", - "simplex-private-1-title": "طبقتان من <br> التشفير بين الطرفين", - "simplex-private-2-title": "طبقة إضافية من <br> تشفير الخادم", + "simplex-private-1-title": "طبقتان من <br> التعمية بين الطرفين", + "simplex-private-2-title": "طبقة إضافية من <br> تعمية الخادم", "simplex-private-3-title": "تأمين نقل <br> TLS المصدق عليه", "simplex-private-5-title": "طبقات متعددة من <br> حشوة المحتوى", "simplex-private-7-title": "التحقق <br> من سلامة الرسالة", "simplex-private-8-title": "خلط الرسائل <br> لتقليل من الارتباط", - "simplex-private-10-title": "معرفات مزدوجة مؤقتة مجهولة الهوية", + "simplex-private-10-title": "معرّفات زوجية مجهولة مؤقتة", "simplex-private-card-3-point-1": "يتم استخدام TLS 1.2 / 1.3 مع خوارزميات قوية فقط لاتصالات الخادم والعميل.", "simplex-private-card-3-point-2": "تعمل بصمة الخادم وربط القناة على منع هجمات الوسيط (MITM) وإعادة التشغيل.", "simplex-private-card-3-point-3": "استئناف الاتصال معطل لمنع هجمات الجلسة.", - "simplex-private-card-4-point-1": "لحماية عنوان IP الخاص بك، يمكنك الوصول إلى الخوادم عبر Tor أو بعض شبكات تراكب النقل الأخرى.", - "simplex-private-card-5-point-1": "يستخدم SimpleX حشوة المحتوى لكل طبقة تشفير لإحباط هجمات حجم الرسالة.", - "simplex-private-card-6-point-2": "لمنع ذلك، تقوم تطبيقات SimpleX بتمرير مفاتيح لمرة واحدة خارج النطاق، عند مشاركة عنوان كرابط أو رمز QR.", + "simplex-private-card-4-point-1": "لحماية عنوان IP الخاص بك، يمكنك الوصول إلى الخوادم عبر تور أو بعض شبكات تراكب النقل الأخرى.", + "simplex-private-card-5-point-1": "يستخدم SimpleX حشوة المحتوى لكل طبقة تعمية لإحباط هجمات حجم الرسالة.", + "simplex-private-card-6-point-2": "لمنع ذلك، تقوم تطبيقات SimpleX بتمرير مفاتيح لمرة واحدة خارج النطاق، عندما تُشارك عنوان كرابط أو رمز QR.", "simplex-private-card-8-point-1": "تعمل خوادم SimpleX كعقد مختلطة بزمن انتقال منخفض — الرسائل الواردة والصادرة لها ترتيب مختلف.", "simplex-private-card-9-point-1": "كل رسالة انتظار تمرر الرسائل في اتجاه واحد، بعناوين إرسال واستلام مختلفة.", "simplex-private-card-9-point-2": "إنه يقلل من نواقل الهجوم، مقارنة بوسطاء الرسائل التقليديين، والبيانات الوصفية المتاحة.", - "simplex-private-card-10-point-1": "يستخدم SimpleX معرفات مزدوجة مؤقتة مجهولة الهوية للعناوين وبيانات الاعتماد لكل جهة اتصال مستخدم أو عضو مجموعة.", + "simplex-private-card-10-point-1": "يستخدم SimpleX معرّفات مزدوجة مؤقتة مجهولة الهوية للعناوين وبيانات الاعتماد لكل جهة اتصال مستخدم أو عضو مجموعة.", "privacy-matters-1-title": "الإعلانات والتمييز السعري", "privacy-matters-1-overlay-1-title": "الخصوصية توفر لك المال", "privacy-matters-1-overlay-1-linkText": "الخصوصية توفر لك المال", @@ -72,69 +72,68 @@ "privacy-matters-3-overlay-1-linkText": "الخصوصية تحمي حريتك", "simplex-unique-1-title": "لديك خصوصية كاملة", "simplex-unique-1-overlay-1-title": "الخصوصية الكاملة لهويتك وملفك الشخصي وجهات الاتصال والبيانات الوصفية", - "simplex-unique-2-title": "أنت محمي <br> من البريد العشوائي وإساءة الاستخدام", + "simplex-unique-2-title": "أنت محمي <br> من رسائل الإزعاج وإساءة الاستخدام", "simplex-unique-3-title": "أنت تتحكم في بياناتك", - "simplex-unique-3-overlay-1-title": "ملكية ومراقبة وأمان بياناتك", + "simplex-unique-3-overlay-1-title": "الملكية والتحكم وأمن بياناتك", "simplex-unique-4-title": "أنت تمتلك شبكة SimpleX", "simplex-unique-4-overlay-1-title": "لامركزية بالكامل — يمتلك المستخدمون شبكة SimpleX", "hero-overlay-card-1-p-4": "هذا التصميم يمنع تسريب أي البيانات الوصفية للمستخدمين على مستوى التطبيق. لزيادة تحسين الخصوصية وحماية عنوان IP الخاص بك، يمكنك الاتصال بخوادم المراسلة عبر Tor.", - "hero-overlay-card-1-p-5": "الأجهزة العميلة فقط هي التي تخزن ملفات تعريف المستخدمين، جهات الاتصال والمجموعات؛ يتم إرسال الرسائل بتشفير ثنائي الطبقة من طرف إلى طرف.", + "hero-overlay-card-1-p-5": "الأجهزة العميلة فقط هي التي تخزن ملفات تعريف المستخدمين، جهات الاتصال والمجموعات؛ يتم إرسال الرسائل بتعمية ثنائية الطبقة بين الطرفين.", "hero-overlay-card-2-p-2": "يمكنهم بعد ذلك ربط هذه المعلومات بالشبكات الاجتماعية العامة الحالية، وتحديد بعض الهويات الحقيقية.", "simplex-network-overlay-card-1-li-3": "P2P لا يحل مشكلة <a href='https://en.wikipedia.org/wiki/Man-in-the-middle_attack'>هجوم الوسيط (MITM)</a>، ومعظم التطبيقات الحالية لا تستخدم خارج النطاق رسائل للتبادل الأولي للمفاتيح. يستخدم SimpleX رسائل خارج النطاق أو، في بعض الحالات، اتصالات آمنة وموثوق بها موجودة مسبقًا لتبادل المفاتيح الأولي.", "privacy-matters-overlay-card-1-p-3": "تستخدم بعض الشركات المالية والتأمين رسوماً بيانية اجتماعية لتحديد أسعار الفائدة وأقساط التأمين. عادةً ما تجعل الأشخاص ذوي الدخل المنخفض يدفعون أكثر — أو كما يسمى<a href='https://fairbydesign.com/povertypremium/' target='_blank'> \"علاوة الفقر\"</a>.", "privacy-matters-overlay-card-1-p-2": "يعرف تجار التجزئة عبر الإنترنت أن الأشخاص ذوي الدخل المنخفض هم أكثر عرضة لإجراء عمليات شراء عاجلة، لذلك قد يفرضون أسعارًا أعلى أو يزيلون الخصومات.", "simplex-private-6-title": "تبديل <br> خارج النطاق", "simplex-private-9-title": "قوائم انتظار <br> أحادية الاتجاه", - "privacy-matters-overlay-card-1-p-4": "تحمي منصة SimpleX خصوصية اتصالاتك بشكل أفضل من أي بديل آخر، مما يمنع تمامًا الرسم البياني الاجتماعي الخاص بك من أن يصبح متاحًا لأي شركات أو مؤسسات. حتى عندما يستخدم الأشخاص الخوادم التي توفرها SimpleX Chat، فإننا لا نعرف عدد المستخدمين أو اتصالاتهم.", + "privacy-matters-overlay-card-1-p-4": "تحمي شبكة SimpleX خصوصية اتصالاتك بشكل أفضل من أي بديل آخر، مما يمنع تمامًا الرسم البياني الاجتماعي الخاص بك من أن يصبح متاحًا لأي شركات أو مؤسسات. حتى عندما يستخدم الأشخاص الخوادم التي توفرها SimpleX Chat، فإننا لا نعرف عدد المستخدمين أو اتصالاتهم.", "simplex-private-card-1-point-1": "بروتوكول السقاطة المزدوجة — <br> رسائل OTR مع السرية المستمرة واستعادة الاختراق.", "simplex-private-card-1-point-2": "NaCL cryptobox في كل قائمة انتظار لمنع ارتباط حركة مرور البيانات بين قوائم انتظار الرسائل في حالة اختراق TLS.", - "simplex-private-card-2-point-1": "طبقة إضافية من تشفير الخادم للتسليم إلى المستلم، لمنع الارتباط بين حركة مرور بيانات الخادم المستلمة والمرسلة في حالة اختراق TLS.", - "simplex-private-card-4-point-2": "لاستخدام SimpleX عبر Tor، يرجى تثبيت <a href=\"https://guardianproject.info/apps/org.torproject.android/\" target=\"_blank\"> تطبيق Orbot </a> وتمكين وكيل SOCKS5 (أو VPN <a href = \"https://apps.apple.com/us/app/orbot/id1609461599؟platform=iphone\" target = \"_ blank\"> على iOS </a>).", + "simplex-private-card-2-point-1": "طبقة إضافية من تعمية الخادم للتسليم إلى المُستلم، لمنع الارتباط بين حركة مرور بيانات الخادم المُستلمة والمُرسلة في حالة اختراق TLS.", + "simplex-private-card-4-point-2": "لاستخدام SimpleX عبر تور، يُرجى تثبيت <a href=\"https://guardianproject.info/apps/org.torproject.android/\" target=\"_blank\"> تطبيق Orbot </a> وتمكّين وكيل SOCKS5 (أو VPN <a href=\"https://apps.apple.com/us/app/orbot/id1609461599?platform=iphone\" target=\"_blank\">على iOS </a>).", "simplex-private-card-5-point-2": "يجعل الرسائل ذات الأحجام المختلفة تبدو متشابهة للخوادم ومراقبي الشبكة.", - "simplex-private-card-6-point-1": "العديد من منصات الاتصال عرضة لهجمات الوسيط (MITM) من قبل الخوادم أو موفري الشبكات.", + "simplex-private-card-6-point-1": "العديد من شبكات التواصل عرضة لهجمات الوسيط (MITM) من قِبل الخوادم أو موفري الشبكات.", "simplex-private-card-7-point-1": "لضمان سلامة الرسائل يتم ترقيمها بالتسلسل وتضمين تجزئة الرسالة السابقة.", - "simplex-private-card-7-point-2": "إذا تمت إضافة أي رسالة أو إزالتها أو تغييرها، فسيتم تنبيه المستلم.", - "simplex-private-card-10-point-2": "يسمح بتسليم الرسائل بدون معرفات ملف تعريف المستخدم، مما يوفر خصوصية بيانات وصفية أفضل من البدائل.", + "simplex-private-card-7-point-2": "إذا أُضيفت أي رسالة أو أُزيلت أو تغيّرت، فسيتم تنبيه المُستلم.", + "simplex-private-card-10-point-2": "يسمح بتسليم الرسائل بدون معرّفات ملف تعريف المستخدم، مما يوفر خصوصية للبيانات الوصفية أفضل من البدائل.", "privacy-matters-2-overlay-1-linkText": "تمنحك الخصوصية القوة", - "simplex-unique-2-overlay-1-title": "أفضل حماية من البريد العشوائي وإساءة الاستخدام", - "hero-overlay-card-1-p-3": "أنت تحدد الخادم (الخوادم) المراد استخدامه لتلقي الرسائل وجهات الاتصال الخاصة بك — الخوادم التي تستخدمها لإرسال الرسائل إليهم. من المرجح أن تستخدم كل محادثة خادمين مختلفين.", - "hero-overlay-card-1-p-1": "سأل العديد من المستخدمين: <em> إذا لم يكن لدى SimpleX معرفات مستخدم، فكيف يمكنها معرفة مكان تسليم الرسائل؟ </em>", - "hero-overlay-card-1-p-2": "لتوصيل الرسائل، بدلاً من معرفات المستخدم التي تستخدمها جميع المنصات الأخرى، يستخدم SimpleX معرفات مزدوجة مؤقتة مجهولة الهوية لقوائم انتظار الرسائل، مختلفة لكل اتصال من اتصالاتك — لا توجد معرفات مستخدم دائمة.", - "simplex-network-overlay-card-1-p-1": "بروتوكولات المراسلة والتطبيقات <a href='https://en.wikipedia.org/wiki/Peer-to-peer'> P2P </a> بها مشاكل مختلفة تجعلها أقل موثوقية من SimpleX وأكثر تعقيدًا في التحليل و عرضة لعدة أنواع من الهجمات.", - "hero-overlay-card-2-p-1": "عندما يكون لدى المستخدمين هويات ثابتة، حتى لو كان هذا مجرد رقم عشوائي، مثل معرف الجلسة، فهناك خطر يتمثل في أن الموفر أو المهاجم يمكنه مراقبة كيفية اتصال المستخدمين وعدد الرسائل التي يرسلونها.", + "simplex-unique-2-overlay-1-title": "أفضل حماية من رسائل الإزعاج وإساءة الاستخدام", + "hero-overlay-card-1-p-3": "أنت تحدد الخادم (الخوادم) المراد استخدامه لاستلام الرسائل وجهات اتصالك — الخوادم التي تستخدمها لإرسال الرسائل إليهم. من المرجح أن تستخدم كل مُحادثة خادمين مختلفين.", + "hero-overlay-card-1-p-1": "سأل العديد من المستخدمين: <em> إذا لم يكن لدى SimpleX معرّفات مستخدم، فكيف يمكنها معرفة مكان تسليم الرسائل؟ </em>", + "hero-overlay-card-1-p-2": "لتسليم الرسائل، بدلاً من معرّفات المُستخدم المُستخدمة من قِبل جميع الشبكات الأخرى، يستخدم SimpleX معرّفات زوجية مجهولة مؤقتة لقوائم الرسائل، منفصلة لكل اتصال من اتصالاتك — ولا توجد معرّفات طويلة الأجل.", + "simplex-network-overlay-card-1-p-1": "بروتوكولات المُراسلة والتطبيقات <a href='https://en.wikipedia.org/wiki/Peer-to-peer'> P2P </a> بها مشاكل مختلفة تجعلها أقل موثوقية من SimpleX وأكثر تعقيدًا في التحليل و عرضة لعدة أنواع من الهجمات.", + "hero-overlay-card-2-p-1": "عندما يكون لدى المستخدمين هويات ثابتة، حتى لو كان هذا مجرد رقم عشوائي، مثل معرّف الجلسة، فهناك خطر يتمثل في أن الموفر أو المهاجم يمكنه مراقبة كيفية اتصال المستخدمين وعدد الرسائل التي يرسلونها.", "hero-overlay-card-1-p-6": "اقرأ المزيد في <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'> SimpleX whitepaper </a>.", "hero-overlay-card-2-p-3": "حتى مع معظم التطبيقات الخاصة التي تستخدم خدمات Tor v3، إذا تحدثت إلى جهتي اتصال مختلفتين عبر نفس الملف الشخصي، فيمكنهما إثبات أنهما متصلان بنفس الشخص.", - "hero-overlay-card-2-p-4": "يحمي SimpleX من هذه الهجمات من خلال عدم وجود أي معرفات مستخدم في تصميمه. وإذا كنت تستخدم وضع التخفي، فسيكون لديك اسم عرض مختلف لكل جهة اتصال، مع تجنب أي بيانات مشتركة بينهما.", - "simplex-network-overlay-card-1-li-6": "قد تكون شبكات P2P عرضة <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'> هجوم DRDoS</a>، عندما يتمكن العملاء من إعادة بث حركة مرور البيانات وتضخيمها، مما يؤدي إلى رفض الخدمة على مستوى الشبكة. يقوم عملاء SimpleX فقط بترحيل حركة مرور البيانات من اتصال معروف ولا يمكن للمهاجم استخدامها لتضخيم حركة مرور البيانات في الشبكة بالكامل.", - "simplex-network-overlay-card-1-li-1": "تعتمد شبكات P2P على بعض المتغيرات من <a href='https://en.wikipedia.org/wiki/Distributed_hash_table'> DHT </a> لتوجيه الرسائل. يجب أن توازن تصميمات DHT بين ضمان التسليم وزمن الوصول. تتمتع SimpleX بضمان تسليم أفضل وزمن انتقال أقل من P2P، لأنه يمكن تمرير الرسالة بشكل متكرر عبر عدة خوادم بالتوازي، باستخدام الخوادم التي اختارها المستلم. في شبكات P2P، يتم تمرير الرسالة عبر عُقد <em> O (log N) </em> بالتسلسل، باستخدام العقد التي تختارها الخوارزمية.", + "hero-overlay-card-2-p-4": "يحمي SimpleX من هذه الهجمات من خلال عدم وجود أي معرّفات مستخدم في تصميمه. وإذا كنت تستخدم وضع التخفي، فسيكون لديك اسم عرض مختلف لكل جهة اتصال، مع تجنب أي بيانات مشتركة بينهما.", + "simplex-network-overlay-card-1-li-6": "قد تكون شبكات P2P عرضة <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'> هجوم DRDoS</a>، عندما يتمكّن العملاء من إعادة بث حركة مرور البيانات وتضخيمها، مما يؤدي إلى رفض الخدمة على مستوى الشبكة. يقوم عملاء SimpleX فقط بترحيل حركة مرور البيانات من اتصال معروف ولا يمكن للمهاجم استخدامها لتضخيم حركة مرور البيانات في الشبكة بالكامل.", + "simplex-network-overlay-card-1-li-1": "تعتمد شبكات P2P على بعض المتغيرات من <a href='https://en.wikipedia.org/wiki/Distributed_hash_table'> DHT </a> لتوجيه الرسائل. يجب أن توازن تصميمات DHT بين ضمان التسليم وزمن الوصول. تتمتع SimpleX بضمان تسليم أفضل وزمن انتقال أقل من P2P، لأنه يمكّن تمرير الرسالة بشكل متكرر عبر عدة خوادم بالتوازي، باستخدام الخوادم التي اختارها المُستلم. في شبكات P2P، يتم تمرير الرسالة عبر عُقد <em> O (log N) </em> بالتسلسل، باستخدام العُقد التي تختارها الخوارزمية.", "simplex-network-overlay-card-1-li-2": "لا يحتوي تصميم SimpleX، عكس معظم شبكات P2P، على معرّفات مستخدم عالمية من أي نوع، حتى مؤقتة، ويستخدم فقط المعرّفات المزدوجة المؤقتة، مما يوفر إخفاء هوية أفضل وحماية للبيانات الوصفية.", "simplex-network-overlay-card-1-li-4": "يمكن لبعض مزودي خدمة الإنترنت حظر تطبيقات P2P (مثل <a href='https://en.wikipedia.org/wiki/BitTorrent'> BitTorrent </a>). SimpleX حيادي النقل - يمكنه العمل عبر بروتوكولات الويب القياسية، على سبيل المثال WebSockets.", "simplex-network-overlay-card-1-li-5": "قد تكون جميع شبكات P2P المعروفة عرضة <a href='https://en.wikipedia.org/wiki/Sybil_attack'> لهجوم Sybil</a>، لأن كل عقدة قابلة للاكتشاف، وتعمل الشبكة ككل. تتطلب الإجراءات المعروفة لتخفيفها إما مكونًا مركزيًا أو <a href='https://en.wikipedia.org/wiki/Proof_of_work'> إثبات عمل مكلف </a>. لا تحتوي شبكة SimpleX على إمكانية اكتشاف الخادم، فهي مجزأة وتعمل كشبكات فرعية متعددة ومعزولة، مما يجعل الهجمات على مستوى الشبكة مستحيلة.", - "privacy-matters-overlay-card-3-p-1": "يجب على الجميع الاهتمام بخصوصية وأمان اتصالاتهم — يمكن للمحادثات غير الضارة أن تعرضك للخطر، حتى لو لم يكن لديك ما تخفيه.", - "privacy-matters-overlay-card-3-p-4": "لا يكفي استخدام برنامج مراسلة مشفر من طرف إلى طرف، يجب علينا جميعًا استخدام برامج المراسلة التي تحمي خصوصية شبكاتنا الشخصية — مع من نحن مرتبطون.", - "simplex-unique-overlay-card-1-p-3": "يحمي هذا التصميم خصوصية الأشخاص الذين تتواصل معهم، ويخفيها عن خوادم منصة SimpleX ومن أي مراقبين. لإخفاء عنوان IP الخاص بك من الخوادم، يمكنك <strong> الاتصال بخوادم SimpleX عبر Tor</strong>.", - "simplex-unique-overlay-card-2-p-1": "نظرًا لعدم وجود معرف لديك على نظام SimpleX، لا يمكن لأي شخص الاتصال بك ما لم تشارك عنوان مستخدم لمرة واحدة أو مؤقتًا، كرمز QR أو رابط.", - "simplex-unique-overlay-card-2-p-2": "حتى مع عنوان المستخدم الاختياري، بينما يمكن استخدامه لإرسال طلبات جهات اتصال غير مرغوب فيها، يمكنك تغييره أو حذفه بالكامل دون فقد أي من اتصالاتك.", - "simplex-unique-overlay-card-3-p-2": "يتم الاحتفاظ بالرسائل المشفرة من طرف إلى طرف مؤقتًا على خوادم ترحيل SimpleX حتى يتم استلامها، ثم يتم حذفها نهائيًا.", - "simplex-unique-overlay-card-3-p-4": "لا توجد معرفات أو نص مشفر مشترك بين حركة مرور بيانات الخادم المرسلة والمستلمة — ؛ إذا كان أي شخص يراقب ذلك، فلن يتمكن بسهولة من تحديد من يتواصل مع من، حتى لو تم اختراق TLS.", - "simplex-unique-card-1-p-1": "يحمي SimpleX خصوصية ملف التعريف الخاص بك، جهات الاتصال والبيانات الوصفية، ويخفيه عن خوادم منصة SimpleX وأي مراقبين.", + "privacy-matters-overlay-card-3-p-1": "يجب على الجميع الاهتمام بخصوصية وأمان اتصالاتهم — يمكن للمُحادثات غير الضارة أن تعرضك للخطر، حتى لو لم يكن لديك ما تخفيه.", + "privacy-matters-overlay-card-3-p-4": "لا يكفي استخدام برنامج مُراسلة مُعمَّاة بين الطرفين، يجب علينا جميعًا استخدام برامج مُراسلة التي تحمي خصوصية شبكاتنا الشخصية — مع من نحن مرتبطون.", + "simplex-unique-overlay-card-1-p-3": "يحمي هذا التصميم خصوصية الأشخاص الذين تتواصل معهم، ويخفيها عن خوادم شبكة SimpleX ومن أي مراقبين. لإخفاء عنوان IP الخاص بك من الخوادم، يمكنك <strong> الاتصال بخوادم SimpleX عبر تور</strong>.", + "simplex-unique-overlay-card-2-p-1": "نظرًا لعدم وجود معرّف لديك على شبكة SimpleX، لا يمكن لأي شخص الاتصال بك ما لم تشارك عنوان مستخدم لمرة واحدة أو مؤقتًا، كرمز QR أو رابط.", + "simplex-unique-overlay-card-2-p-2": "حتى مع عنوان المستخدم الاختياري، بينما يمكن استخدامه لإرسال طلبات جهات اتصال مزعجة، يمكنك تغييره أو حذفه بالكامل دون فقد أي من اتصالاتك.", + "simplex-unique-overlay-card-3-p-2": "يتم الاحتفاظ بالرسائل المُعمَّاة بين الطرفين مؤقتًا على خوادم ترحيل SimpleX حتى يتم استلامها، ثُمَّ تُحذف نهائيًا.", + "simplex-unique-overlay-card-3-p-4": "لا توجد معرفّات أو نص مُعَمَّى مشترك بين حركة مرور بيانات الخادم المُرسلة والمُستلمة — إذا كان أي شخص يراقب ذلك، فلن يتمكّن بسهولة من تحديد من يتواصل مع من، حتى لو اختُرق TLS.", + "simplex-unique-card-1-p-1": "يحمي SimpleX خصوصية ملف تعريفك، جهات اتصالك والبيانات الوصفية، ويخفيه عن خوادم شبكة SimpleX وأي مراقبين.", "privacy-matters-overlay-card-2-p-1": "منذ وقت ليس ببعيد، لاحظنا أن الانتخابات الرئيسية يتم التلاعب بها بواسطة <a href='https://en.wikipedia.org/wiki/Facebook–Cambridge_Analytica_data_scandal' target='_blank'> شركة استشارية ذات سمعة طيبة </a> التي استخدمت الرسوم البيانية الاجتماعية لتشويه نظرتنا للعالم الحقيقي والتلاعب بأصواتنا.", - "privacy-matters-overlay-card-2-p-2": "لكي تكون موضوعيًا وتتخذ قرارات مستقلة، يجب أن تكون متحكمًا في مساحة المعلومات الخاصة بك. هذا ممكن فقط إذا كنت تستخدم منصة اتصالات خاصة لا يمكنها الوصول إلى الرسم البياني الاجتماعي الخاص بك.", - "privacy-matters-overlay-card-2-p-3": "SimpleX هو النظام الأساسي الأول الذي لا يحتوي على أي معرفات مستخدم صمّم ليكون خاصًا، وبهذه الطريقة تحمي مخطط اتصالاتك بشكل أفضل من أي بديل معروف.", + "privacy-matters-overlay-card-2-p-2": "لكي تكون موضوعيًا وتتخذ قرارات مستقلة، عليك التحكم في مساحة معلوماتك. لا يمكن تحقيق ذلك إلا باستخدام شبكة اتصال خاصة لا يمكنها الوصول إلى حسابك على مواقع التواصل الاجتماعي.", + "privacy-matters-overlay-card-2-p-3": "SimpleX هي أول شبكة لا تحتوي على أي معرّفات مستخدم من حيث التصميم، وبهذه الطريقة تحمي مخطط اتصالاتك بشكل أفضل من أي بديل معروف.", "privacy-matters-overlay-card-3-p-2": "واحدة من أكثر القصص إثارة للصدمة هي تجربة <a href='https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi' target='_blank'> محمدو ولد صلاحي </a> الموصوفة في مذكراته والموضحة في فيلم موريتاني. تم وضعه في معتقل غوانتانامو بدون محاكمة، وتعرض للتعذيب هناك لمدة 15 عامًا بعد مكالمة هاتفية مع قريبه في أفغانستان، للاشتباه في تورطه في هجمات 11 سبتمبر، على الرغم من أنه عاش في ألمانيا طوال السنوات العشر الماضية.", "privacy-matters-overlay-card-3-p-3": "يتم القبض على الأشخاص العاديين بسبب ما يشاركونه عبر الإنترنت، حتى عبر حساباتهم \"المجهولة\"، <a href='https://www.dailymail.co.uk/news/article-11282263/Moment-police-swoop-house-devout-catholic-mother-malicious-online-posts.html' target='_blank'>وحتى في البلدان الديمقراطية</a>.", - "simplex-unique-overlay-card-1-p-1": "على عكس أنظمة المراسلة الأخرى، <strong>لا يحتوي SimpleX على معرفات مخصصة للمستخدمين</strong>. لا يعتمد على أرقام الهواتف أو العناوين المستندة إلى النطاقات (مثل البريد الإلكتروني أو XMPP)، أسماء المستخدمين، المفاتيح العامة أو حتى الأرقام العشوائية لتحديد مستخدميها — لا نعرف عدد الأشخاص الذين يستخدمون خوادم SimpleX الخاصة بنا.", - "simplex-unique-overlay-card-1-p-2": "لتسليم الرسائل، يستخدم SimpleX <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'> العناوين المزدوجة المجهولة </a> لقوائم انتظار الرسائل أحادية الاتجاه، منفصلة عن الرسائل المستلمة والمرسلة، عادةً عبر خوادم مختلفة. إن استخدام SimpleX يشبه امتلاك <strong>بريد إلكتروني أو هاتف “مؤقت” مختلف لكل جهة اتصال</strong>، ولا توجد متاعب في إدارتها.", - "simplex-unique-overlay-card-3-p-1": "يخزن SimpleX Chat جميع بيانات المستخدم على أجهزة العميل فقط باستخدام <strong> تنسيق قاعدة بيانات محمولة مشفرة </strong> يمكن تصديرها ونقلها إلى أي جهاز مدعوم.", - "simplex-unique-overlay-card-3-p-3": "على عكس خوادم الشبكات الموحدة (البريد الإلكتروني أو XMPP أو Matrix)، لا تقوم خوادم SimpleX بتخزين حسابات المستخدمين، فهي تقوم فقط بترحيل الرسائل، مما يحمي خصوصية كلا الطرفين.", - "simplex-unique-overlay-card-4-p-1": "يمكنك <strong> استخدام SimpleX مع الخوادم الخاصة بك </strong> والاستمرار في التواصل مع الأشخاص الذين يستخدمون الخوادم المهيأة مسبقًا التي نقدمها.", - "simplex-unique-overlay-card-4-p-3": "إذا كنت تفكر في التطوير لمنصة SimpleX، على سبيل المثال، بوت الدردشة لمستخدمي تطبيق SimpleX، أو دمج مكتبة SimpleX Chat في تطبيقات الأجهزة المحمولة، من فضلك <a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>تواصل معي</a> لأي نصيحة والدعم.", + "simplex-unique-overlay-card-1-p-1": "على عكس شبكات المُراسلة الأخرى، <strong>لا يحتوي SimpleX على معرّفات مخصصة للمستخدمين</strong>. لا يعتمد على أرقام الهواتف أو العناوين المستندة إلى النطاقات (مثل البريد الإلكتروني أو XMPP)، أسماء المستخدمين، المفاتيح العامة أو حتى الأرقام العشوائية لتحديد مستخدميها — مُشغلي خادم SimpleX لا يعرفون عدد الأشخاص الذين يستخدمون خوادمهم.", + "simplex-unique-overlay-card-1-p-2": "لتسليم الرسائل، يستخدم SimpleX <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'> العناوين المزدوجة المجهولة </a> لقوائم انتظار الرسائل أحادية الاتجاه، منفصلة عن الرسائل المُستلمة والمُرسلة، عادةً عبر خوادم مختلفة.", + "simplex-unique-overlay-card-3-p-1": "يخزن SimpleX Chat جميع بيانات المستخدم على أجهزة العميل فقط باستخدام <strong> تنسيق قاعدة بيانات محمولة مُعمَّاة </strong> يمكّن تصديرها ونقلها إلى أي جهاز مدعوم.", + "simplex-unique-overlay-card-3-p-3": "على عكس خوادم الشبكات الاتحادية (البريد الإلكتروني أو XMPP أو Matrix)، لا تقوم خوادم SimpleX بتخزين حسابات المستخدمين، فهي تقوم فقط بترحيل الرسائل، مما يحمي خصوصية كلا الطرفين.", + "simplex-unique-overlay-card-4-p-1": "يمكنك <strong> استخدام SimpleX مع خوادمك </strong> والاستمرار في التواصل مع الأشخاص الذين يستخدمون الخوادم المُهيأة مسبقًا في التطبيقات.", + "simplex-unique-overlay-card-4-p-3": "إذا كنت تفكر في التطوير شبكة SimpleX، على سبيل المثال، بوت الدردشة لمستخدمي تطبيق SimpleX، أو دمج مكتبة SimpleX Chat في تطبيقات الأجهزة المحمولة، من فضلك <a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>تواصل معي</a> لأي نصيحة والدعم.", "donate-here-to-help-us": "تبرّع هنا لمساعدتنا", "sign-up-to-receive-our-updates": "اشترك للحصول على آخر مستجداتنا", "enter-your-email-address": "أدخل عنوان بريدك الإلكتروني", "get-simplex": "احصل على <a href=\"/downloads\">تطبيق سطح المكتب</a> SimpleX", - "why-simplex-is": "لماذا SimpleX", - "unique": "فريد من نوعه", + "why-simplex-is-unique": "لماذا SimpleX <span class='gradient-text'>فريد من نوعه</span>", "learn-more": "اقرأ أكثر", "more-info": "معلومات أكثر", "hide-info": "معلومات أقل", @@ -151,21 +150,21 @@ "no-private": "لا - خصوصي", "no-resilient": "لا - مرن", "no-decentralized": "لا - لامركزي", - "no-federated": "لا - فدرالي", + "no-federated": "لا - اتِحاديّ", "comparison-section-list-point-2": "العناوين تعتمد على الـDNS", - "comparison-section-list-point-3": "المفتاح العام أو معرف آخر فريد وعام", + "comparison-section-list-point-3": "المفتاح العام أو معرّف آخر فريد وعام", "comparison-section-list-point-7": "شبكات P2P إما لديها سلطة مركزية أو أن الشبكة كلها يمكن عرضة للخطر", "see-here": "اقرأ هنا", "no-secure": "لا - آمن", "comparison-section-list-point-5": "لا يحمي خصوصية البيانات الوصفية للمستخدمين", - "comparison-section-list-point-6": "على الرغم من أن الـP2P موزعة، إلا أنها ليست فدرالية - يعملون كشبكة واحدة", + "comparison-section-list-point-6": "على الرغم من أن الـP2P موزعة، إلا أنها ليست اتِحاديَّة — يعملون كشبكة واحدة", "comparison-section-list-point-1": "عادة ما يكون مكوناً من رقم الهاتف، أو اسم المستخدم في بعض الأحيان", "comparison-section-list-point-4": "إذا خوادم المشغّل مُخترقة. تحقق من رمز الأمان في Signal وبعض التطبيقات الأخرى للتخفيف منه", - "simplex-unique-card-3-p-1": "يخزن SimpleX جميع بيانات المستخدم على الأجهزة العميلة<strong> بتنسيق قاعدة بيانات محمولة مشفرة — </strong>يمكن نقله إلى جهاز آخر.", - "simplex-unique-card-4-p-1": "شبكة SimpleX لا مركزية بالكامل ومستقلة عن أي عملة مشفرة أو أي منصة أخرى، بخلاف الإنترنت.", + "simplex-unique-card-3-p-1": "يخزن SimpleX جميع بيانات المستخدم على الأجهزة العميلة<strong> بتنسيق قاعدة بيانات محمولة مُعمَّاة — </strong>يمكّن نقلها إلى جهاز آخر.", + "simplex-unique-card-4-p-1": "شبكة SimpleX لا مركزية بالكامل ومستقلة عن أي عملة مُعمَّاة أو أي شبكة أخرى، بخلاف الإنترنت.", "simplex-unique-card-4-p-2": "يمكنك<strong> استخدام SimpleX مع خوادمك الخاصة </strong> أو مع الخوادم التي نوفرها — ولا يزال الاتصال ممكن بأي مستخدم.", "join": "انضم إلى", - "we-invite-you-to-join-the-conversation": "نحن ندعوك للانضمام إلى محادثة", + "we-invite-you-to-join-the-conversation": "نحن ندعوك للانضمام إلى المُحادثة", "join-the-REDDIT-community": "انضم إلى مجتمع REDDIT", "join-us-on-GitHub": "انضم إلينا على GitHub", "contact-hero-p-2": "لم تقم بتنزيل SimpleX Chat حتى الآن؟", @@ -179,44 +178,44 @@ "use-this-command": "استخدم هذا الأمر:", "see-simplex-chat": "انظر SimpleX Chat", "github-repository": "مستودع Github", - "the-instructions--source-code": "التعليمات حول كيفية تنزيله أو تجميعه من التعليمات البرمجية المصدر.", + "the-instructions--source-code": "للتعليمات حول كيفية تنزيله أو تجميعه من التعليمات البرمجية المصدر.", "if-you-already-installed-simplex-chat-for-the-terminal": "إذا قمت بالفعل بتثبيت SimpleX Chat للوحدة الطرفية", "simplex-chat-for-the-terminal": "SimpleX Chat للوحدة الطرفية", "privacy-matters-section-header": "لماذا الخصوصية <span class='gradient-text'> مهمة</span>", - "privacy-matters-section-label": "تأكد من أن برنامج المراسلة الخاص بك لا يمكنه الوصول إلى بياناتك!", + "privacy-matters-section-label": "تأكّد من أن برنامج المُراسلة الخاص بك لا يمكنه الوصول إلى بياناتك!", "tap-to-close": "انقر للإغلاق", "simplex-network-section-header": "شبكة<span class='gradient-text'> SimpleX </span>", "simplex-network-1-header": "على عكس شبكات P2P", "simplex-network-3-header": "شبكة SimpleX", "protocol-1-text": "Signal، منصات كبيرة", "protocol-2-text": "XMPP ،Matrix", - "simplex-unique-card-1-p-2": "بخلاف أي نظام أساسي آخر للمراسلة، لا يحتوي SimpleX على معرّفات مخصصة للمستخدمين —<strong> ولا حتى أرقام عشوائية</strong>.", - "simplex-unique-card-2-p-1": "نظرًا لعدم وجود معرف أو عنوان ثابت على منصة SimpleX، لا يمكن لأي شخص الاتصال بك ما لم تشارك عنوان مستخدم لمرة واحدة أو مؤقتًا، كرمز QR أو رابط.", - "simplex-unique-card-3-p-2": "يتم الاحتفاظ بالرسائل المشفرة من طرف إلى طرف مؤقتًا على خوادم ترحيل SimpleX حتى يتم استلامها، ثم يتم حذفها نهائيًا.", + "simplex-unique-card-1-p-2": "على عكس أي شبكة مُراسلة آخر، لا يحتوي SimpleX على معرّفات مخصّصة للمستخدمين —<strong> ولا حتى أرقام عشوائية</strong>.", + "simplex-unique-card-2-p-1": "نظرًا لعدم وجود معرّف أو عنوان ثابت على شبكة SimpleX، لا يمكن لأي شخص الاتصال بك ما لم تشارك عنوان مستخدم لمرة واحدة أو مؤقتًا، كرمز QR أو رابط.", + "simplex-unique-card-3-p-2": "يتم الاحتفاظ بالرسائل المُعمَّاة بين الطرفين مؤقتًا على خوادم ترحيل SimpleX حتى يتم استلامها، ثُمَّ تُحذف نهائيًا.", "tap-the-connect-button-in-the-app": "اضغط على زر<span class='text-active-blue'> \"اتصال\"</span> في التطبيق", "scan-the-qr-code-with-the-simplex-chat-app": "امسح رمز QR باستخدام تطبيق SimpleX Chat", "scan-the-qr-code-with-the-simplex-chat-app-description": "لا يتم إرسال المفاتيح العامة وعنوان قائمة انتظار الرسائل في هذا الارتباط عبر الشبكة عند عرض هذه الصفحة —<br> فهي موجودة في جزء التجزئة لعنوان URL للرابط.", "if-you-already-installed": "إذا قمت بالفعل بتثبيت", "copy-the-command-below-text": "انسخ الأمر الذي في الاسفل واستخدمه في الدردشة:", - "simplex-private-section-header": "ما الذي يجعل SimpleX<span class='gradient-text'> خاصًا </span>", + "simplex-private-section-header": "ما الذي يجعل SimpleX<span class='gradient-text'> خصوصيًّا</span>", "privacy-matters-section-subheader": "الحفاظ على خصوصية بياناتك الوصفية — <span class='text-active-blue'> مع من تتحدث</span> — يحميك من:", "simplex-network-1-overlay-linktext": "مشاكل شبكات P2P", - "simplex-network-section-desc": "يوفر Simplex Chat أفضل خصوصية من خلال الجمع بين مزايا P2P والشبكات الموحدة.", + "simplex-network-section-desc": "يوفر Simplex Chat أفضل خصوصية من خلال الجمع بين مزايا P2P والشبكات الاتحادية.", "simplex-network-1-desc": "يتم إرسال جميع الرسائل عبر الخوادم، مما يوفر خصوصية أفضل للبيانات الوصفية وتسليمًا موثوقًا للرسائل غير المتزامنة، مع تجنب الكثير", - "simplex-network-2-header": "على عكس الشبكات الفيدرالية", + "simplex-network-2-header": "على عكس الشبكات الاتحادية", "simplex-network-3-desc": "توفر الخوادم <span class='text-active-blue'> قوائم انتظار أحادية الاتجاه </span> لتوصيل المستخدمين، لكن ليس لديهم رؤية للرسم البياني لاتصال الشبكة — إلا للمستخدمين فقط.", "comparison-section-header": "مقارنة مع البروتوكولات الأخرى", "simplex-network-2-desc": "لا تقوم خوادم الترحيل SimpleX بتخزين ملفات تعريف المستخدمين وجهات الاتصال والرسائل التي تم تسليمها، ولا تتصل ببعضها البعض، ولا يوجد دليل خوادم.", "comparison-point-1-text": "يتطلب هوية عالمية", "protocol-3-text": "بروتوكولات P2P", - "simplex-unique-overlay-card-4-p-2": "تستخدم منصة SimpleX<a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'> بروتوكول مفتوح</a> وتوفر <a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'> SDK لإنشاء روبوتات دردشة</a>، مما يسمح بتنفيذ الخدمات التي يمكن للمستخدمين التفاعل معها عبر تطبيقات SimpleX Chat — التي تتطلع حقًا لمعرفة خدمات SimpleX التي يمكنك إنشاؤها.", + "simplex-unique-overlay-card-4-p-2": "تستخدم شبكة SimpleX<a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'> بروتوكول مفتوح</a> وتوفر <a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'> SDK لإنشاء روبوتات دردشة</a>، مما يسمح بتنفيذ الخدمات التي يمكن للمستخدمين التفاعل معها عبر تطبيقات SimpleX Chat — التي تتطلع حقًا لمعرفة خدمات SimpleX التي ستبنيها.", "guide-dropdown-1": "بداية سريعة", "guide-dropdown-2": "إرسال الرسائل", "guide-dropdown-3": "مجموعات سرية", "guide-dropdown-4": "ملفات تعريف الدردشة", "guide-dropdown-5": "إدارة البيانات", "guide-dropdown-6": "مكالمات الصوت والفيديو", - "guide-dropdown-7": "الخصوصية والأمن", + "guide-dropdown-7": "الخصوصية و الأمان", "guide-dropdown-8": "إعدادات التطبيق", "guide-dropdown-9": "إجراء اتصالات", "docs-dropdown-4": "استضافة خادم SMP", @@ -228,7 +227,7 @@ "menu": "قائمة", "on-this-page": "على هذه الصفحة", "back-to-top": "عد إلى الأعلى", - "docs-dropdown-1": "منصة SimpleX", + "docs-dropdown-1": "شبكة SimpleX", "guide": "الدليل", "docs-dropdown-2": "الوصول إلى ملفات اندرويد", "docs-dropdown-3": "الوصول إلى قاعدة بيانات الدردشة", @@ -240,15 +239,21 @@ "signing-key-fingerprint": "توقيع مفتاح البصمة (SHA-256)", "f-droid-org-repo": "مستودع F-Droid.org", "stable-versions-built-by-f-droid-org": "الإصدارات الثابتة التي تم إنشاؤها بواسطة F-Droid.org", - "releases-to-this-repo-are-done-1-2-days-later": "يتم إصدار الإصدارات إلى هذا المستودع بعد يوم أو يومين", + "releases-to-this-repo-are-done-1-2-days-later": "تتم الإصدارات إلى هذا المستودع بعد عِدة أيام", "f-droid-page-simplex-chat-repo-section-text": "لإضافته إلى عميل F-Droid، <span class='hide-on-mobile'>امسح رمز QR أو</span> استخدم عنوان URL هذا:", - "f-droid-page-f-droid-org-repo-section-text": "مستودعات SimpleX Chat و F-Droid.org مبنية على مفاتيح مختلفة. للتبديل، يرجى <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>تصدير</a> قاعدة بيانات الدردشة وإعادة تثبيت التطبيق.", - "comparison-section-list-point-4a": "مُرحلات SimpleX لا يمكنها أن تتنازل عن تشفير بين الطرفين. تحقق من رمز الأمان للتخفيف من الهجوم على القناة خارج النطاق", - "hero-overlay-3-title": "التقييم الأمني", - "hero-overlay-card-3-p-2": "قامت Trail of Bits بمراجعة مكونات التشفير والشبكات الخاصة بمنصة SimpleX في نوفمبر 2022.", - "hero-overlay-card-3-p-3": "اقرأ المزيد في <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">الإعلان</a>.", + "f-droid-page-f-droid-org-repo-section-text": "مستودعات SimpleX Chat و F-Droid.org مبنية على مفاتيح مختلفة. للتبديل، يُرجى <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>تصدير</a> قاعدة بيانات الدردشة وإعادة تثبيت التطبيق.", + "comparison-section-list-point-4a": "مُرحلات SimpleX لا يمكنها أن تتنازل عن تعمية بين الطرفين. تحقق من رمز الأمان للتخفيف من الهجوم على القناة خارج النطاق", + "hero-overlay-3-title": "التقييمات الأمنية", + "hero-overlay-card-3-p-2": "قامت Trail of Bits بمراجعة مكونات التشفير والشبكات الخاصة بشبكة SimpleX في نوفمبر 2022. <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">اقرأ المزيد في الإعلان</a>.", "jobs": "انضم للفريق", - "hero-overlay-3-textlink": "التقييم الأمني", + "hero-overlay-3-textlink": "التقييمات الأمنية", "hero-overlay-card-3-p-1": "<a href=\"https://www.trailofbits.com/about/\">Trail of Bits</a> هي شركة رائدة في مجال الاستشارات الأمنية والتكنولوجية، ومن بين عملائها شركات التكنولوجيا الكبرى والوكالات الحكومية ومشاريع blockchain الكبرى.", - "docs-dropdown-9": "التنزيلات" + "docs-dropdown-9": "التنزيلات", + "please-enable-javascript": "الرجاء تفعيل جافا سكريبت (JavaScript) لرؤية رمز QR.", + "please-use-link-in-mobile-app": "يُرجى استخدام الرابط في تطبيق الجوال", + "docs-dropdown-10": "الشفافية", + "docs-dropdown-11": "الأسئلة الأكثر شيوعًا", + "docs-dropdown-12": "الأمان", + "hero-overlay-card-3-p-3": "قامت Trail of Bits بمراجعة التصميم التعموي لبروتوكولات شبكة SimpleX في يوليو 2024. <a href=\"/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html\">اقرأ المزيد</a>.", + "docs-dropdown-14": "SimpleX للأعمال التجارية" } diff --git a/website/langs/bg.json b/website/langs/bg.json index 8ab6d958ed..21fb93c2cb 100644 --- a/website/langs/bg.json +++ b/website/langs/bg.json @@ -1,3 +1,37 @@ { - "developers": "Разработчици" + "developers": "Разработчици", + "glossary": "Речник", + "yes": "Да", + "no": "Не", + "home": "Дом", + "reference": "Справка", + "blog": "Блог", + "features": "Характеристика", + "why-simplex": "Защо SimpleX", + "simplex-privacy": "Поверителност на SimpleX", + "simplex-network": "SimpleX мрежа", + "simplex-explained-tab-1-text": "1. Какъв опит като потребител", + "simplex-explained-tab-2-text": "2. Как работи", + "simplex-explained-tab-3-text": "3. Какво виждат сървърите", + "simplex-explained-tab-1-p-1": "Можете да създавате контакти и групи и да провеждате двупосочни разговори, както във всеки друго чат приложение.", + "simplex-explained-tab-2-p-1": "За всяка връзка използвате две отделни опашки за съобщения, за да изпращате и получавате съобщения през различни сървъри.", + "simplex-explained": "Обяснението на SimpleX", + "simplex-explained-tab-2-p-2": "Сървърите предават съобщения само еднопосочно, без да имат картина от разговора или връзките на потребителя.", + "chat-bot-example": "Пример за чат бот", + "smp-protocol": "СМП Протокол", + "chat-protocol": "Чат протокол", + "donate": "Дарете", + "copyright-label": "© 2020-2025 SimpleX | Проект с отворен код", + "simplex-chat-protocol": "SimpleX Чат протокол", + "terminal-cli": "Системна конзола", + "terms-and-privacy-policy": "Условия и политика за поверителност", + "hero-header": "Предефинирана поверителност", + "hero-overlay-1-textlink": "Защо потребителските идентификатори са лоши за поверителността?", + "hero-overlay-2-textlink": "Как работи SimpleX?", + "hero-2-header": "Осъществяване на частна връзка", + "simplex-explained-tab-1-p-2": "Как може да работи с еднопосочни опашки и без идентификатори на потребителски профили?", + "simplex-explained-tab-3-p-2": "Потребителите могат допълнително да подобрят поверителността на метаданните, като използват Tor за достъп до сървъри, предотвратявайки корелация по IP адрес.", + "hero-2-header-desc": "Видеото показва как се свързвате с приятеля си чрез неговия еднократен QR-код лично или чрез видео връзка. Можете също да се свържете, като споделите покана за връзка.", + "hero-overlay-3-textlink": "Оценка на сигурността", + "hero-overlay-1-title": "Как работи SimpleX?" } diff --git a/website/langs/cs.json b/website/langs/cs.json index d087fd5bec..47ab2c99d0 100644 --- a/website/langs/cs.json +++ b/website/langs/cs.json @@ -14,7 +14,7 @@ "features": "Funkce", "why-simplex": "Proč SimpleX", "simplex-privacy": "SimpleX soukromí", - "simplex-explained": "Simplex vysvětlení", + "simplex-explained": "SimpleX vysvětlení", "simplex-explained-tab-1-text": "1. Co vidí uživatel", "simplex-explained-tab-2-text": "2. Jak to funguje", "simplex-explained-tab-3-text": "3. Co vidí servery", @@ -25,10 +25,10 @@ "smp-protocol": "SMP protokol", "chat-protocol": "Chat protokol", "donate": "Darovat", - "copyright-label": "© 2020-2023 SimpleX | Projekt s otevřeným zdrojovým kódem", + "copyright-label": "© 2020-2025 SimpleX | Projekt s otevřeným zdrojovým kódem", "simplex-chat-protocol": "SimpleX Chat protokol", "terminal-cli": "Terminálové rozhraní příkazového řádku", - "terms-and-privacy-policy": "Podmínky a zásady ochrany osobních údajů", + "terms-and-privacy-policy": "Ochrana soukromí", "hero-header": "Nová definice ochrany osobních údajů", "hero-subheader": "První messenger<br>bez uživatelských ID", "hero-overlay-1-textlink": "Proč jsou uživatelská ID špatná pro soukromí?", @@ -37,10 +37,10 @@ "hero-overlay-1-title": "Jak funguje SimpleX?", "hero-overlay-2-title": "Proč jsou uživatelská ID špatná pro soukromí?", "feature-1-title": "E2E šifrované zprávy s Markdown a editací", - "feature-3-title": "Decentralizované tajné skupiny —<br>pouze uživatelé vědí, že existují", + "feature-3-title": "E2E-šifrované decentralizované skupiny — pouze uživatelé vědí, že existují", "feature-4-title": "Hlasové zprávy šifrované E2E", "feature-6-title": "E2E šifrované<br>hlasové a videohovory", - "feature-7-title": "Přenosná šifrovaná databáze— přeneste svůj profil do jiného zařízení", + "feature-7-title": "Přenosné šifrované úložiště aplikace — přesuňte profil do jiného zařízení", "feature-8-title": "režim Inkognito —<br>Unikátní pro SimpleX Chat", "simplex-network-overlay-1-title": "Srovnání s protokoly zpráv P2P", "simplex-private-2-title": "Další vrstva<br>šifrování serveru", @@ -115,8 +115,7 @@ "sign-up-to-receive-our-updates": "Přihlaste se k odběru novinek", "enter-your-email-address": "vložte svou e-mailovou adresu", "get-simplex": "Získat SimpleX <a href=\"/downloads\">desktop app</a>", - "why-simplex-is": "Proč je SimpleX", - "unique": "jedinečný", + "why-simplex-is-unique": "Proč je SimpleX <span class='gradient-text'>jedinečný</span>", "learn-more": "Další informace", "more-info": "Více informací", "hide-info": "Skrýt informace", @@ -170,8 +169,8 @@ "comparison-section-list-point-1": "Založeném obvykle na telefonním čísle a v některých případech uživatelském jménu", "comparison-section-list-point-2": "Adresy založené na DNS", "comparison-section-list-point-3": "Veřejný klíč nebo jiné globálně jedinečné ID", - "comparison-section-list-point-4": "Pokud jsou servery operátora ohroženy", - "comparison-section-list-point-5": "Nechrání metadata uživatelů", + "comparison-section-list-point-4": "Pokud jsou servery operátora kompromitovány. Ověřte bezpečnostní kód v Signálu a nějaké jiné aplikaci ke zmírnění", + "comparison-section-list-point-5": "Nechrání soukromí metadat uživatelů", "comparison-section-list-point-6": "Zatímco P2P jsou distribuovány, nejsou federované – fungují jako jediná síť", "comparison-section-list-point-7": "P2P sítě mají buď centrální autoritu, nebo může být ohrožena celá síť", "see-here": "viz zde", @@ -180,7 +179,7 @@ "privacy-matters-overlay-card-1-p-2": "Internetoví prodejci vědí, že lidé s nižšími příjmy častěji provádějí urgentní nákupy, takže mohou účtovat vyšší ceny nebo odebírat slevy.", "privacy-matters-overlay-card-1-p-4": "Platforma SimpleX chrání soukromí vašich připojení lépe než jakákoli jiná alternativa a plně zabraňuje tomu, aby byl váš sociální graf dostupný všem společnostem nebo organizacím. I když lidé používají servery poskytované SimpleX Chat, neznáme počet uživatelů ani jejich připojení.", "privacy-matters-overlay-card-2-p-2": "Chcete-li být objektivní a činit nezávislá rozhodnutí, musíte mít svůj informační prostor pod kontrolou. Je to možné pouze v případě, že používáte soukromou komunikační platformu, která nemá přístup k vašemu sociálnímu grafu.", - "simplex-unique-overlay-card-1-p-2": "K doručování zpráv SimpleX používá <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>párové anonymní adresy</a> jednosměrných front zpráv, oddělených pro přijaté a odeslané zprávy, obvykle přes různé servery. Používání SimpleX je jako mít <strong> jinou “ vypalovačku” e-mail nebo telefon pro každý kontakt</strong> a žádné potíže s jejich správou.", + "simplex-unique-overlay-card-1-p-2": "K doručování zpráv SimpleX používá <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>párové anonymní adresy</a> jednosměrných front zpráv, oddělených pro přijaté a odeslané zprávy, obvykle přes různé servery.", "privacy-matters-overlay-card-3-p-2": "Jedním z nejvíce šokujících příběhů je zkušenost <a href='https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi' target='_blank'>Mohamedoua Oulda Salahiho</a> popsaná v jeho pamětech a zobrazená v Mauritánském filmu. Byl umístěn do tábora na Guantánamu bez soudu a byl tam 15 let mučen po telefonátu svému příbuznému v Afghánistánu pro podezření z účasti na útocích z 11. září, i když předchozích 10 let žil v Německu.", "simplex-unique-overlay-card-1-p-1": "Na rozdíl od jiných platforem pro zasílání zpráv nemá SimpleX <strong> žádné identifikátory přiřazené uživatelům</strong>. Nespoléhá se na telefonní čísla, adresy založené na doméně (jako je e-mail nebo XMPP), uživatelská jména, veřejné klíče nebo dokonce náhodná čísla k identifikaci svých uživatelů —. Nevíme', kolik lidí používá naše servery SimpleX.", "invitation-hero-header": "Byl vám zaslán odkaz pro připojení na SimpleX Chat", @@ -195,7 +194,7 @@ "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.<br> SimpleX ne, <strong>ani náhodná čísla</strong>.<br> 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.", - "feature-2-title": "Obrázky a soubory šifrované E2E<br>", + "feature-2-title": "E2E šifrované<br>obrázky, videa a soubory", "feature-5-title": "Mizící tajné konverzace", "simplex-private-1-title": "2-vrstvé<br>end-to-end šifrování", "simplex-private-8-title": "Smíchání zpráv<br>ke snížení korelace", @@ -209,7 +208,7 @@ "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": "Protokol s dvojitou západkou —<br>zprávy OTR s dokonalým dopředným utajením a obnovení po vloupání.", + "simplex-private-card-1-point-1": "Double-ratchet protokol —<br>OTR messaging s dokonalým dopředným utajením a obnovou po vloupání.", "guide-dropdown-1": "Rychlý start", "guide-dropdown-2": "Odesílání zpráv", "guide-dropdown-3": "Tajné skupiny", @@ -232,5 +231,29 @@ "on-this-page": "Na této stránce", "back-to-top": "Zpět nahoru", "newer-version-of-eng-msg": "V angličtině je novější verze této stránky.", - "glossary": "Rejstřík" + "glossary": "Rejstřík", + "hero-overlay-3-title": "Hodnocení zabezpečení", + "hero-overlay-3-textlink": "Hodnocení zabezpečení", + "hero-overlay-card-3-p-1": "<a href=\"https://www.trailofbits.com/about/\">Trail of Bits</a> 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, <span class='hide-on-mobile'> naskenujte QR kód nebo</span> 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 <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>exportujte</a> 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": "Služba SimpleX Directory", + "please-enable-javascript": "Prosím, povolte JavaScript k zobrazení QR kódu.", + "please-use-link-in-mobile-app": "Prosím použijte odkaz v mobilní aplikaci", + "simplex-chat-via-f-droid": "SimpleX Chat přes F-Droid", + "simplex-chat-repo": "SimpleX Chat repozitář", + "stable-and-beta-versions-built-by-developers": "Stabilní a beta verze vytvořené vývojáři", + "signing-key-fingerprint": "Otisk podpisového klíče (SHA-256)", + "f-droid-org-repo": "F-Droid.org repozitář", + "stable-versions-built-by-f-droid-org": "Stabilní verze vytvořené F-Droid.org", + "releases-to-this-repo-are-done-1-2-days-later": "Vydání v tomto repozitáři se provádí o několik dní později", + "jobs": "Připojit k týmu", + "hero-overlay-card-3-p-2": "Trail of Bits přezkoumala kryptografii a síťové komponenty SimpleX platformy v listopadu 2022. Přečtěte si více v <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">ohlášení</a>.", + "docs-dropdown-9": "Ke stažení", + "docs-dropdown-10": "Transparentnost", + "docs-dropdown-11": "FAQ (často kladené dotazy)", + "docs-dropdown-12": "Bezpečnost", + "hero-overlay-card-3-p-3": "Trail of Bits přezkoumala kryptografický design síťových protokolů SimpleX v červenci 2024. <a href=\"/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html\">Přečíst více</a>.", + "docs-dropdown-14": "SimpleX pro podnikání" } diff --git a/website/langs/de.json b/website/langs/de.json index cb03588e1f..3e5ef9fbc4 100644 --- a/website/langs/de.json +++ b/website/langs/de.json @@ -15,65 +15,65 @@ "simplex-explained-tab-1-p-1": "Sie können Kontakte und Gruppen erstellen und haben Zwei-Wege-Kommunikation wie in jedem anderen Messenger.", "simplex-explained-tab-1-p-2": "Wie funktioniert das mit den unidirektionalen Warteschlangen und ohne Profilkennungen?", "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-Unterhaltung oder seiner Verbindungen zu kennen.", + "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.", - "smp-protocol": "SMP Protokoll", + "smp-protocol": "SMP-Protokoll", "chat-bot-example": "Beispiel für einen Chatbot", "donate": "Spenden", - "copyright-label": "© 2020-2023 SimpleX | Open-Source Projekt", - "chat-protocol": "Chat Protokoll", - "simplex-chat-protocol": "SimpleX Chat Protokoll", - "terminal-cli": "Terminal Kommandozeilen-Schnittstelle", - "terms-and-privacy-policy": "Bedingungen & Datenschutzbestimmungen", + "copyright-label": "© 2020-2025 SimpleX | Open-Source-Projekt", + "chat-protocol": "Chat-Protokoll", + "simplex-chat-protocol": "SimpleX-Chat-Protokoll", + "terminal-cli": "Terminal-Kommandozeilen-Schnittstelle", + "terms-and-privacy-policy": "Datenschutzrichtlinie", "hero-header": "Privatsphäre neu definiert", "hero-overlay-2-textlink": "Wie funktioniert SimpleX?", "hero-subheader": "Der erste Messenger<br>ohne Nutzerkennungen", "hero-p-1": "Andere Apps haben Nutzerkennungen: Signal, Matrix, Session, Briar, Jami, Cwtch usw.<br> SimpleX arbeitet ohne diese, <strong>nicht einmal mit Zufallszahlen</strong>.<br> Dies steigert die Privatsphäre ungemein.", "hero-overlay-1-textlink": "Warum sind Nutzerkennungen schlecht für die Privatspäre?", - "hero-overlay-2-title": "Warum Benutzerkennungen schlecht für die Privatsphäre sind?", + "hero-overlay-2-title": "Warum sind Benutzerkennungen schlecht für die Privatsphäre?", "hero-2-header": "Aufbau einer privaten Verbindung", "hero-overlay-1-title": "Wie funktioniert SimpleX?", "hero-2-header-desc": "Das Video zeigt Ihnen, wie Sie sich mit einem Kontakt mit dessen Einmal-QR-Code persönlich oder per Videotreff verbinden. Sie können sich auch über einen geteilten Einladungslink miteinander verbinden.", - "feature-2-title": "Ende-zu-Ende verschlüsselte<br>Bilder, Videos und Dateien", - "feature-4-title": "Ende-zu-Ende verschlüsselte Sprachnachrichten", - "feature-1-title": "Ende-zu-Ende verschlüsselte Nachrichten mit Markdowns und Bearbeitungsmöglichkeiten", - "feature-3-title": "Ende-zu-Ende verschlüsselte dezentralisierte Gruppen — Nur die Nutzer wissen, dass diese überhaupt existieren", + "feature-2-title": "Ende-zu-Ende-verschlüsselte<br>Bilder, Videos und Dateien", + "feature-4-title": "Ende-zu-Ende-verschlüsselte Sprachnachrichten", + "feature-1-title": "Ende-zu-Ende-verschlüsselte Nachrichten mit Markdowns und Bearbeitungsmöglichkeiten", + "feature-3-title": "Ende-zu-Ende-verschlüsselte dezentralisierte Gruppen — Nur die Nutzer wissen, dass diese überhaupt existieren", "simplex-private-8-title": "Mischen von Nachrichten,<br>um Korrelationen zu reduzieren", "feature-5-title": "Verschwindende geheime Unterhaltungen", - "feature-6-title": "Ende-zu-Ende verschlüsselte Sprach- und Videoanrufe", + "feature-6-title": "Ende-zu-Ende-verschlüsselte Sprach- und Videoanrufe", "feature-7-title": "Portable und verschlüsselte App-Datenspeicherung — verschieben Sie das komplette Profil einfach auf ein anderes Gerät", "feature-8-title": "Inkognito-Modus —<br>Einzigartig in SimpleX Chat", "simplex-network-overlay-1-title": "Vergleich mit P2P Nachrichten-Protokollen", - "simplex-private-1-title": "Zwei Schichten der<br>Ende-zu-Ende Verschlüsselung", + "simplex-private-1-title": "Zwei Schichten der<br>Ende-zu-Ende-Verschlüsselung", "simplex-private-2-title": "Zusätzliche Schicht der<br>Server-Verschlüsselung", "simplex-private-3-title": "Sichere authentifizierte<br>TLS-Transportschicht", "simplex-private-4-title": "Optionaler<br>Zugang per Tor", "simplex-private-5-title": "Mehrere Schichten der<br>Inhalteauffüllung", - "simplex-private-7-title": "Nachrichten-Integrität<br>Überprüfung", - "simplex-private-6-title": "Out-of-Band<br>Schlüsselaustausch", + "simplex-private-7-title": "Nachrichten-Integritäts-<br>Überprüfung", + "simplex-private-6-title": "Out-of-Band-<br>Schlüsselaustausch", "simplex-private-9-title": "Unidirektionale<br>Nachrichten-Warteschlangen", "simplex-private-10-title": "Temporäre, anonyme paarweise Kennungen", - "simplex-private-card-1-point-1": "Doppeltes-Ratchet Protokoll —<br>Off-the-Record Nachrichten mit Perfect Forward Secrecy und Einbruchserholung.", - "simplex-private-card-1-point-2": "NaCL Kryptobox in jeder Warteschlange, um eine Korrelation des Datenverkehrs zwischen Nachrichtenwarteschlangen zu verhindern, falls TLS kompromittiert wurde.", + "simplex-private-card-1-point-1": "Double-Ratchet-Protokoll —<br>Off-the-Record-Nachrichten mit Perfect Forward Secrecy, Abstreitbarkeit und Wiederherstellung nach einer Kompromittierung.", + "simplex-private-card-1-point-2": "NaCL-Kryptobox in jeder Warteschlange, um eine Korrelation des Datenverkehrs zwischen Nachrichtenwarteschlangen zu verhindern, falls TLS kompromittiert wurde.", "simplex-private-card-3-point-1": "Für Client-Server-Verbindungen wird nur TLS 1.2/1.3 mit starken Algorithmen verwendet.", "simplex-private-card-2-point-1": "Zusätzliche Server-Verschlüsselungs-Schicht für die Zustellung an den Empfänger, um eine Korrelation zwischen empfangenen und gesendeten Server-Daten zu vermeiden, falls TLS kompromittiert wurde.", "simplex-private-card-3-point-2": "Server-Fingerabdrücke und Kanalbindung verhindern MITM- und Replay-Angriffe.", "simplex-private-card-3-point-3": "Die Wiederaufnahme von Verbindungen ist deaktiviert, um Sitzungs-Angriffe zu verhindern.", "simplex-private-card-4-point-1": "Um Ihre IP-Adresse zu schützen, können Sie per Tor oder irgendeinem anderen Transportschichten-Netzwerk auf Server zugreifen.", - "simplex-private-card-4-point-2": "Um SimpleX per Tor zu nutzen, installieren Sie unter Android bitte die <a href=\"https://guardianproject.info/apps/org.torproject.android/\" target=\"_blank\">Orbot App</a> und aktivieren Sie den SOCKS5 Proxy oder <a href=\"https://apps.apple.com/us/app/orbot/id1609461599?platform=iphone\" target=\"_blank\">unter iOS</a> per VPN.", + "simplex-private-card-4-point-2": "Um SimpleX per Tor zu nutzen, installieren Sie unter Android bitte die <a href=\"https://guardianproject.info/apps/org.torproject.android/\" target=\"_blank\">Orbot-App</a> und aktivieren Sie den SOCKS5-Proxy oder <a href=\"https://apps.apple.com/us/app/orbot/id1609461599?platform=iphone\" target=\"_blank\">unter iOS</a> per VPN.", "simplex-private-card-5-point-1": "SimpleX nutzt Inhalte-Auffüllung für jede Verschlüsselungs-Schicht, um Angriffe auf die Nachrichtengröße zu vereiteln.", "simplex-private-card-5-point-2": "Erzeugt Nachrichten mit unterschiedlichen Größen, die für Server und Netzwerk-Beobachter identisch aussehen.", - "simplex-private-card-6-point-1": "Viele Kommunikations-Plattformen sind für MITM-Angriffe durch Server oder Netzwerk-Provider anfällig.", + "simplex-private-card-6-point-1": "Viele Kommunikations-Netzwerke sind für MITM-Angriffe durch Server oder Netzwerk-Anbieter anfällig.", "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 Meta-Daten reduziert.", + "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-unique-4-title": "Sie besitzen das SimpleX Netzwerk", + "simplex-unique-4-title": "Sie besitzen das SimpleX-Netzwerk", "privacy-matters-1-title": "Werbung und Preisdiskriminierung", - "privacy-matters-1-overlay-1-title": "Privatsphäre sichert Ihnen Geld", - "privacy-matters-2-overlay-1-title": "Privatsphäre gibt Ihnen Kraft", - "privacy-matters-1-overlay-1-linkText": "Privatsphäre sichert Ihnen Geld", + "privacy-matters-1-overlay-1-title": "Privatsphäre spart Ihnen Geld", + "privacy-matters-2-overlay-1-title": "Privatsphäre gibt Ihnen Macht", + "privacy-matters-1-overlay-1-linkText": "Privatsphäre spart Ihnen Geld", "privacy-matters-2-overlay-1-linkText": "Privatsphäre gibt Ihnen Kraft", "privacy-matters-3-title": "Strafverfolgung wegen einer harmlosen Verbindung", "privacy-matters-3-overlay-1-title": "Privatsphäre schützt Ihre Freiheit", @@ -81,43 +81,43 @@ "privacy-matters-3-overlay-1-linkText": "Privatsphäre schützt Ihre Freiheit", "simplex-unique-1-title": "Sie erhalten eine vollumfängliche Privatsphäre", "simplex-unique-1-overlay-1-title": "Komplette Privatsphäre für Ihre Identität, ihr Profil, Ihre Kontakte und Metadaten", - "simplex-unique-2-title": "Sie sind geschützt vor<br>SPAM und Missbrauch", - "simplex-unique-2-overlay-1-title": "Der beste Schutz vor SPAM und Missbrauch", + "simplex-unique-2-title": "Sie sind geschützt vor<br>Spam und Missbrauch", + "simplex-unique-2-overlay-1-title": "Der beste Schutz vor Spam und Missbrauch", "simplex-unique-3-title": "Sie haben die Kontrolle über Ihre Daten", "simplex-unique-3-overlay-1-title": "Besitz, Kontrolle und Sicherheit Ihrer Daten", - "simplex-unique-4-overlay-1-title": "Voll dezentralisiert — Die Nutzer besitzen das SimpleX Netzwerk", - "hero-overlay-card-1-p-1": "Viele Nutzer fragen:<em>Woher weiß SimpleX, an wenn es Nachrichten versenden muss, wenn es keine Benutzerkennungen gibt?</em>", + "simplex-unique-4-overlay-1-title": "Voll dezentralisiert — Die Nutzer besitzen das SimpleX-Netzwerk", + "hero-overlay-card-1-p-1": "Viele Nutzer fragen:<em>Woher weiß SimpleX, an wen es Nachrichten versenden muss, wenn es keine Benutzerkennungen gibt?</em>", "simplex-private-card-7-point-1": "Um die Integrität von Nachrichten sicherzustellen, werden sie sequentiell durchnummeriert und beinhalten den Hash der vorhergehenden Nachricht.", "simplex-private-card-7-point-2": "Der Empfänger wird alarmiert, sobald eine Nachricht ergänzt, entfernt oder verändert wird.", - "simplex-private-card-8-point-1": "Die SimpleX Server arbeiten als Mix-Knoten mit geringer Verzögerung — eingehende und ausgehende Nachrichten haben eine unterschiedliche Reihenfolge.", + "simplex-private-card-8-point-1": "Die SimpleX-Server arbeiten als Mix-Knoten mit geringer Verzögerung — eingehende und ausgehende Nachrichten haben eine unterschiedliche Reihenfolge.", "hero-overlay-card-1-p-3": "Sie definieren, welche(n) Server Sie für den Empfang von Nachrichten nutzen. Ihre Kontakte — nutzen diese Server, um ihnen Nachrichten darüber zu senden. Jede Konversation nutzt üblicherweise also zwei unterschiedliche Server.", - "hero-overlay-card-1-p-5": "Die Benutzer-Profile, Kontakte und Gruppen werden nur auf den Endgerät des Nutzers gespeichert. Die Nachrichten werden mit einer 2-Schichten Ende-zu-Ende Verschlüsselung versendet.", - "hero-overlay-card-1-p-6": "Lesen Sie mehr darüber im <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>SimpleX Whitepaper</a>.", + "hero-overlay-card-1-p-5": "Die Benutzer-Profile, Kontakte und Gruppen werden nur auf den Endgerät des Nutzers gespeichert. Die Nachrichten werden mit einer 2-Schichten-Ende-zu-Ende-Verschlüsselung versendet.", + "hero-overlay-card-1-p-6": "Lesen Sie mehr darüber im <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>SimpleX-Whitepaper</a>.", "hero-overlay-card-2-p-2": "Sie können diese Informationen mit bestehenden öffentlichen sozialen Netzwerken korrelieren und damit wahre Identitäten herausfinden.", - "hero-overlay-card-2-p-3": "Wenn Sie sich mit zwei unterschiedlichen Kontakten über das selbe Profil unterhalten, können sie, selbst bei sehr auf Privatsphäre achtenden Apps, die Tor v3 Dienste nutzen, feststellen, dass diese Kontakte mit der selben Person verbunden sind.", - "hero-overlay-card-1-p-2": "Um Nachrichten auszuliefern, nutzt SimpleX statt Benutzerkennungen wie auf allen anderen Plattformen, temporäre, anonyme und paarweise Kennungen für Nachrichten-Warteschlangen, die für jede Ihrer Verbindungen unterschiedlich sind — Es gibt keinerlei Langzeit-Kennungen.", + "hero-overlay-card-2-p-3": "Wenn Sie sich mit zwei unterschiedlichen Kontakten über dasselbe Profil unterhalten, können sie, selbst bei sehr auf Privatsphäre bedachten Apps, die Tor-v3-Dienste nutzen, feststellen, dass diese Kontakte mit derselben Person verbunden sind.", + "hero-overlay-card-1-p-2": "Um Nachrichten auszuliefern, nutzt SimpleX statt Benutzerkennungen wie alle anderen Netzwerke nur temporäre, anonyme und paarweise Kennungen für Nachrichten-Warteschlangen, die für jede Ihrer Verbindungen unterschiedlich sind — es gibt keinerlei Langzeit-Kennungen.", "hero-overlay-card-1-p-4": "Dieses Design verhindert schon auf der Applikations-Ebene Datenlecks für jegliche Benutzer'Metadaten. Sie können sich über Tor mit Nachrichten-Servern verbinden, um Ihre Privatsphäre weiter zu verbessern und die von Ihnen genutzte IP-Adresse zu schützen.", "hero-overlay-card-2-p-1": "Wenn Nutzer dauerhafte Identitäten besitzen, selbst wenn diese eine Zufallsnummer, wie eine Sitzungs-ID, ist, besteht ein Risiko, das Provider oder Angreifer feststellen können, wie Nutzer miteinander verbunden sind und wie viele Nachrichten sie versenden.", "hero-overlay-card-2-p-4": "SimpleX schützt gegen solche Angriffe, weil es vom Design her keinerlei Benutzerkennungen besitzt. Und Sie haben sogar unterschiedliche Anzeigenamen für jeden Kontakt und vermeiden jegliche geteilte Daten zwischen diesen, wenn Sie den Inkognito-Modus nutzen.", "simplex-network-overlay-card-1-p-1": "<a href='https://en.wikipedia.org/wiki/Peer-to-peer'>Peer-to-Peer</a>-Nachrichten-Protokolle und -Applikationen haben verschiedene Probleme, die diese weniger vertrauenswürdig, die Analyse wesentlich komplexer und anfälliger gegen verschiedene Arten von Angriffen, als bei SimpleX machen.", "simplex-network-overlay-card-1-li-2": "Das SimpleX Design hat, im Gegensatz zu den meisten P2P-Netzwerken, keinerlei globalen Benutzerkennungen, auch keine temporären. Es nutzt ausschließlich temporäre paarweise Kennungen, die bessere Anonymität und Metadaten-Schutz bieten.", - "simplex-network-overlay-card-1-li-4": "P2P-Implementierungen können durch Internet-Provider blockiert werden, wie beispielweise <a href='https://en.wikipedia.org/wiki/BitTorrent'>BitTorrent</a>). SimpleX ist transportunabhängig - es kann über Standard-Web-Protokolle, wie beispielsweise WebSockets, arbeiten.", + "simplex-network-overlay-card-1-li-4": "——P2P-Implementierungen können durch Internetanbieter blockiert werden, wie beispielweise <a href='https://en.wikipedia.org/wiki/BitTorrent'>BitTorrent</a>). SimpleX ist transportunabhängig — es kann über Standard-Web-Protokolle, wie beispielsweise WebSockets, arbeiten.", "simplex-network-overlay-card-1-li-6": "P2P-Netzwerke können anfällig für <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'>DRDoS-Angriffe</a> sein, wenn die Clients den Datenverkehr erneut senden und verstärken können, was zu einem netzwerkweiten Denial-of-Service führt. SimpleX-Clients leiten nur Datenverkehr von bekannten Verbindungen weiter und können von einem Angreifer nicht dazu verwendet werden, den Datenverkehr im gesamten Netzwerk zu verstärken.", "privacy-matters-overlay-card-1-p-1": "Viele große Unternehmen nutzen Informationen, mit wem Sie in Verbindung stehen, um Ihr Einkommen zu schätzen, Ihnen Produkte zu verkaufen, die Sie nicht wirklich benötigen und um die Preise zu bestimmen.", "privacy-matters-overlay-card-1-p-2": "Online-Händler wissen, dass Menschen mit geringerem Einkommen eher dringende Einkäufe tätigen, sodass sie möglicherweise höhere Preise verlangen können oder Rabatte streichen.", "privacy-matters-overlay-card-1-p-3": "Einige Finanz- und Versicherungsunternehmen verwenden soziale Graphen, um Zinssätze und Prämien zu ermitteln. Menschen mit niedrigerem Einkommen zahlen so häufig mehr — dies ist als <a href='https://fairbydesign.com/povertypremium/' target='_blank'>\"Armutsprämie\"</a> bekannt.", - "privacy-matters-overlay-card-2-p-2": "Um objektiv zu sein und unabhängige Entscheidungen treffen zu können, müssen Sie die Kontrolle über Ihren Informationsraum haben. Dies ist nur möglich, wenn Sie eine private Kommunikationsplattform verwenden, die keinen Zugriff auf Ihren sozialen Graphen hat.", - "privacy-matters-overlay-card-2-p-3": "SimpleX ist die erste Plattform, die per Design keinerlei Benutzerkennungen hat und auf diese Weise Ihren Verbindungsgraphen besser schützt als jede andere bekannte Alternative.", + "privacy-matters-overlay-card-2-p-2": "Um objektiv zu sein und unabhängige Entscheidungen treffen zu können, müssen Sie die Kontrolle über Ihren Informationsraum haben. Dies ist nur möglich, wenn Sie ein privates Kommunikations-Netzwerk verwenden, welches keinen Zugriff auf Ihren sozialen Graphen hat.", + "privacy-matters-overlay-card-2-p-3": "SimpleX ist das erste Netzwerk, welches per Design keinerlei Benutzerkennungen hat und auf diese Weise Ihren Verbindungsgraphen besser schützt als jede andere bekannte Alternative.", "privacy-matters-overlay-card-3-p-1": "Jede Person sollte sich um ihre Privatsphäre und die Sicherheit ihrer Kommunikation kümmern — Harmlose Gespräche könnten Sie in Gefahr bringen, selbst wenn Sie nichts zu verbergen haben.", "privacy-matters-overlay-card-3-p-4": "Es reicht nicht aus, einfach einen Ende-zu-Ende-verschlüsselten Messenger zu verwenden. Wir alle sollten den Messenger verwenden, der die Privatsphäre unserer persönlichen Netzwerke schützt, mit welchen wir verbunden sind.", - "simplex-unique-overlay-card-1-p-3": "Dieses Design schützt die Privatsphäre von Ihnen und der Personen, mit denen Sie kommunizieren, und verbirgt die Verbindungen vor den SimpleX-Plattform-Servern und möglichen Beobachtern. Um Ihre IP-Adresse vor den Servern zu verbergen, können Sie sich <strong>per Tor mit den SimpleX-Servern verbinden</strong>.", - "simplex-unique-overlay-card-2-p-1": "Da Sie auf der SimpleX-Plattform keine Kennungen haben, kann Sie niemand kontaktieren, es sei denn, Sie geben eine einmalige oder vorübergehende Benutzeradresse in Form eines QR-Codes oder eines Links weiter.", + "simplex-unique-overlay-card-1-p-3": "Dieses Design schützt die Privatsphäre von Ihnen und der Nutzer, mit denen Sie kommunizieren, und verbirgt die Verbindungen vor den SimpleX-Netzwerk-Servern und möglichen Beobachtern. Um Ihre IP-Adresse vor den Servern zu verbergen, können Sie sich <strong>per Tor mit den SimpleX-Servern verbinden</strong>.", + "simplex-unique-overlay-card-2-p-1": "Da Sie im SimpleX-Netzwerk keine Kennungen haben, kann Sie niemand kontaktieren, es sei denn, Sie geben eine einmalige oder vorübergehende Benutzeradresse in Form eines QR-Codes oder eines Links weiter.", "simplex-unique-overlay-card-3-p-2": "Die Ende-zu-Ende-verschlüsselten Nachrichten werden vorübergehend auf SimpleX-Relay-Servern gespeichert, bis sie vom Endgerät empfangen und danach endgültig gelöscht werden.", "simplex-unique-overlay-card-3-p-3": "Im Gegensatz zu föderierten Netzwerkservern (wie z.B. Mail, XMPP oder Matrix) speichern die SimpleX-Server keine Benutzerkonten, sondern leiten Nachrichten nur weiter, so dass die Privatsphäre der beteiligten Parteien geschützt ist.", - "simplex-unique-overlay-card-4-p-1": "Sie können <strong>SimpleX mit Ihren eigenen Servern verwenden</strong> und trotzdem mit Personen kommunizieren, welche die von uns bereitgestellten und vorkonfigurierten Server verwenden.", - "simplex-unique-card-1-p-1": "SimpleX schützt die Privatsphäre Ihres Profils, Ihrer Kontakte und Metadaten und verbirgt sie vor den SimpleX-Plattform-Servern und allen Beobachtern.", - "simplex-unique-card-1-p-2": "Im Gegensatz zu allen anderen bestehenden Messaging-Plattformen werden den Nutzern von SimpleX keine Kennungen zugewiesen — <strong>nicht einmal Zufallszahlen</strong>.", - "simplex-unique-card-2-p-1": "Da Sie keine Kennung oder feste Adresse auf der SimpleX-Plattform haben, kann Sie niemand kontaktieren, es sei denn, Sie geben eine einmalige oder vorübergehende Benutzeradresse in Form eines QR-Codes oder Links weiter.", + "simplex-unique-overlay-card-4-p-1": "Sie können <strong>SimpleX mit Ihren eigenen Servern verwenden</strong> und trotzdem mit Nutzern kommunizieren, welche die vorkonfigurierten Server der App verwenden.", + "simplex-unique-card-1-p-1": "SimpleX schützt die Privatsphäre Ihres Profils, Ihrer Kontakte und Metadaten und verbirgt sie vor den SimpleX-Netzwerk-Servern und weiteren möglichen Beobachtern.", + "simplex-unique-card-1-p-2": "Im Gegensatz zu allen anderen bestehenden Messaging-Netzwerken werden den Nutzern von SimpleX keine Kennungen zugewiesen — <strong>nicht einmal Zufallszahlen</strong>.", + "simplex-unique-card-2-p-1": "Da Sie keine Kennung oder feste Adresse im SimpleX-Netzwerk haben, kann Sie niemand kontaktieren, es sei denn, Sie geben eine einmalige oder vorübergehende Benutzeradresse in Form eines QR-Codes oder Links weiter.", "simplex-unique-card-3-p-1": "SimpleX speichert Benutzerdaten nur auf den Endgeräten und das in einem <strong>portablen, verschlüsselten Datenbankformat</strong> — welches auf ein anderes Gerät übertragen werden kann.", "simplex-unique-card-3-p-2": "Die Ende-zu-Ende-verschlüsselten Nachrichten werden vorübergehend auf SimpleX-Relay-Servern gespeichert, bis sie vom Endgerät empfangen und danach endgültig gelöscht werden.", "join": "Nutzen Sie", @@ -131,28 +131,28 @@ "learn-more": "Erfahren Sie mehr darüber", "more-info": "Weitere Informationen", "hide-info": "Informationen verbergen", - "contact-hero-subheader": "Scannen Sie den QR-Code mit der SimpleX Chat-Applikation auf Ihrem Mobilgerät oder Tablet.", - "contact-hero-p-2": "Bisher SimpleX Chat noch nicht herunter geladen?", + "contact-hero-subheader": "Scannen Sie den QR-Code mit der SimpleX-Chat-Applikation auf Ihrem Mobilgerät oder Tablet.", + "contact-hero-p-2": "SimpleX Chat noch nicht heruntergeladen?", "scan-qr-code-from-mobile-app": "Scannen Sie den QR-Code aus der mobilen Applikation", - "install-simplex-app": "Installieren Sie die SimpleX Applikation", + "install-simplex-app": "Installieren Sie die SimpleX-App", "connect-in-app": "Verbinden Sie sich in der Applikation", - "open-simplex-app": "Öffnen Sie die SimpleX Applikation", + "open-simplex-app": "Öffnen Sie die SimpleX-App", "scan-the-qr-code-with-the-simplex-chat-app-description": "Die öffentlichen Schlüssel und die Adresse der Nachrichtenwarteschlange in diesem Link werden NICHT über das Netzwerk gesendet, wenn Sie diese Seite aufrufen —<br> sie sind in dem Hash-Fragment der Link-URL enthalten.", "installing-simplex-chat-to-terminal": "Installation von SimpleX-Chat im Terminal", "use-this-command": "Benutzen Sie dieses Kommando:", "see-simplex-chat": "Siehe SimpleX-Chat", - "github-repository": "GitHub Repository", - "the-instructions--source-code": "Die Anleitung, wie Sie es herunter laden und aus dem Quellcode kompilieren.", + "github-repository": "GitHub-Repository", + "the-instructions--source-code": "Für die Anleitungen, wie Sie es herunterladen oder aus dem Quellcode kompilieren.", "if-you-already-installed": "Wenn Sie es schon installiert haben", "simplex-chat-for-the-terminal": "SimpleX Chat für das Terminal", "privacy-matters-section-header": "Warum es auf Privatsphäre <span class='gradient-text'>ankommt</span>", "privacy-matters-section-label": "Stellen Sie sicher, dass Ihr Messenger keinen Zugriff auf Ihre Daten hat!", "tap-to-close": "Zum Schließen drücken", - "simplex-network-section-header": "SimpleX <span class='gradient-text'>Netzwerk</span>", + "simplex-network-section-header": "SimpleX-<span class='gradient-text'>Netzwerk</span>", "simplex-network-1-header": "Im Gegensatz zu P2P-Netzwerken", "simplex-network-1-desc": "Alle Nachrichten werden über Server versandt, was sowohl einen besseren Schutz der Metadaten als auch eine zuverlässigere asynchrone Nachrichtenübermittlung ermöglicht und dabei Vieles vermeidet", "simplex-network-1-overlay-linktext": "Probleme von P2P-Netzwerken", - "simplex-network-2-desc": "Simple X-Relay-Server speichern KEINE Benutzerprofile, Kontakte und zugestellte Nachrichten und stellen KEINE Verbindungen untereinander her und es gibt KEIN Serververzeichnis.", + "simplex-network-2-desc": "SimpleX-Relay-Server speichern KEINE Benutzerprofile, Kontakte und zugestellte Nachrichten und stellen KEINE Verbindungen untereinander her, und es gibt KEIN Serververzeichnis.", "simplex-network-3-header": "SimpleX-Netzwerk", "comparison-section-header": "Vergleich mit anderen Protokollen", "protocol-1-text": "Signal, große Plattformen", @@ -164,28 +164,27 @@ "comparison-point-4-text": "Einzelnes oder zentralisiertes Netzwerk", "yes": "Ja", "no": "Nein", - "no-private": "Nein - vertraulich", - "no-secure": "Nein - sicher", - "no-resilient": "Nein - widerstandsfähig", - "no-federated": "Nein - föderiert", + "no-private": "Nein – vertraulich", + "no-secure": "Nein – sicher", + "no-resilient": "Nein – widerstandsfähig", + "no-federated": "Nein – föderiert", "comparison-section-list-point-2": "DNS-basierte Adressen", "comparison-section-list-point-3": "Öffentlicher Schlüssel oder eine andere weltweit eindeutige ID", - "comparison-section-list-point-4": "Wenn die Server des Betreibers kompromittiert werden. In Signal und weiteren Apps kann der Securitycode überprüft werden, um dies zu entschärfen", - "comparison-section-list-point-6": "P2P sind zwar verteilt, aber nicht föderiert - sie arbeiten als ein einziges Netzwerk", + "comparison-section-list-point-4": "Wenn die Server des Betreibers kompromittiert werden. In Signal und weiteren Apps kann der Sicherheitscode überprüft werden, um dies zu entschärfen", + "comparison-section-list-point-6": "P2P sind zwar verteilt, aber nicht föderiert — sie arbeiten als ein einziges Netzwerk", "comparison-section-list-point-7": "P2P-Netzwerke haben entweder eine zentrale Verwaltung oder das gesamte Netzwerk kann kompromittiert werden", "see-here": "Siehe hier", - "simplex-unique-card-4-p-1": "Das SimpleX-Netzwerk ist vollständig dezentralisiert und unabhängig von Kryptowährungen oder anderen Plattformen außer dem Internet.", - "unique": "einmalig", + "simplex-unique-card-4-p-1": "Das SimpleX-Netzwerk ist vollständig dezentralisiert und unabhängig von Kryptowährungen oder anderen Netzwerken außer dem Internet.", "privacy-matters-overlay-card-2-p-1": "Vor nicht allzu langer Zeit beobachteten wir, wie große Wahlen von einem <a href='https://en.wikipedia.org/wiki/Facebook–Cambridge_Analytica_data_scandal' target='_blank'>angesehenen Beratungsunternehmen</a> manipuliert wurden, welches unsere sozialen Graphen nutzte, um unsere Sicht auf die reale Welt zu verzerren und unsere Stimmen zu manipulieren.", - "privacy-matters-overlay-card-3-p-2": "Eine der schockierendsten Geschichten ist die Erfahrung von <a href='https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi' target='_blank'>Mohamedou Ould Salahi</a>, welche in seinen Memoiren beschrieben und im Film \"The Mauritanian\" gezeigt wird. Er kam nach einem Anruf bei seinen Verwandten in Afghanistan und ohne Gerichtsverfahren in das Guantanamo-Lager und wurde dort einige Jahre lang gefoltert, weil er verdächtigt wurde, an den 9/11-Angriffen beteiligt gewesen zu sein, obwohl er die vorhergehenden 10 Jahre in Deutschland gelebt hatte.", - "simplex-unique-overlay-card-1-p-1": "Im Gegensatz zu anderen Nachrichten-Plattformen weist SimpleX <strong>den Benutzern keine Kennungen</strong> zu. Es verlässt sich nicht auf Telefonnummern, domänenbasierte Adressen (wie E-Mail oder XMPP), Benutzernamen, öffentliche Schlüssel oder sogar Zufallszahlen, um seine Benutzer zu identifizieren — Wir wissen nicht, wie viele Personen unsere SimpleX-Server verwenden.", - "simplex-unique-overlay-card-1-p-2": "Um Nachrichten auszuliefern nutzt SimpleX <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>paarweise anonyme Adressen</a> aus unidirektionalen Nachrichten-Warteschlangen, die für empfangene und gesendete Nachrichten separiert sind und gewöhnlich über verschiedene Server gesendet werden. Die Nutzung von SimpleX entspricht der Nutzung von <strong>unterschiedlichen Mailservern oder Telefonen für jeden einzelnen Kontakt</strong> und vermeidet dabei eine mühsame Verwaltung.", + "privacy-matters-overlay-card-3-p-2": "Eine der schockierendsten Geschichten ist die Erfahrung von <a href='https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi' target='_blank'>Mohamedou Ould Salahi</a>, welche in seinen Memoiren beschrieben und im Film „The Mauritanian“ gezeigt wird. Er kam nach einem Anruf bei seinen Verwandten in Afghanistan und ohne Gerichtsverfahren in das Guantanamo-Lager und wurde dort einige Jahre lang gefoltert, weil er verdächtigt wurde, an den 9/11-Angriffen beteiligt gewesen zu sein, obwohl er die vorhergehenden 10 Jahre in Deutschland gelebt hatte.", + "simplex-unique-overlay-card-1-p-1": "Im Gegensatz zu anderen Nachrichten-Netzwerken weist SimpleX <strong>den Benutzern keine Kennungen</strong> zu. Es verlässt sich nicht auf Telefonnummern, domänenbasierte Adressen (wie E-Mail oder XMPP), Benutzernamen, öffentliche Schlüssel oder Zufallszahlen, um seine Benutzer zu identifizieren — selbst SimpleX-Server-Betreiber wissen nicht, wie viele Personen deren Server verwenden.", + "simplex-unique-overlay-card-1-p-2": "Um Nachrichten auszuliefern nutzt SimpleX <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>paarweise anonyme Adressen</a> aus unidirektionalen Nachrichten-Warteschlangen, die für empfangene und gesendete Nachrichten separiert sind und gewöhnlich über verschiedene Server gesendet werden.", "simplex-network-overlay-card-1-li-5": "Alle bekannten P2P-Netzwerke können anfällig für <a href='https://en.wikipedia.org/wiki/Sybil_attack'>Sybil-Angriffe</a> sein, da jeder Knoten ermittelbar ist und das Netzwerk als Ganzes funktioniert. Bekannte Maßnahmen zur Verhinderung erfordern entweder eine zentralisierte Komponente oder einen teuren <a href='https://en.wikipedia.org/wiki/Proof_of_work'>Ausführungsnachweis</a>. Das SimpleX-Netzwerk bietet keine Ermittlung der Server, ist fragmentiert und arbeitet mit mehreren isolierten Subnetzwerken, wodurch netzwerkweite Angriffe unmöglich werden.", "simplex-network-overlay-card-1-li-3": "P2P löst nicht das Problem des <a href='https://en.wikipedia.org/wiki/Man-in-the-middle_attack'>MITM-Angriffs</a> und die meisten bestehenden Implementierungen nutzen für den initialen Schlüsselaustausch keine Out-of-Band-Nachrichten. Im Gegensatz hierzu nutzt SimpleX für den initialen Schlüsselaustausch Out-of-Band-Nachrichten oder zum Teil schon bestehende sichere und vertrauenswürdige Verbindungen.", - "tap-the-connect-button-in-the-app": "Drücken Sie die <span class='text-active-blue'>‘Verbinden’</span> Taste in der Applikation", + "tap-the-connect-button-in-the-app": "Drücken Sie die <span class='text-active-blue'>„Verbinden“</span>-Taste in der Applikation", "to-make-a-connection": "Um eine Verbindung zu starten:", "contact-hero-p-3": "Nutzen Sie die unten genannten Links, um die Applikation herunterzuladen.", - "scan-the-qr-code-with-the-simplex-chat-app": "Scannen Sie den QR-Code mit der SimpleX Chat-Applikation", + "scan-the-qr-code-with-the-simplex-chat-app": "Scannen Sie den QR-Code mit der SimpleX-Chat-Applikation", "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 — <span class='text-active-blue'>mit wem Sie wann Kontakt haben</span> — schützt Sie vor:", "simplex-private-section-header": "Was macht SimpleX <span class='gradient-text'>vertraulich</span>", @@ -193,20 +192,20 @@ "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", - "no-decentralized": "Nein - dezentralisiert", + "no-decentralized": "Nein – dezentralisiert", "comparison-section-list-point-5": "Die Privatsphäre-Metadaten des Nutzers werden nicht geschützt", - "simplex-network-overlay-card-1-li-1": "P2P-Netzwerke vertrauen auf Varianten von <a href='https://en.wikipedia.org/wiki/Distributed_hash_table'>DHT</a>, 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 <em>O(log N)</em> Knoten gesendet, wobei die Knoten durch einen Algorithmus ausgewählt werden.", + "simplex-network-overlay-card-1-li-1": "P2P-Netzwerke vertrauen auf Varianten von <a href='https://en.wikipedia.org/wiki/Distributed_hash_table'>DHT</a>, 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 <em>O(log N)</em>-Knoten gesendet, wobei die Knoten durch einen Algorithmus ausgewählt werden.", "simplex-unique-overlay-card-3-p-4": "Zwischen dem gesendeten und empfangenen Serververkehr gibt es keine gemeinsamen Kennungen oder Chiffriertexte — sodass ein Beobachter nicht ohne weiteres feststellen kann, wer mit wem kommuniziert, selbst wenn TLS kompromittiert wurde.", - "simplex-unique-overlay-card-4-p-3": "Wenn Sie darüber nachdenken, für die SimpleX-Plattform entwickeln zu wollen, z.B. einen Chatbot für SimpleX-App-Nutzer oder die Integration der SimpleX-Chat-Bibliothek in Ihre mobilen Apps, <a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>kontaktieren Sie uns bitte</a> für eine weitere Beratung und Unterstützung.", - "privacy-matters-overlay-card-1-p-4": "Die SimpleX-Plattform 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 die Anwender SimpleX-Chat-Server verwenden, kennen wir die Anzahl der Benutzer oder ihre Verbindungen nicht.", + "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, <a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>kontaktieren Sie uns bitte</a> 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.", "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": "Normale Menschen werden für das, was sie online teilen, sogar unter Nutzung ihrer \"anonymen\" Konten, <a href='https://www.dailymail.co.uk/news/article-11282263/Moment-police-swoop-house-devout-catholic-mother-malicious-online-posts.html' target='_blank'>selbst in demokratischen Ländern</a> verhaftet.", + "privacy-matters-overlay-card-3-p-3": "Normale Menschen werden für das, was sie online teilen, sogar unter Nutzung ihrer „anonymen“ Konten, <a href='https://www.dailymail.co.uk/news/article-11282263/Moment-police-swoop-house-devout-catholic-mother-malicious-online-posts.html' target='_blank'>selbst in demokratischen Ländern</a> verhaftet.", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat speichert alle Benutzerdaten ausschließlich auf den Endgeräten in einem <strong>portablen und verschlüsselten Datenbankformat</strong>, 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": "Die SimpleX-Plattform verwendet ein <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>offenes Protokoll</a> und bietet ein <a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>SDK zur Erstellung von Chatbots</a> an, welches die Erstellung von Diensten ermöglicht, mit denen Nutzer über SimpleX-Chat interagieren können — Wir sind gespannt, welche SimpleX-Dienste Sie erstellen können.", + "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 <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>offenes Protokoll</a> und bietet ein <a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>SDK zur Erstellung von Chatbots</a> 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-card-4-p-2": "Sie können <strong>SimpleX mit Ihren eigenen Servern</strong> oder mit den von uns zur Verfügung gestellten Servern verwenden — und sich trotzdem mit jedem Benutzer verbinden.", - "why-simplex-is": "Warum ist SimpleX", + "why-simplex-is-unique": "Warum ist SimpleX <span class='gradient-text'>einmalig</span>", "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.", "if-you-already-installed-simplex-chat-for-the-terminal": "Wenn Sie SimpleX schon für das Terminal installiert haben", "simplex-network-3-desc": "Die Server stellen <span class='text-active-blue'>unidirektionale Warteschlangen</span> zur Verfügung, um die Benutzer miteinander zu verbinden. Sie haben aber keinen Einblick in den Verbindungs-Graphen des Netzwerks — Diesen haben nur die Benutzer selbst.", @@ -219,7 +218,7 @@ "guide-dropdown-8": "App-Einstellungen", "guide-dropdown-9": "Verbindungen herstellen", "guide": "Leitfaden", - "docs-dropdown-1": "SimpleX-Plattform", + "docs-dropdown-1": "SimpleX-Netzwerk", "docs-dropdown-2": "Zugriff auf Android-Dateien", "docs-dropdown-3": "Zugriff auf die Chat-Datenbank", "docs-dropdown-4": "Den SMP-Server hosten", @@ -234,21 +233,27 @@ "docs-dropdown-7": "SimpleX Chat übersetzen", "glossary": "Glossar", "signing-key-fingerprint": "Fingerabdruck des Signaturschlüssels (SHA-256)", - "f-droid-org-repo": "F-Droid.org Repository", + "f-droid-org-repo": "F-Droid.org-Repository", "stable-versions-built-by-f-droid-org": "Von F-Droid.org erstellte stabile Versionen", - "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat- und F-Droid.org-Repositorys signieren ihre Builds mit verschiedenen Schlüsseln. Zum Umschalten bitte <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>die Chat-Datenbank exportieren</a> und die App neu installieren.", - "releases-to-this-repo-are-done-1-2-days-later": "Die Versionen für dieses Repository werden 1..2 Tage später erstellt", - "docs-dropdown-8": "SimpleX Verzeichnisdienst", + "f-droid-page-f-droid-org-repo-section-text": "SimpleX-Chat- und F-Droid.org-Repositorys signieren ihre Builds mit verschiedenen Schlüsseln. Zum Umschalten bitte <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>die Chat-Datenbank exportieren</a> und die App neu installieren.", + "releases-to-this-repo-are-done-1-2-days-later": "Die Versionen für dieses Repository werden einige Tage später erstellt", + "docs-dropdown-8": "SimpleX-Verzeichnisdienst", "simplex-chat-via-f-droid": "SimpleX Chat per F-Droid", - "simplex-chat-repo": "SimpleX Chat Repository", + "simplex-chat-repo": "SimpleX-Chat-Repository", "stable-and-beta-versions-built-by-developers": "Von den Entwicklern erstellte stabile und Beta-Versionen", - "f-droid-page-simplex-chat-repo-section-text": "Um es Ihrem F-Droid-Client hinzuzufügen <span class='hide-on-mobile'>scannen Sie den QR-Code oder</span> nutzen Sie diese URL:", + "f-droid-page-simplex-chat-repo-section-text": "Um es Ihrem F-Droid-Client hinzuzufügen, <span class='hide-on-mobile'>scannen Sie den QR-Code oder</span> nutzen Sie diese URL:", "comparison-section-list-point-4a": "SimpleX-Relais können die E2E-Verschlüsselung nicht kompromittieren. Überprüfen Sie den Sicherheitscode, um einen möglichen Angriff auf den Out-of-Band-Kanal zu entschärfen", "hero-overlay-3-title": "Sicherheits-Gutachten", - "hero-overlay-card-3-p-2": "Trail of Bits untersuchte im November 2022 die kryptografischen und Netzwerk-Komponenten der SimpleX-Plattform.", - "hero-overlay-card-3-p-3": "Lesen Sie mehr dazu in der <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">Ankündigung</a>.", + "hero-overlay-card-3-p-2": "Trail of Bits untersuchte im November 2022 die kryptografischen und Netzwerk-Komponenten der SimpleX-Plattform. Lesen Sie mehr dazu in der <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">Ankündigung</a>.", "jobs": "Treten Sie dem Team bei", "hero-overlay-3-textlink": "Sicherheits-Gutachten", "hero-overlay-card-3-p-1": "<a href=\"https://www.trailofbits.com/about/\">Trail of Bits</a> ist eine führende Security- und Technologie-Unternehmensberatung, deren Kunden aus den Bereichen Big-Tech, Regierungsbehörden und großen Blockchain-Projekten stammen.", - "docs-dropdown-9": "Downloads" + "docs-dropdown-9": "Downloads", + "please-enable-javascript": "Bitte aktivieren Sie JavaScript, um den QR-Code zu sehen.", + "please-use-link-in-mobile-app": "Bitte nutzen Sie den Link in der Mobiltelefon-App", + "docs-dropdown-10": "Transparent", + "docs-dropdown-11": "FAQ", + "docs-dropdown-12": "Sicherheit", + "hero-overlay-card-3-p-3": "Trail of Bits hat das kryptografische Design des Netzwerk-Protokolls von SimpleX im Juli 2024 überprüft. <a href=\"/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html\">Hier finden Sie weitere Informationen dazu</a>.", + "docs-dropdown-14": "SimpleX für geschäftliche Anwendungen" } diff --git a/website/langs/en.json b/website/langs/en.json index 1bb64c7efa..f9691e2594 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -7,35 +7,35 @@ "why-simplex": "Why SimpleX", "simplex-privacy": "SimpleX privacy", "simplex-network": "SimpleX network", - "simplex-explained": "Simplex explained", + "simplex-explained": "SimpleX explained", "simplex-explained-tab-1-text": "1. What users experience", "simplex-explained-tab-2-text": "2. How does it work", "simplex-explained-tab-3-text": "3. What servers see", "simplex-explained-tab-1-p-1": "You can create contacts and groups, and have two-way conversations, as in any other messenger.", "simplex-explained-tab-1-p-2": "How can it work with unidirectional queues and without user profile identifiers?", "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 conversation or connections.", + "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.", "chat-bot-example": "Chat bot example", "smp-protocol": "SMP protocol", "chat-protocol": "Chat protocol", "donate": "Donate", - "copyright-label": "© 2020-2023 SimpleX | Open-Source Project", + "copyright-label": "© 2020-2025 SimpleX | Open-Source Project", "simplex-chat-protocol": "SimpleX Chat protocol", "terminal-cli": "Terminal CLI", - "terms-and-privacy-policy": "Terms & Privacy Policy", + "terms-and-privacy-policy": "Privacy Policy", "hero-header": "Privacy redefined", "hero-subheader": "The first messenger<br>without user IDs", "hero-p-1": "Other apps have user IDs: Signal, Matrix, Session, Briar, Jami, Cwtch, etc.<br> SimpleX does not, <strong>not even random numbers</strong>.<br> This radically improves your privacy.", "hero-overlay-1-textlink": "Why user IDs are bad for privacy?", "hero-overlay-2-textlink": "How does SimpleX work?", - "hero-overlay-3-textlink": "Security assessment", + "hero-overlay-3-textlink": "Security assessments", "hero-2-header": "Make a private connection", "hero-2-header-desc": "The video shows how you connect to your friend via their 1-time QR-code, in person or via a video link. You can also connect by sharing an invitation link.", "hero-overlay-1-title": "How does SimpleX work?", "hero-overlay-2-title": "Why user IDs are bad for privacy?", - "hero-overlay-3-title": "Security assessment", + "hero-overlay-3-title": "Security assessments", "feature-1-title": "E2E-encrypted messages with markdown and editing", "feature-2-title": "E2E-encrypted<br>images, videos and files", "feature-3-title": "E2E-encrypted decentralized groups — only users know they exist", @@ -65,7 +65,7 @@ "simplex-private-card-4-point-2": "To use SimpleX via Tor please install <a href=\"https://guardianproject.info/apps/org.torproject.android/\" target=\"_blank\">Orbot app</a> and enable SOCKS5 proxy (or VPN <a href=\"https://apps.apple.com/us/app/orbot/id1609461599?platform=iphone\" target=\"_blank\">on iOS</a>).", "simplex-private-card-5-point-1": "SimpleX uses content padding for each encryption layer to frustrate message size attacks.", "simplex-private-card-5-point-2": "It makes messages of different sizes look the same to the servers and network observers.", - "simplex-private-card-6-point-1": "Many communication platforms are vulnerable to MITM attacks by servers or network providers.", + "simplex-private-card-6-point-1": "Many communication networks are vulnerable to MITM attacks by servers or network providers.", "simplex-private-card-6-point-2": "To prevent it SimpleX apps pass one-time keys out-of-band, when you share an address as a link or a QR code.", "simplex-private-card-7-point-1": "To guarantee integrity the messages are sequentially numbered and include the hash of the previous message.", "simplex-private-card-7-point-2": "If any message is added, removed or changed the recipient will be alerted.", @@ -92,7 +92,7 @@ "simplex-unique-4-title": "You own SimpleX network", "simplex-unique-4-overlay-1-title": "Fully decentralised — users own the SimpleX network", "hero-overlay-card-1-p-1": "Many users asked: <em>if SimpleX has no user identifiers, how can it know where to deliver messages?</em>", - "hero-overlay-card-1-p-2": "To deliver messages, instead of user IDs used by all other platforms, SimpleX uses temporary anonymous pairwise identifiers of message queues, separate for each of your connections — there are no long term identifiers.", + "hero-overlay-card-1-p-2": "To deliver messages, instead of user IDs used by all other networks, SimpleX uses temporary anonymous pairwise identifiers of message queues, separate for each of your connections — there are no long term identifiers.", "hero-overlay-card-1-p-3": "You define which server(s) to use to receive the messages, your contacts — the servers you use to send the messages to them. Every conversation is likely to use two different servers.", "hero-overlay-card-1-p-4": "This design prevents leaking any users' metadata on the application level. To further improve privacy and protect your IP address you can connect to messaging servers via Tor.", "hero-overlay-card-1-p-5": "Only client devices store user profiles, contacts and groups; the messages are sent with 2-layer end-to-end encryption.", @@ -102,44 +102,44 @@ "hero-overlay-card-2-p-3": "Even with the most private apps that use Tor v3 services, if you talk to two different contacts via the same profile they can prove that they are connected to the same person.", "hero-overlay-card-2-p-4": "SimpleX protects against these attacks by not having any user IDs in its design. And, if you use Incognito mode, you will have a different display name for each contact, avoiding any shared data between them.", "hero-overlay-card-3-p-1": "<a href=\"https://www.trailofbits.com/about/\">Trail of Bits</a> is a leading security and technology consultancy whose clients include big tech, governmental agencies and major blockchain projects.", - "hero-overlay-card-3-p-2": "Trail of Bits reviewed SimpleX platform cryptography and networking components in November 2022.", - "hero-overlay-card-3-p-3": "Read more in <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">the announcement</a>.", + "hero-overlay-card-3-p-2": "Trail of Bits reviewed SimpleX network cryptography and networking components in November 2022. <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">Read more</a>.", + "hero-overlay-card-3-p-3": "Trail of Bits reviewed cryptographic design of SimpleX network protocols in July 2024. <a href=\"/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html\">Read more</a>.", "simplex-network-overlay-card-1-p-1": "<a href='https://en.wikipedia.org/wiki/Peer-to-peer'>P2P</a> messaging protocols and apps have various problems that make them less reliable than SimpleX, more complex to analyse, and vulnerable to several types of attack.", "simplex-network-overlay-card-1-li-1": "P2P networks rely on some variant of <a href='https://en.wikipedia.org/wiki/Distributed_hash_table'>DHT</a> to route messages. DHT designs have to balance delivery guarantee and latency. SimpleX has both better delivery guarantee and lower latency than P2P, because the message can be redundantly passed via several servers in parallel, using the servers chosen by the recipient. In P2P networks the message is passed through <em>O(log N)</em> nodes sequentially, using nodes chosen by the algorithm.", "simplex-network-overlay-card-1-li-2": "SimpleX design, unlike most P2P networks, has no global user identifiers of any kind, even temporary, and only uses temporary pairwise identifiers, providing better anonymity and metadata protection.", "simplex-network-overlay-card-1-li-3": "P2P does not solve <a href='https://en.wikipedia.org/wiki/Man-in-the-middle_attack'>MITM attack</a> 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 <a href='https://en.wikipedia.org/wiki/BitTorrent'>BitTorrent</a>). SimpleX is transport agnostic - it can work over standard web protocols, e.g. WebSockets.", + "simplex-network-overlay-card-1-li-4": "P2P implementations can be blocked by some Internet providers (like <a href='https://en.wikipedia.org/wiki/BitTorrent'>BitTorrent</a>). 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 <a href='https://en.wikipedia.org/wiki/Sybil_attack'>Sybil attack</a>, because each node is discoverable, and the network operates as a whole. Known measures to mitigate it require either a centralized component or expensive <a href='https://en.wikipedia.org/wiki/Proof_of_work'>proof of work</a>. 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 <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'>DRDoS attack</a>, 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.", "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 <a href='https://fairbydesign.com/povertypremium/' target='_blank'>'poverty premium'</a>.", - "privacy-matters-overlay-card-1-p-4": "SimpleX platform 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 provided by SimpleX Chat, we 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 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 href='https://en.wikipedia.org/wiki/Facebook–Cambridge_Analytica_data_scandal' target='_blank'>a reputable consulting company</a> 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 platform that does not have access to your social graph.", - "privacy-matters-overlay-card-2-p-3": "SimpleX is the first platform 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-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-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 <a href='https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi' target='_blank'>Mohamedou Ould Salahi</a> 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.", "privacy-matters-overlay-card-3-p-3": "Ordinary people get arrested for what they share online, even via their 'anonymous' accounts, <a href='https://www.dailymail.co.uk/news/article-11282263/Moment-police-swoop-house-devout-catholic-mother-malicious-online-posts.html' target='_blank'>even in democratic countries</a>.", "privacy-matters-overlay-card-3-p-4": "It is not enough to use an end-to-end encrypted messenger, we all should use the messengers that protect the privacy of our personal networks — who we are connected with.", - "simplex-unique-overlay-card-1-p-1": "Unlike other messaging platforms, SimpleX has <strong>no identifiers assigned to the users</strong>. It does not rely on phone numbers, domain-based addresses (like email or XMPP), usernames, public keys or even random numbers to identify its users — we don't know how many people use our SimpleX servers.", - "simplex-unique-overlay-card-1-p-2": "To deliver messages SimpleX uses <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>pairwise anonymous addresses</a> of unidirectional message queues, separate for received and sent messages, usually via different servers. Using SimpleX is like having <strong>a different “burner” email or phone for each contact</strong>, and no hassle to manage them.", - "simplex-unique-overlay-card-1-p-3": "This design protects the privacy of who you are communicating with, hiding it from SimpleX platform servers and from any observers. To hide your IP address from the servers, you can <strong>connect to SimpleX servers via Tor</strong>.", - "simplex-unique-overlay-card-2-p-1": "Because you have no identifier on the SimpleX platform, nobody can contact you unless you share a one-time or temporary user address, as a QR code or a link.", + "simplex-unique-overlay-card-1-p-1": "Unlike other messaging networks, SimpleX has <strong>no identifiers assigned to the users</strong>. It does not rely on phone numbers, domain-based addresses (like email or XMPP), usernames, public keys or even random numbers to identify its users — SimpleX server operators don't know how many people use their servers.", + "simplex-unique-overlay-card-1-p-2": "To deliver messages SimpleX uses <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>pairwise anonymous addresses</a> of unidirectional message queues, separate for received and sent messages, usually via different servers.", + "simplex-unique-overlay-card-1-p-3": "This design protects the privacy of who you are communicating with, hiding it from SimpleX network servers and from any observers. To hide your IP address from the servers, you can <strong>connect to SimpleX servers via Tor</strong>.", + "simplex-unique-overlay-card-2-p-1": "Because you have no identifier on the SimpleX network, nobody can contact you unless you share a one-time or temporary user address, as a QR code or a link.", "simplex-unique-overlay-card-2-p-2": "Even with the optional user address, while it can be used to send spam contact requests, you can change or completely delete it without losing any of your connections.", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat stores all user data only on client devices using a <strong>portable encrypted database format</strong> that can be exported and transferred to any supported device.", "simplex-unique-overlay-card-3-p-2": "The end-to-end encrypted messages are held temporarily on SimpleX relay servers until received, then they are permanently deleted.", "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 <strong>use SimpleX with your own servers</strong> and still communicate with people who use the pre-configured servers provided by us.", - "simplex-unique-overlay-card-4-p-2": "SimpleX platform uses an <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>open protocol</a> and provides <a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>SDK to create chat bots</a>, allowing implementation of services that users can interact with via SimpleX Chat apps — we're really looking forward to see what SimpleX services you can build.", - "simplex-unique-overlay-card-4-p-3": "If you are considering developing for the SimpleX platform, for example, the chat bot for SimpleX app users, or the integration of the SimpleX Chat library into your mobile apps, please <a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>get in touch</a> 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 platform servers and any observers.", - "simplex-unique-card-1-p-2": "Unlike any other existing messaging platform, SimpleX has no identifiers assigned to the users — <strong>not even random numbers</strong>.", - "simplex-unique-card-2-p-1": "Because you have no identifier or fixed address on the SimpleX platform, nobody can contact you unless you share a one-time or temporary user address, as a QR code or a link.", + "simplex-unique-overlay-card-4-p-1": "You can <strong>use SimpleX with your own servers</strong> and still communicate with people who use the servers preconfigured in the apps.", + "simplex-unique-overlay-card-4-p-2": "SimpleX network uses an <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>open protocol</a> and provides <a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>SDK to create chat bots</a>, 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-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 <a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>get in touch</a> 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 — <strong>not even random numbers</strong>.", + "simplex-unique-card-2-p-1": "Because you have no identifier or fixed address on the SimpleX network, nobody can contact you unless you share a one-time or temporary user address, as a QR code or a link.", "simplex-unique-card-3-p-1": "SimpleX stores all user data on client devices in a <strong>portable encrypted database format</strong> — it can be transferred to another device.", "simplex-unique-card-3-p-2": "The end-to-end encrypted messages are held temporarily on SimpleX relay servers until received, then they are permanently deleted.", - "simplex-unique-card-4-p-1": "The SimpleX network is fully decentralised and independent of any crypto-currency or any other platform, other than the Internet.", + "simplex-unique-card-4-p-1": "The SimpleX network is fully decentralised and independent of any crypto-currency or any other network, other than the Internet.", "simplex-unique-card-4-p-2": "You can <strong>use SimpleX with your own servers</strong> or with the servers provided by us — and still connect to any user.", "join": "Join", "we-invite-you-to-join-the-conversation": "We invite you to join the conversation", @@ -149,8 +149,7 @@ "sign-up-to-receive-our-updates": "Sign up to receive our updates", "enter-your-email-address": "Enter your email address", "get-simplex": "Get SimpleX <a href=\"/downloads\">desktop app</a>", - "why-simplex-is": "Why SimpleX is", - "unique": "unique", + "why-simplex-is-unique": "Why SimpleX is <span class='gradient-text'>unique</span>", "learn-more": "Learn more", "more-info": "More info", "hide-info": "Hide info", @@ -172,7 +171,7 @@ "use-this-command": "Use this command:", "see-simplex-chat": "See SimpleX Chat", "github-repository": "GitHub repository", - "the-instructions--source-code": "the instructions how to download or compile it from the source code.", + "the-instructions--source-code": "for the instructions how to download or compile it from the source code.", "if-you-already-installed-simplex-chat-for-the-terminal": "If you already installed SimpleX Chat for the terminal", "if-you-already-installed": "If you already installed", "simplex-chat-for-the-terminal": "SimpleX Chat for the terminal", @@ -213,7 +212,7 @@ "comparison-section-list-point-4a": "SimpleX relays cannot compromise e2e encryption. Verify security code to mitigate attack on out-of-band channel", "comparison-section-list-point-4": "If operator’s servers are compromised. Verify security code in Signal and some other apps to mitigate it", "comparison-section-list-point-5": "Does not protect users' metadata privacy", - "comparison-section-list-point-6": "While P2P are distributed, they are not federated - they operate as a single network", + "comparison-section-list-point-6": "While P2P are distributed, they are not federated — they operate as a single network", "comparison-section-list-point-7": "P2P networks either have a central authority or the whole network can be compromised", "see-here": "see here", "guide-dropdown-1": "Quick start", @@ -226,7 +225,7 @@ "guide-dropdown-8": "App settings", "guide-dropdown-9": "Making connections", "guide": "Guide", - "docs-dropdown-1": "SimpleX platform", + "docs-dropdown-1": "SimpleX network", "docs-dropdown-2": "Accessing Android files", "docs-dropdown-3": "Accessing chat database", "docs-dropdown-4": "Host SMP Server", @@ -235,6 +234,10 @@ "docs-dropdown-7": "Translate SimpleX Chat", "docs-dropdown-8": "SimpleX Directory Service", "docs-dropdown-9": "Downloads", + "docs-dropdown-10": "Transparency", + "docs-dropdown-11": "FAQ", + "docs-dropdown-12": "Security", + "docs-dropdown-14": "SimpleX for business", "newer-version-of-eng-msg": "There is a newer version of this page in English.", "click-to-see": "Click to see", "menu": "Menu", @@ -248,7 +251,9 @@ "signing-key-fingerprint": "Signing key fingerprint (SHA-256)", "f-droid-org-repo": "F-Droid.org repo", "stable-versions-built-by-f-droid-org": "Stable versions built by F-Droid.org", - "releases-to-this-repo-are-done-1-2-days-later": "The releases to this repo are done 1-2 days later", + "releases-to-this-repo-are-done-1-2-days-later": "The releases to this repo are done several days later", "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat and F-Droid.org repositories sign builds with the different keys. To switch, please <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>export</a> the chat database and re-install the app.", - "jobs": "Join team" -} + "jobs": "Join team", + "please-enable-javascript": "Please enable JavaScript to see the QR code.", + "please-use-link-in-mobile-app": "Please use the link in the mobile app" +} \ No newline at end of file diff --git a/website/langs/es.json b/website/langs/es.json index 7c418b7e25..b3cfc83969 100644 --- a/website/langs/es.json +++ b/website/langs/es.json @@ -10,9 +10,9 @@ "simplex-explained-tab-3-p-2": "El usuario puede mejorar aún más la privacidad de sus metadatos haciendo uso de la red Tor para acceder a los servidores, evitando así la correlación por dirección IP.", "smp-protocol": "Protocolo SMP", "donate": "Donación", - "copyright-label": "© 2020-2023 SimpleX | Proyecto de Código Abierto", + "copyright-label": "© 2020-2025 SimpleX | Proyecto de Código Abierto", "simplex-chat-protocol": "Protocolo de SimpleX Chat", - "terms-and-privacy-policy": "Términos y Política de Privacidad", + "terms-and-privacy-policy": "Política de Privacidad", "hero-header": "Privacidad redefinida", "hero-overlay-1-textlink": "¿Por qué los ID de usuario son perjudiciales para la privacidad?", "hero-overlay-2-textlink": "¿Cómo funciona SimpleX?", @@ -20,7 +20,7 @@ "hero-overlay-1-title": "¿Cómo funciona SimpleX?", "hero-overlay-2-title": "¿Por qué los ID de usuario son perjudiciales para la privacidad?", "feature-1-title": "Mensajes cifrados E2E con sintáxis markdown y edición", - "feature-3-title": "Grupos secretos descentralizados —<br>sólo los usuarios saben de su existencia", + "feature-3-title": "Grupos descentralizados cifrados E2E — sólo los usuarios conocen su existencia", "feature-6-title": "Llamadas y videollamadas con cifrado E2E", "simplex-network-overlay-1-title": "Comparativa con protocolos de mensajería P2P", "developers": "Desarrolladores", @@ -29,8 +29,8 @@ "home": "Inicio", "simplex-explained-tab-3-p-1": "Para cada cola los servidores disponen de credenciales separadas y anónimas, por lo que desconocen a qué usuarios pertenecen.", "hero-p-1": "Las demás aplicaciones usan ID de usuario: Signal, Matrix, Session, Briar, Jami, Cwtch, etc.<br> SimpleX no los tiene, <strong>ni siquiera números aleatorios</strong>.<br> Esto mejora radicalmente su privacidad.", - "hero-2-header-desc": "El video muestra cómo se conecta con sus amistades a través del código QR de un solo uso, en persona o a través de videollamada. También puede conectarse compartiendo un enlace de invitación.", - "feature-7-title": "Base de datos portable cifrada — transfiera su perfil a otro dispositivo", + "hero-2-header-desc": "El vídeo muestra cómo se conecta con sus amistades a través del código QR de un solo uso, en persona o a través de videollamada. También puede conectarse compartiendo un enlace de invitación.", + "feature-7-title": "Almacenamiento portable y cifrado — podrá transferir su perfil a otro dispositivo", "simplex-private-card-4-point-2": "Para usar SimpleX a través de Tor, instala la aplicación <a href=\"https://guardianproject.info/apps/org.torproject.android/\" target=\"_blank\">Orbot</a> y activa el proxy SOCKS5 (o VPN <a href=\"https://apps.apple.com/us/app/orbot/id1609461599?platform=iphone\" target=\"_blank\">en iOS</a>).", "simplex-private-card-3-point-1": "Para las conexiones cliente servidor se usan exclusivamente el protocolo TLS 1.2/1.3 con algoritmos robustos.", "simplex-private-card-4-point-1": "Para proteger tu dirección IP puedes acceder a los servidores a través de la red Tor u otras redes de transporte superpuesto.", @@ -40,7 +40,7 @@ "simplex-private-card-7-point-2": "Si se añade, elimina o modifica algún mensaje, el destinatario es avisado.", "simplex-private-card-8-point-1": "Los servidores SimpleX actúan como nodos de mezcla de baja latencia — los mensajes entrantes y salientes siguen un orden diferente.", "simplex-private-card-9-point-1": "Cada cola de mensajes transmite los mensajes en un solo sentido, con las distintas direcciones de envío y recepción.", - "simplex-explained": "Simplex explicado", + "simplex-explained": "SimpleX explicado", "simplex-explained-tab-1-p-1": "Puedes crear contactos y grupos, y mantener conversaciones bidireccionales como en cualquier aplicación de mensajería.", "simplex-explained-tab-1-p-2": "¿Cómo puede funcionar con colas unidireccionales y sin identificadores de usuario?", "simplex-explained-tab-2-p-1": "Por cada conexión se usan dos colas de mensajes separadas para que el envío y recepción se hagan a través de servidores diferentes.", @@ -48,11 +48,11 @@ "chat-protocol": "Protocolo Chat", "terminal-cli": "Terminal CLI", "hero-subheader": "La primera aplicación de mensajería<br>sin ID de usuario", - "feature-2-title": "Cifrado E2E<br>de imágenes y archivos", + "feature-2-title": "Cifrado E2E<br>de imágenes, vídeos y archivos", "feature-8-title": "Modo incógnito —<br>exclusivo de SimpleX Chat", "simplex-private-1-title": "Doble capa de<br>cifrado de extremo a extremo", "simplex-private-2-title": "Capa de cifrado<br>adicional en el servidor", - "simplex-private-3-title": "Transporte TLS<br>seguro y auténticado", + "simplex-private-3-title": "Transporte TLS<br>seguro y autenticado", "simplex-private-4-title": "Acceso opcional<br>a través de Tor", "simplex-private-7-title": "Verificación de la<br>integridad del mensaje", "feature-4-title": "Mensajes de voz cifrados E2E", @@ -122,7 +122,7 @@ "privacy-matters-overlay-card-3-p-2": "Una de las historias más estremecedoras es la experiencia de <a href='https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi' target='_blank'>Mohamedou Ould Salahi</a> descrita en sus memorias y plasmada en la película The Mauritanian. Fue internado en la prisión militar de Guantánamo sin juicio previo, donde fue torturado durante 15 años tras una llamada telefónica a su pariente en Afganistán, bajo sospecha de estar implicado en los atentados del 11-S a pesar de que vivía en Alemania desde hacía 10 años.", "privacy-matters-overlay-card-3-p-4": "No basta con usar mensajería cifrada de extremo a extremo, todos deberíamos utilizar las aplicaciones de mensajería que protegen la privacidad de nuestras redes personales y con quién estamos conectados.", "privacy-matters-overlay-card-3-p-3": "Personas corrientes son detenidas por lo que comparten en Internet, incluso a través de sus cuentas \"anónimas\" e <a href='https://www.dailymail.co.uk/news/article-11282263/Moment-police-swoop-house-devout-catholic-mother-malicious-online-posts.html' target='_blank'>incluso en países democráticos</a>.", - "simplex-unique-overlay-card-1-p-2": "Para enviar mensajes, SimpleX utiliza <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>direcciones anónimas por pares</a> de colas de mensajes unidireccionales, separadas para mensajes recibidos y enviados, y normalmente a través de servidores diferentes. Utilizar SimpleX es como tener <strong>un correo electrónico “desechable” o un teléfono diferente para cada contacto</strong>, pero sin las complicaciones que implicaría gestionarlos.", + "simplex-unique-overlay-card-1-p-2": "Para enviar mensajes, SimpleX utiliza <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>direcciones anónimas por pares</a> de colas de mensajes unidireccionales, separadas para mensajes recibidos y enviados, y normalmente a través de servidores diferentes.", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat almacena todos los datos de usuario únicamente en los dispositivos cliente usando un <strong>formato cifrado y portable de la base de datos</strong>, la cual puede ser exportada y transferida a cualquier dispositivo compatible.", "simplex-unique-overlay-card-3-p-3": "A diferencia de los servidores de redes federadas (correo electrónico, XMPP o Matrix), los servidores SimpleX no almacenan cuentas de usuario, sólo retransmiten mensajes, protegiendo así la privacidad de ambas partes.", "simplex-unique-overlay-card-4-p-1": "Puede <strong>usar SimpleX con sus propios servidores</strong> y aún así comunicarte con personas conectadas a los servidores preconfigurados y proporcionados por nosotros.", @@ -138,8 +138,7 @@ "donate-here-to-help-us": "Para ayudarnos haga una donación aquí", "enter-your-email-address": "Escriba su dirección de correo electrónico", "get-simplex": "Obtenga SimpleX <a href=\"/downloads\">desktop app</a>", - "why-simplex-is": "Por qué SimpleX es", - "unique": "único", + "why-simplex-is-unique": "Por qué SimpleX es <span class='gradient-text'>único</span>", "learn-more": "Descubra más", "more-info": "Más información", "hide-info": "Ocultar información", @@ -242,13 +241,19 @@ "stable-versions-built-by-f-droid-org": "Versión estable compilada por F-Droid.org", "f-droid-page-f-droid-org-repo-section-text": "Los repositorios de SimpleX Chat y F-Droid.org firman con distinto certificado. Para cambiar, por favor <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>exportar</a> la base de datos y reinstala la aplicación.", "signing-key-fingerprint": "Huella digital de la clave de firma (SHA-256)", - "releases-to-this-repo-are-done-1-2-days-later": "Las versiones aparecen 1-2 días más tarde en este repositorio", + "releases-to-this-repo-are-done-1-2-days-later": "Las versiones aparecen varios días más tarde en este repositorio", "comparison-section-list-point-4a": "Los servidores de retransmisión no pueden comprometer la encriptación e2e. Para evitar posibles ataques, verifique el código de seguridad mediante un canal alternativo", - "hero-overlay-3-title": "Evaluación de la seguridad", - "hero-overlay-card-3-p-2": "Trail of Bits revisó la criptografía y los componentes de red de la plataforma SimpleX en noviembre de 2022.", - "hero-overlay-card-3-p-3": "Más información en <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">la noticia</a>.", + "hero-overlay-3-title": "Evaluaciones de seguridad", + "hero-overlay-card-3-p-2": "Trail of Bits revisó la criptografía y los componentes de red de la plataforma SimpleX en noviembre de 2022. <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">Más información</a>.", "jobs": "Únete al equipo", - "hero-overlay-3-textlink": "Evaluación de la seguridad", + "hero-overlay-3-textlink": "Evaluaciones de seguridad", "hero-overlay-card-3-p-1": "<a href=\"https://www.trailofbits.com/about/\">Trail of Bits</a> es una consultora de seguridad y tecnología líder cuyos clientes incluyen grandes tecnológicas, agencias gubernamentales e importantes proyectos de blockchain.", - "docs-dropdown-9": "Descargas" + "docs-dropdown-9": "Descargas", + "please-enable-javascript": "Habilita JavaScript para ver el código QR.", + "please-use-link-in-mobile-app": "Usa el enlace en la apliación móvil", + "docs-dropdown-10": "Transparencia", + "docs-dropdown-11": "FAQ", + "docs-dropdown-12": "Seguridad", + "docs-dropdown-14": "SimpleX para empresas", + "hero-overlay-card-3-p-3": "Trail of Bits revisó el diseño criptográfico de los protocolos de red SimpleX en julio de 2024." } diff --git a/website/langs/fi.json b/website/langs/fi.json index 963ed42c87..460aa7989b 100644 --- a/website/langs/fi.json +++ b/website/langs/fi.json @@ -27,8 +27,8 @@ "hero-overlay-1-title": "Kuinka SimpleX toimii?", "hero-overlay-2-title": "Miksi käyttäjätunnukset ovat huonoja yksityisyydelle?", "feature-1-title": "Päästä päähän salattuja viestejä markdownin ja muokkaamisen kera", - "feature-2-title": "Päästä päähän salattuja<br>kuvia ja tiedostoja", - "feature-3-title": "Hajautetut salaiset ryhmät —<br>vain käyttäjät tietävät niiden olemassaolosta", + "feature-2-title": "Päästä päähän salattuja<br>kuvia, videoita ja tiedostoja", + "feature-3-title": "E2E-salatut hajautetut ryhmät — vain käyttäjät tietävät niiden olemassaolosta", "feature-4-title": "Päästä päähän salattuja ääniviestejä", "feature-5-title": "Katoavia viestejä", "feature-8-title": "Incognito-tila —<br>ainutlaatuinen SimpleX Chatille", @@ -108,11 +108,11 @@ "hero-2-header": "Luo yksityinen yhteys", "hero-2-header-desc": "Video näyttää, kuinka muodostat yhteyden ystävääsi heidän kertakäyttöiseen QR-koodiinsa, henkilökohtaisesti tai videolinkin kautta. Voit myös liittyä jakamalla kutsulinkin kautta.", "feature-6-title": "Päästä päähän salattuja<br>puheluita ja videopuheluja", - "feature-7-title": "Siirrettävä salattu tietokanta — siirrä profiilisi toiselle laitteelle", + "feature-7-title": "Kannettava salattu sovellustallennus — siirrä profiili toiseen laitteeseen", "simplex-explained-tab-1-p-1": "Voit luoda yhteyshenkilöitä ja ryhmiä sekä käydä kaksisuuntaisia keskusteluja kuten missä tahansa muussa viestisovelluksessa.", "simplex-explained-tab-3-p-1": "Palvelimilla on erilliset anonyymit tunnistetiedot kullekin jonolle, eivätkä ne tiedä, mille käyttäjille ne kuuluvat.", "donate": "Lahjoita", - "copyright-label": "© 2020-2023 SimpleX | Avoin projekti", + "copyright-label": "© 2020-2025 SimpleX | Avoin projekti", "hero-p-1": "Muissa sovelluksissa on käyttäjätunnuksia: Signal, Matrix, Session, Briar, Jami, Cwtch, jne.<br> SimpleX ei käytä niitä, <strong>ei edes satunnaisia numeroita</strong>.<br> Tämä parantaa yksityisyyttäsi radikaalisti.", "simplex-private-1-title": "2 kerrosta päästä päähän salattua viestintää", "simplex-private-2-title": "Lisäkerros palvelimen salaukselle", @@ -168,7 +168,7 @@ "privacy-matters-overlay-card-3-p-3": "Tavalliset ihmiset pidätetään siitä, mitä he jakavat verkossa, jopa 'anonyymien' tiliensä kautta, <a href='https://www.dailymail.co.uk/news/article-11282263/Moment-police-swoop-house-devout-catholic-mother-malicious-online-posts.html' target='_blank'>jopa demokraattisissa maissa</a>.", "privacy-matters-overlay-card-3-p-4": "Ei riitä, että käytät päästä päähän salattua viestintäsovellusta, meidän kaikkien pitäisi käyttää viestintäsovelluksia, jotka suojelevat henkilökohtaisten verkostojemme yksityisyyttä — keiden kanssa olemme yhteydessä.", "simplex-unique-overlay-card-1-p-1": "Toisin kuin muut viestintäalustat, SimpleX:llä ei ole <strong>mitään tunnisteita käyttäjille</strong>. Se ei luota puhelinnumeroihin, verkkotunnuksiin perustuviin osoitteisiin (kuten sähköposti tai XMPP), käyttäjänimiin, julkisiin avaimiin tai edes satunnaisiin numeroihin tunnistaakseen käyttäjänsä — emme tiedä kuinka monta ihmistä käyttää SimpleX-palvelimiamme.", - "simplex-unique-overlay-card-1-p-2": "Viestien toimittamiseksi SimpleX käyttää <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>parittaisia nimettömiä osoitteita</a> kaksisuuntaisille viestijonoille, jotka ovat erilliset vastaanotetuille ja lähetetyille viesteille, yleensä eri palvelimien kautta. SimpleX:n käyttö on kuin <strong>eri “kertakäyttöinen” sähköposti tai puhelin jokaiselle yhteydelle</strong>, eikä sinun tarvitse vaivautua niiden hallitsemiseen.", + "simplex-unique-overlay-card-1-p-2": "Viestien toimittamiseksi SimpleX käyttää <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>parittaisia nimettömiä osoitteita</a> kaksisuuntaisille viestijonoille, jotka ovat erilliset vastaanotetuille ja lähetetyille viesteille, yleensä eri palvelimien kautta.", "simplex-unique-overlay-card-1-p-3": "Tämä suunnittelu suojaa sitä, kenen kanssa kommunikoit, piilottamalla sen SimpleX-alustan palvelimilta ja kaikilta havainnoijilta. Piilottaaksesi IP-osoitteesi palvelimilta, voit <strong>yhdistää SimpleX-palvelimiin Tor-verkon kautta</strong>.", "simplex-unique-overlay-card-2-p-1": "Koska sinulla ei ole tunnistetta SimpleX-alustalla, kukaan ei voi ottaa sinuun yhteyttä, ellei jaa kertakäyttöistä tai väliaikaista käyttäjäosoitetta, kuten QR-koodia tai linkkiä.", "simplex-unique-overlay-card-2-p-2": "Jopa valinnaisen käyttäjäosoitteen kanssa, vaikka sitä voitaisiin käyttää roskapostiyhteyspyyntöjen lähettämiseen, voit vaihtaa sen tai poistaa sen kokonaan menettämättä mitään yhteyksiäsi.", @@ -187,8 +187,7 @@ "sign-up-to-receive-our-updates": "Tilaa päivityksemme", "enter-your-email-address": "Syötä sähköpostiosoitteesi", "get-simplex": "Hanki SimpleX <a href=\"/downloads\">desktop app</a>", - "why-simplex-is": "Miksi SimpleX on", - "unique": "ainutlaatuinen", + "why-simplex-is-unique": "Miksi SimpleX on <span class='gradient-text'>ainutlaatuinen</span>", "learn-more": "Lue lisää", "more-info": "Lisätietoja", "contact-hero-subheader": "Skannaa QR-koodi SimpleX Chat -sovelluksella puhelimessasi tai tabletissasi.", @@ -243,5 +242,13 @@ "menu": "Valikko", "simplex-chat-via-f-droid": "SimpleX Chat F-Droidin kautta", "stable-and-beta-versions-built-by-developers": "Kehittäjien luomat vakaat ja beta-versiot", - "f-droid-page-simplex-chat-repo-section-text": "Lisätäksesi sen F-Droid-asiakkaaseesi, <span class='hide-on-mobile'>skannaa QR-koodi tai</span> käytä tätä URL-osoitetta:" + "f-droid-page-simplex-chat-repo-section-text": "Lisätäksesi sen F-Droid-asiakkaaseesi, <span class='hide-on-mobile'>skannaa QR-koodi tai</span> käytä tätä URL-osoitetta:", + "jobs": "Liity tiimiin", + "docs-dropdown-9": "Lataukset", + "hero-overlay-3-title": "Turvallisuuden arviointi", + "hero-overlay-card-3-p-1": "<a href=\"https://www.trailofbits.com/about/\">Trail of Bits</a> on johtava turvallisuus- ja teknologiakonsultointiyritys, jonka asiakkaita ovat muun muassa suuret teknologiayritykset, valtion virastot ja suuret lohkoketjuprojektit.", + "hero-overlay-3-textlink": "Turvallisuusarviointi", + "hero-overlay-card-3-p-2": "Trail of Bits tarkasteli SimpleX-alustan salaus- ja verkkokomponentteja marraskuussa 2022. Lue lisää <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">ilmoituksesta</a>.", + "please-enable-javascript": "Ota JavaScript käyttöön nähdäksesi QR-koodin.", + "please-use-link-in-mobile-app": "Käytä mobiilisovelluksessa olevaa linkkiä" } diff --git a/website/langs/fr.json b/website/langs/fr.json index 304630a091..efc8d6c9c6 100644 --- a/website/langs/fr.json +++ b/website/langs/fr.json @@ -7,7 +7,7 @@ "why-simplex": "Pourquoi SimpleX", "simplex-privacy": "Confidentialité SimpleX", "simplex-network": "Réseau SimpleX", - "simplex-explained": "Simplex expliqué", + "simplex-explained": "SimpleX expliqué", "simplex-explained-tab-1-text": "1. L'expérience des utilisateurs", "simplex-explained-tab-2-text": "2. Comment ça marche", "simplex-explained-tab-3-text": "3. Ce que voient les serveurs", @@ -21,10 +21,10 @@ "smp-protocol": "Protocole SMP", "chat-protocol": "Protocole de chat", "donate": "Faire un don", - "copyright-label": "© 2020-2023 SimpleX | Projet Open-Source", + "copyright-label": "© 2020-2025 SimpleX | Projet Open-Source", "simplex-chat-protocol": "Protocole SimpleX Chat", "terminal-cli": "Terminal CLI", - "terms-and-privacy-policy": "Conditions et politique de confidentialité", + "terms-and-privacy-policy": "Politique de confidentialité", "hero-header": "La vie privée redéfinie", "hero-subheader": "La première messagerie<br>sans identifiant d'utilisateur", "hero-p-1": "Les autres applications ont des IDs d'utilisateur : Signal, Matrix, Session, Briar, Jami, Cwtch, etc.<br> SimpleX n'en a pas, <strong>pas même des nombres aléatoires</strong>.<br > Ce qui améliore radicalement votre vie privée.", @@ -105,7 +105,7 @@ "simplex-network-overlay-card-1-li-3": "Le P2P ne résout pas <a href='https://fr.wikipedia.org/wiki/Attaque_de_l%27homme_du_milieu'>l'attaque MITM</a> et la plupart des implémentations existantes n'utilisent pas de messages hors bande pour l'échange de clé initial. SimpleX utilise des messages hors bande ou, dans certains cas, des connexions sécurisées et approuvées préexistantes pour l'échange de clé initial .", "simplex-network-overlay-card-1-li-4": "Les réseaux P2P peuvent être bloquées par certains fournisseurs Internet (comme <a href='https://fr.wikipedia.org/wiki/BitTorrent'>BitTorrent</a>). SimpleX est indépendant du transport - il peut fonctionner sur des protocoles Web standard, par exemple WebSockets.", "simplex-network-overlay-card-1-li-5": "Tous les réseaux P2P connus sont susceptibles d'être vulnérables à une <a href='https://fr.wikipedia.org/wiki/Attaque_Sybil'>attaque Sybil</a>, car chaque nœud peut être découvert et le réseau fonctionne comme un tout. Les mesures connues pour réduire la probabilité d'une attaque Sybil nécessitent soit un composant centralisé, soit des <a href='https://fr.wikipedia.org/wiki/Preuve_de_travail'>preuves de travail</a> coûteuses. Le réseau SimpleX ne permet pas de découvrir les serveurs, il est fragmenté et fonctionne comme de multiples sous-réseaux isolées, ce qui rend impossible les attaques à l'échelle du réseau.", - "simplex-network-overlay-card-1-li-6": "Les réseaux P2P sont susceptibles d'être vulnérables aux <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p- file-sharing-hell-exploiting-bittorrent'>attaques DRDoS</a>, lorsque les clients peuvent rediffuser et amplifier le trafic, entraînant un déni de service à l'échelle du réseau. Les clients SimpleX relaient uniquement le trafic à partir d'une connexion connue et ne peuvent pas être utilisés par un attaquant pour amplifier le trafic sur l'ensemble du réseau.", + "simplex-network-overlay-card-1-li-6": "Les réseaux P2P sont susceptibles d'être vulnérables aux <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'>attaques DRDoS</a>, lorsque les clients peuvent rediffuser et amplifier le trafic, entraînant un déni de service à l'échelle du réseau. Les clients SimpleX relaient uniquement le trafic à partir d'une connexion connue et ne peuvent pas être utilisés par un attaquant pour amplifier le trafic sur l'ensemble du réseau.", "privacy-matters-overlay-card-1-p-1": "De nombreuses grandes entreprises utilisent les informations sur les personnes avec lesquelles vous êtes connecté pour estimer vos revenus, vous vendre des produits dont vous n'avez pas vraiment besoin et déterminer les prix.", "privacy-matters-overlay-card-1-p-2": "Les vendeurs en ligne savent que les personnes à faible revenu sont plus susceptibles d'effectuer des achats urgents. Ils peuvent donc pratiquer des prix plus élevés ou supprimer des remises.", "privacy-matters-overlay-card-1-p-3": "Certaines sociétés financières et d'assurance utilisent des graphiques sociaux pour déterminer les taux d'intérêt et les primes. Cela fait souvent payer plus les personnes à faible revenu - c'est connu sous le nom de <a href ='https://fairbydesign.com/povertypremium/' target='_blank'>'prime à la pauvreté'</a>.", @@ -118,7 +118,7 @@ "privacy-matters-overlay-card-3-p-3": "Des personnes lambda se font arrêter pour ce qu'elles partagent en ligne, même via leurs comptes \"anonymes\", <a href='https://www.dailymail.co.uk/news/article-11282263/Moment-police-swoop-house-devout-catholic-mother-malicious-online-posts.html' target='_blank'>même dans les pays démocratiques</a>.", "privacy-matters-overlay-card-3-p-4": "Il ne suffit pas d'utiliser une messagerie chiffrée de bout en bout, nous devrions tous utiliser des messageries qui protègent la vie privée de nos réseaux personnels — les personnes avec lesquelles nous sommes connectés.", "simplex-unique-overlay-card-1-p-1": "Contrairement aux autres plateformes de messagerie, SimpleX <strong>n'a pas d'identifiant attribué aux utilisateurs</strong>. Il ne s'appuie pas sur des numéros de téléphone, des adresses basées sur des domaines (comme l'e-mail ou XMPP), des noms d'utilisateur, des clés publiques ou même des nombres aléatoires pour identifier ses utilisateurs — nous ne savons pas combien de personnes utilisent nos serveurs SimpleX.", - "simplex-unique-overlay-card-1-p-2": "Pour envoyer des messages, SimpleX utilise des <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>adresses anonymes par paires</a > de files d'attente de messages unidirectionnelles, séparées pour les messages reçus et envoyés, généralement via des serveurs différents. Utiliser SimpleX, c'est comme avoir <strong>un e-mail ou un téléphone jetable différent pour chaque contact</strong>, sans aucune difficulté pour les gérer.", + "simplex-unique-overlay-card-1-p-2": "Pour envoyer des messages, SimpleX utilise des <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>adresses anonymes par paires</a > de files d'attente de messages unidirectionnelles, séparées pour les messages reçus et envoyés, généralement via des serveurs différents.", "simplex-unique-overlay-card-1-p-3": "Cette approche protège la confidentialité des personnes avec lesquelles vous communiquez, en la cachant des serveurs de la plateforme SimpleX et de tout observateur. Pour cacher votre adresse IP aux serveurs, vous pouvez <strong>vous connecter aux serveurs SimpleX via Tor</strong>.", "simplex-unique-overlay-card-2-p-1": "Parce que vous n'avez pas d'identifiant sur la plateforme SimpleX, personne ne peut vous contacter sauf si vous partagez une adresse d'utilisateur unique ou temporaire, sous forme de code QR ou de lien .", "simplex-unique-overlay-card-2-p-2": "Même avec l'adresse utilisateur facultative, bien qu'elle puisse être utilisée pour envoyer des demandes de contact de spam, vous pouvez la modifier ou la supprimer complètement sans perdre aucune de vos connexions.", @@ -144,8 +144,7 @@ "sign-up-to-receive-our-updates": "Inscrivez-vous pour recevoir nos mises à jour", "enter-your-email-address": "Entrez votre adresse e-mail", "get-simplex": "Obtenir SimpleX <a href=\"/downloads\">desktop app</a>", - "why-simplex-is": "Pourquoi SimpleX est", - "unique": "unique", + "why-simplex-is-unique": "Pourquoi SimpleX est <span class='gradient-text'>unique</span>", "learn-more": "En savoir plus", "more-info": "Plus d'infos", "hide-info": "Masquer les infos", @@ -234,7 +233,7 @@ "docs-dropdown-4": "Hébergement d'un serveur SMP", "on-this-page": "Sur cette page", "glossary": "Glossaire", - "releases-to-this-repo-are-done-1-2-days-later": "Les mises à jour de ce dépôt sont faites 1 à 2 jours plus tard", + "releases-to-this-repo-are-done-1-2-days-later": "Les mises à jour de ce dépôt sont faites quelques jours plus tard", "f-droid-page-f-droid-org-repo-section-text": "Les dépôts SimpleX Chat et F-Droid.org signent les builds avec des clés différentes. Pour changer, veuillez <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>exporter</a> la base de données des chats et réinstaller l'application.", "docs-dropdown-8": "Service de répertoire SimpleX", "simplex-chat-via-f-droid": "SimpleX Chat via F-Droid", @@ -245,11 +244,17 @@ "f-droid-org-repo": "Dépot F-Droid.org", "stable-versions-built-by-f-droid-org": "Versions stables créées par F-Droid.org", "comparison-section-list-point-4a": "Les relais SimpleX ne peuvent pas compromettre le chiffrement e2e. Vérifier le code de sécurité pour limiter les attaques sur le canal hors bande", - "hero-overlay-3-title": "Évaluation de sécurité", - "hero-overlay-card-3-p-2": "Trail of Bits a examiné les composants cryptographiques et réseau de la plateforme SimpleX en novembre 2022.", - "hero-overlay-card-3-p-3": "En savoir plus <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">sur l'annonce</a>.", + "hero-overlay-3-title": "Évaluation de la sécurité", + "hero-overlay-card-3-p-2": "Trail of Bits a examiné les composants cryptographiques et réseau de la plateforme SimpleX en novembre 2022. <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">En savoir plus</a>.", "jobs": "Rejoignez notre équipe", - "hero-overlay-3-textlink": "Évaluation de sécurité", + "hero-overlay-3-textlink": "Évaluations de la sécurité", "hero-overlay-card-3-p-1": "<a href=\"https://www.trailofbits.com/about/\">Trail of Bits</a> est un cabinet leader dans le secteur de la sécurité et des technologies qui compte parmi ses clients des grandes entreprises de la tech, des agences gouvernementales et d'importants projets de blockchain.", - "docs-dropdown-9": "Téléchargements" + "docs-dropdown-9": "Téléchargements", + "please-enable-javascript": "Veuillez activer JavaScript pour voir le code QR.", + "please-use-link-in-mobile-app": "Veuillez utiliser le lien dans l'application mobile", + "docs-dropdown-10": "Transparence", + "docs-dropdown-12": "Sécurité", + "docs-dropdown-11": "FAQ", + "hero-overlay-card-3-p-3": "Trail of Bits a examiné la conception cryptographique des protocoles réseau SimpleX en juillet 2024.", + "docs-dropdown-14": "SimpleX pour les entreprises" } diff --git a/website/langs/he.json b/website/langs/he.json index f4e776c67f..4a61999896 100644 --- a/website/langs/he.json +++ b/website/langs/he.json @@ -12,7 +12,7 @@ "simplex-explained-tab-2-text": "2. איך זה עובד", "simplex-chat-protocol": "פרוטוקול SimpleX Chat", "terminal-cli": "ממשק שורת פקודה", - "terms-and-privacy-policy": "תנאים ומדיניות פרטיות", + "terms-and-privacy-policy": "מדיניות הפרטיות", "hero-header": "פרטיות מוגדרת מחדש", "hero-subheader": "מערכת העברת ההודעות הראשונה<br>ללא מזהי שתמש", "hero-overlay-1-textlink": "מדוע מזהי משתמש מזיקים לפרטיות?", @@ -21,7 +21,7 @@ "hero-2-header-desc": "הסרטון מראה כיצד אתם יוצרים קשר עם חברכם באמצעות קוד QR חד פעמי, באופן אישי או באמצעות קישור וידאו. באפשרותכם גם להתחבר על-ידי שיתוף קישור ההזמנה.", "hero-overlay-1-title": "איך SimpleX עובד?", "feature-1-title": "הודעות מוצפנות מקצה לקצה עם סימונים ואפשרויות עריכה", - "feature-2-title": "תמונות וקבצים<br>מוצפנים מקצה לקצה", + "feature-2-title": "תמונות, וידאו וקבצים<br>בהצפנת קצה-אל-קצה", "feature-3-title": "קבוצות סודיות מבוזרות —<br>רק המשתמשים יודעים שהן קיימות", "simplex-private-3-title": "תעבורת TLS<br>מאובטחת ומאומתת", "simplex-private-card-1-point-2": "תיבת הצפנה NaCL בכל תור כדי למנוע קורלציית תעבורה בין תורי הודעות במקרה שאבטחת TLS נפגעה.", @@ -53,13 +53,13 @@ "smp-protocol": "פרוטוקול SMP", "chat-protocol": "פרוטוקול צ'אט", "donate": "תרומה", - "copyright-label": "© 2020-2023 SimpleX | פרויקט קוד פתוח", + "copyright-label": "© 2020-2025 SimpleX | פרויקט קוד פתוח", "hero-p-1": "לאפליקציות אחרות יש מזהי משתמש: Signal, Matrix, Session, Briar, Jami, Cwtch וכו'.<br> ל-SimpleX אין, <strong>אפילו לא מספרים אקראיים</strong>.<br> זה משפר באופן קיצוני את הפרטיות שלך.", "hero-overlay-2-title": "מדוע מזהי משתמש מזיקים לפרטיות?", "feature-6-title": "שיחות שמע ווידאו<br>מוצפנות מקצה לקצה", "feature-4-title": "הודעות קוליות מוצפנות מקצה לקצה", "feature-5-title": "הודעות נעלמות", - "feature-7-title": "מסד נתונים מוצפן נייד — העברת הפרופיל שלכם למכשיר אחר", + "feature-7-title": "מסד נתונים מוצפן נייד — העבר פרופיל למכשיר אחר", "feature-8-title": "מצב זהות נסתרת —<br>ייחודי ל-SimpleX Chat", "simplex-private-4-title": "אופציונלי<br>גישה דרך Tor", "simplex-network-overlay-1-title": "השוואה לפרוטוקולי העברת הודעות P2P", @@ -75,24 +75,23 @@ "privacy-matters-2-overlay-1-linkText": "פרטיות מעניקה לכם עוצמה", "privacy-matters-3-overlay-1-linkText": "פרטיות מגנה על החופש שלכם", "simplex-explained-tab-1-p-1": "אתם יכולים ליצור אנשי קשר וקבוצות, ולנהל שיחות דו-כיווניות, כמו בכל תוכנה אחרת לשליחת הודעות.", - "hero-overlay-3-title": "הערכת אבטחה", + "hero-overlay-3-title": "הערכות אבטחה", "simplex-unique-1-overlay-1-title": "פרטיות מלאה של הזהות, הפרופיל, אנשי הקשר והמטא נתונים שלך", "simplex-unique-3-overlay-1-title": "בעלות, שליטה ואבטחה של הנתונים שלך", "simplex-unique-2-overlay-1-title": "ההגנה הטובה ביותר מפני ספאם וניצול לרעה", "simplex-unique-3-title": "אתה שולט בנתונים שלך", - "hero-overlay-3-textlink": "הערכת אבטחה", + "hero-overlay-3-textlink": "הערכות אבטחה", "simplex-unique-4-overlay-1-title": "מבוזר לחלוטין — המשתמשים הם הבעלים של רשת SimpleX", "simplex-unique-2-title": "אתה מוגן<br>מפני ספאם וניצול לרעה", "simplex-unique-4-title": "רשת SimpleX בבעלותך", "simplex-unique-1-title": "יש לך פרטיות מלאה", "simplex-private-card-10-point-1": "SimpleX משתמש בכתובות ואישורים אנונימיים זמניים בזוגות עבור כל איש קשר של משתמש או חבר בקבוצה.", "privacy-matters-overlay-card-1-p-1": "הרבה חברות גדולות משתמשות במידע עם מי אתה בקשר כדי להעריך את ההכנסה שלך, למכור לך מוצרים שאתה לא באמת צריך ולקבוע את המחירים.", - "hero-overlay-card-3-p-2": "Trail of Bits סקרה את רכיבי ההצפנה והרשת של פלטפורמת SimpleX בנובמבר 2022.", + "hero-overlay-card-3-p-2": "Trail of Bits סקרה את רכיבי ההצפנה והרשת של פלטפורמת SimpleX בנובמבר 2022. <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">קרא עוד ב הודעה</a>.", "hero-overlay-card-2-p-4": "SimpleX מגן מפני התקפות אלה בכך שאין בעיצובו מזהי משתמש. ואם אתה משתמש במצב זהות נסתרת, יהיה לך שם תצוגה שונה לכל איש קשר, תוך הימנעות מכל מידע משותף ביניהם.", "hero-overlay-card-2-p-1": "כאשר למשתמשים יש זהויות מתמשכות, גם אם זה רק מספר אקראי, כמו מזהה הפעלה, קיים סיכון שהספק או התוקף יוכלו לראות כיצד המשתמשים מחוברים וכמה הודעות הם שולחים.", "hero-overlay-card-1-p-5": "רק מכשירי לקוח מאחסנים פרופילי משתמשים, אנשי קשר וקבוצות; ההודעות נשלחות עם הצפנה דו-שכבתית מקצה לקצה.", "privacy-matters-overlay-card-1-p-2": "קמעונאים מקוונים יודעים שאנשים עם הכנסה נמוכה יותר נוטים יותר לבצע רכישות דחופות, ולכן הם עשויים לגבות מחירים גבוהים יותר או להסיר הנחות.", - "hero-overlay-card-3-p-3": "קרא עוד ב<a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">הודעה</a>.", "hero-overlay-card-1-p-2": "כדי להעביר הודעות, במקום מזהי משתמש המשמשים את כל הפלטפורמות האחרות, SimpleX משתמש במזהים אנונימיים זמניים זוגיים של תורי הודעות, נפרדים עבור כל אחד מהחיבורים שלך — אין מזהים לטווח ארוך.", "hero-overlay-card-2-p-2": "לאחר מכן הם יוכלו לקשר מידע זה עם הרשתות החברתיות הציבוריות הקיימות, ולקבוע כמה זהויות אמיתיות.", "privacy-matters-overlay-card-1-p-3": "חלק מחברות פיננסיות וביטוח משתמשות בגרפים חברתיים כדי לקבוע שיעורי ריבית ופרמיות. לעתים קרובות זה גורם לאנשים עם הכנסה נמוכה יותר לשלם יותר — זה ידוע בתור <a href='https://fairbydesign.com/povertypremium/' target='_blank'>'פרמיית עוני'</a>.", @@ -164,10 +163,9 @@ "simplex-unique-overlay-card-3-p-2": "ההודעות המוצפנות מקצה לקצה מוחזקות באופן זמני בשרתי ממסר של SimpleX עד שמתקבלות, ואז הן נמחקות לצמיתות.", "simplex-unique-overlay-card-4-p-2": "פלטפורמת SimpleX משתמשת ב<a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>פרוטוקול פתוח</a> ומספקת <a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>SDK ליצירת צ'אט בוטים</a>, המאפשר הטמעה של שירותים שמשתמשים יכולים לתקשר איתם באמצעות אפליקציות SimpleX Chat — אנחנו ממש מצפים לראות אילו שירותי SimpleX אתם יכולים לבנות.", "contact-hero-p-2": "עדיין לא הורדתם את ה-SimpleX Chat?", - "why-simplex-is": "מדוע SimpleX הוא", + "why-simplex-is-unique": "מדוע SimpleX הוא <span class='gradient-text'>ייחודי</span>", "simplex-network-section-header": "<span class='gradient-text'>רשת</span> SimpleX", "tap-the-connect-button-in-the-app": "הקישו על הלחצן <span class='text-active-blue'>'התחבר'</span> באפליקציה", - "unique": "ייחודי", "simplex-network-1-overlay-linktext": "בעיות של רשתות P2P", "protocol-2-text": "XMPP, Matrix", "simplex-network-overlay-card-1-li-4": "יישומי P2P יכולים להיחסם על ידי ספקי אינטרנט מסוימים (כמו <a href='https://en.wikipedia.org/wiki/BitTorrent'>BitTorrent</a>). SimpleX הוא אגנוסטי לתעבורה - הוא יכול לעבוד על פרוטוקולי אינטרנט סטנדרטיים, למשל WebSockets.", @@ -242,7 +240,7 @@ "simplex-chat-via-f-droid": "SimpleX Chat דרך F-Droid", "glossary": "מילון מונחים", "jobs": "הצטרפו לצוות", - "releases-to-this-repo-are-done-1-2-days-later": "גרסאות למאגר זה משוחררות לאחר יום או יומיים", + "releases-to-this-repo-are-done-1-2-days-later": "גרסאות למאגר זה משוחררות לאחר כמה ימים", "f-droid-org-repo": "מאגר F-Droid.org", "stable-versions-built-by-f-droid-org": "גרסאות יציבות שנבנו על ידי F-Droid.org", "back-to-top": "חזרה למעלה", @@ -250,5 +248,12 @@ "stable-and-beta-versions-built-by-developers": "גרסאות יציבות ובטא שנבנו על ידי המפתחים", "f-droid-page-simplex-chat-repo-section-text": "כדי להוסיף אותו ללקוח F-Droid שלכם, <span class='hide-on-mobile'>סרקו את קוד ה-QR או</span> השתמשו בכתובת האתר הזו:", "docs-dropdown-8": "שירות מדריך כתובות SimpleX", - "f-droid-page-f-droid-org-repo-section-text": "מאגרי SimpleX Chat ו-F-Droid.org חותמים על גרסאות עם מפתחות שונים. כדי לעבור, אנא <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>ייצא</a> את מסד הנתונים של הצ'אט והתקן מחדש את האפליקציה." + "f-droid-page-f-droid-org-repo-section-text": "מאגרי SimpleX Chat ו-F-Droid.org חותמים על גרסאות עם מפתחות שונים. כדי לעבור, אנא <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>ייצא</a> את מסד הנתונים של הצ'אט והתקן מחדש את האפליקציה.", + "please-enable-javascript": "אנא הפעל JavaScript כדי לראות את קוד ה-QR.", + "please-use-link-in-mobile-app": "אנא השתמש בקישור באפליקציה במכשיר נייד", + "docs-dropdown-10": "שקיפות", + "docs-dropdown-11": "שאלות ותשובות", + "docs-dropdown-12": "אבטחה", + "hero-overlay-card-3-p-3": "Trail of bits סקר את הקוד הקריפטוגרפי של פרוטוקולי רשת SimpleX ביולי 2024. <a href=\"/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html\">קרא עוד</a>.", + "docs-dropdown-14": "SimpleX לעסקים" } diff --git a/website/langs/hu.json b/website/langs/hu.json new file mode 100644 index 0000000000..9dfd3d9f65 --- /dev/null +++ b/website/langs/hu.json @@ -0,0 +1,259 @@ +{ + "home": "Kezdőoldal", + "developers": "Fejlesztők", + "reference": "Hivatkozás", + "blog": "Blog", + "features": "Funkciók", + "why-simplex": "Miért válassza a SimpleXet", + "simplex-privacy": "A SimpleX adatvédelme", + "simplex-network": "A SimpleX-hálózat", + "simplex-explained": "A SimpleX bemutatása", + "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": "Létrehozhat kapcsolatokat és csoportokat, valamint kétirányú beszélgetéseket folytathat, ugyanúgy mint bármely más üzenetváltó-alkalmazásban.", + "simplex-explained-tab-1-p-2": "Hogyan működhet egyirányú üzenet-sorballítással é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 sorbaállítást 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.", + "simplex-explained-tab-3-p-1": "A kiszolgálók minden egyes üzenetsorbaállításhoz külön névtelen hitelesítő-adatokkal rendelkeznek, és nem tudják, hogy melyik felhasználóhoz tartoznak.", + "simplex-explained-tab-3-p-2": "A felhasználók tovább fokozhatják a metaadatok adatvédelmét, ha a Tor segítségével férnek hozzá a kiszolgálókhoz, így megakadályozva az IP-cím szerinti korrelációt.", + "smp-protocol": "SMP-protokoll", + "chat-protocol": "Csevegési protokoll", + "donate": "Adományozás", + "copyright-label": "© 2020-2025 SimpleX | Nyílt forráskódú projekt", + "simplex-chat-protocol": "A 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<br>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.<br> A SimpleX azonban nem, <strong>még véletlenszerű számokkal sem</strong>.<br> 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?", + "hero-overlay-3-textlink": "Biztonsági felmérések", + "hero-2-header": "Privát kapcsolat létrehozása", + "hero-2-header-desc": "A videó bemutatja, hogyan kapcsolódhat a partneréhez egy egyszer használható QR-kód segítségével, személyesen vagy videokapcsolaton keresztül. Ugyanakkor egy meghívási hivatkozás megosztásával is kapcsolódhat.", + "hero-overlay-1-title": "Hogyan működik a SimpleX?", + "hero-overlay-2-title": "Miért ártanak a felhasználói azonosítók az adatvédelemnek?", + "hero-overlay-3-title": "Biztonsági felmérések", + "feature-1-title": "E2E-titkosított üzenetek markdown formázással és szerkesztéssel", + "feature-2-title": "E2E-titkosított<br>képek, videók és fájlok", + "feature-3-title": "E2E-titkosított decentralizált csoportok — csak a felhasználók tudják, hogy ezek léteznek", + "feature-4-title": "E2E-titkosított hangüzenetek", + "feature-5-title": "Eltűnő üzenetek", + "feature-6-title": "E2E-titkosított<br>hang- és videohívások", + "feature-7-title": "Hordozható titkosított alkalmazás-adattárolás — profil átköltöztetése egy másik eszközre", + "feature-8-title": "Az inkognitómód —<br>egyedülálló a SimpleX Chatben", + "simplex-network-overlay-1-title": "Összehasonlítás más P2P-üzenetküldő protokollokkal", + "simplex-private-1-title": "2 rétegű végpontok közötti titkosítás", + "simplex-private-2-title": "További rétege a<br>kiszolgáló-titkosítás", + "simplex-private-4-title": "Nem kötelező<br>hozzáférés Tor-on keresztül", + "simplex-private-5-title": "Több rétegű<br>tartalom kitöltés", + "simplex-private-6-title": "Sávon kívüli<br>kulcscsere", + "simplex-private-7-title": "Üzenetintegritás<br>hitelesítés", + "simplex-private-8-title": "Üzenetek keverése<br>a korreláció csökkentése érdekében", + "simplex-private-9-title": "Egyirányú<br>üzenetsorbaállítás", + "simplex-private-10-title": "Ideiglenes, névtelen, páronkénti azonosítók", + "simplex-private-card-1-point-1": "Double-Ratchet-protokoll —<br>OTR-üzenetküldés, sérülés utáni titkosság-védelemmel és -helyreállítással.", + "simplex-private-card-1-point-2": "NaCL cryptobox minden egyes üzenet sorbaállításához, hogy megakadályozza a forgalom korrelációját az üzenetsorbaállítások között, ha a TLS veszélybe kerül.", + "simplex-private-card-2-point-1": "Kiegészítő kiszolgáló titkosítási réteg a címzettnek történő kézbesítéshez, hogy megakadályozza a fogadott és az elküldött kiszolgálóforgalom közötti korrelációt, ha a TLS veszélybe kerül.", + "simplex-private-card-3-point-1": "A kliens és a kiszolgálók közötti kapcsolatokhoz csak az erős algoritmusokkal rendelkező TLS 1.2/1.3 protokollt használja.", + "simplex-private-card-3-point-2": "A kiszolgáló ujjlenyomata és a csatornakötés megakadályozza a MITM- és a visszajátszási támadásokat.", + "simplex-private-card-3-point-3": "Az újrakapcsolódás le van tiltva a munkamenet elleni támadások megelőzése érdekében.", + "simplex-private-card-4-point-1": "Az IP-címe védelme érdekében a kiszolgálókat a Tor-on vagy más átvitel-átfedő-hálózaton keresztül is elérheti.", + "simplex-private-card-6-point-1": "Számos kommunikációs hálózat sebezhető a kiszolgálók vagy a hálózat-szolgáltatók MITM-támadásaival szemben.", + "simplex-private-card-6-point-2": "Ennek megakadályozása érdekében a SimpleX-alkalmazások egyszeri kulcsokat adnak át sávon kívül, amikor egy címet hivatkozásként vagy QR-kódként oszt meg.", + "simplex-private-card-7-point-1": "Az integritás garantálása érdekében az üzenetek sorszámozással vannak ellátva, és tartalmazzák az előző üzenet hasítóértékét.", + "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 üzenetsorbaállítás egy irányba továbbítja az üzeneteket, a különböző küldési és vételi címekkel.", + "simplex-private-card-9-point-2": "A hagyományos üzenetküldőkhöz képest csökkenti a támadási vektorokat és a rendelkezésre álló metaadatokat.", + "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", + "privacy-matters-1-overlay-1-linkText": "Az adatvédelemmel pénzt spórol meg", + "privacy-matters-2-title": "A választások manipulálása", + "privacy-matters-2-overlay-1-title": "Az adatvédelem hatalmat ad", + "privacy-matters-2-overlay-1-linkText": "Az adatvédelem hatalmat ad", + "privacy-matters-3-title": "Ártatlan összefüggés miatti vádemelés", + "privacy-matters-3-overlay-1-title": "Az adatvédelem szabaddá tesz", + "privacy-matters-3-overlay-1-linkText": "Az adatvédelem szabaddá tesz", + "simplex-unique-1-title": "Teljes magánéletet élvezhet", + "simplex-unique-1-overlay-1-title": "Személyazonosságának, profiljának, kapcsolatainak és metaadatainak teljes körű védelme", + "simplex-unique-2-title": "Véd<br>a kéretlen tartalmaktól és a visszaélésektől", + "simplex-unique-2-overlay-1-title": "A legjobb védelem a kéretlen tartalmak és a visszaélések ellen", + "simplex-unique-3-title": "Ön kezeli az adatait", + "simplex-unique-3-overlay-1-title": "Az adatok biztonsága és kezelése az Ön kezében van", + "simplex-unique-4-title": "Öné a SimpleX-hálózat", + "simplex-unique-4-overlay-1-title": "Teljesen decentralizált — a SimpleX-hálózat a felhasználóké", + "hero-overlay-card-1-p-1": "Sok felhasználó kérdezte: <em>ha a SimpleXnek nincsenek felhasználói azonosítói, honnan tudja, hogy hová kell eljuttatni az üzeneteket?</em>", + "hero-overlay-card-1-p-2": "Az üzenetek kézbesítéséhez az összes többi hálózat által használt felhasználói azonosítók helyett a SimpleX az üzenetek sorbaállításához ideiglenes, névtelen, páros azonosítókat használ, külön-külön minden egyes kapcsolathoz — nincsenek hosszú távú azonosítók.", + "hero-overlay-card-1-p-4": "Ez a kialakítás megakadályozza a felhasználók metaadatainak kiszivárgását az alkalmazás szintjén. Az adatvédelem további javítása és az IP-cím védelme érdekében az üzenetküldő kiszolgálókhoz Tor hálózaton keresztül is kapcsolódhat.", + "hero-overlay-card-1-p-5": "Csak a kliensek tárolják a felhasználói profilokat, kapcsolatokat és csoportokat; az üzenetek küldése 2 rétegű végpontok közötti titkosítással történik.", + "hero-overlay-card-1-p-6": "További leírást a <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>SimpleX ismertetőben</a> olvashat.", + "hero-overlay-card-2-p-1": "Ha a felhasználók állandó azonosítóval rendelkeznek, még akkor is, ha ez csak egy véletlenszerű szám, például egy munkamenet-azonosító, fennáll annak a veszélye, hogy a szolgáltató vagy egy támadó megfigyelheti, azt hogy hogyan kapcsolódnak a felhasználók egymáshoz, és hány üzenetet küldenek egymásnak.", + "hero-overlay-card-2-p-2": "Ezt az információt aztán összefüggésbe hozhatják a meglévő nyilvános közösségi hálózatokkal, és meghatározhatnak néhány valódi személyazonosságot.", + "hero-overlay-card-2-p-3": "Még a Tor v3 szolgáltatásokat használó, legprivátabb alkalmazások esetében is, ha két különböző kapcsolattartóval beszél ugyanazon a profilon keresztül, bizonyítani tudják, hogy ugyanahhoz a személyhez kapcsolódnak.", + "hero-overlay-card-2-p-4": "A SimpleX úgy védekezik ezen támadások ellen, hogy nem tartalmaz felhasználói azonosítókat. Ha pedig használja az inkognitómódot, akkor minden egyes létrejött kapcsolatban más-más felhasználó név jelenik meg, így elkerülhető a közöttük lévő összefüggések teljes bizonyítása.", + "hero-overlay-card-3-p-1": "<a href=\"https://www.trailofbits.com/about/\">Trail of Bits</a> egy vezető biztonsági és technológiai tanácsadó cég, amelynek az ügyfelei közé tartoznak nagy technológiai cégek, kormányzati ügynökségek és jelentős blokklánc projektek.", + "hero-overlay-card-3-p-2": "A Trail of Bits 2022 novemberében áttekintette a SimpleX-platform kriptográfiai és hálózati komponenseit. <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">További információk</a>.", + "simplex-network-overlay-card-1-li-1": "A P2P-hálózatok az üzenetek továbbítására a <a href='https://en.wikipedia.org/wiki/Distributed_hash_table'>DHT</a> 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 <em>O(log N)</em> 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 metaadatvédelmet biztosít.", + "simplex-network-overlay-card-1-li-3": "A P2P nem oldja meg a <a href='https://en.wikipedia.org/wiki/Man-in-the-middle_attack'>MITM-támadás</a> 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 <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'>DRDoS-támadással</a> 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-5": "Minden ismert P2P-hálózat sebezhető <a href='https://en.wikipedia.org/wiki/Sybil_attack'>Sybil támadással</a>, 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ó (pl.: tracker), vagy egy drága <a href='https://en.wikipedia.org/wiki/Proof_of_work'>tanúsítvány</a>. 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 az önnel kapcsolatban álló személyek adatait, hogy megbecsülje az ön jövedelmét, hogy olyan termékeket adjon el önnek, amelyekre valójában nincs is szüksége, és 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 <a href='https://fairbydesign.com/povertypremium/' target='_blank'>„szegénységi prémium”</a>.", + "privacy-matters-overlay-card-1-p-4": "A SimpleX-hálózat minden alternatívánál jobban védi a kapcsolatainak 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 <a href='https://en.wikipedia.org/wiki/Facebook-Cambridge_Analytica_data_scandal' target='_blank'>neves tanácsadó cég</a>, 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-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 <a href='https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi' target='_blank'>Mohamedou Ould Salahi</a> 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, <a href='https://www.dailymail.co.uk/news/article-11282263/Moment-police-swoop-house-devout-catholic-mother-malicious-online-posts.html' target='_blank'>még demokratikus országokban is</a>.", + "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 <strong>nem rendel azonosítókat a felhasználókhoz</strong>. 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 az egyirányú üzenet várakoztatást használ <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>páronkénti névtelen címekkel</a>, külön a fogadott és külön az elküldött üzenetek számára, általában különböző kiszolgálókon keresztül.", + "simplex-unique-overlay-card-1-p-3": "Ez a kialakítás megvédi annak titkosságát, hogy kivel kommunikál, elrejtve azt a SimpleX-hálózat kiszolgálói és a megfigyelők elől. IP-címének a kiszolgálók elől való elrejtéséhez azt teheti meg, hogy <strong> Toron keresztül kapcsolódik a SimpleX-kiszolgálókhoz</strong>.", + "simplex-unique-overlay-card-2-p-1": "Mivel ön nem rendelkezik azonosítóval a SimpleX-hálózaton, 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 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 <strong>hordozható titkosított adatbázis-formátumban</strong>, 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 tartózkodnak, amíg be 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 <strong>a SimpleXet a saját kiszolgálóival</strong>, és továbbra is kommunikálhat azokkal, akik az előre beállított kiszolgálókat használják az alkalmazásban.", + "simplex-unique-overlay-card-4-p-2": "A SimpleX-hálózat <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>nyitott protokollt</a> használ és <a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>SDK-t biztosít a chatbotok létrehozásához</a>, lehetővé téve olyan szolgáltatások megvalósítását, amelyekkel a felhasználók a SimpleX Chat alkalmazásokon keresztül léphetnek kapcsolatba — mi már nagyon várjuk, hogy milyen SimpleX szolgáltatásokat készítenek a lelkes közreműködők.", + "simplex-unique-overlay-card-4-p-3": "Ha a SimpleX-hálózatra való fejlesztést fontolgatja, például a SimpleX-alkalmazások felhasználóinak szánt chatbotot, vagy a SimpleX Chat könyvtárbotjának integrálását más mobilalkalmazásba, <a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>lépjen velünk kapcsolatba</a>, ha bármilyen tanácsot vagy támogatást szeretne kapni.", + "simplex-unique-card-1-p-1": "A SimpleX megvédi a profilhoz tartozó kapcsolatokat és metaadatokat, elrejtve azokat a SimpleX-hálózat kiszolgálói és a megfigyelők elől.", + "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 — <strong>még véletlenszerű számokkal sem</strong>.", + "simplex-unique-card-2-p-1": "Mivel a SimpleX-hálózaton nincs azonosítója vagy állandó címe, 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-card-3-p-1": "A SimpleX Chat az összes felhasználói adatot kizárólag a klienseken tárolja egy <strong>hordozható titkosított adatbázis-formátumban</strong> —, 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-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 <strong>a SimpleXet a saját kiszolgálóival</strong> 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", + "we-invite-you-to-join-the-conversation": "Meghívjuk Önt, hogy csatlakozzon a beszélgetésekhez", + "join-the-REDDIT-community": "Csatlakozzon a REDDIT közösséghez", + "join-us-on-GitHub": "Csatlakozzon hozzánk a GitHubon", + "donate-here-to-help-us": "Adományozzon és segítsen nekünk", + "sign-up-to-receive-our-updates": "Regisztráljon a hírleveleinkre, hogy ne maradjon le semmiről", + "enter-your-email-address": "Adja meg az e-mail-címét", + "get-simplex": "A SimpleX számítógépes alkalmazásának <a href=\"/downloads\">letöltése</a>", + "why-simplex-is-unique": "Mitől <span class='gradient-text'>egyedülálló</span> a SimpleX", + "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árakoztatási címét NEM küldi el a hálózaton keresztül, amikor megtekinti ezt az oldalt — azokat, a hivatkozás webcímének hasítóérték-tö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.", + "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", + "tap-the-connect-button-in-the-app": "Koppintson a <span class='text-active-blue'>„kapcsolódás”</span> 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árakoztatási címét NEM küldjük el a hálózaton keresztül, amikor ezt az oldalt megtekinti —<br> ezek a hivatkozás webcímének hasítóérték-töredékében szerepelnek.", + "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", + "connect-in-app": "Kapcsolódás az alkalmazásban", + "the-instructions--source-code": "és választ kaphat arra, hogy hogyan töltse le vagy fordítsa le a forráskódból.", + "if-you-already-installed-simplex-chat-for-the-terminal": "Ha már telepítette a SimpleX Chatet a terminálhoz", + "if-you-already-installed": "Ha már telepítette a", + "simplex-chat-for-the-terminal": "SimpleX Chat a terminálhoz", + "copy-the-command-below-text": "másolja be az alábbi parancsot, és használja a csevegésben:", + "privacy-matters-section-header": "Miért <span class='gradient-text'>számít</span> az adatvédelem", + "privacy-matters-section-subheader": "A metaadatok védelmének megőrzése — <span class='text-active-blue'>kivel beszélget</span> — megvédi a következőktől:", + "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 <span class='gradient-text'>privát</span>", + "simplex-network-section-header": "SimpleX-<span class='gradient-text'>hálózat</span>", + "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ókönyvtár.", + "simplex-network-3-header": "SimpleX-hálózat", + "simplex-network-3-desc": "a kiszolgálók <span class='text-active-blue'>egyirányú üzenet várakoztatásokat</span> 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", + "protocol-1-text": "Signal, nagy platformok", + "protocol-2-text": "XMPP, Matrix", + "protocol-3-text": "P2P-protokollok", + "comparison-point-1-text": "Globális személyazonosságot igényel", + "comparison-point-2-text": "MITM lehetősége", + "comparison-point-4-text": "Egyetlen vagy központosított hálózat", + "comparison-point-5-text": "Központi komponens vagy más hálózati szintű támadás", + "no": "Nem", + "no-private": "Nem - privát", + "no-secure": "Nem - biztonságos", + "no-resilient": "Nem - ellenálló", + "no-decentralized": "Nem - decentralizált", + "no-federated": "Nem - föderált", + "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 az e2e 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-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-2": "Üzenetek küldése", + "guide-dropdown-3": "Titkos csoportok", + "guide-dropdown-4": "Csevegési profilok", + "guide-dropdown-5": "Adatkezelés", + "guide-dropdown-6": "Hang- és videó hívások", + "guide-dropdown-7": "Adatvédelem és biztonság", + "guide-dropdown-8": "Alkalmazás beállításai", + "guide": "Útmutató", + "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 jegyzékszolgáltatás", + "docs-dropdown-9": "Letöltések", + "f-droid-page-simplex-chat-repo-section-text": "Ha hozzá szeretné adni az F-Droid klienséhez, <span class='hide-on-mobile'>olvassa be a QR-kódot, vagy</span> használja ezt a webcímet:", + "signing-key-fingerprint": "Az aláírókulcs ujjlenyomata (SHA-256)", + "f-droid-org-repo": "F-Droid.org tároló", + "stable-versions-built-by-f-droid-org": "F-Droid.org által készített stabil verziók", + "releases-to-this-repo-are-done-1-2-days-later": "A kiadások ebben a tárolóban néhány napot késnek", + "f-droid-page-f-droid-org-repo-section-text": "A SimpleX Chat és az F-Droid.org tárolók különböző kulcsokkal írják alá az összeállításokat. A váltáshoz <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>exportálja</a> 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", + "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 <a href='https://en.wikipedia.org/wiki/BitTorrent'>BitTorrent</a>). 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-on keresztüli használatához telepítse az <a href=\"https://guardianproject.info/apps/org.torproject.android/\" target=\"_blank\">Orbot alkalmazást</a> és engedélyezze a SOCKS5 proxyt (vagy a VPN-t <a href=\"https://apps.apple.com/us/app/orbot/id1609461599?platform=iphone\" target=\"_blank\">az iOS-ban</a>).", + "simplex-private-card-5-point-1": "A SimpleX minden titkosítási réteghez tartalomkitöltést használ, hogy meghiúsítsa az üzenetméret ellen irányuló támadásokat.", + "simplex-private-card-5-point-2": "A kiszolgálók és a hálózatot megfigyelők számára a különböző méretű üzenetek egyformának tűnnek.", + "privacy-matters-1-title": "Hirdetés és árdiszkrimináció", + "hero-overlay-card-1-p-3": "Ön határozza meg, hogy melyik kiszolgáló(ka)t használja az üzenetek fogadására, a kapcsolatokhoz — azokat a kiszolgálókat, amelyeket az üzenetek küldésére használ. Minden beszélgetés két különböző kiszolgálót használ.", + "simplex-network-overlay-card-1-p-1": "A <a href='https://en.wikipedia.org/wiki/Peer-to-peer'>P2P</a> üzenetküldő protokollok és alkalmazások számos problémával küzdenek, amelyek miatt kevésbé megbízhatóak, mint a SimpleX, bonyolultabb az elemzésük és többféle támadással szemben sebezhetőek.", + "chat-bot-example": "Példa csevegési botra", + "simplex-private-3-title": "Biztonságos, hitelesített<br>TLS adatátvitel", + "github-repository": "GitHub tárolójában", + "tap-to-close": "Koppintson a bezáráshoz", + "simplex-network-1-header": "A P2P-hálózatokkal ellentétben", + "simplex-network-1-overlay-linktext": "a P2P-hálózatok problémái", + "comparison-point-3-text": "Függés a DNS-től", + "yes": "Igen", + "guide-dropdown-9": "Kapcsolatok létrehozása", + "docs-dropdown-4": "SMP-kiszolgáló üzemeltetése", + "docs-dropdown-5": "XFTP-kiszolgáló üzemeltetése", + "docs-dropdown-6": "WebRTC-kiszolgálók", + "docs-dropdown-7": "SimpleX Chat honosítása", + "docs-dropdown-10": "Átláthatóság", + "docs-dropdown-11": "GY.I.K.", + "docs-dropdown-12": "Biztonság", + "newer-version-of-eng-msg": "Ennek az oldalnak van egy újabb angol nyelvű változata.", + "click-to-see": "Kattintson a megtekintéséhez", + "menu": "Menü", + "on-this-page": "Ezen az oldalon", + "back-to-top": "Vissza a tetejére", + "glossary": "Fogalomtár", + "simplex-chat-via-f-droid": "SimpleX Chat az F-Droidon keresztül", + "simplex-chat-repo": "A SimpleX Chat tárolója", + "stable-and-beta-versions-built-by-developers": "A fejlesztők által készített stabil és béta verziók", + "hero-overlay-card-3-p-3": "A Trail of Bits 2024 júliusában felülvizsgálta a SimpleX hálózati protokollok kriptográfiai felépítését. <a href=\"/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html\">Tudjon meg többet</a>.", + "docs-dropdown-14": "SimpleX üzleti célra" +} diff --git a/website/langs/it.json b/website/langs/it.json index 1f3ad7fdda..18a47bfe9f 100644 --- a/website/langs/it.json +++ b/website/langs/it.json @@ -10,10 +10,10 @@ "simplex-explained-tab-3-p-1": "I server hanno credenziali anonime separate per ogni coda e non sanno a quali utenti appartengano.", "chat-protocol": "Protocollo di chat", "donate": "Dona", - "copyright-label": "© 2020-2023 SimpleX | Progetto Open-Source", + "copyright-label": "© 2020-2025 SimpleX | Progetto Open-Source", "simplex-chat-protocol": "Protocollo di SimpleX Chat", "terminal-cli": "Terminale CLI", - "terms-and-privacy-policy": "Termini e Informativa sulla privacy", + "terms-and-privacy-policy": "Informativa sulla privacy", "hero-header": "Privacy ridefinita", "hero-subheader": "Il primo messenger<br>senza ID utente", "hero-p-1": "Le altre app hanno gli ID utente: Signal, Matrix, Session, Briar, Jami, Cwtch, ecc.<br> SimpleX invece no, <strong>neanche dei numeri casuali</strong>.<br> Ciò aumenta radicalmente la tua privacy.", @@ -54,36 +54,35 @@ "simplex-unique-4-title": "Possiedi la rete SimpleX", "simplex-unique-4-overlay-1-title": "Completamente decentralizzata — gli utenti possiedono la rete SimpleX", "hero-overlay-card-1-p-1": "Molti utenti hanno chiesto: <em>se SimpleX non ha identificatori utente, come può sapere dove recapitare i messaggi? </em>", - "hero-overlay-card-1-p-3": "Sei tu a definire quale/i server usare per ricevere i messaggi, i tuoi contatti — i server che usi per inviare loro i messaggi. È probabile che ogni conversazione utilizzi due server diversi.", + "hero-overlay-card-1-p-3": "Sei tu a definire quale/i server usare per ricevere i messaggi, i tuoi contatti i server che usi per inviare loro i messaggi. È probabile che ogni conversazione utilizzi due server diversi.", "hero-overlay-card-1-p-5": "Solo i dispositivi client conservano profili utente, contatti e gruppi; i messaggi vengono inviati con crittografia end-to-end a 2 livelli.", "hero-overlay-card-1-p-6": "Maggiori informazioni nel <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>libro bianco di SimpleX</a>.", "hero-overlay-card-2-p-2": "Potrebbero quindi correlare queste informazioni con i social network pubblici esistenti e determinare alcune identità reali.", "hero-overlay-card-2-p-4": "SimpleX protegge da questi attacchi non avendo alcun ID utente per design. Inoltre, se usi la modalità di modalità in incognito, il tuo nome mostrato sarà diverso per ogni contatto, evitando dati condivisi tra di loro.", "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 <a href='https://it.wikipedia.org/wiki/BitTorrent'>BitTorrent</a>). SimpleX è indipendente dal trasporto - può funzionare su protocolli web standard, es. WebSocket.", + "simplex-network-overlay-card-1-li-4": "Le implementazioni P2P possono essere bloccate da alcuni fornitori di internet (come <a href='https://it.wikipedia.org/wiki/BitTorrent'>BitTorrent</a>). 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'<a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'>attacco DRDoS</a>, 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 piattaforma 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 forniti da SimpleX Chat, non conosciamo il numero di utenti o le loro connessioni.", - "privacy-matters-overlay-card-2-p-3": "SimpleX è la prima piattaforma che non ha alcun identificatore utente per design, proteggendo così il tuo grafico delle connessioni meglio di qualsiasi alternativa conosciuta.", + "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.", + "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\", <a href='https://www.dailymail.co.uk/news/article-11282263/Moment-police-swoop-house-devout-catholic-mother-malicious-online-posts.html' target='_blank'>anche nei Paesi democratici</a>.", - "simplex-unique-overlay-card-1-p-3": "Questo design protegge la privacy di chi stai comunicando, nascondendola ai server della piattaforma SimpleX e a qualsiasi osservatore. Per nascondere il tuo indirizzo IP ai server, puoi <strong>connetterti ai server SimpleX tramite Tor</strong>.", - "simplex-unique-overlay-card-2-p-1": "Poiché non hai alcun identificatore sulla piattaforma SimpleX, nessuno può contattarti a meno che tu non condivida un indirizzo utente una tantum o temporaneo, come un codice QR o un link.", + "simplex-unique-overlay-card-1-p-3": "Questo design protegge la privacy di chi stai comunicando, nascondendola ai server della rete SimpleX e a qualsiasi osservatore. Per nascondere il tuo indirizzo IP ai server, puoi <strong>connetterti ai server SimpleX tramite Tor</strong>.", + "simplex-unique-overlay-card-2-p-1": "Poiché non hai alcun identificatore sulla rete SimpleX, nessuno può contattarti a meno che tu non condivida un indirizzo utente una tantum o temporaneo, come un codice QR o un link.", "simplex-unique-overlay-card-2-p-2": "Anche l'indirizzo utente opzionale, che può essere usato per inviare richieste di contatto spam, è possibile modificarlo o eliminarlo completamente senza perdere alcuna connessione.", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat conserva tutti i dati utente solo sui dispositivi client usando un <strong>formato trasferibile di database crittografato</strong> che può essere esportato e trasferito su qualsiasi dispositivo supportato.", "simplex-unique-overlay-card-3-p-2": "I messaggi crittografati end-to-end vengono conservati temporaneamente sui server di inoltro SimpleX fino alla ricezione, quindi vengono eliminati definitivamente.", - "simplex-unique-overlay-card-4-p-1": "Puoi <strong>usare SimpleX con i tuoi server personali</strong> e continuare a comunicare con le persone che usano i server preconfigurati forniti da noi.", - "simplex-unique-overlay-card-4-p-3": "Se stai pensando di sviluppare per la piattaforma SimpleX, ad esempio, il chat bot per gli utenti dell'app SimpleX o l'integrazione della libreria SimpleX Chat nelle tue app mobili, <a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>contattaci</a> per qualsiasi consiglio e supporto.", - "simplex-unique-card-1-p-1": "SimpleX protegge la privacy del tuo profilo, contatti e metadati, nascondendoli ai server della piattaforma SimpleX e ad eventuali osservatori.", - "simplex-unique-card-1-p-2": "A differenza di qualsiasi altra piattaforma di messaggistica esistente, SimpleX non ha identificatori assegnati agli utenti — <strong>nemmeno numeri casuali</strong>.", - "simplex-unique-card-2-p-1": "Poiché non hai un identificatore o un indirizzo fisso sulla piattaforma SimpleX, nessuno può contattarti a meno che tu non condivida un indirizzo utente una tantum o temporaneo, come codice un QR o un link.", + "simplex-unique-overlay-card-4-p-1": "Puoi <strong>usare SimpleX con i tuoi server personali</strong> e continuare a comunicare con le persone che usano i server preconfigurati nelle app.", + "simplex-unique-overlay-card-4-p-3": "Se stai pensando di sviluppare per la rete SimpleX, ad esempio, il chat bot per gli utenti dell'app SimpleX o l'integrazione della libreria SimpleX Chat nelle tue app mobili, <a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>contattaci</a> per qualsiasi consiglio e supporto.", + "simplex-unique-card-1-p-1": "SimpleX protegge la privacy del tuo profilo, contatti e metadati, nascondendoli ai server della rete SimpleX e ad eventuali osservatori.", + "simplex-unique-card-1-p-2": "A differenza di qualsiasi altra rete di messaggistica esistente, SimpleX non ha identificatori assegnati agli utenti — <strong>nemmeno numeri casuali</strong>.", + "simplex-unique-card-2-p-1": "Poiché non hai un identificatore o un indirizzo fisso sulla rete SimpleX, nessuno può contattarti a meno che tu non condivida un indirizzo utente una tantum o temporaneo, come codice un QR o un link.", "simplex-unique-card-3-p-1": "SimpleX conserva tutti i dati utente sui dispositivi client in un <strong>formato trasferibile di database crittografato</strong> — può essere trasferito su un altro dispositivo.", "join": "Unisciti a", "we-invite-you-to-join-the-conversation": "Ti invitiamo a unirti alla conversazione", "enter-your-email-address": "Inserisci il tuo indirizzo email", "get-simplex": "Ottieni SimpleX <a href=\"/downloads\">desktop app</a>", - "why-simplex-is": "Perché SimpleX è", - "unique": "unico", + "why-simplex-is-unique": "Perché SimpleX è <span class='gradient-text'>unico</span>", "learn-more": "Maggiori informazioni", "more-info": "Mostra info", "hide-info": "Nascondi info", @@ -103,7 +102,7 @@ "use-this-command": "Usa questo comando:", "see-simplex-chat": "Vedi SimpleX Chat", "github-repository": "Repository GitHub", - "the-instructions--source-code": "le istruzioni su come scaricarlo o compilarlo dal codice sorgente.", + "the-instructions--source-code": "per le istruzioni su come scaricarlo o compilarlo dal codice sorgente.", "if-you-already-installed-simplex-chat-for-the-terminal": "Se hai già installato SimpleX Chat per il terminale", "if-you-already-installed": "Se hai già installato", "privacy-matters-section-subheader": "Preservare la privacy dei tuoi metadati — <span class='text-active-blue'>con chi parli</span> — ti protegge da:", @@ -131,13 +130,13 @@ "no-federated": "No - federato", "comparison-section-list-point-1": "Solitamente si basa su un numero di telefono, in alcuni casi su nomi utente", "comparison-section-list-point-2": "Indirizzi basati su DNS", - "comparison-section-list-point-6": "Sebbene i P2P siano distribuiti, non sono federati - operano come un'unica rete", + "comparison-section-list-point-6": "Sebbene i P2P siano distribuiti, non sono federati — operano come un'unica rete", "comparison-section-list-point-7": "Le reti P2P hanno un'autorità centrale o l'intera rete può essere compromessa", "see-here": "vedi qui", "simplex-network-3-header": "Nella rete di SimpleX", "simplex-private-card-7-point-2": "Se un messaggio viene aggiunto, rimosso o modificato, il destinatario verrà avvisato.", - "hero-overlay-card-1-p-2": "Per consegnare i messaggi, invece degli ID utente utilizzati da tutte le altre piattaforme, SimpleX usa identificatori temporanei anonimi a coppie delle code di messaggi, separati per ciascuna delle tue connessioni — non ci sono identificatori a lungo termine.", - "simplex-private-card-6-point-1": "Molte piattaforme di comunicazione sono vulnerabili agli attacchi MITM da parte di server o fornitori di rete.", + "hero-overlay-card-1-p-2": "Per consegnare i messaggi, invece degli ID utente utilizzati da tutte le altre reti, SimpleX usa identificatori temporanei anonimi a coppie delle code di messaggi, separati per ciascuna delle tue connessioni — non ci sono identificatori a lungo termine.", + "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.", @@ -162,7 +161,7 @@ "feature-3-title": "Gruppi decentralizzati crittografati E2E — solo gli utenti sanno che esistono", "blog": "Blog", "simplex-explained-tab-2-p-1": "Per ogni connessione usi due code di messaggi distinte per inviare e ricevere i messaggi attraverso server diversi.", - "simplex-explained-tab-2-p-2": "I server passano i messaggi solo in una direzione, senza avere il quadro completo della conversazione dell'utente o delle connessioni.", + "simplex-explained-tab-2-p-2": "I server passano i messaggi solo in una direzione, senza avere il quadro completo delle conversazioni dell'utente o delle connessioni.", "simplex-explained-tab-3-p-2": "Gli utenti possono aumentare ulteriormente la privacy dei metadati usando Tor per accedere ai server, evitando correlazioni per indirizzo IP.", "smp-protocol": "Protocollo SMP", "feature-1-title": "Messaggi crittografati E2E con markdown e modifica", @@ -182,21 +181,21 @@ "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 <a href='https://fairbydesign.com/povertypremium/' target='_blank'> \"premio di povertà\" </a>.", "privacy-matters-overlay-card-2-p-1": "Non molto tempo fa abbiamo assistito alla manipolazione delle principali elezioni da <a href='https://it.wikipedia.org/wiki/Scandalo_Facebook-Cambridge_Analytica' target='_blank'>una rispettabile società di consulenza</a> 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 piattaforma 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 utilizzi 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 <a href='https://it.wikipedia.org/wiki/Mohamedou_Ould_Slahi' target='_blank'>Mohamedou Ould Salahi</a> 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", "privacy-matters-overlay-card-3-p-4": "Non è sufficiente usare un messenger crittografato end-to-end, tutti dovremmo usare i messenger che proteggono la privacy delle nostre reti personali — con chi siamo connessi.", - "simplex-unique-overlay-card-1-p-2": "Per recapitare i messaggi, SimpleX usa <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>indirizzi anonimi a coppie</a> di code di messaggi unidirezionali, separate per i messaggi ricevuti e inviati, di solito tramite server diversi. Usare SimpleX è come avere <strong>un'email o telefono “temporanei” per ogni contatto</strong>, e nessuna seccatura per gestirli.", - "simplex-unique-overlay-card-1-p-1": "A differenza di altre piattaforme di messaggistica, SimpleX non ha <strong>alcun identificatore assegnato agli utenti</strong>. Non si basa su numeri di telefono, indirizzi basati su domini (come email o XMPP), nomi utente, chiavi pubbliche o persino numeri casuali per identificare i suoi utenti — non sappiamo quante persone usano i nostri server SimpleX.", + "simplex-unique-overlay-card-1-p-2": "Per recapitare i messaggi, SimpleX usa <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>indirizzi anonimi a coppie</a> di code di messaggi unidirezionali, separate per i messaggi ricevuti e inviati, di solito tramite server diversi.", + "simplex-unique-overlay-card-1-p-1": "A differenza di altre reti di messaggistica, SimpleX non ha <strong>alcun identificatore assegnato agli utenti</strong>. Non si basa su numeri di telefono, indirizzi basati su domini (come email o XMPP), nomi utente, chiavi pubbliche o persino numeri casuali per identificare i suoi utenti — gli operatori dei server non sanno quante persone usano i loro server.", "simplex-unique-overlay-card-3-p-3": "A differenza dei server di reti federate (email, XMPP o Matrix), i server SimpleX non conservano gli account utente, ma trasmettono solo i messaggi, proteggendo la privacy di entrambe le parti.", "simplex-unique-card-3-p-2": "I messaggi crittografati end-to-end vengono conservati temporaneamente sui server di inoltro SimpleX fino alla ricezione, quindi vengono eliminati definitivamente.", "simplex-unique-card-4-p-2": "Puoi <strong>usare SimpleX con i tuoi server personali</strong> o con i server forniti da noi — e connetterti comunque a qualsiasi utente.", "join-the-REDDIT-community": "Unisciti alla comunità di REDDIT", "sign-up-to-receive-our-updates": "Iscriviti per ricevere i nostri aggiornamenti", "simplex-unique-overlay-card-3-p-4": "Non ci sono identificatori o testi cifrati in comune tra il traffico del server inviato e quello ricevuto — se qualcuno lo osserva, non può determinare facilmente chi comunica con chi, anche se il TLS è compromesso.", - "simplex-unique-overlay-card-4-p-2": "La piattaforma SimpleX usa un <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>protocollo aperto</a> e fornisce un <a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>SDK per creare chat bot</a>, consentendo l'implementazione di servizi con cui gli utenti possono interagire tramite le app SimpleX Chat — siamo impazienti di vedere quali servizi SimpleX puoi creare.", - "simplex-unique-card-4-p-1": "La rete SimpleX è completamente decentralizzata e indipendente da qualsiasi criptovaluta o altra piattaforma, ad eccezione di internet.", + "simplex-unique-overlay-card-4-p-2": "La rete di SimpleX usa un <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>protocollo aperto</a> e fornisce un <a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>SDK per creare chat bot</a>, consentendo l'implementazione di servizi con cui gli utenti possono interagire tramite le app SimpleX Chat — siamo impazienti di vedere quali servizi SimpleX creerai.", + "simplex-unique-card-4-p-1": "La rete SimpleX è completamente decentralizzata e indipendente da qualsiasi criptovaluta o altra rete, ad eccezione di internet.", "donate-here-to-help-us": "Dona qui per aiutarci", "to-make-a-connection": "Per stabilire una connessione:", "scan-the-qr-code-with-the-simplex-chat-app-description": "Le chiavi pubbliche e l'indirizzo della coda di messaggi in questo link NON vengono inviati in rete quando vedi questa pagina —<br> sono contenuti nel frammento hash dell'URL del link.", @@ -219,7 +218,7 @@ "guide-dropdown-7": "Privacy e sicurezza", "guide-dropdown-9": "Creare connessioni", "guide": "Guida", - "docs-dropdown-1": "Piattaforma SimpleX", + "docs-dropdown-1": "Rete di SimpleX", "docs-dropdown-2": "Accesso ai file Android", "docs-dropdown-3": "Accesso al database di chat", "docs-dropdown-4": "Ospita un server SMP", @@ -233,7 +232,7 @@ "guide-dropdown-8": "Impostazioni dell'app", "docs-dropdown-7": "Traduci SimpleX Chat", "glossary": "Glossario", - "releases-to-this-repo-are-done-1-2-days-later": "Le pubblicazioni su questo repo avvengono 1-2 giorni dopo", + "releases-to-this-repo-are-done-1-2-days-later": "Le pubblicazioni su questo repo avvengono diversi giorni dopo", "f-droid-page-f-droid-org-repo-section-text": "I repository di SimpleX Chat e F-Droid.org firmano i pacchetti con chiavi diverse. Per passare da uno all'altro, <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>esporta</a> il database della chat e reinstalla l'app.", "signing-key-fingerprint": "Impronta della chiave di firma (SHA-256)", "f-droid-org-repo": "Repo di F-Droid.org", @@ -244,11 +243,17 @@ "stable-and-beta-versions-built-by-developers": "Versioni stabili e beta compilate dagli sviluppatori", "f-droid-page-simplex-chat-repo-section-text": "Per aggiungerlo al tuo client F-Droid <span class='hide-on-mobile'>scansiona il codice QR o</span> usa questo URL:", "comparison-section-list-point-4a": "I relay di SimpleX non possono compromettere la crittografia e2e. Verifica il codice di sicurezza per mitigare gli attacchi sul canale fuori banda", - "hero-overlay-3-title": "Valutazione della sicurezza", - "hero-overlay-card-3-p-2": "Trail of Bits ha revisionato i componenti di crittografia e di rete della piattaforma SimpleX nel novembre 2022.", - "hero-overlay-card-3-p-3": "Maggiori informazioni nell'<a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">annuncio</a>.", + "hero-overlay-3-title": "Valutazioni della sicurezza", + "hero-overlay-card-3-p-2": "Trail of Bits ha revisionato i componenti di crittografia e di rete della piattaforma SimpleX nel novembre 2022. <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">Maggiori informazioni</a>.", "jobs": "Unisciti al team", - "hero-overlay-3-textlink": "Valutazione della sicurezza", + "hero-overlay-3-textlink": "Valutazioni della sicurezza", "hero-overlay-card-3-p-1": "<a href=\"https://www.trailofbits.com/about/\">Trail of Bits</a> è leader nella consulenza di sicurezza e tecnologia, i cui clienti includono grandi aziende, agenzie governative e importanti progetti di blockchain.", - "docs-dropdown-9": "Download" + "docs-dropdown-9": "Download", + "please-enable-javascript": "Attiva JavaScript per vedere il codice QR.", + "please-use-link-in-mobile-app": "Usa il link nell'app mobile", + "docs-dropdown-10": "Trasparenza", + "docs-dropdown-12": "Sicurezza", + "docs-dropdown-11": "Domande frequenti", + "hero-overlay-card-3-p-3": "Trail of Bits ha analizzato la progettazione crittografica dei protocolli della rete SimpleX nel luglio 2024. <a href=\"/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html\">Leggi di più</a>.", + "docs-dropdown-14": "SimpleX per il lavoro" } diff --git a/website/langs/ja.json b/website/langs/ja.json index b2ac7ecbc0..05365e2271 100644 --- a/website/langs/ja.json +++ b/website/langs/ja.json @@ -32,7 +32,7 @@ "simplex-explained-tab-1-p-2": "ユーザー プロファイル識別子なしで単方向キューをどのように処理できるのでしょうか?", "simplex-chat-protocol": "SimpleX チャットプロトコル", "terminal-cli": "ターミナル CLI", - "terms-and-privacy-policy": "利用規約とプライバシーポリシー", + "terms-and-privacy-policy": "プライバシーポリシー", "hero-header": "プライバシーの基準を新境地に", "hero-subheader": "<br>ユーザーIDを持たない最初のメッセンジャー", "hero-overlay-1-textlink": "ユーザー ID がプライバシーに悪影響を与えるのはなぜですか?", @@ -52,7 +52,7 @@ "chat-protocol": "チャットプロトコル", "chat-bot-example": "チャットボットの例", "donate": "寄付", - "copyright-label": "© 2020-2023 SimpleX | Open-Source Project", + "copyright-label": "© 2020-2025 SimpleX | Open-Source Project", "hero-p-1": "他のアプリにはユーザー ID があります: Signal、Matrix、Session、Briar、Jami、Cwtch など。<br> SimpleX にはありません。<strong>乱数さえもありません</strong>。<br> これにより、プライバシーが大幅に向上します。", "copy-the-command-below-text": "以下のコマンドをコピーしてチャットで使用します:", "simplex-private-card-9-point-1": "各メッセージ キューは、異なる送信アドレスと受信アドレスを使用してメッセージを一方向に渡します。", @@ -93,7 +93,7 @@ "docs-dropdown-1": "SimpleXプラットフォーム", "hero-overlay-card-1-p-5": "クライアント デバイスのみがユーザー プロファイル、連絡先、およびグループを保存します。 メッセージは 2 レイヤーのエンドツーエンド暗号化を使用して送信されます。", "simplex-chat-for-the-terminal": "ターミナル用 SimpleX チャット", - "simplex-network-overlay-card-1-li-3": "P2P は <a href='https://en.wikipedia.org/wiki/Man-in-the-middle_ Attack'>MITM 攻撃</a> 問題を解決せず、既存の実装のほとんどは最初の鍵交換に帯域外メッセージを使用していません 。 SimpleX は、最初のキー交換に帯域外メッセージを使用するか、場合によっては既存の安全で信頼できる接続を使用します。", + "simplex-network-overlay-card-1-li-3": "P2P は <a href='https://en.wikipedia.org/wiki/Man-in-the-middle_attack'>MITM 攻撃</a> 問題を解決せず、既存の実装のほとんどは最初の鍵交換に帯域外メッセージを使用していません 。 SimpleX は、最初のキー交換に帯域外メッセージを使用するか、場合によっては既存の安全で信頼できる接続を使用します。", "the-instructions--source-code": "ソース コードからダウンロードまたはコンパイルする方法を説明します。", "simplex-network-section-desc": "Simplex Chat は、P2P とフェデレーション ネットワークの利点を組み合わせて最高のプライバシーを提供します。", "privacy-matters-section-subheader": "メタデータのプライバシーを保護する — <span class='text-active-blue'>話す相手</span> — 以下のことからあなたを守ります:", @@ -123,7 +123,7 @@ "no-resilient": "いいえ - 弾力性", "hide-info": "情報を隠す", "privacy-matters-overlay-card-3-p-4": "エンドツーエンドで暗号化されたメッセンジャーを使用するだけでは十分ではありません。私たちは皆、個人ネットワークのプライバシーを保護するメッセンジャーを使用する必要があります — 私たちがつながっているのは誰なのか。", - "releases-to-this-repo-are-done-1-2-days-later": "このリポジトリへのリリースは 1 ~ 2 日後に行われます", + "releases-to-this-repo-are-done-1-2-days-later": "このリポジトリへのリリースは数日後に行われます", "comparison-point-1-text": "グローバル ID が必要", "comparison-section-list-point-5": "ユーザーのメタデータのプライバシーを保護しない", "hero-overlay-card-2-p-2": "その後、この情報を既存の公開ソーシャル ネットワークと関連付けて、本当の身元を特定することができます。", @@ -150,7 +150,7 @@ "privacy-matters-2-overlay-1-title": "プライバシーはあなたに力を与えます", "simplex-unique-overlay-card-2-p-2": "オプションのユーザー アドレスを使用しても、スパムの連絡先リクエストの送信に使用される可能性がありますが、接続を失うことなく変更または完全に削除できます。", "simplex-unique-4-overlay-1-title": "完全に分散化されています — ユーザーは SimpleX ネットワークを所有します", - "simplex-network-overlay-card-1-li-5": "すべての既知の P2P ネットワークは、各ノードが検出可能であり、ネットワーク全体が動作するため、<a href='https://en.wikipedia.org/wiki/Sybil_question'>Sybil 攻撃</a>に対して脆弱である可能性があります。 この問題を軽減する既知の対策には、一元化されたコンポーネントか、高価な<a href='https://en.wikipedia.org/wiki/Proof_of_work'>作業証明</a>が必要です。 SimpleX ネットワークにはサーバーの検出機能がなく、断片化されており、複数の分離されたサブネットワークとして動作するため、ネットワーク全体への攻撃は不可能です。", + "simplex-network-overlay-card-1-li-5": "すべての既知の P2P ネットワークは、各ノードが検出可能であり、ネットワーク全体が動作するため、<a href='https://en.wikipedia.org/wiki/Sybil_attack'>Sybil 攻撃</a>に対して脆弱である可能性があります。 この問題を軽減する既知の対策には、一元化されたコンポーネントか、高価な<a href='https://en.wikipedia.org/wiki/Proof_of_work'>作業証明</a>が必要です。 SimpleX ネットワークにはサーバーの検出機能がなく、断片化されており、複数の分離されたサブネットワークとして動作するため、ネットワーク全体への攻撃は不可能です。", "simplex-private-2-title": "追加レイヤーの<br>サーバー暗号化", "hero-overlay-card-1-p-4": "この設計により、ユーザーの情報の漏洩が防止されます' アプリケーションレベルのメタデータ。 プライバシーをさらに向上させ、IP アドレスを保護するために、Tor 経由でメッセージング サーバーに接続できます。", "f-droid-org-repo": "F-Droid.org リポジトリ", @@ -193,13 +193,12 @@ "simplex-unique-overlay-card-3-p-2": "エンドツーエンドで暗号化されたメッセージは、SimpleXのリレーサーバーで受信するまで一時的に保持され、その後永久に削除されます。", "simplex-private-card-7-point-1": "整合性を保証するために、メッセージには連続した番号が付けられ、前のメッセージのハッシュが含まれます。", "contact-hero-p-2": "SimpleX Chat をまだダウンロードしていませんか?", - "why-simplex-is": "なぜSimpleXなのか", + "why-simplex-is-unique": "なぜSimpleXなのか<span class='gradient-text'>唯一</span>", "simplex-network-section-header": "SimpleX <span class='gradient-text'>ネットワーク</span>", "simplex-private-10-title": "一時的な匿名のペア識別子", "privacy-matters-1-overlay-1-linkText": "プライバシーの保護はコストを削減します", "tap-the-connect-button-in-the-app": "アプリの <span class='text-active-blue'>「接続」</span> ボタンをタップします", "comparison-section-list-point-4a": "SimpleX リレーは e2e 暗号化を侵害できません。 セキュリティ コードを検証して帯域外チャネルへの攻撃を軽減します", - "unique": "唯一", "simplex-network-1-overlay-linktext": "P2Pネットワークの問題点", "no-private": "いいえ - プライベート", "simplex-unique-1-title": "プライバシーが完全に守られます", @@ -243,5 +242,18 @@ "hero-overlay-card-1-p-3": "メッセージの受信に使用するサーバー、連絡先を定義します —メッセージを送信するために使用するサーバー。 すべての会話では 2 つの異なるサーバーが使用される可能性があります。", "simplex-unique-overlay-card-1-p-1": "他のメッセージング プラットフォームとは異なり、SimpleX には<strong>ユーザーに割り当てられる識別子がありません</strong>。 ユーザーを識別するために、電話番号、ドメインベースのアドレス (電子メールや XMPP など)、ユーザー名、公開キー、さらには乱数にも依存しません。 —我々もSimpleX サーバーを何人が使用しているかはわかりません。", "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat と F-Droid.org リポジトリは、異なるキーを使用してビルドに署名します。 切り替えるには、チャット データベースを<a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>エクスポート</a>し、アプリを再インストールしてください。", - "simplex-private-5-title": "何レイヤーもの<br>コンテンツパディング" + "simplex-private-5-title": "何レイヤーもの<br>コンテンツパディング", + "hero-overlay-card-3-p-1": "<a href=\"https://www.trailofbits.com/about/\">Trail of Bits</a>は、大手ハイテク企業、政府機関、主要なブロックチェーン・プロジェクトなどを顧客に持つ、セキュリティとテクノロジーの大手コンサルタント会社です。", + "jobs": "チームに参加する", + "hero-overlay-3-textlink": "セキュリティ評価", + "hero-overlay-3-title": "セキュリティ評価", + "hero-overlay-card-3-p-2": "Trail of Bitsは2022年11月にSimpleXプラットフォームの暗号とネットワークのコンポーネントを検証しました。<a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">詳しくは お知らせをご覧ください。</a>", + "docs-dropdown-9": "ダウンロード", + "please-enable-javascript": "QRコードを表示するためにJavaScriptを有効にしてください。", + "please-use-link-in-mobile-app": "このリンクをモバイルアプリで使用してください", + "docs-dropdown-10": "透明性", + "docs-dropdown-11": "よくある質問", + "docs-dropdown-12": "セキュリティ", + "docs-dropdown-14": "ビジネス向けSimpleX", + "hero-overlay-card-3-p-3": "Trail of Bits は 2024 年 7 月に SimpleX ネットワーク プロトコルの暗号設計をレビューしました。" } diff --git a/website/langs/nl.json b/website/langs/nl.json index 51c5855b51..30e751c9f9 100644 --- a/website/langs/nl.json +++ b/website/langs/nl.json @@ -7,7 +7,7 @@ "why-simplex": "Waarom SimpleX", "simplex-privacy": "SimpleX privacy", "simplex-network": "SimpleX netwerk", - "simplex-explained": "Simplex uitgelegd", + "simplex-explained": "SimpleX uitgelegd", "simplex-explained-tab-1-text": "1. Wat gebruikers ervaren", "simplex-explained-tab-3-text": "3. Wat servers zien", "simplex-explained-tab-1-p-1": "U kunt contacten en groepen maken en tweerichtings gesprekken voeren, zoals in elke andere messenger.", @@ -17,10 +17,10 @@ "chat-bot-example": "Chatbot voorbeeld", "smp-protocol": "SMP protocol", "donate": "Doneer", - "copyright-label": "© 2020-2023 SimpleX | Open-sourceproject", + "copyright-label": "© 2020-2025 SimpleX | Open-sourceproject", "simplex-chat-protocol": "SimpleX Chat protocol", "terminal-cli": "Terminal CLI", - "terms-and-privacy-policy": "Voorwaarden & Privacybeleid", + "terms-and-privacy-policy": "Privacybeleid", "hero-subheader": "De eerste messenger<br>zonder gebruikers-ID's", "hero-overlay-1-textlink": "Waarom zijn gebruikers-ID's slecht voor de privacy?", "hero-overlay-2-textlink": "Hoe werkt SimpleX?", @@ -116,8 +116,7 @@ "join-us-on-GitHub": "Volg ons op GitHub", "donate-here-to-help-us": "Doneer hier om ons te helpen", "sign-up-to-receive-our-updates": "Schrijf u in om onze updates te ontvangen", - "unique": "uniek", - "why-simplex-is": "Waarom is SimpleX", + "why-simplex-is-unique": "Waarom is SimpleX <span class='gradient-text'>uniek</span>", "learn-more": "Kom meer te weten", "more-info": "Meer informatie", "hide-info": "Info verbergen", @@ -183,7 +182,7 @@ "privacy-matters-overlay-card-3-p-2": "Een van de meest schokkende verhalen is de ervaring van <a href='https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi' target='_blank'>Mohamedou Ould Salahi</a> beschreven in zijn memoires en getoond in The Mauritanian movie. Hij werd zonder proces in het Guantanamo kamp geplaatst en werd daar 15 jaar lang gemarteld na een telefoontje naar zijn familielid in Afghanistan, op verdenking van betrokkenheid bij aanslagen van 9/11, ook al woonde hij de afgelopen 10 jaar in Duitsland.", "privacy-matters-overlay-card-3-p-3": "Gewone mensen worden gearresteerd voor wat ze online delen, zelfs via hun 'anonieme' accounts, <a href='https://www.dailymail.co.uk/news/article-11282263/Moment-police-swoop-house-devout-catholic-mother-malicious-online-posts.html' target='_blank'>zelfs in democratische landen</a>.", "privacy-matters-overlay-card-3-p-4": "Het is niet genoeg om een end-to-end versleutelde messenger te gebruiken, we zouden allemaal de messengers moeten gebruiken die de privacy van onze persoonlijke netwerken beschermen — met wie we verbonden zijn.", - "simplex-unique-overlay-card-1-p-2": "Om berichten af te leveren gebruikt SimpleX <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>paarsgewijze anonieme adressen</a> van unidirectionele berichten wachtrijen, gescheiden voor ontvangen en verzonden berichten, meestal via verschillende servers. Het gebruik van SimpleX is als het hebben van <strong>een andere “brander” e-mail of telefoon voor elk contact</strong>, en geen gedoe om ze te beheren.", + "simplex-unique-overlay-card-1-p-2": "Om berichten af te leveren gebruikt SimpleX <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>paarsgewijze anonieme adressen</a> van unidirectionele berichten wachtrijen, gescheiden voor ontvangen en verzonden berichten, meestal via verschillende servers.", "simplex-unique-overlay-card-1-p-3": "Dit ontwerp beschermt de privacy van met wie u communiceert en verbergt deze voor SimpleX platform servers en voor waarnemers. Om uw IP-adres voor de servers te verbergen, kunt u <strong> verbinding maken met SimpleX servers via Tor </strong>.", "simplex-unique-overlay-card-2-p-1": "Omdat je geen identificatie hebt op het SimpleX platform, kan niemand contact met je opnemen, tenzij je een eenmalig of tijdelijk gebruikers adres deelt, als een QR-code of een link.", "simplex-unique-overlay-card-2-p-2": "Zelfs met het optionele gebruikers adres, hoewel het kan worden gebruikt om spam-contact verzoeken te verzenden, kunt u het wijzigen of volledig verwijderen zonder uw verbindingen te verliezen.", @@ -240,15 +239,21 @@ "f-droid-org-repo": "F-Droid.org repo", "signing-key-fingerprint": "Signing key fingerprint (SHA-256)", "stable-versions-built-by-f-droid-org": "Stabiele versies gebouwd door F-Droid.org", - "releases-to-this-repo-are-done-1-2-days-later": "De releases voor deze repository vinden 1-2 dagen later plaats", + "releases-to-this-repo-are-done-1-2-days-later": "De releases voor deze repository vinden enkele dagen later plaats", "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat- en F-Droid.org-repository's ondertekenen builds met de verschillende sleutels. Om over te stappen, alstublieft <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>exporteer</a> de chatdatabase en installeer de app opnieuw.", "docs-dropdown-8": "SimpleX Directory Service", "comparison-section-list-point-4a": "SimpleX relais kunnen de e2e-versleuteling niet in gevaar brengen. Controleer de beveiligingscode om aanvallen op out-of-band kanalen te beperken", "hero-overlay-3-title": "Beveiligings beoordeling", - "hero-overlay-card-3-p-2": "Trail of Bits heeft in november 2022 de cryptografie en netwerkcomponenten van het SimpleX-platform beoordeeld.", - "hero-overlay-card-3-p-3": "Lees meer in <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">de aankondiging</a>.", + "hero-overlay-card-3-p-2": "Trail of Bits heeft in november 2022 de cryptografie en netwerkcomponenten van het SimpleX-platform beoordeeld. Lees meer in <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">de aankondiging</a>.", "jobs": "Sluit je aan bij het team", "hero-overlay-3-textlink": "Beveiligings beoordeling", "hero-overlay-card-3-p-1": "<a href=\"https://www.trailofbits.com/about/\">Trail of Bits</a> is een toonaangevend beveiligings- en technologieadviesbureau met klanten onder meer grote technologiebedrijven, overheidsinstanties en grote blockchain-projecten.", - "docs-dropdown-9": "Downloads" + "docs-dropdown-9": "Downloads", + "please-enable-javascript": "Schakel JavaScript in om de QR-code te zien.", + "please-use-link-in-mobile-app": "Gebruik de link in de mobiele app", + "docs-dropdown-10": "Transparantie", + "docs-dropdown-11": "FAQ", + "docs-dropdown-12": "Beveiliging", + "hero-overlay-card-3-p-3": "Trail of Bits heeft in juli 2024 het cryptografische ontwerp van SimpleX-netwerkprotocollen beoordeeld. <a href=\"/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html\">Lees meer</a>.", + "docs-dropdown-14": "SimpleX voor bedrijven" } diff --git a/website/langs/pl.json b/website/langs/pl.json index ba0c72da7e..d45207a709 100644 --- a/website/langs/pl.json +++ b/website/langs/pl.json @@ -15,10 +15,10 @@ "smp-protocol": "Protokół SMP", "chat-protocol": "Protokół czatu", "donate": "Darowizna", - "copyright-label": "© 2020-2023 SimpleX | Projekt Open-Source", + "copyright-label": "© 2020-2025 SimpleX | Projekt Open-Source", "simplex-chat-protocol": "Protokół SimpleX Chat", "terminal-cli": "Terminal CLI", - "terms-and-privacy-policy": "Warunki i polityka prywatności", + "terms-and-privacy-policy": "Polityka prywatności", "hero-header": "Prywatność zdefiniowana na nowo", "hero-subheader": "Pierwszy komunikator<br>bez identyfikatorów użytkowników (ID)", "hero-overlay-1-textlink": "Dlaczego identyfikatory użytkowników (ID) są złe dla prywatności?", @@ -45,10 +45,10 @@ "home": "Strona główna", "developers": "Deweloperzy", "simplex-explained-tab-1-text": "1. Jakie są doświadczenia użytkowników", - "simplex-explained-tab-1-p-1": "Można tworzyć kontakty i grupy oraz prowadzić dwukierunkowe rozmowy, jak w każdym innym komunikatorze.", + "simplex-explained-tab-1-p-1": "Możesz tworzyć kontakty i grupy oraz prowadzić dwukierunkowe rozmowy, jak w każdym innym komunikatorze.", "simplex-explained-tab-2-p-2": "Serwery przekazują wiadomości tylko w jedną stronę, nie mając pełnego obrazu konwersacji i połączeń użytkownika.", "simplex-explained-tab-3-p-2": "Użytkownicy mogą jeszcze bardziej zwiększyć prywatność metadanych, używając Tor do uzyskania dostępu do serwerów, co zapobiega korelacji na podstawie adresu IP.", - "hero-p-1": "Inne aplikacje mają identyfikatory użytkowników (ID): Signal, Matrix, Session, Briar, Jami, Cwtch itp.<br> SimpleX nie, <strong>nie ma nawet losowych numerów</strong>.<br> To radykalnie poprawia Twoją prywatność.", + "hero-p-1": "Inne aplikacje posiadają identyfikatory użytkowników (ID): Signal, Matrix, Session, Briar, Jami, Cwtch itp.<br> SimpleX nie, <strong>nie posiada nawet losowych numerów</strong>.<br> To radykalnie poprawia Twoją prywatność.", "feature-2-title": "Obrazy, wideo i pliki<br>zaszyfrowane przez E2E", "feature-8-title": "Tryb incognito —<br>unikalny dla SimpleX Chat", "simplex-network-overlay-1-title": "Porównanie z protokołami komunikacyjnymi P2P", @@ -140,7 +140,7 @@ "simplex-network-overlay-card-1-li-6": "Sieci P2P mogą być podatne na atak <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'>DRDoS</a>, kiedy klienci mogą rozgłaszać i wzmacniać ruch, co powoduje odmowę usługi w całej sieci. Klienci SimpleX przekazują jedynie ruch ze znanego połączenia i nie mogą być wykorzystani przez atakującego do wzmocnienia ruchu w całej sieci.", "privacy-matters-overlay-card-1-p-1": "Wiele dużych firm wykorzystuje informacje o tym, z kim jesteś połączony, aby oszacować Twoje dochody, sprzedać Ci produkty, których tak naprawdę nie potrzebujesz, oraz ustalić ceny.", "simplex-unique-overlay-card-2-p-2": "Nawet w przypadku opcjonalnego adresu użytkownika, podczas gdy może on być używany do wysyłania spamowych zapytań o kontakt, możesz go zmienić lub całkowicie usunąć bez utraty jakichkolwiek połączeń.", - "simplex-unique-overlay-card-1-p-2": "Do dostarczania wiadomości SimpleX używa <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>parami anonimowych adresów</a> jednokierunkowych kolejek z wiadomościami, oddzielnych dla wiadomości odbieranych i wysyłanych, zwykle przez różne serwery. Korzystanie z SimpleX jest jak posiadanie <strong>wielu “jednorazowych” emailów lub osobnego telefonu dla każdego kontaktu</strong> i braku problemów z zarządzaniem nimi.", + "simplex-unique-overlay-card-1-p-2": "Do dostarczania wiadomości SimpleX używa <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>parami anonimowych adresów</a> jednokierunkowych kolejek z wiadomościami, oddzielnych dla wiadomości odbieranych i wysyłanych, zwykle przez różne serwery.", "simplex-unique-overlay-card-1-p-3": "Taka konstrukcja chroni prywatność tego, z kim się komunikujesz, ukrywając ją przed serwerami platformy SimpleX i przed wszelkimi obserwatorami. Aby ukryć swój adres IP przed serwerami, możesz <strong>połączyć się z serwerami SimpleX za pośrednictwem sieci Tor</strong>.", "simplex-unique-overlay-card-3-p-4": "Nie ma żadnych identyfikatorów ani szyfrogramów wspólnych między wysyłanym i odbieranym ruchem serwera — jeśli ktokolwiek to obserwuje, nie może łatwo określić, kto komunikuje się z kim, nawet jeśli bezpieczeństwo protokołu TLS zostało zagrożone.", "simplex-unique-overlay-card-2-p-1": "Ponieważ nie masz identyfikatora na platformie SimpleX, nikt nie może się z Tobą skontaktować, chyba że udostępnisz jednorazowy lub tymczasowy adres użytkownika, w postaci kodu QR lub linku.", @@ -154,13 +154,12 @@ "simplex-unique-card-1-p-2": "W przeciwieństwie do każdej innej istniejącej platformy komunikacyjnej, SimpleX nie ma żadnych identyfikatorów przypisanych do użytkowników — <strong>nawet losowych liczb</strong>.", "tap-the-connect-button-in-the-app": "Stuknij przycisk <span class='text-active-blue'>'połącz się'</span> w aplikacji", "join-the-REDDIT-community": "Dołącz do społeczności REDDIT", - "unique": "unikalny", "hide-info": "Ukryj informacje", "simplex-unique-card-4-p-2": "Możesz <strong>używać SimpleX z własnymi serwerami</strong> lub z serwerami dostarczonymi przez nas — i nadal łączyć się z dowolnym użytkownikiem.", "we-invite-you-to-join-the-conversation": "Zapraszamy do udziału w rozmowie", "enter-your-email-address": "Wpisz swój adres e-mail", "get-simplex": "Pobierz SimpleX <a href=\"/downloads\">desktop app</a>", - "why-simplex-is": "Dlaczego SimpleX jest", + "why-simplex-is-unique": "Dlaczego SimpleX jest <span class='gradient-text'>unikalny</span>", "join": "Dołącz do", "join-us-on-GitHub": "Dołącz do nas na GitHubie", "sign-up-to-receive-our-updates": "Zapisz się, aby otrzymywać nasze aktualizacje", @@ -194,7 +193,7 @@ "simplex-network-1-header": "W przeciwieństwie do sieci P2P", "simplex-network-section-desc": "Simplex Chat zapewnia najlepszą prywatność dzięki połączeniu zalet sieci P2P i sieci federacyjnych.", "simplex-network-2-header": "W przeciwieństwie do sieci federacyjnych", - "simplex-private-section-header": "Co sprawia, że SimpleX jest<span class='gradient-text'>prywatny</span>", + "simplex-private-section-header": "Co sprawia, że SimpleX jest <span class='gradient-text'>prywatny</span>", "tap-to-close": "Stuknij, aby zamknąć", "simplex-network-section-header": "SimpleX <span class='gradient-text'>Sieć</span>", "simplex-network-2-desc": "Serwery przekaźnikowe SimpleX NIE przechowują profili użytkowników, kontaktów i dostarczonych wiadomości, NIE łączą się ze sobą i NIE ma katalogu serwerów.", @@ -242,13 +241,19 @@ "signing-key-fingerprint": "Odcisk klucza podpisu (SHA-256)", "f-droid-org-repo": "Repo F-Droid.org", "stable-versions-built-by-f-droid-org": "Wersje stabilne zbudowane przez F-Droid.org", - "releases-to-this-repo-are-done-1-2-days-later": "Wydania na tym repo są 1-2 dni później", + "releases-to-this-repo-are-done-1-2-days-later": "Wydania na tym repo są kilka dni później", "comparison-section-list-point-4a": "Przekaźniki SimpleX nie mogą skompromitować szyfrowania e2e. Zweryfikuj kody bezpieczeństwa aby złagodzić atak na kanał pozapasmowy", - "hero-overlay-3-title": "Ocena bezpieczeństwa", - "hero-overlay-card-3-p-2": "Trail of Bits przejrzał komponenty kryptograficzne i sieciowe platformy SimpleX w listopadzie 2022.", - "hero-overlay-card-3-p-3": "Przeczytaj więcej w <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">ogłoszeniach</a>.", + "hero-overlay-3-title": "Oceny bezpieczeństwa", + "hero-overlay-card-3-p-2": "Trail of Bits przejrzał komponenty kryptograficzne i sieciowe platformy SimpleX w listopadzie 2022. Przeczytaj więcej w <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">ogłoszeniach</a>.", "jobs": "Dołącz do zespołu", - "hero-overlay-3-textlink": "Ocena bezpieczeństwa", + "hero-overlay-3-textlink": "Analizy bezpieczeństwa", "hero-overlay-card-3-p-1": "<a href=\"https://www.trailofbits.com/about/\">Trail of Bits</a> jest wiodącą firmą konsultingową w zakresie bezpieczeństwa i technologii, której klientami są duże firmy technologiczne, agencje rządowe i główne projekty blockchain.", - "docs-dropdown-9": "Pliki do pobrania" + "docs-dropdown-9": "Pliki do pobrania", + "please-enable-javascript": "Prosimy o włączenie JavaScript w celu wyświetlenia kodu QR.", + "please-use-link-in-mobile-app": "Prosimy o skorzystanie z linku w aplikacji mobilnej", + "docs-dropdown-10": "Przezroczystość", + "docs-dropdown-12": "Bezpieczeństwo", + "docs-dropdown-11": "Często zadawane pytania", + "hero-overlay-card-3-p-3": "Firma Trail of Bits dokonała analizy projektu kryptograficznego protokołów sieciowych SimpleX w lipcu 2024 roku. <a href=\"/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html\">Dowiedz się więcej</a>.", + "docs-dropdown-14": "SimpleX dla firm" } diff --git a/website/langs/pt_BR.json b/website/langs/pt_BR.json index 278d54c43a..77854f5be1 100644 --- a/website/langs/pt_BR.json +++ b/website/langs/pt_BR.json @@ -11,22 +11,22 @@ "simplex-explained-tab-1-p-1": "Você pode criar contatos e grupos e ter conversas bidirecionais, como em qualquer outro mensageiro.", "simplex-explained-tab-2-p-1": "Para cada conexão, são usadas duas filas de mensagens separadas para enviar e receber mensagens por meio de servidores diferentes.", "hero-p-1": "Outros aplicativos possuem IDs de usuário: Signal, Matrix, Session, Briar, Jami, Cwtch, etc.<br> O SimpleX não tem, <strong>nem mesmo números aleatórios</strong>.<br> Isso melhora radicalmente a sua privacidade.", - "terms-and-privacy-policy": "Termos e Política de Privacidade", + "terms-and-privacy-policy": "Termos de serviço & Política de Privacidade", "hero-2-header-desc": "O vídeo mostra como você se conecta com seu amigo através do QR code de uso único dele, pessoalmente ou via link de vídeo. Você também pode se conectar compartilhando um link de convite.", - "feature-7-title": "Banco de dados criptografado portátil — mova seu perfil para outro dispositivo", + "feature-7-title": "Armazenamento do aplicativo criptografado portátil — mova o perfil para outro dispositivo", "simplex-explained": "Explicação do SimpleX", "simplex-explained-tab-1-text": "1. O que os usuários experimentam", "simplex-explained-tab-3-text": "3. O que os servidores veem", "simplex-explained-tab-1-p-2": "Como ele pode funcionar com filas unidirecionais e sem identificadores de perfil de usuário?", "simplex-explained-tab-2-p-2": "Os servidores só passam mensagens em uma direção, sem ter a imagem completa da conversa ou conexões do usuário.", "simplex-explained-tab-3-p-1": "Os servidores têm credenciais anônimas separadas para cada envio e não sabem a que usuários elas pertencem.", - "simplex-explained-tab-3-p-2": "Os usuários podem melhorar ainda mais a privacidade de metadados usando o Tor para acessar os servidores, impedindo a correlação pelo endereço IP.", + "simplex-explained-tab-3-p-2": "Os usuários podem melhorar ainda mais a privacidade de metadados usando o Tor para acessar os servidores, impedindo a correlação pelo endereço de IP.", "chat-bot-example": "Exemplo de chat bot", "smp-protocol": "Protocolo SMP", - "chat-protocol": "Protocolo de chat", + "chat-protocol": "Protocolo de bate-papo", "donate": "Doar", - "copyright-label": "© 2020-2023 SimpleX | Projeto de código livre", - "simplex-chat-protocol": "Protocolo de Chat SimpleX", + "copyright-label": "© 2020-2025 SimpleX | Projeto de Código Livre", + "simplex-chat-protocol": "Protocolo Chat SimpleX", "terminal-cli": "CLI Terminal", "hero-header": "Privacidade redefinida", "hero-subheader": "O primeiro mensageiro<br>sem ID de usuário", @@ -36,8 +36,8 @@ "hero-overlay-1-textlink": "Por que os IDs são ruins para a privacidade?", "hero-overlay-2-title": "Por que os IDs são ruins para a privacidade?", "feature-1-title": "Mensagens criptografadas de ponta-a-ponta com markdown e edição", - "feature-2-title": "Imagens e arquivos<br>criptografados de ponta-a-ponta", - "feature-3-title": "Grupos secretos descentralizados —<br>somente os usuários sabem que eles existem", + "feature-2-title": "Criptografia de ponta-a-ponta<br>imagens, vídeos e arquivos", + "feature-3-title": "Criptografia de ponta-a-ponta descentralizada de grupos — somente os usuários sabem que eles existem", "feature-4-title": "Mensagens de voz criptografadas de ponta-a-ponta", "feature-5-title": "Mensagens que desaparecem", "feature-6-title": "Chamadas de áudio e vídeo<br>criptografadas de ponta-a-ponta", @@ -65,13 +65,13 @@ "privacy-matters-overlay-card-1-p-2": "Os varejistas online sabem que as pessoas com renda mais baixa têm maior probabilidade de fazer compras urgentes e, por isso, podem cobrar preços mais altos ou remover descontos.", "privacy-matters-overlay-card-1-p-4": "A plataforma SimpleX protege a privacidade de suas conexões melhor do que qualquer alternativa, impedindo totalmente que seu gráfico social fique disponível para quaisquer empresas ou organizações. Mesmo quando as pessoas usam servidores fornecidos pelo SimpleX Chat, não sabemos o número de usuários ou suas conexões.", "privacy-matters-overlay-card-2-p-2": "Para ser objetivo e tomar decisões independentes, você precisa ter o controle do seu espaço de informações. Isso só é possível se você usar uma plataforma de comunicação privada que não tenha acesso ao seu gráfico social.", - "privacy-matters-overlay-card-3-p-1": "Todos devem se preocupar com a privacidade e a segurança de suas comunicações - conversas inofensivas podem colocá-lo em perigo, mesmo que você não tenha nada a esconder.", + "privacy-matters-overlay-card-3-p-1": "Todos devem se preocupar com a privacidade e a segurança de suas comunicações — conversas inofensivas podem colocá-lo em perigo, mesmo que você não tenha nada a esconder.", "simplex-unique-overlay-card-3-p-1": "O SimpleX Chat armazena todos os dados do usuário somente em dispositivos clientes usando um <strong>formato de banco de dados criptografado portátil</strong> que pode ser exportado e transferido para qualquer dispositivo compatível.", - "privacy-matters-overlay-card-3-p-2": "Uma das histórias mais chocantes é a experiência de <a href='https://pt.wikipedia.org/wiki/Mohamedou_Ould_Slahi' target='_blank'>Mohamedou Ould Salahi</a>, descrita em seu livro de memórias e mostrada no filme O Mauritano. Ele foi colocado no campo de Guantánamo, sem julgamento, e lá foi torturado por 15 anos após um telefonema para seu parente no Afeganistão, sob suspeita de estar envolvido nos ataques de 11 de setembro, embora tenha vivido na Alemanha nos 10 anos anteriores.", - "simplex-unique-overlay-card-1-p-1": "Ao contrário de outras plataformas de mensagens, o SimpleX <strong>não tem identificadores atribuídos aos usuários</strong>. Ele não depende de números de telefone, endereços baseados em domínio (como e-mail ou XMPP), nomes de usuário, chaves públicas ou mesmo números aleatórios para identificar seus usuários — não sabemos quantas pessoas usam nossos servidores SimpleX.", - "simplex-private-card-1-point-1": "Protocolo de dupla catraca —<br>mensagens OTR com sigilo de encaminhamento perfeito e recuperação de invasão.", + "privacy-matters-overlay-card-3-p-2": "Uma das histórias mais chocantes é a experiência de <a href='https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi' target='_blank'>Mohamedou Ould Salahi</a>, descrita em seu livro de memórias e mostrada no filme O Mauritano. Ele foi colocado no campo de Guantánamo, sem julgamento, e lá foi torturado por 15 anos após um telefonema para seu parente no Afeganistão, sob suspeita de estar envolvido nos ataques de 11 de setembro, embora tenha vivido na Alemanha nos 10 anos anteriores.", + "simplex-unique-overlay-card-1-p-1": "Ao contrário de outras plataformas de mensagens, o SimpleX <strong>não tem identificadores atribuídos aos usuários</strong>. Ele não depende de números de telefone, endereços baseados em domínio (como email ou XMPP), nomes de usuário, chaves públicas ou mesmo números aleatórios para identificar seus usuários — não sabemos quantas pessoas usam nossos servidores SimpleX.", + "simplex-private-card-1-point-1": "Protocolo de dupla catraca —<br>mensagens OTR com Sigilo de Encaminhamento Perfeito (Perfect Forward Secrecy) e recuperação de invasão.", "simplex-private-8-title": "Mistura de mensagens<br>para reduzir a correlação", - "simplex-unique-overlay-card-4-p-2": "A plataforma SimpleX usa um <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>protocolo aberto</a> e fornece um <a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>SDK para criar bots de chat</a>, permitindo a implementação de serviços com os quais os usuários podem interagir por meio dos aplicativos SimpleX Chat — estamos realmente ansiosos para ver quais serviços SimpleX você pode criar.", + "simplex-unique-overlay-card-4-p-2": "A plataforma SimpleX usa um <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>protocolo aberto</a> e fornece um <a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>SDK para criar bots de chat</a>, permitindo a implementação de serviços com os quais os usuários podem interagir por meio dos aplicativos SimpleX Chat — estamos' realmente ansiosos para ver quais serviços SimpleX você pode criar.", "simplex-unique-overlay-card-3-p-4": "Não há identificadores ou texto cifrado em comum entre o tráfego de servidor enviado e recebido — se alguém estiver observando, não poderá determinar facilmente quem se comunica com quem, mesmo que o TLS esteja comprometido.", "simplex-unique-overlay-card-4-p-3": "Se estiver pensando em desenvolver para a plataforma SimpleX, por exemplo, o bot de chat para os usuários do aplicativo SimpleX ou a integração da biblioteca SimpleX Chat em seus aplicativos móveis, <a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>entre em contato</a> para qualquer orientação e suporte.", "simplex-unique-card-2-p-1": "Como você não tem um identificador ou endereço fixo na plataforma SimpleX, ninguém pode entrar em contato com você, a menos que você compartilhe um endereço de usuário único ou temporário, como um QR code ou um link.", @@ -81,7 +81,7 @@ "contact-hero-p-3": "Use os links abaixo para baixar o aplicativo.", "feature-8-title": "Modo anônimo —<br> único do SimpleX Chat", "simplex-network-overlay-1-title": "Comparação com protocolos de mensagens P2P", - "simplex-private-2-title": "Camada adicional de<br>encriptação do servidor", + "simplex-private-2-title": "Camada adicional de<br>criptografia do servidor", "simplex-private-4-title": "Acesso opcional<br>via Tor", "simplex-private-6-title": "Troca de chaves<br>fora da rede", "simplex-private-7-title": "Verificação de integridade<br>da mensagem", @@ -91,7 +91,7 @@ "simplex-private-card-3-point-1": "Somente o TLS 1.2/1.3 com algoritmos fortes é usado para conexões cliente-servidor.", "simplex-private-card-3-point-2": "A impressão digital do servidor e a vinculação de canais evitam ataques MITM e de repetição.", "simplex-private-card-3-point-3": "A retomada da conexão é desativada para evitar ataques à sessão.", - "simplex-private-card-5-point-1": "O SimpleX usa proteção de conteúdo em cada camada de criptografia para impedir ataques ao tamanho da mensagem.", + "simplex-private-card-5-point-1": "O SimpleX usa preenchimento de conteúdo em cada camada de criptografia para impedir ataques ao tamanho da mensagem.", "simplex-private-5-title": "Múltiplas camadas de<br>preenchimento de conteúdos", "simplex-private-card-5-point-2": "Isso faz com que mensagens de tamanhos diferentes tenham a mesma aparência para os servidores e observadores de rede.", "simplex-private-card-6-point-1": "Muitas plataformas de comunicação são vulneráveis a ataques MITM por servidores ou provedores de rede.", @@ -117,22 +117,22 @@ "hero-overlay-card-1-p-4": "Esse design evita o vazamento de metadados de qualquer usuário' no nível do aplicativo. Para aumentar ainda mais a privacidade e proteger seu endereço IP, você pode se conectar aos servidores de mensagens via Tor.", "hero-overlay-card-1-p-6": "Leia mais no <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>informativo técnico do SimpleX</a>.", "hero-overlay-card-2-p-2": "Eles poderiam então correlacionar essas informações com as redes sociais públicas existentes e determinar algumas identidades reais.", - "hero-overlay-card-2-p-4": "O SimpleX se protege contra esses ataques por não ter nenhum ID de usuário em seu design. E, se você usar o modo Incognito, terá um nome de exibição diferente para cada contato, evitando qualquer compartilhamento de dados entre eles.", + "hero-overlay-card-2-p-4": "O SimpleX se protege contra esses ataques por não ter nenhum ID de usuário em seu design. E, se você usar o modo Anônimo, terá um nome de exibição diferente para cada contato, evitando qualquer compartilhamento de dados entre eles.", "simplex-network-overlay-card-1-p-1": "Os protocolos e aplicativos de mensagens <a href='https://pt.wikipedia.org/wiki/Peer-to-peer'>P2P</a> têm vários problemas que os tornam menos confiáveis do que o SimpleX, mais complexos de analisar e vulneráveis a vários tipos de ataque.", "simplex-network-overlay-card-1-li-2": "O design do SimpleX, ao contrário da maioria das redes P2P, não tem identificadores de usuário globais de qualquer tipo, mesmo temporários, e usa apenas identificadores temporários em pares, proporcionando melhor anonimato e proteção de metadados.", "simplex-network-overlay-card-1-li-3": "O P2P não resolve o problema do <a href='https://pt.wikipedia.org/wiki/Ataque_man-in-the-middle'>ataque MITM</a>, e a maioria das implementações existentes não usa mensagens fora de banda para a troca de chaves inicial. O SimpleX usa mensagens fora de banda ou, em alguns casos, conexões pré-existentes seguras e confiáveis para a troca de chaves inicial.", "simplex-network-overlay-card-1-li-6": "As redes P2P podem ser vulneráveis a <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'>ataques DRDoS</a>, quando os clientes podem retransmitir e amplificar o tráfego, resultando em uma negação de serviço em toda a rede. Os clientes SimpleX apenas retransmitem o tráfego de uma conexão conhecida e não podem ser usados por um invasor para amplificar o tráfego em toda a rede.", "privacy-matters-overlay-card-1-p-3": "Algumas empresas financeiras e de seguros usam gráficos sociais para determinar taxas de juros e prêmios. Isso geralmente faz com que as pessoas com renda mais baixa paguem mais — isso é conhecido como <a href='https://fairbydesign.com/povertypremium/' target='_blank'>\"prêmio de pobreza\"</a>.", - "privacy-matters-overlay-card-2-p-1": "Não faz muito tempo que observamos as eleições sendo manipuladas por <a href='https://pt.wikipedia.org/wiki/Esc%C3%A2ndalo_de_dados_Facebook%E2%80%93Cambridge_Analytica' target='_blank'>uma empresa de consultoria respeitável</a> que usou nossos gráficos sociais para distorcer nossa visão do mundo real e manipular nossos votos.", + "privacy-matters-overlay-card-2-p-1": "Não faz muito tempo que observamos as eleições sendo manipuladas por <a href='https://en.wikipedia.org/wiki/Facebook–Cambridge_Analytica_data_scandal' target='_blank'>uma empresa de consultoria respeitável</a> que usou nossos gráficos sociais para distorcer nossa visão do mundo real e manipular nossos votos.", "privacy-matters-overlay-card-2-p-3": "O SimpleX é a primeira plataforma que não tem nenhum identificador de usuário por design, protegendo assim seu gráfico de conexões melhor do que qualquer alternativa conhecida.", "privacy-matters-overlay-card-3-p-3": "Pessoas comuns são presas pelo que compartilham online, mesmo por meio de suas contas \"anônimas\", <a href='https://www.dailymail.co.uk/news/article-11282263/Moment-police-swoop-house-devout-catholic-mother-malicious-online-posts.html' target='_blank'>mesmo em países democráticos</a>.", "privacy-matters-overlay-card-3-p-4": "Não basta usar um mensageiro criptografado de ponta-a-ponta, todos nós devemos usar os mensageiros que protegem a privacidade de nossas redes pessoais — com quem estamos conectados.", - "simplex-unique-overlay-card-1-p-2": "Para entregar mensagens, o SimpleX usa <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>endereços anônimos em pares</a> de filas de mensagens unidirecionais, separadas para mensagens recebidas e enviadas, geralmente por meio de servidores diferentes. Usar o SimpleX é como ter <strong>um e-mail ou telefone de “gravação” diferente para cada contato</strong>, sem a necessidade de gerenciá-los.", + "simplex-unique-overlay-card-1-p-2": "Para entregar mensagens, o SimpleX usa <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>endereços anônimos em pares</a> de filas de mensagens unidirecionais, separadas para mensagens recebidas e enviadas, geralmente por meio de servidores diferentes.", "simplex-unique-overlay-card-1-p-3": "Esse design protege a privacidade de quem está se comunicando com você, ocultando-a dos servidores da plataforma SimpleX e de quaisquer observadores. Para ocultar seu endereço IP dos servidores, você pode <strong>se conectar aos servidores do SimpleX via Tor</strong>.", "simplex-unique-overlay-card-2-p-2": "Mesmo com o endereço de usuário opcional, embora ele possa ser usado para enviar solicitações de contato de spam, você pode alterá-lo ou excluí-lo completamente sem perder nenhuma das suas conexões.", "simplex-unique-overlay-card-2-p-1": "Como você não tem um identificador na plataforma SimpleX, ninguém pode entrar em contato com você, a menos que compartilhe um endereço de usuário único ou temporário, como um QR code ou um link.", "simplex-unique-overlay-card-3-p-2": "As mensagens criptografadas de ponta-a-ponta são mantidas temporariamente nos servidores de retransmissão SimpleX até serem recebidas e, em seguida, são excluídas permanentemente.", - "simplex-unique-overlay-card-3-p-3": "Diferente dos servidores de redes federadas (e-mail, XMPP ou Matrix), os servidores SimpleX não armazenam contas de usuários, apenas retransmitem mensagens, protegendo a privacidade de ambas as partes.", + "simplex-unique-overlay-card-3-p-3": "Diferente dos servidores de redes federadas (email, XMPP ou Matrix), os servidores SimpleX não armazenam contas de usuários, apenas retransmitem mensagens, protegendo a privacidade de ambas as partes.", "simplex-unique-overlay-card-4-p-1": "Você pode <strong>usar o SimpleX com seus próprios servidores</strong> e ainda se comunicar com pessoas que usam os servidores pré-configurados fornecidos por nós.", "simplex-unique-card-1-p-1": "O SimpleX protege a privacidade do seu perfil, contatos e metadados, ocultando-os dos servidores da plataforma SimpleX e de quaisquer observadores.", "simplex-unique-card-1-p-2": "Diferente de qualquer outra plataforma de mensagens existente, o SimpleX não tem identificadores atribuídos aos usuários — <strong>nem mesmo números aleatórios</strong>.", @@ -145,10 +145,9 @@ "join-us-on-GitHub": "Junte-se a nós no GitHub", "donate-here-to-help-us": "Doe aqui para nos ajudar", "sign-up-to-receive-our-updates": "Inscreva-se para receber nossas atualizações", - "enter-your-email-address": "Digite seu endereço de e-mail", - "get-simplex": "Obtenha o SimpleX <a href=\"/downloads\">desktop app</a>", - "why-simplex-is": "Por que o SimpleX é", - "unique": "único", + "enter-your-email-address": "Digite seu endereço de email", + "get-simplex": "Obtenha o SimpleX <a href=\"/downloads\">aplicativo desktop</a>", + "why-simplex-is-unique": "Por que o SimpleX é <span class='gradient-text'>único</span>", "learn-more": "Saiba mais", "more-info": "Mais informações", "contact-hero-header": "Você recebeu um endereço para se conectar no SimpleX Chat", @@ -164,14 +163,14 @@ "guide-dropdown-2": "Enviando mensagens", "guide-dropdown-8": "Configurações do aplicativo", "guide-dropdown-9": "Fazendo conexões", - "docs-dropdown-4": "Servidor SMP de host", - "docs-dropdown-5": "Servidor XFTP de host", + "docs-dropdown-4": "Hospedar Servidor SMP", + "docs-dropdown-5": "Hospedar Servidor XFTP", "docs-dropdown-6": "Servidores WebRTC", - "docs-dropdown-7": "Traduzir o SimpleX Chat", + "docs-dropdown-7": "Traduza o SimpleX Chat", "use-this-command": "Use esse comando:", "github-repository": "Repositório no GitHub", "if-you-already-installed-simplex-chat-for-the-terminal": "Se você já instalou o SimpleX Chat para o terminal", - "copy-the-command-below-text": "copie o comando abaixo e use-o no chat:", + "copy-the-command-below-text": "copie o comando abaixo e use-o no bate-papo:", "privacy-matters-section-header": "Por que a privacidade é <span class='gradient-text'>importante</span>", "privacy-matters-section-label": "Certifique-se de que seu mensageiro não possa acessar seus dados!", "simplex-network-section-header": "<span class='gradient-text'>Rede</span> SimpleX", @@ -183,8 +182,8 @@ "simplex-network-1-header": "Diferente das redes P2P", "docs-dropdown-2": "Acessando arquivos do Android", "comparison-section-list-point-3": "Chave pública ou alguma outra ID globalmente exclusiva", - "comparison-section-list-point-4": "Se os servidores da operadora forem comprometidos", - "comparison-section-list-point-5": "Não protege os metadados dos usuários", + "comparison-section-list-point-4": "Se os servidores da operadora forem comprometidos. Verifique o código de segurança no Signal e outros aplicativos para mitigá-lo", + "comparison-section-list-point-5": "Não protege a privacidade da metadados dos usuários", "guide-dropdown-1": "Início rápido", "guide-dropdown-4": "Perfis de chat", "guide-dropdown-5": "Gerenciando dados", @@ -192,7 +191,7 @@ "guide-dropdown-7": "Privacidade e segurança", "guide": "Manual", "docs-dropdown-1": "Plataforma SimpleX", - "docs-dropdown-3": "Acesso ao banco de dados do chat", + "docs-dropdown-3": "Acessando o banco de dados do bate-papo", "on-this-page": "Nesta página", "newer-version-of-eng-msg": "Há uma versão mais recente desta página em inglês.", "click-to-see": "Clique para ver", @@ -232,5 +231,29 @@ "comparison-section-list-point-7": "As redes P2P têm uma autoridade central ou toda a rede pode ser comprometida", "protocol-2-text": "Matrix, XMPP", "see-here": "veja aqui", - "glossary": "Glossário" + "glossary": "Glossário", + "f-droid-page-simplex-chat-repo-section-text": "Para adicioná-lo ao seu cliente F-Droid, <span class='hide-on-mobile'>escaneie o QR code ou</span> use este URL:", + "comparison-section-list-point-4a": "Os relays SimpleX não podem comprometer a criptografia e2e. Verifique o código de segurança para mitigar ataques em canais fora de banda", + "please-use-link-in-mobile-app": "Use o link no aplicativo móvel", + "stable-and-beta-versions-built-by-developers": "Versões estáveis e beta criadas pelos desenvolvedores", + "signing-key-fingerprint": "Assinatura de impressão digital de chave (SHA-256)", + "simplex-chat-via-f-droid": "SimpleX Chat pelo F-Droid", + "simplex-chat-repo": "Repositório Simplex Chat", + "f-droid-org-repo": "Repositório F-Droid.org", + "stable-versions-built-by-f-droid-org": "Versões estáveis criadas por F-Droid.org", + "releases-to-this-repo-are-done-1-2-days-later": "Os lançamentos para este repositório são feitos 1 ou 2 dias depois", + "hero-overlay-3-textlink": "Avaliações de segurança", + "hero-overlay-3-title": "Avaliações de segurança", + "hero-overlay-card-3-p-1": "<a href=\"https://www.trailofbits.com/about/\">Trail of Bits</a> é uma consultoria líder em segurança e tecnologia cujos clientes incluem grandes empresas de tecnologia, agências governamentais e grandes projetos de blockchain.", + "hero-overlay-card-3-p-2": "Trail of Bits analisou a criptografia da plataforma SimpleX e os componentes de rede em novembro de 2022. Leia mais em <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">o anúncio</a>.", + "f-droid-page-f-droid-org-repo-section-text": "Os repositórios SimpleX Chat e F-Droid.org assinam compilações com chaves diferentes. Para mudar, <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>exporte</a> o banco de dados de bate-papo e reinstale o aplicativo.", + "please-enable-javascript": "Por favor habilite o JavaScript para ver o QR code.", + "jobs": "Junte-se à equipe", + "docs-dropdown-8": "Serviço de Diretório SimpleX", + "docs-dropdown-9": "Baixar", + "docs-dropdown-11": "FAQ", + "docs-dropdown-10": "Transparência", + "docs-dropdown-12": "Segurança", + "hero-overlay-card-3-p-3": "Trail of Bits revisou o design criptografico das redes utilizadas pelo SimpleX em julho de 2024", + "docs-dropdown-14": "SimpleX para negócios" } diff --git a/website/langs/ru.json b/website/langs/ru.json index 73fba95480..335b19ab11 100644 --- a/website/langs/ru.json +++ b/website/langs/ru.json @@ -1,254 +1,259 @@ { "copy-the-command-below-text": "скопируйте приведенную ниже команду и используйте ее в чате:", - "copyright-label": "© 2020-2023 SimpleX | Проект с открытым исходным кодом", + "copyright-label": "© 2020-2025 SimpleX | Проект с открытым исходным кодом", "chat-bot-example": "Пример Чат бота", "simplex-private-card-9-point-1": "Каждая очередь сообщений передает сообщения в одном направлении с разными адресами отправки и получения.", - "simplex-private-card-1-point-2": "Криптобокс NaCL в каждой очереди для предотвращения корреляции трафика между очередями сообщений, в случае компрометации TLS.", - "contact-hero-p-1": "Открытые ключи и адрес очереди сообщений по этой ссылке НЕ отправляются по сети при просмотре этой страницы — они содержатся в хэш-фрагменте URL-адреса ссылки.", + "simplex-private-card-1-point-2": "NaCL cryptobox в каждой очереди для предотвращения корреляции трафика между очередями сообщений, в случае компрометированного TLS.", + "contact-hero-p-1": "Публичные ключи и адрес очереди сообщений в этой ссылке НЕ отправляются по сети при просмотре этой страницы — они содержатся в хэш-фрагменте URL-адреса ссылки.", "guide-dropdown-5": "Управление данными", "scan-the-qr-code-with-the-simplex-chat-app": "Отсканируйте QR-код с помощью приложения SimpleX Chat", - "simplex-private-card-9-point-2": "Это уменьшает векторы атак и доступные метаданные, по сравнению с традиционными посредниками для доставки сообщений.", + "simplex-private-card-9-point-2": "Это уменьшает векторы атак и доступные метаданные, по сравнению с традиционными серверами доставки сообщений.", "simplex-unique-card-3-p-2": "Сквозные зашифрованные сообщения временно хранятся на серверах SimpleX до получения, после чего они удаляются безвозвратно.", - "feature-7-title": "Портативное, зашифрованное хранилище в приложении — можно перенести профиль на другое устройство", - "no-federated": "Нет - федеративный", - "hero-2-header": "Как начать общаться приватно", + "feature-7-title": "Зашифрованное база данных — Вы можете перенести профиль на другое устройство", + "no-federated": "Нет - федеративные", + "hero-2-header": "Установите конфиденциальное соединение", "simplex-unique-overlay-card-3-p-3": "В отличие от серверов федеративных сетей (электронной почты, XMPP или Matrix), серверы SimpleX не хранят учетные записи пользователей, они только ретранслируют сообщения, защищая конфиденциальность обеих сторон.", - "hero-subheader": "Первый мессенджер<br>не нуждающийся в идентификаторах<br>пользователя", - "privacy-matters-overlay-card-3-p-2": "Одна из самых шокирующих историй - это опыт <a href='https://ru.wikipedia.org/wiki/Слахи,_Мохаммед_Ульд' target='_blank'>Слахи, Мохаммеда Ульда</a>, описанный в его мемуарах и показанный в фильме <a href='https://ru.wikipedia.org/wiki/Мавританец' target='_blank'>Мавританец</a>. Он был помещен в лагерь Гуантанамо без суда и следствия и подвергался там пыткам в течение 15 лет после телефонного звонка своему родственнику в Афганистан под подозревается в причастности к терактам 11 сентября, хотя предыдущие 10 лет он жил в Германии.", - "signing-key-fingerprint": "Отпечаток ключа подписи (SHA-256)", + "hero-subheader": "Первый мессенджер<br>без идентификаторов пользователей", + "privacy-matters-overlay-card-3-p-2": "Одна из самых шокирующих историй — это опыт <a href='https://ru.wikipedia.org/wiki/Слахи,_Мохаммед_Ульд' target='_blank'>Слахи, Мохаммеда Ульда</a>, описанный в его мемуарах и показанный в фильме <a href='https://ru.wikipedia.org/wiki/Мавританец' target='_blank'>Мавританец</a>. Он был помещен в лагерь Гуантанамо без суда и следствия и подвергался там пыткам в течение 15 лет после телефонного звонка своему родственнику в Афганистане, из за подозрения в причастности к терактам 11 сентября, хотя предыдущие 10 лет он жил в Германии.", + "signing-key-fingerprint": "Идентификатор ключа подписи (SHA-256)", "simplex-network-2-desc": "Серверные узлы SimpleX НЕ хранят профили пользователей, контакты и доставленные сообщения, НЕ подключаются друг к другу, и НЕ имеют каталога серверов.", "simplex-privacy": "Конфиденциальность SimpleX", "docs-dropdown-5": "Свой XFTP Сервер", - "simplex-private-card-3-point-2": "Отпечаток сервера и привязка канала предотвращают MITM атаки и Атаки повторного воспроизведения.", + "simplex-private-card-3-point-2": "Идентификатор сервера и привязка к TLS сессии предотвращают атаки перехвата (MITM) и повторного использования.", "docs-dropdown-3": "Доступ к в базе данных чата", "installing-simplex-chat-to-terminal": "Установка SimpleX Chat в терминале", "use-this-command": "Используйте эту команду:", - "simplex-explained": "Простое объяснение вкратце", + "simplex-explained": "Как SimpleX работает", "to-make-a-connection": "Чтобы установить соединение:", - "comparison-section-list-point-6": "Хотя P2P распределены, они не являются федеративными, то есть P2P - работают как единая сеть", - "hero-overlay-2-textlink": "Как работает SimpleX?", + "comparison-section-list-point-6": "Хотя P2P распределены, они не являются федеративными, то есть P2P — работают как единая сеть", + "hero-overlay-2-textlink": "Как SimpleX работает?", "simplex-chat-via-f-droid": "SimpleX Chat в F-Droid", - "privacy-matters-overlay-card-1-p-1": "Многие крупные компании используют информацию о том, с кем вы связаны, чтобы оценить ваш доход, продавать вам больше товаров, которые вам на самом деле не нужны, и определять из этой информации, выгодные для них цены.", - "privacy-matters-1-overlay-1-title": "Конфиденциальность экономит ваши деньги", - "simplex-private-card-5-point-1": "SimpleX использует заполнение содержимого для каждого уровня шифрования, чтобы предотвратить атаки на размер сообщения.", - "privacy-matters-2-overlay-1-linkText": "Конфиденциальность дает вам власть", - "hero-overlay-3-title": "Оценка безопасности", - "enter-your-email-address": "Email адрес", - "simplex-explained-tab-1-text": "1. Как это видят пользователи", + "privacy-matters-overlay-card-1-p-1": "Многие крупные компании используют информацию о том, с кем Вы связаны, чтобы оценить Ваш доход, продавать Вам больше товаров, которые Вам на самом деле не нужны, и определять из этой информации, выгодные для них цены.", + "privacy-matters-1-overlay-1-title": "Конфиденциальность экономит Ваши деньги", + "simplex-private-card-5-point-1": "SimpleX использует дополнение содержания для каждого уровня шифрования, чтобы скрыть размер сообщений.", + "privacy-matters-2-overlay-1-linkText": "Конфиденциальность дает Вам власть", + "hero-overlay-3-title": "Аудит безопасности", + "enter-your-email-address": "Введите Ваш адрес Email", + "simplex-explained-tab-1-text": "1. Что видят пользователи", "tap-to-close": "Нажмите, чтобы закрыть", "simplex-unique-card-4-p-2": "Вы можете <strong>использовать SimpleX со своими собственными серверами</strong> или с серверами, предоставленными нами — и при этом подключаться к любому пользователю SimpleX.", - "hero-overlay-card-3-p-2": "В ноябре 2022 года Trail of Bits провела обзор криптографии и сетевых компонентов SimpleX.", - "feature-1-title": "Сообщения зашифрованные E2E-шифрованием<br>с поддержкой markdown и редактированием", + "hero-overlay-card-3-p-2": "В ноябре 2022 года Trail of Bits провела аудит криптографии и сетевых компонентов SimpleX. <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">Дополнительная информация</a>.", + "hero-overlay-card-3-p-3": "В июле 2024 года Trail of Bits провела аудит криптографического дизайна протоколов SimpleX. <a href=\"/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html\">Дополнительная информация</a>.", + "feature-1-title": "Сообщения с E2E-шифрованием<br>с форматированием и редактированием", "comparison-point-4-text": "Единая или Централизованная сеть", "guide-dropdown-9": "Установление соединений", - "simplex-unique-1-overlay-1-title": "Полная конфиденциальность вашей личности, профиля, контактов и других метаданных", - "hero-overlay-card-2-p-4": "SimpleX защищает от этих атак, поскольку в его конструкции нет никаких идентификаторов пользователей. И, если вы используете режим инкогнито, у вас будет другое отображаемое имя для каждого контакта, что позволит избежать какого-либо пересечения между ними.", - "privacy-matters-overlay-card-2-p-2": "Чтобы быть объективным и принимать независимые решения, вам необходимо контролировать свое информационное пространство. Это возможно только в том случае, если вы используете приватную, коммуникационную платформу, которая не имеет доступа к вашему социальному графу.", - "hero-overlay-card-2-p-1": "Когда у пользователей есть постоянные идентификаторы, даже если это просто случайное число, например идентификатор сеанса, существует риск того, что провайдер или злоумышленник могут наблюдайте за тем, как подключены пользователи и сколько сообщений они отправляют.", - "feature-3-title": "Децентрализованные группы — только<br>их участники знают, что они существуют", + "simplex-unique-1-overlay-1-title": "Полная конфиденциальность Вашей личности, профиля, контактов и метаданных", + "hero-overlay-card-2-p-4": "SimpleX защищает от этих атак, поскольку он не использует никакие идентификаторы профилей пользователей. И, если Вы используете режим инкогнито, у Вас будет другое отображаемое имя для каждого контакта, что позволит избежать какого-либо пересечения между ними.", + "privacy-matters-overlay-card-2-p-2": "Чтобы быть объективным и принимать независимые решения, необходимо контролировать свое информационное пространство. Это возможно только, если Вы используете конфиденциальную коммуникационную сеть, которая не имеет доступа к контактам Вашей социальной сети.", + "hero-overlay-card-2-p-1": "Когда у пользователя есть постоянный идентификатор, даже если это просто случайное число, например Session ID, существует риск того, что провайдер или злоумышленник могут наблюдать за тем, как пользователи соединены и сколько сообщений они отправляют.", + "feature-3-title": "Децентрализованные группы — известные только участникам", "glossary": "Глоссарий", "simplex-network-overlay-1-title": "Сравнение с протоколами обмена сообщениями P2P", - "comparison-section-list-point-7": "Сети P2P либо имеют центральный орган управления, либо вся сеть может быть скомпрометирована", - "simplex-unique-overlay-card-4-p-1": "Вы можете <strong>использовать SimpleX со своими собственными серверами</strong> или предоставленными нами серверами, при этом имея возможность общаться с любым пользователем.", - "simplex-explained-tab-3-p-1": "Серверы имеют отдельные, Анонимные учётные данные для каждой очереди и не знают, к каким пользователям они принадлежат.", - "docs-dropdown-1": "Платформа SimpleX", - "hero-overlay-card-1-p-5": "Только клиентские устройства хранят профили пользователей, контакты и группы; сообщения отправляются с двухуровневым, Сквозным шифрованием.", + "comparison-section-list-point-7": "Сети P2P либо имеют центральный компонент, либо вся сеть может быть атакована", + "simplex-unique-overlay-card-4-p-1": "Вы можете <strong>использовать SimpleX со своими собственными серверами</strong> или с серверами предустановленными в приложении, при этом имея возможность общаться с любым пользователем.", + "simplex-explained-tab-3-p-1": "Серверы имеют отдельные, анонимные учётные данные для каждой очереди и не знают, каким пользователям они принадлежат.", + "docs-dropdown-1": "Сеть SimpleX", + "hero-overlay-card-1-p-5": "Только клиентские устройства хранят профили пользователей, контакты и группы; сообщения отправляются с двухуровневым сквозным шифрованием.", "simplex-chat-for-the-terminal": "SimpleX Chat для терминала", - "simplex-network-overlay-card-1-li-3": "P2P не решает проблему <a href='https://ru.wikipedia.org/wiki/%D0%90%D1%82%D0%B0%D0%BA%D0%B0_%D0%BF%D0%BE%D1%81%D1%80%D0%B5%D0%B4%D0%BD%D0%B8%D0%BA%D0%B0'>MITM-атаки (Атака посредника)</a>, и большинство существующих реализаций не используют внеполосные сообщения для первоначального обмена ключами. SimpleX использует внеполосные сообщения или, в некоторых случаях, ранее существовавшие защищенные и доверенные соединения для первоначального обмена ключами.", + "simplex-network-overlay-card-1-li-3": "P2P не решает проблему <a href='https://ru.wikipedia.org/wiki/%D0%90%D1%82%D0%B0%D0%BA%D0%B0_%D0%BF%D0%BE%D1%81%D1%80%D0%B5%D0%B4%D0%BD%D0%B8%D0%BA%D0%B0'>MITM-атаки (Атака посредника)</a>, и многие сети не используют сообщения вне протокола для первоначального обмена ключами. SimpleX использует сообщения вне протокола или, в некоторых случаях, ранее существовавшие защищенные и доверенные соединения для первоначального обмена ключами.", "the-instructions--source-code": "SimpleX Chat.", "simplex-network-section-desc": "SimpleX Chat обеспечивает наилучшую конфиденциальность, сочетая преимущества P2P и федеративных сетей.", - "privacy-matters-section-subheader": "Сохранение конфиденциальности ваших метаданных — <span class='text-active-blue'>с кем вы общаетесь</span> — защищает вас от:", - "if-you-already-installed": "Если вы уже установили", - "simplex-explained-tab-3-p-2": "Пользователи могут еще больше повысить свою конфиденциальность скрыв свой IP-адрес, например используя сеть Tor для доступа к серверам.", - "join": "Присоединяйся к", - "privacy-matters-section-header": "Почему приватность <span class='gradient-text'>важна</span>", - "hero-overlay-1-textlink": "Почему идентификаторы пользователя - вредны для приватности?", + "privacy-matters-section-subheader": "Сохранение конфиденциальности Ваших метаданных — <span class='text-active-blue'>с кем Вы общаетесь</span> — защищает Вас от:", + "if-you-already-installed": "Если Вы уже установили", + "simplex-explained-tab-3-p-2": "Пользователи могут повысить свою конфиденциальность используя сеть Tor для доступа к серверам.", + "join": "Присоединяйтесь к", + "privacy-matters-section-header": "Почему конфиденциальность <span class='gradient-text'>важна</span>", + "hero-overlay-1-textlink": "Почему идентификаторы пользователя уменьшают конфиденциальность?", "on-this-page": "На этой странице", "privacy-matters-overlay-card-1-p-2": "Интернет-магазины знают, что люди с более низкими доходами с большей вероятностью совершают срочные покупки, поэтому они могут устанавливать более высокие цены или отменять скидки.", - "simplex-unique-3-overlay-1-title": "Контроль и безопасность ваших данных", + "simplex-unique-3-overlay-1-title": "Контроль и безопасность Ваших данных", "protocol-3-text": "Протоколы P2P", - "simplex-private-card-6-point-2": "Чтобы предотвратить это, приложения SimpleX передают одноразовые ключи внеполосно, когда вы делитесь адресом в виде ссылки или QR-кода.", + "simplex-private-card-6-point-2": "Чтобы предотвратить это, приложения SimpleX передают одноразовые ключи вне протокола, когда Вы делитесь адресом в виде ссылки или QR-кода.", "no": "Нет", - "contact-hero-header": "Вы получили адрес для подключения в SimpleX Chat", + "contact-hero-header": "Вы получили адрес контакта для соединения в SimpleX Chat", "feature-8-title": "Режим инкогнито —<br>уникальный для SimpleX Chat", - "why-simplex": "Что делает SimpleX уникальным", + "why-simplex": "Почему SimpleX", "simplex-private-card-4-point-2": "Чтобы использовать SimpleX через сеть Tor, пожалуйста, установите <a href=\"https://guardianproject.info/apps/org.torproject.android/\" target=\"_blank\">приложение Orbot</a> и включите прокси (или режим VPN <a href=\"https://apps.apple.com/us/app/orbot/id1609461599?platform=iphone\" target=\"_blank\">на iOS</a>).", - "contact-hero-subheader": "Отсканируйте QR-код с помощью приложения SimpleX Chat на вашем телефоне или планшете.", + "contact-hero-subheader": "Отсканируйте QR-код с помощью приложения SimpleX Chat на Вашем телефоне или планшете.", "simplex-unique-2-overlay-1-title": "Лучшая защита от спама и злоупотреблений", - "simplex-private-6-title": "Внеполосный<br>Обмен ключами", - "join-us-on-GitHub": "Присоединяйтесь к нам на GitHub", - "hero-overlay-card-3-p-3": "Подробнее читайте в <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">анонсе</a>.", + "simplex-private-6-title": "Обмен ключами<br>вне протокола", + "join-us-on-GitHub": "Присоединяйтесь на GitHub", "comparison-section-header": "Сравнение с другими протоколами", - "invitation-hero-header": "Вы получили одноразовую ссылку для подключения в SimpleX Chat", - "no-secure": "Нет - безопасно", - "hero-overlay-card-1-p-2": "Для доставки сообщений вместо идентификаторов пользователей, используемых всеми другими платформами, SimpleX использует временные, анонимные, попарные идентификаторы очередей сообщений, отдельно для каждого из ваших контактов — нет долгосрочных идентификаторов.", + "invitation-hero-header": "Вы получили одноразовую ссылку для соединения в SimpleX Chat", + "no-secure": "Нет - безопасный", + "hero-overlay-card-1-p-2": "Для доставки сообщений вместо идентификаторов пользователей, используемых другими сетями, SimpleX использует временные, анонимные, парные идентификаторы очередей сообщений, отдельные для каждого Вашего контакта — без долгосрочных идентификаторов.", "simplex-explained-tab-1-p-2": "Как это может работать с однонаправленными очередями и без идентификаторов профиля пользователя?", "simplex-network-1-header": "В отличие от P2P-сетей", "jobs": "Присоединиться к команде", "simplex-private-card-7-point-2": "Если какое-либо сообщение будет добавлено, удалено или изменено, получатель будет предупрежден об этом.", - "simplex-unique-3-title": "Только вы контролируете<br>свои данные", + "simplex-unique-3-title": "Только Вы контролируете<br>Ваши данные", "guide-dropdown-3": "Секретные группы", "no-resilient": "Нет - устойчив", - "hide-info": "Спрятать информацию", + "hide-info": "Скрыть информацию", "privacy-matters-overlay-card-3-p-4": "Недостаточно просто использовать мессенджер со сквозным шифрованием, мы все должны использовать мессенджеры, которые защищают конфиденциальность наших личных сетей — с какими людьми мы связаны.", - "releases-to-this-repo-are-done-1-2-days-later": "Выпуск новых версий в этом репозитории выходит с задержкой в 1-2 дня", + "releases-to-this-repo-are-done-1-2-days-later": "Новые версии в этом репозитории публикуются с задержкой в несколько дней", "comparison-point-1-text": "Требуется глобальный идентификатор", "comparison-section-list-point-5": "Не защищает конфиденциальность пользовательских метаданных", - "hero-overlay-card-2-p-2": "Затем они могли бы сопоставить эту информацию с существующими общедоступными социальными сетями и определить некоторые реальные личности.", - "privacy-matters-overlay-card-1-p-3": "Некоторые финансовые и страховые компании используют социальные графики для определения процентных ставок и премий. Это часто заставляет людей с более низкими доходами платить больше — это известно как <a href='https://fairbydesign.com/povertypremium/' target='_blank'>\"премия за бедность\"</a>.", + "hero-overlay-card-2-p-2": "Эта информация может быть сопоставлена с социальными сетями, чтобы определить реальные личности пользователей.", + "privacy-matters-overlay-card-1-p-3": "Некоторые финансовые и страховые компании используют социальные сети для определения процентных ставок и премий. Это часто заставляет людей с более низкими доходами платить больше — это известно как <a href='https://fairbydesign.com/povertypremium/' target='_blank'>\"наценка за бедность\"</a>.", "comparison-point-3-text": "Зависимость от DNS", "yes": "Да", "docs-dropdown-6": "Сервера WebRTC", "newer-version-of-eng-msg": "Существует более новая версия этой страницы на английском языке.", "install-simplex-app": "Установите приложение SimpleX", - "hero-overlay-3-textlink": "Оценка безопасности", + "hero-overlay-3-textlink": "Аудит безопасности", "comparison-point-2-text": "Возможность MITM", - "scan-the-qr-code-with-the-simplex-chat-app-description": "Открытые ключи и адрес очереди сообщений, указанные в этой ссылке, НЕ отправляются по сети при просмотре этой страницы —<br> они содержатся в хэш-фрагменте URL-адреса ссылки.", + "scan-the-qr-code-with-the-simplex-chat-app-description": "Публичные ключи и адрес очереди сообщений, указанные в этой ссылке, НЕ отправляются по сети при просмотре этой страницы —<br> они содержатся в хэш-фрагменте URL-адреса ссылки.", "guide-dropdown-8": "Настройки приложения", - "simplex-explained-tab-2-p-2": "Серверы передают сообщения только в одну сторону, не имея полной картины общения пользователя или подключений.", + "simplex-explained-tab-2-p-2": "Серверы передают сообщения только в одну сторону, не имея полной информации о разговорах и контактах пользователя.", "smp-protocol": "Протокол SMP", "open-simplex-app": "Откройте приложение SimpleX", "see-simplex-chat": "Инструкции по загрузке или компиляции SimpleX Chat из исходного кода приведены в", "terminal-cli": "Приложение для терминала (CLI)", "comparison-section-list-point-1": "Обычно требуется номера телефона, в некоторых случаях — имя пользователя", - "simplex-explained-tab-3-text": "3. Что видят сервера", - "github-repository": "репозиторий Github", + "simplex-explained-tab-3-text": "3. Что видят серверы", + "github-repository": "репозитории GitHub", "feature-5-title": "Исчезающие сообщения", "connect-in-app": "Подключитесь в приложении", "menu": "Меню", - "simplex-private-card-4-point-1": "Чтобы защитить свой IP-адрес вы можете подключаться к серверам через сеть Tor или какую-либо другую транспортную оверлейную сеть.", - "privacy-matters-3-title": "Судебное преследование не виновных", - "comparison-point-5-text": "Атака на центральный компонент или другая сетевая атака", + "simplex-private-card-4-point-1": "Чтобы защитить свой IP-адрес Вы можете подключаться к серверам через сеть Tor или какую-либо другую транспортную оверлейную сеть.", + "privacy-matters-3-title": "Преследование из-за невиновного общения", + "comparison-point-5-text": "Центральный компонент или атака на всю сеть", "click-to-see": "Нажать здесь, чтобы увидеть", - "donate-here-to-help-us": "Пожертвуйте здесь, чтобы помочь нам", + "donate-here-to-help-us": "Пожертвовать", "simplex-private-1-title": "2-уровневое<br>сквозное шифрование", - "simplex-unique-card-1-p-2": "В отличие от любой другой существующей платформы обмена сообщениями, SimpleX не имеет идентификаторов пользователей — <strong>нету даже случайных цифр</strong>.", - "privacy-matters-2-overlay-1-title": "Конфиденциальность дает вам власть", - "simplex-unique-overlay-card-2-p-2": "Хоть злоумышленники и могут использовать постоянный адрес для отправки нежелательных запросов или спама, вы можете легко его изменить или просто удалить, не теряя связи с уже установленными контактами.", + "simplex-unique-card-1-p-2": "В отличие от любой другой существующей сети обмена сообщениями, SimpleX не имеет идентификаторов пользователей — <strong>нет даже случайных цифр</strong>.", + "privacy-matters-2-overlay-1-title": "Конфиденциальность дает Вам власть", + "simplex-unique-overlay-card-2-p-2": "Хоть злоумышленники и могут использовать постоянный адрес для отправки нежелательных запросов или спама, Вы можете легко его изменить или просто удалить, не теряя связи с уже установленными контактами.", "simplex-unique-4-overlay-1-title": "Полностью децентрализованная — пользователи владеют сетью SimpleX", "guide-dropdown-2": "Отправка сообщений", - "simplex-network-overlay-card-1-li-5": "Все известные P2P-сети могут быть уязвимы для <a href='https://ru.wikipedia.org/wiki/Sybil_attack'>Атаки Сивиллы</a>, поскольку каждый узел доступен для обнаружения, и сеть работает как единое целое. Известные меры по его смягчению требуют либо централизованного компонента, либо дорогостоящего <a href='https://ru.wikipedia.org/wiki/Proof-of-work'>Proof-of-work</a>. Сеть SimpleX не имеет функционала по обмену серверами, она фрагментирована и работает как множество изолированных подсетей, из-за чего провести атаку по всей сети - невозможно.", - "simplex-private-2-title": "Дополнительный уровень<br>шифрования сервера", - "hero-overlay-card-1-p-4": "Такая конструкция предотвращает утечку любых пользовательских метаданных на уровне приложения. Для дальнейшего улучшения конфиденциальности и защиты вашего IP-адреса вы можете подключиться к серверам обмена сообщениями через сеть Tor.", + "simplex-network-overlay-card-1-li-5": "Все известные P2P-сети могут быть уязвимы для <a href='https://ru.wikipedia.org/wiki/Sybil_attack'>Атаки Сивиллы</a>, поскольку каждый узел доступен для обнаружения, и сеть работает как единое целое. Известные меры по уменьшению риска требуют либо централизованного компонента, либо дорогостоящего <a href='https://ru.wikipedia.org/wiki/Proof-of-work'>Proof-of-work</a>. Сеть SimpleX не позволяет обнаруживать серверы, она фрагментирована и работает как множество изолированных подсетей, из-за чего провести атаку на всю сеть невозможно.", + "simplex-private-2-title": "Дополнительный уровень<br>шифрования с сервером", + "hero-overlay-card-1-p-4": "Этот подход предотвращает утечку любых пользовательских метаданных на уровне приложения. Для дальнейшего улучшения конфиденциальности и защиты Вашего IP-адреса Вы можете подключиться к серверам обмена сообщениями через сеть Tor.", "f-droid-org-repo": "Репозиторий F-Droid.org", "guide-dropdown-4": "Профили чата", "simplex-network-2-header": "В отличие от федеративных сетей", - "see-here": "подробней тут", - "simplex-private-3-title": "Безопасный аутентифицированный<br>протокол TLS", - "comparison-section-list-point-3": "Открытый ключ или какой-либо другой глобально уникальный идентификатор", - "hero-overlay-card-2-p-3": "Даже в самых приватных приложениях, использующих скрытые сервисы Tor v3, если вы общаетесь с двумя разными контактами через один и тот же профиль, они могут доказать, что они являются связаны с одним и тем же человеком.", - "simplex-private-4-title": "Вариант доступа<br>через сеть Tor", + "see-here": "подробнее здесь", + "simplex-private-3-title": "Безопасное аутентифицированное<br>соединение TLS", + "comparison-section-list-point-3": "Публичный ключ или какой-либо другой глобально уникальный идентификатор", + "hero-overlay-card-2-p-3": "Даже в самых конфиденциальных приложениях, использующих скрытые адреса Tor, если Вы общаетесь с двумя разными контактами через один и тот же профиль, они могут доказать, что они связаны с одним и тем же человеком.", + "simplex-private-4-title": "Возможен доступ<br>через сеть Tor", "privacy-matters-1-title": "Реклама и ценовая дискриминация", "simplex-unique-card-3-p-1": "SimpleX хранит все пользовательские данные на клиентских устройствах в <strong>портативном формате зашифрованной базы данных</strong> — их можно перенести на другое устройство.", - "hero-overlay-1-title": "Как работает SimpleX?", + "hero-overlay-1-title": "Как SimpleX работает?", "stable-versions-built-by-f-droid-org": "Стабильные версии, созданные F-Droid.org", - "contact-hero-p-3": "Воспользуйтесь ссылками ниже, чтобы загрузить приложение.", + "contact-hero-p-3": "Используйте ссылки ниже, чтобы загрузить приложение.", "simplex-network": "Сеть SimpleX", - "privacy-matters-3-overlay-1-title": "Конфиденциальность защищает вашу свободу", + "privacy-matters-3-overlay-1-title": "Конфиденциальность защищает Вашу свободу", "docs-dropdown-7": "Перевести SimpleX Chat", "back-to-top": "Вернуться к началу", "simplex-network-1-desc": "Все сообщения отправляются через серверы, что обеспечивает лучшую конфиденциальность метаданных и надежную асинхронную доставку сообщений, избегая при этом многих", "simplex-chat-repo": "Репозиторий SimpleX Chat", "simplex-private-card-6-point-1": "Многие коммуникационные платформы уязвимы для MITM-атак со стороны серверов или сетевых провайдеров.", - "privacy-matters-3-overlay-1-linkText": "Конфиденциальность защищает вашу свободу", - "simplex-unique-overlay-card-1-p-2": "Для доставки сообщений SimpleX использует <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>попарные, анонимные адреса</a> однонаправленных очередей сообщений, раздельные для полученных и отправленных сообщений, обычно через разные серверы. Использование SimpleX это как иметь <strong>отдельный “одноразовый” адрес электронной почты или номер телефона для каждого контакта</strong>, при это, не обременяя вас управлять эти вручную.", - "simplex-unique-overlay-card-3-p-4": "Со стороны не видно разницу между отправлением или получением сообщений — если кто-то наблюдает за этим, он не cможет легко определить, кто с кем общается, даже если протокол TLS будет скомпрометирован.", + "privacy-matters-3-overlay-1-linkText": "Конфиденциальность защищает Вашу свободу", + "simplex-unique-overlay-card-1-p-2": "Для доставки сообщений SimpleX использует <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>попарные, анонимные адреса</a> однонаправленных очередей сообщений, раздельные для полученных и отправленных сообщений, обычно через разные серверы.", + "simplex-unique-overlay-card-3-p-4": "Со стороны не видно разницы между отправлением или получением сообщений — если кто-то наблюдает за этим, он не cможет легко определить, кто с кем общается, даже если протокол TLS будет скомпрометирован.", "docs-dropdown-2": "Доступ к файлам в версии для Android", - "get-simplex": "Скачать SimpleX <a href=\"/downloads/#desktop-app\">для ПК</a>", - "privacy-matters-overlay-card-3-p-1": "Каждый должен заботиться о конфиденциальности и безопасности своих коммуникаций — безобидные разговоры могут подвергнуть вас опасности, например за ваши политические взгляды, даже если кажется, что вам \"нечего скрывать\".", + "get-simplex": "Скачать SimpleX <a href=\"/downloads/#desktop-app\">для компьютера</a>", + "privacy-matters-overlay-card-3-p-1": "Каждый должен заботиться о конфиденциальности и безопасности своих коммуникаций — безобидные разговоры могут подвергнуть Вас опасности, например за Ваши политические взгляды, даже если кажется, что Вам \"нечего скрывать\".", "simplex-unique-2-title": "Вы защищены от<br>спама и злоупотреблений", - "simplex-unique-overlay-card-4-p-3": "Если вы рассматриваете возможность разработки платформе SimpleX, например, чат-бота для пользователей SimpleX или интеграции библиотеки SimpleX Chat в ваше мобильное приложение, пожалуйста, <a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>обращайтесь</a> за любыми советами и поддержкой.", + "simplex-unique-overlay-card-4-p-3": "Если Вы рассматриваете возможность разработки для сети SimpleX, например, чат-бота для пользователей SimpleX или интеграции библиотеки SimpleX Chat в Ваше мобильное приложение, пожалуйста, <a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>обращайтесь</a> за любыми советами и поддержкой.", "comparison-section-list-point-2": "Адреса на основе DNS", "stable-and-beta-versions-built-by-developers": "Стабильные и бета-версии, созданные разработчиками", "simplex-network-3-header": "Сеть SimpleX", - "simplex-unique-card-2-p-1": "Поскольку у вас нет идентификатора или фиксированного адреса на платформе SimpleX, никто не сможет связаться с вами, без вашего явного согласия, только если вы сами поделитесь адресом в виде QR-кода или ссылки.", - "hero-overlay-card-3-p-1": "<a href=\"https://www.trailofbits.com/about/\">Trail of Bits</a> - ведущая консалтинговая компания в области безопасности и технологий, клиентами которой являются крупные технологические компании, правительственные агентства и крупные блокчейн проекты.", - "hero-header": "Иной взгляд на приватность", + "simplex-unique-card-2-p-1": "Поскольку у Вас нет идентификатора или фиксированного адреса в сети SimpleX, никто не сможет связаться с Вами без Вашего явного согласия mdash; это возможно, только если Вы сами поделитесь адресом в виде QR-кода или ссылки.", + "hero-overlay-card-3-p-1": "<a href=\"https://www.trailofbits.com/about/\">Trail of Bits</a> — ведущая консалтинговая компания в области безопасности, клиентами которой являются крупные технологические компании, правительственные агентства и крупные блокчейн проекты.", + "hero-header": "Более конфиденциальный", "comparison-section-list-point-4": "Если операторы серверов скомпрометированы. В Signal, и некоторых других приложениях, есть возможность подтвердить код безопасности", - "simplex-private-card-2-point-1": "Дополнительный уровень серверного шифрования для доставки получателю, чтобы предотвратить корреляцию между полученным и отправленным трафиком сервера, если используемый протокол TLS скомпрометированный.", - "f-droid-page-simplex-chat-repo-section-text": "Чтобы добавить его в свой клиент F-Droid, <span class='hide-on-mobile'>отсканируйте QR-код или </span>воспользуйтесь этим URL-адресом:", - "guide-dropdown-7": "Приватность и безопасность", - "join-the-REDDIT-community": "Присоединяйтесь к нам в сообществе REDDIT", - "simplex-private-card-10-point-2": "Это позволяет доставлять сообщения без идентификаторов профиля пользователя, обеспечивая лучшую конфиденциальность метаданных, чем другие альтернативы.", + "simplex-private-card-2-point-1": "Дополнительный уровень шифрования между сервером и получателем для предотвращения корреляции полученного и отправленного трафика сервера, в случае компрометированного TLS.", + "f-droid-page-simplex-chat-repo-section-text": "Чтобы добавить его в свой клиент F-Droid, <span class='hide-on-mobile'>отсканируйте QR-код или </span>используйте этот адрес:", + "guide-dropdown-7": "Конфиденциальность и безопасность", + "join-the-REDDIT-community": "Присоединяйтесь в REDDIT", + "simplex-private-card-10-point-2": "Это позволяет доставлять сообщения без идентификаторов профиля пользователя, и обеспечивает лучшую конфиденциальность метаданных, чем альтернативы.", "privacy-matters-2-title": "Манипулирование выборами", - "home": "Домашняя страница", + "home": "Главная", "chat-protocol": "Протокол чата", - "simplex-private-card-5-point-2": "Это позволяет сообщениям разного размера выглядеть одинаково для серверов и сетевых наблюдателей.", - "hero-overlay-card-1-p-1": "Многие спрашивают:<em>Если у SimpleX нету никаких идентификаторов пользователя, то как приложение знает, куда доставлять сообщения?</em>", - "feature-6-title": "Зашифрованные E2E-шифрованием аудио и видео звонки", - "hero-p-1": "Другие приложения имеют ID своих пользователей: Signal, Matrix, Session, Briar, Jami, Cwtch и т. п.<br>SimpleX не имет, <strong>нету даже случайных цифр</strong>.<br>Это значительно повышает вашу приватность.", - "simplex-network-overlay-card-1-li-2": "В отличие от многих P2P сетей, SimpleX спроектирован так, чтобы не нуждаться в глобальных идентификаторов его пользователей, даже временных, используя только временные попарные идентификаторы, обеспечивая лучшую анонимность и защиту метаданных пользователя.", - "simplex-unique-4-title": "Только вы владеете<br>сетью SimpleX", + "simplex-private-card-5-point-2": "Это делает сообщениям разного размера одинаковыми для серверов и сети.", + "hero-overlay-card-1-p-1": "Многие спрашивают: <em>Если у SimpleX нет никаких идентификаторов пользователя, то как приложение знает, куда доставлять сообщения?</em>", + "feature-6-title": "Аудио и видео звонки с E2E-шифрованием", + "hero-p-1": "Другие приложения используют ID пользователей: Signal, Matrix, Session, Briar, Jami, Cwtch и т. п.<br>SimpleX не использует, <strong>даже случайных цифр</strong>.<br>Это значительно повышает Вашу конфиденциальность.", + "simplex-network-overlay-card-1-li-2": "В отличие от многих P2P сетей, SimpleX спроектирован так, чтобы не нуждаться в глобальных идентификаторах пользователей, даже временных, используя только временные попарные идентификаторы, обеспечивая лучшую анонимность и защиту метаданных пользователей.", + "simplex-unique-4-title": "Вы владеете<br>сетью SimpleX", "privacy-matters-overlay-card-3-p-3": "Обычных людей арестовывают за то, чем они делятся в Интернете, даже через свои \"анонимные\" аккаунты, <a href='https://www.dailymail.co.uk/news/article-11282263/Moment-police-swoop-house-devout-catholic-mother-malicious-online-posts.html' target='_blank'>даже в демократических странах</a>.", "simplex-unique-overlay-card-3-p-2": "Сквозные зашифрованные сообщения временно хранятся на серверах SimpleX до получения, после чего они удаляются безвозвратно.", - "blog": "Новости", - "simplex-private-card-7-point-1": "Чтобы гарантировать целостность, сообщения последовательно нумеруются и включают в себя хэш предыдущего сообщения.", - "simplex-unique-overlay-card-4-p-2": "Платформа SimpleX использует <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>открытый протокол</a> и предоставляет <a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>SDK для создания чат-ботов</a>, позволяя внедрять сервисы, с которыми пользователи могут взаимодействовать через приложение SimpleX Chat — мы с нетерпением ждем возможности увидеть, какие сервисы SimpleX вы сможете создать.", + "blog": "Блог", + "simplex-private-card-7-point-1": "Для обеспечения неизменности, сообщения нумеруются по порядку и содержат хэш предыдущего сообщения.", + "simplex-unique-overlay-card-4-p-2": "Сеть SimpleX использует <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>открытый протокол</a> и предоставляет <a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>SDK для создания чат-ботов</a>, позволяя внедрять сервисы, с которыми пользователи могут взаимодействовать через приложение SimpleX Chat — мы с нетерпением ждем сервисы SimpleX, которые Вы создадите.", "simplex-explained-tab-1-p-1": "Вы можете создавать контакты и группы, а также вести двусторонние беседы, как и в любом другом мессенджере.", "contact-hero-p-2": "Еще не скачали SimpleX Chat?", - "why-simplex-is": "Почему SimpleX", + "why-simplex-is-unique": "Почему SimpleX <span class='gradient-text'>уникальный</span>", "simplex-network-section-header": "<span class='gradient-text'>Сеть</span> SimpleX", "simplex-private-10-title": "Временные анонимные парные идентификаторы", - "privacy-matters-1-overlay-1-linkText": "Конфиденциальность экономит ваши деньги", + "privacy-matters-1-overlay-1-linkText": "Конфиденциальность экономит Ваши деньги", "tap-the-connect-button-in-the-app": "Нажмите на кнопку <span class='text-active-blue'>’подключиться’</span> в приложении", "comparison-section-list-point-4a": "Сервера SimpleX не могут скомпрометировать сквозное шифрование", - "unique": "уникальный", "simplex-network-1-overlay-linktext": "проблем P2P сетей", - "no-private": "Нет - приватно", - "simplex-unique-1-title": "У вас есть полная<br>конфиденциальность", + "no-private": "Нет - конфиденциальный", + "simplex-unique-1-title": "У Вас есть полная<br>конфиденциальность", "protocol-2-text": "XMPP, Matrix", "guide": "Руководство", - "simplex-network-overlay-card-1-li-4": "Реализации P2P могут быть заблокированы некоторыми интернет-провайдерами (например, <a href='https://ru.wikipedia.org/wiki/BitTorrent'>BitTorrent</a>). SimpleX не зависит от транспорта - он может работать по стандартным веб-протоколам, например WebSockets.", - "hero-overlay-2-title": "Почему идентификаторы пользователя - вредны для приватности?", + "simplex-network-overlay-card-1-li-4": "Реализации P2P могут быть заблокированы некоторыми интернет-провайдерами (например, <a href='https://ru.wikipedia.org/wiki/BitTorrent'>BitTorrent</a>). SimpleX протокол не зависит от транспорта — он может быть реализован через стандартные веб-протоколы, например WebSockets.", + "hero-overlay-2-title": "Почему идентификаторы пользователя уменьшают конфиденциальность?", "simplex-explained-tab-2-text": "2. Как это работает", "docs-dropdown-4": "Свой SMP Сервер", - "feature-4-title": "Зашифрованные E2E-шифрованием голосовые сообщения", - "privacy-matters-overlay-card-2-p-1": "Не так давно мы наблюдали, как <a href='https://en.wikipedia.org/wiki/Facebook–Cambridge_Analytica_data_scandal' target='_blank'>авторитетная консалтинговая компания</a> манипулировала крупными выборами, используя наши социальные графики для искажения нашего мнения из реального мира и манипулируют нашими голосами.", - "privacy-matters-overlay-card-2-p-3": "SimpleX - это первая платформа, которая по своей конструкции не имеет никаких идентификаторов пользователей, таким образом защищая график ваших контактов лучше, чем любая известная альтернатива.", - "learn-more": "Учить больше", + "feature-4-title": "Голосовые сообщения с E2E-шифрованием", + "privacy-matters-overlay-card-2-p-1": "Не так давно мы наблюдали, как <a href='https://en.wikipedia.org/wiki/Facebook–Cambridge_Analytica_data_scandal' target='_blank'>авторитетная консалтинговая компания</a> манипулировала крупными выборами, используя наши контакты в социальных сетях, чтобы изменять наши мнения и манипулировать нашими голосами.", + "privacy-matters-overlay-card-2-p-3": "SimpleX — это первая сеть, которая не имеет никаких идентификаторов пользователей, таким образом защищая Ваши контакты лучше, чем любая известная альтернатива.", + "learn-more": "Узнать больше", "donate": "Пожертвовать", "simplex-private-8-title": "Смешивание сообщений<br>для уменьшения корреляции", "scan-qr-code-from-mobile-app": "Отсканируйте QR-код в мобильном приложении", "simplex-private-card-3-point-3": "Возобновление соединения отключено для предотвращения сеансовых атак.", - "simplex-private-card-10-point-1": "SimpleX использует временные анонимные попарные адреса и учетные данные для каждого контакта пользователя или члена группы.", + "simplex-private-card-10-point-1": "SimpleX использует временные анонимные парные адреса и учетные данные для каждого контакта пользователя или члена группы.", "guide-dropdown-6": "Аудио и видео Звонки", - "more-info": "Больше информации", + "more-info": "Дополнительная информации", "no-decentralized": "Нет - децентрализованный", "protocol-1-text": "Signal, большие платформы", - "hero-2-header-desc": "В видео показано, как подключиться к своему другу с помощью его одноразового QR-кода, лично или по видеосвязи. Вы также можете подключиться, поделившись ссылкой-приглашением.", - "simplex-network-overlay-card-1-li-6": "Сети P2P могут быть уязвимы для <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'>DRDoS атаки</a>, когда клиенты могут ретранслировать и усиливать/увеличивать объём трафика, что приводит к отказу в обслуживании по всей сети. Клиенты SimpleX ретранслируют трафик только из известного соединения и не могут быть использованы злоумышленником для нагрузки трафика всей сети.", - "if-you-already-installed-simplex-chat-for-the-terminal": "Если вы уже установили SimpleX Chat для терминала", + "hero-2-header-desc": "В видео показано, как подключиться к Вашему другу через одноразовый QR-код, при встрече или во время видеосвязи. Вы также можете соединится, поделившись ссылкой-приглашением.", + "simplex-network-overlay-card-1-li-6": "Сети P2P могут быть уязвимы для <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'>DRDoS атаки</a>, когда клиенты могут ретранслировать и увеличивать трафик, что приводит к отказу всей сети. Клиенты SimpleX ретранслируют трафик только из известного соединения и не могут быть использованы злоумышленником для создания трафика во всей сети.", + "if-you-already-installed-simplex-chat-for-the-terminal": "Если Вы уже установили SimpleX Chat для терминала", "docs-dropdown-8": "Служба Каталогов SimpleX", - "simplex-private-card-1-point-1": "Протокол с двойным храповым механизмом —<br> обмен сообщениями OTR с идеальной секретностью пересылки и восстановлением после взлома.", + "simplex-private-card-1-point-1": "Протокол двойного обновления ключей —<br>\"отрицаемые\" сообщения с идеальной прямой секретностью и восстановлением после взлома", "simplex-private-card-8-point-1": "Серверы SimpleX действуют как узлы-миксеры с низкой задержкой — входящие и исходящие сообщения имеют разный порядок.", - "simplex-unique-overlay-card-2-p-1": "Поскольку у вас нет идентификатора на платформе SimpleX, никто не сможет связаться с вами, если вы сами не предоставите одноразовый или временный адрес в виде QR-кода или ссылки.", - "sign-up-to-receive-our-updates": "Введите ваш Email, чтобы получать рассылку обновлений от нас", + "simplex-unique-overlay-card-2-p-1": "Поскольку у Вас нет идентификатора в сети SimpleX, никто не сможет связаться с Вами, если Вы сами не предоставите одноразовый или временный адрес в виде QR-кода или ссылки.", + "sign-up-to-receive-our-updates": "Подпишитесь на нашу рассылку новостей", "guide-dropdown-1": "Быстрый старт", - "simplex-explained-tab-2-p-1": "Для каждого подключения вы используете две отдельные очереди обмена сообщениями, то есть отправка и получения сообщений происходит через разные серверы.", - "simplex-private-section-header": "Что делает SimpleX <span class='gradient-text'>приватным</span>", - "we-invite-you-to-join-the-conversation": "Мы приглашаем вас присоединиться к беседе", - "feature-2-title": "Изображения, видео и файлы<br>зашифрованные E2E-шифрованием", + "simplex-explained-tab-2-p-1": "Для каждого контакта Вы используете две отдельные очереди сообщений, отправляя и получая сообщения через разные серверы.", + "simplex-private-section-header": "Что делает SimpleX <span class='gradient-text'>конфиденциальным</span>", + "we-invite-you-to-join-the-conversation": "Мы приглашаем Вас присоединиться к разговору", + "feature-2-title": "Изображения, видео и файлы<br>с E2E-шифрованием", "simplex-private-9-title": "Однонаправленные<br>очереди сообщений", - "simplex-unique-overlay-card-1-p-3": "Этот дизайн защищает конфиденциальность того, с кем вы общаетесь, скрывая это от серверов SimpleX и от любых наблюдателей из вне. Чтобы скрыть свой IP-адрес от серверов, вы можете <strong>подключиться к серверам SimpleX через сеть Tor</strong>.", + "simplex-unique-overlay-card-1-p-3": "Этот дизайн защищает конфиденциальность Ваших контактов, скрывая их от серверов SimpleX и от любых внешних наблюдателей. Чтобы скрыть свой IP-адрес от серверов, Вы можете <strong>подключиться к серверам SimpleX через сеть Tor</strong>.", "developers": "Разработчики", - "simplex-private-7-title": "Проверка целостности<br>сообщения", - "privacy-matters-overlay-card-1-p-4": "Платформа SimpleX защищает конфиденциальность ваших контактов лучше, чем любая другая альтернатива, полностью предотвращая доступ к вашему социальному графику каким-либо компаниям или организациям. Даже когда люди используют серверы, предоставляемые SimpleX Chat, мы не знаем точное количество пользователей или с кем они общаются.", - "hero-overlay-card-1-p-6": "Подробнее читайте в <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>техническом документе SimpleX</a>.", + "simplex-private-7-title": "Проверка неизменности<br>сообщений", + "privacy-matters-overlay-card-1-p-4": "Сеть SimpleX защищает конфиденциальность Ваших контактов лучше, чем альтернативы, предотвращая доступ к Вашей социальной сети каким-либо компаниям или организациям. Даже когда люди используют серверы, предоставляемые SimpleX Chat, мы не знаем точное количество пользователей или с кем они общаются.", + "hero-overlay-card-1-p-6": "Подробнее читайте в <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>техническом описании SimpleX</a>.", "simplex-network-overlay-card-1-p-1": "Протоколы и приложения для обмена сообщениями <a href='https://ru.wikipedia.org/wiki/Peer-to-peer'>P2P</a> имеют различные проблемы, которые делают их менее надежными, чем SimpleX, более сложными для анализа и уязвимыми для нескольких типов атак.", - "terms-and-privacy-policy": "Условия & Политика Конфиденциальности", - "simplex-network-overlay-card-1-li-1": "Сети P2P полагаются на тот или иной вариант <a href='https://ru.wikipedia.org/wiki/Distributed_hash_table'>DHT</a> для маршрутизации сообщений. Проекты DHT должны обеспечивать баланс между гарантией доставки и задержкой. SimpleX имеет как лучшую гарантию доставки, так и меньшую задержку, чем P2P. В сетях P2P сообщение передается через нескольких узлов, последовательно, кол-во узлов-посредников будет расти параллельно размеру сети - <em>O(log N)</em>.", - "privacy-matters-section-label": "Убедитесь, что ваш мессенджер не может получить доступ к вашим данным!", + "terms-and-privacy-policy": "Политика Конфиденциальности", + "simplex-network-overlay-card-1-li-1": "Сети P2P используют <a href='https://ru.wikipedia.org/wiki/Distributed_hash_table'>DHT</a> (распределенные хэш-таблицы) для маршрутизации сообщений. DHT должны обеспечивать баланс между гарантией доставки и задержкой. SimpleX имеет как лучшую гарантию доставки, так и меньшую задержку, чем P2P. В сетях P2P сообщение передается через нескольких узлов, последовательно, кол-во узлов-посредников будет расти параллельно размеру сети — <em>O(log N)</em>.", + "privacy-matters-section-label": "Убедитесь, что Ваш мессенджер не может получить доступ к Вашим данным!", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat хранит все пользовательские данные на клиентских устройствах в <strong>портативном формате зашифрованной базы данных</strong> которую можно перенести на другое устройство.", "simplex-network-3-desc": "серверы предоставляют <span class='text-active-blue'>однонаправленные очереди</span> для подключения пользователей, но у них нет видимости графика сетевых подключений — это делают только пользователи.", - "simplex-unique-card-1-p-1": "SimpleX защищает конфиденциальность вашего профиля, контактов и метаданных, скрывая их от серверов платформы SimpleX и любых наблюдателей.", - "simplex-private-card-3-point-1": "Для соединений клиент-сервер используется только протокол TLS 1.2/1.3 с надежными алгоритмами.", - "simplex-unique-card-4-p-1": "Сеть SimpleX полностью децентрализована и независима от любой криптовалюты/блокчейна или любой другой платформы, кроме Интернета.", - "features": "Возможности", - "hero-overlay-card-1-p-3": "Вы определяете, какие серверы будете использовать для получения сообщений, а ваши контакты — серверы, которые вы используете для отправки им сообщений. Каждый новый чат, скорее всего, будет вестись на двух разных серверах.", + "simplex-unique-card-1-p-1": "SimpleX защищает конфиденциальность Вашего профиля, контактов и метаданных, скрывая их от серверов сети SimpleX и любых наблюдателей.", + "simplex-private-card-3-point-1": "Для соединений клиента и сервера используется только протокол TLS 1.2/1.3 с безопасными алгоритмами.", + "simplex-unique-card-4-p-1": "Сеть SimpleX полностью децентрализована и независима от любой криптовалюты/блокчейна или любой другой сети, кроме Интернета.", + "features": "Функции", + "hero-overlay-card-1-p-3": "Вы определяете, какие серверы использовать для получения сообщений, а Ваши контакты — серверы, которые Вы используете для отправки им сообщений. Каждый новый чат, скорее всего, будет вестись на двух разных серверах.", "docs-dropdown-9": "Скачать", "simplex-chat-protocol": "Протокол SimpleX Chat", - "simplex-unique-overlay-card-1-p-1": "В отличие от других платформ обмена сообщениями, SimpleX <strong>не имеет идентификаторов, присвоенных пользователям</strong>. Он не полагается на номера телефонов, доменные адреса (например, электронную почту или XMPP), имена пользователей, открытые ключи или даже случайные числа для идентификации своих пользователей — мы не знаем, сколько людей пользуются нашими SimpleX серверами.", - "reference": "Ссылки", - "f-droid-page-f-droid-org-repo-section-text": "Приложение SimpleX Chat от разработчиков и от репозитория F-Droid.org имеют разные ключи подписи. Если вы хотите сменить одно на другое, вам сначала нужно будет <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>экспортировать</a> базу данных и только потом скачать другое приложение.", - "simplex-private-5-title": "Многоуровневое<br>Заполнения содержимого" + "simplex-unique-overlay-card-1-p-1": "В отличие от других сетей обмена сообщениями, SimpleX <strong>не имеет идентификаторов, присвоенных пользователям</strong>. Он не полагается на номера телефонов, доменные адреса (например, электронную почту или XMPP), имена пользователей, публичные ключи или даже случайные числа для идентификации своих пользователей — операторы серверов SimpleX не знают, сколько людей пользуются их серверами.", + "reference": "Документы", + "f-droid-page-f-droid-org-repo-section-text": "Приложение SimpleX Chat от разработчиков и от репозитория F-Droid.org имеют разные ключи подписи. Если Вы хотите сменить одно на другое, Вам сначала нужно будет <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>экспортировать</a> базу данных и только потом скачать другое приложение.", + "simplex-private-5-title": "Многоуровневое<br>дополнение сообщений", + "please-use-link-in-mobile-app": "Пожалуйста, используйте ссылку в мобильном приложении", + "please-enable-javascript": "Пожалуйста, включите JavaScript, чтобы увидеть QR-код.", + "docs-dropdown-10": "Запросы данных", + "docs-dropdown-12": "Безопасность", + "docs-dropdown-11": "Часто задаваемые вопросы", + "docs-dropdown-14": "SimpleX для бизнеса" } diff --git a/website/langs/uk.json b/website/langs/uk.json index 6a48cc5640..1c1780edb3 100644 --- a/website/langs/uk.json +++ b/website/langs/uk.json @@ -1,246 +1,259 @@ { "features": "Особливості", "simplex-explained-tab-3-text": "3. Що бачать сервери", - "terms-and-privacy-policy": "Умови та політика конфіденційності", - "feature-4-title": "E2E-зашифровані голосові повідомлення", - "feature-5-title": "Зникаючі повідомлення", - "simplex-private-card-3-point-3": "Відновлення підключення вимкнено для запобігання атакам сеансів.", - "simplex-private-card-7-point-1": "Щоб гарантувати цілісність, повідомлення послідовно нумеруються і включають хеш попереднього повідомлення.", - "simplex-private-card-10-point-2": "Він дозволяє доставляти повідомлення без ідентифікаторів профілю користувача, забезпечуючи кращу конфіденційність метаданих, ніж альтернативи.", - "privacy-matters-2-overlay-1-linkText": "Конфіденційність дає вам силу", - "hero-overlay-card-1-p-5": "Тільки клієнтські пристрої зберігають профілі користувачів, контакти та групи; повідомлення надсилаються з двошаровим наскрізним шифруванням.", - "hero-overlay-card-2-p-1": "Коли користувачі мають постійні ідентифікатори, навіть якщо це просто випадкове число, наприклад, ідентифікатор сеансу, існує ризик, що провайдер або зловмисник може спостерігати, як користувачі підключаються і скільки повідомлень вони надсилають.", - "simplex-network-overlay-card-1-li-5": "Всі відомі P2P-мережі можуть бути вразливими до <a href='https://en.wikipedia.org/wiki/Sybil_attack'>атаки Sybil</a>, оскільки кожен вузол можна виявити, а мережа працює як єдине ціле. Відомі заходи для її пом'якшення вимагають або централізованого компоненту, або дорогого <a href='https://en.wikipedia.org/wiki/Proof_of_work'>доказу роботи</a>. Мережа SimpleX не має можливості виявлення серверів, вона фрагментована і працює як кілька ізольованих підмереж, що унеможливлює атаки на всю мережу.", - "simplex-network-overlay-card-1-li-6": "P2P-мережі можуть бути вразливими до <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'>DRDoS-атаки</a>, коли клієнти можуть повторно транслювати та збільшувати трафік, що призводить до відмови в обслуговуванні всієї мережі. Клієнти SimpleX лише ретранслюють трафік із відомого з’єднання, і зловмисники не можуть використовувати їх для посилення трафіку в усій мережі.", - "privacy-matters-overlay-card-1-p-2": "Інтернет-магазини знають, що люди з нижчими доходами більш схильні до термінових покупок, тому вони можуть встановлювати вищі ціни або скасовувати знижки.", - "privacy-matters-overlay-card-1-p-3": "Деякі фінансові та страхові компанії використовують соціальні графіки для визначення процентних ставок і премій. Це часто змушує людей з нижчими доходами платити більше —, він відомий як <a href='https://fairbydesign.com/povertypremium/' target='_blank'>' премія за бідність'</a>.", - "privacy-matters-overlay-card-1-p-4": "Платформа SimpleX захищає конфіденційність ваших з'єднань краще, ніж будь-яка інша альтернатива, повністю запобігаючи тому, щоб ваш соціальний графік став доступним будь-яким компаніям або організаціям. Навіть коли люди використовують сервери, надані SimpleX Chat, ми не знаємо кількість користувачів та їхніх з'єднань.", - "privacy-matters-overlay-card-2-p-1": "Не так давно ми стали свідками маніпуляцій на великих виборах з боку <a href='https://en.wikipedia.org/wiki/Facebook–Cambridge_Analytica_data_scandal' target='_blank'>авторитетної консалтингової компанії</a>, яка використовувала наші соціальні графіки, щоб спотворити наше уявлення про реальний світ і маніпулювати нашими голосами.", - "privacy-matters-overlay-card-2-p-2": "Щоб бути об'єктивним і приймати незалежні рішення, необхідно контролювати свій інформаційний простір. Це можливо лише за умови використання приватної комунікаційної платформи, яка не має доступу до вашого соціального профілю.", - "privacy-matters-overlay-card-2-p-3": "SimpleX - це перша платформа, яка не має жодних ідентифікаторів користувачів, таким чином захищаючи ваш граф зв'язків краще, ніж будь-яка інша відома альтернатива.", - "privacy-matters-overlay-card-3-p-1": "Кожен має дбати про конфіденційність і безпеку своїх комунікацій — нешкідливі розмови можуть наражати вас на небезпеку, навіть якщо вам нема чого приховувати.", - "privacy-matters-overlay-card-3-p-2": "Однією з найбільш шокуючих історій є досвід <a href='https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi' target='_blank'>Мохамеду Ульд Салахі</a>, описаний у його мемуарах та показаний у фільмі \"Мавританець\". Його без суду і слідства помістили в табір Гуантанамо і катували там протягом 15 років після телефонного дзвінка родичу в Афганістані за підозрою в причетності до терактів 11 вересня, хоча він жив у Німеччині протягом останніх 10 років.", - "privacy-matters-overlay-card-3-p-4": "Недостатньо використовувати наскрізний зашифрований месенджер, ми всі повинні використовувати месенджери, які захищають конфіденційність наших особистих мереж — тих, з ким ми пов'язані.", - "simplex-unique-overlay-card-1-p-1": "На відміну від інших платформ обміну повідомленнями, SimpleX не має <strong>ніяких ідентифікаторів, призначених користувачам</strong>. Він не покладається на телефонні номери, доменні адреси (наприклад, електронну пошту або XMPP), імена користувачів, відкриті ключі або навіть випадкові числа для ідентифікації своїх користувачів — ми не ' знаємо, скільки людей користуються нашими серверами SimpleX.", - "simplex-unique-overlay-card-3-p-3": "На відміну від серверів об'єднаних мереж (електронна пошта, XMPP або Matrix), сервери SimpleX не зберігають облікові записи користувачів, вони лише пересилають повідомлення, захищаючи конфіденційність обох сторін.", - "simplex-unique-card-3-p-2": "Наскрізні зашифровані повідомлення тимчасово зберігаються на релейних серверах SimpleX до моменту отримання, після чого вони назавжди видаляються.", - "simplex-unique-card-4-p-1": "Мережа SimpleX повністю децентралізована і не залежить від будь-якої криптовалюти або будь-якої іншої платформи, окрім Інтернету.", - "simplex-network-overlay-card-1-li-3": "P2P не вирішує <a href='https://en.wikipedia.org/wiki/Man-in-the-middle_attack'>атаки MITM</a> проблема, і більшість існуючих реалізацій не використовують позасмугові повідомлення для початкового обміну ключами. SimpleX використовує позасмугові повідомлення або, в деяких випадках, уже існуючі безпечні та надійні з’єднання для початкового обміну ключами.", - "simplex-network-overlay-card-1-li-4": "Реалізації P2P можуть бути заблоковані деякими інтернет-провайдерами (наприклад, <a href='https://en.wikipedia.org/wiki/BitTorrent'>BitTorrent</a>). SimpleX є транспортно-агностичним - він може працювати через стандартні веб-протоколи, наприклад, WebSockets.", - "simplex-unique-card-4-p-2": "Ви можете <strong>використовувати SimpleX з власними серверами</strong> або з серверами, наданими нами — і при цьому підключатися до будь-якого користувача.", - "contact-hero-p-1": "Відкриті ключі та адреса черги повідомлень у цьому посиланні НЕ передаються мережею при перегляді цієї сторінки — вони містяться в хеш-фрагменті URL-адреси посилання.", - "scan-qr-code-from-mobile-app": "Відскануйте QR-код з мобільного додатку", - "to-make-a-connection": "Щоб встановити зв'язок:", - "install-simplex-app": "Встановіть додаток SimpleX", + "terms-and-privacy-policy": "Політика конфіденційності", + "feature-4-title": "Голосові повідомлення з шифруванням від кінця до кінця", + "feature-5-title": "Зникнення повідомлень", + "simplex-private-card-3-point-3": "Відновлення з'єднання вимкнено для запобігання атакам на сесію.", + "simplex-private-card-7-point-1": "Для гарантії цілісності повідомлення пронумеровані послідовно та містять хеш попереднього повідомлення.", + "simplex-private-card-10-point-2": "Це дозволяє доставляти повідомлення без ідентифікаторів профілю користувача та забезпечує кращу конфіденційність метаданих, ніж альтернативи.", + "privacy-matters-2-overlay-1-linkText": "Конфіденційність дає вам владу", + "hero-overlay-card-1-p-5": "Тільки пристрої клієнта зберігають профілі користувачів, контакти та групи; повідомлення відправляються із шифруванням від кінця до кінця з двома шарами.", + "hero-overlay-card-2-p-1": "Коли у користувачів є постійні ідентифікатори, навіть якщо це просто випадковий номер, як, наприклад, ідентифікатор сеансу, існує ризик того, що провайдер чи зловмисник може спостерігати, як користувачі підключені та скільки повідомлень вони відправляють.", + "simplex-network-overlay-card-1-li-5": "Всі відомі P2P-мережі можуть бути вразливими до атаки <a href='https://en.wikipedia.org/wiki/Sybil_attack'>Sybil</a>, оскільки кожен вузол є відкритим, і мережа працює як ціле. Відомі заходи для пом'якшення цієї атаки вимагають або централізованого компонента, або дорогого <a href='https://en.wikipedia.org/wiki/Proof_of_work'>доказу роботи</a>. Мережа SimpleX не має виявності сервера, вона роздроблена і працює як кілька ізольованих підмереж, що робить неможливими атаки на всю мережу.", + "simplex-network-overlay-card-1-li-6": "P2P-мережі можуть бути вразливими до атаки <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'>DRDoS</a>, коли клієнти можуть ребродкастити та збільшувати трафік, що призводить до відмови в обслуговуванні на рівні всієї мережі. Клієнти SimpleX лише пересилають трафік від відомого підключення і не можуть бути використані зловмисником для збільшення трафіку в усій мережі.", + "privacy-matters-overlay-card-1-p-2": "Інтернет-роздрібники знають, що люди з низьким доходом частіше роблять термінові покупки, тому вони можуть встановлювати вищі ціни або скасовувати знижки.", + "privacy-matters-overlay-card-1-p-3": "Деякі фінансові та страхові компанії використовують соціальні графи для визначення ставок та страхових премій. Це часто змушує людей з низькими доходами платити більше — це відомо як <a href='https://fairbydesign.com/povertypremium/' target='_blank'>'поширений преміум'</a>.", + "privacy-matters-overlay-card-1-p-4": "Мережа SimpleX краще захищає конфіденційність ваших з'єднань, ніж будь-яка інша альтернатива, повністю запобігаючи тому, щоб ваш соціальний граф став доступним для будь-яких компаній чи організацій. Навіть коли люди використовують сервери, попередньо налаштовані в додатках SimpleX Chat, оператори серверів не знають кількості користувачів або їхніх з'єднань.", + "privacy-matters-overlay-card-2-p-1": "Не так давно ми спостерігали, як великі вибори маніпулювалися <a href='https://en.wikipedia.org/wiki/Facebook–Cambridge_Analytica_data_scandal' target='_blank'>поважною консалтинговою компанією</a>, яка використовувала наші соціальні графи для спотворення нашого уявлення про реальний світ та маніпулювання нашими голосами.", + "privacy-matters-overlay-card-2-p-2": "Щоб бути об'єктивним та приймати незалежні рішення, вам потрібно контролювати ваш інформаційний простір. Це можливо лише за умови використання приватної мережі для спілкування, яка не має доступу до вашого соціального графа.", + "privacy-matters-overlay-card-2-p-3": "SimpleX - це перша мережа, яка не має жодних ідентифікаторів користувачів за своїм дизайном, таким чином, захищаючи ваш графік з'єднань краще, ніж будь-яка відома альтернатива.", + "privacy-matters-overlay-card-3-p-1": "Кожен повинен турбуватися про конфіденційність та безпеку свого спілкування — безпечні розмови можуть поставити вас під загрозу, навіть якщо у вас немає чого приховувати.", + "privacy-matters-overlay-card-3-p-2": "Однією з найшокуючих історій є досвід <a href='https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi' target='_blank'>Мохамеду Ульд Слахі</a>, описаний в його мемуарах і показаний у фільмі \"Мавританець\". Його посадили в табір Гуантанамо без суду і мукили там 15 років після телефонного дзвінка йому родичеві в Афганістані, під підозрою в причетності до атак 11 вересня, навіть не дивлячись на те, що він проживав у Німеччині протягом попередніх 10 років.", + "privacy-matters-overlay-card-3-p-4": "Недостатньо використовувати зашифрований від кінця до кінця месенджер; ми всі повинні використовувати месенджери, які захищають конфіденційність наших особистих мереж — з ким ми з'єднані.", + "simplex-unique-overlay-card-1-p-1": "На відміну від інших мереж для обміну повідомленнями, у SimpleX <strong>немає ідентифікаторів, призначених користувачам</strong>. Він не покладається на номери телефонів, адреси на основі доменів (як електронна пошта або XMPP), імена користувачів, публічні ключі або навіть випадкові числа для ідентифікації своїх користувачів — оператори серверів SimpleX не знають, скільки людей використовують їхні сервери.", + "simplex-unique-overlay-card-3-p-3": "На відміну від федеративних мереж серверів (електронна пошта, XMPP або Matrix), сервери SimpleX не зберігають облікові записи користувачів, вони лише ретранслюють повідомлення, захищаючи приватність обох сторін.", + "simplex-unique-card-3-p-2": "Зашифровані повідомлення від кінця до кінця тимчасово зберігаються на ретрансляційних серверах SimpleX до їх отримання, після чого вони назавжди видаляються.", + "simplex-unique-card-4-p-1": "Мережа SimpleX повністю децентралізована та незалежна від будь-якої криптовалюти або будь-якої іншої мережі, окрім Інтернету.", + "simplex-network-overlay-card-1-li-3": "P2P не вирішує проблему атаки <a href='https://en.wikipedia.org/wiki/Man-in-the-middle_attack'>MITM</a>, і більшість існуючих реалізацій не використовують поза каналом повідомлень для початкового обміну ключами. SimpleX використовує поза каналом повідомлень або, у деяких випадках, передбачені і безпечні з'єднання для початкового обміну ключами.", + "simplex-network-overlay-card-1-li-4": "Реалізації P2P можуть бути заблоковані деякими інтернет-провайдерами (наприклад, <a href='https://en.wikipedia.org/wiki/BitTorrent'>BitTorrent</a>). SimpleX є незалежним від транспорту — він може працювати через стандартні веб-протоколи, наприклад, WebSockets.", + "simplex-unique-card-4-p-2": "Ви можете <strong>використовувати SimpleX з власними серверами</strong> або з серверами, які ми надаємо — і все одно підключатися до будь-якого користувача.", + "contact-hero-p-1": "Публічні ключі та адреса черги повідомлень в цьому посиланні НЕ відправляються по мережі під час перегляду цієї сторінки — вони містяться в хеш-фрагменті URL-посилання.", + "scan-qr-code-from-mobile-app": "Сканувати QR-код з мобільного додатка", + "to-make-a-connection": "Щоб здійснити підключення:", + "install-simplex-app": "Встановити додаток SimpleX", "github-repository": "Репозиторій GitHub", - "no-federated": "Ні - федеративний", - "comparison-section-list-point-1": "Зазвичай на основі номера телефону, в деяких випадках на основі імені користувача", - "comparison-section-list-point-2": "Адреси на основі DNS", - "comparison-section-list-point-3": "Відкритий ключ або інший глобально унікальний ідентифікатор", - "comparison-section-list-point-7": "Мережі P2P або мають центральний орган, або вся мережа може бути скомпрометована", + "no-federated": "Ні - федеративно", + "comparison-section-list-point-1": "Зазвичай базується на номері телефону, у деяких випадках на іменах користувачів", + "comparison-section-list-point-2": "Адреси, засновані на DNS", + "comparison-section-list-point-3": "Відкритий ключ чи який-небудь інший глобально унікальний ідентифікатор", + "comparison-section-list-point-7": "Мережі P2P мають або центральний орган управління, або всю мережу можна порушити", "see-here": "дивіться тут", "guide-dropdown-4": "Профілі чату", - "guide-dropdown-5": "Керування даними", - "guide-dropdown-6": "Аудіо і відео дзвінки", - "guide-dropdown-7": "Конфіденційність і безпека", - "guide-dropdown-8": "Налаштування програми", - "guide-dropdown-9": "Встановлення зв'язків", - "guide": "Путівник", - "docs-dropdown-1": "Платформа SimpleX", + "guide-dropdown-5": "Управління даними", + "guide-dropdown-6": "Аудіо та відеовиклики", + "guide-dropdown-7": "Конфіденційність та безпека", + "guide-dropdown-8": "Налаштування додатка", + "guide-dropdown-9": "Створення підключень", + "guide": "Посібник", + "docs-dropdown-1": "SimpleX мережа", "docs-dropdown-2": "Доступ до файлів Android", "docs-dropdown-3": "Доступ до бази даних чату", - "docs-dropdown-4": "Хост SMP-сервер", - "docs-dropdown-5": "Хост XFTP-сервер", + "docs-dropdown-4": "Хостинг сервера SMP", + "docs-dropdown-5": "Хостинг сервера XFTP", "docs-dropdown-6": "Сервери WebRTC", - "docs-dropdown-7": "Перекласти чат SimpleX", - "newer-version-of-eng-msg": "Існує новіша версія цієї сторінки англійською мовою.", + "docs-dropdown-7": "Переклад SimpleX Chat", + "newer-version-of-eng-msg": "Є нова версія цієї сторінки англійською мовою.", "click-to-see": "Натисніть, щоб побачити", "menu": "Меню", "on-this-page": "На цій сторінці", - "back-to-top": "Повернутися до початку", + "back-to-top": "Повернутися наверх", "home": "Головна", "developers": "Розробники", "reference": "Посилання", "blog": "Блог", "why-simplex": "Чому SimpleX", - "simplex-privacy": "Конфіденційність SimpleX", + "simplex-privacy": "Приватність у SimpleX", "simplex-network": "Мережа SimpleX", - "simplex-explained": "Simplex пояснення", + "simplex-explained": "Пояснення SimpleX", "simplex-explained-tab-1-text": "1. Що відчувають користувачі", "simplex-explained-tab-2-text": "2. Як це працює", - "simplex-explained-tab-1-p-1": "Ви можете створювати контакти та групи, а також вести двосторонні розмови, як і в будь-якому іншому месенджері.", - "simplex-explained-tab-1-p-2": "Як він може працювати з односпрямованими чергами та без ідентифікаторів профілю користувача?", - "simplex-explained-tab-2-p-1": "Для кожного підключення використовуються дві окремі черги обміну повідомленнями для надсилання та отримання повідомлень через різні сервери.", - "simplex-explained-tab-2-p-2": "Сервери передають повідомлення лише в один бік, не маючи повної картини розмови або з'єднань користувача.", - "simplex-explained-tab-3-p-1": "Сервери мають окремі анонімні облікові дані для кожної черги і не знають, яким користувачам вони належать.", - "simplex-explained-tab-3-p-2": "Користувачі можуть ще більше підвищити конфіденційність метаданих, використовуючи Tor для доступу до серверів, запобігаючи кореляції за IP-адресою.", + "simplex-explained-tab-1-p-1": "Ви можете створювати контакти та групи і вести двосторонні розмови, як у будь-якому іншому месенджері.", + "simplex-explained-tab-1-p-2": "Як це може працювати з однобічними чергами та без ідентифікаторів профілю користувача?", + "simplex-explained-tab-2-p-1": "Для кожного з'єднання ви використовуєте дві окремі черги обміну повідомленнями для відправки та отримання повідомлень через різні сервери.", + "simplex-explained-tab-2-p-2": "Сервери передають повідомлення тільки в одному напрямку, не маючи повної картини розмов або підключень користувача.", + "simplex-explained-tab-3-p-1": "Сервери мають окремі анонімні облікові дані для кожної черги і не знають, які користувачі до них відносяться.", + "simplex-explained-tab-3-p-2": "Користувачі можуть додатково підвищити конфіденційність метаданих, використовуючи Tor для доступу до серверів, що запобігає кореляції за IP-адресою.", "chat-bot-example": "Приклад чат-бота", - "smp-protocol": "SMP протокол", + "smp-protocol": "Протокол SMP", "chat-protocol": "Протокол чату", - "donate": "Пожертвуйте", - "copyright-label": "© 2020-2023 SimpleX | Проект з відкритим вихідним кодом", + "donate": "Пожертвувати", + "copyright-label": "© 2020-2025 SimpleX | Проект з відкритим кодом", "simplex-chat-protocol": "Протокол чату SimpleX", "terminal-cli": "Термінал CLI", - "hero-header": "Конфіденційність переглянута", + "hero-header": "Приватність переосмислена", "hero-subheader": "Перший месенджер<br>без ідентифікаторів користувачів", - "hero-p-1": "Інші додатки мають ідентифікатори користувачів: Signal, Matrix, Session, Briar, Jami, Cwtch тощо. <br> SimpleX цього не робить, <strong>навіть випадкових чисел</strong>. <br> Це радикально покращує вашу конфіденційність.", - "hero-overlay-1-textlink": "Чому ідентифікатори користувачів шкодять конфіденційності?", + "hero-p-1": "У інших додатках є ідентифікатори користувачів: Signal, Matrix, Session, Briar, Jami, Cwtch, тощо.<br> У SimpleX немає жодних, <strong>навіть випадкових номерів</strong>.<br> Це радикально покращує вашу конфіденційність.", + "hero-overlay-1-textlink": "Чому ідентифікатори користувачів шкідливі для конфіденційності?", "hero-overlay-2-textlink": "Як працює SimpleX?", "hero-2-header": "Створіть приватне з'єднання", - "hero-2-header-desc": "У відео показано, як з'єднатися з другом за допомогою одноразового QR-коду, особисто або за допомогою відеозв'язку. Ви також можете підключитися, поділившись посиланням-запрошенням.", + "hero-2-header-desc": "У відео показано, як ви можете підключитися до свого друга за його одноразовим QR-кодом, особисто або через відеозв'язок. Ви також можете підключитися, поділившись посиланням на запрошення.", "hero-overlay-1-title": "Як працює SimpleX?", - "hero-overlay-2-title": "Чому ідентифікатори користувачів шкодять конфіденційності?", - "feature-1-title": "E2E-зашифровані повідомлення з розміткою і редагуванням", - "feature-2-title": "E2E-зашифровані<br>зображення та файли", - "feature-3-title": "Децентралізовані секретні групи —<br>лише користувачі знають про їх існування", - "feature-6-title": "E2E-шифрується<br>audio і відеодзвінки", - "feature-7-title": "Портативна зашифрована база даних — перенесіть свій профіль на інший пристрій", + "hero-overlay-2-title": "Чому ідентифікатори користувачів шкідливі для конфіденційності?", + "feature-1-title": "Повідомлення з шифруванням від кінця до кінця з можливістю використання Markdown та редагування", + "feature-2-title": "Зображення, відео та файли з шифруванням від кінця до кінця", + "feature-3-title": "Децентралізовані групи з шифруванням від кінця до кінця — тільки користувачі знають про їх існування", + "feature-6-title": "Дзвінки з шифруванням від кінця до кінця для аудіо та відео", + "feature-7-title": "Переносне зашифроване зберігання додатку — переміщуйте профіль на інший пристрій", "feature-8-title": "Режим інкогніто —<br>унікальний для SimpleX Chat", - "simplex-network-overlay-1-title": "Порівняння з протоколами обміну повідомленнями P2P", - "simplex-private-1-title": "2-рівневе наскрізне шифрування", + "simplex-network-overlay-1-title": "Порівняння з протоколами P2P-повідомлень", + "simplex-private-1-title": "2 рівні шифрування від кінця до кінця", "simplex-private-2-title": "Додатковий рівень<br>шифрування сервера", - "simplex-private-3-title": "Безпечне автентифіковане<br>транспортування TLS", - "simplex-private-4-title": "Додатковий<br>доступ через Tor", - "simplex-private-5-title": "Кілька шарів<br>заповнення вмісту", - "simplex-private-6-title": "Позадіапазонний<br>обмін ключами", + "simplex-private-3-title": "Безпечний аутентифікований<br>транспорт TLS", + "simplex-private-4-title": "Опційний<br>доступ через Tor", + "simplex-private-5-title": "Кілька рівнів<br>наповнення вмісту", + "simplex-private-6-title": "Позаканальний<br>обмін ключами", "simplex-private-7-title": "Перевірка<br>цілісності повідомлення", - "simplex-private-9-title": "Односпрямовані<br>черги повідомлень", + "simplex-private-9-title": "Однобічні<br>черги повідомлень", "simplex-private-8-title": "Змішування повідомлень<br>для зменшення кореляції", "simplex-private-10-title": "Тимчасові анонімні парні ідентифікатори", - "simplex-private-card-1-point-1": "Протокол double-ratchet —<br>OTR messaging з ідеальною конфіденційністю та відновленням зламів.", - "simplex-private-card-1-point-2": "Криптобокс NaCL у кожній черзі для запобігання кореляції трафіку між чергами повідомлень, якщо TLS скомпрометовано.", - "simplex-private-card-2-point-1": "Додатковий рівень серверного шифрування для доставки одержувачу, щоб запобігти кореляції між отриманим і відправленим серверним трафіком, якщо TLS скомпрометовано.", - "simplex-private-card-3-point-1": "Для клієнт-серверних з'єднань використовується тільки TLS 1.2/1.3 з сильними алгоритмами.", - "simplex-private-card-3-point-2": "Відбитки пальців сервера та прив'язка каналів запобігають MITM-атакам та повторному відтворенню.", - "simplex-private-card-4-point-1": "Щоб захистити свою IP-адресу, ви можете отримати доступ до серверів через Tor або іншу транспортну мережу.", - "simplex-private-card-4-point-2": "Щоб використовувати SimpleX через Tor, установіть <a href=\"https://guardianproject.info/apps/org.torproject.android/\" target=\"_blank\">програму Orbot</a> і ввімкніть проксі-сервер SOCKS5 (або VPN <a href=\"https://apps.apple.com/us/app/orbot/id1609461599?platform=iphone\" target=\"_blank\">на iOS</a>).", - "simplex-private-card-5-point-1": "SimpleX використовує заповнення вмісту для кожного рівня шифрування, щоб запобігти атакам на розмір повідомлення.", - "simplex-private-card-5-point-2": "Це дозволяє повідомленням різного розміру виглядати однаково для серверів і мережевих спостерігачів.", - "simplex-private-card-6-point-1": "Багато комунікаційних платформ вразливі до MITM-атак з боку серверів або мережевих провайдерів.", - "simplex-private-card-6-point-2": "Щоб запобігти цьому, програми SimpleX передають одноразові ключі поза смугою, коли ви надаєте адресу як посилання або QR-код.", - "simplex-private-card-7-point-2": "Якщо будь-яке повідомлення буде додано, видалено або змінено, одержувач отримає сповіщення.", - "simplex-private-card-8-point-1": "Сервери SimpleX працюють як вузли змішування з низькою затримкою - вхідні та вихідні повідомлення йдуть в різному порядку.", - "simplex-private-card-9-point-1": "Кожна черга повідомлень передає повідомлення в одному напрямку, з різними адресами відправлення та отримання.", - "simplex-private-card-9-point-2": "У порівнянні з традиційними брокерами повідомлень, він зменшує кількість векторів атак і доступних мета-даних.", - "simplex-private-card-10-point-1": "SimpleX використовує тимчасові анонімні попарні адреси та облікові дані для кожного контакту користувача або члена групи.", - "privacy-matters-1-title": "Реклама та цінова дискримінація", - "privacy-matters-1-overlay-1-title": "Конфіденційність економить ваші гроші", - "privacy-matters-1-overlay-1-linkText": "Конфіденційність економить ваші гроші", + "simplex-private-card-1-point-1": "Протокол подвійної рейки — OTR-повідомлення з ідеальною передачею секрету та відновленням злому.", + "simplex-private-card-1-point-2": "NaCL криптокоробка в кожній черзі для запобігання кореляції трафіку між чергами повідомлень у разі компрометації TLS.", + "simplex-private-card-2-point-1": "Додатковий рівень шифрування сервера для доставки отримувачу для запобігання кореляції між отриманим та відісланим трафіком сервера у разі компрометації TLS.", + "simplex-private-card-3-point-1": "Використовується лише TLS 1.2/1.3 із сильними алгоритмами для з'єднань клієнт-сервер.", + "simplex-private-card-3-point-2": "Відбиток сервера та зв'язування каналу запобігають MITM та атакам повтору.", + "simplex-private-card-4-point-1": "Для захисту вашої IP-адреси ви можете отримати доступ до серверів через Tor чи іншу транспортну оверлейну мережу.", + "simplex-private-card-4-point-2": "Для використання SimpleX через Tor, будь ласка, встановіть <a href=\"https://guardianproject.info/apps/org.torproject.android/\" target=\"_blank\">додаток Orbot</a> та активуйте SOCKS5-проксі (або VPN <a href=\"https://apps.apple.com/us/app/orbot/id1609461599?platform=iphone\" target=\"_blank\">на iOS</a>).", + "simplex-private-card-5-point-1": "SimpleX використовує наповнення вмісту для кожного шару шифрування для ускладнення атак за розміром повідомлень.", + "simplex-private-card-5-point-2": "Це забезпечує, що повідомлення різних розмірів виглядають однаково для серверів та спостерігачів мережі.", + "simplex-private-card-6-point-1": "Багато комунікаційних мереж вразливі до MITM-атак серверів чи постачальників мережі.", + "simplex-private-card-6-point-2": "Щоб запобігти цьому, програми SimpleX передають одноразові ключі позаканально, коли ви ділитесь адресою як посиланням або QR-кодом.", + "simplex-private-card-7-point-2": "Якщо будь-яке повідомлення додається, вилучається чи змінюється, отримувач буде проінформований.", + "simplex-private-card-8-point-1": "Сервери SimpleX виступають як вузли низької затримки для змішування — вхідні та вихідні повідомлення мають різний порядок.", + "simplex-private-card-9-point-1": "Кожна черга повідомлень передає повідомлення в одному напрямку, з різними адресами для відправки та отримання.", + "simplex-private-card-9-point-2": "Це зменшує вектори атак порівняно із традиційними брокерами повідомлень та доступними метаданими.", + "simplex-private-card-10-point-1": "SimpleX використовує тимчасові анонімні парні адреси та облікові дані для кожного користувача, контакту чи учасника групи.", + "privacy-matters-1-title": "Реклама та дискримінація ціни", + "privacy-matters-1-overlay-1-title": "Конфіденційність зекономить вам гроші", + "privacy-matters-1-overlay-1-linkText": "Конфіденційність зекономить вам гроші", "privacy-matters-2-title": "Маніпулювання виборами", - "privacy-matters-2-overlay-1-title": "Конфіденційність дає вам силу", - "privacy-matters-3-title": "Переслідування через невинну асоціацію", + "privacy-matters-2-overlay-1-title": "Конфіденційність дає вам владу", + "privacy-matters-3-title": "Переслідування за невинним зв'язком", "privacy-matters-3-overlay-1-title": "Конфіденційність захищає вашу свободу", "privacy-matters-3-overlay-1-linkText": "Конфіденційність захищає вашу свободу", "simplex-unique-1-title": "Ви маєте повну конфіденційність", - "simplex-unique-1-overlay-1-title": "Повна конфіденційність вашої особи, профілю, контактів і метаданих", - "simplex-unique-2-title": "Ви захищені<br>від спаму та зловживань", + "simplex-unique-1-overlay-1-title": "Повна конфіденційність вашої ідентичності, профілю, контактів та метаданих", + "simplex-unique-2-title": "Вас захищено<br>від спаму та зловживань", "simplex-unique-2-overlay-1-title": "Найкращий захист від спаму та зловживань", "simplex-unique-3-title": "Ви контролюєте свої дані", - "simplex-unique-3-overlay-1-title": "Право власності, контроль та безпека ваших даних", - "simplex-unique-4-title": "Ви володієте мережею SimpleX", + "simplex-unique-3-overlay-1-title": "Власність, контроль та безпека ваших даних", + "simplex-unique-4-title": "Ви власник мережі SimpleX", "simplex-unique-4-overlay-1-title": "Повністю децентралізована — користувачі володіють мережею SimpleX", - "hero-overlay-card-1-p-1": "Багато користувачів запитували: <em>якщо SimpleX не має ідентифікаторів користувачів, як він може знати, куди доставляти повідомлення?</em>", - "hero-overlay-card-1-p-2": "Для доставки повідомлень замість ідентифікаторів користувачів, які використовуються всіма іншими платформами, SimpleX використовує тимчасові анонімні парні ідентифікатори черг повідомлень, окремі для кожного з ваших з'єднань — немає ніяких довгострокових ідентифікаторів.", - "hero-overlay-card-1-p-3": "Ви визначаєте, який сервер(и) використовувати для отримання повідомлень, ваші контакти — сервери, які ви використовуєте для надсилання їм повідомлень. У кожній розмові, швидше за все, будуть використовуватися два різних сервери.", - "hero-overlay-card-1-p-4": "Такий дизайн запобігає витоку будь-яких метаданих користувачів на рівні програми. Щоб ще більше підвищити конфіденційність і захистити свою IP-адресу, ви можете підключитися до серверів обміну повідомленнями через Tor.", - "hero-overlay-card-1-p-6": "Читайте більше в <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>Пропонуємо ознайомитися з технічним документом SimpleX</a>.", - "hero-overlay-card-2-p-2": "Потім вони могли співвіднести цю інформацію з існуючими публічними соціальними мережами і встановити деякі реальні особи.", - "hero-overlay-card-2-p-3": "Навіть у найбільш приватних додатках, які використовують сервіси Tor v3, якщо ви розмовляєте з двома різними контактами через один і той самий профіль, вони можуть довести, що пов'язані з однією і тією ж людиною.", - "hero-overlay-card-2-p-4": "SimpleX захищає від цих атак, не використовуючи жодних ідентифікаторів користувачів. А якщо ви використовуєте режим \"Інкогніто\", то для кожного контакту ви будете мати окреме ім'я для відображення, що дозволить уникнути обміну даними між ними.", - "simplex-network-overlay-card-1-p-1": "<a href='https://en.wikipedia.org/wiki/Peer-to-peer'>Протоколи та програми обміну повідомленнями P2P</a> мають різні проблеми, які роблять їх менш надійними, ніж SimpleX, складнішими для аналізу та вразливими до кількох типів атак.", - "simplex-network-overlay-card-1-li-1": "Мережі P2P покладаються на певний варіант <a href='https://en.wikipedia.org/wiki/Distributed_hash_table'>DHT</a> для маршрутизації повідомлень. Дизайн DHT має балансувати між гарантією доставки та затримкою. SimpleX має кращу гарантію доставки і меншу затримку, ніж P2P, тому що повідомлення може бути передано через кілька серверів паралельно, використовуючи сервери, обрані одержувачем. У P2P-мережах повідомлення передається через <em>O(log N)</em> вузлів послідовно, використовуючи вузли, обрані алгоритмом.", - "simplex-network-overlay-card-1-li-2": "Дизайн SimpleX, на відміну від більшості мереж P2P, не має жодних глобальних ідентифікаторів користувачів, навіть тимчасових, а використовує тимчасові ідентифікатори лише парами, забезпечуючи кращу анонімність і захист метаданих.", - "privacy-matters-overlay-card-1-p-1": "Багато великих компаній використовують інформацію про те, з ким ви пов'язані, щоб оцінити ваш дохід, продати вам товари, які вам насправді не потрібні, і визначити ціни.", - "privacy-matters-overlay-card-3-p-3": "Звичайних людей заарештовують за те, чим вони діляться в Інтернеті, навіть через свої 'анонімні' облікові записи, <a href='https://www.dailymail.co. uk/news/article-11282263/Moment-police-swoop-house-devout-catholic-mother-malicious-online-posts.html' target='_blank'>навіть у демократичних країнах</a>.", - "simplex-unique-overlay-card-1-p-2": "Для доставки повідомлень SimpleX використовує <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>парні анонімні адреси</a> односпрямованих черг повідомлень, окремо для отриманих і відправлених повідомлень, зазвичай через різні сервери. Використання SimpleX - це все одно, що мати <strong>різну “конфорку” електронну пошту або телефон для кожного контакту</strong>, і не мати клопоту з управлінням ними.", - "simplex-unique-overlay-card-1-p-3": "Такий дизайн захищає конфіденційність того, з ким ви спілкуєтеся, приховуючи її від серверів платформи SimpleX і від будь-яких спостерігачів. Щоб приховати свою IP-адресу від серверів, ви можете <strong>підключитися до серверів SimpleX через Tor</strong>.", - "simplex-unique-overlay-card-2-p-1": "Оскільки у вас немає ідентифікатора на платформі SimpleX, ніхто не може зв'язатися з вами, якщо ви не надасте одноразову або тимчасову адресу користувача у вигляді QR-коду або посилання.", - "simplex-unique-overlay-card-2-p-2": "Навіть якщо необов'язкова адреса користувача може бути використана для надсилання спам-повідомлень, ви можете змінити або повністю видалити її, не втрачаючи при цьому жодного з ваших зв'язків.", - "simplex-unique-overlay-card-3-p-1": "SimpleX Chat зберігає всі дані користувача лише на клієнтських пристроях за допомогою <strong>портативного формату зашифрованої бази даних</strong>, який можна експортувати та передавати на будь-який підтримуваний пристрій.", - "simplex-unique-overlay-card-3-p-2": "Наскрізні зашифровані повідомлення тимчасово зберігаються на релейних серверах SimpleX до моменту отримання, після чого вони назавжди видаляються.", - "simplex-unique-overlay-card-3-p-4": "Між відправленим і отриманим трафіком сервера немає спільних ідентифікаторів або зашифрованого тексту — якщо хтось спостерігає за ним, він не зможе легко визначити, хто з ким спілкується, навіть якщо TLS скомпрометований.", - "simplex-unique-overlay-card-4-p-1": "Ви можете <strong>використовувати SimpleX з власними серверами</strong> і при цьому спілкуватися з людьми, які використовують попередньо налаштовані сервери, надані нами.", - "simplex-unique-overlay-card-4-p-2": "Платформа SimpleX використовує <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>відкритий протокол</a> та надає <a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>SDK для створення чат-ботів</a>, що дозволяє реалізовувати сервіси, з якими користувачі можуть взаємодіяти через додатки SimpleX Chat —. Ми'з нетерпінням чекаємо на те, які сервіси SimpleX ви можете створити.", - "simplex-unique-overlay-card-4-p-3": "Якщо ви розглядаєте можливість розробки для платформи SimpleX, наприклад, чат-бота для користувачів додатків SimpleX або інтеграції бібліотеки SimpleX Chat у ваші мобільні додатки, будь ласка, <a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>зв'яжіться з нами</a> для отримання будь-якої консультації та підтримки.", - "simplex-unique-card-1-p-1": "SimpleX захищає конфіденційність вашого профілю, контактів і метаданих, приховуючи їх від серверів платформи SimpleX і будь-яких спостерігачів.", - "simplex-unique-card-1-p-2": "На відміну від будь-якої іншої існуючої платформи обміну повідомленнями, SimpleX не має ніяких ідентифікаторів, призначених користувачам — <strong>, навіть випадкових чисел</strong>.", - "simplex-unique-card-2-p-1": "Оскільки у вас немає ідентифікатора або фіксованої адреси на платформі SimpleX, ніхто не може зв'язатися з вами, якщо ви не надасте одноразову або тимчасову адресу користувача, наприклад, у вигляді QR-коду або посилання.", - "simplex-unique-card-3-p-1": "SimpleX зберігає всі дані користувача на клієнтських пристроях у <strong>портативному форматі зашифрованої бази даних </strong> —, його можна перенести на інший пристрій.", - "join": "Приєднуйтесь", - "we-invite-you-to-join-the-conversation": "Запрошуємо вас долучитися до розмови", + "hero-overlay-card-1-p-1": "Багато користувачів запитували: <em>якщо у SimpleX немає ідентифікаторів користувачів, як він може знати, куди відправити повідомлення?</em>", + "hero-overlay-card-1-p-2": "Для доставки повідомлень, замість ідентифікаторів користувачів, які використовують усі інші мережі, SimpleX використовує тимчасові анонімні парні ідентифікатори черг повідомлень, окремі для кожного вашого підключення — тут немає довгострокових ідентифікаторів.", + "hero-overlay-card-1-p-3": "Ви визначаєте, який(кі) сервер(и) використовувати для отримання повідомлень, ваші контакти — сервери, які ви використовуєте для відправки повідомлень їм. Кожна розмова, ймовірно, використовує два різних сервери.", + "hero-overlay-card-1-p-4": "Цей дизайн запобігає витоку метаданих будь-яких користувачів на рівні додатка. Для подальшого покращення конфіденційності та захисту вашої IP-адреси ви можете підключитися до серверів обміну повідомленнями через Tor.", + "hero-overlay-card-1-p-6": "Докладніше читайте у <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>білетному запису SimpleX</a>.", + "hero-overlay-card-2-p-2": "Вони можуть потім корелювати цю інформацію з існуючими громадськими соціальними мережами та визначати деякі реальні ідентифікатори.", + "hero-overlay-card-2-p-3": "Навіть з найбільш приватними додатками, які використовують служби Tor v3, якщо ви розмовляєте з двома різними контактами через той самий профіль, вони можуть довести, що вони підключені до однієї й тієї ж особи.", + "hero-overlay-card-2-p-4": "SimpleX захищає від цих атак, не маючи жодних ідентифікаторів користувачів в своєму дизайні. І, якщо ви використовуєте режим інкогніто, у вас буде різне відображення для кожного контакту, уникнення будь-яких спільних даних між ними.", + "simplex-network-overlay-card-1-p-1": "<a href='https://en.wikipedia.org/wiki/Peer-to-peer'>Протоколи та додатки для P2P-зв'язку</a> мають різні проблеми, які роблять їх менш надійними порівняно із SimpleX, складнішими для аналізу та вразливими до кількох типів атак.", + "simplex-network-overlay-card-1-li-1": "P2P-мережі покладаються на якусь варіацію <a href='https://en.wikipedia.org/wiki/Distributed_hash_table'>розподіленої хеш-таблиці (DHT)</a> для маршрутизації повідомлень. Дизайни DHT повинні балансувати гарантію доставки та затримку. У SimpleX є як краща гарантія доставки, так і менша затримка порівняно із P2P, оскільки повідомлення може передаватися паралельно кількома серверами, використовуючи сервери, обрані отримувачем. У P2P-мережах повідомлення проходить через вузли <em>O(log N)</em> послідовно, використовуючи вузли, обрані алгоритмом.", + "simplex-network-overlay-card-1-li-2": "У дизайні SimpleX, на відміну від більшості P2P-мереж, немає жодних глобальних ідентифікаторів користувачів будь-якого виду, навіть тимчасових, та використовуються лише тимчасові парні ідентифікатори, що забезпечує кращу анонімність та захист метаданих.", + "privacy-matters-overlay-card-1-p-1": "Багато великих компаній використовують інформацію про те, з ким ви з'єднані, щоб оцінити ваш дохід, продавати вам продукти, які вам дійсно не потрібні, і визначати ціни.", + "privacy-matters-overlay-card-3-p-3": "Звичайних людей арештовують за те, що вони публікують онлайн, навіть через свої 'анонімні' облікові записи, <a href='https://www.dailymail.co.uk/news/article-11282263/Moment-police-swoop-house-devout-catholic-mother-malicious-online-posts.html' target='_blank'>навіть у демократичних країнах</a>.", + "simplex-unique-overlay-card-1-p-2": "Для доставки повідомлень SimpleX використовує <a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>парні анонімні адреси</a> однобічних черг повідомлень, окремо для отриманих та відправлених повідомлень, зазвичай через різні сервери.", + "simplex-unique-overlay-card-1-p-3": "Цей дизайн захищає конфіденційність того, з ким ви спілкуєтесь, приховуючи це від серверів мережі SimpleX та від будь-яких спостерігачів. Щоб приховати вашу IP-адресу від серверів, ви можете <strong>підключатися до серверів SimpleX через Tor</strong>.", + "simplex-unique-overlay-card-2-p-1": "Оскільки у вас немає ідентифікатора в мережі SimpleX, ніхто не може зв'язатися з вами, якщо ви не поділитеся одноразовою або тимчасовою адресою користувача, як QR-кодом або посиланням.", + "simplex-unique-overlay-card-2-p-2": "Навіть з необов'язковою адресою користувача, яка може бути використана для відправки спамових запитань на зв'язок, ви можете змінити або повністю видалити її, не втрачаючи жодного з ваших з'єднань.", + "simplex-unique-overlay-card-3-p-1": "SimpleX Chat зберігає всі дані користувачів лише на пристроях клієнтів за допомогою <strong>переносного зашифрованого формату бази даних</strong>, який можна експортувати і передавати на будь-який підтримуваний пристрій.", + "simplex-unique-overlay-card-3-p-2": "Зашифровані повідомлення від кінця до кінця тимчасово зберігаються на ретрансляційних серверах SimpleX до їх отримання, після чого вони назавжди видаляються.", + "simplex-unique-overlay-card-3-p-4": "Між надісланим і отриманим серверним трафіком немає спільних ідентифікаторів чи шифрованого тексту — якщо хтось його спостерігає, він не може легко визначити, хто спілкується з ким, навіть якщо TLS скомпрометовано.", + "simplex-unique-overlay-card-4-p-1": "Ви можете <strong>використовувати SimpleX зі своїми власними серверами</strong> і при цьому спілкуватися з людьми, які використовують сервери, попередньо налаштовані в додатках.", + "simplex-unique-overlay-card-4-p-2": "Мережа SimpleX використовує <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>відкритий протокол</a> і надає <a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>SDK для створення чат-ботів</a>, що дозволяє реалізувати сервіси, з якими користувачі можуть взаємодіяти через додатки SimpleX Chat — ми справді чекаємо, щоб побачити, які сервіси SimpleX ви створите.", + "simplex-unique-overlay-card-4-p-3": "Якщо ви плануєте розробляти для мережі SimpleX, наприклад, чат-бота для користувачів додатка SimpleX або інтеграцію бібліотеки SimpleX Chat у ваші мобільні додатки, будь ласка, <a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>зв'яжіться з нами</a> для отримання порад та підтримки.", + "simplex-unique-card-1-p-1": "SimpleX захищає конфіденційність вашого профілю, контактів та метаданих, приховуючи їх від серверів мережі SimpleX та будь-яких спостерігачів.", + "simplex-unique-card-1-p-2": "На відміну від будь-якої іншої існуючої мережі для обміну повідомленнями, у SimpleX немає ідентифікаторів, призначених користувачам — <strong>не навіть випадкових чисел</strong>.", + "simplex-unique-card-2-p-1": "Оскільки у вас немає ідентифікатора або фіксованої адреси в мережі SimpleX, ніхто не може зв'язатися з вами, якщо ви не поділитеся одноразовою або тимчасовою адресою користувача, як QR-кодом або посиланням.", + "simplex-unique-card-3-p-1": "SimpleX зберігає всі дані користувачів на пристроях клієнтів у <strong>переносному зашифрованому форматі бази даних</strong> — його можна передавати на інший пристрій.", + "join": "Приєднатися", + "we-invite-you-to-join-the-conversation": "Ми запрошуємо вас приєднатися до розмови", "join-the-REDDIT-community": "Приєднуйтесь до спільноти REDDIT", "join-us-on-GitHub": "Приєднуйтесь до нас на GitHub", "donate-here-to-help-us": "Пожертвуйте тут, щоб допомогти нам", - "sign-up-to-receive-our-updates": "Підпишіться на наші оновлення", - "enter-your-email-address": "Введіть адресу вашої електронної пошти", - "get-simplex": "Отримати SimpleX <a href=\"/downloads\">desktop app</a>", - "why-simplex-is": "Чому SimpleX це", - "unique": "унікальний", - "learn-more": "Дізнайтеся більше", - "more-info": "Більше інформації", + "sign-up-to-receive-our-updates": "Підпишіться, щоб отримувати наші оновлення", + "enter-your-email-address": "Введіть свою електронну адресу", + "get-simplex": "Отримати SimpleX <a href=\"/downloads\">додаток для настільних комп'ютерів</a>", + "why-simplex-is-unique": "Чому SimpleX є <span class='gradient-text'>унікальним</span>", + "learn-more": "Дізнатися більше", + "more-info": "Додаткова інформація", "hide-info": "Приховати інформацію", - "contact-hero-header": "Ви отримали адресу для підключення до чату SimpleX", - "invitation-hero-header": "Ви отримали одноразове посилання для підключення до чату SimpleX", - "contact-hero-subheader": "Відскануйте QR-код за допомогою програми SimpleX Chat на телефоні або планшеті.", + "contact-hero-header": "Ви отримали адресу для підключення в SimpleX Chat", + "invitation-hero-header": "Ви отримали 1-разове посилання для підключення в SimpleX Chat", + "contact-hero-subheader": "Скануйте QR-код за допомогою додатка SimpleX Chat на своєму телефоні чи планшеті.", "contact-hero-p-2": "Ще не завантажили SimpleX Chat?", - "contact-hero-p-3": "Щоб завантажити додаток, скористайтеся посиланнями нижче.", - "connect-in-app": "Підключіться в додатку", - "open-simplex-app": "Відкрийте програму Simplex", - "tap-the-connect-button-in-the-app": "Натисніть кнопку <span class='text-active-blue'>‘підключитися‘</span> у додатку", - "scan-the-qr-code-with-the-simplex-chat-app": "Відскануйте QR-код за допомогою програми SimpleX Chat", - "scan-the-qr-code-with-the-simplex-chat-app-description": "Відкриті ключі та адреса черги повідомлень за цим посиланням НЕ передаються мережею при перегляді цієї сторінки —<br> вони містяться в хеш-фрагменті URL-адреси посилання.", - "installing-simplex-chat-to-terminal": "Встановлення чату SimpleX на термінал", - "use-this-command": "Використовуй цю команду:", - "see-simplex-chat": "Дивіться Чат SimpleX", - "the-instructions--source-code": "інструкції, як завантажити або скомпілювати його з вихідного коду.", + "contact-hero-p-3": "Скористайтеся посиланнями нижче, щоб завантажити додаток.", + "connect-in-app": "Підключитися в додатку", + "open-simplex-app": "Відкрити додаток SimpleX", + "tap-the-connect-button-in-the-app": "Торкніться кнопки <span class='text-active-blue'>‘підключити’</span> в додатку", + "scan-the-qr-code-with-the-simplex-chat-app": "Скануйте QR-код за допомогою додатка SimpleX Chat", + "scan-the-qr-code-with-the-simplex-chat-app-description": "Публічні ключі та адреса черги повідомлень в цьому посиланні НЕ відправляються по мережі під час перегляду цієї сторінки —<br> вони містяться в хеш-фрагменті URL-посилання.", + "installing-simplex-chat-to-terminal": "Встановлення SimpleX Chat для терміналу", + "use-this-command": "Використовуйте цю команду:", + "see-simplex-chat": "Дивіться SimpleX Chat", + "the-instructions--source-code": "для інструкції з того, як його завантажити чи скомпілювати з вихідного коду.", "if-you-already-installed-simplex-chat-for-the-terminal": "Якщо ви вже встановили SimpleX Chat для терміналу", "if-you-already-installed": "Якщо ви вже встановили", "simplex-chat-for-the-terminal": "SimpleX Chat для терміналу", - "copy-the-command-below-text": "скопіюйте команду нижче і використовуйте її в чаті:", + "copy-the-command-below-text": "скопіюйте команду нижче та використовуйте її в чаті:", "privacy-matters-section-header": "Чому конфіденційність <span class='gradient-text'>має значення</span>", - "privacy-matters-section-subheader": "Збереження конфіденційності ваших метаданих — <span class='text-active-blue'>від кого ви спілкуєтеся</span> — захищає вас:", - "privacy-matters-section-label": "Переконайтеся, що ваш месенджер не може отримати доступ до ваших даних!", - "simplex-private-section-header": "Що робить SimpleX <span class='gradient-text'>приватним</span>", - "tap-to-close": "Натисніть, щоб закрити", - "simplex-network-section-header": "SimpleX <span class='gradient-text'>Мережа</span>", - "simplex-network-section-desc": "Simplex Chat забезпечує найкращу конфіденційність, поєднуючи переваги P2P та об'єднаних мереж.", - "simplex-network-1-desc": "Всі повідомлення надсилаються через сервери, що забезпечує кращу конфіденційність метаданих і надійну асинхронну доставку повідомлень, уникаючи при цьому багатьох", - "simplex-network-1-header": "На відміну від P2P-мереж", - "simplex-network-1-overlay-linktext": "проблеми P2P-мереж", - "simplex-network-2-header": "На відміну від об'єднаних мереж", - "simplex-network-2-desc": "Сервери ретрансляції SimpleX НЕ зберігають профілі користувачів, контакти та доставлені повідомлення, НЕ з'єднуються один з одним і НЕ мають каталогу серверів.", + "privacy-matters-section-subheader": "Збереження конфіденційності ваших метаданих — <span class='text-active-blue'>з ким ви спілкуєтеся</span> — захищає вас від:", + "privacy-matters-section-label": "Переконайтеся, що ваш месенджер не має доступу до ваших даних!", + "simplex-private-section-header": "Що робить SimpleX <span class='gradient-text'>конфіденційним</span>", + "tap-to-close": "Торкніться, щоб закрити", + "simplex-network-section-header": "SimpleX <span class='gradient-text'>Network</span>", + "simplex-network-section-desc": "Simplex Chat надає найкращу конфіденційність, поєднуючи переваги P2P та федеративних мереж.", + "simplex-network-1-desc": "Всі повідомлення відправляються через сервери, що забезпечує кращу конфіденційність метаданих та надійну асинхронну доставку повідомлень, уникаючи багатьох", + "simplex-network-1-header": "На відміну від мереж P2P", + "simplex-network-1-overlay-linktext": "проблем P2P-мереж", + "simplex-network-2-header": "На відміну від федеративних мереж", + "simplex-network-2-desc": "Сервери SimpleX Chat НЕ зберігають профілі користувачів, контакти та доставлені повідомлення, НЕ підключаються один до одного, і НЕ існує каталог серверів.", "simplex-network-3-header": "Мережа SimpleX", - "simplex-network-3-desc": "Сервери надають <span class='text-active-blue'>односпрямовані черги</span> для підключення користувачів, але вони не мають видимості графу мережевих з'єднань —, його бачать лише користувачі.", + "simplex-network-3-desc": "сервери надають <span class='text-active-blue'>однобічні черги</span> для з'єднання користувачів, але вони не мають видимості графа підключень в мережі — лише користувачі.", "comparison-section-header": "Порівняння з іншими протоколами", - "protocol-1-text": "Сигнал, великі платформи", - "protocol-2-text": "XMPP, Матрикс", + "protocol-1-text": "Signal, великі платформи", + "protocol-2-text": "XMPP, Matrix", "protocol-3-text": "Протоколи P2P", - "comparison-point-1-text": "Вимагає глобальної ідентичності", + "comparison-point-1-text": "Потребує глобального ідентифікатора", "comparison-point-2-text": "Можливість MITM", "comparison-point-3-text": "Залежність від DNS", - "comparison-point-4-text": "Окрема або централізована мережа", - "comparison-point-5-text": "Атака на центральний компонент або інша мережева атака", + "comparison-point-4-text": "Одна чи централізована мережа", + "comparison-point-5-text": "Центральний компонент чи інша атака на всю мережу", "yes": "Так", "no": "Ні", - "no-private": "Ні - приватний", + "no-private": "Ні - конфіденційно", "no-secure": "Ні - безпечно", - "no-resilient": "Ні - стійкий", + "no-resilient": "Ні - стійко", "no-decentralized": "Ні - децентралізовано", - "comparison-section-list-point-4": "Якщо сервери оператора скомпрометовані", - "comparison-section-list-point-5": "Не захищає метадані користувачів", - "comparison-section-list-point-6": "Хоча P2P розподілені, вони не об'єднані - вони працюють як єдина мережа", + "comparison-section-list-point-4": "Якщо сервери оператора порушені. Перевірте безпековий код в Signal та деяких інших додатках для зменшення ризику", + "comparison-section-list-point-5": "Не захищає конфіденційність метаданих користувачів", + "comparison-section-list-point-6": "Хоча P2P є розподіленими, вони не є федеративними — вони працюють як одна мережа", "guide-dropdown-1": "Швидкий старт", - "guide-dropdown-2": "Надсилання повідомлень", + "guide-dropdown-2": "Відправлення повідомлень", "guide-dropdown-3": "Таємні групи", "glossary": "Глосарій", "docs-dropdown-8": "Служба каталогів SimpleX", - "f-droid-page-simplex-chat-repo-section-text": "Щоб додати його в клієнт F-Droid <span class='hide-on-mobile'>відскануйте QR-код або</span> використовуйте цю URL-адресу:", + "f-droid-page-simplex-chat-repo-section-text": "Щоб додати його до свого клієнта F-Droid, <span class='hide-on-mobile'>скануйте QR-код або</span> використовуйте цей URL:", "simplex-chat-via-f-droid": "SimpleX Chat через F-Droid", - "signing-key-fingerprint": "Відбиток ключа підпису (SHA-256)", - "stable-versions-built-by-f-droid-org": "Стабільні версії, зібрані на F-Droid.org", - "simplex-chat-repo": "Репо SimpleX Chat", - "f-droid-org-repo": "Репо F-Droid.org", - "releases-to-this-repo-are-done-1-2-days-later": "Релізи в це репо відбуваються на 1-2 дні пізніше", - "stable-and-beta-versions-built-by-developers": "Стабільні та бета-версії, створені розробниками", - "f-droid-page-f-droid-org-repo-section-text": "Репозиторії SimpleX Chat та F-Droid.org підписують збірки з різними ключами. Щоб перемикнутися, будь ласка <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>експорт</a> базу даних чату та перевстановіть додаток." -} \ No newline at end of file + "signing-key-fingerprint": "Відбиток підпису ключа (SHA-256)", + "stable-versions-built-by-f-droid-org": "Стабільні версії, побудовані F-Droid.org", + "simplex-chat-repo": "Репозитарій SimpleX Chat", + "f-droid-org-repo": "Репозитарій F-Droid.org", + "releases-to-this-repo-are-done-1-2-days-later": "Релізи в це репо відбуваються на кілька днів пізніше", + "stable-and-beta-versions-built-by-developers": "Стабільні та бета-версії, побудовані розробниками", + "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat та репозитарії F-Droid.org підписують збірки різними ключами. Щоб переключитися, будь ласка, <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>експортуйте</a> базу даних чату та перевстановіть додаток.", + "hero-overlay-3-title": "Оцінка безпеки", + "hero-overlay-card-3-p-2": "Trail of Bits переглянувало криптографію та компоненти мережі платформи SimpleX у листопаді 2022 року. Читайте більше в <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">оголошенні</a>.", + "jobs": "Приєднатися до команди", + "hero-overlay-3-textlink": "Оцінка безпеки", + "hero-overlay-card-3-p-1": "<a href=\"https://www.trailofbits.com/about/\">Trail of Bits</a> є провідною консалтинговою фірмою з безпеки та технологій, клієнтами якої є великі технологічні компанії, урядові агенції та великі проекти у сфері блокчейну.", + "comparison-section-list-point-4a": "Ретранслятори SimpleX не можуть порушити e2e-шифрування. Перевірте безпековий код для зменшення ризику атаки на зовнішньобандовий канал", + "docs-dropdown-9": "Завантаження", + "please-enable-javascript": "Будь ласка, увімкніть JavaScript, щоб побачити QR-код.", + "please-use-link-in-mobile-app": "Будь ласка, скористайтеся посиланням у мобільному додатку", + "docs-dropdown-11": "ПОШИРЕНІ ЗАПИТАННЯ", + "docs-dropdown-10": "Прозорість", + "docs-dropdown-12": "Безпека", + "hero-overlay-card-3-p-3": "Trail of Bits переглянув криптографічний дизайн мережевих протоколів SimpleX в липні 2024 року. <a href=\"/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html\">Детальніше</a>.", + "docs-dropdown-14": "SimpleX для бізнесу" +} diff --git a/website/langs/zh_Hans.json b/website/langs/zh_Hans.json index 8fb012963a..c0b843ca9e 100644 --- a/website/langs/zh_Hans.json +++ b/website/langs/zh_Hans.json @@ -1,7 +1,7 @@ { "simplex-explained-tab-3-p-2": "用户可以通过使用 Tor 访问服务器以进一步提高元数据隐私,防止通过 IP 地址关联实际身份。", "chat-bot-example": "聊天机器人示例", - "terms-and-privacy-policy": "条款和隐私政策", + "terms-and-privacy-policy": "隐私政策", "hero-overlay-1-textlink": "为什么用户 ID 不利于隐私?", "hero-2-header": "建立私人连接", "hero-overlay-2-title": "为什么用户 ID 不利于隐私?", @@ -24,7 +24,7 @@ "simplex-network-overlay-card-1-li-2": "与大多数 P2P 网络不同,SimpleX 在设计上没有任何类型的全局用户标识符,甚至临时的也没有。SimpleX 仅使用临时的成对标识符,提供更好的匿名性和元数据保护。", "privacy-matters-overlay-card-1-p-1": "许多大型公司会使用您的人际关系来估算您的收入,决定商品的价格,并向您兜售您并不真正需要的产品。", "simplex-unique-overlay-card-3-p-4": "发送和接收的服务器流量之间没有共同的标识符或密文—— 如果有人在观察它,他们也无法轻易确定谁与谁通信,即使 TLS 受到威胁。", - "simplex-unique-card-4-p-1": "SimpleX 网络是完全去中心化的,并且独立于任何加密货币或除互联网以外的任何其他平台。", + "simplex-unique-card-4-p-1": "SimpleX 网络是完全去中心化的,并且独立于任何加密货币或除互联网以外的任何其他网络。", "join": "加入", "get-simplex": "获取 SimpleX <a href=\"/downloads\">desktop app</a>", "hide-info": "隐藏信息", @@ -34,7 +34,7 @@ "simplex-network-1-header": "与 P2P 网络不同", "simplex-network-1-overlay-linktext": "P2P网络存在的问题", "simplex-network-2-header": "不同于联邦网络", - "comparison-point-1-text": "需要全球身份", + "comparison-point-1-text": "需要全局身份", "no-secure": "不可能 - 安全", "simplex-privacy": "SimpleX 的隐私性", "home": "主页", @@ -49,19 +49,19 @@ "simplex-explained": "SimpleX 简述", "simplex-explained-tab-3-text": "3. 服务器能看到什么", "hero-header": "重新定义隐私", - "simplex-explained-tab-1-p-2": "它是如何在单向消息队列与没有用户识别符的情况下工作的?", - "simplex-explained-tab-2-p-2": "服务器只会单向传输消息,而无法掌握用户的会话或连接的全貌。", + "simplex-explained-tab-1-p-2": "它是如何利用单向消息队列并不利用用户识别符工作的?", + "simplex-explained-tab-2-p-2": "服务器只会单向传输消息,而无法掌握用户的对话或连接的全貌。", "simplex-explained-tab-3-p-1": "服务器对每个队列都有单独的匿名凭证,并且不知道这些凭证属于哪些用户。", "donate": "捐赠", "simplex-explained-tab-2-p-1": "对于每个连接,您都会使用两个单独的消息队列,通过不同的服务器发送和接收消息。", "simplex-chat-protocol": "SimpleX 聊天协议", "smp-protocol": "SMP协议", "chat-protocol": "聊天协议", - "copyright-label": "© 2020-2023 SimpleX | 开源项目", + "copyright-label": "© 2020-2025 SimpleX | 开源项目", "terminal-cli": "命令行程式", "simplex-explained-tab-1-p-1": "您可以创建联系人和群组,并进行双向对话,就像是任何其他即时通讯软件一样。", "hero-p-1": "其他应用——如Signal、Matrix、Session、Briar、Jami、Cwtch 等——都需要用户 ID。<br>而SimpleX 不需要用户ID,连<strong>随机生成</strong>的也不需要。<br>这从根本上改善了您的隐私。", - "hero-subheader": "首个不使用用户ID的即时通讯软件", + "hero-subheader": "首个<br>没有用户ID的即时通讯软件", "hero-overlay-2-textlink": "SimpleX 是如何工作的?", "hero-2-header-desc": "右侧的视频向您展示了如何通过一次性二维码、面对面交流或通过视频交换链接来连接到您的朋友。您同样可以通过共享邀请链接来进行连接。", "hero-overlay-1-title": "SimpleX 是如何工作的?", @@ -104,7 +104,7 @@ "simplex-private-card-10-point-1": "SimpleX 为每个用户的联系人或群组成员均使用临时匿名成对地址和凭据。", "privacy-matters-3-title": "因无辜结社而被起诉", "privacy-matters-2-overlay-1-title": "隐私给您力量", - "hero-overlay-card-1-p-2": "为了传递消息,SimpleX 使用消息队列的临时匿名成对标识符(而非其他平台使用的用户 ID)来区分您的每个连接——没有长期标识符这种东西。", + "hero-overlay-card-1-p-2": "SimpleX 使用消息队列的临时匿名成对标识符(而非其他网络使用的用户 ID)来传递消息,而每个连接的标识符均不尽相同 — 没有长期标识符这种东西。", "hero-overlay-card-1-p-3": "您定义用于接收消息的服务器、您的联系人—— 您用来向他们发送消息的服务器。 每个会话都很可能会使用两个不同的服务器。", "hero-overlay-card-1-p-4": "此设计可防止在应用程序级别泄漏任何用户元数据。 为了进一步改善隐私并保护您的 IP 地址,您可以通过 Tor 连接到消息服务器。", "hero-overlay-card-1-p-5": "只有客户端设备存储用户配置文件、联系人和群组; 消息均使用两层端到端加密发送。", @@ -115,35 +115,35 @@ "hero-overlay-card-2-p-3": "即使是使用 Tor v3 服务的最私密的应用程序,如果您通过同一个人资料与两个不同的联系人交谈,他们也可以证明他们与同一个人有联系。", "hero-overlay-card-2-p-4": "为防止这些攻击,SimpleX 在其设计中不包含任何用户 ID 。 而且,如果您使用隐身模式,您将为每个联系人显示不同的名称,避免他们之间共享任何数据。", "simplex-network-overlay-card-1-li-1": "P2P 网络依赖于<a href='https://zh.wikipedia.org/wiki/%E5%88%86%E6%95%A3%E5%BC%8F%E9%9B%9C%E6%B9%8A%E8%A1%A8'> 分布式散列表(DHT) </a> 的某些变体来路由消息。 DHT 在设计上必须平衡可达性和延迟。 SimpleX 比 P2P 具有更好的可达性和更低的延迟,因为消息可以通过通讯双方选择的多个服务器并行地冗余传递。若是在 P2P 网络中,消息则需要使用算法选择,并依次通过 <em>O(log N)</em> 个节点。", - "simplex-network-overlay-card-1-li-4": "P2P 实现(如 <a href='https://zh.wikipedia.org/wiki/BitTorrent_(%E5%8D%8F%E8%AE%AE)'>BitTorrent</a>)可能会被某些互联网提供商阻止。 SimpleX 与传输协议无关——它可以在标准网络协议上工作,例如 WebSockets。", + "simplex-network-overlay-card-1-li-4": "P2P 实现(如 <a href='https://zh.wikipedia.org/wiki/BitTorrent_(%E5%8D%8F%E8%AE%AE)'>BitTorrent</a>)可能会被某些互联网提供商阻止。 SimpleX 与传输协议无关 — 它可以在标准网络协议上工作,例如 WebSockets。", "simplex-network-overlay-card-1-li-3": "P2P 并未解决<a href='https://zh.wikipedia.org/wiki/%E4%B8%AD%E9%97%B4%E4%BA%BA%E6%94%BB%E5%87%BB'>中间人攻击(MITM Attack)</a> 问题。大多数现有的 P2P 实现没有使用带外通讯来进行初始密钥的交换,而 SimpleX 使用带外通讯,或者在某些情况下,使用预先存在的安全和可信连接来进行初始密钥交换。", "simplex-network-overlay-card-1-li-5": "所有已知的 P2P 网络都可能受到 <a href='https://en.wikipedia.org/wiki/Sybil_attack'>Sybil 攻击</a>,因为每个节点都是可发现的,并且网络作为一个整体运行。 已知的缓解措施不是需要一个中心化的组件就是需要昂贵的<a href='https://en.wikipedia.org/wiki/Proof_of_work'>工作量证明</a>。而 SimpleX 网络没有服务器可发现性,它是碎片化的并且作为多个隔离的子网运行,这样全网络范围的攻击便无从实现。", "simplex-network-overlay-card-1-li-6": "P2P 网络可能受到 <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'>DRDoS 攻击</a> 。客户端有能力重新广播和放大流量,从而导致整个网络范围内的服务中断。 SimpleX 客户端仅中继来自已知连接的流量,因此不能被攻击者用来放大整个网络的流量。", "privacy-matters-overlay-card-1-p-2": "在线零售商知道收入较低的人更有可能在紧急情况下购买商品,因此他们可能会收取更高的价格或取消折扣。", "privacy-matters-overlay-card-1-p-3": "一些金融和保险公司使用社交图谱来确定利率和保费。 它通常会让收入较低的人支付更多—— 它被称为<a href='https://fairbydesign.com/povertypremium/' target='_blank'>“贫困溢价”</a>。", - "privacy-matters-overlay-card-1-p-4": "SimpleX 平台比任何替代方案都能更好地保护您人际关系层面的隐私,防止您的社交图谱被任何公司或组织使用。 即使人们使用 SimpleX Chat 提供的服务器,我们也不知道用户数量或他们的连接数。", + "privacy-matters-overlay-card-1-p-4": "SimpleX 网络比任何替代方案都能更好地保护您人际关系层面的隐私,防止您的社交图谱被任何公司或组织使用。 即使人们使用 SimpleX Chat 应用预配置的服务器,服务器运营方也不知道用户数量或他们的连接数。", "privacy-matters-overlay-card-2-p-1": "不久前,我们观察到几次大选被<a href='https://en.wikipedia.org/wiki/Facebook–Cambridge_Analytica_data_scandal' target='_blank'>一家知名咨询公司</a>操纵,该公司使用我们的社交图谱扭曲我们对现实世界的看法并操纵我们的选票。", - "privacy-matters-overlay-card-2-p-2": "为了客观并做出独立的决定,您需要控制您的信息空间。 而这只有当您使用没有能力访问您的社交图谱的,注重隐私的通信平台时,这才有可能。", - "privacy-matters-overlay-card-2-p-3": "SimpleX 是第一个没有设计任何用户标识符的平台,这样能比任何已知的替代方案都更好地保护您的连接图谱。", + "privacy-matters-overlay-card-2-p-2": "为了客观并做出独立的决定,您需要控制您的信息空间。 而这只有当您使用没有能力访问您的社交图谱的,注重隐私的通信网络时,这才有可能。", + "privacy-matters-overlay-card-2-p-3": "SimpleX 是第一个没有设计任何用户标识符的网络,这样能比任何已知的替代方案都更好地保护您的连接图谱。", "privacy-matters-overlay-card-3-p-2": "最令人震惊的故事之一是 <a href='https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi' target='_blank'>Mohamedou Ould Salahi</a> 在他的回忆录中描述并在毛里塔尼亚电影中展示的经历。 他在未经审判的情况下被关进关塔那摩集中营,并在打电话给他在阿富汗的亲戚后在那里遭受了 15 年的折磨,他被怀疑参与了 9/11 袭击,尽管他在过去 10 年住在德国。", "privacy-matters-overlay-card-3-p-1": "每个人都应该关心他们通信的隐私和安全——无害的谈话会让您处于危险之中,即使您没有什么可隐瞒的。", "privacy-matters-overlay-card-3-p-3": "普通人会因为他们在网上分享的内容而被捕,即使是通过他们的“匿名”帐户,<a href='https://www.dailymail.co.uk/news/article-11282263/Moment-police-swoop-house-devout -catholic-mother-malicious-online-posts.html' target='_blank'>即使是在民主国家</a>。", "privacy-matters-overlay-card-3-p-4": "使用端到端加密的即时通讯软件还不够,我们都应该使用保护我们个人网络隐私——即我们与谁有联系的即时通讯软件。", - "simplex-unique-overlay-card-1-p-1": "与其他消息传递平台不同,SimpleX <strong>没有分配给用户的标识符</strong>。 它不依赖电话号码、基于域的地址(如电子邮件或 XMPP)、用户名、公钥甚至随机数来识别其用户—— 我们不知道有多少人使用我们的 SimpleX 服务器。", - "simplex-unique-overlay-card-1-p-2": "为了传递消息,SimpleX 使用单向消息队列的<a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>成对匿名地址</a>,通常通过不同的服务器将接收和发送的消息分开。 使用 SimpleX 就像<strong>为每个联系人使用不同的即时电子邮件或电话</strong>,并且无需管理它们。", - "simplex-unique-overlay-card-1-p-3": "这种设计保护了您正在与之通信的人的隐私,将其隐藏在 SimpleX 平台服务器和任何观察者之外。 要对服务器隐藏您的 IP 地址,您可以<strong>通过 Tor 连接到 SimpleX 服务器</strong>。", - "simplex-unique-overlay-card-4-p-1": "您可以<strong>将 SimpleX 与您自己的服务器一起使用</strong>,并且仍然可以与使用我们提供的预配置服务器的人进行通信。", + "simplex-unique-overlay-card-1-p-1": "与其他消息网络不同,SimpleX <strong>没有分配给用户的标识符</strong>。 它不依赖电话号码、基于域的地址(如电子邮件或 XMPP)、用户名、公钥甚至随机数来识别其用户—— SimpleX 服务器运营方不知道有多少人使用其服务器。", + "simplex-unique-overlay-card-1-p-2": "为了传递消息,SimpleX 使用单向消息队列的<a href='https://csrc.nist.gov/glossary/term/Pairwise_Pseudonymous_Identifier'>成对匿名地址</a>,通常通过不同的服务器将接收和发送的消息分开。", + "simplex-unique-overlay-card-1-p-3": "这种设计保护了您正在与之通信的人的隐私,将其隐藏在 SimpleX 网络的服务器和任何观察者之外。 要对服务器隐藏您的 IP 地址,您可以<strong>通过 Tor 连接到 SimpleX 服务器</strong>。", + "simplex-unique-overlay-card-4-p-1": "您可以<strong>将 SimpleX 与您自己的服务器一起使用</strong>,并且仍然可以与使用应用中预配置服务器的人们进行通信。", "simplex-unique-card-3-p-2": "端到端加密的消息在被收到前会暂时保存在 SimpleX 中继服务器上,传送完成后它们会被永久删除。", - "simplex-unique-card-1-p-1": "SimpleX 保护您的个人资料、联系人和元数据的隐私,将其隐藏在 SimpleX 平台服务器和任何观察者之外。", - "simplex-unique-overlay-card-4-p-3": "如果您正在考虑为在SimpleX 平台上开发,例如,为 SimpleX 应用程序用户开发聊天机器人,或将 SimpleX 聊天库集成到您的移动应用程序中,请 <a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>联系我们</a> 以获取建议和支持。", + "simplex-unique-card-1-p-1": "SimpleX 保护您的个人资料、联系人和元数据的隐私,不让 SimpleX 网络服务器和任何观察者看到它们。", + "simplex-unique-overlay-card-4-p-3": "如果您正在考虑为在SimpleX 网络上开发,例如,为 SimpleX 应用程序用户开发聊天机器人,或将 SimpleX 聊天库集成到您的移动应用程序中,请 <a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>联系我们</a> 以获取建议和支持。", "simplex-unique-card-4-p-2": "您可以<strong>搭配自己的服务器来使用 SimpleX </strong> 或使用我们提供的服务器 — 并仍然连接到任何用户。", - "simplex-unique-card-2-p-1": "因为您在 SimpleX 平台上没有标识符或固定地址,所以除非您以二维码或链接的形式分享一次性或临时用户地址,没有人可以联系您。", + "simplex-unique-card-2-p-1": "因为您在 SimpleX 网络上没有标识符或固定地址,所以除非您以二维码或链接的形式分享一次性或临时用户地址,没有人可以联系您。", "simplex-unique-card-3-p-1": "SimpleX 以<strong>便携式加密数据库格式</strong>将所有用户数据存储在客户端设备上—— 它可以转移到另一个设备。", "sign-up-to-receive-our-updates": "注册以接收我们的更新", "we-invite-you-to-join-the-conversation": "我们邀请您加入对话", "enter-your-email-address": "输入您的电子邮箱地址", "join-the-REDDIT-community": "加入 REDDIT 社区", - "why-simplex-is": "为什么 SimpleX 是", + "why-simplex-is-unique": "为什么 SimpleX 是 <span class='gradient-text'>独特的</span>", "learn-more": "了解更多", "join-us-on-GitHub": "在 GitHub 上加入我们", "more-info": "更多信息", @@ -164,7 +164,7 @@ "github-repository": "GitHub 仓库", "copy-the-command-below-text": "复制下面的命令并在聊天中使用它:", "privacy-matters-section-header": "为什么隐私<span class='gradient-text'>很重要</span>", - "the-instructions--source-code": "如何从源代码下载或编译它的说明。", + "the-instructions--source-code": "如何下载或从源代码下载进行编译的说明。", "if-you-already-installed-simplex-chat-for-the-terminal": "如果您已经为终端安装了 SimpleX Chat", "if-you-already-installed": "如果您已经安装", "simplex-chat-for-the-terminal": "用于终端的 SimpleX Chat", @@ -175,21 +175,21 @@ "tap-to-close": "点击关闭", "simplex-network-section-header": "SimpleX <span class='gradient-text'>网络</span>", "simplex-network-section-desc": "Simplex Chat 通过结合 P2P 和联邦网络的优势使其保密性无与伦比。", - "no-private": "不需要 - 私密", + "no-private": "否 - 私密", "simplex-network-1-desc": "所有消息都通过服务器发送,既能更好地保护元数据隐私和可靠地传递异步消息,同时也能避免许多", - "simplex-network-3-header": "SimpleX网络", + "simplex-network-3-header": "SimpleX 网络", "protocol-1-text": "Signal、其他大平台", - "comparison-point-4-text": "单一或集中式网络", + "comparison-point-4-text": "单一或中心化网络", "simplex-network-3-desc": "服务器提供<span class='text-active-blue'>单向队列</span>来连接用户,但是他们看不到网络连接图图谱— 只有用户可以。", "comparison-section-header": "与其他协议的比较", "protocol-3-text": "P2P协议", - "no": "不需要", + "no": "否", "comparison-point-3-text": "对 DNS 的依赖", "no-federated": "不依赖 - 联邦式网络", "protocol-2-text": "XMPP、Matrix", "comparison-point-2-text": "中间人攻击的可能性", "comparison-point-5-text": "中央组件或其他全网攻击", - "yes": "需要", + "yes": "是", "comparison-section-list-point-5": "不保护用户的元数据", "no-resilient": "不依赖 - 有韧性", "no-decentralized": "不依赖 - 去中心化的", @@ -197,17 +197,16 @@ "comparison-section-list-point-4": "如果运营商的服务器受到威胁。 验证 Signal 和其他一些应用程序中的安全代码以缓解该问题", "comparison-section-list-point-1": "通常基于电话号码,在某些情况下基于用户名", "comparison-section-list-point-2": "基于 DNS 的地址", - "comparison-section-list-point-6": "P2P 是分布式的,而非联邦式的 - 它们作为单个网络运行", + "comparison-section-list-point-6": "P2P 是分布式的,但并非联邦式的 — 它们作为单个网络运行", "see-here": "参见此处", - "comparison-section-list-point-7": "P2P 网络要么拥有中央管理机制,要么整个网络都可能受到损害", + "comparison-section-list-point-7": "P2P 网络要么拥有中央权威,要么整个网络可能被攻陷", "simplex-private-card-5-point-1": "SimpleX 为每个加密层进行内容填充来对抗长度扩展攻击。", - "simplex-unique-overlay-card-4-p-2": "SimpleX 平台使用<a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>开放协议</a>并提供<a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>用于创建聊天机器人的 SDK</a>, 允许用户实现通过 SimpleX Chat 应用程序与之交互的服务—— 我们真的很期待看到您可以依托SimpleX构建哪些服务。", + "simplex-unique-overlay-card-4-p-2": "SimpleX 网络使用<a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>开放协议</a>并提供<a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>用于创建聊天机器人的 SDK</a>, 允许用户实现通过 SimpleX Chat 应用程序与之交互的服务—— 我们真的很期待看到您会依托SimpleX构建哪些服务。", "simplex-unique-overlay-card-2-p-2": "即使使用可选的用户地址,当它被用于发送垃圾邮件联系请求,您可以更改或完全删除它而不会丢失任何连接。", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat 使用<strong>便携式加密数据库格式</strong>仅将所有用户数据存储在客户端设备上,该格式可以导出并传输到任何支持的设备。", "donate-here-to-help-us": "在这里捐款来帮助我们", - "unique": "独特的", - "simplex-unique-card-1-p-2": "与任何其他现有的消息传递平台不同,SimpleX 没有分配给用户的标识符—— <strong>甚至随机数也没有</strong>。", - "simplex-unique-overlay-card-2-p-1": "因为您在 SimpleX 平台上没有标识符,所以除非您以二维码或链接的形式分享一次性或临时用户地址,没有人可以联系您。", + "simplex-unique-card-1-p-2": "与任何其他现有的消息传递网络不同,SimpleX 没有分配给用户的标识符—— <strong>甚至随机数也没有</strong>。", + "simplex-unique-overlay-card-2-p-1": "因为您在 SimpleX 网络上没有标识符,所以除非您以二维码或链接的形式分享一次性或临时用户地址,没有人可以联系您。", "simplex-unique-overlay-card-3-p-2": "端到端加密的消息在被收到前会暂时保存在 SimpleX 中继服务器上,传送完成后它们会被永久删除。", "simplex-unique-overlay-card-3-p-3": "与联合网络服务器(电子邮件、XMPP 或 Matrix)不同,SimpleX 服务器不存储用户帐户,它们仅中继消息,保护双方的隐私。", "guide-dropdown-2": "发送消息", @@ -224,7 +223,7 @@ "click-to-see": "点击查看", "menu": "菜单", "guide-dropdown-1": "快速指南", - "docs-dropdown-1": "SimpleX 平台", + "docs-dropdown-1": "SimpleX 网络", "docs-dropdown-5": "托管 XFTP 服务器", "newer-version-of-eng-msg": "本页面有较新的英语版本。", "guide-dropdown-4": "聊天档案", @@ -235,7 +234,7 @@ "glossary": "术语表", "signing-key-fingerprint": "签名密钥指纹 (SHA-256)", "simplex-chat-via-f-droid": "通过 F-Droid 下载 SimpleX", - "releases-to-this-repo-are-done-1-2-days-later": "此存储库的版本将延迟 1-2 天发布", + "releases-to-this-repo-are-done-1-2-days-later": "此存储库的版本将延迟数天发布", "f-droid-org-repo": "F-Droid.org 存储库", "stable-versions-built-by-f-droid-org": "由 F-Droid.org 构建的稳定版本", "simplex-chat-repo": "SimpleX 存储库", @@ -245,10 +244,16 @@ "docs-dropdown-8": "SimpleX 目录服务", "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat 和 F-Droid.org 存储库使用不同的密钥对构建进行签名。 如需切换,请<a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>导出</a>聊天数据库并重新安装应用。", "hero-overlay-3-title": "安全性评估", - "hero-overlay-card-3-p-2": "2022年11月份,Trail of Bits 审核了 SimpleX 平台的密码学和网络部件。", - "hero-overlay-card-3-p-3": "更多内容见 <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">该公告</a>。", + "hero-overlay-card-3-p-2": "2022年11月份,Trail of Bits 审核了 SimpleX 平台的密码学和网络部件。<a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">更多内容见 该公告</a>。", "jobs": "加入团队", "hero-overlay-3-textlink": "安全性评估", "hero-overlay-card-3-p-1": "<a href=\"https://www.trailofbits.com/about/\">Trail of Bits</a> 是一家领先的安全和技术咨询企业,其客户包括大型科技公司、政府机构和重要的区块链项目。", - "docs-dropdown-9": "下载" + "docs-dropdown-9": "下载", + "please-use-link-in-mobile-app": "请使用移动应用程序中的链接", + "please-enable-javascript": "请启用 JavaScript 以查看二维码。", + "docs-dropdown-10": "透明度", + "docs-dropdown-11": "常问问题", + "docs-dropdown-12": "安全性", + "hero-overlay-card-3-p-3": "Trail of Bits 于 2024 年 7 月审核了 SimpleX 网络协议的加密设计。<a href=\"/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html\">了解更多信息</a>。", + "docs-dropdown-14": "企业版 SimpleX" } diff --git a/website/langs/zh_Hant.json b/website/langs/zh_Hant.json index 0967ef424b..324360cb19 100644 --- a/website/langs/zh_Hant.json +++ b/website/langs/zh_Hant.json @@ -1 +1,206 @@ -{} +{ + "home": "开始", + "developers": "開發人員", + "reference": "參考", + "blog": "部落格", + "features": "特徵", + "why-simplex": "為什麼選擇SimpleX", + "simplex-privacy": "SimpleX 隱私", + "simplex-network": "SimpleX 網路", + "simplex-explained": "SimpleX 解釋", + "simplex-explained-tab-1-text": "1. 用戶體驗", + "simplex-explained-tab-2-text": "2. 它是如何工作的", + "simplex-explained-tab-3-text": "3. 伺服器可以看到什麼", + "terminal-cli": "終端 CLI", + "hero-header": "重新定義隱私", + "hero-subheader": "第一個沒有 User ID 的 Messenger", + "simplex-explained-tab-3-p-2": "用戶可以通過使用 Tor 訪問伺服器來進一步提高元數據隱私,防止按 IP 位址進行序列化。", + "smp-protocol": "SMP 協定", + "simplex-explained-tab-2-p-2": "伺服器僅單向傳遞消息,無法全面瞭解使用者的對話記錄或連接。", + "simplex-explained-tab-2-p-1": "對於每個連接,您可以使用兩個單獨的消息佇列通過不同的伺服器發送和接收消息。", + "chat-protocol": "聊天協定", + "copyright-label": "© 2020-2025 SimpleX |開源專案", + "donate": "捐助", + "simplex-explained-tab-1-p-1": "您可以創建聯繫人和群組,並進行雙向對話,就像在任何其他 Messenger 中一樣。", + "simplex-explained-tab-1-p-2": "它如何在沒有使用者配置檔標識符的情況下使用單向佇列?", + "simplex-explained-tab-3-p-1": "伺服器對每個佇列都有單獨的匿名憑證,並且不知道它們屬於哪些使用者。", + "chat-bot-example": "聊天機器人示例", + "simplex-chat-protocol": "SimpleX Chat 協定", + "terms-and-privacy-policy": "隱私策略", + "hero-overlay-card-3-p-3": "Trail of Bits 在 2024 年 7 月檢閱了 SimpleX 網路通訊協定的加密設計。<a href=\"/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html\">閱讀更多</a>。", + "hero-overlay-3-textlink": "安全評估", + "hero-overlay-card-3-p-2": "Trail of Bits 在 2022 年 11 月檢閱了 SimpleX 網路加密和網路元件。<a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">閱讀更多</a>。", + "hero-overlay-1-textlink": "用戶ID為何有害私隱?", + "hero-overlay-2-textlink": "SimpleX如何工作?", + "simplex-private-card-5-point-2": "它讓不同長度的訊息在伺服器和網路觀察者看來相同。", + "simplex-private-card-10-point-1": "對用戶與群組成員,SimpleX使用臨時、匿名的地址對與憑證對。", + "privacy-matters-1-title": "廣告投放與價格歧視", + "privacy-matters-1-overlay-1-linkText": "私隱為您省錢", + "privacy-matters-2-title": "操縱選舉", + "privacy-matters-2-overlay-1-title": "私隱給您權力", + "privacy-matters-2-overlay-1-linkText": "私隱給您權力", + "privacy-matters-3-overlay-1-title": "私隱保護您的自由", + "privacy-matters-3-overlay-1-linkText": "私隱保護您的自由", + "simplex-unique-4-title": "您擁有SimpleX網路", + "simplex-unique-4-overlay-1-title": "完全去中心化—用戶擁有 SimpleX 網路", + "hero-overlay-card-1-p-4": "此設計可防止在應用程式層級洩漏任何使用者' 元資料。為了進一步改善隱私並保護您的 IP 位址,您可以透過 Tor 連接到訊息伺服器。", + "hero-overlay-card-1-p-5": "只有用戶端會儲存使用者設定檔、聯絡人和群組;訊息傳送採用雙層端對端加密。", + "hero-overlay-card-2-p-2": "然後他們可以將這些資訊與現有的公開社交網路進行關聯,並確定一些真實身份。", + "simplex-unique-card-1-p-1": "SimpleX 保護您的個人資料、聯絡人和元資料的隱私,使其對 SimpleX 網路伺服器和任何觀察者隱藏。", + "scan-qr-code-from-mobile-app": "用手機應用程式掃描二維碼", + "scan-the-qr-code-with-the-simplex-chat-app": "使用SimpleX Chat應用程式掃描二維碼", + "protocol-3-text": "點對點協議", + "hero-p-1": "其他聊天應用,如Signal、Matrix、Session、Briar、Jami、Cwtch等,使用用戶ID;SimpleX不這樣做。它甚至不使用隨機數。這種做法極大地保護用戶私隱。", + "hero-2-header-desc": "此視訊教您以一次性QR碼、親身或以視訊連結的方式與聯繫人連線。您也可以通過分享邀請連結與人連線。", + "feature-2-title": "端到端加密的圖像、視訊、檔案", + "simplex-private-card-9-point-2": "與傳統的訊息代理相比,它可減小攻擊媒介及可見的詮釋資料量。", + "simplex-private-10-title": "臨時匿名標識符對", + "simplex-private-card-1-point-1": "雙棘輪協定—帶有前向保密、入侵恢復特質的不留記錄即時通訊協定。", + "privacy-matters-3-title": "因無辜關聯而被起訴", + "signing-key-fingerprint": "簽名密鑰指紋(SHA-256)", + "hero-overlay-card-1-p-3": "您決定用哪個/哪些伺服器接收訊息。您的聯絡人用此伺服器向您傳送訊息。往往每個對話都使用兩台不同的伺服器。", + "docs-dropdown-2": "訪問Android檔案", + "simplex-unique-card-2-p-1": "由於您在 SimpleX 網路上沒有任何標識符或固定地址,因此除非您分享一次性或臨時地址 (如 二維碼或連結),任何人都無法與您聯絡。", + "see-here": "見此", + "privacy-matters-overlay-card-3-p-4": "僅使用端對端加密的通訊工具是不夠的,我們都應使用能保護個人網路隱私的通訊工具。", + "hero-overlay-card-1-p-1": "很多用戶問: <em>如果 SimpleX 沒有用戶標識符,它何以知道向何處傳送訊息?</em>", + "simplex-unique-overlay-card-4-p-2": "SimpleX網路使用<a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>開源協議</a>並提供<a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>開發包以編寫聊天機器人</a>,允許使用者透過SimpleX Chat應用程式與服務互動—我們非常期待看到您的SimpleX服務之作!", + "hero-overlay-card-1-p-2": "為傳送訊息,SimpleX 使用消息佇列中的臨時匿名標識符對,每連線各異,而不像其他網路一樣使用用戶ID— SimpleX 沒有長期標識符。", + "privacy-matters-overlay-card-3-p-2": "最令人震驚的故事之一是 <a href='https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi' target='_blank'>Mohamedou Ould Salahi</a> 的經歷,描述於他的回憶錄中,並在《The Mauritanian movie》中呈現。他在未經審訊的情況下被送進關塔納摩集中營,並在那裏遭受了長達 15 年的酷刑,原因是他被懷疑參與了 9/11 襲擊事件,而此前 10 年他一直住在德國。", + "simplex-private-2-title": "伺服器附加加密層", + "hero-overlay-card-2-p-1": "如使用者擁有持久性身份,即使只是一個隨機數,例如 Session ID,也會有服務提供者或攻擊者可以觀察使用者如何連線以及傳送了多少條訊息的風險。", + "simplex-unique-overlay-card-4-p-3": "如您正考慮針對 SimpleX 網路進行開發,例如針對 SimpleX 使用者的聊天機器人,或將 SimpleX 聊天函式庫整合至您的手機應用程式,請<a href='https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D' target='_blank'>與我們聯絡</a>以獲取支持與建議。", + "simplex-network-overlay-card-1-li-1": "點對點網路依賴 <a href='https://en.wikipedia.org/wiki/Distributed_hash_table'>DHT</a> 的某些變體來路由訊息。DHT 設計必須平衡傳送保證和延遲。與點對點相比,SimpleX 具有更好的傳送保證和更低的延遲,因為訊息可以使用收件者選擇的伺服器,經由多個伺服器並行冗餘地傳送。在點對點網路中,訊息依次經由<em>O(log N)</em>個由演算法選擇的節點。", + "simplex-private-card-3-point-3": "連線恢復被停用,以防止會話攻擊。", + "simplex-private-card-2-point-1": "傳送至收件者的伺服器加密附加層,以防止 TLS 遭到攻擊時,接收和傳送的伺服器流量之間的關聯。", + "simplex-private-card-5-point-1": "SimpleX 為每個加密層使用內容填充,以挫敗通過監控訊息長度的攻擊。", + "comparison-section-list-point-4a": "SimpleX中繼不可能威脅端到端加密。由其他方式驗證安全碼以杜絕攻擊之可能", + "docs-dropdown-6": "WebRTC伺服器", + "feature-3-title": "端到端加密的、去中心化的群組—存在只有用戶自己知道", + "feature-7-title": "可攜、加密存儲—將設定檔移至另一設備", + "back-to-top": "回到頂部", + "click-to-see": "點擊查看", + "menu": "功能表", + "on-this-page": "此頁中", + "docs-dropdown-14": "在商業中使用SimpleX", + "glossary": "詞彙表", + "simplex-chat-repo": "SimpleX Chat儲存庫", + "newer-version-of-eng-msg": "本頁面有更新的英文版本。", + "f-droid-page-simplex-chat-repo-section-text": "<span class='hide-on-mobile'>掃描二維碼或</span>使用此URL以添加SimpleX儲存庫至您的F-Droid客戶端:", + "jobs": "加入團隊", + "please-use-link-in-mobile-app": "請使用手機應用程式中的連結", + "stable-versions-built-by-f-droid-org": "由F-Droid.org編譯的穩定版", + "please-enable-javascript": "啟用JavaScript以查看二維碼。", + "docs-dropdown-8": "SimpleX通訊錄服務", + "hero-2-header": "創建私密連線", + "hero-overlay-1-title": "SimpleX如何工作?", + "hero-overlay-2-title": "用戶ID為何有害私隱?", + "hero-overlay-3-title": "安全評估", + "feature-1-title": "支持Markdown與編輯的、端到端加密的訊息", + "feature-4-title": "端到端加密的語音訊息", + "feature-5-title": "自刪除訊息", + "feature-6-title": "端到端加密的語音、視訊通話", + "simplex-network-overlay-1-title": "與點對點訊息傳輸協定的比較", + "simplex-private-1-title": "雙層端到端加密", + "simplex-private-3-title": "經安全鑑權的TLS 傳送", + "simplex-private-4-title": "可選的經由Tor訪問", + "simplex-private-5-title": "多層內容填充", + "feature-8-title": "隱身模式—SimpleX獨有", + "simplex-private-6-title": "頻帶外密鑰交換", + "simplex-private-7-title": "訊息完整性驗證", + "simplex-private-8-title": "訊息混雜以降低關聯性", + "simplex-private-card-1-point-2": "如TLS安全受威脅,每個隊列中的NaCL cryptobox可防止關聯訊息隊列間的通訊。", + "simplex-private-9-title": "單向訊息隊列", + "simplex-private-card-10-point-2": "SimpleX使不用用戶設定檔傳輸訊息成為可能,與其他軟體相比提供更強的詮釋資料私隱性。", + "privacy-matters-1-overlay-1-title": "私隱為您省錢", + "simplex-unique-1-title": "您擁有完整的私隱權", + "simplex-unique-1-overlay-1-title": "您的身份、個人資料、聯絡人與詮釋資料完全隱密", + "simplex-unique-2-title": "SimpleX保護您免受垃圾訊息、濫用之害", + "simplex-private-card-4-point-1": "為保護您的 IP 位址,您可透過 Tor 或其他傳輸覆蓋網路訪問伺服器。", + "simplex-unique-2-overlay-1-title": "防止垃圾郵件和濫用的最佳保護", + "simplex-unique-3-title": "您掌控您的數據", + "simplex-unique-3-overlay-1-title": "您的數據的擁有權、控制與安全", + "hero-overlay-card-2-p-4": "SimpleX 通過在設計中不使用任何使用者 ID以防止這些攻擊。而且,如果您使用匿名聊天模式,每個聯絡人都會有不同的顯示名稱,避免他們之間有任何共用資料。", + "hero-overlay-card-2-p-3": "即使是使用 Tor v3 服務的最隱私的應用程式,如果您透過相同的設定檔與兩個不同的人交談,他們也可以證明他們連線的是同一個人。", + "hero-overlay-card-1-p-6": "請參閱 <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>SimpleX 白皮書</a>以閱讀更多資訊。", + "hero-overlay-card-3-p-1": "<a href=\"https://www.trailofbits.com/about/\">Trail of Bits</a> 是一家領先的安全和技術顧問公司,客戶包括大型科技公司、政府機構和主要的區塊鏈專案。", + "simplex-network-overlay-card-1-p-1": "<a href='https://en.wikipedia.org/wiki/Peer-to-peer'>點對點</a>通訊協定和應用程式有多種問題,使得它們不如 SimpleX 可靠、分析起來更複雜,而且易受幾種類型的攻擊。", + "simplex-network-overlay-card-1-li-2": "SimpleX 的設計與大多數點對點網路不同,沒有任何類型的全局使用者標識符,即使是臨時標識符。SimpleX 只使用臨時的標識符對,提供更好的匿名性和元資料保護。", + "privacy-matters-overlay-card-1-p-1": "許多大公司利用與您有聯繫的人的資訊來估計您的收入,向您銷售您並不真正需要的產品,以及定價。", + "privacy-matters-overlay-card-1-p-2": "線上零售商知道收入較低的人更有可能進行緊急購物,因此他們可能收取較高的價格或取消折扣。", + "enter-your-email-address": "輸入您的電郵地址", + "get-simplex": "下載SimpleX <a href=\"/downloads\">桌面版應用程式</a>", + "join-us-on-GitHub": "在GitHub上加入我們", + "donate-here-to-help-us": "於此捐助以幫助我們", + "sign-up-to-receive-our-updates": "登記以收取更新", + "join": "加入", + "simplex-unique-card-3-p-1": "SimpleX 以 <strong>可攜式加密數據庫格式</strong> 儲存用戶端裝置上的所有使用者資料;這些資料可傳輸至其他設備。", + "simplex-unique-card-3-p-2": "端對端加密的訊息暫時保留在 SimpleX 中繼伺服器上,收到訊息後會被永久刪除。", + "simplex-unique-card-4-p-2": "您可以<strong>使用您自己的伺服器</strong>運行SimpleX,或使用我們提供的伺服器並仍連線到任何使用者。", + "simplex-unique-overlay-card-3-p-4": "傳送與接收的伺服器流量之間沒有共同的標識符或密文—如果有人觀察,即使 TLS 遭到破壞,也無法輕易確定誰與誰通訊。", + "join-the-REDDIT-community": "加入Reddit社群", + "privacy-matters-overlay-card-3-p-1": "人人都應關心自己通訊的隱私和安全;無害的對話可能會讓您陷入危險,即使您沒有什麼要隱瞞的。", + "privacy-matters-overlay-card-3-p-3": "普通人會因為他們在網路上分享的內容而被捕,即使是透過他們的「匿名」帳號,<a href='https://www.dailymail.co.uk/news/article-11282263/Moment-police-swoop-house-devout-catholic-mother-malicious-online-posts.html' target='_blank'>即使是在民主國家</a>。", + "privacy-matters-overlay-card-2-p-1": "不久前我們觀察到大選被<a href='https://en.wikipedia.org/wiki/Facebook-Cambridge_Analytica_data_scandal' target='_blank'>一家聲譽良好的顧問公司</a>操縱,該公司利用我們的社交圖來扭曲我們對真實世界的看法,並操縱我們的選票。", + "comparison-section-list-point-2": "基於DNS的位址", + "comparison-section-list-point-1": "通常基於電話號碼,有時用戶名", + "simplex-private-card-6-point-1": "許多通訊網路容易受到伺服器或網路供應商的中間人攻擊。", + "privacy-matters-overlay-card-1-p-4": "SimpleX 網路比任何其他方式都能更好地保護您的連線隱私,完全防止您的社交圖表被任何公司或組織獲得。即使使用 SimpleX Chat 應用程式中預設的伺服器,伺服器操作員也不會知道使用者的數量或他們的連線。", + "privacy-matters-overlay-card-2-p-2": "要做到客觀和獨立決策,您需要控制自己的資訊空間。只有當使用無法存取您的社交圖表的私人通訊網路時,您才可能做到這一點。", + "simplex-unique-overlay-card-1-p-1": "與其他通訊網路不同,SimpleX <strong>不為使用者指定標識符</strong>。它不依賴電話號碼、網域地址 (如電子郵件或 XMPP)、用戶名、公鑰或甚至隨機數來識別使用者;SimpleX 伺服器操作員不知道有多少人使用他們的伺服器。", + "privacy-matters-overlay-card-2-p-3": "SimpleX 是第一個在設計上不使用任何使用者標識符的網路,因此比任何已知的其他方案都能更好地保護您的社交圖。", + "simplex-unique-overlay-card-4-p-1": "您可以<strong>使用您自己的伺服器</strong>運行SimpleX,並仍與使用應用程式中預設伺服器的人進行通訊。", + "simplex-unique-card-1-p-2": "與任何其他現有的訊息網路不同,SimpleX 不指定使用者的標識符—<strong>甚至沒有隨機數</strong>。", + "why-simplex-is-unique": "SimpleX為何<span class='gradient-text'>與眾不同</span>", + "learn-more": "查看更多", + "more-info": "更多訊息", + "hide-info": "隱藏訊息", + "contact-hero-header": "您收到了SimpleX聯絡地址", + "simplex-unique-card-4-p-1": "SimpleX 網路是完全去中心化的,獨立於任何加密貨幣或任何其他網路(除Internet)。", + "invitation-hero-header": "您收到了SimpleX一次性連結", + "contact-hero-subheader": "使用手提電話或平板電腦中的SimpleX應用程式掃描二維碼。", + "connect-in-app": "在應用中連結", + "contact-hero-p-2": "尚未下載SimpleX?", + "contact-hero-p-1": "當您檢視此頁面時,此連結中的公鑰和訊息佇列位址不透過網路傳送;它們包含在連結 URL 的雜湊片段中。", + "contact-hero-p-3": "使用以下連結下載應用程式。", + "to-make-a-connection": "建立連結:", + "install-simplex-app": "安裝 SimpleX 應用程式", + "use-this-command": "使用此命令:", + "scan-the-qr-code-with-the-simplex-chat-app-description": "當您檢視此頁面時,此連結中的公鑰和訊息佇列位址不會透過網路傳送 —它們包含在連結 URL 的雜湊片段中。", + "installing-simplex-chat-to-terminal": "安裝SimpleX至終端", + "open-simplex-app": "開啟SimpleX應用程式", + "if-you-already-installed": "如已安裝", + "simplex-chat-for-the-terminal": "用於終端的 SimpleX Chat", + "if-you-already-installed-simplex-chat-for-the-terminal": "如您已經安裝了用於終端的 SimpleX Chat", + "tap-to-close": "輕觸以關閉", + "simplex-network-1-desc": "所有訊息都經由伺服器傳送,既能提供更好的元資料隱私和可靠的異步訊息傳送,又能避免許多", + "simplex-network-section-desc": "SinpleX Chat 結合點對點和互聯網路的優點,提供最佳的隱私性。", + "copy-the-command-below-text": "複製下面的命令並用於聊天:", + "privacy-matters-section-header": "隱私為何 <span class='gradient-text'>重要</span>", + "privacy-matters-section-label": "確保您的通訊應用程式無法竊取您的資料!", + "simplex-network-3-header": "SimpleX網路", + "simplex-network-1-overlay-linktext": "點對點網路的弊病", + "simplex-network-2-desc": "SimpleX 中繼伺服器不儲存使用者個人資料、聯絡人和傳送的訊息,也不連線彼此,也沒有伺服器目錄。", + "simplex-network-2-header": "與互聯網路不同", + "comparison-section-header": "與其他協議之比較", + "protocol-2-text": "XMPP、Matrix", + "comparison-point-1-text": "需要全局身份", + "simplex-network-3-desc": "伺服器提供<span class='text-active-blue'>單向佇列</span>以連接使用者,但伺服器無法看到網路連線圖 —只有使用者能看到。", + "protocol-1-text": "Signal,大平台", + "comparison-point-2-text": "中間人攻擊之可能", + "comparison-point-3-text": "對DNS的依賴", + "comparison-point-4-text": "單一或集中式網路", + "comparison-section-list-point-3": "公鑰或其他某種全局獨一的標識符", + "comparison-section-list-point-5": "不保護使用者的元數據隱私", + "comparison-section-list-point-7": "點對點網路必須有中央權威,否則整個網路都可能受到攻擊", + "comparison-section-list-point-4": "如果運營商的伺服器受到攻擊,用其他應用程式(如 Signal)驗證 SimpleX 安全碼,以減輕風險", + "guide-dropdown-8": "應用程式設定", + "guide-dropdown-6": "音視訊通話", + "docs-dropdown-1": "SimpleX 網路", + "guide": "指南", + "docs-dropdown-9": "下載", + "docs-dropdown-11": "常見問題", + "docs-dropdown-12": "安全性", + "docs-dropdown-7": "翻譯SimpleX", + "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat 和F-Droid.org 儲存庫使用不同的金鑰為安裝包簽名。若要切換,請<a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>匯出聊天資料庫</a>並重新安裝應用程式。" +} diff --git a/website/src/.well-known/apple-app-site-association/index.json b/website/src/.well-known/apple-app-site-association/index.json index 3cd3fdd043..3b513fe61e 100644 --- a/website/src/.well-known/apple-app-site-association/index.json +++ b/website/src/.well-known/apple-app-site-association/index.json @@ -17,6 +17,30 @@ }, { "/": "/invitation" + }, + { + "/": "/a/*" + }, + { + "/": "/a" + }, + { + "/": "/c/*" + }, + { + "/": "/c" + }, + { + "/": "/g/*" + }, + { + "/": "/g" + }, + { + "/": "/i/*" + }, + { + "/": "/i" } ] } diff --git a/website/src/.well-known/org.flathub.VerifiedApps.txt b/website/src/.well-known/org.flathub.VerifiedApps.txt new file mode 100644 index 0000000000..3a9a2d04de --- /dev/null +++ b/website/src/.well-known/org.flathub.VerifiedApps.txt @@ -0,0 +1 @@ +ae8b5b2e-76c9-4a31-a044-bcbda1cdf264 diff --git a/website/src/_data/docs_dropdown.json b/website/src/_data/docs_dropdown.json index a8c6e634b2..f97c2aeff2 100644 --- a/website/src/_data/docs_dropdown.json +++ b/website/src/_data/docs_dropdown.json @@ -5,12 +5,12 @@ "url": "/docs/simplex.html" }, { - "title": "docs-dropdown-8", - "url": "/docs/directory.html" + "title": "docs-dropdown-14", + "url": "/docs/business.html" }, { - "title": "docs-dropdown-2", - "url": "/docs/android.html" + "title": "docs-dropdown-8", + "url": "/docs/directory.html" }, { "title": "docs-dropdown-3", @@ -28,13 +28,21 @@ "title": "docs-dropdown-6", "url": "/docs/webrtc.html" }, - { - "title": "docs-dropdown-7", - "url": "/docs/translations.html" - }, { "title": "docs-dropdown-9", "url": "/downloads/" + }, + { + "title": "docs-dropdown-10", + "url": "/transparency/" + }, + { + "title": "docs-dropdown-12", + "url": "/security/" + }, + { + "title": "docs-dropdown-11", + "url": "/faq/" } ] } \ No newline at end of file diff --git a/website/src/_data/docs_sidebar.json b/website/src/_data/docs_sidebar.json index 857684661b..e370ccc078 100644 --- a/website/src/_data/docs_sidebar.json +++ b/website/src/_data/docs_sidebar.json @@ -18,6 +18,7 @@ "menu": "Reference", "data": [ "SIMPLEX.md", + "BUSINESS.md", "DIRECTORY.md", "ANDROID.md", "CLI.md", @@ -26,7 +27,10 @@ "TRANSLATIONS.md", "WEBRTC.md", "XFTP-SERVER.md", - "DOWNLOADS.md" + "DOWNLOADS.md", + "TRANSPARENCY.md", + "SECURITY.md", + "FAQ.md" ] }, { diff --git a/website/src/_data/glossary.json b/website/src/_data/glossary.json index fd420ccaa6..a16c2b9541 100644 --- a/website/src/_data/glossary.json +++ b/website/src/_data/glossary.json @@ -59,6 +59,10 @@ "term": "Man-in-the-middle attack", "definition": "Man-in-the-middle attack" }, + { + "term": "MITM attack", + "definition": "Man-in-the-middle attack" + }, { "term": "Merkle directed acyclic graph", "definition": "Merkle directed acyclic graph" @@ -67,6 +71,10 @@ "term": "Message padding", "definition": "Message padding" }, + { + "term": "Non-repudiation", + "definition": "Non-repudiation" + }, { "term": "Onion routing", "definition": "Onion routing" @@ -103,6 +111,10 @@ "term": "Recovery from compromise", "definition": "Post-compromise security" }, + { + "term": "Repudiation", + "definition": "Repudiation" + }, { "term": "User identity", "definition": "User identity" diff --git a/website/src/_data/languages.json b/website/src/_data/languages.json index 0ac05063cc..340cc59da9 100644 --- a/website/src/_data/languages.json +++ b/website/src/_data/languages.json @@ -33,12 +33,31 @@ "flag": "/img/flags/es.svg", "enabled": true }, + { + "label": "fi", + "name": "Suomi", + "flag": "/img/flags/fi.svg", + "enabled": true + }, { "label": "fr", "name": "Français", "flag": "/img/flags/fr.svg", "enabled": true }, + { + "label": "he", + "name": "עִברִית", + "flag": "/img/flags/il.svg", + "enabled": true, + "rtl": true + }, + { + "label": "hu", + "name": "Magyar", + "flag": "/img/flags/hu.svg", + "enabled": true + }, { "label": "it", "name": "Italiano", @@ -89,9 +108,9 @@ }, { "label": "ru", - "name": "Russian", + "name": "Русский", "flag": "/img/flags/ru.svg", - "enabled": false + "enabled": true } - ] + ] } \ No newline at end of file diff --git a/website/src/_includes/blog_previews/20221206.html b/website/src/_includes/blog_previews/20221206.html index 0b54f5c32a..55530cd301 100644 --- a/website/src/_includes/blog_previews/20221206.html +++ b/website/src/_includes/blog_previews/20221206.html @@ -3,7 +3,7 @@ <p><a href="https://www.privacyguides.org/real-time-communication/#simplex-chat" target="_blank">Privacy Guides</a> recommendations.</p> <p><a href="https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/" target="_blank">Review by Mike Kuketz</a>.</p> <p><a href="https://www.messenger-matrix.de" target="_blank">The messenger matrix</a>.</p> -<p class="mb-[12px]"><a href="https://supernova.tilde.team/detailed_reviews.html#simplex" target="_blank">Supernova review</a> and <a href="https://supernova.tilde.team/messengers.html" target="_blank">messenger ratings</a>.</p> +<p class="mb-[12px]"><a href="https://supernovas.space/detailed_reviews.html#simplex" target="_blank">Supernova review</a> and <a href="https://supernovas.space/messengers.html" target="_blank">messenger ratings</a>.</p> <p>v4.3 is released:</p> diff --git a/website/src/_includes/blog_previews/20240124.html b/website/src/_includes/blog_previews/20240124.html new file mode 100644 index 0000000000..799e75426c --- /dev/null +++ b/website/src/_includes/blog_previews/20240124.html @@ -0,0 +1,20 @@ +<p><strong>SimpleX Chat infrastructure on Linode:</strong></p> + +<ul class="mb-[12px]"> + <li>free infrastructure.</li> + <li>SimpleX servers in Linode Marketplace.</li> + <li>high capacity messaging servers.</li> +</ul> + +<p><strong>v5.5 is released:</strong></p> + +<ul class="mb-[12px]"> + <li>private notes.</li> + <li>group history</li> + <li>simpler UX to connect to other people</li> + <li>message delivery, battery usage and other improvements</li> +</ul> + +<p>Also, the app interface is now available in Hungarian and Turkish - thanks to our users.</p> + +<p>SimpleX Chat Android and desktop apps are now available in 20 languages!</p> \ No newline at end of file diff --git a/website/src/_includes/blog_previews/20240314.html b/website/src/_includes/blog_previews/20240314.html new file mode 100644 index 0000000000..8a9ac949a7 --- /dev/null +++ b/website/src/_includes/blog_previews/20240314.html @@ -0,0 +1,12 @@ +<p class="mb-[12px]">This is a major upgrade for SimpleX Chat messaging protocols!</p> + +<p class="mb-[12px]">This post also covers various aspects of end-to-end encryption:</p> + +<ul> + <li>Why do we need end-to-end encryption?</li> + <li>Why encryption is even allowed?</li> + <li>End-to-end encryption security: attacks and defence.</li> + <li>How secure is encryption in different messengers?</li> + <li>When can you start using quantum resistant chats?</li> + <li>Next for post-quantum crypto - all direct chats, small groups and security audit</li> +</ul> diff --git a/website/src/_includes/blog_previews/20240323.html b/website/src/_includes/blog_previews/20240323.html new file mode 100644 index 0000000000..7fe4c78179 --- /dev/null +++ b/website/src/_includes/blog_previews/20240323.html @@ -0,0 +1,16 @@ +<p><strong>SimpleX network: deliver real privacy via a profitable business and non-profit protocol governance:</strong></p> + +<ul class="mb-[12px]"> + <li>community and business interests are aligned.</li> + <li>the journey to the decentralized non-profit protocol governance.</li> + <li>welcome, Esra'a!</li> +</ul> + +<p><strong>v5.6 is released:</strong></p> + +<ul class="mb-[12px]"> + <li>quantum resistant end-to-end encryption (BETA) - enable it for the new contacts.</li> + <li>use the app during the audio and video calls.</li> + <li>migrate all app data to another device via QR code.</li> + <li>blocking members and many other improvements.</li> +</ul> diff --git a/website/src/_includes/blog_previews/20240404.html b/website/src/_includes/blog_previews/20240404.html new file mode 100644 index 0000000000..b2407b3ab2 --- /dev/null +++ b/website/src/_includes/blog_previews/20240404.html @@ -0,0 +1,8 @@ +<em>By Esra'a al Shafei</em> + +<p>Transitioning from a lifelong career dedicated to nonprofits, +including Board roles at organizations like the Wikimedia Foundation, Access Now and Tor, +my decision to join SimpleX Chat may come as a surprise to some. +But, as I step into this new chapter, I want to share the insights and convictions +that have guided me here, shedding light on what I think sets SimpleX Chat apart +and why this move feels like an essential learning opportunity.</p> diff --git a/website/src/_includes/blog_previews/20240416.html b/website/src/_includes/blog_previews/20240416.html new file mode 100644 index 0000000000..6c6edfb6c1 --- /dev/null +++ b/website/src/_includes/blog_previews/20240416.html @@ -0,0 +1,5 @@ +<em>By Esra'a al Shafei</em> + +<p>It's important not to be complacent with the current standards of messaging, + where metadata aggregation is still normalized in apps falsely and dangerously marketed as "private". + This is a post exploring the fundamental differences between privacy and security.</p> \ No newline at end of file diff --git a/website/src/_includes/blog_previews/20240426.html b/website/src/_includes/blog_previews/20240426.html new file mode 100644 index 0000000000..648bcc0744 --- /dev/null +++ b/website/src/_includes/blog_previews/20240426.html @@ -0,0 +1,16 @@ +<p><strong>Legally binding transparency</strong></p> + +<ul class="mb-[12px]"> + <li>We published Transparency Reports, Security Policy, and Frequently Asked Questions, and updated Privacy Policy.</li> + <li>We are committed to have full consistency between marketing promises and legally binding documents.</li> +</ul> + +<p><strong>v5.7 is released:</strong></p> + +<ul class="mb-[12px]"> + <li>quantum resistant end-to-end encryption with all contacts.</li> + <li>forward and save messages without revealing the source.</li> + <li>in-call sounds and switching sound sources.</li> + <li>better network connection management.</li> + <li>customizable profile images.</li> +</ul> diff --git a/website/src/_includes/blog_previews/20240516.html b/website/src/_includes/blog_previews/20240516.html new file mode 100644 index 0000000000..0d434eac36 --- /dev/null +++ b/website/src/_includes/blog_previews/20240516.html @@ -0,0 +1,17 @@ +<p class="mb-[12px]">When it comes to open source privacy tools, the status quo often dictates the limitations of + existing protocols and + structures. However, these norms need to be challenged to radically shift how we approach genuinely + private communication. This requires doing some uncomfortable things, like making hard choices as it relates to + funding, alternative decentralization models, doubling down on privacy over convenience, and more. +</p> + +<p class="mb-[12px]">In this post we explain a bit more about why SimpleX operates and makes decisions the way it does: +</p> + +<ul class="mb-[12px]"> + <li>No user accounts.</li> + <li>Privacy over convenience.</li> + <li>Network decentralization.</li> + <li>Funding and profitability.</li> + <li>Company jurisdiction.</li> +</ul> \ No newline at end of file diff --git a/website/src/_includes/blog_previews/20240601.html b/website/src/_includes/blog_previews/20240601.html new file mode 100644 index 0000000000..5e0ca2de49 --- /dev/null +++ b/website/src/_includes/blog_previews/20240601.html @@ -0,0 +1,2 @@ +<p>As lawmakers grapple with the serious issue of child exploitation online, + some proposed solutions would fuel the very problem they aim to solve.</p> \ No newline at end of file diff --git a/website/src/_includes/blog_previews/20240604.html b/website/src/_includes/blog_previews/20240604.html new file mode 100644 index 0000000000..50ae43161d --- /dev/null +++ b/website/src/_includes/blog_previews/20240604.html @@ -0,0 +1,14 @@ +<p><strong>v5.8 is released:</strong></p> + +<ul class="mb-[12px]"> + <li>private message routing.</li> + <li>server transparency.</li> + <li>protect IP address when downloading files & media.</li> + <li>chat themes* for better conversation privacy.</li> + <li>group improvements - reduced traffic and additional preferences.</li> + <li>improved networking, message and file delivery.</li> +</ul> + +<p>Also, we added Persian interface language*, thanks to our users and Weblate.</p> + +<p>* Android and desktop apps only.</p> \ No newline at end of file diff --git a/website/src/_includes/blog_previews/20240704.html b/website/src/_includes/blog_previews/20240704.html new file mode 100644 index 0000000000..581ed20720 --- /dev/null +++ b/website/src/_includes/blog_previews/20240704.html @@ -0,0 +1,3 @@ +<p>It's time we shift the focus: privacy should be a non-negotiable duty of technology providers, not just a + right users must fight to protect, and not something that users can be asked to consent away as a + condition of access to a service.</p> \ No newline at end of file diff --git a/website/src/_includes/blog_previews/20240814.html b/website/src/_includes/blog_previews/20240814.html new file mode 100644 index 0000000000..56d9711098 --- /dev/null +++ b/website/src/_includes/blog_previews/20240814.html @@ -0,0 +1,18 @@ +<p><strong>SimpleX Chat vision and funding 2.0:</strong> past, present, future.</p> + +<p class="mb-[12px]">Announcing the investment from Jack Dorsey and Asymmetric.</p> + +<p><strong>v6.0 is released:</strong></p> + +<ul class="mb-[12px]"> + <li>Private message routing - enabled by default</li> + <li> + <p>New chat experience:</p> + <ul> + <li>connect to your friends faster.</li> + <li>new reachable interface.</li> + <li>and much more!</li> + </ul> + </li> + <li>Improved networking and battery usage</li> +</ul> \ No newline at end of file diff --git a/website/src/_includes/blog_previews/20241014.html b/website/src/_includes/blog_previews/20241014.html new file mode 100644 index 0000000000..2683910e73 --- /dev/null +++ b/website/src/_includes/blog_previews/20241014.html @@ -0,0 +1,12 @@ +<p><strong>New security audit!</strong></p> + +<p class="mb-[12px]">Trail of Bits reviewed the cryptographic design of protocols used in SimpleX network and apps. +</p> + +<p><strong>v6.1 is released:</strong></p> + +<ul> + <li>Better calls: switch audio and video during the call.</li> + <li>Better iOS notifications: improved delivery, reduced traffic usage.</li> + <li>Better user experience: switch chat profiles, customizable message shapes, forward up to 20 messages.</li> +</ul> \ No newline at end of file diff --git a/website/src/_includes/blog_previews/20241016.html b/website/src/_includes/blog_previews/20241016.html new file mode 100644 index 0000000000..d7bd7f5483 --- /dev/null +++ b/website/src/_includes/blog_previews/20241016.html @@ -0,0 +1,4 @@ +<p>The Wired article by David Gilbert focusing on neo-Nazis moving to SimpleX Chat following the Telegram's changes in + privacy policy is biased and misleading. By cherry-picking information from the report by the Institute for + Strategic Dialogue (ISD), Wired fails to mention that SimpleX network design prioritizes privacy in order to protect + human rights defenders, journalists, and everyday users who value their privacy.</p> \ No newline at end of file diff --git a/website/src/_includes/blog_previews/20241125.html b/website/src/_includes/blog_previews/20241125.html new file mode 100644 index 0000000000..8e71807c0b --- /dev/null +++ b/website/src/_includes/blog_previews/20241125.html @@ -0,0 +1,7 @@ +<ul> + <li><strong>Welcome, Flux</strong> — the new servers in <strong>v6.2-beta.1!</strong></li> + <li>What's the problem?</li> + <li>Using two operators improves connection privacy.</li> + <li>SimpleX decentralization compared with Matrix, Session and Tor.</li> + <li>What's next for SimpleX decentralization?</li> +</ul> \ No newline at end of file diff --git a/website/src/_includes/blog_previews/20241210.html b/website/src/_includes/blog_previews/20241210.html new file mode 100644 index 0000000000..48962be17a --- /dev/null +++ b/website/src/_includes/blog_previews/20241210.html @@ -0,0 +1,8 @@ +<p><strong>v6.2 is released:</strong></p> + +<ul> + <li>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app to improve metadata privacy in SimpleX network.</li> + <li>Business chats for better privacy and support of your customers.</li> + <li>Better user experience: open on the first unread, jump to quoted messages, see who reacted.</li> + <li>Improving notifications in iOS app.</li> +</ul> diff --git a/website/src/_includes/blog_previews/20250308.html b/website/src/_includes/blog_previews/20250308.html new file mode 100644 index 0000000000..ab55ecdbca --- /dev/null +++ b/website/src/_includes/blog_previews/20250308.html @@ -0,0 +1,12 @@ +<p><strong>v6.3 is released:</strong></p> + +<ul class="mb-[12px]"> + <li>Preventing spam and abuse in public groups.</li> + <li>Group improvements: mention other members and improved performance.</li> + <li>Better chat navigation: organize chats into lists and jump to found and forwarded messages.</li> + <li>Privacy and security improvements: chat retention period and private media file names.</li> +</ul> + +<p>Also, we added Catalan interface language, thanks to our users and Weblate.</p> + +<p>The last but not the least - server builds are now reproducible!</p> diff --git a/website/src/_includes/contact_page.html b/website/src/_includes/contact_page.html index 6beb148f8d..b5f7442a75 100644 --- a/website/src/_includes/contact_page.html +++ b/website/src/_includes/contact_page.html @@ -30,8 +30,12 @@ <div class="absolute mt-[-100px]"> <img class="" src="/img/new/contact_page_mobile.png" alt=""> </div> + + <noscript class="z-10 flex flex-col items-center pt-[40px] ml-[-15px]"> + <p class="text-2xl font-medium text-center max-w-[234px] mb-32">{{ "please-enable-javascript" | i18n({}, lang ) | safe }}</p> + </noscript> - <div class="z-10 flex flex-col items-center pt-[40px] ml-[-15px]"> + <div class="z-10 flex flex-col items-center pt-[40px] ml-[-15px] d-none-if-js-disabled"> <p class="text-base font-medium text-center max-w-[234px]">{{ "scan-qr-code-from-mobile-app" | i18n({}, lang ) | safe }}</p> <canvas class="conn_req_uri_qrcode"></canvas> </div> @@ -61,7 +65,11 @@ </div> <div class="flex flex-col justify-center items-center w-full max-w-[468px] h-[131px] rounded-[30px] border-[1px] border-[#A8B0B4] dark:border-white border-opacity-60 mb-6 relative"> - <p class="text-xl font-medium text-grey-black dark:text-white mb-4">{{ "connect-in-app" | i18n({}, lang ) | safe }}</p> + <p class="text-xl font-medium text-grey-black dark:text-white mb-4 d-none-if-js-disabled">{{ "connect-in-app" | i18n({}, lang ) | safe }}</p> + <noscript> + <p class="text-xl font-medium text-grey-black dark:text-white mb-4">{{ "please-use-link-in-mobile-app" | i18n({}, lang ) | safe }}</p> + </noscript> + <a id="mobile_conn_req_uri" class="bg-[#0053D0] text-white py-3 px-8 rounded-[34px] h-[44px] text-[16px] leading-[19px] tracking-[0.02em]">{{ "open-simplex-app" | i18n({}, lang ) | safe }}</a> <div class="absolute bg-[#0197FF] h-[44px] w-[44px] rounded-full flex items-center justify-center top-0 left-0 translate-x-[-30%] translate-y-[-30%]"> @@ -69,7 +77,7 @@ </div> </div> - <div class="flex flex-col justify-center items-center w-full max-w-[468px] h-[131px] rounded-[30px] border-[1px] border-[#A8B0B4] dark:border-white border-opacity-60 relative"> + <div class="flex flex-col justify-center items-center w-full max-w-[468px] h-[131px] rounded-[30px] border-[1px] border-[#A8B0B4] dark:border-white border-opacity-60 relative d-none-if-js-disabled"> <p class="text-xl font-medium text-grey-black dark:text-white max-w-[230px] text-center">{{ "tap-the-connect-button-in-the-app" | i18n({}, lang ) | safe }}</p> <div class="absolute bg-[#0197FF] h-[44px] w-[44px] rounded-full flex items-center justify-center top-0 left-0 translate-x-[-30%] translate-y-[-30%]"> @@ -81,7 +89,7 @@ </section> -<section class="hidden md:block bg-secondary-bg-light dark:bg-secondary-bg-dark py-[20px]"> +<section class="hidden md:block bg-secondary-bg-light dark:bg-secondary-bg-dark py-[20px] d-none-if-js-disabled"> <div class="container px-5"> <div class="text-grey-black dark:text-white"> @@ -164,3 +172,7 @@ {# join simplex #} {% include "sections/join_simplex.html" %} + +<script> + document.querySelectorAll('.d-none-if-js-disabled').forEach(el => el.classList.remove('d-none-if-js-disabled')); +</script> diff --git a/website/src/_includes/footer.html b/website/src/_includes/footer.html index 1c23806760..0cc1b16f8d 100644 --- a/website/src/_includes/footer.html +++ b/website/src/_includes/footer.html @@ -20,11 +20,9 @@ {{ "simplex-chat-protocol" | i18n({}, lang ) | safe }} <svg class="float-right" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg> </a> - <a href="https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md" - target="_blank" + <a href="/privacy" class="text-grey-black dark:text-white text-[14px] font-medium leading-[28px] tracking-[0.01em] mb-3 flex items-center gap-1"> {{ "terms-and-privacy-policy" | i18n({}, lang ) | safe }} - <svg class="float-right" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg> </a> <a href="https://github.com/simplex-chat/simplex-chat#help-us-with-donations" target="_blank" diff --git a/website/src/_includes/hero.html b/website/src/_includes/hero.html index c0e2b3f30b..6ebfb883ef 100644 --- a/website/src/_includes/hero.html +++ b/website/src/_includes/hero.html @@ -31,6 +31,10 @@ <img class="h-[40px] w-auto block dark:hidden" src="/img/privacy-guides-light.png" alt="Privacy Guides"> <img class="h-[40px] w-auto hidden dark:block" src="/img/privacy-guides-dark.png" alt="Privacy Guides"> </a> + <a class="block rounded overflow-hidden" title="Whonix messenger recommendations" href="https://www.whonix.org/wiki/Chat#Recommendation" target="_blank"> + <img class="h-[24px] mb-[8px] w-auto block dark:hidden" src="/img/whonix-light.png" alt="Whonix"> + <img class="h-[24px] mb-[8px] w-auto hidden dark:block" src="/img/whonix-dark.png" alt="Whonix"> + </a> <a class="block rounded overflow-hidden" title="publication" href="https://www.heise.de/suche/?q=simplex+chat&sort_by=date&rm=search" target="_blank"> <img class="h-[40px] w-auto block dark:hidden" src="/img/heise-light.png" alt="Heise Online"> <img class="h-[40px] w-auto hidden dark:block" src="/img/heise-dark.png" alt="Heise Online"> @@ -43,7 +47,7 @@ <img class="h-[40px] w-auto" src="/img/optout.jpg" alt="Opt Out Podcast"> </a> </div> - + <div> <p class="text-black dark:text-white hidden md:block text-center xl:text-left xl:rtl:text-right text-[16px] leading-[26px] mb-[11px] md:mt-6">{{ "get-simplex" | i18n({}, lang ) | safe }}</p> <div class="socials flex items-center justify-center xl:justify-start gap-4 flex-wrap mt-[30px]"> @@ -66,7 +70,7 @@ <video class="absolute rounded-lg top-10 w-[235px] ml-[-6px] mt-1" controls> <source src="/video/connect.mp4" type="video/mp4"> </video> - + {# <div class="absolute flex flex-col items-center gap-3"> <img src="/img/new/play-btn.svg" alt="" /> <p class="text-center text-[16px] leading-[24px] tracking-[0.04em] text-white">PLAY</p> diff --git a/website/src/_includes/layouts/privacy.html b/website/src/_includes/layouts/privacy.html new file mode 100644 index 0000000000..7527852694 --- /dev/null +++ b/website/src/_includes/layouts/privacy.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html lang="{{ page.url | getlang }}" + {% for language in languages.languages %} + {% if language.label == page.url | getlang %} + dir="{{ "rtl" if language.rtl else "ltr" }}" + {% endif %} + {% endfor %}> + + <head> + <meta charset="UTF-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>SimpleX Privacy Policy + + + + + + {% if image %} + + {% else %} + + {% endif %} + + + + + + + + + + {% include "navbar.html" %} + +
+
{{ content | safe }}
+
+ + {% include "footer.html" %} + + + diff --git a/website/src/_includes/navbar.html b/website/src/_includes/navbar.html index 55836bd576..6e69c559b0 100644 --- a/website/src/_includes/navbar.html +++ b/website/src/_includes/navbar.html @@ -13,7 +13,7 @@ -